feat(water-records): 新增喝水记录功能模块
新增完整的喝水记录管理功能,支持用户记录每日喝水情况、设置目标和查看统计信息。功能包括: - 创建、查询、更新和删除喝水记录 - 设置和管理每日喝水目标 - 获取今日喝水统计和完成率分析 - 支持分页查询和日期范围筛选 - 完整的数据验证和错误处理机制 该模块已从用户模块中独立出来,提供REST API接口,包含数据库迁移脚本和详细文档。
This commit is contained in:
140
docs/WATER_RECORDS.md
Normal file
140
docs/WATER_RECORDS.md
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# 喝水记录功能
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
新增了用户喝水记录功能,支持用户记录每日的喝水情况,设置喝水目标,并查看统计信息。
|
||||||
|
|
||||||
|
## 新增文件
|
||||||
|
|
||||||
|
### 模型文件
|
||||||
|
- `src/users/models/user-water-history.model.ts` - 喝水记录模型
|
||||||
|
- 更新了 `src/users/models/user-profile.model.ts` - 添加了 `dailyWaterGoal` 字段
|
||||||
|
|
||||||
|
### DTO文件
|
||||||
|
- `src/users/dto/water-record.dto.ts` - 喝水记录相关的DTO
|
||||||
|
|
||||||
|
### 服务文件
|
||||||
|
- `src/users/services/water-record.service.ts` - 喝水记录服务
|
||||||
|
|
||||||
|
### 数据库脚本
|
||||||
|
- `sql-scripts/user-water-records-table.sql` - 数据库迁移脚本
|
||||||
|
|
||||||
|
### 测试脚本
|
||||||
|
- `test-water-records.sh` - API接口测试脚本
|
||||||
|
|
||||||
|
## API接口
|
||||||
|
|
||||||
|
### 1. 创建喝水记录
|
||||||
|
```
|
||||||
|
POST /users/water-records
|
||||||
|
```
|
||||||
|
|
||||||
|
请求体:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"amount": 250,
|
||||||
|
"source": "manual",
|
||||||
|
"remark": "早晨第一杯水"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 获取喝水记录列表
|
||||||
|
```
|
||||||
|
GET /users/water-records?limit=10&offset=0
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 更新喝水记录
|
||||||
|
```
|
||||||
|
PUT /users/water-records/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
请求体:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"amount": 300,
|
||||||
|
"remark": "修改后的备注"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 删除喝水记录
|
||||||
|
```
|
||||||
|
DELETE /users/water-records/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 更新喝水目标
|
||||||
|
```
|
||||||
|
PUT /users/water-goal
|
||||||
|
```
|
||||||
|
|
||||||
|
请求体:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dailyWaterGoal": 2000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 获取今日喝水统计
|
||||||
|
```
|
||||||
|
GET /users/water-stats/today
|
||||||
|
```
|
||||||
|
|
||||||
|
响应:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"totalAmount": 1500,
|
||||||
|
"recordCount": 6,
|
||||||
|
"dailyGoal": 2000,
|
||||||
|
"completionRate": 0.75
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据库表结构
|
||||||
|
|
||||||
|
### t_user_water_history (喝水记录表)
|
||||||
|
- `id` - 主键,自增
|
||||||
|
- `user_id` - 用户ID
|
||||||
|
- `amount` - 喝水量(毫升)
|
||||||
|
- `source` - 记录来源(manual/auto/other)
|
||||||
|
- `remark` - 备注
|
||||||
|
- `created_at` - 创建时间
|
||||||
|
- `updated_at` - 更新时间
|
||||||
|
|
||||||
|
### t_user_profile (用户档案表 - 新增字段)
|
||||||
|
- `daily_water_goal` - 每日喝水目标(毫升)
|
||||||
|
|
||||||
|
## 功能特点
|
||||||
|
|
||||||
|
1. **完整的CRUD操作** - 支持喝水记录的增删改查
|
||||||
|
2. **目标设置** - 用户可以设置每日喝水目标
|
||||||
|
3. **统计功能** - 提供今日喝水统计,包括总量、记录数、完成率等
|
||||||
|
4. **数据验证** - 对输入数据进行严格验证
|
||||||
|
5. **错误处理** - 完善的错误处理机制
|
||||||
|
6. **日志记录** - 详细的操作日志
|
||||||
|
7. **权限控制** - 所有接口都需要JWT认证
|
||||||
|
|
||||||
|
## 部署说明
|
||||||
|
|
||||||
|
1. 运行数据库迁移脚本:
|
||||||
|
```bash
|
||||||
|
mysql -u username -p database_name < sql-scripts/user-water-records-table.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 重启应用服务
|
||||||
|
|
||||||
|
3. 使用测试脚本验证功能:
|
||||||
|
```bash
|
||||||
|
./test-water-records.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 喝水目标字段是可选的,可以为空
|
||||||
|
2. 喝水记录的来源默认为 'manual'
|
||||||
|
3. 喝水量的范围限制在 1-5000 毫升之间
|
||||||
|
4. 喝水目标的范围限制在 500-10000 毫升之间
|
||||||
|
5. 获取profile接口会返回用户的喝水目标
|
||||||
|
6. 喝水目标的更新集成在喝水接口中,避免用户服务文件过大
|
||||||
182
docs/WATER_RECORDS_MODULE.md
Normal file
182
docs/WATER_RECORDS_MODULE.md
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
# 喝水记录模块 (Water Records Module)
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
喝水记录模块是一个独立的NestJS模块,用于管理用户的喝水记录和喝水目标。该模块已从用户模块中分离出来,以提高代码的可维护性和模块化程度。
|
||||||
|
|
||||||
|
## 模块结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/water-records/
|
||||||
|
├── water-records.controller.ts # 控制器 - 处理HTTP请求
|
||||||
|
├── water-records.service.ts # 服务 - 业务逻辑处理
|
||||||
|
├── water-records.module.ts # 模块定义
|
||||||
|
├── models/
|
||||||
|
│ └── user-water-history.model.ts # 喝水记录数据模型
|
||||||
|
└── dto/
|
||||||
|
└── water-record.dto.ts # 数据传输对象
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 接口
|
||||||
|
|
||||||
|
### 基础路径: `/water-records`
|
||||||
|
|
||||||
|
| 方法 | 路径 | 描述 | 权限 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| POST | `/` | 创建喝水记录 | JWT |
|
||||||
|
| GET | `/` | 获取喝水记录列表 | JWT |
|
||||||
|
| PUT | `/:id` | 更新喝水记录 | JWT |
|
||||||
|
| DELETE | `/:id` | 删除喝水记录 | JWT |
|
||||||
|
| PUT | `/goal/daily` | 更新每日喝水目标 | JWT |
|
||||||
|
| GET | `/stats/today` | 获取今日喝水统计 | JWT |
|
||||||
|
|
||||||
|
## 数据模型
|
||||||
|
|
||||||
|
### UserWaterHistory (喝水记录)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
id: number; // 记录ID
|
||||||
|
userId: string; // 用户ID
|
||||||
|
amount: number; // 喝水量(毫升)
|
||||||
|
source: WaterRecordSource; // 记录来源
|
||||||
|
note: string | null; // 备注
|
||||||
|
recordedAt: Date; // 记录时间
|
||||||
|
createdAt: Date; // 创建时间
|
||||||
|
updatedAt: Date; // 更新时间
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### WaterRecordSource (记录来源枚举)
|
||||||
|
|
||||||
|
- `manual` - 手动记录
|
||||||
|
- `auto` - 自动记录
|
||||||
|
- `other` - 其他来源
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
### 1. 喝水记录管理
|
||||||
|
- ✅ 创建喝水记录
|
||||||
|
- ✅ 查询喝水记录(支持日期范围筛选和分页)
|
||||||
|
- ✅ 更新喝水记录
|
||||||
|
- ✅ 删除喝水记录
|
||||||
|
|
||||||
|
### 2. 喝水目标管理
|
||||||
|
- ✅ 设置每日喝水目标
|
||||||
|
- ✅ 在用户档案中返回喝水目标
|
||||||
|
|
||||||
|
### 3. 统计分析
|
||||||
|
- ✅ 今日喝水统计
|
||||||
|
- ✅ 完成率计算
|
||||||
|
- ✅ 记录数量统计
|
||||||
|
|
||||||
|
### 4. 数据验证
|
||||||
|
- ✅ 喝水量范围验证(1-5000ml)
|
||||||
|
- ✅ 喝水目标范围验证(500-10000ml)
|
||||||
|
- ✅ 输入数据格式验证
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### 创建喝水记录
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:3000/water-records" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||||
|
-d '{
|
||||||
|
"amount": 250,
|
||||||
|
"note": "早晨第一杯水",
|
||||||
|
"recordedAt": "2023-12-01T08:00:00.000Z"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 获取喝水记录列表
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X GET "http://localhost:3000/water-records?startDate=2023-12-01&endDate=2023-12-31&page=1&limit=20" \
|
||||||
|
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 更新喝水目标
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X PUT "http://localhost:3000/water-records/goal/daily" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||||
|
-d '{
|
||||||
|
"dailyWaterGoal": 2500
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 获取今日统计
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X GET "http://localhost:3000/water-records/stats/today" \
|
||||||
|
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据库表结构
|
||||||
|
|
||||||
|
### t_user_water_history
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE t_user_water_history (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
user_id VARCHAR(255) NOT NULL COMMENT '用户ID',
|
||||||
|
amount INT NOT NULL COMMENT '喝水量(毫升)',
|
||||||
|
source ENUM('manual', 'auto', 'other') NOT NULL DEFAULT 'manual' COMMENT '记录来源',
|
||||||
|
note VARCHAR(255) NULL COMMENT '备注',
|
||||||
|
recorded_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录时间',
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_user_id (user_id),
|
||||||
|
INDEX idx_recorded_at (recorded_at)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 模块依赖
|
||||||
|
|
||||||
|
- `@nestjs/common` - NestJS核心功能
|
||||||
|
- `@nestjs/sequelize` - Sequelize ORM集成
|
||||||
|
- `sequelize-typescript` - TypeScript装饰器支持
|
||||||
|
- `class-validator` - 数据验证
|
||||||
|
- `class-transformer` - 数据转换
|
||||||
|
|
||||||
|
## 与其他模块的关系
|
||||||
|
|
||||||
|
- **Users Module**: 依赖用户模块的UserProfile模型来管理喝水目标
|
||||||
|
- **Activity Logs Module**: 可选的活动日志记录
|
||||||
|
- **App Module**: 在主应用模块中注册
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
|
||||||
|
运行测试脚本:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x test-water-records-module.sh
|
||||||
|
./test-water-records-module.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **权限控制**: 所有接口都需要JWT认证
|
||||||
|
2. **数据验证**: 严格的输入验证确保数据质量
|
||||||
|
3. **错误处理**: 完善的错误处理和日志记录
|
||||||
|
4. **性能优化**: 支持分页查询,避免大量数据加载
|
||||||
|
5. **数据一致性**: 使用事务确保数据操作的一致性
|
||||||
|
|
||||||
|
## 迁移说明
|
||||||
|
|
||||||
|
该模块从原来的用户模块中分离出来,主要变化:
|
||||||
|
|
||||||
|
1. **路径变更**: 从 `/users/water-*` 变更为 `/water-records/*`
|
||||||
|
2. **模块独立**: 独立的控制器、服务和模块
|
||||||
|
3. **代码分离**: 减少了用户模块的复杂度
|
||||||
|
4. **维护性提升**: 更好的代码组织和维护性
|
||||||
|
|
||||||
|
## 版本历史
|
||||||
|
|
||||||
|
- **v1.0.0** - 初始版本,从用户模块分离
|
||||||
|
- 支持完整的CRUD操作
|
||||||
|
- 支持喝水目标管理
|
||||||
|
- 支持统计分析功能
|
||||||
17
sql-scripts/user-water-records-table.sql
Normal file
17
sql-scripts/user-water-records-table.sql
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
-- 创建用户喝水记录表
|
||||||
|
CREATE TABLE IF NOT EXISTS `t_user_water_history` (
|
||||||
|
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` VARCHAR(255) NOT NULL COMMENT '用户ID',
|
||||||
|
`amount` INT NOT NULL COMMENT '喝水量(毫升)',
|
||||||
|
`source` ENUM('manual', 'auto', 'other') NOT NULL DEFAULT 'manual' COMMENT '记录来源',
|
||||||
|
`remark` VARCHAR(255) NULL COMMENT '备注',
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
INDEX `idx_user_id` (`user_id`),
|
||||||
|
INDEX `idx_created_at` (`created_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户喝水记录表';
|
||||||
|
|
||||||
|
-- 为用户档案表添加喝水目标字段
|
||||||
|
ALTER TABLE `t_user_profile`
|
||||||
|
ADD COLUMN `daily_water_goal` INT NULL COMMENT '每日喝水目标(毫升)' AFTER `activity_level`;
|
||||||
@@ -17,6 +17,7 @@ import { MoodCheckinsModule } from './mood-checkins/mood-checkins.module';
|
|||||||
import { GoalsModule } from './goals/goals.module';
|
import { GoalsModule } from './goals/goals.module';
|
||||||
import { DietRecordsModule } from './diet-records/diet-records.module';
|
import { DietRecordsModule } from './diet-records/diet-records.module';
|
||||||
import { FoodLibraryModule } from './food-library/food-library.module';
|
import { FoodLibraryModule } from './food-library/food-library.module';
|
||||||
|
import { WaterRecordsModule } from './water-records/water-records.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -39,6 +40,7 @@ import { FoodLibraryModule } from './food-library/food-library.module';
|
|||||||
GoalsModule,
|
GoalsModule,
|
||||||
DietRecordsModule,
|
DietRecordsModule,
|
||||||
FoodLibraryModule,
|
FoodLibraryModule,
|
||||||
|
WaterRecordsModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
|
|||||||
@@ -86,6 +86,13 @@ export class UserProfile extends Model {
|
|||||||
})
|
})
|
||||||
declare activityLevel: ActivityLevel | null;
|
declare activityLevel: ActivityLevel | null;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: DataType.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
comment: '每日喝水目标(毫升)',
|
||||||
|
})
|
||||||
|
declare dailyWaterGoal: number;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
type: DataType.DATE,
|
type: DataType.DATE,
|
||||||
defaultValue: DataType.NOW,
|
defaultValue: DataType.NOW,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { AppStoreServerNotificationDto, ProcessNotificationResponseDto } from '.
|
|||||||
import { RestorePurchaseDto, RestorePurchaseResponseDto } from './dto/restore-purchase.dto';
|
import { RestorePurchaseDto, RestorePurchaseResponseDto } from './dto/restore-purchase.dto';
|
||||||
import { GetUserActivityHistoryResponseDto } from './dto/user-activity.dto';
|
import { GetUserActivityHistoryResponseDto } from './dto/user-activity.dto';
|
||||||
import { UpdateWeightRecordDto, WeightRecordResponseDto, DeleteWeightRecordResponseDto } from './dto/weight-record.dto';
|
import { UpdateWeightRecordDto, WeightRecordResponseDto, DeleteWeightRecordResponseDto } from './dto/weight-record.dto';
|
||||||
|
|
||||||
import { Public } from '../common/decorators/public.decorator';
|
import { Public } from '../common/decorators/public.decorator';
|
||||||
import { CurrentUser } from '../common/decorators/current-user.decorator';
|
import { CurrentUser } from '../common/decorators/current-user.decorator';
|
||||||
import { AccessTokenPayload } from './services/apple-auth.service';
|
import { AccessTokenPayload } from './services/apple-auth.service';
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import { UsersService } from "./users.service";
|
|||||||
import { User } from "./models/user.model";
|
import { User } from "./models/user.model";
|
||||||
import { UserProfile } from "./models/user-profile.model";
|
import { UserProfile } from "./models/user-profile.model";
|
||||||
import { UserWeightHistory } from "./models/user-weight-history.model";
|
import { UserWeightHistory } from "./models/user-weight-history.model";
|
||||||
|
|
||||||
import { UserDietHistory } from "./models/user-diet-history.model";
|
import { UserDietHistory } from "./models/user-diet-history.model";
|
||||||
import { ApplePurchaseService } from "./services/apple-purchase.service";
|
import { ApplePurchaseService } from "./services/apple-purchase.service";
|
||||||
import { UserActivity } from "./models/user-activity.model";
|
import { UserActivity } from "./models/user-activity.model";
|
||||||
import { UserActivityService } from "./services/user-activity.service";
|
import { UserActivityService } from "./services/user-activity.service";
|
||||||
|
|
||||||
import { EncryptionService } from "../common/encryption.service";
|
import { EncryptionService } from "../common/encryption.service";
|
||||||
import { AppleAuthService } from "./services/apple-auth.service";
|
import { AppleAuthService } from "./services/apple-auth.service";
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
@@ -29,6 +31,7 @@ import { ActivityLogsModule } from '../activity-logs/activity-logs.module';
|
|||||||
RevenueCatEvent,
|
RevenueCatEvent,
|
||||||
UserProfile,
|
UserProfile,
|
||||||
UserWeightHistory,
|
UserWeightHistory,
|
||||||
|
|
||||||
UserDietHistory,
|
UserDietHistory,
|
||||||
UserActivity,
|
UserActivity,
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -34,10 +34,12 @@ import { UserWeightHistory, WeightUpdateSource } from './models/user-weight-hist
|
|||||||
|
|
||||||
import { ActivityLogsService } from '../activity-logs/activity-logs.service';
|
import { ActivityLogsService } from '../activity-logs/activity-logs.service';
|
||||||
import { UserActivityService } from './services/user-activity.service';
|
import { UserActivityService } from './services/user-activity.service';
|
||||||
|
|
||||||
import { GetUserActivityHistoryResponseDto } from './dto/user-activity.dto';
|
import { GetUserActivityHistoryResponseDto } from './dto/user-activity.dto';
|
||||||
import { ActivityActionType, ActivityEntityType } from '../activity-logs/models/activity-log.model';
|
import { ActivityActionType, ActivityEntityType } from '../activity-logs/models/activity-log.model';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const DEFAULT_FREE_USAGE_COUNT = 5;
|
const DEFAULT_FREE_USAGE_COUNT = 5;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -111,6 +113,7 @@ export class UsersService {
|
|||||||
targetWeight: profile?.targetWeight,
|
targetWeight: profile?.targetWeight,
|
||||||
height: profile?.height,
|
height: profile?.height,
|
||||||
activityLevel: profile?.activityLevel,
|
activityLevel: profile?.activityLevel,
|
||||||
|
dailyWaterGoal: profile?.dailyWaterGoal,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`getProfile returnData: ${JSON.stringify(returnData, null, 2)}`);
|
this.logger.log(`getProfile returnData: ${JSON.stringify(returnData, null, 2)}`);
|
||||||
|
|||||||
272
src/water-records/dto/water-record.dto.ts
Normal file
272
src/water-records/dto/water-record.dto.ts
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsNumber, IsOptional, IsEnum, Min, Max, IsString, MaxLength, IsDateString } from 'class-validator';
|
||||||
|
import { Transform } from 'class-transformer';
|
||||||
|
import { WaterRecordSource } from '../models/user-water-history.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建喝水记录请求DTO
|
||||||
|
*/
|
||||||
|
export class CreateWaterRecordDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: '喝水量(毫升)',
|
||||||
|
example: 250,
|
||||||
|
minimum: 1,
|
||||||
|
maximum: 5000,
|
||||||
|
})
|
||||||
|
@IsNumber({}, { message: '喝水量必须是数字' })
|
||||||
|
@Min(1, { message: '喝水量不能小于1毫升' })
|
||||||
|
@Max(5000, { message: '喝水量不能大于5000毫升' })
|
||||||
|
amount: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '记录时间',
|
||||||
|
example: '2023-12-01T10:00:00.000Z',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString({}, { message: '记录时间格式不正确' })
|
||||||
|
recordedAt?: Date;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '记录来源',
|
||||||
|
enum: WaterRecordSource,
|
||||||
|
example: WaterRecordSource.Manual,
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(WaterRecordSource, { message: '记录来源必须是有效值' })
|
||||||
|
source?: WaterRecordSource;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '备注',
|
||||||
|
example: '早晨第一杯水',
|
||||||
|
required: false,
|
||||||
|
maxLength: 100,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString({ message: '备注必须是字符串' })
|
||||||
|
@MaxLength(100, { message: '备注长度不能超过100个字符' })
|
||||||
|
note?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新喝水记录请求DTO
|
||||||
|
*/
|
||||||
|
export class UpdateWaterRecordDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: '喝水量(毫升)',
|
||||||
|
example: 250,
|
||||||
|
minimum: 1,
|
||||||
|
maximum: 5000,
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber({}, { message: '喝水量必须是数字' })
|
||||||
|
@Min(1, { message: '喝水量不能小于1毫升' })
|
||||||
|
@Max(5000, { message: '喝水量不能大于5000毫升' })
|
||||||
|
amount?: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '记录时间',
|
||||||
|
example: '2023-12-01T10:00:00.000Z',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString({}, { message: '记录时间格式不正确' })
|
||||||
|
recordedAt?: Date;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '记录来源',
|
||||||
|
enum: WaterRecordSource,
|
||||||
|
example: WaterRecordSource.Manual,
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(WaterRecordSource, { message: '记录来源必须是有效值' })
|
||||||
|
source?: WaterRecordSource;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '备注',
|
||||||
|
example: '早晨第一杯水',
|
||||||
|
required: false,
|
||||||
|
maxLength: 100,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString({ message: '备注必须是字符串' })
|
||||||
|
@MaxLength(100, { message: '备注长度不能超过100个字符' })
|
||||||
|
note?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取喝水记录查询DTO
|
||||||
|
*/
|
||||||
|
export class GetWaterRecordsQueryDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: '开始日期 (YYYY-MM-DD)',
|
||||||
|
example: '2023-12-01',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
startDate?: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '结束日期 (YYYY-MM-DD)',
|
||||||
|
example: '2023-12-31',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
endDate?: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '页码,默认1',
|
||||||
|
example: 1,
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
@IsNumber({}, { message: '页码必须是数字' })
|
||||||
|
@Min(1, { message: '页码不能小于1' })
|
||||||
|
page?: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '每页数量,默认20',
|
||||||
|
example: 20,
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
@IsNumber({}, { message: '每页数量必须是数字' })
|
||||||
|
@Min(1, { message: '每页数量不能小于1' })
|
||||||
|
@Max(100, { message: '每页数量不能大于100' })
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新喝水目标请求DTO
|
||||||
|
*/
|
||||||
|
export class UpdateWaterGoalDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: '每日喝水目标(毫升)',
|
||||||
|
example: 2000,
|
||||||
|
minimum: 500,
|
||||||
|
maximum: 10000,
|
||||||
|
})
|
||||||
|
@IsNumber({}, { message: '喝水目标必须是数字' })
|
||||||
|
@Min(500, { message: '喝水目标不能小于500毫升' })
|
||||||
|
@Max(10000, { message: '喝水目标不能大于10000毫升' })
|
||||||
|
dailyWaterGoal: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础响应DTO
|
||||||
|
*/
|
||||||
|
export class BaseResponseDto {
|
||||||
|
@ApiProperty({ description: '是否成功', example: true })
|
||||||
|
success: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '响应消息', example: '操作成功' })
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 喝水记录数据DTO
|
||||||
|
*/
|
||||||
|
export class WaterRecordDataDto {
|
||||||
|
@ApiProperty({ description: '记录ID', example: 1 })
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '喝水量(毫升)', example: 250 })
|
||||||
|
amount: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '记录时间', example: '2023-12-01T10:00:00.000Z' })
|
||||||
|
recordedAt: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '备注', example: '早晨第一杯水', nullable: true })
|
||||||
|
note: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '创建时间', example: '2023-12-01T10:00:00.000Z' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '更新时间', example: '2023-12-01T10:00:00.000Z' })
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 喝水记录响应DTO
|
||||||
|
*/
|
||||||
|
export class WaterRecordResponseDto extends BaseResponseDto {
|
||||||
|
@ApiProperty({ description: '喝水记录数据', type: WaterRecordDataDto })
|
||||||
|
data: WaterRecordDataDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页信息DTO
|
||||||
|
*/
|
||||||
|
export class PaginationDto {
|
||||||
|
@ApiProperty({ description: '当前页码', example: 1 })
|
||||||
|
page: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '每页数量', example: 20 })
|
||||||
|
limit: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '总记录数', example: 100 })
|
||||||
|
total: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '总页数', example: 5 })
|
||||||
|
totalPages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 喝水记录列表响应DTO
|
||||||
|
*/
|
||||||
|
export class WaterRecordsListResponseDto extends BaseResponseDto {
|
||||||
|
data: {
|
||||||
|
records: WaterRecordDataDto[];
|
||||||
|
pagination: PaginationDto;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 喝水目标响应DTO
|
||||||
|
*/
|
||||||
|
export class WaterGoalResponseDto extends BaseResponseDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: '喝水目标数据',
|
||||||
|
example: { dailyWaterGoal: 2000 },
|
||||||
|
})
|
||||||
|
data: {
|
||||||
|
dailyWaterGoal: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 今日喝水统计响应DTO
|
||||||
|
*/
|
||||||
|
export class TodayWaterStatsResponseDto extends BaseResponseDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: '今日喝水统计',
|
||||||
|
example: {
|
||||||
|
date: '2023-12-01',
|
||||||
|
totalAmount: 1500,
|
||||||
|
dailyGoal: 2000,
|
||||||
|
completionRate: 75.0,
|
||||||
|
recordCount: 6,
|
||||||
|
records: [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
data: {
|
||||||
|
date: string;
|
||||||
|
totalAmount: number;
|
||||||
|
dailyGoal: number;
|
||||||
|
completionRate: number;
|
||||||
|
recordCount: number;
|
||||||
|
records: {
|
||||||
|
id: number;
|
||||||
|
amount: number;
|
||||||
|
recordedAt: Date;
|
||||||
|
note: string | null;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
}
|
||||||
69
src/water-records/models/user-water-history.model.ts
Normal file
69
src/water-records/models/user-water-history.model.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Column, DataType, Index, Model, PrimaryKey, Table } from 'sequelize-typescript';
|
||||||
|
|
||||||
|
export enum WaterRecordSource {
|
||||||
|
Manual = 'manual',
|
||||||
|
Auto = 'auto',
|
||||||
|
Other = 'other',
|
||||||
|
}
|
||||||
|
|
||||||
|
@Table({
|
||||||
|
tableName: 't_user_water_history',
|
||||||
|
underscored: true,
|
||||||
|
})
|
||||||
|
export class UserWaterHistory extends Model {
|
||||||
|
@PrimaryKey
|
||||||
|
@Column({
|
||||||
|
type: DataType.BIGINT,
|
||||||
|
autoIncrement: true,
|
||||||
|
})
|
||||||
|
declare id: number;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: DataType.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
comment: '用户ID',
|
||||||
|
})
|
||||||
|
declare userId: string;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: DataType.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
comment: '喝水量(毫升)',
|
||||||
|
})
|
||||||
|
declare amount: number;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: DataType.ENUM('manual', 'auto', 'other'),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 'manual',
|
||||||
|
comment: '记录来源',
|
||||||
|
})
|
||||||
|
declare source: WaterRecordSource;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: DataType.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
comment: '备注',
|
||||||
|
})
|
||||||
|
declare note: string | null;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: DataType.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: DataType.NOW,
|
||||||
|
comment: '记录时间',
|
||||||
|
})
|
||||||
|
declare recordedAt: Date;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: DataType.DATE,
|
||||||
|
defaultValue: DataType.NOW,
|
||||||
|
})
|
||||||
|
declare createdAt: Date;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: DataType.DATE,
|
||||||
|
defaultValue: DataType.NOW,
|
||||||
|
})
|
||||||
|
declare updatedAt: Date;
|
||||||
|
}
|
||||||
146
src/water-records/water-records.controller.ts
Normal file
146
src/water-records/water-records.controller.ts
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Param,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Put,
|
||||||
|
Delete,
|
||||||
|
Query,
|
||||||
|
Logger,
|
||||||
|
UseGuards,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiOperation, ApiBody, ApiResponse, ApiTags, ApiQuery } from '@nestjs/swagger';
|
||||||
|
import { WaterRecordsService } from './water-records.service';
|
||||||
|
import {
|
||||||
|
CreateWaterRecordDto,
|
||||||
|
UpdateWaterRecordDto,
|
||||||
|
WaterRecordResponseDto,
|
||||||
|
GetWaterRecordsQueryDto,
|
||||||
|
WaterRecordsListResponseDto,
|
||||||
|
UpdateWaterGoalDto,
|
||||||
|
WaterGoalResponseDto,
|
||||||
|
TodayWaterStatsResponseDto
|
||||||
|
} from './dto/water-record.dto';
|
||||||
|
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
|
||||||
|
import { CurrentUser } from '../common/decorators/current-user.decorator';
|
||||||
|
import { AccessTokenPayload } from '../users/services/apple-auth.service';
|
||||||
|
|
||||||
|
@ApiTags('water-records')
|
||||||
|
@Controller('water-records')
|
||||||
|
export class WaterRecordsController {
|
||||||
|
private readonly logger = new Logger(WaterRecordsController.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly waterRecordsService: WaterRecordsService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建喝水记录
|
||||||
|
*/
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Post()
|
||||||
|
@HttpCode(HttpStatus.CREATED)
|
||||||
|
@ApiOperation({ summary: '创建喝水记录' })
|
||||||
|
@ApiBody({ type: CreateWaterRecordDto })
|
||||||
|
@ApiResponse({ status: 201, description: '成功创建喝水记录', type: WaterRecordResponseDto })
|
||||||
|
async createWaterRecord(
|
||||||
|
@Body() createDto: CreateWaterRecordDto,
|
||||||
|
@CurrentUser() user: AccessTokenPayload,
|
||||||
|
): Promise<WaterRecordResponseDto> {
|
||||||
|
this.logger.log(`创建喝水记录 - 用户ID: ${user.sub}, 喝水量: ${createDto.amount}ml`);
|
||||||
|
return this.waterRecordsService.createWaterRecord(user.sub, createDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取喝水记录列表
|
||||||
|
*/
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Get()
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({ summary: '获取喝水记录列表' })
|
||||||
|
@ApiQuery({ name: 'startDate', required: false, description: '开始日期 (YYYY-MM-DD)' })
|
||||||
|
@ApiQuery({ name: 'endDate', required: false, description: '结束日期 (YYYY-MM-DD)' })
|
||||||
|
@ApiQuery({ name: 'page', required: false, description: '页码,默认1' })
|
||||||
|
@ApiQuery({ name: 'limit', required: false, description: '每页数量,默认20' })
|
||||||
|
@ApiResponse({ status: 200, description: '成功获取喝水记录列表', type: WaterRecordsListResponseDto })
|
||||||
|
async getWaterRecords(
|
||||||
|
@Query() query: GetWaterRecordsQueryDto,
|
||||||
|
@CurrentUser() user: AccessTokenPayload,
|
||||||
|
): Promise<WaterRecordsListResponseDto> {
|
||||||
|
this.logger.log(`获取喝水记录列表 - 用户ID: ${user.sub}`);
|
||||||
|
return this.waterRecordsService.getWaterRecords(user.sub, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新喝水记录
|
||||||
|
*/
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Put(':id')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({ summary: '更新喝水记录' })
|
||||||
|
@ApiBody({ type: UpdateWaterRecordDto })
|
||||||
|
@ApiResponse({ status: 200, description: '成功更新喝水记录', type: WaterRecordResponseDto })
|
||||||
|
async updateWaterRecord(
|
||||||
|
@Param('id') recordId: string,
|
||||||
|
@Body() updateDto: UpdateWaterRecordDto,
|
||||||
|
@CurrentUser() user: AccessTokenPayload,
|
||||||
|
): Promise<WaterRecordResponseDto> {
|
||||||
|
this.logger.log(`更新喝水记录 - 用户ID: ${user.sub}, 记录ID: ${recordId}`);
|
||||||
|
return this.waterRecordsService.updateWaterRecord(user.sub, parseInt(recordId), updateDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除喝水记录
|
||||||
|
*/
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Delete(':id')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
@ApiOperation({ summary: '删除喝水记录' })
|
||||||
|
@ApiResponse({ status: 204, description: '成功删除喝水记录' })
|
||||||
|
async deleteWaterRecord(
|
||||||
|
@Param('id') recordId: string,
|
||||||
|
@CurrentUser() user: AccessTokenPayload,
|
||||||
|
): Promise<void> {
|
||||||
|
this.logger.log(`删除喝水记录 - 用户ID: ${user.sub}, 记录ID: ${recordId}`);
|
||||||
|
const success = await this.waterRecordsService.deleteWaterRecord(user.sub, parseInt(recordId));
|
||||||
|
if (!success) {
|
||||||
|
throw new NotFoundException('喝水记录不存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新喝水目标
|
||||||
|
*/
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Put('goal/daily')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({ summary: '更新每日喝水目标' })
|
||||||
|
@ApiBody({ type: UpdateWaterGoalDto })
|
||||||
|
@ApiResponse({ status: 200, description: '成功更新喝水目标', type: WaterGoalResponseDto })
|
||||||
|
async updateWaterGoal(
|
||||||
|
@Body() updateDto: UpdateWaterGoalDto,
|
||||||
|
@CurrentUser() user: AccessTokenPayload,
|
||||||
|
): Promise<WaterGoalResponseDto> {
|
||||||
|
this.logger.log(`更新喝水目标 - 用户ID: ${user.sub}, 目标: ${updateDto.dailyWaterGoal}ml`);
|
||||||
|
return this.waterRecordsService.updateWaterGoal(user.sub, updateDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取今日喝水统计
|
||||||
|
*/
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Get('stats/today')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({ summary: '获取今日喝水统计' })
|
||||||
|
@ApiResponse({ status: 200, description: '成功获取今日喝水统计', type: TodayWaterStatsResponseDto })
|
||||||
|
async getTodayWaterStats(
|
||||||
|
@CurrentUser() user: AccessTokenPayload,
|
||||||
|
): Promise<TodayWaterStatsResponseDto> {
|
||||||
|
this.logger.log(`获取今日喝水统计 - 用户ID: ${user.sub}`);
|
||||||
|
return this.waterRecordsService.getTodayWaterStats(user.sub);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/water-records/water-records.module.ts
Normal file
19
src/water-records/water-records.module.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Module, forwardRef } from '@nestjs/common';
|
||||||
|
import { SequelizeModule } from '@nestjs/sequelize';
|
||||||
|
import { WaterRecordsController } from './water-records.controller';
|
||||||
|
import { WaterRecordsService } from './water-records.service';
|
||||||
|
import { UserWaterHistory } from './models/user-water-history.model';
|
||||||
|
import { UserProfile } from '../users/models/user-profile.model';
|
||||||
|
import { ActivityLog } from '../activity-logs/models/activity-log.model';
|
||||||
|
import { UsersModule } from '../users/users.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
SequelizeModule.forFeature([UserWaterHistory, UserProfile, ActivityLog]),
|
||||||
|
forwardRef(() => UsersModule),
|
||||||
|
],
|
||||||
|
controllers: [WaterRecordsController],
|
||||||
|
providers: [WaterRecordsService],
|
||||||
|
exports: [WaterRecordsService],
|
||||||
|
})
|
||||||
|
export class WaterRecordsModule { }
|
||||||
270
src/water-records/water-records.service.ts
Normal file
270
src/water-records/water-records.service.ts
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
import { Injectable, Logger, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||||
|
import { InjectModel } from '@nestjs/sequelize';
|
||||||
|
import { UserWaterHistory } from './models/user-water-history.model';
|
||||||
|
import { UserProfile } from '../users/models/user-profile.model';
|
||||||
|
import {
|
||||||
|
CreateWaterRecordDto,
|
||||||
|
UpdateWaterRecordDto,
|
||||||
|
WaterRecordResponseDto,
|
||||||
|
GetWaterRecordsQueryDto,
|
||||||
|
WaterRecordsListResponseDto,
|
||||||
|
UpdateWaterGoalDto,
|
||||||
|
WaterGoalResponseDto,
|
||||||
|
TodayWaterStatsResponseDto
|
||||||
|
} from './dto/water-record.dto';
|
||||||
|
import { Op } from 'sequelize';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WaterRecordsService {
|
||||||
|
private readonly logger = new Logger(WaterRecordsService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectModel(UserWaterHistory)
|
||||||
|
private readonly userWaterHistoryModel: typeof UserWaterHistory,
|
||||||
|
@InjectModel(UserProfile)
|
||||||
|
private readonly userProfileModel: typeof UserProfile,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建喝水记录
|
||||||
|
*/
|
||||||
|
async createWaterRecord(userId: string, createDto: CreateWaterRecordDto): Promise<WaterRecordResponseDto> {
|
||||||
|
try {
|
||||||
|
const waterRecord = await this.userWaterHistoryModel.create({
|
||||||
|
userId,
|
||||||
|
amount: createDto.amount,
|
||||||
|
recordedAt: createDto.recordedAt || new Date(),
|
||||||
|
note: createDto.note,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`用户 ${userId} 创建喝水记录成功,记录ID: ${waterRecord.id}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: '喝水记录创建成功',
|
||||||
|
data: {
|
||||||
|
id: waterRecord.id,
|
||||||
|
amount: waterRecord.amount,
|
||||||
|
recordedAt: waterRecord.recordedAt,
|
||||||
|
note: waterRecord.note,
|
||||||
|
createdAt: waterRecord.createdAt,
|
||||||
|
updatedAt: waterRecord.updatedAt,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`创建喝水记录失败: ${error.message}`, error.stack);
|
||||||
|
throw new BadRequestException('创建喝水记录失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取喝水记录列表
|
||||||
|
*/
|
||||||
|
async getWaterRecords(userId: string, query: GetWaterRecordsQueryDto): Promise<WaterRecordsListResponseDto> {
|
||||||
|
try {
|
||||||
|
const { startDate, endDate, page = 1, limit = 20 } = query;
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
|
const whereCondition: any = { userId };
|
||||||
|
|
||||||
|
if (startDate || endDate) {
|
||||||
|
whereCondition.recordedAt = {};
|
||||||
|
if (startDate) {
|
||||||
|
whereCondition.recordedAt[Op.gte] = new Date(startDate);
|
||||||
|
}
|
||||||
|
if (endDate) {
|
||||||
|
const endDateTime = new Date(endDate);
|
||||||
|
endDateTime.setHours(23, 59, 59, 999);
|
||||||
|
whereCondition.recordedAt[Op.lte] = endDateTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { rows: records, count: total } = await this.userWaterHistoryModel.findAndCountAll({
|
||||||
|
where: whereCondition,
|
||||||
|
order: [['recordedAt', 'DESC']],
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(total / limit);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: '获取喝水记录成功',
|
||||||
|
data: {
|
||||||
|
records: records.map(record => ({
|
||||||
|
id: record.id,
|
||||||
|
amount: record.amount,
|
||||||
|
recordedAt: record.recordedAt,
|
||||||
|
note: record.note,
|
||||||
|
createdAt: record.createdAt,
|
||||||
|
updatedAt: record.updatedAt,
|
||||||
|
})),
|
||||||
|
pagination: {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
totalPages,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`获取喝水记录失败: ${error.message}`, error.stack);
|
||||||
|
throw new BadRequestException('获取喝水记录失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新喝水记录
|
||||||
|
*/
|
||||||
|
async updateWaterRecord(userId: string, recordId: number, updateDto: UpdateWaterRecordDto): Promise<WaterRecordResponseDto> {
|
||||||
|
try {
|
||||||
|
const waterRecord = await this.userWaterHistoryModel.findOne({
|
||||||
|
where: { id: recordId, userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!waterRecord) {
|
||||||
|
throw new NotFoundException('喝水记录不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
await waterRecord.update({
|
||||||
|
amount: updateDto.amount ?? waterRecord.amount,
|
||||||
|
recordedAt: updateDto.recordedAt ?? waterRecord.recordedAt,
|
||||||
|
note: updateDto.note ?? waterRecord.note,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`用户 ${userId} 更新喝水记录成功,记录ID: ${recordId}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: '喝水记录更新成功',
|
||||||
|
data: {
|
||||||
|
id: waterRecord.id,
|
||||||
|
amount: waterRecord.amount,
|
||||||
|
recordedAt: waterRecord.recordedAt,
|
||||||
|
note: waterRecord.note,
|
||||||
|
createdAt: waterRecord.createdAt,
|
||||||
|
updatedAt: waterRecord.updatedAt,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof NotFoundException) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
this.logger.error(`更新喝水记录失败: ${error.message}`, error.stack);
|
||||||
|
throw new BadRequestException('更新喝水记录失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除喝水记录
|
||||||
|
*/
|
||||||
|
async deleteWaterRecord(userId: string, recordId: number): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const result = await this.userWaterHistoryModel.destroy({
|
||||||
|
where: { id: recordId, userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`用户 ${userId} 删除喝水记录成功,记录ID: ${recordId}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`删除喝水记录失败: ${error.message}`, error.stack);
|
||||||
|
throw new BadRequestException('删除喝水记录失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新喝水目标
|
||||||
|
*/
|
||||||
|
async updateWaterGoal(userId: string, updateDto: UpdateWaterGoalDto): Promise<WaterGoalResponseDto> {
|
||||||
|
try {
|
||||||
|
const userProfile = await this.userProfileModel.findOne({
|
||||||
|
where: { userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userProfile) {
|
||||||
|
throw new NotFoundException('用户档案不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
await userProfile.update({
|
||||||
|
dailyWaterGoal: updateDto.dailyWaterGoal,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`用户 ${userId} 更新喝水目标成功: ${updateDto.dailyWaterGoal}ml`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: '喝水目标更新成功',
|
||||||
|
data: {
|
||||||
|
dailyWaterGoal: userProfile.dailyWaterGoal,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof NotFoundException) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
this.logger.error(`更新喝水目标失败: ${error.message}`, error.stack);
|
||||||
|
throw new BadRequestException('更新喝水目标失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取今日喝水统计
|
||||||
|
*/
|
||||||
|
async getTodayWaterStats(userId: string): Promise<TodayWaterStatsResponseDto> {
|
||||||
|
try {
|
||||||
|
// 获取今天的开始和结束时间
|
||||||
|
const today = new Date();
|
||||||
|
const startOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||||
|
const endOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 23, 59, 59, 999);
|
||||||
|
|
||||||
|
// 获取今日喝水记录
|
||||||
|
const todayRecords = await this.userWaterHistoryModel.findAll({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
recordedAt: {
|
||||||
|
[Op.between]: [startOfDay, endOfDay],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
order: [['recordedAt', 'ASC']],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算今日总喝水量
|
||||||
|
const totalAmount = todayRecords.reduce((sum, record) => sum + record.amount, 0);
|
||||||
|
|
||||||
|
// 获取用户的喝水目标
|
||||||
|
const userProfile = await this.userProfileModel.findOne({
|
||||||
|
where: { userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
const dailyGoal = userProfile?.dailyWaterGoal || 2000; // 默认目标2000ml
|
||||||
|
const completionRate = Math.min((totalAmount / dailyGoal) * 100, 100);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: '获取今日喝水统计成功',
|
||||||
|
data: {
|
||||||
|
date: startOfDay.toISOString().split('T')[0],
|
||||||
|
totalAmount,
|
||||||
|
dailyGoal,
|
||||||
|
completionRate: Math.round(completionRate * 100) / 100,
|
||||||
|
recordCount: todayRecords.length,
|
||||||
|
records: todayRecords.map(record => ({
|
||||||
|
id: record.id,
|
||||||
|
amount: record.amount,
|
||||||
|
recordedAt: record.recordedAt,
|
||||||
|
note: record.note,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`获取今日喝水统计失败: ${error.message}`, error.stack);
|
||||||
|
throw new BadRequestException('获取今日喝水统计失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user