|
@@ -497,83 +497,114 @@ func BuildSyntheticCases(records []recordRow, r *refs, perCat int, now time.Time
|
|
|
return got
|
|
return got
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // ---- 类 1:四级兜底(5 条,覆盖四级)----
|
|
|
|
|
- // teacher-grade-subject ×1 / generic-tier ×1 / teacher-classify ×2(造数)/ cert-tier ×1(造数)
|
|
|
|
|
- gsPool := r.collect(records, now, func(f caseFacts) bool { return baseOK(f) && f.tier == TierTeacherGradeSubject })
|
|
|
|
|
- genPool := r.collect(records, now, func(f caseFacts) bool { return baseOK(f) && f.tier == TierGeneric })
|
|
|
|
|
- // 造 classify 级:需要该方案下无老师专属规则(oneself==0 也可,但插入后即 >0),
|
|
|
|
|
- // 用 generic 池的记录最稳(说明当前没有老师专属规则能命中它)
|
|
|
|
|
- classifyPool := r.collect(records, now, func(f caseFacts) bool {
|
|
|
|
|
- return baseOK(f) && f.tier == TierGeneric && f.cls != nil && f.rec.teacherid > 0
|
|
|
|
|
- })
|
|
|
|
|
- certPool := r.collect(records, now, func(f caseFacts) bool {
|
|
|
|
|
- return baseOK(f) && f.tier == TierGeneric && f.teacher.cert == 0 && f.cls != nil
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
add := func(c Case) { out = append(out, c) }
|
|
add := func(c Case) { out = append(out, c) }
|
|
|
|
|
|
|
|
- if got := take(gsPool, 1); len(got) == 1 {
|
|
|
|
|
- f := got[0]
|
|
|
|
|
- add(Case{ID: "syn-tier-grade-subject-1", Kind: KindSynthetic, Category: CatTier, RecordID: f.rec.id,
|
|
|
|
|
- ExpectTier: TierTeacherGradeSubject,
|
|
|
|
|
- Expectation: "现成记录即命中 teacher-grade-subject(无需造数)",
|
|
|
|
|
- Note: "四级兜底之一"})
|
|
|
|
|
- } else {
|
|
|
|
|
- notes = append(notes, "tier-teacher-grade-subject:无法造数(无现成命中记录)")
|
|
|
|
|
- }
|
|
|
|
|
- if got := take(genPool, 1); len(got) == 1 {
|
|
|
|
|
- f := got[0]
|
|
|
|
|
- add(Case{ID: "syn-tier-generic-1", Kind: KindSynthetic, Category: CatTier, RecordID: f.rec.id,
|
|
|
|
|
- ExpectTier: TierGeneric,
|
|
|
|
|
- Expectation: "现成记录即命中 generic-tier(无需造数)",
|
|
|
|
|
- Note: "四级兜底之一"})
|
|
|
|
|
- } else {
|
|
|
|
|
- notes = append(notes, "tier-generic:无法造数(无现成命中记录)")
|
|
|
|
|
- }
|
|
|
|
|
- // teacher-classify ×2:插入一条 classifyid=当前分类 的老师专属规则
|
|
|
|
|
- for i, f := range take(classifyPool, 2) {
|
|
|
|
|
- payid := r.resolvePayment(f.rec.schoolid, f.rec.campusid, f.rec.startTime)
|
|
|
|
|
- add(Case{
|
|
|
|
|
- ID: fmt.Sprintf("syn-tier-teacher-classify-%d", i+1), Kind: KindSynthetic, Category: CatTier,
|
|
|
|
|
- RecordID: f.rec.id, ExpectTier: TierTeacherClassify,
|
|
|
|
|
- Mutations: []string{insertRuleSQL(payid, f.cls.classType, f.cls.id, f.rec.teacherid, 0, 33.0, 0, 0, 0, 0, 99, "ac15-syn-classify")},
|
|
|
|
|
- Expectation: "造一条 teacherid=该老师 且 classifyid=该分类 的规则 ⇒ 应命中 teacher-classify",
|
|
|
|
|
- Note: "真实数据里该级命中数为 0,必须造数(P0 §13)",
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ // ---- 类 1:四级兜底,**每级各 perCat 条**(默认 5 × 4 = 20 条)----
|
|
|
|
|
+ //
|
|
|
|
|
+ // 为什么要每级 5 条而不是凑 5 条:四级是"最担心的未验证路径"(真实数据里两级 0 命中),
|
|
|
|
|
+ // 造数成本几乎为零;而 1~2 条测不出「有 min 无 max / 无 min 无 max / 有 max 无 min」这类组合差异 ——
|
|
|
|
|
+ // `min_payment=0 表示不设限` 这个语义正是 D10 踩过的坑。
|
|
|
|
|
+ tierSpecs := []struct {
|
|
|
|
|
+ tier string
|
|
|
|
|
+ pred func(f caseFacts) bool
|
|
|
|
|
+ note string
|
|
|
|
|
+ }{
|
|
|
|
|
+ {TierTeacherGradeSubject, func(f caseFacts) bool {
|
|
|
|
|
+ return baseOK(f) && f.tier == TierTeacherGradeSubject
|
|
|
|
|
+ }, "现成记录即命中 teacher-grade-subject(无需造数)"},
|
|
|
|
|
+ {TierGeneric, func(f caseFacts) bool {
|
|
|
|
|
+ return baseOK(f) && f.tier == TierGeneric
|
|
|
|
|
+ }, "现成记录即命中 generic-tier(无需造数)"},
|
|
|
|
|
+ {TierTeacherClassify, func(f caseFacts) bool {
|
|
|
|
|
+ return baseOK(f) && f.tier == TierGeneric && f.cls != nil && f.rec.teacherid > 0
|
|
|
|
|
+ }, "造一条 teacherid=该老师 且 classifyid=该分类 的规则 ⇒ 应命中 teacher-classify"},
|
|
|
|
|
+ {TierCert, func(f caseFacts) bool {
|
|
|
|
|
+ return baseOK(f) && f.tier == TierGeneric && f.teacher.cert == 0 && f.cls != nil
|
|
|
|
|
+ }, "老师置持证 + 造 teacher_cert=1 且教龄 0~99 的通用规则 ⇒ 应命中 cert-tier"},
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, spec := range tierSpecs {
|
|
|
|
|
+ pool := r.collect(records, now, spec.pred)
|
|
|
|
|
+ got := take(pool, perCat)
|
|
|
|
|
+ if len(got) < perCat {
|
|
|
|
|
+ notes = append(notes, fmt.Sprintf("tier-%s:候选只有 %d 条(要求 %d)", spec.tier, len(got), perCat))
|
|
|
|
|
+ }
|
|
|
|
|
+ for i, f := range got {
|
|
|
|
|
+ c := Case{
|
|
|
|
|
+ ID: fmt.Sprintf("syn-tier-%s-%d", strings.ReplaceAll(spec.tier, "-", "_"), i+1),
|
|
|
|
|
+ Kind: KindSynthetic, Category: CatTier, RecordID: f.rec.id,
|
|
|
|
|
+ ExpectTier: spec.tier, Expectation: spec.note,
|
|
|
|
|
+ Note: "四级兜底之一(P0 §13:classify / cert 真实命中数为 0,必须造数)",
|
|
|
|
|
+ }
|
|
|
|
|
+ payid := r.resolvePayment(f.rec.schoolid, f.rec.campusid, f.rec.startTime)
|
|
|
|
|
+ switch spec.tier {
|
|
|
|
|
+ case TierTeacherClassify:
|
|
|
|
|
+ // 三种 min/max 组合轮转,覆盖「只设 min」「只设 max」「都不设」
|
|
|
|
|
+ minPay, maxPay := 0.0, 0.0
|
|
|
|
|
+ switch i % 3 {
|
|
|
|
|
+ case 0:
|
|
|
|
|
+ minPay = 80.0
|
|
|
|
|
+ case 1:
|
|
|
|
|
+ maxPay = 260.0
|
|
|
|
|
+ case 2:
|
|
|
|
|
+ minPay, maxPay = 90.0, 240.0
|
|
|
|
|
+ }
|
|
|
|
|
+ c.Mutations = []string{insertRuleSQL(payid, f.cls.classType, f.cls.id, f.rec.teacherid, 0,
|
|
|
|
|
+ 33.0, minPay, maxPay, 0, 0, 99, "ac15-syn-classify")}
|
|
|
|
|
+ c.Expectation += fmt.Sprintf("(min=%.0f max=%.0f)", minPay, maxPay)
|
|
|
|
|
+ case TierCert:
|
|
|
|
|
+ minPay, maxPay := 0.0, 0.0
|
|
|
|
|
+ switch i % 3 {
|
|
|
|
|
+ case 0:
|
|
|
|
|
+ minPay = 70.0
|
|
|
|
|
+ case 1:
|
|
|
|
|
+ maxPay = 300.0
|
|
|
|
|
+ case 2:
|
|
|
|
|
+ minPay, maxPay = 60.0, 280.0
|
|
|
|
|
+ }
|
|
|
|
|
+ c.Mutations = []string{
|
|
|
|
|
+ fmt.Sprintf("UPDATE t_teacher_information SET teacher_cert = b'1' WHERE teacherid = %d", f.rec.teacherid),
|
|
|
|
|
+ insertRuleSQL(payid, f.cls.classType, 0, 0, 1, 44.0, minPay, maxPay, 0, 0, 99, "ac15-syn-cert"),
|
|
|
|
|
+ }
|
|
|
|
|
+ c.Expectation += fmt.Sprintf("(min=%.0f max=%.0f)", minPay, maxPay)
|
|
|
|
|
+ }
|
|
|
|
|
+ add(c)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- if len(classifyPool) < 2 {
|
|
|
|
|
- notes = append(notes, fmt.Sprintf("tier-teacher-classify:候选只有 %d 条,不足 2 条", len(classifyPool)))
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // ---- 类 2:income 带 3 位小数,**五种尾数各一条**----
|
|
|
|
|
+ //
|
|
|
|
|
+ // 五个尾数分别对应五种收敛方向,缺一种就等于没测:
|
|
|
|
|
+ // .333 舍 / .667 入 / .999 跨整数进位 / .005 恰好半分 / .347 保留原样
|
|
|
|
|
+ tails := []struct {
|
|
|
|
|
+ total float64
|
|
|
|
|
+ tail string
|
|
|
|
|
+ why string
|
|
|
|
|
+ }{
|
|
|
|
|
+ {100.333, "333", "舍(100.333 → 100.33)"},
|
|
|
|
|
+ {200.667, "667", "入(200.667 → 200.67)"},
|
|
|
|
|
+ {299.999, "999", "跨整数进位(299.999 → 300.00),Δincome=+0.001 最易漏"},
|
|
|
|
|
+ {300.005, "005", "恰好半分(300.005 → 300.01),四舍五入边界"},
|
|
|
|
|
+ {349.347, "347", "保留原样(349.347 → 349.35)"},
|
|
|
|
|
+ }
|
|
|
|
|
+ pool3dpBase := r.collect(records, now, func(f caseFacts) bool {
|
|
|
|
|
+ return baseOK(f) && f.agg.elig >= 1
|
|
|
|
|
+ })
|
|
|
|
|
+ got3dp := take(pool3dpBase, len(tails))
|
|
|
|
|
+ if len(got3dp) < len(tails) {
|
|
|
|
|
+ notes = append(notes, fmt.Sprintf("income-3dp:候选只有 %d 条(要求 %d)", len(got3dp), len(tails)))
|
|
|
}
|
|
}
|
|
|
- // cert-tier ×1:把老师置为持证 + 插入 teacher_cert=1 的通用档规则
|
|
|
|
|
- if got := take(certPool, 1); len(got) == 1 {
|
|
|
|
|
- f := got[0]
|
|
|
|
|
- payid := r.resolvePayment(f.rec.schoolid, f.rec.campusid, f.rec.startTime)
|
|
|
|
|
|
|
+ for i, f := range got3dp {
|
|
|
|
|
+ t := tails[i]
|
|
|
add(Case{
|
|
add(Case{
|
|
|
- ID: "syn-tier-cert-1", Kind: KindSynthetic, Category: CatTier, RecordID: f.rec.id,
|
|
|
|
|
- ExpectTier: TierCert,
|
|
|
|
|
|
|
+ ID: fmt.Sprintf("syn-income-3dp-%s", t.tail), Kind: KindSynthetic, Category: CatIncome3DP,
|
|
|
|
|
+ RecordID: f.rec.id, ExpectTier: f.tier,
|
|
|
Mutations: []string{
|
|
Mutations: []string{
|
|
|
- fmt.Sprintf("UPDATE t_teacher_information SET teacher_cert = b'1' WHERE teacherid = %d", f.rec.teacherid),
|
|
|
|
|
- insertRuleSQL(payid, f.cls.classType, 0, 0, 1, 44.0, 0, 0, 0, 0, 99, "ac15-syn-cert"),
|
|
|
|
|
|
|
+ fmt.Sprintf("UPDATE t_courses_record_student SET cost = 0 WHERE recordid = '%s'", f.rec.id),
|
|
|
|
|
+ fmt.Sprintf("UPDATE t_courses_record_student SET cost = %.3f WHERE recordid = '%s' AND type IN (1,2) ORDER BY studentid LIMIT 1", t.total, f.rec.id),
|
|
|
},
|
|
},
|
|
|
- Expectation: "老师置持证 + 造 teacher_cert=1 且教龄 0~99 的通用规则 ⇒ 应命中 cert-tier",
|
|
|
|
|
- Note: "真实数据里该级命中数为 0,必须造数(P0 §13)",
|
|
|
|
|
|
|
+ Expectation: fmt.Sprintf("事务内把 income 钉成 %.3f(尾数 .%s):%s ⇒ 迁移后 ΣROUND(cost,2) 与 legacy round(Σ,3) 必不同", t.total, t.tail, t.why),
|
|
|
|
|
+ Note: "专门验证 PRECISION-CONVERGENCE 的五个收敛方向",
|
|
|
})
|
|
})
|
|
|
- } else {
|
|
|
|
|
- notes = append(notes, "tier-cert:无法造数(找不到「generic 命中 + 老师未持证」的记录)")
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ---- 类 2:income 带 3 位小数 ----
|
|
|
|
|
- pool3dp := r.collect(records, now, func(f caseFacts) bool {
|
|
|
|
|
- return baseOK(f) && hasThirdDecimal(f.agg.sumCost)
|
|
|
|
|
- })
|
|
|
|
|
- for i, f := range take(pool3dp, perCat) {
|
|
|
|
|
- add(Case{ID: fmt.Sprintf("syn-income-3dp-%d", i+1), Kind: KindSynthetic, Category: CatIncome3DP,
|
|
|
|
|
- RecordID: f.rec.id, ExpectTier: f.tier,
|
|
|
|
|
- Expectation: fmt.Sprintf("Σcost=%.3f 有第三位小数 ⇒ 迁移后 ΣROUND(cost,2) 会变,用于验证 Δincome 归因", f.agg.sumCost),
|
|
|
|
|
- Note: "专门验证 PRECISION-CONVERGENCE"})
|
|
|
|
|
- }
|
|
|
|
|
- if len(pool3dp) < perCat {
|
|
|
|
|
- notes = append(notes, fmt.Sprintf("income-3dp:候选只有 %d 条", len(pool3dp)))
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ---- 类 3:income=0 但 student_number>0(min_payment 夹取)----
|
|
// ---- 类 3:income=0 但 student_number>0(min_payment 夹取)----
|