All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m18s
- 移除 IoT 卡和号卡的 card_type 字段(数据库迁移) - 优化账号列表查询,支持按店铺和企业筛选 - 账号响应增加店铺名称和企业名称字段 - 实现批量加载店铺和企业名称,避免 N+1 查询 - 更新权限检查中间件,完善权限验证逻辑 - 更新相关测试用例,确保功能正确性
794 lines
24 KiB
Go
794 lines
24 KiB
Go
// Package account 提供账号管理的业务逻辑服务
|
||
// 包含账号创建、查询、更新、删除、密码管理等功能
|
||
package account
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
|
||
"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/constants"
|
||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||
"golang.org/x/crypto/bcrypt"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// Service 账号业务服务
|
||
type Service struct {
|
||
accountStore *postgres.AccountStore
|
||
roleStore *postgres.RoleStore
|
||
accountRoleStore *postgres.AccountRoleStore
|
||
shopRoleStore *postgres.ShopRoleStore
|
||
shopStore middleware.ShopStoreInterface
|
||
enterpriseStore middleware.EnterpriseStoreInterface
|
||
auditService AuditServiceInterface
|
||
}
|
||
|
||
type AuditServiceInterface interface {
|
||
LogOperation(ctx context.Context, log *model.AccountOperationLog)
|
||
}
|
||
|
||
// New 创建账号服务
|
||
func New(
|
||
accountStore *postgres.AccountStore,
|
||
roleStore *postgres.RoleStore,
|
||
accountRoleStore *postgres.AccountRoleStore,
|
||
shopRoleStore *postgres.ShopRoleStore,
|
||
shopStore middleware.ShopStoreInterface,
|
||
enterpriseStore middleware.EnterpriseStoreInterface,
|
||
auditService AuditServiceInterface,
|
||
) *Service {
|
||
return &Service{
|
||
accountStore: accountStore,
|
||
roleStore: roleStore,
|
||
accountRoleStore: accountRoleStore,
|
||
shopRoleStore: shopRoleStore,
|
||
shopStore: shopStore,
|
||
enterpriseStore: enterpriseStore,
|
||
auditService: auditService,
|
||
}
|
||
}
|
||
|
||
// 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, s.shopStore); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
if req.UserType == constants.UserTypeEnterprise && req.EnterpriseID != nil {
|
||
if err := middleware.CanManageEnterprise(ctx, *req.EnterpriseID, s.enterpriseStore, s.shopStore); 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.accountStore.Create(ctx, account); err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "创建账号失败")
|
||
}
|
||
|
||
currentAccount, _ := s.accountStore.GetByID(ctx, currentUserID)
|
||
operatorName := ""
|
||
if currentAccount != nil {
|
||
operatorName = currentAccount.Username
|
||
}
|
||
|
||
afterData := model.JSONB{
|
||
"id": account.ID,
|
||
"username": account.Username,
|
||
"phone": account.Phone,
|
||
"user_type": account.UserType,
|
||
"shop_id": account.ShopID,
|
||
"enterprise_id": account.EnterpriseID,
|
||
"status": account.Status,
|
||
}
|
||
|
||
requestID := middleware.GetRequestIDFromContext(ctx)
|
||
ipAddress := middleware.GetIPFromContext(ctx)
|
||
userAgent := middleware.GetUserAgentFromContext(ctx)
|
||
|
||
s.auditService.LogOperation(ctx, &model.AccountOperationLog{
|
||
OperatorID: currentUserID,
|
||
OperatorType: userType,
|
||
OperatorName: operatorName,
|
||
TargetAccountID: &account.ID,
|
||
TargetUsername: &account.Username,
|
||
TargetUserType: &account.UserType,
|
||
OperationType: "create",
|
||
OperationDesc: fmt.Sprintf("创建账号: %s", account.Username),
|
||
AfterData: afterData,
|
||
RequestID: requestID,
|
||
IPAddress: ipAddress,
|
||
UserAgent: userAgent,
|
||
})
|
||
|
||
return account, nil
|
||
}
|
||
|
||
// Get 获取账号
|
||
func (s *Service) Get(ctx context.Context, id uint) (*model.Account, error) {
|
||
account, err := s.accountStore.GetByID(ctx, id)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, errors.New(errors.CodeAccountNotFound, "账号不存在")
|
||
}
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "获取账号失败")
|
||
}
|
||
return account, 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 {
|
||
return nil, errors.New(errors.CodeForbidden, "无权限操作该账号")
|
||
}
|
||
if err := middleware.CanManageShop(ctx, *account.ShopID, s.shopStore); err != nil {
|
||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||
}
|
||
}
|
||
|
||
beforeData := model.JSONB{
|
||
"username": account.Username,
|
||
"phone": account.Phone,
|
||
"status": account.Status,
|
||
}
|
||
|
||
if req.Username != nil {
|
||
existing, err := s.accountStore.GetByUsername(ctx, *req.Username)
|
||
if err == nil && existing != nil && existing.ID != id {
|
||
return nil, errors.New(errors.CodeUsernameExists, "用户名已存在")
|
||
}
|
||
account.Username = *req.Username
|
||
}
|
||
|
||
if req.Phone != nil {
|
||
existing, err := s.accountStore.GetByPhone(ctx, *req.Phone)
|
||
if err == nil && existing != nil && existing.ID != id {
|
||
return nil, errors.New(errors.CodePhoneExists, "手机号已存在")
|
||
}
|
||
account.Phone = *req.Phone
|
||
}
|
||
|
||
if req.Password != nil {
|
||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(*req.Password), bcrypt.DefaultCost)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "密码哈希失败")
|
||
}
|
||
account.Password = string(hashedPassword)
|
||
}
|
||
|
||
if req.Status != nil {
|
||
account.Status = *req.Status
|
||
}
|
||
|
||
account.Updater = currentUserID
|
||
|
||
if err := s.accountStore.Update(ctx, account); err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "更新账号失败")
|
||
}
|
||
|
||
currentAccount, _ := s.accountStore.GetByID(ctx, currentUserID)
|
||
operatorName := ""
|
||
if currentAccount != nil {
|
||
operatorName = currentAccount.Username
|
||
}
|
||
|
||
afterData := model.JSONB{
|
||
"username": account.Username,
|
||
"phone": account.Phone,
|
||
"status": account.Status,
|
||
}
|
||
|
||
requestID := middleware.GetRequestIDFromContext(ctx)
|
||
ipAddress := middleware.GetIPFromContext(ctx)
|
||
userAgent := middleware.GetUserAgentFromContext(ctx)
|
||
|
||
s.auditService.LogOperation(ctx, &model.AccountOperationLog{
|
||
OperatorID: currentUserID,
|
||
OperatorType: userType,
|
||
OperatorName: operatorName,
|
||
TargetAccountID: &account.ID,
|
||
TargetUsername: &account.Username,
|
||
TargetUserType: &account.UserType,
|
||
OperationType: "update",
|
||
OperationDesc: fmt.Sprintf("更新账号: %s", account.Username),
|
||
BeforeData: beforeData,
|
||
AfterData: afterData,
|
||
RequestID: requestID,
|
||
IPAddress: ipAddress,
|
||
UserAgent: userAgent,
|
||
})
|
||
|
||
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 {
|
||
return errors.New(errors.CodeForbidden, "无权限操作该账号")
|
||
}
|
||
if err := middleware.CanManageShop(ctx, *account.ShopID, s.shopStore); err != nil {
|
||
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||
}
|
||
}
|
||
|
||
beforeData := model.JSONB{
|
||
"id": account.ID,
|
||
"username": account.Username,
|
||
"phone": account.Phone,
|
||
"status": account.Status,
|
||
}
|
||
|
||
if err := s.accountStore.Delete(ctx, id); err != nil {
|
||
return errors.Wrap(errors.CodeInternalError, err, "删除账号失败")
|
||
}
|
||
|
||
currentAccount, _ := s.accountStore.GetByID(ctx, currentUserID)
|
||
operatorName := ""
|
||
if currentAccount != nil {
|
||
operatorName = currentAccount.Username
|
||
}
|
||
|
||
requestID := middleware.GetRequestIDFromContext(ctx)
|
||
ipAddress := middleware.GetIPFromContext(ctx)
|
||
userAgent := middleware.GetUserAgentFromContext(ctx)
|
||
|
||
s.auditService.LogOperation(ctx, &model.AccountOperationLog{
|
||
OperatorID: currentUserID,
|
||
OperatorType: userType,
|
||
OperatorName: operatorName,
|
||
TargetAccountID: &account.ID,
|
||
TargetUsername: &account.Username,
|
||
TargetUserType: &account.UserType,
|
||
OperationType: "delete",
|
||
OperationDesc: fmt.Sprintf("删除账号: %s", account.Username),
|
||
BeforeData: beforeData,
|
||
RequestID: requestID,
|
||
IPAddress: ipAddress,
|
||
UserAgent: userAgent,
|
||
})
|
||
|
||
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 {
|
||
return nil, errors.New(errors.CodeForbidden, "无权限操作该账号")
|
||
}
|
||
if err := middleware.CanManageShop(ctx, *account.ShopID, s.shopStore); err != nil {
|
||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||
}
|
||
}
|
||
|
||
if account.UserType == constants.UserTypeSuperAdmin {
|
||
return nil, errors.New(errors.CodeInvalidParam, "超级管理员不允许分配角色")
|
||
}
|
||
|
||
// 空数组:清空所有角色
|
||
if len(roleIDs) == 0 {
|
||
if err := s.accountRoleStore.DeleteByAccountID(ctx, accountID); err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "清空账号角色失败")
|
||
}
|
||
return []*model.AccountRole{}, nil
|
||
}
|
||
|
||
maxRoles := constants.GetMaxRolesForUserType(account.UserType)
|
||
if maxRoles == 0 {
|
||
return nil, errors.New(errors.CodeInvalidParam, "该用户类型不需要分配角色")
|
||
}
|
||
|
||
existingCount, err := s.accountRoleStore.CountByAccountID(ctx, accountID)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "统计现有角色数量失败")
|
||
}
|
||
|
||
newRoleCount := 0
|
||
for _, roleID := range roleIDs {
|
||
exists, _ := s.accountRoleStore.Exists(ctx, accountID, roleID)
|
||
if !exists {
|
||
newRoleCount++
|
||
}
|
||
}
|
||
|
||
if maxRoles != -1 && int(existingCount)+newRoleCount > maxRoles {
|
||
return nil, errors.New(errors.CodeInvalidParam, fmt.Sprintf("该用户类型最多只能分配 %d 个角色", maxRoles))
|
||
}
|
||
|
||
for _, roleID := range roleIDs {
|
||
role, err := s.roleStore.GetByID(ctx, roleID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, errors.New(errors.CodeRoleNotFound, fmt.Sprintf("角色 %d 不存在", roleID))
|
||
}
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "获取角色失败")
|
||
}
|
||
|
||
if !constants.IsRoleTypeMatchUserType(role.RoleType, account.UserType) {
|
||
return nil, errors.New(errors.CodeInvalidParam, "角色类型与账号类型不匹配")
|
||
}
|
||
}
|
||
|
||
var ars []*model.AccountRole
|
||
for _, roleID := range roleIDs {
|
||
exists, _ := s.accountRoleStore.Exists(ctx, accountID, roleID)
|
||
if exists {
|
||
continue
|
||
}
|
||
|
||
ar := &model.AccountRole{
|
||
AccountID: accountID,
|
||
RoleID: roleID,
|
||
Status: constants.StatusEnabled,
|
||
Creator: currentUserID,
|
||
Updater: currentUserID,
|
||
}
|
||
if err := s.accountRoleStore.Create(ctx, ar); err != nil {
|
||
return nil, errors.Wrap(errors.CodeInternalError, err, "创建账号-角色关联失败")
|
||
}
|
||
ars = append(ars, ar)
|
||
}
|
||
|
||
currentAccount, _ := s.accountStore.GetByID(ctx, currentUserID)
|
||
operatorName := ""
|
||
if currentAccount != nil {
|
||
operatorName = currentAccount.Username
|
||
}
|
||
|
||
afterData := model.JSONB{
|
||
"role_ids": roleIDs,
|
||
}
|
||
|
||
requestID := middleware.GetRequestIDFromContext(ctx)
|
||
ipAddress := middleware.GetIPFromContext(ctx)
|
||
userAgent := middleware.GetUserAgentFromContext(ctx)
|
||
|
||
s.auditService.LogOperation(ctx, &model.AccountOperationLog{
|
||
OperatorID: currentUserID,
|
||
OperatorType: userType,
|
||
OperatorName: operatorName,
|
||
TargetAccountID: &account.ID,
|
||
TargetUsername: &account.Username,
|
||
TargetUserType: &account.UserType,
|
||
OperationType: "assign_roles",
|
||
OperationDesc: fmt.Sprintf("为账号 %s 分配角色", account.Username),
|
||
AfterData: afterData,
|
||
RequestID: requestID,
|
||
IPAddress: ipAddress,
|
||
UserAgent: userAgent,
|
||
})
|
||
|
||
return ars, nil
|
||
}
|
||
|
||
// GetRoles 获取账号的所有角色
|
||
func (s *Service) GetRoles(ctx context.Context, accountID uint) ([]*model.Role, error) {
|
||
// 检查账号存在
|
||
_, err := s.accountStore.GetByID(ctx, accountID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, errors.New(errors.CodeAccountNotFound, "账号不存在")
|
||
}
|
||
return nil, 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 {
|
||
return errors.New(errors.CodeForbidden, "无权限操作该账号")
|
||
}
|
||
if err := middleware.CanManageShop(ctx, *account.ShopID, s.shopStore); err != nil {
|
||
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||
}
|
||
}
|
||
|
||
if err := s.accountRoleStore.Delete(ctx, accountID, roleID); err != nil {
|
||
return errors.Wrap(errors.CodeInternalError, err, "删除账号-角色关联失败")
|
||
}
|
||
|
||
currentAccount, _ := s.accountStore.GetByID(ctx, currentUserID)
|
||
operatorName := ""
|
||
if currentAccount != nil {
|
||
operatorName = currentAccount.Username
|
||
}
|
||
|
||
afterData := model.JSONB{
|
||
"removed_role_id": roleID,
|
||
}
|
||
|
||
requestID := middleware.GetRequestIDFromContext(ctx)
|
||
ipAddress := middleware.GetIPFromContext(ctx)
|
||
userAgent := middleware.GetUserAgentFromContext(ctx)
|
||
|
||
s.auditService.LogOperation(ctx, &model.AccountOperationLog{
|
||
OperatorID: currentUserID,
|
||
OperatorType: userType,
|
||
OperatorName: operatorName,
|
||
TargetAccountID: &account.ID,
|
||
TargetUsername: &account.Username,
|
||
TargetUserType: &account.UserType,
|
||
OperationType: "remove_role",
|
||
OperationDesc: fmt.Sprintf("移除账号 %s 的角色", account.Username),
|
||
AfterData: afterData,
|
||
RequestID: requestID,
|
||
IPAddress: ipAddress,
|
||
UserAgent: userAgent,
|
||
})
|
||
|
||
return nil
|
||
}
|
||
|
||
// 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, "未授权访问")
|
||
}
|
||
|
||
_, 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 {
|
||
return errors.Wrap(errors.CodeInternalError, err, "密码哈希失败")
|
||
}
|
||
|
||
if err := s.accountStore.UpdatePassword(ctx, accountID, string(hashedPassword), currentUserID); err != nil {
|
||
return errors.Wrap(errors.CodeInternalError, err, "更新密码失败")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// 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, "未授权访问")
|
||
}
|
||
|
||
_, 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 err := s.accountStore.UpdateStatus(ctx, accountID, status, currentUserID); err != nil {
|
||
return errors.Wrap(errors.CodeInternalError, err, "更新状态失败")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// 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,
|
||
Status: 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
|
||
}
|