feat(core): bind request services and implement registered tasks

Wire plugin services through Bind/InjectFrom and AppContext so HTTP and
workers resolve dependencies after Apply. Register TaskHandler objects
with persisted results, and implement send_email_code, mail:send,
cleanup_inactive_users, and dispatch_bot_msg.
This commit is contained in:
ryan
2026-09-02 16:59:00 +08:00
parent 30bbe965bf
commit 4f50f6a8f9
49 changed files with 1406 additions and 499 deletions
@@ -48,7 +48,7 @@ func abortTaskLogicError(c *gin.Context, err error) bool {
// @Failure 403 {object} response.Any "无管理员权限"
// @Router /api/v1/admin/tasks/types [get]
func ListTaskTypes(c *gin.Context) {
c.JSON(http.StatusOK, response.OK(service.ListTaskTypes()))
c.JSON(http.StatusOK, response.OK(service.ListTaskTypes(c.Request.Context())))
}
// DispatchTask 下发任务
+8 -56
View File
@@ -12,7 +12,6 @@ import (
"Wavelet/plugins/domain/admin/handler"
"Wavelet/plugins/domain/admin/model"
"Wavelet/plugins/domain/admin/service"
"context"
"embed"
"reflect"
@@ -85,56 +84,13 @@ func (p *Plugin) Apply(ctx *core.Context) error {
_ = ctx.Config().Bind("clickhouse", &chCfg)
service.SetClickHouseConfig(chCfg)
// 0. Bind Services reactively
if db, err := core.Inject[contracts.DBService](ctx); err == nil && db != nil {
service.SetDBService(db)
} else {
core.When[contracts.DBService](ctx, func(db contracts.DBService) {
service.SetDBService(db)
})
}
if cache, err := core.Inject[contracts.CacheService](ctx); err == nil && cache != nil {
service.SetCacheService(cache)
} else {
core.When[contracts.CacheService](ctx, func(cache contracts.CacheService) {
service.SetCacheService(cache)
})
}
if user, err := core.Inject[contracts.UserService](ctx); err == nil && user != nil {
service.SetUserService(user)
} else {
core.When[contracts.UserService](ctx, func(user contracts.UserService) {
service.SetUserService(user)
})
}
if auth, err := core.Inject[contracts.AuthService](ctx); err == nil && auth != nil {
service.SetAuthService(auth)
} else {
core.When[contracts.AuthService](ctx, func(auth contracts.AuthService) {
service.SetAuthService(auth)
})
}
if task, err := core.Inject[contracts.TaskService](ctx); err == nil && task != nil {
service.SetTaskService(task)
} else {
core.When[contracts.TaskService](ctx, func(task contracts.TaskService) {
service.SetTaskService(task)
})
}
if storage, err := core.Inject[contracts.StorageService](ctx); err == nil && storage != nil {
service.SetStorageService(storage)
} else {
core.When[contracts.StorageService](ctx, func(storage contracts.StorageService) {
service.SetStorageService(storage)
})
}
if rc, err := core.Inject[contracts.RiskControlService](ctx); err == nil && rc != nil {
service.SetRiskControlService(rc)
} else {
core.When[contracts.RiskControlService](ctx, func(rc contracts.RiskControlService) {
service.SetRiskControlService(rc)
})
}
core.Bind[contracts.DBService](ctx, service.SetDBService)
core.Bind[contracts.CacheService](ctx, service.SetCacheService)
core.Bind[contracts.UserService](ctx, service.SetUserService)
core.Bind[contracts.AuthService](ctx, service.SetAuthService)
core.Bind[contracts.TaskService](ctx, service.SetTaskService)
core.Bind[contracts.StorageService](ctx, service.SetStorageService)
core.Bind[contracts.RiskControlService](ctx, service.SetRiskControlService)
service.SetEventEmitter(ctx.Events().Emit)
ctx.OnDispose(func() error {
@@ -175,11 +131,7 @@ func (p *Plugin) Apply(ctx *core.Context) error {
ctx.Router().RegisterWhitelist("/robots.txt")
// 2. Register Background Tasks
logSwitchHandler := &service.LogDBSwitchHandler{}
ctx.Task().Register(service.LogDBSwitchTask, func(c context.Context, payload []byte) error {
_, err := logSwitchHandler.Execute(c, payload)
return err
}, extpoints.WithTaskMeta(service.LogDBSwitchMeta))
ctx.Task().Register(service.LogDBSwitchTask, &service.LogDBSwitchHandler{}, extpoints.WithTaskMeta(service.LogDBSwitchMeta))
// 3. Register Settings Schemas
ctx.Settings().Register(extpoints.SettingSchema{
@@ -5,6 +5,7 @@
package repository
import (
"Wavelet/core"
"Wavelet/core/contracts"
"Wavelet/pkg/cache/ram"
"Wavelet/pkg/logger"
@@ -56,6 +57,9 @@ func ResetServices() {
// GetDB returns the GORM DB instance bound to the context if available.
func GetDB(ctx context.Context) *gorm.DB {
if s, err := core.InjectFrom[contracts.DBService](ctx); err == nil && s != nil {
return s.DB(ctx)
}
repoMu.RLock()
defer repoMu.RUnlock()
if dbService == nil {
@@ -65,7 +69,10 @@ func GetDB(ctx context.Context) *gorm.DB {
}
// GetCache returns the unified CacheService instance.
func GetCache(_ context.Context) contracts.CacheService {
func GetCache(ctx context.Context) contracts.CacheService {
if s, err := core.InjectFrom[contracts.CacheService](ctx); err == nil && s != nil {
return s
}
repoMu.RLock()
defer repoMu.RUnlock()
return cacheService
+2 -2
View File
@@ -75,7 +75,7 @@ func IsAllowedLogOrigin(ctx context.Context, origin, host string) bool {
// AccessLogs queries the analytical access log store and decorates rows with user names.
func AccessLogs(ctx context.Context, q model.AccessLogQuery) (model.AccessLogsResponse, error) {
rc := GetRiskControlService()
rc := GetRiskControlService(ctx)
if rc == nil {
return model.AccessLogsResponse{}, errs.ErrLogStoreUnavailable
}
@@ -117,7 +117,7 @@ func AccessLogs(ctx context.Context, q model.AccessLogQuery) (model.AccessLogsRe
// AccessLogAnalytics aggregates the daily trend of the access log store.
func AccessLogAnalytics(ctx context.Context) (model.LogsAnalyticsResponse, error) {
rc := GetRiskControlService()
rc := GetRiskControlService(ctx)
if rc == nil {
return model.LogsAnalyticsResponse{}, errs.ErrLogStoreUnavailable
}
@@ -109,7 +109,7 @@ func (h *LogDBSwitchHandler) Execute(ctx context.Context, payload []byte) (*cont
return nil, err
}
taskSvc := GetTaskService()
taskSvc := GetTaskService(ctx)
if taskSvc != nil {
taskSvc.AppendLog(ctx, "开始切换日志数据库:%s -> %s", source, p.Target)
}
@@ -123,7 +123,7 @@ func (h *LogDBSwitchHandler) Execute(ctx context.Context, payload []byte) (*cont
}
}()
rc := GetRiskControlService()
rc := GetRiskControlService(ctx)
if rc != nil {
if err := rc.SwitchLogEngine(ctx, p.Target); err != nil {
return nil, err
@@ -5,6 +5,7 @@
package service
import (
"Wavelet/core"
"Wavelet/core/contracts"
"Wavelet/plugins/domain/admin/errs"
"Wavelet/plugins/domain/admin/repository"
@@ -112,6 +113,9 @@ func ResetServices() {
// GetDB returns the GORM DB instance bound to the context if available.
func GetDB(ctx context.Context) *gorm.DB {
if s, err := core.InjectFrom[contracts.DBService](ctx); err == nil && s != nil {
return s.DB(ctx)
}
servicesMu.RLock()
defer servicesMu.RUnlock()
if dbService == nil {
@@ -121,42 +125,60 @@ func GetDB(ctx context.Context) *gorm.DB {
}
// GetCache returns the unified CacheService instance.
func GetCache(_ context.Context) contracts.CacheService {
func GetCache(ctx context.Context) contracts.CacheService {
if s, err := core.InjectFrom[contracts.CacheService](ctx); err == nil && s != nil {
return s
}
servicesMu.RLock()
defer servicesMu.RUnlock()
return cacheService
}
// GetUserService returns the UserService instance.
func GetUserService(_ context.Context) contracts.UserService {
func GetUserService(ctx context.Context) contracts.UserService {
if s, err := core.InjectFrom[contracts.UserService](ctx); err == nil && s != nil {
return s
}
servicesMu.RLock()
defer servicesMu.RUnlock()
return userService
}
// GetAuthService returns the AuthService instance.
func GetAuthService(_ context.Context) contracts.AuthService {
func GetAuthService(ctx context.Context) contracts.AuthService {
if s, err := core.InjectFrom[contracts.AuthService](ctx); err == nil && s != nil {
return s
}
servicesMu.RLock()
defer servicesMu.RUnlock()
return authService
}
// GetTaskService returns the TaskService instance.
func GetTaskService() contracts.TaskService {
func GetTaskService(ctx context.Context) contracts.TaskService {
if s, err := core.InjectFrom[contracts.TaskService](ctx); err == nil && s != nil {
return s
}
servicesMu.RLock()
defer servicesMu.RUnlock()
return taskService
}
// GetStorageService returns the StorageService instance.
func GetStorageService() contracts.StorageService {
func GetStorageService(ctx context.Context) contracts.StorageService {
if s, err := core.InjectFrom[contracts.StorageService](ctx); err == nil && s != nil {
return s
}
servicesMu.RLock()
defer servicesMu.RUnlock()
return storageSvc
}
// GetRiskControlService returns the RiskControlService instance.
func GetRiskControlService() contracts.RiskControlService {
func GetRiskControlService(ctx context.Context) contracts.RiskControlService {
if s, err := core.InjectFrom[contracts.RiskControlService](ctx); err == nil && s != nil {
return s
}
servicesMu.RLock()
defer servicesMu.RUnlock()
return riskControlService
@@ -195,8 +217,8 @@ func requireAuthService(ctx context.Context) (contracts.AuthService, error) {
}
// requireTaskService resolves the injected task contract service.
func requireTaskService() (contracts.TaskService, error) {
taskSvc := GetTaskService()
func requireTaskService(ctx context.Context) (contracts.TaskService, error) {
taskSvc := GetTaskService(ctx)
if taskSvc == nil {
return nil, errs.ErrTaskServiceUnavailable
}
@@ -117,7 +117,7 @@ func formatDuration(d time.Duration) string {
func LogDatabaseStatus(ctx context.Context) model.LogDatabaseStatus {
activeDB := logDBNameSQLite
migration := logMigrationIdle
if rc := GetRiskControlService(); rc != nil {
if rc := GetRiskControlService(ctx); rc != nil {
activeDB = rc.ActiveLogEngine(ctx)
if rc.IsLogEngineMigrating(ctx) {
migration = logMigrationInProgress
+60 -14
View File
@@ -17,8 +17,8 @@ import (
)
// ListTaskTypes returns every dispatchable task type declared in the task registry.
func ListTaskTypes() []contracts.TaskMetaDTO {
taskSvc := GetTaskService()
func ListTaskTypes(ctx context.Context) []contracts.TaskMetaDTO {
taskSvc := GetTaskService(ctx)
if taskSvc == nil {
return []contracts.TaskMetaDTO{}
}
@@ -27,7 +27,7 @@ func ListTaskTypes() []contracts.TaskMetaDTO {
// DispatchTask validates and enqueues a manual task run, returning the new task id.
func DispatchTask(ctx context.Context, req model.DispatchTaskRequest) (string, error) {
taskSvc, err := requireTaskService()
taskSvc, err := requireTaskService(ctx)
if err != nil {
return "", err
}
@@ -67,29 +67,75 @@ func ListTaskExecutions(
ctx context.Context,
req model.ListTaskExecutionsRequest,
) ([]model.TaskExecution, int64, error) {
if req.TaskType != "" {
if taskSvc := GetTaskService(); taskSvc != nil {
if meta, ok := taskSvc.GetTaskMeta(req.TaskType); ok {
req.TaskType = meta.Name
}
taskSvc, err := requireTaskService(ctx)
if err != nil {
return nil, 0, err
}
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 {
req.PageSize = 20
}
filterType := req.TaskType
if filterType != "" {
if meta, ok := taskSvc.GetTaskMeta(filterType); ok {
filterType = meta.AsynqTask
}
}
executions, total, err := repository.ListTaskExecutionRecords(ctx, req)
rows, total, err := taskSvc.ListExecutions(ctx, filterType, req.Status, req.Page, req.PageSize)
if err != nil {
return nil, 0, err
}
executions := make([]model.TaskExecution, 0, len(rows))
for i := range rows {
executions = append(executions, executionFromDTO(rows[i]))
}
return executions, total, nil
}
// TaskExecution loads a single execution record including its buffered log.
func TaskExecution(ctx context.Context, id uint64) (*model.TaskExecution, error) {
return repository.GetTaskExecutionByID(ctx, id)
taskSvc, err := requireTaskService(ctx)
if err != nil {
return nil, err
}
dto, err := taskSvc.GetExecution(ctx, id)
if err != nil || dto == nil {
return nil, err
}
row := executionFromDTO(*dto)
return &row, nil
}
func executionFromDTO(dto contracts.TaskExecutionDTO) model.TaskExecution {
return model.TaskExecution{
ID: dto.ID,
TaskID: dto.TaskID,
TaskType: dto.TaskType,
TaskName: dto.TaskName,
Status: model.TaskExecutionStatus(dto.Status),
Retryable: dto.Retryable,
MaxRetry: dto.MaxRetry,
RetryCount: dto.RetryCount,
Log: dto.Log,
ErrorMessage: dto.ErrorMessage,
Result: dto.Result,
StartedAt: dto.StartedAt,
FinishedAt: dto.FinishedAt,
Duration: dto.Duration,
Payload: dto.Payload,
TriggeredBy: dto.TriggeredBy,
CreatedAt: dto.CreatedAt,
UpdatedAt: dto.UpdatedAt,
}
}
// RetryTask re-dispatches a failed execution as a new task run.
func RetryTask(ctx context.Context, id uint64) (string, error) {
taskSvc, err := requireTaskService()
taskSvc, err := requireTaskService(ctx)
if err != nil {
return "", err
}
@@ -126,7 +172,7 @@ func CreateSchedule(ctx context.Context, req model.CreateScheduleRequest) (*mode
return nil, errs.ErrInvalidCronExpression
}
taskSvc, err := requireTaskService()
taskSvc, err := requireTaskService(ctx)
if err != nil {
return nil, err
}
@@ -168,7 +214,7 @@ func UpdateSchedule(ctx context.Context, id uint64, req model.UpdateScheduleRequ
return nil, errs.ErrInvalidCronExpression
}
taskSvc, err := requireTaskService()
taskSvc, err := requireTaskService(ctx)
if err != nil {
return nil, err
}
@@ -203,7 +249,7 @@ func DeleteSchedule(ctx context.Context, id uint64) error {
return fmt.Errorf("%s: %w", errs.ScheduleDeleteFailed, err)
}
if taskSvc := GetTaskService(); taskSvc != nil {
if taskSvc := GetTaskService(ctx); taskSvc != nil {
reloadScheduler(ctx, taskSvc)
}
return nil