七月迭代短暂完结,还有很多后端的关键东西没有弄,这是一版赶时间做的东西
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m26s

This commit is contained in:
2026-07-25 17:06:58 +08:00
parent ad9f613dd6
commit 73f5125d3d
249 changed files with 17137 additions and 877 deletions

View File

@@ -13,10 +13,12 @@ import (
"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"
@@ -44,6 +46,7 @@ type Service struct {
agentRechargeStore *postgres.AgentRechargeStore
agentWalletStore *postgres.AgentWalletStore
agentWalletPosting *walletapp.PostingService
offlineCreation *agentrechargeapp.OfflineCreationService
shopStore *postgres.ShopStore
wechatConfigService WechatConfigServiceInterface
auditService AuditServiceInterface
@@ -82,6 +85,11 @@ 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) {
@@ -95,12 +103,12 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateAgentRechargeReques
}
// 线下充值仅平台可用
if req.PaymentMethod == "offline" && userType != constants.UserTypePlatform && userType != constants.UserTypeSuperAdmin {
if req.PaymentMethod == constants.RechargeMethodOffline && userType != constants.UserTypePlatform && userType != constants.UserTypeSuperAdmin {
return nil, errors.New(errors.CodeForbidden, "线下充值仅平台管理员可操作")
}
// 线下充值必须上传支付凭证
if req.PaymentMethod == "offline" && len(req.PaymentVoucherKey) == 0 {
if req.PaymentMethod == constants.RechargeMethodOffline && len(req.PaymentVoucherKey) == 0 {
return nil, errors.New(errors.CodeInvalidParam, "线下充值必须上传支付凭证")
}
@@ -108,6 +116,11 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateAgentRechargeReques
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 {
@@ -140,8 +153,6 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateAgentRechargeReques
paymentChannel = "offline"
}
rechargeNo := s.generateRechargeNo()
record := &model.AgentRechargeRecord{
UserID: userID,
AgentWalletID: wallet.ID,
@@ -170,12 +181,55 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateAgentRechargeReques
zap.Uint("user_id", userID),
)
return toResponse(record, shop.ShopName), nil
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)
@@ -197,9 +251,12 @@ func (s *Service) OfflinePay(ctx context.Context, id uint, req *dto.AgentOffline
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询充值记录失败")
}
if record.PaymentMethod != "offline" {
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, "该订单状态不允许确认支付")
}
@@ -266,7 +323,19 @@ func (s *Service) OfflinePay(ctx context.Context, id uint, req *dto.AgentOffline
record.PaidAt = &now
record.CompletedAt = &now
return toResponse(record, shopName), nil
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 处理支付回调
@@ -388,6 +457,9 @@ func (s *Service) Reject(ctx context.Context, id uint, rejectionReason string) e
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 {
@@ -420,7 +492,14 @@ func (s *Service) GetByID(ctx context.Context, id uint) (*dto.AgentRechargeRespo
shopName = shop.ShopName
}
return toResponse(record, shopName), nil
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 分页查询充值订单列表
@@ -475,10 +554,27 @@ func (s *Service) List(ctx context.Context, req *dto.AgentRechargeListRequest) (
}
}
}
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 {
list = append(list, toResponse(r, shopMap[r.ShopID]))
item := toResponse(r, shopMap[r.ShopID])
item.SubmitterName = submitterNames[r.UserID]
applyApprovalSummary(item, approvalSummaries, r.ApprovalInstanceID)
list = append(list, item)
}
return list, total, nil
@@ -495,20 +591,22 @@ func (s *Service) generateRechargeNo() string {
// 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,
CreatedAt: record.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: record.UpdatedAt.Format("2006-01-02 15:04:05"),
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 {
@@ -531,3 +629,74 @@ func toResponse(record *model.AgentRechargeRecord, shopName string) *dto.AgentRe
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]
}