feat: 实现 RBAC 权限系统和数据权限控制 (004-rbac-data-permission)
主要功能: - 实现完整的 RBAC 权限系统(账号、角色、权限的多对多关联) - 基于 owner_id + shop_id 的自动数据权限过滤 - 使用 PostgreSQL WITH RECURSIVE 查询下级账号 - Redis 缓存优化下级账号查询性能(30分钟过期) - 支持多租户数据隔离和层级权限管理 技术实现: - 新增 Account、Role、Permission 模型及关联关系表 - 实现 GORM Scopes 自动应用数据权限过滤 - 添加数据库迁移脚本(000002_rbac_data_permission、000003_add_owner_id_shop_id) - 完善错误码定义(1010-1027 为 RBAC 相关错误) - 重构 main.go 采用函数拆分提高可读性 测试覆盖: - 添加 Account、Role、Permission 的集成测试 - 添加数据权限过滤的单元测试和集成测试 - 添加下级账号查询和缓存的单元测试 - 添加 API 回归测试确保向后兼容 文档更新: - 更新 README.md 添加 RBAC 功能说明 - 更新 CLAUDE.md 添加技术栈和开发原则 - 添加 docs/004-rbac-data-permission/ 功能总结和使用指南 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
324
internal/service/account/service.go
Normal file
324
internal/service/account/service.go
Normal file
@@ -0,0 +1,324 @@
|
||||
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, "未授权访问")
|
||||
}
|
||||
|
||||
// 验证非 root 用户必须提供 parent_id
|
||||
if req.UserType != constants.UserTypeRoot && req.ParentID == nil {
|
||||
return nil, errors.New(errors.CodeParentIDRequired, "非 root 用户必须提供上级账号")
|
||||
}
|
||||
|
||||
// 检查用户名唯一性
|
||||
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, "手机号已存在")
|
||||
}
|
||||
|
||||
// 验证 parent_id 存在(如果提供)
|
||||
if req.ParentID != nil {
|
||||
parent, err := s.accountStore.GetByID(ctx, *req.ParentID)
|
||||
if err != nil || parent == nil {
|
||||
return nil, errors.New(errors.CodeInvalidParentID, "上级账号不存在或无效")
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
ParentID: req.ParentID,
|
||||
Status: constants.StatusEnabled,
|
||||
Creator: currentUserID,
|
||||
Updater: currentUserID,
|
||||
}
|
||||
|
||||
if err := s.accountStore.Create(ctx, account); err != nil {
|
||||
return nil, fmt.Errorf("创建账号失败: %w", err)
|
||||
}
|
||||
|
||||
// 清除父账号的下级 ID 缓存
|
||||
if account.ParentID != nil {
|
||||
_ = s.accountStore.ClearSubordinatesCacheForParents(ctx, *account.ParentID)
|
||||
}
|
||||
|
||||
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 {
|
||||
// 检查账号存在
|
||||
account, 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)
|
||||
}
|
||||
|
||||
// 清除该账号和所有上级的下级 ID 缓存
|
||||
_ = s.accountStore.ClearSubordinatesCacheForParents(ctx, id)
|
||||
|
||||
// 如果有上级,也需要清除上级的缓存
|
||||
if account.ParentID != nil {
|
||||
_ = s.accountStore.ClearSubordinatesCacheForParents(ctx, *account.ParentID)
|
||||
}
|
||||
|
||||
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, "未授权访问")
|
||||
}
|
||||
|
||||
// 检查账号存在
|
||||
_, 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)
|
||||
}
|
||||
|
||||
// 验证所有角色存在
|
||||
for _, roleID := range roleIDs {
|
||||
_, 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建关联
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user