272 lines
7.5 KiB
TypeScript
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
|
|
};
|
|
}
|
|
} |