From 82e2f1713fcd3d83b3521f9607f3f8ad7d08065a Mon Sep 17 00:00:00 2001 From: wx Date: Thu, 30 Apr 2026 23:42:39 +0800 Subject: [PATCH] =?UTF-8?q?2026-04-30:=20=E4=B8=8A=E6=9E=B6=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E8=B7=B3=E8=BF=87=E6=9C=80=E5=90=8E=E4=B8=80=E5=A4=A9?= =?UTF-8?q?=E9=85=92=E5=BA=97=20(PR=20#1587)=20+=20=E5=89=8D=E7=AB=AF=20BU?= =?UTF-8?q?G=20changelog=20=E5=8D=87=E7=BA=A7=E6=8E=92=E6=9F=A5=E6=8C=87?= =?UTF-8?q?=E5=BC=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix(product): 上架/完成设计校验跳过最后一天酒店(返程日豁免) (PR #1587, Closes #1586) - update: 前端 includedFees 缺 feeType BUG changelog 加 yst 第二次反馈截图 + 后端 round-trip 实测证据 + JS 完整打印调试指引 --- ...ix_product_validate-skip-last-day-hotel.md | 81 +++++++++++++++++++ ..._cost-desc-panel-missing-youwan-feetype.md | 67 +++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 changelogs/2026-04/30_fix_product_validate-skip-last-day-hotel.md diff --git a/changelogs/2026-04/30_fix_product_validate-skip-last-day-hotel.md b/changelogs/2026-04/30_fix_product_validate-skip-last-day-hotel.md new file mode 100644 index 0000000..2008ae5 --- /dev/null +++ b/changelogs/2026-04/30_fix_product_validate-skip-last-day-hotel.md @@ -0,0 +1,81 @@ +# 修复: 上架/完成设计校验跳过最后一天酒店(返程日豁免) + +**类型**: 后端 BUG 修复 +**关联**: 工单 #1586 / PR #1587 +**日期**: 2026-04-30 +**前端处理者**: yst / mmg +**影响范围**: 管理后台「完成设计」「提交审核 / 直接上架」按钮的预检逻辑 + +--- + +## 现象 + +用户反馈:产品上架/完成设计预检时,如果**最后一天**没在 `product_day_hotel` 配酒店,就被「Day N 住宿安排为空」拦截无法上架。但**最后一天通常是返程日不需要住宿**,这个校验是错的。 + +用户原文:「最后一天行程提示也得有住宿安排,实际不需要」 + +## 根因 + +`hl-product-service-v2` `ProductValidationService.validateItineraryCompleteness()` 对每一天循环硬要求 `product_day_hotel` 非空,**没有跳过最后一天**。 + +## 修复 + +判定最后一天豁免酒店校验: + +```java +int totalDays = days.size(); +for (int i = 0; i < totalDays; i++) { + ProductItineraryDayDO day = days.get(i); + // ... 描述/集合地/解散地/活动校验保持不变 ... + + // 最后一天通常是返程,不强制要求配酒店(2026-04-30 用户反馈) + boolean isLastDay = (day.getDayNumber() != null) + ? day.getDayNumber() == totalDays + : i == totalDays - 1; + if (!isLastDay) { + if (dayHotels.isEmpty()) issues.add(dayLabel + " 住宿安排为空"); + } +} +``` + +判定规则: +- 优先 `day.getDayNumber() == totalDays` +- `dayNumber` 为 null 时回退索引 `i == totalDays - 1` + +## 行为变化 + +| tripDays | 最后一天 | 行为 | +|---|---|---| +| 1 | i=0 是唯一一天即最后一天 | 单天产品没酒店通过 | +| 2 | Day1 校验 / Day2 豁免 | OK | +| 3 | Day1/Day2 校验 / Day3 豁免 | OK | +| dayNumber=null | 列表最后一项兜底 | 跳过 | + +## 影响接口 + +| 方法 | 路径 | 改动 | +|------|------|------| +| POST | `/admin/product/item/{id}/complete-design` | 最后一天没酒店不再返 400 | +| POST | `/admin/product/item/{id}/toggle-publish` (SUBMIT_PUBLISH/DIRECT_PUBLISH) | 同上 | +| PUT | `/admin/product/item/{id}/supplement` | 不变(草稿允许任意) | + +请求/响应签名不变,只是错误条件减少。 + +## 前端要做的改动 + +**0 改动**。如果前端之前对「Day N 住宿安排为空」错误做过 UX 引导(比如跳到行程编排页面),保留即可,不会被误触发。 + +## 测试覆盖 + +新增 4 单测: +- `validateItinerary_lastDayNoHotel_shouldPass` — 3 天 day3 没酒店,通过 +- `validateItinerary_middleDayNoHotel_shouldFail` — 3 天 day2 没酒店,报错;day3 不计入 +- `validateItinerary_singleDay_noHotel_shouldPass` — 1 天产品没酒店通过 +- `validateItinerary_dayNumberNull_useListIndexFallback` — dayNumber=null 兜底 + +`mvn test`: ProductValidationServiceTest **53/53 全绿**。 + +## 部署状态 + +- ✅ 测试服 hl-product-service-v2 双实例部署完成(2026-04-30 23:40) +- 正式环境暂未发布(等 dev → main release PR) diff --git a/changelogs/2026-04/30_frontend_cost-desc-panel-missing-youwan-feetype.md b/changelogs/2026-04/30_frontend_cost-desc-panel-missing-youwan-feetype.md index fdc2c3a..fcdd4c9 100644 --- a/changelogs/2026-04/30_frontend_cost-desc-panel-missing-youwan-feetype.md +++ b/changelogs/2026-04/30_frontend_cost-desc-panel-missing-youwan-feetype.md @@ -11,6 +11,73 @@ **日期**: 2026-04-30 **影响范围**: 管理后台「产品编辑 → 补充信息 → 费用说明」页面 — 自动生成的费用包含列表 +## ⚠️ 用户 yst 第二次反馈(2026-04-30 20:43) + +yst 截图: +``` +includedFees: [{name: "扎赉诺尔博物馆,巴尔虎蒙古部落,海拉尔国家森林公园", description: null}, ...] +0: {name: "扎赉诺尔博物馆,巴尔虎蒙古部落,海拉尔国家森林公园", description: null} +1: {name: "海拉尔首旅京伦酒店, 额尔古纳豪星酒店", description: null} +``` + +→ "返回的费用包含不对 自动的没有类型" + +## 后端实测(管理者再次 round-trip 确认) + +`GET /admin/product/item/2045018534152478721`(测试服) 实测响应: + +```jsonc +includedFees: [ + { + "id": "2049778058755268609", + "productId": "2045018534152478721", + "feeCategory": "INCLUDED", + "feeType": "门票", // ← 后端有这个字段! + "source": "AUTO", + "name": "黑山头", + "description": null, + ... + }, ... // 17 条单条记录,全部含 feeType +] +``` + +`PUT /admin/product/item/{id}/supplement` 写入 `[{feeType:"景区",name:"X1,X2",sortOrder:1}]` → +`GET /admin/product/item/{id}` 读出来 `includedFees=[{feeType:"景区",name:"X1,X2",...}]` + +**字符串完全一致透传**,后端 round-trip 验证齐全。 + +## 前端排查焦点 + +1. yst console 截图只看到 2 项(0/1),**第三项「游玩项目」消失** — 跟首次反馈一致 +2. 每项 keys 只显示 `name + description`,但后端实际返 16 个字段(`id/productId/feeCategory/feeType/source/name/description/resourceType/resourceId/costItemId/unitPrice/quantity/daily/unit/icon/sortOrder`)→ 前端 console 打印应该是只挑了部分字段(可能 `console.log({name:..., description:...})`)还是真的丢了完整字段? + +## 排查建议升级版 + +请前端在 `mounted()` 钩子加完整打印: + +```js +async function loadIncludedCosts() { + const res = await getFeeDeductionPreview(productId) + console.log('[FULL res]', JSON.parse(JSON.stringify(res))) // ← 看完整字段 + console.log('[res.length]', res?.length) // ← 看是不是真的少了 1 项 + console.log('[item keys]', res?.[0] && Object.keys(res[0])) // ← 看每项实际字段 + // ... +} +``` + +如果 `JSON.parse(JSON.stringify(res))` 完整打印里: +- 看到 3 项含 `feeType="景区"/"游玩项目"/"住宿"` 字段 → 是渲染层 v-for 过滤了「游玩项目」(跟之前推测一致),修 v-for 模板 / filter 逻辑 +- 看到 2 项缺「游玩项目」/字段不全 → axios 拦截器 / store / 数据预处理吞了字段(grep `filter.*feeType` / `pick.*name.*description` 之类) + +## 后端 round-trip 实测命令(yst 可复测) + +```bash +TOKEN=... +curl -k "https://api.test.1814.love:9443/admin/product/item/2045018534152478721/fee-deduction-preview" \ + -H "Authorization: Bearer $TOKEN" | jq '.data | length, .data[].feeType' +# 期望:3 / "景区" / "游玩项目" / "住宿" +``` + --- ## 现象