比较提交

...

1 次代码提交

作者 SHA1 备注 提交日期
API Changelog Bot
7ec9de8c3b docs(fleet): hand off itinerary shortlink contract (#5245) 2026-07-25 09:48:37 +08:00
共有 1438 个文件被更改,包括 187 次插入301373 次删除

查看文件

@ -1,46 +0,0 @@
name: changelog-filename-gate
on:
workflow_dispatch:
pull_request:
branches:
- main
types:
- opened
- reopened
- synchronize
- edited
push:
branches:
- main
jobs:
validate:
name: validate
runs-on: ubuntu-latest
steps:
# Keep the gate self-contained: the test environment cannot reliably clone GitHub Actions repositories.
- name: Checkout full history
run: |
git init -q .
git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
git fetch --no-tags --prune origin \
"+refs/heads/*:refs/remotes/origin/*" \
"+refs/pull/*/head:refs/remotes/pull/*/head" \
"+refs/pull/*/merge:refs/remotes/pull/*/merge"
git checkout --detach "$GITHUB_SHA"
- name: Verify Node.js runtime
run: |
node --version
npm --version
node -e "if (Number(process.versions.node.split('.')[0]) < 20) process.exit(1)"
- name: Run regression tests
run: npm test
- name: Validate new changelog filenames
run: npm run check:filenames -- --event "$GITHUB_EVENT_PATH"
- name: Validate changelog frontmatter
run: npm run check:frontmatter -- --event "$GITHUB_EVENT_PATH"

文件差异内容过多而无法显示 加载差异

文件差异内容过多而无法显示 加载差异

文件差异内容过多而无法显示 加载差异

文件差异内容过多而无法显示 加载差异

文件差异内容过多而无法显示 加载差异

文件差异内容过多而无法显示 加载差异

文件差异内容过多而无法显示 加载差异

文件差异内容过多而无法显示 加载差异

文件差异内容过多而无法显示 加载差异

文件差异内容过多而无法显示 加载差异

文件差异内容过多而无法显示 加载差异

文件差异内容过多而无法显示 加载差异

文件差异内容过多而无法显示 加载差异

文件差异内容过多而无法显示 加载差异

文件差异内容过多而无法显示 加载差异

查看文件

@ -1,75 +0,0 @@
# 后端 API Changelog 推送说明
接口发生新增、修改或删除时,在 `hl-api-changelog` 仓库提交一份 changelog。
## 1. 放在哪里
- 管理后台:`changelogs-v2/YYYY-MM/`
- 小程序:`changelogs-v2-mp/YYYY-MM/`
文件名:
```text
DD_issue_业务标题-{新增接口|修改接口|删除接口}-{管理后台|小程序端}.md
```
例如:
```text
changelogs-v2/2026-07/24_5205_车务首页汇总状态补全-修改接口-管理后台.md
```
日期使用提交时的上海日期;不要把“前端待处理”“已完成”等状态写进文件名。
## 2. 写什么
可以复制仓库根目录的 `CHANGELOG_TEMPLATE.md`,至少写清:
- 关联的 Issue 和后端 PR;
- 接口路径和 HTTP 方法;
- 新增、修改或删除的请求/响应字段;
- 字段必填性、枚举、状态、空值、金额和兼容规则;
- 前端需要做什么;
- 后端测试、部署和网关验证结果。
元数据中:
```yaml
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
```
- 需要前端修改:`frontend_status: "pending"`
- 不需要前端修改:`frontend_status: "not_required"`
- 后端不要代替前端填写 `implemented``released``verified`
## 3. 校验
`hl-api-changelog` 仓库执行:
```powershell
npm test
npm run check:filenames -- --base origin/main --head HEAD
npm run check:frontmatter -- --base origin/main --head HEAD
```
确保正文没有 `TODO``待补充` 或模板占位符。
## 4. 提交和推送
只暂存本次 changelog 文件:
```powershell
git status --short
git add -- changelogs-v2/2026-07/24_5205_车务首页汇总状态补全-修改接口-管理后台.md
git diff --cached --check
git commit -m "docs: hand off API contract (#5205)"
git push -u origin <任务分支>
```
然后向 `main` 创建 PR。不要提交其他任务的 changelog、`.tmp-*` 文件或任何凭据。

查看文件

@ -1,195 +0,0 @@
---
schema: "hl-changelog/v2"
ticket: "{issue-no}"
title: "{一句话概括变化}"
consumer: "{admin|mp|internal|multiple}"
change_type: "{新增接口|修改接口|删除接口}"
backend_status: "pending"
gateway_status: "pending"
frontend_status: "{pending|not_required}"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: ""
updated_at: "YYYY-MM-DD"
base: "{dev|dev-v3}"
---
# {模块名}: {一句话概括变化}
> **存放目录**:
> - 一期(v2,无 `order-v3` 标签的工单)→ `changelogs/{YYYY-MM}/`
> - 二期(v3,`order-v3` 标签的工单)→ `changelogs-v2/{YYYY-MM}/`
>
> **服务**: hl-{服务名} (端口 80XX)
> **PR**: #{pr-no}
> **Issue**: #{issue-no}
> **日期**: YYYY-MM-DD
> **影响范围**: {只写一行,比如"管理端产品编辑订金表单" 或 "C 端行程详情三字段"}
---
## ⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写)
一行红字说清:
- 本次变了什么
- 前端/调用方以前以为的是什么
- 实际现在是什么
示例: "前一版 #903 说 4 个接口会对 CUSTOM 返回 500。**这个判断是错的,现已撤销**。"
---
## 一、背景(选填)
业务原因。纠错/撤销时尤其应该用 **DB 实证****实际数据** 驳回上一版的误判。
| 维度 | 证据 A | 证据 B |
|------|--------|--------|
| 档位数 | 1 档 | 2 档 |
| 价格日历记录 | 62 条 | 92 条 |
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 保存产品基础信息 | POST | `/admin/product-basic/save` | 请求体新增校验 | 字段互斥 |
---
## 三、接口详情
### 1. {接口名} `{METHOD} {路径}`
**VO**: `{VO 类名}`
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| xxx | Body | String | ✅ | - | - |
#### 出参 `Result<XxxRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| xxx | String | - |
#### 请求示例
```json
{ "xxx": "yyy" }
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": { "xxx": "yyy" },
"success": true
}
```
#### 空数据 / 降级响应
```json
{ "code": 200, "data": [], "success": true }
```
#### 错误响应
```json
{
"code": 400,
"message": "depositMutex: 订金固定金额与比例互斥,只能填写其中一个",
"success": false,
"data": null
}
```
---
## 四、契约约束与正确调用方式(选填,字段互斥/联动/切换场景必写)
> 本节只写**后端接受/拒绝 payload 的规则**,不写 UI 渲染建议。
### ✅ 正确 / ❌ 错误 payload 对照
| 场景 | payload |
|------|---------|
| ✅ 单独填固定金额 | `{ "depositAmount": 500, "depositRatio": null }` |
| ✅ 单独填比例 | `{ "depositAmount": null, "depositRatio": 10 }` |
| ✅ 全款(无订金) | `{ "paymentType": "FULL", "depositAmount": null, "depositRatio": null }` |
| ❌ 两字段同时有值 | `{ "depositAmount": 500, "depositRatio": 10 }` → 400 |
### 切换状态时的必要动作
若字段有"要么 A 要么 B"的互斥关系,请求前必须把对方字段显式置 null,不要依赖"隐藏输入框"的 UI 行为(后端只看 payload)。
---
## 五、数据库行为(涉及写操作时必写)
| 前端提交 | `deposit_amount` 列 | `deposit_ratio` 列 |
|----------|---------------------|---------------------|
| `depositAmount=500, depositRatio=null` | `500.00` | `NULL` |
| `depositAmount=null, depositRatio=10` | `NULL` | `10` |
**显式 SET NULL 说明**: 即使前端不传"对方字段", 后端也会显式 `SET column = NULL`, 不保留数据库历史残留值。
---
## 六、边界行为
- 未登录 → 401 (网关拦截)
- 资源不存在 → 404
- 下游服务降级 → 返 `[]` / null, 不 500 不阻断页面
- 老数据兼容 → 旧 snapshot 无新字段 → 字段为 null, 不异常
---
## 七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
- **仅影响**: 管理后台 X 表单
- **零影响**:
- C 端算价接口
- 订单创建接口
- 订单详情读取
- 历史数据(存量不迁移, 下次编辑保存时才触发新校验)
---
## 八、测试环境已验证
真实接口 curl/DBeaver 输出, 带 ✓ 标记:
```
GET /mp/product/{id}/price-calendar?month=2026-04 → 200 + days 数组 ✓
GET /mp/product/{id}/tier-compare → 200 + tiers ✓
POST /mp/product/{id}/quote → 200 + 报价成功 ✓
```
验证产品: `productId=2045390643479412737` (测试定制wx-01)
---
## 九、相关历史 PR纠错 / 功能演进时必写)
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| #900 | #898 | 首次放行主详情 | ✅ 有效 |
| #903 | #901 | 误判定性 CUSTOM 不支持 | ❌ 已被 #906 撤销 |
| **本 PR #906** | **#905** | 撤销 #903, 全量放行 | ✅ 最新 |
---
## 十、相关文档
- 关联 Issue: [wx/HL#{issue}](https://git.1814.love:8443/wx/HL/issues/{issue})
- 关联 PR: [wx/HL#{pr}](https://git.1814.love:8443/wx/HL/pulls/{pr})
- 后续计划: 见 `{另一条 changelog 路径}`

查看文件

@ -1,89 +0,0 @@
# Changelog 贡献规则
## 二期文件名
`/v3/admin/*` 接口写入 `changelogs-v2/`
```text
changelogs-v2/YYYY-MM/DD_issue_业务标题-{新增接口|修改接口|删除接口}-管理后台.md
```
`/v3/mp/*` 接口写入 `changelogs-v2-mp/`
```text
changelogs-v2-mp/YYYY-MM/DD_issue_业务标题-{新增接口|修改接口|删除接口}-小程序端.md
```
其中:
- `YYYY-MM``DD` 必须是校验运行时 `Asia/Shanghai` 的真实年月日,且均须补齐两位。跨越上海零点后仍未合并的 PR,需要把新文件重命名为当天日期。
- `issue` 必须是不带 `#` 的十进制正整数,不允许 `0`、负数或前缀符号。
- 业务标题不能为空。
- 变更类型只能是 `新增接口``修改接口``删除接口`
- 端类型由目录唯一决定:`changelogs-v2/` 固定为 `管理后台``changelogs-v2-mp/` 固定为 `小程序端`
- 同一改动同时影响 `/v3/admin/*``/v3/mp/*` 时,应按目录拆成两份。
一期 `changelogs/` 沿用现行格式,不套用上述强制模板。
## 校验范围
检测器读取 `git diff --name-status -z --find-renames` 的结果,只校验本次 diff 新出现的目标路径:
- `A`(新增)、`C`(复制)和 `R`(重命名)的目标路径必须通过规则。
- `M`(修改历史文件)和 `D`(删除)豁免,不会因存量错误命名阻断。
- 重命名到受控目录时,新目标路径必须使用校验当天的上海日期。
本地校验:
```bash
npm test
npm run check:filenames -- --base origin/main --head HEAD
```
生产 CLI 故意不提供 `--date` 或日期环境变量;测试只通过导出的纯函数注入 `Date`。规则失败返回退出码 `1`,Git/事件/参数等基础设施错误返回 `2`
## 前端消费状态
新增二期 changelog 必须使用 `hl-changelog/v2` YAML Front Matter。状态只写在元数据中,不写入文件名
```yaml
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "pending"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
updated_at: "2026-07-24"
```
前端状态正常流转为:
```text
pending → claimed → implemented → released → verified
```
不需要前端修改时使用 `not_required`。字段一致性、必填证据和新增文档 frontmatter 由 `check:frontmatter` 校验。
完整职责和命令见:
- `FRONTEND_CONSUMPTION_STATUS_GUIDE.md`
- `BACKEND_CHANGELOG_DELIVERY_GUIDE.md`
本地校验:
```bash
npm test
npm run check:filenames -- --base origin/main --head HEAD
npm run check:frontmatter -- --base origin/main --head HEAD
```
## CI 与服务端阻断边界
Gitea Actions 会在指向 `main` 的 PR 和 `main` 的 push 上运行回归测试与文件名检测。该 workflow 是检测器:
- 在 `main` 未开启分支保护和 required status 时,失败状态不能硬性阻止合并。
- `push` 事件发生在写入之后,只能检测/报警,不能撤销直推。
- 只有管理员另行保护 `main`、关闭直推,并在 workflow 首次成功运行后,从 Gitea 最近上报的 status context 列表中选择实际值作为 required status,才能宣称服务端 hard gate 已激活。激活记录必须保存首次运行链接和 status API/分支保护回读证据;不得预设 job id/name `validate` 就是 Gitea 实际上报的 context。
因此,本仓库文件交付的准确表述是:**detector 已安装;在 `main` 未保护时,服务端 hard gate 尚未激活。**

查看文件

@ -1,109 +0,0 @@
# API Changelog 前端消费状态协作说明
## 可直接转发给前端的通知
API changelog 从 `hl-changelog/v2` 开始记录前端消费进度。后端交接时会填写:
```yaml
backend_status: "deployed"
frontend_status: "pending"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
```
请前端在领取、实现、发布和页面验证时更新对应状态,并填写可追溯的前端 PR、提交或发布版本。
这不会把前端工作纳入后端工单验收,也不要求在 `hl-ui` 创建配合工单。它只用于区分:
- 后端接口是否已经部署并验证;
- 前端是否已经领取;
- 前端代码是否已经实现;
- 页面是否已经发布并验证。
只有 `frontend_status: "verified"` 才表示用户页面形成完整闭环。
## 状态流转
```text
pending → claimed → implemented → released → verified
```
不需要前端修改时:
```text
not_required
```
| 状态 | 含义 | 必填证据 |
|---|---|---|
| `not_required` | 不需要前端修改 | 不填写前端负责人、引用和版本 |
| `pending` | 等待前端领取 | 无 |
| `claimed` | 前端已领取 | `frontend_owner` |
| `implemented` | 前端代码已实现 | `frontend_owner``frontend_ref` |
| `released` | 已发布 | 再填写 `target_release` |
| `verified` | 页面已验证 | 再填写 `verified_at` |
跨级迁移会被自动校验拒绝。状态回退或改为/取消 `not_required` 时必须填写原因。
## 更新命令
领取:
```powershell
hl changelog transition 5205 D:/path/changelog.md claimed `
--owner frontend-team --write
```
实现:
```powershell
hl changelog transition 5205 D:/path/changelog.md implemented `
--owner frontend-team `
--frontend-ref "mmg/hl-ui@abc1234" `
--write
```
发布:
```powershell
hl changelog transition 5205 D:/path/changelog.md released `
--target-release "test-2026.07.24" `
--write
```
验证:
```powershell
hl changelog transition 5205 D:/path/changelog.md verified `
--verified-at "2026-07-24" `
--write
```
命令默认只预览;只有 `--write` 才修改文件。写入命令会自动获取 `changelog` 单写租约。
## 职责边界
后端负责:
- 完成后端测试、部署和网关验证;
- 初始化 v2 元数据;
- 需要前端时设置 `pending`,不需要时设置 `not_required`
- 不替前端填写 `implemented``released``verified`
前端负责:
- 领取时填写负责人;
- 实现后填写前端引用;
- 发布后填写目标版本或环境;
- 页面验证后填写验证日期。
QA 或产品可以协助更新 `verified_at`,但必须基于实际页面验证,不能只根据接口成功或代码已合并标记完成。
## 存量文档
- 新 changelog 全部使用 `hl-changelog/v2`
- `hl-changelog/v1` 继续可读和索引,不强制一次性迁移。
- 文件名带“前端待处理”不代表真实状态;需要继续流转时补充 v2 元数据。
- 不通过重命名表达消费状态,避免破坏文件名校验和历史链接。

查看文件

@ -1,15 +0,0 @@
# changelogs-v2-mp/
二期v3**小程序端** changelog 目录。
| 接口路径 | 写到 |
|---|---|
| `/v3/mp/*` | 本目录changelogs-v2-mp/ |
| `/v3/admin/*` | `../changelogs-v2/` |
| 非 `/v3/*`(一期 admin / mp | `../changelogs/` |
文件命名:`{YYYY-MM}/{DD}_{issue号}_{业务标题}-{变更类型}-小程序端.md`
- 变更类型三选一:新增接口 / 修改接口 / 删除接口
- 端类型固定写「小程序端」
详见 HL 项目 `.claude/CLAUDE.md` § Changelog / 通知约定。

查看文件

@ -1,244 +0,0 @@
# 景区新增「是否收费」字段(小程序端)
- **端类型**:小程序端
- **变更类型**:修改接口
- **日期**2026-06-01
- **Issue**[#3315](https://git.1814.love:8443/wx/HL/issues/3315)
- **PR**[#3316](https://git.1814.love:8443/wx/HL/pulls/3316)
- **Commit**[63539c3](https://git.1814.love:8443/wx/HL/commit/63539c3dd2435c0e3d499302e53877944311e27b)
- **后端负责人**腰苏图yst@1814.love
---
## ① 接口背景
景区资源新增「是否收费」属性。小程序端的景区详情接口和列表接口均已同步透传该字段,供小程序展示景区是否需要付费(如收取门票费)。字典来源 `sys_yes_no`,后端已配置并完成翻译,小程序无需任何字典配置。上游依赖 #3313/#3314resource-service 新增字段)。
---
## ② 变更清单
| 接口 | 方法 | 路径 | 变更内容 |
|------|------|------|----------|
| 景区详情 | GET | `/mp/scenic/{scenicId}` | 出参新增 `isCharged` + `isChargedLabel` |
| 景区列表 | GET | `/mp/scenic/list` | 出参新增 `isCharged` + `isChargedLabel` |
所有变更均为**向后兼容新增字段**,小程序现有调用代码无需修改。
---
## ③ 接口详情
| 项目 | 说明 |
|------|------|
| **认证方式** | 小程序 JWT Token,请求头 `Authorization: Bearer {mp_token}` |
| **幂等性** | GET 查询,天然幂等 |
| **限流** | 无单独限流配置,走网关全局限流 |
| **缓存** | 详情接口缓存 300 秒(`scenic:detail:{id}`),列表接口缓存 600 秒(`scenic:list:{keyword}:{city}:{page}:{pageSize}`)。新字段随正常缓存刷新即可,无需额外处理 |
| **权限** | 小程序用户,已登录状态 |
---
## ④ 接口入参
### GET `/mp/scenic/{scenicId}` 详情
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| scenicId | Long | 是 | 景区 ID路径参数 |
### GET `/mp/scenic/list` 列表
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| keyword | String | 否 | - | 关键词搜索 |
| city | String | 否 | - | 城市筛选 |
| page | Integer | 否 | 1 | 页码 |
| pageSize | Integer | 否 | 20 | 每页条数 |
> 入参无变化,以上为完整入参文档。
---
## ⑤ 出参字段
### GET `/mp/scenic/{scenicId}` 详情(`MpScenicDetailVO`,新增字段)
| 字段名 | 类型 | 说明 | 可能为 null |
|--------|------|------|-------------|
| isCharged | Integer | 是否收费,1=是 0=否 | 否DB 默认 0 |
| isChargedLabel | String | 是否收费中文文本,取自字典 sys_yes_no"是"/"否" | 是(字典缺失时为 null |
### GET `/mp/scenic/list` 列表(`MpScenicListItemVO`,新增字段)
| 字段名 | 类型 | 说明 | 可能为 null |
|--------|------|------|-------------|
| isCharged | Integer | 是否收费,1=是 0=否 | 否DB 默认 0 |
| isChargedLabel | String | 是否收费中文文本,取自字典 sys_yes_no"是"/"否" | 是(字典缺失时为 null |
---
## ⑥ 枚举 / 数据字典
### sys_yes_no 字典(后端已配置,小程序无需维护)
| 字典值isCharged | 中文标签isChargedLabel | 说明 |
|--------------------|-----------------------------|------|
| 1 | 是 | 该景区收费 |
| 0 | 否 | 该景区免费 |
> `isChargedLabel` 由后端查字典自动翻译并返回,小程序可直接展示。若字典配置异常(不正常情况),该字段返回 null。
---
## ⑦ 错误码
| 错误码 | HTTP 状态 | 场景 |
|--------|-----------|------|
| 401 | 401 | 未登录或 Token 失效 |
| 404 | 404 | 景区 ID 不存在、已删除或已下架 |
---
## ⑧ 示例
### 8.1 典型成功 — 获取收费景区详情
**请求**
```
GET /mp/scenic/1900123456789000001
Authorization: Bearer {mp_token}
```
**响应**
```json
{
"code": 200,
"msg": "成功",
"data": {
"scenicId": "1900123456789000001",
"name": "呼伦贝尔大草原景区",
"cityName": "呼伦贝尔",
"coverUrl": "https://oss.example.com/scenic/cover.jpg",
"isCharged": 1,
"isChargedLabel": "是",
"rating": 4.8,
"status": 1,
"mapImageUrl": "https://restapi.amap.com/v3/staticmap?..."
}
}
```
### 8.2 边界情况 — 历史景区isCharged 默认 0,列表返回"否"
**请求**
```
GET /mp/scenic/list?page=1&pageSize=20
Authorization: Bearer {mp_token}
```
**响应**(列表含收费和免费景区,均有 isCharged/isChargedLabel
```json
{
"code": 200,
"msg": "成功",
"data": {
"total": 2,
"list": [
{
"scenicId": "1900123456789000001",
"name": "呼伦贝尔大草原景区",
"isCharged": 1,
"isChargedLabel": "是"
},
{
"scenicId": "1900000000000000001",
"name": "某历史免费景区",
"isCharged": 0,
"isChargedLabel": "否"
}
]
}
}
```
### 8.3 业务失败 — 访问不存在的景区
**请求**
```
GET /mp/scenic/9999999999999999999
Authorization: Bearer {mp_token}
```
**响应**404,景区不存在
```json
{
"code": 404,
"msg": "景区不存在",
"data": null
}
```
---
## ⑨ 业务边界
**适用场景**
- 小程序景区详情页展示"是否收费"标签(如"免费"/"付费"徽标)
- 小程序景区列表卡片可展示收费状态,帮助用户快速识别
**不适用场景**
- `isCharged` 仅为展示属性,不影响小程序下单流程或费用计算
- 不可作为用户端过滤条件(列表接口不支持按 `isCharged` 筛选)
**特殊边界**
- 历史存量景区PR #3314 上线前创建)`isCharged` 默认为 0`isChargedLabel` 返回 "否"
- 两个接口均有 BFF 缓存,景区数据更新后缓存最多延迟 5 分钟(详情)/ 10 分钟(列表)才生效
---
## ⑩ 修改前后对比
### 字段级对比
**出参(详情 + 列表)**
| 字段名 | 变更前 | 变更后 |
|--------|--------|--------|
| isCharged | 不存在 | ✨ 新增,Integer,1=是/0=否 |
| isChargedLabel | 不存在 | ✨ 新增,String,字典翻译文本,可能为 null |
### 行为级对比
| 行为 | 变更前 | 变更后 |
|------|--------|--------|
| 景区详情接口 | 无收费信息 | 新增 isCharged + isChargedLabel |
| 景区列表接口 | 无收费信息 | 新增 isCharged + isChargedLabel |
---
## ⑪ 影响评估 / 回滚
**破坏兼容性**:无,新增字段,现有小程序代码可忽略不用。
**前端同步上线**:无强制要求,可选接入展示。
**回滚方案**:仅 VO 新增字段,无 DDL 变更;回滚只需回退 hl-mp-service 即可,`isCharged``isChargedLabel` 字段从响应中消失,小程序侧字段不存在时不展示即可。
---
## ⑫ 注意事项
1. 小程序**无需**配置 `sys_yes_no` 字典,后端已完成翻译并在 `isChargedLabel` 中直接返回中文
2. 若不需要展示该字段,忽略即可,不影响任何现有功能
3. 受缓存影响,运营后台修改后小程序侧缓存最多 5-10 分钟后自动刷新,属正常现象
---
## ⑬ 关联 / 联系人
- **Issue**[https://git.1814.love:8443/wx/HL/issues/3315](https://git.1814.love:8443/wx/HL/issues/3315)
- **PR**[https://git.1814.love:8443/wx/HL/pulls/3316](https://git.1814.love:8443/wx/HL/pulls/3316)
- **Commit**[https://git.1814.love:8443/wx/HL/commit/63539c3dd2435c0e3d499302e53877944311e27b](https://git.1814.love:8443/wx/HL/commit/63539c3dd2435c0e3d499302e53877944311e27b)
- **上游依赖**[https://git.1814.love:8443/wx/HL/issues/3313](https://git.1814.love:8443/wx/HL/issues/3313) / [https://git.1814.love:8443/wx/HL/pulls/3314](https://git.1814.love:8443/wx/HL/pulls/3314)
- **后端负责人**:腰苏图(企微 / yst@1814.love

查看文件

@ -1,249 +0,0 @@
# 餐厅新增「是否收费」字段(小程序端)
- **端类型**:小程序端
- **变更类型**:修改接口
- **日期**2026-06-01
- **Issue**[#3317](https://git.1814.love:8443/wx/HL/issues/3317)
- **PR**[#3318](https://git.1814.love:8443/wx/HL/pulls/3318)
- **Commit**[3ad5860](https://git.1814.love:8443/wx/HL/commit/3ad5860883819058a58ec384de0c5aac7c918804)
- **后端负责人**腰苏图yst@1814.love
---
## ① 接口背景
餐厅资源新增「是否收费」属性。小程序端的餐厅详情接口和列表接口均已同步透传该字段,供小程序展示餐厅是否需要付费(如在餐厅卡片或详情页展示"收费"/"免费"标签)。字典来源 `sys_yes_no`,后端已配置并完成翻译,小程序无需任何字典配置。
---
## ② 变更清单
| 接口 | 方法 | 路径 | 变更内容 |
|------|------|------|----------|
| 餐厅详情 | GET | `/mp/restaurant/{restaurantId}` | 出参新增 `isCharged` + `isChargedLabel` |
| 餐厅列表 | GET | `/mp/restaurant/list` | 出参新增 `isCharged` + `isChargedLabel` |
所有变更均为**向后兼容新增字段**,小程序现有调用代码无需修改。
---
## ③ 接口详情
| 项目 | 说明 |
|------|------|
| **认证方式** | 小程序 JWT Token,请求头 `Authorization: Bearer {mp_token}` |
| **幂等性** | GET 查询,天然幂等 |
| **限流** | 无单独限流配置,走网关全局限流 |
| **缓存** | 详情接口缓存 300 秒(`restaurant:detail:{id}`),列表接口缓存 600 秒(`restaurant:list:{keyword}:{city}:{page}:{pageSize}`)。新字段随正常缓存刷新即可,无需额外处理 |
| **权限** | 小程序用户,已登录状态 |
---
## ④ 接口入参
### GET `/mp/restaurant/{restaurantId}` 详情
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| restaurantId | Long | 是 | 餐厅 ID路径参数 |
### GET `/mp/restaurant/list` 列表
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| keyword | String | 否 | - | 关键词搜索 |
| city | String | 否 | - | 城市筛选 |
| page | Integer | 否 | 1 | 页码 |
| pageSize | Integer | 否 | 20 | 每页条数 |
> 入参无变化,以上为完整入参文档。
---
## ⑤ 出参字段
### GET `/mp/restaurant/{restaurantId}` 详情(`MpRestaurantDetailVO`,新增字段)
| 字段名 | 类型 | 说明 | 可能为 null |
|--------|------|------|-------------|
| isCharged | Integer | 是否收费,1=是 0=否 | 否DB 默认 0 |
| isChargedLabel | String | 是否收费中文文本,取自字典 sys_yes_no"是"/"否" | 是(字典缺失时为 null |
### GET `/mp/restaurant/list` 列表(`MpRestaurantListItemVO`,新增字段)
| 字段名 | 类型 | 说明 | 可能为 null |
|--------|------|------|-------------|
| isCharged | Integer | 是否收费,1=是 0=否 | 否DB 默认 0 |
| isChargedLabel | String | 是否收费中文文本,取自字典 sys_yes_no"是"/"否" | 是(字典缺失时为 null |
---
## ⑥ 枚举 / 数据字典
### sys_yes_no 字典(后端已配置,小程序无需维护)
| 字典值isCharged | 中文标签isChargedLabel | 建议小程序展示 |
|--------------------|-----------------------------|----------------|
| 1 | 是 | 收费 / 付费 |
| 0 | 否 | 免费 |
> `isChargedLabel` 由后端查字典自动翻译并返回,小程序可直接展示。若字典配置异常(不正常情况),该字段返回 null,建议展示"-"或不显示该标签。
---
## ⑦ 错误码
| 错误码 | HTTP 状态 | 场景 |
|--------|-----------|------|
| 401 | 401 | 未登录或 Token 失效 |
| 404 | 404 | 餐厅 ID 不存在、已删除或已下架 |
---
## ⑧ 示例
### 8.1 典型成功 — 获取收费餐厅详情
**请求**
```
GET /mp/restaurant/1927000000000001
Authorization: Bearer {mp_token}
```
**响应**
```json
{
"code": 200,
"msg": "成功",
"data": {
"restaurantId": "1927000000000001",
"name": "云端藏餐",
"subtitle": "正宗藏式风味体验",
"categoryCode": "TIBETAN",
"categoryName": "藏式",
"isCharged": 1,
"isChargedLabel": "是",
"status": 1,
"cityName": "拉萨",
"pricePerPerson": 88.00,
...
}
}
```
### 8.2 边界情况 — 免费餐厅列表isChargedLabel 返回"否"
**请求**
```
GET /mp/restaurant/list?city=拉萨&page=1&pageSize=20
Authorization: Bearer {mp_token}
```
**响应**(列表中包含收费和免费餐厅,均有 isCharged/isChargedLabel
```json
{
"code": 200,
"msg": "成功",
"data": {
"total": 2,
"list": [
{
"restaurantId": "1927000000000001",
"name": "云端藏餐",
"isCharged": 1,
"isChargedLabel": "是",
...
},
{
"restaurantId": "1927000000000002",
"name": "团队免费体验餐",
"isCharged": 0,
"isChargedLabel": "否",
...
}
]
}
}
```
### 8.3 业务失败 — 访问已下架餐厅
**请求**
```
GET /mp/restaurant/1927000000000099
Authorization: Bearer {mp_token}
```
**响应**404,餐厅不存在或下架
```json
{
"code": 404,
"msg": "餐厅不存在",
"data": null
}
```
---
## ⑨ 业务边界
**适用场景**
- 小程序餐厅详情页展示"是否收费"标签(如"免费"/"付费"徽标)
- 小程序餐厅列表卡片可展示收费状态,帮助用户快速识别
**不适用场景**
- `isCharged` 仅为展示属性,不影响小程序下单流程或费用计算
- 不可作为用户端过滤条件(列表接口不支持按 `isCharged` 筛选)
**特殊边界**
- 历史存量餐厅PR #3318 上线前创建)`isCharged` 默认为 0,`isChargedLabel` 返回 "否"
- `isChargedLabel` 为 null 属于后台字典配置异常,小程序侧可用 isCharged 自行映射1→"是",0→"否"
- 该字段受缓存影响:管理后台修改 isCharged 后,小程序侧缓存最多 5-10 分钟后自动刷新(详情 300s / 列表 600s
---
## ⑩ 修改前后对比
### 字段级对比
**出参(详情 + 列表)**
| 字段名 | 变更前 | 变更后 |
|--------|--------|--------|
| isCharged | 不存在 | ✨ 新增,Integer,1=是/0=否 |
| isChargedLabel | 不存在 | ✨ 新增,String,字典翻译文本,可能为 null |
### 行为级对比
| 行为 | 变更前 | 变更后 |
|------|--------|--------|
| 餐厅详情接口 | 无收费信息 | 新增 isCharged + isChargedLabel |
| 餐厅列表接口 | 无收费信息 | 新增 isCharged + isChargedLabel |
---
## ⑪ 影响评估 / 回滚
**破坏兼容性**:无,新增字段,现有小程序代码可忽略不用。
**前端同步上线**:无强制要求,可选接入展示。建议在餐厅卡片/详情增加"免费"/"收费"标签。
**回滚方案**:若后端回滚,`isCharged``isChargedLabel` 字段会从响应中消失,小程序需做防御性处理(字段不存在时不展示)。
---
## ⑫ 注意事项
1. 小程序**无需**配置 `sys_yes_no` 字典,后端已完成翻译并在 `isChargedLabel` 中直接返回中文
2. 若不需要展示该字段,忽略即可,不影响任何现有功能
3. 建议小程序对 `isChargedLabel` 做 null 防御(`isChargedLabel ?? isCharged === 1 ? '是' : '否'`
4. 受缓存影响,运营后台修改后小程序最长延迟 10 分钟可见,属正常现象
---
## ⑬ 关联 / 联系人
- **Issue**[https://git.1814.love:8443/wx/HL/issues/3317](https://git.1814.love:8443/wx/HL/issues/3317)
- **PR**[https://git.1814.love:8443/wx/HL/pulls/3318](https://git.1814.love:8443/wx/HL/pulls/3318)
- **Commit**[https://git.1814.love:8443/wx/HL/commit/3ad5860883819058a58ec384de0c5aac7c918804](https://git.1814.love:8443/wx/HL/commit/3ad5860883819058a58ec384de0c5aac7c918804)
- **后端负责人**:腰苏图(企微 / yst@1814.love

查看文件

@ -1,243 +0,0 @@
# 酒店新增「是否收费」字段(小程序端)
- **端类型**:小程序端
- **变更类型**:修改接口
- **日期**2026-06-01
- **Issue**[#3325](https://git.1814.love:8443/wx/HL/issues/3325)
- **PR**[#3326](https://git.1814.love:8443/wx/HL/pulls/3326)
- **Commit**[58afda7](https://git.1814.love:8443/wx/HL/commit/58afda7f72031b8055dd65805815176f88dfe08c)
- **后端负责人**腰苏图yst@1814.love
---
## ① 接口背景
酒店资源新增「是否收费」属性。小程序端的酒店详情接口和列表接口均已同步透传该字段,供小程序展示酒店是否需要游客付费。字典来源 `sys_yes_no`,后端已配置并完成翻译,小程序无需任何字典配置。
---
## ② 变更清单
| 接口 | 方法 | 路径 | 变更内容 |
|------|------|------|----------|
| 酒店详情 | GET | `/mp/hotel/{hotelId}` | 出参新增 `isCharged` + `isChargedLabel` |
| 酒店列表 | GET | `/mp/hotel/list` | 出参新增 `isCharged` + `isChargedLabel` |
所有变更均为**向后兼容新增字段**,小程序现有调用代码无需修改。
---
## ③ 接口详情
| 项目 | 说明 |
|------|------|
| **认证方式** | 小程序 JWT Token,请求头 `Authorization: Bearer {mp_token}` |
| **幂等性** | GET 查询,天然幂等 |
| **限流** | 无单独限流配置,走网关全局限流 |
| **缓存** | 详情接口缓存 300 秒(`hotel:detail:{id}`),列表接口缓存 600 秒(`hotel:list:{keyword}:{city}:{starLevel}:{page}:{pageSize}`)。新字段随正常缓存刷新即可,无需额外处理 |
| **权限** | 小程序用户,已登录状态 |
---
## ④ 接口入参
### GET `/mp/hotel/{hotelId}` 详情
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| hotelId | Long | 是 | 酒店 ID路径参数 |
### GET `/mp/hotel/list` 列表
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| keyword | String | 否 | - | 关键词搜索 |
| city | String | 否 | - | 城市筛选 |
| starLevel | String | 否 | - | 星级筛选(字典: hotel_star_level |
| page | Integer | 否 | 1 | 页码 |
| pageSize | Integer | 否 | 20 | 每页条数 |
> 入参无变化,以上为完整入参文档。
---
## ⑤ 出参字段
### GET `/mp/hotel/{hotelId}` 详情(`MpHotelDetailVO`,新增字段)
| 字段名 | 类型 | 说明 | 可能为 null |
|--------|------|------|-------------|
| isCharged | Integer | 是否收费,1=是 0=否 | 否DB 默认 0 |
| isChargedLabel | String | 是否收费中文文本,取自字典 sys_yes_no"是"/"否" | 是(字典缺失时为 null |
### GET `/mp/hotel/list` 列表(`MpHotelListItemVO`,新增字段)
| 字段名 | 类型 | 说明 | 可能为 null |
|--------|------|------|-------------|
| isCharged | Integer | 是否收费,1=是 0=否 | 否DB 默认 0 |
| isChargedLabel | String | 是否收费中文文本,取自字典 sys_yes_no"是"/"否" | 是(字典缺失时为 null |
---
## ⑥ 枚举 / 数据字典
### sys_yes_no 字典(后端已配置,小程序无需维护)
| 字典值isCharged | 中文标签isChargedLabel | 说明 |
|--------------------|-----------------------------|------|
| 1 | 是 | 该酒店收费 |
| 0 | 否 | 该酒店免费 |
> `isChargedLabel` 由后端查字典自动翻译并返回,小程序可直接展示。若字典配置异常(不正常情况),该字段返回 null。
---
## ⑦ 错误码
| 错误码 | HTTP 状态 | 场景 |
|--------|-----------|------|
| 401 | 401 | 未登录或 Token 失效 |
| 404 | 404 | 酒店 ID 不存在、已删除或已下架 |
---
## ⑧ 示例
### 8.1 典型成功 — 获取收费酒店详情
**请求**
```
GET /mp/hotel/1928000000000001
Authorization: Bearer {mp_token}
```
**响应**
```json
{
"code": 200,
"msg": "成功",
"data": {
"hotelId": "1928000000000001",
"name": "丽江悦榕庄",
"subtitle": "古城边的世外桃源",
"hotelType": "HOTEL",
"isCharged": 1,
"isChargedLabel": "是",
"status": 1,
"city": "丽江市"
}
}
```
### 8.2 边界情况 — 列表中混合收费/免费酒店(均返回 isCharged + isChargedLabel
**请求**
```
GET /mp/hotel/list?city=丽江市&page=1&pageSize=20
Authorization: Bearer {mp_token}
```
**响应**
```json
{
"code": 200,
"msg": "成功",
"data": {
"total": 2,
"list": [
{
"hotelId": "1928000000000001",
"name": "丽江悦榕庄",
"isCharged": 1,
"isChargedLabel": "是"
},
{
"hotelId": "1928000000000002",
"name": "团队免费住宿点",
"isCharged": 0,
"isChargedLabel": "否"
}
]
}
}
```
### 8.3 业务失败 — 访问已下架酒店
**请求**
```
GET /mp/hotel/1928000000000099
Authorization: Bearer {mp_token}
```
**响应**404,酒店不存在或下架
```json
{
"code": 404,
"msg": "酒店不存在",
"data": null
}
```
---
## ⑨ 业务边界
**适用场景**
- 小程序酒店详情页展示"是否收费"信息
- 小程序酒店列表卡片可展示收费状态,帮助用户快速识别
**不适用场景**
- `isCharged` 仅为展示属性,不影响小程序下单流程或费用计算
- 列表接口不支持按 `isCharged` 筛选
**特殊边界**
- 历史存量酒店PR #3326 上线前创建)`isCharged` 默认为 0,`isChargedLabel` 返回 "否"
- 该字段受缓存影响:管理后台修改 isCharged 后,小程序侧缓存最多 5-10 分钟后自动刷新(详情 300s / 列表 600s
---
## ⑩ 修改前后对比
### 字段级对比
**出参(详情 + 列表)**
| 字段名 | 变更前 | 变更后 |
|--------|--------|--------|
| isCharged | 不存在 | ✨ 新增,Integer,1=是/0=否 |
| isChargedLabel | 不存在 | ✨ 新增,String,字典翻译文本,可能为 null |
### 行为级对比
| 行为 | 变更前 | 变更后 |
|------|--------|--------|
| 酒店详情接口 | 无收费信息 | 新增 isCharged + isChargedLabel |
| 酒店列表接口 | 无收费信息 | 新增 isCharged + isChargedLabel |
---
## ⑪ 影响评估 / 回滚
**破坏兼容性**:无,新增字段,现有小程序代码可忽略不用。
**前端同步上线**:无强制要求,可选接入展示。
**回滚方案**:若后端回滚,`isCharged``isChargedLabel` 字段会从响应中消失,小程序侧无感(原本不存在该字段)。
---
## ⑫ 注意事项
1. 小程序**无需**配置 `sys_yes_no` 字典,后端已完成翻译并在 `isChargedLabel` 中直接返回中文
2. 若不需要展示该字段,忽略即可,不影响任何现有功能
3. 受缓存影响,运营后台修改后小程序最长延迟 10 分钟可见,属正常现象
---
## ⑬ 关联 / 联系人
- **Issue**[https://git.1814.love:8443/wx/HL/issues/3325](https://git.1814.love:8443/wx/HL/issues/3325)
- **PR**[https://git.1814.love:8443/wx/HL/pulls/3326](https://git.1814.love:8443/wx/HL/pulls/3326)
- **Commit**[https://git.1814.love:8443/wx/HL/commit/58afda7f72031b8055dd65805815176f88dfe08c](https://git.1814.love:8443/wx/HL/commit/58afda7f72031b8055dd65805815176f88dfe08c)
- **后端负责人**:腰苏图(企微 / yst@1814.love

查看文件

@ -1,239 +0,0 @@
# 游玩项目(活动)新增「是否收费」字段(小程序端)
- **端类型**:小程序端
- **变更类型**:修改接口
- **日期**2026-06-01
- **Issue**[#3331](https://git.1814.love:8443/wx/HL/issues/3331)
- **PR**[#3334](https://git.1814.love:8443/wx/HL/pulls/3334)
- **Commit**[5da05a3](https://git.1814.love:8443/wx/HL/commit/5da05a324813cf8109d35c521c5e7bb9719627be)
- **后端负责人**腰苏图yst@1814.love
---
## ① 接口背景
游玩项目(活动)资源新增「是否收费」属性。小程序端的活动详情接口和活动列表接口均已同步透传该字段,供小程序展示活动是否需要游客付费。字典来源 `sys_yes_no`,后端已配置并完成翻译,小程序无需任何字典配置。备品(物资)域不涉及小程序端接口,本文档不包含备品。
---
## ② 变更清单
| 接口 | 方法 | 路径 | 变更内容 |
|------|------|------|----------|
| 活动详情 | GET | `/mp/activity/{activityId}` | 出参新增 `isCharged` + `isChargedLabel` |
| 活动列表 | GET | `/mp/activity/list` | 出参新增 `isCharged` + `isChargedLabel` |
所有变更均为**向后兼容新增字段**,小程序现有调用代码无需修改。
---
## ③ 接口详情
| 项目 | 说明 |
|------|------|
| **认证方式** | 小程序 JWT Token,请求头 `Authorization: Bearer {mp_token}` |
| **幂等性** | GET 查询,天然幂等 |
| **限流** | 无单独限流配置,走网关全局限流 |
| **权限** | 小程序用户,已登录状态 |
---
## ④ 接口入参
### GET `/mp/activity/{activityId}` 详情
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| activityId | Long | 是 | 游玩项目 ID路径参数 |
### GET `/mp/activity/list` 列表
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| keyword | String | 否 | - | 关键词搜索 |
| categoryCode | String | 否 | - | 分类筛选(字典: activity_category |
| page | Integer | 否 | 1 | 页码 |
| pageSize | Integer | 否 | 20 | 每页条数 |
> 入参无变化,以上为完整入参文档。
---
## ⑤ 出参字段
### GET `/mp/activity/{activityId}` 详情(`MpActivityDetailVO`,新增字段)
| 字段名 | 类型 | 说明 | 可能为 null |
|--------|------|------|-------------|
| isCharged | Integer | 是否收费,1=是 0=否 | 否DB 默认 0 |
| isChargedLabel | String | 是否收费中文文本,取自字典 sys_yes_no"是"/"否" | 是(字典缺失时为 null |
### GET `/mp/activity/list` 列表(`MpActivityListItemVO`,新增字段)
| 字段名 | 类型 | 说明 | 可能为 null |
|--------|------|------|-------------|
| isCharged | Integer | 是否收费,1=是 0=否 | 否DB 默认 0 |
| isChargedLabel | String | 是否收费中文文本,取自字典 sys_yes_no"是"/"否" | 是(字典缺失时为 null |
---
## ⑥ 枚举 / 数据字典
### sys_yes_no 字典(后端已配置,小程序无需维护)
| 字典值isCharged | 中文标签isChargedLabel | 说明 |
|--------------------|-----------------------------|------|
| 1 | 是 | 该活动收费 |
| 0 | 否 | 该活动免费 |
> `isChargedLabel` 由后端查字典自动翻译并返回,小程序可直接展示。若字典配置异常(不正常情况),该字段返回 null。
---
## ⑦ 错误码
| 错误码 | HTTP 状态 | 场景 |
|--------|-----------|------|
| 401 | 401 | 未登录或 Token 失效 |
| 404 | 404 | 活动 ID 不存在、已删除或已下架 |
---
## ⑧ 示例
### 8.1 典型成功 — 获取收费活动详情
**请求**
```
GET /mp/activity/1929000000000001
Authorization: Bearer {mp_token}
```
**响应**
```json
{
"code": 200,
"msg": "成功",
"data": {
"activityId": "1929000000000001",
"name": "高原骑马体验",
"categoryCode": "OUTDOOR",
"isCharged": 1,
"isChargedLabel": "是",
"status": 1
}
}
```
### 8.2 边界情况 — 列表中混合收费/免费活动(均返回 isCharged + isChargedLabel
**请求**
```
GET /mp/activity/list?categoryCode=OUTDOOR&page=1&pageSize=20
Authorization: Bearer {mp_token}
```
**响应**
```json
{
"code": 200,
"msg": "成功",
"data": {
"total": 2,
"list": [
{
"activityId": "1929000000000001",
"name": "高原骑马体验",
"isCharged": 1,
"isChargedLabel": "是"
},
{
"activityId": "1929000000000002",
"name": "免费篝火晚会",
"isCharged": 0,
"isChargedLabel": "否"
}
]
}
}
```
### 8.3 业务失败 — 访问已下架活动
**请求**
```
GET /mp/activity/1929000000000099
Authorization: Bearer {mp_token}
```
**响应**404,活动不存在或下架
```json
{
"code": 404,
"msg": "活动不存在",
"data": null
}
```
---
## ⑨ 业务边界
**适用场景**
- 小程序活动详情页展示"是否收费"信息
- 小程序活动列表卡片可展示收费状态,帮助用户快速识别
**不适用场景**
- `isCharged` 仅为展示属性,不影响小程序下单流程或费用计算
- 列表接口不支持按 `isCharged` 筛选
**特殊边界**
- 历史存量活动PR #3334 上线前创建)`isCharged` 默认为 0,`isChargedLabel` 返回 "否"
- 备品(物资)域无小程序端接口,本次不涉及
---
## ⑩ 修改前后对比
### 字段级对比
**出参(详情 + 列表)**
| 字段名 | 变更前 | 变更后 |
|--------|--------|--------|
| isCharged | 不存在 | ✨ 新增,Integer,1=是/0=否 |
| isChargedLabel | 不存在 | ✨ 新增,String,字典翻译文本,可能为 null |
### 行为级对比
| 行为 | 变更前 | 变更后 |
|------|--------|--------|
| 活动详情接口 | 无收费信息 | 新增 isCharged + isChargedLabel |
| 活动列表接口 | 无收费信息 | 新增 isCharged + isChargedLabel |
---
## ⑪ 影响评估 / 回滚
**破坏兼容性**:无,新增字段,现有小程序代码可忽略不用。
**前端同步上线**:无强制要求,可选接入展示。
**回滚方案**:若后端回滚,`isCharged``isChargedLabel` 字段会从响应中消失,小程序侧无感(原本不存在该字段)。
---
## ⑫ 注意事项
1. 小程序**无需**配置 `sys_yes_no` 字典,后端已完成翻译并在 `isChargedLabel` 中直接返回中文
2. 若不需要展示该字段,忽略即可,不影响任何现有功能
3. 本次仅活动有小程序端接口变更,备品(物资)无小程序端接口,不在本文档范围内
---
## ⑬ 关联 / 联系人
- **Issue**[https://git.1814.love:8443/wx/HL/issues/3331](https://git.1814.love:8443/wx/HL/issues/3331)
- **PR**[https://git.1814.love:8443/wx/HL/pulls/3334](https://git.1814.love:8443/wx/HL/pulls/3334)
- **Commit**[https://git.1814.love:8443/wx/HL/commit/5da05a324813cf8109d35c521c5e7bb9719627be](https://git.1814.love:8443/wx/HL/commit/5da05a324813cf8109d35c521c5e7bb9719627be)
- **后端负责人**:腰苏图(企微 / yst@1814.love

查看文件

@ -1,73 +0,0 @@
# C 端用户自主取消订单(小程序端)
> 端类型:**小程序端**v3变更类型**新增接口**服务hl-mp-serviceFeign → hl-order-service-v3
> Epic #3540 PR #3549 日期2026-06-06
## 接口地址
`POST /mp/order/{orderId}/cancel`
## 接口介绍
小程序 C 端用户自主取消**自己**的订单。仅出行前可取消,按退款政策(距出发天数阶梯)退款,取消后订单进入 `CANCELLED` 终态、不可恢复。
## 本次修改点
新增接口。此前小程序无自主取消能力(取消只能走后台定制师)。本接口固定 `POLICY` 退款模式C 端不暴露退款模式选择),并校验订单归属当前登录用户。
## 入参
- 路径参数:`orderId`Long,订单 ID,必填
- 请求头:`Authorization`(登录 token,必填
- 请求体(可选,可不传):
| 字段 | 类型 | 必填 | 含义 |
|---|---|---|---|
| reason | String | 否 | 取消原因 |
**入参示例**
```
POST /mp/order/2063151085698134017/cancel
Authorization: Bearer <token>
Content-Type: application/json
{ "reason": "行程有变,需要取消" }
```
body 也可整体不传。)
## 出参
`Result<Void>`
**出参示例**
```json
{ "code": 200, "message": "成功", "success": true, "data": null }
```
## 可取消状态(调用约束)
| 订单状态 | 是否可取消 |
|---|---|
| PENDING_PAY 待支付 | ✅ |
| CUSTOMIZING 定制中 | ✅ |
| PENDING_DEPARTURE 待出行 | ✅ |
| TRAVELLING 出行中 | ❌(行程中只能联系客服终止行程) |
| COMPLETED 已完成 | ❌ |
| CANCELLED 已取消 | ❌ |
- 退款模式固定 `POLICY`,按退款政策阶梯计算退款金额(已付订金时退款,未支付时无退款)。
- 仅可取消归属当前登录用户的订单(非本人订单返回 581037
## 错误码
| code | message | 触发 |
|---|---|---|
| 581017 | 当前订单状态不允许取消 | 出行中 / 已完成 / 已取消订单调用 |
| 581037 | 无权取消该订单 | 取消非本人userId 不匹配)订单 |
| 401 | 缺少有效的 Authorization 头 | 未携带 token |
## 关联
- Epichttps://git.1814.love:8443/wx/HL/issues/3540
- PRhttps://git.1814.love:8443/wx/HL/pulls/3549
- 后端负责人:腰苏图(订单 v3

查看文件

@ -1,33 +0,0 @@
# 🔴 安全热修:小程序相册接口横向越权(IDOR)修复 — 小程序
> 变更类型:安全修复(后端归属校验加固,**正常使用无影响**,无契约变更)
> 端类型:小程序(相册)
> 日期2026-06-17
> 服务hl-order-service-v2相册域
> PRhttps://git.1814.love:8443/wx/HL/pulls/3911
---
## 说明
`api-contract-audit` 审计小程序 BFF 时发现:相册的 4 个接口存在**横向越权(IDOR)**——后端收到当前用户 userId 后未做归属校验,任意登录用户改 URL 里的 orderId/folderId/albumFileId 即可读取/操作他人订单的相册。本次已修复:后端强制校验"相册所属订单归属当前登录用户",越权一律返回"不存在"。
涉及接口(小程序相册):
- 订单文件夹列表、文件夹文件列表、文件下载地址获取、文件夹公开授权(写)。
## 前端影响:无需改动
- **正常使用完全不受影响**:用户访问自己订单的相册照常工作。
- 仅"访问他人订单相册"这种越权请求会被拒(返回资源不存在)——前端正常逻辑不会触发。
- 接口路径、入参、返回结构均未变。
> 如果前端有任何调试/测试代码用了非当前用户的 orderId/folderId 访问相册,请改为只访问当前用户自己的资源(之前能拿到是 bug,现已封堵
## 校验
| 项目 | 信息 |
|---|---|
| PR | https://git.1814.love:8443/wx/HL/pulls/3911squash 合并 dev-v3,via api-contract-audit workflow |
| 部署 | 已部署测试服,order-v2 双实例健康(滚动重启 uptime 刷新) |
| 测试 | 相册模块单测全绿(新增越权拒绝反例:非本人订单/null-userId/订单缺失 + happy path + 防穿透 verify |
| 后端负责人 | wx |

查看文件

@ -1,85 +0,0 @@
# 小程序 BFF 接口契约审计修复(金额 String + 字段映射 + 可见性 + 缓存)— 修改接口 — 小程序
> 变更类型:⚠️ 多项修复(金额类型 / 字段补全 / 可见性收紧 / 缓存行为,多数前端正向受益)
> 端类型小程序hl-mp-service BFF
> 日期2026-06-17
> 服务hl-mp-service
> PRhttps://git.1814.love:8443/wx/HL/pulls/3915
---
## ⚠️ 关键说明
`api-contract-audit` 全量审计小程序 BFF 40 个核心 Controller,对抗复核实锤 100 项契约偏差。mp 是聚合层,本次修复 **mp BFF 自身的 67 处**;另有约 40 项根因在下游服务(订单/用户/资源服务),列在文末「待修清单」,会在后续审计中处理。已合并 dev-v3、部署测试服、双实例健康。
最影响前端的是第 1 节(金额→字符串)和第 2 节(原本恒 null 的字段现已修复)。
---
## 1. 金额字段统一为 JSON 字符串
多个 mp 响应 VO 的金额BigDecimal此前输出成 JSON 数字,本次统一改为**字符串**(与平台口径一致)。涉及:退款(退款预览/进度/详情)、发票、订单详情/列表、票务、价格明细(含按人群/优惠/附加费的逐项明细)、保费、推荐产品起价等。
> 前端处理:相关金额字段一律按**字符串**接收;若做过数字运算需先转数值。非金额(里程/经纬度/评分)不变。
---
## 2. 原本恒为 null 的字段已修复(前端可正常取值)
以下字段此前因 BFF 与下游字段名不一致(反序列化失败)或 BFF 未填充而恒为 null,现已修复
| 模块 | 字段 |
|---|---|
| 相册文件 | thumbnailUrl、fileName、fileSize、createdAt |
| 相册订单 | coverUrl、fileCount |
| 发票详情 | invoiceTitle、taxNumber、createdAt |
| 合同 | 状态日志 *Label、出行人证件类型标签 |
| 出行人 | race、roomGroupNo订单出行人、travelerTypeLabel |
| 到达计划 | updateTime |
> 前端处理:这些字段现在会返回真实值,可直接使用(之前拿到的是 null
---
## 3. 行为收紧(前端需配合)
### 3.1 下架资源详情不再返回(可见性收紧)
景点 / 酒店 / 餐厅 / 活动 详情接口在 BFF 层补了「是否上架」校验,**下架status=0的资源详情不再返回**给小程序(之前会原样返回)。前端访问已下架资源详情会得到「不存在/不可见」,请正常处理空态。
### 3.2 浏览量统计 / 错误不再被缓存
- 探索/百科详情的「浏览量+1」此前在缓存命中时不执行统计失真,现已修复浏览量正确累加
- BFF 缓存不再缓存下游降级错误(避免 TTL 内持续返回陈旧错误)。
- 前端无感,仅数据更准确。
### 3.3 定制需求状态字典修正
定制(/mp/custom相关接口的状态字典此前文档值有误,实际枚举为 `PENDING/PROCESSING/REPLIED/CONVERTED/CANCELLED`,请前端按此映射。
---
## 4. 待修清单(根因在下游服务,本次未修,后续处理)
以下问题审计已发现,但根因在下游服务,不在本 PR;记录在此供前端知晓暂仍按现状
- **订单服务order-v2相关 ~21 项**:部分字段缺失/状态机/错误码等(其中相册 4 个越权 P0 已由 PR #3911 安全热修修复)。
- **用户服务相关 ~11 项**出行人默认删除守卫、token 轮转、资料完善校验、足迹 HOTEL 等 — 将在用户服务审计时在源头修复。
- **资源服务相关 ~3 项**:百科推荐排序、足迹等 — 将在资源服务审计时修复。
---
## 5. 前端 Action 清单
1. 第 1 节金额字段按字符串解析。
2. 第 2 节字段现在有值,可接入使用。
3. 下架资源详情做空态处理(第 3.1)。
4. 定制状态映射用真实枚举(第 3.3)。
---
## 6. 关联
| 项目 | 信息 |
|---|---|
| PR | https://git.1814.love:8443/wx/HL/pulls/3915squash 合并 dev-v3,via api-contract-audit + mp-internal-fix workflow |
| 部署 | 已部署测试服,mp 双实例健康(滚动重启 uptime 刷新);公开端点 /mp/banner/active 等返 code:200 |
| 测试 | 963 单测,本次改动 add 0 新失败(另有 3 个 dev-v3 预存红与本次无关,已单独反馈后端) |
| 后端负责人 | wx |

查看文件

@ -1,37 +0,0 @@
# 用户服务接口契约审计修复mp 侧字段/契约对齐)— 修改接口 — 小程序
> 变更类型:字段/契约对齐(无破坏性删除),小程序侧影响较小
> 端类型:小程序(用户中心/首页配置/收藏足迹/出行人 OCR
> 日期2026-06-17
> 服务hl-user-service
> PRhttps://git.1814.love:8443/wx/HL/pulls/3924
> 说明:本文件随 user-service 契约审计一并产出,**暂缓推送**与小程序侧统一节奏。admin 侧见 changelogs-v2/2026-06/17_3924_*.md。
---
## 关键说明
用接口契约语义审计工作流扫描 hl-user-service 全部 Controller,mp小程序 BFF侧修复一批字段名/null 语义/响应结构与 Swagger 契约不符项。已合并 dev-v3、部署测试服、本地全量 2793 单测零新增回归。下面列 mp 侧对接相关变更。
---
## 1. 收藏 / 足迹FavoriteRespVO / FootprintRespVO
- 响应 VO 字段名/缺字段与契约对齐;金额类字段(如有)补 `@JsonSerialize(ToStringSerializer)` 输出字符串。
- 字段语义不变,前端按现有字段名接收即可;如此前对金额字段做数字运算,改为先转数值。
## 2. 首页配置MpHomeConfig / MpHomeScreen2VO
- 响应结构与 Swagger 契约对齐(字段名/层级稳定化),无破坏性删除。
## 3. 出行人 OCR / 内容安全MpTravelerOcr / MpContentSecurity
- 入参校验(`@Valid` / 校验分组)与错误码规范化;正常路径返回结构不变。
## 4. 站内信InternalMpMessage / 未读数 UnreadCountRespVO
- 未读数等响应改强类型 VO,字段名不变。
---
> 待小程序侧统一接线时一并对照本文件;如需提前对接某一节请单独知会。

查看文件

@ -1,227 +0,0 @@
# 【修改接口·小程序端】✨ 到达计划出行人类型中文名 travelerTypeName 新增字段 (#3951)
> **PR**: #3954 | **服务**: hl-order-service-v3 / hl-mp-service | **更新时间**: 2026-06-18
## 1. 接口背景
小程序到达计划接口返回的出行人对象ArrivalPlanTravelerSimpleVO原先只包含 `travelerType` 英文枚举值,前端展示出行人类型标签时需自行维护一套映射表。本次新增 `travelerTypeName` 字段,由后端查数据字典 `traveler_type` 派生中文名直接下发,小程序侧可零配置展示类型标签。原 `travelerType` 字段保留不变,属纯新增、非破坏性变更。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 获取到达计划(一期路径) | GET | /mp/order/{orderId}/arrival | 新增出参字段 | 出行人对象新增 `travelerTypeName` |
| 2 | 获取到达计划v3 路径) | GET | /mp/v3/order/arrival/{orderId} | 新增出参字段 | 出行人对象新增 `travelerTypeName` |
> 以上两个路径均经网关路由至 hl-mp-service,数据源为 order-v3 侧透传。
## 3. 接口详情
### 3.1 获取到达计划(一期路径)
- **使用场景**:小程序出行人填写或出行信息确认页,加载出行人到达计划详情。
- **认证**:需要微信登录态 JWTC 端 token
- **幂等性**:是(只读)。
- **限流**:无。
入参无变化。出参中 `travelers[]` 数组每个 `ArrivalPlanTravelerSimpleVO` 元素新增 `travelerTypeName` 字段String
### 3.2 获取到达计划v3 路径)
- **使用场景**同上,v3 版本接口路径,功能与 3.1 等价。
- **认证**:需要微信登录态 JWTC 端 token
- **幂等性**:是(只读)。
- **限流**:无。
入参无变化。出参中 `travelers[]` 数组每个 `ArrivalPlanTravelerSimpleVO` 元素新增 `travelerTypeName` 字段String
## 4. 接口入参
### 4.1 路径参数 / Query 参数
| 接口 | 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|------|
| GET /mp/order/{orderId}/arrival | orderId | StringLong | 是 | 路径参数,订单 ID |
| GET /mp/v3/order/arrival/{orderId} | orderId | StringLong | 是 | 路径参数,订单 ID |
### 4.2 请求体字段
均为 GET 接口,无请求体。入参无变化。
## 5. 出参(响应)
### 5.1 ArrivalPlanTravelerSimpleVO 字段
| 字段 | 类型 | 变更 | 说明 |
|------|------|------|------|
| id | String | 不变 | 出行人记录 ID |
| name | String | 不变 | 出行人姓名(脱敏后) |
| travelerType | String | 不变 | 出行人类型枚举值,见 §6 |
| travelerTypeName | String | **新增** | 出行人类型中文名,由数据字典 `traveler_type` 派生 |
> 其余字段视接口版本可能含到达信息、证件信息等,本次仅新增 `travelerTypeName`,其余字段不变。
## 6. 枚举 / 数据字典
### 6.1 travelerType数据字典traveler_type
**所属字段**`travelerType`(出参,保留不变)与 `travelerTypeName`(出参,新增中文名) | **类型**`String`
| 枚举值 | 中文名travelerTypeName | 说明 |
|--------|--------------------------|------|
| `ADULT` | 成人 | 成年旅客 |
| `CHILD` | 儿童 | 儿童旅客(含独立占位) |
| `YOUNG_CHILD` | 小童 | 小童旅客(不占位或半占位) |
| `BABY` | 幼童 | 婴幼儿(不占位) |
> 字典降级说明:若数据字典 `traveler_type` 中对应 key 缺失,后端使用枚举内置 label 兜底,小程序侧无需处理。
## 7. 错误码
本次为纯新增字段,无新增错误码。原有错误码不变。
| code | 含义 | 触发场景 |
|------|------|----------|
| 581201 | 订单不存在 | orderId 无效 |
| 401 | 未认证 | 未携带或微信 JWT 过期 |
| 403 | 无权限 | 当前用户无权查看该订单的到达计划 |
## 8. 示例3 组:典型 / 边界 / 异常)
### 8.1 典型成功 — 获取到达计划(含新字段)
请求:
```
GET /mp/v3/order/arrival/2067178767255560193
Authorization: Bearer <mp-token>
```
响应:
```json
{
"code": 200,
"data": {
"orderId": "2067178767255560193",
"travelers": [
{
"id": "2067178767255560201",
"name": "张*明",
"travelerType": "ADULT",
"travelerTypeName": "成人"
},
{
"id": "2067178767255560202",
"name": "张*",
"travelerType": "YOUNG_CHILD",
"travelerTypeName": "小童"
}
]
},
"message": "ok",
"success": true
}
```
### 8.2 边界情况 — 仅含 BABY 类型出行人
请求:
```
GET /mp/order/2067178767255560194/arrival
Authorization: Bearer <mp-token>
```
响应(含 BABY 类型,travelerTypeName 正常返回):
```json
{
"code": 200,
"data": {
"orderId": "2067178767255560194",
"travelers": [
{
"id": "2067178767255560211",
"name": "李*强",
"travelerType": "ADULT",
"travelerTypeName": "成人"
},
{
"id": "2067178767255560212",
"name": "李小宝",
"travelerType": "BABY",
"travelerTypeName": "幼童"
}
]
},
"message": "ok",
"success": true
}
```
### 8.3 业务失败 — 订单不存在
请求:
```
GET /mp/v3/order/arrival/9999999999999999999
Authorization: Bearer <mp-token>
```
响应:
```json
{
"code": 581201,
"data": null,
"message": "订单不存在",
"success": false
}
```
## 9. 业务边界
- 适用:用户已登录且有权限访问该订单,处于任何订单状态均可查询到达计划(只读接口)。
- 适用四种出行人类型ADULT / CHILD / YOUNG_CHILD / BABY均有对应 `travelerTypeName` 中文名。
- 特殊边界:若数据字典维护缺失某枚举值,`travelerTypeName` 降级返回枚举内置中文名,不会返回 null,小程序无需做 null 保护。
- 特殊边界:`travelerType` 原字段值不变,若小程序已有本地映射逻辑,可继续保留或切换为直接展示 `travelerTypeName`,两者等价。
## 10. 修改前后对比
### 10.1 字段级对比
| VO | 字段 | 改前 | 改后 |
|----|------|------|------|
| ArrivalPlanTravelerSimpleVO | travelerTypeName | 不存在 | **新增** String,出行人类型中文名 |
| ArrivalPlanTravelerSimpleVO | travelerType | 原样返回英文枚举值 | 保留不变 |
### 10.2 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 出行人类型展示 | 小程序自行维护 ADULT→成人 等映射表 | 后端直接下发 travelerTypeName,小程序可直接渲染 |
| 字典缺失兜底 | 无 | 降级用枚举内置 label,小程序无感知 |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容****否**,纯新增字段,原字段不变、原结构不变。
- **前端是否必须同步上线****否**,旧小程序代码不读新字段也不会出错,可按需对接。
### 11.2 回滚方案
- **回滚方式**revert PR #3954 并重新部署 hl-order-service-v3 及 hl-mp-service,返回字段恢复为无 travelerTypeName 的旧结构。
## 12. 注意事项
- 前端 workaround 清理点:若小程序已有本地 `travelerType → 中文` 映射对象/函数,上线后可切换为直接读取 `travelerTypeName`,原映射逻辑可清理。
- `travelerTypeName` 由后端数据字典派生,字典修改后立即生效无需小程序发版,字典当前值为ADULT=成人 / CHILD=儿童 / YOUNG_CHILD=小童 / BABY=幼童。
- `/mp/order/{orderId}/arrival`(一期路径)与 `/mp/v3/order/arrival/{orderId}`v3 路径)行为一致,都已包含新字段,小程序按当前接入的路径对接即可。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#3951](https://git.1814.love:8443/wx/HL/issues/3951)
- **PR**: [#3954](https://git.1814.love:8443/wx/HL/pulls/3954)
- **Merge commit**: [16d77c9a5](https://git.1814.love:8443/wx/HL/commit/16d77c9a5)
### 13.2 联系人
- **后端负责人**: @yaosutu

查看文件

@ -1,316 +0,0 @@
# 【修改接口·小程序端】出行人类型改为由出生日期自动派生,birthday 必填、travelerType 入参移除 (#3976)
> **PR**: #3981 | **服务**: hl-order-service-v3 | **更新时间**: 2026-06-18
## 1. 接口背景
小程序客户出行人补全接口batch-edit原先要求客户端传入 `travelerType`(出行人类型枚举值)。本次调整将出行人类型的计算权收归后端:**客户端只需传 `birthday`(出生日期),后端按年龄段自动派生 `travelerType`**,入参不再接受 `travelerType` 字段(传了也会被忽略)。
`birthday` 同步从选填升级为**必填**,不传报 400。响应 VO 不变,`travelerType` / `travelerTypeName` 仍正常返回,展示层无需改动。
> 本次为破坏性入参变更:移除 `travelerType` 入参 + `birthday` 改必填。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 客户出行人补全 | POST | /v3/internal/mp/order/{id}/traveler/batch-edit | 入参破坏性变更 | 出行人对象移除 `travelerType``birthday` 改必填 |
## 3. 接口详情
### 3.1 客户出行人补全POST /v3/internal/mp/order/{id}/traveler/batch-edit
- **使用场景**:小程序用户自助补全出行信息时,批量 upsert 出行人列表id=null 新增,id 有值则更新)。
- **认证**:需要微信登录态 JWTC 端 token
- **幂等性**:否(写入操作)。
- **限流**:无。
入参中每个出行人对象移除 `travelerType` 字段,`birthday` 改为必填。响应 VO 不变。
## 4. 接口入参
### 4.1 路径参数 / Query 参数
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | StringLong | 是 | 路径参数,订单 ID |
### 4.2 请求体字段
请求体外层结构示例:
```json
{
"travelers": [{}]
}
```
**出行人对象字段表**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | StringLong | 否 | null 或不传=新增;有值=更新已有出行人 |
| name | String | 否 | 出行人姓名 |
| gender | String | 否 | 性别字典码1=男 / 2=女 / 0=未知) |
| birthday | String | **是** | 出生日期,格式 yyyy-MM-dd,不能晚于今天。**本次改为必填** |
| idType | String | 否 | 证件类型枚举值,见第 6 节 |
| idNo | String | 否 | 证件号码(明文) |
| nationality | String | 否 | 国籍 |
| race | String | 否 | 民族 |
| phone | String | 否 | 手机号 |
| emergencyContact | String | 否 | 紧急联系人 |
| emergencyPhone | String | 否 | 紧急联系人电话 |
| roomGroupNo | Integer | 否 | 房间分组编号(团期订单使用) |
| travelerType已移除 | — | — | 入参已移除,传入将被忽略;后端按 birthday 自动派生 |
## 5. 出参(响应)
响应 VO 结构**不变**,`travelerType``travelerTypeName` 仍正常返回。
### 5.1 响应列表元素TravelerVO 关键字段)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | String | 出行人记录 ID |
| orderId | String | 所属订单 ID |
| travelerType | String | 出行人类型枚举值(后端由 birthday 派生后返回) |
| travelerTypeName | String | 出行人类型中文名(成人 / 儿童 / 小童 / 幼童) |
| name | String | 出行人姓名 |
| birthday | String | 出生日期yyyy-MM-dd |
| idType | String | 证件类型枚举值 |
| idTypeName | String | 证件类型中文名 |
| idCardMasked | String | 证件号(脱敏后) |
| gender | String | 性别字典码 |
| nationality | String | 国籍 |
## 6. 枚举 / 数据字典
### 6.1 travelerType 派生规则(后端自动计算,前端仅需理解出参含义)
| 年龄段(按 birthday 计算) | travelerType 枚举值 | travelerTypeName |
|--------------------------|-------------------|-----------------|
| 0-1 岁(未满 2 周岁) | BABY | 幼童 |
| 2-6 岁(未满 7 周岁) | YOUNG_CHILD | 小童 |
| 7-17 岁(未满 18 周岁) | CHILD | 儿童 |
| 18 岁及以上 | ADULT | 成人 |
### 6.2 idType证件类型字典id_card_type
| 枚举值 | 中文名 | 说明 |
|--------|--------|------|
| ID_CARD | 身份证 | 中国居民身份证 |
| PASSPORT | 护照 | 中外护照 |
| BIRTH_CERT | 出生证明 | 婴幼儿出生医学证明 |
| HK_MACAU | 港澳通行证 | 港澳居民来往内地通行证 |
| TAIWAN | 台胞证 | 台湾居民来往大陆通行证 |
| MILITARY | 军官证 | 军人证件 |
## 7. 错误码
| code | 含义 | 触发场景 |
|------|------|----------|
| 400 | 出生日期不能为空 | 出行人对象未传 birthday 或传 null |
| 400 | 出生日期不能晚于今天 | birthday 传入了未来日期 |
| 581201 | 订单不存在 | orderId 无效 |
| 581200 | 出行人不存在 | 传了无效的出行人 id更新场景 |
| 401 | 未认证 | 未携带或微信 JWT 过期 |
| 403 | 无权限 | 当前用户无权操作该订单的出行人 |
## 8. 示例3 组:典型 / 边界 / 异常)
### 8.1 典型成功 — 补全两位出行人(成人 + 儿童,含 birthday,不传 travelerType
请求POST /v3/internal/mp/order/2067178767255560193/traveler/batch-edit,Authorization: Bearer <mp-token>
请求体示例:
```json
{
"travelers": [
{
"id": null,
"name": "张三",
"gender": "1",
"birthday": "1990-05-20",
"idType": "ID_CARD",
"idNo": "110101199005201234"
},
{
"id": null,
"name": "张小宝",
"gender": "1",
"birthday": "2019-08-10",
"idType": "BIRTH_CERT",
"idNo": "P110101201908101234"
}
]
}
```
响应示例:
```json
{
"code": 200,
"data": [
{
"id": "2067178767255560201",
"orderId": "2067178767255560193",
"travelerType": "ADULT",
"travelerTypeName": "成人",
"name": "张三",
"birthday": "1990-05-20",
"idType": "ID_CARD",
"idTypeName": "身份证",
"idCardMasked": "110***********1234",
"gender": "1"
},
{
"id": "2067178767255560202",
"orderId": "2067178767255560193",
"travelerType": "CHILD",
"travelerTypeName": "儿童",
"name": "张小宝",
"birthday": "2019-08-10",
"idType": "BIRTH_CERT",
"idTypeName": "出生证明",
"idCardMasked": "P1101012019****1234",
"gender": "1"
}
],
"message": "ok",
"success": true
}
```
### 8.2 边界情况 — 幼童birthday 未满 2 周岁,派生 BABY
请求体示例:
```json
{
"travelers": [
{
"id": null,
"name": "李小婴",
"gender": "2",
"birthday": "2025-06-01",
"idType": "BIRTH_CERT",
"idNo": "P110101202506011234"
}
]
}
```
响应示例:
```json
{
"code": 200,
"data": [
{
"id": "2067178767255560210",
"orderId": "2067178767255560193",
"travelerType": "BABY",
"travelerTypeName": "幼童",
"name": "李小婴",
"birthday": "2025-06-01",
"idType": "BIRTH_CERT",
"idTypeName": "出生证明",
"idCardMasked": "P1101012025****1234",
"gender": "2"
}
],
"message": "ok",
"success": true
}
```
### 8.3 业务失败 — birthday 缺失,报出生日期不能为空
请求体示例(缺少 birthday
```json
{
"travelers": [
{
"name": "王五",
"gender": "1"
}
]
}
```
响应示例:
```json
{
"code": 400,
"data": null,
"message": "出生日期不能为空",
"success": false
}
```
## 9. 业务边界
- 适用:用户已登录且有权限操作该订单,且订单处于可编辑出行人的状态时可调用。
- 不适用订单已完成COMPLETED或已取消CANCELLED后,出行人信息不可再写入。
- 特殊边界birthday 为整周岁当天(如刚满 2 岁、7 岁、18 岁生日当天)时,当天即按新档位计算(满龄升档)。
- 特殊边界:如果小程序端表单中仍有 travelerType 字段的存储或传入,不影响功能(后端忽略),建议同步清理。
- 特殊边界birthday 只允许不晚于今天的日期,传入未来日期(包括明天)报 400。
- 特殊边界birthday 格式须为 yyyy-MM-dd,格式错误报 400。
## 10. 修改前后对比
### 10.1 字段级对比(入参)
| 字段 | 改前 | 改后 |
|------|------|------|
| travelerType入参 | 选填,由客户端传入,控制出行人类型 | 已移除,传入被忽略 |
| birthday入参 | 选填 | 必填,不传报 400 |
### 10.2 字段级对比(出参,不变)
| 字段 | 改前 | 改后 |
|------|------|------|
| travelerType出参 | 返回客户端传入值 | 返回后端按 birthday 派生值(语义不变) |
| travelerTypeName出参 | 返回中文名 | 不变 |
| birthday出参 | 原样返回 | 不变 |
### 10.3 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 出行人类型来源 | 客户端传入 travelerType,后端直接存储 | 后端按 birthday 自动派生,客户端无需传 |
| birthday 必填性 | 选填,不传不报错 | 必填,不传报 400 |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**是,birthday 改必填属破坏性变更。旧版小程序表单若未传 birthday,保存时报 400。
- **小程序是否必须同步上线**:是。调用 batch-edit 接口时,必须确保每个出行人对象带 birthday;原有 travelerType 入参代码可同步清理。
### 11.2 回滚方案
- **回滚方式**revert PR #3981 并重新部署 hl-order-service-v3,恢复 travelerType 入参有效 + birthday 选填的旧行为。
## 12. 注意事项
- 出行人类型选择器如果仅用于控制 travelerType 入参,本次可直接移除,出行人类型由 birthday 派生后在响应中正常回显,展示无需调整。
- birthday 格式严格为 yyyy-MM-dd如 1990-05-20,不接受时间戳或其他格式。
- 响应中的 travelerType / travelerTypeName 仍正常返回,出行人卡片上的类型标签展示逻辑无需改动。
- 年龄以服务器当天日期(北京时间)计算,生日当天视为满周岁。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#3976](https://git.1814.love:8443/wx/HL/issues/3976)
- **PR**: [#3981](https://git.1814.love:8443/wx/HL/pulls/3981)
- **Merge commit**: [8209c5702](https://git.1814.love:8443/wx/HL/commit/8209c5702)
### 13.2 联系人
- **后端负责人**: @yaosutu

查看文件

@ -1,44 +0,0 @@
# 【修改接口·小程序端】到达计划自驾时段 selfDrivePeriodLabel 文案口径含时间范围
> **PR**: #4061 | **服务**: hl-order-service-v3经 mp-service 透传)| **更新时间**: 2026-06-19
## 1. 接口背景
大交通枚举改造(#4061)统一了自驾时段 `SelfDrivePeriod` 枚举的中文 label 口径,含时间范围。到达计划mp出参 `ArrivalPlanRespVO.selfDrivePeriodLabel` 随之变化。前端直接显示该 label,**无需改代码**,只是显示文案更详细了。
## 2. 变更清单
| 接口 | 方法 | 路径 | 变更 |
|---|---|---|---|
| 到达计划查询 | GET | /v3/internal/mp/order/arrival/{orderId} | 出参 selfDrivePeriodLabel 文案口径变化 |
(同枚举也用于 mp 创建/更新/批量接口的回显出参)
## 3. 出参变化
| 字段 | 类型 | 变更 | 说明 |
|---|---|---|---|
| selfDrivePeriodLabel | String | 文案口径调整 | 自驾时段中文,由「上午/下午/晚上」改为含时间范围 |
## 4. 枚举 / 数据字典
| selfDrivePeriod 码值 | 改前 label | 改后 label |
|---|---|---|
| MORNING | 上午 | **上午(06:00-12:00)** |
| AFTERNOON | 下午 | **下午(12:00-18:00)** |
| EVENING | 晚上 | **晚上(18:00-24:00)** |
## 5. 影响评估
- **破坏向后兼容**:否。`selfDrivePeriod` 码值不变,仅 `selfDrivePeriodLabel` 展示文案更详细。
- **前端是否需改**:否,前端直接展示 label 即可(显示内容自动变详细)。**例外**:若前端有按 label 文案做精确匹配/判断的逻辑(不推荐),需改为按 `selfDrivePeriod` 码值判断。
## 6. 注意事项
- 自驾时段判断逻辑请用 `selfDrivePeriod` 码值MORNING/AFTERNOON/EVENING,不要依赖 `selfDrivePeriodLabel` 文案。
## 7. 关联 / 联系人
- **Issue**: [#4058](https://git.1814.love:8443/wx/HL/issues/4058)
- **PR**: [#4061](https://git.1814.love:8443/wx/HL/pulls/4061)
- **后端负责人**: @yaosutu

查看文件

@ -1,65 +0,0 @@
# 【修改接口·小程序端】客户出行人补全:订单确认后禁止 mp 端编辑(新增门禁 + 错误码 581145(CR)
> **PR**: #4154 | **服务**: hl-order-service-v3 | **更新时间**: 2026-06-21
## 1. 接口背景
小程序客户出行人补全接口batch-edit此前**未在后端强制限制可编辑的订单阶段**:客户端只要订单属于本人,任意阶段都能提交编辑。本次安全/一致性收紧:**仅订单确认前(待支付 / 定制中)允许经小程序编辑出行人;订单确认后(待出行起)及已完成 / 已取消一律拒绝**,返回新错误码 `581145`
收紧原因:订单确认后修改出行人 PII姓名 / 手机 / 生日等)会误触发后端「合同作废重签 + 退保重投保」链路,造成非预期的合同与保单churn。证件号此前已在签约后冻结,本次把整个编辑动作按订单阶段收口。
> 本次为行为收紧(破坏性变更面向「确认后仍调用」的旧逻辑)。按产品生命周期,出行人补全本就发生在「待补全信息」窗口(定制中阶段),正常流程不受影响。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 客户出行人补全 | POST | /v3/internal/mp/order/{id}/traveler/batch-edit | 行为收紧 | 订单非「待支付/定制中」阶段调用直接拒绝581145;入参 / 出参结构不变 |
## 3. 关键说明
- **放行阶段(白名单)**`order_status ∈ {PENDING_PAY 待支付, CUSTOMIZING 定制中}`
- **拒绝阶段**`PENDING_DEPARTURE 待出行(确认后)/ TRAVELLING 出行中 / COMPLETED 已完成 / CANCELLED 已取消`,以及任何其他状态(白名单 fail-closed
- 拒绝时**整单不写入**(事务回滚),返回 `code=581145`
- 入参字段、出参 VO 结构**完全不变**,仅新增前置阶段校验。
- 本门禁**只约束小程序mp路径**;定制师 / admin 后台路径不受限(如需确认后变更出行人,请走定制师后台)。
## 4. 入参 / 出参
入参(请求体 travelers[] + 路径 id与响应 VO **均不变**,详见既有文档(如 #3976 出行人补全契约)。本次仅在订单阶段不满足时提前抛 581145。
## 5. 错误码
| code | 含义 | 触发场景 | 本次 |
|------|------|----------|------|
| **581145** | 订单已确认,出行人信息不可再经小程序修改,如需变更请联系定制师 | 订单 status 不在 {待支付, 定制中} 白名单(即确认后 / 已完成 / 已取消)时调用 batch-edit | **新增** |
| 581102 | 订单不存在,无法编辑出行人 | orderId 无效 | 不变 |
| 581122 | 订单不属于当前用户 | 当前登录 userId 与订单归属不一致(横向越权拦截) | 不变 |
## 6. 前端处理建议
- 出行人编辑入口建议按订单状态控制:仅在「待支付 / 定制中(待补全信息)」展示可编辑表单;订单确认后置为只读。
- 若仍调用 batch-edit 命中 581145,按 message 文案提示用户「订单已确认,如需修改出行人请联系定制师」,不要静默失败。
- 正常按生命周期编辑(确认前补全)的前端流程**无需改动**。
## 7. 业务边界
- 适用:订单 `order_status = PENDING_PAY``CUSTOMIZING`,且出行人属本人订单581122 越权拦截照旧)。
- 不适用订单确认后PENDING_DEPARTURE 起)/ 已完成 / 已取消 —— 一律 581145 拒绝。
- 证件号idNo在合同已签contract_status=SIGNED时本就冻结581111,本次门禁覆盖范围更靠前按订单阶段整体拦截
## 8. 影响评估 / 回滚
- **向后兼容**:对「确认前编辑」的正常流程兼容;对「确认后仍调用 batch-edit」的旧行为为破坏性收紧此前会误触发重签,属应修缺陷
- **小程序是否必须同步上线**:建议同步——确认后隐藏 / 禁用编辑入口 + 处理 581145 文案;不同步也不会造成数据错误(后端已兜底拒绝)。
- **回滚**revert PR #4154 重新部署 hl-order-service-v3,恢复不限阶段编辑的旧行为。
## 9. 关联 / 联系人
### 9.1 链接
- **PR**: [#4154](https://git.1814.love:8443/wx/HL/pulls/4154)CR 发现,无关联 Issue
### 9.2 联系人
- **后端负责人**: @wx

查看文件

@ -1,241 +0,0 @@
# 发票补齐 - 小程序端申请 + 可见性过滤 + 签名下载(小程序端)
> Issue: [#4174](https://git.1814.love:8443/wx/HL/issues/4174) / [#4182](https://git.1814.love:8443/wx/HL/issues/4182)
> PR: [#4181](https://git.1814.love:8443/wx/HL/pulls/4181)mp 端申请+可见性)/ [#4184](https://git.1814.love:8443/wx/HL/pulls/4184)(签名下载)
> 日期: 2026-06-21
> 服务: hl-order-service-v3invoice 域)
> 端类型: 小程序端
---
## 1. 接口背景
小程序发票模块本次补齐三项改动:
- **申请门槛收紧**PR4 / Issue #4174):原 collab 域 POST /v3/internal/mp/order/{orderId}/invoice/apply 路径不变,但实现从 collab 迁入 invoice 域,门槛由宽松(可叠加多张)改为严格(一单一票 + 订单必须 COMPLETED + 金额不超订单总价)。
- **出参可见性过滤**PR4GET /v3/internal/mp/order/{orderId}/invoice/list 和 GET /v3/internal/mp/order/invoice/{invoiceId}/detail 出参中,fileUrl 字段现在仅在 ISSUED / PUSHED 状态时返回;REQUESTED 状态返回 null处理中,前端勿展示 PDF 链接)。
- **新增签名下载端点**#4184 / Issue #4182GET /v3/internal/mp/order/invoice/{invoiceId}/download,返回 1 小时有效期 OSS 签名 URL,供前端调 wx.openDocument 预览或下载发票 PDF。
---
## 2. 变更清单
| # | 接口 | 变更类型 | 说明 |
|---|------|----------|------|
| 1 | POST /v3/internal/mp/order/{orderId}/invoice/apply | 修改接口(行为收紧) | 申请门槛由宽松改严格:一单一票 + 订单 COMPLETED + 金额不超总价;resp status 字段值从 APPLIED 改为 REQUESTED |
| 2 | GET /v3/internal/mp/order/{orderId}/invoice/list | 修改接口(字段行为变化) | fileUrl 字段REQUESTED 状态由有值改为 null;ISSUED / PUSHED 保持有值 |
| 3 | GET /v3/internal/mp/order/invoice/{invoiceId}/detail | 修改接口(字段行为变化) | fileUrl 字段同上REQUESTED 返 null,ISSUED / PUSHED 有值 |
| 4 | GET /v3/internal/mp/order/invoice/{invoiceId}/download | 新增接口 | 返回发票 PDF 的 OSS 签名下载 URL1 小时有效期),含 IDOR 鉴权 |
---
## 3. 接口详情
认证:所有接口走小程序 JWT 认证,Header 携带 Authorization: Bearer token小程序 openId → userId
幂等性apply 非幂等(一单一票,重复提交报 581511;download 幂等(每次生成新签名 URL
限流:网关全局限流。
---
## 4. 接口入参
### 4.1 路径参数 / Query 参数
| 接口 | 参数名 | 类型 | 必填 | 说明 |
|------|--------|------|------|------|
| POST /{orderId}/apply | orderId | Long | 是 | 路径参数,订单 ID |
| GET /{orderId}/invoice/list | orderId | Long | 是 | 路径参数,订单 ID |
| GET /invoice/{invoiceId}/detail | invoiceId | Long | 是 | 路径参数,发票 ID |
| GET /invoice/{invoiceId}/download | invoiceId | Long | 是 | 路径参数,发票 ID |
### 4.2 请求体字段
**POST /v3/internal/mp/order/{orderId}/invoice/apply**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| amount | String | 是 | 开票金额(字符串,单位元),不超过订单总价 |
| invoiceType | String | 是 | 发票类型,枚举见 6 节 |
| titleType | String | 是 | 抬头类型PERSONAL / COMPANY |
| title | String | 是 | 发票抬头(姓名或公司名) |
| taxNo | String | 条件必填 | 税号titleType=COMPANY 时必填) |
| bankName | String | 条件必填 | 开户行invoiceType=VAT_SPECIAL 时必填) |
| bankAccount | String | 条件必填 | 银行账号invoiceType=VAT_SPECIAL 时必填) |
| registAddress | String | 条件必填 | 注册地址invoiceType=VAT_SPECIAL 时必填) |
| registPhone | String | 条件必填 | 注册电话invoiceType=VAT_SPECIAL 时必填) |
| email | String | 条件必填 | 收票邮箱invoiceType=ELECTRONIC 时必填) |
| remark | String | 否 | 备注 |
GET 接口无请求体。
---
## 5. 出参字段
**POST /v3/internal/mp/order/{orderId}/invoice/apply**
| 字段 | 类型 | 说明 |
|------|------|------|
| id | String | 新建发票 IDLong 序列化) |
| status | String | 固定为 REQUESTED |
注意status 值此前旧实现返回 APPLIEDcollab 域),新实现返回 REQUESTEDinvoice 域)。请前端对齐新值。
**GET /v3/internal/mp/order/{orderId}/invoice/list单条字段,变化部分**
| 字段 | 原来行为 | 现在行为 |
|------|---------|---------|
| fileUrl | REQUESTED / ISSUED / PUSHED 均有值 | REQUESTED 返 null;ISSUED / PUSHED 有值 |
其余字段不变id / orderId / status / statusName / amount / invoiceType / title / taxNo / createTime 等)。
**GET /v3/internal/mp/order/invoice/{invoiceId}/detail变化部分**
| 字段 | 原来行为 | 现在行为 |
|------|---------|---------|
| fileUrl | 同上,恒有值 | REQUESTED 返 null;ISSUED / PUSHED 有值 |
**GET /v3/internal/mp/order/invoice/{invoiceId}/download新增接口**
| 字段 | 类型 | 说明 |
|------|------|------|
| fileUrl | String | OSS 签名下载 URL1 小时有效期,格式为 HTTPS 带签名参数的 URL |
| pdfName | String | PDF 文件名(可选,用于 wx.openDocument 的 name 参数) |
---
## 6. 枚举 / 数据字典
**InvoiceStatus - 发票状态(影响 fileUrl 可见性)**
| 枚举值 | 中文名 | fileUrl 是否可见 |
|--------|--------|----------------|
| REQUESTED | 待开票 | null处理中,前端勿展示 PDF 链接) |
| ISSUED | 已开票 | 有值 |
| PUSHED | 已推送 | 有值 |
| VOIDED | 已作废 | null |
**InvoiceType - 发票类型**
| 枚举值 | 中文名 |
|--------|--------|
| VAT_NORMAL | 增值税普通发票 |
| VAT_SPECIAL | 增值税专用发票 |
| ELECTRONIC | 电子发票 |
**TitleType - 抬头类型**
| 枚举值 | 中文名 |
|--------|--------|
| PERSONAL | 个人 |
| COMPANY | 公司 |
---
## 7. 错误码
| 错误码 | 常量 | 触发场景 |
|--------|------|----------|
| 581510 | INVOICE_ORDER_NOT_COMPLETED | 订单状态非 COMPLETED,不可申请开票 |
| 581511 | INVOICE_ALREADY_EXISTS | 该订单已有有效发票,不可重复申请(一单一票) |
| 581512 | INVOICE_AMOUNT_EXCEED | 开票金额超过订单总价 |
| 581513 | INVOICE_TYPE_INVALID | 发票类型枚举值非法 |
| 581514 | INVOICE_TAX_NO_REQUIRED | 公司抬头 / 专票时税号为必填 |
| 581515 | INVOICE_VAT_SPECIAL_FIELDS_REQUIRED | 专票时开户行 / 银行账号 / 注册地址 / 注册电话为必填 |
| 581516 | INVOICE_EMAIL_REQUIRED | 电子发票时邮箱为必填 |
| 581517 | INVOICE_NOT_DOWNLOADABLE | 发票尚未开具REQUESTED / VOIDED 状态),暂时无法下载 |
| 581518 | INVOICE_FORBIDDEN | 无权访问该发票IDOR 防护,含发票不存在场景) |
| 581519 | INVOICE_SIGNED_URL_FAILED | 获取签名下载 URL 失败,请稍后重试hl-user-service 暂时不可用) |
---
## 8. 示例
### 8.1 典型成功:申请开票
POST /v3/internal/mp/order/1234567890123456789/invoice/apply
请求体:
响应:
### 8.2 边界情况:列表中 REQUESTED 状态 fileUrl 为 null
GET /v3/internal/mp/order/1234567890123456789/invoice/list 响应示例(某张待开票发票):
### 8.3 业务失败REQUESTED 状态发票不可下载
GET /v3/internal/mp/order/invoice/111/download
响应:
---
## 9. 业务边界
**适用:**
- 申请:订单状态必须为 COMPLETED;无有效发票一单一票;金额不超订单总价
- 下载:发票状态为 ISSUED 或 PUSHED;且该发票属于当前登录用户的订单
**不适用:**
- 订单 COMPLETED 之前不可申请
- 已有有效发票,不可重复申请
- REQUESTED / VOIDED 状态发票不可下载(报 581517
- 访问他人订单的发票报 581518IDOR 防护,不返回 404 以避免泄露存在性)
**特殊边界:**
- 签名 URL 有效期 1 小时。前端如果缓存了 fileUrl,每次打开下载前须重新调 download 接口获取最新签名 URL,不可长期复用。
- wx.openDocument 下载后端直接返回签名 URL,前端无需中转。
---
## 10. 修改前后对比
### POST apply 申请门槛变化
| 项目 | 变更前(旧 collab 实现) | 变更后invoice 域) |
|------|----------------------|-------------------|
| 一单一票限制 | 无(可叠加多张) | 有existsActiveByOrderId 拦截) |
| 金额校验 | 已开+本次 <= 总价 | 本次金额 <= 总价(一单一票前提下等价) |
| 响应 status 值 | APPLIED | REQUESTED |
### GET list / detail fileUrl 可见性变化
| 发票状态 | 变更前 fileUrl | 变更后 fileUrl |
|---------|--------------|--------------|
| REQUESTED | 有值OSS 公链) | null前端显示处理中 |
| ISSUED | 有值 | 有值(不变) |
| PUSHED | 有值 | 有值(不变) |
| VOIDED | 有值 | null |
---
## 11. 影响评估 / 回滚
- 破坏兼容性重要fileUrl 字段在 REQUESTED 状态下由有值变为 null。前端如果之前有展示 REQUESTED 状态下 PDF 链接的逻辑(点击打开 OSS 文件),现在会读到 null,需要做 null 判断,展示「处理中」状态文案。
- apply status 值变化:从 APPLIED 改为 REQUESTED。前端如果有对 status 值做 hardcode 判断if status == APPLIED,需要更新。
- 前端同步上线建议fileUrl null 判断 + apply 响应 status 对齐必须在本次版本同步更新。
- 回滚方案回滚旧版本,fileUrl 可见性恢复,download 端点返回 404。apply 的 status 回退为 APPLIED如旧版本有此值
---
## 12. 注意事项
1. fileUrl null 判断list / detail 接口读到 fileUrl 为 null 时,前端应显示「处理中」或「待开票」而不是空链接。
2. 签名 URL 勿缓存超过 1 小时download 返回的签名 URL 有效期 1 小时,前端每次用前须重新调接口,不可全局缓存复用。
3. apply 响应 status 值变更:旧值 APPLIED 已废弃,现在返回 REQUESTED。如前端有 switch/if 判断该值,须对齐。
4. download IDOR 防护:发票不存在或不属于当前用户均返回 581518INVOICE_FORBIDDEN,不返回 404,避免存在性枚举攻击。
---
## 13. 关联 / 联系人
- Issue小程序端 mp: https://git.1814.love:8443/wx/HL/issues/4174
- Issue签名下载: https://git.1814.love:8443/wx/HL/issues/4182
- PRmp 端 #4181: https://git.1814.love:8443/wx/HL/pulls/4181
- PR签名下载 #4184: https://git.1814.love:8443/wx/HL/pulls/4184
- 后端负责人: yaosutu

查看文件

@ -1,340 +0,0 @@
# 售后申诉 — 小程序端新增接口
- **端类型**:小程序端
- **变更类型**:新增接口
- **日期**2026-06-22
- **关联 Issue**https://git.1814.love:8443/wx/HL/issues/4161
- **关联 PR**#4185(小程序端接口)/ #4162 #4165 #4168 #4187(基础能力)
- **后端负责人**腰苏图yaosutu
---
## 一、接口背景
v3 全新售后服务中心com.hulalv.aftersale上线。小程序端用户可发起投诉或申诉、查看自己的工单列表与详情、以及在工单未处置前随时撤回。
新增 4 个接口(含 1 个原有但归入本域):发起工单、我的工单列表、工单详情、撤回工单。
本次为全新域,不影响任何已有接口。
---
## 二、变更清单
| 编号 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 1 | POST | /v3/mp/aftersale/ticket | 发起投诉/申诉工单 |
| 2 | GET | /v3/mp/aftersale/ticket/my | 我的工单列表(按订单分页) |
| 3 | GET | /v3/mp/aftersale/ticket/{id} | 工单详情 |
| 4 | POST | /v3/mp/aftersale/ticket/{id}/withdraw | 撤回工单 |
---
## 三、接口详情
| 属性 | 说明 |
|------|------|
| 认证方式 | JWT Bearer TokenC 端 Token,Gateway 解析后注入 userId |
| userId 传递 | Gateway 解析 JWT 后注入到请求属性,后端从 HttpServletRequest 取;前端无需传 userId 字段 |
| 限流 | Gateway 默认限流策略 |
| 路径前缀 | /v3/mp/aftersale/ticket |
---
## 四、接口入参
### 4.1 POST /v3/mp/aftersale/ticket — 发起工单
请求体JSON
| 字段 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|
| orderId | Long | 是 | — | 订单 ID |
| category | String | 是 | COMPLAINT / APPEAL | 大类:投诉/申诉 |
| type | String | 是 | 见 §六枚举 | 细分类型,必须与 category 匹配 |
| title | String | 是 | 最多 100 字 | 工单标题 |
| description | String | 是 | 最多 2000 字 | 诉求描述 |
| targetRefId | Long | 条件必填 | — | 申诉时必填:关联退款申请 ID;投诉时可不传不传则用 orderId |
| claimType | String | 否 | REFUND / RECTIFY / EXPLANATION | 用户诉求类型,仅供参考 |
| claimAmount | Number | 否 | — | 用户期望金额,仅供参考,无约束力 |
| attachments | Array<String> | 否 | 最多 10 个 | 附件 URL 列表(图片/视频) |
### 4.2 GET /v3/mp/aftersale/ticket/my — 我的工单列表
Query 参数:
| 字段 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|
| orderId | Long | 是 | — | 订单 ID,只查该订单下的工单 |
| pageNo | Integer | 否 | >= 1 | 页码,默认 1 |
| pageSize | Integer | 否 | 1-50 | 每页条数,默认 20 |
### 4.3 GET /v3/mp/aftersale/ticket/{id} — 工单详情
路径参数idString/Long必填,工单 ID。
注:后端校验工单归属,非本人工单返回 586014。
### 4.4 POST /v3/mp/aftersale/ticket/{id}/withdraw — 撤回工单
路径参数idString/Long必填,工单 ID。
无请求体。后端校验1. 工单归属(非本人返回 586014;2. 状态(只有 SUBMITTED / PROCESSING 可撤回,否则返回 586013
---
## 五、出参字段
### 5.1 发起工单 — 返回工单 ID
```json
{"code": 200, "data": "1895000000000001"}
```
data 为 StringLong,即新建工单的 ID。
### 5.2 我的工单列表 — PageResult 结构
```json
{
"code": 200,
"data": {
"list": [],
"total": 5,
"pageNo": 1,
"pageSize": 20
}
}
```
### 5.3 工单详情及列表行 — TicketMpRespVO 字段
| 字段 | 类型 | 说明 |
|------|------|------|
| id | StringLong | 工单 ID |
| orderId | StringLong | 关联订单 ID |
| category | String | 大类枚举值COMPLAINT / APPEAL |
| categoryLabel | String | 大类中文名:投诉 / 申诉 |
| type | String | 细分类型枚举值,见 §六 |
| typeLabel | String | 细分类型中文名 |
| title | String | 工单标题 |
| description | String | 诉求描述 |
| claimType | String | 用户诉求类型(可选) |
| claimAmount | Number | 用户期望金额(可选) |
| attachments | Array<String> | 附件 URL 列表 |
| status | String | 工单状态枚举值,见 §六 |
| statusLabel | String | 工单状态中文名 |
| resolutionRemark | String | 处置说明RESOLVED / CLOSED 后可见,用于展示给用户) |
| canWithdraw | Boolean | 是否可撤回SUBMITTED / PROCESSING 且非终态时为 true |
| createdAt | StringISO 8601 | 创建时间 |
| updatedAt | StringISO 8601 | 更新时间 |
C 端视图隐藏处置细节decision / resolutionType / resolutionAmount / approvalNo / linkedRefundId,仅展示 status 和 resolutionRemark。
### 5.4 撤回工单
```json
{"code": 200, "data": null}
```
---
## 六、枚举 / 数据字典
### AftersaleCategory — 工单大类
| 枚举值 | 中文名 | 场景 |
|--------|--------|------|
| COMPLAINT | 投诉 | 对已发生的服务质量不满 |
| APPEAL | 申诉 | 对某个处理结果不服,求复核 |
### AftersaleTicketType — 细分类型type 字段)
| 枚举值 | 中文名 | 归属大类 |
|--------|--------|----------|
| ITINERARY | 行程 | COMPLAINT |
| HOTEL | 酒店 | COMPLAINT |
| VEHICLE | 车 | COMPLAINT |
| GUIDE | 导游 | COMPLAINT |
| OTHER | 其他 | COMPLAINT |
| REFUND_REJECTED | 退款被拒 | APPEAL |
| REFUND_AMOUNT_DISPUTE | 退款金额异议 | APPEAL |
type 必须与 category 匹配,否则返回 586012。
### AftersaleTicketStatus — 工单状态
| 枚举值 | 中文名 | 终态 | 说明 |
|--------|--------|------|------|
| SUBMITTED | 已提交 | 否 | 等待受理 |
| PROCESSING | 处理中 | 否 | 客服受理中 |
| RESOLVED | 已处置 | 否 | 处置方案落定(退款类等待到账) |
| CLOSED | 已关闭 | 是 | 流程终结 |
| WITHDRAWN | 已撤回 | 是 | 用户主动撤回 |
canWithdraw = true 的条件status 为 SUBMITTED 或 PROCESSING。
### claimType — 用户诉求类型(参考值,无强制约束)
| 常见值 | 中文名 |
|--------|--------|
| REFUND | 退款 |
| RECTIFY | 整改 |
| EXPLANATION | 要个说法 |
---
## 七、错误码
| 错误码 | 说明 | 触发场景 |
|--------|------|----------|
| 586009 | 工单不存在 | 传入 id 找不到工单 |
| 586011 | 同一对象已存在活跃工单 | 防重复提交(同订单下同 target 已有进行中工单) |
| 586012 | category 与 type 不匹配 | 如 COMPLAINT 传了 REFUND_REJECTED |
| 586013 | 撤回时状态非法 | 终态或 RESOLVED 工单不可撤回 |
| 586014 | 非本人工单 | 查看/撤回他人工单 |
| 586015 | 申诉必须关联退款申请 | APPEAL 类工单未传 targetRefId |
| 586016 | 申诉关联退款状态不合法 | 关联的退款申请状态不允许申诉 |
---
## 八、示例
### 8.1 典型成功 — 发起投诉工单
请求:
```
POST /v3/mp/aftersale/ticket
Authorization: Bearer {mp_token}
Content-Type: application/json
{
"orderId": 1800000000000099,
"category": "COMPLAINT",
"type": "HOTEL",
"title": "酒店降级投诉",
"description": "订单挂四星酒店实际入住三星,要求退差价",
"claimType": "REFUND",
"claimAmount": 600.00,
"attachments": ["https://oss.example.com/img1.jpg"]
}
```
响应:
```json
{"code": 200, "data": "1895000000000001"}
```
### 8.2 典型成功 — 发起申诉工单(退款被拒)
```
POST /v3/mp/aftersale/ticket
Authorization: Bearer {mp_token}
Content-Type: application/json
{
"orderId": 1800000000000099,
"category": "APPEAL",
"type": "REFUND_REJECTED",
"targetRefId": 9876543210,
"title": "退款申请被拒申诉",
"description": "退款申请被拒,但供应商确实未提供服务,要求重新审核"
}
```
响应:
```json
{"code": 200, "data": "1895000000000002"}
```
### 8.3 边界情况 — 查询工单详情(已处置,展示处置说明)
```json
{
"code": 200,
"data": {
"id": "1895000000000001",
"orderId": "1800000000000099",
"category": "COMPLAINT",
"categoryLabel": "投诉",
"type": "HOTEL",
"typeLabel": "酒店",
"title": "酒店降级投诉",
"description": "订单挂四星酒店实际入住三星,要求退差价",
"claimType": "REFUND",
"claimAmount": 600.00,
"attachments": ["https://oss.example.com/img1.jpg"],
"status": "CLOSED",
"statusLabel": "已关闭",
"resolutionRemark": "经核实酒店确实降级,已退差价 500 元",
"canWithdraw": false,
"createdAt": "2026-06-22T09:00:00",
"updatedAt": "2026-06-22T15:30:00"
}
}
```
### 8.4 业务失败 — 申诉未传 targetRefId
```json
{"code": 586015, "msg": "申诉必须关联一笔有效的退款申请"}
```
### 8.5 业务失败 — 撤回已 CLOSED 工单
```json
{"code": 586013, "msg": "撤回时工单状态非法(只有 SUBMITTED / PROCESSING 可撤回)"}
```
---
## 九、业务边界
适用:
- 同一订单下同一 targetRef 只能有一个活跃工单SUBMITTED / PROCESSING / RESOLVED,否则返回 586011
- 申诉APPEAL类工单必须关联一笔有效的退款申请targetRefId 必填)
- SUBMITTED / PROCESSING 状态的工单可撤回,canWithdraw=true
不适用:
- 终态工单CLOSED / WITHDRAWN不可撤回
- 非本人工单不可查看/撤回
特殊边界:
- 工单从 PROCESSING 撤回后,若该工单已提交企微 OA,后端会自动撤回 OA 审批(异步,不影响前端响应)
- attachments 最多 10 个,超出返回参数校验错误
---
## 十、修改前后对比
本次为全新域新增接口,无旧接口对比。
---
## 十一、影响评估 / 回滚
本次为全新域,不影响任何已有小程序接口,可独立上线。
回滚方案:回滚后端服务版本即可。
---
## 十二、注意事项
1. 发起工单返回值是 StringLongdata 字段为新建工单的 ID,是字符串而非数字。
2. C 端视图隐藏处置细节resolutionAmount / decision / approvalNo 等字段在 C 端不返回,不要在小程序页面展示。resolutionRemark 是给用户看的处置说明,可展示。
3. canWithdraw 字段已由后端计算:前端直接用该字段判断是否显示撤回按钮,无需自行判断状态。
4. 所有 ID 为字符串id / orderId 均序列化为字符串,前端不要转 Number。
5. category=APPEAL 时targetRefId 必填(关联退款申请 ID,targetRefType 后端自动设为 REFUND_APPLICATION。
---
## 十三、关联 / 联系人
- Issuehttps://git.1814.love:8443/wx/HL/issues/4161
- PR #4185小程序端接口https://git.1814.love:8443/wx/HL/pulls/4185
- PR #4162售后基建https://git.1814.love:8443/wx/HL/pulls/4162
- PR #4165(受理/处置 + OA 接入https://git.1814.love:8443/wx/HL/pulls/4165
- PR #4168OA 回调内部接口https://git.1814.love:8443/wx/HL/pulls/4168
- PR #4187退款驱动联动https://git.1814.love:8443/wx/HL/pulls/4187
- 后端负责人腰苏图yaosutu

查看文件

@ -1,341 +0,0 @@
# 售后工单接入 wx 内容安全机审 — 修改接口(小程序端)
- **端类型**:小程序端
- **日期**2026-06-22
- **Issue**[#4196](https://git.1814.love:8443/wx/HL/issues/4196)
- **PR**[#4203](https://git.1814.love:8443/wx/HL/pulls/4203)
- **Commit**[8882eb156](https://git.1814.love:8443/wx/HL/commit/8882eb1568a7e5ab8f48a6db4b7e3d4d4b6f2c15)
- **后端负责人**yaosutu
---
## 1. 接口背景
用户在小程序发起售后工单(投诉/申诉)时,若附带图片或视频,前端需先通过微信内容安全 `msgSecCheck` 接口对媒体文件做内容审核,得到微信返回的 `traceId` 后,连同创建工单请求一并透传给后端。后端不直接调用微信安全接口,只存储 traceId,等待微信异步回调后更新审核状态。无图/无视频的工单跳过机审,直接置为 `APPROVED`
---
## 2. 变更清单
| 变更类型 | 接口 | 字段 | 说明 |
|---------|------|------|------|
| ⚠️ 入参新增 | `POST /v3/mp/aftersale/ticket` | `mediaTraceIds` | 新增可选字段,机审 traceId 数组,最多 12 个 |
| ✨ 出参新增 | `GET /v3/mp/aftersale/ticket/{id}` | `auditStatus` | 工单 wx 内容安全机审状态 |
| ✨ 出参新增 | `GET /v3/mp/aftersale/ticket/list` | `auditStatus` | 工单列表每条记录新增机审状态 |
---
## 3. 接口详情
### 3.1 发起售后工单
| 属性 | 值 |
|-----|-----|
| 方法 + 路径 | `POST /v3/mp/aftersale/ticket` |
| 接口描述 | 用户发起售后工单(投诉/申诉),支持附件 + 机审 traceId 透传 |
| 认证 | 需要用户 JWT小程序登录 token |
| 幂等性 | 非幂等,每次调用创建一条新工单 |
| 限流 | 无独立限流(受网关全局限流) |
### 3.2 工单详情
| 属性 | 值 |
|-----|-----|
| 方法 + 路径 | `GET /v3/mp/aftersale/ticket/{id}` |
| 接口描述 | 获取单条售后工单详情 |
| 认证 | 需要用户 JWT,仅可查自己的工单 |
| 限流 | 无独立限流 |
### 3.3 工单列表
| 属性 | 值 |
|-----|-----|
| 方法 + 路径 | `GET /v3/mp/aftersale/ticket/list` |
| 接口描述 | 查询当前用户的售后工单列表(分页) |
| 认证 | 需要用户 JWT |
| 限流 | 无独立限流 |
---
## 4. 接口入参
### 4.1 路径参数 / Query 参数
`GET /v3/mp/aftersale/ticket/{id}`
| 参数名 | 类型 | 必填 | 说明 |
|-------|------|------|------|
| id | Long | 是 | 工单 ID |
### 4.2 请求体字段POST /v3/mp/aftersale/ticket
| 字段名 | 类型 | 必填 | 校验 | 说明 |
|-------|------|------|------|------|
| orderId | Long | 是 | 不能为空 | 关联订单 ID |
| category | String | 是 | 枚举COMPLAINT / APPEAL | 工单分类:投诉 / 申诉 |
| type | String | 是 | 枚举,见 §6 | 具体类型(如 HOTEL / GUIDE / PRICE |
| title | String | 是 | max=100 | 工单标题 |
| description | String | 是 | max=2000 | 问题描述 |
| attachments | List\<String\> | 否 | max=10 | 附件 URL 列表(图片/视频) |
| **mediaTraceIds** | **List\<String\>** | **否** | **max=12** | **微信内容安全 traceId 数组(前端透传,后端只存)** |
> `mediaTraceIds``attachments` 一一对应关系由前端维护。后端不校验对应关系,仅存储用于异步机审回流。无附件时可不传或传空数组。
---
## 5. 出参字段
### TicketMpRespVO工单详情/列表单条)
| 字段名 | 类型 | 说明 |
|-------|------|------|
| id | Long | 工单 ID |
| orderId | Long | 关联订单 ID |
| orderNo | String | 订单编号 |
| category | String | 工单分类枚举COMPLAINT / APPEAL |
| type | String | 工单类型 |
| title | String | 标题 |
| description | String | 描述 |
| status | String | 工单状态SUBMITTED / PROCESSING / RESOLVED / CLOSED / WITHDRAWN |
| statusName | String | 工单状态中文名 |
| canWithdraw | Boolean | 是否可撤回 |
| **auditStatus** | **String** | **wx 内容安全机审状态,取值见 §6** |
| attachments | List\<String\> | 附件 URL |
| createdAt | String | 创建时间ISO 8601 |
| updatedAt | String | 更新时间ISO 8601 |
---
## 6. 枚举 / 数据字典
### auditStatus — wx 内容安全机审状态
| 值 | 含义 | 触发条件 |
|----|------|---------|
| `PENDING` | 机审中 | 创建工单时有 mediaTraceIds异步等待微信回调 |
| `APPROVED` | 已通过 | 无 mediaTraceIds 的工单直接置此(免审);微信回调结果合规也置此 |
| `MANUAL_REVIEW` | 人工复审 | 微信机审认为需人工介入 |
| `REJECTED` | 已驳回 | 微信机审判定内容违规 |
> **重要**`PENDING` 状态为异步,工单创建成功后微信回调可能在数秒到数分钟内到达。前端建议在工单详情页轮询或基于页面刷新展示最新状态,不要把 `PENDING` 当作错误。
### category — 工单分类
| 值 | 含义 |
|----|------|
| `COMPLAINT` | 投诉 |
| `APPEAL` | 申诉 |
---
## 7. 错误码
| 错误码 | HTTP 状态 | 含义 | 触发场景 |
|-------|---------|------|---------|
| 581001 | 400 | 工单关联订单不存在 | orderId 无效 |
| 581002 | 400 | 工单已存在,不可重复提交 | 同一订单同分类已有活跃工单 |
| 581003 | 400 | 订单状态不允许发起售后 | 订单未处于可售后状态 |
| 200-001 | 401 | 未授权 | JWT 缺失或过期 |
| 200-002 | 403 | 无权操作 | 工单不属于当前用户 |
---
## 8. 示例
### 8.1 典型成功——带图片机审
**请求**
```http
POST /v3/mp/aftersale/ticket
Authorization: Bearer <user-jwt>
Content-Type: application/json
{
"orderId": 1234567890,
"category": "COMPLAINT",
"type": "HOTEL",
"title": "酒店降级安排",
"description": "预订了四星酒店,实际安排了三星,差价未退。",
"attachments": [
"https://oss.example.com/media/hotel_photo_1.jpg",
"https://oss.example.com/media/hotel_photo_2.jpg"
],
"mediaTraceIds": [
"trace_abc123def456",
"trace_xyz789uvw012"
]
}
```
**响应**
```json
{
"code": 200,
"msg": "success",
"data": {
"id": 987654321,
"orderId": 1234567890,
"orderNo": "HL20260622001",
"category": "COMPLAINT",
"type": "HOTEL",
"title": "酒店降级安排",
"description": "预订了四星酒店,实际安排了三星,差价未退。",
"status": "SUBMITTED",
"statusName": "已提交",
"canWithdraw": true,
"auditStatus": "PENDING",
"attachments": [
"https://oss.example.com/media/hotel_photo_1.jpg",
"https://oss.example.com/media/hotel_photo_2.jpg"
],
"createdAt": "2026-06-22T10:30:00",
"updatedAt": "2026-06-22T10:30:00"
}
}
```
> `auditStatus` 初始为 `PENDING`,等待微信异步回调后更新为 `APPROVED` / `MANUAL_REVIEW` / `REJECTED`
### 8.2 边界情况——无附件工单(直接 APPROVED
**请求**
```http
POST /v3/mp/aftersale/ticket
Authorization: Bearer <user-jwt>
Content-Type: application/json
{
"orderId": 1234567890,
"category": "APPEAL",
"type": "PRICE",
"title": "价格异议",
"description": "行程中临时要求加收费用,合同未约定。",
"attachments": [],
"mediaTraceIds": []
}
```
**响应**
```json
{
"code": 200,
"msg": "success",
"data": {
"id": 987654322,
"orderId": 1234567890,
"orderNo": "HL20260622001",
"category": "APPEAL",
"type": "PRICE",
"title": "价格异议",
"description": "行程中临时要求加收费用,合同未约定。",
"status": "SUBMITTED",
"statusName": "已提交",
"canWithdraw": true,
"auditStatus": "APPROVED",
"attachments": [],
"createdAt": "2026-06-22T10:35:00",
"updatedAt": "2026-06-22T10:35:00"
}
}
```
> 无 mediaTraceIds 或传空数组时,`auditStatus` 直接为 `APPROVED`,无需等待回调。
### 8.3 业务失败——订单已有活跃工单
**请求**(同一订单同分类已存在未关闭工单)
```http
POST /v3/mp/aftersale/ticket
Authorization: Bearer <user-jwt>
Content-Type: application/json
{
"orderId": 1234567890,
"category": "COMPLAINT",
"type": "GUIDE",
"title": "导游服务差",
"description": "导游态度恶劣。",
"mediaTraceIds": []
}
```
**响应**
```json
{
"code": 581002,
"msg": "工单已存在,不可重复提交",
"data": null
}
```
---
## 9. 业务边界
**适用场景**
- 订单处于可售后状态(已出行、出行中、已完成等,具体由后端校验)
- 有图片/视频附件时前端必须先完成 `wx.msgSecCheck` 得到 traceId 再调此接口
- 无附件或不需要机审时,`mediaTraceIds` 可省略或传空数组
**不适用场景**
- 已取消订单不可发起售后
- 同一订单同分类已有活跃(非 CLOSED/WITHDRAWN工单时,不可重复发起
**特殊边界**
- `mediaTraceIds` 最多 12 个,超出返回参数校验错误
- `auditStatus``PENDING` 时工单照常流转(客服可正常处理),机审结果不阻塞工单流程
- `auditStatus``REJECTED` 时,仅作展示标记,工单处理流程由客服决定是否关闭
---
## 10. 修改前后对比
### 字段级对比
**CreateTicketReqVO入参**
| 字段 | 变更前 | 变更后 |
|------|-------|-------|
| mediaTraceIds | 不存在 | 新增,List\<String\>,可选,max=12 |
**TicketMpRespVO出参**
| 字段 | 变更前 | 变更后 |
|------|-------|-------|
| auditStatus | 不存在 | 新增,String,wx 机审状态 |
### 行为级对比
| 场景 | 变更前 | 变更后 |
|------|-------|-------|
| 带图片发起工单 | 无机审,无审核状态 | 存 traceId,初始 auditStatus=PENDING,微信异步回调更新 |
| 无图片发起工单 | 无机审状态字段 | auditStatus 直接置 APPROVED |
---
## 11. 影响评估 / 回滚
| 维度 | 结论 |
|------|------|
| 破坏兼容性 | 否(入参新增可选字段,旧版不传兼容;出参新增字段,旧版忽略) |
| 前端同步上线 | 建议同步:入参传 mediaTraceIds 发挥机审能力;出参展示 auditStatus |
| 回滚方案 | 后端回滚 PR #4203 即可,无 DDL 数据损失风险DDL 只加列不删) |
---
## 12. 注意事项
1. `mediaTraceIds` 由前端在调用微信 `wx.msgSecCheck` 后获得,后端不直接调微信安全接口,只做存储和转发。
2. `auditStatus=PENDING` 是异步状态,不代表工单异常,客服后台可正常处理。
3. 历史工单(此 PR 上线前创建的)`auditStatus` 字段为 `null`,前端展示时建议视 `null` 等同于 `APPROVED` 处理。
4. 微信机审结果 `REJECTED` 不自动关闭工单,仅打标记,由客服决定后续处理。
---
## 13. 关联 / 联系人
- **Issue**[https://git.1814.love:8443/wx/HL/issues/4196](https://git.1814.love:8443/wx/HL/issues/4196)
- **PR**[https://git.1814.love:8443/wx/HL/pulls/4203](https://git.1814.love:8443/wx/HL/pulls/4203)
- **Commit**[https://git.1814.love:8443/wx/HL/commit/8882eb1568a7e5ab8f48a6db4b7e3d4d4b6f2c15](https://git.1814.love:8443/wx/HL/commit/8882eb1568a7e5ab8f48a6db4b7e3d4d4b6f2c15)
- **关联 Issue**[#4161 售后申诉中心](https://git.1814.love:8443/wx/HL/issues/4161)
- **后端负责人**yaosutu

查看文件

@ -1,31 +0,0 @@
# 【修正·小程序】售后工单 mp 端接口加归属校验(防横向越权)
> 模块order-v3 售后工单aftersale小程序端
> 类型:安全加固(每日审查驱动,新模块上线前修正)
> 影响端点:`/v3/mp/aftersale/ticket/**`(我的工单列表 / 详情 / 撤回)
> 关联:售后工单中心 PR1-5#4162/#4165/#4168/#4173/#4185)的每日审查修正
> 测试服已部署验证9443 实测端点 200
## ⚠️ 关键说明
售后工单 mp 端三个用户入口此前缺少归属校验,任意登录用户传他人 `ticketId`/`orderId` 即可读取或撤回他人工单(横向越权 IDOR。本次加固**经订单归属校验当前用户是否为下单人**,非本人操作返回错误码 **586014TICKET_NOT_OWN**
正常业务流程下,小程序用户只操作自己订单的售后工单,**不会触发**该错误;前端无需改动,仅需对 586014 做兜底提示(如「无权访问该工单」)。
## 1. 受影响端点与行为
| 端点 | 修正 |
|------|------|
| `GET /v3/mp/aftersale/ticket/my`(我的工单列表) | 新增按当前用户校验 `orderId` 归属;端点入参不变,仅服务端加校验 |
| `GET /v3/mp/aftersale/ticket/{id}`(工单详情) | 校验工单所属订单归属当前用户,非本人抛 586014 |
| `POST /v3/mp/aftersale/ticket/{id}/withdraw`(撤回) | 同上归属校验,非本人不可撤回(且不再触发他人 OA 审批撤销) |
## 2. 附带修正:撤回工单不再消失
撤回WITHDRAWN此前被误当软删处理,导致撤回后的工单在「我的工单」列表/详情中**永久不可见**。本次修正后,**撤回是可见终态**,撤回的工单仍可在列表与详情中查看(状态显示「已撤回」)。
## 3. 错误码
| code | 含义 |
|------|------|
| 586014 | 无权操作该售后工单(非本人订单)|

查看文件

@ -1,222 +0,0 @@
# 退款申诉(新增)+ 统一售后工单接口下线(小程序端)
- 端类型:小程序端
- 变更类型新增接口4+ 删除接口(统一售后工单 mp 端)
- 关联 Issue#4250 PR#4251 #4254
- 日期2026-06-23
---
## ① 接口背景
售后体系重构:原"投诉 + 申诉统一售后工单"方向作废,拆为**投诉**与**申诉**两套独立能力。
- **申诉**:用户对"退款被拒"或"退款金额"不服时发起,**小程序入口仍是「退款申诉」**。申诉记录单独存(不再混入退款申请表)。
- **关键变化**:申诉**复用既有退款审批**——发起申诉后系统自动建一笔 PENDING 退款申请,由管理员在退款审批里复核(通过则退款 / 驳回则结束);申诉本身不再有独立审批。
- 原统一售后工单接口(`/v3/mp/aftersale/ticket*`**全部下线**。
---
## ② 变更清单
| # | 方法 | 路径 | 类型 |
|---|---|---|---|
| 1 | POST | `/v3/mp/refund/appeal` | 🆕 新增(发起申诉)|
| 2 | GET | `/v3/mp/refund/appeal/my` | 🆕 新增(我的申诉,分页)|
| 3 | GET | `/v3/mp/refund/appeal/{id}` | 🆕 新增(申诉详情)|
| 4 | POST | `/v3/mp/refund/appeal/{id}/withdraw` | 🆕 新增(撤回申诉)|
| 5 | ALL | `/v3/mp/aftersale/ticket*` | ❌ 删除(统一售后工单下线,调用返回 404|
统一响应包装 `Result<T>``{ code, message, data, success }``code=200` 为成功。
---
## ③ 接口详情
### 1. 发起申诉 `POST /v3/mp/refund/appeal`
用户对一笔退款申请发起申诉。两种类型:`REFUND_REJECTED`(退款被拒,原退款 status=REJECTED 才可申诉)/ `REFUND_AMOUNT_DISPUTE`(退款金额异议,原退款 status=REFUNDED 求补差)。
> 发起成功后:① 落一条申诉记录PENDING② **自动建一笔 PENDING 退款申请**走退款审批 ③ 订单进入「售后中」。
### 2. 我的申诉 `GET /v3/mp/refund/appeal/my`
当前登录用户的申诉分页列表。
### 3. 申诉详情 `GET /v3/mp/refund/appeal/{id}`
单条申诉详情,含关联的两笔退款(来源退款 + 申诉触发的退款)。
### 4. 撤回申诉 `POST /v3/mp/refund/appeal/{id}/withdraw`
`PENDING`(审批中)状态可撤回;撤回会一并取消申诉触发的那笔 PENDING 退款申请。
---
## ④ 入参
### 接口1 create`@RequestBody`,登录态 userId 由网关注入)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| refundApplicationId | long | 是 | 来源退款申请 ID被申诉的那笔退款|
| appealType | string | 是 | 申诉类型:`REFUND_REJECTED` / `REFUND_AMOUNT_DISPUTE` |
| appealReason | string | 是 | 申诉理由≤2000 字)|
| appealAmount | string(decimal) | 否 | 期望退款金额(金额异议时填)|
| mediaTraceIds | string[] | 否 | 凭证图片机审 traceId 列表前端上传后透传,≤12|
### 接口2 my
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| pageNo | int | 否 | 页码,默认 1 |
| pageSize | int | 否 | 每页条数,默认 20 |
### 接口3 detail / 接口4 withdraw
| 参数 | 位置 | 类型 | 说明 |
|---|---|---|---|
| id | path | long | 申诉 ID |
---
## ⑤ 出参
### 接口1 create `Result<Long>`
`data` = 新建申诉 ID字符串化 Long
### 接口2 my `Result<PageResult<AppealMpRespVO>>`;接口3 detail `Result<AppealMpRespVO>`
`PageResult``{ records[], total, page, pageSize }``AppealMpRespVO`
| 字段 | 类型 | 说明 |
|---|---|---|
| id | string(Long) | 申诉 ID |
| orderId | string(Long) | 订单 ID |
| refundApplicationId | string(Long) | 来源退款申请 ID |
| appealType | string | 申诉类型(枚举值,见⑥)|
| appealTypeLabel | string | 申诉类型中文名 |
| appealReason | string | 申诉理由 |
| appealAmount | string(decimal) | 申诉退款金额 |
| appealStatus | string | 申诉状态(枚举值,见⑥)|
| appealStatusLabel | string | 申诉状态中文名 |
| applicantName | string | 申请人姓名 |
| reviewedAt | datetime | 审批完成时间(可空)|
| createTime | datetime | 创建时间 |
| sourceRefund | object | 来源退款信息(被申诉的原退款),见下 |
| triggeredRefund | object | 申诉触发的退款信息(申诉建单后自动生成的退款),见下 |
`sourceRefund` / `triggeredRefund` 结构(关联退款信息):
| 字段 | 类型 | 说明 |
|---|---|---|
| refundApplicationId | string(Long) | 退款申请 ID |
| status | string | 退款状态PENDING/APPROVED/REJECTED/REFUNDING/REFUNDED/CANCELLED/ABNORMAL|
| actualAmount | string(decimal) | 实际退款金额(可空)|
### 接口4 withdraw `Result<Void>`
`data` = null。
---
## ⑥ 枚举 / 数据字典
**申诉类型 appealType**
| 值 | 含义 |
|---|---|
| REFUND_REJECTED | 退款被拒(原退款被驳回后申诉)|
| REFUND_AMOUNT_DISPUTE | 退款金额异议(已退款但金额有异议,求补差)|
**申诉状态 appealStatus**
| 值 | 含义 |
|---|---|
| PENDING | 审批中(申诉触发的退款申请待退款审批)|
| REJECTED | 已驳回(退款审批驳回)|
| REFUNDED | 退款到账(申诉成功,退款已退)|
| WITHDRAWN | 已撤回 |
---
## ⑦ 错误码(段位 530600-530699
| code | message |
|---|---|
| 530601 | 申诉必须关联一笔退款申请 |
| 530602 | 关联的退款申请不存在 |
| 530603 | 退款申请当前状态不满足申诉条件 |
| 530604 | 该退款申请已有处理中的申诉,请勿重复提交 |
| 530605 | 申诉退款金额超出可退余额 |
| 530606 | 申诉记录不存在 |
| 530607 | 无权操作此申诉 |
| 530608 | 当前申诉状态不允许撤回,仅待审核状态可撤回 |
| 530609 | 申诉类型无效 |
---
## ⑧ 示例
### 典型:发起申诉(退款被拒)
请求 `POST /v3/mp/refund/appeal`
```json
{ "refundApplicationId": 20001, "appealType": "REFUND_REJECTED",
"appealReason": "退款审核不合理,申请复核", "appealAmount": "300.00",
"mediaTraceIds": [] }
```
响应:`{ "code":200, "message":"成功", "data":"40001", "success":true }`
### 典型:我的申诉
请求 `GET /v3/mp/refund/appeal/my?pageNo=1&pageSize=10`,响应(节选一项):
```json
{ "code":200, "success":true, "data": { "total":1, "records":[
{ "id":"40001", "orderId":"10001", "appealType":"REFUND_REJECTED", "appealTypeLabel":"退款被拒",
"appealStatus":"PENDING", "appealStatusLabel":"审批中", "appealAmount":"300.00",
"sourceRefund": { "refundApplicationId":"20001", "status":"REJECTED", "actualAmount":null },
"triggeredRefund": { "refundApplicationId":"20009", "status":"PENDING", "actualAmount":null } }
] } }
```
### 异常:重复申诉
对同一退款申请已有处理中申诉时再发起:
```json
{ "code":530604, "message":"该退款申请已有处理中的申诉,请勿重复提交", "success":false }
```
### 异常:调用已下线的工单接口
请求 `POST /v3/mp/aftersale/ticket`(旧统一售后工单接口):
```json
{ "code":404, "message":"Not Found" }
```
---
## ⑨ 业务边界
- 申诉**复用退款审批**:发起后自动建 PENDING 退款申请,管理员在退款审批里复核——**前端不要再调用任何"申诉审批"接口**(已无独立申诉审批)。
- 申诉状态跟随其触发的退款申请退款到账→REFUNDED,退款审批驳回→REJECTED。
- 撤回仅 PENDING 可撤,且会取消触发的 PENDING 退款。
- 申诉发起即把订单标记「售后中」,终结REFUNDED/REJECTED/WITHDRAWN 且无其他活跃售后)后回切。
---
## ⑩ 修改前后对比
| | 修改前 | 修改后 |
|---|---|---|
| 申诉入口 | 统一售后工单 `POST /v3/mp/aftersale/ticket`category=APPEAL| `POST /v3/mp/refund/appeal` |
| 我的申诉 | `/v3/mp/aftersale/ticket/my` | `/v3/mp/refund/appeal/my` |
| 详情/撤回 | `/v3/mp/aftersale/ticket/{id}` `/{id}/withdraw` | `/v3/mp/refund/appeal/{id}` `/{id}/withdraw` |
| 审批 | 申诉独立 OA 审批 | 无(复用退款审批)|
---
## ⑪ 影响评估 / 回滚
- **破坏性**`/v3/mp/aftersale/ticket*` 已删,调用返回 404,前端涉及"退款申诉"的页面**必须切到新申诉接口**。
- 投诉接口不在此列(投诉走 C 端经 BFF 的现有通道,无变化)。
- 回滚:后端回滚 PR #4251 #4254
---
## ⑫ 注意事项
- 金额字段appealAmount / actualAmount均为**字符串**,前端按字符串处理防精度丢失。
- Long 型 ID 均字符串化返回(防 JS 精度)。
- 上传凭证图片仍走既有 wx 内容安全机审通道mediaTraceIds 透传)。
---
## ⑬ 关联 / 联系人
- Issuehttps://git.1814.love:8443/wx/HL/issues/4250
- PRhttps://git.1814.love:8443/wx/HL/pulls/4251 ・ https://git.1814.love:8443/wx/HL/pulls/4254
- 后端负责人:腰苏图
- 已部署测试服并网关实调验证通过(申诉接口 200 / 工单接口 404

查看文件

@ -1,336 +0,0 @@
# 发票申请票种修正(最终契约)— 小程序端
> 变更类型:修改接口(破坏性)
> 端类型:小程序端
> 日期2026-06-23 | Issue#4296 | PR#4292 / #4302 | 服务hl-order-service-v3
> ⚠️ **破坏性变更**`invoiceType` 删除 `ELECTRONIC``email` 改为始终必填 + 格式校验、专票限公司抬头 + 必填五项、删除 `mailAddress` 字段。小程序开票页与详情页均需同步改动,上线时前后端须同步发布。
> **勘误(截至 PR #4311 收口)**:抬头字段统一 `titleName`、金额 `amount` 改 JSON number单位元,非分/x100、票种仅 2 值(`VAT_NORMAL`/`VAT_SPECIAL`)、专票限公司抬头、入参和出参均无 `mailAddress`。关联 Issue [#4290](https://git.1814.love:8443/wx/HL/issues/4290) [#4296](https://git.1814.love:8443/wx/HL/issues/4296) [#4310](https://git.1814.love:8443/wx/HL/issues/4310) / PR [#4292](https://git.1814.love:8443/wx/HL/pulls/4292) [#4302](https://git.1814.love:8443/wx/HL/pulls/4302) [#4311](https://git.1814.love:8443/wx/HL/pulls/4311)。
---
## 接口背景
发票模型经 PR #4292 + #4302 多轮修正后定稿:删除「电子发票」票种(业务决策统一走普票),专票收严字段校验(限公司抬头 + 必填银行注册信息,email 改为始终必填(全系统电子交付),删除纸质邮寄字段 `mailAddress`
本次涉及小程序两个接口:初次申请 `POST /v3/internal/mp/order/{orderId}/invoice/apply` 和重新开票 `POST /v3/internal/mp/invoice/{id}/reissue`,入参字段矩阵相同,出参亦做同步修正(删 `mailAddress`,invoiceType 去 ELECTRONIC
---
## 变更清单
| # | 变更类型 | 说明 |
|---|---------|------|
| 1 | ⚠️ 申请入参删枚举值 | `invoiceType` 删除 `ELECTRONIC`,只保留 `VAT_NORMAL` / `VAT_SPECIAL` |
| 2 | ⚠️ 申请入参删字段 | 删除 `mailAddress`(纸质邮寄地址) |
| 3 | ⚠️ 申请入参校验收严 | `email` 改为始终必填 + 邮箱格式校验 |
| 4 | ⚠️ 申请入参校验新增 | `taxNo`:单位抬头或专票时必填 |
| 5 | ⚠️ 申请入参校验新增 | 专票:`bankName` / `bankAccount` / `registAddress` / `registPhone` 四项必填 |
| 6 | ⚠️ 申请入参新增错误码 | `581523` 专票只能开给单位 |
| 7 | ⚠️ 详情出参删字段 | `GET /v3/internal/mp/order/{orderId}/invoice/detail` 出参删除 `mailAddress` |
| 8 | ⚠️ 列表出参枚举收窄 | `GET /v3/internal/mp/order/{orderId}/invoice/list` 出参 invoiceType 不再出现 ELECTRONIC |
| 9 | 重开接口同步 | `POST /v3/internal/mp/invoice/{id}/reissue` 入参字段矩阵与申请接口保持一致 |
---
## 接口详情
### 小程序申请接口
| 项 | 说明 |
|---|------|
| **方法 + 路径** | `POST /v3/internal/mp/order/{orderId}/invoice/apply` |
| **接口名** | 小程序客户自主申请发票 |
| **描述** | 客户在订单完成后自主提交开票申请,支持普票/专票,全电子交付 |
| **认证** | 小程序 JWTBearer Token,C 端用户) |
| **幂等性** | 非幂等,重复提交触发 581511一单一票 |
| **限流** | 无特殊限流 |
### 小程序重开接口
| 项 | 说明 |
|---|------|
| **方法 + 路径** | `POST /v3/internal/mp/invoice/{id}/reissue` |
| **接口名** | 小程序重新开票 |
| **描述** | 旧发票作废后,客户重新提交开票申请;入参字段矩阵与 apply 接口相同 |
| **认证** | 小程序 JWTBearer Token,C 端用户) |
| **幂等性** | 非幂等 |
| **限流** | 无特殊限流 |
---
## 接口入参
### 路径参数
**apply 接口:**
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `orderId` | stringLong 雪花 ID | 是 | 订单 ID |
**reissue 接口:**
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `id` | stringLong 雪花 ID | 是 | 被重开的旧发票 ID |
### 请求体字段(两接口共用)
| 字段名 | 类型 | 必填条件 | 说明 |
|--------|------|---------|------|
| `invoiceType` | string | 始终必填 | 发票类型:`VAT_NORMAL`(普票)/ `VAT_SPECIAL`(专票);**不含 ELECTRONIC** |
| `titleType` | string | 始终必填 | 抬头类型:`COMPANY`(单位)/ `PERSONAL`(个人);**VAT_SPECIAL 只能 COMPANY** |
| `titleName` | string | 始终必填 | 发票抬头 |
| `taxNo` | string | `titleType=COMPANY``invoiceType=VAT_SPECIAL` 时必填 | 纳税人识别号 |
| `bankName` | string | `invoiceType=VAT_SPECIAL` 时必填 | 开户银行 |
| `bankAccount` | string | `invoiceType=VAT_SPECIAL` 时必填 | 银行账号 |
| `registAddress` | string | `invoiceType=VAT_SPECIAL` 时必填 | 注册地址 |
| `registPhone` | string | `invoiceType=VAT_SPECIAL` 时必填 | 注册电话 |
| `amount` | number | 始终必填 | 开票金额,JSON number,单位**元**(如 `986.00`),须 > 0 且不超过订单总金额 |
| `email` | string | 始终必填 | 收件邮箱,须通过邮箱格式校验 |
| `remark` | string | 选填 | 备注 |
> ⚠️ `mailAddress` 字段**已删除**,不再接收。
---
## 出参字段
### apply / reissue 接口出参
返回结构:`Result<Long>`
| 字段名 | 类型 | 说明 |
|--------|------|------|
| `data` | string | 新建/重开发票 ID,字符串雪花 ID |
### 发票详情出参变化GET /v3/internal/mp/order/{orderId}/invoice/detail
| 字段名 | 变化 | 说明 |
|--------|------|------|
| `mailAddress` | **已删除** | 原出参中此字段已移除,前端不应再渲染邮寄地址 |
| `invoiceType` | 值范围收窄 | 只会出现 `VAT_NORMAL` / `VAT_SPECIAL`,不再出现 `ELECTRONIC` |
| `email` | 始终有值 | 原可能为 null,现始终有值 |
---
## 枚举 / 数据字典
### 发票类型invoiceType
| 枚举值 | 中文名 | 适用抬头 | 专票字段要求 |
|--------|--------|---------|------------|
| `VAT_NORMAL` | 增值税普通发票 | COMPANY / PERSONAL | 专票四项均不需要 |
| `VAT_SPECIAL` | 增值税专用发票 | **仅 COMPANY** | taxNo + bankName + bankAccount + registAddress + registPhone 全必填 |
> ~~`ELECTRONIC`~~ 已删除,不得传入。
### 抬头类型titleType
| 枚举值 | 中文名 | 可选票种 |
|--------|--------|---------|
| `COMPANY` | 单位 | VAT_NORMAL / VAT_SPECIAL |
| `PERSONAL` | 个人 | 只能 VAT_NORMAL,选 VAT_SPECIAL 返回 581523 |
---
## 错误码
| 错误码 | 含义 | 触发场景 |
|--------|------|---------|
| `581510` | 订单未完成,不可开票 | 订单状态不是 COMPLETED |
| `581511` | 一单一票,已有有效发票 | 已有 REQUESTED / ISSUED / PUSHED 状态发票 |
| `581512` | 开票金额超订单总额 | `amount` > 订单 `orderAmount` |
| `581513` | 发票类型非法 | `invoiceType` 不是 VAT_NORMAL 或 VAT_SPECIAL |
| `581514` | 公司抬头或专票须填税号 | 单位抬头或专票时 `taxNo` 为空 |
| `581515` | 专票须填银行及注册信息 | 专票时任一四项为空 |
| `581516` | 收件邮箱不能为空 | HTTP 400,入参层 @NotBlank 校验;格式错误返回 400「邮箱格式不正确」 |
| `581523` | 专票只能开给单位 | `invoiceType=VAT_SPECIAL``titleType=PERSONAL` |
| `401` | 未授权 | 未携带有效 JWT |
| `403` | 无权限 / 越权 | 非订单归属用户操作 |
---
## 示例
### 典型成功(增值税普通发票,个人抬头)
请求:
```
POST /v3/internal/mp/order/1920000000000000001/invoice/apply
Authorization: Bearer <mp-token>
Content-Type: application/json
```
```json
{
"invoiceType": "VAT_NORMAL",
"titleType": "PERSONAL",
"titleName": "张三",
"taxNo": null,
"amount": 986.00,
"email": "zhangsan@qq.com",
"remark": "旅游报销"
}
```
响应:
```json
{
"code": 200,
"msg": "success",
"data": "1934567890123456789"
}
```
### 典型成功(增值税专用发票,公司抬头,全字段)
请求:
```
POST /v3/internal/mp/order/1920000000000000002/invoice/apply
Authorization: Bearer <mp-token>
Content-Type: application/json
```
```json
{
"invoiceType": "VAT_SPECIAL",
"titleType": "COMPANY",
"titleName": "某某科技有限公司",
"taxNo": "91310000XXXXXXXXXX",
"bankName": "招商银行上海支行",
"bankAccount": "1234567890123456",
"registAddress": "上海市浦东新区XX路XX号",
"registPhone": "021-88888888",
"amount": 2980.00,
"email": "finance@company.com"
}
```
响应:
```json
{
"code": 200,
"msg": "success",
"data": "1934567890123456790"
}
```
### 业务失败(专票个人抬头,错误码 581523
请求:
```
POST /v3/internal/mp/order/1920000000000000003/invoice/apply
Authorization: Bearer <mp-token>
Content-Type: application/json
```
```json
{
"invoiceType": "VAT_SPECIAL",
"titleType": "PERSONAL",
"titleName": "李四",
"taxNo": null,
"amount": 500.00,
"email": "lisi@example.com"
}
```
响应:
```json
{
"code": 581523,
"msg": "增值税专用发票只能开给单位,个人抬头不可选专票",
"data": null
}
```
---
## 业务边界
**适用场景**
- 客户在小程序「我的订单 - 订单详情」中点「申请发票」,订单状态为 COMPLETED 时可用
- 旧发票被作废后,客户在小程序发起重新申请reissue 接口)
**不适用场景**
- 订单未完成(非 COMPLETED 状态),返回 581510
- 已有有效发票,须先由财务作废后才能 reissue,不能再次 apply
- 管理后台代客申请走 admin 专属接口
**特殊边界**
- 金额单位:`amount` 为 JSON number,单位**元**(如 `986.00`),前端输入框以元为单位直接提交,无需 x100
- 一单一票:同一订单至多一张有效发票
- 专票抬头联动:选专票后,抬头类型需强制为单位,个人选项需禁用或隐藏
- 详情回显:`mailAddress` 字段已不存在,前端不应渲染邮寄地址块
---
## 修改前后对比
### 发票类型枚举
| 旧值 | 新值 |
|------|------|
| VAT_NORMAL保留 | VAT_NORMAL保留 |
| VAT_SPECIAL保留 | VAT_SPECIAL保留 |
| ELECTRONIC电子发票 | **已删除** |
### email 字段校验
| 旧行为 | 新行为 |
|--------|--------|
| 条件必填ELECTRONIC 时才必填) | **始终必填 + 邮箱格式校验** |
### mailAddress 字段
| 旧行为 | 新行为 |
|--------|--------|
| 申请入参可选填;详情/列表出参包含此字段 | **申请入参已删除;详情出参已删除** |
### 专票限制
| 旧行为 | 新行为 |
|--------|--------|
| titleType 无限制 | 专票只能 COMPANY个人返回 581523 |
| bankName 等无强制要求 | 专票 taxNo + 银行四项全部必填(返回 581515 |
---
## 影响评估 / 回滚
### 小程序开票页必须改动
| 模块 | 必须改动 |
|------|---------|
| 发票类型选择 | 去掉「电子发票」,只保留「增值税普通发票」/「增值税专用发票」 |
| 专票表单区块 | 新增开户行、银行账号、注册地址、注册电话 4 个必填项,仅选专票时显示 |
| 抬头类型联动 | 选专票时隐藏/禁用「个人」选项 |
| email 输入框 | 改为必填(加红星),增加邮箱格式校验 |
| mailAddress 输入框 | 移除,不再渲染邮寄地址块 |
| 详情页 | 移除邮寄地址展示区,invoiceType 显示只有普票/专票两种文案 |
### 回滚方案
后端回滚至 PR #4292 前,ELECTRONIC 重新有效,email 恢复条件必填,mailAddress 重新可用。小程序需同步回滚开票页逻辑。
---
## 注意事项
1. `ELECTRONIC` 已从枚举删除,小程序开票选项只有两项,禁止出现「电子发票」
2. 重开接口reissue入参字段矩阵与 apply 完全相同,改动同步适用
3. 专票五项taxNo + 开户行 + 账号 + 注册地址 + 注册电话)在专票场景下全必填,缺一返回 581515
4. 详情页 `mailAddress` 字段已消失,老版本小程序读到 undefined 需做好空值守卫(不报错)
5. `amount` 为 JSON number,单位**元**(如 `986.00`),小程序输入框直接传元值,无需 x100
---
## 关联 / 联系人
| 项 | 内容 |
|----|------|
| **Issue功能** | [#4290 发票申请票种修正](https://git.1814.love:8443/wx/HL/issues/4290) |
| **Issue小程序** | [#4296 小程序发票申请同步修正](https://git.1814.love:8443/wx/HL/issues/4296) |
| **PR票种删 ELECTRONIC** | [#4292](https://git.1814.love:8443/wx/HL/pulls/4292) |
| **PR专票限公司/email 必填/删 mailAddress** | [#4302](https://git.1814.love:8443/wx/HL/pulls/4302) |
| **后端负责人** | yst |

查看文件

@ -1,217 +0,0 @@
# 发票申请删 amount · 门槛放宽(小程序端)
- **接口**POST /v3/internal/mp/order/{orderId}/invoice/apply
- **变更类型**:修改接口,破坏性变更(申请入参删 amount;申请门槛扩展;新增错误码 581524
- **端类型**:小程序端
- **日期**2026-06-25
- **Issue**[#4353](https://git.1814.love:8443/wx/HL/issues/4353)
- **PR**[#4360](https://git.1814.love:8443/wx/HL/pulls/4360)
---
## 1 接口背景
小程序端发票申请接口POST /v3/internal/mp/order/{orderId}/invoice/apply本次同步两项改动
1. **申请入参删 amount**:开票金额不再前端传入,开票时后端按规则自动计算(正常单=应收总额;取消单=净实收)。
2. **申请门槛放宽**:原门槛仅允许 COMPLETED已完成订单申请;放宽为定制中及以后均可申请,同时支持已取消但净实收 > 0 的订单申请。
---
## 2 变更清单
| # | 变更项 | 变更前 | 变更后 |
|---|--------|--------|--------|
| 1 | 申请入参 amount | 必填字段,客户填写开票金额 | ⚠️ 已删除,后端开票时自动算 |
| 2 | 申请门槛(正常订单) | 订单状态必须 COMPLETED | 状态 ∈ {CUSTOMIZING / PENDING_DEPARTURE / TRAVELLING / COMPLETED} 均可申请 |
| 3 | 申请门槛(取消订单) | 不允许 | CANCELLED 且净实收(已付 - 已退)> 0 时可申请 |
| 4 | 不满足门槛的错误码 | 581510 INVOICE_ORDER_NOT_COMPLETED | ✨ 新增 581524 INVOICE_ORDER_NOT_APPLICABLE更精确语义 |
---
## 3 接口详情
| 属性 | 值 |
|------|----|
| 方法 | POST |
| 路径 | /v3/internal/mp/order/{orderId}/invoice/apply |
| 描述 | 客户自主申请开票(申请阶段不填金额,开票时后端自动算) |
| 认证 | Bearer JWT小程序用户 |
| 幂等性 | 非幂等,重复提交触发 581511一单一票 |
| 限流 | 无特殊限制 |
---
## 4 接口入参
### 4.1 路径参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| orderId | stringLong 雪花) | 是 | 订单 ID |
### 4.2 请求体字段MpInvoiceApplyReqVO,⚠ amount 已删)
当前完整字段列表(无 amount
| 字段 | 类型 | 必填条件 | 说明 |
|------|------|---------|------|
| invoiceType | string | 始终必填 | 发票类型,只有 VAT_NORMAL / VAT_SPECIAL 两值 |
| titleType | string | 始终必填 | 抬头类型COMPANY单位/ PERSONAL个人;VAT_SPECIAL 只能 COMPANY |
| titleName | string | 始终必填 | 发票抬头(企业全称或个人姓名) |
| taxNo | string | titleType=COMPANY 或 invoiceType=VAT_SPECIAL 时必填 | 纳税人识别号 |
| bankName | string | invoiceType=VAT_SPECIAL 时必填 | 开户银行名称 |
| bankAccount | string | invoiceType=VAT_SPECIAL 时必填 | 银行账号 |
| registAddress | string | invoiceType=VAT_SPECIAL 时必填 | 注册地址 |
| registPhone | string | invoiceType=VAT_SPECIAL 时必填 | 注册电话 |
| email | string | 始终必填 | 收件邮箱,需通过邮箱格式校验 |
| remark | string | 选填 | 申请备注 |
---
## 5 出参字段
响应:成功返回新建发票 ID字符串,雪花 ID
---
## 6 枚举 / 数据字典
### 6.1 发票类型invoiceType
| code | 说明 |
|------|------|
| VAT_NORMAL | 增值税普通发票 |
| VAT_SPECIAL | 增值税专用发票 |
### 6.2 抬头类型titleType
| code | 说明 | 限制 |
|------|------|------|
| COMPANY | 单位 | 普票/专票均可 |
| PERSONAL | 个人 | 只能选 VAT_NORMAL;选 VAT_SPECIAL 返回 581523 |
### 6.3 订单状态与可申请关系
| 订单状态code | 中文 | 可申请? | 说明 |
|----------------|------|---------|------|
| CUSTOMIZING | 定制中 | 是 | 放宽新增 |
| PENDING_DEPARTURE | 待出行 | 是 | 放宽新增 |
| TRAVELLING | 出行中 | 是 | 放宽新增 |
| COMPLETED | 已完成 | 是 | 原有 |
| CANCELLED | 已取消 | 条件是 | 净实收(已付 - 已退)> 0 时可申请 |
| 其他状态 | - | 否 | 报 581524 |
### 6.4 开票时金额自动计算规则
| 订单类型 | 金额计算公式 |
|----------|-------------|
| 正常单(非 CANCELLED | 应收总额 = 订单总价 + 增项 - 优惠 |
| 取消单CANCELLED | 净实收 = 已付金额 - 已退金额 |
---
## 7 错误码
| 错误码 | 常量 | 触发场景 |
|--------|------|----------|
| 581511 | INVOICE_ALREADY_EXISTS | 该订单已有有效发票,不可重复申请(一单一票) |
| 581513 | INVOICE_TYPE_INVALID | 发票类型枚举值非法 |
| 581514 | INVOICE_TAX_NO_REQUIRED | 公司抬头/专票时税号为必填 |
| 581515 | INVOICE_VAT_SPECIAL_FIELDS_REQUIRED | 专票时开户行/银行账号/注册地址/注册电话为必填 |
| 581516 | INVOICE_EMAIL_REQUIRED | 收件邮箱为必填或格式错误HTTP 400 |
| 581518 | INVOICE_FORBIDDEN | 无权访问该订单IDOR 防护) |
| 581523 | INVOICE_VAT_SPECIAL_PERSONAL_FORBIDDEN | 专票只能开给单位 |
| 581524 | INVOICE_ORDER_NOT_APPLICABLE | ✨ 新增:订单状态不在可申请集合内,或取消单净实收为 0 |
---
## 8 示例
### 8.1 典型成功——普票申请(无 amount 字段)
请求
响应
### 8.2 边界情况——订单处于定制中提前申请
说明:订单状态 CUSTOMIZING定制中,本次放宽后允许申请。开票金额将在财务执行开票操作时按当时应收总额计算。
请求(结构同 8.1,orderId 为定制中订单 ID
响应
### 8.3 业务失败——取消订单净实收为 0 被拒
说明:订单已取消,实付 1000 元且已全额退款,净实收 = 0,无法申请开票。
请求(结构同 8.1,orderId 为该取消订单 ID
响应
---
## 9 业务边界
适用:
- 订单状态 ∈ {CUSTOMIZING / PENDING_DEPARTURE / TRAVELLING / COMPLETED} 均可申请
- CANCELLED 且净实收(已付 - 已退)> 0 时可申请
不适用:
- PAID已支付但未进入定制阶段等其他状态报 581524
- CANCELLED 且净实收 = 0 报 581524
- 订单不属于当前登录用户,报 581518IDOR 防护)
特殊边界:
- 一单一票同一订单只允许一张有效发票REQUESTED/ISSUED/PUSHED 状态),重复申请报 581511
- 开票金额由后端在财务执行 issue 操作时自动计算,小程序端申请时不确定最终金额
---
## 10 修改前后对比
| 字段 / 规则 | 变更前 | 变更后 |
|-------------|--------|--------|
| 申请入参 amount | 必填,客户填写开票金额 | ⚠️ 已删除,后端开票时自动算 |
| 正常订单申请门槛 | 仅 COMPLETED | CUSTOMIZING / PENDING_DEPARTURE / TRAVELLING / COMPLETED |
| 取消订单申请 | 不允许 | 净实收 > 0 时可申请 |
| 不满足门槛错误码 | 581510 INVOICE_ORDER_NOT_COMPLETED已废弃 | 581524 INVOICE_ORDER_NOT_APPLICABLE新增 |
---
## 11 影响评估 / 回滚
破坏兼容性:是
- 小程序申请发票表单必须删除金额输入框,不再传 amount 字段
- 若现有流程中有基于 amount 的前端金额展示或校验逻辑,须同步移除
- 不满足门槛的错误提示由旧 581510 改为 581524,前端若 hardcode 了 581510 的处理逻辑需同步更新
前端同步上线:申请表单删 amount 必须与后端同期上线。
回滚方案:回滚后端至 #4360 前版本,amount 字段恢复必填,申请门槛收回到仅 COMPLETED,581524 不再存在。
---
## 12 注意事项
1. **amount 字段已删**:小程序申请发票表单必须移除金额输入框,不传 amount,传入后端会忽略。
2. **申请门槛放宽的注意点**:定制中等状态提前申请,开票时金额由财务执行 issue 操作时后端按当时应收总额计算,小程序无法在申请时预知最终开票金额。
3. **581524 vs 旧 581510**:新错误码 581524 取代了旧的 581510,含义更精确包含取消单净实收为 0 的场景)。若前端有 581510 的 hardcode 判断需更新为 581524。
4. **开票金额由财务操作确定**小程序端只负责申请,金额由后端在财务开票PUT issue时写入,前端无法在申请时展示最终金额。
---
## 13 关联 / 联系人
- **Issue**[#4353 发票模块门槛放宽与开票规则优化](https://git.1814.love:8443/wx/HL/issues/4353)
- **PR**[#4360](https://git.1814.love:8443/wx/HL/pulls/4360)
- **后端负责人**腰苏图yaosutu

查看文件

@ -1,238 +0,0 @@
# 大交通批次列表: 接口路径破坏性迁移 (transport-plans → transport-plan/list)
> **存放目录**: 二期(v3,`order-v3` 标签)→ `changelogs-v2/2026-05/`
>
> **服务**: hl-order-v3 (端口 8084)
> **PR**: #2544 (squash 后 commit `be3badaca`)
> **Issue**: #2521
> **日期**: 2026-05-18
> **影响范围**: 管理后台「行程安排 - 接送站」区块 - 大交通批次列表
---
## ⚠️ 关键变化(破坏性 - 前端必同步)
一行红字说清:
- **本次变了什么**: 大交通批次列表接口路径从 `/transport-plans` 改为 `/transport-plan/list`
- **前端以前以为的是什么**: `GET /v3/admin/order/{id}/transport-plans` (旧式复数资源风格)
- **实际现在是什么**: `GET /v3/admin/order/{id}/transport-plan/list` (统一 list/add/edit/delete 动词风格)
**配套破坏性**: §2.6 大交通批次模块 4 个接口路径风格 + 动词 + Method 全部统一迁移,见 #2522/#2523/#2524 changelog。前端需要 4 个接口一并改。
---
## 一、背景
V5.48 §2.6 大交通批次模块 4 个接口(list/add/edit/delete)统一为 `/transport-plan/{动词}` 子路径风格,与订单模块其他子资源(出行人 traveler/list、配房 hotel-requirement 等)保持一致:
| 风格 | 旧 (V5.47 之前) | 新 (V5.48) |
|------|-----------------|------------|
| 列表 | `GET /transport-plans` | `GET /transport-plan/list` |
| 新增 | `POST /transport-plans` | `POST /transport-plan/add` |
| 编辑 | `PUT /transport-plans/{planId}` | `POST /transport-plan/{planId}/edit` |
| 软删 | `DELETE /transport-plans/{planId}` | `POST /transport-plan/{planId}/delete` |
统一动词路径后,网关路由 / 权限 RBAC / 操作审计的资源前缀一致,便于配置和扫描。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 大交通批次列表 | GET | `/v3/admin/order/{id}/transport-plan/list` | 路径迁移 | 旧路径 `/transport-plans` 已下线 |
---
## 三、接口详情
### 1. 大交通批次列表 `GET /v3/admin/order/{id}/transport-plan/list`
**VO**: `TransportPlanVO`
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| `id` | Path | Long | ✅ | - | 订单 ID |
#### 出参 `Result<List<TransportPlanVO>>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | String | 批次 ID(雪花) |
| `orderId` | String | 订单 ID |
| `direction` | String | 方向 `ARRIVAL` / `DEPARTURE` |
| `transportType` | String | 类型 `FLIGHT` / `TRAIN` / `SELF_DRIVE` |
| `transportNo` | String | 航班号 / 车次号(SELF_DRIVE 时为 null) |
| `carrier` | String | 航司 / 铁路公司 |
| `departStation` | String | 出发站 |
| `arriveStation` | String | 到达站 |
| `departTime` | LocalDateTime | 出发时间(SELF_DRIVE 时为 null) |
| `arriveTime` | LocalDateTime | 到达时间(SELF_DRIVE 时为 null) |
| `selfDrivePeriod` | String | 仅 SELF_DRIVE: `MORNING` / `AFTERNOON` / `EVENING` |
| `selfDriveEta` | LocalDateTime | 仅 SELF_DRIVE: 预计抵达时间 |
| `travelers` | List<Map> | 桥接表关联出行人,元素 `{id, name}` |
| `remark` | String | 备注 |
#### 请求示例
```
GET /v3/admin/order/60123456789012/transport-plan/list
```
#### 响应示例
```json
{
"code": 200,
"data": [
{
"id": "80012345",
"orderId": "60123456789012",
"direction": "ARRIVAL",
"transportType": "FLIGHT",
"transportNo": "CA1234",
"carrier": "中国国际航空",
"departStation": "北京首都T3",
"arriveStation": "长春龙嘉",
"departTime": "2026-06-01T08:30:00",
"arriveTime": "2026-06-01T10:15:00",
"selfDrivePeriod": null,
"selfDriveEta": null,
"travelers": [
{"id": "70123456789012", "name": "张三"},
{"id": "70123456789013", "name": "王小明"}
],
"remark": "需要接机举牌"
},
{
"id": "80012346",
"orderId": "60123456789012",
"direction": "DEPARTURE",
"transportType": "SELF_DRIVE",
"transportNo": null,
"carrier": null,
"departStation": null,
"arriveStation": null,
"departTime": null,
"arriveTime": null,
"selfDrivePeriod": "AFTERNOON",
"selfDriveEta": "2026-06-05T15:00:00",
"travelers": [
{"id": "70123456789012", "name": "张三"}
],
"remark": null
}
],
"msg": "success"
}
```
#### 空数据响应
订单尚未登记任何大交通批次时:
```json
{ "code": 200, "data": [], "msg": "success" }
```
#### 错误响应
订单不存在:
```json
{
"code": 581100,
"msg": "订单不存在",
"data": null
}
```
---
## 四、契约约束与正确调用方式
### ✅ 正确 / ❌ 错误调用对照
| 场景 | 请求 |
|------|------|
| ✅ 新路径 | `GET /v3/admin/order/60123456789012/transport-plan/list` |
| ❌ 旧路径(已下线 404) | `GET /v3/admin/order/60123456789012/transport-plans` |
### 字段语义约束
- `direction` 取值固定两值: `ARRIVAL`(到达) / `DEPARTURE`(离开)
- `transportType` 三值: `FLIGHT` / `TRAIN` / `SELF_DRIVE`
- `SELF_DRIVE` 类型行: `transportNo` / `departTime` / `arriveTime` 必为 null;`selfDrivePeriod` 必有值
- `FLIGHT` / `TRAIN` 类型行: `transportNo` / `departTime` / `arriveTime` 必有值;`selfDrivePeriod` / `selfDriveEta` 必为 null
- `travelers` 数组永远非空(后端保证),元素至少 1 个
---
## 五、数据库行为
**只读查询**,无写动作。
| 查询步骤 | 表 | 说明 |
|----------|-----|------|
| 1 | `order_transport_plan` | `WHERE order_id = ? AND deleted = 0`,主表批次记录 |
| 2 | LEFT JOIN `order_transport_plan_traveler` | `WHERE deleted = 0`,桥接表关联出行人 ID |
| 3 | JOIN `order_traveler` | `WHERE deleted = 0`,出行人姓名补全 |
| 4 | 内存装配 | 同一 plan 多 traveler 聚合为 `travelers: [{id, name}]` 数组 |
排序: `ORDER BY direction ASC, depart_time ASC, id ASC` (ARRIVAL 先于 DEPARTURE,同方向按时间升序)。
---
## 六、边界行为
- 未登录 → 401(网关拦截)
- 订单不存在 → `581100`
- 订单存在但无任何批次 → 200 + `data: []`(不报错)
- 批次存在但桥接表全软删 → `travelers: []`(理论上不应出现,后端保证)
- 老数据(v2 历史 plan 无 `mode` 字段)→ 字段为 null,不异常
---
## 七、不影响范围(显式声明,帮前端 / QA 缩小排查面)
- **仅影响**: 管理后台 F21 行程安排 Tab 「接送站」区块 - 大交通批次列表渲染
- **零影响**:
- C 端订单详情 mp 接口(不查 plan 表,只查 arrival_plan)
- 订单创建 / 支付 / 退款主流程
- 出行人模块(travelers list 仍走 `/admin/order/{id}/traveler/list`)
- Feign 内部接口 `GET /internal/order/orders/{orderId}/travelers`(回传 `transportPlanIds`,使用的是同表数据但聚合方向相反,不受路径变化影响)
---
## 八、测试环境已验证
测试服 9443 网关 + 真 admin token round-trip 验证:
```
GET https://web.test.1814.love:9443/v3/admin/order/{orderId}/transport-plan/list
→ 200 + data 数组 ✓
→ travelers 嵌套字段返回正确 (id+name) ✓
→ ARRIVAL/DEPARTURE 排序正确 ✓
→ 空订单返 data: [] ✓
```
(commit `15e2c7eeb` 含 hotfix #2550 修复 conflict markers,4 接口一并 9443 真测过)
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| **本 PR #2544** | **#2521** | 路径破坏性迁移 `/transport-plans``/transport-plan/list` | ✅ 最新 |
---
## 十、相关文档
- 关联 Issue: [wx/HL#2521](https://git.1814.love:8443/wx/HL/issues/2521)
- 关联 PR: [wx/HL#2544](https://git.1814.love:8443/wx/HL/pulls/2544)
- API 文档: `docs/order-v3/api/API-SPEC-V5.48.html` §2.6.1
- 配套破坏性: 见 `18_#2522_transport-plan-add.md` / `18_#2523_transport-plan-edit.md` / `18_#2524_transport-plan-delete.md`

查看文件

@ -1,239 +0,0 @@
# 大交通批次新增: 接口路径破坏性迁移 + 4 个新错误码
> **存放目录**: 二期(v3,`order-v3` 标签)→ `changelogs-v2/2026-05/`
>
> **服务**: hl-order-v3 (端口 8084)
> **PR**: #2545 (commit `2c66137c7`)
> **Issue**: #2522
> **日期**: 2026-05-18
> **影响范围**: 管理后台「行程安排 - 接送站」区块 - 大交通批次新增弹窗
---
## ⚠️ 关键变化(破坏性 - 前端必同步)
一行红字说清:
- **本次变了什么**: 新增大交通批次接口路径从 `/transport-plans` 改为 `/transport-plan/add`,并新增 4 个错误码 `581140`-`581143`
- **前端以前以为的是什么**: `POST /v3/admin/order/{id}/transport-plans` + Body `TransportPlanReqVO`
- **实际现在是什么**: `POST /v3/admin/order/{id}/transport-plan/add` + Body `TransportPlanReqVO`(字段不变)
**配套破坏性**: §2.6 大交通批次模块 4 个接口路径风格 + 动词 + Method 全部统一迁移,见 #2521/#2523/#2524 changelog。
---
## 一、背景
V5.48 §2.6.2 将原 `POST /transport-plans` 拆为 `POST /transport-plan/add`,并补齐"字段组合 / 出行人合法性 / 时间顺序 / 同方向冲突"4 类业务校验错误码,与文档 §2.6 错误码段位 `581140`-`581143` 一一对应。
`orderId` 从 path 取,Body 不传;操作人从 JWT 派生(对齐 §2.6 通用约定)。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 大交通批次新增 | POST | `/v3/admin/order/{id}/transport-plan/add` | 路径迁移 + 错误码新增 | 旧 `/transport-plans` 下线;4 个业务错误码上线 |
---
## 三、接口详情
### 1. 大交通批次新增 `POST /v3/admin/order/{id}/transport-plan/add`
**VO**: `TransportPlanReqVO`(入参) / `TransportPlanVO`(出参)
#### 入参 Body `TransportPlanReqVO`
| 字段 | 类型 | 必填 | 约束 | 说明 |
|------|------|:----:|------|------|
| `direction` | String | ✅ | 枚举 | `ARRIVAL` / `DEPARTURE` |
| `transportType` | String | ✅ | 枚举 | `FLIGHT` / `TRAIN` / `SELF_DRIVE` |
| `transportNo` | String | 条件 | ≤50 | 航班号 / 车次号;`SELF_DRIVE` 时必须为 null |
| `carrier` | String | ❌ | ≤50 | 航司 / 铁路公司 |
| `departStation` | String | ❌ | ≤100 | 出发站 |
| `arriveStation` | String | ❌ | ≤100 | 到达站 |
| `departTime` | LocalDateTime | 条件 | - | `FLIGHT`/`TRAIN` 必填;`SELF_DRIVE` 必须为 null |
| `arriveTime` | LocalDateTime | 条件 | - | `FLIGHT`/`TRAIN` 必填;`SELF_DRIVE` 必须为 null;必须 ≥ `departTime` |
| `selfDrivePeriod` | String | 条件 | 枚举 | 仅 `SELF_DRIVE` 必填: `MORNING` / `AFTERNOON` / `EVENING` |
| `selfDriveEta` | LocalDateTime | ❌ | - | 仅 `SELF_DRIVE` 可选 |
| `travelerIds` | List<Long> | ✅ | size≥1 | 关联出行人 ID,至少 1 个 |
| `remark` | String | ❌ | ≤500 | 备注 |
#### 出参 `Result<TransportPlanVO>`
字段同 #2521 列表的元素结构(含 `travelers: [{id, name}]` 嵌套)。
#### 请求示例
```json
POST /v3/admin/order/60123456789012/transport-plan/add
{
"direction": "ARRIVAL",
"transportType": "FLIGHT",
"transportNo": "CA1234",
"carrier": "中国国际航空",
"departStation": "北京首都T3",
"arriveStation": "长春龙嘉",
"departTime": "2026-06-01T08:30:00",
"arriveTime": "2026-06-01T10:15:00",
"selfDrivePeriod": null,
"selfDriveEta": null,
"travelerIds": [70123456789012, 70123456789013],
"remark": "需要接机举牌"
}
```
#### 响应示例
```json
{
"code": 200,
"data": {
"id": "80012345",
"orderId": "60123456789012",
"direction": "ARRIVAL",
"transportType": "FLIGHT",
"transportNo": "CA1234",
"carrier": "中国国际航空",
"departStation": "北京首都T3",
"arriveStation": "长春龙嘉",
"departTime": "2026-06-01T08:30:00",
"arriveTime": "2026-06-01T10:15:00",
"selfDrivePeriod": null,
"selfDriveEta": null,
"travelers": [
{"id": "70123456789012", "name": "张三"},
{"id": "70123456789013", "name": "王小明"}
],
"remark": "需要接机举牌"
},
"msg": "success"
}
```
#### 错误响应
| 错误码 | 含义 | 触发场景示例 |
|--------|------|--------------|
| `581140` | 大交通字段组合不合法 | `FLIGHT``transportNo` / `SELF_DRIVE` 错带 `transportNo``departTime` |
| `581141` | travelerIds 含订单外的出行人 | 提交的 traveler ID 不属于当前订单或已软删 |
| `581142` | 出发时间晚于到达时间 | `departTime > arriveTime` |
| `581143` | 同方向同一出行人已在另一 plan | 张三已在 ARRIVAL plan A,再为 ARRIVAL plan B 提交张三 |
| `581100` | 订单不存在 | path 中 orderId 无效 |
示例 `581140`:
```json
{ "code": 581140, "msg": "大交通字段组合不合法: FLIGHT 类型必须填 transportNo", "data": null }
```
示例 `581143`:
```json
{ "code": 581143, "msg": "同方向同一出行人已在另一 plan: 张三(70123456789012) 已存在 ARRIVAL 批次 80012340", "data": null }
```
---
## 四、契约约束与正确调用方式
### ✅ 正确 / ❌ 错误 payload 对照
| 场景 | payload 关键字段 | 结果 |
|------|------------------|------|
| ✅ FLIGHT 完整 | `transportType:FLIGHT, transportNo:CA1234, departTime/arriveTime 有值` | 201 |
| ✅ SELF_DRIVE 完整 | `transportType:SELF_DRIVE, transportNo:null, departTime:null, arriveTime:null, selfDrivePeriod:MORNING` | 201 |
| ❌ FLIGHT 缺航班号 | `transportType:FLIGHT, transportNo:null` | `581140` |
| ❌ SELF_DRIVE 带航班号 | `transportType:SELF_DRIVE, transportNo:CA1234` | `581140` |
| ❌ 时间倒挂 | `departTime:10:00, arriveTime:08:00` | `581142` |
| ❌ travelerIds 含他人 | `travelerIds:[别单出行人]` | `581141` |
| ❌ 同方向重复占用 | 同方向同 traveler 在另一 plan | `581143` |
### 并发与幂等
- `@Idempotent(timeout = 3)`: 同 admin 3 秒内重复 POST 同请求体直接拿首次结果,防双击双提
- `@Lock4j(keys = "#id")`: 同订单 30 秒锁,串行化 add/edit/delete,保证桥接表一致
### 同方向冲突规则
- 「同方向」= `direction` 相同的所有 plan(同 ARRIVAL 之间冲突,同 DEPARTURE 之间冲突)
- 「同一出行人」= 同一 `traveler_id`
- 不同方向不冲突(同一人可同时在 1 个 ARRIVAL plan + 1 个 DEPARTURE plan)
- 软删的 plan / 桥接行不参与冲突判定
---
## 五、数据库行为
单事务执行,失败整体回滚:
| 步骤 | 表 | 动作 |
|------|-----|------|
| 1 | `order_transport_plan` | INSERT 新行,雪花 ID,`deleted=0`,`create_time`/`update_time` 自动填 |
| 2 | `order_transport_plan_traveler` | INSERT N 行(N = `travelerIds.size()`),每行 `{plan_id, traveler_id, deleted:0}` |
校验顺序(任一失败即抛业务异常,事务回滚):
1. 订单存在性 → `581100`
2. 字段组合校验(类型 × 字段必填矩阵)→ `581140`
3. 时间顺序校验 → `581142`
4. travelerIds 归属校验(SELECT order_traveler WHERE order_id 比对)→ `581141`
5. 同方向同 traveler 占用校验(SELECT bridge WHERE direction = ? AND traveler_id IN ?)→ `581143`
---
## 六、边界行为
- 未登录 → 401(网关拦截)
- 订单不存在 → `581100`
- `travelerIds` 重复值 → 后端去重后再校验(不报错)
- `SELF_DRIVE` 仅传 `selfDrivePeriod` 不传 `selfDriveEta` → 通过(ETA 选填)
- 一次提交超过单订单合理上限(>10 plan)→ 不在此层拦截,业务上仅靠订单状态自然限制
---
## 七、不影响范围
- **仅影响**: 管理后台 F21 「接送站」新增弹窗
- **零影响**:
- C 端订单详情 / 抵达计划(`order_arrival_plan` 表完全独立,见 detail 文档 §10.2 共存边界)
- 出行人模块自身 CRUD
- 订单状态机(plan 增删不直接变 `order_main.status`)
---
## 八、测试环境已验证
测试服 9443 网关 + 真 admin token round-trip 验证:
```
POST https://web.test.1814.love:9443/v3/admin/order/{orderId}/transport-plan/add
→ 200 + 返回新 plan(含 travelers 嵌套)✓
→ FLIGHT 缺 transportNo → 581140 ✓
→ SELF_DRIVE 错带 transportNo → 581140 ✓
→ departTime > arriveTime → 581142 ✓
→ travelerIds 含别单 → 581141 ✓
→ 同方向同 traveler 重复 → 581143 ✓
→ 重复 POST 3s 内同请求体 → 幂等返同结果 ✓
```
(commit `15e2c7eeb` 含 hotfix #2550,4 接口一并 9443 真测过)
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| **本 PR #2545** | **#2522** | 路径迁移 + 错误码 581140-581143 上线 | ✅ 最新 |
---
## 十、相关文档
- 关联 Issue: [wx/HL#2522](https://git.1814.love:8443/wx/HL/issues/2522)
- 关联 PR: [wx/HL#2545](https://git.1814.love:8443/wx/HL/pulls/2545)
- API 文档: `docs/order-v3/api/API-SPEC-V5.48.html` §2.6.2
- 配套破坏性: 见 `18_#2521_transport-plan-list-path.md` / `18_#2523_transport-plan-edit.md` / `18_#2524_transport-plan-delete.md`

查看文件

@ -1,234 +0,0 @@
# 大交通批次编辑: Method 破坏性变更 (PUT → POST) + 错误码 581144
> **存放目录**: 二期(v3,`order-v3` 标签)→ `changelogs-v2/2026-05/`
>
> **服务**: hl-order-v3 (端口 8084)
> **PR**: #2546 (commit `1106b9d7f`) + hotfix #2550 (commit `15e2c7eeb` 修 conflict markers)
> **Issue**: #2523
> **日期**: 2026-05-18
> **影响范围**: 管理后台「行程安排 - 接送站」区块 - 大交通批次编辑弹窗
---
## ⚠️ 关键变化(破坏性 - 前端必同步)
一行红字说清:
- **本次变了什么**: 编辑大交通批次接口 **Method 从 `PUT` 改为 `POST`** + 路径子段从 `/transport-plans/{planId}` 改为 `/transport-plan/{planId}/edit`
- **前端以前以为的是什么**: `PUT /v3/admin/order/{id}/transport-plans/{planId}`
- **实际现在是什么**: `POST /v3/admin/order/{id}/transport-plan/{planId}/edit`
**Method 变化是头号陷阱**: 前端若沿用 `axios.put(...)` 会直接 404 / 405,必须改 `axios.post(...)`
**配套破坏性**: §2.6 大交通批次模块 4 个接口路径风格 + 动词 + Method 全部统一迁移,见 #2521/#2522/#2524
---
## 一、背景
V5.48 §2.6 统一管理后台所有变更动作走 `POST /{资源}/{动词}` 风格(对齐订单核心模块、出行人模块):
| 旧风格 (REST 经典) | 新风格 (V5.48 统一) |
|--------------------|---------------------|
| `PUT /transport-plans/{id}` | `POST /transport-plan/{id}/edit` |
| `DELETE /transport-plans/{id}` | `POST /transport-plan/{id}/delete` |
POST + 动词路径的好处: 网关 RBAC 配置统一只看 path 不看 Method、操作审计日志埋点统一、防误触缓存。
`#2550 hotfix` 修复合并 dev-v3 时 IDE 残留的 `<<<<<<<` conflict markers(扫描全代码确保零残留)。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 大交通批次编辑 | POST | `/v3/admin/order/{id}/transport-plan/{planId}/edit` | Method + 路径双破坏 | 旧 `PUT /transport-plans/{planId}` 下线;新增错误码 `581144` |
---
## 三、接口详情
### 1. 大交通批次编辑 `POST /v3/admin/order/{id}/transport-plan/{planId}/edit`
**VO**: `TransportPlanReqVO`(入参,与 add 同 VO) / `TransportPlanVO`(出参)
#### 入参
| 字段 | 位置 | 类型 | 必填 | 说明 |
|------|------|------|:----:|------|
| `id` | Path | Long | ✅ | 订单 ID |
| `planId` | Path | Long | ✅ | 批次 ID |
| Body | Body | `TransportPlanReqVO` | ✅ | 字段同 add(见 #2522 入参表) |
#### 出参 `Result<TransportPlanVO>`
字段同 #2521 列表元素结构。
#### 请求示例
```json
POST /v3/admin/order/60123456789012/transport-plan/80012345/edit
{
"direction": "ARRIVAL",
"transportType": "FLIGHT",
"transportNo": "CA1234",
"carrier": "中国国际航空",
"departStation": "北京首都T3",
"arriveStation": "长春龙嘉",
"departTime": "2026-06-01T09:00:00",
"arriveTime": "2026-06-01T11:00:00",
"selfDrivePeriod": null,
"selfDriveEta": null,
"travelerIds": [70123456789012, 70123456789013, 70123456789014],
"remark": "改时间,加一人"
}
```
#### 响应示例
```json
{
"code": 200,
"data": {
"id": "80012345",
"orderId": "60123456789012",
"direction": "ARRIVAL",
"transportType": "FLIGHT",
"transportNo": "CA1234",
"carrier": "中国国际航空",
"departStation": "北京首都T3",
"arriveStation": "长春龙嘉",
"departTime": "2026-06-01T09:00:00",
"arriveTime": "2026-06-01T11:00:00",
"selfDrivePeriod": null,
"selfDriveEta": null,
"travelers": [
{"id": "70123456789012", "name": "张三"},
{"id": "70123456789013", "name": "王小明"},
{"id": "70123456789014", "name": "李四"}
],
"remark": "改时间,加一人"
},
"msg": "success"
}
```
#### 错误响应
复用 add 接口的 4 个错误码(`581140`-`581143`),并新增:
| 错误码 | 含义 | 触发场景 |
|--------|------|----------|
| `581144` | plan 不存在 / 不属于该订单 | `planId` 无效 / 已软删 / order_id 与 path 不匹配 |
示例 `581144`:
```json
{ "code": 581144, "msg": "大交通批次不存在或不属于该订单: planId=80099999, orderId=60123456789012", "data": null }
```
---
## 四、契约约束与正确调用方式
### ✅ 正确 / ❌ 错误调用对照
| 场景 | 请求 | 结果 |
|------|------|------|
| ✅ 新调用 | `POST /transport-plan/80012345/edit` + Body | 200 |
| ❌ 沿用旧 PUT | `PUT /transport-plans/80012345` + Body | 404 / 405 |
| ❌ planId 跨单 | edit path `orderId=A`,planId 属于 `orderId=B` | `581144` |
| ❌ planId 已软删 | edit 已 deleted=1 的 plan | `581144` |
### 全量重建语义
- **edit 不是"增量改字段"**: Body 提交什么就最终持久化什么(包括 `travelerIds` 全量)
- 想去掉 1 个 traveler: 在 `travelerIds` 数组中删掉该 ID 再提交,后端会 DELETE+INSERT 重建桥接表
- 不能用 edit "局部改 1 个字段而保留其他字段不动"的语义,前端必须先 GET list 取完整 plan 再改
### 并发与幂等
- `@Lock4j(keys = "#id")`: 同订单 30 秒锁,串行化 add/edit/delete
- 编辑接口不加 `@Idempotent`(每次编辑都应被尊重,允许相同 Body 重复提交以触发 update_time 刷新)
---
## 五、数据库行为
单事务执行,失败整体回滚:
| 步骤 | 表 | 动作 |
|------|-----|------|
| 1 | `order_transport_plan` | UPDATE 该 plan 行所有业务字段(direction / transportType / transportNo / carrier / 各 station / 各 time / selfDrive* / remark),`update_time` 自动刷新 |
| 2 | `order_transport_plan_traveler` | DELETE(物理删除桥接行 WHERE `plan_id = ?`)|
| 3 | `order_transport_plan_traveler` | INSERT N 行(N = 新 `travelerIds.size()`)|
> 桥接表用「全量重建」而非 diff 算法,简化业务逻辑;桥接表本身无业务历史价值,物理删除不留痕。`order_transport_plan` 主表的 `update_time` 会随 edit 刷新供前端展示「最后修改时间」。
校验顺序(同 add,附加 plan 归属校验):
1. plan 存在性 + 归属 → `581144`
2. 订单存在性 → `581100`
3. 字段组合 → `581140`
4. 时间顺序 → `581142`
5. travelerIds 归属 → `581141`
6. 同方向同 traveler 占用(**排除自己**)→ `581143`
---
## 六、边界行为
- 未登录 → 401(网关拦截)
- planId 不存在 / 跨单 / 已软删 → `581144`
- 同方向同 traveler 占用判定**排除当前 plan 自己**(改自己不算冲突,只跟其他 active plan 比对)
- Body 与原 plan 完全相同(无字段变化)→ 200 + 桥接表 DELETE+INSERT(等价 no-op 但 `update_time` 仍刷新)
- 老数据(v2 历史 plan 字段缺失)→ edit 会以 Body 全量值覆盖
---
## 七、不影响范围
- **仅影响**: 管理后台 F21 「接送站」编辑弹窗
- **零影响**:
- C 端订单详情 / mp 抵达计划接口
- 出行人 CRUD(traveler 表本身不动)
- 订单状态机
- 操作日志(本接口暂不挂 `@OperationLog`,与 v2 行为一致)
---
## 八、测试环境已验证
测试服 9443 网关 + 真 admin token round-trip 验证:
```
POST https://web.test.1814.love:9443/v3/admin/order/{orderId}/transport-plan/{planId}/edit
→ 200 + 返回更新后 plan(travelers 重建)✓
→ planId 不存在 → 581144 ✓
→ planId 属于别单 → 581144 ✓
→ 业务校验复用 581140/581141/581142/581143 全覆盖 ✓
→ 同方向同 traveler 占用判定排除自己 → 200 ✓
→ 桥接表全量 DELETE+INSERT 重建生效 ✓
```
**hotfix #2550**(commit `15e2c7eeb`)清理了合并 dev-v3 时残留的 `<<<<<<<` / `=======` / `>>>>>>>` conflict markers(本接口实现文件曾受影响,hotfix 后 mvn compile 通过 + 9443 验证通过)。
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| **本 PR #2546** | **#2523** | Method PUT→POST + 路径迁移 + 错误码 581144 | ✅ 最新 |
| #2550 | (hotfix) | 修复 conflict markers,无功能变化 | ✅ 配套 |
---
## 十、相关文档
- 关联 Issue: [wx/HL#2523](https://git.1814.love:8443/wx/HL/issues/2523)
- 关联 PR: [wx/HL#2546](https://git.1814.love:8443/wx/HL/pulls/2546)
- Hotfix PR: [wx/HL#2550](https://git.1814.love:8443/wx/HL/pulls/2550)
- API 文档: `docs/order-v3/api/API-SPEC-V5.48.html` §2.6.3
- 配套破坏性: 见 `18_#2521_transport-plan-list-path.md` / `18_#2522_transport-plan-add.md` / `18_#2524_transport-plan-delete.md`

查看文件

@ -1,187 +0,0 @@
# 大交通批次软删: Method 破坏性变更 (DELETE → POST) + 桥接表级联软删
> **存放目录**: 二期(v3,`order-v3` 标签)→ `changelogs-v2/2026-05/`
>
> **服务**: hl-order-v3 (端口 8084)
> **PR**: #2547 (commit `68887aba0`)
> **Issue**: #2524
> **日期**: 2026-05-18
> **影响范围**: 管理后台「行程安排 - 接送站」区块 - 大交通批次删除
---
## ⚠️ 关键变化(破坏性 - 前端必同步)
一行红字说清:
- **本次变了什么**: 删除大交通批次接口 **Method 从 `DELETE` 改为 `POST`** + 路径子段从 `/transport-plans/{planId}` 改为 `/transport-plan/{planId}/delete`
- **前端以前以为的是什么**: `DELETE /v3/admin/order/{id}/transport-plans/{planId}`
- **实际现在是什么**: `POST /v3/admin/order/{id}/transport-plan/{planId}/delete`
**Method 变化提醒**: `axios.delete(...)` 必须改 `axios.post(...)`;部分浏览器/代理对 `DELETE` 体行为不一致,统一 POST 后规避。
**配套破坏性**: §2.6 大交通批次模块 4 个接口路径风格 + 动词 + Method 全部统一迁移,见 #2521/#2522/#2523
---
## 一、背景
V5.48 §2.6.4 与 §2.6.3 编辑接口同步,统一 POST + 动词路径风格(对齐订单核心模块、出行人模块写动作)。
软删行为: 主表 + 桥接表**同事务级联软删**,均置 `deleted=1`,不丢历史,可被审计回溯。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 大交通批次软删 | POST | `/v3/admin/order/{id}/transport-plan/{planId}/delete` | Method + 路径双破坏 | 旧 `DELETE /transport-plans/{planId}` 下线 |
---
## 三、接口详情
### 1. 大交通批次软删 `POST /v3/admin/order/{id}/transport-plan/{planId}/delete`
#### 入参
| 字段 | 位置 | 类型 | 必填 | 说明 |
|------|------|------|:----:|------|
| `id` | Path | Long | ✅ | 订单 ID |
| `planId` | Path | Long | ✅ | 批次 ID |
> 无 Body。
#### 出参 `Result<Boolean>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `data` | Boolean | 始终 `true`(失败走错误码,不会返 `false`)|
#### 请求示例
```
POST /v3/admin/order/60123456789012/transport-plan/80012345/delete
```
#### 响应示例
```json
{
"code": 200,
"data": true,
"msg": "success"
}
```
#### 错误响应
| 错误码 | 含义 | 触发场景 |
|--------|------|----------|
| `581100` | 订单不存在 | path 中 orderId 无效 |
| `581121` | plan 不属于该订单 | path orderId 与 plan.order_id 不一致 / planId 不存在 / 已软删 |
> **错误码段位说明**: 本接口复用 §2 模块 baseline `581121`(plan 归属校验),而非 §2.6 add/edit 新增的 `581144`,这是 V5.48 §2.6.4 文档的有意约定(删除场景与新增/编辑的归属校验语义区分)。前端可对 `581121` / `581144` 做同样的"批次不存在"提示。
示例 `581121`:
```json
{ "code": 581121, "msg": "大交通批次不属于该订单: planId=80099999", "data": null }
```
---
## 四、契约约束与正确调用方式
### ✅ 正确 / ❌ 错误调用对照
| 场景 | 请求 | 结果 |
|------|------|------|
| ✅ 新调用 | `POST /transport-plan/80012345/delete` | 200 + `data:true` |
| ❌ 沿用旧 DELETE | `DELETE /transport-plans/80012345` | 404 / 405 |
| ❌ planId 跨单 | path orderId=A,planId 属于 B | `581121` |
| ❌ 重复 delete | 已 `deleted=1` 的 plan 再 delete | `581121`(被软删的 plan 视同不存在) |
### 幂等语义
- `@Idempotent(timeout = 3)`: 同 admin 3 秒内重复 POST 同 path 直接拿首次结果,防双击重复
- `@Lock4j(keys = "#id")`: 同订单 30 秒锁,串行化 add/edit/delete
- 即使去掉幂等保护,二次 delete 也会因 `581121`(已软删视同不存在)被业务层正确拒绝,不会出现"软删后再软删"的脏数据
---
## 五、数据库行为
单事务执行,失败整体回滚:
| 步骤 | 表 | 动作 |
|------|-----|------|
| 1 | `order_transport_plan` | UPDATE `deleted=1` WHERE `id = ? AND order_id = ? AND deleted = 0`,`update_time` 自动刷新 |
| 2 | `order_transport_plan_traveler` | UPDATE `deleted=1` WHERE `plan_id = ?`(级联软删所有桥接行) |
> **级联软删而非物理删**: 桥接表保留 `deleted=1` 历史行,审计能查"曾经哪些出行人在该 plan",支持事后回溯。与 edit 接口的「全量重建桥接表用物理 DELETE」语义有意不同 — edit 重建后旧关联失去业务意义,delete 软删后旧关联是审计证据。
校验顺序:
1. 订单存在性 → `581100`
2. plan 归属 + 未软删 → `581121`(若 UPDATE 影响 0 行表示 plan 不存在或已删 或 order_id 不匹配)
---
## 六、边界行为
- 未登录 → 401(网关拦截)
- 订单不存在 → `581100`
- planId 不存在 / 已软删 / 跨单 → `581121`(用 UPDATE WHERE 复合条件一次判定,影响行数=0 即归一处理)
- 软删后,#2521 list 接口该 plan 不再返回(因 WHERE deleted=0 过滤)
- 软删后,该 plan 占用的"同方向同 traveler"释放,该出行人可在同方向 add 新 plan(`581143` 判定 WHERE deleted=0)
- 老数据 `order_transport_plan_traveler` 历史无 `deleted` 字段 → schema 已保证字段存在(V5.48 配套迁移已完成)
---
## 七、不影响范围
- **仅影响**: 管理后台 F21 「接送站」删除按钮
- **零影响**:
- C 端订单详情 / mp 抵达计划接口
- 出行人模块本身(traveler 行不动,只动桥接关联)
- 订单状态机
- 已签合同的 PDF 内容(合同生成时是快照,不会回溯查 plan)
---
## 八、测试环境已验证
测试服 9443 网关 + 真 admin token round-trip 验证:
```
POST https://web.test.1814.love:9443/v3/admin/order/{orderId}/transport-plan/{planId}/delete
→ 200 + data:true ✓
→ DB 验证 order_transport_plan.deleted=1 ✓
→ DB 验证 order_transport_plan_traveler.deleted=1 (级联) ✓
→ planId 不存在 → 581121 ✓
→ planId 跨单 → 581121 ✓
→ 重复 delete → 581121 (软删后视同不存在) ✓
→ delete 后 list 不再返回该 plan ✓
→ delete 后 add 同方向同 traveler 成功 (581143 占用释放) ✓
→ 3s 内重复 POST → 幂等返同结果 ✓
```
(commit `15e2c7eeb` 含 hotfix #2550,4 接口一并 9443 真测过)
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| **本 PR #2547** | **#2524** | Method DELETE→POST + 路径迁移 + 桥接表级联软删 | ✅ 最新 |
---
## 十、相关文档
- 关联 Issue: [wx/HL#2524](https://git.1814.love:8443/wx/HL/issues/2524)
- 关联 PR: [wx/HL#2547](https://git.1814.love:8443/wx/HL/pulls/2547)
- API 文档: `docs/order-v3/api/API-SPEC-V5.48.html` §2.6.4
- 配套破坏性: 见 `18_#2521_transport-plan-list-path.md` / `18_#2522_transport-plan-add.md` / `18_#2523_transport-plan-edit.md`

查看文件

@ -1,225 +0,0 @@
# order-v3 出行人模块: 新增 Feign 内部解密接口 + 审计表落库
> **存放目录**: 二期 v3(`order-v3` 标签) → `changelogs-v2/2026-05/`
>
> **服务**: hl-order-v3 (端口 8086)
> **PR**: #2552
> **Issue**: #2525
> **日期**: 2026-05-18
> **影响范围**: **仅后端服务 Feign 内部调用**(合同签署 / 保险出单等内部模块),**前端无关**
---
## ⚠️ 关键变化
- 新增一个 **/internal/** 路径,**仅供后端服务 Feign 调用**,经 9443 网关访问直接 403(网关白名单只放通 admin/mp/internal-feign)。前端/小程序不需要也不应调用。
- 出参含 `decryptedAt` 字段(后端解密时间戳),用于下游业务幂等与审计回溯。
- **新增审计表** `order_decrypt_audit_log`(9 字段 + 2 索引),每次解密同事务落审计行,**调用前请确认下游业务 purpose 取值正确**(枚举严格校验)。
---
## 一、背景
V5.48 §2.7 定义"敏感信息解密接口"。出行人 idNo/phone 在 v3 出行人表里为密文存储,合同签署/保险出单等内部业务需要明文。
风险点:任何解密动作必须留痕(谁、什么时候、为什么、查了哪个订单),否则一旦出现数据滥用无法溯源。本接口的关键不在"返明文",而在 **"返明文 + 强制同事务写审计"**。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 内部查询订单出行人明文(带解密审计) | GET | `/v3/internal/order/orders/{orderId}/travelers` | 新增 | Feign 内部调用,网关 9443 直接 403 |
---
## 三、接口详情
### 1. 内部查询订单出行人明文 `GET /v3/internal/order/orders/{orderId}/travelers`
**VO**: `InternalOrderTravelerRespVO`
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| orderId | Path | Long | 是 | 雪花 ID | 订单 ID |
| purpose | Query | String | 是 | 枚举:`CONTRACT_SIGN` / `INSURANCE_ISSUE` / `OTHER` | 解密用途,不在枚举内抛 589101 |
#### 出参 `Result<List<InternalOrderTravelerRespVO>>`
| 字段 | 类型 | 说明 |
|------|------|------|
| travelerId | Long | 出行人 ID |
| orderId | Long | 订单 ID |
| name | String | 姓名(明文) |
| idType | String | 证件类型 |
| idNo | String | 证件号(**明文**,已解密) |
| phone | String | 手机号(**明文**,已解密) |
| gender | String | 性别 |
| birthday | String | 生日 yyyy-MM-dd |
| race | String | 民族 |
| nationality | String | 国籍 |
| decryptedAt | String | 解密时间戳(yyyy-MM-dd HH:mm:ss),后端服务端时间 |
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": [
{
"travelerId": 9023400111,
"orderId": 9020000333,
"name": "张三",
"idType": "ID_CARD",
"idNo": "110101199001011234",
"phone": "13800138000",
"gender": "MALE",
"birthday": "1990-01-01",
"race": "汉",
"nationality": "CN",
"decryptedAt": "2026-05-18 14:23:11"
}
],
"success": true
}
```
#### 错误响应
```json
{
"code": 589100,
"message": "订单不存在或已删除,无法解密",
"success": false,
"data": null
}
```
```json
{
"code": 589101,
"message": "解密用途 purpose 不在允许枚举(CONTRACT_SIGN/INSURANCE_ISSUE/OTHER)",
"success": false,
"data": null
}
```
---
## 四、契约约束
| 约束 | 说明 |
|------|------|
| 调用方式 | 仅 Feign 内部调用,网关 9443 直接 403(网关白名单不放通 `/v3/internal/`) |
| purpose 必填 | 不传或为空 → 589101 |
| purpose 取值 | 严格枚举:`CONTRACT_SIGN` / `INSURANCE_ISSUE` / `OTHER`,大小写敏感 |
| 订单不存在 | 589100,不区分软删/不存在 |
| 同事务 | 查 traveler → 写审计 → 返 VO,**审计写失败整体回滚不返明文** |
---
## 五、数据库行为
### 新增审计表 `order_decrypt_audit_log`
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGINT | 主键雪花 |
| order_id | BIGINT | 被解密订单 |
| traveler_id | BIGINT | 被解密出行人 |
| purpose | VARCHAR(32) | 解密用途 |
| operator_type | VARCHAR(32) | 调用方类型(FEIGN_INTERNAL) |
| operator_id | VARCHAR(64) | 调用方服务名/标识 |
| field_name | VARCHAR(32) | 解密字段(idNo / phone) |
| decrypted_at | DATETIME | 解密时间 |
| create_time | DATETIME | 行写入时间 |
**索引**:`idx_order_id` / `idx_decrypted_at`
**Flyway**: `V20260518_002__create_order_decrypt_audit_log.sql`
### 写入行为
- 每次调用,**每个出行人每个解密字段写 1 行**审计
- 同事务写,接口返回 N 个出行人则审计行至少 2N(idNo + phone 各 1)
- 失败回滚:整体事务回滚,**已返明文绝不可能**(返回前已 commit)
---
## 六、边界行为
- 网关 9443 调用 → 403(网关白名单拦截,**预期行为**)
- Feign 内部调用 (SSH 跳板内网 8086) → 200
- 订单不存在 / 已删除 → 589100
- purpose 非法 → 589101
- 出行人列表为空 → 返 `[]`,**仍写空审计?否**:无 traveler 不写审计,只返空数组
- 解密失败(密钥错乱等基础设施异常)→ 500,审计不落
---
## 七、不影响范围
- **仅影响**: 后端服务 Feign 内部调用链(合同签署 / 保险出单 / 财务对账等)
- **零影响**:
- 前端所有接口(管理端 + 小程序)
- 现有 `/v3/admin/order/{id}/traveler/*` 全部接口(明文 ↔ 密文转换逻辑不变)
- 出行人增/删/改接口
- 订单创建/详情/列表
- 历史数据(存量出行人无需迁移)
---
## 八、测试环境已验证
SSH 跳板内网 8086 真测:
```
GET /v3/internal/order/orders/{orderId}/travelers?purpose=CONTRACT_SIGN
→ 200 + 1 出行人 + decryptedAt 字段非空 ✓
→ audit log 写入 2 行(idNo + phone)✓
GET /v3/internal/order/orders/{orderId}/travelers?purpose=INSURANCE_ISSUE
→ 200 + 同上 ✓
→ audit log 累计 4 行 ✓
GET /v3/internal/order/orders/{orderId}/travelers?purpose=OTHER
→ 200 ✓
→ audit log 累计 6 行 ✓
GET /v3/internal/order/orders/9999999999/travelers?purpose=CONTRACT_SIGN
→ 589100 订单不存在 ✓
GET /v3/internal/order/orders/{orderId}/travelers?purpose=INVALID
→ 589101 purpose 非法 ✓
```
**网关 9443 验证**:
```
GET https://web.test.1814.love:9443/v3/internal/order/orders/{orderId}/travelers?purpose=CONTRACT_SIGN
→ 403 ✓(网关白名单拦截,预期行为)
```
审计落库总计 5+1 = 6 行,与预期一致。
---
## 九、错误码段位说明
| 错误码 | 含义 | 文档期望 | 实际 |
|--------|------|----------|------|
| 589100 | INTERNAL_ORDER_NOT_FOUND_FOR_DECRYPT | 589100 | ✅ 一致 |
| 589101 | INTERNAL_DECRYPT_PURPOSE_INVALID | 589101 | ✅ 一致 |
---
## 十、相关文档
- 关联 Issue: [wx/HL#2525](https://git.1814.love:8443/wx/HL/issues/2525)
- 关联 PR: [wx/HL#2552](https://git.1814.love:8443/wx/HL/pulls/2552)
- commit: `9cca41a5d`
- 设计文档: `docs/order-v3/V5.48 §2.7 敏感信息解密接口`

查看文件

@ -1,240 +0,0 @@
# order-v3 出行人模块: 新增智能批量解析接口 smart-parse
> **存放目录**: 二期 v3(`order-v3` 标签) → `changelogs-v2/2026-05/`
>
> **服务**: hl-order-v3 (端口 8086)
> **PR**: #2554
> **Issue**: #2526
> **日期**: 2026-05-18
> **影响范围**: 管理后台订单详情"批量导入出行人"功能 → **新增接口,前端必新对接**
---
## ⚠️ 关键变化
- **新增接口** `POST /v3/admin/order/{id}/traveler/smart-parse`,前端需新对接,**不是改造现有接口**。
- 支持 `dryRun=true` 预览解析结果不落库,`dryRun=false` 真正写入。
- **P0 审计安全 5 红线全合规**:不挂 @OperationLog 避免明文落审计 / failures[].maskedSnippet 脱敏 / 限流 / Lock4j / status_log 仅写聚合统计不写明文。
---
## 一、背景
V5.48 §2.8 定义"智能批量解析"。客服收到客户微信发来的一大段文字(姓名+身份证+手机号混排),要求自动解析后批量录入出行人。
风险点 = **审计安全**:身份证/手机号是 P0 敏感字段,若任何中间环节(@OperationLog / 日志 / status_log)落了明文,即一次安全事故。本接口的关键设计就是 **正常落库走加密 TypeHandler,失败片段返脱敏**
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 智能批量解析出行人 | POST | `/v3/admin/order/{id}/traveler/smart-parse` | **新增** | 前端必新对接 |
---
## 三、接口详情
### 1. 智能批量解析 `POST /v3/admin/order/{id}/traveler/smart-parse`
**VO**: `TravelerSmartParseReqVO` / `TravelerSmartParseRespVO`
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | 是 | 雪花 ID | 订单 ID |
| rawText | Body | String | 是 | 非空,长度 ≤ 5000 | 客户原始文本(姓名+身份证+手机号混排) |
| dryRun | Body | Boolean | 否 | 默认 false | true=只预览不落库,false=真正写入 |
#### 出参 `Result<TravelerSmartParseRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| successCount | Integer | 成功解析并落库的数量(dryRun=true 时为"预期会落库的数量") |
| failCount | Integer | 失败片段数量 |
| successList | List<TravelerSimpleVO> | 成功的出行人列表(返脱敏后的预览字段,**不返明文**) |
| failures | List<TravelerSmartParseFailureVO> | 失败片段列表 |
| failures[].maskedSnippet | String | 失败片段(idNo 前6后4、phone 前3后4 脱敏) |
| failures[].reason | String | 失败原因(中文) |
#### 请求示例
```json
{
"rawText": "张三 110101199001011234 13800138000\n李四 身份证: 320101198502028765 手机 13900139000",
"dryRun": false
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"successCount": 2,
"failCount": 0,
"successList": [
{ "travelerId": 9023400777, "name": "张三", "idNo": "110101******1234", "phone": "138****8000" },
{ "travelerId": 9023400778, "name": "李四", "idNo": "320101******8765", "phone": "139****9000" }
],
"failures": []
},
"success": true
}
```
#### 失败片段响应示例
```json
{
"code": 200,
"data": {
"successCount": 1,
"failCount": 1,
"successList": [...],
"failures": [
{
"maskedSnippet": "王五 1101******1234 1380***0000",
"reason": "身份证号校验位错误"
}
]
},
"success": true
}
```
#### 错误响应
```json
{ "code": 581131, "message": "原文本过长,最多 5000 字符", "success": false }
```
```json
{ "code": 581132, "message": "原文本为空或无法识别任何出行人", "success": false }
```
```json
{ "code": 581134, "message": "订单当前状态不允许批量导入出行人", "success": false }
```
```json
{ "code": 100501, "message": "请求过于频繁,请稍后再试", "success": false }
```
---
## 四、契约约束
| 约束 | 说明 |
|------|------|
| dryRun=true | 仅解析返回预览,不落 DB,不写 status_log,不锁订单 |
| dryRun=false | 同事务批量 insert 出行人 + 写 status_log + 加 @Lock4j 锁订单 |
| @Lock4j | `key=#id`,expire=30000ms,同订单串行 |
| @RateLimiter | `count=10, time=60`(每用户 60s 内最多 10 次) |
| 订单状态 | 仅"待出行/已支付"等状态允许,其它 → 581134 |
| 文本长度 | > 5000 字符 → 581131 |
| 无可解析片段 | 整段 0 个能识别 → 581132 |
---
## 五、数据库行为
### dryRun=false 写入
- `order_traveler`: 批量 insert,idNo/phone 走 EncryptTypeHandler 加密落库
- `order_status_log`: 写 1 行 `reason="批量导入出行人 N 人"`(**不含任何明文**)
### dryRun=true
- 无任何写操作
### 失败片段
- 不落 DB
- 仅出现在响应 `failures[]`,带脱敏 snippet
---
## 六、P0 审计安全红线(本接口最核心设计)
| # | 红线 | 实现 |
|---|------|------|
| 1 | **不挂 @OperationLog** | Controller 方法没有此注解,避免 idNo/phone 通过 requestParams 落 audit_log |
| 2 | **failures[].maskedSnippet 脱敏** | idNo 前 6 后 4(`110101******1234`),phone 前 3 后 4(`138****8000`) |
| 3 | **日志仅聚合统计** | `log.info("smart-parse orderId={} successCount={} failCount={}")`,**绝不打印 rawText / idNo / phone** |
| 4 | **限流** | @RateLimiter(count=10, time=60),防御暴力扫描 |
| 5 | **status_log 不写明文** | reason 只写 "批量导入出行人 N 人",不带任何字段值 |
---
## 七、边界行为
- 未登录 → 401(网关)
- 订单不存在 → 404
- 订单状态非法 → 581134
- 限流触发 → 100501(框架统一限流码,非 581133)
- rawText 含全空白 → 581132
- 部分成功部分失败 → 200,在 `successList` / `failures` 中体现
---
## 八、不影响范围
- **仅影响**: 管理后台订单详情页"批量导入出行人"入口
- **零影响**:
- 现有 `/v3/admin/order/{id}/traveler/add`(单人新增)
- 现有 `/v3/admin/order/{id}/traveler/edit`(批量编辑)
- 现有 `/v3/admin/order/{id}/traveler/validate`(完整性校验,见 #2527)
- 现有 `/v3/internal/order/orders/{orderId}/travelers`(Feign 解密,见 #2525)
- 订单创建/详情/列表
- 小程序所有接口
---
## 九、测试环境已验证
经 9443 网关真 admin token 测试:
```
POST /v3/admin/order/{id}/traveler/smart-parse rawText 超 5000 字符
→ 581131 SMART_PARSE_RAW_TEXT_TOO_LONG ✓
POST /v3/admin/order/{id}/traveler/smart-parse 订单状态不允许
→ 581134 SMART_PARSE_ORDER_STATUS_FORBID ✓
POST /v3/admin/order/{id}/traveler/smart-parse 连续 11 次调用
→ 11 次返 100501 限流 ✓
POST /v3/admin/order/{id}/traveler/smart-parse 正常 dryRun=true
→ ⚠️ 测试服无 PENDING 订单,正常路径未在 9443 真测
→ 代码层 UnitTest + IntegrationTest 已覆盖
→ **前端联调阶段补做正常路径回归**
```
---
## 十、错误码段位说明(段位让位,与文档不一致)
V5.48 §2.8 文档期望段位被 baseline 已占用,本 PR 实际段位如下,**调用方按本表为准**:
| 错误码 | 含义 | 文档期望 | 实际 | 原因 |
|--------|------|----------|------|------|
| 581131 | SMART_PARSE_RAW_TEXT_TOO_LONG | 581120 | 581131 | 581120 已被 TRANSPORT_PLAN_NOT_FOUND 占用 |
| 581132 | SMART_PARSE_RAW_TEXT_EMPTY_OR_UNPARSABLE | 581121 | 581132 | 同段位让位 |
| 581133 | SMART_PARSE_RATE_LIMITED | 581122 | doc-only(实际抛 100501) | 框架统一限流码 |
| 581134 | SMART_PARSE_ORDER_STATUS_FORBID | 581123 | 581134 | 同段位让位 |
后续文档侧 follow-up 修正文档段位与代码对齐(本 PR 不改文档)。
---
## 十一、相关文档
- 关联 Issue: [wx/HL#2526](https://git.1814.love:8443/wx/HL/issues/2526)
- 关联 PR: [wx/HL#2554](https://git.1814.love:8443/wx/HL/pulls/2554)
- commit: `f8a34607`
- 设计文档: `docs/order-v3/V5.48 §2.8 智能批量解析`

查看文件

@ -1,212 +0,0 @@
# order-v3 出行人模块: validate 接口字段判定对齐 v2(6 → 4 必填字段)
> **存放目录**: 二期 v3(`order-v3` 标签) → `changelogs-v2/2026-05/`
>
> **服务**: hl-order-v3 (端口 8086)
> **PR**: #2553
> **Issue**: #2527
> **日期**: 2026-05-18
> **影响范围**: 管理后台订单详情页"出行人完整性校验"提示文案 → **路径不变,字段判定变化**
---
## ⚠️ 关键变化
- **路径不变**:`GET /v3/admin/order/{id}/traveler/validate`
- **字段判定变化**:必填字段从 **6 个 → 4 个**(name / idType / idNo / phone)
- `gender` / `birthday` / `race` / `nationality` **改为可选**,**不再进 incompleteList.missingFields**
- `incompleteList[].name` **改为脱敏**(首字符 + `*`)
- 前端调用方式不变,但**展示给客服的"缺哪些字段"提示会变少**
---
## 一、背景
V5.48 §2.9 要求 v3 validate 与 v2 一期对齐。
v2 一期实战中,客服只关心 4 个字段(姓名/证件类型/证件号/手机号),其他字段(性别/生日/民族/国籍)由后端从身份证号自动解析或后续补录,**不算"出行人信息不完整"**。
v3 早期版本沿用了 V5.0 设计的 6 字段判定,导致客服侧总是看到"出行人信息不完整,缺生日"的红字提示,实则不影响出行,产生噪音。
本次对齐 v2,**4 字段全有就算完整**。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 出行人完整性校验 | GET | `/v3/admin/order/{id}/traveler/validate` | **字段判定变化**(路径不变) | 6 → 4 必填字段;name 脱敏 |
---
## 三、接口详情
### 1. 出行人完整性校验 `GET /v3/admin/order/{id}/traveler/validate`
**VO**: `TravelerValidateRespVO`
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | 是 | 雪花 ID | 订单 ID |
#### 出参 `Result<TravelerValidateRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| expectedCount | Integer | 订单应有出行人数(来自订单 traveler_count) |
| actualCount | Integer | 实际已录入出行人数 |
| countMismatch | Boolean | 数量是否不一致 |
| incompleteList | List | 信息不完整的出行人列表 |
| incompleteList[].travelerId | Long | 出行人 ID |
| incompleteList[].name | String | **脱敏后姓名**(首字符 + `*`,如"张*"、"李*") |
| incompleteList[].missingFields | List<String> | 缺失字段名,**仅可能为 `name`/`idType`/`idNo`/`phone` 4 个值之一** |
#### 响应示例(完整)
```json
{
"code": 200,
"message": "成功",
"data": {
"expectedCount": 3,
"actualCount": 3,
"countMismatch": false,
"incompleteList": []
},
"success": true
}
```
#### 响应示例(数量不一致 + 信息不完整)
```json
{
"code": 200,
"data": {
"expectedCount": 3,
"actualCount": 2,
"countMismatch": true,
"incompleteList": [
{
"travelerId": 9023400111,
"name": "张*",
"missingFields": ["phone"]
},
{
"travelerId": 9023400112,
"name": "李*",
"missingFields": ["idNo", "phone"]
}
]
},
"success": true
}
```
#### 错误响应
```json
{ "code": 581102, "message": "订单不存在或已删除", "success": false }
```
---
## 四、字段判定规则(本次重点)
| 字段 | v3 旧版判定 | v3 新版判定(本次) | v2 |
|------|-------------|---------------------|-----|
| name | 必填 | **必填** | 必填 |
| idType | 必填 | **必填** | 必填 |
| idNo | 必填 | **必填** | 必填 |
| phone | 必填 | **必填** | 必填 |
| gender | 必填 | **可选**(不进 missingFields) | 可选 |
| birthday | 必填 | **可选**(不进 missingFields) | 可选 |
| race | 必填 | **可选** | 可选 |
| nationality | 必填 | **可选** | 可选 |
判定逻辑:**4 个必填字段任一为 null 或空字符串 → 该出行人进 incompleteList**,missingFields 只列出缺失的 4 字段之一。
---
## 五、契约约束
| 约束 | 说明 |
|------|------|
| 路径 | **不变**,前端无需改 URL |
| 入参 | **不变**,只有 path 上 orderId |
| 出参字段名 | **不变**,但 `missingFields` 内可能值从 8 → 4 |
| name 脱敏 | 出参 `incompleteList[].name` **改为脱敏**,前端无需自己再脱敏 |
---
## 六、数据库行为
- **无写操作**(纯查询接口)
- 查询逻辑:order_traveler WHERE order_id = ? AND deleted_at IS NULL,在 Service 层逐字段判空
---
## 七、边界行为
- 订单不存在 / 已删除 → 581102
- 订单存在但 traveler_count = 0 → expectedCount=0, actualCount=0, countMismatch=false, incompleteList=[]
- 订单存在但无出行人录入 → expectedCount=N, actualCount=0, countMismatch=true
- 所有出行人 4 字段齐全 → incompleteList=[]
- 出行人 name 字段本身为空 → name 字段返 `*`(单字符脱敏)
---
## 八、不影响范围
- **仅影响**: 管理后台订单详情页"出行人完整性"提示文案的显示
- **零影响**:
- 出行人增/删/改接口(`add` / `edit` / `delete`)
- smart-parse 智能批量解析(见 #2526)
- Feign 内部解密(见 #2525)
- 小程序所有接口
- 订单状态机(本接口不参与状态流转)
- 数据库表结构
---
## 九、测试环境已验证
经 9443 网关真 admin token 测试:
```
GET /v3/admin/order/{id}/traveler/validate 正常订单
→ 200 + expectedCount=2 + actualCount=2 + incompleteList=[] ✓
GET /v3/admin/order/{id}/traveler/validate 数量不一致
→ 200 + countMismatch=true ✓
GET /v3/admin/order/{id}/traveler/validate 缺 phone 字段
→ 200 + incompleteList[0].missingFields=["phone"] ✓
→ name 脱敏 "张*" ✓
GET /v3/admin/order/{id}/traveler/validate 仅缺 gender/birthday(旧版应报缺,新版应通过)
→ 200 + incompleteList=[] ✓(新版 4 字段判定不算缺)
GET /v3/admin/order/9999999999/traveler/validate
→ 581102 订单不存在 ✓
```
---
## 十、错误码段位说明
| 错误码 | 含义 | 文档期望 | 实际 | 原因 |
|--------|------|----------|------|------|
| 581102 | TRAVELER_VALIDATE_ORDER_NOT_FOUND | 581124 | **581102 复用** | 581124 已被 TRANSPORT_PLAN_INVALID_MODE 占用,文档侧 follow-up 修文档 |
---
## 十一、相关文档
- 关联 Issue: [wx/HL#2527](https://git.1814.love:8443/wx/HL/issues/2527)
- 关联 PR: [wx/HL#2553](https://git.1814.love:8443/wx/HL/pulls/2553)
- commit: `d9bc8371`
- 设计文档: `docs/order-v3/V5.48 §2.9 validate 接口对齐 v2`

查看文件

@ -1,196 +0,0 @@
# ⚠️✨ 管理端代下单接口 v3 新增分享追踪字段 + consultantSource 枚举新增 SHARED
> **服务**: hl-order-service-v3
> **接口**: `POST /admin/v3/orders`
> **PR**: #2565 refactor(order-v3): [Agency PR-4 / PR-1] 订单创建补 5 项基础逻辑
> **日期**: 2026-05-18 23:55
> **影响页面**: 管理后台「代下单」流程 + 小程序下单(通过 hl-mp-service 透传同接口)
---
## 变了什么(前端视角)
### 1. 请求体新增 2 个非必填字段
`POST /admin/v3/orders` 的请求 body 新增以下字段,**不传或传 null 行为与改造前完全一致**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `sharerOpenid` | String | 否 | 分享人微信 openid,用于 C 端裂变追踪 / 佣金归属。落表 `order_info.sharer_openid`,admin 代下单时通常不传 |
| `customizerId` | Long | 否 | C 端分享归因:从分享链接中带入的定制师 adminId。校验通过则锁定为该定制师`consultantSource = SHARED`),校验失败兜底系统默认定制师 |
**注意**`customizerId` 类型必须是 **numberLong**,不能传 string。
### 2. 响应体枚举 consultantSource 新增值 SHARED
`OrderCreateRespVO``consultantSource` 字段的枚举值新增 `SHARED`
| 枚举值 | 含义 | 何时出现 |
|--------|------|---------|
| `DEFAULT_ASSIGNED` | 系统默认定制师 | C 端下单,customizerId 为空或校验不通过,且有系统默认定制师 |
| `LINK_BOUND` | 链接绑定定制师 | 历史逻辑v2 遗留) |
| `MANUAL` | admin 手动指定 | admin 代下单时由 JWT adminId 指定 |
| `SHARED`**新增** | C 端分享锁定定制师 | C 端下单,customizerId 校验通过 |
### 3. 新增可能抛出的错误码
| code | message | 触发场景 |
|------|---------|---------|
| `581035` | 订金金额不得超过订单总额 | DEPOSIT 模式下订金金额 > 订单总额时触发 |
### 4. 行为收紧(字段名不变,错误码变化)
| 场景 | 旧行为 | 新行为 |
|------|--------|--------|
| Agency 无默认 mchId | 抛 `MCH_RESOLVE_FAILED` | 抛 `AGENCY_NO_DEFAULT_MCHID` |
| customerName / customerRemark 含 HTML 标签 | 原样入库 | XSS 过滤后入库(前端无感,被清掉的标签不会报错) |
---
## 前端要改的地方
### 管理后台 B 端
1. **consultantSource 展示标签**:若订单详情 / 列表有按 `consultantSource` 显示来源文案的地方,需补 `SHARED` 的 case,建议展示文案为**"分享锁定"**
```js
// 建议
const consultantSourceLabel = {
DEFAULT_ASSIGNED: '系统分配',
LINK_BOUND: '链接绑定',
MANUAL: '手动指定',
SHARED: '分享锁定', // 新增
}
```
2. **错误码监控 / 提示**:若代下单流程中有按错误码展示错误提示,建议给 `581035` 加 case,文案直接透传后端 message`"订金金额不得超过订单总额"`
若之前有对 `MCH_RESOLVE_FAILED` 的特殊提示逻辑,需同步改为 `AGENCY_NO_DEFAULT_MCHID`
### 小程序 C 端(通过 hl-mp-service 透传)
3. **分享链接带 customizerId 下单**:从分享 URL 取出 `adminId`,下单时透传为 `customizerId`(参考 `07_feat_share_lock_customizer.md` 详细步骤):
```js
const customizerId = uni.getStorageSync('share_customizer_id') || null
// 放入 POST /mp/order/create 或 POST /admin/v3/orders 的请求体
```
4. **分享追踪 sharerOpenid**:若小程序有分享溯源需求(如显示"由 XXX 分享"),可在下单时透传 `sharerOpenid`;不传也不影响下单流程。
---
## 接口详细定义
### POST /admin/v3/orders — 管理端 / C 端创建订单
- **使用场景**:管理后台代客户下单,或 C 端小程序通过 hl-mp-service 创建订单
- **方法**: `POST`
- **路径**: `/admin/v3/orders`(通过 gateway 路由到 hl-order-service-v3
#### 请求参数Body JSON
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `productId` | Long | 是 | 产品 ID |
| `productType` | String | 是 | 产品类型CORE / ACTIVITY 等) |
| `startDate` | String | 是 | 出发日期,格式 `yyyy-MM-dd` |
| `adultCount` | Integer | 是 | 成人人数,≥ 1 |
| `childCount` | Integer | 否 | 儿童人数,默认 0 |
| `customerName` | String | 是 | 客户姓名(会走 XSS 过滤,HTML 标签会被清除) |
| `customerPhone` | String | 是 | 客户手机号 |
| `customerRemark` | String | 否 | 客户备注(会走 XSS 过滤) |
| `sharerOpenid` | String | 否 | **[新增]** 分享人微信 openid,C 端裂变追踪用 |
| `customizerId` | Long | 否 | **[新增]** 分享人定制师 adminId,校验通过则锁定为本单定制师 |
#### 请求示例(含新增字段)
```json
{
"productId": 10001,
"productType": "CORE",
"startDate": "2026-06-01",
"adultCount": 2,
"childCount": 0,
"customerName": "张三",
"customerPhone": "13800138000",
"customerRemark": "需要靠窗座位",
"sharerOpenid": "oXXXXXXXXXXX",
"customizerId": 50001
}
```
#### 响应结构
```json
{
"code": 200,
"msg": "success",
"data": {
"orderId": "202605181234567890",
"orderNo": "HL202605181234",
"totalAmount": 9800,
"depositAmount": 2000,
"consultantId": 50001,
"consultantName": "李定制师",
"consultantSource": "SHARED"
}
}
```
#### 响应字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| `orderId` | String | 雪花 ID,前端按字符串处理Long 精度问题) |
| `orderNo` | String | 可读订单号 |
| `totalAmount` | Integer | 订单总额(分) |
| `depositAmount` | Integer | 订金金额,DEPOSIT 模式下有值 |
| `consultantId` | Long | 绑定的定制师 adminId |
| `consultantName` | String | 定制师姓名 |
| `consultantSource` | String | 定制师来源枚举,见下表 |
#### consultantSource 枚举值完整列表
| 值 | 中文展示建议 | 触发场景 |
|----|------------|---------|
| `DEFAULT_ASSIGNED` | 系统分配 | C 端下单,customizerId 无效或未传,有系统默认定制师 |
| `LINK_BOUND` | 链接绑定 | 历史逻辑v2 遗留,v3 基本不再出现) |
| `MANUAL` | 手动指定 | admin 代下单,由登录态 JWT adminId 指定 |
| `SHARED` | 分享锁定 | C 端下单,`customizerId` 校验通过(定制师有效且角色正确) |
---
## 错误码完整列表(本接口可能返回)
| code | message | 处理建议 |
|------|---------|---------|
| `200` | success | 正常 |
| `400` | 参数校验失败 | 检查必填字段 |
| `581035` | 订金金额不得超过订单总额 | **[新增]** 直接 toast 后端 message |
| `AGENCY_NO_DEFAULT_MCHID` | Agency 未配置默认收款账号 | 联系运营配置 agency 默认 mchId |
---
## 兼容性
- 请求字段仅新增非必填字段,**向后兼容**(旧版前端不传新字段行为不变)
- 响应枚举仅新增值不删值,**向后兼容**
- 无 DDL 变更,无需数据迁移
- 只需重启 `hl-order-service-v3``hl-mp-service` 无需重启
---
## customizerId 校验兜底语义(前端无需实现)
后端 `CustomizerValidator` 校验失败时**全部静默兜底为系统默认定制师,不报错**
| 失败原因 | 触发条件 | 前端表现 |
|---------|---------|---------|
| `ADMIN_NOT_FOUND` | adminId 不存在 / 已删除 | 兜底随机,静默 |
| `INACTIVE_STATUS` | admin status ≠ ACTIVE | 兜底随机,静默 |
| `WRONG_ROLE` | admin roleKey ≠ CUSTOMIZER | 兜底随机,静默 |
| `FEIGN_ERROR` | user-service Feign 调用失败 | 兜底随机,静默 |
| `INVALID_INPUT` | customizerId == null / ≤ 0 | 兜底随机,静默 |
前端不需要对这些失败场景做任何处理。

查看文件

@ -1,188 +0,0 @@
# 出行人列表: 路径迁移 /travelers → /traveler/list
> **服务**: hl-order-service-v3 (端口 8084 / 二期)
> **PR**: #2531
> **Issue**: #2517
> **日期**: 2026-05-18
> **影响范围**: 管理后台订单详情概览 Tab 出行人区块 / F27 出行人补全列表读取
> **存放目录**: `changelogs-v2/2026-05/`(二期 v3 专属,带 -v2 后缀)
> **部署 commit**: dev-v3 `68fd00f85`
> **测试服已验证**: ✅(/@qa 通过 9443 网关 + 真 admin token round-trip)
---
## ⚠️ 关键变化(破坏性路径迁移)
接口路径从 v3 早期临时路径 `/travelers` 改为文档 V5.48 §2.1 正式路径 `/traveler/list`:
| 维度 | before(老路径,即将下线) | after(本 PR 起生效) |
|---|---|---|
| 方法 | GET | GET |
| 路径 | `/v3/admin/order/{id}/travelers` | `/v3/admin/order/{id}/traveler/list` |
**对前端的影响**: 调用方需把请求 URL 从 `/travelers` 替换为 `/traveler/list`
**响应结构无变化**(仍是 `Result<List<TravelerVO>>` + 16 字段),前端字段映射不需要改。
---
## 一、背景
V5.48 文档 §2.1 已把出行人列表正式路径定为 `/traveler/list`(与 add / batch-edit / delete 等 §2.2~§2.4 接口的路径风格统一: 一级名词 `traveler` + 二级动词)。早期实现用了简短复数形式 `/travelers`,本 PR 完成对齐。
同时清理了 TravelerConverter 内残留的 Mock 占位注释,补齐 `transportPlanIds` 字段的真实化(从桥接表 `order_transport_plan_traveler` LEFT JOIN 取真值,而非占位空数组)。
---
## 二、变更接口清单
| # | 接口 | 方法 | 老路径 | 新路径 | 变更类型 |
|---|------|------|--------|--------|----------|
| 1 | 订单出行人列表 | GET | `/v3/admin/order/{id}/travelers` | `/v3/admin/order/{id}/traveler/list` | 破坏性路径迁移 |
---
## 三、接口详情
### 1. 订单出行人列表 `GET /v3/admin/order/{id}/traveler/list`
**VO**: `TravelerVO`(16 字段,无 *Label 衍生字段)
#### 入参
| 字段 | 位置 | 类型 | 必填 | 说明 |
|------|------|------|------|------|
| `id` | Path | Long | ✅ | 订单 ID(雪花 ID 字符串安全形式) |
#### 出参 `Result<List<TravelerVO>>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | Long | 出行人 ID |
| `orderId` | Long | 订单 ID |
| `travelerType` | String | ADULT / CHILD / YOUNG_CHILD / BABY |
| `name` | String? | 姓名(占位行为 null) |
| `gender` | String? | MALE / FEMALE / UNKNOWN |
| `birthday` | LocalDate? | 出生日期 |
| `idType` | String? | ID_CARD / PASSPORT / BIRTH_CERT |
| `idNo` | String? | 证件号(admin 明文,DB 加密) |
| `nationality` | String | 国籍(默认"中国") |
| `race` | String | 民族(默认"汉族") |
| `phone` | String? | 出行人手机(admin 明文) |
| `emergencyContact` | String? | 紧急联系人姓名 |
| `emergencyPhone` | String? | 紧急联系人电话(admin 明文) |
| `roomGroupNo` | Integer? | 同住分组号 |
| `profileStatus` | String | PENDING / COMPLETED |
| `transportPlanIds` | List\<Long\> | 关联大交通批次 ID 列表(来自桥接表) |
#### 请求示例
```
GET /v3/admin/order/60123456789012/traveler/list
Authorization: Bearer {admin_jwt}
```
#### 响应示例
```json
{
"code": 200,
"data": [
{
"id": 70123456789012,
"orderId": 60123456789012,
"travelerType": "ADULT",
"name": "张三",
"gender": "MALE",
"birthday": "1985-08-12",
"idType": "ID_CARD",
"idNo": "220103198508121234",
"nationality": "中国",
"race": "汉族",
"phone": "13800002046",
"emergencyContact": "李四",
"emergencyPhone": "13900008888",
"roomGroupNo": 1,
"profileStatus": "COMPLETED",
"transportPlanIds": [80012345, 80012346]
}
],
"msg": "success"
}
```
#### 空数据响应
```json
{ "code": 200, "data": [], "msg": "success" }
```
#### 错误响应
| 错误码 | 含义 |
|---|---|
| `581100` | 出行人主段位—— 订单 / 出行人不存在(本接口下订单不存在时返回空集合即 200 + `data:[]`,不报错;此码主要给 batch-edit / add / delete 用) |
---
## 四、契约约束
- 仅 admin JWT 可访问,网关层鉴权(无 token → 网关 401);跨公司读取由订单详情上下文接口在更上层校验,本接口本身只看订单 ID。
- 敏感字段(idNo / phone / emergencyPhone)**明文返回**,前端勿做二次脱敏渲染(B 端定制师业务诉求,后端已通过 EncryptTypeHandler 在 DB 层加密,VO 层明文)。
---
## 五、数据库行为
只读接口,**不产生任何 DB 写入**。
读关联表: `order_traveler` + LEFT JOIN `order_transport_plan_traveler`(取 `transportPlanIds` 真值)。
---
## 六、边界行为
- 未登录 → 401(网关拦截)
- 订单不存在 / 该订单下无出行人 → 200 + `data: []`(不 404,不抛异常)
- 出行人有未关联任何大交通的 → `transportPlanIds: []`
- 占位出行人(`name=null` `idNo=null`) → 字段为 null,不异常
---
## 七、不影响范围
- **仅影响**: 管理后台调用 `/v3/admin/order/{id}/travelers` 的请求 URL 拼接
- **零影响**:
- 响应结构 / 字段名 / 字段类型 / 字段值(VO 完全不变)
- mp 端出行人列表接口(走 `/v3/mp/...`,独立路径,未涉及)
- 订单详情主聚合接口 `/v3/admin/order/{id}``overview.travelers` 嵌套数组(已复用同 VO,本次未改)
- Feign 跨服务 internal 接口 `/internal/order/orders/{orderId}/travelers`(internal 路径独立,未涉及)
---
## 八、测试环境已验证
```
GET https://web.test.1814.love:9443/v3/admin/order/60123456789012/traveler/list
Authorization: Bearer {admin_jwt}
→ 200 + List<TravelerVO> 16 字段齐全 ✓
→ transportPlanIds 真值(非占位空数组)✓
→ 老路径 /travelers 已无返回(404)✓
```
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| #2124 | #2123 | /traveler/list 入参精简 + 敏感字段改明文(早期路径用 /travelers) | 部分有效(敏感字段口径保留,路径被本 PR 覆盖) |
| #2125 | - | detail.overview.travelers 复用 TravelerVO 完整字段 | ✅ 有效 |
| **本 PR #2531** | **#2517** | 路径正式迁 /traveler/list + transportPlanIds 真实化 | ✅ 最新 |
---
## 十、相关文档
- API SPEC: `D:/work2/HL-v3/docs/order-v3/api/API-SPEC-V5.48.html` §2.1
- 关联 Issue: [wx/HL#2517](https://git.1814.love:8443/wx/HL/issues/2517)
- 关联 PR: [wx/HL#2531](https://git.1814.love:8443/wx/HL/pulls/2531)

查看文件

@ -1,233 +0,0 @@
# 出行人批量编辑: POST /traveler/batch-edit 真实业务化
> **服务**: hl-order-service-v3 (端口 8084 / 二期)
> **PR**: #2535
> **Issue**: #2518
> **日期**: 2026-05-18
> **影响范围**: 管理后台 F27 出行人补全(B 端定制师代填)
> **存放目录**: `changelogs-v2/2026-05/`(二期 v3 专属,带 -v2 后缀)
> **部署 commit**: dev-v3 `83dbeedb4`
> **测试服已验证**: ✅(/@qa 通过 9443 网关 + 真 admin token round-trip)
---
## ⚠️ 关键变化
1. **请求体字段名**: 文档原稿 §2.2 写的是 `items`,**实际 VO 定义为 `travelers`**,前端按 `travelers` 提交(下文示例为准)。
2. **从 Mock 占位变真实业务**: 之前是空骨架返回固定假数据,本 PR 接通真实 UPDATE + 12301 校验 + 合同冻结 + 同住分组校验 + 资料状态联动。
3. **新增 9 个错误码段位**: 581101 / 581102 / 581103 / 581104 / 581105 / 581111 / 581112 / 581113 / 581114(段位安排见下)。
4. **加幂等 @Idempotent(3s)**: 同订单 3 秒窗口内重复提交直接拒绝,防止快速点击 / 网络重试导致补全双写。
---
## 一、背景
文档 V5.48 §2.2 定义 admin 端批量编辑出行人,任一行 12301 字段(国籍 / 民族)校验失败整体回滚,补全完成后异步重算 `profile_status` 并触发"已完善"系统标签。本 PR 完成真实业务化,以及合同 signed 状态对证件号的冻结约束。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 |
|---|------|------|------|----------|
| 1 | 出行人批量编辑 | POST | `/v3/admin/order/{id}/traveler/batch-edit` | 真实业务化 + 9 个新错误码 |
---
## 三、接口详情
### 1. 出行人批量编辑 `POST /v3/admin/order/{id}/traveler/batch-edit`
**VO**: `TravelerBatchEditReqVO` + `TravelerEditItem`(嵌套数组每行)
#### 入参
| 字段 | 位置 | 类型 | 必填 | 说明 |
|------|------|------|------|------|
| `id` | Path | Long | ✅ | 订单 ID |
| `travelers` | Body | List\<TravelerEditItem\> | ✅ | 待修改出行人数组(≤30) |
**TravelerEditItem 每行字段**:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | Long | ✅ | 出行人 ID(必须属于该 orderId) |
| `name` | String | ❌ | 姓名 |
| `gender` | String | ❌ | MALE / FEMALE / UNKNOWN |
| `birthday` | LocalDate | ❌ | 出生日期 |
| `idType` | String | ❌ | ID_CARD / PASSPORT / BIRTH_CERT |
| `idNo` | String | ❌ | 证件号(明文传,DB 加密) |
| `nationality` | String | ❌ | 国籍(默认中国,不可设为空字符串) |
| `race` | String | ❌ | 民族(默认汉族,不可设为空字符串) |
| `phone` | String | ❌ | 出行人手机(明文传,DB 加密) |
| `emergencyContact` | String | ❌ | 紧急联系人姓名 |
| `emergencyPhone` | String | ❌ | 紧急联系人电话(明文传) |
| `roomGroupNo` | Integer | ❌ | 同住分组号 |
#### 出参 `Result<TravelerBatchEditRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `updatedCount` | Integer | 实际更新行数 |
| `completedCount` | Integer | 本次操作后变为 COMPLETED 的行数 |
| `pendingCount` | Integer | 仍为 PENDING 的行数 |
| `allCompleted` | Boolean | 该订单所有出行人是否已完善(联动 5 项 checklist) |
#### 请求示例
```json
POST /v3/admin/order/60123456789012/traveler/batch-edit
Authorization: Bearer {admin_jwt}
{
"travelers": [
{
"id": 70123456789012,
"name": "张三",
"gender": "MALE",
"birthday": "1985-08-12",
"idType": "ID_CARD",
"idNo": "220103198508121234",
"nationality": "中国",
"race": "汉族",
"phone": "13800002046",
"roomGroupNo": 1
},
{
"id": 70123456789013,
"name": "张小宝",
"gender": "MALE",
"birthday": "2018-05-01",
"idType": "BIRTH_CERT",
"idNo": "J012345678",
"roomGroupNo": 1
}
]
}
```
#### 响应示例
```json
{
"code": 200,
"data": {
"updatedCount": 2,
"completedCount": 2,
"pendingCount": 0,
"allCompleted": true
},
"msg": "success"
}
```
#### 错误响应清单(本工单新增段位)
| 错误码 | 含义 |
|---|---|
| `581100` | 出行人不存在 |
| `581101` | 12301 必报字段缺失(国籍 / 民族不能为空字符串) |
| `581102` | 订单不存在,无法编辑出行人 |
| `581103` | 性别枚举不合法(应为 MALE / FEMALE / UNKNOWN) |
| `581104` | 同住分组号超出订单家庭数上限 |
| `581105` | 批量大小超限(>30,@Size 已 fallback,Service 层防御性二次校验) |
| `581110` | 出行人 ID 不属于该订单 |
| `581111` | 已签电子合同后禁止修改证件号(合同 signed 后 idNo 冻结) |
| `581112` | 证件号格式不合法(身份证 18 位 / 护照 5-20 位) |
| `581113` | 手机号格式非法(应为 11 位数字) |
| `581114` | 出生日期不能晚于今天 |
| `581119` | 出行人证件号重复(同订单内 idNo 去重) |
错误响应体示例:
```json
{
"code": 581111,
"msg": "已签电子合同后禁止修改证件号",
"data": null
}
```
---
## 四、契约约束
### 校验顺序(任一失败整事务回滚)
1. 批量大小 ≤30(581105)
2. 订单存在(581102)
3. 身份证去重(581119,同请求体内自查重)
4. 姓名硬拦截(异常态字符,跟 TravelerNameValidator 共享口径)
5. 12301 字段非空字符串(581101)
6. 格式校验: idNo / phone / birthday(581112 / 581113 / 581114) + gender 枚举(581103) + roomGroupNo 范围(581104)
7. 归属校验: 每行 id 必须属于该 orderId(581110)
8. **已签电子合同冻结**: `contract_status = SIGNED` 时禁改证件号(581111)
### 并发控制
- `@Idempotent(timeout=3s)`: 同订单 3 秒重复提交直接拒绝
- `@Lock4j(expire=30000ms)`: 同订单批量更新加 30 秒分布式锁,防 admin / mp 同时提交竞态
---
## 五、数据库行为
- UPDATE N 行 `order_traveler`(只更非 null 字段,null 入参不覆盖原值)
- 派生 `profile_status`(若 idType/idNo/name/birthday/gender 齐全 → COMPLETED 否则 PENDING)
- INSERT 1 行 `order_status_log`,reason="出行人补全"
- 触发"已完善"系统标签事件(联动 confirm-checklist 的 TRAVELER_COMPLETE)
---
## 六、边界行为
- 未登录 → 401(网关拦截)
- 入参 travelers 为 null / 空数组 → 400(@NotEmpty)
- 入参 travelers.size > 30 → 400(@Size,@Idempotent 之前拦截)
- 订单不存在 → 581102
- 重复提交(3 秒内) → 接口直接拒绝(@Idempotent 拦截)
- 已签电子合同 → 581111(只冻结 idNo,其他字段仍可改)
---
## 七、不影响范围
- **仅影响**: 管理后台 F27 出行人补全表单
- **零影响**:
- 单个新增 `/traveler/add`(#2519 独立接口)
- 软删 `/traveler/{travelerId}/delete`(#2520 独立接口)
- 列表 `/traveler/list`(只读,#2517)
- mp 端出行人编辑接口
- 大交通批次 / 桥接表(本接口不动 transport_plan)
---
## 八、测试环境已验证
```
POST https://web.test.1814.love:9443/v3/admin/order/60123456789012/traveler/batch-edit
Body: {"travelers":[{id, name, ...}]}
→ 200 + updatedCount/completedCount/pendingCount/allCompleted ✓
反例:
- 12301 字段为空 → 581101 ✓
- 出行人不属于订单 → 581110 ✓
- 合同 SIGNED 改 idNo → 581111 ✓
- 重复提交(3 秒内) → 接口拒绝 ✓
```
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| #1984 | - | [§2 traveler skeleton] 9 接口空骨架 + Mock ServiceImpl | ❌ 被本 PR 真实化覆盖 |
| **本 PR #2535** | **#2518** | batch-edit 接通真实业务 + 9 错误码 + 幂等 / 锁 | ✅ 最新 |
---
## 十、相关文档
- API SPEC: `D:/work2/HL-v3/docs/order-v3/api/API-SPEC-V5.48.html` §2.2
- 关联 Issue: [wx/HL#2518](https://git.1814.love:8443/wx/HL/issues/2518)
- 关联 PR: [wx/HL#2535](https://git.1814.love:8443/wx/HL/pulls/2535)

查看文件

@ -1,218 +0,0 @@
# 出行人新增: POST /traveler/add 路径迁移 + 真实业务化
> **服务**: hl-order-service-v3 (端口 8084 / 二期)
> **PR**: #2536
> **Issue**: #2519
> **日期**: 2026-05-18
> **影响范围**: 管理后台 F27 改人数场景(临时加 1 个出行人)
> **存放目录**: `changelogs-v2/2026-05/`(二期 v3 专属,带 -v2 后缀)
> **部署 commit**: dev-v3 `16e861a7d`
> **测试服已验证**: ✅(/@qa 通过 9443 网关 + 真 admin token round-trip)
---
## ⚠️ 关键变化
1. **破坏性路径迁移**: 老 `POST /v3/admin/order/{id}/travelers` → 新 `POST /v3/admin/order/{id}/traveler/add`(对齐 V5.48 §2.3 命名规范)
2. **从 Mock 占位变真实业务**: 三道校验 + INSERT + UPDATE order_main 人数 + INSERT order_status_log,而不是直接 insert 不校验。
3. **新增 4 个错误码**: 581115 / 581116 / 581117 / 581118
4. **加幂等 @Idempotent(3s) + 分布式锁 @Lock4j(30s)**: 防止与 batch-edit / 自身重复提交并发,导致 order_main 人数计数 race。
---
## 一、背景
文档 V5.48 §2.3 定义: 当订单实际出行人数 > 创单声明人数(典型场景: 客户带了未声明的孩子)时,定制师 B 端临时加人。后端必须同步 UPDATE `order_main.<type>Count`,且**不允许已结算 / 已取消 / 退款中的订单加人**(避免影响财务封账与退款冲账)。
---
## 二、变更接口清单
| # | 接口 | 方法 | 老路径 | 新路径 | 变更类型 |
|---|------|------|--------|--------|----------|
| 1 | 单个出行人新增 | POST | `/v3/admin/order/{id}/travelers` | `/v3/admin/order/{id}/traveler/add` | 破坏性路径迁移 + 真实业务化 |
---
## 三、接口详情
### 1. 单个出行人新增 `POST /v3/admin/order/{id}/traveler/add`
**VO**: `TravelerCreateReqVO`(请求体) / `TravelerVO`(响应,16 字段)
#### 入参
| 字段 | 位置 | 类型 | 必填 | 说明 |
|------|------|------|------|------|
| `id` | Path | Long | ✅ | 订单 ID |
| `travelerType` | Body | String | ✅ | ADULT / CHILD / YOUNG_CHILD / BABY |
| `name` | Body | String | ❌ | 姓名(可后填) |
| `gender` | Body | String | ❌ | MALE / FEMALE / UNKNOWN |
| `birthday` | Body | LocalDate | ❌ | 出生日期 |
| `idType` | Body | String | ❌ | ID_CARD / PASSPORT / BIRTH_CERT |
| `idNo` | Body | String | ❌ | 证件号(明文传,DB 加密) |
| `nationality` | Body | String | ❌ | 国籍(默认"中国") |
| `race` | Body | String | ❌ | 民族(默认"汉族") |
| `phone` | Body | String | ❌ | 出行人手机(明文传) |
| `emergencyContact` | Body | String | ❌ | 紧急联系人姓名 |
| `emergencyPhone` | Body | String | ❌ | 紧急联系人电话 |
| `roomGroupNo` | Body | Integer | ❌ | 同住分组号 |
#### 出参 `Result<TravelerVO>`
与 §2.1 列表接口完全相同的 16 字段 VO(含 `id` / `orderId` / `travelerType` / `name` / `gender` / `birthday` / `idType` / `idNo` / `nationality` / `race` / `phone` / `emergencyContact` / `emergencyPhone` / `roomGroupNo` / `profileStatus` / `transportPlanIds`)。
#### 请求示例
```json
POST /v3/admin/order/60123456789012/traveler/add
Authorization: Bearer {admin_jwt}
{
"travelerType": "CHILD",
"name": "王小明",
"gender": "MALE",
"birthday": "2018-06-20",
"idType": "BIRTH_CERT",
"idNo": "J012345678",
"nationality": "中国",
"race": "汉族",
"roomGroupNo": 1
}
```
#### 响应示例
```json
{
"code": 200,
"data": {
"id": 70123456789014,
"orderId": 60123456789012,
"travelerType": "CHILD",
"name": "王小明",
"gender": "MALE",
"birthday": "2018-06-20",
"idType": "BIRTH_CERT",
"idNo": "J012345678",
"nationality": "中国",
"race": "汉族",
"phone": null,
"emergencyContact": null,
"emergencyPhone": null,
"roomGroupNo": 1,
"profileStatus": "COMPLETED",
"transportPlanIds": []
},
"msg": "success"
}
```
#### 错误响应清单(本工单新增段位)
| 错误码 | 含义 |
|---|---|
| `581100` | 出行人主段位—— 订单不存在 |
| `581102` | 订单不存在,无法编辑出行人 |
| `581115` | 实际人数已等于声明人数,请先调整订单人数再新增出行人 |
| `581116` | 当前订单状态禁止新增出行人(已结算 / 已取消 / 退款中) |
| `581117` | 出行人类型与现有同住分组冲突 |
| `581118` | 新增出行人缺少必填字段(travelerType 必填) |
错误响应体示例:
```json
{
"code": 581116,
"msg": "当前订单状态禁止新增出行人(已结算 / 已取消 / 退款中)",
"data": null
}
```
---
## 四、契约约束
### 校验顺序(任一失败整事务回滚)
1. **订单存在**(581102)
2. **订单状态白名单**: 拒绝 SETTLED / CANCELLED / REFUNDING(581116)
3. **实际 vs 声明人数**: `count(order_traveler 未软删) >= sum(adultCount + childCount + youngChildCount + babyCount)` 时拒绝(581115,要求先调订单人数)
4. **同住分组冲突**(581117,可选字段校验)
5. **必填**: travelerType(581118)
### 并发控制
- `@Idempotent(timeout=3s)`: 同订单 3 秒重复提交直接拒绝
- `@Lock4j(expire=30000ms,keys="order:traveler:add:" + #id)`: 同订单 30 秒锁,防与 batch-edit 并发导致 order_main 人数计数 race
---
## 五、数据库行为
- INSERT 1 行 `order_traveler`(派生 `profile_status`)
- UPDATE `order_main.<type>Count` +1(按 `travelerType` 选择: adultCount / childCount / youngChildCount / babyCount)
- INSERT 1 行 `order_status_log`,reason="新增出行人",flowStatus 不变(from == to)
| travelerType | order_main 字段 | delta |
|---|---|---|
| ADULT | `adult_count` | +1 |
| CHILD | `child_count` | +1 |
| YOUNG_CHILD | `young_child_count` | +1 |
| BABY | `baby_count` | +1 |
---
## 六、边界行为
- 未登录 → 401(网关拦截)
- 订单不存在 → 581102
- 订单 status = SETTLED / CANCELLED / REFUNDING → 581116(优先于人数校验)
- 实际人数已等于声明 → 581115(典型场景: 客户原本说 2 大 1 小现已 2 大 1 小,要先把声明改成 2 大 2 小再加人)
- travelerType 未识别 → 防御性 return(不报错也不更新人数,实际由前置 @NotBlank 拦截)
---
## 七、不影响范围
- **仅影响**: 管理后台 F27 改人数(新增)场景
- **零影响**:
- 批量编辑 `/traveler/batch-edit`(#2518 独立接口)
- 软删 `/traveler/{travelerId}/delete`(#2520 独立接口)
- 列表 `/traveler/list`(只读)
- mp 端出行人接口
- 老路径 `POST /travelers` 已下线,前端调用必须改用 `/traveler/add`
---
## 八、测试环境已验证
```
POST https://web.test.1814.love:9443/v3/admin/order/60123456789012/traveler/add
Body: {"travelerType":"CHILD","name":"王小明","birthday":"2018-06-20",...}
→ 200 + TravelerVO(含 id / profileStatus / transportPlanIds) ✓
→ order_main.child_count +1 ✓
→ order_status_log 新增 1 行 reason=新增出行人 ✓
反例:
- 订单 status=SETTLED → 581116 ✓
- 实际人数 = 声明人数 → 581115 ✓
- 老路径 POST /travelers → 404 ✓
```
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| #1984 | - | [§2 traveler skeleton] 空骨架 Mock | ❌ 被本 PR 真实化覆盖 |
| #2535 | #2518 | batch-edit 真实业务化(同期工单,共用 581100-581114 段位) | ✅ 有效 |
| **本 PR #2536** | **#2519** | add 接通真实业务 + 路径迁移 + 4 错误码(581115-581118) | ✅ 最新 |
---
## 十、相关文档
- API SPEC: `D:/work2/HL-v3/docs/order-v3/api/API-SPEC-V5.48.html` §2.3
- 关联 Issue: [wx/HL#2519](https://git.1814.love:8443/wx/HL/issues/2519)
- 关联 PR: [wx/HL#2536](https://git.1814.love:8443/wx/HL/pulls/2536)

查看文件

@ -1,188 +0,0 @@
# 出行人软删: DELETE → POST /traveler/{travelerId}/delete 路径迁移 + 真实业务化
> **服务**: hl-order-service-v3 (端口 8084 / 二期)
> **PR**: #2537
> **Issue**: #2520
> **日期**: 2026-05-18
> **影响范围**: 管理后台 F27 改人数场景(临时取消同行 1 人)
> **存放目录**: `changelogs-v2/2026-05/`(二期 v3 专属,带 -v2 后缀)
> **部署 commit**: dev-v3 `34c294465`
> **测试服已验证**: ✅(/@qa 通过 9443 网关 + 真 admin token round-trip)
---
## ⚠️ 关键变化
1. **破坏性路径 + 方法迁移**:
- **方法**: DELETE → POST(对齐 v3 风格,所有写操作统一 POST)
- **路径**: `/travelers/{travelerId}``/traveler/{travelerId}/delete`(对齐 V5.48 §2.4)
2. **从 Mock 占位变真实业务**: 4 道校验 + 软删 + 桥接表级联软删 + count-1 + status_log。
3. **新增 2 个错误码**: `581106`(已签合同禁删) / `581107`(最后 1 成人禁删)。
- ⚠️ 注意码段: **不是 581140/581141**,大交通段位 581120+ 已被占用,出行人主段位空挡是 581106/581107 紧贴主块(文档原稿写的 581119/581120 已被 TRAVELER_ID_CARD_DUPLICATE / TRANSPORT_PLAN_NOT_FOUND 占用)。
4. **加幂等 + 锁整事务**: `@Idempotent(3s) + @Lock4j(30s)`
---
## 一、背景
文档 V5.48 §2.4 定义: 软删除单行 `order_traveler`,同步:
1. UPDATE `order_main.<type>Count` -1
2. UPDATE `order_transport_plan_traveler.deleted=1`(级联解除其在大交通桥接表的所有关联)
3. INSERT `order_status_log`,reason="删除出行人"
并有 2 条业务硬约束:
- **已签电子合同(contract_status = SIGNED)的订单禁止删人**(合同已固定参与人,删除会破坏合同人员一致性)
- **订单最后 1 位成人禁止删除**(无成人订单逻辑上无效,会影响保险 / 大交通 / 房型分配)
---
## 二、变更接口清单
| # | 接口 | 方法 | 老路径 | 新路径 | 变更类型 |
|---|------|------|--------|--------|----------|
| 1 | 出行人软删 | DELETE → **POST** | `/v3/admin/order/{id}/travelers/{travelerId}` | `/v3/admin/order/{id}/traveler/{travelerId}/delete` | 破坏性方法 + 路径迁移 + 真实业务化 |
---
## 三、接口详情
### 1. 出行人软删 `POST /v3/admin/order/{id}/traveler/{travelerId}/delete`
#### 入参
| 字段 | 位置 | 类型 | 必填 | 说明 |
|------|------|------|------|------|
| `id` | Path | Long | ✅ | 订单 ID |
| `travelerId` | Path | Long | ✅ | 出行人 ID |
(请求体: 无)
#### 出参 `Result<Boolean>`
```json
{
"code": 200,
"data": true,
"msg": "success"
}
```
#### 请求示例
```
POST /v3/admin/order/60123456789012/traveler/70123456789013/delete
Authorization: Bearer {admin_jwt}
```
#### 错误响应清单(本工单新增段位)
| 错误码 | 含义 |
|---|---|
| `581100` | 出行人不存在 |
| `581102` | 订单不存在,无法删除出行人 |
| `581106` | **已签电子合同,禁止删除出行人**(本 PR 新增) |
| `581107` | **出行人是订单最后 1 位成人,禁止删除**(本 PR 新增) |
| `581110` | 出行人 ID 不属于该订单 |
> 段位说明: 文档原稿 §2.4 期望 `581119 / 581120`,但 581119 已被 TRAVELER_ID_CARD_DUPLICATE 占用,581120 已被 TRANSPORT_PLAN_NOT_FOUND 占用。本 PR 落到出行人主段位空挡 **581106 / 581107**,紧贴主块,保留 581140-581144 给 §2.6 大交通后续工单。
错误响应体示例:
```json
{
"code": 581106,
"msg": "已签电子合同,禁止删除出行人",
"data": null
}
```
---
## 四、契约约束
### 校验顺序(任一失败整事务回滚,4 道闸)
1. **订单存在性**(581102): 查 `order_main` by `id`
2. **出行人归属**(581110): 出行人的 `order_id` 必须等于 path `id`(防越权 / 错传)
3. **合同冻结**(581106): 订单 `contract_status = SIGNED` → 拒绝
4. **最后 1 成人**(581107): 当前操作是 ADULT 且订单未软删的 ADULT 行数 = 1 → 拒绝
### 并发控制
- `@Idempotent(timeout=3s)`: 同 `(orderId, travelerId)` 3 秒重复提交直接拒绝(防快速双击)
- `@Lock4j(expire=30000ms)`: 同订单 30 秒锁,与 batch-edit / add 互斥,防 count 字段 race
---
## 五、数据库行为
整事务:
| 操作 | 表 | 说明 |
|------|------|------|
| UPDATE | `order_traveler` | `deleted=1` `deleted_at=NOW()` |
| UPDATE | `order_transport_plan_traveler` | 该出行人所有未软删的桥接行 `deleted=1`(级联解除大交通关联) |
| UPDATE | `order_main.<type>Count` | -1(按 `travelerType` 选择: adultCount / childCount / youngChildCount / babyCount) |
| INSERT | `order_status_log` | reason="删除出行人",flowStatus 不变(from == to) |
---
## 六、边界行为
- 未登录 → 401(网关拦截)
- 订单不存在 → 581102
- 出行人不属于订单(越权) → 581110
- 合同 SIGNED → 581106(优先于人数校验)
- 删最后 1 成人 → 581107(允许删 CHILD / YOUNG_CHILD / BABY 即使他们也是最后 1 个)
- 出行人有大交通关联 → 自动级联软删桥接表,不阻断主流程
- 出行人已被软删过(重复删) → 581100(查不到行)
---
## 七、不影响范围
- **仅影响**: 管理后台 F27 改人数(删除)场景
- **零影响**:
- 批量编辑 `/traveler/batch-edit`(#2518 独立接口)
- 新增 `/traveler/add`(#2519 独立接口)
- 列表 `/traveler/list`(只读)
- 大交通错误码段位 581120-581129(保留)
- 老路径 `DELETE /travelers/{travelerId}` 已下线,前端调用必须改用 `POST /traveler/{travelerId}/delete`
---
## 八、测试环境已验证
```
POST https://web.test.1814.love:9443/v3/admin/order/60123456789012/traveler/70123456789013/delete
Authorization: Bearer {admin_jwt}
→ 200 + data: true ✓
→ order_traveler.deleted=1 ✓
→ order_transport_plan_traveler 级联 deleted=1 ✓
→ order_main.child_count -1 ✓
→ order_status_log 新增 1 行 reason=删除出行人 ✓
反例:
- 合同 SIGNED 删人 → 581106 ✓
- 删最后 1 成人 → 581107 ✓
- travelerId 不属于该订单 → 581110 ✓
- DELETE 老方法老路径 → 404 / 405 ✓
```
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| #1984 | - | [§2 traveler skeleton] 空骨架 Mock | ❌ 被本 PR 真实化覆盖 |
| #2535 | #2518 | batch-edit 真实化(同期工单,共用 581100-581114) | ✅ 有效 |
| #2536 | #2519 | add 真实化(同期工单,581115-581118) | ✅ 有效 |
| **本 PR #2537** | **#2520** | delete 接通真实业务 + 方法+路径迁移 + 2 错误码(581106/581107) | ✅ 最新 |
---
## 十、相关文档
- API SPEC: `D:/work2/HL-v3/docs/order-v3/api/API-SPEC-V5.48.html` §2.4
- 关联 Issue: [wx/HL#2520](https://git.1814.love:8443/wx/HL/issues/2520)
- 关联 PR: [wx/HL#2537](https://git.1814.love:8443/wx/HL/pulls/2537)

查看文件

@ -1,209 +0,0 @@
# 合同/保险模块 TODO 接通 — 从占位 → 完整可用 (工单 #2586)
> **存放目录**: 二期(v3, `order-v3` 标签)→ `changelogs-v2/2026-05/`
>
> **服务**: hl-order-v3 (端口 8084 / 测试服 9443 网关 + `/v3/admin/...`)
> **PR**: #2590 (主体) + #2591 (CR 跟进) + #2603 (P2/P3 + Publisher 接入)
> **Issue**: #2586
> **日期**: 2026-05-19
> **影响范围**: 管理后台「合同」「保险」两大模块全部接口 + 「订单状态机 CONFIRM 转换」副作用
---
## ⚠️ 关键变化(前端必看 4 点)
### 1. 合同 / 保险模块从「占位返空 / 抛异常」转为「完整可用」(行为变化,非破坏性)
PR #2590 之前合同 / 保险服务的所有写接口都返 `UnsupportedOperationException` 占位,查询接口返空列表 + `log.warn TODO`。本次合并后**全部真实可用**
- `POST /v3/admin/contract/create-by-scheme` 合同按方案创建 — 真实创建并接入第三方合同平台12301 / 腾讯电子签)
- `POST /v3/admin/insurance/purchase` 手动投保 — 真实写 `insurance_order` + 调宝游 API
- `POST /v3/admin/insurance/auto-purchase` / `auto-purchase-by-scheme` 自动投保 — 真实链路
- `POST /v3/admin/contract/resend-sign-sms` 重发签署短信 — 真实做用户归属校验
- `POST /v3/admin/contract/void/{id}` 作废 — 真实状态流转
- 列表 / 详情 / scheme 列表 — 全部返真实数据
**前端可解除**任何针对这些接口的"待后端实现"占位提示,按真实响应处理。
### 2. `POST /v3/admin/insurance/purchase` 新增订单状态校验(业务规则收紧)
只允许以下组合的订单投保:
| 维度 | 允许值 |
|------|-------|
| `orderStatus` | `PENDING_DEPARTURE` / `PENDING_BALANCE` / `TRAVELLING` |
| `payStatus` | `DEPOSIT_PAID` / `FULLY_PAID` |
任一不符 → 返业务码 `540022` `ORDER_NOT_CONFIRMED_FOR_INSURANCE` 文案 `"请先确认订单后再配置保险(当前订单状态不允许投保)"`
**前端处理建议**
- 投保按钮的可点击态可加前置判断(不必依赖后端报错才提示)
- 若直接拦不到,把 540022 错误码处理成 toast 弹该文案即可
### 3. `POST /v3/admin/order/{id}/transition` event=CONFIRM 自动触发合同 / 保险创建(异步)
订单状态机 `CUSTOMIZING → PENDING_DEPARTURE` 的 CONFIRM 转换会**异步**触发:
- 自动创建合同(前提:`contract_scheme.status=ACTIVE``contract_template_code` 配置过)
- 自动投保(前提:`insurance_scheme.status=ACTIVE`
接口同步返回 `triggeredEvents: ["ASYNC_CONTRACT_GENERATE", "ASYNC_INSURANCE_ISSUE"]`,**真正写库发生在事务 commit 后异步线程**`@TransactionalEventListener(AFTER_COMMIT)`)。
**前端表现**
- CONFIRM 转换 200 返回后,不要立刻显示「合同已生成 / 保险已购买」(异步还没跑完)
- 建议 transition 200 后 toast 提示「订单已确认,合同/保险将在 1 分钟内自动生成」,然后等用户主动刷新合同列表 / 保险列表看到记录
- 自动签约失败时合同/保险域有内部紧急待办 + 通知中心推送(通知中心走 NotificationDispatcher,渠道在 `notification_event_config` 配)
### 4. 出行人变更触发合同 / 保险作废+重做(已签约订单)
`TravelerService` 的 5 个写方法(`batchEdit` / `add` / `delete` / `addByInternal` / `updateByInternal`)末尾按状态白名单异步发布 `TravelerChangedEvent`
| 订单 orderStatus | 是否触发作废重做 |
|---|---|
| `PENDING_DEPARTURE` / `PENDING_BALANCE` / `TRAVELLING` | ✅ 触发:作废旧合同 + 重新签约;作废旧保单 + 重投 |
| `CUSTOMIZING` / `PENDING_PAY` / `PENDING_COMPLETE` 等未签约 | ❌ 不触发(避免事件风暴) |
| `REVIEWING` / `SETTLED` / `CANCELLED` | ❌ 不触发 |
**前端表现**:已签约订单编辑出行人后,合同 / 保险记录会自动作废并重新生成(异步),用户应感知到合同 PDF / 保单号变化。建议在出行人编辑成功后给一个 toast「已签约订单编辑出行人将触发合同/保险重新生成」。
---
## 一、背景
订单 v3 的 contract / insurance 域骨架在 yst 早期 PR 已从 v2 搬运,但接通订单核心域(`OrderInfoMapper` / `OrderTravelerMapper`)的位置全部以 `TODO(PR-order-core)` 占位,方法体抛 `UnsupportedOperationException` 或返 `Collections.emptyList()`
PR #2552-2554 出行人三连完成后,`OrderInfoMapper` / `OrderTravelerMapper` 已在 v3 落位,本工单收尾 — 把 contract / insurance 完整对接、激活 publisher、补全单测、修复 CR 三轮深审找出的 3 P0 + 5 P1 + 10 P2/P3 + 3 P3-NEW。
---
## 二、变更接口清单
### 合同模块admin
| # | 接口 | 方法 | 路径 | 变更类型 |
|---|------|------|------|---------|
| 1 | 合同方案列表(启用) | GET | `/v3/admin/contract/scheme/list` | 行为变化(返真实数据) |
| 2 | 合同列表 | GET | `/v3/admin/contract/list` | 行为变化 |
| 3 | 按方案创建合同 | POST | `/v3/admin/contract/create-by-scheme` | 行为变化(真实写库 + 第三方调用) |
| 4 | 重发签署短信 | POST | `/v3/admin/contract/resend-sign-sms` | 行为变化(接入用户归属校验) |
| 5 | 作废合同 | POST | `/v3/admin/contract/void/{contractId}` | 行为变化 |
| 6 | 合同方案 CRUD | POST | `/v3/admin/contract/scheme/*` | 新增 `defaultDestination` / `defaultDepartureCity` 字段 |
### 合同模块mp 内部)
| # | 接口 | 方法 | 路径 | 变更类型 |
|---|------|------|------|---------|
| 7 | 用户合同列表 | GET | `/internal/mp/contract/list` | 行为变化(按 userId 反查 orderIds |
| 8 | 用户合同详情 | GET | `/internal/mp/contract/detail/{contractId}` | 行为变化(接入归属校验) |
### 保险模块admin
| # | 接口 | 方法 | 路径 | 变更类型 |
|---|------|------|------|---------|
| 9 | 保险方案列表(启用) | GET | `/v3/admin/insurance/scheme/list` | 行为变化 |
| 10 | 保单列表 | GET | `/v3/admin/insurance/orders` | 行为变化 |
| 11 | 手动投保 | POST | `/v3/admin/insurance/purchase` | 行为变化 + **状态校验新增** |
| 12 | 自动投保 | POST | `/v3/admin/insurance/auto-purchase` | 行为变化 |
| 13 | 按方案自动投保 | POST | `/v3/admin/insurance/auto-purchase-by-scheme` | 行为变化 |
| 14 | 取消保险 | POST | `/v3/admin/insurance/cancel` | 行为变化 |
| 15 | 投保预览 | POST | `/v3/admin/insurance/preview-apply` | 行为变化 |
### 订单状态机副作用
| # | 接口 | 方法 | 路径 | 变更类型 |
|---|------|------|------|---------|
| 16 | 订单状态流转 | POST | `/v3/admin/order/{id}/transition` | **CONFIRM 事件副作用扩展**:异步触发自动签约/投保 |
| 17 | 出行人 batchEdit | POST | `/v3/admin/order/{id}/traveler/batch-edit` | 已签约阶段触发作废重做 |
| 18 | 出行人 add | POST | `/v3/admin/order/{id}/traveler/add` | 同上 |
| 19 | 出行人 delete | POST | `/v3/admin/order/{id}/traveler/delete/{travelerId}` | 同上 |
---
## 三、字段变更(合同方案)
`ContractScheme` 实体新增 2 个字段(对应表 `contract_scheme`
| 字段名 | 类型 | 含义 | 业务规则 |
|--------|------|------|---------|
| `defaultDestination` | varchar(64) NULL | 方案默认目的地 | createByScheme 时取值优先级scheme.defaultDestination → productName 关键字提取(呼伦贝尔/阿尔山/新疆/内蒙古/青海/西藏等) → 全局兜底「内蒙古呼伦贝尔」 |
| `defaultDepartureCity` | varchar(64) NULL | 方案默认出发地 | 同上,全局兜底「呼和浩特」 |
**前端影响**admin 合同方案管理表单加 2 个文本输入框(选填)。
---
## 四、错误码新增
| 业务码 | 文案 | 触发条件 |
|--------|------|---------|
| `540022` | 请先确认订单后再配置保险(当前订单状态不允许投保) | 调 `/v3/admin/insurance/purchase` 时订单状态 / 支付状态不符合白名单 |
| `510206` | 合同方案未配置模板 | 调 `/v3/admin/contract/create-by-scheme``scheme.contractTemplateCode = null` |
其他既有错误码不变。
---
## 五、Publisher 异步链路
### `ChecklistConfirmedEvent`
- **发布点**`OrderService.transition()` 守卫 `from=CUSTOMIZING && to=PENDING_DEPARTURE && event=CONFIRM`
- **携带**`orderId / orderNo / operatorId / operatorType (ADMIN/SYSTEM)`
- **operatorId 来源**`AdminContextUtil.getAdminId()`(请求上下文)→ 无则 null + SYSTEM
- **监听者**`ContractEventListener` + `InsuranceEventListener` 都接 `@TransactionalEventListener(AFTER_COMMIT, fallbackExecution=true)`
- **下游行为**listener 走 `contract.createByScheme` / `insurance.autoPurchase`,失败有紧急待办 + 通知中心降级通知
### `TravelerChangedEvent`
- **发布点**`TravelerService` 5 个写方法末尾
- **守卫**:仅在订单 `orderStatus ∈ {PENDING_DEPARTURE, PENDING_BALANCE, TRAVELLING}` 时发布
- **携带**`orderId / orderNo / changeType (ADD/MODIFY/REMOVE) / affectedTravelerIds`
- **下游行为**listener 走 `contract.invalidateByOrderId + createByScheme`(作废重签)、`insurance.cancelByOrderId + autoPurchase`(退旧重投)
---
## 六、数据库迁移
| 文件 | 内容 |
|------|------|
| `V20260519_004__create_order_todo.sql` | 新增 `order_todo` 表(订单内部待办,自动签约/投保失败时生成紧急待办) |
| `V20260519_005__contract_scheme_add_default_destination.sql` | `contract_scheme``default_destination` + `default_departure_city` 两列 |
---
## 七、测试服真测结论
### 部署
- 测试服 v3 已部署 dev-v3 最新 HEAD `af7dd70` 双实例 8086+8186
- Flyway 应用 V20260519_004 + V20260519_005 成功
- Nacos test namespace 注册健康,9443 网关路由通
### API 真测test_admin token 通过 trusted device 通路)
| 接口 | 结果 |
|------|------|
| GET 合同 scheme list / 合同 list / 保险 scheme list / 保单 list | ✅ 200 真实数据 |
| POST insurance.purchase CUSTOMIZING+UNPAID | ✅ 抛 540022 |
| POST insurance.purchase PENDING_DEPARTURE+UNPAID | ✅ 抛 540022**证明 pay_status 单独校验起作用** |
| POST insurance.purchase PENDING_DEPARTURE+DEPOSIT_PAID | ✅ 状态校验过,进入 planId 校验阶段 |
| POST order.transition CONFIRM | ✅ 200,triggeredEvents 含 ASYNC_CONTRACT_GENERATE + ASYNC_INSURANCE_ISSUE,listener 真收到(业务数据 scheme 缺所以 fail-soft |
### 单测
- `mvn -pl hl-order-service-v3 test`**Tests run 1096 / Failures 0 / Errors 0**(基线 1069 → +27
---
## 八、已知限制(非缺陷)
1. **测试服当前 active contract_scheme 都没填 `contract_template_code`**,所以 transition CONFIRM 异步自动签约会 short-circuit「无可用合同方案」并发紧急待办。需要在 admin 后台进 `/v3/admin/contract/scheme/*` 表单配置 `contract_template_code` 后才能完整测端到端。
2. **测试服当前没有 active `insurance_scheme`**,同上。
3. 自动签约时按 v3 当前 list 第一个 ACTIVE 方案挑选(暂无 mchId/productId 路由),后续多公司多产品场景需要在 `ContractScheme``mchId` / `productId` 字段后过滤。
---
## 九、关联
- 主 PR#2590 (https://git.1814.love:8443/wx/HL/pulls/2590)
- CR 跟进:#2591
- 最终零遗留:#2603
- 工单:#2586
---
**前端 mmg**:请同步检查投保 / 合同管理 / 出行人编辑 / 订单确认 4 个页面的 UI 提示逻辑,重点是「投保按钮置灰条件」+「订单确认后异步合同/保险生成提示」+「已签约后编辑出行人的告知 toast」。如有疑问随时找。

查看文件

@ -1,500 +0,0 @@
# tag-library: 私人标签库管理 6 接口v5.14 骨架)
> **存放目录**: `changelogs-v2/2026-05/`v3 二期)
> **服务**: hl-order-service-v3端口 8086
> **PR**: #2045(骨架合并)/ #2240Gateway 路由修复)
> **Issue**: #2045v5.14 §14 章节实现)
> **日期**: 2026-05-19
> **影响范围**: 管理后台「我的标签库」管理页 + 创建订单时的标签下拉
---
## ⚠️ 关键变化(首次发布无)
本次为首次发布;非纠错变更。
⚠️ **当前实现状态**6 个接口已上线,**ServiceImpl 为 Mock 实现**(返回硬编码样例数据),真实 DB 业务在跟进中。**接口契约本次发布后不再变**,前端可按契约对接联调。
---
## 一、背景
v5.14 引入「私人标签库」概念:每个管理员账号有自己独立的标签预设库,可在订单创建/编辑页打标签时从下拉中选取(取代以前自由文本输入)。
**模型隔离**
- userId 从 JWT 自动派生,前端**不传**操作人
- 仅本人可读/写/排序/置顶/删除自己的库
- 删除标签库预设**不**影响已挂订单上的历史 order_tag 实例(保留快照)
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 |
|---|------|------|------|----------|
| 1 | 查我的标签库 | GET | `/v3/admin/tag-library` | 新增 |
| 2 | 新增标签库预设 | POST | `/v3/admin/tag-library` | 新增 |
| 3 | 修改标签库预设(重命名/换色) | PUT | `/v3/admin/tag-library/{id}` | 新增 |
| 4 | 删除标签库预设(软删) | DELETE | `/v3/admin/tag-library/{id}` | 新增 |
| 5 | 批量排序标签库(拖拽) | PUT | `/v3/admin/tag-library/sort` | 新增 |
| 6 | 置顶/取消置顶toggle | PUT | `/v3/admin/tag-library/{id}/pin` | 新增 |
---
## 三、接口详情
### 1. 查我的标签库 `GET /v3/admin/tag-library`
**VO**: `TagLibraryListReqVO``Result<List<TagLibraryItemVO>>`
#### 入参Query
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| keyword | Query | String | ❌ | - | 模糊搜索标签名(管理页搜索框) |
| pinnedOnly | Query | Boolean | ❌ | 默认 false | 只返置顶 |
> userId 从 JWT 自动注入,前端**不要**在 Query 里传 userId。
#### 出参 `Result<List<TagLibraryItemVO>>`
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Long | 标签库行 ID雪花,前端按字符串处理避免精度丢失 |
| tagName | String | 标签名 |
| tagColor | String | HEX 色值(如 `#5B8FF9` |
| sortOrder | Integer | 排序值 |
| isPinned | Boolean | 是否置顶 |
| usedCount | Integer | 使用次数 |
| lastUsedAt | LocalDateTime | 最近使用时间ISO 8601,可能为 null |
**列表排序**`isPinned DESC, sortOrder ASC, lastUsedAt DESC`(后端固定,前端不要前端再排)
#### 请求示例
```
GET /v3/admin/tag-library?keyword=高&pinnedOnly=false
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": [
{
"id": 2050000000000001001,
"tagName": "高净值客户",
"tagColor": "#FF6B6B",
"sortOrder": 1,
"isPinned": true,
"usedCount": 12,
"lastUsedAt": "2026-05-15T14:32:10"
},
{
"id": 2050000000000001002,
"tagName": "高频复购",
"tagColor": "#5B8FF9",
"sortOrder": 2,
"isPinned": false,
"usedCount": 5,
"lastUsedAt": "2026-05-10T09:15:00"
}
],
"success": true
}
```
#### 空数据响应(首次访问无库)
```json
{
"code": 200,
"message": "成功",
"data": [],
"success": true
}
```
---
### 2. 新增标签库预设 `POST /v3/admin/tag-library`
**VO**: `TagLibraryCreateReqVO``Result<TagLibraryItemVO>`
#### 入参Body
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| tagName | Body | String | ✅ | 非空,长度 ≤ 50 | 标签名 |
| tagColor | Body | String | ❌ | HEX 格式 | 默认 `#5B8FF9` |
#### 出参 `Result<TagLibraryItemVO>`
字段同接口 1 出参(返回新建行完整字段,`sortOrder` 追加到末尾,`isPinned=false``usedCount=0``lastUsedAt=null`)。
#### 请求示例
```json
{
"tagName": "VIP",
"tagColor": "#FFD700"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"id": 2050000000000001005,
"tagName": "VIP",
"tagColor": "#FFD700",
"sortOrder": 6,
"isPinned": false,
"usedCount": 0,
"lastUsedAt": null
},
"success": true
}
```
#### 错误响应(重名)
```json
{
"code": 581421,
"message": "标签名已存在",
"success": false,
"data": null
}
```
---
### 3. 修改标签库预设 `PUT /v3/admin/tag-library/{id}`
**VO**: `TagLibraryUpdateReqVO``Result<TagLibraryItemVO>`
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | ✅ | - | 标签库行 ID |
| tagName | Body | String | ❌ | 长度 ≤ 50 | 不传则不改 |
| tagColor | Body | String | ❌ | HEX | 不传则不改 |
> 两字段都不传 → 等同空操作(不报错)。
#### 出参
返回修改后的完整 `TagLibraryItemVO`(字段同接口 1
#### 请求示例
```json
{
"tagName": "VIP-Gold",
"tagColor": "#FFA500"
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": {
"id": 2050000000000001005,
"tagName": "VIP-Gold",
"tagColor": "#FFA500",
"sortOrder": 6,
"isPinned": false,
"usedCount": 0,
"lastUsedAt": null
},
"success": true
}
```
#### 错误响应(非本人标签)
```json
{
"code": 581423,
"message": "无权操作该标签",
"success": false,
"data": null
}
```
---
### 4. 删除标签库预设 `DELETE /v3/admin/tag-library/{id}`
**VO**: - → `Result<Boolean>`
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | ✅ | - | 标签库行 ID |
#### 出参
| 字段 | 类型 | 说明 |
|------|------|------|
| data | Boolean | true=删除成功 |
#### 请求示例
```
DELETE /v3/admin/tag-library/2050000000000001005
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": true,
"success": true
}
```
#### 错误响应id 不存在)
```json
{
"code": 581424,
"message": "标签不存在或已删除",
"success": false,
"data": null
}
```
---
### 5. 批量排序标签库 `PUT /v3/admin/tag-library/sort`
**VO**: `TagLibrarySortReqVO``Result<Boolean>`
#### 入参Body
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| items | Body | Array | ✅ | 非空 | 批量排序映射 |
| items[].id | - | Long | ✅ | - | 标签库行 ID |
| items[].sortOrder | - | Integer | ✅ | - | 新排序值 |
> 仅会更新本人库的行;若 `items` 含非本人 id,**整批拒绝**581425,不部分成功。
#### 出参
| 字段 | 类型 | 说明 |
|------|------|------|
| data | Boolean | true=全量成功 |
#### 请求示例
```json
{
"items": [
{ "id": 2050000000000001001, "sortOrder": 1 },
{ "id": 2050000000000001002, "sortOrder": 2 },
{ "id": 2050000000000001005, "sortOrder": 3 }
]
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": true,
"success": true
}
```
#### 错误响应(混入他人 id
```json
{
"code": 581425,
"message": "排序列表含非本人标签 ID",
"success": false,
"data": null
}
```
---
### 6. 置顶/取消置顶 `PUT /v3/admin/tag-library/{id}/pin`
**VO**: `TagLibraryPinReqVO``Result<Boolean>`
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | ✅ | - | 标签库行 ID |
| pinned | Body | Boolean | ✅ | - | `true`=置顶 / `false`=取消置顶 |
#### 出参
| 字段 | 类型 | 说明 |
|------|------|------|
| data | Boolean | true=操作成功 |
#### 请求示例
```json
{
"pinned": true
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": true,
"success": true
}
```
#### 错误响应(非本人)
```json
{
"code": 581423,
"message": "无权操作该标签",
"success": false,
"data": null
}
```
---
## 四、契约约束与正确调用方式
### userId 派生规则
| 场景 | payload |
|------|---------|
| ✅ 调用任一接口 | 仅传业务字段,不传 userId后端从 JWT 取) |
| ❌ Body / Query 传 userId | 后端忽略(不报错但无效) |
### 排序接口幂等性
| 场景 | 行为 |
|------|------|
| ✅ 整列重新提交全量 items | 全量覆盖排序 |
| ✅ 提交部分 items | 仅更新提交的行,其他行 sort_order 保持 |
| ❌ items 含他人 id | 581425 整批拒绝(不部分成功) |
### tagColor 兜底
- POST 不传 `tagColor` → 写入 `#5B8FF9`(深蓝默认)
- PUT 不传 `tagColor` → 不改原值
---
## 五、数据库行为
| 前端动作 | 数据库影响 |
|----------|-----------|
| POST 新建 | `tag_library` 表插入一行,`user_id` 取 JWT,`sort_order = MAX(sort_order)+1``is_pinned=false``used_count=0` |
| PUT 修改 | 仅更新 `tag_name` / `tag_color`;不传字段不动 |
| DELETE | 软删(`deleted=1`),不物理删;不级联清理已挂订单的历史 `order_tag` |
| PUT sort | 批量 UPDATE `sort_order`,事务包裹(全成功或全回滚) |
| PUT pin | 仅更新 `is_pinned``updated_at` 同步刷新 |
**软删与订单标签的关系**
| 时间线 | 状态 |
|--------|------|
| T0管理员将"VIP"标签挂到订单 A | `order_tag` 表插入快照tag_name="VIP", tag_color="#FFD700" |
| T1管理员从标签库删除"VIP"预设 | `tag_library.deleted=1``order_tag` 表**不变** |
| T2再次打开订单 A | 仍显示"VIP"标签(快照),但下拉里不再出现 |
---
## 六、边界行为
- 未登录 → 401网关拦截
- 列表为空(首次访问/全部删完)→ `Result.success([])`,**不**返 404 / 不返 581420
- DELETE/PUT 操作不存在的 id → 581424
- DELETE/PUT 操作他人 id → 581423
- 排序提交空 items 数组 → 400`@NotEmpty`
- 新增标签名同用户已存在 → 581421
- 新增标签名超长 / 空 / 全空格 → 400 / 581422
---
## 七、不影响范围
- **仅影响**: 管理后台「我的标签库」管理页 + 订单页打标签下拉
- **零影响**:
- C 端小程序(无 /mp 接口)
- 订单创建 / 编辑接口(订单上的 `order_tag` 写入是另外的接口链路,不在本批)
- 历史已挂订单的标签(快照不动)
- 其他管理员的标签库(用户隔离)
---
## 八、测试环境已验证
⚠️ **当前实现为 Mock**ServiceImpl 返回硬编码样例数据,未走 DB。前端可按本契约对接联调;联调通过后后端会接入真实 DB**契约不再变**)。
```
GET /v3/admin/tag-library → 200 + 5 条 Mock 样例 ✓
POST /v3/admin/tag-library → 200 + 新增样例回显 ✓
PUT /v3/admin/tag-library/{id} → 200 + 修改样例回显 ✓
DELETE /v3/admin/tag-library/{id} → 200 + true ✓
PUT /v3/admin/tag-library/sort → 200 + true ✓
PUT /v3/admin/tag-library/{id}/pin → 200 + true ✓
```
Swagger UI`http://<host>:8086/doc.html` → 找「[admin] 私人标签库管理v5.14)」分组。
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| #2045 | - | 首发6 接口空骨架 + Mock ServiceImpl + 错误码段位 581420-581429 | ✅ 有效 |
| #2240 | - | 修复 Gateway 缺 `/v3/admin/tag-library/**` 路由404 修复) | ✅ 有效 |
---
## 十、错误码
| 错误码 | 含义 | 触发场景 |
|--------|------|---------|
| 400 | 参数校验失败 | tagName 空 / 超长 / items 空 |
| 401 | 未登录 | 无 JWT 或 JWT 过期 |
| 581420 | 当前用户暂无标签库 | 保留位(当前实现不抛此错,空库返 `[]` |
| 581421 | 标签名已存在 | POST 时同用户已有同名标签 |
| 581422 | 标签名格式非法或长度超过 50 字符 | 含特殊字符 / 长度 > 50兜底校验 |
| 581423 | 无权操作该标签 | PUT/DELETE/PIN 操作他人的 id |
| 581424 | 标签不存在或已删除 | id 不存在 / 已软删 |
| 581425 | 排序列表含非本人标签 ID | sort 接口 items 混入他人 id |
---
## 十一、相关文档
- 关联 PR骨架[wx/HL#2045](https://git.1814.love:8443/wx/HL/pulls/2045)
- 关联 PR路由修复[wx/HL#2240](https://git.1814.love:8443/wx/HL/pulls/2240)
- 后续:真实 DB 业务实现 PR 合并后将追加一条 follow-up changelog契约不变,仅去掉 Mock 标注)

查看文件

@ -1,137 +0,0 @@
# 【修改接口·两端】Agency 模块路径切 v3
> **更新时间**: 2026-05-19
> **端类型**: 管理后台 + 小程序
> **关联**: HL Issue [#2561](https://git.1814.love:8443/wx/HL/issues/2561) / PR [#2562](https://git.1814.love:8443/wx/HL/pulls/2562)
> **背景**: Agency 域已迁移至 hl-order-service-v3,旧 v2 路径仍可用(雪藏期),但**前端必须切到新 /v3 路径以确保后续维护**。
---
## 0. 模块全貌
| 子模块 | 接口数 | 端 |
|---|---|---|
| §A admin 旅行社 CRUD | ~10 | 管理后台 |
| §B admin 支付配置 | ~5 | 管理后台 |
| §C admin 资质附件 | ~5 | 管理后台 |
| §D mp 公开视图 | ~3 | 小程序 |
---
## 1. 接口背景
Agency旅行社域承载主体公司 + 支付商户号 + 资质附件信息。之前由 v2 老订单服务(`hl-order-service`承载,PR #2556/#2558/#2562 完整迁到 v3`hl-order-service-v3`)。
迁移采用 **Option A 雪藏方案**v2 代码 / 表 / 旧路径 **全部保留**,但前端必须切到 v3 新路径,**新功能 / 维护只在 v3 进行**。
---
## 2. 变更清单(仅路径变更,参数 / 响应 / 错误码 / 业务逻辑 100% 不变)
### §A admin 旅行社 CRUD
| 旧 path | 新 path | 方法 |
|---|---|---|
| `/admin/travel-agency/page` | `/v3/admin/travel-agency/page` | GET |
| `/admin/travel-agency/{id}` | `/v3/admin/travel-agency/{id}` | GET / PUT / DELETE |
| `/admin/travel-agency` | `/v3/admin/travel-agency` | POST |
| `/admin/travel-agency/simple-list` | `/v3/admin/travel-agency/simple-list` | GET |
| `/admin/travel-agency/{id}/toggle-status` | `/v3/admin/travel-agency/{id}/toggle-status` | PUT |
| `/admin/travel-agency/{id}/visible` | `/v3/admin/travel-agency/{id}/visible` | PUT |
| ……(其他 admin 接口同规则,统一前缀替换) | ……同规则 | …… |
### §B admin 支付配置
| 旧 path | 新 path |
|---|---|
| `/admin/travel-agency/{agencyId}/payment/**` | `/v3/admin/travel-agency/{agencyId}/payment/**` |
### §C admin 资质附件
| 旧 path | 新 path |
|---|---|
| `/admin/travel-agency/{agencyId}/qualification/**` | `/v3/admin/travel-agency/{agencyId}/qualification/**` |
### §D mp 公开视图(小程序)
| 旧 path | 新 path |
|---|---|
| `/mp/agency/primary` | `/v3/mp/agency/primary` |
| `/mp/agency/**` | `/v3/mp/agency/**` |
---
## 3. 切换规则
**统一替换**
- admin: `/admin/travel-agency/` 前缀 → `/v3/admin/travel-agency/`
- mp: `/mp/agency/` 前缀 → `/v3/mp/agency/`
其他**完全不变**参数、响应字段、错误码、JWT 鉴权、业务逻辑、性能特征。
---
## 4. 入参 / 出参
无变化。所有接口的请求体 / 查询参数 / 响应 Result 包装 / 字段名 / 类型 / 可空性 / 枚举值 **完全保持原样**。详见 Knife4j重启后 v3 接口在 `http://<v3-host>:8086/doc.html`)。
---
## 5. 错误码
无变化。仍是 `582xxx` 段位Agency 错误码段),含义不变。
---
## 6. 修改前后对比(关键示例)
### 旧调用
```http
GET /admin/travel-agency/page?pageNum=1&pageSize=10
Authorization: Bearer {admin_jwt}
```
### 新调用
```http
GET /v3/admin/travel-agency/page?pageNum=1&pageSize=10
Authorization: Bearer {admin_jwt}
```
响应完全一样。
---
## 7. 影响评估 / 回滚
### 影响
- 前端必须把所有 Agency 域接口 base URL 加 `/v3` 前缀
- 短期内(雪藏期)旧 path 仍可用,但**不接受新写入**(数据在 v2 库冻结)
- 新功能(如新增 admin 字段)只在 v3 推
### 回滚
旧 v2 path 未删,前端可临时改回。Gateway 旧规则保留。但**建议尽快切完**,否则数据漂移风险。
---
## 8. 注意事项
1. **必须改的请求方**admin 管理后台 + 小程序 mp/agency 调用方
2. **不需要改**:内部 Feign 调用方mp-service 已通过 PR-2 内部切换,前端无感)
3. **切换时机**:建议本周内完成,避免错过新功能升级窗口
4. **测试方式**:本地 / 测试服并存期间,新旧 path 可同时调,对比响应一致
5. **gateway 重启**:后端已 merge,gateway 重启后新路由生效(运维通知)
---
## 9. 关联 / 联系人
- Issue: https://git.1814.love:8443/wx/HL/issues/2561
- PR: https://git.1814.love:8443/wx/HL/pulls/2562
- 前序 PRs: #2556PR-1 骨架)/ #2558PR-2 内部切换)
- 方案文档: `.claude/PRPs/2026-05-18-v2-to-v3-agency-migration-plan.md`
- 后端负责人: yst
- 切换问题反馈: 在 PR #2562 评论

查看文件

@ -1,562 +0,0 @@
# 【修改接口·管理后台】v3 行程 day/node 写操作真实化
> **更新时间**: 2026-05-19
> **端类型**: 管理后台
> **关联**: HL Issue [#2598](https://git.1814.love:8443/wx/HL/issues/2598) / PR [#2613](https://git.1814.love:8443/wx/HL/pulls/2613)
> **背景**: hl-order-service-v3 行程模块写接口从 Mock 替换为真实落库,7 个接口行为语义从假数据回显变为真实 DB 读写 + 审计日志。路径/方法/参数结构/响应结构全部不变。
---
## 1. 接口背景
行程编辑页管理后台包含天Day和节点Node两层叙事结构
- **天Day**:对应行程中的第 N 天,含标题、封面、描述、餐饮安排、集合解散地等叙事字段。
- **节点Node**:每天下的具体活动节点,含节点类型(景点/餐厅/活动/服务/自定义)、时间、资源关联等。
之前 6 个写接口均为 Mock返回硬编码 ID 或入参原样回显,不落库。本期PR #2613)全部替换为真实 DB 操作,同时新增编辑历史分页接口F7
**前端调用方式不变**——所有接口路径、方法、入参结构、出参结构与之前约定完全一致,变化的是后端行为从假数据变为真数据落库 + 审计。
---
## 2. 变更清单
| # | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|----------|------|
| F1 | POST | `/v3/admin/order/{orderId}/itinerary/days` | 行为变更 | Mock→真实dayNumber/dayDate 后端派生,真实落库 + 写 edit_log |
| F2 | PUT | `/v3/admin/order/{orderId}/itinerary/days/{dayId}` | 行为变更 | Mock→真实更新叙事字段,dayNumber/dayDate 不可改 |
| F3 | DELETE | `/v3/admin/order/{orderId}/itinerary/days/{dayId}` | 行为变更 | Mock→真实级联软删 nodes + activity/custom_assignment |
| F4 | POST | `/v3/admin/order/{orderId}/itinerary/nodes` | 行为变更 | Mock→真实ACTIVITY/CUSTOM 同事务 INSERT 实配,sortOrder 派生 |
| F5 | PUT | `/v3/admin/order/{orderId}/itinerary/nodes/{nodeId}` | 行为变更 | Mock→真实仅叙事字段,实配不动 |
| F6 | DELETE | `/v3/admin/order/{orderId}/itinerary/nodes/{nodeId}` | 行为变更 | Mock→真实软删节点,实配不动下一期补 |
| F7 | GET | `/v3/admin/order/{orderId}/itinerary/edit-log/page` | 行为变更 | Mock→真实6 字段筛选分页,真实查 order_itinerary_edit_log |
**破坏兼容?** 无。入参字段、出参字段名/类型全部不变。
---
## 3. 接口详情
所有接口:
- **服务**: hl-order-service-v3端口 8086,通过 Gateway :8080 路由)
- **认证**: JWT Bearer Token,Header `Authorization: Bearer {token}`,无效 token 返回 `code=401`
- **限流**: Gateway 全局限流,无接口级特殊限流
- **幂等性**: 写接口非幂等(每次调用均落库 + 写 edit_log;建议前端防重提交按钮 loading 态)
### F1 新增天
- **方法 + 路径**: `POST /v3/admin/order/{orderId}/itinerary/days`
- **描述**: 为指定订单新增一个行程天,dayNumber 由后端 MAX+1 派生
### F2 修改天
- **方法 + 路径**: `PUT /v3/admin/order/{orderId}/itinerary/days/{dayId}`
- **描述**: 修改指定行程天的叙事字段,dayNumber/dayDate 不可改
### F3 删除天
- **方法 + 路径**: `DELETE /v3/admin/order/{orderId}/itinerary/days/{dayId}`
- **描述**: 软删指定行程天,级联软删该天下所有节点及 activity/custom 实配
### F4 新增节点
- **方法 + 路径**: `POST /v3/admin/order/{orderId}/itinerary/nodes`
- **描述**: 在指定天下新增节点,sortOrder 由后端 MAX+1 派生;ACTIVITY/CUSTOM 类型需带 assignmentPayload
### F5 修改节点
- **方法 + 路径**: `PUT /v3/admin/order/{orderId}/itinerary/nodes/{nodeId}`
- **描述**: 修改节点叙事字段,不修改实配数据(下一期)
### F6 删除节点
- **方法 + 路径**: `DELETE /v3/admin/order/{orderId}/itinerary/nodes/{nodeId}`
- **描述**: 软删指定节点,不级联删除实配(与 deleteDay 的级联行为不同)
### F7 编辑历史分页
- **方法 + 路径**: `GET /v3/admin/order/{orderId}/itinerary/edit-log/page`
- **描述**: 分页查询订单行程编辑历史,支持 6 字段筛选
---
## 4. 接口入参
### 4.1 路径参数(所有接口)
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| orderId | LongString | 是 | 订单 ID,雪花 ID 以字符串形式传递 |
F2 / F3 额外:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| dayId | LongString | 是 | 天 ID |
F5 / F6 额外:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| nodeId | LongString | 是 | 节点 ID |
### 4.2 请求体字段
#### F1 新增天 / F2 修改天(请求体相同,均为 ItineraryDayUpsertReqVO
所有字段均为非必填:
| 字段 | 类型 | 校验 | 说明 |
|------|------|------|------|
| dayTitle | String | 长度 ≤128 | 当天标题 |
| quoteText | String | 长度 ≤256 | 金句/小诗 |
| coverImageUrl | String | 长度 ≤512 | 封面图 OSS URL |
| description | String | 长度 ≤4096 | 当日描述(支持富文本) |
| breakfast | String | 枚举见第 6 节 | 早餐安排 |
| lunch | String | 枚举见第 6 节 | 午餐安排 |
| dinner | String | 枚举见第 6 节 | 晚餐安排 |
| diningRemark | String | 长度 ≤256 | 餐饮备注 |
| gatherPlace | Object | — | 集合地点POI 对象,字段见下) |
| dismissalPlace | Object | — | 解散地点 |
| dailyMileage | Integer | 值 ≥0 | 当日里程km |
| dailyDuration | Integer | 值 ≥0 | 行驶时长(分钟) |
| mileageManualOverride | Boolean | — | 是否手动覆盖里程 |
| photoUrls | List\<String\> | — | 图集 URL 列表 |
| editReason | String | 长度 ≤256 | 修改原因(写入 edit_log |
注意F2 修改天): dayNumber 和 dayDate 后端管理,传了也不生效。
gatherPlace / dismissalPlace POI 对象结构(字段均可选):
```json
{name:国家会展中心,address:上海市青浦区崧泽大道,longitude:121.2987,latitude:31.1598}
```
当前响应中 gatherPlace/dismissalPlace 以 JSON 字符串形式返回(非 Object,下一期统一处理。
#### F3 删除天 / F6 删除节点(请求体)
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| editReason | String | 否 | 删除原因(写入 edit_log |
#### F4 新增节点 / F5 修改节点(请求体 ItineraryNodeUpsertReqVO
| 字段 | 类型 | 必填 | 校验 | 说明 |
|------|------|------|------|------|
| nodeName | String | 是F4/F5| @NotBlank,长度 ≤128 | 节点名称 |
| dayId | LongString | 是F4| — | 所属天 ID,F5 修改时无需传 |
| nodeType | String | 是F4 | 枚举见第 6 节 | 节点类型;F5 修改时不可改 |
| resourceType | String | 否 | 枚举见第 6 节 | 关联资源类型 |
| resourceId | LongString | 否 | — | 资源 ID;ACTIVITY 类型时后端校验 DB NOT NULL |
| startTime | String | 否 | HH:mm 格式 | 开始时间,如 "09:00" |
| timePeriod | String | 否 | — | 时段标签,如 "上午" |
| durationMinutes | Integer | 否 | 值 ≥0 | 持续时长(分钟) |
| description | String | 否 | 长度 ≤2000 | 节点描述 |
| images | List\<String\> | 否 | — | 图集 URL 列表 |
| emojiIcon | String | 否 | 长度 ≤8 | Emoji 图标 |
| assignmentPayload | Object | nodeType=ACTIVITY/CUSTOM 时必填 | 见下 | 实配载荷(新增节点时使用) |
| editReason | String | 否 | 长度 ≤256 | 修改原因(写入 edit_log |
assignmentPayload 对象结构NodeAssignmentPayload
| 字段 | 类型 | 必填 | 校验 | 说明 |
|------|------|------|------|------|
| unitPrice | BigDecimal | 是 | 值 ≥0 | 单价 |
| qty | Integer | 是 | 值 ≥1 | 数量 |
| costPrice | BigDecimal | 否 | — | 成本价 |
| supplierName | String | 否 | 长度 ≤128 | 供应商名称 |
| supplierContact | String | 否 | 长度 ≤64 | 供应商联系人 |
| supplierPhone | String | 否 | 长度 ≤32 | 供应商电话 |
totalAmount = unitPrice x qty,后端自动计算,不接受前端传入。
#### F7 编辑历史分页Query 参数)
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| pageNo | Integer | 是 | 页码,从 1 开始 |
| pageSize | Integer | 是 | 每页条数,建议 10-20 |
| editType | String | 否 | 枚举见第 6 节,单值筛选 |
| targetDay | Integer | 否 | 筛选指定天号(如 1、2、3 |
| operatorId | Long | 否 | 筛选指定操作人 ID |
| operatedFrom | String | 否 | 开始时间,**必须 ISO 格式** `2026-05-19T00:00:00` |
| operatedTo | String | 否 | 结束时间,**必须 ISO 格式** `2026-05-19T23:59:59` |
注意operatedFrom/operatedTo 必须使用 ISO 格式(含 T,空格格式2026-05-19 00:00:00会返回 400。
---
## 5. 出参字段
所有接口统一包装:
```json
{"code":200,"msg":"success","data":{}}
```
### F1/F2/F3 响应体ItineraryDayRespVO
| 字段 | 类型 | 说明 |
|------|------|------|
| id | String | 天 IDLong 序列化为 String,防 JS 精度丢失) |
| orderId | String | 订单 ID |
| dayNumber | Integer | 第几天(后端派生,从 1 开始) |
| dayDate | String | 当日日期,格式 yyyy-MM-dd |
| dayTitle | String | 标题 |
| quoteText | String | 金句 |
| coverImageUrl | String | 封面图 URL |
| description | String | 描述 |
| breakfast | String | 早餐枚举值 |
| lunch | String | 午餐枚举值 |
| dinner | String | 晚餐枚举值 |
| diningRemark | String | 餐饮备注 |
| gatherPlace | String | 集合地点(当前为 JSON 字符串,非 Object |
| dismissalPlace | String | 解散地点(同上) |
| dailyMileage | Integer | 当日里程 |
| dailyDuration | Integer | 行驶时长(分钟) |
| mileageManualOverride | Boolean | 是否手动覆盖里程 |
| photoUrls | List<String> | 图集 |
| editLogId | String | 本次操作产生的 edit_log IDLong→String |
| createTime | String | 创建时间 |
| updateTime | String | 更新时间 |
### F4/F5/F6 响应体ItineraryNodeRespVO
| 字段 | 类型 | 说明 |
|------|------|------|
| id | String | 节点 IDLong→String |
| orderId | String | 订单 ID |
| dayId | String | 所属天 IDLong→String |
| nodeType | String | 节点类型枚举值 |
| sortOrder | Integer | 排序权重后端派生,0-based max+1 |
| resourceType | String | 资源类型 |
| resourceId | String | 资源 IDLong→String |
| resourceName | String | 资源名称Feign 反查,SERVICE/CUSTOM/无 resourceId 时为 null |
| nodeName | String | 节点名称 |
| startTime | String | 开始时间HH:mm |
| timePeriod | String | 时段标签 |
| durationMinutes | Integer | 持续时长 |
| description | String | 描述 |
| images | List<String> | 图集 |
| emojiIcon | String | Emoji |
| refType | String | 实配关联类型(枚举见第 6 节) |
| refId | String | 实配 IDLong→String,无实配时 null |
| assignment | Object | 实配数据F4 ACTIVITY/CUSTOM 时有值,F5/F6 当前返回 null |
| editLogId | String | 本次操作产生的 edit_log IDLong→String |
| createTime | String | 创建时间 |
| updateTime | String | 更新时间 |
assignment 对象(仅 F4 ACTIVITY/CUSTOM 时非 null
| 字段 | 类型 | 说明 |
|------|------|------|
| id | String | 实配 ID |
| unitPrice | BigDecimal | 单价 |
| qty | Integer | 数量 |
| totalAmount | BigDecimal | 总额(后端派生) |
| costPrice | BigDecimal | 成本价 |
| supplierName | String | 供应商 |
| status | String | 实配状态,新增时为 PLANNED |
### F7 响应体(分页)
```json
{code:200,data:{records:[],total:9,page:1,pageSize:10}}
```
EditLogVO 字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| id | String | edit_log IDLong→String |
| orderId | String | 订单 ID |
| editType | String | 操作类型枚举值(见第 6 节) |
| targetDay | Integer | 操作涉及的天号 |
| beforeSnapshot | String | 操作前快照JSON 字符串) |
| afterSnapshot | String | 操作后快照JSON 字符串) |
| amountDelta | BigDecimal | 金额变化(叙事层固定为 0 |
| editReason | String | 操作原因 |
| scope | String | 固定为 ITINERARY |
| operatorId | String | 操作人 IDLong→String |
| operatorName | String | 操作人姓名 |
| operatedAt | String | 操作时间ISO 格式) |
| adjustmentId | String | 关联调整单 ID暂未支持,固定 null |
---
## 6. 枚举 / 数据字典
### 餐饮枚举breakfast / lunch / dinner
| 值 | 含义 |
|----|------|
| HOTEL | 酒店/旅馆餐 |
| CAMP | 营地餐 |
| SPECIAL | 特色餐 |
| SELF | 自理 |
### 节点类型nodeType
| 值 | 含义 |
|----|------|
| SCENIC | 景点游览 |
| RESTAURANT | 用餐 |
| ACTIVITY | 活动体验 |
| SERVICE | 服务项 |
| CUSTOM | 自定义 |
### 资源类型resourceType
| 值 | 含义 |
|----|------|
| SCENIC_SPOT | 景点 |
| RESTAURANT | 餐厅 |
| ACTIVITY | 活动 |
### 实配关联类型refType
| 值 | 含义 |
|----|------|
| NONE | 无实配 |
| SCENIC_ASSIGNMENT | 关联景点实配 |
| MEAL_ASSIGNMENT | 关联餐饮实配 |
| ACTIVITY_ASSIGNMENT | 关联活动实配 |
| CUSTOM_ASSIGNMENT | 关联自定义实配 |
| STAFF_ASSIGNMENT | 关联员工实配 |
### edit_log 操作类型editType
| 值 | 含义 |
|----|------|
| ADD_DAY | 新增天 |
| EDIT_DAY | 修改天 |
| REMOVE_DAY | 删除天 |
| ADD_NODE | 新增节点(本期新增枚举) |
| EDIT_NODE | 修改节点(本期新增枚举) |
| REMOVE_NODE | 删除节点(本期新增枚举) |
---
## 7. 错误码
| 错误码 | 含义 | 触发场景 |
|--------|------|----------|
| 583001 | ORDER_NOT_FOUND 订单不存在 | orderId 无对应记录 |
| 583050 | DAY_NOT_FOUND 行程天不存在或已删除 | dayId 不属于该订单,或已软删 |
| 583051 | NODE_NOT_FOUND 节点不存在或已删除 | nodeId 不属于该订单,或已软删 |
| 583052 | ASSIGNMENT_PAYLOAD_REQUIRED 实配载荷必填 | nodeType=ACTIVITY/CUSTOM 但未传 assignmentPayload |
| 583053 | INVALID_NODE_TYPE 节点类型非法 | nodeType 传了非枚举值 |
| 583022 | RESOURCE_NOT_FOUND 资源不存在 | resourceId 对应资源在 resource-service 查不到 |
| 401 | 未认证 / Token 无效 | 无 Authorization Header 或 Token 过期 |
---
## 8. 示例
### 8.1 典型成功 — 新增一天 + 新增节点
**Step 1新增天**
```bash
curl -X POST "http://localhost:8080/v3/admin/order/844477867747184641/itinerary/days" \n -H "Authorization: Bearer {token}" \n -H "Content-Type: application/json" \n -d '{"dayTitle":"第一天·布达拉宫","description":"抵达拉萨,参观布达拉宫","breakfast":"HOTEL","lunch":"SPECIAL","dinner":"SELF","editReason":"初次添加行程天"}'
```
响应:
```json
{
"code": 200,
"msg": "success",
"data": {
"id": "844800000000001001",
"orderId": "844477867747184641",
"dayNumber": 1,
"dayDate": "2026-06-01",
"dayTitle": "第一天·布达拉宫",
"breakfast": "HOTEL",
"lunch": "SPECIAL",
"dinner": "SELF",
"gatherPlace": null,
"editLogId": "844900000000000001",
"createTime": "2026-05-19T15:30:00"
}
}
```
说明第一次新增天,dayNumber=1;dayDate 根据订单出发日自动计算。
**Step 2新增景点节点**
```bash
curl -X POST "http://localhost:8080/v3/admin/order/844477867747184641/itinerary/nodes" \n -H "Authorization: Bearer {token}" \n -H "Content-Type: application/json" \n -d '{"dayId":"844800000000001001","nodeType":"SCENIC","nodeName":"布达拉宫","resourceType":"SCENIC_SPOT","resourceId":"700000000000001","startTime":"10:00","durationMinutes":180}'
```
响应:
```json
{
"code": 200,
"data": {
"id": "844800000000002001",
"dayId": "844800000000001001",
"nodeType": "SCENIC",
"sortOrder": 0,
"resourceName": "布达拉宫",
"nodeName": "布达拉宫",
"startTime": "10:00",
"durationMinutes": 180,
"refType": "NONE",
"refId": null,
"assignment": null,
"editLogId": "844900000000000002"
}
}
```
说明该天第一个节点,sortOrder=0;resourceName 由后端 Feign 反查。
### 8.2 边界情况 — 新增 ACTIVITY 节点(带实配载荷)
```bash
curl -X POST "http://localhost:8080/v3/admin/order/844477867747184641/itinerary/nodes" \n -H "Authorization: Bearer {token}" \n -H "Content-Type: application/json" \n -d '{"dayId":"844800000000001001","nodeType":"ACTIVITY","nodeName":"骑马体验","resourceType":"ACTIVITY","resourceId":"710000000000002","startTime":"14:00","durationMinutes":60,"assignmentPayload":{"unitPrice":200.00,"qty":4,"costPrice":150.00,"supplierName":"高原牧场体验馆","supplierContact":"王师傅","supplierPhone":"18800001234"}}'
```
响应(节点 + 活动实配同事务写入):
```json
{
"code": 200,
"data": {
"id": "844800000000002002",
"dayId": "844800000000001001",
"nodeType": "ACTIVITY",
"sortOrder": 1,
"resourceName": "高原骑马体验",
"nodeName": "骑马体验",
"refType": "ACTIVITY_ASSIGNMENT",
"refId": "860000000000000001",
"assignment": {"id":"860000000000000001","unitPrice":200.00,"qty":4,"totalAmount":800.00,"costPrice":150.00,"supplierName":"高原牧场体验馆","status":"PLANNED"},
"editLogId": "844900000000000003"
}
}
```
边界说明:第 2 个节点,sortOrder=1;totalAmount=200x4=800,后端派生;resourceName 由 Feign 反查不依赖前端传入。
### 8.3 业务失败 — ACTIVITY 节点未传 assignmentPayload
```bash
curl -X POST "http://localhost:8080/v3/admin/order/844477867747184641/itinerary/nodes" \n -H "Authorization: Bearer {token}" \n -H "Content-Type: application/json" \n -d '{"dayId":"844800000000001001","nodeType":"ACTIVITY","nodeName":"骑马体验"}'
```
响应:
```json
{"code":583052,"msg":"nodeType=ACTIVITY/CUSTOM 必须传 assignmentPayload","data":null}
```
---
## 9. 业务边界
### 适用场景
- 管理后台行程编辑页的新增天/删除天/编辑天/新增节点/修改节点/删除节点操作
- 编辑历史面板查看审计日志
### 不适用场景
- 整体行程保存batchSave,POST /itinerary/save——该接口仍为 Mock,下一期处理
- 小程序端行程查看MpItineraryService.getMpItinerary——只读接口,不涉及本期写操作
- staff/scenic/meal 类节点的实配写入——下一期
### 特殊边界
| 场景 | 行为 |
|------|------|
| dayNumber 派生 | 后端 MAX(day_number)+1,同订单第一次加天时 dayNumber=1;前端不传不接收 |
| dayDate 派生 | 订单出发日+(dayNumber-1) 天自动计算;F2 修改天不可改 dayNumber,因此 dayDate 也不变 |
| sortOrder 派生 | addNode 时 MAX(sort_order)+1,该天首个节点 sortOrder=0;前端不传 |
| resourceName 派生 | nodeType=SCENIC_SPOT/RESTAURANT/ACTIVITY 且有 resourceId 时 Feign 反查;nodeType=SERVICE/CUSTOM 或 resourceId=null 时跳过返回 null |
| deleteDay 级联范围 | 软删该天所有节点 + 节点关联的 activity_assignment / custom_assignment;scenic_assignment / meal_assignment 暂不级联(留 TODO |
| deleteNode 不级联 | 单独删节点不删实配(与 deleteDay 的级联不同),下一期补 |
| F5 editNode 实配不动 | 修改节点叙事字段时不同步修改 activity/custom_assignment,响应 assignment=null |
| 并发 addDay 防护 | 两人同时 addDay 触发 uk_order_day 唯一索引冲突时,后端 catch DuplicateKeyException 重试一次,仍失败返回业务错误码(不出 500 |
| 跨订单防护 | dayId/nodeId 校验 order_id 是否匹配,他人订单数据传进来返回 DAY_NOT_FOUND / NODE_NOT_FOUND |
| 已删除天再删 | 返回 DAY_NOT_FOUND583050,不重复软删 |
---
## 10. 修改前后对比
### 字段级对比(入参/出参结构不变)
| 维度 | 修改前Mock | 修改后(真实) |
|------|--------|--------|
| F1 addDay 返回 id | 硬编码 7700000000008 | 真实雪花 ID,String 类型 |
| F2 editDay 返回数据 | 原 dayId 原样回显 | 真实更新后 DB 记录,含 updateTime |
| F3 deleteDay 返回 | 伪 deleted: true | 返回删除前的 day 完整数据 + editLogId |
| F4 addNode 返回 id | 硬编码 7800000000088 | 真实雪花 ID,refType/refId 真实回填 |
| F5 editNode / F6 deleteNode | 入参原样回显 | 真实 DB 操作后记录 |
| F7 edit-log/page | 固定返回 3 条假 log | 真实查询 order_itinerary_edit_log |
| edit_log 是否存在 | 否Mock 不写 DB | 是,每次写操作同事务 1 行 |
### 行为级对比
| 操作 | 修改前 | 修改后 |
|------|--------|--------|
| 新增天两次 | 两次都返回同一 Mock ID | dayNumber 正确派生为 1、2 |
| 页面刷新后数据是否保留 | 否Mock 不落库) | 是(真实落库) |
| 删除天后节点是否消失 | 否 | 是(级联软删) |
| 编辑历史能否查到 | 否 | 是(每个写操作写 edit_log |
---
## 11. 影响评估 / 回滚
### 对前端的影响
- 接口路径/方法/入参结构/出参字段名完全不变,前端代码无需修改
- 行为变化:之前 Mock 数据刷新会丢失,现在真实落库;如果前端有基于 Mock 行为的临时兼容代码(如前端本地缓存假 ID 后续补偿),需检查是否有 workaround 要清理
### 前端需要验证的点
1. id / dayId / nodeId / editLogId / refId / operatorId 等所有 Long ID 字段均已序列化为 String,验证 JS 端是否正确处理(不要 parseInt
2. editNodeF5响应 assignment=null,如需展示实配数据需另调 /itinerary/full 接口
3. gatherPlace / dismissalPlace 当前响应为 JSON 字符串,需 JSON.parse() 后渲染(下一期后端统一改为 Object 返回)
4. F7 edit-log/page 日期筛选必须 ISO 格式2026-05-19T00:00:00,空格格式会 400
### 破坏性兼容
无破坏性兼容。
### 回滚方案
如需回滚,通过 Gitea 创建 revert PR,将 dev-v3 回到 PR #2613 合入前的状态。回滚不影响已落库的 order_itinerary_day / order_itinerary_node / order_itinerary_edit_log 数据(软删数据保留,不清库)。
---
## 12. 注意事项
1. **adjustmentId 暂未支持**EditLogVO.adjustmentId 字段保留但 DDL 无对应列,固定返回 null,下一期 adjustment 流程接入时补齐。
2. **gatherPlace / dismissalPlace 返回 JSON 字符串**:当前接口返回的是 JSON 字符串而非 Object,前端需 JSON.parse() 后再使用。下一期统一改为 Object 返回。
3. **batchSave 仍为 Mock**POST /v3/admin/order/{orderId}/itinerary/save 整体保存接口本期未真实化,仍返回 Mock 数据。
4. **staff/scenic/meal 实配 CRUD 仍 Mock**nodeType=SERVICE/CUSTOM 的节点写入已真实,但 staff、scenic、meal 类型相关实配的增删改仍 Mock,下一期补齐。
5. **F5 editNode 不同步实配**:修改节点叙事字段时不修改关联实配,响应 assignment=null。如需实配数据调 /v3/admin/order/{orderId}/itinerary/full。
6. **deleteNode 不级联实配**单独删节点F6不删关联的 activity/custom_assignment,与 deleteDayF3的级联行为不同,下一期补齐。
7. **resource-service 可用性**nodeType=SCENIC/RESTAURANT/ACTIVITY 且传了 resourceId 时,后端会 Feign 调 resource-service 反查资源名。若 resource-service 不可用,返回 RESOURCE_NOT_FOUND583022,写操作事务回滚。可通过不传 resourceId 规避resourceName 会为 null
8. **Long ID 全为 String**:所有 Long 类型 ID 均序列化为 String防 JS 精度丢失),前端赋值时不要做 parseInt。
---
## 13. 关联 / 联系人
- **Issue**: [#2598 v3 行程 day/node 写操作真实化](https://git.1814.love:8443/wx/HL/issues/2598)
- **PR**: [#2613](https://git.1814.love:8443/wx/HL/pulls/2613)
- **Commitsdev-v3 rebase 后)**:
- [12adf586](https://git.1814.love:8443/wx/HL/commit/12adf5861530f5af429d85aa38db9f801fd1d192) feat: Task 1-15 全量完成
- [38c93408](https://git.1814.love:8443/wx/HL/commit/38c93408c2ca80784cd31cc1e6ee92b724814c9a) fix: Phase 4 QA BUG+WARN 修复
- [d96c6575](https://git.1814.love:8443/wx/HL/commit/d96c657589838301739a47ab816bec2bbab9b042) fix: Phase 5 reviewer 必修项修复
- **后端负责人**: @yaosutu

查看文件

@ -1,121 +0,0 @@
# API 变更通知
**更新时间**: 2026-05-19 03:00
**PR**: #2569 feat(order-v3): admin 订单列表加 consultantName 模糊筛选
## ✨ 订单列表接口新增「定制师姓名」筛选参数
### 变了什么(前端视角)
管理后台订单列表接口 `GET /v3/admin/order` 新增一个**可选查询参数** `consultantName`,支持按定制师姓名模糊筛选订单。
传入后,接口会对数据库 `consultant_name` 列做 `LIKE %xxx%` 匹配;不传(或传 `null`/空字符串)则不过滤,行为与之前完全一致。
**对照表**
| 参数 | 原来 | 现在 |
|------|------|------|
| `consultantName` | 不存在,传了被忽略 | 支持,做 LIKE 模糊匹配 |
### 前端要改的地方
1. **订单列表顶部「定制师」筛选框**:将用户输入的定制师姓名作为 `consultantName` 参数拼到请求 query string 里,随其他已有筛选条件一起传给后端。
2. **清空筛选框时**:将 `consultantName` 置为 `undefined`(不传该字段)或传空字符串均可,后端两种情况都不过滤。
### 涉及的接口 / 模块
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 订单列表分页 | GET | `/v3/admin/order` | ✨ 新增请求参数 | 加 `consultantName` 可选筛选字段 |
### 接口详细定义
#### 订单列表分页
- **使用场景**:管理后台订单列表页,支持多条件组合筛选
- **请求参数(完整)**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| page | Integer | 是 | 页码,从 1 开始 |
| pageSize | Integer | 是 | 每页条数,建议 10 / 20 |
| keyword | String | 否 | 关键词模糊匹配(团号 / 客户姓名 / 产品名 / 订单号任一) |
| status | String | 否 | 订单状态枚举,见枚举表 |
| createSource | String | 否 | 创建来源枚举 |
| departureDateFrom | String | 否 | 出发日期起yyyy-MM-dd |
| departureDateTo | String | 否 | 出发日期止yyyy-MM-dd |
| cancelled | Boolean | 否 | 是否含已取消(默认 false |
| tagNames | List\<String\> | 否 | 按标签名过滤(多选) |
| **consultantName** | **String** | **否** | **定制师姓名模糊匹配LIKE %xxx%)。空/null 不过滤(本次新增)** |
- **请求示例(带定制师筛选)**
```
GET /v3/admin/order?page=1&pageSize=20&consultantName=李定制
Authorization: Bearer {token}
```
- **请求示例(组合筛选)**
```
GET /v3/admin/order?page=1&pageSize=20&status=CONFIRMED&consultantName=王
Authorization: Bearer {token}
```
- **响应示例(完整)**
```json
{
"code": 200,
"msg": "success",
"data": {
"records": [
{
"id": 1234567890,
"orderNo": "HL20260519001",
"status": "CONFIRMED",
"productName": "云南大理 7 日游",
"consultantName": "李定制",
"consultantId": 100001,
"customerName": "张三",
"departDate": "2026-06-01",
"headCount": 4,
"totalAmount": 12800.00,
"createTime": "2026-05-19T10:00:00"
}
],
"total": 5,
"page": 1,
"pageSize": 20
}
}
```
- **响应字段说明**(本次无变化,仅供参考):
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Long | 订单 ID |
| orderNo | String | 订单号 |
| status | String | 订单状态,见枚举表 |
| productName | String | 产品名称 |
| consultantName | String | 定制师姓名 |
| consultantId | Long | 定制师 ID |
| customerName | String | 客户姓名 |
| departDate | String | 出发日期yyyy-MM-dd |
| headCount | Integer | 出行人数 |
| totalAmount | BigDecimal | 订单总金额 |
| createTime | String | 创建时间ISO 8601 |
| total | Long | 满足条件的总记录数 |
| page | Integer | 当前页码 |
| pageSize | Integer | 每页条数 |
### 业务规则 / 校验规则
- `consultantName` 为空字符串或 null 时,SQL 不追加该条件,等价于不筛选
- 匹配方式:`LIKE %{consultantName}%`(前后都带通配符),输入"李"可匹配"李定制"、"王李明"等
- 筛选字段作用于订单表 `consultant_name` 列(存的是定制师的真实姓名,非企微名)
- 与其他筛选条件keyword、status、departureDateFrom 等)是 AND 关系,可自由组合
### 向后兼容性说明
- 本次变更**完全向后兼容**`consultantName` 为可选参数,不传则行为与改动前完全一致
- 响应结构 / 字段无任何变化
- 无需数据迁移,无需重启网关

查看文件

@ -1,167 +0,0 @@
# API 变更通知
**更新时间**: 2026-05-19 00:00
**PR**: #2571 fix(order-v3): 补通 overview.travelers 真实出行人数据Issue #2460 遗留债)
## ✨ 订单详情接口 overview.travelers 字段从占位空数组变为真实出行人列表
### 变了什么(前端视角)
`GET /v3/admin/order/{id}` 的响应中,`data.overview.travelers` 字段此前**永远返回空数组 `[]`**Issue #2460 PR-1 当时留了 TODO 占位)。
本次修复已将其**接通真实数据**,现在返回完整的出行人列表,字段口径与 `GET /v3/admin/order/{id}/traveler/list` 完全一致。
**对照表**
| 字段 | 原来 | 现在 |
|------|------|------|
| `data.overview.travelers` | 永远 `[]` | 真实出行人列表(按 traveler_id 升序) |
### 前端要改的地方
这是**可选优化**,不是必须改:
1. **进入订单详情页时**:可直接从 `overview.travelers` 读取出行人数据,省去再调一次 `/v3/admin/order/{id}/traveler/list`
2. **保持现有调用方式也完全没问题**`/traveler/list` 接口字段口径不变,两个来源数据一致。
3. **如果原先有 workaround**(比如检测到 `travelers` 为空就补一次 list 请求):确认接口已返回真实数据后可以清理掉这段逻辑。
### 涉及的接口 / 模块
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 订单详情 | GET | `/v3/admin/order/{id}` | 🔧 字段行为修复 | `overview.travelers` 从空数组变为真实数据 |
### 接口详细定义
#### 订单详情
- **使用场景**:管理后台进入订单详情页时调用,返回订单概览 + 出行人列表等聚合信息
- **路径参数**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | Long | 是 | 订单 ID |
- **请求示例**
```
GET /v3/admin/order/1234567890
Authorization: Bearer {token}
```
- **响应示例(完整 overview.travelers 部分)**
```json
{
"code": 200,
"msg": "success",
"data": {
"overview": {
"travelers": [
{
"id": 987654321,
"orderId": 1234567890,
"travelerType": "ADULT",
"name": "张三",
"gender": "MALE",
"birthday": "1990-06-15",
"idType": "IDCARD",
"idNo": "110101199006151234",
"nationality": "中国",
"race": "汉族",
"phone": "13800138000",
"emergencyContact": "李四",
"emergencyPhone": "13900139000",
"roomGroupNo": 1,
"profileStatus": "COMPLETED",
"transportPlanIds": [1001, 1002]
},
{
"id": 987654322,
"orderId": 1234567890,
"travelerType": "CHILD",
"name": "张小五",
"gender": "MALE",
"birthday": "2018-03-20",
"idType": "IDCARD",
"idNo": "110101201803201234",
"nationality": "中国",
"race": "汉族",
"phone": null,
"emergencyContact": "张三",
"emergencyPhone": "13800138000",
"roomGroupNo": 1,
"profileStatus": "COMPLETED",
"transportPlanIds": []
}
]
}
}
}
```
- **overview.travelers 数组元素字段说明**
| 字段 | 类型 | 说明 | 备注 |
|------|------|------|------|
| id | Long | 出行人 ID | 雪花 ID |
| orderId | Long | 所属订单 ID | |
| travelerType | String | 出行人类型 | 枚举,见下表 |
| name | String | 姓名 | |
| gender | String | 性别 | 枚举MALE / FEMALE |
| birthday | String | 生日 | 格式 yyyy-MM-dd |
| idType | String | 证件类型 | 枚举,见下表 |
| idNo | String | 证件号 | **admin 端明文返回**(后台 DB 加密存储)|
| nationality | String | 国籍 | 默认"中国" |
| race | String | 民族 | 默认"汉族" |
| phone | String | 手机号 | **admin 端明文返回**;儿童可为 null |
| emergencyContact | String | 紧急联系人姓名 | 可为 null |
| emergencyPhone | String | 紧急联系人手机 | **admin 端明文返回**;可为 null |
| roomGroupNo | Integer | 同住分组号 | 相同数字表示同住一间 |
| profileStatus | String | 资料完善状态 | 枚举,见下表 |
| transportPlanIds | List\<Long\> | 关联的大交通批次 ID 列表 | 无关联时为空数组 `[]` |
### 枚举 / 字典值
#### travelerType出行人类型
| 值 | 中文 | 说明 |
|----|------|------|
| ADULT | 成人 | |
| YOUNG | 青年 / 学生 | |
| CHILD | 儿童 | |
| BABY | 婴儿 | |
#### idType证件类型
| 值 | 中文 | 说明 |
|----|------|------|
| IDCARD | 居民身份证 | 最常见 |
| PASSPORT | 护照 | |
| HKMO | 港澳居民来往内地通行证 | |
| TAIWAN | 台湾居民来往大陆通行证 | |
| OTHER | 其他 | |
#### gender性别
| 值 | 中文 |
|----|------|
| MALE | 男 |
| FEMALE | 女 |
#### profileStatus资料状态
| 值 | 中文 | 说明 |
|----|------|------|
| PENDING | 待完善 | 出行人资料未填完整 |
| COMPLETED | 已完善 | 出行人资料齐全 |
### 业务规则 / 校验规则
- 排序:按出行人 ID`traveler_id`)升序
- 数据来源:与 `GET /v3/admin/order/{id}/traveler/list` **完全一致**,由同一个 `TravelerService.listTravelers` 方法产出,字段值不会有差异
- **敏感字段**`idNo``phone``emergencyPhone` 在 admin 端明文返回业务需要,例如紧急情况联系,在小程序端mp和内部接口internal走脱敏,后台 DB 使用 AES 加密存储
### 向后兼容性说明
- 本次变更**完全向后兼容**:字段名 / 路径 / 类型结构均无变化,只是原来恒为 `[]` 的字段现在有了真实内容
- 前端无需强制修改,保持调用 `/traveler/list` 的逻辑也能正常工作
- 若前端原有 workaround检测 `travelers` 为空时额外请求 list,现在可按需清理

查看文件

@ -1,236 +0,0 @@
# 大交通批次 edit/delete 严格化 follow-up: 错误码 581121 → 581144 + edit 4 项业务校验补齐
> **存放目录**: 二期(v3,`order-v3` 标签)→ `changelogs-v2/2026-05/`
>
> **服务**: hl-order-v3 (端口 8084)
> **PR**: #2560 (commit `08deec4d1`)
> **Issue**: 无新增工单(审查 follow-up,语义对齐 #2523 / #2524 / #2526,原工单已 closed)
> **日期**: 2026-05-19
> **影响范围**: 管理后台「行程安排 - 接送站」区块 - 大交通批次编辑/删除接口
---
## ⚠️ 关键变化(前端必同步两点)
1. **#2524 delete 错误码变更(破坏性,小幅)**:大交通批次软删接口的「plan 不存在/跨单/已删」错误码 **`581121``581144`**。前端若对 `581121` 做了特殊 catch,需改为 `581144`,或合并到通用错误处理。错误消息文案同步改为「大交通批次不存在或不属于该订单」。
2. **#2523 edit 严格化(行为收紧)**:之前 edit 接口只校验 `direction` / `transportType` 两个枚举,本次补齐 add 同款 4 项业务校验(581140 字段组合 / 581141 出行人归属 / 581142 时间顺序 / 581143 同方向同一出行人占用),且 edit 加 `@Idempotent(3s)`,3 秒内重复提交直接返 `100502 处理中`。前端 add 已有的错误处理代码可直接复用到 edit。
> 这是 **#2546(#2523) / #2547(#2524) / #2554(#2526)** 三个原 PR 合并后,/@arch + /@cr 审查发现的尾巴一次性收完,不开新工单。
---
## 一、背景
V5.48 §2.6 大交通模块四接口在 #2521/#2522/#2523/#2524 完成路径风格 + Method 迁移后,审查发现两处实质语义偏差和一处注释笔误:
| 维度 | 原 PR 状态 | 本 follow-up 修正后 |
|------|-----------|---------------------|
| `editTransportPlan` 业务校验 | 仅枚举 direction/transportType,**不校验字段组合/出行人归属/时间顺序/同方向占用** | 全 4 项校验,完全对齐 add |
| `editTransportPlan` 防并发 | 仅 `@Lock4j(30s)`,**无幂等** | `@Lock4j(30s)` + `@Idempotent(3s)` |
| `deleteTransportPlan` 错误码 | `581121`(沿用 §2 baseline) | **`581144`**(对齐 §2.6.4 文档 + #2523 命名) |
| `TravelerErrorCode` 错误码区间注释 | "581131-581135" 整体看作启用 | "581131-581134 启用 + 581135 预留" |
错误码 `581121` 是 §2 出行人模块的"归属校验"基线值,但 §2.6 大交通批次模块已在 add/edit 中使用 `581140-581144` 段位,delete 也应统一使用 `581144`,前端可用同一段位错误处理覆盖全部 §2.6 接口。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 大交通批次编辑 | POST | `/v3/admin/order/{id}/transport-plan/{planId}/edit` | 行为收紧 + 注解新增 | 补 4 项业务校验 + `@Idempotent(3s)` |
| 2 | 大交通批次软删 | POST | `/v3/admin/order/{id}/transport-plan/{planId}/delete` | 错误码变更(破坏性) | `581121``581144`,消息文案同步 |
---
## 三、接口详情
### 1. 大交通批次编辑 `POST /v3/admin/order/{id}/transport-plan/{planId}/edit`
入参 / 出参 / 路径 / Method **均与 PR #2546 一致**,仅以下行为变化:
#### 新增/补齐校验(对齐 add `POST .../transport-plan/add`)
校验顺序(从上到下,任一不通过立刻返错):
| 步骤 | 校验项 | 错误码 | 说明 |
|------|--------|--------|------|
| 1 | plan 存在 + 归属订单 + 未软删 | `581144` | (本 follow-up 改造点,见接口 2)|
| 2 | 订单存在性 | `581100` | |
| 3 | **字段组合合法性** | `581140` | FLIGHT/TRAIN/COACH 缺 `transportNo`,SELF_DRIVE 错带 `transportNo`/站点 等 |
| 4 | **时间顺序** | `581142` | `departTime > arriveTime` 拒绝(SELF_DRIVE 用 `selfDriveEta` 单点时间不触发此校验)|
| 5 | **travelerIds 归属订单** | `581141` | 任一 ID 不在 `order.travelerIds` → 整体拒绝 |
| 6 | **同方向同一出行人已被占用**(排除自己)| `581143` | 同 `direction` + 同 `travelerId` 在其他 active plan 中存在 → 拒绝;**自己的旧关联不算冲突** |
#### 新增注解
```java
@Idempotent(timeout = 3) // 新增:3 秒内重复 POST 同 path+Body 返首次结果
@Lock4j(keys = "#id", expire = 30000) // 已有
```
#### 错误响应示例
```json
// 字段组合非法(FLIGHT 缺 transportNo)
{ "code": 581140, "msg": "transportNo 必填(运输方式=FLIGHT)", "data": null }
// travelerIds 含订单外的出行人
{ "code": 581141, "msg": "出行人不属于该订单: travelerIds=[70123456789099]", "data": null }
// 时间顺序错误
{ "code": 581142, "msg": "departTime 不能晚于 arriveTime", "data": null }
// 同方向同 traveler 占用(其他 plan)
{ "code": 581143, "msg": "出行人在同方向已有另一批次: direction=ARRIVAL, travelerId=70123456789012, conflictPlanId=80012999", "data": null }
// 幂等命中(3 秒内重复 POST)
{ "code": 100502, "msg": "请求处理中,请稍后重试", "data": null }
```
> **edit 排除自身规则**:校验 `581143` 时,SQL `WHERE plan_id != #{currentPlanId}`,改自己不算冲突。该排除是 edit 接口独有,add 接口不存在 currentPlanId 概念。
---
### 2. 大交通批次软删 `POST /v3/admin/order/{id}/transport-plan/{planId}/delete`
入参 / 出参 / 路径 / Method / 数据库行为 **均与 PR #2547 一致**,仅错误码变化:
#### 错误响应变更
| 场景 | 变更前(PR #2547) | 变更后(PR #2560,**当前**) |
|------|------------------|---------------------------|
| plan 不存在 | `581121` "大交通批次不属于该订单" | **`581144` "大交通批次不存在或不属于该订单"** |
| plan 跨单(orderId 不匹配) | `581121` 同上 | **`581144`** 同上 |
| plan 已软删(deleted=1)再 delete | `581121` 同上 | **`581144`** 同上 |
| 订单不存在 | `581100` | `581100`(**不变**)|
示例响应:
```json
{ "code": 581144, "msg": "大交通批次不存在或不属于该订单: planId=80099999, orderId=60123456789012", "data": null }
```
> `581121` 在 §2.6 模块**不再返回**(仍保留在 §2 出行人模块的其他接口中)。
---
## 四、契约约束与正确调用方式
### edit 接口(前端 actionable)
| 场景 | payload | 结果 |
|------|---------|------|
| ✅ 合法 edit | FLIGHT + transportNo + 站点 + travelerIds 都属本单 + 时间顺序对 | 200 |
| ✅ 改自己旧关联的 traveler | edit Body `travelerIds=[A,B]`,A 在原 plan 已有 → 不触发 581143 | 200 |
| ❌ 缺 transportNo(FLIGHT) | `{"transportType":"FLIGHT","transportNo":null}` | `581140` |
| ❌ 时间倒挂 | `departTime=11:00, arriveTime=09:00` | `581142` |
| ❌ traveler 跨单 | `travelerIds=[订单外的 ID]` | `581141` |
| ❌ 同方向 A 已在别 plan | direction=ARRIVAL + travelerA 在 plan #B 占用 | `581143` |
| ❌ 3 秒内双击 | 第二次同 path+Body POST | `100502` 处理中 |
### delete 接口(前端 actionable)
| 错误码 | 前端建议 |
|--------|---------|
| 旧 `581121` | **移除**或合并到通用错误处理 |
| 新 `581144` | 提示「该批次不存在或已被删除」,刷新列表 |
### 前端代码迁移建议
```js
// 旧
if (resp.code === 581121) showToast('批次不属于该订单');
// 新(推荐合并 581121 + 581144,兼容过渡期)
if (resp.code === 581144 || resp.code === 581121) showToast('批次不存在或已被删除');
```
---
## 五、数据库行为
**无变化**。本 follow-up 仅修改 Service 层校验顺序 + Controller 注解 + 错误码常量值,DB schema / 表行为完全沿用 PR #2546 / #2547
---
## 六、边界行为
### edit 接口
- 新校验全部在事务开始前完成,失败不写库
- `@Idempotent(3s)` 缓存键 = path + 用户 ID + Body hash,3 秒内重复同 Body 直接拿首次结果
- 同方向同 traveler 占用判定 SQL: `WHERE direction = ? AND traveler_id IN (?) AND plan_id != #{currentPlanId} AND deleted = 0`
- 已软删的 plan 不参与 `581143` 占用判定
### delete 接口
- `@Idempotent(3s)` + `@Lock4j(30s)` 沿用,不变
- 错误码常量从 `TRANSPORT_PLAN_NOT_BELONG_TO_ORDER(581121)` 改为 `TRANSPORT_PLAN_NOT_FOUND_OR_NOT_BELONG(581144)`
### 通用
- 未登录 → 401(网关拦截,不变)
- 历史 plan(v2 迁移数据)edit/delete 行为完全一致
---
## 七、不影响范围
- **仅影响**: 管理后台 F21 「接送站」编辑弹窗 + 删除按钮
- **零影响**:
- C 端订单详情 / mp 抵达计划接口
- §2.6.1 list 接口 / §2.6.2 add 接口(本 follow-up 不动)
- 出行人 CRUD(traveler 表本身不动)
- 订单状态机
- `581121` 在 §2 出行人模块其他接口仍正常返回,语义未变
- `581131-581134` 出行人模块错误码无功能变化,仅注释笔误修正
---
## 八、测试环境已验证
测试服 9443 网关 + 真 admin token round-trip **9/9 PASS**(/@qa 报告):
```
edit 接口严格化:
✓ FLIGHT 缺 transportNo → 581140
✓ travelerIds 含订单外 ID → 581141
✓ departTime > arriveTime → 581142
✓ 同方向 traveler 在别 plan 占用 → 581143
✓ 同方向 traveler 在自己 plan(改自己)→ 200(排除自身生效)
✓ 3 秒内重复 POST 同 Body → 100502 幂等命中
delete 接口错误码:
✓ planId 不存在 → 581144(原 581121)
✓ planId 跨单 → 581144(原 581121)
✓ 重复 delete 已软删 plan → 581144(原 581121)
```
本地单测 109/109 全绿(5 单测 + 7 IT,注解反射断言验证 `@Idempotent.timeout=3` / `@Lock4j.keys="#id"`)。
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| #2546 | #2523 | edit 接口 Method+路径迁移(原版只校验 2 枚举) | ✅ 仍有效,**本 follow-up 在其上补 4 校验 + 幂等** |
| #2547 | #2524 | delete 接口 Method+路径迁移(原版用 `581121`) | ✅ 仍有效,**本 follow-up 改错误码 → `581144`** |
| #2554 | #2526 | 出行人 smart-parse 新增接口(原版注释笔误)| ✅ 仍有效,**本 follow-up 仅修注释,无 API 变化** |
| **本 PR #2560** | (审查 follow-up,无新工单) | edit 严格化 + delete 错误码 + 注释笔误 | ✅ **最新** |
---
## 十、相关文档
- 关联 PR: [wx/HL#2560](https://git.1814.love:8443/wx/HL/pulls/2560)
- 原 PR(本 follow-up 的修正基础):
- [wx/HL#2546](https://git.1814.love:8443/wx/HL/pulls/2546)(关联 [#2523](https://git.1814.love:8443/wx/HL/issues/2523))
- [wx/HL#2547](https://git.1814.love:8443/wx/HL/pulls/2547)(关联 [#2524](https://git.1814.love:8443/wx/HL/issues/2524))
- [wx/HL#2554](https://git.1814.love:8443/wx/HL/pulls/2554)(关联 [#2526](https://git.1814.love:8443/wx/HL/issues/2526))
- API 文档: `docs/order-v3/api/API-SPEC-V5.48.html` §2.6.3(edit)/ §2.6.4(delete)
- 同月配套 changelog:
- `18_#2523_transport-plan-edit.md`
- `18_#2524_transport-plan-delete.md`
- `18_#2526_traveler-smart-parse.md`

查看文件

@ -1,676 +0,0 @@
# 【新增接口·管理后台】车型管理库 9 接口(含新增的大类/型号分页 2 接口) (#2785)
> **PR**: #2786 | **服务**: hl-fleet-service | **更新时间**: 2026-05-21 14:25
>
> **存放目录**: `changelogs-v2/2026-05/`(前端 v3 项目仓库读取)
> **影响范围**: 管理后台「车管 / 车型管理库」页(左侧大类导航 + 右侧型号列表 + 全模块 CRUD
---
## ⚠️ 关键变化30 秒速读)
**本次实质变化是 2 个新增分页接口**§3.2 大类分页 / §3.7 型号分页),原 7 个 CRUD/树接口契约零变化。一并把整模块 9 接口契约打包发给前端,方便对接「左侧大类导航 + 右侧型号列表」联动场景。
| # | 接口 | 状态 | 用途 |
|---|---|---|---|
| §3.1 | GET `/admin/fleet/vehicle-types` | 已存在·未变 | 一次拉全模块树(大类+型号) |
| **§3.2** | **GET `/admin/fleet/vehicle-types/page`** | **✨ 新增** | 大类分页+过滤(左侧导航专用) |
| §3.3 | POST `/admin/fleet/vehicle-types` | 已存在·未变 | 新增大类 |
| §3.4 | PUT `/admin/fleet/vehicle-types/{typeId}` | 已存在·未变 | 编辑大类 |
| §3.5 | DELETE `/admin/fleet/vehicle-types/{typeId}` | 已存在·未变 | 删除大类(软删) |
| §3.6 | POST `/admin/fleet/vehicle-types/{typeId}/models` | 已存在·未变 | 新增型号 |
| **§3.7** | **GET `/admin/fleet/vehicle-types/models/page`** | **✨ 新增** | 型号分页+过滤vehicleTypeId 可选) |
| §3.8 | PUT `/admin/fleet/vehicle-types/models/{modelId}` | 已存在·未变 | 编辑型号 |
| §3.9 | DELETE `/admin/fleet/vehicle-types/models/{modelId}` | 已存在·未变 | 删除型号(软删) |
---
## 1. 接口背景
车型管理库是车队基础字典:大类(如 SUV/MPV/BUS/Sedan→ 型号(如 丰田汉兰达/本田奥德赛)。管理后台需要:
1. **左侧大类导航 + 右侧型号列表**联动 → 大类分页 + 型号按 typeId 过滤的分页
2. **跨大类按车型名搜索** → 型号分页 typeId 可选 + modelName 模糊
3. 已有的 listTree 一次拉树仍保留(其他场景如派单弹窗筛选 / 价格日历分组继续用)
---
## 2. 变更清单
| # | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|----------|------|
| 1 | GET | `/admin/fleet/vehicle-types` | 未变 | 列出大类+型号整树 |
| 2 | GET | `/admin/fleet/vehicle-types/page` | **新增** | 大类分页 |
| 3 | POST | `/admin/fleet/vehicle-types` | 未变 | 新增大类 |
| 4 | PUT | `/admin/fleet/vehicle-types/{typeId}` | 未变 | 编辑大类typeKey 不可改) |
| 5 | DELETE | `/admin/fleet/vehicle-types/{typeId}` | 未变 | 删除大类(软删) |
| 6 | POST | `/admin/fleet/vehicle-types/{typeId}/models` | 未变 | 新增型号 |
| 7 | GET | `/admin/fleet/vehicle-types/models/page` | **新增** | 型号分页 |
| 8 | PUT | `/admin/fleet/vehicle-types/models/{modelId}` | 未变 | 编辑型号 |
| 9 | DELETE | `/admin/fleet/vehicle-types/models/{modelId}` | 未变 | 删除型号(软删) |
---
## 3. 接口详情
### 3.1 列出大类 + 型号树
**GET** `/admin/fleet/vehicle-types`
- **使用场景**:一次拉取整棵车型树(大类含 models 数组),用于派单弹窗、价格日历分组等需要全量树的场景
- **认证**:管理后台 JWT
- **幂等**:是(只读)
- **入参**:无
**出参**`Result<List<VehicleTypeTreeRespVO>>`
`sort_order` 升序。每个大类含 `models` 数组(同大类内按 `sort_order` 升序)。无大类时返回空数组 `[]`
**典型示例 请求**
```
GET /admin/fleet/vehicle-types
Authorization: Bearer <token>
(无请求体)
```
**典型示例 响应**
```json
{
"code": 200,
"data": [
{
"id": "1234567890123456789",
"typeKey": "suv",
"typeName": "SUV 越野",
"icon": "🚙",
"description": "适合山地越野、多人出行",
"sortOrder": 1,
"models": [
{
"id": "9876543210987654321",
"vehicleTypeId": "1234567890123456789",
"modelName": "丰田汉兰达",
"seats": 7,
"basePrice": "800.00",
"alias": "汉兰达,HIGHLANDER",
"sortOrder": 1
}
]
}
],
"msg": "成功"
}
```
**错误码**:仅 401 未登录
---
### 3.2 大类分页列表 ✨ 新增
**GET** `/admin/fleet/vehicle-types/page`
- **使用场景**管理后台「车型管理库」左侧大类导航的分页查询,支持名称模糊、key 精确过滤
- **认证**:管理后台 JWT
- **幂等**:是(只读)
**Query 入参**
| 字段 | 类型 | 必填 | 默认 | 校验 | 说明 |
|------|------|------|------|------|------|
| `page` | int | ❌ | 1 | `>= 1` | 页码 |
| `pageSize` | int | ❌ | 20 | `1-100` | 每页条数 |
| `typeName` | string | ❌ | — | — | 大类中文名模糊匹配 |
| `typeKey` | string | ❌ | — | — | 大类 key 精确匹配 |
**出参**`Result<PageResult<VehicleTypeRespVO>>`
`sort_order` 升序。**不**含 `models` 数组(需要型号请调 §3.7)。
| 字段 | 类型 | 说明 |
|------|------|------|
| `records[].id` | string | 大类 IDLong 序列化为 String 防精度丢失) |
| `records[].typeKey` | string | 大类 key如 suv/mpv/bus/sedan |
| `records[].typeName` | string | 大类中文名 |
| `records[].icon` | string | emoji 图标(可空) |
| `records[].description` | string | 描述(可空) |
| `records[].sortOrder` | int | 排序权重 |
| `total` | int | 总记录数 |
| `page` | int | 当前页码 |
| `pageSize` | int | 每页条数 |
**典型示例 请求**
```
GET /admin/fleet/vehicle-types/page?page=1&pageSize=20&typeName=SUV
Authorization: Bearer <token>
(无请求体)
```
**典型示例 响应**
```json
{
"code": 200,
"data": {
"records": [
{
"id": "1234567890123456789",
"typeKey": "suv",
"typeName": "SUV 越野",
"icon": "🚙",
"description": "适合山地越野、多人出行",
"sortOrder": 1
}
],
"total": 1,
"page": 1,
"pageSize": 20
},
"msg": "成功"
}
```
**边界 请求**无过滤拿全部,pageSize=100 极限):
```
GET /admin/fleet/vehicle-types/page?page=1&pageSize=100
```
**异常 请求**pageSize=101 超限):
```
GET /admin/fleet/vehicle-types/page?page=1&pageSize=101
```
响应:
```json
{ "code": 400, "msg": "每页条数最大为100", "data": null }
```
**错误码**400 参数校验 / 401 未登录
---
### 3.3 新增大类
**POST** `/admin/fleet/vehicle-types`
- **使用场景**:扩展车型大类(初始化已灌 4 条 suv/mpv/bus/sedan,后续如新增小型客车等
- **认证**:管理后台 JWT
- **幂等**:否(重复调用会触发 600101 重复)
**Body 入参**`VehicleTypeSaveReqVO`
| 字段 | 类型 | 必填 | 校验 | 说明 |
|------|------|------|------|------|
| `typeKey` | string | ✅ | `@Size(max=16)` | 大类 key全局唯一 |
| `typeName` | string | ✅ | `@Size(max=64)` | 大类中文名 |
| `icon` | string | ❌ | `@Size(max=8)` | emoji 图标(可为空) |
| `description` | string | ❌ | `@Size(max=256)` | 描述(可为空) |
| `sortOrder` | int | ✅ | — | 排序权重(升序) |
**出参**`Result<VehicleTypeRespVO>`(同 §3.2 records 项字段)
**典型示例 请求**
```
POST /admin/fleet/vehicle-types
Authorization: Bearer <token>
Content-Type: application/json
{
"typeKey": "minibus",
"typeName": "小型客车",
"icon": "🚐",
"description": "10-19 座中巴",
"sortOrder": 5
}
```
**典型示例 响应**
```json
{
"code": 200,
"data": {
"id": "1234567890123456789",
"typeKey": "minibus",
"typeName": "小型客车",
"icon": "🚐",
"description": "10-19 座中巴",
"sortOrder": 5
},
"msg": "成功"
}
```
**异常 请求**typeKey 重复):
```json
{ "typeKey": "suv", "typeName": "重复 SUV", "sortOrder": 99 }
```
响应:
```json
{ "code": 600101, "msg": "车型大类 key 已存在", "data": null }
```
**错误码**400 参数校验 / **600101** typeKey 已存在 / 401 未登录
---
### 3.4 编辑大类
**PUT** `/admin/fleet/vehicle-types/{typeId}`
- **使用场景**:修改大类的 typeName / icon / description / sortOrder。**typeKey 不可改**(后端忽略入参中的 typeKey 字段)
- **认证**:管理后台 JWT
- **幂等**:是(相同 body 重复调结果一致)
**路径入参**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `typeId` | Long | ✅ | 大类 ID |
**Body 入参**:同 §3.3typeKey 字段后端忽略;其他字段同新增)
**出参**`Result<VehicleTypeRespVO>`(更新后的大类)
**典型示例 请求**
```
PUT /admin/fleet/vehicle-types/1234567890123456789
Authorization: Bearer <token>
Content-Type: application/json
{
"typeKey": "suv",
"typeName": "SUV 越野 (改名)",
"icon": "🚙",
"description": "新描述",
"sortOrder": 2
}
```
**典型示例 响应**
```json
{
"code": 200,
"data": {
"id": "1234567890123456789",
"typeKey": "suv",
"typeName": "SUV 越野 (改名)",
"icon": "🚙",
"description": "新描述",
"sortOrder": 2
},
"msg": "成功"
}
```
**异常 请求**typeId 不存在):
```json
{ "code": 600102, "msg": "车型大类不存在", "data": null }
```
**错误码**400 参数校验 / **600102** 大类不存在 / 401 未登录
---
### 3.5 删除大类(软删)
**DELETE** `/admin/fleet/vehicle-types/{typeId}`
- **使用场景**:下架某个大类。**大类下存在型号时禁止删除**(应用层校验)
- **认证**:管理后台 JWT
- **幂等**:是(重复删返同样结果)
**路径入参**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `typeId` | Long | ✅ | 大类 ID |
**出参**`Result<Void>`(成功时 `data: null`
**典型示例 请求**
```
DELETE /admin/fleet/vehicle-types/1234567890123456789
Authorization: Bearer <token>
(无请求体)
```
**典型示例 响应**
```json
{ "code": 200, "data": null, "msg": "成功" }
```
**异常 请求**(大类下有型号):
```json
{ "code": 600104, "msg": "大类下存在型号,不能删除", "data": null }
```
**错误码****600102** 大类不存在 / **600104** 大类下存在型号 / 401 未登录
---
### 3.6 新增型号(挂在指定大类下)
**POST** `/admin/fleet/vehicle-types/{typeId}/models`
- **使用场景**:在某大类下新增型号(如 SUV 大类下加"路虎揽胜"
- **认证**:管理后台 JWT
- **幂等**:否(同名重复抛 600103
**路径入参**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `typeId` | Long | ✅ | 所属大类 ID |
**Body 入参**`VehicleModelSaveReqVO`
| 字段 | 类型 | 必填 | 校验 | 说明 |
|------|------|------|------|------|
| `modelName` | string | ✅ | `@Size(max=64)` | 型号名(同大类内唯一) |
| `seats` | int | ✅ | `@Min(4) @Max(19)` | 座位数 4-19 |
| `basePrice` | BigDecimal | ✅ | `@DecimalMin("0")` | 基础日单价(¥,≥ 0 |
| `alias` | string | ❌ | `@Size(max=256)` | 别名(搜索匹配用) |
| `sortOrder` | int | ✅ | — | 排序权重 |
**出参**`Result<VehicleModelRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string | 型号 IDLong 序列化为 String |
| `vehicleTypeId` | string | 所属大类 ID |
| `modelName` | string | 型号名 |
| `seats` | int | 座位数 |
| `basePrice` | string | 基础日单价BigDecimal 序列化) |
| `alias` | string | 别名 |
| `sortOrder` | int | 排序 |
**典型示例 请求**
```
POST /admin/fleet/vehicle-types/1234567890123456789/models
Authorization: Bearer <token>
Content-Type: application/json
{
"modelName": "丰田汉兰达",
"seats": 7,
"basePrice": "800.00",
"alias": "汉兰达,HIGHLANDER",
"sortOrder": 1
}
```
**典型示例 响应**
```json
{
"code": 200,
"data": {
"id": "9876543210987654321",
"vehicleTypeId": "1234567890123456789",
"modelName": "丰田汉兰达",
"seats": 7,
"basePrice": "800.00",
"alias": "汉兰达,HIGHLANDER",
"sortOrder": 1
},
"msg": "成功"
}
```
**异常 请求**(座位数 20 超限):
```json
{ "modelName": "大巴车", "seats": 20, "basePrice": "1500.00", "sortOrder": 1 }
```
响应:
```json
{ "code": 400, "msg": "座位数最大为 19", "data": null }
```
**错误码**400 参数校验 / **600102** 大类不存在 / **600103** 型号名已存在 / 401 未登录
---
### 3.7 型号分页列表 ✨ 新增
**GET** `/admin/fleet/vehicle-types/models/page`
- **使用场景**
1. 「左侧点击大类 → 右侧加载该大类型号」 → 传 `vehicleTypeId`
2. 「跨大类按车型名/俗称搜索」 → 不传 `vehicleTypeId`,传 `modelName``alias`
- **认证**:管理后台 JWT
- **幂等**:是(只读)
**Query 入参**
| 字段 | 类型 | 必填 | 默认 | 校验 | 说明 |
|------|------|------|------|------|------|
| `page` | int | ❌ | 1 | `>= 1` | 页码 |
| `pageSize` | int | ❌ | 20 | `1-100` | 每页条数 |
| `vehicleTypeId` | Long | ❌ | — | — | **可选**:传则按大类过滤,不传跨大类全量 |
| `modelName` | string | ❌ | — | — | 型号名模糊匹配 |
| `seats` | int | ❌ | — | — | 座位数精确匹配 |
| `alias` | string | ❌ | — | — | 别名模糊匹配(车型俗称) |
**出参**`Result<PageResult<VehicleModelRespVO>>`
`sort_order` 升序。**不**返大类名称(前端从左侧大类列表自取)。
records 项字段同 §3.6 出参字段表。
**典型示例 请求**(按大类查):
```
GET /admin/fleet/vehicle-types/models/page?page=1&pageSize=20&vehicleTypeId=1234567890123456789
Authorization: Bearer <token>
(无请求体)
```
**典型示例 响应**
```json
{
"code": 200,
"data": {
"records": [
{
"id": "9876543210987654321",
"vehicleTypeId": "1234567890123456789",
"modelName": "丰田汉兰达",
"seats": 7,
"basePrice": "800.00",
"alias": "汉兰达,HIGHLANDER",
"sortOrder": 1
}
],
"total": 1,
"page": 1,
"pageSize": 20
},
"msg": "成功"
}
```
**边界 请求**(跨大类按 modelName 搜"丰田"
```
GET /admin/fleet/vehicle-types/models/page?modelName=丰田
```
**边界 请求**(按 seats=7 精确 + alias 模糊组合):
```
GET /admin/fleet/vehicle-types/models/page?seats=7&alias=ODYSSEY
```
**异常 请求**page=0 不合法):
```
GET /admin/fleet/vehicle-types/models/page?page=0&pageSize=20
```
响应:
```json
{ "code": 400, "msg": "页码最小为1", "data": null }
```
**错误码**400 参数校验 / 401 未登录
---
### 3.8 编辑型号
**PUT** `/admin/fleet/vehicle-types/models/{modelId}`
- **使用场景**:修改型号的 modelName / seats / basePrice / alias / sortOrder
- **认证**:管理后台 JWT
- **幂等**:是
**路径入参**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `modelId` | Long | ✅ | 型号 ID |
**Body 入参**:同 §3.6(不含 typeId,型号所属大类不可改
**出参**`Result<VehicleModelRespVO>`
**典型示例 请求**
```
PUT /admin/fleet/vehicle-types/models/9876543210987654321
Authorization: Bearer <token>
Content-Type: application/json
{
"modelName": "丰田汉兰达 (2025 款)",
"seats": 7,
"basePrice": "900.00",
"alias": "汉兰达,HIGHLANDER",
"sortOrder": 1
}
```
**异常 请求**(型号名同大类内已被占用):
```json
{ "code": 600103, "msg": "车型型号已存在", "data": null }
```
**错误码**400 参数校验 / **600103** 型号名已存在 / **600106** 型号不存在 / 401 未登录
---
### 3.9 删除型号(软删)
**DELETE** `/admin/fleet/vehicle-types/models/{modelId}`
- **使用场景**:下架某个型号。**被车辆引用时禁止删除**(应用层校验)
- **认证**:管理后台 JWT
- **幂等**:是
**路径入参**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `modelId` | Long | ✅ | 型号 ID |
**出参**`Result<Void>`
**典型示例 请求**
```
DELETE /admin/fleet/vehicle-types/models/9876543210987654321
Authorization: Bearer <token>
(无请求体)
```
**典型示例 响应**
```json
{ "code": 200, "data": null, "msg": "成功" }
```
**异常 请求**(型号被车辆引用):
```json
{ "code": 600105, "msg": "型号被车辆引用,不能删除", "data": null }
```
**错误码****600105** 型号被车辆引用 / **600106** 型号不存在 / 401 未登录
---
## 6. 枚举 / 数据字典
本模块**无静态枚举 / 字典**
- `typeKey``fleet_vehicle_type` 表的一个 `VARCHAR(16)` 字段(行级数据),不是后端枚举常量,**没有字典表**。系统初始化灌入了 4 条标准记录(详见 §9 业务边界),但 typeKey 可通过 §3.3 自由扩展
- `seats` 是数值范围4-19,`@Min` / `@Max` 校验),不是枚举
- 其他字段modelName / basePrice / alias 等)均为自由文本/数字
---
## 7. 错误码(全模块汇总)
| code | 含义 | 触发条件 | 涉及接口 |
|------|------|----------|----------|
| 400 | 参数校验失败 | 字段缺失 / 长度 / 范围越界 | 所有写入接口 + 分页接口 |
| 401 | 未登录 | JWT 无效或缺失 | 全部 9 接口 |
| **600101** | 车型大类 key 已存在 | typeKey 全局唯一约束 | §3.3 新增大类 |
| **600102** | 车型大类不存在 | typeId 无效或已软删 | §3.4 §3.5 §3.6 |
| **600103** | 车型型号已存在 | 同大类内型号名唯一约束 | §3.6 §3.8 |
| **600104** | 大类下存在型号,不能删除 | 应用层校验 | §3.5 |
| **600105** | 型号被车辆引用,不能删除 | 应用层校验 | §3.9 |
| **600106** | 车型型号不存在 | modelId 无效或已软删 | §3.8 §3.9 |
> 错误码段位 600101-600106 归属 hl-fleet-service,本次未新增段位。
---
## 9. 业务边界
- ✅ **大类 typeKey 编辑**:编辑大类时后端**忽略**入参的 typeKey 字段(不改),其他字段正常更新
- ✅ **大类软删保护**大类下有未删除型号时禁删600104;先删型号或迁移型号到其他大类
- ✅ **型号软删保护**型号被任一车辆fleet_vehicle引用时禁删600105
- ✅ **同名约束**typeKey **全局**唯一(违反抛 600101;modelName **同大类内**唯一(违反抛 600103
- ✅ **分页默认值**`page=1``pageSize=20``pageSize` 上限 100
- ✅ **跨大类搜索**§3.7 不传 `vehicleTypeId` + 任意过滤 = 跨大类搜索(不报错)
- ⚠️ **新增型号副作用**§3.6 新增型号后会触发价格日历初始化(见 FLEET §9.4),前端无需感知
### typeKey 系统初始化数据(不是枚举,可被 §3.3 扩展)
系统初始化时灌入了 4 条标准大类(来自 DB 初始化脚本):
| typeKey | typeName | 用途参考 |
|---|---|---|
| `suv` | SUV 越野 | 高底盘 / 多人出行 |
| `mpv` | MPV 商务 | 7 座商务车 |
| `bus` | 中巴 | 10-19 座 |
| `sedan` | 轿车 | 5 座 |
> ⚠️ 这是**表里的现有数据**,**不是枚举校验**。运营可随时通过 §3.3 新增 `minibus` / `coach` 等任意 typeKey仅约束全局不重复。前端展示时**建议从 §3.1 或 §3.2 实时查**,不要硬编码上面 4 个值。
---
## 11. 影响评估
- **是否破坏向后兼容**§3.1-§3.6/§3.8/§3.9 契约零变化,仅新增 §3.2 §3.7 两个分页接口)
- **前端是否必须同步上线**:否(不调用新接口的页面不受影响)
- **影响已有数据**:无(无 DDL,无数据迁移
---
## 12. 注意事项
- **分页参数名**`page` / `pageSize`**不是** pageNo,所有继承 PageParam 的入参都遵此约定
- **主键 Long 序列化**`id` / `vehicleTypeId` 等 Long 字段 JSON 返回为 String,前端**不要**当 Number 解析(精度会丢失)。表单提交时仍可用 Number/String 双向兼容
- **整树 vs 分页的取舍**:低基数字典(< 20 大类 / < 200 型号继续用 §3.1 整树拉一次缓存到内存高基数或要服务端搜索时用 §3.2 / §3.7
- **basePrice 序列化**`BigDecimal` 序列化为 String`"800.00"`),前端按字符串接收避免精度问题
---
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#2785](https://git.1814.love:8443/wx/HL/issues/2785)
- **PR**: [#2786](https://git.1814.love:8443/wx/HL/pulls/2786)
- **Merge commit**: [`90109cd58`](https://git.1814.love:8443/wx/HL/commit/90109cd58e33a8c4e7bc802a92cf8cb1d6921050)
- **API 文档**: `docs/order-v3/api/API-SPEC-FLEET-V1.5.html` §1车型管理库 6 接口;实际代码 9 端点 = §1.4 编辑/删除合并算 1 节)
- **同期相关 PR**:
- [#2796](https://git.1814.love:8443/wx/HL/pulls/2796) 司机详情 relatedOrders mock
- [#2799](https://git.1814.love:8443/wx/HL/pulls/2799) 司机自助 H5
- [#2800](https://git.1814.love:8443/wx/HL/pulls/2800) 司机待审核
- [#2804](https://git.1814.love:8443/wx/HL/pulls/2804) fleet 文档 v1.5 同步
### 13.2 联系人
- **后端负责人**: @yst(腰苏图)
- **前端对接(管理后台)**: 待指派

查看文件

@ -1,748 +0,0 @@
# 【修改接口·管理后台】司机档案 6 接口(详情新增 relatedOrders mock 字段 + 全模块快照)
> **PR**: #2796(详情加 relatedOrders mock 字段) | **服务**: hl-fleet-service | **更新时间**: 2026-05-21
>
> **存放目录**: `changelogs-v2/2026-05/`
> **影响范围**: 管理后台「车管 / 司机档案」页(列表 + 详情 + 新增 + 编辑 + 软删 + Excel 批量导入)
---
## ⚠️ 关键变化
| # | 接口 | 状态 | 说明 |
|---|---|---|---|
| §3.2 | GET `/admin/fleet/drivers/{driverId}` | **🔧 修改** | 响应**新增 `relatedOrders` 字段**mock 占位,固定 3 条假数据) |
| 其余 5 接口 | — | 首推 | 契约自模块首次落地PR #2724)以来未变化,本次首推完整 changelog |
> **`relatedOrders` 字段是 mock**!固定返回 3 条假数据,所有司机返回相同内容。前端可基于字段做 UI 但**不要硬编码业务逻辑**(如"看到张总订单 = 司机已派单")。详见 §3.2 + §9 + §12。
---
## 1. 接口背景
司机档案是车队所有司机的主数据:基本信息 / 驾照 / 紧急联系人 / 保险 / 历史统计 / 标签 / 多类目附件(身份证/驾驶证/肖像/健康证/培训证/荣誉/从业资格/其他 8 类)。
PR #2796 在司机详情 §3.2 响应里加了 `relatedOrders` 字段,让前端可以提前对接「司机详情 → 关联订单列表」的 UI 区块。但由于派车模块(订单 ↔ 司机关联)尚未对接到 fleet 服务,**当前返回固定 3 条 mock 假数据**,待后续真实化(详见 #2791)。
---
## 2. 变更清单
| # | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|----------|------|
| 1 | GET | `/admin/fleet/drivers/page` | 首推 | 分页列表 |
| 2 | GET | `/admin/fleet/drivers/{driverId}` | **修改** | 详情,**新增 `relatedOrders` mock 字段** |
| 3 | POST | `/admin/fleet/drivers` | 首推 | 新增(含标签 + 附件批量提交) |
| 4 | PUT | `/admin/fleet/drivers/{driverId}` | 首推 | 编辑attachments diff + tags 全量覆盖) |
| 5 | DELETE | `/admin/fleet/drivers/{driverId}` | 首推 | 软删 |
| 6 | POST | `/admin/fleet/drivers/import` | 首推 | Excel 批量导入(按身份证去重) |
---
## 3. 接口详情
### 3.1 分页查询
**GET** `/admin/fleet/drivers/page`
- **使用场景**司机档案列表页,支持「2026 年赛季在册 / 续费待回复 / 往年档案 / 黑名单 / 全部」5 个 tab 切换(前端按 `season` 字段传值)
- **认证**:管理后台 JWT
- **幂等**:是
**Query 入参**
| 字段 | 类型 | 必填 | 默认 | 校验 | 说明 |
|---|---|---|---|---|---|
| `page` | int | ❌ | 1 | `>= 1` | 页码 |
| `pageSize` | int | ❌ | 20 | `1-100` | 每页条数 |
| `keyword` | string | ❌ | — | — | 姓名模糊匹配 |
| `driverStatus` | string | ❌ | — | `idle/busy/rest/pending` | 状态精确(见 §6.2 |
| `season` | string | ❌ | — | `active/pending/archived/blacklist` | 赛季精确(见 §6.3,对应前端 5 tab |
| `tagName` | string | ❌ | — | — | 标签名精确(拥有该标签的司机) |
| `primaryVehiclePlate` | string | ❌ | — | — | 常驻车车牌精确匹配 |
| `insuranceType` | string | ❌ | — | `annual/perTrip/none` | 保险类型精确(见 §6.4 |
**出参**`Result<PageResult<DriverPageItemRespVO>>` phone/idCard 脱敏;列表项含 tags,**不含**附件详情)
| 字段 | 类型 | 说明 |
|---|---|---|
| `records[].id` | string | 司机 ID |
| `records[].name` | string | 姓名 |
| `records[].phone` | string | 手机号(脱敏,如 `138****1234` |
| `records[].idCard` | string | 身份证号(脱敏,如 `150102******1234` |
| `records[].gender` | string | 性别 |
| `records[].years` | int | 驾龄 |
| `records[].driverStatus` | string | 状态(见 §6.2 |
| `records[].season` | string | 赛季(见 §6.3 |
| `records[].primaryVehiclePlate` | string | 常驻车车牌 |
| `records[].insuranceType` | string | 保险类型(见 §6.4 |
| `records[].tags[]` | array | 标签列表 |
| `records[].createTime` | datetime | — |
| `total` / `page` / `pageSize` | int | 分页元数据 |
**典型示例 请求**5 tab 之一:在册):
```
GET /admin/fleet/drivers/page?page=1&pageSize=20&season=active
Authorization: Bearer <token>
(无请求体)
```
**典型示例 响应**
```json
{
"code": 200,
"data": {
"records": [
{
"id": "1234567890123456789",
"name": "张三",
"phone": "138****1234",
"idCard": "150102******1234",
"gender": "男",
"years": 8,
"driverStatus": "idle",
"season": "active",
"primaryVehiclePlate": "蒙A-88888",
"insuranceType": "annual",
"tags": ["老司机", "蒙语流利"],
"createTime": "2025-05-21 14:00:00"
}
],
"total": 1,
"page": 1,
"pageSize": 20
},
"msg": "成功"
}
```
**前端 tab → season 取值映射**
| 前端 tab | 后端参数 |
|---|---|
| 全部 | (不传 season |
| 2026 年赛季在册 | `season=active` |
| 续费待回复 | `season=pending` |
| 往年档案 | `season=archived` |
| 黑名单 | `season=blacklist` |
> "2026 年"是装饰文案(取自前端业务文案约定),后端 `season` 字段不带年份维度。
**错误码**400 参数校验 / 401 未登录
---
### 3.2 详情 🔧 修改(新增 `relatedOrders` mock 字段)
**GET** `/admin/fleet/drivers/{driverId}`
- **使用场景**:详情页 / 编辑前预填
- **认证**:管理后台 JWT
- **幂等**:是
**路径入参**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `driverId` | Long | ✅ | 司机 ID |
**出参**`Result<DriverDetailRespVO>`
主体字段(含 §3.1 全部字段)+ 以下扩展:
| 字段 | 类型 | 说明 |
|---|---|---|
| `nation` | string | 民族 |
| `preferredTypeKey` | string | 常开车型大类 key |
| `preferredModel` | string | 常开车型名 |
| `vehicleSource` | string | 车源own/company |
| `activeYearsJson` | string | 历次在册年份 JSON 数组字符串(如 `"[2023,2024,2025]"` |
| `license` | object | 驾照信息(见下) |
| `emergency` | object | 紧急联系人(见下) |
| `insurance` | object | 保险信息(见下) |
| `stats` | object | 历史统计(见下) |
| `tags[]` | array | 标签列表 |
| `attachments` | object<string, array> | 附件按类目分组key=category,value=该类目附件列表) |
| **`relatedOrders[]`** | **array** | **关联订单mock 占位,固定 3 条,详见警示)** |
| `createTime` / `updateTime` | datetime | — |
**`license`**`DriverLicenseVO`,DB 字段平铺,API 嵌套返回):
| 字段 | 类型 | 说明 |
|---|---|---|
| `no` | string | 驾照号 |
| `type` | string | 准驾车型A1/A2/B1/B2 等) |
| `expire` | date | 有效期 |
| `issuedBy` | string | 发证机关 |
**`emergency`**`DriverEmergencyVO`
| 字段 | 类型 | 说明 |
|---|---|---|
| `name` | string | 紧急联系人姓名 |
| `phone` | string | 手机号 |
| `relation` | string | 关系(父亲 / 妻子 等) |
**`insurance`**`DriverInsuranceVO`
| 字段 | 类型 | 说明 |
|---|---|---|
| `type` | string | 保险类型(见 §6.4 |
| `company` | string | 保险公司annual 必填) |
| `policyNo` | string | 年保单号annual 必填) |
| `annualPremium` | string | 年保险费BigDecimal 序列化) |
| `annualStart` | date | 年保险起始日 |
| `annualEnd` | date | 年保险结束日 |
| `perDayRate` | string | 行程保险日费率perTrip 必填) |
**`stats`**`DriverStatsVO`,详情专用):
| 字段 | 类型 | 说明 |
|---|---|---|
| `totalOrders` | int | 历史接单总数 |
| `avgRating` | string | 平均评分 0-5BigDecimal 序列化) |
| `lastOrderAt` | date | 最近接单日期 |
**`attachments`**`Map<String, List<DriverAttachmentRespVO>>`,key=类目(见 §6.1,value=该类目下的附件列表(按 sortNo 升序):
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | string | 附件 ID |
| `driverId` | string | 所属司机 ID |
| `category` | string | 类目(见 §6.1 |
| `sortNo` | int | 同类目排序0-based |
| `url` | string | OSS URL |
| `mimeType` | string | 例 `image/jpeg` |
**`relatedOrders[]`****⚠️ mock 占位**
| 字段 | 类型 | 说明 |
|---|---|---|
| `orderId` | string | 订单 ID前端跳转用 |
| `orderNo` | string | 订单号(如 `HL2026060100123` |
| `groupNo` | string | 团号(如 `T2026-001` |
| `customerName` | string | 客户姓名 |
| `tripStartDate` | date | 行程起始日 |
| `tripEndDate` | date | 行程结束日 |
| `destination` | string | 目的地概要 |
| `orderStatus` | string | 订单状态(`pending` / `in_progress` / `completed` |
| `vehicleType` | string | 车型快照 |
| `licensePlate` | string | 车牌快照 |
> **mock 真相**(来自代码 `DriverService.buildMockRelatedOrders()`
> - 当前**固定返回 3 条假数据**(李先生 in_progress / 王女士 completed / 张总 pending
> - 所有 driverId 返回的内容**完全相同**
> - 真实化时机:派车模块对接 fleet 服务后,由 DriverService 改为 Feign 调 order 服务按 staffId 反查Issue #2791
**典型示例 响应**(节选):
```json
{
"code": 200,
"data": {
"id": "1234567890123456789",
"name": "张三",
"phone": "138****1234",
"idCard": "150102******1234",
"gender": "男",
"nation": "蒙古族",
"years": 8,
"driverStatus": "idle",
"season": "active",
"activeYearsJson": "[2023,2024,2025]",
"primaryVehiclePlate": "蒙A-88888",
"preferredTypeKey": "suv",
"preferredModel": "丰田汉兰达",
"vehicleSource": "company",
"license": {
"no": "15010219800101XXXX",
"type": "B2",
"expire": "2030-01-01",
"issuedBy": "内蒙古公安厅交通管理局"
},
"emergency": {
"name": "张三丰",
"phone": "13800000000",
"relation": "妻子"
},
"insurance": {
"type": "annual",
"company": "中国人保财险",
"policyNo": "PICC-2024-XXX",
"annualPremium": "3000.00",
"annualStart": "2024-01-01",
"annualEnd": "2025-01-01",
"perDayRate": null
},
"stats": {
"totalOrders": 128,
"avgRating": "4.85",
"lastOrderAt": "2025-05-18"
},
"tags": ["老司机", "蒙语流利"],
"attachments": {
"id_card": [
{ "id": "...", "driverId": "...", "category": "id_card", "sortNo": 0, "url": "https://oss/.../front.jpg", "mimeType": "image/jpeg" },
{ "id": "...", "driverId": "...", "category": "id_card", "sortNo": 1, "url": "https://oss/.../back.jpg", "mimeType": "image/jpeg" }
],
"driver_license": [...],
"portrait": [...]
},
"relatedOrders": [
{
"orderId": "900000000000000001",
"orderNo": "HL2026060100123",
"groupNo": "T2026-001",
"customerName": "李先生",
"tripStartDate": "2026-06-01",
"tripEndDate": "2026-06-05",
"destination": "呼伦贝尔草原 / 阿尔山 / 海拉尔",
"orderStatus": "in_progress",
"vehicleType": "丰田汉兰达",
"licensePlate": "蒙A-12345"
},
{
"orderId": "900000000000000002",
"orderNo": "HL2026051800456",
"groupNo": "T2026-002",
"customerName": "王女士",
"tripStartDate": "2026-05-18",
"tripEndDate": "2026-05-22",
"destination": "额尔古纳 / 室韦 / 莫尔道嘎",
"orderStatus": "completed",
"vehicleType": "丰田考斯特",
"licensePlate": "蒙A-66666"
},
{
"orderId": "900000000000000003",
"orderNo": "HL2026070200789",
"groupNo": "T2026-003",
"customerName": "张总",
"tripStartDate": "2026-07-02",
"tripEndDate": "2026-07-08",
"destination": "满洲里 / 套娃景区 / 国门",
"orderStatus": "pending",
"vehicleType": "奔驰威霆",
"licensePlate": "蒙A-88888"
}
],
"createTime": "2025-05-21 14:00:00",
"updateTime": "2025-05-21 14:00:00"
},
"msg": "成功"
}
```
**错误码****600205** 司机不存在 / 401 未登录
---
### 3.3 新增
**POST** `/admin/fleet/drivers`
- **使用场景**:新增司机(含 tags 批量 + 附件批量提交,**同一事务**
- **认证**:管理后台 JWT
- **幂等**:否(身份证 / 手机号重复抛 600200 / 600203
**Body 入参**`DriverSaveReqVO`
| 字段 | 类型 | 必填 | 校验 | 说明 |
|---|---|---|---|---|
| `name` | string | ✅ | `@Size(max=32)` | 姓名 |
| `phone` | string | ✅ | `@Pattern(^\d{11}$)` | 手机号 11 位(**编辑时忽略** |
| `idCard` | string | ✅ | `@Size(min=18,max=32)` | 身份证号(**编辑时忽略** |
| `gender` | string | ❌ | `@Size(max=4)` | 性别(男 / 女) |
| `nation` | string | ❌ | `@Size(max=16)` | 民族 |
| `years` | int | ❌ | — | 驾龄(年) |
| `driverStatus` | string | ❌ | `@Pattern(idle\|busy\|rest\|pending)` | 状态 |
| `season` | string | ❌ | `@Pattern(active\|pending\|archived\|blacklist)` | 赛季 |
| `primaryVehiclePlate` | string | ❌ | `@Size(max=16)` | 常驻车车牌 |
| `preferredTypeKey` | string | ❌ | `@Size(max=16)` | 常开车型大类 key |
| `preferredModel` | string | ❌ | `@Size(max=64)` | 常开车型名 |
| `vehicleSource` | string | ❌ | `@Size(max=16)` | 车源own / company |
| `license` | object | ❌ | — | 驾照嵌套对象(同 §3.2 |
| `emergency` | object | ❌ | — | 紧急联系人嵌套对象(同 §3.2 |
| `insurance` | object | ❌ | — | 保险嵌套对象(同 §3.2 |
| `tags[]` | array | ❌ | `@Size(max=20)` | 标签列表(**全量覆盖** |
| `attachments[]` | array | ❌ | — | 附件数组(见 §3.3 attachment 表) |
**`attachments[]` 单项**`DriverAttachmentReqVO`
| 字段 | 类型 | 必填 | 校验 | 说明 |
|---|---|---|---|---|
| `id` | Long | ❌ | — | 新增项不传 |
| `category` | string | ✅ | `@Size(max=32)` | 类目(见 §6.1 |
| `url` | string | ✅ | `@Size(max=512)` | OSS URL |
| `mimeType` | string | ✅ | `@Size(max=64)` | 例 `image/jpeg` |
| `sortNo` | int | ❌ | — | 不传时按入参顺序自动重写 0..N-1 |
**出参**`Result<DriverCreateRespVO>`
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | string | 新增司机 ID |
| `attachmentIds[]` | array<Long> | 附件 ID 列表(按入参顺序) |
**典型示例 请求**(节选):
```
POST /admin/fleet/drivers
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "张三",
"phone": "13812341234",
"idCard": "150102198001011234",
"gender": "男",
"nation": "蒙古族",
"years": 8,
"driverStatus": "idle",
"season": "active",
"primaryVehiclePlate": "蒙A-88888",
"preferredTypeKey": "suv",
"preferredModel": "丰田汉兰达",
"vehicleSource": "company",
"license": {
"no": "15010219800101XXXX",
"type": "B2",
"expire": "2030-01-01",
"issuedBy": "内蒙古公安厅交通管理局"
},
"emergency": {
"name": "张三丰",
"phone": "13800000000",
"relation": "妻子"
},
"insurance": {
"type": "annual",
"company": "中国人保财险",
"policyNo": "PICC-2024-XXX",
"annualPremium": "3000.00",
"annualStart": "2024-01-01",
"annualEnd": "2025-01-01"
},
"tags": ["老司机", "蒙语流利"],
"attachments": [
{ "category": "id_card", "url": "https://oss/.../id-front.jpg", "mimeType": "image/jpeg" },
{ "category": "id_card", "url": "https://oss/.../id-back.jpg", "mimeType": "image/jpeg" },
{ "category": "driver_license", "url": "https://oss/.../license.jpg", "mimeType": "image/jpeg" },
{ "category": "portrait", "url": "https://oss/.../portrait.jpg", "mimeType": "image/jpeg" }
]
}
```
**异常 请求**(身份证重复):
```json
{ "code": 600200, "msg": "身份证已存在", "data": null }
```
**异常 请求**(驾照过期):
```json
{ "code": 600201, "msg": "驾照已过期", "data": null }
```
**错误码**400 参数校验 / **600200** 身份证已存在 / **600201** 驾照已过期 / **600203** 手机号已存在 / **601002** 附件类目非法 / **601003** 类目张数达上限 / 401 未登录
---
### 3.4 编辑attachments diff + tags 全量覆盖)
**PUT** `/admin/fleet/drivers/{driverId}`
- **使用场景**:编辑司机基本信息 + 标签全量覆盖 + 附件增删改一次性提交
- **认证**:管理后台 JWT
- **幂等**:是
**路径入参**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `driverId` | Long | ✅ | 司机 ID |
**Body 入参**:同 §3.3(共用 `DriverSaveReqVO`),但:
- **`idCard` / `phone` 后端忽略**(不可改)
- **`tags[]` 全量覆盖**:物理删除旧 tags + 批量 INSERT 新 tags空数组 = 清空所有标签)
- **`attachments[]` diff 语义**:有 id 命中 → 保留;无 id → INSERT;DB 有但未传 → 软删
**附件 diff 语义**(与车辆档案 §3.4 一致):
| 入参 attachments[] 项 | DB 中的对应行 | 行为 |
|---|---|---|
| 有 `id` + DB 命中 | 存在 | 保留(可更新 sortNo |
| 无 `id`(新增项) | — | INSERT |
| — | 存在但本次未传 | 软删 |
> **特别说明(前 3 类目"换证留历史"语义)**`id_card` / `driver_license` / `portrait` 旧附件不在本次入参里 → 自动软删;新 url 无 id → INSERT。前端"换证"时不需要先调删除接口,直接组装新 attachments 数组即可。
**出参**`Result<DriverUpdateRespVO>`
| 字段 | 类型 | 说明 |
|---|---|---|
| `kept` | int | 保留的附件数 |
| `inserted` | int | 新插入的附件数 |
| `softDeleted` | int | 软删的附件数 |
| `tagsReplaced` | int | 覆盖写入的标签数 |
**典型示例 响应**
```json
{
"code": 200,
"data": { "kept": 3, "inserted": 1, "softDeleted": 2, "tagsReplaced": 3 },
"msg": "成功"
}
```
**错误码**400 参数校验 / **600201** 驾照已过期 / **600205** 司机不存在 / **601002** 类目非法 / **601003** 类目张数达上限 / 401 未登录
---
### 3.5 软删
**DELETE** `/admin/fleet/drivers/{driverId}`
- **使用场景**:下架司机
- **认证**:管理后台 JWT
- **幂等**:是
**路径入参**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `driverId` | Long | ✅ | 司机 ID |
**出参**`Result<Void>`
**典型示例 响应**
```json
{ "code": 200, "data": null, "msg": "成功" }
```
**异常 请求****业务流水 PR 上线后才生效**
```json
{ "code": 600204, "msg": "司机有未完成派单,不能删除", "data": null }
```
**错误码****600204** 有未完成派单(占位,业务流水 PR 上线后生效) / **600205** 司机不存在 / 401 未登录
---
### 3.6 Excel 批量导入
**POST** `/admin/fleet/drivers/import`
- **使用场景**:运营批量录入司机
- **认证**:管理后台 JWT
- **幂等**:是(按身份证去重,已存在则**更新**,不存在则**新增**
**入参**multipart/form-data
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `file` | file | ✅ | Excel`.xlsx` / `.xls`)或 CSV |
**表头约定(第 1 行 15 列,顺序固定)**
```
姓名 | 手机号 | 身份证号 | 性别 | 民族 | 驾龄(年) |
驾照号 | 准驾车型 | 驾照有效期(yyyy-MM-dd) | 发证机关 |
紧急联系人姓名 | 紧急联系人手机 | 与司机关系 |
保险类型(annual/perTrip/none) | 常驻车车牌
```
**出参**`Result<DriverImportRespVO>`**字段名与车辆导入不同**
| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int | 总行数(不含表头) |
| `insertedCount` | int | 新增成功数 |
| `renewedCount` | int | 更新成功数(按身份证去重,已存在则更新) |
| `errorCount` | int | 失败行数 |
| `errors[]` | array | 失败行详情 |
**典型示例 响应**
```json
{
"code": 200,
"data": {
"total": 10,
"insertedCount": 6,
"renewedCount": 2,
"errorCount": 2,
"errors": [
{ "row": 3, "name": "张三", "msg": "驾照已过期" },
{ "row": 7, "name": "李四", "msg": "手机号格式错误" }
]
},
"msg": "成功"
}
```
> ⚠️ 与车辆导入的出参字段不同:车辆是 `success/failed/errorRows`,司机是 `insertedCount/renewedCount/errorCount/errors`。前端**不能**复用同一份解析逻辑。
**错误码**400 文件解析失败 / 401 未登录
---
## 6. 枚举 / 数据字典
### 6.1 附件类目(`category``DriverAttachmentCategoryEnum`
**所属字段**`DriverAttachmentReqVO.category` / `DriverAttachmentRespVO.category` | **类型**`String` | **必填**:✅
**真枚举**(后端硬编码 8 类目,含同类目张数上限):
| 值 | 中文 | 张数上限 | 备注 |
|---|---|---|---|
| `id_card` | 身份证 | 2 | "换证留历史"语义(支持正反 2 张或新旧 2 套) |
| `driver_license` | 驾驶证 | 2 | "换证留历史" |
| `portrait` | 肖像照 | 2 | "换证留历史" |
| `health_cert` | 健康证 | 2 | — |
| `training_cert` | 培训证书 | 5 | — |
| `award` | 荣誉奖励 | 5 | — |
| `qualification` | 从业资格证 | 3 | — |
| `other` | 其他附件 | 5 | — |
> 超出上限触发 **601003** "该类目张数已达上限"。
### 6.2 司机状态(`driverStatus``DriverStatusEnum`
**所属字段**`DriverSaveReqVO.driverStatus` / `DriverPageReqVO.driverStatus` / `DriverPageItemRespVO.driverStatus` / `DriverDetailRespVO.driverStatus` | **类型**`String` | **必填**:❌
**`@Pattern` 强校验枚举**
| 值 | 中文 | 说明 |
|---|---|---|
| `idle` | 空闲 | 可接单 |
| `busy` | 在途 | 正在执行派单 |
| `rest` | 休假 | 暂不接单 |
| `pending` | 待激活 | 新入职 / 回归尚未完成入驻 |
### 6.3 赛季(`season``SeasonEnum`
**所属字段**:同 driverStatus | **类型**`String` | **必填**:❌
**`@Pattern` 强校验枚举**
| 值 | 中文 | 说明 |
|---|---|---|
| `active` | 在册 | 当前赛季正常在职 |
| `pending` | 待续签 | 赛季到期已发出续签邀请 |
| `archived` | 已归档 | 本赛季已退出 |
| `blacklist` | 黑名单 | 永久拉黑 |
### 6.4 保险类型(`insuranceType``InsuranceTypeEnum`
**所属字段**`DriverInsuranceVO.type` / `DriverPageReqVO.insuranceType` / `DriverPageItemRespVO.insuranceType` | **类型**`String` | **必填**:❌
| 值 | 中文 | 必填字段 |
|---|---|---|
| `annual` | 年保险 | 需填 `company` / `policyNo` / `annualPremium` / `annualStart` / `annualEnd` |
| `perTrip` | 行程保险 | 需填 `perDayRate`(按行程计费) |
| `none` | 无保险 | — |
### 6.5 订单状态(`orderStatus`,**mock 字段**
**所属字段**`DriverDetailRespVO.relatedOrders[].orderStatus` | **类型**`String` | **mock 占位**
| 值 | 中文 |
|---|---|
| `pending` | 待出行 |
| `in_progress` | 进行中 |
| `completed` | 已完成 |
> ⚠️ 这是 **mock 数据**里的取值,真实派车模块对接后值列表可能扩展(如 `canceled` / `refunded` 等)。前端**不要硬编码状态映射**,等真实化后由后端文档明确。
---
## 7. 错误码(全模块汇总)
| code | 含义 | 触发条件 | 涉及接口 |
|---|---|---|---|
| 400 | 参数校验失败 | 字段缺失/长度/枚举/手机号格式/MIME 非法 | 所有写入接口 |
| 401 | 未登录 | JWT 无效或缺失 | 全部 6 接口 |
| **600200** | 身份证已存在 | `idCard` 全局唯一约束 | §3.3 |
| **600201** | 驾照已过期 | `license.expire < 今日`(新增 / 编辑主动校验) | §3.3 §3.4 |
| **600203** | 手机号已存在 | `phone` 全局唯一约束 | §3.3 |
| **600204** | 司机有未完成派单 | 业务流水 PR 上线后才触发(目前占位) | §3.5 |
| **600205** | 司机不存在 | `driverId` 无效或软删 | §3.2 §3.4 §3.5 |
| **601001** | 实体不存在 | 附件关联的主司机不存在 | §3.3 §3.4 |
| **601002** | 附件类目非法 | `category` 不在 §6.1 枚举内 | §3.3 §3.4 |
| **601003** | 该类目张数已达上限 | 超出 §6.1 张数上限 | §3.3 §3.4 |
| **601004** | 文件超出大小限制 | 应用层 + OSS HEAD 校验 | §3.3 §3.4 |
| **601005** | 文件类型非法 | MIME 不在白名单 | §3.3 §3.4 |
> 错误码段位 600200-600205 归属司机档案(无与车辆 / 车型管理库冲突;601001-601005 归属附件管理子段(车辆 / 司机共用)。
>
> **未列错误码 600202**(司机已黑名单):占位常量,业务流水 PR 上线后才会触发。
---
## 9. 业务边界
- ✅ **PII 脱敏**`phone` / `idCard` 在 §3.1 列表项 + §3.2 详情**响应**中均脱敏返回(`138****1234` / `150102******1234`;DB 存明文。前端展示时**直接用响应值**即可
- ✅ **编辑不可改 idCard / phone**§3.4 入参中的 `idCard` / `phone` 后端**忽略**,要改身份证 / 手机号需联系后端管理员(业务上极少发生)
- ✅ **驾照过期校验**§3.3 §3.4 主动校验 `license.expire < 今日`(不影响存量数据,仅写入时校验)
- ✅ **tags 全量覆盖语义**§3.4 `tags=[]` = 清空所有标签;`tags=null` 也视为不修改(不传字段 = 不动),建议前端**显式传空数组**清空
- ✅ **附件 diff 语义**:与车辆档案 §3.4 一致;id_card/driver_license/portrait 三类支持"换证留历史"(旧软删 + 新插入由 diff 天然完成)
- ✅ **唯一约束**`idCard` / `phone` **全局**唯一
- ⚠️ **relatedOrders mock 占位**:见 §3.2 / §12
---
## 10. 修改前后对比(仅 §3.2 详情)
### 10.1 字段级对比
| 字段 | 改前 | 改后 |
|---|---|---|
| `DriverDetailRespVO` | 无 `relatedOrders` 字段 | 新增 `relatedOrders[]` 字段mock 数据) |
### 10.2 行为级对比
| 行为 | 改前 | 改后 |
|---|---|---|
| 调 §3.2 详情 | 响应里无关联订单信息 | 响应额外含 3 条 mock 订单 |
---
## 11. 影响评估
- **是否破坏向后兼容**:否(仅新增字段,老前端忽略该字段照常工作)
- **前端是否必须同步上线**:否(前端可按节奏对接 relatedOrders UI 区块)
- **影响已有数据**mock 数据来自代码,不入库)
---
## 12. 注意事项
- **⚠️ `relatedOrders` 是 mock**
- 固定 3 条假数据,**所有司机返回相同内容**
- 前端**不要硬编码**业务逻辑(如"看到张总订单 = 司机已派单 / 不能软删"等推断)
- 真实化时机:派车模块对接 fleet 服务后,由 `DriverService.buildMockRelatedOrders` 替换为 Feign 实现Issue #2791
- 真实化后字段名 / 字段类型与本 changelog 一致(已约定的契约),但**取值范围可能扩展**(如 orderStatus 增加 `canceled`
- **分页参数名**`page` / `pageSize`**不是** pageNo
- **Long 主键序列化**`id` / `attachments[].id` / `relatedOrders[].orderId` 等 Long 字段 JSON 返回为 String,前端**不要**当 Number 解析
- **车辆导入 vs 司机导入字段名不同**:见 §3.6 警示
- **5 个 tab 实现**:见 §3.1 表格,纯通过 `season` 参数实现,无新接口
- **错误码段位连续**600200-600205 全在司机档案段,无避让
---
## 13. 关联 / 联系人
### 13.1 链接
- **IssuerelatedOrders mock**: [#2791](https://git.1814.love:8443/wx/HL/issues/2791)
- **PRrelatedOrders mock**: [#2796](https://git.1814.love:8443/wx/HL/pulls/2796)
- **Merge commit**: [`ec0380439`](https://git.1814.love:8443/wx/HL/commit/ec03804390c7bd39f1b4ec3ddc3bd86aeb8ef47c)
- **API 文档**: `docs/order-v3/api/API-SPEC-FLEET-V1.5.html` §3 司机档案
- **DB 文档**: `docs/order-v3/database/DATABASE-SCHEMA-FLEET-V1.5.html` §2.4 + §2.7
- **同期相关 changelog**:
- 车型管理库 9 接口([`21_2785_车型管理库-新增接口-管理后台.md`](./21_2785_车型管理库-新增接口-管理后台.md)
- 车辆档案 6 接口([`21_车辆档案-修改接口-管理后台.md`](./21_车辆档案-修改接口-管理后台.md)
### 13.2 联系人
- **后端负责人**: @yst(腰苏图)
- **前端对接(管理后台)**: 待指派

查看文件

@ -1,197 +0,0 @@
# 【修改接口·管理后台】司机详情 attachments 弱类型 Map 改强类型 DriverAttachmentsVO (#2812)
> **PR**: #2818 | **服务**: hl-fleet-service | **更新时间**: 2026-05-21 15:30
>
> **存放目录**: `changelogs-v2/2026-05/`
> **关联前序**: [`21_2791_司机档案-修改接口-管理后台.md`](./21_2791_司机档案-修改接口-管理后台.md)commit `761ca07`
---
## ⚠️ 关键变化30 秒速读)
**`GET /admin/fleet/drivers/{driverId}` 的响应字段 `attachments` 类型变了**
| 项 | 改前 | 改后 |
|---|---|---|
| 类型 | `Map<string, DriverAttachmentRespVO[]>`(动态 key | `DriverAttachmentsVO`8 个固定字段) |
| 前端用法 | `data.attachments['id_card']` 字符串访问 | `data.attachments.idCard` 字段访问 |
| TypeScript 类型 | 没有具体字段名 | 8 个具体字段(含类型提示) |
**前序 changelog 废止内容**[`21_2791_司机档案-修改接口-管理后台.md`](./21_2791_司机档案-修改接口-管理后台.md) **§3.2 详情接口** + **§9 业务边界** 中所有把 attachments 描述为 `Map<String, List<>>` 的部分**已废**,以本文档为准。
**前端必须改动**(不是兼容的):所有 `data.attachments[key]` 形式的访问要改成 `data.attachments.xxx` 字段访问。
---
## 1. 接口背景
`attachments` 当前用 `Map<String, List<DriverAttachmentRespVO>>` 返回违反 HL 项目规则 [禁止弱类型返回值]
- Swagger / TypeScript 生成的类型是 `{ [key: string]: DriverAttachmentRespVO[] }`,**没有具体字段名**
- 前端 IDE 不能自动补全
- 拼错 key 编译期不报错
- 文档无法自包含描述每个类目出参
改为强类型 `DriverAttachmentsVO`,8 个 List 字段对应 8 个固定类目(与 `DriverAttachmentCategoryEnum` 严格对齐)。
---
## 2. 变更清单
| # | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|----------|------|
| 1 | GET | `/admin/fleet/drivers/{driverId}` | **修改** | 响应字段 `attachments` 类型变化(详见 §3.1 |
仅 1 个端点受影响。其余 5 个司机档案接口page / create / update / delete / import**不受影响**。
---
## 3. 接口详情
### 3.1 司机详情 — `attachments` 字段类型变化
**GET** `/admin/fleet/drivers/{driverId}`
#### 改后的 `attachments` 字段结构
字段:`DriverAttachmentsVO` 对象(**不再是 Map**)。
| 字段 | 类型 | 默认 | 张数上限(来自 DriverAttachmentCategoryEnum |
|---|---|---|---|
| `idCard` | `DriverAttachmentRespVO[]` | `[]` | 2"换证留历史":支持正反 2 张或新旧 2 套) |
| `driverLicense` | `DriverAttachmentRespVO[]` | `[]` | 2换证留历史 |
| `portrait` | `DriverAttachmentRespVO[]` | `[]` | 2换证留历史 |
| `healthCert` | `DriverAttachmentRespVO[]` | `[]` | 2 |
| `trainingCert` | `DriverAttachmentRespVO[]` | `[]` | 5 |
| `award` | `DriverAttachmentRespVO[]` | `[]` | 5 |
| `qualification` | `DriverAttachmentRespVO[]` | `[]` | 3 |
| `other` | `DriverAttachmentRespVO[]` | `[]` | 5 |
> **每个字段默认空数组**(不为 null,前端**无需判空**,直接 `.map()` 渲染即可。
`DriverAttachmentRespVO[]` 单条项字段(**不变**
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | string | 附件 IDLong 序列化为 String |
| `driverId` | string | 所属司机 ID |
| `category` | string | 类目 |
| `sortNo` | int | 同类目排序0-based |
| `url` | string | OSS URL |
| `mimeType` | string | 例 `image/jpeg` |
#### 改后响应示例(节选)
```json
{
"code": 200,
"data": {
"id": "1234567890123456789",
"name": "张三",
"...其他字段保持不变...": "...",
"attachments": {
"idCard": [
{ "id": "...", "driverId": "...", "category": "id_card", "sortNo": 0, "url": "https://oss/.../id-front.jpg", "mimeType": "image/jpeg" },
{ "id": "...", "driverId": "...", "category": "id_card", "sortNo": 1, "url": "https://oss/.../id-back.jpg", "mimeType": "image/jpeg" }
],
"driverLicense": [
{ "id": "...", "driverId": "...", "category": "driver_license", "sortNo": 0, "url": "https://oss/.../license.jpg", "mimeType": "image/jpeg" }
],
"portrait": [],
"healthCert": [],
"trainingCert": [],
"award": [],
"qualification": [],
"other": []
},
"relatedOrders": [...]
},
"msg": "成功"
}
```
---
## 6. 枚举 / 数据字典
无新增枚举。`category` 字段语义和取值与前序 changelog [`21_2791_司机档案-修改接口-管理后台.md`](./21_2791_司机档案-修改接口-管理后台.md) §6.1 一致8 个值),唯一差异是 **API 响应不再以 category 字符串作为 key**,而是用 VO 字段名(驼峰)。
类目 ↔ VO 字段对应关系:
| DB / category 字段值 | VO 字段(驼峰) |
|---|---|
| `id_card` | `idCard` |
| `driver_license` | `driverLicense` |
| `portrait` | `portrait` |
| `health_cert` | `healthCert` |
| `training_cert` | `trainingCert` |
| `award` | `award` |
| `qualification` | `qualification` |
| `other` | `other` |
注意单条 `DriverAttachmentRespVO.category` 仍然是 `id_card` / `driver_license` 等下划线值(不变)。改的是**外层分组的 key 形式**。
---
## 7. 错误码
无变化。
---
## 10. 修改前后对比
### 10.1 字段级对比
| 字段 | 改前 | 改后 |
|---|---|---|
| `attachments` 类型 | `Map<string, DriverAttachmentRespVO[]>` | `DriverAttachmentsVO`8 个固定 List 字段) |
| `attachments` key 形式 | DB 下划线值(`id_card` / `driver_license` / `portrait` / ... | VO 驼峰字段(`idCard` / `driverLicense` / `portrait` / ... |
| 类目缺失 | 该 key 不在 Map 里 | 该字段是空数组 `[]` |
### 10.2 前端代码对照
```typescript
// 改前
const idCardAttachments = data.attachments['id_card'] ?? [];
const licenseAttachments = data.attachments['driver_license'] ?? [];
// 改后
const idCardAttachments = data.attachments.idCard; // 不用判空
const licenseAttachments = data.attachments.driverLicense;
```
---
## 11. 影响评估
- **是否破坏向后兼容**:⚠️ **是**(字段类型变了,前端必须改)
- **前端是否必须同步上线****是**(老前端按 Map 解析会拿到 undefined
- **影响其他后端服务**DriverDetailRespVO 仅 admin 接口返回,不走 Feign
- **影响已有数据**:无(仅 API 序列化形式变化,DB 无变)
---
## 12. 注意事项
- **前端 workaround 清理点**
- 改前如果前端有 `const map = data.attachments; const idCard = map['id_card'] ?? [];` 之类 workaround,**改成 `data.attachments.idCard`** 即可
- 改前如果前端硬编码遍历类目(如 `for (const key of ['id_card', 'driver_license'])`),**改成直接读 VO 字段**
- 改前如果前端有 `if (map['portrait'])` 判空逻辑,**直接删** —— 强类型 VO 字段必非 null默认空数组
- **前序 changelog 描述废止**[`21_2791_司机档案-修改接口-管理后台.md`](./21_2791_司机档案-修改接口-管理后台.md) 已发出的 attachments Map 描述以本文档为准,前序 changelog **不会修改**changelog 只追加不回写历史)
---
## 13. 关联 / 联系人
### 13.1 链接
- **Issue**: [#2812](https://git.1814.love:8443/wx/HL/issues/2812)
- **PR**: [#2818](https://git.1814.love:8443/wx/HL/pulls/2818)
- **Merge commit**: [`8c340550`](https://git.1814.love:8443/wx/HL/commit/8c340550656bdee3ee2243b98d694b818474aeba)
- **前序 changelogattachments Map 描述废)**: [`21_2791_司机档案-修改接口-管理后台.md`](./21_2791_司机档案-修改接口-管理后台.md)
- **规则**: HL `MEMORY.md``feedback_no-map-return-type.md` (禁止弱类型返回值)
### 13.2 联系人
- **后端负责人**: @yst(腰苏图)
- **前端对接(管理后台)**: 待指派

查看文件

@ -1,186 +0,0 @@
# order-v3 review 跟进:配房工作台 / 询房 URL 重排 + HOUSE swap 多项修复 + 出行人/大交通杂项
> **存放目录**: 二期(v3 `order-v3` 标签)→ `changelogs-v2/2026-05/`
> **服务**: hl-order-service-v3 (8086) + hl-user-service (8081)
> **PR**: #2833 / #2834 / #2835 / #2836 / #2840(hotfix)
> **Issue**: #2821 / #2822 / #2823 / #2824
> **日期**: 2026-05-21
> **影响范围**: 管理端 HOUSE 配房工作台 + HOUSE 询房 + HOUSE 换酒店 + 出行人/大交通若干字段
---
## ⚠️ 关键变化(前端必看)
### 🔴 路径变更(配房工作台 + 询房,5+2=7 个接口)
前端按 `API-SPEC-HOUSE-V1.1.html` 文档对接,**之前 4 个 PR (#2738-2754) 落地的代码路径与文档严重不一致,会全部 404**;本批 PR #2833 已**对齐文档**。前端按文档实现的请直接对接新路径;之前对接到旧路径的,请按下表改 URL:
| 接口 | 旧路径(已废弃) | 新路径(以此为准) |
|------|----------------|-------------------|
| §2.1 配房候选源 | `GET /v3/admin/house/assignments/candidates?requirementId=X` | **`GET /v3/admin/order/hotel-requirements/{rid}/candidates`** (rid 走 PathVariable) |
| §2.2 提交配房 | `POST /v3/admin/house/assignments`(rid 在 body) | **`POST /v3/admin/order/hotel-requirements/{rid}/assignments`**(rid 走 PathVariable;body 不再传 requirementId) |
| §2.3 修改配房 | `PUT /v3/admin/house/assignments/{id}` | **`PUT /v3/admin/order/assignments/{id}`** |
| §2.4 删除配房 | `DELETE /v3/admin/house/assignments/{id}` | **`DELETE /v3/admin/order/assignments/{id}`** |
| §2.5 房间分配 | `POST /v3/admin/house/assignments/{id}/rooms`(POST 写入) | **`GET /v3/admin/order/orders/{orderId}/rooms`**(HTTP 方法 + 路径 + 入参形态全改 — GET 查询语义,按家庭分组返结果) |
| §3.3 询房历史 | `GET /v3/admin/order/inquiry/by-order` | **`GET /v3/admin/order/inquiry?orderId=X`** |
| §3.6 加急 | `POST /v3/admin/order/inquiry/{id}/urgent` | **`POST /v3/admin/order/inquiry/{id}/escalate`** |
§2.5 GET 新 VO `OrderRoomsRespVO`:
```json
{
"orderId": "string",
"families": [
{
"familyNo": "F1",
"travelers": [{ "travelerId": "string", "name": "张三", "phoneMasked": "138****5612" }],
"days": [{ "dayNumber": 1, "rooms": [{ "hotelName": "...", "roomTypeName": "..." }] }]
}
]
}
```
---
## 一、背景
dev-v3 近 3 天(2026-05-18~21)合并 30+ PR / 786 文件 / +74,352 行(出行人 + 大交通 + HOUSE H01-H10 + FIX A-H + 通知中心 internal),全模块工作流 review 检出 10 P0 + 36 P1。本批 4 张 P0/P1 工单(#2821-2824)+ 1 张 hotfix(#2840 Flyway 修)对应 4 张 PR + 1 张 hotfix 全部测试服部署 + Flyway 落库 + admin token round-trip 验证通过。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 配房候选源 | GET | `/v3/admin/order/hotel-requirements/{rid}/candidates` | **路径迁移** | 旧 `/v3/admin/house/assignments/candidates` 废弃 |
| 2 | 提交配房 | POST | `/v3/admin/order/hotel-requirements/{rid}/assignments` | **路径迁移** | rid 改 PathVariable,body 删 requirementId |
| 3 | 修改配房 | PUT | `/v3/admin/order/assignments/{id}` | **路径迁移** | |
| 4 | 删除配房 | DELETE | `/v3/admin/order/assignments/{id}` | **路径迁移** | |
| 5 | 房间分配 | **GET**(原 POST) | `/v3/admin/order/orders/{orderId}/rooms` | **方法+路径全改+新 VO** | 改为查询语义,按家庭分组聚合 |
| 6 | 询房历史 | GET | `/v3/admin/order/inquiry?orderId=X` | **路径迁移** | `/by-order` 后缀去掉 |
| 7 | 询房加急 | POST | `/v3/admin/order/inquiry/{id}/escalate` | **路径迁移** | `/urgent``/escalate` |
| 8 | 换酒店提交 | POST | `/v3/admin/order/swap-hotel/commit` | **权限校验补全** | 越权返 `808402`(详 §三-1) |
| 9 | 大交通 VO 字段类型 | - | `TransportPlanVO.id / orderId / travelers[].id` | **响应字段类型 Long → String** | 防 JS 精度丢失 |
| 10 | 出行人 internal Feign | GET | `/v3/internal/traveler/list-by-order/{orderId}` | **返回字段补值** | `transportPlanIds` 不再永空,合同/保险拿到真大交通批次 |
| 11 | 出行人错误码 | - | 多接口 | **错误码段位修正** | 见 §三-3 |
| 12 | 大交通 batchReplace | POST | `/v3/admin/order/{id}/transport-plan/batch-replace` | **加幂等 + 分布式锁** | 防重复提交触发全量替换 |
---
## 三、接口详情
### 1. 换酒店模块 (PR #2834)
#### 权限校验补齐(原 TODO)
之前 `POST /v3/admin/order/swap-hotel/commit` 没校验"当前用户是订单 assignee" — 任何房务 token 都能改别人的订单。**已修复,跨用户越权返 `808402`**。
```http
HTTP/1.1 200 OK
{ "code": 808402, "message": "当前用户必须为订单 assignee 才能操作", "success": false }
```
#### 状态机审计正确性
之前 `swap` 业务的状态机 fire 写死 `from=CLAIMING`,审计日志失真。现根据 `house_todo` 表 OPEN `SWAP_HOTEL`/`REFUND` 待办推导真实 from(`EXCEPTION``CLAIMING`)。**前端 UI 不直接感知,但订单操作日志看 from 字段会更准确**。
#### DRIVER 通知补发(原漏发,业务 BUG)
API-SPEC-HOUSE §5.3 列了 4 个 receiver_type(`OLD_HOTEL / NEW_HOTEL / DRIVER / ORDER_CUSTOMER`),之前代码只发 3 个,**司机收不到换酒店通知**。本 PR 补 DRIVER。
⚠️ DRIVER 实际企微推送需 FLEET `vehicle_plan` 反查 Feign(留尾,后续工单接通),当前发送链路 = 事件落库 + dispatcher noop 占位(不抛错不阻塞)。
#### 新增错误码(API-SPEC-HOUSE §5.3 / §11.6 已同步)
| 错误码 | 说明 |
|--------|------|
| `808404` | 旧酒店 ID 与新酒店 ID 相同 |
| `808405` | 仅 PROCESSING / EXCEPTION 状态可换酒店 |
| `808406` | 换酒店写库失败,请重试 |
| `808407` | 新酒店库存不足 |
---
### 2. 大交通 — Long 字段类型变化 (PR #2836)
`TransportPlanVO` 中以下三个字段以前响应是 JSON Number(可能精度丢失),现统一返字符串:
| VO 字段 | 旧类型 | 新类型 | 示例 |
|---------|--------|--------|------|
| `TransportPlanVO.id` | number | **string** | `"80012345"` |
| `TransportPlanVO.orderId` | number | **string** | `"30099887"` |
| `TransportPlanVO.travelers[].id` | number | **string** | `"40055001"` |
**前端检查**:`Number(id)` / `parseInt(id)` 这类用法需改;直接 `id` 字符串传回后端 / 当 key 用没影响。
---
### 3. 出行人错误码段位修正 (PR #2836)
`docs/order-v3/api/API-SPEC-V5.54.html` 错误码表 5 处更新(以代码为准):
| 章节 | 旧码 | 新码 |
|------|------|------|
| §2.1 列表 | `581100` | `581102` |
| §2.2 批量编辑 9 行错误码表 | 581100-581124 | **581101-581105 / 581110-581114 / 581111 / 581119** |
| §2.3 单个新增 | `581100``581102`;补 581115/581116/581118 |
| §2.4 删除 | `581119→581106 / 581120→581107 / 581100→581102` |
| §2.8 智能解析 | `581120-581123` | `581131-581134` |
| §2.9 校验 | `581124` | `581102` |
前端如做了 i18n 映射,请按新码更新 — 否则会命中默认文案。
---
### 4. 出行人 internal Feign `transportPlanIds` 真实化 (PR #2836)
`GET /v3/internal/traveler/list-by-order/{orderId}` 之前 `transportPlanIds` 字段硬编码空数组,合同/保险服务跨服务调用永远拿不到出行人绑定的大交通批次 ID。本 PR 修复,字段返真实值。**前端不直接调 internal,影响合同/保险出参的"出行人.transportPlanIds"字段**。
---
### 5. 大交通 batchReplace 并发保护 (PR #2836)
`POST /v3/admin/order/{id}/transport-plan/batch-replace` 全量替换接口加 `@Idempotent`(3s)+ `@Lock4j`(30s)— 短时间重复提交会拒绝,前端无需改但可减少重复提交风险。
---
### 6. 通知中心 internal 套件规范(后端内部) (PR #2835)
`/internal/notification/dispatch` / `/publish` 加幂等 + dispatch 返回类型从 void 改 `DispatchResult { success, channelsTriggered, errorMessage }`。**前端不调 internal,无影响**。
---
### 7. order_todo 唯一索引 (PR #2836 / hotfix #2840)
DB 加 `(order_id, todo_type, sequence, deleted_marker)` 唯一索引,防并发触发重复紧急待办。前端无感。
> hotfix #2840:V20260521_004 generated column 用 `UNIX_TIMESTAMP` 触发 MySQL 8 `ERROR 3763 disallowed function`,已改 `COALESCE(deleted_at, '1970-01-01')`(deterministic)。
---
## 四、测试服验证状态
| 服务 | 部署 commit | uptime | Flyway | admin round-trip |
|------|------------|--------|--------|------------------|
| hl-user-service | dev-v3 含 #2834/#2835 | 已重启 | V20260521_007 success=1 17:49 | ✅ |
| hl-order-service-v3 | dev-v3 含 #2833/#2836/#2840 | 18:12 重启 | V20260521_004 success=1 18:12 | ✅ §3.3 / §3.6 / §2.5 / §2.1 全 200/合理 400 |
唯一索引 `uk_todo_order_type_seq_active` 已在 `hl_order_service_v3.order_todo` 表生效。
---
## 五、留尾(后续 v3 工单)
- **HOUSE-SWAP DRIVER 通知** wework 真值切换 → 需 FLEET `vehicle_plan` 反查 Feign 工单
- **HOUSE-FIX-A 候选源数据真实化**(`loadProductPool`)→ 需 H04 完整接通 `ProductV2FeignClient`
- **`HouseInventoryCheckedEvent` 消费方**(`H09` MQ 桥接 PR)
- **TravelerErrorCode 段位整体迁移代码常量**(大版本可决议)
- **`operationLogMapper` 收敛** 到 `HotelOperationLogService`(待该 Service 出现)
---
## 六、关联
- 工单: #2821 / #2822 / #2823 / #2824(全部 closed)
- PR: #2833 / #2834 / #2835 / #2836 / #2840(全部 merged into dev-v3)
- Review 来源: Claude /@cr 7 个 agent 并发对照 `docs/order-v3/` SRS+API+DB+Detail

查看文件

@ -1,101 +0,0 @@
# tag-library: 私人标签库 6 接口真实化完成 + QA 验收通过
> **存放目录**: `changelogs-v2/2026-05/`v3 二期)
> **服务**: hl-order-service-v3
> **关联**: 19 号已发布骨架版([`19_2045_tag-library模块私人标签库管理-新增接口-管理后台.md`](./19_2045_tag-library模块私人标签库管理-新增接口-管理后台.md)
> **真实化 PR**: #2606DB 业务实现)
> **关联 PR**: #2777(不直接相关,仅同会话)
> **日期**: 2026-05-21
> **影响范围**: 管理后台「我的标签库」管理页 + 创建订单时的标签下拉
---
## ⚠️ 关键变化
**接口契约 0 变化**。19 号骨架版发布时承诺的「契约本次发布后不再变」**已兑现** —— 字段名 / 类型 / 必填 / 路径 / 错误码全部与 19 号一致。
本次变化仅是**实现状态**
| 项 | 19 号骨架版 | 本次2026-05-21 |
|---|---|---|
| ServiceImpl | **Mock 硬编码样例** | **真实 DB**`tag_library` 表) |
| 数据持久化 | ❌ 不落库,每次返样例 | ✅ INSERT / UPDATE / SOFT DELETE 真实生效 |
| 错误码 581421/581423/581424/581425/581427 | 未触发Mock 永远成功) | **全部按设计触发**QA 已验证) |
| 多用户隔离 | 未生效Mock 不区分 userId | **生效**user_id 隔离 + 越权拦截) |
| 排序持久化 | 仅返样例数据,不响应排序请求 | **真实持久化**sort_order 列) |
| 软删后同名复用 | N/A | **支持**(唯一索引 `(user_id, tag_name, deleted_at)` |
**前端意义**:之前按 19 号骨架对接的代码**无需任何改动**,但**联调时机解锁** —— 现在可以真实创建 / 修改 / 删除 / 排序 / 置顶并看到数据持久化和正确的错误码反馈。
---
## 一、背景
19 号 6 接口骨架版上线时 ServiceImpl 是 Mock 实现,前端可按契约对接但无法真实联调(数据不落库、错误码不触发)。
本次 PR #2606 完成 DB 真实化,今天通过 33 项 QA 用例全量验收。
---
## 二、变更清单
| # | 接口 | 方法 | 路径 | 本次变更 |
|---|------|------|------|----------|
| 1 | 查我的标签库 | GET | `/v3/admin/tag-library` | Mock → 真实 DB 查询,支持 keyword 模糊 / pinnedOnly 过滤 / 后端固定排序 |
| 2 | 新增标签库预设 | POST | `/v3/admin/tag-library` | Mock → 真实 INSERT,sortOrder=MAX+1 / 重名 581421 触发 / 默认色 `#5B8FF9` 生效 |
| 3 | 修改(重命名/换色) | PUT | `/v3/admin/tag-library/{id}` | Mock → 真实 UPDATE,越权 581423 / 改名重名 581421 / 排除自身校验生效 |
| 4 | 删除(软删) | DELETE | `/v3/admin/tag-library/{id}` | Mock → 真实软删(`deleted_at=NOW()`),越权 581424 / 软删后同名可复用 |
| 5 | 批量排序 | PUT | `/v3/admin/tag-library/sort` | Mock → 真实事务包裹 UPDATE,含他人 id 581425 整批回滚 / 重复 id 581427 |
| 6 | 置顶 toggle | PUT | `/v3/admin/tag-library/{id}/pin` | Mock → 真实 UPDATE,越权 581424 |
---
## 三、接口契约
**未变更**,详见 [`19_2045_tag-library模块私人标签库管理-新增接口-管理后台.md`](./19_2045_tag-library模块私人标签库管理-新增接口-管理后台.md):入参 / 出参 / 枚举 / 字段约束 / 路径全部一致。
---
## 四、错误码契约(全部经 QA 验证)
| Code | 触发场景 | 接口 |
|---|---|---|
| 581421 | 同用户已有同名标签 | POST 新增 / PUT 改名 |
| 581423 | 标签不存在 / 已软删 / 非本人 | PUT 更新 |
| 581424 | 标签不存在 / 已软删 / 非本人 | DELETE 删除 / PUT 置顶 |
| 581425 | 排序 items 含他人 id**整批回滚,不部分成功** | PUT 排序 |
| 581427 | 排序 items 含重复 id | PUT 排序 |
| 400 | 参数校验失败tagName 空 / 超 50 字 / tagColor 格式非法 / pinned 缺失 / items 空数组) | 全部 |
| 401 | 未登录 | 全部 |
---
## 五、QA 验收结果
- **覆盖**6 接口 × 33 用例(创建 7 / 列表 5 / 更新 7 / 删除 4 / 排序 5 / 置顶 5
- **通过率**33 / 33 = **100%**
- **验证维度**
- 字段校验(必填 / 长度 ≤50 / `#RRGGBB` 格式)
- 错误码契约5 个业务错误码全部按设计触发)
- 多用户隔离user_id=9999 脏数据无法被 user_id=1001 修改 / 删除 / 排序 / 置顶)
- 事务一致性(排序含他人 id 时,本人 id 也未被改)
- 软删行为(删后立即同名 create 成功)
- 排序规则(`is_pinned DESC, sort_order ASC, last_used_at DESC`
- Long ID 序列化(响应中为字符串,无 JS 精度丢失)
---
## 六、注意事项
1. **前端不需要做任何代码改动**。19 号骨架对接的请求 / 响应解析 / 错误码处理逻辑直接复用。
2. **联调时机**:本次发布后可正式进入真实联调(之前 Mock 期不能验持久化和错误码)。
3. **路径**`/v3/admin/tag-library` 前缀,**带 `/v3`**,经 Gateway 转发到 hl-order-service-v3。
4. **userId 自动派生**:前端不传操作人,后端从 JWT 注入。
---
## 七、关联
- 骨架版 changelog`19_2045_tag-library模块私人标签库管理-新增接口-管理后台.md`
- 真实化 PRhttps://git.1814.love:8443/wx/HL/pulls/2606
- v5.14 §14 章节规范:详见 SRS v5.14
- 负责人yst

查看文件

@ -1,553 +0,0 @@
# 【修改接口·管理后台】车辆档案 6 接口(全模块首次推送 changelog
> **PR**: 无(本次为首次推送,实质接口契约未变更,配合 §1 §3 模块 changelog 一并补齐 fleet 全档案契约)
> **服务**: hl-fleet-service | **更新时间**: 2026-05-21
>
> **存放目录**: `changelogs-v2/2026-05/`
> **影响范围**: 管理后台「车管 / 车辆档案」页(列表 + 详情 + 新增 + 编辑 + 软删 + Excel 批量导入)
---
## ⚠️ 关键说明
车辆档案模块 6 接口自 fleet 服务首次落地PR #2724)以来契约**未变化**,本次仅作**首次完整 changelog 推送**,让前端 v3 项目仓库一次拿全 6 接口契约。
**与车型管理库§1 changelog配套**:车辆档案的 `vehicleModelId` 引用车型管理库的型号 ID,前端可在新增车辆页用 §1.5 / §1.7 拉型号下拉。
---
## 1. 接口背景
车辆档案是车队所有车辆的主数据:车牌 / 车型 / VIN / 行驶证 / 保险 / 年检 / 多类目附件(外观/内饰/保险/年检/营运证/其他 6 类)。
前端"车管 / 车辆档案"页面提供:
- 列表筛选(按车牌、车队、车型大类、状态、保险/年检到期日)
- 详情查看(含附件按类目分组)
- 新增 / 编辑(含附件 diff 语义批量提交)
- 软删(业务流水模块上线后会按"未完成派单"拒删)
- Excel 批量导入
---
## 2. 变更清单
| # | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|----------|------|
| 1 | GET | `/admin/fleet/vehicles/page` | 首推 | 分页列表(多条件过滤) |
| 2 | GET | `/admin/fleet/vehicles/{vehicleId}` | 首推 | 详情(含附件) |
| 3 | POST | `/admin/fleet/vehicles` | 首推 | 新增(含附件批量 INSERT |
| 4 | PUT | `/admin/fleet/vehicles/{vehicleId}` | 首推 | 编辑(附件 diff 语义) |
| 5 | DELETE | `/admin/fleet/vehicles/{vehicleId}` | 首推 | 软删 |
| 6 | POST | `/admin/fleet/vehicles/import` | 首推 | Excel 批量导入 |
---
## 3. 接口详情
### 3.1 分页查询
**GET** `/admin/fleet/vehicles/page`
- **使用场景**:车管车辆档案列表页
- **认证**:管理后台 JWT
- **幂等**:是(只读)
**Query 入参**
| 字段 | 类型 | 必填 | 默认 | 校验 | 说明 |
|---|---|---|---|---|---|
| `page` | int | ❌ | 1 | `>= 1` | 页码 |
| `pageSize` | int | ❌ | 20 | `1-100` | 每页条数 |
| `keyword` | string | ❌ | — | — | 车牌模糊匹配 |
| `fleet` | string | ❌ | — | `own/coopA/coopB` | 车队精确过滤 |
| `typeKey` | string | ❌ | — | — | 车型大类 key 精确(如 suv |
| `vehicleStatus` | string | ❌ | — | `idle/busy/maint` | 状态精确 |
| `insureDueBefore` | date | ❌ | — | yyyy-MM-dd | 保险到期日早于此日(含) |
| `inspectDueBefore` | date | ❌ | — | yyyy-MM-dd | 年检到期日早于此日(含) |
**出参**`Result<PageResult<VehiclePageItemRespVO>>` (按 create_time DESC
| 字段 | 类型 | 说明 |
|---|---|---|
| `records[].id` | string | 车辆 IDLong 序列化为 String |
| `records[].plate` | string | 车牌 |
| `records[].vehicleModelId` | string | 型号 ID |
| `records[].vehicleTypeId` | string | 大类 ID |
| `records[].modelName` | string | 型号名(如丰田汉兰达) |
| `records[].seats` | int | 座位数 |
| `records[].fleet` | string | 车队own/coopA/coopB |
| `records[].vehicleStatus` | string | 状态idle/busy/maint |
| `records[].insurer` | string | 保险公司 |
| `records[].insureDue` | date | 保险到期日 |
| `records[].inspectDue` | date | 年检到期日 |
| `records[].createTime` | datetime | 创建时间 |
| `total` / `page` / `pageSize` | int | 分页元数据 |
**典型示例 请求**
```
GET /admin/fleet/vehicles/page?page=1&pageSize=20&fleet=own&insureDueBefore=2025-12-31
Authorization: Bearer <token>
(无请求体)
```
**典型示例 响应**
```json
{
"code": 200,
"data": {
"records": [
{
"id": "1234567890123456789",
"plate": "蒙A-88888",
"vehicleModelId": "1234567890123456001",
"vehicleTypeId": "1234567890123456002",
"modelName": "丰田汉兰达",
"seats": 7,
"fleet": "own",
"vehicleStatus": "idle",
"insurer": "中国人保财险",
"insureDue": "2025-12-31",
"inspectDue": "2025-12-31",
"createTime": "2025-05-21 14:00:00"
}
],
"total": 1,
"page": 1,
"pageSize": 20
},
"msg": "成功"
}
```
**错误码**400 参数校验 / 401 未登录
---
### 3.2 详情
**GET** `/admin/fleet/vehicles/{vehicleId}`
- **使用场景**:详情页 / 编辑前预填
- **认证**:管理后台 JWT
- **幂等**:是
**路径入参**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `vehicleId` | Long | ✅ | 车辆 ID |
**出参**`Result<VehicleDetailRespVO>`
包含 §3.1 全部字段 + 以下扩展:
| 字段 | 类型 | 说明 |
|---|---|---|
| `vin` | string | 车架号VIN |
| `regDate` | date | 注册日期 |
| `policyNo` | string | 保单号 |
| `regCertNo` | string | 行驶证号 |
| `regCertOwner` | string | 行驶证所有人 |
| `regCertUsage` | string | 使用性质 |
| `regCertFrontUrl` | string | 行驶证正面 OSS URL |
| `regCertBackUrl` | string | 行驶证副页 OSS URL |
| `attachments[]` | array | 附件列表(按 category + sortNo 升序) |
| `attachments[].id` | string | 附件 ID |
| `attachments[].category` | string | 类目(见 §6.1 |
| `attachments[].sortNo` | int | 同类目排序0-based |
| `attachments[].url` | string | OSS URL |
| `attachments[].mimeType` | string | 例 `image/jpeg` |
| `primaryDriverName` | string\|null | **目前恒为 null**(待业务流水 PR 注入司机姓名) |
| `updateTime` | datetime | — |
**典型示例 响应**
```json
{
"code": 200,
"data": {
"id": "1234567890123456789",
"plate": "蒙A-88888",
"vehicleModelId": "1234567890123456001",
"vehicleTypeId": "1234567890123456002",
"modelName": "丰田汉兰达",
"seats": 7,
"fleet": "own",
"vehicleStatus": "idle",
"vin": "LSVNV2182E2100001",
"regDate": "2020-01-01",
"inspectDue": "2025-12-31",
"insurer": "中国人保财险",
"policyNo": "PICC-2024-XXX",
"insureDue": "2025-12-31",
"regCertNo": "12345678",
"regCertOwner": "呼籁旅游服务有限公司",
"regCertUsage": "营运租赁",
"regCertFrontUrl": "https://oss.example.com/fleet/cert-front.jpg",
"regCertBackUrl": "https://oss.example.com/fleet/cert-back.jpg",
"attachments": [
{
"id": "1234567890123456900",
"vehicleId": "1234567890123456789",
"category": "exterior",
"sortNo": 0,
"url": "https://oss.example.com/fleet/ext-1.jpg",
"mimeType": "image/jpeg"
}
],
"primaryDriverName": null,
"createTime": "2025-05-21 14:00:00",
"updateTime": "2025-05-21 14:00:00"
},
"msg": "成功"
}
```
**错误码****600110** 车辆不存在 / 401 未登录
---
### 3.3 新增
**POST** `/admin/fleet/vehicles`
- **使用场景**:新增车辆 + 附件批量提交(前端 STS 直传 OSS 后把 URL 数组放主表单一并提交)
- **认证**:管理后台 JWT
- **幂等**:否(重复抛 600100/600107
**Body 入参**`VehicleSaveReqVO`
| 字段 | 类型 | 必填 | 校验 | 说明 |
|---|---|---|---|---|
| `plate` | string | ✅ | `@Size(max=16)` | 车牌(全局唯一) |
| `vehicleModelId` | Long | ✅ | — | 车型型号 ID引用 §1 模块) |
| `fleet` | string | ✅ | `@Pattern(own\|coopA\|coopB)` | 车队 |
| `vehicleStatus` | string | ❌ | `@Pattern(idle\|busy\|maint)` | 状态,默认 idle |
| `vin` | string | ❌ | `@Size(max=32)` | 车架号(传则全局唯一) |
| `regDate` | date | ❌ | — | 注册日期 |
| `inspectDue` | date | ❌ | — | 年检到期日 |
| `insurer` | string | ❌ | `@Size(max=64)` | 保险公司 |
| `policyNo` | string | ❌ | `@Size(max=64)` | 保单号 |
| `insureDue` | date | ❌ | — | 保险到期日 |
| `regCertNo` | string | ❌ | `@Size(max=64)` | 行驶证号 |
| `regCertOwner` | string | ❌ | `@Size(max=128)` | 行驶证所有人 |
| `regCertUsage` | string | ❌ | `@Size(max=32)` | 使用性质 |
| `regCertFrontUrl` | string | ❌ | `@Size(max=512)` | 行驶证正面 OSS URL |
| `regCertBackUrl` | string | ❌ | `@Size(max=512)` | 行驶证副页 OSS URL |
| `attachments[]` | array | ❌ | — | 附件数组(见下) |
**`attachments[]` 单项**`VehicleAttachmentReqVO`
| 字段 | 类型 | 必填 | 校验 | 说明 |
|---|---|---|---|---|
| `id` | Long | ❌ | — | 附件 ID新增项不传 |
| `category` | string | ✅ | `@Size(max=32)` | 类目(见 §6.1 |
| `url` | string | ✅ | `@Size(max=512)` | OSS URL |
| `mimeType` | string | ✅ | `@Size(max=64)` | 例 `image/jpeg` |
| `sortNo` | int | ❌ | — | 同类目排序(不传时按入参顺序自动重写 0..N-1 |
**出参**`Result<VehicleCreateRespVO>`
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | string | 新增车辆 ID |
| `attachmentIds[]` | array<Long> | 附件 ID 列表(按入参顺序) |
**典型示例 请求**
```
POST /admin/fleet/vehicles
Authorization: Bearer <token>
Content-Type: application/json
{
"plate": "蒙A-88888",
"vehicleModelId": "1234567890123456001",
"fleet": "own",
"vehicleStatus": "idle",
"vin": "LSVNV2182E2100001",
"regDate": "2020-01-01",
"inspectDue": "2025-12-31",
"insurer": "中国人保财险",
"policyNo": "PICC-2024-XXX",
"insureDue": "2025-12-31",
"regCertNo": "12345678",
"regCertOwner": "呼籁旅游服务有限公司",
"regCertUsage": "营运租赁",
"regCertFrontUrl": "https://oss.example.com/fleet/cert-front.jpg",
"regCertBackUrl": "https://oss.example.com/fleet/cert-back.jpg",
"attachments": [
{ "category": "exterior", "url": "https://oss.example.com/fleet/ext-1.jpg", "mimeType": "image/jpeg" },
{ "category": "exterior", "url": "https://oss.example.com/fleet/ext-2.jpg", "mimeType": "image/jpeg" },
{ "category": "insurance", "url": "https://oss.example.com/fleet/ins.jpg", "mimeType": "image/jpeg" }
]
}
```
**典型示例 响应**
```json
{
"code": 200,
"data": {
"id": "1234567890123456789",
"attachmentIds": ["1234567890123456900", "1234567890123456901", "1234567890123456902"]
},
"msg": "成功"
}
```
**异常 请求**(车牌重复):
```json
{ "code": 600100, "msg": "车牌已存在", "data": null }
```
**异常 请求**(附件类目张数超上限 `exterior` > 6
```json
{ "code": 601003, "msg": "该类目张数已达上限", "data": null }
```
**错误码**400 参数校验 / **600100** 车牌已存在 / **600107** VIN 已存在 / **600108** 车型不存在 / **601002** 附件类目非法 / **601003** 类目张数达上限 / 401 未登录
---
### 3.4 编辑(附件 diff 语义)
**PUT** `/admin/fleet/vehicles/{vehicleId}`
- **使用场景**:编辑车辆基本信息 + 附件增删改一次性提交
- **认证**:管理后台 JWT
- **幂等**:是
**路径入参**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `vehicleId` | Long | ✅ | 车辆 ID |
**Body 入参**:同 §3.3(共用 `VehicleSaveReqVO`
**附件 diff 语义**
| 入参 attachments[] 项 | DB 中的对应行 | 行为 |
|---|---|---|
| 有 `id` + DB 命中 | 存在 | **保留**(可更新 sortNo |
| 无 `id`(新增项) | — | **INSERT** |
| — | 存在但本次未传 | **软删** |
**出参**`Result<VehicleUpdateRespVO>`
| 字段 | 类型 | 说明 |
|---|---|---|
| `kept` | int | 保留的附件数 |
| `inserted` | int | 新插入的附件数 |
| `softDeleted` | int | 软删的附件数 |
**典型示例 响应**
```json
{
"code": 200,
"data": { "kept": 2, "inserted": 1, "softDeleted": 1 },
"msg": "成功"
}
```
**错误码**400 参数校验 / **600100** 车牌已存在 / **600107** VIN 已存在 / **600108** 车型不存在 / **600110** 车辆不存在 / **601002** 类目非法 / **601003** 类目张数达上限 / 401 未登录
---
### 3.5 软删
**DELETE** `/admin/fleet/vehicles/{vehicleId}`
- **使用场景**:下架车辆
- **认证**:管理后台 JWT
- **幂等**:是
**路径入参**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `vehicleId` | Long | ✅ | 车辆 ID |
**出参**`Result<Void>``data: null`
**典型示例 响应**
```json
{ "code": 200, "data": null, "msg": "成功" }
```
**异常 请求**(车辆有未完成派单 —— **业务流水 PR 落地后才生效**
```json
{ "code": 600109, "msg": "车辆有未完成派单,不能删除", "data": null }
```
**错误码****600109** 有未完成派单(占位,业务流水 PR 上线后生效)/ **600110** 车辆不存在 / 401 未登录
---
### 3.6 Excel 批量导入
**POST** `/admin/fleet/vehicles/import`
- **使用场景**:运营批量录入车辆
- **认证**:管理后台 JWT
- **幂等**:否(重复行按车牌冲突拒绝单行)
**入参**multipart/form-data
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `file` | file | ✅ | Excel`.xlsx` / `.xls`)或 CSV |
**表头约定(第 1 行 12 列,顺序固定)**
```
车牌 | 车型型号ID | 车队(own/coopA/coopB) | 车架号(VIN) |
注册日期(yyyy-MM-dd) | 年检到期日(yyyy-MM-dd) |
保险公司 | 保单号 | 保险到期日(yyyy-MM-dd) |
行驶证号 | 行驶证所有人 | 使用性质
```
**出参**`Result<VehicleImportRespVO>`
| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | int | 总行数(不含表头) |
| `success` | int | 成功导入数 |
| `failed` | int | 失败行数 |
| `errorRows[]` | array | 失败行详情 |
| `errorRows[].row` | int | Excel 行号(从 2 开始,1 为表头) |
| `errorRows[].plate` | string | 车牌(用于定位) |
| `errorRows[].msg` | string | 错误信息(如"车牌已存在" |
**典型示例 响应**
```json
{
"code": 200,
"data": {
"total": 10,
"success": 8,
"failed": 2,
"errorRows": [
{ "row": 3, "plate": "蒙A-88888", "msg": "车牌已存在" },
{ "row": 7, "plate": "蒙A-99999", "msg": "车型型号 ID 不存在" }
]
},
"msg": "成功"
}
```
**错误码**400 文件解析失败 / 401 未登录
---
## 6. 枚举 / 数据字典
### 6.1 附件类目(`category``VehicleAttachmentCategoryEnum`
**所属字段**`VehicleAttachmentReqVO.category` / `VehicleAttachmentRespVO.category` | **类型**`String` | **必填**:✅
**真枚举**(后端硬编码,含同类目张数上限):
| 值 | 中文 | 张数上限 |
|---|---|---|
| `exterior` | 车辆外观 | 6 |
| `interior` | 车辆内饰 | 3 |
| `insurance` | 保险单据 | 2 |
| `inspection` | 年检单据 | 2 |
| `operation_license` | 营运证 | 2 |
| `other` | 其他附件 | 5 |
> 超出上限触发 **601003** "该类目张数已达上限"。
### 6.2 车队(`fleet`
**所属字段**`VehicleSaveReqVO.fleet` / `VehiclePageReqVO.fleet` / `VehiclePageItemRespVO.fleet` | **类型**`String` | **必填**:✅(新增时)
**`@Pattern` 强校验枚举**
| 值 | 含义 |
|---|---|
| `own` | 自营车辆 |
| `coopA` | 合作车队 A |
| `coopB` | 合作车队 B |
### 6.3 车辆状态(`vehicleStatus`
**所属字段**:同 `fleet` | **类型**`String` | **必填**:❌(默认 idle
**`@Pattern` 强校验枚举**
| 值 | 含义 |
|---|---|
| `idle` | 空闲 |
| `busy` | 在途 |
| `maint` | 维修中 |
---
## 7. 错误码(全模块汇总)
| code | 含义 | 触发条件 | 涉及接口 |
|---|---|---|---|
| 400 | 参数校验失败 | 字段缺失/长度/枚举值不合法/MIME 非法 | 所有写入接口 |
| 401 | 未登录 | JWT 无效或缺失 | 全部 6 接口 |
| **600100** | 车牌已存在 | `plate` 全局唯一约束 | §3.3 §3.4 |
| **600107** | VIN 已存在 | `vin` 全局唯一约束 | §3.3 §3.4 |
| **600108** | 车型不存在 | `vehicleModelId` 无效或软删 | §3.3 §3.4 |
| **600109** | 有未完成派单,不能删除 | 业务流水 PR 上线后才触发(目前占位) | §3.5 |
| **600110** | 车辆不存在 | `vehicleId` 无效或软删 | §3.2 §3.4 §3.5 |
| **601001** | 实体不存在 | 附件关联的主车辆不存在 | §3.3 §3.4 |
| **601002** | 附件类目非法 | `category` 不在枚举内 | §3.3 §3.4 |
| **601003** | 该类目张数已达上限 | 超出 §6.1 张数上限 | §3.3 §3.4 |
| **601004** | 文件超出大小限制 | 应用层 + OSS HEAD 校验 | §3.6导入,§3.3/§3.4 主体不直接校验 |
| **601005** | 文件类型非法 | MIME 不在白名单 | §3.3 §3.4 |
> 错误码段位 600100-600110 归属车辆档案;601001-601005 归属附件管理(车辆/司机共用)。
>
> **注意:与文档 v1.4 的差异** —— 文档原计划 VIN 用 600101 / 车型不存在 600102 / 派单冲突 600103,但因这 3 个段位被车型管理库占用(见 §1 changelog,实际代码避让到 **600107/600108/600109**。本 changelog 反映**代码实际值**。
---
## 9. 业务边界
- ✅ **附件 diff 语义统一**§3.3 新增时所有项无 id;§3.4 编辑时按 `id` 命中度决定保留/插入/软删
- ✅ **sortNo 自动重写**:附件 `sortNo` 不传时按入参顺序自动写 0..N-1同 category 内 0-based
- ✅ **行驶证 OSS URL 留主表**`regCertFrontUrl` / `regCertBackUrl` 是主表字段(不走附件表),与附件分离
- ✅ **唯一约束**`plate` / `vin` **全局**唯一(违反抛 600100 / 600107
- ✅ **车队 / 状态强枚举**`fleet` `vehicleStatus``@Pattern` 校验,违反返 400
- ⚠️ **primaryDriverName 占位**§3.2 详情返 `primaryDriverName: null`,待业务流水 PR 上线后注入司机姓名
- ⚠️ **派单删除保护占位**§3.5 错误码 600109 在业务流水 PR 上线后才会真触发
---
## 11. 影响评估
- **是否破坏向后兼容**:否(接口契约自首次落地以来未变更)
- **前端是否必须同步上线**:否(本次仅补发 changelog,无代码改动
- **影响已有数据**:无
---
## 12. 注意事项
- **分页参数名**`page` / `pageSize`**不是** pageNo
- **Long 主键序列化**`id` / `vehicleModelId` / `vehicleTypeId` / `attachments[].id` 等 Long 字段 JSON 返回为 String,前端**不要**当 Number 解析
- **错误码不连续**600100 / 600107-600110中间 600101-600106 是车型管理库的段位),见 §7 注解
- **附件 URL 不可改**§3.4 编辑时 `attachments[].url` 不可改(后端忽略),只能保留 / 新增 / 软删
- **`primaryDriverName` 当前恒为 null**:业务流水 PR 上线前前端**不要**展示该字段或处理为"未指派"
---
## 13. 关联 / 联系人
### 13.1 链接
- **API 文档**: `docs/order-v3/api/API-SPEC-FLEET-V1.5.html` §2 车辆档案
- **DB 文档**: `docs/order-v3/database/DATABASE-SCHEMA-FLEET-V1.5.html` §2.3 / §2.6
- **同期相关 changelog**:
- 车型管理库 9 接口([`21_2785_车型管理库-新增接口-管理后台.md`](./21_2785_车型管理库-新增接口-管理后台.md)
- 司机档案 6 接口([`21_2791_司机档案-修改接口-管理后台.md`](./21_2791_司机档案-修改接口-管理后台.md)
### 13.2 联系人
- **后端负责人**: @yst(腰苏图)
- **前端对接(管理后台)**: 待指派

查看文件

@ -1,119 +0,0 @@
# Round 1 review 跟进:HOUSE 砍 1198 行 + 红线整改反向修复 + product-v2 简化 + 出行人/大交通清理
> **存放目录**: 二期(`order-v3` 标签)→ `changelogs-v2/2026-05/`
> **服务**: hl-user-service + hl-product-service-v2 + hl-resource-service + hl-mp-service + hl-gateway + hl-order-service-v3
> **PR**: #2845 / #2846 / #2847 / #2848
> **Issue**: #2841 / #2842 / #2843 / #2844
> **日期**: 2026-05-22
> **影响范围**: 多服务接口契约**无破坏性变更**,内部代码简化为主,但**操作日志 changes 详情格式从展开改为聚合**
---
## ⚠️ 关键变化(前端需关注)
### 1. 🔴 产品操作日志 `changes` 详情格式收敛(影响:运营后台展示)
PR #2847`EntityFieldDiffUtil.diffCollectionDetailed`(86 行 8 参展开)简化回 `diffCollection` 聚合:
**前**:每个节点 ADD/DELETE/MODIFY 单独一条 changes(改一天 5 节点 5 条),超 30 条裁断"其他 N 项变更未展开"
**后**:聚合为 `共 N → 共 M (新增 X / 删除 Y)` 单条 summary
前端 `/admin/product/operation-log/search` 接口返回的 `changes` JSON 字段格式简化,**展示"动了哪个节点"如有需求需后端补 detail 端点或前端拿原 nodeList diff 渲染**。
### 2. 🟡 大交通 `TransportPlan.mode` 不再有 Converter 默认值
PR #2848`TransportPlanConverter.fromAdminReq` 中硬编码的 `mode="TOGETHER"`。前端如果之前 add/edit 不传 mode 依赖后端默认 TOGETHER 仍能工作(addTransportPlan Service 显式补);**batchReplace 后端显式 SEPARATE**,与文档 §2.6.2 一致。
### 3. 🟡 换酒店 DRIVER 通知暂时撤回(等 FLEET 接通再补)
PR #2848 撤回 `HOUSE_HOTEL_SWAPPED_DRIVER` event 发布 + Dispatcher 删 DRIVER case + V20260522_001 SQL 删 notification_event_config 行。**司机暂不再收到换酒店通知**,等 FLEET vehicle_plan 反查 Feign 工单接通后一次性补 publisher + V*.sql + dispatcher 三件套(独立工单)。
---
## 一、背景
dev-v3 5 天工作流 Round 1 review(2026-05-22),6 agent 并发对 05-16~05-21 wx 提交 50+ PR 复审,维度加 "**反过度设计 + 简洁可读**" 专项,产出 10 P0 + 30+ P1,打包 4 张工单 #2841-#2844 → 4 个 dev agent 并行修 → 4 张 PR squash 合 dev-v3 → 测试服全部署生效。
**总收益**:HOUSE 模块净 -1198 行,product-v2 净 -198 行,user/resource/mp 错误码段位收敛,4 处 Service 内 Wrapper 下沉 Mapper default。**零功能损失,代码可读性显著提升**。
---
## 二、关键改动汇总
### #2845 (Closes #2841) — 红线整改反向违规 + 错误码去重 + Object 字段
- **PR #2439 红线 5 整改反向引入红线 1+4 修复**:LoginLogService / UserService / AdminUserService 4 处 Service 内构造 Wrapper 下沉到 Mapper default
- **CRYPTO/MD5 错误码 4 处去重**:全部复用 `SystemErrorCode.CRYPTO_ALGORITHM_UNAVAILABLE (900102)`
- **BusinessException 加 cause 重载**:JwtAuthFilter/VariFlight/Amap/WxCrypt 异常带 cause 透传堆栈
- **PR #2467 DASHBOARD 4 错误码合并** → 复用 `CommonErrorCode.FEIGN_CALL_FAILED`
- **PR #2430 7 枚举错误码合并** → 复用 `CommonErrorCode.INVALID_PARAM`
- **MpTripDetailVO.suppliesList/contracts/insurances**:`Object``List<Map<String, Object>>`;`dailyNodes``@Deprecated`
- **删纯透传 facade**:AdminUserService.countAll/listByAdminIds + UserService.countAll
- **决策**:不新增 ErrorCode 常量(用户铁规"不引入新错误码");User 未启用 @TableLogic 保留 `isNull(deletedAt)`
### #2846 (Closes #2842) — HOUSE 砍死代码 -1198 行
- 删 **3 个零订阅 Event POJO**:EnrollChangedEvent / OrderCancelledHouseEvent / RoomChangedEvent
- 删 **publishHotelSwapped MQ 链路**(零订阅 topic + HotelSwappedEvent POJO + TOPIC_HOTEL_SWAPPED + RocketMQTemplate 注入)
- 合并 **4 个薄 Service**:HouseStateChangeService → HouseStateMachineHelper / HouseInquiryTimeoutService → HouseInquiryService / HouseAssignmentInternalQueryService → HouseAssignmentService / HouseHotelSwapAdapter → HouseHotelSwapServiceImpl
- **HouseFeignErrorCode 整段并入 HouseInternalErrorCode**(段位重叠 + 语义重复)
- **HouseInquiryErrorCode 595xxx → 808xxx 段位**
- HouseE2EWalkthroughIT 8 个 @Test`Assertions.fail("待 QA 工单 Hxx")`
- 删死状态机:CUSTOMER_CHANGE 5 from(main 零 fire)+ SWAP_HOTEL_COMMIT CLAIMING→CLAIMING 自循环
- 删 HouseInventoryCheckedEvent + publish 调用(零消费方)
- swap 触发收紧:fireSwapStateChange 只在 OPEN SWAP_HOTEL/REFUND 待办存在时触发
### #2847 (Closes #2844) — product-v2 简化 + 餐饮 facade 缓存
- **EntityFieldDiffUtil.diffCollectionDetailed 简化**(86 行 → 删,回 diffCollection 聚合)
- **ProductItineraryService.buildItineraryChanges 同步简化**(-63 行)
- **ProductSnapshotService.restoreSnapshot 加 @Lock4j(keys="#productId")**
- **FieldLabelDict 抽 dict(String...) helper**(-28 行)
- **product-v2 改动文件 javadoc HTML 标签清零**(铁规 15h)
- **ProductDetailAggregator.toOperationLogVO 删,改调 ProductOperationLogService.toRespVO**(消除双份实现)
- **ProductItineraryAggregateFacade meal_option 缓存**(7 天产品 Feign 21 次 → 0 次,类成员 volatile Map + @PostConstruct + @Scheduled 5min)
### #2848 (Closes #2843) — HOUSE 收尾 + 出行人/大交通清理
- **DRIVER 通知占位 noop 撤回**(publisher + dispatcher + V20260522_001 SQL DELETE config 三件套)
- HouseAssignmentService.maskPhone 改调 **复用现有 PiiMaskUtil**(KISS)
- **resolveSwapFromState 合并双查为单 IN 查询**(HouseTodoMapper.selectOpenByOrderAndTypes)
- **HouseAssignmentService.submit hotelName 接通 HouseHotelCacheService 真实拉**(空时落 null 不再写中文占位"待拉取酒店主数据")
- **HouseInquiryService.writeOpLog 签名加 operatorName 参数**,5 调用方显式传(MQ/@Async 场景拿到真实姓名)
- **TravelerConverter 删 2 个 0 调用 toInternalVOList 重载**(单参 + 双参)
- **TravelerErrorCode 删 4 个 0 调用错误码**(581117/581124/581126/581133)+ 581121 @Deprecated 指向 581144
- **TransportPlanConverter.fromAdminReq 不硬编码 mode**(Service 显式传)
- ProductSnapshotService javadoc 措辞修正
---
## 三、变更接口清单(仅前端可感知)
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 产品操作日志 search | GET | `/admin/product/operation-log/search` | **响应字段格式收敛** | `changes` 数组中 collection-diff 由"展开 N 条节点级"改为"1 条聚合 summary" |
| 2 | 大交通 add/edit | POST/PUT | `/v3/admin/order/{id}/transport-plan/*` | **不变** | mode 字段后端 Service 显式设默认值,前端可继续不传 |
| 3 | HOUSE 换酒店通知 | (后端事件) | - | **DRIVER 暂撤** | 司机收不到换酒店通知,等 FLEET Feign 接通后恢复 |
---
## 四、测试服验证状态
| 服务 | 部署 commit | uptime | 验证 |
|------|------------|--------|------|
| hl-user-service | dev-v3 含 #2845/#2848 | 20:40 | ✅ V20260522_001 success=1 + DRIVER config 已删 |
| hl-product-service-v2 | dev-v3 含 #2845/#2847/#2848 | 20:41 | ✅ Spring 启动正常 |
| hl-resource-service | dev-v3 含 #2845 | 20:42 | ✅ |
| hl-mp-service | dev-v3 含 #2845 | 20:43 | ✅ |
| hl-gateway | dev-v3 含 #2845 | 20:44 | ✅ |
| hl-order-service-v3 | dev-v3 含 #2846/#2848 | 20:46(SSH restart 兜底) | ✅ HOUSE 接口活着(grab-pool/inquiry 返 200/400 合规) |
---
## 五、留尾(独立工单后续)
- **HOUSE-SWAP DRIVER 通知 wework 真值切换** → FLEET `vehicle_plan` 反查 Feign 接通后补 publisher + V*.sql + dispatcher 三件套
- **HOUSE-FIX-A `loadProductPool` 真实化** → H04 接通 `ProductV2FeignClient.getOrderProductId`
- **#2515 MaterialCategory ↔ Permission 双 @Lazy 循环依赖** → 抽 `PermissionGate` 接口重构(大改动 backlog)
- **EquipmentTemplateService ↔ ProductLineService 双 @Lazy** → 抽 `ProductEquipmentBindingService`(backlog)
---
## 六、关联
- 工单: #2841 / #2842 / #2843 / #2844(全部 closed)
- PR: #2845 / #2846 / #2847 / #2848(全部 merged into dev-v3)
- Review 来源: Claude /@cr Round 1 6 agent 并发 2026-05-22(维度:反过度设计 + 简洁可读)

查看文件

@ -1,88 +0,0 @@
# Round 2 review 收尾 7 项:Wrapper 漏修 + LockFailure 500 + JavaDoc 旧编号 + 孤儿枚举 + Publisher rename + 脱敏收敛 + Mapper default 单测
> **存放目录**: 二期 → `changelogs-v2/2026-05/`
> **服务**: hl-user-service + hl-common-core + hl-starter-protection + hl-order-service-v3
> **PR**: #2850
> **Issue**: #2849
> **日期**: 2026-05-22
> **影响范围**: 全部内部代码简化 / 无破坏性变更 — 但 **HouseEventPublisher → HouseNotificationPublisher rename + LockFailureException 全局新错误码**
---
## ⚠️ 关键变化(前端可感知)
### 1. 🟡 @Lock4j 抢锁失败错误码 100503 RESOURCE_LOCKED
之前 `@Lock4j(acquireTimeout=5s)` 抢锁失败抛 `LockFailureException` 上抛 GlobalExceptionHandler 兜底返 500,前端体验差。
**现**:全局 `LockFailureExceptionHandler` 拦截返:
```json
{ "code": 100503, "message": "资源被占用,请稍后重试", "success": false }
```
**生效范围**:全部 @Lock4j 注解(目前主要 ProductSnapshotService.restoreSnapshot,后续扩展)。前端如有错误码映射需识别 100503 显示"操作冲突,请稍后重试"提示。
### 2. 🟢 Bug 修复:漏修的 listCustomers Wrapper(后端内部)
`hl-user-service` listCustomers Service 内 Wrapper 残留下沉到 `UserMapper.selectCustomerPage` default。前端无感知。
---
## 一、变更接口清单
| # | 接口 | 变更类型 | 说明 |
|---|------|----------|------|
| 1 | 所有 @Lock4j 注解接口(目前 `/admin/product/snapshot/restore`) | **错误码新增** | 抢锁失败返 `100503 RESOURCE_LOCKED` 而非 500 |
| 2 | `/admin/customer/page` 等 | **内部重构** | listCustomers Wrapper 下沉到 Mapper default(前端响应不变) |
---
## 二、关键改动(内部)
1. **`hl-common-core.CommonErrorCode`** 新增 `RESOURCE_LOCKED(100503, "资源被占用,请稍后重试")`
2. **`hl-starter-protection.LockFailureExceptionHandler`** 新增全局 `@RestControllerAdvice` 拦截 `LockFailureException` 返 100503
3. **`HouseEventEnum.CUSTOMER_CHANGE`** 孤儿常量删除(PR #2846 删了对应状态机规则,枚举常量是孤儿)
4. **`HouseEventPublisher``HouseNotificationPublisher`** rename(单职责后类名误导)— 9 处引用 + 3 IT @MockBean 同步
5. **`ComplaintConverter`** 手工 substring 脱敏 → `PiiMaskUtil.maskPhone()`(删 13 行)
6. **PR #2846 8 处 JavaDoc 旧编号/类名更新**(808990→808900 / 808994→808901 / 808993→808941 / `RESOURCE_SERVICE_UNAVAILABLE``RESOURCE_UNAVAILABLE` / `INVENTORY_DEDUCT_FAILED``ROOM_STOCK_INSUFFICIENT`)
7. **5 个新 Mapper default 单测**:LoginLogMapperTest / UserMapperTest / AdminUserMapperTest(8 测试)
---
## 三、测试服验证
| 服务 | 部署 commit | uptime | 验证 |
|------|------------|--------|------|
| hl-user-service | dev-v3 含 #2850 | 21:34 | ✅ Wrapper 下沉 + 新 Mapper default 单测全绿 |
| hl-order-service-v3 | dev-v3 含 #2850 | 21:36(SSH restart) | ✅ Publisher rename 后启动正常 |
---
## 四、累计 Round 1 + Round 2 收益
| 维度 | 数值 |
|------|------|
| PR 数 | 5 张(#2845 / #2846 / #2847 / #2848 / #2850) |
| 工单数 | 5 张(#2841 / #2842 / #2843 / #2844 / #2849) |
| 净行数 | **-1359(R1) + +311(R2 含 5 新测试文件)= 净 -1048 行** |
| HOUSE 模块评分 | C+ → **A-** |
| 整体评分 | Round 1 A- → Round 2 后整体 **A** |
| 单测数 | 2579 + Round 1 各服务 全绿 |
---
## 五、留尾(独立工单 backlog)
- **product-v2 全模块 294 文件 HTML 标签清零**(P0 铁规 15h 全局)→ 大工程独立工单
- **MpTripDetailVO Object → List<Map> 真实数据回归** → /@qa 工单
- **AdminUserService 9 处跨聚合 Mapper(红线 5 二期)** → 独立工单
- **HOUSE FLEET vehicle_plan Feign 接通后补 DRIVER 通知三件套** → 独立工单
- **MaterialCategory ↔ Permission 双 @Lazy 抽 PermissionGate** → backlog
- **EquipmentTemplateService ↔ ProductLineService 双 @Lazy 抽 ProductEquipmentBindingService** → backlog
---
## 六、关联
- 工单: #2849(closed)
- PR: #2850(merged)
- Round 2 review 来源: Claude /@cr 3 agent 并发(A/B/C)2026-05-22
- **memory P0 铁规 7 升级**:agent 严禁 git stash(同周 3 次违规累计 4+ 次)

查看文件

@ -1,96 +0,0 @@
# Round 3 收尾 13 项:TokenService 灰度删 + swap 状态机假阳性修复 + PiiMaskUtil 三套合一 + youngChild 术语统一 + 杂项
> **服务**: hl-user-service + hl-product-service-v2 + hl-fleet-service + hl-order-service-v3 + hl-common-core + hl-starter-protection
> **PR**: #2853
> **Issue**: #2851
> **日期**: 2026-05-22
> **影响**: 内部 / 注释 / 工具类合一 — **司机/出行人 PII 脱敏掩码格式微调,前端需注意**
---
## ⚠️ 关键变化(前端需关注)
### 1. 🟡 司机/出行人 PII 脱敏掩码格式微调(`fleet maskIdCard`)
**前**:`150102******1234`(6 位星固定长度)
**后**:`150***********1234`(SensitiveDataUtil 变长星)
**影响**:司机 / 出行人列表 `idCardNoMasked` 字段长度从 18 字符变 17 字符(具体看身份证长度差),前端如果有断言式校验需放宽。**业务语义不变,仍是 PII 脱敏**。
### 2. 🟡 "幼儿数 / 幼童" → 统一改"小童"(术语)
跨 7 个 VO/Service:`OrderCreateReqVO` / `OrderMainVO` / `ProductFeignClient` / `GroupOrderStrategy``@ApiModelProperty` 和 javadoc 中"幼儿数"、"幼童"全统一为"小童"。**字段名 `youngChildCount` 不变**,只是中文标签统一。前端如展示了原"幼儿数"或"幼童"字样,改为"小童数"即可。
### 3. 🟢 @Lock4j 抢锁失败错误码扩展(后端)
之前 R2 加的 `LockFailureExceptionHandler` 只拦 `LockFailureException`,现在改拦父类 `LockException`,覆盖 lock4j Redis lua 异常 / 释放锁失败等所有 lock4j 异常,统一返 `100503 RESOURCE_LOCKED`
---
## 一、必修 13 项(6 P0 + 7 P1)
### P0
1. **TokenService 灰度 fallback 删**(2026-05-22 到期):删 8 行旧 key 兼容 + 对应单测
2. **HouseHotelSwapServiceImpl.isInExceptionState 假阳性修复**:`findOpenByOrderAndTypes` 仅查 `SWAP_HOTEL`,REFUND 剔除(REFUND OPEN 待办与 swap 无关,避免误 fire `EXCEPTION→CLAIMING`)
3. **RefundReconcileJob.java:67 Wrapper 下沉**:Job 内 `new LambdaQueryWrapperX``RefundRecordMapper.selectPendingForReconcile` default
4. **youngChildCount 中文名统一「小童」**(7 文件)
5. **PiiMaskUtil 三套合一 → SensitiveDataUtil**:
- 删 `hl-order-service-v3/.../shared/util/PiiMaskUtil.java`
- 删 `hl-fleet-service/.../shared/util/PiiMaskUtil.java`
- `SensitiveDataUtil` 新增 `maskRawLine` 方法收编 v3 正则
- v3 + fleet 共 11 处调用方迁移
- **v2 PiiMaskUtil 保留(legacy 模块,非本工单范围)**
6. **过时 TODO + 占位日志清理**:MaterialDashboardService TODO 删 / ProductDayHotelService + GroupTourBatchService log.warn("[TODO]") → log.debug
### P1
7. `LockFailureExceptionHandler` 拦父类 `LockException`
8. `HotelRequirementMapper.@Select` 删 → `HouseAssignmentService.countConfirmedByRequirement` 暴露
9. `@Autowired` → 构造器注入(AgencyProperties + RefundReviewService)
10. `TravelerErrorCode.TRANSPORT_PLAN_NOT_BELONG_TO_ORDER (581121)` 0 调用 @Deprecated 删(9 行)
11. `HouseStateMachineConfig:154` 重复叙事精简
12. `RouteMapCacheService:27` 主观词"性能优化" → "缓存减少对高德 API 重复请求"
13. `HouseAssignmentService` 两处 TODO 格式统一为 `#2729-followup`
---
## 二、测试服验证
| 服务 | 部署 commit | uptime | 验证 |
|------|------------|--------|------|
| hl-user-service | dev-v3 含 #2853 | 22:30 | ✅ TokenService 灰度 fallback 已删 |
| hl-product-service-v2 | dev-v3 含 #2853 | 22:31 | ✅ |
| hl-fleet-service | dev-v3 含 #2853 | 22:32 | ✅ |
| hl-order-service-v3 | dev-v3 含 #2853 | 22:33(SSH restart) | ✅ |
单测:5 模块全绿(common 463/463 / user 2433/2433 / product-v2 1300/1300 / fleet 178/178 / order-v3 1911/1928 — 17 失败全 baseline 预存)。
---
## 三、Round 1 + 2 + 3 累计成果
| 维度 | Round 1 | Round 2 | Round 3 | 累计 |
|------|---------|---------|---------|------|
| 工单 | #2841-#2844 | #2849 | #2851 | 6 张 |
| PR | #2845-#2848 | #2850 | #2853 | 6 张 |
| 净行数 | -1359 | +311 | -130 | **-1178** |
| HOUSE 评分 | C+ → A- | A- | A | C+ → **A** |
| 整体评分 | C+ → A- | A- → A | A → **A+** | A+ |
---
## 四、留尾(独立工单 backlog,**本轮明确不做**)
- **#2852 HTML 标签全模块清零 3010 处**(大工程独立)
- MpInternalRefundController 444 行 BFF Facade 抽离
- 9 个 Service+Impl 单实现合并(refund/house/agency)
- 巨型 private 方法独立 Builder/Assembler
- 720000/798000 段位上移 CommonErrorCode
- Long ID @JsonSerialize Mixin 全局
- AdminUserService 红线 5 二期(跨聚合 Mapper)
- 手工金额格式化抽 MoneyUtil
- v2 PiiMaskUtil legacy 清理
---
## 五、关联
- 工单: #2851(closed)
- PR: #2853(merged)
- Round 3 review 来源: Claude /@cr 4 agent 并发(A 注释严谨/B 逻辑准确/C 残留过度设计/D 跨模块一致性)2026-05-22

查看文件

@ -1,118 +0,0 @@
# Backlog 五连发:HTML 标签清零 + BFF Facade + 红线 5 二期 + MoneyUtil + Service+Impl 合并
> **服务**: 全模块(hl-common-core / hl-user-service / hl-order-service-v2 / hl-order-service-v3 / hl-product-service-v2 / hl-fleet-service / hl-mp-service)
> **PR**: #2858 / #2859 / #2860 / #2861 / #2862
> **Issue**: #2852 / #2854 / #2855 / #2856 / #2857
> **日期**: 2026-05-22
> **影响**: **0 个 URL 变更 / 0 个 VO 字段变更 / 0 个响应结构变更** — 全部纯重构,**前端无任何感知/无需改代码**
---
## ⚠️ 关键变化(前端可感知)
**无**。本次 5 PR 是 Round 3 留尾 backlog 集中清理,目标是代码质量 + 工程纪律,**前端零影响**:
- URL 全保留(#2861 BFF Facade 拆分时 100% URL 一致)
- VO 字段全保留(#2858 红线 5 二期只动 Service 内部 Mapper 调用)
- 响应结构全保留
- 字典/字段命名零变更
---
## 一、5 PR 改动概览
### PR #2858 红线 5 二期(`refactor(redline5)`)
**Closes #2855** — AdminUserService 跨聚合 Mapper 全迁移到 Service。
- 3 个跨聚合 Mapper 下沉:SysRoleMapper → SysRoleService / WechatUserMapper → WechatUserService(新建) / WechatDepartmentMapper → WechatDepartmentService(新建)
- 新增 Service 方法 5 个 + Mapper default 1 个
- AdminUserService Mapper 注入数 3 → 1(只剩自家 AdminUserRoleMapper)
- 净 +149-103 + 4 个新文件 +344 行(2 新 Service + 2 新 Service 单测)
- 测试:2448 用例全绿
### PR #2859 MoneyUtil 抽取(`refactor(money-util)`)
**Closes #2856** — hl-common-core 新增 MoneyUtil + 全模块迁移。
- `MoneyUtil` 4 方法:formatYuan / toFen / toYuan / parseYuan(105 行 + 单测 135 行 15 例)
- 迁移 10 处(7 formatYuan + 3 toFen):TencentEsign × 2、Tourage × 2、ContractCreateService、NotificationEventHelper、InternalProductService(formatPriceLabel)、PaymentService.yuanToFen、RefundReconcileJob.yuanToFen、RefundChannelService.yuanToFen
- 保留 17 处(语义不匹配:经纬度 setScale / 百分比 .divide(100) / ROUND_DOWN 舍入 / setScale 截 BigDecimal 等)
- 净 +48-38 + 2 新文件 +240 行
- 测试:order-v2 + product-v2 + order-v3 共 3193 + 1886 用例全绿
### PR #2860 Service+Impl 合并(`refactor(yagni)`)
**Closes #2857** — 8 个单实现 Service+Impl 合并(YAGNI 反过度设计)。
- 标准 interface + Impl 合并 3 个:TravelAgencyPaymentService / TravelAgencyQualificationService / HouseHotelSwapService
- 删除"命名兼容层"5 个:RefundAppealServiceImpl / RefundApplyServiceImpl / RefundOrchestrationServiceImpl / RefundReviewServiceImpl / RefundStatusLogServiceImpl(均为 extends RefundXxxService 的空子类,只为测试 new 用)
- 严格按 4 条件筛选(MockBean / Feign / Fallback 全排除)
- 净 -244 行(+1054 -1298)
- 测试:6 关键测试类 102 用例全绿
### PR #2861 BFF Facade 抽离(`refactor(refund-bff)`)
**Closes #2854** — MpInternalRefundController 444 → 107 行(76% 压缩) + 3 Facade。
- 新建 3 Facade(全部 @Service + @RequiredArgsConstructor):
- RefundQueryFacade(265 行,4 查询:preview / reasons / detail / progress + VO 转换)
- RefundApprovalFacade(125 行,2 客户决策:customerAction / directAppeal)
- RefundExecutionFacade(118 行,1 执行:createApplication)
- Controller 107 行(略超 100,7 端点参数声明无法再压)
- 26 新单测(Query 13 + Approval 7 + Execution 6) + Controller 测试重写 14 例 = 40 全绿
- **URL 100% 一致**(8 Mapping:1 类级 + 7 端点)BFF 前端无感知
- 净 +119-680 改 + 1255 新文件(3 Facade 508 + 3 Test 747)≈ 净 +694 行(主要单测翻倍)
### PR #2862 HTML 标签清零(`refactor(html-cleanup)`)
**Closes #2852** — P0 铁规 15h(代码注释禁用 HTML 标签)5+ 月历史债清零。
- 全工程 Java 注释 HTML 标签 → markdown:`<p>` → 空行 / `<b>``**` / `<li>``- ` / `<br>` → 换行 等
- 命中前 **8709 处** → 命中后 **57 处**(全部字符串字面量,如 AuthController/FileService 返回 HTML 模板,必须保留)
- **0 处在注释里**(grep -v 字符串验证)
- 改动 **1589 文件**,**+8764 -8764 完全对称**(纯字符替换,业务代码 0 改动,字节码不变)
- mvn compile 24 模块全绿(javadoc 严格检查通过)
- mvn test 7629 测试,17 失败全部 baseline 与本工单无关(ArchUnit / OrderDetailIntegrationTest H2 schema)
---
## 二、累计成果(Round 1 + 2 + 3 + Backlog R4)
| 维度 | R1 | R2 | R3 | **R4 Backlog** | 累计 |
|------|-----|-----|-----|---------------|------|
| 工单 | #2841-#2844 | #2849 | #2851 | **#2852/#2854/#2855/#2856/#2857** | **11 张** |
| PR | #2845-#2848 | #2850 | #2853 | **#2858-#2862** | **11 张** |
| 净行数 | -1359 | +311 | -130 | **+0(HTML 0)/+839(其他 4)** | ~净 **-340 行** |
| HOUSE 评分 | C+ → A- | A- | A | **A**(无 HOUSE 改动) | C+ → **A** |
| 整体评分 | A- | A | A+ | **A+**(纪律完结) | **A+** |
---
## 三、测试服验证
| 服务 | 部署 commit | uptime | 验证 |
|------|------------|--------|------|
| hl-user-service | dev-v3 含 5 PR | 待部署 | 待验 |
| hl-product-service-v2 | dev-v3 含 5 PR | 待部署 | 待验 |
| hl-order-service-v2 | dev-v3 含 5 PR | 待部署 | 待验 |
| hl-order-service-v3 | dev-v3 含 5 PR | 待部署(SSH restart) | 待验 |
| hl-fleet-service | dev-v3 含 5 PR | 待部署 | 待验 |
| hl-mp-service | dev-v3 含 5 PR | 待部署 | 待验 |
**Round 3 QA 全流程报告**:9 链路全 PASS / 0 BLOCKER / 0 WARN(dev-v3 b352b3036 测试,本批 5 PR 是纯重构,QA 结果同等可信)
---
## 四、留尾 backlog(独立后续工单,**本轮明确不做**)
- AdminUserService 红线 5 三期(剩余跨聚合 Mapper,如有)
- MpTripDetailVO Object → List<Map> 真实数据回归 → /@qa 工单
- HOUSE FLEET vehicle_plan Feign 接通后补 DRIVER 通知三件套
- MaterialCategory ↔ Permission 双 @Lazy 抽 PermissionGate
- EquipmentTemplateService ↔ ProductLineService 双 @Lazy 抽 ProductEquipmentBindingService
- 720000/798000 段位上移 CommonErrorCode
- Long ID @JsonSerialize Mixin 全局
---
## 五、关联
- 工单: #2852 / #2854 / #2855 / #2856 / #2857(全部 closed)
- PR: #2858 / #2859 / #2860 / #2861 / #2862(全部 squash merged)
- Round 4(backlog 集中清理)来源: 用户 5 月 22 日"继续任务"清单

查看文件

@ -1,146 +0,0 @@
# QA-D 文档驱动 + QA-E 字段反查发现 4 个 P0/P1 真实 bug 修复
> **服务**: hl-gateway + hl-order-service-v3
> **PR**: #2864 / #2866
> **Issue**: #2863 / #2865
> **日期**: 2026-05-22
> **影响**: **5 个原本经网关 404 的 v3 接口现在通了** + **list/detail 字段静默不一致 2 bug 修复**
---
## ⚠️ 关键变化(前端必读)
### 1. 🔴 订单列表 `tags` 字段类型变更(breaking change,**前端必须同步改 hl-ui**)
**之前**:`tags` = `string[]``['VIP', '高净值']`
**之后**:`tags` = `Array<{name, type, color}>` 富对象数组(与 detail 端 `main.tags` 完全一致)
**影响**:`/v3/admin/order` 列表 list[].tags 类型变更。**前端用 `tags.join(',')` 字符串操作会 NPE**,必须改为 `tags.map(t => t.name).join(',')`
**示例**:
```js
// 之前
order.tags // ['VIP', '高净值']
order.tags[0] // 'VIP'
// 之后
order.tags // [{name: 'VIP', type: 'level', color: '#FF0000'}, ...]
order.tags[0].name // 'VIP'
order.tags[0].color // 用于显示标签底色
```
### 2. 🟡 订单详情 `main.orderNo` + `main.teamNo` 字段新增
**之前**:`/v3/admin/order/{id}` detail.main 只有 `displayOrderNo`,无 `orderNo``teamNo`
**之后**:detail.main 同时返 `orderNo` + `teamNo` + `displayOrderNo` 三个字段
**影响**:前端可从详情页直接拿原始 `orderNo` 用于搜索/精确比对/退款关联/合同关联,不需要反解 `displayOrderNo`
**何时出现差异**:成团后 `displayOrderNo = "{orderNo}-T{teamNo}"`,之前前端从详情页拿不到原始 orderNo;改后两个字段都有。
### 3. 🟢 5 个原本经网关 404 的 v3 接口现在通了
**之前**:这 5 个 admin/internal 接口经网关访问 100% 404(gateway 缺路由),前端从未能调过
**之后**:网关补路由后,接口可正常调用(401 / 200 等正常响应)
| 接口前缀 | Controller |
|----------|-----------|
| `/v3/admin/complaint/**` | AdminComplaintController |
| `/v3/admin/review/**` | AdminReviewController |
| `/v3/internal/review/**` | InternalReviewController |
| `/v3/internal/mp/review/**` | InternalMpReviewController |
| `/v3/internal/house/**` | HouseInternalAssignmentController |
### 4. 🟡 `/admin/order/{id}/status-log` 改为 `/v3/admin/order/{id}/status-log`
**之前**:`AdminOrderStatusLogController` `@RequestMapping("/admin/order")` 没 v3 前缀,被 v2 路由抢截 100% 404
**之后**:改为 `/v3/admin/order`,与 v3 命名规范一致
**影响**:前端如果调过 `/admin/order/{id}/status-log` 会 404(原本也 404,没人调过),改后用 `/v3/admin/order/{id}/status-log`
---
## 一、改动概览
### PR #2864 — 修 gateway 6 路由(Closes #2863)
- gateway `application.yml` line 205 `order-service-v3` 路由 Path 末尾追加 5 个 path
- `AdminOrderStatusLogController` `@RequestMapping("/admin/order")``/v3/admin/order`
- `AdminOrderStatusLogControllerTest` 3 处 mockMvc 路径同步改 `/v3` 前缀
- 3 文件 +6 -6 lines
- 测试:GatewayRouteAuditTest 1/1 + AdminOrderStatusLogControllerTest 3/3 全绿
### PR #2866 — 修 2 个静默字段 bug(Closes #2865)
- BUG-1: `OrderMainVO``orderNo` + `teamNo` 字段,Converter 同步映射
- BUG-2: list `tags` 类型 `List<String>``List<TagVO>` 富对象(复用 detail 端 TagVO)
- `OrderInfoConverter.toTagVO` 提升 public static,list/detail 共用映射器
- `OrderService.listOrders` 批量聚合 `Map<orderId, List<TagVO>>` 去 N+1
- 6 文件 +179 -9 lines
- 新增 5 单测(OrderInfoConverterTest 3 + OrderServiceTest 2)
- 测试:113 直接相关 0 failures(89 service + 24 converter)
---
## 二、QA 测试覆盖说明
本次 4 个 QA agent 并行覆盖 dev-v3 近 5 天 wx 提交全量:
| Agent | 范围 | 结果 |
|-------|------|------|
| **QA-A** HOUSE 模块全量 | 46 测试点 | 42 PASS / 4 WARN(数据稀疏)/ 0 FAIL / 0 BLOCKER |
| **QA-B** 订单/traveler/agency/合同/tag-library | 53 项 | 51 PASS / 2 WARN(doc-vs-code 错误码)/ 0 BLOCKER |
| **QA-C** 通知/字典/mp/fleet | 40 URL | 全 PASS / 0 BLOCKER |
| **QA-D** 文档驱动业务流端到端 | 8 业务链 + 4 反例 + 6 规则 | 找出 **2 个 P0 BLOCKER**(gateway 路由 5 缺 + 1 冲突) |
| **QA-E** 字段语义反查(API cross-check + DB SELECT) | 10 对接口比对 | 找出 **2 个真实静默 bug**(tags shape / orderNo 丢失) |
---
## 三、测试服验证
| 服务 | 部署 commit | uptime | 验证 |
|------|------------|--------|------|
| hl-gateway | dev-v3 含 #2864 | 部署完毕 | 6 路径 HTTP 200 通(无 404) |
| hl-order-service-v3 | dev-v3 含 #2864+#2866 | 部署完毕 | detail.main.orderNo + list[0].tags 富对象待 mmg 复测 |
---
## 四、5 天 wx 提交累计成果(全部 PR 总览)
| Round | 工单 | PR | 主题 | 净行数 |
|-------|------|-----|------|--------|
| R1 review | #2841-#2844 | #2845-#2848 | 反过度设计 + 简洁可读 | -1359 |
| R2 收尾 | #2849 | #2850 | 7 项可现做项 + Lock4j 100503 | +311 |
| R3 收尾 | #2851 | #2853 | TokenService 灰度 + PiiMaskUtil 三合一 + 小童 | -130 |
| R4 Backlog | #2852/#2854/#2855/#2856/#2857 | #2858-#2862 | HTML 清零 1589 文件 + BFF Facade + 红线 5 + MoneyUtil + Service+Impl | 净 -340 |
| **QA 真测发现** | **#2863/#2865** | **#2864/#2866** | **gateway 6 路由 + list/detail 2 静默 bug** | **+185 -15** |
| **合计** | **13 工单** | **13 PR** | | **净 -1348 行** |
整体评分:**A+ 维持**(QA 真测 4+1 agent 全覆盖,5 个静默 bug 已修)。
---
## 五、hotfix #2881(回滚 PR #2864 引入的回归)
QA 真测验证 PR #2864 时发现 `/v3/admin/order/{id}/status-log` 返 500 Ambiguous handler。PR #2864 误把 wx 故意写的 `AdminOrderStatusLogController @RequestMapping("/admin/order")`(被 v2 抢截 404,设计上的"隐藏入口",PRP § 9 R13)改成 `/v3/admin/order`,反而与 OrderController 早就存在的同 URL 端点撞冲突。
**hotfix PR #2881 (#2880)**:
- AdminOrderStatusLogController 路径回滚 `/v3/admin/order``/admin/order`(故意 404 设计)
- AdminOrderStatusLogControllerTest 3 处 mockMvc 同步回滚
- javadoc 加大量警告注释 + 历史教训("严禁改 v3 前缀,会与 OrderController 撞 URL")
**保留 PR #2864 其他改动**:gateway 5 路由追加(complaint/review/internal-review/internal-mp-review/internal-house)是真 bug 修复,保留。
**最终验证**(部署后):
- `/v3/admin/order/{id}/status-log` code=200,真返 `List<LogTimelineVO>` 1 条(occurredAt/operator/action/fromStatus/toStatus/amount 6 字段)
- `/admin/order/{id}/status-log` code=404 维持设计
## 六、关联
- 工单: #2863 / #2865 / #2880(全部 closed)
- PR: #2864 / #2866 / #2881(全部 squash merged)
- QA 报告:
- `.tmp/qa-house-report.txt`(QA-A 46 项)
- `.tmp/qa-ord-report.txt`(QA-B 53 项)
- `.tmp/qa-msg-report.txt`(QA-C 40 项)
- `.tmp/qa-doc-chain-report.txt`(QA-D 8 业务链)
- `.tmp/qa-cross-check-report.txt`(QA-E 10 字段对比 + DB 反查)
- memory 升级:P0 #16(新 admin Controller 必同步改 gateway 路由)第 5 次复发,强化 reminder

查看文件

@ -1,298 +0,0 @@
# [修改接口·管理后台] 订单详情枚举统一 + service-standard 修复 (#2867 #2868)
> **PR**: #2869 #2874 | **服务**: hl-order-service-v3 | **更新时间**: 2026-05-22
## 1. 接口背景
本次修复合并两个 PR,均针对订单详情页管理后台接口的静默 bug
- **PR #2869Issue #2868枚举统一 + null 兑底**contractStatus / insuranceStatus 两个字段历史上枚举値定义不完整(少了中间过渡态和 NONE 初始态),且在订单还未进入合同/保险流程时后端直接返回 null,前端字典无法匹配导致显示空白或"未知"。本次统一枚举値集并将 null 兑底为 "NONE"。
- **PR #2874Issue #2867service-standard Tab 修复**hasServiceStandard 字段原来只判断快照记录是否存在(行存在即返回 true,未校验反序列化是否成功、itinerary 是否非空,导致前端收到 hasServiceStandard=true 后调 GET /service-standard 接口却收到 data=null 的矛盾。根因是快照 itinerary 字段类型 List<String> 与源头 List<DayItem> 不匹配,反序列化失败被静默吞掉。
## 2. 变更清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 订单详情 | GET | `/v3/admin/order/{id}` | 修改接口 | contractStatus/insuranceStatus 枚举统一;hasServiceStandard 判定逻辑修复 |
| 2 | 合同&保险 Tab | GET | `/v3/admin/order/{id}/contract-insurance` | 修改接口 | contractStatus/insuranceStatus 枚举统一,null 兑底为 NONE |
| 3 | 服务标准 Tab | GET | `/v3/admin/order/{id}/service-standard` | 修改接口(行为修复) | 原来 data 永远 null;现在能正常返回完整数据 |
## 3. 接口详情
### 3.1 GET /v3/admin/order/{id} —— 订单详情
- **使用场景**:管理后台订单详情页首次加载,获取订单主信息
- **认证**:需要管理后台 JWTBearer token,role=ADMIN
- **幂等性**:是(只读)
- **限流**:无
**本次变更字段**
- `data.main.contractStatus`枚举値扩展4→6 値,null → `"NONE"`
- `data.main.insuranceStatus`枚举値扩展4→5 値,null → `"NONE"`
- `data.main.hasServiceStandard`:判定逻辑修复(见 §9
### 3.2 GET /v3/admin/order/{id}/contract-insurance —— 合同&保险 Tab
- **使用场景**:管理后台订单详情页切换到 "合同&保险" Tab 时调用
- **认证**:需要管理后台 JWTBearer token,role=ADMIN
- **幂等性**:是(只读)
- **限流**:无
**本次变更字段**
- `data.contract.contractStatus`枚举値扩展4→6 値,null → `"NONE"`
- `data.insurance.insuranceStatus`枚举値扩展4→5 値,null → `"NONE"`
### 3.3 GET /v3/admin/order/{id}/service-standard —— 服务标准 Tab
- **使用场景**:管理后台订单详情页切换到 "服务标准" Tab 时调用;应在 `hasServiceStandard=true` 时才调此接口
- **认证**:需要管理后台 JWTBearer token,role=ADMIN
- **幂等性**:是(只读)
- **限流**:无
**本次变更**:行为修复。修复前 data 永远为 null;修复后在有效快照时返回完整结构。
## 4. 接口入参
### 4.1 路径参数
三个接口入参相同,无变化。
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | LongString | 是 | 订单 IDsnowflake,JSON 传 String 防精度丢失) |
### 4.2 请求体字段
三个接口均为 GET,无请求体。
## 5. 出参(响应)
### 5.1 GET /v3/admin/order/{id} —— 变更字段
| 字段路径 | 类型 | 说明 |
|----------|------|------|
| `data.main.contractStatus` | String枚举 | 合同状态,见 §6.1。**本次扩展枚举値 + null 兑底** |
| `data.main.insuranceStatus` | String枚举 | 保险状态,见 §6.2。**本次扩展枚举値 + null 兑底** |
| `data.main.hasServiceStandard` | Boolean | 是否有服务标准快照。**本次修复判定逻辑** |
### 5.2 GET /v3/admin/order/{id}/contract-insurance —— 变更字段
| 字段路径 | 类型 | 说明 |
|----------|------|------|
| `data.contract.contractStatus` | String枚举 | 合同状态,见 §6.1。**本次扩展枚举値 + null 兑底** |
| `data.insurance.insuranceStatus` | String枚举 | 保险状态,见 §6.2。**本次扩展枚举値 + null 兑底** |
### 5.3 GET /v3/admin/order/{id}/service-standard —— ServiceStandardVO完整
| 字段 | 类型 | 说明 |
|------|------|------|
| `data.itinerary` | List<String> | 行程列表,元素格式:"Day{n} {title}",如 "Day1 抵达成都"。**修复前恒为 null,修复后正常返回** |
| `data.notice` | String | 出行须知。可为 null |
| `data.refundPolicy` | String | 退款政策说明。可为 null |
当订单无服务标准快照时(`hasServiceStandard=false`),整个 `data` 为 null。
## 6. 枚举 / 数据字典
### 6.1 contractStatus合同状态
**所属字段**`data.main.contractStatus`OrderMainVO`data.contract.contractStatus`ContractInsuranceVO| **类型**`String`
| 値 | 中文 | 说明 |
|----|------|------|
| `NONE` | 无合同 | 订单尚未进入签约流程(修复前此状态返回 null |
| `GENERATING` | 生成中 | 合同正在后台生成 |
| `GENERATED` | 已生成 | 合同已生成,待客户签署 |
| `SIGNED` | 已签署 | 客户已完成签署 |
| `VOIDED` | 已作废 | 合同已作废 |
| `RESIGNING` | 重签中 | 合同正在重新发起签署 |
**修复前仅有4 値**PENDING / GENERATED / SIGNED / VOIDEDPENDING 已废弃,NONE / GENERATING / RESIGNING 为本次新增枚举値)
### 6.2 insuranceStatus保险状态
**所属字段**`data.main.insuranceStatus`OrderMainVO`data.insurance.insuranceStatus`ContractInsuranceVO| **类型**`String`
| 値 | 中文 | 说明 |
|----|------|------|
| `NONE` | 无保险 | 订单尚未进入投保流程(修复前此状态返回 null |
| `ISSUING` | 投保中 | 保险正在后台出单 |
| `ISSUED` | 已出单 | 保险已成功出单 |
| `CANCELLED` | 已取消 | 保险已取消 |
| `FAILED` | 出单失败 | 保险出单失败 |
**修复前仅有4 値**PENDING / ACTIVE / FAILED / CANCELLED旧値已废弃重命名,全部替换为上表5 値)
## 7. 错误码
本次修复无新增错误码。
| code | 含义 | 触发场景 |
|------|------|----------|
| `200` | 成功 | 正常响应 |
| `1000400` | 参数异常 | 路径参数 `id` 格式错误 |
| `1000404` | 订单不存在 | 指定 id 的订单不存在或已删除 |
| `1000403` | 无权限 | 非管理员 token 访问 |
## 8. 示例
### 8.1 典型成功 —— 新建订单 contractStatus 返 NONE修复前返 null
**请求**
```
GET /v3/admin/order/1921234567890123456
Authorization: Bearer <admin-jwt-token>
```
**响应**
```json
{
"code": 200,
"data": {
"main": {
"orderId": "1921234567890123456",
"orderNo": "ORD20260522001",
"contractStatus": "NONE",
"insuranceStatus": "NONE",
"hasServiceStandard": false
}
},
"message": "ok",
"success": true
}
```
> 修复前contractStatus: null,insuranceStatus: null,前端字典无法匹配显示空白。
### 8.2 边界情况 —— service-standard 接口有快照时返完整数据(修复前永远 null
**请求**
```
GET /v3/admin/order/1921234567890123456/service-standard
Authorization: Bearer <admin-jwt-token>
```
**响应(修复后)**
```json
{
"code": 200,
"data": {
"itinerary": [
"Day1 抵达成都,入住民宿",
"Day2 前往峨眉山景区",
"Day3 返程"
],
"notice": "请携带身份证原件,到达后联系导游",
"refundPolicy": "出发7天以上全额退款,7天内收取50%手续费"
},
"message": "ok",
"success": true
}
```
**响应(修复前)**
```json
{
"code": 200,
"data": null,
"message": "ok",
"success": true
}
```
### 8.3 业务失败 —— hasServiceStandard=false 时 service-standard 返 data=null正常行为
**场景说明**:订单未绑定产品行程快照,调 service-standard 接口返 data=null 是正常行为。
**请求**
```
GET /v3/admin/order/1921111111111111111/service-standard
Authorization: Bearer <admin-jwt-token>
```
**响应**
```json
{
"code": 200,
"data": null,
"message": "ok",
"success": true
}
```
## 9. 业务边界
**hasServiceStandard 判定逻辑(修复后)**
同时满足以下三个条件,`hasServiceStandard` 才返回 `true`
1. 订单关联的产品行程快照记录存在
2. 快照 JSON 反序列化成功(无类型错误)
3. 快照中 `itinerary` 字段非空至少有1天行程
任意一个条件不满足,`hasServiceStandard` 返回 `false`,此时调 `GET /service-standard` 将返回 `data=null`
**contractStatus / insuranceStatus 兑底规则**
- 订单刚创建、尚未发起签约/投保时,两个字段返回 "NONE"(修复前返回 null
- 字段値随后端状态机流转更新,不需要前端轮询,刷新页面即可获取最新値
## 10. 修改前后对比
### 10.1 字段级对比
| 字段 | 修复前 | 修复后 |
|------|--------|--------|
| `contractStatus`(初始态) | null | "NONE" |
| `contractStatus`(枚举集合) | PENDING / GENERATED / SIGNED / VOIDED4 値,含PENDING已废弃 | NONE / GENERATING / GENERATED / SIGNED / VOIDED / RESIGNING6 値) |
| `insuranceStatus`(初始态) | null | "NONE" |
| `insuranceStatus`(枚举集合) | PENDING / ACTIVE / FAILED / CANCELLED4 値,均已废弃重命名) | NONE / ISSUING / ISSUED / CANCELLED / FAILED5 値) |
| `hasServiceStandard` | 只判断快照行存在,partial 快照也返 true | 快照存在 + 反序列化成功 + itinerary 非空,三条件全满足才返 true |
| GET /service-standard data | 永远为 null反序列化失败被吐 | 有效快照时返回完整 {itinerary, notice, refundPolicy} |
### 10.2 行为级对比
| 行为 | 修复前 | 修复后 |
|------|--------|--------|
| 新建订单合同/保险状态 | 返回 null,前端字典无法匹配 | 返回 "NONE",字典正常匹配显示"无合同"/"无保险" |
| hasServiceStandard=true 但 Tab 内容空 | 会出现(快照反序列化失败时) | 不再出现;hasServiceStandard=true 一定能拿到有效数据 |
| insuranceStatus 枚举 ACTIVE / PENDING | 后端可能返回这两个値 | 后端不再返回,统一替换为新枚举値集合 |
## 11. 影响评估 / 回滚
### 11.1 影响评估
- **是否破坏向后兼容**:是(枚举値重命名)。旧字典如只有 PENDING / ACTIVE保险或 PENDING合同等旧値,遇到新値会显示 "未知"。
- **前端是否必须同步上线**建议同步更新枚举字典§6.1 和 §6.2 完整値表)再上线;若前端能容忍旧値显示 "未知" 可先不同步。
### 11.2 回滚方案
- **回滚方式**revert PR #2869 / PR #2874,重新打包部署 hl-order-service-v3
- **回滚后影响**contractStatus / insuranceStatus 恢复返回 null 和旧枚举値;service-standard Tab 恢复返回 null
## 12. 注意事项
- **枚举字典必须更新**旧前端字典仅有4 値/4 値(含已废弃的 PENDING / ACTIVE,遇到新枚举値如 NONE / GENERATING / RESIGNING / ISSUING / ISSUED会显示 "未知"。请按 §6.1 和 §6.2 完整値表更新本地枚举字典。
- **旧枚举値已废弃**contractStatus 的 PENDING、insuranceStatus 的 PENDING 和 ACTIVE 后端不再返回,前端字典保留无害但不会再出现。
- **service-standard workaround 清理**:如果前端之前针对 hasServiceStandard=true 但 data=null 的矛盾做过兑底处理如显示固定占位文案、屏蔽Tab,修复后可以移除该 workaround。
## 13. 关联 / 联系人
### 13.1 链接
- **Issue枚举统一**: [#2868](https://git.1814.love:8443/wx/HL/issues/2868)
- **Issueservice-standard 修复)**: [#2867](https://git.1814.love:8443/wx/HL/issues/2867)
- **PR枚举统一**: [#2869](https://git.1814.love:8443/wx/HL/pulls/2869)
- **PRservice-standard 修复)**: [#2874](https://git.1814.love:8443/wx/HL/pulls/2874)
- **Merge commit枚举统一**: [bb4c41969](https://git.1814.love:8443/wx/HL/commit/bb4c41969)
- **Merge commitservice-standard 修复)**: [442fdf089](https://git.1814.love:8443/wx/HL/commit/442fdf089)
### 13.2 联系人
- **后端负责人**: @yst

查看文件

@ -1,113 +0,0 @@
# R5 真测发现 + 修复:PII 应急脱敏 + 配房 BLOCKER + 6 P1
> **服务**: hl-order-service-v3
> **PR**: #2900 / #2901 / #2902
> **Issue**: #2894 / #2895 / #2896
> **日期**: 2026-05-22
> **影响**: **🔴 前端 breaking change(traveler 字段改名)** + 配房链路恢复 + 订单详情补字段 + 金额格式统一
---
## ⚠️ 关键变化(前端 mmg 必读)
### 1. 🔴 traveler VO 字段改名(breaking change,**前端必同步改 hl-ui**)
| 旧字段 | 新字段 | 值格式 |
|--------|--------|--------|
| `idNo` | **`idCardMasked`** | `220***********1234`(17 字符变长星) |
| `phone` | **`phoneMasked`** | `138****2046`(11 字符固定星) |
| `emergencyPhone` | **`emergencyPhoneMasked`** | `139****5678` |
**适用接口**:
- `GET /v3/admin/order/{id}/traveler/list` 列表
- `GET /v3/admin/order/{id}` 详情 `overview.travelers`
- traveler 智能解析 dry-run 输出
**前端如有 `traveler.idNo` / `traveler.phone` 引用必须改为 `traveler.idCardMasked` / `traveler.phoneMasked`**。
### 2. 🟢 订单详情新增 transportPlans + itinerary 字段(BUG-3 修)
`GET /v3/admin/order/{id}` 顶层 data 新增:
- `transportPlans: List<TransportPlanVO>`(大交通列表)
- `itinerary: OrderItineraryRespVO`(行程)
**前端可直接用**(原本就缺,现在补上)。
### 3. 🟢 订单金额字段格式统一 2 位小数
之前 `totalAmount=3105.0`(1 位)/ `refundAmount=0.00`(2 位)不一致,**统一改为 2 位小数**:
- `OrderListItemVO` / `OrderMainVO`:5 字段(totalAmount/paidAmount/balanceAmount/depositAmount/singleRoomSurcharge)走 `OrderAmountUtil.to2()`
- Refund Converter 走 `MoneyUtil.formatYuan()`
**前端如对 `3105.0` 字符串做精确匹配会出问题**,改用 `parseFloat` 或宽松匹配。
### 4. 🟢 5 个原本经网关 503/500 的 v3 接口现在通了
- `POST /v3/admin/order/hotel-requirements/{rid}/assignments`(原 JSON 500)→ **200**
- `GET /v3/admin/house/staff`(原 504)→ 3s 内返(Feign timeout 收紧 + fallback)
- `GET /v3/admin/contract/{id}`(原 500 BadSqlGrammarException)→ **200**
---
## 一、3 PR 改动
### PR #2900 PII 应急脱敏(Closes #2894)
- traveler 3 字段改名(`*Masked` 后缀) + Converter 全 SensitiveDataUtil.maskXxx() 包装
- TravelerSmartParseService.buildDryRunVO 同步脱敏
- 6 新单测 + 红线 PII 不泄漏断言,128 traveler 测试 0 failure
- **后续独立工单**:DB TypeHandler 加密(order_traveler+fleet_driver 明文存储)+ audit_log 接入
### PR #2901 BLOCKER 修复(Closes #2895)
- BLOCKER-1 配房 500:删 `@JsonDeserialize(using = LongDeserializer)` 错误用法 → 用原生 Long
- BLOCKER-2 house/staff 504:houseStaffFeignClient 单独 connectTimeout=2s + readTimeout=3s,触发现有 fallback 返空
- 5 新单测 29/29 全绿
- **经验入 memory**:雪花 Long 字段禁 @JsonDeserialize(LongDeserializer)
### PR #2902 6 P1(Closes #2896)
- P1-1 contract_status_log 缺 4 BaseDO 列 → V20260522_001.sql 补
- P1-2 OrderDetailVO 补 transportPlans + itinerary + Service 聚合
- P1-3 OrderAmountUtil.to2 统一 2 位小数 + RefundConverter 走 MoneyUtil
- P1-4 inquiry_message.dayNumber → V20260522_002.sql + DO + Service 真实写入
- P1-5 INVENTORY_CHECK op_log operator_name → AdminContextUtil.getRealName 兜底
- P1-6 HouseGrabServiceImpl 双写 house_claim_history(opType/fromUserId/toUserId/reason)
- 180 直接相关测试 0 failure
---
## 二、QA R5 真测全量发现(5 agent 累计 22 bug)
| Agent | 方法 | 发现 |
|-------|------|------|
| **R5-F HOUSE 深度** | 5 业务链端到端 + 11 DB 反查 + 2 真并发 | 2 BLOCKER + 3 P1(已修)|
| **R5-G 订单生命周期** | 5 业务链 + 12 DB 反查 + RateLimiter 实测 | 1 P0 + 3 P1 + 3 P2/P3(P0/P1 已修)|
| **R5-H 通知字典** | 6 业务链 + 7 DB 反查 | 全 PASS,环境性 504 已处理 |
| **R5-I fleet+tag+大交通** | 6 业务链 + 8 DB 反查 + 17 错误码命中 | 1 P0 + 1 mock(P0 应急已修)|
---
## 三、测试服验证
| 服务 | commit | 状态 |
|------|--------|------|
| hl-gateway | dev-v3 含 #2900-2902 | 已部署 |
| hl-order-service-v3 | dev-v3 含 #2900-2902 + Flyway V20260522_001/002 | 已部署(双实例 8086+8186) |
---
## 四、留尾(独立工单 backlog)
- DB TypeHandler 加密迁移(order_traveler + fleet_driver phone/id_card 明文存储)
- audit_log 接入 admin path decrypt
- HouseModuleBoundaryTest ArchUnit 违规独立 PR 治理
- TagLibraryIntegrationTest ApplicationContext 问题独立排查
- user-service `/internal/user/admin/house-staff` 真实实装(H07 占位)
- `house_claim_history`@Deprecated 标记,前端历史轨迹切 T9 op_log 后下线本期双写
---
## 五、关联
- 工单:#2894 / #2895 / #2896(全部 closed)
- PR:#2900 / #2901 / #2902(全部 squash merged)
- QA 报告:`.tmp/qa-r5{f,g,h,i}-*-report.txt`(共 4 份)
- memory 升级:`feedback_snowflake-long-no-jackson-deserializer.md`(雪花 Long 禁 @JsonDeserialize(LongDeserializer))

查看文件

@ -1,65 +0,0 @@
# R6 真测发现 + 修复:insurance 翻页 + refund review PARTIAL 静默错位
> **服务**: hl-order-service-v3
> **PR**: #2937
> **Issue**: #2935
> **日期**: 2026-05-23
> **影响**: 1 P0 + 1 P1 + 1 P2(其中 P2 是更深的静默业务错位 bug)
---
## ⚠️ 关键变化(前端 mmg)
### 1. 🔴 退款审批 PARTIAL **必传 approvedAmount**(behavior change)
**之前**:`POST /v3/admin/refund/review {decision: "PARTIAL"}` 不传 `approvedAmount` → Service fallback 到 `calculatedAmount/paidAmount`,**PARTIAL 被静默当 APPROVED 全额处理**(语义错位 bug)
**之后**:必返 `code=530403 REFUND_REVIEW_PARTIAL_AMOUNT_REQUIRED "PARTIAL 审批必须传 approvedAmount"`
**前端如果有 PARTIAL 审批漏传 approvedAmount 的场景必须补传**,否则会被 530403 拒绝。
### 2. 🟢 `/v3/admin/insurance/products` 翻页边界返 400
**之前**:`page=0` → 500 IndexOutOfBoundsException / `pageSize=10000` 不拒
**之后**:`page=0` → 400 / `pageSize > 100` → 400(与其他 7/8 list 接口一致)
---
## 改动
### P0+P1 R6L: AdminInsuranceController.listProducts 翻页校验缺失
- 新建 `InsuranceProductListReqVO` 继承 `PageParam`(`page @Min(1)` + `pageSize @Min(1) @Max(100)`,含 `isOverseas` 字段)
- Controller 改 `@Valid InsuranceProductListReqVO req`,@PageParam 注解直接拦截非法
- 与其余 7 接口风格一致
### P2 R6M: AdminRefundController.review PARTIAL 静默错位修复
- **R6-M 报告误判为 NPE**,实际是更隐蔽的语义错位 bug:
- Service 层映射后枚举 APPROVE/REJECT 丢失 PARTIAL 信息
- approvedAmount=null 时 fallback 到 calculatedAmount/paidAmount
- 实际行为:PARTIAL 被静默当 APPROVED 全额退款 — **业务静默 bug**
- 修:Controller 持有原 decision 枚举,前置校验 `decision=PARTIAL && approvedAmount=null` → 抛 `BusinessException(530403)`
- 错误码 530403 `REFUND_REVIEW_PARTIAL_AMOUNT_REQUIRED``OrderRefundErrorCode:127` 已存在(设计预留实现遗漏),复用
## 验证
- 50 直接相关测试 0 failure
- 3 新单测:
- `listProducts_pageNum_zero_returns400`
- `listProducts_pageSize_over100_returns400`
- `review_PARTIAL_without_approvedAmount_throws530403`(断言 `refundReviewService.review` `never()` 被调,Service 完全没接到错误数据)
## R6 累计测试发现(4 agent)
| Agent | 维度 | 结果 |
|-------|------|------|
| R6-J | 状态机异常 | 22 反例全走业务码 0 BUG + 3 UX 文案改进 |
| R6-K | 并发竞态 | 5 @Lock4j + 4 @Idempotent + 1 死锁实测 0 BUG(R5-G RateLimiter 5 阈值误判 → 实际 10)|
| R6-L | 翻页边界 | 8 接口 × 18 维度 = 144 点 84.7% 通过,1 P0 + 1 P1 |
| R6-M | ErrorCode | 22 类 ~265 总数 50+ 真触发 段位合规 100%,1 P2 |
5 文件 +87 -8 + 1 新 VO。
## 关联
- 工单 #2935(closed)
- PR #2937(squash merged)
- R6 报告 `.tmp/qa-r6{j,k,l,m}-*-report.txt`(共 4 份)

查看文件

@ -1,121 +0,0 @@
# R7 真测发现 + 修复:网关错误响应规范化(5 项 CRITICAL+HIGH)
> **服务**: hl-gateway + hl-user-service + hl-common-core + hl-common-web(全模块共依赖)
> **PR**: #2948
> **Issue**: #2940
> **日期**: 2026-05-23
> **影响**: **🔴 错误响应格式变更 + Result 新增 traceId 字段 + X-Trace-Id 响应头**
---
## ⚠️ 关键变化(前端 mmg 必读)
### 1. 🔴 不存在路径返 Result JSON,**不再返 HTML 静态页**
**之前**:`https://web.test.1814.love:9443/nonexistent-foo` → 网关 fallback 漏到前端 vue 静态页(返完整 HTML),API 调用方 JSON.parse 爆 SyntaxError
**之后**:返 `{code: 404, message: "接口不存在: GET /nonexistent-foo", success: false, data: null, traceId: "xxx"}`
**前端如果有 try/catch 期望 HTML 响应的代码必须改为 JSON 处理**。
### 2. 🟢 Result 类新增 `traceId` 字段(向后兼容)
所有响应(成功 + 错误)的 `data` 字段同级新增 `traceId`:
```json
{
"code": 200,
"message": "成功",
"data": {...},
"success": true,
"traceId": "a1b2c3d4e5f6g7h8" // 新增,16 字符短 UUID
}
```
- 旧客户端不识别 traceId 字段不影响(忽略未知字段)
- 新客户端可记录 traceId 用于线上排查问题(给后端报错时附 traceId 加速定位)
### 3. 🟢 错误响应必含 `X-Trace-Id` 响应头
```
HTTP/1.1 200 OK
X-Trace-Id: a1b2c3d4e5f6g7h8
Content-Type: application/json
```
前端可统一读取(axios interceptor 等)。
### 4. 🟡 鉴权错误响应规范化
**之前**:`Authorization` 缺失 → 网关返手拼 JSON `{code:401, message:"Missing or invalid Authorization header"}``success` 字段 + 英文
**之后**:返 4 字段全 + 中文 message:
```json
{
"code": 401,
"message": "缺少有效的 Authorization 头",
"success": false,
"data": null,
"traceId": "..."
}
```
5 处中文化:
- "Missing or invalid Authorization header" → "缺少有效的 Authorization 头"
- "Invalid token" → "Token 无效"
- "Token expired or revoked" → "Token 已过期或被撤销"
- "Unknown token type" → "Token 类型未知"
- "请使用小程序端接口" 保留
---
## 改动概览
### 1) CRITICAL R7Q-B6 网关 catch-all
- 新建 `GlobalErrorWebExceptionHandler @Order(-2)` 优先于 Spring Boot 默认 `-1`
- NotFoundException → code=404 + 中文"接口不存在"
- 上游异常 → code=500/对应 status + 中文
### 2) HIGH R7Q-B1 JwtAuthFilter
- 注入 ObjectMapper + 抽 `writeErrorResponse(exchange, code, message)` 统一写出
- 5 处 forbidden/unauthorized 改 `Result.error(code, message)`(自带 success=false)
- 5 处英文 message 中文化
### 3) HIGH R7Q-B5 TokenInterceptor
- 注入 ObjectMapper + `writeError(request, response, code, message)` 统一
- 5 处 `response.getWriter().write("{...}")``objectMapper.writeValueAsString(Result.error(...))`
### 4) HIGH R7Q-B2 NoHandlerFoundException 确认已生效
- `hl-common-log/GlobalExceptionHandler.java:255` 已注册
- `hl-common-web/HlMvcDefaultsEnvironmentPostProcessor` 已注 `spring.mvc.throw-exception-if-no-handler-found=true` + `spring.web.resources.add-mappings=false`
### 5) HIGH R7Q-B9 X-Trace-Id 全链路
- `Result.java` 新增 `traceId` 字段
- `TraceIdFilter`(gateway,@Order(-200))无请求头时生成 16 字符短 UUID + 注入下游请求头 + 响应头
- `TraceIdResponseFilter`(hl-common-web)后端服务读请求头 → MDC + 响应头,filter 结束清 MDC 防线程池串数据
- JwtAuthFilter / TokenInterceptor / GlobalErrorWebExceptionHandler 错误响应同步注入 X-Trace-Id
---
## R7 完整测试覆盖(4 agent)
| Agent | 维度 | 发现 |
|-------|------|------|
| R7-N Swagger 三角 | 71 Controller / 360 端点 / 311 VO / 3095 字段 | 1 P0(#2880 设计不修)+ 4 P1 + 4 P2,47% 缺 @ApiOperation(留 backlog)|
| R7-O Long 精度 | 333 VO 文件 / 440 Long 字段 / 18+ 真测接口 | **0 BUG**(全局 NumberSerializer 完备)|
| R7-P 序列化 | 30+ 接口 4 维度 | 6 bug 全 v2 老代码(留 backlog)|
| R7-Q 错误响应 | 57 错误场景 | **1 CRITICAL + 4 HIGH + 3 MEDIUM + 1 LOW**(本工单修 5 项)|
## 测试
20 新单测,5 模块 mvn test 全绿(common-core 484 + common-web 18 + common-log 68 + gateway 11 + user 2486)。
13 文件 +876 -32 lines。
## 留尾(独立 backlog,本工单不做)
- R7N 47% 端点缺 @ApiOperation(168/360)
- R7-P 6 v2 老代码 status/Boolean Integer 化
- R7-Q MEDIUM 3 项:@Valid 缺字段名 / 415 兜底 500 / enum deserialize 吞异常
- R7-Q LOW:6 条 ErrorCode 纯英文(ContractErrorCode 3 + SettlementErrorCode 3)
## 关联
- 工单 #2940(closed after PR merged)
- PR #2948(squash merged)
- R7 4 报告:`.tmp/qa-r7{n,o,p,q}-*-report.txt`

查看文件

@ -1,49 +0,0 @@
# 二期 v3 精选评价补 source / sourceLabel(对齐一期 #2988)
> **服务**: hl-order-service-v3 + hl-product-service-v2
> **PR**: #3054(配套 dev→dev-v3 同步 #3050)
> **Issue**: #2988(一期同步到二期 v3)
> **日期**: 2026-05-26
> **影响**: 🟢 二期 v3 环境精选评价新增填充评价来源字段(契约字段一期 #2988 已加,本次仅二期环境行为对齐,**向后兼容**)
---
## ⚠️ 关键变化(前端 mmg 必读)
### 二期 v3 环境:产品详情精选评价现返回「评价来源」
产品详情接口响应里 `reviewSummary.topReview` 节点下,现正确返回两个来源字段:
```json
{
"reviewSummary": {
"avgScore": 4.8,
"totalCount": 120,
"topReview": {
"reviewId": 123,
"nickname": "张三",
"content": "...",
"score": 5,
"source": "MINIPROGRAM", // 评价来源枚举
"sourceLabel": "小程序" // 来源中文(字典 review_source)
}
}
}
```
- `source`:评价来源枚举 —— `MINIPROGRAM`=小程序 / `YOUZAN`=有赞(字典 `review_source`)
- `sourceLabel`:来源中文名,来自字典 `review_source``dict_label`(运维改字典即时生效,无需重启)
### 行为说明
- **之前**:二期 v3 环境下这两字段恒为 `null`(v3 评价表无 source 列、链路未填充)
- **之后**:正常返回小程序/有赞来源
字段本身一期 #2988 早已加入并上线(一期环境已可用)。**前端若已按一期 #2988 接入了 source/sourceLabel,二期环境无需任何改动**,本次只是把二期 v3 的填充行为对齐到一期。
---
## 📌 备注
- 本次改动已合并 dev-v3(PR #3054),但**截至发稿尚未部署到测试服验证**(应需求方要求提前同步)。前端在二期测试环境实际联调前,请先确认 hl-order-service-v3 / hl-product-service-v2 已部署该版本。
- 后端实现:v3 评价表补 `source` 列(独立库 hl_order_service_v3,V20260526_001)+ `sourceLabel` 复用字典动态获取,与一期口径一致。

查看文件

@ -1,44 +0,0 @@
# 二期 v3 HOUSE 文档-代码对齐:加急错误码迁移 + staff 响应结构 + 分页一致性
> **服务**: hl-order-service-v3
> **PR**: #3096
> **Issue**: #3090
> **日期**: 2026-05-27
> **影响**: 🟡 加急(escalate)错误码值变更(前端错误码映射需同步) + 转单候选员工接口响应结构变更 + 分页行为修复(向后兼容)
---
## ⚠️ 关键变化(前端 mmg 必读)
### 1. 询房加急(escalate)4 个错误码迁移:`808280-808283``808815-808818`
`POST /v3/admin/order/inquiry/{inquiryId}/escalate`(§3.6 加急/催办)相关的 4 个业务错误码,按 API 文档 §11.8 规定从 **808280-808283 段迁移到 808815-808818 段**
- **message 中文文案不变**(动态返回)。
- 前端**若按数字 code 做错误提示映射**:请把这 4 个码从 `8082xx` 更新为 `8088xx`
- 前端**若直接展示 `message` 字段**:无需任何改动。
### 2. 转单候选员工 staff 响应结构:裸数组 → `{list,total}`
`GET /v3/admin/house/staff` 出参从裸数组改为带分页包装,对齐文档 §6.6
```jsonc
// 之前
"data": [ {...}, {...} ]
// 之后
"data": { "list": [ {...} ], "total": 12 }
```
前端请改读 `data.list` / `data.total`
(注:该接口数据源依赖 user-service H07 工单,当前返回空列表 `{list:[],total:0}`,**结构已就位**,待 H07 上线后有数据。)
### 3. 「我的接单」分页一致性修复(行为修复,无需前端改动)
`GET /v3/admin/order/grab-pool/my-claims/hotel` 带筛选条件(keyword / 出行日 等)时,`total``list` 现在保持一致。原实现是 SQL 分页后再内存过滤,导致带筛选时 total 偏大、跨页漏数据,现已下沉到 SQL。前端分页页数/条数将更准确。
---
## 📌 备注
- 已部署测试服(dev-v3)并经真 admin token API 验证staff 返 `{list,total}` 结构正确、加急接口路径连通(返 HOUSE 错误码)。
- 加急错误码 808815-818 的具体触发场景由后端单测覆盖,需真实询房数据触发;前端按上述 code 段更新映射即可。

查看文件

@ -1,69 +0,0 @@
# 二期 v3 FLEET 车务H5 入职接口可达 + 司机 PII 加密 + 车辆/司机契约补齐
> **服务**: hl-fleet-service + hl-gateway
> **PR**: #3102 / #3104 / #3106
> **Issue**: #3101 / #3103 / #3105
> **日期**: 2026-05-27
> **影响**: 🟡 H5 司机自助入职接口现可通过网关访问 + 车辆/司机列表新增字段 + 批量导入响应结构变更 + submit 入参字段名澄清(司机 PII 加密对前端透明)
---
## ⚠️ 关键变化(前端 mmg 必读)
### 1. 【重要】H5 司机自助入职接口现可通过网关访问(原 404)
之前网关缺 `/app/h5/**` 路由,下列 4 个 H5 端点过网关一律 404,现已补齐可正常调用
- `GET /app/h5/driver-onboard/init`
- `POST /app/h5/driver-onboard/ocr/{step}`(multipart 上传)
- `POST /app/h5/driver-onboard/submit/new`
- `POST /app/h5/driver-onboard/submit/renew`
鉴权H5 端走**自带 onboard token**(query/form 参数,由后端 H5OnboardService 自校验),**不需要 admin JWT**。坏 token 返 `data.tokenValid=false`(不报错)。
### 2. 车辆列表新增字段
`GET /admin/fleet/vehicles/page``records[]` 现包含:
- `typeKey` / `typeName`:车型大类(LEFT JOIN 车型库带出)
- `regDate`:注册日期
### 3. 司机列表新增字段
`GET /admin/fleet/drivers/page``records[]` 现包含:
- `licenseType` / `licenseExpire`:驾照类型 / 到期日
- `totalOrders` / `avgRating` / `lastOrderAt`:接单数 / 评分 / 最近接单(业务流水模块上线前为默认 `0`/`0.00`/`null`)
### 4. 批量导入响应结构变更 + 去重行为
**车辆导入** `POST /admin/fleet/vehicles/import` 出参:
```jsonc
// 之前: { total, success, failed, errorRows[] }
// 之后:
{ "totalRows": 10, "insertedCount": 7, "updatedCount": 2, "errorCount": 1,
"errors": [ { "row": 5, "plate": "蒙A12345", "msg": "..." } ] }
```
**司机导入** `POST /admin/fleet/drivers/import` 出参:
```jsonc
{ "totalRows": 10, "insertedCount": 7, "renewedCount": 2, "errorCount": 1,
"errors": [ { "row": 5, "name": "张三", "msg": "..." } ] }
```
**去重行为**:车牌(车辆)/身份证(司机)**已存在的行现执行 UPDATE**(原为报错计入失败),不存在则 INSERT。
### 5. H5 新司机提交入参字段名澄清(文档笔误修正)
`POST /app/h5/driver-onboard/submit/new` 入参用:
- `idCard`(**不是** `idCardNo`)
- `preferredCategory`(**不是** `preferredTypeKey`)
代码一直是这两个名,是文档示例写错;文档已更正。前端按 `idCard` / `preferredCategory` 传参。
### 6. 司机敏感字段加密(后端透明,前端无需改动)
身份证 / 手机 / 驾照号 / 紧急联系电话改为**加密存储**。接口响应仍按原样**脱敏返回**(如 `idCard: "110***********2468"`),与之前一致,**前端无感知**。
---
## 📌 备注
- **已部署测试服(dev-v3)并经真 admin token + 真实数据 API 验证全绿**:车辆列表 typeName/typeKey/regDate 正确填充(真实 MySQL JOIN)、身份证密文存储 + 加密后按身份证去重命中 + 读路径脱敏正常、司机列表 5 个新字段齐全、`/app/h5` 路由连通。
- 司机 PII 仅接通加密通道;测试库 #3104 前的存量明文司机均为软删数据,不影响。

查看文件

@ -1,37 +0,0 @@
# 二期 v3 HOUSE 房务:配房写主链真机阻断修复(提交 / 改价 / 最终确认)
> **服务**: hl-order-service-v3
> **PR**: #3117
> **Issue**: #3115
> **日期**: 2026-05-27
> **影响**: 🟡 配房三个核心写接口(提交配房 / 改单条配房 / 最终确认)此前真机一律失败,现已修复并测试服真机验证通过。**接口路径与出入参字段无任何变化**,前端无需改代码,可正常联调配房写流程。
---
## 背景
真机 API 测试(真 admin token 经网关 + 真实订单数据)发现配房「提交 → 改价 → 最终确认」三个写接口此前 100% 失败(单测 mock 能过、真机全挂),现修复。
## 受影响接口(仅"从报错变为可用",契约不变)
### 1. `POST /v3/admin/order/hotel-requirements/{requirementId}/assignments`§2.2 提交配房)
- 之前:当所选酒店在 resource 无协议价时,返回 `400 字段【proto_price】未填写`,配房无法落库。
- 现在:正常 200 落库。协议价/结算方式/付款模式/酒店名在主数据缺失H04 接通前)时存 NULL,待回填,**不影响提交**。
- 出参结构不变:`{ successCount, failCount, items:[{dayNumber, familyIndex, assignmentId, arrange}] }`
### 2. `PUT /v3/admin/order/assignments/{id}`§2.3 改单条配房)
- 之前:返回 `500`(服务器内部错误,乐观锁拦截器缺失)。
- 现在:正常 200,`sellPrice` 改后 `totalCost` 自动重算、`version` 自增。
### 3. `POST /admin/house/assignments/requirements/{requirementId}/finalize`§2.7 最终确认)
- 之前:闸口校验通过后仍返回 `400 字段【operator_name】未填写`,需求卡在 PROCESSING 无法定稿。
- 现在:正常 200,需求推进到 `DONE`,返回 `{ newStatus:"DONE", finalizedAt }`
## 真机验证(测试服双实例已部署)
完整链路 claim → 提交配房(3 晚) → 改价 → 询房发起/回填 → 最终确认 → 删配房 全部 200 通过;need/requirement 正确流转到 DONE。
## 不在本次范围(仍为占位,依赖后续工单,前端先按空处理)
- §2.1 候选源、§2.0 订单详情聚合 order 子对象、§6.1 酒店列表、§6.6 staff 列表:依赖 H04(product-v2/OrderService Feign) / resource 主数据 / H07(user-service),暂为空/占位。
- §2.5 房间分配「写」入口(家庭维度 assignRooms当前无对外端点,§2.5 GET 的逐日 `assignments` 暂为空(另行评估是否补端点)。

查看文件

@ -1,97 +0,0 @@
# 二期 v3新建订单向导「选主题/选产品」接通真实接口(替换 mockProducts.js
> **服务**: hl-product-service-v2+ hl-order-service-v3 内部统计)
> **PR**: #3155
> **Issue**: #3142
> **日期**: 2026-05-28
> **影响**: 🟡 新建订单页 `order-v2/new` 第 1 步「选主题」可改用真接口;第 2 步「选产品」用已有接口;地区筛选删除
---
## ⚠️ 关键变化(前端 mmg 必读)
`order-v2/new` 第 1、2 步当前用 `src/views/order-v2/new/_shared/mockProducts.js` 写死假数据。现可替换为真实接口。
### 1. 第 1 步「选主题」— 新增接口
```
GET /admin/product/line/order-picker
```
- 无 query 参数,**不分页**,按排序号升序返回
- 仅返回「**启用(ACTIVE) + 在售(该产品线下挂≥1个已上架产品)**」的主题
- **数据权限**:和产品管理一致——超管看全部、核心(CORE)/小蒙马(GROUP)主题全员可见、私人定制(CUSTOM)仅创建人本人可见。**地区(region)筛选不需要,产品线无此字段,请删掉那排 chips。**
返回结构:
```jsonc
{
"code": 200,
"data": [
{
"lineId": 2056937785918369794, // 主题ID(雪花,前端按字符串透传)
"name": "测试核心产品", // 主题名
"description": "...", // 描述(原 mock 的 desc/tagline 都用它)
"coverImageUrl": "https://...", // 真实封面图(替代 mock 的 emoji cover + coverColor)
"productType": "CORE", // CORE=核心 / GROUP=小蒙马
"skuCount": 6, // 该主题下已上架产品的档位总数(原 mock 的 skuCount)
"fromPrice": 3105.00, // 起价/人(已上架产品最低成人起价;未设价为 null,请显示 ¥-- 或灰)
"hot": true // 热门标识 = 该主题近90天下单量较多(前30%且>0)
}
]
}
```
字段映射建议mock → 真接口):
- `id``lineId`(字符串透传,勿 Number()
- `cover`/`coverColor`emoji 装饰)→ 改用 `coverImageUrl` 真实图;如仍要色块可前端按 lineId 取色
- `region`/`regionKey`**删除**(无后端字段,地区 chips 去掉)
- `tagline` → 并入 `description` 或省略
- `skuCount` / `fromPrice` / `hot` → 同名直取
- 顶部统计「在售主题数」= 列表长度;「SKU总数」= Σ`skuCount`;「最低人均」= min(`fromPrice`)
### 2. 第 2 步「选产品」— 新增专用接口PR #3164 / #3159,已测试服验证)
> 注:原先说复用 `/admin/product/item/list`,但 SKU 卡还要「热销」「发团日期」两个字段,通用列表没有;为不拖慢产品管理列表,**改为新建专用选产品 picker**(与选主题对称)。**前端 Step2 改调下面这个**,不要用 `/item/list`
```
GET /admin/product/item/order-picker?lineId={lineId}&page=1&pageSize=20&keyword=
```
- `lineId` 必填(选定主题的 `lineId`);只返该产品线**已上架**产品+档位;返回 `PageResult<OrderPickerProductVO>``data.records[]` + `data.total`
- 数据权限同选主题(@DataScope:超管全部 / CORE·GROUP 全员 / CUSTOM 仅本人)
- 下单提交的 `productId + tierSeq` 从这里取(雪花 `productId` 按字符串透传,勿 Number()
返回 `records[]` 实测字段(与 Step2 SKU 卡片元素映射):
```jsonc
{
"productId": 2043595351268519937, // 产品ID(字符串透传)
"name": "6天5晚旷野版", // 卡片大标题
"subtitle": "装甲车穿越·帐篷营地…", // 副标题
"tags": ["亲子","露营"], // 标签行
"tripDays": 6, "tripNights": 5, // "6天5晚"
"coverImageUrl": "https://...",
"startPrice": 10.00, // 产品级最低起价(可能 null → 显示 ¥--)
"tierPrices": [ // 档位数组(档次过滤 chips = 去重 tierName)
{"tierSeq":1, "tierName":"舒适", "tierDescription":"…", "startPrice":10.00},
{"tierSeq":2, "tierName":"豪华", "tierDescription":"…", "startPrice":null}
],
"hot": true, // 【新】热销徽标 = 该产品近90天下单量较多(本主题内前30%且>0)
"nextSaleDate": "2026-07-01" // 【新】发团日期 = 价格日历/班期里最近的未来可售日(无可售为 null)
}
```
**SKU 卡渲染**mock 里每张卡=一个产品的一个档位;真实是一个产品(`productId`)含多档位(`tierPrices[]`)。
建议按「产品 × 档位」平铺成卡(每个 `tierPrices[i]` 一张卡),卡上:
- 档位标签 ← `tierPrices[i].tierName`"尊享档/挑战档"
- 价格 ← `tierPrices[i].startPrice`**null 显示 ¥-- / 灰**,别显示 0
- 「热销」徽标 ← 产品级 `hot`(同一产品的各档位卡都显示)
- 「发团日期」← `nextSaleDate`最近可售日;null 则不显示该行)
- 提交带 `productId` + `tierPrices[i].tierSeq`
### 3. 提交(不变)
`POST /v3/admin/order`,入参 `productId` + `tierSeq` 等,已就绪。
---
## 验证
测试服 `web.test.1814.love:9443` 真 admin token 实测:
- 选主题 `line/order-picker`:返 9 个在售主题(库内 ACTIVE 29 → 在售 9 精确吻合),字段齐全,主题级 hot 跨服务统计生效
- 选产品 `item/order-picker`HTTP 200,字段齐全;产品级 `hot`(热门线下 4 产品命中 1 个 hot=true`nextSaleDate`(价格日历最近可售日,如 2026-07-01均生效

查看文件

@ -1,142 +0,0 @@
# 二期 v3产品「服务标准」模块 — 模板总则(分组+条目) + 每日特别提醒 + 订单 Tab 富展示
> **服务**: hl-product-service-v2 + hl-order-service-v3
> **PR**: #3157+ hotfix #3163
> **Issue**: #3147
> **日期**: 2026-05-28
> **影响**: 🟢 新增能力,全部向后兼容(旧订单 Tab 自动降级,不报错)
> **状态**: ✅ 测试服 9443 全链路实测通过(模板 CRUD → 产品绑定 → 创单冻结 → 订单 Tab
---
## 背景
「出团服务说明书」需要比原来 `notice.title + 一段富文本` 更丰富的结构:**多分组**(一、出团注意事项…)+ **编号条目**(标题/说明/联系人)+ **每日特别提醒**。本期在产品侧**新建**「服务标准」模块,创单冻结进订单快照,订单/小程序/PDF 读快照展示。
前端需做:① 模板编辑器(分组/条目增删)② 产品 Step5 绑定下拉 ③ 行程 Step2 每日「特别提醒」录入 ④ 订单/小程序服务标准 Tab 富展示 ⑤ 行程单/签单 PDF。
---
## 一、产品侧:服务标准模板管理(新增 admin 接口)
路径前缀 `/admin/product/service-standard-template`(已被现有 `/admin/product/**` 网关路由覆盖)。
### 1. 分页
```
GET /admin/product/service-standard-template/page?pageNo=1&pageSize=10&name=&status=
```
返回 `PageResult``data.records[]` + `data.total`),按 sortOrder/id 升序。
### 2. 详情
```
GET /admin/product/service-standard-template/{id}
```
### 3. 保存id 空=新增 / 非空=修改)
```
POST /admin/product/service-standard-template
```
请求体:
```jsonc
{
"id": null, // 修改时传(字符串透传雪花ID)
"name": "草原出团服务说明书", // 必填
"intro": "出团前请仔细阅读…", // 顶部提示语(截图绿色提示条)
"applicableScope": "全部跟团客户", // 适用对象
"sections": [ // 必填:分组+条目
{
"title": "一、出行服务", // 分组标题(必填)
"items": [
{ "title": "接送站服务", "content": "提供机场/车站免费接送", "contact": "客服 400-xxx" }
]
}
],
"isDefault": false,
"sortOrder": 0,
"status": "ENABLED" // 留空默认 ENABLED;ENABLED=启用/DISABLED=停用
}
```
返回 `data` = 模板ID**字符串透传,勿 Number()**)。
### 4. 删除(软删,已绑定产品不受影响)
```
DELETE /admin/product/service-standard-template/{id}
```
### 5. 下拉Step5 绑定用,仅启用项,不分页)
```
GET /admin/product/service-standard-template/enabled
```
返回 `data: [ { "id": "...", "name": "草原出团服务说明书" } ]`(只 id+name
---
## 二、产品 Step5 补充信息:绑定模板(纯引用)
`PUT /admin/product/item/{id}/supplement` 请求体**新增**字段:
```jsonc
{ "serviceStandardTemplateId": 2059839696602615809 } // 字符串透传;解绑传 null
```
保存后产品详情回显该 ID;用上面的 `/enabled` 下拉选。
> **私人定制例外**:私人定制(CUSTOM)产品「完成设计」时会把当时模板内容**冻结快照**进产品,之后改模板不影响该产品;核心/小蒙马为纯引用(改模板后续新订单实时跟随)。前端无需特殊处理,知悉即可。
---
## 三、产品 Step2 行程:每日「特别提醒」
`PUT /admin/product/item/{id}/itinerary` 的每个 `DayItem` **新增**字段:
```jsonc
{
"dayNumber": 1,
"serviceTips": [ // 本日特别提醒(可空)
{ "title": "本日特别提醒", "content": "今日海拔较高,备好抗高反药" }
]
}
```
保存后行程回显该天的 `serviceTips`
---
## 四、订单详情「服务标准」Tab 响应升级
```
GET /v3/admin/order/{id}/service-standard
```
(小程序端经 hl-mp BFF 聚合,结构一致)
`data` **新增** `serviceStandard` + `dayTips` 两块;保留原 `itinerary``refundPolicy`;原 `notice` 字段**废弃**(恒 null,请改用 `serviceStandard`
```jsonc
{
"itinerary": ["Day1 …","Day2 …"], // 保留:行程天纲
"notice": null, // 废弃,勿再用
"refundPolicy": { /* 原结构保留 */ },
"serviceStandard": { // 新:服务标准总则
"intro": "出团前请仔细阅读…",
"applicableScope": "全部跟团客户",
"sections": [
{ "title": "一、出行服务",
"items": [ { "title": "接送站服务", "content": "…", "contact": "客服 400-xxx" } ] }
]
},
"dayTips": [ // 新:每日特别提醒(按天)
{ "dayNumber": 1, "dayTitle": "Day1 抵达-接机",
"tips": [ { "title": "本日特别提醒", "content": "今日海拔较高…" } ] }
]
}
```
### 向后兼容(重要)
- **本次升级前已下单的旧订单**`serviceStandard` 返回 `null``dayTips` 返回 `[]`(快照里没有新结构,已实测降级不报错)。前端渲染需判空:`serviceStandard` 为 null 时不画总则区,`dayTips` 为空时不画每日提醒。
- 升级后、且产品已绑模板/配每日提醒的**新订单**才有完整数据(已实测)。
---
## 字段约定
- 所有雪花 ID模板 id、serviceStandardTemplateId**按字符串透传,勿 Number()**。
- `sections` / `serviceTips` / `tips` 为空时分别返回 `[]` 或字段缺省,渲染需判空。
---
## 验证
测试服 9443 已用真 admin token 全链路实测:模板 CRUD/下拉 → 产品绑定 → 行程每日提醒 → 创单冻结快照 → 订单 Tab 返回结构化 serviceStandard + dayTips中文非 null→ 旧订单降级不报错。

查看文件

@ -1,64 +0,0 @@
# 二期 v3 房务管家:日历 4 状态点 + 抢单池 productType + 待办生产者全齐(含取消→退订)
> **服务**: hl-order-service-v3
> **PR**: #3180#3200#3202(hotfix)
> **Issue**: #3179#3188
> **日期**: 2026-05-28
> **影响**: 🟡 房务管家「日历视图 / 抢单池 / 待办 Tab」数据更完整;接口路径与字段结构**不变**,仅字段从恒空/恒 0 变为真实值、待办 Tab 新增几类会真正产生的待办
---
## 总览(前端 mmg 必读)
继上一批房务接口真实化后,本批补齐房务管家剩余的完备性缺口。**接口路径、字段结构均不变**,前端可据此把对应 UI 由"空态/恒 0"切到真实渲染。
---
## 1. 日历视图 4 状态点真实化§2.6)— PR #3180
`GET /admin/house/calendar?month=YYYY-MM&scope=mine|all`
此前每天 / 汇总只有"进行中(inProgress)"有真实计数,"待配房(pending) / 询房中(inquiry) / 异常(exception)"恒为 0。现已全部按真实状态聚合
- **待配房 pending**:未被房务抢单的需求(按订单出发日落格)
- **询房中 inquiry**:有 PENDING 询房的订单
- **异常 exception**:有 OPEN 异常类待办(换酒店 / 退订 / 询房超时等)的订单
- **进行中 inProgress**:已配房且不在以上桶
`summary.{inProgress,pending,inquiry,exception}` 与每天 `statusDots[]`(含 label/count/color均返真实值。优先级异常 > 询房中 > 待配房 > 进行中。
## 2. 抢单池 productType 接通§1.1)— PR #3180
`GET /v3/admin/order/grab-pool/hotel-requirements`
- 列表项 `productType`CORE/GROUP/CUSTOM`productNo` 此前恒 null,现经产品服务真实返回`productType=CORE``productNo=C260520001`)。
- `productType` 作为**查询筛选项现已生效**(按产品类型过滤抢单池)。
- 上游产品服务查不到 / 降级时该字段如实 null,不报错。
## 3. 待办 Tab 生产者全齐§4.1 / §3.4)— PR #3180 + #3200
`GET /v3/admin/order/todos` —— 此前部分待办类型只有"消费/RESOLVE"没有"生产者",对应 Tab 永远空。现补齐,7 类待办均会真正产生:
| 待办类型 | 何时产生(新增/原有) |
|---|---|
| `SWAP_HOTEL` 换酒店 | **新增**:酒店回填**拒单**时自动产(#3180);换酒店完成后自动 RESOLVE |
| `INVENTORY_CHECK_OVERDUE` 核房超期 | **新增**:每日巡检 Job 扫到酒店快照过期自动产(#3180);核房后自动 RESOLVE |
| `REFUND` 退订 | **新增**:订单**确认后取消**(房务已配房锁酒店)时自动产,归属原接单房务(#3200);详见下 |
| `INQUIRY_TIMEOUT` 询房超时 / `INQUIRY_ESCALATED` 询房加急 / `RETURN_TO_HK` 退回房务 | 原有,不变 |
> 注:`UNDELIVERED` 为历史遗留类型(其"询房 4h 无回复"语义已由 `INQUIRY_TIMEOUT` 承接),不再单独产生。
## 4. 订单取消 → 退订待办 + 待办清理§1.3 / §4— PR #3200hotfix #3202
订单取消后房务侧的联动此前未生效(监听链路缺陷),现已接通:
- 订单取消时,该订单房务侧"配房前置工作"类 OPEN 待办(换酒店/询房超时/核房超期等)**自动 RESOLVE**(取消后这些已无意义)。
- 若取消时**房务已配房锁酒店**,自动产一条 `REFUND` 待办,归属原接单房务(无归属则广播),提示其处理酒店退订/释放。`reason` 取订单取消原因。
- 业务规则确认:小蒙马(GROUP)库存在**下单时**扣减、其他产品无库存 —— 配房环节不涉及库存扣减/恢复。
---
## 验证
以上全部已合并 dev-v3 + 测试服双实例部署,并经真机验证:日历 4 状态点真实计数、抢单池 productType 真实返回、酒店拒单产换酒店待办、订单取消产退订待办(取消订单 → SWAP_HOTEL 待办 RESOLVED + REFUND 待办 OPEN 归属原房务)。无接口路径/字段结构变更。
如需任一接口完整响应样例或字段疑问,回我即可。

查看文件

@ -1,87 +0,0 @@
# 二期 v3 房务管家:配房/酒店/员工/城市等接口接通真实数据 + 新增房间分配写端点
> **服务**: hl-order-service-v3+ resource / user-service 内部接口)
> **PR**: #3152#3153#3154#3176#3178
> **Issue**: #3148#3149#3150#3156#3172#3177
> **日期**: 2026-05-28
> **影响**: 🟡 房务管家HOUSE一批此前返空/null/桩的接口已接通真实数据;新增"房间分配写"端点;均已测试服双实例部署 + 真机验证
---
## ⚠️ 总览(前端 mmg 必读)
房务管家相关接口此前多处因依赖未落地而返「空列表 / null 字段 / 占位」。现已跨 order-v3 / resource / user-service 三服务接通真实逻辑并真机验证。**接口路径、字段结构均不变**,只是**字段从空/占位变为真实值**,前端可据此把对应 UI 由"空状态"切到真实渲染。逐项如下。
---
## 1. 新增房间分配写端点§2.5)— PR #3152
此前只有 `GET .../rooms`(按家庭分组查),**没有写入端点**,导致逐日 `assignments` 恒空。现补:
```
POST /v3/admin/order/assignments/{assignmentId}/rooms
```
- 幂等 + 分布式锁保护(重复提交安全)
- 请求体:
```jsonc
{
"rooms": [
{
"roomGroupNo": "F1", // 家庭/房间组号
"travelerCount": 2, // 该房入住人数
"travelerNames": "张三,李四", // 入住人姓名(逗号分隔)
"bedType": "double", // 床型
"remark": "可选备注"
}
]
}
```
- 返回 `{ successCount, items:[{id, roomGroupNo, bedType, travelerCount}] }`
- 写入后 `GET /v3/admin/order/orders/{orderId}/rooms` 的逐日 `assignments` 即非空(含床型/人数明细)
---
## 2. 酒店列表 / 详情接通真实数据§6.1 / §6.2)— PR #3153
`GET /v3/admin/house/hotels`(列表)与 `GET /v3/admin/house/hotels/{hotelId}`(详情 4 Tab此前走 Fallback 返空,现接 resource 真实主数据:
- **§6.1 列表**:返真实酒店(测试库 39 家),字段 `city/cityName/level/settleType/paymentMode/protoPrice/roomTypes/status` 等齐全
- `settleType` 字典 `hotel_settle_type``cash` 现付 / `sign` 签单 / `company` 公司付款
- `paymentMode` 字典 `hotel_payment_mode``prepay` 预付款 / `cash` 现结 / `monthly` 月结
- **§6.2 详情**`basic`(基础信息)/ `roomTypes`(房型,含 `protoPrice` + `facilities` 设施数组)/ `priceCalendar30d`(未来 30 天价格日历)/ `recentCheckLog`(核房记录)四 Tab 均返真实数据
> 注:测试库酒店主数据已回填(结算/付款/协议价/房型价格),便于联调;正式环境以 resource 实际维护的数据为准。
---
## 3. 转单候选员工接通§6.6)— PR #3154 + #3178
`GET /v3/admin/house/staff` 此前 user-service 无对应接口返空,现接通:
- user-service 新增房务员工接口,按**房务角色**筛选:`house_keeper`(房务) / `house_lead`(房务组长)
- 返回 `{ list:[{ userId, name, avatar, online, superAdmin, activeCount }], total }`
- **`activeCount`(在跟订单数)口径**#3178 修正):为该员工**当前仍持有的活跃配房需求数**(精确口径),不再是历史 CLAIM 次数近似 —— 转单选人时的负载/上限判断更准确
---
## 4. 行程城市接通§1.5 / §2.0)— PR #3176
此前 `cities` / `days[].city` 恒为 null。现按各天酒店反查城市接通
- **§1.5 我的接单** `GET /v3/admin/order/grab-pool/my-claims/hotel`:列表项 `cities` 返该单**行程去重城市数组**(如 `["呼伦贝尔市","兴安盟"]`
- **§2.0 订单详情** `GET /admin/house/orders/{orderId}``requirement.current.days[].city` 及行程日历 `itinerary[].cityCode/cityName` 返**中文城市名**
- 数据缺失(酒店未维护城市 / 查询降级)时该字段如实 null,不报错
---
## 5. 转单接收人姓名真实化§1.3)— PR #3178
转单后归属人姓名(`claimer_name`,体现在我的接单 / 详情归属人)此前为占位 `user-{id}`,现解析为**真实员工姓名**(经房务员工接口反查;查不到时退回 `user-{id}` 兜底,不影响转单成功)。
---
## 验证
以上全部已合并 dev-v3 + 测试服双实例滚动部署,并经真机(真 admin token,过网关 9443/本地 8080验证房间分配写→查 assignments 非空、酒店列表/详情字段齐全、员工列表真实、cities/city 中文城市、转单后 claimer_name 为真名。无接口路径/字段结构变更,前端按上述把对应 UI 从空态切真实渲染即可。
如需任一接口的完整响应样例,或字段有疑问,回我即可。

查看文件

@ -1,70 +0,0 @@
# 二期 v3 服务标准重构:模板条目改表结构补 6 字段 + 行程点位服务标准实时取资源
> **服务**: hl-product-service-v2 / hl-resource-service / hl-order-service-v3
> **PR**: #3254(模板改表结构)、#3255(点位取资源)、#3256(迁移 hotfix)
> **Issue**: #3253#3250
> **日期**: 2026-05-29
> **影响**: 🟡 服务标准这一页分两块都有字段变化,前端需按 6 字段渲染模板条目 + 在行程点位渲染新的 `serviceStandard`
---
## 总览(前端 mmg 必读)
服务标准(出团服务说明书)这页分**两块**,本次都动了字段:
1. **模板块**(模板标题/提示语/分组[出团注意事项·退费说明·温馨提示]+条目)——条目字段补全为 6 字段。
2. **行程点位块**(每个酒店/景点/活动/服务点位下那段服务标准)——改为**实时从资源取**。
两块**正交**:模板块=出团总则类,点位块=资源自带。已测试服部署 + 验证。
---
## 1. 服务标准模板条目3 字段 → 6 字段(#3254
接口路径不变:`/admin/product/service-standard-template`create/update/detail/page,**响应形状仍是 `sections[].items[]`**。仅条目字段变化:
| 字段 | 说明 | 备注 |
|---|---|---|
| `title` | 主文案 | 必填 |
| `content` | 正文 | 可空 |
| `remark` | 备注/灰色二级说明 | 新增 |
| `color` | 文案颜色 `#RRGGBB` | 新增 |
| `contactName` | 联系人(如「房务·舒馨」) | 新增 |
| `phone` | 手机号(如 15391131404 | 新增 |
- **旧 `contact` 字段移除**(拆成 `contactName` + `phone`)。
- `color` 校验:必须 `#RRGGBB` 格式;**无值传 `null` 或空串 `""` 均可**PR #3257 起空串也放行,仍拒 `red` 等非法值)。
- 后端存储由 JSON 列改为关系表对前端无感,API 形状不变)。
- ✅ 模板详情/列表 RespVO 已返 **`statusLabel`**`ENABLED→启用` / `DISABLED→停用`,PR #3257 起),前端可直接渲染中文,无需自行映射(与装备建议对齐)。
> **补充更新PR #3257**:上面两条「⚠️ 无 statusLabel」「空串会 400」的遗留已修复——`statusLabel` 现已返回;`color` 空串现已放行。
## 2. 订单详情「服务标准」Tab 同步 6 字段(#3254
`GET /v3/admin/order/{orderId}/detail/service-standard``serviceStandard.sections[].items[]` 同步上述 6 字段(快照仍冻结,只是字段变多)。
## 3. 行程点位服务标准:实时从资源取(#3255
`GET /v3/admin/order/{orderId}/itinerary/full``days[].nodes[]` **新增 `serviceStandard` 字段**
- 值**实时按 `resourceId` 从资源(酒店/景点/活动/服务)拉取**,**不冻快照**——运营改了资源的服务标准,**老订单详情同步更新**。
- 资源查不到 / 调用降级时该字段 `null`,不报错。
- 前端在每个点位下渲染这段 `serviceStandard`(区别于点位的电话/联系人)。
## 4. 资源新增「服务标准」字段(#3255
酒店/景点/活动/服务 4 类资源新增 `serviceStandard` 字段admin 编辑表单需加该输入框,最长 10000
- 酒店:`GET/PUT /admin/hotel/item/{id}` 已含 `serviceStandard`
- 景点/活动/服务 admin create/update/detail 同步含该字段
---
## 验证(测试服 dev-v3 已部署 + 实测)
- 模板 6 字段 admin E2E创建含 color/remark/contactName/phone→ 详情回显,6 字段逐项 round-trip 通过;非法 color「red」被 400 拦截;多分组2 组)正确。
- 资源 `serviceStandard`hotel 详情接口已含该字段DB 列 + 内部批量详情已接通)。
- 点位 `NodeVO.serviceStandard`:字段已上线,实时拉取 + 资源缺失降级 null 逻辑已单测覆盖。
- 三服务resource/product-v2/order-v3测试服双实例已部署。
如需任一接口完整响应样例,回我即可。

查看文件

@ -1,296 +0,0 @@
# 二期 v3资源退费说明 CRUD 模块(给司机行程单展示用)
> **服务**: hl-resource-service
> **PR**: #3273
> **Issue**: #3272
> **日期**: 2026-05-30
> **影响**: 🟢 新增能力。给运营在景区/活动详情下"维护退费说明"提供 CRUD 接口;最终用于行程单 PDF 渲染 + 核单 Step 5 录入对账。本期**仅交付资源服务端**,后续 PR 接入产品快照 + PDF + 核单。
---
## 总览(前端 mmg 必读)
每个景区SCENIC和活动ACTIVITY资源都可以挂一份"退费说明",包含资源级备注 + 多条退费明细(标题/金额/单位/备注/生效期)。
**界面**:景区 / 活动列表行加一个按钮 → 弹窗里调本期 3 个接口做 CRUD。
**接口前缀**`/admin/refund-note`**无 `/v3/` 前缀**,因 resource-service 沿用一期路径风格;消费方仍是二期 v3 管理后台)。
**本期不做**:行程单 PDF 渲染、产品 ProductDetailVO 注入 refundNote、订单 OrderProductSnapshotContent 反序列化、核单 Step 5 录入对接。这些留后续 PR。
---
## 接口清单3 个)
### 1. 查询单资源的退费说明
```
GET /admin/refund-note?resourceType=SCENIC&resourceId=12345
```
**入参**query
| 字段 | 类型 | 必填 | 枚举 | 说明 |
|---|---|---|---|---|
| `resourceType` | string | ✅ | `SCENIC` / `ACTIVITY` | 资源类型 |
| `resourceId` | long | ✅ | - | 资源 ID字符串透传雪花|
**返回**
```jsonc
{
"code": 200,
"data": { // 未配置时直接返 null,前端按 null 隐藏弹窗内容
"noteId": "20596378108368322580", // 雪花 ID 字符串透传
"resourceType": "SCENIC",
"resourceId": "12345",
"intro": "苔藓为赠送项目,不退费",
"items": [
{
"title": "成人未参加",
"amount": 44.00,
"unitLabel": "/人", // 展示文案,给人看
"settleScope": "PER_PERSON", // 结算粒度枚举,给规则引擎用
"remark": "",
"effectiveFrom": null, // yyyy-MM-dd
"effectiveTo": null
},
...
],
"createTime": "2026-05-30 10:15:00",
"updateTime": "2026-05-30 10:15:00"
}
}
```
**枚举**
- `settleScope`
- `PER_PERSON` 按人结算(默认)
- `PER_TEAM` 按团结算(如寻龙诀 100/团 整团一次性)
- `PER_VEHICLE` 按车辆结算(如卡丁车 2 人/辆,按辆数乘单价)
**注意**`unitLabel`"/人" / "/团" / "/辆")和 `settleScope` **职责不同**
- 卡丁车场景:`unitLabel="/人"`(给客户看是 120/人)+ `settleScope=PER_VEHICLE`(引擎按车辆数乘单价)
- 两个字段独立维护,前端展示用 `unitLabel`,未来规则引擎用 `settleScope`
---
### ⚡ 更新2026-06-01 PR #3291settleScope 字典化 + 出参补 settleScopeLabel 中文
后端**已落字典 + 接口出参直接返中文**,前端**不用自己维护英文→中文映射**。两种用法二选一:
#### 用法 A直接用出参的 `settleScopeLabel`(展示场景推荐)
`GET /admin/refund-note` 返回的 `items[]` 每条**新增 `settleScopeLabel` 字段**
```jsonc
"items": [
{
"title": "成人未参加",
"settleScope": "PER_PERSON",
"settleScopeLabel": "按人", // 🆕 后端字典拼装,直接展示
...
}
]
```
**列表 / 详情 UI 直接 `{{ item.settleScopeLabel }}` 渲染**即可,无需自己映射。
#### 用法 B调字典接口拉下拉框选项编辑场景推荐
管理后台编辑退费规则时,需要下拉框让运营选"按人/按团/按车"。调统一字典接口:
```http
GET /internal/dict/data?dictType=order_refund_settle_scope
Authorization: Bearer <admin-token>
→ Result<List<SysDictDataDTO>>:
[
{ "dictValue": "PER_PERSON", "dictLabel": "按人", "sortOrder": 10 },
{ "dictValue": "PER_TEAM", "dictLabel": "按团", "sortOrder": 20 },
{ "dictValue": "PER_VEHICLE", "dictLabel": "按车", "sortOrder": 30 }
]
```
- 下拉框 `option.label = dictLabel``option.value = dictValue`
- 提交时 `req.items[].settleScope = dictValue`(提交 `settleScopeLabel` 后端会忽略,label 由后端拼)
- 建议前端**全局缓存字典 5-10 分钟**避免高频请求;如果有统一字典组件直接复用
#### 容错(前端可忽略)
- 字典服务异常 / 字典数据缺失 → `settleScopeLabel = null`,前端展示 fallback 用 `settleScope` 英文原值即可
- 字典文案后续改了(如"按人" → "按人头")→ **运维改 sys_dict_data + 清 Redis 缓存即生效**,后端无需重启,前端无需发版
#### 字典元信息
| 项 | 值 |
|---|---|
| `dict_type` | `order_refund_settle_scope` |
| `dict_name` | 退费明细结算粒度 |
| 字典数据 | PER_PERSON→按人 / PER_TEAM→按团 / PER_VEHICLE→按车 |
| 字典存储 | `hl_user_service.sys_dict_data` |
| 缓存 key | `cache:dict:data:order_refund_settle_scope` |
### 2. 保存upsert 整块)
```
PUT /admin/refund-note
```
**入参**body
```jsonc
{
"resourceType": "SCENIC",
"resourceId": 12345,
"intro": "苔藓为赠送项目,不退费",
"items": [
{
"title": "成人未参加", // 必填
"amount": 44.00, // 必填,>= 0赠送项填 0
"unitLabel": "/人", // 默认 "/人",可省略
"settleScope": "PER_PERSON", // 默认 "PER_PERSON",可省略
"remark": "",
"effectiveFrom": null,
"effectiveTo": null
}
]
}
```
**约束**
- `resourceType`:必须是 `SCENIC` / `ACTIVITY`
- `resourceId`**必须在 scenic_spot / activity 表存在且未软删**,否则返 390805 "关联资源不存在"
- `items`:至少 1 条,最多 50 条
- `items[].amount`>= 0赠送项目填 0
- `items[].settleScope`:必须是上述 3 枚举之一null 时默认 PER_PERSON
- `items[].effectiveFrom` <= `items[].effectiveTo`(同时存在时)
**返回**
```jsonc
{ "code": 200, "data": "20596378108368322580" } // 落库后的 noteId字符串透传
```
**语义**upsert——按 (resourceType, resourceId) 找现有记录,有则**整块覆盖**(不做 item 级 diff,无则 insert。
### 3. 软删整份
```
DELETE /admin/refund-note?resourceType=SCENIC&resourceId=12345
```
**入参**:同 GET。
**返回**`{ "code": 200, "data": true }`(不存在或已软删返 false,不报错
软删用主键自身写 deleted_at,UNIQUE KEY 永不撞键,支持同资源无限次"删→重建"。
---
## 错误码(段位 39080x
| code | 错误信息 |
|---|---|
| 390801 | 退费明细金额必须 ≥ 0 |
| 390802 | 退费明细生效起日不能晚于止日 |
| 390803 | 退费明细结算粒度非法: {0} |
| 390804 | 退费说明资源类型非法: {0} |
| 390805 | 关联资源不存在: type={0}, id={1} |
非业务错误(参数校验失败)走通用 400。
---
## 业务边界
- **资源类型**:一期仅 `SCENIC` + `ACTIVITY`,其他资源(餐饮/酒店/物资等)有各自退订/退款政策,不复用本结构。
- **一份生效**同资源同时刻只有一份生效的退费说明DB 唯一键 `(resource_type, resource_id, deleted_at)` 保证)。
- **关联校验**:保存时校验 resourceId 在主资源表存在,防孤儿数据。
- **资源软删后**:本退费说明仍存在但孤儿(查不到对应资源),不主动清理;运营侧需手动删除。
---
## 数据示例(典型)
### 白桦林4 条规则)
```jsonc
{
"resourceType": "SCENIC", "resourceId": 12345,
"intro": "苔藓为赠送项目, 不退费",
"items": [
{"title": "儿童/学生/无证件未参加", "amount": 15, "unitLabel": "/人", "settleScope": "PER_PERSON"},
{"title": "免票座电瓶车", "amount": 30, "unitLabel": "/人", "settleScope": "PER_PERSON"},
{"title": "白桦林未参加", "amount": 44, "unitLabel": "/人", "settleScope": "PER_PERSON"},
{"title": "桦树皮画未参加", "amount": 50, "unitLabel": "/人", "settleScope": "PER_PERSON", "remark": "仅儿童"}
]
}
```
### 寻龙诀(按团结算)
```jsonc
{
"resourceType": "ACTIVITY", "resourceId": 99999,
"items": [
{"title": "寻龙诀未参加", "amount": 100, "unitLabel": "/团", "settleScope": "PER_TEAM"}
]
}
```
### 卡丁车(按车结算,文案 /人)
```jsonc
{
"resourceType": "ACTIVITY", "resourceId": 88888,
"items": [
{"title": "卡丁车未骑", "amount": 120, "unitLabel": "/人", "settleScope": "PER_VEHICLE",
"remark": "2 人/辆共享单价"}
]
}
```
### 套娃(时间窗口)
```jsonc
{
"resourceType": "SCENIC", "resourceId": 77777,
"items": [
{"title": "老人只看大马戏", "amount": 65, "settleScope": "PER_PERSON"},
{"title": "6/25 后没去或免票", "amount": 165, "settleScope": "PER_PERSON",
"effectiveFrom": "2026-06-25", "effectiveTo": null}
]
}
```
---
## 影响评估
- **后端**:仅 hl-resource-service 新增 1 张表 `resource_refund_note` + 3 个 admin 接口,对其他业务零影响。
- **DB**Flyway `V20260529_002__create_resource_refund_note.sql` 自动建表,重启服务后生效。
- **前端**:新增弹窗 UI建议在景区 / 活动详情页加按钮 → 弹窗 CRUD,完全独立的新页面,老页面不影响。
---
## 注意事项
1. 雪花 ID 透传为字符串(`noteId` / `resourceId`),前端**不要 `Number()`**。
2. 资源软删后的退费说明不会自动清理,运营侧需先删退费说明再删资源(否则成孤儿数据)。
3. 同资源不可有 2 份生效退费说明DB 唯一键约束)。
4. 删除是软删,可同资源重建;不支持物理删除。
---
## 关联
- **Issue**: [#3272](https://git.1814.love:8443/wx/HL/issues/3272)
- **PR**: [#3273](https://git.1814.love:8443/wx/HL/pulls/3273) - feat(resource): 新增资源退费说明 CRUD 模块
- **Commit**: [ffae5ab90](https://git.1814.love:8443/wx/HL/commit/ffae5ab90)
- **后续 PR**(不在本期):
- 产品服务 `ProductDetailVO.NodeItem.refundNote` 注入(参考 #3147 serviceStandard 模式)
- 订单 `OrderProductSnapshotContent` 加 refundNote 反序列化10 行)
- 行程单 PDF 渲染退费表(订单 v3 + PDF 服务)
- 核单 Step 5 录入半结构化对接(可选)

查看文件

@ -1,149 +0,0 @@
# 二期 v3订单 orderStatus / flowStatus 字段补 label 中文映射
> **服务**: hl-order-service-v3
> **PR**: #3284
> **Issue**: #3283
> **日期**: 2026-05-30
> **影响**: 🟢 **非破坏性**新增字段。前端从前自行映射 `AWAITING_PROFILE → "待补全信息"` 等,现在后端直接给。老字段 `orderStatus` / `flowStatus`(英文枚举)保留不动,新增 `orderStatusName` / `flowStatusName` 中文 label 同时返回。
---
## 总览(前端 mmg 必读)
订单列表 / 详情 / 创建响应里的 `orderStatus``flowStatus` 字段历史上**只返英文枚举**`CUSTOMIZING` / `AWAITING_PROFILE` 等),前端要查表自行翻译。本期后端直接拼好中文 label 返回,前端**直接用 `xxxStatusName` 字段展示**即可。
旧字段保留,前端老逻辑零改动。
---
## 接口清单3 个响应增字段)
### 1. 订单列表
```
GET /v3/admin/order
```
**响应** `data.records[].xxx` 新增 2 字段:
```jsonc
{
"orderStatus": "CUSTOMIZING",
"orderStatusName": "定制中", // 🆕 中文名
"flowStatus": "AWAITING_PROFILE",
"flowStatusName": "待补全信息" // 🆕 中文名
}
```
### 2. 订单详情
```
GET /v3/admin/order/{id}/detail
```
**响应** `data.main` 同样新增 2 字段:
```jsonc
"main": {
"orderStatus": "TRAVELLING",
"orderStatusName": "出行中", // 🆕
"flowStatus": "TRAVELLING",
"flowStatusName": "出行中" // 🆕
}
```
### 3. 创建订单响应
```
POST /v3/admin/order
```
**响应** `data` 新增 1 字段(`orderStatusName` 早期已有,本期补 `flowStatusName`
```jsonc
{
"orderStatus": "PENDING_PAY",
"orderStatusName": "待支付",
"flowStatus": "AWAITING_PAY",
"flowStatusName": "待支付" // 🆕
}
```
---
## 状态枚举完整对照表
### `orderStatus`(粗状态 6 个)
| 枚举值 | 中文名 |
|---|---|
| `PENDING_PAY` | 待支付 |
| `CUSTOMIZING` | 定制中 |
| `PENDING_DEPARTURE` | 待出行 |
| `TRAVELLING` | 出行中 |
| `COMPLETED` | 已完成 |
| `CANCELLED` | 已取消 |
### `flowStatus`(细状态 16 个)
| 枚举值 | 中文名 |
|---|---|
| `AWAITING_PAY` | 待支付 |
| `AWAITING_PROFILE` | 待补全信息 |
| `AWAITING_HOTEL_SUBMIT` | 待提交房型 |
| `AWAITING_HOTEL_CLAIM` | 待抢房 |
| `HOTEL_IN_PROGRESS` | 房控处理中 |
| `HOTEL_NEED_ADJUST` | 房控需调整 |
| `AWAITING_VEHICLE_SUBMIT` | 待提交用车 |
| `VEHICLE_IN_PROGRESS` | 车控处理中 |
| `VEHICLE_NEED_ADJUST` | 车控需调整 |
| `PENDING_CONFIRM` | 待确认 |
| `PENDING_DEPARTURE` | 待出行 |
| `TRAVELLING` | 出行中 |
| `PENDING_REVIEW` | 待核单 |
| `REVIEWING` | 核单中 |
| `SETTLED` | 已结算 |
| `COMPLETED` | 已完成 |
| `CANCELLED` | 已取消 |
> ⚠️ **注意**:本 PR 后紧跟的 PR #3286 改了其中 2 个 label —— `待提交房型 → 待提交配房需求``待提交用车 → 待配车需求`。**实际部署后看到的是 PR #3286 的新文案**,前端如果按这俩老文案做字符串硬比较需要更新(详见 PR #3286 changelog
---
## 容错(前端可忽略)
- 后端拿到 null 或未知枚举(历史脏数据)会回退:返回原值而不是抛 500,前端不会看到"待x"等乱码。
- 即便后端返回原英文枚举值(如未知 `XXXXX`),前端展示也能落 fallback。
---
## 业务边界
- 老字段 `orderStatus` / `flowStatus` 保留英文枚举(**契约不变**
- 仅新增 `orderStatusName` / `flowStatusName` 中文
- 前端**老逻辑零改动**也能跑(旧字段还在)
- 改成展示 `xxxStatusName` 后,前端不需要自己维护英文 → 中文映射表
---
## 影响评估
- **后端**:仅 hl-order-service-v3 改了 5 个文件3 VO + Converter + OrderService,无 DB 改动,重启服务后生效。
- **前端**:可选改造——把硬编码映射表删掉,直接用 `xxxStatusName`。改不改都不影响功能。
- **mp 端**:本 PR 暂未改 `OrderLookupMpService`(仍是 mock 假数据),真业务化时一起补。
---
## 注意事项
1. 如果你的前端代码有硬编码的英文→中文映射表(如 `{ AWAITING_PROFILE: "待补全信息" }`),建议删掉,改用 `flowStatusName` 字段,避免后端枚举改了文案前端跟不上。
2. 文案的"权威源"是后端枚举(`OrderStatus` / `OrderFlowStatus`),运营如果要求改文案直接改后端,前端无感跟随。
---
## 关联
- **Issue**: [#3283](https://git.1814.love:8443/wx/HL/issues/3283)
- **PR**: [#3284](https://git.1814.love:8443/wx/HL/pulls/3284) - fix(order-v3): orderStatus/flowStatus 补 label 中文映射
- **Commit**: [cb85f0f76](https://git.1814.love:8443/wx/HL/commit/cb85f0f76)
- **接续 PR**: [#3286](https://git.1814.love:8443/wx/HL/pulls/3286) - feat(order-v3): 8 步步骤条字段 + 修 2 文案(紧接本 PR,建议一起看

查看文件

@ -1,204 +0,0 @@
# 二期 v3订单接口加 flowStep / flowStepTotal / flowDisplayText 步骤条字段 + 修 2 个文案
> **服务**: hl-order-service-v3
> **PR**: #3286
> **Issue**: #3285
> **日期**: 2026-05-30
> **影响**: 🟡 **非破坏性**新增字段 + **文案修订**。前端从前自己拼 `"1/8 · 待补全信息"`,现在后端直接给 `flowStep` / `flowStepTotal` / `flowDisplayText` 三字段,前端拿着就显示。同时修了 2 个枚举 label 文案。
---
## 总览(前端 mmg 必读)
接续 PR #3284status label 中文),本期把"订单步骤条"所需数据全部下沉到后端:
```jsonc
{
"flowStep": 1, // 🆕 当前步序号 0=待支付前, 1-8=进行中, null=终态/未知
"flowStepTotal": 8, // 🆕 总步数固定 8
"flowDisplayText": "待补全信息" // 🆕 中文文案,只中文不带"X/8 · ",前端按需自拼
}
```
前端从前的 `currentStep / totalSteps + 自己映射中文` 逻辑全部可删,直接用本期 3 字段。
**同时**:修了 2 个 `OrderFlowStatus` 枚举 label 文案(影响 `flowStatusName` 字段返回):
| 枚举值 | 旧文案 | 新文案 |
|---|---|---|
| `AWAITING_HOTEL_SUBMIT` | 待提交房型 | **待提交配房需求** |
| `AWAITING_VEHICLE_SUBMIT` | 待提交用车 | **待配车需求** |
---
## 接口清单3 个响应增字段)
### 1. 订单列表
```
GET /v3/admin/order
```
**响应** `data.records[].xxx` 在 PR #3284 基础上再加 3 字段:
```jsonc
{
"orderStatus": "CUSTOMIZING",
"orderStatusName": "定制中",
"flowStatus": "AWAITING_PROFILE",
"flowStatusName": "待补全信息",
"flowStep": 1, // 🆕
"flowStepTotal": 8, // 🆕
"flowDisplayText": "待补全信息" // 🆕
}
```
### 2. 订单详情
```
GET /v3/admin/order/{id}/detail
```
**响应** `data.main` 同样加 3 字段(位置和列表相同)。
### 3. 创建订单响应
```
POST /v3/admin/order
```
**响应** 加 3 字段;创单初态固定:
```jsonc
{
"flowStep": 0,
"flowStepTotal": 8,
"flowDisplayText": "待支付"
}
```
---
## 8 步映射表16 → 8
**优先级**`orderStatus` 终态 > `flowStatus` 步骤映射
| 触发条件 | `flowStep` | `flowDisplayText` |
|---|---|---|
| `orderStatus = CANCELLED` | `null` | "已取消" |
| `orderStatus = COMPLETED` | `null` | "已完成" |
| `flowStatus = AWAITING_PAY` | `0` | "待支付" |
| `flowStatus = AWAITING_PROFILE` | `1` | "待补全信息" |
| `flowStatus = AWAITING_HOTEL_SUBMIT` | `2` | **"待提交配房需求"** |
| `flowStatus = AWAITING_HOTEL_CLAIM` | `2` | "待抢房" |
| `flowStatus = HOTEL_IN_PROGRESS` | `2` | "房控处理中" |
| `flowStatus = HOTEL_NEED_ADJUST` | `2` | "房控需调整" |
| `flowStatus = AWAITING_VEHICLE_SUBMIT` | `3` | **"待配车需求"** |
| `flowStatus = VEHICLE_IN_PROGRESS` | `3` | "车控处理中" |
| `flowStatus = VEHICLE_NEED_ADJUST` | `3` | "车控需调整" |
| `flowStatus = PENDING_CONFIRM` | `4` | "待确认" |
| `flowStatus = PENDING_DEPARTURE` | `5` | "待出行" |
| `flowStatus = TRAVELLING` | `6` | "出行中" |
| `flowStatus = PENDING_REVIEW` | `7` | "待核单" |
| `flowStatus = REVIEWING` | `8` | "核单中" |
| `flowStatus = SETTLED` | `8` | "已结算" |
**说明**
- 配房 4 个并行子态(`AWAITING_HOTEL_SUBMIT` / `_CLAIM` / `HOTEL_IN_PROGRESS` / `_NEED_ADJUST`)都归到 step 2
- 配车 3 个并行子态都归到 step 3
- `REVIEWING``SETTLED` 都归到 step 8结算后整体收尾
- `CANCELLED` / `COMPLETED` 是终态,不在 8 步串行里,`flowStep` 返 null
- 未知 / 历史脏数据:`flowStep=null`, `flowDisplayText=` 原英文值fallback 不抛错)
---
## 前端如何用(推荐)
### 推荐方式 1只显示 `flowDisplayText`(最简单)
```html
<div class="status">{{ order.flowDisplayText }}</div>
<!-- 渲染结果: 待补全信息 / 出行中 / 已取消 等 -->
```
### 推荐方式 2进度条 + 中文(拼分子分母)
```html
<div v-if="order.flowStep !== null">
{{ order.flowStep }}/{{ order.flowStepTotal }} · {{ order.flowDisplayText }}
</div>
<div v-else>
{{ order.flowDisplayText }} <!-- 终态不带 X/8 -->
</div>
<!-- 渲染示例: "1/8 · 待补全信息" / "已取消" -->
```
### 推荐方式 3步骤条 UI按 flowStep 高亮)
如果有 8 段步骤条 UI 组件,按 `flowStep` 高亮当前段:
```jsonc
const stepNames = ["待支付", "补全信息", "配房需求", "配车需求",
"确认", "出行准备", "出行", "核单"];
// 用 flowStep 高亮 stepNames[flowStep - 1]
```
---
## 文案变更详情(重点关注)
| 字段 | 旧值 | 新值 |
|---|---|---|
| `flowStatusName`PR #3284 字段)| "待提交房型" | "待提交配房需求" |
| `flowStatusName` | "待提交用车" | "待配车需求" |
| `flowDisplayText`(本 PR 字段)| - | 同上 |
**前端需要确认的事**
1. 如果有按 **"待提交房型"** / **"待提交用车"** 老文案做字符串硬比较(如 `if (status === "待提交房型")`),需要改成新文案或改用枚举值比较。
2. 如果只是 **展示**(不做逻辑判断),无需改动——文案直接显示新值即可。
我们后端搜了一遍**未发现**前端这个老文案的硬比较,但前端代码后端看不到,**请前端 mmg 自己 grep 确认**。
---
## 容错(前端可忽略)
- `flowStatus` 是历史脏数据(枚举里没有)→ `flowStep = null`, `flowDisplayText = 原始值`
- `orderStatus = null``flowStep = null`, `flowDisplayText = "未知"`
- 前端按 `flowStep === null` 判断终态/未知,按 `flowDisplayText` 兜底展示,绝不会拿到空字符串。
---
## 业务边界
- 老字段 `flowStatus` / `flowStatusName` 保留(契约不变)
- 仅新增 3 字段 + 修订 2 个枚举 label 文案
- 8 步是**前端展示概念**,后端的 `OrderFlowStatus` 仍是 16 个细状态DB 落地不变)
- 步骤条只是**展示视角**的简化,业务逻辑仍按 16 个 `flowStatus`
---
## 影响评估
- **后端**hl-order-service-v3 改了 7 个文件(枚举 + Converter + 3 VO + Service + 测试),无 DB 改动,重启服务后生效。
- **前端**:可选改造——把"1/8"拼接逻辑改用后端 3 字段。改不改都不影响功能(老逻辑还能跑)。
- **mp 端**:本 PR 暂未改 mpmock service,真业务化时一并补。
---
## 注意事项
1. **文案修订是契约变更**:如果前端按老文案做字符串硬比较,必须改。展示用的话无影响。
2. **flowStep 可能为 null**:终态(已取消/已完成/历史脏数据)时为 null,前端按 null 处理"不显示分子"或"显示终态文案"。
3. **创单后立即调列表**:会看到 `flowStep=0` + `flowDisplayText="待支付"`,符合"还没开始走流程"语义。
---
## 关联
- **Issue**: [#3285](https://git.1814.love:8443/wx/HL/issues/3285)
- **PR**: [#3286](https://git.1814.love:8443/wx/HL/pulls/3286) - feat(order-v3): 订单接口加 8 步步骤条字段 + 修 2 个文案
- **Commit**: [fd533fdaa](https://git.1814.love:8443/wx/HL/commit/fd533fdaa)
- **前置 PR**: [#3284](https://git.1814.love:8443/wx/HL/pulls/3284) - status label 中文(建议一起看)

查看文件

@ -1,81 +0,0 @@
# 二期 v3修复服务标准条目 remark/color/contactName/phone 四字段下单时丢失(恒为 null
> **服务**: hl-order-service-v3
> **PR**: #3295
> **Issue**: #3294
> **日期**: 2026-06-01
> **影响**: 🟢 **缺陷修复**,无契约变更。订单详情「服务标准」Tab 的条目,此前 `remark` / `color` / `contactName` / `phone` 四字段因后端缺陷恒为 `null`,本次修复后**新建订单**会正确返回这四个字段的真实值。响应结构不变,前端无需改代码,原本就该读这四字段的渲染逻辑现在能拿到数据。
---
## 总览(前端 mmg 必读)
接续 PR #3254/#3255/#3256(服务标准模板改 6 字段),那一期把产品侧服务标准升级到 6 字段条目,但**下单写订单快照的中间环节漏跟进**
订单详情服务标准来自下单时冻结的产品快照(`order_product_snapshot`)。该快照由订单服务接收产品 Feign 数据后整体序列化,但订单侧接收用的 VO 还停留在旧 3 字段(`title` / `content` / `contact`),导致产品发来的 `remark` / `color` / `contactName` / `phone` 在反序列化时被静默丢弃,冻进快照只剩 `title` + `content`
**结果**:所有订单详情服务标准 Tab 的条目这 4 个字段恒为 `null`。本次补齐订单侧 VO 字段,使整条服务标准 6 字段完整进快照。
---
## 受影响接口(响应行为修正,无字段增减)
```
GET /v3/admin/order/{id}/service-standard
```
**响应** `data.serviceStandard.sections[].items[]` 条目,6 字段修复前后对比:
```jsonc
// 修复前4 字段恒 null
{
"title": "专业司机",
"content": "持有A1驾照,8年以上驾龄",
"remark": null, // ❌ 恒 null
"color": null, // ❌ 恒 null
"contactName": null, // ❌ 恒 null
"phone": null // ❌ 恒 null
}
// 修复后(新建订单)
{
"title": "专业司机",
"content": "持有A1驾照,8年以上驾龄",
"remark": "仅限指定时段", // ✅
"color": "#FF6600", // ✅
"contactName": "李师傅", // ✅
"phone": "13800000000" // ✅
}
```
字段名、类型、嵌套层级**完全不变**,仅是原本恒 null 的值现在有了真实数据。
---
## 重要边界:只对「新建订单」生效
- 快照是**下单时间点的冻结值**,修复部署前已生成的历史订单快照仍只有 `title` + `content`,这 4 字段对老订单仍为 `null`(不回填,符合快照语义)。
- 修复部署后**新建**的订单,服务标准 6 字段完整。
- 前端渲染需对这 4 字段做 `null` 容错(老订单仍可能为 null,有值即展示。
---
## 测试服验证(已通过)
部署 dev-v3 后端到端实测:建含 6 字段服务标准模板的产品订单 → `GET .../service-standard` 与 DB 快照 `snapshot_content` 双查,6 字段(含 remark/color/contactName/phone全部正确返回。
---
## 影响评估
- **后端**hl-order-service-v3 改 1 个传输 VO`ProductDetailVO.ServiceStandard.Item` 补 4 字段、删废弃 `contact`+ 1 个端到端回归测试。无 DB 改动,重启生效。
- **前端**:无需改代码,原服务标准 Tab 条目渲染逻辑现在能拿到 4 个新字段值。
- **mp 端**:本次未涉及。
---
## 关联
- **Issue**: [#3294](https://git.1814.love:8443/wx/HL/issues/3294)
- **PR**: [#3295](https://git.1814.love:8443/wx/HL/pulls/3295) - fix(order-v3): 补齐服务标准条目 6 字段,使下单快照完整冻结
- **前置**: PR #3254/#3255/#3256(服务标准模板改 6 字段)

查看文件

@ -1,85 +0,0 @@
# 二期 v3退费说明接入产品详情/订单快照 + 修复 /admin/refund-note 网关路由 404
> **服务**: hl-resource-service / hl-product-service-v2 / hl-order-service-v3 / hl-gateway
> **PR**: #3308(接入)+ #3312hotfix
> **Issue**: #3307 / #3311
> **日期**: 2026-06-01
> **影响**: 🟢 新增能力 + 🔴 **修复 #3272 退费说明 admin CRUD 经网关 404 完全不可用**。前端现可正常调 `/admin/refund-note` 维护退费说明;退费说明已接入产品行程节点聚合并在下单时冻进订单快照。
---
## 一、前端必读:`/admin/refund-note` 现在可达了(之前 404
#3272 交付了「资源退费说明 CRUD」景区/活动维护退费明细),但**漏配网关路由**,导致 `/admin/refund-note` 经网关一直返:
```json
{"code":404,"message":"接口不存在: /admin/refund-note"}
```
本次已补网关路由。**前端原本对着 #3272 文档写的退费说明 CRUD 弹窗,现在能真正调通了**。三个接口(与 #3272 文档一致,无变化):
```
GET /admin/refund-note?resourceType=SCENIC&resourceId=12345 查
PUT /admin/refund-note upsert 整块
DELETE /admin/refund-note?resourceType=SCENIC&resourceId=12345 软删
```
测试服已实测三个方法经网关全通200。仅 `SCENIC` / `ACTIVITY` 两类资源支持。
---
## 二、退费说明接入产品行程节点 + 订单快照(后端能力)
行程节点绑定 SCENIC/ACTIVITY 资源时,产品详情聚合会回填该资源的退费说明;**下单时随产品详情整体冻进 `order_product_snapshot`**,锁定退费条款(资源后改不影响老订单)。
冻结结构(订单产品快照 `itinerary[].nodes[].refundNote`
```jsonc
{
"refundNote": {
"intro": "苔藓为赠送项目, 不退费",
"items": [
{
"title": "成人未参加",
"amount": 44.00,
"unitLabel": "/人",
"settleScope": "PER_PERSON",
"settleScopeLabel": "按人", // 字典中文,冻结即定格
"remark": "凭票退",
"effectiveFrom": null,
"effectiveTo": null
}
]
}
}
```
仅 SCENIC/ACTIVITY 节点、且资源配过退费说明时才有值,其余节点 / 未配置为 `null`
---
## 三、暂未做(前端注意)
**订单详情「行程安排 Tab」暂不展示退费说明**。原因:订单 `order_itinerary_node` 表当前下单时不物化(仅后台手动编辑写入),退费说明冻在 `order_product_snapshot` JSON 里。要在行程 Tab 逐节点展示,需后续补「快照→节点叠加」或「下单物化行程」,**不在本期**。本期只保证:①退费说明 admin CRUD 可用 ②退费说明已正确冻进订单快照(数据已锁定,前端展示链路待后续 PR
---
## 四、踩坑修复hotfix #3312
测试服建单实测发现退费说明进快照恒 null,直查资源接口暴露 **500 ClassCastException**`JacksonTypeHandler` 反序列化 JSON 列 `List<RefundNoteItem>` 时泛型擦除,运行期元素是 `LinkedHashMap`,用 `List<RefundNoteItem>` 流式处理时 lambda 入口被插 CHECKCAST 在转换前就崩。已改 `List<?>` + `BeanUtil` 兼容(#3272 Converter 同源潜伏 bug 一并修),并补 `LinkedHashMap` 模拟单测锁回归。对前端无感,仅说明为何要 hotfix。
---
## 五、影响评估
- **后端**:资源(DTO+批量查+ClassCast 修)、产品(节点回填)、订单(快照 VO)、网关(路由);hl-common ResourceDetailDTO 加 refundNote 字段(非破坏)
- **前端**:可立即接入 `/admin/refund-note` CRUD之前 404 不可用);订单详情退费说明展示等后续 PR
- **存量订单**:仅新建订单冻结退费说明(快照语义),老订单不回填
---
## 关联
- **Issue**: [#3307](https://git.1814.love:8443/wx/HL/issues/3307) / [#3311](https://git.1814.love:8443/wx/HL/issues/3311)
- **PR**: [#3308](https://git.1814.love:8443/wx/HL/pulls/3308)(接入)/ [#3312](https://git.1814.love:8443/wx/HL/pulls/3312)hotfix + 网关路由)
- **前置**: [#3273](https://git.1814.love:8443/wx/HL/pulls/3273)#3272 退费说明 CRUD 模块)

查看文件

@ -1,280 +0,0 @@
# 景区管理新增「是否收费」字段
- **端类型**:管理后台
- **变更类型**:修改接口
- **日期**2026-06-01
- **Issue**[#3313](https://git.1814.love:8443/wx/HL/issues/3313)
- **PR**[#3314](https://git.1814.love:8443/wx/HL/pulls/3314)
- **Commit**[62bd9be](https://git.1814.love:8443/wx/HL/commit/62bd9be8b9ac67bbb2493d0dcb6f2be41172684b)
- **后端负责人**腰苏图yst@1814.love
---
## ① 接口背景
景区资源新增「是否收费」属性,用于在管理后台创建/编辑景区时标记该景区是否需要游客付费(如收取门票费)。该字段同步透出到列表和详情接口,便于运营人员在列表页一眼识别收费/免费景区。字典来源 `sys_yes_no`,后端已配置,前端无需额外配置。
---
## ② 变更清单
| 接口 | 方法 | 路径 | 变更内容 |
|------|------|------|----------|
| 创建景区 | POST | `/admin/scenic/spot` | 入参新增 `isCharged`Integer,默认 0 |
| 编辑景区 | PUT | `/admin/scenic/spot/{scenicId}` | 入参新增 `isCharged`Integer,可选 |
| 景区详情 | GET | `/admin/scenic/spot/{scenicId}` | 出参新增 `isCharged` + `isChargedLabel` |
| 景区列表 | GET | `/admin/scenic/spots` | 出参新增 `isCharged` + `isChargedLabel` |
所有变更均为**向后兼容新增字段**,不涉及字段改名或删除。
---
## ③ 接口详情
| 项目 | 说明 |
|------|------|
| **认证方式** | 管理后台 JWT Token,请求头 `Authorization: Bearer {token}` |
| **幂等性** | POST 创建非幂等;PUT 更新幂等(相同 scenicId 可重复调用) |
| **限流** | 无单独限流配置,走网关全局限流 |
| **权限** | 管理员角色,普通前端用户无访问权限 |
---
## ④ 接口入参
### 4.1 路径参数 / Query 参数
**PUT `/admin/scenic/spot/{scenicId}`**
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| scenicId | Long | 是 | 景区 ID路径参数 |
**GET `/admin/scenic/spots`**(列表,原有参数不变,无新增查询参数)
### 4.2 请求体字段(新增字段,含于已有请求体中)
**POST `/admin/scenic/spot`**`ScenicSpotCreateReqVO`,新增字段)
| 字段名 | 类型 | 必填 | 校验规则 | 默认值 | 说明 |
|--------|------|------|----------|--------|------|
| isCharged | Integer | 否 | Min=0, Max=1 | 0 | 是否收费,1=是 0=否 |
**PUT `/admin/scenic/spot/{scenicId}`**`ScenicSpotUpdateReqVO`,新增字段)
| 字段名 | 类型 | 必填 | 校验规则 | 默认值 | 说明 |
|--------|------|------|----------|--------|------|
| isCharged | Integer | 否 | Min=0, Max=1 | null不传则不修改 | 是否收费,1=是 0=否 |
---
## ⑤ 出参字段
### GET `/admin/scenic/spot/{scenicId}` 详情(`ScenicSpotVO`,新增字段)
| 字段名 | 类型 | 说明 | 可能为 null |
|--------|------|------|-------------|
| isCharged | Integer | 是否收费,1=是 0=否 | 否DB 默认 0 |
| isChargedLabel | String | 是否收费中文文本,取自字典 sys_yes_no"是"/"否" | 是(字典缺失时为 null |
### GET `/admin/scenic/spots` 列表(`ScenicSpotListVO`,新增字段)
| 字段名 | 类型 | 说明 | 可能为 null |
|--------|------|------|-------------|
| isCharged | Integer | 是否收费,1=是 0=否 | 否DB 默认 0 |
| isChargedLabel | String | 是否收费中文文本,取自字典 sys_yes_no"是"/"否" | 是(字典缺失时为 null |
---
## ⑥ 枚举 / 数据字典
### sys_yes_no 字典(后端已配置,前端无需维护)
| 字典值isCharged | 中文标签isChargedLabel | 说明 |
|--------------------|-----------------------------|------|
| 1 | 是 | 该景区收费 |
| 0 | 否 | 该景区免费 |
> `isChargedLabel` 由后端查字典自动翻译,前端可直接展示。若字典配置缺失(不正常情况),该字段返回 null。
---
## ⑦ 错误码
| 错误码 | HTTP 状态 | 场景 |
|--------|-----------|------|
| 400 | 400 | `isCharged` 传入非 0/1 的值(如 2、-1,Bean Validation 失败 |
| 401 | 401 | 未登录或 Token 失效 |
| 403 | 403 | 无管理员权限 |
| 404 | 404 | 景区 ID 不存在或已删除 |
---
## ⑧ 示例
### 8.1 典型成功 — 创建收费景区
**请求**
```
POST /admin/scenic/spot
Authorization: Bearer {admin_token}
Content-Type: application/json
{
"name": "呼伦贝尔大草原景区",
"province": "内蒙古自治区",
"city": "呼伦贝尔市",
"cityName": "呼伦贝尔",
"longitude": 119.758,
"latitude": 49.215,
"isCharged": 1
}
```
**响应**
```json
{
"code": 200,
"msg": "成功",
"data": {
"scenicId": "1900123456789000001",
"name": "呼伦贝尔大草原景区",
"isCharged": 1,
"isChargedLabel": "是",
"status": 0
}
}
```
### 8.2 边界情况 — 不传 isCharged使用默认值 0
**请求**
```
POST /admin/scenic/spot
Authorization: Bearer {admin_token}
Content-Type: application/json
{
"name": "某免费公园",
"province": "内蒙古自治区",
"city": "呼伦贝尔市",
"cityName": "呼伦贝尔",
"longitude": 119.700,
"latitude": 49.200
}
```
**响应**isCharged 默认为 0
```json
{
"code": 200,
"msg": "成功",
"data": {
"scenicId": "1900123456789000002",
"name": "某免费公园",
"isCharged": 0,
"isChargedLabel": "否",
"status": 0
}
}
```
### 8.3 业务失败 — isCharged 传入越界值
**请求**
```
POST /admin/scenic/spot
Authorization: Bearer {admin_token}
Content-Type: application/json
{
"name": "测试景区",
"isCharged": 2
}
```
**响应**400 参数校验失败)
```json
{
"code": 400,
"msg": "是否收费取值范围为0或1",
"data": null
}
```
---
## ⑨ 业务边界
**适用场景**
- 创建新景区时,可设置是否收费标记(不传默认不收费)
- 编辑已有景区时,可随时修改是否收费状态(传 null 或不传则保持原值不变)
- 列表和详情页均可展示该字段,用于运营人员快速判断景区性质
**不适用场景**
- 该字段仅用于标记属性,不影响产品定价逻辑(定价由费用项管理)
- 不用于权限控制,仅作展示属性
**特殊边界**
- 历史存量景区PR #3314 上线前创建的)`isCharged` 默认为 0`isChargedLabel` 返回 "否"
- PUT 编辑时若不传 `isCharged` 字段,该字段值保持不变(不会被清零)
- `isChargedLabel` 为 null 时属于字典配置异常,前端可用 `isCharged` 值自行映射1→"是",0→"否")兜底
---
## ⑩ 修改前后对比
### 字段级对比
**入参POST 创建 / PUT 编辑)**
| 字段名 | 变更前 | 变更后 |
|--------|--------|--------|
| isCharged | 不存在 | ✨ 新增,Integer,可选,默认 0Create,可 nullUpdate |
**出参(详情 + 列表)**
| 字段名 | 变更前 | 变更后 |
|--------|--------|--------|
| isCharged | 不存在 | ✨ 新增,Integer,1=是/0=否 |
| isChargedLabel | 不存在 | ✨ 新增,String,字典翻译文本,可能为 null |
### 行为级对比
| 行为 | 变更前 | 变更后 |
|------|--------|--------|
| 创建景区 | 无收费标记 | 可设置 isCharged,默认 0 |
| 列表展示 | 无收费信息 | 新增 isCharged + isChargedLabel |
| 详情展示 | 无收费信息 | 新增 isCharged + isChargedLabel |
---
## ⑪ 影响评估 / 回滚
**破坏兼容性**:无,新增字段均为可选,现有调用代码无需修改即可正常运行。
**前端同步上线**
- 创建/编辑表单:可选接入 `isCharged` 开关/选择器,不接入则默认 0
- 列表/详情页:可选展示 `isChargedLabel`,不展示也不影响功能
**回滚方案**
- 若需回滚,执行 `ALTER TABLE scenic_spot DROP COLUMN is_charged;` 并回退服务部署
- 前端零改动时回滚对前端无感
---
## ⑫ 注意事项
1. `isChargedLabel` 依赖字典 `sys_yes_no` 配置,后端已配置好,前端**不需要**维护字典表
2. 列表接口返回 `isChargedLabel` 纯粹为展示便利,前端也可用 `isCharged` 值自行映射1→"是",0→"否"
3. PUT 编辑时 `isCharged` 不传null表示不修改,区别于传 `0`(明确设置为否)
4. 历史数据库存量记录 `is_charged` 列已通过 DDL 添加默认值 0,无历史数据问题
---
## ⑬ 关联 / 联系人
- **Issue**[https://git.1814.love:8443/wx/HL/issues/3313](https://git.1814.love:8443/wx/HL/issues/3313)
- **PR**[https://git.1814.love:8443/wx/HL/pulls/3314](https://git.1814.love:8443/wx/HL/pulls/3314)
- **Commit**[https://git.1814.love:8443/wx/HL/commit/62bd9be8b9ac67bbb2493d0dcb6f2be41172684b](https://git.1814.love:8443/wx/HL/commit/62bd9be8b9ac67bbb2493d0dcb6f2be41172684b)
- **后端负责人**:腰苏图(企微 / yst@1814.love

查看文件

@ -1,291 +0,0 @@
# 餐厅管理新增「是否收费」字段
- **端类型**:管理后台
- **变更类型**:修改接口
- **日期**2026-06-01
- **Issue**[#3317](https://git.1814.love:8443/wx/HL/issues/3317)
- **PR**[#3318](https://git.1814.love:8443/wx/HL/pulls/3318)
- **Commit**[3ad5860](https://git.1814.love:8443/wx/HL/commit/3ad5860883819058a58ec384de0c5aac7c918804)
- **后端负责人**腰苏图yst@1814.love
---
## ① 接口背景
餐厅资源新增「是否收费」属性,用于在管理后台创建/编辑餐厅时标记该餐厅是否需要游客付费。该字段同步透出到列表和详情接口,便于运营人员在列表页一眼识别收费/免费餐厅。字典来源 `sys_yes_no`,后端已配置,前端无需额外配置。
---
## ② 变更清单
| 接口 | 方法 | 路径 | 变更内容 |
|------|------|------|----------|
| 创建餐厅 | POST | `/admin/restaurant/item` | 入参新增 `isCharged`Integer,默认 0 |
| 更新餐厅 | PUT | `/admin/restaurant/item/{restaurantId}` | 入参新增 `isCharged`Integer,可选 |
| 餐厅详情 | GET | `/admin/restaurant/item/{restaurantId}` | 出参新增 `isCharged` + `isChargedLabel` |
| 餐厅列表 | GET | `/admin/restaurant/items` | 出参新增 `isCharged` + `isChargedLabel` |
所有变更均为**向后兼容新增字段**,不涉及字段改名或删除。
---
## ③ 接口详情
| 项目 | 说明 |
|------|------|
| **认证方式** | 管理后台 JWT Token,请求头 `Authorization: Bearer {token}` |
| **幂等性** | POST 创建非幂等;PUT 更新幂等(相同 restaurantId 可重复调用) |
| **限流** | 无单独限流配置,走网关全局限流 |
| **权限** | 管理员角色,普通前端用户无访问权限 |
---
## ④ 接口入参
### 4.1 路径参数 / Query 参数
**PUT `/admin/restaurant/item/{restaurantId}`**
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| restaurantId | Long | 是 | 餐厅 ID路径参数 |
**GET `/admin/restaurant/items`**(列表,原有参数不变,无新增查询参数)
### 4.2 请求体字段(新增字段,含于已有请求体中)
**POST `/admin/restaurant/item`**`RestaurantCreateRequest`,新增字段)
| 字段名 | 类型 | 必填 | 校验规则 | 默认值 | 说明 |
|--------|------|------|----------|--------|------|
| isCharged | Integer | 否 | Min=0, Max=1 | 0 | 是否收费,1=是 0=否 |
**PUT `/admin/restaurant/item/{restaurantId}`**`RestaurantUpdateRequest`,新增字段)
| 字段名 | 类型 | 必填 | 校验规则 | 默认值 | 说明 |
|--------|------|------|----------|--------|------|
| isCharged | Integer | 否 | Min=0, Max=1 | null不传则不修改 | 是否收费,1=是 0=否 |
---
## ⑤ 出参字段
### GET `/admin/restaurant/item/{restaurantId}` 详情(`RestaurantVO`,新增字段)
| 字段名 | 类型 | 说明 | 可能为 null |
|--------|------|------|-------------|
| isCharged | Integer | 是否收费,1=是 0=否 | 否DB 默认 0 |
| isChargedLabel | String | 是否收费中文文本,取自字典 sys_yes_no"是"/"否" | 是(字典缺失时为 null |
### GET `/admin/restaurant/items` 列表(`RestaurantListVO`,新增字段)
| 字段名 | 类型 | 说明 | 可能为 null |
|--------|------|------|-------------|
| isCharged | Integer | 是否收费,1=是 0=否 | 否DB 默认 0 |
| isChargedLabel | String | 是否收费中文文本,取自字典 sys_yes_no"是"/"否" | 是(字典缺失时为 null |
---
## ⑥ 枚举 / 数据字典
### sys_yes_no 字典(后端已配置,前端无需维护)
| 字典值isCharged | 中文标签isChargedLabel | 说明 |
|--------------------|-----------------------------|------|
| 1 | 是 | 该餐厅收费 |
| 0 | 否 | 该餐厅免费 |
> `isChargedLabel` 由后端查字典自动翻译,前端可直接展示。若字典配置缺失(不正常情况),该字段返回 null,前端建议展示"-"或不显示。
---
## ⑦ 错误码
| 错误码 | HTTP 状态 | 场景 |
|--------|-----------|------|
| 400 | 400 | `isCharged` 传入非 0/1 的值(如 2、-1,Bean Validation 失败 |
| 401 | 401 | 未登录或 Token 失效 |
| 403 | 403 | 无管理员权限 |
| 404 | 404 | 餐厅 ID 不存在或已删除 |
---
## ⑧ 示例
### 8.1 典型成功 — 创建收费餐厅
**请求**
```
POST /admin/restaurant/item
Authorization: Bearer {admin_token}
Content-Type: application/json
{
"name": "云端藏餐",
"categoryCode": "TIBETAN",
"province": "西藏自治区",
"city": "拉萨市",
"cityName": "拉萨",
"longitude": 91.132,
"latitude": 29.660,
"isCharged": 1
}
```
**响应**
```json
{
"code": 200,
"msg": "成功",
"data": {
"restaurantId": "1927000000000001",
"name": "云端藏餐",
"isCharged": 1,
"isChargedLabel": "是",
"status": 0,
...
}
}
```
### 8.2 边界情况 — 不传 isCharged使用默认值 0
**请求**
```
POST /admin/restaurant/item
Authorization: Bearer {admin_token}
Content-Type: application/json
{
"name": "免费体验餐厅",
"categoryCode": "CHINESE",
"province": "西藏自治区",
"city": "拉萨市",
"cityName": "拉萨",
"longitude": 91.100,
"latitude": 29.650
// isCharged 字段不传
}
```
**响应**isCharged 默认为 0
```json
{
"code": 200,
"msg": "成功",
"data": {
"restaurantId": "1927000000000002",
"name": "免费体验餐厅",
"isCharged": 0,
"isChargedLabel": "否",
"status": 0,
...
}
}
```
### 8.3 业务失败 — isCharged 传入越界值
**请求**
```
POST /admin/restaurant/item
Authorization: Bearer {admin_token}
Content-Type: application/json
{
"name": "测试餐厅",
"categoryCode": "CHINESE",
"province": "西藏自治区",
"city": "拉萨市",
"cityName": "拉萨",
"longitude": 91.100,
"latitude": 29.650,
"isCharged": 2
}
```
**响应**400 参数校验失败)
```json
{
"code": 400,
"msg": "是否收费取值范围为0或1",
"data": null
}
```
---
## ⑨ 业务边界
**适用场景**
- 创建新餐厅时,可设置是否收费标记(不传默认不收费)
- 编辑已有餐厅时,可随时修改是否收费状态(传 null 或不传则保持原值不变)
- 列表和详情页均可展示该字段,用于运营人员快速判断餐厅性质
**不适用场景**
- 该字段仅用于标记属性,不影响产品定价逻辑(定价由费用项管理)
- 不用于权限控制,仅作展示属性
**特殊边界**
- 历史存量餐厅PR #3318 上线前创建的)`isCharged` 默认为 0`isChargedLabel` 返回 "否"
- PUT 更新时若不传 `isCharged` 字段,该字段值保持不变(不会被清零)
- `isChargedLabel` 为 null 时属于字典配置异常,业务上可展示 "-" 兜底
---
## ⑩ 修改前后对比
### 字段级对比
**入参POST 创建 / PUT 更新)**
| 字段名 | 变更前 | 变更后 |
|--------|--------|--------|
| isCharged | 不存在 | ✨ 新增,Integer,可选,默认 0Create,可 nullUpdate |
**出参(详情 + 列表)**
| 字段名 | 变更前 | 变更后 |
|--------|--------|--------|
| isCharged | 不存在 | ✨ 新增,Integer,1=是/0=否 |
| isChargedLabel | 不存在 | ✨ 新增,String,字典翻译文本,可能为 null |
### 行为级对比
| 行为 | 变更前 | 变更后 |
|------|--------|--------|
| 创建餐厅 | 无收费标记 | 可设置 isCharged,默认 0 |
| 列表展示 | 无收费信息 | 新增 isCharged + isChargedLabel |
| 详情展示 | 无收费信息 | 新增 isCharged + isChargedLabel |
---
## ⑪ 影响评估 / 回滚
**破坏兼容性**:无,新增字段均为可选,现有调用代码无需修改即可正常运行。
**前端同步上线**
- 创建/编辑表单:可选接入 `isCharged` 开关/选择器,不接入则默认 0
- 列表/详情页:可选展示 `isChargedLabel`,不展示也不影响功能
**回滚方案**
- 若需回滚,执行 `ALTER TABLE restaurant DROP COLUMN is_charged;` 并回退服务部署
- 前端零改动时回滚对前端无感
---
## ⑫ 注意事项
1. `isChargedLabel` 依赖字典 `sys_yes_no` 配置,后端已配置好,前端**不需要**维护字典表
2. 列表接口返回 `isChargedLabel` 纯粹为展示便利,前端也可用 `isCharged` 值自行映射1→"是",0→"否"
3. PUT 更新时 `isCharged` 不传null表示不修改,区别于传 `0`(明确设置为否)
4. 历史数据库存量记录 `is_charged` 列已通过 DDL 添加默认值 0,无历史数据问题
---
## ⑬ 关联 / 联系人
- **Issue**[https://git.1814.love:8443/wx/HL/issues/3317](https://git.1814.love:8443/wx/HL/issues/3317)
- **PR**[https://git.1814.love:8443/wx/HL/pulls/3318](https://git.1814.love:8443/wx/HL/pulls/3318)
- **Commit**[https://git.1814.love:8443/wx/HL/commit/3ad5860883819058a58ec384de0c5aac7c918804](https://git.1814.love:8443/wx/HL/commit/3ad5860883819058a58ec384de0c5aac7c918804)
- **后端负责人**:腰苏图(企微 / yst@1814.love

查看文件

@ -1,282 +0,0 @@
# 酒店管理新增「是否收费」字段
- **端类型**:管理后台
- **变更类型**:修改接口
- **日期**2026-06-01
- **Issue**[#3325](https://git.1814.love:8443/wx/HL/issues/3325)
- **PR**[#3326](https://git.1814.love:8443/wx/HL/pulls/3326)
- **Commit**[58afda7](https://git.1814.love:8443/wx/HL/commit/58afda7f72031b8055dd65805815176f88dfe08c)
- **后端负责人**腰苏图yst@1814.love
---
## ① 接口背景
酒店资源新增「是否收费」属性,用于在管理后台创建/编辑酒店时标记该酒店是否需要游客付费。该字段同步透出到列表和详情接口,便于运营人员在列表页一眼识别收费/免费酒店。字典来源 `sys_yes_no`,后端已配置,前端无需额外配置。
---
## ② 变更清单
| 接口 | 方法 | 路径 | 变更内容 |
|------|------|------|----------|
| 创建酒店 | POST | `/admin/hotel/item` | 入参新增 `isCharged`Integer,默认 0 |
| 更新酒店 | PUT | `/admin/hotel/item/{hotelId}` | 入参新增 `isCharged`Integer,可选 |
| 酒店详情 | GET | `/admin/hotel/item/{hotelId}` | 出参新增 `isCharged` + `isChargedLabel` |
| 酒店列表 | GET | `/admin/hotel/items` | 出参新增 `isCharged` + `isChargedLabel` |
所有变更均为**向后兼容新增字段**,不涉及字段改名或删除。
---
## ③ 接口详情
| 项目 | 说明 |
|------|------|
| **认证方式** | 管理后台 JWT Token,请求头 `Authorization: Bearer {token}` |
| **幂等性** | POST 创建非幂等;PUT 更新幂等(相同 hotelId 可重复调用) |
| **限流** | 无单独限流配置,走网关全局限流 |
| **权限** | 管理员角色,普通前端用户无访问权限 |
---
## ④ 接口入参
### 4.1 路径参数 / Query 参数
**PUT `/admin/hotel/item/{hotelId}`**
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| hotelId | Long | 是 | 酒店 ID路径参数 |
**GET `/admin/hotel/items`**(列表,原有参数不变,无新增查询参数)
### 4.2 请求体字段(新增字段,含于已有请求体中)
**POST `/admin/hotel/item`**`HotelCreateRequest`,新增字段)
| 字段名 | 类型 | 必填 | 校验规则 | 默认值 | 说明 |
|--------|------|------|----------|--------|------|
| isCharged | Integer | 否 | Min=0, Max=1 | 0 | 是否收费,1=是 0=否 |
**PUT `/admin/hotel/item/{hotelId}`**`HotelUpdateRequest`,新增字段)
| 字段名 | 类型 | 必填 | 校验规则 | 默认值 | 说明 |
|--------|------|------|----------|--------|------|
| isCharged | Integer | 否 | Min=0, Max=1 | null不传则不修改 | 是否收费,1=是 0=否 |
---
## ⑤ 出参字段
### GET `/admin/hotel/item/{hotelId}` 详情(`HotelVO`,新增字段)
| 字段名 | 类型 | 说明 | 可能为 null |
|--------|------|------|-------------|
| isCharged | Integer | 是否收费,1=是 0=否 | 否DB 默认 0 |
| isChargedLabel | String | 是否收费中文文本,取自字典 sys_yes_no"是"/"否" | 是(字典缺失时为 null |
### GET `/admin/hotel/items` 列表(`HotelListVO`,新增字段)
| 字段名 | 类型 | 说明 | 可能为 null |
|--------|------|------|-------------|
| isCharged | Integer | 是否收费,1=是 0=否 | 否DB 默认 0 |
| isChargedLabel | String | 是否收费中文文本,取自字典 sys_yes_no"是"/"否" | 是(字典缺失时为 null |
---
## ⑥ 枚举 / 数据字典
### sys_yes_no 字典(后端已配置,前端无需维护)
| 字典值isCharged | 中文标签isChargedLabel | 说明 |
|--------------------|-----------------------------|------|
| 1 | 是 | 该酒店收费 |
| 0 | 否 | 该酒店免费 |
> `isChargedLabel` 由后端查字典自动翻译,前端可直接展示。若字典配置缺失(不正常情况),该字段返回 null。
---
## ⑦ 错误码
| 错误码 | HTTP 状态 | 场景 |
|--------|-----------|------|
| 400 | 400 | `isCharged` 传入非 0/1 的值(如 2、-1,Bean Validation 失败 |
| 401 | 401 | 未登录或 Token 失效 |
| 403 | 403 | 无管理员权限 |
| 404 | 404 | 酒店 ID 不存在或已删除 |
---
## ⑧ 示例
### 8.1 典型成功 — 创建收费酒店
**请求**
```
POST /admin/hotel/item
Authorization: Bearer {admin_token}
Content-Type: application/json
{
"name": "丽江悦榕庄",
"hotelType": "HOTEL",
"address": "束河古镇悦榕路1号",
"longitude": 100.2134,
"latitude": 26.8721,
"isCharged": 1
}
```
**响应**
```json
{
"code": 200,
"msg": "成功",
"data": {
"hotelId": "1928000000000001",
"name": "丽江悦榕庄",
"isCharged": 1,
"isChargedLabel": "是",
"status": 0
}
}
```
### 8.2 边界情况 — 不传 isCharged使用默认值 0
**请求**
```
POST /admin/hotel/item
Authorization: Bearer {admin_token}
Content-Type: application/json
{
"name": "团队免费住宿点",
"hotelType": "HOSTEL",
"address": "丽江古城区某路1号",
"longitude": 100.2200,
"latitude": 26.8800
}
```
**响应**isCharged 默认为 0
```json
{
"code": 200,
"msg": "成功",
"data": {
"hotelId": "1928000000000002",
"name": "团队免费住宿点",
"isCharged": 0,
"isChargedLabel": "否",
"status": 0
}
}
```
### 8.3 业务失败 — isCharged 传入越界值
**请求**
```
POST /admin/hotel/item
Authorization: Bearer {admin_token}
Content-Type: application/json
{
"name": "测试酒店",
"hotelType": "HOTEL",
"address": "测试地址",
"longitude": 100.0,
"latitude": 26.0,
"isCharged": 2
}
```
**响应**400 参数校验失败)
```json
{
"code": 400,
"msg": "是否收费取值范围为0或1",
"data": null
}
```
---
## ⑨ 业务边界
**适用场景**
- 创建新酒店时,可设置是否收费标记(不传默认不收费)
- 编辑已有酒店时,可随时修改是否收费状态(传 null 或不传则保持原值不变)
- 列表和详情页均可展示该字段,用于运营人员快速判断酒店性质
**不适用场景**
- 该字段仅用于标记属性,不影响产品定价逻辑(定价由费用项管理)
- 不用于权限控制,仅作展示属性
**特殊边界**
- 历史存量酒店PR #3326 上线前创建的)`isCharged` 默认为 0`isChargedLabel` 返回 "否"
- PUT 更新时若不传 `isCharged` 字段,该字段值保持不变(不会被清零)
- `isChargedLabel` 为 null 时属于字典配置异常,非正常情况
---
## ⑩ 修改前后对比
### 字段级对比
**入参POST 创建 / PUT 更新)**
| 字段名 | 变更前 | 变更后 |
|--------|--------|--------|
| isCharged | 不存在 | ✨ 新增,Integer,可选,默认 0Create,可 nullUpdate |
**出参(详情 + 列表)**
| 字段名 | 变更前 | 变更后 |
|--------|--------|--------|
| isCharged | 不存在 | ✨ 新增,Integer,1=是/0=否 |
| isChargedLabel | 不存在 | ✨ 新增,String,字典翻译文本,可能为 null |
### 行为级对比
| 行为 | 变更前 | 变更后 |
|------|--------|--------|
| 创建酒店 | 无收费标记 | 可设置 isCharged,默认 0 |
| 列表展示 | 无收费信息 | 新增 isCharged + isChargedLabel |
| 详情展示 | 无收费信息 | 新增 isCharged + isChargedLabel |
---
## ⑪ 影响评估 / 回滚
**破坏兼容性**:无,新增字段均为可选,现有调用代码无需修改即可正常运行。
**前端同步上线**
- 创建/编辑表单:可选接入 `isCharged` 开关/选择器,不接入则默认 0
- 列表/详情页:可选展示 `isChargedLabel`,不展示也不影响功能
**回滚方案**
- 若需回滚,执行 `ALTER TABLE hotel DROP COLUMN is_charged;` 并回退服务部署
- 前端零改动时回滚对前端无感
---
## ⑫ 注意事项
1. `isChargedLabel` 依赖字典 `sys_yes_no` 配置,后端已配置好,前端**不需要**维护字典表
2. 列表接口返回 `isChargedLabel` 纯粹为展示便利,前端也可用 `isCharged` 值自行映射1→"是",0→"否"
3. PUT 更新时 `isCharged` 不传null表示不修改,区别于传 `0`(明确设置为否)
4. 历史数据库存量记录 `is_charged` 列已通过 DDL 添加默认值 0,无历史数据问题
---
## ⑬ 关联 / 联系人
- **Issue**[https://git.1814.love:8443/wx/HL/issues/3325](https://git.1814.love:8443/wx/HL/issues/3325)
- **PR**[https://git.1814.love:8443/wx/HL/pulls/3326](https://git.1814.love:8443/wx/HL/pulls/3326)
- **Commit**[https://git.1814.love:8443/wx/HL/commit/58afda7f72031b8055dd65805815176f88dfe08c](https://git.1814.love:8443/wx/HL/commit/58afda7f72031b8055dd65805815176f88dfe08c)
- **后端负责人**:腰苏图(企微 / yst@1814.love

查看文件

@ -1,321 +0,0 @@
# 游玩项目(活动)+ 备品(物资)新增「是否收费」字段
- **端类型**:管理后台
- **变更类型**:修改接口
- **日期**2026-06-01
- **Issue**[#3331](https://git.1814.love:8443/wx/HL/issues/3331)
- **PR**[#3334](https://git.1814.love:8443/wx/HL/pulls/3334)
- **Commit**[5da05a3](https://git.1814.love:8443/wx/HL/commit/5da05a324813cf8109d35c521c5e7bb9719627be)
- **后端负责人**腰苏图yst@1814.love
---
## ① 接口背景
游玩项目(活动)和备品(物资)两个资源域同步新增「是否收费」属性,用于在管理后台创建/编辑时标记该资源是否需要游客付费。该字段同步透出到各自的列表和详情接口,便于运营人员在列表页快速识别收费/免费资源。字典来源 `sys_yes_no`,后端已配置,前端无需额外配置。
---
## ② 变更清单
### 游玩项目ActivityController `/admin/activity`
| 接口 | 方法 | 路径 | 变更内容 |
|------|------|------|----------|
| 创建游玩项目 | POST | `/admin/activity/item` | 入参新增 `isCharged`Integer,默认 0 |
| 更新游玩项目 | PUT | `/admin/activity/item/{activityId}` | 入参新增 `isCharged`Integer,可选 |
| 游玩项目详情 | GET | `/admin/activity/item/{activityId}` | 出参新增 `isCharged` + `isChargedLabel` |
| 游玩项目列表 | GET | `/admin/activity/items` | 出参新增 `isCharged` + `isChargedLabel` |
### 备品SuppliesController `/admin/supplies`
| 接口 | 方法 | 路径 | 变更内容 |
|------|------|------|----------|
| 创建备品 | POST | `/admin/supplies/item` | 入参新增 `isCharged`Integer,默认 0 |
| 更新备品 | PUT | `/admin/supplies/item/{suppliesId}` | 入参新增 `isCharged`Integer,可选 |
| 备品详情 | GET | `/admin/supplies/item/{suppliesId}` | 出参新增 `isCharged` + `isChargedLabel` |
| 备品列表 | GET | `/admin/supplies/items` | 出参新增 `isCharged` + `isChargedLabel` |
所有变更均为**向后兼容新增字段**,不涉及字段改名或删除。
---
## ③ 接口详情
| 项目 | 说明 |
|------|------|
| **认证方式** | 管理后台 JWT Token,请求头 `Authorization: Bearer {token}` |
| **幂等性** | POST 创建非幂等;PUT 更新幂等(相同 activityId / suppliesId 可重复调用) |
| **限流** | 无单独限流配置,走网关全局限流 |
| **权限** | 管理员角色,普通前端用户无访问权限 |
---
## ④ 接口入参
### 4.1 路径参数 / Query 参数
**PUT `/admin/activity/item/{activityId}`**
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| activityId | Long | 是 | 游玩项目 ID路径参数 |
**PUT `/admin/supplies/item/{suppliesId}`**
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| suppliesId | Long | 是 | 备品 ID路径参数 |
**GET `/admin/activity/items`** 和 **GET `/admin/supplies/items`**(列表,原有 Query 参数不变,无新增查询参数)
### 4.2 请求体字段(新增字段)
**POST `/admin/activity/item`**`ActivityCreateRequest`,新增字段)
| 字段名 | 类型 | 必填 | 校验规则 | 默认值 | 说明 |
|--------|------|------|----------|--------|------|
| isCharged | Integer | 否 | Min=0, Max=1 | 0 | 是否收费,1=是 0=否 |
**PUT `/admin/activity/item/{activityId}`**`ActivityUpdateRequest`,新增字段)
| 字段名 | 类型 | 必填 | 校验规则 | 默认值 | 说明 |
|--------|------|------|----------|--------|------|
| isCharged | Integer | 否 | Min=0, Max=1 | null不传则不修改 | 是否收费,1=是 0=否 |
**POST `/admin/supplies/item`**`SuppliesCreateRequest`,新增字段)
| 字段名 | 类型 | 必填 | 校验规则 | 默认值 | 说明 |
|--------|------|------|----------|--------|------|
| isCharged | Integer | 否 | Min=0, Max=1 | 0 | 是否收费,1=是 0=否 |
**PUT `/admin/supplies/item/{suppliesId}`**`SuppliesUpdateRequest`,新增字段)
| 字段名 | 类型 | 必填 | 校验规则 | 默认值 | 说明 |
|--------|------|------|----------|--------|------|
| isCharged | Integer | 否 | Min=0, Max=1 | null不传则不修改 | 是否收费,1=是 0=否 |
---
## ⑤ 出参字段
### GET `/admin/activity/item/{activityId}` 详情(`ActivityVO`,新增字段)
| 字段名 | 类型 | 说明 | 可能为 null |
|--------|------|------|-------------|
| isCharged | Integer | 是否收费,1=是 0=否 | 否DB 默认 0 |
| isChargedLabel | String | 是否收费中文文本,取自字典 sys_yes_no"是"/"否" | 是(字典缺失时为 null |
### GET `/admin/activity/items` 列表(`ActivityListVO`,新增字段)
| 字段名 | 类型 | 说明 | 可能为 null |
|--------|------|------|-------------|
| isCharged | Integer | 是否收费,1=是 0=否 | 否DB 默认 0 |
| isChargedLabel | String | 是否收费中文文本,取自字典 sys_yes_no"是"/"否" | 是(字典缺失时为 null |
### GET `/admin/supplies/item/{suppliesId}` 详情(`SuppliesVO`,新增字段)
| 字段名 | 类型 | 说明 | 可能为 null |
|--------|------|------|-------------|
| isCharged | Integer | 是否收费,1=是 0=否 | 否DB 默认 0 |
| isChargedLabel | String | 是否收费中文文本,取自字典 sys_yes_no"是"/"否" | 是(字典缺失时为 null |
### GET `/admin/supplies/items` 列表(`SuppliesListVO`,新增字段)
| 字段名 | 类型 | 说明 | 可能为 null |
|--------|------|------|-------------|
| isCharged | Integer | 是否收费,1=是 0=否 | 否DB 默认 0 |
| isChargedLabel | String | 是否收费中文文本,取自字典 sys_yes_no"是"/"否" | 是(字典缺失时为 null |
---
## ⑥ 枚举 / 数据字典
### sys_yes_no 字典(后端已配置,前端无需维护)
| 字典值isCharged | 中文标签isChargedLabel | 说明 |
|--------------------|-----------------------------|------|
| 1 | 是 | 该资源收费 |
| 0 | 否 | 该资源免费 |
> `isChargedLabel` 由后端查字典自动翻译,前端可直接展示。若字典配置缺失(不正常情况),该字段返回 null。
---
## ⑦ 错误码
| 错误码 | HTTP 状态 | 场景 |
|--------|-----------|------|
| 400 | 400 | `isCharged` 传入非 0/1 的值(如 2、-1,Bean Validation 失败,msg="是否收费取值范围为0或1" |
| 401 | 401 | 未登录或 Token 失效 |
| 403 | 403 | 无管理员权限 |
| 404 | 404 | 资源 ID 不存在或已删除 |
---
## ⑧ 示例
### 8.1 典型成功 — 创建收费游玩项目
**请求**
```
POST /admin/activity/item
Authorization: Bearer {admin_token}
Content-Type: application/json
{
"name": "高原骑马体验",
"categoryCode": "OUTDOOR",
"isCharged": 1
}
```
**响应**
```json
{
"code": 200,
"msg": "成功",
"data": {
"activityId": "1929000000000001",
"name": "高原骑马体验",
"categoryCode": "OUTDOOR",
"isCharged": 1,
"isChargedLabel": "是",
"status": 0
}
}
```
### 8.2 边界情况 — 创建备品时不传 isCharged默认 0
**请求**
```
POST /admin/supplies/item
Authorization: Bearer {admin_token}
Content-Type: application/json
{
"name": "登山杖",
"categoryCode": "OUTDOOR_GEAR",
"billingType": "PER_ITEM",
"basePrice": 25.00
}
```
**响应**isCharged 默认为 0
```json
{
"code": 200,
"msg": "成功",
"data": {
"suppliesId": "1929000000000002",
"name": "登山杖",
"billingType": "PER_ITEM",
"isCharged": 0,
"isChargedLabel": "否",
"status": 0
}
}
```
### 8.3 业务失败 — isCharged 传入越界值
**请求**
```
PUT /admin/activity/item/1929000000000001
Authorization: Bearer {admin_token}
Content-Type: application/json
{
"name": "高原骑马体验",
"isCharged": 2
}
```
**响应**400 参数校验失败)
```json
{
"code": 400,
"msg": "是否收费取值范围为0或1",
"data": null
}
```
---
## ⑨ 业务边界
**适用场景**
- 创建/编辑游玩项目或备品时,可设置是否收费标记(不传默认不收费)
- 列表和详情页均展示该字段,供运营人员快速识别资源收费属性
**不适用场景**
- 该字段仅用于标记属性,不影响产品定价逻辑(定价由费用项管理)
- 不用于权限控制,仅作展示属性
**特殊边界**
- 历史存量游玩项目和备品PR #3334 上线前创建的)`isCharged` 默认为 0,`isChargedLabel` 返回 "否"
- PUT 更新时若不传 `isCharged` 字段null,该字段值保持不变不会被清零,区别于传 `0`(明确设置为否)
- `isChargedLabel` 为 null 时属于字典配置异常,非正常情况
---
## ⑩ 修改前后对比
### 字段级对比
**入参POST 创建 / PUT 更新)—— 游玩项目和备品相同**
| 字段名 | 变更前 | 变更后 |
|--------|--------|--------|
| isCharged | 不存在 | ✨ 新增,Integer,可选,默认 0Create,null 不修改Update |
**出参(详情 + 列表)—— 游玩项目和备品相同**
| 字段名 | 变更前 | 变更后 |
|--------|--------|--------|
| isCharged | 不存在 | ✨ 新增,Integer,1=是/0=否 |
| isChargedLabel | 不存在 | ✨ 新增,String,字典翻译文本,可能为 null |
### 行为级对比
| 行为 | 变更前 | 变更后 |
|------|--------|--------|
| 创建游玩项目 / 备品 | 无收费标记 | 可设置 isCharged,默认 0 |
| 列表展示 | 无收费信息 | 新增 isCharged + isChargedLabel |
| 详情展示 | 无收费信息 | 新增 isCharged + isChargedLabel |
---
## ⑪ 影响评估 / 回滚
**破坏兼容性**:无,新增字段均为可选,现有调用代码无需修改即可正常运行。
**前端同步上线**
- 创建/编辑表单:可选接入 `isCharged` 开关/选择器,不接入则默认提交 0不收费
- 列表/详情页:可选展示 `isChargedLabel`,不展示也不影响功能
**回滚方案**
- 若需回滚,分别执行以下 DDL 并回退服务部署:
- `ALTER TABLE activity DROP COLUMN is_charged;`
- `ALTER TABLE supplies_item DROP COLUMN is_charged;`
- 前端零改动时回滚对前端无感
---
## ⑫ 注意事项
1. `isChargedLabel` 依赖字典 `sys_yes_no` 配置,后端已配置好,前端**不需要**维护字典表
2. 列表接口返回 `isChargedLabel` 纯粹为展示便利,前端也可用 `isCharged` 值自行映射1→"是",0→"否"
3. PUT 更新时 `isCharged` 不传null表示不修改,区别于传 `0`(明确设置为否)
4. 游玩项目和备品两个域的字段命名、类型、字典完全一致,调用逻辑相同
5. 历史数据库存量记录 `is_charged` 列已通过 DDL 添加默认值 0,无历史数据问题
---
## ⑬ 关联 / 联系人
- **Issue**[https://git.1814.love:8443/wx/HL/issues/3331](https://git.1814.love:8443/wx/HL/issues/3331)
- **PR**[https://git.1814.love:8443/wx/HL/pulls/3334](https://git.1814.love:8443/wx/HL/pulls/3334)
- **Commit**[https://git.1814.love:8443/wx/HL/commit/5da05a324813cf8109d35c521c5e7bb9719627be](https://git.1814.love:8443/wx/HL/commit/5da05a324813cf8109d35c521c5e7bb9719627be)
- **后端负责人**:腰苏图(企微 / yst@1814.love

查看文件

@ -1,58 +0,0 @@
# 订单详情「服务标准」Tab 退费说明refundNotes修复——现可正确返回数据
- **端类型**:管理后台
- **变更类型**:缺陷修复(接口结构不变,仅恢复数据流)
- **日期**2026-06-02
- **Issue**[#3354](https://git.1814.love:8443/wx/HL/issues/3354)
- **PR**[#3355](https://git.1814.love:8443/wx/HL/pulls/3355)
- **Commit**[db61c2d](https://git.1814.love:8443/wx/HL/commit/db61c2da1193be0e0954dacdfd8f154e3ba5d912)
- **后端负责人**wx
---
## ① 背景
订单详情「服务标准」Tab 接口(`GET /v3/admin/order/{id}/service-standard`#3340 已上线的聚合结构)中,`refundNotes`(退费说明)字段此前一直返回空数组,即使绑定资源已配置退费说明。
根因是**资源退费说明根本存不进库**资源服务自定义的字段自动填充处理器只填一期命名字段createdAt/updatedAt,漏填二期命名字段createTime,导致退费说明保存接口报错create_time 不能为空),全表 0 条存活记录 → 下单冻快照时节点 `refundNote` 恒 null → 服务标准 Tab 退费说明永远为空。
本次修复后端自动填充处理器,退费说明可正常保存并随订单快照冻结。
---
## ② 变更清单
**接口结构无任何变化**`refundNotes` 字段在 #3340 已定义)。修复后行为:
| 接口 | 方法 | 路径 | 变更内容 |
|------|------|------|----------|
| 订单服务标准 | GET | `/v3/admin/order/{id}/service-standard` | `refundNotes` 现对「绑定了退费说明的 SCENIC/ACTIVITY 行程节点」正确返回数据(修复前恒为空) |
`refundNotes[]` 结构(不变,复述):
```
refundNotes: [
{
sourceName, // 来源点位名称,如「呼和诺尔草原旅游区」
intro, // 退费说明备注
items: [
{ title, amount, unitLabel, settleScope, settleScopeLabel, remark, effectiveFrom, effectiveTo }
]
}
]
```
测试服实测(订单 2061673442020651009`refundNotes` = 呼和诺尔草原旅游区 → [成人未参加 ¥44 /人(按人), 整团未到 ¥100 /团(按团)],`settleScopeLabel` 字典中文已正确解析。
---
## ③ 前端动作
**无需改动**。#3340 前端已对接 `refundNotes` 结构,仅需知悉:之前看到的退费说明为空是后端缺陷,现已修复,对已配置退费说明的资源会正常返回。
---
## ④ 备注
- 同一 Tab 的「出团注意事项」(`notices`)能否有数据,取决于**产品是否配置了「服务标准模板」**(产品域 Step5 补充信息配置项)。当前测试库暂无产品配置该模板,故 `notices` 仍为空;这属于数据配置,非接口缺陷,与本次修复无关。
- 「行程」(`itinerary`)一直正常。

查看文件

@ -1,182 +0,0 @@
# 二期 v3订单详情「服务标准」Tab 接口返回真实成品数据 + notices 新增 title 字段
> **服务**: hl-order-service-v3产品侧 hl-product-service-v2 配合改造,前端无感)
> **端**: 管理后台
> **接口**: `GET /v3/admin/order/{id}/service-standard`(订单详情 - 服务标准 Tab
> **类型**: ✏️ 修改接口(出参 `notices[]` 新增 `title` 字段 + 接口由恒空改为返回真实成品)
> **日期**: 2026-06-02
> **关联**: Issue #3363 / PR #3364(前序 #3340 建接口、#3353 改只读快照)
---
## 一句话结论
1. 该接口此前对**所有订单恒返 `data: null`**(产品侧没生产服务标准成品)。本次产品侧补上生产端,**新下单的订单**会返回完整服务标准成品(标题 / 简介 / 服务承诺 / 行程 / 退费说明)。
2. 出参 `notices[]`(服务承诺条目)**每条新增 `title` 字段**。
> ⚠️ 仅对**部署之后新创建**的订单生效。部署前的老订单仍返 `null`(其快照未冻入成品)。开发阶段老订单可忽略。
---
## 出参结构(完整,自包含)
`data` 类型 `ServiceStandardVO``data``null` 表示该订单无服务标准成品。
| 字段 | 类型 | 说明 | 可空 |
|---|---|---|---|
| `title` | string | 标题,固定为 `"出团服务标准·" + 产品名`;产品名为空时退化为 `"出团服务标准"` | 否 |
| `subtitle` | string | 副标题 | **本期恒 null** |
| `intro` | string | 服务标准简介 | 是(产品未配服务标准模板时为 null |
| `notices` | NoticeItem[] | 服务承诺条目(扁平,无分组) | 是(无源时为空数组 `[]` |
| `itinerary` | DayVO[] | 行程逐天列表 | 是(无行程时空数组) |
| `refundNotes` | RefundNoteGroup[] | 退费说明分组(按行程节点聚合) | 是(无退费说明时空数组) |
### NoticeItem服务承诺条目
| 字段 | 类型 | 说明 | 可空 |
|---|---|---|---|
| `title` | string | **本次新增**。条目主文案标题(如「专业司机」) | 是 |
| `content` | string | 正文(如「持有 A1 驾照,8 年以上驾龄」) | 是 |
| `remark` | string | 备注 / 灰色二级说明 | 是 |
| `color` | string | 文案颜色 `#RRGGBB` | 是 |
| `contactName` | string | 联系人 | 是 |
| `phone` | string | 手机号 | 是 |
### DayVO行程天
| 字段 | 类型 | 说明 | 可空 |
|---|---|---|---|
| `dayNumber` | integer | 天序号(从 1 起) | 否 |
| `dayTitle` | string | 天标题(如「第一天-接机」) | 是 |
| `remark` | string | 当天备注 | **本期恒 null** |
| `itineraryNode` | ItineraryNode[] | 当天点位列表 | 是 |
### ItineraryNode行程点位
| 字段 | 类型 | 说明 | 可空 |
|---|---|---|---|
| `nodeName` | string | 点位名称(如「呼和诺尔草原旅游区」「早餐」) | 否 |
| `description` | string | 点位描述 | 是 |
| `contactName` | string | 联系人 | **本期恒 null** |
| `phone` | string | 手机号 | **本期恒 null** |
### RefundNoteGroup退费说明分组
| 字段 | 类型 | 说明 | 可空 |
|---|---|---|---|
| `sourceName` | string | 来源点位名称(取行程节点名) | 否 |
| `intro` | string | 退费说明备注(如「苔藓为赠送项目,不退费」) | 是 |
| `items` | RefundItem[] | 退费明细列表 | 是 |
### RefundItem退费明细条目
| 字段 | 类型 | 说明 | 可空 |
|---|---|---|---|
| `title` | string | 展示标题(如「成人未参加」) | 是 |
| `amount` | number | 退费金额(赠送项目为 0 | 是 |
| `unitLabel` | string | 展示文案:`/人` `/团` `/辆` | 是 |
| `settleScope` | string | 结算粒度枚举(见下) | 是 |
| `settleScopeLabel` | string | 结算粒度中文(冻结即定格) | 是 |
| `remark` | string | 备注 | 是 |
| `effectiveFrom` | string(date) | 规则生效起日,`null` = 无限制 | 是 |
| `effectiveTo` | string(date) | 规则生效止日,`null` = 无限制 | 是 |
---
## 枚举 / 数据字典
### settleScope结算粒度
| 枚举值 | settleScopeLabel中文 | 配套 unitLabel |
|---|---|---|
| `PER_PERSON` | 按人 | /人 |
| `PER_TEAM` | 按团 | /团 |
| `PER_VEHICLE` | 按车 | /辆 |
> `settleScopeLabel` 在下单时随快照冻结,定格当时字典中文,资源后改不影响老订单。
---
## 请求示例
```
GET /v3/admin/order/2061762043987247105/service-standard
Authorization: Bearer {adminToken}
```
## 响应示例(真实,测试服 dev-v3 实测)
```json
{
"code": 200,
"message": "成功",
"success": true,
"data": {
"title": "出团服务标准·测试核心产品-单档-固定订金",
"subtitle": null,
"intro": "全程贴心服务保障",
"notices": [
{
"title": "专业司机",
"content": "持有A1驾照,8年以上驾龄",
"remark": "仅限指定时段",
"color": "#FF6600",
"contactName": "李师傅",
"phone": "13800000000"
}
],
"itinerary": [
{
"dayNumber": 1,
"dayTitle": "第一天-接机",
"remark": null,
"itineraryNode": [
{ "nodeName": "海拉尔接机", "description": null, "contactName": null, "phone": null },
{ "nodeName": "早餐", "description": "含(酒店)", "contactName": null, "phone": null }
]
}
],
"refundNotes": [
{
"sourceName": "呼和诺尔草原旅游区",
"intro": "苔藓为赠送项目,不退费",
"items": [
{
"title": "成人未参加", "amount": 44.0, "unitLabel": "/人",
"settleScope": "PER_PERSON", "settleScopeLabel": "按人",
"remark": "凭票退", "effectiveFrom": null, "effectiveTo": null
},
{
"title": "整团未到", "amount": 100.0, "unitLabel": "/团",
"settleScope": "PER_TEAM", "settleScopeLabel": "按团",
"remark": null, "effectiveFrom": null, "effectiveTo": null
}
]
}
]
}
}
```
## 响应示例(老订单 / 无成品)
```json
{ "code": 200, "message": "成功", "success": true, "data": null }
```
---
## 业务边界 / 注意事项
- `data: null` 是合法返回,表示该订单无服务标准成品(部署前老订单 / 产品未配服务标准)。
- `intro``notices` 来源于产品「服务标准模板」:产品未绑模板时,`intro``null``notices``[]`,但 `title` / `itinerary` / `refundNotes`(有各自数据源时)仍正常返回。
- `subtitle``DayVO.remark``ItineraryNode.contactName/phone` 本期固定为 `null`(预留字段)。
- 成品在下单时随产品快照整体冻结,资源 / 产品后续修改不影响已下单订单。
---
## 关联
- Issue: https://git.1814.love:8443/wx/HL/issues/3363
- PR: https://git.1814.love:8443/wx/HL/pulls/3364
- 前序: #3340(建接口)/ #3353(改只读快照)

查看文件

@ -1,155 +0,0 @@
# 二期 v3订单进度模型重构 —— 派生线性 6 步 + 嵌套 progressStepper
> **服务**: hl-order-service-v3 **端**: 管理后台
> **接口**: `GET /v3/admin/order`(列表)、`GET /v3/admin/order/{id}`(详情)、`POST /v3/admin/order`(创建)
> **Issue**: #3365 / #3368 **PR**: #3366(扁平)+ #3369(嵌套)
> **日期**: 2026-06-03
> **影响**: ⚠️ **破坏性**——删字段 + 改结构。订单进度展示统一为「派生线性 6 步 + 嵌套 progressStepper」。
---
## 一、背景
订单进度此前两套打架:列表用 8 步 `flowStep`,详情用 10 节点 `progressStepper`,同一订单两页进度刻度对不上。本次统一为**派生双层模型**(底层一份数据派生两视图),并修正了核单/结算判定 bug。
---
## 二、变更清单
| 变更 | 说明 |
|---|---|
| ❌ 删 `flowItems` | 列表不再返回 |
| ❌ 删 `progress`("X/8") | 列表不再返回 |
| 🔄 `flowStep` 语义改 | 8 步指针 → **线性 6 步**当前步序号 |
| 🔄 `flowStepTotal` | 8 → **6** |
| 🆕 `flowStepCode` | 当前步英文枚举(列表+详情) |
| 🆕 `flowStepStatus` | 当前步状态(列表+详情) |
| 🆕 `currentSubFlows`(列表) | 当前步=资源准备时的 4 子流程数组 |
| 🔄 `progressStepper`(详情) | 扁平 9 节点 → **嵌套 6 主节点**,加 `code`/`isCurrent`/`subFlows`,删 `parallel` |
> `orderStatus`/`orderStatusName`/`flowStatus`/`flowStatusName` **不变**
---
## 三、出参字段(进度部分)
### 列表 `GET /v3/admin/order``data.records[]`,当前步散字段)
| 字段 | 类型 | 说明 |
|---|---|---|
| `flowStep` | Integer | 当前第几步1-6;待支付=0;已取消=null |
| `flowStepTotal` | Integer | 总步数,固定 **6** |
| `flowStepCode` | String | 当前步英文枚举(见枚举表) |
| `flowDisplayText` | String | 当前步中文名 |
| `flowStepStatus` | String | 当前步状态:`DONE`/`PROCESSING`/`WAITING` |
| `currentSubFlows` | Array\<SubFlow\> | **仅当前步=资源准备时非空**,否则 `null` |
### 详情 `GET /v3/admin/order/{id}``data.main`
列表那 6 个散字段**全有**,外加完整 `progressStepper`
| 字段 | 类型 | 说明 |
|---|---|---|
| `progressStepper` | Array\<Node\> | 完整 6 主节点(嵌套) |
**Node 结构**`step`(Integer) / `code`(String) / `name`(String) / `status`(String) / `isCurrent`(Boolean) / `subFlows`(Array\<SubFlow\>,仅资源准备非空)
**SubFlow 结构**`code`(String) / `name`(String) / `status`(String) / `label`(String)
---
## 四、枚举 / 数据字典
### 主步 `code`6 步)
| code | name | 完成判定 |
|---|---|---|
| `PROFILE` | 补全信息 | 已支付 |
| `RESOURCE` | 资源准备 | 配房完成(降级,见业务边界) |
| `CONFIRM` | 确认 | 订单已确认 |
| `DEPART` | 出行 | 已出行 |
| `REVIEW` | 核单 | 已核单 |
| `SETTLE` | 结算 | 已结算 |
### 子流程 `code`资源准备下,4 条)
| code | name |
|---|---|
| `HOTEL` | 配房 |
| `VEHICLE` | 配车 |
| `GUIDE` | 领队 |
| `PHOTOGRAPHER` | 摄影 |
### `status`(主步 + 子流程通用)
| status | 含义 |
|---|---|
| `DONE` | 已完成 |
| `PROCESSING` | 处理中 |
| `WAITING` | 待开始 |
### `flowStep` 序号
`0`=待支付(步骤条未开始)|`1-6`=对应主步|`null`=已取消(前端不画进度条)
---
## 五、示例
### 列表(定制中、配房完成、配车进行中)
```jsonc
{
"orderStatus": "CUSTOMIZING", "orderStatusName": "定制中",
"flowStep": 2, "flowStepTotal": 6,
"flowStepCode": "RESOURCE", "flowDisplayText": "资源准备", "flowStepStatus": "PROCESSING",
"currentSubFlows": [
{"code":"HOTEL","name":"配房","status":"DONE","label":"已完成"},
{"code":"VEHICLE","name":"配车","status":"PROCESSING","label":"处理中"},
{"code":"GUIDE","name":"领队","status":"WAITING","label":"待开始"},
{"code":"PHOTOGRAPHER","name":"摄影","status":"WAITING","label":"待开始"}
]
}
```
### 详情 progressStepper同一订单
```jsonc
"progressStepper": [
{"step":1,"code":"PROFILE","name":"补全信息","status":"DONE","isCurrent":false,"subFlows":null},
{"step":2,"code":"RESOURCE","name":"资源准备","status":"PROCESSING","isCurrent":true,"subFlows":[
{"code":"HOTEL","name":"配房","status":"DONE","label":"已完成"},
{"code":"VEHICLE","name":"配车","status":"PROCESSING","label":"处理中"},
{"code":"GUIDE","name":"领队","status":"WAITING","label":"待开始"},
{"code":"PHOTOGRAPHER","name":"摄影","status":"WAITING","label":"待开始"}
]},
{"step":3,"code":"CONFIRM","name":"确认","status":"WAITING","isCurrent":false,"subFlows":null},
{"step":4,"code":"DEPART","name":"出行","status":"WAITING","isCurrent":false,"subFlows":null},
{"step":5,"code":"REVIEW","name":"核单","status":"WAITING","isCurrent":false,"subFlows":null},
{"step":6,"code":"SETTLE","name":"结算","status":"WAITING","isCurrent":false,"subFlows":null}
]
```
### 已取消订单
```jsonc
{ "flowStep": null, "flowStepCode": null, "flowDisplayText": "已取消",
"flowStepStatus": null, "currentSubFlows": null }
// 详情 progressStepper 返回空数组 []
```
---
## 六、业务边界 / 前端注意
1. **当前步两个口子都能拿**:列表用 `flowStep`;详情用 `isCurrent==true` 的节点(和 `flowStep` 指向同一步)。
2. **⚠️ 降级现状**:资源准备步现**只判配房**——配车/领队/摄影的子状态回写链路尚未接通,所以 `VEHICLE` 子流程恒 `PROCESSING``GUIDE`/`PHOTOGRAPHER``WAITING`,暂不会变 `DONE`。资源准备只要配房 `DONE` 即整步完成。回写接通后会纳入 4 线全 DONE。
3. **⚠️ 进度展示请用 `flowStep`/`flowDisplayText`,不要用 `flowStatusName`**——`flowStatus` 在"定制中"阶段恒为 `AWAITING_PROFILE`(待补全信息)不流转,用它展示进度会一直显示"待补全信息",不准。
4. `currentSubFlows`/`subFlows` 只在资源准备步有值,其余步为 `null`,前端渲染需判空。
---
## 七、关联
- **Issue**: [#3365](https://git.1814.love:8443/wx/HL/issues/3365)、[#3368](https://git.1814.love:8443/wx/HL/issues/3368)
- **PR**: [#3366](https://git.1814.love:8443/wx/HL/pulls/3366)(派生 6 步 + 修 reviewStatus bug、[#3369](https://git.1814.love:8443/wx/HL/pulls/3369)(嵌套 progressStepper
- **设计文档**: ORDER-STATE-MACHINE V1.1 §2.2、API-SPEC v5.56

查看文件

@ -1,124 +0,0 @@
# 【需求·管理后台】资源默认收费 + 收费=否禁价格日历 + 退费说明接入(景区 / 活动 / 酒店)
> PR: #3392 #3395(工单 #3389 服务: hl-resource-service | 更新时间: 2026-06-03
> 存放目录: changelogs-v2/2026-06/ 影响范围: 管理后台「资源管理 / 景区·游玩项目·酒店」编辑页
> 状态: 已合并 dev-v3 + 测试服部署双实例 + API 实测通过
## ⚠️ 关键说明
来自景区编辑页的 3 点反馈,后端已处理(部分是前端没接已实现的接口):
1. **默认收费**:景区 / 游玩项目 / 酒店新建时「是否收费」默认改为 **收费(1)**。前端开关初始态请置为「收费」开。
2. **收费=否 → 没有价格日历**:前端在「是否收费=否」时**隐藏价格日历区**;后端已加兜底——给收费=否的景区 / 活动设价会被拒(错误码 **390901**)。
3. **价格日历不做合并接口**:基本信息与价格日历仍是两个接口,前端保存时**分两次调**(先存基本信息,再批量设价)。
4. **退费说明**:后端早已实现(`/admin/refund-note`,覆盖景区+活动),**前端编辑页缺这块 UI**,本文补齐接口契约请接入。
服务资源 ServiceItem 的「是否收费」是 `isPaid`+真实定价语义(另有定价校验),**不在本次默认收费范围**,保持现状。
## 1. 默认收费(景区 / 活动 / 酒店)
新建资源不传 `isCharged` 时,后端默认 **1=收费**;前端显式传 `0` 仍尊重为「否」。
| 资源 | 字段 | 新建接口 | 默认值 |
|------|------|---------|--------|
| 景区 Scenic | `isCharged` | POST `/admin/scenic/spot` | 1=收费 |
| 游玩项目 Activity | `isCharged` | POST `/admin/activity/item` | 1=收费 |
| 酒店 Hotel | `isCharged` | POST `/admin/hotel/item` | 1=收费 |
前端动作:新建表单「是否收费」开关初始置「收费(开)」。编辑回显仍读后端返回的 `isCharged`
## 2. 收费=否 → 无价格日历
### 2.1 前端
「是否收费=否」时**隐藏价格日历区**(不展示、不允许设价)。
### 2.2 后端兜底(防脏数据)
对收费=否(`isCharged=0`)的景区 / 活动调批量设价接口,后端直接拒绝:
```bash
# 收费=否的景区设价 → 被拦截
curl -X PUT -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{"startDate":"2026-07-01","endDate":"2026-07-03","costPrice":100,"status":1}' \
"https://api.test.1814.love:9443/admin/scenic/spot/{scenicId}/prices"
```
响应(测试服实测):
```json
{ "code": 390901, "message": "资源未设为收费,不可设置价格日历" }
```
- 仅拦**批量设价** `PUT /spot/{id}/prices`;查询、改可售状态、清除价格不受影响。
- 酒店价格日历是 room-type 级,本期不做后端兜底,前端按 2.1 隐藏即可。
## 3. 价格日历保存方式(分两次调,不合并接口)
经确认**不新增合并接口**。前端编辑保存时分两次调用现有接口:
1. 先存基本信息:`PUT /admin/scenic/spot/{scenicId}`
2. 再批量设价:`PUT /admin/scenic/spot/{scenicId}/prices`(仅在收费=是时调)
## 4. 退费说明接入(景区 / 活动)
> ⚠️ **本节已废弃2026-06-03 更新)**:退费说明**已改为融合进资源编辑接口**(不再走独立 `/admin/refund-note`),并扩展支持**服务 SERVICE**。前端请按新 changelog `03_3406_退费说明融合进资源编辑接口-景区活动服务-管理后台.md` 接入,**勿按下方独立接口接**。下方内容仅作历史留存。
后端模块早已上线PR #3273 等),前端编辑页缺 UI,请接入。仅覆盖 **SCENIC / ACTIVITY** 两类。
### 4.1 接口表
| 操作 | 方法 / 路径 | 参数 |
|------|------------|------|
| 查询 | GET `/admin/refund-note` | query: `resourceType`(SCENIC/ACTIVITY) + `resourceId`。未配置返 `data:null`(不报错) |
| 保存(upsert) | PUT `/admin/refund-note` | body: RefundNoteSaveReqVO按 resourceType+resourceId upsert 整块) |
| 删除(软删) | DELETE `/admin/refund-note` | query: `resourceType` + `resourceId`(幂等,未配置也返 code:200 data:false |
### 4.2 保存请求体RefundNoteSaveReqVO
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| resourceType | String | 是 | SCENIC / ACTIVITY |
| resourceId | Long | 是 | 关联资源 ID |
| intro | String | 否 | 资源级备注≤255 字),如「苔藓为赠送项目,不退费」 |
| items | List | 是 | 退费明细,至少 1 条、最多 50 条 |
items[] 每项RefundNoteItemVO
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| title | String | 是 | 展示标题,如「成人未参加」 |
| amount | BigDecimal | 是 | 退费金额(赠送项目填 0 |
| unitLabel | String | 否 | 展示文案 /人 /团 /辆 |
| settleScope | String | 否 | 结算粒度 PER_PERSON / PER_TEAM / PER_VEHICLE |
| settleScopeLabel | String | — | 结算粒度中文名(出参后端拼,入参可省) |
| remark | String | 否 | 备注,如「仅限儿童」 |
| effectiveFrom | LocalDate | 否 | 规则生效起日null=无限制) |
| effectiveTo | LocalDate | 否 | 规则生效止日null=无限制) |
### 4.3 示例
```bash
# 查询景区退费说明
curl -H "Authorization: Bearer <token>" \
"https://api.test.1814.love:9443/admin/refund-note?resourceType=SCENIC&resourceId=3001000000000000019"
# 保存
curl -X PUT -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{
"resourceType": "SCENIC",
"resourceId": 3001000000000000019,
"intro": "苔藓为赠送项目,不退费",
"items": [
{"title":"成人未参加","amount":44.00,"unitLabel":"/人","settleScope":"PER_PERSON","remark":"仅限成人"}
]
}' \
"https://api.test.1814.love:9443/admin/refund-note"
```
查询响应RefundNoteRespVO`noteId / resourceType / resourceId / intro / items[] / createTime / updateTime``noteId` 字符串透传防精度丢失,`settleScopeLabel` 后端回填中文。
## 5. 前端动作清单
1. 景区 / 活动 / 酒店新建表单「是否收费」开关默认置「收费」。
2. 「是否收费=否」时隐藏价格日历区(编辑页 + 新建页)。
3. 保存仍分两次调(基本信息 + 价格日历),价格日历仅收费=是时调。
4. 景区 / 活动编辑页接入「退费说明」模块GET/PUT/DELETE `/admin/refund-note`)。

查看文件

@ -1,559 +0,0 @@
# 【新增接口·管理后台 + 小程序】图标库模块 11 接口(全模块首次推送 changelog
> **PR**: #3396(模块主体)+ #3398 / #3399mp 端点公开化)+ #3401(上线后审计加固)
> **服务**: hl-user-service | **更新时间**: 2026-06-03
>
> **存放目录**: `changelogs-v2/2026-06/`
> **影响范围**: 管理后台「图标库」管理页(分类 + 图标 CRUD + SVG 上传解析);小程序 / 前端 图标渲染(公开只读查询)
---
## ⚠️ 关键说明
图标库是后台统一维护的一套 SVG 图标,结构类似数据字典:一级「图标分类」(`icon_category`) + 二级「图标项」(`icon_item`),图标项归属某分类编码 `categoryCode`
**SVG 直接存库(不走 OSS**:图标内容是 SVG 字符串,直存数据库 `svg_content`MEDIUMTEXT,单图标后端限 256KB。前端拿到 `svgContent` 直接渲染。
**新增图标分两步**:先调 §3.5 `POST /admin/icon/parse-svg` 上传 `.svg` 文件,后端读成字符串并做安全校验后**只返回字符串、不落库**;前端拿到 `svgContent` 再连同 `color` / `size` 等填进保存表单调 §3.6 `POST /admin/icon/item` 保存。
**mp 查询公开无需 token**§3.10 / §3.11 是 UI 参考数据(同字典),无需登录即可访问,只返回 `status=ACTIVE` 的分类与图标,且分类被禁用时其图标也不再返回。
**SVG 安全校验**:上传 / 保存的 SVG 经服务端 XXE-safe 解析校验,拒绝含 `<script>` / `<foreignObject>` 元素、`on*` 事件属性、`javascript:` 外链的内容;前端渲染时仍建议做一次 sanitize 作纵深防御。
---
## 1. 接口背景
运营在后台「图标库」页统一维护 SVG 图标,供小程序 / 前端按分类拉取渲染(如分类入口图标、标签图标等)。
管理后台「图标库」页提供:
- 分类维护(列表 / 分页 / 新增 / 编辑 / 删除)
- 图标维护(分页 / 详情 / 新增 / 编辑 / 删除)
- 上传 SVG 文件解析为字符串
小程序 / 前端:按分类拉启用图标列表渲染(公开只读)。
---
## 2. 变更清单
### 管理后台(`/admin/icon/**`,需 admin JWT
| # | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|----------|------|
| 1 | POST | `/admin/icon/category` | 首推 | 保存分类id 空=新建,非空=修改) |
| 2 | GET | `/admin/icon/category/page` | 首推 | 分类分页status / keyword 过滤) |
| 3 | GET | `/admin/icon/category/list` | 首推 | 启用分类列表(下拉用) |
| 4 | DELETE | `/admin/icon/category/{id}` | 首推 | 删除分类(其下有图标项时拒删) |
| 5 | POST | `/admin/icon/parse-svg` | 首推 | 上传 SVG 文件解析为字符串(不落库) |
| 6 | POST | `/admin/icon/item` | 首推 | 保存图标id 空=新建,非空=修改) |
| 7 | GET | `/admin/icon/item/page` | 首推 | 图标分页categoryCode / keyword 过滤) |
| 8 | GET | `/admin/icon/item/{id}` | 首推 | 图标详情(含 svgContent |
| 9 | DELETE | `/admin/icon/item/{id}` | 首推 | 删除图标 |
### 小程序 / 前端(`/mp/icon/**`,公开无需 token
| # | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|----------|------|
| 10 | GET | `/mp/icon/categories` | 首推 | 启用分类列表 |
| 11 | GET | `/mp/icon/items` | 首推 | 某分类下启用图标列表 |
---
## 3. 接口详情
### 3.1 保存分类
**POST** `/admin/icon/category`
- **使用场景**:图标库分类新增 / 编辑
- **认证**:管理后台 JWT
- **幂等**:否(同 `categoryCode` 重复新建抛 210701
**Body 入参**`IconCategorySaveReqVO`
| 字段 | 类型 | 必填 | 校验 | 说明 |
|---|---|---|---|---|
| `id` | Long | ❌ | — | 空=新建,非空=修改 |
| `categoryCode` | string | ✅ | `@Size(max=64)` | 分类编码(全局唯一,创建后不可改) |
| `categoryName` | string | ✅ | `@Size(max=100)` | 分类名称 |
| `sort` | int | ❌ | — | 排序号(升序),默认 0 |
| `status` | string | ❌ | `@Pattern(ACTIVE\|DISABLED)` | 状态,默认 ACTIVE |
| `remark` | string | ❌ | `@Size(max=255)` | 备注 |
**出参**`Result<Long>``data` = 分类 ID
**典型示例 请求**
```
POST /admin/icon/category
Authorization: Bearer <token>
Content-Type: application/json
{ "categoryCode": "weather", "categoryName": "天气", "sort": 1 }
```
**典型示例 响应**
```json
{ "code": 200, "data": 2062087054098898945, "message": "成功" }
```
**异常 响应**(编码重复):
```json
{ "code": 210701, "message": "图标分类编码已存在", "data": null }
```
**错误码**400 参数校验 / **210701** 分类编码已存在 / **210702** 分类不存在(修改时 id 无效)/ 401 未登录
---
### 3.2 分类分页
**GET** `/admin/icon/category/page`
- **使用场景**:分类管理列表页
- **认证**:管理后台 JWT
- **幂等**:是(只读)
**Query 入参**`IconCategoryPageReqVO`
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
| `page` | int | ❌ | 1 | 页码 |
| `pageSize` | int | ❌ | 20 | 每页条数 |
| `status` | string | ❌ | — | 状态精确过滤ACTIVE / DISABLED |
| `keyword` | string | ❌ | — | 模糊匹配分类编码 / 名称 |
**出参**`Result<PageResult<IconCategoryRespVO>>`
| 字段 | 类型 | 说明 |
|---|---|---|
| `records[].id` | string | 分类 IDLong 序列化为 String |
| `records[].categoryCode` | string | 分类编码 |
| `records[].categoryName` | string | 分类名称 |
| `records[].sort` | int | 排序号 |
| `records[].status` | string | 状态ACTIVE / DISABLED |
| `records[].remark` | string | 备注 |
| `records[].createTime` | datetime | 创建时间 |
| `total` / `page` / `pageSize` | int | 分页元数据 |
**错误码**400 参数校验 / 401 未登录
---
### 3.3 启用分类列表
**GET** `/admin/icon/category/list`
- **使用场景**:图标新增页的分类下拉
- **认证**:管理后台 JWT
- **幂等**:是
**入参**:无
**出参**`Result<List<IconCategoryRespVO>>`(仅 `status=ACTIVE`,按 sort 升序)
**错误码**401 未登录
---
### 3.4 删除分类
**DELETE** `/admin/icon/category/{id}`
- **使用场景**:删除空分类
- **认证**:管理后台 JWT
- **幂等**:是
**路径入参**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | Long | ✅ | 分类 ID |
**出参**`Result<Void>``data: null`
**异常 响应**(分类下仍有图标项):
```json
{ "code": 210703, "message": "该分类下存在图标项,无法删除", "data": null }
```
**错误码****210702** 分类不存在 / **210703** 分类下存在图标项不可删 / 401 未登录
---
### 3.5 上传 SVG 解析(保存图标前置步骤)
**POST** `/admin/icon/parse-svg`
- **使用场景**:新增图标前上传 `.svg` 文件,拿到 SVG 字符串回填表单
- **认证**:管理后台 JWT
- **幂等**:是(仅解析,不落库)
**入参**multipart/form-data
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `file` | file | ✅ | SVG 文件(`.svg` |
**出参**`Result<IconSvgParseRespVO>`
| 字段 | 类型 | 说明 |
|---|---|---|
| `svgContent` | string | 解析出的 SVG 字符串(回填保存表单) |
| `fileName` | string | 原始文件名 |
| `size` | long | 文件字节数 |
**典型示例 请求**
```
POST /admin/icon/parse-svg
Authorization: Bearer <token>
Content-Type: multipart/form-data; boundary=...
form-data: file=@sunny.svg
```
**典型示例 响应**
```json
{
"code": 200,
"data": {
"svgContent": "<svg viewBox=\"0 0 24 24\"><path d=\"M0 0h24v24H0z\"/></svg>",
"fileName": "sunny.svg",
"size": 1234
},
"message": "成功"
}
```
**错误码****210753** 文件为空 / **210754** 非合法 SVG非 XML 或根元素不是 svg/ **210755** 超 256KB / **210756** 文件读取失败 / **210757** 含不安全内容(脚本 / 事件 / 外链)/ 401 未登录
---
### 3.6 保存图标
**POST** `/admin/icon/item`
- **使用场景**:图标新增 / 编辑
- **认证**:管理后台 JWT
- **幂等**:否(同分类 `iconCode` 重复新建抛 210751
**Body 入参**`IconItemSaveReqVO`
| 字段 | 类型 | 必填 | 校验 | 说明 |
|---|---|---|---|---|
| `id` | Long | ❌ | — | 空=新建,非空=修改 |
| `categoryCode` | string | ✅ | `@Size(max=64)` | 所属分类编码(新建校验分类存在;**修改时不可改** |
| `iconCode` | string | ✅ | `@Size(max=64)` | 图标编码(同分类内唯一;**修改时不可改** |
| `name` | string | ✅ | `@Size(max=100)` | 图标名称 |
| `svgContent` | string | ✅ | `@Size(max=262144)` | SVG 字符串(来自 §3.5 解析结果,经安全校验) |
| `color` | string | ❌ | `@Size(max=16)` | 默认色值 hex,如 `#FFCC00` |
| `size` | int | ❌ | — | 默认尺寸(像素) |
| `keywords` | string | ❌ | `@Size(max=255)` | 搜索关键词(逗号分隔) |
| `sort` | int | ❌ | — | 排序号,默认 0 |
| `status` | string | ❌ | `@Pattern(ACTIVE\|DISABLED)` | 状态,默认 ACTIVE |
| `remark` | string | ❌ | `@Size(max=255)` | 备注 |
**出参**`Result<Long>``data` = 图标 ID
**典型示例 请求**
```
POST /admin/icon/item
Authorization: Bearer <token>
Content-Type: application/json
{
"categoryCode": "weather",
"iconCode": "sunny",
"name": "晴天",
"svgContent": "<svg viewBox=\"0 0 24 24\"><path d=\"M0 0h24v24H0z\"/></svg>",
"color": "#FFCC00",
"size": 24,
"keywords": "晴,太阳,sunny"
}
```
**典型示例 响应**
```json
{ "code": 200, "data": 2062087056506441730, "message": "成功" }
```
**异常 响应**SVG 含脚本 / 事件 / 外链):
```json
{ "code": 210757, "message": "SVG 含不安全内容(脚本/事件/外链),请清理后重试", "data": null }
```
**错误码**400 参数校验 / **210702** 分类不存在 / **210751** 同分类图标编码已存在 / **210752** 图标不存在(修改时 id 无效)/ **210754** SVG 非法 / **210755** SVG 超 256KB / **210757** SVG 含不安全内容 / 401 未登录
> **SaveReqVO 全量快照语义**:修改时请先用 §3.8 详情拉取再整体回填提交,未传的可空字段会被覆盖为空。`categoryCode` / `iconCode` 创建后不可变,修改时被忽略。
---
### 3.7 图标分页
**GET** `/admin/icon/item/page`
- **使用场景**:图标管理列表页
- **认证**:管理后台 JWT
- **幂等**:是
**Query 入参**`IconItemPageReqVO`
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
| `page` | int | ❌ | 1 | 页码 |
| `pageSize` | int | ❌ | 20 | 每页条数 |
| `categoryCode` | string | ❌ | — | 所属分类精确过滤 |
| `keyword` | string | ❌ | — | 模糊匹配图标编码 / 名称 / 关键词 |
**出参**`Result<PageResult<IconItemRespVO>>``records[]` 为完整图标 VO字段见 §3.8
**错误码**400 参数校验 / 401 未登录
---
### 3.8 图标详情
**GET** `/admin/icon/item/{id}`
- **使用场景**:详情查看 / 编辑前预填
- **认证**:管理后台 JWT
- **幂等**:是
**路径入参**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | Long | ✅ | 图标 ID |
**出参**`Result<IconItemRespVO>`
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | string | 图标 ID |
| `categoryCode` | string | 所属分类编码 |
| `iconCode` | string | 图标编码 |
| `name` | string | 图标名称 |
| `svgContent` | string | SVG 字符串 |
| `color` | string | 默认色值 hex |
| `size` | int | 默认尺寸(像素) |
| `keywords` | string | 搜索关键词 |
| `sort` | int | 排序号 |
| `status` | string | 状态ACTIVE / DISABLED |
| `remark` | string | 备注 |
| `createTime` / `updateTime` | datetime | 时间 |
**典型示例 响应**
```json
{
"code": 200,
"data": {
"id": "2062087056506441730",
"categoryCode": "weather",
"iconCode": "sunny",
"name": "晴天",
"svgContent": "<svg viewBox=\"0 0 24 24\"><path d=\"M0 0h24v24H0z\"/></svg>",
"color": "#FFCC00",
"size": 24,
"keywords": "晴,太阳,sunny",
"sort": 0,
"status": "ACTIVE",
"remark": null,
"createTime": "2026-06-03 16:00:00",
"updateTime": "2026-06-03 16:00:00"
},
"message": "成功"
}
```
**错误码****210752** 图标不存在 / 401 未登录
---
### 3.9 删除图标
**DELETE** `/admin/icon/item/{id}`
- **使用场景**:删除图标
- **认证**:管理后台 JWT
- **幂等**:是
**路径入参**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | Long | ✅ | 图标 ID |
**出参**`Result<Void>``data: null`
**错误码****210752** 图标不存在 / 401 未登录
---
### 3.10【公开】启用分类列表
**GET** `/mp/icon/categories`
- **使用场景**:小程序 / 前端拉图标分类
- **认证****公开,无需 token**
- **幂等**:是
**入参**:无
**出参**`Result<List<IconCategorySimpleRespVO>>`(仅 `status=ACTIVE`,按 sort 升序)
| 字段 | 类型 | 说明 |
|---|---|---|
| `categoryCode` | string | 分类编码 |
| `categoryName` | string | 分类名称 |
| `sort` | int | 排序号 |
**典型示例 请求**
```
GET /mp/icon/categories
(无需 Authorization
```
**典型示例 响应**
```json
{
"code": 200,
"data": [
{ "categoryCode": "weather", "categoryName": "天气", "sort": 1 }
],
"message": "成功"
}
```
---
### 3.11【公开】某分类下启用图标列表
**GET** `/mp/icon/items`
- **使用场景**:小程序 / 前端按分类渲染图标
- **认证****公开,无需 token**
- **幂等**:是
**Query 入参**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `categoryCode` | string | ✅ | 分类编码 |
**出参**`Result<List<IconItemSimpleRespVO>>`(仅 `status=ACTIVE` 的图标,且分类须为 ACTIVE
| 字段 | 类型 | 说明 |
|---|---|---|
| `iconCode` | string | 图标编码 |
| `name` | string | 图标名称 |
| `svgContent` | string | SVG 字符串(直接渲染) |
| `color` | string | 默认色值 hex |
| `size` | int | 默认尺寸(像素) |
**典型示例 请求**
```
GET /mp/icon/items?categoryCode=weather
(无需 Authorization
```
**典型示例 响应**
```json
{
"code": 200,
"data": [
{
"iconCode": "sunny",
"name": "晴天",
"svgContent": "<svg viewBox=\"0 0 24 24\"><path d=\"M0 0h24v24H0z\"/></svg>",
"color": "#FFCC00",
"size": 24
}
],
"message": "成功"
}
```
**错误码**400 `categoryCode` 缺失
---
## 6. 枚举 / 数据字典
### 6.1 状态(`status`
**所属字段**:分类 / 图标的 `status` | **类型**`String` | **必填**:❌(默认 ACTIVE
**`@Pattern` 强校验枚举**
| 值 | 含义 |
|---|---|
| `ACTIVE` | 启用 |
| `DISABLED` | 禁用 |
> mp 端只返回 ACTIVE 的分类与图标;分类 DISABLED 时其图标在 mp 端也不返回。
---
## 7. 错误码(全模块汇总)
| code | 含义 | 触发条件 | 涉及接口 |
|---|---|---|---|
| 400 | 参数校验失败 | 字段缺失 / 长度 / 枚举值不合法 | 所有写入 + §3.11 |
| 401 | 未登录 | admin JWT 无效或缺失 | 全部 admin 接口mp 端公开不需要) |
| **210701** | 分类编码已存在 | `categoryCode` 唯一约束 | §3.1 |
| **210702** | 分类不存在 | 分类 id / categoryCode 无效 | §3.1 §3.4 §3.6 |
| **210703** | 分类下存在图标项,不可删 | 删分类时其下仍有图标 | §3.4 |
| **210751** | 同分类图标编码已存在 | `(categoryCode, iconCode)` 唯一约束 | §3.6 |
| **210752** | 图标不存在 | 图标 id 无效 | §3.6 §3.8 §3.9 |
| **210753** | 上传 SVG 文件为空 | 文件为空 | §3.5 |
| **210754** | SVG 内容不合法 | 非合法 XML 或根元素不是 `<svg>` | §3.5 §3.6 |
| **210755** | SVG 内容过大 | 超过 256KB | §3.5 §3.6 |
| **210756** | SVG 文件读取失败 | 文件流读取异常 | §3.5 |
| **210757** | SVG 含不安全内容 | 含 `<script>` / `<foreignObject>` / `on*` 事件 / `javascript:` 外链 | §3.5 §3.6 |
> 错误码段位 210700-210799 归属图标库模块。
---
## 9. 业务边界
- ✅ **两级结构**:分类(`icon_category`+ 图标(`icon_item`),图标按 `categoryCode` 归属分类
- ✅ **SVG 直存库**`svg_content` MEDIUMTEXT 直存 SVG 字符串,不走 OSS;单图标后端限 256KB
- ✅ **上传解析与保存分离**§3.5 只解析返回字符串不落库;§3.6 才落库
- ✅ **唯一约束**`categoryCode` 全局唯一;`(categoryCode, iconCode)` 联合唯一
- ✅ **编码不可变**:图标的 `categoryCode` / `iconCode` 创建后不可改(修改时忽略)
- ✅ **SVG 服务端安全校验**XXE-safe 解析 + 拒绝脚本 / 事件 / 外链
- ✅ **mp 公开只读**§3.10 §3.11 无需 token,只返 ACTIVE;分类禁用其图标也不返
---
## 11. 影响评估
- **是否破坏向后兼容**:否(全新模块,无存量接口改动)
- **前端是否必须同步上线**:是(新功能,前端按本 changelog 对接管理页 + 渲染端)
- **影响已有数据**:无(新建 `icon_category` / `icon_item` 两表)
---
## 12. 注意事项
- **分页参数名**`page` / `pageSize`(不是 pageNo
- **Long 主键序列化**`id` 等 Long 字段 JSON 返回为 String,前端不要当 Number 解析
- **新增图标流程**:先 §3.5 上传解析拿 `svgContent` → 填表单(补 color / size / 分类 / 编码)→ §3.6 保存
- **渲染 SVG**:拿到 `svgContent` 直接内联渲染,**务必先做一次前端 sanitize**(后端已拒明显恶意内容,前端作纵深防御)
- **mp 端公开**§3.10 / §3.11 无需 token;先拉分类再用 `categoryCode` 拉图标
- **状态语义**`ACTIVE` 启用 / `DISABLED` 禁用,mp 端只见 ACTIVE
---
## 13. 关联 / 联系人
### 13.1 链接
- **同模块 PR**#3396(主体)/ #3398 / #3399mp 公开化)/ #3401(审计加固)
- **关联工单**#3390
### 13.2 联系人
- **后端负责人**: @wx
- **前端对接(管理后台 + 小程序)**: 待指派

查看文件

@ -1,175 +0,0 @@
# 【修改接口·管理后台】退费说明融合进资源编辑接口(景区 / 活动 / 服务)
> **PR**: #3407(工单 #3406
> **服务**: hl-resource-service | **更新时间**: 2026-06-03
>
> **存放目录**: `changelogs-v2/2026-06/`
> **影响范围**: 管理后台「资源管理 / 景区·游玩项目·服务」编辑页(基本信息)
> **状态**: 已合并 dev-v3 + 测试服部署双实例 + API 实测三类通过
---
## ⚠️ 关键说明
退费说明**不再走独立的** `/admin/refund-note` 接口,已**融合进资源编辑接口**——前端在景区/活动/服务编辑页里直接编辑退费说明,随资源一起保存、随详情一起回显,**一个接口搞定**。
- **覆盖三类资源**:景区 SCENIC、游玩项目 ACTIVITY、**服务 SERVICE本次新增支持**。
- **详情** `GET` 响应新增 `refundNote` 字段(未配置为 `null`)。
- **保存** `POST`(新建) / `PUT`(编辑) 请求体新增 `refundNote` 字段,三态语义见 §3.2。
- **独立接口 `/admin/refund-note` 仍保留**(向后兼容 + 订单快照内部仍用),但前端**改用融合接口**,不要再调独立接口。
> 替代说明:本文取代 `03_3389_...` changelog 中「退费说明接入(独立接口)」一节。前端**按本文融合接口接入**,勿按旧文的 `/admin/refund-note` 独立接口接。
---
## 1. 背景
退费说明resource_refund_note此前是独立 CRUD 模块(`/admin/refund-note`),前端要单独调一套接口维护。运营反馈应在资源编辑页里直接配,故融合进资源的详情/保存接口。同时把适用资源从「景区+活动」扩展到「景区+活动+**服务**」。
---
## 2. 变更清单
| # | 资源 | 详情(回显 refundNote | 保存(含 refundNote |
|---|------|------|------|
| 1 | 景区 Scenic | GET `/admin/scenic/spot/{scenicId}` | POST `/admin/scenic/spot`、PUT `/admin/scenic/spot/{scenicId}` |
| 2 | 游玩项目 Activity | GET `/admin/activity/item/{activityId}` | POST `/admin/activity/item`、PUT `/admin/activity/item/{activityId}` |
| 3 | 服务 Service**新增** | GET `/admin/service/item/{serviceId}` | POST `/admin/service/item`、PUT `/admin/service/item/{serviceId}` |
> 列表接口(`/spots``/items`**不返回** refundNote避免 N+1,退费说明只在单条详情里有。
---
## 3. 接口详情
### 3.1 详情回显
三类资源的详情响应(`ScenicSpotVO` / `ActivityVO` / `ServiceItemVO`)新增字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `refundNote` | object \| null | 退费说明;该资源未配置时为 `null` |
| `refundNote.noteId` | string | 退费说明 ID雪花,String 透传防精度丢失) |
| `refundNote.intro` | string | 资源级备注 |
| `refundNote.items[]` | array | 退费明细(见 §6.2 |
| `refundNote.createTime` / `updateTime` | datetime | — |
### 3.2 保存(三态语义)
三类资源的新建/编辑请求体新增 `refundNote`(类型 `RefundNoteEmbedReqVO`,见 §6.1)。后端按 `refundNote` 是否传、items 是否为空分三态处理:
| 入参 refundNote | 行为 |
|---|---|
| **不传**(字段缺省 / null | 保持该资源现有退费说明**不动** |
| 传了,`items` **为空数组或不传** | **删除**该资源退费说明(清空语义) |
| 传了,`items` **非空** | **整块 upsert**(连 intro 一起覆盖) |
> 编辑接口是字段级更新:只传 `refundNote` 即可单独维护退费说明,不影响其它字段。
### 3.3 示例(以景区为例,活动/服务同理换路径)
**保存退费说明**PUT 只传 refundNote,其它字段不动
```bash
curl -X PUT -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{
"refundNote": {
"intro": "苔藓为赠送项目,不退费",
"items": [
{ "title": "成人未参加", "amount": 50.00, "settleScope": "PER_PERSON", "unitLabel": "/人", "remark": "仅限成人" }
]
}
}' \
"https://api.test.1814.love:9443/admin/scenic/spot/3001000000000000019"
```
**详情回显**GET,data 节选):
```json
{
"code": 200,
"data": {
"scenicId": "3001000000000000019",
"name": "中俄边境公路(卡线)",
"refundNote": {
"noteId": "20596378108368322580",
"intro": "苔藓为赠送项目,不退费",
"items": [
{ "title": "成人未参加", "amount": 50.00, "settleScope": "PER_PERSON", "settleScopeLabel": "按人", "unitLabel": "/人", "remark": "仅限成人" }
],
"createTime": "2026-06-03 17:30:00",
"updateTime": "2026-06-03 17:30:00"
}
}
}
```
**清空退费说明**PUT 传空 items
```bash
curl -X PUT -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{ "refundNote": { "items": [] } }' \
"https://api.test.1814.love:9443/admin/scenic/spot/3001000000000000019"
# 之后 GET,data.refundNote 为 null
```
---
## 6. 数据结构
### 6.1 `refundNote` 请求体(`RefundNoteEmbedReqVO`
前端只传 intro + items,`resourceType` / `resourceId` 由后端从当前资源上下文回填,前端无需传。
| 字段 | 类型 | 必填 | 校验 | 说明 |
|---|---|---|---|---|
| `intro` | string | ❌ | `@Size(max=255)` | 资源级备注 |
| `items` | array | ❌ | `@Size(max=50)` | 退费明细;空/不传=清空 |
### 6.2 退费明细项(`items[]` / `RefundNoteItemVO`
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `title` | string | ✅ | 展示标题,如「成人未参加」 |
| `amount` | BigDecimal | ✅ | 退费金额(赠送项目填 0,不可为负 |
| `unitLabel` | string | ❌ | 展示文案 /人 /团 /辆 |
| `settleScope` | string | ❌ | 结算粒度 PER_PERSON / PER_TEAM / PER_VEHICLE |
| `settleScopeLabel` | string | — | 结算粒度中文(出参后端拼字典 order_refund_settle_scope,入参可省 |
| `remark` | string | ❌ | 备注 |
| `effectiveFrom` / `effectiveTo` | date | ❌ | 规则生效起止null=无限制;同传时 from≤to |
---
## 9. 业务边界
- ✅ **三态语义统一**:不传=不动 / 空 items=删除 / 非空=upsert三类资源一致
- ✅ **同事务**:退费说明与资源主表在同一事务保存,主表失败则退费说明回滚。
- ✅ **列表不返回 refundNote**:仅单条详情回显,避免列表 N+1。
- ✅ **settleScopeLabel 后端拼**:出参按字典 `order_refund_settle_scope` 回填中文,字典异常时 fallback 为 null,不影响主流程。
- ✅ **独立接口保留**`/admin/refund-note`GET/PUT/DELETE仍可用,订单快照内部仍取退费说明;前端改用融合接口即可。
- ✅ **items 上限 50**:与独立端点对齐。
---
## 11. 影响评估
- **是否破坏向后兼容**:否(详情新增字段、保存新增可选字段;独立接口未删)。
- **前端是否必须同步**:是(要在编辑页接入退费说明子表单,改用融合接口)。
- **影响已有数据**:无(存量退费说明照常,独立接口与融合接口读写同一张表)。
---
## 12. 注意事项
- **改用融合接口**:前端不要再调 `/admin/refund-note` 独立接口(虽保留但即将不维护)。
- **清空靠空 items**:要删退费说明就传 `refundNote:{items:[]}`,不是不传(不传=不动)。
- **Long 主键 String**`refundNote.noteId` 为 String,勿当 Number 解析。
- **服务也支持了**服务资源ServiceItem本次起可配退费说明,与景区/活动一致。
---
## 13. 关联
- 取代:`03_3389_资源默认收费+收费否禁价格日历+退费说明接入-管理后台.md` 的「退费说明(独立接口)」一节。
- 后端负责人:@wx
- 前端对接管理后台mmg

某些文件未显示,因为此 diff 中更改的文件太多 显示更多