feat(product-v2): 产品/产品线数据权限切面 @DataScope (PR #849)

- 管理端 37 个接口加 @DataScope
- 3 个 VO 加 canEdit/isOwner 字段
- CORE/GROUP 可见不可改、CUSTOM 仅自己
- 前端按 canEdit 灰化按钮
这个提交包含在:
wx 2026-04-18 20:04:03 +08:00
父节点 aed14f4451
当前提交 2cf5a54f9c

查看文件

@ -0,0 +1,184 @@
# 产品/产品线数据权限切面 @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 分享 URLCUSTOM 产品给客户看)
- 走 `/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 | ✅ |