diff --git a/backend/features/system/http.py b/backend/features/system/http.py
index 531cceb..036bf39 100644
--- a/backend/features/system/http.py
+++ b/backend/features/system/http.py
@@ -26,13 +26,15 @@ class SystemHttpMixin:
def start_background_refresh(self) -> None:
try:
body = self.read_json_body(allow_empty=True)
- started = self.application_service.request_background_sync(
+ refresh = self.application_service.request_background_sync(
str(body.get("trade_date") or date.today().isoformat())
)
+ started = bool(refresh.get("started"))
self.send_json(
{
"ok": True,
"started": started,
+ "job_key": str(refresh.get("job_key") or ""),
"message": "后台刷新已开始" if started else "已有后台刷新任务正在运行",
},
HTTPStatus.ACCEPTED,
diff --git a/backend/jobs/service.py b/backend/jobs/service.py
index 35bc31e..216d22a 100644
--- a/backend/jobs/service.py
+++ b/backend/jobs/service.py
@@ -20,15 +20,16 @@ class JobServiceMixin:
workers_stopped = self.jobs.wait_for_idle(timeout_seconds)
return scheduler_stopped and workers_stopped
- def request_background_sync(self, trade_date: str) -> bool:
+ def request_background_sync(self, trade_date: str) -> dict[str, object]:
normalized = normalize_date(trade_date)
key = f"manual:{normalized}:{time.time_ns()}"
- return self.jobs.submit(
+ started = self.jobs.submit(
"market.refresh",
key,
lambda: self.sync_dashboard(normalized),
{"trade_date": normalized, "trigger": "administrator"},
)
+ return {"started": started, "job_key": key if started else ""}
def _background_refresh_tick(self) -> None:
if not (
diff --git a/config/architecture-inventory.json b/config/architecture-inventory.json
index 12368d2..d06e930 100644
--- a/config/architecture-inventory.json
+++ b/config/architecture-inventory.json
@@ -461,8 +461,8 @@
},
{
"path": "frontend/index.html",
- "bytes": 47891,
- "lines": 661
+ "bytes": 48077,
+ "lines": 662
},
{
"path": "backend/features/screener/catalog.py",
@@ -554,6 +554,11 @@
"bytes": 13176,
"lines": 293
},
+ {
+ "path": "frontend/shared/dashboard.js",
+ "bytes": 12894,
+ "lines": 274
+ },
{
"path": "backend/features/market/insights_auction_data.py",
"bytes": 12829,
@@ -574,11 +579,6 @@
"bytes": 10539,
"lines": 244
},
- {
- "path": "frontend/shared/dashboard.js",
- "bytes": 9993,
- "lines": 220
- },
{
"path": "backend/data/providers/tushare_sectors.py",
"bytes": 9876,
@@ -799,16 +799,16 @@
"bytes": 1919,
"lines": 45
},
+ {
+ "path": "backend/jobs/service.py",
+ "bytes": 1833,
+ "lines": 50
+ },
{
"path": "backend/features/system/routes.py",
"bytes": 1791,
"lines": 46
},
- {
- "path": "backend/jobs/service.py",
- "bytes": 1746,
- "lines": 49
- },
{
"path": "backend/features/alerts/routes.py",
"bytes": 1687,
diff --git a/frontend/index.html b/frontend/index.html
index 94f4181..4f0ca64 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -609,6 +609,7 @@
所有用户读取同一份后台快照,页面不会随后台任务自动重绘。
+ 尚未手动刷新
diff --git a/frontend/shared/auth.css b/frontend/shared/auth.css
index 424d982..22d516c 100644
--- a/frontend/shared/auth.css
+++ b/frontend/shared/auth.css
@@ -471,6 +471,32 @@
line-height: 1.6;
}
+.admin-refresh-status {
+ display: flex;
+ align-items: flex-start;
+ gap: 8px;
+ margin-top: 12px;
+ padding: 10px 12px;
+ border: 1px solid var(--line);
+ border-radius: 10px;
+ background: var(--surface-muted);
+ color: var(--text-muted);
+ font-size: 12px;
+ line-height: 1.55;
+}
+
+.admin-refresh-status svg {
+ flex: 0 0 auto;
+ width: 15px;
+ height: 15px;
+ margin-top: 2px;
+}
+
+.admin-refresh-status[data-tone="running"] { color: var(--primary); }
+.admin-refresh-status[data-tone="success"] { color: var(--down); }
+.admin-refresh-status[data-tone="warning"] { color: var(--warning); }
+.admin-refresh-status[data-tone="failure"] { color: var(--up); }
+
.account-button > span {
flex: 0 0 auto;
diff --git a/frontend/shared/dashboard.js b/frontend/shared/dashboard.js
index 41e9e57..54b612f 100644
--- a/frontend/shared/dashboard.js
+++ b/frontend/shared/dashboard.js
@@ -40,17 +40,71 @@ async function loadDashboard(force = false, background = false, showOverlay = tr
async function startAdminRefresh() {
const buttons = [document.querySelector("#syncButton"), document.querySelector("#adminRefreshButton")].filter(Boolean);
buttons.forEach((button) => { button.disabled = true; });
+ const requestedDate = elements.tradeDate.value;
+ setAdminRefreshStatus("running", `正在刷新 ${requestedDate} 的行情,请稍候…`, "loader-circle");
try {
- const payload = await apiRequest("/api/admin/refresh", "POST", { trade_date: elements.tradeDate.value });
- showToast(payload.message || "后台刷新已提交");
- setStatus("后台刷新运行中,当前页面保持不变");
+ const payload = await apiRequest("/api/admin/refresh", "POST", { trade_date: requestedDate });
+ if (!payload.started || !payload.job_key) {
+ setAdminRefreshStatus("warning", "已有刷新任务正在运行,请稍后再试。", "clock-3");
+ showToast(payload.message || "已有后台刷新任务正在运行");
+ return;
+ }
+ setStatus(`正在刷新 ${requestedDate} 的行情`);
+ const job = await waitForAdminRefresh(payload.job_key);
+ if (job.status === "failed") {
+ const reason = job.message || job.error_code || "数据源未返回结果";
+ setAdminRefreshStatus("failure", `刷新失败:${reason}`, "circle-x");
+ setStatus("后台刷新失败");
+ showToast("后台刷新失败");
+ return;
+ }
+ const query = new URLSearchParams({ trade_date: requestedDate });
+ const dashboard = await apiRequest(`/api/dashboard?${query}`);
+ applyDashboard(dashboard);
+ const meta = dashboard.meta || {};
+ const actualDate = String(meta.trade_date || "").slice(0, 10);
+ const requestedCompact = requestedDate.replaceAll("-", "");
+ const actualCompact = actualDate.replaceAll("-", "");
+ const updated = formatTimestamp(meta.updated_at);
+ if (actualCompact !== requestedCompact || meta.carried_forward) {
+ const reason = meta.notice ? `;${meta.notice}` : "";
+ setAdminRefreshStatus("warning", `刷新已完成,但没有获取到 ${requestedDate} 的最新行情;当前仍是 ${actualDate || "未知日期"}${reason}`, "triangle-alert");
+ showToast("刷新完成,但未获取到所选日期的最新行情");
+ } else if (meta.notice) {
+ setAdminRefreshStatus("warning", `已刷新到 ${actualDate}(${updated}),但数据源提示:${meta.notice}`, "triangle-alert");
+ showToast(`已刷新到 ${actualDate},请留意数据源提示`);
+ } else {
+ setAdminRefreshStatus("success", `刷新成功:已获取 ${actualDate} 的最新行情,更新时间 ${updated}`, "circle-check");
+ showToast(`刷新成功:已获取 ${actualDate} 的最新行情`);
+ }
} catch (error) {
- showToast(error.message || "后台刷新启动失败");
+ const message = error.message || "后台刷新失败";
+ setAdminRefreshStatus("failure", `刷新失败:${message}`, "circle-x");
+ setStatus("后台刷新失败");
+ showToast(message);
} finally {
buttons.forEach((button) => { button.disabled = false; });
}
}
+function setAdminRefreshStatus(tone, message, icon = "circle-dot") {
+ const status = document.querySelector("#adminRefreshStatus");
+ if (!status) return;
+ status.dataset.tone = tone;
+ status.innerHTML = `${escapeHtml(message)}`;
+ refreshIcons();
+}
+
+async function waitForAdminRefresh(jobKey) {
+ for (let attempt = 0; attempt < 120; attempt += 1) {
+ const payload = await apiRequest("/api/admin/settings");
+ const job = (payload.data?.jobs || []).find((item) => item.idempotency_key === jobKey);
+ if (job && ["success", "failed"].includes(job.status)) return job;
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ }
+ throw new Error("刷新等待超时,请稍后重试");
+}
+
function applyDashboard(payload, background = false) {
state.dashboard = payload;
const selectedDate = payload.meta.requested_date || payload.meta.trade_date;