1032 lines
31 KiB
Go
1032 lines
31 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
|
"scheduler-backend/internal/executor"
|
|
"scheduler-backend/internal/jobdef"
|
|
"scheduler-backend/internal/store/model"
|
|
)
|
|
|
|
type handlerItem struct {
|
|
Key string `json:"key"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
type handlerListResponse struct {
|
|
List []handlerItem `json:"list"`
|
|
}
|
|
|
|
type jobItem struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
HandlerKey string `json:"handlerKey"`
|
|
Enabled bool `json:"enabled"`
|
|
ScheduleType string `json:"scheduleType"`
|
|
ScheduleValue string `json:"scheduleValue"`
|
|
DefaultParams string `json:"defaultParams"`
|
|
LastStatus string `json:"lastStatus"`
|
|
LastRunAt string `json:"lastRunAt,omitempty"`
|
|
NextRunAt string `json:"nextRunAt,omitempty"`
|
|
}
|
|
|
|
type jobListResponse struct {
|
|
List []jobItem `json:"list"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type executionItem struct {
|
|
ID string `json:"id"`
|
|
JobName string `json:"jobName"`
|
|
TriggerType string `json:"triggerType"`
|
|
Status string `json:"status"`
|
|
StartedAt string `json:"startedAt,omitempty"`
|
|
FinishedAt string `json:"finishedAt,omitempty"`
|
|
ResultSummary string `json:"resultSummary"`
|
|
}
|
|
|
|
type executionDetailItem struct {
|
|
ID string `json:"id"`
|
|
JobConfigID string `json:"jobConfigId"`
|
|
TriggerType string `json:"triggerType"`
|
|
Status string `json:"status"`
|
|
ParamsSnapshot string `json:"paramsSnapshot"`
|
|
StartedAt string `json:"startedAt,omitempty"`
|
|
FinishedAt string `json:"finishedAt,omitempty"`
|
|
DurationMs int64 `json:"durationMs"`
|
|
ResultSummary string `json:"resultSummary"`
|
|
ErrorMessage string `json:"errorMessage"`
|
|
LogText string `json:"logText"`
|
|
LogFile string `json:"logFile,omitempty"`
|
|
CreatedAt string `json:"createdAt"`
|
|
}
|
|
|
|
type executionListResponse struct {
|
|
List []executionItem `json:"list"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type profileItem struct {
|
|
ID string `json:"id"`
|
|
Account string `json:"account"`
|
|
Nickname string `json:"nickname"`
|
|
Avatar string `json:"avatar"`
|
|
Remark string `json:"remark"`
|
|
CreatedAt string `json:"createdAt"`
|
|
UpdatedAt string `json:"updatedAt"`
|
|
}
|
|
|
|
type adminUserItem struct {
|
|
ID string `json:"id"`
|
|
Account string `json:"account"`
|
|
Nickname string `json:"nickname"`
|
|
Status string `json:"status"`
|
|
Role string `json:"role"`
|
|
Remark string `json:"remark"`
|
|
CreatedAt string `json:"createdAt"`
|
|
UpdatedAt string `json:"updatedAt"`
|
|
}
|
|
|
|
type adminUserListResponse struct {
|
|
List []adminUserItem `json:"list"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type systemConfigItem struct {
|
|
ID string `json:"id"`
|
|
Key string `json:"key"`
|
|
Title string `json:"title"`
|
|
Value string `json:"value"`
|
|
ValueType string `json:"valueType"`
|
|
Description string `json:"description"`
|
|
Enabled bool `json:"enabled"`
|
|
CreatedAt string `json:"createdAt"`
|
|
UpdatedAt string `json:"updatedAt"`
|
|
}
|
|
|
|
type systemConfigListResponse struct {
|
|
List []systemConfigItem `json:"list"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type dashboardOverviewResponse struct {
|
|
JobTotal int `json:"jobTotal"`
|
|
EnabledJobTotal int `json:"enabledJobTotal"`
|
|
ExecutionToday int `json:"executionToday"`
|
|
FailedToday int `json:"failedToday"`
|
|
LastExecutionAt string `json:"lastExecutionAt,omitempty"`
|
|
FailedJobTotal int `json:"failedJobTotal"`
|
|
AdminUserTotal int `json:"adminUserTotal"`
|
|
SystemConfigTotal int `json:"systemConfigTotal"`
|
|
RecentExecutions []executionItem `json:"recentExecutions"`
|
|
}
|
|
|
|
type JobConfigLister interface {
|
|
List(ctx context.Context) ([]model.JobConfig, error)
|
|
GetByID(ctx context.Context, id primitive.ObjectID) (*model.JobConfig, error)
|
|
Create(ctx context.Context, item *model.JobConfig) error
|
|
Update(ctx context.Context, item *model.JobConfig) error
|
|
ToggleEnabled(ctx context.Context, id primitive.ObjectID, enabled bool) error
|
|
UpdateRunState(ctx context.Context, id primitive.ObjectID, status string, lastRunAt *time.Time) error
|
|
}
|
|
|
|
type JobExecutionLister interface {
|
|
List(ctx context.Context) ([]model.JobExecution, error)
|
|
GetByID(ctx context.Context, id primitive.ObjectID) (*model.JobExecution, error)
|
|
Create(ctx context.Context, item *model.JobExecution) error
|
|
UpdateResult(ctx context.Context, item *model.JobExecution) error
|
|
}
|
|
|
|
type AdminProfileStore interface {
|
|
Get(ctx context.Context) (*model.AdminProfile, error)
|
|
Upsert(ctx context.Context, profile *model.AdminProfile) error
|
|
ChangePassword(ctx context.Context, oldPassword, newPassword string) error
|
|
Authenticate(ctx context.Context, account, password string) (*model.AdminProfile, error)
|
|
}
|
|
|
|
type AdminUserStore interface {
|
|
List(ctx context.Context) ([]model.AdminUser, error)
|
|
GetByID(ctx context.Context, id primitive.ObjectID) (*model.AdminUser, error)
|
|
Create(ctx context.Context, item *model.AdminUser) error
|
|
Update(ctx context.Context, item *model.AdminUser) error
|
|
ChangePassword(ctx context.Context, id primitive.ObjectID, newPassword string) error
|
|
Authenticate(ctx context.Context, account, password string) (*model.AdminUser, error)
|
|
}
|
|
|
|
type SystemConfigStore interface {
|
|
List(ctx context.Context) ([]model.SystemConfig, error)
|
|
GetByID(ctx context.Context, id primitive.ObjectID) (*model.SystemConfig, error)
|
|
Create(ctx context.Context, item *model.SystemConfig) error
|
|
Update(ctx context.Context, item *model.SystemConfig) error
|
|
ToggleEnabled(ctx context.Context, id primitive.ObjectID, enabled bool) error
|
|
}
|
|
|
|
type RouterDeps struct {
|
|
Registry *jobdef.Registry
|
|
JobConfigStore JobConfigLister
|
|
ExecutionStore JobExecutionLister
|
|
ProfileStore AdminProfileStore
|
|
AdminUserStore AdminUserStore
|
|
ConfigStore SystemConfigStore
|
|
Executor *executor.Service
|
|
}
|
|
|
|
type upsertJobRequest struct {
|
|
Name string `json:"name" binding:"required"`
|
|
HandlerKey string `json:"handlerKey" binding:"required"`
|
|
Enabled bool `json:"enabled"`
|
|
ScheduleType string `json:"scheduleType" binding:"required"`
|
|
ScheduleValue string `json:"scheduleValue"`
|
|
DefaultParams string `json:"defaultParams"`
|
|
}
|
|
|
|
type toggleJobRequest struct {
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
type runJobRequest struct {
|
|
Params json.RawMessage `json:"params"`
|
|
}
|
|
|
|
type updateProfileRequest struct {
|
|
Account string `json:"account" binding:"required"`
|
|
Nickname string `json:"nickname" binding:"required"`
|
|
Avatar string `json:"avatar"`
|
|
Remark string `json:"remark"`
|
|
}
|
|
|
|
type changeProfilePasswordRequest struct {
|
|
OldPassword string `json:"oldPassword" binding:"required"`
|
|
NewPassword string `json:"newPassword" binding:"required"`
|
|
}
|
|
|
|
type loginRequest struct {
|
|
Account string `json:"account" binding:"required"`
|
|
Password string `json:"password" binding:"required"`
|
|
}
|
|
|
|
type upsertAdminUserRequest struct {
|
|
Account string `json:"account" binding:"required"`
|
|
Nickname string `json:"nickname" binding:"required"`
|
|
Status string `json:"status"`
|
|
Role string `json:"role"`
|
|
Remark string `json:"remark"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
type changeAdminPasswordRequest struct {
|
|
NewPassword string `json:"newPassword" binding:"required"`
|
|
}
|
|
|
|
type upsertSystemConfigRequest struct {
|
|
Key string `json:"key" binding:"required"`
|
|
Title string `json:"title" binding:"required"`
|
|
Value string `json:"value"`
|
|
ValueType string `json:"valueType" binding:"required"`
|
|
Description string `json:"description"`
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
type toggleSystemConfigRequest struct {
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
func NewRouter(deps RouterDeps) *gin.Engine {
|
|
router := gin.Default()
|
|
router.Use(func(c *gin.Context) {
|
|
c.Header("Access-Control-Allow-Origin", "*")
|
|
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
|
c.Header("Access-Control-Expose-Headers", "Content-Length, Content-Type")
|
|
if c.Request.Method == http.MethodOptions {
|
|
c.AbortWithStatus(http.StatusNoContent)
|
|
return
|
|
}
|
|
c.Next()
|
|
})
|
|
router.POST("/admin/account/login", func(c *gin.Context) {
|
|
var req loginRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
profile, err := deps.ProfileStore.Authenticate(c.Request.Context(), req.Account, req.Password)
|
|
if err == nil {
|
|
c.JSON(http.StatusOK, toProfileItem(*profile))
|
|
return
|
|
}
|
|
|
|
adminUser, userErr := deps.AdminUserStore.Authenticate(c.Request.Context(), req.Account, req.Password)
|
|
if userErr != nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid account or password"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, adminUserItem{
|
|
ID: objectIDToString(adminUser.ID),
|
|
Account: adminUser.Account,
|
|
Nickname: adminUser.Nickname,
|
|
Status: adminUser.Status,
|
|
Role: adminUser.Role,
|
|
Remark: adminUser.Remark,
|
|
CreatedAt: adminUser.CreatedAt.Format(time.RFC3339),
|
|
UpdatedAt: adminUser.UpdatedAt.Format(time.RFC3339),
|
|
})
|
|
})
|
|
router.GET("/admin/dashboard/overview", func(c *gin.Context) {
|
|
jobs, err := deps.JobConfigStore.List(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
executions, err := deps.ExecutionStore.List(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
adminUsers, err := deps.AdminUserStore.List(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
systemConfigs, err := deps.ConfigStore.List(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
enabledCount := 0
|
|
failedJobCount := 0
|
|
jobNameMap := make(map[string]string, len(jobs))
|
|
for _, item := range jobs {
|
|
jobNameMap[item.ID.Hex()] = item.Name
|
|
if item.Enabled {
|
|
enabledCount++
|
|
}
|
|
if item.LastStatus == "failed" {
|
|
failedJobCount++
|
|
}
|
|
}
|
|
|
|
now := time.Now()
|
|
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
|
executionToday := 0
|
|
failedToday := 0
|
|
lastExecutionAt := ""
|
|
recent := make([]executionItem, 0, min(len(executions), 5))
|
|
for idx, item := range executions {
|
|
if idx < 5 {
|
|
recent = append(recent, executionItem{
|
|
ID: objectIDToString(item.ID),
|
|
JobName: jobDisplayName(jobNameMap, item.JobConfigID),
|
|
TriggerType: item.TriggerType,
|
|
Status: item.Status,
|
|
StartedAt: formatTime(item.StartedAt),
|
|
FinishedAt: formatTime(item.FinishedAt),
|
|
ResultSummary: item.ResultSummary,
|
|
})
|
|
}
|
|
if lastExecutionAt == "" && item.StartedAt != nil {
|
|
lastExecutionAt = item.StartedAt.Format(time.RFC3339)
|
|
}
|
|
if item.CreatedAt.After(todayStart) {
|
|
executionToday++
|
|
if item.Status == "failed" {
|
|
failedToday++
|
|
}
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, dashboardOverviewResponse{
|
|
JobTotal: len(jobs),
|
|
EnabledJobTotal: enabledCount,
|
|
ExecutionToday: executionToday,
|
|
FailedToday: failedToday,
|
|
LastExecutionAt: lastExecutionAt,
|
|
FailedJobTotal: failedJobCount,
|
|
AdminUserTotal: len(adminUsers),
|
|
SystemConfigTotal: len(systemConfigs),
|
|
RecentExecutions: recent,
|
|
})
|
|
})
|
|
router.GET("/admin/scheduler/handlers", func(c *gin.Context) {
|
|
items := make([]handlerItem, 0, len(deps.Registry.List()))
|
|
for _, handler := range deps.Registry.List() {
|
|
items = append(items, handlerItem{
|
|
Key: handler.Key(),
|
|
Name: handler.Name(),
|
|
Description: handler.Description(),
|
|
})
|
|
}
|
|
|
|
c.JSON(http.StatusOK, handlerListResponse{List: items})
|
|
})
|
|
router.GET("/admin/scheduler/jobs", func(c *gin.Context) {
|
|
items, err := deps.JobConfigStore.List(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
responseItems := make([]jobItem, 0, len(items))
|
|
for _, item := range items {
|
|
responseItems = append(responseItems, toJobItem(item))
|
|
}
|
|
|
|
c.JSON(http.StatusOK, jobListResponse{
|
|
List: responseItems,
|
|
Total: len(responseItems),
|
|
})
|
|
})
|
|
router.GET("/admin/scheduler/jobs/:id", func(c *gin.Context) {
|
|
id, err := primitive.ObjectIDFromHex(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
|
return
|
|
}
|
|
|
|
item, err := deps.JobConfigStore.GetByID(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if item == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "job not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, toJobItem(*item))
|
|
})
|
|
router.POST("/admin/scheduler/jobs", func(c *gin.Context) {
|
|
var req upsertJobRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
item := model.NewJobConfig(req.Name, req.HandlerKey)
|
|
item.Enabled = req.Enabled
|
|
item.ScheduleType = req.ScheduleType
|
|
item.ScheduleValue = req.ScheduleValue
|
|
item.DefaultParams = req.DefaultParams
|
|
|
|
if err := deps.JobConfigStore.Create(c.Request.Context(), &item); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, toJobItem(item))
|
|
})
|
|
router.PUT("/admin/scheduler/jobs/:id", func(c *gin.Context) {
|
|
id, err := primitive.ObjectIDFromHex(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
|
return
|
|
}
|
|
|
|
current, err := deps.JobConfigStore.GetByID(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if current == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "job not found"})
|
|
return
|
|
}
|
|
|
|
var req upsertJobRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
current.Name = req.Name
|
|
current.HandlerKey = req.HandlerKey
|
|
current.Enabled = req.Enabled
|
|
current.ScheduleType = req.ScheduleType
|
|
current.ScheduleValue = req.ScheduleValue
|
|
current.DefaultParams = req.DefaultParams
|
|
|
|
if err := deps.JobConfigStore.Update(c.Request.Context(), current); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, toJobItem(*current))
|
|
})
|
|
router.POST("/admin/scheduler/jobs/:id/toggle", func(c *gin.Context) {
|
|
id, err := primitive.ObjectIDFromHex(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
|
return
|
|
}
|
|
|
|
var req toggleJobRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if err := deps.JobConfigStore.ToggleEnabled(c.Request.Context(), id, req.Enabled); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
})
|
|
router.POST("/admin/scheduler/jobs/:id/run", func(c *gin.Context) {
|
|
id, err := primitive.ObjectIDFromHex(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
|
return
|
|
}
|
|
|
|
job, err := deps.JobConfigStore.GetByID(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if job == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "job not found"})
|
|
return
|
|
}
|
|
|
|
var req runJobRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil && err != io.EOF {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
params := req.Params
|
|
params, err = mergeExecutionParams(job.DefaultParams, req.Params)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
now := time.Now()
|
|
execRecord := model.JobExecution{
|
|
JobConfigID: job.ID,
|
|
TriggerType: "manual",
|
|
Status: "running",
|
|
ParamsSnapshot: string(params),
|
|
StartedAt: &now,
|
|
CreatedAt: now,
|
|
}
|
|
if err := deps.ExecutionStore.Create(c.Request.Context(), &execRecord); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
execReq := jobdef.ExecuteRequest{
|
|
ExecutionID: execRecord.ID.Hex(),
|
|
JobID: job.ID.Hex(),
|
|
TriggerType: "manual",
|
|
Params: params,
|
|
LogCollector: jobdef.NewLogCollector(),
|
|
}
|
|
runErr := deps.Executor.Execute(c.Request.Context(), job.HandlerKey, execReq)
|
|
|
|
finishedAt := time.Now()
|
|
execRecord.FinishedAt = &finishedAt
|
|
execRecord.DurationMs = finishedAt.Sub(now).Milliseconds()
|
|
flushed := execReq.LogCollector.Flush(execRecord.ID.Hex())
|
|
if runErr != nil {
|
|
execRecord.Status = "failed"
|
|
execRecord.ErrorMessage = runErr.Error()
|
|
execRecord.ResultSummary = "manual execution failed"
|
|
if flushed.LogText != "" {
|
|
execRecord.LogText = flushed.LogText
|
|
} else {
|
|
execRecord.LogText = runErr.Error()
|
|
}
|
|
execRecord.LogFile = flushed.LogFile
|
|
_ = deps.JobConfigStore.UpdateRunState(c.Request.Context(), job.ID, "failed", &finishedAt)
|
|
} else {
|
|
execRecord.Status = "success"
|
|
execRecord.ResultSummary = "manual execution succeeded"
|
|
if flushed.LogText != "" {
|
|
execRecord.LogText = flushed.LogText
|
|
} else {
|
|
execRecord.LogText = "handler executed successfully"
|
|
}
|
|
execRecord.LogFile = flushed.LogFile
|
|
_ = deps.JobConfigStore.UpdateRunState(c.Request.Context(), job.ID, "success", &finishedAt)
|
|
}
|
|
|
|
if err := deps.ExecutionStore.UpdateResult(c.Request.Context(), &execRecord); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"executionID": execRecord.ID.Hex(),
|
|
"status": execRecord.Status,
|
|
"resultSummary": execRecord.ResultSummary,
|
|
"errorMessage": execRecord.ErrorMessage,
|
|
})
|
|
})
|
|
router.GET("/admin/scheduler/executions", func(c *gin.Context) {
|
|
jobs, err := deps.JobConfigStore.List(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
items, err := deps.ExecutionStore.List(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
jobNameMap := make(map[string]string, len(jobs))
|
|
for _, item := range jobs {
|
|
jobNameMap[item.ID.Hex()] = item.Name
|
|
}
|
|
|
|
responseItems := make([]executionItem, 0, len(items))
|
|
for _, item := range items {
|
|
responseItems = append(responseItems, executionItem{
|
|
ID: objectIDToString(item.ID),
|
|
JobName: jobDisplayName(jobNameMap, item.JobConfigID),
|
|
TriggerType: item.TriggerType,
|
|
Status: item.Status,
|
|
StartedAt: formatTime(item.StartedAt),
|
|
FinishedAt: formatTime(item.FinishedAt),
|
|
ResultSummary: item.ResultSummary,
|
|
})
|
|
}
|
|
|
|
c.JSON(http.StatusOK, executionListResponse{
|
|
List: responseItems,
|
|
Total: len(responseItems),
|
|
})
|
|
})
|
|
router.GET("/admin/scheduler/executions/:id", func(c *gin.Context) {
|
|
id, err := primitive.ObjectIDFromHex(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
|
return
|
|
}
|
|
|
|
item, err := deps.ExecutionStore.GetByID(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if item == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "execution not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, toExecutionDetailItem(*item))
|
|
})
|
|
router.GET("/admin/scheduler/executions/:id/logfile", func(c *gin.Context) {
|
|
id, err := primitive.ObjectIDFromHex(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
|
return
|
|
}
|
|
item, err := deps.ExecutionStore.GetByID(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if item == nil || item.LogFile == "" {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "log file not found"})
|
|
return
|
|
}
|
|
filePath := filepath.Join("logs", "joblog", item.LogFile)
|
|
if _, statErr := os.Stat(filePath); os.IsNotExist(statErr) {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "log file not found on disk"})
|
|
return
|
|
}
|
|
c.FileAttachment(filePath, item.LogFile)
|
|
})
|
|
router.GET("/admin/account/profile", func(c *gin.Context) {
|
|
profile, err := deps.ProfileStore.Get(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, toProfileItem(*profile))
|
|
})
|
|
router.PUT("/admin/account/profile", func(c *gin.Context) {
|
|
current, err := deps.ProfileStore.Get(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
var req updateProfileRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
current.Account = req.Account
|
|
current.Nickname = req.Nickname
|
|
current.Avatar = req.Avatar
|
|
current.Remark = req.Remark
|
|
|
|
if err := deps.ProfileStore.Upsert(c.Request.Context(), current); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, toProfileItem(*current))
|
|
})
|
|
router.POST("/admin/account/change-password", func(c *gin.Context) {
|
|
var req changeProfilePasswordRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if err := deps.ProfileStore.ChangePassword(c.Request.Context(), req.OldPassword, req.NewPassword); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
})
|
|
router.GET("/admin/admin-users", func(c *gin.Context) {
|
|
items, err := deps.AdminUserStore.List(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
result := make([]adminUserItem, 0, len(items))
|
|
for _, item := range items {
|
|
result = append(result, toAdminUserItem(item))
|
|
}
|
|
c.JSON(http.StatusOK, adminUserListResponse{List: result, Total: len(result)})
|
|
})
|
|
router.POST("/admin/admin-users", func(c *gin.Context) {
|
|
var req upsertAdminUserRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
item := model.AdminUser{
|
|
Account: req.Account,
|
|
Nickname: req.Nickname,
|
|
Status: req.Status,
|
|
Role: req.Role,
|
|
Remark: req.Remark,
|
|
PasswordHash: modelPasswordHash(req.Password),
|
|
}
|
|
if item.PasswordHash == "" {
|
|
item.PasswordHash = modelPasswordHash("admin123")
|
|
}
|
|
if err := deps.AdminUserStore.Create(c.Request.Context(), &item); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusCreated, toAdminUserItem(item))
|
|
})
|
|
router.PUT("/admin/admin-users/:id", func(c *gin.Context) {
|
|
id, err := primitive.ObjectIDFromHex(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
|
return
|
|
}
|
|
current, err := deps.AdminUserStore.GetByID(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if current == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "admin user not found"})
|
|
return
|
|
}
|
|
var req upsertAdminUserRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
current.Account = req.Account
|
|
current.Nickname = req.Nickname
|
|
current.Status = req.Status
|
|
current.Role = req.Role
|
|
current.Remark = req.Remark
|
|
if err := deps.AdminUserStore.Update(c.Request.Context(), current); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, toAdminUserItem(*current))
|
|
})
|
|
router.POST("/admin/admin-users/:id/change-password", func(c *gin.Context) {
|
|
id, err := primitive.ObjectIDFromHex(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
|
return
|
|
}
|
|
var req changeAdminPasswordRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if err := deps.AdminUserStore.ChangePassword(c.Request.Context(), id, req.NewPassword); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
})
|
|
router.GET("/admin/system-configs", func(c *gin.Context) {
|
|
items, err := deps.ConfigStore.List(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
result := make([]systemConfigItem, 0, len(items))
|
|
for _, item := range items {
|
|
result = append(result, toSystemConfigItem(item))
|
|
}
|
|
c.JSON(http.StatusOK, systemConfigListResponse{List: result, Total: len(result)})
|
|
})
|
|
router.POST("/admin/system-configs", func(c *gin.Context) {
|
|
var req upsertSystemConfigRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
item := model.SystemConfig{
|
|
Key: req.Key,
|
|
Title: req.Title,
|
|
Value: req.Value,
|
|
ValueType: req.ValueType,
|
|
Description: req.Description,
|
|
Enabled: req.Enabled,
|
|
}
|
|
if err := deps.ConfigStore.Create(c.Request.Context(), &item); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusCreated, toSystemConfigItem(item))
|
|
})
|
|
router.PUT("/admin/system-configs/:id", func(c *gin.Context) {
|
|
id, err := primitive.ObjectIDFromHex(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
|
return
|
|
}
|
|
current, err := deps.ConfigStore.GetByID(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if current == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "system config not found"})
|
|
return
|
|
}
|
|
var req upsertSystemConfigRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
current.Key = req.Key
|
|
current.Title = req.Title
|
|
current.Value = req.Value
|
|
current.ValueType = req.ValueType
|
|
current.Description = req.Description
|
|
current.Enabled = req.Enabled
|
|
if err := deps.ConfigStore.Update(c.Request.Context(), current); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, toSystemConfigItem(*current))
|
|
})
|
|
router.POST("/admin/system-configs/:id/toggle", func(c *gin.Context) {
|
|
id, err := primitive.ObjectIDFromHex(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
|
return
|
|
}
|
|
var req toggleSystemConfigRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if err := deps.ConfigStore.ToggleEnabled(c.Request.Context(), id, req.Enabled); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
})
|
|
|
|
return router
|
|
}
|
|
|
|
func toJobItem(item model.JobConfig) jobItem {
|
|
return jobItem{
|
|
ID: objectIDToString(item.ID),
|
|
Name: item.Name,
|
|
HandlerKey: item.HandlerKey,
|
|
Enabled: item.Enabled,
|
|
ScheduleType: item.ScheduleType,
|
|
ScheduleValue: item.ScheduleValue,
|
|
DefaultParams: item.DefaultParams,
|
|
LastStatus: item.LastStatus,
|
|
LastRunAt: formatTime(item.LastRunAt),
|
|
NextRunAt: formatTime(item.NextRunAt),
|
|
}
|
|
}
|
|
|
|
func toExecutionDetailItem(item model.JobExecution) executionDetailItem {
|
|
return executionDetailItem{
|
|
ID: objectIDToString(item.ID),
|
|
JobConfigID: objectIDToString(item.JobConfigID),
|
|
TriggerType: item.TriggerType,
|
|
Status: item.Status,
|
|
ParamsSnapshot: item.ParamsSnapshot,
|
|
StartedAt: formatTime(item.StartedAt),
|
|
FinishedAt: formatTime(item.FinishedAt),
|
|
DurationMs: item.DurationMs,
|
|
ResultSummary: item.ResultSummary,
|
|
ErrorMessage: item.ErrorMessage,
|
|
LogText: item.LogText,
|
|
LogFile: item.LogFile,
|
|
CreatedAt: item.CreatedAt.Format(time.RFC3339),
|
|
}
|
|
}
|
|
|
|
func toProfileItem(item model.AdminProfile) profileItem {
|
|
return profileItem{
|
|
ID: objectIDToString(item.ID),
|
|
Account: item.Account,
|
|
Nickname: item.Nickname,
|
|
Avatar: item.Avatar,
|
|
Remark: item.Remark,
|
|
CreatedAt: item.CreatedAt.Format(time.RFC3339),
|
|
UpdatedAt: item.UpdatedAt.Format(time.RFC3339),
|
|
}
|
|
}
|
|
|
|
func toAdminUserItem(item model.AdminUser) adminUserItem {
|
|
return adminUserItem{
|
|
ID: objectIDToString(item.ID),
|
|
Account: item.Account,
|
|
Nickname: item.Nickname,
|
|
Status: item.Status,
|
|
Role: item.Role,
|
|
Remark: item.Remark,
|
|
CreatedAt: item.CreatedAt.Format(time.RFC3339),
|
|
UpdatedAt: item.UpdatedAt.Format(time.RFC3339),
|
|
}
|
|
}
|
|
|
|
func toSystemConfigItem(item model.SystemConfig) systemConfigItem {
|
|
return systemConfigItem{
|
|
ID: objectIDToString(item.ID),
|
|
Key: item.Key,
|
|
Title: item.Title,
|
|
Value: item.Value,
|
|
ValueType: item.ValueType,
|
|
Description: item.Description,
|
|
Enabled: item.Enabled,
|
|
CreatedAt: item.CreatedAt.Format(time.RFC3339),
|
|
UpdatedAt: item.UpdatedAt.Format(time.RFC3339),
|
|
}
|
|
}
|
|
|
|
func mergeExecutionParams(defaultParams string, customParams json.RawMessage) (json.RawMessage, error) {
|
|
if len(customParams) == 0 {
|
|
if defaultParams == "" {
|
|
return nil, nil
|
|
}
|
|
if !json.Valid([]byte(defaultParams)) {
|
|
return nil, errInvalidJSON("defaultParams")
|
|
}
|
|
return json.RawMessage(defaultParams), nil
|
|
}
|
|
|
|
if !json.Valid(customParams) {
|
|
return nil, errInvalidJSON("params")
|
|
}
|
|
|
|
if defaultParams == "" {
|
|
return customParams, nil
|
|
}
|
|
|
|
if !json.Valid([]byte(defaultParams)) {
|
|
return nil, errInvalidJSON("defaultParams")
|
|
}
|
|
|
|
var base map[string]any
|
|
var override map[string]any
|
|
|
|
if err := json.Unmarshal([]byte(defaultParams), &base); err != nil {
|
|
return nil, errInvalidJSON("defaultParams")
|
|
}
|
|
if err := json.Unmarshal(customParams, &override); err != nil {
|
|
return nil, errInvalidJSON("params")
|
|
}
|
|
|
|
if base == nil || override == nil {
|
|
return customParams, nil
|
|
}
|
|
|
|
for key, value := range override {
|
|
base[key] = value
|
|
}
|
|
|
|
merged, err := json.Marshal(base)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return merged, nil
|
|
}
|
|
|
|
func errInvalidJSON(field string) error {
|
|
return fmt.Errorf("%s must be valid JSON", field)
|
|
}
|
|
|
|
func formatTime(value *time.Time) string {
|
|
if value == nil {
|
|
return ""
|
|
}
|
|
|
|
return value.Format(time.RFC3339)
|
|
}
|
|
|
|
func objectIDToString(value primitive.ObjectID) string {
|
|
if value.IsZero() {
|
|
return ""
|
|
}
|
|
|
|
return value.Hex()
|
|
}
|
|
|
|
func jobDisplayName(jobNameMap map[string]string, id primitive.ObjectID) string {
|
|
if name := jobNameMap[id.Hex()]; name != "" {
|
|
return name
|
|
}
|
|
return id.Hex()
|
|
}
|
|
|
|
func min(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func modelPasswordHash(password string) string {
|
|
if password == "" {
|
|
return ""
|
|
}
|
|
sum := sha256.Sum256([]byte(password))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|