Files
fitness-coach/backend/README.md
wm 70ff806042 feat: 小程序连接服务器IP + 同步未提交开发
- miniprogram/config.js: BASE_URL 指向 http://192.227.237.8:3001
- 动作标题中文化(nameZh),搜索匹配 name/nameZh/nameEn
- 新增计划/收藏/我的 等页面与组件
- 后端 Docker 化(Dockerfile/.dockerignore)与翻译脚本
- .gitignore 补充 .DS_Store
2026-07-22 14:05:30 +08:00

129 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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-SHA25630 天有效)与用户资料。
- **鉴权**:受保护接口需 `Authorization: Bearer <token>`;统一 `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 / RecommendQueryDtoclass-validator 校验)
├── utils/normalize.ts # 原始 → 结构化(类型推导、媒体 URL、中文标签
└── data/
├── exercises.json # 数据集(构建时生成)
├── labels.ts # 中英文标签映射
└── collections.ts # 智能训练组合 / 官方计划定义(含 level 分级)
src/common/
├── store.ts # 文件持久化单例 JsonStoredata-store/
├── token.ts # token 签名 / 校验
├── auth.guard.ts # Bearer 鉴权守卫
└── current-user.decorator.ts # 注入当前 openid
src/auth/ src/users/ src/favorites/ src/plans/ # 用户体系四模块
```