feat(代理自充): AUG26-017 代理自充收款方式与线下预存款审批字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m52s
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:
@@ -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, "编码线下代充值审批业务快照失败")
|
||||
|
||||
Reference in New Issue
Block a user