完善退款审批材料与恢复流程
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m35s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m35s
This commit is contained in:
@@ -3,11 +3,14 @@ package approval
|
||||
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 审批恢复适配器方法见文件末尾。
|
||||
|
||||
// PrepareRequest 是业务写入前执行审批渠道可用性检查的请求。
|
||||
type PrepareRequest struct {
|
||||
BusinessType string
|
||||
@@ -79,3 +82,35 @@ type SubmissionRequestedEvent struct {
|
||||
type SubmissionEventWriter interface {
|
||||
Append(ctx context.Context, tx *gorm.DB, event SubmissionRequestedEvent) error
|
||||
}
|
||||
|
||||
type RecoveryPort interface {
|
||||
EnqueueSubmittedSync(ctx context.Context, tx *gorm.DB, instanceID uint) error
|
||||
EnqueueUnknownConfirm(ctx context.Context, tx *gorm.DB, instanceID uint) error
|
||||
RecoverSubmissionEvent(ctx context.Context, tx *gorm.DB, instanceID uint) (bool, error)
|
||||
}
|
||||
|
||||
// RecoveryAdapter 将恢复动作委托给基础设施实现。
|
||||
type RecoveryAdapter struct {
|
||||
SyncFunc func(context.Context, *gorm.DB, uint) error
|
||||
ConfirmFunc func(context.Context, *gorm.DB, uint) error
|
||||
RecoverFunc func(context.Context, *gorm.DB, uint) (bool, error)
|
||||
}
|
||||
|
||||
func (a RecoveryAdapter) SyncSubmitted(ctx context.Context, tx *gorm.DB, id uint) error {
|
||||
if a.SyncFunc == nil {
|
||||
return stderrors.New("审批原实例同步接缝未配置")
|
||||
}
|
||||
return a.SyncFunc(ctx, tx, id)
|
||||
}
|
||||
func (a RecoveryAdapter) ConfirmUnknown(ctx context.Context, tx *gorm.DB, id uint) error {
|
||||
if a.ConfirmFunc == nil {
|
||||
return stderrors.New("审批结果确认接缝未配置")
|
||||
}
|
||||
return a.ConfirmFunc(ctx, tx, id)
|
||||
}
|
||||
func (a RecoveryAdapter) RecoverSubmissionEvent(ctx context.Context, tx *gorm.DB, id uint) (bool, error) {
|
||||
if a.RecoverFunc == nil {
|
||||
return false, stderrors.New("审批提交事件恢复接缝未配置")
|
||||
}
|
||||
return a.RecoverFunc(ctx, tx, id)
|
||||
}
|
||||
|
||||
@@ -161,8 +161,7 @@ func (s *CreationService) TriggerHistorical(ctx context.Context, refundID uint,
|
||||
if err := tx.WithContext(ctx).Create(attempt).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建退款审批尝试记录失败")
|
||||
}
|
||||
|
||||
submitterSnapshot, requestSnapshot, err := refundSnapshots(¤t, account, "", material)
|
||||
submitterSnapshot, requestSnapshot, err := refundSnapshots(¤t, attempt, account, material)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -226,10 +225,7 @@ func (s *CreationService) Execute(ctx context.Context, command CreateCommand) (*
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
submitterSnapshot, requestSnapshot, err := refundSnapshots(command.Refund, account, command.ApplicantRemark, command.Material)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var submitterSnapshot, requestSnapshot []byte
|
||||
var approvalStatus int
|
||||
var attempt *model.RefundRequestAttempt
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
@@ -255,6 +251,10 @@ func (s *CreationService) Execute(ctx context.Context, command CreateCommand) (*
|
||||
if err := tx.WithContext(ctx).Create(attempt).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建退款审批尝试记录失败")
|
||||
}
|
||||
submitterSnapshot, requestSnapshot, err = refundSnapshots(command.Refund, attempt, account, command.Material)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reference, err := s.approval.CreateInTx(ctx, tx, approvalapp.CreateRequest{
|
||||
Preparation: preparation, BusinessType: constants.ApprovalBusinessTypeRefund,
|
||||
BusinessID: attempt.ID, SubmitterAccountID: command.SubmitterAccountID,
|
||||
@@ -359,7 +359,7 @@ func (s *CreationService) Resubmit(ctx context.Context, refundID uint, command R
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
submitterSnapshot, requestSnapshot, err := refundSnapshots(¤t, account, command.ApplicantRemark, command.Material)
|
||||
submitterSnapshot, requestSnapshot, err := refundSnapshots(¤t, attempt, account, command.Material)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -478,6 +478,7 @@ func buildAttempt(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
customerAccountInfo, customerVoucherKeys := refundCustomerMaterial(refund)
|
||||
return &model.RefundRequestAttempt{
|
||||
RefundID: refund.ID,
|
||||
AttemptNo: attemptNo,
|
||||
@@ -486,14 +487,22 @@ func buildAttempt(
|
||||
FrozenActualReceivedAmount: refund.FrozenActualReceivedAmount,
|
||||
RefundReason: refund.RefundReason,
|
||||
Remark: strings.TrimSpace(applicantRemark),
|
||||
CustomerAccountInfo: refund.CustomerAccountInfo,
|
||||
CustomerVoucherKeys: refund.RefundVoucherKey,
|
||||
CustomerAccountInfo: customerAccountInfo,
|
||||
CustomerVoucherKeys: customerVoucherKeys,
|
||||
PackageUsageSnapshot: snapshot,
|
||||
ChannelRefundRequestNo: strings.TrimSpace(channelRefundRequestNo),
|
||||
SubmittedByAccountID: refund.Creator,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// refundCustomerMaterial 只为客户收款信息退款冻结客户收款材料;其它退款方式保持空材料,避免伪造占位内容。
|
||||
func refundCustomerMaterial(refund *model.RefundRequest) (string, model.StringJSONBArray) {
|
||||
if refund == nil || refund.Method != constants.RefundMethodCustomerAccount {
|
||||
return "", model.StringJSONBArray{}
|
||||
}
|
||||
return strings.TrimSpace(refund.CustomerAccountInfo), append(model.StringJSONBArray(nil), refund.RefundVoucherKey...)
|
||||
}
|
||||
|
||||
// nextAttemptNo 返回该退款申请的下一条审批尝试序号;退款申请行已加锁,序号在同一事务内唯一。
|
||||
func nextAttemptNo(ctx context.Context, tx *gorm.DB, refundID uint) (int, error) {
|
||||
var row struct {
|
||||
@@ -593,36 +602,28 @@ func updateRefundLatest(ctx context.Context, tx *gorm.DB, refund *model.RefundRe
|
||||
return nil
|
||||
}
|
||||
|
||||
// refundSnapshots 冻结提交人与审批业务快照。
|
||||
// 审批业务快照是企微表单构建的数据源,因此本变更新增的审批材料字段必须在此出现,
|
||||
// 且即使不可解析也要以空值或零值写入:控件映射已配置时缺失键会被表单构建明确拒绝。
|
||||
func refundSnapshots(refund *model.RefundRequest, account *model.Account, applicantRemark string, material RefundMaterial) ([]byte, []byte, error) {
|
||||
submitterSnapshot, err := sonic.Marshal(map[string]any{
|
||||
"account_id": account.ID, "account_name": account.Username, "user_type": account.UserType,
|
||||
})
|
||||
// refundSnapshots 冻结提交人与审批业务快照,业务可变字段全部取不可变审批尝试。
|
||||
func refundSnapshots(refund *model.RefundRequest, attempt *model.RefundRequestAttempt, account *model.Account, material RefundMaterial) ([]byte, []byte, error) {
|
||||
if refund == nil || attempt == nil || account == nil {
|
||||
return nil, nil, errors.New(errors.CodeInvalidParam, "退款审批快照参数不完整")
|
||||
}
|
||||
submitterSnapshot, err := sonic.Marshal(map[string]any{"account_id": account.ID, "account_name": account.Username, "user_type": account.UserType})
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(errors.CodeInternalError, err, "编码退款提交人快照失败")
|
||||
}
|
||||
customerAccountInfo, customerVoucherKeys := refundCustomerMaterialFromAttempt(attempt)
|
||||
requestSnapshot, err := sonic.Marshal(map[string]any{
|
||||
constants.ApprovalFieldRefundNo: refund.RefundNo,
|
||||
constants.ApprovalFieldOrderID: refund.OrderID,
|
||||
constants.ApprovalFieldOrderNo: refund.OrderNo,
|
||||
constants.ApprovalFieldAssetIdentifier: refund.AssetIdentifier,
|
||||
constants.ApprovalFieldAssetType: refund.OrderType,
|
||||
constants.ApprovalFieldActualReceivedAmount: formatCentAmount(refund.FrozenActualReceivedAmount),
|
||||
constants.ApprovalFieldRequestedRefundAmount: formatCentAmount(refund.RequestedRefundAmount),
|
||||
constants.ApprovalFieldRefundVoucherKey: []string(refund.RefundVoucherKey),
|
||||
constants.ApprovalFieldRefundReason: refund.RefundReason,
|
||||
constants.ApprovalFieldPackageUsageID: refund.PackageUsageID,
|
||||
constants.ApprovalFieldRefundAssetType: material.AssetType,
|
||||
constants.ApprovalFieldRefundDeviceType: material.DeviceType,
|
||||
constants.ApprovalFieldRefundDeviceModel: material.DeviceModel,
|
||||
constants.ApprovalFieldRefundPackageUsedMB: material.PackageUsedMB,
|
||||
constants.ApprovalFieldRefundPackageTotalMB: material.PackageTotalMB,
|
||||
constants.ApprovalFieldRefundChannelTradeNo: material.OriginalChannelTradeNo,
|
||||
constants.ApprovalFieldRefundApplicantRemark: strings.TrimSpace(applicantRemark),
|
||||
constants.ApprovalFieldSubmitterID: account.ID,
|
||||
constants.ApprovalFieldSubmitterName: account.Username,
|
||||
constants.ApprovalFieldRefundNo: refund.RefundNo, constants.ApprovalFieldOrderID: refund.OrderID, constants.ApprovalFieldOrderNo: refund.OrderNo,
|
||||
constants.ApprovalFieldAssetIdentifier: refund.AssetIdentifier, constants.ApprovalFieldAssetType: refund.OrderType,
|
||||
constants.ApprovalFieldActualReceivedAmount: formatCentAmount(attempt.FrozenActualReceivedAmount), constants.ApprovalFieldRequestedRefundAmount: formatCentAmount(attempt.RefundAmount),
|
||||
constants.ApprovalFieldRefundVoucherKey: []string(attempt.CustomerVoucherKeys), constants.ApprovalFieldRefundReason: attempt.RefundReason,
|
||||
constants.ApprovalFieldPackageUsageID: refund.PackageUsageID, constants.ApprovalFieldRefundAssetType: material.AssetType,
|
||||
constants.ApprovalFieldRefundDeviceType: material.DeviceType, constants.ApprovalFieldRefundDeviceModel: material.DeviceModel,
|
||||
constants.ApprovalFieldRefundPackageUsedMB: material.PackageUsedMB, constants.ApprovalFieldRefundPackageTotalMB: material.PackageTotalMB,
|
||||
constants.ApprovalFieldRefundChannelTradeNo: material.OriginalChannelTradeNo, constants.ApprovalFieldRefundApplicantRemark: attempt.Remark,
|
||||
constants.ApprovalFieldRefundMethodCode: attempt.Method, constants.ApprovalFieldRefundMethod: constants.RefundMethodName(attempt.Method),
|
||||
constants.ApprovalFieldCustomerAccountInfo: customerAccountInfo, constants.ApprovalFieldCustomerVoucherKey: []string(customerVoucherKeys),
|
||||
constants.ApprovalFieldSubmitterID: account.ID, constants.ApprovalFieldSubmitterName: account.Username,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(errors.CodeInternalError, err, "编码退款审批业务快照失败")
|
||||
@@ -630,6 +631,13 @@ func refundSnapshots(refund *model.RefundRequest, account *model.Account, applic
|
||||
return submitterSnapshot, requestSnapshot, nil
|
||||
}
|
||||
|
||||
func refundCustomerMaterialFromAttempt(attempt *model.RefundRequestAttempt) (string, model.StringJSONBArray) {
|
||||
if attempt == nil || attempt.Method != constants.RefundMethodCustomerAccount {
|
||||
return "", model.StringJSONBArray{}
|
||||
}
|
||||
return strings.TrimSpace(attempt.CustomerAccountInfo), append(model.StringJSONBArray(nil), attempt.CustomerVoucherKeys...)
|
||||
}
|
||||
|
||||
func formatCentAmount(amount int64) string {
|
||||
return fmt.Sprintf("%d.%02d", amount/100, amount%100)
|
||||
}
|
||||
|
||||
144
internal/application/refundapproval/recovery.go
Normal file
144
internal/application/refundapproval/recovery.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package refundapproval
|
||||
|
||||
import (
|
||||
"context"
|
||||
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"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"github.com/bytedance/sonic"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type RecoveryAuditWriter interface {
|
||||
WriteSnapshotRecovery(ctx context.Context, tx *gorm.DB, instance *model.ApprovalInstance, fields []string, actorID uint) error
|
||||
WriteRecoveryRequest(ctx context.Context, tx *gorm.DB, instance *model.ApprovalInstance, actorID uint, branch string) error
|
||||
}
|
||||
|
||||
type RecoveryService struct {
|
||||
db *gorm.DB
|
||||
recovery approvalapp.RecoveryPort
|
||||
audit RecoveryAuditWriter
|
||||
}
|
||||
|
||||
func NewRecoveryService(db *gorm.DB, recovery approvalapp.RecoveryPort, audit RecoveryAuditWriter) *RecoveryService {
|
||||
return &RecoveryService{db: db, recovery: recovery, audit: audit}
|
||||
}
|
||||
|
||||
func (s *RecoveryService) Execute(ctx context.Context, refundID uint) error {
|
||||
actorID := middleware.GetUserIDFromContext(ctx)
|
||||
if actorID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized, "未认证的恢复操作")
|
||||
}
|
||||
if s == nil || s.db == nil || s.recovery == nil || refundID == 0 {
|
||||
return errors.New(errors.CodeServiceUnavailable, "退款审批恢复能力未配置")
|
||||
}
|
||||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var refund model.RefundRequest
|
||||
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).First(&refund, refundID).Error; err != nil {
|
||||
return errors.New(errors.CodeNotFound, "退款申请不存在")
|
||||
}
|
||||
if refund.Status != model.RefundStatusPending || refund.LatestAttemptID == 0 || refund.LatestApprovalInstanceID == 0 || refund.ChannelRefundStatus == constants.RefundChannelStatusProcessing || refund.Status == model.RefundStatusChannelFailed {
|
||||
return errors.New(errors.CodeConflict, "当前退款状态不允许恢复审批")
|
||||
}
|
||||
var attempt model.RefundRequestAttempt
|
||||
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ? AND refund_id = ?", refund.LatestAttemptID, refund.ID).First(&attempt).Error; err != nil {
|
||||
return errors.New(errors.CodeConflict, "退款审批尝试关联不一致")
|
||||
}
|
||||
if attempt.ApprovalInstanceID == nil || *attempt.ApprovalInstanceID != refund.LatestApprovalInstanceID {
|
||||
return errors.New(errors.CodeConflict, "退款审批实例关联不一致")
|
||||
}
|
||||
var instance model.ApprovalInstance
|
||||
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ? AND business_type = ? AND business_id = ?", refund.LatestApprovalInstanceID, constants.ApprovalBusinessTypeRefund, attempt.ID).First(&instance).Error; err != nil {
|
||||
return errors.New(errors.CodeConflict, "退款审批实例业务关联不一致")
|
||||
}
|
||||
if instance.Status != constants.ApprovalStatusSubmitting && instance.Status != constants.ApprovalStatusSubmissionFailed && instance.Status != constants.ApprovalStatusSubmissionUnknown && instance.Status != constants.ApprovalStatusPending {
|
||||
return errors.New(errors.CodeConflict, "审批终态不允许恢复")
|
||||
}
|
||||
fields, err := fillMissingSnapshot(&instance, &attempt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(fields) > 0 {
|
||||
if err := tx.Model(&model.ApprovalInstance{}).Where("id = ?", instance.ID).Update("request_snapshot", datatypes.JSON(instance.RequestSnapshot)).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "补齐退款审批快照失败")
|
||||
}
|
||||
if s.audit != nil {
|
||||
if err := s.audit.WriteSnapshotRecovery(ctx, tx, &instance, fields, actorID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
var context model.WeComApprovalContext
|
||||
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("approval_instance_id = ? AND business_type = ?", instance.ID, constants.ApprovalBusinessTypeRefund).First(&context).Error; err != nil {
|
||||
return errors.New(errors.CodeConflict, "企业微信审批上下文不存在或业务类型不一致")
|
||||
}
|
||||
branch := "active"
|
||||
if context.SPNo != "" {
|
||||
branch = "sp_no"
|
||||
if s.audit != nil {
|
||||
if err := s.audit.WriteRecoveryRequest(ctx, tx, &instance, actorID, branch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return s.recovery.EnqueueSubmittedSync(ctx, tx, instance.ID)
|
||||
}
|
||||
if context.SubmissionStatus == constants.WeComSubmissionStatusUnknown || instance.Status == constants.ApprovalStatusSubmissionUnknown {
|
||||
branch = "unknown"
|
||||
if s.audit != nil {
|
||||
if err := s.audit.WriteRecoveryRequest(ctx, tx, &instance, actorID, branch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return s.recovery.EnqueueUnknownConfirm(ctx, tx, instance.ID)
|
||||
}
|
||||
if instance.Status == constants.ApprovalStatusSubmitting || instance.Status == constants.ApprovalStatusPending || context.SubmissionStatus == constants.WeComSubmissionStatusReady || context.SubmissionStatus == constants.WeComSubmissionStatusSending {
|
||||
if s.audit != nil {
|
||||
if err := s.audit.WriteRecoveryRequest(ctx, tx, &instance, actorID, branch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if instance.Status != constants.ApprovalStatusSubmissionFailed || context.SubmissionStatus != constants.WeComSubmissionStatusFailed {
|
||||
return errors.New(errors.CodeConflict, "当前审批提交状态不允许恢复")
|
||||
}
|
||||
branch = "replay"
|
||||
if s.audit != nil {
|
||||
if err := s.audit.WriteRecoveryRequest(ctx, tx, &instance, actorID, branch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = s.recovery.RecoverSubmissionEvent(ctx, tx, instance.ID)
|
||||
return err
|
||||
})
|
||||
}
|
||||
func fillMissingSnapshot(instance *model.ApprovalInstance, attempt *model.RefundRequestAttempt) ([]string, error) {
|
||||
var snapshot map[string]any
|
||||
if err := sonic.Unmarshal(instance.RequestSnapshot, &snapshot); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInvalidStatus, err, "退款审批快照无效")
|
||||
}
|
||||
if snapshot == nil {
|
||||
snapshot = map[string]any{}
|
||||
}
|
||||
values := map[string]any{constants.ApprovalFieldRefundMethodCode: attempt.Method, constants.ApprovalFieldRefundMethod: constants.RefundMethodName(attempt.Method), constants.ApprovalFieldCustomerAccountInfo: attempt.CustomerAccountInfo, constants.ApprovalFieldCustomerVoucherKey: []string(attempt.CustomerVoucherKeys)}
|
||||
fields := make([]string, 0, len(values))
|
||||
for key, value := range values {
|
||||
if _, ok := snapshot[key]; ok {
|
||||
continue
|
||||
}
|
||||
snapshot[key] = value
|
||||
fields = append(fields, key)
|
||||
}
|
||||
if len(fields) > 0 {
|
||||
encoded, err := sonic.Marshal(snapshot)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "编码退款审批快照失败")
|
||||
}
|
||||
instance.RequestSnapshot = encoded
|
||||
}
|
||||
return fields, nil
|
||||
}
|
||||
@@ -132,6 +132,10 @@ func (s *SceneService) Save(ctx context.Context, businessType string, request dt
|
||||
s.recordFailure(ctx, businessType, request, constants.AuditResultDenied, errors.CodeInvalidParam, "拒绝保存非法企业微信审批场景映射")
|
||||
return nil, err
|
||||
}
|
||||
if err := validateRefundConditionalMappings(businessType, request.ControlMapping, definition.Controls); err != nil {
|
||||
s.recordFailure(ctx, businessType, request, constants.AuditResultDenied, errors.CodeInvalidParam, "拒绝保存非法退款条件材料控件映射")
|
||||
return nil, err
|
||||
}
|
||||
request.ControlMapping = normalizeSceneMapping(request.ControlMapping)
|
||||
mappingJSON, err := sonic.Marshal(request.ControlMapping)
|
||||
if err != nil {
|
||||
@@ -305,19 +309,14 @@ func validateSceneMapping(businessType string, mapping []dto.WeComControlMapping
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequiredSceneField 描述本变更新增、必须配置控件映射的业务字段。
|
||||
// RequiredSceneField 描述审批字段映射的固定或条件必需规则。
|
||||
type RequiredSceneField struct {
|
||||
// Code 是 control_mapping.business_field 应填写的稳定字段编码。
|
||||
Code string
|
||||
// Name 是字段中文名称,用于在缺失时指出具体缺哪个字段/控件。
|
||||
Name string
|
||||
Code string
|
||||
Name string
|
||||
RequiredMapping bool
|
||||
}
|
||||
|
||||
// RequiredSceneFields 返回指定业务类型下必须配置控件映射的业务字段,顺序与场景白名单一致。
|
||||
//
|
||||
// 这是「必须映射字段」的唯一判定来源:场景映射校验(保存时)与表单构建(提交时)都从
|
||||
// 这里取集合,MUST NOT 再维护第二份清单。既有未标注必须映射的可选字段不在返回结果中,
|
||||
// 因此其缺失映射仍保持既有静默跳过行为。
|
||||
// RequiredSceneFields 返回场景级固定必需字段。
|
||||
func RequiredSceneFields(businessType string) []RequiredSceneField {
|
||||
fields, ok := sceneBusinessFields(businessType)
|
||||
if !ok {
|
||||
@@ -326,16 +325,31 @@ func RequiredSceneFields(businessType string) []RequiredSceneField {
|
||||
result := make([]RequiredSceneField, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
if field.RequiredMapping {
|
||||
result = append(result, RequiredSceneField{Code: field.Code, Name: field.Name})
|
||||
result = append(result, RequiredSceneField{Code: field.Code, Name: field.Name, RequiredMapping: true})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// RequiredRefundFields 返回退款提交固定与条件必需字段;快照方式编码缺失或未知时由表单构建拒绝。
|
||||
func RequiredRefundFields(snapshot map[string]any) []RequiredSceneField {
|
||||
methodCode, ok := snapshot[constants.ApprovalFieldRefundMethodCode].(string)
|
||||
methodCode = strings.TrimSpace(methodCode)
|
||||
if !ok || methodCode == "" || (methodCode != constants.RefundMethodOriginalRoute && methodCode != constants.RefundMethodCustomerAccount && methodCode != constants.RefundMethodAssetWallet && methodCode != constants.RefundMethodAgentWallet) {
|
||||
return []RequiredSceneField{{Code: constants.ApprovalFieldRefundMethodCode, Name: "退款方式编码"}}
|
||||
}
|
||||
result := []RequiredSceneField{{Code: constants.ApprovalFieldRefundMethod, Name: "退款方式", RequiredMapping: true}}
|
||||
if methodCode == constants.RefundMethodCustomerAccount {
|
||||
result = append(result, RequiredSceneField{Code: constants.ApprovalFieldCustomerAccountInfo, Name: "客户收款信息"}, RequiredSceneField{Code: constants.ApprovalFieldCustomerVoucherKey, Name: "客户收款凭证"})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// approvalBusinessTypes 返回全部已登记审批业务场景,供只读字段汇总复用。
|
||||
func approvalBusinessTypes() []string {
|
||||
return []string{
|
||||
constants.ApprovalBusinessTypeRefund,
|
||||
|
||||
constants.ApprovalBusinessTypeOfflineRecharge,
|
||||
constants.ApprovalBusinessTypeEmployeeCollection,
|
||||
constants.ApprovalBusinessTypeAgentDistribution,
|
||||
@@ -344,6 +358,30 @@ func approvalBusinessTypes() []string {
|
||||
}
|
||||
}
|
||||
|
||||
// validateRefundConditionalMappings 校验退款条件字段使用正确的企微控件类型。
|
||||
func validateRefundConditionalMappings(businessType string, mapping []dto.WeComControlMappingItem, controls []TemplateControl) error {
|
||||
if businessType != constants.ApprovalBusinessTypeRefund {
|
||||
return nil
|
||||
}
|
||||
types := make(map[string]string, len(controls))
|
||||
for _, control := range controls {
|
||||
types[control.ID] = strings.ToLower(strings.TrimSpace(control.Type))
|
||||
}
|
||||
for _, item := range mapping {
|
||||
switch item.BusinessField {
|
||||
case constants.ApprovalFieldCustomerAccountInfo:
|
||||
if types[item.ControlID] != "textarea" {
|
||||
return errors.New(errors.CodeInvalidParam, "客户收款信息必须映射到多行文本控件")
|
||||
}
|
||||
case constants.ApprovalFieldCustomerVoucherKey:
|
||||
if types[item.ControlID] != "file" {
|
||||
return errors.New(errors.CodeInvalidParam, "客户收款凭证必须映射到附件控件")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func allowedSceneBusinessField(businessType, businessField string) bool {
|
||||
fields, ok := sceneBusinessFields(businessType)
|
||||
if !ok {
|
||||
@@ -394,6 +432,10 @@ func sceneBusinessFields(businessType string) ([]dto.WeComBusinessFieldResponse,
|
||||
{Code: constants.ApprovalFieldRefundPackageTotalMB, Name: "当前退款套餐总量(MB)", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "当前退款套餐总量(MB,真实流量);解析不到套餐时为 0", RequiredMapping: true},
|
||||
{Code: constants.ApprovalFieldRefundChannelTradeNo, Name: "原支付渠道交易流水号", ValueType: constants.ApprovalFieldValueTypeString, Description: "创建申请时冻结的原成功支付记录渠道交易流水号;线下订单为空字符串", RequiredMapping: true},
|
||||
{Code: constants.ApprovalFieldRefundApplicantRemark, Name: "申请人备注", ValueType: constants.ApprovalFieldValueTypeString, Description: "本次提交填写的申请人备注快照;未填写时为空字符串。与审批备注不是同一字段", RequiredMapping: true},
|
||||
{Code: constants.ApprovalFieldRefundMethodCode, Name: "退款方式编码", ValueType: constants.ApprovalFieldValueTypeString, Description: "仅用于按本次审批快照计算条件材料,不要求配置模板控件"},
|
||||
{Code: constants.ApprovalFieldRefundMethod, Name: "退款方式", ValueType: constants.ApprovalFieldValueTypeString, Description: "本次审批尝试冻结的退款方式中文名称", RequiredMapping: true},
|
||||
{Code: constants.ApprovalFieldCustomerAccountInfo, Name: "客户收款信息", ValueType: constants.ApprovalFieldValueTypeString, Description: "客户收款信息退款的多行收款文本;其它退款方式为空"},
|
||||
{Code: constants.ApprovalFieldCustomerVoucherKey, Name: "客户收款凭证", ValueType: constants.ApprovalFieldValueTypeFileList, Description: "客户收款信息退款的凭证列表;其它退款方式为空"},
|
||||
{Code: constants.ApprovalFieldSubmitterID, Name: "提交人账号 ID", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "本系统真实业务提交人账号 ID"},
|
||||
{Code: constants.ApprovalFieldSubmitterName, Name: "提交人名称", ValueType: constants.ApprovalFieldValueTypeString, Description: "本系统真实业务提交人名称快照"},
|
||||
}, true
|
||||
|
||||
Reference in New Issue
Block a user