收口七月卡状态回调与系列授权兼容契约
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
完成运营商实名回调、业务事件观测序列与受控配置装配,同时恢复 UR43 已交付的 packages[].remove 字段及旧响应兼容,统一更新 OpenSpec、OpenAPI 和交付文档。 Constraint: 七月测试环境里程碑不新增或运行自动化测试 Rejected: 以必填 operation_type 替换 packages[].remove | 会破坏已交付前端契约 Confidence: high Scope-risk: broad Directive: 后续修改系列套餐管理接口必须保持 packages[].remove 和 ShopSeriesGrantResponse 兼容 Tested: go run ./cmd/gendocs;go build -buildvcs=false ./...;openspec validate complete-july-iteration-test-release --strict;git diff --check Not-tested: 按本 Change 约定未运行 go test,真实运营商与 Gateway 联调延期
This commit is contained in:
@@ -2,8 +2,11 @@ package device
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cardObservationApp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation"
|
||||
"github.com/google/uuid"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
@@ -37,6 +40,93 @@ type Service struct {
|
||||
enterpriseDeviceAuthStore *postgres.EnterpriseDeviceAuthorizationStore
|
||||
enterpriseStore *postgres.EnterpriseStore
|
||||
packageExpiryQuery *packageexpiry.Query
|
||||
observationSeriesEvents cardObservationApp.SeriesEventWriter
|
||||
observationSeries cardObservationApp.BestEffortSeriesDispatcher
|
||||
}
|
||||
|
||||
// SetObservationSeriesEventWriter 注入设备停复机成功观测序列 Outbox Writer。
|
||||
func (s *Service) SetObservationSeriesEventWriter(writer cardObservationApp.SeriesEventWriter) {
|
||||
s.observationSeriesEvents = writer
|
||||
}
|
||||
|
||||
// SetObservationSeriesDispatcher 注入设备控制成功后的后台观测分发器。
|
||||
func (s *Service) SetObservationSeriesDispatcher(dispatcher cardObservationApp.BestEffortSeriesDispatcher) {
|
||||
s.observationSeries = dispatcher
|
||||
}
|
||||
|
||||
type deviceControlObservationSnapshot struct {
|
||||
SourceCardID uint
|
||||
TargetCardID uint
|
||||
BoundCardIDs []uint
|
||||
TargetICCID string
|
||||
}
|
||||
|
||||
func (s *Service) captureDeviceControlObservation(ctx context.Context, deviceID, knownTargetCardID uint, targetICCID string) deviceControlObservationSnapshot {
|
||||
snapshot := deviceControlObservationSnapshot{TargetICCID: targetICCID}
|
||||
bindings, err := s.deviceSimBindingStore.ListByDeviceID(ctx, deviceID)
|
||||
if err != nil {
|
||||
logger.GetAppLogger().Warn("冻结设备控制观测绑定快照失败,继续执行原操作", zap.Uint("device_id", deviceID), zap.Error(err))
|
||||
return snapshot
|
||||
}
|
||||
bound := make(map[uint]struct{}, len(bindings))
|
||||
for _, binding := range bindings {
|
||||
if binding == nil || binding.IotCardID == 0 {
|
||||
continue
|
||||
}
|
||||
snapshot.BoundCardIDs = append(snapshot.BoundCardIDs, binding.IotCardID)
|
||||
bound[binding.IotCardID] = struct{}{}
|
||||
if binding.IsCurrent {
|
||||
snapshot.SourceCardID = binding.IotCardID
|
||||
}
|
||||
}
|
||||
if knownTargetCardID != 0 {
|
||||
if _, ok := bound[knownTargetCardID]; ok {
|
||||
snapshot.TargetCardID = knownTargetCardID
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
if strings.TrimSpace(targetICCID) == "" || len(snapshot.BoundCardIDs) == 0 {
|
||||
return snapshot
|
||||
}
|
||||
cards, err := s.iotCardStore.GetByIDs(ctx, snapshot.BoundCardIDs)
|
||||
if err != nil {
|
||||
logger.GetAppLogger().Warn("冻结设备控制观测目标卡快照失败,继续执行原操作", zap.Uint("device_id", deviceID), zap.String("target_iccid", targetICCID), zap.Error(err))
|
||||
return snapshot
|
||||
}
|
||||
for _, card := range cards {
|
||||
if card != nil && cardMatchesICCID(card, targetICCID) {
|
||||
snapshot.TargetCardID = card.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
func cardMatchesICCID(card *model.IotCard, expected string) bool {
|
||||
expected = strings.TrimSpace(expected)
|
||||
return strings.EqualFold(strings.TrimSpace(card.ICCID), expected) ||
|
||||
strings.EqualFold(strings.TrimSpace(card.ICCID19), expected) ||
|
||||
(card.ICCID20 != nil && strings.EqualFold(strings.TrimSpace(*card.ICCID20), expected))
|
||||
}
|
||||
|
||||
func (s *Service) dispatchDeviceControlObservation(ctx context.Context, deviceID uint, scene string, snapshot deviceControlObservationSnapshot, includeTargetTraffic bool) {
|
||||
if s.observationSeries == nil || deviceID == 0 {
|
||||
return
|
||||
}
|
||||
requestID := ""
|
||||
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
|
||||
requestID = *value
|
||||
}
|
||||
request := cardObservationApp.SeriesRequest{
|
||||
Scene: scene, Source: constants.CardObservationSourceBusinessEvent,
|
||||
RequestID: requestID, CorrelationID: requestID,
|
||||
}
|
||||
s.observationSeries.DispatchDeviceControl(ctx, cardObservationApp.DeviceControlSeriesRequest{
|
||||
DeviceID: deviceID, TargetICCID: snapshot.TargetICCID,
|
||||
SourceCardID: snapshot.SourceCardID, TargetCardID: snapshot.TargetCardID,
|
||||
BoundCardIDs: snapshot.BoundCardIDs,
|
||||
IncludeTargetTraffic: includeTargetTraffic, Request: request,
|
||||
})
|
||||
}
|
||||
|
||||
// SetPackageExpiryQuery 注入套餐最终到期查询,供列表使用批量投影。
|
||||
@@ -1678,11 +1768,11 @@ func (s *Service) StopDevice(ctx context.Context, deviceID uint) (*dto.DeviceSus
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if dbErr := s.iotCardStore.UpdateFields(ctx, card.ID, map[string]any{
|
||||
if dbErr := s.updateCardAndAppendNetworkSeries(ctx, card.ID, map[string]any{
|
||||
"network_status": constants.NetworkStatusOffline,
|
||||
"stopped_at": now,
|
||||
"stop_reason": constants.StopReasonManual,
|
||||
}); dbErr != nil {
|
||||
}, constants.CardObservationSceneBusinessStop, "offline", s.gatewayClient != nil); dbErr != nil {
|
||||
log.Error("设备停机-更新卡状态失败",
|
||||
zap.Uint("card_id", card.ID),
|
||||
zap.Error(dbErr))
|
||||
@@ -1699,7 +1789,7 @@ func (s *Service) StopDevice(ctx context.Context, deviceID uint) (*dto.DeviceSus
|
||||
|
||||
// 成功停机至少一张卡后设置保护期
|
||||
if successCount > 0 && s.redis != nil {
|
||||
s.redis.Set(ctx, constants.RedisDeviceProtectKey(deviceID, "stop"), 1, constants.DeviceProtectPeriodDuration)
|
||||
s.redis.Set(ctx, constants.RedisDeviceProtectKey(deviceID, "stop"), uuid.NewString(), constants.DeviceProtectPeriodDuration)
|
||||
s.redis.Del(ctx, constants.RedisDeviceProtectKey(deviceID, "start"))
|
||||
}
|
||||
|
||||
@@ -1907,11 +1997,11 @@ func (s *Service) StartDevice(ctx context.Context, deviceID uint) error {
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if dbErr := s.iotCardStore.UpdateFields(ctx, card.ID, map[string]any{
|
||||
if dbErr := s.updateCardAndAppendNetworkSeries(ctx, card.ID, map[string]any{
|
||||
"network_status": constants.NetworkStatusOnline,
|
||||
"resumed_at": now,
|
||||
"stop_reason": "",
|
||||
}); dbErr != nil {
|
||||
}, constants.CardObservationSceneBusinessResume, "online", s.gatewayClient != nil); dbErr != nil {
|
||||
log.Error("设备复机-更新卡状态失败",
|
||||
zap.Uint("card_id", card.ID),
|
||||
zap.Error(dbErr))
|
||||
@@ -1926,7 +2016,7 @@ func (s *Service) StartDevice(ctx context.Context, deviceID uint) error {
|
||||
|
||||
// 成功复机至少一张卡后设置保护期
|
||||
if successCount > 0 && s.redis != nil {
|
||||
s.redis.Set(ctx, constants.RedisDeviceProtectKey(deviceID, "start"), 1, constants.DeviceProtectPeriodDuration)
|
||||
s.redis.Set(ctx, constants.RedisDeviceProtectKey(deviceID, "start"), uuid.NewString(), constants.DeviceProtectPeriodDuration)
|
||||
s.redis.Del(ctx, constants.RedisDeviceProtectKey(deviceID, "stop"))
|
||||
}
|
||||
|
||||
@@ -2089,3 +2179,29 @@ func (s *Service) invalidatePollingCardCache(cardID uint) {
|
||||
}
|
||||
_ = s.redis.Del(context.Background(), constants.RedisPollingCardInfoKey(cardID)).Err()
|
||||
}
|
||||
|
||||
func (s *Service) updateCardAndAppendNetworkSeries(ctx context.Context, cardID uint, fields map[string]any, scene, expected string, upstreamCalled bool) error {
|
||||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&model.IotCard{}).Where("id = ?", cardID).Updates(fields).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新设备绑定卡停复机状态失败")
|
||||
}
|
||||
if !upstreamCalled || cardObservationApp.IsSeriesTriggerSuppressed(ctx) {
|
||||
return nil
|
||||
}
|
||||
if s.observationSeriesEvents == nil {
|
||||
return errors.New(errors.CodeInternalError, "设备停复机观测 Outbox Writer 未配置")
|
||||
}
|
||||
operationID := uuid.NewString()
|
||||
requestID := operationID
|
||||
if value := middleware.GetRequestIDFromContext(ctx); value != nil && *value != "" {
|
||||
requestID = *value
|
||||
}
|
||||
return s.observationSeriesEvents.AppendSeriesRequested(ctx, tx, cardObservationApp.SeriesRequestedEvent{
|
||||
EventID: "card-observation:network-command:" + operationID,
|
||||
Scene: scene, ResourceType: constants.CardObservationResourceTypeCard, ResourceID: cardID,
|
||||
SyncTypes: []string{constants.CardObservationSyncTypeNetwork}, ExpectedValue: expected,
|
||||
Source: constants.CardObservationSourceBusinessEvent, OccurredAt: time.Now().UTC(),
|
||||
RequestID: requestID, CorrelationID: requestID,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user