All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m50s
221 lines
6.9 KiB
Go
221 lines
6.9 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
|
|
MSISDN string
|
|
DeviceVirtualNo string
|
|
CarrierName string
|
|
ShopName string
|
|
DeviceName string
|
|
RealNameStatus int
|
|
FirstRealnameAt *time.Time
|
|
NetworkStatus int
|
|
}
|
|
|
|
func formatRealNameVerified(status int) string {
|
|
switch status {
|
|
case constants.RealNameStatusVerified:
|
|
return "是"
|
|
case constants.RealNameStatusNotVerified:
|
|
return "否"
|
|
default:
|
|
return "未知"
|
|
}
|
|
}
|