| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278 |
- // Package dualrun 是 AC-15 双跑 harness 的阶段 1 实现。
- //
- // 范围(**刻意收窄**):只比对两条链路
- // - oracle(legacy,PHP 真跑):CoursesRecordHelper::settlementExpenses 结课结算
- // - target(Go,纯函数):service.computeRecordReward + 逐行分摊
- //
- // 不做的:28 个写 SP、活动链、分账链(阶段 2~4)。
- //
- // 两侧**故意不对称**(见 README §1):
- // - oracle 侧真跑 PHP 并写库(事务内,finally 回滚)
- // - target 侧只调纯函数,**不连 yuansiwei_2026**
- //
- // 这个不对称是设计选择,不是遗漏:课酬是"由输入算出一个金额",用纯函数就足以判定,
- // 而且 target 库没有对应历史数据,真跑反而引入无关变量。
- package dualrun
- // Attribution 是差异归因,取值**只有六个**(报告与统计都靠它分类,不得自增)。
- type Attribution string
- const (
- // AttrMatch 两侧一致(按分层判据逐分相等)。
- AttrMatch Attribution = "MATCH"
- // AttrPrecisionConvergence 差异由 income 基数收敛引起(Δreward ≈ Δincome × pct/100)。
- AttrPrecisionConvergence Attribution = "PRECISION-CONVERGENCE"
- // AttrRoundingConvention Δincome = 0 但逐行分摊方式不同(R13:PHP 每行 round(,3) vs Go 分位分配+余数)。
- //
- // 与 AttrPrecisionConvergence 的区别是本质的:前者的 Δincome **恒为 0**,
- // 差异只来自"怎么分";后者 Δincome ≠ 0,差异来自"基数不同"。合并统计会看不清。
- AttrRoundingConvention Attribution = "ROUNDING-CONVENTION"
- // AttrDefect 真缺陷(规则选错 / 差异与基数不成比例 / 公式不一致)。
- AttrDefect Attribution = "DEFECT"
- // AttrOracleDegraded oracle 侧走了退化分支(SP 无命中),不可比。
- AttrOracleDegraded Attribution = "ORACLE-DEGRADED"
- // AttrPathDivergence 数值相同但路径不同(约束 17)。
- AttrPathDivergence Attribution = "PATH-DIVERGENCE"
- // AttrEnvFailure 环境失败(PHP 进程失败 / 守卫不通过 / 数据缺失等),不是业务结论。
- AttrEnvFailure Attribution = "ENV-FAILURE"
- )
- // AllAttributions 供汇总按固定顺序输出六类计数。
- var AllAttributions = []Attribution{
- AttrMatch, AttrPrecisionConvergence, AttrRoundingConvention, AttrDefect,
- AttrOracleDegraded, AttrPathDivergence, AttrEnvFailure,
- }
- // CaseKind 区分真实基线与合成边界。
- type CaseKind string
- const (
- // KindReal 真实基线(从 legacy_oracle 里挑出来的已结课记录)。
- KindReal CaseKind = "real"
- // KindSynthetic 合成边界(事务内造数后跑,或按层级/数值特征定向挑选)。
- KindSynthetic CaseKind = "synthetic"
- )
- // Case 是一个待跑的双跑用例。
- //
- // 字段设计取向:**一切影响结论的输入都显式落在结构体里**,便于 dry-run 审阅与幂等复现。
- type Case struct {
- // ID 稳定可复现的用例 id(real-0001 / syn-tier-cert-1 …),报告以它为主键。
- ID string `json:"id"`
- // Kind 真实 / 合成。
- Kind CaseKind `json:"kind"`
- // Category 用例类别(baseline / tier-* / income-3dp / income-zero-min / supplement-both / nondivisible / rewardtype2)。
- Category string `json:"category"`
- // RecordID t_courses_record.id(varchar(32))。
- RecordID string `json:"recordId"`
- // IsSupplement 传给 settlementExpenses 的第 2 个参数(bool $isSupplement)。
- IsSupplement bool `json:"isSupplement"`
- // BothSupplement 为 true 时,同一 case 会跑 false / true 两遍并各自出结论(类别 supplement-both)。
- BothSupplement bool `json:"bothSupplement"`
- // Mutations 事务内、结算前执行的 SQL(仅合成 case;全部在 legacy_oracle 的事务里,随回滚消失)。
- Mutations []string `json:"mutations,omitempty"`
- // ExpectTier 期望命中的兜底层级(路径证据用;空表示不校验)。
- ExpectTier string `json:"expectTier,omitempty"`
- // Expectation 人类可读的期望说明(dry-run 里给人看)。
- Expectation string `json:"expectation,omitempty"`
- // Note 备注(为什么造这条 case)。
- Note string `json:"note,omitempty"`
- }
- // Rule 是 SP 命中的课酬规则快照(金额单位为元,与旧库 float 列一致)。
- type Rule struct {
- DetailID int64 `json:"detailId"`
- PaymentID int64 `json:"paymentId"`
- Percentage string `json:"percentage"` // "25.0"(百分数)
- Min float64 `json:"min"`
- Max float64 `json:"max"`
- Reward float64 `json:"reward"`
- ClassType int64 `json:"classType"`
- GradeID int64 `json:"gradeId"`
- SubjectID int64 `json:"subjectId"`
- ExpMin float64 `json:"expMin"`
- ExpMax float64 `json:"expMax"`
- TeacherID int64 `json:"teacherId"`
- TeacherCert int64 `json:"teacherCert"`
- ClassifyID int64 `json:"classifyId"`
- }
- // PathEvidence 是"路径证据"(约束 17):数值相等但路径不同必须能被看出来。
- type PathEvidence struct {
- // Tier 命中的兜底层级:teacher-classify / teacher-grade-subject / cert-tier / generic-tier / degraded / none。
- Tier string `json:"tier"`
- // DetailID 命中的规则 id(0 = 未命中)。
- DetailID int64 `json:"detailId"`
- // Degraded oracle 侧是否走了 legacy 退化分支(SP 无命中)。
- Degraded bool `json:"degraded"`
- // IsSupplement 本次执行用的 isSupplement 取值。
- IsSupplement bool `json:"isSupplement"`
- // RewardType 结算方式(1 正常 / 2 固定课酬)。
- RewardType int8 `json:"rewardType"`
- // Recount 是否进入"重写逐行"分支(PHP 的 recountTeacherReward 语义)。
- Recount bool `json:"recount"`
- }
- // OracleResult 是 PHP 探针返回的 oracle 观测值。
- type OracleResult struct {
- OK bool `json:"ok"`
- Error string `json:"error"`
- ErrorDetail string `json:"errorDetail"`
- RecordID string `json:"recordId"`
- IsSupplement bool `json:"isSupplement"`
- Inputs OracleIn `json:"inputs"`
- Quote *RuleRaw `json:"quote"`
- Clock ClockInfo `json:"clock"`
- Mutations []string `json:"mutations"`
- MutationErrs []struct {
- SQL string `json:"sql"`
- Error string `json:"error"`
- } `json:"mutationErrors"`
- Return *bool `json:"return"`
- Exception *OracleError `json:"exception"`
- After *RecordAfter `json:"after"`
- StudentsBefore []StudentRow `json:"studentsBefore"`
- StudentsAfter []StudentRow `json:"studentsAfter"`
- RollbackRestored bool `json:"rollbackRestored"`
- }
- // OracleIn 是 oracle 侧读到的输入。
- type OracleIn struct {
- SchoolID int64 `json:"schoolid"`
- CampusID int64 `json:"campusid"`
- TeacherID int64 `json:"teacherid"`
- CoursesID int64 `json:"coursesid"`
- ClassifyID int64 `json:"classifyid"`
- TeacherExp float64 `json:"teacherExp"`
- TeacherRewardType int8 `json:"teacherRewardType"`
- StudentNumber int `json:"studentNumber"`
- EligibleCount int `json:"eligibleCount"`
- IncomeSumCost3 float64 `json:"incomeSumCost3"`
- StatusBefore int `json:"statusBefore"`
- TeacherRewardBefore float64 `json:"teacherRewardBefore"`
- ActualStartTime string `json:"actualStartTime"`
- }
- // RuleRaw 是探针里 quote 的原始形状(金额为元)。
- type RuleRaw struct {
- DetailID float64 `json:"id"`
- PaymentID float64 `json:"course_paymentid"`
- Percentage float64 `json:"percentage"`
- Min float64 `json:"min_payment"`
- Max float64 `json:"max_payment"`
- Reward float64 `json:"reward"`
- ClassType float64 `json:"class_type"`
- GradeID float64 `json:"gradeid"`
- SubjectID float64 `json:"subjectid"`
- ClassifyID float64 `json:"classifyid"`
- TeacherCert float64 `json:"teacher_cert"`
- ExpMin float64 `json:"teacher_exp_min"`
- ExpMax float64 `json:"teacher_exp_max"`
- }
- // ClockInfo 是冻结时钟的实测证据(A22 用)。
- type ClockInfo struct {
- FrozenEpoch int64 `json:"frozenEpoch"`
- MySQLNow string `json:"now"`
- MySQLUnix string `json:"unixNow"`
- PHPTime int64 `json:"phpTime"`
- PHPDate string `json:"phpDate"`
- TimeZone string `json:"timeZone"`
- }
- // OracleError 是结算抛出的异常(A24 说明它必抛)。
- type OracleError struct {
- Class string `json:"class"`
- Message string `json:"message"`
- File string `json:"file"`
- Line int `json:"line"`
- }
- // RecordAfter 是"异常前已写入"的 record 观测值。
- type RecordAfter struct {
- Status int `json:"status"`
- Income float64 `json:"income"`
- TeacherReward float64 `json:"teacherReward"`
- StudentNumber int `json:"studentNumber"`
- ActualEndTime string `json:"actualEndTime"`
- }
- // StudentRow 是逐行打卡观测值。
- type StudentRow struct {
- StudentID int64 `json:"studentid"`
- Type int `json:"type"`
- Cost float64 `json:"cost"`
- TeacherReward float64 `json:"teacherReward"`
- }
- // TargetResult 是 Go 纯函数算出的 target 侧答案。
- type TargetResult struct {
- Income float64 `json:"income"`
- Reward float64 `json:"reward"`
- PerRow []float64 `json:"perRow"`
- RowStudents []int64 `json:"rowStudents"`
- Error string `json:"error,omitempty"`
- }
- // Step1Income 是三步归因的第一步结果。
- type Step1Income struct {
- Legacy float64 `json:"legacy"` // round(Σcost, 3)
- Target float64 `json:"target"` // Σ ROUND(cost, 2)
- Delta float64 `json:"delta"` // target - legacy
- EqualCents bool `json:"equalCents"` // 分位是否相等
- Explanation string `json:"explanation"`
- }
- // DiffRow 是报告里的一行差异(每条一行)。
- type DiffRow struct {
- CaseID string `json:"caseId"`
- Table string `json:"table"`
- PK string `json:"pk"`
- Column string `json:"column"`
- Legacy string `json:"legacy"`
- Target string `json:"target"`
- Delta string `json:"delta"`
- Attribution Attribution `json:"attribution"`
- }
- // CaseResult 是一条 case 的完整结论(含中间量与路径证据)。
- type CaseResult struct {
- CaseID string `json:"caseId"`
- Category string `json:"category"`
- Kind CaseKind `json:"kind"`
- RecordID string `json:"recordId"`
- IsSupplement bool `json:"isSupplement"`
- Attribution Attribution `json:"attribution"`
- Oracle *OracleResult `json:"oracle,omitempty"`
- Target *TargetResult `json:"target,omitempty"`
- Step1 *Step1Income `json:"step1,omitempty"`
- Rows []DiffRow `json:"rows,omitempty"`
- Note string `json:"note,omitempty"`
- }
- // Report 是整轮双跑的产物(JSON + Markdown 同源)。
- type Report struct {
- Stage string `json:"stage"`
- GeneratedAt string `json:"generatedAt"`
- CaseSet string `json:"caseSet"`
- DryRun bool `json:"dryRun"`
- FrozenEpoch int64 `json:"frozenEpoch"`
- FrozenAt string `json:"frozenAt"`
- Guard map[string]string `json:"guard"`
- Assertions map[string]string `json:"assertions"`
- CaseCounts map[string]int `json:"caseCounts"`
- Summary map[Attribution]int `json:"summary"`
- Cases []CaseResult `json:"cases"`
- DryRunCases []Case `json:"dryRunCases,omitempty"`
- Idempotency []IdempotencyCheck `json:"idempotency,omitempty"`
- MutationNotes []string `json:"mutationNotes,omitempty"`
- }
- // IdempotencyCheck 记录同一 case 连跑两次是否结论一致。
- type IdempotencyCheck struct {
- CaseID string `json:"caseId"`
- First Attribution `json:"first"`
- Second Attribution `json:"second"`
- Same bool `json:"same"`
- RowsEqual bool `json:"rowsEqual"`
- }
|