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

@@ -15,14 +15,23 @@ import (
)
type Service struct {
shopStore *postgres.ShopStore
accountStore *postgres.AccountStore
shopStore *postgres.ShopStore
accountStore *postgres.AccountStore
shopRoleStore *postgres.ShopRoleStore
roleStore *postgres.RoleStore
}
func New(shopStore *postgres.ShopStore, accountStore *postgres.AccountStore) *Service {
func New(
shopStore *postgres.ShopStore,
accountStore *postgres.AccountStore,
shopRoleStore *postgres.ShopRoleStore,
roleStore *postgres.RoleStore,
) *Service {
return &Service{
shopStore: shopStore,
accountStore: accountStore,
shopStore: shopStore,
accountStore: accountStore,
shopRoleStore: shopRoleStore,
roleStore: roleStore,
}
}

View File

@@ -0,0 +1,145 @@
package shop
import (
"context"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
)
func (s *Service) AssignRolesToShop(ctx context.Context, shopID uint, roleIDs []uint) ([]*model.ShopRole, error) {
if err := middleware.CanManageShop(ctx, shopID, s.shopStore); err != nil {
return nil, err
}
shop, err := s.shopStore.GetByID(ctx, shopID)
if err != nil {
return nil, errors.New(errors.CodeNotFound, "店铺不存在")
}
currentUserID := middleware.GetUserIDFromContext(ctx)
if len(roleIDs) == 0 {
if err := s.shopRoleStore.DeleteByShopID(ctx, shopID); err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "清空店铺角色失败")
}
return []*model.ShopRole{}, nil
}
roles, err := s.roleStore.GetByIDs(ctx, roleIDs)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询角色失败")
}
if len(roles) != len(roleIDs) {
return nil, errors.New(errors.CodeNotFound, "部分角色不存在")
}
for _, role := range roles {
if role.RoleType != constants.RoleTypeCustomer {
return nil, errors.New(errors.CodeInvalidParam, "店铺只能分配客户角色")
}
if role.Status != constants.StatusEnabled {
return nil, errors.New(errors.CodeInvalidParam, "角色已禁用")
}
}
if err := s.shopRoleStore.DeleteByShopID(ctx, shopID); err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "删除现有店铺角色失败")
}
shopRoles := make([]*model.ShopRole, 0, len(roleIDs))
for _, roleID := range roleIDs {
shopRole := &model.ShopRole{
ShopID: shop.ID,
RoleID: roleID,
Status: constants.StatusEnabled,
Creator: currentUserID,
Updater: currentUserID,
}
shopRoles = append(shopRoles, shopRole)
}
if err := s.shopRoleStore.BatchCreate(ctx, shopRoles); err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "批量创建店铺角色失败")
}
return shopRoles, nil
}
func (s *Service) GetShopRoles(ctx context.Context, shopID uint) (*dto.ShopRolesResponse, error) {
if err := middleware.CanManageShop(ctx, shopID, s.shopStore); err != nil {
return nil, err
}
_, err := s.shopStore.GetByID(ctx, shopID)
if err != nil {
return nil, errors.New(errors.CodeNotFound, "店铺不存在")
}
shopRoles, err := s.shopRoleStore.GetByShopID(ctx, shopID)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询店铺角色失败")
}
if len(shopRoles) == 0 {
return &dto.ShopRolesResponse{
ShopID: shopID,
Roles: []*dto.ShopRoleResponse{},
}, nil
}
roleIDs := make([]uint, 0, len(shopRoles))
for _, sr := range shopRoles {
roleIDs = append(roleIDs, sr.RoleID)
}
roles, err := s.roleStore.GetByIDs(ctx, roleIDs)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询角色详情失败")
}
roleMap := make(map[uint]*model.Role)
for _, role := range roles {
roleMap[role.ID] = role
}
responses := make([]*dto.ShopRoleResponse, 0, len(shopRoles))
for _, sr := range shopRoles {
role, exists := roleMap[sr.RoleID]
if !exists {
continue
}
responses = append(responses, &dto.ShopRoleResponse{
ShopID: sr.ShopID,
RoleID: sr.RoleID,
RoleName: role.RoleName,
RoleDesc: role.RoleDesc,
Status: sr.Status,
})
}
return &dto.ShopRolesResponse{
ShopID: shopID,
Roles: responses,
}, nil
}
func (s *Service) DeleteShopRole(ctx context.Context, shopID, roleID uint) error {
if err := middleware.CanManageShop(ctx, shopID, s.shopStore); err != nil {
return err
}
_, err := s.shopStore.GetByID(ctx, shopID)
if err != nil {
return errors.New(errors.CodeNotFound, "店铺不存在")
}
if err := s.shopRoleStore.Delete(ctx, shopID, roleID); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "删除店铺角色失败")
}
return nil
}

