feat: 实现设备管理和设备导入功能,修复测试问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m30s

主要变更:
- 实现设备管理模块(创建、查询、列表、更新状态、删除)
- 实现设备批量导入功能(CSV 解析、ICCID 绑定、异步任务处理)
- 添加设备-SIM 卡绑定约束(部分唯一索引防止并发问题)
- 修复 fee_rate 数据库字段类型(numeric -> bigint)
- 修复测试数据隔离问题(基于增量断言)
- 修复集成测试中间件顺序问题
- 清理无用测试文件(PersonalCustomer、Email 相关)
- 归档 enterprise-card-authorization 变更
This commit is contained in:
2026-01-26 18:05:12 +08:00
parent fdcff33058
commit ce0783f96e
68 changed files with 6400 additions and 1482 deletions

View File

@@ -0,0 +1,135 @@
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 DeviceImportTaskStore struct {
db *gorm.DB
redis *redis.Client
}
func NewDeviceImportTaskStore(db *gorm.DB, redis *redis.Client) *DeviceImportTaskStore {
return &DeviceImportTaskStore{
db: db,
redis: redis,
}
}
func (s *DeviceImportTaskStore) Create(ctx context.Context, task *model.DeviceImportTask) error {
return s.db.WithContext(ctx).Create(task).Error
}
func (s *DeviceImportTaskStore) GetByID(ctx context.Context, id uint) (*model.DeviceImportTask, error) {
var task model.DeviceImportTask
if err := s.db.WithContext(ctx).First(&task, id).Error; err != nil {
return nil, err
}
return &task, nil
}
func (s *DeviceImportTaskStore) GetByTaskNo(ctx context.Context, taskNo string) (*model.DeviceImportTask, error) {
var task model.DeviceImportTask
if err := s.db.WithContext(ctx).Where("task_no = ?", taskNo).First(&task).Error; err != nil {
return nil, err
}
return &task, nil
}
func (s *DeviceImportTaskStore) Update(ctx context.Context, task *model.DeviceImportTask) error {
return s.db.WithContext(ctx).Save(task).Error
}
func (s *DeviceImportTaskStore) UpdateStatus(ctx context.Context, id uint, status int, errorMessage string) error {
updates := map[string]any{
"status": status,
"updated_at": time.Now(),
}
if status == model.ImportTaskStatusProcessing {
now := time.Now()
updates["started_at"] = &now
}
if status == model.ImportTaskStatusCompleted || status == model.ImportTaskStatusFailed {
now := time.Now()
updates["completed_at"] = &now
}
if errorMessage != "" {
updates["error_message"] = errorMessage
}
return s.db.WithContext(ctx).Model(&model.DeviceImportTask{}).Where("id = ?", id).Updates(updates).Error
}
func (s *DeviceImportTaskStore) UpdateResult(ctx context.Context, id uint, totalCount, successCount, skipCount, failCount, warningCount int, skippedItems, failedItems, warningItems model.ImportResultItems) error {
updates := map[string]any{
"total_count": totalCount,
"success_count": successCount,
"skip_count": skipCount,
"fail_count": failCount,
"warning_count": warningCount,
"skipped_items": skippedItems,
"failed_items": failedItems,
"warning_items": warningItems,
"updated_at": time.Now(),
}
return s.db.WithContext(ctx).Model(&model.DeviceImportTask{}).Where("id = ?", id).Updates(updates).Error
}
func (s *DeviceImportTaskStore) List(ctx context.Context, opts *store.QueryOptions, filters map[string]any) ([]*model.DeviceImportTask, int64, error) {
var tasks []*model.DeviceImportTask
var total int64
query := s.db.WithContext(ctx).Model(&model.DeviceImportTask{})
if status, ok := filters["status"].(int); ok && status > 0 {
query = query.Where("status = ?", status)
}
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 *DeviceImportTaskStore) GenerateTaskNo(ctx context.Context) string {
now := time.Now()
dateStr := now.Format("20060102")
seq := now.UnixNano() % 1000000
return fmt.Sprintf("DEV-IMP-%s-%06d", dateStr, seq)
}

View File

