/** * 差异对账管理面(TASK_3,挂独立路由 /admin/sync,不动应用设置页)。 * * 三条与直觉不同的口径,前端必须照抄后端语义、不自作聪明: * 1. **派发 2xx ≠ 对账完成**:任务列表里的 DISPATCHED 只代表"消费方收下了指令", * 差异有没有修、修了几条,只看 `latestResult`(由 result 回报推进); * 2. **COMPLETED 与 PARTIAL 是两种结论**:前者=完整扫描且无未决差异,后者=跑完了但还有 * 未决项(只报告的多余行、暂停的修复、重试耗尽的条目),不能并列成"成功"; * 3. **拒绝建任务也返回一行任务**(`state=FAILED` + `errorCode`/`errorDetail`), * 超限与范围不符是留痕事实,不是屏幕上闪过的一行字。 */ import { alovaInstance } from "@/utils/alova/alova-iview"; const BASE = "/api/form/admin/sync"; /** 对账任务行(与 FC FormSyncModels.TaskView 同构) */ export interface SyncTaskRow { taskId: string; consumerAppKey: string; consumerSchoolId: number; formUuid: string; formVersion: number; triggerType?: string; operator?: string; /** QUEUED / DISPATCHED / RUNNING / COMPLETED / PARTIAL / FAILED / CANCELLED */ state: string; /** PENDING / MATERIALIZING / COMPLETE / FAILED */ manifestState: string; manifestComplete?: boolean; totalRows?: number; totalPages?: number; materializedRows?: number; materializedPages?: number; attempt?: number; windowStart?: string; windowEnd?: string; dispatchedAt?: string; finishedAt?: string; cancelRequested?: boolean; errorCode?: string; errorDetail?: string; latestResult?: SyncResultRow; } /** 一次执行的差异与修复计数(一条 = 一个 attempt) */ export interface SyncResultRow { taskId: string; attempt: number; state: string; localScanComplete?: boolean; manifestComplete?: boolean; scannedRows?: number; pagesProcessed?: number; missingRepaired?: number; staleRepaired?: number; hashConflicts?: number; tombstonesRepaired?: number; localAhead?: number; deferred?: number; extraReported?: number; extraRetired?: number; errorCount?: number; errors?: SyncErrorRow[]; resumed?: boolean; reportedAt?: string; } /** 错误明细:每条都能定位 consumer / task / range / answer,永不携带答卷值 */ export interface SyncErrorRow { code: string; taskId?: string; consumerAppKey?: string; consumerSchoolId?: number; formUuid?: string; answerUuid?: string; revision?: number; detail?: string; } /** Spring Page 的极简形态(列表只取这三项) */ export interface SyncTaskPage { content: SyncTaskRow[]; totalElements: number; totalPages: number; } /** 投递失败/死信行(TASK_2 failedPage 的转发,不代表对账结论) */ export interface FailedDeliveryRow { eventId: string; answerUuid: string; answerRevision: number; consumerAppKey: string; consumerSchoolId: number; state: string; retryCount?: number; lastError?: string; nextRetryAt?: string; brokerAcceptedAt?: string; consumerAppliedAt?: string; } export interface FailedDeliveryPage { total: number; rows: FailedDeliveryRow[]; } /** 重放结论(稳定码:QUEUED / ALREADY_DELIVERED / GRANT_REVOKED / REVISION_MISMATCH / NO_DELIVERY_ROW) */ export interface ReplayResult { state: string; eventId?: string; answerRevision?: number; message?: string; } function withSch(params: Record, schId?: number) { if (schId !== undefined && schId !== null) params.schId = schId; return params; } /** 建任务:scope=ANSWER / FORM / ALL(ALL 按生效订阅裂变成多行,受后端上限约束) */ export function createSyncTasks(body: { scope: string; consumerAppKey: string; consumerSchoolId: number; formUuid?: string; answerUuid?: string; idempotencyKey?: string; }, schId?: number) { const params = withSch({}, schId); return alovaInstance.Post(BASE + "/tasks", body, { params }).send(); } /** 任务列表(状态与进度) */ export function listSyncTasks(query: { consumerAppKey?: string; consumerSchoolId?: number; state?: string; page?: number; size?: number; }, schId?: number) { const params: Record = {}; if (query.consumerAppKey) params.consumerAppKey = query.consumerAppKey; if (query.consumerSchoolId !== undefined && query.consumerSchoolId !== null) { params.consumerSchoolId = query.consumerSchoolId; } if (query.state) params.state = query.state; params.page = query.page || 1; params.size = query.size || 20; return alovaInstance.Get(BASE + "/tasks", { params: withSch(params, schId) }).send(); } /** 任务详情(含最近一次回报摘要) */ export function getSyncTask(taskId: string, schId?: number) { return alovaInstance.Get(`${BASE}/tasks/${encodeURIComponent(taskId)}`, { params: withSch({}, schId) }).send(); } /** 逐 attempt 的结果与失败明细 */ export function listSyncResults(taskId: string, schId?: number) { return alovaInstance.Get(`${BASE}/tasks/${encodeURIComponent(taskId)}/results`, { params: withSch({}, schId) }).send(); } /** 取消任务:剩余投递与修复一律停(幂等) */ export function cancelSyncTask(taskId: string, schId?: number) { return alovaInstance.Post(`${BASE}/tasks/${encodeURIComponent(taskId)}/cancel`, undefined, { params: withSch({}, schId) }).send(); } /** 单答卷重放(先重新过 ACL 再执行;旧 outbox 日志不是新答卷,历史版本内容不可重建) */ export function replaySyncAnswer(body: { consumerAppKey: string; consumerSchoolId: number; answerUuid: string; expectedRevision: number; operationId: string; }, schId?: number) { const params = withSch({}, schId); return alovaInstance.Post(BASE + "/replay", body, { params }).send(); } /** 投递失败/死信分页(TASK_2 的投递面查看,与对账结论互不作证) */ export function listFailedDeliveries(query: { consumerAppKey: string; consumerSchoolId: number; page?: number; size?: number; }, schId?: number) { const params: Record = { consumerAppKey: query.consumerAppKey, consumerSchoolId: query.consumerSchoolId, page: query.page || 1, size: query.size || 20, }; return alovaInstance.Get(BASE + "/deliveries/failed", { params: withSch(params, schId) }).send(); }