feat: 新增心情打卡功能模块
实现心情打卡的完整功能,包括数据库表设计、API接口、业务逻辑和文档说明。支持记录多种心情类型、强度评分和统计分析功能。
This commit is contained in:
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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user