feat(退款): AUG26-006 退款方式选择与原路退款
按 PRD 2.3/2.4/2.5 落地套餐退款的方式矩阵与原路渠道退款: - 退款申请派生并冻结权威实收金额(线上取原成功支付记录,钱包/线下取订单实际收款), 提交人不可填写或修改;按来源支付方式生成可选方式矩阵并在创建、提交、执行前重复校验。 - 审批切换为「每次提交一条不可变审批尝试记录 + 独立企业微信审批实例」,业务标识取尝试 记录主键;终态消费按尝试记录优先、退款申请兜底双读,兼容存量无实例与已关联实例申请。 新增活动退款部分唯一索引 (order_id) WHERE status IN (1,5,6)。 - 本地人工终审保持既有开关,补齐通过入口的 approval_instance_id IS NULL 守卫,使三个 入口一致拒绝已关联审批实例的申请;重提按尝试模式重写(仅已拒绝/已退回/原路失败且无异常)。 - 权益时点:企微通过事务写退款终态、按方式确定的订单态、钱包回款、员工账单冲销与可靠 失效事实;套餐失效/接续/停机仍由既有可靠机制最终一致执行,不把外部调用放入资金事务。 订单支付状态按方式置位:凭证退款与退回原钱包在企微通过时置已退款,原路须渠道明确成功。 - 按官方契约实现微信直连 v3、微信 v2(双向证书)、富友(/commonRefund 与 /refundQuery)、 支付宝四类原路退款;能力只由服务商类型与退款必需凭证完整性决定,无人工开关。 渠道请求号在提交时冻结到尝试记录,并以 channel_submitted_at 条件认领保证资金动作至多 提交一次(重复投递只查询不二次提交);不向任何渠道传递退款结果通知地址。 - 新增 refund:channel:recovery 恢复任务只查询回填;本地查询窗口超期(富友 72 小时、 微信 v2 7 天)转原路退款失败、渠道状态已失败、分类超时未知并置异常转人工,不放行自动 重提以避免重复退款。 - 同步退款 DTO/导出/审计资源与审计查询关联、商户凭证文档,并修正 fuiou 集成契约文档。 迁移 000218(退款尝试与渠道退款事实)、000219(微信 v2 客户端证书凭证)成对提供, 未修改既有迁移;测试库 junhong_cmp_test 完成 up/down/up 与行为核对,未调用真实渠道。
This commit is contained in:
@@ -3,6 +3,7 @@ package model
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -42,6 +43,30 @@ type RefundRequest struct {
|
||||
RejectReason string `gorm:"column:reject_reason;type:text;comment:拒绝原因" json:"reject_reason,omitempty"`
|
||||
Remark string `gorm:"column:remark;type:text;comment:审批备注" json:"remark,omitempty"`
|
||||
|
||||
// 退款方式、冻结实收与当前材料快照
|
||||
Method string `gorm:"column:method;type:varchar(20);not null;default:'';comment:退款方式,空表示未接入方式的存量申请" json:"method"`
|
||||
FrozenActualReceivedAmount int64 `gorm:"column:frozen_actual_received_amount;type:bigint;not null;default:0;comment:冻结的权威实收金额(分)" json:"frozen_actual_received_amount"`
|
||||
CustomerAccountInfo string `gorm:"column:customer_account_info;type:text;not null;default:'';comment:客户收款信息自由文本快照" json:"customer_account_info"`
|
||||
|
||||
// 审批尝试引用,仅用于列表与详情展示
|
||||
LatestAttemptID uint `gorm:"column:latest_attempt_id;type:bigint;not null;default:0;comment:最新审批尝试记录ID,仅用于展示" json:"latest_attempt_id"`
|
||||
LatestApprovalInstanceID uint `gorm:"column:latest_approval_instance_id;type:bigint;not null;default:0;comment:最新通用审批实例ID,仅用于展示" json:"latest_approval_instance_id"`
|
||||
|
||||
// 渠道原路退款结果与结构化失败分类
|
||||
ChannelRefundStatus int `gorm:"column:channel_refund_status;type:smallint;not null;default:0;comment:渠道退款状态 0-未发起或不适用 1-处理中 2-明确成功 3-明确失败" json:"channel_refund_status"`
|
||||
ChannelRefundNo string `gorm:"column:channel_refund_no;type:varchar(64);not null;default:'';comment:渠道退款流水号" json:"channel_refund_no"`
|
||||
ChannelRefundRequestNo string `gorm:"column:channel_refund_request_no;type:varchar(64);not null;default:'';comment:渠道退款请求号快照" json:"channel_refund_request_no"`
|
||||
ChannelRefundAmount int64 `gorm:"column:channel_refund_amount;type:bigint;not null;default:0;comment:渠道退款金额快照(分)" json:"channel_refund_amount"`
|
||||
ChannelRefundedAt *time.Time `gorm:"column:channel_refunded_at;comment:渠道明确退款成功时间" json:"channel_refunded_at,omitempty"`
|
||||
// ChannelSubmittedAt 非空表示该尝试已向渠道提交过退款请求;重投只允许查询,不得再次提交。
|
||||
ChannelSubmittedAt *time.Time `gorm:"column:channel_submitted_at;comment:渠道退款请求提交认领时间" json:"channel_submitted_at,omitempty"`
|
||||
FailureReason string `gorm:"column:failure_reason;type:varchar(32);not null;default:'';comment:结构化失败分类稳定编码" json:"failure_reason"`
|
||||
FailureMessage string `gorm:"column:failure_message;type:varchar(500);not null;default:'';comment:失败安全摘要" json:"failure_message"`
|
||||
|
||||
// 正交异常标记:企业微信通过后撤销、渠道结果永久未知等情况转人工处理
|
||||
AnomalyFlag int `gorm:"column:anomaly_flag;type:smallint;not null;default:0;comment:异常标记 0-无异常 1-有异常" json:"anomaly_flag"`
|
||||
AnomalyReason string `gorm:"column:anomaly_reason;type:varchar(500);not null;default:'';comment:异常原因" json:"anomaly_reason"`
|
||||
|
||||
// 后处理标记
|
||||
CommissionDeducted bool `gorm:"column:commission_deducted;not null;default:false;comment:佣金是否已回扣" json:"commission_deducted"`
|
||||
AssetReset bool `gorm:"column:asset_reset;not null;default:false;comment:退款后资产处理是否完成" json:"asset_reset"`
|
||||
@@ -52,10 +77,51 @@ func (RefundRequest) TableName() string {
|
||||
return "tb_refund_request"
|
||||
}
|
||||
|
||||
// RefundRequestAttempt 退款审批尝试记录。
|
||||
// 每次提交或重提新增一条不可变记录,冻结当次方式、金额、冻结实收、原因、客户收款信息、
|
||||
// 凭证与套餐使用快照;主键同时作为通用审批业务标识,使同一退款单每次提交持有独立审批实例。
|
||||
type RefundRequestAttempt struct {
|
||||
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
RefundID uint `gorm:"column:refund_id;not null;index" json:"refund_id"`
|
||||
AttemptNo int `gorm:"column:attempt_no;type:int;not null" json:"attempt_no"`
|
||||
Method string `gorm:"column:method;type:varchar(20);not null" json:"method"`
|
||||
RefundAmount int64 `gorm:"column:refund_amount;type:bigint;not null" json:"refund_amount"`
|
||||
FrozenActualReceivedAmount int64 `gorm:"column:frozen_actual_received_amount;type:bigint;not null" json:"frozen_actual_received_amount"`
|
||||
RefundReason string `gorm:"column:refund_reason;type:text;not null;default:''" json:"refund_reason"`
|
||||
CustomerAccountInfo string `gorm:"column:customer_account_info;type:text;not null;default:''" json:"customer_account_info"`
|
||||
CustomerVoucherKeys StringJSONBArray `gorm:"column:customer_voucher_keys;type:jsonb;not null" json:"customer_voucher_keys"`
|
||||
PackageUsageSnapshot datatypes.JSON `gorm:"column:package_usage_snapshot;type:jsonb;not null" json:"package_usage_snapshot"`
|
||||
ChannelRefundRequestNo string `gorm:"column:channel_refund_request_no;type:varchar(64);not null;default:''" json:"channel_refund_request_no"`
|
||||
SubmittedByAccountID uint `gorm:"column:submitted_by_account_id;not null" json:"submitted_by_account_id"`
|
||||
ApprovalInstanceID *uint `gorm:"column:approval_instance_id" json:"approval_instance_id,omitempty"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null;autoCreateTime" json:"created_at"`
|
||||
}
|
||||
|
||||
// TableName 指定审批尝试记录表名。
|
||||
func (RefundRequestAttempt) TableName() string {
|
||||
return "tb_refund_request_attempt"
|
||||
}
|
||||
|
||||
// 退款状态常量
|
||||
const (
|
||||
RefundStatusPending = 1 // 待审批
|
||||
RefundStatusApproved = 2 // 已通过
|
||||
RefundStatusApproved = 2 // 已通过(退款已完成)
|
||||
RefundStatusRejected = 3 // 已拒绝
|
||||
RefundStatusReturned = 4 // 已退回
|
||||
|
||||
// RefundStatusChannelProcessing 表示企业微信已通过、原路渠道结果尚未确认。
|
||||
RefundStatusChannelProcessing = 5
|
||||
// RefundStatusChannelFailed 表示原路渠道明确失败或超时可恢复失败。
|
||||
RefundStatusChannelFailed = 6
|
||||
)
|
||||
|
||||
// RefundActiveStatuses 返回会阻止同一订单创建新退款申请的状态集合。
|
||||
// 该集合必须与迁移中 uk_refund_request_active_order 的谓词保持一致。
|
||||
func RefundActiveStatuses() []int {
|
||||
return []int{RefundStatusPending, RefundStatusChannelProcessing, RefundStatusChannelFailed}
|
||||
}
|
||||
|
||||
// RefundResubmittableStatuses 返回允许修改材料并重提的状态集合。
|
||||
func RefundResubmittableStatuses() []int {
|
||||
return []int{RefundStatusRejected, RefundStatusReturned, RefundStatusChannelFailed}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user