七月迭代短暂完结,还有很多后端的关键东西没有弄,这是一版赶时间做的东西
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m26s

This commit is contained in:
2026-07-25 17:06:58 +08:00
parent ad9f613dd6
commit 73f5125d3d
249 changed files with 17137 additions and 877 deletions

View File

@@ -0,0 +1,190 @@
package exporter
import (
"context"
"time"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// AgentRechargeDataSource 代理充值导出数据源。
type AgentRechargeDataSource struct {
db *gorm.DB
}
// NewAgentRechargeDataSource 创建代理充值导出数据源。
func NewAgentRechargeDataSource(db *gorm.DB) *AgentRechargeDataSource {
return &AgentRechargeDataSource{db: db}
}
// Scene 返回导出场景编码。
func (s *AgentRechargeDataSource) Scene() string {
return constants.ExportTaskSceneAgentRecharge
}
// Count 统计代理充值导出行数。
func (s *AgentRechargeDataSource) Count(ctx context.Context, params ExportParams) (int, error) {
var total int64
query := s.applyFilters(s.baseQuery(ctx), params)
if err := query.Count(&total).Error; err != nil {
return 0, err
}
return int(total), nil
}
// Headers 返回代理充值导出表头。
func (s *AgentRechargeDataSource) Headers(context.Context, ExportParams) ([]string, error) {
return []string{
"充值单号", "店铺名称", "充值类型", "充值金额(元)", "实付金额(元)", "充值前余额(元)", "充值后余额(元)",
"状态", "支付方式", "运营备注", "驳回原因", "创建时间", "支付时间", "完成时间", "提交人", "审批来源", "审批状态", "支付凭证",
}, nil
}
// Fetch 按 offset/limit 查询代理充值导出数据。
func (s *AgentRechargeDataSource) Fetch(ctx context.Context, params ExportParams, offset, limit int) ([][]string, error) {
if limit <= 0 {
return [][]string{}, nil
}
var items []agentRechargeExportRow
query := s.applyFilters(s.baseQuery(ctx), params).
Select(`
r.recharge_no,
r.amount,
r.payment_method,
r.status,
r.remark,
COALESCE(r.rejection_reason, '') AS rejection_reason,
r.created_at,
r.paid_at,
r.completed_at,
COALESCE(sh.shop_name, '') AS shop_name,
COALESCE(ac.username, '') AS submitter_name,
ai.provider AS approval_provider,
ai.status AS approval_status,
wt.amount AS actual_amount,
wt.balance_before,
wt.balance_after,
COALESCE((SELECT string_agg(voucher.value, ',' ORDER BY voucher.ordinality)
FROM jsonb_array_elements_text(COALESCE(r.payment_voucher_key, '[]'::jsonb)) WITH ORDINALITY AS voucher(value, ordinality)), '') AS voucher_keys
`).
Joins("LEFT JOIN tb_shop AS sh ON sh.id = r.shop_id").
Joins("LEFT JOIN tb_account AS ac ON ac.id = r.user_id").
Joins("LEFT JOIN tb_approval_instance AS ai ON ai.id = r.approval_instance_id").
Joins(`LEFT JOIN LATERAL (
SELECT t.amount, t.balance_before, t.balance_after
FROM tb_agent_wallet_transaction AS t
WHERE t.reference_type = ?
AND t.reference_id = r.id
AND t.transaction_type = ?
AND t.status = ?
AND t.deleted_at IS NULL
ORDER BY t.id ASC
LIMIT 1
) AS wt ON TRUE`, constants.ReferenceTypeTopup, constants.AgentTransactionTypeRecharge, constants.TransactionStatusSuccess).
Order("r.id ASC").
Limit(limit).
Offset(offset)
if err := query.Scan(&items).Error; err != nil {
return nil, err
}
rows := make([][]string, 0, len(items))
for _, item := range items {
rows = append(rows, []string{
item.RechargeNo,
item.ShopName,
formatRechargeType(item.PaymentMethod),
formatMoneyYuan(item.Amount),
formatOptionalMoneyYuan(item.ActualAmount),
formatOptionalMoneyYuan(item.BalanceBefore),
formatOptionalMoneyYuan(item.BalanceAfter),
constants.GetRechargeStatusName(item.Status),
constants.GetBusinessPaymentMethodName(item.PaymentMethod),
item.Remark,
item.RejectionReason,
item.CreatedAt.Format(exportTimeLayout),
formatOptionalTime(item.PaidAt),
formatOptionalTime(item.CompletedAt),
item.SubmitterName,
formatApprovalProvider(item.ApprovalProvider),
formatOptionalApprovalStatus(item.ApprovalStatus),
item.VoucherKeys,
})
}
return rows, nil
}
func (s *AgentRechargeDataSource) baseQuery(ctx context.Context) *gorm.DB {
return s.db.WithContext(ctx).Table("tb_agent_recharge_record AS r").Where("r.deleted_at IS NULL")
}
func (s *AgentRechargeDataSource) applyFilters(query *gorm.DB, params ExportParams) *gorm.DB {
query = applyExportShopScope(query, params, "r.shop_id")
if shopID, ok := filterUint(params.Filters, "shop_id"); ok {
query = query.Where("r.shop_id = ?", shopID)
}
if status, ok := filterInt(params.Filters, "status"); ok {
query = query.Where("r.status = ?", status)
}
if start, ok := filterTime(params.Filters, "start_date"); ok {
query = query.Where("r.created_at >= ?", start)
}
if end, ok := filterEndDate(params.Filters, "end_date"); ok {
query = query.Where("r.created_at <= ?", end)
}
return query
}
type agentRechargeExportRow struct {
RechargeNo string `gorm:"column:recharge_no"`
ShopName string `gorm:"column:shop_name"`
Amount int64 `gorm:"column:amount"`
ActualAmount *int64 `gorm:"column:actual_amount"`
BalanceBefore *int64 `gorm:"column:balance_before"`
BalanceAfter *int64 `gorm:"column:balance_after"`
PaymentMethod string `gorm:"column:payment_method"`
Status int `gorm:"column:status"`
Remark string `gorm:"column:remark"`
RejectionReason string `gorm:"column:rejection_reason"`
CreatedAt time.Time `gorm:"column:created_at"`
PaidAt *time.Time `gorm:"column:paid_at"`
CompletedAt *time.Time `gorm:"column:completed_at"`
SubmitterName string `gorm:"column:submitter_name"`
ApprovalProvider *string `gorm:"column:approval_provider"`
ApprovalStatus *int `gorm:"column:approval_status"`
VoucherKeys string `gorm:"column:voucher_keys"`
}
func formatRechargeType(paymentMethod string) string {
if paymentMethod == constants.RechargeMethodOffline {
return "员工线下代充值"
}
return "在线充值"
}
func formatOptionalMoneyYuan(value *int64) string {
if value == nil {
return ""
}
return formatMoneyYuan(*value)
}
func formatApprovalProvider(provider *string) string {
if provider == nil || *provider == "" {
return ""
}
if *provider == constants.IntegrationProviderWeCom {
return "企业微信"
}
return *provider
}
func formatOptionalApprovalStatus(status *int) string {
if status == nil {
return ""
}
return constants.GetApprovalStatusName(*status)
}

View File

@@ -0,0 +1,168 @@
package exporter
import (
"context"
"strconv"
"time"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// AgentWalletTransactionDataSource 代理主钱包流水导出数据源。
type AgentWalletTransactionDataSource struct {
db *gorm.DB
}
// NewAgentWalletTransactionDataSource 创建代理主钱包流水导出数据源。
func NewAgentWalletTransactionDataSource(db *gorm.DB) *AgentWalletTransactionDataSource {
return &AgentWalletTransactionDataSource{db: db}
}
// Scene 返回导出场景编码。
func (s *AgentWalletTransactionDataSource) Scene() string {
return constants.ExportTaskSceneAgentWalletTransaction
}
// Count 统计代理主钱包流水导出行数。
func (s *AgentWalletTransactionDataSource) Count(ctx context.Context, params ExportParams) (int, error) {
var total int64
query := s.applyFilters(s.baseQuery(ctx), params)
if err := query.Count(&total).Error; err != nil {
return 0, err
}
return int(total), nil
}
// Headers 返回代理主钱包流水导出表头。
func (s *AgentWalletTransactionDataSource) Headers(context.Context, ExportParams) ([]string, error) {
return []string{
"店铺名称", "交易类型", "交易金额(元)", "状态", "资产类型", "资产标识", "交易时间",
"交易前金额(元)", "交易后金额(元)", "购买套餐名称", "操作人", "交易ID", "关联业务订单号", "交易渠道/支付方式",
}, nil
}
// Fetch 按 offset/limit 查询代理主钱包流水导出数据。
func (s *AgentWalletTransactionDataSource) Fetch(ctx context.Context, params ExportParams, offset, limit int) ([][]string, error) {
if limit <= 0 {
return [][]string{}, nil
}
var items []agentWalletTransactionExportRow
query := s.applyFilters(s.baseQuery(ctx), params).
Select(`
t.id,
t.transaction_type,
t.amount,
t.status,
t.asset_type,
t.asset_identifier,
t.created_at,
t.balance_before,
t.balance_after,
COALESCE(sh.shop_name, '') AS shop_name,
COALESCE(items.package_names, t.metadata ->> 'package_name', '') AS package_name,
COALESCE(ac.username, '') AS operator_name,
COALESCE(
CASE t.reference_type
WHEN ? THEN o.order_no
WHEN ? THEN ar.recharge_no
WHEN ? THEN rr.refund_no
WHEN ? THEN wr.withdrawal_no
WHEN ? THEN ex.exchange_no
END,
''
) AS business_order_no,
COALESCE(t.metadata ->> 'payment_method', '') AS payment_method
`, constants.ReferenceTypeOrder, constants.ReferenceTypeTopup, constants.ReferenceTypeRefund, constants.ReferenceTypeWithdrawal, constants.ReferenceTypeExchange).
Joins("LEFT JOIN tb_shop AS sh ON sh.id = t.shop_id").
Joins("LEFT JOIN tb_account AS ac ON ac.id = COALESCE(NULLIF(t.creator, 0), t.user_id)").
Joins("LEFT JOIN tb_order AS o ON t.reference_type = ? AND o.id = t.reference_id AND o.deleted_at IS NULL", constants.ReferenceTypeOrder).
Joins("LEFT JOIN tb_agent_recharge_record AS ar ON t.reference_type = ? AND ar.id = t.reference_id AND ar.deleted_at IS NULL", constants.ReferenceTypeTopup).
Joins("LEFT JOIN tb_refund_request AS rr ON t.reference_type = ? AND rr.id = t.reference_id AND rr.deleted_at IS NULL", constants.ReferenceTypeRefund).
Joins("LEFT JOIN tb_commission_withdrawal_request AS wr ON t.reference_type = ? AND wr.id = t.reference_id AND wr.deleted_at IS NULL", constants.ReferenceTypeWithdrawal).
Joins("LEFT JOIN tb_exchange_order AS ex ON t.reference_type = ? AND ex.id = t.reference_id AND ex.deleted_at IS NULL", constants.ReferenceTypeExchange).
Joins(`LEFT JOIN LATERAL (
SELECT string_agg(oi.package_name, ',' ORDER BY oi.id) AS package_names
FROM tb_order_item AS oi
WHERE t.reference_type = ?
AND oi.order_id = t.reference_id
AND oi.deleted_at IS NULL
) AS items ON TRUE`, constants.ReferenceTypeOrder).
Order("t.id ASC").
Limit(limit).
Offset(offset)
if err := query.Scan(&items).Error; err != nil {
return nil, err
}
rows := make([][]string, 0, len(items))
for _, item := range items {
paymentMethod := item.PaymentMethod
if paymentMethod == "" && item.TransactionType == constants.AgentTransactionTypeDeduct {
paymentMethod = constants.PaymentMethodWallet
}
rows = append(rows, []string{
item.ShopName,
constants.GetAgentTransactionTypeName(item.TransactionType),
formatMoneyYuan(item.Amount),
constants.GetTransactionStatusName(item.Status),
constants.GetWalletAssetTypeName(item.AssetType),
item.AssetIdentifier,
item.CreatedAt.Format(exportTimeLayout),
formatMoneyYuan(item.BalanceBefore),
formatMoneyYuan(item.BalanceAfter),
item.PackageName,
item.OperatorName,
strconv.FormatUint(uint64(item.ID), 10),
item.BusinessOrderNo,
constants.GetBusinessPaymentMethodName(paymentMethod),
})
}
return rows, nil
}
func (s *AgentWalletTransactionDataSource) baseQuery(ctx context.Context) *gorm.DB {
return s.db.WithContext(ctx).
Table("tb_agent_wallet_transaction AS t").
Joins("INNER JOIN tb_agent_wallet AS w ON w.id = t.agent_wallet_id AND w.wallet_type = ? AND w.deleted_at IS NULL", constants.AgentWalletTypeMain).
Where("t.deleted_at IS NULL")
}
func (s *AgentWalletTransactionDataSource) applyFilters(query *gorm.DB, params ExportParams) *gorm.DB {
query = applyExportShopScope(query, params, "t.shop_id")
if shopID, ok := filterUint(params.Filters, "shop_id"); ok {
query = query.Where("t.shop_id = ?", shopID)
}
if transactionType, ok := filterString(params.Filters, "transaction_type"); ok {
query = query.Where("t.transaction_type = ?", transactionType)
}
if start, ok := filterTime(params.Filters, "start_date"); ok {
query = query.Where("t.created_at >= ?", start)
}
if end, ok := filterEndDate(params.Filters, "end_date"); ok {
query = query.Where("t.created_at <= ?", end)
}
if assetIdentifier, ok := filterString(params.Filters, "asset_identifier"); ok {
query = query.Where("t.asset_identifier = ?", assetIdentifier)
}
return query
}
type agentWalletTransactionExportRow struct {
ID uint `gorm:"column:id"`
ShopName string `gorm:"column:shop_name"`
TransactionType string `gorm:"column:transaction_type"`
Amount int64 `gorm:"column:amount"`
Status int `gorm:"column:status"`
AssetType string `gorm:"column:asset_type"`
AssetIdentifier string `gorm:"column:asset_identifier"`
CreatedAt time.Time `gorm:"column:created_at"`
BalanceBefore int64 `gorm:"column:balance_before"`
BalanceAfter int64 `gorm:"column:balance_after"`
PackageName string `gorm:"column:package_name"`
OperatorName string `gorm:"column:operator_name"`
BusinessOrderNo string `gorm:"column:business_order_no"`
PaymentMethod string `gorm:"column:payment_method"`
}

View File

@@ -0,0 +1,176 @@
package exporter
import (
"context"
"time"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// ExchangeDataSource 换货记录导出数据源。
type ExchangeDataSource struct {
db *gorm.DB
}
// NewExchangeDataSource 创建换货记录导出数据源。
func NewExchangeDataSource(db *gorm.DB) *ExchangeDataSource {
return &ExchangeDataSource{db: db}
}
// Scene 返回导出场景编码。
func (s *ExchangeDataSource) Scene() string {
return constants.ExportTaskSceneExchange
}
// Count 统计换货记录导出行数。
func (s *ExchangeDataSource) Count(ctx context.Context, params ExportParams) (int, error) {
var total int64
query := s.applyFilters(s.baseQuery(ctx), params)
if err := query.Count(&total).Error; err != nil {
return 0, err
}
return int(total), nil
}
// Headers 返回换货记录导出表头。
func (s *ExchangeDataSource) Headers(context.Context, ExportParams) ([]string, error) {
return []string{
"换货单号", "换货类型", "换货原因", "问题描述/备注", "旧资产类型", "旧资产标识符", "新资产标识符",
"收货人姓名", "收货人电话", "收货地址", "快递公司", "快递单号", "状态", "创建人", "创建时间",
}, nil
}
// Fetch 按 offset/limit 查询换货记录导出数据。
func (s *ExchangeDataSource) Fetch(ctx context.Context, params ExportParams, offset, limit int) ([][]string, error) {
if limit <= 0 {
return [][]string{}, nil
}
var items []exchangeExportRow
query := s.applyFilters(s.baseQuery(ctx), params).
Select(`
e.exchange_no,
e.flow_type,
e.exchange_reason,
COALESCE(e.remark, '') AS remark,
e.old_asset_type,
e.old_asset_identifier,
e.new_asset_identifier,
e.recipient_name,
e.recipient_phone,
e.recipient_address,
e.express_company,
e.express_no,
e.status,
e.created_at,
COALESCE(ac.username, '') AS creator_name
`).
Joins("LEFT JOIN tb_account AS ac ON ac.id = e.creator").
Order("e.id ASC").
Limit(limit).
Offset(offset)
if err := query.Scan(&items).Error; err != nil {
return nil, err
}
rows := make([][]string, 0, len(items))
for _, item := range items {
rows = append(rows, []string{
item.ExchangeNo,
constants.GetExchangeFlowTypeName(item.FlowType),
item.ExchangeReason,
item.Remark,
formatExchangeAssetType(item.OldAssetType),
item.OldAssetIdentifier,
item.NewAssetIdentifier,
item.RecipientName,
item.RecipientPhone,
item.RecipientAddress,
item.ExpressCompany,
item.ExpressNo,
constants.GetExchangeStatusName(item.Status),
item.CreatorName,
item.CreatedAt.Format(exportTimeLayout),
})
}
return rows, nil
}
func (s *ExchangeDataSource) baseQuery(ctx context.Context) *gorm.DB {
return s.db.WithContext(ctx).Table("tb_exchange_order AS e").Where("e.deleted_at IS NULL")
}
func (s *ExchangeDataSource) applyFilters(query *gorm.DB, params ExportParams) *gorm.DB {
query = applyExportShopScope(query, params, "e.shop_id")
if status, ok := filterInt(params.Filters, "status"); ok {
query = query.Where("e.status = ?", status)
}
if flowType, ok := filterString(params.Filters, "flow_type"); ok {
query = query.Where("COALESCE(NULLIF(e.flow_type, ''), ?) = ?", constants.ExchangeFlowTypeShipping, flowType)
}
query = applyExchangeAssetKeyword(query, "old", filterValue(params.Filters, "old_asset_keyword"))
query = applyExchangeAssetKeyword(query, "new", filterValue(params.Filters, "new_asset_keyword"))
if start, ok := filterTime(params.Filters, "created_at_start"); ok {
query = query.Where("e.created_at >= ?", start)
}
if end, ok := filterTime(params.Filters, "created_at_end"); ok {
query = query.Where("e.created_at <= ?", end)
}
return query
}
func applyExchangeAssetKeyword(query *gorm.DB, side, keyword string) *gorm.DB {
if keyword == "" {
return query
}
like := "%" + keyword + "%"
cardIDs := query.Session(&gorm.Session{NewDB: true}).Table("tb_iot_card").Select("id").
Where("deleted_at IS NULL").
Where("iccid LIKE ? OR msisdn LIKE ? OR virtual_no LIKE ?", like, like, like)
deviceIDs := query.Session(&gorm.Session{NewDB: true}).Table("tb_device").Select("id").
Where("deleted_at IS NULL").
Where("virtual_no LIKE ? OR imei LIKE ? OR sn LIKE ?", like, like, like)
prefix := "e." + side
return query.Where(
"("+prefix+"_asset_type = ? AND "+prefix+"_asset_id IN (?)) OR ("+prefix+"_asset_type = ? AND "+prefix+"_asset_id IN (?))",
constants.ExchangeAssetTypeIotCard, cardIDs, constants.ExchangeAssetTypeDevice, deviceIDs,
)
}
func filterValue(filters map[string]any, key string) string {
value, _ := filterString(filters, key)
return value
}
func formatExchangeAssetType(assetType string) string {
switch assetType {
case constants.ExchangeAssetTypeIotCard:
return "物联网卡"
case constants.ExchangeAssetTypeDevice:
return "设备"
case "":
return ""
default:
return "未知"
}
}
type exchangeExportRow struct {
ExchangeNo string `gorm:"column:exchange_no"`
FlowType string `gorm:"column:flow_type"`
ExchangeReason string `gorm:"column:exchange_reason"`
Remark string `gorm:"column:remark"`
OldAssetType string `gorm:"column:old_asset_type"`
OldAssetIdentifier string `gorm:"column:old_asset_identifier"`
NewAssetIdentifier string `gorm:"column:new_asset_identifier"`
RecipientName string `gorm:"column:recipient_name"`
RecipientPhone string `gorm:"column:recipient_phone"`
RecipientAddress string `gorm:"column:recipient_address"`
ExpressCompany string `gorm:"column:express_company"`
ExpressNo string `gorm:"column:express_no"`
Status int `gorm:"column:status"`
CreatorName string `gorm:"column:creator_name"`
CreatedAt time.Time `gorm:"column:created_at"`
}

View File

@@ -255,6 +255,18 @@ func filterTime(filters map[string]any, key string) (time.Time, bool) {
return time.Time{}, false
}
// filterEndDate 解析截止时间;纯日期输入覆盖到当天结束,带时分秒输入保持原值。
func filterEndDate(filters map[string]any, key string) (time.Time, bool) {
text, ok := filterString(filters, key)
if !ok {
return time.Time{}, false
}
if parsed, err := time.ParseInLocation("2006-01-02", text, time.Local); err == nil {
return parsed.Add(24*time.Hour - time.Nanosecond), true
}
return filterTime(filters, key)
}
func formatOptionalUint(value *uint) string {
if value == nil {
return ""

View File

@@ -2,6 +2,7 @@ package exporter
import (
"context"
"strconv"
"time"
"gorm.io/gorm"
@@ -36,7 +37,7 @@ func (s *IotCardDataSource) Count(ctx context.Context, params ExportParams) (int
// Headers 返回 IoT 卡导出表头。
func (s *IotCardDataSource) Headers(ctx context.Context, params ExportParams) ([]string, error) {
return []string{"ICCID", "MSISDN", "绑定设备虚拟号", "运营商", "店铺名称", "绑定设备名称", "是否实名", "实名时间", "网络状态"}, nil
return []string{"ICCID", "MSISDN", "绑定设备虚拟号", "运营商", "店铺名称", "绑定设备名称", "是否实名", "实名时间", "网络状态", "套餐名称", "使用流量(MB)", "剩余流量(MB)"}, nil
}
// Fetch 按 offset/limit 查询 IoT 卡导出数据。
@@ -56,7 +57,10 @@ func (s *IotCardDataSource) Fetch(ctx context.Context, params ExportParams, offs
COALESCE(d.device_name, '') AS device_name,
c.real_name_status,
c.first_realname_at,
c.network_status
c.network_status,
COALESCE(pkg.package_name, '') AS package_name,
COALESCE(pkg.data_usage_mb, 0) AS data_usage_mb,
COALESCE(pkg.data_limit_mb, 0) AS data_limit_mb
`).
Order("c.id ASC").
Limit(limit).
@@ -77,6 +81,9 @@ func (s *IotCardDataSource) Fetch(ctx context.Context, params ExportParams, offs
formatRealNameVerified(item.RealNameStatus),
formatOptionalTime(item.FirstRealnameAt),
constants.GetNetworkStatusName(item.NetworkStatus),
item.PackageName,
strconv.FormatInt(item.DataUsageMB, 10),
strconv.FormatInt(remainingPackageDataMB(item.DataLimitMB, item.DataUsageMB), 10),
})
}
return rows, nil
@@ -100,6 +107,22 @@ func (s *IotCardDataSource) baseQuery(ctx context.Context) *gorm.DB {
LIMIT 1
) AS d ON TRUE
`, constants.BindStatusBound).
Joins(`
LEFT JOIN LATERAL (
SELECT
COALESCE(NULLIF(pu.package_name, ''), p.package_name, '') AS package_name,
pu.data_usage_mb,
pu.data_limit_mb
FROM tb_package_usage AS pu
LEFT JOIN tb_package AS p ON p.id = pu.package_id AND p.deleted_at IS NULL
WHERE pu.iot_card_id = c.id
AND pu.status = ?
AND pu.master_usage_id IS NULL
AND pu.deleted_at IS NULL
ORDER BY pu.priority ASC, pu.activated_at ASC, pu.id ASC
LIMIT 1
) AS pkg ON TRUE
`, constants.PackageUsageStatusActive).
Where("c.deleted_at IS NULL")
}
@@ -206,6 +229,17 @@ type iotCardExportRow struct {
RealNameStatus int `gorm:"column:real_name_status"`
FirstRealnameAt *time.Time `gorm:"column:first_realname_at"`
NetworkStatus int `gorm:"column:network_status"`
PackageName string `gorm:"column:package_name"`
DataUsageMB int64 `gorm:"column:data_usage_mb"`
DataLimitMB int64 `gorm:"column:data_limit_mb"`
}
func remainingPackageDataMB(limit, used int64) int64 {
remaining := limit - used
if remaining < 0 {
return 0
}
return remaining
}
func formatRealNameVerified(status int) string {

View File

@@ -0,0 +1,228 @@
package exporter
import (
"context"
"strconv"
"time"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// PackageDataSource 套餐导出数据源。
type PackageDataSource struct {
db *gorm.DB
}
// NewPackageDataSource 创建套餐导出数据源。
func NewPackageDataSource(db *gorm.DB) *PackageDataSource {
return &PackageDataSource{db: db}
}
// Scene 返回导出场景编码。
func (s *PackageDataSource) Scene() string {
return constants.ExportTaskScenePackage
}
// Count 统计套餐导出行数。
func (s *PackageDataSource) Count(ctx context.Context, params ExportParams) (int, error) {
var total int64
query := s.applyFilters(s.baseQuery(ctx, params), params)
if err := query.Count(&total).Error; err != nil {
return 0, err
}
return int(total), nil
}
// Headers 返回套餐导出固定表头。
func (s *PackageDataSource) Headers(context.Context, ExportParams) ([]string, error) {
return []string{
"套餐编码", "套餐名称", "套餐系列名称", "套餐类型", "套餐时长(月)",
"套餐时长说明", "套餐周期类型", "套餐天数", "真流量额度(MB)", "虚流量额度(MB)",
"是否启用虚流量", "虚流量比例", "流量重置周期", "到期时间基准", "成本价(元)",
"建议售价(元)", "价格配置状态", "状态", "上架状态", "是否赠送套餐",
"创建人ID", "更新人ID", "创建时间", "更新时间", "删除时间",
}, nil
}
// Fetch 按 offset/limit 查询套餐导出数据。
func (s *PackageDataSource) Fetch(ctx context.Context, params ExportParams, offset, limit int) ([][]string, error) {
if limit <= 0 {
return [][]string{}, nil
}
var items []packageExportRow
query := s.applyFilters(s.baseQuery(ctx, params), params).
Joins("LEFT JOIN tb_package_series AS ps ON ps.id = p.series_id AND ps.deleted_at IS NULL").
Select(s.selectColumns(params)).
Order("p.id ASC").
Limit(limit).
Offset(offset)
if err := query.Scan(&items).Error; err != nil {
return nil, err
}
rows := make([][]string, 0, len(items))
for _, item := range items {
rows = append(rows, buildPackageExportRow(item))
}
return rows, nil
}
func (s *PackageDataSource) baseQuery(ctx context.Context, params ExportParams) *gorm.DB {
query := s.db.WithContext(ctx).Table("tb_package AS p").Where("p.deleted_at IS NULL")
switch params.UserType {
case constants.UserTypeSuperAdmin, constants.UserTypePlatform:
return query
case constants.UserTypeAgent:
if params.CreatorShopID == nil || *params.CreatorShopID == 0 {
return query.Where("1 = 0")
}
return query.
Joins(`INNER JOIN tb_shop_package_allocation AS a
ON a.package_id = p.id
AND a.shop_id = ?
AND a.status = ?
AND a.deleted_at IS NULL`, *params.CreatorShopID, constants.StatusEnabled).
Where("p.is_gift = ?", false)
default:
return query.Where("1 = 0")
}
}
func (s *PackageDataSource) applyFilters(query *gorm.DB, params ExportParams) *gorm.DB {
if packageName, ok := filterString(params.Filters, "package_name"); ok {
query = query.Where("p.package_name LIKE ?", "%"+packageName+"%")
}
if seriesID, ok := filterUint(params.Filters, "series_id"); ok {
query = query.Where("p.series_id = ?", seriesID)
}
if status, ok := filterInt(params.Filters, "status"); ok {
query = query.Where("p.status = ?", status)
}
if shelfStatus, ok := filterInt(params.Filters, "shelf_status"); ok {
if params.UserType == constants.UserTypeAgent {
query = query.Where("a.shelf_status = ?", shelfStatus)
} else {
query = query.Where("p.shelf_status = ?", shelfStatus)
}
}
if packageType, ok := filterString(params.Filters, "package_type"); ok {
query = query.Where("p.package_type = ?", packageType)
}
return query
}
func (s *PackageDataSource) selectColumns(params ExportParams) string {
costPriceColumn := "p.cost_price"
shelfStatusColumn := "p.shelf_status"
if params.UserType == constants.UserTypeAgent {
costPriceColumn = "a.cost_price"
shelfStatusColumn = "a.shelf_status"
}
return `
p.package_code,
p.package_name,
COALESCE(ps.series_name, '') AS series_name,
p.package_type,
p.duration_months,
p.calendar_type,
p.duration_days,
p.real_data_mb,
p.virtual_data_mb,
p.enable_virtual_data,
p.virtual_ratio,
p.data_reset_cycle,
p.expiry_base,
` + costPriceColumn + ` AS cost_price,
p.suggested_retail_price,
p.price_config_status,
p.status,
` + shelfStatusColumn + ` AS shelf_status,
p.is_gift,
p.creator,
p.updater,
p.created_at,
p.updated_at`
}
type packageExportRow struct {
PackageCode string `gorm:"column:package_code"`
PackageName string `gorm:"column:package_name"`
SeriesName string `gorm:"column:series_name"`
PackageType string `gorm:"column:package_type"`
DurationMonths int `gorm:"column:duration_months"`
CalendarType string `gorm:"column:calendar_type"`
DurationDays int `gorm:"column:duration_days"`
RealDataMB int64 `gorm:"column:real_data_mb"`
VirtualDataMB int64 `gorm:"column:virtual_data_mb"`
EnableVirtualData bool `gorm:"column:enable_virtual_data"`
VirtualRatio float64 `gorm:"column:virtual_ratio"`
DataResetCycle string `gorm:"column:data_reset_cycle"`
ExpiryBase string `gorm:"column:expiry_base"`
CostPrice int64 `gorm:"column:cost_price"`
SuggestedRetailPrice int64 `gorm:"column:suggested_retail_price"`
PriceConfigStatus int `gorm:"column:price_config_status"`
Status int `gorm:"column:status"`
ShelfStatus int `gorm:"column:shelf_status"`
IsGift bool `gorm:"column:is_gift"`
Creator uint `gorm:"column:creator"`
Updater uint `gorm:"column:updater"`
CreatedAt time.Time `gorm:"column:created_at"`
UpdatedAt time.Time `gorm:"column:updated_at"`
}
func buildPackageExportRow(item packageExportRow) []string {
return []string{
item.PackageCode,
item.PackageName,
item.SeriesName,
constants.GetPackageTypeName(item.PackageType),
strconv.Itoa(item.DurationMonths),
formatPackageDurationDescription(item),
constants.GetPackageCalendarTypeName(item.CalendarType),
strconv.Itoa(item.DurationDays),
strconv.FormatInt(item.RealDataMB, 10),
strconv.FormatInt(item.VirtualDataMB, 10),
formatYesNo(item.EnableVirtualData),
strconv.FormatFloat(item.VirtualRatio, 'f', 6, 64),
constants.GetPackageDataResetCycleName(item.DataResetCycle),
constants.GetPackageExpiryBaseName(item.ExpiryBase),
formatMoneyYuan(item.CostPrice),
formatPackageSuggestedRetailPrice(item),
constants.GetPackagePriceConfigStatusName(item.PriceConfigStatus),
constants.GetStatusName(item.Status),
constants.GetShelfStatusName(item.ShelfStatus),
formatYesNo(item.IsGift),
strconv.FormatUint(uint64(item.Creator), 10),
strconv.FormatUint(uint64(item.Updater), 10),
item.CreatedAt.Format(exportTimeLayout),
item.UpdatedAt.Format(exportTimeLayout),
"",
}
}
func formatPackageDurationDescription(item packageExportRow) string {
if item.CalendarType == constants.PackageCalendarTypeByDay && item.DurationDays > 0 {
return strconv.Itoa(item.DurationDays) + "天"
}
if item.DurationMonths > 0 {
return strconv.Itoa(item.DurationMonths) + "个月"
}
return ""
}
func formatPackageSuggestedRetailPrice(item packageExportRow) string {
if item.PriceConfigStatus == constants.PackagePriceConfigStatusUnconfigured {
return ""
}
return formatMoneyYuan(item.SuggestedRetailPrice)
}
func formatYesNo(value bool) string {
if value {
return "是"
}
return "否"
}

View File

@@ -0,0 +1,206 @@
package exporter
import (
"context"
"time"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// RefundDataSource 退款记录导出数据源。
type RefundDataSource struct {
db *gorm.DB
}
// NewRefundDataSource 创建退款记录导出数据源。
func NewRefundDataSource(db *gorm.DB) *RefundDataSource {
return &RefundDataSource{db: db}
}
// Scene 返回导出场景编码。
func (s *RefundDataSource) Scene() string {
return constants.ExportTaskSceneRefund
}
// Count 统计退款记录导出行数。
func (s *RefundDataSource) Count(ctx context.Context, params ExportParams) (int, error) {
var total int64
query := s.applyFilters(s.baseQuery(ctx), params)
if err := query.Count(&total).Error; err != nil {
return 0, err
}
return int(total), nil
}
// Headers 返回退款记录导出表头。
func (s *RefundDataSource) Headers(context.Context, ExportParams) ([]string, error) {
return []string{
"退款单号", "代理店铺名称", "关联的支付订单号", "资产类型", "资产标识", "套餐名称", "原订单金额(元)",
"实收金额(元)", "可退金额(元)", "申请退款金额(元)", "实际退款金额(元)", "状态", "退款原因", "审批备注",
"审批来源", "审批状态", "退款处理状态", "退款申请时间", "退款审批时间", "提交人", "退款凭证",
}, nil
}
// Fetch 按 offset/limit 查询退款记录导出数据。
func (s *RefundDataSource) Fetch(ctx context.Context, params ExportParams, offset, limit int) ([][]string, error) {
if limit <= 0 {
return [][]string{}, nil
}
var items []refundExportRow
query := s.applyFilters(s.baseQuery(ctx), params).
Select(`
r.refund_no,
r.order_no,
r.order_type,
r.asset_identifier,
r.actual_received_amount,
r.requested_refund_amount,
r.approved_refund_amount,
r.status,
r.refund_reason,
r.remark,
r.commission_deducted,
r.asset_reset,
r.created_at,
r.processed_at,
COALESCE(sh.shop_name, '') AS shop_name,
o.total_amount AS original_amount,
o.actual_paid_amount AS refundable_amount,
COALESCE(pu.package_name, items.package_names, '') AS package_name,
COALESCE(ac.username, '') AS submitter_name,
ai.provider AS approval_provider,
ai.status AS approval_status,
COALESCE((SELECT string_agg(voucher.value, ',' ORDER BY voucher.ordinality)
FROM jsonb_array_elements_text(COALESCE(r.refund_voucher_key, '[]'::jsonb)) WITH ORDINALITY AS voucher(value, ordinality)), '') AS voucher_keys
`).
Joins("LEFT JOIN tb_shop AS sh ON sh.id = r.shop_id").
Joins("LEFT JOIN tb_order AS o ON o.id = r.order_id AND o.deleted_at IS NULL").
Joins("LEFT JOIN tb_package_usage AS pu ON pu.id = r.package_usage_id AND pu.deleted_at IS NULL").
Joins("LEFT JOIN tb_account AS ac ON ac.id = r.creator").
Joins("LEFT JOIN tb_approval_instance AS ai ON ai.id = r.approval_instance_id").
Joins(`LEFT JOIN LATERAL (
SELECT string_agg(oi.package_name, ',' ORDER BY oi.id) AS package_names
FROM tb_order_item AS oi
WHERE oi.order_id = r.order_id AND oi.deleted_at IS NULL
) AS items ON TRUE`).
Order("r.id ASC").
Limit(limit).
Offset(offset)
if err := query.Scan(&items).Error; err != nil {
return nil, err
}
rows := make([][]string, 0, len(items))
for _, item := range items {
rows = append(rows, []string{
item.RefundNo,
item.ShopName,
item.OrderNo,
formatRefundAssetType(item.OrderType),
item.AssetIdentifier,
item.PackageName,
formatOptionalMoneyYuan(item.OriginalAmount),
formatMoneyYuan(item.ActualReceivedAmount),
formatOptionalMoneyYuan(item.RefundableAmount),
formatMoneyYuan(item.RequestedRefundAmount),
formatOptionalMoneyYuan(item.ApprovedRefundAmount),
constants.GetRefundStatusName(item.Status),
item.RefundReason,
item.Remark,
formatRefundApprovalSource(item.ApprovalProvider),
formatOptionalApprovalStatus(item.ApprovalStatus),
formatRefundProcessingStatus(item.Status, item.CommissionDeducted, item.AssetReset),
item.CreatedAt.Format(exportTimeLayout),
formatOptionalTime(item.ProcessedAt),
item.SubmitterName,
item.VoucherKeys,
})
}
return rows, nil
}
func (s *RefundDataSource) baseQuery(ctx context.Context) *gorm.DB {
return s.db.WithContext(ctx).Table("tb_refund_request AS r").Where("r.deleted_at IS NULL")
}
func (s *RefundDataSource) applyFilters(query *gorm.DB, params ExportParams) *gorm.DB {
query = applyExportShopScope(query, params, "r.shop_id")
if status, ok := filterInt(params.Filters, "status"); ok {
query = query.Where("r.status = ?", status)
}
if orderID, ok := filterUint(params.Filters, "order_id"); ok {
query = query.Where("r.order_id = ?", orderID)
}
if shopID, ok := filterUint(params.Filters, "shop_id"); ok {
query = query.Where("r.shop_id = ?", shopID)
}
if identifier, ok := filterString(params.Filters, "asset_identifier"); ok {
query = query.Where("r.asset_identifier = ?", identifier)
}
return query
}
type refundExportRow struct {
RefundNo string `gorm:"column:refund_no"`
ShopName string `gorm:"column:shop_name"`
OrderNo string `gorm:"column:order_no"`
OrderType string `gorm:"column:order_type"`
AssetIdentifier string `gorm:"column:asset_identifier"`
PackageName string `gorm:"column:package_name"`
OriginalAmount *int64 `gorm:"column:original_amount"`
ActualReceivedAmount int64 `gorm:"column:actual_received_amount"`
RefundableAmount *int64 `gorm:"column:refundable_amount"`
RequestedRefundAmount int64 `gorm:"column:requested_refund_amount"`
ApprovedRefundAmount *int64 `gorm:"column:approved_refund_amount"`
Status int `gorm:"column:status"`
RefundReason string `gorm:"column:refund_reason"`
Remark string `gorm:"column:remark"`
ApprovalProvider *string `gorm:"column:approval_provider"`
ApprovalStatus *int `gorm:"column:approval_status"`
CommissionDeducted bool `gorm:"column:commission_deducted"`
AssetReset bool `gorm:"column:asset_reset"`
CreatedAt time.Time `gorm:"column:created_at"`
ProcessedAt *time.Time `gorm:"column:processed_at"`
SubmitterName string `gorm:"column:submitter_name"`
VoucherKeys string `gorm:"column:voucher_keys"`
}
func formatRefundAssetType(orderType string) string {
switch orderType {
case model.OrderTypeSingleCard:
return "物联网卡"
case model.OrderTypeDevice:
return "设备"
case "":
return ""
default:
return "未知"
}
}
func formatRefundApprovalSource(provider *string) string {
if provider == nil || *provider == "" {
return "历史审批"
}
return formatApprovalProvider(provider)
}
func formatRefundProcessingStatus(status int, commissionDeducted, assetReset bool) string {
switch status {
case model.RefundStatusPending, model.RefundStatusReturned:
return "待审批"
case model.RefundStatusRejected:
return "无需处理"
case model.RefundStatusApproved:
if commissionDeducted && assetReset {
return "已完成"
}
return "处理中"
default:
return "未知"
}
}

View File

@@ -31,6 +31,11 @@ func NewDefaultRegistry(db *gorm.DB) *Registry {
NewDeviceDataSource(db),
NewIotCardDataSource(db),
NewOrderDataSource(db),
NewPackageDataSource(db),
NewAgentWalletTransactionDataSource(db),
NewAgentRechargeDataSource(db),
NewRefundDataSource(db),
NewExchangeDataSource(db),
)
}
@@ -59,7 +64,14 @@ func (r *Registry) Scenes() []string {
// IsSupportedScene 判断是否为受支持的场景。
func IsSupportedScene(scene string) bool {
switch scene {
case constants.ExportTaskSceneDevice, constants.ExportTaskSceneIotCard, constants.ExportTaskSceneOrder:
case constants.ExportTaskSceneDevice,
constants.ExportTaskSceneIotCard,
constants.ExportTaskSceneOrder,
constants.ExportTaskScenePackage,
constants.ExportTaskSceneAgentWalletTransaction,
constants.ExportTaskSceneAgentRecharge,
constants.ExportTaskSceneRefund,
constants.ExportTaskSceneExchange:
return true
default:
return false