diff --git a/backend/gateway/client/activescheduler/client.go b/backend/client/activescheduler/client.go similarity index 100% rename from backend/gateway/client/activescheduler/client.go rename to backend/client/activescheduler/client.go diff --git a/backend/gateway/client/activescheduler/errors.go b/backend/client/activescheduler/errors.go similarity index 98% rename from backend/gateway/client/activescheduler/errors.go rename to backend/client/activescheduler/errors.go index f0aeb71..ca2b951 100644 --- a/backend/gateway/client/activescheduler/errors.go +++ b/backend/client/activescheduler/errors.go @@ -5,8 +5,8 @@ import ( "fmt" "strings" - "github.com/LoveLosita/smartflow/backend/respond" contracts "github.com/LoveLosita/smartflow/backend/shared/contracts/activescheduler" + "github.com/LoveLosita/smartflow/backend/shared/respond" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/backend/gateway/client/agent/client.go b/backend/client/agent/client.go similarity index 100% rename from backend/gateway/client/agent/client.go rename to backend/client/agent/client.go diff --git a/backend/gateway/client/agent/errors.go b/backend/client/agent/errors.go similarity index 97% rename from backend/gateway/client/agent/errors.go rename to backend/client/agent/errors.go index aca9bc9..332ed15 100644 --- a/backend/gateway/client/agent/errors.go +++ b/backend/client/agent/errors.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/shared/respond" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/backend/gateway/client/course/client.go b/backend/client/course/client.go similarity index 100% rename from backend/gateway/client/course/client.go rename to backend/client/course/client.go diff --git a/backend/gateway/client/course/errors.go b/backend/client/course/errors.go similarity index 97% rename from backend/gateway/client/course/errors.go rename to backend/client/course/errors.go index c70fede..ce2553e 100644 --- a/backend/gateway/client/course/errors.go +++ b/backend/client/course/errors.go @@ -6,7 +6,7 @@ import ( "fmt" "strings" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/shared/respond" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/backend/gateway/client/memory/client.go b/backend/client/memory/client.go similarity index 100% rename from backend/gateway/client/memory/client.go rename to backend/client/memory/client.go diff --git a/backend/gateway/client/memory/errors.go b/backend/client/memory/errors.go similarity index 97% rename from backend/gateway/client/memory/errors.go rename to backend/client/memory/errors.go index 5642253..1dcedb7 100644 --- a/backend/gateway/client/memory/errors.go +++ b/backend/client/memory/errors.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/shared/respond" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/backend/gateway/client/notification/client.go b/backend/client/notification/client.go similarity index 100% rename from backend/gateway/client/notification/client.go rename to backend/client/notification/client.go diff --git a/backend/gateway/client/notification/errors.go b/backend/client/notification/errors.go similarity index 98% rename from backend/gateway/client/notification/errors.go rename to backend/client/notification/errors.go index a65f93f..b952fbd 100644 --- a/backend/gateway/client/notification/errors.go +++ b/backend/client/notification/errors.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/shared/respond" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/backend/gateway/client/schedule/client.go b/backend/client/schedule/client.go similarity index 100% rename from backend/gateway/client/schedule/client.go rename to backend/client/schedule/client.go diff --git a/backend/gateway/client/schedule/errors.go b/backend/client/schedule/errors.go similarity index 97% rename from backend/gateway/client/schedule/errors.go rename to backend/client/schedule/errors.go index 7904b38..4ea59b6 100644 --- a/backend/gateway/client/schedule/errors.go +++ b/backend/client/schedule/errors.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/shared/respond" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/backend/gateway/client/task/client.go b/backend/client/task/client.go similarity index 100% rename from backend/gateway/client/task/client.go rename to backend/client/task/client.go diff --git a/backend/gateway/client/task/errors.go b/backend/client/task/errors.go similarity index 97% rename from backend/gateway/client/task/errors.go rename to backend/client/task/errors.go index fb88363..c97b6bb 100644 --- a/backend/gateway/client/task/errors.go +++ b/backend/client/task/errors.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/shared/respond" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/backend/gateway/client/taskclass/client.go b/backend/client/taskclass/client.go similarity index 100% rename from backend/gateway/client/taskclass/client.go rename to backend/client/taskclass/client.go diff --git a/backend/gateway/client/taskclass/errors.go b/backend/client/taskclass/errors.go similarity index 97% rename from backend/gateway/client/taskclass/errors.go rename to backend/client/taskclass/errors.go index 80f631e..cee79a3 100644 --- a/backend/gateway/client/taskclass/errors.go +++ b/backend/client/taskclass/errors.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/shared/respond" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/backend/gateway/client/userauth/client.go b/backend/client/userauth/client.go similarity index 100% rename from backend/gateway/client/userauth/client.go rename to backend/client/userauth/client.go diff --git a/backend/gateway/client/userauth/errors.go b/backend/client/userauth/errors.go similarity index 98% rename from backend/gateway/client/userauth/errors.go rename to backend/client/userauth/errors.go index da2aecf..b4292b9 100644 --- a/backend/gateway/client/userauth/errors.go +++ b/backend/client/userauth/errors.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/shared/respond" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/backend/cmd/active-scheduler/main.go b/backend/cmd/active-scheduler/main.go index 962579c..dfce38f 100644 --- a/backend/cmd/active-scheduler/main.go +++ b/backend/cmd/active-scheduler/main.go @@ -7,14 +7,14 @@ import ( "os/signal" "syscall" - "github.com/LoveLosita/smartflow/backend/bootstrap" - kafkabus "github.com/LoveLosita/smartflow/backend/infra/kafka" - "github.com/LoveLosita/smartflow/backend/inits" activeadapters "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/adapters" activeschedulerdao "github.com/LoveLosita/smartflow/backend/services/active_scheduler/dao" activeschedulerrpc "github.com/LoveLosita/smartflow/backend/services/active_scheduler/rpc" activeschedulersv "github.com/LoveLosita/smartflow/backend/services/active_scheduler/sv" llmservice "github.com/LoveLosita/smartflow/backend/services/llm" + "github.com/LoveLosita/smartflow/backend/shared/infra/bootstrap" + einoinfra "github.com/LoveLosita/smartflow/backend/shared/infra/eino" + kafkabus "github.com/LoveLosita/smartflow/backend/shared/infra/kafka" "github.com/spf13/viper" ) @@ -31,7 +31,7 @@ func main() { log.Fatalf("failed to connect active-scheduler database: %v", err) } - aiHub, err := inits.InitEino() + aiHub, err := einoinfra.InitEino() if err != nil { log.Fatalf("failed to initialize active-scheduler Eino runtime: %v", err) } diff --git a/backend/cmd/agent/active_schedule_rerun.go b/backend/cmd/agent/active_schedule_rerun.go index 93f824e..5f0551e 100644 --- a/backend/cmd/agent/active_schedule_rerun.go +++ b/backend/cmd/agent/active_schedule_rerun.go @@ -7,8 +7,6 @@ import ( "strings" "time" - rootdao "github.com/LoveLosita/smartflow/backend/dao" - "github.com/LoveLosita/smartflow/backend/model" activeapplyadapter "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/applyadapter" activefeedbacklocate "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/feedbacklocate" activegraph "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/graph" @@ -18,6 +16,8 @@ import ( activeTrigger "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/trigger" agentstream "github.com/LoveLosita/smartflow/backend/services/agent/stream" agentsv "github.com/LoveLosita/smartflow/backend/services/agent/sv" + rootdao "github.com/LoveLosita/smartflow/backend/services/runtime/dao" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" ) func buildActiveSchedulePreviewConfirmService(activeDAO *rootdao.ActiveScheduleDAO, dryRun *activesvc.DryRunService, scheduleApplyAdapter interface { diff --git a/backend/cmd/agent/main.go b/backend/cmd/agent/main.go index 4e9f9b1..cdaf989 100644 --- a/backend/cmd/agent/main.go +++ b/backend/cmd/agent/main.go @@ -7,8 +7,8 @@ import ( "os/signal" "syscall" - "github.com/LoveLosita/smartflow/backend/bootstrap" agentrpc "github.com/LoveLosita/smartflow/backend/services/agent/rpc" + "github.com/LoveLosita/smartflow/backend/shared/infra/bootstrap" "github.com/spf13/viper" ) diff --git a/backend/cmd/agent/runtime.go b/backend/cmd/agent/runtime.go index 6f8b95c..7e62d39 100644 --- a/backend/cmd/agent/runtime.go +++ b/backend/cmd/agent/runtime.go @@ -8,19 +8,11 @@ import ( "os" "strings" - rootdao "github.com/LoveLosita/smartflow/backend/dao" - gatewaymemory "github.com/LoveLosita/smartflow/backend/gateway/client/memory" - gatewayschedule "github.com/LoveLosita/smartflow/backend/gateway/client/schedule" - gatewaytask "github.com/LoveLosita/smartflow/backend/gateway/client/task" - gatewaytaskclass "github.com/LoveLosita/smartflow/backend/gateway/client/taskclass" - gatewayuserauth "github.com/LoveLosita/smartflow/backend/gateway/client/userauth" - kafkabus "github.com/LoveLosita/smartflow/backend/infra/kafka" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" - "github.com/LoveLosita/smartflow/backend/inits" - rootmiddleware "github.com/LoveLosita/smartflow/backend/middleware" - "github.com/LoveLosita/smartflow/backend/model" - rootsvc "github.com/LoveLosita/smartflow/backend/service" - eventsvc "github.com/LoveLosita/smartflow/backend/service/events" + memoryclient "github.com/LoveLosita/smartflow/backend/client/memory" + scheduleclient "github.com/LoveLosita/smartflow/backend/client/schedule" + taskclient "github.com/LoveLosita/smartflow/backend/client/task" + taskclassclient "github.com/LoveLosita/smartflow/backend/client/taskclass" + userauthclient "github.com/LoveLosita/smartflow/backend/client/userauth" activeadapters "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/adapters" activefeedbacklocate "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/feedbacklocate" activegraph "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/graph" @@ -35,6 +27,19 @@ import ( memoryobserve "github.com/LoveLosita/smartflow/backend/services/memory/observe" ragservice "github.com/LoveLosita/smartflow/backend/services/rag" ragconfig "github.com/LoveLosita/smartflow/backend/services/rag/config" + rootdao "github.com/LoveLosita/smartflow/backend/services/runtime/dao" + eventsvc "github.com/LoveLosita/smartflow/backend/services/runtime/eventsvc" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + scheduledao "github.com/LoveLosita/smartflow/backend/services/schedule/dao" + schedulesv "github.com/LoveLosita/smartflow/backend/services/schedule/sv" + taskdao "github.com/LoveLosita/smartflow/backend/services/task/dao" + tasksv "github.com/LoveLosita/smartflow/backend/services/task/sv" + einoinfra "github.com/LoveLosita/smartflow/backend/shared/infra/eino" + gormcache "github.com/LoveLosita/smartflow/backend/shared/infra/gormcache" + kafkabus "github.com/LoveLosita/smartflow/backend/shared/infra/kafka" + mysqlinfra "github.com/LoveLosita/smartflow/backend/shared/infra/mysql" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" + redisinfra "github.com/LoveLosita/smartflow/backend/shared/infra/redis" "github.com/LoveLosita/smartflow/backend/shared/ports" "github.com/go-redis/redis/v8" "github.com/spf13/viper" @@ -48,7 +53,7 @@ type agentRuntime struct { repoManager *rootdao.RepoManager agentRepo *rootdao.AgentDAO cacheRepo *rootdao.CacheDAO - userAuthClient *gatewayuserauth.Client + userAuthClient *userauthclient.Client service *agentsv.AgentService workersStarted bool } @@ -59,7 +64,7 @@ func buildAgentRuntime(ctx context.Context) (*agentRuntime, error) { return nil, fmt.Errorf("connect agent database failed: %w", err) } - redisClient, err := inits.OpenRedisFromConfig() + redisClient, err := redisinfra.OpenRedisFromConfig() if err != nil { return nil, fmt.Errorf("connect agent redis failed: %w", err) } @@ -69,7 +74,7 @@ func buildAgentRuntime(ctx context.Context) (*agentRuntime, error) { } cacheRepo := rootdao.NewCacheDAO(redisClient) - if err = db.Use(rootmiddleware.NewGormCachePlugin(cacheRepo)); err != nil { + if err = db.Use(gormcache.NewGormCachePlugin(cacheRepo)); err != nil { return fail(fmt.Errorf("initialize agent cache deleter failed: %w", err)) } @@ -94,8 +99,9 @@ func buildAgentRuntime(ctx context.Context) (*agentRuntime, error) { manager := rootdao.NewManager(db) agentRepo := rootdao.NewAgentDAO(db) taskRepo := rootdao.NewTaskDAO(db) + taskServiceRepo := taskdao.NewTaskDAO(db) taskClassRepo := rootdao.NewTaskClassDAO(db) - scheduleRepo := rootdao.NewScheduleDAO(db) + scheduleServiceRepo := scheduledao.NewScheduleDAO(db) agentCacheRepo := rootdao.NewAgentCache(redisClient) outboxRepo := outboxinfra.NewRepository(db) @@ -110,9 +116,9 @@ func buildAgentRuntime(ctx context.Context) (*agentRuntime, error) { eventPublisher := buildAgentOutboxPublisher(outboxRepo) taskOutboxPublisher := buildTaskOutboxPublisher(outboxRepo) - var userAuthClient *gatewayuserauth.Client + var userAuthClient *userauthclient.Client if eventBus != nil { - userAuthClient, err = gatewayuserauth.NewClient(gatewayuserauth.ClientConfig{ + userAuthClient, err = userauthclient.NewClient(userauthclient.ClientConfig{ Endpoints: viper.GetStringSlice("userauth.rpc.endpoints"), Target: viper.GetString("userauth.rpc.target"), Timeout: viper.GetDuration("userauth.rpc.timeout"), @@ -122,7 +128,7 @@ func buildAgentRuntime(ctx context.Context) (*agentRuntime, error) { } } - taskClient, err := gatewaytask.NewClient(gatewaytask.ClientConfig{ + taskClient, err := taskclient.NewClient(taskclient.ClientConfig{ Endpoints: viper.GetStringSlice("task.rpc.endpoints"), Target: viper.GetString("task.rpc.target"), Timeout: viper.GetDuration("task.rpc.timeout"), @@ -130,7 +136,7 @@ func buildAgentRuntime(ctx context.Context) (*agentRuntime, error) { if err != nil { return fail(fmt.Errorf("initialize task zrpc client failed: %w", err)) } - taskClassClient, err := gatewaytaskclass.NewClient(gatewaytaskclass.ClientConfig{ + taskClassClient, err := taskclassclient.NewClient(taskclassclient.ClientConfig{ Endpoints: viper.GetStringSlice("taskClass.rpc.endpoints"), Target: viper.GetString("taskClass.rpc.target"), Timeout: viper.GetDuration("taskClass.rpc.timeout"), @@ -138,7 +144,7 @@ func buildAgentRuntime(ctx context.Context) (*agentRuntime, error) { if err != nil { return fail(fmt.Errorf("initialize task-class zrpc client failed: %w", err)) } - scheduleClient, err := gatewayschedule.NewClient(gatewayschedule.ClientConfig{ + scheduleClient, err := scheduleclient.NewClient(scheduleclient.ClientConfig{ Endpoints: viper.GetStringSlice("schedule.rpc.endpoints"), Target: viper.GetString("schedule.rpc.target"), Timeout: viper.GetDuration("schedule.rpc.timeout"), @@ -146,7 +152,7 @@ func buildAgentRuntime(ctx context.Context) (*agentRuntime, error) { if err != nil { return fail(fmt.Errorf("initialize schedule zrpc client failed: %w", err)) } - memoryClient, err := gatewaymemory.NewClient(gatewaymemory.ClientConfig{ + memoryClient, err := memoryclient.NewClient(memoryclient.ClientConfig{ Endpoints: viper.GetStringSlice("memory.rpc.endpoints"), Target: viper.GetString("memory.rpc.target"), Timeout: viper.GetDuration("memory.rpc.timeout"), @@ -155,9 +161,9 @@ func buildAgentRuntime(ctx context.Context) (*agentRuntime, error) { return fail(fmt.Errorf("initialize memory zrpc client failed: %w", err)) } - taskService := rootsvc.NewTaskService(taskRepo, cacheRepo, taskOutboxPublisher) + taskService := tasksv.NewTaskService(taskServiceRepo, cacheRepo, taskOutboxPublisher) taskService.SetActiveScheduleDAO(manager.ActiveSchedule) - scheduleService := rootsvc.NewScheduleService(scheduleRepo, taskClassRepo, manager, cacheRepo) + scheduleService := schedulesv.NewScheduleService(scheduleServiceRepo, taskClassRepo, manager, cacheRepo) agentService := agentsv.NewAgentService( llmService, agentRepo, @@ -286,7 +292,7 @@ func (r *agentRuntime) close() { } func openAgentDBFromConfig() (*gorm.DB, error) { - db, err := inits.OpenDBFromConfig() + db, err := mysqlinfra.OpenDBFromConfig() if err != nil { return nil, err } @@ -364,7 +370,7 @@ func ensureAgentRuntimeDependencyTables(db *gorm.DB) error { } func buildAgentLLMService() (*llmservice.Service, error) { - aiHub, err := inits.InitEino() + aiHub, err := einoinfra.InitEino() if err != nil { return nil, err } diff --git a/backend/cmd/course/main.go b/backend/cmd/course/main.go index 141f10b..0a93052 100644 --- a/backend/cmd/course/main.go +++ b/backend/cmd/course/main.go @@ -7,12 +7,12 @@ import ( "os/signal" "syscall" - "github.com/LoveLosita/smartflow/backend/bootstrap" - rootdao "github.com/LoveLosita/smartflow/backend/dao" coursedao "github.com/LoveLosita/smartflow/backend/services/course/dao" courserpc "github.com/LoveLosita/smartflow/backend/services/course/rpc" coursesv "github.com/LoveLosita/smartflow/backend/services/course/sv" llmservice "github.com/LoveLosita/smartflow/backend/services/llm" + rootdao "github.com/LoveLosita/smartflow/backend/services/runtime/dao" + "github.com/LoveLosita/smartflow/backend/shared/infra/bootstrap" "github.com/spf13/viper" ) diff --git a/backend/inits/mysql.go b/backend/cmd/internal/coreinit/mysql.go similarity index 88% rename from backend/inits/mysql.go rename to backend/cmd/internal/coreinit/mysql.go index 23c6051..e6df1f3 100644 --- a/backend/inits/mysql.go +++ b/backend/cmd/internal/coreinit/mysql.go @@ -1,13 +1,12 @@ -package inits +package coreinit import ( "fmt" "log" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/spf13/viper" - "gorm.io/driver/mysql" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + mysqlinfra "github.com/LoveLosita/smartflow/backend/shared/infra/mysql" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" "gorm.io/gorm" ) @@ -102,22 +101,7 @@ func backfillAutoMigrateData(db *gorm.DB) error { // 2. 不负责选择要迁移哪些模型,迁移入口必须由具体服务显式调用; // 3. 调用方负责决定这是单体残留域、user/auth 还是后续新服务的连接。 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 - } - return db, nil + return mysqlinfra.OpenDBFromConfig() } // AutoMigrateCoreStorage 执行当前单体残留域拥有的 schema 初始化。 diff --git a/backend/inits/redis.go b/backend/cmd/internal/coreinit/redis.go similarity index 71% rename from backend/inits/redis.go rename to backend/cmd/internal/coreinit/redis.go index 89abfc5..33cf09b 100644 --- a/backend/inits/redis.go +++ b/backend/cmd/internal/coreinit/redis.go @@ -1,11 +1,10 @@ -package inits +package coreinit import ( - "context" "log" + redisinfra "github.com/LoveLosita/smartflow/backend/shared/infra/redis" "github.com/go-redis/redis/v8" - "github.com/spf13/viper" ) // OpenRedisFromConfig 只创建 Redis client 并做连通性校验。 @@ -15,15 +14,7 @@ import ( // 2. 不承载 user/auth 黑名单、token 额度等业务语义,那些语义已经收进 userauth 服务; // 3. 返回 error 给服务入口统一处理,避免基础设施包直接 log.Fatal 终止进程。 func OpenRedisFromConfig() (*redis.Client, error) { - rdb := redis.NewClient(&redis.Options{ - Addr: viper.GetString("redis.host") + ":" + viper.GetString("redis.port"), - Password: viper.GetString("redis.password"), - DB: 0, - }) - if _, err := rdb.Ping(context.Background()).Result(); err != nil { - return nil, err - } - return rdb, nil + return redisinfra.OpenRedisFromConfig() } // InitCoreRedis 初始化当前单体残留域使用的 Redis 连接。 diff --git a/backend/cmd/memory/main.go b/backend/cmd/memory/main.go index 5d408c8..1b3dcb3 100644 --- a/backend/cmd/memory/main.go +++ b/backend/cmd/memory/main.go @@ -8,10 +8,6 @@ import ( "os/signal" "syscall" - "github.com/LoveLosita/smartflow/backend/bootstrap" - kafkabus "github.com/LoveLosita/smartflow/backend/infra/kafka" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" - "github.com/LoveLosita/smartflow/backend/inits" llmservice "github.com/LoveLosita/smartflow/backend/services/llm" memorymodule "github.com/LoveLosita/smartflow/backend/services/memory" memorydao "github.com/LoveLosita/smartflow/backend/services/memory/dao" @@ -20,6 +16,10 @@ import ( memorysv "github.com/LoveLosita/smartflow/backend/services/memory/sv" ragservice "github.com/LoveLosita/smartflow/backend/services/rag" ragconfig "github.com/LoveLosita/smartflow/backend/services/rag/config" + "github.com/LoveLosita/smartflow/backend/shared/infra/bootstrap" + einoinfra "github.com/LoveLosita/smartflow/backend/shared/infra/eino" + kafkabus "github.com/LoveLosita/smartflow/backend/shared/infra/kafka" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" "github.com/spf13/viper" ) @@ -99,7 +99,7 @@ func main() { // 2. 当前启动入口与 cmd/start.go / cmd/active-scheduler 都需要 Eino 初始化,后续若出现第三处重复装配,应抽公共 bootstrap; // 3. 返回 ProClient 是因为现有 memory.Module 只需要 llmservice.Client,不需要完整 Service。 func buildMemoryLLMClient() (*llmservice.Client, error) { - aiHub, err := inits.InitEino() + aiHub, err := einoinfra.InitEino() if err != nil { return nil, err } diff --git a/backend/cmd/notification/main.go b/backend/cmd/notification/main.go index ef880af..dbb1005 100644 --- a/backend/cmd/notification/main.go +++ b/backend/cmd/notification/main.go @@ -7,12 +7,12 @@ import ( "os/signal" "syscall" - "github.com/LoveLosita/smartflow/backend/bootstrap" - kafkabus "github.com/LoveLosita/smartflow/backend/infra/kafka" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" notificationdao "github.com/LoveLosita/smartflow/backend/services/notification/dao" notificationrpc "github.com/LoveLosita/smartflow/backend/services/notification/rpc" notificationsv "github.com/LoveLosita/smartflow/backend/services/notification/sv" + "github.com/LoveLosita/smartflow/backend/shared/infra/bootstrap" + kafkabus "github.com/LoveLosita/smartflow/backend/shared/infra/kafka" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" "github.com/spf13/viper" ) diff --git a/backend/cmd/schedule/main.go b/backend/cmd/schedule/main.go index 3dd8736..75258f5 100644 --- a/backend/cmd/schedule/main.go +++ b/backend/cmd/schedule/main.go @@ -7,13 +7,13 @@ import ( "os/signal" "syscall" - "github.com/LoveLosita/smartflow/backend/bootstrap" - rootdao "github.com/LoveLosita/smartflow/backend/dao" - rootmiddleware "github.com/LoveLosita/smartflow/backend/middleware" + rootdao "github.com/LoveLosita/smartflow/backend/services/runtime/dao" "github.com/LoveLosita/smartflow/backend/services/schedule/core/applyadapter" scheduledao "github.com/LoveLosita/smartflow/backend/services/schedule/dao" schedulerpc "github.com/LoveLosita/smartflow/backend/services/schedule/rpc" schedulesv "github.com/LoveLosita/smartflow/backend/services/schedule/sv" + "github.com/LoveLosita/smartflow/backend/shared/infra/bootstrap" + gormcache "github.com/LoveLosita/smartflow/backend/shared/infra/gormcache" "github.com/spf13/viper" ) @@ -36,7 +36,7 @@ func main() { defer redisClient.Close() cacheRepo := rootdao.NewCacheDAO(redisClient) - if err := db.Use(rootmiddleware.NewGormCachePlugin(cacheRepo)); err != nil { + if err := db.Use(gormcache.NewGormCachePlugin(cacheRepo)); err != nil { log.Fatalf("failed to initialize schedule cache deleter: %v", err) } diff --git a/backend/cmd/start.go b/backend/cmd/start.go index fd75cdb..e31134d 100644 --- a/backend/cmd/start.go +++ b/backend/cmd/start.go @@ -11,27 +11,18 @@ import ( "syscall" "time" - "github.com/LoveLosita/smartflow/backend/bootstrap" - "github.com/LoveLosita/smartflow/backend/dao" + activeschedulerclient "github.com/LoveLosita/smartflow/backend/client/activescheduler" + agentclient "github.com/LoveLosita/smartflow/backend/client/agent" + courseclient "github.com/LoveLosita/smartflow/backend/client/course" + memoryclient "github.com/LoveLosita/smartflow/backend/client/memory" + notificationclient "github.com/LoveLosita/smartflow/backend/client/notification" + scheduleclient "github.com/LoveLosita/smartflow/backend/client/schedule" + taskclient "github.com/LoveLosita/smartflow/backend/client/task" + taskclassclient "github.com/LoveLosita/smartflow/backend/client/taskclass" + userauthclient "github.com/LoveLosita/smartflow/backend/client/userauth" + coreinit "github.com/LoveLosita/smartflow/backend/cmd/internal/coreinit" "github.com/LoveLosita/smartflow/backend/gateway/api" - gatewayactivescheduler "github.com/LoveLosita/smartflow/backend/gateway/client/activescheduler" - gatewayagent "github.com/LoveLosita/smartflow/backend/gateway/client/agent" - gatewaycourse "github.com/LoveLosita/smartflow/backend/gateway/client/course" - gatewaymemory "github.com/LoveLosita/smartflow/backend/gateway/client/memory" - gatewaynotification "github.com/LoveLosita/smartflow/backend/gateway/client/notification" - gatewayschedule "github.com/LoveLosita/smartflow/backend/gateway/client/schedule" - gatewaytask "github.com/LoveLosita/smartflow/backend/gateway/client/task" - gatewaytaskclass "github.com/LoveLosita/smartflow/backend/gateway/client/taskclass" - gatewayuserauth "github.com/LoveLosita/smartflow/backend/gateway/client/userauth" gatewayrouter "github.com/LoveLosita/smartflow/backend/gateway/router" - kafkabus "github.com/LoveLosita/smartflow/backend/infra/kafka" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" - "github.com/LoveLosita/smartflow/backend/inits" - "github.com/LoveLosita/smartflow/backend/middleware" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/pkg" - "github.com/LoveLosita/smartflow/backend/service" - eventsvc "github.com/LoveLosita/smartflow/backend/service/events" activeadapters "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/adapters" activeapplyadapter "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/applyadapter" activefeedbacklocate "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/feedbacklocate" @@ -44,12 +35,27 @@ import ( agentsv "github.com/LoveLosita/smartflow/backend/services/agent/sv" agenttools "github.com/LoveLosita/smartflow/backend/services/agent/tools" "github.com/LoveLosita/smartflow/backend/services/agent/tools/web" + coursedao "github.com/LoveLosita/smartflow/backend/services/course/dao" + coursesv "github.com/LoveLosita/smartflow/backend/services/course/sv" llmservice "github.com/LoveLosita/smartflow/backend/services/llm" "github.com/LoveLosita/smartflow/backend/services/memory" memorymodel "github.com/LoveLosita/smartflow/backend/services/memory/model" memoryobserve "github.com/LoveLosita/smartflow/backend/services/memory/observe" ragservice "github.com/LoveLosita/smartflow/backend/services/rag" ragconfig "github.com/LoveLosita/smartflow/backend/services/rag/config" + "github.com/LoveLosita/smartflow/backend/services/runtime/dao" + eventsvc "github.com/LoveLosita/smartflow/backend/services/runtime/eventsvc" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + scheduledao "github.com/LoveLosita/smartflow/backend/services/schedule/dao" + schedulesv "github.com/LoveLosita/smartflow/backend/services/schedule/sv" + taskdao "github.com/LoveLosita/smartflow/backend/services/task/dao" + tasksv "github.com/LoveLosita/smartflow/backend/services/task/sv" + "github.com/LoveLosita/smartflow/backend/shared/infra/bootstrap" + einoinfra "github.com/LoveLosita/smartflow/backend/shared/infra/eino" + gormcache "github.com/LoveLosita/smartflow/backend/shared/infra/gormcache" + kafkabus "github.com/LoveLosita/smartflow/backend/shared/infra/kafka" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" + ratelimit "github.com/LoveLosita/smartflow/backend/shared/infra/ratelimit" "github.com/LoveLosita/smartflow/backend/shared/ports" "github.com/go-redis/redis/v8" "github.com/spf13/viper" @@ -75,9 +81,9 @@ type appRuntime struct { agentCache *dao.AgentCache manager *dao.RepoManager outboxRepo *outboxinfra.Repository - limiter *pkg.RateLimiter + limiter *ratelimit.RateLimiter handlers *api.ApiHandlers - userAuthClient *gatewayuserauth.Client + userAuthClient *userauthclient.Client } // loadConfig 锻炼? @@ -154,23 +160,23 @@ func buildRuntime(ctx context.Context) (*appRuntime, error) { return nil, err } - db, err := inits.ConnectCoreDB() + db, err := coreinit.ConnectCoreDB() if err != nil { return nil, fmt.Errorf("failed to connect to database: %w", err) } - rdb, err := inits.InitCoreRedis() + rdb, err := coreinit.InitCoreRedis() if err != nil { return nil, fmt.Errorf("failed to connect to redis: %w", err) } - limiter := pkg.NewRateLimiter(rdb) + limiter := ratelimit.NewRateLimiter(rdb) // DAO 层初始化。 cacheRepo := dao.NewCacheDAO(rdb) - _ = db.Use(middleware.NewGormCachePlugin(cacheRepo)) + _ = db.Use(gormcache.NewGormCachePlugin(cacheRepo)) // Service 层初始化。 - userAuthClient, err := gatewayuserauth.NewClient(gatewayuserauth.ClientConfig{ + userAuthClient, err := userauthclient.NewClient(userauthclient.ClientConfig{ Endpoints: viper.GetStringSlice("userauth.rpc.endpoints"), Target: viper.GetString("userauth.rpc.target"), Timeout: viper.GetDuration("userauth.rpc.timeout"), @@ -178,7 +184,7 @@ func buildRuntime(ctx context.Context) (*appRuntime, error) { if err != nil { return nil, fmt.Errorf("failed to initialize userauth zrpc client: %w", err) } - notificationClient, err := gatewaynotification.NewClient(gatewaynotification.ClientConfig{ + notificationClient, err := notificationclient.NewClient(notificationclient.ClientConfig{ Endpoints: viper.GetStringSlice("notification.rpc.endpoints"), Target: viper.GetString("notification.rpc.target"), Timeout: viper.GetDuration("notification.rpc.timeout"), @@ -186,7 +192,7 @@ func buildRuntime(ctx context.Context) (*appRuntime, error) { if err != nil { return nil, fmt.Errorf("failed to initialize notification zrpc client: %w", err) } - scheduleClient, err := gatewayschedule.NewClient(gatewayschedule.ClientConfig{ + scheduleClient, err := scheduleclient.NewClient(scheduleclient.ClientConfig{ Endpoints: viper.GetStringSlice("schedule.rpc.endpoints"), Target: viper.GetString("schedule.rpc.target"), Timeout: viper.GetDuration("schedule.rpc.timeout"), @@ -194,7 +200,7 @@ func buildRuntime(ctx context.Context) (*appRuntime, error) { if err != nil { return nil, fmt.Errorf("failed to initialize schedule zrpc client: %w", err) } - taskClient, err := gatewaytask.NewClient(gatewaytask.ClientConfig{ + taskClient, err := taskclient.NewClient(taskclient.ClientConfig{ Endpoints: viper.GetStringSlice("task.rpc.endpoints"), Target: viper.GetString("task.rpc.target"), Timeout: viper.GetDuration("task.rpc.timeout"), @@ -202,7 +208,7 @@ func buildRuntime(ctx context.Context) (*appRuntime, error) { if err != nil { return nil, fmt.Errorf("failed to initialize task zrpc client: %w", err) } - taskClassClient, err := gatewaytaskclass.NewClient(gatewaytaskclass.ClientConfig{ + taskClassClient, err := taskclassclient.NewClient(taskclassclient.ClientConfig{ Endpoints: viper.GetStringSlice("taskClass.rpc.endpoints"), Target: viper.GetString("taskClass.rpc.target"), Timeout: viper.GetDuration("taskClass.rpc.timeout"), @@ -210,7 +216,7 @@ func buildRuntime(ctx context.Context) (*appRuntime, error) { if err != nil { return nil, fmt.Errorf("failed to initialize task-class zrpc client: %w", err) } - courseClient, err := gatewaycourse.NewClient(gatewaycourse.ClientConfig{ + courseClient, err := courseclient.NewClient(courseclient.ClientConfig{ Endpoints: viper.GetStringSlice("course.rpc.endpoints"), Target: viper.GetString("course.rpc.target"), Timeout: viper.GetDuration("course.rpc.timeout"), @@ -219,7 +225,7 @@ func buildRuntime(ctx context.Context) (*appRuntime, error) { if err != nil { return nil, fmt.Errorf("failed to initialize course zrpc client: %w", err) } - memoryClient, err := gatewaymemory.NewClient(gatewaymemory.ClientConfig{ + memoryClient, err := memoryclient.NewClient(memoryclient.ClientConfig{ Endpoints: viper.GetStringSlice("memory.rpc.endpoints"), Target: viper.GetString("memory.rpc.target"), Timeout: viper.GetDuration("memory.rpc.timeout"), @@ -227,7 +233,7 @@ func buildRuntime(ctx context.Context) (*appRuntime, error) { if err != nil { return nil, fmt.Errorf("failed to initialize memory zrpc client: %w", err) } - agentRPCClient, err := gatewayagent.NewClient(gatewayagent.ClientConfig{ + agentRPCClient, err := agentclient.NewClient(agentclient.ClientConfig{ Endpoints: viper.GetStringSlice("agent.rpc.endpoints"), Target: viper.GetString("agent.rpc.target"), Timeout: viper.GetDuration("agent.rpc.timeout"), @@ -235,7 +241,7 @@ func buildRuntime(ctx context.Context) (*appRuntime, error) { if err != nil { return nil, fmt.Errorf("failed to initialize agent zrpc client: %w", err) } - activeSchedulerClient, err := gatewayactivescheduler.NewClient(gatewayactivescheduler.ClientConfig{ + activeSchedulerClient, err := activeschedulerclient.NewClient(activeschedulerclient.ClientConfig{ Endpoints: viper.GetStringSlice("activeScheduler.rpc.endpoints"), Target: viper.GetString("activeScheduler.rpc.target"), Timeout: viper.GetDuration("activeScheduler.rpc.timeout"), @@ -251,7 +257,7 @@ func buildRuntime(ctx context.Context) (*appRuntime, error) { if shouldBuildGatewayAgentFallback() { log.Println("Gateway agent RPC fallback is enabled; building local AgentService compatibility path") - aiHub, err := inits.InitEino() + aiHub, err := einoinfra.InitEino() if err != nil { return nil, fmt.Errorf("failed to initialize Eino: %w", err) } @@ -273,8 +279,9 @@ func buildRuntime(ctx context.Context) (*appRuntime, error) { agentCacheRepo = dao.NewAgentCache(rdb) taskRepo := dao.NewTaskDAO(db) + taskServiceRepo := taskdao.NewTaskDAO(db) taskClassRepo := dao.NewTaskClassDAO(db) - scheduleRepo := dao.NewScheduleDAO(db) + scheduleServiceRepo := scheduledao.NewScheduleDAO(db) manager = dao.NewManager(db) agentRepo = dao.NewAgentDAO(db) outboxRepo = outboxinfra.NewRepository(db) @@ -286,9 +293,9 @@ func buildRuntime(ctx context.Context) (*appRuntime, error) { return nil, fmt.Errorf("failed to register task outbox route: %w", err) } taskOutboxPublisher := buildTaskOutboxPublisher(outboxRepo) - taskSv := service.NewTaskService(taskRepo, cacheRepo, taskOutboxPublisher) + taskSv := tasksv.NewTaskService(taskServiceRepo, cacheRepo, taskOutboxPublisher) taskSv.SetActiveScheduleDAO(manager.ActiveSchedule) - scheduleService := service.NewScheduleService(scheduleRepo, taskClassRepo, manager, cacheRepo) + scheduleService := schedulesv.NewScheduleService(scheduleServiceRepo, taskClassRepo, manager, cacheRepo) agentService = agentsv.NewAgentService( llmService, agentRepo, @@ -488,13 +495,13 @@ func (p *repositoryOutboxPublisher) Publish(ctx context.Context, req outboxinfra return err } -func buildCourseService(llmService *llmservice.Service, courseRepo *dao.CourseDAO, scheduleRepo *dao.ScheduleDAO) *service.CourseService { +func buildCourseService(llmService *llmservice.Service, courseRepo *coursedao.CourseDAO, scheduleRepo *dao.ScheduleDAO) *coursesv.CourseService { courseImageResponsesClient := llmService.CourseImageResponsesClient() - return service.NewCourseService( + return coursesv.NewCourseService( courseRepo, scheduleRepo, courseImageResponsesClient, - service.NewCourseImageParseConfig( + coursesv.NewCourseImageParseConfig( viper.GetInt64("courseImport.maxImageBytes"), viper.GetInt("courseImport.maxTokens"), ), @@ -827,7 +834,7 @@ func buildAPIHandlers( courseClient ports.CourseCommandClient, scheduleClient ports.ScheduleCommandClient, agentService *agentsv.AgentService, - agentRPCClient *gatewayagent.Client, + agentRPCClient *agentclient.Client, memoryClient ports.MemoryCommandClient, activeSchedulerClient ports.ActiveSchedulerCommandClient, notificationClient ports.NotificationCommandClient, diff --git a/backend/cmd/task-class/main.go b/backend/cmd/task-class/main.go index 3981318..86eb34b 100644 --- a/backend/cmd/task-class/main.go +++ b/backend/cmd/task-class/main.go @@ -7,12 +7,12 @@ import ( "os/signal" "syscall" - "github.com/LoveLosita/smartflow/backend/bootstrap" - rootdao "github.com/LoveLosita/smartflow/backend/dao" - rootmiddleware "github.com/LoveLosita/smartflow/backend/middleware" + rootdao "github.com/LoveLosita/smartflow/backend/services/runtime/dao" taskclassdao "github.com/LoveLosita/smartflow/backend/services/task_class/dao" taskclassrpc "github.com/LoveLosita/smartflow/backend/services/task_class/rpc" taskclasssv "github.com/LoveLosita/smartflow/backend/services/task_class/sv" + "github.com/LoveLosita/smartflow/backend/shared/infra/bootstrap" + gormcache "github.com/LoveLosita/smartflow/backend/shared/infra/gormcache" "github.com/spf13/viper" ) @@ -35,7 +35,7 @@ func main() { defer redisClient.Close() cacheRepo := rootdao.NewCacheDAO(redisClient) - if err := db.Use(rootmiddleware.NewGormCachePlugin(cacheRepo)); err != nil { + if err := db.Use(gormcache.NewGormCachePlugin(cacheRepo)); err != nil { log.Fatalf("failed to initialize task-class cache deleter: %v", err) } diff --git a/backend/cmd/task/main.go b/backend/cmd/task/main.go index 471dd33..4ee8579 100644 --- a/backend/cmd/task/main.go +++ b/backend/cmd/task/main.go @@ -7,14 +7,14 @@ import ( "os/signal" "syscall" - "github.com/LoveLosita/smartflow/backend/bootstrap" - rootdao "github.com/LoveLosita/smartflow/backend/dao" - kafkabus "github.com/LoveLosita/smartflow/backend/infra/kafka" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" - rootmiddleware "github.com/LoveLosita/smartflow/backend/middleware" + rootdao "github.com/LoveLosita/smartflow/backend/services/runtime/dao" taskdao "github.com/LoveLosita/smartflow/backend/services/task/dao" taskrpc "github.com/LoveLosita/smartflow/backend/services/task/rpc" tasksv "github.com/LoveLosita/smartflow/backend/services/task/sv" + "github.com/LoveLosita/smartflow/backend/shared/infra/bootstrap" + gormcache "github.com/LoveLosita/smartflow/backend/shared/infra/gormcache" + kafkabus "github.com/LoveLosita/smartflow/backend/shared/infra/kafka" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" "github.com/spf13/viper" ) @@ -37,7 +37,7 @@ func main() { defer redisClient.Close() cacheRepo := rootdao.NewCacheDAO(redisClient) - if err := db.Use(rootmiddleware.NewGormCachePlugin(cacheRepo)); err != nil { + if err := db.Use(gormcache.NewGormCachePlugin(cacheRepo)); err != nil { log.Fatalf("failed to initialize task cache deleter: %v", err) } diff --git a/backend/cmd/userauth/main.go b/backend/cmd/userauth/main.go index cbf46d9..256053d 100644 --- a/backend/cmd/userauth/main.go +++ b/backend/cmd/userauth/main.go @@ -3,10 +3,10 @@ package main import ( "log" - "github.com/LoveLosita/smartflow/backend/bootstrap" userauthdao "github.com/LoveLosita/smartflow/backend/services/userauth/dao" userauthrpc "github.com/LoveLosita/smartflow/backend/services/userauth/rpc" userauthsv "github.com/LoveLosita/smartflow/backend/services/userauth/sv" + "github.com/LoveLosita/smartflow/backend/shared/infra/bootstrap" "github.com/spf13/viper" ) diff --git a/backend/gateway/api/active_schedule.go b/backend/gateway/api/active_schedule.go index f56b923..a650bfa 100644 --- a/backend/gateway/api/active_schedule.go +++ b/backend/gateway/api/active_schedule.go @@ -7,7 +7,7 @@ import ( "net/http" "time" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/gateway/shared/respond" contracts "github.com/LoveLosita/smartflow/backend/shared/contracts/activescheduler" "github.com/LoveLosita/smartflow/backend/shared/ports" "github.com/gin-gonic/gin" diff --git a/backend/gateway/api/agent.go b/backend/gateway/api/agent.go index 5adc18e..b5e0c3b 100644 --- a/backend/gateway/api/agent.go +++ b/backend/gateway/api/agent.go @@ -11,10 +11,10 @@ import ( "sync" "time" - gatewayagent "github.com/LoveLosita/smartflow/backend/gateway/client/agent" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" + agentclient "github.com/LoveLosita/smartflow/backend/client/agent" + "github.com/LoveLosita/smartflow/backend/gateway/shared/respond" agentsv "github.com/LoveLosita/smartflow/backend/services/agent/sv" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" agentcontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/agent" "github.com/gin-gonic/gin" "github.com/google/uuid" @@ -30,7 +30,7 @@ const ( type AgentHandler struct { svc *agentsv.AgentService - rpcClient *gatewayagent.Client + rpcClient *agentclient.Client rpcClientMu sync.Mutex } @@ -48,7 +48,7 @@ func NewAgentHandler(svc *agentsv.AgentService) *AgentHandler { // 2. agent RPC 作为 chat stream 与非 chat /agent/* 查询/命令的服务间通道; // 3. svc 只用于 RPC 开关关闭时的迁移期 fallback,当前默认可为 nil; // 4. rpcClient 为空时允许按配置懒加载,避免测试和旧装配必须提前构造 client。 -func NewAgentHandlerWithRPC(svc *agentsv.AgentService, rpcClient *gatewayagent.Client) *AgentHandler { +func NewAgentHandlerWithRPC(svc *agentsv.AgentService, rpcClient *agentclient.Client) *AgentHandler { return &AgentHandler{ svc: svc, rpcClient: rpcClient, @@ -302,7 +302,7 @@ func writeAgentSSEError(w io.Writer, err error) { _ = writeSSEData(w, "[DONE]") } -func (api *AgentHandler) getAgentRPCClient() (*gatewayagent.Client, error) { +func (api *AgentHandler) getAgentRPCClient() (*agentclient.Client, error) { if api == nil { return nil, errors.New("agent handler is not initialized") } @@ -314,7 +314,7 @@ func (api *AgentHandler) getAgentRPCClient() (*gatewayagent.Client, error) { return api.rpcClient, nil } - client, err := gatewayagent.NewClient(gatewayagent.ClientConfig{ + client, err := agentclient.NewClient(agentclient.ClientConfig{ Endpoints: viper.GetStringSlice("agent.rpc.endpoints"), Target: viper.GetString("agent.rpc.target"), Timeout: viper.GetDuration("agent.rpc.timeout"), diff --git a/backend/gateway/api/course.go b/backend/gateway/api/course.go index 7c1d4be..1baa63e 100644 --- a/backend/gateway/api/course.go +++ b/backend/gateway/api/course.go @@ -9,7 +9,7 @@ import ( "net/http" "time" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/gateway/shared/respond" coursecontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/course" "github.com/LoveLosita/smartflow/backend/shared/ports" "github.com/gin-gonic/gin" diff --git a/backend/gateway/api/memory.go b/backend/gateway/api/memory.go index 31cb817..70319f6 100644 --- a/backend/gateway/api/memory.go +++ b/backend/gateway/api/memory.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/gateway/shared/respond" memorycontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/memory" "github.com/LoveLosita/smartflow/backend/shared/ports" "github.com/gin-gonic/gin" diff --git a/backend/gateway/api/notification.go b/backend/gateway/api/notification.go index d1bc661..8c30794 100644 --- a/backend/gateway/api/notification.go +++ b/backend/gateway/api/notification.go @@ -5,7 +5,7 @@ import ( "net/http" "time" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/gateway/shared/respond" contracts "github.com/LoveLosita/smartflow/backend/shared/contracts/notification" "github.com/LoveLosita/smartflow/backend/shared/ports" "github.com/gin-gonic/gin" diff --git a/backend/gateway/api/schedule.go b/backend/gateway/api/schedule.go index 4bfb452..6c969b1 100644 --- a/backend/gateway/api/schedule.go +++ b/backend/gateway/api/schedule.go @@ -6,7 +6,7 @@ import ( "strconv" "time" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/gateway/shared/respond" schedulecontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/schedule" "github.com/LoveLosita/smartflow/backend/shared/ports" "github.com/gin-gonic/gin" diff --git a/backend/gateway/api/task-class.go b/backend/gateway/api/task-class.go index 60ae9f4..264b2e5 100644 --- a/backend/gateway/api/task-class.go +++ b/backend/gateway/api/task-class.go @@ -6,7 +6,7 @@ import ( "strconv" "time" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/gateway/shared/respond" taskclasscontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/taskclass" "github.com/LoveLosita/smartflow/backend/shared/ports" "github.com/gin-gonic/gin" diff --git a/backend/gateway/api/task.go b/backend/gateway/api/task.go index 46ba20b..e0728e8 100644 --- a/backend/gateway/api/task.go +++ b/backend/gateway/api/task.go @@ -5,7 +5,7 @@ import ( "net/http" "time" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/gateway/shared/respond" taskcontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/task" "github.com/LoveLosita/smartflow/backend/shared/ports" "github.com/gin-gonic/gin" diff --git a/backend/gateway/api/userauth/handler.go b/backend/gateway/api/userauth/handler.go index d662b7a..c799637 100644 --- a/backend/gateway/api/userauth/handler.go +++ b/backend/gateway/api/userauth/handler.go @@ -7,7 +7,7 @@ import ( "time" gatewaymiddleware "github.com/LoveLosita/smartflow/backend/gateway/middleware" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/gateway/shared/respond" contracts "github.com/LoveLosita/smartflow/backend/shared/contracts/userauth" "github.com/LoveLosita/smartflow/backend/shared/ports" "github.com/gin-gonic/gin" diff --git a/backend/gateway/api/userauth/routes.go b/backend/gateway/api/userauth/routes.go index 4fda1e1..1cc3300 100644 --- a/backend/gateway/api/userauth/routes.go +++ b/backend/gateway/api/userauth/routes.go @@ -2,8 +2,8 @@ package userauthapi import ( gatewaymiddleware "github.com/LoveLosita/smartflow/backend/gateway/middleware" - rootmiddleware "github.com/LoveLosita/smartflow/backend/middleware" - "github.com/LoveLosita/smartflow/backend/pkg" + rootmiddleware "github.com/LoveLosita/smartflow/backend/gateway/middleware" + ratelimit "github.com/LoveLosita/smartflow/backend/shared/infra/ratelimit" "github.com/LoveLosita/smartflow/backend/shared/ports" "github.com/gin-gonic/gin" ) @@ -13,7 +13,7 @@ import ( // 1. 只注册 /user 下的边缘路由,不关心其它业务域路由; // 2. 登录、注册、刷新 token 只做请求转发;登出需要先经过 access token 边缘鉴权; // 3. 限流仍复用当前通用中间件,后续若 gateway 独立成包,可再整体下沉。 -func RegisterRoutes(apiGroup *gin.RouterGroup, handler *UserHandler, authClient ports.AccessTokenValidator, limiter *pkg.RateLimiter) { +func RegisterRoutes(apiGroup *gin.RouterGroup, handler *UserHandler, authClient ports.AccessTokenValidator, limiter *ratelimit.RateLimiter) { if apiGroup == nil || handler == nil { return } diff --git a/backend/middleware/idempotency.go b/backend/gateway/middleware/idempotency.go similarity index 95% rename from backend/middleware/idempotency.go rename to backend/gateway/middleware/idempotency.go index e8ec515..a93bfbe 100644 --- a/backend/middleware/idempotency.go +++ b/backend/gateway/middleware/idempotency.go @@ -8,8 +8,8 @@ import ( "net/http" "time" - "github.com/LoveLosita/smartflow/backend/dao" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/gateway/shared/respond" + "github.com/LoveLosita/smartflow/backend/services/runtime/dao" "github.com/gin-gonic/gin" ) diff --git a/backend/middleware/rate_limiter.go b/backend/gateway/middleware/rate_limiter.go similarity index 79% rename from backend/middleware/rate_limiter.go rename to backend/gateway/middleware/rate_limiter.go index dee868f..d2d599b 100644 --- a/backend/middleware/rate_limiter.go +++ b/backend/gateway/middleware/rate_limiter.go @@ -4,12 +4,12 @@ import ( "fmt" "log" - "github.com/LoveLosita/smartflow/backend/pkg" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/gateway/shared/respond" + ratelimit "github.com/LoveLosita/smartflow/backend/shared/infra/ratelimit" "github.com/gin-gonic/gin" ) -func RateLimitMiddleware(limiter *pkg.RateLimiter, capacity, rate int) gin.HandlerFunc { +func RateLimitMiddleware(limiter *ratelimit.RateLimiter, capacity, rate int) gin.HandlerFunc { return func(c *gin.Context) { // 1. 确定限流对象:可以用 UserID,也可以用 IP // 这里建议用 UserID,防止某个用户换 IP 疯狂刷 diff --git a/backend/gateway/middleware/respond_error.go b/backend/gateway/middleware/respond_error.go index 3d8c76c..1b6e573 100644 --- a/backend/gateway/middleware/respond_error.go +++ b/backend/gateway/middleware/respond_error.go @@ -4,7 +4,7 @@ import ( "errors" "net/http" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/gateway/shared/respond" "github.com/gin-gonic/gin" ) diff --git a/backend/gateway/middleware/token_handler.go b/backend/gateway/middleware/token_handler.go index 218f1bc..dd2f503 100644 --- a/backend/gateway/middleware/token_handler.go +++ b/backend/gateway/middleware/token_handler.go @@ -7,7 +7,7 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/gateway/shared/respond" "github.com/LoveLosita/smartflow/backend/shared/ports" "github.com/gin-gonic/gin" ) diff --git a/backend/gateway/middleware/token_quota_guard.go b/backend/gateway/middleware/token_quota_guard.go index 8293579..26e6bdf 100644 --- a/backend/gateway/middleware/token_quota_guard.go +++ b/backend/gateway/middleware/token_quota_guard.go @@ -6,7 +6,7 @@ import ( "net/http" "time" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/gateway/shared/respond" "github.com/LoveLosita/smartflow/backend/shared/ports" "github.com/gin-gonic/gin" ) diff --git a/backend/gateway/router/router.go b/backend/gateway/router/router.go index 270cb7d..538cf00 100644 --- a/backend/gateway/router/router.go +++ b/backend/gateway/router/router.go @@ -7,12 +7,12 @@ import ( "net/http" "time" - "github.com/LoveLosita/smartflow/backend/dao" "github.com/LoveLosita/smartflow/backend/gateway/api" userauthapi "github.com/LoveLosita/smartflow/backend/gateway/api/userauth" gatewaymiddleware "github.com/LoveLosita/smartflow/backend/gateway/middleware" - rootmiddleware "github.com/LoveLosita/smartflow/backend/middleware" - "github.com/LoveLosita/smartflow/backend/pkg" + rootmiddleware "github.com/LoveLosita/smartflow/backend/gateway/middleware" + "github.com/LoveLosita/smartflow/backend/services/runtime/dao" + ratelimit "github.com/LoveLosita/smartflow/backend/shared/infra/ratelimit" "github.com/LoveLosita/smartflow/backend/shared/ports" "github.com/gin-gonic/gin" "github.com/spf13/viper" @@ -55,7 +55,7 @@ func StartEngine(ctx context.Context, r *gin.Engine) { } } -func RegisterRouters(handlers *api.ApiHandlers, authClient ports.UserAuthClient, cache *dao.CacheDAO, limiter *pkg.RateLimiter) *gin.Engine { +func RegisterRouters(handlers *api.ApiHandlers, authClient ports.UserAuthClient, cache *dao.CacheDAO, limiter *ratelimit.RateLimiter) *gin.Engine { r := gin.Default() apiGroup := r.Group("/api/v1") { diff --git a/backend/gateway/shared/respond/respond.go b/backend/gateway/shared/respond/respond.go new file mode 100644 index 0000000..a6f907c --- /dev/null +++ b/backend/gateway/shared/respond/respond.go @@ -0,0 +1,78 @@ +// Package respond 承载 gateway HTTP 门面使用的响应适配入口。 +// +// 职责边界: +// 1. 只面向 gateway/api 与 gateway/middleware,统一 HTTP JSON 写回与错误响应常量的 import 位置; +// 2. 迁移期继续复用根 backend/respond 的响应码和错误语义,避免一次性改动服务层、RPC 层和 client 层; +// 3. 不承载任何服务私有业务逻辑,服务代码禁止反向 import backend/gateway/shared/respond。 +package respond + +import ( + "errors" + "net/http" + + rootrespond "github.com/LoveLosita/smartflow/backend/shared/respond" + "github.com/gin-gonic/gin" +) + +type ( + // Response 是 gateway 透传给前端的项目响应码结构。 + Response = rootrespond.Response + + // FinalResponse 是带 data 字段的统一 HTTP 响应结构。 + FinalResponse = rootrespond.FinalResponse +) + +var ( + Ok = rootrespond.Ok + UserTasksEmpty = rootrespond.UserTasksEmpty + NoOngoingOrUpcomingSchedule = rootrespond.NoOngoingOrUpcomingSchedule + TaskAlreadyDeleted = rootrespond.TaskAlreadyDeleted + WrongParamType = rootrespond.WrongParamType + MissingParam = rootrespond.MissingParam + MissingIdempotencyKey = rootrespond.MissingIdempotencyKey + MissingToken = rootrespond.MissingToken + InvalidClaims = rootrespond.InvalidClaims + ErrUnauthorized = rootrespond.ErrUnauthorized + RequestIsProcessing = rootrespond.RequestIsProcessing + ScheduleConflict = rootrespond.ScheduleConflict + TooManyRequests = rootrespond.TooManyRequests + TokenUsageExceedsLimit = rootrespond.TokenUsageExceedsLimit + ConversationNotFound = rootrespond.ConversationNotFound + MissingConversationID = rootrespond.MissingConversationID +) + +// RespWithData 为 gateway HTTP 门面生成带 data 的统一响应体。 +// +// 职责边界: +// 1. 只做响应结构组装,不决定 HTTP 状态码; +// 2. 响应码来源仍是根 respond,保证迁移前后前端协议不变。 +func RespWithData(response Response, data interface{}) FinalResponse { + return rootrespond.RespWithData(response, data) +} + +// DealWithError 将项目 error 映射为 HTTP JSON 响应。 +// +// 职责边界: +// 1. 只在 gateway HTTP 层写响应; +// 2. 业务错误语义仍由根 respond 统一维护; +// 3. nil error 直接忽略,保持旧 DealWithError 的降级语义。 +func DealWithError(c *gin.Context, err error) { + if err == nil { + return + } + var resp Response + if errors.Is(err, UserTasksEmpty) || errors.Is(err, NoOngoingOrUpcomingSchedule) || errors.Is(err, TaskAlreadyDeleted) { + c.JSON(http.StatusOK, err) + return + } + if errors.As(err, &resp) { + c.JSON(resp.HTTPStatus(), resp) + return + } + c.JSON(http.StatusInternalServerError, InternalError(err)) +} + +// InternalError 生成 500 类响应体,供 gateway 依赖缺失等边缘错误使用。 +func InternalError(err error) Response { + return rootrespond.InternalError(err) +} diff --git a/backend/service/course.go b/backend/service/course.go deleted file mode 100644 index 9dd87d6..0000000 --- a/backend/service/course.go +++ /dev/null @@ -1,164 +0,0 @@ -package service - -import ( - "context" - "strings" - - "github.com/LoveLosita/smartflow/backend/conv" - "github.com/LoveLosita/smartflow/backend/dao" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" - llmservice "github.com/LoveLosita/smartflow/backend/services/llm" -) - -type CourseService struct { - // 伸出手:准备接住 DAO - courseDAO *dao.CourseDAO - scheduleDAO *dao.ScheduleDAO - courseImageResponsesClient *llmservice.ArkResponsesClient - courseImageConfig CourseImageParseConfig - courseImageModel string -} - -// NewCourseService 创建 CourseService 实例 -func NewCourseService( - courseDAO *dao.CourseDAO, - scheduleDAO *dao.ScheduleDAO, - courseImageResponsesClient *llmservice.ArkResponsesClient, - courseImageConfig CourseImageParseConfig, - courseImageModel string, -) *CourseService { - return &CourseService{ - courseDAO: courseDAO, - scheduleDAO: scheduleDAO, - courseImageResponsesClient: courseImageResponsesClient, - courseImageConfig: courseImageConfig, - courseImageModel: strings.TrimSpace(courseImageModel), - } -} - -func isUniqueViolation(err error) bool { - if err == nil { - return false - } - // 兼容常见 MySQL / PostgreSQL / SQLite 的报错关键字 - // 也可以进一步精确到你的索引名 idx_user_slot_atomic - msg := strings.ToLower(err.Error()) - if strings.Contains(msg, "duplicate entry") || - strings.Contains(msg, "unique constraint") || - strings.Contains(msg, "unique violation") || - strings.Contains(msg, "duplicate key") { - return true - } - return false -} - -func CheckSingleCourse(req model.UserCheckCourseRequest) bool { - for _, arrangement := range req.Arrangements { - if arrangement.StartWeek > arrangement.EndWeek || - arrangement.DayOfWeek < 1 || arrangement.DayOfWeek > 7 || - arrangement.StartSection < 1 || arrangement.EndSection < arrangement.StartSection || - arrangement.EndSection > 12 || arrangement.StartWeek < 1 || arrangement.EndWeek > 24 { - return false - } - } - return true -} - -// AddUserCourses 添加用户课程表 -func (ss *CourseService) AddUserCourses(ctx context.Context, req model.UserImportCoursesRequest, userID int) ([]model.ScheduleConflictDetail, error) { - //1.先校验参数是否正确 - for _, course := range req.Courses { - result := CheckSingleCourse(course) - if !result { - return nil, respond.WrongCourseInfo - } - } - //2.将前端传来的课程信息转换为 Schedule 和 ScheduleEvent 切片 - var finalSchedules []model.Schedule - var finalScheduleEvents []model.ScheduleEvent - var pos []int - for _, course := range req.Courses { - // 避免取 range 迭代变量字段地址导致指针复用问题 - location := course.Location - for _, arrangement := range course.Arrangements { - weekType := arrangement.WeekType - for week := arrangement.StartWeek; week <= arrangement.EndWeek; week++ { - if weekType == "odd" && week%2 == 0 { - continue - } - if weekType == "even" && week%2 != 0 { - continue - } - //2.转换为 Schedule_event 切片 - st, ed, err := conv.RelativeTimeToRealTime(week, arrangement.DayOfWeek, arrangement.StartSection, arrangement.EndSection) - if err != nil { - return nil, err - } - scheduleEvent := model.ScheduleEvent{ - UserID: userID, - Name: course.CourseName, - Location: &location, - Type: "course", - RelID: nil, - CanBeEmbedded: course.IsAllowTasks, - StartTime: st, - EndTime: ed, - } - finalScheduleEvents = append(finalScheduleEvents, scheduleEvent) - //3.转换为 Schedule 切片 - for section := arrangement.StartSection; section <= arrangement.EndSection; section++ { - schedule := model.Schedule{ - Week: week, - DayOfWeek: arrangement.DayOfWeek, - Section: section, - Status: "normal", - UserID: userID, - EventID: 0, - } - finalSchedules = append(finalSchedules, schedule) - pos = append(pos, len(finalScheduleEvents)-1) - } - } - } - } - //3.先检测是否重复插入了课程(同一周、同一天、同一节已有课程) - exists, err := ss.scheduleDAO.CheckScheduleConflict(ctx, finalSchedules) - if err != nil { - return nil, err - } - if exists { - return nil, respond.InsertCourseTwice - } - //4.再检查是否和某些非课程的日程冲突(同一周、同一天、同一节已有非课程日程),并给出具体的冲突信息 - conflicts, err := ss.scheduleDAO.GetNonCourseScheduleConflicts(ctx, finalSchedules) - if err != nil { - return nil, err - } - if len(conflicts) > 0 { - ret := conv.SchedulesToScheduleConflictDetail(conflicts) - return ret, respond.ScheduleConflict - } - //5.事务:插入两个表要么都成功,要么都回滚 - err = ss.courseDAO.Transaction(func(txDAO *dao.CourseDAO) error { - ids, err := txDAO.AddUserCoursesIntoScheduleEvents(ctx, finalScheduleEvents) - if err != nil { - return err - } - // 将生成的 ScheduleEvent ID 赋值给对应的 Schedule 的 EventID 字段 - for i := range finalSchedules { - finalSchedules[i].EventID = ids[pos[i]] - } - if err := txDAO.AddUserCoursesIntoSchedule(ctx, finalSchedules); err != nil { - return err - } - return nil - }) - if err != nil { - if isUniqueViolation(err) { - return nil, respond.InsertCourseTwice - } - return nil, err - } - return nil, nil -} diff --git a/backend/service/course_parse.go b/backend/service/course_parse.go deleted file mode 100644 index 0ca5ae4..0000000 --- a/backend/service/course_parse.go +++ /dev/null @@ -1,295 +0,0 @@ -package service - -import ( - "errors" - "fmt" - "net/http" - "strings" - - "github.com/LoveLosita/smartflow/backend/model" -) - -const ( - defaultCourseImageMaxBytes = 5 * 1024 * 1024 - defaultCourseImageMaxTokens = 16384 - maxCourseImageDraftRows = 256 - courseImageParseTemperature = 0.1 -) - -var ( - ErrCourseImageParserUnavailable = errors.New("course image parser is not configured") - ErrCourseImageTooLarge = errors.New("course image is too large") - ErrCourseImageUnsupportedMIME = errors.New("course image mime type is not supported") - ErrCourseImageEmpty = errors.New("course image is empty") -) - -type CourseImageParseConfig struct { - MaxImageBytes int64 - MaxTokens int -} - -func NewCourseImageParseConfig(maxImageBytes int64, maxTokens int) CourseImageParseConfig { - if maxImageBytes <= 0 { - maxImageBytes = defaultCourseImageMaxBytes - } - if maxTokens <= 0 { - maxTokens = defaultCourseImageMaxTokens - } - return CourseImageParseConfig{ - MaxImageBytes: maxImageBytes, - MaxTokens: maxTokens, - } -} - -func normalizeCourseImageParseRequest(req model.CourseImageParseRequest, cfg CourseImageParseConfig) (*model.CourseImageParseRequest, error) { - req.Filename = strings.TrimSpace(req.Filename) - req.MIMEType = strings.TrimSpace(strings.ToLower(req.MIMEType)) - if len(req.ImageBytes) == 0 { - return nil, ErrCourseImageEmpty - } - if int64(len(req.ImageBytes)) > cfg.MaxImageBytes { - return nil, ErrCourseImageTooLarge - } - - detected := strings.ToLower(strings.TrimSpace(http.DetectContentType(req.ImageBytes))) - if req.MIMEType == "" || req.MIMEType == "application/octet-stream" { - req.MIMEType = detected - } - if !isSupportedCourseImageMIME(req.MIMEType) { - if isSupportedCourseImageMIME(detected) { - req.MIMEType = detected - } else { - return nil, ErrCourseImageUnsupportedMIME - } - } - - if req.Filename == "" { - req.Filename = "course-table" - } - return &req, nil -} - -func isSupportedCourseImageMIME(mimeType string) bool { - switch strings.TrimSpace(strings.ToLower(mimeType)) { - case "image/jpeg", "image/png", "image/webp": - return true - default: - return false - } -} - -func normalizeCourseImageParseResponse(resp *model.CourseImageParseResponse) (*model.CourseImageParseResponse, error) { - if resp == nil { - return nil, errors.New("course image parse response is nil") - } - - resp.DraftStatus = model.CourseImageParseDraftStatus(strings.ToLower(strings.TrimSpace(string(resp.DraftStatus)))) - resp.Message = strings.TrimSpace(resp.Message) - resp.Warnings = normalizeWarningList(resp.Warnings) - resp.Rows = normalizeCourseImageParseRows(resp.Rows, &resp.Warnings) - - switch resp.DraftStatus { - case model.CourseImageParseDraftStatusSuccess: - if len(resp.Rows) == 0 { - return nil, errors.New("course image parse response has no rows in success status") - } - for idx := range resp.Rows { - if err := validateCourseImageParseRow(&resp.Rows[idx], true); err != nil { - return nil, fmt.Errorf("course image parse success row %d invalid: %w", idx+1, err) - } - } - case model.CourseImageParseDraftStatusPartial: - if len(resp.Rows) == 0 { - return nil, errors.New("course image parse response has no rows in partial status") - } - for idx := range resp.Rows { - if err := validateCourseImageParseRow(&resp.Rows[idx], false); err != nil { - return nil, fmt.Errorf("course image parse partial row %d invalid: %w", idx+1, err) - } - } - case model.CourseImageParseDraftStatusReject: - resp.Rows = make([]model.CourseImageParseRow, 0) - default: - return nil, fmt.Errorf("unsupported draft_status: %s", resp.DraftStatus) - } - - if resp.Message == "" { - resp.Message = defaultCourseImageParseMessage(resp.DraftStatus, len(resp.Rows)) - } - return resp, nil -} - -func normalizeCourseImageParseRows(rows []model.CourseImageParseRow, warnings *[]string) []model.CourseImageParseRow { - if len(rows) == 0 { - return make([]model.CourseImageParseRow, 0) - } - if len(rows) > maxCourseImageDraftRows { - rows = rows[:maxCourseImageDraftRows] - appendUniqueWarning(warnings, "识别结果行数超过上限,后端已截断为 256 行,请重点核对。") - } - - normalized := make([]model.CourseImageParseRow, 0, len(rows)) - for idx := range rows { - row := rows[idx] - row.RowID = strings.TrimSpace(row.RowID) - if row.RowID == "" { - row.RowID = fmt.Sprintf("row_%03d", idx+1) - } - row.CourseName = strings.TrimSpace(row.CourseName) - row.Location = strings.TrimSpace(row.Location) - row.WeekType = normalizeCourseImageWeekType(row.WeekType) - row.RawText = strings.TrimSpace(row.RawText) - row.RowWarnings = normalizeWarningList(row.RowWarnings) - normalizeOptionalPositiveInt(&row.StartWeek) - normalizeOptionalPositiveInt(&row.EndWeek) - normalizeOptionalPositiveInt(&row.DayOfWeek) - normalizeOptionalPositiveInt(&row.StartSection) - normalizeOptionalPositiveInt(&row.EndSection) - if row.Confidence < 0 { - row.Confidence = 0 - } - if row.Confidence > 1 { - row.Confidence = 1 - } - if row.CourseName == "" && - row.StartWeek == nil && - row.EndWeek == nil && - row.DayOfWeek == nil && - row.StartSection == nil && - row.EndSection == nil && - row.RawText == "" { - appendUniqueWarning(warnings, fmt.Sprintf("存在空白草稿行,后端已自动忽略:%s", row.RowID)) - continue - } - normalized = append(normalized, row) - } - - return normalized -} - -func validateCourseImageParseRow(row *model.CourseImageParseRow, strict bool) error { - if row == nil { - return errors.New("row is nil") - } - if strict && row.CourseName == "" { - return errors.New("course_name is empty") - } - if strict && row.WeekType == "" { - return errors.New("week_type is empty") - } - if row.WeekType != "" && row.WeekType != "all" && row.WeekType != "odd" && row.WeekType != "even" { - return fmt.Errorf("week_type is invalid: %s", row.WeekType) - } - - if err := validateOptionalCourseIntPair(row.StartWeek, row.EndWeek, 1, 24, "week", strict); err != nil { - return err - } - if err := validateOptionalCourseIntPair(row.StartSection, row.EndSection, 1, 12, "section", strict); err != nil { - return err - } - if strict && row.DayOfWeek == nil { - return errors.New("day_of_week is empty") - } - if row.DayOfWeek != nil && (*row.DayOfWeek < 1 || *row.DayOfWeek > 7) { - return fmt.Errorf("day_of_week out of range: %d", *row.DayOfWeek) - } - return nil -} - -func validateOptionalCourseIntPair(start *int, end *int, min int, max int, field string, strict bool) error { - if strict { - if start == nil || end == nil { - return fmt.Errorf("%s range is incomplete", field) - } - } - if start == nil && end == nil { - return nil - } - if start == nil || end == nil { - return fmt.Errorf("%s range is incomplete", field) - } - if *start < min || *start > max { - return fmt.Errorf("%s start out of range: %d", field, *start) - } - if *end < min || *end > max { - return fmt.Errorf("%s end out of range: %d", field, *end) - } - if *start > *end { - return fmt.Errorf("%s start is greater than end: %d > %d", field, *start, *end) - } - return nil -} - -func normalizeOptionalPositiveInt(target **int) { - if target == nil || *target == nil { - return - } - if **target <= 0 { - *target = nil - } -} - -func normalizeCourseImageWeekType(raw string) string { - normalized := strings.ToLower(strings.TrimSpace(raw)) - switch normalized { - case "", "unknown", "null": - return "" - case "all", "every", "weekly", "each week", "每周", "全周", "全部": - return "all" - case "odd", "single", "单", "单周": - return "odd" - case "even", "double", "双", "双周": - return "even" - default: - return normalized - } -} - -func normalizeWarningList(items []string) []string { - if len(items) == 0 { - return make([]string, 0) - } - seen := make(map[string]struct{}, len(items)) - result := make([]string, 0, len(items)) - for _, item := range items { - trimmed := strings.TrimSpace(item) - if trimmed == "" { - continue - } - if _, ok := seen[trimmed]; ok { - continue - } - seen[trimmed] = struct{}{} - result = append(result, trimmed) - } - return result -} - -func appendUniqueWarning(target *[]string, warningText string) { - if target == nil { - return - } - trimmed := strings.TrimSpace(warningText) - if trimmed == "" { - return - } - for _, existing := range *target { - if strings.TrimSpace(existing) == trimmed { - return - } - } - *target = append(*target, trimmed) -} - -func defaultCourseImageParseMessage(status model.CourseImageParseDraftStatus, rowCount int) string { - switch status { - case model.CourseImageParseDraftStatusSuccess: - return fmt.Sprintf("已识别 %d 条课程安排,请重点核对周次、星期和节次。", rowCount) - case model.CourseImageParseDraftStatusPartial: - return fmt.Sprintf("已识别 %d 条课程安排,但仍存在不确定字段,请结合 warning 逐项核对。", rowCount) - case model.CourseImageParseDraftStatusReject: - return "图片信息不足,建议重新上传完整、清晰、包含表头和节次栏的总课表截图。" - default: - return "课程表图片识别已完成,请人工核对后再导入。" - } -} diff --git a/backend/service/course_parse_ark.go b/backend/service/course_parse_ark.go deleted file mode 100644 index 45ea9c7..0000000 --- a/backend/service/course_parse_ark.go +++ /dev/null @@ -1,228 +0,0 @@ -package service - -import ( - "context" - "encoding/base64" - "fmt" - "log" - "strings" - "time" - - "github.com/LoveLosita/smartflow/backend/model" - llmservice "github.com/LoveLosita/smartflow/backend/services/llm" -) - -// ParseCourseTableImage 使用 Ark SDK Responses 解析课程表图片。 -func (ss *CourseService) ParseCourseTableImage(ctx context.Context, req model.CourseImageParseRequest) (*model.CourseImageParseResponse, error) { - if ss == nil || ss.courseImageResponsesClient == nil { - modelName := "" - if ss != nil { - modelName = ss.courseImageModel - } - log.Printf( - "[COURSE_PARSE][SERVICE] parser unavailable model_name=%q filename=%q mime=%q bytes=%d", - modelName, - req.Filename, - req.MIMEType, - len(req.ImageBytes), - ) - return nil, ErrCourseImageParserUnavailable - } - - normalizedReq, err := normalizeCourseImageParseRequest(req, ss.courseImageConfig) - if err != nil { - log.Printf( - "[COURSE_PARSE][SERVICE] request normalization failed filename=%q mime=%q bytes=%d err=%v", - req.Filename, - req.MIMEType, - len(req.ImageBytes), - err, - ) - return nil, err - } - - log.Printf( - "[COURSE_PARSE][SERVICE] normalized request model_name=%q filename=%q mime=%q bytes=%d max_bytes=%d", - ss.courseImageModel, - normalizedReq.Filename, - normalizedReq.MIMEType, - len(normalizedReq.ImageBytes), - ss.courseImageConfig.MaxImageBytes, - ) - - messages, base64Chars, promptChars := buildCourseImageParseResponsesMessages(normalizedReq) - startAt := time.Now() - log.Printf( - "[COURSE_PARSE][SERVICE] model invoke start model_name=%q filename=%q mime=%q message_count=%d base64_chars=%d prompt_chars=%d payload_chars_estimate=%d thinking=%s temperature=%.2f max_output_tokens=%d text_format=%s", - ss.courseImageModel, - normalizedReq.Filename, - normalizedReq.MIMEType, - len(messages), - base64Chars, - promptChars, - base64Chars+promptChars+len(strings.TrimSpace(courseImageParseSystemPrompt)), - llmservice.ThinkingModeDisabled, - courseImageParseTemperature, - ss.courseImageConfig.MaxTokens, - "json_object", - ) - - // 1. 课程表图片识别输出体量大,显式透传 max_output_tokens,避免被默认值截断。 - // 2. text_format 固定为 json_object,降低输出混入解释文本导致解析失败的概率。 - // 3. thinking 显式关闭,优先保证课程导入链路稳定性。 - draft, rawResult, err := llmservice.GenerateArkResponsesJSON[model.CourseImageParseResponse](ctx, ss.courseImageResponsesClient, messages, llmservice.ArkResponsesOptions{ - Temperature: courseImageParseTemperature, - MaxOutputTokens: ss.courseImageConfig.MaxTokens, - Thinking: llmservice.ThinkingModeDisabled, - TextFormat: "json_object", - }) - if err != nil { - rawText := "" - rawChars := 0 - status := "" - incompleteReason := "" - errorCode := "" - errorMessage := "" - inputTokens := int64(0) - outputTokens := int64(0) - totalTokens := int64(0) - if rawResult != nil { - rawText = strings.TrimSpace(rawResult.Text) - rawChars = len(rawText) - status = strings.TrimSpace(rawResult.Status) - incompleteReason = strings.TrimSpace(rawResult.IncompleteReason) - errorCode = strings.TrimSpace(rawResult.ErrorCode) - errorMessage = strings.TrimSpace(rawResult.ErrorMessage) - if rawResult.Usage != nil { - inputTokens = rawResult.Usage.InputTokens - outputTokens = rawResult.Usage.OutputTokens - totalTokens = rawResult.Usage.TotalTokens - } - } - log.Printf( - "[COURSE_PARSE][SERVICE] model invoke failed model_name=%q filename=%q mime=%q cost_ms=%d err=%v status=%q incomplete_reason=%q error_code=%q error_message=%q input_tokens=%d output_tokens=%d total_tokens=%d raw_chars=%d raw_full=\n%s", - ss.courseImageModel, - normalizedReq.Filename, - normalizedReq.MIMEType, - time.Since(startAt).Milliseconds(), - err, - status, - incompleteReason, - errorCode, - errorMessage, - inputTokens, - outputTokens, - totalTokens, - rawChars, - rawText, - ) - if isCourseImageOutputTruncated(rawResult) { - return nil, fmt.Errorf( - "课程表识别输出疑似被 max_output_tokens 截断:status=%s incomplete_reason=%s output_tokens=%d max_output_tokens=%d", - status, - incompleteReason, - outputTokens, - ss.courseImageConfig.MaxTokens, - ) - } - return nil, err - } - - rawText := "" - rawChars := 0 - status := "" - incompleteReason := "" - errorCode := "" - errorMessage := "" - inputTokens := int64(0) - outputTokens := int64(0) - totalTokens := int64(0) - if rawResult != nil { - rawText = strings.TrimSpace(rawResult.Text) - rawChars = len(rawText) - status = strings.TrimSpace(rawResult.Status) - incompleteReason = strings.TrimSpace(rawResult.IncompleteReason) - errorCode = strings.TrimSpace(rawResult.ErrorCode) - errorMessage = strings.TrimSpace(rawResult.ErrorMessage) - if rawResult.Usage != nil { - inputTokens = rawResult.Usage.InputTokens - outputTokens = rawResult.Usage.OutputTokens - totalTokens = rawResult.Usage.TotalTokens - } - } - log.Printf( - "[COURSE_PARSE][SERVICE] model invoke success model_name=%q filename=%q mime=%q cost_ms=%d status=%q incomplete_reason=%q error_code=%q error_message=%q input_tokens=%d output_tokens=%d total_tokens=%d raw_chars=%d raw_full=\n%s", - ss.courseImageModel, - normalizedReq.Filename, - normalizedReq.MIMEType, - time.Since(startAt).Milliseconds(), - status, - incompleteReason, - errorCode, - errorMessage, - inputTokens, - outputTokens, - totalTokens, - rawChars, - rawText, - ) - - normalizedDraft, err := normalizeCourseImageParseResponse(draft) - if err != nil { - log.Printf( - "[COURSE_PARSE][SERVICE] draft normalization failed model_name=%q filename=%q err=%v draft_status=%v row_count=%d", - ss.courseImageModel, - normalizedReq.Filename, - err, - draft.DraftStatus, - len(draft.Rows), - ) - return nil, err - } - - log.Printf( - "[COURSE_PARSE][SERVICE] draft normalization success model_name=%q filename=%q draft_status=%s rows=%d warnings=%d", - ss.courseImageModel, - normalizedReq.Filename, - normalizedDraft.DraftStatus, - len(normalizedDraft.Rows), - len(normalizedDraft.Warnings), - ) - - return normalizedDraft, nil -} - -func buildCourseImageParseResponsesMessages(req *model.CourseImageParseRequest) ([]llmservice.ArkResponsesMessage, int, int) { - userPrompt := fmt.Sprintf(courseImageParseUserPromptTemplate, req.Filename, req.MIMEType) - base64Data := base64.StdEncoding.EncodeToString(req.ImageBytes) - imageDataURL := fmt.Sprintf("data:%s;base64,%s", req.MIMEType, base64Data) - - messages := []llmservice.ArkResponsesMessage{ - { - Role: "system", - Text: strings.TrimSpace(courseImageParseSystemPrompt), - }, - { - Role: "user", - Text: strings.TrimSpace(userPrompt), - ImageURL: imageDataURL, - ImageDetail: "high", - }, - } - return messages, len(base64Data), len(strings.TrimSpace(userPrompt)) -} - -func isCourseImageOutputTruncated(rawResult *llmservice.ArkResponsesResult) bool { - if rawResult == nil { - return false - } - - reason := strings.ToLower(strings.TrimSpace(rawResult.IncompleteReason)) - if strings.Contains(reason, "max_output_tokens") || - strings.Contains(reason, "max_tokens") || - strings.Contains(reason, "length") { - return true - } - - return strings.EqualFold(strings.TrimSpace(rawResult.Status), "incomplete") && reason == "" -} diff --git a/backend/service/course_parse_prompt.go b/backend/service/course_parse_prompt.go deleted file mode 100644 index 4b7d779..0000000 --- a/backend/service/course_parse_prompt.go +++ /dev/null @@ -1,59 +0,0 @@ -package service - -const courseImageParseSystemPrompt = ` -你是 SmartFlow 的“总课表图片识别器”。你的唯一任务是读取用户上传的总课表图片,输出结构化 JSON 草稿,供前端人工核对后再导入系统。 - -必须遵守以下规则: -1. 只能输出一个 JSON 对象,禁止输出 Markdown、代码块、解释文字或额外前后缀。 -2. 顶层 JSON 结构必须是: -{ - "draft_status": "success | partial | reject", - "message": "字符串", - "warnings": ["字符串"], - "rows": [ - { - "row_id": "字符串,可为空", - "course_name": "字符串", - "location": "字符串", - "is_allow_tasks": false, - "start_week": 1, - "end_week": 16, - "day_of_week": 1, - "start_section": 1, - "end_section": 2, - "week_type": "all | odd | even", - "confidence": 0.92, - "raw_text": "原图中对应的近似文本", - "row_warnings": ["字符串"] - } - ] -} -3. rows 中一行只表达一个“课程安排片段”,不要把同一门课的多个时间段强行合并成一行。 -4. is_allow_tasks 无法从课表图片稳定识别时,一律返回 false,不要自行猜测。 -5. 若图片完整且大部分字段明确,可返回 success。 -6. 若图片可识别出部分行,但存在裁切、模糊、遮挡、单双周不清晰、节次/周次不确定等问题,返回 partial。 -7. 若图片严重不完整、分辨率过低、主体不是课表、无法可靠识别,返回 reject,同时 rows 置为空数组。 -8. 不要编造信息。看不清的数值字段请返回 null,并在 row_warnings 或 warnings 中明确说明原因。 -9. week_type 只能是: - - all:每周/未标注单双周 - - odd:单周 - - even:双周 -10. day_of_week 使用 1-7 表示周一到周日。 -11. start_section/end_section 使用原子节次编号,例如 1-2 节应输出 start_section=1, end_section=2。 -12. confidence 取 0 到 1 之间的小数;不确定时可以偏保守。 -13. 如果 rows 不为空,优先保证“周次、星期、节次”准确,地点可为空字符串。 -14. 当图片信息不足时,应明确拒绝或降级为 partial,而不是强行补全。 -15. 填写json中course_name时,严格按照截图的课程名称来。例如,有的课可能既有本体,又有实验课,这算是两门不同的课。 -16. 周信息是可能出现中断的,例如一节课可能是第1周和第6-12周,这是正常的课程安排,请不要擅自更改。 -` - -const courseImageParseUserPromptTemplate = ` -请识别这张总课表图片,并严格按照约定 JSON 输出草稿。 - -补充约束: -1. 文件名:%s -2. MIME 类型:%s -3. 这是一张供学生核对的“导入草稿”,不是最终真值;不确定就留空或写 warning。 -4. 如果图片右侧、底部、表头、周次栏、节次栏有缺失,请优先返回 partial 或 reject。 -5. rows 里尽量保留 raw_text,方便前端逐行回显核对。 -` diff --git a/backend/service/schedule.go b/backend/service/schedule.go deleted file mode 100644 index 7f30cd7..0000000 --- a/backend/service/schedule.go +++ /dev/null @@ -1,866 +0,0 @@ -package service - -import ( - "context" - "errors" - "log" - "sort" - "strings" - "time" - - "github.com/LoveLosita/smartflow/backend/conv" - "github.com/LoveLosita/smartflow/backend/dao" - "github.com/LoveLosita/smartflow/backend/logic" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" - "github.com/go-redis/redis/v8" -) - -type ScheduleService struct { - scheduleDAO *dao.ScheduleDAO - taskClassDAO *dao.TaskClassDAO - repoManager *dao.RepoManager // 统一管理多个 DAO 的事务 - cacheDAO *dao.CacheDAO // 需要在 ScheduleService 中使用缓存 -} - -func NewScheduleService(scheduleDAO *dao.ScheduleDAO, taskClassDAO *dao.TaskClassDAO, repoManager *dao.RepoManager, cacheDAO *dao.CacheDAO) *ScheduleService { - return &ScheduleService{ - scheduleDAO: scheduleDAO, - taskClassDAO: taskClassDAO, - repoManager: repoManager, - cacheDAO: cacheDAO, - } -} - -func (ss *ScheduleService) GetUserTodaySchedule(ctx context.Context, userID int) ([]model.UserTodaySchedule, error) { - //1.先尝试从缓存获取数据 - cachedResp, err := ss.cacheDAO.GetUserTodayScheduleFromCache(ctx, userID) - if err == nil { - // 缓存命中,直接返回 - return cachedResp, nil - } - // 如果是 redis.Nil 错误,说明缓存未命中,我们继续查库 - if !errors.Is(err, redis.Nil) { - return nil, err - } - //2.获取当前日期 - /*curTime := time.Now().Format("2006-01-02")*/ - curTime := "2026-03-02" //测试数据 - week, dayOfWeek, err := conv.RealDateToRelativeDate(curTime) - if err != nil { - return nil, err - } - //3.查询用户当天的日程安排 - schedules, err := ss.scheduleDAO.GetUserTodaySchedule(ctx, userID, week, dayOfWeek) //测试数据 - if err != nil { - return nil, err - } - //4.转换为前端需要的格式 - todaySchedules := conv.SchedulesToUserTodaySchedule(schedules) - //5.将查询结果存入缓存,设置过期时间为当天结束 - err = ss.cacheDAO.SetUserTodayScheduleToCache(ctx, userID, todaySchedules) - return todaySchedules, nil -} - -func (ss *ScheduleService) GetUserWeeklySchedule(ctx context.Context, userID, week int) (*model.UserWeekSchedule, error) { - //1.先检查 week 参数是否合法 - if week < 0 || week > 25 { - return nil, respond.WeekOutOfRange - } - //2.先看看缓存里有没有数据(如果有的话直接返回,没有的话继续查库) - cachedResp, err := ss.cacheDAO.GetUserWeeklyScheduleFromCache(ctx, userID, week) - if err == nil { - // 缓存命中,直接返回 - return cachedResp, nil - } - // 如果是 redis.Nil 错误,说明缓存未命中,我们继续查库 - if !errors.Is(err, redis.Nil) { - return nil, err - } - //3.查询用户每周的日程安排 - //如果没有传入 week 参数,则默认查询当前周的日程安排 - if week == 0 { - curTime := time.Now().Format("2006-01-02") - var err error - week, _, err = conv.RealDateToRelativeDate(curTime) - if err != nil { - return nil, err - } - } - schedules, err := ss.scheduleDAO.GetUserWeeklySchedule(ctx, userID, week) - if err != nil { - return nil, err - } - //3.转换为前端需要的格式 - weeklySchedule := conv.SchedulesToUserWeeklySchedule(schedules) - weeklySchedule.Week = week - //4.将查询结果存入缓存,设置过期时间为一周(或者根据实际情况调整) - err = ss.cacheDAO.SetUserWeeklyScheduleToCache(ctx, userID, weeklySchedule) - return weeklySchedule, nil -} - -func (ss *ScheduleService) DeleteScheduleEvent(ctx context.Context, requests []model.UserDeleteScheduleEvent, userID int) error { - err := ss.repoManager.Transaction(ctx, func(txM *dao.RepoManager) error { - for _, req := range requests { - //1.如果要删课程和嵌入的事件 - if req.DeleteEmbeddedTask && req.DeleteCourse { - //通过schedule表的embedded_task_id字段找到对应的task_id - taskID, err := txM.Schedule.GetScheduleEmbeddedTaskID(ctx, req.ID) - if err != nil { - return err - } - //再将task_items表中对应的embedded_time字段设置为null - if taskID != 0 { - err = txM.TaskClass.DeleteTaskClassItemEmbeddedTime(ctx, taskID) - if err != nil { - return err - } - } - //再删除课程事件和嵌入的事件(通过级联删除实现) - err = txM.Schedule.DeleteScheduleEventAndSchedule(ctx, req.ID, userID) - if err != nil { - return err - } - continue - } - //2.只删课程/事件 - if req.DeleteCourse { - //2.1.检查课程是否有嵌入的任务事件 - exists, err := txM.Schedule.IfScheduleEventIDExists(ctx, req.ID) - if err != nil { - return err - } - if !exists { - return respond.WrongScheduleEventID - } - embeddedTaskID, err := txM.Schedule.GetScheduleEmbeddedTaskID(ctx, req.ID) - if err != nil { - return err - } - //2.2.如果有,则需另外为其创建新的scheduleEvent(type=task) - //课程事件先删除后再创建任务事件 - if embeddedTaskID != 0 { - //2.2.1.先通过id取出taskClassItem详情 - taskClassItem, err := txM.TaskClass.GetTaskClassItemByID(ctx, embeddedTaskID) - if err != nil { - return err - } - //下方开启事务,删除课程事件并创建新的任务事件 - //2.2.2.删除课程事件 - txErr := txM.Schedule.DeleteScheduleEventAndSchedule(ctx, req.ID, userID) - if txErr != nil { - return txErr - } - //2.2.3.再复用代码创建新的scheduleEvent,下方代码改编自AddTaskClassItemIntoSchedule函数 - //直接构造Schedule模型 - sections := make([]int, 0, taskClassItem.EmbeddedTime.SectionTo-taskClassItem.EmbeddedTime.SectionFrom+1) - // 这里的 req 主要是为了传递 Week 和 DayOfWeek,其他字段不需要了 - schedules, scheduleEvent, err := conv.UserInsertTaskItemRequestToModel( - &model.UserInsertTaskClassItemToScheduleRequest{ - Week: taskClassItem.EmbeddedTime.Week, - DayOfWeek: taskClassItem.EmbeddedTime.DayOfWeek}, - taskClassItem, nil, userID, taskClassItem.EmbeddedTime.SectionFrom, taskClassItem.EmbeddedTime.SectionTo) - if err != nil { - return err - } - //将节次区间转换为节次切片,方便后续检查冲突 - for section := taskClassItem.EmbeddedTime.SectionFrom; section <= taskClassItem.EmbeddedTime.SectionTo; section++ { - sections = append(sections, section) - } - //单用户不存在删除时这个格子被占用的情况,所以不检查冲突了 - /*//4.1 统一检查冲突(避免逐条查库) - conflict, err := ss.scheduleDAO.HasUserScheduleConflict(ctx, userID, req.Week, req.DayOfWeek, sections) - if err != nil { - return err - } - if conflict { - return respond.ScheduleConflict - }*/ - // 5. 写入数据库(通过 RepoManager 统一管理事务) - // 这里的 sv.daoManager 是你在初始化 Service 时注入的全局 RepoManager 实例 - // 5.1 使用事务中的 ScheduleRepo 插入 Event - eventID, txErr := txM.Schedule.AddScheduleEvent(scheduleEvent) - if txErr != nil { - return txErr // 触发回滚 - } - // 5.2 关联 ID(纯内存操作,无需 tx) - for i := range schedules { - schedules[i].EventID = eventID - } - // 5.3 使用事务中的 ScheduleRepo 批量插入原子槽位 - if _, txErr = txM.Schedule.AddSchedules(schedules); txErr != nil { - return txErr // 触发回滚 - } - // 5.4 使用事务中的 TaskRepo 更新任务状态 - if txErr = txM.TaskClass.UpdateTaskClassItemEmbeddedTime(ctx, embeddedTaskID, taskClassItem.EmbeddedTime); txErr != nil { - return txErr // 触发回滚 - } - continue - } - //2.3.如果没有嵌入的事件,就直接删除课程事件 - err = txM.Schedule.DeleteScheduleEventAndSchedule(ctx, req.ID, userID) - if err != nil { - return err - } - //先通过rel_id找到对应的task_id - taskID, txErr := txM.Schedule.GetRelIDByScheduleEventID(ctx, req.ID) - if txErr != nil { - return err - } - //2.4.如果是任务块,转而去清除task_items表中的嵌入时间 - if taskID != 0 { - //再将task_items表中对应的embedded_time字段设置为null - txErr = txM.TaskClass.DeleteTaskClassItemEmbeddedTime(ctx, taskID) - if txErr != nil { - return txErr - } - } - continue - } - //3.只删嵌入的事件 - if req.DeleteEmbeddedTask { - //下面先设置schedule表的embedded_task_id字段为null,再设置task_items表的embedded_time字段为null,实现删除嵌入事件的效果 - //3.1.先将schedule表的embedded_task_id字段设置为null - taskID, txErr := txM.Schedule.SetScheduleEmbeddedTaskIDToNull(ctx, req.ID) - if txErr != nil { - return txErr - } - //3.2.再将task_items表的embedded_time字段设置为null - txErr = txM.TaskClass.DeleteTaskClassItemEmbeddedTime(ctx, taskID) - if txErr != nil { - return txErr - } - continue - } - } - return nil - }) - if err != nil { - return err - } - return nil -} - -func (ss *ScheduleService) GetUserRecentCompletedSchedules(ctx context.Context, userID, index, limit int) (*model.UserRecentCompletedScheduleResponse, error) { - //1.先查缓存 - cachedResp, err := ss.cacheDAO.GetUserRecentCompletedSchedulesFromCache(ctx, userID, index, limit) - if err == nil { - // 缓存命中,直接返回 - return cachedResp, nil - } - // 如果是 redis.Nil 错误,说明缓存未命中,我们继续查库 - if !errors.Is(err, redis.Nil) { - return nil, err - } - //2.查询用户最近完成的日程安排 - //获取现在的时间 - /*nowTime := time.Now()*/ - nowTime := time.Date(2026, 6, 30, 12, 0, 0, 0, time.Local) //测试数据 - schedules, err := ss.scheduleDAO.GetUserRecentCompletedSchedules(ctx, nowTime, userID, index, limit) - if err != nil { - return nil, err - } - //3.转换为前端需要的格式 - result := conv.SchedulesToRecentCompletedSchedules(schedules) - //4.将查询结果存入缓存,设置过期时间为30分钟(根据实际情况调整) - err = ss.cacheDAO.SetUserRecentCompletedSchedulesToCache(ctx, userID, index, limit, result) - if err != nil { - return nil, err - } - return result, nil -} - -func (ss *ScheduleService) GetUserOngoingSchedule(ctx context.Context, userID int) (*model.OngoingSchedule, error) { - //1.先查缓存 - cachedResp, err := ss.cacheDAO.GetUserOngoingScheduleFromCache(ctx, userID) - if err == nil && cachedResp == nil { - // 之前缓存过没有正在进行的日程,直接返回 nil - return nil, respond.NoOngoingOrUpcomingSchedule - } - if err == nil { - // 缓存命中,直接返回 - return cachedResp, nil - } - // 如果是 redis.Nil 错误,说明缓存未命中,我们继续查库 - if !errors.Is(err, redis.Nil) { - return nil, err - } - //2.查询用户正在进行的日程安排 - /*nowTime := time.Now()*/ - nowTime := time.Date(2026, 6, 30, 18, 50, 0, 0, time.Local) //测试数据 - schedules, err := ss.scheduleDAO.GetUserOngoingSchedule(ctx, userID, nowTime) - if err != nil { - return nil, err - } - //3.转换为前端需要的格式 - result := conv.SchedulesToUserOngoingSchedule(schedules) - if result != nil { - if result.StartTime.After(nowTime) { - result.TimeStatus = "upcoming" - } else { - result.TimeStatus = "ongoing" - } - } - //4.将查询结果存入缓存,设置过期时间直到此任务结束(根据实际情况调整) - err = ss.cacheDAO.SetUserOngoingScheduleToCache(ctx, userID, result) - if err != nil { - return nil, err - } - if result == nil { - // 没有正在进行或即将开始的日程,返回特定错误 - return nil, respond.NoOngoingOrUpcomingSchedule - } - return result, nil -} - -func (ss *ScheduleService) RevocateUserTaskClassItem(ctx context.Context, userID, eventID int) error { - //1.先查库,看看这个event是任务事件还是课程事件,以及判断它是否属于用户 - eventType, err := ss.scheduleDAO.GetScheduleTypeByEventID(ctx, eventID, userID) - if err != nil { - return err - } - //2.根据查询结果进行不同的撤销操作 - if eventType == "course" { - //下面开启事务,撤销嵌入事件 - err := ss.repoManager.Transaction(ctx, func(txM *dao.RepoManager) error { - //下面先设置schedule表的embedded_task_id字段为null,再设置task_items表的embedded_time字段为null,实现删除嵌入事件的效果 - //3.1.先将schedule表的embedded_task_id字段设置为null - taskID, txErr := txM.Schedule.SetScheduleEmbeddedTaskIDToNull(ctx, eventID) - if txErr != nil { - return txErr - } - //3.2.再将task_items表的embedded_time字段设置为null - txErr = txM.TaskClass.DeleteTaskClassItemEmbeddedTime(ctx, taskID) - if txErr != nil { - return txErr - } - //3.3.最后设置task_items表的status字段为已撤销 - txErr = txM.Schedule.RevocateSchedulesByEventID(ctx, eventID) - if txErr != nil { - return txErr - } - return nil - }) - if err != nil { - return err - } - } else if eventType == "task" { - //下面开启事务,撤销任务事件 - err := ss.repoManager.Transaction(ctx, func(txM *dao.RepoManager) error { - //先通过rel_id找到对应的task_id - taskID, txErr := txM.Schedule.GetRelIDByScheduleEventID(ctx, eventID) - if txErr != nil { - return err - } - //再将task_items表中对应的embedded_time字段设置为null - txErr = txM.TaskClass.DeleteTaskClassItemEmbeddedTime(ctx, taskID) - if txErr != nil { - return txErr - } - //最后将其从日程表中删除(通过级联删除实现) - err = txM.Schedule.DeleteScheduleEventAndSchedule(ctx, eventID, userID) - if err != nil { - return err - } - return nil - }) - if err != nil { - return err - } - } else { - log.Println("ScheduleService.RevocateUserTaskClassItem: eventType is neither embedded_task nor task, something must be wrong") - } - return nil -} - -func (ss *ScheduleService) SmartPlanning(ctx context.Context, userID, taskClassID int) ([]model.UserWeekSchedule, error) { - //1.通过任务类id获取任务类详情 - taskClass, err := ss.taskClassDAO.GetCompleteTaskClassByID(ctx, taskClassID, userID) - if err != nil { - return nil, err - } - //2.校验任务类的参数是否合法 - if taskClass == nil { - return nil, respond.WrongTaskClassID - } - if *taskClass.Mode != "auto" { - return nil, respond.TaskClassModeNotAuto - } - //3.获取任务类安排的时间范围内的全部周数信息(左右边界不足一周的情况也要算作一周) - schedules, err := ss.scheduleDAO.GetUserSchedulesByTimeRange(ctx, userID, conv.CalculateFirstDayOfWeek(*taskClass.StartDate), conv.CalculateLastDayOfWeek(*taskClass.EndDate)) - if err != nil { - return nil, err - } - //4.将多个周的信息传入智能排课算法,获取推荐的时间安排(周+周内的天+节次) - result, err := logic.SmartPlanningMainLogic(schedules, taskClass) - if err != nil { - return nil, err - } - //5.将推荐的时间安排转换为前端需要的格式返回 - return result, nil -} - -// SmartPlanningRaw 执行粗排算法并同时返回展示结构和已分配的任务项。 -// -// 职责边界: -// 1. 与 SmartPlanning 共享完全相同的前置校验和粗排逻辑; -// 2. 额外返回 allocatedItems(每项的 EmbeddedTime 已由算法回填), -// 供 Agent 排程链路直接转换为 BatchApplyPlans 请求,无需再让模型"二次分配"。 -func (ss *ScheduleService) SmartPlanningRaw(ctx context.Context, userID, taskClassID int) ([]model.UserWeekSchedule, []model.TaskClassItem, error) { - // 1. 获取任务类详情。 - taskClass, err := ss.taskClassDAO.GetCompleteTaskClassByID(ctx, taskClassID, userID) - if err != nil { - return nil, nil, err - } - if taskClass == nil { - return nil, nil, respond.WrongTaskClassID - } - if *taskClass.Mode != "auto" { - return nil, nil, respond.TaskClassModeNotAuto - } - - // 2. 获取时间范围内的全部日程。 - schedules, err := ss.scheduleDAO.GetUserSchedulesByTimeRange(ctx, userID, conv.CalculateFirstDayOfWeek(*taskClass.StartDate), conv.CalculateLastDayOfWeek(*taskClass.EndDate)) - if err != nil { - return nil, nil, err - } - - // 3. 执行粗排算法,拿到已分配的 items(EmbeddedTime 已回填)。 - allocatedItems, err := logic.SmartPlanningRawItems(schedules, taskClass) - if err != nil { - return nil, nil, err - } - - // 4. 同时生成展示结构,供 SSE 阶段推送给前端预览。 - displayResult := conv.PlanningResultToUserWeekSchedules(schedules, allocatedItems) - return displayResult, allocatedItems, nil -} - -// SmartPlanningMulti 执行“多任务类智能粗排”,仅返回前端展示结构。 -// -// 职责边界: -// 1. 负责把多任务类请求收口到统一粗排流程; -// 2. 负责返回展示结构; -// 3. 不返回底层分配细节(由 SmartPlanningMultiRaw 提供)。 -func (ss *ScheduleService) SmartPlanningMulti(ctx context.Context, userID int, taskClassIDs []int) ([]model.UserWeekSchedule, error) { - displayResult, _, err := ss.SmartPlanningMultiRaw(ctx, userID, taskClassIDs) - if err != nil { - return nil, err - } - return displayResult, nil -} - -// SmartPlanningMultiRaw 执行“多任务类智能粗排”,同时返回展示结构和已分配任务项。 -// -// 职责边界: -// 1. 负责多任务类请求的完整前置处理(归一化/校验/排序/时间窗收敛); -// 2. 负责调用多任务类粗排主逻辑(共享资源池); -// 3. 只计算建议,不负责落库。 -func (ss *ScheduleService) SmartPlanningMultiRaw(ctx context.Context, userID int, taskClassIDs []int) ([]model.UserWeekSchedule, []model.TaskClassItem, error) { - // 1. 输入归一化。 - normalizedIDs := normalizeTaskClassIDsForMultiPlanning(taskClassIDs) - if len(normalizedIDs) == 0 { - return nil, nil, respond.WrongTaskClassID - } - - // 2. 批量读取完整任务类(含 Items)。 - taskClasses, err := ss.taskClassDAO.GetCompleteTaskClassesByIDs(ctx, userID, normalizedIDs) - if err != nil { - return nil, nil, err - } - - // 3. 校验任务类并计算全局时间窗。 - orderedTaskClasses, globalStartDate, globalEndDate, err := prepareTaskClassesForMultiPlanning(taskClasses, normalizedIDs) - if err != nil { - return nil, nil, err - } - - // 4. 拉取全局时间窗内的既有日程底板。 - schedules, err := ss.scheduleDAO.GetUserSchedulesByTimeRange( - ctx, - userID, - conv.CalculateFirstDayOfWeek(globalStartDate), - conv.CalculateLastDayOfWeek(globalEndDate), - ) - if err != nil { - return nil, nil, err - } - - // 5. 执行多任务类粗排(共享资源池 + 增量占位)。 - allocatedItems, err := logic.SmartPlanningRawItemsMulti(schedules, orderedTaskClasses) - if err != nil { - return nil, nil, err - } - - // 6. 转换前端展示结构。 - displayResult := conv.PlanningResultToUserWeekSchedules(schedules, allocatedItems) - return displayResult, allocatedItems, nil -} - -// ResolvePlanningWindowByTaskClasses 解析“多任务类排程窗口”的相对周/天边界。 -// -// 职责边界: -// 1. 只负责根据 task_class_ids 计算全局起止日期并转换成相对周/天; -// 2. 不执行粗排、不查询课表、不生成 HybridEntries; -// 3. 供 Agent 周级 Move 工具做硬边界校验,防止越界移动。 -// -// 返回语义: -// 1. startWeek/startDay:允许排程的起点(含); -// 2. endWeek/endDay:允许排程的终点(含); -// 3. error:任何校验或日期转换失败都返回错误。 -func (ss *ScheduleService) ResolvePlanningWindowByTaskClasses(ctx context.Context, userID int, taskClassIDs []int) (int, int, int, int, error) { - // 1. 输入归一化:过滤非法值并去重。 - normalizedIDs := normalizeTaskClassIDsForMultiPlanning(taskClassIDs) - if len(normalizedIDs) == 0 { - return 0, 0, 0, 0, respond.WrongTaskClassID - } - - // 2. 批量查询任务类并复用统一校验逻辑,拿到全局起止日期。 - taskClasses, err := ss.taskClassDAO.GetCompleteTaskClassesByIDs(ctx, userID, normalizedIDs) - if err != nil { - return 0, 0, 0, 0, err - } - _, globalStartDate, globalEndDate, err := prepareTaskClassesForMultiPlanning(taskClasses, normalizedIDs) - if err != nil { - return 0, 0, 0, 0, err - } - - // 3. 把绝对日期转换为“相对周/天”。 - // 3.1 这里统一复用 conv.RealDateToRelativeDate,确保和现有排程口径一致; - // 3.2 若日期超出学期配置范围,直接返回错误,避免错误边界进入工具层。 - startWeek, startDay, err := conv.RealDateToRelativeDate(globalStartDate.Format(conv.DateFormat)) - if err != nil { - return 0, 0, 0, 0, err - } - endWeek, endDay, err := conv.RealDateToRelativeDate(globalEndDate.Format(conv.DateFormat)) - if err != nil { - return 0, 0, 0, 0, err - } - if endWeek < startWeek || (endWeek == startWeek && endDay < startDay) { - return 0, 0, 0, 0, respond.InvalidDateRange - } - return startWeek, startDay, endWeek, endDay, nil -} - -// normalizeTaskClassIDsForMultiPlanning 归一化 task_class_ids(过滤非法值、去重并保序)。 -func normalizeTaskClassIDsForMultiPlanning(ids []int) []int { - if len(ids) == 0 { - return []int{} - } - normalized := make([]int, 0, len(ids)) - seen := make(map[int]struct{}, len(ids)) - for _, id := range ids { - if id <= 0 { - continue - } - if _, exists := seen[id]; exists { - continue - } - seen[id] = struct{}{} - normalized = append(normalized, id) - } - return normalized -} - -// prepareTaskClassesForMultiPlanning 把 DAO 结果转成可直接粗排的数据集。 -// -// 职责边界: -// 1. 校验每个任务类可参与自动排程; -// 2. 计算全局时间窗(最早开始 ~ 最晚结束); -// 3. 执行多任务类排序策略。 -func prepareTaskClassesForMultiPlanning(taskClasses []model.TaskClass, orderedIDs []int) ([]*model.TaskClass, time.Time, time.Time, error) { - if len(orderedIDs) == 0 { - return nil, time.Time{}, time.Time{}, respond.WrongTaskClassID - } - - classByID := make(map[int]*model.TaskClass, len(taskClasses)) - for i := range taskClasses { - tc := &taskClasses[i] - classByID[tc.ID] = tc - } - - ordered := make([]*model.TaskClass, 0, len(orderedIDs)) - var globalStart time.Time - var globalEnd time.Time - for idx, id := range orderedIDs { - taskClass, exists := classByID[id] - if !exists || taskClass == nil { - return nil, time.Time{}, time.Time{}, respond.WrongTaskClassID - } - if taskClass.Mode == nil || *taskClass.Mode != "auto" { - return nil, time.Time{}, time.Time{}, respond.TaskClassModeNotAuto - } - if taskClass.StartDate == nil || taskClass.EndDate == nil { - return nil, time.Time{}, time.Time{}, respond.InvalidDateRange - } - start := *taskClass.StartDate - end := *taskClass.EndDate - if end.Before(start) { - return nil, time.Time{}, time.Time{}, respond.InvalidDateRange - } - if idx == 0 || start.Before(globalStart) { - globalStart = start - } - if idx == 0 || end.After(globalEnd) { - globalEnd = end - } - ordered = append(ordered, taskClass) - } - - sortTaskClassesForMultiPlanning(ordered, orderedIDs) - return ordered, globalStart, globalEnd, nil -} - -// sortTaskClassesForMultiPlanning 执行稳定排序: -// 1. end_date 早优先; -// 2. rapid 优先于 steady; -// 3. 输入顺序兜底。 -func sortTaskClassesForMultiPlanning(taskClasses []*model.TaskClass, inputOrder []int) { - if len(taskClasses) <= 1 { - return - } - orderIndex := make(map[int]int, len(inputOrder)) - for idx, id := range inputOrder { - orderIndex[id] = idx - } - - sort.SliceStable(taskClasses, func(i, j int) bool { - left := taskClasses[i] - right := taskClasses[j] - if left == nil || right == nil { - return left != nil - } - if left.EndDate != nil && right.EndDate != nil && !left.EndDate.Equal(*right.EndDate) { - return left.EndDate.Before(*right.EndDate) - } - leftRapid := left.Strategy != nil && *left.Strategy == "rapid" - rightRapid := right.Strategy != nil && *right.Strategy == "rapid" - if leftRapid != rightRapid { - return leftRapid - } - leftOrder, leftOK := orderIndex[left.ID] - rightOrder, rightOK := orderIndex[right.ID] - if leftOK && rightOK && leftOrder != rightOrder { - return leftOrder < rightOrder - } - return left.ID < right.ID - }) -} - -// HybridScheduleWithPlan 构建“单任务类”混合日程(existing + suggested)。 -func (ss *ScheduleService) HybridScheduleWithPlan( - ctx context.Context, userID, taskClassID int, -) ([]model.HybridScheduleEntry, []model.TaskClassItem, error) { - // 1. 校验并读取任务类。 - taskClass, err := ss.taskClassDAO.GetCompleteTaskClassByID(ctx, taskClassID, userID) - if err != nil { - return nil, nil, err - } - if taskClass == nil { - return nil, nil, respond.WrongTaskClassID - } - if taskClass.Mode == nil || *taskClass.Mode != "auto" { - return nil, nil, respond.TaskClassModeNotAuto - } - if taskClass.StartDate == nil || taskClass.EndDate == nil { - return nil, nil, respond.InvalidDateRange - } - - // 2. 拉取时间窗内既有日程。 - schedules, err := ss.scheduleDAO.GetUserSchedulesByTimeRange( - ctx, userID, - conv.CalculateFirstDayOfWeek(*taskClass.StartDate), - conv.CalculateLastDayOfWeek(*taskClass.EndDate), - ) - if err != nil { - return nil, nil, err - } - - // 3. 执行粗排。 - allocatedItems, err := logic.SmartPlanningRawItems(schedules, taskClass) - if err != nil { - return nil, nil, err - } - - // 4. 统一合并。 - entries := buildHybridEntriesFromSchedulesAndAllocated(schedules, allocatedItems) - return entries, allocatedItems, nil -} - -// HybridScheduleWithPlanMulti 构建“多任务类”混合日程(existing + suggested)。 -func (ss *ScheduleService) HybridScheduleWithPlanMulti( - ctx context.Context, - userID int, - taskClassIDs []int, -) ([]model.HybridScheduleEntry, []model.TaskClassItem, error) { - // 1. 归一化任务类 ID。 - normalizedIDs := normalizeTaskClassIDsForMultiPlanning(taskClassIDs) - if len(normalizedIDs) == 0 { - return nil, nil, respond.WrongTaskClassID - } - - // 2. 拉取任务类并做校验/排序。 - taskClasses, err := ss.taskClassDAO.GetCompleteTaskClassesByIDs(ctx, userID, normalizedIDs) - if err != nil { - return nil, nil, err - } - orderedTaskClasses, globalStartDate, globalEndDate, err := prepareTaskClassesForMultiPlanning(taskClasses, normalizedIDs) - if err != nil { - return nil, nil, err - } - - // 3. 拉取全局时间窗内既有日程。 - schedules, err := ss.scheduleDAO.GetUserSchedulesByTimeRange( - ctx, - userID, - conv.CalculateFirstDayOfWeek(globalStartDate), - conv.CalculateLastDayOfWeek(globalEndDate), - ) - if err != nil { - return nil, nil, err - } - - // 4. 多任务类粗排。 - allocatedItems, err := logic.SmartPlanningRawItemsMulti(schedules, orderedTaskClasses) - if err != nil { - return nil, nil, err - } - - // 5. 统一合并。 - entries := buildHybridEntriesFromSchedulesAndAllocated(schedules, allocatedItems) - return entries, allocatedItems, nil -} - -// buildHybridEntriesFromSchedulesAndAllocated 合并 existing/suggested 条目。 -// -// 说明: -// 1. existing 按“事件 + 天 + 可嵌入语义 + 阻塞语义”分组,再按连续节次切块; -// 2. suggested 直接根据 allocatedItems 生成; -// 3. 仅做内存组装,不做数据库操作。 -func buildHybridEntriesFromSchedulesAndAllocated( - schedules []model.Schedule, - allocatedItems []model.TaskClassItem, -) []model.HybridScheduleEntry { - entries := make([]model.HybridScheduleEntry, 0, len(schedules)/2+len(allocatedItems)) - - type eventGroupKey struct { - EventID int - Week int - DayOfWeek int - CanBeEmbedded bool - BlockForSuggested bool - } - type eventGroup struct { - Key eventGroupKey - Name string - Type string - Sections []int - } - groupMap := make(map[eventGroupKey]*eventGroup) - - // 1. 先处理 existing。 - for _, s := range schedules { - name := "未知" - typ := "course" - canBeEmbedded := false - if s.Event != nil { - name = s.Event.Name - typ = s.Event.Type - canBeEmbedded = s.Event.CanBeEmbedded - } - - // 1.1 阻塞语义: - // 1.1.1 task 默认阻塞; - // 1.1.2 course 且不可嵌入时阻塞; - // 1.1.3 course 且可嵌入时,若当前原子格未被 embedded_task 占用,则不阻塞。 - blockForSuggested := true - if typ == "course" && canBeEmbedded && s.EmbeddedTaskID == nil { - blockForSuggested = false - } - - key := eventGroupKey{ - EventID: s.EventID, - Week: s.Week, - DayOfWeek: s.DayOfWeek, - CanBeEmbedded: canBeEmbedded, - BlockForSuggested: blockForSuggested, - } - group, ok := groupMap[key] - if !ok { - group = &eventGroup{ - Key: key, - Name: name, - Type: typ, - } - groupMap[key] = group - } - group.Sections = append(group.Sections, s.Section) - } - - for _, group := range groupMap { - if len(group.Sections) == 0 { - continue - } - sort.Ints(group.Sections) - - runStart := group.Sections[0] - prev := group.Sections[0] - flushRun := func(from, to int) { - entries = append(entries, model.HybridScheduleEntry{ - Week: group.Key.Week, - DayOfWeek: group.Key.DayOfWeek, - SectionFrom: from, - SectionTo: to, - Name: group.Name, - Type: group.Type, - Status: "existing", - EventID: group.Key.EventID, - CanBeEmbedded: group.Key.CanBeEmbedded, - BlockForSuggested: group.Key.BlockForSuggested, - }) - } - for i := 1; i < len(group.Sections); i++ { - cur := group.Sections[i] - if cur == prev+1 { - prev = cur - continue - } - flushRun(runStart, prev) - runStart = cur - prev = cur - } - flushRun(runStart, prev) - } - - // 2. 再处理 suggested。 - for _, item := range allocatedItems { - if item.EmbeddedTime == nil { - continue - } - name := "未命名任务" - if item.Content != nil && strings.TrimSpace(*item.Content) != "" { - name = strings.TrimSpace(*item.Content) - } - entries = append(entries, model.HybridScheduleEntry{ - Week: item.EmbeddedTime.Week, - DayOfWeek: item.EmbeddedTime.DayOfWeek, - SectionFrom: item.EmbeddedTime.SectionFrom, - SectionTo: item.EmbeddedTime.SectionTo, - Name: name, - Type: "task", - Status: "suggested", - TaskItemID: item.ID, - TaskClassID: derefInt(item.CategoryID), - BlockForSuggested: true, - }) - } - - return entries -} - -func derefInt(p *int) int { - if p == nil { - return 0 - } - return *p -} diff --git a/backend/service/task-class.go b/backend/service/task-class.go deleted file mode 100644 index 9d8ecec..0000000 --- a/backend/service/task-class.go +++ /dev/null @@ -1,552 +0,0 @@ -package service - -import ( - "context" - "errors" - "fmt" - "log" - "sort" - "time" - - "github.com/LoveLosita/smartflow/backend/conv" - "github.com/LoveLosita/smartflow/backend/dao" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" - "github.com/go-redis/redis/v8" - "gorm.io/gorm" -) - -type TaskClassService struct { - // 这里可以添加数据库连接或其他依赖 - taskClassRepo *dao.TaskClassDAO - cacheRepo *dao.CacheDAO - scheduleRepo *dao.ScheduleDAO - repoManager *dao.RepoManager // 统一管理多个 DAO 的事务 -} - -func NewTaskClassService(taskClassRepo *dao.TaskClassDAO, cacheRepo *dao.CacheDAO, scheduleRepo *dao.ScheduleDAO, manager *dao.RepoManager) *TaskClassService { - return &TaskClassService{ - taskClassRepo: taskClassRepo, - cacheRepo: cacheRepo, - scheduleRepo: scheduleRepo, - repoManager: manager, - } -} - -// AddOrUpdateTaskClass 为指定用户添加任务类 -func (sv *TaskClassService) AddOrUpdateTaskClass(ctx context.Context, req *model.UserAddTaskClassRequest, userID int, method int, targetTaskClassID int) error { - //1.先校验参数 - if req.Mode == "auto" { - if req.StartDate == "" || req.EndDate == "" { - return respond.MissingParamForAutoScheduling - } - st, err := time.Parse("2006-01-02", req.StartDate) - if err != nil { - return respond.WrongParamType - } - ed, err := time.Parse("2006-01-02", req.EndDate) - if err != nil { - return respond.WrongParamType - } - if st.After(ed) { - return respond.InvalidDateRange - } - } - if req.Mode == "" || req.Name == "" || len(req.Items) == 0 { - return respond.MissingParam - } - // 1. excluded_slots 属于“半天块索引”,每个索引映射 2 节(1->1-2,...,6->11-12); - // 2. 若允许 7~12,会在粗排网格展开时产生越界节次,触发运行时 panic; - // 3. 这里统一在写入入口拦截,避免脏数据落库后污染后续排程链路。 - for _, slot := range req.Config.ExcludedSlots { - if slot < 1 || slot > 6 { - return respond.WrongParamType - } - } - // 1. excluded_days_of_week 表示“整天不可排”的硬约束,粗排时会直接整天屏蔽; - // 2. 只允许 1~7,对应周一到周日; - // 3. 若写入非法值,会导致粗排过滤口径和前端展示口径不一致,因此入口直接拦截。 - for _, dayOfWeek := range req.Config.ExcludedDaysOfWeek { - if dayOfWeek < 1 || dayOfWeek > 7 { - return respond.WrongParamType - } - } - //2.写数据库(事务内) - if err := sv.taskClassRepo.Transaction(func(txDAO *dao.TaskClassDAO) error { - taskClass, items, err := conv.ProcessUserAddTaskClassRequest(req, userID) - if err != nil { - return err - } - if method == 1 { // 更新操作 - taskClass.ID = targetTaskClassID - } - - taskClassID, err := txDAO.AddOrUpdateTaskClass(userID, taskClass) - if err != nil { - return err - } - - for i := range items { - items[i].CategoryID = &taskClassID - } - if err := txDAO.AddOrUpdateTaskClassItems(userID, items); err != nil { - return err - } - return nil - }); err != nil { - return err - } - - return nil -} - -func (sv *TaskClassService) GetUserTaskClassInfos(ctx context.Context, userID int) (*model.UserGetTaskClassesResponse, error) { - //1.先查询redis - list, err := sv.cacheRepo.GetTaskClassList(ctx, userID) - if err == nil { - //命中缓存 - return list, nil - } else if !errors.Is(err, redis.Nil) { //不是缓存未命中错误,说明redis可能炸了,照常放行 - log.Println("redis获取任务分类列表失败:", err) - } - //2.缓存未命中,查询数据库 - taskClasses, err := sv.taskClassRepo.GetUserTaskClasses(userID) - if err != nil { - return nil, err - } - resp := conv.TaskClassModelToResponse(taskClasses) - //3.写入缓存 - err = sv.cacheRepo.AddTaskClassList(ctx, userID, resp) - if err != nil { - return nil, err - } - return resp, nil -} - -func (sv *TaskClassService) GetUserCompleteTaskClass(ctx context.Context, userID int, taskClassID int) (*model.UserAddTaskClassRequest, error) { - //1.查询数据库 - taskClass, err := sv.taskClassRepo.GetCompleteTaskClassByID(ctx, taskClassID, userID) - if err != nil { - return nil, err - } - //2.转换为响应结构体 - resp, err := conv.ProcessUserGetCompleteTaskClassRequest(taskClass) - if err != nil { - return nil, err - } - return resp, nil -} - -func (sv *TaskClassService) AddTaskClassItemIntoSchedule(ctx context.Context, req *model.UserInsertTaskClassItemToScheduleRequest, userID int, taskID int) error { - //1.先验证任务块归属 - taskClassID, err := sv.taskClassRepo.GetTaskClassIDByTaskItemID(ctx, taskID) //通过任务块ID获取所属任务类ID - if err != nil { - return err - } - ownerID, err := sv.taskClassRepo.GetTaskClassUserIDByID(ctx, taskClassID) //通过任务类ID获取所属用户ID - if err != nil { - return err - } - if ownerID != userID { - return respond.TaskClassItemNotBelongToUser - } - //2.再检查任务块本身是否已经被安排 - result, err := sv.taskClassRepo.IfTaskClassItemArranged(ctx, taskID) - if err != nil { - return err - } - if result { - return respond.TaskClassItemAlreadyArranged - } - //3.取出任务块信息 - taskItem, err := sv.taskClassRepo.GetTaskClassItemByID(ctx, taskID) //通过任务块ID获取任务块信息 - if err != nil { - return err - } - //更新TaskClassItem的embedded_time字段 - taskItem.EmbeddedTime = &model.TargetTime{ - DayOfWeek: req.DayOfWeek, - Week: req.Week, - SectionFrom: req.StartSection, - SectionTo: req.EndSection, - } - //3.判断是否嵌入课程 - if req.EmbedCourseEventID != 0 { - //先检查看课程是否存在、是否归属该用户以及是否已经被嵌入了其他任务块 - courseOwnerID, err := sv.scheduleRepo.GetCourseUserIDByID(ctx, req.EmbedCourseEventID) - if err != nil { - return err - } - if courseOwnerID != userID { - return respond.CourseNotBelongToUser - } - //再检查用户给的时间是否和课程的时间匹配(目前逻辑是给的区间必须完全匹配) - match, err := sv.scheduleRepo.IsCourseTimeMatch(ctx, req.EmbedCourseEventID, req.Week, req.DayOfWeek, req.StartSection, req.EndSection) - if err != nil { - return err - } - if !match { - return respond.CourseTimeNotMatch - } - //查询对应时段的课程是否已被其他任务块嵌入了(目前业务限制:一个课程只能被一个任务块嵌入,但是目前设计是支持多个任务块嵌入一节课的,只要放得下) - isEmbedded, err := sv.scheduleRepo.IsCourseEmbeddedByOtherTaskBlock(ctx, req.EmbedCourseEventID, req.StartSection, req.EndSection) - if err != nil { - return err - } - if isEmbedded { - return respond.CourseAlreadyEmbeddedByOtherTaskBlock - } - //嵌入课程,直接更新日程表对应时段的 embedded_task_id 字段 - err = sv.scheduleRepo.EmbedTaskIntoSchedule(req.StartSection, req.EndSection, req.DayOfWeek, req.Week, userID, taskID) - if err != nil { - return err - } - //更新任务块的 embedded_time 字段 - err = sv.taskClassRepo.UpdateTaskClassItemEmbeddedTime(ctx, taskID, taskItem.EmbeddedTime) - if err != nil { - return err - } - return nil - } - //4.否则构造Schedule模型 - sections := make([]int, 0, req.EndSection-req.StartSection+1) - schedules, scheduleEvent, err := conv.UserInsertTaskItemRequestToModel(req, taskItem, nil, userID, req.StartSection, req.EndSection) - if err != nil { - return err - } - //将节次区间转换为节次切片,方便后续检查冲突 - for section := req.StartSection; section <= req.EndSection; section++ { - sections = append(sections, section) - } - //4.1 统一检查冲突(避免逐条查库) - conflict, err := sv.scheduleRepo.HasUserScheduleConflict(ctx, userID, req.Week, req.DayOfWeek, sections) - if err != nil { - return err - } - if conflict { - return respond.ScheduleConflict - } - // 5. 写入数据库(通过 RepoManager 统一管理事务) - // 这里的 sv.daoManager 是你在初始化 Service 时注入的全局 RepoManager 实例 - if err := sv.repoManager.Transaction(ctx, func(txM *dao.RepoManager) error { - // 5.1 使用事务中的 ScheduleRepo 插入 Event - // 💡 这里的 txM.Schedule 已经注入了事务句柄 - //此处要将req中的起始section以及第几周、星期几转换成绝对时间,存入scheduleEvent的StartTime和EndTime字段中,方便后续查询和冲突检查 - st, ed, err := conv.RelativeTimeToRealTime(req.Week, req.DayOfWeek, req.StartSection, req.EndSection) - if err != nil { - return err - } - scheduleEvent.StartTime = st - scheduleEvent.EndTime = ed - eventID, err := txM.Schedule.AddScheduleEvent(scheduleEvent) - if err != nil { - return err // 触发回滚 - } - // 5.2 关联 ID(纯内存操作,无需 tx) - for i := range schedules { - schedules[i].EventID = eventID - } - // 5.3 使用事务中的 ScheduleRepo 批量插入原子槽位 - // 💡 如果这里因为外键或唯一索引报错,5.1 的 Event 也会被撤回 - if _, err = txM.Schedule.AddSchedules(schedules); err != nil { - return err // 触发回滚 - } - // 5.4 使用事务中的 TaskRepo 更新任务状态 - // 💡 这里的 txM.Task 取代了你原来的 txDAO - if err := txM.TaskClass.UpdateTaskClassItemEmbeddedTime(ctx, taskID, taskItem.EmbeddedTime); err != nil { - return err // 触发回滚 - } - return nil - }); err != nil { - // 这里处理最终的错误返回,比如 respond.Error - return err - } - return nil -} - -func (sv *TaskClassService) DeleteTaskClassItem(ctx context.Context, userID int, taskItemID int) error { - //1.先验证任务块归属 - taskClassID, err := sv.taskClassRepo.GetTaskClassIDByTaskItemID(ctx, taskItemID) //通过任务块ID获取所属任务类ID - if err != nil { - return err - } - ownerID, err := sv.taskClassRepo.GetTaskClassUserIDByID(ctx, taskClassID) //通过任务类ID获取所属用户ID - if err != nil { - return err - } - if ownerID != userID { - return respond.TaskClassItemNotBelongToUser - } - //2.如果该任务块已经被安排了,先解除安排,再删除任务块(事务) - if err := sv.repoManager.Transaction(ctx, func(txM *dao.RepoManager) error { - //2.1.先检查该任务块是否已经被安排了 - arranged, err := txM.TaskClass.IfTaskClassItemArranged(ctx, taskItemID) - if err != nil { - return err - } - if arranged { - //2.2.如果已经被安排了,先解除安排 - //先扫schedules找到该task_item_id并删除 - _, txErr := txM.Schedule.FindEmbeddedTaskIDAndDeleteIt(ctx, taskItemID) - //2.3.再将task_items表的embedded_time字段设置为null - txErr = txM.TaskClass.DeleteTaskClassItemEmbeddedTime(ctx, taskItemID) - if txErr != nil { - return txErr - } - //再删除schedule_event表中对应的事件 - txErr = txM.Schedule.DeleteScheduleEventByTaskItemID(ctx, taskItemID) - if txErr != nil { - return txErr - } - } - //2.4.最后删除任务块 - err = txM.TaskClass.DeleteTaskClassItemByID(ctx, taskItemID) - if err != nil { - return err - } - return nil - }); err != nil { - return err - } - return nil -} - -func (sv *TaskClassService) DeleteTaskClass(ctx context.Context, userID int, taskClassID int) error { - //1.先验证任务类归属 - ownerID, err := sv.taskClassRepo.GetTaskClassUserIDByID(ctx, taskClassID) //通过任务类ID获取所属用户ID - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return respond.WrongTaskClassID - } - return err - } - if ownerID != userID { - return respond.TaskClassNotBelongToUser - } - //2.删除任务类(事务) - err = sv.taskClassRepo.DeleteTaskClassByID(ctx, taskClassID) - if err != nil { - return err - } - return nil -} - -// GetCompleteTaskClassByID 获取任务类完整详情(含关联的 TaskClassItem 列表)。 -// -// 职责边界: -// 1) 直接委托 DAO 层查询,不做额外业务逻辑; -// 2) 主要供 Agent 排程链路使用,获取 Items 用于 materialize 节点映射。 -func (sv *TaskClassService) GetCompleteTaskClassByID(ctx context.Context, taskClassID, userID int) (*model.TaskClass, error) { - return sv.taskClassRepo.GetCompleteTaskClassByID(ctx, taskClassID, userID) -} - -func (sv *TaskClassService) BatchApplyPlans(ctx context.Context, taskClassID int, userID int, plans *model.UserInsertTaskClassItemToScheduleRequestBatch) error { - //1.通过任务类id获取任务类详情 - taskClass, err := sv.taskClassRepo.GetCompleteTaskClassByID(ctx, taskClassID, userID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return respond.WrongTaskClassID - } - return err - } - //2.校验任务类的参数是否合法 - if taskClass == nil { - return respond.WrongTaskClassID - } - if *taskClass.Mode != "auto" { - return respond.TaskClassModeNotAuto - } - //3.获取任务类安排的时间范围内的全部周数信息(左右边界不足一周的情况也要算作一周),用于下方冲突检查 - startWeekTime := conv.CalculateFirstDayOfWeek(*taskClass.StartDate) - endWeekTime := conv.CalculateLastDayOfWeek(*taskClass.EndDate) - schedules, err := sv.scheduleRepo.GetUserSchedulesByTimeRange(ctx, userID, startWeekTime, endWeekTime) - if err != nil { - return err - } - startWeek, _, err := conv.RealDateToRelativeDate(startWeekTime.Format("2006-01-02")) - if err != nil { - return err - } - endWeek, _, err := conv.RealDateToRelativeDate(endWeekTime.Format("2006-01-02")) - if err != nil { - return err - } - //4.统一检查冲突(避免逐条查库) - //先将日程放入一个map中,key是"周-星期-节次",value是课程信息,方便后续检查冲突 - courseMap := make(map[string]model.Schedule) - for _, schedule := range schedules { - key := fmt.Sprintf("%d-%d-%d", schedule.Week, schedule.DayOfWeek, schedule.Section) - courseMap[key] = schedule - } - //再遍历每个任务块的安排时间,检查是否和课程冲突(目前逻辑是只要有一个时段冲突就算冲突,后续可以优化为统计冲突的时段数量,或者提供具体的冲突时段信息) - for _, plan := range plans.Items { - if plan.Week < startWeek || plan.Week > endWeek { - return respond.TaskClassItemTryingToInsertOutOfTimeRange - } - for section := plan.StartSection; section <= plan.EndSection; section++ { - key := fmt.Sprintf("%d-%d-%d", plan.Week, plan.DayOfWeek, section) - // 如果课程存在,并且满足以下任一条件则认为冲突: - // 1. 课程时段已经被其他任务块嵌入了(不允许多个任务块嵌入同一课程) - // 2. 当前时段的课的EventID与用户计划中指定的EmbedCourseEventID不匹配(说明用户计划要嵌入的课程和当前时段的课不是同一节) - // 3. 用户计划中没有指定EmbedCourseEventID(即EmbedCourseEventID为0),但当前时段有课(不允许在有课的时段安排任务块) - // 4. 当前时段的课不允许被嵌入(即使用户计划中指定了EmbedCourseEventID,但如果课程本身不允许被嵌入了,也算冲突) - if course, exists := courseMap[key]; exists && ((plan.EmbedCourseEventID != 0 && course.EmbeddedTask != nil) || - (plan.EmbedCourseEventID != course.EventID) || plan.EmbedCourseEventID == 0 || !course.Event.CanBeEmbedded) { - return respond.ScheduleConflict - } - } - } - //5.分流批量写入数据库(通过 RepoManager 统一管理事务) - //先分流 - toEmbed := make([]model.SingleTaskClassItem, 0) //需要嵌入课程的任务块 - toNormal := make([]model.SingleTaskClassItem, 0) //需要新建日程的任务块 - for _, item := range plans.Items { - if item.EmbedCourseEventID != 0 { - toEmbed = append(toEmbed, item) - } else { - toNormal = append(toNormal, item) - } - } - //再开事务批量写库 - if err := sv.repoManager.Transaction(ctx, func(txM *dao.RepoManager) error { - //5.1 先处理需要嵌入课程的任务块 - //先提取出需要嵌入的课程ID和TaskItemID列表 - courseIDs := make([]int, 0, len(toEmbed)) - for _, item := range toEmbed { - courseIDs = append(courseIDs, item.EmbedCourseEventID) - } - itemIDs := make([]int, 0, len(toEmbed)) - for _, item := range toEmbed { - itemIDs = append(itemIDs, item.TaskItemID) - } - //检查任务块本身是否已经被安排 - result, err := sv.taskClassRepo.BatchCheckIfTaskClassItemsArranged(ctx, itemIDs) - if err != nil { - return err - } - if result { - return respond.TaskClassItemAlreadyArranged - } - //验证一下plans中的taskItemID确实都属于这个用户和这个任务类(避免用户恶意构造请求把别的用户的任务块或者不属于任何任务类的任务块也安排了) - //同时也能检查是否重复 - result, err = sv.taskClassRepo.ValidateTaskItemIDsBelongToTaskClass(ctx, taskClassID, itemIDs) - if err != nil { - return err - } - if !result { - return respond.TaskClassItemNotBelongToTaskClass - } - //批量更新日程表中对应课程的embedded_task_id字段(目前业务限制:一个课程只能被一个任务块嵌入了,所以直接批量更新,不用担心覆盖问题) - err = txM.Schedule.BatchEmbedTaskIntoSchedule(ctx, courseIDs, itemIDs) - if err != nil { - return err - } - //批量更新任务块的embedded_time字段 - targetTimes := make([]*model.TargetTime, 0, len(toEmbed)) - for _, item := range toEmbed { - targetTimes = append(targetTimes, &model.TargetTime{ - DayOfWeek: item.DayOfWeek, - Week: item.Week, - SectionFrom: item.StartSection, - SectionTo: item.EndSection, - }) - } - err = txM.TaskClass.BatchUpdateTaskClassItemEmbeddedTime(ctx, itemIDs, targetTimes) - if err != nil { - return err - } - //5.2 再处理需要新建日程的任务块 - //先提取出需要新建日程的任务块ID列表 - normalItemIDs := make([]int, 0, len(toNormal)) - for _, item := range toNormal { - normalItemIDs = append(normalItemIDs, item.TaskItemID) - } - //验证一下plans中的taskItemID确实都属于这个任务类(避免用户恶意构造请求把别的用户的任务块或者不属于任何任务类的任务块也安排了) - result, err = sv.taskClassRepo.ValidateTaskItemIDsBelongToTaskClass(ctx, taskClassID, normalItemIDs) - if err != nil { - return err - } - if !result { - return respond.TaskClassItemNotBelongToTaskClass - } - //批量提取TaskItems - taskItems, err := txM.TaskClass.GetTaskClassItemsByIDs(ctx, normalItemIDs) - if err != nil { - return err - } - if len(taskItems) != len(normalItemIDs) { - log.Printf("警告:批量提取任务块时,返回的任务块数量与请求中的任务块ID数量不匹配,可能存在数据问题。请求ID数量:%d,返回任务块数量:%d", len(normalItemIDs), len(taskItems)) - return respond.InternalError(errors.New("返回的任务块数量与请求中的任务块ID数量不匹配,可能存在数据问题")) - } - //将toNormal按照TaskItemID升序排序,将taskItems也按照ID升序排序,保证一一对应关系(上面已经检查过重复) - //如果请求中的任务块ID有重复,这里就无法保证一一对应关系了,后续可以考虑在请求层面加一个校验,拒绝包含重复任务块ID的请求 - sort.SliceStable(toNormal, func(i, j int) bool { - return toNormal[i].TaskItemID < toNormal[j].TaskItemID - }) - sort.SliceStable(taskItems, func(i, j int) bool { - return taskItems[i].ID < taskItems[j].ID - }) - //开始构建event和schedules - finalSchedules := make([]model.Schedule, 0) //最终要插入数据库的Schedule切片 - finalScheduleEvents := make([]model.ScheduleEvent, 0) //最终要插入数据库的ScheduleEvent切片 - pos := make([]int, 0) //记录每个任务块对应的Schedule在finalSchedules中的位置,方便后续批量插入数据库后回填EventID - for i := 0; i < len(toNormal); i++ { - item := toNormal[i] - taskItem := taskItems[i] - if item.StartSection < 1 || item.EndSection > 12 || item.StartSection > item.EndSection { - return respond.InvalidSectionRange - } - schedules, scheduleEvent, err := conv.UserInsertTaskItemRequestToModel(&model.UserInsertTaskClassItemToScheduleRequest{ - Week: item.Week, - DayOfWeek: item.DayOfWeek, - StartSection: item.StartSection, - EndSection: item.EndSection, - EmbedCourseEventID: 0, //不嵌入课程 - }, &taskItem, nil, userID, item.StartSection, item.EndSection) - if err != nil { - return err - } - finalScheduleEvents = append(finalScheduleEvents, *scheduleEvent) - for range schedules { - pos = append(pos, len(finalScheduleEvents)-1) - } - finalSchedules = append(finalSchedules, schedules...) - } - //最后批量插入数据库 - //先插入ScheduleEvent表,获取生成的EventID,再批量插入Schedule表,最后批量更新TaskClassItem的embedded_time字段 - ids, err := txM.Schedule.InsertScheduleEvents(ctx, finalScheduleEvents) - if err != nil { - return err - } - // 将生成的 ScheduleEvent ID 赋值给对应的 Schedule 的 EventID 字段 - for i := range finalSchedules { - finalSchedules[i].EventID = ids[pos[i]] - } - if _, err = txM.Schedule.AddSchedules(finalSchedules); err != nil { - return err - } - //批量更新任务块的embedded_time字段 - targetTimes = make([]*model.TargetTime, 0, len(toEmbed)) - for _, item := range toNormal { - targetTimes = append(targetTimes, &model.TargetTime{ - DayOfWeek: item.DayOfWeek, - Week: item.Week, - SectionFrom: item.StartSection, - SectionTo: item.EndSection, - }) - } - //提取出所有需要更新的任务块ID - itemIDs = make([]int, 0, len(toNormal)) - for _, item := range toNormal { - itemIDs = append(itemIDs, item.TaskItemID) - } - err = txM.TaskClass.BatchUpdateTaskClassItemEmbeddedTime(ctx, itemIDs, targetTimes) - if err != nil { - return err - } - return nil - }); err != nil { - return err - } - return nil -} diff --git a/backend/service/task.go b/backend/service/task.go deleted file mode 100644 index d105b5f..0000000 --- a/backend/service/task.go +++ /dev/null @@ -1,537 +0,0 @@ -package service - -import ( - "context" - "errors" - "fmt" - "log" - "time" - - "github.com/LoveLosita/smartflow/backend/conv" - "github.com/LoveLosita/smartflow/backend/dao" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" - eventsvc "github.com/LoveLosita/smartflow/backend/service/events" - "github.com/go-redis/redis/v8" - "gorm.io/gorm" -) - -const ( - // taskBatchStatusMaxIDs 限制批量状态查询的单次任务 ID 数量,避免大请求放大缓存/内存扫描成本。 - taskBatchStatusMaxIDs = 100 - // taskUrgencyPromoteDedupeTTL 是"同一任务平移请求"的去重锁有效期。 - // - // 设计考虑: - // 1. 太短会导致消费稍慢时被重复投递; - // 2. 太长会导致首次投递失败后恢复变慢; - // 3. 这里先取 120 秒作为折中值,后续可按线上观测再调优。 - taskUrgencyPromoteDedupeTTL = 120 * time.Second - // taskUrgencyPromoteDedupeKeyFmt 是任务平移去重键模板。 - taskUrgencyPromoteDedupeKeyFmt = "smartflow:task:promote:pending:%d:%d" -) - -type TaskService struct { - // dao 负责任务表读写。 - dao *dao.TaskDAO - // cache 负责任务列表缓存与 Redis 去重锁能力。 - cache *dao.CacheDAO - // eventPublisher 负责发布 outbox 事件(可能为空:例如未启用 Kafka/总线时)。 - eventPublisher outboxinfra.EventPublisher - // activeScheduleDAO 负责维护主动调度 due job;为空时保持旧任务链路兼容。 - activeScheduleDAO *dao.ActiveScheduleDAO -} - -// NewTaskService 创建 TaskService 实例。 -// -// 职责边界: -// 1. 只做依赖注入,不做连接可用性探测; -// 2. 允许 eventPublisher 为空(用于本地降级场景)。 -func NewTaskService(taskDAO *dao.TaskDAO, cacheDAO *dao.CacheDAO, eventPublisher outboxinfra.EventPublisher) *TaskService { - return &TaskService{ - dao: taskDAO, - cache: cacheDAO, - eventPublisher: eventPublisher, - } -} - -// SetActiveScheduleDAO 注入主动调度自有表仓储。 -// -// 职责边界: -// 1. 只负责迁移期依赖接线,避免扩大 TaskService 构造函数调用面; -// 2. 不改变任务主流程语义,未注入时主动调度 job 同步自动降级为 no-op。 -func (ts *TaskService) SetActiveScheduleDAO(activeScheduleDAO *dao.ActiveScheduleDAO) { - if ts != nil { - ts.activeScheduleDAO = activeScheduleDAO - } -} - -// AddTask 新增任务。 -// -// 职责边界: -// 1. 负责参数转换、优先级合法性校验与写库; -// 2. 不负责"紧急性自动平移"逻辑(该逻辑发生在任务读取时的懒触发链路)。 -func (ts *TaskService) AddTask(ctx context.Context, req *model.UserAddTaskRequest, userID int) (*model.UserAddTaskResponse, error) { - // 1. 把用户请求转换为内部模型,避免 API 层结构直接泄漏到 DAO。 - taskModel := conv.UserAddTaskRequestToModel(req, userID) - // 2. 优先级范围校验:当前任务体系只允许 1~4。 - if taskModel.Priority < 1 || taskModel.Priority >= 5 { - return nil, respond.InvalidPriority - } - // 3. 写库。 - createdTask, err := ts.dao.AddTask(taskModel) - if err != nil { - return nil, err - } - ts.syncActiveScheduleJobBestEffort(ctx, createdTask) - // 4. 返回对外响应 DTO。 - response := conv.ModelToUserAddTaskResponse(createdTask) - return response, nil -} - -// CompleteTask 将用户指定任务标记为"已完成"。 -// -// 职责边界: -// 1. 负责入参校验与业务错误映射; -// 2. 负责调用 DAO 执行状态更新; -// 3. 不负责幂等键校验(幂等由中间件处理); -// 4. 不负责缓存删除细节(缓存删除由 GORM cache_deleter 回调触发)。 -func (ts *TaskService) CompleteTask(ctx context.Context, req *model.UserCompleteTaskRequest, userID int) (*model.UserCompleteTaskResponse, error) { - // 1. 参数兜底:请求体为空、非法 user 或非法 task_id 直接返回业务错误。 - if req == nil || userID <= 0 || req.TaskID <= 0 { - return nil, respond.WrongTaskID - } - - // 2. 调用 DAO 执行"查询 + 必要时更新"。 - updatedTask, alreadyCompleted, err := ts.dao.CompleteTaskByID(ctx, userID, req.TaskID) - if err != nil { - // 2.1 任务不存在或不属于当前用户时,统一映射为 WrongTaskID。 - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, respond.WrongTaskID - } - // 2.2 其余数据库异常向上透传,交由统一错误处理器返回 500。 - return nil, err - } - if updatedTask == nil { - // 3. 极端防御:DAO 不应返回 nil,若发生则视为内部异常。 - return nil, errors.New("complete task succeeded but task is nil") - } - - // 4. 构造响应: - // 4.1 already_completed=true 表示本次命中幂等,不影响最终成功状态; - // 4.2 is_completed 始终为 true,便于前端直接刷新状态。 - resp := &model.UserCompleteTaskResponse{ - TaskID: updatedTask.ID, - IsCompleted: true, - AlreadyCompleted: alreadyCompleted, - Status: "completed", - } - ts.cancelActiveScheduleJobBestEffort(ctx, updatedTask.UserID, updatedTask.ID, "task_completed") - return resp, nil -} - -// UndoCompleteTask 取消用户任务的"已完成勾选"。 -// -// 职责边界: -// 1. 负责入参校验与业务错误映射; -// 2. 负责调用 DAO 执行状态恢复; -// 3. 不负责幂等缓存(本接口按需求要求:任务未完成时必须报错); -// 4. 不负责缓存删除细节(由 GORM cache_deleter 回调自动处理)。 -func (ts *TaskService) UndoCompleteTask(ctx context.Context, req *model.UserUndoCompleteTaskRequest, userID int) (*model.UserUndoCompleteTaskResponse, error) { - // 1. 参数兜底:请求体为空、非法 user 或非法 task_id 直接返回业务错误。 - if req == nil || userID <= 0 || req.TaskID <= 0 { - return nil, respond.WrongTaskID - } - - // 2. 调用 DAO 执行"恢复未完成"逻辑。 - updatedTask, err := ts.dao.UndoCompleteTaskByID(ctx, userID, req.TaskID) - if err != nil { - // 2.1 任务不存在或不属于当前用户,统一映射为 WrongTaskID。 - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, respond.WrongTaskID - } - // 2.2 任务本来就未完成:按需求返回明确业务错误。 - if errors.Is(err, respond.TaskNotCompleted) { - return nil, respond.TaskNotCompleted - } - // 2.3 其余数据库异常继续向上透传。 - return nil, err - } - if updatedTask == nil { - // 3. 极端防御:DAO 成功但返回 nil,视为内部异常。 - return nil, errors.New("undo complete task succeeded but task is nil") - } - - // 4. 组装响应:恢复成功后 is_completed 恒为 false。 - resp := &model.UserUndoCompleteTaskResponse{ - TaskID: updatedTask.ID, - IsCompleted: false, - Status: "uncompleted", - } - return resp, nil -} - -// GetUserTasks 获取用户任务列表(含"读时紧急性派生"与"异步平移触发")。 -// -// 核心流程(步骤化): -// 1. 先读缓存,未命中再回源 DB,并把"原始模型"回填缓存; -// 2. 在内存里做"读时派生":仅用于本次返回给前端,不直接改库; -// 3. 收集"已到紧急分界线且仍处于非紧急象限"的任务 ID; -// 4. 通过 Redis SETNX 去重后,发布 outbox 事件异步落库; -// 5. 无论发布成功与否,都优先返回本次派生结果,保证用户读体验。 -// -// 一致性策略: -// 1. 缓存里存的是原始任务,不是派生后的优先级; -// 2. 真实平移由异步消费者条件更新 DB; -// 3. DB 更新后由 cache_deleter 自动删缓存,下一次读取自然拿到新状态。 -func (ts *TaskService) GetUserTasks(ctx context.Context, userID int) ([]model.GetUserTaskResp, error) { - derivedTasks, err := ts.GetTasksWithUrgencyPromotion(ctx, userID) - if err != nil { - return nil, err - } - return conv.ModelToGetUserTasksResp(derivedTasks), nil -} - -// BatchTaskStatus 批量查询当前登录用户任务的完成状态。 -// -// 职责边界: -// 1. 负责请求 ID 的过滤、去重和数量限制; -// 2. 只返回当前用户有权访问且仍存在的任务,避免泄露其他用户任务状态; -// 3. 复用 getRawUserTasks 的 Redis 任务列表缓存链路,不新增绕过缓存的 DAO 查询; -// 4. 该接口只读,不触发 GORM cache_deleter,也不反向修改 NewAgent timeline 历史快照。 -func (ts *TaskService) BatchTaskStatus(ctx context.Context, req *model.BatchTaskStatusRequest, userID int) (*model.BatchTaskStatusResponse, error) { - resp := &model.BatchTaskStatusResponse{ - Items: []model.BatchTaskStatusItem{}, - } - if userID <= 0 { - return nil, respond.WrongUserID - } - if req == nil { - return resp, nil - } - - // 1. 先把前端传入的历史卡片 task id 做归一化。 - // 1.1 非法 ID 直接过滤,避免无意义匹配; - // 1.2 保留首次出现顺序,方便前端按请求顺序回填; - // 1.3 超过上限时截断,避免单次 hydration 请求放大服务端成本。 - validIDs := compactPositiveUniqueTaskIDsWithLimit(req.IDs, taskBatchStatusMaxIDs) - if len(validIDs) == 0 { - return resp, nil - } - - // 2. 复用原始任务读取链路。 - // 2.1 命中 Redis 时直接读取 smartflow:tasks:{userID}; - // 2.2 未命中时由 getRawUserTasks 回源 DB 并回填缓存; - // 2.3 用户没有任何任务时映射为空 items,符合 hydration 的“无匹配不报错”语义。 - tasks, err := ts.getRawUserTasks(ctx, userID) - if err != nil { - if errors.Is(err, respond.UserTasksEmpty) { - return resp, nil - } - return nil, err - } - - // 3. 在当前用户任务集合内做内存匹配。 - // 3.1 不命中的 ID 可能是已删除、属于其他用户、或历史快照里的旧任务,统一静默过滤; - // 3.2 返回字段只包含当前模型可用的完成状态,避免伪造不存在的 updated_at。 - taskByID := make(map[int]model.Task, len(tasks)) - for _, task := range tasks { - taskByID[task.ID] = task - } - for _, id := range validIDs { - task, exists := taskByID[id] - if !exists { - continue - } - resp.Items = append(resp.Items, model.BatchTaskStatusItem{ - ID: task.ID, - IsCompleted: task.IsCompleted, - }) - } - return resp, nil -} - -// GetTasksWithUrgencyPromotion 读取用户任务并应用读时紧急性提升 + 异步落库触发。 -// -// 统一入口,供前端查询(GetUserTasks)和 LLM 工具查询(QueryTasksForTool)复用。 -// 调用方不应假设 DB 已更新——持久化是异步的。 -func (ts *TaskService) GetTasksWithUrgencyPromotion(ctx context.Context, userID int) ([]model.Task, error) { - rawTasks, err := ts.getRawUserTasks(ctx, userID) - if err != nil { - return nil, err - } - derivedTasks, duePromoteTaskIDs := deriveTaskUrgencyForRead(rawTasks, time.Now()) - ts.tryEnqueueTaskUrgencyPromote(ctx, userID, duePromoteTaskIDs) - return derivedTasks, nil -} - -// getRawUserTasks 读取"原始任务模型"。 -// -// 职责边界: -// 1. 负责缓存命中/回源 DB/回填缓存; -// 2. 不做优先级派生,不做异步事件投递; -// 3. 缓存写失败只记日志,不阻断主流程。 -func (ts *TaskService) getRawUserTasks(ctx context.Context, userID int) ([]model.Task, error) { - // 1. 先查缓存:命中则直接返回。 - cachedTasks, err := ts.cache.GetUserTasksFromCache(ctx, userID) - if err == nil { - return cachedTasks, nil - } - - // 2. 非 redis.Nil 错误直接返回,避免掩盖真实故障。 - if !errors.Is(err, redis.Nil) { - return nil, err - } - - // 3. 缓存未命中回源 DB。 - dbTasks, err := ts.dao.GetTasksByUserID(userID) - if err != nil { - return nil, err - } - - // 4. 回填缓存(失败不阻断主链路)。 - if setErr := ts.cache.SetUserTasksToCache(ctx, userID, dbTasks); setErr != nil { - log.Printf("写入用户任务缓存失败: user_id=%d err=%v", userID, setErr) - } - return dbTasks, nil -} - -// deriveTaskUrgencyForRead 对任务做"读时紧急性派生",并收集需要异步落库的任务 ID。 -// -// 职责边界: -// 1. 只在内存里改本次返回值,不写 DB; -// 2. 只做"到线且未完成任务"的优先级映射; -// 3. 不处理去重锁和事件发布。 -// -// 返回语义: -// 1. 第一个返回值:可直接用于响应前端的派生任务切片; -// 2. 第二个返回值:需要发"异步平移事件"的任务 ID 列表(可能为空)。 -func deriveTaskUrgencyForRead(tasks []model.Task, now time.Time) ([]model.Task, []int) { - // 1. 拷贝切片,避免修改调用方持有的原始数据。 - derived := make([]model.Task, len(tasks)) - copy(derived, tasks) - - pendingPromoteTaskIDs := make([]int, 0, len(derived)) - - // 2. 逐条判断是否满足"自动平移"条件。 - for idx := range derived { - current := &derived[idx] - - // 2.1 已完成任务不参与平移。 - if current.IsCompleted { - continue - } - // 2.2 没有分界线的任务不参与平移。 - if current.UrgencyThresholdAt == nil { - continue - } - // 2.3 尚未到分界线,不平移。 - if current.UrgencyThresholdAt.After(now) { - continue - } - - // 2.4 到线后,仅把"不紧急象限"平移到对应"紧急象限"。 - // 2.4.1 重要不紧急(2) -> 重要且紧急(1) - // 2.4.2 不简单不重要(4) -> 简单不重要(3) - switch current.Priority { - case 2: - current.Priority = 1 - pendingPromoteTaskIDs = append(pendingPromoteTaskIDs, current.ID) - case 4: - current.Priority = 3 - pendingPromoteTaskIDs = append(pendingPromoteTaskIDs, current.ID) - default: - // 2.4.3 其他优先级不处理(包含已经是 1/3 的情况)。 - } - } - return derived, pendingPromoteTaskIDs -} - -// tryEnqueueTaskUrgencyPromote 尝试发布"任务紧急性平移请求"事件。 -// -// 职责边界: -// 1. 负责 Redis 去重锁 + outbox 发布; -// 2. 不负责真正落库(由消费者负责); -// 3. 发布失败时要释放本次抢到的去重锁,避免任务被长时间"误判已投递"。 -func (ts *TaskService) tryEnqueueTaskUrgencyPromote(ctx context.Context, userID int, taskIDs []int) { - // 1. 基础兜底:无发布器或无候选任务时直接返回。 - if ts.eventPublisher == nil || userID <= 0 || len(taskIDs) == 0 { - return - } - - // 2. 先做任务 ID 清洗,避免无效 ID 参与去重与发布。 - validTaskIDs := compactPositiveUniqueTaskIDs(taskIDs) - if len(validTaskIDs) == 0 { - return - } - - // 3. 逐个抢 SETNX 去重锁: - // 3.1 抢到锁才允许进入本次发布; - // 3.2 抢不到说明已有请求在途,本次跳过即可; - // 3.3 抢锁失败只记录日志,不中断主流程。 - lockedTaskIDs := make([]int, 0, len(validTaskIDs)) - lockedKeys := make([]string, 0, len(validTaskIDs)) - for _, taskID := range validTaskIDs { - lockKey := fmt.Sprintf(taskUrgencyPromoteDedupeKeyFmt, userID, taskID) - locked, lockErr := ts.cache.AcquireLock(ctx, lockKey, taskUrgencyPromoteDedupeTTL) - if lockErr != nil { - log.Printf("任务平移去重锁获取失败: user_id=%d task_id=%d err=%v", userID, taskID, lockErr) - continue - } - if !locked { - continue - } - lockedTaskIDs = append(lockedTaskIDs, taskID) - lockedKeys = append(lockedKeys, lockKey) - } - if len(lockedTaskIDs) == 0 { - return - } - - // 4. 发布 outbox 事件:这里只保证"成功入 outbox 或返回错误",不等待消费者执行完成。 - publishErr := eventsvc.PublishTaskUrgencyPromoteRequested(ctx, ts.eventPublisher, model.TaskUrgencyPromoteRequestedPayload{ - UserID: userID, - TaskIDs: lockedTaskIDs, - TriggeredAt: time.Now(), - }) - if publishErr != nil { - // 4.1 失败回滚:释放本次抢到的去重锁,避免后续请求因误锁而无法再投递。 - ts.releaseTaskPromoteLocks(lockedKeys) - log.Printf("任务平移事件发布失败: user_id=%d task_ids=%v err=%v", userID, lockedTaskIDs, publishErr) - return - } - - log.Printf("任务平移事件已发布: user_id=%d task_ids=%v", userID, lockedTaskIDs) -} - -// releaseTaskPromoteLocks 释放任务平移去重锁。 -// -// 说明: -// 1. 仅用于"发布失败回滚"场景; -// 2. 使用 Background 避免请求上下文已取消时导致锁释放失败。 -func (ts *TaskService) releaseTaskPromoteLocks(lockKeys []string) { - if len(lockKeys) == 0 { - return - } - releaseCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - - for _, key := range lockKeys { - if err := ts.cache.ReleaseLock(releaseCtx, key); err != nil { - log.Printf("任务平移去重锁释放失败: key=%s err=%v", key, err) - } - } -} - -// compactPositiveUniqueTaskIDs 对任务 ID 做"过滤非正数 + 去重"。 -// -// 职责边界: -// 1. 只做参数清洗; -// 2. 不承载业务规则判断。 -func compactPositiveUniqueTaskIDs(taskIDs []int) []int { - return compactPositiveUniqueTaskIDsWithLimit(taskIDs, 0) -} - -// compactPositiveUniqueTaskIDsWithLimit 对任务 ID 做"过滤非正数 + 去重 + 可选限量"。 -// -// 职责边界: -// 1. 只做纯参数归一化,不查询任务、不判断权限; -// 2. limit <= 0 表示不限制数量,供既有调用保持原行为; -// 3. 达到 limit 后立即停止扫描,避免超长请求继续消耗 CPU。 -func compactPositiveUniqueTaskIDsWithLimit(taskIDs []int, limit int) []int { - seen := make(map[int]struct{}, len(taskIDs)) - result := make([]int, 0, len(taskIDs)) - for _, taskID := range taskIDs { - if taskID <= 0 { - continue - } - if _, exists := seen[taskID]; exists { - continue - } - seen[taskID] = struct{}{} - result = append(result, taskID) - if limit > 0 && len(result) >= limit { - break - } - } - return result -} - -// UpdateTask 更新用户指定任务的属性(部分更新)。 -// -// 职责边界: -// 1. 负责参数校验:task_id 合法性、priority_group 范围; -// 2. 负责将请求 DTO 转换为 DAO 层的 updates map; -// 3. 空请求体(无字段需要更新)返回明确业务错误; -// 4. 不负责缓存删除(由 GORM cache_deleter 回调自动处理)。 -func (ts *TaskService) UpdateTask(ctx context.Context, req *model.UserUpdateTaskRequest, userID int) (model.GetUserTaskResp, error) { - // 1. 参数兜底。 - if req == nil || userID <= 0 || req.TaskID <= 0 { - return model.GetUserTaskResp{}, respond.WrongTaskID - } - - // 2. 构造 updates map:只有非 nil 的字段才写入。 - updates := make(map[string]interface{}) - if req.Title != nil { - updates["title"] = *req.Title - } - if req.PriorityGroup != nil { - // 2.1 优先级范围校验:当前任务体系只允许 1~4。 - if *req.PriorityGroup < 1 || *req.PriorityGroup > 4 { - return model.GetUserTaskResp{}, respond.InvalidPriority - } - // 2.2 JSON 字段名是 priority_group,数据库列名是 priority。 - updates["priority"] = *req.PriorityGroup - } - if req.DeadlineAt != nil { - updates["deadline_at"] = *req.DeadlineAt - } - if req.UrgencyThresholdAt != nil { - updates["urgency_threshold_at"] = *req.UrgencyThresholdAt - } - - // 3. 空更新检测:至少需要一个可更新字段。 - if len(updates) == 0 { - return model.GetUserTaskResp{}, respond.TaskUpdateNoFields - } - - // 4. 调用 DAO 执行更新。 - updatedTask, err := ts.dao.UpdateTaskByID(ctx, userID, req.TaskID, updates) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return model.GetUserTaskResp{}, respond.WrongTaskID - } - return model.GetUserTaskResp{}, err - } - ts.syncActiveScheduleJobBestEffort(ctx, updatedTask) - - // 5. 转换为响应 DTO。 - return conv.ModelToGetUserTaskResp(updatedTask), nil -} - -// DeleteTask 永久删除用户指定任务。 -// -// 职责边界: -// 1. 负责入参校验与业务错误映射; -// 2. 负责调用 DAO 执行硬删除; -// 3. 任务不存在时返回幂等信息码(TaskAlreadyDeleted); -// 4. 不负责缓存删除(由 GORM cache_deleter 回调自动处理)。 -func (ts *TaskService) DeleteTask(ctx context.Context, req *model.UserCompleteTaskRequest, userID int) (int, error) { - // 1. 参数兜底。 - if req == nil || userID <= 0 || req.TaskID <= 0 { - return 0, respond.WrongTaskID - } - - // 2. 调用 DAO 执行删除。 - deletedTask, err := ts.dao.DeleteTaskByID(ctx, userID, req.TaskID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - // 2.1 任务不存在或不属于当前用户:按幂等语义返回信息码。 - return 0, respond.TaskAlreadyDeleted - } - return 0, err - } - ts.cancelActiveScheduleJobBestEffort(ctx, deletedTask.UserID, deletedTask.ID, "task_deleted") - - return deletedTask.ID, nil -} diff --git a/backend/service/task_active_schedule.go b/backend/service/task_active_schedule.go deleted file mode 100644 index 413c98e..0000000 --- a/backend/service/task_active_schedule.go +++ /dev/null @@ -1,91 +0,0 @@ -package service - -import ( - "context" - "errors" - "fmt" - "log" - "time" - - "github.com/LoveLosita/smartflow/backend/model" - "gorm.io/gorm" -) - -// syncActiveScheduleJobBestEffort 在任务变更后同步主动调度 due job。 -// -// 职责边界: -// 1. 只维护 important_urgent_task 的 job,不直接触发主动调度主链路; -// 2. 任务未完成且存在 urgency_threshold_at 时 upsert pending job; -// 3. 任务已完成或阈值为空时取消当前 pending job; -// 4. 当前任务接口尚未整体事务化,job 同步失败只记日志,避免任务主写入出现“已落库但接口失败”的更差体验。 -func (ts *TaskService) syncActiveScheduleJobBestEffort(ctx context.Context, task *model.Task) { - if ts == nil || ts.activeScheduleDAO == nil || task == nil { - return - } - if task.IsCompleted || task.UrgencyThresholdAt == nil { - ts.cancelActiveScheduleJobBestEffort(ctx, task.UserID, task.ID, "task_not_schedulable") - return - } - - job := &model.ActiveScheduleJob{ - ID: activeScheduleJobID(task.UserID, task.ID), - UserID: task.UserID, - TaskID: task.ID, - TriggerType: model.ActiveScheduleTriggerTypeImportantUrgentTask, - Status: model.ActiveScheduleJobStatusPending, - TriggerAt: *task.UrgencyThresholdAt, - DedupeKey: activeScheduleTriggerDedupeKey(task.UserID, task.ID, *task.UrgencyThresholdAt), - TraceID: activeScheduleTraceID(task.UserID, task.ID), - } - if err := ts.activeScheduleDAO.CreateOrUpdateJob(ctx, job); err != nil { - log.Printf("主动调度 job upsert 失败: user_id=%d task_id=%d err=%v", task.UserID, task.ID, err) - } -} - -// cancelActiveScheduleJobBestEffort 取消任务当前待触发 job。 -// -// 职责边界: -// 1. 只取消 pending job,历史 triggered/skipped/failed 记录保留审计; -// 2. 找不到 pending job 属于正常幂等场景; -// 3. reason 只进入 last_error_code,方便后续排障知道取消来源。 -func (ts *TaskService) cancelActiveScheduleJobBestEffort(ctx context.Context, userID int, taskID int, reason string) { - if ts == nil || ts.activeScheduleDAO == nil || userID <= 0 || taskID <= 0 { - return - } - job, err := ts.activeScheduleDAO.FindPendingJobByTask(ctx, userID, taskID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return - } - log.Printf("主动调度 pending job 查询失败: user_id=%d task_id=%d err=%v", userID, taskID, err) - return - } - now := time.Now() - updates := map[string]any{ - "status": model.ActiveScheduleJobStatusCanceled, - "last_error_code": reason, - "last_scanned_at": &now, - } - if err = ts.activeScheduleDAO.UpdateJobFields(ctx, job.ID, updates); err != nil { - log.Printf("主动调度 pending job 取消失败: user_id=%d task_id=%d job_id=%s err=%v", userID, taskID, job.ID, err) - } -} - -func activeScheduleJobID(userID int, taskID int) string { - return fmt.Sprintf("asj_task_%d_%d", userID, taskID) -} - -func activeScheduleTraceID(userID int, taskID int) string { - return fmt.Sprintf("trace_active_task_%d_%d", userID, taskID) -} - -func activeScheduleTriggerDedupeKey(userID int, taskID int, triggerAt time.Time) string { - windowStart := triggerAt.Truncate(30 * time.Minute) - return fmt.Sprintf("%d:%s:%s:%d:%s", - userID, - model.ActiveScheduleTriggerTypeImportantUrgentTask, - model.ActiveScheduleTargetTypeTaskPool, - taskID, - windowStart.Format(time.RFC3339), - ) -} diff --git a/backend/services/active_scheduler/core/adapters/gorm_readers.go b/backend/services/active_scheduler/core/adapters/gorm_readers.go index bec1f74..e31f68e 100644 --- a/backend/services/active_scheduler/core/adapters/gorm_readers.go +++ b/backend/services/active_scheduler/core/adapters/gorm_readers.go @@ -6,10 +6,10 @@ import ( "fmt" "time" - "github.com/LoveLosita/smartflow/backend/conv" - "github.com/LoveLosita/smartflow/backend/model" "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/ports" "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/trigger" + "github.com/LoveLosita/smartflow/backend/services/runtime/conv" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "gorm.io/gorm" ) diff --git a/backend/services/active_scheduler/core/apply/convert.go b/backend/services/active_scheduler/core/apply/convert.go index 3acb20d..f64efab 100644 --- a/backend/services/active_scheduler/core/apply/convert.go +++ b/backend/services/active_scheduler/core/apply/convert.go @@ -6,7 +6,7 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" ) const ( diff --git a/backend/services/active_scheduler/core/apply/validate.go b/backend/services/active_scheduler/core/apply/validate.go index 960c712..fc5df71 100644 --- a/backend/services/active_scheduler/core/apply/validate.go +++ b/backend/services/active_scheduler/core/apply/validate.go @@ -4,7 +4,7 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" ) // IsPreviewExpired 判断 preview 是否已经超过确认有效期。 diff --git a/backend/services/active_scheduler/core/applyadapter/adapter.go b/backend/services/active_scheduler/core/applyadapter/adapter.go index 579c3f8..977aeed 100644 --- a/backend/services/active_scheduler/core/applyadapter/adapter.go +++ b/backend/services/active_scheduler/core/applyadapter/adapter.go @@ -8,8 +8,8 @@ import ( "strconv" "strings" - "github.com/LoveLosita/smartflow/backend/conv" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/conv" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "gorm.io/gorm" "gorm.io/gorm/clause" ) diff --git a/backend/services/active_scheduler/core/job/scanner.go b/backend/services/active_scheduler/core/job/scanner.go index cca66e0..c6f917d 100644 --- a/backend/services/active_scheduler/core/job/scanner.go +++ b/backend/services/active_scheduler/core/job/scanner.go @@ -8,11 +8,11 @@ import ( "log" "time" - "github.com/LoveLosita/smartflow/backend/dao" - "github.com/LoveLosita/smartflow/backend/model" "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/ports" activesvc "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/service" "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/trigger" + "github.com/LoveLosita/smartflow/backend/services/runtime/dao" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" ) const ( diff --git a/backend/services/active_scheduler/core/preview/converter.go b/backend/services/active_scheduler/core/preview/converter.go index 5020488..b72ec45 100644 --- a/backend/services/active_scheduler/core/preview/converter.go +++ b/backend/services/active_scheduler/core/preview/converter.go @@ -9,11 +9,11 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/model" "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/candidate" schedulercontext "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/context" "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/observe" "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/ports" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" ) func candidateDTO(item candidate.Candidate) CandidateDTO { diff --git a/backend/services/active_scheduler/core/preview/service.go b/backend/services/active_scheduler/core/preview/service.go index 7910f1a..61eeb60 100644 --- a/backend/services/active_scheduler/core/preview/service.go +++ b/backend/services/active_scheduler/core/preview/service.go @@ -7,10 +7,10 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/model" "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/candidate" schedulercontext "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/context" "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/observe" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "github.com/google/uuid" "gorm.io/gorm" ) diff --git a/backend/services/active_scheduler/core/service/preview_confirm.go b/backend/services/active_scheduler/core/service/preview_confirm.go index c58a0e5..4c0ebb8 100644 --- a/backend/services/active_scheduler/core/service/preview_confirm.go +++ b/backend/services/active_scheduler/core/service/preview_confirm.go @@ -6,11 +6,11 @@ import ( "errors" "time" - "github.com/LoveLosita/smartflow/backend/dao" - "github.com/LoveLosita/smartflow/backend/model" activeapply "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/apply" "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/applyadapter" activepreview "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/preview" + "github.com/LoveLosita/smartflow/backend/services/runtime/dao" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "gorm.io/gorm" ) diff --git a/backend/services/active_scheduler/core/service/session_bridge.go b/backend/services/active_scheduler/core/service/session_bridge.go index efbe77b..bdd35f5 100644 --- a/backend/services/active_scheduler/core/service/session_bridge.go +++ b/backend/services/active_scheduler/core/service/session_bridge.go @@ -8,10 +8,10 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/dao" - "github.com/LoveLosita/smartflow/backend/model" activepreview "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/preview" "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/selection" + "github.com/LoveLosita/smartflow/backend/services/runtime/dao" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "github.com/google/uuid" "gorm.io/gorm" ) diff --git a/backend/services/active_scheduler/core/service/trigger.go b/backend/services/active_scheduler/core/service/trigger.go index 8f9c45a..6576f33 100644 --- a/backend/services/active_scheduler/core/service/trigger.go +++ b/backend/services/active_scheduler/core/service/trigger.go @@ -8,11 +8,11 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/dao" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" - "github.com/LoveLosita/smartflow/backend/model" "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/trigger" + "github.com/LoveLosita/smartflow/backend/services/runtime/dao" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" sharedevents "github.com/LoveLosita/smartflow/backend/shared/events" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" "github.com/google/uuid" "gorm.io/gorm" ) diff --git a/backend/services/active_scheduler/core/service/trigger_outbox.go b/backend/services/active_scheduler/core/service/trigger_outbox.go index 2a9232d..ef6b597 100644 --- a/backend/services/active_scheduler/core/service/trigger_outbox.go +++ b/backend/services/active_scheduler/core/service/trigger_outbox.go @@ -8,9 +8,9 @@ import ( "strings" "time" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" sharedevents "github.com/LoveLosita/smartflow/backend/shared/events" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" ) // EnqueueActiveScheduleTriggeredInTx 在事务内写入 active_schedule.triggered outbox 消息。 diff --git a/backend/services/active_scheduler/core/service/trigger_pipeline.go b/backend/services/active_scheduler/core/service/trigger_pipeline.go index e3b0c73..2162a81 100644 --- a/backend/services/active_scheduler/core/service/trigger_pipeline.go +++ b/backend/services/active_scheduler/core/service/trigger_pipeline.go @@ -7,14 +7,14 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/dao" - kafkabus "github.com/LoveLosita/smartflow/backend/infra/kafka" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" - "github.com/LoveLosita/smartflow/backend/model" activegraph "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/graph" activepreview "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/preview" "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/trigger" + "github.com/LoveLosita/smartflow/backend/services/runtime/dao" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" sharedevents "github.com/LoveLosita/smartflow/backend/shared/events" + kafkabus "github.com/LoveLosita/smartflow/backend/shared/infra/kafka" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" "github.com/google/uuid" "gorm.io/gorm" "gorm.io/gorm/clause" diff --git a/backend/services/active_scheduler/dao/connect.go b/backend/services/active_scheduler/dao/connect.go index 074baa0..b48edb2 100644 --- a/backend/services/active_scheduler/dao/connect.go +++ b/backend/services/active_scheduler/dao/connect.go @@ -3,10 +3,9 @@ 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" + coremodel "github.com/LoveLosita/smartflow/backend/services/runtime/model" + mysqlinfra "github.com/LoveLosita/smartflow/backend/shared/infra/mysql" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" "gorm.io/gorm" ) @@ -17,18 +16,7 @@ import ( // 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{}) + db, err := mysqlinfra.OpenDBFromConfig() if err != nil { return nil, err } diff --git a/backend/services/active_scheduler/rpc/errors.go b/backend/services/active_scheduler/rpc/errors.go index 420371b..666d919 100644 --- a/backend/services/active_scheduler/rpc/errors.go +++ b/backend/services/active_scheduler/rpc/errors.go @@ -5,9 +5,9 @@ import ( "log" "strings" - "github.com/LoveLosita/smartflow/backend/respond" activeapply "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/apply" contracts "github.com/LoveLosita/smartflow/backend/shared/contracts/activescheduler" + "github.com/LoveLosita/smartflow/backend/shared/respond" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/backend/services/active_scheduler/rpc/handler.go b/backend/services/active_scheduler/rpc/handler.go index 51a1fdc..59b05f4 100644 --- a/backend/services/active_scheduler/rpc/handler.go +++ b/backend/services/active_scheduler/rpc/handler.go @@ -6,10 +6,10 @@ import ( "errors" "time" - "github.com/LoveLosita/smartflow/backend/respond" "github.com/LoveLosita/smartflow/backend/services/active_scheduler/rpc/pb" activeschedulersv "github.com/LoveLosita/smartflow/backend/services/active_scheduler/sv" contracts "github.com/LoveLosita/smartflow/backend/shared/contracts/activescheduler" + "github.com/LoveLosita/smartflow/backend/shared/respond" ) type Handler struct { diff --git a/backend/services/active_scheduler/sv/service.go b/backend/services/active_scheduler/sv/service.go index f065383..e66bb97 100644 --- a/backend/services/active_scheduler/sv/service.go +++ b/backend/services/active_scheduler/sv/service.go @@ -8,10 +8,6 @@ import ( "strings" "time" - rootdao "github.com/LoveLosita/smartflow/backend/dao" - kafkabus "github.com/LoveLosita/smartflow/backend/infra/kafka" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" - eventsvc "github.com/LoveLosita/smartflow/backend/service/events" activeadapters "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/adapters" activeapply "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/apply" activeapplyadapter "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/applyadapter" @@ -22,8 +18,12 @@ import ( activesvc "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/service" "github.com/LoveLosita/smartflow/backend/services/active_scheduler/core/trigger" llmservice "github.com/LoveLosita/smartflow/backend/services/llm" + rootdao "github.com/LoveLosita/smartflow/backend/services/runtime/dao" + eventsvc "github.com/LoveLosita/smartflow/backend/services/runtime/eventsvc" contracts "github.com/LoveLosita/smartflow/backend/shared/contracts/activescheduler" sharedevents "github.com/LoveLosita/smartflow/backend/shared/events" + kafkabus "github.com/LoveLosita/smartflow/backend/shared/infra/kafka" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" "gorm.io/gorm" ) diff --git a/backend/services/agent/conv/schedule_preview.go b/backend/services/agent/conv/schedule_preview.go index 1636603..904f4f6 100644 --- a/backend/services/agent/conv/schedule_preview.go +++ b/backend/services/agent/conv/schedule_preview.go @@ -4,8 +4,8 @@ import ( "fmt" "time" - "github.com/LoveLosita/smartflow/backend/model" schedule "github.com/LoveLosita/smartflow/backend/services/agent/tools/schedule" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" ) // ScheduleStateToPreview 将 agent 的 ScheduleState 转换为前端预览缓存格式。 diff --git a/backend/services/agent/conv/schedule_provider.go b/backend/services/agent/conv/schedule_provider.go index 3cfc223..25c053e 100644 --- a/backend/services/agent/conv/schedule_provider.go +++ b/backend/services/agent/conv/schedule_provider.go @@ -6,10 +6,10 @@ import ( "sort" "time" - baseconv "github.com/LoveLosita/smartflow/backend/conv" - "github.com/LoveLosita/smartflow/backend/dao" - "github.com/LoveLosita/smartflow/backend/model" schedule "github.com/LoveLosita/smartflow/backend/services/agent/tools/schedule" + baseconv "github.com/LoveLosita/smartflow/backend/services/runtime/conv" + "github.com/LoveLosita/smartflow/backend/services/runtime/dao" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" ) // ScheduleProvider 实现 model.ScheduleStateProvider 接口。 diff --git a/backend/services/agent/conv/schedule_state.go b/backend/services/agent/conv/schedule_state.go index 41c9aa2..4e9f5c2 100644 --- a/backend/services/agent/conv/schedule_state.go +++ b/backend/services/agent/conv/schedule_state.go @@ -3,8 +3,8 @@ package agentconv import ( "sort" - "github.com/LoveLosita/smartflow/backend/model" schedule "github.com/LoveLosita/smartflow/backend/services/agent/tools/schedule" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" ) // WindowDay 表示排课窗口中的一天(相对周 + 周几)。 diff --git a/backend/services/agent/conv/schedule_state_apply.go b/backend/services/agent/conv/schedule_state_apply.go index 17b7420..0dd2e27 100644 --- a/backend/services/agent/conv/schedule_state_apply.go +++ b/backend/services/agent/conv/schedule_state_apply.go @@ -1,9 +1,9 @@ package agentconv import ( - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" schedule "github.com/LoveLosita/smartflow/backend/services/agent/tools/schedule" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + "github.com/LoveLosita/smartflow/backend/shared/respond" ) // ApplyPlacedItems 将前端提交的绝对时间放置项应用到 ScheduleState。 diff --git a/backend/services/agent/node/quick_task.go b/backend/services/agent/node/quick_task.go index 620b218..54235df 100644 --- a/backend/services/agent/node/quick_task.go +++ b/backend/services/agent/node/quick_task.go @@ -8,13 +8,13 @@ import ( "strings" "time" - taskmodel "github.com/LoveLosita/smartflow/backend/model" agentmodel "github.com/LoveLosita/smartflow/backend/services/agent/model" agentprompt "github.com/LoveLosita/smartflow/backend/services/agent/prompt" agentrouter "github.com/LoveLosita/smartflow/backend/services/agent/router" agentshared "github.com/LoveLosita/smartflow/backend/services/agent/shared" agentstream "github.com/LoveLosita/smartflow/backend/services/agent/stream" llmservice "github.com/LoveLosita/smartflow/backend/services/llm" + taskmodel "github.com/LoveLosita/smartflow/backend/services/runtime/model" "github.com/cloudwego/eino/schema" ) diff --git a/backend/services/agent/node/unified_compact.go b/backend/services/agent/node/unified_compact.go index 87b2268..7ba087d 100644 --- a/backend/services/agent/node/unified_compact.go +++ b/backend/services/agent/node/unified_compact.go @@ -6,9 +6,9 @@ import ( "fmt" "log" - "github.com/LoveLosita/smartflow/backend/pkg" agentmodel "github.com/LoveLosita/smartflow/backend/services/agent/model" agentprompt "github.com/LoveLosita/smartflow/backend/services/agent/prompt" + agentshared "github.com/LoveLosita/smartflow/backend/services/agent/shared" agentstream "github.com/LoveLosita/smartflow/backend/services/agent/stream" llmservice "github.com/LoveLosita/smartflow/backend/services/llm" "github.com/cloudwego/eino/schema" @@ -78,7 +78,7 @@ func compactUnifiedMessagesIfNeeded( msg3 := messages[3].Content // 3. Token 预算检查。 - breakdown, overBudget, needCompactMsg1, needCompactMsg2 := pkg.CheckStageTokenBudget(msg0, msg1, msg2, msg3) + breakdown, overBudget, needCompactMsg1, needCompactMsg2 := agentshared.CheckStageTokenBudget(msg0, msg1, msg2, msg3) log.Printf( "[COMPACT:%s] token budget check: total=%d budget=%d over=%v compactMsg1=%v compactMsg2=%v (msg0=%d msg1=%d msg2=%d msg3=%d)", @@ -97,14 +97,14 @@ func compactUnifiedMessagesIfNeeded( msg1 = compactUnifiedMsg1(ctx, input, msg1) messages[1].Content = msg1 // 压缩 msg1 后重算预算。 - breakdown = pkg.EstimateStageMessagesTokens(msg0, msg1, msg2, msg3) + breakdown = agentshared.EstimateStageMessagesTokens(msg0, msg1, msg2, msg3) } // 6. msg2 压缩(阶段工作区 → LLM 摘要)。 - if needCompactMsg2 || breakdown.Total > pkg.StageTokenBudget { + if needCompactMsg2 || breakdown.Total > agentshared.StageTokenBudget { msg2 = compactUnifiedMsg2(ctx, input, msg2) messages[2].Content = msg2 - breakdown = pkg.EstimateStageMessagesTokens(msg0, msg1, msg2, msg3) + breakdown = agentshared.EstimateStageMessagesTokens(msg0, msg1, msg2, msg3) } // 7. 记录最终 token 分布。 @@ -124,8 +124,8 @@ func compactUnifiedMessagesIfNeeded( // 1. 先按消息类型汇总 token,保证总量准确; // 2. 再把最后一个 user 消息尽量视作 msg3,保留阶段指令语义; // 3. 其他历史内容归入 msg1 / msg2,确保上下文统计不会因为结构不标准而断更。 -func estimateFallbackStageTokenBreakdown(messages []*schema.Message) pkg.StageTokenBreakdown { - breakdown := pkg.StageTokenBreakdown{Budget: pkg.StageTokenBudget} +func estimateFallbackStageTokenBreakdown(messages []*schema.Message) agentshared.StageTokenBreakdown { + breakdown := agentshared.StageTokenBreakdown{Budget: agentshared.StageTokenBudget} if len(messages) == 0 { return breakdown } @@ -146,7 +146,7 @@ func estimateFallbackStageTokenBreakdown(messages []*schema.Message) pkg.StageTo if msg == nil { continue } - tokens := pkg.EstimateMessageTokens(msg) + tokens := agentshared.EstimateMessageTokens(msg) breakdown.Total += tokens switch msg.Role { @@ -199,7 +199,7 @@ func compactUnifiedMsg1( } // 3. SSE: 压缩开始。 - tokenBefore := pkg.EstimateTextTokens(msg1) + tokenBefore := agentshared.EstimateTextTokens(msg1) _ = input.Emitter.EmitStatus( input.StatusBlockID, input.StageName, "context_compact_start", fmt.Sprintf("正在压缩对话历史(%d tokens)...", tokenBefore), @@ -219,7 +219,7 @@ func compactUnifiedMsg1( } // 5. SSE: 压缩完成。 - tokenAfter := pkg.EstimateTextTokens(newSummary) + tokenAfter := agentshared.EstimateTextTokens(newSummary) _ = input.Emitter.EmitStatus( input.StatusBlockID, input.StageName, "context_compact_done", fmt.Sprintf("对话历史已压缩:%d → %d tokens", tokenBefore, tokenAfter), @@ -246,7 +246,7 @@ func compactUnifiedMsg2( msg2 string, ) string { // 1. SSE: 压缩开始。 - tokenBefore := pkg.EstimateTextTokens(msg2) + tokenBefore := agentshared.EstimateTextTokens(msg2) _ = input.Emitter.EmitStatus( input.StatusBlockID, input.StageName, "context_compact_start", fmt.Sprintf("正在压缩执行记录(%d tokens)...", tokenBefore), @@ -266,7 +266,7 @@ func compactUnifiedMsg2( } // 3. SSE: 压缩完成。 - tokenAfter := pkg.EstimateTextTokens(compressed) + tokenAfter := agentshared.EstimateTextTokens(compressed) _ = input.Emitter.EmitStatus( input.StatusBlockID, input.StageName, "context_compact_done", fmt.Sprintf("执行记录已压缩:%d → %d tokens", tokenBefore, tokenAfter), @@ -285,7 +285,7 @@ func compactUnifiedMsg2( func saveUnifiedTokenStats( ctx context.Context, input UnifiedCompactInput, - breakdown pkg.StageTokenBreakdown, + breakdown agentshared.StageTokenBreakdown, ) { if input.CompactionStore == nil || input.FlowState == nil { return diff --git a/backend/services/agent/rpc/errors.go b/backend/services/agent/rpc/errors.go index ef69e8b..df5a3ad 100644 --- a/backend/services/agent/rpc/errors.go +++ b/backend/services/agent/rpc/errors.go @@ -5,7 +5,7 @@ import ( "log" "strings" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/shared/respond" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/backend/services/agent/rpc/handler.go b/backend/services/agent/rpc/handler.go index fc1c937..3c226f2 100644 --- a/backend/services/agent/rpc/handler.go +++ b/backend/services/agent/rpc/handler.go @@ -6,11 +6,11 @@ import ( "errors" "strings" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" "github.com/LoveLosita/smartflow/backend/services/agent/rpc/pb" agentsv "github.com/LoveLosita/smartflow/backend/services/agent/sv" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" agentcontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/agent" + "github.com/LoveLosita/smartflow/backend/shared/respond" ) type Handler struct { diff --git a/backend/services/agent/shared/clone.go b/backend/services/agent/shared/clone.go index 58d738a..a707933 100644 --- a/backend/services/agent/shared/clone.go +++ b/backend/services/agent/shared/clone.go @@ -1,6 +1,6 @@ package agentshared -import "github.com/LoveLosita/smartflow/backend/model" +import "github.com/LoveLosita/smartflow/backend/services/runtime/model" func CloneWeekSchedules(src []model.UserWeekSchedule) []model.UserWeekSchedule { if len(src) == 0 { diff --git a/backend/services/agent/shared/node_unified_compact.go b/backend/services/agent/shared/node_unified_compact.go index 007b4b7..6269d81 100644 --- a/backend/services/agent/shared/node_unified_compact.go +++ b/backend/services/agent/shared/node_unified_compact.go @@ -6,7 +6,6 @@ import ( "fmt" "log" - "github.com/LoveLosita/smartflow/backend/pkg" agentmodel "github.com/LoveLosita/smartflow/backend/services/agent/model" agentprompt "github.com/LoveLosita/smartflow/backend/services/agent/prompt" agentstream "github.com/LoveLosita/smartflow/backend/services/agent/stream" @@ -77,7 +76,7 @@ func CompactUnifiedMessagesIfNeeded( msg3 := messages[3].Content // 3. 执行 token 预算检查,判断是否需要压缩历史对话或阶段工作区。 - breakdown, overBudget, needCompactMsg1, needCompactMsg2 := pkg.CheckStageTokenBudget(msg0, msg1, msg2, msg3) + breakdown, overBudget, needCompactMsg1, needCompactMsg2 := CheckStageTokenBudget(msg0, msg1, msg2, msg3) log.Printf( "[COMPACT:%s] token budget check: total=%d budget=%d over=%v compactMsg1=%v compactMsg2=%v (msg0=%d msg1=%d msg2=%d msg3=%d)", @@ -95,14 +94,14 @@ func CompactUnifiedMessagesIfNeeded( if needCompactMsg1 { msg1 = compactUnifiedMsg1(ctx, input, msg1) messages[1].Content = msg1 - breakdown = pkg.EstimateStageMessagesTokens(msg0, msg1, msg2, msg3) + breakdown = EstimateStageMessagesTokens(msg0, msg1, msg2, msg3) } // 6. 若 msg1 压缩后仍超限,再压缩 msg2(阶段工作区 / ReAct 记录)。 - if needCompactMsg2 || breakdown.Total > pkg.StageTokenBudget { + if needCompactMsg2 || breakdown.Total > StageTokenBudget { msg2 = compactUnifiedMsg2(ctx, input, msg2) messages[2].Content = msg2 - breakdown = pkg.EstimateStageMessagesTokens(msg0, msg1, msg2, msg3) + breakdown = EstimateStageMessagesTokens(msg0, msg1, msg2, msg3) } // 7. 记录最终 token 分布,供后续调试与监控使用。 @@ -122,8 +121,8 @@ func CompactUnifiedMessagesIfNeeded( // 1. 先按消息类型汇总 token,保证总量准确; // 2. 再把最后一个 user 消息尽量视作 msg3,保留阶段指令语义; // 3. 其他历史内容归入 msg1 / msg2,确保上下文统计不会因为结构不标准而断更。 -func estimateFallbackStageTokenBreakdown(messages []*schema.Message) pkg.StageTokenBreakdown { - breakdown := pkg.StageTokenBreakdown{Budget: pkg.StageTokenBudget} +func estimateFallbackStageTokenBreakdown(messages []*schema.Message) StageTokenBreakdown { + breakdown := StageTokenBreakdown{Budget: StageTokenBudget} if len(messages) == 0 { return breakdown } @@ -144,7 +143,7 @@ func estimateFallbackStageTokenBreakdown(messages []*schema.Message) pkg.StageTo if msg == nil { continue } - tokens := pkg.EstimateMessageTokens(msg) + tokens := EstimateMessageTokens(msg) breakdown.Total += tokens switch msg.Role { @@ -194,7 +193,7 @@ func compactUnifiedMsg1( log.Printf("[COMPACT:%s] load existing compaction failed: %v, proceed without cache", input.StageName, err) } - tokenBefore := pkg.EstimateTextTokens(msg1) + tokenBefore := EstimateTextTokens(msg1) _ = input.Emitter.EmitStatus( input.StatusBlockID, input.StageName, "context_compact_start", fmt.Sprintf("正在压缩对话历史(%d tokens)...", tokenBefore), @@ -212,7 +211,7 @@ func compactUnifiedMsg1( return msg1 } - tokenAfter := pkg.EstimateTextTokens(newSummary) + tokenAfter := EstimateTextTokens(newSummary) _ = input.Emitter.EmitStatus( input.StatusBlockID, input.StageName, "context_compact_done", fmt.Sprintf("对话历史已压缩:%d → %d tokens", tokenBefore, tokenAfter), @@ -237,7 +236,7 @@ func compactUnifiedMsg2( input UnifiedCompactInput, msg2 string, ) string { - tokenBefore := pkg.EstimateTextTokens(msg2) + tokenBefore := EstimateTextTokens(msg2) _ = input.Emitter.EmitStatus( input.StatusBlockID, input.StageName, "context_compact_start", fmt.Sprintf("正在压缩执行记录(%d tokens)...", tokenBefore), @@ -255,7 +254,7 @@ func compactUnifiedMsg2( return msg2 } - tokenAfter := pkg.EstimateTextTokens(compressed) + tokenAfter := EstimateTextTokens(compressed) _ = input.Emitter.EmitStatus( input.StatusBlockID, input.StageName, "context_compact_done", fmt.Sprintf("执行记录已压缩:%d → %d tokens", tokenBefore, tokenAfter), @@ -274,7 +273,7 @@ func compactUnifiedMsg2( func saveUnifiedTokenStats( ctx context.Context, input UnifiedCompactInput, - breakdown pkg.StageTokenBreakdown, + breakdown StageTokenBreakdown, ) { if input.CompactionStore == nil || input.FlowState == nil { return diff --git a/backend/pkg/token_budget.go b/backend/services/agent/shared/token_budget.go similarity index 99% rename from backend/pkg/token_budget.go rename to backend/services/agent/shared/token_budget.go index 61554b5..a37f2cd 100644 --- a/backend/pkg/token_budget.go +++ b/backend/services/agent/shared/token_budget.go @@ -1,4 +1,4 @@ -package pkg +package agentshared import ( "math" diff --git a/backend/services/agent/sv/agent.go b/backend/services/agent/sv/agent.go index f681b48..443278b 100644 --- a/backend/services/agent/sv/agent.go +++ b/backend/services/agent/sv/agent.go @@ -9,18 +9,18 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/conv" - "github.com/LoveLosita/smartflow/backend/dao" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/pkg" - eventsvc "github.com/LoveLosita/smartflow/backend/service/events" agentmodel "github.com/LoveLosita/smartflow/backend/services/agent/model" agentprompt "github.com/LoveLosita/smartflow/backend/services/agent/prompt" + agentshared "github.com/LoveLosita/smartflow/backend/services/agent/shared" agenttools "github.com/LoveLosita/smartflow/backend/services/agent/tools" llmservice "github.com/LoveLosita/smartflow/backend/services/llm" memorymodel "github.com/LoveLosita/smartflow/backend/services/memory/model" memoryobserve "github.com/LoveLosita/smartflow/backend/services/memory/observe" + "github.com/LoveLosita/smartflow/backend/services/runtime/conv" + "github.com/LoveLosita/smartflow/backend/services/runtime/dao" + eventsvc "github.com/LoveLosita/smartflow/backend/services/runtime/eventsvc" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" "github.com/cloudwego/eino/schema" "github.com/google/uuid" ) @@ -333,7 +333,7 @@ func (s *AgentService) runNormalChatFlow( if chatHistory == nil { // 2. 缓存未命中时回源 DB,并转换为 Eino message 格式。 cacheMiss = true - histories, hisErr := s.repo.GetUserChatHistories(ctx, userID, pkg.HistoryFetchLimitByModel(resolvedModelName), chatID) + histories, hisErr := s.repo.GetUserChatHistories(ctx, userID, agentshared.HistoryFetchLimitByModel(resolvedModelName), chatID) if hisErr != nil { pushErrNonBlocking(errChan, hisErr) return @@ -343,12 +343,12 @@ func (s *AgentService) runNormalChatFlow( // 3. 计算本次请求可用的历史 token 预算,并执行历史裁剪。 // 这样可以在上下文增长时稳定控制模型窗口,避免超长上下文引发报错或高延迟。 - historyBudget := pkg.HistoryTokenBudgetByModel(resolvedModelName, agentprompt.SystemPrompt, userMessage) - trimmedHistory, totalHistoryTokens, keptHistoryTokens, droppedCount := pkg.TrimHistoryByTokenBudget(chatHistory, historyBudget) + historyBudget := agentshared.HistoryTokenBudgetByModel(resolvedModelName, agentprompt.SystemPrompt, userMessage) + trimmedHistory, totalHistoryTokens, keptHistoryTokens, droppedCount := agentshared.TrimHistoryByTokenBudget(chatHistory, historyBudget) chatHistory = trimmedHistory // 4. 根据裁剪后历史长度更新 Redis 会话窗口配置,并主动执行窗口收敛。 - targetWindow := pkg.CalcSessionWindowSize(len(chatHistory)) + targetWindow := agentshared.CalcSessionWindowSize(len(chatHistory)) if err = s.agentCache.SetSessionWindowSize(ctx, chatID, targetWindow); err != nil { log.Printf("设置历史窗口失败 chat=%s: %v", chatID, err) } diff --git a/backend/services/agent/sv/agent_active_schedule_session.go b/backend/services/agent/sv/agent_active_schedule_session.go index 2ec6a6f..9b6565e 100644 --- a/backend/services/agent/sv/agent_active_schedule_session.go +++ b/backend/services/agent/sv/agent_active_schedule_session.go @@ -7,8 +7,8 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/model" agentstream "github.com/LoveLosita/smartflow/backend/services/agent/stream" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "github.com/cloudwego/eino/schema" ) diff --git a/backend/services/agent/sv/agent_graph.go b/backend/services/agent/sv/agent_graph.go index bf73e3b..a9c65df 100644 --- a/backend/services/agent/sv/agent_graph.go +++ b/backend/services/agent/sv/agent_graph.go @@ -17,12 +17,12 @@ import ( "github.com/cloudwego/eino/schema" "github.com/spf13/viper" - "github.com/LoveLosita/smartflow/backend/conv" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/pkg" - "github.com/LoveLosita/smartflow/backend/respond" - eventsvc "github.com/LoveLosita/smartflow/backend/service/events" agentprompt "github.com/LoveLosita/smartflow/backend/services/agent/prompt" + agentshared "github.com/LoveLosita/smartflow/backend/services/agent/shared" + "github.com/LoveLosita/smartflow/backend/services/runtime/conv" + eventsvc "github.com/LoveLosita/smartflow/backend/services/runtime/eventsvc" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + "github.com/LoveLosita/smartflow/backend/shared/respond" ) const ( @@ -410,7 +410,7 @@ func (s *AgentService) loadConversationContext(ctx context.Context, chatID, user // 缓存未命中时回源 DB。 if history == nil { - histories, hisErr := s.repo.GetUserChatHistories(ctx, 0, pkg.HistoryFetchLimitByModel("worker"), chatID) + histories, hisErr := s.repo.GetUserChatHistories(ctx, 0, agentshared.HistoryFetchLimitByModel("worker"), chatID) if hisErr != nil { log.Printf("从 DB 加载历史失败 chat=%s: %v", chatID, hisErr) } else { diff --git a/backend/services/agent/sv/agent_meta.go b/backend/services/agent/sv/agent_meta.go index d382ac0..509d82d 100644 --- a/backend/services/agent/sv/agent_meta.go +++ b/backend/services/agent/sv/agent_meta.go @@ -8,10 +8,10 @@ import ( "time" "unicode/utf8" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" - eventsvc "github.com/LoveLosita/smartflow/backend/service/events" llmservice "github.com/LoveLosita/smartflow/backend/services/llm" + eventsvc "github.com/LoveLosita/smartflow/backend/services/runtime/eventsvc" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + "github.com/LoveLosita/smartflow/backend/shared/respond" "github.com/cloudwego/eino/schema" ) diff --git a/backend/services/agent/sv/agent_schedule_preview.go b/backend/services/agent/sv/agent_schedule_preview.go index 51fbcf3..adb531f 100644 --- a/backend/services/agent/sv/agent_schedule_preview.go +++ b/backend/services/agent/sv/agent_schedule_preview.go @@ -7,9 +7,9 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" agentshared "github.com/LoveLosita/smartflow/backend/services/agent/shared" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + "github.com/LoveLosita/smartflow/backend/shared/respond" ) // GetSchedulePlanPreview 按 conversation_id 读取结构化排程预览。 diff --git a/backend/services/agent/sv/agent_schedule_state.go b/backend/services/agent/sv/agent_schedule_state.go index ab9756d..6fd689a 100644 --- a/backend/services/agent/sv/agent_schedule_state.go +++ b/backend/services/agent/sv/agent_schedule_state.go @@ -7,11 +7,11 @@ import ( "log" "strings" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" agentconv "github.com/LoveLosita/smartflow/backend/services/agent/conv" agentmodel "github.com/LoveLosita/smartflow/backend/services/agent/model" agentshared "github.com/LoveLosita/smartflow/backend/services/agent/shared" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + "github.com/LoveLosita/smartflow/backend/shared/respond" ) // SaveScheduleState 处理前端拖拽后的“暂存排程状态”请求。 diff --git a/backend/services/agent/sv/agent_task_query.go b/backend/services/agent/sv/agent_task_query.go index 3e4021b..71707a8 100644 --- a/backend/services/agent/sv/agent_task_query.go +++ b/backend/services/agent/sv/agent_task_query.go @@ -7,9 +7,9 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" agentmodel "github.com/LoveLosita/smartflow/backend/services/agent/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + "github.com/LoveLosita/smartflow/backend/shared/respond" ) func (s *AgentService) QueryTasksForTool(ctx context.Context, req agentmodel.TaskQueryRequest) ([]agentmodel.TaskQueryTaskRecord, error) { diff --git a/backend/services/agent/sv/agent_timeline.go b/backend/services/agent/sv/agent_timeline.go index bb757d3..ca4a916 100644 --- a/backend/services/agent/sv/agent_timeline.go +++ b/backend/services/agent/sv/agent_timeline.go @@ -8,9 +8,9 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/model" - eventsvc "github.com/LoveLosita/smartflow/backend/service/events" agentstream "github.com/LoveLosita/smartflow/backend/services/agent/stream" + eventsvc "github.com/LoveLosita/smartflow/backend/services/runtime/eventsvc" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "gorm.io/gorm" ) diff --git a/backend/services/agent/sv/memory_rpc_reader.go b/backend/services/agent/sv/memory_rpc_reader.go index 8acc7c5..76f71dc 100644 --- a/backend/services/agent/sv/memory_rpc_reader.go +++ b/backend/services/agent/sv/memory_rpc_reader.go @@ -13,7 +13,7 @@ import ( // // 职责边界: // 1. 只读取候选记忆,不暴露管理写接口; -// 2. 不要求调用方知道 gateway/client/memory 的具体实现; +// 2. 不要求调用方知道 backend/client/memory 的具体实现; // 3. 错误原样返回给预取链路,由 agent 侧负责软降级和观测记录。 type MemoryRPCReaderClient interface { Retrieve(ctx context.Context, req memorycontracts.RetrieveRequest) ([]memorycontracts.ItemDTO, error) diff --git a/backend/services/agent/sv/schedule_rpc_provider.go b/backend/services/agent/sv/schedule_rpc_provider.go index 51522c1..716af91 100644 --- a/backend/services/agent/sv/schedule_rpc_provider.go +++ b/backend/services/agent/sv/schedule_rpc_provider.go @@ -8,9 +8,9 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/model" agentconv "github.com/LoveLosita/smartflow/backend/services/agent/conv" scheduletool "github.com/LoveLosita/smartflow/backend/services/agent/tools/schedule" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" schedulecontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/schedule" taskclasscontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/taskclass" ) diff --git a/backend/services/agent/sv/task_class_rpc_adapter.go b/backend/services/agent/sv/task_class_rpc_adapter.go index 0167578..4038780 100644 --- a/backend/services/agent/sv/task_class_rpc_adapter.go +++ b/backend/services/agent/sv/task_class_rpc_adapter.go @@ -7,8 +7,8 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/model" agenttools "github.com/LoveLosita/smartflow/backend/services/agent/tools" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" taskclasscontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/taskclass" ) diff --git a/backend/services/agent/sv/task_rpc_adapter.go b/backend/services/agent/sv/task_rpc_adapter.go index 54fbdc9..2cdb372 100644 --- a/backend/services/agent/sv/task_rpc_adapter.go +++ b/backend/services/agent/sv/task_rpc_adapter.go @@ -7,10 +7,10 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" agentmodel "github.com/LoveLosita/smartflow/backend/services/agent/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" taskcontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/task" + "github.com/LoveLosita/smartflow/backend/shared/respond" ) const quickTaskCreateRPCTimeout = 3 * time.Second diff --git a/backend/services/agent/tools/task_class_write.go b/backend/services/agent/tools/task_class_write.go index 689cf17..6fff875 100644 --- a/backend/services/agent/tools/task_class_write.go +++ b/backend/services/agent/tools/task_class_write.go @@ -6,7 +6,7 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" ) // TaskClassUpsertInput 描述任务类写库工具的标准化入参。 diff --git a/backend/services/agent/tools/taskclass_result_handlers.go b/backend/services/agent/tools/taskclass_result_handlers.go index d9bdc47..3d7f3f5 100644 --- a/backend/services/agent/tools/taskclass_result_handlers.go +++ b/backend/services/agent/tools/taskclass_result_handlers.go @@ -3,9 +3,9 @@ package agenttools import ( "strings" - "github.com/LoveLosita/smartflow/backend/model" "github.com/LoveLosita/smartflow/backend/services/agent/tools/schedule" taskclassresult "github.com/LoveLosita/smartflow/backend/services/agent/tools/taskclass_result" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" ) type taskClassUpsertExecutionInput struct { diff --git a/backend/services/course/dao/connect.go b/backend/services/course/dao/connect.go index a3c85fe..bbec990 100644 --- a/backend/services/course/dao/connect.go +++ b/backend/services/course/dao/connect.go @@ -3,8 +3,7 @@ package dao import ( "fmt" - "github.com/spf13/viper" - "gorm.io/driver/mysql" + mysqlinfra "github.com/LoveLosita/smartflow/backend/shared/infra/mysql" "gorm.io/gorm" ) @@ -15,18 +14,7 @@ import ( // 2. 本函数不 AutoMigrate schedule 表,避免 course 进程越权管理 schedule schema; // 3. 启动期只检查运行时依赖表是否存在,缺表时尽早失败。 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{}) + db, err := mysqlinfra.OpenDBFromConfig() if err != nil { return nil, err } diff --git a/backend/services/course/dao/course.go b/backend/services/course/dao/course.go index 30782b5..92bfc6d 100644 --- a/backend/services/course/dao/course.go +++ b/backend/services/course/dao/course.go @@ -3,7 +3,7 @@ package dao import ( "context" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "gorm.io/gorm" ) diff --git a/backend/services/course/rpc/errors.go b/backend/services/course/rpc/errors.go index 65a85f5..e94985d 100644 --- a/backend/services/course/rpc/errors.go +++ b/backend/services/course/rpc/errors.go @@ -5,8 +5,8 @@ import ( "log" "strings" - "github.com/LoveLosita/smartflow/backend/respond" coursesv "github.com/LoveLosita/smartflow/backend/services/course/sv" + "github.com/LoveLosita/smartflow/backend/shared/respond" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/backend/services/course/rpc/handler.go b/backend/services/course/rpc/handler.go index f628d89..e30c267 100644 --- a/backend/services/course/rpc/handler.go +++ b/backend/services/course/rpc/handler.go @@ -5,11 +5,11 @@ import ( "encoding/json" "errors" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" "github.com/LoveLosita/smartflow/backend/services/course/rpc/pb" coursesv "github.com/LoveLosita/smartflow/backend/services/course/sv" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" coursecontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/course" + "github.com/LoveLosita/smartflow/backend/shared/respond" ) type Handler struct { diff --git a/backend/services/course/sv/course.go b/backend/services/course/sv/course.go index 679dc55..c86e50e 100644 --- a/backend/services/course/sv/course.go +++ b/backend/services/course/sv/course.go @@ -4,12 +4,12 @@ import ( "context" "strings" - "github.com/LoveLosita/smartflow/backend/conv" - rootdao "github.com/LoveLosita/smartflow/backend/dao" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" coursedao "github.com/LoveLosita/smartflow/backend/services/course/dao" llmservice "github.com/LoveLosita/smartflow/backend/services/llm" + "github.com/LoveLosita/smartflow/backend/services/runtime/conv" + rootdao "github.com/LoveLosita/smartflow/backend/services/runtime/dao" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + "github.com/LoveLosita/smartflow/backend/shared/respond" ) type CourseService struct { diff --git a/backend/services/course/sv/course_parse.go b/backend/services/course/sv/course_parse.go index 5dda0db..25e1e7d 100644 --- a/backend/services/course/sv/course_parse.go +++ b/backend/services/course/sv/course_parse.go @@ -6,7 +6,7 @@ import ( "net/http" "strings" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" ) const ( diff --git a/backend/services/course/sv/course_parse_ark.go b/backend/services/course/sv/course_parse_ark.go index 93a0154..3e31411 100644 --- a/backend/services/course/sv/course_parse_ark.go +++ b/backend/services/course/sv/course_parse_ark.go @@ -8,8 +8,8 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/model" llmservice "github.com/LoveLosita/smartflow/backend/services/llm" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" ) // ParseCourseTableImage 使用 Ark SDK Responses 解析课程表图片。 diff --git a/backend/services/llm/service.go b/backend/services/llm/service.go index c185444..dbda2f4 100644 --- a/backend/services/llm/service.go +++ b/backend/services/llm/service.go @@ -3,7 +3,7 @@ package llm import ( "strings" - "github.com/LoveLosita/smartflow/backend/inits" + einoinfra "github.com/LoveLosita/smartflow/backend/shared/infra/eino" ) // Service 只负责统一暴露已经构造好的模型客户端,不负责 prompt 和业务编排。 @@ -19,7 +19,7 @@ type Service struct { // 2. CourseImageResponsesClient 允许外部预先注入,便于测试或特殊启动路径复用。 // 3. 某个字段为空时不报错,直接保留 nil,交给上层继续走兼容降级。 type Options struct { - AIHub *inits.AIHub + AIHub *einoinfra.AIHub APIKey string BaseURL string CourseVisionModel string diff --git a/backend/services/memory/dao/connect.go b/backend/services/memory/dao/connect.go index 4199d4a..9ef2a8c 100644 --- a/backend/services/memory/dao/connect.go +++ b/backend/services/memory/dao/connect.go @@ -1,14 +1,13 @@ package dao import ( - "context" "fmt" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" - coremodel "github.com/LoveLosita/smartflow/backend/model" + coremodel "github.com/LoveLosita/smartflow/backend/services/runtime/model" + mysqlinfra "github.com/LoveLosita/smartflow/backend/shared/infra/mysql" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" + redisinfra "github.com/LoveLosita/smartflow/backend/shared/infra/redis" "github.com/go-redis/redis/v8" - "github.com/spf13/viper" - "gorm.io/driver/mysql" "gorm.io/gorm" ) @@ -19,19 +18,8 @@ import ( // 2. 不迁移 agent、task、schedule、active-scheduler、notification 等跨域表,避免独立进程越权管理别的领域; // 3. 返回的 *gorm.DB 供 memory 服务内部 repo、worker 和 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, - ) - // 1. 先按统一配置建立 MySQL 连接;若连接失败,独立 memory 进程直接 fail fast。 - db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) + db, err := mysqlinfra.OpenDBFromConfig() if err != nil { return nil, err } @@ -60,15 +48,7 @@ func OpenDBFromConfig() (*gorm.DB, error) { // 2. 不创建、不预热、不清理任何 memory 业务 key; // 3. Ping 失败直接返回 error,让入口在缓存、锁或幂等依赖异常时尽早暴露问题。 func OpenRedisFromConfig() (*redis.Client, error) { - client := redis.NewClient(&redis.Options{ - Addr: viper.GetString("redis.host") + ":" + viper.GetString("redis.port"), - Password: viper.GetString("redis.password"), - DB: 0, - }) - if _, err := client.Ping(context.Background()).Result(); err != nil { - return nil, err - } - return client, nil + return redisinfra.OpenRedisFromConfig() } // autoMigrateMemoryOutboxTable 只迁移 memory 服务自己的 outbox 物理表。 diff --git a/backend/services/memory/internal/cleanup/dedup_policy.go b/backend/services/memory/internal/cleanup/dedup_policy.go index ec68e70..95fce44 100644 --- a/backend/services/memory/internal/cleanup/dedup_policy.go +++ b/backend/services/memory/internal/cleanup/dedup_policy.go @@ -4,7 +4,7 @@ import ( "sort" "time" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" ) const dedupRecentTieWindow = 24 * time.Hour diff --git a/backend/services/memory/internal/cleanup/dedup_runner.go b/backend/services/memory/internal/cleanup/dedup_runner.go index 8a5d86d..3bf5e58 100644 --- a/backend/services/memory/internal/cleanup/dedup_runner.go +++ b/backend/services/memory/internal/cleanup/dedup_runner.go @@ -7,11 +7,11 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/model" memoryrepo "github.com/LoveLosita/smartflow/backend/services/memory/internal/repo" memoryutils "github.com/LoveLosita/smartflow/backend/services/memory/internal/utils" memoryvectorsync "github.com/LoveLosita/smartflow/backend/services/memory/internal/vectorsync" memoryobserve "github.com/LoveLosita/smartflow/backend/services/memory/observe" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "gorm.io/gorm" ) diff --git a/backend/services/memory/internal/repo/audit_repo.go b/backend/services/memory/internal/repo/audit_repo.go index c6c38b8..d789f79 100644 --- a/backend/services/memory/internal/repo/audit_repo.go +++ b/backend/services/memory/internal/repo/audit_repo.go @@ -4,7 +4,7 @@ import ( "context" "errors" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "gorm.io/gorm" ) diff --git a/backend/services/memory/internal/repo/item_repo.go b/backend/services/memory/internal/repo/item_repo.go index 53c0923..bc3a679 100644 --- a/backend/services/memory/internal/repo/item_repo.go +++ b/backend/services/memory/internal/repo/item_repo.go @@ -6,8 +6,8 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/model" memorymodel "github.com/LoveLosita/smartflow/backend/services/memory/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "gorm.io/gorm" ) diff --git a/backend/services/memory/internal/repo/job_repo.go b/backend/services/memory/internal/repo/job_repo.go index 0735f97..c4a2a9b 100644 --- a/backend/services/memory/internal/repo/job_repo.go +++ b/backend/services/memory/internal/repo/job_repo.go @@ -6,8 +6,8 @@ import ( "errors" "time" - "github.com/LoveLosita/smartflow/backend/model" memorymodel "github.com/LoveLosita/smartflow/backend/services/memory/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "gorm.io/gorm" "gorm.io/gorm/clause" ) diff --git a/backend/services/memory/internal/repo/settings_repo.go b/backend/services/memory/internal/repo/settings_repo.go index bc64139..6f1e46d 100644 --- a/backend/services/memory/internal/repo/settings_repo.go +++ b/backend/services/memory/internal/repo/settings_repo.go @@ -4,7 +4,7 @@ import ( "context" "errors" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "gorm.io/gorm" "gorm.io/gorm/clause" ) diff --git a/backend/services/memory/internal/service/common.go b/backend/services/memory/internal/service/common.go index 621aa88..71b05df 100644 --- a/backend/services/memory/internal/service/common.go +++ b/backend/services/memory/internal/service/common.go @@ -3,9 +3,9 @@ package service import ( "strings" - "github.com/LoveLosita/smartflow/backend/model" memoryutils "github.com/LoveLosita/smartflow/backend/services/memory/internal/utils" memorymodel "github.com/LoveLosita/smartflow/backend/services/memory/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" ) func toItemDTO(item model.MemoryItem) memorymodel.ItemDTO { diff --git a/backend/services/memory/internal/service/manage_service.go b/backend/services/memory/internal/service/manage_service.go index e4dec23..23f5f3b 100644 --- a/backend/services/memory/internal/service/manage_service.go +++ b/backend/services/memory/internal/service/manage_service.go @@ -6,13 +6,13 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" memoryrepo "github.com/LoveLosita/smartflow/backend/services/memory/internal/repo" memoryutils "github.com/LoveLosita/smartflow/backend/services/memory/internal/utils" memoryvectorsync "github.com/LoveLosita/smartflow/backend/services/memory/internal/vectorsync" memorymodel "github.com/LoveLosita/smartflow/backend/services/memory/model" memoryobserve "github.com/LoveLosita/smartflow/backend/services/memory/observe" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + "github.com/LoveLosita/smartflow/backend/shared/respond" "gorm.io/gorm" ) diff --git a/backend/services/memory/internal/service/read_service.go b/backend/services/memory/internal/service/read_service.go index c3645b9..e7aea44 100644 --- a/backend/services/memory/internal/service/read_service.go +++ b/backend/services/memory/internal/service/read_service.go @@ -8,12 +8,12 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/model" memoryrepo "github.com/LoveLosita/smartflow/backend/services/memory/internal/repo" memoryutils "github.com/LoveLosita/smartflow/backend/services/memory/internal/utils" memorymodel "github.com/LoveLosita/smartflow/backend/services/memory/model" memoryobserve "github.com/LoveLosita/smartflow/backend/services/memory/observe" ragservice "github.com/LoveLosita/smartflow/backend/services/rag" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" ) const ( diff --git a/backend/services/memory/internal/service/retrieve_merge.go b/backend/services/memory/internal/service/retrieve_merge.go index d26eded..eb461c4 100644 --- a/backend/services/memory/internal/service/retrieve_merge.go +++ b/backend/services/memory/internal/service/retrieve_merge.go @@ -5,9 +5,9 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/model" memoryutils "github.com/LoveLosita/smartflow/backend/services/memory/internal/utils" memorymodel "github.com/LoveLosita/smartflow/backend/services/memory/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" ) // HybridRetrieve 统一承接读取侧 RAG-first 召回链路。 diff --git a/backend/services/memory/internal/utils/audit.go b/backend/services/memory/internal/utils/audit.go index 2f1cdd1..5e67888 100644 --- a/backend/services/memory/internal/utils/audit.go +++ b/backend/services/memory/internal/utils/audit.go @@ -4,7 +4,7 @@ import ( "encoding/json" "strings" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" ) const ( diff --git a/backend/services/memory/internal/utils/settings.go b/backend/services/memory/internal/utils/settings.go index 287787c..2b51718 100644 --- a/backend/services/memory/internal/utils/settings.go +++ b/backend/services/memory/internal/utils/settings.go @@ -1,8 +1,8 @@ package utils import ( - "github.com/LoveLosita/smartflow/backend/model" memorymodel "github.com/LoveLosita/smartflow/backend/services/memory/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" ) // EffectiveUserSetting 返回用户记忆设置的生效值。 diff --git a/backend/services/memory/internal/vectorsync/syncer.go b/backend/services/memory/internal/vectorsync/syncer.go index 1ed2f6a..926b0f4 100644 --- a/backend/services/memory/internal/vectorsync/syncer.go +++ b/backend/services/memory/internal/vectorsync/syncer.go @@ -6,10 +6,10 @@ import ( "log" "strings" - "github.com/LoveLosita/smartflow/backend/model" memoryrepo "github.com/LoveLosita/smartflow/backend/services/memory/internal/repo" memoryobserve "github.com/LoveLosita/smartflow/backend/services/memory/observe" ragservice "github.com/LoveLosita/smartflow/backend/services/rag" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" ) // Syncer 负责 memory_items 与向量库之间的最小桥接。 diff --git a/backend/services/memory/internal/worker/apply_actions.go b/backend/services/memory/internal/worker/apply_actions.go index 91eb5f1..105932c 100644 --- a/backend/services/memory/internal/worker/apply_actions.go +++ b/backend/services/memory/internal/worker/apply_actions.go @@ -5,10 +5,10 @@ import ( "fmt" "strings" - "github.com/LoveLosita/smartflow/backend/model" memoryrepo "github.com/LoveLosita/smartflow/backend/services/memory/internal/repo" memoryutils "github.com/LoveLosita/smartflow/backend/services/memory/internal/utils" memorymodel "github.com/LoveLosita/smartflow/backend/services/memory/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" ) // ApplyActionOutcome 是单个决策动作的执行结果。 diff --git a/backend/services/memory/internal/worker/decision_flow.go b/backend/services/memory/internal/worker/decision_flow.go index 55682b9..bf6294d 100644 --- a/backend/services/memory/internal/worker/decision_flow.go +++ b/backend/services/memory/internal/worker/decision_flow.go @@ -4,11 +4,11 @@ import ( "context" "fmt" - "github.com/LoveLosita/smartflow/backend/model" memoryrepo "github.com/LoveLosita/smartflow/backend/services/memory/internal/repo" memoryutils "github.com/LoveLosita/smartflow/backend/services/memory/internal/utils" memorymodel "github.com/LoveLosita/smartflow/backend/services/memory/model" ragservice "github.com/LoveLosita/smartflow/backend/services/rag" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "gorm.io/gorm" ) diff --git a/backend/services/memory/internal/worker/runner.go b/backend/services/memory/internal/worker/runner.go index cef87be..96c22d6 100644 --- a/backend/services/memory/internal/worker/runner.go +++ b/backend/services/memory/internal/worker/runner.go @@ -9,7 +9,6 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/model" memoryorchestrator "github.com/LoveLosita/smartflow/backend/services/memory/internal/orchestrator" memoryrepo "github.com/LoveLosita/smartflow/backend/services/memory/internal/repo" memoryutils "github.com/LoveLosita/smartflow/backend/services/memory/internal/utils" @@ -17,6 +16,7 @@ import ( memorymodel "github.com/LoveLosita/smartflow/backend/services/memory/model" memoryobserve "github.com/LoveLosita/smartflow/backend/services/memory/observe" ragservice "github.com/LoveLosita/smartflow/backend/services/rag" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "gorm.io/gorm" ) diff --git a/backend/services/memory/module.go b/backend/services/memory/module.go index 2183f58..4e311f3 100644 --- a/backend/services/memory/module.go +++ b/backend/services/memory/module.go @@ -5,7 +5,6 @@ import ( "errors" "log" - "github.com/LoveLosita/smartflow/backend/model" llmservice "github.com/LoveLosita/smartflow/backend/services/llm" memorycleanup "github.com/LoveLosita/smartflow/backend/services/memory/internal/cleanup" memoryorchestrator "github.com/LoveLosita/smartflow/backend/services/memory/internal/orchestrator" @@ -16,6 +15,7 @@ import ( memorymodel "github.com/LoveLosita/smartflow/backend/services/memory/model" memoryobserve "github.com/LoveLosita/smartflow/backend/services/memory/observe" ragservice "github.com/LoveLosita/smartflow/backend/services/rag" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "gorm.io/gorm" ) diff --git a/backend/services/memory/rpc/errors.go b/backend/services/memory/rpc/errors.go index c9df506..881bccc 100644 --- a/backend/services/memory/rpc/errors.go +++ b/backend/services/memory/rpc/errors.go @@ -5,7 +5,7 @@ import ( "log" "strings" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/shared/respond" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/backend/services/memory/rpc/handler.go b/backend/services/memory/rpc/handler.go index e3a5f66..73caac0 100644 --- a/backend/services/memory/rpc/handler.go +++ b/backend/services/memory/rpc/handler.go @@ -4,10 +4,10 @@ import ( "context" "encoding/json" - "github.com/LoveLosita/smartflow/backend/respond" "github.com/LoveLosita/smartflow/backend/services/memory/rpc/pb" memorysv "github.com/LoveLosita/smartflow/backend/services/memory/sv" memorycontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/memory" + "github.com/LoveLosita/smartflow/backend/shared/respond" ) type Handler struct { diff --git a/backend/services/memory/sv/service.go b/backend/services/memory/sv/service.go index 2626e41..aafca1b 100644 --- a/backend/services/memory/sv/service.go +++ b/backend/services/memory/sv/service.go @@ -5,13 +5,13 @@ import ( "errors" "log" - kafkabus "github.com/LoveLosita/smartflow/backend/infra/kafka" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" - coremodel "github.com/LoveLosita/smartflow/backend/model" - eventsvc "github.com/LoveLosita/smartflow/backend/service/events" memorymodule "github.com/LoveLosita/smartflow/backend/services/memory" memorymodel "github.com/LoveLosita/smartflow/backend/services/memory/model" + eventsvc "github.com/LoveLosita/smartflow/backend/services/runtime/eventsvc" + coremodel "github.com/LoveLosita/smartflow/backend/services/runtime/model" memorycontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/memory" + kafkabus "github.com/LoveLosita/smartflow/backend/shared/infra/kafka" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" ) // Service 是 memory 独立进程的服务门面。 diff --git a/backend/services/notification/dao/connect.go b/backend/services/notification/dao/connect.go index 6d91c81..78827b6 100644 --- a/backend/services/notification/dao/connect.go +++ b/backend/services/notification/dao/connect.go @@ -3,11 +3,10 @@ package dao import ( "fmt" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" - coremodel "github.com/LoveLosita/smartflow/backend/model" notificationmodel "github.com/LoveLosita/smartflow/backend/services/notification/model" - "github.com/spf13/viper" - "gorm.io/driver/mysql" + coremodel "github.com/LoveLosita/smartflow/backend/services/runtime/model" + mysqlinfra "github.com/LoveLosita/smartflow/backend/shared/infra/mysql" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" "gorm.io/gorm" ) @@ -18,18 +17,7 @@ import ( // 2. 不迁移主动调度、agent、userauth 或其它服务表; // 3. 返回的 *gorm.DB 供 notification 服务内 DAO 和 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{}) + db, err := mysqlinfra.OpenDBFromConfig() if err != nil { return nil, err } diff --git a/backend/services/notification/rpc/errors.go b/backend/services/notification/rpc/errors.go index c341358..d39712b 100644 --- a/backend/services/notification/rpc/errors.go +++ b/backend/services/notification/rpc/errors.go @@ -5,7 +5,7 @@ import ( "log" "strings" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/shared/respond" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/backend/services/notification/rpc/handler.go b/backend/services/notification/rpc/handler.go index 0498830..4af11a3 100644 --- a/backend/services/notification/rpc/handler.go +++ b/backend/services/notification/rpc/handler.go @@ -5,10 +5,10 @@ import ( "errors" "time" - "github.com/LoveLosita/smartflow/backend/respond" "github.com/LoveLosita/smartflow/backend/services/notification/rpc/pb" notificationsv "github.com/LoveLosita/smartflow/backend/services/notification/sv" contracts "github.com/LoveLosita/smartflow/backend/shared/contracts/notification" + "github.com/LoveLosita/smartflow/backend/shared/respond" ) type Handler struct { diff --git a/backend/services/notification/sv/channel.go b/backend/services/notification/sv/channel.go index 85d3e03..f12a6bc 100644 --- a/backend/services/notification/sv/channel.go +++ b/backend/services/notification/sv/channel.go @@ -5,10 +5,10 @@ import ( "errors" "strings" - "github.com/LoveLosita/smartflow/backend/respond" notificationfeishu "github.com/LoveLosita/smartflow/backend/services/notification/internal/feishu" notificationmodel "github.com/LoveLosita/smartflow/backend/services/notification/model" contracts "github.com/LoveLosita/smartflow/backend/shared/contracts/notification" + "github.com/LoveLosita/smartflow/backend/shared/respond" "gorm.io/gorm" ) diff --git a/backend/services/notification/sv/outbox.go b/backend/services/notification/sv/outbox.go index 85d2a3f..df56729 100644 --- a/backend/services/notification/sv/outbox.go +++ b/backend/services/notification/sv/outbox.go @@ -7,9 +7,9 @@ import ( "log" "strings" - kafkabus "github.com/LoveLosita/smartflow/backend/infra/kafka" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" sharedevents "github.com/LoveLosita/smartflow/backend/shared/events" + kafkabus "github.com/LoveLosita/smartflow/backend/shared/infra/kafka" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" ) // OutboxBus 是 notification 服务注册消费 handler 需要的最小总线接口。 diff --git a/backend/conv/agent.go b/backend/services/runtime/conv/agent.go similarity index 94% rename from backend/conv/agent.go rename to backend/services/runtime/conv/agent.go index eb5ea97..c27570c 100644 --- a/backend/conv/agent.go +++ b/backend/services/runtime/conv/agent.go @@ -1,7 +1,7 @@ package conv import ( - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "github.com/cloudwego/eino/schema" ) diff --git a/backend/conv/schedule.go b/backend/services/runtime/conv/schedule.go similarity index 99% rename from backend/conv/schedule.go rename to backend/services/runtime/conv/schedule.go index ccf002e..eed8dab 100644 --- a/backend/conv/schedule.go +++ b/backend/services/runtime/conv/schedule.go @@ -3,7 +3,7 @@ package conv import ( "fmt" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" ) import "sort" diff --git a/backend/conv/task-class.go b/backend/services/runtime/conv/task-class.go similarity index 98% rename from backend/conv/task-class.go rename to backend/services/runtime/conv/task-class.go index 954a28c..4cc2052 100644 --- a/backend/conv/task-class.go +++ b/backend/services/runtime/conv/task-class.go @@ -4,8 +4,8 @@ import ( "errors" "time" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + "github.com/LoveLosita/smartflow/backend/shared/respond" ) const dateLayout = "2006-01-02" diff --git a/backend/conv/task.go b/backend/services/runtime/conv/task.go similarity index 97% rename from backend/conv/task.go rename to backend/services/runtime/conv/task.go index 8a524fd..1f05c71 100644 --- a/backend/conv/task.go +++ b/backend/services/runtime/conv/task.go @@ -3,7 +3,7 @@ package conv import ( "time" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" ) func UserAddTaskRequestToModel(request *model.UserAddTaskRequest, userID int) *model.Task { diff --git a/backend/conv/time.go b/backend/services/runtime/conv/time.go similarity index 98% rename from backend/conv/time.go rename to backend/services/runtime/conv/time.go index cd6713e..8ac6a38 100644 --- a/backend/conv/time.go +++ b/backend/services/runtime/conv/time.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/shared/respond" "github.com/spf13/viper" ) diff --git a/backend/dao/active_schedule.go b/backend/services/runtime/dao/active_schedule.go similarity index 99% rename from backend/dao/active_schedule.go rename to backend/services/runtime/dao/active_schedule.go index ff85140..a28e51c 100644 --- a/backend/dao/active_schedule.go +++ b/backend/services/runtime/dao/active_schedule.go @@ -5,7 +5,7 @@ import ( "errors" "time" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "gorm.io/gorm" "gorm.io/gorm/clause" ) diff --git a/backend/dao/active_schedule_session.go b/backend/services/runtime/dao/active_schedule_session.go similarity index 99% rename from backend/dao/active_schedule_session.go rename to backend/services/runtime/dao/active_schedule_session.go index e3a3f9f..236100c 100644 --- a/backend/dao/active_schedule_session.go +++ b/backend/services/runtime/dao/active_schedule_session.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "gorm.io/gorm" "gorm.io/gorm/clause" ) diff --git a/backend/dao/agent-cache.go b/backend/services/runtime/dao/agent-cache.go similarity index 100% rename from backend/dao/agent-cache.go rename to backend/services/runtime/dao/agent-cache.go diff --git a/backend/dao/agent.go b/backend/services/runtime/dao/agent.go similarity index 99% rename from backend/dao/agent.go rename to backend/services/runtime/dao/agent.go index 62dd26f..bb75fa5 100644 --- a/backend/dao/agent.go +++ b/backend/services/runtime/dao/agent.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "gorm.io/gorm" "gorm.io/gorm/clause" ) diff --git a/backend/dao/agent_schedule_state.go b/backend/services/runtime/dao/agent_schedule_state.go similarity index 99% rename from backend/dao/agent_schedule_state.go rename to backend/services/runtime/dao/agent_schedule_state.go index bc040d6..842a753 100644 --- a/backend/dao/agent_schedule_state.go +++ b/backend/services/runtime/dao/agent_schedule_state.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "gorm.io/gorm" "gorm.io/gorm/clause" ) diff --git a/backend/dao/agent_state_store_adapter.go b/backend/services/runtime/dao/agent_state_store_adapter.go similarity index 100% rename from backend/dao/agent_state_store_adapter.go rename to backend/services/runtime/dao/agent_state_store_adapter.go diff --git a/backend/dao/agent_timeline.go b/backend/services/runtime/dao/agent_timeline.go similarity index 97% rename from backend/dao/agent_timeline.go rename to backend/services/runtime/dao/agent_timeline.go index 53918ea..0977592 100644 --- a/backend/dao/agent_timeline.go +++ b/backend/services/runtime/dao/agent_timeline.go @@ -5,7 +5,7 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" ) // SaveConversationTimelineEvent 持久化单条会话时间线事件到 MySQL。 diff --git a/backend/dao/base.go b/backend/services/runtime/dao/base.go similarity index 100% rename from backend/dao/base.go rename to backend/services/runtime/dao/base.go diff --git a/backend/dao/cache.go b/backend/services/runtime/dao/cache.go similarity index 99% rename from backend/dao/cache.go rename to backend/services/runtime/dao/cache.go index 257d5e0..28b9b4b 100644 --- a/backend/dao/cache.go +++ b/backend/services/runtime/dao/cache.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "github.com/go-redis/redis/v8" ) diff --git a/backend/dao/course.go b/backend/services/runtime/dao/course.go similarity index 95% rename from backend/dao/course.go rename to backend/services/runtime/dao/course.go index 30782b5..92bfc6d 100644 --- a/backend/dao/course.go +++ b/backend/services/runtime/dao/course.go @@ -3,7 +3,7 @@ package dao import ( "context" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "gorm.io/gorm" ) diff --git a/backend/dao/schedule.go b/backend/services/runtime/dao/schedule.go similarity index 99% rename from backend/dao/schedule.go rename to backend/services/runtime/dao/schedule.go index 9e6bf97..1e438e1 100644 --- a/backend/dao/schedule.go +++ b/backend/services/runtime/dao/schedule.go @@ -6,8 +6,8 @@ import ( "fmt" "time" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + "github.com/LoveLosita/smartflow/backend/shared/respond" "gorm.io/gorm" ) diff --git a/backend/dao/task-class.go b/backend/services/runtime/dao/task-class.go similarity index 98% rename from backend/dao/task-class.go rename to backend/services/runtime/dao/task-class.go index 37f132f..f60bf32 100644 --- a/backend/dao/task-class.go +++ b/backend/services/runtime/dao/task-class.go @@ -4,8 +4,8 @@ import ( "context" "errors" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + "github.com/LoveLosita/smartflow/backend/shared/respond" "gorm.io/gorm" ) diff --git a/backend/dao/task.go b/backend/services/runtime/dao/task.go similarity index 95% rename from backend/dao/task.go rename to backend/services/runtime/dao/task.go index ac8ec98..9c1bb0e 100644 --- a/backend/dao/task.go +++ b/backend/services/runtime/dao/task.go @@ -5,8 +5,8 @@ import ( "errors" "time" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + "github.com/LoveLosita/smartflow/backend/shared/respond" "gorm.io/gorm" ) @@ -233,10 +233,10 @@ func (dao *TaskDAO) PromoteTaskUrgencyByIDs(ctx context.Context, userID int, tas // 3. 使用 Model(&model.Task{UserID: userID}) 让 cache_deleter 回调拿到 user_id。 // // 返回语义: -// 1. *model.Task:更新后的完整任务快照; -// 2. error: -// 2.1 gorm.ErrRecordNotFound:任务不存在或不属于当前用户; -// 2.2 其他 error:数据库异常。 +// 1. *model.Task:更新后的完整任务快照; +// 2. error: +// 2.1 gorm.ErrRecordNotFound:任务不存在或不属于当前用户; +// 2.2 其他 error:数据库异常。 func (dao *TaskDAO) UpdateTaskByID(ctx context.Context, userID int, taskID int, updates map[string]interface{}) (*model.Task, error) { // 1. 参数兜底:非法参数直接返回"记录不存在"语义。 if userID <= 0 || taskID <= 0 { @@ -282,10 +282,10 @@ func (dao *TaskDAO) UpdateTaskByID(ctx context.Context, userID int, taskID int, // 3. 不负责级联清理日程(tasks 与 schedule_events 无直接外键关联)。 // // 返回语义: -// 1. *model.Task:被删除的任务快照(用于响应前端); -// 2. error: -// 2.1 gorm.ErrRecordNotFound:任务不存在或不属于当前用户; -// 2.2 其他 error:数据库异常。 +// 1. *model.Task:被删除的任务快照(用于响应前端); +// 2. error: +// 2.1 gorm.ErrRecordNotFound:任务不存在或不属于当前用户; +// 2.2 其他 error:数据库异常。 func (dao *TaskDAO) DeleteTaskByID(ctx context.Context, userID int, taskID int) (*model.Task, error) { // 1. 参数兜底。 if userID <= 0 || taskID <= 0 { diff --git a/backend/service/events/active_schedule_triggered.go b/backend/services/runtime/eventsvc/active_schedule_triggered.go similarity index 95% rename from backend/service/events/active_schedule_triggered.go rename to backend/services/runtime/eventsvc/active_schedule_triggered.go index a469a52..62d626b 100644 --- a/backend/service/events/active_schedule_triggered.go +++ b/backend/services/runtime/eventsvc/active_schedule_triggered.go @@ -1,4 +1,4 @@ -package events +package eventsvc import ( "context" @@ -7,9 +7,9 @@ import ( "fmt" "strings" - kafkabus "github.com/LoveLosita/smartflow/backend/infra/kafka" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" sharedevents "github.com/LoveLosita/smartflow/backend/shared/events" + kafkabus "github.com/LoveLosita/smartflow/backend/shared/infra/kafka" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" "gorm.io/gorm" ) diff --git a/backend/service/events/agent_state_persist.go b/backend/services/runtime/eventsvc/agent_state_persist.go similarity index 92% rename from backend/service/events/agent_state_persist.go rename to backend/services/runtime/eventsvc/agent_state_persist.go index 8bdf845..6876a0f 100644 --- a/backend/service/events/agent_state_persist.go +++ b/backend/services/runtime/eventsvc/agent_state_persist.go @@ -1,4 +1,4 @@ -package events +package eventsvc import ( "context" @@ -6,11 +6,11 @@ import ( "errors" "log" - "github.com/LoveLosita/smartflow/backend/dao" - kafkabus "github.com/LoveLosita/smartflow/backend/infra/kafka" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" - "github.com/LoveLosita/smartflow/backend/model" agentmodel "github.com/LoveLosita/smartflow/backend/services/agent/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/dao" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + kafkabus "github.com/LoveLosita/smartflow/backend/shared/infra/kafka" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" "gorm.io/gorm" "gorm.io/gorm/clause" ) diff --git a/backend/service/events/agent_timeline_persist.go b/backend/services/runtime/eventsvc/agent_timeline_persist.go similarity index 97% rename from backend/service/events/agent_timeline_persist.go rename to backend/services/runtime/eventsvc/agent_timeline_persist.go index 57abf93..4302535 100644 --- a/backend/service/events/agent_timeline_persist.go +++ b/backend/services/runtime/eventsvc/agent_timeline_persist.go @@ -1,4 +1,4 @@ -package events +package eventsvc import ( "context" @@ -8,10 +8,10 @@ import ( "log" "strings" - "github.com/LoveLosita/smartflow/backend/dao" - kafkabus "github.com/LoveLosita/smartflow/backend/infra/kafka" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/dao" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + kafkabus "github.com/LoveLosita/smartflow/backend/shared/infra/kafka" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" "gorm.io/gorm" ) diff --git a/backend/service/events/chat_history_persist.go b/backend/services/runtime/eventsvc/chat_history_persist.go similarity index 91% rename from backend/service/events/chat_history_persist.go rename to backend/services/runtime/eventsvc/chat_history_persist.go index b87e317..12e9bc3 100644 --- a/backend/service/events/chat_history_persist.go +++ b/backend/services/runtime/eventsvc/chat_history_persist.go @@ -1,4 +1,4 @@ -package events +package eventsvc import ( "context" @@ -7,11 +7,11 @@ import ( "strconv" "strings" - "github.com/LoveLosita/smartflow/backend/dao" - kafkabus "github.com/LoveLosita/smartflow/backend/infra/kafka" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/dao" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" contracts "github.com/LoveLosita/smartflow/backend/shared/contracts/userauth" + kafkabus "github.com/LoveLosita/smartflow/backend/shared/infra/kafka" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" "github.com/LoveLosita/smartflow/backend/shared/ports" "gorm.io/gorm" ) diff --git a/backend/service/events/chat_token_usage_adjust.go b/backend/services/runtime/eventsvc/chat_token_usage_adjust.go similarity index 93% rename from backend/service/events/chat_token_usage_adjust.go rename to backend/services/runtime/eventsvc/chat_token_usage_adjust.go index cc291d2..5954429 100644 --- a/backend/service/events/chat_token_usage_adjust.go +++ b/backend/services/runtime/eventsvc/chat_token_usage_adjust.go @@ -1,4 +1,4 @@ -package events +package eventsvc import ( "context" @@ -8,11 +8,11 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/dao" - kafkabus "github.com/LoveLosita/smartflow/backend/infra/kafka" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/dao" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" contracts "github.com/LoveLosita/smartflow/backend/shared/contracts/userauth" + kafkabus "github.com/LoveLosita/smartflow/backend/shared/infra/kafka" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" "github.com/LoveLosita/smartflow/backend/shared/ports" "gorm.io/gorm" ) diff --git a/backend/service/events/core_outbox_handlers.go b/backend/services/runtime/eventsvc/core_outbox_handlers.go similarity index 97% rename from backend/service/events/core_outbox_handlers.go rename to backend/services/runtime/eventsvc/core_outbox_handlers.go index 724af98..6b742c1 100644 --- a/backend/service/events/core_outbox_handlers.go +++ b/backend/services/runtime/eventsvc/core_outbox_handlers.go @@ -1,12 +1,12 @@ -package events +package eventsvc import ( "errors" - "github.com/LoveLosita/smartflow/backend/dao" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" "github.com/LoveLosita/smartflow/backend/services/memory" + "github.com/LoveLosita/smartflow/backend/services/runtime/dao" sharedevents "github.com/LoveLosita/smartflow/backend/shared/events" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" "github.com/LoveLosita/smartflow/backend/shared/ports" ) diff --git a/backend/service/events/memory_extract_requested.go b/backend/services/runtime/eventsvc/memory_extract_requested.go similarity index 97% rename from backend/service/events/memory_extract_requested.go rename to backend/services/runtime/eventsvc/memory_extract_requested.go index 28d4cf2..cfcf41d 100644 --- a/backend/service/events/memory_extract_requested.go +++ b/backend/services/runtime/eventsvc/memory_extract_requested.go @@ -1,4 +1,4 @@ -package events +package eventsvc import ( "context" @@ -11,11 +11,11 @@ import ( "strings" "time" - kafkabus "github.com/LoveLosita/smartflow/backend/infra/kafka" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" - "github.com/LoveLosita/smartflow/backend/model" "github.com/LoveLosita/smartflow/backend/services/memory" memorymodel "github.com/LoveLosita/smartflow/backend/services/memory/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + kafkabus "github.com/LoveLosita/smartflow/backend/shared/infra/kafka" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" "github.com/spf13/viper" "gorm.io/gorm" ) diff --git a/backend/service/events/outbox_bus.go b/backend/services/runtime/eventsvc/outbox_bus.go similarity index 97% rename from backend/service/events/outbox_bus.go rename to backend/services/runtime/eventsvc/outbox_bus.go index ca7cc87..ec62811 100644 --- a/backend/service/events/outbox_bus.go +++ b/backend/services/runtime/eventsvc/outbox_bus.go @@ -1,4 +1,4 @@ -package events +package eventsvc import ( "context" @@ -7,8 +7,8 @@ import ( "sort" "strings" - kafkabus "github.com/LoveLosita/smartflow/backend/infra/kafka" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" + kafkabus "github.com/LoveLosita/smartflow/backend/shared/infra/kafka" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" ) // OutboxBus 是启动侧和业务侧共享的 outbox 门面。 diff --git a/backend/service/events/outbox_handler_routes.go b/backend/services/runtime/eventsvc/outbox_handler_routes.go similarity index 96% rename from backend/service/events/outbox_handler_routes.go rename to backend/services/runtime/eventsvc/outbox_handler_routes.go index 1748ca6..e18e7b7 100644 --- a/backend/service/events/outbox_handler_routes.go +++ b/backend/services/runtime/eventsvc/outbox_handler_routes.go @@ -1,10 +1,10 @@ -package events +package eventsvc import ( "fmt" "strings" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" ) // outboxHandlerService 表示 outbox 路由归属的业务服务。 diff --git a/backend/service/events/task_urgency_promote.go b/backend/services/runtime/eventsvc/task_urgency_promote.go similarity index 94% rename from backend/service/events/task_urgency_promote.go rename to backend/services/runtime/eventsvc/task_urgency_promote.go index 6b2bf17..51fb0d2 100644 --- a/backend/service/events/task_urgency_promote.go +++ b/backend/services/runtime/eventsvc/task_urgency_promote.go @@ -1,4 +1,4 @@ -package events +package eventsvc import ( "context" @@ -8,10 +8,10 @@ import ( "strconv" "time" - "github.com/LoveLosita/smartflow/backend/dao" - kafkabus "github.com/LoveLosita/smartflow/backend/infra/kafka" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/dao" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + kafkabus "github.com/LoveLosita/smartflow/backend/shared/infra/kafka" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" "gorm.io/gorm" ) diff --git a/backend/model/active_schedule.go b/backend/services/runtime/model/active_schedule.go similarity index 100% rename from backend/model/active_schedule.go rename to backend/services/runtime/model/active_schedule.go diff --git a/backend/model/active_schedule_session.go b/backend/services/runtime/model/active_schedule_session.go similarity index 100% rename from backend/model/active_schedule_session.go rename to backend/services/runtime/model/active_schedule_session.go diff --git a/backend/model/agent.go b/backend/services/runtime/model/agent.go similarity index 100% rename from backend/model/agent.go rename to backend/services/runtime/model/agent.go diff --git a/backend/model/agent_schedule_state.go b/backend/services/runtime/model/agent_schedule_state.go similarity index 100% rename from backend/model/agent_schedule_state.go rename to backend/services/runtime/model/agent_schedule_state.go diff --git a/backend/model/agent_state_snapshot_record.go b/backend/services/runtime/model/agent_state_snapshot_record.go similarity index 100% rename from backend/model/agent_state_snapshot_record.go rename to backend/services/runtime/model/agent_state_snapshot_record.go diff --git a/backend/model/agent_timeline.go b/backend/services/runtime/model/agent_timeline.go similarity index 100% rename from backend/model/agent_timeline.go rename to backend/services/runtime/model/agent_timeline.go diff --git a/backend/model/course.go b/backend/services/runtime/model/course.go similarity index 100% rename from backend/model/course.go rename to backend/services/runtime/model/course.go diff --git a/backend/model/course_parse.go b/backend/services/runtime/model/course_parse.go similarity index 100% rename from backend/model/course_parse.go rename to backend/services/runtime/model/course_parse.go diff --git a/backend/model/memory.go b/backend/services/runtime/model/memory.go similarity index 100% rename from backend/model/memory.go rename to backend/services/runtime/model/memory.go diff --git a/backend/model/memory_manage.go b/backend/services/runtime/model/memory_manage.go similarity index 100% rename from backend/model/memory_manage.go rename to backend/services/runtime/model/memory_manage.go diff --git a/backend/model/outbox.go b/backend/services/runtime/model/outbox.go similarity index 100% rename from backend/model/outbox.go rename to backend/services/runtime/model/outbox.go diff --git a/backend/model/schedule.go b/backend/services/runtime/model/schedule.go similarity index 100% rename from backend/model/schedule.go rename to backend/services/runtime/model/schedule.go diff --git a/backend/model/task-class.go b/backend/services/runtime/model/task-class.go similarity index 100% rename from backend/model/task-class.go rename to backend/services/runtime/model/task-class.go diff --git a/backend/model/task.go b/backend/services/runtime/model/task.go similarity index 100% rename from backend/model/task.go rename to backend/services/runtime/model/task.go diff --git a/backend/services/schedule/core/applyadapter/adapter.go b/backend/services/schedule/core/applyadapter/adapter.go index 281ec89..ea662fc 100644 --- a/backend/services/schedule/core/applyadapter/adapter.go +++ b/backend/services/schedule/core/applyadapter/adapter.go @@ -8,8 +8,8 @@ import ( "strconv" "strings" - "github.com/LoveLosita/smartflow/backend/conv" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/conv" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "gorm.io/gorm" "gorm.io/gorm/clause" ) diff --git a/backend/logic/smart_planning.go b/backend/services/schedule/core/planning/smart_planning.go similarity index 99% rename from backend/logic/smart_planning.go rename to backend/services/schedule/core/planning/smart_planning.go index 12b8f04..1e6dba2 100644 --- a/backend/logic/smart_planning.go +++ b/backend/services/schedule/core/planning/smart_planning.go @@ -1,11 +1,11 @@ -package logic +package planning import ( "fmt" - "github.com/LoveLosita/smartflow/backend/conv" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/services/runtime/conv" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + "github.com/LoveLosita/smartflow/backend/shared/respond" ) type slotStatus int diff --git a/backend/logic/smart_planning_test.go b/backend/services/schedule/core/planning/smart_planning_test.go similarity index 97% rename from backend/logic/smart_planning_test.go rename to backend/services/schedule/core/planning/smart_planning_test.go index 35b363b..b26c7fe 100644 --- a/backend/logic/smart_planning_test.go +++ b/backend/services/schedule/core/planning/smart_planning_test.go @@ -1,10 +1,10 @@ -package logic +package planning import ( "testing" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + "github.com/LoveLosita/smartflow/backend/shared/respond" ) // newTestGrid 创建仅用于单测的最小 grid。 diff --git a/backend/services/schedule/dao/connect.go b/backend/services/schedule/dao/connect.go index 0265039..825b5b0 100644 --- a/backend/services/schedule/dao/connect.go +++ b/backend/services/schedule/dao/connect.go @@ -1,13 +1,12 @@ package dao import ( - "context" "fmt" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + mysqlinfra "github.com/LoveLosita/smartflow/backend/shared/infra/mysql" + redisinfra "github.com/LoveLosita/smartflow/backend/shared/infra/redis" "github.com/go-redis/redis/v8" - "github.com/spf13/viper" - "gorm.io/driver/mysql" "gorm.io/gorm" ) @@ -18,18 +17,7 @@ import ( // 2. 不迁移 task、task-class、course 或 active-scheduler 的表,避免 schedule 服务越权管理其它领域; // 3. 迁移期仍检查 task/task-class 依赖表是否存在,方便启动阶段暴露部署顺序问题。 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{}) + db, err := mysqlinfra.OpenDBFromConfig() if err != nil { return nil, err } @@ -49,15 +37,7 @@ func OpenDBFromConfig() (*gorm.DB, error) { // 2. 不创建、不清理任何业务 key; // 3. Ping 失败直接返回错误,避免读缓存链路静默降级成难排查的启动问题。 func OpenRedisFromConfig() (*redis.Client, error) { - client := redis.NewClient(&redis.Options{ - Addr: viper.GetString("redis.host") + ":" + viper.GetString("redis.port"), - Password: viper.GetString("redis.password"), - DB: 0, - }) - if _, err := client.Ping(context.Background()).Result(); err != nil { - return nil, err - } - return client, nil + return redisinfra.OpenRedisFromConfig() } // ensureRuntimeDependencyTables 显式检查迁移期仍需读取或锁定的外部表。 diff --git a/backend/services/schedule/dao/facts.go b/backend/services/schedule/dao/facts.go index 58000a3..98976c0 100644 --- a/backend/services/schedule/dao/facts.go +++ b/backend/services/schedule/dao/facts.go @@ -6,8 +6,8 @@ import ( "fmt" "time" - "github.com/LoveLosita/smartflow/backend/conv" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/conv" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" schedulecontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/schedule" "gorm.io/gorm" ) diff --git a/backend/services/schedule/dao/schedule.go b/backend/services/schedule/dao/schedule.go index 9e6bf97..1e438e1 100644 --- a/backend/services/schedule/dao/schedule.go +++ b/backend/services/schedule/dao/schedule.go @@ -6,8 +6,8 @@ import ( "fmt" "time" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + "github.com/LoveLosita/smartflow/backend/shared/respond" "gorm.io/gorm" ) diff --git a/backend/services/schedule/rpc/errors.go b/backend/services/schedule/rpc/errors.go index 7d9db90..171bbc3 100644 --- a/backend/services/schedule/rpc/errors.go +++ b/backend/services/schedule/rpc/errors.go @@ -5,8 +5,8 @@ import ( "log" "strings" - "github.com/LoveLosita/smartflow/backend/respond" "github.com/LoveLosita/smartflow/backend/services/schedule/core/applyadapter" + "github.com/LoveLosita/smartflow/backend/shared/respond" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/backend/services/schedule/rpc/handler.go b/backend/services/schedule/rpc/handler.go index 927085d..bf886d1 100644 --- a/backend/services/schedule/rpc/handler.go +++ b/backend/services/schedule/rpc/handler.go @@ -5,10 +5,10 @@ import ( "encoding/json" "errors" - "github.com/LoveLosita/smartflow/backend/respond" "github.com/LoveLosita/smartflow/backend/services/schedule/rpc/pb" schedulesv "github.com/LoveLosita/smartflow/backend/services/schedule/sv" schedulecontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/schedule" + "github.com/LoveLosita/smartflow/backend/shared/respond" ) type Handler struct { diff --git a/backend/services/schedule/sv/contracts.go b/backend/services/schedule/sv/contracts.go index f63ce10..5e7ed07 100644 --- a/backend/services/schedule/sv/contracts.go +++ b/backend/services/schedule/sv/contracts.go @@ -4,10 +4,10 @@ import ( "context" "errors" - rootmodel "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" + rootmodel "github.com/LoveLosita/smartflow/backend/services/runtime/model" "github.com/LoveLosita/smartflow/backend/services/schedule/core/applyadapter" schedulecontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/schedule" + "github.com/LoveLosita/smartflow/backend/shared/respond" ) // DeleteScheduleEventByContract 把跨进程删除契约转换为既有 schedule 核心逻辑入参。 diff --git a/backend/services/schedule/sv/service.go b/backend/services/schedule/sv/service.go index 441bcb7..d497681 100644 --- a/backend/services/schedule/sv/service.go +++ b/backend/services/schedule/sv/service.go @@ -8,13 +8,13 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/conv" - rootdao "github.com/LoveLosita/smartflow/backend/dao" - "github.com/LoveLosita/smartflow/backend/logic" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/services/runtime/conv" + rootdao "github.com/LoveLosita/smartflow/backend/services/runtime/dao" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "github.com/LoveLosita/smartflow/backend/services/schedule/core/applyadapter" + "github.com/LoveLosita/smartflow/backend/services/schedule/core/planning" scheduledao "github.com/LoveLosita/smartflow/backend/services/schedule/dao" + "github.com/LoveLosita/smartflow/backend/shared/respond" "github.com/go-redis/redis/v8" ) @@ -407,7 +407,7 @@ func (ss *ScheduleService) SmartPlanning(ctx context.Context, userID, taskClassI return nil, err } //4.将多个周的信息传入智能排课算法,获取推荐的时间安排(周+周内的天+节次) - result, err := logic.SmartPlanningMainLogic(schedules, taskClass) + result, err := planning.SmartPlanningMainLogic(schedules, taskClass) if err != nil { return nil, err } @@ -441,7 +441,7 @@ func (ss *ScheduleService) SmartPlanningRaw(ctx context.Context, userID, taskCla } // 3. 执行粗排算法,拿到已分配的 items(EmbeddedTime 已回填)。 - allocatedItems, err := logic.SmartPlanningRawItems(schedules, taskClass) + allocatedItems, err := planning.SmartPlanningRawItems(schedules, taskClass) if err != nil { return nil, nil, err } @@ -502,7 +502,7 @@ func (ss *ScheduleService) SmartPlanningMultiRaw(ctx context.Context, userID int } // 5. 执行多任务类粗排(共享资源池 + 增量占位)。 - allocatedItems, err := logic.SmartPlanningRawItemsMulti(schedules, orderedTaskClasses) + allocatedItems, err := planning.SmartPlanningRawItemsMulti(schedules, orderedTaskClasses) if err != nil { return nil, nil, err } @@ -692,7 +692,7 @@ func (ss *ScheduleService) HybridScheduleWithPlan( } // 3. 执行粗排。 - allocatedItems, err := logic.SmartPlanningRawItems(schedules, taskClass) + allocatedItems, err := planning.SmartPlanningRawItems(schedules, taskClass) if err != nil { return nil, nil, err } @@ -736,7 +736,7 @@ func (ss *ScheduleService) HybridScheduleWithPlanMulti( } // 4. 多任务类粗排。 - allocatedItems, err := logic.SmartPlanningRawItemsMulti(schedules, orderedTaskClasses) + allocatedItems, err := planning.SmartPlanningRawItemsMulti(schedules, orderedTaskClasses) if err != nil { return nil, nil, err } diff --git a/backend/services/task/dao/connect.go b/backend/services/task/dao/connect.go index f5010a1..c48056e 100644 --- a/backend/services/task/dao/connect.go +++ b/backend/services/task/dao/connect.go @@ -1,14 +1,13 @@ package dao import ( - "context" "fmt" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + mysqlinfra "github.com/LoveLosita/smartflow/backend/shared/infra/mysql" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" + redisinfra "github.com/LoveLosita/smartflow/backend/shared/infra/redis" "github.com/go-redis/redis/v8" - "github.com/spf13/viper" - "gorm.io/driver/mysql" "gorm.io/gorm" ) @@ -19,18 +18,7 @@ import ( // 2. 不迁移 active-scheduler、schedule、course 或 task-class 表; // 3. 迁移期仍检查 active_schedule_jobs 是否存在,因为 task 写入后还会 best-effort 同步 due job。 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{}) + db, err := mysqlinfra.OpenDBFromConfig() if err != nil { return nil, err } @@ -53,15 +41,7 @@ func OpenDBFromConfig() (*gorm.DB, error) { // 2. 不清理任何业务 key; // 3. Ping 失败直接返回错误,避免缓存链路静默降级。 func OpenRedisFromConfig() (*redis.Client, error) { - client := redis.NewClient(&redis.Options{ - Addr: viper.GetString("redis.host") + ":" + viper.GetString("redis.port"), - Password: viper.GetString("redis.password"), - DB: 0, - }) - if _, err := client.Ping(context.Background()).Result(); err != nil { - return nil, err - } - return client, nil + return redisinfra.OpenRedisFromConfig() } // autoMigrateTaskOutboxTable 只迁移 task 服务自己的 outbox 物理表。 diff --git a/backend/services/task/dao/task.go b/backend/services/task/dao/task.go index e89d9b7..d140014 100644 --- a/backend/services/task/dao/task.go +++ b/backend/services/task/dao/task.go @@ -5,8 +5,8 @@ import ( "errors" "time" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + "github.com/LoveLosita/smartflow/backend/shared/respond" "gorm.io/gorm" ) diff --git a/backend/services/task/rpc/errors.go b/backend/services/task/rpc/errors.go index 8e2007d..03d6db7 100644 --- a/backend/services/task/rpc/errors.go +++ b/backend/services/task/rpc/errors.go @@ -5,7 +5,7 @@ import ( "log" "strings" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/shared/respond" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/backend/services/task/rpc/handler.go b/backend/services/task/rpc/handler.go index 04e37cb..09e0579 100644 --- a/backend/services/task/rpc/handler.go +++ b/backend/services/task/rpc/handler.go @@ -5,11 +5,11 @@ import ( "encoding/json" "errors" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "github.com/LoveLosita/smartflow/backend/services/task/rpc/pb" tasksv "github.com/LoveLosita/smartflow/backend/services/task/sv" taskcontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/task" + "github.com/LoveLosita/smartflow/backend/shared/respond" ) type Handler struct { diff --git a/backend/services/task/sv/outbox.go b/backend/services/task/sv/outbox.go index 1563268..d7ce9d4 100644 --- a/backend/services/task/sv/outbox.go +++ b/backend/services/task/sv/outbox.go @@ -8,10 +8,10 @@ import ( "strconv" "time" - kafkabus "github.com/LoveLosita/smartflow/backend/infra/kafka" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" taskdao "github.com/LoveLosita/smartflow/backend/services/task/dao" + kafkabus "github.com/LoveLosita/smartflow/backend/shared/infra/kafka" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" "gorm.io/gorm" ) diff --git a/backend/services/task/sv/service.go b/backend/services/task/sv/service.go index 0a1701d..3f335d5 100644 --- a/backend/services/task/sv/service.go +++ b/backend/services/task/sv/service.go @@ -7,13 +7,13 @@ import ( "log" "time" - "github.com/LoveLosita/smartflow/backend/conv" - rootdao "github.com/LoveLosita/smartflow/backend/dao" - outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/services/runtime/conv" + rootdao "github.com/LoveLosita/smartflow/backend/services/runtime/dao" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" taskdao "github.com/LoveLosita/smartflow/backend/services/task/dao" taskcontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/task" + outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox" + "github.com/LoveLosita/smartflow/backend/shared/respond" "github.com/go-redis/redis/v8" "gorm.io/gorm" ) diff --git a/backend/services/task/sv/task_active_schedule.go b/backend/services/task/sv/task_active_schedule.go index 199588f..549a766 100644 --- a/backend/services/task/sv/task_active_schedule.go +++ b/backend/services/task/sv/task_active_schedule.go @@ -7,7 +7,7 @@ import ( "log" "time" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "gorm.io/gorm" ) diff --git a/backend/services/task_class/dao/connect.go b/backend/services/task_class/dao/connect.go index 64d2e65..e34b62e 100644 --- a/backend/services/task_class/dao/connect.go +++ b/backend/services/task_class/dao/connect.go @@ -1,13 +1,12 @@ package dao import ( - "context" "fmt" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + mysqlinfra "github.com/LoveLosita/smartflow/backend/shared/infra/mysql" + redisinfra "github.com/LoveLosita/smartflow/backend/shared/infra/redis" "github.com/go-redis/redis/v8" - "github.com/spf13/viper" - "gorm.io/driver/mysql" "gorm.io/gorm" ) @@ -18,18 +17,7 @@ import ( // 2. 不迁移 schedule_events / schedules,迁移期只检查它们是否存在; // 3. 迁移期允许 task-class 继续直写 schedule 表,以保留原本本地事务语义。 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{}) + db, err := mysqlinfra.OpenDBFromConfig() if err != nil { return nil, err } @@ -49,15 +37,7 @@ func OpenDBFromConfig() (*gorm.DB, error) { // 2. 不清理任何业务 key; // 3. Ping 失败直接返回错误,避免服务启动后才暴露缓存不可用。 func OpenRedisFromConfig() (*redis.Client, error) { - client := redis.NewClient(&redis.Options{ - Addr: viper.GetString("redis.host") + ":" + viper.GetString("redis.port"), - Password: viper.GetString("redis.password"), - DB: 0, - }) - if _, err := client.Ping(context.Background()).Result(); err != nil { - return nil, err - } - return client, nil + return redisinfra.OpenRedisFromConfig() } // ensureRuntimeDependencyTables 显式检查 task-class 迁移期仍直写的外部表。 diff --git a/backend/services/task_class/dao/task_class.go b/backend/services/task_class/dao/task_class.go index dd8fe17..677e09b 100644 --- a/backend/services/task_class/dao/task_class.go +++ b/backend/services/task_class/dao/task_class.go @@ -4,8 +4,8 @@ import ( "context" "errors" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + "github.com/LoveLosita/smartflow/backend/shared/respond" "gorm.io/gorm" ) diff --git a/backend/services/task_class/rpc/errors.go b/backend/services/task_class/rpc/errors.go index bb091c1..e5a36dc 100644 --- a/backend/services/task_class/rpc/errors.go +++ b/backend/services/task_class/rpc/errors.go @@ -5,7 +5,7 @@ import ( "log" "strings" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/shared/respond" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/backend/services/task_class/rpc/handler.go b/backend/services/task_class/rpc/handler.go index 97e8bca..190a342 100644 --- a/backend/services/task_class/rpc/handler.go +++ b/backend/services/task_class/rpc/handler.go @@ -5,11 +5,11 @@ import ( "encoding/json" "errors" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "github.com/LoveLosita/smartflow/backend/services/task_class/rpc/pb" taskclasssv "github.com/LoveLosita/smartflow/backend/services/task_class/sv" taskclasscontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/taskclass" + "github.com/LoveLosita/smartflow/backend/shared/respond" ) const ( diff --git a/backend/services/task_class/sv/service.go b/backend/services/task_class/sv/service.go index 66b104c..36089c3 100644 --- a/backend/services/task_class/sv/service.go +++ b/backend/services/task_class/sv/service.go @@ -8,12 +8,12 @@ import ( "sort" "time" - "github.com/LoveLosita/smartflow/backend/conv" - rootdao "github.com/LoveLosita/smartflow/backend/dao" - "github.com/LoveLosita/smartflow/backend/model" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/services/runtime/conv" + rootdao "github.com/LoveLosita/smartflow/backend/services/runtime/dao" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" taskclassdao "github.com/LoveLosita/smartflow/backend/services/task_class/dao" taskclasscontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/taskclass" + "github.com/LoveLosita/smartflow/backend/shared/respond" "github.com/go-redis/redis/v8" "gorm.io/gorm" ) diff --git a/backend/services/userauth/dao/connect.go b/backend/services/userauth/dao/connect.go index e15c5de..94678e3 100644 --- a/backend/services/userauth/dao/connect.go +++ b/backend/services/userauth/dao/connect.go @@ -1,13 +1,12 @@ package dao import ( - "context" "fmt" userauthmodel "github.com/LoveLosita/smartflow/backend/services/userauth/model" + mysqlinfra "github.com/LoveLosita/smartflow/backend/shared/infra/mysql" + redisinfra "github.com/LoveLosita/smartflow/backend/shared/infra/redis" "github.com/go-redis/redis/v8" - "github.com/spf13/viper" - "gorm.io/driver/mysql" "gorm.io/gorm" ) @@ -18,18 +17,7 @@ import ( // 2. 不负责读取业务配置之外的外部依赖,配置来源仍由 bootstrap.LoadConfig 统一注入; // 3. 返回 *gorm.DB 供服务内 DAO 复用,调用方负责进程生命周期。 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{}) + db, err := mysqlinfra.OpenDBFromConfig() if err != nil { return nil, err } @@ -43,13 +31,5 @@ func OpenDBFromConfig() (*gorm.DB, error) { // // 失败时返回 error,让独立进程入口 fail-fast,避免黑名单和额度门禁静默失效。 func OpenRedisFromConfig() (*redis.Client, error) { - client := redis.NewClient(&redis.Options{ - Addr: viper.GetString("redis.host") + ":" + viper.GetString("redis.port"), - Password: viper.GetString("redis.password"), - DB: 0, - }) - if _, err := client.Ping(context.Background()).Result(); err != nil { - return nil, err - } - return client, nil + return redisinfra.OpenRedisFromConfig() } diff --git a/backend/services/userauth/internal/auth/password.go b/backend/services/userauth/internal/auth/password.go new file mode 100644 index 0000000..3cb858f --- /dev/null +++ b/backend/services/userauth/internal/auth/password.go @@ -0,0 +1,36 @@ +package auth + +import ( + "errors" + + "golang.org/x/crypto/bcrypt" +) + +// HashPassword 负责将明文密码转换为 bcrypt 哈希结果。 +// +// 职责边界: +// 1. 只负责密码哈希,不负责用户名校验、持久化和错误翻译。 +// 2. 输入是明文密码,输出是可直接入库的 bcrypt 字符串。 +func HashPassword(pwd string) (string, error) { + hashedPwd, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(hashedPwd), nil +} + +// CompareHashPwdAndPwd 负责校验明文密码与 bcrypt 哈希是否匹配。 +// +// 职责边界: +// 1. 只负责密码比对,不负责用户存在性判断和登录态处理。 +// 2. 当密码不匹配时返回 false, nil;只有底层异常才返回非 nil error。 +func CompareHashPwdAndPwd(hashedPwd, pwd string) (bool, error) { + err := bcrypt.CompareHashAndPassword([]byte(hashedPwd), []byte(pwd)) + if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { + return false, nil + } + if err != nil { + return false, err + } + return true, nil +} diff --git a/backend/services/userauth/internal/auth/tokens.go b/backend/services/userauth/internal/auth/tokens.go index 2cebc15..a675f47 100644 --- a/backend/services/userauth/internal/auth/tokens.go +++ b/backend/services/userauth/internal/auth/tokens.go @@ -7,8 +7,8 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/respond" contracts "github.com/LoveLosita/smartflow/backend/shared/contracts/userauth" + "github.com/LoveLosita/smartflow/backend/shared/respond" "github.com/golang-jwt/jwt/v4" "github.com/google/uuid" "github.com/spf13/viper" diff --git a/backend/services/userauth/rpc/errors.go b/backend/services/userauth/rpc/errors.go index da4a934..7f9fb1e 100644 --- a/backend/services/userauth/rpc/errors.go +++ b/backend/services/userauth/rpc/errors.go @@ -5,7 +5,7 @@ import ( "log" "strings" - "github.com/LoveLosita/smartflow/backend/respond" + "github.com/LoveLosita/smartflow/backend/shared/respond" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/backend/services/userauth/rpc/handler.go b/backend/services/userauth/rpc/handler.go index 1793fb3..425ec13 100644 --- a/backend/services/userauth/rpc/handler.go +++ b/backend/services/userauth/rpc/handler.go @@ -5,10 +5,10 @@ import ( "errors" "time" - "github.com/LoveLosita/smartflow/backend/respond" "github.com/LoveLosita/smartflow/backend/services/userauth/rpc/pb" userauthsv "github.com/LoveLosita/smartflow/backend/services/userauth/sv" contracts "github.com/LoveLosita/smartflow/backend/shared/contracts/userauth" + "github.com/LoveLosita/smartflow/backend/shared/respond" ) type Handler struct { diff --git a/backend/services/userauth/sv/quota.go b/backend/services/userauth/sv/quota.go index 1bedfbc..60f46f5 100644 --- a/backend/services/userauth/sv/quota.go +++ b/backend/services/userauth/sv/quota.go @@ -7,10 +7,10 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/respond" userauthdao "github.com/LoveLosita/smartflow/backend/services/userauth/dao" userauthmodel "github.com/LoveLosita/smartflow/backend/services/userauth/model" contracts "github.com/LoveLosita/smartflow/backend/shared/contracts/userauth" + "github.com/LoveLosita/smartflow/backend/shared/respond" ) const ( diff --git a/backend/services/userauth/sv/service.go b/backend/services/userauth/sv/service.go index 2bc4275..2f30bb1 100644 --- a/backend/services/userauth/sv/service.go +++ b/backend/services/userauth/sv/service.go @@ -6,12 +6,11 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/respond" userauthdao "github.com/LoveLosita/smartflow/backend/services/userauth/dao" userauthauth "github.com/LoveLosita/smartflow/backend/services/userauth/internal/auth" userauthmodel "github.com/LoveLosita/smartflow/backend/services/userauth/model" contracts "github.com/LoveLosita/smartflow/backend/shared/contracts/userauth" - "github.com/LoveLosita/smartflow/backend/utils" + "github.com/LoveLosita/smartflow/backend/shared/respond" "gorm.io/gorm" ) @@ -74,7 +73,7 @@ func (s *Service) Register(ctx context.Context, req contracts.RegisterRequest) ( return nil, respond.InvalidName } - hashedPwd, err := utils.HashPassword(req.Password) + hashedPwd, err := userauthauth.HashPassword(req.Password) if err != nil { return nil, err } @@ -94,7 +93,7 @@ func (s *Service) Login(ctx context.Context, req contracts.LoginRequest) (*contr return nil, err } - matched, err := utils.CompareHashPwdAndPwd(hashedPwd, req.Password) + matched, err := userauthauth.CompareHashPwdAndPwd(hashedPwd, req.Password) if err != nil { return nil, err } diff --git a/backend/bootstrap/config.go b/backend/shared/infra/bootstrap/config.go similarity index 94% rename from backend/bootstrap/config.go rename to backend/shared/infra/bootstrap/config.go index 6d40391..32cae8e 100644 --- a/backend/bootstrap/config.go +++ b/backend/shared/infra/bootstrap/config.go @@ -8,9 +8,10 @@ import ( ) // LoadConfig 统一加载后端进程配置。 +// // 职责边界: -// 1. 只负责把 config.yaml 读入 viper,不解释具体业务配置语义; -// 2. 同时兼容从仓库根目录和 backend 目录启动的两种路径; +// 1. 只负责把 config.yaml 读入 viper,不解释具体业务配置语义。 +// 2. 同时兼容从仓库根目录和 backend 目录启动的两种路径。 // 3. 失败时返回 error,由各进程入口决定是否退出。 func LoadConfig() error { viper.SetConfigName("config") diff --git a/backend/inits/eino.go b/backend/shared/infra/eino/hub.go similarity index 86% rename from backend/inits/eino.go rename to backend/shared/infra/eino/hub.go index dc62a92..0790bec 100644 --- a/backend/inits/eino.go +++ b/backend/shared/infra/eino/hub.go @@ -1,4 +1,4 @@ -package inits +package einoinfra import ( "context" @@ -8,12 +8,14 @@ import ( "github.com/spf13/viper" ) +// AIHub 承载当前进程内复用的 Ark ChatModel 句柄。 type AIHub struct { Lite *ark.ChatModel Pro *ark.ChatModel Max *ark.ChatModel } +// InitEino 按统一配置创建当前进程复用的 Eino 模型句柄。 func InitEino() (*AIHub, error) { ctx := context.Background() baseURL := viper.GetString("agent.baseURL") diff --git a/backend/middleware/cache_deleter.go b/backend/shared/infra/gormcache/cache_deleter.go similarity index 97% rename from backend/middleware/cache_deleter.go rename to backend/shared/infra/gormcache/cache_deleter.go index 3c07ae3..150ebb4 100644 --- a/backend/middleware/cache_deleter.go +++ b/backend/shared/infra/gormcache/cache_deleter.go @@ -1,4 +1,4 @@ -package middleware +package gormcache import ( "context" @@ -6,8 +6,8 @@ import ( "reflect" "strings" - "github.com/LoveLosita/smartflow/backend/dao" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/dao" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "gorm.io/gorm" ) diff --git a/backend/infra/kafka/admin.go b/backend/shared/infra/kafka/admin.go similarity index 100% rename from backend/infra/kafka/admin.go rename to backend/shared/infra/kafka/admin.go diff --git a/backend/infra/kafka/config.go b/backend/shared/infra/kafka/config.go similarity index 100% rename from backend/infra/kafka/config.go rename to backend/shared/infra/kafka/config.go diff --git a/backend/infra/kafka/consumer.go b/backend/shared/infra/kafka/consumer.go similarity index 100% rename from backend/infra/kafka/consumer.go rename to backend/shared/infra/kafka/consumer.go diff --git a/backend/infra/kafka/envelope.go b/backend/shared/infra/kafka/envelope.go similarity index 100% rename from backend/infra/kafka/envelope.go rename to backend/shared/infra/kafka/envelope.go diff --git a/backend/infra/kafka/producer.go b/backend/shared/infra/kafka/producer.go similarity index 100% rename from backend/infra/kafka/producer.go rename to backend/shared/infra/kafka/producer.go diff --git a/backend/shared/infra/mysql/connect.go b/backend/shared/infra/mysql/connect.go new file mode 100644 index 0000000..8c7ae36 --- /dev/null +++ b/backend/shared/infra/mysql/connect.go @@ -0,0 +1,34 @@ +package mysqlinfra + +import ( + "fmt" + + "github.com/spf13/viper" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +// OpenDBFromConfig 只按统一配置创建 MySQL 连接。 +// +// 职责边界: +// 1. 只负责把 viper 中的 database 配置转换成 *gorm.DB。 +// 2. 不执行任何 AutoMigrate、回填或服务私有依赖检查。 +// 3. 调用方负责决定连接属于哪个服务以及后续初始化动作。 +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 + } + return db, nil +} diff --git a/backend/infra/outbox/engine.go b/backend/shared/infra/outbox/engine.go similarity index 99% rename from backend/infra/outbox/engine.go rename to backend/shared/infra/outbox/engine.go index 31227fe..6b7a1ed 100644 --- a/backend/infra/outbox/engine.go +++ b/backend/shared/infra/outbox/engine.go @@ -11,8 +11,8 @@ import ( "sync" "time" - kafkabus "github.com/LoveLosita/smartflow/backend/infra/kafka" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" + kafkabus "github.com/LoveLosita/smartflow/backend/shared/infra/kafka" segmentkafka "github.com/segmentio/kafka-go" "gorm.io/gorm" ) diff --git a/backend/infra/outbox/event_bus.go b/backend/shared/infra/outbox/event_bus.go similarity index 98% rename from backend/infra/outbox/event_bus.go rename to backend/shared/infra/outbox/event_bus.go index 5ea724a..0650e42 100644 --- a/backend/infra/outbox/event_bus.go +++ b/backend/shared/infra/outbox/event_bus.go @@ -7,7 +7,7 @@ import ( "strings" "sync" - kafkabus "github.com/LoveLosita/smartflow/backend/infra/kafka" + kafkabus "github.com/LoveLosita/smartflow/backend/shared/infra/kafka" ) // EventPublisher 是通用事件发布能力接口。 diff --git a/backend/infra/outbox/event_contract.go b/backend/shared/infra/outbox/event_contract.go similarity index 100% rename from backend/infra/outbox/event_contract.go rename to backend/shared/infra/outbox/event_contract.go diff --git a/backend/infra/outbox/repository.go b/backend/shared/infra/outbox/repository.go similarity index 99% rename from backend/infra/outbox/repository.go rename to backend/shared/infra/outbox/repository.go index f935859..e089380 100644 --- a/backend/infra/outbox/repository.go +++ b/backend/shared/infra/outbox/repository.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/LoveLosita/smartflow/backend/model" + "github.com/LoveLosita/smartflow/backend/services/runtime/model" "gorm.io/gorm" "gorm.io/gorm/clause" ) diff --git a/backend/infra/outbox/route_registry.go b/backend/shared/infra/outbox/route_registry.go similarity index 100% rename from backend/infra/outbox/route_registry.go rename to backend/shared/infra/outbox/route_registry.go diff --git a/backend/infra/outbox/service_catalog.go b/backend/shared/infra/outbox/service_catalog.go similarity index 100% rename from backend/infra/outbox/service_catalog.go rename to backend/shared/infra/outbox/service_catalog.go diff --git a/backend/infra/outbox/service_route.go b/backend/shared/infra/outbox/service_route.go similarity index 100% rename from backend/infra/outbox/service_route.go rename to backend/shared/infra/outbox/service_route.go diff --git a/backend/pkg/rate_limiter.go b/backend/shared/infra/ratelimit/rate_limiter.go similarity index 99% rename from backend/pkg/rate_limiter.go rename to backend/shared/infra/ratelimit/rate_limiter.go index 2a58dab..d0b817f 100644 --- a/backend/pkg/rate_limiter.go +++ b/backend/shared/infra/ratelimit/rate_limiter.go @@ -1,4 +1,4 @@ -package pkg +package ratelimit import ( "context" diff --git a/backend/shared/infra/redis/connect.go b/backend/shared/infra/redis/connect.go new file mode 100644 index 0000000..7b48e3d --- /dev/null +++ b/backend/shared/infra/redis/connect.go @@ -0,0 +1,26 @@ +package redisinfra + +import ( + "context" + + "github.com/go-redis/redis/v8" + "github.com/spf13/viper" +) + +// OpenRedisFromConfig 只按统一配置创建 Redis client 并做连通性校验。 +// +// 职责边界: +// 1. 只负责初始化通用 Redis 连接,不承载任何业务 key 语义。 +// 2. 只做 Ping 校验,失败时返回 error,由调用方决定是否 fail fast。 +// 3. 不创建、不预热、不清理任何缓存或分布式锁数据。 +func OpenRedisFromConfig() (*redis.Client, error) { + client := redis.NewClient(&redis.Options{ + Addr: viper.GetString("redis.host") + ":" + viper.GetString("redis.port"), + Password: viper.GetString("redis.password"), + DB: 0, + }) + if _, err := client.Ping(context.Background()).Result(); err != nil { + return nil, err + } + return client, nil +} diff --git a/backend/respond/respond.go b/backend/shared/respond/respond.go similarity index 95% rename from backend/respond/respond.go rename to backend/shared/respond/respond.go index cd6de2b..60b4943 100644 --- a/backend/respond/respond.go +++ b/backend/shared/respond/respond.go @@ -3,11 +3,8 @@ package respond import ( - "errors" "net/http" "strings" - - "github.com/gin-gonic/gin" ) type Response struct { //响应结构体 @@ -53,22 +50,6 @@ func RespWithData(response Response, data interface{}) FinalResponse { //传入 return finalResponse } -func DealWithError(c *gin.Context, err error) { //处理错误,返回对应的响应结构体 - if err == nil { - return - } - var resp Response - if errors.Is(err, UserTasksEmpty) || errors.Is(err, NoOngoingOrUpcomingSchedule) || errors.Is(err, TaskAlreadyDeleted) { - c.JSON(http.StatusOK, err) - return - } - if errors.As(err, &resp) { - c.JSON(resp.HTTPStatus(), resp) - return - } - c.JSON(http.StatusInternalServerError, InternalError(err)) -} - func InternalError(err error) Response { //服务器错误 return Response{ Status: "500", diff --git a/backend/utils/pwd_encryption.go b/backend/utils/pwd_encryption.go deleted file mode 100644 index 2e5e143..0000000 --- a/backend/utils/pwd_encryption.go +++ /dev/null @@ -1,30 +0,0 @@ -// Package utils 工具函数库 -// 包含各种通用工具函数 -package utils - -import ( - "errors" - - "golang.org/x/crypto/bcrypt" -) - -// HashPassword 用于对密码进行哈希加密 -func HashPassword(pwd string) (string, error) { - hashedPwd, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost) - if err != nil { - return "", err - } - return string(hashedPwd), nil -} - -// CompareHashPwdAndPwd 用于比较哈希密码和密码是否匹配 -func CompareHashPwdAndPwd(hashedPwd, pwd string) (bool, error) { - err := bcrypt.CompareHashAndPassword([]byte(hashedPwd), []byte(pwd)) - if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { //密码不匹配 - return false, nil - } else if err != nil { //其他错误 - return false, err - } else { //密码匹配 - return true, nil - } -} diff --git a/backend/infra/rag/HANDOFF_RAGInfra一步到位接入方案.md b/docs/backend/legacy-infra-rag-from-backend/HANDOFF_RAGInfra一步到位接入方案.md similarity index 100% rename from backend/infra/rag/HANDOFF_RAGInfra一步到位接入方案.md rename to docs/backend/legacy-infra-rag-from-backend/HANDOFF_RAGInfra一步到位接入方案.md diff --git a/backend/infra/rag/RAG复用接口实施计划.md b/docs/backend/legacy-infra-rag-from-backend/RAG复用接口实施计划.md similarity index 100% rename from backend/infra/rag/RAG复用接口实施计划.md rename to docs/backend/legacy-infra-rag-from-backend/RAG复用接口实施计划.md diff --git a/docs/backend/微服务四步迁移与第二阶段并行开发计划.md b/docs/backend/微服务四步迁移与第二阶段并行开发计划.md index 65b3b83..9ceafc3 100644 --- a/docs/backend/微服务四步迁移与第二阶段并行开发计划.md +++ b/docs/backend/微服务四步迁移与第二阶段并行开发计划.md @@ -59,7 +59,7 @@ Gin Gateway 只做边缘层职责: 3. 请求编排。 4. SSE / 流式返回。 5. 前端所需的轻量组合逻辑。 -6. API 层错误响应适配。迁移期可以继续复用 `backend/respond`,等全部服务边界稳定后再整体收进 gateway/shared。 +6. API 层错误响应适配。阶段 6 后 gateway HTTP 门面已通过 `backend/gateway/shared/respond` 统一入口复用根 `backend/respond` 语义;根 `backend/respond` 迁移期继续服务 RPC/client/旧实现,避免服务层反向依赖 gateway。 网关不再承担这些职责: @@ -73,7 +73,7 @@ Gin Gateway 只做边缘层职责: 1. `/api/v1/user/*` 由 `backend/gateway/api/userauth` 承载 HTTP 入口,核心能力通过 zrpc client 调 `cmd/userauth` zrpc。 2. `gateway/middleware` 的 JWT 鉴权和 token quota guard 只调 `userauth`,不直接读写 `users`、Redis 黑名单或额度缓存。 -3. `notification`、`active-scheduler`、`agent`、`memory` 等跨服务 zrpc client 终态统一放在 `backend/client/`;当前 `backend/gateway/client/` 是迁移期旧位置,下一轮目录收口应机械迁出。 +3. `notification`、`active-scheduler`、`agent`、`memory` 等跨服务 zrpc client 终态统一放在 `backend/client/`;当前已完成 `backend/gateway/client/` 到 `backend/client/` 的机械迁移,`backend/gateway/client` 不再作为活跃 client 位置。 4. zrpc client 不放进 `cmd`。`cmd` 只负责进程入口和装配,不承载跨服务 client 语义。 5. HTTP 门面统一放在 `backend/gateway/api`;gateway 内部可新增 `backend/gateway/shared`,只放 HTTP/SSE/bind/multipart/respond 等门面复用能力,禁止服务层 import。 @@ -99,11 +99,11 @@ gozero 服务负责领域能力: > > 当前状态:`llm-service` / `rag-service` 这两个边界已经先做成 `backend/services/*` 的服务内模块,调用仍由 `backend/cmd/start.go` 在同一进程内装配,不是 gozero 独立进程。 > -> 当前状态:`user/auth` 已经完成 go-zero zrpc 独立进程拆分,是阶段 2 样板。服务端在 `backend/services/userauth`,进程入口在 `backend/cmd/userauth`,gateway HTTP 门面在 `backend/gateway/api/userauth`,gateway client 在 `backend/gateway/client/userauth`。 +> 当前状态:`user/auth` 已经完成 go-zero zrpc 独立进程拆分,是阶段 2 样板。服务端在 `backend/services/userauth`,进程入口在 `backend/cmd/userauth`,gateway HTTP 门面在 `backend/gateway/api/userauth`,zrpc client 在 `backend/client/userauth`。 > -> 当前状态:`notification` 已经完成阶段 3 拆分。服务端在 `backend/services/notification`,进程入口在 `backend/cmd/notification`,gateway client 在 `backend/gateway/client/notification`,服务级 outbox consumer 和 retry loop 已随服务入口迁出。 +> 当前状态:`notification` 已经完成阶段 3 拆分。服务端在 `backend/services/notification`,进程入口在 `backend/cmd/notification`,zrpc client 在 `backend/client/notification`,服务级 outbox consumer 和 retry loop 已随服务入口迁出。 > -> 当前状态:`active-scheduler` 已经完成阶段 4 首轮收口。服务端在 `backend/services/active_scheduler`,进程入口在 `backend/cmd/active-scheduler`,gateway HTTP 门面在 `backend/gateway/api`,gateway client 在 `backend/gateway/client/activescheduler`。 +> 当前状态:`active-scheduler` 已经完成阶段 4 首轮收口。服务端在 `backend/services/active_scheduler`,进程入口在 `backend/cmd/active-scheduler`,gateway HTTP 门面在 `backend/gateway/api`,zrpc client 在 `backend/client/activescheduler`。 ### 3.3 事件层 @@ -319,7 +319,7 @@ flowchart LR 1. 新增 `backend/cmd/userauth/main.go` 作为 userauth 独立进程入口。 2. 新增 `backend/services/userauth/**`,承载注册、登录、刷新 token、登出、JWT 签发/校验、黑名单、token 额度治理和 token 记账幂等。 3. 新增并归档到 `backend/gateway/api/userauth/**`,承载 `/api/v1/user/register`、`/api/v1/user/login`、`/api/v1/user/refresh-token`、`/api/v1/user/logout` 的 HTTP handler。 -4. 新增并归档到 `backend/gateway/client/userauth/**`,承载 gateway 侧 zrpc client 和 gRPC 错误反解。 +4. 新增并归档到 `backend/gateway/client/userauth/**`,承载迁移期 gateway 侧 zrpc client 和 gRPC 错误反解;阶段 6 后已机械迁到 `backend/client/userauth/**`。 5. 新增 `backend/gateway/middleware/**`,把 JWT 鉴权和 token quota guard 改成调用 userauth,不再直接碰 users 表或 Redis 黑名单细节。 6. 新增 `backend/shared/contracts/userauth` 与 `backend/shared/ports`,只放跨层契约和端口接口。 7. 拆分 MySQL / Redis 初始化和 AutoMigrate 边界:`cmd/all` 走 `ConnectCoreDB` / `InitCoreRedis`,只迁单体残留域;`cmd/userauth` 自己迁 `users` 和 `user_token_usage_adjustments`。 @@ -335,7 +335,7 @@ flowchart LR 当前切流点: 1. 前端仍访问 `/api/v1/user/*`。 -2. gateway 的 user handler 只做参数绑定、调用 userauth client、复用 `respond` 写回前端。 +2. gateway 的 user handler 只做参数绑定、调用 userauth client、通过 `backend/gateway/shared/respond` 写回前端。 3. gateway 鉴权和 quota guard 只依赖 `ports.UserAuthClient`,不直接依赖 userauth DAO/model。 4. `agent/chat` 的 token quota 门禁已经通过 userauth 服务判断;会话完成后的 token 记账由 agent 事件处理链路调用 userauth `AdjustTokenUsage`。 @@ -349,7 +349,7 @@ flowchart LR 遗留约定: -1. `respond` 暂时继续放在 `backend/respond` 复用;等全部阶段收尾后再整体收进 gateway/shared。 +1. 根 `backend/respond` 暂时继续作为服务层、RPC 层、client 层和旧实现的兼容响应语义;阶段 6 后 gateway HTTP 门面已切到 `backend/gateway/shared/respond`,该包只做薄转发,不允许服务层 import。 2. `cmd/all` 单独启动不再覆盖 user/auth 完整能力,后续 smoke 必须同时确认 userauth 已启动。 3. 不要再把 user/auth 当成后续待办;阶段 3 的接手人应从 notification 开始。 @@ -386,7 +386,7 @@ flowchart LR 本轮收口状态(2026-05-04): 1. `cmd/notification` 已承载 notification zrpc 启动、DB 迁移、服务级 outbox consumer 和重试扫描。 -2. `backend/services/notification` 已收进 DAO、model、sv、rpc、飞书 provider 和 outbox handler;gateway 通过 `backend/gateway/client/notification` zrpc client 调用。 +2. `backend/services/notification` 已收进 DAO、model、sv、rpc、飞书 provider 和 outbox handler;gateway 通过 `backend/client/notification` zrpc client 调用。 3. 主动调度侧只写入 `notification.feishu.requested`,publisher 侧只注册事件归属到 `notification`,不再启动单体 notification consumer。 4. 旧 `backend/notification`、旧 DAO/model 和旧 `service/events/notification_feishu.go` 已删除;review 发现的 sending 租约恢复和 RPC timeout 边界已修复。 5. 真实 smoke 已通过:`notification_outbox_messages.id=3` 已从 `pending` 推进到 `consumed`,`smartflow.notification.outbox` 已出现 `outbox_id=3`,对应 `notification_records` 生成并按未启用通道进入 `skipped`。 @@ -424,7 +424,7 @@ flowchart LR 1. `cmd/active-scheduler` 已承载 active-scheduler zrpc 启动、DB 迁移、服务级 outbox consumer、relay、retry loop 和 due job scanner。 2. `backend/services/active_scheduler` 已收进 DAO、sv、rpc 和主动调度核心逻辑;复杂领域流程统一下沉到 `backend/services/active_scheduler/core`,旧 `backend/active_scheduler` 活跃实现已移除。 -3. gateway HTTP 门面已统一到 `backend/gateway/api`,active-scheduler、notification、userauth 的 zrpc client 已统一到 `backend/gateway/client/*`。 +3. gateway HTTP 门面已统一到 `backend/gateway/api`,active-scheduler、notification、userauth 的 zrpc client 已统一到 `backend/client/*`。 4. 单体 `cmd/start.go` 不再启动 active-scheduler workflow / scanner / handler;gateway 的 `/api/v1/active-schedule/*` 只做鉴权、参数绑定、超时和 zrpc 转发。 5. 迁移期仍共享主库读取 / 写入 task、schedule、agent 会话与 notification outbox 相关表;active-scheduler 启动时会显式检查这些运行时依赖表,后续阶段 5/6 再逐步切成 RPC 或 read model。 6. 已完成真实 smoke:`trigger -> active_scheduler_outbox_messages -> consume -> preview ready` 闭环通过,当前 trigger 没有出现单体误消费导致的 dead handler。 @@ -435,21 +435,21 @@ flowchart LR 当前进展(2026-05-05): -1. 首刀 `schedule` 已完成服务化:新增 `cmd/schedule`、`services/schedule/{dao,rpc,sv,core}`、`gateway/client/schedule`、`shared/contracts/schedule` 和 `shared/ports` schedule port。 +1. 首刀 `schedule` 已完成服务化:新增 `cmd/schedule`、`services/schedule/{dao,rpc,sv,core}`、迁移期 `gateway/client/schedule`(阶段 6 后已迁到 `backend/client/schedule`)、`shared/contracts/schedule` 和 `shared/ports` schedule port。 2. gateway 的 `/api/v1/schedule/*` HTTP 门面已切到 schedule zrpc client;gateway 不再通过 `backend/service.ScheduleService` 直接承载 schedule HTTP 入口业务。 3. active-scheduler 的 schedule facts / feedback / confirm apply 已改为调用 schedule RPC adapter;`cmd/active-scheduler` 启动依赖检查已移除 `schedule_events`、`schedules`、`task_classes`、`task_items`。 -4. 第二刀 `task` 已开始服务化:新增 `cmd/task`、`services/task/{dao,rpc,sv}`、`gateway/client/task`、`shared/contracts/task` 和 `shared/ports` task port。 +4. 第二刀 `task` 已开始服务化:新增 `cmd/task`、`services/task/{dao,rpc,sv}`、迁移期 `gateway/client/task`(阶段 6 后已迁到 `backend/client/task`)、`shared/contracts/task` 和 `shared/ports` task port。 5. gateway 的 `/api/v1/task/*` HTTP 门面已切到 task zrpc client;gateway 只负责鉴权、参数绑定、短超时和响应透传,不再直接调用 `backend/service.TaskService`。 6. active-scheduler 的 task facts / due job scanner 已切到 task RPC adapter;`cmd/active-scheduler` 启动依赖检查已移除 `tasks`,进一步缩小 active-scheduler 对跨域主库表的直接依赖。 7. `task.urgency.promote.requested` 的 handler、relay、retry loop 已迁入 `cmd/task`;单体 outbox worker 只保留 agent / memory consumer,Agent 残留查询链路只允许 publish-only 写入 `task_outbox_messages`,避免单体和 task 独立服务抢同一 task consumer group。 -8. 第三刀 `task-class` 已完成 HTTP 所有权切流:新增 `cmd/task-class`、`services/task_class/{dao,rpc,sv}`、`gateway/client/taskclass`、`shared/contracts/taskclass` 和 `shared/ports` task-class port。 +8. 第三刀 `task-class` 已完成 HTTP 所有权切流:新增 `cmd/task-class`、`services/task_class/{dao,rpc,sv}`、迁移期 `gateway/client/taskclass`(阶段 6 后已迁到 `backend/client/taskclass`)、`shared/contracts/taskclass` 和 `shared/ports` task-class port。 9. gateway 的 `/api/v1/task-class/*` HTTP 门面已切到 task-class zrpc client;gateway 只负责鉴权、参数绑定、短超时和响应透传,不再直接调用 `backend/service.TaskClassService`。 10. task-class 本轮按主人拍板保留迁移期直写 `schedule_events` / `schedules` 权限,不走 schedule RPC bridge,以保留 `insert into schedule` / `apply batch into schedule` 与 task item 状态更新的本地事务语义;`cmd/task-class` 只 AutoMigrate `task_classes` / `task_items`,启动时显式检查 schedule 依赖表是否存在。 11. 旧实现仍保留:`backend/service/schedule.go`、`backend/dao/schedule.go`、`backend/service/task.go`、`backend/dao/task.go`、`backend/service/task-class.go`、`backend/dao/task-class.go`、`backend/service/course*.go`、`backend/dao/course.go`、active-scheduler 旧 Gorm apply adapter 暂时保留,用于 agent 迁移期、单体残留路径和回退。 12. 当前切流点:HTTP schedule 流量进入 `cmd/schedule`;HTTP task 流量进入 `cmd/task`;HTTP task-class 流量进入 `cmd/task-class`;active-scheduler 读取 task/schedule facts 与正式写日程均走 RPC;agent 内部仍存在直接 DAO 调用,后续按 agent/memory 阶段继续收。 13. 当前残留跨域 DB 依赖:task-class 迁移期仍直接写 `schedule_events` / `schedules`;task 服务迁移期仍 best-effort 写 `active_schedule_jobs`;active-scheduler 仍直接写 agent 会话 / timeline 和 notification outbox 相关表;agent 本地 task 查询、task-class upsert 和 schedule provider 仍保留 DAO 适配。 14. 已完成验证:`go test ./...` 通过;避让默认端口启动完整本地服务组(HTTP `18080`,zrpc `19081-19086`)后,task-class add / list / get / insert-into-schedule / delete-item / delete-class smoke 通过,并用 `docker exec` 核对 task-class 与 schedule 相关表无残留。 -15. 第四刀 `course` 已完成 HTTP 所有权切流:新增 `cmd/course`、`services/course/{dao,rpc,sv}`、`gateway/client/course`、`shared/contracts/course` 和 `shared/ports` course port。 +15. 第四刀 `course` 已完成 HTTP 所有权切流:新增 `cmd/course`、`services/course/{dao,rpc,sv}`、迁移期 `gateway/client/course`(阶段 6 后已迁到 `backend/client/course`)、`shared/contracts/course` 和 `shared/ports` course port。 16. gateway 的 `/api/v1/course/*` HTTP 门面已切到 course zrpc client;gateway 只负责鉴权、限流、幂等、multipart 文件读取、短超时和响应透传,不再直接调用 `backend/service.CourseService`。 17. course 本轮保留迁移期直写 `schedule_events` / `schedules` 权限,不走 schedule RPC bridge,以保留课程导入两个表同事务写入和冲突返回语义;`cmd/course` 不 AutoMigrate schedule 表,启动时显式检查依赖表是否存在。 18. 当前切流点更新:HTTP schedule / task / task-class / course 流量均进入各自独立 zrpc 服务;active-scheduler 读取 task/schedule facts 与正式写日程均走 RPC;agent 内部仍存在 task、task-class、schedule DAO 适配,后续按 agent/memory 阶段继续收。 @@ -509,13 +509,24 @@ flowchart LR 3. `backend/cmd/agent/main.go` 已补齐独立进程入口:负责 DB / Redis / LLM / RAG 初始化、agent outbox consumer 启停和 agent zrpc server 生命周期;旧 `backend/cmd/start.go` 的 gateway 本地链路继续保留。 4. agent 事件归属继续复用 `backend/service/events` 与服务级 outbox 路由:`chat.*` / `agent.*` 事件归 `ServiceAgent`,`memory.extract.requested` 只登记路由不再由 agent 进程消费,`task.urgency.promote.requested` 仍是 publish-only 写入 `task_outbox_messages`。 5. `backend/services/agent/rpc` 已补齐 `Ping`、`Chat` server-stream 以及 conversation meta/list/timeline、schedule-preview、context-stats、schedule-state 6 个 unary JSON 透传 RPC;跨进程 chat 边界传 `ChatChunk`,不传 Go channel;Gateway 继续对前端输出原 SSE 协议。 -6. `backend/gateway/client/agent` 与 `gateway/api/agent.go` 已接入 `agent.rpc.chat.enabled` 和 `agent.rpc.api.enabled` 两个开关;本地 `config.yaml` 与 `config.example.yaml` 当前默认 `true`,真实 UTF-8 中文 SSE smoke 已通过,chat 主链路走 `agent RPC Chat(stream)` 再转 SSE,非 chat `/agent/*` 走 agent unary RPC。 +6. `backend/client/agent` 与 `gateway/api/agent.go` 已接入 `agent.rpc.chat.enabled` 和 `agent.rpc.api.enabled` 两个开关;本地 `config.yaml` 与 `config.example.yaml` 当前默认 `true`,真实 UTF-8 中文 SSE smoke 已通过,chat 主链路走 `agent RPC Chat(stream)` 再转 SSE,非 chat `/agent/*` 走 agent unary RPC。 7. 历史 timeline payload key(如 `newagent_history_kind`)暂不改名,避免破坏旧会话兼容。 8. `backend/memory/*` 已物理迁入 `backend/services/memory/*`:`module.go`、`model/`、`observe/` 作为公共门面保留,`cleanup/`、`orchestrator/`、`repo/`、`service/`、`utils/`、`vectorsync/`、`worker/` 收入 `internal/`,旧 `backend/memory` 目录已删除。 9. `cmd/start.go` 不再创建/注册/启动 agent outbox event bus;agent relay / consumer 由 `cmd/agent` 独占,memory worker / 管理能力由 `cmd/memory` 承担。 10. `cmd/start.go` 已收缩 gateway 本地 `AgentService` 构建:当 `agent.rpc.chat.enabled=true` 且 `agent.rpc.api.enabled=true` 时,gateway 不再初始化 agent 本地编排、LLM、RAG、memory reader fallback;只有任一 RPC 开关关闭时才保守装配本地 fallback。 11. 最新验证:重建并重启 `api` / `agent` 后,UTF-8 中文 SSE smoke 通过且只有单个 `[DONE]`;6 个非 chat `/agent/*` HTTP smoke 中 meta/list/timeline/context-stats 返回 200,schedule-preview / schedule-state 在无快照场景返回预期业务 400。 -12. 下一轮目录收口按新口径推进:把 `backend/gateway/client/*` 机械迁到 `backend/client/*`,让 gateway 和服务进程共同复用 zrpc client;保留根 `backend/shared` 承载跨服务契约,同时新增 `backend/gateway/shared` 承载 HTTP/SSE/bind/respond 等 gateway 门面复用。 +12. 本轮目录收口 CP1 已按新口径完成:`backend/gateway/client/*` 已机械迁到 `backend/client/*`,gateway 和服务进程共同复用 zrpc client。 +13. 本轮目录收口 CP2 已新增 `backend/gateway/shared/respond`,gateway/api 与 gateway/middleware 不再直接 import 根 `backend/respond`;根 `backend/respond` 作为迁移期兼容语义保留给 RPC、client、服务层和旧实现,服务代码禁止 import `backend/gateway/shared`。 +14. CP2 验证结果:`go test ./...` 与 `git diff --check` 通过;真实 smoke 覆盖 health、未带 token 的鉴权错误、register/login、受保护 task/memory 接口、agent SSE 和 conversation timeline,确认 `gateway/shared/respond` 写回的 `401 + 40009 missing token` 保持旧协议,SSE 中文合并为 `CP2中文响应正常` 且只有单个 `[DONE]`。 +15. 本轮目录收口 CP3 先完成低风险切流:`cmd/start.go` 与 `cmd/agent/runtime.go` 不再直接 import 根 `backend/service` 主包,gateway fallback 与独立 agent runtime 的 task / schedule 注入已改用 `backend/services/task/sv`、`backend/services/schedule/sv` 及对应服务 DAO;未使用的 course 装配 helper 也改到 `backend/services/course/sv`。 +16. CP3 不硬搬根 `backend/dao` 与 `backend/model`:两者当前仍被 cmd、services、gateway、旧回退面和跨域迁移期链路广泛引用,直接整包搬迁风险过高;后续按单一能力域继续缩小,例如 task due job、schedule apply、agent 会话快照等。 +17. CP3 旧实现保留:根 `backend/service/*.go`、`backend/dao/*.go`、`backend/model/*.go` 继续作为并行迁移期回退面和共享历史模型存在;当前切流点是正式启动装配不再直连根 `backend/service` 主包,服务内活跃实现优先使用 `backend/services//sv`。 +18. CP3 验证结果:`go test ./...`、`git diff --check` 通过;正式 Go 代码里对根 `github.com/LoveLosita/smartflow/backend/service` 主包的 direct import 已归零。真实 smoke 在 `D:\SmartFlow-Agent\.tmp\cp3-service-root-smoke-20260505-203454` 完成,覆盖 health、register/login、`task/create` + `task/get`、`schedule/today`、`memory/items`、agent chat SSE、conversation meta/timeline/context-stats,确认 gateway 已走 `agent RPC` 而非本地 fallback,中文 SSE 合并为 `CP3中文响应正常` 且只有单个 `[DONE]`。 +19. CP4 第一轮先收最窄的根目录公共件:`backend/bootstrap` 当前只承载统一配置加载职责,已新增 canonical 入口 `backend/shared/infra/bootstrap`,并把 `cmd/start.go`、`cmd/agent`、`cmd/memory`、`cmd/notification`、`cmd/active-scheduler`、`cmd/schedule`、`cmd/task`、`cmd/task-class`、`cmd/course`、`cmd/userauth` 的 import 切到新路径。 +20. CP4 当前仍保留根 `backend/bootstrap` 兼容包装层,避免并行迁移期一次性删除旧目录;当前切流点是所有正式启动入口优先依赖 `backend/shared/infra/bootstrap`,后续确认无残留引用后再删除根目录兼容层。 +21. CP4 第二轮继续只收“纯连接底座”这一类:已新增 `backend/shared/infra/mysql`、`backend/shared/infra/redis`、`backend/shared/infra/eino` 三个 canonical 入口,只承载 MySQL 连接、Redis 连接和 Eino `AIHub` 构造,不承载任何服务私有 AutoMigrate、回填、outbox 或 worker 启停语义。 +22. CP4 当前切流点:`cmd/agent` 直接依赖 `shared/infra/mysql|redis|eino`;`cmd/active-scheduler`、`cmd/memory` 以及 `cmd/start.go` 里的本地 agent fallback LLM 构造已切到 `shared/infra/eino`;各服务 `dao/connect.go` 复用 `shared/infra/mysql|redis` 负责“开连接”,但仍在各自目录内保留本服务 own 的 AutoMigrate、依赖表检查与 outbox 建表逻辑。 +23. CP4 旧实现保留:根 `backend/inits` 目前收缩为兼容入口 + core 残留域初始化;其中 `ConnectCoreDB`、`AutoMigrateCoreStorage`、`InitCoreRedis` 仍服务于旧组合壳和迁移期 core 残留域,暂不下沉到 `shared/infra`,避免把服务私有 schema 语义重新做成“大公共篮子”。 建议提交点: @@ -756,11 +767,11 @@ SmartFlow-Agent/ > 当前目录到目标目录的映射: > > 1. `backend/services/userauth/*` 已经是阶段 2 终态样板;旧 `backend/api/user.go`、`backend/service/user.go`、`backend/dao/user.go`、`backend/model/user.go`、`backend/model/auth.go`、`backend/auth/jwt_handler.go`、`backend/middleware/token_handler.go`、`backend/middleware/token_quota_guard.go`、`backend/routers/routers.go` 不再作为活跃实现。 -> 2. `backend/gateway/api/userauth/*` 是 user HTTP 入口,`backend/gateway/client/userauth/*` 是 userauth zrpc client,二者都属于 gateway 边缘层。 +> 2. `backend/gateway/api/userauth/*` 是 user HTTP 入口,`backend/client/userauth/*` 是 userauth zrpc client;HTTP 门面属于 gateway 边缘层,zrpc client 属于跨进程共享 client 层。 > 3. `backend/service/*.go` 这批现有业务逻辑,后面要分别迁到各自服务根目录下的 `sv/`。 > 4. `backend/services/agent/*` 已承接原 `backend/newAgent/*` 内核,`backend/services/agent/sv/*` 已承接原 `backend/service/agentsvc/*` 编排层;后面再按风险拆到 `internal/{prompt,graph,stream,tool,session,router}`。 -> 5. `backend/services/notification/*` 已经是阶段 3 终态样板;`backend/cmd/notification` 是独立进程入口,`backend/gateway/client/notification` 是 gateway 侧 zrpc client,`backend/shared/contracts/notification` 只放跨层契约;旧 `backend/notification/*`、旧 DAO/model 和旧 `service/events/notification_feishu.go` 不再作为活跃实现。 -> 6. `backend/services/active_scheduler/*` 已经是阶段 4 当前样板;`backend/cmd/active-scheduler` 是独立进程入口,`backend/gateway/client/activescheduler` 是 gateway 侧 zrpc client,`backend/services/active_scheduler/core` 承载迁移期领域核心;旧 `backend/active_scheduler/*` 不再作为活跃实现。 +> 5. `backend/services/notification/*` 已经是阶段 3 终态样板;`backend/cmd/notification` 是独立进程入口,`backend/client/notification` 是跨进程 zrpc client,`backend/shared/contracts/notification` 只放跨层契约;旧 `backend/notification/*`、旧 DAO/model 和旧 `service/events/notification_feishu.go` 不再作为活跃实现。 +> 6. `backend/services/active_scheduler/*` 已经是阶段 4 当前样板;`backend/cmd/active-scheduler` 是独立进程入口,`backend/client/activescheduler` 是跨进程 zrpc client,`backend/services/active_scheduler/core` 承载迁移期领域核心;旧 `backend/active_scheduler/*` 不再作为活跃实现。 > 7. `backend/services/memory/*` 已成为 memory 当前 canonical 入口;`module.go`、`model/`、`observe/` 是对外可见门面,服务私有实现已收入 `internal/`,旧 `backend/memory/*` 只在 legacy 文档中作为历史路径出现。 > > 说明 4:`shared` 先保留 `events` 和少量跨服务底座型 `infra`。以后如果真的出现跨服务 DTO / 枚举 / 常量,再新增 `contracts` 一类目录,但不要把 `dao`、`model`、`sv`、`handler` 这类服务私有层塞进去。 @@ -771,7 +782,7 @@ SmartFlow-Agent/ > > 说明 7:目录树里如果暂时写成 `backend/services/llm/` 和 `backend/services/rag/`,那只是目录名写法;后文所有职责判断都以 `llm-service` / `rag-service` 这两个逻辑服务名为准。 > -> 说明 8:阶段 2 已经采用 `backend/services/userauth/` 作为实际目录名,不再使用 `user-auth`。阶段 3 已经采用 `backend/services/notification/` 作为实际目录名。zrpc client 终态放在 `backend/client//`,迁移期旧 `backend/gateway/client//` 要按机械迁移逐步挪出;进程入口放在 `backend/cmd//`,不要把 rpc client 放进 `cmd`。 +> 说明 8:阶段 2 已经采用 `backend/services/userauth/` 作为实际目录名,不再使用 `user-auth`。阶段 3 已经采用 `backend/services/notification/` 作为实际目录名。zrpc client 终态放在 `backend/client//`,迁移期旧 `backend/gateway/client//` 已在阶段 6 后按 CP1 机械迁出;进程入口放在 `backend/cmd//`,不要把 rpc client 放进 `cmd`。 ### 6.3 哪些可以不用变 @@ -814,7 +825,7 @@ SmartFlow-Agent/ | 服务 | 典型用例 | 结构收束建议 | 不允许的改法 | | --- | --- | --- | --- | -| `user/auth` | 注册、登录、刷新、登出、JWT 签发、黑名单、token 额度门禁、token 记账幂等 | 已完成:`cmd/userauth` + `services/userauth/{sv,dao,model,internal/auth,rpc}`;gateway 侧是 `gateway/api/userauth` + `gateway/client/userauth` | 不要恢复旧 Gin user/auth 实现;不要让 gateway 直连 users 表、Redis 黑名单或额度缓存;不要把 zrpc client 放进 `cmd` | +| `user/auth` | 注册、登录、刷新、登出、JWT 签发、黑名单、token 额度门禁、token 记账幂等 | 已完成:`cmd/userauth` + `services/userauth/{sv,dao,model,internal/auth,rpc}`;gateway 侧是 `gateway/api/userauth`,跨进程 client 是 `backend/client/userauth` | 不要恢复旧 Gin user/auth 实现;不要让 gateway 直连 users 表、Redis 黑名单或额度缓存;不要把 zrpc client 放进 `cmd` | | `course` | 课程导入、图片解析、课表校验、课程落表,图片解析走 `llm-service` | `handler.go` / `sv/` / `dao/` / `model/` / `internal/{parse,import,conflict,adapter}/` | 不要把课程解析代码写成网关临时脚本 | | `task-class` | 任务类创建/更新、items 批量 upsert、嵌入时间同步 | `handler.go` / `sv/` / `dao/` / `model/` / `internal/{convert,batch,item}/` | 不要把批处理拼装沉到 handler 里,也不要让 agent 直接改库 | | `notification` | 消费 `notification.feishu.requested`、写通知记录、幂等、重试、provider 投递 | `start.go` / `handler.go` / `sv/` / `dao/` / `model/` / `internal/{provider,runner,dedupe,channel,retry}/` | 不要把通知投递逻辑散回 worker 或 gateway | @@ -922,8 +933,8 @@ graph TD 5. 阶段 3 `notification` 已完成实现、code review 修复和真实 smoke;`llm-service`、`rag-service` 也已完成,不要重新当成待办。 6. 阶段 4 `active-scheduler` 已完成首轮收口;后续不要再把它当成“未拆服务”,除非是在补契约测试或继续替换跨域 DB 访问。 7. `shared` 只保留跨进程契约和少量跨服务底座,不承载业务逻辑、DAO、模型或状态机。 -8. `backend/client` 是 zrpc client 的终态共享位置,`backend/gateway/client` 只是迁移期旧位置;后续服务或 gateway 需要复用 client 时,优先推进机械搬迁到根 `client`。 -9. `backend/gateway/shared` 只放 gateway 门面复用,服务代码禁止 import;跨服务契约仍留在根 `backend/shared`。 +8. `backend/client` 是 zrpc client 的终态共享位置,`backend/gateway/client` 只是迁移期旧位置且 CP1 已完成机械迁出;后续服务或 gateway 需要复用 client 时,统一使用根 `client`。 +9. `backend/gateway/shared` 只放 gateway 门面复用,服务代码禁止 import;跨服务契约仍留在根 `backend/shared`。当前已落地 `backend/gateway/shared/respond` 作为 HTTP 响应适配薄门面。 10. 如果后续要改目录,必须先回答“这个文件属于哪一个典型用例”,回答不清楚就先别动结构。 11. 当前文档已经可以作为切对话基线;后续代理默认按本文件推进。现阶段的迁移基线入口是 `backend/cmd/api`、`backend/cmd/worker`、`backend/cmd/all`,它们只是当前仓库的启动壳,不是终态。`backend/cmd/userauth` 是阶段 2 的独立服务入口,`backend/cmd/notification` 是阶段 3 的独立服务入口,`backend/cmd/active-scheduler` 是阶段 4 的独立服务入口,`backend/cmd/schedule`、`backend/cmd/task`、`backend/cmd/task-class`、`backend/cmd/course` 是阶段 5 已落地的独立服务入口。终态仍然是“一个服务一个独立 `main.go`”,只在出现新的契约风险、边界变化或业务语义变化时再重新讨论架构。 @@ -937,7 +948,7 @@ graph TD 6. 带 worker 的服务可以继续保留多入口角色,例如 `api` / `worker` / `all`,但它们仍然是同一服务的不同可执行角色,不是把多个服务硬塞进一个进程。 7. MySQL / Redis 容器的启动归 `docker compose` 或运维层;Go 服务只负责在自己的进程里建立连接、做自己的 AutoMigrate 和连通性检查。 8. 阶段 5 后,旧 `cmd/start.go` / `cmd/all` 只是 gateway 和迁移期组合壳;本地完整 smoke 必须额外启动 `cmd/userauth`、`cmd/notification`、`cmd/active-scheduler`、`cmd/schedule`、`cmd/task`、`cmd/task-class` 和 `cmd/course`。如果同机已有另一条线占用默认端口,应复制临时配置,把 HTTP / zrpc 端口整体平移后再启动服务。 -9. 阶段 6 后,`cmd/agent` 和 `cmd/memory` 也应纳入完整本地 smoke;目录收口时优先把服务与 gateway 共同使用的 zrpc client 从 `gateway/client` 挪到根 `client`,再清理 gateway 门面复用到 `gateway/shared`。 +9. 阶段 6 后,`cmd/agent` 和 `cmd/memory` 也应纳入完整本地 smoke;目录收口 CP1 已把服务与 gateway 共同使用的 zrpc client 从 `gateway/client` 挪到根 `client`;CP2 已把 gateway HTTP 响应适配入口收进 `gateway/shared/respond`;CP3 已先切掉 `cmd` 对根 `backend/service` 主包的直接装配依赖。下一步再按风险迁 SSE/bind/multipart 等门面复用或继续收窄根 dao/model 依赖。 ### 6.11 测试自动化与 smoke 权限边界 @@ -1057,7 +1068,7 @@ graph TD 5. 跑完 `go test ./...` 后必须清理工作区 `.gocache`。 6. 不擅自回滚、覆盖、删除用户或其他代理的无关改动。 7. 不主动 `git commit` / `git branch`,除非用户明确要求。 -8. 服务间错误传递优先使用 go-zero / gRPC 内置 `error`,调用侧保持 `res, err :=` 风格;API 层对前端错误继续复用 `respond`,后续总收尾再考虑迁到 gateway/shared。 +8. 服务间错误传递优先使用 go-zero / gRPC 内置 `error`,调用侧保持 `res, err :=` 风格;API 层对前端错误通过 `backend/gateway/shared/respond` 写回,根 `backend/respond` 继续承载迁移期跨层响应语义。 9. MySQL / Redis 容器启动归 `docker compose` 或运维层;Go 服务只负责自己的连接初始化、AutoMigrate 和运行时依赖。 10. gateway 只做边缘转发、鉴权和轻量组合;不要把核心业务表、服务内部 Redis key、JWT 签发、额度账本放回 gateway。 11. 新服务开发可以和后续迁移并行,但必须独立目录、端口、配置和契约,不能污染正在迁移的服务边界。 @@ -1094,7 +1105,7 @@ graph TD 1. `backend/cmd/userauth/main.go` 是 userauth 独立进程入口。 2. `backend/services/userauth` 拥有 user/auth 核心业务、DAO、模型、JWT、黑名单、额度治理、zrpc server 和 token 记账幂等表。 -3. `backend/gateway/api/userauth` 是 HTTP user 入口,`backend/gateway/client/userauth` 是 zrpc client,`backend/gateway/middleware` 只调 userauth 做鉴权和额度门禁。 +3. `backend/gateway/api/userauth` 是 HTTP user 入口,`backend/client/userauth` 是 zrpc client,`backend/gateway/middleware` 只调 userauth 做鉴权和额度门禁。 4. `backend/shared/contracts/userauth` 和 `backend/shared/ports` 只承载跨层契约,不承载服务私有业务实现。 5. `cmd/all` 不再迁 `users`,`cmd/userauth` 自己迁 `users` 和 `user_token_usage_adjustments`。 6. 完整本地 smoke 需要同时启动 `cmd/all` 和 `cmd/userauth`。 @@ -1103,7 +1114,7 @@ graph TD 1. `backend/cmd/notification/main.go` 是 notification 独立进程入口,负责 DB 迁移、zrpc server、notification outbox consumer 和 retry loop 的统一生命周期。 2. `backend/services/notification` 拥有 notification 核心业务、DAO、模型、飞书 provider、幂等、投递记录状态机、重试扫描和 outbox handler。 -3. `backend/gateway/client/notification` 是 gateway 侧 zrpc client;gateway 只保留 notification HTTP 入口、鉴权和轻量组合逻辑,不再直连 notification DAO/service。 +3. `backend/client/notification` 是跨进程 zrpc client;gateway 只保留 notification HTTP 入口、鉴权和轻量组合逻辑,不再直连 notification DAO/service。 4. `backend/shared/contracts/notification` 和 `backend/shared/ports` 只承载跨层契约和端口接口,不承载服务私有业务实现。 5. notification 内部是 `userauth` 同款最小手搓 zrpc 框架,不使用 goctl 自动脚手架;`rpc` 只保留 `NewServer` 供 `cmd/notification` 管理 signal、outbox consumer、retry loop 和 server 生命周期。 6. 旧 `backend/notification/*`、旧 `backend/dao/notification_channel.go`、旧 `backend/model/notification_channel.go` 和旧 `backend/service/events/notification_feishu.go` 已删除;若 `backend/notification` 目录壳仍存在,它不参与编译,也不作为活跃实现。 @@ -1114,7 +1125,7 @@ graph TD 1. `backend/cmd/active-scheduler/main.go` 是 active-scheduler 独立进程入口,负责 DB 迁移、zrpc server、active-scheduler outbox consumer、relay、retry loop 和 due job scanner 的统一生命周期。 2. `backend/services/active_scheduler` 拥有 active-scheduler DAO、rpc、sv 和领域核心;核心流程当前在 `backend/services/active_scheduler/core`,旧 `backend/active_scheduler` 不再作为活跃实现存在。 -3. `backend/gateway/api` 是 HTTP 门面统一目录,`backend/gateway/client/activescheduler` 是 gateway 侧 zrpc client。 +3. `backend/gateway/api` 是 HTTP 门面统一目录,`backend/client/activescheduler` 是跨进程 zrpc client。 4. `backend/shared/contracts/activescheduler` 和 `backend/shared/ports` 只承载跨层契约和端口接口,不承载服务私有业务实现。 5. `cmd/all` 不再启动 active-scheduler workflow / scanner / handler;完整本地 smoke 需要同时启动 `cmd/all`、`cmd/userauth`、`cmd/notification` 和 `cmd/active-scheduler`。 6. 阶段 4 收口时仍共享主库访问 task、schedule、agent 会话和 notification outbox 相关表;阶段 5 已先通过 schedule / task RPC 继续缩小这条共享边界。 @@ -1123,7 +1134,7 @@ graph TD 1. `backend/cmd/schedule/main.go` 是 schedule 独立进程入口,`backend/cmd/task/main.go` 是 task 独立进程入口,`backend/cmd/task-class/main.go` 是 task-class 独立进程入口,`backend/cmd/course/main.go` 是 course 独立进程入口,四者各自初始化 DB / Redis / zrpc server 和所需服务内资源。 2. `backend/services/schedule` 拥有正式日程领域核心,`backend/services/task` 拥有任务池读写、完成/撤销、紧急性平移和 task outbox handler,`backend/services/task_class` 拥有任务类与任务块维护、批量排入日程等核心逻辑,`backend/services/course` 拥有课程校验、课程导入和课表图片解析逻辑。 -3. `backend/gateway/api` 继续作为 HTTP 门面统一目录,`backend/gateway/client/schedule`、`backend/gateway/client/task`、`backend/gateway/client/taskclass` 与 `backend/gateway/client/course` 作为 gateway 侧 zrpc client。 +3. `backend/gateway/api` 继续作为 HTTP 门面统一目录,`backend/client/schedule`、`backend/client/task`、`backend/client/taskclass` 与 `backend/client/course` 作为跨进程 zrpc client。 4. `backend/shared/contracts/schedule`、`backend/shared/contracts/task`、`backend/shared/contracts/taskclass`、`backend/shared/contracts/course` 和 `backend/shared/ports` 只承载跨进程契约与端口接口,不放 DAO、model 或业务状态机。 5. active-scheduler 的 schedule facts / feedback / confirm apply 已走 schedule RPC,task facts / due job scanner 已走 task RPC;启动依赖检查不再要求 `schedule_events`、`schedules`、`task_classes`、`task_items` 或 `tasks`。 6. `task.urgency.promote.requested` 的消费边界已迁入 `cmd/task`;单体 outbox worker 不再启动 task service bus,只保留 Agent 残留路径的 publish-only 写入能力,避免迁移期重复 relay / consume。 @@ -1199,6 +1210,43 @@ graph TD 处理: -1. 以当前编译入口和路由装配为准:`gateway/api/userauth` + `gateway/client/userauth` + `services/userauth` 是阶段 2 当前样板。 -2. 任何 user/auth 新能力先放进 `services/userauth`,gateway 只做 HTTP 适配和 respond 响应。 +1. 以当前编译入口和路由装配为准:`gateway/api/userauth` + `backend/client/userauth` + `services/userauth` 是阶段 2 当前样板。 +2. 任何 user/auth 新能力先放进 `services/userauth`,gateway 只做 HTTP 适配,并通过 `backend/gateway/shared/respond` 写响应。 3. 如果 user/auth 调用失败,先查 `cmd/userauth` 是否启动、zrpc endpoint 是否正确、服务内 MySQL/Redis 是否可连,不要把逻辑搬回 gateway。 +## 7. CP4 最终收口记录(2026-05-05) + +1. `backend` 根目录已收口到 5 个一级目录:`services`、`client`、`gateway`、`cmd`、`shared`。 +2. 根 `backend/bootstrap` 已删除;统一配置加载 canonical 入口为 `backend/shared/infra/bootstrap`。 +3. 根 `backend/inits` 已迁入 `backend/cmd/internal/coreinit`,仅保留旧组合壳 `cmd/start.go` 所需的 core 残留域初始化语义;纯连接/句柄构造已切到 `backend/shared/infra/mysql`、`backend/shared/infra/redis`、`backend/shared/infra/eino`,历史 `InitEino` 兼容壳已删除。 +4. 根 `backend/infra/kafka`、`backend/infra/outbox` 已迁入 `backend/shared/infra/{kafka,outbox}`;`backend/infra/rag` 历史文档已迁到 `docs/backend/legacy-infra-rag-from-backend`。 +5. 根 `backend/conv`、`backend/respond`、`backend/pkg`、`backend/middleware` 已分别迁入: + - `backend/shared/conv` + - `backend/shared/respond` + - `backend/shared/pkg` + - `backend/shared/middleware` +6. 根 `backend/dao`、`backend/model` 的活跃迁移桥已整体迁入 `backend/shared/runtime/{dao,model}`;根 `backend/service/events` 的活跃事件胶水已迁入 `backend/shared/runtime/eventsvc`。这三处是 CP4 结束时的迁移期 runtime 收容区,不是长期 canonical 归属,只是为了先把根目录收口并保持编译、启动与回退面可用。 +7. `backend/shared/legacy/service/*.go` 这批未再被 import 的旧业务实现已删除,不再继续以兼容回退面保留。 +8. `backend/utils/pwd_encryption.go` 已下沉到 `backend/services/userauth/internal/auth/password.go`;根 `backend/utils` 已删除。 +9. `backend/logic/smart_planning.go` 已下沉到 `backend/services/schedule/core/planning/smart_planning.go`;根 `backend/logic` 已删除。 +10. `backend/shared/respond` 已收缩为纯共享错误语义与状态映射,不再直接依赖 `gin`;`backend/gateway/shared/respond` 继续只承载 gateway HTTP 写回应与错误适配。 +11. `backend/shared/middleware`、`backend/shared/pkg`、`backend/shared/runtime` 已全部清空并删除;HTTP middleware 已归到 `backend/gateway/middleware`,`GormCachePlugin` 已拆到 `backend/shared/infra/gormcache`,共享 `RateLimiter` 已拆到 `backend/shared/infra/ratelimit`,agent token budget 已下沉到 `backend/services/agent/shared/token_budget.go`。 +12. `backend/shared` 当前目录口径已收敛为:`contracts`、`events`、`infra`、`ports`、`respond`;迁移期 runtime 桥接实现已挪到 `backend/services/runtime/{conv,dao,eventsvc,model}`。 +13. 本轮验证结果: + - `go test ./...` 通过,且 `.gocache` 已清理。 + - `git diff --check` 通过。 + - 最终真实 smoke 通过,产物见 `.tmp/backend-artifacts/cp4-final-clean-smoke-20260505-1/smoke-summary-final.json`。 + - smoke 覆盖:`health`、`register/login`、`task/create + task/get`、`schedule/today`、`task-class/list`、`memory/items`、`agent/chat`、`conversation-meta`、`conversation-timeline`、`context-stats`;全部返回 `200`,SSE 合并结果为 `CP4_OK`,且 `[DONE]` 只有 1 个。 + +## 8. CP5 最终收口记录(2026-05-05) + +1. `backend/shared` 已不再承载迁移期业务实现、DAO、GORM 实体或事件胶水;根共享层当前只保留 `contracts`、`events`、`infra`、`ports`、`respond` 五类内容。 +2. 迁移期 runtime 桥接实现已从根共享层下沉到 `backend/services/runtime/{conv,dao,eventsvc,model}`,不再伪装成“全后端共享契约层”。 +3. `backend/gateway/shared/respond` 已接管 Gin `DealWithError` 写回逻辑;根 `backend/shared/respond` 仅保留共享错误码、错误体和状态映射。 +4. `backend/gateway/middleware` 已接管 HTTP `IdempotencyMiddleware` 与 `RateLimitMiddleware`;共享层只保留基础设施 `ratelimit` 与 `gormcache`。 +5. 旧 `InitEino` 兼容壳、旧 `shared/legacy` 命名以及未再被 import 的 `service/*.go` 旧实现均已删除。 +6. CP5 最终验证: + - `go test ./...` 通过,且 `.gocache` 已清理。 + - `git diff --check` 通过。 + - 最终真实 smoke 通过,产物见 `.tmp/backend-artifacts/cp5-final-smoke-20260505-1/smoke-summary-final.json`。 + - smoke 覆盖:`health`、`register/login`、`task/create + task/get`、`schedule/today`、`task-class/list`、`memory/items`、`agent/chat`、`conversation-meta`、`conversation-timeline`、`context-stats`;全部返回 `200`,SSE 合并结果为 `CP5_OK`,且 `[DONE]` 只有 1 个。 +7. 至此 CP5“最终清理与文档收口”中与目录形态、共享层纯化、兼容壳清理、编译、diff check 和真实 smoke 相关的核心目标均已完成。