Files
break 62419d4b17
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 13m40s
feat(导出时间筛选): AUG26-014 统一时间筛选与临期导出,归档并同步主 Spec 与证据链
统一时间筛选:新增共享严格解析器 pkg/utils/time_range.go,只接受带显式时区的 RFC3339 秒级时间(拒绝小数秒、无时区、date-only、空格分隔、±hhmm、未补零、非法日期与越界偏移),闭区间含两端、归一为 UTC 瞬时,创建期与执行期共用同一份实现。

端点改造(13 个入口):IoT 卡导入任务、设备导入任务、导出任务列表、订单列表参数名不变仅收紧解析;换货、分配记录、代理充值、临期列表改名 start_time/end_time(旧参数名显式拒绝);提现记录两处删除解析失败静默跳过,非法参数一律 1001;授权记录由起始闭结束开改为闭区间含两端;临期列表改按当前生效主套餐最终到期时刻比较,保留剩余天数上下限与既有粗放窗口。

临期导出新建:新场景 expiring_asset 与受控入口 POST /api/admin/expiring-assets/export,复用列表候选预筛与最终到期推算,一行一资产、加油包不单独成行,列序与 111 §18.1 逐列一致,店铺/业务员/用户组按执行时当前归属补充且不超出创建时冻结范围。

佣金明细导出新增按创建时间闭区间筛选(原佣金与回溯两条分支各自创建时间列),记录粒度、列定义与余额口径不变。

冻结与遗留任务:创建期把筛选与时间边界规范化为 UTC RFC3339 秒级串写入既有 query_json,无新列无迁移;执行期只按冻结值严格解析,非法冻结值在任何分片与文件动作前落任务失败并写安全摘要,不放行全量;重试沿用原快照。达量预警导出执行期同样纳入严格解析(其入口契约、列定义与触发快照口径不变)。

归档 add-export-time-filter-standards 并新建主 Spec openspec/specs/export-time-filter/spec.md,同步 requirement-evidence.json 与入口能力矩阵,README 导出场景清单更新为 11 个场景。

验证:junhong_cmp_test + 本地隔离 Redis(DB7,测试部署共享队列 DB6 未被占用)实跑 85 PASS / 0 FAIL(接受/拒绝集合、区间与顺序语义、列表与导出同筛选行集一致、代理 HTTP 全链路与范围冻结、遗留旧格式任务安全失败、列与余额口径回归、表头逐字),门禁 gofmt/go build/gendocs 两次一致/openspec validate/doctor/context-health 全绿;无 Schema 变更、无迁移、无运行时开关。
2026-09-17 18:37:02 +08:00

