fix: some
All checks were successful
构建并部署前端到测试环境 / deploy (push) Successful in 55s

This commit is contained in:
luo
2026-10-13 17:43:38 +08:00
parent c31183205d
commit 16effb69f3
8 changed files with 94 additions and 43 deletions

View File

@@ -70,13 +70,13 @@
创建换货和确认完成同时覆盖 HTTP 200 的非零业务码与 Axios HTTP 错误响应,不假定只有一种承载形式。在受影响的两个服务调用范围关闭重复默认提示,局部共享处理器处理 1202,其余错误仍按既有统一错误分类/文案展示。
1202 提示读取业务 `data.phone`,有号码则显示「新资产已绑定手机号 {phone},请先到『手机号资产关联』解除后再提交」,无号码时显示不含号码的完整指引,不输出 `undefined`。只以文本插值渲染,不把后端内容拼成 HTML。
1202 弹窗原样使用业务 `msg`,不拼接号码或固定替换文案。只有响应明确带有效字符串 `data.phone` 时才允许带参跳转;`data=null` 的已换货等拒绝仅显示后端文案和「知道了」。只以文本渲染,不拼成 HTML。
具备关联管理页面查看权限时可提供跳转;无权限时提示联系有权限人员,不扩大解绑权限。禁止自动调用 DELETE、批量解绑或 CSV 解绑。响应内完整号码不写日志或持久化;换货页现有打印完整错误对象的受影响分支也应修正,防止号码间接泄漏。
手机号冲突且具备原关联页面查看权限时提供跳转;无权限或新资产标识缺失时仅提示,不扩大权限。禁止自动解绑。完整号码不额外写日志或本地存储;用户主动跳转的 query 参数是已明确授权的例外。
创建失败保留资产/原因等字段;完成失败重新加载换货单,保持后端「已发货待确认」事实,不乐观标记完成。解除后由用户再次提交。取消只关闭提示,不当作解绑或换货成功。
用户追加明确要求快捷定位并勾选关联:创建换货冻结当前新资产标识及关联手机号数组,确认完成使用原单新资产标识;错误响应手机号优先。主动跳转通过 `asset_identifier` 和 `phone` 查询参数传递目标,允许该 URL 带号码但不额外写日志或本地存储。关联页按 1 页/20 条加载,仅默认选中精确匹配的有效行;不自动解绑,继续原原因与二次确认。
用户追加快捷定位:创建换货冻结当前新资产上下文,确认完成使用原单新资产标识;手机号只能取当前错误响应 `data.phone`,不得根据历史数组推断其他 1202 是手机号冲突。主动跳转通过 `asset_identifier` 和 `phone` query 传递目标。关联页按 1 页/20 条加载,仅选中精确匹配有效行,继续原原因和二次确认。
通过 ArtTable 透传清空/勾选方法,数据更新后 nextTick 完成实际选择。缓存重入和路由参数变更重置查询,请求序号防止过期响应覆盖;手工筛选/翻页、解绑后刷新清除旧选择。批量结果保持后端布尔契约,仅展示中文「解绑成功/解绑失败」。原查看与批量解绑权限不变。

View File

