# 产品/产品线数据权限切面 @DataScope

**合并时间**：2026-04-18
**PR**：https://git.1814.love:8443/wx/HL/pulls/849
**Issue**：#839
**影响范围**：`hl-product-service-v2` 管理端 37 个 `/admin/product/**` 和 `/admin/product/line/**` 接口
**前端需同步**：✅ 是（VO 新增 canEdit/isOwner + 403/404 语义 + 部分接口响应条数变化）

---

## 一、权限规则（必读）

### 1.1 CORE 产品 / GROUP 产品（小蒙马） / CORE 产品线 / GROUP 产品线
- **所有拥有"产品管理"菜单权限的管理员都可查看**列表和详情（不限创建人）
- **只有创建人可操作**（编辑 / 删除 / 上下架 / 复制 / 班期 / 价格日历 / 行程 / 详情区块等所有写接口）
- 非创建人试图调写接口 → `code=403 message="无操作权限"`

### 1.2 CUSTOM 产品 / CUSTOM 产品线（私人定制）
- **只有创建人可见**（列表返回自动过滤；非创建人按 ID 查详情 → `code=404`）
- **只有创建人可操作**
- 其他管理员（非超管）**完全不知道别人 CUSTOM 产品的存在**

### 1.3 SUPER_ADMIN（超级管理员）
- **所有产品/产品线都可看、都可操作**（不受 1.1 / 1.2 限制）
- 详情返回里 `canEdit=true`（恒真）
- 但 `isOwner` 仍按真实 created_by 比对（**超管不自动算创建人**）

### 1.4 未登录 / Token 失效
- `code=401` 拒绝（即使 Nacos 开关 `hl.data-scope.enabled=false` 关闭切面也拒绝）

### 1.5 分享 URL（CUSTOM 产品给客户看）
- 走 `/mp/**` → Feign → `/internal/**` 链路，**完全不经过本切面**
- 客户端无 adminId，不受数据权限影响

---

## 二、响应字段新增（3 个 VO）

### 2.1 `ProductListRespVO`（列表接口 `GET /admin/product/item/list`）
```json
{
  "productId": 1001,
  "productType": "CUSTOM",
  "name": "草原深度定制",
  "createdBy": 1002,
  ... // 其他原有字段
  "canEdit": true,        // ← 新增
  "isOwner": true         // ← 新增
}
```

### 2.2 `ProductDetailRespVO`（详情接口 `GET /admin/product/item/{id}`）
同 2.1，新增 `canEdit` + `isOwner`。

### 2.3 `ProductLineRespVO`（产品线列表 / 详情）
```json
{
  "lineId": 501,
  "name": "呼伦贝尔经典",
  "productType": "CORE",
  "createdBy": 1001,
  ... // 其他原有字段
  "canEdit": false,       // ← 新增
  "isOwner": false        // ← 新增
}
```

### 2.4 字段语义（所有 3 个 VO 都适用）

| 字段 | 类型 | 语义 | 超管下取值 |
|------|------|------|-----------|
| `canEdit` | Boolean（非 null） | 当前登录管理员是否可编辑该记录 | 恒 `true` |
| `isOwner` | Boolean（非 null） | 当前登录管理员是否为该记录创建人（`created_by == adminId`） | 按真实 `created_by` 判断，不开后门 |

**前端使用建议**：
- 编辑按钮 / 删除按钮 / 上下架按钮：`v-if="row.canEdit"` 或灰化（`disabled="!row.canEdit"`）
- 展示"我创建的" Tag：`v-if="row.isOwner"`
- **不要**单独用 `isOwner` 判断按钮可用（超管时 `isOwner=false` 但应能操作 → 用 `canEdit` 判断才对）

### 2.5 不加字段的接口
- `ProductSimpleListVO`（`GET /admin/product/item/simple-list`）— 下拉用，不返
- `ProductLineDropdownVO` / simple-list — 下拉用，不返

---

## 三、错误码 / 响应码变化

| 场景 | 之前 | 现在 |
|------|------|------|
| 未带 Token / Token 失效 | 401 | **401**（不变） |
| 非超管按 ID 查他人 CUSTOM 详情 | 200 + 数据 | **404 `"资源不存在或无权限"`** |
| 非超管编辑他人 CORE/GROUP/CUSTOM 任一写操作 | 200（数据被改） | **403 `"无操作权限"`** |
| 非超管列表返回其他定制师的 CUSTOM | 返回 | **不返回**（total 也减少） |

**前端错误处理建议**：
- 404：统一弹 "资源不存在或无权限，请刷新列表"
- 403：统一弹 "您没有操作此产品的权限"

---

## 四、受影响接口清单（37 个）

