diff --git a/build_ui/src/views/train/auth/authFeedback.js b/build_ui/src/views/train/auth/authFeedback.js new file mode 100644 index 000000000..461b362ff --- /dev/null +++ b/build_ui/src/views/train/auth/authFeedback.js @@ -0,0 +1,188 @@ +/** + * 门禁凭据与对账交互端点前端结果语义解析器 (Package A.3 - MAJ-05) + * + * 核心原则: + * 1. 杜绝依赖 AjaxResult 顶层 res.msg (防止“操作成功”假成功); + * 2. 严格按后端 DTO 真实契约解析 (ReconciliationResult, IssueResultVO); + * 3. 严禁对排队、未执行、部分失败或人工对账展示“成功”。 + */ + +/** + * 解析门禁注销操作结果反馈 (POST /train/access/auth/{authId}/revoke) + * @param {Object} data - res.data (ReconciliationResult) + * @returns {{ type: 'success'|'info'|'warning'|'error', message: string }} + */ +export function resolveRevokeFeedback(data) { + const d = data || {} + const decision = d.decision || '' + const success = d.success === true + const externalInvoked = d.externalInvoked === true + const reason = d.reason || '' + + // 1. decision == NO_ACTION && success == true -> 无需操作或已处于安全终态 (info/success) + if (decision === 'NO_ACTION' && success) { + return { + type: 'info', + message: reason || '无需操作或已处于安全终态' + } + } + + // 2. decision == REVOKE_REQUIRED && success == true && externalInvoked == true + if (decision === 'REVOKE_REQUIRED' && success && externalInvoked) { + return { + type: 'info', + message: '注销请求已提交,等待设备确认。' + } + } + + // 3. decision == REVOKE_REQUIRED && success == true && externalInvoked == false + if (decision === 'REVOKE_REQUIRED' && success && !externalInvoked) { + return { + type: 'warning', + message: reason || '当前未执行外部注销,等待DMS恢复或后续处理。' + } + } + + // 4. decision == FAILED || success == false -> error 直接展示 reason + if (decision === 'FAILED' || !success) { + return { + type: 'error', + message: reason || '门禁注销失败' + } + } + + // 安全兜底 + return { + type: 'info', + message: reason || '注销请求已提交' + } +} + +/** + * 解析门禁凭据显式下发结果反馈 (POST /train/access/auth/{authId}/issue) + * @param {Object} data - res.data (IssueResultVO) + * @returns {{ type: 'success'|'warning'|'error', message: string, openCredentialDialog: boolean }} + */ +export function resolveIssueFeedback(data) { + const d = data || {} + const status = d.status || '' + const message = d.message || '' + const hasCredential = Boolean(d.oneTimePlaintextCredential && String(d.oneTimePlaintextCredential).trim().length > 0) + + // 1. ACCEPTED: 仅在有 credential 时才允许弹窗;提示“凭据已提交下发,设备确认后生效。” + if (status === 'ACCEPTED') { + return { + type: 'success', + message: '凭据已提交下发,设备确认后生效。', + openCredentialDialog: hasCredential + } + } + + // 2. WAITING_FOR_PREREQUISITE (包含 MISSING_STUDENT_PHONE, INVALID_VALIDITY_WINDOW, DMS_DISABLED 等真实 reason) + if (status === 'WAITING_FOR_PREREQUISITE') { + return { + type: 'warning', + message: message || '前置条件未满足,暂无法下发门禁凭据', + openCredentialDialog: false + } + } + + // 3. AMBIGUOUS_EXTERNAL_MUTATION: 不得允许直接重发 + if (status === 'AMBIGUOUS_EXTERNAL_MUTATION' || status === 'AMBIGUOUS') { + return { + type: 'error', + message: message || '上一次下发结果无法确认,禁止重复下发,请先核对厂商侧凭据。(AMBIGUOUS_EXTERNAL_MUTATION)', + openCredentialDialog: false + } + } + + // 4. FAILED 或其他错误 + return { + type: 'error', + message: message || '门禁凭据下发失败', + openCredentialDialog: false + } +} + +/** + * 解析门禁手动对账结果反馈 (POST /train/access/reconcile/{enrollmentId}) + * @param {Object} data - res.data (ReconciliationResult) + * @returns {{ type: 'success'|'info'|'warning'|'error', message: string, openCredentialDialog: boolean }} + */ +export function resolveReconcileFeedback(data) { + const d = data || {} + const batchResult = d.batchResult || null + const outcome = (batchResult && batchResult.outcome) || d.outcome || (d.decision === 'NO_ACTION' ? 'ALL_SETTLED' : '') + const reason = d.reason || '' + const hasCredential = Boolean(d.oneTimePlaintextCredential && String(d.oneTimePlaintextCredential).trim().length > 0) + + // 1. ALL_SETTLED -> success + if (outcome === 'ALL_SETTLED') { + return { + type: 'success', + message: reason || '对账完成:所有授权均已处于安全终态或无需操作', + openCredentialDialog: hasCredential + } + } + + // 2. IN_PROGRESS -> info + if (outcome === 'IN_PROGRESS') { + return { + type: 'info', + message: reason || '对账处理中:指令已派发至外部设备,等待设备确认', + openCredentialDialog: hasCredential + } + } + + // 3. WAITING_FOR_DMS -> warning + if (outcome === 'WAITING_FOR_DMS') { + return { + type: 'warning', + message: reason || '门禁系统当前未启用,操作已排队等待DMS恢复后处理', + openCredentialDialog: hasCredential + } + } + + // 4. PARTIAL_FAILURE -> error (明确“部分处理失败”,严禁报对账成功) + if (outcome === 'PARTIAL_FAILURE') { + return { + type: 'error', + message: reason ? ('部分处理失败:' + reason) : '对账完成:部分授权处理失败,需关注处理', + openCredentialDialog: false + } + } + + // 5. ALL_FAILED -> error + if (outcome === 'ALL_FAILED') { + return { + type: 'error', + message: reason ? ('对账处理失败:' + reason) : '对账执行失败:所有授权操作均失败', + openCredentialDialog: false + } + } + + // 6. REQUIRES_MANUAL_SETTLEMENT -> error (明确“需要人工核对厂商侧状态”) + if (outcome === 'REQUIRES_MANUAL_SETTLEMENT') { + return { + type: 'error', + message: reason ? ('需要人工核对厂商侧状态:' + reason) : '需要人工核对厂商侧状态:外部凭据状态不确定,阻断自动处理', + openCredentialDialog: false + } + } + + // 7. 兜底逻辑:若明确 success == false + if (d.success === false) { + return { + type: 'error', + message: reason || '对账执行未成功', + openCredentialDialog: false + } + } + + // 8. 默认中性提示 + return { + type: 'info', + message: reason || '对账执行完成', + openCredentialDialog: hasCredential + } +} diff --git a/build_ui/src/views/train/auth/index.vue b/build_ui/src/views/train/auth/index.vue index 85b78ec70..47c39ef49 100644 --- a/build_ui/src/views/train/auth/index.vue +++ b/build_ui/src/views/train/auth/index.vue @@ -301,6 +301,7 @@