hl-api-changelog/changelogs-v2/2026-05/19_2598_v3行程day-node写操作真实化-修改接口-管理后台.md

24 KiB

【修改接口·管理后台】v3 行程 day/node 写操作真实化

更新时间: 2026-05-19 端类型: 管理后台 关联: HL Issue #2598 / PR #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 对象结构(字段均可选):

{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. 出参字段

所有接口统一包装:

{"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 图集
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 图集
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 响应体(分页)

{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新增天

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":"初次添加行程天"}'

响应:

{
  "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新增景点节点

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}'

响应:

{
  "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 节点(带实配载荷)

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"}}'

响应(节点 + 活动实配同事务写入):

{
  "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

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":"骑马体验"}'

响应:

{"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 仍为 MockPOST /v3/admin/order/{orderId}/itinerary/save 整体保存接口本期未真实化,仍返回 Mock 数据。

  4. staff/scenic/meal 实配 CRUD 仍 MocknodeType=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. 关联 / 联系人