Files
junhong_cmp_fiber/internal/exporter/package_silence_scene.go
邹进 909f4d0ec2
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Failing after 8m39s
feat: 实名即生效套餐沉默期、延期办理与资产出库时间
- 迁移 000259~000265:套餐记录沉默期字段与扫描索引、套餐默认沉默期、延期政策/流水/订单/批次/明细 5 张表、默认政策与系统配置(总开关默认关闭)、企微审批白名单、卡与设备出库时间及出库记录
- 沉默期设置(订单发放、自动购包、钱包续费)、后台购买时调整(超默认值按政策免审或企微审批)、统一判定与激活闸门、排队与孤儿扫描跳过过期套餐、过期套餐禁止退款、每 10 分钟扫描
- 总开关专用接口与打开前 4 项检查;延期政策;延期资格与计费;H5 自助与后台二维码在线支付(商户池路由)、支付回调与查单恢复、线下登记、特殊/批量延期、异常重试与原路退款;沉默期审批场景与终态处理
- 换货沉默期流水与出库时间;卡/设备列表、详情、导出与出库记录接口;延期订单与即将过期套餐列表及导出;财务流水纳入延期支付
- 沉默期与出库时间回填工具 cmd/silence-backfill;前端接口变更说明

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:32:58 +09:00

219 lines
8.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package exporter
import (
"context"
"strconv"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
packagesilencequery "github.com/break/junhong_cmp_fiber/internal/query/packagesilence"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// 沉默期延期订单与即将过期 / 已过期套餐导出(add-package-silence-period)。
// 导出在 Worker 中执行,数据范围按任务创建时冻结的账号类型与店铺范围重建上下文。
var silenceOrderHeaders = []string{
"订单号", "批次号", "资产类型", "资产标识", "套餐名称", "来源", "状态", "输入天数", "计费月数", "延长天数",
"单价(元/月)", "金额(元)", "政策", "下单时截止", "预计新截止", "支付方式", "收款商户", "交易流水号",
"线下收款方式", "线下交易单号", "实付金额(元)", "支付时间", "延期生效时间", "退款单号", "退款金额(元)", "退款时间", "异常说明", "创建时间",
}
var silenceUsageHeaders = []string{
"套餐记录ID", "订单号", "资产类型", "资产标识", "店铺", "套餐名称", "购买时间", "沉默期状态", "沉默期天数",
"截止时间", "过期时间", "累计延期天数", "付费延期天数", "延期次数", "免费延期次数", "调整审批中",
}
// silenceExportContext 用任务冻结的账号类型与店铺范围构造查询上下文。
func silenceExportContext(ctx context.Context, params ExportParams) context.Context {
ctx = context.WithValue(ctx, constants.ContextKeyUserType, params.UserType)
if params.CreatorShopID != nil {
ctx = context.WithValue(ctx, constants.ContextKeyShopID, *params.CreatorShopID)
}
if params.UserType == constants.UserTypeAgent {
ctx = context.WithValue(ctx, constants.ContextKeySubordinateShopIDs, append([]uint{}, params.ScopeShopIDs...))
}
return ctx
}
// SilenceOrderDataSource 沉默期延期订单导出数据源(场景 package_silence_order)。
type SilenceOrderDataSource struct {
query *packagesilencequery.Query
}
// NewSilenceOrderDataSource 创建延期订单导出数据源。
func NewSilenceOrderDataSource(db *gorm.DB) *SilenceOrderDataSource {
return &SilenceOrderDataSource{query: packagesilencequery.NewQuery(db)}
}
// Scene 返回导出场景编码。
func (s *SilenceOrderDataSource) Scene() string { return constants.ExportTaskSceneSilenceOrder }
func (s *SilenceOrderDataSource) filter(params ExportParams) packagesilencequery.OrderFilter {
req := dto.SilenceOrderListRequest{}
if id, ok := filterUint(params.Filters, "package_usage_id"); ok && id > 0 {
req.PackageUsageID = &id
}
if id, ok := filterUint(params.Filters, "package_id"); ok && id > 0 {
req.PackageID = &id
}
req.AssetIdentifier, _ = filterString(params.Filters, "asset_identifier")
req.OrderNo, _ = filterString(params.Filters, "order_no")
req.Status, _ = filterString(params.Filters, "status")
req.Source, _ = filterString(params.Filters, "source")
req.PaymentMethod, _ = filterString(params.Filters, "payment_method")
req.MerchantName, _ = filterString(params.Filters, "merchant_name")
req.ThirdPartyTradeNo, _ = filterString(params.Filters, "third_party_trade_no")
req.PaidStart, _ = filterString(params.Filters, "paid_start")
req.PaidEnd, _ = filterString(params.Filters, "paid_end")
return packagesilencequery.OrderFilter{Request: req}
}
// Count 统计导出行数。
func (s *SilenceOrderDataSource) Count(ctx context.Context, params ExportParams) (int, error) {
query, err := s.query.OrderQuery(silenceExportContext(ctx, params), s.filter(params))
if err != nil {
return 0, err
}
var total int64
if err := query.Count(&total).Error; err != nil {
return 0, err
}
return int(total), nil
}
// Headers 返回导出表头。
func (s *SilenceOrderDataSource) Headers(context.Context, ExportParams) ([]string, error) {
return silenceOrderHeaders, nil
}
// Fetch 按 offset/limit 返回导出行。
func (s *SilenceOrderDataSource) Fetch(ctx context.Context, params ExportParams, offset, limit int) ([][]string, error) {
if limit <= 0 {
return [][]string{}, nil
}
ctx = silenceExportContext(ctx, params)
query, err := s.query.OrderQuery(ctx, s.filter(params))
if err != nil {
return nil, err
}
var orders []model.PackageSilenceExtensionOrder
if err := query.Select("o.*").Order("o.id ASC").Offset(offset).Limit(limit).Find(&orders).Error; err != nil {
return nil, err
}
items, err := s.query.OrderItems(ctx, orders)
if err != nil {
return nil, err
}
rows := make([][]string, 0, len(items))
for _, item := range items {
rows = append(rows, []string{
item.OrderNo, item.BatchNo, item.AssetType, item.AssetIdentifier, item.PackageName, item.SourceName, item.StatusName,
strconv.Itoa(item.InputDays), strconv.Itoa(item.Months), strconv.Itoa(item.ExtensionDays),
formatMoneyYuan(item.UnitPrice), formatMoneyYuan(item.Amount), item.PolicyName,
item.BeforeDeadlineAt.Format(exportTimeLayout), item.ExpectedDeadlineAt.Format(exportTimeLayout),
item.PaymentMethod, item.PaymentMerchantName, item.ThirdPartyTradeNo, item.OfflinePaymentMethodName, item.OfflineTradeNo,
formatOptionalMoney(item.PaidAmount), formatOptionalTime(item.PaidAt), formatOptionalTime(item.CompletedAt),
item.RefundNo, formatOptionalMoney(item.RefundAmount), formatOptionalTime(item.RefundedAt), item.ExceptionSummary,
item.CreatedAt.Format(exportTimeLayout),
})
}
return rows, nil
}
// SilenceUsageDataSource 即将过期 / 已过期套餐导出数据源(场景 package_silence_usage)。
type SilenceUsageDataSource struct {
query *packagesilencequery.Query
}
// NewSilenceUsageDataSource 创建沉默期套餐导出数据源。
func NewSilenceUsageDataSource(db *gorm.DB) *SilenceUsageDataSource {
return &SilenceUsageDataSource{query: packagesilencequery.NewQuery(db)}
}
// Scene 返回导出场景编码。
func (s *SilenceUsageDataSource) Scene() string { return constants.ExportTaskSceneSilenceUsage }
func (s *SilenceUsageDataSource) request(params ExportParams) dto.SilenceUsageListRequest {
req := dto.SilenceUsageListRequest{}
req.View, _ = filterString(params.Filters, "view")
if days, ok := filterInt(params.Filters, "within_days"); ok {
req.WithinDays = days
}
req.AssetIdentifier, _ = filterString(params.Filters, "asset_identifier")
if id, ok := filterUint(params.Filters, "package_id"); ok && id > 0 {
req.PackageID = &id
}
if id, ok := filterUint(params.Filters, "shop_id"); ok && id > 0 {
req.ShopID = &id
}
req.DeadlineStart, _ = filterString(params.Filters, "deadline_start")
req.DeadlineEnd, _ = filterString(params.Filters, "deadline_end")
return req
}
// Count 统计导出行数。
func (s *SilenceUsageDataSource) Count(ctx context.Context, params ExportParams) (int, error) {
query, err := s.query.UsageQuery(silenceExportContext(ctx, params), s.request(params))
if err != nil {
return 0, err
}
var total int64
if err := query.Count(&total).Error; err != nil {
return 0, err
}
return int(total), nil
}
// Headers 返回导出表头。
func (s *SilenceUsageDataSource) Headers(context.Context, ExportParams) ([]string, error) {
return silenceUsageHeaders, nil
}
// Fetch 按 offset/limit 返回导出行。
func (s *SilenceUsageDataSource) Fetch(ctx context.Context, params ExportParams, offset, limit int) ([][]string, error) {
if limit <= 0 {
return [][]string{}, nil
}
ctx = silenceExportContext(ctx, params)
query, err := s.query.UsageQuery(ctx, s.request(params))
if err != nil {
return nil, err
}
items, err := s.query.FetchUsages(ctx, query, offset, limit)
if err != nil {
return nil, err
}
rows := make([][]string, 0, len(items))
for _, item := range items {
view := item.Silence
if view == nil {
view = &dto.PackageSilenceView{}
}
rows = append(rows, []string{
strconv.FormatUint(uint64(item.PackageUsageID), 10), item.OrderNo, item.AssetType, item.AssetIdentifier, item.ShopName,
item.PackageName, item.CreatedAt.Format(exportTimeLayout), view.SilenceStatusName, formatOptionalInt(view.SilencePeriodDays),
formatOptionalTime(view.SilenceDeadlineAt), formatOptionalTime(view.SilenceExpiredAt), strconv.Itoa(view.SilenceExtendedDays),
strconv.Itoa(view.SilencePaidExtendedDays), strconv.Itoa(view.SilenceExtensionCount), strconv.Itoa(view.SilenceFreeExtensionUsed),
formatYesNo(view.SilenceAdjustPending),
})
}
return rows, nil
}
func formatOptionalMoney(value *int64) string {
if value == nil {
return ""
}
return formatMoneyYuan(*value)
}
func formatOptionalInt(value *int) string {
if value == nil {
return ""
}
return strconv.Itoa(*value)
}