feat: 新增全局操作密码功能,将线下充值验证从登录密码改为统一操作密码
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
- 新增 RedisSystemOperationPasswordKey 常量函数(pkg/constants/redis.go) - 新增 operation_password Service(Set/IsSet/Verify,bcrypt 哈希存 Redis) - 新增 SuperAdminHandler 及两个接口: - POST /api/admin/super-admin/operation-password(设置/重置,仅超级管理员) - GET /api/admin/super-admin/operation-password/status(查询是否已设置) - AgentRecharge.OfflinePay 操作密码验证从"查当前用户登录密码"改为"全局操作密码 Verify" - Bootstrap/路由/文档生成器同步注册
This commit is contained in:
@@ -133,5 +133,6 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
|
||||
AgentRecharge: admin.NewAgentRechargeHandler(svc.AgentRecharge, validate),
|
||||
Refund: admin.NewRefundHandler(svc.Refund),
|
||||
ClientWechat: app.NewClientWechatHandler(svc.WechatConfig, deps.Redis, deps.Logger),
|
||||
SuperAdmin: admin.NewSuperAdminHandler(svc.OperationPassword),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import (
|
||||
shopSvc "github.com/break/junhong_cmp_fiber/internal/service/shop"
|
||||
|
||||
agentRechargeSvc "github.com/break/junhong_cmp_fiber/internal/service/agent_recharge"
|
||||
operationPasswordSvc "github.com/break/junhong_cmp_fiber/internal/service/operation_password"
|
||||
pollingSvc "github.com/break/junhong_cmp_fiber/internal/service/polling"
|
||||
refundSvc "github.com/break/junhong_cmp_fiber/internal/service/refund"
|
||||
shopCommissionSvc "github.com/break/junhong_cmp_fiber/internal/service/shop_commission"
|
||||
@@ -101,6 +102,7 @@ type services struct {
|
||||
PackageActivation *packageSvc.ActivationService
|
||||
Refund *refundSvc.Service
|
||||
TrafficQuery *trafficSvc.QueryService
|
||||
OperationPassword *operationPasswordSvc.Service
|
||||
}
|
||||
|
||||
func initServices(s *stores, deps *Dependencies) *services {
|
||||
@@ -140,6 +142,7 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
|
||||
stopResumeService := iotCardSvc.NewStopResumeService(deps.Redis, s.IotCard, s.PackageUsage, s.DeviceSimBinding, deps.GatewayClient, deps.Logger)
|
||||
device := deviceSvc.New(deps.DB, deps.Redis, s.Device, s.DeviceSimBinding, s.IotCard, s.Shop, s.AssetAllocationRecord, s.ShopPackageAllocation, s.ShopSeriesAllocation, s.PackageSeries, deps.GatewayClient, s.AssetIdentifier)
|
||||
operationPassword := operationPasswordSvc.New(deps.Redis)
|
||||
|
||||
return &services{
|
||||
Account: account,
|
||||
@@ -223,14 +226,15 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
s.AgentWallet,
|
||||
s.AgentWalletTransaction,
|
||||
s.Shop,
|
||||
s.Account,
|
||||
wechatConfig,
|
||||
accountAudit,
|
||||
operationPassword,
|
||||
deps.Redis,
|
||||
deps.Logger,
|
||||
),
|
||||
PackageActivation: packageActivation,
|
||||
TrafficQuery: trafficSvc.NewQueryService(deps.Redis, s.CardDailyUsage),
|
||||
OperationPassword: operationPassword,
|
||||
Refund: refundSvc.New(
|
||||
deps.DB,
|
||||
s.RefundRequest,
|
||||
|
||||
@@ -62,6 +62,7 @@ type Handlers struct {
|
||||
AgentRecharge *admin.AgentRechargeHandler
|
||||
Refund *admin.RefundHandler
|
||||
ClientWechat *app.ClientWechatHandler
|
||||
SuperAdmin *admin.SuperAdminHandler
|
||||
}
|
||||
|
||||
// Middlewares 封装所有中间件
|
||||
|
||||
71
internal/handler/admin/super_admin.go
Normal file
71
internal/handler/admin/super_admin.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/operation_password"
|
||||
"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/response"
|
||||
)
|
||||
|
||||
// SuperAdminHandler 超级管理员专属操作处理器
|
||||
type SuperAdminHandler struct {
|
||||
operationPasswordService *operation_password.Service
|
||||
}
|
||||
|
||||
// NewSuperAdminHandler 创建超级管理员处理器
|
||||
func NewSuperAdminHandler(operationPasswordService *operation_password.Service) *SuperAdminHandler {
|
||||
return &SuperAdminHandler{
|
||||
operationPasswordService: operationPasswordService,
|
||||
}
|
||||
}
|
||||
|
||||
// SetOperationPassword 设置/修改全局操作密码(仅超级管理员)
|
||||
// POST /api/admin/super-admin/operation-password
|
||||
func (h *SuperAdminHandler) SetOperationPassword(c *fiber.Ctx) error {
|
||||
userType := middleware.GetUserTypeFromContext(c.UserContext())
|
||||
if userType != constants.UserTypeSuperAdmin {
|
||||
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
|
||||
var req dto.SetOperationPasswordRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
if req.Password == "" || req.ConfirmPassword == "" {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
if req.Password != req.ConfirmPassword {
|
||||
return errors.New(errors.CodeInvalidParam, "两次输入的密码不一致")
|
||||
}
|
||||
|
||||
if len(req.Password) < 6 || len(req.Password) > 50 {
|
||||
return errors.New(errors.CodeInvalidParam, "密码长度必须在 6~50 位之间")
|
||||
}
|
||||
|
||||
ctx := c.UserContext()
|
||||
if err := h.operationPasswordService.Set(ctx, req.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, nil)
|
||||
}
|
||||
|
||||
// GetOperationPasswordStatus 查询操作密码是否已设置(仅超级管理员)
|
||||
// GET /api/admin/super-admin/operation-password/status
|
||||
func (h *SuperAdminHandler) GetOperationPasswordStatus(c *fiber.Ctx) error {
|
||||
userType := middleware.GetUserTypeFromContext(c.UserContext())
|
||||
if userType != constants.UserTypeSuperAdmin {
|
||||
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
|
||||
ctx := c.UserContext()
|
||||
isSet := h.operationPasswordService.IsSet(ctx)
|
||||
|
||||
return response.Success(c, &dto.OperationPasswordStatusResponse{IsSet: isSet})
|
||||
}
|
||||
12
internal/model/dto/super_admin_dto.go
Normal file
12
internal/model/dto/super_admin_dto.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package dto
|
||||
|
||||
// SetOperationPasswordRequest 设置全局操作密码请求
|
||||
type SetOperationPasswordRequest struct {
|
||||
Password string `json:"password" validate:"required,min=6,max=50" required:"true" minLength:"6" maxLength:"50" description:"新操作密码,长度 6~50 位"`
|
||||
ConfirmPassword string `json:"confirm_password" validate:"required" required:"true" description:"确认密码,必须与 password 一致"`
|
||||
}
|
||||
|
||||
// OperationPasswordStatusResponse 操作密码状态响应
|
||||
type OperationPasswordStatusResponse struct {
|
||||
IsSet bool `json:"is_set" description:"操作密码是否已设置"`
|
||||
}
|
||||
@@ -122,4 +122,7 @@ func RegisterAdminRoutes(router fiber.Router, handlers *bootstrap.Handlers, midd
|
||||
if handlers.Refund != nil {
|
||||
registerRefundRoutes(authGroup, handlers.Refund, doc, basePath)
|
||||
}
|
||||
if handlers.SuperAdmin != nil {
|
||||
registerSuperAdminRoutes(authGroup, handlers.SuperAdmin, doc, basePath)
|
||||
}
|
||||
}
|
||||
|
||||
28
internal/routes/super_admin.go
Normal file
28
internal/routes/super_admin.go
Normal file
@@ -0,0 +1,28 @@
|
||||
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 registerSuperAdminRoutes(router fiber.Router, handler *admin.SuperAdminHandler, doc *openapi.Generator, basePath string) {
|
||||
group := router.Group("/super-admin")
|
||||
groupPath := basePath + "/super-admin"
|
||||
|
||||
Register(group, doc, groupPath, "POST", "/operation-password", handler.SetOperationPassword, RouteSpec{
|
||||
Summary: "设置/修改全局操作密码",
|
||||
Tags: []string{"超级管理员"},
|
||||
Input: new(dto.SetOperationPasswordRequest),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(group, doc, groupPath, "GET", "/operation-password/status", handler.GetOperationPasswordStatus, RouteSpec{
|
||||
Summary: "查询操作密码是否已设置",
|
||||
Tags: []string{"超级管理员"},
|
||||
Output: new(dto.OperationPasswordStatusResponse),
|
||||
Auth: true,
|
||||
})
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
@@ -27,6 +26,11 @@ type AuditServiceInterface interface {
|
||||
LogOperation(ctx context.Context, log *model.AccountOperationLog)
|
||||
}
|
||||
|
||||
// OperationPasswordServiceInterface 全局操作密码服务接口
|
||||
type OperationPasswordServiceInterface interface {
|
||||
Verify(ctx context.Context, inputPassword string) error
|
||||
}
|
||||
|
||||
// WechatConfigServiceInterface 支付配置服务接口
|
||||
type WechatConfigServiceInterface interface {
|
||||
GetActiveConfig(ctx context.Context) (*model.WechatConfig, error)
|
||||
@@ -35,16 +39,16 @@ type WechatConfigServiceInterface interface {
|
||||
// Service 代理预充值业务服务
|
||||
// 负责代理钱包充值订单的创建、线下确认、回调处理等业务逻辑
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
agentRechargeStore *postgres.AgentRechargeStore
|
||||
agentWalletStore *postgres.AgentWalletStore
|
||||
agentWalletTxStore *postgres.AgentWalletTransactionStore
|
||||
shopStore *postgres.ShopStore
|
||||
accountStore *postgres.AccountStore
|
||||
wechatConfigService WechatConfigServiceInterface
|
||||
auditService AuditServiceInterface
|
||||
redis *redis.Client
|
||||
logger *zap.Logger
|
||||
db *gorm.DB
|
||||
agentRechargeStore *postgres.AgentRechargeStore
|
||||
agentWalletStore *postgres.AgentWalletStore
|
||||
agentWalletTxStore *postgres.AgentWalletTransactionStore
|
||||
shopStore *postgres.ShopStore
|
||||
wechatConfigService WechatConfigServiceInterface
|
||||
auditService AuditServiceInterface
|
||||
operationPasswordService OperationPasswordServiceInterface
|
||||
redis *redis.Client
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// New 创建代理预充值服务实例
|
||||
@@ -54,23 +58,23 @@ func New(
|
||||
agentWalletStore *postgres.AgentWalletStore,
|
||||
agentWalletTxStore *postgres.AgentWalletTransactionStore,
|
||||
shopStore *postgres.ShopStore,
|
||||
accountStore *postgres.AccountStore,
|
||||
wechatConfigService WechatConfigServiceInterface,
|
||||
auditService AuditServiceInterface,
|
||||
operationPasswordService OperationPasswordServiceInterface,
|
||||
rdb *redis.Client,
|
||||
logger *zap.Logger,
|
||||
) *Service {
|
||||
return &Service{
|
||||
db: db,
|
||||
agentRechargeStore: agentRechargeStore,
|
||||
agentWalletStore: agentWalletStore,
|
||||
agentWalletTxStore: agentWalletTxStore,
|
||||
shopStore: shopStore,
|
||||
accountStore: accountStore,
|
||||
wechatConfigService: wechatConfigService,
|
||||
auditService: auditService,
|
||||
redis: rdb,
|
||||
logger: logger,
|
||||
db: db,
|
||||
agentRechargeStore: agentRechargeStore,
|
||||
agentWalletStore: agentWalletStore,
|
||||
agentWalletTxStore: agentWalletTxStore,
|
||||
shopStore: shopStore,
|
||||
wechatConfigService: wechatConfigService,
|
||||
auditService: auditService,
|
||||
operationPasswordService: operationPasswordService,
|
||||
redis: rdb,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,13 +179,9 @@ func (s *Service) OfflinePay(ctx context.Context, id uint, req *dto.AgentOffline
|
||||
return nil, errors.New(errors.CodeForbidden, "仅平台管理员可确认线下充值")
|
||||
}
|
||||
|
||||
// 验证操作密码
|
||||
account, err := s.accountStore.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询操作人账号失败")
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(req.OperationPassword)); err != nil {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "操作密码错误")
|
||||
// 验证全局操作密码
|
||||
if err := s.operationPasswordService.Verify(ctx, req.OperationPassword); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
record, err := s.agentRechargeStore.GetByID(ctx, id)
|
||||
|
||||
68
internal/service/operation_password/service.go
Normal file
68
internal/service/operation_password/service.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// Package operation_password 提供全局操作密码的设置、查询与验证服务
|
||||
// 操作密码以 bcrypt 哈希存储于 Redis,仅超级管理员可维护
|
||||
package operation_password
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// Service 全局操作密码服务
|
||||
type Service struct {
|
||||
redis *redis.Client
|
||||
}
|
||||
|
||||
// New 创建操作密码服务实例
|
||||
func New(rdb *redis.Client) *Service {
|
||||
return &Service{redis: rdb}
|
||||
}
|
||||
|
||||
// Set 设置/修改全局操作密码
|
||||
// 对明文密码做 bcrypt 哈希后永久存储到 Redis(无 TTL)
|
||||
func (s *Service) Set(ctx context.Context, password string) error {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "操作密码加密失败")
|
||||
}
|
||||
|
||||
key := constants.RedisSystemOperationPasswordKey()
|
||||
if err := s.redis.Set(ctx, key, string(hash), 0).Err(); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "操作密码存储失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsSet 查询操作密码是否已设置
|
||||
func (s *Service) IsSet(ctx context.Context) bool {
|
||||
key := constants.RedisSystemOperationPasswordKey()
|
||||
n, err := s.redis.Exists(ctx, key).Result()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return n > 0
|
||||
}
|
||||
|
||||
// Verify 验证操作密码是否正确
|
||||
// key 不存在时返回"操作密码未设置"错误,密码错误时返回"操作密码错误"
|
||||
func (s *Service) Verify(ctx context.Context, inputPassword string) error {
|
||||
key := constants.RedisSystemOperationPasswordKey()
|
||||
hash, err := s.redis.Get(ctx, key).Result()
|
||||
if err == redis.Nil {
|
||||
return errors.New(errors.CodeInvalidParam, "操作密码未设置,请联系超级管理员")
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "查询操作密码失败")
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(inputPassword)); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "操作密码错误")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user