瀏覽代碼

feat: 初始化数智健调系统后端项目结构

- 添加后端项目的基本文件结构,包括 TypeScript 配置、环境变量、数据库初始化脚本等
- 创建 RESTful API 相关的控制器、服务和路由
- 实现体重管理模块的数据库模型和数据传输对象
- 配置中间件以处理请求日志和错误
- 添加 README 文档以说明项目结构和使用方法
- 配置 .gitignore 文件以排除不必要的文件
17846405080 2 月之前
父節點
當前提交
0e40fea456
共有 35 個文件被更改,包括 4387 次插入412 次删除
  1. 41 0
      campus_health_app/backend/.gitignore
  2. 522 0
      campus_health_app/backend/README.md
  3. 355 0
      campus_health_app/backend/database/README.md
  4. 26 0
      campus_health_app/backend/database/init.sql
  5. 180 0
      campus_health_app/backend/database/schema.sql
  6. 200 0
      campus_health_app/backend/database/seed.sql
  7. 63 0
      campus_health_app/backend/package.json
  8. 60 0
      campus_health_app/backend/scripts/setup.bat
  9. 66 0
      campus_health_app/backend/scripts/setup.sh
  10. 67 0
      campus_health_app/backend/src/config/database.ts
  11. 366 0
      campus_health_app/backend/src/controllers/WeightController.ts
  12. 17 0
      campus_health_app/backend/src/dto/tag.dto.ts
  13. 42 0
      campus_health_app/backend/src/dto/weight-goal.dto.ts
  14. 113 0
      campus_health_app/backend/src/dto/weight-record.dto.ts
  15. 71 0
      campus_health_app/backend/src/entities/AnomalyLog.ts
  16. 58 0
      campus_health_app/backend/src/entities/Tag.ts
  17. 60 0
      campus_health_app/backend/src/entities/User.ts
  18. 78 0
      campus_health_app/backend/src/entities/WeightGoal.ts
  19. 76 0
      campus_health_app/backend/src/entities/WeightRecord.ts
  20. 43 0
      campus_health_app/backend/src/entities/WeightRecordTag.ts
  21. 32 0
      campus_health_app/backend/src/middlewares/auth.middleware.ts
  22. 38 0
      campus_health_app/backend/src/middlewares/error.middleware.ts
  23. 25 0
      campus_health_app/backend/src/middlewares/logger.middleware.ts
  24. 19 0
      campus_health_app/backend/src/routes/index.ts
  25. 58 0
      campus_health_app/backend/src/routes/weight.routes.ts
  26. 118 0
      campus_health_app/backend/src/server.ts
  27. 100 0
      campus_health_app/backend/src/services/StatsService.ts
  28. 74 0
      campus_health_app/backend/src/services/TagService.ts
  29. 127 0
      campus_health_app/backend/src/services/WeightGoalService.ts
  30. 233 0
      campus_health_app/backend/src/services/WeightRecordService.ts
  31. 47 0
      campus_health_app/backend/tsconfig.json
  32. 2 2
      campus_health_app/frontend/campus-health-app/src/app/modules/exercise/exercise.component.scss
  33. 417 33
      campus_health_app/frontend/campus-health-app/src/app/modules/monitoring/monitoring.component.html
  34. 0 370
      campus_health_app/frontend/campus-health-app/src/app/modules/monitoring/monitoring.component.scss
  35. 593 7
      campus_health_app/frontend/campus-health-app/src/app/modules/monitoring/monitoring.component.ts

+ 41 - 0
campus_health_app/backend/.gitignore

@@ -0,0 +1,41 @@
+# Dependencies
+node_modules/
+package-lock.json
+
+# Build outputs
+dist/
+build/
+*.tsbuildinfo
+
+# Environment variables
+.env
+.env.local
+.env.*.local
+
+# Logs
+logs/
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Test coverage
+coverage/
+.nyc_output/
+
+# Temporary files
+tmp/
+temp/
+*.tmp
+

+ 522 - 0
campus_health_app/backend/README.md

@@ -0,0 +1,522 @@
+# 数智健调系统 - 后端API
+
+## 项目简介
+
+数智健调系统后端API,基于Node.js + Express + TypeORM + MySQL开发,为体重管理模块提供完整的RESTful API服务。
+
+## 技术栈
+
+- **框架**: Express 4.18+
+- **语言**: TypeScript 5.3+
+- **ORM**: TypeORM 0.3+
+- **数据库**: MySQL 8.0+
+- **验证**: class-validator
+- **其他**: Redis(可选)、Winston(日志)
+
+## 项目结构
+
+```
+backend/
+├── database/                # 数据库脚本
+│   ├── schema.sql          # 表结构定义
+│   ├── init.sql            # 数据库初始化
+│   ├── seed.sql            # 测试数据
+│   └── README.md           # 数据库文档
+├── src/                    # 源代码
+│   ├── config/             # 配置文件
+│   │   └── database.ts     # 数据库配置
+│   ├── controllers/        # 控制器层
+│   │   └── WeightController.ts
+│   ├── dto/                # 数据传输对象
+│   │   ├── weight-record.dto.ts
+│   │   ├── weight-goal.dto.ts
+│   │   └── tag.dto.ts
+│   ├── entities/           # ORM实体模型
+│   │   ├── User.ts
+│   │   ├── WeightRecord.ts
+│   │   ├── WeightGoal.ts
+│   │   ├── Tag.ts
+│   │   ├── WeightRecordTag.ts
+│   │   └── AnomalyLog.ts
+│   ├── middlewares/        # 中间件
+│   │   ├── auth.middleware.ts
+│   │   ├── error.middleware.ts
+│   │   └── logger.middleware.ts
+│   ├── routes/             # 路由定义
+│   │   ├── index.ts
+│   │   └── weight.routes.ts
+│   ├── services/           # 服务层
+│   │   ├── WeightRecordService.ts
+│   │   ├── WeightGoalService.ts
+│   │   ├── TagService.ts
+│   │   └── StatsService.ts
+│   └── server.ts           # 服务器入口
+├── .env.example            # 环境变量示例
+├── .gitignore              # Git忽略文件
+├── package.json            # 项目依赖
+├── tsconfig.json           # TypeScript配置
+└── README.md               # 本文档
+```
+
+## 快速开始
+
+### 1. 环境要求
+
+- Node.js >= 18.0.0
+- npm >= 9.0.0
+- MySQL >= 8.0
+
+### 2. 安装依赖
+
+```bash
+cd campus_health_app/backend
+npm install
+```
+
+### 3. 配置环境变量
+
+复制环境变量示例文件并修改配置:
+
+```bash
+cp .env.example .env
+```
+
+编辑 `.env` 文件,配置数据库连接:
+
+```env
+NODE_ENV=development
+PORT=3000
+API_PREFIX=/api
+
+DB_TYPE=mysql
+DB_HOST=localhost
+DB_PORT=3306
+DB_USERNAME=root
+DB_PASSWORD=your_password
+DB_DATABASE=campus_health
+DB_SYNCHRONIZE=false
+DB_LOGGING=true
+
+CORS_ORIGIN=http://localhost:4200
+```
+
+### 4. 初始化数据库
+
+```bash
+# 创建数据库
+mysql -u root -p < database/init.sql
+
+# 创建表结构
+mysql -u root -p campus_health < database/schema.sql
+
+# (可选)插入测试数据
+mysql -u root -p campus_health < database/seed.sql
+```
+
+### 5. 启动开发服务器
+
+```bash
+npm run dev
+```
+
+服务器将在 `http://localhost:3000` 启动。
+
+### 6. 构建生产版本
+
+```bash
+# 编译TypeScript
+npm run build
+
+# 启动生产服务器
+npm start
+```
+
+## API文档
+
+### 基础信息
+
+- **Base URL**: `http://localhost:3000/api`
+- **认证方式**: 请求头 `x-user-id`(临时方案)
+- **响应格式**: JSON
+
+### 通用响应格式
+
+**成功响应**:
+
+```json
+{
+  "success": true,
+  "data": { ... },
+  "message": "操作成功"
+}
+```
+
+**错误响应**:
+
+```json
+{
+  "success": false,
+  "message": "错误信息",
+  "errors": ["详细错误1", "详细错误2"]
+}
+```
+
+### API端点
+
+#### 1. 体重记录相关
+
+##### 获取体重记录列表
+
+```http
+GET /api/weight/records
+Headers:
+  x-user-id: test-user-001
+
+Query Parameters:
+  - from: string (YYYY-MM-DD, optional)
+  - to: string (YYYY-MM-DD, optional)
+  - condition: string (fasting|after_meal, optional)
+  - tags: string[] (optional)
+  - page: number (default: 1)
+  - limit: number (default: 100)
+
+Response 200:
+{
+  "success": true,
+  "data": {
+    "records": [...],
+    "total": 90,
+    "page": 1,
+    "limit": 100
+  }
+}
+```
+
+##### 添加体重记录
+
+```http
+POST /api/weight/records
+Headers:
+  x-user-id: test-user-001
+  Content-Type: application/json
+
+Body:
+{
+  "date": "2025-10-22",
+  "measurementTime": "08:00",
+  "weight": 65.5,
+  "bodyFat": 22.3,
+  "muscleMass": 28.5,
+  "measurementCondition": "fasting",
+  "notes": "早晨测量",
+  "tags": ["开始运动"]
+}
+
+Response 201:
+{
+  "success": true,
+  "data": { ... },
+  "message": "体重记录添加成功"
+}
+```
+
+##### 更新体重记录
+
+```http
+PUT /api/weight/records/:id
+Headers:
+  x-user-id: test-user-001
+  Content-Type: application/json
+
+Body:
+{
+  "weight": 65.3,
+  "notes": "更新备注"
+}
+
+Response 200:
+{
+  "success": true,
+  "data": { ... },
+  "message": "更新成功"
+}
+```
+
+##### 删除体重记录
+
+```http
+DELETE /api/weight/records/:id
+Headers:
+  x-user-id: test-user-001
+
+Response 200:
+{
+  "success": true,
+  "message": "删除成功"
+}
+```
+
+#### 2. 目标管理相关
+
+##### 获取当前目标
+
+```http
+GET /api/weight/goal
+Headers:
+  x-user-id: test-user-001
+
+Response 200:
+{
+  "success": true,
+  "data": {
+    "id": "...",
+    "targetWeight": 65.0,
+    "targetBodyFat": 18.0,
+    "targetDate": "2025-12-31",
+    ...
+  }
+}
+```
+
+##### 设置目标
+
+```http
+POST /api/weight/goal
+Headers:
+  x-user-id: test-user-001
+  Content-Type: application/json
+
+Body:
+{
+  "targetWeight": 65.0,
+  "targetBodyFat": 18.0,
+  "targetDate": "2025-12-31"
+}
+
+Response 201:
+{
+  "success": true,
+  "data": { ... },
+  "message": "目标设置成功"
+}
+```
+
+##### 更新目标
+
+```http
+PUT /api/weight/goal
+Headers:
+  x-user-id: test-user-001
+  Content-Type: application/json
+
+Body:
+{
+  "targetWeight": 63.0
+}
+
+Response 200:
+{
+  "success": true,
+  "data": { ... },
+  "message": "目标更新成功"
+}
+```
+
+#### 3. 标签相关
+
+##### 获取标签列表
+
+```http
+GET /api/weight/tags
+Headers:
+  x-user-id: test-user-001
+
+Response 200:
+{
+  "success": true,
+  "data": [
+    {
+      "id": "...",
+      "name": "开始运动",
+      "type": "system",
+      "color": "#3b82f6"
+    },
+    ...
+  ]
+}
+```
+
+##### 创建自定义标签
+
+```http
+POST /api/weight/tags
+Headers:
+  x-user-id: test-user-001
+  Content-Type: application/json
+
+Body:
+{
+  "name": "新标签",
+  "color": "#f59e0b"
+}
+
+Response 201:
+{
+  "success": true,
+  "data": { ... },
+  "message": "标签创建成功"
+}
+```
+
+#### 4. 统计分析
+
+##### 获取统计数据
+
+```http
+GET /api/weight/stats
+Headers:
+  x-user-id: test-user-001
+
+Query Parameters:
+  - from: string (YYYY-MM-DD, optional)
+  - to: string (YYYY-MM-DD, optional)
+
+Response 200:
+{
+  "success": true,
+  "data": {
+    "currentWeight": 65.5,
+    "weightChange": -4.5,
+    "daysTracked": 60,
+    "avgWeeklyChange": -0.75,
+    "bodyFatChange": -2.8,
+    "goalProgress": 75.0,
+    "goalETA": "2025-11-15"
+  }
+}
+```
+
+## 测试
+
+### 使用curl测试
+
+```bash
+# 健康检查
+curl http://localhost:3000/api/health
+
+# 获取体重记录
+curl -H "x-user-id: test-user-001" \
+  http://localhost:3000/api/weight/records
+
+# 添加体重记录
+curl -X POST http://localhost:3000/api/weight/records \
+  -H "x-user-id: test-user-001" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "date": "2025-10-22",
+    "weight": 65.5,
+    "bodyFat": 22.3,
+    "muscleMass": 28.5
+  }'
+
+# 获取当前目标
+curl -H "x-user-id: test-user-001" \
+  http://localhost:3000/api/weight/goal
+
+# 获取统计数据
+curl -H "x-user-id: test-user-001" \
+  http://localhost:3000/api/weight/stats
+```
+
+### 使用Postman测试
+
+1. 导入 Postman Collection(待创建)
+2. 设置环境变量 `base_url = http://localhost:3000/api`
+3. 设置全局请求头 `x-user-id = test-user-001`
+
+## 部署
+
+### Docker部署(推荐)
+
+(待补充)
+
+### 传统部署
+
+1. 安装Node.js和MySQL
+2. 克隆代码并安装依赖
+3. 配置环境变量
+4. 初始化数据库
+5. 编译并启动服务
+
+```bash
+npm run build
+npm start
+```
+
+### 使用PM2管理
+
+```bash
+# 安装PM2
+npm install -g pm2
+
+# 启动服务
+pm2 start dist/server.js --name campus-health-api
+
+# 查看日志
+pm2 logs campus-health-api
+
+# 重启服务
+pm2 restart campus-health-api
+```
+
+## 开发指南
+
+### 添加新API接口
+
+1. 在 `src/dto/` 创建DTO
+2. 在 `src/services/` 创建服务
+3. 在 `src/controllers/` 创建控制器方法
+4. 在 `src/routes/` 添加路由
+
+### 代码规范
+
+```bash
+# 运行ESLint
+npm run lint
+
+# 格式化代码
+npm run format
+```
+
+## 常见问题
+
+### 1. 数据库连接失败
+
+检查 `.env` 文件中的数据库配置是否正确,确保MySQL服务已启动。
+
+### 2. 端口被占用
+
+修改 `.env` 中的 `PORT` 配置,或停止占用3000端口的进程。
+
+### 3. TypeORM同步错误
+
+确保 `DB_SYNCHRONIZE=false`,使用SQL脚本管理数据库结构。
+
+## 后续开发计划
+
+- [ ] 实现JWT认证
+- [ ] 添加异常检测API
+- [ ] 实现Redis缓存
+- [ ] 添加单元测试
+- [ ] 完善API文档
+- [ ] Docker容器化
+- [ ] 性能优化和监控
+
+## 许可证
+
+MIT
+
+## 联系方式
+
+如有问题或建议,请联系开发团队。
+

+ 355 - 0
campus_health_app/backend/database/README.md