@@ -9,7 +9,7 @@
| 规则测试与本地订单详情 | 支付摘要、独立支付单状态、失败记录、空时间与金额展示正常 | `src/types/api/order.ts`、订单列表/详情、`orderPayment.ts` |
| 规则测试与模拟核销/代理注册详情 | 终态优先于陈旧恢复标志,核销可重提、代理注册提示重新扫码 | 核销展示工具、退款常量、注册状态辅助函数及相应页面 |
| 规则测试与模拟改密弹窗 | 新密码禁止符号,旧密码不受新长度限制;375px 下错误说明不重叠 | `loginPassword.ts`、后台密码表单、本人改密弹窗与双语文案 |
| 规则测试与模拟换货确认完成 | 1202 从 `data.phone` 展示号码,提示可取消,单据仍待确认,不自动解绑 | `exchangeError.ts`、换货页面与服务 |
| 规则测试与原模拟换货确认完成 | 1202 按最新规则原样展示 `msg`,有效响应手机号才提供跳转;原待确认状态保持,不自动解绑 | `exchangeError.ts`、换货页面与服务 |
| 数值测试与模拟预警规则/详情 | 规则额度移除,MB 快照两位小数,零和缺失区分,套餐商品额度保留 | 预警类型、规则页、记录列表/详情及 `trafficAlert.ts` |
| 模板和源码回归检查 | 新四列表头与手机号说明,设备 IMEI 必填说明、原 Excel 模板和行级失败反馈保持 | `shopBusinessOwnerImport.ts`、导入页面 |
@@ -56,7 +56,7 @@ node scripts/tests/verifyBuild.mjs --serve
已在用户明确要求下补充以下行为:
- 创建换货捕获提交时新资产 ICCID/虚拟号及 `associated_phones`;确认完成使用换货单的新资产标识。跳转手机号优先取 1202 `data.phone`,缺失时取新资产手机号数组首个有效值,不使用旧资产。
- 创建换货捕获提交时新资产上下文,确认完成使用换货单的新资产标识,不使用旧资产。按用户最新修正,跳转手机号只取当前 1202 `data.phone`,缺失时不再回退历史手机号数组。
- 用户点击跳转后,路由带 `asset_identifier` 和 `phone`,目标页从 1 页/20 条精确查询。只实际勾选匹配两项条件且有效的记录,不全选历史失效记录。
- 表格勾选方法由已有 ArtTable 透传;手工取消后不立即重新勾选,重置、筛选、翻页及解绑后刷新清空旧选择;缓存重入或路由参数变化重新取目标,过期响应不能覆盖新查询。
- 批量解绑仍使用 `phone_asset_association:unbind_batch` 和原原因/二次确认表单;结果列展示「解绑成功/解绑失败」,资产类型同时显示中文,不修改后端布尔值契约。
@@ -64,3 +64,9 @@ node scripts/tests/verifyBuild.mjs --serve
原说明要求号码不额外记录或持久化;用户追加明确要求两项路由参数,因此用户主动跳转时允许号码进入该 URL 查询参数,不额外记录日志或写入本地存储。
新增三项自动测试,全套 66 项通过,类型检查、受影响文件 lint、编码和严格规范校验通过,生产构建通过(原有警告仍在)。本地模拟确认了完整的 1202 弹窗 → 带参跳转 → 筛选并勾选有效记录 → 手工取消禁用按钮 → 原确认表单 → 中文成功结果链路,没有调用真实后台解绑接口。
### 用户修正:区分共用的 1202 错误码
同为 1202 的「已完成过换货」响应 `data=null`,只弹出后端 `msg` 和「知道了」,不出现关联管理按钮;带有效 `data.phone` 的绑定冲突才保留原权限内的去解绑入口。两种弹窗正文都使用原始 `msg`,不再拼接手机号或固定指引。新增回归覆盖 HTTP/业务承载、null/空白/非字符串号码及资产历史数组不得绕过门槛,全套测试增至 67 项。
该修正的 67 项自动回归、类型检查、lint、格式、编码及严格规范校验通过。重新做浏览器检查时本地会话回到了资产首页,未完成两种新弹窗的视觉验证;未将此项标为已通过,也未操作真实换货或解绑数据。

View File

