文件
hl-api-changelog/changelogs-v2/2026-09/23_8170_订单协作域三只读端点补归属守卫-修改接口-管理后台.md
T
2026-09-23 20:58:36 +08:00

32 KiB
原始文件 Blame 文件历史

schema, ticket, title, consumer, author, change_type, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
schema ticket title consumer author change_type backend_status gateway_status frontend_status frontend_owner frontend_ref target_release verified_at status_note updated_at base
hl-changelog/v2 8170 订单协作域三只读端点补订单归属守卫(新增返回 581008/581045);行程价格字段与团期批量打回契约补充说明 admin wx(GIT) 修改接口 deployed verified not_required PR #8290 已合并 dev-v3(841ea7b06)。部署:hl-order-service-v3 dev-v3 @ 841ea7b06,2026-09-23 20:07:37 部署,deploy-status STATE=ok,BEHIND 0。真实网关实测(夹具订单 2100542584106496001,归属定制师为他人):CUSTOMIZER(非归属)在 tags / tag-picker / itinerary-document 三个端点均返回 581008;SUPER_ADMIN(对照)三个端点均正常返回 200;itinerary-document 不带 documentType 时先因参数校验返回业务码 400,发生在归属守卫之前。GET /{id}/itinerary 与批量打回 resourceType 两处仅补充字段说明文案,接口行为、请求/响应结构均未变。;前端判 not_required:GET /tags 与 itinerary-document 前端零调用;tag-picker 唯一调用点 TagPickerModal catch 透 err.message 不吞(581008/581045 走通用 toast);价格字段/批量打回两文档补充零行为变化 2026-09-23 dev-v3

order-v3: 协作域三只读端点补订单归属守卫 + 行程价格字段/团期批量打回契约说明

存放目录: 二期(v3) → changelogs-v2/2026-09/

服务: hl-order-service-v3 PR: #8290 Issue: #8170 日期: 2026-09-23 影响范围: 管理后台订单详情「标签」区、打标签弹窗、生成电子行程单(对客)、订单详情「行程安排」Tab、团期「查看需求」批量打回


⚠️ 关键变化(非必须,本版与上版行为不同 / 纠错 / 撤销时必写)

  • 本次变化:GET /v3/admin/order/{orderId}/tags、GET /v3/admin/order/{orderId}/tag-picker、GET /v3/admin/order/{orderId}/itinerary-document 三个端点新增订单归属校验。
  • 前端以前以为的:只要拿到 token、打开订单详情页,就能对任意 orderId 调这三个接口拿到数据——改前这三个端点方法体零权限判断,任意后台角色都能读到他人订单的标签、打标签弹窗数据、完整电子行程单(含出行人、酒店、大交通)。
  • 实际新行为:非归属定制师、非 ADMIN/SUPER_ADMIN/VEHICLE_MANAGER、且不是在读团期子订单的团期管理员,会收到 code=581008;房务管理员/房务组长一律先被拒绝为 code=581045。请求体、响应体结构不变,只是多了这两种失败响应。
  • 同一次提交里顺带把 GET /{id}/itinerary 的四个价格字段(协议价快照、结算价快照、计划日单价、房型行协议价)与批量打回 resourceType 字段的既有契约写进了字段说明(不是行为变化,是文档补充):这四个价格字段是供应商采购价,不是对客报价;批量打回 resourceType=ALL 的车侧范围只覆盖游览车(TRAVEL),不含接送机(TRANSFER)。

一、背景(选填)

#8170 AC-18 复核发现协作域三个只读端点(标签列表、打标签弹窗、电子行程单)方法体零归属校验——Controller 与 Service 都没有调用任何 OrderViewGuard,只要能拿到 JWT(任意后台角色)就能传任意 orderId 读到他人订单的标签与完整行程文档。本次在 CollabService 三个方法体内补 OrderViewGuard.assertOrderReadable。

