重构数据权限模型并清理旧RBAC代码

核心变更:
- 数据权限过滤从基于账号层级改为基于用户类型的多策略过滤
- 移除 AccountStore 中的 GetSubordinateIDs 等旧方法
- 重构认证中间件,支持 enterprise_id 和 customer_id
- 更新 GORM Callback,根据用户类型自动选择过滤策略(代理/企业/个人客户)
- 更新所有集成测试以适配新的 API 签名
- 添加功能总结文档和 OpenSpec 归档

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-10 15:08:11 +08:00
parent 9c6d4a3bd4
commit 743db126f7
26 changed files with 1292 additions and 322 deletions

View File

@@ -9,20 +9,19 @@ import (
"github.com/stretchr/testify/assert"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
// mockAccountStore 模拟账号 Store
type mockAccountStore struct {
subordinateIDs []uint
err error
// mockShopStore 模拟店铺 Store
type mockShopStore struct {
subordinateShopIDs []uint
err error
}
func (m *mockAccountStore) GetSubordinateIDs(ctx context.Context, accountID uint) ([]uint, error) {
func (m *mockShopStore) GetSubordinateShopIDs(ctx context.Context, shopID uint) ([]uint, error) {
if m.err != nil {
return nil, m.err
}
return m.subordinateIDs, nil
return m.subordinateShopIDs, nil
}
// TestSkipDataPermission 测试跳过数据权限过滤
@@ -38,95 +37,15 @@ func TestSkipDataPermission(t *testing.T) {
assert.True(t, skip)
}
// TestHasCreatorField 测试检查 creator 字段
func TestHasCreatorField(t *testing.T) {
tests := []struct {
name string
schema *schema.Schema
expected bool
}{
{
name: "nil schema",
schema: nil,
expected: false,
},
{
name: "schema with creator field",
schema: &schema.Schema{
FieldsByDBName: map[string]*schema.Field{
"creator": {},
},
},
expected: true,
},
{
name: "schema without creator field",
schema: &schema.Schema{
FieldsByDBName: map[string]*schema.Field{
"id": {},
},
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := hasCreatorField(tt.schema)
assert.Equal(t, tt.expected, result)
})
}
}
// TestHasShopIDField 测试检查 shop_id 字段
func TestHasShopIDField(t *testing.T) {
tests := []struct {
name string
schema *schema.Schema
expected bool
}{
{
name: "nil schema",
schema: nil,
expected: false,
},
{
name: "schema with shop_id field",
schema: &schema.Schema{
FieldsByDBName: map[string]*schema.Field{
"shop_id": {},
},
},
expected: true,
},
{
name: "schema without shop_id field",
schema: &schema.Schema{
FieldsByDBName: map[string]*schema.Field{
"id": {},
},
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := hasShopIDField(tt.schema)
assert.Equal(t, tt.expected, result)
})
}
}
// TestRegisterDataPermissionCallback 测试注册数据权限 Callback
func TestRegisterDataPermissionCallback(t *testing.T) {
// 创建内存数据库
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
assert.NoError(t, err)
// 创建 mock AccountStore
mockStore := &mockAccountStore{
subordinateIDs: []uint{1, 2, 3},
// 创建 mock ShopStore
mockStore := &mockShopStore{
subordinateShopIDs: []uint{1, 2, 3},
}
// 注册 Callback
@@ -134,8 +53,8 @@ func TestRegisterDataPermissionCallback(t *testing.T) {
assert.NoError(t, err)
}
// TestDataPermissionCallback_SkipForRootUser 测试 root 用户跳过过滤
func TestDataPermissionCallback_SkipForRootUser(t *testing.T) {
// TestDataPermissionCallback_SkipForSuperAdmin 测试超级管理员跳过过滤
func TestDataPermissionCallback_SkipForSuperAdmin(t *testing.T) {
// 创建内存数据库
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
assert.NoError(t, err)
@@ -143,6 +62,7 @@ func TestDataPermissionCallback_SkipForRootUser(t *testing.T) {
// 创建测试表
type TestModel struct {
ID uint
ShopID uint
Creator uint
Name string
}
@@ -151,33 +71,39 @@ func TestDataPermissionCallback_SkipForRootUser(t *testing.T) {
assert.NoError(t, err)
// 插入测试数据
db.Create(&TestModel{ID: 1, Creator: 1, Name: "test1"})
db.Create(&TestModel{ID: 2, Creator: 2, Name: "test2"})
db.Create(&TestModel{ID: 1, ShopID: 100, Creator: 1, Name: "test1"})
db.Create(&TestModel{ID: 2, ShopID: 200, Creator: 2, Name: "test2"})
// 创建 mock AccountStore
mockStore := &mockAccountStore{
subordinateIDs: []uint{1}, // 只有 ID 1
// 创建 mock ShopStore
mockStore := &mockShopStore{
subordinateShopIDs: []uint{100}, // 只有店铺 100
}
// 注册 Callback
err = RegisterDataPermissionCallback(db, mockStore)
assert.NoError(t, err)
// 设置 root 用户 context
// 设置超级管理员 context
ctx := context.Background()
ctx = middleware.SetUserContext(ctx, 1, constants.UserTypeSuperAdmin, 0)
ctx = middleware.SetUserContext(ctx, &middleware.UserContextInfo{
UserID: 1,
UserType: constants.UserTypeSuperAdmin,
ShopID: 0,
EnterpriseID: 0,
CustomerID: 0,
})
// 查询数据
var results []TestModel
err = db.WithContext(ctx).Find(&results).Error
assert.NoError(t, err)
// root 用户应该看到所有数据
// 超级管理员应该看到所有数据
assert.Equal(t, 2, len(results))
}
// TestDataPermissionCallback_FilterForNormalUser 测试普通用户过滤
func TestDataPermissionCallback_FilterForNormalUser(t *testing.T) {
// TestDataPermissionCallback_SkipForPlatform 测试平台用户跳过过滤
func TestDataPermissionCallback_SkipForPlatform(t *testing.T) {
// 创建内存数据库
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
assert.NoError(t, err)
@@ -185,6 +111,7 @@ func TestDataPermissionCallback_FilterForNormalUser(t *testing.T) {
// 创建测试表
type TestModel struct {
ID uint
ShopID uint
Creator uint
Name string
}
@@ -193,32 +120,86 @@ func TestDataPermissionCallback_FilterForNormalUser(t *testing.T) {
assert.NoError(t, err)
// 插入测试数据
db.Create(&TestModel{ID: 1, Creator: 1, Name: "test1"})
db.Create(&TestModel{ID: 2, Creator: 2, Name: "test2"})
db.Create(&TestModel{ID: 3, Creator: 3, Name: "test3"})
db.Create(&TestModel{ID: 1, ShopID: 100, Creator: 1, Name: "test1"})
db.Create(&TestModel{ID: 2, ShopID: 200, Creator: 2, Name: "test2"})
// 创建 mock AccountStore
mockStore := &mockAccountStore{
subordinateIDs: []uint{1, 2}, // 只能看到 1 和 2
// 创建 mock ShopStore
mockStore := &mockShopStore{
subordinateShopIDs: []uint{100}, // 只有店铺 100
}
// 注册 Callback
err = RegisterDataPermissionCallback(db, mockStore)
assert.NoError(t, err)
// 设置普通用户 context (非 root)
// 设置平台用户 context
ctx := context.Background()
ctx = middleware.SetUserContext(ctx, 1, constants.UserTypeAgent, 0)
ctx = middleware.SetUserContext(ctx, &middleware.UserContextInfo{
UserID: 1,
UserType: constants.UserTypePlatform,
ShopID: 0,
EnterpriseID: 0,
CustomerID: 0,
})
// 查询数据
var results []TestModel
err = db.WithContext(ctx).Find(&results).Error
assert.NoError(t, err)
// 普通用户只能看到自己和下级的数据
// 平台用户应该看到所有数据
assert.Equal(t, 2, len(results))
assert.Equal(t, uint(1), results[0].Creator)
assert.Equal(t, uint(2), results[1].Creator)
}
// TestDataPermissionCallback_FilterForAgent 测试代理用户过滤
func TestDataPermissionCallback_FilterForAgent(t *testing.T) {
// 创建内存数据库
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
assert.NoError(t, err)
// 创建测试表(包含 shop_id 字段以触发店铺层级过滤)
type TestModel struct {
ID uint
ShopID uint
Name string
}
err = db.AutoMigrate(&TestModel{})
assert.NoError(t, err)
// 插入测试数据
db.Create(&TestModel{ID: 1, ShopID: 100, Name: "test1"})
db.Create(&TestModel{ID: 2, ShopID: 200, Name: "test2"})
db.Create(&TestModel{ID: 3, ShopID: 300, Name: "test3"})
// 创建 mock ShopStore
mockStore := &mockShopStore{
subordinateShopIDs: []uint{100, 200}, // 只能看到店铺 100 和 200
}
// 注册 Callback
err = RegisterDataPermissionCallback(db, mockStore)
assert.NoError(t, err)
// 设置代理用户 context (shop_id = 100)
ctx := context.Background()
ctx = middleware.SetUserContext(ctx, &middleware.UserContextInfo{
UserID: 1,
UserType: constants.UserTypeAgent,
ShopID: 100,
EnterpriseID: 0,
CustomerID: 0,
})
// 查询数据
var results []TestModel
err = db.WithContext(ctx).Find(&results).Error
assert.NoError(t, err)
// 代理用户只能看到自己店铺和下级店铺的数据
assert.Equal(t, 2, len(results))
assert.Equal(t, uint(100), results[0].ShopID)
assert.Equal(t, uint(200), results[1].ShopID)
}
// TestDataPermissionCallback_SkipWithContext 测试通过 Context 跳过过滤
@@ -230,6 +211,7 @@ func TestDataPermissionCallback_SkipWithContext(t *testing.T) {
// 创建测试表
type TestModel struct {
ID uint
ShopID uint
Creator uint
Name string
}
@@ -238,21 +220,27 @@ func TestDataPermissionCallback_SkipWithContext(t *testing.T) {
assert.NoError(t, err)
// 插入测试数据
db.Create(&TestModel{ID: 1, Creator: 1, Name: "test1"})
db.Create(&TestModel{ID: 2, Creator: 2, Name: "test2"})
db.Create(&TestModel{ID: 1, ShopID: 100, Creator: 1, Name: "test1"})
db.Create(&TestModel{ID: 2, ShopID: 200, Creator: 2, Name: "test2"})
// 创建 mock AccountStore
mockStore := &mockAccountStore{
subordinateIDs: []uint{1}, // 只有 ID 1
// 创建 mock ShopStore
mockStore := &mockShopStore{
subordinateShopIDs: []uint{100}, // 只有店铺 100
}
// 注册 Callback
err = RegisterDataPermissionCallback(db, mockStore)
assert.NoError(t, err)
// 设置普通用户 context 并跳过过滤
// 设置代理用户 context 并跳过过滤
ctx := context.Background()
ctx = middleware.SetUserContext(ctx, 1, constants.UserTypeAgent, 0)
ctx = middleware.SetUserContext(ctx, &middleware.UserContextInfo{
UserID: 1,
UserType: constants.UserTypeAgent,
ShopID: 100,
EnterpriseID: 0,
CustomerID: 0,
})
ctx = SkipDataPermission(ctx)
// 查询数据
@@ -286,27 +274,134 @@ func TestDataPermissionCallback_WithShopID(t *testing.T) {
db.Create(&TestModel{ID: 2, Creator: 2, ShopID: 100, Name: "test2"})
db.Create(&TestModel{ID: 3, Creator: 2, ShopID: 200, Name: "test3"}) // 不同 shop_id
// 创建 mock AccountStore
mockStore := &mockAccountStore{
subordinateIDs: []uint{1, 2}, // 可以看到 1 和 2
// 创建 mock ShopStore
mockStore := &mockShopStore{
subordinateShopIDs: []uint{100, 200}, // 可以看到店铺 100 和 200
}
// 注册 Callback
err = RegisterDataPermissionCallback(db, mockStore)
assert.NoError(t, err)
// 设置普通用户 context (shop_id = 100)
// 设置代理用户 context (shop_id = 100)
ctx := context.Background()
ctx = middleware.SetUserContext(ctx, 1, constants.UserTypeAgent, 100)
ctx = middleware.SetUserContext(ctx, &middleware.UserContextInfo{
UserID: 1,
UserType: constants.UserTypeAgent,
ShopID: 100,
EnterpriseID: 0,
CustomerID: 0,
})
// 查询数据
var results []TestModel
err = db.WithContext(ctx).Find(&results).Error
assert.NoError(t, err)
// 只能看到 shop_id = 100 的数据
// 应该看到 shop_id = 100 和 200 的所有数据(因为 mockStore 返回了这两个店铺 ID
assert.Equal(t, 3, len(results))
}
// TestDataPermissionCallback_FilterForEnterprise 测试企业用户过滤
func TestDataPermissionCallback_FilterForEnterprise(t *testing.T) {
// 创建内存数据库
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
assert.NoError(t, err)
// 创建测试表(包含 enterprise_id 字段)
type TestModel struct {
ID uint
EnterpriseID uint
Name string
}
err = db.AutoMigrate(&TestModel{})
assert.NoError(t, err)
// 插入测试数据
db.Create(&TestModel{ID: 1, EnterpriseID: 1001, Name: "test1"})
db.Create(&TestModel{ID: 2, EnterpriseID: 1001, Name: "test2"})
db.Create(&TestModel{ID: 3, EnterpriseID: 1002, Name: "test3"})
// 创建 mock ShopStore企业用户不需要但注册时需要
mockStore := &mockShopStore{
subordinateShopIDs: []uint{},
}
// 注册 Callback
err = RegisterDataPermissionCallback(db, mockStore)
assert.NoError(t, err)
// 设置企业用户 context
ctx := context.Background()
ctx = middleware.SetUserContext(ctx, &middleware.UserContextInfo{
UserID: 1,
UserType: constants.UserTypeEnterprise,
ShopID: 0,
EnterpriseID: 1001,
CustomerID: 0,
})
// 查询数据
var results []TestModel
err = db.WithContext(ctx).Find(&results).Error
assert.NoError(t, err)
// 企业用户只能看到自己企业的数据
assert.Equal(t, 2, len(results))
for _, r := range results {
assert.Equal(t, uint(100), r.ShopID)
assert.Equal(t, uint(1001), r.EnterpriseID)
}
}
// TestDataPermissionCallback_FilterForPersonalCustomer 测试个人客户过滤
func TestDataPermissionCallback_FilterForPersonalCustomer(t *testing.T) {
// 创建内存数据库
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
assert.NoError(t, err)
// 创建测试表(包含 creator 字段)
type TestModel struct {
ID uint
Creator uint
Name string
}
err = db.AutoMigrate(&TestModel{})
assert.NoError(t, err)
// 插入测试数据
db.Create(&TestModel{ID: 1, Creator: 1, Name: "test1"})
db.Create(&TestModel{ID: 2, Creator: 2, Name: "test2"})
db.Create(&TestModel{ID: 3, Creator: 1, Name: "test3"})
// 创建 mock ShopStore个人客户不需要但注册时需要
mockStore := &mockShopStore{
subordinateShopIDs: []uint{},
}
// 注册 Callback
err = RegisterDataPermissionCallback(db, mockStore)
assert.NoError(t, err)
// 设置个人客户 context
ctx := context.Background()
ctx = middleware.SetUserContext(ctx, &middleware.UserContextInfo{
UserID: 1,
UserType: constants.UserTypePersonalCustomer,
ShopID: 0,
EnterpriseID: 0,
CustomerID: 1,
})
// 查询数据
var results []TestModel
err = db.WithContext(ctx).Find(&results).Error
assert.NoError(t, err)
// 个人客户只能看到自己创建的数据
assert.Equal(t, 2, len(results))
for _, r := range results {
assert.Equal(t, uint(1), r.Creator)
}
}