ai-query对接新版freesun-agent接口的分支
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

"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 : "导出方案失败",
};
}
};