Files
junhong_cmp_fiber/internal/infrastructure/messaging/outbox/repository.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

93 lines
3.3 KiB
Go

// Package outbox 实现公共 Outbox 的持久化与投递基础设施。
package outbox
import (
"context"
stderrors "errors"
"strings"
"time"
"github.com/bytedance/sonic"
"github.com/google/uuid"
"gorm.io/datatypes"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/asynctask"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// Envelope 是业务 UseCase 在事务内追加的公共事件信封。
type Envelope struct {
EventID string `json:"event_id"`
EventType string `json:"event_type"`
PayloadVersion int `json:"payload_version"`
AggregateType string `json:"aggregate_type"`
AggregateID string `json:"aggregate_id"`
ResourceType string `json:"resource_type"`
ResourceID string `json:"resource_id"`
BusinessKey string `json:"business_key,omitempty"`
RequestID string `json:"request_id,omitempty"`
CorrelationID string `json:"correlation_id,omitempty"`
Payload any `json:"payload"`
}
// Repository 通过调用方显式传入的 GORM 事务句柄追加事件。
type Repository struct{}
// NewRepository 创建公共 Outbox Repository。
func NewRepository() *Repository {
return &Repository{}
}
// Append 在业务事务中持久化稳定事件身份和必要快照。
func (r *Repository) Append(ctx context.Context, tx *gorm.DB, envelope Envelope) (*model.OutboxEvent, error) {
return r.append(ctx, tx, envelope, false)
}
// AppendIdempotent 在稳定 EventID 重投时保持原事件,不重复创建事实。
func (r *Repository) AppendIdempotent(ctx context.Context, tx *gorm.DB, envelope Envelope) (*model.OutboxEvent, error) {
return r.append(ctx, tx, envelope, true)
}
func (r *Repository) append(ctx context.Context, tx *gorm.DB, envelope Envelope, idempotent bool) (*model.OutboxEvent, error) {
if tx == nil {
return nil, stderrors.New("Outbox 追加必须传入 GORM 事务句柄")
}
if strings.TrimSpace(envelope.EventType) == "" || strings.TrimSpace(envelope.AggregateType) == "" ||
strings.TrimSpace(envelope.AggregateID) == "" || strings.TrimSpace(envelope.ResourceType) == "" ||
strings.TrimSpace(envelope.ResourceID) == "" {
return nil, stderrors.New("Outbox 事件类型和资源定位不能为空")
}
if err := asynctask.ValidatePayload(envelope.Payload); err != nil {
return nil, err
}
payload, err := sonic.Marshal(envelope.Payload)
if err != nil {
return nil, err
}
if envelope.EventID == "" {
envelope.EventID = uuid.NewString()
}
if envelope.PayloadVersion <= 0 {
envelope.PayloadVersion = 1
}
now := time.Now().UTC()
event := &model.OutboxEvent{
EventID: envelope.EventID, EventType: envelope.EventType, PayloadVersion: envelope.PayloadVersion,
AggregateType: envelope.AggregateType, AggregateID: envelope.AggregateID,
ResourceType: envelope.ResourceType, ResourceID: envelope.ResourceID, BusinessKey: envelope.BusinessKey,
RequestID: envelope.RequestID, CorrelationID: envelope.CorrelationID, Payload: datatypes.JSON(payload),
Status: constants.OutboxStatusPending, MaxRetries: constants.OutboxDefaultMaxRetries, NextAttemptAt: now,
}
create := tx.WithContext(ctx)
if idempotent {
create = create.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "event_id"}}, DoNothing: true})
}
if err := create.Create(event).Error; err != nil {
return nil, err
}
return event, nil
}