refactor(account): 统一账号管理API、完善权限检查和操作审计
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m17s
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:
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
167
internal/handler/auth/handler.go
Normal file
167
internal/handler/auth/handler.go
Normal 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)
|
||||
}
|
||||
63
internal/model/account_operation_log.go
Normal file
63
internal/model/account_operation_log.go
Normal 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)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
57
internal/routes/auth.go
Normal 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,
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
3640
internal/service/account/service_test.go
Normal file
3640
internal/service/account/service_test.go
Normal file
File diff suppressed because it is too large
Load Diff
42
internal/service/account_audit/service.go
Normal file
42
internal/service/account_audit/service.go
Normal 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))
|
||||
}
|
||||
}()
|
||||
}
|
||||
145
internal/service/account_audit/service_test.go
Normal file
145
internal/service/account_audit/service_test.go
Normal 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)
|
||||
}
|
||||
@@ -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 "禁用"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
25
internal/store/postgres/account_operation_log_store.go
Normal file
25
internal/store/postgres/account_operation_log_store.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user