This commit is contained in:
@@ -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 完成实际选择。缓存重入和路由参数变更重置查询,请求序号防止过期响应覆盖;手工筛选/翻页、解绑后刷新清除旧选择。批量结果保持后端布尔契约,仅展示中文「解绑成功/解绑失败」。原查看与批量解绑权限不变。
|
||||
|
||||
|
||||
@@ -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、格式、编码及严格规范校验通过。重新做浏览器检查时本地会话回到了资产首页,未完成两种新弹窗的视觉验证;未将此项标为已通过,也未操作真实换货或解绑数据。
|
||||
|
||||
@@ -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: 创建换货选择新资产后被拒
|
||||
|
||||
|
||||
@@ -66,3 +66,4 @@
|
||||
- [x] 8.2 关联页预填两项筛选,默认 1 页/20 条,仅勾选匹配有效记录;缓存、参数变化、过期响应和手工筛选清除旧选择。
|
||||
- [x] 8.3 复用原批量解绑权限、原因与二次确认,结果布尔值改中文成功/失败。
|
||||
- [x] 8.4 66 项回归测试通过,本地完整跳转、实际勾选、手工取消及模拟批量结果验证通过;未操作真实资产。
|
||||
- [x] 8.5 按用户修正区分共用 1202:`data=null` 只展示原始 `msg` 和「知道了」,仅带有效响应手机号才提供去解绑;不从历史数组回退。新增回归,全套 67 项通过。
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: [] }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user