暂存一下,防止丢失
This commit is contained in:
154
internal/infrastructure/notification/registry.go
Normal file
154
internal/infrastructure/notification/registry.go
Normal file
@@ -0,0 +1,154 @@
|
||||
// Package notification 提供站内通知模板注册、渲染和 PostgreSQL 持久化 Adapter。
|
||||
package notification
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
)
|
||||
|
||||
var (
|
||||
htmlTagPattern = regexp.MustCompile(`(?i)<\s*/?\s*[a-z][^>]*>`)
|
||||
sensitiveTextPattern = regexp.MustCompile(`(?i)(password|operation_password|token|secret|credential|media_id|密码|令牌|密钥)\s*[:=:]\s*\S+`)
|
||||
longURLPattern = regexp.MustCompile(`(?i)https?://\S+`)
|
||||
)
|
||||
|
||||
// Definition 定义一个受控通知类型的类别、级别、模板和允许资源引用。
|
||||
type Definition struct {
|
||||
Type string
|
||||
Category string
|
||||
Severity string
|
||||
TitleTemplate string
|
||||
BodyTemplate string
|
||||
TemplateFields map[string]struct{}
|
||||
RecipientKinds map[string]struct{}
|
||||
AllowedRefTypes map[string]struct{}
|
||||
}
|
||||
|
||||
// Rendered 是模板渲染后的纯文本快照。
|
||||
type Rendered struct {
|
||||
Category string
|
||||
Type string
|
||||
Severity string
|
||||
Title string
|
||||
Body string
|
||||
}
|
||||
|
||||
// Registry 保存代码内受控通知类型注册关系。
|
||||
type Registry struct {
|
||||
definitions map[string]Definition
|
||||
}
|
||||
|
||||
// NewRegistry 创建内置受控通知类型注册表。
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{definitions: map[string]Definition{
|
||||
constants.NotificationTypeSystemNotice: {
|
||||
Type: constants.NotificationTypeSystemNotice, Category: constants.NotificationCategorySystem,
|
||||
Severity: constants.NotificationSeverityInfo,
|
||||
TitleTemplate: "系统通知",
|
||||
BodyTemplate: "有一项系统事项需要处理,请进入对应页面查看。",
|
||||
TemplateFields: map[string]struct{}{},
|
||||
RecipientKinds: map[string]struct{}{constants.NotificationRecipientKindAccount: {}},
|
||||
AllowedRefTypes: map[string]struct{}{
|
||||
constants.NotificationRefTypeSystemConfig: {},
|
||||
constants.NotificationRefTypeIntegrationLog: {},
|
||||
},
|
||||
},
|
||||
constants.NotificationTypePackageExpiring: {
|
||||
Type: constants.NotificationTypePackageExpiring, Category: constants.NotificationCategoryExpiry,
|
||||
Severity: constants.NotificationSeverityWarning,
|
||||
TitleTemplate: "套餐即将到期",
|
||||
BodyTemplate: "您的套餐即将到期,请及时查看并处理。",
|
||||
TemplateFields: map[string]struct{}{},
|
||||
RecipientKinds: map[string]struct{}{constants.NotificationRecipientKindPersonalCustomer: {}},
|
||||
AllowedRefTypes: map[string]struct{}{
|
||||
constants.NotificationRefTypePackage: {},
|
||||
constants.NotificationRefTypeAsset: {},
|
||||
},
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
// Render 校验注册类型、模板字段、资源引用与敏感内容后生成纯文本快照。
|
||||
func (r *Registry) Render(notificationType string, data map[string]string, refType, recipientKind string) (Rendered, error) {
|
||||
definition, exists := r.definitions[notificationType]
|
||||
if !exists {
|
||||
return Rendered{}, errors.New("通知类型未注册")
|
||||
}
|
||||
if _, allowed := definition.RecipientKinds[recipientKind]; !allowed {
|
||||
return Rendered{}, errors.New("通知类型未向当前接收人开放")
|
||||
}
|
||||
if err := validateTemplateData(data, definition.TemplateFields); err != nil {
|
||||
return Rendered{}, err
|
||||
}
|
||||
if refType != "" {
|
||||
if _, allowed := definition.AllowedRefTypes[refType]; !allowed {
|
||||
return Rendered{}, errors.New("通知资源类型未注册")
|
||||
}
|
||||
}
|
||||
title, err := executeTemplate("通知标题", definition.TitleTemplate, data)
|
||||
if err != nil {
|
||||
return Rendered{}, err
|
||||
}
|
||||
body, err := executeTemplate("通知正文", definition.BodyTemplate, data)
|
||||
if err != nil {
|
||||
return Rendered{}, err
|
||||
}
|
||||
if title == "" || body == "" {
|
||||
return Rendered{}, errors.New("通知模板字段为空")
|
||||
}
|
||||
if len([]rune(title)) > constants.NotificationMaxTitleLength {
|
||||
return Rendered{}, errors.New("通知标题超过长度限制")
|
||||
}
|
||||
if len([]rune(body)) > constants.NotificationMaxBodyLength {
|
||||
return Rendered{}, errors.New("通知正文超过长度限制")
|
||||
}
|
||||
if htmlTagPattern.MatchString(title) || htmlTagPattern.MatchString(body) {
|
||||
return Rendered{}, errors.New("通知正文禁止包含 HTML")
|
||||
}
|
||||
if sensitiveTextPattern.MatchString(title) || sensitiveTextPattern.MatchString(body) || longURLPattern.MatchString(title) || longURLPattern.MatchString(body) {
|
||||
return Rendered{}, errors.New("通知正文包含禁止的敏感内容")
|
||||
}
|
||||
return Rendered{
|
||||
Category: definition.Category, Type: definition.Type, Severity: definition.Severity,
|
||||
Title: title, Body: body,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func executeTemplate(name, source string, data map[string]string) (string, error) {
|
||||
tmpl, err := template.New(name).Option("missingkey=error").Parse(source)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var buffer bytes.Buffer
|
||||
if err := tmpl.Execute(&buffer, data); err != nil {
|
||||
return "", errors.New("通知模板字段缺失")
|
||||
}
|
||||
return strings.TrimSpace(buffer.String()), nil
|
||||
}
|
||||
|
||||
func validateTemplateData(data map[string]string, fields map[string]struct{}) error {
|
||||
if data == nil && len(fields) > 0 {
|
||||
return errors.New("通知模板数据不能为空")
|
||||
}
|
||||
for key := range data {
|
||||
normalized := strings.ToLower(strings.TrimSpace(key))
|
||||
switch normalized {
|
||||
case "password", "operation_password", "token", "secret", "credential", "id_card", "callback", "media_id", "url":
|
||||
return errors.New("通知模板数据包含禁止字段")
|
||||
}
|
||||
if _, allowed := fields[normalized]; !allowed {
|
||||
return errors.New("通知模板数据包含未注册字段")
|
||||
}
|
||||
}
|
||||
for field := range fields {
|
||||
if strings.TrimSpace(data[field]) == "" {
|
||||
return errors.New("通知模板必填字段缺失")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user