七月迭代短暂完结,还有很多后端的关键东西没有弄,这是一版赶时间做的东西
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

@@ -0,0 +1,119 @@
package agentrecharge
import (
"context"
"strings"
"gorm.io/gorm"
"gorm.io/gorm/clause"
approvalapp "github.com/break/junhong_cmp_fiber/internal/application/approval"
walletapp "github.com/break/junhong_cmp_fiber/internal/application/wallet"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
// ApprovalDecisionHandler 将渠道无关审批终态应用到员工线下代充值业务。
type ApprovalDecisionHandler struct {
db *gorm.DB
posting *walletapp.PostingService
}
// NewApprovalDecisionHandler 创建员工线下代充值审批终态消费者。
func NewApprovalDecisionHandler(db *gorm.DB, posting *walletapp.PostingService) *ApprovalDecisionHandler {
return &ApprovalDecisionHandler{db: db, posting: posting}
}
// Handle 幂等处理标准审批终态;只有 approved 首次入账,其他终态不修改钱包。
func (h *ApprovalDecisionHandler) Handle(ctx context.Context, event approvalapp.TerminalDecisionEvent) error {
if h == nil || h.db == nil || h.posting == nil {
return errors.New(errors.CodeInternalError, "员工线下代充值审批终态能力未配置")
}
if event.BusinessType != constants.ApprovalBusinessTypeOfflineRecharge || event.BusinessID == 0 || event.InstanceID == 0 {
return errors.New(errors.CodeInvalidParam, "员工线下代充值审批终态参数无效")
}
return h.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var record model.AgentRechargeRecord
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).
Where("id = ? AND payment_method = ?", event.BusinessID, constants.RechargeMethodOffline).
First(&record).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
return errors.Wrap(errors.CodeDatabaseError, err, "锁定员工线下代充值申请失败")
}
if record.ApprovalInstanceID == nil || *record.ApprovalInstanceID != event.InstanceID {
return errors.New(errors.CodeConflict, "线下代充值申请关联的审批实例不一致")
}
switch event.Decision {
case constants.ApprovalDecisionApproved:
return h.applyApproved(ctx, tx, &record, event)
case constants.ApprovalDecisionRejected:
return closeOfflineRecharge(ctx, tx, &record, constants.RechargeStatusRejected, "企业微信审批已拒绝")
case constants.ApprovalDecisionCancelled:
return closeOfflineRecharge(ctx, tx, &record, constants.RechargeStatusClosed, "企业微信审批已撤销")
case constants.ApprovalDecisionDeleted:
return closeOfflineRecharge(ctx, tx, &record, constants.RechargeStatusClosed, "企业微信审批已删除")
case constants.ApprovalDecisionRevokedAfterApproved:
return nil
default:
return errors.New(errors.CodeInvalidParam, "不支持的线下代充值审批终态")
}
})
}
func (h *ApprovalDecisionHandler) applyApproved(
ctx context.Context,
tx *gorm.DB,
record *model.AgentRechargeRecord,
event approvalapp.TerminalDecisionEvent,
) error {
if record.Status != constants.RechargeStatusPending && record.Status != constants.RechargeStatusCompleted {
return errors.New(errors.CodeInvalidStatus, "线下代充值申请状态不允许审批入账")
}
if record.Status == constants.RechargeStatusPending {
result := tx.WithContext(ctx).Model(&model.AgentRechargeRecord{}).
Where("id = ? AND status = ?", record.ID, constants.RechargeStatusPending).
Updates(map[string]any{
"status": constants.RechargeStatusCompleted, "paid_at": event.OccurredAt,
"completed_at": event.OccurredAt, "updated_at": event.OccurredAt,
})
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "完成线下代充值审批申请失败")
}
if result.RowsAffected != 1 {
return errors.New(errors.CodeConflict, "线下代充值申请状态已变化")
}
}
_, err := h.posting.PostInTx(ctx, tx, walletapp.PostingCommand{
ShopID: record.ShopID, WalletID: record.AgentWalletID, Amount: record.Amount,
ReferenceType: constants.ReferenceTypeTopup, ReferenceID: record.ID,
TransactionType: constants.AgentTransactionTypeRecharge,
UserID: event.SubmitterAccountID, Creator: event.SubmitterAccountID,
Remark: "企业微信审批通过线下充值", CorrelationID: event.CorrelationID,
})
return err
}
func closeOfflineRecharge(ctx context.Context, tx *gorm.DB, record *model.AgentRechargeRecord, status int, reason string) error {
if record.Status == status {
return nil
}
if record.Status != constants.RechargeStatusPending {
return errors.New(errors.CodeInvalidStatus, "线下代充值申请状态不允许结束审批")
}
reason = strings.TrimSpace(reason)
result := tx.WithContext(ctx).Model(&model.AgentRechargeRecord{}).
Where("id = ? AND status = ?", record.ID, constants.RechargeStatusPending).
Updates(map[string]any{"status": status, "rejection_reason": reason})
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "结束线下代充值审批申请失败")
}
if result.RowsAffected != 1 {
return errors.New(errors.CodeConflict, "线下代充值申请状态已变化")
}
return nil
}
var _ approvalapp.BusinessDecisionHandler = (*ApprovalDecisionHandler)(nil)