@@ -0,0 +1,355 @@
+# 数据库文档
+
+## 概述
+
+本目录包含数智健调系统体重管理模块的数据库架构定义和相关脚本。
+
+## 文件说明
+
+| 文件名 | 说明 | 用途 |
+|--------|------|------|
+| `schema.sql` | 数据库架构定义 | 创建所有表结构、索引、约束 |
+| `init.sql` | 数据库初始化脚本 | 创建数据库和基本配置 |
+| `seed.sql` | 测试数据种子脚本 | 插入测试数据用于开发 |
+| `README.md` | 本文档 | 使用说明和参考 |
+
+## 快速开始
+
+### 前提条件
+
+- MySQL 8.0+ 或 MariaDB 10.5+
+- 数据库管理员权限
+
+### 安装步骤
+
+#### 1. 创建数据库
+
+```bash
+mysql -u root -p < init.sql
+```
+
+这将创建名为 `campus_health` 的数据库。
+
+#### 2. 创建表结构
+
+```bash
+mysql -u root -p campus_health < schema.sql
+```
+
+这将创建以下表:
+- `users` - 用户表
+- `weight_records` - 体重记录表 ⭐
+- `weight_goals` - 体重目标表 ⭐
+- `tags` - 标签表
+- `weight_record_tags` - 体重记录-标签关联表
+- `anomaly_logs` - 异常日志表
+
+#### 3. 插入测试数据(可选)
+
+```bash
+mysql -u root -p campus_health < seed.sql
+```
+
+这将插入:
+- 3个测试用户
+- 90天的体重记录数据
+- 1个活跃的体重目标
+- 系统预设标签
+- 示例异常日志
+
+## 数据库架构
+
+### 核心表
+
+#### users(用户表)
+存储用户基本信息。
+
+**关键字段**:
+- `id`: 用户唯一标识(UUID)
+- `phone`: 手机号(唯一)
+- `nickname`: 昵称
+- `school_id`: 学校ID
+- `is_under_14`: 是否未满14岁
+
+#### weight_records(体重记录表)⭐
+存储用户的体重测量记录。
+
+**关键字段**:
+- `id`: 记录唯一标识
+- `user_id`: 用户ID(外键)
+- `record_date`: 记录日期
+- `weight`: 体重(30-200kg)
+- `body_fat`: 体脂率(5-60%)
+- `muscle_mass`: 肌肉含量(10-100kg)
+- `measurement_condition`: 测量条件(空腹/餐后)
+
+**索引**:
+- `idx_user_date`: 复合索引(user_id, record_date DESC)- 高频查询优化
+- `idx_user_created`: 复合索引(user_id, created_at DESC)- 按创建时间查询
+
+#### weight_goals(体重目标表)⭐
+存储用户的体重目标设置。
+
+**关键字段**:
+- `target_weight`: 目标体重
+- `target_body_fat`: 目标体脂率
+- `target_date`: 目标日期
+- `start_weight`: 起始体重
+- `weekly_target`: 每周目标减重量
+- `status`: 目标状态(active/completed/abandoned)
+
+#### tags(标签表)
+存储系统预设和用户自定义标签。
+
+**系统预设标签**:
+- 开始运动
+- 目标调整
+- 饮食改变
+- 生病
+- 假期
+- 压力期
+
+#### weight_record_tags(关联表)
+多对多关系表,关联体重记录和标签。
+
+#### anomaly_logs(异常日志表)
+记录系统检测到的异常情况。
+
+**异常类型**:
+- `rapid_change`: 快速体重变化
+- `body_fat_anomaly`: 体脂率异常
+- `missing_data`: 缺失数据
+- `extreme_value`: 极端值
+
+### 数据关系
+
+```
+users (1) -----> (N) weight_records
+users (1) -----> (N) weight_goals
+users (1) -----> (N) tags (自定义标签)
+users (1) -----> (N) anomaly_logs
+
+weight_records (N) <-----> (N) tags (通过 weight_record_tags)
+```
+
+## 性能优化
+
+### 索引策略
+
+1. **主键索引**:所有表都有UUID主键
+2. **唯一索引**:`users.phone`, `tags.uk_user_tag`
+3. **复合索引**:
+   - `weight_records.idx_user_date` - 按用户和日期查询
+   - `weight_goals.idx_user_status` - 按用户和状态查询
+4. **单列索引**:外键字段、软删除字段
+
+### 查询优化建议
+
+```sql
+-- ✅ 好的查询:使用索引
+SELECT * FROM weight_records 
+WHERE user_id = 'xxx' 
+  AND record_date >= '2025-01-01'
+  AND deleted_at IS NULL
+ORDER BY record_date DESC;
+
+-- ❌ 避免:全表扫描
+SELECT * FROM weight_records 
+WHERE weight > 70
+  AND deleted_at IS NULL;
+
+-- ✅ 好的查询:使用复合索引
+SELECT * FROM weight_goals
+WHERE user_id = 'xxx'
+  AND status = 'active'
+  AND deleted_at IS NULL;
+```
+
+## 数据约束
+
+### Check 约束
+
+- `weight`: 30.00 - 200.00 kg
+- `body_fat`: 5.00 - 60.00 %
+- `muscle_mass`: 10.00 - 100.00 kg
+- `target_date`: 必须大于 start_date
+
+### 外键约束
+
+所有外键都设置了 `ON DELETE CASCADE`,确保数据一致性。
+
+## 备份与恢复
+
+### 备份数据库
+
+```bash
+# 完整备份
+mysqldump -u root -p campus_health > backup_$(date +%Y%m%d).sql
+
+# 仅备份结构
+mysqldump -u root -p --no-data campus_health > schema_backup.sql
+
+# 仅备份数据
+mysqldump -u root -p --no-create-info campus_health > data_backup.sql
+```
+
+### 恢复数据库
+
+```bash
+mysql -u root -p campus_health < backup_20251020.sql
+```
+
+## 常用查询示例
+
+### 1. 获取用户最近30天体重记录
+
+```sql
+SELECT 
+  record_date,
+  weight,
+  body_fat,
+  muscle_mass
+FROM weight_records
+WHERE user_id = 'test-user-001'
+  AND record_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
+  AND deleted_at IS NULL
+ORDER BY record_date DESC;
+```
+
+### 2. 获取用户当前活跃目标
+
+```sql
+SELECT *
+FROM weight_goals
+WHERE user_id = 'test-user-001'
+  AND status = 'active'
+  AND deleted_at IS NULL
+ORDER BY created_at DESC
+LIMIT 1;
+```
+
+### 3. 获取带标签的体重记录
+
+```sql
+SELECT 
+  wr.record_date,
+  wr.weight,
+  wr.body_fat,
+  GROUP_CONCAT(t.tag_name) AS tags
+FROM weight_records wr
+LEFT JOIN weight_record_tags wrt ON wr.id = wrt.weight_record_id
+LEFT JOIN tags t ON wrt.tag_id = t.id
+WHERE wr.user_id = 'test-user-001'
+  AND wr.deleted_at IS NULL
+GROUP BY wr.id, wr.record_date, wr.weight, wr.body_fat
+ORDER BY wr.record_date DESC
+LIMIT 10;
+```
+
+### 4. 计算用户体重统计
+
+```sql
+SELECT 
+  COUNT(*) AS total_records,
+  MIN(record_date) AS first_date,
+  MAX(record_date) AS last_date,
+  MIN(weight) AS min_weight,
+  MAX(weight) AS max_weight,
+  AVG(weight) AS avg_weight,
+  (MAX(weight) - MIN(weight)) AS weight_range
+FROM weight_records
+WHERE user_id = 'test-user-001'
+  AND deleted_at IS NULL;
+```
+
+### 5. 获取周体重变化
+
+```sql
+SELECT 
+  YEARWEEK(record_date) AS week,
+  AVG(weight) AS avg_weight,
+  MIN(weight) AS min_weight,
+  MAX(weight) AS max_weight,
+  COUNT(*) AS record_count
+FROM weight_records
+WHERE user_id = 'test-user-001'
+  AND record_date >= DATE_SUB(CURDATE(), INTERVAL 12 WEEK)
+  AND deleted_at IS NULL
+GROUP BY YEARWEEK(record_date)
+ORDER BY week DESC;
+```
+
+## 维护建议
+
+### 定期任务
+
+1. **数据清理**(建议每月执行)
+   ```sql
+   -- 清理1年前的软删除记录
+   DELETE FROM weight_records 
+   WHERE deleted_at IS NOT NULL 
+     AND deleted_at < DATE_SUB(NOW(), INTERVAL 1 YEAR);
+   ```
+
+2. **索引优化**(建议每季度执行)
+   ```sql
+   -- 分析表
+   ANALYZE TABLE weight_records;
+   ANALYZE TABLE weight_goals;
+   
+   -- 优化表
+   OPTIMIZE TABLE weight_records;
+   OPTIMIZE TABLE weight_goals;
+   ```
+
+3. **统计更新**
+   ```sql
+   -- 更新表统计信息
+   ANALYZE TABLE weight_records;
+   ```
+
+### 监控指标
+
+- 表大小增长趋势
+- 查询响应时间
+- 索引使用率
+- 慢查询日志
+
+## 故障排查
+
+### 常见问题
+
+#### 1. 外键约束错误
+
+```
+ERROR 1452: Cannot add or update a child row
+```
+
+**解决方案**:确保父表(users)中存在对应的记录。
+
+#### 2. Check 约束错误
+
+```
+ERROR 3819: Check constraint is violated
+```
+
+**解决方案**:检查数值是否在允许范围内(体重30-200kg,体脂率5-60%)。
+
+#### 3. 唯一键冲突
+
+```
+ERROR 1062: Duplicate entry
+```
+
+**解决方案**:检查是否插入了重复的手机号或用户标签组合。
+
+## 版本历史
+
+| 版本 | 日期 | 变更说明 |
+|------|------|---------|
+| 1.0 | 2025-10-20 | 初始版本,包含6张核心表 |
+
+## 联系方式
+
+如有问题或建议,请联系开发团队。
+

+ 26 - 0
campus_health_app/backend/database/init.sql

@@ -0,0 +1,26 @@
+-- ========================================
+-- 数据库初始化脚本
+-- ========================================
+-- 用途:创建数据库并设置基本配置
+-- 使用方法:mysql -u root -p < init.sql
+-- ========================================
+
+-- 创建数据库
+CREATE DATABASE IF NOT EXISTS `campus_health` 
+  DEFAULT CHARACTER SET utf8mb4 
+  DEFAULT COLLATE utf8mb4_unicode_ci;
+
+-- 使用数据库
+USE `campus_health`;
+
+-- 设置时区
+SET time_zone = '+08:00';
+
+-- 显示数据库信息
+SELECT 
+  '数据库创建成功' AS status,
+  DATABASE() AS database_name,
+  @@character_set_database AS charset,
+  @@collation_database AS collation,
+  NOW() AS created_at;
+

+ 180 - 0
campus_health_app/backend/database/schema.sql

