|
|
@@ -12,6 +12,8 @@ import (
|
|
|
"strings"
|
|
|
"testing"
|
|
|
|
|
|
+ "gorm.io/gorm"
|
|
|
+
|
|
|
"yuansiwei/unified-backend/internal/academic/model"
|
|
|
)
|
|
|
|
|
|
@@ -315,7 +317,11 @@ func TestTransferTargetAccountCreatedWhenMissing(t *testing.T) {
|
|
|
}
|
|
|
|
|
|
// TestTransferRejectsInstitutionMismatch R4-bis ③:入参 toInstitutionId 与校区归属机构矛盾 → **3140**,
|
|
|
-// 且**不建户、不写流水、不动任何账户**(dump 会按错机构建僵尸户 → 同校区双账户 → 差值冻结破)。
|
|
|
+// 且**表零变化**:两张账户表行数都不变、不写流水、源校区与全局的金额逐列不变。
|
|
|
+//
|
|
|
+// 为什么必须断言"表零变化"而不是"目标校区 count=0":dump 按入参机构建户会留下**机构错配的僵尸账户**,
|
|
|
+// 而账户只增不删、UK 又让它按正确机构永远查不到 → 不可自愈。只断言"该 campus 没有行"会漏掉
|
|
|
+// "建到了错机构下"这一种(campus 相同、institution 不同)。
|
|
|
func TestTransferRejectsInstitutionMismatch(t *testing.T) {
|
|
|
db := newFundServiceDB(t)
|
|
|
svc := NewFundService(db, fundSuperScope(), nil)
|
|
|
@@ -328,7 +334,11 @@ func TestTransferRejectsInstitutionMismatch(t *testing.T) {
|
|
|
Money: mustAmount(t, "100.00"), RequestID: "REQ-R4BIS-SEED"}); err != nil {
|
|
|
t.Fatalf("准备源校区余额失败: %v", err)
|
|
|
}
|
|
|
+ srcBefore := readSchool(t, db, user, 1, 10)
|
|
|
+ globalBefore := readGlobal(t, db, user)
|
|
|
logsBefore := len(balanceLogs(t, db, user))
|
|
|
+ globalRowsBefore := countRows(t, db, &model.UserExpense{}, user)
|
|
|
+ schoolRowsBefore := countRows(t, db, &model.UserExpenseSchool{}, user)
|
|
|
|
|
|
// 入参声明机构 2,但校区 20 属于机构 1 → 3140
|
|
|
_, err := svc.Transfer(ctx, TransferInput{UserID: user,
|
|
|
@@ -336,20 +346,49 @@ func TestTransferRejectsInstitutionMismatch(t *testing.T) {
|
|
|
Money: mustAmount(t, "30.00"), RequestID: "REQ-R4BIS-BAD"})
|
|
|
requireAppCode(t, err, 3140)
|
|
|
|
|
|
- var cnt int64
|
|
|
- if e := db.Model(&model.UserExpenseSchool{}).
|
|
|
- Where("user_id = ? AND campus_id = ?", user, 20).Count(&cnt).Error; e != nil {
|
|
|
+ // ① 两张账户表行数都不变(既没建错机构的户,也没多建全局户)
|
|
|
+ if got := countRows(t, db, &model.UserExpenseSchool{}, user); got != schoolRowsBefore {
|
|
|
+ t.Fatalf("R4-bis 拒绝时校区账户行数必须不变:前 %d 后 %d", schoolRowsBefore, got)
|
|
|
+ }
|
|
|
+ if got := countRows(t, db, &model.UserExpense{}, user); got != globalRowsBefore {
|
|
|
+ t.Fatalf("R4-bis 拒绝时全局账户行数必须不变:前 %d 后 %d", globalRowsBefore, got)
|
|
|
+ }
|
|
|
+ // ② 目标校区在**任何机构**下都不得有行(防"建到错机构"的僵尸户)
|
|
|
+ var anywhere int64
|
|
|
+ if e := db.Model(&model.UserExpenseSchool{}).Where("user_id = ? AND campus_id = ?", user, 20).
|
|
|
+ Count(&anywhere).Error; e != nil {
|
|
|
t.Fatalf("统计目标校区账户失败: %v", e)
|
|
|
}
|
|
|
- if cnt != 0 {
|
|
|
- t.Fatalf("R4-bis 拒绝时不得建户,实得 %d 行(机构错配的僵尸账户)", cnt)
|
|
|
+ if anywhere != 0 {
|
|
|
+ t.Fatalf("R4-bis 拒绝时不得建户(任意机构下),实得 %d 行", anywhere)
|
|
|
}
|
|
|
+ // ③ 不写流水
|
|
|
if got := len(balanceLogs(t, db, user)); got != logsBefore {
|
|
|
t.Fatalf("R4-bis 拒绝时不得写流水:前 %d 后 %d", logsBefore, got)
|
|
|
}
|
|
|
- if got := cents(t, readSchool(t, db, user, 1, 10).TotalIncome); got != 10000 {
|
|
|
- t.Fatalf("R4-bis 拒绝时源校区不得变动(应 10000),实得 %d", got)
|
|
|
+ // ④ 源校区与全局金额逐列不变
|
|
|
+ srcAfter := readSchool(t, db, user, 1, 10)
|
|
|
+ if cents(t, srcAfter.Balance) != cents(t, srcBefore.Balance) ||
|
|
|
+ cents(t, srcAfter.TotalIncome) != cents(t, srcBefore.TotalIncome) ||
|
|
|
+ cents(t, srcAfter.TotalExpenditure) != cents(t, srcBefore.TotalExpenditure) {
|
|
|
+ t.Fatalf("R4-bis 拒绝时源校区不得变动:前 %+v 后 %+v", srcBefore, srcAfter)
|
|
|
+ }
|
|
|
+ globalAfter := readGlobal(t, db, user)
|
|
|
+ if cents(t, globalAfter.Balance) != cents(t, globalBefore.Balance) ||
|
|
|
+ cents(t, globalAfter.TotalIncome) != cents(t, globalBefore.TotalIncome) ||
|
|
|
+ cents(t, globalAfter.TotalExpenditure) != cents(t, globalBefore.TotalExpenditure) {
|
|
|
+ t.Fatalf("R4-bis 拒绝时全局账户不得变动:前 %+v 后 %+v", globalBefore, globalAfter)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// countRows 统计某 user 在给定表的行数("零副作用"断言用)。
|
|
|
+func countRows(t *testing.T, db *gorm.DB, table any, userID string) int64 {
|
|
|
+ t.Helper()
|
|
|
+ var n int64
|
|
|
+ if err := db.Model(table).Where("user_id = ?", userID).Count(&n).Error; err != nil {
|
|
|
+ t.Fatalf("统计 %T 行数失败: %v", table, err)
|
|
|
}
|
|
|
+ return n
|
|
|
}
|
|
|
|
|
|
// TestTransferTargetCampusUnknown R4-bis ②:转入校区在 t_campus 里查不到 / 未归属机构 → **3104**。
|