722 lines
27 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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"
employeecollectionapp "github.com/break/junhong_cmp_fiber/internal/application/employeecollection"
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"
)
// 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
operationPasswordService OperationPasswordServiceInterface
redis *redis.Client
logger *zap.Logger
rechargeAudit agentrechargeapp.RechargeAuditWriter
billCreation *employeecollectionapp.BillCreationService
}
// SetEmployeeCollectionBillCreation 注入员工代收款账单建账用例。
func (s *Service) SetEmployeeCollectionBillCreation(creation *employeecollectionapp.BillCreationService) {
s.billCreation = creation
}
// New 创建代理预充值服务实例
func New(
db *gorm.DB,
agentRechargeStore *postgres.AgentRechargeStore,
agentWalletStore *postgres.AgentWalletStore,
shopStore *postgres.ShopStore,
wechatConfigService WechatConfigServiceInterface,
operationPasswordService OperationPasswordServiceInterface,
rdb *redis.Client,
logger *zap.Logger,
) *Service {
return &Service{
db: db,
agentRechargeStore: agentRechargeStore,
agentWalletStore: agentWalletStore,
shopStore: shopStore,
wechatConfigService: wechatConfigService,
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
}
// SetRechargeAudit 注入代理充值统一审计 Writer。
func (s *Service) SetRechargeAudit(writer agentrechargeapp.RechargeAuditWriter) {
s.rechargeAudit = writer
}
// 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)
if req.PaymentMethod != constants.RechargeMethodOffline {
return nil, errors.New(errors.CodeInvalidStatus, "在线充值必须通过代理在线创建用例处理")
}
if userType != constants.UserTypePlatform && userType != constants.UserTypeSuperAdmin {
return nil, errors.New(errors.CodeForbidden, "线下充值仅平台管理员可操作")
}
if req.ShopID == nil || *req.ShopID == 0 {
return nil, errors.New(errors.CodeInvalidParam, "线下充值必须指定目标店铺")
}
if 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()
return s.createOffline(ctx, req, userID, userType, rechargeNo)
}
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,
OtherVoucherKeys: req.OtherVoucherKey,
OfflinePaymentMethodID: req.OfflinePaymentMethodID,
ExternalTransactionNo: req.ExternalTransactionNo,
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
}
_, 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,
})
if err != nil {
return err
}
// 员工代收款建账:人工确认入账与企微终审入账同属“线下充值入账成功”事实。
if s.billCreation == nil {
return errors.New(errors.CodeInternalError, "员工代收款建账能力未配置")
}
if _, err := s.billCreation.CreateFromRechargeInTx(ctx, tx, record); err != nil {
return err
}
return s.appendCreditedAudit(ctx, tx, record, nil, constants.RechargeStatusCompleted, "线下充值确认已入账")
})
if err != nil {
return nil, err
}
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(&current).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
}
_, 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,
})
if err != nil {
return err
}
return s.appendCreditedAudit(ctx, tx, record, nil, constants.RechargeStatusCompleted, "代理充值支付回调已入账")
})
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 s.rechargeAudit == nil {
return errors.New(errors.CodeInvalidStatus, "代理充值统一审计接缝未配置")
}
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
result := tx.Model(&model.AgentRechargeRecord{}).
Where("id = ? AND status = ?", record.ID, constants.RechargeStatusPending).
Updates(map[string]any{"status": constants.RechargeStatusRejected, "rejection_reason": strings.TrimSpace(rejectionReason)})
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "驳回充值订单失败")
}
if result.RowsAffected != 1 {
return errors.New(errors.CodeInvalidStatus, "仅待支付订单可驳回")
}
after := *record
after.Status = constants.RechargeStatusRejected
reason := strings.TrimSpace(rejectionReason)
after.RejectionReason = &reason
return s.rechargeAudit.WriteAgentRecharge(ctx, tx, agentrechargeapp.RechargeAudit{
ActionCode: constants.AuditActionAgentRechargeClosed, Summary: "驳回代理充值申请",
Record: &after, BeforeData: map[string]any{"status": record.Status},
AfterData: map[string]any{"status": after.Status, "rejection_reason": reason},
})
}); err != nil {
return err
}
s.logger.Info("代理充值订单驳回成功",
zap.Uint("record_id", id),
zap.String("rejection_reason", rejectionReason),
)
return nil
}
func (s *Service) appendCreditedAudit(ctx context.Context, tx *gorm.DB, record *model.AgentRechargeRecord, payment *model.Payment, status int, summary string) error {
if s.rechargeAudit == nil {
return errors.New(errors.CodeInvalidStatus, "代理充值统一审计接缝未配置")
}
var wallet model.AgentWallet
if err := tx.WithContext(ctx).First(&wallet, record.AgentWalletID).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询代理充值钱包审计快照失败")
}
var transaction model.AgentWalletTransaction
if err := tx.WithContext(ctx).Where("reference_type = ? AND reference_id = ? AND transaction_type = ? AND status = ?",
constants.ReferenceTypeTopup, record.ID, constants.AgentTransactionTypeRecharge, constants.TransactionStatusSuccess).
First(&transaction).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询代理充值入账流水审计快照失败")
}
after := *record
after.Status = status
return s.rechargeAudit.WriteAgentRecharge(ctx, tx, agentrechargeapp.RechargeAudit{
ActionCode: constants.AuditActionAgentRechargeCredited, Summary: summary,
Record: &after, Payment: payment, Wallet: &wallet, Transaction: &transaction,
BeforeData: map[string]any{"status": record.Status}, AfterData: map[string]any{"status": status},
})
}
// TriggerApproval 为历史线下代理充值主动补发企业微信审批。
func (s *Service) TriggerApproval(ctx context.Context, id uint) (*dto.AgentRechargeResponse, error) {
if s.offlineCreation == nil {
return nil, errors.New(errors.CodeServiceUnavailable, "员工线下代充值审批能力未配置")
}
record, err := s.agentRechargeStore.GetByID(ctx, id)
if err != nil {
return nil, errors.New(errors.CodeNotFound, "充值记录不存在")
}
result, err := s.offlineCreation.TriggerHistorical(ctx, record.ID)
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)
return resp, 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.CodeForbidden, "无权限操作该资源或资源不存在")
}
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, startTime, endTime *time.Time) ([]*dto.AgentRechargeResponse, int64, error) {
page := req.Page
pageSize := req.PageSize
if page == 0 {
page = 1
}
if pageSize == 0 {
pageSize = constants.DefaultPageSize
}
query := middleware.ApplyStrictShopFilter(ctx, 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)
}
switch req.RechargeSource {
case constants.AgentRechargeSourcePlatformOffline:
query = query.Where("payment_method NOT IN ?", []string{constants.RechargeMethodWechat, constants.RechargeMethodAlipay})
case constants.AgentRechargeSourceAgentOnline:
query = query.Where("payment_method IN ?", []string{constants.RechargeMethodWechat, constants.RechargeMethodAlipay})
}
if startTime != nil {
query = query.Where("created_at >= ?", *startTime)
}
if endTime != nil {
query = query.Where("created_at <= ?", *endTime)
}
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 {
rechargeSource, rechargeSourceName := constants.GetAgentRechargeSource(record.PaymentMethod)
resp := &dto.AgentRechargeResponse{
ID: record.ID,
RechargeNo: record.RechargeNo,
ShopID: record.ShopID,
ShopName: shopName,
AgentWalletID: record.AgentWalletID,
Amount: record.Amount,
PaymentMethod: record.PaymentMethod,
RechargeSource: rechargeSource,
RechargeSourceName: rechargeSourceName,
PaymentVoucherKey: []string(record.PaymentVoucherKey),
OtherVoucherKey: []string(record.OtherVoucherKeys),
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.ExternalTransactionNo != nil {
resp.ExternalTransactionNo = *record.ExternalTransactionNo
}
if record.OfflinePaymentMethodID != nil {
resp.OfflinePaymentMethodID = *record.OfflinePaymentMethodID
}
if record.OfflinePaymentMethodCode != nil {
resp.OfflinePaymentMethodCode = *record.OfflinePaymentMethodCode
}
if record.OfflinePaymentMethodName != nil {
resp.OfflinePaymentMethodName = *record.OfflinePaymentMethodName
}
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]
}