Files
junhong_cmp_fiber/internal/exporter/iot_card_scene.go
Break e37aad9e1d
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m41s
修复导出数据不全的问题
2026-06-15 15:55:32 +08:00

221 lines
7.2 KiB
Go

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 "未知"
}
}