feat: 换货退款拦截错误展示
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 11m41s

This commit is contained in:
luo
2026-07-22 10:14:13 +08:00
parent 5079f97326
commit fbfdd01eec
5 changed files with 134 additions and 7 deletions

View File

@@ -0,0 +1,26 @@
# Change: 展示换货退款拦截错误
## Why
创建换货单时,后端会拦截存在活跃退款申请的资产,但当前创建页仅使用通用提示或控制台日志,运营无法直接知道资产因退款申请不能换货。
## What Changes
- 复用 `POST /api/admin/exchanges` 的现有失败 `code``msg` 契约,不改变成功响应结构。
- 创建换货单被退款拦截或其他后端业务规则拒绝时,在创建换货单表单顶部或旧资产行直接展示后端业务原因,例如“该资产存在退款申请”。
- 创建失败后保留当前填写的表单、已选资产与流程类型,恢复提交按钮使用户可更换资产后重新提交。
- 前端不新增退款状态查询、预检查区域或退款申请入口。
- 前端不读取或推断退款审批、撤销、拒绝或处理完成状态;是否允许换货完全依据创建接口结果。
## Impact
- Affected specs: `exchange-management`
- Affected code:
- `src/api/modules/exchange.ts`
- `src/views/asset-management/exchange-management/index.vue`
- API contract:
- `POST /api/admin/exchanges`
- Out of scope:
- 退款状态查询和预检查
- 退款审批、撤销、拒绝或处理功能
- 换货创建成功响应结构调整

View File

@@ -0,0 +1,54 @@
## ADDED Requirements
### Requirement: Exchange Creation Refund Interception Feedback
The exchange creation page SHALL display the backend business reason when `POST /api/admin/exchanges` rejects the selected old asset because of an active refund application. The page MUST use the response `code` and `msg` contract without changing the successful exchange response structure.
#### Scenario: Display active refund interception reason
- **GIVEN** 用户已填写创建换货单表单并选择存在活跃退款申请的旧资产
- **WHEN** `POST /api/admin/exchanges` 返回失败 `code``msg`
- **THEN** 页面 MUST display the backend business reason in the create exchange form context
- **AND** 当后端 `msg` 为“该资产存在退款申请”时,页面 MUST display that message directly
- **AND** 系统 MUST NOT create a new exchange order
#### Scenario: Display other exchange creation business reason
- **GIVEN** 用户已填写创建换货单表单
- **WHEN** 创建接口返回非退款相关的业务失败 `code``msg`
- **THEN** 页面 MUST display the backend `msg` in the create exchange form context
- **AND** 页面 MUST NOT replace the backend business reason with a generic frontend failure message
### Requirement: Exchange Creation Retry After Interception
The exchange creation page SHALL preserve the current form values after a backend rejection and allow the user to change assets and submit again after the request completes.
#### Scenario: Preserve form after refund interception
- **GIVEN** 创建换货单请求被后端退款规则拒绝
- **WHEN** 页面展示后端业务原因
- **THEN** 页面 MUST retain the selected old asset, new asset when applicable, flow type, reason, and other form values
- **AND** 提交按钮 MUST leave its loading state after the request completes
#### Scenario: Retry with another asset
- **GIVEN** 用户已收到退款拦截错误
- **WHEN** 用户更换旧资产并再次提交创建换货单
- **THEN** 系统 MUST send a new `POST /api/admin/exchanges` request using the updated form values
### Requirement: Backend-Controlled Exchange Refund Eligibility
The exchange creation page MUST rely exclusively on the exchange creation response to determine refund-related eligibility. The frontend MUST NOT add a refund status lookup, a refund pre-check area, or infer eligibility from refund approval, rejection, withdrawal, or completion states.
#### Scenario: Do not pre-check refund eligibility
- **WHEN** 用户打开或填写创建换货单表单
- **THEN** 页面 MUST NOT issue a refund status query solely to determine exchange eligibility
- **AND** 页面 MUST NOT render a refund status pre-check area
#### Scenario: Permit assets according to backend decision
- **GIVEN** 用户选择曾有退款申请但后端允许换货的资产
- **WHEN** 创建接口成功返回
- **THEN** 页面 MUST treat the exchange creation as successful
- **AND** 页面 MUST NOT block the operation based on a frontend interpretation of refund history

View File

