@stackonward/cli 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +255 -0
  2. package/dist/index.js +14335 -603
  3. package/dist/index.js.map +1 -1
  4. package/dist/resources/deploy/ci/github/caller.yml +41 -0
  5. package/dist/resources/deploy/ci/onedev/import.yml +40 -0
  6. package/dist/resources/deploy/nginx/cache.conf +17 -0
  7. package/dist/resources/deploy/nginx/compression.conf +23 -0
  8. package/dist/resources/deploy/nginx/proxy.conf +15 -0
  9. package/dist/resources/deploy/profiles/go-service/compose.yaml +19 -0
  10. package/dist/resources/deploy/profiles/nuxt-node/Dockerfile +17 -0
  11. package/dist/resources/deploy/profiles/static-files/nginx/site.conf.template +55 -0
  12. package/dist/resources/deploy/profiles/static-nginx/Dockerfile +18 -0
  13. package/dist/resources/deploy/profiles/static-nginx/nginx/default.conf.template +28 -0
  14. package/dist/resources/deploy/targets/compose-host/deploy.config.example.json +114 -0
  15. package/dist/resources/deploy/traffic/nginx/stackonward-nginx-traffic +96 -0
  16. package/dist/resources/registry.json +30 -30
  17. package/dist/resources/templates/base/.claude/docs/architecture.md +4 -0
  18. package/dist/resources/templates/base/AGENTS.md +15 -0
  19. package/dist/resources/templates/base/CLAUDE.md +15 -0
  20. package/dist/resources/templates/go-backend/.dockerignore +25 -3
  21. package/dist/resources/templates/go-backend/deploy.config.json +26 -0
  22. package/dist/resources/templates/go-backend/docs/runtime-provenance.json +9 -6
  23. package/dist/resources/templates/go-backend/docs/source-provenance.json +33 -22
  24. package/dist/resources/templates/go-backend/internal/common/enums/register_locale.go +7 -0
  25. package/dist/resources/templates/go-backend/internal/common/enums/register_locale_test.go +30 -0
  26. package/dist/resources/templates/go-backend/internal/common/locale/locale.go +102 -76
  27. package/dist/resources/templates/go-backend/internal/common/locale/locale_test.go +168 -45
  28. package/dist/resources/templates/go-backend/internal/core/commandreplay/model.go +5 -5
  29. package/dist/resources/templates/go-backend/internal/core/commandreplay/persistence.go +20 -7
  30. package/dist/resources/templates/go-backend/internal/core/commandreplay/persistence_test.go +51 -22
  31. package/dist/resources/templates/go-backend/internal/core/commandreplay/repository.go +2 -2
  32. package/dist/resources/templates/go-backend/internal/core/commandreplay/service.go +13 -24
  33. package/dist/resources/templates/go-backend/internal/core/commandreplay/service_persistence_test.go +98 -13
  34. package/dist/resources/templates/go-backend/internal/core/commandreplay/service_test.go +61 -36
  35. package/dist/resources/templates/go-backend/internal/core/commandreplay/types.go +6 -6
  36. package/dist/resources/templates/go-backend/internal/core/integrationevent/config.go +24 -0
  37. package/dist/resources/templates/go-backend/internal/core/integrationevent/errors.go +53 -0
  38. package/dist/resources/templates/go-backend/internal/core/integrationevent/model.go +71 -0
  39. package/dist/resources/templates/go-backend/internal/core/integrationevent/persistence.go +193 -0
  40. package/dist/resources/templates/go-backend/internal/core/integrationevent/persistence_postgres_test.go +178 -0
  41. package/dist/resources/templates/go-backend/internal/core/integrationevent/persistence_test.go +168 -0
  42. package/dist/resources/templates/go-backend/internal/core/integrationevent/provider.go +16 -0
  43. package/dist/resources/templates/go-backend/internal/core/integrationevent/registry.go +69 -0
  44. package/dist/resources/templates/go-backend/internal/core/integrationevent/repository.go +17 -0
  45. package/dist/resources/templates/go-backend/internal/core/integrationevent/service.go +283 -0
  46. package/dist/resources/templates/go-backend/internal/core/integrationevent/service_test.go +433 -0
  47. package/dist/resources/templates/go-backend/internal/core/integrationevent/types.go +67 -0
  48. package/dist/resources/templates/go-backend/migrations/000001_infrastructure.down.sql +6 -0
  49. package/dist/resources/templates/go-backend/migrations/000001_infrastructure.up.sql +176 -16
  50. package/dist/resources/templates/go-backend/tests/infrastructure.sql +109 -6
  51. package/dist/resources/templates/nuxt/.claude/docs/stack.md +5 -1
  52. package/dist/resources/templates/nuxt/.dockerignore +19 -0
  53. package/dist/resources/templates/nuxt/deploy.config.json +27 -0
  54. package/dist/resources/templates/nuxt/package.json +8 -8
  55. package/package.json +5 -3
