Article

2026-06-27-襷の系譜-v0.1-技术方案

个人项目

襷の系譜 V0.1 技术方案

日期:2026-06-27

关联文档:2026-06-27-襷の系譜-v0.1-PRD.md

更新记录:

  • 2026-06-28:补充 V0.1 P0 数据范围、所属履历时间线模型、四层赛事模型、审计模型和 V0.2 边界。
  • 2026-06-28:明确 V0.1 实际部署方案:先与个人站点共存在 ubuntu-3,通过目录、服务、数据库、反向代理和备份隔离,保留后续迁移到 ubuntu-4 的路径。
  • 2026-06-28:新增独立部署方案文档,明确后续采用与 personal-site 一致的 GitHub Actions 镜像归档部署,并单独讨论数据同步策略。
  • 2026-06-29:调整真实数据更新路线,明确 V0.1 采用单库直连更新,seed 退出真实数据日常维护主链路。
  • 2026-06-29:补充姓名标准化规则、来源抽样要求和存量重复治理原则。

1. 技术目标

V0.1 的技术目标是支撑一个小而可信的选手资料站:

  • 以选手档案为核心页面
  • 支持日文主体验,中文和英文保留界面框架
  • 支持青山学院大学、國學院大學、中央大学的代表样本数据录入
  • 支持 P0 数据:基础资料、所属履历、当前 PB、箱根记录、来源、确认状态
  • 支持从选手跳转到同高中、同大学、同当前所属的相关选手
  • 支持后续扩展到更多大学、高中、实业团、赛事和成绩库

V0.1 不追求复杂架构。优先级是:

  1. 数据模型清楚
  2. 数据可维护
  3. 页面可读
  4. 来源可追溯
  5. 后续可演进

2. 技术栈选择

2.1 应用框架

采用:

  • Next.js
  • React
  • TypeScript
  • App Router

理由:

  • 前后端一体,适合个人项目快速推进
  • SSR/SSG 能支持资料站和 SEO
  • API Route / Server Action 足够支撑后台管理和数据查询
  • TypeScript 有利于长期维护数据结构
  • 后续部署到 Vercel、自有服务器或 Docker 都比较顺滑

2.2 样式与组件

采用:

  • Tailwind CSS
  • shadcn/ui
  • lucide-react

理由:

  • 适合快速搭建克制、清爽、资料馆风格的界面
  • 表格、表单、弹窗、标签、Tabs 等基础组件容易复用
  • lucide-react 能满足搜索、筛选、外链、语言切换、来源等图标需求

视觉方向:

Archive, not Dashboard.

界面应像现代化选手名鉴和资料馆,不做新闻站,也不做复杂数据大屏。

2.3 数据库

采用:

  • PostgreSQL
  • Prisma ORM

理由:

  • 项目早期最重要的是实体、关系、来源、状态和约束
  • PostgreSQL 适合做主业务库
  • Prisma 能提供清晰 schema、迁移、类型生成和开发体验
  • 后续可以平滑增加全文搜索、物化视图、分析同步等能力

明确不在 V0.1 使用:

  • Neo4j
  • ClickHouse
  • BigQuery
  • 独立数据仓库
  • 独立图数据库

关系图和关系跳转在早期完全可以由 PostgreSQL 查询支撑。

2.4 版本范围决策

V0.1 只做“选手核心档案”,不做完整比赛库。

P0 数据范围:

  • 姓名
  • 读音
  • 罗马字
  • 生年月日
  • 出身地
  • 高中
  • 大学
  • 当前所属,若已毕业或转入实业团
  • 5000m PB
  • 10000m PB
  • 半马 PB
  • 马拉松 PB,若有
  • 箱根出场记录
  • 数据来源
  • 数据确认状态

V0.1 不做:

  • PB 历史
  • 2nd best
  • 全量比赛成绩
  • 出雲駅伝、全日本大学駅伝系统收录
  • 普通田径比赛成绩库
  • SNS、趣味、座右铭、Q&A 等粉丝向字段
  • 复杂关系图

V0.2 再开始建设“比赛结果库”,收录每场比赛数据,并为关系图提供更细粒度的比赛关系。

3. 架构概览

V0.1 架构:

Next.js App
  ├─ Public Pages
  │   ├─ Home
  │   ├─ Player List
  │   ├─ Player Detail
  │   └─ Search
  │
  ├─ Admin Pages
  │   ├─ Player Management
  │   ├─ Organization Management
  │   ├─ Record Management
  │   └─ Source Management
  │
  ├─ Server Actions / API Routes
  │   ├─ Query public data
  │   ├─ Mutate admin data
  │   └─ Maintain seed/sample data
  │
  └─ PostgreSQL
      ├─ Person
      ├─ Organization
      ├─ Membership
      ├─ Competition
      ├─ CompetitionEdition
      ├─ Race
      ├─ RaceResult
      ├─ PersonalBest
      ├─ Source
      ├─ AuditLog
      └─ Translation / Alias

