refactor: 数据权限过滤从 GORM Callback 改为 Store 层显式调用
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m2s

- 移除 RegisterDataPermissionCallback 和 SkipDataPermission 机制
- 在 Auth 中间件预计算 SubordinateShopIDs 并注入 Context
- 新增 ApplyShopFilter/ApplyEnterpriseFilter/ApplyOwnerShopFilter 等 Helper 函数
- 所有 Store 层查询方法显式调用数据权限过滤函数
- 权限检查函数 CanManageShop/CanManageEnterprise 改为从 Context 获取数据

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 16:38:52 +08:00
parent 4ba1f5b99d
commit 03a0960c4d
46 changed files with 1573 additions and 705 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-26

View File

@@ -0,0 +1,330 @@
# Design: refactor-data-permission-filter
## Context
当前系统使用 GORM Callback 在查询前自动注入数据权限过滤条件。该机制存在以下问题:
1. **隐式行为**开发者不知道查询被加了什么条件SQL 调试困难
2. **跳过率高**20+ 处使用 `SkipDataPermission`,说明自动过滤不适用于大量场景
3. **特殊处理多**`tb_shop``tb_tag``tb_enterprise_card_authorization` 等需要特殊逻辑
4. **重复查询**:同一请求中 Service 层 `CanManageShop` 和 Callback 都调用 `GetSubordinateShopIDs`
5. **原生 SQL 失效**Callback 无法处理原生 SQL需要在 Store 里手动重复写过滤逻辑
**涉及的 Store 统计:**
- 需要改动16 个 Store、85+ 个查询方法
- 无需改动32 个 Store系统全局表、关联表等
## Goals / Non-Goals
**Goals:**
- 数据权限过滤行为显式可控,便于调试
- 单次请求内下级店铺 ID 只查询一次(中间件预计算)
- 保持现有的数据隔离行为不变(代理看自己+下级,企业看自己,平台看全部)
- NULL `shop_id` 记录对代理用户不可见(保持现有行为)
**Non-Goals:**
- 不改变数据权限的业务规则
- 不修改 Redis 缓存策略(仍保持 30 分钟过期)
- 不处理个人客户的数据权限(保持 `customer_id` / `creator` 字段的现有逻辑)
## Decisions
### Decision 1: 中间件预计算 vs 懒加载
**选择**:中间件预计算
**方案对比:**
| 方案 | 优点 | 缺点 |
|------|------|------|
| 中间件预计算 | 单次请求只查一次;代码简单 | 所有请求都计算,即使不需要 |
| 懒加载(首次使用时计算) | 按需计算 | 需要加锁防止并发重复计算;代码复杂 |
**理由**
1. 绝大多数 API 都需要数据权限过滤,"不需要"是少数情况
2. `GetSubordinateShopIDs` 有 Redis 缓存,命中率高,预计算开销小
3. 代码更简单,不需要处理并发问题
### Decision 2: Helper 函数设计
**选择**:多个专用函数而非通用函数
```go
// 专用函数(选择)
func ApplyShopFilter(ctx context.Context, query *gorm.DB) *gorm.DB
func ApplyEnterpriseFilter(ctx context.Context, query *gorm.DB) *gorm.DB
func ApplyOwnerShopFilter(ctx context.Context, query *gorm.DB) *gorm.DB
// 通用函数(否决)
func ApplyDataPermission(ctx context.Context, query *gorm.DB, field string) *gorm.DB
```
**理由**
1. 字段名固定(`shop_id``enterprise_id``owner_shop_id`),无需参数化
2. 专用函数调用更清晰IDE 自动补全友好
3. 不同字段的过滤逻辑可能有细微差异,专用函数更灵活
### Decision 3: UserContextInfo 扩展 vs 新增 DataScope 结构体
**选择**:扩展 UserContextInfo
```go
// 扩展现有结构体(选择)
type UserContextInfo struct {
UserID uint
UserType int
ShopID uint
EnterpriseID uint
CustomerID uint
SubordinateShopIDs []uint // 新增
}
// 新增结构体(否决)
type DataScope struct {
SubordinateShopIDs []uint
}
```
**理由**
1. 减少 Context 中的 key 数量
2. 用户信息和权限范围本来就是强关联的
3. 现有代码已经大量使用 `UserContextInfo`,扩展更自然
### Decision 4: AuthConfig 传入 ShopStore vs 全局注册
**选择**AuthConfig 传入
```go
// AuthConfig 传入(选择)
type AuthConfig struct {
TokenExtractor func(c *fiber.Ctx) string
TokenValidator func(token string) (*UserContextInfo, error)
SkipPaths []string
ShopStore ShopStoreInterface // 新增
}
// 全局注册(否决)
var globalShopStore ShopStoreInterface
func RegisterShopStore(s ShopStoreInterface) { ... }
```
**理由**
1. 显式依赖注入,便于测试
2. 避免全局变量,符合 Go 最佳实践
3. 与现有 `TokenValidator` 传入方式一致
### Decision 5: nil vs 空切片表示"不限制"
**选择**nil 表示不限制
```go
// nil 表示不限制(选择)
if shopIDs := GetSubordinateShopIDs(ctx); shopIDs != nil {
query = query.Where("shop_id IN ?", shopIDs)
}
// 空切片表示不限制(否决)
if len(shopIDs) > 0 {
query = query.Where("shop_id IN ?", shopIDs)
}
```
**理由**
1. 语义更清晰nil = 未设置/不限制,`[]uint{}` = 设置了但列表为空
2. 空切片 `WHERE shop_id IN ()` 在 SQL 中是无效语法
3. 便于区分"平台用户不限制"和"代理用户但店铺列表为空(异常情况)"
## Implementation
### 文件结构
```
pkg/middleware/
├── auth.go # 扩展 UserContextInfo修改 Auth 中间件
├── data_scope.go # 新增Helper 函数
└── permission_helper.go # 修改CanManageShop 等函数签名
pkg/gorm/
└── callback.go # 移除 RegisterDataPermissionCallback
```
### 核心代码结构
**1. UserContextInfo 扩展auth.go**
```go
type UserContextInfo struct {
UserID uint
UserType int
ShopID uint
EnterpriseID uint
CustomerID uint
SubordinateShopIDs []uint // 新增代理用户的下级店铺ID列表nil表示不限制
}
```
**2. Auth 中间件改造auth.go**
```go
func Auth(config AuthConfig) fiber.Handler {
return func(c *fiber.Ctx) error {
// ... 现有 token 验证逻辑 ...
// 新增:预计算 SubordinateShopIDs
if config.ShopStore != nil &&
userInfo.UserType == constants.UserTypeAgent &&
userInfo.ShopID > 0 {
shopIDs, err := config.ShopStore.GetSubordinateShopIDs(c.UserContext(), userInfo.ShopID)
if err != nil {
// 降级处理
shopIDs = []uint{userInfo.ShopID}
logger.Warn("获取下级店铺失败,降级为只包含自己", zap.Error(err))
}
userInfo.SubordinateShopIDs = shopIDs
}
SetUserToFiberContext(c, userInfo)
return c.Next()
}
}
```
**3. Helper 函数data_scope.go**
```go
// GetSubordinateShopIDs 获取当前用户可管理的店铺ID列表
// 返回 nil 表示不受限制(平台用户/超管)
func GetSubordinateShopIDs(ctx context.Context) []uint {
if ctx == nil {
return nil
}
if ids, ok := ctx.Value(constants.ContextKeySubordinateShopIDs).([]uint); ok {
return ids
}
return nil
}
// ApplyShopFilter 应用店铺数据权限过滤
// 平台用户/超管:不添加条件
// 代理用户WHERE shop_id IN (subordinateShopIDs)
func ApplyShopFilter(ctx context.Context, query *gorm.DB) *gorm.DB {
shopIDs := GetSubordinateShopIDs(ctx)
if shopIDs == nil {
return query
}
return query.Where("shop_id IN ?", shopIDs)
}
// ApplyEnterpriseFilter 应用企业数据权限过滤
// 非企业用户:不添加条件
// 企业用户WHERE enterprise_id = ?
func ApplyEnterpriseFilter(ctx context.Context, query *gorm.DB) *gorm.DB {
userType := GetUserTypeFromContext(ctx)
if userType != constants.UserTypeEnterprise {
return query
}
enterpriseID := GetEnterpriseIDFromContext(ctx)
if enterpriseID == 0 {
return query.Where("1 = 0") // 企业用户但无企业ID返回空
}
return query.Where("enterprise_id = ?", enterpriseID)
}
// ApplyOwnerShopFilter 应用归属店铺数据权限过滤
// 用于 Enterprise 等使用 owner_shop_id 的表
func ApplyOwnerShopFilter(ctx context.Context, query *gorm.DB) *gorm.DB {
shopIDs := GetSubordinateShopIDs(ctx)
if shopIDs == nil {
return query
}
return query.Where("owner_shop_id IN ?", shopIDs)
}
```
**4. Store 层调用示例**
```go
// 改造前
func (s *DeviceStore) List(ctx context.Context, opts *QueryOptions) ([]*model.Device, error) {
query := s.db.WithContext(ctx).Model(&model.Device{})
// ... GORM Callback 自动添加 WHERE shop_id IN (...) ...
return ...
}
// 改造后
func (s *DeviceStore) List(ctx context.Context, opts *QueryOptions) ([]*model.Device, error) {
query := s.db.WithContext(ctx).Model(&model.Device{})
query = middleware.ApplyShopFilter(ctx, query) // 显式调用
// ...
return ...
}
```
## Risks / Trade-offs
### Risk 1: Store 层遗漏过滤调用
**风险**:改造过程中可能遗漏某些查询方法,导致数据泄露
**缓解措施**
1. 按照 proposal 中的 Store 清单逐一检查
2. 代码审查重点关注权限过滤
3. 可考虑在开发环境添加检测中间件,对未过滤的敏感表查询打印告警
### Risk 2: 预计算开销
**风险**:每个请求都预计算 `SubordinateShopIDs`,即使某些请求不需要
**缓解措施**
1. `GetSubordinateShopIDs` 有 Redis 缓存30分钟命中率高
2. Redis 查询通常 < 1ms
3. 实际影响可忽略
### Risk 3: 改造期间的并行开发冲突
**风险**:改造涉及 16 个 Store 文件,可能与其他开发任务冲突
**缓解措施**
1. 分阶段改造:先基础设施,再 Store 层
2. 优先改造高复杂度 Store降低后期风险
3. 改造期间及时 rebase 和解决冲突
## Migration Plan
### Phase 1: 基础设施(不影响现有功能)
1. 扩展 `UserContextInfo`,添加 `SubordinateShopIDs` 字段
2. 新增 `pkg/middleware/data_scope.go`,实现 Helper 函数
3. 修改 Auth 中间件,预计算 `SubordinateShopIDs`
4. 此阶段 GORM Callback 仍然生效,两套机制并存
### Phase 2: 改造权限检查函数
1. 修改 `CanManageShop`,从 Context 获取数据,移除 `shopStore` 参数
2. 修改 `CanManageEnterprise`,从 Context 获取数据
3. 更新所有调用点
### Phase 3: Store 层改造(按复杂度分批)
1. **低复杂度 Store9 个)**agent_wallet、commission_record 等
2. **中复杂度 Store4 个)**device、order、shop_package_allocation 等
3. **高复杂度 Store3 个)**iot_card、account、enterprise_card_authorization
### Phase 4: 清理
1. 移除 `RegisterDataPermissionCallback` 及其调用
2. 移除 `SkipDataPermission` 函数及所有调用点20+ 处)
3. 移除 `pkg/gorm/callback.go` 中的相关代码
### Rollback Strategy
如果发现问题,可以:
1. Phase 1-2 期间直接回滚代码GORM Callback 仍在工作
2. Phase 3 期间:保留 Callback 代码,只回滚 Store 改动
3. Phase 4 后:需要重新启用 Callback 代码
## Open Questions
1. ~~NULL shop_id 的记录对代理用户是否可见?~~ **已确认:不可见(保持现有行为)**
2. ~~是否需要为开发环境添加"未过滤敏感表查询"的告警机制?~~ **暂不需要,通过代码审查保证**