@@ -0,0 +1,180 @@
+-- ========================================
+-- 数智健调系统 - 体重管理模块数据库架构
+-- ========================================
+-- 版本: 1.0
+-- 创建日期: 2025-10-20
+-- 说明: 体重管理核心表结构定义
+-- ========================================
+
+-- 设置字符集
+SET NAMES utf8mb4;
+SET CHARACTER SET utf8mb4;
+
+-- ========================================
+-- 表1: users (用户表)
+-- ========================================
+CREATE TABLE IF NOT EXISTS `users` (
+  `id` VARCHAR(36) NOT NULL COMMENT '用户ID(UUID)',
+  `phone` VARCHAR(20) NOT NULL COMMENT '手机号',
+  `nickname` VARCHAR(50) NOT NULL COMMENT '昵称',
+  `school_id` VARCHAR(36) DEFAULT NULL COMMENT '学校ID',
+  `grade` VARCHAR(20) DEFAULT NULL COMMENT '年级',
+  `is_under_14` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否未满14岁',
+  `parent_phone` VARCHAR(20) DEFAULT NULL COMMENT '家长手机号',
+  `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updated_at` TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `deleted_at` TIMESTAMP NULL DEFAULT NULL COMMENT '软删除时间',
+  
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_phone` (`phone`),
+  KEY `idx_school_id` (`school_id`),
+  KEY `idx_deleted_at` (`deleted_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
+
+-- ========================================
+-- 表2: weight_records (体重记录表) ⭐
+-- ========================================
+CREATE TABLE IF NOT EXISTS `weight_records` (
+  `id` VARCHAR(36) NOT NULL COMMENT '记录ID(UUID)',
+  `user_id` VARCHAR(36) NOT NULL COMMENT '用户ID',
+  `record_date` DATE NOT NULL COMMENT '记录日期',
+  `measurement_time` TIME DEFAULT NULL COMMENT '测量时间',
+  `weight` DECIMAL(5,2) NOT NULL COMMENT '体重(kg),范围30.00-200.00',
+  `body_fat` DECIMAL(4,2) NOT NULL COMMENT '体脂率(%),范围5.00-60.00',
+  `muscle_mass` DECIMAL(5,2) NOT NULL COMMENT '肌肉含量(kg),范围10.00-100.00',
+  `measurement_condition` ENUM('fasting', 'after_meal') DEFAULT NULL COMMENT '测量条件:空腹/餐后',
+  `notes` TEXT DEFAULT NULL COMMENT '备注',
+  `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updated_at` TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `deleted_at` TIMESTAMP NULL DEFAULT NULL COMMENT '软删除时间',
+  
+  PRIMARY KEY (`id`),
+  KEY `idx_user_date` (`user_id`, `record_date` DESC),
+  KEY `idx_user_created` (`user_id`, `created_at` DESC),
+  KEY `idx_measurement_condition` (`measurement_condition`),
+  KEY `idx_deleted_at` (`deleted_at`),
+  
+  CONSTRAINT `fk_weight_records_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
+  CONSTRAINT `chk_weight` CHECK (`weight` >= 30.00 AND `weight` <= 200.00),
+  CONSTRAINT `chk_body_fat` CHECK (`body_fat` >= 5.00 AND `body_fat` <= 60.00),
+  CONSTRAINT `chk_muscle_mass` CHECK (`muscle_mass` >= 10.00 AND `muscle_mass` <= 100.00)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='体重记录表';
+
+-- ========================================
+-- 表3: weight_goals (体重目标表) ⭐
+-- ========================================
+CREATE TABLE IF NOT EXISTS `weight_goals` (
+  `id` VARCHAR(36) NOT NULL COMMENT '目标ID(UUID)',
+  `user_id` VARCHAR(36) NOT NULL COMMENT '用户ID',
+  `target_weight` DECIMAL(5,2) NOT NULL COMMENT '目标体重(kg)',
+  `target_body_fat` DECIMAL(4,2) DEFAULT NULL COMMENT '目标体脂率(%)',
+  `target_date` DATE NOT NULL COMMENT '目标日期',
+  `start_weight` DECIMAL(5,2) NOT NULL COMMENT '起始体重(kg)',
+  `start_body_fat` DECIMAL(4,2) DEFAULT NULL COMMENT '起始体脂率(%)',
+  `start_date` DATE NOT NULL COMMENT '开始日期',
+  `weekly_target` DECIMAL(4,2) DEFAULT NULL COMMENT '每周目标减重量(kg)',
+  `status` ENUM('active', 'completed', 'abandoned') NOT NULL DEFAULT 'active' COMMENT '状态',
+  `completed_at` TIMESTAMP NULL DEFAULT NULL COMMENT '完成时间',
+  `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updated_at` TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `deleted_at` TIMESTAMP NULL DEFAULT NULL COMMENT '软删除时间',
+  
+  PRIMARY KEY (`id`),
+  KEY `idx_user_status` (`user_id`, `status`),
+  KEY `idx_user_created` (`user_id`, `created_at` DESC),
+  KEY `idx_deleted_at` (`deleted_at`),
+  
+  CONSTRAINT `fk_weight_goals_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
+  CONSTRAINT `chk_target_date` CHECK (`target_date` > `start_date`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='体重目标表';
+
+-- ========================================
+-- 表4: tags (标签表)
+-- ========================================
+CREATE TABLE IF NOT EXISTS `tags` (
+  `id` VARCHAR(36) NOT NULL COMMENT '标签ID(UUID)',
+  `user_id` VARCHAR(36) DEFAULT NULL COMMENT '用户ID(NULL表示系统标签)',
+  `tag_name` VARCHAR(50) NOT NULL COMMENT '标签名称',
+  `tag_type` ENUM('system', 'custom') NOT NULL DEFAULT 'custom' COMMENT '类型:系统/自定义',
+  `color` VARCHAR(7) DEFAULT NULL COMMENT '颜色代码(HEX)',
+  `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_user_tag` (`user_id`, `tag_name`),
+  KEY `idx_tag_type` (`tag_type`),
+  
+  CONSTRAINT `fk_tags_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='标签表';
+
+-- ========================================
+-- 表5: weight_record_tags (体重记录-标签关联表)
+-- ========================================
+CREATE TABLE IF NOT EXISTS `weight_record_tags` (
+  `id` VARCHAR(36) NOT NULL COMMENT '关联ID(UUID)',
+  `weight_record_id` VARCHAR(36) NOT NULL COMMENT '体重记录ID',
+  `tag_id` VARCHAR(36) NOT NULL COMMENT '标签ID',
+  `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_record_tag` (`weight_record_id`, `tag_id`),
+  KEY `idx_tag_id` (`tag_id`),
+  
+  CONSTRAINT `fk_wrt_record` FOREIGN KEY (`weight_record_id`) REFERENCES `weight_records` (`id`) ON DELETE CASCADE,
+  CONSTRAINT `fk_wrt_tag` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='体重记录-标签关联表';
+
+-- ========================================
+-- 表6: anomaly_logs (异常日志表) - 扩展表
+-- ========================================
+CREATE TABLE IF NOT EXISTS `anomaly_logs` (
+  `id` VARCHAR(36) NOT NULL COMMENT '日志ID(UUID)',
+  `user_id` VARCHAR(36) NOT NULL COMMENT '用户ID',
+  `anomaly_type` ENUM('rapid_change', 'body_fat_anomaly', 'missing_data', 'extreme_value') NOT NULL COMMENT '异常类型',
+  `severity` ENUM('info', 'warning', 'danger') NOT NULL COMMENT '严重程度',
+  `message` TEXT NOT NULL COMMENT '提示信息',
+  `detected_date` DATE NOT NULL COMMENT '检测日期',
+  `related_record_ids` JSON DEFAULT NULL COMMENT '相关记录ID数组',
+  `is_read` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否已读',
+  `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  
+  PRIMARY KEY (`id`),
+  KEY `idx_user_date` (`user_id`, `detected_date` DESC),
+  KEY `idx_is_read` (`is_read`),
+  
+  CONSTRAINT `fk_anomaly_logs_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='异常日志表';
+
+-- ========================================
+-- 插入系统预设标签
+-- ========================================
+INSERT INTO `tags` (`id`, `user_id`, `tag_name`, `tag_type`, `color`) VALUES
+(UUID(), NULL, '开始运动', 'system', '#3b82f6'),
+(UUID(), NULL, '目标调整', 'system', '#f59e0b'),
+(UUID(), NULL, '饮食改变', 'system', '#10b981'),
+(UUID(), NULL, '生病', 'system', '#ef4444'),
+(UUID(), NULL, '假期', 'system', '#8b5cf6'),
+(UUID(), NULL, '压力期', 'system', '#f97316')
+ON DUPLICATE KEY UPDATE `tag_name` = VALUES(`tag_name`);
+
+-- ========================================
+-- 创建视图:用户体重统计
+-- ========================================
+CREATE OR REPLACE VIEW `v_user_weight_stats` AS
+SELECT 
+  u.id AS user_id,
+  u.nickname,
+  COUNT(wr.id) AS total_records,
+  MIN(wr.record_date) AS first_record_date,
+  MAX(wr.record_date) AS last_record_date,
+  (SELECT weight FROM weight_records WHERE user_id = u.id AND deleted_at IS NULL ORDER BY record_date DESC LIMIT 1) AS current_weight,
+  (SELECT weight FROM weight_records WHERE user_id = u.id AND deleted_at IS NULL ORDER BY record_date ASC LIMIT 1) AS initial_weight,
+  (SELECT body_fat FROM weight_records WHERE user_id = u.id AND deleted_at IS NULL ORDER BY record_date DESC LIMIT 1) AS current_body_fat
+FROM users u
+LEFT JOIN weight_records wr ON u.id = wr.user_id AND wr.deleted_at IS NULL
+WHERE u.deleted_at IS NULL
+GROUP BY u.id, u.nickname;
+
+-- ========================================
+-- 表结构创建完成
+-- ========================================
+

+ 200 - 0
campus_health_app/backend/database/seed.sql

@@ -0,0 +1,200 @@
+-- ========================================
+-- 测试数据种子脚本
+-- ========================================
+-- 用途:插入测试数据用于开发和演示
+-- 使用方法:mysql -u root -p campus_health < seed.sql
+-- ========================================
+
+USE `campus_health`;
+
+-- ========================================
+-- 1. 插入测试用户
+-- ========================================
+INSERT INTO `users` (`id`, `phone`, `nickname`, `school_id`, `grade`, `is_under_14`, `parent_phone`) VALUES
+('test-user-001', '13800138001', '张三', 'school-001', '大一', FALSE, NULL),
+('test-user-002', '13800138002', '李四', 'school-001', '大二', FALSE, NULL),
+('test-user-003', '13800138003', '王五', 'school-001', '高一', TRUE, '13900139001')
+ON DUPLICATE KEY UPDATE nickname = VALUES(nickname);
+
+-- ========================================
+-- 2. 插入体重记录(90天数据)
+-- ========================================
+-- 为用户 test-user-001 生成90天的体重数据
+DELIMITER //
+
+CREATE PROCEDURE IF NOT EXISTS generate_weight_records()
+BEGIN
+  DECLARE i INT DEFAULT 0;
+  DECLARE test_date DATE;
+  DECLARE base_weight DECIMAL(5,2) DEFAULT 70.00;
+  DECLARE weight_var DECIMAL(5,2);
+  DECLARE body_fat_var DECIMAL(4,2);
+  
+  -- 删除已存在的测试数据
+  DELETE FROM weight_records WHERE user_id = 'test-user-001';
+  
+  -- 生成90天数据
+  WHILE i < 90 DO
+    SET test_date = DATE_SUB(CURDATE(), INTERVAL (90 - i) DAY);
+    SET weight_var = base_weight - (i * 0.05) + (RAND() * 0.4 - 0.2);
+    SET body_fat_var = 23.0 - (i * 0.03) + (RAND() * 0.3);
+    
+    INSERT INTO weight_records (
+      id, 
+      user_id, 
+      record_date, 
+      measurement_time,
+      weight, 
+      body_fat, 
+      muscle_mass,
+      measurement_condition
+    ) VALUES (
+      UUID(),
+      'test-user-001',
+      test_date,
+      '08:00:00',
+      ROUND(weight_var, 2),
+      ROUND(body_fat_var, 2),
+      ROUND(28.0 + (RAND() * 2), 2),
+      IF(i % 2 = 0, 'fasting', 'after_meal')
+    );
+    
+    SET i = i + 1;
+  END WHILE;
+  
+  SELECT CONCAT('成功生成 ', i, ' 条体重记录') AS result;
+END //
+
+DELIMITER ;
+
+-- 执行存储过程
+CALL generate_weight_records();
+
+-- 删除存储过程
+DROP PROCEDURE IF EXISTS generate_weight_records;
+
+-- ========================================
+-- 3. 插入体重目标
+-- ========================================
+INSERT INTO `weight_goals` (
+  `id`,
+  `user_id`,
+  `target_weight`,
+  `target_body_fat`,
+  `target_date`,
+  `start_weight`,
+  `start_body_fat`,
+  `start_date`,
+  `weekly_target`,
+  `status`
+) VALUES (
+  UUID(),
+  'test-user-001',
+  65.00,
+  18.00,
+  DATE_ADD(CURDATE(), INTERVAL 60 DAY),
+  70.00,
+  23.00,
+  DATE_SUB(CURDATE(), INTERVAL 90 DAY),
+  0.50,
+  'active'
+)
+ON DUPLICATE KEY UPDATE target_weight = VALUES(target_weight);
+
+-- ========================================
+-- 4. 为部分记录添加标签
+-- ========================================
+-- 获取系统标签ID并为记录添加标签
+INSERT INTO weight_record_tags (id, weight_record_id, tag_id)
+SELECT 
+  UUID(),
+  wr.id,
+  (SELECT id FROM tags WHERE tag_name = '开始运动' AND user_id IS NULL LIMIT 1)
+FROM weight_records wr
+WHERE wr.user_id = 'test-user-001'
+  AND wr.record_date = DATE_SUB(CURDATE(), INTERVAL 76 DAY)
+LIMIT 1
+ON DUPLICATE KEY UPDATE weight_record_id = VALUES(weight_record_id);
+
+INSERT INTO weight_record_tags (id, weight_record_id, tag_id)
+SELECT 
+  UUID(),
+  wr.id,
+  (SELECT id FROM tags WHERE tag_name = '目标调整' AND user_id IS NULL LIMIT 1)
+FROM weight_records wr
+WHERE wr.user_id = 'test-user-001'
+  AND wr.record_date = DATE_SUB(CURDATE(), INTERVAL 48 DAY)
+LIMIT 1
+ON DUPLICATE KEY UPDATE weight_record_id = VALUES(weight_record_id);
+
+-- ========================================
+-- 5. 插入异常日志示例
+-- ========================================
+INSERT INTO `anomaly_logs` (
+  `id`,
+  `user_id`,
+  `anomaly_type`,
+  `severity`,
+  `message`,
+  `detected_date`,
+  `is_read`
+) VALUES (
+  UUID(),
+  'test-user-001',
+  'rapid_change',
+  'warning',
+  '近7天体重变化过快(-2.5kg/周),建议放慢减重速度',
+  DATE_SUB(CURDATE(), INTERVAL 3 DAY),
+  FALSE
+)
+ON DUPLICATE KEY UPDATE message = VALUES(message);
+
+-- ========================================
+-- 6. 显示测试数据统计
+-- ========================================
+SELECT '=== 测试数据插入完成 ===' AS status;
+
+SELECT 
+  '用户数量' AS metric,
+  COUNT(*) AS count
+FROM users
+WHERE id LIKE 'test-user-%'
+UNION ALL
+SELECT 
+  '体重记录数量' AS metric,
+  COUNT(*) AS count
+FROM weight_records
+WHERE user_id = 'test-user-001'
+UNION ALL
+SELECT 
+  '目标数量' AS metric,
+  COUNT(*) AS count
+FROM weight_goals
+WHERE user_id = 'test-user-001'
+UNION ALL
+SELECT 
+  '标签数量' AS metric,
+  COUNT(*) AS count
+FROM tags
+UNION ALL
+SELECT 
+  '异常日志数量' AS metric,
+  COUNT(*) AS count
+FROM anomaly_logs
+WHERE user_id = 'test-user-001';
+
+-- 显示最近10条体重记录
+SELECT 
+  '=== 最近10条体重记录 ===' AS info;
+
+SELECT 
+  record_date,
+  weight,
+  body_fat,
+  muscle_mass,
+  measurement_condition
+FROM weight_records
+WHERE user_id = 'test-user-001'
+ORDER BY record_date DESC
+LIMIT 10;
+

+ 63 - 0
campus_health_app/backend/package.json

@@ -0,0 +1,63 @@
+{
+  "name": "campus-health-backend",
+  "version": "1.0.0",
+  "description": "数智健调系统后端API",
+  "main": "dist/server.js",
+  "scripts": {
+    "dev": "ts-node-dev --respawn --transpile-only src/server.ts",
+    "build": "tsc",
+    "start": "node dist/server.js",
+    "typeorm": "typeorm-ts-node-commonjs",
+    "migration:generate": "npm run typeorm -- migration:generate",
+    "migration:run": "npm run typeorm -- migration:run",
+    "migration:revert": "npm run typeorm -- migration:revert",
+    "test": "jest",
+    "lint": "eslint src --ext .ts",
+    "format": "prettier --write \"src/**/*.ts\""
+  },
+  "keywords": [
+    "health",
+    "weight-management",
+    "campus",
+    "api"
+  ],
+  "author": "",
+  "license": "MIT",
+  "dependencies": {
+    "express": "^4.18.2",
+    "typeorm": "^0.3.17",
+    "mysql2": "^3.6.5",
+    "reflect-metadata": "^0.1.13",
+    "class-validator": "^0.14.0",
+    "class-transformer": "^0.5.1",
+    "dotenv": "^16.3.1",
+    "cors": "^2.8.5",
+    "helmet": "^7.1.0",
+    "express-rate-limit": "^7.1.5",
+    "uuid": "^9.0.1",
+    "redis": "^4.6.11",
+    "winston": "^3.11.0",
+    "compression": "^1.7.4"
+  },
+  "devDependencies": {
+    "@types/express": "^4.17.21",
+    "@types/node": "^20.10.5",
+    "@types/cors": "^2.8.17",
+    "@types/uuid": "^9.0.7",
+    "@types/compression": "^1.7.5",
+    "@typescript-eslint/eslint-plugin": "^6.15.0",
+    "@typescript-eslint/parser": "^6.15.0",
+    "eslint": "^8.56.0",
+    "prettier": "^3.1.1",
+    "ts-node": "^10.9.2",
+    "ts-node-dev": "^2.0.0",
+    "typescript": "^5.3.3",
+    "jest": "^29.7.0",
+    "@types/jest": "^29.5.11"
+  },
+  "engines": {
+    "node": ">=18.0.0",
+    "npm": ">=9.0.0"
+  }
+}
+

+ 60 - 0
campus_health_app/backend/scripts/setup.bat

@@ -0,0 +1,60 @@
+@echo off
+REM ========================================
+REM 数智健调系统 - 后端快速设置脚本 (Windows)
+REM ========================================
+
+echo ========================================
+echo 数智健调系统 - 后端环境设置
+echo ========================================
+echo.
+
+REM 检查Node.js
+echo 检查Node.js版本...
+where node >nul 2>nul
+if %ERRORLEVEL% NEQ 0 (
+    echo ❌ 未检测到Node.js,请先安装Node.js 18+
+    exit /b 1
+)
+
+node -v
+echo ✅ Node.js已安装
+echo.
+
+REM 检查MySQL
+echo 检查MySQL...
+where mysql >nul 2>nul
+if %ERRORLEVEL% NEQ 0 (
+    echo ⚠️  未检测到MySQL,请确保已安装MySQL 8.0+
+)
+echo.
+
+REM 安装依赖
+echo 安装npm依赖...
+call npm install
+echo.
+
+REM 检查环境变量文件
+if not exist ".env" (
+    echo ⚠️  未找到.env文件
+    echo 请参考项目文档创建.env文件
+) else (
+    echo ✅ 环境变量文件已存在
+)
+echo.
+
+echo ========================================
+echo 下一步操作:
+echo ========================================
+echo 1. 配置.env文件(数据库连接信息)
+echo 2. 初始化数据库:
+echo    mysql -u root -p ^< database/init.sql
+echo    mysql -u root -p campus_health ^< database/schema.sql
+echo    mysql -u root -p campus_health ^< database/seed.sql
+echo 3. 启动开发服务器:
+echo    npm run dev
+echo.
+echo 更多信息请查看 README.md
+echo ========================================
+
+pause
+

+ 66 - 0
campus_health_app/backend/scripts/setup.sh

@@ -0,0 +1,66 @@
+#!/bin/bash
+
+# ========================================
+# 数智健调系统 - 后端快速设置脚本
+# ========================================
+
+set -e
+
+echo "========================================
+数智健调系统 - 后端环境设置
+========================================
+"
+
+# 检查Node.js版本
+echo "检查Node.js版本..."
+if ! command -v node &> /dev/null; then
+    echo "❌ 未检测到Node.js,请先安装Node.js 18+"
+    exit 1
+fi
+
+NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
+if [ "$NODE_VERSION" -lt 18 ]; then
+    echo "❌ Node.js版本过低,需要18+,当前版本: $(node -v)"
+    exit 1
+fi
+
+echo "✅ Node.js版本: $(node -v)"
+
+# 检查MySQL
+echo ""
+echo "检查MySQL..."
+if ! command -v mysql &> /dev/null; then
+    echo "⚠️  未检测到MySQL,请确保已安装MySQL 8.0+"
+fi
+
+# 安装依赖
+echo ""
+echo "安装npm依赖..."
+npm install
+
+# 检查环境变量文件
+echo ""
+if [ ! -f ".env" ]; then
+    echo "⚠️  未找到.env文件"
+    echo "请参考项目文档创建.env文件"
+else
+    echo "✅ 环境变量文件已存在"
+fi
+
+# 提示初始化数据库
+echo ""
+echo "========================================
+下一步操作:
+========================================
+1. 配置.env文件(数据库连接信息)
+2. 初始化数据库:
+   mysql -u root -p < database/init.sql
+   mysql -u root -p campus_health < database/schema.sql
+   mysql -u root -p campus_health < database/seed.sql
+3. 启动开发服务器:
+   npm run dev
+
+更多信息请查看 README.md
+========================================
+"
+

+ 67 - 0
campus_health_app/backend/src/config/database.ts

@@ -0,0 +1,67 @@
+import 'reflect-metadata';
+import { DataSource } from 'typeorm';
+import * as dotenv from 'dotenv';
+import * as path from 'path';
+
+// 加载环境变量
+dotenv.config();
+
+// 实体导入
+import { User } from '../entities/User';
+import { WeightRecord } from '../entities/WeightRecord';
+import { WeightGoal } from '../entities/WeightGoal';
+import { Tag } from '../entities/Tag';
+import { WeightRecordTag } from '../entities/WeightRecordTag';
+import { AnomalyLog } from '../entities/AnomalyLog';
+
+export const AppDataSource = new DataSource({
+  type: 'mysql',
+  host: process.env.DB_HOST || 'localhost',
+  port: parseInt(process.env.DB_PORT || '3306'),
+  username: process.env.DB_USERNAME || 'root',
+  password: process.env.DB_PASSWORD || '',
+  database: process.env.DB_DATABASE || 'campus_health',
+  synchronize: process.env.DB_SYNCHRONIZE === 'true',
+  logging: process.env.DB_LOGGING === 'true',
+  entities: [
+    User,
+    WeightRecord,
+    WeightGoal,
+    Tag,
+    WeightRecordTag,
+    AnomalyLog
+  ],
+  migrations: [path.join(__dirname, '../migrations/*.ts')],
+  subscribers: [],
+  charset: 'utf8mb4',
+  timezone: '+08:00',
+  extra: {
+    connectionLimit: 10
+  }
+});
+
+/**
+ * 初始化数据库连接
+ */
+export const initializeDatabase = async (): Promise<void> => {
+  try {
+    await AppDataSource.initialize();
+    console.log('✅ 数据库连接成功');
+  } catch (error) {
+    console.error('❌ 数据库连接失败:', error);
+    process.exit(1);
+  }
+};
+
+/**
+ * 关闭数据库连接
+ */
+export const closeDatabase = async (): Promise<void> => {
+  try {
+    await AppDataSource.destroy();
+    console.log('✅ 数据库连接已关闭');
+  } catch (error) {
+    console.error('❌ 关闭数据库连接失败:', error);
+  }
+};
+

+ 366 - 0
campus_health_app/backend/src/controllers/WeightController.ts

@@ -0,0 +1,366 @@
+import { Request, Response } from 'express';
+import { validate } from 'class-validator';
+import { plainToInstance } from 'class-transformer';
+import { WeightRecordService } from '../services/WeightRecordService';
+import { WeightGoalService } from '../services/WeightGoalService';
+import { TagService } from '../services/TagService';
+import { StatsService } from '../services/StatsService';
+import {
+  CreateWeightRecordDto,
+  UpdateWeightRecordDto,
+  WeightRecordQueryDto
+} from '../dto/weight-record.dto';
+import {
+  CreateWeightGoalDto,
+  UpdateWeightGoalDto
+} from '../dto/weight-goal.dto';
+import { CreateTagDto } from '../dto/tag.dto';
+
+export class WeightController {
+  private weightRecordService: WeightRecordService;
+  private weightGoalService: WeightGoalService;
+  private tagService: TagService;
+  private statsService: StatsService;
+
+  constructor() {
+    this.weightRecordService = new WeightRecordService();
+    this.weightGoalService = new WeightGoalService();
+    this.tagService = new TagService();
+    this.statsService = new StatsService();
+  }
+
+  // ========================================
+  // 体重记录相关接口
+  // ========================================
+
+  /**
+   * 获取体重记录列表
+   * GET /api/weight/records
+   */
+  getRecords = async (req: Request, res: Response): Promise<void> => {
+    try {
+      const userId = req.userId!; // 从认证中间件获取
+      const query = plainToInstance(WeightRecordQueryDto, req.query);
+
+      const errors = await validate(query);
+      if (errors.length > 0) {
+        res.status(400).json({
+          success: false,
+          message: '参数验证失败',
+          errors: errors.map(e => Object.values(e.constraints || {})).flat()
+        });
+        return;
+      }
+
+      const result = await this.weightRecordService.getRecords(userId, query);
+
+      res.json({
+        success: true,
+        data: result
+      });
+    } catch (error: any) {
+      console.error('获取体重记录失败:', error);
+      res.status(500).json({
+        success: false,
+        message: error.message || '服务器错误'
+      });
+    }
+  };
+
+  /**
+   * 添加体重记录
+   * POST /api/weight/records
+   */
+  createRecord = async (req: Request, res: Response): Promise<void> => {
+    try {
+      const userId = req.userId!;
+      const dto = plainToInstance(CreateWeightRecordDto, req.body);
+
+      const errors = await validate(dto);
+      if (errors.length > 0) {
+        res.status(400).json({
+          success: false,
+          message: '参数验证失败',
+          errors: errors.map(e => Object.values(e.constraints || {})).flat()
+        });
+        return;
+      }
+
+      const record = await this.weightRecordService.createRecord(userId, dto);
+
+      res.status(201).json({
+        success: true,
+        data: record,
+        message: '体重记录添加成功'
+      });
+    } catch (error: any) {
+      console.error('创建体重记录失败:', error);
+      res.status(500).json({
+        success: false,
+        message: error.message || '服务器错误'
+      });
+    }
+  };
+
+  /**
+   * 更新体重记录
+   * PUT /api/weight/records/:id
+   */
+  updateRecord = async (req: Request, res: Response): Promise<void> => {
+    try {
+      const userId = req.userId!;
+      const recordId = req.params.id;
+      const dto = plainToInstance(UpdateWeightRecordDto, req.body);
+
+      const errors = await validate(dto);
+      if (errors.length > 0) {
+        res.status(400).json({
+          success: false,
+          message: '参数验证失败',
+          errors: errors.map(e => Object.values(e.constraints || {})).flat()
+        });
+        return;
+      }
+
+      const record = await this.weightRecordService.updateRecord(recordId, userId, dto);
+
+      res.json({
+        success: true,
+        data: record,
+        message: '更新成功'
+      });
+    } catch (error: any) {
+      console.error('更新体重记录失败:', error);
+      const status = error.message === '记录不存在' ? 404 : 500;
+      res.status(status).json({
+        success: false,
+        message: error.message || '服务器错误'
+      });
+    }
+  };
+
+  /**
+   * 删除体重记录
+   * DELETE /api/weight/records/:id
+   */
+  deleteRecord = async (req: Request, res: Response): Promise<void> => {
+    try {
+      const userId = req.userId!;
+      const recordId = req.params.id;
+
+      await this.weightRecordService.deleteRecord(recordId, userId);
+
+      res.json({
+        success: true,
+        message: '删除成功'
+      });
+    } catch (error: any) {
+      console.error('删除体重记录失败:', error);
+      const status = error.message === '记录不存在' ? 404 : 500;
+      res.status(status).json({
+        success: false,
+        message: error.message || '服务器错误'
+      });
+    }
+  };
+
+  // ========================================
+  // 目标管理相关接口
+  // ========================================
+
+  /**
+   * 获取当前目标
+   * GET /api/weight/goal
+   */
+  getGoal = async (req: Request, res: Response): Promise<void> => {
+    try {
+      const userId = req.userId!;
+      const goal = await this.weightGoalService.getCurrentGoal(userId);
+
+      if (!goal) {
+        res.json({
+          success: true,
+          data: null
+        });
+        return;
+      }
+
+      res.json({
+        success: true,
+        data: this.weightGoalService.formatGoal(goal)
+      });
+    } catch (error: any) {
+      console.error('获取目标失败:', error);
+      res.status(500).json({
+        success: false,
+        message: error.message || '服务器错误'
+      });
+    }
+  };
+
+  /**
+   * 设置目标
+   * POST /api/weight/goal
+   */
+  createGoal = async (req: Request, res: Response): Promise<void> => {
+    try {
+      const userId = req.userId!;
+      const dto = plainToInstance(CreateWeightGoalDto, req.body);
+
+      const errors = await validate(dto);
+      if (errors.length > 0) {
+        res.status(400).json({
+          success: false,
+          message: '参数验证失败',
+          errors: errors.map(e => Object.values(e.constraints || {})).flat()
+        });
+        return;
+      }
+
+      const goal = await this.weightGoalService.createGoal(userId, dto);
+
+      res.status(201).json({
+        success: true,
+        data: this.weightGoalService.formatGoal(goal),
+        message: '目标设置成功'
+      });
+    } catch (error: any) {
+      console.error('创建目标失败:', error);
+      res.status(500).json({
+        success: false,
+        message: error.message || '服务器错误'
+      });
+    }
+  };
+
+  /**
+   * 更新目标
+   * PUT /api/weight/goal
+   */
+  updateGoal = async (req: Request, res: Response): Promise<void> => {
+    try {
+      const userId = req.userId!;
+      const dto = plainToInstance(UpdateWeightGoalDto, req.body);
+
+      const errors = await validate(dto);
+      if (errors.length > 0) {
+        res.status(400).json({
+          success: false,
+          message: '参数验证失败',
+          errors: errors.map(e => Object.values(e.constraints || {})).flat()
+        });
+        return;
+      }
+
+      const goal = await this.weightGoalService.updateGoal(userId, dto);
+
+      res.json({
+        success: true,
+        data: this.weightGoalService.formatGoal(goal),
+        message: '目标更新成功'
+      });
+    } catch (error: any) {
+      console.error('更新目标失败:', error);
+      const status = error.message === '未找到活跃的目标' ? 404 : 500;
+      res.status(status).json({
+        success: false,
+        message: error.message || '服务器错误'
+      });
+    }
+  };
+
+  // ========================================
+  // 标签相关接口
+  // ========================================
+
+  /**
+   * 获取标签列表
+   * GET /api/weight/tags
+   */
+  getTags = async (req: Request, res: Response): Promise<void> => {
+    try {
+      const userId = req.userId!;
+      const tags = await this.tagService.getTags(userId);
+
+      res.json({
+        success: true,
+        data: this.tagService.formatTags(tags)
+      });
+    } catch (error: any) {
+      console.error('获取标签失败:', error);
+      res.status(500).json({
+        success: false,
+        message: error.message || '服务器错误'
+      });
+    }
+  };
+
+  /**
+   * 创建自定义标签
+   * POST /api/weight/tags
+   */
+  createTag = async (req: Request, res: Response): Promise<void> => {
+    try {
+      const userId = req.userId!;
+      const dto = plainToInstance(CreateTagDto, req.body);
+
+      const errors = await validate(dto);
+      if (errors.length > 0) {
+        res.status(400).json({
+          success: false,
+          message: '参数验证失败',
+          errors: errors.map(e => Object.values(e.constraints || {})).flat()
+        });
+        return;
+      }
+
+      const tag = await this.tagService.createTag(userId, dto);
+
+      res.status(201).json({
+        success: true,
+        data: this.tagService.formatTag(tag),
+        message: '标签创建成功'
+      });
+    } catch (error: any) {
+      console.error('创建标签失败:', error);
+      const status = error.message === '标签已存在' ? 409 : 500;
+      res.status(status).json({
+        success: false,
+        message: error.message || '服务器错误'
+      });
+    }
+  };
+
+  // ========================================
+  // 统计分析接口
+  // ========================================
+
+  /**
+   * 获取统计数据
+   * GET /api/weight/stats
+   */
+  getStats = async (req: Request, res: Response): Promise<void> => {
+    try {
+      const userId = req.userId!;
+      const { from, to } = req.query;
+
+      const stats = await this.statsService.getStats(
+        userId,
+        from as string,
+        to as string
+      );
+
+      res.json({
+        success: true,
+        data: stats
+      });
+    } catch (error: any) {
+      console.error('获取统计数据失败:', error);
+      res.status(500).json({
+        success: false,
+        message: error.message || '服务器错误'
+      });
+    }
+  };
+}
+

+ 17 - 0
campus_health_app/backend/src/dto/tag.dto.ts

@@ -0,0 +1,17 @@
+import {
+  IsString,
+  IsOptional,
+  Length,
+  Matches
+} from 'class-validator';
+
+export class CreateTagDto {
+  @IsString({ message: '标签名称必须是字符串' })
+  @Length(1, 50, { message: '标签名称长度必须在1-50个字符之间' })
+  name!: string;
+
+  @IsOptional()
+  @Matches(/^#[0-9A-Fa-f]{6}$/, { message: '颜色必须是有效的HEX格式(如 #3b82f6)' })
+  color?: string;
+}
+

+ 42 - 0
campus_health_app/backend/src/dto/weight-goal.dto.ts

@@ -0,0 +1,42 @@
+import {
+  IsNumber,
+  IsOptional,
+  IsDateString,
+  Min,
+  Max
+} from 'class-validator';
+
+export class CreateWeightGoalDto {
+  @IsNumber({}, { message: '目标体重必须是数字' })
+  @Min(30, { message: '目标体重不能小于30kg' })
+  @Max(200, { message: '目标体重不能大于200kg' })
+  targetWeight!: number;
+
+  @IsOptional()
+  @IsNumber({}, { message: '目标体脂率必须是数字' })
+  @Min(5, { message: '目标体脂率不能小于5%' })
+  @Max(60, { message: '目标体脂率不能大于60%' })
+  targetBodyFat?: number;
+
+  @IsDateString({}, { message: '目标日期格式不正确' })
+  targetDate!: string;
+}
+
+export class UpdateWeightGoalDto {
+  @IsOptional()
+  @IsNumber({}, { message: '目标体重必须是数字' })
+  @Min(30, { message: '目标体重不能小于30kg' })
+  @Max(200, { message: '目标体重不能大于200kg' })
+  targetWeight?: number;
+
+  @IsOptional()
+  @IsNumber({}, { message: '目标体脂率必须是数字' })
+  @Min(5, { message: '目标体脂率不能小于5%' })
+  @Max(60, { message: '目标体脂率不能大于60%' })
+  targetBodyFat?: number;
+
+  @IsOptional()
+  @IsDateString({}, { message: '目标日期格式不正确' })
+  targetDate?: string;
+}
+

+ 113 - 0
campus_health_app/backend/src/dto/weight-record.dto.ts

@@ -0,0 +1,113 @@
+import {
+  IsString,
+  IsNumber,
+  IsOptional,
+  IsEnum,
+  IsDateString,
+  Min,
+  Max,
+  IsArray,
+  Matches
+} from 'class-validator';
+import { MeasurementCondition } from '../entities/WeightRecord';
+
+export class CreateWeightRecordDto {
+  @IsDateString({}, { message: '日期格式不正确' })
+  date!: string;
+
+  @IsOptional()
+  @Matches(/^([01]\d|2[0-3]):([0-5]\d)$/, { message: '时间格式不正确,应为 HH:MM' })
+  measurementTime?: string;
+
+  @IsNumber({}, { message: '体重必须是数字' })
+  @Min(30, { message: '体重不能小于30kg' })
+  @Max(200, { message: '体重不能大于200kg' })
+  weight!: number;
+
+  @IsNumber({}, { message: '体脂率必须是数字' })
+  @Min(5, { message: '体脂率不能小于5%' })
+  @Max(60, { message: '体脂率不能大于60%' })
+  bodyFat!: number;
+
+  @IsNumber({}, { message: '肌肉量必须是数字' })
+  @Min(10, { message: '肌肉量不能小于10kg' })
+  @Max(100, { message: '肌肉量不能大于100kg' })
+  muscleMass!: number;
+
+  @IsOptional()
+  @IsEnum(MeasurementCondition, { message: '测量条件只能是 fasting 或 after_meal' })
+  measurementCondition?: MeasurementCondition;
+
+  @IsOptional()
+  @IsString({ message: '备注必须是字符串' })
+  notes?: string;
+
+  @IsOptional()
+  @IsArray({ message: '标签必须是数组' })
+  @IsString({ each: true, message: '每个标签必须是字符串' })
+  tags?: string[];
+}
+
+export class UpdateWeightRecordDto {
+  @IsOptional()
+  @IsNumber({}, { message: '体重必须是数字' })
+  @Min(30, { message: '体重不能小于30kg' })
+  @Max(200, { message: '体重不能大于200kg' })
+  weight?: number;
+
+  @IsOptional()
+  @IsNumber({}, { message: '体脂率必须是数字' })
+  @Min(5, { message: '体脂率不能小于5%' })
+  @Max(60, { message: '体脂率不能大于60%' })
+  bodyFat?: number;
+
+  @IsOptional()
+  @IsNumber({}, { message: '肌肉量必须是数字' })
+  @Min(10, { message: '肌肉量不能小于10kg' })
+  @Max(100, { message: '肌肉量不能大于100kg' })
+  muscleMass?: number;
+
+  @IsOptional()
+  @IsEnum(MeasurementCondition, { message: '测量条件只能是 fasting 或 after_meal' })
+  measurementCondition?: MeasurementCondition;
+
+  @IsOptional()
+  @IsString({ message: '备注必须是字符串' })
+  notes?: string;
+
+  @IsOptional()
+  @IsArray({ message: '标签必须是数组' })
+  @IsString({ each: true, message: '每个标签必须是字符串' })
+  tags?: string[];
+}
+
+export class WeightRecordQueryDto {
+  @IsOptional()
+  @IsDateString({}, { message: '开始日期格式不正确' })
+  from?: string;
+
+  @IsOptional()
+  @IsDateString({}, { message: '结束日期格式不正确' })
+  to?: string;
+
+  @IsOptional()
+  @IsEnum(MeasurementCondition, { message: '测量条件只能是 fasting 或 after_meal' })
+  condition?: MeasurementCondition;
+
+  @IsOptional()
+  @IsArray({ message: '标签必须是数组' })
+  @IsString({ each: true, message: '每个标签必须是字符串' })
+  tags?: string[];
+
+  @IsOptional()
+  @IsNumber({}, { message: '页码必须是数字' })
+  @Min(1, { message: '页码不能小于1' })
+  page?: number;
+
+  @IsOptional()
+  @IsNumber({}, { message: '每页数量必须是数字' })
+  @Min(1, { message: '每页数量不能小于1' })
+  @Max(100, { message: '每页数量不能大于100' })
+  limit?: number;
+}
+

+ 71 - 0
campus_health_app/backend/src/entities/AnomalyLog.ts

@@ -0,0 +1,71 @@
+import {
+  Entity,
+  PrimaryColumn,
+  Column,
+  CreateDateColumn,
+  ManyToOne,
+  JoinColumn,
+  Index
+} from 'typeorm';
+import { User } from './User';
+
+export enum AnomalyType {
+  RAPID_CHANGE = 'rapid_change',
+  BODY_FAT_ANOMALY = 'body_fat_anomaly',
+  MISSING_DATA = 'missing_data',
+  EXTREME_VALUE = 'extreme_value'
+}
+
+export enum AnomalySeverity {
+  INFO = 'info',
+  WARNING = 'warning',
+  DANGER = 'danger'
+}
+
+@Entity('anomaly_logs')
+@Index('idx_user_date', ['userId', 'detectedDate'])
+@Index('idx_is_read', ['isRead'])
+export class AnomalyLog {
+  @PrimaryColumn('varchar', { length: 36, comment: '日志ID(UUID)' })
+  id!: string;
+
+  @Column('varchar', { length: 36, comment: '用户ID' })
+  userId!: string;
+
+  @Column({
+    type: 'enum',
+    enum: AnomalyType,
+    comment: '异常类型'
+  })
+  anomalyType!: AnomalyType;
+
+  @Column({
+    type: 'enum',
+    enum: AnomalySeverity,
+    comment: '严重程度'
+  })
+  severity!: AnomalySeverity;
+
+  @Column('text', { comment: '提示信息' })
+  message!: string;
+
+  @Column('date', { comment: '检测日期' })
+  detectedDate!: string;
+
+  @Column('json', { nullable: true, comment: '相关记录ID数组' })
+  relatedRecordIds!: string[] | null;
+
+  @Column('boolean', { default: false, comment: '是否已读' })
+  isRead!: boolean;
+
+  @CreateDateColumn({ comment: '创建时间' })
+  createdAt!: Date;
+
+  // 关联关系
+  @ManyToOne(() => User, user => user.anomalyLogs, {
+    onDelete: 'CASCADE'
+  })
+  @JoinColumn({ name: 'user_id' })
+  user!: User;
+}
+

+ 58 - 0
campus_health_app/backend/src/entities/Tag.ts

@@ -0,0 +1,58 @@
+import {
+  Entity,
+  PrimaryColumn,
+  Column,
+  CreateDateColumn,
+  ManyToOne,
+  JoinColumn,
+  OneToMany,
+  Index,
+  Unique
+} from 'typeorm';
+import { User } from './User';
+import { WeightRecordTag } from './WeightRecordTag';
+
+export enum TagType {
+  SYSTEM = 'system',
+  CUSTOM = 'custom'
+}
+
+@Entity('tags')
+@Unique('uk_user_tag', ['userId', 'tagName'])
+@Index('idx_tag_type', ['tagType'])
+export class Tag {
+  @PrimaryColumn('varchar', { length: 36, comment: '标签ID(UUID)' })
+  id!: string;
+
+  @Column('varchar', { length: 36, nullable: true, comment: '用户ID(NULL表示系统标签)' })
+  userId!: string | null;
+
+  @Column('varchar', { length: 50, comment: '标签名称' })
+  tagName!: string;
+
+  @Column({
+    type: 'enum',
+    enum: TagType,
+    default: TagType.CUSTOM,
+    comment: '类型'
+  })
+  tagType!: TagType;
+
+  @Column('varchar', { length: 7, nullable: true, comment: '颜色代码(HEX)' })
+  color!: string | null;
+
+  @CreateDateColumn({ comment: '创建时间' })
+  createdAt!: Date;
+
+  // 关联关系
+  @ManyToOne(() => User, user => user.tags, {
+    onDelete: 'CASCADE',
+    nullable: true
+  })
+  @JoinColumn({ name: 'user_id' })
+  user!: User | null;
+
+  @OneToMany(() => WeightRecordTag, weightRecordTag => weightRecordTag.tag)
+  weightRecordTags!: WeightRecordTag[];
+}
+

+ 60 - 0
campus_health_app/backend/src/entities/User.ts

@@ -0,0 +1,60 @@
+import {
+  Entity,
+  PrimaryColumn,
+  Column,
+  CreateDateColumn,
+  UpdateDateColumn,
+  DeleteDateColumn,
+  OneToMany
+} from 'typeorm';
+import { WeightRecord } from './WeightRecord';
+import { WeightGoal } from './WeightGoal';
+import { Tag } from './Tag';
+import { AnomalyLog } from './AnomalyLog';
+
+@Entity('users')
+export class User {
+  @PrimaryColumn('varchar', { length: 36, comment: '用户ID(UUID)' })
+  id!: string;
+
+  @Column('varchar', { length: 20, unique: true, comment: '手机号' })
+  phone!: string;
+
+  @Column('varchar', { length: 50, comment: '昵称' })
+  nickname!: string;
+
+  @Column('varchar', { length: 36, nullable: true, comment: '学校ID' })
+  schoolId!: string | null;
+
+  @Column('varchar', { length: 20, nullable: true, comment: '年级' })
+  grade!: string | null;
+
+  @Column('boolean', { default: false, comment: '是否未满14岁' })
+  isUnder14!: boolean;
+
+  @Column('varchar', { length: 20, nullable: true, comment: '家长手机号' })
+  parentPhone!: string | null;
+
+  @CreateDateColumn({ comment: '创建时间' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ nullable: true, comment: '更新时间' })
+  updatedAt!: Date | null;
+
+  @DeleteDateColumn({ nullable: true, comment: '软删除时间' })
+  deletedAt!: Date | null;
+
+  // 关联关系
+  @OneToMany(() => WeightRecord, weightRecord => weightRecord.user)
+  weightRecords!: WeightRecord[];
+
+  @OneToMany(() => WeightGoal, weightGoal => weightGoal.user)
+  weightGoals!: WeightGoal[];
+
+  @OneToMany(() => Tag, tag => tag.user)
+  tags!: Tag[];
+
+  @OneToMany(() => AnomalyLog, anomalyLog => anomalyLog.user)
+  anomalyLogs!: AnomalyLog[];
+}
+

+ 78 - 0
campus_health_app/backend/src/entities/WeightGoal.ts

@@ -0,0 +1,78 @@
+import {
+  Entity,
+  PrimaryColumn,
+  Column,
+  CreateDateColumn,
+  UpdateDateColumn,
+  DeleteDateColumn,
+  ManyToOne,
+  JoinColumn,
+  Index
+} from 'typeorm';
+import { User } from './User';
+
+export enum GoalStatus {
+  ACTIVE = 'active',
+  COMPLETED = 'completed',
+  ABANDONED = 'abandoned'
+}
+
+@Entity('weight_goals')
+@Index('idx_user_status', ['userId', 'status'])
+@Index('idx_user_created', ['userId', 'createdAt'])
+export class WeightGoal {
+  @PrimaryColumn('varchar', { length: 36, comment: '目标ID(UUID)' })
+  id!: string;
+
+  @Column('varchar', { length: 36, comment: '用户ID' })
+  userId!: string;
+
+  @Column('decimal', { precision: 5, scale: 2, comment: '目标体重(kg)' })
+  targetWeight!: number;
+
+  @Column('decimal', { precision: 4, scale: 2, nullable: true, comment: '目标体脂率(%)' })
+  targetBodyFat!: number | null;
+
+  @Column('date', { comment: '目标日期' })
+  targetDate!: string;
+
+  @Column('decimal', { precision: 5, scale: 2, comment: '起始体重(kg)' })
+  startWeight!: number;
+
+  @Column('decimal', { precision: 4, scale: 2, nullable: true, comment: '起始体脂率(%)' })
+  startBodyFat!: number | null;
+
+  @Column('date', { comment: '开始日期' })
+  startDate!: string;
+
+  @Column('decimal', { precision: 4, scale: 2, nullable: true, comment: '每周目标减重量(kg)' })
+  weeklyTarget!: number | null;
+
+  @Column({
+    type: 'enum',
+    enum: GoalStatus,
+    default: GoalStatus.ACTIVE,
+    comment: '状态'
+  })
+  status!: GoalStatus;
+
+  @Column('timestamp', { nullable: true, comment: '完成时间' })
+  completedAt!: Date | null;
+
+  @CreateDateColumn({ comment: '创建时间' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ nullable: true, comment: '更新时间' })
+  updatedAt!: Date | null;
+
+  @DeleteDateColumn({ nullable: true, comment: '软删除时间' })
+  deletedAt!: Date | null;
+
+  // 关联关系
+  @ManyToOne(() => User, user => user.weightGoals, {
+    onDelete: 'CASCADE'
+  })
+  @JoinColumn({ name: 'user_id' })
+  user!: User;
+}
+

+ 76 - 0
campus_health_app/backend/src/entities/WeightRecord.ts

@@ -0,0 +1,76 @@
+import {
+  Entity,
+  PrimaryColumn,
+  Column,
+  CreateDateColumn,
+  UpdateDateColumn,
+  DeleteDateColumn,
+  ManyToOne,
+  JoinColumn,
+  OneToMany,
+  Index
+} from 'typeorm';
+import { User } from './User';
+import { WeightRecordTag } from './WeightRecordTag';
+
+export enum MeasurementCondition {
+  FASTING = 'fasting',
+  AFTER_MEAL = 'after_meal'
+}
+
+@Entity('weight_records')
+@Index('idx_user_date', ['userId', 'recordDate'])
+@Index('idx_user_created', ['userId', 'createdAt'])
+export class WeightRecord {
+  @PrimaryColumn('varchar', { length: 36, comment: '记录ID(UUID)' })
+  id!: string;
+
+  @Column('varchar', { length: 36, comment: '用户ID' })
+  userId!: string;
+
+  @Column('date', { comment: '记录日期' })
+  recordDate!: string;
+
+  @Column('time', { nullable: true, comment: '测量时间' })
+  measurementTime!: string | null;
+
+  @Column('decimal', { precision: 5, scale: 2, comment: '体重(kg)' })
+  weight!: number;
+
+  @Column('decimal', { precision: 4, scale: 2, comment: '体脂率(%)' })
+  bodyFat!: number;
+
+  @Column('decimal', { precision: 5, scale: 2, comment: '肌肉含量(kg)' })
+  muscleMass!: number;
+
+  @Column({
+    type: 'enum',
+    enum: MeasurementCondition,
+    nullable: true,
+    comment: '测量条件'
+  })
+  measurementCondition!: MeasurementCondition | null;
+
+  @Column('text', { nullable: true, comment: '备注' })
+  notes!: string | null;
+
+  @CreateDateColumn({ comment: '创建时间' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ nullable: true, comment: '更新时间' })
+  updatedAt!: Date | null;
+
+  @DeleteDateColumn({ nullable: true, comment: '软删除时间' })
+  deletedAt!: Date | null;
+
+  // 关联关系
+  @ManyToOne(() => User, user => user.weightRecords, {
+    onDelete: 'CASCADE'
+  })
+  @JoinColumn({ name: 'user_id' })
+  user!: User;
+
+  @OneToMany(() => WeightRecordTag, weightRecordTag => weightRecordTag.weightRecord)
+  weightRecordTags!: WeightRecordTag[];
+}
+

+ 43 - 0
campus_health_app/backend/src/entities/WeightRecordTag.ts

@@ -0,0 +1,43 @@
+import {
+  Entity,
+  PrimaryColumn,
+  Column,
+  CreateDateColumn,
+  ManyToOne,
+  JoinColumn,
+  Index,
+  Unique
+} from 'typeorm';
+import { WeightRecord } from './WeightRecord';
+import { Tag } from './Tag';
+
+@Entity('weight_record_tags')
+@Unique('uk_record_tag', ['weightRecordId', 'tagId'])
+@Index('idx_tag_id', ['tagId'])
+export class WeightRecordTag {
+  @PrimaryColumn('varchar', { length: 36, comment: '关联ID(UUID)' })
+  id!: string;
+
+  @Column('varchar', { length: 36, comment: '体重记录ID' })
+  weightRecordId!: string;
+
+  @Column('varchar', { length: 36, comment: '标签ID' })
+  tagId!: string;
+
+  @CreateDateColumn({ comment: '创建时间' })
+  createdAt!: Date;
+
+  // 关联关系
+  @ManyToOne(() => WeightRecord, weightRecord => weightRecord.weightRecordTags, {
+    onDelete: 'CASCADE'
+  })
+  @JoinColumn({ name: 'weight_record_id' })
+  weightRecord!: WeightRecord;
+
+  @ManyToOne(() => Tag, tag => tag.weightRecordTags, {
+    onDelete: 'CASCADE'
+  })
+  @JoinColumn({ name: 'tag_id' })
+  tag!: Tag;
+}
+

+ 32 - 0
campus_health_app/backend/src/middlewares/auth.middleware.ts

@@ -0,0 +1,32 @@
+import { Request, Response, NextFunction } from 'express';
+
+/**
+ * 临时认证中间件
+ * TODO: 实现真实的JWT认证
+ */
+export const authMiddleware = (req: Request, res: Response, next: NextFunction): void => {
+  // 临时方案:从请求头获取用户ID
+  const userId = req.headers['x-user-id'] as string;
+
+  if (!userId) {
+    res.status(401).json({
+      success: false,
+      message: '未授权:请提供用户ID'
+    });
+    return;
+  }
+
+  // 将用户ID附加到请求对象
+  req.userId = userId;
+  next();
+};
+
+// 扩展Express Request类型
+declare global {
+  namespace Express {
+    interface Request {
+      userId?: string;
+    }
+  }
+}
+

+ 38 - 0
campus_health_app/backend/src/middlewares/error.middleware.ts

@@ -0,0 +1,38 @@
+import { Request, Response, NextFunction } from 'express';
+
+/**
+ * 全局错误处理中间件
+ */
+export const errorMiddleware = (
+  err: any,
+  req: Request,
+  res: Response,
+  next: NextFunction
+): void => {
+  console.error('错误:', err);
+
+  const status = err.status || 500;
+  const message = err.message || '服务器内部错误';
+
+  res.status(status).json({
+    success: false,
+    message,
+    ...(process.env.NODE_ENV === 'development' && {
+      stack: err.stack
+    })
+  });
+};
+
+/**
+ * 404处理中间件
+ */
+export const notFoundMiddleware = (
+  req: Request,
+  res: Response
+): void => {
+  res.status(404).json({
+    success: false,
+    message: `路由未找到: ${req.method} ${req.path}`
+  });
+};
+

+ 25 - 0
campus_health_app/backend/src/middlewares/logger.middleware.ts

@@ -0,0 +1,25 @@
+import { Request, Response, NextFunction } from 'express';
+
+/**
+ * 请求日志中间件
+ */
+export const loggerMiddleware = (
+  req: Request,
+  res: Response,
+  next: NextFunction
+): void => {
+  const start = Date.now();
+
+  res.on('finish', () => {
+    const duration = Date.now() - start;
+    const { method, originalUrl, ip } = req;
+    const { statusCode } = res;
+
+    console.log(
+      `${method} ${originalUrl} ${statusCode} ${duration}ms - ${ip}`
+    );
+  });
+
+  next();
+};
+

+ 19 - 0
campus_health_app/backend/src/routes/index.ts

@@ -0,0 +1,19 @@
+import { Router } from 'express';
+import weightRoutes from './weight.routes';
+
+const router = Router();
+
+// 挂载体重管理路由
+router.use('/weight', weightRoutes);
+
+// 健康检查路由
+router.get('/health', (req, res) => {
+  res.json({
+    success: true,
+    message: 'API运行正常',
+    timestamp: new Date().toISOString()
+  });
+});
+
+export default router;
+

+ 58 - 0
campus_health_app/backend/src/routes/weight.routes.ts

@@ -0,0 +1,58 @@
+import { Router } from 'express';
+import { WeightController } from '../controllers/WeightController';
+import { authMiddleware } from '../middlewares/auth.middleware';
+
+const router = Router();
+const weightController = new WeightController();
+
+// 所有路由都需要认证
+router.use(authMiddleware);
+
+// ========================================
+// 体重记录相关路由
+// ========================================
+
+// 获取体重记录列表
+router.get('/records', weightController.getRecords);
+
+// 添加体重记录
+router.post('/records', weightController.createRecord);
+
+// 更新体重记录
+router.put('/records/:id', weightController.updateRecord);
+
+// 删除体重记录
+router.delete('/records/:id', weightController.deleteRecord);
+
+// ========================================
+// 目标管理相关路由
+// ========================================
+
+// 获取当前目标
+router.get('/goal', weightController.getGoal);
+
+// 设置目标
+router.post('/goal', weightController.createGoal);
+
+// 更新目标
+router.put('/goal', weightController.updateGoal);
+
+// ========================================
+// 标签相关路由
+// ========================================
+
+// 获取标签列表
+router.get('/tags', weightController.getTags);
+
+// 创建自定义标签
+router.post('/tags', weightController.createTag);
+
+// ========================================
+// 统计分析路由
+// ========================================
+
+// 获取统计数据
+router.get('/stats', weightController.getStats);
+
+export default router;
+

+ 118 - 0
campus_health_app/backend/src/server.ts

@@ -0,0 +1,118 @@
+import 'reflect-metadata';
+import express, { Express } from 'express';
+import cors from 'cors';
+import helmet from 'helmet';
+import compression from 'compression';
+import rateLimit from 'express-rate-limit';
+import * as dotenv from 'dotenv';
+
+import { initializeDatabase, closeDatabase } from './config/database';
+import routes from './routes';
+import { loggerMiddleware } from './middlewares/logger.middleware';
+import { errorMiddleware, notFoundMiddleware } from './middlewares/error.middleware';
+
+// 加载环境变量
+dotenv.config();
+
+const app: Express = express();
+const PORT = process.env.PORT || 3000;
+const API_PREFIX = process.env.API_PREFIX || '/api';
+
+// ========================================
+// 中间件配置
+// ========================================
+
+// 安全头部
+app.use(helmet());
+
+// 压缩响应
+app.use(compression());
+
+// CORS配置
+app.use(cors({
+  origin: process.env.CORS_ORIGIN || 'http://localhost:4200',
+  credentials: true
+}));
+
+// 请求体解析
+app.use(express.json({ limit: '10mb' }));
+app.use(express.urlencoded({ extended: true, limit: '10mb' }));
+
+// 请求日志
+app.use(loggerMiddleware);
+
+// 限流配置
+const limiter = rateLimit({
+  windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000'), // 15分钟
+  max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100'), // 最多100个请求
+  message: {
+    success: false,
+    message: '请求过于频繁,请稍后再试'
+  }
+});
+app.use(API_PREFIX, limiter);
+
+// ========================================
+// 路由配置
+// ========================================
+
+app.use(API_PREFIX, routes);
+
+// ========================================
+// 错误处理
+// ========================================
+
+app.use(notFoundMiddleware);
+app.use(errorMiddleware);
+
+// ========================================
+// 启动服务器
+// ========================================
+
+const startServer = async () => {
+  try {
+    // 初始化数据库连接
+    await initializeDatabase();
+
+    // 启动HTTP服务器
+    app.listen(PORT, () => {
+      console.log('========================================');
+      console.log('🚀 数智健调系统后端API启动成功');
+      console.log('========================================');
+      console.log(`📡 服务器地址: http://localhost:${PORT}`);
+      console.log(`🔗 API地址: http://localhost:${PORT}${API_PREFIX}`);
+      console.log(`🌍 环境: ${process.env.NODE_ENV || 'development'}`);
+      console.log(`📊 数据库: ${process.env.DB_DATABASE}`);
+      console.log('========================================');
+    });
+
+    // 优雅关闭
+    process.on('SIGTERM', gracefulShutdown);
+    process.on('SIGINT', gracefulShutdown);
+  } catch (error) {
+    console.error('❌ 服务器启动失败:', error);
+    process.exit(1);
+  }
+};
+
+/**
+ * 优雅关闭
+ */
+const gracefulShutdown = async () => {
+  console.log('\n正在关闭服务器...');
+  
+  try {
+    await closeDatabase();
+    console.log('✅ 服务器已安全关闭');
+    process.exit(0);
+  } catch (error) {
+    console.error('❌ 关闭服务器时发生错误:', error);
+    process.exit(1);
+  }
+};
+
+// 启动服务器
+startServer();
+
+export default app;
+

