import type { ReviewStatus } from '../../shared/types.js'; import { databaseDialect, withTransaction, type QueryContext } from '../database.js'; import { recalculateCollectionStatus } from './collectionsService.js'; import { recalculateProjectReviewStatus } from './projectsService.js'; export class ReviewDecisionError extends Error { constructor(public statusCode: number, message: string) { super(message); } } export interface RoundDecisionInput { noteId: number; versionNumber: number; projectId: number; decision: 'approved' | 'changes_requested'; reason: string; actorName: string; actorRole: 'client'; } export async function decideRoundInTransaction(tx: QueryContext, input: RoundDecisionInput) { const round = await tx.one<{ review_round_id: number; review_status: ReviewStatus; collection_id: number; project_id: number; active_round_id: number | null; round_status: string; project_status: string; title: string; description: string; tags: string; }>(`SELECT v.review_round_id,v.review_status,v.title,v.description,v.tags, n.collection_id,n.project_id,n.active_round_id,r.status AS round_status,p.status AS project_status FROM work_versions v JOIN notes n ON n.id=v.note_id JOIN review_rounds r ON r.id=v.review_round_id JOIN projects p ON p.id=n.project_id WHERE v.note_id=? AND v.version_number=? AND n.project_id=?${databaseDialect === 'postgres' ? ' FOR NO KEY UPDATE' : ''}`, [input.noteId, input.versionNumber, input.projectId]); if (!round) throw new ReviewDecisionError(404, '验收轮次不存在'); if (round.project_status !== 'active') throw new ReviewDecisionError(409, '已关闭或归档项目为只读状态'); if (Number(round.active_round_id) !== Number(round.review_round_id) || round.round_status !== 'reviewing') { throw new ReviewDecisionError(409, '历史验收轮次为只读状态'); } if (!['pending', 'changes_requested'].includes(round.review_status)) { throw new ReviewDecisionError(409, '该轮次当前不能重复验收'); } const now = new Date().toISOString(); if (input.decision === 'approved') { await tx.execute("UPDATE work_versions SET candidate_status='selected', review_status='approved' WHERE note_id=? AND version_number=?", [input.noteId, input.versionNumber]); await tx.execute("UPDATE review_rounds SET status='completed', completion_reason='approved', selected_version_number=?, completed_at=? WHERE id=?", [input.versionNumber, now, round.review_round_id]); await tx.execute("UPDATE notes SET title=?,description=?,tags=?,version_number=?,approved_version_number=?,review_status='approved' WHERE id=?", [round.title, round.description, round.tags, input.versionNumber, input.versionNumber, input.noteId]); await tx.execute("UPDATE annotations SET status='confirmed', closure_reason='approved_with_round' WHERE image_id IN (SELECT id FROM images WHERE note_id=? AND version_number=?) AND status='open' AND withdrawn_at IS NULL", [input.noteId, input.versionNumber]); await tx.execute("UPDATE text_annotations SET status='confirmed', closure_reason='approved_with_round' WHERE note_id=? AND version_number=? AND status='open' AND withdrawn_at IS NULL", [input.noteId, input.versionNumber]); await tx.execute("UPDATE work_comments SET status='confirmed', closure_reason='approved_with_round' WHERE note_id=? AND version_number=? AND status='open' AND withdrawn_at IS NULL", [input.noteId, input.versionNumber]); } else { await tx.execute("UPDATE work_versions SET candidate_status='changes_requested', review_status='changes_requested' WHERE note_id=? AND version_number=?", [input.noteId, input.versionNumber]); await tx.execute("UPDATE review_rounds SET status='completed', completion_reason='changes_requested', completed_at=? WHERE id=?", [now, round.review_round_id]); await tx.execute("UPDATE notes SET title=?,description=?,tags=?,version_number=?,review_status='changes_requested' WHERE id=?", [round.title, round.description, round.tags, input.versionNumber, input.noteId]); } await tx.execute('INSERT INTO review_events (note_id,version_number,event_type,from_status,to_status,reason,actor_name,actor_role) VALUES (?,?,?,?,?,?,?,?)', [input.noteId, input.versionNumber, input.decision, round.review_status, input.decision, input.reason, input.actorName, input.actorRole]); if (input.reason) await tx.execute("INSERT INTO work_comments (note_id,version_number,content,author_name,author_role) VALUES (?,?,?,?,'client')", [input.noteId, input.versionNumber, input.reason, input.actorName]); await recalculateCollectionStatus(Number(round.collection_id), tx); await recalculateProjectReviewStatus(Number(round.project_id), tx); return { success: true as const, status: input.decision, version_number: input.versionNumber }; } export async function decideRound(input: RoundDecisionInput) { return withTransaction((tx) => decideRoundInTransaction(tx, input)); } export const decideCandidate = decideRound; export const decideCandidateInTransaction = decideRoundInTransaction;