You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2528 lines
80 KiB
2528 lines
80 KiB
"use strict";
|
|
|
|
const superagent = require("superagent");
|
|
const XLSXS = require("xlsx-js-style");
|
|
const moment = require("moment");
|
|
const { generationXlsxData, getMainPointData } = require("./schemeList");
|
|
const { reportBusinessCall } = require("../services/dashboardReporter");
|
|
|
|
const STABLE_LIST_APPLICATION_ID = "stable-list";
|
|
|
|
const buildTableDataFromFactors = (factorList = []) => {
|
|
const groups = new Map();
|
|
factorList.forEach((factor, index) => {
|
|
const factorName = String(factor?.factor_name || "").trim();
|
|
if (!factorName) return;
|
|
if (!groups.has(factorName)) {
|
|
groups.set(factorName, []);
|
|
}
|
|
const sensors = Array.isArray(factor?.sensors) ? factor.sensors : [];
|
|
sensors.forEach((sensor, sensorIndex) => {
|
|
const deviceName = sensor?.target_name || sensor?.source_name || "";
|
|
const deviceModel = sensor?.model || "";
|
|
const deviceErp = sensor?.erp || "";
|
|
const deviceUnit = sensor?.unit || "";
|
|
const deviceCategory = sensor?.signal_type || sensor?.type || "";
|
|
groups.get(factorName).push({
|
|
id: `${index + 1}-${sensorIndex + 1}`,
|
|
category: deviceCategory,
|
|
name: deviceName,
|
|
model: deviceModel,
|
|
code: deviceErp,
|
|
unit: deviceUnit,
|
|
price: "0.00",
|
|
qty: sensor?.count != null ? String(sensor.count) : "1",
|
|
total: "0.00",
|
|
isAux: false,
|
|
});
|
|
});
|
|
|
|
const auxiliaries = Array.isArray(factor?.auxiliary_materials)
|
|
? factor.auxiliary_materials
|
|
: [];
|
|
auxiliaries.forEach((aux, auxIndex) => {
|
|
const auxCategory = aux?.note || aux?.type || "";
|
|
groups.get(factorName).push({
|
|
id: `${index + 1}-aux-${auxIndex + 1}`,
|
|
category: auxCategory,
|
|
name: aux?.name || "",
|
|
model: aux?.model || "",
|
|
code: aux?.erp || "",
|
|
unit: aux?.unit || "",
|
|
price: "0.00",
|
|
qty: aux?.count != null ? String(aux.count) : "1",
|
|
total: "0.00",
|
|
isAux: true,
|
|
});
|
|
});
|
|
});
|
|
return Array.from(groups.entries()).map(([factorName, items], idx) => ({
|
|
id: `G${idx + 1}`,
|
|
name: factorName,
|
|
items: items.length
|
|
? items
|
|
: [
|
|
{
|
|
id: `G${idx + 1}-1`,
|
|
category: "",
|
|
name: "",
|
|
model: "",
|
|
code: "",
|
|
unit: "",
|
|
price: "0.00",
|
|
qty: "1",
|
|
total: "0.00",
|
|
},
|
|
],
|
|
}));
|
|
};
|
|
|
|
const parseCountValue = (value) => {
|
|
if (value === null || value === undefined) return null;
|
|
const text = String(value).trim();
|
|
if (!text) return null;
|
|
const num = Number(text);
|
|
return Number.isFinite(num) ? num : null;
|
|
};
|
|
|
|
const normalizeText = (value = "") =>
|
|
String(value || "")
|
|
.replace(/\s+/g, "")
|
|
.replace(/[,、,]/g, "")
|
|
.replace(/[*]/g, "")
|
|
.replace(/[()()]/g, "")
|
|
.replace(/监测$/g, "")
|
|
.toLowerCase();
|
|
|
|
const matchByKeywords = (value, keywords = [], matchAll = false) => {
|
|
if (!keywords.length) return false;
|
|
const normalized = keywords.map((key) => normalizeText(key));
|
|
if (matchAll) {
|
|
return normalized.every((key) => value.includes(key));
|
|
}
|
|
return normalized.some((key) => value.includes(key));
|
|
};
|
|
|
|
const USER_AUX_RULES = [
|
|
{
|
|
id: "deflection",
|
|
keywords: ["挠度"],
|
|
exclude: ["动挠度", "动态挠度"],
|
|
auxiliaries: [
|
|
{
|
|
name: "储液罐(压差水箱)",
|
|
model: "FS-LTG-MD-V2.0",
|
|
note: "数量=基点+转点数量(由用户问题提供)",
|
|
},
|
|
{
|
|
name: "连接水管(聚醚型,12×8)",
|
|
model: "",
|
|
note: "数量=由用户问题提供",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "dynamic_deflection",
|
|
keywords: ["动挠度", "动态挠度"],
|
|
auxiliaries: [
|
|
{
|
|
name: "红外标靶",
|
|
model: "FS-HWBB-6",
|
|
note: "数量=由用户问题提供",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "communication_system",
|
|
keywords: ["通信系统"],
|
|
auxiliaries: [
|
|
{
|
|
name: "PVC管",
|
|
model: "φ32",
|
|
note: "数量=由用户问题提供",
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const RESUME_FIXED_FACTORS = [
|
|
{
|
|
factor_name: "电力系统(太阳能)",
|
|
auxiliary_material_config: [
|
|
{ name: "太阳能板数量", value: 1 },
|
|
{ name: "电池数量", value: 5 },
|
|
],
|
|
},
|
|
{
|
|
factor_name: "软件系统",
|
|
auxiliary_material_config: [{ name: "平台类型", value: "简约版" }],
|
|
},
|
|
{
|
|
factor_name: "运维服务",
|
|
auxiliary_material_config: [{ name: "运维年限", value: 3 }],
|
|
},
|
|
];
|
|
|
|
const getUserProvidedAuxiliaries = (factorName = "", sensors = []) => {
|
|
const normalizedFactor = normalizeText(factorName);
|
|
const normalizedSensors = (sensors || [])
|
|
.map((sensor) =>
|
|
normalizeText(
|
|
sensor?.target_name ||
|
|
sensor?.source_name ||
|
|
sensor?.device ||
|
|
sensor?.name ||
|
|
"",
|
|
),
|
|
)
|
|
.filter(Boolean);
|
|
const matchedRule = USER_AUX_RULES.find((rule) => {
|
|
if (rule.exclude && matchByKeywords(normalizedFactor, rule.exclude)) {
|
|
return false;
|
|
}
|
|
if (rule.matchAll) {
|
|
const combined = [normalizedFactor, ...normalizedSensors].join(" ");
|
|
return matchByKeywords(combined, rule.keywords, true);
|
|
}
|
|
const factorMatched = matchByKeywords(normalizedFactor, rule.keywords);
|
|
if (factorMatched) return true;
|
|
return normalizedSensors.some((sensor) =>
|
|
matchByKeywords(sensor, rule.keywords),
|
|
);
|
|
});
|
|
if (!matchedRule) return [];
|
|
return matchedRule.auxiliaries.map((aux) => ({
|
|
...aux,
|
|
ruleId: matchedRule.id,
|
|
}));
|
|
};
|
|
|
|
const appendUserProvidedAuxConfig = (factorList = [], options = {}) => {
|
|
const { includeAuxiliaries = true, includeConfig = true } = options;
|
|
if (!Array.isArray(factorList)) return factorList;
|
|
return factorList.map((factor) => {
|
|
const factorName = factor?.factor_name || factor?.name || "";
|
|
const sensors = Array.isArray(factor?.sensors) ? factor.sensors : [];
|
|
const required = getUserProvidedAuxiliaries(factorName, sensors);
|
|
if (!required.length) return factor;
|
|
const nextAux = Array.isArray(factor?.auxiliary_materials)
|
|
? [...factor.auxiliary_materials]
|
|
: [];
|
|
const nextConfig = Array.isArray(factor?.auxiliary_material_config)
|
|
? [...factor.auxiliary_material_config]
|
|
: [];
|
|
const auxKeySet = new Set(
|
|
(includeAuxiliaries ? nextAux : [])
|
|
.map((aux) => {
|
|
const nameKey = normalizeText(aux?.name || "");
|
|
const modelKey = normalizeText(aux?.model || "");
|
|
return nameKey ? `${nameKey}|${modelKey}` : "";
|
|
})
|
|
.filter(Boolean),
|
|
);
|
|
const configKeySet = new Set(
|
|
(includeConfig ? nextConfig : [])
|
|
.map((item) => normalizeText(item?.name || ""))
|
|
.filter(Boolean),
|
|
);
|
|
required.forEach((aux) => {
|
|
const nameKey = normalizeText(aux?.name || "");
|
|
const modelKey = normalizeText(aux?.model || "");
|
|
const auxKey = nameKey ? `${nameKey}|${modelKey}` : "";
|
|
if (includeAuxiliaries && auxKey && !auxKeySet.has(auxKey)) {
|
|
nextAux.push({
|
|
name: aux.name,
|
|
model: aux.model || "",
|
|
count: null,
|
|
unit: aux.unit || "",
|
|
type: aux.note || "",
|
|
});
|
|
auxKeySet.add(auxKey);
|
|
}
|
|
if (includeConfig && nameKey && !configKeySet.has(nameKey)) {
|
|
nextConfig.push({ name: aux.name, value: null });
|
|
configKeySet.add(nameKey);
|
|
}
|
|
});
|
|
return {
|
|
...factor,
|
|
auxiliary_materials: includeAuxiliaries
|
|
? nextAux
|
|
: factor?.auxiliary_materials,
|
|
auxiliary_material_config: includeConfig
|
|
? nextConfig
|
|
: factor?.auxiliary_material_config,
|
|
};
|
|
});
|
|
};
|
|
|
|
const stripAuxiliaryMaterials = (factorList = []) => {
|
|
if (!Array.isArray(factorList)) return factorList;
|
|
return factorList.map((factor) => {
|
|
if (!factor || typeof factor !== "object") return factor;
|
|
const { auxiliary_materials, ...rest } = factor;
|
|
return rest;
|
|
});
|
|
};
|
|
|
|
const mergeAuxConfig = (existing = [], required = []) => {
|
|
const next = Array.isArray(existing) ? [...existing] : [];
|
|
const existingMap = new Map();
|
|
next.forEach((item, index) => {
|
|
const key = normalizeText(item?.name || "");
|
|
if (key) existingMap.set(key, { item, index });
|
|
});
|
|
required.forEach((item) => {
|
|
const key = normalizeText(item?.name || "");
|
|
if (!key) return;
|
|
if (existingMap.has(key)) {
|
|
const { item: prevItem, index } = existingMap.get(key);
|
|
if (
|
|
prevItem?.value === null ||
|
|
prevItem?.value === undefined ||
|
|
prevItem?.value === ""
|
|
) {
|
|
next[index] = { ...prevItem, value: item.value };
|
|
}
|
|
} else {
|
|
next.push({ name: item.name, value: item.value });
|
|
}
|
|
});
|
|
return next;
|
|
};
|
|
|
|
const ensureResumeFixedFactors = (factorList = []) => {
|
|
if (!Array.isArray(factorList)) return factorList;
|
|
const next = [...factorList];
|
|
const indexMap = new Map();
|
|
next.forEach((factor, index) => {
|
|
const key = normalizeText(factor?.factor_name || factor?.name || "");
|
|
if (key) indexMap.set(key, index);
|
|
});
|
|
RESUME_FIXED_FACTORS.forEach((fixed) => {
|
|
const key = normalizeText(fixed.factor_name);
|
|
if (!key) return;
|
|
if (indexMap.has(key)) {
|
|
const idx = indexMap.get(key);
|
|
const current = next[idx] || {};
|
|
const mergedConfig = mergeAuxConfig(
|
|
current?.auxiliary_material_config,
|
|
fixed.auxiliary_material_config,
|
|
);
|
|
next[idx] = {
|
|
...current,
|
|
factor_name: current?.factor_name || fixed.factor_name,
|
|
auxiliary_material_config: mergedConfig,
|
|
};
|
|
} else {
|
|
next.push({
|
|
factor_name: fixed.factor_name,
|
|
sensors: [],
|
|
auxiliary_material_config: fixed.auxiliary_material_config,
|
|
});
|
|
}
|
|
});
|
|
return next;
|
|
};
|
|
|
|
const applyResumeRules = (factorList = [], resumeData = {}) => {
|
|
if (!Array.isArray(factorList) || !resumeData) return factorList;
|
|
const resumeFactors =
|
|
resumeData?.factor_devices_list ||
|
|
resumeData?.monitoring_device_list ||
|
|
resumeData?.content ||
|
|
[];
|
|
if (!Array.isArray(resumeFactors) || !resumeFactors.length)
|
|
return factorList;
|
|
const resumeMap = new Map();
|
|
resumeFactors.forEach((factor) => {
|
|
const name = String(factor?.factor_name || "").trim();
|
|
if (!name) return;
|
|
const auxConfig = Array.isArray(factor?.auxiliary_material_config)
|
|
? factor.auxiliary_material_config
|
|
: [];
|
|
const auxMap = new Map();
|
|
auxConfig.forEach((item) => {
|
|
const auxName = String(item?.name || "").trim();
|
|
if (!auxName) return;
|
|
const value = parseCountValue(item?.value);
|
|
if (value == null) return;
|
|
auxMap.set(auxName, value);
|
|
});
|
|
resumeMap.set(name, {
|
|
auxMap,
|
|
});
|
|
});
|
|
return factorList.map((factor) => {
|
|
const name = String(factor?.factor_name || "").trim();
|
|
if (!name || !resumeMap.has(name)) return factor;
|
|
const meta = resumeMap.get(name) || {};
|
|
const auxMap = meta.auxMap || new Map();
|
|
const nextFactor = { ...factor };
|
|
const sensorName = factor?.sensors?.[0]?.target_name || "";
|
|
if (Array.isArray(factor?.auxiliary_materials)) {
|
|
const auxMaterials = factor.auxiliary_materials.map((aux) => {
|
|
const auxName = String(aux?.name || "").trim();
|
|
if (auxMap.has(auxName)) {
|
|
return { ...aux, count: auxMap.get(auxName) };
|
|
}
|
|
return aux;
|
|
});
|
|
if (sensorName === "静力水准仪") {
|
|
let fluid = null;
|
|
let water = null;
|
|
auxMap.forEach((value, key) => {
|
|
if (key.includes("储液罐") || key.includes("压差水箱")) {
|
|
fluid = value;
|
|
}
|
|
if (key.includes("连接水管")) {
|
|
water = value;
|
|
}
|
|
});
|
|
if (water == null && fluid != null) {
|
|
water = fluid;
|
|
}
|
|
if (water != null) {
|
|
const safeFluid = fluid != null ? fluid : water;
|
|
nextFactor.auxiliary_materials = auxMaterials.map((aux) => {
|
|
const auxName = String(aux?.name || "");
|
|
if (auxName.includes("导气管")) {
|
|
return { ...aux, count: water * 2 };
|
|
}
|
|
if (auxName.includes("防冻液")) {
|
|
return {
|
|
...aux,
|
|
count: Math.ceil(0.012 * water + safeFluid),
|
|
};
|
|
}
|
|
if (auxName.includes("桥架")) {
|
|
return { ...aux, count: water };
|
|
}
|
|
return aux;
|
|
});
|
|
} else {
|
|
nextFactor.auxiliary_materials = auxMaterials;
|
|
}
|
|
} else {
|
|
nextFactor.auxiliary_materials = auxMaterials;
|
|
}
|
|
}
|
|
return nextFactor;
|
|
});
|
|
};
|
|
|
|
const getGeneratorErrorMessage = (error, fallback = "识别服务返回异常") => {
|
|
if (!error) return fallback;
|
|
if (typeof error === "string") return error;
|
|
const payload = error?.response?.body;
|
|
if (payload?.error) return payload.error;
|
|
if (payload?.message) return payload.message;
|
|
if (error?.message) return error.message;
|
|
return fallback;
|
|
};
|
|
|
|
const getFailStatus = (mode = "generate") =>
|
|
mode === "resume" ? "fail_resume" : "fail_generate";
|
|
|
|
const canRetryScheme = (status) =>
|
|
["fail_generate", "fail_resume", "fail"].includes(status);
|
|
|
|
const markSchemeFail = async (
|
|
ctx,
|
|
schemeId,
|
|
errorMessage,
|
|
mode = "generate",
|
|
) => {
|
|
if (!schemeId) return;
|
|
const { models } = ctx.app.fs.dc;
|
|
await models.SchemeListV2.update(
|
|
{
|
|
status: getFailStatus(mode),
|
|
errorMessage,
|
|
updateAt: new Date(),
|
|
},
|
|
{ where: { id: schemeId } },
|
|
);
|
|
};
|
|
|
|
const requestSchemeListGenerator = async (ctx, payload) => {
|
|
const apiUrl =
|
|
ctx.app.fs.config.schemeListGenerator?.apiUrl || "http://localhost:8001";
|
|
try {
|
|
const res = await superagent
|
|
.post(`${apiUrl}/api/v1/chat`)
|
|
.send(payload)
|
|
.set("Content-Type", "application/json");
|
|
return res?.body || {};
|
|
} catch (error) {
|
|
const status = error?.status || error?.response?.status;
|
|
if (status === 504) {
|
|
throw "项企数据同步中,请稍后再试";
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const requestSchemeListResume = async (ctx, payload) => {
|
|
const apiUrl =
|
|
ctx.app.fs.config.schemeListGenerator?.apiUrl || "http://localhost:8001";
|
|
try {
|
|
const res = await superagent
|
|
.post(`${apiUrl}/api/v1/resume`)
|
|
.send(payload)
|
|
.set("Content-Type", "application/json");
|
|
return res?.body || {};
|
|
} catch (error) {
|
|
const status = error?.status || error?.response?.status;
|
|
if (status === 504) {
|
|
throw "项企数据同步中,请稍后再试";
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 功能:上报方案清单生成业务调用。
|
|
* 使用场景:AI 生成或补充资料生成成功写库后,记录稳定版方案清单应用调用。
|
|
*
|
|
* 入参:ctx、schemeId、userId、mode。
|
|
*
|
|
* 返回:无。
|
|
*
|
|
* 注意:上报失败不能影响方案生成结果。
|
|
*/
|
|
const reportSchemeListBusinessCall = async (
|
|
ctx,
|
|
{ schemeId, userId, mode },
|
|
) => {
|
|
if (!schemeId) return;
|
|
try {
|
|
await reportBusinessCall({
|
|
ctx,
|
|
applicationId: STABLE_LIST_APPLICATION_ID,
|
|
eventId: `stable-list:${schemeId}:${mode}:${Date.now()}`,
|
|
userId,
|
|
traceId: `stable-list:${schemeId}`,
|
|
reportContext: { schemeId, mode },
|
|
});
|
|
} catch (error) {
|
|
ctx.logger?.warn?.(
|
|
`[schemeListV2] 方案清单上报失败,schemeId:${schemeId},mode:${mode}`,
|
|
error,
|
|
);
|
|
}
|
|
};
|
|
|
|
const groupMaterialsByFactor = (rows = []) => {
|
|
const map = new Map();
|
|
rows.forEach((row) => {
|
|
const factor = row.factor || "";
|
|
if (!map.has(factor)) {
|
|
map.set(factor, []);
|
|
}
|
|
map.get(factor).push(row);
|
|
});
|
|
return Array.from(map.entries()).map(([factor, options]) => ({
|
|
factor,
|
|
options,
|
|
}));
|
|
};
|
|
|
|
const normalizeFactorName = (value) => String(value || "").trim();
|
|
const structureTypeMap = {
|
|
bridge: "桥梁",
|
|
tunnel: "隧道",
|
|
slope: "边坡",
|
|
};
|
|
|
|
const normalizeStructureType = (value) =>
|
|
structureTypeMap[value] || value || "";
|
|
|
|
const normalizeTransportMode = (value) => {
|
|
const text = String(value || "").trim();
|
|
if (!text) return "";
|
|
if (text.includes("无线") || text.includes("wireless")) return "无线";
|
|
if (text.includes("有线") || text.includes("wired")) return "有线";
|
|
return text;
|
|
};
|
|
|
|
const buildGeneratorMessage = (message) => String(message || "").trim();
|
|
|
|
const buildRegionName = (region) => {
|
|
if (!region) return "";
|
|
const { prov, city, dist } = region;
|
|
return [prov, city, dist].filter(Boolean).join("");
|
|
};
|
|
|
|
const guessRegionType = (regionName = "") => {
|
|
const northKeys = [
|
|
"北京",
|
|
"天津",
|
|
"河北",
|
|
"山西",
|
|
"内蒙古",
|
|
"辽宁",
|
|
"吉林",
|
|
"黑龙江",
|
|
"山东",
|
|
"河南",
|
|
"陕西",
|
|
"甘肃",
|
|
"青海",
|
|
"宁夏",
|
|
"新疆",
|
|
];
|
|
const matched = northKeys.some((key) => regionName.includes(key));
|
|
return matched ? "北方地区" : "南方平原";
|
|
};
|
|
|
|
const toNumber = (value) => {
|
|
const num = Number(value);
|
|
return Number.isFinite(num) ? num : 0;
|
|
};
|
|
|
|
const flattenSchemeItems = (tableData = []) => {
|
|
const items = [];
|
|
(tableData || []).forEach((group) => {
|
|
(group?.items || []).forEach((item) => {
|
|
const qty = toNumber(item?.qty);
|
|
if (!item?.name && !item?.model) return;
|
|
if (item?.selected === false) return;
|
|
items.push({
|
|
name: String(item?.name || ""),
|
|
model: String(item?.model || ""),
|
|
qty,
|
|
});
|
|
});
|
|
});
|
|
return items;
|
|
};
|
|
|
|
const buildSolarRows = ({
|
|
items = [],
|
|
regionName = "",
|
|
rainyDays = 0,
|
|
materials = [],
|
|
}) => {
|
|
const request4Params = {
|
|
拉线位移传感器: 0,
|
|
静力水准仪: 0,
|
|
裂缝计: 0,
|
|
盒式固定测斜仪: 0,
|
|
温湿度传感器: 0,
|
|
超声波风速风向仪: 0,
|
|
多通道振弦采集仪: 0,
|
|
数据采集系统: 0,
|
|
"磁通量采集系统v1.0": 0,
|
|
云振动采集仪: 0,
|
|
振动采集仪: 0,
|
|
GPRS无线模块: 0,
|
|
光纤收发器1310: 0,
|
|
光纤收发器1550: 0,
|
|
称重系统平台: 0,
|
|
串口服务器: 0,
|
|
工业级交换机: 0,
|
|
光电挠度仪: 0,
|
|
红外标靶: 0,
|
|
无线路由器: 0,
|
|
直线位移传感器: 0,
|
|
"4G网络球机": 0,
|
|
硬盘录像机: 0,
|
|
测地型GNSS接收机: 0,
|
|
};
|
|
items.forEach((sensor) => {
|
|
const name = String(sensor?.name || "");
|
|
const model = String(sensor?.model || "");
|
|
const qty = toNumber(sensor?.qty);
|
|
Object.keys(request4Params).forEach((key) => {
|
|
if (name === key) {
|
|
request4Params[key] += qty;
|
|
}
|
|
if (
|
|
key === "光纤收发器1310" &&
|
|
name === "光纤收发器" &&
|
|
model === "1310"
|
|
) {
|
|
request4Params[key] += qty;
|
|
}
|
|
if (
|
|
key === "光纤收发器1550" &&
|
|
name === "光纤收发器" &&
|
|
model === "1550"
|
|
) {
|
|
request4Params[key] += qty;
|
|
}
|
|
});
|
|
});
|
|
const regionType = guessRegionType(regionName);
|
|
const batterySafetyFactorMap = {
|
|
北方地区: 1.1,
|
|
南方平原: 1.1,
|
|
南方山区: 1.4,
|
|
};
|
|
const tempCorrectionFactorMap = {
|
|
北方地区: 1.2,
|
|
南方平原: 1.0,
|
|
南方山区: 1.1,
|
|
};
|
|
const batterySafetyFactor = batterySafetyFactorMap[regionType] || 1.1;
|
|
const tempCorrectionFactor = tempCorrectionFactorMap[regionType] || 1.0;
|
|
const deviceList = [
|
|
{
|
|
name: "拉线位移传感器",
|
|
model: "FS-LXWY-Z-500",
|
|
power: 0.24,
|
|
key: "拉线位移传感器",
|
|
},
|
|
{
|
|
name: "静力水准仪",
|
|
model: "FS-JLSZ-20-V1.00",
|
|
power: 0.24,
|
|
key: "静力水准仪",
|
|
},
|
|
{ name: "裂缝计", model: "FS-LF10-Z", power: 0.24, key: "裂缝计" },
|
|
{
|
|
name: "盒式固定测斜仪",
|
|
model: "FS-HGC01-V2.00",
|
|
power: 0.6,
|
|
key: "盒式固定测斜仪",
|
|
},
|
|
{
|
|
name: "温湿度传感器",
|
|
model: "FS-BDS-WSD",
|
|
power: 0.24,
|
|
key: "温湿度传感器",
|
|
},
|
|
{
|
|
name: "超声波风速风向仪",
|
|
model: "FS-FSFXY-S",
|
|
power: 0.144,
|
|
key: "超声波风速风向仪",
|
|
},
|
|
{
|
|
name: "多通道振弦采集仪",
|
|
model: "FS-F08",
|
|
power: 2.56,
|
|
key: "多通道振弦采集仪",
|
|
},
|
|
{
|
|
name: "数据采集系统V1.0",
|
|
model: "FS-D04",
|
|
power: 1.7,
|
|
key: "数据采集系统",
|
|
},
|
|
{
|
|
name: "磁通量采集系统v1.0",
|
|
model: "FS-CTL",
|
|
power: 52,
|
|
key: "磁通量采集系统v1.0",
|
|
},
|
|
{
|
|
name: "云振动采集仪",
|
|
model: "FS-iZD08",
|
|
power: 18,
|
|
key: "云振动采集仪",
|
|
},
|
|
{ name: "振动采集仪", model: "FS-ZD08", power: 17, key: "振动采集仪" },
|
|
{
|
|
name: "GPRS无线模块",
|
|
model: "FS-DTU-4G-W-V1.00",
|
|
power: 0.96,
|
|
key: "GPRS无线模块",
|
|
},
|
|
{ name: "光纤收发器", model: "1310", power: 6, key: "光纤收发器1310" },
|
|
{ name: "光纤收发器", model: "1550", power: 6, key: "光纤收发器1550" },
|
|
{ name: "称重系统平台", model: "", power: 30, key: "称重系统平台" },
|
|
{
|
|
name: "串口服务器",
|
|
model: "1D(RS485/232)",
|
|
power: 0.84,
|
|
key: "串口服务器",
|
|
},
|
|
{ name: "工业级交换机", model: "", power: 2.5, key: "工业级交换机" },
|
|
{
|
|
name: "光电挠度仪",
|
|
model: "FS-GDND-125-V1.0",
|
|
power: 6,
|
|
key: "光电挠度仪",
|
|
},
|
|
{ name: "红外标靶", model: "FS-HWBB-6", power: 15, key: "红外标靶" },
|
|
{ name: "无线路由器", model: "工业级", power: 1.2, key: "无线路由器" },
|
|
{
|
|
name: "直线位移传感器",
|
|
model: "FS-ZWY-500",
|
|
power: 1.92,
|
|
key: "直线位移传感器",
|
|
},
|
|
{ name: "4G网络球机", model: "3寸 400万", power: 15, key: "4G网络球机" },
|
|
{ name: "硬盘录像机", model: "4路1盘位", power: 18, key: "硬盘录像机" },
|
|
{
|
|
name: "测地型GNSS接收机",
|
|
model: "A300",
|
|
power: 0,
|
|
key: "测地型GNSS接收机",
|
|
},
|
|
];
|
|
let totalPower = 0;
|
|
deviceList.forEach((device) => {
|
|
const qty = toNumber(request4Params[device.key]);
|
|
totalPower += Number((device.power * qty).toFixed(6));
|
|
});
|
|
const days = Math.max(0, toNumber(rainyDays));
|
|
const dailyConsumption = totalPower * 24;
|
|
const averageDailyConsumption = totalPower * 2;
|
|
const batteryCapacity =
|
|
(batterySafetyFactor *
|
|
averageDailyConsumption *
|
|
days *
|
|
tempCorrectionFactor) /
|
|
0.7;
|
|
let batteryCount = Math.ceil(batteryCapacity / 120);
|
|
const singlePanelPower = 200;
|
|
const sunlightHours = 4;
|
|
const singlePanelDailySupply = singlePanelPower * sunlightHours * 0.63;
|
|
const deviceDailyConsumption = dailyConsumption / 0.9;
|
|
let solarPanelCount = Math.ceil(
|
|
deviceDailyConsumption / singlePanelDailySupply,
|
|
);
|
|
const gnssCount = toNumber(request4Params["测地型GNSS接收机"]);
|
|
if (gnssCount > 0) {
|
|
solarPanelCount += gnssCount;
|
|
batteryCount += 2 * gnssCount;
|
|
}
|
|
const unitOverride = {
|
|
"多通道振弦采集仪|FS-F08": "台(8口)",
|
|
"数据采集系统V1.0|FS-D04": "台(4口)",
|
|
"云振动采集仪|FS-iZD08": "台(8口)",
|
|
"振动采集仪|FS-ZD08": "台(8口)",
|
|
"磁通量采集系统v1.0|FS-CTL": "台",
|
|
};
|
|
const getUnit = (name, model) => {
|
|
const overrideKey = `${name}|${model || ""}`;
|
|
if (unitOverride[overrideKey]) return unitOverride[overrideKey];
|
|
let m = materials.find(
|
|
(it) => it.name === name && (model ? it.model === model : true),
|
|
);
|
|
if (!m && model) m = materials.find((it) => it.model === model);
|
|
if (!m) m = materials.find((it) => it.name === name);
|
|
return m?.unit || "";
|
|
};
|
|
const powerRows = deviceList.map((device, index) => {
|
|
const qty = toNumber(request4Params[device.key]);
|
|
const rowPower = Number(device.power || 0);
|
|
const rowTotalPower = Number((rowPower * qty).toFixed(6));
|
|
return {
|
|
id: `power-${index + 1}`,
|
|
device: device.name,
|
|
model: device.model,
|
|
unit: getUnit(device.name, device.model),
|
|
power: String(rowPower),
|
|
qty: String(qty),
|
|
totalPower: String(rowTotalPower),
|
|
};
|
|
});
|
|
const summaryRows = [
|
|
{
|
|
id: "solar-1",
|
|
power: String(Number(totalPower.toFixed(6))),
|
|
totalPower: String(Number(totalPower.toFixed(6))),
|
|
daily: String(Number(dailyConsumption.toFixed(6))),
|
|
rainy: String(days),
|
|
safety: String(batterySafetyFactor),
|
|
battery: String(Number(batteryCapacity.toFixed(6))),
|
|
batteryCount: String(batteryCount),
|
|
sunshine: String(sunlightHours),
|
|
panel: String(solarPanelCount),
|
|
},
|
|
];
|
|
return { summaryRows, powerRows };
|
|
};
|
|
|
|
const buildDurationRows = (items = []) => {
|
|
const inputQtyMap = {};
|
|
items.forEach((item) => {
|
|
const name = String(item?.name || "").trim();
|
|
const qty = toNumber(item?.qty);
|
|
if (!name) return;
|
|
inputQtyMap[name] = (inputQtyMap[name] || 0) + qty;
|
|
});
|
|
const totalCountMap = { ...inputQtyMap };
|
|
const cableSum = Object.entries(totalCountMap).reduce((s, [k, v]) => {
|
|
return (
|
|
s +
|
|
(String(k).includes("通信电缆") || String(k).includes("接地电缆")
|
|
? toNumber(v)
|
|
: 0)
|
|
);
|
|
}, 0);
|
|
const PVCNum = toNumber(totalCountMap["PVC管"] || 0);
|
|
const inputQtyPatch = {
|
|
GNSS接收机: toNumber(totalCountMap["测地型GNSS接收机"] || 0),
|
|
"4G球机": toNumber(totalCountMap["4G网络球机"] || 0),
|
|
"称重系统(1套2车道)": toNumber(totalCountMap["两车道(全套)"] || 0),
|
|
太阳能供电: toNumber(totalCountMap["太阳能控制器"] || 0),
|
|
"线缆(h/100米)": toNumber(cableSum || 0),
|
|
"桥架1(h/100米)": toNumber(totalCountMap["桥架"] || 0),
|
|
"PVC管1(h/100米)": toNumber(PVCNum || 0),
|
|
};
|
|
Object.keys(inputQtyPatch).forEach((key) => {
|
|
inputQtyMap[key] = inputQtyPatch[key];
|
|
});
|
|
const sensorsForDebug = [
|
|
"表面式应变计",
|
|
"内埋式应变计",
|
|
"温度传感器",
|
|
"温湿度传感器",
|
|
"盒式固定测斜仪",
|
|
"裂缝计",
|
|
"静力水准仪",
|
|
"拉线位移传感器",
|
|
"超声波风速风向仪",
|
|
"光电挠度仪",
|
|
"GNSS接收机",
|
|
"4G球机",
|
|
];
|
|
const sensorTotal = sensorsForDebug.reduce(
|
|
(sum, k) => sum + toNumber(inputQtyMap[k] || 0),
|
|
0,
|
|
);
|
|
const systemDebugDays = sensorTotal <= 50 ? 2 : sensorTotal <= 100 ? 3 : 5;
|
|
const acceptanceTrainingDays = sensorTotal <= 100 ? 3 : 5;
|
|
const projectRiskDays = sensorTotal <= 100 ? 2 : 3;
|
|
const orderedItems = [
|
|
"表面式应变计",
|
|
"内埋式应变计",
|
|
"钢筋计",
|
|
"土压力计",
|
|
"孔隙水压计",
|
|
"轴力计",
|
|
"锚索计",
|
|
"温度传感器",
|
|
"温湿度传感器",
|
|
"土壤温湿度传感器",
|
|
"闭环磁通量标定",
|
|
"开环磁通量绕线(小)",
|
|
"开环磁通量绕线(中)",
|
|
"开环磁通量绕线(大)",
|
|
"人工磁通量温补",
|
|
"盒式固定测斜仪",
|
|
"串联式固定测斜仪",
|
|
"裂缝计",
|
|
"静力水准仪",
|
|
"物位计",
|
|
"雷达液位计",
|
|
"拉线位移传感器",
|
|
"多点位移计(振弦式)",
|
|
"加速度计",
|
|
"激光测距仪",
|
|
"投入式水位计",
|
|
"超声波水位计",
|
|
"雨量计",
|
|
"超声波风速风向仪",
|
|
"光电挠度仪",
|
|
"全站仪",
|
|
"钢尺水位计",
|
|
"一体化裂缝计(成品)",
|
|
"一体化地灾(加速度计)",
|
|
"一体化地灾倾角计",
|
|
"GNSS接收机",
|
|
"4G球机",
|
|
"管式含水率仪",
|
|
"分布式节点",
|
|
"称重系统(1套2车道)",
|
|
"拼接屏(4*6)",
|
|
"太阳能供电",
|
|
"监控中心(3天)",
|
|
"墩座1",
|
|
"墩座2",
|
|
"墩座3",
|
|
"墩座4",
|
|
"基础1(具备挖机、吊车)",
|
|
"基础2",
|
|
"基础3",
|
|
"基础4",
|
|
"地网",
|
|
"线缆(h/100米)",
|
|
"桥架1(h/100米)",
|
|
"桥架2(h/100米)",
|
|
"PVC管1(h/100米)",
|
|
"PVC管2(h/100米)",
|
|
"系统调试",
|
|
"外包(天)",
|
|
"验收、培训",
|
|
"项目风险天数(设备转场、开路、技术难点等)",
|
|
"项目经理(天)",
|
|
];
|
|
const installQtyMap = {
|
|
表面式应变计: 8,
|
|
内埋式应变计: 8,
|
|
钢筋计: 8,
|
|
土压力计: 6.4,
|
|
孔隙水压计: 8,
|
|
轴力计: 2,
|
|
锚索计: 2,
|
|
温度传感器: 8,
|
|
温湿度传感器: 4,
|
|
土壤温湿度传感器: 4,
|
|
闭环磁通量标定: 8,
|
|
"开环磁通量绕线(小)": 3,
|
|
"开环磁通量绕线(中)": 1.6,
|
|
"开环磁通量绕线(大)": 0.7,
|
|
人工磁通量温补: 5.3,
|
|
盒式固定测斜仪: 5.3,
|
|
串联式固定测斜仪: 6,
|
|
裂缝计: 5.3,
|
|
静力水准仪: 6.4,
|
|
物位计: 6.4,
|
|
雷达液位计: 2,
|
|
拉线位移传感器: 3.2,
|
|
"多点位移计(振弦式)": 2.7,
|
|
加速度计: 5.3,
|
|
激光测距仪: 5,
|
|
投入式水位计: 1.5,
|
|
超声波水位计: 2,
|
|
雨量计: 2,
|
|
超声波风速风向仪: 2.5,
|
|
光电挠度仪: 0.7,
|
|
全站仪: 48,
|
|
钢尺水位计: 8,
|
|
GNSS接收机: 18,
|
|
"4G球机": 8,
|
|
分布式节点: 4,
|
|
"称重系统(1套2车道)": 64,
|
|
"拼接屏(4*6)": 48,
|
|
太阳能供电: 16,
|
|
"监控中心(3天)": 5,
|
|
墩座1: 4,
|
|
墩座2: 4,
|
|
墩座3: 6,
|
|
墩座4: 8,
|
|
"基础1(具备挖机、吊车)": 24,
|
|
基础2: 6,
|
|
基础3: 6,
|
|
基础4: 8,
|
|
地网: 16,
|
|
"线缆(h/100米)": 1.9,
|
|
"桥架1(h/100米)": 12,
|
|
"桥架2(h/100米)": 16,
|
|
"PVC管1(h/100米)": 3,
|
|
"PVC管2(h/100米)": 8,
|
|
};
|
|
const hoursPerPersonMap = {
|
|
表面式应变计: "2",
|
|
内埋式应变计: "2",
|
|
钢筋计: "2",
|
|
土压力计: "2.5",
|
|
孔隙水压计: "2",
|
|
轴力计: "8",
|
|
锚索计: "8",
|
|
温度传感器: "2",
|
|
温湿度传感器: "4",
|
|
土壤温湿度传感器: "4",
|
|
闭环磁通量标定: "2",
|
|
"开环磁通量绕线(小)": "4",
|
|
"开环磁通量绕线(中)": "10",
|
|
"开环磁通量绕线(大)": "24",
|
|
人工磁通量温补: "3",
|
|
盒式固定测斜仪: "3",
|
|
串联式固定测斜仪: "2",
|
|
裂缝计: "3",
|
|
静力水准仪: "2.5",
|
|
拉线位移传感器: "5",
|
|
"多点位移计(振弦式)": "6",
|
|
振动传感器: "3",
|
|
激光测距仪: "3",
|
|
投入式水位计: "12",
|
|
超声波水位计: "8",
|
|
雨量计: "8",
|
|
超声波风速风向仪: "6",
|
|
光电挠度仪: "24",
|
|
全站仪: "48",
|
|
钢尺水位计: "8",
|
|
GNSS接收机: "18",
|
|
"4G球机": "8",
|
|
分布式节点: "4",
|
|
"称重系统(1套2车道)": "64",
|
|
"拼接屏(4*6)": "48",
|
|
太阳能供电: "16",
|
|
"监控中心(3天)": "5",
|
|
墩座1: "4",
|
|
墩座2: "4",
|
|
墩座3: "6",
|
|
墩座4: "8",
|
|
"基础1(具备挖机、吊车)": "24",
|
|
基础2: "6",
|
|
基础3: "6",
|
|
基础4: "8",
|
|
地网: "16",
|
|
"线缆(h/100米)": "1.9",
|
|
"桥架1(h/100米)": "12",
|
|
"桥架2(h/100米)": "16",
|
|
"PVC管1(h/100米)": "3",
|
|
"PVC管2(h/100米)": "8",
|
|
};
|
|
const remarksMap = {
|
|
土压力计: "根据挖槽深度、难度",
|
|
轴力计: "含加预应力过程时间",
|
|
锚索计: "含张拉过程时间",
|
|
闭环磁通量标定: "需另外加上集成调试时间,固定传感器时间",
|
|
"开环磁通量绕线(小)": "内径50以下",
|
|
"开环磁通量绕线(中)": "内径50-100左右",
|
|
"开环磁通量绕线(大)": "内径100以上",
|
|
人工磁通量温补: "距离近可转场按照1天6个计算,不需要外包",
|
|
串联式固定测斜仪: "单位:串",
|
|
"多点位移计(振弦式)": "含打孔注浆",
|
|
光电挠度仪: "需要参考标靶数量",
|
|
全站仪: "全站仪安装3天计(含基础及电脑),含棱镜",
|
|
钢尺水位计: "含混凝土立柱",
|
|
GNSS接收机: "含基础浇筑",
|
|
"4G球机": "混凝土凝固,折返时间",
|
|
"监控中心(3天)": "监控中心整体考虑2人3天",
|
|
墩座1: "雨量计:300*300*250mm",
|
|
墩座2: "测斜保护墩:250*250*200mm",
|
|
墩座3: "采集箱底座:600*300*200mm",
|
|
墩座4: "GNSS混凝土墩:1800*315mm(高*直径)",
|
|
"基础1(具备挖机、吊车)": "12米横臂拍立杆:1800*1800*2000mm",
|
|
基础2: "视频立杆:300*300*800mm",
|
|
基础3: "雷达液位计立杆基础:300*300*800mm",
|
|
基础4: "GNSS钢立柱基础:600*600*800mm",
|
|
地网: "标准防雷地网(土质)",
|
|
"线缆(h/100米)": "需要机械,线缆及水管气管",
|
|
"桥架1(h/100米)": "不需机械脚手架",
|
|
"桥架2(h/100米)": "需机械脚手架",
|
|
"PVC管1(h/100米)": "不需机械脚手架",
|
|
"PVC管2(h/100米)": "需机械脚手架",
|
|
系统调试:
|
|
"传感器≤50,按2天;100≥传感器数量>50,按4天;传感器数量>100,按5天",
|
|
"验收、培训": "传感器≤100,按3天;传感器数量>100,按4天",
|
|
"项目风险天数(设备转场、开路、技术难点等)":
|
|
"传感器≤100,按2天;传感器数量>100,按5天",
|
|
};
|
|
const getCategory = (name) => {
|
|
const sensorSet = new Set([
|
|
"表面式应变计",
|
|
"内埋式应变计",
|
|
"钢筋计",
|
|
"土压力计",
|
|
"孔隙水压计",
|
|
"轴力计",
|
|
"锚索计",
|
|
"温度传感器",
|
|
"温湿度传感器",
|
|
"土壤温湿度传感器",
|
|
"闭环磁通量标定",
|
|
"开环磁通量绕线(小)",
|
|
"开环磁通量绕线(中)",
|
|
"开环磁通量绕线(大)",
|
|
"人工磁通量温补",
|
|
"盒式固定测斜仪",
|
|
"串联式固定测斜仪",
|
|
"裂缝计",
|
|
"静力水准仪",
|
|
"物位计",
|
|
"雷达液位计",
|
|
"拉线位移传感器",
|
|
"多点位移计(振弦式)",
|
|
"加速度计",
|
|
"激光测距仪",
|
|
"投入式水位计",
|
|
"超声波水位计",
|
|
"雨量计",
|
|
"超声波风速风向仪",
|
|
"光电挠度仪",
|
|
"全站仪",
|
|
"钢尺水位计",
|
|
"一体化裂缝计(成品)",
|
|
"一体化地灾(加速度计)",
|
|
"一体化地灾倾角计",
|
|
"振动传感器",
|
|
"GNSS接收机",
|
|
"4G球机",
|
|
]);
|
|
const deviceSet = new Set([
|
|
"分布式节点",
|
|
"称重系统(1套2车道)",
|
|
"拼接屏(4*6)",
|
|
"太阳能供电",
|
|
"监控中心(3天)",
|
|
]);
|
|
const civilSet = new Set([
|
|
"墩座1",
|
|
"墩座2",
|
|
"墩座3",
|
|
"墩座4",
|
|
"基础1(具备挖机、吊车)",
|
|
"基础2",
|
|
"基础3",
|
|
"基础4",
|
|
"地网",
|
|
]);
|
|
if (sensorSet.has(name)) return "传感器类";
|
|
if (deviceSet.has(name)) return "设备类";
|
|
if (civilSet.has(name)) return "土建类(地笼、地网、基座、立杆、熔纤)";
|
|
if (name.endsWith("(h/100米)") || name === "系统调试") return "集成";
|
|
if (name === "外包(天)") return "外包(天)";
|
|
if (name === "验收、培训") return "验收及培训";
|
|
if (name === "项目风险天数(设备转场、开路、技术难点等)")
|
|
return "风险天数";
|
|
if (name === "项目经理(天)") return "项目经理(天)";
|
|
return "其他类";
|
|
};
|
|
const getInstallDisplay = (name, installQty, qty) => {
|
|
if (name === "地网") return "1";
|
|
if (name === "系统调试") return "2-5天";
|
|
if (name === "验收、培训") return "2-4天";
|
|
if (name === "项目风险天数(设备转场、开路、技术难点等)") return "2-5天";
|
|
if (name === "外包(天)" || name === "项目经理(天)") return "";
|
|
return String(installQty ?? "");
|
|
};
|
|
const computeDuration = (name, installQty, qty) => {
|
|
if (name === "地网") return Number(((qty || 0) / 3).toFixed(3));
|
|
if (name.endsWith("(h/100米)")) {
|
|
if (!qty) return 0;
|
|
const installDisplay = installQty || 0 ? (installQty * qty) / 100 : 0;
|
|
if (!installDisplay) return 0;
|
|
return Number(((qty || 0) / installDisplay).toFixed(3));
|
|
}
|
|
if (name === "监控中心(3天)") return 3;
|
|
if (name === "系统调试") return Number(systemDebugDays.toFixed(3));
|
|
if (name === "验收、培训")
|
|
return Number(acceptanceTrainingDays.toFixed(3));
|
|
if (name === "项目风险天数(设备转场、开路、技术难点等)")
|
|
return Number(projectRiskDays.toFixed(3));
|
|
if (name === "外包(天)") return 0;
|
|
if (!installQty) return 0;
|
|
return Number(((qty || 0) / installQty).toFixed(3));
|
|
};
|
|
const rows = [];
|
|
for (let i = 0; i < orderedItems.length; i++) {
|
|
const name = orderedItems[i];
|
|
const installQty = installQtyMap[name] ?? 0;
|
|
const qty = inputQtyMap[name] ?? 0;
|
|
const durationVal = computeDuration(name, installQty, qty);
|
|
const displayName =
|
|
name === "外包(天)" || name === "项目经理(天)" ? "" : name;
|
|
const installDisplay = getInstallDisplay(name, installQty, qty);
|
|
rows.push({
|
|
id: `duration-${i + 1}`,
|
|
item: displayName || name,
|
|
hours: hoursPerPersonMap[name] ?? "",
|
|
count: installDisplay,
|
|
note: remarksMap[name] ?? "",
|
|
days: String(durationVal),
|
|
rawQty: qty,
|
|
category: getCategory(name),
|
|
name,
|
|
});
|
|
}
|
|
const includeNames = new Set([
|
|
"系统调试",
|
|
"验收、培训",
|
|
"项目风险天数(设备转场、开路、技术难点等)",
|
|
"外包(天)",
|
|
"项目经理(天)",
|
|
]);
|
|
return rows
|
|
.filter((row) => {
|
|
if (includeNames.has(row.name)) return true;
|
|
return toNumber(row.rawQty) > 0;
|
|
})
|
|
.map(({ name, ...rest }) => rest);
|
|
};
|
|
|
|
const buildCheckRows = ({
|
|
epiboleDays = 0,
|
|
hasSolar = false,
|
|
hasFiber = false,
|
|
}) => {
|
|
const mainPointData = getMainPointData({}, epiboleDays, hasSolar, hasFiber);
|
|
return (mainPointData.rows || []).map((row, index) => {
|
|
const item = row[1] ? `${row[0]}-${row[1]}` : String(row[0] || "");
|
|
return {
|
|
id: `check-${index + 1}`,
|
|
item,
|
|
detail: String(row[2] || ""),
|
|
status: String(row[6] || ""),
|
|
};
|
|
});
|
|
};
|
|
|
|
const buildSchemeExtras = async (ctx, scheme) => {
|
|
const tableData = Array.isArray(scheme?.tableData) ? scheme.tableData : [];
|
|
const items = flattenSchemeItems(tableData);
|
|
const regionName = buildRegionName(scheme?.region);
|
|
const { models } = ctx.app.fs.dc;
|
|
const materials = await models.Materials.findAll({
|
|
raw: true,
|
|
where: { structureType: normalizeStructureType(scheme?.structureType) },
|
|
});
|
|
const solarResult = buildSolarRows({
|
|
items,
|
|
regionName,
|
|
rainyDays: scheme?.rainyDays || 0,
|
|
materials,
|
|
});
|
|
const solarRows = solarResult.summaryRows;
|
|
const solarPowerRows = solarResult.powerRows;
|
|
const durationRows = buildDurationRows(items);
|
|
let hasSolar = false;
|
|
let hasFiber = false;
|
|
items.forEach((item) => {
|
|
const name = String(item?.name || "");
|
|
if (name.includes("太阳能")) hasSolar = true;
|
|
if (name.includes("光纤")) hasFiber = true;
|
|
});
|
|
const epiboleDays = durationRows.reduce(
|
|
(acc, row) => acc + toNumber(row.days),
|
|
0,
|
|
);
|
|
const checkRows = buildCheckRows({ epiboleDays, hasSolar, hasFiber });
|
|
return { solarRows, solarPowerRows, durationRows, checkRows };
|
|
};
|
|
|
|
const parseFactorList = (...inputs) => {
|
|
const list = [];
|
|
inputs.forEach((input) => {
|
|
if (!input) return;
|
|
if (Array.isArray(input)) {
|
|
input.forEach((val) => {
|
|
const factor = normalizeFactorName(val);
|
|
if (factor) list.push(factor);
|
|
});
|
|
return;
|
|
}
|
|
String(input)
|
|
.split(",")
|
|
.forEach((val) => {
|
|
const factor = normalizeFactorName(val);
|
|
if (factor) list.push(factor);
|
|
});
|
|
});
|
|
return Array.from(new Set(list));
|
|
};
|
|
|
|
const stripFactorPrefix = (value) => {
|
|
const text = String(value || "").trim();
|
|
if (!text) return text;
|
|
return text.replace(/^\d+\s*[.、]\s*/g, "");
|
|
};
|
|
|
|
const normalizeTableDataNames = (tableData) => {
|
|
if (!Array.isArray(tableData)) return tableData;
|
|
return tableData.map((group) => ({
|
|
...group,
|
|
name: stripFactorPrefix(group?.name),
|
|
}));
|
|
};
|
|
|
|
const runSchemeGeneration = async (
|
|
ctx,
|
|
schemeId,
|
|
payload,
|
|
mode = "generate",
|
|
) => {
|
|
const { models } = ctx.app.fs.dc;
|
|
try {
|
|
const generatorRes =
|
|
mode === "resume"
|
|
? await requestSchemeListResume(ctx, payload)
|
|
: await requestSchemeListGenerator(ctx, payload);
|
|
if (generatorRes?.type === "normal") {
|
|
const factorList =
|
|
generatorRes?.data?.monitoring_device_list ||
|
|
generatorRes?.data?.content ||
|
|
generatorRes?.data?.data?.content ||
|
|
[];
|
|
const enrichedFactorList =
|
|
mode === "resume"
|
|
? ensureResumeFixedFactors(
|
|
appendUserProvidedAuxConfig(factorList, {
|
|
includeAuxiliaries: true,
|
|
includeConfig: true,
|
|
}),
|
|
)
|
|
: appendUserProvidedAuxConfig(factorList);
|
|
const patchedFactorList =
|
|
mode === "resume"
|
|
? applyResumeRules(enrichedFactorList, payload?.data)
|
|
: enrichedFactorList;
|
|
const nextTableData = buildTableDataFromFactors(patchedFactorList);
|
|
const nextStatus = mode === "resume" ? "success" : "unconfirmed";
|
|
await models.SchemeListV2.update(
|
|
{
|
|
status: nextStatus,
|
|
analysisItems: patchedFactorList,
|
|
tableData: nextTableData,
|
|
updateAt: new Date(),
|
|
errorMessage: null,
|
|
},
|
|
{ where: { id: schemeId } },
|
|
);
|
|
await reportSchemeListBusinessCall(ctx, {
|
|
schemeId,
|
|
userId: payload?.userId || payload?.data?.userId,
|
|
mode,
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (generatorRes?.type === "interrupt") {
|
|
const interruptData = generatorRes?.data;
|
|
const factorList =
|
|
interruptData?.factor_devices_list ||
|
|
interruptData?.monitoring_device_list ||
|
|
interruptData?.content ||
|
|
[];
|
|
const normalizedFactors = Array.isArray(factorList)
|
|
? appendUserProvidedAuxConfig(factorList)
|
|
: [];
|
|
const normalizedSpecs = Array.isArray(interruptData?.sensor_tech_spec)
|
|
? interruptData.sensor_tech_spec
|
|
: [];
|
|
let errorMessage = interruptData || "需要补充资料";
|
|
if (interruptData && typeof interruptData === "object") {
|
|
try {
|
|
errorMessage = JSON.stringify(interruptData);
|
|
} catch (stringifyError) {
|
|
errorMessage = "需要补充资料";
|
|
}
|
|
}
|
|
await models.SchemeListV2.update(
|
|
{
|
|
status: "unresume",
|
|
analysisItems: normalizedFactors,
|
|
analysisSpecs: normalizedSpecs,
|
|
tableData: [],
|
|
errorMessage,
|
|
updateAt: new Date(),
|
|
},
|
|
{ where: { id: schemeId } },
|
|
);
|
|
return;
|
|
}
|
|
|
|
await models.SchemeListV2.update(
|
|
{
|
|
status: getFailStatus(mode),
|
|
errorMessage:
|
|
generatorRes?.error ||
|
|
generatorRes?.data?.error ||
|
|
generatorRes?.data?.message ||
|
|
generatorRes?.data ||
|
|
"识别服务返回异常",
|
|
updateAt: new Date(),
|
|
},
|
|
{ where: { id: schemeId } },
|
|
);
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
await models.SchemeListV2.update(
|
|
{
|
|
status: getFailStatus(mode),
|
|
errorMessage: getGeneratorErrorMessage(error),
|
|
updateAt: new Date(),
|
|
},
|
|
{ where: { id: schemeId } },
|
|
);
|
|
}
|
|
};
|
|
|
|
module.exports.getSchemeListV2 = async (ctx, next) => {
|
|
try {
|
|
const { models, ORM, orm } = ctx.app.fs.dc;
|
|
const Op = ORM?.Op || orm?.Op;
|
|
const { page, pageSize, userId, status, keyword } = ctx.request.query;
|
|
const where = {};
|
|
if (userId) {
|
|
where.userId = userId;
|
|
}
|
|
if (status) {
|
|
where.status = status;
|
|
}
|
|
if (keyword && Op) {
|
|
where.name = { [Op.like]: `%${keyword}%` };
|
|
}
|
|
const queryOptions = {
|
|
where,
|
|
order: [["updateAt", "DESC"]],
|
|
raw: true,
|
|
};
|
|
if (page && pageSize) {
|
|
queryOptions.offset = (page - 1) * pageSize;
|
|
queryOptions.limit = parseInt(pageSize);
|
|
}
|
|
const schemeList =
|
|
await models.SchemeListV2.findAndCountAll(queryOptions);
|
|
if (Array.isArray(schemeList?.rows)) {
|
|
schemeList.rows = schemeList.rows.map((scheme) => ({
|
|
...scheme,
|
|
canRetry: canRetryScheme(scheme?.status),
|
|
}));
|
|
}
|
|
ctx.body = schemeList;
|
|
ctx.status = 200;
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error == "string" ? error : "获取方案清单失败",
|
|
};
|
|
}
|
|
};
|
|
|
|
module.exports.getSchemeDetailV2 = async (ctx, next) => {
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const { schemeId } = ctx.params;
|
|
if (!schemeId) {
|
|
throw "缺少参数";
|
|
}
|
|
const scheme = await models.SchemeListV2.findByPk(schemeId, {
|
|
raw: true,
|
|
});
|
|
if (!scheme) {
|
|
ctx.status = 404;
|
|
ctx.body = { message: "方案不存在" };
|
|
return;
|
|
}
|
|
if (
|
|
scheme.status === "success" &&
|
|
(!Array.isArray(scheme.solarRows) ||
|
|
!Array.isArray(scheme.solarPowerRows) ||
|
|
!Array.isArray(scheme.durationRows) ||
|
|
!Array.isArray(scheme.checkRows))
|
|
) {
|
|
const extras = await buildSchemeExtras(ctx, scheme);
|
|
await models.SchemeListV2.update(
|
|
{
|
|
solarRows: extras.solarRows,
|
|
solarPowerRows: extras.solarPowerRows,
|
|
durationRows: extras.durationRows,
|
|
checkRows: extras.checkRows,
|
|
updateAt: new Date(),
|
|
},
|
|
{ where: { id: schemeId } },
|
|
);
|
|
scheme.solarRows = extras.solarRows;
|
|
scheme.solarPowerRows = extras.solarPowerRows;
|
|
scheme.durationRows = extras.durationRows;
|
|
scheme.checkRows = extras.checkRows;
|
|
}
|
|
ctx.body = {
|
|
...scheme,
|
|
tableData: normalizeTableDataNames(scheme.tableData),
|
|
canRetry: canRetryScheme(scheme?.status),
|
|
};
|
|
ctx.status = 200;
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error == "string" ? error : "获取方案详情失败",
|
|
};
|
|
}
|
|
};
|
|
|
|
module.exports.addSchemeV2 = async (ctx, next) => {
|
|
let createdScheme = null;
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const {
|
|
name,
|
|
userId,
|
|
structureType,
|
|
transmitMethod,
|
|
region,
|
|
extraInfo,
|
|
layoutImgUrls,
|
|
statsImgUrls,
|
|
specImgUrls,
|
|
analysisItems,
|
|
analysisSpecs,
|
|
tableData,
|
|
status,
|
|
onlySave,
|
|
} = ctx.request.body;
|
|
|
|
if (!name) {
|
|
throw "缺少方案名称";
|
|
}
|
|
|
|
const nextStatus = onlySave ? status || "draft" : "generating";
|
|
const scheme = await models.SchemeListV2.create(
|
|
{
|
|
name,
|
|
userId,
|
|
structureType: structureType || "",
|
|
transmitMethod,
|
|
region,
|
|
extraInfo,
|
|
layoutImgUrls,
|
|
statsImgUrls,
|
|
specImgUrls,
|
|
analysisItems,
|
|
analysisSpecs,
|
|
tableData,
|
|
status: nextStatus,
|
|
errorMessage: null,
|
|
},
|
|
{ returning: true },
|
|
);
|
|
createdScheme = scheme;
|
|
|
|
if (onlySave) {
|
|
const schemeDetail = await models.SchemeListV2.findByPk(scheme.id, {
|
|
raw: true,
|
|
});
|
|
ctx.body = {
|
|
scheme: {
|
|
...schemeDetail,
|
|
tableData: normalizeTableDataNames(schemeDetail.tableData),
|
|
},
|
|
generator: { type: "saved" },
|
|
};
|
|
ctx.status = 200;
|
|
return;
|
|
}
|
|
|
|
const transportMode = normalizeTransportMode(transmitMethod);
|
|
const generatorPayload = {
|
|
message: buildGeneratorMessage(extraInfo || name || ""),
|
|
transport_mode: transportMode,
|
|
layout_img_urls: layoutImgUrls || [],
|
|
sensor_stats_img_urls: statsImgUrls || [],
|
|
sensor_spec_img_urls: specImgUrls || [],
|
|
};
|
|
if (userId) {
|
|
generatorPayload.userId = userId;
|
|
}
|
|
|
|
const schemeDetail = await models.SchemeListV2.findByPk(scheme.id, {
|
|
raw: true,
|
|
});
|
|
ctx.body = {
|
|
scheme: {
|
|
...schemeDetail,
|
|
tableData: normalizeTableDataNames(schemeDetail.tableData),
|
|
},
|
|
generator: { type: "generating" },
|
|
};
|
|
ctx.status = 200;
|
|
|
|
setImmediate(() => {
|
|
runSchemeGeneration(ctx, scheme.id, generatorPayload, "generate");
|
|
});
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
if (createdScheme?.id) {
|
|
await markSchemeFail(
|
|
ctx,
|
|
createdScheme.id,
|
|
getGeneratorErrorMessage(error),
|
|
"generate",
|
|
);
|
|
}
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: getGeneratorErrorMessage(error, "新增方案失败"),
|
|
};
|
|
}
|
|
};
|
|
|
|
module.exports.putSchemeV2 = async (ctx, next) => {
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const { schemeId } = ctx.params;
|
|
if (!schemeId) {
|
|
throw "缺少参数";
|
|
}
|
|
|
|
const payload = ctx.request.body || {};
|
|
const { status: _ignoredStatus, ...updates } = payload;
|
|
const shouldSetSuccess = Object.prototype.hasOwnProperty.call(
|
|
updates,
|
|
"tableData",
|
|
);
|
|
if (shouldSetSuccess) {
|
|
updates.status = "success";
|
|
}
|
|
updates.updateAt = new Date();
|
|
|
|
const result = await models.SchemeListV2.update(updates, {
|
|
where: { id: schemeId },
|
|
returning: true,
|
|
});
|
|
let scheme = result?.[1]?.[0]?.dataValues || null;
|
|
if (!scheme) {
|
|
scheme = await models.SchemeListV2.findByPk(schemeId, { raw: true });
|
|
}
|
|
if (scheme && (updates.status === "success" || updates.tableData)) {
|
|
const extras = await buildSchemeExtras(ctx, scheme);
|
|
await models.SchemeListV2.update(
|
|
{
|
|
solarRows: extras.solarRows,
|
|
solarPowerRows: extras.solarPowerRows,
|
|
durationRows: extras.durationRows,
|
|
checkRows: extras.checkRows,
|
|
updateAt: new Date(),
|
|
},
|
|
{ where: { id: schemeId } },
|
|
);
|
|
scheme.solarRows = extras.solarRows;
|
|
scheme.solarPowerRows = extras.solarPowerRows;
|
|
scheme.durationRows = extras.durationRows;
|
|
scheme.checkRows = extras.checkRows;
|
|
}
|
|
ctx.body = { scheme };
|
|
ctx.status = 200;
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error == "string" ? error : "修改方案失败",
|
|
};
|
|
}
|
|
};
|
|
|
|
module.exports.delSchemeV2 = async (ctx, next) => {
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const { schemeId } = ctx.params;
|
|
if (!schemeId) {
|
|
throw "缺少参数";
|
|
}
|
|
await models.SchemeListV2.destroy({ where: { id: schemeId } });
|
|
ctx.status = 200;
|
|
ctx.body = { message: "删除成功" };
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error == "string" ? error : "删除方案失败",
|
|
};
|
|
}
|
|
};
|
|
|
|
module.exports.retryGenerationSchemeV2 = async (ctx, next) => {
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const { schemeId } = ctx.params;
|
|
if (!schemeId) {
|
|
throw "缺少参数";
|
|
}
|
|
const scheme = await models.SchemeListV2.findByPk(schemeId, {
|
|
raw: true,
|
|
});
|
|
if (!scheme) {
|
|
ctx.status = 404;
|
|
ctx.body = { message: "方案不存在" };
|
|
return;
|
|
}
|
|
|
|
await models.SchemeListV2.update(
|
|
{ status: "resumeing", errorMessage: null, updateAt: new Date() },
|
|
{ where: { id: schemeId } },
|
|
);
|
|
|
|
const transportMode = normalizeTransportMode(scheme.transmitMethod);
|
|
const generatorPayload = {
|
|
message: buildGeneratorMessage(scheme.extraInfo || scheme.name || ""),
|
|
transport_mode: transportMode,
|
|
layout_img_urls: scheme.layoutImgUrls || [],
|
|
sensor_stats_img_urls: scheme.statsImgUrls || [],
|
|
sensor_spec_img_urls: scheme.specImgUrls || [],
|
|
};
|
|
if (scheme.userId) {
|
|
generatorPayload.userId = scheme.userId;
|
|
}
|
|
|
|
const schemeDetail = await models.SchemeListV2.findByPk(schemeId, {
|
|
raw: true,
|
|
});
|
|
ctx.body = {
|
|
scheme: {
|
|
...schemeDetail,
|
|
tableData: normalizeTableDataNames(schemeDetail.tableData),
|
|
},
|
|
generator: { type: "generating" },
|
|
};
|
|
ctx.status = 200;
|
|
|
|
setImmediate(() => {
|
|
runSchemeGeneration(ctx, schemeId, generatorPayload, "generate");
|
|
});
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
await markSchemeFail(
|
|
ctx,
|
|
schemeId,
|
|
getGeneratorErrorMessage(error),
|
|
"generate",
|
|
);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: getGeneratorErrorMessage(error, "重新生成方案失败"),
|
|
};
|
|
}
|
|
};
|
|
|
|
module.exports.resumeSchemeV2 = async (ctx, next) => {
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const { schemeId } = ctx.params;
|
|
const { data } = ctx.request.body || {};
|
|
if (!schemeId) {
|
|
throw "缺少参数";
|
|
}
|
|
if (!data) {
|
|
throw "缺少补充资料";
|
|
}
|
|
const scheme = await models.SchemeListV2.findByPk(schemeId, {
|
|
raw: true,
|
|
});
|
|
if (!scheme) {
|
|
ctx.status = 404;
|
|
ctx.body = { message: "方案不存在" };
|
|
return;
|
|
}
|
|
|
|
await models.SchemeListV2.update(
|
|
{ status: "resumeing", errorMessage: null, updateAt: new Date() },
|
|
{ where: { id: schemeId } },
|
|
);
|
|
|
|
const schemeDetail = await models.SchemeListV2.findByPk(schemeId, {
|
|
raw: true,
|
|
});
|
|
ctx.body = {
|
|
scheme: {
|
|
...schemeDetail,
|
|
tableData: normalizeTableDataNames(schemeDetail.tableData),
|
|
},
|
|
generator: { type: "generating" },
|
|
};
|
|
ctx.status = 200;
|
|
|
|
if (data) {
|
|
if (
|
|
Array.isArray(data.factor_devices_list) &&
|
|
Array.isArray(scheme.analysisItems)
|
|
) {
|
|
const analysisMap = new Map();
|
|
scheme.analysisItems.forEach((factor) => {
|
|
const name = String(
|
|
factor?.factor_name || factor?.name || "",
|
|
).trim();
|
|
if (name) analysisMap.set(name, factor);
|
|
});
|
|
|
|
data.factor_devices_list = data.factor_devices_list.map(
|
|
(factor) => {
|
|
const name = String(
|
|
factor?.factor_name || factor?.name || "",
|
|
).trim();
|
|
const originalFactor = analysisMap.get(name);
|
|
if (!originalFactor) return factor;
|
|
|
|
const sensors = Array.isArray(factor.sensors)
|
|
? factor.sensors
|
|
: Array.isArray(factor.items)
|
|
? factor.items.filter((it) => !it.isAux)
|
|
: [];
|
|
const auxItems = Array.isArray(factor.items)
|
|
? factor.items.filter((it) => it.isAux)
|
|
: [];
|
|
|
|
const originalSensors = Array.isArray(originalFactor.sensors)
|
|
? originalFactor.sensors
|
|
: [];
|
|
const originalAux = Array.isArray(
|
|
originalFactor.auxiliary_materials,
|
|
)
|
|
? originalFactor.auxiliary_materials
|
|
: [];
|
|
|
|
const mergedSensors = sensors.map((sensor) => {
|
|
const targetName = sensor.target_name || sensor.name || "";
|
|
const originalSensor = originalSensors.find(
|
|
(s) => (s.target_name || s.source_name) === targetName,
|
|
);
|
|
if (originalSensor) {
|
|
const merged = { ...originalSensor, ...sensor };
|
|
if (sensor.qty !== undefined)
|
|
merged.count = parseCountValue(sensor.qty);
|
|
if (sensor.code !== undefined) merged.erp = sensor.code;
|
|
if (sensor.name !== undefined)
|
|
merged.target_name = sensor.name;
|
|
return merged;
|
|
}
|
|
return sensor;
|
|
});
|
|
|
|
const mergedAux = auxItems.map((aux) => {
|
|
const name = aux.name || "";
|
|
const original = originalAux.find((a) => a.name === name);
|
|
if (original) {
|
|
const merged = { ...original, ...aux };
|
|
if (aux.qty !== undefined)
|
|
merged.count = parseCountValue(aux.qty);
|
|
if (aux.code !== undefined) merged.erp = aux.code;
|
|
return merged;
|
|
}
|
|
return aux;
|
|
});
|
|
|
|
return {
|
|
...originalFactor,
|
|
...factor,
|
|
sensors: mergedSensors,
|
|
auxiliary_materials: mergedAux,
|
|
};
|
|
},
|
|
);
|
|
}
|
|
|
|
if (!data.sensor_tech_spec) {
|
|
data.sensor_tech_spec = [];
|
|
}
|
|
if (!data.transport_mode && scheme.transmitMethod) {
|
|
data.transport_mode = normalizeTransportMode(scheme.transmitMethod);
|
|
}
|
|
if (!data.layout_img_urls && scheme.layoutImgUrls) {
|
|
data.layout_img_urls = scheme.layoutImgUrls;
|
|
}
|
|
if (!data.sensor_stats_img_urls && scheme.statsImgUrls) {
|
|
data.sensor_stats_img_urls = scheme.statsImgUrls;
|
|
}
|
|
if (!data.sensor_spec_img_urls && scheme.specImgUrls) {
|
|
data.sensor_spec_img_urls = scheme.specImgUrls;
|
|
}
|
|
if (!data.message) {
|
|
data.message = buildGeneratorMessage(
|
|
scheme.extraInfo || scheme.name || "",
|
|
);
|
|
}
|
|
if (!data.userId && scheme.userId) {
|
|
data.userId = scheme.userId;
|
|
}
|
|
}
|
|
const payloadUserId = data?.userId || scheme.userId || null;
|
|
setImmediate(() => {
|
|
runSchemeGeneration(
|
|
ctx,
|
|
schemeId,
|
|
payloadUserId ? { data, userId: payloadUserId } : { data },
|
|
"resume",
|
|
);
|
|
});
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
await markSchemeFail(
|
|
ctx,
|
|
schemeId,
|
|
getGeneratorErrorMessage(error),
|
|
"resume",
|
|
);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: getGeneratorErrorMessage(error, "补充资料失败"),
|
|
};
|
|
}
|
|
};
|
|
|
|
module.exports.getMaterialsByFactor = async (ctx, next) => {
|
|
try {
|
|
const { models, ORM, orm } = ctx.app.fs.dc;
|
|
const Op = ORM?.Op || orm?.Op;
|
|
const { factor, factors, structureType } = ctx.request.query;
|
|
const where = {};
|
|
if (structureType) {
|
|
const normalized = normalizeStructureType(structureType);
|
|
const reverseKey = Object.keys(structureTypeMap).find(
|
|
(key) => structureTypeMap[key] === structureType,
|
|
);
|
|
const candidates = Array.from(
|
|
new Set([structureType, normalized, reverseKey].filter(Boolean)),
|
|
);
|
|
if (Op) {
|
|
where.structureType =
|
|
candidates.length > 1 ? { [Op.in]: candidates } : candidates[0];
|
|
} else {
|
|
where.structureType = candidates[0];
|
|
}
|
|
}
|
|
const factorList = parseFactorList(factor, factors);
|
|
if (factorList.length === 1) {
|
|
where.factor = factorList[0];
|
|
} else if (factorList.length > 1 && Op) {
|
|
where.factor = { [Op.in]: factorList };
|
|
} else if (factorList.length > 1) {
|
|
where.factor = factorList[0];
|
|
}
|
|
const rows = await models.Materials.findAll({
|
|
where,
|
|
order: [["id", "ASC"]],
|
|
raw: true,
|
|
});
|
|
ctx.body = groupMaterialsByFactor(rows);
|
|
ctx.status = 200;
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error == "string" ? error : "获取物料失败",
|
|
};
|
|
}
|
|
};
|
|
|
|
module.exports.getMaterialFactors = async (ctx, next) => {
|
|
try {
|
|
const { models, ORM, orm } = ctx.app.fs.dc;
|
|
const Op = ORM?.Op || orm?.Op;
|
|
const { structureType } = ctx.request.query;
|
|
const where = {};
|
|
if (structureType) {
|
|
const normalized = normalizeStructureType(structureType);
|
|
const reverseKey = Object.keys(structureTypeMap).find(
|
|
(key) => structureTypeMap[key] === structureType,
|
|
);
|
|
const candidates = Array.from(
|
|
new Set([structureType, normalized, reverseKey].filter(Boolean)),
|
|
);
|
|
if (Op) {
|
|
where.structureType =
|
|
candidates.length > 1 ? { [Op.in]: candidates } : candidates[0];
|
|
} else {
|
|
where.structureType = candidates[0];
|
|
}
|
|
}
|
|
const rows = await models.Materials.findAll({
|
|
where,
|
|
attributes: ["factor"],
|
|
raw: true,
|
|
});
|
|
const factors = Array.from(
|
|
new Set(
|
|
rows.map((row) => String(row?.factor || "").trim()).filter(Boolean),
|
|
),
|
|
);
|
|
ctx.body = { rows: factors };
|
|
ctx.status = 200;
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error == "string" ? error : "获取监测项失败",
|
|
};
|
|
}
|
|
};
|
|
|
|
// FastGPT: 获取监测因素与设备物料信息(可选携带方案表格)
|
|
module.exports.getFastgptMaterials = async (ctx, next) => {
|
|
try {
|
|
const { models, ORM, orm } = ctx.app.fs.dc;
|
|
const Op = ORM?.Op || orm?.Op;
|
|
const { factor, factors, structureType, schemeId } =
|
|
ctx.request.body || {};
|
|
const where = {};
|
|
|
|
if (structureType) {
|
|
const normalized = normalizeStructureType(structureType);
|
|
const reverseKey = Object.keys(structureTypeMap).find(
|
|
(key) => structureTypeMap[key] === structureType,
|
|
);
|
|
const candidates = Array.from(
|
|
new Set([structureType, normalized, reverseKey].filter(Boolean)),
|
|
);
|
|
if (Op) {
|
|
where.structureType =
|
|
candidates.length > 1 ? { [Op.in]: candidates } : candidates[0];
|
|
} else {
|
|
where.structureType = candidates[0];
|
|
}
|
|
}
|
|
|
|
const factorList = parseFactorList(factor, factors);
|
|
if (factorList.length === 1) {
|
|
where.factor = factorList[0];
|
|
} else if (factorList.length > 1 && Op) {
|
|
where.factor = { [Op.in]: factorList };
|
|
} else if (factorList.length > 1) {
|
|
where.factor = factorList[0];
|
|
}
|
|
|
|
const rows = await models.Materials.findAll({
|
|
where,
|
|
order: [["id", "ASC"]],
|
|
raw: true,
|
|
});
|
|
|
|
const factorRows = Array.from(
|
|
new Set(
|
|
rows.map((row) => String(row?.factor || "").trim()).filter(Boolean),
|
|
),
|
|
);
|
|
|
|
let scheme = null;
|
|
if (schemeId) {
|
|
const schemeDetail = await models.SchemeListV2.findByPk(schemeId, {
|
|
raw: true,
|
|
});
|
|
if (schemeDetail) {
|
|
scheme = {
|
|
id: schemeDetail.id,
|
|
name: schemeDetail.name,
|
|
structureType: schemeDetail.structureType,
|
|
tableData: normalizeTableDataNames(schemeDetail.tableData),
|
|
};
|
|
}
|
|
}
|
|
|
|
ctx.body = {
|
|
structureType: normalizeStructureType(structureType),
|
|
factors: factorRows,
|
|
groups: groupMaterialsByFactor(rows),
|
|
scheme,
|
|
};
|
|
ctx.status = 200;
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error == "string" ? error : "获取监测因素物料失败",
|
|
};
|
|
}
|
|
};
|
|
|
|
// FastGPT: 监测因素与设备的增删改查
|
|
module.exports.postFastgptPlanList = async (ctx, next) => {
|
|
try {
|
|
const { userRequest, equipmentGroups, context, userId } =
|
|
ctx.request.body || {};
|
|
const apiUrl =
|
|
ctx.app.fs.config.schemeListGenerator?.apiUrl ||
|
|
"http://localhost:8001";
|
|
|
|
const taskType = "修改清单";
|
|
const normalizeEquipmentGroups = (groups = []) => {
|
|
if (!Array.isArray(groups)) return [];
|
|
return groups.map((group) => {
|
|
const factorName =
|
|
group?.factor_name || group?.factor || group?.name || "";
|
|
const sensors = [];
|
|
const auxiliary_materials = [];
|
|
const items = Array.isArray(group?.items) ? group.items : [];
|
|
if (
|
|
Array.isArray(group?.sensors) ||
|
|
Array.isArray(group?.auxiliary_materials)
|
|
) {
|
|
return {
|
|
factor_name: factorName,
|
|
sensors: Array.isArray(group?.sensors) ? group.sensors : [],
|
|
auxiliary_materials: Array.isArray(group?.auxiliary_materials)
|
|
? group.auxiliary_materials
|
|
: [],
|
|
};
|
|
}
|
|
items.forEach((item) => {
|
|
if (!item) return;
|
|
const isAux = Boolean(item?.isAux);
|
|
if (isAux) {
|
|
auxiliary_materials.push({
|
|
type: item?.category || item?.note || "",
|
|
name: item?.name || "",
|
|
model: item?.model || "",
|
|
erp: item?.code || item?.erp || "",
|
|
unit: item?.unit || "",
|
|
count: item?.qty ?? item?.count ?? "",
|
|
});
|
|
} else {
|
|
sensors.push({
|
|
count: item?.qty ?? item?.count ?? "",
|
|
source_name: item?.source_name || item?.name || "",
|
|
type: item?.category || item?.type || "传感器单元",
|
|
target_name: item?.target_name || item?.name || "",
|
|
model: item?.model || "",
|
|
erp: item?.code || item?.erp || null,
|
|
unit: item?.unit || "",
|
|
signal_type: item?.signal_type || "",
|
|
});
|
|
}
|
|
});
|
|
return {
|
|
factor_name: factorName,
|
|
sensors,
|
|
auxiliary_materials,
|
|
};
|
|
});
|
|
};
|
|
|
|
const factorList = normalizeEquipmentGroups(equipmentGroups || []);
|
|
const requestText =
|
|
typeof userRequest === "string" && userRequest.trim()
|
|
? userRequest.trim()
|
|
: "";
|
|
let messagePayload = JSON.stringify(factorList || []);
|
|
if (requestText) {
|
|
messagePayload = `${messagePayload}\n\n${requestText}`;
|
|
}
|
|
|
|
const buildPreviewGroups = (factors = []) => {
|
|
if (!Array.isArray(factors)) return [];
|
|
return factors.map((factor, index) => {
|
|
const name =
|
|
factor?.factor_name || factor?.name || `监测因素${index + 1}`;
|
|
const items = [];
|
|
const sensors = Array.isArray(factor?.sensors)
|
|
? factor.sensors
|
|
: [];
|
|
const auxiliaries = Array.isArray(factor?.auxiliary_materials)
|
|
? factor.auxiliary_materials
|
|
: [];
|
|
sensors.forEach((sensor) => {
|
|
items.push({
|
|
category: sensor?.type || "传感器单元",
|
|
name: sensor?.target_name || sensor?.source_name || "",
|
|
model: sensor?.model || "",
|
|
code: sensor?.erp || "",
|
|
unit: sensor?.unit || "",
|
|
qty: sensor?.count ?? "",
|
|
note: sensor?.signal_type || "",
|
|
});
|
|
});
|
|
auxiliaries.forEach((aux) => {
|
|
items.push({
|
|
category: aux?.type || "",
|
|
name: aux?.name || "",
|
|
model: aux?.model || "",
|
|
code: aux?.erp || "",
|
|
unit: aux?.unit || "",
|
|
qty: aux?.count ?? "",
|
|
note: aux?.note || "",
|
|
});
|
|
});
|
|
return { name, items };
|
|
});
|
|
};
|
|
|
|
let res = null;
|
|
let aiPayload = null;
|
|
const requestUserId =
|
|
userId || context?.userId || ctx?.state?.user?.id || null;
|
|
try {
|
|
res = await superagent
|
|
.post(`${apiUrl}/api/v1/chat`)
|
|
.send({
|
|
task_type: taskType,
|
|
message: messagePayload,
|
|
userId: requestUserId || undefined,
|
|
})
|
|
.timeout(1000 * 60 * 10)
|
|
.set({
|
|
"Content-Type": "application/json",
|
|
});
|
|
aiPayload = res?.body || {};
|
|
const aiFactorList = Array.isArray(aiPayload)
|
|
? aiPayload
|
|
: Array.isArray(aiPayload?.data?.monitoring_device_list)
|
|
? aiPayload.data.monitoring_device_list
|
|
: Array.isArray(aiPayload?.factor_devices_list)
|
|
? aiPayload.factor_devices_list
|
|
: Array.isArray(aiPayload?.monitoring_device_list)
|
|
? aiPayload.monitoring_device_list
|
|
: [];
|
|
const preview_groups = buildPreviewGroups(aiFactorList);
|
|
ctx.body = {
|
|
content: {
|
|
preview_groups,
|
|
raw: aiPayload,
|
|
},
|
|
raw: aiPayload,
|
|
};
|
|
ctx.status = 200;
|
|
} catch (apiError) {
|
|
ctx.logger.log(apiError);
|
|
const aiFactorList = [];
|
|
const preview_groups = buildPreviewGroups(aiFactorList);
|
|
const fallbackRaw =
|
|
aiPayload ||
|
|
apiError?.response?.body ||
|
|
apiError?.message ||
|
|
"ai助手调用失败";
|
|
ctx.body = {
|
|
content: {
|
|
preview_groups,
|
|
raw: fallbackRaw,
|
|
},
|
|
raw: fallbackRaw,
|
|
};
|
|
ctx.status = 200;
|
|
}
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error === "string" ? error : "ai助手调用失败",
|
|
};
|
|
}
|
|
};
|
|
|
|
const buildSheetDataFromTableData = (tableData = []) => {
|
|
const sheet1Key = [];
|
|
const sheet1Data = [];
|
|
const stripAuxPrefix = (value) =>
|
|
String(value || "")
|
|
.replace(/^【辅材】/, "")
|
|
.trim();
|
|
(tableData || []).forEach((group) => {
|
|
const factorName = String(group?.name || "").trim();
|
|
if (!factorName) return;
|
|
sheet1Key.push(factorName);
|
|
const data = (group?.items || []).map((item) => ({
|
|
name: stripAuxPrefix(item?.name || ""),
|
|
model: item?.model || "",
|
|
count: toNumber(item?.qty),
|
|
type: item?.category || "",
|
|
unit: item?.unit || "",
|
|
code: item?.code || "",
|
|
price: item?.price != null ? Number(item.price) : undefined,
|
|
lineLength: item?.lineLength || "",
|
|
note: item?.note || item?.description || "",
|
|
}));
|
|
sheet1Data.push({ factor: factorName, data });
|
|
});
|
|
return { sheet1Key, sheet1Data };
|
|
};
|
|
|
|
const buildSolarEnergyData = ({
|
|
items = [],
|
|
materials = [],
|
|
regionName = "",
|
|
rainyDays = 0,
|
|
}) => {
|
|
const solarResult = buildSolarRows({
|
|
items,
|
|
regionName,
|
|
rainyDays,
|
|
materials,
|
|
});
|
|
const powerHeaders = [
|
|
"序号",
|
|
"设备名称",
|
|
"默认型号",
|
|
"单位",
|
|
"功率(W)",
|
|
"数量",
|
|
"总功率(W)",
|
|
];
|
|
const powerRows = (solarResult.powerRows || []).map((row, index) => [
|
|
String(index + 1),
|
|
row.device || "",
|
|
row.model || "",
|
|
row.unit || "",
|
|
row.power || "",
|
|
row.qty || "",
|
|
row.totalPower || "",
|
|
]);
|
|
const solarHeaders = [
|
|
"总功率",
|
|
"日耗电量",
|
|
"日平均耗电量",
|
|
"连续阴雨天",
|
|
"蓄电池容量安全系数",
|
|
"温度修正系数",
|
|
"电池容量",
|
|
"电池数量",
|
|
"单块太阳能功率",
|
|
"日平均光照时间",
|
|
"设备单日耗电量",
|
|
"太阳能板单日供电量",
|
|
"太阳能板数量",
|
|
];
|
|
const summary = (solarResult.summaryRows || [])[0] || {};
|
|
const totalPower = summary.totalPower || summary.power || "0";
|
|
const daily = summary.daily || "0";
|
|
const averageDaily = String(Number(totalPower) * 2 || 0);
|
|
const batterySafety = summary.safety || "1.1";
|
|
const tempFactor =
|
|
guessRegionType(regionName) === "北方地区" ? "1.2" : "1.0";
|
|
const battery = summary.battery || "0";
|
|
const batteryCount = summary.batteryCount || "0";
|
|
const singlePanelPower = "200";
|
|
const sunshineHours = summary.sunshine || "4";
|
|
const deviceDaily = String(Number(daily) / 0.9 || 0);
|
|
const singlePanelDaily = String(
|
|
Number(singlePanelPower) * Number(sunshineHours) * 0.63 || 0,
|
|
);
|
|
const panelCount = summary.panel || "0";
|
|
const solarEnergy = {
|
|
solarEnergyTitle: guessRegionType(regionName),
|
|
solarEnergyData: {
|
|
headers: solarHeaders,
|
|
rows: [
|
|
[
|
|
String(totalPower),
|
|
String(daily),
|
|
String(Number(averageDaily)),
|
|
String(rainyDays || 0),
|
|
String(batterySafety),
|
|
String(tempFactor),
|
|
String(battery),
|
|
String(batteryCount),
|
|
String(singlePanelPower),
|
|
String(sunshineHours),
|
|
String(deviceDaily),
|
|
String(singlePanelDaily),
|
|
String(panelCount),
|
|
],
|
|
],
|
|
},
|
|
powerTitle: "功率统计表",
|
|
powerData: { headers: powerHeaders, rows: powerRows },
|
|
};
|
|
return solarEnergy;
|
|
};
|
|
|
|
const buildDurationData = (items = []) => {
|
|
const headers6 = [
|
|
"序号",
|
|
"分类",
|
|
"事项",
|
|
"耗时(小时/人)",
|
|
"安装数量/个(2人8小时计)",
|
|
"备注",
|
|
"数量输入",
|
|
"工期(2人天)",
|
|
];
|
|
const durationRows = buildDurationRows(items);
|
|
const rows6 = durationRows.map((row, index) => [
|
|
String(index + 1),
|
|
row.category || "",
|
|
row.item || "",
|
|
row.hours || "",
|
|
row.count || "",
|
|
row.note || "",
|
|
row.rawQty != null ? String(row.rawQty) : row.count || "",
|
|
row.days || "",
|
|
]);
|
|
const epiboleDays = rows6.reduce((acc, row) => acc + toNumber(row[7]), 0);
|
|
const managerHoursPerDay = 8;
|
|
const projectManagerDays = Number(
|
|
((epiboleDays * 8) / (managerHoursPerDay || 8)).toFixed(6),
|
|
);
|
|
return {
|
|
durationData: { headers: headers6, rows: rows6 },
|
|
epiboleDays,
|
|
projectManagerDays,
|
|
};
|
|
};
|
|
|
|
module.exports.exportSchemeV2 = async (ctx, next) => {
|
|
try {
|
|
const { models } = ctx.app.fs.dc;
|
|
const schemeId = ctx.params.schemeId;
|
|
if (!schemeId) {
|
|
throw "缺少参数";
|
|
}
|
|
const scheme = await models.SchemeListV2.findByPk(schemeId, {
|
|
raw: true,
|
|
});
|
|
if (!scheme) {
|
|
throw "方案不存在";
|
|
}
|
|
if (scheme.status !== "success") {
|
|
throw "方案未完成";
|
|
}
|
|
if (
|
|
!Array.isArray(scheme.solarRows) ||
|
|
!Array.isArray(scheme.solarPowerRows) ||
|
|
!Array.isArray(scheme.durationRows) ||
|
|
!Array.isArray(scheme.checkRows)
|
|
) {
|
|
const extras = await buildSchemeExtras(ctx, scheme);
|
|
await models.SchemeListV2.update(
|
|
{
|
|
solarRows: extras.solarRows,
|
|
solarPowerRows: extras.solarPowerRows,
|
|
durationRows: extras.durationRows,
|
|
checkRows: extras.checkRows,
|
|
updateAt: new Date(),
|
|
},
|
|
{ where: { id: schemeId } },
|
|
);
|
|
scheme.solarRows = extras.solarRows;
|
|
scheme.solarPowerRows = extras.solarPowerRows;
|
|
scheme.durationRows = extras.durationRows;
|
|
scheme.checkRows = extras.checkRows;
|
|
}
|
|
|
|
const { sheet1Data, sheet1Key } = buildSheetDataFromTableData(
|
|
scheme.tableData || [],
|
|
);
|
|
const materials = await models.Materials.findAll({
|
|
raw: true,
|
|
where: { structureType: normalizeStructureType(scheme.structureType) },
|
|
});
|
|
const items = flattenSchemeItems(scheme.tableData || []);
|
|
const solarEnergy = buildSolarEnergyData({
|
|
items,
|
|
materials,
|
|
regionName: buildRegionName(scheme.region),
|
|
rainyDays: scheme?.rainyDays || 0,
|
|
});
|
|
const durationPayload = buildDurationData(items);
|
|
let hasSolar = false;
|
|
let hasFiber = false;
|
|
items.forEach((item) => {
|
|
const name = String(item?.name || "");
|
|
if (name.includes("太阳能")) hasSolar = true;
|
|
if (name.includes("光纤")) hasFiber = true;
|
|
});
|
|
const mainPointData = getMainPointData(
|
|
{},
|
|
durationPayload.epiboleDays,
|
|
hasSolar,
|
|
hasFiber,
|
|
);
|
|
const tableData = generationXlsxData({
|
|
materials,
|
|
sheet1Data,
|
|
sheet1Key,
|
|
solarEnergy,
|
|
duration: durationPayload,
|
|
mainPointData,
|
|
});
|
|
|
|
const wb = XLSXS.utils.book_new();
|
|
for (let sheetData of tableData) {
|
|
const ws = XLSXS.utils.aoa_to_sheet(sheetData.data);
|
|
XLSXS.utils.book_append_sheet(wb, ws, sheetData.name);
|
|
if (sheetData.merges && sheetData.merges.length > 0) {
|
|
ws["!merges"] = sheetData.merges;
|
|
}
|
|
if (sheetData.cols && sheetData.cols.length > 0) {
|
|
ws["!cols"] = sheetData.cols;
|
|
}
|
|
}
|
|
|
|
const xlsxBuffer = XLSXS.write(wb, { type: "buffer", bookType: "xlsx" });
|
|
const xlsxFileName = `${scheme.name}.xlsx`;
|
|
ctx.set(
|
|
"Content-Type",
|
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
);
|
|
ctx.set(
|
|
"Content-Disposition",
|
|
`attachment; filename="${encodeURIComponent(xlsxFileName)}"`,
|
|
);
|
|
ctx.set("Content-Length", xlsxBuffer.length);
|
|
ctx.body = xlsxBuffer;
|
|
ctx.status = 200;
|
|
} catch (error) {
|
|
ctx.logger.log(error);
|
|
ctx.status = 400;
|
|
ctx.body = {
|
|
message: typeof error == "string" ? error : "导出方案失败",
|
|
};
|
|
}
|
|
};
|
|
|