Files
junhong_cmp_fiber/internal/store/postgres/refund_store.go
邹进 94cbf2adab feat: 采集记录成功失败分表并保存网关原始数据,到期归档 csv.gz,新增记录分页查询接口
- 网关客户端新增原始数据收集器,记录最后一次尝试的明文请求、HTTP 状态、响应体与 code/msg/trace_id
- tb_card_traffic_collection 只写成功读数,新增 carrier_id 与原始数据列;新增 tb_card_traffic_collection_failure,失败不判断有无套餐并带错误详情
- 采集记录在线保留当月加最近 2 个整月,到期分区摘出、导出 csv.gz、校验行数与 SHA-256 后删除,本地保留 3 个月
- 新增采集成功/失败、卡日流量、套餐日流量详单、冲正流水、退款后处理尝试列表接口:可查单个也可查全部,分页与条件筛选
- 停复机记录支持资产标识、场景、原因、指令号筛选;单卡历史支持动作、结果、来源筛选
- 单卡采集记录支持筛选与合并失败记录;套餐日流量详单支持分页;退款列表支持后处理状态筛选
- 测试环境 api、worker 日志分目录,保留 40 个压缩备份;worker 挂载 archive 目录
- 迁移 000272、000273 需维护者在测试库执行后再部署

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 18:47:00 +09:00

208 lines
6.7 KiB
Go

