337 lines
14 KiB
Go
337 lines
14 KiB
Go
// Package cardobservation 提供卡观测的 Outbox、缓存和消费者适配器。
|
|
package cardobservation
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/bytedance/sonic"
|
|
"github.com/redis/go-redis/v9"
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
|
|
cardapp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation"
|
|
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
|
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
|
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
|
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
|
)
|
|
|
|
func uintString(value uint) string {
|
|
return strconv.FormatUint(uint64(value), 10)
|
|
}
|
|
|
|
// EventWriter 将卡实名状态变化写入公共 Outbox。
|
|
type EventWriter struct {
|
|
outbox *outbox.Repository
|
|
}
|
|
|
|
// NewEventWriter 创建卡实名事件 Writer。
|
|
func NewEventWriter(repository *outbox.Repository) *EventWriter {
|
|
return &EventWriter{outbox: repository}
|
|
}
|
|
|
|
// Append 在调用方事务中追加卡实名状态变化事件。
|
|
func (w *EventWriter) AppendRealname(ctx context.Context, tx *gorm.DB, event cardapp.RealnameChangedEvent) error {
|
|
if w == nil || w.outbox == nil {
|
|
return errors.New(errors.CodeInternalError, "卡实名 Outbox Writer 未配置")
|
|
}
|
|
_, err := w.outbox.Append(ctx, tx, outbox.Envelope{
|
|
EventID: event.EventID, EventType: constants.OutboxEventTypeCardRealnameChanged,
|
|
PayloadVersion: constants.CardRealnameChangedPayloadVersionV1,
|
|
AggregateType: "iot_card", AggregateID: uintString(event.CardID),
|
|
ResourceType: constants.AssetTypeIotCard, ResourceID: uintString(event.CardID),
|
|
BusinessKey: event.EventID, RequestID: event.RequestID, CorrelationID: event.CorrelationID,
|
|
Payload: event,
|
|
})
|
|
return err
|
|
}
|
|
|
|
// AppendTraffic 在调用方事务中追加卡流量正增量事件。
|
|
func (w *EventWriter) AppendTraffic(ctx context.Context, tx *gorm.DB, event cardapp.TrafficIncrementedEvent) error {
|
|
if w == nil || w.outbox == nil {
|
|
return errors.New(errors.CodeInternalError, "卡流量 Outbox Writer 未配置")
|
|
}
|
|
_, err := w.outbox.Append(ctx, tx, outbox.Envelope{
|
|
EventID: event.EventID, EventType: constants.OutboxEventTypeCardTrafficIncremented,
|
|
PayloadVersion: constants.CardTrafficIncrementedPayloadVersionV1,
|
|
AggregateType: "iot_card", AggregateID: uintString(event.CardID),
|
|
ResourceType: constants.AssetTypeIotCard, ResourceID: uintString(event.CardID),
|
|
BusinessKey: event.EventID, RequestID: event.RequestID, CorrelationID: event.CorrelationID,
|
|
Payload: event,
|
|
})
|
|
return err
|
|
}
|
|
|
|
// AppendNetwork 在调用方事务中追加卡网络状态变化事件。
|
|
func (w *EventWriter) AppendNetwork(ctx context.Context, tx *gorm.DB, event cardapp.NetworkChangedEvent) error {
|
|
if w == nil || w.outbox == nil {
|
|
return errors.New(errors.CodeInternalError, "卡网络 Outbox Writer 未配置")
|
|
}
|
|
_, err := w.outbox.Append(ctx, tx, outbox.Envelope{
|
|
EventID: event.EventID, EventType: constants.OutboxEventTypeCardNetworkChanged,
|
|
PayloadVersion: constants.CardNetworkChangedPayloadVersionV1,
|
|
AggregateType: "iot_card", AggregateID: uintString(event.CardID),
|
|
ResourceType: constants.AssetTypeIotCard, ResourceID: uintString(event.CardID),
|
|
BusinessKey: event.EventID, RequestID: event.RequestID, CorrelationID: event.CorrelationID,
|
|
Payload: event,
|
|
})
|
|
return err
|
|
}
|
|
|
|
// CacheInvalidator 在提交后删除轮询卡缓存和旧 Redis 逆转计数。
|
|
type CacheInvalidator struct {
|
|
redis *redis.Client
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewCacheInvalidator 创建卡观测缓存失效适配器。
|
|
func NewCacheInvalidator(redisClient *redis.Client, logger *zap.Logger) *CacheInvalidator {
|
|
return &CacheInvalidator{redis: redisClient, logger: logger}
|
|
}
|
|
|
|
// Invalidate 删除可由数据库重建的缓存;失败只告警,不伪造事务回滚。
|
|
func (i *CacheInvalidator) Invalidate(ctx context.Context, cardID uint) {
|
|
if i == nil || i.redis == nil {
|
|
return
|
|
}
|
|
if err := i.redis.Del(ctx, constants.RedisPollingCardInfoKey(cardID), constants.RedisPollingRealnameReversalCountKey(cardID)).Err(); err != nil && i.logger != nil {
|
|
i.logger.Warn("卡实名事实已提交但缓存失效失败", zap.Uint("card_id", cardID), zap.Error(err))
|
|
}
|
|
}
|
|
|
|
// RealnameActivator 激活卡或设备待实名套餐。
|
|
type RealnameActivator interface {
|
|
ActivateByRealname(ctx context.Context, carrierType string, carrierID uint) error
|
|
}
|
|
|
|
// TrafficDeductor 按卡流量正增量扣减套餐流量。
|
|
type TrafficDeductor interface {
|
|
DeductDataUsage(ctx context.Context, carrierType string, carrierID uint, usageMB float64) error
|
|
}
|
|
|
|
// StopResumeEvaluator 根据卡的当前事实执行停复机评估。
|
|
type StopResumeEvaluator interface {
|
|
EvaluateAndAct(ctx context.Context, card *model.IotCard) error
|
|
}
|
|
|
|
// DeviceBindingReader 查询卡当前绑定的设备。
|
|
type DeviceBindingReader interface {
|
|
GetActiveBindingByCardID(ctx context.Context, cardID uint) (*model.DeviceSimBinding, error)
|
|
}
|
|
|
|
// RealnameChangedConsumer 消费实名变化事件并串联现有幂等业务副作用。
|
|
type RealnameChangedConsumer struct {
|
|
db *gorm.DB
|
|
activator RealnameActivator
|
|
binding DeviceBindingReader
|
|
evaluator StopResumeEvaluator
|
|
}
|
|
|
|
// NetworkChangedConsumer 消费网络状态变化并按当前权威事实执行停复机评估。
|
|
type NetworkChangedConsumer struct {
|
|
db *gorm.DB
|
|
evaluator StopResumeEvaluator
|
|
}
|
|
|
|
// NewNetworkChangedConsumer 创建卡网络状态变化事件消费者。
|
|
func NewNetworkChangedConsumer(db *gorm.DB, evaluator StopResumeEvaluator) *NetworkChangedConsumer {
|
|
return &NetworkChangedConsumer{db: db, evaluator: evaluator}
|
|
}
|
|
|
|
// Consume 校验网络事件并按当前卡事实执行评估。
|
|
func (c *NetworkChangedConsumer) Consume(ctx context.Context, envelope outbox.DeliveryEnvelope) error {
|
|
if c == nil || c.db == nil {
|
|
return errors.New(errors.CodeInternalError, "卡网络事件消费者未配置")
|
|
}
|
|
if envelope.EventType != constants.OutboxEventTypeCardNetworkChanged || envelope.PayloadVersion != constants.CardNetworkChangedPayloadVersionV1 {
|
|
return errors.New(errors.CodeInvalidParam, "卡网络事件类型或版本不受支持")
|
|
}
|
|
var event cardapp.NetworkChangedEvent
|
|
if err := sonic.Unmarshal(envelope.Payload, &event); err != nil {
|
|
return errors.Wrap(errors.CodeInvalidParam, err, "卡网络事件载荷无法解析")
|
|
}
|
|
if event.EventID != envelope.EventID || event.CardID == 0 || event.BeforeStatus == event.AfterStatus {
|
|
return errors.New(errors.CodeInvalidParam, "卡网络事件载荷不完整")
|
|
}
|
|
if c.evaluator == nil {
|
|
return nil
|
|
}
|
|
var card model.IotCard
|
|
if err := c.db.WithContext(ctx).Where("id = ?", event.CardID).First(&card).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil
|
|
}
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "查询网络变化后的卡事实失败")
|
|
}
|
|
return c.evaluator.EvaluateAndAct(ctx, &card)
|
|
}
|
|
|
|
// NewRealnameChangedConsumer 创建卡实名变化事件消费者。
|
|
func NewRealnameChangedConsumer(db *gorm.DB, activator RealnameActivator, binding DeviceBindingReader, evaluator StopResumeEvaluator) *RealnameChangedConsumer {
|
|
return &RealnameChangedConsumer{db: db, activator: activator, binding: binding, evaluator: evaluator}
|
|
}
|
|
|
|
// Consume 校验载荷,并以当前权威卡事实执行可重入副作用。
|
|
func (c *RealnameChangedConsumer) Consume(ctx context.Context, envelope outbox.DeliveryEnvelope) error {
|
|
if c == nil || c.db == nil {
|
|
return errors.New(errors.CodeInternalError, "卡实名事件消费者未配置")
|
|
}
|
|
if envelope.EventType != constants.OutboxEventTypeCardRealnameChanged || envelope.PayloadVersion != constants.CardRealnameChangedPayloadVersionV1 {
|
|
return errors.New(errors.CodeInvalidParam, "卡实名事件类型或版本不受支持")
|
|
}
|
|
var event cardapp.RealnameChangedEvent
|
|
if err := sonic.Unmarshal(envelope.Payload, &event); err != nil {
|
|
return errors.Wrap(errors.CodeInvalidParam, err, "卡实名事件载荷无法解析")
|
|
}
|
|
if event.EventID != envelope.EventID || event.CardID == 0 || event.BeforeStatus == event.AfterStatus {
|
|
return errors.New(errors.CodeInvalidParam, "卡实名事件载荷不完整")
|
|
}
|
|
if event.FirstVerified && c.activator != nil {
|
|
if err := c.activator.ActivateByRealname(ctx, constants.AssetTypeIotCard, event.CardID); err != nil {
|
|
return err
|
|
}
|
|
if c.binding != nil {
|
|
binding, err := c.binding.GetActiveBindingByCardID(ctx, event.CardID)
|
|
if err == nil && binding != nil {
|
|
if err := c.activator.ActivateByRealname(ctx, constants.AssetTypeDevice, binding.DeviceID); err != nil {
|
|
return err
|
|
}
|
|
} else if err != nil && err != gorm.ErrRecordNotFound {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "查询实名卡绑定设备失败")
|
|
}
|
|
}
|
|
}
|
|
if c.evaluator == nil {
|
|
return nil
|
|
}
|
|
var card model.IotCard
|
|
if err := c.db.WithContext(ctx).Where("id = ?", event.CardID).First(&card).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil
|
|
}
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "查询实名变化后的卡事实失败")
|
|
}
|
|
return c.evaluator.EvaluateAndAct(ctx, &card)
|
|
}
|
|
|
|
// TrafficIncrementedConsumer 消费卡流量正增量并推进幂等副作用进度。
|
|
type TrafficIncrementedConsumer struct {
|
|
db *gorm.DB
|
|
redis *redis.Client
|
|
deductor TrafficDeductor
|
|
evaluator StopResumeEvaluator
|
|
}
|
|
|
|
// NewTrafficIncrementedConsumer 创建卡流量正增量事件消费者。
|
|
func NewTrafficIncrementedConsumer(db *gorm.DB, redisClient *redis.Client, deductor TrafficDeductor, evaluator StopResumeEvaluator) *TrafficIncrementedConsumer {
|
|
return &TrafficIncrementedConsumer{db: db, redis: redisClient, deductor: deductor, evaluator: evaluator}
|
|
}
|
|
|
|
// Consume 按事件处理进度避免至少一次投递重复扣减套餐流量。
|
|
func (c *TrafficIncrementedConsumer) Consume(ctx context.Context, envelope outbox.DeliveryEnvelope) error {
|
|
if c == nil || c.db == nil || c.redis == nil || c.deductor == nil {
|
|
return errors.New(errors.CodeInternalError, "卡流量事件消费者未配置")
|
|
}
|
|
if envelope.EventType != constants.OutboxEventTypeCardTrafficIncremented || envelope.PayloadVersion != constants.CardTrafficIncrementedPayloadVersionV1 {
|
|
return errors.New(errors.CodeInvalidParam, "卡流量事件类型或版本不受支持")
|
|
}
|
|
var event cardapp.TrafficIncrementedEvent
|
|
if err := sonic.Unmarshal(envelope.Payload, &event); err != nil {
|
|
return errors.Wrap(errors.CodeInvalidParam, err, "卡流量事件载荷无法解析")
|
|
}
|
|
if event.EventID != envelope.EventID || event.CardID == 0 || event.IncrementMB <= 0 {
|
|
return errors.New(errors.CodeInvalidParam, "卡流量事件载荷不完整")
|
|
}
|
|
receipt, err := c.acquireTrafficEffect(ctx, event.EventID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if receipt.Status == constants.CardObservationEffectStatusCompleted {
|
|
return nil
|
|
}
|
|
if receipt.Status == constants.CardObservationEffectStatusProcessing {
|
|
dailyKey := constants.RedisCardDailyTrafficKey(event.CardID, event.ObservedAt.Format("2006-01-02"))
|
|
pipe := c.redis.TxPipeline()
|
|
pipe.IncrByFloat(ctx, dailyKey, event.IncrementMB)
|
|
pipe.Expire(ctx, dailyKey, 48*time.Hour)
|
|
if _, err := pipe.Exec(ctx); err != nil {
|
|
return errors.Wrap(errors.CodeInternalError, err, "记录卡日流量缓冲失败,处理结果未知")
|
|
}
|
|
if err := c.updateTrafficEffectStatus(ctx, receipt.ID, constants.CardObservationEffectStatusProcessing, constants.CardObservationEffectStatusDailySaved); err != nil {
|
|
return err
|
|
}
|
|
receipt.Status = constants.CardObservationEffectStatusDailySaved
|
|
}
|
|
if receipt.Status == constants.CardObservationEffectStatusDailySaved {
|
|
if err := c.updateTrafficEffectStatus(ctx, receipt.ID, constants.CardObservationEffectStatusDailySaved, constants.CardObservationEffectStatusProcessing); err != nil {
|
|
return err
|
|
}
|
|
if err := c.deductor.DeductDataUsage(ctx, constants.AssetTypeIotCard, event.CardID, event.IncrementMB); err != nil {
|
|
return err
|
|
}
|
|
if err := c.updateTrafficEffectStatus(ctx, receipt.ID, constants.CardObservationEffectStatusProcessing, constants.CardObservationEffectStatusDeducted); err != nil {
|
|
return err
|
|
}
|
|
receipt.Status = constants.CardObservationEffectStatusDeducted
|
|
}
|
|
if c.evaluator != nil {
|
|
var card model.IotCard
|
|
if err := c.db.WithContext(ctx).Where("id = ?", event.CardID).First(&card).Error; err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "查询流量变化后的卡事实失败")
|
|
}
|
|
if err := c.evaluator.EvaluateAndAct(ctx, &card); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := c.updateTrafficEffectStatus(ctx, receipt.ID, constants.CardObservationEffectStatusDeducted, constants.CardObservationEffectStatusCompleted); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *TrafficIncrementedConsumer) acquireTrafficEffect(ctx context.Context, eventID string) (*model.CardObservationEffect, error) {
|
|
var receipt model.CardObservationEffect
|
|
err := c.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("event_id = ?", eventID).First(&receipt).Error
|
|
if err == gorm.ErrRecordNotFound {
|
|
receipt = model.CardObservationEffect{
|
|
EventID: eventID, EffectType: constants.CardObservationEffectTypeTrafficIncrement,
|
|
Status: constants.CardObservationEffectStatusProcessing,
|
|
}
|
|
return tx.Create(&receipt).Error
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch receipt.Status {
|
|
case constants.CardObservationEffectStatusCompleted, constants.CardObservationEffectStatusDailySaved, constants.CardObservationEffectStatusDeducted:
|
|
return nil
|
|
case constants.CardObservationEffectStatusProcessing:
|
|
return errors.New(errors.CodeConflict, "卡流量副作用仍在处理或结果未知")
|
|
default:
|
|
return tx.Model(&receipt).Where("status = ?", constants.CardObservationEffectStatusPending).
|
|
Update("status", constants.CardObservationEffectStatusProcessing).Error
|
|
}
|
|
})
|
|
if err != nil {
|
|
return nil, errors.Wrap(errors.CodeDatabaseError, err, "获取卡流量副作用处理权失败")
|
|
}
|
|
return &receipt, nil
|
|
}
|
|
|
|
func (c *TrafficIncrementedConsumer) updateTrafficEffectStatus(ctx context.Context, id uint, beforeStatus, afterStatus int) error {
|
|
result := c.db.WithContext(ctx).Model(&model.CardObservationEffect{}).
|
|
Where("id = ? AND status = ?", id, beforeStatus).
|
|
Update("status", afterStatus)
|
|
if result.Error != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新卡流量副作用进度失败")
|
|
}
|
|
if result.RowsAffected != 1 {
|
|
return errors.New(errors.CodeConflict, "卡流量副作用进度已被其他消费者更新")
|
|
}
|
|
return nil
|
|
}
|