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

@@ -3,6 +3,7 @@ package bootstrap
import (
"github.com/break/junhong_cmp_fiber/internal/handler/admin"
"github.com/break/junhong_cmp_fiber/internal/handler/app"
authHandler "github.com/break/junhong_cmp_fiber/internal/handler/auth"
"github.com/break/junhong_cmp_fiber/internal/handler/callback"
"github.com/break/junhong_cmp_fiber/internal/handler/h5"
"github.com/go-playground/validator/v10"
@@ -12,12 +13,12 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
validate := validator.New()
return &Handlers{
Auth: authHandler.NewHandler(svc.Auth, validate),
Account: admin.NewAccountHandler(svc.Account),
Role: admin.NewRoleHandler(svc.Role, validate),
Permission: admin.NewPermissionHandler(svc.Permission),
PersonalCustomer: app.NewPersonalCustomerHandler(svc.PersonalCustomer, deps.Logger),
Shop: admin.NewShopHandler(svc.Shop),
ShopAccount: admin.NewShopAccountHandler(svc.ShopAccount),
AdminAuth: admin.NewAuthHandler(svc.Auth, validate),
H5Auth: h5.NewAuthHandler(svc.Auth, validate),
ShopCommission: admin.NewShopCommissionHandler(svc.ShopCommission),
@@ -28,7 +29,6 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
EnterpriseDevice: admin.NewEnterpriseDeviceHandler(svc.EnterpriseDevice),
EnterpriseDeviceH5: h5.NewEnterpriseDeviceHandler(svc.EnterpriseDevice),
Authorization: admin.NewAuthorizationHandler(svc.Authorization),
CustomerAccount: admin.NewCustomerAccountHandler(svc.CustomerAccount),
MyCommission: admin.NewMyCommissionHandler(svc.MyCommission),
IotCard: admin.NewIotCardHandler(svc.IotCard),
IotCardImport: admin.NewIotCardImportHandler(svc.IotCardImport),

View File

@@ -2,6 +2,7 @@ package bootstrap
import (
accountSvc "github.com/break/junhong_cmp_fiber/internal/service/account"
accountAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/account_audit"
assetAllocationRecordSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_allocation_record"
authSvc "github.com/break/junhong_cmp_fiber/internal/service/auth"
carrierSvc "github.com/break/junhong_cmp_fiber/internal/service/carrier"
@@ -9,7 +10,7 @@ import (
commissionStatsSvc "github.com/break/junhong_cmp_fiber/internal/service/commission_stats"
commissionWithdrawalSvc "github.com/break/junhong_cmp_fiber/internal/service/commission_withdrawal"
commissionWithdrawalSettingSvc "github.com/break/junhong_cmp_fiber/internal/service/commission_withdrawal_setting"
customerAccountSvc "github.com/break/junhong_cmp_fiber/internal/service/customer_account"
deviceSvc "github.com/break/junhong_cmp_fiber/internal/service/device"
deviceImportSvc "github.com/break/junhong_cmp_fiber/internal/service/device_import"
enterpriseSvc "github.com/break/junhong_cmp_fiber/internal/service/enterprise"
@@ -27,7 +28,7 @@ import (
rechargeSvc "github.com/break/junhong_cmp_fiber/internal/service/recharge"
roleSvc "github.com/break/junhong_cmp_fiber/internal/service/role"
shopSvc "github.com/break/junhong_cmp_fiber/internal/service/shop"
shopAccountSvc "github.com/break/junhong_cmp_fiber/internal/service/shop_account"
shopCommissionSvc "github.com/break/junhong_cmp_fiber/internal/service/shop_commission"
shopPackageAllocationSvc "github.com/break/junhong_cmp_fiber/internal/service/shop_package_allocation"
shopPackageBatchAllocationSvc "github.com/break/junhong_cmp_fiber/internal/service/shop_package_batch_allocation"
@@ -37,11 +38,11 @@ import (
type services struct {
Account *accountSvc.Service
AccountAudit *accountAuditSvc.Service
Role *roleSvc.Service
Permission *permissionSvc.Service
PersonalCustomer *personalCustomerSvc.Service
Shop *shopSvc.Service
ShopAccount *shopAccountSvc.Service
Auth *authSvc.Service
ShopCommission *shopCommissionSvc.Service
CommissionWithdrawal *commissionWithdrawalSvc.Service
@@ -51,7 +52,6 @@ type services struct {
EnterpriseCard *enterpriseCardSvc.Service
EnterpriseDevice *enterpriseDeviceSvc.Service
Authorization *enterpriseCardSvc.AuthorizationService
CustomerAccount *customerAccountSvc.Service
MyCommission *myCommissionSvc.Service
IotCard *iotCardSvc.Service
IotCardImport *iotCardImportSvc.Service
@@ -73,14 +73,15 @@ type services struct {
func initServices(s *stores, deps *Dependencies) *services {
purchaseValidation := purchaseValidationSvc.New(deps.DB, s.IotCard, s.Device, s.Package, s.ShopSeriesAllocation)
accountAudit := accountAuditSvc.NewService(s.AccountOperationLog)
return &services{
Account: accountSvc.New(s.Account, s.Role, s.AccountRole),
Account: accountSvc.New(s.Account, s.Role, s.AccountRole, s.Shop, s.Enterprise, accountAudit),
AccountAudit: accountAudit,
Role: roleSvc.New(s.Role, s.Permission, s.RolePermission),
Permission: permissionSvc.New(s.Permission, s.AccountRole, s.RolePermission, deps.Redis),
PersonalCustomer: personalCustomerSvc.NewService(s.PersonalCustomer, s.PersonalCustomerPhone, deps.VerificationService, deps.JWTManager, deps.WechatOfficialAccount, deps.Logger),
Shop: shopSvc.New(s.Shop, s.Account),
ShopAccount: shopAccountSvc.New(s.Account, s.Shop),
Auth: authSvc.New(s.Account, s.AccountRole, s.RolePermission, s.Permission, deps.TokenManager, deps.Logger),
ShopCommission: shopCommissionSvc.New(s.Shop, s.Account, s.Wallet, s.CommissionWithdrawalRequest, s.CommissionRecord),
CommissionWithdrawal: commissionWithdrawalSvc.New(deps.DB, s.Shop, s.Account, s.Wallet, s.WalletTransaction, s.CommissionWithdrawalRequest),
@@ -105,7 +106,6 @@ func initServices(s *stores, deps *Dependencies) *services {
EnterpriseCard: enterpriseCardSvc.New(deps.DB, s.Enterprise, s.EnterpriseCardAuthorization),
EnterpriseDevice: enterpriseDeviceSvc.New(deps.DB, s.Enterprise, s.Device, s.DeviceSimBinding, s.EnterpriseDeviceAuthorization, s.EnterpriseCardAuthorization, deps.Logger),
Authorization: enterpriseCardSvc.NewAuthorizationService(s.Enterprise, s.IotCard, s.EnterpriseCardAuthorization, deps.Logger),
CustomerAccount: customerAccountSvc.New(deps.DB, s.Account, s.Shop, s.Enterprise),
MyCommission: myCommissionSvc.New(deps.DB, s.Shop, s.Wallet, s.CommissionWithdrawalRequest, s.CommissionWithdrawalSetting, s.CommissionRecord, s.WalletTransaction),
IotCard: iotCardSvc.New(deps.DB, s.IotCard, s.Shop, s.AssetAllocationRecord, s.ShopSeriesAllocation, s.PackageSeries, deps.GatewayClient, deps.Logger),
IotCardImport: iotCardImportSvc.New(deps.DB, s.IotCardImportTask, deps.QueueClient),

View File

@@ -6,6 +6,7 @@ import (
type stores struct {
Account *postgres.AccountStore
AccountOperationLog *postgres.AccountOperationLogStore
Shop *postgres.ShopStore
Role *postgres.RoleStore
Permission *postgres.PermissionStore
@@ -44,6 +45,7 @@ type stores struct {
func initStores(deps *Dependencies) *stores {
return &stores{
Account: postgres.NewAccountStore(deps.DB, deps.Redis),
AccountOperationLog: postgres.NewAccountOperationLogStore(deps.DB),
Shop: postgres.NewShopStore(deps.DB, deps.Redis),
Role: postgres.NewRoleStore(deps.DB),
Permission: postgres.NewPermissionStore(deps.DB),

View File

@@ -3,6 +3,7 @@ package bootstrap
import (
"github.com/break/junhong_cmp_fiber/internal/handler/admin"
"github.com/break/junhong_cmp_fiber/internal/handler/app"
authHandler "github.com/break/junhong_cmp_fiber/internal/handler/auth"
"github.com/break/junhong_cmp_fiber/internal/handler/callback"
"github.com/break/junhong_cmp_fiber/internal/handler/h5"
"github.com/break/junhong_cmp_fiber/internal/middleware"
@@ -10,12 +11,12 @@ import (
)
type Handlers struct {
Auth *authHandler.Handler
Account *admin.AccountHandler
Role *admin.RoleHandler
Permission *admin.PermissionHandler
PersonalCustomer *app.PersonalCustomerHandler
Shop *admin.ShopHandler
ShopAccount *admin.ShopAccountHandler
AdminAuth *admin.AuthHandler
H5Auth *h5.AuthHandler
ShopCommission *admin.ShopCommissionHandler
@@ -26,7 +27,6 @@ type Handlers struct {
EnterpriseDevice *admin.EnterpriseDeviceHandler
EnterpriseDeviceH5 *h5.EnterpriseDeviceHandler
Authorization *admin.AuthorizationHandler
CustomerAccount *admin.CustomerAccountHandler
MyCommission *admin.MyCommissionHandler
IotCard *admin.IotCardHandler
IotCardImport *admin.IotCardImportHandler

View File

@@ -148,7 +148,7 @@ func (h *AccountHandler) GetRoles(c *fiber.Ctx) error {
// RemoveRole 移除账号的角色
// DELETE /api/admin/accounts/:account_id/roles/:role_id
func (h *AccountHandler) RemoveRole(c *fiber.Ctx) error {
accountID, err := strconv.ParseUint(c.Params("account_id"), 10, 64)
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil {
return errors.New(errors.CodeInvalidParam, "无效的账号 ID")
}
@@ -158,7 +158,7 @@ func (h *AccountHandler) RemoveRole(c *fiber.Ctx) error {
return errors.New(errors.CodeInvalidParam, "无效的角色 ID")
}
if err := h.service.RemoveRole(c.UserContext(), uint(accountID), uint(roleID)); err != nil {
if err := h.service.RemoveRole(c.UserContext(), uint(id), uint(roleID)); err != nil {
return err
}
@@ -166,7 +166,7 @@ func (h *AccountHandler) RemoveRole(c *fiber.Ctx) error {
}
// UpdatePassword 修改账号密码
// PUT /api/admin/platform-accounts/:id/password
// PUT /api/admin/accounts/:id/password
func (h *AccountHandler) UpdatePassword(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil {
@@ -186,7 +186,7 @@ func (h *AccountHandler) UpdatePassword(c *fiber.Ctx) error {
}
// UpdateStatus 修改账号状态
// PUT /api/admin/platform-accounts/:id/status
// PUT /api/admin/accounts/:id/status
func (h *AccountHandler) UpdateStatus(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil {
@@ -205,8 +205,9 @@ func (h *AccountHandler) UpdateStatus(c *fiber.Ctx) error {
return response.Success(c, nil)
}
// ListPlatformAccounts 查询平台账号列表
// GET /api/admin/platform-accounts
// ListPlatformAccounts 查询平台账号列表(兼容旧路由)
// 自动筛选 user_type IN (1, 2) 的账号
// GET /api/admin/accounts - 查询平台账号列表
func (h *AccountHandler) ListPlatformAccounts(c *fiber.Ctx) error {
var req dto.PlatformAccountListRequest
if err := c.QueryParser(&req); err != nil {

View File

@@ -1,106 +0,0 @@
package admin
import (
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
customerAccountService "github.com/break/junhong_cmp_fiber/internal/service/customer_account"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/response"
)
type CustomerAccountHandler struct {
service *customerAccountService.Service
}
func NewCustomerAccountHandler(service *customerAccountService.Service) *CustomerAccountHandler {
return &CustomerAccountHandler{service: service}
}
func (h *CustomerAccountHandler) List(c *fiber.Ctx) error {
var req dto.CustomerAccountListReq
if err := c.QueryParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
result, err := h.service.List(c.UserContext(), &req)
if err != nil {
return err
}
return response.SuccessWithPagination(c, result.Items, result.Total, result.Page, result.Size)
}
func (h *CustomerAccountHandler) Create(c *fiber.Ctx) error {
var req dto.CreateCustomerAccountReq
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
result, err := h.service.Create(c.UserContext(), &req)
if err != nil {
return err
}
return response.Success(c, result)
}
func (h *CustomerAccountHandler) Update(c *fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return errors.New(errors.CodeInvalidParam, "无效的账号ID")
}
var req dto.UpdateCustomerAccountRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
result, err := h.service.Update(c.UserContext(), uint(id), &req)
if err != nil {
return err
}
return response.Success(c, result)
}
func (h *CustomerAccountHandler) UpdatePassword(c *fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return errors.New(errors.CodeInvalidParam, "无效的账号ID")
}
var req dto.UpdateCustomerAccountPasswordRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
if err := h.service.UpdatePassword(c.UserContext(), uint(id), req.Password); err != nil {
return err
}
return response.Success(c, nil)
}
func (h *CustomerAccountHandler) UpdateStatus(c *fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return errors.New(errors.CodeInvalidParam, "无效的账号ID")
}
var req dto.UpdateCustomerAccountStatusRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
if err := h.service.UpdateStatus(c.UserContext(), uint(id), req.Status); err != nil {
return err
}
return response.Success(c, nil)
}

View File

@@ -1,103 +0,0 @@
package admin
import (
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
shopAccountService "github.com/break/junhong_cmp_fiber/internal/service/shop_account"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/response"
)
type ShopAccountHandler struct {
service *shopAccountService.Service
}
func NewShopAccountHandler(service *shopAccountService.Service) *ShopAccountHandler {
return &ShopAccountHandler{service: service}
}
func (h *ShopAccountHandler) List(c *fiber.Ctx) error {
var req dto.ShopAccountListRequest
if err := c.QueryParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
accounts, total, err := h.service.List(c.UserContext(), &req)
if err != nil {
return err
}
return response.SuccessWithPagination(c, accounts, total, req.Page, req.PageSize)
}
func (h *ShopAccountHandler) Create(c *fiber.Ctx) error {
var req dto.CreateShopAccountRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
account, err := h.service.Create(c.UserContext(), &req)
if err != nil {
return err
}
return response.Success(c, account)
}
func (h *ShopAccountHandler) Update(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil {
return errors.New(errors.CodeInvalidParam, "无效的账号 ID")
}
var req dto.UpdateShopAccountRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
account, err := h.service.Update(c.UserContext(), uint(id), &req)
if err != nil {
return err
}
return response.Success(c, account)
}
func (h *ShopAccountHandler) UpdatePassword(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil {
return errors.New(errors.CodeInvalidParam, "无效的账号 ID")
}
var req dto.UpdateShopAccountPasswordRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
if err := h.service.UpdatePassword(c.UserContext(), uint(id), &req); err != nil {
return err
}
return response.Success(c, nil)
}
func (h *ShopAccountHandler) UpdateStatus(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil {
return errors.New(errors.CodeInvalidParam, "无效的账号 ID")
}
var req dto.UpdateShopAccountStatusRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
if err := h.service.UpdateStatus(c.UserContext(), uint(id), &req); err != nil {
return err
}
return response.Success(c, nil)
}

View File

@@ -0,0 +1,167 @@
package auth
import (
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/internal/service/auth"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/logger"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
"github.com/break/junhong_cmp_fiber/pkg/response"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
)
// Handler 统一认证处理器
// 合并后台和 H5 认证接口
type Handler struct {
authService *auth.Service
validator *validator.Validate
}
// NewHandler 创建统一认证处理器
func NewHandler(authService *auth.Service, validator *validator.Validate) *Handler {
return &Handler{
authService: authService,
validator: validator,
}
}
// Login 统一登录(后台+H5
// POST /api/auth/login - 统一登录(后台+H5
func (h *Handler) Login(c *fiber.Ctx) error {
var req dto.LoginRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
if err := h.validator.Struct(&req); err != nil {
logger.GetAppLogger().Warn("参数验证失败",
zap.String("path", c.Path()),
zap.String("method", c.Method()),
zap.Error(err),
)
return errors.New(errors.CodeInvalidParam)
}
clientIP := c.IP()
ctx := c.UserContext()
resp, err := h.authService.Login(ctx, &req, clientIP)
if err != nil {
return err
}
return response.Success(c, resp)
}
// Logout 统一登出
// POST /api/auth/logout - 统一登出
func (h *Handler) Logout(c *fiber.Ctx) error {
authorization := c.Get("Authorization")
accessToken := ""
if len(authorization) > 7 && authorization[:7] == "Bearer " {
accessToken = authorization[7:]
}
// 尝试从请求体获取 refresh_token可选
refreshToken := ""
var req dto.RefreshTokenRequest
if err := c.BodyParser(&req); err == nil {
refreshToken = req.RefreshToken
}
ctx := c.UserContext()
if err := h.authService.Logout(ctx, accessToken, refreshToken); err != nil {
return err
}
return response.Success(c, nil)
}
// RefreshToken 刷新 Token
// POST /api/auth/refresh-token - 刷新 Token
func (h *Handler) RefreshToken(c *fiber.Ctx) error {
var req dto.RefreshTokenRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
if err := h.validator.Struct(&req); err != nil {
logger.GetAppLogger().Warn("参数验证失败",
zap.String("path", c.Path()),
zap.String("method", c.Method()),
zap.Error(err),
)
return errors.New(errors.CodeInvalidParam)
}
ctx := c.UserContext()
newAccessToken, err := h.authService.RefreshToken(ctx, req.RefreshToken)
if err != nil {
return err
}
resp := &dto.RefreshTokenResponse{
AccessToken: newAccessToken,
ExpiresIn: 86400,
}
return response.Success(c, resp)
}
// GetMe 获取用户信息
// GET /api/auth/me - 获取用户信息
func (h *Handler) GetMe(c *fiber.Ctx) error {
userID := middleware.GetUserIDFromContext(c.UserContext())
if userID == 0 {
return errors.New(errors.CodeUnauthorized, "未授权访问")
}
ctx := c.UserContext()
userInfo, permissions, err := h.authService.GetCurrentUser(ctx, userID)
if err != nil {
return err
}
data := map[string]interface{}{
"user": userInfo,
"permissions": permissions,
}
return response.Success(c, data)
}
// ChangePassword 修改密码
// PUT /api/auth/password - 修改密码
func (h *Handler) ChangePassword(c *fiber.Ctx) error {
userID := middleware.GetUserIDFromContext(c.UserContext())
if userID == 0 {
return errors.New(errors.CodeUnauthorized, "未授权访问")
}
var req dto.ChangePasswordRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
if err := h.validator.Struct(&req); err != nil {
logger.GetAppLogger().Warn("参数验证失败",
zap.String("path", c.Path()),
zap.String("method", c.Method()),
zap.Error(err),
)
return errors.New(errors.CodeInvalidParam)
}
ctx := c.UserContext()
if err := h.authService.ChangePassword(ctx, userID, req.OldPassword, req.NewPassword); err != nil {
return err
}
return response.Success(c, nil)
}

View File

@@ -0,0 +1,63 @@
package model
import (
"database/sql/driver"
"encoding/json"
"time"
)
// AccountOperationLog 账号操作审计日志模型
// 记录所有账号管理操作,包括创建、更新、删除、角色分配等
// 用于审计追踪和合规要求
type AccountOperationLog struct {
ID uint `gorm:"column:id;primaryKey;comment:主键ID" json:"id"`
CreatedAt time.Time `gorm:"column:created_at;not null;comment:创建时间" json:"created_at"`
OperatorID uint `gorm:"column:operator_id;not null;index:idx_account_log_operator,priority:1;comment:操作人ID" json:"operator_id"`
OperatorType int `gorm:"column:operator_type;type:int;not null;comment:操作人类型 1=超级管理员 2=平台用户 3=代理账号 4=企业账号" json:"operator_type"`
OperatorName string `gorm:"column:operator_name;type:varchar(255);not null;comment:操作人用户名" json:"operator_name"`
TargetAccountID *uint `gorm:"column:target_account_id;type:bigint;index:idx_account_log_target,priority:1;comment:目标账号ID删除操作后可能查不到" json:"target_account_id,omitempty"`
TargetUsername *string `gorm:"column:target_username;type:varchar(255);comment:目标账号用户名" json:"target_username,omitempty"`
TargetUserType *int `gorm:"column:target_user_type;type:int;comment:目标账号类型" json:"target_user_type,omitempty"`
OperationType string `gorm:"column:operation_type;type:varchar(50);not null;comment:操作类型 create/update/delete/assign_roles/remove_role" json:"operation_type"`
OperationDesc string `gorm:"column:operation_desc;type:text;not null;comment:操作描述(中文)" json:"operation_desc"`
BeforeData JSONB `gorm:"column:before_data;type:jsonb;comment:变更前数据JSONB格式用于update操作" json:"before_data,omitempty"`
AfterData JSONB `gorm:"column:after_data;type:jsonb;comment:变更后数据JSONB格式用于create/update操作" json:"after_data,omitempty"`
RequestID *string `gorm:"column:request_id;type:varchar(255);comment:请求ID可关联访问日志" json:"request_id,omitempty"`
IPAddress *string `gorm:"column:ip_address;type:varchar(50);comment:操作来源IP地址" json:"ip_address,omitempty"`
UserAgent *string `gorm:"column:user_agent;type:text;comment:用户代理(浏览器信息)" json:"user_agent,omitempty"`
}
// TableName 指定表名
func (AccountOperationLog) TableName() string {
return "tb_account_operation_log"
}
// JSONB 自定义JSONB类型用于存储变更数据
type JSONB map[string]interface{}
// Value 实现 driver.Valuer 接口
func (j JSONB) Value() (driver.Value, error) {
if j == nil {
return nil, nil
}
return json.Marshal(j)
}
// Scan 实现 sql.Scanner 接口
func (j *JSONB) Scan(value interface{}) error {
if value == nil {
*j = nil
return nil
}
bytes, ok := value.([]byte)
if !ok {
return json.Unmarshal([]byte(value.(string)), j)
}
return json.Unmarshal(bytes, j)
}

View File

@@ -4,163 +4,114 @@ import (
"github.com/gofiber/fiber/v2"
"github.com/break/junhong_cmp_fiber/internal/handler/admin"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
"github.com/break/junhong_cmp_fiber/pkg/openapi"
)
// registerAccountRoutes 注册账号相关路由
// 统一路由结构:/api/admin/accounts/*
// 账号类型通过请求体的 user_type 字段区分2=平台用户3=代理账号4=企业账号)
func registerAccountRoutes(api fiber.Router, h *admin.AccountHandler, doc *openapi.Generator, basePath string) {
accounts := api.Group("/accounts")
groupPath := basePath + "/accounts"
accountsPath := basePath + "/accounts"
// 账号 CRUD
Register(accounts, doc, groupPath, "POST", "", h.Create, RouteSpec{
// 企业用户拦截中间件:禁止企业用户访问账号管理接口
accounts.Use(func(c *fiber.Ctx) error {
userType := middleware.GetUserTypeFromContext(c.UserContext())
if userType == constants.UserTypeEnterprise {
return errors.New(errors.CodeForbidden, "无权限访问账号管理功能")
}
return c.Next()
})
// 创建账号user_type: 2=平台, 3=代理, 4=企业)
Register(accounts, doc, accountsPath, "POST", "", h.Create, RouteSpec{
Summary: "创建账号",
Tags: []string{"账号相关"},
Tags: []string{"账号管理"},
Input: new(dto.CreateAccountRequest),
Output: new(dto.AccountResponse),
Auth: true,
})
Register(accounts, doc, groupPath, "GET", "", h.List, RouteSpec{
Summary: "账号列表",
Tags: []string{"账号相关"},
// 查询账号列表(可通过 user_type 参数筛选)
Register(accounts, doc, accountsPath, "GET", "", h.List, RouteSpec{
Summary: "查询账号列表",
Tags: []string{"账号管理"},
Input: new(dto.AccountListRequest),
Output: new(dto.AccountPageResult),
Auth: true,
})
Register(accounts, doc, groupPath, "GET", "/:id", h.Get, RouteSpec{
// 获取账号详情
Register(accounts, doc, accountsPath, "GET", "/:id", h.Get, RouteSpec{
Summary: "获取账号详情",
Tags: []string{"账号相关"},
Tags: []string{"账号管理"},
Input: new(dto.IDReq),
Output: new(dto.AccountResponse),
Auth: true,
})
Register(accounts, doc, groupPath, "PUT", "/:id", h.Update, RouteSpec{
// 更新账号
Register(accounts, doc, accountsPath, "PUT", "/:id", h.Update, RouteSpec{
Summary: "更新账号",
Tags: []string{"账号相关"},
Tags: []string{"账号管理"},
Input: new(dto.UpdateAccountParams),
Output: new(dto.AccountResponse),
Auth: true,
})
Register(accounts, doc, groupPath, "DELETE", "/:id", h.Delete, RouteSpec{
// 删除账号
Register(accounts, doc, accountsPath, "DELETE", "/:id", h.Delete, RouteSpec{
Summary: "删除账号",
Tags: []string{"账号相关"},
Tags: []string{"账号管理"},
Input: new(dto.IDReq),
Output: nil,
Auth: true,
})
// 账号-角色关联
Register(accounts, doc, groupPath, "POST", "/:id/roles", h.AssignRoles, RouteSpec{
Summary: "分配角色",
Tags: []string{"账号相关"},
Input: new(dto.AssignRolesParams),
Output: nil, // TODO: Define AccountRole response DTO
})
Register(accounts, doc, groupPath, "GET", "/:id/roles", h.GetRoles, RouteSpec{
Summary: "获取账号角色",
Tags: []string{"账号相关"},
Input: new(dto.IDReq),
Output: new([]model.Role),
Auth: true,
})
Register(accounts, doc, groupPath, "DELETE", "/:account_id/roles/:role_id", h.RemoveRole, RouteSpec{
Summary: "移除角色",
Tags: []string{"账号相关"},
Input: new(dto.RemoveRoleParams),
Output: nil,
Auth: true,
})
registerPlatformAccountRoutes(api, h, doc, basePath)
}
func registerPlatformAccountRoutes(api fiber.Router, h *admin.AccountHandler, doc *openapi.Generator, basePath string) {
platformAccounts := api.Group("/platform-accounts")
groupPath := basePath + "/platform-accounts"
Register(platformAccounts, doc, groupPath, "GET", "", h.ListPlatformAccounts, RouteSpec{
Summary: "平台账号列表",
Tags: []string{"平台账号"},
Input: new(dto.PlatformAccountListRequest),
Output: new(dto.AccountPageResult),
Auth: true,
})
Register(platformAccounts, doc, groupPath, "POST", "", h.Create, RouteSpec{
Summary: "新增平台账号",
Tags: []string{"平台账号"},
Input: new(dto.CreateAccountRequest),
Output: new(dto.AccountResponse),
Auth: true,
})
Register(platformAccounts, doc, groupPath, "GET", "/:id", h.Get, RouteSpec{
Summary: "获取平台账号详情",
Tags: []string{"平台账号"},
Input: new(dto.IDReq),
Output: new(dto.AccountResponse),
Auth: true,
})
Register(platformAccounts, doc, groupPath, "PUT", "/:id", h.Update, RouteSpec{
Summary: "编辑平台账号",
Tags: []string{"平台账号"},
Input: new(dto.UpdateAccountParams),
Output: new(dto.AccountResponse),
Auth: true,
})
Register(platformAccounts, doc, groupPath, "DELETE", "/:id", h.Delete, RouteSpec{
Summary: "删除平台账号",
Tags: []string{"平台账号"},
Input: new(dto.IDReq),
Output: nil,
Auth: true,
})
Register(platformAccounts, doc, groupPath, "PUT", "/:id/password", h.UpdatePassword, RouteSpec{
Summary: "修改密码",
Tags: []string{"平台账号"},
// 修改账号密码
Register(accounts, doc, accountsPath, "PUT", "/:id/password", h.UpdatePassword, RouteSpec{
Summary: "修改账号密码",
Tags: []string{"账号管理"},
Input: new(dto.UpdatePasswordParams),
Output: nil,
Auth: true,
})
Register(platformAccounts, doc, groupPath, "PUT", "/:id/status", h.UpdateStatus, RouteSpec{
Summary: "启用/禁用账号",
Tags: []string{"平台账号"},
// 修改账号状态
Register(accounts, doc, accountsPath, "PUT", "/:id/status", h.UpdateStatus, RouteSpec{
Summary: "修改账号状态",
Tags: []string{"账号管理"},
Input: new(dto.UpdateStatusParams),
Output: nil,
Auth: true,
})
Register(platformAccounts, doc, groupPath, "POST", "/:id/roles", h.AssignRoles, RouteSpec{
Summary: "分配角色",
Tags: []string{"平台账号"},
// 为账号分配角色
Register(accounts, doc, accountsPath, "POST", "/:id/roles", h.AssignRoles, RouteSpec{
Summary: "为账号分配角色",
Tags: []string{"账号管理"},
Input: new(dto.AssignRolesParams),
Output: nil,
Output: new([]dto.AccountRoleResponse),
Auth: true,
})
Register(platformAccounts, doc, groupPath, "GET", "/:id/roles", h.GetRoles, RouteSpec{
// 获取账号角色
Register(accounts, doc, accountsPath, "GET", "/:id/roles", h.GetRoles, RouteSpec{
Summary: "获取账号角色",
Tags: []string{"平台账号"},
Tags: []string{"账号管理"},
Input: new(dto.IDReq),
Output: new([]model.Role),
Output: new(dto.AccountRolesResponse),
Auth: true,
})
Register(platformAccounts, doc, groupPath, "DELETE", "/:account_id/roles/:role_id", h.RemoveRole, RouteSpec{
Summary: "移除角色",
Tags: []string{"平台账号"},
// 移除账号角色
Register(accounts, doc, accountsPath, "DELETE", "/:account_id/roles/:role_id", h.RemoveRole, RouteSpec{
Summary: "移除账号角色",
Tags: []string{"账号管理"},
Input: new(dto.RemoveRoleParams),
Output: nil,
Auth: true,

View File

@@ -4,16 +4,12 @@ import (
"github.com/gofiber/fiber/v2"
"github.com/break/junhong_cmp_fiber/internal/bootstrap"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/pkg/openapi"
)
// RegisterAdminRoutes 注册管理后台相关路由
func RegisterAdminRoutes(router fiber.Router, handlers *bootstrap.Handlers, middlewares *bootstrap.Middlewares, doc *openapi.Generator, basePath string) {
if handlers.AdminAuth != nil {
registerAdminAuthRoutes(router, handlers.AdminAuth, middlewares.AdminAuth, doc, basePath)
}
// 认证路由已迁移到 /api/auth参见 RegisterAuthRoutes
authGroup := router.Group("", middlewares.AdminAuth)
if handlers.Account != nil {
@@ -28,9 +24,7 @@ func RegisterAdminRoutes(router fiber.Router, handlers *bootstrap.Handlers, midd
if handlers.Shop != nil {
registerShopRoutes(authGroup, handlers.Shop, doc, basePath)
}
if handlers.ShopAccount != nil {
registerShopAccountRoutes(authGroup, handlers.ShopAccount, doc, basePath)
}
if handlers.ShopCommission != nil {
registerShopCommissionRoutes(authGroup, handlers.ShopCommission, doc, basePath)
}
@@ -52,9 +46,7 @@ func RegisterAdminRoutes(router fiber.Router, handlers *bootstrap.Handlers, midd
if handlers.Authorization != nil {
registerAuthorizationRoutes(authGroup, handlers.Authorization, doc, basePath)
}
if handlers.CustomerAccount != nil {
registerCustomerAccountRoutes(authGroup, handlers.CustomerAccount, doc, basePath)
}
if handlers.MyCommission != nil {
registerMyCommissionRoutes(authGroup, handlers.MyCommission, doc, basePath)
}
@@ -95,55 +87,3 @@ func RegisterAdminRoutes(router fiber.Router, handlers *bootstrap.Handlers, midd
registerAdminOrderRoutes(authGroup, handlers.AdminOrder, doc, basePath)
}
}
func registerAdminAuthRoutes(router fiber.Router, handler interface{}, authMiddleware fiber.Handler, doc *openapi.Generator, basePath string) {
h := handler.(interface {
Login(c *fiber.Ctx) error
Logout(c *fiber.Ctx) error
RefreshToken(c *fiber.Ctx) error
GetMe(c *fiber.Ctx) error
ChangePassword(c *fiber.Ctx) error
})
Register(router, doc, basePath, "POST", "/login", h.Login, RouteSpec{
Summary: "后台登录",
Tags: []string{"认证"},
Input: new(dto.LoginRequest),
Output: new(dto.LoginResponse),
Auth: false,
})
Register(router, doc, basePath, "POST", "/refresh-token", h.RefreshToken, RouteSpec{
Summary: "刷新 Token",
Tags: []string{"认证"},
Input: new(dto.RefreshTokenRequest),
Output: new(dto.RefreshTokenResponse),
Auth: false,
})
authGroup := router.Group("", authMiddleware)
Register(authGroup, doc, basePath, "POST", "/logout", h.Logout, RouteSpec{
Summary: "登出",
Tags: []string{"认证"},
Input: nil,
Output: nil,
Auth: true,
})
Register(authGroup, doc, basePath, "GET", "/me", h.GetMe, RouteSpec{
Summary: "获取当前用户信息",
Tags: []string{"认证"},
Input: nil,
Output: new(dto.UserInfo),
Auth: true,
})
Register(authGroup, doc, basePath, "PUT", "/password", h.ChangePassword, RouteSpec{
Summary: "修改密码",
Tags: []string{"认证"},
Input: new(dto.ChangePasswordRequest),
Output: nil,
Auth: true,
})
}

57
internal/routes/auth.go Normal file
View File

@@ -0,0 +1,57 @@
package routes
import (
"github.com/gofiber/fiber/v2"
authHandler "github.com/break/junhong_cmp_fiber/internal/handler/auth"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/pkg/openapi"
)
// RegisterAuthRoutes 注册统一认证路由
// 路由挂载在 /api/auth 下
func RegisterAuthRoutes(router fiber.Router, handler *authHandler.Handler, authMiddleware fiber.Handler, doc *openapi.Generator, basePath string) {
// 公开路由(不需要认证)
Register(router, doc, basePath, "POST", "/login", handler.Login, RouteSpec{
Summary: "统一登录(后台+H5",
Tags: []string{"统一认证"},
Input: new(dto.LoginRequest),
Output: new(dto.LoginResponse),
Auth: false,
})
Register(router, doc, basePath, "POST", "/refresh-token", handler.RefreshToken, RouteSpec{
Summary: "刷新 Token",
Tags: []string{"统一认证"},
Input: new(dto.RefreshTokenRequest),
Output: new(dto.RefreshTokenResponse),
Auth: false,
})
// 需要认证的路由
authGroup := router.Group("", authMiddleware)
Register(authGroup, doc, basePath, "POST", "/logout", handler.Logout, RouteSpec{
Summary: "统一登出",
Tags: []string{"统一认证"},
Input: nil,
Output: nil,
Auth: true,
})
Register(authGroup, doc, basePath, "GET", "/me", handler.GetMe, RouteSpec{
Summary: "获取用户信息",
Tags: []string{"统一认证"},
Input: nil,
Output: new(dto.UserInfo),
Auth: true,
})
Register(authGroup, doc, basePath, "PUT", "/password", handler.ChangePassword, RouteSpec{
Summary: "修改密码",
Tags: []string{"统一认证"},
Input: new(dto.ChangePasswordRequest),
Output: nil,
Auth: true,
})
}

View File

@@ -1,54 +0,0 @@
package routes
import (
"github.com/gofiber/fiber/v2"
"github.com/break/junhong_cmp_fiber/internal/handler/admin"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/pkg/openapi"
)
func registerCustomerAccountRoutes(router fiber.Router, handler *admin.CustomerAccountHandler, doc *openapi.Generator, basePath string) {
accounts := router.Group("/customer-accounts")
groupPath := basePath + "/customer-accounts"
Register(accounts, doc, groupPath, "GET", "", handler.List, RouteSpec{
Summary: "客户账号列表",
Tags: []string{"客户账号管理"},
Input: new(dto.CustomerAccountListReq),
Output: new(dto.CustomerAccountPageResult),
Auth: true,
})
Register(accounts, doc, groupPath, "POST", "", handler.Create, RouteSpec{
Summary: "新增代理商账号",
Tags: []string{"客户账号管理"},
Input: new(dto.CreateCustomerAccountReq),
Output: new(dto.CustomerAccountItem),
Auth: true,
})
Register(accounts, doc, groupPath, "PUT", "/:id", handler.Update, RouteSpec{
Summary: "编辑账号",
Tags: []string{"客户账号管理"},
Input: new(dto.UpdateCustomerAccountReq),
Output: new(dto.CustomerAccountItem),
Auth: true,
})
Register(accounts, doc, groupPath, "PUT", "/:id/password", handler.UpdatePassword, RouteSpec{
Summary: "修改账号密码",
Tags: []string{"客户账号管理"},
Input: new(dto.UpdateCustomerAccountPasswordReq),
Output: nil,
Auth: true,
})
Register(accounts, doc, groupPath, "PUT", "/:id/status", handler.UpdateStatus, RouteSpec{
Summary: "修改账号状态",
Tags: []string{"客户账号管理"},
Input: new(dto.UpdateCustomerAccountStatusReq),
Output: nil,
Auth: true,
})
}

View File

@@ -4,17 +4,12 @@ import (
"github.com/gofiber/fiber/v2"
"github.com/break/junhong_cmp_fiber/internal/bootstrap"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/pkg/openapi"
)
// RegisterH5Routes 注册H5相关路由
func RegisterH5Routes(router fiber.Router, handlers *bootstrap.Handlers, middlewares *bootstrap.Middlewares, doc *openapi.Generator, basePath string) {
if handlers.H5Auth != nil {
registerH5AuthRoutes(router, handlers.H5Auth, middlewares.H5Auth, doc, basePath)
}
// 需要认证的路由组
// 认证路由已迁移到 /api/auth参见 RegisterAuthRoutes
authGroup := router.Group("", middlewares.H5Auth)
if handlers.H5Order != nil {
@@ -27,55 +22,3 @@ func RegisterH5Routes(router fiber.Router, handlers *bootstrap.Handlers, middlew
registerH5EnterpriseDeviceRoutes(authGroup, handlers.EnterpriseDeviceH5, doc, basePath)
}
}
func registerH5AuthRoutes(router fiber.Router, handler interface{}, authMiddleware fiber.Handler, doc *openapi.Generator, basePath string) {
h := handler.(interface {
Login(c *fiber.Ctx) error
Logout(c *fiber.Ctx) error
RefreshToken(c *fiber.Ctx) error
GetMe(c *fiber.Ctx) error
ChangePassword(c *fiber.Ctx) error
})
Register(router, doc, basePath, "POST", "/login", h.Login, RouteSpec{
Summary: "H5 登录",
Tags: []string{"H5 认证"},
Input: new(dto.LoginRequest),
Output: new(dto.LoginResponse),
Auth: false,
})
Register(router, doc, basePath, "POST", "/refresh-token", h.RefreshToken, RouteSpec{
Summary: "刷新 Token",
Tags: []string{"H5 认证"},
Input: new(dto.RefreshTokenRequest),
Output: new(dto.RefreshTokenResponse),
Auth: false,
})
authGroup := router.Group("", authMiddleware)
Register(authGroup, doc, basePath, "POST", "/logout", h.Logout, RouteSpec{
Summary: "登出",
Tags: []string{"H5 认证"},
Input: nil,
Output: nil,
Auth: true,
})
Register(authGroup, doc, basePath, "GET", "/me", h.GetMe, RouteSpec{
Summary: "获取当前用户信息",
Tags: []string{"H5 认证"},
Input: nil,
Output: new(dto.UserInfo),
Auth: true,
})
Register(authGroup, doc, basePath, "PUT", "/password", h.ChangePassword, RouteSpec{
Summary: "修改密码",
Tags: []string{"H5 认证"},
Input: new(dto.ChangePasswordRequest),
Output: nil,
Auth: true,
})
}

View File

@@ -18,19 +18,25 @@ func RegisterRoutesWithDoc(app *fiber.App, handlers *bootstrap.Handlers, middlew
// 1. 全局路由
registerHealthRoutes(app, doc)
// 2. Admin 域 (挂载在 /api/admin)
// 2. 统一认证路由 (挂载在 /api/auth)
if handlers.Auth != nil {
authGroup := app.Group("/api/auth")
RegisterAuthRoutes(authGroup, handlers.Auth, middlewares.AdminAuth, doc, "/api/auth")
}
// 3. Admin 域 (挂载在 /api/admin)
adminGroup := app.Group("/api/admin")
RegisterAdminRoutes(adminGroup, handlers, middlewares, doc, "/api/admin")
// 3. H5 域 (挂载在 /api/h5)
// 4. H5 域 (挂载在 /api/h5)
h5Group := app.Group("/api/h5")
RegisterH5Routes(h5Group, handlers, middlewares, doc, "/api/h5")
// 4. 个人客户路由 (挂载在 /api/c/v1)
// 5. 个人客户路由 (挂载在 /api/c/v1)
personalGroup := app.Group("/api/c/v1")
RegisterPersonalCustomerRoutes(personalGroup, doc, "/api/c/v1", handlers, middlewares.PersonalAuth)
// 5. 支付回调路由 (挂载在 /api/callback无需认证)
// 6. 支付回调路由 (挂载在 /api/callback无需认证)
if handlers.PaymentCallback != nil {
callbackGroup := app.Group("/api/callback")
registerPaymentCallbackRoutes(callbackGroup, handlers.PaymentCallback, doc, "/api/callback")

View File

@@ -45,51 +45,6 @@ func registerShopRoutes(router fiber.Router, handler *admin.ShopHandler, doc *op
})
}
func registerShopAccountRoutes(router fiber.Router, handler *admin.ShopAccountHandler, doc *openapi.Generator, basePath string) {
shopAccounts := router.Group("/shop-accounts")
groupPath := basePath + "/shop-accounts"
Register(shopAccounts, doc, groupPath, "GET", "", handler.List, RouteSpec{
Summary: "代理账号列表",
Tags: []string{"代理账号管理"},
Input: new(dto.ShopAccountListRequest),
Output: new(dto.ShopAccountPageResult),
Auth: true,
})
Register(shopAccounts, doc, groupPath, "POST", "", handler.Create, RouteSpec{
Summary: "创建代理账号",
Tags: []string{"代理账号管理"},
Input: new(dto.CreateShopAccountRequest),
Output: new(dto.ShopAccountResponse),
Auth: true,
})
Register(shopAccounts, doc, groupPath, "PUT", "/:id", handler.Update, RouteSpec{
Summary: "更新代理账号",
Tags: []string{"代理账号管理"},
Input: new(dto.UpdateShopAccountParams),
Output: new(dto.ShopAccountResponse),
Auth: true,
})
Register(shopAccounts, doc, groupPath, "PUT", "/:id/password", handler.UpdatePassword, RouteSpec{
Summary: "重置代理账号密码",
Tags: []string{"代理账号管理"},
Input: new(dto.UpdateShopAccountPasswordParams),
Output: nil,
Auth: true,
})
Register(shopAccounts, doc, groupPath, "PUT", "/:id/status", handler.UpdateStatus, RouteSpec{
Summary: "启用/禁用代理账号",
Tags: []string{"代理账号管理"},
Input: new(dto.UpdateShopAccountStatusParams),
Output: nil,
Auth: true,
})
}
func registerShopCommissionRoutes(router fiber.Router, handler *admin.ShopCommissionHandler, doc *openapi.Generator, basePath string) {
shops := router.Group("/shops")
groupPath := basePath + "/shops"

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
}

View File

@@ -0,0 +1,25 @@
package postgres
import (
"context"
"github.com/break/junhong_cmp_fiber/internal/model"
"gorm.io/gorm"
)
// AccountOperationLogStore 账号操作日志存储层
type AccountOperationLogStore struct {
db *gorm.DB
}
// NewAccountOperationLogStore 创建账号操作日志存储实例
func NewAccountOperationLogStore(db *gorm.DB) *AccountOperationLogStore {
return &AccountOperationLogStore{
db: db,
}
}
// Create 创建账号操作日志记录
func (s *AccountOperationLogStore) Create(ctx context.Context, log *model.AccountOperationLog) error {
return s.db.WithContext(ctx).Create(log).Error
}