Files
YMhut-box-C-/server/unified-management/internal/db/feedback_store.go
T
QWQLwToo 962a2f2143
build-winui / winui (push) Waiting to run
更新 update 门户站点界面和后台功能
2026-06-27 18:09:11 +08:00

376 lines
14 KiB
Go

package db
import (
"database/sql"
"encoding/json"
"errors"
"os"
"path/filepath"
)
func (s *Store) InsertFeedback(item Feedback) error {
now := Now()
if item.Code == "" {
item.Code = NewFeedbackCode()
}
if item.Status == "" {
item.Status = "new"
}
if item.Category == "" {
item.Category = normalizeCategory(item.Type)
}
if item.Priority == "" {
item.Priority = normalizePriority(item.Severity)
}
if item.SLALevel == "" {
item.SLALevel = defaultSLA(item.Priority)
}
if item.SourceChannel == "" {
item.SourceChannel = "winui"
}
if item.RiskScore == 0 {
item.RiskScore = defaultRisk(item.Priority)
}
if item.StatusDetail == "" {
item.StatusDetail = "反馈已接收,等待后台处理。"
}
if item.CreatedAt == "" {
item.CreatedAt = now
}
item.UpdatedAt = now
item.LastActivityAt = now
tagsJSON, _ := json.Marshal(normalizeTags(item.Tags))
_, err := s.exec(`INSERT INTO feedback_tickets (
code, title, type, severity, category, priority, contact, body, status, status_detail,
public_reply, note, assignee, handled_by, due_at, resolved_at, archived_at, sla_level,
source_channel, risk_score, resolution, attachment, package_path, encrypted_package_path,
package_sha256, plain_package_sha256, summary_text, included_files, mail_sent, remote_addr,
tags, created_at, updated_at, last_activity_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
item.Code, sanitize(item.Title), sanitize(item.Type), sanitize(item.Severity), item.Category, item.Priority,
sanitize(item.Contact), sanitizeLong(item.Body, 5000), item.Status, sanitize(item.StatusDetail),
sanitizeLong(item.PublicReply, 3000), sanitizeLong(item.Note, 3000), sanitize(item.Assignee), sanitize(item.HandledBy),
item.DueAt, item.ResolvedAt, item.ArchivedAt, item.SLALevel, item.SourceChannel, item.RiskScore,
sanitizeLong(item.Resolution, 3000), item.Attachment, item.PackagePath, item.EncryptedPackagePath,
item.PackageSha256, item.PlainPackageSha256, sanitizeLong(item.SummaryText, 6000), item.IncludedFiles,
boolInt(item.MailSent), sanitize(item.RemoteAddr), string(tagsJSON), item.CreatedAt, item.UpdatedAt, item.LastActivityAt)
if err != nil {
return err
}
if item.PackagePath != "" {
_ = s.InsertFeedbackAttachment(FeedbackAttachment{FeedbackCode: item.Code, Kind: "package", Path: item.PackagePath, FileName: filepath.Base(item.PackagePath), SHA256: item.PlainPackageSha256})
}
if item.EncryptedPackagePath != "" {
_ = s.InsertFeedbackAttachment(FeedbackAttachment{FeedbackCode: item.Code, Kind: "encrypted_package", Path: item.EncryptedPackagePath, FileName: filepath.Base(item.EncryptedPackagePath), SHA256: item.PackageSha256})
}
return nil
}
func (s *Store) GetFeedback(code string) (Feedback, error) {
item, err := s.scanFeedbackRow(s.queryRow(feedbackSelectSQL()+` WHERE code = ?`, code))
if errors.Is(err, sql.ErrNoRows) {
return Feedback{}, errors.New("feedback not found")
}
return item, err
}
func (s *Store) GetFeedbackDetail(code string) (*FeedbackDetail, error) {
item, err := s.GetFeedback(code)
if err != nil {
return nil, err
}
comments, err := s.ListFeedbackComments(code)
if err != nil {
return nil, err
}
attachments, err := s.ListFeedbackAttachments(code)
if err != nil {
return nil, err
}
events, _ := s.ListAuditLogsForTarget(code, 100)
legacyEvents, _ := s.ListFeedbackEvents(code, 100)
mailRecords, _ := s.ListMailRecords(code, 100)
return &FeedbackDetail{Feedback: item, Comments: comments, Attachments: attachments, Events: events, LegacyEvents: legacyEvents, MailRecords: mailRecords}, nil
}
func (s *Store) ListFeedbacks(limit int) ([]Feedback, error) {
if limit <= 0 || limit > 200 {
limit = 50
}
rows, err := s.query(feedbackSelectSQL()+` ORDER BY last_activity_at DESC, created_at DESC LIMIT ?`, limit)
if err != nil {
return nil, err
}
defer rows.Close()
return scanFeedbackRows(rows)
}
func (s *Store) ListFeedbacksFiltered(page, perPage int, filters FeedbackFilters) ([]Feedback, int, error) {
page, perPage = normalizePage(page, perPage)
where, args := feedbackWhere(filters)
var total int
if err := s.queryRow(`SELECT COUNT(*) FROM feedback_tickets`+where, args...).Scan(&total); err != nil {
return nil, 0, err
}
order := ` ORDER BY last_activity_at DESC, created_at DESC`
if filters.Sort == "oldest" {
order = ` ORDER BY created_at ASC`
}
args = append(args, perPage, (page-1)*perPage)
rows, err := s.query(feedbackSelectSQL()+where+order+` LIMIT ? OFFSET ?`, args...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
items, err := scanFeedbackRows(rows)
return items, total, err
}
func (s *Store) UpdateFeedback(code, status, detail, reply string) error {
update := FeedbackUpdate{Status: status, StatusDetail: detail, PublicReply: reply, Actor: "admin"}
return s.UpdateFeedbackTicket(code, update)
}
func (s *Store) UpdateFeedbackTicket(code string, update FeedbackUpdate) error {
current, err := s.GetFeedback(code)
if err != nil {
return err
}
if update.Status == "" {
update.Status = current.Status
}
if update.Category == "" {
update.Category = current.Category
}
if update.Priority == "" {
update.Priority = current.Priority
}
if update.SLALevel == "" {
update.SLALevel = current.SLALevel
}
if update.StatusDetail == "" {
update.StatusDetail = current.StatusDetail
}
if update.PublicReply == "" {
update.PublicReply = current.PublicReply
}
if update.Note == "" {
update.Note = current.Note
}
if update.Assignee == "" {
update.Assignee = current.Assignee
}
if update.HandledBy == "" {
update.HandledBy = current.HandledBy
}
if update.DueAt == "" {
update.DueAt = current.DueAt
}
if update.Resolution == "" {
update.Resolution = current.Resolution
}
tags := current.Tags
if len(update.Tags) > 0 {
tags = update.Tags
}
tagsJSON, _ := json.Marshal(normalizeTags(tags))
now := Now()
_, err = s.exec(`UPDATE feedback_tickets SET status = ?, category = ?, priority = ?, status_detail = ?, public_reply = ?,
note = ?, assignee = ?, handled_by = ?, due_at = ?, sla_level = ?, resolution = ?, tags = ?, updated_at = ?, last_activity_at = ?
WHERE code = ?`,
update.Status, update.Category, update.Priority, sanitize(update.StatusDetail), sanitizeLong(update.PublicReply, 3000),
sanitizeLong(update.Note, 3000), sanitize(update.Assignee), sanitize(update.HandledBy), update.DueAt, update.SLALevel,
sanitizeLong(update.Resolution, 3000), string(tagsJSON), now, now, code)
if err == nil {
_ = s.InsertAudit(AuditLog{Actor: firstNonEmpty(update.Actor, "admin"), Type: "feedback.updated", Target: code, Message: "反馈工单已更新"})
}
return err
}
func (s *Store) BulkUpdateFeedback(codes []string, update FeedbackUpdate) error {
for _, code := range codes {
if err := s.UpdateFeedbackTicket(code, update); err != nil {
return err
}
}
return nil
}
func (s *Store) InsertFeedbackComment(comment FeedbackComment) (FeedbackComment, error) {
if comment.CreatedAt == "" {
comment.CreatedAt = Now()
}
id, err := s.insertID(`INSERT INTO feedback_comments (feedback_code, author, body, internal, created_at) VALUES (?, ?, ?, ?, ?)`,
comment.Code, sanitize(comment.Author), sanitizeLong(comment.Body, 3000), boolInt(comment.Internal), comment.CreatedAt)
if err != nil {
return FeedbackComment{}, err
}
comment.ID = id
return comment, nil
}
func (s *Store) ListFeedbackComments(code string) ([]FeedbackComment, error) {
rows, err := s.query(`SELECT id, feedback_code, author, body, internal, created_at FROM feedback_comments WHERE feedback_code = ? ORDER BY id ASC`, code)
if err != nil {
return nil, err
}
defer rows.Close()
items := []FeedbackComment{}
for rows.Next() {
var item FeedbackComment
var internal int
if err := rows.Scan(&item.ID, &item.Code, &item.Author, &item.Body, &internal, &item.CreatedAt); err != nil {
return nil, err
}
item.Internal = internal == 1
items = append(items, item)
}
return items, rows.Err()
}
func (s *Store) InsertFeedbackAttachment(item FeedbackAttachment) error {
if item.CreatedAt == "" {
item.CreatedAt = Now()
}
if item.FileName == "" {
item.FileName = filepath.Base(item.Path)
}
if item.SizeBytes == 0 {
if info, err := os.Stat(item.Path); err == nil {
item.SizeBytes = info.Size()
}
}
_, err := s.exec(`INSERT INTO feedback_attachments (feedback_code, kind, path, file_name, sha256, size_bytes, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`,
item.FeedbackCode, item.Kind, item.Path, item.FileName, item.SHA256, item.SizeBytes, item.CreatedAt)
return err
}
func (s *Store) ListFeedbackAttachments(code string) ([]FeedbackAttachment, error) {
rows, err := s.query(`SELECT id, feedback_code, kind, path, file_name, sha256, size_bytes, created_at FROM feedback_attachments WHERE feedback_code = ? ORDER BY id ASC`, code)
if err != nil {
return nil, err
}
defer rows.Close()
items := []FeedbackAttachment{}
for rows.Next() {
var item FeedbackAttachment
if err := rows.Scan(&item.ID, &item.FeedbackCode, &item.Kind, &item.Path, &item.FileName, &item.SHA256, &item.SizeBytes, &item.CreatedAt); err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}
func (s *Store) UpsertFeedbackEvent(item LegacyFeedbackEvent) error {
if item.CreatedAt == "" {
item.CreatedAt = Now()
}
conn, d := s.active()
columns := []string{"id", "feedback_code", "event_type", "actor", "from_value", "to_value", "message", "created_at"}
_, err := conn.Exec(d.rebind(d.upsert("feedback_events", columns, []string{"id"})),
item.ID, sanitize(item.FeedbackCode), sanitize(item.EventType), sanitize(item.Actor), sanitize(item.FromValue), sanitize(item.ToValue), sanitizeLong(item.Message, 1000), item.CreatedAt)
if err != nil {
s.markFailover(err)
}
return err
}
func (s *Store) UpsertFeedbackTag(code, tag, createdAt string) error {
if createdAt == "" {
createdAt = Now()
}
conn, d := s.active()
columns := []string{"feedback_code", "tag", "created_at"}
_, err := conn.Exec(d.rebind(d.upsert("feedback_tags", columns, []string{"feedback_code", "tag"})), sanitize(code), sanitize(tag), createdAt)
if err != nil {
s.markFailover(err)
}
return err
}
func (s *Store) UpsertMailRecord(item LegacyMailRecord) error {
if item.CreatedAt == "" {
item.CreatedAt = Now()
}
conn, d := s.active()
columns := []string{"id", "feedback_code", "kind", "status", "to_address", "subject", "plain_body", "html_body", "attachment_path", "attachment_name", "error_message", "created_at", "sent_at"}
_, err := conn.Exec(d.rebind(d.upsert("mail_records", columns, []string{"id"})),
item.ID, sanitize(item.FeedbackCode), sanitize(item.Kind), sanitize(item.Status), sanitize(item.ToAddress), sanitizeLong(item.Subject, 1000),
"", "", item.AttachmentPath, item.AttachmentName, sanitizeLong(item.ErrorMessage, 1000), item.CreatedAt, item.SentAt)
if err != nil {
s.markFailover(err)
}
return err
}
func (s *Store) InsertMailRecord(item LegacyMailRecord) (int64, error) {
if item.CreatedAt == "" {
item.CreatedAt = Now()
}
id, err := s.insertID(`INSERT INTO mail_records (
feedback_code, kind, status, to_address, subject, plain_body, html_body,
attachment_path, attachment_name, error_message, created_at, sent_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
sanitize(item.FeedbackCode), sanitize(firstNonEmpty(item.Kind, "feedback")), sanitize(firstNonEmpty(item.Status, "pending")),
sanitize(item.ToAddress), sanitizeLong(item.Subject, 1000), sanitizeLong(item.PlainBody, 12000), sanitizeLong(item.HTMLBody, 12000),
item.AttachmentPath, item.AttachmentName, sanitizeLong(item.ErrorMessage, 1000), item.CreatedAt, item.SentAt)
return id, err
}
func (s *Store) UpdateMailState(id int64, status, errorMessage string) error {
sentAt := ""
if status == "sent" {
sentAt = Now()
}
_, err := s.exec(`UPDATE mail_records SET status = ?, error_message = ?, sent_at = ? WHERE id = ?`,
sanitize(status), sanitizeLong(errorMessage, 1000), sentAt, id)
return err
}
func (s *Store) UpdateFeedbackMailState(code string, sent bool) error {
_, err := s.exec(`UPDATE feedback_tickets SET mail_sent = ?, updated_at = ?, last_activity_at = ? WHERE code = ?`,
boolInt(sent), Now(), Now(), code)
return err
}
func (s *Store) ListFeedbackEvents(code string, limit int) ([]LegacyFeedbackEvent, error) {
if limit <= 0 || limit > 200 {
limit = 100
}
rows, err := s.query(`SELECT id, feedback_code, event_type, actor, from_value, to_value, message, created_at FROM feedback_events WHERE feedback_code = ? ORDER BY created_at DESC, id DESC LIMIT ?`, code, limit)
if err != nil {
return nil, err
}
defer rows.Close()
items := []LegacyFeedbackEvent{}
for rows.Next() {
var item LegacyFeedbackEvent
if err := rows.Scan(&item.ID, &item.FeedbackCode, &item.EventType, &item.Actor, &item.FromValue, &item.ToValue, &item.Message, &item.CreatedAt); err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}
func (s *Store) ListMailRecords(code string, limit int) ([]LegacyMailRecord, error) {
if limit <= 0 || limit > 200 {
limit = 100
}
rows, err := s.query(`SELECT id, feedback_code, kind, status, to_address, subject, attachment_path, attachment_name, error_message, created_at, sent_at FROM mail_records WHERE feedback_code = ? ORDER BY created_at DESC, id DESC LIMIT ?`, code, limit)
if err != nil {
return nil, err
}
defer rows.Close()
items := []LegacyMailRecord{}
for rows.Next() {
var item LegacyMailRecord
if err := rows.Scan(&item.ID, &item.FeedbackCode, &item.Kind, &item.Status, &item.ToAddress, &item.Subject, &item.AttachmentPath, &item.AttachmentName, &item.ErrorMessage, &item.CreatedAt, &item.SentAt); err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}