Article
2026-06-30-襷の系譜-v0.2-数据模型与功能改造技术方案
襷の系譜 V0.2 数据模型与功能改造技术方案
日期:2026-06-30
关联文档:
1. 背景与目标
V0.1 已经完成了以选手为中心的基础资料站骨架,但随着箱根、出云、全日本大学駅伝数据的引入,现有模型开始出现三个明显限制:
Person页面语义过度绑定选手,无法自然承接教练与 staff- 驿传只有“个人区间成绩”,没有“队伍整届结果”和“队伍分段累计状态”
Competition.type仍是字符串,无法稳定驱动驿传类页面和查询
V0.2 的目标不是做复杂数据分析,而是完成一轮 事实层升级,让后续组织页、人物页、届次页都能基于更合理的模型继续增长。
本版目标:
- 把站点从“选手中心”升级为“人物中心”
- 正式支持驿传队伍级结果
- 把模型改造对应地体现在页面功能上
- 先不做荣誉摘要、图表分析、复杂聚合
2. 本版范围
本版纳入:
Person.type收敛为三类:athlete / coach / staffMembership.role正式入模,并与Person.type使用同一组枚举Competition.type从字符串改为正式枚举- 新增驿传队伍级结果模型:
TeamCompetitionResultTeamCompetitionLegSnapshot
- 组织页支持展示
athlete / coach / staff - 组织页支持展示已导入驿传届次成绩
- 届次页支持展示队伍最终成绩和分段累计状态表
- 人物页支持非 athlete 人物成立
本版不纳入:
- 荣誉模型
- 组织赛事摘要实体
- 驿传折线图、名次变化图等分析型可视化
manager独立枚举監督 / 主務 / 学生スタッフ等更细岗位枚举- 复杂头衔体系
3. 核心建模原则
3.1 Person 是统一人物实体
Person 不再被视作“选手表”,而是统一人物实体。
选手、教练、staff 共用同一实体层。
3.2 Membership 承载组织中的具体身份
一个人是否是选手、教练还是 staff,真实事实以 Membership.role 为准。
Person.type 保留,但它是:
- 站内人物主分类字段
- 派生字段
- 不要求人工维护
3.3 驿传分为个人层与队伍层
驿传相关事实必须拆成三层:
CompetitionEdition:整届赛事Race:单区- 队伍结果层:
TeamCompetitionResultTeamCompetitionLegSnapshot
规则:
- 个人区间成绩挂在
RaceResult - 队伍整届最终结果挂在
CompetitionEdition - 队伍每段结束后的累计状态也挂在
CompetitionEdition语义之下
3.4 先补事实层,再做摘要层
V0.2 只补齐事实模型,不引入荣誉、摘要、分析等二次聚合层。
组织页后续若要扩展摘要,应基于本版新增的队伍结果事实层派生。
4. 数据模型改造
4.1 枚举调整
4.1.1 PersonType
将 PersonType 收敛为:
enum PersonType {
athlete
coach
staff
}
说明:
- 现有
manager并入staff - 现有
coach保留 - 现有
athlete保留
4.1.2 MembershipRole
新增:
enum MembershipRole {
athlete
coach
staff
}
说明:
- V0.2 中
Membership.role与Person.type保持同一组值 - 语义上两者不同:
Person.type是主类型Membership.role是该段组织关系中的身份事实
4.1.3 CompetitionType
新增:
enum CompetitionType {
university_ekiden
high_school_ekiden
corporate_ekiden
mixed_ekiden
track_meet
road_race
marathon
}
说明:
Competition.type从String改为CompetitionType- 保留
Race.discipline.ekiden_leg - 两者分别承担赛事级和比赛单元级语义
4.2 现有模型字段变更
4.2.1 Person
保留现有主结构,type 调整为新的三值枚举:
model Person {
id String @id @default(cuid())
slug String @unique
displayNameJa String
displayNameKana String?
displayNameRoman String?
displayNameZh String?
displayNameEn String?
birthDate DateTime?
hometown String?
nationality String?
heightCm Int?
weightKg Int?
type PersonType @default(athlete)
status DataStatus @default(pending)
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
设计结论:
- 不新增“当前角色”冗余字段
type继续保留type统一由 membership 推导并回填
4.2.2 Membership
在现有 Membership 基础上增加 role:
model Membership {
id String @id @default(cuid())
personId String
organizationId String
type MembershipType
role MembershipRole @default(athlete)
startDate DateTime?
endDate DateTime?
startYear Int?
endYear Int?
faculty String?
department String?
status DataStatus @default(pending)
notes String?
sourceId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
设计结论:
Membership.type保留,继续表示origin / enrolled / affiliated / graduated / staff这类关系类型Membership.role新增,表示该段关系中的组织身份- 这样可以同时表达:
type = enrolled, role = athletetype = affiliated, role = coachtype = staff, role = staff
4.3 Competition 模型变更
将 Competition.type 从字符串调整为枚举:
model Competition {
id String @id @default(cuid())
slug String @unique
nameJa String
nameKana String?
nameRoman String?
nameZh String?
nameEn String?
type CompetitionType?
organizer String?
region String?
websiteUrl String?
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
说明:
- 允许暂时为
null,便于兼容旧数据和分批回填 - 完成历史数据回填后,可在后续版本评估是否改为必填
4.4 新增驿传队伍级结果模型
4.4.1 TeamCompetitionResult
表示某支队伍在某届赛事中的最终结果。
model TeamCompetitionResult {
id String @id @default(cuid())
competitionEditionId String
organizationId String
finalRank Int?
finalMark String?
finalMarkMillis Int?
status DataStatus @default(pending)
notes String?
sourceId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
competitionEdition CompetitionEdition @relation(fields: [competitionEditionId], references: [id], onDelete: Cascade)
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
source Source? @relation(fields: [sourceId], references: [id])
legSnapshots TeamCompetitionLegSnapshot[]
@@unique([competitionEditionId, organizationId])
@@index([competitionEditionId, finalRank])
@@index([organizationId, competitionEditionId])
}
设计结论:
- 只负责整届最终结果
- 不放区间跑者、区间成绩等字段
competitionEditionId + organizationId必须唯一
4.4.2 TeamCompetitionLegSnapshot
表示某支队伍在某届赛事跑完第 N 区时的累计状态。
model TeamCompetitionLegSnapshot {
id String @id @default(cuid())
teamCompetitionResultId String
leg Int
cumulativeRank Int?
cumulativeMark String?
cumulativeMarkMillis Int?
gapFromLeader String?
gapFromLeaderMillis Int?
status DataStatus @default(pending)
notes String?
sourceId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
teamCompetitionResult TeamCompetitionResult @relation(fields: [teamCompetitionResultId], references: [id], onDelete: Cascade)
source Source? @relation(fields: [sourceId], references: [id])
@@unique([teamCompetitionResultId, leg])
@@index([leg, cumulativeRank])
}
设计结论:
- 只负责累计状态
- 不冗余存储区间跑者和区间个人成绩
- 页面要看“这一段谁跑”,通过
RaceResult联查
4.4.3 与旧字段的关系
RaceResult.teamRank 先保留,但定位改为:
- 兼容字段
- 历史数据过渡字段
- 不再作为驿传队伍结果的核心事实来源
后续新功能应优先使用:
TeamCompetitionResult.finalRankTeamCompetitionLegSnapshot.cumulativeRank
5. 迁移与回填策略
V0.2 先改模型,再逐步接功能,兼容以“可平滑过渡”为目标,不以长期保留旧语义为目标。
5.1 PersonType 收敛
迁移规则:
- 现有
athlete保持不变 - 现有
coach保持不变 - 现有
manager映射为staff - 现有
staff保持不变
5.2 Membership.role 回填
由于当前库里的主体人物几乎都是选手,首轮迁移可采用保守策略:
- 所有现有 membership 默认回填为
role = athlete
后续新增教练、staff 数据时,再按导入源写入真实 role。
5.3 Person.type 重算
新增一份统一的 Person.type 重算逻辑,规则如下:
- 优先取当前有效 membership
- 如果没有当前有效 membership,取结束时间最近的一段 membership
- 如果仍无法判断,则保留现值;若现值为空,则回退为
athlete
“当前有效 membership” 判定:
startDate为空或早于当前日期- 且
endDate为空或晚于当前日期
若同一人物存在多个当前有效 membership,优先级:
coachstaffathlete
说明:
- 此优先级用于推导站内主类型
- 不改变真实历史关系事实
5.4 Competition.type 回填
已知赛事优先回填:
- 箱根駅伝:
university_ekiden - 出雲駅伝:
university_ekiden - 全日本大学駅伝:
university_ekiden
后续可按新增赛事逐步补齐。
5.5 驿传队伍结果回填
V0.2 上线前,不要求一次性把所有历史届次的队伍级结果回填完整。
建议策略:
- 先为已有完整箱根届次回填
TeamCompetitionResult - 再逐届补
TeamCompetitionLegSnapshot - 出雲、全日本大学駅伝在正式导入时直接走新模型
允许阶段性存在:
RaceResult已完整TeamCompetitionResult部分完成TeamCompetitionLegSnapshot尚未完成
页面功能应基于“有数据才展示”的策略渐进启用。
6. 功能改造方案
6.1 组织页
组织页从“成员列表页”升级为“组织资料页”。
6.1.1 Current People
展示当前有效 membership,按 role 分组:
- Athletes
- Coaches
- Staff
展示字段:
- 人物名
- 所属起止时间
- 人物详情页入口
6.1.2 Former People
展示非当前 membership,同样按 role 分组:
- Athletes
- Coaches
- Staff
6.1.3 Ekiden Results
仅展示当前已有数据、且所属赛事 Competition.type 为驿传类型的结果。
首版展示字段:
- 赛事名
- 届次名
- 最终排名
- 总成绩
- 届次详情页入口
展示来源:
TeamCompetitionResult
不做:
- 历史摘要
- 冠军次数
- 趋势图
6.2 人物页
人物页不再默认假设所有人都是 athlete。
6.2.1 athlete 页面
保留并增强现有内容:
- 所属履历
- PB
- 比赛结果
- 相关人物
6.2.2 coach / staff 页面
首版保持克制,重点展示:
- 人物主信息
- 所属履历
- 关联组织
- 相关赛事痕迹,若已有可查询数据
不做:
- 独立“执教战绩分析”
- 复杂教练专题模块
6.2.3 路由策略
V0.2 不强制把 /players 全量改名为 /people。
建议:
- 保留现有
/players路由,优先保证兼容上线 - 文案层逐步从“选手”过渡到“人物 / 选手”
- 后续若决定做
/people,再单独规划 SEO 和路径迁移
6.3 届次页 / 驿传详情页
如果届次属于驿传类型,应支持展示队伍级结果。
6.3.1 队伍最终结果表
展示来源:
TeamCompetitionResult
首版字段:
- 学校
- 最终排名
- 总成绩
6.3.2 队伍分段累计状态表
展示来源:
TeamCompetitionLegSnapshot
首版字段:
- 第几区
- 累计名次
- 累计成绩
- 与首位差
不做:
- 图表
- 折线图
- 多队对比分析
7. 查询层与接口建议
7.1 组织页查询
组织页建议拆成三组查询:
memberships按当前/历史分组memberships按role分组teamCompetitionResults取已导入驿传成绩
建议不要继续只按“current players / former players”查询。
7.2 人物页查询
人物页查询需支持:
Person.typememberships.role- 非 athlete 页面在没有 PB 时正常成立
PB 和比赛结果区块可以根据 Person.type 与是否有数据决定展示。
7.3 驿传届次页查询
建议按 CompetitionEdition 聚合:
- 拉取
CompetitionEdition - 拉取关联
TeamCompetitionResult - 为每个
TeamCompetitionResult拉取legSnapshots - 如需联查某段个人成绩,再从
Race -> RaceResult查询
8. 实施顺序
建议按以下顺序推进:
Phase 1:Schema 改造
- 调整
PersonType - 新增
MembershipRole - 新增
CompetitionType - 修改
Competition.type - 新增
Membership.role - 新增:
TeamCompetitionResultTeamCompetitionLegSnapshot
Phase 2:数据迁移与重算脚本
- 回填
Membership.role - 回填
Person.type - 回填
Competition.type - 为已有箱根届次补一批队伍结果
Phase 3:查询层改造
- 组织页查询改造
- 人物页查询改造
- 驿传届次页查询落地
Phase 4:页面改造
- 组织页
- 人物页
- 届次页 / 驿传详情页
9. 风险与约束
9.1 历史数据会阶段性不完整
V0.2 上线后,一段时间内可能出现:
- 个人区间成绩齐全
- 队伍结果层尚未补齐
这是允许的。页面按有数据展示即可。
9.2 Person.type 是派生字段,不应变成人工心智负担
一旦仍要求导入时手工维护 Person.type,模型升级价值会大打折扣。
因此必须配套统一重算逻辑。
9.3 RaceResult.teamRank 会进入过渡期
短期保留它有助于平滑迁移,但长期不应继续让它承担队伍结果层职责。
10. 本版交付结果
V0.2 完成后,站点应具备以下能力:
- 正式支持
athlete / coach / staff - 组织页可以展示不同角色的人物关系
- 驿传可以展示队伍整届结果
- 驿传可以展示每段累计状态表
- 人物页不再被选手语义绑死
- 后续图表、摘要、荣誉、分析都能基于更稳定的事实层继续扩展