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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
// Package dashboard 汇总首页需要的统计数据
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/ssdomei232/goodBaby/api/response"
|
||||
"github.com/ssdomei232/goodBaby/api/user"
|
||||
"github.com/ssdomei232/goodBaby/handler/db"
|
||||
"github.com/ssdomei232/goodBaby/model"
|
||||
)
|
||||
|
||||
// HandleGetOverview 返回首页概览:各类对象数量、最紧急的 Timer、最近的执行日志
|
||||
func HandleGetOverview(c *gin.Context) {
|
||||
userInfo, err := user.GetUserInfoByGinCtx(c)
|
||||
if err != nil {
|
||||
response.Unauthorized(c, "获取用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
gormDB, err := db.GetGormDB()
|
||||
if err != nil {
|
||||
response.ServerError(c, "服务器内部错误")
|
||||
return
|
||||
}
|
||||
|
||||
timers := []model.Timer{}
|
||||
if err := gormDB.Where("uid = ?", userInfo.ID).Find(&timers).Error; err != nil {
|
||||
response.ServerError(c, "获取统计数据失败")
|
||||
return
|
||||
}
|
||||
|
||||
var ruleCount, accountCount int64
|
||||
gormDB.Model(&model.Rule{}).Where("uid = ?", userInfo.ID).Count(&ruleCount)
|
||||
gormDB.Model(&model.Account{}).Where("uid = ?", userInfo.ID).Count(&accountCount)
|
||||
|
||||
recentLogs := []model.ExecutionLog{}
|
||||
gormDB.Where("uid = ?", userInfo.ID).Order("id DESC").Limit(10).Find(&recentLogs)
|
||||
|
||||
now := time.Now().Unix()
|
||||
var (
|
||||
enabledCount int
|
||||
triggeredCount int
|
||||
urgent *model.Timer
|
||||
)
|
||||
|
||||
for i := range timers {
|
||||
timer := timers[i]
|
||||
if !timer.Enabled {
|
||||
continue
|
||||
}
|
||||
enabledCount++
|
||||
if timer.Triggered {
|
||||
triggeredCount++
|
||||
continue
|
||||
}
|
||||
// 剩余时间最短的启用中 Timer
|
||||
if urgent == nil || timer.NextSignTime() < urgent.NextSignTime() {
|
||||
urgent = &timers[i]
|
||||
}
|
||||
}
|
||||
|
||||
overview := gin.H{
|
||||
"timer_count": len(timers),
|
||||
"enabled_timers": enabledCount,
|
||||
"triggered_count": triggeredCount,
|
||||
"rule_count": ruleCount,
|
||||
"account_count": accountCount,
|
||||
"server_time": now,
|
||||
"recent_logs": recentLogs,
|
||||
}
|
||||
|
||||
if urgent != nil {
|
||||
overview["urgent_timer"] = urgent
|
||||
overview["urgent_seconds_left"] = urgent.NextSignTime() - now
|
||||
}
|
||||
|
||||
response.OK(c, overview)
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
// Package log 提供规则执行日志的查询接口
|
||||
package log
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/ssdomei232/goodBaby/api/response"
|
||||
"github.com/ssdomei232/goodBaby/api/user"
|
||||
"github.com/ssdomei232/goodBaby/handler/db"
|
||||
"github.com/ssdomei232/goodBaby/model"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPageSize = 20
|
||||
maxPageSize = 200
|
||||
)
|
||||
|
||||
// HandleGetLogs 分页查询当前用户的执行日志
|
||||
func HandleGetLogs(c *gin.Context) {
|
||||
userInfo, err := user.GetUserInfoByGinCtx(c)
|
||||
if err != nil {
|
||||
response.Unauthorized(c, "获取用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
gormDB, err := db.GetGormDB()
|
||||
if err != nil {
|
||||
response.ServerError(c, "服务器内部错误")
|
||||
return
|
||||
}
|
||||
|
||||
page := intQuery(c, "page", 1, 1, 1<<20)
|
||||
pageSize := intQuery(c, "page_size", defaultPageSize, 1, maxPageSize)
|
||||
|
||||
query := gormDB.Model(&model.ExecutionLog{}).Where("uid = ?", userInfo.ID)
|
||||
if ruleID := c.Query("rule_id"); ruleID != "" {
|
||||
query = query.Where("rule_id = ?", ruleID)
|
||||
}
|
||||
if success := c.Query("success"); success == "true" || success == "false" {
|
||||
query = query.Where("success = ?", success == "true")
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
response.ServerError(c, "获取执行日志失败")
|
||||
return
|
||||
}
|
||||
|
||||
logs := []model.ExecutionLog{}
|
||||
if err := query.Order("id DESC").
|
||||
Offset((page - 1) * pageSize).
|
||||
Limit(pageSize).
|
||||
Find(&logs).Error; err != nil {
|
||||
response.ServerError(c, "获取执行日志失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, gin.H{
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"items": logs,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleClearLogs 清空当前用户的执行日志
|
||||
func HandleClearLogs(c *gin.Context) {
|
||||
userInfo, err := user.GetUserInfoByGinCtx(c)
|
||||
if err != nil {
|
||||
response.Unauthorized(c, "获取用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
gormDB, err := db.GetGormDB()
|
||||
if err != nil {
|
||||
response.ServerError(c, "服务器内部错误")
|
||||
return
|
||||
}
|
||||
|
||||
if err := gormDB.Where("uid = ?", userInfo.ID).Delete(&model.ExecutionLog{}).Error; err != nil {
|
||||
response.ServerError(c, "清空执行日志失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, "已清空执行日志")
|
||||
}
|
||||
|
||||
func intQuery(c *gin.Context, key string, fallback, min, max int) int {
|
||||
raw := c.Query(key)
|
||||
if raw == "" {
|
||||
return fallback
|
||||
}
|
||||
value, err := strconv.Atoi(raw)
|
||||
if err != nil || value < min {
|
||||
return fallback
|
||||
}
|
||||
if value > max {
|
||||
return max
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Package meta 向 WebUI 暴露驱动能力与站点信息
|
||||
package meta
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/ssdomei232/goodBaby/api/response"
|
||||
"github.com/ssdomei232/goodBaby/api/user"
|
||||
"github.com/ssdomei232/goodBaby/configs"
|
||||
"github.com/ssdomei232/goodBaby/internal/accountConfigChecker"
|
||||
"github.com/ssdomei232/goodBaby/internal/ruleConfigChecker"
|
||||
)
|
||||
|
||||
// HandleGetProviders 返回所有账号类型与规则类型的元数据
|
||||
//
|
||||
// 前端据此动态渲染配置表单,新增驱动时前端无需改动。
|
||||
func HandleGetProviders(c *gin.Context) {
|
||||
response.OK(c, gin.H{
|
||||
"accounts": accountConfigChecker.InitValidatorRegistry().Metas(),
|
||||
"rules": ruleConfigChecker.InitValidatorRegistry().Metas(),
|
||||
})
|
||||
}
|
||||
|
||||
// HandleGetSiteInfo 返回无需登录即可获取的站点信息
|
||||
func HandleGetSiteInfo(c *gin.Context) {
|
||||
config, err := configs.GetConfig()
|
||||
if err != nil {
|
||||
response.ServerError(c, "获取配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
userCount, err := user.CountUsers()
|
||||
if err != nil {
|
||||
response.ServerError(c, "服务器内部错误")
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, gin.H{
|
||||
"name": "goodBaby",
|
||||
// 系统内还没有用户时始终允许注册,方便全新部署创建第一个账号
|
||||
"enable_registry": config.EnableRegistry || userCount == 0,
|
||||
"need_initial_user": userCount == 0,
|
||||
"check_interval_minutes": config.CheckIntervalMinutes,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Package response 统一 API 的返回格式
|
||||
//
|
||||
// {"code": 200, "data": ...}
|
||||
//
|
||||
// 出错时 data 为可以直接展示给用户的错误信息。
|
||||
package response
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/ssdomei232/goodBaby/model"
|
||||
)
|
||||
|
||||
// OK 返回成功响应
|
||||
func OK(c *gin.Context, data any) {
|
||||
c.JSON(http.StatusOK, gin.H{"code": http.StatusOK, "data": data})
|
||||
}
|
||||
|
||||
// Fail 返回错误响应
|
||||
func Fail(c *gin.Context, code int, msg string) {
|
||||
c.JSON(code, gin.H{"code": code, "data": msg})
|
||||
}
|
||||
|
||||
// AbortWith 返回错误响应并终止后续 handler
|
||||
func AbortWith(c *gin.Context, code int, msg string) {
|
||||
c.AbortWithStatusJSON(code, gin.H{"code": code, "data": msg})
|
||||
}
|
||||
|
||||
// BadRequest 参数错误
|
||||
func BadRequest(c *gin.Context, msg string) { Fail(c, http.StatusBadRequest, msg) }
|
||||
|
||||
// Unauthorized 未登录
|
||||
func Unauthorized(c *gin.Context, msg string) { Fail(c, http.StatusUnauthorized, msg) }
|
||||
|
||||
// NotFound 资源不存在
|
||||
func NotFound(c *gin.Context, msg string) { Fail(c, http.StatusNotFound, msg) }
|
||||
|
||||
// ServerError 服务端错误
|
||||
func ServerError(c *gin.Context, msg string) { Fail(c, http.StatusInternalServerError, msg) }
|
||||
|
||||
// FromError 按错误类型选择合适的状态码:
|
||||
// 参数校验错误返回 400,其余返回 500
|
||||
func FromError(c *gin.Context, err error, fallback string) {
|
||||
var validationErr model.ValidationError
|
||||
if errors.As(err, &validationErr) {
|
||||
BadRequest(c, validationErr.Msg)
|
||||
return
|
||||
}
|
||||
ServerError(c, fallback)
|
||||
}
|
||||
+152
-104
@@ -1,188 +1,236 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/ssdomei232/goodBaby/api/response"
|
||||
"github.com/ssdomei232/goodBaby/api/user"
|
||||
"github.com/ssdomei232/goodBaby/handler/db"
|
||||
"github.com/ssdomei232/goodBaby/handler/runner"
|
||||
"github.com/ssdomei232/goodBaby/internal/meta"
|
||||
"github.com/ssdomei232/goodBaby/internal/retry"
|
||||
"github.com/ssdomei232/goodBaby/internal/ruleConfigChecker"
|
||||
"github.com/ssdomei232/goodBaby/model"
|
||||
)
|
||||
|
||||
// 获取用户的所有规则
|
||||
// HandleGetAllRules 获取用户的所有规则,支持按 timer_id 过滤
|
||||
func HandleGetAllRules(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 rules []model.Rule
|
||||
result := gormDB.Where("uid = ?", userInfo.ID).Find(&rules)
|
||||
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": rules})
|
||||
query := gormDB.Where("uid = ?", userInfo.ID)
|
||||
if raw := c.Query("timer_id"); raw != "" {
|
||||
timerID, err := parseID(raw)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "timer_id 格式错误")
|
||||
return
|
||||
}
|
||||
query = query.Where("timer_id = ?", timerID)
|
||||
}
|
||||
|
||||
rules := []model.Rule{}
|
||||
if err := query.Order("id DESC").Find(&rules).Error; err != nil {
|
||||
response.ServerError(c, "获取规则失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, maskRules(rules))
|
||||
}
|
||||
|
||||
// 创建新规则
|
||||
// HandleCreateRule 创建新规则
|
||||
func HandleCreateRule(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 newRule model.Rule
|
||||
if err := c.BindJSON(&newRule); err != nil {
|
||||
c.JSON(400, gin.H{"code": 400, "data": "输入参数错误"})
|
||||
var req model.RuleRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "输入参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if newRule.Type == "" {
|
||||
c.JSON(400, gin.H{"code": 400, "data": "规则类型不能为空"})
|
||||
newRule := model.Rule{
|
||||
UID: userInfo.ID,
|
||||
Name: req.Name,
|
||||
TimerID: req.TimerID,
|
||||
AccountID: req.AccountID,
|
||||
Type: req.Type,
|
||||
ConfigJson: req.ConfigJson,
|
||||
Enabled: boolOr(req.Enabled, true),
|
||||
CreateAt: time.Now().Unix(),
|
||||
}
|
||||
|
||||
if err := validateRule(&req, &newRule); err != nil {
|
||||
response.FromError(c, err, "创建规则失败")
|
||||
return
|
||||
}
|
||||
|
||||
if newRule.Name == "" {
|
||||
c.JSON(400, gin.H{"code": 400, "data": "规则名称不能为空"})
|
||||
gormDB, err := db.GetGormDB()
|
||||
if err != nil {
|
||||
response.ServerError(c, "服务器内部错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查关联账号和 Timer 是否存在(可以没有关联账号)
|
||||
if exist, err := checkRuleConfigAccountAndTimerExist(newRule); err != nil {
|
||||
c.JSON(500, gin.H{"code": 500, "data": "服务器内部错误"})
|
||||
return
|
||||
} else if !exist {
|
||||
c.JSON(400, gin.H{"code": 400, "data": "关联的账户或Timer不存在"})
|
||||
if err := gormDB.Create(&newRule).Error; err != nil {
|
||||
response.ServerError(c, "创建规则失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 规则校验
|
||||
validatorRegistry := ruleConfigChecker.InitValidatorRegistry()
|
||||
if err := validatorRegistry.Validate(newRule.Type, newRule.ConfigJson); err != nil {
|
||||
c.JSON(400, gin.H{"code": 400, "data": fmt.Sprintf("规则配置验证失败: %s", err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
newRule.UID = userInfo.ID
|
||||
result := gormDB.Create(&newRule)
|
||||
if result.Error != nil {
|
||||
c.JSON(500, gin.H{"code": 500, "data": "创建规则失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{"code": 200, "data": "规则创建成功"})
|
||||
response.OK(c, maskRule(newRule))
|
||||
}
|
||||
|
||||
// 编辑规则
|
||||
// HandleEditRule 编辑规则
|
||||
func HandleEditRule(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
|
||||
}
|
||||
|
||||
ruleID, err := strconv.Atoi(c.Param("ruleID"))
|
||||
ruleID, err := parseID(c.Param("ruleID"))
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"code": 400, "data": "规则 ID 格式错误"})
|
||||
return
|
||||
}
|
||||
var rule model.Rule
|
||||
result := gormDB.Where("id = ? AND uid = ?", ruleID, userInfo.ID).First(&rule)
|
||||
if result.Error != nil {
|
||||
c.JSON(404, gin.H{"code": 404, "data": "规则不存在"})
|
||||
response.BadRequest(c, "规则 ID 格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
var updatedRule model.Rule
|
||||
if err := c.BindJSON(&updatedRule); err != nil {
|
||||
c.JSON(400, gin.H{"code": 400, "data": "输入参数错误"})
|
||||
existing, err := findRule(ruleID, userInfo.ID)
|
||||
if err != nil {
|
||||
response.NotFound(c, "规则不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if updatedRule.Type == "" {
|
||||
c.JSON(400, gin.H{"code": 400, "data": "规则类型不能为空"})
|
||||
var req model.RuleRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "输入参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if updatedRule.Name == "" {
|
||||
c.JSON(400, gin.H{"code": 400, "data": "规则名称不能为空"})
|
||||
updated := *existing
|
||||
updated.Name = req.Name
|
||||
updated.Type = req.Type
|
||||
updated.TimerID = req.TimerID
|
||||
updated.AccountID = req.AccountID
|
||||
updated.Enabled = boolOr(req.Enabled, existing.Enabled)
|
||||
// 前端提交的敏感字段可能是掩码占位符,用旧配置补回
|
||||
updated.ConfigJson = unmaskRuleConfig(req.Type, req.ConfigJson, existing.ConfigJson)
|
||||
|
||||
req.ConfigJson = updated.ConfigJson
|
||||
if err := validateRule(&req, &updated); err != nil {
|
||||
response.FromError(c, err, "更新规则失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查关联账号和 Timer 是否存在(可以没有关联账号)
|
||||
if exist, err := checkRuleConfigAccountAndTimerExist(updatedRule); err != nil {
|
||||
c.JSON(500, gin.H{"code": 500, "data": "服务器内部错误"})
|
||||
return
|
||||
} else if !exist {
|
||||
c.JSON(400, gin.H{"code": 400, "data": "关联的账户或Timer不存在"})
|
||||
gormDB, err := db.GetGormDB()
|
||||
if err != nil {
|
||||
response.ServerError(c, "服务器内部错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 规则校验
|
||||
validatorRegistry := ruleConfigChecker.InitValidatorRegistry()
|
||||
if err := validatorRegistry.Validate(updatedRule.Type, updatedRule.ConfigJson); err != nil {
|
||||
c.JSON(400, gin.H{"code": 400, "data": fmt.Sprintf("规则配置验证失败: %s", err.Error())})
|
||||
if err := gormDB.Save(&updated).Error; err != nil {
|
||||
response.ServerError(c, "更新规则失败")
|
||||
return
|
||||
}
|
||||
|
||||
rule.Name = updatedRule.Name
|
||||
rule.Type = updatedRule.Type
|
||||
rule.ConfigJson = updatedRule.ConfigJson
|
||||
rule.AccountID = updatedRule.AccountID
|
||||
rule.TimerID = updatedRule.TimerID
|
||||
|
||||
result = gormDB.Save(&rule)
|
||||
if result.Error != nil {
|
||||
c.JSON(500, gin.H{"code": 500, "data": "更新规则失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{"code": 200, "data": "规则更新成功"})
|
||||
response.OK(c, maskRule(updated))
|
||||
}
|
||||
|
||||
// 根据删除规则
|
||||
// HandleDeleteRule 删除规则
|
||||
func HandleDeleteRule(c *gin.Context) {
|
||||
userInfo, err := user.GetUserInfoByGinCtx(c)
|
||||
if err != nil {
|
||||
c.JSON(401, gin.H{"code": 401, "data": "获取用户信息失败"})
|
||||
response.Unauthorized(c, "获取用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
ruleID, err := strconv.Atoi(c.Param("ruleID"))
|
||||
ruleID, err := parseID(c.Param("ruleID"))
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"code": 400, "data": "规则 ID 格式错误"})
|
||||
response.BadRequest(c, "规则 ID 格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
err = DeleteRuleByID(uint(ruleID), userInfo.ID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"code": 500, "data": "删除规则失败"})
|
||||
if err := DeleteRuleByID(ruleID, userInfo.ID); err != nil {
|
||||
response.ServerError(c, "删除规则失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{"code": 200, "data": "规则删除成功"})
|
||||
response.OK(c, "规则删除成功")
|
||||
}
|
||||
|
||||
// HandleTestRule 立即执行一次规则用于验证配置
|
||||
//
|
||||
// 使用较短的超时,避免在 WebUI 上等待数小时的指数退避。
|
||||
func HandleTestRule(c *gin.Context) {
|
||||
userInfo, err := user.GetUserInfoByGinCtx(c)
|
||||
if err != nil {
|
||||
response.Unauthorized(c, "获取用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
ruleID, err := parseID(c.Param("ruleID"))
|
||||
if err != nil {
|
||||
response.BadRequest(c, "规则 ID 格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
target, err := findRule(ruleID, userInfo.ID)
|
||||
if err != nil {
|
||||
response.NotFound(c, "规则不存在")
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), retry.TestTimeout)
|
||||
defer cancel()
|
||||
|
||||
if err := runner.ExecuteRuleWithContext(ctx, target, model.TriggerManual); err != nil {
|
||||
response.BadRequest(c, fmt.Sprintf("规则执行失败: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, "规则执行成功")
|
||||
}
|
||||
|
||||
// validateRule 校验规则的通用字段、关联对象与类型专属配置
|
||||
func validateRule(req *model.RuleRequest, rule *model.Rule) error {
|
||||
if err := req.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ruleMeta, ok := ruleConfigChecker.InitValidatorRegistry().MetaOf(req.Type)
|
||||
if !ok {
|
||||
return model.ErrValidation(fmt.Sprintf("不支持的规则类型: %s", req.Type))
|
||||
}
|
||||
|
||||
// 检查关联的 Timer 与账号是否存在且属于当前用户
|
||||
if err := checkRuleConfigAccountAndTimerExist(rule, ruleMeta.AccountType); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ruleConfigChecker.InitValidatorRegistry().Validate(req.Type, rule.ConfigJson); err != nil {
|
||||
return model.ErrValidation(fmt.Sprintf("规则配置验证失败: %s", err.Error()))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// unmaskRuleConfig 把提交上来的掩码字段还原成旧值
|
||||
func unmaskRuleConfig(ruleType, newConfig, oldConfig string) string {
|
||||
ruleMeta, ok := ruleConfigChecker.InitValidatorRegistry().MetaOf(ruleType)
|
||||
if !ok {
|
||||
return newConfig
|
||||
}
|
||||
return meta.Unmask(newConfig, oldConfig, ruleMeta.Fields)
|
||||
}
|
||||
|
||||
+80
-27
@@ -1,52 +1,105 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/ssdomei232/goodBaby/handler/db"
|
||||
"github.com/ssdomei232/goodBaby/internal/meta"
|
||||
"github.com/ssdomei232/goodBaby/internal/ruleConfigChecker"
|
||||
"github.com/ssdomei232/goodBaby/model"
|
||||
)
|
||||
|
||||
// 根据 ID 删除规则
|
||||
func parseID(raw string) (uint, error) {
|
||||
id, err := strconv.ParseUint(raw, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint(id), nil
|
||||
}
|
||||
|
||||
func boolOr(v *bool, fallback bool) bool {
|
||||
if v == nil {
|
||||
return fallback
|
||||
}
|
||||
return *v
|
||||
}
|
||||
|
||||
// findRule 查找属于该用户的规则
|
||||
func findRule(ruleID, uid uint) (*model.Rule, error) {
|
||||
gormDB, err := db.GetGormDB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var target model.Rule
|
||||
if err := gormDB.Where("id = ? AND uid = ?", ruleID, uid).First(&target).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &target, nil
|
||||
}
|
||||
|
||||
// DeleteRuleByID 根据 ID 删除规则
|
||||
func DeleteRuleByID(id uint, uid uint) error {
|
||||
gormDB, err := db.GetGormDB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result := gormDB.Where("id = ? AND uid = ?", id, uid).Delete(&model.Rule{})
|
||||
return result.Error
|
||||
return gormDB.Where("id = ? AND uid = ?", id, uid).Delete(&model.Rule{}).Error
|
||||
}
|
||||
|
||||
// 检查关联账号和 Timer 是否存在(可以没有关联账号)
|
||||
func checkRuleConfigAccountAndTimerExist(rule model.Rule) (exist bool, err error) {
|
||||
// checkRuleConfigAccountAndTimerExist 检查关联的 Timer 与账号是否存在且归属当前用户
|
||||
//
|
||||
// requiredAccountType 为空表示该规则类型不需要账号。
|
||||
func checkRuleConfigAccountAndTimerExist(rule *model.Rule, requiredAccountType string) error {
|
||||
gormDB, err := db.GetGormDB()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if rule.TimerID == 0 {
|
||||
return false, nil
|
||||
return err
|
||||
}
|
||||
|
||||
var count int64
|
||||
if rule.AccountID != 0 {
|
||||
result := gormDB.Model(&model.Account{}).Where("id = ? AND uid = ?", rule.AccountID, rule.UID).Count(&count)
|
||||
if result.Error != nil {
|
||||
return false, result.Error
|
||||
}
|
||||
if count == 0 {
|
||||
return false, nil
|
||||
}
|
||||
if err := gormDB.Model(&model.Timer{}).
|
||||
Where("id = ? AND uid = ?", rule.TimerID, rule.UID).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return model.ErrValidation("关联的 Timer 不存在")
|
||||
}
|
||||
|
||||
if rule.TimerID != 0 {
|
||||
result := gormDB.Model(&model.Timer{}).Where("id = ? AND uid = ?", rule.TimerID, rule.UID).Count(&count)
|
||||
if result.Error != nil {
|
||||
return false, result.Error
|
||||
}
|
||||
if count == 0 {
|
||||
return false, nil
|
||||
}
|
||||
if requiredAccountType == "" {
|
||||
// 该类型不需要账号,忽略前端可能传来的 account_id
|
||||
rule.AccountID = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
if rule.AccountID == 0 {
|
||||
return model.ErrValidation("该规则类型必须关联一个账号")
|
||||
}
|
||||
|
||||
var account model.Account
|
||||
if err := gormDB.Where("id = ? AND uid = ?", rule.AccountID, rule.UID).First(&account).Error; err != nil {
|
||||
return model.ErrValidation("关联的账号不存在")
|
||||
}
|
||||
if account.Type != requiredAccountType {
|
||||
return model.ErrValidation(fmt.Sprintf("规则需要 %s 类型的账号,但关联的是 %s", requiredAccountType, account.Type))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// maskRule 掩码规则配置中的敏感字段
|
||||
func maskRule(target model.Rule) model.Rule {
|
||||
if ruleMeta, ok := ruleConfigChecker.InitValidatorRegistry().MetaOf(target.Type); ok {
|
||||
target.ConfigJson = meta.Mask(target.ConfigJson, ruleMeta.Fields)
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
func maskRules(rules []model.Rule) []model.Rule {
|
||||
masked := make([]model.Rule, 0, len(rules))
|
||||
for _, r := range rules {
|
||||
masked = append(masked, maskRule(r))
|
||||
}
|
||||
return masked
|
||||
}
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
package timer
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/ssdomei232/goodBaby/api/response"
|
||||
"github.com/ssdomei232/goodBaby/api/user"
|
||||
"github.com/ssdomei232/goodBaby/handler/db"
|
||||
"github.com/ssdomei232/goodBaby/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// HandleGetAllTimers 获取当前用户的所有 Timer
|
||||
func HandleGetAllTimers(c *gin.Context) {
|
||||
userInfo, err := user.GetUserInfoByGinCtx(c)
|
||||
if err != nil {
|
||||
response.Unauthorized(c, "获取用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
gormDB, err := db.GetGormDB()
|
||||
if err != nil {
|
||||
response.ServerError(c, "服务器内部错误")
|
||||
return
|
||||
}
|
||||
|
||||
timers := []model.Timer{}
|
||||
if err := gormDB.Where("uid = ?", userInfo.ID).Order("id DESC").Find(&timers).Error; err != nil {
|
||||
response.ServerError(c, "获取 Timer 失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, timers)
|
||||
}
|
||||
|
||||
// HandleGetTimer 获取单个 Timer 及其关联规则数量
|
||||
func HandleGetTimer(c *gin.Context) {
|
||||
userInfo, err := user.GetUserInfoByGinCtx(c)
|
||||
if err != nil {
|
||||
response.Unauthorized(c, "获取用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
timerID, err := parseID(c.Param("timerID"))
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Timer ID 格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
timer, err := findTimer(timerID, userInfo.ID)
|
||||
if err != nil {
|
||||
response.NotFound(c, "Timer 不存在")
|
||||
return
|
||||
}
|
||||
|
||||
ruleCount, err := countRules(timerID, userInfo.ID)
|
||||
if err != nil {
|
||||
response.ServerError(c, "获取关联规则失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, gin.H{"timer": timer, "rule_count": ruleCount})
|
||||
}
|
||||
|
||||
// HandleCreateTimer 创建 Timer
|
||||
func HandleCreateTimer(c *gin.Context) {
|
||||
userInfo, err := user.GetUserInfoByGinCtx(c)
|
||||
if err != nil {
|
||||
response.Unauthorized(c, "获取用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
var req model.TimerRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "输入参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if err := req.Validate(); err != nil {
|
||||
response.FromError(c, err, "创建 Timer 失败")
|
||||
return
|
||||
}
|
||||
|
||||
gormDB, err := db.GetGormDB()
|
||||
if err != nil {
|
||||
response.ServerError(c, "服务器内部错误")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
timer := model.Timer{
|
||||
UID: userInfo.ID,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Enabled: boolOr(req.Enabled, true),
|
||||
SignDerationSeconds: req.SignDerationSeconds,
|
||||
RemindTimeSeconds: req.RemindTimeSeconds,
|
||||
// 创建即视为完成一次签到,从当前时间开始计时
|
||||
LastSign: now,
|
||||
CreateAt: now,
|
||||
}
|
||||
|
||||
if err := gormDB.Create(&timer).Error; err != nil {
|
||||
response.ServerError(c, "创建 Timer 失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, timer)
|
||||
}
|
||||
|
||||
// HandleEditTimer 编辑 Timer
|
||||
func HandleEditTimer(c *gin.Context) {
|
||||
userInfo, err := user.GetUserInfoByGinCtx(c)
|
||||
if err != nil {
|
||||
response.Unauthorized(c, "获取用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
timerID, err := parseID(c.Param("timerID"))
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Timer ID 格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
timer, err := findTimer(timerID, userInfo.ID)
|
||||
if err != nil {
|
||||
response.NotFound(c, "Timer 不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var req model.TimerRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "输入参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if err := req.Validate(); err != nil {
|
||||
response.FromError(c, err, "更新 Timer 失败")
|
||||
return
|
||||
}
|
||||
|
||||
gormDB, err := db.GetGormDB()
|
||||
if err != nil {
|
||||
response.ServerError(c, "服务器内部错误")
|
||||
return
|
||||
}
|
||||
|
||||
timer.Name = req.Name
|
||||
timer.Description = req.Description
|
||||
timer.Enabled = boolOr(req.Enabled, timer.Enabled)
|
||||
timer.SignDerationSeconds = req.SignDerationSeconds
|
||||
timer.RemindTimeSeconds = req.RemindTimeSeconds
|
||||
|
||||
if err := gormDB.Save(timer).Error; err != nil {
|
||||
response.ServerError(c, "更新 Timer 失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, timer)
|
||||
}
|
||||
|
||||
// HandleSignTimer 签到:重置计时并解除已触发状态
|
||||
func HandleSignTimer(c *gin.Context) {
|
||||
userInfo, err := user.GetUserInfoByGinCtx(c)
|
||||
if err != nil {
|
||||
response.Unauthorized(c, "获取用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
timerID, err := parseID(c.Param("timerID"))
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Timer ID 格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
timer, err := findTimer(timerID, userInfo.ID)
|
||||
if err != nil {
|
||||
response.NotFound(c, "Timer 不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if err := signTimer(timer); err != nil {
|
||||
response.ServerError(c, "签到失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, timer)
|
||||
}
|
||||
|
||||
// HandleSignAll 一键签到当前用户的所有启用中的 Timer
|
||||
func HandleSignAll(c *gin.Context) {
|
||||
userInfo, err := user.GetUserInfoByGinCtx(c)
|
||||
if err != nil {
|
||||
response.Unauthorized(c, "获取用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
gormDB, err := db.GetGormDB()
|
||||
if err != nil {
|
||||
response.ServerError(c, "服务器内部错误")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
result := gormDB.Model(&model.Timer{}).
|
||||
Where("uid = ? AND enabled = ?", userInfo.ID, true).
|
||||
Updates(map[string]any{
|
||||
"last_sign": now,
|
||||
"last_remind": 0,
|
||||
"triggered": false,
|
||||
})
|
||||
if result.Error != nil {
|
||||
response.ServerError(c, "签到失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, gin.H{"signed": result.RowsAffected, "last_sign": now})
|
||||
}
|
||||
|
||||
// HandleDeleteTimer 删除 Timer
|
||||
//
|
||||
// Timer 被删除后,挂在它下面的规则就没有触发来源了,因此一并删除。
|
||||
func HandleDeleteTimer(c *gin.Context) {
|
||||
userInfo, err := user.GetUserInfoByGinCtx(c)
|
||||
if err != nil {
|
||||
response.Unauthorized(c, "获取用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
timerID, err := parseID(c.Param("timerID"))
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Timer ID 格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := findTimer(timerID, userInfo.ID); err != nil {
|
||||
response.NotFound(c, "Timer 不存在")
|
||||
return
|
||||
}
|
||||
|
||||
gormDB, err := db.GetGormDB()
|
||||
if err != nil {
|
||||
response.ServerError(c, "服务器内部错误")
|
||||
return
|
||||
}
|
||||
|
||||
err = gormDB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("timer_id = ? AND uid = ?", timerID, userInfo.ID).Delete(&model.Rule{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Where("id = ? AND uid = ?", timerID, userInfo.ID).Delete(&model.Timer{}).Error
|
||||
})
|
||||
if err != nil {
|
||||
response.ServerError(c, "删除 Timer 失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, "Timer 删除成功")
|
||||
}
|
||||
|
||||
// HandleCheckDeleteTimer 删除前查询会被一并删除的规则
|
||||
func HandleCheckDeleteTimer(c *gin.Context) {
|
||||
userInfo, err := user.GetUserInfoByGinCtx(c)
|
||||
if err != nil {
|
||||
response.Unauthorized(c, "获取用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
timerID, err := parseID(c.Param("timerID"))
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Timer ID 格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
gormDB, err := db.GetGormDB()
|
||||
if err != nil {
|
||||
response.ServerError(c, "服务器内部错误")
|
||||
return
|
||||
}
|
||||
|
||||
rules := []model.Rule{}
|
||||
if err := gormDB.Where("timer_id = ? AND uid = ?", timerID, userInfo.ID).Find(&rules).Error; err != nil {
|
||||
response.ServerError(c, "获取相关规则失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, rules)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package timer
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ssdomei232/goodBaby/handler/db"
|
||||
"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
|
||||
}
|
||||
|
||||
func boolOr(v *bool, fallback bool) bool {
|
||||
if v == nil {
|
||||
return fallback
|
||||
}
|
||||
return *v
|
||||
}
|
||||
|
||||
// findTimer 查找属于该用户的 Timer
|
||||
func findTimer(timerID, uid uint) (*model.Timer, error) {
|
||||
gormDB, err := db.GetGormDB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var timer model.Timer
|
||||
if err := gormDB.Where("id = ? AND uid = ?", timerID, uid).First(&timer).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &timer, nil
|
||||
}
|
||||
|
||||
func countRules(timerID, uid uint) (int64, error) {
|
||||
gormDB, err := db.GetGormDB()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var count int64
|
||||
err = gormDB.Model(&model.Rule{}).Where("timer_id = ? AND uid = ?", timerID, uid).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// signTimer 完成一次签到:重置计时、清除提醒与触发标记
|
||||
func signTimer(timer *model.Timer) error {
|
||||
gormDB, err := db.GetGormDB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
if err := gormDB.Model(&model.Timer{}).Where("id = ?", timer.ID).
|
||||
Updates(map[string]any{
|
||||
"last_sign": now,
|
||||
"last_remind": 0,
|
||||
"triggered": false,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
timer.LastSign = now
|
||||
timer.LastRemind = 0
|
||||
timer.Triggered = false
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package timer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/ssdomei232/goodBaby/api/response"
|
||||
"github.com/ssdomei232/goodBaby/api/user"
|
||||
"github.com/ssdomei232/goodBaby/handler/db"
|
||||
"github.com/ssdomei232/goodBaby/handler/runner"
|
||||
"github.com/ssdomei232/goodBaby/internal/retry"
|
||||
"github.com/ssdomei232/goodBaby/model"
|
||||
)
|
||||
|
||||
// HandleTriggerTimer 手动触发一个 Timer,用于调试。
|
||||
//
|
||||
// 会真实执行该 Timer 下所有启用的规则,但不会改变 Timer 的签到/触发状态;
|
||||
// 使用较短的测试超时,避免在页面上等待数小时的重试。
|
||||
func HandleTriggerTimer(c *gin.Context) {
|
||||
userInfo, err := user.GetUserInfoByGinCtx(c)
|
||||
if err != nil {
|
||||
response.Unauthorized(c, "获取用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
timerID, err := parseID(c.Param("timerID"))
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Timer ID 格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := findTimer(timerID, userInfo.ID); err != nil {
|
||||
response.NotFound(c, "Timer 不存在")
|
||||
return
|
||||
}
|
||||
|
||||
gormDB, err := db.GetGormDB()
|
||||
if err != nil {
|
||||
response.ServerError(c, "服务器内部错误")
|
||||
return
|
||||
}
|
||||
|
||||
var rules []model.Rule
|
||||
if err := gormDB.Where("timer_id = ? AND uid = ? AND enabled = ?", timerID, userInfo.ID, true).
|
||||
Find(&rules).Error; err != nil {
|
||||
response.ServerError(c, "获取关联规则失败")
|
||||
return
|
||||
}
|
||||
|
||||
if len(rules) == 0 {
|
||||
response.BadRequest(c, "该定时器下没有启用的规则")
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), retry.TestTimeout)
|
||||
defer cancel()
|
||||
|
||||
// 并发执行所有规则,收集失败信息
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
fails []string
|
||||
)
|
||||
for i := range rules {
|
||||
rule := rules[i]
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := runner.ExecuteRuleWithContext(ctx, &rule, model.TriggerManual); err != nil {
|
||||
mu.Lock()
|
||||
fails = append(fails, rule.Name+": "+err.Error())
|
||||
mu.Unlock()
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
response.OK(c, gin.H{
|
||||
"total": len(rules),
|
||||
"failed": fails,
|
||||
})
|
||||
}
|
||||
+128
-24
@@ -1,6 +1,9 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
@@ -8,15 +11,25 @@ import (
|
||||
"github.com/ssdomei232/goodBaby/handler/db"
|
||||
"github.com/ssdomei232/goodBaby/model"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ErrUserNotFound 用户不存在
|
||||
var ErrUserNotFound = errors.New("用户不存在")
|
||||
|
||||
// ErrWrongPassword 密码错误
|
||||
var ErrWrongPassword = errors.New("用户名或密码错误")
|
||||
|
||||
// contextKey AuthMiddleware 把当前用户放进 gin.Context 时使用的 key
|
||||
const contextKey = "currentUser"
|
||||
|
||||
// 加密密码
|
||||
func encryptPassword(password string) (string, error) {
|
||||
hashedID, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hashedID), nil
|
||||
return string(hashed), nil
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
@@ -35,30 +48,62 @@ func createUser(u *model.User) error {
|
||||
return err
|
||||
}
|
||||
u.Password = hashedPassword
|
||||
|
||||
u.CreateAt = time.Now().Unix()
|
||||
|
||||
result := gormDB.Create(u)
|
||||
return result.Error
|
||||
return gormDB.Create(u).Error
|
||||
}
|
||||
|
||||
// 校验密码
|
||||
func verifyUser(u *model.User) error {
|
||||
// IsUsernameTaken 用户名是否已被占用
|
||||
func IsUsernameTaken(username string) (bool, error) {
|
||||
gormDB, err := db.GetGormDB()
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
var hashedPassword string
|
||||
result := gormDB.Where("username = ?", u.Username).First(&model.User{}).Select("password").Scan(&hashedPassword)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
var count int64
|
||||
if err := gormDB.Model(&model.User{}).Where("username = ?", username).Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return verifyPassword(hashedPassword, u.Password)
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// 通过用户名获取用户信息
|
||||
// CountUsers 返回已注册用户数,用于首个用户的引导注册
|
||||
func CountUsers() (int64, error) {
|
||||
gormDB, err := db.GetGormDB()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var count int64
|
||||
err = gormDB.Model(&model.User{}).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// verifyUser 校验用户名与密码,成功时返回用户记录
|
||||
//
|
||||
// 原实现把 First 与 Select().Scan() 串在同一条语句上,实际拿不到密码哈希,
|
||||
// 且用 sql.ErrNoRows 去比较 gorm 的错误永远不成立。
|
||||
func verifyUser(username, password string) (*model.User, error) {
|
||||
gormDB, err := db.GetGormDB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var user model.User
|
||||
if err := gormDB.Where("username = ?", username).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := verifyPassword(user.Password, password); err != nil {
|
||||
return nil, ErrWrongPassword
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetUserInfo 通过用户名获取用户信息
|
||||
func GetUserInfo(username string) (*model.User, error) {
|
||||
gormDB, err := db.GetGormDB()
|
||||
if err != nil {
|
||||
@@ -66,20 +111,79 @@ func GetUserInfo(username string) (*model.User, error) {
|
||||
}
|
||||
|
||||
var user model.User
|
||||
result := gormDB.Where("username = ?", username).First(&user)
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
if err := gormDB.Where("username = ?", username).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// 通过 gin.Context 获取用户信息
|
||||
func GetUserInfoByGinCtx(c *gin.Context) (userInfo *model.User, err error) {
|
||||
session := sessions.Default(c)
|
||||
username := session.Get("username")
|
||||
userInfo, err = GetUserInfo(username.(string))
|
||||
// GetUserByID 通过 ID 获取用户信息
|
||||
func GetUserByID(id uint) (*model.User, error) {
|
||||
gormDB, err := db.GetGormDB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return userInfo, nil
|
||||
|
||||
var user model.User
|
||||
if err := gormDB.First(&user, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetUserInfoByGinCtx 获取当前登录用户
|
||||
//
|
||||
// AuthMiddleware 已经查过一次库并写入了 context,这里优先读缓存。
|
||||
func GetUserInfoByGinCtx(c *gin.Context) (*model.User, error) {
|
||||
if cached, ok := c.Get(contextKey); ok {
|
||||
if user, ok := cached.(*model.User); ok {
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
|
||||
session := sessions.Default(c)
|
||||
uid, ok := session.Get("uid").(uint)
|
||||
if !ok {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
|
||||
user, err := GetUserByID(uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.Set(contextKey, user)
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// setSession 登录成功后写入会话
|
||||
func setSession(c *gin.Context, user *model.User) error {
|
||||
session := sessions.Default(c)
|
||||
session.Set("uid", user.ID)
|
||||
session.Set("username", user.Username)
|
||||
return session.Save()
|
||||
}
|
||||
|
||||
// validateNotifyConfig 校验钉钉提醒配置
|
||||
func validateNotifyConfig(raw *string) error {
|
||||
if raw == nil || *raw == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var config struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(*raw), &config); err != nil {
|
||||
return fmt.Errorf("钉钉配置 JSON 格式错误: %v", err)
|
||||
}
|
||||
if config.AccessToken == "" {
|
||||
return fmt.Errorf("钉钉配置中 access_token 不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+170
-58
@@ -1,118 +1,230 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/ssdomei232/goodBaby/api/response"
|
||||
"github.com/ssdomei232/goodBaby/configs"
|
||||
"github.com/ssdomei232/goodBaby/handler/db"
|
||||
"github.com/ssdomei232/goodBaby/model"
|
||||
)
|
||||
|
||||
// 处理注册请求,配置文件中可设置禁用注册
|
||||
// HandleRegistry 处理注册请求,配置文件中可设置禁用注册
|
||||
//
|
||||
// 系统内还没有任何用户时始终允许注册,否则全新部署将无法创建第一个账号。
|
||||
func HandleRegistry(c *gin.Context) {
|
||||
var userRegistryRequest model.UserRegistryReuest
|
||||
var err error
|
||||
|
||||
config, err := configs.GetConfig()
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"code": 500, "data": "获取配置文件失败"})
|
||||
response.ServerError(c, "获取配置文件失败")
|
||||
return
|
||||
}
|
||||
|
||||
if !config.EnableRegistry {
|
||||
c.JSON(403, gin.H{"code": 403, "data": "注册功能已关闭"})
|
||||
return
|
||||
}
|
||||
|
||||
err = c.BindJSON(&userRegistryRequest)
|
||||
userCount, err := CountUsers()
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"code": 400, "data": "输入错误"})
|
||||
response.ServerError(c, "服务器内部错误")
|
||||
return
|
||||
}
|
||||
|
||||
if !config.EnableRegistry && userCount > 0 {
|
||||
response.Fail(c, http.StatusForbidden, "注册功能已关闭")
|
||||
return
|
||||
}
|
||||
|
||||
var registryRequest model.UserRegistryReuest
|
||||
if err := c.ShouldBindJSON(®istryRequest); err != nil {
|
||||
response.BadRequest(c, "输入错误")
|
||||
return
|
||||
}
|
||||
|
||||
user := model.User{
|
||||
Username: userRegistryRequest.Username,
|
||||
Password: userRegistryRequest.Password,
|
||||
Username: registryRequest.Username,
|
||||
Password: registryRequest.Password,
|
||||
}
|
||||
|
||||
if err = user.IsValid(); err != nil {
|
||||
c.JSON(400, gin.H{"code": 400, "data": err.Error()})
|
||||
if err := user.IsValid(); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if isExist := user.IsExist(); isExist {
|
||||
c.JSON(400, gin.H{"code": 400, "data": "用户名已存在"})
|
||||
taken, err := IsUsernameTaken(user.Username)
|
||||
if err != nil {
|
||||
response.ServerError(c, "服务器内部错误")
|
||||
return
|
||||
}
|
||||
if taken {
|
||||
response.BadRequest(c, "用户名已存在")
|
||||
return
|
||||
}
|
||||
|
||||
if err = createUser(&user); err != nil {
|
||||
c.JSON(500, gin.H{"code": 500, "data": "注册失败"})
|
||||
if err := createUser(&user); err != nil {
|
||||
response.ServerError(c, "注册失败")
|
||||
return
|
||||
}
|
||||
|
||||
session := sessions.Default(c)
|
||||
session.Set("username", user.Username)
|
||||
session.Save()
|
||||
if err := setSession(c, &user); err != nil {
|
||||
response.ServerError(c, "写入会话失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{"code": 200, "data": "注册成功"})
|
||||
response.OK(c, "注册成功")
|
||||
}
|
||||
|
||||
// 处理登录请求
|
||||
// HandleLogin 处理登录请求
|
||||
func HandleLogin(c *gin.Context) {
|
||||
var userLoginRequest model.UserRegistryReuest
|
||||
var err error
|
||||
|
||||
err = c.BindJSON(&userLoginRequest)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"code": 400, "data": "输入错误"})
|
||||
var loginRequest model.UserRegistryReuest
|
||||
if err := c.ShouldBindJSON(&loginRequest); err != nil {
|
||||
response.BadRequest(c, "输入错误")
|
||||
return
|
||||
}
|
||||
|
||||
user := model.User{
|
||||
Username: userLoginRequest.Username,
|
||||
Password: userLoginRequest.Password,
|
||||
}
|
||||
|
||||
if err = user.IsValid(); err != nil {
|
||||
c.JSON(400, gin.H{"code": 400, "data": err.Error()})
|
||||
}
|
||||
|
||||
if err = verifyUser(&user); err == sql.ErrNoRows {
|
||||
c.JSON(400, gin.H{"code": 400, "data": "用户不存在"})
|
||||
return
|
||||
} else if err != nil {
|
||||
c.JSON(500, gin.H{"code": 500, "data": "登录失败"})
|
||||
if loginRequest.Username == "" || loginRequest.Password == "" {
|
||||
response.BadRequest(c, "用户名或密码不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
session := sessions.Default(c)
|
||||
session.Set("username", user.Username)
|
||||
session.Save()
|
||||
user, err := verifyUser(loginRequest.Username, loginRequest.Password)
|
||||
switch {
|
||||
case errors.Is(err, ErrUserNotFound), errors.Is(err, ErrWrongPassword):
|
||||
// 不区分“用户不存在”和“密码错误”,避免泄露用户名是否存在
|
||||
response.Unauthorized(c, "用户名或密码错误")
|
||||
return
|
||||
case err != nil:
|
||||
response.ServerError(c, "登录失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{"code": 200, "data": "登录成功"})
|
||||
if err := setSession(c, user); err != nil {
|
||||
response.ServerError(c, "写入会话失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, "登录成功")
|
||||
}
|
||||
|
||||
// 处理获取用户信息请求
|
||||
// HandleLogout 退出登录
|
||||
func HandleLogout(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
session.Clear()
|
||||
session.Options(sessions.Options{Path: "/", MaxAge: -1})
|
||||
if err := session.Save(); err != nil {
|
||||
response.ServerError(c, "退出登录失败")
|
||||
return
|
||||
}
|
||||
response.OK(c, "已退出登录")
|
||||
}
|
||||
|
||||
// HandleGetUserInfo 获取当前登录用户信息
|
||||
func HandleGetUserInfo(c *gin.Context) {
|
||||
userInfo, err := GetUserInfoByGinCtx(c)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"code": 500, "data": "获取用户信息失败"})
|
||||
response.Unauthorized(c, "获取用户信息失败")
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"code": 200, "data": userInfo})
|
||||
response.OK(c, userInfo)
|
||||
}
|
||||
|
||||
// 认证中间件
|
||||
// HandleChangePassword 修改密码
|
||||
func HandleChangePassword(c *gin.Context) {
|
||||
userInfo, err := GetUserInfoByGinCtx(c)
|
||||
if err != nil {
|
||||
response.Unauthorized(c, "获取用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
var req model.ChangePasswordRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "输入错误")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := verifyUser(userInfo.Username, req.OldPassword); err != nil {
|
||||
response.BadRequest(c, "原密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
candidate := model.User{Username: userInfo.Username, Password: req.NewPassword}
|
||||
if err := candidate.IsValid(); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
hashed, err := encryptPassword(req.NewPassword)
|
||||
if err != nil {
|
||||
response.ServerError(c, "修改密码失败")
|
||||
return
|
||||
}
|
||||
|
||||
gormDB, err := db.GetGormDB()
|
||||
if err != nil {
|
||||
response.ServerError(c, "服务器内部错误")
|
||||
return
|
||||
}
|
||||
if err := gormDB.Model(&model.User{}).Where("id = ?", userInfo.ID).
|
||||
Update("password", hashed).Error; err != nil {
|
||||
response.ServerError(c, "修改密码失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, "密码修改成功")
|
||||
}
|
||||
|
||||
// HandleUpdateNotifyConfig 更新提醒渠道(钉钉机器人)配置
|
||||
func HandleUpdateNotifyConfig(c *gin.Context) {
|
||||
userInfo, err := GetUserInfoByGinCtx(c)
|
||||
if err != nil {
|
||||
response.Unauthorized(c, "获取用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
var req model.NotifyConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "输入错误")
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateNotifyConfig(req.DingTalkConfig); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
gormDB, err := db.GetGormDB()
|
||||
if err != nil {
|
||||
response.ServerError(c, "服务器内部错误")
|
||||
return
|
||||
}
|
||||
|
||||
if err := gormDB.Model(&model.User{}).Where("id = ?", userInfo.ID).
|
||||
Update("ding_talk_config", req.DingTalkConfig).Error; err != nil {
|
||||
response.ServerError(c, "保存提醒配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, "提醒配置已保存")
|
||||
}
|
||||
|
||||
// AuthMiddleware 认证中间件,同时把当前用户放进 context 供后续 handler 复用
|
||||
func AuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
username := session.Get("username")
|
||||
if username == nil {
|
||||
c.JSON(401, gin.H{"code": 401, "data": "未登录"})
|
||||
c.Abort()
|
||||
uid, ok := session.Get("uid").(uint)
|
||||
if !ok {
|
||||
response.AbortWith(c, http.StatusUnauthorized, "未登录")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := GetUserByID(uid)
|
||||
if err != nil {
|
||||
// 用户已被删除,清理会话
|
||||
session.Clear()
|
||||
_ = session.Save()
|
||||
response.AbortWith(c, http.StatusUnauthorized, "未登录")
|
||||
return
|
||||
}
|
||||
|
||||
c.Set(contextKey, user)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user