Files
junhong_cmp_fiber/internal/service/account/service.go
huang 409a68d60b
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m45s
feat: OpenAPI 契约对齐与框架优化
主要变更:
1. OpenAPI 文档契约对齐
   - 统一错误响应字段名为 msg(非 message)
   - 规范 envelope 响应结构(code, msg, data, timestamp)
   - 个人客户路由纳入文档体系(使用 Register 机制)
   - 新增 BuildDocHandlers() 统一管理 handler 构造
   - 确保文档生成的幂等性

2. Service 层错误处理统一
   - 全面替换 fmt.Errorf 为 errors.New/Wrap
   - 统一错误码使用规范
   - Handler 层参数校验不泄露底层细节
   - 新增错误码验证集成测试

3. 代码质量提升
   - 删除未使用的 Task handler 和路由
   - 新增代码规范检查脚本(check-service-errors.sh)
   - 新增注释路径一致性检查(check-comment-paths.sh)
   - 更新 API 文档生成指南

4. OpenSpec 归档
   - 归档 openapi-contract-alignment 变更(63 tasks)
   - 归档 service-error-unify-core 变更
   - 归档 service-error-unify-support 变更
   - 归档 code-cleanup-docs-update 变更
   - 归档 handler-validation-security 变更
   - 同步 delta specs 到主规范文件

影响范围:
- pkg/openapi: 新增 handlers.go,优化 generator.go
- internal/service/*: 48 个 service 文件错误处理统一
- internal/handler/admin: 优化参数校验错误提示
- internal/routes: 个人客户路由改造,删除 task 路由
- scripts: 新增 3 个代码检查脚本
- docs: 更新 OpenAPI 文档(15750+ 行)
- openspec/specs: 同步 3 个主规范文件

破坏性变更:无
向后兼容:是
2026-01-30 11:40:36 +08:00

462 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/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
}
// 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 *dto.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, 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, "创建账号失败")
}
// 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, errors.Wrap(errors.CodeInternalError, err, "获取账号失败")
}
return account, nil
}
// Update 更新账号
func (s *Service) Update(ctx context.Context, id uint, req *dto.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, errors.Wrap(errors.CodeInternalError, 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, 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, "更新账号失败")
}
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 errors.Wrap(errors.CodeInternalError, err, "获取账号失败")
}
if err := s.accountStore.Delete(ctx, id); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "删除账号失败")
}
// 账号删除后不需要清理缓存
// 数据权限过滤现在基于店铺层级,店铺相关的缓存清理由 ShopService 负责
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.CodeAccountNotFound, "账号不存在")
}
return nil, errors.Wrap(errors.CodeInternalError, 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, 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)
}
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 {
// 检查账号存在
_, 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.accountRoleStore.Delete(ctx, accountID, roleID); err != nil {
return errors.Wrap(errors.CodeInternalError, 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 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
}