|
|
@@ -9,48 +9,83 @@ import (
|
|
|
|
|
|
// 本文件是比对与编排。
|
|
|
|
|
|
-// DefaultCases 返回三支 case:正常 / 幂等 / R15(余额不足)。
|
|
|
+// DefaultCases 返回批 A 的 6 个 case(3 个 SP × 正常 + 幂等)+ 3 个消费回归 case。
|
|
|
//
|
|
|
-// 用户与机构校区取自真实数据(userid=17735 / 63317 / 5):
|
|
|
-// 该用户全局与校区余额都是 100.00、且四个金额列在 legacy 侧都是 2 位小数,
|
|
|
-// 因此**两侧基线天然逐分相等**(不需要人为造基线)—— 这正是 case 有效性的前提。
|
|
|
-func DefaultCases(uid string) []Case {
|
|
|
+// 用户选择:三个 SP 各用一个**不同**的真实用户,且这些用户的
|
|
|
+// balance / spend / total_income / total_expenditure 在 legacy 侧都是 2 位小数,
|
|
|
+// 因此两侧基线天然逐分相等,不需要人为造基线。
|
|
|
+//
|
|
|
+// 17735 → 机构 63317 / 校区 5,全局与校区余额都是 100.00
|
|
|
+// 31477 → 机构 63317 / 校区 5,余额 1350.00
|
|
|
+// 40575 → 机构 63317 / 校区 4,余额 1920.00
|
|
|
+func DefaultCases(_ string) []Case {
|
|
|
+ var out []Case
|
|
|
+
|
|
|
+ // ---- 批 A ----
|
|
|
+ out = append(out,
|
|
|
+ Case{
|
|
|
+ Name: "A1 充值 sp_user_expense_recharge(200.00,type=100)",
|
|
|
+ Op: OpRecharge, LegacyUserID: 17735, InstitutionID: 63317, CampusID: 5,
|
|
|
+ AmountCents: 20000, Type: 100, RequestID: "ac15-a1", RunTimes: 1,
|
|
|
+ ExpectTargetSuccess: true, ExpectLegacySuccess: true,
|
|
|
+ Note: "balance+200 / total_income+200,**全局与校区两张表都写**;默认备注\"余额充值\"",
|
|
|
+ },
|
|
|
+ Case{
|
|
|
+ Name: "A2 充值·幂等(同一 requestId 跑 2 次)",
|
|
|
+ Op: OpRecharge, LegacyUserID: 31477, InstitutionID: 63317, CampusID: 5,
|
|
|
+ AmountCents: 20000, Type: 100, RequestID: "ac15-a2", RunTimes: 2,
|
|
|
+ ExpectTargetSuccess: true, ExpectLegacySuccess: true,
|
|
|
+ Note: "target 幂等只入账 1 次;legacy SP 无幂等键,会重复入账 2 次(设计内 Go 增强)",
|
|
|
+ },
|
|
|
+ Case{
|
|
|
+ Name: "A3 校区充值 sp_user_expense_school_recharge(150.00)",
|
|
|
+ Op: OpSchoolRecharge, LegacyUserID: 40575, InstitutionID: 63317, CampusID: 4,
|
|
|
+ AmountCents: 15000, Type: 0, RequestID: "ac15-a3", RunTimes: 1,
|
|
|
+ ExpectTargetSuccess: true, ExpectLegacySuccess: true,
|
|
|
+ Note: "balance+150 / total_income+150,双表;**type 由 SP 固定 100**;默认备注\"充值缴费\"",
|
|
|
+ },
|
|
|
+ Case{
|
|
|
+ Name: "A4 校区充值·幂等(同一 requestId 跑 2 次)",
|
|
|
+ Op: OpSchoolRecharge, LegacyUserID: 17735, InstitutionID: 63317, CampusID: 5,
|
|
|
+ AmountCents: 15000, Type: 0, RequestID: "ac15-a4", RunTimes: 2,
|
|
|
+ ExpectTargetSuccess: true, ExpectLegacySuccess: true,
|
|
|
+ Note: "同上,验证 uk_request_type 在充值路径上的幂等",
|
|
|
+ },
|
|
|
+ Case{
|
|
|
+ Name: "A5 退费 sp_user_expense_school_refund(100.00,type=207)",
|
|
|
+ Op: OpRefund, LegacyUserID: 31477, InstitutionID: 63317, CampusID: 5,
|
|
|
+ AmountCents: 10000, Type: 207, RequestID: "ac15-a5", RunTimes: 1,
|
|
|
+ ExpectTargetSuccess: true, ExpectLegacySuccess: true,
|
|
|
+ Note: "**方向与消费相反**:balance+100 / total_expenditure−100(双表);默认备注\"报课的费用退回\"",
|
|
|
+ },
|
|
|
+ Case{
|
|
|
+ Name: "A6 退费·幂等(同一 requestId 跑 2 次)",
|
|
|
+ Op: OpRefund, LegacyUserID: 40575, InstitutionID: 63317, CampusID: 4,
|
|
|
+ AmountCents: 10000, Type: 207, RequestID: "ac15-a6", RunTimes: 2,
|
|
|
+ ExpectTargetSuccess: true, ExpectLegacySuccess: true,
|
|
|
+ Note: "退费路径的幂等",
|
|
|
+ },
|
|
|
+ )
|
|
|
+
|
|
|
+ // ---- 消费(第二批已做,留作回归)----
|
|
|
base := Case{
|
|
|
- LegacyUserID: 17735,
|
|
|
- InstitutionID: 63317,
|
|
|
- CampusID: 5,
|
|
|
- Type: 201,
|
|
|
- Remark: "",
|
|
|
- RunTimes: 1,
|
|
|
- ExpectTargetSuccess: true,
|
|
|
- ExpectLegacySuccess: true,
|
|
|
+ Op: OpExpenditure, LegacyUserID: 17735, InstitutionID: 63317, CampusID: 5,
|
|
|
+ Type: 201, RunTimes: 1, ExpectTargetSuccess: true, ExpectLegacySuccess: true,
|
|
|
}
|
|
|
- _ = uid
|
|
|
-
|
|
|
c1 := base
|
|
|
- c1.Name = "C1 正常消费(type=201,30.00)"
|
|
|
- c1.AmountCents = 3000
|
|
|
- c1.RequestID = "ac15-s2-c1"
|
|
|
- c1.Note = "两侧都应成功,Δbalance 同为 -30.00,各写 1 条流水"
|
|
|
+ c1.Name = "R1 回归·消费(type=201,30.00)"
|
|
|
+ c1.AmountCents, c1.RequestID = 3000, "ac15-s2-c1"
|
|
|
+ c1.Note = "第二批的消费 case,留作回归"
|
|
|
|
|
|
c2 := base
|
|
|
- c2.Name = "C2 幂等(同一 requestId 连跑两次)"
|
|
|
- c2.AmountCents = 3000
|
|
|
- c2.RequestID = "ac15-s2-c2"
|
|
|
- c2.RunTimes = 2
|
|
|
- c2.Note = "target(Go)应 repeated=true 且只入账一次;legacy SP **没有幂等**,会重复入账 —— 这是设计内的 Go 增强"
|
|
|
-
|
|
|
- c3 := base
|
|
|
- c3.Name = "C3 R15 余额不足(余额 10.00,消费 100.00)"
|
|
|
- c3.AmountCents = 10000
|
|
|
- c3.RequestID = "ac15-s2-c3"
|
|
|
- c3.Type = 200 // 历史扣费(同时动 spend,覆盖三分支里最复杂的一支)
|
|
|
- c3.Note = "R15:SP 不校验余额 → 两侧都应成功且扣成 -90.00"
|
|
|
- c3.Baseline = &Baseline{
|
|
|
- GlobalBalance: 1000, GlobalSpend: 0, GlobalIncome: 0, GlobalExpenditure: 0,
|
|
|
- CampusBalance: 1000, CampusSpend: 0, CampusIncome: 0, CampusExpenditure: 0,
|
|
|
- }
|
|
|
- return []Case{c1, c2, c3}
|
|
|
+ c2.Name = "R2 回归·消费 R15 余额不足"
|
|
|
+ c2.AmountCents, c2.RequestID, c2.Type = 10000, "ac15-s2-c3", 200
|
|
|
+ c2.Note = "R15:余额不足两侧都成功且扣成负数"
|
|
|
+ c2.Baseline = &Baseline{
|
|
|
+ GlobalBalance: 1000, CampusBalance: 1000,
|
|
|
+ }
|
|
|
+ out = append(out, c1, c2)
|
|
|
+ return out
|
|
|
}
|
|
|
|
|
|
// RunOne 跑一个 case 的完整双跑闭环。
|
|
|
@@ -152,8 +187,8 @@ func compare(c Case, lg, tg *SideResult) ([]Diff, []Assertion) {
|
|
|
if c.RunTimes >= 2 {
|
|
|
noteRuns = fmt.Sprintf("预期差异:legacy 入账 %d 次(SP 无幂等)/ target 入账 1 次(幂等命中)", runsLegacy)
|
|
|
}
|
|
|
- wantL := expectedDelta(c.Type, c.AmountCents, runsLegacy)
|
|
|
- wantT := expectedDelta(c.Type, c.AmountCents, runsTarget)
|
|
|
+ wantL := expectedDeltaFor(c.Op, c.Type, c.AmountCents, runsLegacy)
|
|
|
+ wantT := expectedDeltaFor(c.Op, c.Type, c.AmountCents, runsTarget)
|
|
|
|
|
|
dGlobal := sub(lg.After.Global, lg.Before.Global)
|
|
|
dCampus := sub(lg.After.Campus, lg.Before.Campus)
|
|
|
@@ -167,11 +202,12 @@ func compare(c Case, lg, tg *SideResult) ([]Diff, []Assertion) {
|
|
|
dCampus.Balance == wantL.Balance && tCampus.Balance == wantT.Balance, "同上")
|
|
|
add("Δspend(全局)", yuan(dGlobal.Spend), yuan(tGlobal.Spend),
|
|
|
dGlobal.Spend == wantL.Spend && tGlobal.Spend == wantT.Spend,
|
|
|
- "type=200 才动 spend;期望 "+yuan(wantL.Spend)+" / "+yuan(wantT.Spend))
|
|
|
+ "只有 type=200 的消费才动 spend;期望 "+yuan(wantL.Spend)+" / "+yuan(wantT.Spend))
|
|
|
add("Δspend(校区)", yuan(dCampus.Spend), yuan(tCampus.Spend),
|
|
|
dCampus.Spend == wantL.Spend && tCampus.Spend == wantT.Spend, "同上")
|
|
|
add("Δtotal_income(全局)", yuan(dGlobal.Income), yuan(tGlobal.Income),
|
|
|
- dGlobal.Income == wantL.Income && tGlobal.Income == wantT.Income, "whitelist 内 type 都不动 total_income")
|
|
|
+ dGlobal.Income == wantL.Income && tGlobal.Income == wantT.Income,
|
|
|
+ "充值才动 total_income;期望 "+yuan(wantL.Income)+" / "+yuan(wantT.Income))
|
|
|
add("Δtotal_income(校区)", yuan(dCampus.Income), yuan(tCampus.Income),
|
|
|
dCampus.Income == wantL.Income && tCampus.Income == wantT.Income, "同上")
|
|
|
add("Δtotal_expenditure(全局)", yuan(dGlobal.Expenditure), yuan(tGlobal.Expenditure),
|
|
|
@@ -207,7 +243,8 @@ func compare(c Case, lg, tg *SideResult) ([]Diff, []Assertion) {
|
|
|
}
|
|
|
}
|
|
|
add("流水 type", firstOr(detailJoin(tDetail), "全部一致"), fmt.Sprint(n)+" 条", sameType, "")
|
|
|
- add("流水 money", firstOr(detailJoin(mDetail), "逐分相等"), fmt.Sprint(n)+" 条", sameMoney, "两侧都为负数(支出方向)")
|
|
|
+ moneyNote := "符号:消费为负、充值与退费为正(照 dump,不修正)"
|
|
|
+ add("流水 money", firstOr(detailJoin(mDetail), "逐分相等"), fmt.Sprint(n)+" 条", sameMoney, moneyNote)
|
|
|
add("流水 logid/log_id", firstOr(detailJoin(lDetail), "全部一致"), fmt.Sprint(n)+" 条", sameLogID,
|
|
|
"legacy logid = inputRequestid;target 同时写 log_id 与 request_id")
|
|
|
|
|
|
@@ -234,6 +271,19 @@ func compare(c Case, lg, tg *SideResult) ([]Diff, []Assertion) {
|
|
|
add("target.balance_after(legacy 无此列)", "N/A",
|
|
|
firstOr(detailJoin(balDetail), "全部 == 调用后全局余额"), balOK, "Go 增强:余额快照")
|
|
|
|
|
|
+ // ④.5 remark(默认备注是 SP 之间唯一的"文案差异",必须逐字比对)
|
|
|
+ remarkOK := true
|
|
|
+ var remarkDetail []string
|
|
|
+ for i := 0; i < n; i++ {
|
|
|
+ if lg.After.Logs[i].Remark != tg.After.Logs[i].Remark {
|
|
|
+ remarkOK = false
|
|
|
+ remarkDetail = append(remarkDetail, fmt.Sprintf("#%d legacy=%q target=%q",
|
|
|
+ i, lg.After.Logs[i].Remark, tg.After.Logs[i].Remark))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ add("流水 remark(默认备注)", firstOr(detailJoin(remarkDetail), "逐字一致"), fmt.Sprint(n)+" 条",
|
|
|
+ remarkOK, "SP1\"余额充值\"/SP2\"充值缴费\"/SP4\"报课的费用退回\"")
|
|
|
+
|
|
|
// ⑤ 返回码 / 成功与否
|
|
|
lgOK := len(lg.Calls) > 0 && lg.Calls[0].Success
|
|
|
tgOK := len(tg.Calls) > 0 && tg.Calls[0].Success
|
|
|
@@ -262,6 +312,23 @@ func compare(c Case, lg, tg *SideResult) ([]Diff, []Assertion) {
|
|
|
true, "legacy=2(重复入账)/ target=1(幂等命中)—— 设计内的 Go 增强,不是缺陷")
|
|
|
}
|
|
|
|
|
|
+ // ⑥.5 双表写入断言(替代"recharge 只写全局"这个**错误前提**)
|
|
|
+ //
|
|
|
+ // dump 事实:SP1 `sp_user_expense_recharge` 与 SP2 `sp_user_expense_school_recharge`
|
|
|
+ // **都** UPDATE 了 t_user_expense_school 和 t_user_expense(两段 UPDATE 都在)。
|
|
|
+ // 所以真正要防的错是"把充值写成只写全局"——那样校区表 Δ=0,这条会立刻红。
|
|
|
+ if c.Op == OpRecharge || c.Op == OpSchoolRecharge {
|
|
|
+ okL := dCampus.Balance == wantL.Balance && dCampus.Income == wantL.Income
|
|
|
+ okT := tCampus.Balance == wantT.Balance && tCampus.Income == wantT.Income
|
|
|
+ asserts = append(asserts, Assertion{
|
|
|
+ Name: "双表写入:校区表 Δbalance / Δtotal_income 必须等于充值额(若被写成\"只写全局\"会红)",
|
|
|
+ Pass: okL && okT,
|
|
|
+ Detail: fmt.Sprintf("校区 Δbalance legacy=%s target=%s(期望 %s / %s);Δincome legacy=%s target=%s(期望 %s / %s)",
|
|
|
+ yuan(dCampus.Balance), yuan(tCampus.Balance), yuan(wantL.Balance), yuan(wantT.Balance),
|
|
|
+ yuan(dCampus.Income), yuan(tCampus.Income), yuan(wantL.Income), yuan(wantT.Income)),
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
// ⑦ R15:两侧余额都必须为负
|
|
|
if c.Baseline != nil {
|
|
|
negOK := lg.After.Global.Balance < 0 && tg.After.Global.Balance < 0 &&
|