-- 团体报名的「队员身份」(devdocs/20 的实现)。 -- -- 背景:双打这类团体 / 对抗项目,界面本来就能一次勾多个运动员(EntryPicker),但提交时只把 -- 姓名拼成文字写进 entries.member_names —— 系统只知道「这条报名属于某单位」,不知道队里坐着谁, -- 于是所有配了个人通道的团体项目都给不出个人分、个人奖牌,上不了个人积分榜。 -- -- 表达方式:**队员是报名的人员关联,不是新的一条报名**。一条团体报名 0..N 个队员 -- (0 个 = 只报单位,与升级前逐字节一致)。队员与「队伍」(0025 的 entries.squad)是两个 -- 正交的维度:squad 是「这个单位派的第几支队」,队员是「这支队的队员是谁」,可以叠加。 -- **报名唯一键 (category_id, team_id, squad) WHERE athlete_id IS NULL 一个字都不动**: -- 队员不参与报名去重(同一个人在同一组别的两条报名里出现是允许的,见 devdocs/20 §五)。 -- -- member_names 保留:它是**显示用的文字名单**(导出、大屏、录入页都读它),老数据靠它, -- 也是「人没录档案、只登记不关联」的降级形态。人员身份只认这张表。 -- -- 老数据回填(尽力而为,匹配不上就不关联,绝不硬猜): -- 1) 号码布优先:member_names 里的每一项先按 athletes.bib 认人(0014 有部分唯一索引, -- 号码布是运动员的唯一标识); -- 2) 其次「姓名 + 单位」,且**同单位同名的人只有一个**时才认(重名留给人工确认); -- 3) 其余留空 = 维持升级前的行为(只报单位,上不了个人榜)。 -- 回填只在升级那一刻做一次(写进 schema_migrations),不会在每次启动时重跑。它是 -- INSERT OR IGNORE:万一被重放(schema_migrations 少了一行被补跑),已有的关联——包括人工 -- 确认过的——一行都不会被覆盖或重复,最多补上「还没有的关联」(与 0024 的份额回填同一口径)。 -- 回填只碰**纯团体报名**(athlete_id IS NULL):带运动员的报名本来就带身份,不需要队员。 -- -- 回填之后,配了个人通道(scope=athlete)的老团体项目会开始给队员发个人分 / 个人奖牌 -- ——这正是本单的目的;**单位维度(单位总分、单位奖牌)不受影响**(见 domain 侧的口径与 -- httpapi 的端到端对拍)。只配了团体通道的球类项目(现场绝大多数)一个字节都不变。 CREATE TABLE IF NOT EXISTS entry_athletes ( entry_id INTEGER NOT NULL REFERENCES entries(id) ON DELETE CASCADE, athlete_id INTEGER NOT NULL REFERENCES athletes(id) ON DELETE CASCADE, -- 展示与流水顺序都用它:写侧按请求给的顺序编号,老数据回填按 member_names 里的书写顺序。 -- 名次与积分不依赖顺序(每个队员各拿一整份),它只影响「队里有谁」的排列。 sort_order INTEGER NOT NULL DEFAULT 0, -- 同一条报名里同一个人只能出现一次(重复 = 同一条成绩给同一个人记两份分、两枚牌)。 PRIMARY KEY (entry_id, athlete_id) ); -- 不另建 entry_id 索引:主键 (entry_id, athlete_id) 的索引已经以 entry_id 打头, -- 读侧的「按小项批量取队员」走的正是它(与 0024 的份额表同一个取舍)。 -- athlete_id 上的级联删除(人员被彻底删除时带走关联行)走全表扫描,但这张表只有报名量级。 -- 回填 1:号码布优先。member_names 里写号码布(导入规范允许)时按号码布认人。 -- json_valid 的守卫是必须的:member_names 在库里是 JSON 数组文本,但历史上不排除被手工写成 -- 别的样子;json_each 遇到坏 JSON 会直接报错,那会让整个升级失败、现场开不了机。 -- 坏值按「没有名单」处理('[]'),等价于不回填这条报名。 INSERT OR IGNORE INTO entry_athletes (entry_id, athlete_id, sort_order) SELECT e.id, a.id, je.key FROM entries e JOIN json_each(CASE WHEN json_valid(e.member_names) THEN e.member_names ELSE '[]' END) AS je JOIN athletes a ON a.bib = TRIM(je.value) WHERE e.athlete_id IS NULL AND TRIM(COALESCE(je.value, '')) <> ''; -- 回填 2:姓名 + 单位。同单位同名不止一个时(重名)不认——猜错会把分记到另一个人头上, -- 而人还能事后在界面上补;猜错的分却已经上了榜。 INSERT OR IGNORE INTO entry_athletes (entry_id, athlete_id, sort_order) SELECT e.id, a.id, MIN(je.key) FROM entries e JOIN json_each(CASE WHEN json_valid(e.member_names) THEN e.member_names ELSE '[]' END) AS je JOIN athletes a ON a.name = TRIM(je.value) AND a.team_id = e.team_id WHERE e.athlete_id IS NULL AND TRIM(COALESCE(je.value, '')) <> '' -- 上面那条已经按号码布认过人时不再按姓名认第二遍(同一条报名里同一个人只能一行,主键兜着, -- 这条只是让「认了谁」这件事一眼可读)。 AND NOT EXISTS (SELECT 1 FROM entry_athletes ea WHERE ea.entry_id = e.id AND ea.athlete_id = a.id) AND (SELECT COUNT(*) FROM athletes a2 WHERE a2.name = TRIM(je.value) AND a2.team_id = e.team_id) = 1 GROUP BY e.id, a.id;