服务端媒体源导入/保存/客户端输出链路修复:支持 snake/camel、subcategories/sources,默认客户端可见,保存后发布兼容 media-types.json。
build-winui / winui (push) Waiting to run

新增数据库同步 Job API、持久化状态、实时输出、最新任务恢复,以及系统日志聚合接口。
管理端优化:日志中心、运维实时状态框、同步输出自动滚动、仪表盘“输出”列、真实延迟空态、本地 favicon/avatar。
新增 server/unified-management/assets/favicon.ico 和 developer-avatar.png,并接好 /favicon.ico、/admin/favicon.ico、/setup/favicon.ico、/assets/*。
WinUI 随机放映室卡片优先显示子接口原始 Description。
Inno 安装器输出框改为选区末尾 + SendMessage 滚动到底部。
This commit is contained in:
QWQLwToo
2026-06-29 22:28:58 +08:00
parent f00124c1c0
commit 7745e7a2d4
36 changed files with 1482 additions and 153 deletions
@@ -21,31 +21,80 @@ func (s *Store) CopyRemoteToSQLite() (string, error) {
}
func (s *Store) ImportSQLiteToRemote() (SyncResult, error) {
if !s.trySyncLock() {
return SyncResult{Direction: "sqlite_to_remote", Status: "running", Skipped: true, Tables: map[string]int{}, FinishedAt: Now(), Warnings: []string{"database sync is already running"}}, errors.New("database sync is already running")
}
defer s.syncMu.Unlock()
s.mu.RLock()
remote := s.remoteDB
remoteDialect := s.remoteDialect
local := s.localDB
localDialect := s.localDialect
s.mu.RUnlock()
if remote == nil {
result := SyncResult{Direction: "sqlite_to_remote", Status: "skipped", Skipped: true, Tables: map[string]int{}, FinishedAt: Now(), Warnings: []string{"remote database is not configured"}}
s.setSyncStatus(result, nil)
return result, nil
}
result, err := copyAllTables(local, localDialect, remote, remoteDialect, "sqlite_to_remote")
s.setSyncStatus(result, err)
return result, err
return s.RunDatabaseSync("sqlite_to_remote")
}
func (s *Store) SyncNow() (SyncResult, error) {
return s.RunDatabaseSync("remote_to_sqlite")
}
func (s *Store) QueueDatabaseSync(direction string) (DatabaseSyncJob, error) {
normalized, err := normalizeSyncDirection(direction)
if err != nil {
return DatabaseSyncJob{}, err
}
if !s.trySyncLock() {
return SyncResult{Direction: "remote_to_sqlite", Status: "running", Skipped: true, Tables: map[string]int{}, FinishedAt: Now(), Warnings: []string{"database sync is already running"}}, errors.New("database sync is already running")
job, insertErr := s.insertDatabaseSyncJob(normalized, "skipped", []string{"同步任务未启动:已有数据库同步正在执行。"}, []string{"database sync is already running"}, nil, nil, Now())
if insertErr != nil {
return DatabaseSyncJob{}, insertErr
}
return job, errors.New("database sync is already running")
}
job, err := s.insertDatabaseSyncJob(normalized, "running", []string{"同步任务已创建,正在准备数据库连接。"}, nil, nil, map[string]int{}, "")
if err != nil {
s.syncMu.Unlock()
return DatabaseSyncJob{}, err
}
s.setCurrentSyncJob(&job)
go func(jobID int64, jobDirection string) {
defer s.syncMu.Unlock()
_, _ = s.runDatabaseSyncLocked(jobID, jobDirection)
}(job.ID, normalized)
return job, nil
}
func (s *Store) RunDatabaseSync(direction string) (SyncResult, error) {
normalized, err := normalizeSyncDirection(direction)
if err != nil {
return SyncResult{}, err
}
if !s.trySyncLock() {
result := SyncResult{
Direction: normalized,
Status: "skipped",
Skipped: true,
StartedAt: Now(),
FinishedAt: Now(),
Tables: map[string]int{},
Warnings: []string{"database sync is already running"},
Output: []string{"同步任务未启动:已有数据库同步正在执行。"},
}
_, _ = s.insertDatabaseSyncJob(normalized, result.Status, result.Output, result.Warnings, result.Errors, result.Tables, result.FinishedAt)
s.setSyncStatus(result, errors.New("database sync is already running"))
return result, errors.New("database sync is already running")
}
defer s.syncMu.Unlock()
job, err := s.insertDatabaseSyncJob(normalized, "running", []string{"同步任务已创建,正在准备数据库连接。"}, nil, nil, map[string]int{}, "")
if err != nil {
return SyncResult{}, err
}
s.setCurrentSyncJob(&job)
return s.runDatabaseSyncLocked(job.ID, normalized)
}
func (s *Store) runDatabaseSyncLocked(jobID int64, direction string) (SyncResult, error) {
started := Now()
if job, err := s.GetDatabaseSyncJob(jobID); err == nil && job.StartedAt != "" {
started = job.StartedAt
}
result := SyncResult{
Direction: direction,
Status: "completed",
StartedAt: started,
Tables: map[string]int{},
Output: []string{"同步开始:" + directionLabel(direction)},
}
_ = s.updateDatabaseSyncJob(jobID, "running", result.Output, result.Warnings, result.Errors, result.Tables, "")
s.mu.RLock()
remote := s.remoteDB
remoteDialect := s.remoteDialect
@@ -53,18 +102,43 @@ func (s *Store) SyncNow() (SyncResult, error) {
localDialect := s.localDialect
s.mu.RUnlock()
if remote == nil {
result := SyncResult{Direction: "remote_to_sqlite", Status: "skipped", Skipped: true, Tables: map[string]int{}, FinishedAt: Now(), Warnings: []string{"remote database is not configured"}}
result.Status = "skipped"
result.Skipped = true
result.Warnings = append(result.Warnings, "remote database is not configured")
result.Output = append(result.Output, "远程 MySQL 未配置或不可用,未执行数据复制。")
result.FinishedAt = Now()
_ = s.updateDatabaseSyncJob(jobID, result.Status, result.Output, result.Warnings, result.Errors, result.Tables, result.FinishedAt)
s.setSyncStatus(result, nil)
return result, nil
}
result, err := copyAllTables(remote, remoteDialect, local, localDialect, "remote_to_sqlite")
s.setSyncStatus(result, err)
return result, err
src, srcDialect, dst, dstDialect := local, localDialect, remote, remoteDialect
if direction == "remote_to_sqlite" {
src, srcDialect, dst, dstDialect = remote, remoteDialect, local, localDialect
}
err := copyAllTablesWithProgress(src, srcDialect, dst, dstDialect, direction, func(table string, count int) {
result.Tables[table] = count
result.Output = append(result.Output, fmt.Sprintf("%s%d 条", table, count))
_ = s.updateDatabaseSyncJob(jobID, "running", result.Output, result.Warnings, result.Errors, result.Tables, "")
})
result.FinishedAt = Now()
if err != nil {
result.Status = "failed"
result.Errors = append(result.Errors, err.Error())
result.Output = append(result.Output, "同步失败:"+err.Error())
_ = s.updateDatabaseSyncJob(jobID, result.Status, result.Output, result.Warnings, result.Errors, result.Tables, result.FinishedAt)
s.setSyncStatus(result, err)
return result, err
}
result.Output = append(result.Output, "同步完成:"+directionLabel(direction))
_ = s.updateDatabaseSyncJob(jobID, result.Status, result.Output, result.Warnings, result.Errors, result.Tables, result.FinishedAt)
s.setSyncStatus(result, nil)
return result, nil
}
func (s *Store) setSyncStatus(result SyncResult, err error) {
s.mu.Lock()
defer s.mu.Unlock()
s.status.CurrentSyncJob = nil
if err != nil {
s.status.LastSyncAt = result.FinishedAt
s.status.LastSyncError = err.Error()
@@ -78,6 +152,33 @@ func (s *Store) trySyncLock() bool {
return s.syncMu.TryLock()
}
func (s *Store) setCurrentSyncJob(job *DatabaseSyncJob) {
s.mu.Lock()
defer s.mu.Unlock()
s.status.CurrentSyncJob = job
}
func normalizeSyncDirection(value string) (string, error) {
switch strings.ToLower(strings.TrimSpace(value)) {
case "sqlite_to_remote", "import", "sqlite_to_mysql":
return "sqlite_to_remote", nil
case "remote_to_sqlite", "sync", "mysql_to_sqlite":
return "remote_to_sqlite", nil
default:
return "", errors.New("unsupported database sync direction")
}
}
func directionLabel(direction string) string {
if direction == "sqlite_to_remote" {
return "SQLite -> MySQL"
}
if direction == "remote_to_sqlite" {
return "MySQL -> SQLite"
}
return direction
}
type tableSpec struct {
Name string
Columns []string
@@ -100,6 +201,7 @@ var syncTables = []tableSpec{
{"source_endpoints", []string{"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"}, []string{"id"}},
{"endpoint_health_checks", []string{"id", "source_db_id", "status", "latency_ms", "error", "checked_at"}, []string{"id"}},
{"endpoint_call_logs", []string{"id", "source_id", "status", "latency_ms", "error", "client", "created_at"}, []string{"id"}},
{"database_sync_jobs", []string{"id", "direction", "status", "message", "tables_json", "started_at", "finished_at"}, []string{"id"}},
{"system_settings", []string{"key", "value", "updated_at"}, []string{"key"}},
{"audit_logs", []string{"id", "actor", "type", "target", "message", "ip", "user_agent", "created_at"}, []string{"id"}},
{"legacy_json_revisions", []string{"id", "name", "raw", "note", "created_by", "created_at"}, []string{"id"}},
@@ -109,19 +211,31 @@ var syncTables = []tableSpec{
func copyAllTables(src *sql.DB, srcDialect dialect, dst *sql.DB, dstDialect dialect, direction string) (SyncResult, error) {
result := SyncResult{Direction: direction, Status: "completed", Tables: map[string]int{}, FinishedAt: Now()}
for _, table := range syncTables {
count, err := copyTable(src, srcDialect, dst, dstDialect, table)
if err != nil {
result.Status = "failed"
result.FinishedAt = Now()
return result, err
}
result.Tables[table.Name] = count
err := copyAllTablesWithProgress(src, srcDialect, dst, dstDialect, direction, func(table string, count int) {
result.Tables[table] = count
})
if err != nil {
result.Status = "failed"
result.FinishedAt = Now()
return result, err
}
result.FinishedAt = Now()
return result, nil
}
func copyAllTablesWithProgress(src *sql.DB, srcDialect dialect, dst *sql.DB, dstDialect dialect, direction string, progress func(table string, count int)) error {
for _, table := range syncTables {
count, err := copyTable(src, srcDialect, dst, dstDialect, table)
if err != nil {
return err
}
if progress != nil {
progress(table.Name, count)
}
}
return nil
}
func copyTable(src *sql.DB, srcDialect dialect, dst *sql.DB, dstDialect dialect, spec tableSpec) (int, error) {
rows, err := src.Query(srcDialect.rebind("SELECT " + srcDialect.columnList(spec.Columns) + " FROM " + spec.Name))
if err != nil {