feat(admin): 添加邮件模板管理接口

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
benjamin 2026-05-20 11:05:44 +08:00
parent ee1bb84727
commit 88346b4d53
3 changed files with 232 additions and 7 deletions

View File

@ -56,13 +56,14 @@ func firstNonEmpty(values ...string) string {
// SettingHandler 系统设置处理器
type SettingHandler struct {
settingService *service.SettingService
emailService *service.EmailService
turnstileService *service.TurnstileService
opsService *service.OpsService
paymentConfigService *service.PaymentConfigService
paymentService *service.PaymentService
userAttributeService *service.UserAttributeService
settingService *service.SettingService
emailService *service.EmailService
turnstileService *service.TurnstileService
opsService *service.OpsService
paymentConfigService *service.PaymentConfigService
paymentService *service.PaymentService
userAttributeService *service.UserAttributeService
notificationEmailService *service.NotificationEmailService
}
// NewSettingHandler 创建系统设置处理器
@ -78,6 +79,12 @@ func NewSettingHandler(settingService *service.SettingService, emailService *ser
}
}
// SetNotificationEmailService attaches the notification template service without changing
// the constructor signature used by existing unit tests.
func (h *SettingHandler) SetNotificationEmailService(notificationEmailService *service.NotificationEmailService) {
h.notificationEmailService = notificationEmailService
}
// GetSettings 获取所有系统设置
// GET /api/v1/admin/settings
func (h *SettingHandler) GetSettings(c *gin.Context) {
@ -3332,3 +3339,160 @@ func (h *SettingHandler) ensureUserAttributeDefinition(ctx context.Context, key,
}
slog.Info("dingtalk: created user attribute definition", "key", key, "name", name, "type", attrType)
}
// ListEmailTemplates returns all editable notification email templates.
// GET /api/v1/admin/settings/email-templates
func (h *SettingHandler) ListEmailTemplates(c *gin.Context) {
if h.notificationEmailService == nil {
response.InternalError(c, "notification email service is not configured")
return
}
events := h.notificationEmailService.ListEventInfos()
templates, err := h.notificationEmailService.ListTemplates(c.Request.Context())
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, dto.EmailTemplateListResponse{
Events: emailTemplateEventOptionsToDTO(events),
Locales: h.notificationEmailService.SupportedLocales(),
Templates: emailTemplateSummariesToDTO(templates),
Placeholders: emailTemplatePlaceholderUnion(events),
})
}
// GetEmailTemplate returns one editable notification email template.
// GET /api/v1/admin/settings/email-templates/:event/:locale
func (h *SettingHandler) GetEmailTemplate(c *gin.Context) {
if h.notificationEmailService == nil {
response.InternalError(c, "notification email service is not configured")
return
}
tmpl, err := h.notificationEmailService.GetTemplate(c.Request.Context(), c.Param("event"), c.Param("locale"))
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.Success(c, emailTemplateDetailToDTO(tmpl))
}
// UpdateEmailTemplate saves an override for one event/locale template.
// PUT /api/v1/admin/settings/email-templates/:event/:locale
func (h *SettingHandler) UpdateEmailTemplate(c *gin.Context) {
if h.notificationEmailService == nil {
response.InternalError(c, "notification email service is not configured")
return
}
var req dto.UpdateEmailTemplateRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request: "+err.Error())
return
}
tmpl, err := h.notificationEmailService.UpdateTemplate(c.Request.Context(), c.Param("event"), c.Param("locale"), req.Subject, req.HTML)
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.Success(c, emailTemplateDetailToDTO(tmpl))
}
// RestoreOfficialEmailTemplate removes an override and returns the built-in template.
// POST /api/v1/admin/settings/email-templates/:event/:locale/restore-official
func (h *SettingHandler) RestoreOfficialEmailTemplate(c *gin.Context) {
if h.notificationEmailService == nil {
response.InternalError(c, "notification email service is not configured")
return
}
tmpl, err := h.notificationEmailService.RestoreOfficialTemplate(c.Request.Context(), c.Param("event"), c.Param("locale"))
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.Success(c, emailTemplateDetailToDTO(tmpl))
}
// PreviewEmailTemplate renders a template with safe sample variables without saving it.
// POST /api/v1/admin/settings/email-templates/preview
func (h *SettingHandler) PreviewEmailTemplate(c *gin.Context) {
if h.notificationEmailService == nil {
response.InternalError(c, "notification email service is not configured")
return
}
var req dto.PreviewEmailTemplateRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request: "+err.Error())
return
}
preview, err := h.notificationEmailService.PreviewTemplate(c.Request.Context(), service.NotificationEmailPreviewInput{
Event: req.Event,
Locale: req.Locale,
Subject: req.Subject,
HTML: req.HTML,
Variables: req.Variables,
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.Success(c, dto.EmailTemplatePreviewResponse{Subject: preview.Subject, HTML: preview.HTML})
}
func emailTemplateEventOptionsToDTO(events []service.NotificationEmailEventInfo) []dto.EmailTemplateEventOption {
items := make([]dto.EmailTemplateEventOption, 0, len(events))
for _, event := range events {
items = append(items, dto.EmailTemplateEventOption{
Value: event.Event,
Label: event.Label,
Description: event.Description,
})
}
return items
}
func emailTemplateSummariesToDTO(templates []service.NotificationEmailTemplate) []dto.EmailTemplateSummary {
items := make([]dto.EmailTemplateSummary, 0, len(templates))
for _, tmpl := range templates {
items = append(items, dto.EmailTemplateSummary{
Event: tmpl.Event,
Locale: tmpl.Locale,
Subject: tmpl.Subject,
IsCustom: tmpl.IsCustom,
UpdatedAt: emailTemplateUpdatedAt(tmpl),
})
}
return items
}
func emailTemplateDetailToDTO(tmpl service.NotificationEmailTemplate) dto.EmailTemplateDetail {
return dto.EmailTemplateDetail{
Event: tmpl.Event,
Locale: tmpl.Locale,
Subject: tmpl.Subject,
HTML: tmpl.HTML,
IsCustom: tmpl.IsCustom,
UpdatedAt: emailTemplateUpdatedAt(tmpl),
Placeholders: tmpl.Placeholders,
}
}
func emailTemplateUpdatedAt(tmpl service.NotificationEmailTemplate) string {
if tmpl.UpdatedAt == nil {
return ""
}
return tmpl.UpdatedAt.Format("2006-01-02T15:04:05Z07:00")
}
func emailTemplatePlaceholderUnion(events []service.NotificationEmailEventInfo) []string {
seen := make(map[string]struct{})
placeholders := make([]string, 0)
for _, event := range events {
for _, placeholder := range event.Placeholders {
if _, ok := seen[placeholder]; ok {
continue
}
seen[placeholder] = struct{}{}
placeholders = append(placeholders, placeholder)
}
}
return placeholders
}

View File

@ -374,6 +374,62 @@ type OpenAIFastPolicySettings struct {
Rules []OpenAIFastPolicyRule `json:"rules"`
}
// EmailTemplateEventOption describes an editable notification email event.
type EmailTemplateEventOption struct {
Value string `json:"value"`
Label string `json:"label,omitempty"`
Description string `json:"description,omitempty"`
}
// EmailTemplateSummary is shown in the admin email template list.
type EmailTemplateSummary struct {
Event string `json:"event"`
Locale string `json:"locale"`
Subject string `json:"subject"`
IsCustom bool `json:"is_custom,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
}
// EmailTemplateListResponse is returned by GET /admin/settings/email-templates.
type EmailTemplateListResponse struct {
Events []EmailTemplateEventOption `json:"events"`
Locales []string `json:"locales"`
Templates []EmailTemplateSummary `json:"templates,omitempty"`
Placeholders []string `json:"placeholders,omitempty"`
}
// EmailTemplateDetail is returned for a specific event/locale template.
type EmailTemplateDetail struct {
Event string `json:"event"`
Locale string `json:"locale"`
Subject string `json:"subject"`
HTML string `json:"html"`
IsCustom bool `json:"is_custom,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
Placeholders []string `json:"placeholders,omitempty"`
}
// UpdateEmailTemplateRequest updates a template override.
type UpdateEmailTemplateRequest struct {
Subject string `json:"subject"`
HTML string `json:"html"`
}
// PreviewEmailTemplateRequest previews a template without saving it.
type PreviewEmailTemplateRequest struct {
Event string `json:"event"`
Locale string `json:"locale"`
Subject string `json:"subject"`
HTML string `json:"html"`
Variables map[string]string `json:"variables,omitempty"`
}
// EmailTemplatePreviewResponse is the rendered preview payload.
type EmailTemplatePreviewResponse struct {
Subject string `json:"subject"`
HTML string `json:"html"`
}
// ParseCustomMenuItems parses a JSON string into a slice of CustomMenuItem.
// Returns empty slice on empty/invalid input.
func ParseCustomMenuItems(raw string) []CustomMenuItem {

View File

@ -421,6 +421,11 @@ func registerSettingsRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
adminSettings.PUT("", h.Admin.Setting.UpdateSettings)
adminSettings.POST("/test-smtp", h.Admin.Setting.TestSMTPConnection)
adminSettings.POST("/send-test-email", h.Admin.Setting.SendTestEmail)
adminSettings.GET("/email-templates", h.Admin.Setting.ListEmailTemplates)
adminSettings.POST("/email-template-preview", h.Admin.Setting.PreviewEmailTemplate)
adminSettings.GET("/email-templates/:event/:locale", h.Admin.Setting.GetEmailTemplate)
adminSettings.PUT("/email-templates/:event/:locale", h.Admin.Setting.UpdateEmailTemplate)
adminSettings.POST("/email-templates/:event/:locale/restore-official", h.Admin.Setting.RestoreOfficialEmailTemplate)
// Admin API Key 管理
adminSettings.GET("/admin-api-key", h.Admin.Setting.GetAdminAPIKey)
adminSettings.POST("/admin-api-key/regenerate", h.Admin.Setting.RegenerateAdminAPIKey)