diff --git a/openspec/changes/update-refund-wecom-approval-processing-status/design.md b/openspec/changes/update-refund-wecom-approval-processing-status/design.md new file mode 100644 index 0000000..c06109d --- /dev/null +++ b/openspec/changes/update-refund-wecom-approval-processing-status/design.md @@ -0,0 +1,64 @@ +## Context + +退款申请由后台提交业务资料,审批过程在企微完成,后台只负责展示企微审批信息以及订单退款的业务处理结果。当前页面仍按本地审批模型展示通过、拒绝和退回操作,需要调整为只读状态展示并保留受限的重新申请能力。 + +## Goals / Non-Goals + +- Goals: 让退款创建、列表和详情与企微审批及业务处理状态保持一致。 +- Goals: 展示可追溯的审批单号、参与人、意见、附件和时间线。 +- Goals: 明确区分审批状态与业务处理状态,并覆盖处理失败和自动重试提示。 +- Goals: 允许符合条件的退款重新申请,同时锁定订单、资产和提交人快照。 +- Non-Goals: 不在前端实现企微审批动作、审批回调或审批引擎。 +- Non-Goals: 不在后台执行非代理钱包的人工退款。 +- Non-Goals: 不根据本地旧状态推断企微审批状态或处理状态。 + +## Decisions + +- Decision: 详情接口返回的 `approval` 对象作为企微审批信息的唯一数据源。 +- Rationale: 审批动作在企微完成,前端不应通过本地状态或额外请求重建审批过程。 + +- Decision: `processing_status` 与审批状态独立展示,并使用后端返回的状态名称、失败摘要和业务处理结果。 +- Rationale: “审批通过”不等于退款处理完成,两个状态必须避免混用。 + +- Decision: 创建和重新申请的附件统一使用 `{ file_key, file_name, file_size }[]`。 +- Rationale: 页面既要提交对象存储文件标识,也要在审批详情中保留可读的文件元数据。 + +- Decision: 重新申请资格由后端最终校验,前端按已驳回、已撤销或已删除状态控制入口并处理接口拒绝。 +- Rationale: 状态可能被并发更新,前端展示控制不能替代服务端业务校验。 + +- Decision: 移除本地通过、驳回、退回和人工退款确认入口及调用。 +- Rationale: 这些操作与企微审批职责冲突,继续保留会造成重复审批或错误退款。 + +## Data Contract + +- Create request: refund amount, reason, remark and `attachments`. +- Detail response: refund business data, `approval` object and `processing_status`. +- Approval object: `source`, `sp_no`, `status`, `status_name`, `template_version`, `applicant`, `approvers`, `comments`, `attachments`, `timeline`, `business_process_result`. +- Resubmit request: editable refund amount, attachments and reason; immutable order, asset and submitter snapshots. +- Resubmit eligible statuses: rejected, revoked or deleted, using the backend status contract. + +## Risks / Trade-offs + +- Risk: 企微审批状态名称或时间线字段可能为空或增加新状态. +- Mitigation: 保留后端原始字段,缺失时显示稳定占位,不在前端硬编码未知状态映射。 + +- Risk: 审批通过后业务处理可能失败或进入重试. +- Mitigation: 独立展示处理状态、失败摘要,并按后端结果显示系统重试或联系管理员提示。 + +- Risk: 后端仍允许旧本地审批接口调用. +- Mitigation: 前端移除入口和调用,同时由后端按新审批模式拒绝不适用的本地操作。 + +## Migration Plan + +1. 确认退款创建、详情和重新申请接口的字段名称及状态枚举。 +2. 扩展退款类型和对象存储附件元数据类型。 +3. 更新创建退款表单和附件提交结构。 +4. 重构退款详情为业务信息、企微审批信息和业务处理结果三个区域。 +5. 移除本地审批及人工退款操作,保留并限制重新申请入口。 +6. 验证审批中、通过处理中、处理成功、驳回、撤销和通过后撤销异常状态。 + +## Open Questions + +- `processing_status` 的完整枚举、展示名称和“系统重试中/联系管理员”的触发字段需要后端确认。 +- “已删除”是退款记录状态、审批状态还是业务处理状态,需要接口契约明确。 +- `approval.attachments` 是否与退款创建附件共用文件对象结构,需要联调确认。 diff --git a/openspec/changes/update-refund-wecom-approval-processing-status/proposal.md b/openspec/changes/update-refund-wecom-approval-processing-status/proposal.md new file mode 100644 index 0000000..4c60164 --- /dev/null +++ b/openspec/changes/update-refund-wecom-approval-processing-status/proposal.md @@ -0,0 +1,41 @@ +# Change: 退款企微审批详情与业务处理状态 + +## Why + +退款申请创建时需要一次性提交完整业务资料,后续审批由企微完成,后台系统不再承担本地通过、驳回、退回或人工退款确认。当前退款页面仍展示本地审批操作,且详情无法完整呈现企微审批过程和退款业务处理结果,容易造成审批状态与实际处理状态混淆。 + +## What Changes + +- 退款创建表单支持退款金额、退款原因、备注和附件,并以对象存储附件对象数组提交。 +- 创建后申请退款金额不可在本地修改,审批金额以企微审批结果为准。 +- 退款详情拆分为退款业务信息、企微审批信息和业务处理结果三个区域。 +- 企微审批区域展示 `sp_no`、审批状态、申请人、审批人、意见、附件和审批时间线。 +- 业务处理区域展示 `processing_status`、处理失败摘要和“系统重试中/联系管理员”提示。 +- 退款列表和详情仅展示审批及处理状态,不提供本地通过、驳回、退回或人工退款确认按钮。 +- 重新申请仅允许已驳回、已撤销或已删除的退款记录,并允许修改金额、凭证和原因;订单、资产和提交人快照不可修改。 +- 继续使用 `POST /api/admin/refunds`、`GET /api/admin/refunds/{id}` 和 `POST /api/admin/refunds/{id}/resubmit`,同步扩展请求和响应契约。 + +## Impact + +- Affected specs: + - `refund-management` +- Affected code: + - `src/types/api/refund.ts` + - `src/api/modules/refund.ts` + - `src/views/finance/refund/index.vue` + - `src/views/finance/refund/detail.vue` + - 退款创建与附件展示组件 +- API contracts: + - `POST /api/admin/refunds` + - `GET /api/admin/refunds/{id}` + - `POST /api/admin/refunds/{id}/resubmit` +- Dependencies: + - 后端返回完整 `approval` 对象和 `processing_status`。 + - 对象存储上传接口可返回 `file_key`,前端维护文件名和文件大小。 + - 企微审批系统负责审批动作和审批结果同步。 +- Out of scope: + - 前端发起、撤回或执行企微审批动作。 + - 前端实现财务线下退款。 + - 前端根据状态码自行推导审批结果或业务处理结果。 +- Breaking changes: + - 退款审批相关本地操作按钮和对应前端调用不再适用于企微审批退款流程。 diff --git a/openspec/changes/update-refund-wecom-approval-processing-status/specs/refund-management/spec.md b/openspec/changes/update-refund-wecom-approval-processing-status/specs/refund-management/spec.md new file mode 100644 index 0000000..19dbe43 --- /dev/null +++ b/openspec/changes/update-refund-wecom-approval-processing-status/specs/refund-management/spec.md @@ -0,0 +1,89 @@ +## ADDED Requirements + +### Requirement: Refund Creation Submits WeCom Approval Materials + +The refund creation flow SHALL submit the refund business materials required for an external WeCom approval. + +#### Scenario: Create refund with business materials + +- **GIVEN** 用户打开退款创建表单 +- **WHEN** 用户填写退款金额、退款原因、备注并完成附件上传 +- **THEN** 系统 MUST submit the refund amount, reason, remark and attachment metadata to `POST /api/admin/refunds` +- **AND** 每个附件 MUST use the structure `{ file_key, file_name, file_size }` + +#### Scenario: Submitted amount is immutable locally + +- **GIVEN** 退款申请已创建并进入企微审批 +- **WHEN** 用户查看退款记录 +- **THEN** 页面 MUST NOT provide a local control to modify the submitted refund amount +- **AND** any changed amount MUST be submitted through the defined resubmission flow only + +### Requirement: Refund Detail Displays WeCom Approval + +The refund detail page SHALL display the backend-provided WeCom approval object as a read-only approval record. + +#### Scenario: Display approval details + +- **GIVEN** `GET /api/admin/refunds/{id}` returns an `approval` object +- **WHEN** 用户打开退款详情 +- **THEN** 页面 MUST display the approval source, `sp_no`, status, status name, applicant, approvers, comments, attachments and timeline +- **AND** 页面 MUST preserve the backend approval values without locally changing or deriving them + +#### Scenario: No local approval controls + +- **GIVEN** 用户查看退款列表或退款详情 +- **WHEN** 页面展示企微审批信息 +- **THEN** 页面 MUST NOT display local approve, reject, return or manual refund confirmation buttons +- **AND** 页面 MUST NOT call local approval action endpoints for this flow + +### Requirement: Refund Processing Result Is Independent From Approval + +The refund detail page SHALL display business processing status separately from WeCom approval status. + +#### Scenario: Display processing result + +- **GIVEN** 退款详情返回 `processing_status` and its result fields +- **WHEN** 用户查看退款业务处理区域 +- **THEN** 页面 MUST display the processing status, failure summary and business process result +- **AND** 页面 MUST display a system-retrying or contact-administrator message when indicated by the backend + +#### Scenario: Display required lifecycle states + +- **GIVEN** 后端返回审批中、通过处理中、处理成功、驳回、撤销或通过后撤销异常状态 +- **WHEN** 页面渲染退款列表或详情 +- **THEN** 页面 MUST show an explicit status for the returned state +- **AND** 页面 MUST NOT infer processing completion from approval status alone + +### Requirement: Refund Resubmission Has Restricted Editable Fields + +The refund resubmission flow SHALL allow resubmission only for rejected, revoked or deleted records and SHALL preserve immutable business snapshots. + +#### Scenario: Resubmit an eligible refund + +- **GIVEN** 退款状态为已驳回、已撤销或已删除 +- **WHEN** 用户发起重新申请 +- **THEN** 页面 MUST allow editing the refund amount, attachments and refund reason +- **AND** 系统 MUST submit the request to `POST /api/admin/refunds/{id}/resubmit` + +#### Scenario: Reject resubmission for an ineligible refund + +- **GIVEN** 退款记录不属于已驳回、已撤销或已删除状态 +- **WHEN** 用户查看退款详情 +- **THEN** 页面 MUST NOT enable the resubmission action +- **AND** 页面 MUST handle a backend rejection without creating a new approval request + +#### Scenario: Preserve refund snapshots on resubmission + +- **GIVEN** 用户填写重新申请表单 +- **WHEN** 用户修改可编辑字段 +- **THEN** 页面 MUST NOT provide controls to change the order snapshot, asset snapshot or submitter snapshot + +### Requirement: Refund Pages Use Backend Approval And Processing Data + +The refund list and detail pages SHALL use backend response data as the source of truth for approval and processing display. + +#### Scenario: Refresh refund status + +- **WHEN** 用户刷新退款列表或重新加载退款详情 +- **THEN** 页面 MUST use the latest refund, approval and processing data returned by the backend +- **AND** 页面 MUST NOT reconstruct a status from stale local form data or legacy status mappings diff --git a/openspec/changes/update-refund-wecom-approval-processing-status/tasks.md b/openspec/changes/update-refund-wecom-approval-processing-status/tasks.md new file mode 100644 index 0000000..8d023cc --- /dev/null +++ b/openspec/changes/update-refund-wecom-approval-processing-status/tasks.md @@ -0,0 +1,38 @@ +## 1. Contract And Types + +- [x] 1.1 按产品文档落地退款创建接口的金额、原因、备注和附件字段名称。 +- [x] 1.2 按产品文档扩展退款详情响应中的 `approval`、`processing_status`、失败摘要和业务处理结果字段。 +- [x] 1.3 按产品文档限制重新申请的可编辑字段、允许状态和不可变快照字段。 +- [x] 1.4 扩展退款请求、响应和审批对象 TypeScript 类型,支持附件对象数组。 +- [x] 1.5 更新退款 API service 类型及注释,移除不再适用的本地审批调用契约。 + +## 2. Refund Creation + +- [x] 2.1 更新退款创建表单,提交退款金额、原因、备注和附件。 +- [x] 2.2 复用现有对象存储上传流程,并将上传结果规范化为 `{ file_key, file_name, file_size }[]`。 +- [x] 2.3 创建成功后明确提示已提交企微审批,创建后的申请金额不可在本地修改。 +- [x] 2.4 验证附件上传中、上传失败、空附件和接口失败时不能提交错误数据。 + +## 3. Refund List And Detail + +- [x] 3.1 退款详情拆分为退款业务信息、企微审批信息和业务处理结果三个区域。 +- [x] 3.2 展示 `sp_no`、审批状态、申请人、审批人、意见、附件和审批时间线。 +- [x] 3.3 展示 `processing_status`、失败摘要,以及系统重试中/联系管理员提示。 +- [x] 3.4 退款列表和详情不显示本地通过、驳回、退回或人工退款确认按钮。 +- [x] 3.5 保留既有退款业务信息和附件查看能力,不根据前端状态推导审批或处理结果。 + +## 4. Resubmission + +- [x] 4.1 仅在已驳回、已撤销或已删除状态显示重新申请入口。 +- [x] 4.2 重新申请允许修改退款金额、凭证和退款原因。 +- [x] 4.3 重新申请禁止修改订单、资产和提交人快照。 +- [x] 4.4 提交 `/api/admin/refunds/{id}/resubmit` 后刷新详情并展示新的企微审批状态。 +- [x] 4.5 处理后端状态变化或无权限时的重新申请失败提示。 + +## 5. Verification + +- [x] 5.1 验证审批中、通过处理中、处理成功、驳回、撤销和通过后撤销异常展示明确。 +- [x] 5.2 验证审批信息与业务处理状态分区展示且互不推导。 +- [x] 5.3 验证本地审批及人工退款操作入口和 API 调用均已移除。 +- [x] 5.4 验证刷新、列表进入详情和重新申请后的数据来自后端最新响应。 +- [x] 5.5 运行类型检查、lint、相关测试和构建。 diff --git a/src/api/modules/refund.ts b/src/api/modules/refund.ts index d8271a9..2752a7a 100644 --- a/src/api/modules/refund.ts +++ b/src/api/modules/refund.ts @@ -8,10 +8,7 @@ import type { RefundQueryParams, RefundListResponse, CreateRefundRequest, - ApproveRefundRequest, - RejectRefundRequest, ResubmitRefundRequest, - ReturnRefundRequest, BaseResponse } from '@/types/api' @@ -40,24 +37,6 @@ export class RefundService extends BaseService { return this.post>('/api/admin/refunds', data) } - /** - * 审批通过退款申请 - * @param id 退款申请ID - * @param data 审批通过请求参数 - */ - static approveRefund(id: number, data: ApproveRefundRequest): Promise> { - return this.post>(`/api/admin/refunds/${id}/approve`, data) - } - - /** - * 审批拒绝退款申请 - * @param id 退款申请ID - * @param data 审批拒绝请求参数 - */ - static rejectRefund(id: number, data: RejectRefundRequest): Promise> { - return this.post>(`/api/admin/refunds/${id}/reject`, data) - } - /** * 重新提交退款申请 * @param id 退款申请ID @@ -66,13 +45,4 @@ export class RefundService extends BaseService { static resubmitRefund(id: number, data: ResubmitRefundRequest): Promise> { return this.post>(`/api/admin/refunds/${id}/resubmit`, data) } - - /** - * 驳回申请 - * @param id 退款申请ID - * @param data 退回请求参数 - */ - static returnRefund(id: number, data: ReturnRefundRequest): Promise> { - return this.post>(`/api/admin/refunds/${id}/return`, data) - } } diff --git a/src/components/business/CreateRefundDialog.vue b/src/components/business/CreateRefundDialog.vue index 3cca01b..726b326 100644 --- a/src/components/business/CreateRefundDialog.vue +++ b/src/components/business/CreateRefundDialog.vue @@ -46,6 +46,16 @@ placeholder="请输入退款原因" /> + + + @@ -77,7 +88,7 @@ import { ElMessage } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus' import { RefundService, OrderService } from '@/api/modules' - import type { CreateRefundRequest, Order } from '@/types/api' + import type { CreateRefundRequest, Order, RefundAttachment } from '@/types/api' import { formatMoney, yuanToFen } from '@/utils/business/format' import { getErrorMessage } from '@/utils/business' import VoucherUpload from '@/components/business/VoucherUpload.vue' @@ -111,13 +122,17 @@ requested_refund_amount?: number actual_received_amount: number refund_reason: string + remark: string refund_voucher_key: string[] + attachments: RefundAttachment[] }>({ order_id: null, requested_refund_amount: undefined, actual_received_amount: 0, refund_reason: '', - refund_voucher_key: [] + remark: '', + refund_voucher_key: [], + attachments: [] }) const validateRefundAmount = (rule: any, value: number, callback: any) => { @@ -201,7 +216,9 @@ formData.requested_refund_amount = undefined formData.actual_received_amount = 0 formData.refund_reason = '' + formData.remark = '' formData.refund_voucher_key = [] + formData.attachments = [] orderSearchOptions.value = [] uploadRef.value?.clearFiles(false) } @@ -221,8 +238,9 @@ order_id: formData.order_id!, requested_refund_amount: yuanToFen(formData.requested_refund_amount) ?? 0, actual_received_amount: formData.actual_received_amount, - refund_voucher_key: formData.refund_voucher_key, - refund_reason: formData.refund_reason || undefined + attachments: formData.attachments, + refund_reason: formData.refund_reason || undefined, + remark: formData.remark || undefined } await RefundService.createRefund(data) ElMessage.success('退款申请创建成功') diff --git a/src/components/business/VoucherUpload.vue b/src/components/business/VoucherUpload.vue index f3f8270..898f154 100644 --- a/src/components/business/VoucherUpload.vue +++ b/src/components/business/VoucherUpload.vue @@ -57,6 +57,7 @@ import { Close, Document, UploadFilled } from '@element-plus/icons-vue' import type { UploadFile, UploadInstance, UploadRawFile } from 'element-plus' import { StorageService } from '@/api/modules' + import type { RefundAttachment } from '@/types/api/refund' interface Props { modelValue?: string[] | string @@ -77,12 +78,14 @@ 'update:modelValue': [value: string[]] 'uploading-change': [value: boolean] change: [value: string[]] + 'files-change': [value: RefundAttachment[]] }>() const rootRef = ref() const uploadRef = ref() const uploadingCount = ref(0) const voucherFileKeyMap = new Map() + const voucherFileMetadataMap = new Map() const removedUploadUids = new Set() const fileObjectUrlMap = new Map() const selectedUploadUids = new Set() @@ -100,6 +103,10 @@ emit('change', keys) } + const emitFileMetadata = () => { + emit('files-change', Array.from(voucherFileMetadataMap.values())) + } + const setUploadingCount = (count: number) => { uploadingCount.value = Math.max(0, count) emit('uploading-change', uploadingCount.value > 0) @@ -109,12 +116,14 @@ activeUploadBatch += 1 setUploadingCount(0) voucherFileKeyMap.clear() + voucherFileMetadataMap.clear() removedUploadUids.clear() selectedUploadUids.clear() revokeAllFileObjectUrls() uploadRef.value?.clearFiles() if (emitValue) { emitVoucherKeys() + emitFileMetadata() } } @@ -228,7 +237,13 @@ } voucherFileKeyMap.set(uploadFile.uid, file_key) + voucherFileMetadataMap.set(uploadFile.uid, { + file_key, + file_name: file.name, + file_size: file.size + }) emitVoucherKeys() + emitFileMetadata() ElMessage.success('上传成功') } catch (error: any) { if (uploadBatch !== activeUploadBatch) return @@ -236,7 +251,9 @@ console.error(`上传${props.voucherName}失败:`, error) ElMessage.error(error?.message || '上传失败,请重试') voucherFileKeyMap.delete(uploadFile.uid) + voucherFileMetadataMap.delete(uploadFile.uid) emitVoucherKeys() + emitFileMetadata() removeUploadFile(uploadFile) } finally { if (uploadBatch === activeUploadBatch) { @@ -250,7 +267,9 @@ selectedUploadUids.delete(uploadFile.uid) revokeFileObjectUrl(uploadFile.uid) voucherFileKeyMap.delete(uploadFile.uid) + voucherFileMetadataMap.delete(uploadFile.uid) emitVoucherKeys() + emitFileMetadata() } const focusUploadArea = () => { diff --git a/src/types/api/refund.ts b/src/types/api/refund.ts index a345ce3..5ad49ea 100644 --- a/src/types/api/refund.ts +++ b/src/types/api/refund.ts @@ -10,6 +10,38 @@ export enum RefundStatus { RETURNED = 4 // 已退回 } +export interface RefundAttachment { + file_key: string + file_name?: string + file_size?: number +} + +export interface RefundApprovalTimelineItem { + time?: string + timestamp?: string + status?: string + status_name?: string + operator?: string + operator_name?: string + comment?: string + content?: string + [key: string]: unknown +} + +export interface RefundApproval { + source?: string | null + sp_no?: string | null + status?: string | null + status_name?: string | null + template_version?: string | null + applicant?: unknown + approvers?: unknown[] + comments?: unknown[] + attachments?: RefundAttachment[] + timeline?: RefundApprovalTimelineItem[] + business_process_result?: unknown +} + // 退款申请 export interface Refund { id: number @@ -25,8 +57,10 @@ export interface Refund { approved_refund_amount?: number | null // 实际退款金额(分) actual_received_amount?: number | null // 实收金额(分) status: RefundStatus + status_name?: string | null refund_reason: string refund_voucher_key?: string[] | string // 退款凭证附件列表;历史数据可能为单字符串或逗号字符串 + attachments?: RefundAttachment[] reject_reason: string remark: string asset_reset: boolean @@ -38,6 +72,10 @@ export interface Refund { current_approver_summary?: string | null // 当前审批人摘要 processing_status?: string | null // 业务处理状态 processing_status_name?: string | null // 业务处理状态名称 + processing_failure_summary?: string | null + processing_message?: string | null + business_process_result?: unknown + approval?: RefundApproval | null creator: number processor_id: number | null processed_at: string | null @@ -71,30 +109,15 @@ export interface CreateRefundRequest { order_id: number requested_refund_amount: number // 申请退款金额(分) actual_received_amount: number // 实收金额(分) - refund_voucher_key: string[] // 退款凭证附件列表 + attachments: RefundAttachment[] refund_reason?: string -} - -// 审批通过退款申请请求 -export interface ApproveRefundRequest { - approved_refund_amount?: number // 实际退款金额(分) remark?: string } -// 审批拒绝退款申请请求 -export interface RejectRefundRequest { - reject_reason: string -} - // 重新提交退款申请请求 export interface ResubmitRefundRequest { requested_refund_amount?: number actual_received_amount?: number - refund_voucher_key?: string[] + attachments?: RefundAttachment[] refund_reason?: string } - -// 退回退款申请请求 -export interface ReturnRefundRequest { - remark?: string -} diff --git a/src/views/finance/refund/detail.vue b/src/views/finance/refund/detail.vue index 0b3ff29..e83f786 100644 --- a/src/views/finance/refund/detail.vue +++ b/src/views/finance/refund/detail.vue @@ -10,6 +10,13 @@ 返回

{{ pageTitle }}

+ + 重新申请 + @@ -24,90 +31,10 @@ - - - - - {{ refund?.refund_no }} - - - {{ formatCurrency(refund?.requested_refund_amount) }} - - - -
- 不填则默认使用申请金额 -
-
- - - -
- -
- - - - - - {{ refund?.refund_no }} - - - - - - - - @@ -157,6 +84,7 @@ voucher-name="退款凭证" @uploading-change="resubmitVoucherUploading = $event" @change="resubmitFormRef?.validateField('refund_voucher_key')" + @files-change="resubmitForm.attachments = $event" /> @@ -169,7 +97,7 @@ :loading="resubmitLoading || resubmitVoucherUploading" :disabled="resubmitVoucherUploading" > - {{ resubmitVoucherUploading ? '凭证上传中...' : '确认提交' }} + {{ resubmitVoucherUploading ? '凭证上传中...' : '确认申请' }} @@ -180,27 +108,23 @@