feat: 新增心情打卡功能模块
实现心情打卡的完整功能,包括数据库表设计、API接口、业务逻辑和文档说明。支持记录多种心情类型、强度评分和统计分析功能。
This commit is contained in:
24
sql-scripts/mood-checkins-table-create.sql
Normal file
24
sql-scripts/mood-checkins-table-create.sql
Normal file
@@ -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`);
|
||||
@@ -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],
|
||||
|
||||
145
src/mood-checkins/README.md
Normal file
145
src/mood-checkins/README.md
Normal file
@@ -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. **数据导出**: 支持心情数据导出功能
|
||||
133
src/mood-checkins/dto/mood-checkin.dto.ts
Normal file
133
src/mood-checkins/dto/mood-checkin.dto.ts
Normal file
@@ -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<string, any>;
|
||||
}
|
||||
|
||||
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<T = any> {
|
||||
@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<MoodType, number>;
|
||||
mostFrequentMood: MoodType | null;
|
||||
}
|
||||
97
src/mood-checkins/models/mood-checkin.model.ts
Normal file
97
src/mood-checkins/models/mood-checkin.model.ts
Normal file
@@ -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<string, any> | 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;
|
||||
}
|
||||
106
src/mood-checkins/mood-checkins.controller.ts
Normal file
106
src/mood-checkins/mood-checkins.controller.ts
Normal file
@@ -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<MoodCheckinResponseDto> {
|
||||
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<MoodCheckinResponseDto> {
|
||||
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<MoodCheckinResponseDto> {
|
||||
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<MoodCheckinResponseDto> {
|
||||
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<MoodCheckinResponseDto> {
|
||||
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<MoodCheckinResponseDto<MoodStatistics>> {
|
||||
this.logger.log(`用户 ${userId} 获取心情统计: ${startDate} - ${endDate}`);
|
||||
return this.moodCheckinsService.getStatistics(userId, startDate, endDate);
|
||||
}
|
||||
}
|
||||
15
src/mood-checkins/mood-checkins.module.ts
Normal file
15
src/mood-checkins/mood-checkins.module.ts
Normal file
@@ -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 { }
|
||||
272
src/mood-checkins/mood-checkins.service.ts
Normal file
272
src/mood-checkins/mood-checkins.service.ts
Normal file
@@ -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<MoodCheckinResponseDto> {
|
||||
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<MoodCheckinResponseDto> {
|
||||
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<string, any> = {};
|
||||
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<MoodCheckinResponseDto> {
|
||||
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<MoodCheckinResponseDto> {
|
||||
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<MoodCheckinResponseDto> {
|
||||
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<MoodCheckinResponseDto<MoodStatistics>> {
|
||||
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, number> = {
|
||||
[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
|
||||
};
|
||||
}
|
||||
}
|
||||
94
test-mood-checkins.http
Normal file
94
test-mood-checkins.http
Normal file
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user