feat(gateway): 优化gateway相关功能
Quality check / Web UI (push) Successful in 9m19s

This commit is contained in:
mei
2026-09-12 15:32:27 +08:00
parent 0d89b7fe8f
commit 9b656cebee
84 changed files with 2769 additions and 825 deletions
+24 -2
View File
@@ -181,7 +181,7 @@ func HandleTestAccount(c *gin.Context) {
// HandleCheckDeleteAccount 检查删除账号请求
//
// 删除账号会同时删除相关规则,先请求该接口获取受影响的规则
// 删除账号会同时删除相关规则(含消息网关规则),先请求该接口获取受影响的规则
func HandleCheckDeleteAccount(c *gin.Context) {
userInfo, err := user.GetUserInfoByGinCtx(c)
if err != nil {
@@ -201,7 +201,16 @@ func HandleCheckDeleteAccount(c *gin.Context) {
return
}
response.OK(c, rules)
gatewayRules, err := getGatewayRulesByAccountID(accountID, userInfo.ID)
if err != nil {
response.ServerError(c, "获取相关网关规则失败")
return
}
response.OK(c, gin.H{
"rules": rules,
"gateway_rules": gatewayRules,
})
}
// HandleDeleteAccount 删除账号及其关联规则
@@ -243,6 +252,19 @@ func HandleDeleteAccount(c *gin.Context) {
}
}
// 删除相关的消息网关规则
gatewayRules, err := getGatewayRulesByAccountID(accountID, userInfo.ID)
if err != nil {
response.ServerError(c, "获取相关网关规则失败")
return
}
for _, oneRule := range gatewayRules {
if err := rule.DeleteGatewayRuleByID(oneRule.ID, userInfo.ID); err != nil {
response.ServerError(c, "删除相关网关规则失败")
return
}
}
// 删除账号
gormDB, err := db.GetGormDB()
if err != nil {
+14
View File
@@ -45,6 +45,20 @@ func getRulesByAccountID(accountID uint, uid uint) ([]*model.Rule, error) {
return rules, nil
}
// getGatewayRulesByAccountID 获取账号相关的消息网关规则列表
func getGatewayRulesByAccountID(accountID uint, uid uint) ([]*model.GatewayRule, error) {
gormDB, err := db.GetGormDB()
if err != nil {
return nil, err
}
rules := []*model.GatewayRule{}
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)
+12 -8
View File
@@ -31,8 +31,10 @@ func HandleGetOverview(c *gin.Context) {
return
}
var ruleCount, accountCount int64
var ruleCount, gatewayCount, gatewayRuleCount, accountCount int64
gormDB.Model(&model.Rule{}).Where("uid = ?", userInfo.ID).Count(&ruleCount)
gormDB.Model(&model.MessageGateway{}).Where("uid = ?", userInfo.ID).Count(&gatewayCount)
gormDB.Model(&model.GatewayRule{}).Where("uid = ?", userInfo.ID).Count(&gatewayRuleCount)
gormDB.Model(&model.Account{}).Where("uid = ?", userInfo.ID).Count(&accountCount)
recentLogs := []model.ExecutionLog{}
@@ -62,13 +64,15 @@ func HandleGetOverview(c *gin.Context) {
}
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,
"timer_count": len(timers),
"enabled_timers": enabledCount,
"triggered_count": triggeredCount,
"rule_count": ruleCount,
"gateway_count": gatewayCount,
"gateway_rule_count": gatewayRuleCount,
"account_count": accountCount,
"server_time": now,
"recent_logs": recentLogs,
}
if urgent != nil {
+96 -77
View File
@@ -1,11 +1,11 @@
// Package gateway 提供消息网关的 HTTP 接口。
//
// 数据库读写都发生在这一层,真正的投递逻辑在 internal/gateway 里,
// 这里只负责鉴权、取数、组装投递任务。
package gateway
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
@@ -14,155 +14,174 @@ import (
"github.com/ssdomei232/goodBaby/api/response"
"github.com/ssdomei232/goodBaby/api/user"
"github.com/ssdomei232/goodBaby/handler/db"
"github.com/ssdomei232/goodBaby/handler/runner"
gatewaycore "github.com/ssdomei232/goodBaby/internal/gateway"
"github.com/ssdomei232/goodBaby/internal/retry"
"github.com/ssdomei232/goodBaby/model"
"gorm.io/gorm"
)
// webhookRequest 外部系统投递消息的请求体
type webhookRequest struct {
Message string `json:"message"`
Title string `json:"title"`
}
func token() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return "gw_" + hex.EncodeToString(b), nil
}
// HandleList 获取当前用户的所有消息网关
func HandleList(c *gin.Context) {
u, err := user.GetUserInfoByGinCtx(c)
userInfo, err := user.GetUserInfoByGinCtx(c)
if err != nil {
response.Unauthorized(c, "未登录")
return
}
dbConn, err := db.GetGormDB()
if err != nil {
response.ServerError(c, "获取网关失败")
return
}
var items []model.MessageGateway
if err := dbConn.Where("uid = ?", u.ID).Order("id DESC").Find(&items).Error; err != nil {
items := []model.MessageGateway{}
if err := dbConn.Where("uid = ?", userInfo.ID).Order("id DESC").Find(&items).Error; err != nil {
response.ServerError(c, "获取网关失败")
return
}
response.OK(c, items)
}
// HandleCreate 创建消息网关
func HandleCreate(c *gin.Context) {
u, err := user.GetUserInfoByGinCtx(c)
userInfo, err := user.GetUserInfoByGinCtx(c)
if err != nil {
response.Unauthorized(c, "未登录")
return
}
var req model.MessageGatewayRequest
if c.ShouldBindJSON(&req) != nil || strings.TrimSpace(req.Name) == "" {
response.BadRequest(c, "网关名称和规则不能为空")
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "输入参数错误")
return
}
dbConn, err := db.GetGormDB()
if err != nil {
response.ServerError(c, "创建网关失败")
req.Name = strings.TrimSpace(req.Name)
if err := req.Validate(); err != nil {
response.FromError(c, err, "创建网关失败")
return
}
key, err := token()
// 不填类型时用默认网关,填了就必须是已注册的类型
if req.Type == "" {
req.Type = model.GatewayTypeWebhook
}
if _, ok := gatewaycore.InitGatewayRegistry().Resolve(req.Type); !ok {
response.BadRequest(c, "不支持的消息网关类型: "+req.Type)
return
}
token, err := gatewaycore.NewToken()
if err != nil {
response.ServerError(c, "生成网关 Token 失败")
return
}
item := model.MessageGateway{UID: u.ID, Name: strings.TrimSpace(req.Name), Token: key, CreateAt: time.Now().Unix()}
dbConn, err := db.GetGormDB()
if err != nil {
response.ServerError(c, "创建网关失败")
return
}
item := model.MessageGateway{
UID: userInfo.ID,
Name: req.Name,
Type: req.Type,
Token: token,
CreateAt: time.Now().Unix(),
}
if err := dbConn.Create(&item).Error; err != nil {
response.ServerError(c, "创建网关失败")
return
}
response.OK(c, item)
}
// HandleDelete 删除消息网关,绑定在它下面的规则一并删除
func HandleDelete(c *gin.Context) {
u, err := user.GetUserInfoByGinCtx(c)
userInfo, err := user.GetUserInfoByGinCtx(c)
if err != nil {
response.Unauthorized(c, "未登录")
return
}
gatewayID, err := parseID(c.Param("gatewayID"))
if err != nil {
response.BadRequest(c, "网关 ID 格式错误")
return
}
dbConn, err := db.GetGormDB()
if err != nil {
response.ServerError(c, "删除网关失败")
return
}
if err := dbConn.Where("id = ? AND uid = ?", c.Param("gatewayID"), u.ID).Delete(&model.MessageGateway{}).Error; err != nil {
err = dbConn.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("gateway_id = ? AND uid = ?", gatewayID, userInfo.ID).
Delete(&model.GatewayRule{}).Error; err != nil {
return err
}
return tx.Where("id = ? AND uid = ?", gatewayID, userInfo.ID).
Delete(&model.MessageGateway{}).Error
})
if err != nil {
response.ServerError(c, "删除网关失败")
return
}
response.OK(c, "网关已删除")
}
// HandleWebhook 接收外部系统投递的消息,触发绑定在该网关上的规则
func HandleWebhook(c *gin.Context) {
var req webhookRequest
if err := c.ShouldBindJSON(&req); err != nil || strings.TrimSpace(req.Message) == "" {
response.BadRequest(c, "message 不能为空")
return
}
dbConn, err := db.GetGormDB()
if err != nil {
response.ServerError(c, "网关不可用")
return
}
var gw model.MessageGateway
if err := dbConn.Where("token = ?", c.Param("token")).First(&gw).Error; err != nil {
var target model.MessageGateway
if err := dbConn.Where("token = ?", c.Param("token")).First(&target).Error; err != nil {
response.Fail(c, http.StatusNotFound, "网关不存在")
return
}
var req webhookRequest
if c.ShouldBindJSON(&req) != nil || strings.TrimSpace(req.Message) == "" {
response.BadRequest(c, "message 不能为空")
return
}
var rules []model.Rule
if err := dbConn.Where("uid = ? AND gateway_id = ? AND enabled = ?", gw.UID, gw.ID, true).Find(&rules).Error; err != nil {
rules := []model.GatewayRule{}
if err := dbConn.Where("uid = ? AND gateway_id = ? AND enabled = ?", target.UID, target.ID, true).
Find(&rules).Error; err != nil {
response.ServerError(c, "读取网关规则失败")
return
}
// 投递需要在 HTTP 请求内返回结果,因此用较短的超时
ctx, cancel := context.WithTimeout(context.Background(), retry.TestTimeout)
defer cancel()
fails := make([]string, 0)
for i := range rules {
rule := rules[i]
cfg, err := overrideMessage(rule.ConfigJson, req.Title, req.Message)
if err != nil {
fails = append(fails, rule.Name+": 规则不支持消息网关")
continue
}
rule.ConfigJson = cfg
if err := runner.ExecuteRuleWithContext(ctx, &rule, "webhook"); err != nil {
fails = append(fails, rule.Name+": "+err.Error())
}
}
response.OK(c, gin.H{"total": len(rules), "failed": fails})
}
func overrideMessage(raw, title, message string) (string, error) {
var obj map[string]any
if err := json.Unmarshal([]byte(raw), &obj); err != nil {
return raw, err
result, err := gatewaycore.InitGatewayRegistry().Deliver(ctx, target.Type, &gatewaycore.Task{
Gateway: &target,
Rules: rules,
Message: gatewaycore.Message{
Title: strings.TrimSpace(req.Title),
Content: strings.TrimSpace(req.Message),
},
})
if err != nil {
response.ServerError(c, err.Error())
return
}
matched := false
if _, ok := obj["msg"]; ok {
obj["msg"] = message
matched = true
}
if _, ok := obj["message"]; ok {
obj["message"] = message
matched = true
}
if _, ok := obj["body"]; ok {
obj["body"] = message
matched = true
}
if !matched {
return raw, fmt.Errorf("message field not found")
}
if title != "" {
if _, ok := obj["title"]; ok {
obj["title"] = title
}
}
b, err := json.Marshal(obj)
return string(b), err
response.OK(c, result)
}
+12
View File
@@ -0,0 +1,12 @@
package gateway
import "strconv"
// parseID 解析 URL 里的数字 ID
func parseID(raw string) (uint, error) {
id, err := strconv.ParseUint(raw, 10, 64)
if err != nil {
return 0, err
}
return uint(id), nil
}
+3
View File
@@ -37,6 +37,9 @@ func HandleGetLogs(c *gin.Context) {
if ruleID := c.Query("rule_id"); ruleID != "" {
query = query.Where("rule_id = ?", ruleID)
}
if gatewayRuleID := c.Query("gateway_rule_id"); gatewayRuleID != "" {
query = query.Where("gateway_rule_id = ?", gatewayRuleID)
}
if success := c.Query("success"); success == "true" || success == "false" {
query = query.Where("success = ?", success == "true")
}
+3 -1
View File
@@ -7,16 +7,18 @@ import (
"github.com/ssdomei232/goodBaby/api/user"
"github.com/ssdomei232/goodBaby/configs"
"github.com/ssdomei232/goodBaby/internal/accountConfigChecker"
"github.com/ssdomei232/goodBaby/internal/gateway"
"github.com/ssdomei232/goodBaby/internal/ruleConfigChecker"
)
// HandleGetProviders 返回所有账号类型与规则类型的元数据
// HandleGetProviders 返回所有账号类型、规则类型与消息网关类型的元数据
//
// 前端据此动态渲染配置表单,新增驱动时前端无需改动。
func HandleGetProviders(c *gin.Context) {
response.OK(c, gin.H{
"accounts": accountConfigChecker.InitValidatorRegistry().Metas(),
"rules": ruleConfigChecker.InitValidatorRegistry().Metas(),
"gateways": gateway.InitGatewayRegistry().Metas(),
})
}
+246
View File
@@ -0,0 +1,246 @@
package rule
import (
"context"
"fmt"
"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/gateway"
"github.com/ssdomei232/goodBaby/internal/meta"
"github.com/ssdomei232/goodBaby/internal/retry"
"github.com/ssdomei232/goodBaby/model"
)
// HandleGetAllGatewayRules 获取用户的消息网关规则,支持按 gateway_id 过滤
//
// 消息网关规则和定时器规则是两张表,互不影响,因此这里单独一组接口。
func HandleGetAllGatewayRules(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
}
query := gormDB.Where("uid = ?", userInfo.ID)
if raw := c.Query("gateway_id"); raw != "" {
gatewayID, err := parseID(raw)
if err != nil {
response.BadRequest(c, "gateway_id 格式错误")
return
}
query = query.Where("gateway_id = ?", gatewayID)
}
rules := []model.GatewayRule{}
if err := query.Order("id DESC").Find(&rules).Error; err != nil {
response.ServerError(c, "获取网关规则失败")
return
}
response.OK(c, maskGatewayRules(rules))
}
// HandleCreateGatewayRule 创建消息网关规则
func HandleCreateGatewayRule(c *gin.Context) {
userInfo, err := user.GetUserInfoByGinCtx(c)
if err != nil {
response.Unauthorized(c, "获取用户信息失败")
return
}
var req model.GatewayRuleRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "输入参数错误")
return
}
newRule := model.GatewayRule{
UID: userInfo.ID,
Name: req.Name,
GatewayID: req.GatewayID,
AccountID: req.AccountID,
Type: req.Type,
ConfigJson: req.ConfigJson,
Enabled: boolOr(req.Enabled, true),
CreateAt: time.Now().Unix(),
}
if err := validateGatewayRule(&req, &newRule); err != nil {
response.FromError(c, err, "创建网关规则失败")
return
}
gormDB, err := db.GetGormDB()
if err != nil {
response.ServerError(c, "服务器内部错误")
return
}
if err := gormDB.Create(&newRule).Error; err != nil {
response.ServerError(c, "创建网关规则失败")
return
}
response.OK(c, maskGatewayRule(newRule))
}
// HandleEditGatewayRule 编辑消息网关规则
func HandleEditGatewayRule(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
}
existing, err := findGatewayRule(ruleID, userInfo.ID)
if err != nil {
response.NotFound(c, "网关规则不存在")
return
}
var req model.GatewayRuleRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "输入参数错误")
return
}
updated := *existing
updated.Name = req.Name
updated.Type = req.Type
updated.GatewayID = req.GatewayID
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 := validateGatewayRule(&req, &updated); err != nil {
response.FromError(c, err, "更新网关规则失败")
return
}
gormDB, err := db.GetGormDB()
if err != nil {
response.ServerError(c, "服务器内部错误")
return
}
if err := gormDB.Save(&updated).Error; err != nil {
response.ServerError(c, "更新网关规则失败")
return
}
response.OK(c, maskGatewayRule(updated))
}
// HandleDeleteGatewayRule 删除消息网关规则
func HandleDeleteGatewayRule(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
}
if _, err := findGatewayRule(ruleID, userInfo.ID); err != nil {
response.NotFound(c, "网关规则不存在")
return
}
if err := DeleteGatewayRuleByID(ruleID, userInfo.ID); err != nil {
response.ServerError(c, "删除网关规则失败")
return
}
response.OK(c, "网关规则删除成功")
}
// HandleTestGatewayRule 立即执行一次网关规则,用于验证配置
//
// 与投递不同:这里用规则里保存的配置,不覆盖外部消息。
func HandleTestGatewayRule(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 := findGatewayRule(ruleID, userInfo.ID)
if err != nil {
response.NotFound(c, "网关规则不存在")
return
}
// 用一条示例消息走一次真实的投递覆盖,让用户看到规则最终会发出什么内容
if configJSON, applied, err := gateway.ApplyMessage(target.ConfigJson, gateway.TestMessage); err == nil && applied {
target.ConfigJson = configJSON
}
ctx, cancel := context.WithTimeout(context.Background(), retry.TestTimeout)
defer cancel()
if err := runner.ExecuteGatewayRuleWithContext(ctx, target, model.TriggerManual); err != nil {
response.BadRequest(c, fmt.Sprintf("规则执行失败: %s", err.Error()))
return
}
response.OK(c, "规则执行成功")
}
// validateGatewayRule 校验网关规则的通用字段、关联对象与类型专属配置
func validateGatewayRule(req *model.GatewayRuleRequest, rule *model.GatewayRule) error {
if err := req.Validate(); err != nil {
return err
}
if err := checkGatewayExists(rule.GatewayID, rule.UID); err != nil {
return err
}
ruleMeta, err := ruleMetaOf(req.Type)
if err != nil {
return err
}
// 标题/内容由投递请求提供,页面不填,这里补占位值后再校验
rule.ConfigJson = meta.FillGatewayMessages(rule.ConfigJson, ruleMeta.Fields)
if err := validateRuleConfig(req.Type, rule.ConfigJson); err != nil {
return err
}
accountID, err := checkAccount(rule.AccountID, rule.UID, ruleMeta.AccountType)
if err != nil {
return err
}
rule.AccountID = accountID
return nil
}
+14 -11
View File
@@ -67,7 +67,6 @@ func HandleCreateRule(c *gin.Context) {
UID: userInfo.ID,
Name: req.Name,
TimerID: req.TimerID,
GatewayID: req.GatewayID,
AccountID: req.AccountID,
Type: req.Type,
ConfigJson: req.ConfigJson,
@@ -124,7 +123,6 @@ func HandleEditRule(c *gin.Context) {
updated.Name = req.Name
updated.Type = req.Type
updated.TimerID = req.TimerID
updated.GatewayID = req.GatewayID
updated.AccountID = req.AccountID
updated.Enabled = boolOr(req.Enabled, existing.Enabled)
// 前端提交的敏感字段可能是掩码占位符,用旧配置补回
@@ -221,19 +219,24 @@ func validateRule(req *model.RuleRequest, rule *model.Rule) error {
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 {
// 检查关联的 Timer 是否存在、账号是否可用
if err := checkTimerExists(rule.TimerID, rule.UID); err != nil {
return err
}
if err := ruleConfigChecker.InitValidatorRegistry().Validate(req.Type, rule.ConfigJson); err != nil {
return model.ErrValidation(fmt.Sprintf("规则配置验证失败: %s", err.Error()))
ruleMeta, err := ruleMetaOf(req.Type)
if err != nil {
return err
}
if err := validateRuleConfig(req.Type, rule.ConfigJson); err != nil {
return err
}
accountID, err := checkAccount(rule.AccountID, rule.UID, ruleMeta.AccountType)
if err != nil {
return err
}
rule.AccountID = accountID
return nil
}
+136 -46
View File
@@ -25,7 +25,7 @@ func boolOr(v *bool, fallback bool) bool {
return *v
}
// findRule 查找属于该用户的规则
// findRule 查找属于该用户的定时器规则
func findRule(ruleID, uid uint) (*model.Rule, error) {
gormDB, err := db.GetGormDB()
if err != nil {
@@ -39,7 +39,21 @@ func findRule(ruleID, uid uint) (*model.Rule, error) {
return &target, nil
}
// DeleteRuleByID 根据 ID 删除规则
// findGatewayRule 查找属于该用户的消息网关规则
func findGatewayRule(ruleID, uid uint) (*model.GatewayRule, error) {
gormDB, err := db.GetGormDB()
if err != nil {
return nil, err
}
var target model.GatewayRule
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 {
@@ -49,58 +63,99 @@ func DeleteRuleByID(id uint, uid uint) error {
return gormDB.Where("id = ? AND uid = ?", id, uid).Delete(&model.Rule{}).Error
}
// checkRuleConfigAccountAndTimerExist 检查关联的 Timer 与账号是否存在且归属当前用户
//
// requiredAccountType 为空表示该规则类型不需要账号。
func checkRuleConfigAccountAndTimerExist(rule *model.Rule, requiredAccountType string) error {
// DeleteGatewayRuleByID 根据 ID 删除消息网关规则
func DeleteGatewayRuleByID(id uint, uid uint) error {
gormDB, err := db.GetGormDB()
if err != nil {
return err
}
return gormDB.Where("id = ? AND uid = ?", id, uid).Delete(&model.GatewayRule{}).Error
}
// checkTimerExists 检查关联的 Timer 是否存在且归属当前用户
func checkTimerExists(timerID, uid uint) error {
gormDB, err := db.GetGormDB()
if err != nil {
return err
}
var count int64
if rule.TimerID != 0 {
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 不存在")
}
} else {
if err := gormDB.Model(&model.MessageGateway{}).Where("id = ? AND uid = ?", rule.GatewayID, rule.UID).Count(&count).Error; err != nil {
return err
}
if count == 0 {
return model.ErrValidation("关联的消息网关不存在")
}
if err := gormDB.Model(&model.Timer{}).Where("id = ? AND uid = ?", timerID, uid).
Count(&count).Error; err != nil {
return err
}
if requiredAccountType == "" {
// 该类型不需要账号,忽略前端可能传来的 account_id
rule.AccountID = 0
return nil
if count == 0 {
return model.ErrValidation("关联的 Timer 不存在")
}
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)
// checkGatewayExists 检查关联的消息网关是否存在且归属当前用户
func checkGatewayExists(gatewayID, uid uint) error {
gormDB, err := db.GetGormDB()
if err != nil {
return err
}
var count int64
if err := gormDB.Model(&model.MessageGateway{}).Where("id = ? AND uid = ?", gatewayID, uid).
Count(&count).Error; err != nil {
return err
}
if count == 0 {
return model.ErrValidation("关联的消息网关不存在")
}
return nil
}
// checkAccount 校验规则关联的账号存在、归属当前用户且类型匹配。
//
// requiredType 为空表示该规则类型不需要账号,此时返回 0,调用方据此清掉 account_id。
func checkAccount(accountID, uid uint, requiredType string) (uint, error) {
if requiredType == "" {
return 0, nil
}
if accountID == 0 {
return 0, model.ErrValidation("该规则类型必须关联一个账号")
}
gormDB, err := db.GetGormDB()
if err != nil {
return 0, err
}
var account model.Account
if err := gormDB.Where("id = ? AND uid = ?", accountID, uid).First(&account).Error; err != nil {
return 0, model.ErrValidation("关联的账号不存在")
}
if account.Type != requiredType {
return 0, model.ErrValidation(
fmt.Sprintf("规则需要 %s 类型的账号,但关联的是 %s", requiredType, account.Type))
}
return accountID, nil
}
// ruleMetaOf 取规则类型的元数据,不支持的类型返回校验错误
func ruleMetaOf(ruleType string) (meta.RuleMeta, error) {
ruleMeta, ok := ruleConfigChecker.InitValidatorRegistry().MetaOf(ruleType)
if !ok {
return meta.RuleMeta{}, model.ErrValidation(fmt.Sprintf("不支持的规则类型: %s", ruleType))
}
return ruleMeta, nil
}
// validateRuleConfig 校验类型专属的规则配置
func validateRuleConfig(ruleType, configJSON string) error {
if err := ruleConfigChecker.InitValidatorRegistry().Validate(ruleType, configJSON); err != nil {
return model.ErrValidation(fmt.Sprintf("规则配置验证失败: %s", err.Error()))
}
return nil
}
// maskRule 掩码定时器规则配置中的敏感字段
func maskRule(target model.Rule) model.Rule {
target.ConfigJson = maskConfig(target.Type, target.ConfigJson)
return target
}
@@ -112,17 +167,52 @@ func maskRules(rules []model.Rule) []model.Rule {
return masked
}
// getRuleOwnerUID 获取规则所属用户的 UID
// maskGatewayRule 掩码消息网关规则配置中的敏感字段
func maskGatewayRule(target model.GatewayRule) model.GatewayRule {
target.ConfigJson = maskConfig(target.Type, target.ConfigJson)
return target
}
func maskGatewayRules(rules []model.GatewayRule) []model.GatewayRule {
masked := make([]model.GatewayRule, 0, len(rules))
for _, r := range rules {
masked = append(masked, maskGatewayRule(r))
}
return masked
}
// maskConfig 掩码规则配置中的敏感字段
func maskConfig(ruleType, configJSON string) string {
if ruleMeta, ok := ruleConfigChecker.InitValidatorRegistry().MetaOf(ruleType); ok {
return meta.Mask(configJSON, ruleMeta.Fields)
}
return configJSON
}
// getRuleOwnerUID 获取定时器规则所属用户的 UID
func getRuleOwnerUID(ruleID uint) (uint, error) {
gormDB, err := db.GetGormDB()
if err != nil {
return 0, err
}
var rule model.Rule
if err := gormDB.Select("uid").Where("id = ?", ruleID).First(&rule).Error; err != nil {
var target model.Rule
if err := gormDB.Select("uid").Where("id = ?", ruleID).First(&target).Error; err != nil {
return 0, err
}
return target.UID, nil
}
// getGatewayRuleOwnerUID 获取消息网关规则所属用户的 UID
func getGatewayRuleOwnerUID(ruleID uint) (uint, error) {
gormDB, err := db.GetGormDB()
if err != nil {
return 0, err
}
return rule.UID, nil
var target model.GatewayRule
if err := gormDB.Select("uid").Where("id = ?", ruleID).First(&target).Error; err != nil {
return 0, err
}
return target.UID, nil
}
+5 -6
View File
@@ -3,13 +3,13 @@ package user
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/ssdomei232/goodBaby/drivers/dingtalk"
"github.com/ssdomei232/goodBaby/handler/db"
"github.com/ssdomei232/goodBaby/model"
"golang.org/x/crypto/bcrypt"
@@ -214,11 +214,10 @@ func validateNotifyConfig(raw *string) error {
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)
// 复用钉钉驱动里的解析逻辑,避免两边对配置格式的理解不一致
config, err := dingtalk.ParseAccountConfig(*raw)
if err != nil {
return err
}
if config.AccessToken == "" {
return fmt.Errorf("钉钉配置中 access_token 不能为空")