# 产品/产品线数据权限切面 @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 | ✅ |