### 4.1 `AdminProductController`（`/admin/product/item/**`）33 个
- **列表查询**：`GET /list`、`GET /simple-list`
- **详情**：`GET /{id}`、`GET /{id}/price-calendar`、`GET /{id}/schedule/list`、`GET /{id}/schedule/team`、`GET /{id}/suggest-price`、`GET /{id}/supplement`、`GET /{id}/calculate-mileage`、`GET /{id}/detail-blocks`、`GET /{id}/operation-logs`、`GET /{id}/validate-publish`、`GET /{id}/fee-deduction-preview`、`GET /{productId}/tiers`
- **写操作**（行级校验）：
  - `PUT /{id}/itinerary`、`POST /{id}/itinerary/day`、`DELETE /{id}/itinerary/day/{dayNumber}`
  - `PUT /{id}/route`、`POST /{id}/price-calendar/batch`、`DELETE /{id}/price-calendar`
  - `POST /{id}/schedule`、`DELETE /{id}/schedule/{scheduleId}`、`PUT /{id}/schedule`、`POST /{id}/schedule/batch-create`、`POST /{id}/schedule/{scheduleId}/cancel`、`PUT /{id}/schedule/team`
  - `POST /{id}/quote`、`PUT /{id}/supplement`、`POST /{id}/daily-mileage`
  - `PUT /{id}/detail-blocks`、`POST /{id}/toggle-publish`、`POST /{id}/complete`、`POST /{id}/action`（@Deprecated）、`DELETE /{id}`
- **不加切面，Service 层做权限**：
  - `POST /basic`（创建+更新合一，更新分支 Service 层 assertCanWrite）
  - `POST /{id}/copy`（Service 层 assertCanRead，允许任何"可见者"复制；R9 规则）
  - `PUT /{id}/transfer`（Service 层仅超管可操作）

### 4.2 `AdminProductLineController`（`/admin/product/line/**`）4 个
- `GET /list`、`GET /simple-list`（按产品线 product_type 过滤）
- `GET /item/{id}`（ROW_CHECK）
- `DELETE /item/{id}`（ROW_CHECK）
- `POST /item`（创建+更新合一，更新分支 Service 层 assertCanWrite）

---

## 五、服务端配置（运维关注）

### 5.1 Nacos 灰度开关
```yaml
# hl-product-service-v2-dev.yml / -prod.yml
hl:
  data-scope:
    enabled: true           # 总开关；false 时切面不过滤（401 校验仍生效）
    reject-status: 404      # 越权访问响应码（CUSTOM 不泄露存在性）
    log-level: warn         # 越权日志级别
```

### 5.2 数据库迁移（上线前 DBA 执行）
```sql
-- 1. 补齐 created_by NULL 数据（防 R3 行级校验误伤）
UPDATE product SET created_by=1, updated_by=COALESCE(updated_by, 1)
WHERE created_by IS NULL AND deleted_at IS NULL;

UPDATE product_line SET created_by=1, updated_by=COALESCE(updated_by, 1)
WHERE created_by IS NULL AND deleted_at IS NULL;

-- 2. 补齐 product_line.product_type NULL 数据
UPDATE product_line SET product_type='CORE'
WHERE (product_type IS NULL OR product_type='') AND deleted_at IS NULL;

-- 3. 组合索引
ALTER TABLE product
  ADD INDEX idx_type_creator_deleted (product_type, created_by, deleted_at);
ALTER TABLE product_line
  ADD INDEX idx_type_creator (product_type, created_by);
```

详见 `hl-product-service-v2/src/main/resources/db/migration/V20260418__data_scope_created_by_backfill.sql`。

---

## 六、部署影响

需要**重启 hl-product-service-v2**。其他服务无影响（/mp/**、/internal/**、前端静态资源均未变）。

---

## 七、设计文档

完整需求 / 评审 / 架构 / 测试报告详见 PR 关联的任务文档 `docs/tasks/20260418_设计_产品线与产品数据权限.md`（1600+ 行）。

---

## 八、本地验证摘录（smoke test 结果）

| 测试场景 | 预期 | 实际 |
|---------|------|------|
| 超管列表 /item/list | total=7396 全 canEdit=true | ✅ |
| CUSTOMIZER 列表 | total=7394（自动过滤 2 条他人 CUSTOM） | ✅ |
| CUSTOMIZER 查他人 CUSTOM 详情 | code=404 | ✅ |
| CUSTOMIZER 查他人 CORE 详情 | code=200, canEdit=false, isOwner=false | ✅ |
| CUSTOMIZER 删他人 CORE | code=403 | ✅ |
| CUSTOMIZER 删他人 CUSTOM | code=403 | ✅ |
| 超管查 CUSTOM 详情 | code=200, canEdit=true, isOwner=false（超管不开后门） | ✅ |
| 无 Token | code=401 | ✅ |
