Files
YMhut-box-C-/server/unified-management/internal/db/schema.go
T
admin_gitea f00124c1c0
build-winui / winui (push) Waiting to run
更新客户端适配性问题
2026-06-28 08:56:45 +08:00

357 lines
13 KiB
Go

package db
import (
"database/sql"
"fmt"
)
const CurrentSchemaVersion = "2026-06-compat-baseline"
func (s *Store) migrate(conn *sql.DB, d dialect) error {
statements := []string{}
if d.name == "sqlite" {
statements = append(statements,
"PRAGMA busy_timeout = 5000",
"PRAGMA journal_mode = WAL",
"PRAGMA foreign_keys = ON",
)
}
statements = append(statements, schemaStatements(d)...)
for _, statement := range statements {
if _, err := conn.Exec(d.rebind(statement)); err != nil {
return err
}
}
if err := createSchemaIndexes(conn, d); err != nil {
return err
}
return s.recordSchemaVersion(conn, d)
}
func schemaStatements(d dialect) []string {
keyText := d.keyTextType()
shortText := d.shortTextType()
mediumText := d.mediumTextType()
longText := d.longTextType()
return []string{
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS schema_migrations (
version VARCHAR(64) NOT NULL PRIMARY KEY,
applied_at %s NOT NULL,
description VARCHAR(255) NOT NULL DEFAULT ''
)`, shortText),
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS admin_users (
id %s,
username %s NOT NULL UNIQUE,
password_hash %s NOT NULL,
password_changed INTEGER NOT NULL DEFAULT 0,
created_at %s NOT NULL,
updated_at %s NOT NULL
)`, d.idType(), keyText, shortText, shortText, shortText),
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS sessions (
id %s,
session_id %s NOT NULL UNIQUE,
username %s NOT NULL,
csrf %s NOT NULL,
expires_at %s NOT NULL,
created_at %s NOT NULL
)`, d.idType(), keyText, keyText, shortText, shortText, shortText),
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS release_packages (
id %s,
product %s NOT NULL,
version %s NOT NULL,
platform %s NOT NULL,
arch %s NOT NULL,
file_name %s NOT NULL UNIQUE,
url %s NOT NULL,
sha256 %s NOT NULL,
size_bytes BIGINT NOT NULL DEFAULT 0,
enabled INTEGER NOT NULL DEFAULT 1,
created_at %s NOT NULL,
updated_at %s NOT NULL
)`, d.idType(), keyText, keyText, keyText, keyText, keyText, mediumText, shortText, shortText, shortText),
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS release_notices (
id %s,
version %s NOT NULL UNIQUE,
build %s NOT NULL DEFAULT '',
channel %s NOT NULL DEFAULT 'stable',
title %s NOT NULL DEFAULT '',
message %s NOT NULL,
release_notes %s NOT NULL,
message_md %s NOT NULL,
release_notes_md %s NOT NULL,
download_url %s NOT NULL DEFAULT '',
notice_file %s NOT NULL DEFAULT '',
raw_json %s NOT NULL,
published_at %s NOT NULL DEFAULT '',
created_at %s NOT NULL,
updated_at %s NOT NULL
)`, d.idType(), keyText, shortText, shortText, mediumText, longText, longText, longText, longText, mediumText, keyText, longText, shortText, shortText, shortText),
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS release_notice_revisions (
id %s,
version %s NOT NULL,
raw_json %s NOT NULL,
note %s NOT NULL DEFAULT '',
created_by %s NOT NULL DEFAULT '',
created_at %s NOT NULL
)`, d.idType(), keyText, longText, mediumText, keyText, shortText),
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS feedback_tickets (
code %s PRIMARY KEY,
title %s NOT NULL,
type %s NOT NULL,
severity %s NOT NULL,
category %s NOT NULL DEFAULT '',
priority %s NOT NULL DEFAULT '',
contact %s NOT NULL DEFAULT '',
body %s NOT NULL,
status %s NOT NULL,
status_detail %s NOT NULL DEFAULT '',
public_reply %s NOT NULL,
note %s NOT NULL,
assignee %s NOT NULL DEFAULT '',
handled_by %s NOT NULL DEFAULT '',
due_at %s NOT NULL DEFAULT '',
resolved_at %s NOT NULL DEFAULT '',
archived_at %s NOT NULL DEFAULT '',
sla_level %s NOT NULL DEFAULT '',
source_channel %s NOT NULL DEFAULT '',
risk_score INTEGER NOT NULL DEFAULT 0,
resolution %s NOT NULL,
attachment %s NOT NULL DEFAULT '',
package_path %s NOT NULL DEFAULT '',
encrypted_package_path %s NOT NULL DEFAULT '',
package_sha256 %s NOT NULL DEFAULT '',
plain_package_sha256 %s NOT NULL DEFAULT '',
summary_text %s NOT NULL,
included_files %s NOT NULL,
mail_sent INTEGER NOT NULL DEFAULT 0,
remote_addr %s NOT NULL DEFAULT '',
tags %s NOT NULL,
created_at %s NOT NULL,
updated_at %s NOT NULL,
last_activity_at %s NOT NULL
)`, keyText, mediumText, keyText, keyText, keyText, keyText, mediumText, longText, keyText, mediumText, longText, longText, keyText, keyText, shortText, shortText, shortText, keyText, keyText, longText, mediumText, mediumText, mediumText, shortText, shortText, longText, longText, shortText, longText, shortText, shortText, shortText),
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS feedback_comments (
id %s,
feedback_code %s NOT NULL,
author %s NOT NULL DEFAULT '',
body %s NOT NULL,
internal INTEGER NOT NULL DEFAULT 1,
created_at %s NOT NULL
)`, d.idType(), keyText, keyText, longText, shortText),
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS feedback_attachments (
id %s,
feedback_code %s NOT NULL,
kind %s NOT NULL,
path %s NOT NULL,
file_name %s NOT NULL,
sha256 %s NOT NULL DEFAULT '',
size_bytes BIGINT NOT NULL DEFAULT 0,
created_at %s NOT NULL
)`, d.idType(), keyText, keyText, mediumText, mediumText, shortText, shortText),
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS feedback_events (
id %s,
feedback_code %s NOT NULL,
event_type %s NOT NULL,
actor %s NOT NULL DEFAULT '',
from_value %s NOT NULL DEFAULT '',
to_value %s NOT NULL DEFAULT '',
message %s NOT NULL DEFAULT '',
created_at %s NOT NULL
)`, d.idType(), keyText, keyText, keyText, mediumText, mediumText, mediumText, shortText),
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS feedback_tags (
feedback_code %s NOT NULL,
tag %s NOT NULL,
created_at %s NOT NULL,
PRIMARY KEY (feedback_code, tag)
)`, keyText, keyText, shortText),
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS mail_records (
id %s,
feedback_code %s NOT NULL DEFAULT '',
kind %s NOT NULL DEFAULT '',
status %s NOT NULL DEFAULT '',
to_address %s NOT NULL DEFAULT '',
subject %s NOT NULL DEFAULT '',
plain_body %s NOT NULL,
html_body %s NOT NULL,
attachment_path %s NOT NULL DEFAULT '',
attachment_name %s NOT NULL DEFAULT '',
error_message %s NOT NULL,
created_at %s NOT NULL,
sent_at %s NOT NULL DEFAULT ''
)`, d.idType(), keyText, keyText, keyText, mediumText, mediumText, longText, longText, mediumText, mediumText, longText, shortText, shortText),
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS source_categories (
id %s,
category_id %s NOT NULL UNIQUE,
name %s NOT NULL,
enabled INTEGER NOT NULL DEFAULT 1,
ui_config %s NOT NULL,
created_at %s NOT NULL,
updated_at %s NOT NULL
)`, d.idType(), keyText, shortText, longText, shortText, shortText),
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS source_endpoints (
id %s,
category_id %s NOT NULL,
category_name %s NOT NULL,
source_id %s NOT NULL UNIQUE,
name %s NOT NULL,
description %s NOT NULL DEFAULT '',
method %s NOT NULL DEFAULT 'GET',
api_url %s NOT NULL DEFAULT '',
url_template %s NOT NULL DEFAULT '',
thumbnail_url %s NOT NULL DEFAULT '',
proxy_mode %s NOT NULL DEFAULT 'client_direct',
timeout_ms INTEGER NOT NULL DEFAULT 8000,
retry_count INTEGER NOT NULL DEFAULT 1,
cache_seconds INTEGER NOT NULL DEFAULT 300,
check_interval_sec INTEGER NOT NULL DEFAULT 300,
enabled INTEGER NOT NULL DEFAULT 1,
client_visible INTEGER NOT NULL DEFAULT 1,
supported_formats %s NOT NULL,
last_status %s NOT NULL DEFAULT 'unknown',
last_latency_ms INTEGER NOT NULL DEFAULT 0,
last_checked_at %s NOT NULL DEFAULT '',
last_error %s NOT NULL,
consecutive_failure INTEGER NOT NULL DEFAULT 0,
created_at %s NOT NULL,
updated_at %s NOT NULL
)`, d.idType(), keyText, shortText, keyText, shortText, mediumText, keyText, mediumText, mediumText, mediumText, keyText, longText, keyText, shortText, longText, shortText, shortText),
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS endpoint_health_checks (
id %s,
source_db_id BIGINT NOT NULL,
status %s NOT NULL,
latency_ms INTEGER NOT NULL DEFAULT 0,
error %s NOT NULL,
checked_at %s NOT NULL
)`, d.idType(), keyText, longText, shortText),
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS endpoint_call_logs (
id %s,
source_id %s NOT NULL,
status %s NOT NULL,
latency_ms INTEGER NOT NULL DEFAULT 0,
error %s NOT NULL,
client %s NOT NULL DEFAULT '',
created_at %s NOT NULL
)`, d.idType(), keyText, keyText, longText, mediumText, shortText),
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS database_sync_jobs (
id %s,
direction %s NOT NULL,
status %s NOT NULL,
message %s NOT NULL,
tables_json %s NOT NULL,
started_at %s NOT NULL,
finished_at %s NOT NULL DEFAULT ''
)`, d.idType(), keyText, keyText, longText, longText, shortText, shortText),
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS system_settings (
%s %s NOT NULL PRIMARY KEY,
value %s NOT NULL,
updated_at %s NOT NULL
)`, d.quoteIdent("key"), keyText, longText, shortText),
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS legacy_sync_jobs (
id %s,
status %s NOT NULL,
summary %s NOT NULL,
stats_json %s NOT NULL,
started_at %s NOT NULL,
finished_at %s NOT NULL DEFAULT ''
)`, d.idType(), keyText, longText, longText, shortText, shortText),
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS audit_logs (
id %s,
actor %s NOT NULL DEFAULT '',
type %s NOT NULL,
target %s NOT NULL DEFAULT '',
message %s NOT NULL,
ip %s NOT NULL DEFAULT '',
user_agent %s NOT NULL DEFAULT '',
created_at %s NOT NULL
)`, d.idType(), keyText, keyText, keyText, longText, keyText, mediumText, shortText),
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS legacy_json_revisions (
id %s,
name %s NOT NULL,
raw %s NOT NULL,
note %s NOT NULL DEFAULT '',
created_by %s NOT NULL DEFAULT '',
created_at %s NOT NULL
)`, d.idType(), keyText, longText, mediumText, keyText, shortText),
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS webhook_deliveries (
id %s,
webhook_name %s NOT NULL DEFAULT '',
event %s NOT NULL DEFAULT '',
status %s NOT NULL DEFAULT '',
attempts INTEGER NOT NULL DEFAULT 0,
response_code INTEGER NOT NULL DEFAULT 0,
error_message %s NOT NULL,
payload_sha256 %s NOT NULL DEFAULT '',
created_at %s NOT NULL,
finished_at %s NOT NULL DEFAULT ''
)`, d.idType(), keyText, keyText, keyText, longText, shortText, shortText, shortText),
}
}
type schemaIndex struct {
name string
table string
columns string
}
func schemaIndexes() []schemaIndex {
return []schemaIndex{
{name: "idx_feedback_tickets_activity", table: "feedback_tickets", columns: "last_activity_at"},
{name: "idx_feedback_comments_code", table: "feedback_comments", columns: "feedback_code"},
{name: "idx_feedback_attachments_code", table: "feedback_attachments", columns: "feedback_code"},
{name: "idx_feedback_events_code", table: "feedback_events", columns: "feedback_code"},
{name: "idx_mail_records_code", table: "mail_records", columns: "feedback_code"},
{name: "idx_endpoint_call_logs_source", table: "endpoint_call_logs", columns: "source_id"},
{name: "idx_audit_logs_created", table: "audit_logs", columns: "created_at"},
{name: "idx_audit_logs_type", table: "audit_logs", columns: "type"},
{name: "idx_audit_logs_target", table: "audit_logs", columns: "target"},
{name: "idx_legacy_json_revisions_name", table: "legacy_json_revisions", columns: "name, id"},
{name: "idx_release_notices_version", table: "release_notices", columns: "version"},
{name: "idx_release_notice_revisions_version", table: "release_notice_revisions", columns: "version, id"},
}
}
func createSchemaIndexes(conn *sql.DB, d dialect) error {
for _, index := range schemaIndexes() {
if d.name == "mysql" {
exists, err := mysqlIndexExists(conn, index)
if err != nil {
return err
}
if exists {
continue
}
}
if _, err := conn.Exec(d.rebind(createIndexStatement(d, index))); err != nil {
return err
}
}
return nil
}
func mysqlIndexExists(conn *sql.DB, index schemaIndex) (bool, error) {
var count int
err := conn.QueryRow(`SELECT COUNT(1)
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = ?
AND index_name = ?`, index.table, index.name).Scan(&count)
return count > 0, err
}
func createIndexStatement(d dialect, index schemaIndex) string {
if d.name == "mysql" {
return fmt.Sprintf("CREATE INDEX %s ON %s(%s)", d.quoteIdent(index.name), d.quoteIdent(index.table), index.columns)
}
return fmt.Sprintf("CREATE INDEX IF NOT EXISTS %s ON %s(%s)", d.quoteIdent(index.name), d.quoteIdent(index.table), index.columns)
}
func (s *Store) recordSchemaVersion(conn *sql.DB, d dialect) error {
columns := []string{"version", "applied_at", "description"}
_, err := conn.Exec(d.rebind(d.upsert("schema_migrations", columns, []string{"version"})),
CurrentSchemaVersion,
Now(),
"unified-management layered monolith baseline",
)
return err
}