一、工作进展

本阶段完成训练模块后端核心功能的完整开发,跨越两个提交:

第一个提交:训练记录 CRUD + 周期性计划 + 训练汇总 + 接入营养汇总 API。实现了训练记录的新增、查询、更新、删除,周期性计划的全量存取,训练数据的日级和趋势级汇总,并完成了与营养模块的数据对接——训练汇总计算完热量后写入 nutrition_summary.consumption_calories

第二个提交:动作库/器材库 HTTP 接口 + 训练部位记录 + COS 媒体存储。动作库和器材库从管理员脚本录入变为 HTTP 接口,训练部位记录系统上线,COS 对象存储接入用于存放动作图片和器材图片。

二、详细内容

1. 训练记录的设计

1.1 Plan 与 Record 的合并设计

训练记录表(training_records)通过 record_type 字段区分「计划」和「记录」两种数据:用户说「下周我要练卧推」是计划(plan),「刚练完卧推」是记录(record)。两类数据存在同一张表里,共享同一套字段结构。

合并的好处是:用户查看训练历史时,计划和实际完成的记录可以在一起展示,不需要 JOIN 两张表。「计划」需要记录目标组数/次数/重量,「记录」需要记录实际完成的数字,两者结构相同,合并后前端渲染时只需要根据 record_type 显示不同的标签文字。

另一个实际的好处是:计划可以直接转为记录。用户在执行计划时,把 record_type 从 plan 改成 record,更新 actual_* 字段,就是一条完整的实际训练记录。不需要先查计划、再新建一条记录、再删计划这类两步操作。

1.2 热量字段的来源

训练消耗热量(calories)是训练记录的可选字段,来自 LLM 估算——用户在记录「刚练完卧推」时,AI 会估算这次训练消耗了多少千卡,写入 calories 字段。这个值不是后端计算的,后端只负责存储。

LLM 估算的好处是不需要接入运动传感器或查运动数据库,在对话场景下直接出结果。健身新手不知道「卧推一小时消耗多少卡路里」,让 AI 来估比让用户自己查表再填要自然得多。

2. 训练汇总的设计

2.1 当日汇总与趋势查询

训练汇总 API 分两种:当日汇总和趋势查询。

当日汇总返回当日训练的总动作数、总组数、总次数、总热量,用于展示「今天练得怎么样」。趋势查询按日期范围聚合每天的上述数字,用于展示「近一周/近一月训练趋势」。

趋势查询的 SQL 是按日期的 GROUP BY 聚合。关键点是用 COUNT(DISTINCT action_name) 而不是 COUNT(*) 来计算动作数——因为同一天同一条动作可能有多条记录(比如分上午下午两次练),去重后的数字才是用户真正练了多少个不同动作。

2.2 跨模块热量写入

训练汇总 API 在计算完当日热量后,会把结果写入 nutrition_summary.consumption_calories。这是训练模块和营养模块之间的唯一数据通道。

热量缺口 = 饮食摄入热量 - 训练消耗热量。消耗热量来自训练记录,摄入热量来自饮食记录,两者需要聚合到同一张表才能做减法。nutrition_summary 承担了这个「共享聚合点」的角色:饮食模块写入摄入字段,训练模块写入消耗字段,查询热量缺口只需要读这一张表。

训练模块写入 nutrition_summary 是单向依赖:训练模块知道营养模块的表结构,营养模块不关心是谁写入了 consumption_calories。这个耦合是模块协作中不可避免的成本,关键在于它是单向的而不是双向的——训练模块可以修改营养模块的数据,但营养模块不知道训练模块的存在。

3. 周期性计划的设计

每个用户的周期性计划存在 periodic_plans 表,periodic_plans 字段是 JSONB 类型。一条记录存一个用户的完整计划 JSON,结构大致是:

[
  {
    "day": "monday",
    "actions": [
      {"name": "杠铃卧推", "sets": 4, "reps": 8, "weight": 60},
      {"name": "哑铃飞鸟", "sets": 3, "reps": 12, "weight": 15}
    ]
  },
  {
    "day": "wednesday",
    "actions": [...]
  }
]

这个场景适合 JSONB 而不适合关联表,原因不在于「JSON 更灵活」,而是这个数据有三个特点:第一,它是树形结构,关联表需要头表加行表加 JOIN;第二,它是个人使用的数据,不需要做跨用户统计查询;第三,计划结构变动的频率低,每次修改都是全量覆盖,不需要逐行更新。JSONB 在这三个条件下是纯收益,关系表带来的 JOIN 复杂度是纯代价。

