All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m33s
补齐 PRD §2.3.1「退款管理补充字段」:退款列表、详情与导出新增 「当前退款套餐已用量」与「当前退款套餐总量」两个纯展示字段。 - 套餐定位口径与退款套餐失效保持一致,按优先级取唯一一条: 冻结的 package_usage_id(且属于该订单)→ 订单主套餐 → 订单任一套餐, 同级按标识升序。不按当前世代或当前生效套餐推断;不按套餐状态过滤, 使退款后套餐转已失效时仍能回看用量。 - 列表与详情用固定两次查询批量解析(按标识、按订单),查询次数不随条数增长; 详情复用同一函数。解析不到套餐或记录已物理删除时返回 0,不阻断读取。 - 导出新增两列并改用同一优先级的 LATERAL 取法,不再依赖只按 r.package_usage_id 的 join——生产库 1296 条退款仅 157 条带该字段,旧取法会让多数行显示零值。 - 不改变退款金额校验、冻结实收、套餐失效、接续、停机与佣金回溯任何规则。 验证:测试库 junhong_cmp_test 实测冻结记录、订单主套餐回退、记录缺失返回 0 三项 解析场景与「4 条退款固定 2 次查询」;并以同批 43 条退款对拍 Go 解析器与导出 SQL, 口径不一致 0 条;导出 43 行列数与表头一致。无迁移、无接口路径变化。
137 lines
5.0 KiB
Go
137 lines
5.0 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/errors"
|
||
)
|
||
|
||
// refundPackageUsage 是退款展示所需的当前套餐用量。
|
||
type refundPackageUsage struct {
|
||
UsedMB int64
|
||
TotalMB int64
|
||
}
|
||
|
||
// loadRefundPackageUsages 批量解析每个退款申请的「当前退款套餐」用量。
|
||
//
|
||
// 定位口径与退款套餐失效保持一致,按优先级取唯一一条:
|
||
// 1. 退款申请冻结的套餐使用记录(package_usage_id);
|
||
// 2. 该退款关联订单下主套餐使用记录(master_usage_id 为空);
|
||
// 3. 该退款关联订单下任一套餐使用记录。
|
||
//
|
||
// 同一优先级内按使用记录标识升序取第一条,保证同一退款每次返回相同结果。
|
||
// 不按当前生效套餐或当前世代推断,也不跨订单取套餐:换货后权益仍保留原订单关系,
|
||
// 按世代推断会指向错误的资产。
|
||
//
|
||
// 用量的性质是「当前可取得」:不按套餐状态过滤。退款完成后套餐已转已失效,
|
||
// 此时仍需展示其用量;若按状态过滤,退款完成后字段会集体归零,与用途矛盾。
|
||
//
|
||
// 实现固定为两次查询(按标识、按订单),与退款条数无关;解析不到时返回 0 而非报错,
|
||
// 使展示增强不阻断列表、详情或导出。
|
||
func (s *Service) loadRefundPackageUsages(ctx context.Context, refunds []*model.RefundRequest) (map[uint]refundPackageUsage, error) {
|
||
result := make(map[uint]refundPackageUsage, len(refunds))
|
||
usageIDs := make([]uint, 0, len(refunds))
|
||
orderIDs := make([]uint, 0, len(refunds))
|
||
seenUsage := make(map[uint]struct{}, len(refunds))
|
||
seenOrder := make(map[uint]struct{}, len(refunds))
|
||
for _, refund := range refunds {
|
||
if refund == nil || refund.ID == 0 {
|
||
continue
|
||
}
|
||
result[refund.ID] = refundPackageUsage{}
|
||
if refund.PackageUsageID != nil && *refund.PackageUsageID > 0 {
|
||
if _, exists := seenUsage[*refund.PackageUsageID]; !exists {
|
||
seenUsage[*refund.PackageUsageID] = struct{}{}
|
||
usageIDs = append(usageIDs, *refund.PackageUsageID)
|
||
}
|
||
}
|
||
if refund.OrderID > 0 {
|
||
if _, exists := seenOrder[refund.OrderID]; !exists {
|
||
seenOrder[refund.OrderID] = struct{}{}
|
||
orderIDs = append(orderIDs, refund.OrderID)
|
||
}
|
||
}
|
||
}
|
||
if len(usageIDs) == 0 && len(orderIDs) == 0 {
|
||
return result, nil
|
||
}
|
||
|
||
// 第一次查询:按套餐使用记录标识取冻结记录。
|
||
byUsageID := make(map[uint]*model.PackageUsage, len(usageIDs))
|
||
// 第二次查询:按订单取全部候选,再在内存中按优先级择一。
|
||
byOrderID := make(map[uint][]*model.PackageUsage, len(orderIDs))
|
||
|
||
if len(usageIDs) > 0 {
|
||
var usages []model.PackageUsage
|
||
if err := s.db.WithContext(ctx).
|
||
Select("id", "order_id", "master_usage_id", "data_usage_mb", "data_limit_mb").
|
||
Where("id IN ?", usageIDs).Find(&usages).Error; err != nil {
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询退款套餐使用记录失败")
|
||
}
|
||
for index := range usages {
|
||
byUsageID[usages[index].ID] = &usages[index]
|
||
}
|
||
}
|
||
if len(orderIDs) > 0 {
|
||
var usages []model.PackageUsage
|
||
if err := s.db.WithContext(ctx).
|
||
Select("id", "order_id", "master_usage_id", "data_usage_mb", "data_limit_mb").
|
||
Where("order_id IN ?", orderIDs).
|
||
Order("id ASC").Find(&usages).Error; err != nil {
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询退款订单套餐使用记录失败")
|
||
}
|
||
for index := range usages {
|
||
usage := &usages[index]
|
||
byOrderID[usage.OrderID] = append(byOrderID[usage.OrderID], usage)
|
||
}
|
||
}
|
||
|
||
for _, refund := range refunds {
|
||
if refund == nil || refund.ID == 0 {
|
||
continue
|
||
}
|
||
usage := resolveRefundPackageUsage(refund, byUsageID, byOrderID)
|
||
if usage == nil {
|
||
continue
|
||
}
|
||
result[refund.ID] = refundPackageUsage{UsedMB: usage.DataUsageMB, TotalMB: usage.DataLimitMB}
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// resolveRefundPackageUsage 按优先级为单个退款申请择一当前套餐使用记录。
|
||
func resolveRefundPackageUsage(
|
||
refund *model.RefundRequest,
|
||
byUsageID map[uint]*model.PackageUsage,
|
||
byOrderID map[uint][]*model.PackageUsage,
|
||
) *model.PackageUsage {
|
||
// 优先级一:冻结的套餐使用记录,且必须属于该退款关联订单(防止跨订单取值)。
|
||
if refund.PackageUsageID != nil && *refund.PackageUsageID > 0 {
|
||
if usage, exists := byUsageID[*refund.PackageUsageID]; exists && usage.OrderID == refund.OrderID {
|
||
return usage
|
||
}
|
||
}
|
||
// 优先级二:订单主套餐;优先级三:订单任一套餐。候选已按标识升序,取首个命中。
|
||
candidates := byOrderID[refund.OrderID]
|
||
if len(candidates) == 0 {
|
||
return nil
|
||
}
|
||
for _, usage := range candidates {
|
||
if usage.MasterUsageID == nil {
|
||
return usage
|
||
}
|
||
}
|
||
return candidates[0]
|
||
}
|
||
|
||
// applyRefundPackageUsage 把解析结果写入响应字段。
|
||
func applyRefundPackageUsage(response *dto.RefundResponse, usage refundPackageUsage) {
|
||
if response == nil {
|
||
return
|
||
}
|
||
response.RefundPackageUsedMB = usage.UsedMB
|
||
response.RefundPackageTotalMB = usage.TotalMB
|
||
}
|