部署早期可以采用单体应用:

Next.js + PostgreSQL

无需拆分独立后端服务。

4. 数据建模原则

4.1 宽收集,窄展示

V0.1 页面展示可以很克制,但数据收集应尽量完整。

原因:

  • 大学官网、箱根官方、NTV、月陸、粉丝站提供的字段各不相同
  • 部分字段当前不用,后续可能成为关系探索、筛选或专题内容的基础
  • 对个人项目来说,早期字段略多的成本低于后期反复补历史数据

原则:

  • 能稳定确认的公开事实可以收
  • 不确定字段可以收,但必须标注状态
  • 粉丝向字段可以收,但默认不作为核心展示
  • 每条重要数据尽量关联来源

4.2 关系优先于页面

数据模型应优先表达实体之间的关系,而不是只服务当前页面。

重点关系:

  • 选手属于大学
  • 选手出身高中
  • 选手曾经属于某组织
  • 选手参加某届赛事
  • 选手在某区间有成绩
  • 选手拥有某项目 PB
  • 数据来自某个来源

V0.1 的页面很少,但模型要能支撑:

  • 同大学
  • 同高中
  • 同当前所属
  • 所属履历时间线
  • 高中到大学流向
  • 大学到实业团流向
  • 赛事出场历史

核心约定:

  • Person 表示人物/选手。
  • Organization 统一表示高中、大学、实业团、俱乐部、公司、联盟等组织。
  • Membership 表示“所属履历/生涯所属”,即某人在某时间段与某组织的关系。
  • 高中、大学、实业团不直接写死在 Person 上,而是通过 Membership 串联。
  • 当前所属通过 Membership.endDate = nullendDate >= today 判断。

4.3 来源和确认状态是一等数据

数据站的可信度来自来源和校验,而不是字段数量。

所有关键事实都应支持:

  • 来源 URL
  • 来源名称
  • 来源类型
  • 抓取或录入日期
  • 确认状态
  • 备注

建议确认状态:

  • verified:已确认
  • pending:待确认
  • conflicting:存在冲突
  • missing:缺失

4.4 多语言以原文为核心

日文是主语言和事实原文。

多语言策略:

  • 日文名称作为主名称
  • 中文和英文作为翻译或辅助展示
  • 罗马字、读音、旧字体、简称、媒体常见写法作为 name variant 管理
  • 不要求 V0.1 翻译所有数据

<<<<<<< HEAD

4.5 审计优先于事件流

=======

4.5 姓名标准化规则

人物姓名相关字段需要明确分工,避免继续出现“同一人因罗马字顺序不同被创建两次”的问题。

统一规则:

  • displayNameJa:以日文原文为主,采用日文常规顺序,保存为“姓 名”,姓名中间统一使用单个半角空格。
  • displayNameRoman:统一采用 GivenName FamilyName
  • slug:一旦创建即冻结,不再根据新来源中的罗马字顺序重新生成。
  • 来源页面看到的原始姓名顺序,不直接覆盖内部主身份字段,而是作为来源证据保留。

落地原则:

  1. 日文名是主事实字段。
  2. 罗马字是辅助展示和辅助检索字段,不作为主身份键。
  3. slug 是内部稳定标识,不要求与任一来源页面的罗马字顺序完全一致。
  4. 若后续同名、同罗马字、同读音仍无法区分,再通过后缀方式生成新 slug,例如 _2,但这只能作为最后手段。

站点访问规则:

  • 每访问一个新的来源类型,先抽样 2 到 3 个已知选手,判断该站点的罗马字顺序是 Family Given 还是 Given Family
  • 抽样结果写入来源策略或本地 skill。
  • 未完成抽样前,不批量导入该站点的人名。

4.6 审计优先于事件流

4d3e3d7 (Add featured article and sync latest posts)

V0.1 建设 AuditLog,暂不建设 domain_event

原因:

  • 真实世界事实已经可以由业务表表达,例如 MembershipPersonalBestRaceResult
  • 单独维护事件表会导致重复录入和同步问题。
  • 当前阶段还没有“选手动态流”“新闻事件”“用户时间线”等明确产品需求。

当前原则:

  • 业务表记录事实。
  • AuditLog 记录数据维护行为。
  • 未来如需选手生涯时间线,优先从业务表聚合生成。

5. 核心数据域

5.1 Person:选手/人物

用于表示选手、教练、监督、主务等人。

V0.1 主要使用选手,后续可扩展到教练和工作人员。

建议收集字段类型:

  • 姓名
  • 读音
  • 罗马字
  • 中文译名
  • 英文名
  • 生年月日
  • 年龄
  • 出身地
  • 国籍
  • 登録陸協
  • 身高
  • 体重
  • 人物类型:选手、监督、教练、主务等
  • 备注
  • 数据状态

