|
|
@@ -214,7 +214,7 @@ SP11 的签名只有 4 个参数(`inputUserid, inputSchoolid, inputCampusid, i
|
|
|
同理,`seedTarget` 在 `campus_id == 0` 时**不建校区行、不建 `t_legacy_mapping` 的 campus 关联**——
|
|
|
如果为了"让 case 能跑"而造一个 `campus_id=0` 的校区行,就绕过了要验证的守卫。
|
|
|
|
|
|
-## 3.7 全部 13 个 case 一览
|
|
|
+## 3.7 批 A / 批 B 的 13 个 case 一览(批 C1 见 §7,批 C1 补齐后全量 24 个)
|
|
|
|
|
|
|case|Op|入参|期望|
|
|
|
|---|---|---|---|
|
|
|
@@ -440,7 +440,13 @@ CampusMissing: true // 本 case 故意用不存在的校区;不建 t_campus
|
|
|
没有它,"读不到账户"会被读成 0 余额,两侧都是 0 → 基线相等断言**空过**,
|
|
|
整个 case 看起来在跑、其实什么都没测。
|
|
|
|
|
|
-### 7.8 批 C1 变异实验(3 组,全部还原)
|
|
|
+### 7.8 批 C1 变异实验(全部还原)
|
|
|
+
|
|
|
+变异分两侧打,结论不一样,所以要分开记:
|
|
|
+**实现侧**变异证明"代码写错会被抓住",**判据侧**变异证明"判据本身不是摆设"
|
|
|
+(判据侧若改了还不红,说明那条断言是空的 —— 它绿不绿跟被测代码无关)。
|
|
|
+
|
|
|
+#### 7.8.1 实现侧(3 组)
|
|
|
|
|
|
| # | 改错什么 | 靶子 | 实测 |
|
|
|
|---|---|---|---|
|
|
|
@@ -448,6 +454,25 @@ CampusMissing: true // 本 case 故意用不存在的校区;不建 t_campus
|
|
|
| **M2** | `t_campus.self_support` 硬编码成 1 | 校区元数据一致 + 活动失效范围 | `✘ 两侧看到的校区元数据一致`(target 自营=true ≠ 声明 false);`✘ 活动失效范围 → target 多失效 92:2→4` |
|
|
|
| **M3** | 取消活动把报名状态写成 **4**(过期)而非 **0**(取消) | `归 0 不是 4` | `✘ 报名状态归 **0** → target {91=4,…}`;`✘ 只有本活动的报名状态变` |
|
|
|
|
|
|
+> M1 的注:`68542` / `40583` 的**真实**余额都是"全局 == 该校区"(9740/9740、5400/5400),
|
|
|
+> 在这种基线上"减 campusBalance"与"归 0"结果**完全一样**,M1 改错也不会红。
|
|
|
+> 所以 C1-1/C1-2 用 `Baseline` 把两者拉开成 5000/1200、3000/900 —— 差值 3800 / 2100,
|
|
|
+> 这条断言才真正 load-bearing。**绿了不等于测到了**,这一条是本批最大的教训。
|
|
|
+
|
|
|
+#### 7.8.2 判据侧(4 组,批 C1 交付时补做)
|
|
|
+
|
|
|
+| # | 改错什么(改判据,不改实现) | 预期红 | 实际红 | 一致? |
|
|
|
+|---|---|---|---|---|
|
|
|
+| **M1'** | `expirable()` 自营分支删掉 `\|\| it.CampusID == 0` | 只 C1-1 红,C1-2 保持绿 | `✘ 活动失效范围`:期望 `{91=4, 92=2, …}` vs 实测 `{91=4, 92=4, …}`;C1-1 FAIL 1 项;**C1-2 仍 PASS** | ✅ 完全一致 |
|
|
|
+| **M2'** | `clearedBalanceExpected()` 删掉 `Special: campusBalance` | C1-1 / C1-2 红在 `Δspecial_expenses` | C1-2:`✘ 全局 Δspecial_expenses legacy=900.00 期望 0.00`、`✘ 转出校区 Δspecial_expenses …`,FAIL 2 项 | ✅ 一致(C1-1 同机理,未单独跑) |
|
|
|
+| **M3'** | `signedAll(c, lg, 0)` 的 `0` 改成 `4` | C1-6 红 | `✘ 取消充值活动:报名状态归 0(不是 4)`,FAIL 1 项 | ✅ 一致 |
|
|
|
+| **M4'** | 清零断言改成"期望全局归 0"(`-Before.Global.Balance`) | C1-1 红 | `✘ 清零:全局余额只减 campusBalance`(实测 Δ−1200.00,期望 −5000.00),FAIL 1 项 | ✅ 一致 |
|
|
|
+
|
|
|
+M3' 有一个值得记的细节:改错 `signedAll` 之后,**`只有本活动的报名状态变`(整张 map 全等那条)仍然是绿的**。
|
|
|
+这两条不是重复:`signedAll` 只盯"活动 91 是不是 0",整表全等盯"别的活动有没有被顺手动"。
|
|
|
+判据 M3' 只动前者 → 只有前者红;实现 M3(改实现)两条都红。**一条断言只钉一件事**,
|
|
|
+所以两条都要留。
|
|
|
+
|
|
|
M2 特别值得记:它同时红了两条,而且**第一条直接指出原因**(元数据不一致),
|
|
|
第二条才是指标(失效范围错)。"断言拆得细"不是为了严格,是为了**出错时能定位**。
|
|
|
|
|
|
@@ -459,3 +484,74 @@ C1-1 的余额根本不该为负。
|
|
|
|
|
|
改成显式声明:`Case.ExpectNegativeBalance`,只有 R2 设它。
|
|
|
**"用到了某个字段"不等于"这个字段的原意成立"** —— 判据要写在 case 上,不要从别的字段反推。
|
|
|
+
|
|
|
+### 7.10 补齐的三个边界 case(C1-9 / C1-10 / C1-11)
|
|
|
+
|
|
|
+前 8 条都在"正常金额 + 正常账户 + 正常请求号"的区间里,三个边界一条都没踩到。
|
|
|
+
|
|
|
+| case | 构型 | 真实/合成 | 结论 |
|
|
|
+|---|---|---|---|
|
|
|
+| **C1-9** | `requestid` **33 字符** | 真实构型(68542 + campus 5),请求号是**构造**的(生产不会有 33 字符) | `INTENTIONAL-WIDEN` |
|
|
|
+| **C1-10** | 余额 100.00 退 500.00 | 金额**构造**(`Baseline` 压到 100.00),用户/校区真实 | 两侧都 −400.00(R15 第 3 条来源) |
|
|
|
+| **C1-11** | 校区在、账户行**不存在** | 真实构型:40585 全局 5280.00、campus 5 无账户行(已核) | `LEGACY-DEFECT`(见 7.11) |
|
|
|
+
|
|
|
+**C1-9 的真正发现不是"列宽不同",是"legacy 半途失败会留下无凭证的扣款"**:
|
|
|
+
|
|
|
+```
|
|
|
+legacy: UPDATE balance=balance-m, total_income=total_income-m ← 成功
|
|
|
+ INSERT t_user_expense_log(logid=33 字符) ← 1406,整个 CALL 中止
|
|
|
+ ⇒ money 扣了,log 没有
|
|
|
+target: log_id / request_id 都是 varchar(64) ⇒ 正常入库
|
|
|
+```
|
|
|
+
|
|
|
+MySQL 只回滚**失败的那条语句**,不回滚同一次 CALL 里已经执行过的前置语句。
|
|
|
+所以 legacy 的终态是"余额少了 30,但查不到任何凭证"。这不是我们引入的差异,
|
|
|
+但**必须在台账里写清楚**:迁移后如果只按流水对账,这 30 元会变成"凭空消失"。
|
|
|
+
|
|
|
+列宽实测(`information_schema`):
|
|
|
+
|
|
|
+| 列 | legacy | target |
|
|
|
+|---|---|---|
|
|
|
+| `t_user_expense_log.logid` / `log_id` | `varchar(32)` | `varchar(64)` |
|
|
|
+| (target 独有)`request_id` | — | `varchar(64)` |
|
|
|
+
|
|
|
+判定原则:target 列宽 **>** 32 → `INTENTIONAL-WIDEN`(我们更宽,不是缺陷,也不并进
|
|
|
+`IntentionalStrict` —— Strict 是"我们更严",Widen 是"我们更宽",台账要能分开数);
|
|
|
+**=** 32 → 两侧一致,按普通 case;**<** 32 → 真缺陷(我们比 legacy 窄,部分历史请求号无法复用)。
|
|
|
+本次实测是 **64 > 32**。
|
|
|
+
|
|
|
+### 7.11 C1-11 挖出的 legacy 数据缺陷:建行之后金额列是 NULL
|
|
|
+
|
|
|
+dump 的建行语句只写三列:
|
|
|
+
|
|
|
+```sql
|
|
|
+IF logCount=0 THEN
|
|
|
+ INSERT INTO `t_user_expense_school`(`userid`,`schoolid`,`campusid`) VALUES(...);
|
|
|
+END IF;
|
|
|
+```
|
|
|
+
|
|
|
+而两张表的列定义**不一样**(`information_schema.COLUMNS`):
|
|
|
+
|
|
|
+| 表 | 列 | 类型 | 默认值 |
|
|
|
+|---|---|---|---|
|
|
|
+| `t_user_expense_school` | balance / spend / total_income / total_expenditure / special_expenses | `decimal(10,3) **NULL**` | **无(DEFAULT NULL)** |
|
|
|
+| `t_user_expense` | balance / total_income | `float(10,3)` | **`0.000`** |
|
|
|
+
|
|
|
+于是:
|
|
|
+
|
|
|
+```
|
|
|
+校区分支:INSERT → balance=NULL → UPDATE balance=balance-30 → NULL-30 = **NULL**(NULL 传染)
|
|
|
+全局分支:INSERT → balance=0.000 → UPDATE balance=balance-30 → **-30.000**
|
|
|
+```
|
|
|
+
|
|
|
+**同一笔业务,校区一分没扣、全局扣了 30,而 type=209 的凭证照写。** 两表从此永久不一致。
|
|
|
+
|
|
|
+target 侧同名列是 `decimal(12,2) NOT NULL DEFAULT 0.00`,建行即 0,真扣成 −30.00,两表一致。
|
|
|
+所以这条判定是 **`LEGACY-DEFECT`:legacy 错、target 对,不跟**。
|
|
|
+
|
|
|
+> 附带一个工程教训:这第一条 case 一跑就在**读回阶段**崩了
|
|
|
+> (`converting NULL to string is unsupported`),连断言都走不到。
|
|
|
+> 当时的直觉是"case 写错了",实际是"读的工具不够宽容" ——
|
|
|
+> `snapshotLegacy` 的校区列必须用 `sql.NullString`,并用
|
|
|
+> `SideSnapshot.CampusNullBalance` 把"行在、但金额是 NULL"显式记下来。
|
|
|
+> **读不出来的状态,等于没有这个状态。**
|