更新客户端适配性问题
build-winui / winui (push) Waiting to run

This commit is contained in:
2026-06-28 08:56:45 +08:00
parent 962a2f2143
commit f00124c1c0
21 changed files with 428 additions and 63 deletions
@@ -22,6 +22,7 @@ type adminRow struct {
type DatabaseStatus struct {
ActiveProvider string `json:"activeProvider"`
ConfigProvider string `json:"configProvider"`
SchemaVersion string `json:"schemaVersion"`
SQLiteReady bool `json:"sqliteReady"`
RemoteReady bool `json:"remoteReady"`
FailoverActive bool `json:"failoverActive"`
+61 -12
View File
@@ -22,6 +22,9 @@ func (s *Store) migrate(conn *sql.DB, d dialect) error {
return err
}
}
if err := createSchemaIndexes(conn, d); err != nil {
return err
}
return s.recordSchemaVersion(conn, d)
}
@@ -281,21 +284,67 @@ func schemaStatements(d dialect) []string {
created_at %s NOT NULL,
finished_at %s NOT NULL DEFAULT ''
)`, d.idType(), keyText, keyText, keyText, longText, shortText, shortText, shortText),
`CREATE INDEX IF NOT EXISTS idx_feedback_tickets_activity ON feedback_tickets(last_activity_at)`,
`CREATE INDEX IF NOT EXISTS idx_feedback_comments_code ON feedback_comments(feedback_code)`,
`CREATE INDEX IF NOT EXISTS idx_feedback_attachments_code ON feedback_attachments(feedback_code)`,
`CREATE INDEX IF NOT EXISTS idx_feedback_events_code ON feedback_events(feedback_code)`,
`CREATE INDEX IF NOT EXISTS idx_mail_records_code ON mail_records(feedback_code)`,
`CREATE INDEX IF NOT EXISTS idx_endpoint_call_logs_source ON endpoint_call_logs(source_id)`,
`CREATE INDEX IF NOT EXISTS idx_audit_logs_created ON audit_logs(created_at)`,
`CREATE INDEX IF NOT EXISTS idx_audit_logs_type ON audit_logs(type)`,
`CREATE INDEX IF NOT EXISTS idx_audit_logs_target ON audit_logs(target)`,
`CREATE INDEX IF NOT EXISTS idx_legacy_json_revisions_name ON legacy_json_revisions(name, id)`,
`CREATE INDEX IF NOT EXISTS idx_release_notices_version ON release_notices(version)`,
`CREATE INDEX IF NOT EXISTS idx_release_notice_revisions_version ON release_notice_revisions(version, id)`,
}
}
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"})),
@@ -66,6 +66,7 @@ func Open(cfg *config.Config) (*Store, error) {
status: DatabaseStatus{
ActiveProvider: "sqlite",
ConfigProvider: cfg.Database.Provider,
SchemaVersion: CurrentSchemaVersion,
SQLiteReady: true,
LastRecoveredAt: Now(),
},
@@ -165,6 +166,7 @@ func (s *Store) ReconfigureDatabase(cfg *config.Config) error {
s.remoteDB = remote
s.remoteDialect = remoteDialect
s.status.ConfigProvider = cfg.Database.Provider
s.status.SchemaVersion = CurrentSchemaVersion
s.status.SQLiteReady = true
s.status.RemoteReady = remote != nil
s.status.LastError = ""
@@ -269,6 +271,7 @@ func (s *Store) openRemote() error {
s.dialect = remoteDialect
s.status.ActiveProvider = "mysql"
s.status.ConfigProvider = "mysql"
s.status.SchemaVersion = CurrentSchemaVersion
s.status.RemoteReady = true
s.status.FailoverActive = false
s.status.LastError = ""
@@ -304,6 +307,7 @@ func (s *Store) checkRemote() {
}
s.status.ActiveProvider = "mysql"
s.status.RemoteReady = true
s.status.SchemaVersion = CurrentSchemaVersion
s.status.FailoverActive = false
s.status.LastError = ""
s.status.LastRecoveredAt = Now()
@@ -320,6 +324,7 @@ func (s *Store) markFailover(err error) {
s.dialect = s.localDialect
s.status.ActiveProvider = "sqlite"
s.status.ConfigProvider = s.cfg.Database.Provider
s.status.SchemaVersion = CurrentSchemaVersion
s.status.RemoteReady = false
s.status.FailoverActive = !strings.EqualFold(s.cfg.Database.Provider, "sqlite")
s.status.LastError = err.Error()
@@ -138,6 +138,51 @@ func TestOpenRecordsCurrentSchemaVersion(t *testing.T) {
}
}
func TestSchemaIndexStatementsAreDialectAware(t *testing.T) {
mysql := dialectFor("mysql")
sqlite := dialectFor("sqlite")
for _, statement := range schemaStatements(mysql) {
if strings.Contains(strings.ToUpper(statement), "CREATE INDEX IF NOT EXISTS") {
t.Fatalf("mysql schema statement contains unsupported index syntax: %s", statement)
}
}
index := schemaIndexes()[0]
mysqlStatement := createIndexStatement(mysql, index)
if strings.Contains(strings.ToUpper(mysqlStatement), "IF NOT EXISTS") {
t.Fatalf("mysql index statement contains unsupported syntax: %s", mysqlStatement)
}
if !strings.Contains(createIndexStatement(sqlite, index), "IF NOT EXISTS") {
t.Fatalf("sqlite index statement should remain idempotent")
}
}
func TestMigrateCreatesSQLiteIndexesIdempotently(t *testing.T) {
root := t.TempDir()
conn, err := sql.Open("sqlite", filepath.Join(root, "indexes.sqlite"))
if err != nil {
t.Fatal(err)
}
defer conn.Close()
store := &Store{}
d := dialectFor("sqlite")
if err := store.migrate(conn, d); err != nil {
t.Fatal(err)
}
if err := store.migrate(conn, d); err != nil {
t.Fatal(err)
}
var name string
if err := conn.QueryRow(`SELECT name FROM sqlite_master WHERE type = 'index' AND name = ?`, "idx_feedback_tickets_activity").Scan(&name); err != nil {
t.Fatal(err)
}
if name != "idx_feedback_tickets_activity" {
t.Fatalf("unexpected index name %q", name)
}
}
func TestChangeAdminPasswordPersistsWhenRemoteSyncFails(t *testing.T) {
root := t.TempDir()
path := filepath.Join(root, "unified.sqlite")
@@ -271,12 +271,16 @@ func (r *router) handleMigrationStatus(w http.ResponseWriter, req *http.Request)
"fileAssets": []map[string]string{
{"name": "downloads", "path": r.cfg.DownloadsDir, "description": "发布包和下载文件"},
{"name": "update public", "path": r.cfg.UpdatePublicDir, "description": "旧客户端兼容 JSON 生成物"},
{"name": "feedback packages", "path": filepath.Join(r.cfg.StorageDir, "feedback-packages"), "description": "反馈附件包"},
{"name": "feedback packages", "path": filepath.Join(r.cfg.StorageDir, "feedback"), "description": "反馈加密包和解密后的本地包"},
},
"sqlitePath": r.store.Path(),
"mysql": config.SafeDatabase(r.cfg.BaseDir, r.cfg.Database),
"schemaVersion": status.SchemaVersion,
"lastSyncAt": status.LastSyncAt,
"lastSyncError": status.LastSyncError,
"lastError": status.LastError,
"failoverActive": status.FailoverActive,
"remoteReady": status.RemoteReady,
"activeProvider": status.ActiveProvider,
},
})
@@ -30,9 +30,12 @@ const tabs = [
</div>
<div class="kv-grid">
<span>配置类型</span><strong>{{ ctx.database?.configProvider || "-" }}</strong>
<span>Schema</span><strong>{{ ctx.database?.schemaVersion || "-" }}</strong>
<span>活动数据库</span><strong>{{ ctx.database?.activeProvider || "-" }}</strong>
<span>SQLite</span><strong>{{ ctx.database?.sqliteReady ? "ready" : "missing" }}</strong>
<span>MySQL</span><strong>{{ ctx.database?.remoteReady ? "ready" : "offline" }}</strong>
<span>Failover</span><strong>{{ ctx.database?.failoverActive ? "active" : "standby" }}</strong>
<span>恢复时间</span><strong>{{ ctx.database?.lastRecoveredAt || "-" }}</strong>
<span>最后同步</span><strong>{{ ctx.database?.lastSyncAt || "-" }}</strong>
<span>最近错误</span><strong>{{ ctx.database?.lastSyncError || ctx.database?.lastError || "-" }}</strong>
</div>
@@ -98,9 +101,12 @@ const tabs = [
<div class="kv-grid">
<span>策略</span><strong>{{ ctx.migrationStatus?.strategy || "-" }}</strong>
<span>SQLite 文件</span><strong>{{ ctx.migrationStatus?.sqlitePath || "-" }}</strong>
<span>Schema</span><strong>{{ ctx.migrationStatus?.schemaVersion || "-" }}</strong>
<span>活动数据库</span><strong>{{ ctx.migrationStatus?.activeProvider || "-" }}</strong>
<span>MySQL</span><strong>{{ ctx.migrationStatus?.remoteReady ? "ready" : "offline" }}</strong>
<span>Failover</span><strong>{{ ctx.migrationStatus?.failoverActive ? "active" : "standby" }}</strong>
<span>最后同步</span><strong>{{ ctx.migrationStatus?.lastSyncAt || "-" }}</strong>
<span>同步错误</span><strong>{{ ctx.migrationStatus?.lastSyncError || "-" }}</strong>
<span>同步错误</span><strong>{{ ctx.migrationStatus?.lastSyncError || ctx.migrationStatus?.lastError || "-" }}</strong>
</div>
<div class="ops-note">
<AlertTriangle :size="16" />