5.2 Organization:组织

用于表示高中、大学、实业团、公司、俱乐部、赛事组织方。

建议收集字段类型:

  • 正式名称
  • 简称
  • 读音
  • 罗马字
  • 中文名
  • 英文名
  • 组织类型:高中、大学、实业团、公司、俱乐部、联盟等
  • 所在地
  • 都道府县
  • 官方网站
  • 备注
  • 数据状态

注意:

  • 大学和大学长距离队伍可以先视为同一组织,后续需要时再拆分
  • 实业团队伍和母公司可先用组织类型区分,后续再细分关系

5.3 Membership:所属关系

中文含义:所属履历 / 生涯所属。

用于表示人物在某时间段与某组织的关系。它是未来关系图和选手生涯时间线的核心表。

示例:

黒田朝日 - 玉野光南高校 - 2019-04-01 ~ 2022-03-31
黒田朝日 - 青山学院大学 - 2022-04-01 ~ 2026-03-31
黒田朝日 - GMOインターネットグループ - 2026-04-01 ~ null

建议收集字段类型:

  • 人物
  • 组织
  • 关系类型
  • 开始日期
  • 结束日期
  • 开始年份
  • 结束年份
  • 年级,作为来源快照,不作为关系核心
  • 学部
  • 学科
  • 期/代
  • 队内角色:主将、主务等
  • 来源
  • 数据状态

V0.1 用它表达:

  • 高中履历
  • 大学履历
  • 当前实业团或当前所属
  • 学部・学科
  • 队内角色,例如主将

关系类型约定:

  • 高中:Organization.type = high_schoolMembership.type = enrolled
  • 大学:Organization.type = universityMembership.type = enrolled
  • 实业团:Organization.type = corporate_teamMembership.type = affiliated
  • origin 暂时保留,但 V0.1 不作为出身高中主表达方式

时间约定:

  • V0.1 使用 startDate / endDate,不增加时间精度字段。
  • 日本高中/大学入学默认使用 04-01
  • 日本高中/大学毕业默认使用 03-31
  • 实业团入社默认使用 04-01
  • 不确定性写入备注。
  • startYear / endYear 可保留,方便查询。
  • grade 只表示某来源中的年级快照,不作为长期事实。

5.4 Personal Best:个人最好成绩

用于表示选手在某项目上的当前 PB 快照。

V0.1 不做 PB 历史,不做 2nd best,也不从全量比赛成绩推导 PB。

规则:

  • 每个选手每个项目只保留当前最好成绩。
  • PB 可以记录创造日期、赛事名、地点和来源。
  • 旧 PB 暂不录入。
  • V0.2 建设比赛结果库后,再考虑用比赛成绩推导或同步 PB。

建议收集项目:

  • 1500m
  • 3000m
  • 3000mSC
  • 5000m
  • 10000m
  • 10マイル
  • ハーフマラソン
  • マラソン

建议收集字段类型:

  • 人物
  • 项目
  • 记录
  • 创造日期
  • 创造赛事
  • 创造地点
  • 当时所属
  • 当时年级或阶段
  • 是否高校 PB
  • 是否大学 PB
  • 来源
  • 数据状态

V0.1 页面优先展示:

  • 5000m
  • 10000m
  • ハーフマラソン
  • マラソン,若有

其他项目可先录入但不重点展示。

5.5 Competition:赛事

用于表示赛事系列。

示例:

  • 箱根駅伝
  • 出雲駅伝
  • 全日本大学駅伝
  • 全国高校駅伝
  • ニューイヤー駅伝

建议收集字段类型:

  • 赛事名称
  • 赛事类型
  • 主办方
  • 地区
  • 官方网站
  • 备注

V0.1 只将箱根駅伝作为核心赛事系列样本。出雲駅伝、全日本大学駅伝、路跑和田径赛事进入 V0.2 比赛结果库阶段。

5.6 Competition Edition:赛事届次

用于表示某一年或某一届赛事。

示例:

  • 第102回東京箱根間往復大学駅伝競走

建议收集字段类型:

  • 赛事
  • 届次
  • 年份
  • 日期
  • 官方名称
  • 简称
  • 备注
  • 来源

5.7 Race:比赛单元

用于表示某一届赛事下面的具体比赛、区间、项目或组别。

这是 V0.1 采用的四层赛事模型中的第三层:

Competition -> CompetitionEdition -> Race -> RaceResult

示例:

Competition: 箱根駅伝
CompetitionEdition: 第102回箱根駅伝
Race: 5区
RaceResult: 黒田朝日 / 1:07:16 / 区間1位
Competition: MARCH対抗戦
CompetitionEdition: MARCH対抗戦2025
Race: 男子10000m 4組
RaceResult: 黒田朝日 / 27:37.62 / 1位

