package db import ( "crypto/rand" "database/sql" "encoding/hex" "encoding/json" "sort" "strings" "time" ) func (s *Store) scanFeedbackRow(scanner feedbackScanner) (Feedback, error) { return scanFeedback(scanner) } func feedbackSelectSQL() string { return `SELECT 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 FROM feedback_tickets` } func releaseNoticeSelectSQL() string { return `SELECT id, version, build, channel, title, message, release_notes, message_md, release_notes_md, download_url, notice_file, raw_json, published_at, created_at, updated_at FROM release_notices` } type feedbackScanner interface { Scan(dest ...any) error } func scanReleaseNotice(scanner interface{ Scan(dest ...any) error }) (ReleaseNotice, error) { var item ReleaseNotice err := scanner.Scan(&item.ID, &item.Version, &item.Build, &item.Channel, &item.Title, &item.Message, &item.ReleaseNotes, &item.MessageMD, &item.ReleaseNotesMD, &item.DownloadURL, &item.NoticeFile, &item.RawJSON, &item.PublishedAt, &item.CreatedAt, &item.UpdatedAt) return item, err } func scanFeedback(scanner feedbackScanner) (Feedback, error) { var item Feedback var mailSent int var tags string err := scanner.Scan(&item.Code, &item.Title, &item.Type, &item.Severity, &item.Category, &item.Priority, &item.Contact, &item.Body, &item.Status, &item.StatusDetail, &item.PublicReply, &item.Note, &item.Assignee, &item.HandledBy, &item.DueAt, &item.ResolvedAt, &item.ArchivedAt, &item.SLALevel, &item.SourceChannel, &item.RiskScore, &item.Resolution, &item.Attachment, &item.PackagePath, &item.EncryptedPackagePath, &item.PackageSha256, &item.PlainPackageSha256, &item.SummaryText, &item.IncludedFiles, &mailSent, &item.RemoteAddr, &tags, &item.CreatedAt, &item.UpdatedAt, &item.LastActivityAt) item.MailSent = mailSent == 1 _ = json.Unmarshal([]byte(tags), &item.Tags) return item, err } func scanFeedbackRows(rows *sql.Rows) ([]Feedback, error) { items := []Feedback{} for rows.Next() { item, err := scanFeedback(rows) if err != nil { return nil, err } items = append(items, item) } return items, rows.Err() } func sourceSelectSQL() string { return `SELECT id, category_id, category_name, source_id, name, description, method, api_url, url_template, thumbnail_url, proxy_mode, timeout_ms, retry_count, cache_seconds, check_interval_sec, enabled, client_visible, supported_formats, last_status, last_latency_ms, last_checked_at, last_error, consecutive_failure, created_at, updated_at FROM source_endpoints` } func scanSourceRow(scanner sourceScanner) (Source, error) { return scanSource(scanner) } type sourceScanner interface { Scan(dest ...any) error } func scanSourceRowsCurrent(scanner sourceScanner) (Source, error) { return scanSource(scanner) } func scanSource(scanner sourceScanner) (Source, error) { var item Source var enabled, visible int err := scanner.Scan(&item.ID, &item.CategoryID, &item.CategoryName, &item.SourceID, &item.Name, &item.Description, &item.Method, &item.APIURL, &item.URLTemplate, &item.ThumbnailURL, &item.ProxyMode, &item.TimeoutMS, &item.RetryCount, &item.CacheSeconds, &item.CheckIntervalSec, &enabled, &visible, &item.SupportedFormats, &item.LastStatus, &item.LastLatencyMS, &item.LastCheckedAt, &item.LastError, &item.ConsecutiveFailure, &item.CreatedAt, &item.UpdatedAt) item.Enabled = enabled == 1 item.ClientVisible = visible == 1 return item, err } func scanAuditRows(rows *sql.Rows) ([]AuditLog, error) { items := []AuditLog{} for rows.Next() { var item AuditLog if err := rows.Scan(&item.ID, &item.Actor, &item.Type, &item.Target, &item.Message, &item.IP, &item.UserAgent, &item.CreatedAt); err != nil { return nil, err } items = append(items, item) } return items, rows.Err() } func feedbackWhere(filters FeedbackFilters) (string, []any) { clauses := []string{} args := []any{} if filters.Status != "" { clauses = append(clauses, "status = ?") args = append(args, filters.Status) } if filters.Category != "" { clauses = append(clauses, "category = ?") args = append(args, filters.Category) } if filters.Priority != "" { clauses = append(clauses, "priority = ?") args = append(args, filters.Priority) } if filters.Assignee != "" { clauses = append(clauses, "assignee = ?") args = append(args, filters.Assignee) } if filters.Query != "" { like := "%" + filters.Query + "%" clauses = append(clauses, "(code LIKE ? OR title LIKE ? OR contact LIKE ? OR body LIKE ?)") args = append(args, like, like, like, like) } if len(clauses) == 0 { return "", args } return " WHERE " + strings.Join(clauses, " AND "), args } func normalizePage(page, perPage int) (int, int) { if page < 1 { page = 1 } if perPage <= 0 || perPage > 100 { perPage = 20 } return page, perPage } func NewFeedbackCode() string { var data [3]byte if _, err := rand.Read(data[:]); err != nil { return "FB-" + time.Now().UTC().Format("20060102-150405") } return "FB-" + time.Now().UTC().Format("20060102") + "-" + strings.ToUpper(hex.EncodeToString(data[:])) } func Now() string { return time.Now().UTC().Format(time.RFC3339) } func sanitize(value string) string { return sanitizeLong(value, 1000) } func sanitizeLong(value string, max int) string { value = strings.TrimSpace(strings.ReplaceAll(value, "\x00", "")) value = strings.Map(func(r rune) rune { if r == '\n' || r == '\r' || r == '\t' { return r } if r < 32 { return -1 } return r }, value) runes := []rune(value) if max > 0 && len(runes) > max { return string(runes[:max]) } return value } func boolInt(value bool) int { if value { return 1 } return 0 } func normalizeCategory(value string) string { switch strings.ToLower(strings.TrimSpace(value)) { case "suggestion", "ui", "other": return strings.ToLower(strings.TrimSpace(value)) default: return "issue" } } func normalizePriority(value string) string { switch strings.ToLower(strings.TrimSpace(value)) { case "major", "blocking": return strings.ToLower(strings.TrimSpace(value)) default: return "normal" } } func defaultSLA(priority string) string { switch normalizePriority(priority) { case "blocking": return "urgent" case "major": return "elevated" default: return "standard" } } func defaultRisk(priority string) int { switch normalizePriority(priority) { case "blocking": return 90 case "major": return 65 default: return 30 } } func normalizeTags(tags []string) []string { seen := map[string]bool{} out := []string{} for _, tag := range tags { tag = strings.ToLower(strings.Trim(strings.TrimSpace(tag), ",;#")) if tag == "" || seen[tag] { continue } runes := []rune(tag) if len(runes) > 32 { tag = string(runes[:32]) } seen[tag] = true out = append(out, tag) } sort.Strings(out) return out } func normalizeProxyMode(value, category, name, url string) string { value = strings.ToLower(strings.TrimSpace(value)) switch value { case "server_proxy", "proxy": return "server_proxy" case "disabled": return "disabled" case "client_direct", "direct": return "client_direct" } haystack := strings.ToLower(category + " " + name + " " + url) for _, token := range []string{"ip", "weather", "location", "定位", "天气"} { if strings.Contains(haystack, token) { return "client_direct" } } return "client_direct" } func firstNonEmpty(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return strings.TrimSpace(value) } } return "" }