diff --git a/changelogs-v2/2026-09/06_7135_团期创单收紧productBatchId校验-修改接口-管理后台.md b/changelogs-v2/2026-09/06_7135_团期创单收紧productBatchId校验-修改接口-管理后台.md new file mode 100644 index 00000000..64c2911d --- /dev/null +++ b/changelogs-v2/2026-09/06_7135_团期创单收紧productBatchId校验-修改接口-管理后台.md @@ -0,0 +1,351 @@ +--- +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 diff --git a/changelogs-v2/2026-09/06_7142_订单详情列表创单响应透出判团字段-修改接口-管理后台.md b/changelogs-v2/2026-09/06_7142_订单详情列表创单响应透出判团字段-修改接口-管理后台.md new file mode 100644 index 00000000..c95f9607 --- /dev/null +++ b/changelogs-v2/2026-09/06_7142_订单详情列表创单响应透出判团字段-修改接口-管理后台.md @@ -0,0 +1,509 @@ +--- +schema: "hl-changelog/v2" +ticket: "7142" +title: "订单详情/列表/创单响应透出 groupBatchId·productBatchId·groupOrder 判团字段" +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: "" +updated_at: "2026-09-06" +base: "dev-v3" +--- + +# 订单模块:判团字段透出(详情·列表·创单) + +三个订单读接口响应透出判团字段 `groupBatchId` / `productBatchId` / `groupOrder`,供前端判断订单是否为团订单。关键口径:**判团只读 `groupBatchId`(非空=团订单)或 `groupOrder`**;`productBatchId` 仅供溯源,不参与判团。 + +## ⚠️ 关键变化 + +- **判团唯一口径** `groupBatchId` 非空或 `groupOrder = true` → 团订单;为空/false → 普通订单。 +- **不要拿 `productBatchId` 反推团单**,该字段仅供产品侧班期溯源展示,产品侧可能有多个班期映射同一团期。 +- 前一版 #7083 仅涉及订单主表冻结,本次全量透出给前端消费,与主表字段同源。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 订单详情 - 主单数据 | GET | `/v3/admin/order/{id}` | 响应新增字段 | data.main 新增 4 字段 | +| 2 | 订单分页列表 | GET | `/v3/admin/order` | 响应新增字段 | data.list[] 新增 2 字段 | +| 3 | 创建订单 | POST | `/v3/admin/order` | 响应新增字段 | data 新增 3 字段 | + +--- + +## 三、接口详情 + +### 1. 订单详情 - 主单数据 `GET /v3/admin/order/{id}` + +**VO**: `OrderMainVO`(响应位置:`data.main`) + +#### 使用场景 + +打开订单详情页面时,读取订单主单数据及其判团标记,供前端决定显示团期相关内容(如「团期编号」、「团期名称」等)。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| `id` | Path | String | 是 | 正整数 ID | 订单 ID | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `data.main.groupBatchId` | String/null | 运营团期主键(order_group_batch.group_batch_id);非空=团订单,为空=普通订单 | +| `data.main.productBatchId` | String/null | 产品侧班期 ID(product_v2.group_tour_batch.batch_id);仅供溯源,不参与判团 | +| `data.main.groupOrder` | Boolean | 派生值,= groupBatchId != null;直接用于前端判团 | +| `data.main.batchNo` | String/null | 团期编号(order_group_batch.batch_no);普通单为 null,团期软删也为 null | +| `data.main.batchName` | String/null | 团期名称(order_group_batch.batch_name);普通单为 null,团期软删也为 null | + +#### 请求示例 + +```http +GET /v3/admin/order/2096414445365338113 +Authorization: Bearer +``` + +无请求体。 + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "main": { + "id": "2096414445365338113", + "orderNo": "HL20260906094527867", + "orderStatus": "PENDING_PAY", + "orderStatusName": "待支付", + "flowStatus": "AWAITING_PAY", + "flowStatusName": "待补全信息", + "flowStep": 0, + "flowStepTotal": 6, + "totalAmount": "5850.00", + "depositAmount": "1000.00", + "departureDate": "2026-10-01", + "returnDate": "2026-10-03", + "groupBatchId": "2096412454643802114", + "productBatchId": "2052935476557328386", + "groupOrder": true, + "batchNo": "Q202610012052935476548939777", + "batchName": "10月1日长白山亲子团", + "progressStepper": [] + }, + "profile": {}, + "resource": {}, + "contract": {}, + "insurance": {}, + "refund": {}, + "aftersale": {}, + "financial": {} + } +} +``` + +#### 空数据 / 降级响应 + +普通订单时,`groupBatchId`、`productBatchId`、`batchNo`、`batchName` 均为 null;`groupOrder` 为 false。 + +#### 错误响应 + +```json +{ + "code": 581007, + "message": "订单不存在", + "success": false, + "data": null +} +``` + +示例错误码: +- 581007:订单不存在 +- 581045:房务角色无权查看订单详情(HTTP 200 code) + +#### 业务边界 + +- 沿用订单详情权限校验;业务失败仍为 HTTP 200,需检查 code。 +- 普通订单与团单在出参结构上无区别,仅字段值不同(null vs 有值)。 +- 团期已软删时,`groupBatchId` 存在但对应记录不可查,`batchNo` / `batchName` 回退为 null。 +- `productBatchId` 与 `groupBatchId` 无必然对应,前端不做交叉校验。 + +--- + +### 2. 订单分页列表 `GET /v3/admin/order` + +别名接口:`GET /v3/admin/order/list` + +**VO**: `OrderListItemRespVO`(响应位置:`data.list[]`) + +#### 使用场景 + +订单列表页加载数据时,带上新增的判团标记,供前端快速判断每行是否为团订单,可用于条件展示「团期信息」列或其他团期特化功能。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| `orderStatus` | Query | String | 否 | 枚举多值(逗号分隔) | 粗状态过滤(PENDING_PAY、CUSTOMIZING 等) | +| `flowStatus` | Query | String | 否 | 枚举多值(逗号分隔) | 细状态过滤(AWAITING_PAY、RESOURCE_PREPARING 等) | +| `tagNames` | Query | Array | 否 | - | 按标签过滤(多标签 OR 关系) | +| `keyword` | Query | String | 否 | - | 搜索关键字(团号/客户姓名/产品名/订单号 LIKE) | +| `departureDateFrom` | Query | String | 否 | yyyy-MM-dd | 出发日期范围起始 | +| `departureDateTo` | Query | String | 否 | yyyy-MM-dd | 出发日期范围结束 | +| `createSource` | Query | String | 否 | - | 来源过滤(CONSULTANT/MP/...) | +| `cancelled` | Query | Boolean | 否 | - | 是否含已取消订单(默认 false) | +| `consultantName` | Query | String | 否 | - | 定制师姓名 LIKE 模糊匹配 | +| `statusGroup` | Query | String | 否 | 枚举单值 | 按 Tab 分组(ALL/BEFORE_TRIP/ON_TRIP/SETTLEMENT/ABNORMAL/AFTERSALE) | +| `page` | Query | Integer | 是 | ≥1 | 页码 | +| `pageSize` | Query | Integer | 是 | ≤100 | 每页条数 | + +#### 出参 `Result>` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `data.list[].groupBatchId` | String/null | 运营团期主键(order_group_batch.group_batch_id);非空=团订单 | +| `data.list[].groupOrder` | Boolean | 派生值,= groupBatchId != null;直接用于前端判团 | + +其他字段详见现有订单列表接口文档。 + +#### 请求示例 + +```http +GET /v3/admin/order?page=1&pageSize=20 +Authorization: Bearer +``` + +无请求体。 + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "total": 150, + "list": [ + { + "id": "2096414445365338113", + "orderNo": "HL20260906094527867", + "productName": "冻干粉发短信给", + "customerName": "张三", + "departureDate": "2026-10-01", + "orderStatus": "PENDING_PAY", + "flowStatus": "AWAITING_PAY", + "totalAmount": "5850.00", + "groupBatchId": "2096412454643802114", + "groupOrder": true + }, + { + "id": "2096414445365338114", + "orderNo": "HL20260906094527868", + "productName": "其他产品", + "customerName": "李四", + "departureDate": "2026-10-02", + "orderStatus": "PENDING_DEPARTURE", + "flowStatus": "PENDING_DEPARTURE", + "totalAmount": "8000.00", + "groupBatchId": null, + "groupOrder": false + } + ] + } +} +``` + +#### 空数据 / 降级响应 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "total": 0, + "list": [] + } +} +``` + +#### 错误响应 + +```json +{ + "code": 401, + "message": "未登录或登录已过期", + "success": false, + "data": null +} +``` + +#### 业务边界 + +- 分页字段 `page` / `pageSize` 沿用现有约束。 +- 列表返回最新 50 条或 100 条时,两个新增字段保证同时返回,不存在部分返回的情况。 +- `groupOrder` 是 `groupBatchId != null` 的派生布尔值,前端可二选一使用。 +- 普通订单与团单混合返回,字段值直接对标订单属性。 + +--- + +### 3. 创建订单 `POST /v3/admin/order` + +**VO**: `OrderCreateRespVO`(响应位置:`data`) + +#### 使用场景 + +创建订单后,管理端「订单已创建」弹窗或后续流程需判断该单是否为团单,及时显示团期相关信息(如「团期编号」、「出发日期」等)。 + +#### 入参 + +沿用现有 `OrderCreateReqVO`,**请求体不变**(本单只改响应)。字段表: + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| `productId` | Body | String(Long) | ✅ | 正整数 | 产品 ID | +| `tierSeq` | Body | Integer | ✅ | ≥1 | 档位序号 | +| `departureDate` | Body | String(yyyy-MM-dd) | ✅ | 不早于今天 | 出发日期 | +| `adultCount` | Body | Integer | ✅ | ≥1 | 成人数 | +| `childCount` | Body | Integer | 否 | ≥0,默认 0 | 儿童数 | +| `youngChildCount` | Body | Integer | 否 | ≥0,默认 0 | 小童数 | +| `babyCount` | Body | Integer | 否 | ≥0,默认 0 | 婴儿数 | +| `customerName` | Body | String | ✅ | @NotBlank | 客户姓名 | +| `customerPhone` | Body | String | ✅ | ^1[3-9]\d{9}$ | 客户手机号 | +| `customerRemark` | Body | String | 否 | ≤500 | 备注 | +| `createSource` | Body | String | 否 | 默认 CONSULTANT | 创建来源 | +| `productBatchId` | Body | String(Long) | 否 | GROUP 必传、非 GROUP 禁传 | 团期 ID(见 #7135) | +| `roomCount` | Body | Integer | 否 | ≥1 | 房间数 | +| `tags` | Body | Array<String> | 否 | — | 订单标签 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `data.id` | String | 订单主键 | +| `data.orderNo` | String | 订单号 | +| `data.orderStatus` | String | 粗状态(创单后为 PENDING_PAY) | +| `data.flowStatus` | String | 细状态(创单后为 AWAITING_PAY) | +| `data.flowStep` | Integer | 线性步序(创单初态为 0) | +| `data.flowStepTotal` | Integer | 总步数(固定 6) | +| `data.productName` | String | 产品名 | +| `data.tierName` | String | 档位名 | +| `data.departureDate` | String | 出发日期 | +| `data.returnDate` | String | 返团日期 | +| `data.totalAmount` | String | 订单总价 | +| `data.depositAmount` | String | 建议定金金额 | +| `data.depositRatio` | Integer/null | 定金比例百分比 | +| `data.depositMode` | String | 定金计算模式(FIXED/RATIO/FULL) | +| `data.paymentMode` | String | 支付模式(DEPOSIT/FULL) | +| `data.groupBatchId` | String/null | 运营团期 ID(创单同事务回写);非空=团订单 | +| `data.productBatchId` | String/null | 产品侧班期 ID(创单入参原样固化);仅供溯源 | +| `data.groupOrder` | Boolean | 派生值,= groupBatchId != null;直接用于判团 | + +#### 请求示例 + +```json +{ + "productId": 2044306857534636034, + "tierSeq": 1, + "departureDate": "2026-10-01", + "adultCount": 2, + "childCount": 1, + "customerName": "张三", + "customerPhone": "13800138000", + "productBatchId": 2052935476557328386, + "createSource": "CONSULTANT" +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "id": "2096414445365338113", + "orderNo": "HL20260906094527867", + "orderStatus": "PENDING_PAY", + "orderStatusName": "待支付", + "flowStatus": "AWAITING_PAY", + "flowStatusName": "待补全信息", + "flowStep": 0, + "flowStepTotal": 6, + "flowStepName": "待支付", + "productName": "冻干粉发短信给", + "tierName": "经典档", + "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 + } +} +``` + +实测数据示例(2026-09-06 测试服): +- 产品「冻干粉发短信给」productId=2044306857534636034 +- 班期 2026-10-01 productBatchId=2052935476557328386 +- 团期主键 groupBatchId=2096412454643802114 +- 团期编号 batchNo=Q202610012052935476548939777 + +#### 空数据 / 降级响应 + +创建订单成功后无空数据响应。 + +#### 错误响应 + +```json +{ + "code": 400, + "message": "产品 ID 非法或产品已下架", + "success": false, + "data": null +} +``` + +#### 业务边界 + +- 创建普通订单时,`groupBatchId` / `productBatchId` 为 null,`groupOrder` 为 false。 +- 创建 GROUP 产品订单时(必须提交 `productBatchId`),后端在同事务内懒创建或命中已有团期,回写 `groupBatchId`。 +- `groupBatchName` 字段当前恒为 null(仅为兼容既有前端契约),**前端不要读它**。 +- 响应中 `groupBatchId` / `productBatchId` 为字符串(JSON 序列化后,避免 JS 精度丢失);前端若需数值运算应转换为字符串存储。 + +--- + +## 四、契约约束与正确调用方式 + +| 场景 | 正确做法 | +|------|---------| +| 判断订单是否团单 | 读 `groupBatchId` 非空 或 `groupOrder == true`,两者等价 | +| 不要用 productBatchId 判团 | `productBatchId` 仅供溯源,可能 null(普通单)或有值(团单/非团单均可能) | +| 团单需显示团期名 | `groupBatchName` 恒为 null,读 `batchName`;团期软删时也为 null | +| 普通单与团单混合渲染 | 按 `groupOrder` 条件渲染,普通单该字段为 false;两类订单出参结构一致,仅值不同 | + +--- + +## 五、数据库行为 + +| 订单类型 | groupBatchId | productBatchId | groupOrder | batchNo | batchName | +|---------|-------------|----------------|-----------|---------|-----------| +| 普通订单 | null | null | false | null | null | +| 团单(命中或懒建) | 非空 | 非空 | true | 有值 | 有值 | +| 团期已软删 | 非空 | 非空 | true | null | null | + +--- + +## 六、边界行为 + +- 业务失败可能仍为 HTTP 200,必须同时检查 `code`、`success` 和 `message`。 +- 详情接口 404/权限 403 时直接返回对应 HTTP 状态码;业务类失败(如订单状态不符)返回 HTTP 200 + code。 +- 列表空结果返回 `total=0, list=[]`,分页参数超界时返回空列表(无 5XX)。 +- 创单失败不落库,响应 HTTP 200 + code,data 为 null。 +- 新增判团字段与现有字段同源、同时刷新,无时间差。 + +--- + +## 六.6、修改前后对比 + +### 字段级对比 + +| 字段 | 改前 | 改后 | +|------|------|------| +| `groupBatchId` | 无此字段 | 新增;运营团期主键 | +| `productBatchId` | 无此字段 | 新增;产品班期 ID(仅溯源) | +| `groupOrder` | 无此字段 | 新增;派生布尔,= groupBatchId != null | +| `batchNo` | 无此字段 | 新增;团期编号(详情/列表) | +| `batchName` | 无此字段 | 新增;团期名称(详情/列表) | + +### 行为级对比 + +| 行为 | 改前 | 改后 | +|------|------|------| +| 前端判团依据 | 无判团字段,无法直接判别 | 读 groupBatchId 非空 或 groupOrder = true | +| 团单信息展示 | 依赖联查或额外接口 | 直接在订单响应中获得 | +| 产品班期溯源 | 不支持 | 新增 productBatchId(仅溯源展示) | + +--- + +## 六.7、影响评估 + +- **是否破坏向后兼容**: 否。新增字段对旧客户端透明,非必需字段缺失时前端框架可靠 null 处理。 +- **前端是否必须同步上线**: 是。前端需接入新增四个字段至详情/列表/创建成功弹窗模板,判团逻辑改用 groupBatchId 或 groupOrder。 +- **前端 workaround 清理点**: + - 删除旧的"通过产品 ID 推断团单"逻辑,改用 groupBatchId 判别 + - 不要硬编码团期编号/名称,改用响应中的 batchNo / batchName + - groupBatchName 恒为 null,勿读之;用 batchName 替代 + +--- + +## 七、不影响范围 + +- **仅影响**: 管理后台订单详情、列表和创建流程的前端渲染 +- **零影响**: + - 订单创建/编辑/取消接口 + - 小程序端(MpOrderDetailVO 不变) + - 数据库结构(新字段冻结在 order_main 表,无表改动) + - 订单写操作和业务流程 + +--- + +## 八、测试环境已验证 + +- **单测**: 559 条测试用例绿✓(新增判团字段相关的 UT 已覆盖普通单/团单双路径) +- **ArchTest**: 45 条架构测试绿✓ +- **测试服网关实测**: 单测 + CR 通过;测试服网关实测见管理者补充 + +实测产品: `productId=2044306857534636034`(冻干粉发短信给),班期 `2026-10-01`(productBatchId=2052935476557328386),团期主键 `groupBatchId=2096412454643802114`,批号 `batchNo=Q202610012052935476548939777`。 + +--- + +## 九、相关历史 PR(功能演进) + +| PR | Issue | 说明 | 是否仍有效 | +|----|-------|------|------------| +| #7083 | - | order_group_batch 一跳直连,订单主表冻结 groupBatchId/productBatchId | ✅ 有效 | +| #7135 | - | GROUP 产品创单校验与重整 | ✅ 有效 | +| #7143 | - | 团期看板 VO 补 productId(配合本 PR) | ✅ 有效 | +| **本 PR #7155** | **#7142** | **订单详情/列表/创单响应透出判团字段** | ✅ 最新 | + +--- + +## 十、相关文档 + +- 关联 Issue: [wx/HL#7142](https://git.1814.love:8443/wx/HL/issues/7142) +- 关联 PR: [wx/HL#7155](https://git.1814.love:8443/wx/HL/pulls/7155) +- 团期接口文档: `docs/ARCHITECTURE.md` §0A.2.2(团单冻结口径) +- 团单业务规范: 见 #7135 changelog 中的团期创建规则 + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#7142](https://git.1814.love:8443/wx/HL/issues/7142) +- **PR**: [#7155](https://git.1814.love:8443/wx/HL/pulls/7155) +- **Merge commit**: [21d3d00de](https://git.1814.love:8443/wx/HL/commit/21d3d00de) + +### 联系人 + +- **后端负责人**: @wx diff --git a/changelogs-v2/2026-09/06_7143_团期看板VO补productId-修改接口-管理后台.md b/changelogs-v2/2026-09/06_7143_团期看板VO补productId-修改接口-管理后台.md new file mode 100644 index 00000000..0e5e10da --- /dev/null +++ b/changelogs-v2/2026-09/06_7143_团期看板VO补productId-修改接口-管理后台.md @@ -0,0 +1,573 @@ +--- +schema: "hl-changelog/v2" +ticket: "7143" +title: "团期看板分页项/详情/看板 VO 补 productId(供新增子订单深链预填产品)" +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: "" +updated_at: "2026-09-06" +base: "dev-v3" +--- + +# 团期模块:看板 VO 补 productId + +三个团期看板读接口响应新增 `productId` 字段,供前端「新增子订单」深链到订单创建向导时预填产品和班期 ID,锁定出发日期。前端逻辑:**仅 GROUP 产品创单时才在 payload 中带 productBatchId**;非 GROUP 产品忽略 productBatchId。 + +## ⚠️ 关键变化 + +- 新增 `productId` 是产品主键,用于深链向导 `/order-v2/new?productId={productId}&productBatchId={productBatchId}&departureDate={date}` 的预填参数。 +- 向导跳转后,**仅 GROUP 产品**应将 productBatchId 放入订单创建 POST payload;其他产品类型忽略该参数。 +- 前端缺陷单已发,说明旧建单向导漏传 productBatchId,新版本补全(见关联链接)。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 团期分页列表 | GET | `/v3/admin/order/group-batch` | 响应新增字段 | data.list[] 各项新增 productId | +| 2 | 团期详情 | GET | `/v3/admin/order/group-batch/{groupBatchId}` | 响应新增字段 | data 新增 productId | +| 3 | 团期看板列表 | GET | `/v3/admin/order/group-batch/board?productId=` | 响应新增字段 | data[] 各项新增 productId | + +--- + +## 三、接口详情 + +### 1. 团期分页列表 `GET /v3/admin/order/group-batch` + +**VO**: `GroupBatchPageItemRespVO`(响应位置:`data.list[]`) + +#### 使用场景 + +加载团期分页列表时,每一行包含产品 ID,前端点击「新增子订单」时取该行的 productId / productBatchId / departureDate 拼接深链,跳转到订单创建向导。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| `productId` | Query | String | 否 | 正整数 ID | 按产品筛选 | +| `batchStatus` | Query | String | 否 | 枚举值 | 按团期状态码筛选(RECRUITING/RESOURCE_PREPARING/...) | +| `deadlineFrom` | Query | String | 否 | yyyy-MM-dd | 报名截止日起 | +| `deadlineTo` | Query | String | 否 | yyyy-MM-dd | 报名截止日止 | +| `opsStage` | Query | String | 否 | 枚举值 | 按运营阶段筛选(RECRUIT/FORMED/PENDING_TRIP/TRAVELLING/AUDITING/CHECKED/DISBANDED) | +| `month` | Query | String | 否 | yyyy-MM | 按出发月份筛选 | +| `keyword` | Query | String | 否 | - | 班期编号/班期名称模糊关键词 | +| `pageNo` | Query | Integer | 是 | ≥1 | 页码,从 1 开始 | +| `pageSize` | Query | Integer | 是 | ≤100 | 每页条数,默认 20 | + +#### 出参 `Result>` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `data.list[].groupBatchId` | String | 团期聚合主键 | +| `data.list[].productBatchId` | String | product 侧班期 ID | +| `data.list[].productId` | String | **【新增】** 产品 ID,供深链预填 | +| `data.list[].productName` | String | 产品名称 | +| `data.list[].batchNo` | String | 班期编号 | +| `data.list[].batchName` | String | 班期名称 | +| `data.list[].batchStatus` | String | 团期状态码 | +| `data.list[].batchStatusName` | String | 状态中文名 | +| `data.list[].minGroupPeople` | Integer | 最低成团人数 | +| `data.list[].maxRooms` | Integer | 房间容量 | +| `data.list[].maxParticipants` | Integer | 人数容量 | +| `data.list[].enrolledPeople` | Integer | 已报名人数 | +| `data.list[].enrolledRooms` | Integer | 已用房间数 | +| `data.list[].remainRooms` | Integer | 剩余房间数 | +| `data.list[].remainParticipants` | Integer | 剩余人数 | +| `data.list[].orderCount` | Integer | 子订单数 | +| `data.list[].enrollDeadline` | String | 报名截止日 | +| `data.list[].departDate` | String | 出发日期 | +| `data.list[].endDate` | String | 结束日期 | +| `data.list[].receivableAmount` | String | 整团应收合计 | +| `data.list[].receivedAmount` | String | 整团已收合计 | +| `data.list[].chips` | Object | 六芯片整团聚合态(hotel/vehicle/guide/photo/contract/insurance,各为字符串状态:全部完成为 `DONE`,存在未完成项为待办态;完整取值见六芯片文档 GB-ADM-090~095) | + +#### 请求示例 + +```http +GET /v3/admin/order/group-batch?pageNo=1&pageSize=20 +Authorization: Bearer +``` + +无请求体。 + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "total": 5, + "list": [ + { + "groupBatchId": "2096412454643802114", + "productBatchId": "2052935476557328386", + "productId": "2044306857534636034", + "productName": "冻干粉发短信给", + "batchNo": "Q202610012052935476548939777", + "batchName": "10月1日长白山亲子团", + "batchStatus": "RECRUITING", + "batchStatusName": "招募中", + "minGroupPeople": 6, + "maxRooms": 4, + "maxParticipants": 10, + "enrolledPeople": 4, + "enrolledRooms": 2, + "remainRooms": 2, + "remainParticipants": 6, + "orderCount": 2, + "enrollDeadline": "2026-09-25", + "departDate": "2026-10-01", + "endDate": "2026-10-03", + "receivableAmount": "48000.00", + "receivedAmount": "36000.00", + "chips": { + "hotel": "DONE", + "vehicle": "DONE", + "guide": "DONE", + "photo": "DONE", + "contract": "DONE", + "insurance": "DONE" + } + } + ] + } +} +``` + +#### 空数据 / 降级响应 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "total": 0, + "list": [] + } +} +``` + +#### 错误响应 + +```json +{ + "code": 589507, + "message": "无操作权限", + "success": false, + "data": null +} +``` + +#### 业务边界 + +- 沿用团期列表权限校验(团期管理员/运营/定制师等);无权返回 589507。 +- `productId` 与其他字段同时生效,始终非空(命中行/未命中行/孤儿行均返回)。 +- 分页参数超界时返回空列表。 +- 业务失败仍为 HTTP 200,需检查 code。 + +--- + +### 2. 团期详情 `GET /v3/admin/order/group-batch/{groupBatchId}` + +**VO**: `GroupBatchDetailRespVO`(响应位置:`data`) + +#### 使用场景 + +打开团期详情页时,读取产品 ID,供「新增子订单」按钮拼接深链,跳转订单创建向导并预填产品及班期。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| `groupBatchId` | Path | String | 是 | 正整数 ID | 团期主键 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `data.groupBatchId` | String | 团期聚合主键 | +| `data.productBatchId` | String | product 侧班期 ID | +| `data.productId` | String | **【新增】** 产品 ID,供深链预填 | +| `data.productName` | String | 产品名称 | +| `data.batchNo` | String | 班期编号 | +| `data.batchName` | String | 班期名称 | +| `data.batchLabel` | String/null | 班期标签快照 | +| `data.batchStatus` | String | 团期状态码 | +| `data.batchStatusName` | String | 状态中文名 | +| `data.minGroupPeople` | Integer | 最低成团人数 | +| `data.maxRooms` | Integer | 房间容量 | +| `data.maxParticipants` | Integer | 人数容量 | +| `data.enrolledPeople` | Integer | 已报名人数 | +| `data.enrolledRooms` | Integer | 已用房间数 | +| `data.remainRooms` | Integer | 剩余房间数 | +| `data.remainParticipants` | Integer | 剩余人数 | +| `data.hotelReady` | Boolean | 配房完成标志 | +| `data.vehicleReady` | Boolean | 配车完成标志 | +| `data.guideReady` | Boolean | 导游完成标志 | +| `data.photographerReady` | Boolean | 摄影完成标志 | +| `data.materialConfirmed` | Boolean | 物资确认标志 | +| `data.requirementConfirmed` | Boolean | 需求整体确认标志 | +| `data.departDate` | String | 出发日期 | +| `data.endDate` | String | 结束日期 | +| `data.enrollDeadline` | String | 报名截止日 | +| `data.totalReceivable` | String | 整团应收合计 | +| `data.totalReceived` | String | 整团已收合计 | +| `data.remark` | String/null | 备注 | + +#### 请求示例 + +```http +GET /v3/admin/order/group-batch/2096412454643802114 +Authorization: Bearer +``` + +无请求体。 + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "groupBatchId": "2096412454643802114", + "productBatchId": "2052935476557328386", + "productId": "2044306857534636034", + "productName": "冻干粉发短信给", + "batchNo": "Q202610012052935476548939777", + "batchName": "10月1日长白山亲子团", + "batchLabel": null, + "batchStatus": "RESOURCE_PREPARING", + "batchStatusName": "资源准备中", + "minGroupPeople": 6, + "maxRooms": 4, + "maxParticipants": 10, + "enrolledPeople": 10, + "enrolledRooms": 4, + "remainRooms": 0, + "remainParticipants": 0, + "hotelReady": false, + "vehicleReady": false, + "guideReady": false, + "photographerReady": false, + "materialConfirmed": false, + "requirementConfirmed": false, + "departDate": "2026-10-01", + "endDate": "2026-10-03", + "enrollDeadline": "2026-09-25", + "totalReceivable": "48000.00", + "totalReceived": "36000.00", + "remark": null + } +} +``` + +#### 空数据 / 降级响应 + +详情接口不存在空数据响应。 + +#### 错误响应 + +```json +{ + "code": 589501, + "message": "团期不存在", + "success": false, + "data": null +} +``` + +业务失败错误码沿用 GroupBatchErrorCode(权限/不存在等)。 + +#### 业务边界 + +- 沿用团期详情权限校验;无权返回 589507。 +- `productId` 与其他字段同时返回,始终非空。 +- 团期不存在返回业务码 589501(HTTP 200)。 + +--- + +### 3. 团期看板列表 `GET /v3/admin/order/group-batch/board?productId=` + +**VO**: `GroupBatchBoardItemRespVO`(响应位置:`data[]`) + +#### 使用场景 + +打开团期看板时,按产品维度加载该产品下的所有班期(产品侧班期为基底,左连运营侧团期数据),每一行携带 productId,供「新增子订单」深链按行数据拼接预填参数。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| `productId` | Query | String | 是 | 正整数 ID | 按产品筛选(必填;缺参返回 400) | + +#### 出参 `Result>` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `data[].productBatchId` | String | product 侧班期 ID | +| `data[].productId` | String | **【新增】** 产品 ID(= 请求入参;命中/未命中/孤儿行均非空) | +| `data[].batchNo` | String | 班期编号 | +| `data[].batchName` | String | 班期名称 | +| `data[].departureDate` | String | 出发日期 | +| `data[].endDate` | String | 结束日期 | +| `data[].enrollmentDeadline` | String | 报名截止日 | +| `data[].maxRooms` | Integer | 房间容量 | +| `data[].maxParticipants` | Integer | 人数容量 | +| `data[].enrolledRooms` | Integer | 已报名房数 | +| `data[].enrolledPeople` | Integer | 已报名人数 | +| `data[].remainRooms` | Integer | 剩余房间数 | +| `data[].remainParticipants` | Integer | 剩余人数 | +| `data[].batchStatus` | String | 团期状态码 | +| `data[].batchStatusLabel` | String | 状态中文名 | +| `data[].hotelReady` | Boolean | 配房完成标志 | +| `data[].vehicleReady` | Boolean | 配车完成标志 | +| `data[].guideReady` | Boolean | 导游完成标志 | +| `data[].photographerReady` | Boolean | 摄影完成标志 | +| `data[].needsGuide` | Boolean | 是否需领队 | +| `data[].needsPhotographer` | Boolean | 是否需摄影 | +| `data[].orderCount` | Integer | 活跃子订单数 | +| `data[].groupBatchId` | String/null | 团期聚合主键(未成团时为 null) | +| `data[].productBatchRemoved` | Boolean | 是否孤儿行(product 侧已删除该班期) | +| `data[].contractSignedCount` | Integer | 合同已签子订单数 | +| `data[].insuranceInsuredCount` | Integer | 保险已出子订单数 | + +#### 请求示例 + +```http +GET /v3/admin/order/group-batch/board?productId=2044306857534636034 +Authorization: Bearer +``` + +无请求体。 + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": [ + { + "productBatchId": "2052935476557328386", + "productId": "2044306857534636034", + "batchNo": "Q202610012052935476548939777", + "batchName": "10月1日长白山亲子团", + "departureDate": "2026-10-01", + "endDate": "2026-10-03", + "enrollmentDeadline": "2026-09-25", + "maxRooms": 4, + "maxParticipants": 10, + "enrolledRooms": 2, + "enrolledPeople": 4, + "remainRooms": 2, + "remainParticipants": 6, + "batchStatus": "RECRUITING", + "batchStatusLabel": "招募中", + "hotelReady": false, + "vehicleReady": false, + "guideReady": false, + "photographerReady": false, + "needsGuide": true, + "needsPhotographer": false, + "orderCount": 2, + "groupBatchId": "2096412454643802114", + "productBatchRemoved": false, + "contractSignedCount": 1, + "insuranceInsuredCount": 1 + }, + { + "productBatchId": "2052935476557328387", + "productId": "2044306857534636034", + "batchNo": "Q202610022052935476548939778", + "batchName": "10月2日长白山亲子团", + "departureDate": "2026-10-02", + "endDate": "2026-10-04", + "enrollmentDeadline": "2026-09-26", + "maxRooms": 4, + "maxParticipants": 10, + "enrolledRooms": 0, + "enrolledPeople": 0, + "remainRooms": 4, + "remainParticipants": 10, + "batchStatus": "RECRUITING", + "batchStatusLabel": "招募中", + "hotelReady": false, + "vehicleReady": false, + "guideReady": false, + "photographerReady": false, + "needsGuide": true, + "needsPhotographer": false, + "orderCount": 0, + "groupBatchId": null, + "productBatchRemoved": false, + "contractSignedCount": 0, + "insuranceInsuredCount": 0 + } + ] +} +``` + +#### 空数据 / 降级响应 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": [] +} +``` + +#### 错误响应 + +缺参 productId: +```json +{ + "code": 400, + "message": "productId 参数缺失", + "success": false, + "data": null +} +``` + +--- + +#### 业务边界 + +- `productId` 必填,缺参返回 HTTP 200 + code 400;不要用 GET 参数默认值。 +- 返回结构按产品侧班期为基底(左连团期数据): + - **命中行**(班期有对应团期):团期状态/容量/ready 取订单侧值;batchStatus ≠ RECRUITING + - **未命中行**(班期无团期):batchStatus 固定 RECRUITING;groupBatchId = null;容量取产品侧 maxRooms/maxParticipants + - **孤儿行**(团期但班期已删):productBatchRemoved = true;仅 orderCount > 0 时出现 +- `productId` 在命中行、未命中行、孤儿行中均等于请求入参,始终非空。 +- 表合并按 productBatchId 左连 order_group_batch;无团期记录时新增一行(未命中)。 + +--- + +## 四、契约约束与正确调用方式 + +| 场景 | 正确做法 | +|------|---------| +| 新增子订单深链 | 按行数据拼接 `/order-v2/new?productId={productId}&productBatchId={productBatchId}&departureDate={departureDate}` | +| 向导内 GROUP 产品创单 | 检查产品类型,仅 GROUP 类型在 POST payload 中带 productBatchId;其他类型忽略 | +| 非 GROUP 产品创单 | 忽略 productBatchId 参数,后端根据 productId 与 departureDate 自行逻辑 | +| 孤儿行处理 | 若 productBatchRemoved = true,需向用户提示"班期已下架,不可创建子订单"或禁用按钮 | +| 未成团行处理 | groupBatchId = null 时无团期记录;前端可选择隐藏"团期信息"列或显示"待成团" | + +--- + +## 五、数据库行为 + +| 班期状态 | productBatchId | groupBatchId | batchStatus | 数据来源 | +|---------|----------------|-------------|------------|---------| +| 命中(有团期) | 产品侧 | 订单侧 | 订单侧 | order_group_batch 存在 | +| 未命中(无团期) | 产品侧 | null | RECRUITING | 无 order_group_batch 行 | +| 孤儿(班期删)| 产品侧 | 订单侧 | 订单侧 | order_group_batch 存在但班期无 | + +新增 productId 字段在 productBatchId 后(响应顺序一致)。 + +--- + +## 六、边界行为 + +- 业务失败可能仍为 HTTP 200,必须同时检查 `code`、`success` 和 `message`。 +- 看板接口 `productId` 缺参返回 400(HTTP 200);不要依赖前端参数校验。 +- 分页/列表接口分页参数超界时返回空列表(无 5XX)。 +- `productId` 在所有三个接口的所有行中均非空,无特殊情况返回 null。 +- Long 型 ID 在 JSON 字符串化后,前端若需数值运算应保持字符串存储。 + +--- + +## 六.6、修改前后对比 + +### 字段级对比 + +| 字段 | 改前 | 改后 | +|------|------|------| +| `productId`(分页项) | 无此字段 | 新增;产品 ID | +| `productId`(详情) | 无此字段 | 新增;产品 ID | +| `productId`(看板项) | 无此字段 | 新增;产品 ID | + +### 行为级对比 + +| 行为 | 改前 | 改后 | +|------|------|------| +| 新增子订单跳转 | 无法从响应直接拼深链参数 | 新增 productId,与 productBatchId/departureDate 配合直接拼链 | +| 向导内产品预填 | 依赖约定俗成或外部 context | 直接从深链 query string 传入 | +| GROUP 产品识别 | 向导内自行判别 | 向导根据产品类型自动识别是否需 productBatchId | + +--- + +## 六.7、影响评估 + +- **是否破坏向后兼容**: 否。新增字段对旧客户端透明。 +- **前端是否必须同步上线**: 否(从旧口径讲);但为完整支持团期看板「新增子订单」功能,前端应同步接入新增 productId 于深链拼接(仅一处改动)。 +- **前端 workaround 清理点**: + - 新增子订单按钮处,改用响应中的 productId 拼深链,而非硬编码产品 ID + - 向导内创建 GROUP 订单时,检查产品类型再决定是否传 productBatchId(无需前端重构,只需补一个类型判断) + +--- + +## 七、不影响范围 + +- **仅影响**: 管理后台团期看板的「新增子订单」功能入口 +- **零影响**: + - 团期创建、编辑、审批、资源配置等写接口 + - 团期与订单间的关联关系和业务流程 + - 产品侧班期相关接口和定义 + +--- + +## 八、测试环境已验证 + +- **单测**: 79 条测试用例绿✓(新增 productId 相关的 UT 已覆盖命中/未命中/孤儿行三路径) +- **ArchTest**: 45 条架构测试绿✓ +- **测试服网关实测**: 单测 + CR 通过;测试服网关实测见管理者补充 + +实测产品: `productId=2044306857534636034`(冻干粉发短信给),班期 `2026-10-01`(productBatchId=2052935476557328386),团期主键 `groupBatchId=2096412454643802114`,团期名 `batchName=10月1日长白山亲子团`。 + +--- + +## 九、相关历史 PR(功能演进) + +| PR | Issue | 说明 | 是否仍有效 | +|----|-------|------|------------| +| #7135 | - | GROUP 产品创单校验与重整(定义 productBatchId 入参) | ✅ 有效 | +| #7142 | - | 订单详情/列表/创单响应透出判团字段(groupBatchId/productBatchId/groupOrder)| ✅ 有效 | +| **本 PR #7157** | **#7143** | **团期看板 VO 补 productId(配合深链预填)** | ✅ 最新 | + +--- + +## 十、相关文档 + +- 关联 Issue: [wx/HL#7143](https://git.1814.love:8443/wx/HL/issues/7143) +- 关联 PR: [wx/HL#7157](https://git.1814.love:8443/wx/HL/pulls/7157) +- 相关工单: #7142(订单判团字段)、#7135(GROUP 产品规范) +- 前端缺陷: 新建订单向导漏传 productBatchId(另发前端 changelog) + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#7143](https://git.1814.love:8443/wx/HL/issues/7143) +- **PR**: [#7157](https://git.1814.love:8443/wx/HL/pulls/7157) +- **Merge commit**: [ce2809c15](https://git.1814.love:8443/wx/HL/commit/ce2809c15) + +### 联系人 + +- **后端负责人**: @wx diff --git a/changelogs-v2/2026-09/06_frontend_团期产品新建订单向导创单漏传productBatchId-前端缺陷-管理后台.md b/changelogs-v2/2026-09/06_frontend_团期产品新建订单向导创单漏传productBatchId-前端缺陷-管理后台.md new file mode 100644 index 00000000..25552eef --- /dev/null +++ b/changelogs-v2/2026-09/06_frontend_团期产品新建订单向导创单漏传productBatchId-前端缺陷-管理后台.md @@ -0,0 +1,249 @@ +--- +schema: "hl-changelog/v2" +ticket: "frontend" +title: "团期产品新建订单向导创单漏传 productBatchId" +consumer: "admin" +author: "wx(GIT)" +change_type: "前端缺陷" +backend_status: "not_required" +gateway_status: "not_required" +frontend_status: "pending" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "后端已随 #7135/#7159 加固并部署测试服 dev-v3、网关复测 15/15 通过(新增 581055/581056/581057、修复 581034 人数预查,合并提交 977be08e);前端可据此联调:在 order-v2/new 向导创单 payload 补 productBatchId(仅 GROUP 产品,且必须是该产品的班期),并修团期看板「新增子订单」入口带团期上下文。" +updated_at: "2026-09-06" +base: "dev-v3" +--- + +# 团期订单:新建订单向导对 GROUP 产品创单漏传 productBatchId(前端缺陷) + +## ⚠️ 关键变化 + +**现象**:团期(GROUP)产品在管理后台新建订单向导完成表单、点"确认创建"后报 HTTP 200 `code=581026 message='团期产品必须选择团期'`,无法创建。 + +**根因**:前端 `src/views/order-v2/new/index.vue` `handleCreate()` 组装的创单 payload 漏传 `productBatchId` 字段。而向导其实已在报价阶段成功拿到班期 ID(存于 `pricingContext.batchId`),报价后端响应 ¥5,850 正确,但创建时没把它放入请求体。 + +**结论**:前端补 `productBatchId` 是主修复。**后端已随 #7135/#7159 同步加固**(不再是「零改动」):现在仅 GROUP 产品可传 `productBatchId`,CORE/CUSTOM 传了会被拒(581056);班期必须属于所选产品(否则 581055);`tierSeq` 必须在产品配置内(否则 581057);人数超班期剩余名额会被拒(581034,此前因字段名漂移长期失效,#7159 修复)。因此前端务必**只对 GROUP 产品**传 `productBatchId`,且取自该产品价格日历的 `items[].batchId`。 + +--- + +## 一、背景 + +### 复现步骤 + +页面:管理后台 `192.168.100.219:9527/order-v2/new`(hl-ui 路由 `orderV2NewRoute`,`src/router/routes.js:166-175`,四步向导:选主题 → 选产品/档位 → 基本信息 → 确认创建)。 + +1. **Step 0 选主题**:「阿斯蒂芬撒点」(GROUP 产品线 `lineId=2044248925572919297`) +2. **Step 1 选产品**:「冻干粉发短信给」(`productId=2044306857534636034`,GROUP,`tierSeq=1` 档位「轻奢」) +3. **Step 2 基本信息**:出发日期 2026-10-01,成人 1 名、儿童 1 名 → 系统报价 ¥5,850.00(其中单房差 +¥500.00) +4. **Step 3 确认创建**:填客户姓名、手机号 → 点「确认创建订单」 +5. **结果**:Toast 弹窗「团期产品必须选择团期」,订单创建失败 + +### 调用链 + +1. hl-ui `src/views/order-v2/new/index.vue:405-450` `handleCreate()` 组装 payload → `createOrder(payload)` +2. `src/api/orderV2.js:79-81` `createOrder(data)` = `http.post('/v3/admin/order', data, V3)` +3. hl-gateway `application.yml:246-249` 路由 `/v3/admin/**` → `lb://hl-order-service-v3` +4. hl-order-service-v3 `OrderController.java:86-91` 接收 → `orderService.createOrder(req, ...)` +5. `OrderService.java:630` 直接 `ctx.setProductBatchId(req.getProductBatchId())`,无兜底反查 +6. `OrderService.java:635` `strategy.validate(ctx, productDetail)` +7. `GroupOrderStrategy.java:46-49` 校验失败:`if (ctx.getProductBatchId() == null) throw new BusinessException(OrderCoreErrorCode.GROUP_BATCH_REQUIRED)` +8. `OrderCoreErrorCode.java:102-103` 返回 `581026`「团期产品必须选择团期」 + +### 地面真相(测试库验证) + +| 属性 | 值 | +|------|-----| +| 主题(product_line) | 阿斯蒂芬撒点 `2044248925572919297` | +| 产品(product) | 冻干粉发短信给 `2044306857534636034`,`product_type=GROUP` | +| 档位(tier) | `tierSeq=1` 轻奢 | +| 班期(group_tour_batch) | `batch_id=2052935476557328386`,`batch_no=Q202610012052935476548939777`,`departure_date=2026-10-01`,`batch_status=ENROLLING`,`end_date=2026-10-03`,`adult_price=2925`,`child_price=2425`,`single_room_diff=500` | +| 预计金额 | 2925 + 2425 = 5,350,加单房差 500 = **5,850** ✓ (与前端报价一致,证明向导已成功拿到班期信息计价) | + +--- + +## 二、变更接口清单 + +| # | 接口名 | 方法 | 网关路径 | 前端函数 | 变更 | 说明 | +|---|--------|------|---------|---------|------|------| +| 1 | 创建订单 | POST | `/v3/admin/order` | `createOrder()` | **调用方式修正** | GROUP 产品**必传** `productBatchId`;CORE/CUSTOM 必须**不传** | +| 2 | 统一价格日历 | GET | `/admin/product/item/{productId}/pricing-calendar` | `getUnifiedPricingCalendar()` | 无变更 | GROUP 时返回班期列表,`items[].batchId` 即为所需班期 ID | +| 3 | 报价 | POST | `/admin/product/item/{productId}/quote` | `quoteProduct()` | 无变更 | GROUP 时现已传 `batchId`,保持即可 | + +--- + +## 三、接口详情 + +### 1. 创建订单 `POST /v3/admin/order` + +**VO**: `OrderCreateReqVO → Result` + +#### 使用场景 + +新建订单向导 Step 3 确认创建时调用,后端落库并触发团期聚合等副作用。 + +#### 入参 + +| 字段 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------| +| productId | String(Long 值) | ✓ | 正整数 | 产品 ID | +| tierSeq | Integer | ✓ | 1~N,且必须在产品已配档位内 | 档位序号;不在产品 tierPrices ∪ tiers 配置内 → 581057(#7135 起全类型校验;产品未配任何档位时不拦) | +| departureDate | String (yyyy-MM-dd) | ✓ | 不早于今天 | 出发日期;GROUP 下服务端会用班期权威出发日落库,但仍必填;CORE/CUSTOM 按字面值 | +| adultCount | Integer | ✓ | ≥1 | 成人数 | +| childCount | Integer | — | ≥0,默认 0 | 儿童数(6~12 岁) | +| youngChildCount | Integer | — | ≥0,默认 0 | 小童数(2~5 岁) | +| babyCount | Integer | — | ≥0,默认 0 | 婴儿数;GROUP 下服务端强制置为 0,不计入名额和价格 | +| customerName | String | ✓ | 非空,≤50 | 客户姓名 | +| customerPhone | String | ✓ | 格式 `^1[3-9]\d{9}$` | 手机号明文 | +| customerRemark | String | — | ≤500 | 订单备注 | +| createSource | String | — | ≤20,默认 CONSULTANT | 创建来源标记 | +| **productBatchId** | **String(Long 值)** | **GROUP ✓ / CORE、CUSTOM ✗** | 仅 GROUP 可传且必传;班期须属于本 productId | **GROUP 产品必传班期 ID**(来自价格日历 `items[].batchId`),**非 GROUP 产品禁止传递**(CORE/CUSTOM 带了 → 581056);班期 productId 须等于请求 productId(否则 581055);建议用字符串如 `"2052935476557328386"`(JSON Number 亦可,后端接受,但字符串防前端精度丢失) | +| roomCount | Integer | — | ≥1,默认 1 | 房间数;超过班期剩余房间数报 `581031` | +| tags | Array | — | — | 订单标签 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| code | Integer | 200 = 成功,否则为业务错误码 | +| data.id | String | 订单 ID(雪花 Long)| +| data.orderNo | String | 订单编号(HL+时间+序号,如 HL20260906093733163) | +| data.orderStatus | String | 订单状态(创建初态 = PENDING_PAY) | +| data.totalAmount | String | 订单总金额,格式 decimal(12,2) | +| data.depositAmount | String | 订金额 | +| data.departureDate | String | 出发日期(yyyy-MM-dd);GROUP 以班期为准,回显班期出发日 | +| data.returnDate | String | 归程日期(yyyy-MM-dd);根据 tripDays = endDate - departureDate + 1 推算 | +| data.groupBatchName | String/null | 团批次名称(当前实现为 null) | +| data.tierName | String/null | 档位名称(#7135 起 GROUP 也回显来自 tiers 配置的档位名;产品未配档位时为 null) | +| data.consultantId | String | 发单定制师 ID(创建时落 1001) | + +#### 请求示例(GROUP 产品,正例) + +```json +{ + "productId": "2044306857534636034", + "tierSeq": 1, + "departureDate": "2026-10-01", + "adultCount": 1, + "childCount": 1, + "youngChildCount": 0, + "babyCount": 0, + "customerName": "张三", + "customerPhone": "13800009601", + "createSource": "CONSULTANT", + "customerRemark": "团期测试订单", + "productBatchId": "2052935476557328386", + "roomCount": 1 +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "id": "2096412454488612866", + "orderNo": "HL20260906093733163", + "orderStatus": "PENDING_PAY", + "totalAmount": "5850.00", + "depositAmount": "1000.00", + "departureDate": "2026-10-01", + "returnDate": "2026-10-03", + "groupBatchName": null, + "tierName": null, + "consultantId": "1001" + }, + "success": true +} +``` + +#### 错误响应 + +| code | message | 触发条件 | +|------|---------|---------| +| 581026 | 团期产品必须选择团期 | productBatchId 为 null 且 productType = GROUP | +| 581027 | 团期信息获取失败 | Feign 调班期服务异常 | +| 581028 | 团期状态不允许报名 | 班期状态不在可订范围 | +| 581029 | 团期已过报名截止日 | 当前日期 > enrollment_deadline | +| 581031 | 团期剩余房间不足 | roomCount > 班期剩余房间数 | +| 581034 | 团期剩余名额不足 | 成人+儿童+小童 > 班期剩余名额(#7159 修复:此前字段名漂移致此校验长期失效;人数不限的班期不拦) | +| 581055 | 所选团期不属于该产品 | 班期 productId ≠ 请求 productId(跨产品串号;#7135 新增) | +| 581056 | 非团期产品不能指定团期 | CORE/CUSTOM 请求带了 productBatchId(#7135 新增) | +| 581057 | 所选档位不存在 | tierSeq 不在产品 tierPrices ∪ tiers 配置内(#7135 新增;产品未配档位时不拦) | + +#### 业务边界 + +- 后端已校验 tierSeq 存在性(581057)与班期归属产品(581055);GROUP 出发日/返团日以班期为准回显(#7135) +- babyCount 强制 0,不参与计价 +- returnDate 按班期 tripDays 计算 +- 创建后触发 staff 分配、group_batch 懒建 + +--- + +### 2. 统一价格日历 `GET /admin/product/item/{productId}/pricing-calendar` + +GROUP 时返回班期列表,items[] 每条含: +- date、batchId(JSON Number,**前端必须 String() 转换**) +- batchNo、batchName、batchStatus、endDate、enrollmentDeadline +- maxRooms、bookedRooms、remainParticipants(null 表不限) +- 价格字段、sellable(仅 ENROLLING 为 true) + +--- + +### 3. 报价 `POST /admin/product/item/{productId}/quote` + +GROUP 时入参需 batchId,出参 grandTotal、singleRoomSurcharge 等。 + +--- + +## 四、前端修复要点与自测清单 + +### 修复要点 + +1. `index.vue handleCreate()`:GROUP 时 `payload.productBatchId = String(pricingContext.batchId)` +2. Step3 确认前二次校验 pricingContext 非空且与当前表单一致 +3. Step3 展示 batchNo/batchName +4. 团期看板 onAddSub() 带深链 ?productBatchId&departureDate + +### 自测清单 + +- ✓ GROUP 选日期 → 报价 → 创建成功 +- ✓ CORE 创建不含 productBatchId +- ✓ 日期未选时按钮拦截 + +--- + +## 五、验证证据 + +### 场景矩阵(测试服 2026-09-06) + +14 场景通过,1 场景失败(S8),10 单已清理。 + +### DB 落库 + +orderNo HL20260906093733163,product_batch_id=2052935476557328386 + +--- + +## 六、影响与不影响范围 + +**不影响**: +- 正常 CORE/CUSTOM 创单(不带 productBatchId):完全不变 +- 订单读接口(列表 / 详情 / 编辑):不变 +- 前端改动只涉及 `order-v2/new` 向导创单 payload 与团期看板「新增子订单」入口 + +**受后端加固影响(前端需知晓)**: +- 小程序端 `POST /v3/mp/order` 与管理端共用内核,581055/581056/581057/581034 同样生效,但小程序请求契约不变(`groupBatchId` 语义不变) +- CORE/CUSTOM 若误带 productBatchId:此前静默落库,现在直接 581056 拒单 + +--- + +## 七、关联 + +- 前端向导既有工单 #7083(已合);后端判团字段透出 #7142、看板 productId 深链 #7143(均已合 dev-v3) +- 后端加固 #7135(收紧 productBatchId 校验:581055/581056/581057、日期按班期)+ #7159(人数预查改读 remainingParticipants 使 581034 生效),随 PR #7169 合入 dev-v3 +- 证据:gateway-verify.txt、gateway-matrix.txt \ No newline at end of file