import { Injectable, NotFoundException, Logger, ForbiddenException } from '@nestjs/common'; import { InjectModel } from '@nestjs/sequelize'; import { Checkin, CheckinStatus } from './models/checkin.model'; import { CreateCheckinDto, UpdateCheckinDto, CompleteCheckinDto, RemoveCheckinDto, CheckinResponseDto } from './dto/checkin.dto'; import { ResponseCode } from '../base.dto'; import * as dayjs from 'dayjs'; import { Op } from 'sequelize'; @Injectable() export class CheckinsService { private readonly logger = new Logger(CheckinsService.name); constructor( @InjectModel(Checkin) private readonly checkinModel: typeof Checkin, ) { } async create(dto: CreateCheckinDto): Promise { const record = await this.checkinModel.create({ userId: dto.userId, workoutId: dto.workoutId || null, planId: dto.planId || null, title: dto.title || null, checkinDate: dto.checkinDate || null, startedAt: dto.startedAt ? new Date(dto.startedAt) : null, notes: dto.notes || null, metrics: dto.metrics || null, status: CheckinStatus.PENDING, }); return { code: ResponseCode.SUCCESS, message: 'success', data: record.toJSON() }; } async update(dto: UpdateCheckinDto, userId: string): Promise { const record = await this.checkinModel.findByPk(dto.id); if (!record) { throw new NotFoundException('打卡记录不存在'); } if (record.userId !== userId) { throw new ForbiddenException('无权操作该打卡记录'); } if (dto.workoutId !== undefined) record.workoutId = dto.workoutId; if (dto.planId !== undefined) record.planId = dto.planId; if (dto.title !== undefined) record.title = dto.title; if (dto.checkinDate !== undefined) record.checkinDate = dto.checkinDate as any; if (dto.startedAt !== undefined) record.startedAt = dto.startedAt ? new Date(dto.startedAt) : null; if (dto.notes !== undefined) record.notes = dto.notes; if (dto.metrics !== undefined) record.metrics = dto.metrics as any; if (dto.status !== undefined) record.status = dto.status; if (dto.completedAt !== undefined) record.completedAt = dto.completedAt ? new Date(dto.completedAt) : null; if (dto.durationSeconds !== undefined) record.durationSeconds = dto.durationSeconds; await record.save(); return { code: ResponseCode.SUCCESS, message: 'success', data: record.toJSON() }; } async complete(dto: CompleteCheckinDto, userId: string): Promise { const record = await this.checkinModel.findByPk(dto.id); if (!record) { throw new NotFoundException('打卡记录不存在'); } if (record.userId !== userId) { throw new ForbiddenException('无权操作该打卡记录'); } record.status = CheckinStatus.COMPLETED; record.completedAt = dto.completedAt ? new Date(dto.completedAt) : new Date(); if (dto.durationSeconds !== undefined) record.durationSeconds = dto.durationSeconds; if (dto.notes !== undefined) record.notes = dto.notes; if (dto.metrics !== undefined) record.metrics = dto.metrics as any; await record.save(); return { code: ResponseCode.SUCCESS, message: 'success', data: record.toJSON() }; } async remove(dto: RemoveCheckinDto, userId: string): Promise { const record = await this.checkinModel.findByPk(dto.id); if (!record) { throw new NotFoundException('打卡记录不存在'); } if (record.userId !== userId) { throw new ForbiddenException('无权操作该打卡记录'); } await record.destroy(); return { code: ResponseCode.SUCCESS, message: 'success', data: { id: dto.id } }; } async getDaily(userId: string, date?: string): Promise { const target = date ? dayjs(date) : dayjs(); if (!target.isValid()) { return { code: ResponseCode.ERROR, message: '无效日期', data: [] }; } const start = target.startOf('day').toDate(); const end = target.endOf('day').toDate(); // 覆盖两类数据: // 1) checkinDate == YYYY-MM-DD(精确日) // 2) startedAt/ completedAt 落在该日范围内(更鲁棒) const rows = await this.checkinModel.findAll({ where: { userId, [Op.or]: [ { checkinDate: target.format('YYYY-MM-DD') as any }, { [Op.or]: [ { startedAt: { [Op.between]: [start, end] } }, { completedAt: { [Op.between]: [start, end] } }, ], }, ], }, order: [['createdAt', 'ASC']], }); return { code: ResponseCode.SUCCESS, message: 'success', data: rows.map(r => r.toJSON()) }; } }