feat: 新增代理预充值模块(DTO、Service、Handler、路由)
- agent_recharge_dto.go: 创建/列表/详情请求响应 DTO - service.go: 权限验证(代理只能充自己店铺)、金额范围校验、查询 active 配置、创建订单、线下充值确认(乐观锁+审计日志)、回调幂等处理 - agent_recharge.go Handler: Create/List/Get/OfflinePay 共 4 个方法 - agent_recharge.go 路由: 注册到 /api/admin/agent-recharges/*,路由层拦截企业账号 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
91
internal/handler/admin/agent_recharge.go
Normal file
91
internal/handler/admin/agent_recharge.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
agentRechargeSvc "github.com/break/junhong_cmp_fiber/internal/service/agent_recharge"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
)
|
||||
|
||||
// AgentRechargeHandler 代理预充值 Handler
|
||||
type AgentRechargeHandler struct {
|
||||
service *agentRechargeSvc.Service
|
||||
}
|
||||
|
||||
// NewAgentRechargeHandler 创建代理预充值 Handler
|
||||
func NewAgentRechargeHandler(service *agentRechargeSvc.Service) *AgentRechargeHandler {
|
||||
return &AgentRechargeHandler{service: service}
|
||||
}
|
||||
|
||||
// Create 创建代理充值订单
|
||||
// POST /api/admin/agent-recharges
|
||||
func (h *AgentRechargeHandler) Create(c *fiber.Ctx) error {
|
||||
var req dto.CreateAgentRechargeRequest
|
||||
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)
|
||||
}
|
||||
|
||||
// List 查询代理充值订单列表
|
||||
// GET /api/admin/agent-recharges
|
||||
func (h *AgentRechargeHandler) List(c *fiber.Ctx) error {
|
||||
var req dto.AgentRechargeListRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
list, total, err := h.service.List(c.UserContext(), &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.SuccessWithPagination(c, list, total, req.Page, req.PageSize)
|
||||
}
|
||||
|
||||
// Get 查询代理充值订单详情
|
||||
// GET /api/admin/agent-recharges/:id
|
||||
func (h *AgentRechargeHandler) Get(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的充值记录ID")
|
||||
}
|
||||
|
||||
result, err := h.service.GetByID(c.UserContext(), uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, result)
|
||||
}
|
||||
|
||||
// OfflinePay 确认线下充值
|
||||
// POST /api/admin/agent-recharges/:id/offline-pay
|
||||
func (h *AgentRechargeHandler) OfflinePay(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.AgentOfflinePayRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
result, err := h.service.OfflinePay(c.UserContext(), uint(id), &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, result)
|
||||
}
|
||||
50
internal/model/dto/agent_recharge_dto.go
Normal file
50
internal/model/dto/agent_recharge_dto.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package dto
|
||||
|
||||
// CreateAgentRechargeRequest 创建代理充值请求
|
||||
type CreateAgentRechargeRequest struct {
|
||||
ShopID uint `json:"shop_id" validate:"required" required:"true" description:"目标店铺ID,代理只能填自己店铺"`
|
||||
Amount int64 `json:"amount" validate:"required,min=10000,max=100000000" required:"true" minimum:"10000" maximum:"100000000" description:"充值金额(分),范围100元~100万元"`
|
||||
PaymentMethod string `json:"payment_method" validate:"required,oneof=wechat offline" required:"true" description:"支付方式 (wechat:微信在线支付, offline:线下转账仅平台可用)"`
|
||||
}
|
||||
|
||||
// AgentOfflinePayRequest 代理线下充值确认请求
|
||||
type AgentOfflinePayRequest struct {
|
||||
OperationPassword string `json:"operation_password" validate:"required" required:"true" description:"操作密码"`
|
||||
}
|
||||
|
||||
// AgentRechargeResponse 代理充值记录响应
|
||||
type AgentRechargeResponse struct {
|
||||
ID uint `json:"id" description:"充值记录ID"`
|
||||
RechargeNo string `json:"recharge_no" description:"充值单号(ARCH前缀)"`
|
||||
ShopID uint `json:"shop_id" description:"店铺ID"`
|
||||
ShopName string `json:"shop_name" description:"店铺名称"`
|
||||
AgentWalletID uint `json:"agent_wallet_id" description:"代理钱包ID"`
|
||||
Amount int64 `json:"amount" description:"充值金额(分)"`
|
||||
PaymentMethod string `json:"payment_method" description:"支付方式 (wechat:微信在线支付, offline:线下转账)"`
|
||||
PaymentChannel string `json:"payment_channel" description:"实际支付通道 (wechat_direct:微信直连, fuyou:富友, offline:线下转账)"`
|
||||
PaymentConfigID *uint `json:"payment_config_id" description:"关联支付配置ID,线下充值为null"`
|
||||
PaymentTransactionID string `json:"payment_transaction_id" description:"第三方支付流水号"`
|
||||
Status int `json:"status" description:"状态 (1:待支付, 2:已完成, 3:已取消)"`
|
||||
PaidAt *string `json:"paid_at" description:"支付时间"`
|
||||
CompletedAt *string `json:"completed_at" description:"完成时间"`
|
||||
CreatedAt string `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt string `json:"updated_at" description:"更新时间"`
|
||||
}
|
||||
|
||||
// AgentRechargeListRequest 代理充值记录列表请求
|
||||
type AgentRechargeListRequest struct {
|
||||
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码,默认1"`
|
||||
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页条数,默认20,最大100"`
|
||||
ShopID *uint `json:"shop_id" query:"shop_id" description:"按店铺ID过滤"`
|
||||
Status *int `json:"status" query:"status" description:"按状态过滤 (1:待支付, 2:已完成, 3:已取消)"`
|
||||
StartDate string `json:"start_date" query:"start_date" description:"创建时间起始日期(YYYY-MM-DD)"`
|
||||
EndDate string `json:"end_date" query:"end_date" description:"创建时间截止日期(YYYY-MM-DD)"`
|
||||
}
|
||||
|
||||
// AgentRechargeListResponse 代理充值记录列表响应
|
||||
type AgentRechargeListResponse struct {
|
||||
Total int64 `json:"total" description:"总记录数"`
|
||||
Page int `json:"page" description:"当前页码"`
|
||||
PageSize int `json:"page_size" description:"每页条数"`
|
||||
List []*AgentRechargeResponse `json:"list" description:"充值记录列表"`
|
||||
}
|
||||
55
internal/routes/agent_recharge.go
Normal file
55
internal/routes/agent_recharge.go
Normal file
@@ -0,0 +1,55 @@
|
||||
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/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"
|
||||
)
|
||||
|
||||
func registerAgentRechargeRoutes(router fiber.Router, handler *admin.AgentRechargeHandler, doc *openapi.Generator, basePath string) {
|
||||
group := router.Group("/agent-recharges", func(c *fiber.Ctx) error {
|
||||
userType := middleware.GetUserTypeFromContext(c.UserContext())
|
||||
if userType == constants.UserTypeEnterprise {
|
||||
return errors.New(errors.CodeForbidden, "企业账号无权访问代理充值功能")
|
||||
}
|
||||
return c.Next()
|
||||
})
|
||||
groupPath := basePath + "/agent-recharges"
|
||||
|
||||
Register(group, doc, groupPath, "POST", "", handler.Create, RouteSpec{
|
||||
Summary: "创建代理充值订单",
|
||||
Tags: []string{"代理预充值"},
|
||||
Input: new(dto.CreateAgentRechargeRequest),
|
||||
Output: new(dto.AgentRechargeResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(group, doc, groupPath, "GET", "", handler.List, RouteSpec{
|
||||
Summary: "查询代理充值订单列表",
|
||||
Tags: []string{"代理预充值"},
|
||||
Input: new(dto.AgentRechargeListRequest),
|
||||
Output: new(dto.AgentRechargeListResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(group, doc, groupPath, "GET", "/:id", handler.Get, RouteSpec{
|
||||
Summary: "查询代理充值订单详情",
|
||||
Tags: []string{"代理预充值"},
|
||||
Input: new(dto.IDReq),
|
||||
Output: new(dto.AgentRechargeResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(group, doc, groupPath, "POST", "/:id/offline-pay", handler.OfflinePay, RouteSpec{
|
||||
Summary: "确认线下充值",
|
||||
Tags: []string{"代理预充值"},
|
||||
Input: new(dto.AgentOfflinePayRequest),
|
||||
Output: new(dto.AgentRechargeResponse),
|
||||
Auth: true,
|
||||
})
|
||||
}
|
||||
506
internal/service/agent_recharge/service.go
Normal file
506
internal/service/agent_recharge/service.go
Normal file
@@ -0,0 +1,506 @@
|
||||
// Package agent_recharge 提供代理预充值的业务逻辑服务
|
||||
// 包含充值订单创建、线下确认、支付回调处理、列表查询等功能
|
||||
package agent_recharge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"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"
|
||||
)
|
||||
|
||||
// AuditServiceInterface 审计日志服务接口
|
||||
type AuditServiceInterface interface {
|
||||
LogOperation(ctx context.Context, log *model.AccountOperationLog)
|
||||
}
|
||||
|
||||
// WechatConfigServiceInterface 支付配置服务接口
|
||||
type WechatConfigServiceInterface interface {
|
||||
GetActiveConfig(ctx context.Context) (*model.WechatConfig, error)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// New 创建代理预充值服务实例
|
||||
func New(
|
||||
db *gorm.DB,
|
||||
agentRechargeStore *postgres.AgentRechargeStore,
|
||||
agentWalletStore *postgres.AgentWalletStore,
|
||||
agentWalletTxStore *postgres.AgentWalletTransactionStore,
|
||||
shopStore *postgres.ShopStore,
|
||||
accountStore *postgres.AccountStore,
|
||||
wechatConfigService WechatConfigServiceInterface,
|
||||
auditService AuditServiceInterface,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建代理充值订单
|
||||
// POST /api/admin/agent-recharges
|
||||
func (s *Service) Create(ctx context.Context, req *dto.CreateAgentRechargeRequest) (*dto.AgentRechargeResponse, error) {
|
||||
userID := middleware.GetUserIDFromContext(ctx)
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
userShopID := middleware.GetShopIDFromContext(ctx)
|
||||
|
||||
// 代理只能充自己店铺
|
||||
if userType == constants.UserTypeAgent && req.ShopID != userShopID {
|
||||
return nil, errors.New(errors.CodeForbidden, "代理只能为自己的店铺充值")
|
||||
}
|
||||
|
||||
// 线下充值仅平台可用
|
||||
if req.PaymentMethod == "offline" && userType != constants.UserTypePlatform && userType != constants.UserTypeSuperAdmin {
|
||||
return nil, errors.New(errors.CodeForbidden, "线下充值仅平台管理员可操作")
|
||||
}
|
||||
|
||||
if req.Amount < constants.AgentRechargeMinAmount || req.Amount > constants.AgentRechargeMaxAmount {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "充值金额超出允许范围")
|
||||
}
|
||||
|
||||
// 查找目标店铺的主钱包
|
||||
wallet, err := s.agentWalletStore.GetMainWallet(ctx, req.ShopID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "目标店铺主钱包不存在")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询店铺主钱包失败")
|
||||
}
|
||||
|
||||
// 查询店铺名称
|
||||
shop, err := s.shopStore.GetByID(ctx, req.ShopID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "目标店铺不存在")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询店铺失败")
|
||||
}
|
||||
|
||||
// 在线支付需要查询生效的支付配置
|
||||
var paymentConfigID *uint
|
||||
var paymentChannel string
|
||||
if req.PaymentMethod == "wechat" {
|
||||
activeConfig, cfgErr := s.wechatConfigService.GetActiveConfig(ctx)
|
||||
if cfgErr != nil || activeConfig == nil {
|
||||
return nil, errors.New(errors.CodeNoPaymentConfig, "当前无可用的支付配置,请联系管理员")
|
||||
}
|
||||
paymentConfigID = &activeConfig.ID
|
||||
paymentChannel = activeConfig.ProviderType
|
||||
} else {
|
||||
paymentChannel = "offline"
|
||||
}
|
||||
|
||||
rechargeNo := s.generateRechargeNo()
|
||||
|
||||
record := &model.AgentRechargeRecord{
|
||||
UserID: userID,
|
||||
AgentWalletID: wallet.ID,
|
||||
ShopID: req.ShopID,
|
||||
RechargeNo: rechargeNo,
|
||||
Amount: req.Amount,
|
||||
PaymentMethod: req.PaymentMethod,
|
||||
PaymentChannel: &paymentChannel,
|
||||
PaymentConfigID: paymentConfigID,
|
||||
Status: constants.RechargeStatusPending,
|
||||
ShopIDTag: wallet.ShopIDTag,
|
||||
EnterpriseIDTag: wallet.EnterpriseIDTag,
|
||||
}
|
||||
|
||||
if err := s.agentRechargeStore.Create(ctx, record); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败")
|
||||
}
|
||||
|
||||
s.logger.Info("创建代理充值订单成功",
|
||||
zap.Uint("recharge_id", record.ID),
|
||||
zap.String("recharge_no", rechargeNo),
|
||||
zap.Int64("amount", req.Amount),
|
||||
zap.Uint("shop_id", req.ShopID),
|
||||
zap.Uint("user_id", userID),
|
||||
)
|
||||
|
||||
return toResponse(record, shop.ShopName), nil
|
||||
}
|
||||
|
||||
// OfflinePay 线下充值确认
|
||||
// POST /api/admin/agent-recharges/:id/offline-pay
|
||||
func (s *Service) OfflinePay(ctx context.Context, id uint, req *dto.AgentOfflinePayRequest) (*dto.AgentRechargeResponse, error) {
|
||||
userID := middleware.GetUserIDFromContext(ctx)
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
|
||||
// 仅平台账号可操作
|
||||
if userType != constants.UserTypePlatform && userType != constants.UserTypeSuperAdmin {
|
||||
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, "操作密码错误")
|
||||
}
|
||||
|
||||
record, err := s.agentRechargeStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "充值记录不存在")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询充值记录失败")
|
||||
}
|
||||
|
||||
if record.PaymentMethod != "offline" {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "该订单非线下充值,不支持此操作")
|
||||
}
|
||||
if record.Status != constants.RechargeStatusPending {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "该订单状态不允许确认支付")
|
||||
}
|
||||
|
||||
// 查询钱包(事务内需要用到 version)
|
||||
wallet, err := s.agentWalletStore.GetByID(ctx, record.AgentWalletID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询代理钱包失败")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
err = s.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 条件更新充值记录状态
|
||||
result := tx.Model(&model.AgentRechargeRecord{}).
|
||||
Where("id = ? AND status = ?", record.ID, constants.RechargeStatusPending).
|
||||
Updates(map[string]interface{}{
|
||||
"status": constants.RechargeStatusCompleted,
|
||||
"paid_at": now,
|
||||
"completed_at": now,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新充值记录状态失败")
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeInvalidParam, "充值记录状态已变更")
|
||||
}
|
||||
|
||||
// 增加钱包余额(乐观锁)
|
||||
balanceResult := tx.Model(&model.AgentWallet{}).
|
||||
Where("id = ? AND version = ?", wallet.ID, wallet.Version).
|
||||
Updates(map[string]interface{}{
|
||||
"balance": gorm.Expr("balance + ?", record.Amount),
|
||||
"version": gorm.Expr("version + 1"),
|
||||
})
|
||||
if balanceResult.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, balanceResult.Error, "更新钱包余额失败")
|
||||
}
|
||||
if balanceResult.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeInternalError, "钱包余额更新冲突,请重试")
|
||||
}
|
||||
|
||||
// 创建钱包交易记录
|
||||
remark := "线下充值确认"
|
||||
refType := "topup"
|
||||
txRecord := &model.AgentWalletTransaction{
|
||||
AgentWalletID: wallet.ID,
|
||||
ShopID: record.ShopID,
|
||||
UserID: userID,
|
||||
TransactionType: constants.AgentTransactionTypeRecharge,
|
||||
Amount: record.Amount,
|
||||
BalanceBefore: wallet.Balance,
|
||||
BalanceAfter: wallet.Balance + record.Amount,
|
||||
Status: constants.TransactionStatusSuccess,
|
||||
ReferenceType: &refType,
|
||||
ReferenceID: &record.ID,
|
||||
Remark: &remark,
|
||||
Creator: userID,
|
||||
ShopIDTag: wallet.ShopIDTag,
|
||||
EnterpriseIDTag: wallet.EnterpriseIDTag,
|
||||
}
|
||||
if err := s.agentWalletTxStore.CreateWithTx(ctx, tx, txRecord); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建钱包交易记录失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 异步记录审计日志
|
||||
go s.auditService.LogOperation(ctx, &model.AccountOperationLog{
|
||||
OperatorID: userID,
|
||||
OperatorType: userType,
|
||||
OperationType: "offline_recharge_confirm",
|
||||
OperationDesc: fmt.Sprintf("确认线下充值,充值单号: %s,金额: %d分", record.RechargeNo, record.Amount),
|
||||
RequestID: middleware.GetRequestIDFromContext(ctx),
|
||||
IPAddress: middleware.GetIPFromContext(ctx),
|
||||
UserAgent: middleware.GetUserAgentFromContext(ctx),
|
||||
})
|
||||
|
||||
shop, _ := s.shopStore.GetByID(ctx, record.ShopID)
|
||||
shopName := ""
|
||||
if shop != nil {
|
||||
shopName = shop.ShopName
|
||||
}
|
||||
|
||||
// 更新本地对象以反映最新状态
|
||||
record.Status = constants.RechargeStatusCompleted
|
||||
record.PaidAt = &now
|
||||
record.CompletedAt = &now
|
||||
|
||||
return toResponse(record, shopName), nil
|
||||
}
|
||||
|
||||
// HandlePaymentCallback 处理支付回调
|
||||
// 幂等处理:status != 待支付则直接返回成功
|
||||
func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string, paymentMethod string, paymentTransactionID string) error {
|
||||
record, err := s.agentRechargeStore.GetByRechargeNo(ctx, rechargeNo)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeNotFound, "充值订单不存在")
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询充值订单失败")
|
||||
}
|
||||
|
||||
// 幂等检查
|
||||
if record.Status != constants.RechargeStatusPending {
|
||||
s.logger.Info("代理充值订单已处理,跳过",
|
||||
zap.String("recharge_no", rechargeNo),
|
||||
zap.Int("status", record.Status),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
wallet, err := s.agentWalletStore.GetByID(ctx, record.AgentWalletID)
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询代理钱包失败")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
err = s.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 条件更新(WHERE status = 1)
|
||||
result := tx.Model(&model.AgentRechargeRecord{}).
|
||||
Where("id = ? AND status = ?", record.ID, constants.RechargeStatusPending).
|
||||
Updates(map[string]interface{}{
|
||||
"status": constants.RechargeStatusCompleted,
|
||||
"payment_transaction_id": paymentTransactionID,
|
||||
"paid_at": now,
|
||||
"completed_at": now,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新充值记录状态失败")
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 增加钱包余额(乐观锁)
|
||||
balanceResult := tx.Model(&model.AgentWallet{}).
|
||||
Where("id = ? AND version = ?", wallet.ID, wallet.Version).
|
||||
Updates(map[string]interface{}{
|
||||
"balance": gorm.Expr("balance + ?", record.Amount),
|
||||
"version": gorm.Expr("version + 1"),
|
||||
})
|
||||
if balanceResult.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, balanceResult.Error, "更新钱包余额失败")
|
||||
}
|
||||
if balanceResult.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeInternalError, "钱包余额更新冲突,请重试")
|
||||
}
|
||||
|
||||
// 创建交易记录
|
||||
remark := "在线支付充值"
|
||||
refType := "topup"
|
||||
txRecord := &model.AgentWalletTransaction{
|
||||
AgentWalletID: wallet.ID,
|
||||
ShopID: record.ShopID,
|
||||
UserID: record.UserID,
|
||||
TransactionType: constants.AgentTransactionTypeRecharge,
|
||||
Amount: record.Amount,
|
||||
BalanceBefore: wallet.Balance,
|
||||
BalanceAfter: wallet.Balance + record.Amount,
|
||||
Status: constants.TransactionStatusSuccess,
|
||||
ReferenceType: &refType,
|
||||
ReferenceID: &record.ID,
|
||||
Remark: &remark,
|
||||
Creator: record.UserID,
|
||||
ShopIDTag: wallet.ShopIDTag,
|
||||
EnterpriseIDTag: wallet.EnterpriseIDTag,
|
||||
}
|
||||
if err := s.agentWalletTxStore.CreateWithTx(ctx, tx, txRecord); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建钱包交易记录失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("代理充值支付回调处理成功",
|
||||
zap.String("recharge_no", rechargeNo),
|
||||
zap.Int64("amount", record.Amount),
|
||||
zap.Uint("shop_id", record.ShopID),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 根据ID查询充值订单详情
|
||||
// GET /api/admin/agent-recharges/:id
|
||||
func (s *Service) GetByID(ctx context.Context, id uint) (*dto.AgentRechargeResponse, error) {
|
||||
record, err := s.agentRechargeStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "充值记录不存在")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询充值记录失败")
|
||||
}
|
||||
|
||||
shop, _ := s.shopStore.GetByID(ctx, record.ShopID)
|
||||
shopName := ""
|
||||
if shop != nil {
|
||||
shopName = shop.ShopName
|
||||
}
|
||||
|
||||
return toResponse(record, shopName), nil
|
||||
}
|
||||
|
||||
// List 分页查询充值订单列表
|
||||
// GET /api/admin/agent-recharges
|
||||
func (s *Service) List(ctx context.Context, req *dto.AgentRechargeListRequest) ([]*dto.AgentRechargeResponse, int64, 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.AgentRechargeRecord{})
|
||||
|
||||
if req.ShopID != nil {
|
||||
query = query.Where("shop_id = ?", *req.ShopID)
|
||||
}
|
||||
if req.Status != nil {
|
||||
query = query.Where("status = ?", *req.Status)
|
||||
}
|
||||
if req.StartDate != "" {
|
||||
query = query.Where("created_at >= ?", req.StartDate+" 00:00:00")
|
||||
}
|
||||
if req.EndDate != "" {
|
||||
query = query.Where("created_at <= ?", req.EndDate+" 23:59:59")
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, errors.Wrap(errors.CodeDatabaseError, err, "查询充值记录总数失败")
|
||||
}
|
||||
|
||||
var records []*model.AgentRechargeRecord
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&records).Error; err != nil {
|
||||
return nil, 0, errors.Wrap(errors.CodeDatabaseError, err, "查询充值记录列表失败")
|
||||
}
|
||||
|
||||
// 批量查询店铺名称
|
||||
shopIDs := make([]uint, 0, len(records))
|
||||
for _, r := range records {
|
||||
shopIDs = append(shopIDs, r.ShopID)
|
||||
}
|
||||
shopMap := make(map[uint]string)
|
||||
if len(shopIDs) > 0 {
|
||||
shops, err := s.shopStore.GetByIDs(ctx, shopIDs)
|
||||
if err == nil {
|
||||
for _, sh := range shops {
|
||||
shopMap[sh.ID] = sh.ShopName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list := make([]*dto.AgentRechargeResponse, 0, len(records))
|
||||
for _, r := range records {
|
||||
list = append(list, toResponse(r, shopMap[r.ShopID]))
|
||||
}
|
||||
|
||||
return list, total, nil
|
||||
}
|
||||
|
||||
// generateRechargeNo 生成代理充值订单号
|
||||
// 格式: ARCH + 14位时间戳 + 6位随机数
|
||||
func (s *Service) generateRechargeNo() string {
|
||||
timestamp := time.Now().Format("20060102150405")
|
||||
randomNum := rand.Intn(1000000)
|
||||
return fmt.Sprintf("%s%s%06d", constants.AgentRechargeOrderPrefix, timestamp, randomNum)
|
||||
}
|
||||
|
||||
// toResponse 将模型转换为响应 DTO
|
||||
func toResponse(record *model.AgentRechargeRecord, shopName string) *dto.AgentRechargeResponse {
|
||||
resp := &dto.AgentRechargeResponse{
|
||||
ID: record.ID,
|
||||
RechargeNo: record.RechargeNo,
|
||||
ShopID: record.ShopID,
|
||||
ShopName: shopName,
|
||||
AgentWalletID: record.AgentWalletID,
|
||||
Amount: record.Amount,
|
||||
PaymentMethod: record.PaymentMethod,
|
||||
Status: record.Status,
|
||||
CreatedAt: record.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: record.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
|
||||
if record.PaymentChannel != nil {
|
||||
resp.PaymentChannel = *record.PaymentChannel
|
||||
}
|
||||
if record.PaymentConfigID != nil {
|
||||
resp.PaymentConfigID = record.PaymentConfigID
|
||||
}
|
||||
if record.PaymentTransactionID != nil {
|
||||
resp.PaymentTransactionID = *record.PaymentTransactionID
|
||||
}
|
||||
if record.PaidAt != nil {
|
||||
t := record.PaidAt.Format("2006-01-02 15:04:05")
|
||||
resp.PaidAt = &t
|
||||
}
|
||||
if record.CompletedAt != nil {
|
||||
t := record.CompletedAt.Format("2006-01-02 15:04:05")
|
||||
resp.CompletedAt = &t
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
Reference in New Issue
Block a user