package domain // TeamPoints 一个单位的分项汇总;系数只在汇总时折算,历史流水保持原始分。 type TeamPoints struct { Total float64 // 单位总分 TeamOnly float64 // 仅记单位的流水 Individual float64 // 记到个人的流水(原始分,未乘系数) } // SumByTeam 汇总各单位得分:总分 = 仅记单位的流水 + 系数 × 记到个人的流水。 // weight <= 0 时按 1 处理,避免零值把个人分抹掉。 // count_to_team = 0 的流水(如「个人+团体」项目的个人通道)只记在个人头上,不计入单位总分。 // // 刻意不按 EventID 过滤(也不该有人来「补」这条过滤): // - 「event_id 为空」不等于「孤儿流水」。人工调整(source_type='adjustment')本来就允许不挂小项, // 有 event_id 为 NULL 的合法行(写入口 repo_misc.go 的 createAdjustment,由迁移测试钉住), // 一刀切过滤会把合法的调整分一起吞掉; // - 就算再收窄到 source_type='result',过滤也只是「让分悄悄消失」:没有作废人、没有作废原因、 // 没有审计行。积分变动必须可追溯,所以作废只能发生在写侧(如 deleteEvent 在同一事务里 // 按 void_reason 作废),而不是读取时假装看不见; // - 同一个「删掉的小项」在别处仍会被读到:单位积分明细(httpapi/repo_misc.go 的 teamBreakdown) // 是另一条 SQL,过滤这里只会变成「榜上无分、明细有分」的新矛盾。 // // 因此本函数只认调用方给它的流水:要让它少算分,就在写侧把流水作废掉。 func SumByTeam(entries []LedgerEntry, weight float64) map[int64]TeamPoints { if weight <= 0 { weight = 1 } out := make(map[int64]TeamPoints) for _, e := range entries { if e.Voided || !e.CountToTeam { continue } p := out[e.TeamID] if e.AthleteID == 0 { p.TeamOnly += e.Points } else { p.Individual += e.Points } p.Total = p.TeamOnly + weight*p.Individual out[e.TeamID] = p } return out } // SumByAthlete 汇总每个运动员的个人总分(原始分,不折算系数)。 // 不计入单位总分的流水(count_to_team = 0)照样算在个人头上,个人榜不受影响。 // 与 SumByTeam 同口径,同样不按 EventID 过滤(理由见 SumByTeam 的注释)。 func SumByAthlete(entries []LedgerEntry) map[int64]float64 { out := make(map[int64]float64) for _, e := range entries { if e.Voided || e.AthleteID == 0 { continue } out[e.AthleteID] += e.Points } return out }