同一份工单里还有两条纯文档补充(无代码行为变化):AC-2 把「团期管理员为什么能在行程 Tab 看到供应商成本价」这条 2026-09-22 已拍板的口径写进 ItineraryVO 字段注释;AC-11 把「批量打回 ALL 不含接送机」这条既有行为写进 RejectRequirementReqVO 与 GroupBatchRequirementService#doReject 的契约说明。


二、变更接口清单

# 接口 方法 路径 变更类型 说明
1 查订单已挂标签 GET /v3/admin/order/{orderId}/tags 新增归属校验 新增 OrderViewGuard.assertOrderReadable;非放行角色返 581008,房务/组长返 581045
2 打标签弹窗数据 GET /v3/admin/order/{orderId}/tag-picker 新增归属校验 同上;守卫排在 OperatorHolder 取当前操作人之前
3 生成电子行程单(对客) GET /v3/admin/order/{orderId}/itinerary-document 新增归属校验 同上;documentType 缺失时先于守卫返回业务码 400
4 行程安排 Tab GET /v3/admin/order/{id}/itinerary 字段说明变更 四个价格字段补注「供应商采购价,非对客报价」;接口行为、字段结构不变
5 按户打回需求 POST /v3/admin/order/group-batch/{groupBatchId}/requirement/reject 字段说明变更 resourceType 契约补充:ALL/VEHICLE 车侧仅覆盖 TRAVEL;接口行为不变

三、接口详情

1. 查订单已挂标签 GET /v3/admin/order/{orderId}/tags

VO: 无请求体 → OrderTagListRespVO

使用场景

订单详情页展示已挂在该订单上的标签(扁平列表,不分类型)。

入参

字段 位置 类型 必填 约束 说明
Authorization Header String ✅ - 管理端登录令牌
orderId Path Long ✅ 订单须存在(581430);当前登录角色须对该订单具备归属读权(581008/581045,本次新增) 订单 ID

出参 Result<OrderTagListRespVO>

字段 类型 说明
attached List 当前订单已挂标签列表(扁平,不分类型)
attached[].id String 标签主键(Long,超 JS 安全整数范围时序列化为字符串)
attached[].orderId String 关联订单 ID
attached[].tagName String 标签名
attached[].tagColor String 标签色值,如 #FF6B6B
attached[].creator String 创建人姓名(定制师 or SYSTEM)
attached[].createdAt String 创建时间,格式 yyyy-MM-dd HH:mm:ss

请求示例

GET /v3/admin/order/2100542584106496001/tags
Authorization: Bearer {token}

响应示例

{
  "code": 200,
  "message": "成功",
  "data": {
    "attached": [
      {
        "id": "2101000000000000001",
        "orderId": "2100542584106496001",
        "tagName": "VIP",
        "tagColor": "#FF6B6B",
        "creator": "张定制",
        "createdAt": "2026-09-20 10:15:00"
      }
    ]
  },
  "traceId": null,
  "success": true
}

空数据 / 降级响应

无标签时 attached 为空数组,不是 null:

{ "code": 200, "message": "成功", "data": { "attached": [] }, "traceId": null, "success": true }

错误响应

本次新增(TEST 实测,夹具订单 2100542584106496001,归属定制师为他人;CUSTOMIZER 非归属角色实测返回):

{ "code": 581008, "message": "无权查看此订单", "data": null, "traceId": null, "success": false }

房务管理员/房务组长(ROOM_MANAGER/house_keeper_lead)先于归属判定被拒绝:

{ "code": 581045, "message": "房务角色无权查看订单详情,房务仅可配房", "data": null, "traceId": null, "success": false }

订单不存在(既有行为,未变):

{ "code": 581430, "message": "订单不存在", "data": null, "traceId": null, "success": false }

