All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m2s
- 移除 RegisterDataPermissionCallback 和 SkipDataPermission 机制 - 在 Auth 中间件预计算 SubordinateShopIDs 并注入 Context - 新增 ApplyShopFilter/ApplyEnterpriseFilter/ApplyOwnerShopFilter 等 Helper 函数 - 所有 Store 层查询方法显式调用数据权限过滤函数 - 权限检查函数 CanManageShop/CanManageEnterprise 改为从 Context 获取数据 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
414 lines
12 KiB
Go
414 lines
12 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
|
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
|
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
|
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
|
"github.com/break/junhong_cmp_fiber/pkg/auth"
|
|
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
|
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
|
"go.uber.org/zap"
|
|
"golang.org/x/crypto/bcrypt"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type Service struct {
|
|
accountStore *postgres.AccountStore
|
|
accountRoleStore *postgres.AccountRoleStore
|
|
rolePermStore *postgres.RolePermissionStore
|
|
permissionStore *postgres.PermissionStore
|
|
shopStore *postgres.ShopStore
|
|
tokenManager *auth.TokenManager
|
|
logger *zap.Logger
|
|
}
|
|
|
|
func New(
|
|
accountStore *postgres.AccountStore,
|
|
accountRoleStore *postgres.AccountRoleStore,
|
|
rolePermStore *postgres.RolePermissionStore,
|
|
permissionStore *postgres.PermissionStore,
|
|
shopStore *postgres.ShopStore,
|
|
tokenManager *auth.TokenManager,
|
|
logger *zap.Logger,
|
|
) *Service {
|
|
return &Service{
|
|
accountStore: accountStore,
|
|
accountRoleStore: accountRoleStore,
|
|
rolePermStore: rolePermStore,
|
|
permissionStore: permissionStore,
|
|
shopStore: shopStore,
|
|
tokenManager: tokenManager,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
func (s *Service) Login(ctx context.Context, req *dto.LoginRequest, clientIP string) (*dto.LoginResponse, error) {
|
|
account, err := s.accountStore.GetByUsernameOrPhone(ctx, req.Username)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
s.logger.Warn("登录失败:用户名不存在", zap.String("username", req.Username), zap.String("ip", clientIP))
|
|
return nil, errors.New(errors.CodeInvalidCredentials, "用户名或密码错误")
|
|
}
|
|
return nil, errors.Wrap(errors.CodeInternalError, err, "查询账号失败")
|
|
}
|
|
|
|
if err := bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(req.Password)); err != nil {
|
|
s.logger.Warn("登录失败:密码错误", zap.String("username", req.Username), zap.String("ip", clientIP))
|
|
return nil, errors.New(errors.CodeInvalidCredentials, "用户名或密码错误")
|
|
}
|
|
|
|
if account.Status != 1 {
|
|
s.logger.Warn("登录失败:账号已禁用", zap.String("username", req.Username), zap.Uint("user_id", account.ID))
|
|
return nil, errors.New(errors.CodeAccountDisabled, "账号已禁用")
|
|
}
|
|
|
|
// 检查店铺状态(代理账号必须关联店铺且店铺必须启用)
|
|
if account.ShopID != nil && *account.ShopID > 0 {
|
|
shop, err := s.shopStore.GetByID(ctx, *account.ShopID)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
s.logger.Warn("登录失败:关联店铺不存在", zap.String("username", req.Username), zap.Uint("shop_id", *account.ShopID))
|
|
return nil, errors.New(errors.CodeShopNotFound, "关联店铺不存在")
|
|
}
|
|
return nil, errors.Wrap(errors.CodeInternalError, err, "查询店铺失败")
|
|
}
|
|
if shop.Status != constants.StatusEnabled {
|
|
s.logger.Warn("登录失败:关联店铺已禁用", zap.String("username", req.Username), zap.Uint("shop_id", *account.ShopID))
|
|
return nil, errors.New(errors.CodeShopDisabled, "店铺已禁用,无法登录")
|
|
}
|
|
}
|
|
|
|
device := req.Device
|
|
if device == "" {
|
|
device = "web"
|
|
}
|
|
|
|
var shopID, enterpriseID uint
|
|
if account.ShopID != nil {
|
|
shopID = *account.ShopID
|
|
}
|
|
if account.EnterpriseID != nil {
|
|
enterpriseID = *account.EnterpriseID
|
|
}
|
|
|
|
tokenInfo := &auth.TokenInfo{
|
|
UserID: account.ID,
|
|
UserType: account.UserType,
|
|
ShopID: shopID,
|
|
EnterpriseID: enterpriseID,
|
|
Username: account.Username,
|
|
Device: device,
|
|
IP: clientIP,
|
|
}
|
|
|
|
accessToken, refreshToken, err := s.tokenManager.GenerateTokenPair(ctx, tokenInfo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
permissions, menus, buttons, err := s.getUserPermissionsAndMenus(ctx, account.ID, account.UserType, device)
|
|
if err != nil {
|
|
s.logger.Error("查询用户权限失败", zap.Uint("user_id", account.ID), zap.Error(err))
|
|
permissions = []string{}
|
|
menus = []dto.MenuNode{}
|
|
buttons = []string{}
|
|
}
|
|
|
|
userInfo := s.buildUserInfo(account)
|
|
|
|
s.logger.Info("用户登录成功",
|
|
zap.Uint("user_id", account.ID),
|
|
zap.String("username", account.Username),
|
|
zap.String("device", device),
|
|
zap.String("ip", clientIP),
|
|
)
|
|
|
|
return &dto.LoginResponse{
|
|
AccessToken: accessToken,
|
|
RefreshToken: refreshToken,
|
|
ExpiresIn: int64(constants.DefaultAccessTokenTTL.Seconds()),
|
|
User: userInfo,
|
|
Permissions: permissions,
|
|
Menus: menus,
|
|
Buttons: buttons,
|
|
}, nil
|
|
}
|
|
|
|
func (s *Service) Logout(ctx context.Context, accessToken, refreshToken string) error {
|
|
if err := s.tokenManager.RevokeToken(ctx, accessToken); err != nil {
|
|
return err
|
|
}
|
|
|
|
if refreshToken != "" {
|
|
if err := s.tokenManager.RevokeToken(ctx, refreshToken); err != nil {
|
|
s.logger.Warn("撤销 refresh token 失败", zap.Error(err))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) RefreshToken(ctx context.Context, refreshToken string) (string, error) {
|
|
return s.tokenManager.RefreshAccessToken(ctx, refreshToken)
|
|
}
|
|
|
|
func (s *Service) GetCurrentUser(ctx context.Context, userID uint) (*dto.UserInfo, []string, error) {
|
|
account, err := s.accountStore.GetByID(ctx, userID)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil, nil, errors.New(errors.CodeAccountNotFound, "账号不存在")
|
|
}
|
|
return nil, nil, errors.Wrap(errors.CodeInternalError, err, "查询账号失败")
|
|
}
|
|
|
|
permissions, err := s.getUserPermissions(ctx, userID)
|
|
if err != nil {
|
|
s.logger.Error("查询用户权限失败", zap.Uint("user_id", userID), zap.Error(err))
|
|
permissions = []string{}
|
|
}
|
|
|
|
userInfo := s.buildUserInfo(account)
|
|
|
|
return &userInfo, permissions, nil
|
|
}
|
|
|
|
func (s *Service) ChangePassword(ctx context.Context, userID uint, oldPassword, newPassword string) error {
|
|
account, err := s.accountStore.GetByID(ctx, userID)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return errors.New(errors.CodeAccountNotFound, "账号不存在")
|
|
}
|
|
return errors.Wrap(errors.CodeInternalError, err, "查询账号失败")
|
|
}
|
|
|
|
if err := bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(oldPassword)); err != nil {
|
|
return errors.New(errors.CodeInvalidOldPassword, "旧密码错误")
|
|
}
|
|
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return errors.Wrap(errors.CodeInternalError, err, "密码加密失败")
|
|
}
|
|
|
|
if err := s.accountStore.UpdatePassword(ctx, userID, string(hashedPassword), userID); err != nil {
|
|
return errors.Wrap(errors.CodeInternalError, err, "更新密码失败")
|
|
}
|
|
|
|
if err := s.tokenManager.RevokeAllUserTokens(ctx, userID); err != nil {
|
|
s.logger.Warn("撤销用户所有 token 失败", zap.Uint("user_id", userID), zap.Error(err))
|
|
}
|
|
|
|
s.logger.Info("用户修改密码成功", zap.Uint("user_id", userID))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) getUserPermissions(ctx context.Context, userID uint) ([]string, error) {
|
|
accountRoles, err := s.accountRoleStore.GetByAccountID(ctx, userID)
|
|
if err != nil {
|
|
return nil, errors.Wrap(errors.CodeInternalError, err, "查询用户角色失败")
|
|
}
|
|
|
|
if len(accountRoles) == 0 {
|
|
return []string{}, nil
|
|
}
|
|
|
|
roleIDs := make([]uint, 0, len(accountRoles))
|
|
for _, ar := range accountRoles {
|
|
roleIDs = append(roleIDs, ar.RoleID)
|
|
}
|
|
|
|
permIDs, err := s.rolePermStore.GetPermIDsByRoleIDs(ctx, roleIDs)
|
|
if err != nil {
|
|
return nil, errors.Wrap(errors.CodeInternalError, err, "查询角色权限失败")
|
|
}
|
|
|
|
if len(permIDs) == 0 {
|
|
return []string{}, nil
|
|
}
|
|
|
|
permissions, err := s.permissionStore.GetByIDs(ctx, permIDs)
|
|
if err != nil {
|
|
return nil, errors.Wrap(errors.CodeInternalError, err, "查询权限详情失败")
|
|
}
|
|
|
|
permCodes := make([]string, 0, len(permissions))
|
|
for _, perm := range permissions {
|
|
permCodes = append(permCodes, perm.PermCode)
|
|
}
|
|
|
|
return permCodes, nil
|
|
}
|
|
|
|
func (s *Service) buildUserInfo(account *model.Account) dto.UserInfo {
|
|
userTypeName := s.getUserTypeName(account.UserType)
|
|
|
|
var shopID, enterpriseID uint
|
|
if account.ShopID != nil {
|
|
shopID = *account.ShopID
|
|
}
|
|
if account.EnterpriseID != nil {
|
|
enterpriseID = *account.EnterpriseID
|
|
}
|
|
|
|
return dto.UserInfo{
|
|
ID: account.ID,
|
|
Username: account.Username,
|
|
Phone: account.Phone,
|
|
UserType: account.UserType,
|
|
UserTypeName: userTypeName,
|
|
ShopID: shopID,
|
|
EnterpriseID: enterpriseID,
|
|
}
|
|
}
|
|
|
|
func (s *Service) getUserTypeName(userType int) string {
|
|
switch userType {
|
|
case constants.UserTypeSuperAdmin:
|
|
return "超级管理员"
|
|
case constants.UserTypePlatform:
|
|
return "平台用户"
|
|
case constants.UserTypeAgent:
|
|
return "代理账号"
|
|
case constants.UserTypeEnterprise:
|
|
return "企业账号"
|
|
default:
|
|
return "未知"
|
|
}
|
|
}
|
|
|
|
func (s *Service) getUserPermissionsAndMenus(ctx context.Context, userID uint, userType int, device string) ([]string, []dto.MenuNode, []string, error) {
|
|
if userType == constants.UserTypeSuperAdmin {
|
|
return s.getAllPermissionsForSuperAdmin(ctx, device)
|
|
}
|
|
|
|
accountRoles, err := s.accountRoleStore.GetByAccountID(ctx, userID)
|
|
if err != nil {
|
|
return nil, nil, nil, errors.Wrap(errors.CodeInternalError, err, "查询用户角色失败")
|
|
}
|
|
|
|
if len(accountRoles) == 0 {
|
|
return []string{}, []dto.MenuNode{}, []string{}, nil
|
|
}
|
|
|
|
roleIDs := make([]uint, 0, len(accountRoles))
|
|
for _, ar := range accountRoles {
|
|
roleIDs = append(roleIDs, ar.RoleID)
|
|
}
|
|
|
|
permIDs, err := s.rolePermStore.GetPermIDsByRoleIDs(ctx, roleIDs)
|
|
if err != nil {
|
|
return nil, nil, nil, errors.Wrap(errors.CodeInternalError, err, "查询角色权限失败")
|
|
}
|
|
|
|
if len(permIDs) == 0 {
|
|
return []string{}, []dto.MenuNode{}, []string{}, nil
|
|
}
|
|
|
|
permissions, err := s.permissionStore.GetByIDs(ctx, permIDs)
|
|
if err != nil {
|
|
return nil, nil, nil, errors.Wrap(errors.CodeInternalError, err, "查询权限详情失败")
|
|
}
|
|
|
|
return s.classifyPermissions(permissions, device)
|
|
}
|
|
|
|
func (s *Service) getAllPermissionsForSuperAdmin(ctx context.Context, device string) ([]string, []dto.MenuNode, []string, error) {
|
|
permissions, err := s.permissionStore.GetAll(ctx, nil, nil)
|
|
if err != nil {
|
|
return nil, nil, nil, errors.Wrap(errors.CodeInternalError, err, "查询所有权限失败")
|
|
}
|
|
|
|
return s.classifyPermissions(permissions, device)
|
|
}
|
|
|
|
func (s *Service) classifyPermissions(permissions []*model.Permission, device string) ([]string, []dto.MenuNode, []string, error) {
|
|
var menuPerms []*model.Permission
|
|
var buttonCodes []string
|
|
var allCodes []string
|
|
|
|
for _, perm := range permissions {
|
|
if perm.Status != constants.StatusEnabled {
|
|
continue
|
|
}
|
|
|
|
if perm.Platform != constants.PlatformAll && perm.Platform != device {
|
|
continue
|
|
}
|
|
|
|
allCodes = append(allCodes, perm.PermCode)
|
|
|
|
if perm.PermType == constants.PermissionTypeMenu {
|
|
menuPerms = append(menuPerms, perm)
|
|
} else if perm.PermType == constants.PermissionTypeButton {
|
|
buttonCodes = append(buttonCodes, perm.PermCode)
|
|
}
|
|
}
|
|
|
|
menuTree := s.buildMenuTree(menuPerms)
|
|
|
|
return allCodes, menuTree, buttonCodes, nil
|
|
}
|
|
|
|
func (s *Service) buildMenuTree(permissions []*model.Permission) []dto.MenuNode {
|
|
if len(permissions) == 0 {
|
|
return []dto.MenuNode{}
|
|
}
|
|
|
|
permMap := make(map[uint]*model.Permission)
|
|
for _, p := range permissions {
|
|
permMap[p.ID] = p
|
|
}
|
|
|
|
var roots []dto.MenuNode
|
|
for _, p := range permissions {
|
|
if p.ParentID == nil || *p.ParentID == 0 {
|
|
roots = append(roots, s.buildNode(p, permMap))
|
|
} else if _, ok := permMap[*p.ParentID]; !ok {
|
|
s.logger.Warn("检测到孤儿节点",
|
|
zap.Uint("child_id", p.ID),
|
|
zap.String("perm_code", p.PermCode),
|
|
zap.Uint("parent_id", *p.ParentID),
|
|
)
|
|
roots = append(roots, s.buildNode(p, permMap))
|
|
}
|
|
}
|
|
|
|
s.sortMenuNodes(roots)
|
|
return roots
|
|
}
|
|
|
|
func (s *Service) buildNode(perm *model.Permission, permMap map[uint]*model.Permission) dto.MenuNode {
|
|
node := dto.MenuNode{
|
|
ID: perm.ID,
|
|
PermCode: perm.PermCode,
|
|
Name: perm.PermName,
|
|
URL: perm.URL,
|
|
Sort: perm.Sort,
|
|
Children: []dto.MenuNode{},
|
|
}
|
|
|
|
for _, p := range permMap {
|
|
if p.ParentID != nil && *p.ParentID == perm.ID {
|
|
node.Children = append(node.Children, s.buildNode(p, permMap))
|
|
}
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
func (s *Service) sortMenuNodes(nodes []dto.MenuNode) {
|
|
sort.Slice(nodes, func(i, j int) bool {
|
|
return nodes[i].Sort < nodes[j].Sort
|
|
})
|
|
|
|
for i := range nodes {
|
|
if len(nodes[i].Children) > 0 {
|
|
s.sortMenuNodes(nodes[i].Children)
|
|
}
|
|
}
|
|
}
|