View File

@@ -0,0 +1,243 @@
package shop
import (
"context"
"testing"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
"github.com/break/junhong_cmp_fiber/tests/testutils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAssignRolesToShop(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
shopStore := postgres.NewShopStore(tx, rdb)
accountStore := postgres.NewAccountStore(tx, rdb)
shopRoleStore := postgres.NewShopRoleStore(tx, rdb)
roleStore := postgres.NewRoleStore(tx)
service := New(shopStore, accountStore, shopRoleStore, roleStore)
shop := &model.Shop{
ShopName: "测试店铺",
ShopCode: "TEST_SHOP_001",
Level: 1,
Status: constants.StatusEnabled,
}
require.NoError(t, tx.Create(shop).Error)
role := &model.Role{
RoleName: "代理店长",
RoleType: constants.RoleTypeCustomer,
Status: constants.StatusEnabled,
}
require.NoError(t, roleStore.Create(context.Background(), role))
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
UserID: 1,
UserType: constants.UserTypeSuperAdmin,
})
t.Run("成功分配单个角色", func(t *testing.T) {
result, err := service.AssignRolesToShop(ctx, shop.ID, []uint{role.ID})
require.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, shop.ID, result[0].ShopID)
assert.Equal(t, role.ID, result[0].RoleID)
})
t.Run("清空所有角色", func(t *testing.T) {
result, err := service.AssignRolesToShop(ctx, shop.ID, []uint{})
require.NoError(t, err)
assert.Empty(t, result)
roles, err := service.GetShopRoles(ctx, shop.ID)
require.NoError(t, err)
assert.Empty(t, roles.Roles)
})
t.Run("替换现有角色", func(t *testing.T) {
require.NoError(t, shopRoleStore.Create(ctx, &model.ShopRole{
ShopID: shop.ID,
RoleID: role.ID,
Status: constants.StatusEnabled,
Creator: 1,
Updater: 1,
}))
newRole := &model.Role{
RoleName: "代理经理",
RoleType: constants.RoleTypeCustomer,
Status: constants.StatusEnabled,
}
require.NoError(t, roleStore.Create(ctx, newRole))
result, err := service.AssignRolesToShop(ctx, shop.ID, []uint{newRole.ID})
require.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, newRole.ID, result[0].RoleID)
})
t.Run("角色类型校验失败", func(t *testing.T) {
platformRole := &model.Role{
RoleName: "平台角色",
RoleType: constants.RoleTypePlatform,
Status: constants.StatusEnabled,
}
require.NoError(t, roleStore.Create(ctx, platformRole))
_, err := service.AssignRolesToShop(ctx, shop.ID, []uint{platformRole.ID})
require.Error(t, err)
assert.Contains(t, err.Error(), "店铺只能分配客户角色")
})
t.Run("角色不存在", func(t *testing.T) {
_, err := service.AssignRolesToShop(ctx, shop.ID, []uint{99999})
require.Error(t, err)
assert.Contains(t, err.Error(), "部分角色不存在")
})
t.Run("店铺不存在", func(t *testing.T) {
_, err := service.AssignRolesToShop(ctx, 99999, []uint{role.ID})
require.Error(t, err)
assert.Contains(t, err.Error(), "店铺不存在")
})
}
func TestGetShopRoles(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
shopStore := postgres.NewShopStore(tx, rdb)
accountStore := postgres.NewAccountStore(tx, rdb)
shopRoleStore := postgres.NewShopRoleStore(tx, rdb)
roleStore := postgres.NewRoleStore(tx)
service := New(shopStore, accountStore, shopRoleStore, roleStore)
shop := &model.Shop{
ShopName: "测试店铺2",
ShopCode: "TEST_SHOP_002",
Level: 1,
Status: constants.StatusEnabled,
}
require.NoError(t, tx.Create(shop).Error)
role := &model.Role{
RoleName: "代理店长",
RoleType: constants.RoleTypeCustomer,
Status: constants.StatusEnabled,
}
require.NoError(t, roleStore.Create(context.Background(), role))
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
UserID: 1,
UserType: constants.UserTypeSuperAdmin,
})
t.Run("查询已分配角色", func(t *testing.T) {
require.NoError(t, shopRoleStore.Create(ctx, &model.ShopRole{
ShopID: shop.ID,
RoleID: role.ID,
Status: constants.StatusEnabled,
Creator: 1,
Updater: 1,
}))
result, err := service.GetShopRoles(ctx, shop.ID)
require.NoError(t, err)
assert.Len(t, result.Roles, 1)
assert.Equal(t, shop.ID, result.ShopID)
assert.Equal(t, role.ID, result.Roles[0].RoleID)
assert.Equal(t, "代理店长", result.Roles[0].RoleName)
})
t.Run("查询未分配角色的店铺", func(t *testing.T) {
emptyShop := &model.Shop{
ShopName: "空店铺",
ShopCode: "EMPTY_SHOP",
Level: 1,
Status: constants.StatusEnabled,
}
require.NoError(t, tx.Create(emptyShop).Error)
result, err := service.GetShopRoles(ctx, emptyShop.ID)
require.NoError(t, err)
assert.Empty(t, result.Roles)
})
t.Run("店铺不存在", func(t *testing.T) {
_, err := service.GetShopRoles(ctx, 99999)
require.Error(t, err)
assert.Contains(t, err.Error(), "店铺不存在")
})
}
func TestDeleteShopRole(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
shopStore := postgres.NewShopStore(tx, rdb)
accountStore := postgres.NewAccountStore(tx, rdb)
shopRoleStore := postgres.NewShopRoleStore(tx, rdb)
roleStore := postgres.NewRoleStore(tx)
service := New(shopStore, accountStore, shopRoleStore, roleStore)
shop := &model.Shop{
ShopName: "测试店铺3",
ShopCode: "TEST_SHOP_003",
Level: 1,
Status: constants.StatusEnabled,
}
require.NoError(t, tx.Create(shop).Error)
role := &model.Role{
RoleName: "代理店长",
RoleType: constants.RoleTypeCustomer,
Status: constants.StatusEnabled,
}
require.NoError(t, roleStore.Create(context.Background(), role))
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
UserID: 1,
UserType: constants.UserTypeSuperAdmin,
})
t.Run("成功删除角色", func(t *testing.T) {
require.NoError(t, shopRoleStore.Create(ctx, &model.ShopRole{
ShopID: shop.ID,
RoleID: role.ID,
Status: constants.StatusEnabled,
Creator: 1,
Updater: 1,
}))
err := service.DeleteShopRole(ctx, shop.ID, role.ID)
require.NoError(t, err)
result, err := service.GetShopRoles(ctx, shop.ID)
require.NoError(t, err)
assert.Empty(t, result.Roles)
})
t.Run("删除不存在的角色关联(幂等)", func(t *testing.T) {
err := service.DeleteShopRole(ctx, shop.ID, role.ID)
require.NoError(t, err)
})
t.Run("店铺不存在", func(t *testing.T) {
err := service.DeleteShopRole(ctx, 99999, role.ID)
require.Error(t, err)
assert.Contains(t, err.Error(), "店铺不存在")
})
}