业务边界

  • 放行角色(TEST 实测 SUPER_ADMIN 正常返回):ADMIN/SUPER_ADMIN 恒放行;VEHICLE_MANAGER 恒放行;GROUP_BATCH_MANAGER 仅当目标订单挂在团期下(order.groupBatchId != null)才放行,散客单仍 581008;其余角色(定制师/客服/运营/财务等)必须是该订单的归属定制师(adminId == order.consultantId),否则 581008。
  • 房务管理员/房务组长无论订单归属如何,一律先于其他判定被拒绝为 581045(OrderViewGuard.assertReadable,hl-order-service-v3/src/main/java/com/hulalv/order/core/guard/OrderViewGuard.java:279-281)。
  • 判定顺序:先查订单是否存在(581430)→ 再判角色归属(581008/581045,CollabService.java:172,174);非请求上下文(MQ 回放/定时任务/内部 Feign/单测)无角色视为系统态放行。

2. 打标签弹窗数据 GET /v3/admin/order/{orderId}/tag-picker

VO: 无请求体 → List<TagPickerItemVO>

使用场景

订单详情页点「打标签」按钮弹出的选择面板:标签库全集 + 当前订单已挂标记 + 游离标签(已挂但不在库里的)。

入参

字段 位置 类型 必填 约束 说明
Authorization Header String ✅ - 管理端登录令牌
orderId Path Long ✅ 订单须存在(581401);当前登录角色须对该订单具备归属读权(581008/581045,本次新增) 订单 ID

出参 Result<List<TagPickerItemVO>>

字段 类型 说明
(根) List 弹窗选择项列表;先按标签库排序(isPinned DESC, sortOrder ASC, lastUsedAt DESC),游离标签追加末尾
[].tagName String 标签名
[].tagColor String HEX 色值,如 #5B8FF9
[].tagScope String SYSTEM=系统标签 / PERSONAL=个人标签;游离标签为 null
[].isPinned Boolean 是否置顶(游离标签为 false)
[].sortOrder Integer 排序值(游离标签为 null)
[].selected Boolean 是否已挂在当前订单
[].inLibrary Boolean 是否来自标签库(false=游离标签,仅存在于 order_tag 中)

请求示例

GET /v3/admin/order/2100542584106496001/tag-picker
Authorization: Bearer {token}

响应示例

{
  "code": 200,
  "message": "成功",
  "data": [
    { "tagName": "VIP", "tagColor": "#5B8FF9", "tagScope": "SYSTEM", "isPinned": true, "sortOrder": 1, "selected": true, "inLibrary": true },
    { "tagName": "临时标记", "tagColor": "#FF9900", "tagScope": null, "isPinned": false, "sortOrder": null, "selected": true, "inLibrary": false }
  ],
  "traceId": null,
  "success": true
}

空数据 / 降级响应

标签库为空且订单无游离标签时返回空数组:

{ "code": 200, "message": "成功", "data": [], "traceId": null, "success": true }

错误响应

本次新增(TEST 实测,CUSTOMIZER 非归属角色实测返回):

{ "code": 581008, "message": "无权查看此订单", "data": null, "traceId": null, "success": false }
{ "code": 581045, "message": "房务角色无权查看订单详情,房务仅可配房", "data": null, "traceId": null, "success": false }

订单不存在(既有行为,未变):

{ "code": 581401, "message": "订单不存在", "data": null, "traceId": null, "success": false }

业务边界

  • 放行/拒绝口径与「查订单已挂标签」完全一致(同一个 OrderViewGuard.assertOrderReadable,见该接口业务边界第 1 条的角色表)。
  • 归属守卫排在 OperatorHolder.get() 取当前操作人之前(CollabService.java:250 早于 :252):OperatorHolder 判空只在「拿不到操作人」时拦截,拿得到操作人的越权请求它不管,不能替代归属校验。

3. 生成电子行程单(对客) GET /v3/admin/order/{orderId}/itinerary-document

VO: 无请求体 → OrderItineraryDocumentVO

使用场景

