--- schema: hl-changelog/v2 ticket: "8739" title: "小程序团期下单撞上改期联动时最多等 5 秒,超时返回 100503,不再误报请勿重复提交" consumer: mp author: wx(GIT) change_type: 修改接口 backend_status: deployed gateway_status: not_required frontend_status: pending frontend_owner: "" frontend_ref: "" target_release: "" verified_at: "" status_note: "" updated_at: "2026-10-03" base: dev-v3 --- # 小程序团期下单撞上改期联动时最多等 5 秒,超时返回 100503,不再误报请勿重复提交 ## ⚠️ 关键变化 - 场景:`POST /mp/order/create` 带 `groupBatchId` 下团期单,而管理端此刻正在给该团期「改出发日」并联动订单(#8666 引入的联动)。 - 下单会先等联动结束,最多等 5 秒。 - 5 秒内联动结束:按改后的日期正常成单,响应比平时慢几秒。 - 5 秒后联动仍未结束:返回 `100503`「资源被占用,请稍后重试」,不成单,可以直接重提。 - 改前(#8666 上线后、本次之前),同一场景下小程序约 3.5 秒就收到 `100502`「请勿重复提交订单」。测试服实测: - 联动在 5 秒内结束时,后台其实**已经成单**; - 联动占用超过 5 秒时,**没有成单**。 也就是说,这条路径上的 `100502` 既可能是「成了」,也可能是「没成」。本次起这条路径不再返回 `100502`。 - 小程序服务等订单服务的上限由 2 秒放宽到 10 秒,超时后也不再自动重发下单请求。 - 超过 10 秒仍无结果时,返回 `500`「服务暂时不可用,请稍后重试」。 - 此时订单服务端的那次创单不会因此中止,订单**可能已经生成**。 - 不带 `groupBatchId` 的下单不等联动、不会多等。10 秒上限和「不自动重发」对它同样适用。 ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 创建订单 | POST | `/mp/order/create` | 错误响应语义变更 | 团期下单撞上改期联动时最多等 5s,超时返 100503,该路径不再返 100502;服务端超过 10s 返 500,不再自动重发 | ## 三、接口详情 ### 1. 创建订单 `POST /mp/order/create` **VO**: `MpCreateOrderRequest → MpOrderDetailVO` #### 使用场景 用户在产品详情页选好档位、人数并填好联系人后提交下单。 - 本次只改变下单失败和变慢时返回什么。 - 对带 `groupBatchId` 的团期单影响最大:该团期正被管理端改出发日时,下单会先等联动结束。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | productId | Body | String | 是 | 不能为空;必须是数字字符串,否则返回 600002「产品ID格式错误」 | 产品 ID(对外收字符串,防止 JS 精度丢失) | | groupBatchId | Body | String | 否 | GROUP 产品传;null 或空串视为未指定;非数字返回 600003「团期ID格式错误」;≤0 视为未指定 | 团期 ID。有效时下单与该团期的改期联动互斥 | | departureDate | Body | LocalDate | 否 | `yyyy-MM-dd` | 出发日期 | | adultCount | Body | Integer | 否 | ≥1,缺省 1 | 成人数 | | childCount | Body | Integer | 否 | ≥0,缺省 0 | 儿童数 | | youngChildCount | Body | Integer | 否 | ≥0,缺省 0 | 小童数 | | babyCount | Body | Integer | 否 | ≥0,缺省 0 | 幼童数 | | childNeedBed | Body | Boolean | 否 | 缺省 false | 儿童是否需要床位 | | tierSeq | Body | Integer | 是 | ≥1 | 档位序号 | | roomCount | Body | Integer | 否 | 传则 ≥1 | 房间数 | | sharerOpenid | Body | String | 否 | ≤64;可传空串;非空时只能含字母、数字、下划线、短横线 | 分享人 OpenID | | customizerId | Body | Long | 否 | ≥1 | 分享人定制师 ID | | contactName | Body | String | 是 | 非空白,≤50 | 联系人姓名 | | contactPhone | Body | String | 是 | 非空白,≤20 | 联系人电话 | | remark | Body | String | 否 | ≤500 | 订单备注 | 本次请求字段零变更。上表为完整字段表,与改前一致。 #### 出参 | 字段 | 类型 | 说明 | |------|------|------| | orderId | String | 订单 ID。后端为 Long,超出 JS 安全整数范围时序列化为字符串,一律按字符串处理 | | orderNo | String | 订单编号,格式为 `HL` + 年月日时分秒 + 3 位毫秒 | | groupCode | String | 4 位团号,订金支付成功时生成;未生成时该字段不出现 | | groupBatchId | String | 团期 ID(序列化同 orderId),只有团期订单才出现 | | productId | String | 产品 ID(序列化同 orderId) | | departureDate | LocalDate | 出发日期 | | returnDate | LocalDate | 返程日期 | - 响应对象带 `@JsonInclude(NON_NULL)`:值为空的字段(如订金支付前的 `groupCode`)直接不出现,不会以 `null` 返回。前端按「字段缺失」判空。 - 本次响应字段零变更。出参与 `GET /mp/order/{orderId}` 详情接口同构;上表只列标识字段,完整字段见既有详情接口文档。 #### 请求示例 ```json { "productId": "1900123456789000001", "groupBatchId": "1900123456789000002", "departureDate": "2027-01-27", "adultCount": 2, "childCount": 1, "tierSeq": 1, "contactName": "林晓梅", "contactPhone": "13900001234", "remark": "老人同行,希望安排低楼层" } ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": { "orderId": "1900123456789000003", "orderNo": "HL20270127143025001", "groupBatchId": "1900123456789000002", "productId": "1900123456789000001", "departureDate": "2027-01-27", "returnDate": "2027-02-01" }, "traceId": null, "success": true } ``` #### 空数据 / 降级响应 本接口没有空数据形态。 - 订单服务等待超过 10 秒,或暂时不可达时,返回下方错误响应里的 5xx(文案「服务暂时不可用,请稍后重试」),不会返回半成品数据。 - 上述错误响应的 HTTP 状态码都是 200,结果看 `code`。 #### 错误响应 团期下单撞上改期联动,等满 5 秒联动仍未结束(不成单,可以直接重提): ```json { "code": 100503, "message": "资源被占用,请稍后重试", "data": null, "traceId": null, "success": false } ``` 订单服务超过 10 秒没给出结果(订单可能已经生成,先查订单列表再决定是否重提): ```json { "code": 500, "message": "服务暂时不可用,请稍后重试", "data": null, "traceId": null, "success": false } ``` 真的重复提交(上一次下单请求已被受理或仍在处理,不要自动重提): ```json { "code": 100502, "message": "请勿重复提交订单", "data": null, "traceId": null, "success": false } ``` #### 业务边界 - **什么时候会等**:只有 `groupBatchId` 解析为正数时,下单才与该团期的改期联动互斥。不带团期的下单不等。 - **等多久**:最多 5 秒,后端常量,前端不可调整。测试服实测一次改期联动占用 41–688 毫秒(12 次),所以多数情况是「慢一点但成单」,`100503` 只在联动占用超过 5 秒时出现。 - **`100503`**:不成单,因为等锁失败时创单业务还没开始执行。可以直接重提,不需要任何清理。 - 重提不会撞 `100502`。小程序层的防重窗口 5 秒,`100503` 返回时已过;订单服务层的防重键在 `100503` 时随失败释放。 - **`500` 等 5xx(等待超过 10 秒或订单服务不可达)**:小程序服务不再自动重发,但订单服务端已开始的那次创单不会中止,**可能已经成单**。 - 此时直接重提可能产生两张订单:订单服务层同用户同产品的防重窗口是 10 秒,从原请求开始计时,到 `500` 返回时已基本过期。(按代码机制推导,测试服未构造过超过 10 秒的场景。) - 建议提示「网络繁忙,请到我的订单查看」,先刷新订单列表,确认没有新订单再让用户重提。 - **`100502` 现在只表示真的重复提交**,有两层窗口: - 小程序层:同一用户 5 秒内第二次下单,不分产品。成功下单后 5 秒内再下也算。 - 订单服务层:同一用户同一产品 10 秒内第二次下单。 收到时说明前一次请求已被受理或仍在处理,引导用户到订单列表查看,不要自动重提。 - **`100501`「下单过于频繁,请稍后再试」**:订单服务层按用户限流,每个用户 60 秒内最多 5 次下单请求。既有行为,未变。 ## 四、契约约束与正确调用方式 1. 按 `code` 区分错误,不要按 `message` 判断。`100502`、`100503` 和 5xx 的文案相近,处理方式完全不同。 2. 下单请求的前端超时不要短于 15 秒:服务端最长 10 秒给出结果,另有网关与网络开销。前端先放弃时,用户看不到 `100503`,后台却可能已成单。 3. 提交期间禁用提交按钮,直到收到响应。团期下单撞上改期联动时,正常响应也可能慢约 5 秒。 4. 收到 `100503` 可以直接重提原请求。 5. 收到 5xx(`code` ≥ 500)先刷新订单列表,确认没成单再重提。 6. 收到 `100502` 不要自动重提,引导用户到订单列表查看。 ## 五、数据库行为 | 结果 | 数据库写入 | |------|------------| | `100503` | 零写入:等锁失败发生在创单业务开始之前 | | `100502` / `100501` | 零写入:在进入创单业务前就被拦截 | | 5xx(等待超过 10 秒) | 订单服务端那次创单照常提交或失败,与小程序有没有收到结果无关 | | 成功 | 写入与改前完全一致;本次不涉及任何表结构变更 | ## 六、边界行为 - 两个时限(等锁 5 秒、小程序服务调订单服务超时 10 秒)都是服务端固定值,请求参数无法调整。 - 本接口与管理端创单复用同一套创单内核,锁判断完全一致。 - 管理端用 `productBatchId` 数字。 - 小程序用 `groupBatchId` 字符串,两者是同一个 ID。 ## 六.6、修改前后对比 ### 字段级对比 本接口请求、响应字段均**零变更**,变的只是失败和变慢时的返回。 ### 行为级对比 | 场景 | 改前(#8666 上线后、本次之前) | 改后 | |------|------|------| | 团期下单,没撞上改期联动 | 正常成单 | 正常成单,无差异 | | 团期下单,撞上改期联动且联动在 5 秒内结束 | 约 3.5 秒返回 `100502`,后台其实已成单 | 等联动结束后正常成单,响应最多慢约 5 秒 | | 团期下单,联动占用超过 5 秒 | 约 3.5 秒返回 `100502`,没有成单 | 约 5 秒返回 `100503`,没有成单,可直接重提 | | 任意下单,订单服务处理超过 2 秒 | 小程序服务约 2 秒超时后自动重发同一请求,用户可能收到 `100502` 而后台已成单 | 等到 10 秒;超过 10 秒返回 `500`,不重发 | 在 #8666 之前,下单完全不与改期联动互斥。撞上改期时可能按旧出发日成单,且日期与团期永久错位、无人察觉。#8666 堵住了这个窗口,本次修正的是它在小程序链路上的错误码表现。 ## 六.7、影响评估 - **是否破坏向后兼容**:否。请求、响应字段零变更,正常成单路径不变。 - **前端是否必须同步上线**:否。未识别 `100503` 时展示原始 `message` 不会白屏。但若前端下单请求的超时短于 15 秒,需要调整,见第四节第 2 条。 - **前端 workaround 清理点**:无。 ## 七、不影响范围 - 小程序其他订单接口(列表、详情、取消、改单等)调订单服务的等待上限与重试策略**未变**。 - 请求、响应字段零变更。 - 网关路由零变更,无数据库结构变更。 ## 八、测试环境已验证 2026-10-03 测试服部署 hl-mp-service(dev-v3 `f4e45c2f1`)后实测。做法:用测试 C 端用户直连小程序服务,在自建团期上人为占住改期联动锁。 | 场景 | 响应 | 耗时 | 成单 | 订单服务收到的请求 | |------|------|------|------|------| | 联动占用约 3.5 秒 | 成功,`data` 为完整订单详情 | 约 3.5 秒 | 1 单 | 1 次 | | 联动占用约 6 秒 | `100503`「资源被占用,请稍后重试」 | 约 5 秒 | 0 | 1 次 | | 上一行锁释放后间隔 ≥5 秒重提 | 成功 | 正常 | 1 单 | 1 次 | | 收到 `100503` 后 0.3 秒内立即重提(锁剩余不足 1 秒) | 未返回 `100502`,等联动结束后成功 | 未单独计时 | 1 单 | 2 次,即两次真实提交,无自动重发 | | 不占锁的常规下单 | 成功 | 正常 | 1 单 | 1 次 | - 常规下单的响应 `data` 与 `GET /mp/order/{orderId}` 的 `data` 做了字段集比对:61 个字段路径,零差异,二者同为 `MpOrderDetailVO`。 - 改前基线是部署前用同一夹具、同一方法测的: - 联动占用约 3.5 秒:约 3.5 秒返回 `100502`,但后台已成单,订单服务收到 2 次。 - 联动占用约 6 秒:同样返回 `100502`,未成单。 - 本轮未构造订单服务超过 10 秒才返回的场景,所以 `500` 之后可能已成单这一点仍是按代码推导,见业务边界。 ## 十、相关文档 - 管理端同源改动:`changelogs-v2/2026-10/03_8666_产品班期改出发日联动团期与子单日期并新增改期拒绝码-修改接口-管理后台.md`(#8666)。改期联动本身的事件与拒绝码以该条为准。 - 内部转发路径:`POST /mp/order/create`(hl-mp-service)→ `POST /v3/internal/mp/order/create`(order-v3)。`/v3/internal/**` 只供服务间调用,不经网关,前端不可达。 - `100503` 是全仓统一的锁竞争可重试错误码。 ## 关联 / 联系人 ### 链接 - **Issue**: [#8739](https://git.1814.love/wx/HL/issues/8739)(本次)、[#8666](https://git.1814.love/wx/HL/issues/8666)(引入改期联动锁) - **PR**: [#8740](https://git.1814.love/wx/HL/pulls/8740)、[#8738](https://git.1814.love/wx/HL/pulls/8738) - **Merge commit**: `f4e45c2f147eb5e0deec4dbd920e6c82c496ec7b`(#8740)、`fad5d7814b2d1fb940d740457dbed754e46d03f9`(#8738) ### 联系人 - **后端负责人**: @wx