refactor: 数据权限过滤从 GORM Callback 改为 Store 层显式调用
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m2s
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:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-02-26
|
||||
@@ -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. **低复杂度 Store(9 个)**:agent_wallet、commission_record 等
|
||||
2. **中复杂度 Store(4 个)**:device、order、shop_package_allocation 等
|
||||
3. **高复杂度 Store(3 个)**: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. ~~是否需要为开发环境添加"未过滤敏感表查询"的告警机制?~~ **暂不需要,通过代码审查保证**
|
||||
@@ -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` 的记录对代理用户不可见(保持现有行为)
|
||||
- 平台用户/超管不受数据权限限制(保持现有行为)
|
||||
- 数据权限过滤从隐式变为显式,需要业务层主动调用
|
||||
@@ -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** 将结果缓存到 Redis(30 分钟过期)
|
||||
|
||||
#### 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** 可查询所有数据
|
||||
@@ -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
|
||||
@@ -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 ./...` 确认无编译警告
|
||||
@@ -1,77 +1,60 @@
|
||||
# data-permission Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change refactor-framework-cleanup. Update Purpose after archive.
|
||||
|
||||
数据权限过滤机制,通过业务层显式调用实现数据隔离。
|
||||
|
||||
## Requirements
|
||||
### Requirement: GORM Callback Data Permission
|
||||
|
||||
系统 SHALL 使用 GORM Callback 机制自动为所有查询添加数据权限过滤。
|
||||
|
||||
#### Scenario: 自动应用权限过滤
|
||||
- **WHEN** 执行 GORM 查询
|
||||
- **AND** Context 包含用户信息
|
||||
- **AND** 表包含 owner_id 字段
|
||||
- **THEN** 自动添加 WHERE owner_id IN (subordinateIDs) 条件
|
||||
|
||||
#### Scenario: Root 用户跳过过滤
|
||||
- **WHEN** 当前用户是 Root 用户
|
||||
- **THEN** 不添加任何数据权限过滤条件
|
||||
- **AND** 可查询所有数据
|
||||
|
||||
#### Scenario: 无 owner_id 字段的表
|
||||
- **WHEN** 表不包含 owner_id 字段
|
||||
- **THEN** 不添加数据权限过滤条件
|
||||
|
||||
#### Scenario: 授权记录表特殊处理
|
||||
- **WHEN** 查询 `tb_enterprise_card_authorization` 表
|
||||
- **AND** 当前用户是代理用户
|
||||
- **THEN** 自动添加 WHERE enterprise_id IN (SELECT id FROM tb_enterprise WHERE owner_shop_id = 当前店铺ID) 条件
|
||||
- **AND** 不包含下级店铺的数据
|
||||
|
||||
#### Scenario: 平台用户查询授权记录
|
||||
- **WHEN** 查询 `tb_enterprise_card_authorization` 表
|
||||
- **AND** 当前用户是平台用户或超级管理员
|
||||
- **THEN** 不添加数据权限过滤条件
|
||||
- **AND** 可查询所有授权记录
|
||||
|
||||
### Requirement: Skip Data Permission
|
||||
|
||||
系统 SHALL 支持通过 Context 绕过数据权限过滤。
|
||||
|
||||
#### Scenario: 显式跳过权限过滤
|
||||
- **WHEN** 调用 SkipDataPermission(ctx) 获取新 Context
|
||||
- **AND** 使用该 Context 执行 GORM 查询
|
||||
- **THEN** 不添加任何数据权限过滤条件
|
||||
|
||||
#### Scenario: 内部操作跳过过滤
|
||||
- **WHEN** 执行内部同步、批量操作或管理员操作
|
||||
- **THEN** 应使用 SkipDataPermission 绕过过滤
|
||||
|
||||
### Requirement: Subordinate IDs Caching
|
||||
|
||||
系统 SHALL 缓存用户的下级 ID 列表以提高查询性能。
|
||||
系统 SHALL 缓存用户的下级店铺 ID 列表以提高查询性能。
|
||||
|
||||
#### Scenario: 缓存命中
|
||||
- **WHEN** 获取用户下级 ID 列表
|
||||
- **WHEN** 获取用户下级店铺 ID 列表
|
||||
- **AND** Redis 缓存存在
|
||||
- **THEN** 直接返回缓存数据
|
||||
|
||||
#### Scenario: 缓存未命中
|
||||
- **WHEN** 获取用户下级 ID 列表
|
||||
- **WHEN** 获取用户下级店铺 ID 列表
|
||||
- **AND** Redis 缓存不存在
|
||||
- **THEN** 执行递归 CTE 查询获取下级 ID
|
||||
- **THEN** 执行递归查询获取下级店铺 ID
|
||||
- **AND** 将结果缓存到 Redis(30 分钟过期)
|
||||
|
||||
### Requirement: Callback Registration
|
||||
#### Scenario: 请求级别复用
|
||||
- **WHEN** 同一请求内多次需要下级店铺 ID 列表
|
||||
- **THEN** 从 Context 中获取预计算的值
|
||||
- **AND** 不重复查询 Redis 或数据库
|
||||
|
||||
系统 SHALL 在应用启动时注册 GORM 数据权限 Callback。
|
||||
### Requirement: Store 层显式数据权限过滤
|
||||
|
||||
#### Scenario: 注册 Callback
|
||||
- **WHEN** 调用 RegisterDataPermissionCallback(db, accountStore)
|
||||
- **THEN** 注册 Query Before Callback
|
||||
- **AND** Callback 名称为 "data_permission"
|
||||
系统 SHALL 在 Store 层查询方法中显式调用数据权限过滤函数。
|
||||
|
||||
#### Scenario: AccountStore 依赖
|
||||
- **WHEN** 注册 Callback 时
|
||||
- **THEN** 需要传入 AccountStore 实例用于获取下级 ID
|
||||
#### 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** 可查询所有数据
|
||||
|
||||
130
openspec/specs/data-scope-middleware/spec.md
Normal file
130
openspec/specs/data-scope-middleware/spec.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# data-scope-middleware Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
数据权限范围中间件,负责在请求入口预计算用户的数据访问范围并注入 Context,供业务层显式使用。
|
||||
|
||||
## 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
|
||||
Reference in New Issue
Block a user