--- schema: "hl-changelog/v2" ticket: "7135" title: "团期创单收紧 productBatchId 校验(班期归属产品 / 非 GROUP 拒收 / tierSeq 存在性 / 日期按班期)" consumer: "admin" author: "wx(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "pending" frontend_owner: "" frontend_ref: "" target_release: "" verified_at: "" status_note: "服务端对 POST /v3/admin/order 新增三个校验错误码 581055/581056/581057,响应 departureDate/returnDate 改为落库值;前端新建订单向导须仅对 GROUP 产品传 productBatchId 且班期必须属于所选产品。" updated_at: "2026-09-06" base: "dev-v3" --- # 订单模块:团期创单校验收紧(班期归属产品 / 非 GROUP 拒收 / tierSeq 存在性 / 日期按班期) 管理后台创单接口 `POST /v3/admin/order` 对 GROUP 产品的团期校验收紧,新增三个拒单错误码(581055/581056/581057),同时出发日期、返团日期改为班期的权威值。修复跨产品班期串号导致订单错误归团、CORE/CUSTOM 单被误命中团期逻辑等问题。 ## ⚠️ 关键变化 - **仅 GROUP 产品可传 productBatchId**:CORE/CUSTOM 请求含 productBatchId → 拒单 581056「非团期产品不能指定团期」(改前静默落库并误命中团期分支)。 - **班期必须属于所选产品**:productBatchId 对应班期的 productId ≠ 请求 productId → 拒单 581055「所选团期不属于该产品」(改前会按别家班期计价并挂到别家的团)。 - **tierSeq 必须在产品配置内**(全产品类型):不在 tierPrices ∪ tiers 并集中 → 拒单 581057「所选档位不存在」;产品未配档位时不拦(改前 tier_name 落 NULL)。 - **响应 departureDate/returnDate 改为班期权威值**:GROUP 单出发日 = 班期出发日;返团日 = 班期 endDate,或 班期出发日 + 行程天数 − 1(班期无 endDate 时)。改前响应回显请求日期,落库日期与班期脱钩。 - **GROUP 单响应 tierName 现有值**:来自产品 tiers 配置,改前恒 null。 - **人数超班期剩余名额拒单(581034)**(#7159 并入本 PR):`成人+儿童+小童 > 班期剩余名额` → 581034;此前订单侧读的 `remainingSlots` 字段在产品侧「库存改造 Phase 2」后已无来源、恒 null,致该预查长期 no-op,现改读产品侧权威字段 `remainingParticipants`(null=人数不限,跳过校验)。 --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 创建订单(管理端) | POST | `/v3/admin/order` | 请求新增校验 / 响应日期改值 | productBatchId 仅 GROUP 可传;班期归属;tierSeq 存在性;日期按班期 | --- ## 三、接口详情 ### 1. 创建订单(管理端) `POST /v3/admin/order` **VO**: `OrderCreateReqVO → OrderCreateRespVO` #### 使用场景 管理端新建订单向导或团期看板新增子订单调用。创建跟团游、定制游、线路游订单,GROUP 产品必须指定班期,CORE/CUSTOM 产品不允许指定班期。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | `productId` | Body | Long(String)| ✅ | 正整数 | 产品 ID | | `tierSeq` | Body | Integer | ✅ | ≥ 1;必须存在于产品配置档位 | 档位序号,不在产品 tierPrices ∪ tiers 配置内 → 581057 | | `departureDate` | Body | yyyy-MM-dd | ✅ | 不早于当天;日期格式 | 出发日期;出发日早于今天 → 581011 | | `adultCount` | Body | Integer | ✅ | ≥ 1 | 成人数 | | `childCount` | Body | Integer | 否 | ≥ 0,默认 0 | 儿童数(5-12 岁) | | `youngChildCount` | Body | Integer | 否 | ≥ 0,默认 0 | 小童数(3-4 岁) | | `babyCount` | Body | Integer | 否 | ≥ 0,默认 0 | 婴儿数(0-2 岁) | | `customerName` | Body | String | ✅ | @NotBlank | 客户姓名 | | `customerPhone` | Body | String | ✅ | ^1[3-9]\d{9}$ | 客户手机号(明文传,DB 层 AES 加密) | | `customerRemark` | Body | String | 否 | ≤ 500 | 客户备注 | | `createSource` | Body | String | 否 | ≤ 20 字;枚举:CUSTOMER / CONSULTANT / OTA / WALK_IN / B2B / VIP_REPURCHASE / REFERRAL / PROMOTION / INTERNAL;默认 CONSULTANT | 创建来源 | | `productBatchId` | Body | Long(String) | 条件必填 | 仅 GROUP 产品可传;CORE/CUSTOM 传了 → 581056;班期 productId 必须等于请求 productId,否则 → 581055 | 团期 ID(product 侧班期 batchId);**GROUP 产品必传,CORE/CUSTOM/ROUTE 禁传**;值须属于请求 productId 对应产品 | | `roomCount` | Body | Integer | 否 | ≥ 1 | 房间数 | | `tags` | Body | List | 否 | 无约束 | 订单标签名列表 | | `sharerOpenid` | Body | String | 否 | 无约束 | 分享人 openid(C 端裂变追踪) | | `customizerId` | Body | Long | 否 | 无约束 | 分享归因定制师 ID | #### 出参 `Result` | 字段 | 类型 | 说明 | |------|------|------| | `data.id` | Long | 订单主键(雪花 ID 序列化为 String) | | `data.orderNo` | String | 订单号(创单瞬间生成,永不变) | | `data.orderStatus` | String | 粗状态枚举(创单后 = PENDING_PAY) | | `data.orderStatusName` | String | 粗状态中文名(创单后 = 待支付) | | `data.flowStatus` | String | 细状态枚举值(创单后 = AWAITING_PAY) | | `data.flowStatusName` | String | 细状态中文名 | | `data.flowStep` | Integer | 线性 6 步当前步序号(创单初态 = 0) | | `data.flowStepTotal` | Integer | 线性 6 步总步数(固定 6) | | `data.flowStepName` | String | 线性 6 步当前步中文名 | | `data.consultantId` | Long | 实际绑定定制师 ID(序列化为 String) | | `data.consultantSource` | String | 定制师来源(DEFAULT_ASSIGNED / LINK_BOUND / MANUAL / SHARED) | | `data.tags` | List | 系统自动打的标签 | | `data.createdAt` | yyyy-MM-ddTHH:mm:ss | 创单时间 | | `data.productName` | String | 产品名称 | | `data.tierName` | String | 档位名(v5.18 新增,**GROUP 单现有值来自产品 tiers 配置,CORE/CUSTOM/ROUTE 仍为 null**) | | `data.groupBatchName` | String | 拼团批次名(仅供兼容,恒 null,勿读) | | `data.departureDate` | yyyy-MM-dd | **出发日期(v5.18;改后 = 班期权威出发日,与请求值可能不同)** | | `data.returnDate` | yyyy-MM-dd | **返团日期(v5.18;改后 = 班期 endDate 或班期出发日 + 行程天数 − 1,与请求值可能不同)** | | `data.totalAmount` | BigDecimal(String) | 订单总价(元) | | `data.depositAmount` | BigDecimal(String) | 建议定金金额(元) | | `data.depositRatio` | Integer | 定金比例百分比;RATIO 模式有值,FIXED 模式为 null,FULL 模式 = 100 | | `data.depositMode` | String | 定金计算模式(FIXED / RATIO / FULL) | | `data.paymentMode` | String | 支付模式(DEPOSIT / FULL) | | `data.expiryMinutes` | Integer | 支付时限分钟数(默认 1440 = 24h) | | `data.payUrl` | String | 支付页绝对 URL | | `data.customerName` | String | 客户姓名(回显) | | `data.groupBatchId` | Long(String) | **运营团期 ID(order 侧),非空 = 团订单,为空 = 普通订单,这是唯一判别** | | `data.productBatchId` | Long(String) | 团期产品排期 ID(product 侧 batchId,仅供溯源) | | `data.groupOrder` | Boolean | 是否团订单(= groupBatchId 非空的派生值) | #### 请求示例 ```json { "productId": "2044306857534636034", "tierSeq": 1, "departureDate": "2026-10-01", "adultCount": 1, "childCount": 1, "youngChildCount": 0, "babyCount": 0, "customerName": "张三", "customerPhone": "13800009601", "createSource": "CONSULTANT", "productBatchId": "2052935476557328386" } ``` #### 响应示例(成功) ```json { "code": 200, "message": "成功", "data": { "id": "2096414445365338113", "orderNo": "HL20260906094527867", "orderStatus": "PENDING_PAY", "orderStatusName": "待支付", "flowStatus": "AWAITING_PAY", "flowStatusName": "待支付", "flowStep": 0, "flowStepTotal": 6, "flowStepName": "待支付", "consultantId": "50001234567890", "consultantSource": "DEFAULT_ASSIGNED", "tags": [], "createdAt": "2026-09-06T09:45:27", "productName": "冻干粉发短信给", "tierName": "标准档", "groupBatchName": null, "departureDate": "2026-10-01", "returnDate": "2026-10-03", "totalAmount": "5850.00", "depositAmount": "1000.00", "depositRatio": null, "depositMode": "FIXED", "paymentMode": "DEPOSIT", "expiryMinutes": 1440, "payUrl": "https://pay.hulalv.com/pay/HL20260906094527867", "customerName": "张三", "groupBatchId": "2096412454643802114", "productBatchId": "2052935476557328386", "groupOrder": true }, "success": true } ``` #### 空数据 / 降级响应 创建接口无「空数据」场景(成功即返回订单对象)。降级路径:所依赖的产品服务 Feign 不可用时按错误码降级而非返回空——拉班期失败返 581027、拉报价失败返 581032,前端据 `code` 提示重试,不会返回 `data=null` 的成功包。 #### 错误响应 **出发日期早于今天** ```json { "code": 581011, "message": "出发日期不能早于今天", "data": null, "success": false } ``` **档位不存在(tierSeq 未在产品配置内)** ```json { "code": 581057, "message": "所选档位不存在", "data": null, "success": false } ``` **非团期产品不能指定团期** ```json { "code": 581056, "message": "非团期产品不能指定团期", "data": null, "success": false } ``` **所选团期不属于该产品** ```json { "code": 581055, "message": "所选团期不属于该产品", "data": null, "success": false } ``` **既有 GROUP 校验错误(团期缺失)** ```json { "code": 581026, "message": "团期产品必须选择团期", "data": null, "success": false } ``` **参数校验错误(如手机号格式错误)** ```json { "code": 400, "message": "手机号格式错误", "data": null, "success": false } ``` #### 业务边界 - **productBatchId 传值规则**:仅 GROUP 产品可传且必传(除非为空表示不下单);CORE/CUSTOM/ROUTE 产品绝不能带。 - **班期归属校验**:productBatchId 对应班期的 productId 必须等于请求 productId;不同则拒单 581055,不会创建订单或部分落库。 - **tierSeq 校验**:必须存在于产品的 tierPrices JSON(CORE/CUSTOM/ROUTE)或 tiers JSON(GROUP);产品未配任何档位时保持放行(存量产品兼容)。 - **出发日期与返团日期**:响应值为班期或产品的权威日期,可能与请求值不同。前端展示及后续行程渲染必须以响应值为准,不要缓存请求值。 - **错误码优先级顺序**:出发日期 581011 → 非 GROUP 拒收 581056 → tierSeq 581057 → GROUP 缺 productBatchId 581026 → 拉班期 581027 → 班期归属 581055 → 后续班期状态 / 截止 / 库存检查。 - **鉴权**:网关注入 `X-Admin-Id` 头,consultantId 无法解析返 581013。 - **幂等性**:客户端不得重试;同一 orderNo 存量幂等托管由 order-v3 内核保证。 --- ## 四、契约约束与正确调用方式 | 场景 | 正确调用 | 错误调用 | 结果 | |------|---------|---------|------| | GROUP 产品创单 | 传 productBatchId,值取价格日历 items[].batchId | 不传 productBatchId | 581026 团期产品必须选择团期 | | CORE 产品创单 | 不传 productBatchId / productBatchId = null | 传 productBatchId(任何值) | 581056 非团期产品不能指定团期 | | CUSTOM 产品创单 | 不传 productBatchId / productBatchId = null | 传 productBatchId(任何值) | 581056 非团期产品不能指定团期 | | 档位选择 | tierSeq 必须在产品已配档位内 | tierSeq 超过产品最大档位序号 | 581057 所选档位不存在 | | 班期串号防护 | 班期 batchId 必须属于所选 productId | 前端用不同产品的 batchId | 581055 所选团期不属于该产品 | | 日期展示 | 使用响应 departureDate / returnDate | 使用请求的出发日期 | 行程日期与班期脱钩,退款档位错位 | --- ## 五、数据库行为 | 操作 | 落库字段 | 说明 | |------|---------|------| | GROUP 创单(班期出发 2026-10-01,endDate 2026-10-03,tripDays=3) | order_main.depart_date = 2026-10-01(班期值) | 请求可能为 2026-10-02,但落库为班期出发日 | | GROUP 创单(班期无 endDate) | order_main.return_date = 班期出发日 + tripDays - 1 | 如班期 2026-10-01,tripDays=3,则 return_date=2026-10-03 | | GROUP 创单(班期有 endDate) | order_main.return_date = 班期 endDate | endDate 为准,与 tripDays 无关 | | GROUP 创单 | order_main.tier_name = 产品 tiers JSON 对应 tierSeq 的值 | 改前恒 null;CORE/CUSTOM 仍为 null(无 tiers JSON) | | CORE 创单带 productBatchId | 不创建,拒单 581056 | 改前会落库 productBatchId,误命中团期逻辑 | | 班期串号 productBatchId 错 | 不创建,拒单 581055 | 改前会按班期产品计价,订单挂错团 | --- ## 六、边界行为 - 业务失败仍 HTTP 200,**必须检查 `code` 字段判成败**。 - 存量订单不回溯修正;仅新建订单应用新规则。 - 产品未配档位时保持放行(向后兼容存量产品);即使 tierSeq 传 99 也不拦。 - 班期 productId 缺失时仅告警日志,不拦单(product 侧老数据未回填,宁缺毋滥)。 - 网关注入 `X-Admin-Id` 为 null 时返 581013,改前静默取 JWT;两者互斥。 --- ## 六.6、修改前后对比 针对 `POST /v3/admin/order`(GROUP 产品): | 维度 | 修改前 | 修改后 | |------|--------|--------| | CORE/CUSTOM 带 productBatchId | 静默落库 product_batch_id,误命中团期 staff 扇出分支 | 拒单 581056「非团期产品不能指定团期」,不落库 | | 跨产品班期(batchId 属别的产品) | 按别家班期计价,订单挂错团 | 拒单 581055「所选团期不属于该产品」(任一侧 productId 为空时仅告警放行) | | tierSeq 不在产品档位内 | 不校验,tier_name 落 NULL | 拒单 581057「所选档位不存在」(产品未配档位时不拦) | | GROUP 单响应/落库出发日 | 回显请求出发日,可能与班期脱钩 | = 班期出发日 | | GROUP 单响应/落库返团日 | 按请求出发日推算 | = 班期 endDate(缺则 班期出发日 + 行程天数 − 1) | | GROUP 单响应 tierName | 恒 null | 现有值(来自产品 tiers 配置) | | 人数超班期剩余名额(#7159) | 预查读 remainingSlots 恒 null,整段 no-op(不拦) | 读 remainingParticipants,超额拒单 581034(人数不限的班期跳过) | ## 六.7、影响评估 - **前端必改**:新建订单向导与团期看板「新增子订单」仅对 GROUP 产品传 `productBatchId`,且取该产品价格日历的 `items[].batchId`;CORE/CUSTOM 绝不能带,否则 581056。 - **前端展示**:出发日期 / 返团日期以创单响应值为准(GROUP 单被班期覆盖),不要回显请求值。 - **向后兼容**:正常 CORE/CUSTOM 创单(不带 productBatchId)行为完全不变;错误码新增不影响既有成功路径。 - **其他端**:小程序 `POST /v3/mp/order` 共用内核,四个校验同样生效,请求契约不变。 - **数据安全**:跨产品串号单此前会把订单挂到别家团、扣错名额,本次从源头拒绝。 - **回滚**:回滚本次发布即恢复旧行为;已按新规则创建的订单不受影响。 ## 七、不影响范围 - **仅影响**:管理后台「新建订单向导」和「团期看板新增子订单」流程。 - **零影响**: - 小程序端 `POST /v3/mp/order` 共用内核,新校验同样生效但请求契约不变。 - 表结构:无 Flyway 变更、无新列新表。 - 订单列表、详情、订单编辑等读操作。 - CORE/CUSTOM 正常创单(不带 productBatchId)完全不变。 - 团期相关接口(看板、价格日历、班期详情)。 --- ## 八、测试环境已验证 **测试服创单(产品「冻干粉发短信给」productId=2044306857534636034,班期 2026-10-01 productBatchId=2052935476557328386,1 成人 1 儿童,2026-09-06 测试)** - ✓ GROUP 单正例:响应 departureDate/returnDate 与班期一致(班期 2026-10-01 出发、2026-10-03 返);groupBatchId / productBatchId / groupOrder 三字段已填值。 - ✓ CORE 产品 2056944461216100353 带 productBatchId → HTTP 200,code 581056「非团期产品不能指定团期」。 - ✓ productId=2044306857534636034 + productBatchId=2089667212070612995(属另一产品) → HTTP 200,code 581055「所选团期不属于该产品」。 - ✓ tierSeq=9(产品只配 1-3 档) → HTTP 200,code 581057「所选档位不存在」。 - ✓ GROUP 单不传 productBatchId → HTTP 200,code 581026「团期产品必须选择团期」。 **单测覆盖**(定向复测全绿):OrderServiceTest 193/193 ✓ | GroupOrderStrategyTest 30/30 ✓(含 581034 三例)| OrderCreateTransactionExecutorTest 21/21 ✓ | ProductTierResolverTest 11/11 ✓ | OrderMpCreateServiceTest 6/6 ✓ | BatchInfoVODeserializationTest 2/2 ✓ | OrderMpReadServiceTest 15/15 ✓ | InternalOrderSnapshotControllerTest 4/4 ✓ | E2eScopedOrderCreateServiceTest 19/19 ✓;ArchTest 全绿(LayerEnforcement 5 / RedLine 9 / MapperBoundary 26 / HouseModuleBoundary 4 / DashboardLayer 2 / LocalCacheVetting 1)。 **网关验证(已部署 dev-v3 复测,2026-09-06 15:00)**:合并提交 977be08e 部署测试服,双实例滚动重启完成(8086/8186 新 PID、jar 已更新)。网关实测 15/15 通过:S2 正例 200(日期=班期 10-01/10-03)、S10 跨产品班期 581055、S11 日期不一致回显班期日期、S13 tierSeq=9 581057、S14 CORE 带班期 581056、S16 GROUP tierName=轻奢;#7142 判团字段在创单响应/详情/列表三处透出、#7143 productId 在看板/分页/详情三接口透出,均已核。581034(#7159)判定逻辑经三条单测证实生效;线上端到端受测试账号对产品 schedule 无编辑权限(403)与限额班期 getBatchInfo 存量异常(581027)所限未实跑,详见 Issue #7159 评论。 --- ## 十、相关文档 - 关联 Issue: [wx/HL#7135](https://git.1814.love:8443/wx/HL/issues/7135) - 关联 PR: [wx/HL#7169](https://git.1814.love:8443/wx/HL/pulls/7169)(Closes #7135、#7159;合并提交 977be08e) - 关联工单 #7142(判团字段 groupBatchId / productBatchId / groupOrder)、#7143(看板 productId 显示)。 - 团期接口文档:GB-ADM-00B(OpenWiki)。 --- ## 关联 / 联系人 ### 链接 - **Issue**: [#7135](https://git.1814.love:8443/wx/HL/issues/7135) - **PR**: [#7169](https://git.1814.love:8443/wx/HL/pulls/7169)(含 #7159) - **Merge commit**: 977be08eccd0a32e27c01dce41de210a53f13e5d ### 联系人 - **后端负责人**: @wx