按 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 与行为核对,未调用真实渠道。
88 lines
3.3 KiB
Go
88 lines
3.3 KiB
Go
package refund
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
|
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
|
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
|
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
|
)
|
|
|
|
// loadAttemptResponses 批量读取退款申请的审批尝试历史,并补齐每次尝试的审批实例状态。
|
|
//
|
|
// 尝试记录按提交次序升序返回,历史材料与审批结果不被覆盖;未接入尝试模式的存量申请返回空切片。
|
|
func (s *Service) loadAttemptResponses(ctx context.Context, refunds []*model.RefundRequest) (map[uint][]dto.RefundAttemptResponse, error) {
|
|
result := make(map[uint][]dto.RefundAttemptResponse, len(refunds))
|
|
refundIDs := make([]uint, 0, len(refunds))
|
|
for _, refund := range refunds {
|
|
if refund == nil || refund.ID == 0 {
|
|
continue
|
|
}
|
|
refundIDs = append(refundIDs, refund.ID)
|
|
result[refund.ID] = []dto.RefundAttemptResponse{}
|
|
}
|
|
if len(refundIDs) == 0 {
|
|
return result, nil
|
|
}
|
|
|
|
var attempts []model.RefundRequestAttempt
|
|
if err := s.db.WithContext(ctx).
|
|
Where("refund_id IN ?", refundIDs).
|
|
Order("refund_id ASC, attempt_no ASC, id ASC").
|
|
Find(&attempts).Error; err != nil {
|
|
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询退款审批尝试记录失败")
|
|
}
|
|
if len(attempts) == 0 {
|
|
return result, nil
|
|
}
|
|
|
|
instanceIDs := make([]uint, 0, len(attempts))
|
|
for index := range attempts {
|
|
if attempts[index].ApprovalInstanceID != nil && *attempts[index].ApprovalInstanceID > 0 {
|
|
instanceIDs = append(instanceIDs, *attempts[index].ApprovalInstanceID)
|
|
}
|
|
}
|
|
statuses := map[uint]int{}
|
|
if len(instanceIDs) > 0 {
|
|
var instances []model.ApprovalInstance
|
|
if err := s.db.WithContext(ctx).Select("id", "status").Where("id IN ?", instanceIDs).Find(&instances).Error; err != nil {
|
|
return nil, errors.Wrap(errors.CodeDatabaseError, err, "批量查询退款审批尝试实例状态失败")
|
|
}
|
|
for _, instance := range instances {
|
|
statuses[instance.ID] = instance.Status
|
|
}
|
|
}
|
|
|
|
for index := range attempts {
|
|
attempt := &attempts[index]
|
|
item := dto.RefundAttemptResponse{
|
|
ID: attempt.ID,
|
|
AttemptNo: attempt.AttemptNo,
|
|
Method: attempt.Method,
|
|
MethodName: constants.RefundMethodName(attempt.Method),
|
|
RefundAmount: attempt.RefundAmount,
|
|
FrozenActualReceivedAmount: attempt.FrozenActualReceivedAmount,
|
|
RefundReason: attempt.RefundReason,
|
|
CustomerAccountInfo: attempt.CustomerAccountInfo,
|
|
CustomerVoucherKey: []string(attempt.CustomerVoucherKeys),
|
|
ChannelRefundRequestNo: attempt.ChannelRefundRequestNo,
|
|
SubmittedByAccountID: attempt.SubmittedByAccountID,
|
|
CreatedAt: attempt.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
}
|
|
if len(item.CustomerVoucherKey) == 0 {
|
|
item.CustomerVoucherKey = []string{}
|
|
}
|
|
if attempt.ApprovalInstanceID != nil && *attempt.ApprovalInstanceID > 0 {
|
|
item.ApprovalInstanceID = *attempt.ApprovalInstanceID
|
|
if status, exists := statuses[*attempt.ApprovalInstanceID]; exists {
|
|
value := status
|
|
item.ApprovalStatus = &value
|
|
item.ApprovalStatusName = constants.GetApprovalStatusName(status)
|
|
}
|
|
}
|
|
result[attempt.RefundID] = append(result[attempt.RefundID], item)
|
|
}
|
|
return result, nil
|
|
}
|