Files
form-center-front/src/api/module/formSync.ts
T

198 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 差异对账管理面(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<string, string | number>, 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<SyncTaskRow[]>(BASE + "/tasks", body, { params }).send();
}
/** 任务列表(状态与进度) */
export function listSyncTasks(query: {
consumerAppKey?: string;
consumerSchoolId?: number;
state?: string;
page?: number;
size?: number;
}, schId?: number) {
const params: Record<string, string | number> = {};
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<SyncTaskPage>(BASE + "/tasks", { params: withSch(params, schId) }).send();
}
/** 任务详情(含最近一次回报摘要) */
export function getSyncTask(taskId: string, schId?: number) {
return alovaInstance.Get<SyncTaskRow>(`${BASE}/tasks/${encodeURIComponent(taskId)}`,
{ params: withSch({}, schId) }).send();
}
/** 逐 attempt 的结果与失败明细 */
export function listSyncResults(taskId: string, schId?: number) {
return alovaInstance.Get<SyncResultRow[]>(`${BASE}/tasks/${encodeURIComponent(taskId)}/results`,
{ params: withSch({}, schId) }).send();
}
/** 取消任务:剩余投递与修复一律停(幂等) */
export function cancelSyncTask(taskId: string, schId?: number) {
return alovaInstance.Post<SyncTaskRow>(`${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<ReplayResult>(BASE + "/replay", body, { params }).send();
}
/** 投递失败/死信分页(TASK_2 的投递面查看,与对账结论互不作证) */
export function listFailedDeliveries(query: {
consumerAppKey: string;
consumerSchoolId: number;
page?: number;
size?: number;
}, schId?: number) {
const params: Record<string, string | number> = {
consumerAppKey: query.consumerAppKey,
consumerSchoolId: query.consumerSchoolId,
page: query.page || 1,
size: query.size || 20,
};
return alovaInstance.Get<FailedDeliveryPage>(BASE + "/deliveries/failed",
{ params: withSch(params, schId) }).send();
}