feat: 新增数据库迁移,重命名 device_no 为 virtual_no,新增 iot_card.virtual_no 和 package.virtual_ratio 字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m3s
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:
@@ -2,6 +2,7 @@ package iot_card
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/gateway"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
@@ -795,20 +796,9 @@ func (s *Service) buildCardNotFoundFailedItems(iccids []string) []dto.CardSeries
|
||||
return items
|
||||
}
|
||||
|
||||
// SyncCardStatusFromGateway 从 Gateway 同步卡状态(示例方法)
|
||||
func (s *Service) SyncCardStatusFromGateway(ctx context.Context, iccid string) error {
|
||||
if s.gatewayClient == nil {
|
||||
return errors.New(errors.CodeGatewayError, "Gateway 客户端未配置")
|
||||
}
|
||||
|
||||
resp, err := s.gatewayClient.QueryCardStatus(ctx, &gateway.CardStatusReq{
|
||||
CardNo: iccid,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error("查询卡状态失败", zap.String("iccid", iccid), zap.Error(err))
|
||||
return errors.Wrap(errors.CodeGatewayError, err, "查询卡状态失败")
|
||||
}
|
||||
|
||||
// RefreshCardDataFromGateway 从 Gateway 完整同步卡数据
|
||||
// 调用网关查询网络状态、实名状态、本月流量,并写回数据库
|
||||
func (s *Service) RefreshCardDataFromGateway(ctx context.Context, iccid string) error {
|
||||
card, err := s.iotCardStore.GetByICCID(ctx, iccid)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
@@ -817,40 +807,73 @@ func (s *Service) SyncCardStatusFromGateway(ctx context.Context, iccid string) e
|
||||
return err
|
||||
}
|
||||
|
||||
var newStatus int
|
||||
switch resp.CardStatus {
|
||||
case "准备":
|
||||
newStatus = constants.IotCardStatusInStock
|
||||
case "正常":
|
||||
newStatus = constants.IotCardStatusDistributed
|
||||
case "停机":
|
||||
newStatus = constants.IotCardStatusSuspended
|
||||
default:
|
||||
s.logger.Warn("未知的卡状态", zap.String("cardStatus", resp.CardStatus))
|
||||
return nil
|
||||
syncTime := time.Now()
|
||||
updates := map[string]any{
|
||||
"last_sync_time": syncTime,
|
||||
}
|
||||
|
||||
if card.Status != newStatus {
|
||||
oldStatus := card.Status
|
||||
card.Status = newStatus
|
||||
if err := s.iotCardStore.Update(ctx, card); err != nil {
|
||||
return err
|
||||
if s.gatewayClient != nil {
|
||||
// 1. 查询网络状态(卡的开/停机状态)
|
||||
statusResp, err := s.gatewayClient.QueryCardStatus(ctx, &gateway.CardStatusReq{
|
||||
CardNo: iccid,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Warn("刷新卡数据:查询网络状态失败", zap.String("iccid", iccid), zap.Error(err))
|
||||
} else {
|
||||
networkStatus := parseNetworkStatus(statusResp.CardStatus)
|
||||
updates["network_status"] = networkStatus
|
||||
}
|
||||
s.logger.Info("同步卡状态成功",
|
||||
zap.String("iccid", iccid),
|
||||
zap.Int("oldStatus", oldStatus),
|
||||
zap.Int("newStatus", newStatus),
|
||||
)
|
||||
|
||||
// 通知轮询调度器状态变化
|
||||
if s.pollingCallback != nil {
|
||||
s.pollingCallback.OnCardStatusChanged(ctx, card.ID)
|
||||
// 2. 查询实名状态
|
||||
realnameResp, err := s.gatewayClient.QueryRealnameStatus(ctx, &gateway.CardStatusReq{
|
||||
CardNo: iccid,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Warn("刷新卡数据:查询实名状态失败", zap.String("iccid", iccid), zap.Error(err))
|
||||
} else {
|
||||
realNameStatus := parseGatewayRealnameStatus(realnameResp.RealStatus)
|
||||
updates["real_name_status"] = realNameStatus
|
||||
}
|
||||
|
||||
// 3. 查询本月流量用量
|
||||
flowResp, err := s.gatewayClient.QueryFlow(ctx, &gateway.FlowQueryReq{
|
||||
CardNo: iccid,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Warn("刷新卡数据:查询流量失败", zap.String("iccid", iccid), zap.Error(err))
|
||||
} else {
|
||||
updates["current_month_usage_mb"] = flowResp.Used
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.db.WithContext(ctx).Model(&model.IotCard{}).
|
||||
Where("id = ?", card.ID).
|
||||
Updates(updates).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "更新卡数据失败")
|
||||
}
|
||||
|
||||
s.logger.Info("刷新卡数据成功", zap.String("iccid", iccid), zap.Uint("card_id", card.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseNetworkStatus 将网关返回的卡状态字符串转换为 network_status 数值
|
||||
// 停机→0,其他(准备/正常)→1
|
||||
func parseNetworkStatus(cardStatus string) int {
|
||||
if cardStatus == "停机" {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// parseGatewayRealnameStatus 将网关返回的实名状态布尔值转换为 real_name_status 数值
|
||||
// true=已实名(2),false=未实名(0)
|
||||
func parseGatewayRealnameStatus(realStatus bool) int {
|
||||
if realStatus {
|
||||
return 2
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// UpdatePollingStatus 更新卡的轮询状态
|
||||
// 启用或禁用卡的轮询功能
|
||||
func (s *Service) UpdatePollingStatus(ctx context.Context, cardID uint, enablePolling bool) error {
|
||||
|
||||
@@ -8,22 +8,25 @@ import (
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
stderrors "errors"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/gateway"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// StopResumeService 停复机服务
|
||||
// 任务 24.2: 处理 IoT 卡的自动停机和复机逻辑
|
||||
// 处理 IoT 卡的自动停机、复机和手动停复机逻辑
|
||||
type StopResumeService struct {
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
iotCardStore *postgres.IotCardStore
|
||||
gatewayClient *gateway.Client
|
||||
logger *zap.Logger
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceSimBindingStore *postgres.DeviceSimBindingStore
|
||||
gatewayClient *gateway.Client
|
||||
logger *zap.Logger
|
||||
|
||||
// 重试配置
|
||||
maxRetries int
|
||||
retryInterval time.Duration
|
||||
}
|
||||
@@ -33,17 +36,19 @@ func NewStopResumeService(
|
||||
db *gorm.DB,
|
||||
redis *redis.Client,
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
deviceSimBindingStore *postgres.DeviceSimBindingStore,
|
||||
gatewayClient *gateway.Client,
|
||||
logger *zap.Logger,
|
||||
) *StopResumeService {
|
||||
return &StopResumeService{
|
||||
db: db,
|
||||
redis: redis,
|
||||
iotCardStore: iotCardStore,
|
||||
gatewayClient: gatewayClient,
|
||||
logger: logger,
|
||||
maxRetries: 3, // 默认最多重试 3 次
|
||||
retryInterval: 2 * time.Second, // 默认重试间隔 2 秒
|
||||
db: db,
|
||||
redis: redis,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceSimBindingStore: deviceSimBindingStore,
|
||||
gatewayClient: gatewayClient,
|
||||
logger: logger,
|
||||
maxRetries: 3,
|
||||
retryInterval: 2 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,3 +238,75 @@ func (s *StopResumeService) resumeCardWithRetry(ctx context.Context, card *model
|
||||
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// ManualStopCard 手动停机单张卡(通过ICCID)
|
||||
func (s *StopResumeService) ManualStopCard(ctx context.Context, iccid string) error {
|
||||
card, err := s.iotCardStore.GetByICCID(ctx, iccid)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeNotFound, "卡不存在")
|
||||
}
|
||||
|
||||
if card.RealNameStatus != constants.RealNameStatusVerified {
|
||||
return errors.New(errors.CodeForbidden, "卡未实名,无法操作")
|
||||
}
|
||||
|
||||
// 检查绑定设备是否在复机保护期
|
||||
if s.deviceSimBindingStore != nil && s.redis != nil {
|
||||
binding, bindErr := s.deviceSimBindingStore.GetActiveBindingByCardID(ctx, card.ID)
|
||||
if bindErr == nil && binding != nil {
|
||||
exists, _ := s.redis.Exists(ctx, constants.RedisDeviceProtectKey(binding.DeviceID, "start")).Result()
|
||||
if exists > 0 {
|
||||
return errors.New(errors.CodeForbidden, "设备复机保护期内,禁止停机")
|
||||
}
|
||||
} else if bindErr != nil && !stderrors.Is(bindErr, gorm.ErrRecordNotFound) {
|
||||
return errors.Wrap(errors.CodeInternalError, bindErr, "查询卡绑定关系失败")
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.stopCardWithRetry(ctx, card); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "调网关停机失败")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
return s.db.WithContext(ctx).Model(card).Updates(map[string]any{
|
||||
"network_status": constants.NetworkStatusOffline,
|
||||
"stopped_at": now,
|
||||
"stop_reason": constants.StopReasonManual,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// ManualStartCard 手动复机单张卡(通过ICCID)
|
||||
func (s *StopResumeService) ManualStartCard(ctx context.Context, iccid string) error {
|
||||
card, err := s.iotCardStore.GetByICCID(ctx, iccid)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeNotFound, "卡不存在")
|
||||
}
|
||||
|
||||
if card.RealNameStatus != constants.RealNameStatusVerified {
|
||||
return errors.New(errors.CodeForbidden, "卡未实名,无法操作")
|
||||
}
|
||||
|
||||
// 检查绑定设备是否在停机保护期
|
||||
if s.deviceSimBindingStore != nil && s.redis != nil {
|
||||
binding, bindErr := s.deviceSimBindingStore.GetActiveBindingByCardID(ctx, card.ID)
|
||||
if bindErr == nil && binding != nil {
|
||||
exists, _ := s.redis.Exists(ctx, constants.RedisDeviceProtectKey(binding.DeviceID, "stop")).Result()
|
||||
if exists > 0 {
|
||||
return errors.New(errors.CodeForbidden, "设备停机保护期内,禁止复机")
|
||||
}
|
||||
} else if bindErr != nil && !stderrors.Is(bindErr, gorm.ErrRecordNotFound) {
|
||||
return errors.Wrap(errors.CodeInternalError, bindErr, "查询卡绑定关系失败")
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.resumeCardWithRetry(ctx, card); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "调网关复机失败")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
return s.db.WithContext(ctx).Model(card).Updates(map[string]any{
|
||||
"network_status": constants.NetworkStatusOnline,
|
||||
"resumed_at": now,
|
||||
"stop_reason": "",
|
||||
}).Error
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user