feat(water-records): 新增喝水记录功能模块
新增完整的喝水记录管理功能,支持用户记录每日喝水情况、设置目标和查看统计信息。功能包括: - 创建、查询、更新和删除喝水记录 - 设置和管理每日喝水目标 - 获取今日喝水统计和完成率分析 - 支持分页查询和日期范围筛选 - 完整的数据验证和错误处理机制 该模块已从用户模块中独立出来,提供REST API接口,包含数据库迁移脚本和详细文档。
This commit is contained in:
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