# 新建订单「出发日期」选择器需限制为可售日期（前端待修） — 前端待修 — 管理后台

> 变更类型：🐛 前端 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 <adminToken>"
```

实测返回（节选，已上线测试服）：

```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` 区分渲染。
- 小程序端下单日历亦是同口径（按可售日置灰），管理后台对齐即可。
