feat(shop-role): 实现店铺角色继承功能和权限检查优化
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m39s

- 新增店铺角色管理 API 和数据模型
- 实现角色继承和权限检查逻辑
- 添加流程测试框架和集成测试
- 更新权限服务和账号管理逻辑
- 添加数据库迁移脚本
- 归档 OpenSpec 变更文档

Ultraworked with Sisyphus
This commit is contained in:
2026-02-03 10:06:13 +08:00
parent bc7e5d6f6d
commit 5a90caa619
61 changed files with 21284 additions and 131 deletions

View File

@@ -0,0 +1,101 @@
package postgres
import (
"context"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
type ShopRoleStore struct {
db *gorm.DB
redisClient *redis.Client
}
func NewShopRoleStore(db *gorm.DB, redisClient *redis.Client) *ShopRoleStore {
return &ShopRoleStore{
db: db,
redisClient: redisClient,
}
}
func (s *ShopRoleStore) Create(ctx context.Context, sr *model.ShopRole) error {
if err := s.db.WithContext(ctx).Create(sr).Error; err != nil {
return err
}
s.clearShopRoleCache(ctx, sr.ShopID)
return nil
}
func (s *ShopRoleStore) BatchCreate(ctx context.Context, srs []*model.ShopRole) error {
if len(srs) == 0 {
return nil
}
if err := s.db.WithContext(ctx).Create(&srs).Error; err != nil {
return err
}
s.clearShopRoleCache(ctx, srs[0].ShopID)
return nil
}
func (s *ShopRoleStore) Delete(ctx context.Context, shopID, roleID uint) error {
if err := s.db.WithContext(ctx).
Where("shop_id = ? AND role_id = ?", shopID, roleID).
Delete(&model.ShopRole{}).Error; err != nil {
return err
}
s.clearShopRoleCache(ctx, shopID)
return nil
}
func (s *ShopRoleStore) DeleteByShopID(ctx context.Context, shopID uint) error {
if err := s.db.WithContext(ctx).
Where("shop_id = ?", shopID).
Delete(&model.ShopRole{}).Error; err != nil {
return err
}
s.clearShopRoleCache(ctx, shopID)
return nil
}
func (s *ShopRoleStore) GetByShopID(ctx context.Context, shopID uint) ([]*model.ShopRole, error) {
var srs []*model.ShopRole
if err := s.db.WithContext(ctx).
Where("shop_id = ?", shopID).
Find(&srs).Error; err != nil {
return nil, err
}
return srs, nil
}
func (s *ShopRoleStore) GetRoleIDsByShopID(ctx context.Context, shopID uint) ([]uint, error) {
var roleIDs []uint
if err := s.db.WithContext(ctx).
Model(&model.ShopRole{}).
Where("shop_id = ?", shopID).
Pluck("role_id", &roleIDs).Error; err != nil {
return nil, err
}
return roleIDs, nil
}
func (s *ShopRoleStore) clearShopRoleCache(ctx context.Context, shopID uint) {
if s.redisClient == nil {
return
}
var accountIDs []uint
if err := s.db.WithContext(ctx).
Model(&model.Account{}).
Where("shop_id = ?", shopID).
Pluck("id", &accountIDs).Error; err != nil {
return
}
for _, accountID := range accountIDs {
key := constants.RedisUserPermissionsKey(accountID)
s.redisClient.Del(ctx, key)
}
}

View File

@@ -0,0 +1,171 @@
package postgres
import (
"context"
"testing"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/tests/testutils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestShopRoleStore_Create(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
store := NewShopRoleStore(tx, rdb)
ctx := context.Background()
sr := &model.ShopRole{
ShopID: 1,
RoleID: 5,
Status: constants.StatusEnabled,
Creator: 1,
Updater: 1,
}
err := store.Create(ctx, sr)
require.NoError(t, err)
assert.NotZero(t, sr.ID)
}
func TestShopRoleStore_BatchCreate(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
store := NewShopRoleStore(tx, rdb)
ctx := context.Background()
srs := []*model.ShopRole{
{
ShopID: 1,
RoleID: 5,
Status: constants.StatusEnabled,
Creator: 1,
Updater: 1,
},
}
err := store.BatchCreate(ctx, srs)
require.NoError(t, err)
assert.NotZero(t, srs[0].ID)
}
func TestShopRoleStore_Delete(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
store := NewShopRoleStore(tx, rdb)
ctx := context.Background()
sr := &model.ShopRole{
ShopID: 1,
RoleID: 5,
Status: constants.StatusEnabled,
Creator: 1,
Updater: 1,
}
require.NoError(t, store.Create(ctx, sr))
err := store.Delete(ctx, 1, 5)
require.NoError(t, err)
results, err := store.GetByShopID(ctx, 1)
require.NoError(t, err)
assert.Empty(t, results)
}
func TestShopRoleStore_DeleteByShopID(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
store := NewShopRoleStore(tx, rdb)
ctx := context.Background()
srs := []*model.ShopRole{
{
ShopID: 1,
RoleID: 5,
Status: constants.StatusEnabled,
Creator: 1,
Updater: 1,
},
{
ShopID: 1,
RoleID: 6,
Status: constants.StatusEnabled,
Creator: 1,
Updater: 1,
},
}
require.NoError(t, store.BatchCreate(ctx, srs))
err := store.DeleteByShopID(ctx, 1)
require.NoError(t, err)
results, err := store.GetByShopID(ctx, 1)
require.NoError(t, err)
assert.Empty(t, results)
}
func TestShopRoleStore_GetByShopID(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
store := NewShopRoleStore(tx, rdb)
ctx := context.Background()
t.Run("查询已分配角色", func(t *testing.T) {
sr := &model.ShopRole{
ShopID: 1,
RoleID: 5,
Status: constants.StatusEnabled,
Creator: 1,
Updater: 1,
}
require.NoError(t, store.Create(ctx, sr))
results, err := store.GetByShopID(ctx, 1)
require.NoError(t, err)
assert.Len(t, results, 1)
assert.Equal(t, uint(1), results[0].ShopID)
assert.Equal(t, uint(5), results[0].RoleID)
})
t.Run("查询未分配角色的店铺", func(t *testing.T) {
results, err := store.GetByShopID(ctx, 999)
require.NoError(t, err)
assert.Empty(t, results)
})
}
func TestShopRoleStore_GetRoleIDsByShopID(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
store := NewShopRoleStore(tx, rdb)
ctx := context.Background()
t.Run("查询已分配角色的店铺", func(t *testing.T) {
sr := &model.ShopRole{
ShopID: 1,
RoleID: 5,
Status: constants.StatusEnabled,
Creator: 1,
Updater: 1,
}
require.NoError(t, store.Create(ctx, sr))
roleIDs, err := store.GetRoleIDsByShopID(ctx, 1)
require.NoError(t, err)
assert.Equal(t, []uint{5}, roleIDs)
})
t.Run("查询未分配角色的店铺", func(t *testing.T) {
roleIDs, err := store.GetRoleIDsByShopID(ctx, 999)
require.NoError(t, err)
assert.Empty(t, roleIDs)
})
}