198 lines
6.7 KiB
TypeScript
198 lines
6.7 KiB
TypeScript
/**
|
||
* 差异对账管理面(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();
|
||
}
|