Files
knowledge-governance/internal/httpapi/qa_weekly_mail.go
T
hermes 6816289552
build / build (push) Successful in 1m23s
feat(qa): 周报确认后按设定时段自动发邮件
周报页新增自动发送区:管理员配置 SMTP/收件人/发送星期与小时,授权码用主密钥加密保存。
确认绑定当前草稿版本与正文哈希,正文再改则确认失效;worker 每分钟仅在启用、到点、周已结束且未发送过时发一次。
邮件同时提供渲染后的 HTML 与可读纯文本;SMTP 仅允许 TLS,拒绝明文。
迁移 000060 新增设置、确认与发送记录三张表,唯一键防重复发送。
2026-10-26 09:49:23 +08:00

134 lines
4.2 KiB
Go

package httpapi
import (
"context"
"errors"
"knowledge-governance/internal/dobotaiops"
"knowledge-governance/internal/qawemail"
"net/http"
"time"
)
type weeklyMailStore interface {
ReadSettings(context.Context) (qawemail.Settings, error)
SaveSettings(context.Context, string, qawemail.SettingsInput) (qawemail.Settings, error)
Confirm(context.Context, string, string) (qawemail.Confirmation, error)
Unconfirm(context.Context, string) error
ReadConfirmation(context.Context, string) (*qawemail.Confirmation, error)
Sends(context.Context, string) ([]qawemail.Send, error)
SendDue(context.Context, time.Time) (qawemail.DueResult, error)
}
func weeklyMailError(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, qawemail.ErrConflict):
writeJSON(w, http.StatusConflict, map[string]string{"error": "发送设置已被其他人修改,请重新读取后再保存"})
case errors.Is(err, qawemail.ErrInvalid):
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "发送设置无效:请检查服务器、发件地址、收件人与时间"})
case errors.Is(err, qawemail.ErrUnavailable):
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "周报邮件暂不可用,请稍后重试;未标记为已发送"})
default:
qaOpsError(w, dobotaiops.ErrUnavailable)
}
}
func qaWeeklyMailSettings(w http.ResponseWriter, r *http.Request, c Config) {
if !authorizeQAOps(w, r) {
return
}
store, ok := c.QAWeeklyMail.(weeklyMailStore)
if !ok {
weeklyMailError(w, qawemail.ErrUnavailable)
return
}
switch r.Method {
case http.MethodGet:
settings, err := store.ReadSettings(r.Context())
if err != nil {
weeklyMailError(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]any{"data": settings})
case http.MethodPost:
if !authorizeQAIntent(w, r, "qa-weekly-mail") {
return
}
if identity, ok := sessionFrom(r.Context()); !ok || identity.Role != "admin" {
writeJSON(w, http.StatusForbidden, map[string]string{"error": "仅管理员可修改邮件发送设置"})
return
}
var input qawemail.SettingsInput
if decodeQualityJSON(w, r, &input) != nil {
weeklyMailError(w, qawemail.ErrInvalid)
return
}
identity, _ := sessionFrom(r.Context())
actor := identity.Username
settings, err := store.SaveSettings(r.Context(), actor, input)
if err != nil {
weeklyMailError(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]any{"data": settings})
default:
w.Header().Set("Allow", "GET, POST")
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "不支持的方法"})
}
}
func qaWeeklyMailConfirm(w http.ResponseWriter, r *http.Request, c Config) {
if !authorizeQAOps(w, r) || !authorizeQAIntent(w, r, "qa-weekly-mail") {
return
}
store, ok := c.QAWeeklyMail.(weeklyMailStore)
if !ok {
weeklyMailError(w, qawemail.ErrUnavailable)
return
}
week := r.PathValue("week")
switch r.Method {
case http.MethodGet:
confirmation, err := store.ReadConfirmation(r.Context(), week)
if err != nil {
weeklyMailError(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"confirmation": confirmation}})
case http.MethodPost:
identity, _ := sessionFrom(r.Context())
actor := identity.Username
confirmation, err := store.Confirm(r.Context(), week, actor)
if err != nil {
weeklyMailError(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"confirmation": confirmation}})
case http.MethodDelete:
if err := store.Unconfirm(r.Context(), week); err != nil {
weeklyMailError(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"confirmation": nil}})
default:
w.Header().Set("Allow", "GET, POST, DELETE")
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "不支持的方法"})
}
}
func qaWeeklyMailSends(w http.ResponseWriter, r *http.Request, c Config) {
if !authorizeQAOps(w, r) {
return
}
store, ok := c.QAWeeklyMail.(weeklyMailStore)
if !ok {
weeklyMailError(w, qawemail.ErrUnavailable)
return
}
sends, err := store.Sends(r.Context(), r.URL.Query().Get("week"))
if err != nil {
weeklyMailError(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"sends": sends}})
}