---
date: 2026-05-12
type: backend-improvement
service: hl-resource-service
priority: high
notify: ["@mmg"]
status: verified
---

# 资源下架放开"被产品引用"硬拦, 新增 /admin/resource-reference/in-use 预览接口 (已测试服验证)

> **PR**: [#2120](https://git.1814.love:8443/wx/HL/pulls/2120) — 已合并 dev + 部署测试服
> **Issue**: [#2115](https://git.1814.love:8443/wx/HL/issues/2115)
> **状态**: ✅ **测试服已验证** (api.test.1814.love:9443) — 6/6 case 端到端通过

---

## 一、背景

之前所有资源 (景区/活动/酒店/房型/餐厅/服务/人员/备品/备品组合/费用项/车型) 下架时, 后端会调 ProductFeignClient.checkResourceInUse 校验"是否被上架产品引用", 被引用就抛 `*_DISABLE_IN_USE` 错误码 (300103/310103/310206/320103/340103/350103/360103/380103/390103) 硬拦.

实际运营场景中, 资源被产品使用是常态, 强制要求"先解除产品绑定才能下架"把管理员锁死. 现改为: **后端放行下架, 由前端调预览接口拿到引用产品 list 弹二次确认**.

**删除/审批工作流逻辑不动** — 删除被引用资源仍硬拦, 审批模板/状态机/企微推送全部保留.

---

## 二、测试服已验证 (硬性凭证)

景区 `scenicId=3001000000000000019` (中俄边境公路) 被 **8 个上架产品**引用.

```
A. GET /admin/resource-reference/in-use?type=SCENIC&id=3001000000000000019
   → 200 code=200 data=["结婚不回家", "发的是干啥地方", "冻干粉发短信给",
                         "有今天放假n", "游牧的森林-短途版",
                         "呼伦贝尔草原深度定制·3天2晚家庭私享团",
                         "呼伦贝尔周边摄影季", "游牧的森林-短途版（来自wx的分享）"]

B. PUT /admin/scenic/spot/3001000000000000019/status body={"status":0}
   → 200 code=200 success  (历史 410103)  ✓ 关键验收

C. PUT /admin/scenic/spot/3001000000000000019/status body={"status":1}  (上架还原)
   → 200 code=200 success

D. PUT /admin/scenic/spots/batch/status body={"scenicIds":[3 个含 in-use],"status":0}
   → 200 code=200 全部下架成功  ✓ (历史会静默跳过 in-use 的)
   PUT 同上 status=1 上架还原 → 200 success

E. GET /admin/resource-reference/in-use 边界:
   - type=SCENIC&id=99999999 → 200 data=[]
   - type=RESTAURANT&id=1 → 200 data=[]
   - type=HOTEL&id=1 → 200 data=[]
   - type=ROOM_TYPE&id=1 → 200 data=[]
   - type=ACTIVITY&id=1 → 200 data=[]

F. DELETE /admin/scenic/spots/batch body={"scenicIds":[3001000000000000019]}
   → 200 deniedIds=["3001000000000000019"]
   → deniedReasons=["被产品使用: 结婚不回家、发的是干啥地方、冻干粉发短信给、…"]
   ✓ 删除逻辑保留, 仍硬拦
```

---

## 三、变更接口清单

### 新接口

| # | 接口 | 方法 | 说明 |
|---|------|------|------|
| 1 | `GET /admin/resource-reference/in-use` | GET | **新增**. 查询资源被哪些上架产品使用. 入参 `type` + `id`. 返回 `Result<List<String>>` 引用产品名 list. fail-closed: Feign 失败返 500 FEIGN_RPC_FAIL |

**type 取值**: `SCENIC` / `ACTIVITY` / `HOTEL` / `ROOM_TYPE` / `RESTAURANT` / `SERVICE` / `STAFF` / `SUPPLIES` / `SUPPLIES_COMBO` / `COST` / `VEHICLE`

### 行为变更接口 (响应结构不变, 只是不再返 410xxx)

所有资源下架/禁用相关接口 **不再返** `*_DISABLE_IN_USE` 错误码:

| 资源类型 | 单条下架 | 批量下架 | 提审批下架 | 已删的错误码 |
|----------|----------|----------|-----------|--------------|
| 景区 | `PUT /admin/scenic/spot/{id}/status` | `PUT /admin/scenic/spots/batch/status` | `POST /admin/scenic/spot/{id}/approval` | **300103** |
| 活动 | (同模式) | (同模式) | (同模式) | **320103** |
| 酒店 | `PUT /admin/hotel/...` | | | **310103** |
| 房型 | (同模式) | | | **310206** |
| 餐厅 | | | | (本就没此校验) |
| 服务 | (同模式) | | | **350103** |
| 人员 | (同模式) | | | **360103** |
| 备品 | (同模式) | | | **380103** |
| 备品组合 | (同模式) | | | **380103 (SUPPLIES_DISABLE_IN_USE 复用)** |
| 费用项 | (同模式) | | | **390103** |
| 车型 | (同模式) | | | **340103** |

前端原有针对这些 code 的特殊错误提示可以删掉.

### 删除/审批保留不动

- 删除被引用 (4 个 *_DELETE_IN_USE) 保留 ✓
- "已启用不能删除请先下架" (3 个 STATUS_ON_CANT_DELETE) 保留 ✓
- 审批模板/状态机/企微推送 全部保留 ✓

---

## 四、需要前端做的事 (@mmg)

### 1) 所有资源下架按钮加二次确认弹窗

**通用模板** (景区为例):

```vue
<script setup lang="ts">
import { useDialog } from 'naive-ui'
import { request } from '@/utils/request'

const dialog = useDialog()

async function handleOffline(scenicId: string, scenicName: string) {
  // 1. 先查引用情况
  const refRes = await request.get('/admin/resource-reference/in-use', {
    params: { type: 'SCENIC', id: scenicId }
  })
  const referenced = (refRes.data ?? []) as string[]

  // 2. 被引用 → 弹二次确认
  if (referenced.length > 0) {
    const productList = referenced.slice(0, 5).join('、')
              + (referenced.length > 5 ? `... 等 ${referenced.length} 个产品` : '')
    const ok = await new Promise<boolean>(resolve => {
      dialog.warning({
        title: '确认下架?',
        content: `「${scenicName}」正被以下 ${referenced.length} 个上架产品使用:\n${productList}\n\n下架后这些产品仍可正常售卖, 但请告知运营.`,
        positiveText: '确认下架',
        negativeText: '取消',
        onPositiveClick: () => resolve(true),
        onNegativeClick: () => resolve(false),
      })
    })
    if (!ok) return
  }

  // 3. 调下架 (历史 410103 不再发生)
  await request.put(`/admin/scenic/spot/${scenicId}/status`, { status: 0 })
  message.success('下架成功')
}
</script>
```

### 2) 删掉对 *_DISABLE_IN_USE 错误码的特殊处理

前端如有 `if (code === 410103 || code === 320103 || ...)` 弹特定提示的逻辑, **删掉**. 服务端不会再返这些 code.

### 3) type 参数映射 (前端各模块按需用)

| 资源类型 | type 值 |
|----------|---------|
| 景区 | `SCENIC` |
| 活动 | `ACTIVITY` |
| 酒店 | `HOTEL` |
| 房型 | `ROOM_TYPE` |
| 餐厅 | `RESTAURANT` |
| 服务 | `SERVICE` |
| 人员 | `STAFF` |
| 备品 | `SUPPLIES` |
| 备品组合 | `SUPPLIES_COMBO` |
| 费用项 | `COST` |
| 车型 | `VEHICLE` |

### 4) (可选) 列表页"被X个产品使用"角标

资源列表页可以批量调 `/admin/resource-reference/in-use` 给每个资源加角标"被N个产品使用", 但**不必须** — 二次确认弹窗已经足够.

---

## 五、风险/兼容性

- **下架后产品端不变**: 产品行程节点拿的是资源的名字/概要快照, 不实时查 status. 下架仅影响"资源列表筛选可见性"和"管理员新建产品时能选哪些资源".
- **历史脏数据**: 没有.
- **fail-closed**: 新接口 Feign 失败 (product-service 挂了) 抛 500 FEIGN_RPC_FAIL, 前端弹"无法确认引用情况, 请稍后再试" 即可.
- **删除路径仍硬拦**: 数据不会因为允许下架而丢失.

---

## 六、技术债清理 (跟改动一起完成)

- 删 9 个 0 调用的 `*_DISABLE_IN_USE` 错误码常量 (`ScenicErrorCode` / `HotelErrorCode` / `ActivityErrorCode` 等)
- 7 个 Service 删 `referenceCheckHelper` 字段 (Activity/Cost/Vehicle/Hotel/RoomType/Service/SuppliesCombo) — 这些 Service 不再有引用校验
- 4 个 Service 保留字段 (Scenic/Restaurant/Staff/Supplies — 它们 batchDelete 还在用)
- `ResourceReferenceCheckHelper` javadoc 更新, 标注只服务删除路径
- 13 个 test 文件同步: 5 个删 in-use 测试, 7 个 Service 测试构造器去 helper 参数, 1 个清字段 mock

**mvn test: Tests run: 1148, Failures: 0, Errors: 0, Skipped: 41 — 全绿**

---

@mmg 看到先弹窗模板, 改完通知我闭环.