@@ -2,21 +2,27 @@
### Requirement: 换货新资产手机号绑定冲突提示
后台创建换货和确认完成 SHALL 在错误码 `1202` 时读取错误业务响应的 `data.phone`,用一次文本弹窗提示解除关联的后台入口;处理 MUST 覆盖正常 HTTP 承载的业务失败和 HTTP 错误响应,不自动解绑、预查扫码记录或扩大权限。
后台创建换货和确认完成 SHALL 在错误码 `1202` 时用一次文本弹窗原样展示后端 `msg`。`1202` 同时用于已换货等业务拒绝,只有错误响应带有效字符串 `data.phone` 且用户有原查看权限时 MUST 提供解除关联跳转;处理 MUST 覆盖业务失败与 HTTP 错误,不自动解绑或扩大权限。
#### Scenario: 1202 返回手机号
- **WHEN** `POST /api/admin/exchanges` 或 `POST /api/admin/exchanges/{id}/complete` 返回 `code=1202` 且含 `data.phone`
- **THEN** 页面 MUST 提示「新资产已绑定手机号 {phone},请先到『手机号资产关联』解除后再提交」
- **THEN** 页面 MUST 原样展示后端 `msg`,不得拼接手机号或替换为固定文案
- **AND** 有原查看权限且已知新资产时 MUST 提供「前往手机号资产关联」按钮并带参跳转
- **AND** 默认错误提示与该弹窗 MUST NOT 重复展示
- **AND** 手机号 MUST 只作为文本渲染,不写日志或持久化
- **AND** 手机号 MUST 只用于用户主动发起的路由参数,不额外写日志或本地存储
- **AND** 前端 MUST NOT 自动调用单条、批量或 CSV 解绑接口
#### Scenario: 号码缺失或无管理权限
- **WHEN** 1202 响应不含有效号码,或用户没有关联管理入口权限
- **THEN** 页面 MUST 显示不含 `undefined` 的解除指引,无号码时不自行猜测
- **AND** 无管理权限时 MUST 提示联系有权限人员,不暴露越权操作按钮
- **THEN** 页面 MUST 使用仅有「知道了」的提示弹窗原样展示 `msg`,不展示跳转/解绑按钮
- **AND** MUST 不根据选中资产 `associated_phones` 推断该错误为手机号绑定冲突
#### Scenario: 已完成过换货的 1202
- **WHEN** 接口返回 `code=1202,data=null,msg=该资产已完成过换货,如需再次换货请使用换货后的新资产`
- **THEN** 页面 MUST 显示该原始 `msg` 和「知道了」,不显示「前往手机号资产关联」
### Requirement: 换货冲突后保持数据并允许手动重试
@@ -45,7 +51,7 @@
### Requirement: 换货冲突跳转携带新资产与手机号
收到 1202 后,用户点击「前往手机号资产关联」时,后台 SHALL 通过 `asset_identifier` 和 `phone` 路由查询参数传递本次提交的新资产标识及关联手机号。创建换货 MUST 使用提交时选中的新卡 ICCID 或设备虚拟号;确认完成 MUST 使用换货单的新资产标识,不得带入旧资产。手机号 MUST 优先使用错误响应 `data.phone`,缺失时可取新资产 `associated_phones` 的首个有效值。
收到带有效 `data.phone` 的 1202 后,用户点击「前往手机号资产关联」时,后台 SHALL 通过 `asset_identifier` 和 `phone` 路由参数传递本次提交的新资产标识与错误响应手机号。创建换货 MUST 使用提交时选中的新卡 ICCID 或设备虚拟号;确认完成 MUST 使用换货单的新资产标识,不得带入旧资产。手机号缺失时 MUST 不从 `associated_phones` 回退生成解绑入口。
#### Scenario: 创建换货选择新资产后被拒

View File

@@ -66,3 +66,4 @@
- [x] 8.2 关联页预填两项筛选,默认 1 页/20 条,仅勾选匹配有效记录;缓存、参数变化、过期响应和手工筛选清除旧选择。
- [x] 8.3 复用原批量解绑权限、原因与二次确认,结果布尔值改中文成功/失败。
- [x] 8.4 66 项回归测试通过,本地完整跳转、实际勾选、手工取消及模拟批量结果验证通过;未操作真实资产。
- [x] 8.5 按用户修正区分共用 1202:`data=null` 只展示原始 `msg` 和「知道了」,仅带有效响应手机号才提供去解绑;不从历史数组回退。新增回归,全套 67 项通过。

