Files
plates-server/src/mood-checkins/mood-checkins.service.ts
richarjiang f26d8e64c6 feat: 新增心情打卡功能模块
实现心情打卡的完整功能,包括数据库表设计、API接口、业务逻辑和文档说明。支持记录多种心情类型、强度评分和统计分析功能。
2025-08-21 15:20:05 +08:00

272 lines
7.5 KiB
TypeScript

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
};
}
}