diff --git a/openspec/changes/update-october-batch-one-admin-integration/design.md b/openspec/changes/update-october-batch-one-admin-integration/design.md index 209d033..ecf00e6 100644 --- a/openspec/changes/update-october-batch-one-admin-integration/design.md +++ b/openspec/changes/update-october-batch-one-admin-integration/design.md @@ -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 完成实际选择。缓存重入和路由参数变更重置查询,请求序号防止过期响应覆盖;手工筛选/翻页、解绑后刷新清除旧选择。批量结果保持后端布尔契约,仅展示中文「解绑成功/解绑失败」。原查看与批量解绑权限不变。 diff --git a/openspec/changes/update-october-batch-one-admin-integration/implementation.md b/openspec/changes/update-october-batch-one-admin-integration/implementation.md index f08788d..8a16a77 100644 --- a/openspec/changes/update-october-batch-one-admin-integration/implementation.md +++ b/openspec/changes/update-october-batch-one-admin-integration/implementation.md @@ -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、格式、编码及严格规范校验通过。重新做浏览器检查时本地会话回到了资产首页,未完成两种新弹窗的视觉验证;未将此项标为已通过,也未操作真实换货或解绑数据。 diff --git a/openspec/changes/update-october-batch-one-admin-integration/specs/exchange-management/spec.md b/openspec/changes/update-october-batch-one-admin-integration/specs/exchange-management/spec.md index 1d21baf..b53433a 100644 --- a/openspec/changes/update-october-batch-one-admin-integration/specs/exchange-management/spec.md +++ b/openspec/changes/update-october-batch-one-admin-integration/specs/exchange-management/spec.md @@ -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: 创建换货选择新资产后被拒 diff --git a/openspec/changes/update-october-batch-one-admin-integration/tasks.md b/openspec/changes/update-october-batch-one-admin-integration/tasks.md index d19ae4d..d751f76 100644 --- a/openspec/changes/update-october-batch-one-admin-integration/tasks.md +++ b/openspec/changes/update-october-batch-one-admin-integration/tasks.md @@ -66,3 +66,4 @@ - [x] 8.2 关联页预填两项筛选,默认 1 页/20 条,仅勾选匹配有效记录;缓存、参数变化、过期响应和手工筛选清除旧选择。 - [x] 8.3 复用原批量解绑权限、原因与二次确认,结果布尔值改中文成功/失败。 - [x] 8.4 66 项回归测试通过,本地完整跳转、实际勾选、手工取消及模拟批量结果验证通过;未操作真实资产。 +- [x] 8.5 按用户修正区分共用 1202:`data=null` 只展示原始 `msg` 和「知道了」,仅带有效响应手机号才提供去解绑;不从历史数组回退。新增回归,全套 67 项通过。 diff --git a/scripts/tests/octoberBatchOne.test.mjs b/scripts/tests/octoberBatchOne.test.mjs index 65dea25..14b97e5 100644 --- a/scripts/tests/octoberBatchOne.test.mjs +++ b/scripts/tests/octoberBatchOne.test.mjs @@ -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, diff --git a/scripts/tests/octoberBrowserFixtures.mjs b/scripts/tests/octoberBrowserFixtures.mjs index 813d05b..0bc12ee 100644 --- a/scripts/tests/octoberBrowserFixtures.mjs +++ b/scripts/tests/octoberBrowserFixtures.mjs @@ -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: [] } diff --git a/src/utils/business/exchangeError.ts b/src/utils/business/exchangeError.ts index 26e137f..21c295d 100644 --- a/src/utils/business/exchangeError.ts +++ b/src/utils/business/exchangeError.ts @@ -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 } diff --git a/src/views/asset-management/exchange-management/index.vue b/src/views/asset-management/exchange-management/index.vue index 2a834ef..71ac1fe 100644 --- a/src/views/asset-management/exchange-management/index.vue +++ b/src/views/asset-management/exchange-management/index.vue @@ -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 {