+ 100 - 0
campus_health_app/backend/src/services/StatsService.ts

@@ -0,0 +1,100 @@
+import { Repository } from 'typeorm';
+import { AppDataSource } from '../config/database';
+import { WeightRecord } from '../entities/WeightRecord';
+import { WeightGoal, GoalStatus } from '../entities/WeightGoal';
+
+export class StatsService {
+  private weightRecordRepository: Repository<WeightRecord>;
+  private weightGoalRepository: Repository<WeightGoal>;
+
+  constructor() {
+    this.weightRecordRepository = AppDataSource.getRepository(WeightRecord);
+    this.weightGoalRepository = AppDataSource.getRepository(WeightGoal);
+  }
+
+  /**
+   * 获取用户统计数据
+   */
+  async getStats(userId: string, from?: string, to?: string): Promise<any> {
+    // 获取记录
+    const queryBuilder = this.weightRecordRepository
+      .createQueryBuilder('wr')
+      .where('wr.userId = :userId', { userId })
+      .andWhere('wr.deletedAt IS NULL');
+
+    if (from) {
+      queryBuilder.andWhere('wr.recordDate >= :from', { from });
+    }
+    if (to) {
+      queryBuilder.andWhere('wr.recordDate <= :to', { to });
+    }
+
+    const records = await queryBuilder
+      .orderBy('wr.recordDate', 'ASC')
+      .getMany();
+
+    if (records.length === 0) {
+      return {
+        currentWeight: null,
+        weightChange: null,
+        daysTracked: 0,
+        avgWeeklyChange: null,
+        bodyFatChange: null,
+        goalProgress: null,
+        goalETA: null
+      };
+    }
+
+    // 基础统计
+    const firstRecord = records[0];
+    const latestRecord = records[records.length - 1];
+    const currentWeight = Number(latestRecord.weight);
+    const initialWeight = Number(firstRecord.weight);
+    const weightChange = Number((currentWeight - initialWeight).toFixed(2));
+    const bodyFatChange = Number((Number(latestRecord.bodyFat) - Number(firstRecord.bodyFat)).toFixed(2));
+
+    // 天数统计
+    const firstDate = new Date(firstRecord.recordDate);
+    const latestDate = new Date(latestRecord.recordDate);
+    const daysTracked = Math.ceil((latestDate.getTime() - firstDate.getTime()) / (1000 * 60 * 60 * 24)) + 1;
+
+    // 平均每周变化
+    const weeksTracked = daysTracked / 7;
+    const avgWeeklyChange = weeksTracked > 0 ? Number((weightChange / weeksTracked).toFixed(2)) : 0;
+
+    // 获取活跃目标
+    const goal = await this.weightGoalRepository.findOne({
+      where: { userId, status: GoalStatus.ACTIVE }
+    });
+
+    let goalProgress: number | null = null;
+    let goalETA: string | null = null;
+
+    if (goal) {
+      const totalWeightDiff = Math.abs(Number(goal.targetWeight) - Number(goal.startWeight));
+      const currentWeightDiff = Math.abs(Number(goal.targetWeight) - currentWeight);
+      goalProgress = totalWeightDiff > 0 
+        ? Number((((totalWeightDiff - currentWeightDiff) / totalWeightDiff) * 100).toFixed(1))
+        : 100;
+
+      // 计算ETA
+      if (avgWeeklyChange !== 0 && currentWeightDiff > 0) {
+        const weeksNeeded = currentWeightDiff / Math.abs(avgWeeklyChange);
+        const etaDate = new Date();
+        etaDate.setDate(etaDate.getDate() + Math.ceil(weeksNeeded * 7));
+        goalETA = etaDate.toISOString().split('T')[0];
+      }
+    }
+
+    return {
+      currentWeight,
+      weightChange,
+      daysTracked,
+      avgWeeklyChange,
+      bodyFatChange,
+      goalProgress,
+      goalETA
+    };
+  }
+}
+

