后端: 1. 阶段 4 active-scheduler 服务边界落地,新增 `cmd/active-scheduler`、`services/active_scheduler`、`shared/contracts/activescheduler` 和 active-scheduler port,迁移 dry-run、trigger、preview、confirm zrpc 能力 2. active-scheduler outbox consumer、relay、retry loop 和 due job scanner 迁入独立服务入口,gateway `/active-schedule/*` 改为通过 zrpc client 调用 3. gateway 目录收口为 `gateway/api` + `gateway/client`,统一归档 userauth、notification、active-scheduler 的 HTTP 门面和 zrpc client 4. 将旧 `backend/active_scheduler` 领域核心下沉到 `services/active_scheduler/core`,清退旧根目录活跃实现,并补充 active-scheduler 启动期跨域依赖表检查 5. 调整单体启动与 outbox 归属,`cmd/all` 不再启动 active-scheduler workflow、scanner 或 handler 文档: 1. 更新微服务迁移计划,将阶段 4 active-scheduler 标记为首轮收口完成,并明确下一阶段进入 schedule / task / course / task-class
132 lines
5.5 KiB
Go
132 lines
5.5 KiB
Go
package dao
|
||
|
||
import (
|
||
"fmt"
|
||
|
||
outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox"
|
||
coremodel "github.com/LoveLosita/smartflow/backend/model"
|
||
"github.com/spf13/viper"
|
||
"gorm.io/driver/mysql"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// OpenDBFromConfig 创建 active-scheduler 服务自己的数据库句柄。
|
||
//
|
||
// 职责边界:
|
||
// 1. 只迁移 active-scheduler 拥有的 trigger / preview / job / session 表和本服务 outbox 表;
|
||
// 2. 不迁移 task、schedule、agent、notification 或 user/auth 表,避免独立进程越权管理其它服务模型;
|
||
// 3. 返回的 *gorm.DB 供服务内主链路、due job scanner 和 outbox consumer 复用。
|
||
func OpenDBFromConfig() (*gorm.DB, error) {
|
||
host := viper.GetString("database.host")
|
||
port := viper.GetString("database.port")
|
||
user := viper.GetString("database.user")
|
||
password := viper.GetString("database.password")
|
||
dbname := viper.GetString("database.dbname")
|
||
|
||
dsn := fmt.Sprintf(
|
||
"%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||
user, password, host, port, dbname,
|
||
)
|
||
|
||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if err = db.AutoMigrate(
|
||
&coremodel.ActiveScheduleJob{},
|
||
&coremodel.ActiveScheduleTrigger{},
|
||
&coremodel.ActiveSchedulePreview{},
|
||
&coremodel.ActiveScheduleSession{},
|
||
); err != nil {
|
||
return nil, fmt.Errorf("auto migrate active-scheduler tables failed: %w", err)
|
||
}
|
||
if err = autoMigrateActiveSchedulerOutboxTable(db); err != nil {
|
||
return nil, err
|
||
}
|
||
if err = ensureRuntimeDependencyTables(db); err != nil {
|
||
return nil, err
|
||
}
|
||
return db, nil
|
||
}
|
||
|
||
// autoMigrateActiveSchedulerOutboxTable 只迁移 active-scheduler 服务自己的 outbox 物理表。
|
||
//
|
||
// 职责边界:
|
||
// 1. 只负责 active-scheduler.outbox 对应表,不碰其它服务 outbox;
|
||
// 2. 让独立 active-scheduler 服务可以单独发布 trigger 并消费 active_schedule.triggered;
|
||
// 3. 若后续调整 outbox 表名,只改 service catalog,不在这里硬编码。
|
||
func autoMigrateActiveSchedulerOutboxTable(db *gorm.DB) error {
|
||
cfg, ok := outboxinfra.ResolveServiceConfig(outboxinfra.ServiceActiveScheduler)
|
||
if !ok {
|
||
return fmt.Errorf("resolve active-scheduler outbox config failed")
|
||
}
|
||
if err := db.Table(cfg.TableName).AutoMigrate(&coremodel.AgentOutboxMessage{}); err != nil {
|
||
return fmt.Errorf("auto migrate active-scheduler outbox table failed for %s (%s): %w", cfg.Name, cfg.TableName, err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
type runtimeDependencyTable struct {
|
||
Name string
|
||
Reason string
|
||
}
|
||
|
||
// ensureRuntimeDependencyTables 在服务启动期校验迁移期共享主库依赖。
|
||
//
|
||
// 职责边界:
|
||
// 1. 只检查表是否存在,不 AutoMigrate、不补列、不修改任何跨域表;
|
||
// 2. 把 active-scheduler 运行时仍然需要的 task / schedule / agent / notification outbox 边界显式化;
|
||
// 3. 若部署顺序、库权限或表结构归属不满足,启动阶段直接 fail fast,避免第一次 trigger 才反复重试。
|
||
func ensureRuntimeDependencyTables(db *gorm.DB) error {
|
||
if db == nil {
|
||
return fmt.Errorf("active-scheduler runtime dependency check failed: db is nil")
|
||
}
|
||
for _, table := range activeSchedulerRuntimeDependencyTables() {
|
||
if err := ensureTableExists(db, table); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ensureTableExists 只做存在性探测。
|
||
//
|
||
// 职责边界:
|
||
// 1. 不负责判断字段是否兼容,字段级契约由拥有该表的服务迁移脚本保证;
|
||
// 2. 不负责自动修复缺失表,避免 active-scheduler 越权创建其它服务的数据模型;
|
||
// 3. 返回错误会阻止服务启动,让部署问题尽早显现。
|
||
func ensureTableExists(db *gorm.DB, table runtimeDependencyTable) error {
|
||
if table.Name == "" {
|
||
return fmt.Errorf("active-scheduler runtime dependency table name is empty: %s", table.Reason)
|
||
}
|
||
if db.Migrator().HasTable(table.Name) {
|
||
return nil
|
||
}
|
||
return fmt.Errorf("active-scheduler runtime dependency table missing: %s (%s)", table.Name, table.Reason)
|
||
}
|
||
|
||
// activeSchedulerRuntimeDependencyTables 列出迁移期运行仍需共享主库访问的外部表。
|
||
//
|
||
// 说明:
|
||
// 1. active-scheduler 自有表在 OpenDBFromConfig 内迁移,这里只放跨域依赖;
|
||
// 2. notification outbox 表名来自 service catalog,避免和 outbox 多表路由配置漂移;
|
||
// 3. 后续切到 task/schedule/agent/notification RPC 或 read model 后,应从这里移除对应表依赖。
|
||
func activeSchedulerRuntimeDependencyTables() []runtimeDependencyTable {
|
||
notificationOutboxTable := "notification_outbox_messages"
|
||
if cfg, ok := outboxinfra.ResolveServiceConfig(outboxinfra.ServiceNotification); ok && cfg.TableName != "" {
|
||
notificationOutboxTable = cfg.TableName
|
||
}
|
||
|
||
return []runtimeDependencyTable{
|
||
{Name: "tasks", Reason: "dry-run 读取 task_pool 事实,confirm 时锁定 task_pool 目标"},
|
||
{Name: "schedule_events", Reason: "dry-run 读取日程事实,confirm 时写入正式日程事件"},
|
||
{Name: "schedules", Reason: "dry-run 读取节次占用,confirm 时写入正式节次"},
|
||
{Name: "task_classes", Reason: "confirm create_makeup 时校验 task_item 归属"},
|
||
{Name: "task_items", Reason: "confirm create_makeup 时锁定 task_item 目标"},
|
||
{Name: "agent_chats", Reason: "trigger 生成 preview 后预建主动调度会话"},
|
||
{Name: "chat_histories", Reason: "trigger 生成 preview 后写入会话首屏消息"},
|
||
{Name: "agent_timeline_events", Reason: "trigger 生成 preview 后写入主动调度时间线卡片"},
|
||
{Name: notificationOutboxTable, Reason: "ShouldNotify=true 时投递 notification.feishu.requested 事件"},
|
||
}
|
||
}
|