feat(HEL-164): 显示管理员刷新实际结果
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -26,13 +26,15 @@ class SystemHttpMixin:
|
|||||||
def start_background_refresh(self) -> None:
|
def start_background_refresh(self) -> None:
|
||||||
try:
|
try:
|
||||||
body = self.read_json_body(allow_empty=True)
|
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())
|
str(body.get("trade_date") or date.today().isoformat())
|
||||||
)
|
)
|
||||||
|
started = bool(refresh.get("started"))
|
||||||
self.send_json(
|
self.send_json(
|
||||||
{
|
{
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"started": started,
|
"started": started,
|
||||||
|
"job_key": str(refresh.get("job_key") or ""),
|
||||||
"message": "后台刷新已开始" if started else "已有后台刷新任务正在运行",
|
"message": "后台刷新已开始" if started else "已有后台刷新任务正在运行",
|
||||||
},
|
},
|
||||||
HTTPStatus.ACCEPTED,
|
HTTPStatus.ACCEPTED,
|
||||||
|
|||||||
@@ -20,15 +20,16 @@ class JobServiceMixin:
|
|||||||
workers_stopped = self.jobs.wait_for_idle(timeout_seconds)
|
workers_stopped = self.jobs.wait_for_idle(timeout_seconds)
|
||||||
return scheduler_stopped and workers_stopped
|
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)
|
normalized = normalize_date(trade_date)
|
||||||
key = f"manual:{normalized}:{time.time_ns()}"
|
key = f"manual:{normalized}:{time.time_ns()}"
|
||||||
return self.jobs.submit(
|
started = self.jobs.submit(
|
||||||
"market.refresh",
|
"market.refresh",
|
||||||
key,
|
key,
|
||||||
lambda: self.sync_dashboard(normalized),
|
lambda: self.sync_dashboard(normalized),
|
||||||
{"trade_date": normalized, "trigger": "administrator"},
|
{"trade_date": normalized, "trigger": "administrator"},
|
||||||
)
|
)
|
||||||
|
return {"started": started, "job_key": key if started else ""}
|
||||||
|
|
||||||
def _background_refresh_tick(self) -> None:
|
def _background_refresh_tick(self) -> None:
|
||||||
if not (
|
if not (
|
||||||
|
|||||||
@@ -461,8 +461,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "frontend/index.html",
|
"path": "frontend/index.html",
|
||||||
"bytes": 47891,
|
"bytes": 48077,
|
||||||
"lines": 661
|
"lines": 662
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "backend/features/screener/catalog.py",
|
"path": "backend/features/screener/catalog.py",
|
||||||
@@ -554,6 +554,11 @@
|
|||||||
"bytes": 13176,
|
"bytes": 13176,
|
||||||
"lines": 293
|
"lines": 293
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "frontend/shared/dashboard.js",
|
||||||
|
"bytes": 12894,
|
||||||
|
"lines": 274
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "backend/features/market/insights_auction_data.py",
|
"path": "backend/features/market/insights_auction_data.py",
|
||||||
"bytes": 12829,
|
"bytes": 12829,
|
||||||
@@ -574,11 +579,6 @@
|
|||||||
"bytes": 10539,
|
"bytes": 10539,
|
||||||
"lines": 244
|
"lines": 244
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"path": "frontend/shared/dashboard.js",
|
|
||||||
"bytes": 9993,
|
|
||||||
"lines": 220
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path": "backend/data/providers/tushare_sectors.py",
|
"path": "backend/data/providers/tushare_sectors.py",
|
||||||
"bytes": 9876,
|
"bytes": 9876,
|
||||||
@@ -799,16 +799,16 @@
|
|||||||
"bytes": 1919,
|
"bytes": 1919,
|
||||||
"lines": 45
|
"lines": 45
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "backend/jobs/service.py",
|
||||||
|
"bytes": 1833,
|
||||||
|
"lines": 50
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "backend/features/system/routes.py",
|
"path": "backend/features/system/routes.py",
|
||||||
"bytes": 1791,
|
"bytes": 1791,
|
||||||
"lines": 46
|
"lines": 46
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"path": "backend/jobs/service.py",
|
|
||||||
"bytes": 1746,
|
|
||||||
"lines": 49
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path": "backend/features/alerts/routes.py",
|
"path": "backend/features/alerts/routes.py",
|
||||||
"bytes": 1687,
|
"bytes": 1687,
|
||||||
|
|||||||
@@ -609,6 +609,7 @@
|
|||||||
<label class="form-field"><span>iFinD Refresh Token</span><input id="systemIfindTokenInput" type="password" autocomplete="off" maxlength="2048" placeholder="留空保留现有 Token"></label>
|
<label class="form-field"><span>iFinD Refresh Token</span><input id="systemIfindTokenInput" type="password" autocomplete="off" maxlength="2048" placeholder="留空保留现有 Token"></label>
|
||||||
<label class="switch-control"><input id="systemBackgroundRefresh" type="checkbox"><span>启用交易时段后台刷新</span></label>
|
<label class="switch-control"><input id="systemBackgroundRefresh" type="checkbox"><span>启用交易时段后台刷新</span></label>
|
||||||
<p class="form-hint">所有用户读取同一份后台快照,页面不会随后台任务自动重绘。</p>
|
<p class="form-hint">所有用户读取同一份后台快照,页面不会随后台任务自动重绘。</p>
|
||||||
|
<div id="adminRefreshStatus" class="admin-refresh-status" data-tone="idle" role="status" aria-live="polite"><i data-lucide="circle-dot"></i><span>尚未手动刷新</span></div>
|
||||||
<div class="dialog-actions admin-inline-actions"><button id="adminRefreshButton" class="button" type="button"><i data-lucide="refresh-cw"></i>立即后台刷新</button><button class="button primary" type="submit">保存行情配置</button></div>
|
<div class="dialog-actions admin-inline-actions"><button id="adminRefreshButton" class="button" type="button"><i data-lucide="refresh-cw"></i>立即后台刷新</button><button class="button primary" type="submit">保存行情配置</button></div>
|
||||||
</form>
|
</form>
|
||||||
<section class="settings-section">
|
<section class="settings-section">
|
||||||
|
|||||||
@@ -471,6 +471,32 @@
|
|||||||
line-height: 1.6;
|
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 {
|
.account-button > span {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|
||||||
|
|||||||
@@ -40,17 +40,71 @@ async function loadDashboard(force = false, background = false, showOverlay = tr
|
|||||||
async function startAdminRefresh() {
|
async function startAdminRefresh() {
|
||||||
const buttons = [document.querySelector("#syncButton"), document.querySelector("#adminRefreshButton")].filter(Boolean);
|
const buttons = [document.querySelector("#syncButton"), document.querySelector("#adminRefreshButton")].filter(Boolean);
|
||||||
buttons.forEach((button) => { button.disabled = true; });
|
buttons.forEach((button) => { button.disabled = true; });
|
||||||
|
const requestedDate = elements.tradeDate.value;
|
||||||
|
setAdminRefreshStatus("running", `正在刷新 ${requestedDate} 的行情,请稍候…`, "loader-circle");
|
||||||
try {
|
try {
|
||||||
const payload = await apiRequest("/api/admin/refresh", "POST", { trade_date: elements.tradeDate.value });
|
const payload = await apiRequest("/api/admin/refresh", "POST", { trade_date: requestedDate });
|
||||||
showToast(payload.message || "后台刷新已提交");
|
if (!payload.started || !payload.job_key) {
|
||||||
setStatus("后台刷新运行中,当前页面保持不变");
|
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) {
|
} catch (error) {
|
||||||
showToast(error.message || "后台刷新启动失败");
|
const message = error.message || "后台刷新失败";
|
||||||
|
setAdminRefreshStatus("failure", `刷新失败:${message}`, "circle-x");
|
||||||
|
setStatus("后台刷新失败");
|
||||||
|
showToast(message);
|
||||||
} finally {
|
} finally {
|
||||||
buttons.forEach((button) => { button.disabled = false; });
|
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 = `<i data-lucide="${icon}"></i><span>${escapeHtml(message)}</span>`;
|
||||||
|
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) {
|
function applyDashboard(payload, background = false) {
|
||||||
state.dashboard = payload;
|
state.dashboard = payload;
|
||||||
const selectedDate = payload.meta.requested_date || payload.meta.trade_date;
|
const selectedDate = payload.meta.requested_date || payload.meta.trade_date;
|
||||||
|
|||||||
Reference in New Issue
Block a user