From f26d8e64c6a8dbc8ea6d8bdb487582d9dab84755 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Thu, 21 Aug 2025 15:20:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=BF=83=E6=83=85?= =?UTF-8?q?=E6=89=93=E5=8D=A1=E5=8A=9F=E8=83=BD=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现心情打卡的完整功能,包括数据库表设计、API接口、业务逻辑和文档说明。支持记录多种心情类型、强度评分和统计分析功能。 --- sql-scripts/mood-checkins-table-create.sql | 24 ++ src/app.module.ts | 2 + src/mood-checkins/README.md | 145 ++++++++++ src/mood-checkins/dto/mood-checkin.dto.ts | 133 +++++++++ .../models/mood-checkin.model.ts | 97 +++++++ src/mood-checkins/mood-checkins.controller.ts | 106 +++++++ src/mood-checkins/mood-checkins.module.ts | 15 + src/mood-checkins/mood-checkins.service.ts | 272 ++++++++++++++++++ test-mood-checkins.http | 94 ++++++ 9 files changed, 888 insertions(+) create mode 100644 sql-scripts/mood-checkins-table-create.sql create mode 100644 src/mood-checkins/README.md create mode 100644 src/mood-checkins/dto/mood-checkin.dto.ts create mode 100644 src/mood-checkins/models/mood-checkin.model.ts create mode 100644 src/mood-checkins/mood-checkins.controller.ts create mode 100644 src/mood-checkins/mood-checkins.module.ts create mode 100644 src/mood-checkins/mood-checkins.service.ts create mode 100644 test-mood-checkins.http diff --git a/sql-scripts/mood-checkins-table-create.sql b/sql-scripts/mood-checkins-table-create.sql new file mode 100644 index 0000000..3e49f52 --- /dev/null +++ b/sql-scripts/mood-checkins-table-create.sql @@ -0,0 +1,24 @@ +-- 心情打卡表 +CREATE TABLE IF NOT EXISTS `t_mood_checkins` ( + `id` varchar(36) NOT NULL COMMENT '主键ID', + `user_id` varchar(255) NOT NULL COMMENT '用户ID', + `mood_type` enum('happy','excited','thrilled','calm','anxious','sad','lonely','wronged','angry','tired') NOT NULL COMMENT '心情类型:开心、心动、兴奋、平静、焦虑、难过、孤独、委屈、生气、心累', + `intensity` int NOT NULL DEFAULT '5' COMMENT '心情强度(1-10)', + `description` text COMMENT '心情描述', + `checkin_date` date NOT NULL COMMENT '打卡日期(YYYY-MM-DD)', + `metadata` json DEFAULT NULL COMMENT '扩展数据(标签、触发事件等)', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_checkin_date` (`checkin_date`), + KEY `idx_mood_type` (`mood_type`), + KEY `idx_user_date` (`user_id`, `checkin_date`), + KEY `idx_deleted` (`deleted`), + CONSTRAINT `fk_mood_checkins_user_id` FOREIGN KEY (`user_id`) REFERENCES `t_users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='心情打卡表'; + +-- 添加索引以优化查询性能 +CREATE INDEX `idx_user_mood_date` ON `t_mood_checkins` (`user_id`, `mood_type`, `checkin_date`); +CREATE INDEX `idx_intensity` ON `t_mood_checkins` (`intensity`); \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index 5ee4c5b..ecefe59 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -13,6 +13,7 @@ import { RecommendationsModule } from './recommendations/recommendations.module' import { ActivityLogsModule } from './activity-logs/activity-logs.module'; import { ExercisesModule } from './exercises/exercises.module'; import { WorkoutsModule } from './workouts/workouts.module'; +import { MoodCheckinsModule } from './mood-checkins/mood-checkins.module'; @Module({ imports: [ @@ -31,6 +32,7 @@ import { WorkoutsModule } from './workouts/workouts.module'; ActivityLogsModule, ExercisesModule, WorkoutsModule, + MoodCheckinsModule, ], controllers: [AppController], providers: [AppService], diff --git a/src/mood-checkins/README.md b/src/mood-checkins/README.md new file mode 100644 index 0000000..f5c6523 --- /dev/null +++ b/src/mood-checkins/README.md @@ -0,0 +1,145 @@ +# 心情打卡功能 + +## 功能概述 + +心情打卡功能允许用户记录每日的情绪状态,包括喜怒哀乐四种基本情绪类型,并可以添加强度评分和详细描述。 + +## 功能特性 + +### 1. 心情类型 +- **开心 (happy)**: 愉悦、满足的情绪 +- **心动 (excited)**: 因喜欢而心跳加速的情绪 +- **兴奋 (thrilled)**: 激动、充满活力的情绪 +- **平静 (calm)**: 宁静、安详的情绪 +- **焦虑 (anxious)**: 紧张、担忧的情绪 +- **难过 (sad)**: 悲伤、失落的情绪 +- **孤独 (lonely)**: 寂寞、孤单的情绪 +- **委屈 (wronged)**: 受到不公正对待的情绪 +- **生气 (angry)**: 愤怒、恼火的情绪 +- **心累 (tired)**: 精神疲惫、倦怠的情绪 + +### 2. 核心功能 +- ✅ 创建心情打卡记录 +- ✅ 更新已有心情记录 +- ✅ 删除心情记录(软删除) +- ✅ 查看每日心情记录 +- ✅ 查看历史心情记录 +- ✅ 心情统计分析 + +### 3. 数据字段 +- `moodType`: 心情类型(必填) +- `intensity`: 心情强度 1-10(必填,默认5) +- `description`: 心情描述(可选) +- `checkinDate`: 打卡日期(可选,默认当天) +- `metadata`: 扩展数据,如标签、触发事件等(可选) + +## API 接口 + +### 创建心情打卡 +```http +POST /mood-checkins +``` + +### 更新心情打卡 +```http +PUT /mood-checkins +``` + +### 删除心情打卡 +```http +DELETE /mood-checkins +``` + +### 获取每日心情 +```http +GET /mood-checkins/daily?date=2025-08-21 +``` + +### 获取心情历史 +```http +GET /mood-checkins/history?startDate=2025-08-01&endDate=2025-08-31&moodType=joy +``` + +### 获取心情统计 +```http +GET /mood-checkins/statistics?startDate=2025-08-01&endDate=2025-08-31 +``` + +## 数据库设计 + +### 表结构 +- 表名: `t_mood_checkins` +- 主键: `id` (UUID) +- 外键: `user_id` (关联用户表) +- 索引: 用户ID、打卡日期、心情类型等 + +### 约束条件 +- 同一用户同一天同一心情类型只能有一条记录(自动更新) +- 心情强度范围: 1-10 +- 支持软删除 + +## 业务逻辑 + +### 1. 创建逻辑 +- 检查当天是否已有相同心情类型的记录 +- 如果存在则更新,不存在则创建新记录 +- 记录活动日志 + +### 2. 统计功能 +- 总打卡次数 +- 平均心情强度 +- 各心情类型分布 +- 最频繁的心情类型 + +### 3. 权限控制 +- 用户只能操作自己的心情记录 +- 需要JWT认证 +- 支持活动日志记录 + +## 使用示例 + +### 创建心情打卡 +```json +{ + "moodType": "joy", + "intensity": 8, + "description": "今天工作顺利,心情很好", + "metadata": { + "tags": ["工作", "成就感"], + "trigger": "完成重要项目" + } +} +``` + +### 统计数据响应 +```json +{ + "code": 200, + "message": "success", + "data": { + "totalCheckins": 30, + "averageIntensity": 6.8, + "moodDistribution": { + "happy": 8, + "excited": 5, + "thrilled": 4, + "calm": 6, + "anxious": 3, + "sad": 2, + "lonely": 1, + "wronged": 0, + "angry": 1, + "tired": 0 + }, + "mostFrequentMood": "happy" + } +} +``` + +## 扩展功能建议 + +1. **心情趋势分析**: 基于历史数据分析心情变化趋势 +2. **心情提醒**: 定时提醒用户进行心情打卡 +3. **心情分享**: 允许用户分享心情状态 +4. **心情建议**: 基于心情状态提供改善建议 +5. **数据导出**: 支持心情数据导出功能 \ No newline at end of file diff --git a/src/mood-checkins/dto/mood-checkin.dto.ts b/src/mood-checkins/dto/mood-checkin.dto.ts new file mode 100644 index 0000000..e91b11a --- /dev/null +++ b/src/mood-checkins/dto/mood-checkin.dto.ts @@ -0,0 +1,133 @@ +import { ApiProperty, PartialType } from '@nestjs/swagger'; +import { IsString, IsOptional, IsEnum, IsInt, Min, Max, IsObject, IsDateString } from 'class-validator'; +import { MoodType } from '../models/mood-checkin.model'; +import { ResponseCode } from '../../base.dto'; + +export class CreateMoodCheckinDto { + // 由后端从登录态注入 + @IsOptional() + @IsString() + userId?: string; + + @ApiProperty({ + enum: MoodType, + description: '心情类型:开心、心动、兴奋、平静、焦虑、难过、孤独、委屈、生气、心累', + example: MoodType.HAPPY + }) + @IsEnum(MoodType) + moodType: MoodType; + + @ApiProperty({ + description: '心情强度(1-10)', + minimum: 1, + maximum: 10, + example: 7 + }) + @IsInt() + @Min(1) + @Max(10) + intensity: number; + + @ApiProperty({ + description: '心情描述', + required: false, + example: '今天工作顺利,心情很好' + }) + @IsOptional() + @IsString() + description?: string; + + @ApiProperty({ + description: '打卡日期(YYYY-MM-DD)', + required: false, + example: '2025-08-21' + }) + @IsOptional() + @IsDateString() + checkinDate?: string; + + @ApiProperty({ + description: '扩展数据', + required: false, + example: { tags: ['工作', '运动'], trigger: '完成重要项目' } + }) + @IsOptional() + @IsObject() + metadata?: Record; +} + +export class UpdateMoodCheckinDto extends PartialType(CreateMoodCheckinDto) { + @ApiProperty({ description: '心情打卡ID' }) + @IsString() + id: string; + + // 由后端从登录态注入 + @IsOptional() + @IsString() + userId?: string; +} + +export class RemoveMoodCheckinDto { + @ApiProperty({ description: '心情打卡ID' }) + @IsString() + id: string; + + // 由后端从登录态注入 + @IsOptional() + @IsString() + userId?: string; +} + +export class MoodCheckinResponseDto { + @ApiProperty({ description: '状态码', example: ResponseCode.SUCCESS }) + code: ResponseCode; + + @ApiProperty({ description: '消息', example: 'success' }) + message: string; + + @ApiProperty({ description: '数据' }) + data: T; +} + +export class GetMoodCheckinsQueryDto { + @ApiProperty({ + description: '日期(YYYY-MM-DD),不传则默认今天', + required: false, + example: '2025-08-21' + }) + @IsOptional() + @IsString() + date?: string; +} + +export class GetMoodHistoryQueryDto { + @ApiProperty({ + description: '开始日期(YYYY-MM-DD)', + example: '2025-08-01' + }) + @IsString() + startDate: string; + + @ApiProperty({ + description: '结束日期(YYYY-MM-DD)', + example: '2025-08-31' + }) + @IsString() + endDate: string; + + @ApiProperty({ + description: '心情类型过滤:开心、心动、兴奋、平静、焦虑、难过、孤独、委屈、生气、心累', + enum: MoodType, + required: false + }) + @IsOptional() + @IsEnum(MoodType) + moodType?: MoodType; +} + +export interface MoodStatistics { + totalCheckins: number; + averageIntensity: number; + moodDistribution: Record; + mostFrequentMood: MoodType | null; +} \ No newline at end of file diff --git a/src/mood-checkins/models/mood-checkin.model.ts b/src/mood-checkins/models/mood-checkin.model.ts new file mode 100644 index 0000000..1afb475 --- /dev/null +++ b/src/mood-checkins/models/mood-checkin.model.ts @@ -0,0 +1,97 @@ +import { Column, Model, Table, DataType, ForeignKey, BelongsTo } from 'sequelize-typescript'; +import { User } from '../../users/models/user.model'; + +export enum MoodType { + HAPPY = 'happy', // 开心 + EXCITED = 'excited', // 心动 + THRILLED = 'thrilled', // 兴奋 + CALM = 'calm', // 平静 + ANXIOUS = 'anxious', // 焦虑 + SAD = 'sad', // 难过 + LONELY = 'lonely', // 孤独 + WRONGED = 'wronged', // 委屈 + ANGRY = 'angry', // 生气 + TIRED = 'tired' // 心累 +} + +@Table({ + tableName: 't_mood_checkins', + underscored: true, +}) +export class MoodCheckin extends Model { + @Column({ + type: DataType.UUID, + defaultValue: DataType.UUIDV4, + primaryKey: true, + }) + declare id: string; + + @ForeignKey(() => User) + @Column({ + type: DataType.STRING, + allowNull: false, + comment: '用户ID', + }) + declare userId: string; + + @BelongsTo(() => User) + declare user: User; + + @Column({ + type: DataType.ENUM(...Object.values(MoodType)), + allowNull: false, + comment: '心情类型:喜怒哀乐', + }) + declare moodType: MoodType; + + @Column({ + type: DataType.INTEGER, + allowNull: false, + defaultValue: 5, + comment: '心情强度(1-10)', + validate: { + min: 1, + max: 10, + }, + }) + declare intensity: number; + + @Column({ + type: DataType.TEXT, + allowNull: true, + comment: '心情描述', + }) + declare description: string | null; + + @Column({ + type: DataType.DATEONLY, + allowNull: false, + comment: '打卡日期(YYYY-MM-DD)', + }) + declare checkinDate: string; + + @Column({ + type: DataType.JSON, + allowNull: true, + comment: '扩展数据(标签、触发事件等)', + }) + declare metadata: Record | null; + + @Column({ + type: DataType.DATE, + defaultValue: DataType.NOW, + }) + declare createdAt: Date; + + @Column({ + type: DataType.DATE, + defaultValue: DataType.NOW, + }) + declare updatedAt: Date; + + @Column({ + type: DataType.BOOLEAN, + defaultValue: false, + }) + declare deleted: boolean; +} \ No newline at end of file diff --git a/src/mood-checkins/mood-checkins.controller.ts b/src/mood-checkins/mood-checkins.controller.ts new file mode 100644 index 0000000..ea8ebb7 --- /dev/null +++ b/src/mood-checkins/mood-checkins.controller.ts @@ -0,0 +1,106 @@ +import { Controller, Get, Post, Put, Delete, Body, Query, UseGuards, Logger } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; +import { MoodCheckinsService } from './mood-checkins.service'; +import { + CreateMoodCheckinDto, + UpdateMoodCheckinDto, + RemoveMoodCheckinDto, + MoodCheckinResponseDto, + GetMoodCheckinsQueryDto, + GetMoodHistoryQueryDto, + MoodStatistics +} from './dto/mood-checkin.dto'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { CurrentUser } from '../common/decorators/current-user.decorator'; + +@ApiTags('心情打卡') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard) +@Controller('mood-checkins') +export class MoodCheckinsController { + private readonly logger = new Logger(MoodCheckinsController.name); + + constructor(private readonly moodCheckinsService: MoodCheckinsService) { } + + @Post() + @ApiOperation({ summary: '创建心情打卡' }) + @ApiResponse({ status: 201, description: '心情打卡创建成功', type: MoodCheckinResponseDto }) + @ApiResponse({ status: 400, description: '请求参数错误' }) + @ApiResponse({ status: 401, description: '未授权' }) + async create( + @Body() createMoodCheckinDto: CreateMoodCheckinDto, + @CurrentUser('id') userId: string, + ): Promise { + this.logger.log(`用户 ${userId} 创建心情打卡: ${JSON.stringify(createMoodCheckinDto)}`); + return this.moodCheckinsService.create(createMoodCheckinDto, userId); + } + + @Put() + @ApiOperation({ summary: '更新心情打卡' }) + @ApiResponse({ status: 200, description: '心情打卡更新成功', type: MoodCheckinResponseDto }) + @ApiResponse({ status: 400, description: '请求参数错误' }) + @ApiResponse({ status: 401, description: '未授权' }) + @ApiResponse({ status: 403, description: '无权操作' }) + @ApiResponse({ status: 404, description: '记录不存在' }) + async update( + @Body() updateMoodCheckinDto: UpdateMoodCheckinDto, + @CurrentUser('id') userId: string, + ): Promise { + this.logger.log(`用户 ${userId} 更新心情打卡: ${JSON.stringify(updateMoodCheckinDto)}`); + return this.moodCheckinsService.update(updateMoodCheckinDto, userId); + } + + @Delete() + @ApiOperation({ summary: '删除心情打卡' }) + @ApiResponse({ status: 200, description: '心情打卡删除成功', type: MoodCheckinResponseDto }) + @ApiResponse({ status: 401, description: '未授权' }) + @ApiResponse({ status: 403, description: '无权操作' }) + @ApiResponse({ status: 404, description: '记录不存在' }) + async remove( + @Body() removeMoodCheckinDto: RemoveMoodCheckinDto, + @CurrentUser('id') userId: string, + ): Promise { + this.logger.log(`用户 ${userId} 删除心情打卡: ${removeMoodCheckinDto.id}`); + return this.moodCheckinsService.remove(removeMoodCheckinDto, userId); + } + + @Get('daily') + @ApiOperation({ summary: '获取每日心情打卡' }) + @ApiResponse({ status: 200, description: '获取成功', type: MoodCheckinResponseDto }) + @ApiResponse({ status: 400, description: '日期格式错误' }) + @ApiResponse({ status: 401, description: '未授权' }) + async getDaily( + @Query() query: GetMoodCheckinsQueryDto, + @CurrentUser('id') userId: string, + ): Promise { + this.logger.log(`用户 ${userId} 获取每日心情打卡: ${query.date || '今天'}`); + return this.moodCheckinsService.getDaily(userId, query.date); + } + + @Get('history') + @ApiOperation({ summary: '获取心情打卡历史' }) + @ApiResponse({ status: 200, description: '获取成功', type: MoodCheckinResponseDto }) + @ApiResponse({ status: 400, description: '日期范围错误' }) + @ApiResponse({ status: 401, description: '未授权' }) + async getHistory( + @Query() query: GetMoodHistoryQueryDto, + @CurrentUser('id') userId: string, + ): Promise { + this.logger.log(`用户 ${userId} 获取心情打卡历史: ${query.startDate} - ${query.endDate}`); + return this.moodCheckinsService.getHistory(userId, query); + } + + @Get('statistics') + @ApiOperation({ summary: '获取心情统计数据' }) + @ApiResponse({ status: 200, description: '获取成功', type: MoodCheckinResponseDto }) + @ApiResponse({ status: 400, description: '日期范围错误' }) + @ApiResponse({ status: 401, description: '未授权' }) + async getStatistics( + @Query('startDate') startDate: string, + @Query('endDate') endDate: string, + @CurrentUser('id') userId: string, + ): Promise> { + this.logger.log(`用户 ${userId} 获取心情统计: ${startDate} - ${endDate}`); + return this.moodCheckinsService.getStatistics(userId, startDate, endDate); + } +} \ No newline at end of file diff --git a/src/mood-checkins/mood-checkins.module.ts b/src/mood-checkins/mood-checkins.module.ts new file mode 100644 index 0000000..d681d45 --- /dev/null +++ b/src/mood-checkins/mood-checkins.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { SequelizeModule } from '@nestjs/sequelize'; +import { MoodCheckinsService } from './mood-checkins.service'; +import { MoodCheckinsController } from './mood-checkins.controller'; +import { MoodCheckin } from './models/mood-checkin.model'; +import { UsersModule } from '../users/users.module'; +import { ActivityLogsModule } from '../activity-logs/activity-logs.module'; + +@Module({ + imports: [SequelizeModule.forFeature([MoodCheckin]), UsersModule, ActivityLogsModule], + providers: [MoodCheckinsService], + controllers: [MoodCheckinsController], + exports: [MoodCheckinsService], +}) +export class MoodCheckinsModule { } \ No newline at end of file diff --git a/src/mood-checkins/mood-checkins.service.ts b/src/mood-checkins/mood-checkins.service.ts new file mode 100644 index 0000000..dffa02c --- /dev/null +++ b/src/mood-checkins/mood-checkins.service.ts @@ -0,0 +1,272 @@ +import { Injectable, NotFoundException, Logger, ForbiddenException, BadRequestException } from '@nestjs/common'; +import { InjectModel } from '@nestjs/sequelize'; +import { MoodCheckin, MoodType } from './models/mood-checkin.model'; +import { + CreateMoodCheckinDto, + UpdateMoodCheckinDto, + RemoveMoodCheckinDto, + MoodCheckinResponseDto, + GetMoodHistoryQueryDto, + MoodStatistics +} from './dto/mood-checkin.dto'; +import { ResponseCode } from '../base.dto'; +import * as dayjs from 'dayjs'; +import { Op } from 'sequelize'; +import { ActivityLogsService } from '../activity-logs/activity-logs.service'; +import { ActivityActionType, ActivityEntityType } from '../activity-logs/models/activity-log.model'; + +@Injectable() +export class MoodCheckinsService { + private readonly logger = new Logger(MoodCheckinsService.name); + + constructor( + @InjectModel(MoodCheckin) + private readonly moodCheckinModel: typeof MoodCheckin, + private readonly activityLogsService: ActivityLogsService, + ) { } + + async create(dto: CreateMoodCheckinDto, userId: string): Promise { + const checkinDate = dto.checkinDate || dayjs().format('YYYY-MM-DD'); + + // 检查当天是否已有相同心情类型的记录 + const existingRecord = await this.moodCheckinModel.findOne({ + where: { + userId, + moodType: dto.moodType, + checkinDate, + deleted: false, + }, + }); + + if (existingRecord) { + // 如果存在,则更新现有记录 + return this.update({ + ...dto, + id: existingRecord.id, + checkinDate, + }, userId); + } + + const record = await this.moodCheckinModel.create({ + userId, + moodType: dto.moodType, + intensity: dto.intensity, + description: dto.description || null, + checkinDate, + metadata: dto.metadata || null, + }); + + await this.activityLogsService.record({ + userId, + entityType: ActivityEntityType.CHECKIN, + action: ActivityActionType.CREATE, + entityId: record.id, + changes: record.toJSON(), + }); + + return { + code: ResponseCode.SUCCESS, + message: '心情打卡成功', + data: record.toJSON() + }; + } + + async update(dto: UpdateMoodCheckinDto, userId: string): Promise { + const record = await this.moodCheckinModel.findOne({ + where: { + id: dto.id, + deleted: false, + }, + }); + + if (!record) { + throw new NotFoundException('心情打卡记录不存在'); + } + + if (record.userId !== userId) { + throw new ForbiddenException('无权操作该心情打卡记录'); + } + + const changes: Record = {}; + if (dto.moodType !== undefined) { + record.moodType = dto.moodType; + changes.moodType = dto.moodType; + } + if (dto.intensity !== undefined) { + record.intensity = dto.intensity; + changes.intensity = dto.intensity; + } + if (dto.description !== undefined) { + record.description = dto.description; + changes.description = dto.description; + } + if (dto.checkinDate !== undefined) { + record.checkinDate = dto.checkinDate; + changes.checkinDate = dto.checkinDate; + } + if (dto.metadata !== undefined) { + record.metadata = dto.metadata as any; + changes.metadata = dto.metadata; + } + + await record.save(); + + await this.activityLogsService.record({ + userId: record.userId, + entityType: ActivityEntityType.CHECKIN, + action: ActivityActionType.UPDATE, + entityId: record.id, + changes, + }); + + return { + code: ResponseCode.SUCCESS, + message: '心情打卡更新成功', + data: record.toJSON() + }; + } + + async remove(dto: RemoveMoodCheckinDto, userId: string): Promise { + const record = await this.moodCheckinModel.findByPk(dto.id); + + if (!record) { + throw new NotFoundException('心情打卡记录不存在'); + } + + if (record.userId !== userId) { + throw new ForbiddenException('无权操作该心情打卡记录'); + } + + record.deleted = true; + await record.save(); + + await this.activityLogsService.record({ + userId: record.userId, + entityType: ActivityEntityType.CHECKIN, + action: ActivityActionType.DELETE, + entityId: record.id, + changes: null, + }); + + return { + code: ResponseCode.SUCCESS, + message: '心情打卡删除成功', + data: { id: dto.id } + }; + } + + async getDaily(userId: string, date?: string): Promise { + const targetDate = date || dayjs().format('YYYY-MM-DD'); + + if (!dayjs(targetDate, 'YYYY-MM-DD').isValid()) { + throw new BadRequestException('无效日期格式'); + } + + const records = await this.moodCheckinModel.findAll({ + where: { + userId, + checkinDate: targetDate, + deleted: false, + }, + order: [['createdAt', 'ASC']], + }); + + return { + code: ResponseCode.SUCCESS, + message: 'success', + data: records.map(r => r.toJSON()) + }; + } + + async getHistory(userId: string, query: GetMoodHistoryQueryDto): Promise { + const start = dayjs(query.startDate, 'YYYY-MM-DD'); + const end = dayjs(query.endDate, 'YYYY-MM-DD'); + + if (!start.isValid() || !end.isValid() || end.isBefore(start)) { + throw new BadRequestException('无效日期范围'); + } + + const whereCondition: any = { + userId, + checkinDate: { + [Op.between]: [start.format('YYYY-MM-DD'), end.format('YYYY-MM-DD')], + }, + deleted: false, + }; + + if (query.moodType) { + whereCondition.moodType = query.moodType; + } + + const records = await this.moodCheckinModel.findAll({ + where: whereCondition, + order: [['checkinDate', 'DESC'], ['createdAt', 'DESC']], + }); + + return { + code: ResponseCode.SUCCESS, + message: 'success', + data: records.map(r => r.toJSON()) + }; + } + + async getStatistics(userId: string, startDate: string, endDate: string): Promise> { + const start = dayjs(startDate, 'YYYY-MM-DD'); + const end = dayjs(endDate, 'YYYY-MM-DD'); + + if (!start.isValid() || !end.isValid() || end.isBefore(start)) { + throw new BadRequestException('无效日期范围'); + } + + const records = await this.moodCheckinModel.findAll({ + where: { + userId, + checkinDate: { + [Op.between]: [start.format('YYYY-MM-DD'), end.format('YYYY-MM-DD')], + }, + deleted: false, + }, + }); + + const totalCheckins = records.length; + const averageIntensity = totalCheckins > 0 + ? records.reduce((sum, record) => sum + record.intensity, 0) / totalCheckins + : 0; + + const moodDistribution: Record = { + [MoodType.HAPPY]: 0, + [MoodType.EXCITED]: 0, + [MoodType.THRILLED]: 0, + [MoodType.CALM]: 0, + [MoodType.ANXIOUS]: 0, + [MoodType.SAD]: 0, + [MoodType.LONELY]: 0, + [MoodType.WRONGED]: 0, + [MoodType.ANGRY]: 0, + [MoodType.TIRED]: 0, + }; + + records.forEach(record => { + moodDistribution[record.moodType]++; + }); + + const mostFrequentMood = totalCheckins > 0 + ? Object.entries(moodDistribution).reduce((a, b) => + moodDistribution[a[0] as MoodType] > moodDistribution[b[0] as MoodType] ? a : b + )[0] as MoodType + : null; + + const statistics: MoodStatistics = { + totalCheckins, + averageIntensity: Math.round(averageIntensity * 100) / 100, + moodDistribution, + mostFrequentMood, + }; + + return { + code: ResponseCode.SUCCESS, + message: 'success', + data: statistics + }; + } +} \ No newline at end of file diff --git a/test-mood-checkins.http b/test-mood-checkins.http new file mode 100644 index 0000000..18000d5 --- /dev/null +++ b/test-mood-checkins.http @@ -0,0 +1,94 @@ +### 心情打卡功能测试 + +### 1. 创建心情打卡 - 开心 +POST http://localhost:3000/mood-checkins +Authorization: Bearer YOUR_JWT_TOKEN +Content-Type: application/json + +{ + "moodType": "happy", + "intensity": 8, + "description": "今天完成了重要的项目,心情很好!", + "checkinDate": "2025-08-21", + "metadata": { + "tags": ["工作", "成就感"], + "trigger": "完成项目里程碑" + } +} + +### 2. 创建心情打卡 - 焦虑 +POST http://localhost:3000/mood-checkins +Authorization: Bearer YOUR_JWT_TOKEN +Content-Type: application/json + +{ + "moodType": "anxious", + "intensity": 6, + "description": "明天有重要的会议,感到有些紧张", + "checkinDate": "2025-08-21", + "metadata": { + "tags": ["工作", "压力"], + "trigger": "重要会议前的紧张" + } +} + +### 3. 创建心情打卡 - 心动 +POST http://localhost:3000/mood-checkins +Authorization: Bearer YOUR_JWT_TOKEN +Content-Type: application/json + +{ + "moodType": "excited", + "intensity": 9, + "description": "看到喜欢的人发来消息,心跳加速", + "checkinDate": "2025-08-21", + "metadata": { + "tags": ["感情", "甜蜜"], + "trigger": "收到心仪对象的消息" + } +} + +### 3. 获取今日心情打卡 +GET http://localhost:3000/mood-checkins/daily +Authorization: Bearer YOUR_JWT_TOKEN + +### 4. 获取指定日期心情打卡 +GET http://localhost:3000/mood-checkins/daily?date=2025-08-21 +Authorization: Bearer YOUR_JWT_TOKEN + +### 5. 获取心情打卡历史 +GET http://localhost:3000/mood-checkins/history?startDate=2025-08-01&endDate=2025-08-31 +Authorization: Bearer YOUR_JWT_TOKEN + +### 6. 获取心情打卡历史(按类型过滤) +GET http://localhost:3000/mood-checkins/history?startDate=2025-08-01&endDate=2025-08-31&moodType=happy +Authorization: Bearer YOUR_JWT_TOKEN + +### 7. 获取心情统计数据 +GET http://localhost:3000/mood-checkins/statistics?startDate=2025-08-01&endDate=2025-08-31 +Authorization: Bearer YOUR_JWT_TOKEN + +### 8. 更新心情打卡 +PUT http://localhost:3000/mood-checkins +Authorization: Bearer YOUR_JWT_TOKEN +Content-Type: application/json + +{ + "id": "MOOD_CHECKIN_ID", + "moodType": "thrilled", + "intensity": 9, + "description": "更新后的心情描述 - 非常兴奋!", + "metadata": { + "tags": ["更新", "兴奋"], + "trigger": "收到好消息" + } +} + +### 9. 删除心情打卡 +DELETE http://localhost:3000/mood-checkins +Authorization: Bearer YOUR_JWT_TOKEN +Content-Type: application/json + +{ + "id": "MOOD_CHECKIN_ID" +} \ No newline at end of file