1a9d1fff83
build / build (push) Successful in 1m13s
附图 base64 远超 64KiB 的质量编辑上限,被误判为无效设置并报“发送设置无效”。 改为 12MB 上限的通用解码,并返回明确的图片错误文案。
215 lines
7.0 KiB
Go
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)}})
|
|
}
|