建议收集字段类型:

  • 赛事届次
  • 比赛单元名称
  • 项目
  • 区间号,駅伝使用
  • 轮次
  • 组别
  • 距离
  • 开始时间
  • 来源
  • 数据状态
  • 备注

V0.1 主要用于箱根区间,例如:

  • 第100回箱根駅伝 2区
  • 第101回箱根駅伝 2区
  • 第102回箱根駅伝 5区

5.8 Race Result:比赛成绩

用于表示选手在某个 Race 中的出场和成绩。

建议收集字段类型:

  • 人物
  • 所属大学
  • 比赛单元
  • 是否报名
  • 是否实际出场
  • 是否当日変更
  • 成绩
  • 名次
  • 综合排名相关信息
  • 备注
  • 来源
  • 数据状态

V0.1 优先用于箱根出场记录。

箱根不做特殊化:

  • 不新增箱根专用表。
  • 不新增 isSectionPrize / isSectionRecord 等箱根专用字段。
  • 区間賞、区間新等标签 V0.1 先写入 notes
  • V0.2 比赛结果库阶段,再考虑 ResultTagAchievement

5.9 Source:数据来源

用于记录数据来自哪里。

建议来源类型:

  • 大学官网
  • 箱根官方
  • NTV 箱根页面
  • 日本陸連
  • 月陸
  • 数据站
  • 粉丝站
  • PDF
  • World Athletics
  • 当前所属队伍官网
  • 人工整理

建议收集字段类型:

  • 来源名称
  • 来源 URL
  • 来源类型
  • 发布日期,若有
  • 访问日期
  • 备注
  • 可信等级

5.10 AuditLog:审计日志

用于记录数据维护行为。

它不表示现实世界事件,只表示我们的数据库发生了什么维护动作。

适合记录:

  • 新增选手
  • 修改基础资料
  • 新增或修正 PB
  • 新增箱根记录
  • 修正所属履历
  • 修改确认状态
  • 合并或拆分实体
  • 批量导入导致的变更

建议收集字段类型:

  • 实体类型
  • 实体 ID
  • 动作:create / update / delete / verify / merge / split
  • 字段名
  • 旧值
  • 新值
  • 变更原因
  • 变更说明
  • 来源
  • 操作者类型:admin / import_script / system
  • 批次 ID
  • 创建时间

V0.1 暂不要求所有操作自动写入审计,但模型先建立。

5.11 Name Variant:名称变体

用于管理日文、中文、英文、罗马字、简称、旧字体等。

适用对象:

  • 人物
  • 组织
  • 赛事

建议收集字段类型:

  • 目标对象
  • 名称
  • 语言
  • 名称类型:正式名、简称、读音、罗马字、中文译名、英文译名、旧称、媒体写法
  • 是否主名称
  • 来源

6. V0.1 页面设计

6.1 首页

首页第一屏应包含:

  • 站点名称:襷の系譜
  • 副标题
  • 搜索框
  • 三所大学入口
  • 最近更新选手
  • 语言切换

首页不是营销页,而是资料入口。

6.2 选手列表页

列表支持:

  • 按当前所属筛选
  • 按大学筛选,基于所属履历
  • 按出身高中筛选,若数据足够
  • 搜索姓名

展示字段:

  • 姓名
  • 读音或罗马字
  • 当前所属
  • 大学履历摘要
  • 出身高中
  • 主要 PB 摘要
  • 数据状态

6.3 选手详情页

页面结构:

  • 基本资料
  • 当前所属
  • 所属履历
  • 主要 PB
  • 箱根记录
  • 相关选手
  • 数据来源

相关选手模块:

  • 同当前所属
  • 同大学
  • 同高中

V0.1 不做图谱,只做清晰跳转。

6.4 管理后台

V0.1 需要一个轻量后台,因为数据维护比公开页面更重要。

后台功能:

  • 新增/编辑选手
  • 新增/编辑组织
  • 新增/编辑所属关系
  • 新增/编辑 PB
  • 新增/编辑箱根记录
  • 新增/编辑来源
  • 数据状态标记
  • 审计记录查看,后置也可以

V0.1 不急于做完整 CSV 导入。真实数据导入管线会在模型经过单选手深样本验证后再设计。

后台可以先很朴素,但必须好用。

7. 数据导入策略

7.1 初始数据策略

V0.1 不急于大规模导入三校完整名单。

推荐先做单选手深样本,以黒田朝日为代表,验证模型和页面:

  • 基础资料
  • 高中 -> 大学 -> 实业团所属履历
  • 当前 PB
  • 箱根记录
  • 来源优先级
  • 来源冲突
  • 审计记录

单选手深样本稳定后,再扩展到三校代表选手,最后再考虑 CSV/批量导入。

7.2 初始数据来源