View File

@@ -0,0 +1,93 @@
# Proposal: refactor-data-permission-filter
## Why
当前 GORM Callback 自动数据权限过滤机制存在以下问题:
1. **跳过率高** - 20+ 处使用 `SkipDataPermission`(异步任务、登录、复杂查询等场景都需要跳过)
2. **特殊处理多** - `tb_shop``tb_tag``tb_enterprise_card_authorization` 等表需要特殊逻辑
3. **重复查询** - 同一请求中 Service 层的 `CanManageShop` 和 Callback 都调用 `GetSubordinateShopIDs`
4. **隐式行为** - 开发者不知道 GORM 查询被加了什么条件,调试困难
5. **原生 SQL 失效** - 原生 SQL 无法自动应用,需要手动在 Store 里重复写权限过滤逻辑
需要重构为显式调用模式,让数据权限过滤行为可预测、可控。
## What Changes
### 新增
- **中间件预加载**:扩展 Auth 中间件,对代理用户预计算 `SubordinateShopIDs` 并放入 Context
- **UserContextInfo 扩展**:新增 `SubordinateShopIDs []uint` 字段
- **Helper 函数**
- `GetSubordinateShopIDs(ctx) []uint` - 获取下级店铺 ID 列表
- `ApplyShopFilter(ctx, query) *gorm.DB` - 应用 `WHERE shop_id IN ?` 过滤
- `ApplyEnterpriseFilter(ctx, query) *gorm.DB` - 应用 `WHERE enterprise_id = ?` 过滤
- `ApplyOwnerShopFilter(ctx, query) *gorm.DB` - 应用 `WHERE owner_shop_id IN ?` 过滤
### 移除
- **BREAKING**:移除 `RegisterDataPermissionCallback` 函数
- **BREAKING**:移除 `SkipDataPermission` 函数及所有调用点20+ 处)
- **BREAKING**`CanManageShop` / `CanManageEnterprise` 函数签名变更,不再需要 Store 参数
### 修改
- **Store 层显式过滤**16 个 Store、85+ 个查询方法需要显式调用 Helper 函数添加权限过滤
## Capabilities
### New Capabilities
- `data-scope-middleware`: 数据权限范围中间件,负责预计算用户的数据访问范围并注入 Context
### Modified Capabilities
- `data-permission`: 数据权限过滤机制从 GORM Callback 自动过滤改为业务层显式调用
## Impact
### 代码影响
| 类型 | 文件/模块 | 改动说明 |
|------|-----------|----------|
| 新增 | `pkg/middleware/data_scope.go` | Helper 函数和 Context 操作 |
| 修改 | `pkg/middleware/auth.go` | 扩展 `UserContextInfo`Auth 中间件预计算 |
| 修改 | `pkg/middleware/permission_helper.go` | `CanManageShop` 等函数改为从 Context 取数据 |
| 移除 | `pkg/gorm/callback.go` | 移除 `RegisterDataPermissionCallback` |
| 修改 | 16 个 Store 文件 | 显式调用过滤 Helper 函数 |
| 移除 | 20+ 处 `SkipDataPermission` 调用 | 不再需要跳过机制 |
### 需要改动的 Store按复杂度分级
**高复杂度3 个)**
- `iot_card_store.go` - 9+ 方法,已有部分手动实现
- `account_store.go` - 7+ 方法双字段过滤shop_id / enterprise_id
- `enterprise_card_authorization_store.go` - 8+ 方法,已有参考实现
**中复杂度4 个)**
- `device_store.go` - 4+ 方法NULL shop_id 表示平台库存
- `order_store.go` - 3 方法,使用 seller_shop_id
- `shop_package_allocation_store.go` - 6 方法
- `shop_series_allocation_store.go` - 6 方法
**低复杂度9 个)**
- `agent_wallet_store.go` - 5 方法
- `agent_wallet_transaction_store.go` - 4 方法
- `commission_record_store.go` - 4 方法
- `enterprise_device_authorization_store.go` - 5 方法
- `enterprise_store.go` - 3 方法
- `shop_role_store.go` - 2 方法
- `iot_card_import_task_store.go` - 2 方法
- `commission_withdrawal_request_store.go` - 3 方法
- `card_wallet_store.go` - 5+ 方法
### API 影响
- 无外部 API 变更
- 内部函数签名变更:`CanManageShop(ctx, targetShopID, shopStore)``CanManageShop(ctx, targetShopID)`
### 行为变更
- NULL `shop_id` 的记录对代理用户不可见(保持现有行为)
- 平台用户/超管不受数据权限限制(保持现有行为)
- 数据权限过滤从隐式变为显式,需要业务层主动调用

