All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m31s
1198 lines
45 KiB
Go
1198 lines
45 KiB
Go
// Package account 提供账号管理的业务逻辑服务
|
||
// 包含账号创建、查询、更新、删除、密码管理等功能
|
||
package account
|
||
|
||
import (
|
||
"context"
|
||
stdErrors "errors"
|
||
"fmt"
|
||
"slices"
|
||
"strconv"
|
||
"strings"
|
||
|
||
accessauditapp "github.com/break/junhong_cmp_fiber/internal/application/accessaudit"
|
||
accountauditapp "github.com/break/junhong_cmp_fiber/internal/application/accountaudit"
|
||
"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"
|
||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||
"github.com/break/junhong_cmp_fiber/pkg/auditfailure"
|
||
pkgAuth "github.com/break/junhong_cmp_fiber/pkg/auth"
|
||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||
"github.com/break/junhong_cmp_fiber/pkg/logger"
|
||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||
"github.com/jackc/pgx/v5/pgconn"
|
||
"github.com/redis/go-redis/v9"
|
||
"go.uber.org/zap"
|
||
"golang.org/x/crypto/bcrypt"
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/clause"
|
||
)
|
||
|
||
// ShopStoreInterface 店铺存储接口(仅用于获取店铺信息)
|
||
type ShopStoreInterface interface {
|
||
GetByIDs(ctx context.Context, ids []uint) ([]*model.Shop, error)
|
||
}
|
||
|
||
// Service 账号业务服务
|
||
type Service struct {
|
||
db *gorm.DB
|
||
lifecycleAudit accountauditapp.Writer
|
||
accessAudit accessauditapp.Writer
|
||
redisClient *redis.Client
|
||
accountStore *postgres.AccountStore
|
||
roleStore *postgres.RoleStore
|
||
accountRoleStore *postgres.AccountRoleStore
|
||
shopRoleStore *postgres.ShopRoleStore
|
||
shopStore ShopStoreInterface
|
||
enterpriseStore middleware.EnterpriseStoreInterface
|
||
wecomMembers WeComMemberFinder
|
||
tokenManager *pkgAuth.TokenManager
|
||
}
|
||
|
||
// SetLifecycleAudit 注入账号生命周期事务和统一审计边界。
|
||
func (s *Service) SetLifecycleAudit(db *gorm.DB, writer accountauditapp.Writer) {
|
||
s.db = db
|
||
s.lifecycleAudit = writer
|
||
}
|
||
|
||
// SetAccessAudit 注入账号角色授权的事务、缓存和统一审计边界。
|
||
func (s *Service) SetAccessAudit(db *gorm.DB, redisClient *redis.Client, writer accessauditapp.Writer) {
|
||
s.db = db
|
||
s.redisClient = redisClient
|
||
s.accessAudit = writer
|
||
}
|
||
|
||
// SetTokenManager 注入改密后撤销现有会话所需的令牌管理器。
|
||
func (s *Service) SetTokenManager(tokenManager *pkgAuth.TokenManager) {
|
||
s.tokenManager = tokenManager
|
||
}
|
||
|
||
// WeComMemberFinder 定义账号绑定时校验应用可见成员的边界。
|
||
type WeComMemberFinder interface {
|
||
GetVisible(ctx context.Context, applicationID uint, userID string) (*model.WeComMember, error)
|
||
}
|
||
|
||
// New 创建账号服务
|
||
func New(
|
||
accountStore *postgres.AccountStore,
|
||
roleStore *postgres.RoleStore,
|
||
accountRoleStore *postgres.AccountRoleStore,
|
||
shopRoleStore *postgres.ShopRoleStore,
|
||
shopStore ShopStoreInterface,
|
||
enterpriseStore middleware.EnterpriseStoreInterface,
|
||
) *Service {
|
||
return &Service{
|
||
accountStore: accountStore,
|
||
roleStore: roleStore,
|
||
accountRoleStore: accountRoleStore,
|
||
shopRoleStore: shopRoleStore,
|
||
shopStore: shopStore,
|
||
enterpriseStore: enterpriseStore,
|
||
}
|
||
}
|
||
|
||
// SetWeComMemberFinder 注入企业微信应用可见成员查询边界。
|
||
func (s *Service) SetWeComMemberFinder(finder WeComMemberFinder) {
|
||
s.wecomMembers = finder
|
||
}
|
||
|
||
// Create 创建账号
|
||
func (s *Service) Create(ctx context.Context, req *dto.CreateAccountRequest) (*model.Account, error) {
|
||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||
if currentUserID == 0 {
|
||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||
}
|
||
|
||
userType := middleware.GetUserTypeFromContext(ctx)
|
||
|
||
if userType == constants.UserTypeEnterprise {
|
||
return nil, errors.New(errors.CodeForbidden, "企业账号不允许创建账号")
|
||
}
|
||
|
||
if userType == constants.UserTypeAgent && req.UserType == constants.UserTypePlatform {
|
||
return nil, errors.New(errors.CodeForbidden, "无权限创建平台账号")
|
||
}
|
||
|
||
if req.UserType == constants.UserTypeAgent && req.ShopID == nil {
|
||
return nil, errors.New(errors.CodeInvalidParam, "代理账号必须提供店铺ID")
|
||
}
|
||
|
||
if req.UserType == constants.UserTypeEnterprise && req.EnterpriseID == nil {
|
||
return nil, errors.New(errors.CodeInvalidParam, "企业账号必须提供企业ID")
|
||
}
|
||
|
||
if req.UserType == constants.UserTypeAgent && req.ShopID != nil {
|
||
if err := middleware.CanManageShop(ctx, *req.ShopID); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
if req.UserType == constants.UserTypeEnterprise && req.EnterpriseID != nil {
|
||
if err := middleware.CanManageEnterprise(ctx, *req.EnterpriseID, s.enterpriseStore); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
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, "手机号已存在")
|
||
}
|
||
|
||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, 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.runLifecycleTransaction(ctx, func(tx *gorm.DB) error {
|
||
if err := postgres.NewAccountStore(tx, nil).Create(ctx, account); err != nil {
|
||
return err
|
||
}
|
||
if req.UserType == constants.UserTypeAgent && req.ShopID != nil {
|
||
var roleIDs []uint
|
||
_ = tx.Transaction(func(roleTx *gorm.DB) error {
|
||
var err error
|
||
roleIDs, err = postgres.NewShopRoleStore(roleTx, nil).GetRoleIDsByShopID(ctx, *req.ShopID)
|
||
return err
|
||
})
|
||
for _, roleID := range roleIDs {
|
||
accountRole := &model.AccountRole{AccountID: account.ID, RoleID: roleID, Status: constants.StatusEnabled, Creator: currentUserID, Updater: currentUserID}
|
||
_ = tx.Transaction(func(roleTx *gorm.DB) error {
|
||
return postgres.NewAccountRoleStore(roleTx, nil).Create(ctx, accountRole)
|
||
})
|
||
}
|
||
}
|
||
shop, enterprise, roles, err := loadLifecycleResources(ctx, tx, account)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return s.lifecycleAudit.WriteAccountLifecycle(ctx, tx, accountauditapp.LifecycleAudit{
|
||
ActionCode: constants.AuditActionAccountCreated, Summary: "创建账号", Result: constants.AuditResultSuccess,
|
||
Account: account, Shop: shop, Enterprise: enterprise, Roles: roles, AfterData: accountLifecycleData(account),
|
||
})
|
||
}); err != nil {
|
||
account.ID = 0
|
||
s.recordLifecycleFailure(ctx, constants.AuditActionAccountCreated, "创建账号失败", constants.AuditResultFailed, account, nil, nil, err)
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "创建账号失败")
|
||
}
|
||
|
||
return account, nil
|
||
}
|
||
|
||
// Get 获取账号
|
||
func (s *Service) Get(ctx context.Context, id uint) (*dto.AccountResponse, error) {
|
||
account, err := s.accountStore.GetByID(ctx, id)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, errors.New(errors.CodeAccountNotFound, "账号不存在")
|
||
}
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "获取账号失败")
|
||
}
|
||
accounts := []*model.Account{account}
|
||
return s.toAccountResponse(account, s.loadShopNames(ctx, accounts), s.loadEnterpriseNames(ctx, accounts)), nil
|
||
}
|
||
|
||
// BindWeCom 将系统账号绑定到管理员明确选择的企微应用可见成员。
|
||
func (s *Service) BindWeCom(ctx context.Context, accountID uint, request dto.BindAccountWeComRequest) (*dto.AccountResponse, error) {
|
||
operatorID := middleware.GetUserIDFromContext(ctx)
|
||
operatorType := middleware.GetUserTypeFromContext(ctx)
|
||
if operatorID == 0 {
|
||
return nil, errors.New(errors.CodeUnauthorized)
|
||
}
|
||
if operatorType != constants.UserTypeSuperAdmin && operatorType != constants.UserTypePlatform {
|
||
return nil, errors.New(errors.CodeForbidden)
|
||
}
|
||
if s.wecomMembers == nil || request.ApplicationID == 0 || strings.TrimSpace(request.UserID) == "" {
|
||
return nil, errors.New(errors.CodeInvalidParam)
|
||
}
|
||
account, err := s.accountStore.GetByID(ctx, accountID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||
}
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询待绑定账号失败")
|
||
}
|
||
member, err := s.wecomMembers.GetVisible(ctx, request.ApplicationID, request.UserID)
|
||
if err != nil {
|
||
s.recordSecurityFailure(ctx, constants.AuditActionAccountWeComBound, "绑定账号企业微信身份失败", constants.AuditResultFailed, account, map[string]any{
|
||
"account_id": account.ID, "auth_method": "wecom", "state": "failed",
|
||
}, nil, nil, err)
|
||
return nil, err
|
||
}
|
||
beforeData := model.JSONB{
|
||
"wecom_corp_id": account.WeComCorpID, "wecom_userid": account.WeComUserID,
|
||
"wecom_name": account.WeComName,
|
||
}
|
||
afterData := map[string]any{
|
||
"wecom_corp_id": member.CorpID, "wecom_userid": member.UserID, "wecom_name": member.Name,
|
||
}
|
||
updatedAccount := *account
|
||
updatedAccount.WeComCorpID = member.CorpID
|
||
updatedAccount.WeComUserID = member.UserID
|
||
updatedAccount.WeComName = member.Name
|
||
updatedAccount.Updater = operatorID
|
||
if err := s.runLifecycleTransaction(ctx, func(tx *gorm.DB) error {
|
||
if err := postgres.NewAccountStore(tx, nil).BindWeCom(ctx, accountID, member.CorpID, member.UserID, member.Name, operatorID); err != nil {
|
||
return err
|
||
}
|
||
return s.lifecycleAudit.WriteAccountSecurity(ctx, tx, accountauditapp.SecurityAudit{
|
||
ActionCode: constants.AuditActionAccountWeComBound, Summary: "绑定账号企业微信身份",
|
||
Result: constants.AuditResultSuccess, ActorID: operatorID, Account: &updatedAccount,
|
||
AuthenticationKey: fmt.Sprintf("account:%d:wecom", account.ID),
|
||
Authentication: map[string]any{
|
||
"account_id": account.ID, "auth_method": "wecom", "state": "bound",
|
||
"wecom_corp_id": member.CorpID, "wecom_userid": member.UserID, "wecom_name": member.Name,
|
||
},
|
||
BeforeData: beforeData, AfterData: afterData,
|
||
})
|
||
}); err != nil {
|
||
var pgErr *pgconn.PgError
|
||
if stdErrors.As(err, &pgErr) && pgErr.Code == "23505" {
|
||
appErr := errors.New(errors.CodeConflict, "该企业微信成员已绑定其他系统账号")
|
||
s.recordSecurityFailure(ctx, constants.AuditActionAccountWeComBound, "拒绝绑定账号企业微信身份", constants.AuditResultDenied, account, map[string]any{
|
||
"account_id": account.ID, "auth_method": "wecom", "state": "denied",
|
||
}, beforeData, nil, appErr)
|
||
return nil, appErr
|
||
}
|
||
s.recordSecurityFailure(ctx, constants.AuditActionAccountWeComBound, "绑定账号企业微信身份失败", constants.AuditResultFailed, account, map[string]any{
|
||
"account_id": account.ID, "auth_method": "wecom", "state": "failed",
|
||
}, beforeData, nil, err)
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "绑定企业微信成员失败")
|
||
}
|
||
account = &updatedAccount
|
||
accounts := []*model.Account{account}
|
||
return s.toAccountResponse(account, s.loadShopNames(ctx, accounts), s.loadEnterpriseNames(ctx, accounts)), nil
|
||
}
|
||
|
||
// Update 更新账号
|
||
func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateAccountRequest) (*model.Account, error) {
|
||
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.CodeForbidden, "无权限操作该资源或资源不存在")
|
||
}
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "获取账号失败")
|
||
}
|
||
|
||
userType := middleware.GetUserTypeFromContext(ctx)
|
||
|
||
if userType == constants.UserTypeAgent {
|
||
if account.ShopID == nil {
|
||
s.recordLifecycleFailure(ctx, constants.AuditActionAccountUpdated, "拒绝更新账号", constants.AuditResultDenied, account, nil, nil, errors.New(errors.CodeForbidden))
|
||
return nil, errors.New(errors.CodeForbidden, "无权限操作该账号")
|
||
}
|
||
if err := middleware.CanManageShop(ctx, *account.ShopID); err != nil {
|
||
s.recordLifecycleFailure(ctx, constants.AuditActionAccountUpdated, "拒绝更新账号", constants.AuditResultDenied, account, nil, nil, err)
|
||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||
}
|
||
}
|
||
|
||
beforeData := accountLifecycleData(account)
|
||
|
||
if req.Username != nil {
|
||
existing, err := s.accountStore.GetByUsername(ctx, *req.Username)
|
||
if err == nil && existing != nil && existing.ID != id {
|
||
s.recordLifecycleFailure(ctx, constants.AuditActionAccountUpdated, "拒绝更新重复用户名", constants.AuditResultDenied, account, beforeData, nil, errors.New(errors.CodeUsernameExists))
|
||
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 {
|
||
s.recordLifecycleFailure(ctx, constants.AuditActionAccountUpdated, "拒绝更新重复手机号", constants.AuditResultDenied, account, beforeData, nil, errors.New(errors.CodePhoneExists))
|
||
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 {
|
||
s.recordSecurityFailure(ctx, constants.AuditActionAccountPasswordReset, "更新账号凭据处理失败", constants.AuditResultFailed, account, passwordAuthentication(account, "failed"), beforeData, nil, err)
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "密码哈希失败")
|
||
}
|
||
account.Password = string(hashedPassword)
|
||
}
|
||
|
||
if req.Status != nil {
|
||
account.Status = *req.Status
|
||
}
|
||
|
||
account.Updater = currentUserID
|
||
|
||
if err := s.runLifecycleTransaction(ctx, func(tx *gorm.DB) error {
|
||
if err := postgres.NewAccountStore(tx, nil).Update(ctx, account); err != nil {
|
||
return err
|
||
}
|
||
if req.Password != nil {
|
||
return s.lifecycleAudit.WriteAccountSecurity(ctx, tx, accountauditapp.SecurityAudit{
|
||
ActionCode: constants.AuditActionAccountPasswordReset, Summary: "更新账号安全资料", Result: constants.AuditResultSuccess,
|
||
ActorID: currentUserID, Account: account,
|
||
AuthenticationKey: fmt.Sprintf("account:%d:password", account.ID),
|
||
Authentication: passwordAuthentication(account, "changed"),
|
||
BeforeData: beforeData, AfterData: accountLifecycleData(account),
|
||
})
|
||
}
|
||
shop, enterprise, roles, err := loadLifecycleResources(ctx, tx, account)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return s.lifecycleAudit.WriteAccountLifecycle(ctx, tx, accountauditapp.LifecycleAudit{
|
||
ActionCode: constants.AuditActionAccountUpdated, Summary: "更新账号", Result: constants.AuditResultSuccess,
|
||
Account: account, Shop: shop, Enterprise: enterprise, Roles: roles,
|
||
BeforeData: beforeData, AfterData: accountLifecycleData(account),
|
||
})
|
||
}); err != nil {
|
||
if req.Password != nil {
|
||
s.recordSecurityFailure(ctx, constants.AuditActionAccountPasswordReset, "更新账号安全资料失败", constants.AuditResultFailed, account, passwordAuthentication(account, "failed"), beforeData, nil, err)
|
||
} else {
|
||
s.recordLifecycleFailure(ctx, constants.AuditActionAccountUpdated, "更新账号失败", constants.AuditResultFailed, account, beforeData, nil, err)
|
||
}
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "更新账号失败")
|
||
}
|
||
if req.Password != nil {
|
||
s.revokeAccountTokens(ctx, account.ID)
|
||
}
|
||
|
||
return account, nil
|
||
}
|
||
|
||
// Delete 软删除账号
|
||
func (s *Service) Delete(ctx context.Context, id uint) error {
|
||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||
if currentUserID == 0 {
|
||
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
||
}
|
||
|
||
account, err := s.accountStore.GetByID(ctx, id)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||
}
|
||
return errors.Wrap(errors.CodeInternalError, err, "获取账号失败")
|
||
}
|
||
|
||
userType := middleware.GetUserTypeFromContext(ctx)
|
||
|
||
if userType == constants.UserTypeAgent {
|
||
if account.ShopID == nil {
|
||
s.recordLifecycleFailure(ctx, constants.AuditActionAccountDeleted, "拒绝删除账号", constants.AuditResultDenied, account, nil, nil, errors.New(errors.CodeForbidden))
|
||
return errors.New(errors.CodeForbidden, "无权限操作该账号")
|
||
}
|
||
if err := middleware.CanManageShop(ctx, *account.ShopID); err != nil {
|
||
s.recordLifecycleFailure(ctx, constants.AuditActionAccountDeleted, "拒绝删除账号", constants.AuditResultDenied, account, nil, nil, err)
|
||
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||
}
|
||
}
|
||
|
||
beforeData := accountLifecycleData(account)
|
||
if err := s.runLifecycleTransaction(ctx, func(tx *gorm.DB) error {
|
||
shop, enterprise, roles, err := loadLifecycleResources(ctx, tx, account)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err := postgres.NewAccountStore(tx, nil).Delete(ctx, id); err != nil {
|
||
return err
|
||
}
|
||
return s.lifecycleAudit.WriteAccountLifecycle(ctx, tx, accountauditapp.LifecycleAudit{
|
||
ActionCode: constants.AuditActionAccountDeleted, Summary: "删除账号", Result: constants.AuditResultSuccess,
|
||
Account: account, Shop: shop, Enterprise: enterprise, Roles: roles, BeforeData: beforeData,
|
||
})
|
||
}); err != nil {
|
||
s.recordLifecycleFailure(ctx, constants.AuditActionAccountDeleted, "删除账号失败", constants.AuditResultFailed, account, beforeData, nil, err)
|
||
return errors.Wrap(errors.CodeInternalError, err, "删除账号失败")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// List 查询账号列表
|
||
func (s *Service) List(ctx context.Context, req *dto.AccountListRequest) ([]*dto.AccountResponse, 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
|
||
}
|
||
if req.ShopID != nil {
|
||
filters["shop_id"] = *req.ShopID
|
||
}
|
||
if req.EnterpriseID != nil {
|
||
filters["enterprise_id"] = *req.EnterpriseID
|
||
}
|
||
|
||
accounts, total, err := s.accountStore.List(ctx, opts, filters)
|
||
if err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
shopMap := s.loadShopNames(ctx, accounts)
|
||
enterpriseMap := s.loadEnterpriseNames(ctx, accounts)
|
||
|
||
responses := make([]*dto.AccountResponse, 0, len(accounts))
|
||
for _, acc := range accounts {
|
||
resp := s.toAccountResponse(acc, shopMap, enterpriseMap)
|
||
responses = append(responses, resp)
|
||
}
|
||
|
||
return responses, total, nil
|
||
}
|
||
|
||
// AssignRoles 为账号分配角色(支持空数组清空所有角色,超级管理员禁止分配)
|
||
func (s *Service) AssignRoles(ctx context.Context, accountID uint, roleIDs []uint) ([]*model.AccountRole, error) {
|
||
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.CodeForbidden, "无权限操作该资源或资源不存在")
|
||
}
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "获取账号失败")
|
||
}
|
||
|
||
userType := middleware.GetUserTypeFromContext(ctx)
|
||
|
||
if userType == constants.UserTypeAgent {
|
||
if account.ShopID == nil {
|
||
err := errors.New(errors.CodeForbidden, "无权限操作该账号")
|
||
s.recordRoleAssignmentFailure(ctx, constants.AuditActionAccountRolesAssigned, account, nil, err)
|
||
return nil, err
|
||
}
|
||
if err := middleware.CanManageShop(ctx, *account.ShopID); err != nil {
|
||
appErr := errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||
s.recordRoleAssignmentFailure(ctx, constants.AuditActionAccountRolesAssigned, account, nil, appErr)
|
||
return nil, appErr
|
||
}
|
||
}
|
||
|
||
if account.UserType == constants.UserTypeSuperAdmin {
|
||
err := errors.New(errors.CodeInvalidParam, "超级管理员不允许分配角色")
|
||
s.recordRoleAssignmentFailure(ctx, constants.AuditActionAccountRolesAssigned, account, nil, err)
|
||
return nil, err
|
||
}
|
||
|
||
assigned, changedRoles, err := s.assignAccountRoles(ctx, account, currentUserID, roleIDs)
|
||
if err != nil {
|
||
s.recordRoleAssignmentFailure(ctx, constants.AuditActionAccountRolesAssigned, account, changedRoles, err)
|
||
return nil, err
|
||
}
|
||
return assigned, 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, errors.Wrap(errors.CodeInternalError, err, "获取账号失败")
|
||
}
|
||
|
||
// 获取角色 ID 列表
|
||
roleIDs, err := s.accountRoleStore.GetRoleIDsByAccountID(ctx, accountID)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "获取账号角色 ID 失败")
|
||
}
|
||
|
||
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 {
|
||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||
if currentUserID == 0 {
|
||
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
||
}
|
||
|
||
account, err := s.accountStore.GetByID(ctx, accountID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||
}
|
||
return errors.Wrap(errors.CodeInternalError, err, "获取账号失败")
|
||
}
|
||
|
||
userType := middleware.GetUserTypeFromContext(ctx)
|
||
|
||
if userType == constants.UserTypeAgent {
|
||
if account.ShopID == nil {
|
||
err := errors.New(errors.CodeForbidden, "无权限操作该账号")
|
||
s.recordRoleAssignmentFailure(ctx, constants.AuditActionAccountRoleRemoved, account, nil, err)
|
||
return err
|
||
}
|
||
if err := middleware.CanManageShop(ctx, *account.ShopID); err != nil {
|
||
appErr := errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||
s.recordRoleAssignmentFailure(ctx, constants.AuditActionAccountRoleRemoved, account, nil, appErr)
|
||
return appErr
|
||
}
|
||
}
|
||
|
||
role, err := s.removeAccountRole(ctx, account, currentUserID, roleID)
|
||
if err != nil {
|
||
roles := []*model.Role(nil)
|
||
if role != nil {
|
||
roles = []*model.Role{role}
|
||
}
|
||
s.recordRoleAssignmentFailure(ctx, constants.AuditActionAccountRoleRemoved, account, roles, err)
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (s *Service) assignAccountRoles(ctx context.Context, account *model.Account, operatorID uint, requested []uint) ([]*model.AccountRole, []*model.Role, error) {
|
||
if s.db == nil || s.accessAudit == nil {
|
||
return nil, nil, errors.New(errors.CodeInvalidStatus, "账号角色审计接缝未配置")
|
||
}
|
||
assigned := make([]*model.AccountRole, 0, len(requested))
|
||
var changedRoles []*model.Role
|
||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Select("id").First(&model.Account{}, account.ID).Error; err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "锁定账号角色关系失败")
|
||
}
|
||
accountRoles := postgres.NewAccountRoleStore(tx, nil)
|
||
roles := postgres.NewRoleStore(tx)
|
||
scopeShop, err := accountRoleScopeShop(ctx, tx, account.ShopID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
beforeIDs, err := accountRoles.GetRoleIDsByAccountID(ctx, account.ID)
|
||
if err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "查询账号现有角色失败")
|
||
}
|
||
afterIDs, changes, err := s.applyAccountRoleAssignment(ctx, accountRoles, roles, account, operatorID, beforeIDs, requested, &assigned)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
changedRoles = changes
|
||
if len(changes) == 0 {
|
||
return nil
|
||
}
|
||
if err := s.accessAudit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{
|
||
ActionCode: constants.AuditActionAccountRolesAssigned, Summary: "为账号分配角色",
|
||
OperatorID: operatorID, Account: account, Shop: scopeShop, Roles: roleAssignmentChanges(changes, beforeIDs, afterIDs),
|
||
BeforeData: map[string]any{"role_ids": sortedRoleIDs(beforeIDs)},
|
||
AfterData: map[string]any{"role_ids": sortedRoleIDs(afterIDs)},
|
||
}); err != nil {
|
||
return errors.Wrap(errors.CodeInternalError, err, "写入账号角色审计失败")
|
||
}
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
return nil, changedRoles, err
|
||
}
|
||
s.clearAccountPermissionCache(ctx, account.ID)
|
||
return assigned, changedRoles, nil
|
||
}
|
||
|
||
func (s *Service) applyAccountRoleAssignment(ctx context.Context, accountRoles *postgres.AccountRoleStore, roles *postgres.RoleStore, account *model.Account, operatorID uint, beforeIDs, requested []uint, assigned *[]*model.AccountRole) ([]uint, []*model.Role, error) {
|
||
if len(requested) == 0 {
|
||
if len(beforeIDs) == 0 {
|
||
return []uint{}, nil, nil
|
||
}
|
||
removed, err := roles.GetByIDs(ctx, beforeIDs)
|
||
if err != nil {
|
||
return nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询待移除角色失败")
|
||
}
|
||
if err := accountRoles.DeleteByAccountID(ctx, account.ID); err != nil {
|
||
return nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "清空账号角色失败")
|
||
}
|
||
return []uint{}, removed, nil
|
||
}
|
||
requestedRoles := make([]*model.Role, 0, len(requested))
|
||
for _, roleID := range requested {
|
||
role, err := roles.GetByID(ctx, roleID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, nil, errors.New(errors.CodeRoleNotFound, fmt.Sprintf("角色 %d 不存在", roleID))
|
||
}
|
||
return nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询角色失败")
|
||
}
|
||
if !constants.IsRoleTypeMatchUserType(role.RoleType, account.UserType) {
|
||
return nil, nil, errors.New(errors.CodeInvalidParam, "角色类型与账号类型不匹配")
|
||
}
|
||
requestedRoles = append(requestedRoles, role)
|
||
}
|
||
existing := roleIDSet(beforeIDs)
|
||
newRoleCount := 0
|
||
for _, roleID := range requested {
|
||
if !existing[roleID] {
|
||
newRoleCount++
|
||
}
|
||
}
|
||
maxRoles := constants.GetMaxRolesForUserType(account.UserType)
|
||
if maxRoles == 0 {
|
||
return nil, nil, errors.New(errors.CodeInvalidParam, "该用户类型不需要分配角色")
|
||
}
|
||
if maxRoles != -1 && len(beforeIDs)+newRoleCount > maxRoles {
|
||
return nil, nil, errors.New(errors.CodeInvalidParam, fmt.Sprintf("该用户类型最多只能分配 %d 个角色", maxRoles))
|
||
}
|
||
changed := make([]*model.Role, 0, newRoleCount)
|
||
addedIDs := make([]uint, 0, newRoleCount)
|
||
for index, roleID := range requested {
|
||
if existing[roleID] {
|
||
continue
|
||
}
|
||
accountRole := &model.AccountRole{AccountID: account.ID, RoleID: roleID, Status: constants.StatusEnabled, Creator: operatorID, Updater: operatorID}
|
||
if err := accountRoles.Create(ctx, accountRole); err != nil {
|
||
return nil, changed, errors.Wrap(errors.CodeDatabaseError, err, "创建账号-角色关联失败")
|
||
}
|
||
*assigned = append(*assigned, accountRole)
|
||
changed = append(changed, requestedRoles[index])
|
||
addedIDs = append(addedIDs, roleID)
|
||
existing[roleID] = true
|
||
}
|
||
return append(append([]uint(nil), beforeIDs...), addedIDs...), changed, nil
|
||
}
|
||
|
||
func (s *Service) removeAccountRole(ctx context.Context, account *model.Account, operatorID, roleID uint) (*model.Role, error) {
|
||
if s.db == nil || s.accessAudit == nil {
|
||
return nil, errors.New(errors.CodeInvalidStatus, "账号角色审计接缝未配置")
|
||
}
|
||
var removed *model.Role
|
||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Select("id").First(&model.Account{}, account.ID).Error; err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "锁定账号角色关系失败")
|
||
}
|
||
accountRoles := postgres.NewAccountRoleStore(tx, nil)
|
||
scopeShop, err := accountRoleScopeShop(ctx, tx, account.ShopID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
beforeIDs, err := accountRoles.GetRoleIDsByAccountID(ctx, account.ID)
|
||
if err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "查询账号现有角色失败")
|
||
}
|
||
if !slices.Contains(beforeIDs, roleID) {
|
||
return nil
|
||
}
|
||
removed, err = postgres.NewRoleStore(tx).GetByID(ctx, roleID)
|
||
if err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "查询待移除角色失败")
|
||
}
|
||
if err := accountRoles.Delete(ctx, account.ID, roleID); err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "删除账号-角色关联失败")
|
||
}
|
||
afterIDs := removeRoleID(beforeIDs, roleID)
|
||
if err := s.accessAudit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{
|
||
ActionCode: constants.AuditActionAccountRoleRemoved, Summary: "移除账号角色",
|
||
OperatorID: operatorID, Account: account, Shop: scopeShop,
|
||
Roles: []accessauditapp.RoleChange{{Role: removed, BeforeData: map[string]any{"assigned": true}, AfterData: map[string]any{"assigned": false}}},
|
||
BeforeData: map[string]any{"role_ids": sortedRoleIDs(beforeIDs)},
|
||
AfterData: map[string]any{"role_ids": sortedRoleIDs(afterIDs)},
|
||
}); err != nil {
|
||
return errors.Wrap(errors.CodeInternalError, err, "写入账号角色审计失败")
|
||
}
|
||
return nil
|
||
})
|
||
if err == nil && removed != nil {
|
||
s.clearAccountPermissionCache(ctx, account.ID)
|
||
}
|
||
return removed, err
|
||
}
|
||
|
||
func (s *Service) clearAccountPermissionCache(ctx context.Context, accountID uint) {
|
||
if s.redisClient == nil {
|
||
return
|
||
}
|
||
if err := s.redisClient.Del(ctx, constants.RedisUserPermissionsKey(accountID)).Err(); err != nil {
|
||
logger.GetAppLogger().Warn("清理账号权限缓存失败", zap.Uint("account_id", accountID), zap.Error(err))
|
||
}
|
||
}
|
||
|
||
func (s *Service) recordRoleAssignmentFailure(ctx context.Context, action string, account *model.Account, roles []*model.Role, originalErr error) {
|
||
changes := make([]accessauditapp.RoleChange, 0, len(roles))
|
||
for _, role := range roles {
|
||
changes = append(changes, accessauditapp.RoleChange{Role: role})
|
||
}
|
||
accessauditapp.RecordFailure(ctx, s.db, s.accessAudit, accessauditapp.ChangeAudit{
|
||
ActionCode: action, Summary: "账号角色操作失败", Result: accessFailureResult(originalErr),
|
||
OperatorID: middleware.GetUserIDFromContext(ctx), Account: account, Shop: s.loadAccountRoleScopeShop(ctx, account), Roles: changes,
|
||
}, originalErr)
|
||
}
|
||
|
||
func accountRoleScopeShop(ctx context.Context, tx *gorm.DB, shopID *uint) (*model.Shop, error) {
|
||
if shopID == nil {
|
||
return nil, nil
|
||
}
|
||
shop, err := postgres.NewShopStore(tx, nil).GetByID(ctx, *shopID)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询账号所属店铺失败")
|
||
}
|
||
return shop, nil
|
||
}
|
||
|
||
func (s *Service) loadAccountRoleScopeShop(ctx context.Context, account *model.Account) *model.Shop {
|
||
if account == nil || account.ShopID == nil || s.shopStore == nil {
|
||
return nil
|
||
}
|
||
shops, err := s.shopStore.GetByIDs(ctx, []uint{*account.ShopID})
|
||
if err != nil || len(shops) == 0 {
|
||
return nil
|
||
}
|
||
return shops[0]
|
||
}
|
||
|
||
func roleAssignmentChanges(roles []*model.Role, beforeIDs, afterIDs []uint) []accessauditapp.RoleChange {
|
||
before, after := roleIDSet(beforeIDs), roleIDSet(afterIDs)
|
||
changes := make([]accessauditapp.RoleChange, 0, len(roles))
|
||
for _, role := range roles {
|
||
changes = append(changes, accessauditapp.RoleChange{
|
||
Role: role, BeforeData: map[string]any{"assigned": before[role.ID]}, AfterData: map[string]any{"assigned": after[role.ID]},
|
||
})
|
||
}
|
||
return changes
|
||
}
|
||
|
||
func accessFailureResult(err error) string {
|
||
var appErr *errors.AppError
|
||
if stdErrors.As(err, &appErr) {
|
||
switch appErr.Code {
|
||
case errors.CodeForbidden, errors.CodeInvalidParam, errors.CodeNotFound, errors.CodeRoleNotFound:
|
||
return constants.AuditResultDenied
|
||
}
|
||
}
|
||
return constants.AuditResultFailed
|
||
}
|
||
|
||
func roleIDSet(ids []uint) map[uint]bool {
|
||
set := make(map[uint]bool, len(ids))
|
||
for _, id := range ids {
|
||
set[id] = true
|
||
}
|
||
return set
|
||
}
|
||
|
||
func sortedRoleIDs(ids []uint) []uint {
|
||
result := append([]uint(nil), ids...)
|
||
slices.Sort(result)
|
||
return result
|
||
}
|
||
|
||
func removeRoleID(ids []uint, removed uint) []uint {
|
||
result := make([]uint, 0, len(ids))
|
||
for _, id := range ids {
|
||
if id != removed {
|
||
result = append(result, id)
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
// ValidatePassword 验证密码
|
||
func (s *Service) ValidatePassword(plainPassword, hashedPassword string) bool {
|
||
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(plainPassword))
|
||
return err == nil
|
||
}
|
||
|
||
// UpdatePassword 修改账号密码(管理员重置场景,无需旧密码)
|
||
func (s *Service) UpdatePassword(ctx context.Context, accountID uint, newPassword string) error {
|
||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||
if currentUserID == 0 {
|
||
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
||
}
|
||
|
||
account, err := s.accountStore.GetByID(ctx, accountID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return errors.New(errors.CodeAccountNotFound, "账号不存在")
|
||
}
|
||
return errors.Wrap(errors.CodeInternalError, err, "获取账号失败")
|
||
}
|
||
|
||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||
if err != nil {
|
||
s.recordSecurityFailure(ctx, constants.AuditActionAccountPasswordReset, "重置账号密码失败", constants.AuditResultFailed, account, passwordAuthentication(account, "failed"), nil, nil, err)
|
||
return errors.Wrap(errors.CodeInternalError, err, "密码哈希失败")
|
||
}
|
||
|
||
if err := s.runLifecycleTransaction(ctx, func(tx *gorm.DB) error {
|
||
if err := postgres.NewAccountStore(tx, nil).UpdatePassword(ctx, accountID, string(hashedPassword), currentUserID); err != nil {
|
||
return err
|
||
}
|
||
return s.lifecycleAudit.WriteAccountSecurity(ctx, tx, accountauditapp.SecurityAudit{
|
||
ActionCode: constants.AuditActionAccountPasswordReset, Summary: "重置账号密码",
|
||
Result: constants.AuditResultSuccess, ActorID: currentUserID, Account: account,
|
||
AuthenticationKey: fmt.Sprintf("account:%d:password", account.ID),
|
||
Authentication: passwordAuthentication(account, "changed"),
|
||
BeforeData: map[string]any{"credentials_configured": account.Password != ""},
|
||
AfterData: map[string]any{"credentials_configured": true},
|
||
})
|
||
}); err != nil {
|
||
s.recordSecurityFailure(ctx, constants.AuditActionAccountPasswordReset, "重置账号密码失败", constants.AuditResultFailed, account, passwordAuthentication(account, "failed"), nil, nil, err)
|
||
return errors.Wrap(errors.CodeInternalError, err, "更新密码失败")
|
||
}
|
||
s.revokeAccountTokens(ctx, account.ID)
|
||
|
||
return nil
|
||
}
|
||
|
||
func (s *Service) revokeAccountTokens(ctx context.Context, accountID uint) {
|
||
if s.tokenManager == nil {
|
||
return
|
||
}
|
||
if err := s.tokenManager.RevokeAllUserTokens(ctx, accountID); err != nil {
|
||
logger.GetAppLogger().Warn("改密后撤销账号令牌失败", zap.Uint("account_id", accountID), zap.Error(err))
|
||
}
|
||
}
|
||
|
||
func (s *Service) recordSecurityFailure(
|
||
ctx context.Context,
|
||
actionCode, summary, result string,
|
||
account *model.Account,
|
||
authentication, beforeData, afterData map[string]any,
|
||
originalErr error,
|
||
) {
|
||
if s.db == nil || s.lifecycleAudit == nil || account == nil || account.ID == 0 {
|
||
return
|
||
}
|
||
errorCode := strconv.Itoa(errors.CodeInternalError)
|
||
var appErr *errors.AppError
|
||
if stdErrors.As(originalErr, &appErr) {
|
||
errorCode = strconv.Itoa(appErr.Code)
|
||
} else if result == constants.AuditResultDenied {
|
||
errorCode = strconv.Itoa(errors.CodeForbidden)
|
||
}
|
||
operatorID := middleware.GetUserIDFromContext(ctx)
|
||
if operatorID == 0 {
|
||
operatorID = account.ID
|
||
}
|
||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
return s.lifecycleAudit.WriteAccountSecurity(ctx, tx, accountauditapp.SecurityAudit{
|
||
ActionCode: actionCode, Summary: summary, Result: result, ErrorCode: errorCode, ErrorSummary: summary,
|
||
ActorID: operatorID, Account: account, AuthenticationKey: fmt.Sprintf("account:%d:security", account.ID),
|
||
Authentication: authentication, BeforeData: beforeData, AfterData: afterData,
|
||
})
|
||
})
|
||
if err != nil {
|
||
requestID := ""
|
||
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
|
||
requestID = *value
|
||
}
|
||
auditfailure.RecordSecondaryWriteFailure(actionCode, account.Username, requestID, requestID, errorCode, err)
|
||
}
|
||
}
|
||
|
||
func passwordAuthentication(account *model.Account, state string) map[string]any {
|
||
return map[string]any{"account_id": account.ID, "auth_method": "password", "state": state}
|
||
}
|
||
|
||
// UpdateStatus 修改账号状态(启用/禁用)
|
||
func (s *Service) UpdateStatus(ctx context.Context, accountID uint, status int) error {
|
||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||
if currentUserID == 0 {
|
||
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
||
}
|
||
if status != constants.StatusDisabled && status != constants.StatusEnabled {
|
||
return errors.New(errors.CodeInvalidParam, "账号状态无效")
|
||
}
|
||
|
||
account, err := s.accountStore.GetByID(ctx, accountID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return errors.New(errors.CodeAccountNotFound, "账号不存在")
|
||
}
|
||
return errors.Wrap(errors.CodeInternalError, err, "获取账号失败")
|
||
}
|
||
if middleware.GetUserTypeFromContext(ctx) == constants.UserTypeAgent {
|
||
if account.ShopID == nil || middleware.CanManageShop(ctx, *account.ShopID) != nil {
|
||
s.recordLifecycleFailure(ctx, constants.AuditActionAccountUpdated, "拒绝更新账号状态", constants.AuditResultDenied, account, nil, nil, errors.New(errors.CodeForbidden))
|
||
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||
}
|
||
}
|
||
beforeData := accountLifecycleData(account)
|
||
if err := s.runLifecycleTransaction(ctx, func(tx *gorm.DB) error {
|
||
if err := postgres.NewAccountStore(tx, nil).UpdateStatus(ctx, accountID, status, currentUserID); err != nil {
|
||
return err
|
||
}
|
||
account.Status = status
|
||
account.Updater = currentUserID
|
||
shop, enterprise, roles, err := loadLifecycleResources(ctx, tx, account)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return s.lifecycleAudit.WriteAccountLifecycle(ctx, tx, accountauditapp.LifecycleAudit{
|
||
ActionCode: constants.AuditActionAccountUpdated, Summary: "更新账号状态", Result: constants.AuditResultSuccess,
|
||
Account: account, Shop: shop, Enterprise: enterprise, Roles: roles,
|
||
BeforeData: beforeData, AfterData: accountLifecycleData(account),
|
||
})
|
||
}); err != nil {
|
||
s.recordLifecycleFailure(ctx, constants.AuditActionAccountUpdated, "更新账号状态失败", constants.AuditResultFailed, account, beforeData, nil, err)
|
||
return errors.Wrap(errors.CodeInternalError, err, "更新状态失败")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (s *Service) runLifecycleTransaction(ctx context.Context, fn func(*gorm.DB) error) error {
|
||
if s.db == nil || s.lifecycleAudit == nil {
|
||
return errors.New(errors.CodeInvalidStatus, "账号生命周期审计接缝未配置")
|
||
}
|
||
return s.db.WithContext(ctx).Transaction(fn)
|
||
}
|
||
|
||
func (s *Service) recordLifecycleFailure(
|
||
ctx context.Context,
|
||
actionCode, summary, result string,
|
||
account *model.Account,
|
||
beforeData, afterData map[string]any,
|
||
originalErr error,
|
||
) {
|
||
if s.db == nil || s.lifecycleAudit == nil || account == nil {
|
||
return
|
||
}
|
||
errorCode := strconv.Itoa(errors.CodeInternalError)
|
||
var appErr *errors.AppError
|
||
if stdErrors.As(originalErr, &appErr) {
|
||
errorCode = strconv.Itoa(appErr.Code)
|
||
} else if result == constants.AuditResultDenied {
|
||
errorCode = strconv.Itoa(errors.CodeForbidden)
|
||
}
|
||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
shop, enterprise, roles, loadErr := loadLifecycleResources(ctx, tx, account)
|
||
if loadErr != nil {
|
||
return loadErr
|
||
}
|
||
return s.lifecycleAudit.WriteAccountLifecycle(ctx, tx, accountauditapp.LifecycleAudit{
|
||
ActionCode: actionCode, Summary: summary, Result: result, ErrorCode: errorCode,
|
||
ErrorSummary: summary, Account: account, Shop: shop, Enterprise: enterprise, Roles: roles,
|
||
BeforeData: beforeData, AfterData: afterData,
|
||
})
|
||
})
|
||
if err != nil {
|
||
requestID := ""
|
||
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
|
||
requestID = *value
|
||
}
|
||
auditfailure.RecordSecondaryWriteFailure(actionCode, account.Username, requestID, requestID, errorCode, err)
|
||
}
|
||
}
|
||
|
||
func loadLifecycleResources(ctx context.Context, tx *gorm.DB, account *model.Account) (*model.Shop, *model.Enterprise, []*model.Role, error) {
|
||
var shop *model.Shop
|
||
if account.ShopID != nil {
|
||
shop = &model.Shop{}
|
||
if err := tx.WithContext(ctx).Unscoped().First(shop, *account.ShopID).Error; err != nil {
|
||
return nil, nil, nil, err
|
||
}
|
||
}
|
||
var enterprise *model.Enterprise
|
||
if account.EnterpriseID != nil {
|
||
enterprise = &model.Enterprise{}
|
||
if err := tx.WithContext(ctx).Unscoped().First(enterprise, *account.EnterpriseID).Error; err != nil {
|
||
return nil, nil, nil, err
|
||
}
|
||
}
|
||
var roles []*model.Role
|
||
if account.ID != 0 {
|
||
if err := tx.WithContext(ctx).Table("tb_role AS r").
|
||
Joins("JOIN tb_account_role AS ar ON ar.role_id = r.id AND ar.deleted_at IS NULL").
|
||
Where("ar.account_id = ?", account.ID).Order("r.id ASC").Find(&roles).Error; err != nil {
|
||
return nil, nil, nil, err
|
||
}
|
||
}
|
||
return shop, enterprise, roles, nil
|
||
}
|
||
|
||
func accountLifecycleData(account *model.Account) map[string]any {
|
||
return map[string]any{
|
||
"id": account.ID, "username": account.Username, "phone": account.Phone,
|
||
"user_type": account.UserType, "shop_id": account.ShopID,
|
||
"enterprise_id": account.EnterpriseID, "status": account.Status,
|
||
}
|
||
}
|
||
|
||
// ListPlatformAccounts 查询平台账号列表(自动筛选 user_type IN (1, 2))
|
||
func (s *Service) ListPlatformAccounts(ctx context.Context, req *dto.PlatformAccountListRequest) ([]*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.Status != nil {
|
||
filters["status"] = *req.Status
|
||
}
|
||
|
||
return s.accountStore.ListPlatformAccounts(ctx, opts, filters)
|
||
}
|
||
|
||
// CreateSystemAccount 系统内部创建账号方法,用于系统初始化场景(绕过当前用户检查)
|
||
func (s *Service) CreateSystemAccount(ctx context.Context, account *model.Account) error {
|
||
if account.Username == "" {
|
||
return errors.New(errors.CodeInvalidParam, "用户名不能为空")
|
||
}
|
||
if account.Phone == "" {
|
||
return errors.New(errors.CodeInvalidParam, "手机号不能为空")
|
||
}
|
||
if account.Password == "" {
|
||
return errors.New(errors.CodeInvalidParam, "密码不能为空")
|
||
}
|
||
|
||
existing, err := s.accountStore.GetByUsername(ctx, account.Username)
|
||
if err == nil && existing != nil {
|
||
return errors.New(errors.CodeUsernameExists, "用户名已存在")
|
||
}
|
||
|
||
existing, err = s.accountStore.GetByPhone(ctx, account.Phone)
|
||
if err == nil && existing != nil {
|
||
return errors.New(errors.CodePhoneExists, "手机号已存在")
|
||
}
|
||
|
||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(account.Password), bcrypt.DefaultCost)
|
||
if err != nil {
|
||
return errors.Wrap(errors.CodeInternalError, err, "密码哈希失败")
|
||
}
|
||
account.Password = string(hashedPassword)
|
||
|
||
if err := s.accountStore.Create(ctx, account); err != nil {
|
||
return errors.Wrap(errors.CodeInternalError, err, "创建账号失败")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// loadShopNames 批量加载店铺名称
|
||
func (s *Service) loadShopNames(ctx context.Context, accounts []*model.Account) map[uint]string {
|
||
shopIDs := make([]uint, 0)
|
||
shopIDSet := make(map[uint]bool)
|
||
|
||
for _, acc := range accounts {
|
||
if acc.ShopID != nil && *acc.ShopID > 0 && !shopIDSet[*acc.ShopID] {
|
||
shopIDs = append(shopIDs, *acc.ShopID)
|
||
shopIDSet[*acc.ShopID] = true
|
||
}
|
||
}
|
||
|
||
shopMap := make(map[uint]string)
|
||
if len(shopIDs) > 0 {
|
||
shops, err := s.shopStore.GetByIDs(ctx, shopIDs)
|
||
if err == nil {
|
||
for _, shop := range shops {
|
||
shopMap[shop.ID] = shop.ShopName
|
||
}
|
||
}
|
||
}
|
||
return shopMap
|
||
}
|
||
|
||
// loadEnterpriseNames 批量加载企业名称
|
||
func (s *Service) loadEnterpriseNames(ctx context.Context, accounts []*model.Account) map[uint]string {
|
||
enterpriseIDs := make([]uint, 0)
|
||
enterpriseIDSet := make(map[uint]bool)
|
||
|
||
for _, acc := range accounts {
|
||
if acc.EnterpriseID != nil && *acc.EnterpriseID > 0 && !enterpriseIDSet[*acc.EnterpriseID] {
|
||
enterpriseIDs = append(enterpriseIDs, *acc.EnterpriseID)
|
||
enterpriseIDSet[*acc.EnterpriseID] = true
|
||
}
|
||
}
|
||
|
||
enterpriseMap := make(map[uint]string)
|
||
if len(enterpriseIDs) > 0 {
|
||
enterprises, err := s.enterpriseStore.GetByIDs(ctx, enterpriseIDs)
|
||
if err == nil {
|
||
for _, ent := range enterprises {
|
||
enterpriseMap[ent.ID] = ent.EnterpriseName
|
||
}
|
||
}
|
||
}
|
||
return enterpriseMap
|
||
}
|
||
|
||
// toAccountResponse 组装账号响应,填充关联名称
|
||
func (s *Service) toAccountResponse(acc *model.Account, shopMap map[uint]string, enterpriseMap map[uint]string) *dto.AccountResponse {
|
||
resp := &dto.AccountResponse{
|
||
ID: acc.ID,
|
||
Username: acc.Username,
|
||
Phone: acc.Phone,
|
||
UserType: acc.UserType,
|
||
ShopID: acc.ShopID,
|
||
EnterpriseID: acc.EnterpriseID,
|
||
WeComCorpID: acc.WeComCorpID,
|
||
WeComUserID: acc.WeComUserID,
|
||
WeComName: acc.WeComName,
|
||
WeComBound: acc.WeComCorpID != "" && acc.WeComUserID != "",
|
||
Status: acc.Status,
|
||
StatusName: constants.GetStatusName(acc.Status),
|
||
Creator: acc.Creator,
|
||
Updater: acc.Updater,
|
||
CreatedAt: acc.CreatedAt.Format("2006-01-02 15:04:05"),
|
||
UpdatedAt: acc.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||
}
|
||
|
||
if acc.ShopID != nil && *acc.ShopID > 0 {
|
||
resp.ShopName = shopMap[*acc.ShopID]
|
||
}
|
||
|
||
if acc.EnterpriseID != nil && *acc.EnterpriseID > 0 {
|
||
resp.EnterpriseName = enterpriseMap[*acc.EnterpriseID]
|
||
}
|
||
|
||
return resp
|
||
}
|