Reuse the existing simulation endpoint and align backfill to the hydraulic timestep. Block backfill when event metadata fails so the fallback query window cannot be calculated as event history.
80 lines
3.6 KiB
TypeScript
80 lines
3.6 KiB
TypeScript
import dayjs from "dayjs";
|
|
import utc from "dayjs/plugin/utc";
|
|
import timezone from "dayjs/plugin/timezone";
|
|
import config from "@/config/config";
|
|
import { apiFetch } from "@/lib/apiFetch";
|
|
import { useProjectStore } from "@/store/projectStore";
|
|
|
|
dayjs.extend(utc);
|
|
dayjs.extend(timezone);
|
|
|
|
export type SimulationRange = { from: Date; to: Date };
|
|
|
|
export const parseHydraulicStepSeconds = (value: unknown): number | undefined => {
|
|
if (typeof value !== "string" || !/^\d+:\d{1,2}(:\d{1,2})?$/.test(value.trim())) {
|
|
return undefined;
|
|
}
|
|
const [hours, minutes, seconds = 0] = value.trim().split(":").map(Number);
|
|
const step = hours * 3600 + minutes * 60 + seconds;
|
|
return minutes < 60 && seconds < 60 && step > 0 ? step : undefined;
|
|
};
|
|
|
|
/** 与地图时间轴一致,以北京时间零点为基准向外取整,跨日仍保持同一步长。 */
|
|
export const alignSimulationRange = (
|
|
range: SimulationRange,
|
|
stepSeconds: number,
|
|
): SimulationRange => {
|
|
if (!Number.isFinite(stepSeconds) || stepSeconds <= 0 ||
|
|
!Number.isFinite(range.from.getTime()) || !Number.isFinite(range.to.getTime())) {
|
|
throw new Error("查询时间或水力时间步长无效");
|
|
}
|
|
const start = Math.min(range.from.getTime(), range.to.getTime());
|
|
const end = Math.max(range.from.getTime(), range.to.getTime());
|
|
const origin = dayjs(start).tz("Asia/Shanghai").startOf("day").valueOf();
|
|
const step = stepSeconds * 1000;
|
|
const from = origin + Math.floor((start - origin) / step) * step;
|
|
const to = Math.max(from + step, origin + Math.ceil((end - origin) / step) * step);
|
|
return { from: new Date(from), to: new Date(to) };
|
|
};
|
|
|
|
/** 复用同步在线模拟接口,成功响应表示计算和保存已完成;不自动重试写请求。 */
|
|
export const runOnlineSimulation = async (
|
|
range: SimulationRange,
|
|
signal?: AbortSignal,
|
|
): Promise<{ range: SimulationRange; stepSeconds: number }> => {
|
|
const projectId = useProjectStore.getState().currentProjectId;
|
|
if (!projectId) throw new Error("请先选择项目,未启动在线模拟");
|
|
const projectHeaders = { "X-Project-Id": projectId };
|
|
const timeResponse = await apiFetch(
|
|
`${config.BACKEND_URL}/api/v1/network-options/time`, {
|
|
signal, headers: projectHeaders, projectHeaderMode: "omit",
|
|
},
|
|
);
|
|
if (!timeResponse.ok) throw new Error("无法读取管网时间步长,未启动在线模拟");
|
|
const payload = await timeResponse.json();
|
|
const stepSeconds = parseHydraulicStepSeconds(
|
|
(payload?.times ?? payload)?.["HYDRAULIC TIMESTEP"],
|
|
);
|
|
if (!stepSeconds) throw new Error("管网水力时间步长无效,未启动在线模拟");
|
|
if (signal?.aborted || useProjectStore.getState().currentProjectId !== projectId) {
|
|
throw new Error("项目已切换或面板已关闭,未启动在线模拟");
|
|
}
|
|
const alignedRange = alignSimulationRange(range, stepSeconds);
|
|
const response = await apiFetch(`${config.BACKEND_URL}/api/v1/simulation-runs`, {
|
|
method: "POST",
|
|
headers: { ...projectHeaders, "Content-Type": "application/json" },
|
|
projectHeaderMode: "omit",
|
|
// 后端循环的结束时间不包含在计算内,多算一个步长以覆盖图表右端点。
|
|
body: JSON.stringify({
|
|
start_time: alignedRange.from.toISOString(),
|
|
duration: Math.ceil((alignedRange.to.getTime() - alignedRange.from.getTime() + stepSeconds * 1000) / 60000),
|
|
}),
|
|
signal,
|
|
});
|
|
const result = await response.json().catch(() => null);
|
|
if (!response.ok || result?.status !== "success") {
|
|
throw new Error(typeof result?.detail === "string" ? result.detail : "在线模拟未确认完成,请刷新查看已生成的数据");
|
|
}
|
|
return { range: alignedRange, stepSeconds };
|
|
};
|