View File

@@ -0,0 +1,90 @@
# data-permission Delta Specification
## Purpose
数据权限过滤机制从 GORM Callback 自动过滤改为业务层显式调用。
## REMOVED Requirements
### Requirement: GORM Callback Data Permission
**Reason**: GORM Callback 自动过滤存在以下问题跳过率高20+ 处 SkipDataPermission、特殊处理多、重复查询、隐式行为难调试、原生 SQL 失效。改为业务层显式调用模式。
**Migration**:
1. Store 层查询方法显式调用 `ApplyShopFilter``ApplyEnterpriseFilter` 等 Helper 函数
2. 参考 `pkg/middleware/data_scope.go` 中的 Helper 函数用法
### Requirement: Skip Data Permission
**Reason**: 移除 GORM Callback 后,不再需要跳过机制。数据权限过滤由业务层显式控制。
**Migration**:
1. 删除所有 `SkipDataPermission(ctx)` 调用
2. 删除所有 `pkggorm.SkipDataPermission` 引用
3. 业务逻辑直接控制是否调用过滤函数
### Requirement: Callback Registration
**Reason**: 不再使用 GORM Callback 机制。数据权限范围在 Auth 中间件预计算,过滤在业务层显式调用。
**Migration**:
1. 移除 `RegisterDataPermissionCallback` 函数
2. 移除应用启动时的 Callback 注册调用
3. Auth 中间件配置 `ShopStore` 以支持预计算
## MODIFIED Requirements
### Requirement: Subordinate IDs Caching
系统 SHALL 缓存用户的下级店铺 ID 列表以提高查询性能。
#### Scenario: 缓存命中
- **WHEN** 获取用户下级店铺 ID 列表
- **AND** Redis 缓存存在
- **THEN** 直接返回缓存数据
#### Scenario: 缓存未命中
- **WHEN** 获取用户下级店铺 ID 列表
- **AND** Redis 缓存不存在
- **THEN** 执行递归查询获取下级店铺 ID
- **AND** 将结果缓存到 Redis30 分钟过期)
#### Scenario: 请求级别复用
- **WHEN** 同一请求内多次需要下级店铺 ID 列表
- **THEN** 从 Context 中获取预计算的值
- **AND** 不重复查询 Redis 或数据库
## ADDED Requirements
### Requirement: Store 层显式数据权限过滤
系统 SHALL 在 Store 层查询方法中显式调用数据权限过滤函数。
#### Scenario: 有 shop_id 字段的表
- **WHEN** Store 执行列表查询
- **AND** 表包含 `shop_id` 字段
- **THEN** 显式调用 `ApplyShopFilter(ctx, query)`
- **AND** 代理用户只能查询 `shop_id IN (subordinateShopIDs)` 的数据
#### Scenario: 有 enterprise_id 字段的表
- **WHEN** Store 执行列表查询
- **AND** 表包含 `enterprise_id` 字段
- **AND** 当前用户为企业用户
- **THEN** 显式调用 `ApplyEnterpriseFilter(ctx, query)`
- **AND** 企业用户只能查询 `enterprise_id = ?` 的数据
#### Scenario: 有 owner_shop_id 字段的表
- **WHEN** Store 执行列表查询
- **AND** 表包含 `owner_shop_id` 字段(如 Enterprise 表)
- **THEN** 显式调用 `ApplyOwnerShopFilter(ctx, query)`
- **AND** 代理用户只能查询 `owner_shop_id IN (subordinateShopIDs)` 的数据
#### Scenario: NULL shop_id 不可见
- **WHEN** 代理用户查询有 `shop_id` 字段的表
- **AND** 记录的 `shop_id` 为 NULL平台库存
- **THEN** 该记录对代理用户不可见
#### Scenario: 平台用户/超管不过滤
- **WHEN** 平台用户或超级管理员执行查询
- **THEN** Helper 函数不添加任何过滤条件
- **AND** 可查询所有数据

