feat(代理自充): AUG26-017 代理自充收款方式与线下预存款审批字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m52s

- 受控配置新增代理在线自充允许范围(仅微信/仅支付宝/同时支持),读侧与创建侧取允许范围与可用商户池交集,两侧失败关闭
- 新增允许范围查询与修改端点,读限代理与平台账号、写限超级管理员,复用受控配置写服务留痕
- tb_agent_recharge_record 新增交易流水号、线下收款方式三列快照与其他凭证列(成对迁移 000213)
- 线下申请校验启用的收款方式字典项与必填交易流水号,交易流水号独立于在线渠道交易号、不参与去重
- 扩展 offline_recharge_approval 场景可映射字段白名单与字典引用保护
- 新增付款凭证识别能力与交易流水号预填接口,识别不落库、日志不记录载荷
This commit is contained in:
2026-09-11 15:21:23 +08:00
parent e687a266e6
commit 7891189712
32 changed files with 1168 additions and 172 deletions

View File

@@ -24,7 +24,12 @@ type CreateOfflineCommand struct {
RechargeNo string
Amount int64
PaymentVoucherKeys []string
Remark string
OtherVoucherKeys []string
// OfflinePaymentMethodID 是提交人选择的线下收款方式字典项 ID。
OfflinePaymentMethodID uint
// ExternalTransactionNo 是人工确认后的交易流水号,独立于在线渠道第三方交易号。
ExternalTransactionNo string
Remark string
}
// CreateOfflineResult 返回已原子保存的业务申请和初始审批状态。
@@ -78,8 +83,20 @@ func (s *OfflineCreationService) TriggerHistorical(ctx context.Context, recordID
SubmitterAccountID: record.UserID, SubmitterUserType: account.UserType, ShopID: record.ShopID,
RechargeNo: record.RechargeNo, Amount: record.Amount,
PaymentVoucherKeys: []string(record.PaymentVoucherKey), Remark: record.Remark,
OtherVoucherKeys: []string(record.OtherVoucherKeys),
}
submitterSnapshot, requestSnapshot, err := offlineApprovalSnapshots(command, account.Username, shop.ShopName)
if record.ExternalTransactionNo != nil {
command.ExternalTransactionNo = *record.ExternalTransactionNo
}
// 补发审批使用历史记录已冻结的收款方式快照,不回查当前字典,避免历史材料被字典变更改写。
var frozenCode, frozenName string
if record.OfflinePaymentMethodCode != nil {
frozenCode = *record.OfflinePaymentMethodCode
}
if record.OfflinePaymentMethodName != nil {
frozenName = *record.OfflinePaymentMethodName
}
submitterSnapshot, requestSnapshot, err := offlineApprovalSnapshots(command, account.Username, shop.ShopName, frozenCode, frozenName)
if err != nil {
return nil, err
}
@@ -154,20 +171,31 @@ func (s *OfflineCreationService) Execute(ctx context.Context, command CreateOffl
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,
}
externalTransactionNo := strings.TrimSpace(command.ExternalTransactionNo)
var record *model.AgentRechargeRecord
var approvalStatus int
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
paymentMethod, err := loadEnabledOfflinePaymentMethod(ctx, tx, command.OfflinePaymentMethodID)
if err != nil {
return err
}
submitterSnapshot, requestSnapshot, err := offlineApprovalSnapshots(command, account.Username, shop.ShopName, paymentMethod.Code, paymentMethod.Name)
if err != nil {
return err
}
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),
ExternalTransactionNo: &externalTransactionNo,
OfflinePaymentMethodID: &paymentMethod.ID,
OfflinePaymentMethodCode: &paymentMethod.Code,
OfflinePaymentMethodName: &paymentMethod.Name,
OtherVoucherKeys: model.StringJSONBArray(command.OtherVoucherKeys),
Status: constants.RechargeStatusPending, ShopIDTag: wallet.ShopIDTag, EnterpriseIDTag: wallet.EnterpriseIDTag,
}
if err := tx.WithContext(ctx).Create(record).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建员工线下代充值申请失败")
}
@@ -218,17 +246,71 @@ func validateCreateOfflineCommand(command CreateOfflineCommand) error {
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 个支付凭证")
if command.OfflinePaymentMethodID == 0 {
return errors.New(errors.CodeInvalidParam, "线下充值必须选择线下收款方式")
}
for _, key := range command.PaymentVoucherKeys {
if strings.TrimSpace(key) == "" {
return errors.New(errors.CodeInvalidParam, "线下充值支付凭证不能为空")
}
if err := validateRechargeTransactionNo(command.ExternalTransactionNo); err != nil {
return err
}
if err := validateVoucherKeys(command.PaymentVoucherKeys, 1, constants.AgentRechargePaymentVoucherMaxCount, "线下充值必须上传 1 至 5 个支付凭证"); err != nil {
return err
}
return validateVoucherKeys(command.OtherVoucherKeys, 0, constants.AgentRechargeOtherVoucherMaxCount, "线下充值其他凭证最多 5 个")
}
// validateRechargeTransactionNo 校验交易流水号必填且不超过长度上限;不参与去重与幂等判定。
func validateRechargeTransactionNo(value string) error {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return errors.New(errors.CodeInvalidParam, "线下充值必须填写交易流水号")
}
if len([]rune(trimmed)) > constants.AgentRechargeExternalTransactionNoMaxLength {
return errors.New(errors.CodeInvalidParam, "交易流水号长度超出限制")
}
return nil
}
// validateVoucherKeys 校验凭证对象键数量与内容minCount 为 0 时允许为空。
func validateVoucherKeys(keys []string, minCount, maxCount int, message string) error {
if len(keys) < minCount || len(keys) > maxCount {
return errors.New(errors.CodeInvalidParam, message)
}
seen := make(map[string]struct{}, len(keys))
for _, key := range keys {
trimmed := strings.TrimSpace(key)
if trimmed == "" {
return errors.New(errors.CodeInvalidParam, "线下充值凭证对象键不能为空")
}
if len([]rune(trimmed)) > constants.AgentRechargeVoucherKeyMaxLength {
return errors.New(errors.CodeInvalidParam, "线下充值凭证对象键长度超出限制")
}
if _, exists := seen[trimmed]; exists {
return errors.New(errors.CodeInvalidParam, "线下充值凭证对象键不能重复")
}
seen[trimmed] = struct{}{}
}
return nil
}
// loadEnabledOfflinePaymentMethod 读取启用的线下收款方式字典项;不存在或已停用一律拒绝。
// 仅校验存在性与启停,不做编码或名称的二次改写,快照以字典当前值为准。
func loadEnabledOfflinePaymentMethod(ctx context.Context, tx *gorm.DB, id uint) (*model.EmployeeCollectionPaymentMethod, error) {
if id == 0 {
return nil, errors.New(errors.CodeInvalidParam, "线下充值必须选择线下收款方式")
}
var paymentMethod model.EmployeeCollectionPaymentMethod
if err := tx.WithContext(ctx).First(&paymentMethod, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeEmployeeCollectionPaymentMethodNotFound)
}
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询线下收款方式失败")
}
if paymentMethod.Status != constants.EmployeeCollectionPaymentMethodStatusEnabled {
return nil, errors.New(errors.CodeEmployeeCollectionPaymentMethodDisabled)
}
return &paymentMethod, nil
}
func (s *OfflineCreationService) loadHistoricalFacts(
ctx context.Context, record *model.AgentRechargeRecord,
) (*model.Account, *model.Shop, *model.AgentWallet, error) {
@@ -291,7 +373,7 @@ func (s *OfflineCreationService) loadCreationFacts(
return &account, &shop, &wallet, nil
}
func offlineApprovalSnapshots(command CreateOfflineCommand, submitterName, shopName string) ([]byte, []byte, error) {
func offlineApprovalSnapshots(command CreateOfflineCommand, submitterName, shopName, paymentMethodCode, paymentMethodName string) ([]byte, []byte, error) {
submitterSnapshot, err := sonic.Marshal(map[string]any{
"account_id": command.SubmitterAccountID, "account_name": submitterName,
"user_type": command.SubmitterUserType,
@@ -300,15 +382,19 @@ func offlineApprovalSnapshots(command CreateOfflineCommand, submitterName, shopN
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,
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,
constants.ApprovalFieldOfflinePaymentMethod: paymentMethodName,
constants.ApprovalFieldOfflinePaymentMethodCode: paymentMethodCode,
constants.ApprovalFieldExternalTransactionNo: strings.TrimSpace(command.ExternalTransactionNo),
constants.ApprovalFieldOtherVoucherKey: command.OtherVoucherKeys,
})
if err != nil {
return nil, nil, errors.Wrap(errors.CodeInternalError, err, "编码线下代充值审批业务快照失败")

View File

@@ -53,6 +53,12 @@ type OnlineCreationService struct {
alipay OnlinePaymentPort
fuiou OnlinePaymentPort
audit PaymentAuditWriter
policy *OnlinePaymentMethodPolicy
}
// SetPaymentMethodPolicy 注入代理在线自充允许范围策略。
func (s *OnlineCreationService) SetPaymentMethodPolicy(policy *OnlinePaymentMethodPolicy) {
s.policy = policy
}
// NewOnlineCreationService 创建代理在线充值用例并以结构体字段注入运行时路由和三个渠道 Adapter。
@@ -62,7 +68,7 @@ func NewOnlineCreationService(db *gorm.DB, runtime *merchantpayment.RuntimeLoade
// Execute 以短事务建单,事务外生成支付链接,再条件保存链接或关闭失败订单。
func (s *OnlineCreationService) Execute(ctx context.Context, command CreateOnlineCommand) (*CreateOnlineResult, error) {
if s == nil || s.db == nil || s.runtime == nil || s.wechat == nil || s.alipay == nil || s.fuiou == nil || s.audit == nil {
if s == nil || s.db == nil || s.runtime == nil || s.wechat == nil || s.alipay == nil || s.fuiou == nil || s.audit == nil || s.policy == nil {
return nil, apperrors.New(apperrors.CodeServiceUnavailable, "代理在线充值能力未配置")
}
command.PaymentMethod = strings.TrimSpace(command.PaymentMethod)
@@ -81,9 +87,14 @@ func (s *OnlineCreationService) Execute(ctx context.Context, command CreateOnlin
if err != nil {
return nil, apperrors.Wrap(apperrors.CodeInternalError, err, "生成在线充值请求指纹失败")
}
// 幂等回放先于允许范围门禁:同一 request_id 的重试属于既有单,不是新单,
// 不因允许范围变更被拒绝;允许范围只拦截会真正新建充值单与支付单的路径。
if replay, found, err := s.loadReplay(ctx, command, fingerprint); err != nil || found {
return replay, err
}
if err := s.policy.IsAllowed(ctx, command.PaymentMethod); err != nil {
return nil, err
}
account, shop, wallet, err := s.loadCreationFacts(ctx, command)
if err != nil {
return nil, err
@@ -127,15 +138,25 @@ func (s *OnlineCreationService) Execute(ctx context.Context, command CreateOnlin
return result, nil
}
// AvailablePaymentMethods 按固定顺序返回已有可用商户池且凭证完整的在线支付方式。
// AvailablePaymentMethods 按允许范围与可用商户池交集返回在线支付方式。
func (s *OnlineCreationService) AvailablePaymentMethods(ctx context.Context, userType int) (AvailablePaymentMethodsResult, error) {
result := AvailablePaymentMethodsResult{
Methods: []string{}, MinAmount: constants.AgentOnlineRechargeMinAmount, MaxAmount: constants.AgentRechargeMaxAmount,
}
if userType != constants.UserTypeAgent {
return result, apperrors.New(apperrors.CodeForbidden, "代理账号可以查询在线支付方式")
if s == nil || s.db == nil || s.runtime == nil {
return result, apperrors.New(apperrors.CodeServiceUnavailable, "代理在线充值能力未配置")
}
for _, method := range []string{constants.RechargeMethodWechat, constants.RechargeMethodAlipay} {
if userType != constants.UserTypeAgent && userType != constants.UserTypePlatform {
return result, apperrors.New(apperrors.CodeForbidden, "仅代理或平台账号可以查询在线支付方式")
}
if s.policy == nil {
return result, apperrors.New(apperrors.CodeServiceUnavailable, "代理在线充值允许范围策略未配置")
}
allowed, err := s.policy.AllowedMethods(ctx)
if err != nil {
return result, err
}
for _, method := range allowed {
var merchants []model.PaymentMerchant
err := s.db.WithContext(ctx).
Model(&model.PaymentMerchant{}).

View File

@@ -0,0 +1,58 @@
package agentrecharge
import (
"context"
"github.com/break/junhong_cmp_fiber/pkg/constants"
apperrors "github.com/break/junhong_cmp_fiber/pkg/errors"
)
// OnlinePaymentMethodConfigReader 提供代理在线自充允许范围的严格读取能力。
type OnlinePaymentMethodConfigReader interface {
GetStrict(ctx context.Context, key string) (string, error)
}
// OnlinePaymentMethodPolicy 将受控配置值映射为对外可见的线上支付方式集合。
type OnlinePaymentMethodPolicy struct {
reader OnlinePaymentMethodConfigReader
}
// NewOnlinePaymentMethodPolicy 创建代理在线自充允许范围策略。
func NewOnlinePaymentMethodPolicy(reader OnlinePaymentMethodConfigReader) *OnlinePaymentMethodPolicy {
return &OnlinePaymentMethodPolicy{reader: reader}
}
// AllowedMethods 严格读取允许范围;配置缺失使用注册默认值,非法值失败关闭。
func (p *OnlinePaymentMethodPolicy) AllowedMethods(ctx context.Context) ([]string, error) {
if p == nil || p.reader == nil {
return nil, apperrors.New(apperrors.CodeServiceUnavailable, "代理在线充值允许范围未配置")
}
value, err := p.reader.GetStrict(ctx, constants.SystemConfigAgentSelfRechargeAllowedMethods)
if err != nil {
return nil, apperrors.Wrap(apperrors.CodeNoPaymentConfig, err, "读取代理在线充值允许范围失败")
}
switch value {
case constants.AgentSelfRechargeAllowedWechatOnly:
return []string{constants.RechargeMethodWechat}, nil
case constants.AgentSelfRechargeAllowedAlipayOnly:
return []string{constants.RechargeMethodAlipay}, nil
case constants.AgentSelfRechargeAllowedBoth:
return []string{constants.RechargeMethodWechat, constants.RechargeMethodAlipay}, nil
default:
return nil, apperrors.New(apperrors.CodeNoPaymentConfig, "代理在线充值允许范围值非法")
}
}
// IsAllowed 判断业务支付方式是否在当前受控允许范围内。
func (p *OnlinePaymentMethodPolicy) IsAllowed(ctx context.Context, method string) error {
methods, err := p.AllowedMethods(ctx)
if err != nil {
return err
}
for _, allowed := range methods {
if allowed == method {
return nil
}
}
return apperrors.New(apperrors.CodeNoPaymentConfig, "当前支付方式不在代理在线充值允许范围内")
}

View File

@@ -0,0 +1,109 @@
package agentrecharge
import (
"context"
"encoding/base64"
"io"
"net/http"
"strings"
"github.com/break/junhong_cmp_fiber/pkg/constants"
apperrors "github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/storage"
)
// PaymentVoucherObjectStore 提供付款凭证附件的元数据与内容读取能力。
type PaymentVoucherObjectStore interface {
Stat(ctx context.Context, key string) (*storage.ObjectMetadata, error)
Download(ctx context.Context, key string) (io.ReadCloser, error)
}
// PaymentVoucherRecognizer 是付款凭证识别的外部能力接缝,只暴露支付单号。
type PaymentVoucherRecognizer interface {
ExtractPaymentVoucherOrderNumber(ctx context.Context, imageBase64 string) (string, error)
}
// PaymentVoucherRecognitionResult 是识别结果中本系统消费的唯一字段。
type PaymentVoucherRecognitionResult struct {
// ExternalTransactionNo 是识别出的支付单号,仅作交易流水号表单预填值。
ExternalTransactionNo string
}
// PaymentVoucherOCRService 按附件对象键识别付款凭证,只返回交易流水号预填值。
// 识别不创建申请、不写入任何资金事实字段;其余识别字段一律不返回、不落库。
//
// ENG-AUDIT-001 事实决定:识别调用不产生状态变更、不涉及资金与权限,因此
// 不写 Audit Event、Domain Ledger、Integration Log 与 Outbox调用记录由 Access Log 与
// Gateway 客户端的路径级日志承载,识别载荷与原始结果不进入任何一类事实。
type PaymentVoucherOCRService struct {
objects PaymentVoucherObjectStore
recognizer PaymentVoucherRecognizer
}
// NewPaymentVoucherOCRService 创建付款凭证识别用例。
func NewPaymentVoucherOCRService(objects PaymentVoucherObjectStore, recognizer PaymentVoucherRecognizer) *PaymentVoucherOCRService {
return &PaymentVoucherOCRService{objects: objects, recognizer: recognizer}
}
// Recognize 校验附件为图片后调用识别能力,只返回交易流水号预填值。
// 非图片、对象不存在、内容为空或识别失败都返回明确失败,不阻断人工填写。
func (s *PaymentVoucherOCRService) Recognize(ctx context.Context, objectKey string) (*PaymentVoucherRecognitionResult, error) {
if s == nil || s.objects == nil || s.recognizer == nil {
return nil, apperrors.New(apperrors.CodeServiceUnavailable, "付款凭证识别能力未配置")
}
key := strings.TrimSpace(objectKey)
if key == "" || len([]rune(key)) > constants.AgentRechargeVoucherKeyMaxLength {
return nil, apperrors.New(apperrors.CodeInvalidParam, "付款凭证对象键无效")
}
metadata, err := s.objects.Stat(ctx, key)
if err != nil {
return nil, apperrors.Wrap(apperrors.CodeInvalidParam, err, "付款凭证对象不存在或不可读")
}
if metadata == nil || metadata.Size <= 0 {
return nil, apperrors.New(apperrors.CodeInvalidParam, "付款凭证对象内容为空")
}
if metadata.Size > constants.AgentRechargeVoucherMaxBytes {
return nil, apperrors.New(apperrors.CodeInvalidParam, "付款凭证图片超过允许大小")
}
reader, err := s.objects.Download(ctx, key)
if err != nil {
return nil, apperrors.Wrap(apperrors.CodeInvalidParam, err, "读取付款凭证对象失败")
}
defer func() { _ = reader.Close() }()
content, err := io.ReadAll(io.LimitReader(reader, constants.AgentRechargeVoucherMaxBytes+1))
if err != nil {
return nil, apperrors.Wrap(apperrors.CodeInvalidParam, err, "读取付款凭证内容失败")
}
if int64(len(content)) > constants.AgentRechargeVoucherMaxBytes {
return nil, apperrors.New(apperrors.CodeInvalidParam, "付款凭证图片超过允许大小")
}
if len(content) == 0 {
return nil, apperrors.New(apperrors.CodeInvalidParam, "付款凭证对象内容为空")
}
if !isPaymentVoucherImage(metadata.ContentType, content) {
return nil, apperrors.New(apperrors.CodeInvalidParam, "付款凭证必须是图片文件")
}
// base64 编码只存在于本次调用内存中,禁止写入日志、审计或错误信息。
orderNumber, err := s.recognizer.ExtractPaymentVoucherOrderNumber(ctx, base64.StdEncoding.EncodeToString(content))
if err != nil {
return nil, err
}
if strings.TrimSpace(orderNumber) == "" {
return nil, apperrors.New(apperrors.CodeGatewayInvalidResp, "未从付款凭证中识别出交易流水号")
}
return &PaymentVoucherRecognitionResult{ExternalTransactionNo: strings.TrimSpace(orderNumber)}, nil
}
// isPaymentVoucherImage 校验对象声明的类型为图片,并用内容嗅探拦截被改名的非图片文件。
// 嗅探结果为空或 application/octet-stream 表示未知容器(如 webp交由识别服务判定
// 明确识别为其他类型的PDF、压缩包、文本等直接拒绝。
func isPaymentVoucherImage(declaredContentType string, content []byte) bool {
if !strings.HasPrefix(strings.ToLower(strings.TrimSpace(declaredContentType)), "image/") {
return false
}
sniffed := strings.ToLower(strings.TrimSpace(http.DetectContentType(content)))
if sniffed == "" || sniffed == "application/octet-stream" || strings.HasPrefix(sniffed, "image/") {
return true
}
return false
}

View File

@@ -130,7 +130,7 @@ func (s *PaymentMethodService) Update(
}
if referenced > 0 {
return errors.New(errors.CodeEmployeeCollectionPaymentMethodReferenced,
"线下收款方式已被核销申请引用,不能修改稳定编码")
"线下收款方式已被核销申请或代理充值申请引用,不能修改稳定编码")
}
if err := ensurePaymentMethodCodeAvailable(ctx, tx, normalized.Code, id); err != nil {
return err
@@ -276,14 +276,23 @@ func ensurePaymentMethodCodeAvailable(ctx context.Context, tx *gorm.DB, code str
return nil
}
// countPaymentMethodReferences 统计引用该收款方式的核销申请数量。
// countPaymentMethodReferences 统计引用该收款方式的核销申请与代理充值申请数量。
// 两类引用任一存在即禁止物理删除与改码,历史快照由各自记录冻结。
func countPaymentMethodReferences(ctx context.Context, tx *gorm.DB, id uint) (int64, error) {
var count int64
var applicationCount int64
if err := tx.WithContext(ctx).Model(&model.EmployeeCollectionApplication{}).
Where("payment_method_id = ?", id).Count(&count).Error; err != nil {
Where("payment_method_id = ?", id).Count(&applicationCount).Error; err != nil {
return 0, errors.Wrap(errors.CodeDatabaseError, err, "统计线下收款方式引用失败")
}
return count, nil
if applicationCount > 0 {
return applicationCount, nil
}
var rechargeCount int64
if err := tx.WithContext(ctx).Model(&model.AgentRechargeRecord{}).
Where("offline_payment_method_id = ?", id).Count(&rechargeCount).Error; err != nil {
return 0, errors.Wrap(errors.CodeDatabaseError, err, "统计代理充值线下收款方式引用失败")
}
return rechargeCount, nil
}
// mapPaymentMethodCodeConflict 把稳定编码唯一索引冲突映射为稳定业务错误。

View File

@@ -317,6 +317,10 @@ func sceneBusinessFields(businessType string) ([]dto.WeComBusinessFieldResponse,
{Code: constants.ApprovalFieldRemark, Name: "备注", ValueType: constants.ApprovalFieldValueTypeString, Description: "员工提交线下代充值时填写的备注"},
{Code: constants.ApprovalFieldSubmitterID, Name: "提交人账号 ID", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "本系统真实业务提交人账号 ID"},
{Code: constants.ApprovalFieldSubmitterName, Name: "提交人名称", ValueType: constants.ApprovalFieldValueTypeString, Description: "本系统真实业务提交人名称快照"},
{Code: constants.ApprovalFieldOfflinePaymentMethod, Name: "线下收款方式", ValueType: constants.ApprovalFieldValueTypeString, Description: "本次充值使用的线下收款方式名称快照"},
{Code: constants.ApprovalFieldOfflinePaymentMethodCode, Name: "线下收款方式编码", ValueType: constants.ApprovalFieldValueTypeString, Description: "本次充值使用的线下收款方式稳定编码快照"},
{Code: constants.ApprovalFieldExternalTransactionNo, Name: "交易流水号", ValueType: constants.ApprovalFieldValueTypeString, Description: "人工确认的第三方交易流水号,用于审批人核验;与在线渠道交易号无关"},
{Code: constants.ApprovalFieldOtherVoucherKey, Name: "其他凭证", ValueType: constants.ApprovalFieldValueTypeFileList, Description: "提交时上传到企微文件控件的其他凭证列表"},
}, true
case constants.ApprovalBusinessTypeRefund:
return []dto.WeComBusinessFieldResponse{