package exporter import ( "context" "time" "gorm.io/gorm" "github.com/break/junhong_cmp_fiber/pkg/constants" ) // IotCardDataSource IoT 卡导出数据源。 type IotCardDataSource struct { db *gorm.DB } // NewIotCardDataSource 创建 IoT 卡导出数据源。 func NewIotCardDataSource(db *gorm.DB) *IotCardDataSource { return &IotCardDataSource{db: db} } // Scene 返回导出场景编码。 func (s *IotCardDataSource) Scene() string { return constants.ExportTaskSceneIotCard } // Count 统计 IoT 卡导出行数。 func (s *IotCardDataSource) Count(ctx context.Context, params ExportParams) (int, error) { var total int64 query := s.applyFilters(ctx, s.baseQuery(ctx), params) if err := query.Count(&total).Error; err != nil { return 0, err } return int(total), nil } // Headers 返回 IoT 卡导出表头。 func (s *IotCardDataSource) Headers(ctx context.Context, params ExportParams) ([]string, error) { return []string{"ICCID", "MSISDN", "绑定设备虚拟号", "运营商", "店铺名称", "绑定设备名称", "是否实名", "实名时间", "网络状态"}, nil } // Fetch 按 offset/limit 查询 IoT 卡导出数据。 func (s *IotCardDataSource) Fetch(ctx context.Context, params ExportParams, offset, limit int) ([][]string, error) { if limit <= 0 { return [][]string{}, nil } var items []iotCardExportRow query := s.applyFilters(ctx, s.baseQuery(ctx), params). Select(` c.iccid, c.msisdn, c.device_virtual_no, c.carrier_name, COALESCE(sh.shop_name, '') AS shop_name, COALESCE(d.device_name, '') AS device_name, c.real_name_status, c.first_realname_at, c.network_status `). Order("c.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.ICCID, item.MSISDN, item.DeviceVirtualNo, item.CarrierName, item.ShopName, item.DeviceName, formatRealNameVerified(item.RealNameStatus), formatOptionalTime(item.FirstRealnameAt), constants.GetNetworkStatusName(item.NetworkStatus), }) } return rows, nil } // baseQuery 构造卡导出的基础查询。 // 绑定设备名称从当前有效绑定中取一条,避免多绑定历史记录放大导出行数。 func (s *IotCardDataSource) baseQuery(ctx context.Context) *gorm.DB { return s.db.WithContext(ctx). Table("tb_iot_card AS c"). Joins("LEFT JOIN tb_shop AS sh ON sh.id = c.shop_id"). Joins(` LEFT JOIN LATERAL ( SELECT d.device_name FROM tb_device_sim_binding AS b JOIN tb_device AS d ON d.id = b.device_id AND d.deleted_at IS NULL WHERE b.iot_card_id = c.id AND b.bind_status = ? AND b.deleted_at IS NULL ORDER BY b.is_current DESC, b.id DESC LIMIT 1 ) AS d ON TRUE `, constants.BindStatusBound). Where("c.deleted_at IS NULL") } // applyFilters 应用与 /api/admin/iot-cards/standalone 一致的筛选条件。 // 导出任务运行在 Worker 中,权限只能使用任务创建时保存的店铺范围快照。 func (s *IotCardDataSource) applyFilters(ctx context.Context, query *gorm.DB, params ExportParams) *gorm.DB { query = applyExportShopScope(query, params, "c.shop_id") if isStandalone, ok := filterOptionalBool(params.Filters, "is_standalone"); ok { query = query.Where("c.is_standalone = ?", isStandalone) } if status, ok := filterInt(params.Filters, "status"); ok && status > 0 { query = query.Where("c.status = ?", status) } if carrierID, ok := filterUint(params.Filters, "carrier_id"); ok { query = query.Where("c.carrier_id = ?", carrierID) } if shopIDs, ok := filterUintSlice(params.Filters, "shop_ids"); ok { if len(shopIDs) == 0 { query = query.Where("1 = 0") } else { query = query.Where("c.shop_id IN ?", shopIDs) } } else if shopID, ok := filterInt(params.Filters, "shop_id"); ok { if shopID <= 0 { query = query.Where("1 = 0") } else { query = query.Where("c.shop_id = ?", shopID) } } if iccid, ok := filterString(params.Filters, "iccid"); ok { query = query.Where("c.iccid LIKE ?", "%"+iccid+"%") } if msisdn, ok := filterString(params.Filters, "msisdn"); ok { query = query.Where("c.msisdn LIKE ?", "%"+msisdn+"%") } if virtualNo, ok := filterString(params.Filters, "virtual_no"); ok { query = query.Where("c.virtual_no LIKE ?", "%"+virtualNo+"%") } if keyword, ok := filterString(params.Filters, "keyword"); ok { query = query.Where("c.iccid LIKE ? OR c.virtual_no LIKE ?", "%"+keyword+"%", "%"+keyword+"%") } if batchNo, ok := filterString(params.Filters, "batch_no"); ok { query = query.Where("c.batch_no = ?", batchNo) } if packageID, ok := filterUint(params.Filters, "package_id"); ok { query = query.Where("c.id IN (?)", s.db.WithContext(ctx).Table("tb_package_usage"). Select("iot_card_id"). Where("package_id = ? AND deleted_at IS NULL", packageID)) } if isDistributed, ok := filterOptionalBool(params.Filters, "is_distributed"); ok { if isDistributed { query = query.Where("c.shop_id IS NOT NULL") } else { query = query.Where("c.shop_id IS NULL") } } if iccidStart, ok := filterString(params.Filters, "iccid_start"); ok { query = query.Where("c.iccid >= ?", iccidStart) } if iccidEnd, ok := filterString(params.Filters, "iccid_end"); ok { query = query.Where("c.iccid <= ?", iccidEnd) } if isReplaced, ok := filterOptionalBool(params.Filters, "is_replaced"); ok { subQuery := s.db.WithContext(ctx).Table("tb_exchange_order"). Select("old_asset_id"). Where("old_asset_type = ? AND status IN ? AND deleted_at IS NULL", constants.ExchangeAssetTypeIotCard, []int{constants.ExchangeStatusShipped, constants.ExchangeStatusCompleted}, ) if isReplaced { query = query.Where("c.id IN (?)", subQuery) } else { query = query.Where("c.id NOT IN (?)", subQuery) } } if seriesID, ok := filterUint(params.Filters, "series_id"); ok { query = query.Where("c.series_id = ?", seriesID) } if carrierName, ok := filterString(params.Filters, "carrier_name"); ok { query = query.Where("c.carrier_name LIKE ?", "%"+carrierName+"%") } if hasActivePackage, ok := filterOptionalBool(params.Filters, "has_active_package"); ok { subQuery := s.db.WithContext(ctx).Table("tb_package_usage AS pu"). Select("1"). Where("pu.iot_card_id = c.id AND pu.status = ? AND pu.master_usage_id IS NULL AND pu.deleted_at IS NULL", constants.PackageUsageStatusActive) if hasActivePackage { query = query.Where("EXISTS (?)", subQuery) } else { query = query.Where("NOT EXISTS (?)", subQuery) } } return query } type iotCardExportRow struct { ICCID string `gorm:"column:iccid"` MSISDN string `gorm:"column:msisdn"` DeviceVirtualNo string `gorm:"column:device_virtual_no"` CarrierName string `gorm:"column:carrier_name"` ShopName string `gorm:"column:shop_name"` DeviceName string `gorm:"column:device_name"` RealNameStatus int `gorm:"column:real_name_status"` FirstRealnameAt *time.Time `gorm:"column:first_realname_at"` NetworkStatus int `gorm:"column:network_status"` } func formatRealNameVerified(status int) string { switch status { case constants.RealNameStatusVerified: return "是" case constants.RealNameStatusNotVerified: return "否" default: return "未知" } }