Files
junhong_cmp_fiber/internal/infrastructure/cardobservation/event.go
2026-07-24 16:07:18 +08:00

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
}