实现七月迭代公共技术基础
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m20s

This commit is contained in:
2026-07-23 17:52:48 +09:00
parent f7c42252c0
commit 17782d5f8e
76 changed files with 5246 additions and 158 deletions

View File

@@ -0,0 +1,57 @@
// Command foundation-check 执行公共基础迁移前后发布门禁。
package main
import (
"context"
"flag"
"fmt"
"os"
"time"
"github.com/bytedance/sonic"
"go.uber.org/zap"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/releasegate"
"github.com/break/junhong_cmp_fiber/pkg/config"
"github.com/break/junhong_cmp_fiber/pkg/database"
)
func main() {
phase := flag.String("phase", releasegate.PhasePre, "检查阶段pre 或 post")
timeout := flag.Duration("timeout", time.Minute, "检查超时时间")
flag.Parse()
if err := run(*phase, *timeout); err != nil {
fmt.Fprintf(os.Stderr, "公共基础发布门禁失败:%v\n", err)
os.Exit(1)
}
}
func run(phase string, timeout time.Duration) error {
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("加载配置失败:%w", err)
}
db, err := database.InitPostgreSQL(&cfg.Database, zap.NewNop())
if err != nil {
return fmt.Errorf("连接 PostgreSQL 失败:%w", err)
}
if sqlDB, dbErr := db.DB(); dbErr == nil {
defer func() { _ = sqlDB.Close() }()
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
report, err := releasegate.NewChecker(db).Run(ctx, phase)
if err != nil {
return err
}
encoded, err := sonic.Marshal(report)
if err != nil {
return fmt.Errorf("序列化门禁报告失败:%w", err)
}
fmt.Println(string(encoded))
if !report.Passed() {
return fmt.Errorf("发现阻断项,请按稳定错误码处理后重试")
}
return nil
}

View File

@@ -16,6 +16,7 @@ import (
"github.com/break/junhong_cmp_fiber/internal/bootstrap"
"github.com/break/junhong_cmp_fiber/internal/gateway"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/polling"
iot_card_svc "github.com/break/junhong_cmp_fiber/internal/service/iot_card"
@@ -36,6 +37,7 @@ const (
workerModulePollingInitializer = "polling_initializer"
workerModulePollingScheduler = "polling_scheduler"
workerModuleAsynqScheduler = "asynq_scheduler"
workerModuleOutboxRelay = "outbox_relay"
importRescueLimit = 500 // 启动补偿单次扫描的最大导入任务数
)
@@ -55,6 +57,8 @@ type workerRuntime struct {
asynqClient *asynq.Client
workerResult *bootstrap.WorkerBootstrapResult
workerServer *queue.Server
outboxQueueClient *queue.Client
outboxConsumers *outbox.ConsumerRegistry
pollingConfigMgr *polling.PollingConfigManager
pollingQueueMgr *polling.PollingQueueManager
pollingIotCardStore *postgres.IotCardStore
@@ -124,6 +128,9 @@ func runWorker(cfg *config.Config) {
taskHandler := createTaskHandler(runtime, appLogger)
taskHandler.RegisterHandlers()
outboxHandler := outbox.NewHandler(runtime.outboxConsumers)
taskHandler.GetMux().HandleFunc(constants.TaskTypeOutboxDeliver, outboxHandler.Handle)
startOutboxRelay(ctx, runtime, cfg.Worker.InstanceName, appLogger)
rescuePendingImportTasks(ctx, runtime, appLogger)
appLogger.Info("Worker 服务器配置完成",
@@ -149,7 +156,7 @@ func runWorker(cfg *config.Config) {
// buildWorkerModuleStatus 根据角色生成启用/禁用模块列表,供启动日志和人工验收使用。
func buildWorkerModuleStatus(role string) workerModuleStatus {
status := workerModuleStatus{
enabled: []string{workerModuleQueueServer},
enabled: []string{workerModuleQueueServer, workerModuleOutboxRelay},
}
if runsSingletonModules(role) {
@@ -233,6 +240,7 @@ func initWorkerRuntime(ctx context.Context, cfg *config.Config, appLogger *zap.L
}
workerServer := queue.NewServer(redisClient, &cfg.Queue, appLogger)
outboxQueueClient := queue.NewClient(redisClient, appLogger)
pollingConfigStore := postgres.NewPollingConfigStore(db)
pollingConfigMgr := polling.NewPollingConfigManager(pollingConfigStore, redisClient, appLogger)
@@ -275,6 +283,8 @@ func initWorkerRuntime(ctx context.Context, cfg *config.Config, appLogger *zap.L
asynqClient: asynqClient,
workerResult: workerResult,
workerServer: workerServer,
outboxQueueClient: outboxQueueClient,
outboxConsumers: outbox.NewConsumerRegistry(),
pollingConfigMgr: pollingConfigMgr,
pollingQueueMgr: pollingQueueMgr,
pollingIotCardStore: pollingIotCardStore,
@@ -285,6 +295,11 @@ func initWorkerRuntime(ctx context.Context, cfg *config.Config, appLogger *zap.L
// close 在 Worker 退出时关闭共享客户端与数据库连接。
func (r *workerRuntime) close(appLogger *zap.Logger) {
if r.outboxQueueClient != nil {
if err := r.outboxQueueClient.Close(); err != nil {
appLogger.Error("关闭 Outbox 队列客户端失败", zap.Error(err))
}
}
if r.asynqClient != nil {
if err := r.asynqClient.Close(); err != nil {
appLogger.Error("关闭 Asynq 客户端失败", zap.Error(err))
@@ -305,6 +320,39 @@ func (r *workerRuntime) close(appLogger *zap.Logger) {
}
}
// startOutboxRelay 启动公共 Outbox 的可取消轮询;多个 Worker 依靠 PostgreSQL 租约并发协调。
func startOutboxRelay(ctx context.Context, runtime *workerRuntime, instanceName string, appLogger *zap.Logger) {
owner := instanceName
if owner == "" {
owner = fmt.Sprintf("worker-%d", os.Getpid())
}
relay, err := outbox.NewRelay(
runtime.db,
outbox.NewQueuePublisher(runtime.outboxQueueClient),
appLogger,
outbox.RelayOptions{Owner: owner},
)
if err != nil {
appLogger.Fatal("初始化 Outbox Relay 失败", zap.Error(err))
}
go func() {
ticker := time.NewTicker(constants.OutboxRelayPollInterval)
defer ticker.Stop()
for {
if _, err := relay.ProcessBatch(ctx); err != nil && ctx.Err() == nil {
appLogger.Error("Outbox Relay 批次处理失败",
zap.String("component", "outbox_relay"), zap.String("error_code", "OUTBOX_RELAY_BATCH_FAILED"), zap.Error(err))
}
select {
case <-ctx.Done():
return
case <-ticker.C:
}
}
}()
appLogger.Info("Outbox Relay 已启动", zap.String("lease_owner", owner))
}
// startPollingInitializer 为 leader/all 启动渐进式初始化器,并保留配置变更重启逻辑。
func startPollingInitializer(ctx context.Context, runtime *workerRuntime, appLogger *zap.Logger) *polling.PollingInitializer {
pollingInitializer := polling.NewPollingInitializer(