Files
junhong_cmp_fiber/internal/service/account/service.go
break c64f3d8b80
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m31s
全局审计完成
2026-08-07 11:02:52 +08:00

1198 lines
45 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}