View File

@@ -0,0 +1,130 @@
# data-scope-middleware Specification
## Purpose
数据权限范围中间件,负责在请求入口预计算用户的数据访问范围并注入 Context供业务层显式使用。
## ADDED Requirements
### Requirement: UserContextInfo 扩展
系统 SHALL 扩展 `UserContextInfo` 结构体以包含预计算的数据权限范围。
#### Scenario: 代理用户包含下级店铺 ID 列表
- **WHEN** 代理用户登录成功
- **AND** 用户有关联的店铺 ID
- **THEN** `UserContextInfo.SubordinateShopIDs` 包含自己店铺及所有下级店铺的 ID 列表
#### Scenario: 平台用户/超管不限制
- **WHEN** 平台用户或超级管理员登录成功
- **THEN** `UserContextInfo.SubordinateShopIDs` 为 nil
- **AND** nil 表示不受数据权限限制
#### Scenario: 企业用户使用 EnterpriseID
- **WHEN** 企业用户登录成功
- **THEN** `UserContextInfo.EnterpriseID` 包含用户所属企业 ID
- **AND** `UserContextInfo.SubordinateShopIDs` 为 nil
### Requirement: Auth 中间件预计算
系统 SHALL 在 Auth 中间件中预计算用户的数据访问范围。
#### Scenario: 代理用户预计算下级店铺
- **WHEN** Auth 中间件验证 token 成功
- **AND** 用户类型为代理用户
- **AND** 用户有关联的店铺 ID
- **THEN** 调用 `GetSubordinateShopIDs` 获取下级店铺 ID 列表
- **AND** 将结果设置到 `UserContextInfo.SubordinateShopIDs`
#### Scenario: 获取下级店铺失败降级处理
- **WHEN** 调用 `GetSubordinateShopIDs` 失败
- **THEN** `SubordinateShopIDs` 降级为只包含用户自己的店铺 ID
- **AND** 记录 Error 日志
#### Scenario: 非代理用户跳过预计算
- **WHEN** Auth 中间件验证 token 成功
- **AND** 用户类型不是代理用户
- **THEN** 不调用 `GetSubordinateShopIDs`
- **AND** `SubordinateShopIDs` 保持为 nil
### Requirement: Context 数据获取函数
系统 SHALL 提供从 Context 获取数据权限范围的函数。
#### Scenario: 获取下级店铺 ID 列表
- **WHEN** 调用 `GetSubordinateShopIDs(ctx)`
- **AND** Context 包含 `SubordinateShopIDs`
- **THEN** 返回下级店铺 ID 列表
#### Scenario: 获取空列表表示不限制
- **WHEN** 调用 `GetSubordinateShopIDs(ctx)`
- **AND** Context 中 `SubordinateShopIDs` 为 nil
- **THEN** 返回 nil
- **AND** 调用方应理解 nil 表示不受数据权限限制
### Requirement: 查询过滤 Helper 函数
系统 SHALL 提供查询过滤 Helper 函数,供 Store 层显式调用。
#### Scenario: ApplyShopFilter 过滤店铺数据
- **WHEN** 调用 `ApplyShopFilter(ctx, query)`
- **AND** `SubordinateShopIDs` 不为 nil
- **THEN** 返回添加了 `WHERE shop_id IN (?)` 条件的查询
- **AND** 参数为 `SubordinateShopIDs`
#### Scenario: ApplyShopFilter 不限制时不添加条件
- **WHEN** 调用 `ApplyShopFilter(ctx, query)`
- **AND** `SubordinateShopIDs` 为 nil
- **THEN** 返回原查询,不添加任何条件
#### Scenario: ApplyEnterpriseFilter 过滤企业数据
- **WHEN** 调用 `ApplyEnterpriseFilter(ctx, query)`
- **AND** 用户类型为企业用户
- **AND** `EnterpriseID` 大于 0
- **THEN** 返回添加了 `WHERE enterprise_id = ?` 条件的查询
#### Scenario: ApplyEnterpriseFilter 非企业用户不添加条件
- **WHEN** 调用 `ApplyEnterpriseFilter(ctx, query)`
- **AND** 用户类型不是企业用户
- **THEN** 返回原查询,不添加任何条件
#### Scenario: ApplyOwnerShopFilter 过滤归属店铺数据
- **WHEN** 调用 `ApplyOwnerShopFilter(ctx, query)`
- **AND** `SubordinateShopIDs` 不为 nil
- **THEN** 返回添加了 `WHERE owner_shop_id IN (?)` 条件的查询
### Requirement: 权限检查函数改造
系统 SHALL 改造权限检查函数,从 Context 获取数据而非传入 Store。
#### Scenario: CanManageShop 从 Context 获取数据
- **WHEN** 调用 `CanManageShop(ctx, targetShopID)`
- **AND** 用户类型为代理用户
- **THEN** 从 Context 获取 `SubordinateShopIDs`
- **AND** 检查 `targetShopID` 是否在列表中
#### Scenario: CanManageShop 平台用户自动通过
- **WHEN** 调用 `CanManageShop(ctx, targetShopID)`
- **AND** `SubordinateShopIDs` 为 nil
- **THEN** 返回成功(不受限制)
#### Scenario: CanManageEnterprise 从 Context 获取数据
- **WHEN** 调用 `CanManageEnterprise(ctx, targetEnterpriseID)`
- **AND** 用户类型为代理用户
- **THEN** 从 Context 获取 `SubordinateShopIDs`
- **AND** 查询目标企业的 `owner_shop_id`
- **AND** 检查 `owner_shop_id` 是否在列表中
### Requirement: AuthConfig 扩展
系统 SHALL 扩展 `AuthConfig` 以支持传入 ShopStore。
#### Scenario: AuthConfig 包含 ShopStore
- **WHEN** 初始化 Auth 中间件
- **THEN** `AuthConfig` 可选包含 `ShopStore ShopStoreInterface`
- **AND** 用于调用 `GetSubordinateShopIDs`
#### Scenario: ShopStore 未配置时跳过预计算
- **WHEN** `AuthConfig.ShopStore` 为 nil
- **THEN** 不预计算 `SubordinateShopIDs`
- **AND** 所有用户的 `SubordinateShopIDs` 为 nil

