This commit is contained in:
@@ -2,97 +2,219 @@ package exporter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
)
|
||||
|
||||
// IotCardSceneStrategy IoT 卡导出场景策略。
|
||||
type IotCardSceneStrategy struct {
|
||||
// IotCardDataSource IoT 卡导出数据源。
|
||||
type IotCardDataSource struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewIotCardSceneStrategy 创建 IoT 卡场景策略。
|
||||
func NewIotCardSceneStrategy(db *gorm.DB) *IotCardSceneStrategy {
|
||||
return &IotCardSceneStrategy{db: db}
|
||||
// NewIotCardDataSource 创建 IoT 卡导出数据源。
|
||||
func NewIotCardDataSource(db *gorm.DB) *IotCardDataSource {
|
||||
return &IotCardDataSource{db: db}
|
||||
}
|
||||
|
||||
// Scene 场景编码。
|
||||
func (s *IotCardSceneStrategy) Scene() string {
|
||||
// Scene 返回导出场景编码。
|
||||
func (s *IotCardDataSource) Scene() string {
|
||||
return constants.ExportTaskSceneIotCard
|
||||
}
|
||||
|
||||
// Headers 导出表头。
|
||||
func (s *IotCardSceneStrategy) Headers() []string {
|
||||
return []string{"ID", "ICCID", "MSISDN", "虚拟号", "运营商名称", "状态", "店铺ID", "创建时间"}
|
||||
// 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
|
||||
}
|
||||
|
||||
// NextShardBoundary 获取下一段分片边界。
|
||||
func (s *IotCardSceneStrategy) NextShardBoundary(ctx context.Context, task *model.ExportTask, afterID uint64, limit int) (*ShardBoundary, error) {
|
||||
// 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 {
|
||||
limit = constants.ExportDefaultShardSize
|
||||
}
|
||||
|
||||
var ids []uint64
|
||||
query := s.db.WithContext(ctx).
|
||||
Model(&model.IotCard{}).
|
||||
Where("id > ?", afterID).
|
||||
Order("id ASC").
|
||||
Limit(limit)
|
||||
query = applyShopScopeForTask(query, task)
|
||||
|
||||
if err := query.Pluck("id", &ids).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &ShardBoundary{
|
||||
StartID: afterID,
|
||||
EndID: ids[len(ids)-1],
|
||||
RowCount: len(ids),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// QueryRows 查询分片内数据行。
|
||||
func (s *IotCardSceneStrategy) QueryRows(ctx context.Context, task *model.ExportTask, startID, endID uint64) ([][]string, error) {
|
||||
if endID == 0 {
|
||||
return [][]string{}, nil
|
||||
}
|
||||
|
||||
var cards []model.IotCard
|
||||
query := s.db.WithContext(ctx).
|
||||
Model(&model.IotCard{}).
|
||||
Where("id > ? AND id <= ?", startID, endID).
|
||||
Order("id ASC")
|
||||
query = applyShopScopeForTask(query, task)
|
||||
|
||||
if err := query.Find(&cards).Error; err != 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(cards))
|
||||
for _, item := range cards {
|
||||
shopID := ""
|
||||
if item.ShopID != nil {
|
||||
shopID = strconv.FormatUint(uint64(*item.ShopID), 10)
|
||||
}
|
||||
|
||||
rows := make([][]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
rows = append(rows, []string{
|
||||
strconv.FormatUint(uint64(item.ID), 10),
|
||||
item.ICCID,
|
||||
item.MSISDN,
|
||||
item.VirtualNo,
|
||||
item.DeviceVirtualNo,
|
||||
item.CarrierName,
|
||||
strconv.Itoa(item.Status),
|
||||
shopID,
|
||||
item.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
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 "未知"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user