|
@@ -77,9 +77,12 @@ func recommendCompare(c Case, lg, tg *SideResult) ([]Diff, []Assertion) {
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
// 107 流水:**每人一条**(`type=107`、`money=+m`)。
|
|
// 107 流水:**每人一条**(`type=107`、`money=+m`)。
|
|
|
- // 只数本 case 用户的(推荐人的流水不在共享快照里,见文件末的未覆盖说明)。
|
|
|
|
|
- lgLogs := countLogsByType(lg.Before, lg.After, 107)
|
|
|
|
|
- tgLogs := countLogsByType(tg.Before, tg.After, 107)
|
|
|
|
|
|
|
+ //
|
|
|
|
|
+ // 两条 INSERT 是**独立的**(dump 763386 / 763407),各用同一个 `inputOrder` 作 `logid`。
|
|
|
|
|
+ // 所以"金额到账"不能推出"凭证写成" —— 必须分别数。
|
|
|
|
|
+ lgNew := newLogs(lg.Before.Logs, lg.After.Logs)
|
|
|
|
|
+ tgNew := newLogs(tg.Before.Logs, tg.After.Logs)
|
|
|
|
|
+ lgLogs, tgLogs := countType(lgNew, 107), countType(tgNew, 107)
|
|
|
add("被推荐人新增 type=107 流水条数", fmt.Sprint(lgLogs), fmt.Sprint(tgLogs), lgLogs == tgLogs, "")
|
|
add("被推荐人新增 type=107 流水条数", fmt.Sprint(lgLogs), fmt.Sprint(tgLogs), lgLogs == tgLogs, "")
|
|
|
asserts = append(asserts, Assertion{Kind: AssertionBusiness,
|
|
asserts = append(asserts, Assertion{Kind: AssertionBusiness,
|
|
|
Name: "被推荐人新增**恰好 1 条** type=107 流水(奖励入账凭证)",
|
|
Name: "被推荐人新增**恰好 1 条** type=107 流水(奖励入账凭证)",
|
|
@@ -87,6 +90,26 @@ func recommendCompare(c Case, lg, tg *SideResult) ([]Diff, []Assertion) {
|
|
|
Detail: fmt.Sprintf("legacy=%d target=%d(期望各 1)。0 条 = 没入账;2 条 = 重复入账", lgLogs, tgLogs),
|
|
Detail: fmt.Sprintf("legacy=%d target=%d(期望各 1)。0 条 = 没入账;2 条 = 重复入账", lgLogs, tgLogs),
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+ // 推荐人的 107:**这一条是 F24 的探针**。
|
|
|
|
|
+ //
|
|
|
|
|
+ // target 侧的幂等键是 `uk_request_type(request_id, type)` —— **不含 `user_id`**。
|
|
|
|
|
+ // 而本 SP 的语义是"同一个 `inputOrder` 下**两个用户各写一条 type=107**"。
|
|
|
|
|
+ // ⇒ 若 target 把 `inputOrder` 直接当 `request_id`,第二条 107 会撞键:
|
|
|
|
|
+ // 要么凭证丢失、要么整块回滚(半写)、要么 target 自己加了用户维度(需登记)。
|
|
|
|
|
+ // 三种后果完全不同,**只有把推荐人那条数出来才能分辨**。
|
|
|
|
|
+ lgRNew := newLogs(lg.Before.RecommenderLogs, lg.After.RecommenderLogs)
|
|
|
|
|
+ tgRNew := newLogs(tg.Before.RecommenderLogs, tg.After.RecommenderLogs)
|
|
|
|
|
+ lgR, tgR := countType(lgRNew, 107), countType(tgRNew, 107)
|
|
|
|
|
+ lgRSum, tgRSum := sumMoneyOfType(lgRNew, 107), sumMoneyOfType(tgRNew, 107)
|
|
|
|
|
+ add("推荐人新增 type=107 流水条数", fmt.Sprint(lgR), fmt.Sprint(tgR), lgR == tgR, "F24 探针")
|
|
|
|
|
+ asserts = append(asserts, Assertion{Kind: AssertionBusiness,
|
|
|
|
|
+ Name: "**推荐人也新增恰好 1 条** type=107 流水,且 money = +m(F24:uk_request_type 不含 user_id,两人同单号有撞键风险)",
|
|
|
|
|
+ Pass: lgR == 1 && tgR == 1 && lgRSum == want && tgRSum == want,
|
|
|
|
|
+ Detail: fmt.Sprintf("legacy 条数=%d 金额合计=%+d | target 条数=%d 金额合计=%+d | m=%d。"+
|
|
|
|
|
+ "条数 0 ⇒ 凭证丢失(钱到了、账没记);金额不符 ⇒ 记错数;两侧条数不同 ⇒ 幂等键口径不同(需登记)",
|
|
|
|
|
+ lgR, lgRSum, tgR, tgRSum, want),
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
// 活动结算态:`signed.status` 变 2。
|
|
// 活动结算态:`signed.status` 变 2。
|
|
|
lgSt, tgSt := signedStatus(lg, c.ActivityID), signedStatus(tg, c.ActivityID)
|
|
lgSt, tgSt := signedStatus(lg, c.ActivityID), signedStatus(tg, c.ActivityID)
|
|
|
add("报名状态(结算后)", fmt.Sprint(lgSt), fmt.Sprint(tgSt), lgSt == tgSt, "dump: status=2")
|
|
add("报名状态(结算后)", fmt.Sprint(lgSt), fmt.Sprint(tgSt), lgSt == tgSt, "dump: status=2")
|
|
@@ -107,42 +130,59 @@ func recommendCompare(c Case, lg, tg *SideResult) ([]Diff, []Assertion) {
|
|
|
lgOK, c.ExpectLegacySuccess, errOf(lg.Calls), tgOK, c.ExpectTargetSuccess, errOf(tg.Calls)),
|
|
lgOK, c.ExpectLegacySuccess, errOf(lg.Calls), tgOK, c.ExpectTargetSuccess, errOf(tg.Calls)),
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- // 未覆盖项显式登记(不允许"没测"冒充"测过")。
|
|
|
|
|
- asserts = append(asserts, Assertion{Kind: AssertionInfrastructure, Skipped: true,
|
|
|
|
|
- Name: "未覆盖:**推荐人**那条 type=107 流水(共享快照只收本 case 用户的流水)",
|
|
|
|
|
- Pass: true,
|
|
|
|
|
- Detail: "推荐人的 `t_user_expense_log` 不在 `SideSnapshot.Logs` 里 ⇒ 本条打 `—` 不算通过。" +
|
|
|
|
|
- "「两人各一条凭证」目前由「两个用户 Δbalance/Δrecommended 都 =+m」间接支撑;" +
|
|
|
|
|
- "补齐方式:给快照加 `RecommenderLogs`(未做,**主动登记**而不是默认没测)",
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
return diffs, asserts
|
|
return diffs, asserts
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// countLogsByType 数"本次**新增**"的某类流水条数。
|
|
|
|
|
|
|
+// newLogs 取"本次新增"的流水行。
|
|
|
|
|
+//
|
|
|
|
|
+// ⚠ 这里要写清两个**看起来相反**的事实(本轮的坑就在这儿):
|
|
|
//
|
|
//
|
|
|
-// 为什么不能写成 `afterCount − beforeCount`(本轮真踩了,出来是 **−2**):
|
|
|
|
|
|
|
+// ① `After.Logs` **已经**被 `filterLogs(…, maxLogID)` 过滤成"仅本次新增行"
|
|
|
|
|
+// (两侧都是,`legacy.go` / `target.go` 的快照函数里);
|
|
|
|
|
+// ② 而 `RecommenderLogs` 是**全量**(读的时候没算推荐人的 maxID)。
|
|
|
//
|
|
//
|
|
|
-// `SideSnapshot.Logs` 是**窗口**(按 id 倒序取最近若干条),不是全表。
|
|
|
|
|
-// 新增两条后窗口滑动,旧的两条被挤出去 ⇒ 差值可能是负数,
|
|
|
|
|
-// 而负数的含义是"快照窗口变了",不是"流水变少了"。
|
|
|
|
|
|
|
+// 所以同一套判据必须能同时吃这两种输入 ⇒ 一律用**按 id 集合求差**:
|
|
|
|
|
+// 对①求差得到它自己(无害),对②求差得到真正的新增行。
|
|
|
//
|
|
//
|
|
|
-// 正确判据是**按 id 集合求差**:`after` 里 id 不在 `before` 集合中的那些行就是本次新增。
|
|
|
|
|
-// 新增行的 id 最大,必然落在窗口内,所以这个判据不受窗口大小影响。
|
|
|
|
|
-func countLogsByType(before, after SideSnapshot, typ int32) int {
|
|
|
|
|
- seen := make(map[int64]bool, len(before.Logs))
|
|
|
|
|
- for _, r := range before.Logs {
|
|
|
|
|
|
|
+// 为什么不能写成 `afterCount − beforeCount`:第一版就是这么写的,跑出 **−2**。
|
|
|
|
|
+// 因为 `after` 是"仅新增"、`before` 是"全量",相减是拿两组不同口径的数做差 ——
|
|
|
|
|
+// **负数不是"流水变少了",是两组数根本不可比**。
|
|
|
|
|
+func newLogs(before, after []LogRow) []LogRow {
|
|
|
|
|
+ seen := make(map[int64]bool, len(before))
|
|
|
|
|
+ for _, r := range before {
|
|
|
seen[r.ID] = true
|
|
seen[r.ID] = true
|
|
|
}
|
|
}
|
|
|
|
|
+ out := make([]LogRow, 0, 2)
|
|
|
|
|
+ for _, r := range after {
|
|
|
|
|
+ if !seen[r.ID] {
|
|
|
|
|
+ out = append(out, r)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return out
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// countType 数某类条数。
|
|
|
|
|
+func countType(rows []LogRow, typ int32) int {
|
|
|
n := 0
|
|
n := 0
|
|
|
- for _, r := range after.Logs {
|
|
|
|
|
- if r.Type == typ && !seen[r.ID] {
|
|
|
|
|
|
|
+ for _, r := range rows {
|
|
|
|
|
+ if r.Type == typ {
|
|
|
n++
|
|
n++
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
return n
|
|
return n
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// sumMoneyOfType 求某类流水的金额合计(分)。
|
|
|
|
|
+func sumMoneyOfType(rows []LogRow, typ int32) int64 {
|
|
|
|
|
+ var s int64
|
|
|
|
|
+ for _, r := range rows {
|
|
|
|
|
+ if r.Type == typ {
|
|
|
|
|
+ s += r.Money
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return s
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// signedStatus 取某活动的报名状态(读不到返回 −1,与"状态是 0"区分开)。
|
|
// signedStatus 取某活动的报名状态(读不到返回 −1,与"状态是 0"区分开)。
|
|
|
func signedStatus(s *SideResult, activityID int64) int32 {
|
|
func signedStatus(s *SideResult, activityID int64) int32 {
|
|
|
if s.After.Signed == nil {
|
|
if s.After.Signed == nil {
|