package httpapi import ( "context" "encoding/base64" "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) TestSend(context.Context, string) (string, error) SaveDashboardImage(context.Context, string, []byte) 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) { return } if r.Method != http.MethodGet && !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}}) } func qaWeeklyMailTest(w http.ResponseWriter, r *http.Request, c Config) { if !authorizeQAOps(w, r) || !authorizeQAIntent(w, r, "qa-weekly-mail") { return } identity, ok := sessionFrom(r.Context()) if !ok || identity.Role != "admin" { writeJSON(w, http.StatusForbidden, map[string]string{"error": "仅管理员可发送测试邮件"}) return } store, found := c.QAWeeklyMail.(weeklyMailStore) if !found { weeklyMailError(w, qawemail.ErrUnavailable) return } var request struct { Week string `json:"week"` } if decodeQualityJSON(w, r, &request) != nil { weeklyMailError(w, qawemail.ErrInvalid) return } messageID, err := store.TestSend(r.Context(), request.Week) if err != nil { if errors.Is(err, qawemail.ErrInvalid) { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "请先保存该周周报正文,并确认已配置服务器、发件人、收件人与授权码"}) return } reason := err.Error() if len(reason) > 300 { reason = reason[:300] } writeJSON(w, http.StatusBadGateway, map[string]string{"error": "测试发送失败:" + reason}) return } writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"sent": true, "week": request.Week, "message_id": messageID}}) } func qaWeeklyMailImage(w http.ResponseWriter, r *http.Request, c Config) { if !authorizeQAOps(w, r) || !authorizeQAIntent(w, r, "qa-weekly-mail") { return } identity, ok := sessionFrom(r.Context()) if !ok || identity.Role != "admin" { writeJSON(w, http.StatusForbidden, map[string]string{"error": "仅管理员可上传周报附图"}) return } store, found := c.QAWeeklyMail.(weeklyMailStore) if !found { weeklyMailError(w, qawemail.ErrUnavailable) return } var request struct { PNGBase64 string `json:"png_base64"` } // The chart is megabytes of base64, far above the tiny quality-edit limit. if decodeGovernanceJSON(w, r, &request, 12<<20) != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "看板图片数据无效或过大(上限 8MB)"}) return } png, err := base64.StdEncoding.DecodeString(request.PNGBase64) if err != nil || len(png) < 8 || len(png) > 8<<20 { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "图片无效或超过 8MB"}) return } if err := store.SaveDashboardImage(r.Context(), r.PathValue("week"), png); err != nil { if errors.Is(err, qawemail.ErrInvalid) { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "请先确认本周周报,再附上统计看板图片"}) return } weeklyMailError(w, err) return } writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"saved": true, "bytes": len(png)}}) }