This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* 差异对账管理面(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();
|
||||
}
|
||||
Reference in New Issue
Block a user