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 { ActivityLogsModule } from './activity-logs/activity-logs.module';
|
||||||
import { ExercisesModule } from './exercises/exercises.module';
|
import { ExercisesModule } from './exercises/exercises.module';
|
||||||
import { WorkoutsModule } from './workouts/workouts.module';
|
import { WorkoutsModule } from './workouts/workouts.module';
|
||||||
|
import { MoodCheckinsModule } from './mood-checkins/mood-checkins.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -31,6 +32,7 @@ import { WorkoutsModule } from './workouts/workouts.module';
|
|||||||
ActivityLogsModule,
|
ActivityLogsModule,
|
||||||
ExercisesModule,
|
ExercisesModule,
|
||||||
WorkoutsModule,
|
WorkoutsModule,
|
||||||
|
MoodCheckinsModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
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