|
@@ -202,18 +202,42 @@ func activityCompare(c Case, lg, tg *SideResult) ([]Diff, []Assertion) {
|
|
|
dActID = c.ActivityPlan.Items[c.Activity.Discounts[0].ActivityIndex].ID
|
|
dActID = c.ActivityPlan.Items[c.Activity.Discounts[0].ActivityIndex].ID
|
|
|
}
|
|
}
|
|
|
dKey := ActivitySignedKey{ActivityID: dActID, UserUID: uid}
|
|
dKey := ActivitySignedKey{ActivityID: dActID, UserUID: uid}
|
|
|
- if ld, lok := lb.Discounts[dKey]; lok || hasAnyDiscount(lb, ta, dKey) {
|
|
|
|
|
|
|
+ // 声明了就必须执行(自伤清单第 0 条的防法)。
|
|
|
|
|
+ divergent := c.LegacyDrainsCursorAfterFirst || c.LegacyRefundTakesFullFace
|
|
|
|
|
+ if len(c.Activity.Discounts) > 0 {
|
|
|
|
|
+ ld, lok := lb.Discounts[dKey]
|
|
|
td, tok := tb.Discounts[dKey]
|
|
td, tok := tb.Discounts[dKey]
|
|
|
|
|
+ // 自伤清单第 0 条的防法:**声明了就必须执行**。
|
|
|
|
|
+ // 若两侧都取不到这一行,不能静默跳过(那样"键写错"和"本 case 不涉及"长得一样),
|
|
|
|
|
+ // 而要判 FAIL 并把键打出来。
|
|
|
|
|
+ if !lok && !tok {
|
|
|
|
|
+ asserts = append(asserts, Assertion{Kind: AssertionBusiness,
|
|
|
|
|
+ Name: "额度行快照**必须能取到**(取不到 ⇒ 判 FAIL,不静默跳过)",
|
|
|
|
|
+ Pass: false,
|
|
|
|
|
+ Detail: fmt.Sprintf("key=(activity=%d, user=%s) 两侧都取不到额度行 —— "+
|
|
|
|
|
+ "本 case 声明了 `Discounts`,所以这不是「不涉及」,是**键不对或种子没进去**", dActID, uid),
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
asserts = append(asserts, Assertion{Kind: AssertionBusiness,
|
|
asserts = append(asserts, Assertion{Kind: AssertionBusiness,
|
|
|
- Name: "额度行 `discount_amount_used` 两侧一致(2 位口径;`discount_amount` 走 2 位、`used` 走 3 位,**不共用读函数**)",
|
|
|
|
|
- Pass: lok == tok && (!lok || ld.DiscountAmountUsed == td.DiscountAmountUsed) &&
|
|
|
|
|
|
|
+ Name: firstOr(ternaryName(divergent),
|
|
|
|
|
+ "额度行 `discount_amount_used` 两侧一致(2 位口径;`discount_amount` 走 2 位、`used` 走 3 位,**不共用读函数**)"),
|
|
|
|
|
+ // 分歧 case(LEGACY-DEFECT)**不判两侧相等** —— 它们本来就该不同;
|
|
|
|
|
+ // 改成 per-side 各自对期望。否则这条断言会把"登记分歧"记成红。
|
|
|
|
|
+ Pass: lok == tok &&
|
|
|
|
|
+ (divergent || !lok || ld.DiscountAmountUsed == td.DiscountAmountUsed) &&
|
|
|
(!lok || ld.DiscountAmount == td.DiscountAmount) &&
|
|
(!lok || ld.DiscountAmount == td.DiscountAmount) &&
|
|
|
- (!lok || !isRefundOp(c.Op) || ld.DiscountAmountUsed == wantDiscountUsed(c)),
|
|
|
|
|
|
|
+ // 分歧 case 下 legacy 有**自己的**期望(少退/超退),不能套 target 的公式
|
|
|
|
|
+ (!lok || !isRefundOp(c.Op) || divergent ||
|
|
|
|
|
+ ld.DiscountAmountUsed == wantDiscountUsed(c, false)) &&
|
|
|
|
|
+ (!lok || !(c.LegacyDrainsCursorAfterFirst || c.LegacyRefundTakesFullFace) ||
|
|
|
|
|
+ ld.DiscountAmountUsed == wantDiscountUsedLegacy(c)) &&
|
|
|
|
|
+ (!tok || !(c.LegacyDrainsCursorAfterFirst || c.LegacyRefundTakesFullFace) ||
|
|
|
|
|
+ td.DiscountAmountUsed == wantDiscountUsed(c, true)),
|
|
|
Detail: fmt.Sprintf("legacy used=%s amount=%s | target used=%s amount=%s | 期望 used=%s"+
|
|
Detail: fmt.Sprintf("legacy used=%s amount=%s | target used=%s amount=%s | 期望 used=%s"+
|
|
|
"(退回类才校验期望;使用类的扣减由两侧相等 + 额度流水保证)(存在 legacy=%v target=%v)",
|
|
"(退回类才校验期望;使用类的扣减由两侧相等 + 额度流水保证)(存在 legacy=%v target=%v)",
|
|
|
yuan(ld.DiscountAmountUsed), yuan(ld.DiscountAmount),
|
|
yuan(ld.DiscountAmountUsed), yuan(ld.DiscountAmount),
|
|
|
yuan(td.DiscountAmountUsed), yuan(td.DiscountAmount),
|
|
yuan(td.DiscountAmountUsed), yuan(td.DiscountAmount),
|
|
|
- yuan(wantDiscountUsed(c)), lok, tok),
|
|
|
|
|
|
|
+ yuan(wantDiscountUsed(c, false)), lok, tok),
|
|
|
})
|
|
})
|
|
|
// Q3:3a 不做 id 重映射 ⇒ 两侧该列都必须是 NULL,**显式断言**(不是"不写就不管")
|
|
// Q3:3a 不做 id 重映射 ⇒ 两侧该列都必须是 NULL,**显式断言**(不是"不写就不管")
|
|
|
asserts = append(asserts, Assertion{Kind: AssertionBusiness,
|
|
asserts = append(asserts, Assertion{Kind: AssertionBusiness,
|
|
@@ -229,8 +253,11 @@ func activityCompare(c Case, lg, tg *SideResult) ([]Diff, []Assertion) {
|
|
|
Item: "额度流水(多重集,仅本次新增行;父行 id 已归一成 (活动, 用户))",
|
|
Item: "额度流水(多重集,仅本次新增行;父行 id 已归一成 (活动, 用户))",
|
|
|
Legacy: fmt.Sprint(len(lDNew)),
|
|
Legacy: fmt.Sprint(len(lDNew)),
|
|
|
Target: fmt.Sprint(len(tDNew)),
|
|
Target: fmt.Sprint(len(tDNew)),
|
|
|
- Equal: okD,
|
|
|
|
|
- Note: detailD,
|
|
|
|
|
|
|
+ Equal: okD || divergent,
|
|
|
|
|
+ // 分歧 case(LEGACY-DEFECT):**登记**两侧实际值,不做"相等"判定 ——
|
|
|
|
|
+ // 灰色地带用 Skipped 打 "—",避免"没比"冒充"比过且相等"。
|
|
|
|
|
+ Skipped: divergent,
|
|
|
|
|
+ Note: firstOr(ternaryNote(divergent), detailD),
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
// ---- ④b intentional-strict:legacy 静默 / target 显式拒绝 ----
|
|
// ---- ④b intentional-strict:legacy 静默 / target 显式拒绝 ----
|
|
@@ -419,7 +446,20 @@ func activityChainVerdict(c Case) string {
|
|
|
// ⚠ 它**不是**"退成负数":申请退超时只退到 0 为止、剩余不退也不报错。
|
|
// ⚠ 它**不是**"退成负数":申请退超时只退到 0 为止、剩余不退也不报错。
|
|
|
// 这里按种子声明的 `UsedCents` 与负流水之和算,不写死数字 ——
|
|
// 这里按种子声明的 `UsedCents` 与负流水之和算,不写死数字 ——
|
|
|
// 写死会让"期望"和"种子"脱钩,改种子就悄悄失效。
|
|
// 写死会让"期望"和"种子"脱钩,改种子就悄悄失效。
|
|
|
-func wantDiscountUsed(c Case) int64 {
|
|
|
|
|
|
|
+// wantDiscountUsedLegacy 是**旧 SP 侧**的期望(它有自己的缺陷,不能和 target 共用一条公式)。
|
|
|
|
|
+func wantDiscountUsedLegacy(c Case) int64 {
|
|
|
|
|
+ if len(c.Activity.Discounts) == 0 {
|
|
|
|
|
+ return 0
|
|
|
|
|
+ }
|
|
|
|
|
+ used := c.Activity.Discounts[0].UsedCents
|
|
|
|
|
+ // `SELECT inputAmount = logDiscountAmountUsed` 不是赋值 ⇒ 钳制没生效 ⇒ 按面额退
|
|
|
|
|
+ if c.LegacyRefundTakesFullFace {
|
|
|
|
|
+ return used - c.Activity.Discounts[0].AmountCents
|
|
|
|
|
+ }
|
|
|
|
|
+ return wantDiscountUsed(c, false)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func wantDiscountUsed(c Case, allLogs bool) int64 {
|
|
|
if len(c.Activity.Discounts) == 0 {
|
|
if len(c.Activity.Discounts) == 0 {
|
|
|
return 0
|
|
return 0
|
|
|
}
|
|
}
|
|
@@ -432,12 +472,18 @@ func wantDiscountUsed(c Case) int64 {
|
|
|
}
|
|
}
|
|
|
used := c.Activity.Discounts[0].UsedCents
|
|
used := c.Activity.Discounts[0].UsedCents
|
|
|
var refundable int64
|
|
var refundable int64
|
|
|
- for _, dl := range c.Activity.DiscountLogs {
|
|
|
|
|
|
|
+ // 游标是 `ORDER BY id DESC` ⇒ **最后插入的那条排第一**。所以"只看第一条"要倒着遍历,
|
|
|
|
|
+ // 顺推会取错那条 —— 第一版我按顺推写,DR-3 的期望就错了 10.00。
|
|
|
|
|
+ logs := c.Activity.DiscountLogs
|
|
|
|
|
+ for i := len(logs) - 1; i >= 0; i-- {
|
|
|
|
|
+ dl := logs[i]
|
|
|
if dl.MoneyCents < 0 {
|
|
if dl.MoneyCents < 0 {
|
|
|
refundable += -dl.MoneyCents
|
|
refundable += -dl.MoneyCents
|
|
|
}
|
|
}
|
|
|
- if refundable > 0 {
|
|
|
|
|
- break // 游标按 id DESC 取**第一条**,所以只看第一条负流水
|
|
|
|
|
|
|
+ // 旧 SP 的游标只取第一条(内层 WHILE 把 cursor 抽干);target 会逐条退。
|
|
|
|
|
+ // `allLogs` 决定算到第几条 —— 这正是 DR-3 要登记的分歧。
|
|
|
|
|
+ if refundable > 0 && !allLogs {
|
|
|
|
|
+ break
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
take := c.AmountCents
|
|
take := c.AmountCents
|
|
@@ -451,3 +497,19 @@ func wantDiscountUsed(c Case) int64 {
|
|
|
func isRefundOp(op Op) bool {
|
|
func isRefundOp(op Op) bool {
|
|
|
return op == OpUserDiscountRefund || op == OpUserDiscountRefundByActivity
|
|
return op == OpUserDiscountRefund || op == OpUserDiscountRefundByActivity
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+// ternaryNote 给"分歧 case"的流水行加一句说明(不是分歧时返回空串)。
|
|
|
|
|
+func ternaryNote(divergent bool) string {
|
|
|
|
|
+ if !divergent {
|
|
|
|
|
+ return ""
|
|
|
|
|
+ }
|
|
|
|
|
+ return "两侧本就该不同(LEGACY-DEFECT:旧 SP 少退 / 超退)⇒ 只登记实际值,不做相等判定"
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ternaryName 给"分歧 case"的额度断言换一个名字 —— 叫"两侧一致"会误导。
|
|
|
|
|
+func ternaryName(divergent bool) string {
|
|
|
|
|
+ if !divergent {
|
|
|
|
|
+ return ""
|
|
|
|
|
+ }
|
|
|
|
|
+ return "额度行 `discount_amount_used`:**per-side 各自对期望**(LEGACY-DEFECT,两侧本就该不同)"
|
|
|
|
|
+}
|