# 新建订单「出发日期」选择器需限制为可售日期(前端待修) — 前端待修 — 管理后台 > 变更类型:🐛 前端 UX 缺陷(**无后端代码变更、零 DDL**;后端所需接口已存在且测试服可用,本文档给出对接方式) > 端类型:管理后台(新建订单向导 → 第 3 步「基本信息」→「出发日期」选择器) > 日期:2026-06-18 > 服务:hl-product-service-v2(价格日历查询接口,已上线);hl-order-service-v3(创单服务端兜底,已上线) > 责任端:前端 hl-ui(mmg) --- ## ⚠️ 关键说明 **现象**:新建订单第 3 步的「出发日期」选择器是一个**不受限的普通日期控件**,任意日期都可选(截图里日历停在 `2026 年 11 月` 且全部可点)。 **应有行为**:只允许选择该产品 + 该档位下的**可售日期**(与产品卡「最近可出发」、小程序端下单日历口径一致)。 **根因 = 前端未把日期选择器与已有的「价格日历」接口打通**,不是后端缺接口: 1. **限制可选范围所需的接口已存在且测试服可用**:`GET /admin/product/item/{productId}/pricing-calendar`,逐日返回 `sellable`(是否可售,后端算好)。前端在第 3 步已持有 `productId`(第 2 步选品)+ `tierSeq`(第 2 步选档),直接调它把非可售日期置灰即可(详见 §2)。 2. **后端已在服务端兜底**:即便选了非可售日期提交,创单也会被拦下、不会落库(详见 §3)。所以这是**纯前端的「提前拦截 / 体验」问题**——当前用户能选到注定失败的日期,提交后才报错,且报错文案误导(见 §3)。 > 实测样本(测试服 9443 + 真实 admin token):产品「游牧的森林-短途版」(productId=`2045345825172639746`,CORE 类型) **可售日仅 `2026-06-18`~`2026-06-30` 共 13 天**;其余 78 天(含整个 11 月)`sellable=false`。截图里 11 月全部可选属错误,应整月禁选。 --- ## 1. 复现 1. 新建订单 → 选主题「游牧的森林」→ 选产品「游牧的森林-短途版」→ 选档位「轻奢」。 2. 第 3 步「基本信息」点「出发日期」,日历弹出。 3. 现状:所有日期都能点(截图停在 2026-11,全月可选)。 4. 期望:只有 `2026-06-18`~`2026-06-30` 可点,其余置灰;最好默认定位到「最近可出发」所在月(2026-06)而不是空月份。 --- ## 2. 修复方式:用 `pricing-calendar` 给选择器置灰 **`GET /admin/product/item/{productId}/pricing-calendar`** | 参数 | 位置 | 必填 | 说明 | |---|---|---|---| | productId | path | 是 | 第 2 步选中的产品 ID | | month | query | 否 | `yyyy-MM`;不传返回全部已配日历 | | tierSeq | query | 否 | 档位序号,仅 CORE/CUSTOM 生效;**传第 2 步选中的档位**(不同档位可售日可能不同) | 响应 `data`:`{ productType, items[] }`,`items[]` 子项关键字段: | 字段 | 类型 | 说明 | |---|---|---| | date | LocalDate(yyyy-MM-dd) | 日期 | | **sellable** | **Boolean** | **是否可售(置灰判据:`sellable !== true` 即禁选)** | | adultPrice | String | 成人价(金额=带引号字符串) | | childPrice / infantPrice / toddlerDiscount | String | 各人群价/优惠 | | stock / sold | Integer | 库存上限 / 已售(stock=null 表示不限) | | tierSeq / priceType | Integer / String | 档位序号 / 价格类型 | **前端对接建议**: 1. 进入第 3 步(或用户在日历翻月)时,按 `productId` + `tierSeq` 拉日历(可不传 month 一次性取全部,数据量小)。 2. 用 `items` 里 `sellable === true` 的 `date` 构造「可选日期集合」,其余日期一律 disabled;列表里没有的日期也按不可选处理。 3. 默认打开月份用产品卡已有的「最近可出发」(`nextSaleDate`) 所在月,避免落到空月份。 4. 用户在第 2 步改了档位再回到第 3 步时,需按新 `tierSeq` 重新拉取。 ```bash # 拉「游牧的森林-短途版」轻奢档(tierSeq=1)的可售日历 curl -k "https://api.test.1814.love:9443/admin/product/item/2045345825172639746/pricing-calendar?tierSeq=1" \ -H "Authorization: Bearer " ``` 实测返回(节选,已上线测试服): ```json { "code": 200, "success": true, "data": { "productType": "CORE", "items": [ { "date": "2026-06-18", "adultPrice": "3.00", "childPrice": "3.00", "sellable": true, "tierSeq": 1, "priceType": "NORMAL", "stock": null, "sold": 0 }, { "date": "2026-06-30", "adultPrice": "3.00", "sellable": true, "tierSeq": 1 }, { "date": "2026-11-15", "sellable": false, "tierSeq": 1 } ] } } ``` > 本例中 `sellable=true` 仅 13 天(`2026-06-18`~`2026-06-30`),其余(含 11 月全月)`sellable=false`。 --- ## 3. 后端服务端兜底(已生效,前端可放心) 创单接口 `POST /v3/admin/order` 已对出发日期做服务端校验,**非可售日期 / 过去日期都进不去**(实测,均未落库): | 提交的出发日期 | 结果 | code | message | |---|---|---|---| | `2026-06-20`(可售) | 通过报价、正常创单 | 200 | — | | `2026-11-15`(不可售) | **被拦截**,不落库 | **581041** | `所选出发日期不可售或未配置价格,请重新选择出发日期` | | `2026-06-01`(早于今天) | 被拦截,不落库 | 581011 | `出发日期不能早于今天` | > ✅ 已优化(PR #4008,已合 dev-v3 + 部署测试服双实例实测):非可售日期之前误报 `581014「拉取产品信息失败,请稍后重试」`(像系统故障),现已改为 `581041「所选出发日期不可售或未配置价格,请重新选择出发日期」`,前端可直接透传该 message 给用户。`581014` 现仅表示真正的产品服务不可达。前端按 §2 限制好选择器后,正常路径不会触发 581041,它是用户绕过限制时的兜底提示。 --- ## 4. 实测确认 - 价格日历接口 `GET /admin/product/item/2045345825172639746/pricing-calendar?tierSeq=1`:`200`,`data.items` 共 91 条,`sellable=true` 13 条(`2026-06-18`~`2026-06-30`)✓ - 创单非可售日期 `2026-11-15`:被拦截 `581041「所选出发日期不可售或未配置价格…」`(PR #4008 优化后),无新增订单 ✓ - 创单过去日期 `2026-06-01`:被拦截 `581011` ✓ - 均经测试服网关 9443 + 真实 admin token 验证。 --- ## 备注 - 本文为前端缺陷通知 + 现有接口对接说明,无后端代码变更、零 DDL、无新增/改动接口。 - `pricing-calendar` 接口为产品管理「价格日历」长期已上线端点,CORE/CUSTOM 走 `product_price_calendar` 逐日价、GROUP 走 `group_tour_batch` 班期(按出发日),前端按 `data.productType` 区分渲染。 - 小程序端下单日历亦是同口径(按可售日置灰),管理后台对齐即可。