服务端媒体源导入/保存/客户端输出链路修复:支持 snake/camel、subcategories/sources,默认客户端可见,保存后发布兼容 media-types.json。
build-winui / winui (push) Waiting to run
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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user