fix(api): align frontend with REST contracts

This commit is contained in:
2026-07-30 20:38:52 +08:00
parent b57e58ff87
commit 782363cfb6
65 changed files with 99362 additions and 318 deletions
@@ -322,16 +322,15 @@ const AnalysisParameters: React.FC<AnalysisParametersProps> = ({
: "";
const modify_total_duration = duration;
const params = {
network: network,
modify_pattern_start_time: modify_pattern_start_time,
burst_ID: burst_ID,
burst_id: burst_ID,
burst_size: burst_size,
modify_total_duration: modify_total_duration,
scheme_name: schemeName,
};
try {
await api.get(`${config.BACKEND_URL}/api/v1/burst-analysis`, {
await api.post(`${config.BACKEND_URL}/api/v1/burst-analyses`, undefined, {
params,
paramsSerializer: {
indexes: null, // 移除数组索引,即由 burst_ID[] 变为 burst_ID
@@ -29,9 +29,12 @@ import {
PrintOutlined,
} from "@mui/icons-material";
import { NETWORK_NAME } from "@config/config";
import { queryFeaturesByIds } from "@/utils/mapQueryService";
import {
clearPipeDiameterCache,
getCachedPipeDiameters,
getPipeDiameterDisplay,
getPipeDiameterQueryKey,
loadPipeDiameters,
type PipeDiameterMap,
} from "./schemePipeDiameters";
import { SchemeRecord, ValveIsolationResult } from "./types";
@@ -54,66 +57,7 @@ const REPORT_BLUE = "#0b4f87";
const REPORT_INK = "#172435";
const REPORT_MUTED = "#5f6f80";
const REPORT_LINE = "#d8e0e8";
const diameterCache = new Map<string, PipeDiameterMap>();
const diameterRequestCache = new Map<string, Promise<PipeDiameterMap>>();
export const clearAnalysisReportDiameterCache = () => {
diameterCache.clear();
diameterRequestCache.clear();
};
const loadPipeDiameters = async (
pipeIds: string[],
queryKey: string,
): Promise<PipeDiameterMap> => {
const cachedDiameters = diameterCache.get(queryKey);
if (cachedDiameters) {
return cachedDiameters;
}
const cachedRequest = diameterRequestCache.get(queryKey);
if (cachedRequest) {
return cachedRequest;
}
const request = (async () => {
let features = await queryFeaturesByIds(pipeIds, "geo_pipes_mat");
const foundIds = new Set(
features.map((feature) => String(feature.getProperties().id)),
);
const missingIds = pipeIds.filter((pipeId) => !foundIds.has(pipeId));
if (missingIds.length) {
features = [
...features,
...(await queryFeaturesByIds(missingIds, "geo_pipes")),
];
}
const diameters: PipeDiameterMap = Object.fromEntries(
pipeIds.map((pipeId) => [pipeId, null]),
);
features.forEach((feature) => {
const properties = feature.getProperties();
const pipeId = String(properties.id);
const diameter = Number(properties.diameter);
if (pipeIds.includes(pipeId)) {
diameters[pipeId] = Number.isFinite(diameter) ? diameter : null;
}
});
diameterCache.set(queryKey, diameters);
return diameters;
})();
diameterRequestCache.set(queryKey, request);
try {
return await request;
} finally {
if (diameterRequestCache.get(queryKey) === request) {
diameterRequestCache.delete(queryKey);
}
}
};
export const clearAnalysisReportDiameterCache = clearPipeDiameterCache;
const formatDateTime = (value: Date | string | null | undefined) => {
if (!value) return "未记录";
@@ -411,8 +355,8 @@ const AnalysisReport: React.FC<AnalysisReportProps> = ({
() => scheme?.schemeDetail?.burst_ID ?? [],
[scheme],
);
const diameterQueryKey = pipeIds.join("\u0000");
const cachedDiameters = diameterCache.get(diameterQueryKey);
const diameterQueryKey = getPipeDiameterQueryKey(pipeIds);
const cachedDiameters = getCachedPipeDiameters(pipeIds);
const [diameterState, setDiameterState] = useState<{
queryKey: string | null;
values: PipeDiameterMap;
@@ -435,13 +379,13 @@ const AnalysisReport: React.FC<AnalysisReportProps> = ({
if (!pipeIds.length) {
return;
}
if (diameterCache.has(diameterQueryKey)) {
if (getCachedPipeDiameters(pipeIds)) {
return;
}
let cancelled = false;
loadPipeDiameters(pipeIds, diameterQueryKey)
loadPipeDiameters(pipeIds)
.then((nextDiameters) => {
if (!cancelled) {
setDiameterState({
@@ -53,7 +53,9 @@ import { toLonLat } from "ol/proj";
import Timeline from "@components/olmap/core/Controls/Timeline";
import { SchemaItem, SchemeRecord } from "./types";
import {
getCachedPipeDiameters,
getPipeDiameterDisplay,
loadPipeDiameters,
type PipeDiameterMap,
} from "./schemePipeDiameters";
import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName";
@@ -160,7 +162,6 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
setLoading(true);
try {
const params: Record<string, string> = {
network,
scheme_type: SCHEME_TYPE,
};
if (!queryAll && queryDate) {
@@ -251,40 +252,23 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
return;
}
const cachedDiameters = getCachedPipeDiameters(pipeIds);
if (cachedDiameters) {
setPipeDiametersByScheme((previous) => ({
...previous,
[expandedId]: cachedDiameters,
}));
return;
}
let cancelled = false;
setLoadingDiameterByScheme((previous) => ({
...previous,
[expandedId]: true,
}));
const loadPipeDiameters = async () => {
let features = await queryFeaturesByIds(pipeIds, "geo_pipes_mat");
const foundPipeIds = new Set(
features.map((feature) => String(feature.getProperties().id)),
);
const missingPipeIds = pipeIds.filter(
(pipeId) => !foundPipeIds.has(pipeId),
);
if (missingPipeIds.length > 0) {
const fallbackFeatures = await queryFeaturesByIds(
missingPipeIds,
"geo_pipes",
);
features = [...features, ...fallbackFeatures];
}
const nextDiameters: PipeDiameterMap = Object.fromEntries(
pipeIds.map((pipeId) => [pipeId, null]),
);
features.forEach((feature) => {
const properties = feature.getProperties();
const pipeId = String(properties.id);
const diameter = Number(properties.diameter);
nextDiameters[pipeId] = Number.isFinite(diameter) ? diameter : null;
});
const applyPipeDiameters = async () => {
const nextDiameters = await loadPipeDiameters(pipeIds);
if (cancelled) {
return;
}
@@ -299,7 +283,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
}));
};
loadPipeDiameters().catch((error) => {
applyPipeDiameters().catch((error) => {
console.error("查询管径失败:", error);
if (!cancelled) {
setLoadingDiameterByScheme((previous) => ({
@@ -361,14 +361,14 @@ const ValveIsolation: React.FC<ValveIsolationProps> = ({
}
try {
const params: any = {
network: NETWORK_NAME,
accident_element: ids,
};
if (disabled.length > 0) {
params.disabled_valves = disabled;
}
const response = await api.get<ValveIsolationResult>(
`${config.BACKEND_URL}/api/v1/valve-isolation-analysis`,
const response = await api.post<ValveIsolationResult>(
`${config.BACKEND_URL}/api/v1/valve-isolation-analyses`,
undefined,
{
params,
paramsSerializer: {
@@ -1,4 +1,48 @@
import { getPipeDiameterDisplay } from "./schemePipeDiameters";
import { queryFeaturesByIds } from "@/utils/mapQueryService";
import {
clearPipeDiameterCache,
getPipeDiameterDisplay,
loadPipeDiameters,
} from "./schemePipeDiameters";
jest.mock("@/utils/mapQueryService", () => ({
queryFeaturesByIds: jest.fn(),
}));
const feature = (id: string, diameter: number) => ({
getProperties: () => ({ id, diameter }),
});
afterEach(() => {
clearPipeDiameterCache();
jest.clearAllMocks();
});
describe("loadPipeDiameters", () => {
it("shares an in-flight WFS request and reuses its resolved result", async () => {
let resolveFeatures: ((features: ReturnType<typeof feature>[]) => void) | undefined;
const pendingFeatures = new Promise<ReturnType<typeof feature>[]>((resolve) => {
resolveFeatures = resolve;
});
(queryFeaturesByIds as jest.Mock).mockImplementation(
async () => pendingFeatures,
);
const firstRequest = loadPipeDiameters(["P-1"]);
const duplicateRequest = loadPipeDiameters(["P-1"]);
expect(queryFeaturesByIds).toHaveBeenCalledTimes(1);
resolveFeatures?.([feature("P-1", 315)]);
await expect(firstRequest).resolves.toEqual({ "P-1": 315 });
await expect(duplicateRequest).resolves.toEqual({ "P-1": 315 });
await expect(loadPipeDiameters(["P-1"])).resolves.toEqual({
"P-1": 315,
});
expect(queryFeaturesByIds).toHaveBeenCalledTimes(1);
});
});
describe("getPipeDiameterDisplay", () => {
it("shows the actual diameter for one burst pipe", () => {
@@ -1,5 +1,94 @@
import { queryFeaturesByIds } from "@/utils/mapQueryService";
import config from "@config/config";
export type PipeDiameterMap = Record<string, number | null | undefined>;
const diameterCache = new Map<string, PipeDiameterMap>();
const diameterRequestCache = new Map<string, Promise<PipeDiameterMap>>();
const normalizePipeIds = (pipeIds: string[]) =>
[...new Set(pipeIds.map((pipeId) => String(pipeId).trim()).filter(Boolean))];
export const getPipeDiameterQueryKey = (pipeIds: string[]) =>
[
config.MAP_URL,
config.MAP_WORKSPACE,
...normalizePipeIds(pipeIds).sort(),
].join("\u0000");
export const getCachedPipeDiameters = (pipeIds: string[]) =>
diameterCache.get(getPipeDiameterQueryKey(pipeIds));
export const clearPipeDiameterCache = () => {
diameterCache.clear();
diameterRequestCache.clear();
};
export const loadPipeDiameters = async (
pipeIds: string[],
): Promise<PipeDiameterMap> => {
const normalizedPipeIds = normalizePipeIds(pipeIds);
if (normalizedPipeIds.length === 0) {
return {};
}
const queryKey = getPipeDiameterQueryKey(normalizedPipeIds);
const cachedDiameters = diameterCache.get(queryKey);
if (cachedDiameters) {
return cachedDiameters;
}
const cachedRequest = diameterRequestCache.get(queryKey);
if (cachedRequest) {
return cachedRequest;
}
const request = (async () => {
let features = await queryFeaturesByIds(
normalizedPipeIds,
"geo_pipes_mat",
);
const foundPipeIds = new Set(
features.map((feature) => String(feature.getProperties().id)),
);
const missingPipeIds = normalizedPipeIds.filter(
(pipeId) => !foundPipeIds.has(pipeId),
);
if (missingPipeIds.length > 0) {
const fallbackFeatures = await queryFeaturesByIds(
missingPipeIds,
"geo_pipes",
);
features = [...features, ...fallbackFeatures];
}
const diameters: PipeDiameterMap = Object.fromEntries(
normalizedPipeIds.map((pipeId) => [pipeId, null]),
);
features.forEach((feature) => {
const properties = feature.getProperties();
const pipeId = String(properties.id);
const diameter = Number(properties.diameter);
if (pipeId in diameters) {
diameters[pipeId] = Number.isFinite(diameter) ? diameter : null;
}
});
diameterCache.set(queryKey, diameters);
return diameters;
})();
diameterRequestCache.set(queryKey, request);
try {
return await request;
} finally {
if (diameterRequestCache.get(queryKey) === request) {
diameterRequestCache.delete(queryKey);
}
}
};
export const getPipeDiameterDisplay = (
pipeIds: string[] | undefined,
diameters: PipeDiameterMap | undefined,