diff --git a/changelogs/2026-04/2026-04-18_product-v2_datascope-permission.md b/changelogs/2026-04/2026-04-18_product-v2_datascope-permission.md new file mode 100644 index 0000000..18336ad --- /dev/null +++ b/changelogs/2026-04/2026-04-18_product-v2_datascope-permission.md @@ -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 分享 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 | ✅ |