| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798 |
- 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.StudentsBefore {
- 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.StudentsBefore))
- for _, s := range obs.StudentsBefore {
- 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.StudentsBefore {
- sum += s.Cost
- }
- return math.Round(sum*1000) / 1000
- }
|