diff --git a/changelogs/2026-05/07_feat_product-internal_resource-in-use_and_fail_closed.md b/changelogs/2026-05/07_feat_product-internal_resource-in-use_and_fail_closed.md new file mode 100644 index 0000000..d2c6993 --- /dev/null +++ b/changelogs/2026-05/07_feat_product-internal_resource-in-use_and_fail_closed.md @@ -0,0 +1,104 @@ +--- +date: 2026-05-07 +type: backend-feat +module: hl-product-service-v2/internal + hl-resource-service +priority: medium +notify: ["@mmg"] +status: tested-passed +restart_service: hl-product-service-v2 + hl-resource-service +gitea_pr: [1838, 1839] +gitea_issue: [1821] +--- + +# 资源删除/下架引用校验真实生效 + 产品服务不可用时改 fail-closed + +## 背景 + +PR #1815 + #1807 让资源侧 11~16 处 service 在删除/下架前调 `productFeignClient.checkResourceInUse(...)` 校验,但 **`hl-product-service-v2` 侧 endpoint 一直未实现**:`ProductFeignFallbackFactory` 默认 fail-open 返空 list → **所有引用校验都被静默放行**(测试服老景区 3001000000000000016 被 13 个产品引用却能 batchDelete 直接删)。 + +PR #1838 + 紧急 hotfix #1839 一次性闭环。 + +## 后端改动 + +### product 侧 (PR #1838) +- 新增 `/internal/product/resource-in-use` endpoint (11 类资源:SCENIC/RESTAURANT/ACTIVITY/SERVICE/HOTEL/ROOM_TYPE/SUPPLIES/SUPPLIES_COMBO/STAFF/VEHICLE/COST) +- 返回 `Result>`(被引用的产品名列表,空 list = 无引用) +- 上限 100,升序稳定输出 +- Flyway V20260507_004 幂等补 `product_supplies.from_combo_id` 列(legacy alter 未入 migration) + +### product 侧 (hotfix PR #1839) +- `ProductItineraryNodeMapper.selectProductIdsByResource` WHERE 条件改为 `(node_type=? OR resource_type=?)` OR 双兜底 +- 修因:历史 product_itinerary_node 数据 `resource_type` 列长期 NULL,真实类型存于 `node_type` 列(MpProductSnapshotAssembler:346 注释 "快照:资源类型用 nodeType 兜底"已说明) +- 测试服 round-trip 实测:删除 3001000000000000016 → deniedIds 含 + reasons 含 13 个中文产品名 + +### resource 侧 +- `ProductFeignFallbackFactory` 改 fail-closed:产品服务失败/熔断时返 `code=100903 "产品服务不可用,无法校验资源引用,请稍后重试"` +- 16 处调用点统一改 `if (!result.isSuccess()) throw FEIGN_RPC_FAIL` 模式 + +### 顺带修复 PR #1815 两处 Service 层 LambdaQueryWrapper 违规 +- `ScenicSpotService.updateSpot` 下沉到 `ScenicSpotMapper.countByNameExcludeId` +- `StaffService.validatePhoneUnique` 下沉到 `StaffMapper.countByPhoneExcludeId` + +## 前端可见变化 + +### 资源删除/下架接口异常情况新增 + +资源(景区/餐厅/活动/酒店/房型/服务人员/备品/备品组合/用车/成本项 等)删除/下架接口在**产品服务不可用时**会返: + +```json +{ + "code": 100903, + "message": "产品服务不可用,无法校验资源引用,请稍后重试" +} +``` + +@mmg 前端需要捕获 100903,提示用户重试。 + +### 被引用资源删除/下架的响应(原有但现在真生效) + +batchDelete 接口返: +```json +{ + "code": 200, + "data": { + "successCount": 0, + "deniedIds": ["3001000000000000016"], + "deniedReasons": ["被产品使用: 产品A、产品B、产品C..."] + } +} +``` + +前端原有展示 deniedReasons 即可。 + +### batchDelete 行为变化 +批量删除时若产品服务调用失败,**整体抛异常中止**(原可能 N 次重试,部分 denied 部分成功)。前端弹"产品服务不可用,请稍后重试"即可,无需逐条处理。 + +## 重启服务 + +测试服已部署:`hl-product-service-v2`(PR #1838 → #1839 hotfix 已 redeploy)+ `hl-resource-service`(PR #1838)。 + +## 验收(测试服 round-trip 实测) + +- [x] **创建产品引用资源(景区) → batchDelete 该景区** → `deniedReasons` 含中文产品名,景区未被删除 (UC-A 实测 13 个产品名) +- [x] **删除/下架未被引用的资源** → 正常成功 (UC-D 创建新景区→batchDelete successCount=1) +- [x] **fail-closed 模式生效**:Mapper OR 双兜底命中 nodeType 字段 +- [x] **PR #1815 两处分层违规下沉到 Mapper** (UC-5b 编辑自身保留同名 code=200) +- [x] **跨服务 Feign 不配 fallback 仅返 isSuccess=false** (`Result.error`) +- [ ] hotel/activity/vehicle/cost 4 资源服务 fail-closed 同步覆盖 (代码已改,测试服 round-trip 时间所限只跑 SCENIC,留监控验证) + +## 关联工单 + +- Gitea #1821 (本 PR 闭环, 已 auto-close via PR #1838) + +## Follow-up(本 PR 不修) + +- 16 处 fail-closed 模板代码可提取 `ResourceReferenceCheckHelper` +- RoomType 禁用按 HOTEL 维度校验语义偏严(PR #1815 引入) +- `product_itinerary_node(resource_type, resource_id)` 复合索引(数据量起来后) +- `product_supplies(supplies_resource_id)` / `product_route_info(vehicle_model_id)` 索引 +- batchDelete TOCTOU 时序问题 +- PR #1838 单测 14 用例都用 mock,Mocked Mapper 无法暴露 SQL 与 DB schema 错位 — 后续应加 H2 集成测试或 `@MybatisPlusTest` 真跑 SQL + +## 已知影响(测试服) + +- QA round-trip 期间 SCENIC `3001000000000000005` + RESTAURANT `2023382103407751169` 被前轮(hotfix 前)误删,无法恢复;测试人员需重建相关测试数据