t1~t3+精修: 授权面板/对账同步管理页/填报三通道;Fill.vue 幂等键失败重试复用(切坐标才重置)
build / build (push) Successful in 2m7s

This commit is contained in:
chenxing
2026-10-22 10:03:01 +08:00
parent 2661a4d4d3
commit 61398f354e
10 changed files with 1262 additions and 12 deletions
+197
View File
@@ -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();
}