+ 74 - 0
campus_health_app/backend/src/services/TagService.ts

@@ -0,0 +1,74 @@
+import { Repository } from 'typeorm';
+import { v4 as uuidv4 } from 'uuid';
+import { AppDataSource } from '../config/database';
+import { Tag, TagType } from '../entities/Tag';
+import { CreateTagDto } from '../dto/tag.dto';
+
+export class TagService {
+  private tagRepository: Repository<Tag>;
+
+  constructor() {
+    this.tagRepository = AppDataSource.getRepository(Tag);
+  }
+
+  /**
+   * 获取标签列表(系统标签 + 用户自定义标签)
+   */
+  async getTags(userId: string): Promise<Tag[]> {
+    const tags = await this.tagRepository.find({
+      where: [
+        { userId: null }, // 系统标签
+        { userId } // 用户自定义标签
+      ],
+      order: { tagType: 'ASC', createdAt: 'DESC' }
+    });
+
+    return tags;
+  }
+
+  /**
+   * 创建自定义标签
+   */
+  async createTag(userId: string, dto: CreateTagDto): Promise<Tag> {
+    // 检查标签是否已存在
+    const existingTag = await this.tagRepository.findOne({
+      where: { userId, tagName: dto.name }
+    });
+
+    if (existingTag) {
+      throw new Error('标签已存在');
+    }
+
+    const tag = this.tagRepository.create({
+      id: uuidv4(),
+      userId,
+      tagName: dto.name,
+      tagType: TagType.CUSTOM,
+      color: dto.color || null
+    });
+
+    await this.tagRepository.save(tag);
+
+    return tag;
+  }
+
+  /**
+   * 格式化标签输出
+   */
+  formatTag(tag: Tag): any {
+    return {
+      id: tag.id,
+      name: tag.tagName,
+      type: tag.tagType,
+      color: tag.color
+    };
+  }
+
+  /**
+   * 批量格式化标签
+   */
+  formatTags(tags: Tag[]): any[] {
+    return tags.map(tag => this.formatTag(tag));
+  }
+}
+

+ 127 - 0
campus_health_app/backend/src/services/WeightGoalService.ts

