Files
knowledge-governance/internal/httpapi/qa_weekly_mail.go
T
hermes 1a9d1fff83
build / build (push) Successful in 1m13s
fix(qa): 看板附图上传改用大请求体解码
附图 base64 远超 64KiB 的质量编辑上限,被误判为无效设置并报“发送设置无效”。
改为 12MB 上限的通用解码,并返回明确的图片错误文案。
2026-10-26 16:03:52 +08:00

215 lines
7.0 KiB
Go

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)}})
}