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:
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/application/refundapproval"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
retentionquery "github.com/break/junhong_cmp_fiber/internal/query/retention"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
@@ -520,13 +521,17 @@ func (q *Query) expandRecharges(ctx context.Context, refs *financeRefs) error {
|
||||
}
|
||||
|
||||
// expandApprovals 只按审批业务类型关联退款或线下代理充值。
|
||||
//
|
||||
// 退款审批的业务标识在尝试模式下指向审批尝试记录、存量模式下指向退款申请,两者来自独立自增序列,
|
||||
// 因此按退款单过滤时必须同时展开「尝试记录指向的退款单」与「退款单自身」两种审批实例,
|
||||
// 命中后再经共享解析器还原真实退款单,避免把尝试记录主键当作退款单编号收集。
|
||||
func (q *Query) expandApprovals(ctx context.Context, refs *financeRefs) error {
|
||||
conditions, args := make([]string, 0, 3), make([]any, 0, 3)
|
||||
if len(refs.approvals) > 0 {
|
||||
conditions, args = append(conditions, "id IN ?"), append(args, uintKeys(refs.approvals))
|
||||
}
|
||||
if len(refs.refunds) > 0 {
|
||||
conditions, args = append(conditions, "business_type = ? AND business_id IN ?"), append(args, constants.ApprovalBusinessTypeRefund, uintKeys(refs.refunds))
|
||||
conditions, args = append(conditions, refundApprovalBusinessCondition()), append(args, constants.ApprovalBusinessTypeRefund, uintKeys(refs.refunds), uintKeys(refs.refunds))
|
||||
}
|
||||
if len(refs.agentRecharges) > 0 {
|
||||
conditions, args = append(conditions, "business_type = ? AND business_id IN ?"), append(args, constants.ApprovalBusinessTypeOfflineRecharge, uintKeys(refs.agentRecharges))
|
||||
@@ -542,7 +547,11 @@ func (q *Query) expandApprovals(ctx context.Context, refs *financeRefs) error {
|
||||
addUint(refs.approvals, row.ID)
|
||||
switch row.BusinessType {
|
||||
case constants.ApprovalBusinessTypeRefund:
|
||||
addUint(refs.refunds, row.BusinessID)
|
||||
refundID, err := refundapproval.ResolveRefundIDInTx(ctx, q.db, row.BusinessID, row.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
addUint(refs.refunds, refundID)
|
||||
case constants.ApprovalBusinessTypeOfflineRecharge:
|
||||
addUint(refs.agentRecharges, row.BusinessID)
|
||||
}
|
||||
@@ -550,6 +559,12 @@ func (q *Query) expandApprovals(ctx context.Context, refs *financeRefs) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// refundApprovalBusinessCondition 返回退款审批实例的过滤条件,覆盖尝试模式与存量模式两种业务标识语义。
|
||||
// 三个占位符依次为业务类型、尝试记录主键、退款单主键。
|
||||
func refundApprovalBusinessCondition() string {
|
||||
return "business_type = ? AND (business_id IN ? OR business_id IN (SELECT refund_attempt.id FROM tb_refund_request_attempt AS refund_attempt WHERE refund_attempt.refund_id IN ?))"
|
||||
}
|
||||
|
||||
// loadFinanceAuditRows 只读取具有资金资源的审计事件,并保留操作者权威。
|
||||
func (q *Query) loadFinanceAuditRows(ctx context.Context, filter FinanceFilter, refs *financeRefs) ([]model.AuditEvent, int64, error) {
|
||||
resourceTypes := []string{
|
||||
@@ -1133,11 +1148,12 @@ func (q *Query) loadApprovalFinance(ctx context.Context, filter FinanceFilter, r
|
||||
query = applyFinanceTime(query, filter, "status_changed_at")
|
||||
conditions, args := make([]string, 0, 5), make([]any, 0, 5)
|
||||
appendUintCondition(&conditions, &args, "id", refs.approvals)
|
||||
appendApprovalBusinessCondition(&conditions, &args, constants.ApprovalBusinessTypeRefund, refs.refunds)
|
||||
appendRefundApprovalBusinessCondition(&conditions, &args, refs.refunds)
|
||||
appendApprovalBusinessCondition(&conditions, &args, constants.ApprovalBusinessTypeOfflineRecharge, refs.agentRecharges)
|
||||
if filter.ShopID != 0 {
|
||||
conditions = append(conditions, `(business_type = ? AND EXISTS (SELECT 1 FROM tb_refund_request r WHERE r.id = tb_approval_instance.business_id AND r.deleted_at IS NULL AND r.shop_id = ?)) OR (business_type = ? AND EXISTS (SELECT 1 FROM tb_agent_recharge_record ar WHERE ar.id = tb_approval_instance.business_id AND ar.deleted_at IS NULL AND ar.shop_id = ?))`)
|
||||
args = append(args, constants.ApprovalBusinessTypeRefund, filter.ShopID, constants.ApprovalBusinessTypeOfflineRecharge, filter.ShopID)
|
||||
// 退款审批的店铺归属要同时覆盖尝试模式(business_id 指向尝试记录)与存量模式(business_id 指向退款单)。
|
||||
conditions = append(conditions, `(business_type = ? AND (EXISTS (SELECT 1 FROM tb_refund_request_attempt a JOIN tb_refund_request r ON r.id = a.refund_id AND r.deleted_at IS NULL WHERE a.id = tb_approval_instance.business_id AND r.shop_id = ?) OR EXISTS (SELECT 1 FROM tb_refund_request r WHERE r.id = tb_approval_instance.business_id AND r.deleted_at IS NULL AND r.shop_id = ?))) OR (business_type = ? AND EXISTS (SELECT 1 FROM tb_agent_recharge_record ar WHERE ar.id = tb_approval_instance.business_id AND ar.deleted_at IS NULL AND ar.shop_id = ?))`)
|
||||
args = append(args, constants.ApprovalBusinessTypeRefund, filter.ShopID, filter.ShopID, constants.ApprovalBusinessTypeOfflineRecharge, filter.ShopID)
|
||||
}
|
||||
appendAccountActorCondition(&conditions, &args, filter, "submitter_account_id")
|
||||
if filter.CorrelationID != "" {
|
||||
@@ -1152,7 +1168,11 @@ func (q *Query) loadApprovalFinance(ctx context.Context, filter FinanceFilter, r
|
||||
for _, row := range rows {
|
||||
refsView := ledgerRefs(constants.AuditResourceApprovalInstance, strconv.FormatUint(uint64(row.ID), 10), strconv.FormatUint(uint64(row.ID), 10), row.SubmitterAccountID)
|
||||
refsView.CorrelationID = stringPointer(row.CorrelationID)
|
||||
refsView.ResourceRefs = append(refsView.ResourceRefs, approvalBusinessRefs(row)...)
|
||||
businessRefs, err := approvalBusinessRefs(ctx, q.db, row)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
refsView.ResourceRefs = append(refsView.ResourceRefs, businessRefs...)
|
||||
nodes = append(nodes, FinanceTimelineNode{
|
||||
RecordSource: constants.AuditRecordSourceApprovalInstance, NodeID: strconv.FormatUint(uint64(row.ID), 10), OccurredAt: row.StatusChangedAt,
|
||||
Code: row.BusinessType, Title: "审批实例", Result: strconv.Itoa(row.Status), ResultName: constants.GetApprovalStatusName(row.Status),
|
||||
@@ -1259,6 +1279,17 @@ func appendApprovalBusinessCondition(conditions *[]string, args *[]any, business
|
||||
*args = append(*args, businessType, uintKeys(values))
|
||||
}
|
||||
|
||||
// appendRefundApprovalBusinessCondition 关联按退款单过滤的退款审批实例。
|
||||
// 业务标识在尝试模式下指向审批尝试记录、存量模式下指向退款申请,因此两种语义都要命中。
|
||||
func appendRefundApprovalBusinessCondition(conditions *[]string, args *[]any, values map[uint]struct{}) {
|
||||
if len(values) == 0 {
|
||||
return
|
||||
}
|
||||
refundIDs := uintKeys(values)
|
||||
*conditions = append(*conditions, refundApprovalBusinessCondition())
|
||||
*args = append(*args, constants.ApprovalBusinessTypeRefund, refundIDs, refundIDs)
|
||||
}
|
||||
|
||||
func appendAccountActorCondition(conditions *[]string, args *[]any, filter FinanceFilter, columns ...string) {
|
||||
if filter.ActorKind != constants.AuditActorAccount || filter.ActorID == "" {
|
||||
return
|
||||
@@ -1354,18 +1385,27 @@ func paymentBusinessRefs(row model.Payment) []InvestigationResourceRef {
|
||||
}
|
||||
|
||||
// approvalBusinessRefs 按审批实例声明的业务类型生成稳定跳转。
|
||||
func approvalBusinessRefs(row model.ApprovalInstance) []InvestigationResourceRef {
|
||||
//
|
||||
// 退款审批的业务标识在尝试模式下指向审批尝试记录,直接用 business_id 跳转会指向错误资源,
|
||||
// 因此这里经共享解析器还原真实退款单;存量模式(business_id 即退款单主键)由解析器兜底命中。
|
||||
func approvalBusinessRefs(ctx context.Context, db *gorm.DB, row model.ApprovalInstance) ([]InvestigationResourceRef, error) {
|
||||
resourceType := ""
|
||||
resourceID := row.BusinessID
|
||||
switch row.BusinessType {
|
||||
case constants.ApprovalBusinessTypeRefund:
|
||||
resourceType = constants.AuditResourceRefund
|
||||
refundID, err := refundapproval.ResolveRefundIDInTx(ctx, db, row.BusinessID, row.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resourceID = refundID
|
||||
case constants.ApprovalBusinessTypeOfflineRecharge:
|
||||
resourceType = constants.AuditResourceAgentRecharge
|
||||
}
|
||||
if resourceType == "" {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
return []InvestigationResourceRef{financeResourceRef(resourceType, row.BusinessID, "")}
|
||||
return []InvestigationResourceRef{financeResourceRef(resourceType, resourceID, "")}, nil
|
||||
}
|
||||
|
||||
func authoritativeAmount(table, field string) FinanceAmountAuthority {
|
||||
|
||||
Reference in New Issue
Block a user