package dualrun import ( "fmt" "math" "sort" "yuansiwei/unified-backend/internal/academic/money" "yuansiwei/unified-backend/internal/academic/service" ) // yuanToCents 元 → 分(四舍五入,负数对称)。 func yuanToCents(v float64) money.Amount { return money.FromCents(centsOf(v)) } func centsOf(v float64) int64 { if v < 0 { return -int64(math.Round(-v * 100)) } return int64(math.Round(v * 100)) } // centsToYuan 分 → 元(仅用于报告展示)。 func centsToYuan(c int64) float64 { return float64(c) / 100 } // ComputeTarget 用**纯函数**算出 target 侧答案。 // // 关键设计(也是必须写进报告的不对称): // - target 侧**不连** yuansiwei_2026、不写库,只调 `service.computeRecordReward` 与 `distributeAmount`; // - 输入全部取自 oracle 侧同一份观测(规则、人数、结算方式); // - **唯一按迁移口径重算的是 income**:`Σ ROUND(cost,2)`(目标态 `DECIMAL(12,2)`), // 而 oracle 侧是 `round(Σ cost, 3)`。这个差异正是三步归因第 1 步要量化的东西。 func ComputeTarget(obs *OracleResult) (*TargetResult, error) { if obs == nil || obs.Inputs.EligibleCount < 0 { return nil, fmt.Errorf("oracle 观测为空") } out := &TargetResult{} // ---- income:按迁移后口径逐行 ROUND 到分再求和 ---- var sumCents int64 for _, s := range obs.StudentsEffective { sumCents += centsOf(s.Cost) } out.Income = centsToYuan(sumCents) // ---- 规则:直接取 oracle 侧命中的同一条(隔离公式,不引入规则选择变量)---- if obs.Quote == nil { // SP 无命中 ⇒ oracle 走退化分支,target 侧按"无方案"处理(3112 语义 → 0) out.Reward = 0 out.PerRow = nil return out, nil } q := &service.PayrollQuote{ Percentage: fmt.Sprintf("%.1f", obs.Quote.Percentage), Min: yuanToCents(obs.Quote.Min), Max: yuanToCents(obs.Quote.Max), Reward: yuanToCents(obs.Quote.Reward), } got, err := service.ComputeRecordRewardForDualRun( q, money.FromCents(sumCents), int32(obs.Inputs.StudentNumber), obs.Inputs.TeacherRewardType) if err != nil { out.Error = err.Error() return out, err } out.Reward = centsToYuan(got.Cents()) // ---- 逐行分摊:只分摊给 type ∈ {1,2},除数 = 该集合大小 ---- eligible := make([]StudentRow, 0, len(obs.StudentsEffective)) for _, s := range obs.StudentsEffective { if s.Type == 1 || s.Type == 2 { eligible = append(eligible, s) } } sort.Slice(eligible, func(i, j int) bool { return eligible[i].StudentID < eligible[j].StudentID }) if n := len(eligible); n > 0 { shares := service.DistributeRecordRewardForDualRun(got, n) for i := range eligible { out.RowStudents = append(out.RowStudents, eligible[i].StudentID) out.PerRow = append(out.PerRow, centsToYuan(shares[i].Cents())) } } return out, nil } // TargetIncomeLegacy 计算"按旧库 3 位精度"的 income(= oracle 侧的收入口径),用于第 1 步对比。 func TargetIncomeLegacy(obs *OracleResult) float64 { if obs == nil { return 0 } var sum float64 for _, s := range obs.StudentsEffective { sum += s.Cost } return math.Round(sum*1000) / 1000 }