All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m26s
703 lines
25 KiB
Go
703 lines
25 KiB
Go
// Package agent_recharge 提供代理预充值的业务逻辑服务
|
||
// 包含充值订单创建、线下确认、支付回调处理、列表查询等功能
|
||
package agent_recharge
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"math/rand"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/redis/go-redis/v9"
|
||
"go.uber.org/zap"
|
||
"gorm.io/gorm"
|
||
|
||
agentrechargeapp "github.com/break/junhong_cmp_fiber/internal/application/agentrecharge"
|
||
walletapp "github.com/break/junhong_cmp_fiber/internal/application/wallet"
|
||
"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/config"
|
||
"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)
|
||
}
|
||
|
||
// OperationPasswordServiceInterface 全局操作密码服务接口
|
||
type OperationPasswordServiceInterface interface {
|
||
Verify(ctx context.Context, inputPassword string) error
|
||
}
|
||
|
||
// WechatConfigServiceInterface 支付配置服务接口
|
||
type WechatConfigServiceInterface interface {
|
||
GetActiveConfig(ctx context.Context) (*model.WechatConfig, error)
|
||
}
|
||
|
||
// Service 代理预充值业务服务
|
||
// 负责代理钱包充值订单的创建、线下确认、回调处理等业务逻辑
|
||
type Service struct {
|
||
db *gorm.DB
|
||
agentRechargeStore *postgres.AgentRechargeStore
|
||
agentWalletStore *postgres.AgentWalletStore
|
||
agentWalletPosting *walletapp.PostingService
|
||
offlineCreation *agentrechargeapp.OfflineCreationService
|
||
shopStore *postgres.ShopStore
|
||
wechatConfigService WechatConfigServiceInterface
|
||
auditService AuditServiceInterface
|
||
operationPasswordService OperationPasswordServiceInterface
|
||
redis *redis.Client
|
||
logger *zap.Logger
|
||
}
|
||
|
||
// New 创建代理预充值服务实例
|
||
func New(
|
||
db *gorm.DB,
|
||
agentRechargeStore *postgres.AgentRechargeStore,
|
||
agentWalletStore *postgres.AgentWalletStore,
|
||
shopStore *postgres.ShopStore,
|
||
wechatConfigService WechatConfigServiceInterface,
|
||
auditService AuditServiceInterface,
|
||
operationPasswordService OperationPasswordServiceInterface,
|
||
rdb *redis.Client,
|
||
logger *zap.Logger,
|
||
) *Service {
|
||
return &Service{
|
||
db: db,
|
||
agentRechargeStore: agentRechargeStore,
|
||
agentWalletStore: agentWalletStore,
|
||
shopStore: shopStore,
|
||
wechatConfigService: wechatConfigService,
|
||
auditService: auditService,
|
||
operationPasswordService: operationPasswordService,
|
||
redis: rdb,
|
||
logger: logger,
|
||
}
|
||
}
|
||
|
||
// SetAgentWalletPostingService 注入代理主钱包统一入账用例。
|
||
func (s *Service) SetAgentWalletPostingService(service *walletapp.PostingService) {
|
||
s.agentWalletPosting = service
|
||
}
|
||
|
||
// SetOfflineCreationService 注入员工线下代充值审批申请用例。
|
||
func (s *Service) SetOfflineCreationService(service *agentrechargeapp.OfflineCreationService) {
|
||
s.offlineCreation = service
|
||
}
|
||
|
||
// 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 == constants.RechargeMethodOffline && userType != constants.UserTypePlatform && userType != constants.UserTypeSuperAdmin {
|
||
return nil, errors.New(errors.CodeForbidden, "线下充值仅平台管理员可操作")
|
||
}
|
||
|
||
// 线下充值必须上传支付凭证
|
||
if req.PaymentMethod == constants.RechargeMethodOffline && len(req.PaymentVoucherKey) == 0 {
|
||
return nil, errors.New(errors.CodeInvalidParam, "线下充值必须上传支付凭证")
|
||
}
|
||
|
||
if req.Amount < constants.AgentRechargeMinAmount || req.Amount > constants.AgentRechargeMaxAmount {
|
||
return nil, errors.New(errors.CodeInvalidParam, "充值金额超出允许范围")
|
||
}
|
||
|
||
rechargeNo := s.generateRechargeNo()
|
||
if req.PaymentMethod == constants.RechargeMethodOffline {
|
||
return s.createOffline(ctx, req, userID, userType, rechargeNo)
|
||
}
|
||
|
||
// 查找目标店铺的主钱包
|
||
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"
|
||
}
|
||
|
||
record := &model.AgentRechargeRecord{
|
||
UserID: userID,
|
||
AgentWalletID: wallet.ID,
|
||
ShopID: req.ShopID,
|
||
RechargeNo: rechargeNo,
|
||
Amount: req.Amount,
|
||
PaymentMethod: req.PaymentMethod,
|
||
PaymentChannel: &paymentChannel,
|
||
PaymentConfigID: paymentConfigID,
|
||
PaymentVoucherKey: model.StringJSONBArray(req.PaymentVoucherKey),
|
||
Remark: req.Remark,
|
||
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),
|
||
)
|
||
|
||
resp := toResponse(record, shop.ShopName)
|
||
resp.SubmitterName = s.loadSubmitterNameBestEffort(ctx, record.UserID)
|
||
return resp, nil
|
||
}
|
||
|
||
func (s *Service) createOffline(
|
||
ctx context.Context,
|
||
req *dto.CreateAgentRechargeRequest,
|
||
userID uint,
|
||
userType int,
|
||
rechargeNo string,
|
||
) (*dto.AgentRechargeResponse, error) {
|
||
if s.offlineCreation == nil {
|
||
return nil, errors.New(errors.CodeServiceUnavailable, "员工线下代充值审批能力未配置")
|
||
}
|
||
result, err := s.offlineCreation.Execute(ctx, agentrechargeapp.CreateOfflineCommand{
|
||
SubmitterAccountID: userID,
|
||
SubmitterUserType: userType,
|
||
ShopID: req.ShopID,
|
||
RechargeNo: rechargeNo,
|
||
Amount: req.Amount,
|
||
PaymentVoucherKeys: req.PaymentVoucherKey,
|
||
Remark: req.Remark,
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp := toResponse(result.Record, result.ShopName)
|
||
resp.SubmitterName = result.SubmitterName
|
||
resp.ApprovalProvider = constants.IntegrationProviderWeCom
|
||
resp.ApprovalStatus = &result.ApprovalStatus
|
||
resp.ApprovalStatusName = constants.GetApprovalStatusName(result.ApprovalStatus)
|
||
s.logger.Info("创建员工线下代充值审批申请成功",
|
||
zap.Uint("recharge_id", result.Record.ID),
|
||
zap.String("recharge_no", result.Record.RechargeNo),
|
||
zap.Uint("approval_instance_id", *result.Record.ApprovalInstanceID),
|
||
zap.Int64("amount", result.Record.Amount),
|
||
zap.Uint("shop_id", result.Record.ShopID),
|
||
zap.Uint("submitter_id", result.Record.UserID),
|
||
)
|
||
return resp, 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) {
|
||
if !legacyOfflineRechargePayEnabled() {
|
||
return nil, errors.New(errors.CodeInvalidStatus, "线下充值人工确认入口已停用,请查看企业微信审批状态")
|
||
}
|
||
userID := middleware.GetUserIDFromContext(ctx)
|
||
userType := middleware.GetUserTypeFromContext(ctx)
|
||
|
||
// 仅平台账号可操作
|
||
if userType != constants.UserTypePlatform && userType != constants.UserTypeSuperAdmin {
|
||
return nil, errors.New(errors.CodeForbidden, "仅平台管理员可确认线下充值")
|
||
}
|
||
|
||
// 验证全局操作密码
|
||
if err := s.operationPasswordService.Verify(ctx, req.OperationPassword); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
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 != constants.RechargeMethodOffline {
|
||
return nil, errors.New(errors.CodeInvalidParam, "该订单非线下充值,不支持此操作")
|
||
}
|
||
if record.ApprovalInstanceID != nil {
|
||
return nil, errors.New(errors.CodeInvalidStatus, "该线下充值申请由企业微信审批决定,不能人工确认")
|
||
}
|
||
if record.Status != constants.RechargeStatusPending {
|
||
return nil, errors.New(errors.CodeInvalidParam, "该订单状态不允许确认支付")
|
||
}
|
||
|
||
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, "充值记录状态已变更")
|
||
}
|
||
|
||
if s.agentWalletPosting == nil {
|
||
return errors.New(errors.CodeInternalError, "代理主钱包入账能力未配置")
|
||
}
|
||
requestID := ""
|
||
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
|
||
requestID = *value
|
||
}
|
||
if _, err := s.agentWalletPosting.PostInTx(ctx, tx, walletapp.PostingCommand{
|
||
ShopID: record.ShopID, WalletID: record.AgentWalletID, Amount: record.Amount,
|
||
ReferenceType: constants.ReferenceTypeTopup, ReferenceID: record.ID,
|
||
TransactionType: constants.AgentTransactionTypeRecharge, UserID: userID, Creator: userID,
|
||
Remark: "线下充值确认", RequestID: requestID, CorrelationID: record.RechargeNo,
|
||
}); err != nil {
|
||
return 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
|
||
|
||
resp := toResponse(record, shopName)
|
||
resp.SubmitterName = s.loadSubmitterNameBestEffort(ctx, record.UserID)
|
||
approvalSummaries, err := s.loadApprovalSummaries(ctx, []*model.AgentRechargeRecord{record})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
applyApprovalSummary(resp, approvalSummaries, record.ApprovalInstanceID)
|
||
return resp, nil
|
||
}
|
||
|
||
func legacyOfflineRechargePayEnabled() bool {
|
||
cfg := config.Get()
|
||
return cfg == nil || cfg.Approval.LegacyOfflineRechargePayEnabled
|
||
}
|
||
|
||
// HandlePaymentCallback 处理支付回调
|
||
// 幂等处理:status != 待支付则直接返回成功
|
||
func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string, paymentMethod string, paymentTransactionID string, paidAmount int64) error {
|
||
paymentTransactionID = strings.TrimSpace(paymentTransactionID)
|
||
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 err := validateAgentRechargePayment(record, paymentMethod, paymentTransactionID, paidAmount); err != nil {
|
||
return err
|
||
}
|
||
|
||
// 已完成订单仅在第三方交易号一致时按同一回调幂等成功。
|
||
if record.Status != constants.RechargeStatusPending {
|
||
if record.Status == constants.RechargeStatusCompleted && record.PaymentTransactionID != nil && *record.PaymentTransactionID == strings.TrimSpace(paymentTransactionID) {
|
||
s.logger.Info("代理充值支付回调幂等命中", zap.String("recharge_no", rechargeNo), zap.Int("status", record.Status))
|
||
return nil
|
||
}
|
||
return errors.New(errors.CodeInvalidStatus, "充值订单状态不允许确认支付")
|
||
}
|
||
now := time.Now()
|
||
err = s.db.Transaction(func(tx *gorm.DB) error {
|
||
// 条件更新(WHERE status = 1)
|
||
result := tx.Model(&model.AgentRechargeRecord{}).
|
||
Where("id = ? AND status = ? AND payment_method <> ? AND amount = ?", record.ID, constants.RechargeStatusPending, constants.RechargeMethodOffline, paidAmount).
|
||
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 {
|
||
var current model.AgentRechargeRecord
|
||
if err := tx.WithContext(ctx).Unscoped().
|
||
Where("id = ?", record.ID).First(¤t).Error; err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "复核充值记录终态失败")
|
||
}
|
||
if current.Status == constants.RechargeStatusCompleted && current.PaymentTransactionID != nil && *current.PaymentTransactionID == paymentTransactionID {
|
||
return nil
|
||
}
|
||
return errors.New(errors.CodeConflict, "充值订单已被其他请求处理")
|
||
}
|
||
|
||
if s.agentWalletPosting == nil {
|
||
return errors.New(errors.CodeInternalError, "代理主钱包入账能力未配置")
|
||
}
|
||
requestID := ""
|
||
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
|
||
requestID = *value
|
||
}
|
||
if _, err := s.agentWalletPosting.PostInTx(ctx, tx, walletapp.PostingCommand{
|
||
ShopID: record.ShopID, WalletID: record.AgentWalletID, Amount: record.Amount,
|
||
ReferenceType: constants.ReferenceTypeTopup, ReferenceID: record.ID,
|
||
TransactionType: constants.AgentTransactionTypeRecharge, UserID: record.UserID, Creator: record.UserID,
|
||
Remark: "在线支付充值", RequestID: requestID, CorrelationID: record.RechargeNo,
|
||
}); err != nil {
|
||
return 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
|
||
}
|
||
|
||
func validateAgentRechargePayment(record *model.AgentRechargeRecord, paymentMethod, paymentTransactionID string, paidAmount int64) error {
|
||
if record == nil || strings.TrimSpace(paymentTransactionID) == "" || paidAmount <= 0 || paidAmount != record.Amount {
|
||
return errors.New(errors.CodeInvalidParam, "代理充值支付回调金额或交易号不匹配")
|
||
}
|
||
if record.PaymentMethod == constants.RechargeMethodOffline || record.PaymentConfigID == nil || record.PaymentChannel == nil {
|
||
return errors.New(errors.CodeInvalidStatus, "线下充值订单不能通过支付回调确认")
|
||
}
|
||
channel := strings.TrimSpace(*record.PaymentChannel)
|
||
switch strings.TrimSpace(paymentMethod) {
|
||
case model.PaymentMethodWechat:
|
||
if record.PaymentMethod != constants.RechargeMethodWechat || (channel != model.ProviderTypeWechat && channel != model.ProviderTypeWechatV2) {
|
||
return errors.New(errors.CodeInvalidParam, "代理充值支付渠道不匹配")
|
||
}
|
||
case model.ProviderTypeFuiou:
|
||
if record.PaymentMethod != constants.RechargeMethodWechat || channel != model.ProviderTypeFuiou {
|
||
return errors.New(errors.CodeInvalidParam, "代理充值支付渠道不匹配")
|
||
}
|
||
default:
|
||
return errors.New(errors.CodeInvalidParam, "代理充值支付渠道不受支持")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Reject 驳回代理充值订单
|
||
// 仅 status=1(待支付)的订单可驳回,驳回后状态变为 6(已驳回),为终态
|
||
func (s *Service) Reject(ctx context.Context, id uint, rejectionReason string) error {
|
||
record, err := s.agentRechargeStore.GetByID(ctx, id)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return errors.New(errors.CodeNotFound, "充值记录不存在")
|
||
}
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "查询充值记录失败")
|
||
}
|
||
|
||
if record.Status != constants.RechargeStatusPending {
|
||
return errors.New(errors.CodeInvalidStatus, "仅待支付订单可驳回")
|
||
}
|
||
if record.ApprovalInstanceID != nil {
|
||
return errors.New(errors.CodeInvalidStatus, "该线下充值申请由企业微信审批决定,不能人工驳回")
|
||
}
|
||
|
||
if err := s.agentRechargeStore.UpdateStatusWithRejection(ctx, id, rejectionReason); err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return errors.New(errors.CodeInvalidStatus, "仅待支付订单可驳回")
|
||
}
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "驳回充值订单失败")
|
||
}
|
||
|
||
s.logger.Info("代理充值订单驳回成功",
|
||
zap.Uint("record_id", id),
|
||
zap.String("rejection_reason", rejectionReason),
|
||
)
|
||
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
|
||
}
|
||
|
||
resp := toResponse(record, shopName)
|
||
resp.SubmitterName = s.loadSubmitterNameBestEffort(ctx, record.UserID)
|
||
approvalSummaries, err := s.loadApprovalSummaries(ctx, []*model.AgentRechargeRecord{record})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
applyApprovalSummary(resp, approvalSummaries, record.ApprovalInstanceID)
|
||
return resp, 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
|
||
}
|
||
}
|
||
}
|
||
submitterIDs := make([]uint, 0, len(records))
|
||
for _, record := range records {
|
||
if record.UserID > 0 {
|
||
submitterIDs = append(submitterIDs, record.UserID)
|
||
}
|
||
}
|
||
submitterNames, err := s.loadSubmitterNames(ctx, submitterIDs)
|
||
if err != nil {
|
||
return nil, 0, errors.Wrap(errors.CodeDatabaseError, err, "查询充值提交人失败")
|
||
}
|
||
approvalSummaries, err := s.loadApprovalSummaries(ctx, records)
|
||
if err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
list := make([]*dto.AgentRechargeResponse, 0, len(records))
|
||
for _, r := range records {
|
||
item := toResponse(r, shopMap[r.ShopID])
|
||
item.SubmitterName = submitterNames[r.UserID]
|
||
applyApprovalSummary(item, approvalSummaries, r.ApprovalInstanceID)
|
||
list = append(list, item)
|
||
}
|
||
|
||
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,
|
||
PaymentVoucherKey: []string(record.PaymentVoucherKey),
|
||
Remark: record.Remark,
|
||
Status: record.Status,
|
||
StatusName: constants.GetRechargeStatusName(record.Status),
|
||
RejectionReason: record.RejectionReason,
|
||
SubmitterID: record.UserID,
|
||
ApprovalInstanceID: record.ApprovalInstanceID,
|
||
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
|
||
}
|
||
|
||
type approvalSummary struct {
|
||
Provider string
|
||
Status int
|
||
}
|
||
|
||
func (s *Service) loadApprovalSummaries(
|
||
ctx context.Context,
|
||
records []*model.AgentRechargeRecord,
|
||
) (map[uint]approvalSummary, error) {
|
||
ids := make([]uint, 0, len(records))
|
||
seen := make(map[uint]struct{}, len(records))
|
||
for _, record := range records {
|
||
if record == nil || record.ApprovalInstanceID == nil || *record.ApprovalInstanceID == 0 {
|
||
continue
|
||
}
|
||
id := *record.ApprovalInstanceID
|
||
if _, exists := seen[id]; exists {
|
||
continue
|
||
}
|
||
seen[id] = struct{}{}
|
||
ids = append(ids, id)
|
||
}
|
||
summaries := make(map[uint]approvalSummary, len(ids))
|
||
if len(ids) == 0 {
|
||
return summaries, nil
|
||
}
|
||
var instances []model.ApprovalInstance
|
||
if err := s.db.WithContext(ctx).Select("id", "provider", "status").Where("id IN ?", ids).Find(&instances).Error; err != nil {
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "批量查询充值审批状态失败")
|
||
}
|
||
for _, instance := range instances {
|
||
summaries[instance.ID] = approvalSummary{Provider: instance.Provider, Status: instance.Status}
|
||
}
|
||
return summaries, nil
|
||
}
|
||
|
||
func applyApprovalSummary(response *dto.AgentRechargeResponse, summaries map[uint]approvalSummary, instanceID *uint) {
|
||
if response == nil || instanceID == nil {
|
||
return
|
||
}
|
||
summary, exists := summaries[*instanceID]
|
||
if !exists {
|
||
return
|
||
}
|
||
status := summary.Status
|
||
response.ApprovalProvider = summary.Provider
|
||
response.ApprovalStatus = &status
|
||
response.ApprovalStatusName = constants.GetApprovalStatusName(status)
|
||
}
|
||
|
||
func (s *Service) loadSubmitterNames(ctx context.Context, ids []uint) (map[uint]string, error) {
|
||
accounts, err := postgres.NewAccountStore(s.db, nil).GetDisplayAccountsByIDs(ctx, ids)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
names := make(map[uint]string, len(accounts))
|
||
for _, account := range accounts {
|
||
names[account.ID] = account.Username
|
||
}
|
||
return names, nil
|
||
}
|
||
|
||
func (s *Service) loadSubmitterNameBestEffort(ctx context.Context, id uint) string {
|
||
names, err := s.loadSubmitterNames(ctx, []uint{id})
|
||
if err != nil {
|
||
s.logger.Warn("查询充值提交人失败", zap.Uint("submitter_id", id), zap.Error(err))
|
||
return ""
|
||
}
|
||
return names[id]
|
||
}
|