V0.1 初始数据优先来自:

  • 青山学院大学长距离成员页
  • 國學院大學成员页
  • 中央大学长距离成员页
  • 箱根官方报名 PDF
  • NTV 箱根页面
  • 月陸 Online 名鑑
  • 当前所属实业团官网,若选手已毕业
  • World Athletics,主要用于 PB 与国际记录参考
  • JAAF / 赛事官方结果,若用于确认 PB

这些来源覆盖:

  • 当前名单
  • 姓名读音
  • 罗马字
  • 学部・学科
  • 出身高中
  • 出身地
  • PB
  • 箱根出场记录
  • 队伍角色

7.3 数据来源优先级

人工规则先写入文档和维护流程,不急于写死到系统逻辑。

初步规则:

  • 选手当前所属:当前所属队伍官网优先。
  • 高中/大学履历:大学官网、队伍官网、箱根/KGRR 报名资料优先。
  • 箱根成绩:箱根官方历史记录优先,其次 KGRR / NTV。
  • PB:赛事官方结果 / JAAF / World Athletics / 当前所属队伍官网交叉确认。
  • 媒体名鑑:字段很全,但主要作为补全来源。
  • 粉丝站/数据站:适合发现线索和交叉检查,关键事实需要二次确认。

7.4 导入方式

V0.1 不做自动爬虫,也不优先做批量 CSV 导入。

推荐流程:

  1. 在本地抓取和整理来源数据
  2. 使用本地脚本完成抽取、归一化和匹配
  3. 直接连接正式在线数据库写入业务表
  4. 同时写入来源引用和审计记录
  5. 通过前台页面和数据库查询立即验证结果

原因:

  • 各站结构差异很大
  • 数据量早期不大
  • 早期重点是确认模型、匹配和去重,而不是离线环境隔离

这里的“本地”只表示脚本运行位置,不再表示一套独立的离线数据库环境。

从当前阶段开始,默认只有一套主业务库:

  • 开发时连接它
  • 导入真实数据时连接它
  • 前台页面展示它
  • 审计和来源也写在它上面

7.5 seed 的角色调整

seed 仍然保留,但职责收缩。

保留用途:

  • 初始化最小开发环境
  • 准备演示样本
  • 在本地快速重建基础站点骨架

不再承担:

  • 持续录入真实数据
  • 批量导入真实比赛结果
  • 线上数据库的日常更新
  • 来源校验后的正式发布

原因:

  • seed 把代码和真实数据混在一起,随着数据量增长会迅速失控。
  • seed 更适合“重建初始状态”,不适合“增量维护真实世界事实”。
  • 真实数据更新需要更强的去重、审计、来源引用和失败回滚能力。

7.6 单库直连更新方案

V0.1 的真实数据更新采用:

本地脚本 + 在线数据库直连

即:

  • 抓取、抽取、归一化、校验在本地完成
  • 最终写入直接连接正式 PostgreSQL
  • 不再设计“线下库整理完再同步线上”的双阶段方案

这一方案的前提判断是:

  • 当前项目规模小
  • 当前维护者极少
  • 当前优先级是速度和一致性
  • 当前不优先处理复杂权限和安全治理

因此,V0.1 更关心:

  1. 数据写入入口统一
  2. 原始来源可回溯
  3. 实体匹配可解释
  4. 重复写入可阻断
  5. 变更可审计

7.7 数据更新链路

推荐链路分为四步:

  1. fetch

    • 访问来源页面、JSON、PDF 或结果页
    • 将原始内容保存到本地工作目录
  2. normalize

    • 抽取选手、组织、赛事、成绩、PB 等结构化信息
    • 形成统一输入格式
  3. reconcile

    • 匹配已有 PersonOrganizationRace
    • 检查重名、倒序罗马字、同组织同届冲突
    • 生成待确认项或阻断项
  4. publish

    • 通过统一导入脚本写入正式数据库
    • 同时写入 Source、来源引用和 AuditLog

V0.1 的重点是先把 publish 收口,而不是先追求全自动抓取。

7.8 正式导入入口

后续不允许长期并行使用多种随意入口更新真实数据。

真实数据更新应尽量收口到少数几类命令:

  • import:person
  • import:race
  • import:organization
  • reconcile:person
  • verify:import

这些命令的具体脚本名可以后定,但职责需要稳定。

不推荐长期保留以下方式作为主链路:

  • 手工改 seed.ts
  • 临时 SQL 直接写表
  • 没有审计的 ad hoc 脚本
  • 一次性脚本写完即丢

7.9 原始来源落盘策略

虽然正式写库直接连在线数据库,但原始来源仍建议保留在本地文件系统中。

原因:

  • 方便复查抽取逻辑
  • 方便处理来源冲突
  • 方便后续抽取器复用
  • 方便为本地 skill 累积来源攻略

建议至少保留:

  • 原始 URL
  • 抓取时间
  • 原始 HTML / JSON / PDF 路径
  • 关键页面说明
  • 当前抽取规则备注

