|
|
@@ -0,0 +1,151 @@
|
|
|
+package stage2
|
|
|
+
|
|
|
+// 本文件是 **A21 扇出**(RS-4)的**声明体**。
|
|
|
+//
|
|
|
+// 本轮只落"声明",不落种子 / 求值 / 单测 —— 分步提交,避免再出现"回合结束但零产出"。
|
|
|
+// ⚠ 因此 `rsFanoutCase()` **暂不注册**进 `ResultSetCases()`:它的 `Divergence` 求值还没实现,
|
|
|
+// 注册进去会让 `resultsets` 直接 FATAL(骨架已显式拒绝静默忽略,见 `compareResultSet` 开头)。
|
|
|
+//
|
|
|
+// # 分歧是什么
|
|
|
+//
|
|
|
+// `sp_user_expense_get_bill` 的赠送结余:
|
|
|
+//
|
|
|
+// SELECT SUM(activity_money), SUM(activity_money_used)
|
|
|
+// FROM (SELECT singed.activity_money, usedLog.activity_money_used
|
|
|
+// FROM temp_courses_activity_signed singed
|
|
|
+// RIGHT JOIN (SELECT activityid, SUM(activity_money_used) activity_money_used
|
|
|
+// FROM temp_courses_activity_used_log
|
|
|
+// WHERE create_time < cutoff GROUP BY activityid) usedLog
|
|
|
+// ON singed.activityid = usedLog.activityid) t1;
|
|
|
+//
|
|
|
+// `usedLog` 已按活动聚合成**一行**,再与 signed 逐行 JOIN ⇒ 同一活动有 k 条 signed 时,
|
|
|
+// 这一行被复制 k 份,`SUM(activity_money_used)` 被计入 **k 次**。
|
|
|
+// Go(`giveBalanceBefore`)按 activity 去重,used 只计 **1 次** —— 设计 §3.8-A21 的
|
|
|
+// **DUMP-BUGFIX:刻意不复刻**。
|
|
|
+//
|
|
|
+// # 两条硬约束(下一轮落地时用)
|
|
|
+//
|
|
|
+// 1. **k 必须先被断言再被使用**:机制式 `used×(k−1)` 里的 k 来自声明。若种子静默只落 1 条,
|
|
|
+// 该式退化为 0 ⇒ 两侧相等 ⇒ 判定行红,而归因会误指"缺陷被补回去了"。
|
|
|
+// 2. ⚠ **target 侧当前 schema 无法表达该状态**:`t_courses_activity_signed` 上有唯一键
|
|
|
+// `uk_user_activity(user_id, activity_id)`(实测插入第二条报 1062)。所以"两侧都种 k=2"
|
|
|
+// 在本库**做不到**;落地形态要与 team-lead 确认是"legacy-only 登记 + target 唯一键拒绝断言"
|
|
|
+// 还是别的。本文件的 per-side 期望按"target 能表达"的假设写,仅为把声明先固定下来。
|
|
|
+
|
|
|
+// 本 case 用独立的活动 id(与 RS-1/RS-2 的 9000xx 不重叠,报告里一眼能分开)。
|
|
|
+const rsFanoutActivityID int64 = 900011
|
|
|
+
|
|
|
+// 扇出条数 k。**它是声明**,不是"从库里读出来直接用"——库里读到的值只用于断言它。
|
|
|
+const rsFanoutK = 2
|
|
|
+
|
|
|
+// 种子的单一声明(金额单位:分)。
|
|
|
+const (
|
|
|
+ rsFanoutSignedMoneyCents int64 = 10000 // 每条 signed 的 activity_money = 100.00
|
|
|
+ rsFanoutUsedPrevCents int64 = 4000 // 期初前发生的 used = 40.00
|
|
|
+ rsFanoutUsedEndCents int64 = 6500 // 期末前累计 used = 40.00 + 25.00 = 65.00
|
|
|
+)
|
|
|
+
|
|
|
+// rsFanoutCommonCols 是"仍必须相等"的 6 列(分歧面之外)。
|
|
|
+func rsFanoutCommonCols() []string {
|
|
|
+ return []string{
|
|
|
+ "previousBalance", "currentTotalIncome", "currentTotalActivityMoney",
|
|
|
+ "currentTotalRecommendedAmount", "currentSpend", "currentBalance",
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// rsFanoutDivergentCols 是"允许且要求不相等"的 3 列。
|
|
|
+func rsFanoutDivergentCols() []string {
|
|
|
+ return []string{"previousGiveCost", "currentGiveCost", "currentUsedGiveCost"}
|
|
|
+}
|
|
|
+
|
|
|
+// rsFanoutExpected 由声明算出**两侧各自**应得的值(先算再跑,不跑出来再抄)。
|
|
|
+//
|
|
|
+// legacy:k 条 signed × (activity_money + used) —— used 被计入 k 次
|
|
|
+// target:Σ activity_money(k 条都算)+ used(只算 1 次)
|
|
|
+//
|
|
|
+// 其余 6 列两侧相同:期初 1000、窗口内只有一条 −100、无 type=100/107 流水。
|
|
|
+func rsFanoutExpected() (legacy, target map[string]int64) {
|
|
|
+ signedSum := rsFanoutSignedMoneyCents * rsFanoutK
|
|
|
+ usedPrev := rsFanoutUsedPrevCents
|
|
|
+ usedEnd := rsFanoutUsedEndCents
|
|
|
+
|
|
|
+ common := map[string]int64{
|
|
|
+ "previousBalance": 100000,
|
|
|
+ "currentTotalIncome": 0,
|
|
|
+ "currentTotalActivityMoney": 0,
|
|
|
+ "currentTotalRecommendedAmount": 0,
|
|
|
+ "currentSpend": -10000,
|
|
|
+ "currentBalance": 90000,
|
|
|
+ }
|
|
|
+
|
|
|
+ legacy = map[string]int64{
|
|
|
+ "previousBalance": common["previousBalance"],
|
|
|
+ "previousGiveCost": signedSum + usedPrev*rsFanoutK,
|
|
|
+ "currentTotalIncome": common["currentTotalIncome"],
|
|
|
+ "currentTotalActivityMoney": common["currentTotalActivityMoney"],
|
|
|
+ "currentTotalRecommendedAmount": common["currentTotalRecommendedAmount"],
|
|
|
+ "currentSpend": common["currentSpend"],
|
|
|
+ "currentUsedGiveCost": (signedSum + usedPrev*rsFanoutK) - (signedSum + usedEnd*rsFanoutK),
|
|
|
+ "currentBalance": common["currentBalance"],
|
|
|
+ "currentGiveCost": signedSum + usedEnd*rsFanoutK,
|
|
|
+ }
|
|
|
+ target = map[string]int64{
|
|
|
+ "previousBalance": common["previousBalance"],
|
|
|
+ "previousGiveCost": signedSum + usedPrev,
|
|
|
+ "currentTotalIncome": common["currentTotalIncome"],
|
|
|
+ "currentTotalActivityMoney": common["currentTotalActivityMoney"],
|
|
|
+ "currentTotalRecommendedAmount": common["currentTotalRecommendedAmount"],
|
|
|
+ "currentSpend": common["currentSpend"],
|
|
|
+ "currentUsedGiveCost": (signedSum + usedPrev) - (signedSum + usedEnd),
|
|
|
+ "currentBalance": common["currentBalance"],
|
|
|
+ "currentGiveCost": signedSum + usedEnd,
|
|
|
+ }
|
|
|
+ return legacy, target
|
|
|
+}
|
|
|
+
|
|
|
+// rsFanoutExpectedNorm 把一侧的期望渲染成 2 位小数字符串(比对口径)。
|
|
|
+func rsFanoutExpectedNorm(side map[string]int64) map[string]string {
|
|
|
+ out := make(map[string]string, len(side))
|
|
|
+ for k, v := range side {
|
|
|
+ out[k] = yuan(v)
|
|
|
+ }
|
|
|
+ return out
|
|
|
+}
|
|
|
+
|
|
|
+// rsFanoutCase 是 RS-4 的声明体。
|
|
|
+//
|
|
|
+// ⚠ **暂不注册**(见文件头):`Divergence` 求值未实现,注册即 FATAL。
|
|
|
+func rsFanoutCase() ResultSetCase {
|
|
|
+ lgExp, tgExp := rsFanoutExpected()
|
|
|
+ return ResultSetCase{
|
|
|
+ Name: "RS-4 账单·A21 扇出(同一活动 k 条报名 ⇒ used 重复计入)INTENTIONAL-STRICT",
|
|
|
+ Note: "两侧**本来就不该相等**:legacy 把 used 计入 k 次(dump 缺陷),target 去重只计 1 次(刻意不复刻)。" +
|
|
|
+ "绿的含义是「分歧已登记且双方符合各自声明」;两侧一旦相等就红(缺陷被补回 / target 被改坏)," +
|
|
|
+ "分歧扩散到声明外的列也红",
|
|
|
+ LegacyUserID: rsUserID,
|
|
|
+ InstitutionID: rsInstID,
|
|
|
+ CampusID: rsCampusID,
|
|
|
+ // LegacyCall / Seed* / CallTarget / Preconditions / ExtraAssertions 下一轮补。
|
|
|
+ Compare: func(lg ResultSet, tg TargetPayload) ResultSetInput {
|
|
|
+ common := make(map[string]string, len(rsFanoutCommonCols()))
|
|
|
+ for _, col := range rsFanoutCommonCols() {
|
|
|
+ common[col] = yuan(lgExp[col])
|
|
|
+ }
|
|
|
+ return ResultSetInput{
|
|
|
+ CaseName: "RS-4",
|
|
|
+ Legacy: lg,
|
|
|
+ ExpectRows: 1,
|
|
|
+ Target: tg,
|
|
|
+ Columns: rsBillColumns(), // 照抄 RS-2 的 9 列映射
|
|
|
+ Expect: common, // 其余 6 列按相等处理
|
|
|
+ Divergence: &Divergence{
|
|
|
+ Reason: "A21 扇出:legacy 对同一活动的 k 条 signed 把 used 额重复计入 k 次;" +
|
|
|
+ "target 按 activity 去重只计 1 次(dump 缺陷,刻意不复刻)",
|
|
|
+ Columns: rsFanoutDivergentCols(),
|
|
|
+ Legacy: rsFanoutExpectedNorm(lgExp),
|
|
|
+ Target: rsFanoutExpectedNorm(tgExp),
|
|
|
+ },
|
|
|
+ }
|
|
|
+ },
|
|
|
+ }
|
|
|
+}
|