@@ -0,0 +1,22 @@
## 1. Exchange Creation Failure Contract
- [x] 1.1 保持 `POST /api/admin/exchanges` 及其成功响应结构不变。
- [x] 1.2 读取创建失败响应的统一 `code``msg`,不将后端业务原因替换为通用前端文案。
## 2. Refund Interception Feedback
- [x] 2.1 在创建换货单表单顶部或旧资产行展示后端返回的退款拦截业务原因。
- [x] 2.2 退款拦截失败后保留表单输入、已选资产和流程类型。
- [x] 2.3 请求结束后恢复提交按钮,使用户能够更换资产并再次提交。
## 3. Backend-Controlled Eligibility
- [x] 3.1 不增加退款状态查询、预检查区域或退款申请入口。
- [x] 3.2 不根据退款审批、撤销、拒绝或处理状态自行判断资产能否换货。
## 4. Verification
- [ ] 4.1 验证活跃退款资产创建换货时显示“该资产存在退款申请”或后端等价业务原因,且不创建换货单。
- [ ] 4.2 验证已拒绝、撤销或处理完成退款的资产按后端结果可继续创建换货。
- [ ] 4.3 验证失败后保留表单并可更换资产重新提交。
- [x] 4.4 运行相关前端校验,并执行 `openspec validate update-exchange-refund-interception-feedback --strict`

View File

@@ -88,8 +88,11 @@ export class ExchangeService extends BaseService {
* POST /api/admin/exchanges
* @param data 创建参数
*/
static createExchange(data: CreateExchangeRequest): Promise<BaseResponse<ExchangeResponse>> {
return this.create<ExchangeResponse>('/api/admin/exchanges', data)
static createExchange(
data: CreateExchangeRequest,
config?: Record<string, any>
): Promise<BaseResponse<ExchangeResponse>> {
return this.post<BaseResponse<ExchangeResponse>>('/api/admin/exchanges', data, config)
}
/**

View File

@@ -50,6 +50,14 @@
@closed="handleCloseCreateDialog"
>
<ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="120px">
<ElAlert
v-if="createFailureMessage"
:title="createFailureMessage"
type="error"
:closable="false"
show-icon
style="margin-bottom: 18px"
/>
<ElFormItem label="流程类型" prop="flow_type">
<ElSelect
v-model="createForm.flow_type"
@@ -123,6 +131,7 @@
:loading="oldCardSearchLoading"
style="width: 100%"
clearable
@change="handleOldIdentifierChange"
>
<ElOption
v-for="card in oldIotCardOptions"
@@ -154,6 +163,7 @@
:loading="oldDeviceSearchLoading"
style="width: 100%"
clearable
@change="handleOldIdentifierChange"
>
<ElOption
v-for="device in oldDeviceOptions"
@@ -374,7 +384,7 @@
import { ExchangeService, CardService, DeviceService } from '@/api/modules'
import type { ExchangeResponse } from '@/api/modules/exchange'
import type { StandaloneIotCard, Device } from '@/types/api'
import { ElMessage, ElTag, ElButton, ElMessageBox, ElSwitch } from 'element-plus'
import { ElAlert, ElMessage, ElTag, ElButton, ElMessageBox, ElSwitch } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
@@ -394,6 +404,7 @@
const shipFormRef = ref<FormInstance>()
const createDialogVisible = ref(false)
const createLoading = ref(false)
const createFailureMessage = ref('')
const tableRef = ref()
const createFormRef = ref<FormInstance>()
@@ -823,6 +834,7 @@
// 显示创建对话框
const showCreateDialog = () => {
createFailureMessage.value = ''
createDialogVisible.value = true
}
@@ -835,6 +847,7 @@
// 流程类型变更
const handleFlowTypeChange = async () => {
createFailureMessage.value = ''
// 清空新资产标识符和数据迁移选项
createForm.new_identifier = ''
createForm.migrate_data = false
@@ -854,6 +867,7 @@
// 旧资产类型变更
const handleOldAssetTypeChange = async () => {
createFailureMessage.value = ''
// 清空旧资产标识符
createForm.old_identifier = ''
oldIotCardOptions.value = []
@@ -882,6 +896,10 @@
}
}
const handleOldIdentifierChange = () => {
createFailureMessage.value = ''
}
const refreshExchangeDetail = async (id: number) => {
try {
const res = await ExchangeService.getExchangeDetail(id)
@@ -1198,6 +1216,7 @@
createFormRef.value?.validate(async (valid) => {
if (!valid || createLoading.value) return
createFailureMessage.value = ''
createLoading.value = true
try {
const exchangeReason =
@@ -1219,7 +1238,9 @@
requestData.migrate_data = createForm.migrate_data
}
const res = await ExchangeService.createExchange(requestData)
const res = await ExchangeService.createExchange(requestData, {
requestOptions: { errorMessageMode: 'none' }
})
if (res.code === 0) {
ElMessage.success('换货单创建成功')
createDialogVisible.value = false
@@ -1227,9 +1248,9 @@
return
}
ElMessage.error(res.msg || '换货单创建失败')
} catch (error) {
console.error('创建换货单失败:', error)
createFailureMessage.value = res.msg || '换货单创建失败'
} catch (error: any) {
createFailureMessage.value = error?.response?.data?.msg || '换货单创建失败'
} finally {
createLoading.value = false
}
@@ -1238,6 +1259,7 @@
// 关闭创建对话框
const handleCloseCreateDialog = () => {
createFailureMessage.value = ''
createFormRef.value?.resetFields()
// 清空搜索选项
oldIotCardOptions.value = []