feat: 完善后端并新增 Vue3 WebUI
后端: - 修复登录校验、DB 并发(单例+WAL+纯Go sqlite)、会话密钥持久化等核心缺陷 - 新增 Timer CRUD/签到/手动触发、执行日志、仪表盘、驱动元数据等 API - 统一驱动接口: 带 context 的执行器、真实错误返回、敏感字段掩码、连通性测试 - 补全钉钉执行器; Timer 触发/提醒去重; 配置支持默认值与环境变量覆盖 前端: - Vite+Vue3+TS+Pinia+Element Plus, 由驱动元数据动态渲染配置表单 - 登录/仪表盘(倒计时环)/定时器/规则/账号/日志/设置页面, 支持暗色模式 - 主题色 #66ccff, 信号波纹 logo, go:embed 单二进制部署 构建: Dockerfile 增加前端构建阶段; 补充各驱动配置文档 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,33 +1,71 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/ssdomei232/goodBaby/api/account"
|
||||
"github.com/ssdomei232/goodBaby/api/dashboard"
|
||||
apilog "github.com/ssdomei232/goodBaby/api/log"
|
||||
apimeta "github.com/ssdomei232/goodBaby/api/meta"
|
||||
"github.com/ssdomei232/goodBaby/api/rule"
|
||||
"github.com/ssdomei232/goodBaby/api/timer"
|
||||
"github.com/ssdomei232/goodBaby/api/user"
|
||||
"github.com/ssdomei232/goodBaby/configs"
|
||||
"github.com/ssdomei232/goodBaby/handler/checker"
|
||||
"github.com/ssdomei232/goodBaby/handler/db"
|
||||
"github.com/ssdomei232/goodBaby/handler/runner"
|
||||
"github.com/ssdomei232/goodBaby/web"
|
||||
)
|
||||
|
||||
var (
|
||||
version = "dev"
|
||||
buildDate = "unknown"
|
||||
gitCommit = "unknown"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.Printf("goodBaby %s (%s, %s) 启动中...", version, gitCommit, buildDate)
|
||||
|
||||
config := configs.MustGetConfig()
|
||||
|
||||
// 初始化数据库(建表/迁移)与执行器
|
||||
db.MustInit()
|
||||
runner.InitExecutorRegistry()
|
||||
|
||||
// 启动定时检查
|
||||
c := cron.New()
|
||||
c.AddFunc("@every 10m", checker.CheckTimers)
|
||||
spec := fmt.Sprintf("@every %dm", config.CheckIntervalMinutes)
|
||||
if _, err := c.AddFunc(spec, checker.CheckTimers); err != nil {
|
||||
log.Fatalf("注册定时任务失败: %v", err)
|
||||
}
|
||||
c.Start()
|
||||
|
||||
r := gin.Default()
|
||||
store := cookie.NewStore(generateRandomKey(32))
|
||||
|
||||
// 持久化的 session 密钥:重启后已登录用户不会掉线
|
||||
store := cookie.NewStore([]byte(config.SessionSecret))
|
||||
store.Options(sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: config.SessionMaxAgeHours * 3600,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
r.Use(sessions.Sessions("goodbaby-session", store))
|
||||
|
||||
// 前端本地开发时的 CORS 支持
|
||||
if len(config.AllowedOrigins) > 0 {
|
||||
r.Use(corsMiddleware(config.AllowedOrigins))
|
||||
}
|
||||
|
||||
v1 := r.Group("/api/v1")
|
||||
{
|
||||
v1.GET("/site", apimeta.HandleGetSiteInfo)
|
||||
v1.POST("/user/registry", user.HandleRegistry)
|
||||
v1.POST("/user/login", user.HandleLogin)
|
||||
}
|
||||
@@ -36,9 +74,28 @@ func main() {
|
||||
authorized := v1.Group("/")
|
||||
authorized.Use(user.AuthMiddleware())
|
||||
{
|
||||
authorized.GET("/providers", apimeta.HandleGetProviders)
|
||||
authorized.GET("/dashboard", dashboard.HandleGetOverview)
|
||||
|
||||
users := authorized.Group("/user")
|
||||
{
|
||||
users.GET("/info", user.HandleGetUserInfo)
|
||||
users.POST("/logout", user.HandleLogout)
|
||||
users.POST("/password", user.HandleChangePassword)
|
||||
users.PUT("/notify", user.HandleUpdateNotifyConfig)
|
||||
}
|
||||
|
||||
timers := authorized.Group("/timers")
|
||||
{
|
||||
timers.GET("/", timer.HandleGetAllTimers)
|
||||
timers.POST("/", timer.HandleCreateTimer)
|
||||
timers.POST("/sign", timer.HandleSignAll)
|
||||
timers.GET("/:timerID", timer.HandleGetTimer)
|
||||
timers.PUT("/:timerID", timer.HandleEditTimer)
|
||||
timers.POST("/:timerID/sign", timer.HandleSignTimer)
|
||||
timers.POST("/:timerID/trigger", timer.HandleTriggerTimer)
|
||||
timers.GET("/:timerID/check", timer.HandleCheckDeleteTimer)
|
||||
timers.DELETE("/:timerID", timer.HandleDeleteTimer)
|
||||
}
|
||||
|
||||
rules := authorized.Group("/rules")
|
||||
@@ -46,6 +103,7 @@ func main() {
|
||||
rules.GET("/", rule.HandleGetAllRules)
|
||||
rules.POST("/", rule.HandleCreateRule)
|
||||
rules.PUT("/:ruleID", rule.HandleEditRule)
|
||||
rules.POST("/:ruleID/test", rule.HandleTestRule)
|
||||
rules.DELETE("/:ruleID", rule.HandleDeleteRule)
|
||||
}
|
||||
|
||||
@@ -53,19 +111,49 @@ func main() {
|
||||
{
|
||||
accounts.GET("/", account.HandleGetAllAccounts)
|
||||
accounts.POST("/", account.HandleAddAccount)
|
||||
accounts.PUT("/:accountID", account.HandleEditAccount)
|
||||
accounts.POST("/:accountID/test", account.HandleTestAccount)
|
||||
accounts.GET("/:accountID/check", account.HandleCheckDeleteAccount)
|
||||
accounts.DELETE("/:accountID", account.HandleDeleteAccount)
|
||||
}
|
||||
|
||||
logs := authorized.Group("/logs")
|
||||
{
|
||||
logs.GET("/", apilog.HandleGetLogs)
|
||||
logs.DELETE("/", apilog.HandleClearLogs)
|
||||
}
|
||||
}
|
||||
|
||||
r.Run(":8088")
|
||||
// 挂载内嵌前端
|
||||
web.Register(r)
|
||||
|
||||
log.Printf("goodBaby 正在监听 %s", config.ListenAddr)
|
||||
if err := r.Run(config.ListenAddr); err != nil {
|
||||
log.Fatalf("启动 HTTP 服务失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func generateRandomKey(length int) []byte {
|
||||
key := make([]byte, length)
|
||||
_, err := rand.Read(key)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("无法生成随机密钥: %v", err))
|
||||
// corsMiddleware 允许配置中的来源跨域访问(携带 Cookie)
|
||||
func corsMiddleware(allowedOrigins []string) gin.HandlerFunc {
|
||||
allowed := make(map[string]struct{}, len(allowedOrigins))
|
||||
for _, origin := range allowedOrigins {
|
||||
allowed[origin] = struct{}{}
|
||||
}
|
||||
|
||||
return func(c *gin.Context) {
|
||||
origin := c.GetHeader("Origin")
|
||||
if _, ok := allowed[origin]; ok {
|
||||
c.Header("Access-Control-Allow-Origin", origin)
|
||||
c.Header("Access-Control-Allow-Credentials", "true")
|
||||
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
c.Header("Access-Control-Allow-Headers", "Content-Type")
|
||||
c.Header("Vary", "Origin")
|
||||
}
|
||||
|
||||
if c.Request.Method == http.MethodOptions {
|
||||
c.AbortWithStatus(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user