feat: 新增数据库迁移,重命名 device_no 为 virtual_no,新增 iot_card.virtual_no 和 package.virtual_ratio 字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m3s

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-03-14 18:27:28 +08:00
parent b5147d1acb
commit b9c3875c08
77 changed files with 5832 additions and 2393 deletions

View File

@@ -757,6 +757,8 @@ func (h *PollingHandler) requeueCard(ctx context.Context, cardID uint, taskType
intervalSeconds = 600 // 默认 10 分钟
case constants.TaskTypePollingPackage:
intervalSeconds = 600 // 默认 10 分钟
case constants.TaskTypePollingProtect:
intervalSeconds = 300 // 默认 5 分钟
default:
return nil
}
@@ -773,6 +775,8 @@ func (h *PollingHandler) requeueCard(ctx context.Context, cardID uint, taskType
queueKey = constants.RedisPollingQueueCarddataKey()
case constants.TaskTypePollingPackage:
queueKey = constants.RedisPollingQueuePackageKey()
case constants.TaskTypePollingProtect:
queueKey = constants.RedisPollingQueueProtectKey()
}
// 添加到队列
@@ -943,6 +947,103 @@ func (h *PollingHandler) getCardWithCache(ctx context.Context, cardID uint) (*mo
return card, nil
}
// HandleProtectConsistencyCheck 保护期一致性检查
// 检查绑定设备is_standalone=false且已实名real_name_status=2的卡
// stop 保护期 + 开机 → 调网关停机start 保护期 + 停机 → 调网关复机
func (h *PollingHandler) HandleProtectConsistencyCheck(ctx context.Context, t *asynq.Task) error {
var payload PollingTaskPayload
if err := sonic.Unmarshal(t.Payload(), &payload); err != nil {
h.logger.Error("解析保护期检查任务载荷失败", zap.Error(err))
return nil
}
cardID, err := strconv.ParseUint(payload.CardID, 10, 64)
if err != nil {
h.logger.Error("解析卡ID失败", zap.String("card_id", payload.CardID), zap.Error(err))
return nil
}
// 查询卡信息
var card model.IotCard
if err := h.db.WithContext(ctx).Where("id = ?", cardID).First(&card).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil
}
h.logger.Error("查询卡信息失败", zap.Uint64("card_id", cardID), zap.Error(err))
return nil
}
// 未绑设备(独立卡),跳过
if card.IsStandalone {
return nil
}
// 未实名,跳过
if card.RealNameStatus != constants.RealNameStatusVerified {
return nil
}
// 查绑定设备
binding, err := h.deviceSimBindingStore.GetActiveBindingByCardID(ctx, card.ID)
if err != nil || binding == nil {
return nil
}
deviceID := binding.DeviceID
// 检查 stop 保护期:设备处于停机保护期,但卡是开机状态 → 需要停机
stopProtect := h.redis.Exists(ctx, constants.RedisDeviceProtectKey(deviceID, "stop")).Val() > 0
if stopProtect && card.NetworkStatus == constants.NetworkStatusOnline {
h.logger.Info("保护期一致性检查:停机保护期内发现开机卡,执行停机",
zap.Uint("card_id", card.ID),
zap.String("iccid", card.ICCID),
zap.Uint("device_id", deviceID))
if h.gatewayClient != nil {
if err := h.gatewayClient.StopCard(ctx, &gateway.CardOperationReq{CardNo: card.ICCID}); err != nil {
h.logger.Error("保护期一致性停机失败",
zap.Uint("card_id", card.ID),
zap.Error(err))
return nil
}
}
h.db.Model(&model.IotCard{}).Where("id = ?", card.ID).Updates(map[string]any{
"network_status": constants.NetworkStatusOffline,
"stopped_at": time.Now(),
"stop_reason": "保护期一致性检查自动停机",
})
h.updateCardCache(ctx, card.ID, map[string]any{"network_status": constants.NetworkStatusOffline})
return nil
}
// 检查 start 保护期:设备处于复机保护期,但卡是停机状态 → 需要复机
startProtect := h.redis.Exists(ctx, constants.RedisDeviceProtectKey(deviceID, "start")).Val() > 0
if startProtect && card.NetworkStatus == constants.NetworkStatusOffline {
h.logger.Info("保护期一致性检查:复机保护期内发现停机卡,执行复机",
zap.Uint("card_id", card.ID),
zap.String("iccid", card.ICCID),
zap.Uint("device_id", deviceID))
if h.gatewayClient != nil {
if err := h.gatewayClient.StartCard(ctx, &gateway.CardOperationReq{CardNo: card.ICCID}); err != nil {
h.logger.Error("保护期一致性复机失败",
zap.Uint("card_id", card.ID),
zap.Error(err))
return nil
}
}
h.db.Model(&model.IotCard{}).Where("id = ?", card.ID).Updates(map[string]any{
"network_status": constants.NetworkStatusOnline,
"resumed_at": time.Now(),
"stop_reason": "",
})
h.updateCardCache(ctx, card.ID, map[string]any{"network_status": constants.NetworkStatusOnline})
}
return nil
}
// triggerFirstRealnameActivation 任务 21.3-21.4: 首次实名后触发套餐激活
func (h *PollingHandler) triggerFirstRealnameActivation(ctx context.Context, cardID uint) {
// 任务 21.3: 查询该卡是否有待激活套餐