|
|
@@ -177,17 +177,36 @@ func DefaultCases(_ string) []Case {
|
|
|
Note: "SP10 第二条守卫 `ISNULL(totalBalance)||totalBalance<=0 → LEAVE`",
|
|
|
},
|
|
|
Case{
|
|
|
- // 这一条**用真实构型,不用 Baseline 造数**:
|
|
|
- // `finance_bk` 里 `schoolid=63317 且 校区余额 > 全局余额 > 0` 有 21 行,
|
|
|
- // 48170 是其中最干净的一个(campus 5 = 0.270,全局 = 0.269,都是 2 位小数)。
|
|
|
- // 造数能造出"实际不会发生的组合",绿了也没意义;真实构型证明这个守卫在真数据上会被触发。
|
|
|
- Name: "C1-5 清零前置校验·校区余额 > 全局余额(真实构型,legacy 静默 vs Go 3120)",
|
|
|
- Op: OpClearBalance, LegacyUserID: 48170, InstitutionID: 63317, CampusID: 5,
|
|
|
- RequestID: "ac15-c1-5", RunTimes: 1, SelfSupport: true,
|
|
|
+ // 这一条测的是**守卫本身**:用 Baseline 造 500/800,两位小数下不等式仍成立,
|
|
|
+ // 所以迁移不会改变判据 —— 两侧都该 LEAVE。
|
|
|
+ Name: "C1-5 清零前置校验·校区余额 > 全局余额(legacy 静默 vs Go 3120)",
|
|
|
+ Op: OpClearBalance, LegacyUserID: 40575, InstitutionID: 63317, CampusID: 4,
|
|
|
+ RequestID: "ac15-c1-5", RunTimes: 1, SelfSupport: false,
|
|
|
+ Baseline: &Baseline{GlobalBalance: 50000, CampusBalance: 80000},
|
|
|
ExpectTargetSuccess: false, ExpectLegacySuccess: true,
|
|
|
ExpectTargetCode: 3120, IntentionalStrict: true,
|
|
|
- Note: "SP10 第三条守卫 `campusBalance>totalBalance → LEAVE`。真实数据:校区 0.270 > 全局 0.269(差 1 分)," +
|
|
|
- "正是「余额 ≠ 流水累计」那 6.6% 用户的一种表现",
|
|
|
+ Note: "SP10 第三条守卫 `campusBalance>totalBalance → LEAVE`(数据异常,拒绝对负余额清零)。" +
|
|
|
+ "**测守卫本身**:500/800 在两位小数下不等式仍成立。真实 3 位构型(守卫被收敛抹平)见 C1-5b",
|
|
|
+ },
|
|
|
+ Case{
|
|
|
+ // 这一条测的是**判据输入被小数收敛抹平**:真实构型,不做任何造数。
|
|
|
+ //
|
|
|
+ // 65496:校区 balance=1565.688 / 全局 balance=1565.687(只差 0.001)
|
|
|
+ // legacy 按 3 位比:1565.688 > 1565.687 成立 ⇒ LEAVE(零写入)
|
|
|
+ // target 只有 2 位:1565.69 > 1565.69 不成立 ⇒ 放行清零
|
|
|
+ // ⇒ 归类 PRECISION-CONVERGENCE / PREDICATE-COLLAPSE(分支不同,不是"差几分钱")。
|
|
|
+ //
|
|
|
+ // 选 65496 而不是 48170:① 1565 元级、报告好读;② 它的**可失效报名 = 0**
|
|
|
+ // (唯一那条是 activity 72 的 type=4 老带新,天然不参与失效),
|
|
|
+ // 将来谁给这条 case 补 `Activity:` 种子也不会突然变成"两侧失效范围不同"。
|
|
|
+ Name: "C1-5b 清零前置校验·校区余额 > 全局余额(真实 3 位构型,2 位收敛后守卫消失)",
|
|
|
+ Op: OpClearBalance, LegacyUserID: 65496, InstitutionID: 63317, CampusID: 5,
|
|
|
+ RequestID: "ac15-c1-5b", RunTimes: 1, SelfSupport: true,
|
|
|
+ SpecialCase: SpecialPredicateCollapse,
|
|
|
+ ExpectTargetSuccess: true, ExpectLegacySuccess: true,
|
|
|
+ Note: "PRECISION-CONVERGENCE / PREDICATE-COLLAPSE:校区 1565.688 > 全局 1565.687," +
|
|
|
+ "两位收敛后都是 1565.69 ⇒ legacy 按 3 位判据 LEAVE、target 按 2 位放行清零。两侧**本来就不同**," +
|
|
|
+ "本 case 只把差异钉死;报告 detail 会打 3 位原值,否则两侧都显示 1565.69、红得不可读",
|
|
|
},
|
|
|
)
|
|
|
|
|
|
@@ -354,6 +373,14 @@ func RunOne(ctx context.Context, cfg Config, c Case) (*CaseResult, error) {
|
|
|
}
|
|
|
res.Target = tg
|
|
|
|
|
|
+ // 3 位原值:**只在** SpecialPredicateCollapse 上读,不进共享快照
|
|
|
+ // (进了就会改变另外 24 条的读数口径,而其它 case 不需要第 3 位)。
|
|
|
+ if c.SpecialCase == SpecialPredicateCollapse {
|
|
|
+ if err := readRaw3dp(ctx, legacyDB, c, lg); err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// ---- 比对 ----
|
|
|
res.Diffs, res.Assertions = compare(c, lg, tg)
|
|
|
|
|
|
@@ -442,6 +469,8 @@ func compare(c Case, lg, tg *SideResult) ([]Diff, []Assertion) {
|
|
|
return requestIDOverflowCompare(c, lg, tg)
|
|
|
case SpecialCampusRowNullBalance:
|
|
|
return campusRowNullCompare(c, lg, tg)
|
|
|
+ case SpecialPredicateCollapse:
|
|
|
+ return predicateCollapseCompare(c, lg, tg)
|
|
|
}
|
|
|
return nil, []Assertion{{
|
|
|
Name: "未知的 SpecialCase:" + c.SpecialCase, Pass: false,
|
|
|
@@ -558,6 +587,25 @@ func compare(c Case, lg, tg *SideResult) ([]Diff, []Assertion) {
|
|
|
sameMoney(dl.Special, dl.Nulls.Special, wl.Special, wl.Nulls.Special) &&
|
|
|
sameMoney(dt.Special, dt.Nulls.Special, wt.Special, wt.Nulls.Special),
|
|
|
"期望 "+moneyStr(wl.Special, wl.Nulls.Special)+" / "+moneyStr(wt.Special, wt.Nulls.Special))
|
|
|
+
|
|
|
+ // total_recommended_amount:当前 7 支 SP **都不写**它 ⇒ 期望恒为 0。
|
|
|
+ //
|
|
|
+ // 但"两侧都恰好是 0"在 25 条里会占绝大多数,照常打 ✔ 就等于造一个空过
|
|
|
+ // (`0 == 0` 与"真的比过且相等"长得一样)。所以这里分两种:
|
|
|
+ // - 结构性不适用(本 Op 不写该列,且两侧实测都是 0)→ 打 "—",并写明原因;
|
|
|
+ // - 其它一律真断言:一旦哪天有实现意外写了它,这里立刻红。
|
|
|
+ // 位置必须保留:等 `use_activity_recommend`(唯一写它的 SP)进来,它自动变成真断言。
|
|
|
+ recSkipped := !opWritesRecommended(c.Op) &&
|
|
|
+ sameMoney(dl.Recommended, dl.Nulls.Recommended, 0, false) &&
|
|
|
+ sameMoney(dt.Recommended, dt.Nulls.Recommended, 0, false)
|
|
|
+ recEqual := sameMoney(dl.Recommended, dl.Nulls.Recommended, wl.Recommended, wl.Nulls.Recommended) &&
|
|
|
+ sameMoney(dt.Recommended, dt.Nulls.Recommended, wt.Recommended, wt.Nulls.Recommended)
|
|
|
+ addDeltaRowEx(&diffs, name+" Δtotal_recommended_amount",
|
|
|
+ moneyStr(dl.Recommended, dl.Nulls.Recommended), moneyStr(dt.Recommended, dt.Nulls.Recommended),
|
|
|
+ recEqual, recSkipped,
|
|
|
+ "期望 "+moneyStr(wl.Recommended, wl.Nulls.Recommended)+" / "+
|
|
|
+ moneyStr(wt.Recommended, wt.Nulls.Recommended)+
|
|
|
+ "("+recommendedNote(c.Op)+")")
|
|
|
}
|
|
|
addDelta("全局", dGlobal, tGlobal, wgL, wgT)
|
|
|
addDelta("转出校区", dCampus, tCampus, wfL, wfT)
|
|
|
@@ -880,6 +928,7 @@ func sub(a, b Account) Account {
|
|
|
d.Income, d.Nulls.Income = subOne(a.Income, a.Nulls.Income, b.Income, b.Nulls.Income)
|
|
|
d.Expenditure, d.Nulls.Expenditure = subOne(a.Expenditure, a.Nulls.Expenditure, b.Expenditure, b.Nulls.Expenditure)
|
|
|
d.Special, d.Nulls.Special = subOne(a.Special, a.Nulls.Special, b.Special, b.Nulls.Special)
|
|
|
+ d.Recommended, d.Nulls.Recommended = subOne(a.Recommended, a.Nulls.Recommended, b.Recommended, b.Nulls.Recommended)
|
|
|
return d
|
|
|
}
|
|
|
|
|
|
@@ -1007,6 +1056,32 @@ func remarkNote(op Op) string {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// opWritesRecommended 标记"该 Op 对应的 SP 会写 total_recommended_amount"。
|
|
|
+//
|
|
|
+// 当前 7 支**都不写**(只有 `sp_courses_activity_use_activity_recommend` 写,它还没进来)。
|
|
|
+// 这条函数就是那个"将来会变 true"的开关:谁把推荐链做进来,把对应 Op 加进来即可,
|
|
|
+// 断言会自动从"不适用"变成"真断言"。
|
|
|
+func opWritesRecommended(op Op) bool {
|
|
|
+ switch op {
|
|
|
+ // 预留:OpUseActivityRecommend(老带新双边入账,4 处 UPDATE)
|
|
|
+ default:
|
|
|
+ return false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// recommendedNote 说明该 Op 下 total_recommended_amount 这一行的性质。
|
|
|
+func recommendedNote(op Op) string {
|
|
|
+ if opWritesRecommended(op) {
|
|
|
+ return "本 Op 会写该列,逐分比对"
|
|
|
+ }
|
|
|
+ return "本 Op 不写该列 ⇒ 结构性不适用(若真被写了会变红)"
|
|
|
+}
|
|
|
+
|
|
|
+// addDeltaRowEx 是 addDeltaRow 的扩展版:多一个 skipped 维度(报告里打 "—")。
|
|
|
+func addDeltaRowEx(diffs *[]Diff, item, l, t string, equal, skipped bool, note string) {
|
|
|
+ *diffs = append(*diffs, Diff{Item: item, Legacy: l, Target: t, Equal: equal, Note: note, Skipped: skipped})
|
|
|
+}
|
|
|
+
|
|
|
// addDeltaRow 往 diffs 追加一行 Δ 比对。
|
|
|
func addDeltaRow(diffs *[]Diff, item, l, t string, equal bool, note string) {
|
|
|
*diffs = append(*diffs, Diff{Item: item, Legacy: l, Target: t, Equal: equal, Note: note})
|
|
|
@@ -1061,6 +1136,9 @@ func PrintResult(w io.Writer, r *CaseResult) {
|
|
|
if !d.Equal {
|
|
|
mark = "✘"
|
|
|
}
|
|
|
+ if d.Skipped {
|
|
|
+ mark = "—" // 结构性不适用:不是"测过且相等",是"本 case 不涉及这一列"
|
|
|
+ }
|
|
|
fmt.Fprintf(w, " %s %-32s legacy=%-22s target=%-22s %s\n", mark, d.Item, d.Legacy, d.Target, d.Note)
|
|
|
}
|
|
|
|