这部分是“本地证据层”,不是离线数据库。

7.10 重复实体防线

重复选手问题说明,V0.1 不能继续使用“看到一个新罗马字就生成一个新 slug”的方式。

后续导入必须遵守:

  1. slug 一旦确定,不再因新来源的罗马字词序变化而重新生成。 <<<<<<< HEAD
  2. displayNameRoman 只能作为辅助匹配字段,不能作为主身份键。
  3. 优先使用来源映射或既有实体匹配,而不是字符串猜测。
  4. 在正式写库前,必须执行重复检查。 =======
  5. displayNameRoman 统一采用 GivenName FamilyName,并且只能作为辅助匹配字段,不能作为主身份键。
  6. 优先使用来源映射或既有实体匹配,而不是字符串猜测。
  7. 在正式写库前,必须执行重复检查。
  8. 新来源首次接入前,先做姓名顺序抽样,再决定解析规则。

4d3e3d7 (Add featured article and sync latest posts)

至少检查:

  • 规范化日文名重复
  • 罗马字正反序重复
  • 同组织同年级高相似重复
  • 同一来源重复指向不同人物

命中后应进入待确认或阻断,而不是自动写入。

7.11 来源映射表

为了避免重复导入,V0.1 后续应增加来源映射机制。

核心思想是:

某个外部来源中的人物、组织、比赛页面,应能稳定映射到本站内部实体。

例如人物至少需要记录:

  • 来源站点
  • 来源页面 URL
  • 来源侧标识或可稳定定位的 key
  • 对应的内部 personId

这样下次再次访问同来源时,系统优先复用既有映射,而不是重新猜 slug。

7.12 数据校验

基础校验包括:

  • 同名选手提示
  • 规范化日文名重复提示
  • 罗马字正反序重复提示
  • 同一时间段所属履历冲突提示
  • 同一项目重复 PB 提示
  • PB 时间格式校验
  • 成绩项目枚举校验
  • 来源缺失提示
  • 出身高中不存在时提示创建组织
  • 当前所属缺失提示
  • 数据来源冲突提示

7.13 审计要求

既然 V0.1 直接写正式数据库,就更不能省略审计。

每次正式导入至少应能回答:

  • 改了什么
  • 为什么改
  • 来源是什么
  • 是新增、修正还是覆盖

因此正式导入脚本默认应写入:

  • AuditLog
  • 导入批次标识
  • 来源引用

这部分先服务于站主自己,不以用户前台展示为目标。

8. 搜索策略

V0.1 使用 PostgreSQL 基础查询即可。

支持:

  • 日文姓名搜索
  • 罗马字搜索
  • 大学筛选
  • 高中筛选

后续再引入:

  • Meilisearch
  • Typesense
  • Elasticsearch

搜索服务不应该成为 V0.1 的复杂度来源。

9. 多语言实现策略

URL 建议:

/ja
/zh
/en

主语言:

  • /ja

V0.1 翻译范围:

  • 导航
  • 页面标题
  • 字段标签
  • 站点说明
  • 数据状态
  • 来源说明

不要求翻译:

  • 所有选手简介
  • 所有学校名称
  • 所有备注内容

人名和学校名优先展示日文原文,必要时追加罗马字、中文名或英文名。

实现策略:

  • 第一层 i18n 只翻译界面文案,使用 JSON dictionary。
  • 实体翻译仍保留在 DB 字段或名称变体中,但 V0.1 不要求完整维护。
  • 路由使用 /ja/zh/en
  • 语言切换需要在首页、列表页、详情页都可用,并尽量保留当前路径。

10. 权限与后台安全

V0.1 后台只面向站主使用。

建议:

  • 单管理员账号
  • 登录后访问 /admin
  • 不开放注册
  • 不开放社区投稿
  • 不开放评论

认证可以使用:

  • Auth.js

或者更简单的:

  • 单管理员密码 + session

如果项目会部署到公开网络,至少需要:

  • 后台登录保护
  • 管理接口鉴权
  • 基础 CSRF 防护
  • 环境变量管理密钥

11. 部署与域名方案

V0.1 采用自有服务器部署,先与个人站点共存在 ubuntu-3

部署细节、GitHub Actions 方案和数据同步策略单独维护在:

当前不新开 ubuntu-4。原因:

  • V0.1 仍处在验证期,访问量和数据量都很小
  • 与个人站点共用机器可以减少早期运维成本
  • 只要从一开始做好隔离,后续迁移成本可控
  • 项目需要稳定迭代数据模型和页面功能,部署层先保持简单

11.1 部署原则

tasuki-keifu 与个人站点只共享同一台服务器,不共享应用进程、数据库、环境变量或反向代理配置。

隔离原则:

  • 独立代码目录
  • 独立运行端口
  • 独立 systemd service
  • 独立 PostgreSQL database
  • 独立 PostgreSQL user
  • 独立 .env
  • 独立 Nginx server block
  • 独立日志和备份路径

