本次提交完成了角色权限体系的重构,主要包括: 1. 数据库迁移 - 添加 tb_permission.platform 字段(all/web/h5) - 更新 tb_role.role_type 注释(1=平台角色,2=客户角色) 2. GORM 模型更新 - Permission 模型添加 Platform 字段 - Role 模型更新 RoleType 注释 3. 常量定义 - 新增角色类型常量(RoleTypePlatform, RoleTypeCustomer) - 新增权限端口常量(PlatformAll, PlatformWeb, PlatformH5) - 添加角色类型与用户类型匹配规则函数 4. Store 层实现 - Permission Store 支持按 platform 过滤 - Account Role Store 添加 CountByAccountID 方法 5. Service 层实现 - 角色分配支持类型匹配校验 - 角色分配支持数量限制(超级管理员0个,平台用户无限制,代理/企业1个) - Permission Service 支持 platform 过滤 6. 权限校验中间件 - 实现 RequirePermission、RequireAnyPermission、RequireAllPermissions - 支持 platform 字段过滤 - 支持跳过超级管理员检查 7. 测试用例 - 角色类型匹配规则单元测试 - 角色分配数量限制单元测试 - 权限 platform 过滤单元测试 - 权限校验中间件集成测试(占位) 8. 代码清理 - 删除过时的 subordinate 测试文件 - 移除 Account.ParentID 相关引用 - 更新 DTO 验证规则 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
345 lines
10 KiB
Go
345 lines
10 KiB
Go
package account
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
|
"github.com/break/junhong_cmp_fiber/internal/store"
|
|
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
|
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
|
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
|
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
|
"golang.org/x/crypto/bcrypt"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// Service 账号业务服务
|
|
type Service struct {
|
|
accountStore *postgres.AccountStore
|
|
roleStore *postgres.RoleStore
|
|
accountRoleStore *postgres.AccountRoleStore
|
|
}
|
|
|
|
// New 创建账号服务
|
|
func New(accountStore *postgres.AccountStore, roleStore *postgres.RoleStore, accountRoleStore *postgres.AccountRoleStore) *Service {
|
|
return &Service{
|
|
accountStore: accountStore,
|
|
roleStore: roleStore,
|
|
accountRoleStore: accountRoleStore,
|
|
}
|
|
}
|
|
|
|
// Create 创建账号
|
|
func (s *Service) Create(ctx context.Context, req *model.CreateAccountRequest) (*model.Account, error) {
|
|
// 获取当前用户 ID
|
|
currentUserID := middleware.GetUserIDFromContext(ctx)
|
|
if currentUserID == 0 {
|
|
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
|
}
|
|
|
|
// 验证代理账号必须提供 shop_id
|
|
if req.UserType == constants.UserTypeAgent && req.ShopID == nil {
|
|
return nil, errors.New(errors.CodeInvalidParam, "代理账号必须提供店铺ID")
|
|
}
|
|
|
|
// 验证企业账号必须提供 enterprise_id
|
|
if req.UserType == constants.UserTypeEnterprise && req.EnterpriseID == nil {
|
|
return nil, errors.New(errors.CodeInvalidParam, "企业账号必须提供企业ID")
|
|
}
|
|
|
|
// 检查用户名唯一性
|
|
existing, err := s.accountStore.GetByUsername(ctx, req.Username)
|
|
if err == nil && existing != nil {
|
|
return nil, errors.New(errors.CodeUsernameExists, "用户名已存在")
|
|
}
|
|
|
|
// 检查手机号唯一性
|
|
existing, err = s.accountStore.GetByPhone(ctx, req.Phone)
|
|
if err == nil && existing != nil {
|
|
return nil, errors.New(errors.CodePhoneExists, "手机号已存在")
|
|
}
|
|
|
|
// bcrypt 哈希密码
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("密码哈希失败: %w", err)
|
|
}
|
|
|
|
// 创建账号
|
|
account := &model.Account{
|
|
Username: req.Username,
|
|
Phone: req.Phone,
|
|
Password: string(hashedPassword),
|
|
UserType: req.UserType,
|
|
ShopID: req.ShopID,
|
|
EnterpriseID: req.EnterpriseID,
|
|
Status: constants.StatusEnabled,
|
|
}
|
|
|
|
if err := s.accountStore.Create(ctx, account); err != nil {
|
|
return nil, fmt.Errorf("创建账号失败: %w", err)
|
|
}
|
|
|
|
// TODO: 清除店铺的下级 ID 缓存(需要在 Service 层处理)
|
|
// 由于账号层级关系改为通过 Shop 表维护,这里的缓存清理逻辑已废弃
|
|
|
|
return account, nil
|
|
}
|
|
|
|
// Get 获取账号
|
|
func (s *Service) Get(ctx context.Context, id uint) (*model.Account, error) {
|
|
account, err := s.accountStore.GetByID(ctx, id)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil, errors.New(errors.CodeAccountNotFound, "账号不存在")
|
|
}
|
|
return nil, fmt.Errorf("获取账号失败: %w", err)
|
|
}
|
|
return account, nil
|
|
}
|
|
|
|
// Update 更新账号
|
|
func (s *Service) Update(ctx context.Context, id uint, req *model.UpdateAccountRequest) (*model.Account, error) {
|
|
// 获取当前用户 ID
|
|
currentUserID := middleware.GetUserIDFromContext(ctx)
|
|
if currentUserID == 0 {
|
|
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
|
}
|
|
|
|
// 获取现有账号
|
|
account, err := s.accountStore.GetByID(ctx, id)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil, errors.New(errors.CodeAccountNotFound, "账号不存在")
|
|
}
|
|
return nil, fmt.Errorf("获取账号失败: %w", err)
|
|
}
|
|
|
|
// 更新字段
|
|
if req.Username != nil {
|
|
// 检查新用户名唯一性
|
|
existing, err := s.accountStore.GetByUsername(ctx, *req.Username)
|
|
if err == nil && existing != nil && existing.ID != id {
|
|
return nil, errors.New(errors.CodeUsernameExists, "用户名已存在")
|
|
}
|
|
account.Username = *req.Username
|
|
}
|
|
|
|
if req.Phone != nil {
|
|
// 检查新手机号唯一性
|
|
existing, err := s.accountStore.GetByPhone(ctx, *req.Phone)
|
|
if err == nil && existing != nil && existing.ID != id {
|
|
return nil, errors.New(errors.CodePhoneExists, "手机号已存在")
|
|
}
|
|
account.Phone = *req.Phone
|
|
}
|
|
|
|
if req.Password != nil {
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(*req.Password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("密码哈希失败: %w", err)
|
|
}
|
|
account.Password = string(hashedPassword)
|
|
}
|
|
|
|
if req.Status != nil {
|
|
account.Status = *req.Status
|
|
}
|
|
|
|
account.Updater = currentUserID
|
|
|
|
if err := s.accountStore.Update(ctx, account); err != nil {
|
|
return nil, fmt.Errorf("更新账号失败: %w", err)
|
|
}
|
|
|
|
return account, nil
|
|
}
|
|
|
|
// Delete 软删除账号
|
|
func (s *Service) Delete(ctx context.Context, id uint) error {
|
|
// 检查账号存在
|
|
_, err := s.accountStore.GetByID(ctx, id)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return errors.New(errors.CodeAccountNotFound, "账号不存在")
|
|
}
|
|
return fmt.Errorf("获取账号失败: %w", err)
|
|
}
|
|
|
|
if err := s.accountStore.Delete(ctx, id); err != nil {
|
|
return fmt.Errorf("删除账号失败: %w", err)
|
|
}
|
|
|
|
// TODO: 清除店铺的下级 ID 缓存(需要在 Service 层处理)
|
|
// 由于账号层级关系改为通过 Shop 表维护,这里的缓存清理逻辑已废弃
|
|
_ = s.accountStore.ClearSubordinatesCacheForParents(ctx, id)
|
|
|
|
return nil
|
|
}
|
|
|
|
// List 查询账号列表
|
|
func (s *Service) List(ctx context.Context, req *model.AccountListRequest) ([]*model.Account, int64, error) {
|
|
opts := &store.QueryOptions{
|
|
Page: req.Page,
|
|
PageSize: req.PageSize,
|
|
OrderBy: "id DESC",
|
|
}
|
|
if opts.Page == 0 {
|
|
opts.Page = 1
|
|
}
|
|
if opts.PageSize == 0 {
|
|
opts.PageSize = constants.DefaultPageSize
|
|
}
|
|
|
|
filters := make(map[string]interface{})
|
|
if req.Username != "" {
|
|
filters["username"] = req.Username
|
|
}
|
|
if req.Phone != "" {
|
|
filters["phone"] = req.Phone
|
|
}
|
|
if req.UserType != nil {
|
|
filters["user_type"] = *req.UserType
|
|
}
|
|
if req.Status != nil {
|
|
filters["status"] = *req.Status
|
|
}
|
|
|
|
return s.accountStore.List(ctx, opts, filters)
|
|
}
|
|
|
|
// AssignRoles 为账号分配角色
|
|
func (s *Service) AssignRoles(ctx context.Context, accountID uint, roleIDs []uint) ([]*model.AccountRole, error) {
|
|
// 获取当前用户 ID
|
|
currentUserID := middleware.GetUserIDFromContext(ctx)
|
|
if currentUserID == 0 {
|
|
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
|
}
|
|
|
|
// 检查账号存在
|
|
account, err := s.accountStore.GetByID(ctx, accountID)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil, errors.New(errors.CodeAccountNotFound, "账号不存在")
|
|
}
|
|
return nil, fmt.Errorf("获取账号失败: %w", err)
|
|
}
|
|
|
|
// 检查用户类型是否允许分配角色
|
|
maxRoles := constants.GetMaxRolesForUserType(account.UserType)
|
|
if maxRoles == 0 {
|
|
return nil, errors.New(errors.CodeInvalidParam, "该用户类型不需要分配角色")
|
|
}
|
|
|
|
// 检查角色数量限制
|
|
existingCount, err := s.accountRoleStore.CountByAccountID(ctx, accountID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("统计现有角色数量失败: %w", err)
|
|
}
|
|
|
|
// 计算将要分配的新角色数量(排除已存在的)
|
|
newRoleCount := 0
|
|
for _, roleID := range roleIDs {
|
|
exists, _ := s.accountRoleStore.Exists(ctx, accountID, roleID)
|
|
if !exists {
|
|
newRoleCount++
|
|
}
|
|
}
|
|
|
|
// 检查角色数量限制(-1 表示无限制)
|
|
if maxRoles != -1 && int(existingCount)+newRoleCount > maxRoles {
|
|
return nil, errors.New(errors.CodeInvalidParam, fmt.Sprintf("该用户类型最多只能分配 %d 个角色", maxRoles))
|
|
}
|
|
|
|
// 验证所有角色存在并检查角色类型是否匹配
|
|
for _, roleID := range roleIDs {
|
|
role, err := s.roleStore.GetByID(ctx, roleID)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil, errors.New(errors.CodeRoleNotFound, fmt.Sprintf("角色 %d 不存在", roleID))
|
|
}
|
|
return nil, fmt.Errorf("获取角色失败: %w", err)
|
|
}
|
|
|
|
// 检查角色类型与用户类型是否匹配
|
|
if !constants.IsRoleTypeMatchUserType(role.RoleType, account.UserType) {
|
|
return nil, errors.New(errors.CodeInvalidParam, "角色类型与账号类型不匹配")
|
|
}
|
|
}
|
|
|
|
// 创建关联
|
|
var ars []*model.AccountRole
|
|
for _, roleID := range roleIDs {
|
|
// 检查是否已分配
|
|
exists, _ := s.accountRoleStore.Exists(ctx, accountID, roleID)
|
|
if exists {
|
|
continue // 跳过已存在的关联
|
|
}
|
|
|
|
ar := &model.AccountRole{
|
|
AccountID: accountID,
|
|
RoleID: roleID,
|
|
Status: constants.StatusEnabled,
|
|
Creator: currentUserID,
|
|
Updater: currentUserID,
|
|
}
|
|
if err := s.accountRoleStore.Create(ctx, ar); err != nil {
|
|
return nil, fmt.Errorf("创建账号-角色关联失败: %w", err)
|
|
}
|
|
ars = append(ars, ar)
|
|
}
|
|
|
|
return ars, nil
|
|
}
|
|
|
|
// GetRoles 获取账号的所有角色
|
|
func (s *Service) GetRoles(ctx context.Context, accountID uint) ([]*model.Role, error) {
|
|
// 检查账号存在
|
|
_, err := s.accountStore.GetByID(ctx, accountID)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil, errors.New(errors.CodeAccountNotFound, "账号不存在")
|
|
}
|
|
return nil, fmt.Errorf("获取账号失败: %w", err)
|
|
}
|
|
|
|
// 获取角色 ID 列表
|
|
roleIDs, err := s.accountRoleStore.GetRoleIDsByAccountID(ctx, accountID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("获取账号角色 ID 失败: %w", err)
|
|
}
|
|
|
|
if len(roleIDs) == 0 {
|
|
return []*model.Role{}, nil
|
|
}
|
|
|
|
// 获取角色详情
|
|
return s.roleStore.GetByIDs(ctx, roleIDs)
|
|
}
|
|
|
|
// RemoveRole 移除账号的角色
|
|
func (s *Service) RemoveRole(ctx context.Context, accountID, roleID uint) error {
|
|
// 检查账号存在
|
|
_, err := s.accountStore.GetByID(ctx, accountID)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return errors.New(errors.CodeAccountNotFound, "账号不存在")
|
|
}
|
|
return fmt.Errorf("获取账号失败: %w", err)
|
|
}
|
|
|
|
// 删除关联
|
|
if err := s.accountRoleStore.Delete(ctx, accountID, roleID); err != nil {
|
|
return fmt.Errorf("删除账号-角色关联失败: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidatePassword 验证密码
|
|
func (s *Service) ValidatePassword(plainPassword, hashedPassword string) bool {
|
|
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(plainPassword))
|
|
return err == nil
|
|
}
|