feat: 实现物联网卡独立管理和批量导入功能
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 4m42s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 4m42s
新增物联网卡独立管理模块,支持单卡查询、批量导入和状态管理。主要变更包括: 功能特性: - 新增物联网卡 CRUD 接口(查询、分页列表、删除) - 支持 CSV/Excel 批量导入物联网卡 - 实现异步导入任务处理和进度跟踪 - 新增 ICCID 号码格式校验器(支持 Luhn 算法) - 新增 CSV 文件解析工具(支持编码检测和错误处理) 数据库变更: - 移除 iot_card 和 device 表的 owner_id/owner_type 字段 - 新增 iot_card_import_task 导入任务表 - 为导入任务添加运营商类型字段 测试覆盖: - 新增 IoT 卡 Store 层单元测试 - 新增 IoT 卡导入任务单元测试 - 新增 IoT 卡集成测试(包含导入流程测试) - 新增 CSV 工具和 ICCID 校验器测试 文档更新: - 更新 OpenAPI 文档(新增 7 个 IoT 卡接口) - 归档 OpenSpec 变更提案 - 更新 API 文档规范和生成器指南 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
133
internal/store/postgres/iot_card_import_task_store.go
Normal file
133
internal/store/postgres/iot_card_import_task_store.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type IotCardImportTaskStore struct {
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
}
|
||||
|
||||
func NewIotCardImportTaskStore(db *gorm.DB, redis *redis.Client) *IotCardImportTaskStore {
|
||||
return &IotCardImportTaskStore{
|
||||
db: db,
|
||||
redis: redis,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *IotCardImportTaskStore) Create(ctx context.Context, task *model.IotCardImportTask) error {
|
||||
return s.db.WithContext(ctx).Create(task).Error
|
||||
}
|
||||
|
||||
func (s *IotCardImportTaskStore) GetByID(ctx context.Context, id uint) (*model.IotCardImportTask, error) {
|
||||
var task model.IotCardImportTask
|
||||
if err := s.db.WithContext(ctx).First(&task, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
func (s *IotCardImportTaskStore) GetByTaskNo(ctx context.Context, taskNo string) (*model.IotCardImportTask, error) {
|
||||
var task model.IotCardImportTask
|
||||
if err := s.db.WithContext(ctx).Where("task_no = ?", taskNo).First(&task).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
func (s *IotCardImportTaskStore) Update(ctx context.Context, task *model.IotCardImportTask) error {
|
||||
return s.db.WithContext(ctx).Save(task).Error
|
||||
}
|
||||
|
||||
func (s *IotCardImportTaskStore) UpdateStatus(ctx context.Context, id uint, status int, errorMessage string) error {
|
||||
updates := map[string]interface{}{
|
||||
"status": status,
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
if status == model.ImportTaskStatusProcessing {
|
||||
updates["started_at"] = time.Now()
|
||||
}
|
||||
if status == model.ImportTaskStatusCompleted || status == model.ImportTaskStatusFailed {
|
||||
updates["completed_at"] = time.Now()
|
||||
}
|
||||
if errorMessage != "" {
|
||||
updates["error_message"] = errorMessage
|
||||
}
|
||||
return s.db.WithContext(ctx).Model(&model.IotCardImportTask{}).Where("id = ?", id).Updates(updates).Error
|
||||
}
|
||||
|
||||
func (s *IotCardImportTaskStore) UpdateResult(ctx context.Context, id uint, successCount, skipCount, failCount int, skippedItems, failedItems model.ImportResultItems) error {
|
||||
updates := map[string]interface{}{
|
||||
"success_count": successCount,
|
||||
"skip_count": skipCount,
|
||||
"fail_count": failCount,
|
||||
"skipped_items": skippedItems,
|
||||
"failed_items": failedItems,
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
return s.db.WithContext(ctx).Model(&model.IotCardImportTask{}).Where("id = ?", id).Updates(updates).Error
|
||||
}
|
||||
|
||||
func (s *IotCardImportTaskStore) List(ctx context.Context, opts *store.QueryOptions, filters map[string]interface{}) ([]*model.IotCardImportTask, int64, error) {
|
||||
var tasks []*model.IotCardImportTask
|
||||
var total int64
|
||||
|
||||
query := s.db.WithContext(ctx).Model(&model.IotCardImportTask{})
|
||||
|
||||
if status, ok := filters["status"].(int); ok && status > 0 {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
if carrierID, ok := filters["carrier_id"].(uint); ok && carrierID > 0 {
|
||||
query = query.Where("carrier_id = ?", carrierID)
|
||||
}
|
||||
if batchNo, ok := filters["batch_no"].(string); ok && batchNo != "" {
|
||||
query = query.Where("batch_no LIKE ?", "%"+batchNo+"%")
|
||||
}
|
||||
if startTime, ok := filters["start_time"].(time.Time); ok && !startTime.IsZero() {
|
||||
query = query.Where("created_at >= ?", startTime)
|
||||
}
|
||||
if endTime, ok := filters["end_time"].(time.Time); ok && !endTime.IsZero() {
|
||||
query = query.Where("created_at <= ?", endTime)
|
||||
}
|
||||
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if opts == nil {
|
||||
opts = &store.QueryOptions{
|
||||
Page: 1,
|
||||
PageSize: constants.DefaultPageSize,
|
||||
}
|
||||
}
|
||||
offset := (opts.Page - 1) * opts.PageSize
|
||||
query = query.Offset(offset).Limit(opts.PageSize)
|
||||
|
||||
if opts.OrderBy != "" {
|
||||
query = query.Order(opts.OrderBy)
|
||||
} else {
|
||||
query = query.Order("created_at DESC")
|
||||
}
|
||||
|
||||
if err := query.Find(&tasks).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return tasks, total, nil
|
||||
}
|
||||
|
||||
func (s *IotCardImportTaskStore) GenerateTaskNo(ctx context.Context) string {
|
||||
now := time.Now()
|
||||
dateStr := now.Format("20060102")
|
||||
seq := now.UnixNano() % 1000000
|
||||
return fmt.Sprintf("IMP-%s-%06d", dateStr, seq)
|
||||
}
|
||||
218
internal/store/postgres/iot_card_store.go
Normal file
218
internal/store/postgres/iot_card_store.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type IotCardStore struct {
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
}
|
||||
|
||||
func NewIotCardStore(db *gorm.DB, redis *redis.Client) *IotCardStore {
|
||||
return &IotCardStore{
|
||||
db: db,
|
||||
redis: redis,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *IotCardStore) Create(ctx context.Context, card *model.IotCard) error {
|
||||
return s.db.WithContext(ctx).Create(card).Error
|
||||
}
|
||||
|
||||
func (s *IotCardStore) CreateBatch(ctx context.Context, cards []*model.IotCard) error {
|
||||
if len(cards) == 0 {
|
||||
return nil
|
||||
}
|
||||
return s.db.WithContext(ctx).CreateInBatches(cards, 100).Error
|
||||
}
|
||||
|
||||
func (s *IotCardStore) GetByID(ctx context.Context, id uint) (*model.IotCard, error) {
|
||||
var card model.IotCard
|
||||
if err := s.db.WithContext(ctx).First(&card, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &card, nil
|
||||
}
|
||||
|
||||
func (s *IotCardStore) GetByICCID(ctx context.Context, iccid string) (*model.IotCard, error) {
|
||||
var card model.IotCard
|
||||
if err := s.db.WithContext(ctx).Where("iccid = ?", iccid).First(&card).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &card, nil
|
||||
}
|
||||
|
||||
func (s *IotCardStore) ExistsByICCID(ctx context.Context, iccid string) (bool, error) {
|
||||
var count int64
|
||||
if err := s.db.WithContext(ctx).Model(&model.IotCard{}).Where("iccid = ?", iccid).Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (s *IotCardStore) ExistsByICCIDBatch(ctx context.Context, iccids []string) (map[string]bool, error) {
|
||||
if len(iccids) == 0 {
|
||||
return make(map[string]bool), nil
|
||||
}
|
||||
|
||||
var existingICCIDs []string
|
||||
if err := s.db.WithContext(ctx).Model(&model.IotCard{}).
|
||||
Where("iccid IN ?", iccids).
|
||||
Pluck("iccid", &existingICCIDs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string]bool)
|
||||
for _, iccid := range existingICCIDs {
|
||||
result[iccid] = true
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *IotCardStore) Update(ctx context.Context, card *model.IotCard) error {
|
||||
return s.db.WithContext(ctx).Save(card).Error
|
||||
}
|
||||
|
||||
func (s *IotCardStore) Delete(ctx context.Context, id uint) error {
|
||||
return s.db.WithContext(ctx).Delete(&model.IotCard{}, id).Error
|
||||
}
|
||||
|
||||
func (s *IotCardStore) ListStandalone(ctx context.Context, opts *store.QueryOptions, filters map[string]any) ([]*model.IotCard, int64, error) {
|
||||
var cards []*model.IotCard
|
||||
var total int64
|
||||
|
||||
query := s.db.WithContext(ctx).Model(&model.IotCard{})
|
||||
|
||||
query = query.Where("id NOT IN (?)",
|
||||
s.db.Model(&model.DeviceSimBinding{}).
|
||||
Select("iot_card_id").
|
||||
Where("bind_status = ?", 1))
|
||||
|
||||
if status, ok := filters["status"].(int); ok && status > 0 {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
if carrierID, ok := filters["carrier_id"].(uint); ok && carrierID > 0 {
|
||||
query = query.Where("carrier_id = ?", carrierID)
|
||||
}
|
||||
if shopID, ok := filters["shop_id"].(uint); ok && shopID > 0 {
|
||||
query = query.Where("shop_id = ?", shopID)
|
||||
}
|
||||
if iccid, ok := filters["iccid"].(string); ok && iccid != "" {
|
||||
query = query.Where("iccid LIKE ?", "%"+iccid+"%")
|
||||
}
|
||||
if msisdn, ok := filters["msisdn"].(string); ok && msisdn != "" {
|
||||
query = query.Where("msisdn LIKE ?", "%"+msisdn+"%")
|
||||
}
|
||||
if batchNo, ok := filters["batch_no"].(string); ok && batchNo != "" {
|
||||
query = query.Where("batch_no = ?", batchNo)
|
||||
}
|
||||
if packageID, ok := filters["package_id"].(uint); ok && packageID > 0 {
|
||||
query = query.Where("id IN (?)",
|
||||
s.db.Table("tb_package_usage").
|
||||
Select("iot_card_id").
|
||||
Where("package_id = ? AND deleted_at IS NULL", packageID))
|
||||
}
|
||||
if isDistributed, ok := filters["is_distributed"].(bool); ok {
|
||||
if isDistributed {
|
||||
query = query.Where("shop_id IS NOT NULL")
|
||||
} else {
|
||||
query = query.Where("shop_id IS NULL")
|
||||
}
|
||||
}
|
||||
if iccidStart, ok := filters["iccid_start"].(string); ok && iccidStart != "" {
|
||||
query = query.Where("iccid >= ?", iccidStart)
|
||||
}
|
||||
if iccidEnd, ok := filters["iccid_end"].(string); ok && iccidEnd != "" {
|
||||
query = query.Where("iccid <= ?", iccidEnd)
|
||||
}
|
||||
if isReplaced, ok := filters["is_replaced"].(bool); ok {
|
||||
if isReplaced {
|
||||
query = query.Where("id IN (?)",
|
||||
s.db.Table("tb_card_replacement_record").
|
||||
Select("old_iot_card_id").
|
||||
Where("deleted_at IS NULL"))
|
||||
} else {
|
||||
query = query.Where("id NOT IN (?)",
|
||||
s.db.Table("tb_card_replacement_record").
|
||||
Select("old_iot_card_id").
|
||||
Where("deleted_at IS NULL"))
|
||||
}
|
||||
}
|
||||
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if opts == nil {
|
||||
opts = &store.QueryOptions{
|
||||
Page: 1,
|
||||
PageSize: constants.DefaultPageSize,
|
||||
}
|
||||
}
|
||||
offset := (opts.Page - 1) * opts.PageSize
|
||||
query = query.Offset(offset).Limit(opts.PageSize)
|
||||
|
||||
if opts.OrderBy != "" {
|
||||
query = query.Order(opts.OrderBy)
|
||||
} else {
|
||||
query = query.Order("created_at DESC")
|
||||
}
|
||||
|
||||
if err := query.Find(&cards).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return cards, total, nil
|
||||
}
|
||||
|
||||
func (s *IotCardStore) List(ctx context.Context, opts *store.QueryOptions, filters map[string]any) ([]*model.IotCard, int64, error) {
|
||||
var cards []*model.IotCard
|
||||
var total int64
|
||||
|
||||
query := s.db.WithContext(ctx).Model(&model.IotCard{})
|
||||
|
||||
if status, ok := filters["status"].(int); ok && status > 0 {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
if carrierID, ok := filters["carrier_id"].(uint); ok && carrierID > 0 {
|
||||
query = query.Where("carrier_id = ?", carrierID)
|
||||
}
|
||||
if shopID, ok := filters["shop_id"].(uint); ok && shopID > 0 {
|
||||
query = query.Where("shop_id = ?", shopID)
|
||||
}
|
||||
if iccid, ok := filters["iccid"].(string); ok && iccid != "" {
|
||||
query = query.Where("iccid LIKE ?", "%"+iccid+"%")
|
||||
}
|
||||
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if opts == nil {
|
||||
opts = &store.QueryOptions{
|
||||
Page: 1,
|
||||
PageSize: constants.DefaultPageSize,
|
||||
}
|
||||
}
|
||||
offset := (opts.Page - 1) * opts.PageSize
|
||||
query = query.Offset(offset).Limit(opts.PageSize)
|
||||
|
||||
if opts.OrderBy != "" {
|
||||
query = query.Order(opts.OrderBy)
|
||||
} else {
|
||||
query = query.Order("created_at DESC")
|
||||
}
|
||||
|
||||
if err := query.Find(&cards).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return cards, total, nil
|
||||
}
|
||||
242
internal/store/postgres/iot_card_store_test.go
Normal file
242
internal/store/postgres/iot_card_store_test.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIotCardStore_Create(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
s := NewIotCardStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
card := &model.IotCard{
|
||||
ICCID: "89860012345678901234",
|
||||
CardType: "data_card",
|
||||
CarrierID: 1,
|
||||
Status: 1,
|
||||
}
|
||||
|
||||
err := s.Create(ctx, card)
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, card.ID)
|
||||
}
|
||||
|
||||
func TestIotCardStore_ExistsByICCID(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
s := NewIotCardStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
card := &model.IotCard{
|
||||
ICCID: "89860012345678901111",
|
||||
CardType: "data_card",
|
||||
CarrierID: 1,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, card))
|
||||
|
||||
exists, err := s.ExistsByICCID(ctx, "89860012345678901111")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, exists)
|
||||
|
||||
exists, err = s.ExistsByICCID(ctx, "89860012345678909999")
|
||||
require.NoError(t, err)
|
||||
assert.False(t, exists)
|
||||
}
|
||||
|
||||
func TestIotCardStore_ExistsByICCIDBatch(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
s := NewIotCardStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
cards := []*model.IotCard{
|
||||
{ICCID: "89860012345678902001", CardType: "data_card", CarrierID: 1, Status: 1},
|
||||
{ICCID: "89860012345678902002", CardType: "data_card", CarrierID: 1, Status: 1},
|
||||
{ICCID: "89860012345678902003", CardType: "data_card", CarrierID: 1, Status: 1},
|
||||
}
|
||||
require.NoError(t, s.CreateBatch(ctx, cards))
|
||||
|
||||
result, err := s.ExistsByICCIDBatch(ctx, []string{
|
||||
"89860012345678902001",
|
||||
"89860012345678902002",
|
||||
"89860012345678909999",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result["89860012345678902001"])
|
||||
assert.True(t, result["89860012345678902002"])
|
||||
assert.False(t, result["89860012345678909999"])
|
||||
|
||||
emptyResult, err := s.ExistsByICCIDBatch(ctx, []string{})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, emptyResult)
|
||||
}
|
||||
|
||||
func TestIotCardStore_ListStandalone(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
s := NewIotCardStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
standaloneCards := []*model.IotCard{
|
||||
{ICCID: "89860012345678903001", CardType: "data_card", CarrierID: 1, Status: 1},
|
||||
{ICCID: "89860012345678903002", CardType: "data_card", CarrierID: 1, Status: 1},
|
||||
{ICCID: "89860012345678903003", CardType: "data_card", CarrierID: 2, Status: 2},
|
||||
}
|
||||
require.NoError(t, s.CreateBatch(ctx, standaloneCards))
|
||||
|
||||
boundCard := &model.IotCard{
|
||||
ICCID: "89860012345678903004",
|
||||
CardType: "data_card",
|
||||
CarrierID: 1,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, boundCard))
|
||||
|
||||
binding := &model.DeviceSimBinding{
|
||||
DeviceID: 1,
|
||||
IotCardID: boundCard.ID,
|
||||
BindStatus: 1,
|
||||
}
|
||||
require.NoError(t, tx.Create(binding).Error)
|
||||
|
||||
t.Run("查询所有单卡", func(t *testing.T) {
|
||||
cards, total, err := s.ListStandalone(ctx, &store.QueryOptions{Page: 1, PageSize: 20}, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(3), total)
|
||||
assert.Len(t, cards, 3)
|
||||
|
||||
for _, card := range cards {
|
||||
assert.NotEqual(t, boundCard.ID, card.ID, "已绑定的卡不应出现在单卡列表中")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("按运营商ID过滤", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"carrier_id": uint(1)}
|
||||
cards, total, err := s.ListStandalone(ctx, &store.QueryOptions{Page: 1, PageSize: 20}, filters)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), total)
|
||||
for _, card := range cards {
|
||||
assert.Equal(t, uint(1), card.CarrierID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("按状态过滤", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"status": 2}
|
||||
cards, total, err := s.ListStandalone(ctx, &store.QueryOptions{Page: 1, PageSize: 20}, filters)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
assert.Len(t, cards, 1)
|
||||
assert.Equal(t, 2, cards[0].Status)
|
||||
})
|
||||
|
||||
t.Run("按ICCID模糊查询", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"iccid": "903001"}
|
||||
cards, total, err := s.ListStandalone(ctx, &store.QueryOptions{Page: 1, PageSize: 20}, filters)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
assert.Contains(t, cards[0].ICCID, "903001")
|
||||
})
|
||||
|
||||
t.Run("分页查询", func(t *testing.T) {
|
||||
cards, total, err := s.ListStandalone(ctx, &store.QueryOptions{Page: 1, PageSize: 2}, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(3), total)
|
||||
assert.Len(t, cards, 2)
|
||||
|
||||
cards2, _, err := s.ListStandalone(ctx, &store.QueryOptions{Page: 2, PageSize: 2}, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, cards2, 1)
|
||||
})
|
||||
|
||||
t.Run("默认分页选项", func(t *testing.T) {
|
||||
cards, total, err := s.ListStandalone(ctx, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(3), total)
|
||||
assert.Len(t, cards, 3)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIotCardStore_ListStandalone_Filters(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
s := NewIotCardStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
shopID := uint(100)
|
||||
cards := []*model.IotCard{
|
||||
{ICCID: "89860012345678904001", CardType: "data_card", CarrierID: 1, Status: 1, ShopID: &shopID, BatchNo: "BATCH001", MSISDN: "13800000001"},
|
||||
{ICCID: "89860012345678904002", CardType: "data_card", CarrierID: 1, Status: 1, ShopID: nil, BatchNo: "BATCH001", MSISDN: "13800000002"},
|
||||
{ICCID: "89860012345678904003", CardType: "data_card", CarrierID: 1, Status: 1, ShopID: nil, BatchNo: "BATCH002", MSISDN: "13800000003"},
|
||||
}
|
||||
require.NoError(t, s.CreateBatch(ctx, cards))
|
||||
|
||||
t.Run("按店铺ID过滤", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"shop_id": shopID}
|
||||
cards, total, err := s.ListStandalone(ctx, nil, filters)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
assert.Equal(t, shopID, *cards[0].ShopID)
|
||||
})
|
||||
|
||||
t.Run("按批次号过滤", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"batch_no": "BATCH001"}
|
||||
_, total, err := s.ListStandalone(ctx, nil, filters)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), total)
|
||||
})
|
||||
|
||||
t.Run("按MSISDN模糊查询", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"msisdn": "000001"}
|
||||
result, total, err := s.ListStandalone(ctx, nil, filters)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
assert.Contains(t, result[0].MSISDN, "000001")
|
||||
})
|
||||
|
||||
t.Run("已分销过滤-true", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"is_distributed": true}
|
||||
result, total, err := s.ListStandalone(ctx, nil, filters)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
assert.NotNil(t, result[0].ShopID)
|
||||
})
|
||||
|
||||
t.Run("已分销过滤-false", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"is_distributed": false}
|
||||
result, total, err := s.ListStandalone(ctx, nil, filters)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), total)
|
||||
for _, card := range result {
|
||||
assert.Nil(t, card.ShopID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ICCID范围查询", func(t *testing.T) {
|
||||
filters := map[string]interface{}{
|
||||
"iccid_start": "89860012345678904001",
|
||||
"iccid_end": "89860012345678904002",
|
||||
}
|
||||
_, total, err := s.ListStandalone(ctx, nil, filters)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), total)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user