这样后续迁移到 ubuntu-4 时,可以整体搬迁 tasuki-keifu,不会和个人站点互相缠绕。

11.2 ubuntu-3 上的目标形态

建议目录:

/srv/tasuki-keifu
  ├─ app
  ├─ releases
  ├─ shared
  │   └─ .env
  └─ backups

建议运行方式:

Nginx
  └─ tasuki-keifu 子域名
      └─ proxy_pass http://127.0.0.1:3020

systemd
  └─ tasuki-keifu.service
      └─ pnpm start / next start

PostgreSQL
  ├─ database: tasuki_keifu
  └─ user: tasuki_keifu_user

端口约定:

  • 个人站点继续使用原有端口
  • tasuki-keifu 使用独立端口,例如 3020
  • 本地开发端口仍可使用 3000

环境变量:

  • DATABASE_URL
  • NEXT_PUBLIC_SITE_URL
  • SESSION_SECRET,上线管理后台前需要
  • 未来导入脚本需要的外部 API key 或管理密钥

11.3 域名策略

V0.1 先使用已有主域名下的子域名。

建议:

tasuki-keifu.gelatoni.uk

短期不急于购买独立域名。等以下条件满足后,再考虑独立品牌域名:

  • 数据覆盖超过样例阶段
  • 日文页面具备基本公开浏览价值
  • 数据来源和确认状态足够清楚
  • 有持续维护计划

如果后续购买独立域名,可以通过 Nginx 新增 server block 或直接替换现有子域名配置,不影响应用代码。

当前部署状态:

  • tasuki-keifu 已部署到 ubuntu-3
  • 应用目录:/opt/apps/tasuki-keifu
  • Docker Compose 文件:/opt/apps/tasuki-keifu/docker-compose.prod.yml
  • Nginx 配置:/etc/nginx/sites-available/tasuki-keifu
  • 本机应用地址:http://127.0.0.1:3020
  • 目标域名:tasuki-keifu.gelatoni.uk
  • Cloudflare DNS 已指向 13.230.244.67
  • HTTP 反向代理已在服务器侧验证通过
  • HTTPS 证书已通过 certbot 签发
  • 公网访问 https://tasuki-keifu.gelatoni.uk/ja 已验证 200

11.4 数据库与备份

V0.1 使用 PostgreSQL 作为主业务库。

生产数据库建议:

  • 与个人站点不同库
  • 与个人站点不同用户
  • 用户只授予 tasuki_keifu 所需权限

备份策略:

  • 初期使用 pg_dump
  • 每日一次逻辑备份
  • 保留最近 7-14 天
  • 备份文件放在独立目录,例如 /srv/tasuki-keifu/backups
  • 重要数据录入前后可以手动执行一次备份

后续如果数据量扩大,再考虑:

  • 异地备份
  • S3 / Lightsail object storage
  • 定期恢复演练
  • 数据导入前自动生成快照

11.5 发布流程

当前已完成一次手动部署:

本地代码 -> 同步到 Ubuntu-3 -> Docker Compose 构建镜像 -> Prisma migrate deploy -> 重启 app

下一步发布流程应切换为 GitHub Actions:

  1. push 到 GitHub main
  2. GitHub Actions 安装依赖、类型检查、构建应用
  3. GitHub Actions 构建 Docker 镜像并保存为归档
  4. 通过 SSH/SCP 上传镜像归档和部署文件到 Ubuntu-3
  5. 服务器 docker load
  6. 执行 prisma migrate deploy
  7. docker compose up -d app
  8. 检查健康页面和核心页面

常规部署不执行 seed。pnpm db:seed 只用于本地开发或明确的初始化动作,不能成为生产发布步骤。

上线前至少检查:

  • /ja
  • /zh
  • /en
  • /ja/players
  • 一个选手详情页
  • 数据库连接
  • Nginx HTTPS

11.6 后台上线约束

公开部署后,如果没有管理后台,公共页面可以先上线。

如果上线 /admin 或任何写操作接口,必须先完成:

  • 登录保护
  • session secret
  • 管理接口鉴权
  • CSRF 或等价保护
  • 服务端校验
  • 审计日志写入

V0.1 早期可以先不开放管理后台,但真实数据维护应通过统一的本地导入脚本直连正式数据库完成,而不是继续依赖 seed 作为主入口。

11.7 迁移到 ubuntu-4 的路径

如果后续访问量、数据导入任务或运维边界要求提高,可以迁移到 ubuntu-4