package postgres
import (
"context"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/store"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
"gorm.io/gorm"
)
// RefundListFilters 退款列表筛选条件
type RefundListFilters struct {
Status *int `json:"status"`
OrderID *uint `json:"order_id"`
ShopID *uint `json:"shop_id"`
AssetIdentifier string `json:"asset_identifier"`
AssetPostStatus *int `json:"asset_post_status"`
}
// RefundStore 退款申请数据访问层
type RefundStore struct {
db *gorm.DB
}
// NewRefundStore 创建退款申请 Store 实例
func NewRefundStore(db *gorm.DB) *RefundStore {
return &RefundStore{db: db}
}
// Create 创建退款申请记录
func (s *RefundStore) Create(ctx context.Context, req *model.RefundRequest) error {
return s.db.WithContext(ctx).Create(req).Error
}
// GetByID 根据 ID 查询退款申请详情(含读取数据权限过滤)。
func (s *RefundStore) GetByID(ctx context.Context, id uint) (*model.RefundRequest, error) {
var req model.RefundRequest
query := s.db.WithContext(ctx).
Model(&model.RefundRequest{}).
Select("tb_refund_request.*, tb_shop.shop_name AS shop_name").
Joins("LEFT JOIN tb_shop ON tb_shop.id = tb_refund_request.shop_id AND tb_shop.deleted_at IS NULL").
Where("tb_refund_request.id = ?", id)
query = applyRefundReadScope(ctx, query)
if err := query.First(&req).Error; err != nil {
return nil, err
}
return &req, nil
}
// GetByIDForOperation 根据 ID 查询退款申请(含写操作数据权限过滤)。
func (s *RefundStore) GetByIDForOperation(ctx context.Context, id uint) (*model.RefundRequest, error) {
var req model.RefundRequest
query := s.db.WithContext(ctx).
Model(&model.RefundRequest{}).
Select("tb_refund_request.*, tb_shop.shop_name AS shop_name").
Joins("LEFT JOIN tb_shop ON tb_shop.id = tb_refund_request.shop_id AND tb_shop.deleted_at IS NULL").
Where("tb_refund_request.id = ?", id)
query = applyRefundOperationScope(ctx, query)
if err := query.First(&req).Error; err != nil {
return nil, err
}
return &req, nil
}
// Update 更新退款申请记录(全字段保存)
func (s *RefundStore) Update(ctx context.Context, req *model.RefundRequest) error {
return s.db.WithContext(ctx).Save(req).Error
}
// UpdateFields 按字段更新退款申请(用于条件更新场景)
func (s *RefundStore) UpdateFields(ctx context.Context, id uint, updates map[string]any) error {
return s.db.WithContext(ctx).
Model(&model.RefundRequest{}).
Where("id = ?", id).
Updates(updates).Error
}
// List 分页查询退款申请列表(含数据权限过滤和筛选条件)
func (s *RefundStore) List(ctx context.Context, opts *store.QueryOptions, filters *RefundListFilters) ([]*model.RefundRequest, int64, error) {
var requests []*model.RefundRequest
var total int64
query := s.db.WithContext(ctx).
Model(&model.RefundRequest{}).
Select("tb_refund_request.*, tb_shop.shop_name AS shop_name").
Joins("LEFT JOIN tb_shop ON tb_shop.id = tb_refund_request.shop_id AND tb_shop.deleted_at IS NULL")
query = applyRefundReadScope(ctx, query)
if filters != nil {
if filters.Status != nil {
query = query.Where("tb_refund_request.status = ?", *filters.Status)
}
if filters.OrderID != nil {
query = query.Where("tb_refund_request.order_id = ?", *filters.OrderID)
}
if filters.ShopID != nil {
query = query.Where("tb_refund_request.shop_id = ?", *filters.ShopID)
}
if filters.AssetIdentifier != "" {
query = query.Where("tb_refund_request.asset_identifier = ?", filters.AssetIdentifier)
}
if filters.AssetPostStatus != nil {
query = query.Where("tb_refund_request.asset_post_status = ?", *filters.AssetPostStatus)
}
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
if opts == nil {
opts = &store.QueryOptions{
Page: 1,
PageSize: constants.DefaultPageSize,
}
}
offset := (opts.Page - 1) * opts.PageSize
query = query.Offset(offset).Limit(opts.PageSize)
if opts.OrderBy != "" {
query = query.Order(opts.OrderBy)
} else {
query = query.Order("tb_refund_request.created_at DESC")
}
if err := query.Find(&requests).Error; err != nil {
return nil, 0, err
}
return requests, total, nil
}
// applyRefundReadScope 应用退款单读取数据权限。
// 代理仅可查看直接所属店铺的申请,不包含下级代理店铺。
func applyRefundReadScope(ctx context.Context, query *gorm.DB) *gorm.DB {
switch middleware.GetUserTypeFromContext(ctx) {
case constants.UserTypeSuperAdmin, constants.UserTypePlatform:
return query
case constants.UserTypeAgent:
shopID := middleware.GetShopIDFromContext(ctx)
if shopID == 0 {
return query.Where("1 = 0")
}
return query.Where("tb_refund_request.shop_id = ?", shopID)
default:
return query.Where("1 = 0")
}
}
// applyRefundOperationScope 应用退款单写操作数据权限。
func applyRefundOperationScope(ctx context.Context, query *gorm.DB) *gorm.DB {
switch middleware.GetUserTypeFromContext(ctx) {
case constants.UserTypeSuperAdmin, constants.UserTypePlatform:
return query
case constants.UserTypeAgent:
userID := middleware.GetUserIDFromContext(ctx)
if userID == 0 {
return query.Where("1 = 0")
}
return query.Where("tb_refund_request.creator = ?", userID)
default:
return query.Where("1 = 0")
}
}
// FindActiveByOrderID 查找订单关联的活跃退款申请(状态为待审批、已通过)
func (s *RefundStore) FindActiveByOrderID(ctx context.Context, orderID uint) (*model.RefundRequest, error) {
var req model.RefundRequest
err := s.db.WithContext(ctx).
Where("order_id = ? AND status IN ?", orderID, []int{
model.RefundStatusPending,
model.RefundStatusApproved,
}).
First(&req).Error
if err != nil {
return nil, err
}
return &req, nil
}
// HasUnfinishedByAsset 检查资产是否存在仍会影响资产状态的退款申请。
// 待审批、已退回,以及已通过但退款后资产处理尚未完成的申请均视为未终结。
func (s *RefundStore) HasUnfinishedByAsset(ctx context.Context, assetType string, assetID uint) (bool, error) {
var assetColumn string
switch assetType {
case constants.ExchangeAssetTypeIotCard:
assetColumn = "iot_card_id"
case constants.ExchangeAssetTypeDevice:
assetColumn = "device_id"
default:
return false, gorm.ErrInvalidData
}
var count int64
err := s.db.WithContext(ctx).
Model(&model.RefundRequest{}).
Where(assetColumn+" = ?", assetID).
Where("status IN ? OR (status = ? AND asset_reset = ?)",
[]int{model.RefundStatusPending, model.RefundStatusReturned},
model.RefundStatusApproved,
false,
).
Count(&count).Error
return count > 0, err
}