refactor(account): 统一账号管理API、完善权限检查和操作审计
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m17s

- 合并 customer_account 和 shop_account 路由到统一的 account 接口
- 新增统一认证接口 (auth handler)
- 实现越权防护中间件和权限检查工具函数
- 新增操作审计日志模型和服务
- 更新数据库迁移 (版本 39: account_operation_log 表)
- 补充集成测试覆盖权限检查和审计日志场景
This commit is contained in:
2026-02-02 17:23:20 +08:00
parent 5851cc6403
commit 80f560df33
58 changed files with 10743 additions and 4915 deletions

View File

@@ -22,54 +22,86 @@ 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) *Service {
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) {
// 获取当前用户 ID
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
}
// 验证代理账号必须提供 shop_id
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")
}
// 验证企业账号必须提供 enterprise_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, "手机号已存在")
}
// 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,
@@ -84,8 +116,40 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateAccountRequest) (*m
return nil, errors.Wrap(errors.CodeInternalError, err, "创建账号失败")
}
// TODO: 清除店铺的下级 ID 缓存(需要在 Service 层处理)
// 由于账号层级关系改为通过 Shop 表维护,这里的缓存清理逻辑已废弃
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
}
@@ -104,24 +168,37 @@ func (s *Service) Get(ctx context.Context, id uint) (*model.Account, error) {
// 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.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, "用户名已存在")
@@ -130,7 +207,6 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateAccountReq
}
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, "手机号已存在")
@@ -156,26 +232,102 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateAccountReq
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 {
// 检查账号存在
_, err := s.accountStore.GetByID(ctx, id)
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 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, "删除账号失败")
}
// 账号删除后不需要清理缓存
// 数据权限过滤现在基于店铺层级,店铺相关的缓存清理由 ShopService 负责
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
}
@@ -221,12 +373,22 @@ func (s *Service) AssignRoles(ctx context.Context, accountID uint, roleIDs []uin
account, err := s.accountStore.GetByID(ctx, accountID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeAccountNotFound, "账号不存在")
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, "超级管理员不允许分配角色")
}
@@ -295,6 +457,35 @@ func (s *Service) AssignRoles(ctx context.Context, accountID uint, roleIDs []uin
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
}
@@ -325,20 +516,63 @@ func (s *Service) GetRoles(ctx context.Context, accountID uint) ([]*model.Role,
// RemoveRole 移除账号的角色
func (s *Service) RemoveRole(ctx context.Context, accountID, roleID uint) error {
// 检查账号存在
_, err := s.accountStore.GetByID(ctx, accountID)
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.CodeAccountNotFound, "账号不存在")
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
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
// Package account_audit 提供账号操作审计日志服务
// 负责记录所有账号管理操作,用于审计追踪和合规要求
package account_audit
import (
"context"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/logger"
"go.uber.org/zap"
)
// AccountOperationLogStore 账号操作日志存储接口
type AccountOperationLogStore interface {
Create(ctx context.Context, log *model.AccountOperationLog) error
}
// Service 账号审计服务
type Service struct {
store AccountOperationLogStore
}
// NewService 创建账号审计服务实例
func NewService(store AccountOperationLogStore) *Service {
return &Service{
store: store,
}
}
// LogOperation 记录账号操作日志(异步写入,不阻塞主流程)
func (s *Service) LogOperation(ctx context.Context, log *model.AccountOperationLog) {
// 异步写入审计日志,不阻塞业务操作
go func() {
if err := s.store.Create(context.Background(), log); err != nil {
// 写入失败只记录错误日志,不影响业务
logger.GetAppLogger().Error("写入账号操作日志失败",
zap.Uint("operator_id", log.OperatorID),
zap.String("operation_type", log.OperationType),
zap.Error(err))
}
}()
}

View File

@@ -0,0 +1,145 @@
package account_audit
import (
"context"
"errors"
"testing"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type MockAccountOperationLogStore struct {
mock.Mock
}
func (m *MockAccountOperationLogStore) Create(ctx context.Context, log *model.AccountOperationLog) error {
args := m.Called(ctx, log)
return args.Error(0)
}
func TestLogOperation_Success(t *testing.T) {
mockStore := new(MockAccountOperationLogStore)
service := NewService(mockStore)
log := &model.AccountOperationLog{
OperatorID: 1,
OperatorType: 2,
OperatorName: "admin",
OperationType: "create",
OperationDesc: "创建账号: testuser",
}
mockStore.On("Create", mock.Anything, log).Return(nil)
ctx := context.Background()
service.LogOperation(ctx, log)
time.Sleep(50 * time.Millisecond)
mockStore.AssertCalled(t, "Create", mock.Anything, log)
}
func TestLogOperation_Failure(t *testing.T) {
mockStore := new(MockAccountOperationLogStore)
service := NewService(mockStore)
log := &model.AccountOperationLog{
OperatorID: 1,
OperatorType: 2,
OperatorName: "admin",
OperationType: "create",
OperationDesc: "创建账号: testuser",
}
mockStore.On("Create", mock.Anything, log).Return(errors.New("database error"))
ctx := context.Background()
assert.NotPanics(t, func() {
service.LogOperation(ctx, log)
})
time.Sleep(50 * time.Millisecond)
mockStore.AssertCalled(t, "Create", mock.Anything, log)
}
func TestLogOperation_NonBlocking(t *testing.T) {
mockStore := new(MockAccountOperationLogStore)
service := NewService(mockStore)
log := &model.AccountOperationLog{
OperatorID: 1,
OperatorType: 2,
OperatorName: "admin",
OperationType: "create",
OperationDesc: "创建账号: testuser",
}
mockStore.On("Create", mock.Anything, log).Run(func(args mock.Arguments) {
time.Sleep(100 * time.Millisecond)
}).Return(nil)
ctx := context.Background()
start := time.Now()
service.LogOperation(ctx, log)
elapsed := time.Since(start)
assert.Less(t, elapsed, 50*time.Millisecond, "LogOperation should return immediately")
time.Sleep(150 * time.Millisecond)
mockStore.AssertCalled(t, "Create", mock.Anything, log)
}
func TestNewService(t *testing.T) {
mockStore := new(MockAccountOperationLogStore)
service := NewService(mockStore)
assert.NotNil(t, service)
assert.Equal(t, mockStore, service.store)
}
func TestLogOperation_WithAllFields(t *testing.T) {
mockStore := new(MockAccountOperationLogStore)
service := NewService(mockStore)
targetAccountID := uint(10)
targetUsername := "targetuser"
targetUserType := 3
requestID := "req-12345"
ipAddress := "127.0.0.1"
userAgent := "Mozilla/5.0"
log := &model.AccountOperationLog{
OperatorID: 1,
OperatorType: 2,
OperatorName: "admin",
TargetAccountID: &targetAccountID,
TargetUsername: &targetUsername,
TargetUserType: &targetUserType,
OperationType: "update",
OperationDesc: "更新账号: targetuser",
BeforeData: model.JSONB{
"username": "oldname",
},
AfterData: model.JSONB{
"username": "newname",
},
RequestID: &requestID,
IPAddress: &ipAddress,
UserAgent: &userAgent,
}
mockStore.On("Create", mock.Anything, log).Return(nil)
ctx := context.Background()
service.LogOperation(ctx, log)
time.Sleep(50 * time.Millisecond)
mockStore.AssertCalled(t, "Create", mock.Anything, log)
}

View File

@@ -1,328 +0,0 @@
package customer_account
import (
"context"
"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/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 {
db *gorm.DB
accountStore *postgres.AccountStore
shopStore *postgres.ShopStore
enterpriseStore *postgres.EnterpriseStore
}
func New(
db *gorm.DB,
accountStore *postgres.AccountStore,
shopStore *postgres.ShopStore,
enterpriseStore *postgres.EnterpriseStore,
) *Service {
return &Service{
db: db,
accountStore: accountStore,
shopStore: shopStore,
enterpriseStore: enterpriseStore,
}
}
func (s *Service) List(ctx context.Context, req *dto.CustomerAccountListReq) (*dto.CustomerAccountPageResult, error) {
page := req.Page
pageSize := req.PageSize
if page == 0 {
page = 1
}
if pageSize == 0 {
pageSize = constants.DefaultPageSize
}
query := s.db.WithContext(ctx).Model(&model.Account{}).
Where("user_type IN ?", []int{constants.UserTypeAgent, constants.UserTypeEnterprise})
if req.Username != "" {
query = query.Where("username LIKE ?", "%"+req.Username+"%")
}
if req.Phone != "" {
query = query.Where("phone LIKE ?", "%"+req.Phone+"%")
}
if req.UserType != nil {
query = query.Where("user_type = ?", *req.UserType)
}
if req.ShopID != nil {
query = query.Where("shop_id = ?", *req.ShopID)
}
if req.EnterpriseID != nil {
query = query.Where("enterprise_id = ?", *req.EnterpriseID)
}
if req.Status != nil {
query = query.Where("status = ?", *req.Status)
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "统计账号数量失败")
}
var accounts []model.Account
offset := (page - 1) * pageSize
if err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&accounts).Error; err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询账号列表失败")
}
shopIDs := make([]uint, 0)
enterpriseIDs := make([]uint, 0)
for _, acc := range accounts {
if acc.ShopID != nil {
shopIDs = append(shopIDs, *acc.ShopID)
}
if acc.EnterpriseID != nil {
enterpriseIDs = append(enterpriseIDs, *acc.EnterpriseID)
}
}
shopMap := make(map[uint]string)
if len(shopIDs) > 0 {
var shops []model.Shop
s.db.WithContext(ctx).Where("id IN ?", shopIDs).Find(&shops)
for _, shop := range shops {
shopMap[shop.ID] = shop.ShopName
}
}
enterpriseMap := make(map[uint]string)
if len(enterpriseIDs) > 0 {
var enterprises []model.Enterprise
s.db.WithContext(ctx).Where("id IN ?", enterpriseIDs).Find(&enterprises)
for _, ent := range enterprises {
enterpriseMap[ent.ID] = ent.EnterpriseName
}
}
items := make([]dto.CustomerAccountItem, 0, len(accounts))
for _, acc := range accounts {
shopName := ""
if acc.ShopID != nil {
shopName = shopMap[*acc.ShopID]
}
enterpriseName := ""
if acc.EnterpriseID != nil {
enterpriseName = enterpriseMap[*acc.EnterpriseID]
}
items = append(items, dto.CustomerAccountItem{
ID: acc.ID,
Username: acc.Username,
Phone: acc.Phone,
UserType: acc.UserType,
UserTypeName: getUserTypeName(acc.UserType),
ShopID: acc.ShopID,
ShopName: shopName,
EnterpriseID: acc.EnterpriseID,
EnterpriseName: enterpriseName,
Status: acc.Status,
StatusName: getStatusName(acc.Status),
CreatedAt: acc.CreatedAt.Format("2006-01-02 15:04:05"),
})
}
return &dto.CustomerAccountPageResult{
Items: items,
Total: total,
Page: page,
Size: pageSize,
}, nil
}
func (s *Service) Create(ctx context.Context, req *dto.CreateCustomerAccountReq) (*dto.CustomerAccountItem, error) {
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
}
_, err := s.shopStore.GetByID(ctx, req.ShopID)
if err != nil {
return nil, errors.New(errors.CodeShopNotFound, "店铺不存在")
}
existingAccount, _ := s.accountStore.GetByPhone(ctx, req.Phone)
if existingAccount != 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: constants.UserTypeAgent,
ShopID: &req.ShopID,
Status: constants.StatusEnabled,
}
account.Creator = currentUserID
account.Updater = currentUserID
if err := s.db.WithContext(ctx).Create(account).Error; err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "创建账号失败")
}
shop, _ := s.shopStore.GetByID(ctx, req.ShopID)
shopName := ""
if shop != nil {
shopName = shop.ShopName
}
return &dto.CustomerAccountItem{
ID: account.ID,
Username: account.Username,
Phone: account.Phone,
UserType: account.UserType,
UserTypeName: getUserTypeName(account.UserType),
ShopID: account.ShopID,
ShopName: shopName,
Status: account.Status,
StatusName: getStatusName(account.Status),
CreatedAt: account.CreatedAt.Format("2006-01-02 15:04:05"),
}, nil
}
func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateCustomerAccountRequest) (*dto.CustomerAccountItem, error) {
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
}
account, err := s.accountStore.GetByID(ctx, id)
if err != nil {
return nil, errors.New(errors.CodeAccountNotFound, "账号不存在")
}
if account.UserType != constants.UserTypeAgent && account.UserType != constants.UserTypeEnterprise {
return nil, errors.New(errors.CodeForbidden, "无权限操作此账号")
}
if req.Username != nil {
account.Username = *req.Username
}
if req.Phone != nil {
if *req.Phone != account.Phone {
existingAccount, _ := s.accountStore.GetByPhone(ctx, *req.Phone)
if existingAccount != nil && existingAccount.ID != id {
return nil, errors.New(errors.CodePhoneExists, "手机号已被使用")
}
account.Phone = *req.Phone
}
}
account.Updater = currentUserID
if err := s.db.WithContext(ctx).Save(account).Error; err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "更新账号失败")
}
shopName := ""
if account.ShopID != nil {
if shop, _ := s.shopStore.GetByID(ctx, *account.ShopID); shop != nil {
shopName = shop.ShopName
}
}
enterpriseName := ""
if account.EnterpriseID != nil {
if ent, _ := s.enterpriseStore.GetByID(ctx, *account.EnterpriseID); ent != nil {
enterpriseName = ent.EnterpriseName
}
}
return &dto.CustomerAccountItem{
ID: account.ID,
Username: account.Username,
Phone: account.Phone,
UserType: account.UserType,
UserTypeName: getUserTypeName(account.UserType),
ShopID: account.ShopID,
ShopName: shopName,
EnterpriseID: account.EnterpriseID,
EnterpriseName: enterpriseName,
Status: account.Status,
StatusName: getStatusName(account.Status),
CreatedAt: account.CreatedAt.Format("2006-01-02 15:04:05"),
}, nil
}
func (s *Service) UpdatePassword(ctx context.Context, id uint, password string) error {
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return errors.New(errors.CodeUnauthorized, "未授权访问")
}
account, err := s.accountStore.GetByID(ctx, id)
if err != nil {
return errors.New(errors.CodeAccountNotFound, "账号不存在")
}
if account.UserType != constants.UserTypeAgent && account.UserType != constants.UserTypeEnterprise {
return errors.New(errors.CodeForbidden, "无权限操作此账号")
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return errors.Wrap(errors.CodeInternalError, err, "密码加密失败")
}
return s.db.WithContext(ctx).Model(&model.Account{}).
Where("id = ?", id).
Updates(map[string]interface{}{
"password": string(hashedPassword),
"updater": currentUserID,
}).Error
}
func (s *Service) UpdateStatus(ctx context.Context, id uint, status int) error {
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return errors.New(errors.CodeUnauthorized, "未授权访问")
}
account, err := s.accountStore.GetByID(ctx, id)
if err != nil {
return errors.New(errors.CodeAccountNotFound, "账号不存在")
}
if account.UserType != constants.UserTypeAgent && account.UserType != constants.UserTypeEnterprise {
return errors.New(errors.CodeForbidden, "无权限操作此账号")
}
return s.db.WithContext(ctx).Model(&model.Account{}).
Where("id = ?", id).
Updates(map[string]interface{}{
"status": status,
"updater": currentUserID,
}).Error
}
func getUserTypeName(userType int) string {
switch userType {
case constants.UserTypeAgent:
return "代理账号"
case constants.UserTypeEnterprise:
return "企业账号"
default:
return "未知"
}
}
func getStatusName(status int) string {
if status == constants.StatusEnabled {
return "启用"
}
return "禁用"
}

