docs(frontend-bug): 测试报告 3 个工单 /@bug 翻盘转前端 (2026-05-07)

/@bug 实测后这 3 个 Gitea 工单从"后端 BUG"翻盘成前端 BUG,已关闭:

1. #1788 HT-001 合同生成"网络错误"
   → axios interceptor 误把业务码 5xxxxx 当 HTTP 5xx
   → 雪花 ID schemeId 没字符串化 → JS Number 精度丢失

2. #1790 YY-001 退款原因新增失败
   → hl-ui/api/order.js 4 个 API 路径缺 /admin/ 前缀
   → http.post(url, null, {params:data}) 把 body 当 querystring

3. #1792 JQ-027/CT-010/BP-013/YW-013 4 模块批量删除失败
   → hl-ui/api/{scenic,restaurant,spare,playservice}.js 多包一层 data
   → 同 PR #1494 时期 anti-pattern 的反转版本

@mmg 看到请修复,每个 changelog 都附了 round-trip 实测证据。

完整分析报告在 wx/HL repo 的 docs/tasks/20260507_bug_*_analysis.md。
这个提交包含在:
wx 2026-05-07 14:55:25 +08:00
父节点 6f1e0f0b98
当前提交 2b0b64248a
共有 3 个文件被更改,包括 287 次插入0 次删除

查看文件

