277 lines
7.7 KiB
Go
277 lines
7.7 KiB
Go
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 ""
|
|
}
|