Files
delivery-desk/api/services/reviewService.ts

65 lines
4.9 KiB
TypeScript
Raw Permalink Normal View History

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;