View File

@@ -1,265 +0,0 @@
package shop_account
import (
"context"
"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"
)
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 *dto.ShopAccountListRequest) ([]*dto.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, errors.Wrap(errors.CodeInternalError, 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([]*dto.ShopAccountResponse, 0, len(accounts))
for _, account := range accounts {
resp := &dto.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 *dto.CreateShopAccountRequest) (*dto.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, errors.Wrap(errors.CodeInternalError, 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: 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, errors.Wrap(errors.CodeInternalError, err, "创建代理商账号失败")
}
return &dto.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 *dto.UpdateShopAccountRequest) (*dto.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, errors.Wrap(errors.CodeInternalError, 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, errors.Wrap(errors.CodeInternalError, err, "更新代理商账号失败")
}
var shopName string
if account.ShopID != nil {
shop, err := s.shopStore.GetByID(ctx, *account.ShopID)
if err == nil {
shopName = shop.ShopName
}
}
return &dto.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 *dto.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 errors.Wrap(errors.CodeInternalError, err, "获取账号失败")
}
if account.UserType != constants.UserTypeAgent {
return errors.New(errors.CodeInvalidParam, "只能更新代理商账号密码")
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
if err != nil {
return errors.Wrap(errors.CodeInternalError, err, "密码哈希失败")
}
if err := s.accountStore.UpdatePassword(ctx, id, string(hashedPassword), currentUserID); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "更新密码失败")
}
return nil
}
func (s *Service) UpdateStatus(ctx context.Context, id uint, req *dto.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 errors.Wrap(errors.CodeInternalError, err, "获取账号失败")
}
if account.UserType != constants.UserTypeAgent {
return errors.New(errors.CodeInvalidParam, "只能更新代理商账号状态")
}
if err := s.accountStore.UpdateStatus(ctx, id, req.Status, currentUserID); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "更新账号状态失败")
}
return nil
}