Files
junhong_cmp_fiber/internal/infrastructure/cardobservation/series_event.go
break 5c4d17e9fc
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 联调延期
2026-07-24 19:59:24 +08:00

120 lines
4.9 KiB
Go

package cardobservation
import (
"context"
"strconv"
"strings"
"github.com/bytedance/sonic"
"gorm.io/gorm"
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"
)
// SeriesEventWriter 将业务成功观测请求写入公共 Outbox。
type SeriesEventWriter struct {
outbox *outbox.Repository
}
// NewSeriesEventWriter 创建业务观测事件 Writer。
func NewSeriesEventWriter(repository *outbox.Repository) *SeriesEventWriter {
return &SeriesEventWriter{outbox: repository}
}
// AppendSeriesRequested 在原业务事务中追加稳定观测请求事件。
func (w *SeriesEventWriter) AppendSeriesRequested(ctx context.Context, tx *gorm.DB, event cardapp.SeriesRequestedEvent) error {
if w == nil || w.outbox == nil {
return errors.New(errors.CodeInternalError, "业务观测 Outbox Writer 未配置")
}
if cardapp.IsSeriesTriggerSuppressed(ctx) {
return nil
}
if strings.TrimSpace(event.EventID) == "" || event.ResourceID == 0 || len(event.SyncTypes) == 0 {
return errors.New(errors.CodeInvalidParam, "业务观测事件载荷不完整")
}
if tx == nil {
return errors.New(errors.CodeInternalError, "业务观测事件缺少原业务事务")
}
if event.ResourceType == constants.CardObservationResourceTypeDevice {
event.ResourceIDs = nil
if err := tx.WithContext(ctx).
Model(&model.DeviceSimBinding{}).
Where("device_id = ? AND bind_status = ?", event.ResourceID, constants.BindStatusBound).
Order("slot_position ASC").
Pluck("iot_card_id", &event.ResourceIDs).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "冻结业务观测设备绑定卡快照失败")
}
}
_, err := w.outbox.AppendIdempotent(ctx, tx, outbox.Envelope{
EventID: event.EventID, EventType: constants.OutboxEventTypeCardSeriesRequested,
PayloadVersion: constants.CardSeriesRequestedPayloadVersionV1,
AggregateType: event.ResourceType, AggregateID: strconv.FormatUint(uint64(event.ResourceID), 10),
ResourceType: event.ResourceType, ResourceID: strconv.FormatUint(uint64(event.ResourceID), 10),
BusinessKey: event.EventID, RequestID: event.RequestID, CorrelationID: event.CorrelationID,
Payload: event,
})
if err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "写入业务观测 Outbox 事件失败")
}
return nil
}
// SeriesRequestedConsumer 消费业务成功事实并触发卡观测序列。
type SeriesRequestedConsumer struct {
trigger *cardapp.SeriesTrigger
}
// NewSeriesRequestedConsumer 创建业务观测序列消费者。
func NewSeriesRequestedConsumer(trigger *cardapp.SeriesTrigger) *SeriesRequestedConsumer {
return &SeriesRequestedConsumer{trigger: trigger}
}
// Consume 将一个业务事件展开为卡观测序列;重复投递由稳定序列键幂等。
func (c *SeriesRequestedConsumer) Consume(ctx context.Context, envelope outbox.DeliveryEnvelope) error {
if c == nil || c.trigger == nil {
return errors.New(errors.CodeInternalError, "业务观测序列消费者未配置")
}
if envelope.EventType != constants.OutboxEventTypeCardSeriesRequested || envelope.PayloadVersion != constants.CardSeriesRequestedPayloadVersionV1 {
return errors.New(errors.CodeInvalidParam, "业务观测事件类型或版本不受支持")
}
var event cardapp.SeriesRequestedEvent
if err := sonic.Unmarshal(envelope.Payload, &event); err != nil {
return errors.Wrap(errors.CodeInvalidParam, err, "业务观测事件载荷无法解析")
}
if event.EventID != envelope.EventID || event.ResourceID == 0 || len(event.SyncTypes) == 0 {
return errors.New(errors.CodeInvalidParam, "业务观测事件载荷不完整")
}
if event.ResourceType != constants.CardObservationResourceTypeCard && event.ResourceType != constants.CardObservationResourceTypeDevice {
return errors.New(errors.CodeInvalidParam, "业务观测事件资源类型不受支持")
}
cardIDs := []uint{event.ResourceID}
if event.ResourceType == constants.CardObservationResourceTypeDevice {
cardIDs = event.ResourceIDs
}
if len(cardIDs) == 0 {
return nil
}
for _, cardID := range cardIDs {
for _, syncType := range event.SyncTypes {
request := cardapp.SeriesRequest{
Scene: event.Scene, ResourceType: constants.CardObservationResourceTypeCard,
ResourceID: strconv.FormatUint(uint64(cardID), 10), SyncType: syncType,
ExpectedValue: event.ExpectedValue, Source: event.Source,
RequestID: event.RequestID, CorrelationID: event.CorrelationID,
}
key := event.EventID + ":" + strconv.FormatUint(uint64(cardID), 10) + ":" + syncType
if _, _, err := c.trigger.TriggerEvent(ctx, key, request); err != nil {
return err
}
}
}
return nil
}
var _ cardapp.SeriesEventWriter = (*SeriesEventWriter)(nil)
var _ outbox.EventConsumer = (*SeriesRequestedConsumer)(nil)