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:
+171
-54
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user