@ -0,0 +1,107 @@
---
date: 2026-05-07
type: frontend-bug
module: contract-generate
priority: high
notify: ["@mmg"]
status: pending
source: 测试用例汇总.xlsx (2026-05-07) + /@bug 实测
related_cases: [HT-001]
gitea_issue_closed: 1788
---
# 合同生成显示"网络错误" — 实为前端 axios 误判 + 雪花 ID 精度丢失
## 原始现象
测试用例 HT-001合同生成报"网络错误"。
## /@bug 实测结论admin 真 token round-trip
后端 `POST /admin/contract/create-by-scheme` HTTP **全部 200**,5 个 case 测试结果:
| Case | 入参 | 业务 code | 含义 |
|------|------|-----------|------|
| A | DEPOSIT_PAID 订单+schemeId=3 | 510208 | 出行人未补全(业务校验,正确) |
| B | 订单+schemeId=`2046840448906940417`19 位 Long | 510205 | **前端把雪花 ID 序列化为 JS Number,后 4 位精度丢失** |
| C | CONFIRMED 订单+schemeId=3 | **200** | **生成成功**,contractId=2052278173260824577 |
| D | 不存在 orderId | 510207 | 订单不存在(正确) |
**结论**:后端纯净,根因在前端两处。
## 根因 1axios interceptor 误把业务码当 HTTP 5xx
本项目 P0 铁律:**HTTP 始终 200,业务码在 `Result.code`**。
前端 axios interceptor 看到 `code:5xxxxx`(业务码格式 `xxxxxx` 6 位),可能误判为 HTTP 5xx 异常,统一显示"网络错误",**用户根本看不到 body.message** 提示的真实业务原因(如"出行人未补全")。
### 修复方向
```js
// 错误(推断):
axios.interceptors.response.use(resp => {
if (String(resp.data.code).startsWith('5')) {
showToast('网络错误') // ❌ 业务码 510208 被误判为 5xx
throw new Error()
}
return resp.data
})
// 正确:
axios.interceptors.response.use(resp => {
const { code, message, data } = resp.data
if (code !== 200) {
showToast(message || `操作失败 (${code})`) // ✅ 显示后端真实 message
throw new BizError(code, message)
}
return data
})
```
## 根因 2雪花 ID schemeId 没字符串化
后端 schemeId 是 19 位 Long雪花算法,最大值 `2^63-1 ≈ 9.22e18`
JS Number 精度上限 `Number.MAX_SAFE_INTEGER = 2^53-1 ≈ 9.007e15`
19 位 Long 直接被 JS Number 接收会**丢失后 3-4 位精度**,发回后端的 ID 已经不是原值。
### 实测证据case B
```
原值: 2046840448906940417
JS 后: 2046840448906940000 ← 后 4 位变 0
后端: schemeId 不存在 → code:510205
```
### 修复方向
```js
// 1. axios 配置 transformResponse 用 json-bigint 解析
import JSONbig from 'json-bigint'
axios.defaults.transformResponse = [data => JSONbig.parse(data)]
// 2. 或后端 Long 字段 @JsonSerialize(using = ToStringSerializer.class)
// → 但本项目历史决定,前端处理更省事
// 3. 或前端拿到 ID 后立即转 String,传回时也按 String 传
const schemeId = String(resp.schemeId) // 显式字符串化
```
类似已知坑memory `feedback_http-200-check-business-code.md`、项目其他列表用雪花 ID 的地方都需要复检。
## 影响
- 合同生成完全无法触发(用户看到"网络错误"放弃)
- 间接影响所有调用合同接口的页面
## 验收(前端)
- [ ] 合同生成失败时 toast 显示后端真实 message如"出行人未补全"
- [ ] CONFIRMED 订单 + 真实 schemeId → 合同生成成功HTTP 200 + code:200
- [ ] axios 全局检查:所有调用涉及雪花 ID 的接口(订单 ID/产品 ID/方案 ID/合同 ID都按 String 处理
- [ ] 业务码 5xxxxx 不再被误判为网络错误
## 关联工单
Gitea #1788 已关闭并留 closing comment项目规则前端 BUG 不建工单)。
完整分析报告:`docs/tasks/20260507_bug_contract_analysis.md`

查看文件

@ -0,0 +1,85 @@
---
date: 2026-05-07
type: frontend-bug
module: refund-reason-api
priority: high
notify: ["@mmg"]
status: pending
source: 测试用例汇总.xlsx (2026-05-07) + /@bug 实测
related_cases: [YY-001]
gitea_issue_closed: 1790
---
# 退款原因新增失败 — 前端 4 个 API 路径错 + body 当 querystring
## 原始现象
测试用例 YY-001新增退款原因不通过保存失败
## /@bug 实测结论admin 真 token round-trip
后端 `POST /admin/refund-reason/create` 实测:
```
Body: {"reasonName":"测试取消原因","reasonType":"CUSTOMER","sortOrder":1}
HTTP: 200
Response: {"code":200, "data":{"reasonId":2052278650115342338, ...}}
```
**后端完全正常,DB 也已落库。问题在前端 API 调用方式。**
## 根因hl-ui/src/api/order.js
4 个退款原因相关 API 都犯同样 2 个错:
| 行号 | 函数 | 错误 1 | 错误 2 |
|------|------|--------|--------|
| 393 | `createRefundReason()` | 路径缺 `/admin/` 前缀 | `http.post(url, null, { params: data })` 把 body 当 querystring |
| 404 | `updateRefundReason()` | 同上 | 同上 |
| 416 | `deleteRefundReason()` | 同上 | 同上 |
| 424 | `listRefundReason()` | 同上 | (GET 可能影响小) |
### 错误后果
1. **缺 `/admin/` 前缀** → 网关 9443 拿到 `/refund-reason/create` 没匹配路由 → 404
2. **`http.post(url, null, { params })`** → axios 把 `{reasonName: '...'}` 拼到 query string `?reasonName=...`,body 是 null。后端 `@Valid @RequestBody RefundReasonSaveReqVO` 拿到 null → 400 必填字段为空
### 正确写法
```js
// 错 ❌
export function createRefundReason(data) {
return http.post('/refund-reason/create', null, { params: data })
}
// 对 ✅
export function createRefundReason(data) {
return http.post('/admin/refund-reason/create', data)
// ^^^^^^^ ^^^^
// 加 /admin/ body 直接传,不用 params
}
```
## 同源 anti-pattern
跟 memory `experience/backend-dev/frontend-http-delete-double-wrap-data-trap.md` 反向:
- 之前:前端正确给 body,但 wrapper 误把第二参数当 config → 后端拿到空 body
- 这次:前端把数据当 query → 后端 @RequestBody 收到 null
**建议**mmg 用全局搜 `http.post(.*null.*params``http.post(.*\{.*params:` 模式扫一下,这是高风险写法,可能 hl-ui 还有其他历史遗留。
## 影响
- 退款原因 admin 配置无法新增/编辑/删除YY-001 直接症状)
- 列表查询走 GET 可能不受影响,但路径缺前缀也会 404
## 验收(前端)
- [ ] `api/order.js` 4 个退款原因 API 全部加 `/admin/` 前缀 + 把 data 直接传 body
- [ ] 测试服 round-tripadmin 端能新增/修改/删除/列表退款原因
- [ ] 全局 grep `http.post.*null.*params` 模式,发现的同类问题一起修
## 关联工单
Gitea #1790 已关闭并留 closing comment项目规则前端 BUG 不建工单)。
完整分析报告:`docs/tasks/20260507_bug_order_analysis.md`

查看文件

