Files
junhong_cmp_fiber/internal/service/account/service.go
huang 80f560df33
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m17s
refactor(account): 统一账号管理API、完善权限检查和操作审计
- 合并 customer_account 和 shop_account 路由到统一的 account 接口
- 新增统一认证接口 (auth handler)
- 实现越权防护中间件和权限检查工具函数
- 新增操作审计日志模型和服务
- 更新数据库迁移 (版本 39: account_operation_log 表)
- 补充集成测试覆盖权限检查和审计日志场景
2026-02-02 17:23:20 +08:00

696 lines
21 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"
"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
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,
shopStore middleware.ShopStoreInterface,
enterpriseStore middleware.EnterpriseStoreInterface,
auditService AuditServiceInterface,
) *Service {
return &Service{
accountStore: accountStore,
roleStore: roleStore,
accountRoleStore: accountRoleStore,
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) ([]*model.Account, int64, error) {
opts := &store.QueryOptions{
Page: req.Page,
PageSize: req.PageSize,
OrderBy: "id DESC",
}
if opts.Page == 0 {
opts.Page = 1
}
if opts.PageSize == 0 {
opts.PageSize = constants.DefaultPageSize
}
filters := make(map[string]interface{})
if req.Username != "" {
filters["username"] = req.Username
}
if req.Phone != "" {
filters["phone"] = req.Phone
}
if req.UserType != nil {
filters["user_type"] = *req.UserType
}
if req.Status != nil {
filters["status"] = *req.Status
}
return s.accountStore.List(ctx, opts, filters)
}
// AssignRoles 为账号分配角色(支持空数组清空所有角色,超级管理员禁止分配)
func (s *Service) AssignRoles(ctx context.Context, accountID uint, roleIDs []uint) ([]*model.AccountRole, error) {
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
}