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