生成对客视角的电子行程单/打印版/核价单(CUSTOMER/CUSTOMER_PRINT/CUSTOMER_QUOTE 三类 documentType 共用本接口,前端按值渲染不同模板)。签单(供应商视角 sign-voucher)、司机出团单(print-itinerary)走另外两个独立接口,不在本次变更范围内。

入参

字段 位置 类型 必填 约束 说明
Authorization Header String ✅ - 管理端登录令牌
orderId Path Long ✅ 订单须存在(581401);当前登录角色须对该订单具备归属读权(581008/581045,本次新增) 订单 ID
documentType Query String ✅ CUSTOMER / CUSTOMER_PRINT / CUSTOMER_QUOTE;缺失返回业务码 400 文档类型(对客视角)
includeResourceDetail Query Boolean ❌ 默认 true;CUSTOMER 场景可传 false 跳过 Feign 是否调 Feign 拉资源详情,false 时节点 scenicDetail 等字段为 null

出参 Result<OrderItineraryDocumentVO>

字段 类型 说明
documentType String 回显请求的文档类型
header Object 抬头信息(订单号等;CUSTOMER_QUOTE 才含 totalAmount)
travelers List 出行人列表
days List 按天行程(含节点、场景/餐厅详情)
hotels List 酒店段列表
feeNotes List 费用说明
supplies List 随行物资
transports List 大交通
summary Object 汇总信息
resourceDetailHealth Object 资源详情 Feign 健康度报告(降级不阻断)
generatedAt String 生成时间,格式 yyyy-MM-dd HH:mm:ss

(完整字段树见既有 Swagger OrderItineraryDocumentVO;本次变更不涉及该 VO 任何字段,此表只列顶层结构定位用)

请求示例

GET /v3/admin/order/2100542584106496001/itinerary-document?documentType=CUSTOMER&includeResourceDetail=true
Authorization: Bearer {token}

响应示例

{
  "code": 200,
  "message": "成功",
  "data": {
    "documentType": "CUSTOMER",
    "header": { "orderNo": "HL2026092012345" },
    "travelers": [],
    "days": [],
    "hotels": [],
    "feeNotes": [],
    "supplies": [],
    "transports": [],
    "summary": {},
    "resourceDetailHealth": { "feignOk": true, "feignError": null },
    "generatedAt": "2026-09-23 20:10:00"
  },
  "traceId": null,
  "success": true
}

空数据 / 降级响应

includeResourceDetail=false 或 Feign 调用失败时,各节点的资源详情字段(如 scenicDetail)为 null,resourceDetailHealth.feignOk=false 且 feignError 非空,接口仍返回 200,不阻断整份文档:

{ "code": 200, "message": "成功", "data": { "resourceDetailHealth": { "feignOk": false, "feignError": "resource-service 调用超时" } }, "traceId": null, "success": true }

错误响应

本次新增(TEST 实测,CUSTOMIZER 非归属角色实测返回):

{ "code": 581008, "message": "无权查看此订单", "data": null, "traceId": null, "success": false }
{ "code": 581045, "message": "房务角色无权查看订单详情,房务仅可配房", "data": null, "traceId": null, "success": false }