View File

@@ -0,0 +1,188 @@
// Package agentrecharge 收口员工线下代充值申请和审批终态业务用例。
package agentrecharge
import (
"context"
"fmt"
"strings"
"github.com/bytedance/sonic"
"gorm.io/gorm"
approvalapp "github.com/break/junhong_cmp_fiber/internal/application/approval"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
// CreateOfflineCommand 描述员工创建线下代充值审批申请的稳定输入。
type CreateOfflineCommand struct {
SubmitterAccountID uint
SubmitterUserType int
ShopID uint
RechargeNo string
Amount int64
PaymentVoucherKeys []string
Remark string
}
// CreateOfflineResult 返回已原子保存的业务申请和初始审批状态。
type CreateOfflineResult struct {
Record *model.AgentRechargeRecord
ShopName string
SubmitterName string
ApprovalStatus int
}
// OfflineCreationService 创建员工线下代充值申请及唯一通用审批实例。
type OfflineCreationService struct {
db *gorm.DB
approval approvalapp.Port
}
// NewOfflineCreationService 创建员工线下代充值申请用例。
func NewOfflineCreationService(db *gorm.DB, approval approvalapp.Port) *OfflineCreationService {
return &OfflineCreationService{db: db, approval: approval}
}
// Execute 在业务写入前校验审批渠道,并在同一事务保存充值申请、审批实例和提交 Outbox。
func (s *OfflineCreationService) Execute(ctx context.Context, command CreateOfflineCommand) (*CreateOfflineResult, error) {
if s == nil || s.db == nil || s.approval == nil {
return nil, errors.New(errors.CodeServiceUnavailable, "员工线下代充值审批能力未配置")
}
if err := validateCreateOfflineCommand(command); err != nil {
return nil, err
}
account, shop, wallet, err := s.loadCreationFacts(ctx, command)
if err != nil {
return nil, err
}
preparation, err := s.approval.Prepare(ctx, approvalapp.PrepareRequest{
BusinessType: constants.ApprovalBusinessTypeOfflineRecharge, SubmitterAccountID: command.SubmitterAccountID,
CorrelationID: strings.TrimSpace(command.RechargeNo),
})
if err != nil {
return nil, err
}
submitterSnapshot, requestSnapshot, err := offlineApprovalSnapshots(command, account.Username, shop.ShopName)
if err != nil {
return nil, err
}
paymentChannel := constants.RechargeMethodOffline
record := &model.AgentRechargeRecord{
UserID: command.SubmitterAccountID, AgentWalletID: wallet.ID, ShopID: command.ShopID,
RechargeNo: strings.TrimSpace(command.RechargeNo), Amount: command.Amount,
PaymentMethod: constants.RechargeMethodOffline, PaymentChannel: &paymentChannel,
PaymentVoucherKey: model.StringJSONBArray(command.PaymentVoucherKeys), Remark: strings.TrimSpace(command.Remark),
Status: constants.RechargeStatusPending, ShopIDTag: wallet.ShopIDTag, EnterpriseIDTag: wallet.EnterpriseIDTag,
}
var approvalStatus int
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.WithContext(ctx).Create(record).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建员工线下代充值申请失败")
}
reference, err := s.approval.CreateInTx(ctx, tx, approvalapp.CreateRequest{
Preparation: preparation, BusinessType: constants.ApprovalBusinessTypeOfflineRecharge,
BusinessID: record.ID, SubmitterAccountID: command.SubmitterAccountID,
SubmitterSnapshot: submitterSnapshot, RequestSnapshot: requestSnapshot,
CorrelationID: record.RechargeNo,
})
if err != nil {
return err
}
result := tx.WithContext(ctx).Model(&model.AgentRechargeRecord{}).
Where("id = ? AND approval_instance_id IS NULL", record.ID).
Update("approval_instance_id", reference.InstanceID)
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "关联员工线下代充值审批实例失败")
}
if result.RowsAffected != 1 {
return errors.New(errors.CodeConflict, "员工线下代充值审批实例关联已变化")
}
record.ApprovalInstanceID = &reference.InstanceID
approvalStatus = reference.Status
return nil
})
if err != nil {
return nil, err
}
return &CreateOfflineResult{
Record: record, ShopName: shop.ShopName, SubmitterName: account.Username, ApprovalStatus: approvalStatus,
}, nil
}
func validateCreateOfflineCommand(command CreateOfflineCommand) error {
if command.SubmitterUserType != constants.UserTypePlatform && command.SubmitterUserType != constants.UserTypeSuperAdmin {
return errors.New(errors.CodeForbidden, "线下充值仅平台管理员可操作")
}
if command.SubmitterAccountID == 0 || command.ShopID == 0 || strings.TrimSpace(command.RechargeNo) == "" {
return errors.New(errors.CodeInvalidParam)
}
if command.Amount < constants.AgentRechargeMinAmount || command.Amount > constants.AgentRechargeMaxAmount {
return errors.New(errors.CodeInvalidParam, "充值金额超出允许范围")
}
if len(command.PaymentVoucherKeys) == 0 || len(command.PaymentVoucherKeys) > 5 {
return errors.New(errors.CodeInvalidParam, "线下充值必须上传 1 至 5 个支付凭证")
}
for _, key := range command.PaymentVoucherKeys {
if strings.TrimSpace(key) == "" {
return errors.New(errors.CodeInvalidParam, "线下充值支付凭证不能为空")
}
}
return nil
}
func (s *OfflineCreationService) loadCreationFacts(
ctx context.Context,
command CreateOfflineCommand,
) (*model.Account, *model.Shop, *model.AgentWallet, error) {
var account model.Account
if err := s.db.WithContext(ctx).Where("id = ? AND status = ?", command.SubmitterAccountID, constants.StatusEnabled).First(&account).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil, nil, errors.New(errors.CodeForbidden, "提交人账号不可用")
}
return nil, nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询线下代充值提交人失败")
}
var shop model.Shop
if err := s.db.WithContext(ctx).Where("id = ?", command.ShopID).First(&shop).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil, nil, errors.New(errors.CodeNotFound, "目标店铺不存在")
}
return nil, nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询目标店铺失败")
}
var wallet model.AgentWallet
if err := s.db.WithContext(ctx).
Where("shop_id = ? AND wallet_type = ? AND status = ?", command.ShopID, constants.AgentWalletTypeMain, constants.AgentWalletStatusNormal).
First(&wallet).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil, nil, errors.New(errors.CodeWalletNotFound, "目标店铺主钱包不存在或不可用")
}
return nil, nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询目标店铺主钱包失败")
}
return &account, &shop, &wallet, nil
}
func offlineApprovalSnapshots(command CreateOfflineCommand, submitterName, shopName string) ([]byte, []byte, error) {
submitterSnapshot, err := sonic.Marshal(map[string]any{
"account_id": command.SubmitterAccountID, "account_name": submitterName,
"user_type": command.SubmitterUserType,
})
if err != nil {
return nil, nil, errors.Wrap(errors.CodeInternalError, err, "编码线下代充值提交人快照失败")
}
requestSnapshot, err := sonic.Marshal(map[string]any{
constants.ApprovalFieldRechargeNo: strings.TrimSpace(command.RechargeNo),
constants.ApprovalFieldShopID: command.ShopID,
constants.ApprovalFieldShopName: shopName,
constants.ApprovalFieldAmount: fmt.Sprintf("%d.%02d", command.Amount/100, command.Amount%100),
constants.ApprovalFieldAmountCent: command.Amount,
constants.ApprovalFieldPaymentVoucherKey: command.PaymentVoucherKeys,
constants.ApprovalFieldRemark: strings.TrimSpace(command.Remark),
constants.ApprovalFieldSubmitterID: command.SubmitterAccountID,
constants.ApprovalFieldSubmitterName: submitterName,
})
if err != nil {
return nil, nil, errors.Wrap(errors.CodeInternalError, err, "编码线下代充值审批业务快照失败")
}
return submitterSnapshot, requestSnapshot, nil
}