@ -0,0 +1,95 @@
---
date: 2026-05-07
type: frontend-bug
module: resource-batch-delete
priority: high
notify: ["@mmg"]
status: pending
source: 测试用例汇总.xlsx (2026-05-07) + /@bug 实测
related_cases: [JQ-027, CT-010, BP-013, YW-013]
gitea_issue_closed: 1792
---
# 4 个资源模块批量删除失败 — 前端多包一层 data
## 原始现象
测试用例 JQ-027 / CT-010 / BP-013 / YW-013景区/餐厅/备品/游玩项目("物业"实为游玩项目)批量删除时,勾选数据后点删除"系统提示但无法删除"。
## /@bug 实测结论admin 真 token round-trip 双向对照)
| 模块 | "正确 body" | "前端字面 body" | 结论 |
|------|------------|-----------------|------|
| 景区 | `{"scenicIds":[3001,3002]}` → code:200 | `{"data":{"scenicIds":[...]}}` → code:400 字段【scenicIds】不能为空 | 前端多包 |
| 餐厅 | `{"restaurantIds":[...]}` → code:200 | `{"data":{"restaurantIds":[...]}}` → code:400 | 前端多包 |
| 备品 | `{"suppliesIds":[...]}` → code:200 | `{"data":{"suppliesIds":[...]}}` → code:400 | 前端多包 |
| 游玩项目 | `{"playServiceIds":[...]}` → code:200 | `{"data":{"playServiceIds":[...]}}` → code:400 | 前端多包 |
**后端纯净,4 个模块前端 api 文件统一多包了一层 `data`**。
## 根因详情
| 文件:行号 | 现状 |
|----------|------|
| `hl-ui/src/api/scenic.js:421` | `http.delete(url, { data: { scenicIds } })` ❌ |
| `hl-ui/src/api/restaurant.js:78` | `http.delete(url, { data: { restaurantIds } })` ❌ |
| `hl-ui/src/api/spare.js:69` | `http.delete(url, { data: { suppliesIds } })` ❌ |
| `hl-ui/src/api/playservice.js:71` | `http.delete(url, { data: { playServiceIds } })` ❌ |
### 项目 wrapper 行为(`hl-ui/src/utils/request.js:582-583`
项目自定义的 `http.delete(url, data, config)` 第二参数**直接当 body**,不像 axios 原生 `axios.delete(url, { data: body })` 那样从 config.data 取。
所以前端写 `http.delete(url, { data: { scenicIds } })` 时:
- 第二参数是 `{ data: { scenicIds } }`
- wrapper 直接当 body 发出去
- 后端拿到 `{ "data": { "scenicIds": [...] } }`
- 后端 DTO 是 `{ scenicIds: List<Long> }`,找不到顶层 `scenicIds` 字段 → @NotEmpty fail
### 正确写法
```js
// 错 ❌
export function batchDelete(scenicIds) {
return http.delete('/admin/scenic/batch', { data: { scenicIds } })
}
// 对 ✅
export function batchDelete(scenicIds) {
return http.delete('/admin/scenic/batch', { scenicIds })
// ^^^^^^^^^
// 第二参数直接是 body 对象,不要再包 { data: ... }
}
```
## 同源 anti-pattern重要更新
跟 memory `experience/backend-dev/frontend-http-delete-double-wrap-data-trap.md` 是**同一个坑的反转版本**
- **9 天前**PR #1494 时期):项目 wrapper 第二参数当 body,前端按 axios 原生写 `{ data: payload }`,后端拿到多包一层 → 一样的 code:400
- **现在**wrapper 仍然这样行为,但部分模块前端代码似乎曾经改对过又改错回去(或者一开始就错),症状一模一样
**建议 mmg**
1. 修这 4 处当前确认的错误
2. 全局 grep `http\.delete\(.*\{.*data:` / `http\.put\(.*\{.*data:` 扫一遍,看历史遗留
3. 考虑把 wrapper 改成兼容 axios 原生用法(即从 config.data 取)减少这种坑
## 影响
- 4 个资源模块批量删除完全失效
- 单个删除(非批量)通常不走这个路径,可能正常
## 验收(前端)
- [ ] 4 个文件改完,传第二参数直接是 IDs 对象不要包 `{ data: ... }`
- [ ] 测试服 round-trip景区/餐厅/备品/游玩项目 4 模块批量删除 5 条 → 列表 deleted_at 已写
- [ ] 全局 grep 同类 anti-pattern 一起改
## 关联工单
Gitea #1792 已关闭并留 closing comment项目规则前端 BUG 不建工单)。
完整分析报告:`docs/tasks/20260507_bug_resource_analysis.md`
## 顺手另一个共性问题(建议 PM 复核)
/@bug 还发现8 个资源服务hotel/scenic/activity/restaurant/staff/supplies/cost/vehicle`updateStatus` 方法**全部缺 checkResourceInUse 引用计数检查**(只有 `submitApproval` 路径有)。本批 PR 只修景区一个JQ-011 #1794),其余 7 个建议 PM 决策是否扩大修复范围。