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:
mei
2026-07-26 22:19:41 +08:00
co-authored by Claude Fable 5
parent 89e8b7fd66
commit 79774070fc
110 changed files with 8936 additions and 849 deletions
+171 -54
View File
@@ -1,116 +1,232 @@
package account
import (
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/ssdomei232/goodBaby/api/response"
"github.com/ssdomei232/goodBaby/api/rule"
"github.com/ssdomei232/goodBaby/api/user"
"github.com/ssdomei232/goodBaby/handler/db"
"github.com/ssdomei232/goodBaby/internal/accountConfigChecker"
"github.com/ssdomei232/goodBaby/internal/meta"
"github.com/ssdomei232/goodBaby/model"
)
// 添加账号
// HandleAddAccount 添加账号
func HandleAddAccount(c *gin.Context) {
gormDB, err := db.GetGormDB()
if err != nil {
c.JSON(500, gin.H{"code": 500, "data": "服务器内部错误"})
return
}
userInfo, err := user.GetUserInfoByGinCtx(c)
if err != nil {
c.JSON(401, gin.H{"code": 401, "data": "获取用户信息失败"})
response.Unauthorized(c, "获取用户信息失败")
return
}
var newAccount model.Account
if err := c.BindJSON(&newAccount); err != nil {
c.JSON(400, gin.H{"code": 400, "data": "输入参数错误"})
var req model.AccountRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "输入参数错误")
return
}
newAccount.UID = userInfo.ID
result := gormDB.Create(&newAccount)
if result.Error != nil {
c.JSON(500, gin.H{"code": 500, "data": "添加账号失败"})
if err := req.Validate(); err != nil {
response.FromError(c, err, "添加账号失败")
return
}
c.JSON(200, gin.H{"code": 200, "data": "添加账号成功"})
// 之前这里完全没有走校验器,任何 JSON 都能存进去,直到规则触发才暴露问题
registry := accountConfigChecker.InitValidatorRegistry()
if err := registry.Validate(req.Type, req.Config); err != nil {
response.BadRequest(c, err.Error())
return
}
gormDB, err := db.GetGormDB()
if err != nil {
response.ServerError(c, "服务器内部错误")
return
}
newAccount := model.Account{
UID: userInfo.ID,
Name: req.Name,
Type: req.Type,
Config: req.Config,
CreateAt: time.Now().Unix(),
}
if err := gormDB.Create(&newAccount).Error; err != nil {
response.ServerError(c, "添加账号失败")
return
}
response.OK(c, maskAccount(newAccount))
}
// 获取用户的所有账号
// HandleGetAllAccounts 获取用户的所有账号
func HandleGetAllAccounts(c *gin.Context) {
gormDB, err := db.GetGormDB()
if err != nil {
c.JSON(500, gin.H{"code": 500, "data": "服务器内部错误"})
return
}
userInfo, err := user.GetUserInfoByGinCtx(c)
if err != nil {
c.JSON(401, gin.H{"code": 401, "data": "获取用户信息失败"})
response.Unauthorized(c, "获取用户信息失败")
return
}
var accounts []model.Account
result := gormDB.Where("uid = ?", userInfo.ID).Find(&accounts)
if result.Error != nil {
c.JSON(500, gin.H{"code": 500, "data": "获取账号失败"})
gormDB, err := db.GetGormDB()
if err != nil {
response.ServerError(c, "服务器内部错误")
return
}
c.JSON(200, gin.H{"code": 200, "data": accounts})
query := gormDB.Where("uid = ?", userInfo.ID)
if accountType := c.Query("type"); accountType != "" {
query = query.Where("type = ?", accountType)
}
accounts := []model.Account{}
if err := query.Order("id DESC").Find(&accounts).Error; err != nil {
response.ServerError(c, "获取账号失败")
return
}
response.OK(c, maskAccounts(accounts))
}
// 检查删除账号请求
// HandleEditAccount 编辑账号
func HandleEditAccount(c *gin.Context) {
userInfo, err := user.GetUserInfoByGinCtx(c)
if err != nil {
response.Unauthorized(c, "获取用户信息失败")
return
}
accountID, err := parseID(c.Param("accountID"))
if err != nil {
response.BadRequest(c, "账号ID格式错误")
return
}
existing, err := findAccount(accountID, userInfo.ID)
if err != nil {
response.NotFound(c, "账号不存在")
return
}
var req model.AccountRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "输入参数错误")
return
}
if err := req.Validate(); err != nil {
response.FromError(c, err, "更新账号失败")
return
}
if req.Type != existing.Type {
response.BadRequest(c, "不支持修改账号类型,请新建账号")
return
}
registry := accountConfigChecker.InitValidatorRegistry()
// 前端提交的密码等字段可能是掩码占位符,用旧配置补回
config := meta.Unmask(req.Config, existing.Config, registry.FieldsOf(req.Type))
if err := registry.Validate(req.Type, config); err != nil {
response.BadRequest(c, err.Error())
return
}
gormDB, err := db.GetGormDB()
if err != nil {
response.ServerError(c, "服务器内部错误")
return
}
existing.Name = req.Name
existing.Config = config
if err := gormDB.Save(existing).Error; err != nil {
response.ServerError(c, "更新账号失败")
return
}
response.OK(c, maskAccount(*existing))
}
// HandleTestAccount 测试账号凭据是否可用
func HandleTestAccount(c *gin.Context) {
userInfo, err := user.GetUserInfoByGinCtx(c)
if err != nil {
response.Unauthorized(c, "获取用户信息失败")
return
}
accountID, err := parseID(c.Param("accountID"))
if err != nil {
response.BadRequest(c, "账号ID格式错误")
return
}
existing, err := findAccount(accountID, userInfo.ID)
if err != nil {
response.NotFound(c, "账号不存在")
return
}
registry := accountConfigChecker.InitValidatorRegistry()
if err := registry.Test(existing.Type, existing.Config); err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, "账号可用")
}
// HandleCheckDeleteAccount 检查删除账号请求
//
// 删除账号会同时删除相关规则,先请求该接口获取受影响的规则
func HandleCheckDeleteAccount(c *gin.Context) {
userInfo, err := user.GetUserInfoByGinCtx(c)
if err != nil {
c.JSON(401, gin.H{"code": 401, "data": "获取用户信息失败"})
return
}
accountID, err := strconv.Atoi(c.Param("accountID"))
if err != nil {
c.JSON(400, gin.H{"code": 400, "data": "账号ID格式错误"})
response.Unauthorized(c, "获取用户信息失败")
return
}
rules, err := getRulesByAccountID(uint(accountID), userInfo.ID)
accountID, err := parseID(c.Param("accountID"))
if err != nil {
c.JSON(500, gin.H{"code": 500, "data": "获取相关规则失败"})
response.BadRequest(c, "账号ID格式错误")
return
}
c.JSON(200, gin.H{"code": 200, "data": rules})
rules, err := getRulesByAccountID(accountID, userInfo.ID)
if err != nil {
response.ServerError(c, "获取相关规则失败")
return
}
response.OK(c, rules)
}
// 删除账号
// HandleDeleteAccount 删除账号及其关联规则
func HandleDeleteAccount(c *gin.Context) {
userInfo, err := user.GetUserInfoByGinCtx(c)
if err != nil {
c.JSON(401, gin.H{"code": 401, "data": "获取用户信息失败"})
response.Unauthorized(c, "获取用户信息失败")
return
}
accountID, err := strconv.Atoi(c.Param("accountID"))
accountID, err := parseID(c.Param("accountID"))
if err != nil {
c.JSON(400, gin.H{"code": 400, "data": "账号ID格式错误"})
response.BadRequest(c, "账号ID格式错误")
return
}
// 删除相关规则
rules, err := getRulesByAccountID(uint(accountID), userInfo.ID)
rules, err := getRulesByAccountID(accountID, userInfo.ID)
if err != nil {
c.JSON(500, gin.H{"code": 500, "data": "获取相关规则失败"})
response.ServerError(c, "获取相关规则失败")
return
}
for _, oneRule := range rules {
err = rule.DeleteRuleByID(oneRule.ID, userInfo.ID)
if err != nil {
c.JSON(500, gin.H{"code": 500, "data": "删除相关规则失败"})
if err := rule.DeleteRuleByID(oneRule.ID, userInfo.ID); err != nil {
response.ServerError(c, "删除相关规则失败")
return
}
}
@@ -118,13 +234,14 @@ func HandleDeleteAccount(c *gin.Context) {
// 删除账号
gormDB, err := db.GetGormDB()
if err != nil {
c.JSON(500, gin.H{"code": 500, "data": "服务器内部错误"})
response.ServerError(c, "服务器内部错误")
return
}
result := gormDB.Where("id = ? AND uid = ?", accountID, userInfo.ID).Delete(&model.Account{})
if result.Error != nil {
c.JSON(500, gin.H{"code": 500, "data": "删除账号失败"})
if err := gormDB.Where("id = ? AND uid = ?", accountID, userInfo.ID).
Delete(&model.Account{}).Error; err != nil {
response.ServerError(c, "删除账号失败")
return
}
c.JSON(200, gin.H{"code": 200, "data": "删除账号成功"})
response.OK(c, "删除账号成功")
}
+45 -5
View File
@@ -1,21 +1,61 @@
package account
import (
"strconv"
"github.com/ssdomei232/goodBaby/handler/db"
"github.com/ssdomei232/goodBaby/internal/accountConfigChecker"
"github.com/ssdomei232/goodBaby/internal/meta"
"github.com/ssdomei232/goodBaby/model"
)
// 获取账号相关的规则列表
func parseID(raw string) (uint, error) {
id, err := strconv.ParseUint(raw, 10, 64)
if err != nil {
return 0, err
}
return uint(id), nil
}
// findAccount 查找属于该用户的账号
func findAccount(accountID, uid uint) (*model.Account, error) {
gormDB, err := db.GetGormDB()
if err != nil {
return nil, err
}
var account model.Account
if err := gormDB.Where("id = ? AND uid = ?", accountID, uid).First(&account).Error; err != nil {
return nil, err
}
return &account, nil
}
// getRulesByAccountID 获取账号相关的规则列表
func getRulesByAccountID(accountID uint, uid uint) ([]*model.Rule, error) {
gormDB, err := db.GetGormDB()
if err != nil {
return nil, err
}
var rules []*model.Rule
result := gormDB.Where("account_id = ? AND uid = ?", accountID, uid).Find(&rules)
if result.Error != nil {
return nil, result.Error
rules := []*model.Rule{}
if err := gormDB.Where("account_id = ? AND uid = ?", accountID, uid).Find(&rules).Error; err != nil {
return nil, err
}
return rules, nil
}
// maskAccount 掩码账号配置中的敏感字段,避免 Cookie/密码原样返回给前端
func maskAccount(account model.Account) model.Account {
fields := accountConfigChecker.InitValidatorRegistry().FieldsOf(account.Type)
account.Config = meta.Mask(account.Config, fields)
return account
}
func maskAccounts(accounts []model.Account) []model.Account {
masked := make([]model.Account, 0, len(accounts))
for _, a := range accounts {
masked = append(masked, maskAccount(a))
}
return masked
}