feat: 平台同步状态记录(last_synced_at + 前端展示最后同步时间)
This commit is contained in:
+10
-6
@@ -51,15 +51,19 @@ def init_db() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _migrate_assets_db() -> None:
|
def _migrate_assets_db() -> None:
|
||||||
"""轻量迁移:为已有 assets 表补充新增列(SQLite create_all 不会修改已有表结构)"""
|
"""轻量迁移:为已有表补充新增列(SQLite create_all 不会修改已有表结构)"""
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
with assets_engine.begin() as conn:
|
with assets_engine.begin() as conn:
|
||||||
if not sa.inspect(conn).has_table("assets"):
|
insp = sa.inspect(conn)
|
||||||
return
|
if insp.has_table("assets"):
|
||||||
cols = {c["name"] for c in sa.inspect(conn).get_columns("assets")}
|
cols = {c["name"] for c in insp.get_columns("assets")}
|
||||||
if "external_id" not in cols:
|
if "external_id" not in cols:
|
||||||
conn.execute(sa.text("ALTER TABLE assets ADD COLUMN external_id VARCHAR"))
|
conn.execute(sa.text("ALTER TABLE assets ADD COLUMN external_id VARCHAR"))
|
||||||
|
if insp.has_table("providers"):
|
||||||
|
cols = {c["name"] for c in insp.get_columns("providers")}
|
||||||
|
if "last_synced_at" not in cols:
|
||||||
|
conn.execute(sa.text("ALTER TABLE providers ADD COLUMN last_synced_at DATETIME"))
|
||||||
|
|
||||||
|
|
||||||
def get_session() -> Generator[Session, None, None]:
|
def get_session() -> Generator[Session, None, None]:
|
||||||
|
|||||||
@@ -41,4 +41,7 @@ class Provider(SQLModel, table=True):
|
|||||||
)
|
)
|
||||||
enabled: bool = Field(default=True, description="是否启用")
|
enabled: bool = Field(default=True, description="是否启用")
|
||||||
remark: Optional[str] = Field(default=None, description="备注")
|
remark: Optional[str] = Field(default=None, description="备注")
|
||||||
|
last_synced_at: Optional[datetime] = Field(
|
||||||
|
default=None, description="最近一次 SDK 同步时间"
|
||||||
|
)
|
||||||
created_at: datetime = Field(default_factory=datetime.utcnow, description="创建时间")
|
created_at: datetime = Field(default_factory=datetime.utcnow, description="创建时间")
|
||||||
|
|||||||
@@ -42,4 +42,5 @@ class ProviderUpdate(SQLModel):
|
|||||||
class ProviderRead(ProviderBase):
|
class ProviderRead(ProviderBase):
|
||||||
id: int
|
id: int
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
last_synced_at: Optional[datetime] = None
|
||||||
has_api_config: bool = False
|
has_api_config: bool = False
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import HTTPException, status
|
from fastapi import HTTPException, status
|
||||||
@@ -217,4 +218,8 @@ def sync_provider(session: Session, provider_id: int) -> dict:
|
|||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
result["account_error"] = str(e)
|
result["account_error"] = str(e)
|
||||||
|
|
||||||
|
provider.last_synced_at = datetime.utcnow()
|
||||||
|
session.add(provider)
|
||||||
|
session.commit()
|
||||||
|
result["last_synced_at"] = provider.last_synced_at.isoformat()
|
||||||
return result
|
return result
|
||||||
|
|||||||
+6
-1
@@ -315,6 +315,7 @@ const ProvidersView = {
|
|||||||
<span v-else class="text-slate-400">未配置 API</span>
|
<span v-else class="text-slate-400">未配置 API</span>
|
||||||
<span :class="p.enabled ? 'text-emerald-600 dark:text-emerald-400' : 'text-slate-400'">{{ p.enabled ? '启用' : '停用' }}</span>
|
<span :class="p.enabled ? 'text-emerald-600 dark:text-emerald-400' : 'text-slate-400'">{{ p.enabled ? '启用' : '停用' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="p.last_synced_at" class="text-slate-400">最后同步:{{ fmtTime(p.last_synced_at) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-4 mt-3 pt-2 border-t border-slate-100 dark:border-slate-800 text-xs flex-wrap">
|
<div class="flex gap-4 mt-3 pt-2 border-t border-slate-100 dark:border-slate-800 text-xs flex-wrap">
|
||||||
<button @click="edit(p)" class="text-blue-600 dark:text-blue-400">编辑</button>
|
<button @click="edit(p)" class="text-blue-600 dark:text-blue-400">编辑</button>
|
||||||
@@ -335,6 +336,10 @@ const ProvidersView = {
|
|||||||
try { const r = await Api.get('/providers/adapters'); supported.value = r.supported_types || []; } catch (e) { /* ignore */ }
|
try { const r = await Api.get('/providers/adapters'); supported.value = r.supported_types || []; } catch (e) { /* ignore */ }
|
||||||
});
|
});
|
||||||
function isSupported(t) { return !!t && supported.value.includes(t); }
|
function isSupported(t) { return !!t && supported.value.includes(t); }
|
||||||
|
function fmtTime(iso) {
|
||||||
|
if (!iso) return '';
|
||||||
|
return new Date(iso).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
|
||||||
|
}
|
||||||
async function test(p) {
|
async function test(p) {
|
||||||
testing[p.id] = true; delete results[p.id];
|
testing[p.id] = true; delete results[p.id];
|
||||||
try {
|
try {
|
||||||
@@ -358,7 +363,7 @@ const ProvidersView = {
|
|||||||
} catch (e) { results[p.id] = { ok: false, text: '✗ ' + e.message }; }
|
} catch (e) { results[p.id] = { ok: false, text: '✗ ' + e.message }; }
|
||||||
finally { syncing[p.id] = false; }
|
finally { syncing[p.id] = false; }
|
||||||
}
|
}
|
||||||
return { store, Fmt, testing, syncing, results, isSupported, test, sync, seed: seedProviders, create: openProviderCreate, edit: openProviderEdit, del: deleteProvider };
|
return { store, Fmt, testing, syncing, results, isSupported, fmtTime, test, sync, seed: seedProviders, create: openProviderCreate, edit: openProviderEdit, del: deleteProvider };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user