View File

@@ -202,13 +202,17 @@ test('order payment record status is independent from order status and channels
test('exchange phone conflicts support HTTP error and business response without inventing a missing number', async () => {
const module = await loadModule('src/utils/business/exchangeError.ts')
const failure = { code: 1202, data: { phone: '13800000000' } }
assert.match(module.getExchangePhoneConflictMessage(failure), /13800000000/)
const failure = {
code: 1202,
data: { phone: '13800000000' },
msg: '新资产已绑定手机号,请先在后台解除该资产的手机号绑定,再重新提交换货申请'
}
assert.equal(module.getExchangePhoneConflictMessage(failure), failure.msg)
assert.equal(
module.getExchangePhoneConflictMessage({ response: { data: failure } }),
module.getExchangePhoneConflictMessage(failure)
)
assert.match(module.getExchangePhoneConflictMessage({ code: 1202 }), /请先到/)
assert.match(module.getExchangePhoneConflictMessage({ code: 1202 }), /换货申请未完成/)
assert.doesNotMatch(module.getExchangePhoneConflictMessage({ code: 1202 }), /undefined|null/)
assert.equal(module.getExchangePhoneConflictMessage({ code: 1206 }), undefined)
assert.equal(
@@ -238,12 +242,9 @@ test('exchange association handoff carries only the new asset and its bound phon
phone: '19232713115'
}
)
assert.deepEqual(
assert.equal(
getExchangePhoneAssociationQuery({ response: { data: { code: 1202 } } }, newAsset),
{
asset_identifier: '89860624590014594318',
phone: '19232713115'
}
undefined
)
assert.equal(getExchangePhoneAssociationQuery({ code: 1206 }, newAsset), undefined)
assert.equal(
@@ -266,6 +267,31 @@ test('exchange association handoff carries only the new asset and its bound phon
)
})
test('1202 with data null keeps the original msg and cannot navigate to unbind despite asset phone history', async () => {
const { getExchangePhoneConflictMessage, getExchangePhoneAssociationQuery } = await loadModule(
'src/utils/business/exchangeError.ts'
)
const failure = {
code: 1202,
data: null,
msg: '该资产已完成过换货,如需再次换货请使用换货后的新资产'
}
const newAsset = { asset_identifier: '89860624590014594318', associated_phones: ['19232713115'] }
for (const value of [failure, { response: { data: failure } }]) {
assert.equal(getExchangePhoneConflictMessage(value), failure.msg)
assert.equal(getExchangePhoneAssociationQuery(value, newAsset), undefined)
}
for (const phone of ['', ' ', null, 19232713115]) {
assert.equal(
getExchangePhoneAssociationQuery({ ...failure, data: { phone } }, newAsset),
undefined
)
}
const page = await readFile('src/views/asset-management/exchange-management/index.vue', 'utf8')
assert.match(page, /if \(!canManageAssociations\)/)
assert.match(page, /ElMessageBox.alert\(conflictMessage, '换货提示'/)
})
test('association preselection excludes old assets, different phones and invalid historical rows', async () => {
const {
getPhoneAssetAssociationTarget,

View File

@@ -178,6 +178,17 @@ export const mockApiPlugin = {
return
}
if (req.method !== 'GET') {
if (path === '/api/admin/exchanges/2/complete') {
res.setHeader('Content-Type', 'application/json; charset=utf-8')
res.end(
JSON.stringify({
code: 1202,
data: null,
msg: '该资产已完成过换货,如需再次换货请使用换货后的新资产'
})
)
return
}
if (path === '/api/admin/phone-asset-associations/batch-unbind') {
let body = ''
req.on('data', (chunk) => {
@@ -213,7 +224,7 @@ export const mockApiPlugin = {
res.end(
JSON.stringify({
code: 1202,
msg: '模拟请求:未调用真实后台',
msg: '新资产已绑定手机号,请先在后台解除该资产的手机号绑定,再重新提交换货申请',
data: { phone: '13800000000' }
})
)
@@ -238,8 +249,11 @@ export const mockApiPlugin = {
)
)
if (path === '/api/admin/orders/1') data = order
if (path === '/api/admin/exchanges') data = page([exchange])
if (path === '/api/admin/exchanges')
data = page([exchange, { ...exchange, id: 2, exchange_no: 'MOCK-EXCHANGE-002' }])
if (path === '/api/admin/exchanges/1') data = exchange
if (path === '/api/admin/exchanges/2')
data = { ...exchange, id: 2, exchange_no: 'MOCK-EXCHANGE-002' }
if (path === '/api/admin/orders') data = page([order])
if (path === '/api/admin/employee-collection-applications/1')
data = { application, allocations: [], attempts: [] }

View File

@@ -2,7 +2,7 @@ type BusinessFailure = {
code?: number | string
msg?: string
message?: string
data?: { phone?: unknown }
data?: { phone?: unknown } | null
}
const getBusinessFailure = (value: unknown): BusinessFailure | undefined => {
@@ -14,8 +14,8 @@ const getBusinessFailure = (value: unknown): BusinessFailure | undefined => {
export const getExchangePhoneConflictMessage = (value: unknown): string | undefined => {
const failure = getBusinessFailure(value)
if (Number(failure?.code) !== 1202) return undefined
const phone = typeof failure?.data?.phone === 'string' ? failure.data.phone.trim() : ''
return `新资产已绑定手机号${phone ? ` ${phone}` : ''},请先到『手机号资产关联』解除后再提交`
// 1202 同时用于旧资产已换货等业务拒绝,必须原样展示后端文案。
return failure?.msg || failure?.message || '换货申请未完成,请检查当前资产是否符合换货条件'
}
export const getExchangeBusinessErrorMessage = (value: unknown): string | undefined => {
@@ -37,11 +37,6 @@ export const getExchangePhoneAssociationQuery = (
if (Number(response?.code) !== 1202) return undefined
const asset_identifier = newAsset.asset_identifier?.trim() || ''
const responsePhone = typeof response?.data?.phone === 'string' ? response.data.phone.trim() : ''
const phone =
responsePhone ||
newAsset.associated_phones
?.find((value) => typeof value === 'string' && value.trim())
?.trim() ||
''
return asset_identifier && phone ? { asset_identifier, phone } : undefined
// 仅响应明确包含 phone 才能视为手机号冲突;不能凭资产历史关联给其他 1202 添加解绑入口。
return asset_identifier && responsePhone ? { asset_identifier, phone: responsePhone } : undefined
}

View File

@@ -472,22 +472,25 @@
)
return
}
const canManageAssociations = hasAuth(AUGUST_PERMISSIONS.phoneAssetAssociation.list)
const associationQuery = getExchangePhoneAssociationQuery(failure, newAsset)
const canManageAssociations =
!!associationQuery && hasAuth(AUGUST_PERMISSIONS.phoneAssetAssociation.list)
try {
await ElMessageBox.confirm(
canManageAssociations
? conflictMessage
: `${conflictMessage}。您暂无管理权限,请联系管理员解除。`,
'新资产手机号绑定冲突',
{
if (!canManageAssociations) {
await ElMessageBox.alert(conflictMessage, '换货提示', {
type: 'warning',
confirmButtonText: canManageAssociations ? '前往手机号资产关联' : '知道了',
cancelButtonText: '暂不处理',
showCancelButton: canManageAssociations,
confirmButtonText: '知道了',
dangerouslyUseHTMLString: false
}
)
})
return
}
await ElMessageBox.confirm(conflictMessage, '新资产手机号绑定冲突', {
type: 'warning',
confirmButtonText: '前往手机号资产关联',
cancelButtonText: '暂不处理',
showCancelButton: true,
dangerouslyUseHTMLString: false
})
if (canManageAssociations)
await router.push({ path: RoutesAlias.PhoneAssetAssociation, query: associationQuery })
} catch {