全量更新(upsert)的实现用了 ON CONFLICT DO UPDATE:如果用户已有计划则全量替换,没有则新建。这是最简单直接的做法——周期性计划本来就不需要保留历史版本,用户每次提交的都是最新版本。

4. 训练部位记录的设计

4.1 固定字段设计

部位记录表用 16 个固定字段记录「今天练了哪些部位、练得多狠」:chestlatstrapsbiceps 等,每个字段的值域是 0/1/2/3 四个等级(未练/练了/强度良好/练得很狠)。

这个场景不需要 EAV(Entity-Attribute-Value)模式。人体主要肌群是封闭集合,不会今天加一个「小腿前侧」明天加一个「肩袖」。固定字段让查询和统计极其简单——一条 SQL 就能知道昨天胸肌有没有被充分刺激。

用四个等级而不是布尔值,是因为「练了」和「练得很好」有训练层面的实际差别。等级 2/3 意味着这次训练已经充分刺激了该部位,AI 在生成后续计划时可以据此判断是否需要减少该部位的训练量——这个信息对训练计划的质量至关重要。

4.2 部位与动作的配合

部位记录和动作记录回答的是不同问题:动作记录回答「我具体做了哪些动作」,部位记录回答「我的训练是否均衡」。同一个动作会练到多个肌群(「杠铃卧推」同时练胸大肌、肩前束、三头肌),从动作反推部位需要额外逻辑。AI Agent 在生成训练计划时,需要直接读取部位记录判断「昨天背阔肌已经练得很狠了,今天应该换个部位」——这个场景下部位记录是独立的上下文输入,不能从动作记录派生。

AI 在记录「刚练完卧推」时,自动把 chest 更新为对应等级,具体的肌群对应关系维护在 Agent 的提示词里,不写在后端代码里。后端只提供 upsert 能力:接收 16 个字段的整数值、执行插入或更新。语义理解是 Agent 的职责,后端不负责「卧推练哪里」这个业务知识。

5. 动作库与器材库的 HTTP 接口

动作库和器材库在第一版是管理员脚本录入、直写数据库。第二版新增了 HTTP 查询接口,普通用户可以通过接口读取动作和器材列表。

改用 HTTP 接口的原因是:动作库数据会被前端的动作选择器和 AI Agent 共同使用。如果直接读数据库,前端要绕过 API 层直连数据库,AI Agent 用数据库客户端直接跑 SQL,等于放弃了 API 层统一做的认证、参数校验和响应格式标准化。HTTP 接口保证了所有访问都经过同一个入口。

数据写入仍然走管理员脚本,不通过 HTTP 接口——动作库和器材库是相对稳定的基础数据,不需要普通用户修改,这是有意为之的安全设计。

6. COS 媒体存储

6.1 为什么不用本地存储

动作图片和器材图片如果存在服务器本地,在多实例部署时会遇到根本性问题:每个实例的文件系统是独立的,A 实例上传的图片 B 实例看不到。用户请求被负载均衡分发到不同实例,一个实例上传的图片在另一个实例上返回 404。

健身新手场景下,图片是动作说明的重要组成部分,必须保证所有实例都能稳定访问同一个资源。本地存储在这个阶段就会成为瓶颈——随着用户量增加,必然要上多实例部署,届时所有本地存储的图片路径都要迁移,改动成本极高。所以从一开始就选择对象存储,用简单的上传逻辑换来了水平扩展能力。

6.2 公有读私有写的策略选择

桶策略是公有读私有写:服务端持有写入密钥,能上传;任何人都能读取图片。这个策略在安全性和开发成本之间取得了平衡——健身内容本来就是要给用户看的,「谁都能看」是预期行为,不需要额外的访问控制。

如果将来需求变成「有相关权限的会员才能看动作视频」,再改成签名 URL 也不迟。现在的策略留出了这个扩展空间,不需要在一开始就为不确定的需求付出实现成本。

上传路径按类型分开:动作图片存 actions/ 目录,器材图片存 equipment/ 目录,与数据库的 image_url 和 video_url 字段一一对应,后续如果需要扩展视频字段,路径规范已经预留好了。

三、总结

这次训练模块的开发分两个阶段:第一阶段完成核心 CRUD(训练记录、周期性计划、训练汇总),第二阶段完成配套系统(部位记录、动作库 HTTP 接口、COS 存储)。

核心的数据流设计是:训练记录存储原始动作数据,周期性计划存储用户的中长期安排,训练部位记录存储肌群维度的训练反馈,训练汇总负责跨模块输出。三套数据各司其职,汇总层通过单向写入 nutrition_summary 与营养模块对接。

下一阶段的工作重点是 Agent 调度层和 MCP 工具的完善,让 AI 能真正「理解」用户输入并调用对应的后端 API。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