/** * 评估向导草稿持久化(服务端,跨设备)。 * - source_assessment_id 为空:全新建评估的草稿 * - source_assessment_id 非空:编辑某既有评估时的草稿 * form 存完整向导快照(任意 JSON)。 */ import type pg from 'pg'; export interface DraftRecord { id: string; assessorId: string | null; sourceAssessmentId: string | null; projectName: string | null; form: unknown; updatedAt: string; } /** 列表项(不含 form 大字段,仅元信息)。 */ export interface DraftSummary { id: string; assessorId: string | null; sourceAssessmentId: string | null; projectName: string | null; updatedAt: string; } function isoOf(v: unknown): string { return v instanceof Date ? v.toISOString() : String(v); } /** JSONB 不能存 \u0000(空字符),而指标复合键用其作分隔符。存储时编码为私有区字符,读取时还原。 */ const NUL = '\u0000'; const NUL_SENTINEL = '\uE000'; function deepReplace(value: unknown, from: string, to: string): unknown { if (typeof value === 'string') return value.split(from).join(to); if (Array.isArray(value)) return value.map((v) => deepReplace(v, from, to)); if (value !== null && typeof value === 'object') { const out: Record = {}; for (const [k, v] of Object.entries(value as Record)) { out[k.split(from).join(to)] = deepReplace(v, from, to); } return out; } return value; } /** 存库前编码(去除 \u0000)。 */ function encodeForm(form: unknown): unknown { return deepReplace(form ?? null, NUL, NUL_SENTINEL); } /** 读出后还原。 */ function decodeForm(form: unknown): unknown { return deepReplace(form ?? null, NUL_SENTINEL, NUL); } /** 列出草稿(可按 assessorId 过滤),不返回 form 大字段。 */ export async function listDrafts(pool: pg.Pool, assessorId?: string): Promise { const where = assessorId ? 'WHERE assessor_id = $1' : ''; const params = assessorId ? [assessorId] : []; const res = await pool.query( `SELECT id, assessor_id, source_assessment_id, project_name, updated_at FROM wizard_drafts ${where} ORDER BY updated_at DESC`, params, ); return (res.rows as Array>).map((r) => ({ id: String(r.id), assessorId: r.assessor_id != null ? String(r.assessor_id) : null, sourceAssessmentId: r.source_assessment_id != null ? String(r.source_assessment_id) : null, projectName: r.project_name != null ? String(r.project_name) : null, updatedAt: isoOf(r.updated_at), })); } /** 取单个草稿(含 form)。 */ export async function getDraft(pool: pg.Pool, id: string): Promise { const res = await pool.query( `SELECT id, assessor_id, source_assessment_id, project_name, form, updated_at FROM wizard_drafts WHERE id = $1`, [id], ); const r = (res.rows as Array>)[0]; if (!r) return null; return { id: String(r.id), assessorId: r.assessor_id != null ? String(r.assessor_id) : null, sourceAssessmentId: r.source_assessment_id != null ? String(r.source_assessment_id) : null, projectName: r.project_name != null ? String(r.project_name) : null, form: decodeForm(r.form), updatedAt: isoOf(r.updated_at), }; } export interface UpsertDraftInput { id: string; assessorId?: string | null; sourceAssessmentId?: string | null; projectName?: string | null; form: unknown; } /** 新增或更新草稿(按 id)。 */ export async function upsertDraft(pool: pg.Pool, input: UpsertDraftInput): Promise { const res = await pool.query( `INSERT INTO wizard_drafts(id, assessor_id, source_assessment_id, project_name, form, updated_at) VALUES($1,$2,$3,$4,$5,now()) ON CONFLICT(id) DO UPDATE SET assessor_id = EXCLUDED.assessor_id, source_assessment_id = EXCLUDED.source_assessment_id, project_name = EXCLUDED.project_name, form = EXCLUDED.form, updated_at = now() RETURNING id, assessor_id, source_assessment_id, project_name, form, updated_at`, [ input.id, input.assessorId ?? null, input.sourceAssessmentId ?? null, input.projectName ?? null, JSON.stringify(encodeForm(input.form)), ], ); const r = (res.rows as Array>)[0]!; return { id: String(r.id), assessorId: r.assessor_id != null ? String(r.assessor_id) : null, sourceAssessmentId: r.source_assessment_id != null ? String(r.source_assessment_id) : null, projectName: r.project_name != null ? String(r.project_name) : null, form: decodeForm(r.form), updatedAt: isoOf(r.updated_at), }; } /** 删除草稿。 */ export async function deleteDraft(pool: pg.Pool, id: string): Promise { await pool.query('DELETE FROM wizard_drafts WHERE id = $1', [id]); }