151 lines
6.1 KiB
Go
151 lines
6.1 KiB
Go
package web
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"ymhut-box/server/unified-management/internal/db"
|
|
)
|
|
|
|
func (r *router) handleAdminFeedbacks(w http.ResponseWriter, req *http.Request) {
|
|
path := cleanPath(req.URL.Path)
|
|
if req.Method == http.MethodGet && path == "/api/admin/feedbacks" {
|
|
if req.URL.Query().Get("page") != "" {
|
|
page, _ := strconv.Atoi(req.URL.Query().Get("page"))
|
|
perPage, _ := strconv.Atoi(req.URL.Query().Get("perPage"))
|
|
items, total, err := r.store.ListFeedbacksFiltered(page, perPage, db.FeedbackFilters{
|
|
Status: req.URL.Query().Get("status"),
|
|
Category: req.URL.Query().Get("category"),
|
|
Priority: req.URL.Query().Get("priority"),
|
|
Query: req.URL.Query().Get("q"),
|
|
Assignee: req.URL.Query().Get("assignee"),
|
|
Sort: req.URL.Query().Get("sort"),
|
|
})
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "FEEDBACK_LIST_FAILED", err)
|
|
return
|
|
}
|
|
if page <= 0 {
|
|
page = 1
|
|
}
|
|
if perPage <= 0 {
|
|
perPage = 20
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "page": map[string]any{"items": items, "total": total, "page": page, "perPage": perPage}})
|
|
return
|
|
}
|
|
limit, _ := strconv.Atoi(req.URL.Query().Get("limit"))
|
|
items, err := r.store.ListFeedbacks(limit)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "FEEDBACK_LIST_FAILED", err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "items": items})
|
|
return
|
|
}
|
|
if req.Method == http.MethodGet && path == "/api/admin/feedbacks/export" {
|
|
items, _, err := r.store.ListFeedbacksFiltered(1, 100, db.FeedbackFilters{
|
|
Status: req.URL.Query().Get("status"),
|
|
Category: req.URL.Query().Get("category"),
|
|
Priority: req.URL.Query().Get("priority"),
|
|
Query: req.URL.Query().Get("q"),
|
|
})
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "EXPORT_FAILED", err)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
|
|
w.Header().Set("Content-Disposition", `attachment; filename="feedbacks.csv"`)
|
|
writer := csv.NewWriter(w)
|
|
_ = writer.Write([]string{"code", "created_at", "title", "status", "category", "priority", "contact", "status_detail", "public_reply"})
|
|
for _, item := range items {
|
|
_ = writer.Write([]string{item.Code, item.CreatedAt, item.Title, item.Status, item.Category, item.Priority, item.Contact, item.StatusDetail, item.PublicReply})
|
|
}
|
|
writer.Flush()
|
|
return
|
|
}
|
|
if req.Method == http.MethodGet && strings.HasPrefix(path, "/api/admin/feedbacks/") {
|
|
code := strings.TrimPrefix(path, "/api/admin/feedbacks/")
|
|
detail, err := r.store.GetFeedbackDetail(code)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "NOT_FOUND", err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "feedback": detail})
|
|
return
|
|
}
|
|
if req.Method == http.MethodPatch && path == "/api/admin/feedbacks/bulk" {
|
|
var body struct {
|
|
Codes []string `json:"codes"`
|
|
Status string `json:"status"`
|
|
StatusDetail string `json:"statusDetail"`
|
|
PublicReply string `json:"publicReply"`
|
|
Assignee string `json:"assignee"`
|
|
Tags []string `json:"tags"`
|
|
}
|
|
if err := json.NewDecoder(req.Body).Decode(&body); err != nil || len(body.Codes) == 0 {
|
|
writeError(w, http.StatusBadRequest, "INVALID_PAYLOAD", errors.New("codes are required"))
|
|
return
|
|
}
|
|
if err := r.store.BulkUpdateFeedback(body.Codes, db.FeedbackUpdate{Status: body.Status, StatusDetail: body.StatusDetail, PublicReply: body.PublicReply, Assignee: body.Assignee, Actor: "admin", Tags: body.Tags}); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "BULK_UPDATE_FAILED", err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "updated": len(body.Codes)})
|
|
return
|
|
}
|
|
if req.Method == http.MethodPost && strings.HasPrefix(path, "/api/admin/feedbacks/") && strings.HasSuffix(path, "/comments") {
|
|
code := strings.TrimSuffix(strings.TrimPrefix(path, "/api/admin/feedbacks/"), "/comments")
|
|
var body struct {
|
|
Author string `json:"author"`
|
|
Body string `json:"body"`
|
|
Internal bool `json:"internal"`
|
|
}
|
|
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
|
|
writeError(w, http.StatusBadRequest, "INVALID_PAYLOAD", err)
|
|
return
|
|
}
|
|
comment, err := r.store.InsertFeedbackComment(db.FeedbackComment{Code: code, Author: firstNonEmpty(body.Author, "admin"), Body: body.Body, Internal: body.Internal})
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "COMMENT_FAILED", err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "comment": comment})
|
|
return
|
|
}
|
|
if req.Method == http.MethodPost && strings.HasPrefix(path, "/api/admin/feedbacks/") && strings.HasSuffix(path, "/mail/retry") {
|
|
code := strings.TrimSuffix(strings.TrimPrefix(path, "/api/admin/feedbacks/"), "/mail/retry")
|
|
if err := r.feedback.RetryMail(code); err != nil {
|
|
writeError(w, http.StatusBadGateway, "MAIL_RETRY_FAILED", err)
|
|
return
|
|
}
|
|
_ = r.store.InsertAudit(db.AuditLog{Actor: "admin", Type: "feedback.mail.retry", Target: code, Message: "反馈邮件已重试发送", IP: req.RemoteAddr, UserAgent: req.UserAgent()})
|
|
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
return
|
|
}
|
|
if req.Method == http.MethodPatch && strings.HasPrefix(path, "/api/admin/feedbacks/") {
|
|
code := strings.TrimPrefix(path, "/api/admin/feedbacks/")
|
|
var body struct {
|
|
Status string `json:"status"`
|
|
Priority string `json:"priority"`
|
|
StatusDetail string `json:"statusDetail"`
|
|
PublicReply string `json:"publicReply"`
|
|
}
|
|
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
|
|
writeError(w, http.StatusBadRequest, "INVALID_PAYLOAD", err)
|
|
return
|
|
}
|
|
if err := r.store.UpdateFeedbackTicket(code, db.FeedbackUpdate{Status: firstNonEmpty(body.Status, "new"), Priority: body.Priority, StatusDetail: body.StatusDetail, PublicReply: body.PublicReply, Actor: "admin"}); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "FEEDBACK_UPDATE_FAILED", err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
return
|
|
}
|
|
http.NotFound(w, req)
|
|
}
|