View File

@@ -0,0 +1,83 @@
# Tasks: refactor-data-permission-filter
## 1. 基础设施 - 数据结构和 Helper 函数
- [x] 1.1 扩展 `UserContextInfo` 结构体,添加 `SubordinateShopIDs []uint` 字段(`pkg/middleware/auth.go`
- [x] 1.2 新增 Context key 常量 `ContextKeySubordinateShopIDs``pkg/constants/constants.go`
- [x] 1.3 新增 `SetUserContext``SetUserToFiberContext``SubordinateShopIDs` 的处理
- [x] 1.4 新增 `pkg/middleware/data_scope.go` 文件,实现 `GetSubordinateShopIDs` 函数
- [x] 1.5 实现 `ApplyShopFilter` Helper 函数
- [x] 1.6 实现 `ApplyEnterpriseFilter` Helper 函数
- [x] 1.7 实现 `ApplyOwnerShopFilter` Helper 函数
- [x] 1.8 验证:编译通过,`go build ./...`
## 2. Auth 中间件改造
- [x] 2.1 扩展 `AuthConfig` 结构体,添加 `ShopStore ShopStoreInterface` 字段
- [x] 2.2 定义 `AuthShopStoreInterface` 接口(包含 `GetSubordinateShopIDs` 方法)
- [x] 2.3 修改 `Auth` 中间件,在 token 验证成功后预计算 `SubordinateShopIDs`
- [x] 2.4 实现降级逻辑:获取下级店铺失败时降级为只包含自己的店铺 ID
- [x] 2.5 更新 Admin API 和 H5 API 的 Auth 中间件配置,传入 ShopStore
- [x] 2.6 验证:编译通过(运行时验证将在最终验证阶段进行)
## 3. 权限检查函数改造
- [x] 3.1 修改 `CanManageShop` 函数签名,移除 `shopStore` 参数,改为从 Context 获取数据
- [x] 3.2 修改 `CanManageEnterprise` 函数签名,移除 `shopStore` 参数(保留 enterpriseStore
- [x] 3.3 更新所有 `CanManageShop` 调用点Service 层)
- [x] 3.4 更新所有 `CanManageEnterprise` 调用点Service 层)
- [x] 3.5 验证:编译通过
## 4. Store 层改造 - 低复杂度9 个)
- [x] 4.1 改造 `agent_wallet_store.go`List、GetByShopID 等方法添加 `ApplyShopFilter`
- [x] 4.2 改造 `agent_wallet_transaction_store.go`List 等方法添加 `ApplyShopFilter`
- [x] 4.3 改造 `commission_record_store.go`List 等方法添加 `ApplyShopFilter`
- [x] 4.4 改造 `enterprise_device_authorization_store.go`List 等方法添加 `ApplyEnterpriseFilter`
- [x] 4.5 改造 `enterprise_store.go`List 等方法添加 `ApplyOwnerShopFilter`
- [x] 4.6 改造 `shop_role_store.go`List 等方法添加 `ApplyShopFilter`
- [x] 4.7 改造 `iot_card_import_task_store.go`List 等方法添加 `ApplyShopFilter`
- [x] 4.8 改造 `commission_withdrawal_request_store.go`List 等方法添加 `ApplyShopFilter`
- [x] 4.9 改造 `card_wallet_store.go``card_wallet_transaction_store.go`
- [x] 4.10 验证:编译通过,低复杂度 Store 的列表接口数据过滤正常
## 5. Store 层改造 - 中复杂度4 个)
- [x] 5.1 改造 `device_store.go`List、Count 等方法添加 `ApplyShopFilter`,注意 NULL shop_id 处理
- [x] 5.2 改造 `order_store.go`List 等方法添加店铺过滤(使用 seller_shop_id 字段)
- [x] 5.3 改造 `shop_package_allocation_store.go`List 等方法添加 `ApplyShopFilter`
- [x] 5.4 改造 `shop_series_allocation_store.go`List 等方法添加 `ApplyShopFilter`
- [x] 5.5 验证:编译通过,中复杂度 Store 的列表接口数据过滤正常
## 6. Store 层改造 - 高复杂度3 个)
- [x] 6.1 改造 `account_store.go`List、GetByID 等方法,根据用户类型选择 `ApplyShopFilter``ApplyEnterpriseFilter`
- [x] 6.2 改造 `iot_card_store.go`List、ListStandalone 等方法添加 `ApplyShopFilter`,移除已有的手动过滤逻辑
- [x] 6.3 改造 `enterprise_card_authorization_store.go`List、ListWithJoin 等方法,整合现有的手动权限过滤
- [x] 6.4 验证:编译通过,高复杂度 Store 的列表接口数据过滤正常
## 7. 清理 - 移除 GORM Callback
- [x] 7.1 移除 `pkg/gorm/callback.go` 中的 `RegisterDataPermissionCallback` 函数
- [x] 7.2 移除 `SkipDataPermission` 函数和 `SkipDataPermissionKey` 常量
- [x] 7.3 移除应用启动时的 `RegisterDataPermissionCallback` 调用
- [x] 7.4 验证:编译通过
## 8. 清理 - 移除 SkipDataPermission 调用
- [x] 8.1 移除 `internal/task/*.go` 中的 `SkipDataPermission` 调用6 处)
- [x] 8.2 移除 `internal/service/auth/service.go` 中的 `SkipDataPermission` 调用
- [x] 8.3 移除 `internal/service/shop_series_allocation/service.go` 中的 `SkipDataPermission` 调用
- [x] 8.4 移除 `internal/service/enterprise_device/service.go` 中的 `SkipDataPermission` 调用5 处)
- [x] 8.5 移除 `internal/store/postgres/iot_card_store.go` 中的 `SkipDataPermission` 调用5 处)
- [x] 8.6 移除 `internal/store/postgres/enterprise_card_authorization_store.go` 中的 `SkipDataPermission` 调用
- [x] 8.7 移除 `internal/bootstrap/admin.go` 中的 `SkipDataPermission` 调用
- [x] 8.8 验证:编译通过,全局搜索 `SkipDataPermission` 无结果
## 9. 最终验证
- [ ] 9.1 启动服务,验证平台管理员可以查看所有数据
- [ ] 9.2 验证代理用户只能看到自己店铺及下级店铺的数据
- [ ] 9.3 验证企业用户只能看到自己企业的数据
- [ ] 9.4 验证 NULL shop_id 的记录对代理用户不可见
- [x] 9.5 运行 `go build ./...``go vet ./...` 确认无编译警告