Files
junhong_cmp_fiber/internal/exporter/order_scene.go
Break 1f634eb465
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m3s
init
2026-06-16 15:15:50 +08:00

600 lines
17 KiB
Go

package exporter
import (
"context"
"fmt"
"strings"
"time"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
const (
orderExportBaseHeaderCount = 11
orderExportCardGroupSize = 2
orderExportMinCardGroups = 1
)
// OrderDataSource 订单导出数据源。
type OrderDataSource struct {
db *gorm.DB
}
// NewOrderDataSource 创建订单导出数据源。
func NewOrderDataSource(db *gorm.DB) *OrderDataSource {
return &OrderDataSource{db: db}
}
// Scene 返回导出场景编码。
func (s *OrderDataSource) Scene() string {
return constants.ExportTaskSceneOrder
}
// Count 统计订单导出行数。
func (s *OrderDataSource) Count(ctx context.Context, params ExportParams) (int, error) {
query, err := s.applyFilters(ctx, s.baseQuery(ctx), 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 *OrderDataSource) Headers(ctx context.Context, params ExportParams) ([]string, error) {
cardGroups, err := s.resolveCardGroupCount(ctx, params)
if err != nil {
return nil, err
}
return buildOrderExportHeaders(cardGroups), nil
}
// Fetch 按 offset/limit 查询订单导出数据。
func (s *OrderDataSource) Fetch(ctx context.Context, params ExportParams, offset, limit int) ([][]string, error) {
if limit <= 0 {
return [][]string{}, nil
}
cardGroups := orderCardGroupCountFromHeaders(params.ResolvedHeaders)
if cardGroups <= 0 {
resolved, err := s.resolveCardGroupCount(ctx, params)
if err != nil {
return nil, err
}
cardGroups = resolved
}
orders, err := s.fetchOrders(ctx, params, offset, limit)
if err != nil {
return nil, err
}
if len(orders) == 0 {
return [][]string{}, nil
}
orderIDs := make([]uint, 0, len(orders))
deviceIDs := make([]uint, 0, len(orders))
for _, item := range orders {
orderIDs = append(orderIDs, item.ID)
if item.DeviceID != nil {
deviceIDs = append(deviceIDs, *item.DeviceID)
}
}
itemMap, err := s.fetchOrderItems(ctx, orderIDs)
if err != nil {
return nil, err
}
cardMap, err := s.fetchBoundCards(ctx, deviceIDs)
if err != nil {
return nil, err
}
paymentMap, err := s.fetchPreferredPayments(ctx, orderIDs)
if err != nil {
return nil, err
}
rows := make([][]string, 0, len(orders))
for _, item := range orders {
var cards []orderExportCardRow
if item.DeviceID != nil {
cards = cardMap[*item.DeviceID]
}
rows = append(rows, buildOrderExportRow(item, itemMap[item.ID], paymentMap[item.ID], cards, cardGroups))
}
return rows, nil
}
func (s *OrderDataSource) baseQuery(ctx context.Context) *gorm.DB {
return s.db.WithContext(ctx).
Table("tb_order AS o").
Where("o.deleted_at IS NULL")
}
// applyFilters 应用与 /api/admin/orders 当前实际行为一致的筛选条件。
// is_expired 在接口 Service 中未进入 Store 筛选,这里按当前接口行为忽略。
func (s *OrderDataSource) applyFilters(ctx context.Context, query *gorm.DB, params ExportParams) (*gorm.DB, error) {
query = s.applyOrderScope(query, params)
if params.UserType == constants.UserTypeAgent {
if params.CreatorShopID != nil {
query = query.Where("o.buyer_type = ? AND o.buyer_id = ?", model.BuyerTypeAgent, *params.CreatorShopID)
} else {
query = query.Where("o.buyer_type = ?", model.BuyerTypeAgent)
}
}
if paymentStatus, ok := filterInt(params.Filters, "payment_status"); ok {
query = query.Where("o.payment_status = ?", paymentStatus)
}
if paymentMethod, ok := filterString(params.Filters, "payment_method"); ok {
query = query.Where("o.payment_method = ?", paymentMethod)
}
if orderType, ok := filterString(params.Filters, "order_type"); ok {
query = query.Where("o.order_type = ?", orderType)
}
if orderNo, ok := filterString(params.Filters, "order_no"); ok {
query = query.Where("o.order_no = ?", orderNo)
}
if purchaseRole, ok := filterString(params.Filters, "purchase_role"); ok {
query = query.Where("o.purchase_role = ?", purchaseRole)
}
if sellerShopID, ok := filterUint(params.Filters, "seller_shop_id"); ok {
query = query.Where("o.seller_shop_id = ?", sellerShopID)
}
if start, ok := filterTime(params.Filters, "start_time"); ok {
query = query.Where("o.created_at >= ?", start)
}
if end, ok := filterTime(params.Filters, "end_time"); ok {
query = query.Where("o.created_at <= ?", end)
}
if buyerPhone, ok := filterString(params.Filters, "buyer_phone"); ok {
query = query.Where("o.buyer_phone = ?", buyerPhone)
}
if identifier, ok := filterString(params.Filters, "identifier"); ok {
iotCardID, deviceID, err := s.resolveOrderListAssetIdentifier(ctx, params, identifier)
if err != nil {
return nil, err
}
assetQuery := s.db.Where("o.asset_identifier = ?", identifier)
if iotCardID != nil {
assetQuery = assetQuery.Or("o.iot_card_id = ?", *iotCardID)
}
if deviceID != nil {
assetQuery = assetQuery.Or("o.device_id = ?", *deviceID)
}
query = query.Where(assetQuery)
}
return query, nil
}
func (s *OrderDataSource) applyOrderScope(query *gorm.DB, params ExportParams) *gorm.DB {
switch params.UserType {
case constants.UserTypeSuperAdmin, constants.UserTypePlatform:
return query
case constants.UserTypeAgent:
if len(params.ScopeShopIDs) == 0 {
return query.Where("1 = 0")
}
return query.Where(
s.db.Where("o.buyer_type = ? AND o.buyer_id IN ?", model.BuyerTypeAgent, params.ScopeShopIDs).
Or("o.seller_shop_id IN ?", params.ScopeShopIDs).
Or(s.db.Where("o.seller_shop_id IS NULL AND o.operator_type = ? AND o.operator_id IN ?", model.OperatorAccountTypeAgent, params.ScopeShopIDs)),
)
default:
return query.Where("1 = 0")
}
}
func (s *OrderDataSource) resolveCardGroupCount(ctx context.Context, params ExportParams) (int, error) {
filtered, err := s.applyFilters(ctx, s.baseQuery(ctx), params)
if err != nil {
return 0, err
}
var count int64
query := filtered.
Select("COALESCE(MAX(bound_cards.card_count), 0)").
Joins(`
LEFT JOIN LATERAL (
SELECT COUNT(*) AS card_count
FROM tb_device_sim_binding AS b
WHERE b.device_id = o.device_id
AND b.bind_status = ?
AND b.deleted_at IS NULL
) AS bound_cards ON TRUE
`, constants.BindStatusBound)
if err := query.Scan(&count).Error; err != nil {
return 0, err
}
if count < orderExportMinCardGroups {
return orderExportMinCardGroups, nil
}
return int(count), nil
}
func (s *OrderDataSource) fetchOrders(ctx context.Context, params ExportParams, offset, limit int) ([]orderExportRow, error) {
var orders []orderExportRow
filtered, err := s.applyFilters(ctx, s.baseQuery(ctx), params)
if err != nil {
return nil, err
}
query := filtered.
Joins("LEFT JOIN tb_shop AS sh ON sh.id = o.seller_shop_id").
Select(`
o.id,
o.order_no,
o.asset_identifier,
o.seller_cost_price,
o.total_amount,
o.actual_paid_amount,
o.payment_status,
o.payment_method,
o.created_at,
o.device_id,
COALESCE(sh.shop_name, '') AS shop_name
`).
Order("o.id DESC").
Limit(limit).
Offset(offset)
if err := query.Scan(&orders).Error; err != nil {
return nil, err
}
return orders, nil
}
func (s *OrderDataSource) fetchOrderItems(ctx context.Context, orderIDs []uint) (map[uint][]orderExportItemRow, error) {
result := make(map[uint][]orderExportItemRow, len(orderIDs))
if len(orderIDs) == 0 {
return result, nil
}
var items []orderExportItemRow
if err := s.db.WithContext(ctx).
Table("tb_order_item").
Select("order_id, package_name, quantity").
Where("order_id IN ?", orderIDs).
Where("deleted_at IS NULL").
Order("order_id ASC, id ASC").
Scan(&items).Error; err != nil {
return nil, err
}
for _, item := range items {
result[item.OrderID] = append(result[item.OrderID], item)
}
return result, nil
}
func (s *OrderDataSource) fetchBoundCards(ctx context.Context, deviceIDs []uint) (map[uint][]orderExportCardRow, error) {
result := make(map[uint][]orderExportCardRow, len(deviceIDs))
deviceIDs = normalizeUintSlice(deviceIDs)
if len(deviceIDs) == 0 {
return result, nil
}
var cards []orderExportCardRow
if err := s.db.WithContext(ctx).
Table("tb_device_sim_binding AS b").
Select(`
b.device_id,
b.slot_position,
c.msisdn,
c.iccid
`).
Joins("JOIN tb_iot_card AS c ON c.id = b.iot_card_id AND c.deleted_at IS NULL").
Where("b.device_id IN ?", deviceIDs).
Where("b.bind_status = ?", constants.BindStatusBound).
Where("b.deleted_at IS NULL").
Order("b.device_id ASC, b.slot_position ASC, b.id ASC").
Scan(&cards).Error; err != nil {
return nil, err
}
for _, card := range cards {
result[card.DeviceID] = append(result[card.DeviceID], card)
}
return result, nil
}
func (s *OrderDataSource) fetchPreferredPayments(ctx context.Context, orderIDs []uint) (map[uint]orderExportPaymentRow, error) {
result := make(map[uint]orderExportPaymentRow, len(orderIDs))
if len(orderIDs) == 0 {
return result, nil
}
var payments []orderExportPaymentRow
if err := s.db.WithContext(ctx).
Table("tb_payment").
Select("order_id, third_party_trade_no, status").
Where("order_id IN ?", orderIDs).
Where("order_type = ?", model.PaymentOrderTypePackage).
Where("deleted_at IS NULL").
Order("order_id ASC, CASE WHEN status = 1 THEN 0 ELSE 1 END ASC, created_at DESC, id DESC").
Scan(&payments).Error; err != nil {
return nil, err
}
for _, payment := range payments {
if _, exists := result[payment.OrderID]; exists {
continue
}
result[payment.OrderID] = payment
}
return result, nil
}
func (s *OrderDataSource) resolveOrderListAssetIdentifier(ctx context.Context, params ExportParams, identifier string) (*uint, *uint, error) {
if identifier == "" {
return nil, nil, nil
}
var registry orderExportAssetIdentifierRow
registryErr := s.db.WithContext(ctx).
Table("tb_asset_identifier").
Select("asset_type, asset_id").
Where("identifier = ?", identifier).
First(&registry).Error
if registryErr != nil && registryErr != gorm.ErrRecordNotFound {
return nil, nil, registryErr
}
if registryErr == nil {
switch registry.AssetType {
case model.AssetTypeIotCard:
if s.assetVisible(ctx, params, "tb_iot_card", registry.AssetID) {
cardID := registry.AssetID
return &cardID, nil, nil
}
case model.AssetTypeDevice:
if s.assetVisible(ctx, params, "tb_device", registry.AssetID) {
deviceID := registry.AssetID
return nil, &deviceID, nil
}
}
}
if deviceID, err := s.resolveDeviceIDByIdentifier(ctx, params, identifier); err != nil {
return nil, nil, err
} else if deviceID != nil {
return nil, deviceID, nil
}
cardID, err := s.resolveCardIDByIdentifier(ctx, params, identifier)
if err != nil {
return nil, nil, err
}
return cardID, nil, nil
}
func (s *OrderDataSource) assetVisible(ctx context.Context, params ExportParams, table string, id uint) bool {
var count int64
query := s.db.WithContext(ctx).Table(table).Where("id = ? AND deleted_at IS NULL", id)
if params.UserType == constants.UserTypeAgent {
if len(params.ScopeShopIDs) == 0 {
return false
}
query = query.Where("shop_id IN ?", params.ScopeShopIDs)
}
return query.Count(&count).Error == nil && count > 0
}
func (s *OrderDataSource) resolveDeviceIDByIdentifier(ctx context.Context, params ExportParams, identifier string) (*uint, error) {
var row struct {
ID uint `gorm:"column:id"`
}
query := s.db.WithContext(ctx).
Table("tb_device").
Select("id").
Where("deleted_at IS NULL").
Where("virtual_no = ? OR imei = ? OR sn = ?", identifier, identifier, identifier)
if params.UserType == constants.UserTypeAgent {
if len(params.ScopeShopIDs) == 0 {
return nil, nil
}
query = query.Where("shop_id IN ?", params.ScopeShopIDs)
}
err := query.First(&row).Error
if err == gorm.ErrRecordNotFound {
return nil, nil
}
if err != nil {
return nil, err
}
return &row.ID, nil
}
func (s *OrderDataSource) resolveCardIDByIdentifier(ctx context.Context, params ExportParams, identifier string) (*uint, error) {
conditions := []string{"virtual_no = ?", "msisdn = ?", "iccid = ?"}
args := []any{identifier, identifier, identifier}
if len(identifier) == 19 {
conditions = append(conditions, "iccid_19 = ?")
args = append(args, identifier)
} else if len(identifier) == 20 {
conditions = append(conditions, "iccid_20 = ?")
args = append(args, identifier)
}
var row struct {
ID uint `gorm:"column:id"`
}
query := s.db.WithContext(ctx).
Table("tb_iot_card").
Select("id").
Where("deleted_at IS NULL").
Where(strings.Join(conditions, " OR "), args...)
if params.UserType == constants.UserTypeAgent {
if len(params.ScopeShopIDs) == 0 {
return nil, nil
}
query = query.Where("shop_id IN ?", params.ScopeShopIDs)
}
err := query.First(&row).Error
if err == gorm.ErrRecordNotFound {
return nil, nil
}
if err != nil {
return nil, err
}
return &row.ID, nil
}
type orderExportRow struct {
ID uint `gorm:"column:id"`
OrderNo string `gorm:"column:order_no"`
ShopName string `gorm:"column:shop_name"`
AssetIdentifier string `gorm:"column:asset_identifier"`
SellerCostPrice int64 `gorm:"column:seller_cost_price"`
TotalAmount int64 `gorm:"column:total_amount"`
ActualPaidAmount *int64 `gorm:"column:actual_paid_amount"`
PaymentStatus int `gorm:"column:payment_status"`
PaymentMethod string `gorm:"column:payment_method"`
CreatedAt time.Time `gorm:"column:created_at"`
DeviceID *uint `gorm:"column:device_id"`
}
type orderExportItemRow struct {
OrderID uint `gorm:"column:order_id"`
PackageName string `gorm:"column:package_name"`
Quantity int `gorm:"column:quantity"`
}
type orderExportPaymentRow struct {
OrderID uint `gorm:"column:order_id"`
ThirdPartyTradeNo string `gorm:"column:third_party_trade_no"`
Status int `gorm:"column:status"`
}
type orderExportCardRow struct {
DeviceID uint `gorm:"column:device_id"`
SlotPosition int `gorm:"column:slot_position"`
MSISDN string `gorm:"column:msisdn"`
ICCID string `gorm:"column:iccid"`
}
type orderExportAssetIdentifierRow struct {
AssetType string `gorm:"column:asset_type"`
AssetID uint `gorm:"column:asset_id"`
}
func buildOrderExportHeaders(cardGroups int) []string {
if cardGroups < orderExportMinCardGroups {
cardGroups = orderExportMinCardGroups
}
headers := []string{
"订单号",
"店铺",
"虚拟号",
"套餐名称",
"成本价",
"下单金额",
"支付状态",
"支付方式",
"数量",
"下单时间",
"第三方单号",
}
for i := 1; i <= cardGroups; i++ {
headers = append(headers,
fmt.Sprintf("接入号%d", i),
fmt.Sprintf("ICCID%d", i),
)
}
return headers
}
func buildOrderExportRow(item orderExportRow, items []orderExportItemRow, payment orderExportPaymentRow, cards []orderExportCardRow, cardGroups int) []string {
if cardGroups < orderExportMinCardGroups {
cardGroups = orderExportMinCardGroups
}
row := []string{
item.OrderNo,
item.ShopName,
item.AssetIdentifier,
joinOrderPackageNames(items),
formatMoneyYuan(item.SellerCostPrice),
formatMoneyYuan(orderActualPaidAmount(item)),
constants.GetOrderPaymentStatusName(item.PaymentStatus),
formatOrderPaymentMethod(item.PaymentMethod),
formatOrderQuantity(items),
item.CreatedAt.Format(exportTimeLayout),
payment.ThirdPartyTradeNo,
}
for i := 0; i < cardGroups; i++ {
if i >= len(cards) {
row = append(row, "", "")
continue
}
row = append(row, cards[i].MSISDN, cards[i].ICCID)
}
return row
}
func orderCardGroupCountFromHeaders(headers []string) int {
if len(headers) < orderExportBaseHeaderCount {
return 0
}
cardColumnCount := len(headers) - orderExportBaseHeaderCount
if cardColumnCount <= 0 || cardColumnCount%orderExportCardGroupSize != 0 {
return 0
}
return cardColumnCount / orderExportCardGroupSize
}
func joinOrderPackageNames(items []orderExportItemRow) string {
if len(items) == 0 {
return ""
}
names := make([]string, 0, len(items))
for _, item := range items {
if item.PackageName != "" {
names = append(names, item.PackageName)
}
}
return strings.Join(names, ",")
}
func formatOrderQuantity(items []orderExportItemRow) string {
if len(items) == 0 {
return "0"
}
total := 0
for _, item := range items {
total += item.Quantity
}
return fmt.Sprintf("%d", total)
}
func orderActualPaidAmount(item orderExportRow) int64 {
if item.ActualPaidAmount != nil {
return *item.ActualPaidAmount
}
if item.PaymentStatus == model.PaymentStatusPaid {
return item.TotalAmount
}
return 0
}
func formatOrderPaymentMethod(method string) string {
switch method {
case model.PaymentMethodWallet:
return "钱包支付"
case model.PaymentMethodWechat:
return "微信支付"
case model.PaymentMethodAlipay:
return "支付宝支付"
case model.PaymentMethodOffline:
return "线下支付"
default:
return method
}
}