feat: IoT卡新增绑定设备虚拟号快照字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m28s

- tb_iot_card 新增 device_virtual_no 字段,存储绑定设备的虚拟号快照
- 列表/详情接口响应新增 device_virtual_no 字段
- 列表接口 is_standalone 改为可选参数(不传返回全部卡)
- 移除列表接口 virtual_no 查询参数
- 绑卡/解绑时同步更新 device_virtual_no 快照
- 设备导入时批量写入 device_virtual_no 快照
- 归档 feat-iot-card-device-virtual-no 变更提案

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-09 10:50:46 +08:00
parent cc56e27c01
commit e9df1e7ded
16 changed files with 421 additions and 704 deletions

View File

@@ -271,8 +271,7 @@ func (s *IotCardStore) ListStandalone(ctx context.Context, opts *store.QueryOpti
func (s *IotCardStore) listStandaloneTwoPhase(ctx context.Context, opts *store.QueryOptions, filters map[string]any) ([]*model.IotCard, int64, error) {
var total int64
query := s.db.WithContext(ctx).Model(&model.IotCard{}).
Where("is_standalone = true")
query := s.db.WithContext(ctx).Model(&model.IotCard{})
// 应用数据权限过滤
query = middleware.ApplyShopFilter(ctx, query)
query = s.applyStandaloneFilters(ctx, query, filters)
@@ -333,8 +332,7 @@ func (s *IotCardStore) listStandaloneDefault(ctx context.Context, opts *store.Qu
var cards []*model.IotCard
var total int64
query := s.db.WithContext(ctx).Model(&model.IotCard{}).
Where("is_standalone = true")
query := s.db.WithContext(ctx).Model(&model.IotCard{})
// 应用数据权限过滤
query = middleware.ApplyShopFilter(ctx, query)
query = s.applyStandaloneFilters(ctx, query, filters)
@@ -395,7 +393,7 @@ func (s *IotCardStore) listStandaloneParallel(ctx context.Context, opts *store.Q
defer wg.Done()
q := s.db.WithContext(ctx).Model(&model.IotCard{}).
Where("is_standalone = true AND deleted_at IS NULL AND shop_id = ?", sid)
Where("deleted_at IS NULL AND shop_id = ?", sid)
q = s.applyStandaloneFilters(ctx, q, filters)
var cards []*model.IotCard
@@ -410,7 +408,7 @@ func (s *IotCardStore) listStandaloneParallel(ctx context.Context, opts *store.Q
var count int64
if !hasCachedTotal {
countQ := s.db.WithContext(ctx).Model(&model.IotCard{}).
Where("is_standalone = true AND deleted_at IS NULL AND shop_id = ?", sid)
Where("deleted_at IS NULL AND shop_id = ?", sid)
countQ = s.applyStandaloneFilters(ctx, countQ, filters)
if err := countQ.Count(&count).Error; err != nil {
results[idx] = shopResult{err: err}
@@ -609,9 +607,14 @@ func (s *IotCardStore) listStandaloneParallelTwoPhase(ctx context.Context, opts
}
// applyStandaloneFilters 应用独立卡列表的通用过滤条件
// 注意:不包含 is_standalone、shop_id、deleted_at 条件(由调用方控制)
// 注意:不包含 shop_id、deleted_at 条件(由调用方控制)
// 也不包含 subordinate_shop_ids仅用于路由选择不作为查询条件
// is_standalone 仅在 filters["is_standalone"] 非 nil 时拼接(不传则不过滤)
func (s *IotCardStore) applyStandaloneFilters(ctx context.Context, query *gorm.DB, filters map[string]any) *gorm.DB {
// is_standalone 为可选过滤条件,仅在明确传入时应用
if isStandalone, ok := filters["is_standalone"].(*bool); ok && isStandalone != nil {
query = query.Where("is_standalone = ?", *isStandalone)
}
// 子查询无需数据权限过滤(在不同表上执行)
if status, ok := filters["status"].(int); ok && status > 0 {
@@ -823,6 +826,24 @@ func (s *IotCardStore) GetByIDsWithEnterpriseFilter(ctx context.Context, cardIDs
return cards, nil
}
// UpdateDeviceVirtualNo 更新卡的设备虚拟号快照
// 绑定设备时写入 virtual_no解绑时写入空字符串
func (s *IotCardStore) UpdateDeviceVirtualNo(ctx context.Context, cardID uint, deviceVirtualNo string) error {
return s.db.WithContext(ctx).Model(&model.IotCard{}).
Where("id = ?", cardID).
Update("device_virtual_no", deviceVirtualNo).Error
}
// BatchUpdateDeviceVirtualNo 批量更新卡的设备虚拟号快照(设备导入时使用)
func (s *IotCardStore) BatchUpdateDeviceVirtualNo(ctx context.Context, cardIDs []uint, deviceVirtualNo string) error {
if len(cardIDs) == 0 {
return nil
}
return s.db.WithContext(ctx).Model(&model.IotCard{}).
Where("id IN ?", cardIDs).
Update("device_virtual_no", deviceVirtualNo).Error
}
// BatchUpdateSeriesID 批量更新卡的套餐系列ID
// 用于批量设置或清除卡与套餐系列的关联关系
func (s *IotCardStore) BatchUpdateSeriesID(ctx context.Context, cardIDs []uint, seriesID *uint) error {