feat(收口): 补齐 8 月迭代缺口并同步 Spec 与证据链
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
- 新增六对成对迁移 000232–000237:H5 弹窗类型、退款结算标识与申请人备注、优先轮询事实字段与两个新终态、通道阈值命中留痕、手机号最近解绑人、提现资格校验留痕 - 退款:原因必填与申请人备注、来源支付与渠道流水冻结、线下处理流水号补录审计、按订单查询可选退款方式、企微审批材料补齐且新增字段缺失映射即明确失败 - 优先轮询:人工关闭、有效期到期独立周期任务、失败与过期人工重触发、事实字段与异常重试查询、资产解析端点只读投影 - 通道阈值:命中事实同事务留痕与命中记录查询;员工账单:列表筛选与详情投影;商户池:列表投影与统计周期语义;H5:弹窗类型与类别排序 - 手机号:有效关联数量与最近解绑人、短信验证码失败次数限制;导出:佣金明细十五列与报表序号列 - 时间筛选:三处新增筛选纳入统一严格解析契约,员工账单产生时间参数改名 - 同步 12 份主 Spec 需求、两端点与异步任务证据链,门禁 context-health 与 OpenSpec 校验通过
This commit is contained in:
@@ -19,16 +19,37 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// RefundMaterial 承载本次提交由调用方按既有展示口径解析出的审批材料事实。
|
||||
//
|
||||
// 材料由服务层解析后传入,使审批材料与退款展示字段共用同一份解析规则(ENG-ARCH-002:
|
||||
// 读取口径不复制);材料不可解析时以空值或零值传入,MUST NOT 阻断审批提交。
|
||||
type RefundMaterial struct {
|
||||
// AssetType 是退款关联资产类型:card 单卡、device 设备,沿用退款申请的下单快照推导。
|
||||
AssetType string
|
||||
// DeviceType 与 DeviceModel 取退款申请关联设备,无关联设备时为空串。
|
||||
DeviceType string
|
||||
DeviceModel string
|
||||
// PackageUsedMB 与 PackageTotalMB 是当前退款套餐的已用量与总量(MB,真实流量),解析不到时为 0。
|
||||
PackageUsedMB int64
|
||||
PackageTotalMB int64
|
||||
// OriginalChannelTradeNo 是创建申请时冻结的原支付渠道交易流水号,线下订单为空串。
|
||||
OriginalChannelTradeNo string
|
||||
}
|
||||
|
||||
// CreateCommand 描述已通过订单与金额校验的退款审批申请。
|
||||
//
|
||||
// 本次提交的审批尝试记录由本用例在退款申请落库后于同一事务内构造,调用方只提供
|
||||
// 需要按冻结商户派生、应用层无法自行生成的渠道退款请求号。
|
||||
// 需要按冻结商户派生、应用层无法自行生成的渠道退款请求号,以及本次提交的申请人备注与审批材料。
|
||||
type CreateCommand struct {
|
||||
Refund *model.RefundRequest
|
||||
Order *model.Order
|
||||
SubmitterAccountID uint
|
||||
// ChannelRefundRequestNo 是原路退款本次尝试冻结的渠道退款请求号;非原路方式为空。
|
||||
ChannelRefundRequestNo string
|
||||
// ApplicantRemark 是本次提交的申请人备注,冻结进当次审批尝试快照;不写入退款主表审批备注。
|
||||
ApplicantRemark string
|
||||
// Material 是本次提交的审批材料事实,冻结进当次尝试快照与审批业务快照。
|
||||
Material RefundMaterial
|
||||
}
|
||||
|
||||
// ApplicationAudit 描述退款申请、审批、订单和提交人的同事务审计事实。
|
||||
@@ -76,7 +97,10 @@ func NewCreationService(db *gorm.DB, approval approvalapp.Port, audit AuditWrite
|
||||
|
||||
// TriggerHistorical 为历史待审批退款补发一次企业微信审批。
|
||||
// 历史申请尚未接入尝试模式,因此本次补发同时建立首条尝试记录并把业务标识切换到该记录。
|
||||
func (s *CreationService) TriggerHistorical(ctx context.Context, refundID uint) (*CreateResult, error) {
|
||||
//
|
||||
// 补发不为历史申请追溯退款原因必填(该校验只作用于创建与重提用例),但材料仍按既有展示口径
|
||||
// 解析后传入,使新增控件的审批材料在补发路径同样完整,避免模板缺字段导致提交失败。
|
||||
func (s *CreationService) TriggerHistorical(ctx context.Context, refundID uint, material RefundMaterial) (*CreateResult, error) {
|
||||
if s == nil || s.db == nil || s.approval == nil || s.audit == nil || refundID == 0 {
|
||||
return nil, errors.New(errors.CodeServiceUnavailable, "退款审批能力未配置")
|
||||
}
|
||||
@@ -129,7 +153,7 @@ func (s *CreationService) TriggerHistorical(ctx context.Context, refundID uint)
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询退款关联订单失败")
|
||||
}
|
||||
|
||||
attempt, err := buildAttempt(ctx, tx, ¤t, ¤tOrder, "")
|
||||
attempt, err := buildAttempt(ctx, tx, ¤t, ¤tOrder, "", "", material)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -138,7 +162,7 @@ func (s *CreationService) TriggerHistorical(ctx context.Context, refundID uint)
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建退款审批尝试记录失败")
|
||||
}
|
||||
|
||||
submitterSnapshot, requestSnapshot, err := refundSnapshots(¤t, account)
|
||||
submitterSnapshot, requestSnapshot, err := refundSnapshots(¤t, account, "", material)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -202,7 +226,7 @@ func (s *CreationService) Execute(ctx context.Context, command CreateCommand) (*
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
submitterSnapshot, requestSnapshot, err := refundSnapshots(command.Refund, account)
|
||||
submitterSnapshot, requestSnapshot, err := refundSnapshots(command.Refund, account, command.ApplicantRemark, command.Material)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -224,7 +248,7 @@ func (s *CreationService) Execute(ctx context.Context, command CreateCommand) (*
|
||||
if err := tx.WithContext(ctx).Create(command.Refund).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建退款申请失败")
|
||||
}
|
||||
attempt, err = buildAttempt(ctx, tx, command.Refund, command.Order, command.ChannelRefundRequestNo)
|
||||
attempt, err = buildAttempt(ctx, tx, command.Refund, command.Order, command.ChannelRefundRequestNo, command.ApplicantRemark, command.Material)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -271,6 +295,10 @@ type ResubmitCommand struct {
|
||||
Refund *model.RefundRequest
|
||||
// ChannelRefundRequestNo 是原路退款本次重提冻结的渠道退款请求号;非原路方式为空。
|
||||
ChannelRefundRequestNo string
|
||||
// ApplicantRemark 是本次重提的申请人备注,冻结进新的审批尝试快照;历史尝试备注不被改写。
|
||||
ApplicantRemark string
|
||||
// Material 是本次重提的审批材料事实,冻结进新的尝试快照与审批业务快照。
|
||||
Material RefundMaterial
|
||||
}
|
||||
|
||||
// Resubmit 修改并重提未成功退款申请,新增审批尝试记录与新的企业微信审批实例。
|
||||
@@ -318,7 +346,7 @@ func (s *CreationService) Resubmit(ctx context.Context, refundID uint, command R
|
||||
current.RefundVoucherKey = command.Refund.RefundVoucherKey
|
||||
current.CustomerAccountInfo = command.Refund.CustomerAccountInfo
|
||||
|
||||
attempt, err := buildAttempt(ctx, tx, ¤t, &order, command.ChannelRefundRequestNo)
|
||||
attempt, err := buildAttempt(ctx, tx, ¤t, &order, command.ChannelRefundRequestNo, command.ApplicantRemark, command.Material)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -331,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)
|
||||
submitterSnapshot, requestSnapshot, err := refundSnapshots(¤t, account, command.ApplicantRemark, command.Material)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -345,6 +373,8 @@ func (s *CreationService) Resubmit(ctx context.Context, refundID uint, command R
|
||||
"refund_reason": current.RefundReason,
|
||||
"refund_voucher_key": current.RefundVoucherKey,
|
||||
"customer_account_info": current.CustomerAccountInfo,
|
||||
"source_payment_no": current.SourcePaymentNo,
|
||||
"original_channel_trade_no": current.OriginalChannelTradeNo,
|
||||
"failure_reason": "",
|
||||
"failure_message": "",
|
||||
"channel_refund_status": constants.RefundChannelStatusNone,
|
||||
@@ -428,15 +458,23 @@ func (s *CreationService) loadSubmitter(ctx context.Context, accountID uint) (*m
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
// buildAttempt 构造一条不可变审批尝试记录,冻结当次方式、金额、冻结实收、原因、客户收款信息与套餐使用快照。
|
||||
// buildAttempt 构造一条不可变审批尝试记录,冻结当次方式、金额、冻结实收、原因、申请人备注、客户收款信息与套餐使用快照。
|
||||
// attempt_no 在退款申请行已加锁的前提下于同一事务内递增,因此申请内唯一。
|
||||
// package_usage_snapshot 必须是非空 JSON 对象,因此快照只能在这里按订单事实生成,不能由调用方预置。
|
||||
func buildAttempt(ctx context.Context, tx *gorm.DB, refund *model.RefundRequest, order *model.Order, channelRefundRequestNo string) (*model.RefundRequestAttempt, error) {
|
||||
func buildAttempt(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
refund *model.RefundRequest,
|
||||
order *model.Order,
|
||||
channelRefundRequestNo string,
|
||||
applicantRemark string,
|
||||
material RefundMaterial,
|
||||
) (*model.RefundRequestAttempt, error) {
|
||||
attemptNo, err := nextAttemptNo(ctx, tx, refund.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
snapshot, err := packageUsageSnapshot(ctx, tx, refund, order)
|
||||
snapshot, err := packageUsageSnapshot(ctx, tx, refund, order, material)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -447,6 +485,7 @@ func buildAttempt(ctx context.Context, tx *gorm.DB, refund *model.RefundRequest,
|
||||
RefundAmount: refund.RequestedRefundAmount,
|
||||
FrozenActualReceivedAmount: refund.FrozenActualReceivedAmount,
|
||||
RefundReason: refund.RefundReason,
|
||||
Remark: strings.TrimSpace(applicantRemark),
|
||||
CustomerAccountInfo: refund.CustomerAccountInfo,
|
||||
CustomerVoucherKeys: refund.RefundVoucherKey,
|
||||
PackageUsageSnapshot: snapshot,
|
||||
@@ -468,12 +507,21 @@ func nextAttemptNo(ctx context.Context, tx *gorm.DB, refundID uint) (int, error)
|
||||
return row.MaxAttemptNo + 1, nil
|
||||
}
|
||||
|
||||
// packageUsageSnapshot 冻结本次申请关联的套餐使用情况,作为企业微信审批判断材料。
|
||||
// packageUsageSnapshot 冻结本次申请关联的套餐使用情况与本次提交的审批材料,作为企业微信审批判断材料。
|
||||
// 本期退款不按套餐已用流量计算金额,因此该快照只作审批与追溯材料,不参与金额校验。
|
||||
func packageUsageSnapshot(ctx context.Context, tx *gorm.DB, refund *model.RefundRequest, order *model.Order) (datatypes.JSON, error) {
|
||||
//
|
||||
// 用量、设备维度与原渠道交易流水号由调用方按既有展示口径解析后传入(material):材料不可解析时
|
||||
// 以空值或零值写入,使快照键始终存在而 MUST NOT 阻断审批提交。
|
||||
func packageUsageSnapshot(ctx context.Context, tx *gorm.DB, refund *model.RefundRequest, order *model.Order, material RefundMaterial) (datatypes.JSON, error) {
|
||||
snapshot := map[string]any{
|
||||
"order_type": order.OrderType,
|
||||
"asset_identifier": order.AssetIdentifier,
|
||||
"asset_type": material.AssetType,
|
||||
"device_type": material.DeviceType,
|
||||
"device_model": material.DeviceModel,
|
||||
"package_used_mb": material.PackageUsedMB,
|
||||
"package_total_mb": material.PackageTotalMB,
|
||||
"channel_trade_no": material.OriginalChannelTradeNo,
|
||||
}
|
||||
if refund.PackageUsageID != nil && *refund.PackageUsageID > 0 {
|
||||
var usage model.PackageUsage
|
||||
@@ -545,7 +593,10 @@ func updateRefundLatest(ctx context.Context, tx *gorm.DB, refund *model.RefundRe
|
||||
return nil
|
||||
}
|
||||
|
||||
func refundSnapshots(refund *model.RefundRequest, account *model.Account) ([]byte, []byte, error) {
|
||||
// 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,
|
||||
})
|
||||
@@ -563,6 +614,13 @@ func refundSnapshots(refund *model.RefundRequest, account *model.Account) ([]byt
|
||||
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,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user