核心概念
| 概念 | 落地表 / 字段 | 含义 |
|---|---|---|
| 用户 | public.users | 登录白名单 + 角色 + 部门 + 职位 |
| 竞选 | public.campaigns | 一场公投;ongoing 表示是否接受投票 |
| 票数规则 | campaign_rule_weights | 本场「部门 + 职位」→ 正整数票数;无匹配默认 1 |
| 候选人名簿 | campaign_candidate_options | 展示名列表;投票指向选项 id,不是用户 id |
| 参与 | participation | 用户是否加入本场;isCandidate 对名簿模型已废弃 |
| 选票行 | ballots | 每用户每场一行;weight 是可投总票数快照 |
| 分票 | ballot_allocations | 一张选票拆给多个选项,合计须等于 weight |
权重怎么算
lib/campaign-voter-weight.ts 用用户当前的 department、position 去匹配本场规则(trim 后精确相等)。改规则后 syncBallotWeightsForCampaign 会刷新 ballots.weight,并对已有分票按比例缩放(最大余额法),使分票合计仍等于新权重。
名簿替换
保存候选人名单会 DELETE 本场全部 campaign_candidate_options 再插入。ballot_allocations.candidate_option_id 外键 ON DELETE CASCADE,已投明细会一起消失。生产改名单前必须确认是否已有投票。
遗留字段
campaigns.code、candidateCode、defaultBallots 仍写入数据库,编辑表单以隐藏字段保留旧值,加入与投票主路径不读它们。不要按旧文档实现邀请码流程。