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(®istry).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 } }