Files
junhong_cmp_fiber/internal/service/account/service.go
huang b1195c16df feat(account): 实现平台账号管理功能
- 新增平台账号列表查询接口(自动筛选超级管理员和平台用户)
- 新增密码修改和状态切换专用接口
- 增强角色分配功能,支持空数组清空所有角色
- 新增超级管理员保护机制,禁止分配角色
- 新增完整的集成测试和OpenSpec规范文档
2026-01-14 17:00:30 +08:00

461 lines
14 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/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
}
// New 创建账号服务
func New(accountStore *postgres.AccountStore, roleStore *postgres.RoleStore, accountRoleStore *postgres.AccountRoleStore) *Service {
return &Service{
accountStore: accountStore,
roleStore: roleStore,
accountRoleStore: accountRoleStore,
}
}
// Create 创建账号
func (s *Service) Create(ctx context.Context, req *model.CreateAccountRequest) (*model.Account, error) {
// 获取当前用户 ID
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
}
// 验证代理账号必须提供 shop_id
if req.UserType == constants.UserTypeAgent && req.ShopID == nil {
return nil, errors.New(errors.CodeInvalidParam, "代理账号必须提供店铺ID")
}
// 验证企业账号必须提供 enterprise_id
if req.UserType == constants.UserTypeEnterprise && req.EnterpriseID == nil {
return nil, errors.New(errors.CodeInvalidParam, "企业账号必须提供企业ID")
}
// 检查用户名唯一性
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, "手机号已存在")
}
// bcrypt 哈希密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, fmt.Errorf("密码哈希失败: %w", 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, fmt.Errorf("创建账号失败: %w", err)
}
// TODO: 清除店铺的下级 ID 缓存(需要在 Service 层处理)
// 由于账号层级关系改为通过 Shop 表维护,这里的缓存清理逻辑已废弃
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, fmt.Errorf("获取账号失败: %w", err)
}
return account, nil
}
// Update 更新账号
func (s *Service) Update(ctx context.Context, id uint, req *model.UpdateAccountRequest) (*model.Account, error) {
// 获取当前用户 ID
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.CodeAccountNotFound, "账号不存在")
}
return nil, fmt.Errorf("获取账号失败: %w", err)
}
// 更新字段
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, fmt.Errorf("密码哈希失败: %w", 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, fmt.Errorf("更新账号失败: %w", err)
}
return account, nil
}
// Delete 软删除账号
func (s *Service) Delete(ctx context.Context, id uint) error {
// 检查账号存在
_, err := s.accountStore.GetByID(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeAccountNotFound, "账号不存在")
}
return fmt.Errorf("获取账号失败: %w", err)
}
if err := s.accountStore.Delete(ctx, id); err != nil {
return fmt.Errorf("删除账号失败: %w", err)
}
// 账号删除后不需要清理缓存
// 数据权限过滤现在基于店铺层级,店铺相关的缓存清理由 ShopService 负责
return nil
}
// List 查询账号列表
func (s *Service) List(ctx context.Context, req *model.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.CodeAccountNotFound, "账号不存在")
}
return nil, fmt.Errorf("获取账号失败: %w", err)
}
// 超级管理员禁止分配角色
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, fmt.Errorf("清空账号角色失败: %w", 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, fmt.Errorf("统计现有角色数量失败: %w", 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, fmt.Errorf("获取角色失败: %w", 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, fmt.Errorf("创建账号-角色关联失败: %w", err)
}
ars = append(ars, ar)
}
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, fmt.Errorf("获取账号失败: %w", err)
}
// 获取角色 ID 列表
roleIDs, err := s.accountRoleStore.GetRoleIDsByAccountID(ctx, accountID)
if err != nil {
return nil, fmt.Errorf("获取账号角色 ID 失败: %w", err)
}
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 {
// 检查账号存在
_, err := s.accountStore.GetByID(ctx, accountID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeAccountNotFound, "账号不存在")
}
return fmt.Errorf("获取账号失败: %w", err)
}
// 删除关联
if err := s.accountRoleStore.Delete(ctx, accountID, roleID); err != nil {
return fmt.Errorf("删除账号-角色关联失败: %w", err)
}
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 fmt.Errorf("获取账号失败: %w", err)
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("密码哈希失败: %w", err)
}
if err := s.accountStore.UpdatePassword(ctx, accountID, string(hashedPassword), currentUserID); err != nil {
return fmt.Errorf("更新密码失败: %w", 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 fmt.Errorf("获取账号失败: %w", err)
}
if err := s.accountStore.UpdateStatus(ctx, accountID, status, currentUserID); err != nil {
return fmt.Errorf("更新状态失败: %w", err)
}
return nil
}
// ListPlatformAccounts 查询平台账号列表(自动筛选 user_type IN (1, 2)
func (s *Service) ListPlatformAccounts(ctx context.Context, req *model.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 fmt.Errorf("密码哈希失败: %w", err)
}
account.Password = string(hashedPassword)
if err := s.accountStore.Create(ctx, account); err != nil {
return fmt.Errorf("创建账号失败: %w", err)
}
return nil
}