@@ -0,0 +1,127 @@
+import { Repository } from 'typeorm';
+import { v4 as uuidv4 } from 'uuid';
+import { AppDataSource } from '../config/database';
+import { WeightGoal, GoalStatus } from '../entities/WeightGoal';
+import { WeightRecord } from '../entities/WeightRecord';
+import { CreateWeightGoalDto, UpdateWeightGoalDto } from '../dto/weight-goal.dto';
+
+export class WeightGoalService {
+  private weightGoalRepository: Repository<WeightGoal>;
+  private weightRecordRepository: Repository<WeightRecord>;
+
+  constructor() {
+    this.weightGoalRepository = AppDataSource.getRepository(WeightGoal);
+    this.weightRecordRepository = AppDataSource.getRepository(WeightRecord);
+  }
+
+  /**
+   * 创建体重目标
+   */
+  async createGoal(userId: string, dto: CreateWeightGoalDto): Promise<WeightGoal> {
+    // 获取用户最新体重记录
+    const latestRecord = await this.weightRecordRepository.findOne({
+      where: { userId },
+      order: { recordDate: 'DESC', createdAt: 'DESC' }
+    });
+
+    if (!latestRecord) {
+      throw new Error('请先添加体重记录');
+    }
+
+    // 将之前的活跃目标标记为已放弃
+    await this.weightGoalRepository.update(
+      { userId, status: GoalStatus.ACTIVE },
+      { status: GoalStatus.ABANDONED }
+    );
+
+    // 计算每周目标
+    const startDate = new Date(latestRecord.recordDate);
+    const targetDate = new Date(dto.targetDate);
+    const daysDiff = Math.ceil((targetDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
+    const weeksDiff = daysDiff / 7;
+    const weightDiff = Math.abs(Number(latestRecord.weight) - dto.targetWeight);
+    const weeklyTarget = weeksDiff > 0 ? Number((weightDiff / weeksDiff).toFixed(2)) : 0;
+
+    // 创建新目标
+    const goal = this.weightGoalRepository.create({
+      id: uuidv4(),
+      userId,
+      targetWeight: dto.targetWeight,
+      targetBodyFat: dto.targetBodyFat || null,
+      targetDate: dto.targetDate,
+      startWeight: Number(latestRecord.weight),
+      startBodyFat: Number(latestRecord.bodyFat),
+      startDate: latestRecord.recordDate,
+      weeklyTarget,
+      status: GoalStatus.ACTIVE
+    });
+
+    await this.weightGoalRepository.save(goal);
+
+    return goal;
+  }
+
+  /**
+   * 更新体重目标
+   */
+  async updateGoal(userId: string, dto: UpdateWeightGoalDto): Promise<WeightGoal> {
+    const goal = await this.weightGoalRepository.findOne({
+      where: { userId, status: GoalStatus.ACTIVE }
+    });
+
+    if (!goal) {
+      throw new Error('未找到活跃的目标');
+    }
+
+    // 更新目标
+    if (dto.targetWeight !== undefined) goal.targetWeight = dto.targetWeight;
+    if (dto.targetBodyFat !== undefined) goal.targetBodyFat = dto.targetBodyFat;
+    if (dto.targetDate !== undefined) goal.targetDate = dto.targetDate;
+
+    // 重新计算每周目标
+    if (dto.targetWeight !== undefined || dto.targetDate !== undefined) {
+      const startDate = new Date(goal.startDate);
+      const targetDate = new Date(goal.targetDate);
+      const daysDiff = Math.ceil((targetDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
+      const weeksDiff = daysDiff / 7;
+      const weightDiff = Math.abs(Number(goal.startWeight) - goal.targetWeight);
+      goal.weeklyTarget = weeksDiff > 0 ? Number((weightDiff / weeksDiff).toFixed(2)) : 0;
+    }
+
+    await this.weightGoalRepository.save(goal);
+
+    return goal;
+  }
+
+  /**
+   * 获取当前活跃目标
+   */
+  async getCurrentGoal(userId: string): Promise<WeightGoal | null> {
+    const goal = await this.weightGoalRepository.findOne({
+      where: { userId, status: GoalStatus.ACTIVE },
+      order: { createdAt: 'DESC' }
+    });
+
+    return goal;
+  }
+
+  /**
+   * 格式化目标输出
+   */
+  formatGoal(goal: WeightGoal): any {
+    return {
+      id: goal.id,
+      targetWeight: Number(goal.targetWeight),
+      targetBodyFat: goal.targetBodyFat ? Number(goal.targetBodyFat) : null,
+      targetDate: goal.targetDate,
+      startWeight: Number(goal.startWeight),
+      startBodyFat: goal.startBodyFat ? Number(goal.startBodyFat) : null,
+      startDate: goal.startDate,
+      weeklyTarget: goal.weeklyTarget ? Number(goal.weeklyTarget) : null,
+      status: goal.status,
+      createdAt: goal.createdAt.getTime(),
+      updatedAt: goal.updatedAt?.getTime() || goal.createdAt.getTime()
+    };
+  }
+}
+

+ 233 - 0
campus_health_app/backend/src/services/WeightRecordService.ts

@@ -0,0 +1,233 @@
+import { Repository, Between, In } from 'typeorm';
+import { v4 as uuidv4 } from 'uuid';
+import { AppDataSource } from '../config/database';
+import { WeightRecord, MeasurementCondition } from '../entities/WeightRecord';
+import { WeightRecordTag } from '../entities/WeightRecordTag';
+import { Tag } from '../entities/Tag';
+import { CreateWeightRecordDto, UpdateWeightRecordDto, WeightRecordQueryDto } from '../dto/weight-record.dto';
+
+export class WeightRecordService {
+  private weightRecordRepository: Repository<WeightRecord>;
+  private weightRecordTagRepository: Repository<WeightRecordTag>;
+  private tagRepository: Repository<Tag>;
+
+  constructor() {
+    this.weightRecordRepository = AppDataSource.getRepository(WeightRecord);
+    this.weightRecordTagRepository = AppDataSource.getRepository(WeightRecordTag);
+    this.tagRepository = AppDataSource.getRepository(Tag);
+  }
+
+  /**
+   * 创建体重记录
+   */
+  async createRecord(userId: string, dto: CreateWeightRecordDto): Promise<WeightRecord> {
+    const recordId = uuidv4();
+
+    // 创建体重记录
+    const record = this.weightRecordRepository.create({
+      id: recordId,
+      userId,
+      recordDate: dto.date,
+      measurementTime: dto.measurementTime || null,
+      weight: dto.weight,
+      bodyFat: dto.bodyFat,
+      muscleMass: dto.muscleMass,
+      measurementCondition: dto.measurementCondition || null,
+      notes: dto.notes || null
+    });
+
+    await this.weightRecordRepository.save(record);
+
+    // 处理标签
+    if (dto.tags && dto.tags.length > 0) {
+      await this.addTagsToRecord(recordId, userId, dto.tags);
+    }
+
+    return this.getRecordById(recordId);
+  }
+
+  /**
+   * 更新体重记录
+   */
+  async updateRecord(recordId: string, userId: string, dto: UpdateWeightRecordDto): Promise<WeightRecord> {
+    const record = await this.weightRecordRepository.findOne({
+      where: { id: recordId, userId }
+    });
+
+    if (!record) {
+      throw new Error('记录不存在');
+    }
+
+    // 更新基本信息
+    if (dto.weight !== undefined) record.weight = dto.weight;
+    if (dto.bodyFat !== undefined) record.bodyFat = dto.bodyFat;
+    if (dto.muscleMass !== undefined) record.muscleMass = dto.muscleMass;
+    if (dto.measurementCondition !== undefined) record.measurementCondition = dto.measurementCondition;
+    if (dto.notes !== undefined) record.notes = dto.notes;
+
+    await this.weightRecordRepository.save(record);
+
+    // 更新标签
+    if (dto.tags !== undefined) {
+      // 删除旧标签关联
+      await this.weightRecordTagRepository.delete({ weightRecordId: recordId });
+      // 添加新标签
+      if (dto.tags.length > 0) {
+        await this.addTagsToRecord(recordId, userId, dto.tags);
+      }
+    }
+
+    return this.getRecordById(recordId);
+  }
+
+  /**
+   * 删除体重记录(软删除)
+   */
+  async deleteRecord(recordId: string, userId: string): Promise<void> {
+    const record = await this.weightRecordRepository.findOne({
+      where: { id: recordId, userId }
+    });
+
+    if (!record) {
+      throw new Error('记录不存在');
+    }
+
+    await this.weightRecordRepository.softDelete(recordId);
+  }
+
+  /**
+   * 获取体重记录列表
+   */
+  async getRecords(userId: string, query: WeightRecordQueryDto): Promise<{
+    records: any[];
+    total: number;
+    page: number;
+    limit: number;
+  }> {
+    const page = query.page || 1;
+    const limit = query.limit || 100;
+    const skip = (page - 1) * limit;
+
+    // 构建查询条件
+    const queryBuilder = this.weightRecordRepository
+      .createQueryBuilder('wr')
+      .leftJoinAndSelect('wr.weightRecordTags', 'wrt')
+      .leftJoinAndSelect('wrt.tag', 't')
+      .where('wr.userId = :userId', { userId })
+      .andWhere('wr.deletedAt IS NULL');
+
+    // 日期范围筛选
+    if (query.from) {
+      queryBuilder.andWhere('wr.recordDate >= :from', { from: query.from });
+    }
+    if (query.to) {
+      queryBuilder.andWhere('wr.recordDate <= :to', { to: query.to });
+    }
+
+    // 测量条件筛选
+    if (query.condition) {
+      queryBuilder.andWhere('wr.measurementCondition = :condition', { condition: query.condition });
+    }
+
+    // 标签筛选
+    if (query.tags && query.tags.length > 0) {
+      queryBuilder.andWhere('t.tagName IN (:...tags)', { tags: query.tags });
+    }
+
+    // 获取总数
+    const total = await queryBuilder.getCount();
+
+    // 获取记录
+    const records = await queryBuilder
+      .orderBy('wr.recordDate', 'DESC')
+      .addOrderBy('wr.createdAt', 'DESC')
+      .skip(skip)
+      .take(limit)
+      .getMany();
+
+    // 格式化输出
+    const formattedRecords = records.map(record => this.formatRecord(record));
+
+    return {
+      records: formattedRecords,
+      total,
+      page,
+      limit
+    };
+  }
+
+  /**
+   * 根据ID获取记录
+   */
+  async getRecordById(recordId: string): Promise<WeightRecord> {
+    const record = await this.weightRecordRepository
+      .createQueryBuilder('wr')
+      .leftJoinAndSelect('wr.weightRecordTags', 'wrt')
+      .leftJoinAndSelect('wrt.tag', 't')
+      .where('wr.id = :recordId', { recordId })
+      .andWhere('wr.deletedAt IS NULL')
+      .getOne();
+
+    if (!record) {
+      throw new Error('记录不存在');
+    }
+
+    return record;
+  }
+
+  /**
+   * 为记录添加标签
+   */
+  private async addTagsToRecord(recordId: string, userId: string, tagNames: string[]): Promise<void> {
+    for (const tagName of tagNames) {
+      // 查找标签(系统标签或用户自定义标签)
+      let tag = await this.tagRepository.findOne({
+        where: [
+          { tagName, userId },
+          { tagName, userId: null } // 系统标签
+        ]
+      });
+
+      // 如果标签不存在,创建用户自定义标签
+      if (!tag) {
+        tag = this.tagRepository.create({
+          id: uuidv4(),
+          userId,
+          tagName,
+          tagType: 'custom' as any
+        });
+        await this.tagRepository.save(tag);
+      }
+
+      // 创建关联
+      const recordTag = this.weightRecordTagRepository.create({
+        id: uuidv4(),
+        weightRecordId: recordId,
+        tagId: tag.id
+      });
+      await this.weightRecordTagRepository.save(recordTag);
+    }
+  }
+
+  /**
+   * 格式化记录输出
+   */
+  private formatRecord(record: WeightRecord): any {
+    const tags = record.weightRecordTags?.map(wrt => wrt.tag?.tagName).filter(Boolean) || [];
+
+    return {
+      id: record.id,
+      date: record.recordDate,
+      measurementTime: record.measurementTime,
+      weight: Number(record.weight),
+      bodyFat: Number(record.bodyFat),
+      muscleMass: Number(record.muscleMass),
+      measurementCondition: record.measurementCondition,
+      notes: record.notes,
+      tags,
+      createdAt: record.createdAt.getTime(),
+      updatedAt: record.updatedAt?.getTime() || record.createdAt.getTime()
+    };
+  }
+}
+

+ 47 - 0
campus_health_app/backend/tsconfig.json

@@ -0,0 +1,47 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "module": "commonjs",
+    "lib": ["ES2020"],
+    "outDir": "./dist",
+    "rootDir": "./src",
+    "strict": true,
+    "esModuleInterop": true,
+    "skipLibCheck": true,
+    "forceConsistentCasingInFileNames": true,
+    "resolveJsonModule": true,
+    "moduleResolution": "node",
+    "declaration": true,
+    "declarationMap": true,
+    "sourceMap": true,
+    "experimentalDecorators": true,
+    "emitDecoratorMetadata": true,
+    "strictPropertyInitialization": false,
+    "noImplicitAny": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noImplicitReturns": true,
+    "noFallthroughCasesInSwitch": true,
+    "allowSyntheticDefaultImports": true,
+    "baseUrl": "./src",
+    "paths": {
+      "@/*": ["./*"],
+      "@entities/*": ["entities/*"],
+      "@controllers/*": ["controllers/*"],
+      "@services/*": ["services/*"],
+      "@middlewares/*": ["middlewares/*"],
+      "@utils/*": ["utils/*"],
+      "@config/*": ["config/*"]
+    }
+  },
+  "include": [
+    "src/**/*.ts"
+  ],
+  "exclude": [
+    "node_modules",
+    "dist",
+    "**/*.spec.ts",
+    "**/*.test.ts"
+  ]
+}
+

+ 2 - 2
campus_health_app/frontend/campus-health-app/src/app/modules/exercise/exercise.component.scss

@@ -13,8 +13,8 @@
   display: flex;
   align-items: center;
   padding: 1.5rem 2rem;
-  background: linear-gradient(135deg, #10b981 0%, #059669 100%);
-  color: white;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: (135deg, #667eea 0%, #764ba2 100%);
   box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
 }
 

+ 417 - 33
campus_health_app/frontend/campus-health-app/src/app/modules/monitoring/monitoring.component.html

@@ -1,19 +1,23 @@
 <div class="monitoring-container">
-  <header class="monitoring-header">
+  <header class="page-header">
     <button class="back-button" (click)="backToDashboard()">← 返回</button>
     <h1>健康监测</h1>
   </header>
 
-  <main class="monitoring-main">
+  <main class="main-content">
     <div class="monitoring-content">
       <!-- 健康指标概览 -->
       <section class="health-overview">
-        <h2>健康指标概览</h2>
+        <div class="section-header">
+          <h2>健康指标概览</h2>
+          <button class="add-record-btn" (click)="openRecordModal('heartRate')">+ 记录数据</button>
+        </div>
         <div class="health-cards">
           <!-- 心率监测卡片 -->
-          <div class="health-card">
+          <div class="health-card" (click)="openDetailModal('heartRate')">
             <div class="card-header">
               <h3>心率</h3>
+              <span class="status-indicator" [ngClass]="heartRate.status" title="{{ getStatusText(heartRate.status) }}"></span>
               <span class="last-updated">最后更新: {{ formatDate(heartRate.timestamp) }}</span>
             </div>
             <div class="card-content">
@@ -32,12 +36,17 @@
               </div>
               <p class="health-advice">{{ getHealthAdvice('heartRate') }}</p>
             </div>
+            <div class="card-actions">
+              <button class="card-button primary" (click)="$event.stopPropagation(); openRecordModal('heartRate')">记录</button>
+              <button class="card-button secondary" (click)="$event.stopPropagation(); openDetailModal('heartRate')">详情</button>
+            </div>
           </div>
 
           <!-- 血压监测卡片 -->
-          <div class="health-card">
+          <div class="health-card" (click)="openDetailModal('bloodPressure')">
             <div class="card-header">
               <h3>血压</h3>
+              <span class="status-indicator" [ngClass]="bloodPressure.systolic.status" title="{{ getStatusText(bloodPressure.systolic.status) }}"></span>
               <span class="last-updated">最后更新: {{ formatDate(bloodPressure.systolic.timestamp) }}</span>
             </div>
             <div class="card-content">
@@ -53,27 +62,145 @@
               </div>
               <p class="health-advice">{{ getHealthAdvice('bloodPressure') }}</p>
             </div>
+            <div class="card-actions">
+              <button class="card-button primary" (click)="$event.stopPropagation(); openRecordModal('bloodPressure')">记录</button>
+              <button class="card-button secondary" (click)="$event.stopPropagation(); openDetailModal('bloodPressure')">详情</button>
+            </div>
           </div>
 
-          <!-- 经期监测卡片 (仅女性用户显示) -->
-          @if (isFemaleUser) {
-            <div class="health-card">
-              <div class="card-header">
-                <h3>经期监测</h3>
+          <!-- 血氧监测卡片 -->
+          <div class="health-card" (click)="openDetailModal('bloodOxygen')">
+            <div class="card-header">
+              <h3>血氧</h3>
+              <span class="status-indicator" [ngClass]="bloodOxygen.status" title="{{ getStatusText(bloodOxygen.status) }}"></span>
+              <span class="last-updated">最后更新: {{ formatDate(bloodOxygen.timestamp) }}</span>
+            </div>
+            <div class="card-content">
+              <div class="metric-value {{ getStatusClass(bloodOxygen.status) }}">
+                {{ bloodOxygen.value }}<span class="metric-unit">{{ bloodOxygen.unit }}</span>
+                <span class="trend-icon">{{ getTrendIcon(bloodOxygen.trend) }}</span>
               </div>
-              <div class="card-content">
-                <div class="menstrual-info">
-                  <p><strong>上次经期:</strong> {{ formatDate(menstrualCycle.startDate) }} - {{ formatDate(menstrualCycle.endDate) }}</p>
-                  <p><strong>预计下次经期:</strong> {{ formatDate(menstrualCycle.predictedNextStartDate || today) }}</p>
-                  <p><strong>距离下次经期:</strong> {{ getDaysUntilNextPeriod() }} 天</p>
-                  @if (menstrualCycle.symptoms && menstrualCycle.symptoms.length > 0) {
-                    <p><strong>常见症状:</strong> {{ menstrualCycle.symptoms.join('、') }}</p>
-                  }
+              <div class="health-progress">
+                <div class="progress-bar">
+                  <div class="progress-fill {{ getStatusClass(bloodOxygen.status) }}" style="width: 90%;"></div>
+                </div>
+                <div class="progress-range">
+                  <span>90</span>
+                  <span>100</span>
                 </div>
-                <p class="health-advice">{{ getHealthAdvice('menstrualCycle') }}</p>
               </div>
+              <p class="health-advice">{{ getHealthAdvice('bloodOxygen') }}</p>
             </div>
-          }
+            <div class="card-actions">
+              <button class="card-button primary" (click)="$event.stopPropagation(); openRecordModal('bloodOxygen')">记录</button>
+              <button class="card-button secondary" (click)="$event.stopPropagation(); openDetailModal('bloodOxygen')">详情</button>
+            </div>
+          </div>
+        </div>
+      </section>
+
+      <!-- 综合健康状态卡片 -->
+      <section class="health-status-section">
+        <div class="health-status-card">
+          <div class="health-status-header">
+            <h3>综合健康状态</h3>
+            <div class="overall-status good">良好</div>
+          </div>
+          <p class="status-description">您的整体健康状况良好,心率、血压和血氧指标均在正常范围内。建议继续保持当前的生活方式和健康习惯。</p>
+          <div class="key-metrics-summary">
+            <div class="key-metric-item">
+              <span class="metric-label">健康评分:</span>
+              <span class="metric-value">85/100</span>
+            </div>
+            <div class="key-metric-item">
+              <span class="metric-label">心率:</span>
+              <span class="metric-value">{{ heartRate.value }} {{ heartRate.unit }}</span>
+            </div>
+            <div class="key-metric-item">
+              <span class="metric-label">血压:</span>
+              <span class="metric-value">{{ bloodPressure.systolic.value }}/{{ bloodPressure.diastolic.value }}</span>
+            </div>
+            <div class="key-metric-item">
+              <span class="metric-label">血氧:</span>
+              <span class="metric-value">{{ bloodOxygen.value }}%</span>
+            </div>
+          </div>
+          <div class="card-actions">
+            <button class="card-button secondary" (click)="openDetailModal('comprehensive')">查看综合报告</button>
+          </div>
+        </div>
+      </section>
+
+      <!-- 经期监测卡片 (仅女性用户显示) -->
+      @if (isFemaleUser) {
+        <div class="health-card" (click)="openDetailModal('menstrualCycle')">
+          <div class="card-header">
+            <h3>经期监测</h3>
+            <span class="status-indicator normal" title="规律"></span>
+          </div>
+          <div class="card-content">
+            <div class="menstrual-info">
+              <p><strong>上次经期:</strong> {{ formatDate(menstrualCycle.startDate) }} - {{ formatDate(menstrualCycle.endDate) }}</p>
+              <p><strong>预计下次经期:</strong> {{ formatDate(menstrualCycle.predictedNextStartDate || today) }}</p>
+              <p><strong>距离下次经期:</strong> {{ getDaysUntilNextPeriod() }} 天</p>
+              @if (menstrualCycle.symptoms && menstrualCycle.symptoms.length > 0) {
+                <p><strong>常见症状:</strong> {{ menstrualCycle.symptoms.join('、') }}</p>
+              }
+            </div>
+            <p class="health-advice">{{ getHealthAdvice('menstrualCycle') }}</p>
+          </div>
+          <div class="card-actions">
+            <button class="card-button secondary" (click)="$event.stopPropagation(); openDetailModal('menstrualCycle')">详情</button>
+          </div>
+        </div>
+      }
+
+      <!-- 数据可视化区域 -->
+      <section class="data-visualization">
+        <div class="section-header">
+          <h2>数据趋势分析</h2>
+          <div class="time-range-selector">
+            <select [(ngModel)]="selectedTimeRange" (change)="onTimeRangeChange()">
+              <option value="week">最近7天</option>
+              <option value="month">最近30天</option>
+              <option value="threeMonths">最近3个月</option>
+            </select>
+          </div>
+        </div>
+
+        <div class="charts-container">
+          <!-- 心率图表 -->
+          <div class="chart-card">
+            <h3>心率趋势</h3>
+            <div class="chart-wrapper">
+              <canvas id="heartRateChart"></canvas>
+            </div>
+          </div>
+
+          <!-- 血压图表 -->
+          <div class="chart-card">
+            <h3>血压趋势</h3>
+            <div class="chart-wrapper">
+              <canvas id="bloodPressureChart"></canvas>
+            </div>
+          </div>
+
+          <!-- 血氧图表 -->
+          <div class="chart-card">
+            <h3>血氧趋势</h3>
+            <div class="chart-wrapper">
+              <canvas id="bloodOxygenChart"></canvas>
+            </div>
+          </div>
+
+          <!-- 健康指标关联分析图表 -->
+          <div class="chart-card correlation-chart">
+            <h3>健康指标关联分析</h3>
+            <div class="chart-wrapper">
+              <canvas id="correlationChart"></canvas>
+            </div>
+            <p class="chart-description">心率与收缩压的相关性分析,帮助了解身体状况的整体表现</p>
+          </div>
         </div>
       </section>
 
@@ -110,23 +237,280 @@
         </section>
       }
 
-      <!-- 健康建议区域 -->
+      <!-- 生活方式建议区域 -->
+      <section class="lifestyle-tips-section">
+        <h2>生活方式建议</h2>
+        <div class="lifestyle-tips-cards">
+          <div class="lifestyle-tip-card exercise">
+            <h3>运动建议</h3>
+            <p>建议每日进行30分钟中等强度有氧运动,如快走、游泳或骑行,可以有效提升心肺功能,维持健康体重。</p>
+            <div class="tip-highlight">
+              运动前记得热身,运动后进行拉伸,避免剧烈运动导致受伤。
+            </div>
+          </div>
+          <div class="lifestyle-tip-card diet">
+            <h3>饮食建议</h3>
+            <p>保持饮食均衡,多摄入新鲜蔬菜和水果,减少盐分和脂肪摄入,选择全谷物和优质蛋白质来源。</p>
+            <div class="tip-highlight">
+              建议每日盐摄入量不超过5克,避免加工食品和高糖饮料。
+            </div>
+          </div>
+          <div class="lifestyle-tip-card sleep">
+            <h3>睡眠建议</h3>
+            <p>保证每日7-8小时的充足睡眠,建立规律的睡眠习惯,避免熬夜和睡前使用电子设备。</p>
+            <div class="tip-highlight">
+              睡前放松身心,可尝试冥想或听轻音乐帮助入睡。
+            </div>
+          </div>
+          <div class="lifestyle-tip-card hydration">
+            <h3>水分补充</h3>
+            <p>每天饮水约2000毫升,保持身体水分充足,促进新陈代谢和废物排出。</p>
+            <div class="tip-highlight">
+              避免等到口渴才喝水,养成定时饮水的好习惯。
+            </div>
+          </div>
+        </div>
+      </section>
+
+      <!-- AI健康建议区域 -->
       <section class="health-tips-section">
         <h2>AI健康建议</h2>
-        <div class="health-tips-card">
-          <p>根据您的健康监测数据,AI系统为您提供以下建议:</p>
-          <ul>
-            <li>保持每日30分钟中等强度有氧运动,如快走、游泳或骑自行车</li>
-            <li>保持充足睡眠(7-8小时/天),有助于维持正常心率和血压</li>
-            <li>减少盐和加工食品的摄入,增加新鲜蔬果的比例</li>
-            <li>定期监测健康指标,建议每周至少记录一次血压和心率</li>
-            <li>保持良好的心态,适当进行放松活动如冥想或瑜伽</li>
-          </ul>
-          <div class="recommendation-note">
-            <p>💡 <strong>个性化建议:</strong> 基于您的健康数据,当前无需特别调整运动计划,请继续保持现有健康习惯。</p>
+        <div class="health-tips-cards">
+          <div class="health-tip-card">
+            <h3>心率建议</h3>
+            <p>{{ getHealthAdvice('heartRate') }}</p>
+          </div>
+          <div class="health-tip-card">
+            <h3>血压建议</h3>
+            <p>{{ getHealthAdvice('bloodPressure') }}</p>
+          </div>
+          <div class="health-tip-card">
+            <h3>血氧建议</h3>
+            <p>{{ getHealthAdvice('bloodOxygen') }}</p>
           </div>
+          @if (isFemaleUser) {
+            <div class="health-tip-card">
+              <h3>经期建议</h3>
+              <p>{{ getHealthAdvice('menstrualCycle') }}</p>
+            </div>
+          }
         </div>
+        <button class="report-btn" (click)="openDetailModal('comprehensive')">查看完整健康报告</button>
       </section>
     </div>
   </main>
-</div>
+</div>
+
+<!-- 数据记录模态框 -->
+@if (showRecordModal) {
+<div class="modal">
+  <div class="modal-content">
+    <div class="modal-header">
+      <h3>{{ recordForm.type === 'heartRate' ? '记录心率' : recordForm.type === 'bloodPressure' ? '记录血压' : '记录血氧' }}</h3>
+      <button class="close-btn" (click)="closeRecordModal()">×</button>
+    </div>
+    <div class="modal-body">
+      <form (ngSubmit)="submitRecord()">
+        <div class="form-group">
+          <label for="recordValue">{{ recordForm.type === 'heartRate' ? '心率值' : recordForm.type === 'bloodPressure' ? '收缩压' : '血氧饱和度' }}:</label>
+          <input 
+            type="number" 
+            id="recordValue" 
+            [(ngModel)]="recordForm.value" 
+            name="recordValue"
+            placeholder="请输入数值"
+            min="0"
+            required
+          >
+          <span class="unit">{{ recordForm.type === 'heartRate' ? 'bpm' : recordForm.type === 'bloodPressure' ? 'mmHg' : '%' }}</span>
+        </div>
+        
+        <div class="form-group" *ngIf="recordForm.type === 'bloodPressure'">
+          <label for="diastolicValue">舒张压:</label>
+          <input 
+            type="number" 
+            id="diastolicValue" 
+            [(ngModel)]="recordForm.diastolic" 
+            name="diastolicValue"
+            placeholder="请输入舒张压"
+            min="0"
+            required
+          >
+          <span class="unit">mmHg</span>
+        </div>
+        
+        <div class="form-group">
+          <label for="recordDate">记录时间:</label>
+          <input 
+            type="datetime-local" 
+            id="recordDate" 
+            [(ngModel)]="recordForm.date" 
+            name="recordDate"
+            format="yyyy年MM月dd日 HH:mm"
+            required
+          >
+        </div>
+        
+        <div class="form-actions">
+          <button type="button" class="cancel-btn" (click)="closeRecordModal()">取消</button>
+          <button type="submit" class="submit-btn">保存</button>
+        </div>
+      </form>
+    </div>
+  </div>
+</div>
+}
+
+<!-- 详情模态框 -->
+@if (showDetailModal) {
+<div class="modal">
+  <div class="modal-content detail-modal">
+    <div class="modal-header">
+      <h3>
+        {{ selectedMetric === 'heartRate' ? '心率详情' : 
+           selectedMetric === 'bloodPressure' ? '血压详情' : 
+           selectedMetric === 'bloodOxygen' ? '血氧详情' : 
+           selectedMetric === 'menstrualCycle' ? '经期详情' : '健康综合报告' }}
+      </h3>
+      <button class="close-btn" (click)="closeDetailModal()">×</button>
+    </div>
+    <div class="modal-body">
+      @if (selectedMetric === 'comprehensive') {
+        <div class="report-content">
+          <h2>健康状况报告</h2>
+          
+          <h3>心率状况</h3>
+          <ul>
+            <li>当前值: {{ heartRate.value }} {{ heartRate.unit }}</li>
+            <li>状态: {{ getStatusText(heartRate.status) }}</li>
+            <li>趋势: {{ getTrendText(heartRate.trend) }}</li>
+          </ul>
+          
+          <h3>血压状况</h3>
+          <ul>
+            <li>当前值: {{ bloodPressure.systolic.value }}/{{ bloodPressure.diastolic.value }} {{ bloodPressure.systolic.unit }}</li>
+            <li>状态: {{ getStatusText(bloodPressure.systolic.status) }}</li>
+            <li>趋势: {{ getTrendText(bloodPressure.systolic.trend) }}</li>
+          </ul>
+          
+          <h3>血氧状况</h3>
+          <ul>
+            <li>当前值: {{ bloodOxygen.value }} {{ bloodOxygen.unit }}</li>
+            <li>状态: {{ getStatusText(bloodOxygen.status) }}</li>
+            <li>趋势: {{ getTrendText(bloodOxygen.trend) }}</li>
+          </ul>
+          
+          <h3>综合建议</h3>
+          <ul>
+            <li>保持每日30分钟中等强度有氧运动</li>
+            <li>饮食清淡,减少盐分和脂肪摄入</li>
+            <li>保证充足睡眠,避免熬夜</li>
+            <li>保持良好心态,避免长期精神压力</li>
+            @if (bloodPressure.systolic.status !== 'normal' || bloodPressure.diastolic.status !== 'normal') {
+              <li>建议每日测量血压并记录,如有异常及时就医</li>
+            }
+          </ul>
+        </div>
+      } @else if (selectedMetric === 'heartRate') {
+        <div class="metric-details">
+          <div class="detail-item">
+            <span class="label">当前心率:</span>
+            <span class="value">{{ heartRate.value }} {{ heartRate.unit }}</span>
+          </div>
+          <div class="detail-item">
+            <span class="label">状态:</span>
+            <span class="value" [ngClass]="heartRate.status">{{ getStatusText(heartRate.status) }}</span>
+          </div>
+          <div class="detail-item">
+            <span class="label">记录时间:</span>
+            <span class="value">{{ heartRate.timestamp | date: 'yyyy-MM-dd HH:mm:ss' }}</span>
+          </div>
+          <div class="detail-item">
+            <span class="label">趋势:</span>
+            <span class="value trend" [ngClass]="heartRate.trend">
+              {{ getTrendText(heartRate.trend) }}
+            </span>
+          </div>
+          <div class="advice-section">
+            <h4>健康建议</h4>
+            <p>{{ getHealthAdvice('heartRate') }}</p>
+          </div>
+        </div>
+      } @else if (selectedMetric === 'bloodPressure') {
+        <div class="metric-details">
+          <div class="detail-item">
+            <span class="label">收缩压:</span>
+            <span class="value">{{ bloodPressure.systolic.value }} {{ bloodPressure.systolic.unit }}</span>
+          </div>
+          <div class="detail-item">
+            <span class="label">舒张压:</span>
+            <span class="value">{{ bloodPressure.diastolic.value }} {{ bloodPressure.diastolic.unit }}</span>
+          </div>
+          <div class="detail-item">
+            <span class="label">状态:</span>
+            <span class="value" [ngClass]="bloodPressure.systolic.status">{{ getStatusText(bloodPressure.systolic.status) }}</span>
+          </div>
+          <div class="detail-item">
+            <span class="label">记录时间:</span>
+            <span class="value">{{ bloodPressure.systolic.timestamp | date: 'yyyy-MM-dd HH:mm:ss' }}</span>
+          </div>
+          <div class="advice-section">
+            <h4>健康建议</h4>
+            <p>{{ getHealthAdvice('bloodPressure') }}</p>
+          </div>
+        </div>
+      } @else if (selectedMetric === 'bloodOxygen') {
+        <div class="metric-details">
+          <div class="detail-item">
+            <span class="label">血氧饱和度:</span>
+            <span class="value">{{ bloodOxygen.value }} {{ bloodOxygen.unit }}</span>
+          </div>
+          <div class="detail-item">
+            <span class="label">状态:</span>
+            <span class="value" [ngClass]="bloodOxygen.status">{{ getStatusText(bloodOxygen.status) }}</span>
+          </div>
+          <div class="detail-item">
+            <span class="label">记录时间:</span>
+            <span class="value">{{ bloodOxygen.timestamp | date: 'yyyy-MM-dd HH:mm:ss' }}</span>
+          </div>
+          <div class="advice-section">
+            <h4>健康建议</h4>
+            <p>{{ getHealthAdvice('bloodOxygen') }}</p>
+          </div>
+        </div>
+      } @else if (selectedMetric === 'menstrualCycle') {
+        <div class="metric-details">
+          <div class="detail-item">
+            <span class="label">上次经期开始:</span>
+            <span class="value">{{ menstrualCycle.startDate | date: 'yyyy-MM-dd' }}</span>
+          </div>
+          <div class="detail-item">
+            <span class="label">上次经期结束:</span>
+            <span class="value">{{ menstrualCycle.endDate | date: 'yyyy-MM-dd' }}</span>
+          </div>
+          <div class="detail-item">
+            <span class="label">下次经期预计:</span>
+            <span class="value">{{ menstrualCycle.predictedNextStartDate | date: 'yyyy-MM-dd' }}</span>
+          </div>
+          <div class="detail-item">
+            <span class="label">月经周期:</span>
+            <span class="value">{{ menstrualCycle.length }}天</span>
+          </div>
+          <div class="symptoms-section">
+            <h4>记录的症状</h4>
+            <ul>
+              @for (symptom of menstrualCycle.symptoms; track symptom) {
+            <li>{{ symptom }}</li>
+          }
+            </ul>
+          </div>
+          <div class="advice-section">
+            <h4>健康建议</h4>
+            <p>{{ getHealthAdvice('menstrualCycle') }}</p>
+          </div>
+        </div>
+      }
+    </div>
+  </div>
+</div>
+}

文件差異過大導致無法顯示
+ 0 - 370
campus_health_app/frontend/campus-health-app/src/app/modules/monitoring/monitoring.component.scss


+ 593 - 7
campus_health_app/frontend/campus-health-app/src/app/modules/monitoring/monitoring.component.ts

@@ -1,6 +1,11 @@
-import { Component } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { Router, RouterModule } from '@angular/router';
+import { FormsModule } from '@angular/forms';
+import { Chart, ChartConfiguration, registerables } from 'chart.js';
+
+// 注册Chart.js所需组件
+Chart.register(...registerables);
 
 // 健康指标数据接口
 export interface HealthMetric {
@@ -11,6 +16,23 @@ export interface HealthMetric {
   trend?: 'up' | 'down' | 'stable';
 }
 
+// 历史健康数据接口
+export interface HealthMetricHistory {
+  date: Date;
+  value: number;
+}
+
+// 数据记录表单接口
+export interface DataRecordForm {
+  type: 'heartRate' | 'bloodPressure' | 'bloodOxygen';
+  value: number;
+  diastolic?: number;
+  date: Date;
+}
+
+// 时间范围选择
+export type TimeRange = 'week' | 'month' | 'threeMonths' | 'custom';
+
 // 经期数据接口
 export interface MenstrualCycle {
   startDate: Date;
@@ -32,11 +54,11 @@ export interface ChronicCondition {
 @Component({
   selector: 'app-monitoring',
   standalone: true,
-  imports: [CommonModule, RouterModule],
+  imports: [CommonModule, RouterModule, FormsModule],
   templateUrl: './monitoring.component.html',
   styleUrl: './monitoring.component.scss'
 })
-export class MonitoringComponent {
+export class MonitoringComponent implements OnInit {
   today: Date = new Date();
   
   // 心率数据
@@ -66,6 +88,15 @@ export class MonitoringComponent {
     }
   };
 
+  // 血氧数据
+  bloodOxygen: HealthMetric = {
+    value: 98,
+    unit: '%',
+    status: 'normal',
+    timestamp: new Date(),
+    trend: 'stable'
+  };
+
   // 经期数据
   menstrualCycle: MenstrualCycle = {
     startDate: new Date('2025-09-15'),
@@ -95,8 +126,481 @@ export class MonitoringComponent {
   // 是否为女性用户 (用于显示经期监测)
   isFemaleUser: boolean = true;
 
+  // 图表实例
+  heartRateChart: Chart | null = null;
+  bloodPressureChart: Chart | null = null;
+  bloodOxygenChart: Chart | null = null;
+  correlationChart: Chart | null = null;
+
+  // 历史数据
+  heartRateHistory: HealthMetricHistory[] = [];
+  bloodPressureHistory: { systolic: HealthMetricHistory[]; diastolic: HealthMetricHistory[] } = {
+    systolic: [],
+    diastolic: []
+  };
+  bloodOxygenHistory: HealthMetricHistory[] = [];
+
+  // 数据记录表单
+  recordForm: DataRecordForm = {
+    type: 'heartRate',
+    value: 0,
+    date: new Date()
+  };
+
+  // 时间范围选择
+  selectedTimeRange: TimeRange = 'month';
+  
+  // 模态框控制
+  showRecordModal: boolean = false;
+  showDetailModal: boolean = false;
+  selectedMetric: string = 'heartRate';
+
   constructor(private router: Router) {}
 
+  ngOnInit(): void {
+    this.generateMockHistoryData();
+    // 延迟初始化图表,确保DOM已加载
+    setTimeout(() => {
+      this.initializeCharts();
+    }, 100);
+  }
+
+  // 生成模拟历史数据
+  generateMockHistoryData(): void {
+    const now = new Date();
+    const daysToGenerate = 90; // 生成3个月的历史数据
+
+    for (let i = daysToGenerate; i >= 0; i--) {
+      const date = new Date(now);
+      date.setDate(date.getDate() - i);
+
+      // 心率历史数据(60-100之间波动)
+      const heartRateValue = 60 + Math.floor(Math.random() * 40);
+      this.heartRateHistory.push({
+        date: new Date(date),
+        value: heartRateValue
+      });
+
+      // 血压历史数据
+      const systolicValue = 110 + Math.floor(Math.random() * 30);
+      const diastolicValue = 70 + Math.floor(Math.random() * 20);
+      this.bloodPressureHistory.systolic.push({
+        date: new Date(date),
+        value: systolicValue
+      });
+      this.bloodPressureHistory.diastolic.push({
+        date: new Date(date),
+        value: diastolicValue
+      });
+
+      // 血氧历史数据(95-100之间波动)
+      const bloodOxygenValue = 95 + Math.floor(Math.random() * 5);
+      this.bloodOxygenHistory.push({
+        date: new Date(date),
+        value: bloodOxygenValue
+      });
+    }
+  }
+
+  // 初始化图表
+  initializeCharts(): void {
+    // 根据选择的时间范围获取数据
+    const filteredData = this.getFilteredHistoryData();
+    
+    // 心率图表
+    this.initializeHeartRateChart(filteredData.heartRate);
+    
+    // 血压图表
+    this.initializeBloodPressureChart(filteredData.bloodPressure);
+    
+    // 血氧图表
+    this.initializeBloodOxygenChart(filteredData.bloodOxygen);
+    
+    // 心率-血压关联图表
+    this.initializeCorrelationChart(filteredData.heartRate, filteredData.bloodPressure.systolic);
+  }
+
+  // 获取过滤后的历史数据
+  getFilteredHistoryData() {
+    const now = new Date();
+    let startDate = new Date();
+    
+    switch (this.selectedTimeRange) {
+      case 'week':
+        startDate.setDate(now.getDate() - 7);
+        break;
+      case 'month':
+        startDate.setMonth(now.getMonth() - 1);
+        break;
+      case 'threeMonths':
+        startDate.setMonth(now.getMonth() - 3);
+        break;
+      default:
+        startDate.setMonth(now.getMonth() - 1);
+    }
+
+    return {
+      heartRate: this.heartRateHistory.filter(item => item.date >= startDate),
+      bloodPressure: {
+        systolic: this.bloodPressureHistory.systolic.filter(item => item.date >= startDate),
+        diastolic: this.bloodPressureHistory.diastolic.filter(item => item.date >= startDate)
+      },
+      bloodOxygen: this.bloodOxygenHistory.filter(item => item.date >= startDate)
+    };
+  }
+
+  // 初始化心率图表
+  initializeHeartRateChart(data: HealthMetricHistory[]): void {
+    const ctx = document.getElementById('heartRateChart') as HTMLCanvasElement;
+    if (!ctx) return;
+
+    // 销毁现有图表
+    if (this.heartRateChart) {
+      this.heartRateChart.destroy();
+    }
+
+    const chartConfig: ChartConfiguration = {
+      type: 'line',
+      data: {
+        labels: data.map(item => item.date.toLocaleDateString('zh-CN')),
+        datasets: [{
+          label: '心率 (bpm)',
+          data: data.map(item => item.value),
+          borderColor: '#ef4444',
+          backgroundColor: 'rgba(239, 68, 68, 0.1)',
+          tension: 0.3,
+          fill: true
+        }]
+      },
+      options: {
+        responsive: true,
+        maintainAspectRatio: false,
+        plugins: {
+          legend: {
+            display: false
+          },
+          tooltip: {
+            mode: 'index',
+            intersect: false
+          }
+        },
+        scales: {
+          y: {
+            beginAtZero: false,
+            min: Math.min(...data.map(d => d.value)) - 10,
+            max: Math.max(...data.map(d => d.value)) + 10
+          }
+        }
+      }
+    };
+
+    this.heartRateChart = new Chart(ctx, chartConfig);
+  }
+
+  // 初始化血压图表
+  initializeBloodPressureChart(data: { systolic: HealthMetricHistory[]; diastolic: HealthMetricHistory[] }): void {
+    const ctx = document.getElementById('bloodPressureChart') as HTMLCanvasElement;
+    if (!ctx) return;
+
+    // 销毁现有图表
+    if (this.bloodPressureChart) {
+      this.bloodPressureChart.destroy();
+    }
+
+    const chartConfig: ChartConfiguration = {
+      type: 'line',
+      data: {
+        labels: data.systolic.map(item => item.date.toLocaleDateString('zh-CN')),
+        datasets: [
+          {
+            label: '收缩压 (mmHg)',
+            data: data.systolic.map(item => item.value),
+            borderColor: '#3b82f6',
+            backgroundColor: 'rgba(59, 130, 246, 0.1)',
+            tension: 0.3
+          },
+          {
+            label: '舒张压 (mmHg)',
+            data: data.diastolic.map(item => item.value),
+            borderColor: '#10b981',
+            backgroundColor: 'rgba(16, 185, 129, 0.1)',
+            tension: 0.3
+          }
+        ]
+      },
+      options: {
+        responsive: true,
+        maintainAspectRatio: false,
+        plugins: {
+          tooltip: {
+            mode: 'index',
+            intersect: false
+          }
+        },
+        scales: {
+          y: {
+            beginAtZero: false
+          }
+        }
+      }
+    };
+
+    this.bloodPressureChart = new Chart(ctx, chartConfig);
+  }
+
+  // 初始化血氧图表
+  initializeBloodOxygenChart(data: HealthMetricHistory[]): void {
+    const ctx = document.getElementById('bloodOxygenChart') as HTMLCanvasElement;
+    if (!ctx) return;
+
+    // 销毁现有图表
+    if (this.bloodOxygenChart) {
+      this.bloodOxygenChart.destroy();
+    }
+
+    const chartConfig: ChartConfiguration = {
+      type: 'line',
+      data: {
+        labels: data.map(item => item.date.toLocaleDateString('zh-CN')),
+        datasets: [{
+          label: '血氧饱和度 (%)',
+          data: data.map(item => item.value),
+          borderColor: '#8b5cf6',
+          backgroundColor: 'rgba(139, 92, 246, 0.1)',
+          tension: 0.3,
+          fill: true
+        }]
+      },
+      options: {
+        responsive: true,
+        maintainAspectRatio: false,
+        plugins: {
+          legend: {
+            display: false
+          },
+          tooltip: {
+            mode: 'index',
+            intersect: false
+          }
+        },
+        scales: {
+          y: {
+            beginAtZero: false,
+            min: 90,
+            max: 100
+          }
+        }
+      }
+    };
+
+    this.bloodOxygenChart = new Chart(ctx, chartConfig);
+  }
+
+  // 初始化心率-血压关联图表
+  initializeCorrelationChart(heartRateData: HealthMetricHistory[], bloodPressureData: HealthMetricHistory[]): void {
+    const ctx = document.getElementById('correlationChart') as HTMLCanvasElement;
+    if (!ctx) return;
+
+    // 销毁现有图表
+    if (this.correlationChart) {
+      this.correlationChart.destroy();
+    }
+
+    // 确保数据点数量一致
+    const dataLength = Math.min(heartRateData.length, bloodPressureData.length);
+    const scatterData: { x: number; y: number }[] = [];
+    const labels: string[] = [];
+    
+    for (let i = 0; i < dataLength; i++) {
+      scatterData.push({
+        x: heartRateData[i].value,
+        y: bloodPressureData[i].value
+      });
+      labels.push(heartRateData[i].date.toLocaleDateString('zh-CN'));
+    }
+
+    const chartConfig: ChartConfiguration = {
+      type: 'scatter',
+      data: {
+        datasets: [{
+          label: '心率 vs 收缩压',
+          data: scatterData,
+          backgroundColor: 'rgba(239, 68, 68, 0.7)',
+          pointRadius: 6,
+          pointHoverRadius: 8
+        }]
+      },
+      options: {
+        responsive: true,
+        maintainAspectRatio: false,
+        plugins: {
+          tooltip: {
+            callbacks: {
+              label: function(context) {
+                const index = context.dataIndex;
+                return [
+                  `心率: ${context.parsed.x} bpm`,
+                  `收缩压: ${context.parsed.y} mmHg`,
+                  `日期: ${labels[index]}`
+                ];
+              }
+            }
+          }
+        },
+        scales: {
+          x: {
+            title: {
+              display: true,
+              text: '心率 (bpm)'
+            }
+          },
+          y: {
+            title: {
+              display: true,
+              text: '收缩压 (mmHg)'
+            }
+          }
+        }
+      }
+    };
+
+    this.correlationChart = new Chart(ctx, chartConfig);
+  }
+
+  // 时间范围改变
+  onTimeRangeChange(): void {
+    this.initializeCharts();
+  }
+
+  // 打开数据记录模态框
+  openRecordModal(type: 'heartRate' | 'bloodPressure' | 'bloodOxygen'): void {
+    this.recordForm = {
+      type,
+      value: 0,
+      diastolic: type === 'bloodPressure' ? 0 : undefined,
+      date: new Date()
+    };
+    this.showRecordModal = true;
+  }
+
+  // 关闭数据记录模态框
+  closeRecordModal(): void {
+    this.showRecordModal = false;
+  }
+
+  // 提交数据记录
+  submitRecord(): void {
+    // 验证数据
+    if (this.recordForm.value <= 0) {
+      alert('请输入有效的数值');
+      return;
+    }
+
+    if (this.recordForm.type === 'bloodPressure' && (!this.recordForm.diastolic || this.recordForm.diastolic <= 0)) {
+      alert('请输入有效的舒张压数值');
+      return;
+    }
+
+    // 更新当前数据
+    switch (this.recordForm.type) {
+      case 'heartRate':
+        this.heartRate = {
+          ...this.heartRate,
+          value: this.recordForm.value,
+          timestamp: this.recordForm.date,
+          status: this.calculateHeartRateStatus(this.recordForm.value)
+        };
+        // 添加到历史记录
+        this.heartRateHistory.push({
+          date: new Date(this.recordForm.date),
+          value: this.recordForm.value
+        });
+        break;
+      case 'bloodPressure':
+        this.bloodPressure = {
+          systolic: {
+            ...this.bloodPressure.systolic,
+            value: this.recordForm.value,
+            timestamp: this.recordForm.date,
+            status: this.calculateBloodPressureStatus(this.recordForm.value, 'systolic')
+          },
+          diastolic: {
+            ...this.bloodPressure.diastolic,
+            value: this.recordForm.diastolic || 0,
+            timestamp: this.recordForm.date,
+            status: this.calculateBloodPressureStatus(this.recordForm.diastolic || 0, 'diastolic')
+          }
+        };
+        // 添加到历史记录
+        this.bloodPressureHistory.systolic.push({
+          date: new Date(this.recordForm.date),
+          value: this.recordForm.value
+        });
+        this.bloodPressureHistory.diastolic.push({
+          date: new Date(this.recordForm.date),
+          value: this.recordForm.diastolic || 0
+        });
+        break;
+      case 'bloodOxygen':
+        this.bloodOxygen = {
+          ...this.bloodOxygen,
+          value: this.recordForm.value,
+          timestamp: this.recordForm.date,
+          status: this.calculateBloodOxygenStatus(this.recordForm.value)
+        };
+        // 添加到历史记录
+        this.bloodOxygenHistory.push({
+          date: new Date(this.recordForm.date),
+          value: this.recordForm.value
+        });
+        break;
+    }
+
+    // 更新图表
+    this.initializeCharts();
+    
+    // 关闭模态框
+    this.closeRecordModal();
+  }
+
+  // 打开详情模态框
+  openDetailModal(metric: string): void {
+    this.selectedMetric = metric;
+    this.showDetailModal = true;
+  }
+
+  // 关闭详情模态框
+  closeDetailModal(): void {
+    this.showDetailModal = false;
+  }
+
+  // 计算心率状态
+  calculateHeartRateStatus(value: number): 'normal' | 'warning' | 'danger' {
+    if (value >= 60 && value <= 100) return 'normal';
+    if ((value >= 50 && value < 60) || (value > 100 && value <= 120)) return 'warning';
+    return 'danger';
+  }
+
+  // 计算血压状态
+  calculateBloodPressureStatus(value: number, type: 'systolic' | 'diastolic'): 'normal' | 'warning' | 'danger' {
+    if (type === 'systolic') {
+      if (value < 120) return 'normal';
+      if (value < 140) return 'warning';
+      return 'danger';
+    } else {
+      if (value < 80) return 'normal';
+      if (value < 90) return 'warning';
+      return 'danger';
+    }
+  }
+
+  // 计算血氧状态
+  calculateBloodOxygenStatus(value: number): 'normal' | 'warning' | 'danger' {
+    if (value >= 95) return 'normal';
+    if (value >= 90) return 'warning';
+    return 'danger';
+  }
+
   // 返回仪表盘
   backToDashboard(): void {
     this.router.navigate(['/dashboard']);
@@ -130,9 +634,12 @@ export class MonitoringComponent {
     }
   }
 
-  // 格式化日期
+  // 格式化日期 - 统一为年/月/日格式
   formatDate(date: Date): string {
-    return date.toLocaleDateString('zh-CN');
+    const year = date.getFullYear();
+    const month = String(date.getMonth() + 1).padStart(2, '0');
+    const day = String(date.getDate()).padStart(2, '0');
+    return `${year}/${month}/${day}`;
   }
 
   // 计算距离下次经期的天数
@@ -148,13 +655,92 @@ export class MonitoringComponent {
   getHealthAdvice(metric: string): string {
     switch (metric) {
       case 'heartRate':
-        return '您的心率处于正常范围,请继续保持规律运动和健康饮食。';
+        if (this.heartRate.status === 'normal') {
+          return '您的心率处于正常范围,请继续保持规律运动和健康饮食。';
+        } else if (this.heartRate.status === 'warning') {
+          return this.heartRate.value < 60 ? '心率偏低,建议增加有氧运动,如有不适请咨询医生。' : '心率偏高,建议避免剧烈运动,保持情绪稳定,充分休息。';
+        } else {
+          return '心率异常,建议尽快就医检查,并避免剧烈运动。';
+        }
       case 'bloodPressure':
-        return '血压控制良好,建议减少盐的摄入,保持适量运动。';
+        const systolicStatus = this.bloodPressure.systolic.status;
+        const diastolicStatus = this.bloodPressure.diastolic.status;
+        
+        if (systolicStatus === 'normal' && diastolicStatus === 'normal') {
+          return '血压控制良好,建议减少盐的摄入,保持适量运动。';
+        } else if (systolicStatus === 'warning' || diastolicStatus === 'warning') {
+          return '血压略偏高,建议减少盐分摄入,增加运动,定期监测。';
+        } else {
+          return '血压明显偏高,建议遵循医嘱,限制盐分,避免剧烈运动。';
+        }
+      case 'bloodOxygen':
+        if (this.bloodOxygen.status === 'normal') {
+          return '血氧饱和度正常,继续保持良好的生活习惯。';
+        } else if (this.bloodOxygen.status === 'warning') {
+          return '血氧饱和度略低,建议增加户外活动,保持良好通风环境。';
+        } else {
+          return '血氧饱和度偏低,建议尽快就医检查,避免高海拔和密闭环境。';
+        }
       case 'menstrualCycle':
         return '经期规律,注意休息和保暖,避免过度劳累。';
       default:
         return '保持健康的生活方式,定期监测健康指标。';
     }
   }
+
+  // 获取详细健康报告
+  getDetailedHealthReport(): string {
+    let report = '## 健康状况报告\n\n';
+    
+    // 心率评估
+    report += `### 心率状况\n`;
+    report += `- 当前值: ${this.heartRate.value} ${this.heartRate.unit}\n`;
+    report += `- 状态: ${this.getStatusText(this.heartRate.status)}\n`;
+    report += `- 趋势: ${this.getTrendText(this.heartRate.trend)}\n\n`;
+    
+    // 血压评估
+    report += `### 血压状况\n`;
+    report += `- 当前值: ${this.bloodPressure.systolic.value}/${this.bloodPressure.diastolic.value} ${this.bloodPressure.systolic.unit}\n`;
+    report += `- 状态: ${this.getStatusText(this.bloodPressure.systolic.status)}\n`;
+    report += `- 趋势: ${this.getTrendText(this.bloodPressure.systolic.trend)}\n\n`;
+    
+    // 血氧评估
+    report += `### 血氧状况\n`;
+    report += `- 当前值: ${this.bloodOxygen.value} ${this.bloodOxygen.unit}\n`;
+    report += `- 状态: ${this.getStatusText(this.bloodOxygen.status)}\n`;
+    report += `- 趋势: ${this.getTrendText(this.bloodOxygen.trend)}\n\n`;
+    
+    // 综合建议
+    report += `### 综合建议\n`;
+    report += `- 保持每日30分钟中等强度有氧运动\n`;
+    report += `- 饮食清淡,减少盐分和脂肪摄入\n`;
+    report += `- 保证充足睡眠,避免熬夜\n`;
+    report += `- 保持良好心态,避免长期精神压力\n`;
+    
+    if (this.bloodPressure.systolic.status !== 'normal' || this.bloodPressure.diastolic.status !== 'normal') {
+      report += `- 建议每日测量血压并记录,如有异常及时就医\n`;
+    }
+    
+    return report;
+  }
+
+  // 获取状态文本
+  getStatusText(status?: string): string {
+    switch (status) {
+      case 'normal': return '正常';
+      case 'warning': return '注意';
+      case 'danger': return '异常';
+      default: return '未知';
+    }
+  }
+
+  // 获取趋势文本
+  getTrendText(trend?: string): string {
+    switch (trend) {
+      case 'up': return '上升';
+      case 'down': return '下降';
+      case 'stable': return '稳定';
+      default: return '未知';
+    }
+  }
 }

部分文件因文件數量過多而無法顯示