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:
2026-09-14 11:55:16 +08:00
parent 48c85a4916
commit ba0855d9eb
51 changed files with 5995 additions and 986 deletions

View File

@@ -3,9 +3,11 @@ package dto
// CreateRefundRequest 创建退款申请请求
type CreateRefundRequest struct {
OrderID uint `json:"order_id" validate:"required" required:"true" description:"关联订单ID"`
ActualReceivedAmount int64 `json:"actual_received_amount" validate:"required,min=1" required:"true" minimum:"1" description:"实收金额(分)"`
ActualReceivedAmount *int64 `json:"actual_received_amount" validate:"omitempty" description:"已废弃:实收金额由系统从原成功支付记录或订单实际收款派生并冻结,提交人填写无效"`
Method string `json:"method" validate:"required,oneof=original_route customer_account asset_wallet agent_wallet" required:"true" description:"退款方式 (original_route:原路退款, customer_account:客户收款信息退款, asset_wallet:退回资产钱包, agent_wallet:退回代理主钱包)"`
CustomerAccountInfo string `json:"customer_account_info" validate:"omitempty,max=1000" maxLength:"1000" description:"客户收款信息,仅客户收款信息退款方式必填,不得复用公司线下收款方式字典"`
RequestedRefundAmount int64 `json:"requested_refund_amount" validate:"required,min=1" required:"true" minimum:"1" description:"申请退款金额(分)"`
RefundVoucherKey []string `json:"refund_voucher_key" validate:"required,min=1,max=5,dive,max=500" required:"true" minItems:"1" maxItems:"5" description:"退款凭证对象存储file_key列表至少1个最多5个通过/storage/upload-url上传图片后获得"`
RefundVoucherKey []string `json:"refund_voucher_key" validate:"omitempty,max=5,dive,max=500" maxItems:"5" description:"退款凭证对象存储file_key列表最多5个,仅客户收款信息退款方式必填,通过/storage/upload-url上传图片后获得"`
RefundReason string `json:"refund_reason" validate:"omitempty,max=1000" maxLength:"1000" description:"退款原因"`
PackageUsageID *uint `json:"package_usage_id" validate:"omitempty" description:"关联套餐使用记录ID可选"`
}
@@ -25,9 +27,11 @@ type RejectRefundRequest struct {
// 退款单被退回后,可修改部分字段后重新提交
type ResubmitRefundRequest struct {
ID uint `json:"-" params:"id" path:"id" validate:"required" description:"退款申请ID"`
ActualReceivedAmount *int64 `json:"actual_received_amount" validate:"omitempty,min=1" minimum:"1" description:"实收金额(分)"`
ActualReceivedAmount *int64 `json:"actual_received_amount" validate:"omitempty,min=1" minimum:"1" description:"已废弃:实收金额由系统从原成功支付记录或订单实际收款派生并冻结,提交人填写无效"`
Method *string `json:"method" validate:"omitempty,oneof=original_route customer_account asset_wallet agent_wallet" description:"退款方式 (original_route:原路退款, customer_account:客户收款信息退款, asset_wallet:退回资产钱包, agent_wallet:退回代理主钱包),不填沿用原有方式"`
CustomerAccountInfo *string `json:"customer_account_info" validate:"omitempty,max=1000" maxLength:"1000" description:"客户收款信息,仅客户收款信息退款方式必填,不得复用公司线下收款方式字典"`
RequestedRefundAmount *int64 `json:"requested_refund_amount" validate:"omitempty,min=1" minimum:"1" description:"申请退款金额(分)"`
RefundVoucherKey *[]string `json:"refund_voucher_key" validate:"omitempty,max=5,dive,max=500" maxItems:"5" description:"退款凭证对象存储file_key列表重新提交时可替换历史记录缺失时必填最多5个"`
RefundVoucherKey *[]string `json:"refund_voucher_key" validate:"omitempty,max=5,dive,max=500" maxItems:"5" description:"退款凭证对象存储file_key列表重新提交时可替换客户收款信息退款方式必填最多5个"`
RefundReason *string `json:"refund_reason" validate:"omitempty,max=1000" maxLength:"1000" description:"退款原因"`
}
@@ -48,7 +52,7 @@ type ReturnRefundRequest struct {
type RefundListRequest struct {
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码默认1"`
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量默认20最大100"`
Status *int `json:"status" query:"status" validate:"omitempty,min=1,max=4" minimum:"1" maximum:"4" description:"状态 (1:待审批, 2:已通过, 3:已拒绝, 4:已退回)"`
Status *int `json:"status" query:"status" validate:"omitempty,min=1,max=6" minimum:"1" maximum:"6" description:"状态 (1:待审批, 2:已通过, 3:已拒绝, 4:已退回, 5:原路退款处理中, 6:原路退款失败)"`
OrderID *uint `json:"order_id" query:"order_id" validate:"omitempty" description:"关联订单ID"`
ShopID *uint `json:"shop_id" query:"shop_id" validate:"omitempty" description:"店铺ID"`
AssetIdentifier string `json:"asset_identifier" query:"asset_identifier" validate:"omitempty,max=100" maxLength:"100" description:"资产标识精确检索ICCID 或 设备虚拟号,非空时精确匹配)"`
@@ -56,40 +60,58 @@ type RefundListRequest struct {
// RefundResponse 退款申请详情响应
type RefundResponse struct {
ID uint `json:"id" description:"退款申请ID"`
RefundNo string `json:"refund_no" description:"退款单号"`
OrderID uint `json:"order_id" description:"关联订单ID"`
OrderNo string `json:"order_no" description:"订单号"`
AssetIdentifier string `json:"asset_identifier,omitempty" description:"下单时资产的标识符快照(卡为 ICCID设备优先使用 VirtualNo缺失时使用 IMEI"`
AssetType string `json:"asset_type,omitempty" description:"资产类型 (card:单卡, device:设备)"`
IotCardID *uint `json:"iot_card_id,omitempty" description:"IoT卡ID"`
DeviceID *uint `json:"device_id,omitempty" description:"设备ID"`
PackageUsageID *uint `json:"package_usage_id,omitempty" description:"关联套餐使用记录ID"`
ShopID *uint `json:"shop_id,omitempty" description:"店铺ID"`
ShopName string `json:"shop_name,omitempty" description:"店铺名称"`
ActualReceivedAmount int64 `json:"actual_received_amount" description:"实收金额(分)"`
RequestedRefundAmount int64 `json:"requested_refund_amount" description:"申请退款金额(分)"`
ApprovedRefundAmount *int64 `json:"approved_refund_amount,omitempty" description:"审批实际退款金额(分)"`
RefundVoucherKey []string `json:"refund_voucher_key" description:"退款凭证对象存储file_key列表最多5个"`
RefundReason string `json:"refund_reason" description:"退款原因"`
Status int `json:"status" description:"状态 (1:待审批, 2:已通过, 3:已拒绝, 4:已退回)"`
StatusName string `json:"status_name" description:"状态名称(中文)"`
ProcessorID *uint `json:"processor_id,omitempty" description:"审批人ID"`
ProcessedAt string `json:"processed_at,omitempty" description:"审批时间"`
RejectReason string `json:"reject_reason,omitempty" description:"拒绝原因"`
Remark string `json:"remark,omitempty" description:"审批备注"`
CommissionDeducted bool `json:"commission_deducted" description:"佣金是否已回扣"`
AssetReset bool `json:"asset_reset" description:"退款后资产处理是否完成"`
SubmitterID uint `json:"submitter_id" description:"提交人账号ID"`
SubmitterName string `json:"submitter_name" description:"提交人账号名称"`
ApprovalInstanceID *uint `json:"approval_instance_id,omitempty" description:"通用审批实例ID"`
ApprovalProvider string `json:"approval_provider,omitempty" description:"审批渠道企业微信为wecom"`
ApprovalStatus *int `json:"approval_status,omitempty" description:"审批状态 (0:提交中, 1:审批中, 2:已通过, 3:已拒绝, 4:已撤销, 5:通过后撤销, 6:已删除, 7:提交失败, 8:提交结果未知)"`
ApprovalStatusName string `json:"approval_status_name,omitempty" description:"审批状态名称(中文)"`
Creator uint `json:"creator" description:"创建人ID"`
Updater uint `json:"updater" description:"更新人ID"`
CreatedAt string `json:"created_at" description:"创建时间"`
UpdatedAt string `json:"updated_at" description:"更新时间"`
ID uint `json:"id" description:"退款申请ID"`
RefundNo string `json:"refund_no" description:"退款单号"`
OrderID uint `json:"order_id" description:"关联订单ID"`
OrderNo string `json:"order_no" description:"订单号"`
AssetIdentifier string `json:"asset_identifier,omitempty" description:"下单时资产的标识符快照(卡为 ICCID设备优先使用 VirtualNo缺失时使用 IMEI"`
AssetType string `json:"asset_type,omitempty" description:"资产类型 (card:单卡, device:设备)"`
IotCardID *uint `json:"iot_card_id,omitempty" description:"IoT卡ID"`
DeviceID *uint `json:"device_id,omitempty" description:"设备ID"`
PackageUsageID *uint `json:"package_usage_id,omitempty" description:"关联套餐使用记录ID"`
ShopID *uint `json:"shop_id,omitempty" description:"店铺ID"`
ShopName string `json:"shop_name,omitempty" description:"店铺名称"`
ActualReceivedAmount int64 `json:"actual_received_amount" description:"实收金额(分)"`
RequestedRefundAmount int64 `json:"requested_refund_amount" description:"申请退款金额(分)"`
ApprovedRefundAmount *int64 `json:"approved_refund_amount,omitempty" description:"审批实际退款金额(分)"`
RefundVoucherKey []string `json:"refund_voucher_key" description:"退款凭证对象存储file_key列表最多5个"`
RefundReason string `json:"refund_reason" description:"退款原因"`
Status int `json:"status" description:"状态 (1:待审批, 2:已通过, 3:已拒绝, 4:已退回, 5:原路退款处理中, 6:原路退款失败)"`
StatusName string `json:"status_name" description:"状态名称(中文)"`
Method string `json:"method" description:"退款方式 (original_route:原路退款, customer_account:客户收款信息退款, asset_wallet:退回资产钱包, agent_wallet:退回代理主钱包),空表示未接入方式的存量申请"`
MethodName string `json:"method_name" description:"退款方式中文名称"`
FrozenActualReceivedAmount int64 `json:"frozen_actual_received_amount" description:"系统派生并冻结的权威实收金额(分),作为可退金额上限"`
CustomerAccountInfo string `json:"customer_account_info" description:"客户收款信息自由文本快照,仅客户收款信息退款方式有值"`
ChannelRefundStatus int `json:"channel_refund_status" description:"渠道原路退款状态 (0:未发起, 1:处理中, 2:已成功, 3:已失败)0 表示未发起渠道退款或不适用该退款方式"`
ChannelRefundStatusName string `json:"channel_refund_status_name" description:"渠道原路退款状态中文名称"`
ChannelRefundNo string `json:"channel_refund_no" description:"渠道退款流水号,渠道明确成功或失败后回填"`
ChannelRefundRequestNo string `json:"channel_refund_request_no" description:"渠道退款请求号快照,用于幂等与对账"`
ChannelRefundAmount int64 `json:"channel_refund_amount" description:"提交渠道的退款金额快照(分)"`
ChannelRefundedAt string `json:"channel_refunded_at,omitempty" description:"渠道明确退款成功时间"`
FailureReason string `json:"failure_reason" description:"结构化失败分类稳定编码 (channel_rejected:渠道明确拒绝, credential_invalid:渠道凭证失效, insufficient_balance:渠道余额不足, timeout_unknown:超时或结果未知, approval_rejected:企业微信驳回或关闭, revoked_after_approved:企业微信通过后撤销, payment_fact_invalid:本地原支付事实不可用),空表示无失败"`
FailureReasonName string `json:"failure_reason_name" description:"失败分类中文名称"`
FailureMessage string `json:"failure_message" description:"失败安全摘要,供人工排查;不含渠道凭证等敏感内容"`
AnomalyFlag int `json:"anomaly_flag" description:"异常标记 (0:无异常, 1:有异常,需人工处理)"`
AnomalyReason string `json:"anomaly_reason" description:"异常原因说明,无异常时为空"`
LatestAttemptID uint `json:"latest_attempt_id" description:"最新审批尝试记录ID仅用于展示"`
LatestApprovalInstanceID uint `json:"latest_approval_instance_id" description:"最新通用审批实例ID仅用于展示"`
Attempts []RefundAttemptResponse `json:"attempts" description:"审批尝试记录,按提交顺序排列,历史材料不被覆盖;无尝试记录时为空数组"`
ProcessorID *uint `json:"processor_id,omitempty" description:"审批人ID"`
ProcessedAt string `json:"processed_at,omitempty" description:"审批时间"`
RejectReason string `json:"reject_reason,omitempty" description:"拒绝原因"`
Remark string `json:"remark,omitempty" description:"审批备注"`
CommissionDeducted bool `json:"commission_deducted" description:"佣金是否已回扣"`
AssetReset bool `json:"asset_reset" description:"退款后资产处理是否完成"`
SubmitterID uint `json:"submitter_id" description:"提交人账号ID"`
SubmitterName string `json:"submitter_name" description:"提交人账号名称"`
ApprovalInstanceID *uint `json:"approval_instance_id,omitempty" description:"通用审批实例ID"`
ApprovalProvider string `json:"approval_provider,omitempty" description:"审批渠道企业微信为wecom"`
ApprovalStatus *int `json:"approval_status,omitempty" description:"审批状态 (0:提交中, 1:审批中, 2:已通过, 3:已拒绝, 4:已撤销, 5:通过后撤销, 6:已删除, 7:提交失败, 8:提交结果未知)"`
ApprovalStatusName string `json:"approval_status_name,omitempty" description:"审批状态名称(中文)"`
Creator uint `json:"creator" description:"创建人ID"`
Updater uint `json:"updater" description:"更新人ID"`
CreatedAt string `json:"created_at" description:"创建时间"`
UpdatedAt string `json:"updated_at" description:"更新时间"`
}
// RefundListResponse 退款申请列表分页响应
@@ -99,3 +121,22 @@ type RefundListResponse struct {
Page int `json:"page" description:"当前页码"`
Size int `json:"size" description:"每页数量"`
}
// RefundAttemptResponse 退款审批尝试响应,材料为本次提交的冻结快照。
type RefundAttemptResponse struct {
ID uint `json:"id" description:"审批尝试记录ID同时是通用审批业务ID"`
AttemptNo int `json:"attempt_no" description:"第几次提交,从 1 递增"`
Method string `json:"method" description:"本次冻结的退款方式 (original_route:原路退款, customer_account:客户收款信息退款, asset_wallet:退回资产钱包, agent_wallet:退回代理主钱包)"`
MethodName string `json:"method_name" description:"本次冻结的退款方式中文名称"`
RefundAmount int64 `json:"refund_amount" description:"本次提交冻结的申请退款金额(分)"`
FrozenActualReceivedAmount int64 `json:"frozen_actual_received_amount" description:"本次提交冻结的权威实收金额(分)"`
RefundReason string `json:"refund_reason" description:"本次提交的退款原因"`
CustomerAccountInfo string `json:"customer_account_info" description:"本次冻结的客户收款信息快照,非客户收款信息退款方式为空"`
CustomerVoucherKey []string `json:"customer_voucher_key" description:"客户收款凭证对象存储file_key列表仅返回对象键引用"`
ChannelRefundRequestNo string `json:"channel_refund_request_no" description:"本次提交使用的渠道退款请求号快照,用于幂等与对账"`
SubmittedByAccountID uint `json:"submitted_by_account_id" description:"本次实际提交账号ID"`
ApprovalInstanceID uint `json:"approval_instance_id" description:"本次尝试关联的通用审批实例ID0 表示未关联"`
ApprovalStatus *int `json:"approval_status,omitempty" description:"通用审批实例状态 (0:提交中, 1:审批中, 2:已通过, 3:已拒绝, 4:已撤销, 5:通过后撤销, 6:已删除, 7:提交失败, 8:提交结果未知)"`
ApprovalStatusName string `json:"approval_status_name" description:"通用审批实例状态中文名称"`
CreatedAt string `json:"created_at" description:"创建时间"`
}

View File

@@ -28,6 +28,9 @@ type CreateWechatConfigRequest struct {
WxKeyContent string `json:"wx_key_content" validate:"omitempty" description:"微信支付密钥内容(PEM格式)"`
WxSerialNo string `json:"wx_serial_no" validate:"omitempty,max=200" maxLength:"200" description:"微信证书序列号"`
WxNotifyURL string `json:"wx_notify_url" validate:"omitempty,max=500" maxLength:"500" description:"微信支付回调地址"`
// v2 退款接口为双向证书接口,支付与查单不依赖该证书。
WxClientCertContent string `json:"wx_client_cert_content" validate:"omitempty" description:"微信支付API客户端证书内容(PEM格式v2 退款双向证书所需)"`
WxClientKeyContent string `json:"wx_client_key_content" validate:"omitempty" description:"微信支付API客户端证书私钥内容(PEM格式v2 退款双向证书所需)"`
FyInsCd string `json:"fy_ins_cd" validate:"omitempty,max=50" maxLength:"50" description:"富友机构号"`
FyMchntCd string `json:"fy_mchnt_cd" validate:"omitempty,max=50" maxLength:"50" description:"富友商户号"`
@@ -69,6 +72,9 @@ type UpdateWechatConfigRequest struct {
WxKeyContent *string `json:"wx_key_content" validate:"omitempty" description:"微信支付密钥内容(PEM格式)"`
WxSerialNo *string `json:"wx_serial_no" validate:"omitempty,max=200" maxLength:"200" description:"微信证书序列号"`
WxNotifyURL *string `json:"wx_notify_url" validate:"omitempty,max=500" maxLength:"500" description:"微信支付回调地址"`
// v2 退款接口为双向证书接口,支付与查单不依赖该证书。
WxClientCertContent *string `json:"wx_client_cert_content" validate:"omitempty" description:"微信支付API客户端证书内容(PEM格式v2 退款双向证书所需)"`
WxClientKeyContent *string `json:"wx_client_key_content" validate:"omitempty" description:"微信支付API客户端证书私钥内容(PEM格式v2 退款双向证书所需)"`
FyInsCd *string `json:"fy_ins_cd" validate:"omitempty,max=50" maxLength:"50" description:"富友机构号"`
FyMchntCd *string `json:"fy_mchnt_cd" validate:"omitempty,max=50" maxLength:"50" description:"富友商户号"`
@@ -119,13 +125,15 @@ type WechatConfigResponse struct {
MiniappAppID string `json:"miniapp_app_id" description:"小程序AppID"`
MiniappAppSecret string `json:"miniapp_app_secret" description:"小程序AppSecret(已脱敏)"`
WxMchID string `json:"wx_mch_id" description:"微信商户号"`
WxAPIV3Key string `json:"wx_api_v3_key" description:"微信APIv3密钥(已脱敏)"`
WxAPIV2Key string `json:"wx_api_v2_key" description:"微信APIv2密钥(已脱敏)"`
WxCertContent string `json:"wx_cert_content" description:"微信支付证书内容(配置状态)"`
WxKeyContent string `json:"wx_key_content" description:"微信支付密钥内容(配置状态)"`
WxSerialNo string `json:"wx_serial_no" description:"微信证书序列号"`
WxNotifyURL string `json:"wx_notify_url" description:"微信支付回调地址"`
WxMchID string `json:"wx_mch_id" description:"微信商户号"`
WxAPIV3Key string `json:"wx_api_v3_key" description:"微信APIv3密钥(已脱敏)"`
WxAPIV2Key string `json:"wx_api_v2_key" description:"微信APIv2密钥(已脱敏)"`
WxCertContent string `json:"wx_cert_content" description:"微信支付证书内容(配置状态)"`
WxKeyContent string `json:"wx_key_content" description:"微信支付密钥内容(配置状态)"`
WxSerialNo string `json:"wx_serial_no" description:"微信证书序列号"`
WxNotifyURL string `json:"wx_notify_url" description:"微信支付回调地址"`
WxClientCertContent string `json:"wx_client_cert_content" description:"微信支付API客户端证书内容(配置状态v2 退款双向证书所需)"`
WxClientKeyContent string `json:"wx_client_key_content" description:"微信支付API客户端证书私钥内容(配置状态v2 退款双向证书所需)"`
FyInsCd string `json:"fy_ins_cd" description:"富友机构号"`
FyMchntCd string `json:"fy_mchnt_cd" description:"富友商户号"`

View File

@@ -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}
}

View File

@@ -43,6 +43,9 @@ type WechatConfig struct {
WxKeyContent string `gorm:"column:wx_key_content;type:text;default:'';comment:微信支付密钥内容" json:"wx_key_content"`
WxSerialNo string `gorm:"column:wx_serial_no;type:varchar(200);default:'';comment:微信证书序列号" json:"wx_serial_no"`
WxNotifyURL string `gorm:"column:wx_notify_url;type:varchar(500);default:'';comment:微信支付回调地址" json:"wx_notify_url"`
// v2 退款接口(/secapi/pay/refund请求需要双向证书仅 APIv2 密钥不足以退款。
WxClientCertContent string `gorm:"column:wx_client_cert_content;type:text;default:'';comment:微信支付API客户端证书内容apiclient_cert.pem" json:"wx_client_cert_content"`
WxClientKeyContent string `gorm:"column:wx_client_key_content;type:text;default:'';comment:微信支付API客户端证书私钥内容apiclient_key.pem" json:"wx_client_key_content"`
// 支付-富友
FyInsCd string `gorm:"column:fy_ins_cd;type:varchar(50);default:'';comment:富友机构号" json:"fy_ins_cd"`