# FITCOACH 后端(NestJS) 健身动作数据代理与推荐服务。所有动作数据统一存储在后端,向前端小程序提供 REST 接口。 ## 技术栈 - NestJS 10 + Express - 内存数据集(构建时由 `exercises-dataset` 导入),查询/筛选/推荐均在内存完成,毫秒级响应 - 可选静态媒体服务(`/media`,指向 `backend/media/`) ## 环境变量(`.env`,参考 `.env.example`) | 变量 | 默认值 | 说明 | |------|--------|------| | `PORT` | `3000` | 服务端口 | | `API_BASE_URL` | `http://localhost:3000` | 小程序访问基址(生成媒体 URL 用) | | `MEDIA_BASE_URL` | `https://cdn.jsdelivr.net/gh/hasaneyldrm/exercises-dataset@main` | 媒体资源基址(CDN 直链,免本地下载) | | `CORS_ENABLED` | `true` | 是否允许跨域(Web 调试用) | | `WX_APPID` | 空 | 微信小程序 AppID;**不填则登录走开发降级模式**(任意 code 生成稳定虚拟 openid) | | `WX_SECRET` | 空 | 微信小程序 AppSecret,配合 `WX_APPID` 调用 code2session 换取真实 openid | | `JWT_SECRET` | `fitcoach-dev-secret` | token 签名密钥,生产务必修改为强随机值 | ## 安装与运行 ```bash npm install npm run build # 触发 prebuild: 自动下载 exercises.json 到 src/exercises/data/ npm run start:dev # 开发(watch) npm run start:prod # 生产:node dist/main.js ``` ## 脚本 - `npm run sync:media` — 将数据集的图片/动画同步到 `backend/media/`(供 `/media` 提供)。可选环境变量 `MEDIA_CONCURRENCY`(并发)、`MEDIA_ONLY=gif|image`。 - `node scripts/analyze.mjs` — 扫描数据集,输出各维度去重值与缺失的中文标签(用于补全 `labels.ts`)。 ## 数据导入说明 数据集 `exercises.json`(1,324 条)通过 `scripts/ensure-data.mjs` 在构建时自动从 GitHub 下载; 也可手动把 `exercises-dataset/data/exercises.json` 复制到 `src/exercises/data/exercises.json`。 服务启动时由 `ExercisesService.onModuleInit()` 读取并 `normalize()` 为带中文字段的结构化对象。 ## 接口示例 ```bash # 列表 + 筛选(按器械 dumbbell,分页) curl "http://localhost:3000/api/exercises?equipment=dumbbell&pageSize=5" # 动作详情 curl "http://localhost:3000/api/exercises/0001" # 目标肌群智能推荐(肱二头肌,限定哑铃) curl "http://localhost:3000/api/recommend?target=biceps&equipment=dumbbell&limit=10" # 分类元数据 curl "http://localhost:3000/api/categories/targets" curl "http://localhost:3000/api/categories/equipment" # 训练组合 curl "http://localhost:3000/api/collections" curl "http://localhost:3000/api/collections/legs/exercises?pageSize=10" # 搜索 curl "http://localhost:3000/api/search?q=abs" # ===== 用户体系(登录 / 收藏 / 计划)===== # 登录(开发模式:任意 code 均可,后端自动建号) TOKEN=$(curl -s -X POST http://localhost:3000/api/auth/login -H 'content-type: application/json' \ -d '{"code":"dev_123","userInfo":{"nickname":"阿强","avatar":"🦊"}}' | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>console.log(JSON.parse(d).token))") # 个人资料 curl http://localhost:3000/api/auth/me -H "Authorization: Bearer $TOKEN" curl -X PATCH http://localhost:3000/api/users/me -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' -d '{"nickname":"阿强Pro"}' # 收藏动作 curl -X POST http://localhost:3000/api/favorites/exercises -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' -d '{"target":"0001"}' curl http://localhost:3000/api/favorites/exercises -H "Authorization: Bearer $TOKEN" curl http://localhost:3000/api/favorites/exercises/0001/status -H "Authorization: Bearer $TOKEN" # 我的计划 curl -X POST http://localhost:3000/api/plans -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' -d '{"name":"胸肌日","exerciseIds":["0001","0002"]}' curl http://localhost:3000/api/plans -H "Authorization: Bearer $TOKEN" # 从官方计划一键导入为我的计划 curl -X POST http://localhost:3000/api/plans/import -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' -d '{"slug":"home"}' # 官方计划(按水平分级) curl http://localhost:3000/api/plans/official -H "Authorization: Bearer $TOKEN" curl "http://localhost:3000/api/plans/official/home/exercises?pageSize=10" -H "Authorization: Bearer $TOKEN" ``` ## 推荐算法 对每条动作计算相关性评分: - 目标肌群完全匹配 `target`:+100,理由「主要训练 XX」 - 出现在协同肌群 `secondaryMuscles`:+40,理由「协同训练 XX」 - 指定器械匹配:+30;若为自重可替代:+10 仅返回评分 > 0 的动作,按评分降序、名称升序返回。 ## 用户体系 / 收藏 / 计划(v2 新增) 独立的 `auth` / `users` / `favorites` / `plans` 四个模块,构成完整用户闭环: - **登录**:`POST /api/auth/login` 接收 `wx.login` 的 `code`,生产环境调用微信 `code2session` 换取真实 `openid`;**未配置 `WX_APPID`/`WX_SECRET` 时自动降级**,用任意 code 生成稳定虚拟 `openid`,方便本地联调。返回自签 `token`(HMAC-SHA256,30 天有效)与用户资料。 - **鉴权**:受保护接口需 `Authorization: Bearer `;统一 `AuthGuard` 校验,非法/过期返回 401。 - **资料**:`GET /api/auth/me`、`PATCH /api/users/me`(昵称/头像 emoji)。 - **收藏**:动作收藏(`/api/favorites/exercises`)+ 官方计划收藏(`/api/favorites/plans`),均带状态查询接口。 - **我的计划**:完整 CRUD(`/api/plans`)+ 向计划增删动作;`POST /api/plans/import` 可把官方计划一键复制为个人计划。 - **官方计划**:在原有 8 个训练组合基础上新增「按水平分级」的官方推荐计划(新手七天 / 新手燃脂 / 进阶增肌 / 进阶力量 / 高级竞技 / 舒展放松 / 女子塑形等),通过 `GET /api/plans/official` 按 `新手入门 / 进阶提升 / 高级挑战 / 全阶段适用` 分组下发。 ### 数据持久化 用户、收藏、计划数据落地在 **`backend/data-store/*.json`**(`users.json` / `favorites-exercises.json` / `favorites-plans.json` / `plans.json`),由 `src/common/store.ts` 的单例 `JsonStore` 在进程内加载、每次写入整体落盘。无需数据库即可持久化;首次启动自动建目录与空文件。 > 该目录为运行时数据,**建议加入 `.gitignore`**(已在仓库根 `.gitignore` 忽略 `data-store/`)。 ## 目录 ``` src/exercises/ ├── exercises.module.ts ├── exercises.service.ts # 数据加载、筛选、分类、推荐、组合 ├── exercises.controller.ts # /api 路由 ├── exercises.interface.ts # 类型定义 ├── dto/ # QueryExercisesDto / RecommendQueryDto(class-validator 校验) ├── utils/normalize.ts # 原始 → 结构化(类型推导、媒体 URL、中文标签) └── data/ ├── exercises.json # 数据集(构建时生成) ├── labels.ts # 中英文标签映射 └── collections.ts # 智能训练组合 / 官方计划定义(含 level 分级) src/common/ ├── store.ts # 文件持久化单例 JsonStore(data-store/) ├── token.ts # token 签名 / 校验 ├── auth.guard.ts # Bearer 鉴权守卫 └── current-user.decorator.ts # 注入当前 openid src/auth/ src/users/ src/favorites/ src/plans/ # 用户体系四模块 ```