迁移步骤:

  1. ubuntu-4 安装 Node.js、pnpm、PostgreSQL、Nginx
  2. 创建 tasuki_keifu database 和独立用户
  3. ubuntu-3 执行 pg_dump
  4. ubuntu-4 执行 restore
  5. 将代码部署到 /srv/tasuki-keifu
  6. 复制并调整 .env
  7. 创建 tasuki-keifu.service
  8. 配置 Nginx 和 HTTPS
  9. 用临时域名或 hosts 验证页面
  10. 切换 DNS 到 ubuntu-4
  11. 确认稳定后,停止 ubuntu-3 上的 tasuki-keifu.service

因为应用目录、数据库、服务和 Nginx 配置从一开始就是独立的,迁移时不需要拆个人站点。

11.8 备选方案

如果后续希望减少服务器运维,也可以改用:

  • Vercel
  • Neon / Supabase Postgres

这条路线适合快速公开验证,但不是当前 V0.1 的默认方案。

12. 后续演进

12.1 V0.2

  • 比赛结果库第一版
  • 开始收录每场比赛数据
  • 支持箱根以外的比赛:出雲駅伝、全日本大学駅伝、主要田径赛、路跑/马拉松
  • 完善 RaceRaceResult 的字段
  • 引入结果标签或成就模型,例如区間賞、区間新、学生新、大会新
  • 建立更完整的数据来源审计
  • 设计 CSV/批量导入管线

12.2 V0.3

  • 关系图与可视化第一版
  • 高中 -> 大学 -> 实业团流向
  • 同场比赛、同区间、同队参赛关系
  • 大学页
  • 高中页
  • 实业团队伍页
  • 选手对比和筛选

12.3 V0.4

  • 搜索索引服务
  • 更完善的中文/英文体验
  • 解释日本长跑体系的内容页
  • SEO 优化

12.4 V1.0 以后

  • 自动化数据导入
  • 数据变更监控
  • 社区纠错
  • 投稿审核
  • 图谱可视化
  • 数据仓库或分析库
  • API / 数据下载

13. 不做事项

V0.1 不做:

  • 独立后端服务
  • 微服务
  • 数据仓库
  • 图数据库
  • 复杂爬虫
  • 实时比赛更新
  • 用户系统
  • 社区互动
  • 复杂图谱可视化
  • 大规模 BI 分析
  • 完整比赛结果库
  • PB 历史
  • SNS 和粉丝向 Q&A 字段
  • domain_event 事件流

这些都不是第一版风险点。

14. 关键风险

14.1 数据来源分散

不同来源字段不一致,且可能更新频率不同。

应对:

  • 数据模型保留来源和状态
  • 先人工整理
  • 不急于自动化

14.2 同名和名称变体

日文姓名、旧字体、罗马字、中文译名可能不一致。

应对:

  • 建立 name variant
  • 不把翻译名作为主键
  • 所有关系使用内部 ID

14.3 字段过多导致后台难用

宽收集可能让后台表单变重。

应对:

  • 后台按模块编辑
  • 选手基础资料、PB、所属关系、来源分开维护
  • V0.1 前台只展示核心字段

14.4 所属关系缺少时间线会影响关系图

如果只记录当前大学和年级,数据很快会过时,也无法表达高中 -> 大学 -> 实业团的生涯路径。

应对:

  • 所属履历使用 startDate / endDate
  • 高中、大学、实业团统一走 Organization + Membership
  • 当前所属由时间段推导
  • 年级只作为来源快照

14.5 赛事模型过早特殊化

如果针对箱根单独建特殊字段,后续接入出雲、全日本、田径赛、马拉松会变困难。

应对:

  • 采用四层赛事模型:Competition -> CompetitionEdition -> Race -> RaceResult
  • 箱根区间只是 Race 的一种
  • 区間賞/区間新 V0.1 先放 notes
  • V0.2 再结构化结果标签

14.6 版权和引用边界

媒体名鑑和图片存在版权风险。

应对:

  • 只收公开事实数据
  • 不复制文章正文
  • 不直接搬运图片作为核心资产
  • 页面保留来源链接

15. 实施顺序

建议按以下顺序实施:

  1. 初始化 Next.js 项目
  2. 接入 PostgreSQL 和 Prisma
  3. 建立核心 schema:Person / Organization / Membership / PersonalBest / Competition / CompetitionEdition / Race / RaceResult / Source / AuditLog / NameVariant
  4. 建立 Membership.startDate / endDate 时间线模型
  5. 建立四层赛事模型
  6. 实现基础 seed 数据
  7. 实现首页、选手列表页、选手详情页
  8. 补充多语言界面第一层:JSON dictionary + /ja/zh/en
  9. 以黒田朝日为单选手深样本,补齐 P0 数据
  10. 校验页面和数据关系
  11. 根据样本调整页面展示:当前所属、所属履历、PB、箱根记录、来源
  12. 实现基础搜索
  13. 实现轻量后台
  14. 做视觉打磨
  15. 准备 V0.1 内部预览

CSV/批量导入不作为当前最近一步。它应在模型经过单选手深样本验证后再设计。