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:
parent
ee1bb84727
commit
88346b4d53
@ -56,13 +56,14 @@ func firstNonEmpty(values ...string) string {
|
|||||||
|
|
||||||
// SettingHandler 系统设置处理器
|
// SettingHandler 系统设置处理器
|
||||||
type SettingHandler struct {
|
type SettingHandler struct {
|
||||||
settingService *service.SettingService
|
settingService *service.SettingService
|
||||||
emailService *service.EmailService
|
emailService *service.EmailService
|
||||||
turnstileService *service.TurnstileService
|
turnstileService *service.TurnstileService
|
||||||
opsService *service.OpsService
|
opsService *service.OpsService
|
||||||
paymentConfigService *service.PaymentConfigService
|
paymentConfigService *service.PaymentConfigService
|
||||||
paymentService *service.PaymentService
|
paymentService *service.PaymentService
|
||||||
userAttributeService *service.UserAttributeService
|
userAttributeService *service.UserAttributeService
|
||||||
|
notificationEmailService *service.NotificationEmailService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSettingHandler 创建系统设置处理器
|
// 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 获取所有系统设置
|
// GetSettings 获取所有系统设置
|
||||||
// GET /api/v1/admin/settings
|
// GET /api/v1/admin/settings
|
||||||
func (h *SettingHandler) GetSettings(c *gin.Context) {
|
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)
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -374,6 +374,62 @@ type OpenAIFastPolicySettings struct {
|
|||||||
Rules []OpenAIFastPolicyRule `json:"rules"`
|
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.
|
// ParseCustomMenuItems parses a JSON string into a slice of CustomMenuItem.
|
||||||
// Returns empty slice on empty/invalid input.
|
// Returns empty slice on empty/invalid input.
|
||||||
func ParseCustomMenuItems(raw string) []CustomMenuItem {
|
func ParseCustomMenuItems(raw string) []CustomMenuItem {
|
||||||
|
|||||||
@ -421,6 +421,11 @@ func registerSettingsRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|||||||
adminSettings.PUT("", h.Admin.Setting.UpdateSettings)
|
adminSettings.PUT("", h.Admin.Setting.UpdateSettings)
|
||||||
adminSettings.POST("/test-smtp", h.Admin.Setting.TestSMTPConnection)
|
adminSettings.POST("/test-smtp", h.Admin.Setting.TestSMTPConnection)
|
||||||
adminSettings.POST("/send-test-email", h.Admin.Setting.SendTestEmail)
|
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 管理
|
// Admin API Key 管理
|
||||||
adminSettings.GET("/admin-api-key", h.Admin.Setting.GetAdminAPIKey)
|
adminSettings.GET("/admin-api-key", h.Admin.Setting.GetAdminAPIKey)
|
||||||
adminSettings.POST("/admin-api-key/regenerate", h.Admin.Setting.RegenerateAdminAPIKey)
|
adminSettings.POST("/admin-api-key/regenerate", h.Admin.Setting.RegenerateAdminAPIKey)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user