This commit is contained in:
50
openspec/changes/add-refund-approval-recovery/design.md
Normal file
50
openspec/changes/add-refund-approval-recovery/design.md
Normal file
@@ -0,0 +1,50 @@
|
||||
## Context
|
||||
|
||||
退款审批提交由后端和企业微信协同完成。网络超时或异步任务异常时,本地不能仅凭旧字段判断提交结果:已有 `sp_no` 需要同步,结果未知需要先确认,明确失败才允许恢复原提交事件。恢复必须绑定当前最新审批尝试,避免重复创建审批实例或覆盖历史材料。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
- Goals: 为可恢复的退款提供一个安全、可追踪的恢复入口;展示提交状态和最近恢复结果;在列表和详情保持一致。
|
||||
- Goals: 将资格判断、数据范围和并发冲突交给后端,前端只消费后端返回的可恢复标记。
|
||||
- Non-Goals: 不实现审批引擎、企业微信调用、自动重试调度或新的审批流程。
|
||||
|
||||
## Decisions
|
||||
|
||||
- Decision: 新增独立的 `recoverApproval` API,而不是复用历史 `triggerApproval`。
|
||||
- Rationale: `trigger-approval` 面向没有审批状态的历史记录;恢复接口面向已有审批尝试的同步/结果确认/原事件恢复,两者的幂等语义和审计含义不同。
|
||||
- Decision: 以 `approval_recoverable` 作为按钮展示条件,后端作为最终授权和状态校验方。
|
||||
- Rationale: 前端无法可靠重建“结果未知”和“明确失败”等状态,且状态可能在页面打开后发生变化。
|
||||
- Decision: 恢复接口只接收路径中的退款 ID,响应返回完整退款详情。
|
||||
- Rationale: 防止客户端篡改审批实例、材料或单号;一次响应即可刷新列表/详情所需字段。
|
||||
- Decision: 将 `submission_status`、失败摘要和最近恢复时间作为普通退款响应字段展示。
|
||||
- Rationale: 列表需要快速筛选/判断,详情需要审计和排障;字段由后端脱敏并保持一致。
|
||||
|
||||
## State and Concurrency
|
||||
|
||||
1. 若最新尝试已有 `sp_no`,后端执行原审批单同步。
|
||||
2. 若提交结果未知,后端先确认结果;确认成功或已存在时不得再次提交。
|
||||
3. 若结果明确提交失败,后端恢复原提交事件;不得创建新的尝试或审批实例。
|
||||
4. 若资格在请求期间失效,接口返回状态冲突;前端提示固定文案并刷新详情/列表。
|
||||
5. 后端应保证重复请求幂等,前端按钮在请求期间置为 loading,避免重复点击。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- Risk: 列表数据在恢复完成前已过期。
|
||||
- Mitigation: 成功和状态冲突都强制重新请求后端;不在前端乐观修改审批状态。
|
||||
- Risk: 失败摘要包含凭证或个人信息。
|
||||
- Mitigation: 仅展示后端提供的脱敏 `approval_failure_summary`,前端不拼接原始异常。
|
||||
- Risk: 新入口与旧历史补发入口混淆。
|
||||
- Mitigation: 使用“恢复审批”文案和独立 API,保留两种入口各自的资格条件与说明。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 扩展退款响应类型与 API service。
|
||||
2. 在列表和详情接入恢复按钮、确认/加载/错误处理和刷新逻辑。
|
||||
3. 增加 API、资格显示、冲突刷新和字段展示测试。
|
||||
4. 与后端联调已有单号、未知结果、明确失败和不可恢复四类状态。
|
||||
|
||||
## Open Questions
|
||||
|
||||
- `submission_status` 的完整枚举值及名称由后端提供,前端是否需要筛选取决于最终列表需求。
|
||||
- 状态冲突错误是否有稳定错误码;在未确定前以前端按错误消息或统一冲突码兼容处理。
|
||||
|
||||
37
openspec/changes/add-refund-approval-recovery/proposal.md
Normal file
37
openspec/changes/add-refund-approval-recovery/proposal.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Change: 增加退款原审批恢复
|
||||
|
||||
## Why
|
||||
|
||||
退款审批提交可能出现企业微信单号已生成但本地结果未知、原提交事件明确失败,或需要重新同步已有审批单的情况。当前退款页面只有历史记录的“补发审批”能力,无法安全地恢复同一审批尝试;运营人员也缺少提交状态、失败摘要和最近恢复时间,难以判断是否可以处理。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增 `POST /api/admin/refunds/{id}/recover-approval`,无请求体,调用后恢复原审批提交事件或确认未知结果。
|
||||
- 新增 `RefundService.recoverApproval(id)`,成功响应解析为更新后的退款详情。
|
||||
- 扩展退款列表和详情模型,支持 `submission_status`、`submission_status_name`、`approval_recoverable`、`approval_failure_summary`、`approval_last_recovery_at`。
|
||||
- 在退款列表和详情中,仅当后端返回 `approval_recoverable = true` 且当前账号具备退款管理权限及数据范围时显示“恢复审批”。
|
||||
- 恢复成功后刷新当前列表/详情;遇到状态冲突时提示“当前退款审批状态不允许恢复”并重新加载后端状态。
|
||||
- 恢复动作沿用退款管理权限和数据范围,不新增审批实例、审批尝试、审批材料、审批实例 ID 或企业微信单号输入,也不替代现有历史审批“补发”能力。
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs:
|
||||
- `refund-management`
|
||||
- Affected code:
|
||||
- `src/api/modules/refund.ts`
|
||||
- `src/types/api/refund.ts`
|
||||
- `src/views/finance/refund/index.vue`
|
||||
- `src/views/finance/refund/detail.vue`
|
||||
- API contract:
|
||||
- `POST /api/admin/refunds/{id}/recover-approval`
|
||||
- `GET /api/admin/refunds`
|
||||
- `GET /api/admin/refunds/{id}`
|
||||
- Dependencies:
|
||||
- 后端返回 `approval_recoverable` 的最终资格判断,并在服务端保证幂等、并发状态校验和数据范围校验。
|
||||
- 后端成功响应包含完整退款详情,失败响应遵循现有统一错误结构。
|
||||
- Out of scope:
|
||||
- 创建新的审批尝试或审批实例。
|
||||
- 前端直接调用企业微信、传入审批实例 ID、审批材料或 `sp_no`。
|
||||
- 修改退款金额、退款方式、审批结果或渠道退款流程。
|
||||
- 批量恢复、自动轮询恢复或替代 `trigger-approval` 历史审批补发。
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Refund Approval Recovery API
|
||||
|
||||
The refund service SHALL expose `POST /api/admin/refunds/{id}/recover-approval` with no request body. The operation SHALL use the existing refund-management permission and data scope, and SHALL return the updated complete refund record on success.
|
||||
|
||||
#### Scenario: Recover an existing approval submission
|
||||
|
||||
- **GIVEN** a refund record is marked `approval_recoverable = true`
|
||||
- **WHEN** the client posts to `/api/admin/refunds/{id}/recover-approval`
|
||||
- **THEN** the request MUST contain only the refund ID path parameter and no body
|
||||
- **AND** a successful response MUST contain the updated refund detail, including the latest submission and approval fields
|
||||
|
||||
#### Scenario: Reject unauthorized or out-of-scope recovery
|
||||
|
||||
- **GIVEN** the current account lacks refund-management permission or data scope for the refund
|
||||
- **WHEN** the client calls the recovery endpoint
|
||||
- **THEN** the service MUST reject the request using the existing authentication/authorization error contract
|
||||
|
||||
### Requirement: Recovery Preserves the Original Approval Attempt
|
||||
|
||||
The recovery operation SHALL synchronize an existing approval identified by `sp_no`, confirm an unknown submission result before retrying, or restore the original submission event after an explicit submission failure. It MUST NOT create a new approval attempt or approval instance.
|
||||
|
||||
#### Scenario: Existing WeCom approval number
|
||||
|
||||
- **GIVEN** the latest approval attempt already has an `sp_no`
|
||||
- **WHEN** recovery is requested
|
||||
- **THEN** the service MUST trigger synchronization of that original approval
|
||||
- **AND** MUST preserve the existing approval attempt and instance
|
||||
|
||||
#### Scenario: Unknown submission result
|
||||
|
||||
- **GIVEN** the submission result is unknown
|
||||
- **WHEN** recovery is requested
|
||||
- **THEN** the service MUST confirm the submission result before any resubmission
|
||||
- **AND** MUST NOT create a duplicate approval when the original submission is found
|
||||
|
||||
#### Scenario: Explicit submission failure
|
||||
|
||||
- **GIVEN** the original submission is explicitly known to have failed
|
||||
- **WHEN** recovery is requested
|
||||
- **THEN** the service MUST restore the original submission event
|
||||
- **AND** MUST NOT create a new approval attempt or approval instance
|
||||
|
||||
### Requirement: Refund Approval Recovery Fields
|
||||
|
||||
Refund list and detail responses SHALL expose `submission_status`, `submission_status_name`, `approval_recoverable`, `approval_failure_summary`, and `approval_last_recovery_at`. The failure summary MUST be a redacted diagnostic value supplied by the backend.
|
||||
|
||||
#### Scenario: Display recovery metadata
|
||||
|
||||
- **GIVEN** a refund response contains recovery metadata
|
||||
- **WHEN** the list or detail page renders the record
|
||||
- **THEN** it MUST display the submission status name, redacted failure summary when present, and the latest recovery time
|
||||
- **AND** it MUST use `approval_recoverable` from the response without deriving eligibility from stale local fields
|
||||
|
||||
### Requirement: Refund Approval Recovery Entry
|
||||
|
||||
The refund list and detail pages SHALL show a “恢复审批” action only when the backend returns `approval_recoverable = true` and the existing refund-management permission and data scope allow access.
|
||||
|
||||
#### Scenario: Show recovery action for an eligible record
|
||||
|
||||
- **GIVEN** `approval_recoverable` is `true`
|
||||
- **AND** the current account can access the refund under existing permission and data scope
|
||||
- **WHEN** the page renders the record
|
||||
- **THEN** the page MUST show the “恢复审批” action
|
||||
|
||||
#### Scenario: Hide recovery action for an ineligible record
|
||||
|
||||
- **GIVEN** `approval_recoverable` is `false`, absent, or the account cannot access the refund
|
||||
- **WHEN** the page renders the record
|
||||
- **THEN** the page MUST NOT show the “恢复审批” action
|
||||
|
||||
#### Scenario: Recovery succeeds
|
||||
|
||||
- **GIVEN** the user confirms recovery for an eligible record
|
||||
- **WHEN** the recovery endpoint succeeds
|
||||
- **THEN** the page MUST show a success message and reload the list or detail from the backend
|
||||
- **AND** the refreshed record MUST show the latest submission and approval state
|
||||
|
||||
#### Scenario: Recovery conflicts with a concurrent state change
|
||||
|
||||
- **GIVEN** the record becomes non-recoverable before the request is processed
|
||||
- **WHEN** the endpoint returns a status conflict
|
||||
- **THEN** the page MUST show “当前退款审批状态不允许恢复”
|
||||
- **AND** MUST reload the current record instead of applying an optimistic state
|
||||
|
||||
22
openspec/changes/add-refund-approval-recovery/tasks.md
Normal file
22
openspec/changes/add-refund-approval-recovery/tasks.md
Normal file
@@ -0,0 +1,22 @@
|
||||
## 1. Contract And Types
|
||||
|
||||
- [ ] 1.1 在 `Refund` 列表/详情类型中增加 `submission_status`、`submission_status_name`、`approval_recoverable`、`approval_failure_summary`、`approval_last_recovery_at`。
|
||||
- [ ] 1.2 在 `RefundService` 增加 `recoverApproval(id)`,调用无请求体的 `POST /api/admin/refunds/{id}/recover-approval` 并返回完整 `Refund`。
|
||||
- [ ] 1.3 补充 API 注释和统一错误响应处理,明确不传审批实例 ID、材料或企业微信单号。
|
||||
|
||||
## 2. Refund List And Detail
|
||||
|
||||
- [ ] 2.1 列表展示提交状态名称、脱敏失败摘要和最近恢复时间(按页面现有字段展示约定接入)。
|
||||
- [ ] 2.2 仅在 `approval_recoverable === true` 且通过现有退款管理权限/数据范围校验时显示“恢复审批”操作。
|
||||
- [ ] 2.3 恢复操作增加确认和请求中 loading,成功后刷新列表并展示最新响应数据。
|
||||
- [ ] 2.4 详情页展示上述字段和“恢复审批”操作;成功后刷新详情。
|
||||
- [ ] 2.5 处理状态冲突:提示“当前退款审批状态不允许恢复”,并刷新当前数据;其他后端错误沿用统一错误提示。
|
||||
|
||||
## 3. Verification
|
||||
|
||||
- [ ] 3.1 测试已有 `sp_no`、提交结果未知、明确提交失败和不可恢复记录的入口展示与刷新行为。
|
||||
- [ ] 3.2 测试无退款权限、越权数据和 `approval_recoverable = false` 时不显示按钮。
|
||||
- [ ] 3.3 测试重复点击被 loading 阻止,且请求体为空、不携带审批材料或实例 ID。
|
||||
- [ ] 3.4 运行相关单元测试、类型检查、lint 和构建。
|
||||
- [ ] 3.5 运行 `openspec.cmd validate add-refund-approval-recovery --strict`。
|
||||
|
||||
@@ -83,4 +83,15 @@ export class RefundService extends BaseService {
|
||||
static triggerApproval(id: number): Promise<BaseResponse<Refund>> {
|
||||
return this.post<BaseResponse<Refund>>(`/api/admin/refunds/${id}/trigger-approval`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复退款原审批提交
|
||||
*
|
||||
* 后端负责同步已有审批单、确认未知提交结果或恢复明确失败的原提交事件。
|
||||
* 此操作不接收审批实例 ID、审批材料或企业微信单号,也不会创建新的审批尝试。
|
||||
* @param id 退款申请ID
|
||||
*/
|
||||
static recoverApproval(id: number): Promise<BaseResponse<Refund>> {
|
||||
return this.post<BaseResponse<Refund>>(`/api/admin/refunds/${id}/recover-approval`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +140,11 @@ export interface Refund {
|
||||
approval_source?: 'none' | 'legacy' | 'wecom' | null // 审批来源
|
||||
approval_status?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | null // 审批状态
|
||||
approval_status_name?: string | null // 审批状态名称
|
||||
submission_status?: number | null // 企业微信审批提交状态
|
||||
submission_status_name?: string | null // 企业微信审批提交状态名称
|
||||
approval_recoverable?: boolean // 是否允许恢复原审批提交
|
||||
approval_failure_summary?: string | null // 脱敏审批失败摘要
|
||||
approval_last_recovery_at?: string | null // 最近一次审批恢复时间
|
||||
current_approver_summary?: string | null // 当前审批人摘要
|
||||
processing_status?: string | null // 业务处理状态
|
||||
processing_status_name?: string | null // 业务处理状态名称
|
||||
@@ -210,4 +215,4 @@ export interface RejectRefundRequest {
|
||||
// 退回退款申请请求
|
||||
export interface ReturnRefundRequest {
|
||||
remark?: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,14 @@
|
||||
>
|
||||
重新申请
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="refund && canRecoverApproval(refund) && hasAuth('refund:detail')"
|
||||
type="warning"
|
||||
:loading="recoverApprovalLoading"
|
||||
@click="handleRecoverApproval"
|
||||
>
|
||||
恢复审批
|
||||
</ElButton>
|
||||
<template v-if="refund && canManualApproveRefund(refund)">
|
||||
<ElButton
|
||||
v-if="hasAuth('refund:approve')"
|
||||
@@ -173,7 +181,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed, h, reactive } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElButton, ElCard, ElIcon, ElMessage } from 'element-plus'
|
||||
import { ElButton, ElCard, ElIcon, ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
||||
import DetailPage from '@/components/common/DetailPage.vue'
|
||||
@@ -208,6 +216,7 @@
|
||||
const isRestrictedCustomerRole = computed(() => [3, 4].includes(Number(userStore.info.user_type)))
|
||||
|
||||
const loading = ref(false)
|
||||
const recoverApprovalLoading = ref(false)
|
||||
const refund = ref<Refund | null>(null)
|
||||
const refundVoucherFileKeys = ref<string[]>([])
|
||||
const approvalDialogVisible = ref(false)
|
||||
@@ -572,6 +581,20 @@
|
||||
label: '审批状态',
|
||||
formatter: (_, data) => getRefundApprovalStatusText(data)
|
||||
},
|
||||
{
|
||||
label: '审批提交状态',
|
||||
formatter: (_, data) => data.submission_status_name || '-'
|
||||
},
|
||||
{
|
||||
label: '审批失败摘要',
|
||||
formatter: (_, data) => data.approval_failure_summary || '-',
|
||||
fullWidth: true
|
||||
},
|
||||
{
|
||||
label: '最近恢复时间',
|
||||
formatter: (_, data) =>
|
||||
data.approval_last_recovery_at ? formatDateTime(data.approval_last_recovery_at) : '-'
|
||||
},
|
||||
...(!isRestrictedCustomerRole.value
|
||||
? [
|
||||
{
|
||||
@@ -707,6 +730,72 @@
|
||||
return canResubmitRefund(item)
|
||||
}
|
||||
|
||||
const canRecoverApproval = (item: Refund) => item.approval_recoverable === true
|
||||
|
||||
const isApprovalRecoveryConflict = (error: unknown, message = '') => {
|
||||
if (!error || typeof error !== 'object') {
|
||||
return message.includes('状态不允许恢复') || message.includes('不可恢复')
|
||||
}
|
||||
|
||||
const candidate = error as {
|
||||
code?: unknown
|
||||
response?: { status?: unknown; data?: { code?: unknown } }
|
||||
}
|
||||
const code = candidate.response?.data?.code ?? candidate.code
|
||||
const normalizedMessage = message || getErrorMessage(error, '')
|
||||
return (
|
||||
candidate.response?.status === 409 ||
|
||||
code === 'APPROVAL_RECOVERY_CONFLICT' ||
|
||||
code === 'REFUND_APPROVAL_NOT_RECOVERABLE' ||
|
||||
normalizedMessage.includes('状态不允许恢复') ||
|
||||
normalizedMessage.includes('不可恢复')
|
||||
)
|
||||
}
|
||||
|
||||
const handleRecoverApproval = async () => {
|
||||
if (!refund.value || !canRecoverApproval(refund.value) || recoverApprovalLoading.value) return
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要恢复退款单号 ${refund.value.refund_no} 的原审批提交吗?`,
|
||||
'恢复审批',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
recoverApprovalLoading.value = true
|
||||
try {
|
||||
const res = await RefundService.recoverApproval(refund.value.id)
|
||||
if (res.code !== 0) {
|
||||
if (isApprovalRecoveryConflict(res, res.msg || '')) {
|
||||
ElMessage.error('当前退款审批状态不允许恢复')
|
||||
await fetchRefundDetail(refund.value.id)
|
||||
return
|
||||
}
|
||||
ElMessage.error(res.msg || '恢复审批失败')
|
||||
return
|
||||
}
|
||||
ElMessage.success('恢复审批成功')
|
||||
await fetchRefundDetail(refund.value.id)
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error, '恢复审批失败')
|
||||
if (isApprovalRecoveryConflict(error, message)) {
|
||||
ElMessage.error('当前退款审批状态不允许恢复')
|
||||
await fetchRefundDetail(refund.value.id)
|
||||
} else {
|
||||
ElMessage.error(message)
|
||||
}
|
||||
} finally {
|
||||
recoverApprovalLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleShowResubmit = () => {
|
||||
if (!refund.value || !canResubmit(refund.value)) return
|
||||
resubmitForm.requested_refund_amount =
|
||||
|
||||
@@ -216,7 +216,7 @@
|
||||
getCurrentApproverSummaryText,
|
||||
getProcessingStatusText
|
||||
} from '@/utils/business/approvalSummary'
|
||||
import { toVoucherKeyList } from '@/utils/business'
|
||||
import { getErrorMessage, toVoucherKeyList } from '@/utils/business'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { AUDIT_PERMISSIONS } from '@/config/constants/audit'
|
||||
import {
|
||||
@@ -255,6 +255,7 @@
|
||||
const resubmitLoading = ref(false)
|
||||
const resubmitVoucherUploading = ref(false)
|
||||
const triggerApprovalLoading = ref(false)
|
||||
const recoverApprovalLoading = ref(false)
|
||||
const tableRef = ref()
|
||||
const resubmitUploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||
const createDialogVisible = ref(false)
|
||||
@@ -380,6 +381,9 @@
|
||||
{ label: '审批渠道', prop: 'approval_provider' },
|
||||
{ label: '提交人', prop: 'submitter_name' },
|
||||
{ label: '审批状态', prop: 'approval_status' },
|
||||
{ label: '审批提交状态', prop: 'submission_status_name' },
|
||||
{ label: '审批失败摘要', prop: 'approval_failure_summary' },
|
||||
{ label: '最近恢复时间', prop: 'approval_last_recovery_at' },
|
||||
{ label: '当前审批人摘要', prop: 'current_approver_summary' },
|
||||
{ label: '业务处理状态', prop: 'processing_status_name' },
|
||||
{ label: '退款原因', prop: 'refund_reason' },
|
||||
@@ -628,6 +632,27 @@
|
||||
width: 120,
|
||||
formatter: (row: Refund) => row.approval_status_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'submission_status_name',
|
||||
label: '审批提交状态',
|
||||
width: 140,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: Refund) => row.submission_status_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'approval_failure_summary',
|
||||
label: '审批失败摘要',
|
||||
minWidth: 200,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: Refund) => row.approval_failure_summary || '-'
|
||||
},
|
||||
{
|
||||
prop: 'approval_last_recovery_at',
|
||||
label: '最近恢复时间',
|
||||
width: 180,
|
||||
formatter: (row: Refund) =>
|
||||
row.approval_last_recovery_at ? formatDateTime(row.approval_last_recovery_at) : '-'
|
||||
},
|
||||
{
|
||||
prop: 'current_approver_summary',
|
||||
label: '当前审批人摘要',
|
||||
@@ -926,6 +951,28 @@
|
||||
return row.status === RefundStatus.PENDING && approvalStatusEmpty
|
||||
}
|
||||
|
||||
const canRecoverApproval = (row: Refund) => row.approval_recoverable === true
|
||||
|
||||
const isApprovalRecoveryConflict = (error: unknown, message = '') => {
|
||||
if (!error || typeof error !== 'object') {
|
||||
return message.includes('状态不允许恢复') || message.includes('不可恢复')
|
||||
}
|
||||
|
||||
const candidate = error as {
|
||||
code?: unknown
|
||||
response?: { status?: unknown; data?: { code?: unknown; msg?: unknown } }
|
||||
}
|
||||
const code = candidate.response?.data?.code ?? candidate.code
|
||||
const normalizedMessage = message || getErrorMessage(error, '')
|
||||
return (
|
||||
candidate.response?.status === 409 ||
|
||||
code === 'APPROVAL_RECOVERY_CONFLICT' ||
|
||||
code === 'REFUND_APPROVAL_NOT_RECOVERABLE' ||
|
||||
normalizedMessage.includes('状态不允许恢复') ||
|
||||
normalizedMessage.includes('不可恢复')
|
||||
)
|
||||
}
|
||||
|
||||
// 补发历史退款审批
|
||||
const handleTriggerApproval = (row: Refund) => {
|
||||
if (triggerApprovalLoading.value) return
|
||||
@@ -957,6 +1004,47 @@
|
||||
})
|
||||
}
|
||||
|
||||
// 恢复退款原审批提交
|
||||
const handleRecoverApproval = (row: Refund) => {
|
||||
if (recoverApprovalLoading.value || !canRecoverApproval(row)) return
|
||||
|
||||
ElMessageBox.confirm(`确定要恢复退款单号 ${row.refund_no} 的原审批提交吗?`, '恢复审批', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
recoverApprovalLoading.value = true
|
||||
try {
|
||||
const res = await RefundService.recoverApproval(row.id)
|
||||
if (res.code !== 0) {
|
||||
if (isApprovalRecoveryConflict(res, res.msg || '')) {
|
||||
ElMessage.error('当前退款审批状态不允许恢复')
|
||||
await getTableData()
|
||||
return
|
||||
}
|
||||
ElMessage.error(res.msg || '恢复审批失败')
|
||||
return
|
||||
}
|
||||
ElMessage.success('恢复审批成功')
|
||||
await getTableData()
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error, '恢复审批失败')
|
||||
if (isApprovalRecoveryConflict(error, message)) {
|
||||
ElMessage.error('当前退款审批状态不允许恢复')
|
||||
await getTableData()
|
||||
} else {
|
||||
ElMessage.error(message)
|
||||
}
|
||||
} finally {
|
||||
recoverApprovalLoading.value = false
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户取消
|
||||
})
|
||||
}
|
||||
|
||||
// 获取操作按钮
|
||||
const getActions = (row: Refund) => {
|
||||
const actions: any[] = []
|
||||
@@ -1024,6 +1112,14 @@
|
||||
})
|
||||
}
|
||||
|
||||
if (canRecoverApproval(row) && hasAuth('refund:detail')) {
|
||||
actions.push({
|
||||
label: '恢复审批',
|
||||
handler: () => handleRecoverApproval(row),
|
||||
type: 'warning'
|
||||
})
|
||||
}
|
||||
|
||||
if (canManualApproveRefund(row) && hasAuth('refund:approve')) {
|
||||
actions.push({
|
||||
label: '通过',
|
||||
|
||||
Reference in New Issue
Block a user