feat: 实现设备管理和设备导入功能,修复测试问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m30s
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:
135
internal/store/postgres/device_import_task_store.go
Normal file
135
internal/store/postgres/device_import_task_store.go
Normal 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)
|
||||
}
|
||||
202
internal/store/postgres/device_sim_binding_store.go
Normal file
202
internal/store/postgres/device_sim_binding_store.go
Normal 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
|
||||
}
|
||||
209
internal/store/postgres/device_sim_binding_store_test.go
Normal file
209
internal/store/postgres/device_sim_binding_store_test.go
Normal 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, "应该有一个请求失败")
|
||||
})
|
||||
}
|
||||
183
internal/store/postgres/device_store.go
Normal file
183
internal/store/postgres/device_store.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user