@@ -0,0 +1,202 @@
package postgres
import (
"context"
stderrors "errors"
"strings"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/jackc/pgx/v5/pgconn"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
type DeviceSimBindingStore struct {
db *gorm.DB
redis *redis.Client
}
func NewDeviceSimBindingStore(db *gorm.DB, redis *redis.Client) *DeviceSimBindingStore {
return &DeviceSimBindingStore{
db: db,
redis: redis,
}
}
func (s *DeviceSimBindingStore) Create(ctx context.Context, binding *model.DeviceSimBinding) error {
err := s.db.WithContext(ctx).Create(binding).Error
if err != nil {
if isUniqueViolation(err) {
if strings.Contains(err.Error(), "idx_active_device_slot") {
return errors.New(errors.CodeConflict, "该插槽已有绑定的卡")
}
if strings.Contains(err.Error(), "idx_device_sim_bindings_active_card") {
return errors.New(errors.CodeIotCardBoundToDevice, "该卡已绑定到其他设备")
}
}
return err
}
return nil
}
func isUniqueViolation(err error) bool {
var pgErr *pgconn.PgError
if stderrors.As(err, &pgErr) {
return pgErr.Code == "23505"
}
return false
}
func (s *DeviceSimBindingStore) CreateBatch(ctx context.Context, bindings []*model.DeviceSimBinding) error {
if len(bindings) == 0 {
return nil
}
return s.db.WithContext(ctx).CreateInBatches(bindings, 100).Error
}
func (s *DeviceSimBindingStore) GetByID(ctx context.Context, id uint) (*model.DeviceSimBinding, error) {
var binding model.DeviceSimBinding
if err := s.db.WithContext(ctx).First(&binding, id).Error; err != nil {
return nil, err
}
return &binding, nil
}
func (s *DeviceSimBindingStore) ListByDeviceID(ctx context.Context, deviceID uint) ([]*model.DeviceSimBinding, error) {
var bindings []*model.DeviceSimBinding
if err := s.db.WithContext(ctx).
Where("device_id = ? AND bind_status = 1", deviceID).
Order("slot_position ASC").
Find(&bindings).Error; err != nil {
return nil, err
}
return bindings, nil
}
func (s *DeviceSimBindingStore) ListByDeviceIDs(ctx context.Context, deviceIDs []uint) ([]*model.DeviceSimBinding, error) {
var bindings []*model.DeviceSimBinding
if len(deviceIDs) == 0 {
return bindings, nil
}
if err := s.db.WithContext(ctx).
Where("device_id IN ? AND bind_status = 1", deviceIDs).
Find(&bindings).Error; err != nil {
return nil, err
}
return bindings, nil
}
func (s *DeviceSimBindingStore) GetByDeviceAndCard(ctx context.Context, deviceID, iotCardID uint) (*model.DeviceSimBinding, error) {
var binding model.DeviceSimBinding
if err := s.db.WithContext(ctx).
Where("device_id = ? AND iot_card_id = ? AND bind_status = 1", deviceID, iotCardID).
First(&binding).Error; err != nil {
return nil, err
}
return &binding, nil
}
func (s *DeviceSimBindingStore) GetByDeviceAndSlot(ctx context.Context, deviceID uint, slotPosition int) (*model.DeviceSimBinding, error) {
var binding model.DeviceSimBinding
if err := s.db.WithContext(ctx).
Where("device_id = ? AND slot_position = ? AND bind_status = 1", deviceID, slotPosition).
First(&binding).Error; err != nil {
return nil, err
}
return &binding, nil
}
func (s *DeviceSimBindingStore) GetActiveBindingByCardID(ctx context.Context, iotCardID uint) (*model.DeviceSimBinding, error) {
var binding model.DeviceSimBinding
if err := s.db.WithContext(ctx).
Where("iot_card_id = ? AND bind_status = 1", iotCardID).
First(&binding).Error; err != nil {
return nil, err
}
return &binding, nil
}
func (s *DeviceSimBindingStore) GetActiveBindingsByCardIDs(ctx context.Context, iotCardIDs []uint) ([]*model.DeviceSimBinding, error) {
var bindings []*model.DeviceSimBinding
if len(iotCardIDs) == 0 {
return bindings, nil
}
if err := s.db.WithContext(ctx).
Where("iot_card_id IN ? AND bind_status = 1", iotCardIDs).
Find(&bindings).Error; err != nil {
return nil, err
}
return bindings, nil
}
func (s *DeviceSimBindingStore) Unbind(ctx context.Context, id uint) error {
now := time.Now()
updates := map[string]any{
"bind_status": 2,
"unbind_time": now,
"updated_at": now,
}
return s.db.WithContext(ctx).Model(&model.DeviceSimBinding{}).Where("id = ?", id).Updates(updates).Error
}
func (s *DeviceSimBindingStore) UnbindByDeviceID(ctx context.Context, deviceID uint) error {
now := time.Now()
updates := map[string]any{
"bind_status": 2,
"unbind_time": now,
"updated_at": now,
}
return s.db.WithContext(ctx).Model(&model.DeviceSimBinding{}).
Where("device_id = ? AND bind_status = 1", deviceID).
Updates(updates).Error
}
func (s *DeviceSimBindingStore) CountByDeviceID(ctx context.Context, deviceID uint) (int64, error) {
var count int64
if err := s.db.WithContext(ctx).Model(&model.DeviceSimBinding{}).
Where("device_id = ? AND bind_status = 1", deviceID).
Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
func (s *DeviceSimBindingStore) GetBoundCardIDsByDeviceIDs(ctx context.Context, deviceIDs []uint) ([]uint, error) {
if len(deviceIDs) == 0 {
return nil, nil
}
var cardIDs []uint
if err := s.db.WithContext(ctx).Model(&model.DeviceSimBinding{}).
Select("iot_card_id").
Where("device_id IN ? AND bind_status = 1", deviceIDs).
Pluck("iot_card_id", &cardIDs).Error; err != nil {
return nil, err
}
return cardIDs, nil
}
func (s *DeviceSimBindingStore) GetBoundICCIDs(ctx context.Context, iccids []string) (map[string]bool, error) {
result := make(map[string]bool)
if len(iccids) == 0 {
return result, nil
}
var bindings []struct {
ICCID string
}
if err := s.db.WithContext(ctx).
Table("tb_device_sim_binding b").
Select("c.iccid").
Joins("JOIN tb_iot_card c ON c.id = b.iot_card_id").
Where("c.iccid IN ? AND b.bind_status = 1 AND c.deleted_at IS NULL", iccids).
Find(&bindings).Error; err != nil {
return nil, err
}
for _, b := range bindings {
result[b.ICCID] = true
}
return result, nil
}

View File

@@ -0,0 +1,209 @@
package postgres
import (
"context"
"sync"
"testing"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/tests/testutils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDeviceSimBindingStore_Create_DuplicateCard(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
bindingStore := NewDeviceSimBindingStore(tx, rdb)
deviceStore := NewDeviceStore(tx, rdb)
cardStore := NewIotCardStore(tx, rdb)
ctx := context.Background()
device1 := &model.Device{DeviceNo: "TEST-DEV-UC-001", Status: 1, MaxSimSlots: 4}
device2 := &model.Device{DeviceNo: "TEST-DEV-UC-002", Status: 1, MaxSimSlots: 4}
require.NoError(t, deviceStore.Create(ctx, device1))
require.NoError(t, deviceStore.Create(ctx, device2))
card := &model.IotCard{ICCID: "89860012345678910001", CardType: "data_card", CarrierID: 1, Status: 1}
require.NoError(t, cardStore.Create(ctx, card))
now := time.Now()
binding1 := &model.DeviceSimBinding{
DeviceID: device1.ID,
IotCardID: card.ID,
SlotPosition: 1,
BindStatus: 1,
BindTime: &now,
}
require.NoError(t, bindingStore.Create(ctx, binding1))
binding2 := &model.DeviceSimBinding{
DeviceID: device2.ID,
IotCardID: card.ID,
SlotPosition: 1,
BindStatus: 1,
BindTime: &now,
}
err := bindingStore.Create(ctx, binding2)
require.Error(t, err)
appErr, ok := err.(*errors.AppError)
require.True(t, ok, "错误应该是 AppError 类型")
assert.Equal(t, errors.CodeIotCardBoundToDevice, appErr.Code)
assert.Contains(t, appErr.Message, "该卡已绑定到其他设备")
}
func TestDeviceSimBindingStore_Create_DuplicateSlot(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
bindingStore := NewDeviceSimBindingStore(tx, rdb)
deviceStore := NewDeviceStore(tx, rdb)
cardStore := NewIotCardStore(tx, rdb)
ctx := context.Background()
device := &model.Device{DeviceNo: "TEST-DEV-UC-003", Status: 1, MaxSimSlots: 4}
require.NoError(t, deviceStore.Create(ctx, device))
card1 := &model.IotCard{ICCID: "89860012345678910011", CardType: "data_card", CarrierID: 1, Status: 1}
card2 := &model.IotCard{ICCID: "89860012345678910012", CardType: "data_card", CarrierID: 1, Status: 1}
require.NoError(t, cardStore.Create(ctx, card1))
require.NoError(t, cardStore.Create(ctx, card2))
now := time.Now()
binding1 := &model.DeviceSimBinding{
DeviceID: device.ID,
IotCardID: card1.ID,
SlotPosition: 1,
BindStatus: 1,
BindTime: &now,
}
require.NoError(t, bindingStore.Create(ctx, binding1))
binding2 := &model.DeviceSimBinding{
DeviceID: device.ID,
IotCardID: card2.ID,
SlotPosition: 1,
BindStatus: 1,
BindTime: &now,
}
err := bindingStore.Create(ctx, binding2)
require.Error(t, err)
appErr, ok := err.(*errors.AppError)
require.True(t, ok, "错误应该是 AppError 类型")
assert.Equal(t, errors.CodeConflict, appErr.Code)
assert.Contains(t, appErr.Message, "该插槽已有绑定的卡")
}
func TestDeviceSimBindingStore_Create_DifferentSlots(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
bindingStore := NewDeviceSimBindingStore(tx, rdb)
deviceStore := NewDeviceStore(tx, rdb)
cardStore := NewIotCardStore(tx, rdb)
ctx := context.Background()
device := &model.Device{DeviceNo: "TEST-DEV-UC-004", Status: 1, MaxSimSlots: 4}
require.NoError(t, deviceStore.Create(ctx, device))
card1 := &model.IotCard{ICCID: "89860012345678910021", CardType: "data_card", CarrierID: 1, Status: 1}
card2 := &model.IotCard{ICCID: "89860012345678910022", CardType: "data_card", CarrierID: 1, Status: 1}
require.NoError(t, cardStore.Create(ctx, card1))
require.NoError(t, cardStore.Create(ctx, card2))
now := time.Now()
binding1 := &model.DeviceSimBinding{
DeviceID: device.ID,
IotCardID: card1.ID,
SlotPosition: 1,
BindStatus: 1,
BindTime: &now,
}
require.NoError(t, bindingStore.Create(ctx, binding1))
assert.NotZero(t, binding1.ID)
binding2 := &model.DeviceSimBinding{
DeviceID: device.ID,
IotCardID: card2.ID,
SlotPosition: 2,
BindStatus: 1,
BindTime: &now,
}
err := bindingStore.Create(ctx, binding2)
require.NoError(t, err)
assert.NotZero(t, binding2.ID)
}
func TestDeviceSimBindingStore_ConcurrentBinding(t *testing.T) {
db := testutils.GetTestDB(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
deviceStore := NewDeviceStore(db, rdb)
cardStore := NewIotCardStore(db, rdb)
ctx := context.Background()
device1 := &model.Device{DeviceNo: "TEST-CONCURRENT-001", Status: 1, MaxSimSlots: 4}
device2 := &model.Device{DeviceNo: "TEST-CONCURRENT-002", Status: 1, MaxSimSlots: 4}
require.NoError(t, deviceStore.Create(ctx, device1))
require.NoError(t, deviceStore.Create(ctx, device2))
card := &model.IotCard{ICCID: "89860012345678920001", CardType: "data_card", CarrierID: 1, Status: 1}
require.NoError(t, cardStore.Create(ctx, card))
t.Cleanup(func() {
db.Where("device_id IN ?", []uint{device1.ID, device2.ID}).Delete(&model.DeviceSimBinding{})
db.Delete(device1)
db.Delete(device2)
db.Delete(card)
})
t.Run("并发绑定同一张卡到不同设备", func(t *testing.T) {
bindingStore := NewDeviceSimBindingStore(db, rdb)
var wg sync.WaitGroup
results := make(chan error, 2)
for i, deviceID := range []uint{device1.ID, device2.ID} {
wg.Add(1)
go func(devID uint, slot int) {
defer wg.Done()
now := time.Now()
binding := &model.DeviceSimBinding{
DeviceID: devID,
IotCardID: card.ID,
SlotPosition: slot,
BindStatus: 1,
BindTime: &now,
}
results <- bindingStore.Create(ctx, binding)
}(deviceID, i+1)
}
wg.Wait()
close(results)
var successCount, errorCount int
for err := range results {
if err == nil {
successCount++
} else {
errorCount++
appErr, ok := err.(*errors.AppError)
if ok {
assert.Equal(t, errors.CodeIotCardBoundToDevice, appErr.Code)
}
}
}
assert.Equal(t, 1, successCount, "应该只有一个请求成功")
assert.Equal(t, 1, errorCount, "应该有一个请求失败")
})
}

View File

@@ -0,0 +1,183 @@
package postgres
import (
"context"
"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 DeviceStore struct {
db *gorm.DB
redis *redis.Client
}
func NewDeviceStore(db *gorm.DB, redis *redis.Client) *DeviceStore {
return &DeviceStore{
db: db,
redis: redis,
}
}
func (s *DeviceStore) Create(ctx context.Context, device *model.Device) error {
return s.db.WithContext(ctx).Create(device).Error
}
func (s *DeviceStore) CreateBatch(ctx context.Context, devices []*model.Device) error {
if len(devices) == 0 {
return nil
}
return s.db.WithContext(ctx).CreateInBatches(devices, 100).Error
}
func (s *DeviceStore) GetByID(ctx context.Context, id uint) (*model.Device, error) {
var device model.Device
if err := s.db.WithContext(ctx).First(&device, id).Error; err != nil {
return nil, err
}
return &device, nil
}
func (s *DeviceStore) GetByDeviceNo(ctx context.Context, deviceNo string) (*model.Device, error) {
var device model.Device
if err := s.db.WithContext(ctx).Where("device_no = ?", deviceNo).First(&device).Error; err != nil {
return nil, err
}
return &device, nil
}
func (s *DeviceStore) GetByIDs(ctx context.Context, ids []uint) ([]*model.Device, error) {
var devices []*model.Device
if len(ids) == 0 {
return devices, nil
}
if err := s.db.WithContext(ctx).Where("id IN ?", ids).Find(&devices).Error; err != nil {
return nil, err
}
return devices, nil
}
func (s *DeviceStore) Update(ctx context.Context, device *model.Device) error {
return s.db.WithContext(ctx).Save(device).Error
}
func (s *DeviceStore) Delete(ctx context.Context, id uint) error {
return s.db.WithContext(ctx).Delete(&model.Device{}, id).Error
}
func (s *DeviceStore) List(ctx context.Context, opts *store.QueryOptions, filters map[string]any) ([]*model.Device, int64, error) {
var devices []*model.Device
var total int64
query := s.db.WithContext(ctx).Model(&model.Device{})
if deviceNo, ok := filters["device_no"].(string); ok && deviceNo != "" {
query = query.Where("device_no LIKE ?", "%"+deviceNo+"%")
}
if deviceName, ok := filters["device_name"].(string); ok && deviceName != "" {
query = query.Where("device_name LIKE ?", "%"+deviceName+"%")
}
if status, ok := filters["status"].(int); ok && status > 0 {
query = query.Where("status = ?", status)
}
if shopID, ok := filters["shop_id"].(*uint); ok {
if shopID == nil {
query = query.Where("shop_id IS NULL")
} else {
query = query.Where("shop_id = ?", *shopID)
}
}
if batchNo, ok := filters["batch_no"].(string); ok && batchNo != "" {
query = query.Where("batch_no = ?", batchNo)
}
if deviceType, ok := filters["device_type"].(string); ok && deviceType != "" {
query = query.Where("device_type = ?", deviceType)
}
if manufacturer, ok := filters["manufacturer"].(string); ok && manufacturer != "" {
query = query.Where("manufacturer LIKE ?", "%"+manufacturer+"%")
}
if createdAtStart, ok := filters["created_at_start"].(time.Time); ok && !createdAtStart.IsZero() {
query = query.Where("created_at >= ?", createdAtStart)
}
if createdAtEnd, ok := filters["created_at_end"].(time.Time); ok && !createdAtEnd.IsZero() {
query = query.Where("created_at <= ?", createdAtEnd)
}
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(&devices).Error; err != nil {
return nil, 0, err
}
return devices, total, nil
}
func (s *DeviceStore) UpdateShopID(ctx context.Context, id uint, shopID *uint) error {
return s.db.WithContext(ctx).Model(&model.Device{}).Where("id = ?", id).Update("shop_id", shopID).Error
}
func (s *DeviceStore) BatchUpdateShopIDAndStatus(ctx context.Context, ids []uint, shopID *uint, status int) error {
if len(ids) == 0 {
return nil
}
updates := map[string]any{
"shop_id": shopID,
"status": status,
"updated_at": time.Now(),
}
return s.db.WithContext(ctx).Model(&model.Device{}).Where("id IN ?", ids).Updates(updates).Error
}
func (s *DeviceStore) ExistsByDeviceNoBatch(ctx context.Context, deviceNos []string) (map[string]bool, error) {
result := make(map[string]bool)
if len(deviceNos) == 0 {
return result, nil
}
var existingDevices []struct {
DeviceNo string
}
if err := s.db.WithContext(ctx).Model(&model.Device{}).
Select("device_no").
Where("device_no IN ?", deviceNos).
Find(&existingDevices).Error; err != nil {
return nil, err
}
for _, d := range existingDevices {
result[d.DeviceNo] = true
}
return result, nil
}
func (s *DeviceStore) GetByDeviceNos(ctx context.Context, deviceNos []string) ([]*model.Device, error) {
var devices []*model.Device
if len(deviceNos) == 0 {
return devices, nil
}
if err := s.db.WithContext(ctx).Where("device_no IN ?", deviceNos).Find(&devices).Error; err != nil {
return nil, err
}
return devices, nil
}