@@ -0,0 +1,178 @@
1
+ package integrationevent
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "os"
7
+ "strings"
8
+ "testing"
9
+ "time"
10
+
11
+ "github.com/jackc/pgx/v5"
12
+ "github.com/jackc/pgx/v5/stdlib"
13
+ "gorm.io/driver/postgres"
14
+ "gorm.io/gorm"
15
+ "gorm.io/gorm/logger"
16
+
17
+ "{{module}}/internal/core/database"
18
+ )
19
+
20
+ func TestReceiverServiceSerializesAggregateVersionsOnPostgres(t *testing.T) {
21
+ db := openIntegrationEventPostgres(t)
22
+ firstStarted := make(chan struct{})
23
+ releaseFirst := make(chan struct{})
24
+ secondHandled := make(chan struct{})
25
+ registry := NewHandlerRegistry()
26
+ if err := registry.Register(HandlerRegistration{
27
+ SourceSystem: "source-a", EventType: "resource.changed", SchemaVersion: "1.0.0",
28
+ Handler: HandlerFunc(func(_ context.Context, envelope Envelope) error {
29
+ switch envelope.AggregateVersion {
30
+ case 1:
31
+ close(firstStarted)
32
+ <-releaseFirst
33
+ case 2:
34
+ close(secondHandled)
35
+ }
36
+ return nil
37
+ }),
38
+ }); err != nil {
39
+ t.Fatalf("Register() error = %v", err)
40
+ }
41
+ service, err := NewReceiverService(
42
+ NewRepository(db), database.NewTransactionManager(db), registry,
43
+ ReceiverConfig{SubscriberCode: "subscriber-a"},
44
+ )
45
+ if err != nil {
46
+ t.Fatalf("NewReceiverService() error = %v", err)
47
+ }
48
+
49
+ firstDone := make(chan error, 1)
50
+ go func() {
51
+ _, receiveErr := service.Receive(context.Background(), fakeEnvelope("event-1", 1, []byte(`{"value":1}`)))
52
+ firstDone <- receiveErr
53
+ }()
54
+ select {
55
+ case <-firstStarted:
56
+ case <-time.After(5 * time.Second):
57
+ close(releaseFirst)
58
+ t.Fatal("version 1 handler did not start")
59
+ }
60
+
61
+ secondDone := make(chan error, 1)
62
+ go func() {
63
+ _, receiveErr := service.Receive(context.Background(), fakeEnvelope("event-2", 2, []byte(`{"value":2}`)))
64
+ secondDone <- receiveErr
65
+ }()
66
+ select {
67
+ case <-secondHandled:
68
+ close(releaseFirst)
69
+ t.Fatal("version 2 handler ran before version 1 committed")
70
+ case <-time.After(100 * time.Millisecond):
71
+ }
72
+ close(releaseFirst)
73
+
74
+ for name, done := range map[string]<-chan error{"version 1": firstDone, "version 2": secondDone} {
75
+ select {
76
+ case receiveErr := <-done:
77
+ if receiveErr != nil {
78
+ t.Fatalf("%s Receive() error = %v", name, receiveErr)
79
+ }
80
+ case <-time.After(5 * time.Second):
81
+ t.Fatalf("%s Receive() did not complete", name)
82
+ }
83
+ }
84
+
85
+ var checkpoint AggregateCheckpoint
86
+ if err := db.Take(&checkpoint).Error; err != nil {
87
+ t.Fatalf("load checkpoint: %v", err)
88
+ }
89
+ if checkpoint.LastAggregateVersion != 2 || checkpoint.RevisionNo != 2 {
90
+ t.Fatalf("checkpoint = %#v, want version/revision 2", checkpoint)
91
+ }
92
+ }
93
+
94
+ func openIntegrationEventPostgres(t *testing.T) *gorm.DB {
95
+ t.Helper()
96
+ baseDSN := strings.TrimSpace(os.Getenv("ONEX_TEST_POSTGRES_DSN"))
97
+ if baseDSN == "" {
98
+ t.Skip("ONEX_TEST_POSTGRES_DSN is not set")
99
+ }
100
+ adminConfig, err := pgx.ParseConfig(baseDSN)
101
+ if err != nil {
102
+ t.Fatalf("parse PostgreSQL test DSN: %v", err)
103
+ }
104
+ adminDB := stdlib.OpenDB(*adminConfig)
105
+ if err := adminDB.PingContext(context.Background()); err != nil {
106
+ _ = adminDB.Close()
107
+ t.Fatalf("ping PostgreSQL admin database: %v", err)
108
+ }
109
+ databaseName := fmt.Sprintf("integration_event_%d", time.Now().UnixNano())
110
+ if _, err := adminDB.ExecContext(context.Background(), `CREATE DATABASE "`+databaseName+`"`); err != nil {
111
+ _ = adminDB.Close()
112
+ t.Fatalf("create PostgreSQL test database: %v", err)
113
+ }
114
+
115
+ testConfig := adminConfig.Copy()
116
+ testConfig.Database = databaseName
117
+ testDB := stdlib.OpenDB(*testConfig)
118
+ if err := testDB.PingContext(context.Background()); err != nil {
119
+ _ = testDB.Close()
120
+ _ = adminDB.Close()
121
+ t.Fatalf("ping PostgreSQL test database: %v", err)
122
+ }
123
+ db, err := gorm.Open(postgres.New(postgres.Config{Conn: testDB}), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
124
+ if err != nil {
125
+ _ = testDB.Close()
126
+ _ = adminDB.Close()
127
+ t.Fatalf("open PostgreSQL test database: %v", err)
128
+ }
129
+ t.Cleanup(func() {
130
+ if err := testDB.Close(); err != nil {
131
+ t.Errorf("close PostgreSQL test database: %v", err)
132
+ }
133
+ if _, err := adminDB.ExecContext(
134
+ context.Background(),
135
+ "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()",
136
+ databaseName,
137
+ ); err != nil {
138
+ t.Errorf("terminate PostgreSQL test sessions: %v", err)
139
+ }
140
+ if _, err := adminDB.ExecContext(context.Background(), `DROP DATABASE IF EXISTS "`+databaseName+`"`); err != nil {
141
+ t.Errorf("drop PostgreSQL test database: %v", err)
142
+ }
143
+ if err := adminDB.Close(); err != nil {
144
+ t.Errorf("close PostgreSQL admin database: %v", err)
145
+ }
146
+ })
147
+
148
+ statements := []string{
149
+ `CREATE TABLE sys_integration_event_inbox (
150
+ id BIGSERIAL PRIMARY KEY, source_system VARCHAR(191) NOT NULL, event_id VARCHAR(191) NOT NULL,
151
+ event_type VARCHAR(191) NOT NULL, schema_version VARCHAR(32) NOT NULL, product_code VARCHAR(50) NOT NULL,
152
+ aggregate_type VARCHAR(128) NOT NULL, aggregate_ref VARCHAR(191) NOT NULL, aggregate_version BIGINT NOT NULL,
153
+ payload_hash CHAR(64) NOT NULL, event_payload JSONB NOT NULL, received_at TIMESTAMPTZ(3) NOT NULL,
154
+ created_at TIMESTAMPTZ(3) NOT NULL, UNIQUE(source_system, event_id)
155
+ )`,
156
+ `CREATE TABLE sys_integration_event_receipts (
157
+ id BIGSERIAL PRIMARY KEY, source_system VARCHAR(191) NOT NULL, subscriber_code VARCHAR(64) NOT NULL,
158
+ event_id VARCHAR(191) NOT NULL, status VARCHAR(20) NOT NULL, receipt_status VARCHAR(20), error_code VARCHAR(120),
159
+ processing_started_at TIMESTAMPTZ(3) NOT NULL, processed_at TIMESTAMPTZ(3),
160
+ created_at TIMESTAMPTZ(3) NOT NULL, updated_at TIMESTAMPTZ(3) NOT NULL,
161
+ UNIQUE(source_system, subscriber_code, event_id)
162
+ )`,
163
+ `CREATE TABLE sys_integration_aggregate_checkpoints (
164
+ id BIGSERIAL PRIMARY KEY, source_system VARCHAR(191) NOT NULL, subscriber_code VARCHAR(64) NOT NULL,
165
+ aggregate_type VARCHAR(128) NOT NULL, aggregate_ref VARCHAR(191) NOT NULL,
166
+ last_aggregate_version BIGINT NOT NULL, last_event_id VARCHAR(191) NOT NULL,
167
+ last_payload_hash CHAR(64) NOT NULL, revision_no BIGINT NOT NULL,
168
+ created_at TIMESTAMPTZ(3) NOT NULL, updated_at TIMESTAMPTZ(3) NOT NULL,
169
+ UNIQUE(source_system, subscriber_code, aggregate_type, aggregate_ref)
170
+ )`,
171
+ }
172
+ for _, statement := range statements {
173
+ if err := db.Exec(statement).Error; err != nil {
174
+ t.Fatalf("create PostgreSQL integration event schema: %v", err)
175
+ }
176
+ }
177
+ return db
178
+ }
@@ -0,0 +1,168 @@
1
+ package integrationevent
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+ "strings"
8
+ "testing"
9
+
10
+ "gorm.io/driver/sqlite"
11
+ "gorm.io/gorm"
12
+ "gorm.io/gorm/logger"
13
+
14
+ "{{module}}/internal/core/database"
15
+ )
16
+
17
+ func TestRepositoryRequiresTransactionForInboxMutation(t *testing.T) {
18
+ db := openSQLiteIntegrationEventDB(t)
19
+ repo := NewRepository(db)
20
+ _, _, err := repo.CreateOrLoadInbox(context.Background(), &InboxRecord{})
21
+ if err == nil || !strings.Contains(err.Error(), "transaction context is required") {
22
+ t.Fatalf("CreateOrLoadInbox() error = %v, want transaction requirement", err)
23
+ }
24
+ }
25
+
26
+ func TestReceiverServiceSQLiteCommitsHandlerAndCoreStateTogether(t *testing.T) {
27
+ db := openSQLiteIntegrationEventDB(t)
28
+ service := newSQLiteReceiverService(t, db, func(ctx context.Context, envelope Envelope) error {
29
+ return database.GetDB(ctx, db).Exec(
30
+ "INSERT INTO integration_event_test_projections (event_id, aggregate_version) VALUES (?, ?)",
31
+ envelope.EventID,
32
+ envelope.AggregateVersion,
33
+ ).Error
34
+ })
35
+
36
+ result, err := service.Receive(context.Background(), fakeEnvelope("event-1", 1, []byte(`{"value":1}`)))
37
+ if err != nil {
38
+ t.Fatalf("Receive() error = %v", err)
39
+ }
40
+ if result.Outcome != ReceiptOutcomeProcessed {
41
+ t.Fatalf("Receive() outcome = %q, want processed", result.Outcome)
42
+ }
43
+ assertSQLiteCount(t, db, "sys_integration_event_inbox", 1)
44
+ assertSQLiteCount(t, db, "sys_integration_event_receipts", 1)
45
+ assertSQLiteCount(t, db, "sys_integration_aggregate_checkpoints", 1)
46
+ assertSQLiteCount(t, db, "integration_event_test_projections", 1)
47
+
48
+ duplicate, err := service.Receive(context.Background(), fakeEnvelope("event-1", 1, []byte(`{"value":1}`)))
49
+ if err != nil {
50
+ t.Fatalf("Receive(duplicate) error = %v", err)
51
+ }
52
+ if duplicate.Outcome != ReceiptOutcomeDuplicate {
53
+ t.Fatalf("Receive(duplicate) outcome = %q, want duplicate", duplicate.Outcome)
54
+ }
55
+ assertSQLiteCount(t, db, "integration_event_test_projections", 1)
56
+ }
57
+
58
+ func TestReceiverServiceSQLiteRollsBackHandlerFailure(t *testing.T) {
59
+ db := openSQLiteIntegrationEventDB(t)
60
+ handlerErr := errors.New("projection failed")
61
+ service := newSQLiteReceiverService(t, db, func(ctx context.Context, envelope Envelope) error {
62
+ if err := database.GetDB(ctx, db).Exec(
63
+ "INSERT INTO integration_event_test_projections (event_id, aggregate_version) VALUES (?, ?)",
64
+ envelope.EventID,
65
+ envelope.AggregateVersion,
66
+ ).Error; err != nil {
67
+ return err
68
+ }
69
+ return handlerErr
70
+ })
71
+
72
+ _, err := service.Receive(context.Background(), fakeEnvelope("event-1", 1, []byte(`{"value":1}`)))
73
+ if !errors.Is(err, handlerErr) {
74
+ t.Fatalf("Receive() error = %v, want handler error", err)
75
+ }
76
+ assertSQLiteCount(t, db, "sys_integration_event_inbox", 0)
77
+ assertSQLiteCount(t, db, "sys_integration_event_receipts", 0)
78
+ assertSQLiteCount(t, db, "sys_integration_aggregate_checkpoints", 0)
79
+ assertSQLiteCount(t, db, "integration_event_test_projections", 0)
80
+ }
81
+
82
+ func TestReceiverServiceSQLiteRollsBackVersionGap(t *testing.T) {
83
+ db := openSQLiteIntegrationEventDB(t)
84
+ service := newSQLiteReceiverService(t, db, func(context.Context, Envelope) error {
85
+ t.Fatal("handler must not run for a version gap")
86
+ return nil
87
+ })
88
+
89
+ _, err := service.Receive(context.Background(), fakeEnvelope("event-2", 2, []byte(`{"value":2}`)))
90
+ var gap *VersionGapError
91
+ if !errors.As(err, &gap) {
92
+ t.Fatalf("Receive() error = %v, want VersionGapError", err)
93
+ }
94
+ assertSQLiteCount(t, db, "sys_integration_event_inbox", 0)
95
+ assertSQLiteCount(t, db, "sys_integration_event_receipts", 0)
96
+ assertSQLiteCount(t, db, "sys_integration_aggregate_checkpoints", 0)
97
+ }
98
+
99
+ func newSQLiteReceiverService(t *testing.T, db *gorm.DB, handler HandlerFunc) *ReceiverService {
100
+ t.Helper()
101
+ registry := NewHandlerRegistry()
102
+ if err := registry.Register(HandlerRegistration{
103
+ SourceSystem: "source-a", EventType: "resource.changed", SchemaVersion: "1.0.0", Handler: handler,
104
+ }); err != nil {
105
+ t.Fatalf("Register() error = %v", err)
106
+ }
107
+ service, err := NewReceiverService(
108
+ NewRepository(db),
109
+ database.NewTransactionManager(db),
110
+ registry,
111
+ ReceiverConfig{SubscriberCode: "subscriber-a"},
112
+ )
113
+ if err != nil {
114
+ t.Fatalf("NewReceiverService() error = %v", err)
115
+ }
116
+ return service
117
+ }
118
+
119
+ func openSQLiteIntegrationEventDB(t *testing.T) *gorm.DB {
120
+ t.Helper()
121
+ name := strings.NewReplacer("/", "_", " ", "_").Replace(t.Name())
122
+ db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared&_loc=auto", name)), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
123
+ if err != nil {
124
+ t.Fatalf("open SQLite: %v", err)
125
+ }
126
+ statements := []string{
127
+ `CREATE TABLE sys_integration_event_inbox (
128
+ id INTEGER PRIMARY KEY AUTOINCREMENT, source_system TEXT NOT NULL, event_id TEXT NOT NULL,
129
+ event_type TEXT NOT NULL, schema_version TEXT NOT NULL, product_code TEXT NOT NULL,
130
+ aggregate_type TEXT NOT NULL, aggregate_ref TEXT NOT NULL, aggregate_version INTEGER NOT NULL,
131
+ payload_hash TEXT NOT NULL, event_payload TEXT NOT NULL, received_at DATETIME NOT NULL,
132
+ created_at DATETIME NOT NULL, UNIQUE(source_system, event_id)
133
+ )`,
134
+ `CREATE TABLE sys_integration_event_receipts (
135
+ id INTEGER PRIMARY KEY AUTOINCREMENT, source_system TEXT NOT NULL, subscriber_code TEXT NOT NULL,
136
+ event_id TEXT NOT NULL, status TEXT NOT NULL, receipt_status TEXT, error_code TEXT,
137
+ processing_started_at DATETIME NOT NULL, processed_at DATETIME, created_at DATETIME NOT NULL,
138
+ updated_at DATETIME NOT NULL, UNIQUE(source_system, subscriber_code, event_id)
139
+ )`,
140
+ `CREATE TABLE sys_integration_aggregate_checkpoints (
141
+ id INTEGER PRIMARY KEY AUTOINCREMENT, source_system TEXT NOT NULL, subscriber_code TEXT NOT NULL,
142
+ aggregate_type TEXT NOT NULL, aggregate_ref TEXT NOT NULL, last_aggregate_version INTEGER NOT NULL,
143
+ last_event_id TEXT NOT NULL, last_payload_hash TEXT NOT NULL, revision_no INTEGER NOT NULL,
144
+ created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL,
145
+ UNIQUE(source_system, subscriber_code, aggregate_type, aggregate_ref)
146
+ )`,
147
+ `CREATE TABLE integration_event_test_projections (
148
+ event_id TEXT PRIMARY KEY, aggregate_version INTEGER NOT NULL
149
+ )`,
150
+ }
151
+ for _, statement := range statements {
152
+ if err := db.Exec(statement).Error; err != nil {
153
+ t.Fatalf("create SQLite integration event schema: %v", err)
154
+ }
155
+ }
156
+ return db
157
+ }
158
+
159
+ func assertSQLiteCount(t *testing.T, db *gorm.DB, table string, want int64) {
160
+ t.Helper()
161
+ var count int64
162
+ if err := db.Table(table).Count(&count).Error; err != nil {
163
+ t.Fatalf("count %s: %v", table, err)
164
+ }
165
+ if count != want {
166
+ t.Fatalf("%s count = %d, want %d", table, count, want)
167
+ }
168
+ }
@@ -0,0 +1,16 @@
1
+ package integrationevent
2
+
3
+ import (
4
+ "github.com/google/wire"
5
+
6
+ "{{module}}/internal/core/database"
7
+ )
8
+
9
+ // ProviderSet 提供入站事件仓储、处理器注册表与接收服务。
10
+ // ReceiverConfig 由具体消费者提供,处理器由消费者在启动阶段精确注册。
11
+ var ProviderSet = wire.NewSet(
12
+ NewRepository,
13
+ NewHandlerRegistry,
14
+ NewReceiverService,
15
+ wire.Bind(new(TransactionManager), new(*database.TransactionManager)),
16
+ )
@@ -0,0 +1,69 @@
1
+ package integrationevent
2
+
3
+ import (
4
+ "fmt"
5
+ "strings"
6
+ "sync"
7
+ )
8
+
9
+ type handlerKey struct {
10
+ sourceSystem string
11
+ eventType string
12
+ schemaVersion string
13
+ }
14
+
15
+ // HandlerRegistry 保存消费者启动时注册的精确事件版本处理器。
16
+ type HandlerRegistry struct {
17
+ mu sync.RWMutex
18
+ handlers map[handlerKey]Handler
19
+ }
20
+
21
+ // NewHandlerRegistry 创建空的处理器注册表。
22
+ func NewHandlerRegistry() *HandlerRegistry {
23
+ return &HandlerRegistry{handlers: make(map[handlerKey]Handler)}
24
+ }
25
+
26
+ // Register 注册精确 source/type/schema 处理器,重复注册会失败。
27
+ func (r *HandlerRegistry) Register(registration HandlerRegistration) error {
28
+ key, err := normalizeHandlerKey(registration.SourceSystem, registration.EventType, registration.SchemaVersion)
29
+ if err != nil {
30
+ return err
31
+ }
32
+ if registration.Handler == nil {
33
+ return fmt.Errorf("integration event handler is required")
34
+ }
35
+
36
+ r.mu.Lock()
37
+ defer r.mu.Unlock()
38
+ if _, exists := r.handlers[key]; exists {
39
+ return fmt.Errorf("integration event handler is already registered for %s/%s/%s", key.sourceSystem, key.eventType, key.schemaVersion)
40
+ }
41
+ r.handlers[key] = registration.Handler
42
+ return nil
43
+ }
44
+
45
+ func (r *HandlerRegistry) resolve(sourceSystem, eventType, schemaVersion string) (Handler, bool) {
46
+ if r == nil {
47
+ return nil, false
48
+ }
49
+ key, err := normalizeHandlerKey(sourceSystem, eventType, schemaVersion)
50
+ if err != nil {
51
+ return nil, false
52
+ }
53
+ r.mu.RLock()
54
+ defer r.mu.RUnlock()
55
+ handler, exists := r.handlers[key]
56
+ return handler, exists
57
+ }
58
+
59
+ func normalizeHandlerKey(sourceSystem, eventType, schemaVersion string) (handlerKey, error) {
60
+ key := handlerKey{
61
+ sourceSystem: strings.TrimSpace(sourceSystem),
62
+ eventType: strings.TrimSpace(eventType),
63
+ schemaVersion: strings.TrimSpace(schemaVersion),
64
+ }
65
+ if key.sourceSystem == "" || key.eventType == "" || key.schemaVersion == "" {
66
+ return handlerKey{}, fmt.Errorf("integration event handler key is incomplete")
67
+ }
68
+ return key, nil
69
+ }
@@ -0,0 +1,17 @@
1
+ package integrationevent
2
+
3
+ import (
4
+ "context"
5
+ "time"
6
+ )
7
+
8
+ // Repository 定义入站事件身份、订阅者回执与聚合水位的事务仓储。
9
+ type Repository interface {
10
+ CreateOrLoadInbox(ctx context.Context, record *InboxRecord) (*InboxRecord, bool, error)
11
+ CreateOrLockReceipt(ctx context.Context, record *ReceiptRecord) (*ReceiptRecord, bool, error)
12
+ LockAggregateScope(ctx context.Context, sourceSystem, subscriberCode, aggregateType, aggregateRef string) error
13
+ FindCheckpointForUpdate(ctx context.Context, sourceSystem, subscriberCode, aggregateType, aggregateRef string) (*AggregateCheckpoint, error)
14
+ CreateCheckpoint(ctx context.Context, checkpoint *AggregateCheckpoint) error
15
+ AdvanceCheckpoint(ctx context.Context, checkpoint *AggregateCheckpoint, expectedRevision int64) error
16
+ MarkReceiptProcessed(ctx context.Context, id int64, outcome ReceiptOutcome, processedAt time.Time) error
17
+ }