feat: 完成B端认证系统和商户管理模块测试补全
主要变更: - 新增B端认证系统(后台+H5):登录、登出、Token刷新、密码修改 - 完善商户管理和商户账号管理功能 - 补全单元测试(ShopService: 72.5%, ShopAccountService: 79.8%) - 新增集成测试(商户管理+商户账号管理) - 归档OpenSpec提案(add-shop-account-management, implement-b-end-auth-system) - 完善文档(使用指南、API文档、认证架构说明) 测试统计: - 13个测试套件,37个测试用例,100%通过率 - 平均覆盖率76.2%,达标 OpenSpec验证:通过(strict模式)
This commit is contained in:
260
internal/service/auth/service.go
Normal file
260
internal/service/auth/service.go
Normal file
@@ -0,0 +1,260 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auth"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
pkgGorm "github.com/break/junhong_cmp_fiber/pkg/gorm"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
accountStore *postgres.AccountStore
|
||||
accountRoleStore *postgres.AccountRoleStore
|
||||
rolePermStore *postgres.RolePermissionStore
|
||||
permissionStore *postgres.PermissionStore
|
||||
tokenManager *auth.TokenManager
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func New(
|
||||
accountStore *postgres.AccountStore,
|
||||
accountRoleStore *postgres.AccountRoleStore,
|
||||
rolePermStore *postgres.RolePermissionStore,
|
||||
permissionStore *postgres.PermissionStore,
|
||||
tokenManager *auth.TokenManager,
|
||||
logger *zap.Logger,
|
||||
) *Service {
|
||||
return &Service{
|
||||
accountStore: accountStore,
|
||||
accountRoleStore: accountRoleStore,
|
||||
rolePermStore: rolePermStore,
|
||||
permissionStore: permissionStore,
|
||||
tokenManager: tokenManager,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Login(ctx context.Context, req *model.LoginRequest, clientIP string) (*model.LoginResponse, error) {
|
||||
ctx = pkgGorm.SkipDataPermission(ctx)
|
||||
|
||||
account, err := s.accountStore.GetByUsernameOrPhone(ctx, req.Username)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
s.logger.Warn("登录失败:用户名不存在", zap.String("username", req.Username), zap.String("ip", clientIP))
|
||||
return nil, errors.New(errors.CodeInvalidCredentials, "用户名或密码错误")
|
||||
}
|
||||
return nil, errors.New(errors.CodeDatabaseError, fmt.Sprintf("查询账号失败: %v", err))
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(req.Password)); err != nil {
|
||||
s.logger.Warn("登录失败:密码错误", zap.String("username", req.Username), zap.String("ip", clientIP))
|
||||
return nil, errors.New(errors.CodeInvalidCredentials, "用户名或密码错误")
|
||||
}
|
||||
|
||||
if account.Status != 1 {
|
||||
s.logger.Warn("登录失败:账号已禁用", zap.String("username", req.Username), zap.Uint("user_id", account.ID))
|
||||
return nil, errors.New(errors.CodeAccountDisabled, "账号已禁用")
|
||||
}
|
||||
|
||||
device := req.Device
|
||||
if device == "" {
|
||||
device = "web"
|
||||
}
|
||||
|
||||
var shopID, enterpriseID uint
|
||||
if account.ShopID != nil {
|
||||
shopID = *account.ShopID
|
||||
}
|
||||
if account.EnterpriseID != nil {
|
||||
enterpriseID = *account.EnterpriseID
|
||||
}
|
||||
|
||||
tokenInfo := &auth.TokenInfo{
|
||||
UserID: account.ID,
|
||||
UserType: account.UserType,
|
||||
ShopID: shopID,
|
||||
EnterpriseID: enterpriseID,
|
||||
Username: account.Username,
|
||||
Device: device,
|
||||
IP: clientIP,
|
||||
}
|
||||
|
||||
accessToken, refreshToken, err := s.tokenManager.GenerateTokenPair(ctx, tokenInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
permissions, err := s.getUserPermissions(ctx, account.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("查询用户权限失败", zap.Uint("user_id", account.ID), zap.Error(err))
|
||||
permissions = []string{}
|
||||
}
|
||||
|
||||
userInfo := s.buildUserInfo(account)
|
||||
|
||||
s.logger.Info("用户登录成功",
|
||||
zap.Uint("user_id", account.ID),
|
||||
zap.String("username", account.Username),
|
||||
zap.String("device", device),
|
||||
zap.String("ip", clientIP),
|
||||
)
|
||||
|
||||
return &model.LoginResponse{
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: int64(constants.DefaultAccessTokenTTL.Seconds()),
|
||||
User: userInfo,
|
||||
Permissions: permissions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) Logout(ctx context.Context, accessToken, refreshToken string) error {
|
||||
if err := s.tokenManager.RevokeToken(ctx, accessToken); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if refreshToken != "" {
|
||||
if err := s.tokenManager.RevokeToken(ctx, refreshToken); err != nil {
|
||||
s.logger.Warn("撤销 refresh token 失败", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) RefreshToken(ctx context.Context, refreshToken string) (string, error) {
|
||||
return s.tokenManager.RefreshAccessToken(ctx, refreshToken)
|
||||
}
|
||||
|
||||
func (s *Service) GetCurrentUser(ctx context.Context, userID uint) (*model.UserInfo, []string, error) {
|
||||
account, err := s.accountStore.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil, errors.New(errors.CodeAccountNotFound, "账号不存在")
|
||||
}
|
||||
return nil, nil, errors.New(errors.CodeDatabaseError, fmt.Sprintf("查询账号失败: %v", err))
|
||||
}
|
||||
|
||||
permissions, err := s.getUserPermissions(ctx, userID)
|
||||
if err != nil {
|
||||
s.logger.Error("查询用户权限失败", zap.Uint("user_id", userID), zap.Error(err))
|
||||
permissions = []string{}
|
||||
}
|
||||
|
||||
userInfo := s.buildUserInfo(account)
|
||||
|
||||
return &userInfo, permissions, nil
|
||||
}
|
||||
|
||||
func (s *Service) ChangePassword(ctx context.Context, userID uint, oldPassword, newPassword string) error {
|
||||
account, err := s.accountStore.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeAccountNotFound, "账号不存在")
|
||||
}
|
||||
return errors.New(errors.CodeDatabaseError, fmt.Sprintf("查询账号失败: %v", err))
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(oldPassword)); err != nil {
|
||||
return errors.New(errors.CodeInvalidOldPassword, "旧密码错误")
|
||||
}
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to hash password: %w", err)
|
||||
}
|
||||
|
||||
if err := s.accountStore.UpdatePassword(ctx, userID, string(hashedPassword), userID); err != nil {
|
||||
return errors.New(errors.CodeDatabaseError, fmt.Sprintf("更新密码失败: %v", err))
|
||||
}
|
||||
|
||||
if err := s.tokenManager.RevokeAllUserTokens(ctx, userID); err != nil {
|
||||
s.logger.Warn("撤销用户所有 token 失败", zap.Uint("user_id", userID), zap.Error(err))
|
||||
}
|
||||
|
||||
s.logger.Info("用户修改密码成功", zap.Uint("user_id", userID))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) getUserPermissions(ctx context.Context, userID uint) ([]string, error) {
|
||||
accountRoles, err := s.accountRoleStore.GetByAccountID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get account roles: %w", err)
|
||||
}
|
||||
|
||||
if len(accountRoles) == 0 {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
roleIDs := make([]uint, 0, len(accountRoles))
|
||||
for _, ar := range accountRoles {
|
||||
roleIDs = append(roleIDs, ar.RoleID)
|
||||
}
|
||||
|
||||
permIDs, err := s.rolePermStore.GetPermIDsByRoleIDs(ctx, roleIDs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get permission IDs: %w", err)
|
||||
}
|
||||
|
||||
if len(permIDs) == 0 {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
permissions, err := s.permissionStore.GetByIDs(ctx, permIDs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get permissions: %w", err)
|
||||
}
|
||||
|
||||
permCodes := make([]string, 0, len(permissions))
|
||||
for _, perm := range permissions {
|
||||
permCodes = append(permCodes, perm.PermCode)
|
||||
}
|
||||
|
||||
return permCodes, nil
|
||||
}
|
||||
|
||||
func (s *Service) buildUserInfo(account *model.Account) model.UserInfo {
|
||||
userTypeName := s.getUserTypeName(account.UserType)
|
||||
|
||||
var shopID, enterpriseID uint
|
||||
if account.ShopID != nil {
|
||||
shopID = *account.ShopID
|
||||
}
|
||||
if account.EnterpriseID != nil {
|
||||
enterpriseID = *account.EnterpriseID
|
||||
}
|
||||
|
||||
return model.UserInfo{
|
||||
ID: account.ID,
|
||||
Username: account.Username,
|
||||
Phone: account.Phone,
|
||||
UserType: account.UserType,
|
||||
UserTypeName: userTypeName,
|
||||
ShopID: shopID,
|
||||
EnterpriseID: enterpriseID,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) getUserTypeName(userType int) string {
|
||||
switch userType {
|
||||
case constants.UserTypeSuperAdmin:
|
||||
return "超级管理员"
|
||||
case constants.UserTypePlatform:
|
||||
return "平台用户"
|
||||
case constants.UserTypeAgent:
|
||||
return "代理账号"
|
||||
case constants.UserTypeEnterprise:
|
||||
return "企业账号"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
// Package shop 提供店铺管理的业务逻辑服务
|
||||
// 包含店铺创建、查询、更新、删除等功能
|
||||
package shop
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store"
|
||||
@@ -11,55 +10,55 @@ import (
|
||||
"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 {
|
||||
shopStore *postgres.ShopStore
|
||||
shopStore *postgres.ShopStore
|
||||
accountStore *postgres.AccountStore
|
||||
}
|
||||
|
||||
// New 创建店铺服务
|
||||
func New(shopStore *postgres.ShopStore) *Service {
|
||||
func New(shopStore *postgres.ShopStore, accountStore *postgres.AccountStore) *Service {
|
||||
return &Service{
|
||||
shopStore: shopStore,
|
||||
shopStore: shopStore,
|
||||
accountStore: accountStore,
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建店铺
|
||||
func (s *Service) Create(ctx context.Context, req *model.CreateShopRequest) (*model.Shop, error) {
|
||||
// 获取当前用户 ID
|
||||
func (s *Service) Create(ctx context.Context, req *model.CreateShopRequest) (*model.ShopResponse, error) {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
// 检查店铺编号唯一性
|
||||
if req.ShopCode != "" {
|
||||
existing, err := s.shopStore.GetByCode(ctx, req.ShopCode)
|
||||
if err == nil && existing != nil {
|
||||
return nil, errors.New(errors.CodeShopCodeExists, "店铺编号已存在")
|
||||
}
|
||||
existing, err := s.shopStore.GetByCode(ctx, req.ShopCode)
|
||||
if err == nil && existing != nil {
|
||||
return nil, errors.New(errors.CodeShopCodeExists, "店铺编号已存在")
|
||||
}
|
||||
|
||||
// 计算层级
|
||||
level := 1
|
||||
if req.ParentID != nil {
|
||||
// 验证上级店铺存在
|
||||
parent, err := s.shopStore.GetByID(ctx, *req.ParentID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.CodeInvalidParentID, "上级店铺不存在或无效")
|
||||
}
|
||||
|
||||
// 计算新店铺的层级
|
||||
level = parent.Level + 1
|
||||
|
||||
// 校验层级不超过最大值
|
||||
if level > constants.MaxShopLevel {
|
||||
if level > constants.ShopMaxLevel {
|
||||
return nil, errors.New(errors.CodeShopLevelExceeded, "店铺层级不能超过 7 级")
|
||||
}
|
||||
}
|
||||
|
||||
// 创建店铺
|
||||
existingAccount, err := s.accountStore.GetByUsername(ctx, req.InitUsername)
|
||||
if err == nil && existingAccount != nil {
|
||||
return nil, errors.New(errors.CodeUsernameExists, "初始账号用户名已存在")
|
||||
}
|
||||
|
||||
existingAccount, err = s.accountStore.GetByPhone(ctx, req.InitPhone)
|
||||
if err == nil && existingAccount != nil {
|
||||
return nil, errors.New(errors.CodePhoneExists, "初始账号手机号已存在")
|
||||
}
|
||||
|
||||
shop := &model.Shop{
|
||||
ShopName: req.ShopName,
|
||||
ShopCode: req.ShopCode,
|
||||
@@ -71,71 +70,94 @@ func (s *Service) Create(ctx context.Context, req *model.CreateShopRequest) (*mo
|
||||
City: req.City,
|
||||
District: req.District,
|
||||
Address: req.Address,
|
||||
Status: constants.StatusEnabled,
|
||||
Status: constants.ShopStatusEnabled,
|
||||
}
|
||||
shop.Creator = currentUserID
|
||||
shop.Updater = currentUserID
|
||||
|
||||
if err := s.shopStore.Create(ctx, shop); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("创建店铺失败: %w", err)
|
||||
}
|
||||
|
||||
return shop, nil
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.InitPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("密码哈希失败: %w", err)
|
||||
}
|
||||
|
||||
account := &model.Account{
|
||||
Username: req.InitUsername,
|
||||
Phone: req.InitPhone,
|
||||
Password: string(hashedPassword),
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &shop.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
account.Creator = currentUserID
|
||||
account.Updater = currentUserID
|
||||
|
||||
if err := s.accountStore.Create(ctx, account); err != nil {
|
||||
return nil, fmt.Errorf("创建初始账号失败: %w", err)
|
||||
}
|
||||
|
||||
return &model.ShopResponse{
|
||||
ID: shop.ID,
|
||||
ShopName: shop.ShopName,
|
||||
ShopCode: shop.ShopCode,
|
||||
ParentID: shop.ParentID,
|
||||
Level: shop.Level,
|
||||
ContactName: shop.ContactName,
|
||||
ContactPhone: shop.ContactPhone,
|
||||
Province: shop.Province,
|
||||
City: shop.City,
|
||||
District: shop.District,
|
||||
Address: shop.Address,
|
||||
Status: shop.Status,
|
||||
CreatedAt: shop.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: shop.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Update 更新店铺信息
|
||||
func (s *Service) Update(ctx context.Context, id uint, req *model.UpdateShopRequest) (*model.Shop, error) {
|
||||
// 获取当前用户 ID
|
||||
func (s *Service) Update(ctx context.Context, id uint, req *model.UpdateShopRequest) (*model.ShopResponse, error) {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
// 查询店铺
|
||||
shop, err := s.shopStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.CodeShopNotFound, "店铺不存在")
|
||||
}
|
||||
|
||||
// 检查店铺编号唯一性(如果修改了编号)
|
||||
if req.ShopCode != nil && *req.ShopCode != shop.ShopCode {
|
||||
existing, err := s.shopStore.GetByCode(ctx, *req.ShopCode)
|
||||
if err == nil && existing != nil && existing.ID != id {
|
||||
return nil, errors.New(errors.CodeShopCodeExists, "店铺编号已存在")
|
||||
}
|
||||
shop.ShopCode = *req.ShopCode
|
||||
}
|
||||
|
||||
// 更新字段
|
||||
if req.ShopName != nil {
|
||||
shop.ShopName = *req.ShopName
|
||||
}
|
||||
if req.ContactName != nil {
|
||||
shop.ContactName = *req.ContactName
|
||||
}
|
||||
if req.ContactPhone != nil {
|
||||
shop.ContactPhone = *req.ContactPhone
|
||||
}
|
||||
if req.Province != nil {
|
||||
shop.Province = *req.Province
|
||||
}
|
||||
if req.City != nil {
|
||||
shop.City = *req.City
|
||||
}
|
||||
if req.District != nil {
|
||||
shop.District = *req.District
|
||||
}
|
||||
if req.Address != nil {
|
||||
shop.Address = *req.Address
|
||||
}
|
||||
|
||||
shop.ShopName = req.ShopName
|
||||
shop.ContactName = req.ContactName
|
||||
shop.ContactPhone = req.ContactPhone
|
||||
shop.Province = req.Province
|
||||
shop.City = req.City
|
||||
shop.District = req.District
|
||||
shop.Address = req.Address
|
||||
shop.Status = req.Status
|
||||
shop.Updater = currentUserID
|
||||
|
||||
if err := s.shopStore.Update(ctx, shop); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return shop, nil
|
||||
return &model.ShopResponse{
|
||||
ID: shop.ID,
|
||||
ShopName: shop.ShopName,
|
||||
ShopCode: shop.ShopCode,
|
||||
ParentID: shop.ParentID,
|
||||
Level: shop.Level,
|
||||
ContactName: shop.ContactName,
|
||||
ContactPhone: shop.ContactPhone,
|
||||
Province: shop.Province,
|
||||
City: shop.City,
|
||||
District: shop.District,
|
||||
Address: shop.Address,
|
||||
Status: shop.Status,
|
||||
CreatedAt: shop.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: shop.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Disable 禁用店铺
|
||||
@@ -189,11 +211,104 @@ func (s *Service) GetByID(ctx context.Context, id uint) (*model.Shop, error) {
|
||||
return shop, nil
|
||||
}
|
||||
|
||||
// List 查询店铺列表
|
||||
func (s *Service) ListShopResponses(ctx context.Context, req *model.ShopListRequest) ([]*model.ShopResponse, int64, error) {
|
||||
opts := &store.QueryOptions{
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
OrderBy: "created_at DESC",
|
||||
}
|
||||
if opts.Page == 0 {
|
||||
opts.Page = 1
|
||||
}
|
||||
if opts.PageSize == 0 {
|
||||
opts.PageSize = constants.DefaultPageSize
|
||||
}
|
||||
|
||||
filters := make(map[string]interface{})
|
||||
if req.ShopName != "" {
|
||||
filters["shop_name"] = req.ShopName
|
||||
}
|
||||
if req.ShopCode != "" {
|
||||
filters["shop_code"] = req.ShopCode
|
||||
}
|
||||
if req.ParentID != nil {
|
||||
filters["parent_id"] = *req.ParentID
|
||||
}
|
||||
if req.Level != nil {
|
||||
filters["level"] = *req.Level
|
||||
}
|
||||
if req.Status != nil {
|
||||
filters["status"] = *req.Status
|
||||
}
|
||||
|
||||
shops, total, err := s.shopStore.List(ctx, opts, filters)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("查询店铺列表失败: %w", err)
|
||||
}
|
||||
|
||||
responses := make([]*model.ShopResponse, 0, len(shops))
|
||||
for _, shop := range shops {
|
||||
responses = append(responses, &model.ShopResponse{
|
||||
ID: shop.ID,
|
||||
ShopName: shop.ShopName,
|
||||
ShopCode: shop.ShopCode,
|
||||
ParentID: shop.ParentID,
|
||||
Level: shop.Level,
|
||||
ContactName: shop.ContactName,
|
||||
ContactPhone: shop.ContactPhone,
|
||||
Province: shop.Province,
|
||||
City: shop.City,
|
||||
District: shop.District,
|
||||
Address: shop.Address,
|
||||
Status: shop.Status,
|
||||
CreatedAt: shop.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: shop.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
|
||||
return responses, total, nil
|
||||
}
|
||||
|
||||
func (s *Service) List(ctx context.Context, opts *store.QueryOptions, filters map[string]interface{}) ([]*model.Shop, int64, error) {
|
||||
return s.shopStore.List(ctx, opts, filters)
|
||||
}
|
||||
|
||||
func (s *Service) Delete(ctx context.Context, id uint) error {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
shop, err := s.shopStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeShopNotFound, "店铺不存在")
|
||||
}
|
||||
return fmt.Errorf("获取店铺失败: %w", err)
|
||||
}
|
||||
|
||||
accounts, err := s.accountStore.GetByShopID(ctx, shop.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询店铺账号失败: %w", err)
|
||||
}
|
||||
|
||||
if len(accounts) > 0 {
|
||||
accountIDs := make([]uint, 0, len(accounts))
|
||||
for _, account := range accounts {
|
||||
accountIDs = append(accountIDs, account.ID)
|
||||
}
|
||||
if err := s.accountStore.BulkUpdateStatus(ctx, accountIDs, constants.StatusDisabled, currentUserID); err != nil {
|
||||
return fmt.Errorf("禁用店铺账号失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.shopStore.Delete(ctx, id); err != nil {
|
||||
return fmt.Errorf("删除店铺失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSubordinateShopIDs 获取下级店铺 ID 列表(包含自己)
|
||||
func (s *Service) GetSubordinateShopIDs(ctx context.Context, shopID uint) ([]uint, error) {
|
||||
return s.shopStore.GetSubordinateShopIDs(ctx, shopID)
|
||||
|
||||
265
internal/service/shop_account/service.go
Normal file
265
internal/service/shop_account/service.go
Normal file
@@ -0,0 +1,265 @@
|
||||
package shop_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"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
accountStore *postgres.AccountStore
|
||||
shopStore *postgres.ShopStore
|
||||
}
|
||||
|
||||
func New(accountStore *postgres.AccountStore, shopStore *postgres.ShopStore) *Service {
|
||||
return &Service{
|
||||
accountStore: accountStore,
|
||||
shopStore: shopStore,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) List(ctx context.Context, req *model.ShopAccountListRequest) ([]*model.ShopAccountResponse, int64, error) {
|
||||
opts := &store.QueryOptions{
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
OrderBy: "created_at DESC",
|
||||
}
|
||||
if opts.Page == 0 {
|
||||
opts.Page = 1
|
||||
}
|
||||
if opts.PageSize == 0 {
|
||||
opts.PageSize = constants.DefaultPageSize
|
||||
}
|
||||
|
||||
filters := make(map[string]interface{})
|
||||
filters["user_type"] = constants.UserTypeAgent
|
||||
if req.Username != "" {
|
||||
filters["username"] = req.Username
|
||||
}
|
||||
if req.Phone != "" {
|
||||
filters["phone"] = req.Phone
|
||||
}
|
||||
if req.Status != nil {
|
||||
filters["status"] = *req.Status
|
||||
}
|
||||
|
||||
var accounts []*model.Account
|
||||
var total int64
|
||||
var err error
|
||||
|
||||
if req.ShopID != nil {
|
||||
accounts, total, err = s.accountStore.ListByShopID(ctx, *req.ShopID, opts, filters)
|
||||
} else {
|
||||
filters["user_type"] = constants.UserTypeAgent
|
||||
accounts, total, err = s.accountStore.List(ctx, opts, filters)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("查询代理商账号列表失败: %w", err)
|
||||
}
|
||||
|
||||
shopMap := make(map[uint]string)
|
||||
for _, account := range accounts {
|
||||
if account.ShopID != nil {
|
||||
if _, exists := shopMap[*account.ShopID]; !exists {
|
||||
shop, err := s.shopStore.GetByID(ctx, *account.ShopID)
|
||||
if err == nil {
|
||||
shopMap[*account.ShopID] = shop.ShopName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
responses := make([]*model.ShopAccountResponse, 0, len(accounts))
|
||||
for _, account := range accounts {
|
||||
resp := &model.ShopAccountResponse{
|
||||
ID: account.ID,
|
||||
Username: account.Username,
|
||||
Phone: account.Phone,
|
||||
UserType: account.UserType,
|
||||
Status: account.Status,
|
||||
CreatedAt: account.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: account.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
if account.ShopID != nil {
|
||||
resp.ShopID = *account.ShopID
|
||||
if shopName, ok := shopMap[*account.ShopID]; ok {
|
||||
resp.ShopName = shopName
|
||||
}
|
||||
}
|
||||
responses = append(responses, resp)
|
||||
}
|
||||
|
||||
return responses, total, nil
|
||||
}
|
||||
|
||||
func (s *Service) Create(ctx context.Context, req *model.CreateShopAccountRequest) (*model.ShopAccountResponse, error) {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
shop, err := s.shopStore.GetByID(ctx, req.ShopID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeShopNotFound, "店铺不存在")
|
||||
}
|
||||
return nil, fmt.Errorf("获取店铺失败: %w", 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, fmt.Errorf("密码哈希失败: %w", err)
|
||||
}
|
||||
|
||||
account := &model.Account{
|
||||
Username: req.Username,
|
||||
Phone: req.Phone,
|
||||
Password: string(hashedPassword),
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &req.ShopID,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
account.Creator = currentUserID
|
||||
account.Updater = currentUserID
|
||||
|
||||
if err := s.accountStore.Create(ctx, account); err != nil {
|
||||
return nil, fmt.Errorf("创建代理商账号失败: %w", err)
|
||||
}
|
||||
|
||||
return &model.ShopAccountResponse{
|
||||
ID: account.ID,
|
||||
ShopID: *account.ShopID,
|
||||
ShopName: shop.ShopName,
|
||||
Username: account.Username,
|
||||
Phone: account.Phone,
|
||||
UserType: account.UserType,
|
||||
Status: account.Status,
|
||||
CreatedAt: account.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: account.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) Update(ctx context.Context, id uint, req *model.UpdateShopAccountRequest) (*model.ShopAccountResponse, 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.CodeAccountNotFound, "账号不存在")
|
||||
}
|
||||
return nil, fmt.Errorf("获取账号失败: %w", err)
|
||||
}
|
||||
|
||||
if account.UserType != constants.UserTypeAgent {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "只能更新代理商账号")
|
||||
}
|
||||
|
||||
existingAccount, err := s.accountStore.GetByUsername(ctx, req.Username)
|
||||
if err == nil && existingAccount != nil && existingAccount.ID != id {
|
||||
return nil, errors.New(errors.CodeUsernameExists, "用户名已存在")
|
||||
}
|
||||
|
||||
account.Username = req.Username
|
||||
account.Updater = currentUserID
|
||||
|
||||
if err := s.accountStore.Update(ctx, account); err != nil {
|
||||
return nil, fmt.Errorf("更新代理商账号失败: %w", err)
|
||||
}
|
||||
|
||||
var shopName string
|
||||
if account.ShopID != nil {
|
||||
shop, err := s.shopStore.GetByID(ctx, *account.ShopID)
|
||||
if err == nil {
|
||||
shopName = shop.ShopName
|
||||
}
|
||||
}
|
||||
|
||||
return &model.ShopAccountResponse{
|
||||
ID: account.ID,
|
||||
ShopID: *account.ShopID,
|
||||
ShopName: shopName,
|
||||
Username: account.Username,
|
||||
Phone: account.Phone,
|
||||
UserType: account.UserType,
|
||||
Status: account.Status,
|
||||
CreatedAt: account.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: account.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdatePassword(ctx context.Context, id uint, req *model.UpdateShopAccountPasswordRequest) 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.CodeAccountNotFound, "账号不存在")
|
||||
}
|
||||
return fmt.Errorf("获取账号失败: %w", err)
|
||||
}
|
||||
|
||||
if account.UserType != constants.UserTypeAgent {
|
||||
return errors.New(errors.CodeInvalidParam, "只能更新代理商账号密码")
|
||||
}
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("密码哈希失败: %w", err)
|
||||
}
|
||||
|
||||
if err := s.accountStore.UpdatePassword(ctx, id, string(hashedPassword), currentUserID); err != nil {
|
||||
return fmt.Errorf("更新密码失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdateStatus(ctx context.Context, id uint, req *model.UpdateShopAccountStatusRequest) 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.CodeAccountNotFound, "账号不存在")
|
||||
}
|
||||
return fmt.Errorf("获取账号失败: %w", err)
|
||||
}
|
||||
|
||||
if account.UserType != constants.UserTypeAgent {
|
||||
return errors.New(errors.CodeInvalidParam, "只能更新代理商账号状态")
|
||||
}
|
||||
|
||||
if err := s.accountStore.UpdateStatus(ctx, id, req.Status, currentUserID); err != nil {
|
||||
return fmt.Errorf("更新账号状态失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user