documentType 缺失(既有行为,未变;TEST 实测确认发生在归属守卫之前,源码为通用 Spring 参数绑定异常处理 GlobalExceptionHandler#handleMissingParam):

{ "code": 400, "message": "缺少必要参数: documentType", "data": null, "traceId": null, "success": false }

订单不存在(既有行为,未变):

{ "code": 581401, "message": "订单不存在", "data": null, "traceId": null, "success": false }

业务边界

  • 放行/拒绝口径与前两个接口完全一致。
  • documentType 缺失的 400 判定在归属守卫之前(Spring 参数绑定先于方法体执行),所以「非归属角色 + 不传 documentType」拿到的是 400,不是 581008;这不是本次改动,是既有行为,TEST 已实测确认。
  • 归属守卫(CollabService.java:417)与下游第 7 步 OrderDetailService#getServiceStandard → requireOrderById 里的同一道守卫会被连续调用两次,是有意保留、非冗余:本处这道是本端点自己的,下游那道挂在一个可选步骤上,不能替代(改动下游步骤的人不会打开本文件)。

4. 订单详情 - 行程安排 Tab GET /v3/admin/order/{id}/itinerary

VO: 无请求体 → ItineraryVO

使用场景

订单详情页「行程安排」Tab:配房/配车的需求与实配对照、未失活配车历史等。本次只补充四个价格字段的说明文字,接口行为、归属校验、响应结构均未变——该端点的归属守卫是既有能力,不是本次新增。

入参

字段 位置 类型 必填 约束 说明
Authorization Header String ✅ - 管理端登录令牌
id Path Long ✅ 订单须存在;当前登录角色须对该订单具备归属读权(既有能力,未变) 订单 ID

出参 Result<ItineraryVO>(仅列本次文档变更涉及的字段,完整契约不在本次变更范围内)

字段 类型 说明
hotelGroup.assignments[].protoPrice String 协议价快照(元/间·晚,house protoPrice)。本次补充说明:这是供应商采购价,不是对客报价
hotelGroup.assignments[].settlementPrice String 结算价快照(元/间·晚,house settlementPrice)。本次补充说明:同样是供应商采购价,不是对客报价
vehicleGroup.assignments[].plannedDailyFee String 计划日单价(元/车·天,付给车队的采购价)。本次补充说明:非对客报价
hotelGroup.requirement.days[].segments[].candidates[].rooms[].protocolPrice String 本房型行协议价(元/间·晚,可为 null)。本次补充说明:这是供应商采购价,不是对客报价

请求示例

GET /v3/admin/order/2100542584106496001/itinerary
Authorization: Bearer {token}

响应示例

{
  "code": 200,
  "message": "成功",
  "data": {
    "hotelGroup": {
      "assignments": [
        { "assignmentId": "700001", "hotelName": "云台山大酒店", "roomType": "大床房", "roomCount": 2, "protoPrice": "588.00", "settlementPrice": "688.00" }
      ]
    },
    "vehicleGroup": {
      "assignments": [
        { "plannedDailyFee": "800.00", "driverName": "王师傅" }
      ]
    }
  },
  "traceId": null,
  "success": true
}

空数据 / 降级响应

未配房/未配车时 hotelGroup.assignments/vehicleGroup.assignments 为空数组,各价格字段随之不出现:

{ "code": 200, "message": "成功", "data": { "hotelGroup": { "assignments": [] }, "vehicleGroup": { "assignments": [] } }, "traceId": null, "success": true }

错误响应

既有行为,未变:

{ "code": 581008, "message": "无权查看此订单", "data": null, "traceId": null, "success": false }

业务边界

  • 该端点走 OrderController#getItinerary → OrderDetailService.assembleItinerary → requireOrderById,仍是 OrderViewGuard.assertOrderReadable(OrderDetailService.java:794-803),不是本次新增,本次改动只补了字段说明。
  • 四个价格字段对团期管理员(GROUP_BATCH_MANAGER,仅限该订单挂在团期下)刻意不脱敏:这是 2026-09-22 管理者拍板的已知设计,理由是团期管理员核对「团期需求」与实配是否对得上,价格本身就是核对基准;同一条定案记在 OrderViewGuard#assertOrderFinanceReadable 的 javadoc(OrderViewGuard.java:164-168),本端点未被收窄到 assertOrderFinanceReadable。
  • 四个字段均为供应商采购成本,不是对客报价;本次只是把这条口径写进了字段注释,不是新发现的行为变化。

5. 按户打回需求 POST /v3/admin/order/group-batch/{groupBatchId}/requirement/reject

VO: RejectRequirementReqVO → GroupBatchRequirementRejectRespVO

使用场景

团期「查看需求」页批量打回:运营勾选若干户,把已提交需求退回定制师重填。本次只补充 resourceType 字段的契约说明,请求/响应结构、错误码均未变。

入参

字段 位置 类型 必填 约束 说明
Authorization Header String ✅ - 管理端登录令牌
groupBatchId Path Long ✅ 团期须存在且处于可打回阶段(589501) 团期主订单 ID
orderIds Body List ✅ 1~200 户,服务端去重 被打回的子订单 ID 列表
reason Body String ✅ ≤500 字 打回原因
resourceType Body String ❌ HOTEL / VEHICLE / ALL,默认 ALL;本次补充说明:VEHICLE/ALL 车侧仅覆盖游览车(TRAVEL),不含接送机(TRANSFER) 打回的资源类型

出参 Result<GroupBatchRequirementRejectRespVO>

字段 类型 说明
groupBatchId String 团期聚合主键
requirementConfirmed Boolean 团期需求整体确认标记(本端点成功后恒 false)
rejected List 本次被打回的需求条目(每户每资源类型一条)
rejected[].orderId String 子订单 ID
rejected[].resourceType String 资源类型:HOTEL / VEHICLE
rejected[].requirementId String 被打回的需求行 ID
rejected[].sourceStatus String 打回前的需求状态:PENDING_REVIEW / PENDING

请求示例

{
  "orderIds": [60123456789001, 60123456789002],
  "reason": "房间需求与套餐不匹配,请重新填写",
  "resourceType": "ALL"
}

响应示例

{
  "code": 200,
  "message": "成功",
  "data": {
    "groupBatchId": "1867000000001",
    "requirementConfirmed": false,
    "rejected": [
      { "orderId": "60123456789001", "resourceType": "HOTEL", "requirementId": "90011223344", "sourceStatus": "PENDING_REVIEW" }
    ]
  },
  "traceId": null,
  "success": true
}

空数据 / 降级响应

写接口没有空数据场景;所有勾选户都不可打回时整单拒绝(见错误响应 589534),不会返回 rejected: [] 的成功响应。

错误响应

本次澄清的既有行为(resourceType=ALL 且勾选户只有接送机需求):

{ "code": 589534, "message": "子订单 {0} 不属于本团期、已退出或无可打回的需求", "data": null, "traceId": null, "success": false }

其余既有错误码未变:

{ "code": 589501, "message": "团期状态不允许当前操作", "data": null, "traceId": null, "success": false }
{ "code": 589535, "message": "子订单 {0} 已分房,请先由房务调整配房后再打回", "data": null, "traceId": null, "success": false }

业务边界

  • 本次澄清、代码逻辑未变:resourceType=ALL 不等于「全部资源」——车侧只走游览车(TRAVEL),接送机(TRANSFER)需求不在本入口可见范围内。一户只有接送机需求时,不是「打回了但没生效」,是该户在候选筛选阶段就没被选中,因「无可打回的需求」落 589534。
  • 接送机需求要打回,走单户接口 POST /v3/admin/order/{id}/vehicle-requirement/reject?kind=TRANSFER,本次未变。
  • 任一户不可打回则整单拒绝、所有户零副作用(含合法户在内);不存在部分成功。

四、契约约束与正确调用方式(接口类必写)

本节只写后端接受/拒绝 payload 的规则,不写 UI 渲染建议。

✅ 正确 / ❌ 错误 payload 对照

场景 请求 结果
✅ SUPER_ADMIN 读任意订单的标签/弹窗数据/行程单 上述三接口任一 200
✅ 定制师读自己归属的订单 上述三接口任一,orderId=自己 consultantId 名下订单 200
✅ 团期管理员读团期子订单(order.groupBatchId != null) 上述三接口任一 200
❌ 定制师/客服/运营/财务读他人归属的订单 上述三接口任一,orderId=非本人订单 581008(本次新增)
❌ 团期管理员读散客单(order.groupBatchId == null) 上述三接口任一 581008(本次新增)
❌ 房务管理员/房务组长读任意订单的标签/弹窗数据/行程单 上述三接口任一 581045(本次新增,前置于归属判定)
❌ itinerary-document 不传 documentType 任意角色 400「缺少必要参数: documentType」(既有行为,发生在归属守卫之前)
❌ 批量打回 resourceType=ALL、勾选户只有接送机需求 POST .../requirement/reject 589534(既有行为,本次补充契约说明)
  • 三个协作域端点(tags/tag-picker/itinerary-document)的归属判定口径完全一致,判定顺序恒为:订单存在性 → 房务前置拒绝(581045)→ ADMIN/SUPER_ADMIN/VEHICLE_MANAGER 放行 → 团期管理员限团期子订单放行 → 其余角色须为归属定制师,否则 581008(OrderViewGuard.java:267-298)。
  • GET /{id}/itinerary 的归属守卫本次未变(既有能力);仅字段说明变化。
  • 批量打回 resourceType 字段行为本次未变;仅补充契约说明,防止把 ALL 误读为「全部资源类型」。

五、数据库行为(涉及写操作时必写)

  • 无 Flyway migration、无 DDL、无表结构变更。
  • 三个协作域端点新增的归属守卫是纯内存态角色/字段判定,复用 Service 层判空后已持有的 OrderInfo 实体,零额外 DB 查询。
  • 批量打回 doReject 的写库范围与事务边界本次未变:仍在团级 Lock4j 锁 + @Transactional(rollbackFor=Exception.class) 内逐户跑 rejectRequirementByGroupAdmin + groupBatchService.rejectRequirement;任一户校验失败即整单回滚、零写入。

六、边界行为

  • 非请求上下文(MQ 回放/定时任务/内部 Feign/单测)无角色 → 三个新增守卫的端点与既有的 /itinerary 端点均视为系统态直接放行(OrderViewGuard.assertReadable 捕获 IllegalStateException 后 return,OrderViewGuard.java:271-276)。
  • 房务角色的 581045 判定先于归属判定,与该订单是否属于本人无关——房务管理员/组长在这三个端点上无论如何都拿不到数据,只能通过配房相关接口工作。
  • 团期管理员的放行只看 OrderInfo.getGroupBatchId() != null(订单主表原始列),散客单一律 581008,即使该团期管理员对其他团期子订单有权限。
  • itinerary-document 的守卫在方法体第 2 步(订单判空之后)触发,比 documentType 缺失的 400 校验晚——所以「非归属角色 + 不传 documentType」拿到的是 400,不是 581008。

六.6、修改前后对比(修改/删除类接口必写,新增跳过)

字段级对比

接口 改前 改后
GET .../tags 无归属校验,请求体/响应体结构不变 新增 581008/581045 两种拒绝响应,请求体/响应体结构不变
GET .../tag-picker 同上 同上
GET .../itinerary-document 同上 同上
GET .../itinerary 四个价格字段 @ApiModelProperty 文案未标注采购价/对客报价区分 文案补充「供应商采购价,非对客报价」;字段名、类型、序列化方式均不变
resourceType(打回请求体) 字段说明未提及 TRAVEL/TRANSFER 边界 字段说明补充「ALL/VEHICLE 车侧仅覆盖 TRAVEL,不含 TRANSFER」;@Pattern 校验、默认值、字段名均不变

行为级对比

行为 改前 改后
任意后台角色传任意 orderId 访问已挂标签/打标签弹窗/电子行程单 200,直接返回目标订单数据(含他人订单) 非放行角色 581008 或 581045,读不到他人订单数据
resourceType=ALL 批量打回、勾选户只有接送机需求 该户因「无可打回的需求」落 589534,行为未变 行为完全未变,仅补充契约说明
/itinerary 四个价格字段的语义 字段名暗示价格但未明确采购价/售价 字段注释明确标注为供应商采购价,防止误当对客报价展示

六.7、影响评估(修改/删除类必写)

  • 是否破坏向后兼容:三个协作域端点对非放行角色(多数后台角色,若非订单归属定制师)是破坏性收紧——改前 200 能拿到数据,改后 581008/581045。对归属定制师、ADMIN/SUPER_ADMIN/VEHICLE_MANAGER,以及读团期子订单的团期管理员,行为不变仍 200。/itinerary 与批量打回两个纯文档改动零行为影响。
  • 前端是否必须同步上线:三个协作域端点——如果前端当前允许任意角色打开任意订单的标签/打标签弹窗/生成行程单(例如客服临时查看非本人订单),上线后这部分角色会开始收到 581008/581045,前端需要能正确展示失败提示(走通用错误提示即可,不需要特殊 UI,但不能吞掉这个错误)。
  • 前端 workaround 清理点:无——本次是后端补权限收紧,不涉及前端此前绕过某个限制的逻辑。

七、不影响范围(显式声明, 帮前端/QA 缩小排查面)

  • 仅影响:GET /v3/admin/order/{orderId}/tags、GET /v3/admin/order/{orderId}/tag-picker、GET /v3/admin/order/{orderId}/itinerary-document 三个端点新增归属校验;GET /v3/admin/order/{id}/itinerary 与 POST /v3/admin/order/group-batch/{groupBatchId}/requirement/reject 仅字段说明文案变化。
  • 零影响:
    • CollabAdminController 的四个写端点(addTag/deleteTag/patchTag/replaceTags):本次未改动,仍走既有的 OrderViewGuard.assertNotGroupBatchManagerWrite()(只拒团期管理员写操作,不判定其余角色归属)。已知缺口:这四个写端点至今不判订单归属,读面收紧后形成「看不到但能改」,由 #8292 承接(口径未定)。
    • OrderViewGuard.assertOrderFinanceReadable 覆盖的财务/成本/流水端点:本次未改动其挂载。
    • 接送机相关的批量确认入口(batchConfirmTransfer 等)与单户打回接口 POST .../vehicle-requirement/reject?kind=TRANSFER:本次未变。
    • ItineraryVO、RejectRequirementReqVO、GroupBatchRequirementRejectRespVO 的字段名、类型、序列化方式、@Pattern/@Size/@NotEmpty 校验规则:全部未变。

八、测试环境已验证

部署:hl-order-service-v3 dev-v3 @ 841ea7b06,2026-09-23 20:07:37 部署,deploy-status STATE=ok,BEHIND 0(读数来源 #8170 评论 #60783)。

真实网关实测(夹具订单 2100542584106496001,归属定制师为他人):

GET /v3/admin/order/2100542584106496001/tags               CUSTOMIZER(非归属) → 581008 ✓
GET /v3/admin/order/2100542584106496001/tag-picker          CUSTOMIZER(非归属) → 581008 ✓
GET /v3/admin/order/2100542584106496001/itinerary-document  CUSTOMIZER(非归属) → 581008 ✓
GET /v3/admin/order/2100542584106496001/tags               SUPER_ADMIN(对照)  → 200 ✓
GET /v3/admin/order/2100542584106496001/tag-picker          SUPER_ADMIN(对照)  → 200 ✓
GET /v3/admin/order/2100542584106496001/itinerary-document  SUPER_ADMIN(对照)  → 200 ✓
GET /v3/admin/order/2100542584106496001/itinerary-document  不带 documentType   → 400,先于守卫触发 ✓

单元测试:本单随 PR 新增/改动测试文件 CollabServiceTest(+80 行,补三接口归属守卫放行/拒绝用例)、CollabServiceItineraryDocumentTest(+60 行,补 itinerary-document 归属守卫用例)、GroupBatchRequirementServiceTest(+50 行,补 resourceType 契约用例),随 PR 一并合并。


十、相关文档

关联 / 联系人

链接

联系人

  • 后端负责人: @wx
  • 前端负责人: @mmg