'use strict'; const superagent = require('superagent'); const XLSXS = require('xlsx-js-style'); const moment = require('moment'); const { markdownTableToArray } = require('../utils/mdUtils'); const { bridgePrompts, tunnelPrompts, slopePrompts } = require('../static/factorPrompts'); module.exports.getSchemeList = async (ctx, next) => { try { const { models } = ctx.app.fs.dc; const { page, pageSize, userId } = ctx.request.query; const where = {}; if (userId) { where.userId = userId; } if (page && pageSize) { where.offset = (page - 1) * pageSize; where.limit = parseInt(pageSize); } const schemeList = await models.SchemeList.findAndCountAll({ attributes: ['id', 'name', 'userText', 'status', 'createAt', 'updateAt', 'structureType', 'transmitMethod'], where, order: [['updateAt', 'DESC']], raw: true, }); ctx.body = schemeList; ctx.status = 200; } catch (error) { ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error == 'string' ? error : '获取方案清单失败' }; } } module.exports.addScheme = async (ctx, next) => { try { const { models } = ctx.app.fs.dc; const { name, userText, userId, structureType, transmitMethod, curIp } = ctx.request.body; if (!name || !userText || !structureType || !transmitMethod) { throw '缺少参数' }; // 检查是否缺少必要数据 if (Object.keys(userText).length === 0) { throw '至少选择一个监测项'; } // 新增数据,状态pending const scheme = await models.SchemeList.create( { name, userText, status: 'pending', userId, structureType, transmitMethod }, { returning: true } ); await models.AiQueryRecord.create({ userId: userId, ipAddress: curIp, clientId: 'pep-feixiaoshang', feature: '方案清单生成', time: moment(), }); ctx.body = scheme; ctx.status = 200; // 后台执行方案内容生成,不影响此接口返回 generationScheme(ctx, scheme.id, userText, structureType, transmitMethod); } catch (error) { ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error == 'string' ? error : '新增方案失败' }; } } module.exports.retryGenerationScheme = async (ctx, next) => { const transaction = await ctx.app.fs.dc.orm.transaction(); try { const { models } = ctx.app.fs.dc; const id = ctx.params.schemeId; if (!id) { throw '缺少参数' }; const scheme = await models.SchemeList.update( { status: 'pending' }, { where: { id }, returning: true, transaction } ); const { userText, structureType } = scheme[1][0].dataValues; ctx.body = scheme; ctx.status = 200; transaction.commit(); generationScheme(ctx, id, userText, structureType); } catch (error) { transaction.rollback(); ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error == 'string' ? error : '重新生成方案失败' }; } } // 调用AI生成方案 ---> 硬代码生成方案 async function generationScheme(ctx, schemeId, userText, structureType, transmitMethod) { switch (structureType) { case '桥梁': generationBridgeScheme(ctx, schemeId, userText, structureType, transmitMethod); break; case '隧道': generationTunnelScheme(ctx, schemeId, userText, structureType, transmitMethod); break; case '边坡': generationSlopeScheme(ctx, schemeId, userText, structureType, transmitMethod); break; default: console.error(`未知结构类型:${structureType}`); try { const { models } = ctx.app.fs.dc; await models.SchemeList.update( { status: 'fail' }, { where: { id: schemeId } } ); } catch (err) { console.log(`修改状态失败,scheme.id=${schemeId}`); ctx.logger.log(err); } break; } } // 生成桥梁方案清单 async function generationBridgeScheme(ctx, schemeId, userText, structureType, transmitMethod) { const transaction = await ctx.app.fs.dc.orm.transaction(); const { models } = ctx.app.fs.dc; try { // const { schemeAppKey, apiUrl } = ctx.app.fs.config.fastGpt; const { apiUrl } = ctx.app.fs.config.fastGpt; const schemeAppKey = 'fastgpt-faESFm8XQk7ffnhtEUewoVoXIo16xCfmRrkToru6g2A86zEZPy22LSC8a4sg' const regionAppKey = 'fastgpt-kzqnfXUusbIP6R42we1KMAST9SqBAPCtbAxTV1oQHWRuM7CEb1am2VF7z0' /********** 1.请求数据 **********/ const allRequestKey = Object.keys(userText); const request1Key = allRequestKey.filter(key => !['机箱配套', '通信系统', '电力系统(太阳能)'].includes(key)); async function readManifestFromFile(fileUrl) { try { const res = await superagent .get(fileUrl) .buffer(true) .parse((response, cb) => { const data = []; response.on('data', (chunk) => { data.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); }); response.on('end', () => cb(null, Buffer.concat(data))); response.on('error', (err) => cb(err)); }); const buf = res.body; const wb = XLSXS.read(buf, { type: 'buffer' }); const sheetNames = wb.SheetNames || []; if (!sheetNames.length) return []; const all = []; for (let s = 0; s < sheetNames.length; s++) { const ws = wb.Sheets[sheetNames[s]]; if (!ws) continue; const rows = XLSXS.utils.sheet_to_json(ws, { header: 1, blankrows: false, defval: '' }) || []; let nameIdx = -1, modelIdx = -1, countIdx = -1, startIdx = -1, typeIdx = -1, unitIdx = -1, codeIdx = -1, priceIdx = -1, lineLenIdx = -1; const headerMaxScan = Math.min(rows.length, 20); for (let i = 0; i < headerMaxScan; i++) { const row = rows[i] || []; for (let j = 0; j < row.length; j++) { const cell = String(row[j] || '').replace(/\s+/g, ''); if (nameIdx < 0 && (cell === '名称' || cell.indexOf('设备名称') > -1 || cell.indexOf('物料名称') > -1 || cell.indexOf('品名') > -1)) nameIdx = j; if (modelIdx < 0 && (cell === '规格/型号' || cell.indexOf('规格') > -1 || cell.indexOf('型号') > -1)) modelIdx = j; if (countIdx < 0 && (cell === '数量' || cell.indexOf('数量') > -1 || cell.indexOf('总数') > -1)) countIdx = j; if (typeIdx < 0 && (cell === '类型' || cell.indexOf('类型') > -1)) typeIdx = j; if (unitIdx < 0 && (cell === '单位' || cell.indexOf('单位') > -1)) unitIdx = j; if (codeIdx < 0 && (cell === '物料代码' || cell.indexOf('物料编码') > -1 || cell.indexOf('物料代码') > -1)) codeIdx = j; if (priceIdx < 0 && (cell === '单价' || cell.indexOf('单价') > -1 || cell.indexOf('价格') > -1)) priceIdx = j; if (lineLenIdx < 0 && (cell.indexOf('线长') > -1)) lineLenIdx = j; } if (nameIdx >= 0 && countIdx >= 0) { startIdx = i + 1; break; } } if (startIdx < 0) continue; for (let i = startIdx; i < rows.length; i++) { const row = rows[i] || []; const nameCell = row[nameIdx]; const rawName = nameCell == null ? '' : String(nameCell); const nameStr = rawName.trim(); if (!nameStr) continue; const lower = nameStr.toLowerCase(); if (nameStr.indexOf('小计') > -1 || nameStr.indexOf('合计') > -1 || lower.indexOf('total') > -1) break; const modelStr = modelIdx >= 0 ? String(row[modelIdx] || '').trim() : ''; const countVal = row[countIdx]; const countStr = String(countVal == null ? '' : countVal).trim(); const match = countStr.match(/-?\d+(\.\d+)?/); const countNum = match ? Number(match[0]) : Number(countStr || 0); if (!isNaN(countNum) && countNum > 0) { const typeStr = typeIdx >= 0 ? String(row[typeIdx] || '').trim() : ''; const unitStr = unitIdx >= 0 ? String(row[unitIdx] || '').trim() : ''; const codeStr = codeIdx >= 0 ? String(row[codeIdx] || '').trim() : ''; const priceVal = priceIdx >= 0 ? String(row[priceIdx] || '').trim() : ''; const lineLenVal = lineLenIdx >= 0 ? String(row[lineLenIdx] || '').trim() : ''; all.push({ name: nameStr, model: modelStr, count: countNum, type: typeStr, unit: unitStr, code: codeStr, price: priceVal ? Number(priceVal) : undefined, lineLength: lineLenVal }); } } } return all; } catch (e) { return []; } } function mergeManifest(primary, fallback) { const map = new Map(); const normKey = (n, m) => `${String(n || '').trim().toLowerCase()}|${String(m || '').trim().toLowerCase()}`; for (const it of primary || []) { map.set(normKey(it.name, it.model), { name: String(it.name || '').trim(), model: String(it.model || '').trim(), count: Number(it.count || 0), type: it.type, unit: it.unit, code: it.code, price: it.price, lineLength: it.lineLength }); } for (const it of fallback || []) { const k = normKey(it.name, it.model); if (map.has(k)) { const v = map.get(k); if (!v.type && it.type) v.type = it.type; if (!v.unit && it.unit) v.unit = it.unit; if (!v.code && it.code) v.code = it.code; if (v.price == null && it.price != null) v.price = it.price; if (v.lineLength == null && it.lineLength != null) v.lineLength = it.lineLength; map.set(k, v); } } return Array.from(map.values()); } const request1Data = await Promise.all(request1Key.map(async key => { try { const expectSolarParamKeys = new Set([ '桥墩/桥塔位移监测(北斗)', '视频监控(有线)', '视频监控(无线1)', '机箱配套', ]); const isSolarSelected = !!userText['电力系统(太阳能)']; const params = [].concat( bridgePrompts[key].sensors.map(sensor => { const v = userText[key]?.[sensor]; if (v === undefined && /数量|长度|桥长|车道|天数|弦数/.test(sensor)) return 0; return v; }), [expectSolarParamKeys.has(key) ? isSolarSelected : transmitMethod] ); let data = bridgePrompts[key].getManifest(params) || []; data = data.map(it => ({ ...it, name: String(it.name || '').trim(), model: String(it.model || '').trim() })); const fileUrl = bridgePrompts[key]?.file; if (fileUrl) { const fileData = await readManifestFromFile(fileUrl); data = mergeManifest(data, fileData); } return { factor: key, data: data || [] }; } catch (error) { console.error(`Calculation failed for ${key}`, error); return { factor: key, data: [] }; } })); const groundBlacklist = new Set(['接地桩(镀锌角铁)', '镀锌扁铁', '降阻剂']); for (let i = 0; i < request1Data.length; i++) { const it = request1Data[i]; it.data = (it.data || []).filter(x => !groundBlacklist.has(String(x?.name || ''))); } const opsSelected = String(userText['运维服务']?.['选择子服务'] || '').split('、').map(x => x.trim()).filter(Boolean); for (let i = 0; i < request1Data.length; i++) { const it = request1Data[i]; if (it.factor === '运维服务' && Array.isArray(it.data)) { it.data = opsSelected.length > 0 ? it.data.filter(row => opsSelected.includes(String(row?.name || ''))) : []; } } // 机箱配套 let request2Data = { factor: "机箱配套", data: [] }; let otherInstrumentCount = 0; // 其他模块采集仪总数 let vibratingWireSensorCount = 0; // 振弦信号传感器总数 let signal485SensorCount = 0; // 485信号传感器总数 for (const item of request1Data) { if (item && item.data && item.data.length > 0) { for (const sensor of item.data) { if (sensor.name.includes("采集仪")) { otherInstrumentCount += Number(sensor.count || 0); } if (["表面式应变计", "内埋式应变计", "温度传感器"].includes(sensor.name)) { vibratingWireSensorCount += Number(sensor.count || 0); } if (["储液罐(压差水箱)", "裂缝计", "直线位移传感器", "拉线位移传感器", "盒式固定测斜仪", "温湿度传感器", "超声波风速风向仪"].includes(sensor.name)) { signal485SensorCount += Number(sensor.count || 0); } } } } if (userText['机箱配套']) { try { const isSolar = true; const vw = Number(vibratingWireSensorCount || 0); const explicitOrFallbackCount = (factorKey, itemName, model, fallback) => { const direct = Number(userText[factorKey]?.[itemName]); if (!isNaN(direct) && direct > 0) return direct; const modelKey = `${itemName}|${model || ''}`; const byModel = Number(userText[factorKey]?.[modelKey] || userText[factorKey]?.[model]); if (!isNaN(byModel) && byModel > 0) return byModel; return Number(fallback || 0); }; let items = []; let hubs = []; if (vw > 0) { let chosenName = ''; let chosenModel = ''; let chosenSystemName = ''; let chosenSystemModel = ''; let chosenCount = 0; if (vw <= 2) { chosenName = '单通道振弦采集模块'; chosenModel = 'FS-FD01'; chosenSystemName = '单通道振弦采集系统V1.0'; chosenSystemModel = 'FS-FD01-内置软件'; chosenCount = vw; } else if (vw <= 32) { chosenName = '多通道振弦采集仪'; chosenModel = 'FS-F08'; chosenSystemName = '多通道振弦采集仪原位监测系统V1.0'; chosenSystemModel = 'FS-F08-内置软件'; chosenCount = Math.ceil(vw / 8); } else if (vw <= 64) { chosenName = '多通道振弦采集仪'; chosenModel = 'FS-F16'; chosenSystemName = '多通道振弦采集仪原位监测系统V1.0'; chosenSystemModel = 'FS-F16-内置软件'; chosenCount = Math.ceil(vw / 16); } else { chosenName = '多通道振弦采集仪'; chosenModel = 'FS-F32'; chosenSystemName = '多通道振弦采集仪原位监测系统V1.0'; chosenSystemModel = 'FS-F32-内置软件'; chosenCount = Math.ceil(vw / 32); } const cMain = explicitOrFallbackCount('机箱配套', chosenName, chosenModel, chosenCount); if (cMain > 0) { items.push({ name: chosenName, model: chosenModel, count: cMain }); } const cSys = explicitOrFallbackCount('机箱配套', chosenSystemName, chosenSystemModel, cMain); if (cSys > 0) { items.push({ name: chosenSystemName, model: chosenSystemModel, count: cSys }); } const s485 = Number(signal485SensorCount || 0); if (s485 > 0) { if (s485 <= 8) { const c4 = Math.ceil(s485 / 4); const hc = explicitOrFallbackCount('机箱配套', 'RS485集线器', 'FS-485JXQ-04-A', c4); if (hc > 0) hubs.push({ name: 'RS485集线器', model: 'FS-485JXQ-04-A', count: hc }); } else { const c8 = Math.floor(s485 / 8); const hc8 = explicitOrFallbackCount('机箱配套', 'RS485集线器', 'FS-485JXQ-08-A', c8); if (hc8 > 0) hubs.push({ name: 'RS485集线器', model: 'FS-485JXQ-08-A', count: hc8 }); const r = s485 % 8; if (r > 0) { const c4 = Math.ceil(r / 4); const hc4 = explicitOrFallbackCount('机箱配套', 'RS485集线器', 'FS-485JXQ-04-A', c4); if (hc4 > 0) hubs.push({ name: 'RS485集线器', model: 'FS-485JXQ-04-A', count: hc4 }); } } } const instrumentCountForBreaker = items .filter(x => x.name === '多通道振弦采集仪' || x.name === '单通道振弦采集模块') .reduce((sum, x) => sum + Number(x.count || 0), 0); const basics = [ { name: '断路器', model: 'DZ47-60', count: Number(otherInstrumentCount || 0) + instrumentCountForBreaker }, { name: '12V直流电源防雷器', model: '12V', count: Number(otherInstrumentCount || 0) + instrumentCountForBreaker }, { name: '数据采集箱', model: 'FS-CJX03-A', count: Number(otherInstrumentCount || 0) + instrumentCountForBreaker }, { name: '接地电缆', model: 'BVR 1*16', count: (Number(otherInstrumentCount || 0) + instrumentCountForBreaker) * 10 }, { name: 'GPRS无线模块', model: 'FS-DTU-4G-W-V1.00', count: Number(otherInstrumentCount || 0) + instrumentCountForBreaker }, { name: '物联网卡', model: '20G', count: Number(otherInstrumentCount || 0) + instrumentCountForBreaker }, // { name: '接地桩(镀锌角铁)', model: 'FS-DXJT-2', count: (Number(otherInstrumentCount || 0) + instrumentCountForBreaker) * 3 }, // { name: '镀锌扁铁', model: 'FS-DXBL-40*40', count: (Number(otherInstrumentCount || 0) + instrumentCountForBreaker) * 10 }, // { name: '降阻剂', model: '25kg/袋', count: (Number(otherInstrumentCount || 0) + instrumentCountForBreaker) * 10 }, ]; const mains = isSolar ? [] : [ { name: '电源电涌保护器', model: 'AM2-40', count: Number(otherInstrumentCount || 0) + instrumentCountForBreaker }, { name: '开关电源1', model: '25-12-A', count: Number(otherInstrumentCount || 0) + instrumentCountForBreaker }, { name: '导轨插座', model: '10A', count: Number(otherInstrumentCount || 0) + instrumentCountForBreaker }, { name: '220V交流供电电缆', model: 'RVV 3*2.5', count: (Number(otherInstrumentCount || 0) + instrumentCountForBreaker) * 100 }, ]; const breakerCount = Number(otherInstrumentCount || 0) + instrumentCountForBreaker; request2Data = { factor: "机箱配套", data: breakerCount > 0 ? items.concat(hubs).concat(basics).concat(mains) : [] }; } else { request2Data = { factor: "机箱配套", data: [] }; } } catch (error) { console.error('机箱配套 calculation failed', error); } } let collectingBoxCount = 0; // 采集箱总数 let vibratingWireAcquisitionInstrumentCount = 0; // 振弦采集仪总数 let dataAcquisitionInstrumentCount = otherInstrumentCount; // 数据采集仪总数 let GNSSCount = Number(userText['桥墩/桥塔位移监测(北斗)']?.['测地型GNSS接收机数量'] || 0); // GNSS接收机总数 let PVCNum = Number(userText['通信系统']?.['PVC管长度'] || 0); // PVC管总数 let fiberOpticsLength = Number(userText['通信系统']?.['通信光缆(光纤)长度'] || 0); // 通信光缆(光纤)长度 for (const item of request1Data.concat([request2Data])) { if (item && item.data && item.data.length > 0) { for (const sensor of item.data) { if (sensor.name === "数据采集箱") { collectingBoxCount += Number(sensor.count || 0); } if (sensor.name.includes("振弦采集仪")) { vibratingWireAcquisitionInstrumentCount += Number(sensor.count || 0); } } } } // 通信系统 let request3Data = { factor: "通信系统", data: [] }; if (userText['通信系统']) { try { let data = bridgePrompts['通信系统'].getManifest([collectingBoxCount, vibratingWireAcquisitionInstrumentCount, dataAcquisitionInstrumentCount, GNSSCount, PVCNum, fiberOpticsLength]); request3Data = { factor: "通信系统", data: data || [] }; } catch (error) { console.error('通信系统 calculation failed', error); request3Data = { factor: "通信系统", data: [] }; } } // 太阳能计算 let request5Data = null; let solarEnergy = { solarEnergyTitle: null, solarEnergyData: null, powerTitle: null, powerData: null, }; const materials = await models.Materials.findAll({ raw: true, where: { structureType: structureType } }); if (userText['电力系统(太阳能)']) { 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, } for (const item of request1Data.concat([request2Data, request3Data])) { if (item && item.data && item.data.length > 0) { for (const sensor of item.data) { for (const key in request4Params) { if (sensor.name === key) { request4Params[key] += Number(sensor.count || 0); } if (key === '光纤收发器1310' && sensor.name === '光纤收发器' && sensor.model === '1310') { request4Params[key] += Number(sensor.count || 0); } if (key === '光纤收发器1550' && sensor.name === '光纤收发器' && sensor.model === '1550') { request4Params[key] += Number(sensor.count || 0); } } } } } const regionName = String(userText['电力系统(太阳能)']?.['地区名称'] || ''); const days = Number(userText['电力系统(太阳能)']?.['连续阴雨天数'] || 0) || 0; const region = await superagent .post(`${apiUrl}/api/v1/chat/completions`) .send({ "stream": false, "detail": false, "messages": [ { "role": "user", "content": [ { "type": "text", "text": regionName } ] } ] }) .set({ Authorization: `Bearer ${regionAppKey}`, "Content-Type": "application/json", }) .timeout({ response: 1000 * 60 * 30, // 等待服务器发送数据的超时时间(毫秒) deadline: 1000 * 60 * 30, // 整个请求完成的最大时间(毫秒) }) const regionType = JSON.parse(region?.body?.choices[0]?.message?.content?.trim()).region; const batterySafetyFactorMap = { '北方地区': 1.1, '南方平原': 1.1, '南方山区': 1.4 }; const tempCorrectionFactorMap = { '北方地区': 1.2, '南方平原': 1.0, '南方山区': 1.1 }; const batterySafetyFactor = batterySafetyFactorMap[regionType]; const tempCorrectionFactor = tempCorrectionFactorMap[regionType]; 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接收机' }, ]; solarEnergy.powerTitle = '功率统计表'; const powerHeaders = ['序号', '设备名称', '默认型号', '单位', '功率(W)', '数量', '总功率(W)']; const powerRows = []; 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(m => m.name === name && (model ? m.model === model : true)); if (!m && model) m = materials.find(m => m.model === model); if (!m) m = materials.find(m => m.name === name); return m?.unit || ''; }; let totalPower = 0; for (let i = 0; i < deviceList.length; i++) { const d = deviceList[i]; const qty = Number(request4Params[d.key] || 0); const rowPower = Number(d.power || 0); const rowTotalPower = Number((rowPower * qty).toFixed(6)); totalPower += rowTotalPower; const unit = getUnit(d.name, d.model); powerRows.push([ String(i + 1), d.name, d.model, unit, String(rowPower), String(qty), String(rowTotalPower) ]); } solarEnergy.powerData = { headers: powerHeaders, rows: powerRows }; 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 = Number(request4Params['测地型GNSS接收机'] || 0); if (gnssCount > 0) { solarPanelCount += gnssCount; batteryCount += 2 * gnssCount; } solarEnergy.solarEnergyTitle = regionType; const solarHeaders = ['总功率', '日耗电量', '日平均耗电量', '连续阴雨天', '蓄电池容量安全系数', '温度修正系数', '电池容量', '电池数量', '单块太阳能功率', '日平均光照时间', '设备单日耗电量', '太阳能板单日供电量', '太阳能板数量']; const solarRow = [ // regionName, String(Number(totalPower.toFixed(6))), String(Number(dailyConsumption.toFixed(6))), String(Number(averageDailyConsumption.toFixed(6))), String(days), String(batterySafetyFactor), String(tempCorrectionFactor), String(Number(batteryCapacity.toFixed(6))), String(batteryCount), String(singlePanelPower), String(sunlightHours), String(Number(deviceDailyConsumption.toFixed(6))), String(Number(singlePanelDailySupply.toFixed(6))), String(solarPanelCount) ]; solarEnergy.solarEnergyData = { headers: solarHeaders, rows: [solarRow] }; try { const data = bridgePrompts['电力系统(太阳能)'].getManifest([solarPanelCount, batteryCount]) || []; const controllerLoad = (200 / 18) * solarPanelCount; const controllerModel = controllerLoad < 10 ? '12V10A' : controllerLoad < 30 ? '12V30A' : '12V50A'; const controllerCount = Math.ceil((200 * solarPanelCount) / 900); const controllerDesc = '太阳能控制器:根据太阳能控制器选型计算公式从“12V10A、12V30A、12V50A”中选择一个合适的型号。默认太阳能板功率为200,太阳能控制器选型计算公式:(太阳能板功率/18)*太阳能板数量,根据该公式的计算结果选择对应型号:如果计算结果<10,选择太阳能控制器12V10A;如果计算结果>=10且<30选择太阳能控制器12V30A;如果计算结果>=30选择太阳能控制器12V50A。太阳能控制器数量=太阳能板功率*太阳能板数量/900(向上取整)。'; for (let k = 0; k < data.length; k++) { const it = data[k]; if (String(it?.name) === '太阳能控制器') { it.model = controllerModel; it.count = controllerCount; } } request5Data = { data }; } catch (error) { console.error('电力系统(太阳能) calculation failed', error); } } const allListData = request1Data.concat([request2Data, request3Data]); if (request5Data) { allListData.push(request5Data) } // 工期核算清单 const request6Params = { '表面式应变计': 0, '内埋式应变计': 0, '温度传感器': 0, '温湿度传感器': 0, '盒式固定测斜仪': 0, '裂缝计': 0, '静力水准仪': 0, '拉线位移传感器': 0, '超声波风速风向仪': 0, '光电挠度仪': 0, '测地型GNSS接收机': 0, '4G网络球机': 0, '两车道(全套)': 0, '太阳能控制器': 0, '接地桩(镀锌角铁)': 0, '通信电缆': 0, '接地电缆': 0, '桥架': 0, 'PVC管': 0, } for (const item of allListData) { if (item && item.data && item.data.length > 0) { for (const sensor of item.data) { for (const key in request6Params) { if (sensor.name === key) { request6Params[key] += Number(sensor.count || 0); } } } } } const totalCountMap = {}; for (const item of allListData) { if (item && item.data && item.data.length > 0) { for (const sensor of item.data) { const k = sensor.name; const v = Number(sensor.count || 0); totalCountMap[k] = (totalCountMap[k] || 0) + v; } } } const cableSum = Object.entries(totalCountMap).reduce((s, [k, v]) => s + ((String(k).includes('通信电缆') || String(k).includes('接地电缆')) ? Number(v || 0) : 0), 0); 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, '全站仪': 0.3, '钢尺水位计': 2, '一体化裂缝计(成品)': 2.7, '一体化地灾(加速度计)': 4, '一体化地灾倾角计': 4, 'GNSS接收机': 0.9, '4G球机': 2, '管式含水率仪': 4, '分布式节点': 4, '称重系统(1套2车道)': 0.3, '拼接屏(4*6)': 0.3, '太阳能供电': 1, '监控中心(3天)': 3.2, '墩座1': 4, '墩座2': 4, '墩座3': 2.7, '墩座4': 2, '基础1(具备挖机、吊车)': 0.7, '基础2': 2.7, '基础3': 2.7, '基础4': 2, '地网': 1, '线缆(h/100米)': 8.4, '桥架1(h/100米)': 1.3, '桥架2(h/100米)': 1.3, 'PVC管1(h/100米)': 5.3, 'PVC管2(h/100米)': 5.3 }; // Object.assign(installQtyMap, getHoursPerPerson()); const inputQtyMap = { '表面式应变计': Number(totalCountMap['表面式应变计'] || 0), '内埋式应变计': Number(totalCountMap['内埋式应变计'] || 0), '钢筋计': Number(totalCountMap['钢筋计'] || 0), '土压力计': Number(totalCountMap['土压力计'] || 0), '孔隙水压计': Number(totalCountMap['孔隙水压计'] || 0), '轴力计': Number(totalCountMap['轴力计'] || 0), '锚索计': Number(totalCountMap['锚索计'] || 0), '温度传感器': Number(totalCountMap['温度传感器'] || 0), '温湿度传感器': Number(totalCountMap['温湿度传感器'] || 0), '土壤温湿度传感器': Number(totalCountMap['土壤温湿度传感器'] || 0), '闭环磁通量标定': 0, '开环磁通量绕线(小)': 0, '开环磁通量绕线(中)': 0, '开环磁通量绕线(大)': 0, '人工磁通量温补': 0, '盒式固定测斜仪': Number(totalCountMap['盒式固定测斜仪'] || 0), '串联式固定测斜仪': Number(totalCountMap['串联式固定测斜仪'] || 0), '裂缝计': Number(totalCountMap['裂缝计'] || 0), '静力水准仪': Number(totalCountMap['静力水准仪'] || 0), '物位计': Number(totalCountMap['物位计'] || 0), '雷达液位计': Number(totalCountMap['雷达液位计'] || 0), '拉线位移传感器': Number(totalCountMap['拉线位移传感器'] || 0), '多点位移计(振弦式)': Number(totalCountMap['多点位移计(振弦式)'] || 0), '加速度计': Number(totalCountMap['加速度计'] || 0), '激光测距仪': Number(totalCountMap['激光测距仪'] || 0), '投入式水位计': Number(totalCountMap['投入式水位计'] || 0), '超声波水位计': Number(totalCountMap['超声波水位计'] || 0), '雨量计': Number(totalCountMap['雨量计'] || 0), '超声波风速风向仪': Number(totalCountMap['超声波风速风向仪'] || 0), '光电挠度仪': Number(totalCountMap['光电挠度仪'] || 0), '全站仪': Number(totalCountMap['全站仪'] || 0), '钢尺水位计': Number(totalCountMap['钢尺水位计'] || 0), '一体化裂缝计(成品)': Number(totalCountMap['一体化裂缝计(成品)'] || 0), '一体化地灾(加速度计)': Number(totalCountMap['一体化地灾(加速度计)'] || 0), '一体化地灾倾角计': Number(totalCountMap['一体化地灾倾角计'] || 0), 'GNSS接收机': Number(totalCountMap['测地型GNSS接收机'] || 0), '4G球机': Number(totalCountMap['4G网络球机'] || 0), '管式含水率仪': Number(totalCountMap['管式含水率仪'] || 0), '分布式节点': Number(totalCountMap['分布式节点'] || 0), '称重系统(1套2车道)': Number(totalCountMap['两车道(全套)'] || 0), '拼接屏(4*6)': Number(totalCountMap['拼接屏(4*6)'] || 0), '太阳能供电': Number(totalCountMap['太阳能控制器'] || 0), '监控中心(3天)': 0, '墩座1': Number(totalCountMap['墩座1'] || 0), '墩座2': Number(totalCountMap['墩座2'] || 0), '墩座3': Number(totalCountMap['墩座3'] || 0), '墩座4': Number(totalCountMap['墩座4'] || 0), '基础1(具备挖机、吊车)': Number(totalCountMap['基础1(具备挖机、吊车)'] || 0), '基础2': Number(totalCountMap['基础2'] || 0), '基础3': Number(totalCountMap['基础3'] || 0), '基础4': Number(totalCountMap['基础4'] || 0), '地网': Number(totalCountMap['接地桩(镀锌角铁)'] || 0), '线缆(h/100米)': Number(cableSum || 0), '桥架1(h/100米)': Number(totalCountMap['桥架'] || 0), '桥架2(h/100米)': 0, 'PVC管1(h/100米)': Number(PVCNum || 0), 'PVC管2(h/100米)': 0 }; const sensorsForDebug = [ '表面式应变计', '内埋式应变计', '温度传感器', '温湿度传感器', '盒式固定测斜仪', '裂缝计', '静力水准仪', '拉线位移传感器', '超声波风速风向仪', '光电挠度仪', 'GNSS接收机', '4G球机' ]; const sensorTotal = sensorsForDebug.reduce((sum, k) => sum + Number(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 headers6 = ['序号', '分类', '事项', '耗时(小时/人)', '安装数量/个(2人8小时计)', '备注', '数量输入', '工期(2人天)']; 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米)', '系统调试', '外包(天)', '验收、培训', '项目风险天数(设备转场、开路、技术难点等)', '项目经理(天)' ]; function 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 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', }; function getHoursPerPerson(name) { return hoursPerPersonMap[name] ?? ''; } 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天', }; function getInstallDisplay(name, installQty, qty) { // if (name.endsWith('(h/100米)')) { // const v = Number(((installQty || 0) * (qty || 0) / 100).toFixed(3)); // return String(v); // } 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 ?? ''); } function installRuleText(name) { if (name === '地网') return '安装数量×数量输入÷3'; if (name.endsWith('(h/100米)')) return '安装数量×数量输入÷100'; if (name === '监控中心(3天)') return '固定3天'; if (name === '系统调试') return '总数≤50→2;≤100→3;>100→5'; if (name === '验收、培训') return '总数≤100→3;>100→5'; if (name === '项目风险天数(设备转场、开路、技术难点等)') return '总数≤100→2;>100→3'; return '数量输入÷安装数量'; } function 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 rows6 = []; 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); const qtyStr = (name === '外包(天)' || name === '项目经理(天)') ? '' : String(Number((qty || 0).toFixed(3))); rows6.push([ String(i + 1), getCategory(name), displayName, getHoursPerPerson(name), installDisplay, remarksMap[name] ?? '', qtyStr, String(durationVal) ]); } const epiboleLimitIdx = orderedItems.indexOf('系统调试') + 1; const epiboleDays = rows6.reduce((acc, row) => { const idx = parseInt(row[0], 10); if (isNaN(idx) || idx > epiboleLimitIdx) return acc; return acc + Number(row[row.length - 1] || 0); }, 0); const managerHoursPerDay = Number(userText['工期核算清单']?.['项目经理每日工时'] || 8); const projectManagerDays = Number((((epiboleDays + acceptanceTrainingDays + projectRiskDays) * 8) / (managerHoursPerDay || 8)).toFixed(6)); for (let i = 0; i < rows6.length; i++) { const category = rows6[i][1]; if (category === '外包(天)') { const v = String(Number(epiboleDays.toFixed(3))); rows6[i][6] = ''; rows6[i][7] = v; } else if (category === '项目经理(天)') { const v = String(Number(projectManagerDays.toFixed(3))); rows6[i][6] = ''; rows6[i][7] = v; } } const durationData = { headers: headers6, rows: rows6 }; let hasSolar = false let request7Power = ''; let hasFiber = false; for (const item of allListData) { if (item && item.data && item.data.length > 0) { for (const sensor of item.data) { if (sensor.name.includes('太阳能') && !request7Power) { request7Power = ',太阳能供电'; hasSolar = true; } if (sensor.name.includes('光纤')) { request7Power = ',光纤通信'; hasFiber = true; break; } } } if (hasFiber) break; } const mainPointData = getMainPointData(userText, epiboleDays, hasSolar, hasFiber); let sheet1Data = request1Data; let sheet1Key = request1Key; if (userText['机箱配套']) { sheet1Data.push(request2Data); sheet1Key.push('机箱配套'); } if (userText['通信系统']) { sheet1Data.push(request3Data); sheet1Key.push('通信系统'); } if (request5Data) { sheet1Data.push(request5Data); sheet1Key.push('电力系统(太阳能)'); } /********** 2.生成excel **********/ const tableData = generationXlsxData({ materials, sheet1Data, sheet1Key, solarEnergy: userText['电力系统(太阳能)'] ? solarEnergy : null, duration: { durationData, epiboleDays, projectManagerDays }, mainPointData }); /********** 3.保存数据 **********/ await models.SchemeList.update( { status: 'success', updateAt: new Date(), tableData, }, { where: { id: schemeId } } ); ctx.logger.log(`方案清单数据生成成功,schemeId:${schemeId}`); await transaction.commit(); } catch (error) { console.log('完整的 schemeAppKey:', JSON.stringify(ctx.app.fs.config.fastGpt)); // 或者在环境变量中 console.log('环境变量中的API Key:', process.env.FASTGPT_API_KEY); await transaction.rollback(); ctx.logger.log(error); try { await models.SchemeList.update( { status: 'fail' }, { where: { id: schemeId } } ); } catch (err) { console.log(`修改状态失败,scheme.id=${schemeId}`); ctx.logger.log(err); } } } // 生成隧道方案清单 async function generationTunnelScheme(ctx, schemeId, userText, structureType, transmitMethod) { const transaction = await ctx.app.fs.dc.orm.transaction(); const { models } = ctx.app.fs.dc; try { const { apiUrl } = ctx.app.fs.config.fastGpt; const regionAppKey = 'fastgpt-kzqnfXUusbIP6R42we1KMAST9SqBAPCtbAxTV1oQHWRuM7CEb1am2VF7z0' const allRequestKey = Object.keys(userText); const request1Key = allRequestKey.filter(key => !['机箱配套', '通信系统', '电力系统(太阳能)'].includes(key)); const request1Data = await Promise.all(request1Key.map(async key => { try { const expectSolarParamKeys = new Set([ '桥墩/桥塔位移监测(北斗)', '视频监控(有线)', '视频监控(无线1)', '机箱配套', ]); const isSolarSelected = !!userText['电力系统(太阳能)']; const params = [].concat( tunnelPrompts[key].sensors.map(sensor => { const v = userText[key]?.[sensor]; if (v === undefined && /数量|长度|桥长|车道|天数|弦数/.test(sensor)) return 0; return v; }), [expectSolarParamKeys.has(key) ? isSolarSelected : transmitMethod] ); let data = tunnelPrompts[key].getManifest(params) || []; data = data.map(it => ({ ...it, name: String(it.name || '').trim(), model: String(it.model || '').trim() })); const fileUrl = tunnelPrompts[key]?.file; if (fileUrl) { const fileData = await readManifestFromFile(fileUrl); const map = new Map(); const keyFn = (n, m) => `${String(n || '').trim().toLowerCase()}|${String(m || '').trim().toLowerCase()}`; for (const it of data || []) map.set(keyFn(it.name, it.model), { name: String(it.name || '').trim(), model: String(it.model || '').trim(), count: Number(it.count || 0), type: it.type, unit: it.unit, code: it.code, price: it.price, lineLength: it.lineLength }); for (const it of fileData || []) { const k = keyFn(it.name, it.model); if (map.has(k)) { const v = map.get(k); if (!v.type && it.type) v.type = it.type; if (!v.unit && it.unit) v.unit = it.unit; if (!v.code && it.code) v.code = it.code; if (v.price == null && it.price != null) v.price = it.price; if (v.lineLength == null && it.lineLength != null) v.lineLength = it.lineLength; map.set(k, v); } } data = Array.from(map.values()); } return { factor: key, data: data || [] }; } catch (error) { console.error(`Calculation failed for ${key}`, error); return { factor: key, data: [] }; } })); const opsSelected2 = String(userText['运维服务']?.['选择子服务'] || '').split('、').map(x => x.trim()).filter(Boolean); for (let i = 0; i < request1Data.length; i++) { const it = request1Data[i]; if (it.factor === '运维服务' && Array.isArray(it.data)) { it.data = opsSelected2.length > 0 ? it.data.filter(row => opsSelected2.includes(String(row?.name || ''))) : []; } } async function readManifestFromFile(fileUrl) { try { const res = await superagent .get(fileUrl) .buffer(true) .parse((response, cb) => { const data = []; response.on('data', (chunk) => { data.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); }); response.on('end', () => cb(null, Buffer.concat(data))); response.on('error', (err) => cb(err)); }); const buf = res.body; const wb = XLSXS.read(buf, { type: 'buffer' }); const sheetNames = wb.SheetNames || []; if (!sheetNames.length) return []; const all = []; for (let s = 0; s < sheetNames.length; s++) { const ws = wb.Sheets[sheetNames[s]]; if (!ws) continue; const rows = XLSXS.utils.sheet_to_json(ws, { header: 1, blankrows: false, defval: '' }) || []; let nameIdx = -1, modelIdx = -1, countIdx = -1, startIdx = -1, typeIdx = -1, unitIdx = -1, codeIdx = -1, priceIdx = -1, lineLenIdx = -1; const headerMaxScan = Math.min(rows.length, 20); for (let i = 0; i < headerMaxScan; i++) { const row = rows[i] || []; for (let j = 0; j < row.length; j++) { const cell = String(row[j] || '').replace(/\s+/g, ''); if (nameIdx < 0 && (cell === '名称' || cell.indexOf('设备名称') > -1 || cell.indexOf('物料名称') > -1 || cell.indexOf('品名') > -1)) nameIdx = j; if (modelIdx < 0 && (cell === '规格/型号' || cell.indexOf('规格') > -1 || cell.indexOf('型号') > -1)) modelIdx = j; if (countIdx < 0 && (cell === '数量' || cell.indexOf('数量') > -1 || cell.indexOf('总数') > -1)) countIdx = j; if (typeIdx < 0 && (cell === '类型' || cell.indexOf('类型') > -1)) typeIdx = j; if (unitIdx < 0 && (cell === '单位' || cell.indexOf('单位') > -1)) unitIdx = j; if (codeIdx < 0 && (cell === '物料代码' || cell.indexOf('物料编码') > -1 || cell.indexOf('物料代码') > -1)) codeIdx = j; if (priceIdx < 0 && (cell === '单价' || cell.indexOf('单价') > -1 || cell.indexOf('价格') > -1)) priceIdx = j; if (lineLenIdx < 0 && (cell.indexOf('线长') > -1)) lineLenIdx = j; } if (nameIdx >= 0 && countIdx >= 0) { startIdx = i + 1; break; } } if (startIdx < 0) continue; for (let i = startIdx; i < rows.length; i++) { const row = rows[i] || []; const nameCell = row[nameIdx]; const rawName = nameCell == null ? '' : String(nameCell); const nameStr = rawName.trim(); if (!nameStr) continue; const lower = nameStr.toLowerCase(); if (nameStr.indexOf('小计') > -1 || nameStr.indexOf('合计') > -1 || lower.indexOf('total') > -1) break; const modelStr = modelIdx >= 0 ? String(row[modelIdx] || '').trim() : ''; const countVal = row[countIdx]; const countStr = String(countVal == null ? '' : countVal).trim(); const match = countStr.match(/-?\d+(\.\d+)?/); const countNum = match ? Number(match[0]) : Number(countStr || 0); if (!isNaN(countNum) && countNum > 0) { const typeStr = typeIdx >= 0 ? String(row[typeIdx] || '').trim() : ''; const unitStr = unitIdx >= 0 ? String(row[unitIdx] || '').trim() : ''; const codeStr = codeIdx >= 0 ? String(row[codeIdx] || '').trim() : ''; const priceVal = priceIdx >= 0 ? String(row[priceIdx] || '').trim() : ''; const lineLenVal = lineLenIdx >= 0 ? String(row[lineLenIdx] || '').trim() : ''; all.push({ name: nameStr, model: modelStr, count: countNum, type: typeStr, unit: unitStr, code: codeStr, price: priceVal ? Number(priceVal) : undefined, lineLength: lineLenVal }); } } } return all; } catch (e) { return []; } } // 机箱配套 let request2Data = { factor: "机箱配套", data: [] }; let otherInstrumentCount = 0; // 其他模块采集仪总数 let vibratingWireSensorCount = 0; // 振弦信号传感器总数 let signal485SensorCount = 0; // 485信号传感器总数 let multiChannelVibratingWireRecorder = 0 // 多通道振弦采集仪 let singleChannelVibratingWireAcquisitionInstrumentCount = 0 // 单通道振弦采集仪 for (const item of request1Data) { if (item && item.data && item.data.length > 0) { for (const sensor of item.data) { if (sensor.name.includes("采集仪")) { otherInstrumentCount += Number(sensor.count || 0); } if (["锚索计", "钢筋计", "土压力计(单膜)", "位移计", "表面式应变计", "内埋式应变计", "孔隙水压计"].includes(sensor.name)) { vibratingWireSensorCount += Number(sensor.count || 0); } if (["激光测距仪", "裂缝计", "静力水准仪", "土壤温湿度传感器", "温湿度传感器", "雨量计"].includes(sensor.name)) { signal485SensorCount += Number(sensor.count || 0); } if (["多通道振弦采集仪"].includes(sensor.name)) { multiChannelVibratingWireRecorder += Number(sensor.count || 0); } if (["单通道振弦采集仪"].includes(sensor.name)) { singleChannelVibratingWireAcquisitionInstrumentCount += Number(sensor.count || 0); } } } } let items = []; let hubs = []; if (userText['机箱配套']) { try { const isSolar = true; const vw = Number(vibratingWireSensorCount || 0); let fd01 = 0, f08 = 0, f16 = 0, f32 = 0; const explicitOrFallbackCount = (factorKey, itemName, model, fallback) => { const direct = Number(userText[factorKey]?.[itemName]); if (!isNaN(direct) && direct > 0) return direct; const modelKey = `${itemName}|${model || ''}`; const byModel = Number(userText[factorKey]?.[modelKey] || userText[factorKey]?.[model]); if (!isNaN(byModel) && byModel > 0) return byModel; return Number(fallback || 0); }; if (vw > 0) { if (vw <= 2) { fd01 = vw; } else if (vw <= 32) { f08 = Math.ceil(vw / 8); } else if (vw <= 64) { f16 = Math.floor(vw / 16); const r = vw % 16; if (r > 0) { if (r <= 8) { f08 += Math.ceil(r / 8); } else { f16 += Math.ceil(r / 16); } } } else { f32 = Math.floor(vw / 32); const r = vw % 32; if (r > 0) { if (r <= 8) { f08 += Math.ceil(r / 8); } else { f16 += Math.ceil(r / 16); } } } singleChannelVibratingWireAcquisitionInstrumentCount = fd01; multiChannelVibratingWireRecorder = f08 + f16 + f32; if (fd01 > 0) { const c = explicitOrFallbackCount('机箱配套', '单通道振弦采集模块', 'FS-FD01', fd01); if (c > 0) { items.push({ name: '单通道振弦采集模块', model: 'FS-FD01', count: c }); // items.push({ name: '单通道振弦采集系统V1.0', model: 'FS-FD01-内置软件', count: c }); } } if (f08 > 0) { const c = explicitOrFallbackCount('机箱配套', '多通道振弦采集仪', 'FS-F08', f08); if (c > 0) { items.push({ name: '多通道振弦采集仪', model: 'FS-F08', count: c }); // items.push({ name: '多通道振弦采集仪原位监测系统V1.0', model: 'FS-F08-内置软件', count: c }); } } if (f16 > 0) { const c = explicitOrFallbackCount('机箱配套', '多通道振弦采集仪', 'FS-F16', f16); if (c > 0) { items.push({ name: '多通道振弦采集仪', model: 'FS-F16', count: c }); // items.push({ name: '多通道振弦采集仪原位监测系统V1.0', model: 'FS-F16-内置软件', count: c }); } } if (f32 > 0) { const c = explicitOrFallbackCount('机箱配套', '多通道振弦采集仪', 'FS-F32', f32); if (c > 0) { items.push({ name: '多通道振弦采集仪', model: 'FS-F32', count: c }); // items.push({ name: '多通道振弦采集仪原位监测系统V1.0', model: 'FS-F32-内置软件', count: c }); } } const s485 = Number(signal485SensorCount || 0); if (s485 > 0) { if (s485 <= 8) { const c4 = Math.ceil(s485 / 4); const hc = explicitOrFallbackCount('机箱配套', 'RS485集线器', 'FS-485JXQ-04-A', c4); if (hc > 0) hubs.push({ name: 'RS485集线器', model: 'FS-485JXQ-04-A', count: hc }); } else { const c8 = Math.floor(s485 / 8); const hc8 = explicitOrFallbackCount('机箱配套', 'RS485集线器', 'FS-485JXQ-08-A', c8); if (hc8 > 0) hubs.push({ name: 'RS485集线器', model: 'FS-485JXQ-08-A', count: hc8 }); const r = s485 % 8; if (r > 0) { const c4 = Math.ceil(r / 4); const hc4 = explicitOrFallbackCount('机箱配套', 'RS485集线器', 'FS-485JXQ-04-A', c4); if (hc4 > 0) hubs.push({ name: 'RS485集线器', model: 'FS-485JXQ-04-A', count: hc4 }); } } } } else { } const instrumentCountForBreaker = items .filter(x => x.name === '多通道振弦采集仪' || x.name === '单通道振弦采集模块') .reduce((sum, x) => sum + Number(x.count || 0), 0); const basics = [ { name: '断路器', model: 'DZ47-60', count: Number(otherInstrumentCount || 0) + instrumentCountForBreaker }, { name: '12V直流电源防雷器', model: '12V', count: Number(otherInstrumentCount || 0) + instrumentCountForBreaker }, { name: '数据采集箱', model: 'FS-CJX03-A', count: Number(otherInstrumentCount || 0) + instrumentCountForBreaker }, { name: '接地电缆', model: 'BVR 1*16', count: (Number(otherInstrumentCount || 0) + instrumentCountForBreaker) * 10 }, { name: 'GPRS无线模块', model: 'FS-DTU-4G-W-V1.00', count: Number(otherInstrumentCount || 0) + instrumentCountForBreaker }, { name: '物联网卡', model: '20G', count: Number(otherInstrumentCount || 0) + instrumentCountForBreaker }, { name: '接地桩(镀锌角铁)', model: 'FS-DXJT-2', count: (Number(otherInstrumentCount || 0) + instrumentCountForBreaker) * 3 }, { name: '镀锌扁铁', model: 'FS-DXBL-40*40', count: (Number(otherInstrumentCount || 0) + instrumentCountForBreaker) * 10 }, { name: '降阻剂', model: '25kg/袋', count: (Number(otherInstrumentCount || 0) + instrumentCountForBreaker) * 10 }, ]; const mains = isSolar ? [] : [ { name: '电源电涌保护器', model: 'AM2-40', count: Number(otherInstrumentCount || 0) + instrumentCountForBreaker }, { name: '开关电源1', model: '25-12-A', count: Number(otherInstrumentCount || 0) + instrumentCountForBreaker }, { name: '导轨插座', model: '10A', count: Number(otherInstrumentCount || 0) + instrumentCountForBreaker }, { name: '220V交流供电电缆', model: 'RVV 3*2.5', count: (Number(otherInstrumentCount || 0) + instrumentCountForBreaker) * 100 }, ]; const breakerCount = Number(otherInstrumentCount || 0) + instrumentCountForBreaker; request2Data = { factor: "机箱配套", data: breakerCount > 0 ? items.concat(hubs).concat(basics).concat(mains) : [] }; } catch (error) { console.error('机箱配套 calculation failed', error); } } let collectingBoxCount = 0; // 采集箱总数 let vibratingWireAcquisitionInstrumentCount = 0; // 振弦采集仪总数 let dataAcquisitionInstrumentCount = otherInstrumentCount; // 数据采集仪总数 let GNSSCount = 0; // GNSS接收机总数 let PVCNum = Number(userText['通信系统']?.['PVC管长度'] || 0); // PVC管总数 let fiberOpticsLength = Number(userText['通信系统']?.['通信光缆(光纤)长度'] || 0); // 通信光缆(光纤)长度 for (const item of request1Data.concat([request2Data])) { if (item && item.data && item.data.length > 0) { for (const sensor of item.data) { if (sensor.name === "数据采集箱") { collectingBoxCount += Number(sensor.count || 0); } const n1 = String(sensor.name || ''); const m1 = String(sensor.model || ''); if (n1.includes("振弦采集仪") && !(n1.includes('原位监测系统') && n1.includes('V1.0')) && !m1.includes('内置软件')) { vibratingWireAcquisitionInstrumentCount += Number(sensor.count || 0); } } } } if (vibratingWireAcquisitionInstrumentCount === 0) { vibratingWireAcquisitionInstrumentCount = Number(multiChannelVibratingWireRecorder || 0) + Number(singleChannelVibratingWireAcquisitionInstrumentCount || 0); } // 通信系统 let request3Data = { factor: "通信系统", data: [] }; if (userText['通信系统']) { try { let data = tunnelPrompts['通信系统'].getManifest([collectingBoxCount, vibratingWireAcquisitionInstrumentCount, dataAcquisitionInstrumentCount, GNSSCount, PVCNum, fiberOpticsLength]); request3Data = { factor: "通信系统", data: data || [] }; } catch (error) { console.error('通信系统 calculation failed', error); request3Data = { factor: "通信系统", data: [] }; } } // 太阳能计算 let request5Data = null; let solarEnergy = { solarEnergyTitle: null, solarEnergyData: null, powerTitle: null, powerData: null, }; const materials = await models.Materials.findAll({ raw: true, where: { structureType: structureType } }); if (userText['电力系统(太阳能)']) { const request4Params = { '静力水准仪': 0, '裂缝计': 0, '土壤温湿度传感器': 0, '温湿度传感器': 0, '雨量计': 0, '激光测距仪': 0, '多通道振弦采集仪': 0, '数据采集系统V1.0': 0, 'GPRS无线模块': 0, '工业级光纤收发器': 0, '串口服务器': 0, '工业级交换机': 0, '4G网络球机': 0, '硬盘录像机': 0, } for (const item of request1Data.concat([request2Data, request3Data])) { if (item && item.data && item.data.length > 0) { for (const sensor of item.data) { for (const key in request4Params) { if (sensor.name === key) { request4Params[key] += Number(sensor.count || 0); } } } } } const regionName = String(userText['电力系统(太阳能)']?.['地区名称'] || ''); const region = await superagent .post(`${apiUrl}/api/v1/chat/completions`) .send({ "stream": false, "detail": false, "messages": [ { "role": "user", "content": [ { "type": "text", "text": regionName } ] } ] }) .set({ Authorization: `Bearer ${regionAppKey}`, "Content-Type": "application/json", }) .timeout({ response: 1000 * 60 * 30, // 等待服务器发送数据的超时时间(毫秒) deadline: 1000 * 60 * 30, // 整个请求完成的最大时间(毫秒) }) const regionType = JSON.parse(region?.body?.choices[0]?.message?.content?.trim()).region; const batterySafetyFactorMap = { '北方地区': 1.1, '南方平原': 1.1, '南方山区': 1.4 }; const tempCorrectionFactorMap = { '北方地区': 1.2, '南方平原': 1.0, '南方山区': 1.1 }; const batterySafetyFactor = batterySafetyFactorMap[regionType]; const tempCorrectionFactor = tempCorrectionFactorMap[regionType]; const deviceList = [ { name: '静力水准仪', model: 'FS-JLSZ-20-V1.00', power: 0.24, key: '静力水准仪' }, { name: '裂缝计', model: 'FS-LF10-Z', power: 0.24, key: '裂缝计' }, { name: '土壤温湿度传感器', model: 'FS-TRWSD', power: 0.576, key: '土壤温湿度传感器' }, { name: '温湿度传感器', model: 'FS-BDS-WSD', power: 0.24, key: '温湿度传感器' }, { name: '雨量计', model: 'FS-FDYLJ', power: 1.2, key: '雨量计' }, { name: '激光测距仪', model: 'FS-LRF-V1.00', power: 0.4, key: '激光测距仪' }, { name: '多通道振弦采集仪', model: 'FS-F08', power: 2.56, key: '多通道振弦采集仪' }, { name: '数据采集系统V1.0', model: 'FS-D04', power: 1.7, key: '数据采集系统V1.0' }, { name: 'GPRS无线模块', model: 'FS-DTU-4G-W-V1.00', power: 0.96, key: 'GPRS无线模块' }, { name: '工业级光纤收发器', model: 'SKMSW-02011L', power: 6, key: '工业级光纤收发器' }, { name: '串口服务器', model: '1D(RS485/232)', power: 0.84, key: '串口服务器' }, { name: '工业级交换机', model: '', power: 2.5, key: '工业级交换机' }, { name: '4G网络球机', model: '3寸 400万', power: 15, key: '4G网络球机' }, { name: '硬盘录像机', model: '4路1盘位', power: 18, key: '硬盘录像机' }, ]; solarEnergy.powerTitle = '功率统计表'; const powerHeaders = ['序号', '设备名称', '默认型号', '单位', '功率(W)', '数量', '总功率(W)']; const powerRows = []; const unitOverride = { '多通道振弦采集仪|FS-F08': '台(8口)', '数据采集系统V1.0|FS-D04': '台(4口)', }; const getUnit = (name, model) => { const overrideKey = `${name}|${model || ''}`; if (unitOverride[overrideKey]) return unitOverride[overrideKey]; let m = materials.find(m => m.name === name && (model ? m.model === model : true)); if (!m && model) m = materials.find(m => m.model === model); if (!m) m = materials.find(m => m.name === name); return m?.unit || ''; }; let totalPower = 0; for (let i = 0; i < deviceList.length; i++) { const d = deviceList[i]; const qty = Number(request4Params[d.key] || 0); const rowPower = Number(d.power || 0); const rowTotalPower = Number((rowPower * qty).toFixed(6)); totalPower += rowTotalPower; const unit = getUnit(d.name, d.model); powerRows.push([ String(i + 1), d.name, d.model, unit, String(rowPower), String(qty), String(rowTotalPower) ]); } solarEnergy.powerData = { headers: powerHeaders, rows: powerRows }; const days = Number(userText['电力系统(太阳能)']?.['连续阴雨天数'] || 0) || 0; 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); solarEnergy.solarEnergyTitle = regionType; const solarHeaders = ['总功率', '日耗电量', '日平均耗电量', '连续阴雨天', '蓄电池容量安全系数', '温度修正系数', '电池容量', '电池数量', '单块太阳能功率', '日平均光照时间', '设备单日耗电量', '太阳能板单日供电量', '太阳能板数量']; const solarRow = [ String(Number(totalPower.toFixed(6))), String(Number(dailyConsumption.toFixed(6))), String(Number(averageDailyConsumption.toFixed(6))), String(days), String(batterySafetyFactor), String(tempCorrectionFactor), String(Number(batteryCapacity.toFixed(6))), String(batteryCount), String(singlePanelPower), String(sunlightHours), String(Number(deviceDailyConsumption.toFixed(6))), String(Number(singlePanelDailySupply.toFixed(6))), String(solarPanelCount) ]; solarEnergy.solarEnergyData = { headers: solarHeaders, rows: [solarRow] }; try { const data = tunnelPrompts['电力系统(太阳能)'].getManifest([solarPanelCount, batteryCount]) || []; const controllerLoad = (200 / 18) * solarPanelCount; const controllerModel = controllerLoad < 10 ? '12V10A' : controllerLoad < 30 ? '12V30A' : '12V50A'; const controllerCount = Math.ceil((200 * solarPanelCount) / 900); const controllerDesc = '太阳能控制器:根据太阳能控制器选型计算公式从“12V10A、12V30A、12V50A”中选择一个合适的型号。默认太阳能板功率为200,太阳能控制器选型计算公式:(太阳能板功率/18)*太阳能板数量,根据该公式的计算结果选择对应型号:如果计算结果<10,选择太阳能控制器12V10A;如果计算结果>=10且<30选择太阳能控制器12V30A;如果计算结果>=30选择太阳能控制器12V50A。太阳能控制器数量=太阳能板功率*太阳能板数量/900(向上取整)。'; for (let k = 0; k < data.length; k++) { const it = data[k]; if (String(it?.name) === '太阳能控制器') { it.model = controllerModel; it.count = controllerCount; } } request5Data = { data }; } catch (error) { console.error('电力系统(太阳能) calculation failed', error); } } const allListData = request1Data.concat([request2Data, request3Data]); if (request5Data) { allListData.push(request5Data) } // 工期核算清单 const totalCountMap = {}; for (const item of allListData) { if (item && item.data && item.data.length > 0) { for (const sensor of item.data) { const k = sensor.name; const v = Number(sensor.count || 0); totalCountMap[k] = (totalCountMap[k] || 0) + v; } } } const cableSum = Object.entries(totalCountMap).reduce((s, [k, v]) => s + ((String(k).includes('通信电缆') || String(k).includes('接地电缆')) ? Number(v || 0) : 0), 0); 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, '全站仪': 0.3, '钢尺水位计': 2, '一体化裂缝计(成品)': 2.7, '一体化地灾(加速度计)': 4, '一体化地灾倾角计': 4, 'GNSS接收机': 0.9, '4G球机': 2, '管式含水率仪': 4, '分布式节点': 4, '称重系统(1套2车道)': 0.3, '拼接屏(4*6)': 0.3, '太阳能供电': 1, '监控中心(3天)': 3.2, '墩座1': 4, '墩座2': 4, '墩座3': 2.7, '墩座4': 2, '基础1(具备挖机、吊车)': 0.7, '基础2': 2.7, '基础3': 2.7, '基础4': 2, '地网': 1, '线缆(h/100米)': 8.4, '桥架1(h/100米)': 1.3, '桥架2(h/100米)': 1.3, 'PVC管1(h/100米)': 5.3, 'PVC管2(h/100米)': 5.3 }; const inputQtyMap = { '表面式应变计': Number(totalCountMap['表面式应变计'] || 0), '内埋式应变计': Number(totalCountMap['内埋式应变计'] || 0), '钢筋计': Number(totalCountMap['钢筋计'] || 0), '土压力计': Number(totalCountMap['土压力计(单膜)'] || 0), '孔隙水压计': Number(totalCountMap['孔隙水压计'] || 0), '轴力计': Number(totalCountMap['轴力计'] || 0), '锚索计': Number(totalCountMap['锚索计'] || 0), '温度传感器': Number(totalCountMap['温度传感器'] || 0), '温湿度传感器': Number(totalCountMap['温湿度传感器'] || 0), '土壤温湿度传感器': Number(totalCountMap['土壤温湿度传感器'] || 0), '闭环磁通量标定': 0, '开环磁通量绕线(小)': 0, '开环磁通量绕线(中)': 0, '开环磁通量绕线(大)': 0, '人工磁通量温补': 0, '盒式固定测斜仪': Number(totalCountMap['盒式固定测斜仪'] || 0), '串联式固定测斜仪': Number(totalCountMap['串联式固定测斜仪'] || 0), '裂缝计': Number(totalCountMap['裂缝计'] || 0), '静力水准仪': Number(totalCountMap['静力水准仪'] || 0), '物位计': Number(totalCountMap['物位计'] || 0), '雷达液位计': Number(totalCountMap['雷达液位计'] || 0), '拉线位移传感器': Number(totalCountMap['拉线位移传感器'] || 0), '多点位移计(振弦式)': Number(totalCountMap['多点位移计(振弦式)'] || 0), '加速度计': Number(totalCountMap['加速度计'] || 0), '激光测距仪': Number(totalCountMap['激光测距仪'] || 0), '投入式水位计': Number(totalCountMap['投入式水位计'] || 0), '超声波水位计': Number(totalCountMap['超声波水位计'] || 0), '雨量计': Number(totalCountMap['雨量计'] || 0), '超声波风速风向仪': Number(totalCountMap['超声波风速风向仪'] || 0), '光电挠度仪': Number(totalCountMap['光电挠度仪'] || 0), '全站仪': Number(totalCountMap['全站仪'] || 0), '钢尺水位计': Number(totalCountMap['钢尺水位计'] || 0), '一体化裂缝计(成品)': Number(totalCountMap['一体化裂缝计(成品)'] || 0), '一体化地灾(加速度计)': Number(totalCountMap['一体化地灾(加速度计)'] || 0), '一体化地灾倾角计': Number(totalCountMap['一体化地灾倾角计'] || 0), 'GNSS接收机': Number(totalCountMap['测地型GNSS接收机'] || 0), '4G球机': Number(totalCountMap['4G网络球机'] || 0), '管式含水率仪': Number(totalCountMap['管式含水率仪'] || 0), '分布式节点': Number(totalCountMap['分布式节点'] || 0), '称重系统(1套2车道)': Number(totalCountMap['两车道(全套)'] || 0), '拼接屏(4*6)': Number(totalCountMap['拼接屏(4*6)'] || 0), '太阳能供电': Number(totalCountMap['太阳能控制器'] || 0), '监控中心(3天)': 0, '墩座1': Number(totalCountMap['墩座1'] || 0), '墩座2': Number(totalCountMap['墩座2'] || 0), '墩座3': Number(totalCountMap['墩座3'] || 0), '墩座4': Number(totalCountMap['墩座4'] || 0), '基础1(具备挖机、吊车)': Number(totalCountMap['基础1(具备挖机、吊车)'] || 0), '基础2': Number(totalCountMap['基础2'] || 0), '基础3': Number(totalCountMap['基础3'] || 0), '基础4': Number(totalCountMap['基础4'] || 0), '地网': Number(totalCountMap['接地桩(镀锌角铁)'] || 0), '线缆(h/100米)': Number(cableSum || 0), '桥架1(h/100米)': Number(totalCountMap['桥架'] || 0), '桥架2(h/100米)': 0, 'PVC管1(h/100米)': Number(PVCNum || 0), 'PVC管2(h/100米)': 0 }; const sensorsForDebug = [ '表面式应变计', '内埋式应变计', '温度传感器', '温湿度传感器', '盒式固定测斜仪', '裂缝计', '静力水准仪', '拉线位移传感器', '超声波风速风向仪', '光电挠度仪', 'GNSS接收机', '4G球机' ]; const sensorTotal = sensorsForDebug.reduce((sum, k) => sum + Number(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 headers6 = ['序号', '分类', '事项', '耗时(小时/人)', '安装数量/个(2人8小时计)', '备注', '数量输入', '工期(2人天)']; 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米)', '系统调试', '外包(天)', '验收、培训', '项目风险天数(设备转场、开路、技术难点等)', '项目经理(天)' ]; function 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 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', }; function getHoursPerPerson(name) { return hoursPerPersonMap[name] ?? ''; } 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天', }; function getInstallDisplay(name, installQty, qty) { // if (name.endsWith('(h/100米)')) { // const v = Number(((installQty || 0) * (qty || 0) / 100).toFixed(3)); // return String(v); // } 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 ?? ''); } function installRuleText(name) { if (name === '地网') return '安装数量×数量输入÷3'; if (name.endsWith('(h/100米)')) return '安装数量×数量输入÷100'; if (name === '监控中心(3天)') return '固定3天'; if (name === '系统调试') return '总数≤50→2;≤100→3;>100→5'; if (name === '验收、培训') return '总数≤100→3;>100→5'; if (name === '项目风险天数(设备转场、开路、技术难点等)') return '总数≤100→2;>100→3'; return '数量输入÷安装数量'; } function 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 rows6 = []; 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); rows6.push([ String(i + 1), getCategory(name), displayName, getHoursPerPerson(name), installDisplay, remarksMap[name] ?? '', (name === '外包(天)' || name === '项目经理(天)') ? '' : String(Number((qty || 0).toFixed(3))), String(durationVal) ]); } const epiboleLimitIdx2 = orderedItems.indexOf('系统调试') + 1; const epiboleDays = rows6.reduce((acc, row) => { const idx = parseInt(row[0], 10); if (isNaN(idx) || idx > epiboleLimitIdx2) return acc; return acc + Number(row[row.length - 1] || 0); }, 0); const managerHoursPerDay = Number(userText['工期核算清单']?.['项目经理每日工时'] || 8); const projectManagerDays = Number((((epiboleDays + acceptanceTrainingDays + projectRiskDays) * 8) / (managerHoursPerDay || 8)).toFixed(6)); const durationData = { headers: headers6, rows: rows6 }; // 核算要点 let hasSolar = false let request7Power = ''; let hasFiber = false; for (const item of allListData) { if (item && item.data && item.data.length > 0) { for (const sensor of item.data) { if (sensor.name.includes('太阳能') && !request7Power) { request7Power = ',太阳能供电'; hasSolar = true; } if (sensor.name.includes('光纤')) { request7Power = ',光纤通信'; hasFiber = true; break; } } } if (hasFiber) break; } const mainPointData = getMainPointData(userText, epiboleDays, hasSolar, hasFiber); let sheet1Data = request1Data; let sheet1Key = request1Key; if (userText['机箱配套']) { sheet1Data.push(request2Data); sheet1Key.push('机箱配套'); } if (userText['通信系统']) { sheet1Data.push(request3Data); sheet1Key.push('通信系统'); } if (request5Data) { sheet1Data.push(request5Data); sheet1Key.push('电力系统(太阳能)'); } /********** 2.生成excel **********/ const tableData = generationXlsxData({ materials, sheet1Data, sheet1Key, solarEnergy: userText['电力系统(太阳能)'] ? solarEnergy : null, duration: { durationData, epiboleDays, projectManagerDays }, mainPointData }); /********** 3.保存数据 **********/ await models.SchemeList.update( { status: 'success', updateAt: new Date(), tableData, }, { where: { id: schemeId } } ); ctx.logger.log(`方案清单数据生成成功,schemeId:${schemeId}`); await transaction.commit(); } catch (error) { await transaction.rollback(); ctx.logger.log(error); try { await models.SchemeList.update( { status: 'fail' }, { where: { id: schemeId } } ); } catch (err) { console.log(`修改状态失败,scheme.id=${schemeId}`); ctx.logger.log(err); } } } // 生成边坡方案清单 async function generationSlopeScheme(ctx, schemeId, userText, structureType, transmitMethod) { const transaction = await ctx.app.fs.dc.orm.transaction(); const { models } = ctx.app.fs.dc; try { const { apiUrl } = ctx.app.fs.config.fastGpt; const regionAppKey = 'fastgpt-kzqnfXUusbIP6R42we1KMAST9SqBAPCtbAxTV1oQHWRuM7CEb1am2VF7z0' /********** 1.请求数据 **********/ const allRequestKey = Object.keys(userText); const request1Key = allRequestKey.filter(key => !['机箱配套', '通信系统', '电力系统(太阳能)'].includes(key)); async function readManifestFromFile(fileUrl) { try { const res = await superagent .get(fileUrl) .buffer(true) .parse((response, cb) => { const data = []; response.on('data', (chunk) => { data.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); }); response.on('end', () => cb(null, Buffer.concat(data))); response.on('error', (err) => cb(err)); }); const buf = res.body; const wb = XLSXS.read(buf, { type: 'buffer' }); const sheetNames = wb.SheetNames || []; if (!sheetNames.length) return []; const all = []; for (let s = 0; s < sheetNames.length; s++) { const ws = wb.Sheets[sheetNames[s]]; if (!ws) continue; const rows = XLSXS.utils.sheet_to_json(ws, { header: 1, blankrows: false, defval: '' }) || []; let nameIdx = -1, modelIdx = -1, countIdx = -1, startIdx = -1, typeIdx = -1, unitIdx = -1, codeIdx = -1, priceIdx = -1, lineLenIdx = -1; const headerMaxScan = Math.min(rows.length, 20); for (let i = 0; i < headerMaxScan; i++) { const row = rows[i] || []; for (let j = 0; j < row.length; j++) { const cell = String(row[j] || '').replace(/\s+/g, ''); if (nameIdx < 0 && (cell === '名称' || cell.indexOf('设备名称') > -1 || cell.indexOf('物料名称') > -1 || cell.indexOf('品名') > -1)) nameIdx = j; if (modelIdx < 0 && (cell === '规格/型号' || cell.indexOf('规格') > -1 || cell.indexOf('型号') > -1)) modelIdx = j; if (countIdx < 0 && (cell === '数量' || cell.indexOf('数量') > -1 || cell.indexOf('总数') > -1)) countIdx = j; if (typeIdx < 0 && (cell === '类型' || cell.indexOf('类型') > -1)) typeIdx = j; if (unitIdx < 0 && (cell === '单位' || cell.indexOf('单位') > -1)) unitIdx = j; if (codeIdx < 0 && (cell === '物料代码' || cell.indexOf('物料编码') > -1 || cell.indexOf('物料代码') > -1)) codeIdx = j; if (priceIdx < 0 && (cell === '单价' || cell.indexOf('单价') > -1 || cell.indexOf('价格') > -1)) priceIdx = j; if (lineLenIdx < 0 && (cell.indexOf('线长') > -1)) lineLenIdx = j; } if (nameIdx >= 0 && countIdx >= 0) { startIdx = i + 1; break; } } if (startIdx < 0) continue; for (let i = startIdx; i < rows.length; i++) { const row = rows[i] || []; const nameCell = row[nameIdx]; const rawName = nameCell == null ? '' : String(nameCell); const nameStr = rawName.trim(); if (!nameStr) continue; const lower = nameStr.toLowerCase(); if (nameStr.indexOf('小计') > -1 || nameStr.indexOf('合计') > -1 || lower.indexOf('total') > -1) break; const modelStr = modelIdx >= 0 ? String(row[modelIdx] || '').trim() : ''; const countVal = row[countIdx]; const countStr = String(countVal == null ? '' : countVal).trim(); const match = countStr.match(/-?\d+(\.\d+)?/); const countNum = match ? Number(match[0]) : Number(countStr || 0); if (!isNaN(countNum) && countNum > 0) { const typeStr = typeIdx >= 0 ? String(row[typeIdx] || '').trim() : ''; const unitStr = unitIdx >= 0 ? String(row[unitIdx] || '').trim() : ''; const codeStr = codeIdx >= 0 ? String(row[codeIdx] || '').trim() : ''; const priceVal = priceIdx >= 0 ? String(row[priceIdx] || '').trim() : ''; const lineLenVal = lineLenIdx >= 0 ? String(row[lineLenIdx] || '').trim() : ''; all.push({ name: nameStr, model: modelStr, count: countNum, type: typeStr, unit: unitStr, code: codeStr, price: priceVal ? Number(priceVal) : undefined, lineLength: lineLenVal }); } } } return all; } catch (e) { return []; } } const request1Data = await Promise.all(request1Key.map(async key => { try { const expectSolarParamKeys = new Set([ '桥墩/桥塔位移监测(北斗)', '视频监控(有线)', '视频监控(无线1)', '机箱配套', '表面位移监测', ]); const isSolarSelected = !!userText['电力系统(太阳能)']; const params = [].concat( slopePrompts[key].sensors.map(sensor => { const v = userText[key]?.[sensor]; if (v === undefined && /数量|长度|桥长|车道|天数|弦数/.test(sensor)) return 0; return v; }), [expectSolarParamKeys.has(key) ? isSolarSelected : transmitMethod] ); let data = slopePrompts[key].getManifest(params) || []; const fileUrl = slopePrompts[key]?.file; if (fileUrl) { const fileData = await readManifestFromFile(fileUrl); const map = new Map(); const keyFn = (n, m) => `${String(n || '').trim().toLowerCase()}|${String(m || '').trim().toLowerCase()}`; for (const it of data || []) map.set(keyFn(it.name, it.model), { name: String(it.name || '').trim(), model: String(it.model || '').trim(), count: Number(it.count || 0), type: it.type, unit: it.unit, code: it.code, price: it.price, lineLength: it.lineLength }); for (const it of fileData || []) { const k = keyFn(it.name, it.model); if (map.has(k)) { const v = map.get(k); if (!v.type && it.type) v.type = it.type; if (!v.unit && it.unit) v.unit = it.unit; if (!v.code && it.code) v.code = it.code; if (v.price == null && it.price != null) v.price = it.price; if (v.lineLength == null && it.lineLength != null) v.lineLength = it.lineLength; map.set(k, v); } } data = Array.from(map.values()); } return { factor: key, data: data || [] }; } catch (error) { console.error(`Calculation failed for ${key}`, error); return { factor: key, data: [] }; } })); for (let i = 0; i < request1Data.length; i++) { const it = request1Data[i]; if (it.factor === '表面位移监测') { const cityPowerSet = new Set(['电源电涌保护器', '开关电源1', '导轨插座', '220V交流供电电缆']); it.data = (it.data || []).filter(x => !cityPowerSet.has(String(x?.name || ''))); } } const opsSelected3 = String(userText['运维服务']?.['选择子服务'] || '').split('、').map(x => x.trim()).filter(Boolean); for (let i = 0; i < request1Data.length; i++) { const it = request1Data[i]; if (it.factor === '运维服务' && Array.isArray(it.data)) { it.data = opsSelected3.length > 0 ? it.data.filter(row => opsSelected3.includes(String(row?.name || ''))) : []; } } // 机箱配套 let request2Data = { factor: "机箱配套", data: [] }; let otherInstrumentCount = 0; // 其他模块采集仪总数 let vibratingWireSensorCount = 0; // 振弦信号传感器总数 let signal485SensorCount = 0; // 485信号传感器总数 for (const item of request1Data) { if (item && item.data && item.data.length > 0) { for (const sensor of item.data) { if (sensor.name.includes("采集仪")) { otherInstrumentCount += Number(sensor.count || 0); } if (["孔隙水压计", "土压力计(单膜)", "表面式应变计"].includes(sensor.name)) { vibratingWireSensorCount += Number(sensor.count || 0); } if (["串联式固定测斜仪", "物位计", "投入式水位计", "雷达水位计", "雨量计", "温湿度传感器"].includes(sensor.name)) { signal485SensorCount += Number(sensor.count || 0); } } } } let multiChannelVibratingWireRecorder = 0; let singleChannelVibratingWireAcquisitionInstrumentCount = 0; if (userText['机箱配套']) { try { const isSolar = true; const vw = Number(vibratingWireSensorCount || 0); let fd01 = 0, f08 = 0, f16 = 0, f32 = 0; const explicitOrFallbackCount = (factorKey, itemName, model, fallback) => { const direct = Number(userText[factorKey]?.[itemName]); if (!isNaN(direct) && direct > 0) return direct; const modelKey = `${itemName}|${model || ''}`; const byModel = Number(userText[factorKey]?.[modelKey] || userText[factorKey]?.[model]); if (!isNaN(byModel) && byModel > 0) return byModel; return Number(fallback || 0); }; let items = []; let hubs = []; if (vw > 0) { if (vw <= 2) { fd01 = vw; } else if (vw <= 32) { f08 = Math.ceil(vw / 8); } else if (vw <= 64) { f16 = Math.floor(vw / 16); const r = vw % 16; if (r > 0) { if (r <= 8) { f08 += Math.ceil(r / 8); } else { f16 += Math.ceil(r / 16); } } } else { f32 = Math.floor(vw / 32); const r = vw % 32; if (r > 0) { if (r <= 8) { f08 += Math.ceil(r / 8); } else { f16 += Math.ceil(r / 16); } } } singleChannelVibratingWireAcquisitionInstrumentCount = fd01; multiChannelVibratingWireRecorder = f08 + f16 + f32; if (fd01 > 0) { const c = explicitOrFallbackCount('机箱配套', '单通道振弦采集模块', 'FS-FD01', fd01); if (c > 0) { items.push({ name: '单通道振弦采集模块', model: 'FS-FD01', count: c }); } } if (f08 > 0) { const c = explicitOrFallbackCount('机箱配套', '多通道振弦采集仪', 'FS-F08', f08); if (c > 0) { items.push({ name: '多通道振弦采集仪', model: 'FS-F08', count: c }); } } if (f16 > 0) { const c = explicitOrFallbackCount('机箱配套', '多通道振弦采集仪', 'FS-F16', f16); if (c > 0) { items.push({ name: '多通道振弦采集仪', model: 'FS-F16', count: c }); } } if (f32 > 0) { const c = explicitOrFallbackCount('机箱配套', '多通道振弦采集仪', 'FS-F32', f32); if (c > 0) { items.push({ name: '多通道振弦采集仪', model: 'FS-F32', count: c }); } } const s485 = Number(signal485SensorCount || 0); if (s485 > 0) { if (s485 <= 8) { const c4 = Math.ceil(s485 / 4); const hc = explicitOrFallbackCount('机箱配套', 'RS485集线器', 'FS-485JXQ-04-A', c4); if (hc > 0) hubs.push({ name: 'RS485集线器', model: 'FS-485JXQ-04-A', count: hc }); } else { const c8 = Math.floor(s485 / 8); const hc8 = explicitOrFallbackCount('机箱配套', 'RS485集线器', 'FS-485JXQ-08-A', c8); if (hc8 > 0) hubs.push({ name: 'RS485集线器', model: 'FS-485JXQ-08-A', count: hc8 }); const r = s485 % 8; if (r > 0) { const c4 = Math.ceil(r / 4); const hc4 = explicitOrFallbackCount('机箱配套', 'RS485集线器', 'FS-485JXQ-04-A', c4); if (hc4 > 0) hubs.push({ name: 'RS485集线器', model: 'FS-485JXQ-04-A', count: hc4 }); } } } } else { } const instrumentCountForBreaker = items .filter(x => x.name === '多通道振弦采集仪' || x.name === '单通道振弦采集模块') .reduce((sum, x) => sum + Number(x.count || 0), 0); const basics = [ { name: '断路器', model: 'DZ47-60', count: Number(otherInstrumentCount || 0) + instrumentCountForBreaker }, { name: '12V直流电源防雷器', model: '12V', count: Number(otherInstrumentCount || 0) + instrumentCountForBreaker }, { name: '数据采集箱', model: 'FS-CJX03-A', count: Number(otherInstrumentCount || 0) + instrumentCountForBreaker }, { name: '接地电缆', model: 'BVR 1*16', count: (Number(otherInstrumentCount || 0) + instrumentCountForBreaker) * 10 }, { name: 'GPRS无线模块', model: 'FS-DTU-4G-W-V1.00', count: Number(otherInstrumentCount || 0) + instrumentCountForBreaker }, { name: '物联网卡', model: '20G', count: Number(otherInstrumentCount || 0) + instrumentCountForBreaker }, { name: '接地桩(镀锌角铁)', model: 'FS-DXJT-2', count: (Number(otherInstrumentCount || 0) + instrumentCountForBreaker) * 3 }, { name: '镀锌扁铁', model: 'FS-DXBL-40*40', count: (Number(otherInstrumentCount || 0) + instrumentCountForBreaker) * 10 }, { name: '降阻剂', model: '25kg/袋', count: (Number(otherInstrumentCount || 0) + instrumentCountForBreaker) * 10 }, ]; const mains = isSolar ? [] : [ { name: '电源电涌保护器', model: 'AM2-40', count: Number(otherInstrumentCount || 0) + instrumentCountForBreaker }, { name: '开关电源1', model: '25-12-A', count: Number(otherInstrumentCount || 0) + instrumentCountForBreaker }, { name: '导轨插座', model: '10A', count: Number(otherInstrumentCount || 0) + instrumentCountForBreaker }, { name: '220V交流供电电缆', model: 'RVV 3*2.5', count: (Number(otherInstrumentCount || 0) + instrumentCountForBreaker) * 100 }, ]; const breakerCount = Number(otherInstrumentCount || 0) + instrumentCountForBreaker; request2Data = { factor: "机箱配套", data: breakerCount > 0 ? items.concat(hubs).concat(basics).concat(mains) : [] }; } catch (error) { request2Data = { factor: "机箱配套", data: [] }; } } let collectingBoxCount = 0; // 采集箱总数 let vibratingWireAcquisitionInstrumentCount = 0; // 振弦采集仪总数 let dataAcquisitionInstrumentCount = otherInstrumentCount; // 数据采集仪总数 let GNSSCount = 0; // GNSS接收机总数 let PVCNum = Number(userText['通信系统']?.['PVC管长度'] || 0); // PVC管总数 let fiberOpticsLength = Number(userText['通信系统']?.['通信光缆(光纤)长度'] || 0); // 通信光缆(光纤)长度 for (const item of request1Data.concat([request2Data])) { if (item && item.data && item.data.length > 0) { for (const sensor of item.data) { if (sensor.name === "数据采集箱") { collectingBoxCount += Number(sensor.count || 0); } const n2 = String(sensor.name || ''); const m2 = String(sensor.model || ''); if (n2.includes("振弦采集仪") && !(n2.includes('原位监测系统') && n2.includes('V1.0')) && !m2.includes('内置软件')) { vibratingWireAcquisitionInstrumentCount += Number(sensor.count || 0); } if (sensor.name === "测地型GNSS接收机") { GNSSCount += Number(sensor.count || 0); } } } } // 通信系统 let request3Data = { factor: "通信系统", data: [] }; if (userText['通信系统']) { try { let data = slopePrompts['通信系统'].getManifest([collectingBoxCount, vibratingWireAcquisitionInstrumentCount, dataAcquisitionInstrumentCount, GNSSCount, PVCNum, fiberOpticsLength]); request3Data = { factor: "通信系统", data: data || [] }; } catch (error) { console.error('通信系统 calculation failed', error); request3Data = { factor: "通信系统", data: [] }; } } // 太阳能计算 let request5Data = null; let solarEnergy = { solarEnergyTitle: null, solarEnergyData: null, powerTitle: null, powerData: null, }; const materials = await models.Materials.findAll({ raw: true, where: { structureType: structureType } }); if (userText['电力系统(太阳能)']) { const request4Params = { '物位计': 0, '串联式固定测斜仪': 0, '投入式水位计': 0, '温湿度传感器': 0, '雨量计': 0, '多通道振弦采集仪': 0, '数据采集系统V1.0': 0, 'GPRS无线模块': 0, '工业级光纤收发器': 0, '串口服务器': 0, '工业级交换机': 0, '4G网络球机': 0, '硬盘录像机': 0, '测地型GNSS接收机': 0, } for (const item of request1Data.concat([request2Data, request3Data])) { if (item && item.data && item.data.length > 0) { for (const sensor of item.data) { for (const key in request4Params) { if (sensor.name === key) { request4Params[key] += Number(sensor.count || 0); } } } } } const regionName = String(userText['电力系统(太阳能)']?.['地区名称'] || ''); const region = await superagent .post(`${apiUrl}/api/v1/chat/completions`) .send({ "stream": false, "detail": false, "messages": [ { "role": "user", "content": [ { "type": "text", "text": regionName } ] } ] }) .set({ Authorization: `Bearer ${regionAppKey}`, "Content-Type": "application/json", }) .timeout({ response: 1000 * 60 * 30, deadline: 1000 * 60 * 30, }) const regionType = JSON.parse(region?.body?.choices[0]?.message?.content?.trim()).region; const batterySafetyFactorMap = { '北方地区': 1.1, '南方平原': 1.1, '南方山区': 1.4 }; const tempCorrectionFactorMap = { '北方地区': 1.2, '南方平原': 1.0, '南方山区': 1.1 }; const batterySafetyFactor = batterySafetyFactorMap[regionType]; const tempCorrectionFactor = tempCorrectionFactorMap[regionType]; const deviceList = [ { name: '物位计', model: 'FS-WWJ', power: 0.24, key: '物位计' }, { name: '串联式固定测斜仪', model: 'FS-GGCL01-V1.00', power: 0.36, key: '串联式固定测斜仪' }, { name: '投入式水位计', model: 'FS-TRSW', power: 1.0, key: '投入式水位计' }, { name: '温湿度传感器', model: 'FS-BDS-WSD', power: 0.24, key: '温湿度传感器' }, { name: '雨量计', model: 'FS-FDYLJ', power: 1.2, key: '雨量计' }, { name: '多通道振弦采集仪', model: 'FS-F08', power: 2.56, key: '多通道振弦采集仪' }, { name: '数据采集系统V1.0', model: 'FS-D04', power: 1.7, key: '数据采集系统V1.0' }, { name: 'GPRS无线模块', model: 'FS-DTU-4G-W-V1.00', power: 0.96, key: 'GPRS无线模块' }, { name: '工业级光纤收发器', model: 'SKMSW-02011L', power: 6, key: '工业级光纤收发器' }, { name: '串口服务器', model: '1D(RS485/232)', power: 0.84, key: '串口服务器' }, { name: '工业级交换机', model: '', power: 2.5, 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接收机' }, ]; solarEnergy.powerTitle = '功率统计表'; const powerHeaders = ['序号', '设备名称', '默认型号', '单位', '功率(W)', '数量', '总功率(W)']; const powerRows = []; const unitOverride = { '多通道振弦采集仪|FS-F08': '台(8口)', '数据采集系统V1.0|FS-D04': '台(4口)', }; const getUnit = (name, model) => { const overrideKey = `${name}|${model || ''}`; if (unitOverride[overrideKey]) return unitOverride[overrideKey]; let m = materials.find(m => m.name === name && (model ? m.model === model : true)); if (!m && model) m = materials.find(m => m.model === model); if (!m) m = materials.find(m => m.name === name); return m?.unit || ''; }; let totalPower = 0; for (let i = 0; i < deviceList.length; i++) { const d = deviceList[i]; const qty = Number(request4Params[d.key] || 0); const rowPower = Number(d.power || 0); const rowTotalPower = Number((rowPower * qty).toFixed(6)); totalPower += rowTotalPower; const unit = getUnit(d.name, d.model); powerRows.push([ String(i + 1), d.name, d.model, unit, String(rowPower), String(qty), String(rowTotalPower) ]); } solarEnergy.powerData = { headers: powerHeaders, rows: powerRows }; const days = Number(userText['电力系统(太阳能)']?.['连续阴雨天数'] || 0) || 0; 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 = Number(request4Params['测地型GNSS接收机'] || 0); if (gnssCount > 0) { solarPanelCount += gnssCount; batteryCount += 2 * gnssCount; } solarEnergy.solarEnergyTitle = regionType; const solarHeaders = ['总功率', '日耗电量', '日平均耗电量', '连续阴雨天', '蓄电池容量安全系数', '温度修正系数', '电池容量', '电池数量', '单块太阳能功率', '日平均光照时间', '设备单日耗电量', '太阳能板单日供电量', '太阳能板数量']; const solarRow = [ String(Number(totalPower.toFixed(6))), String(Number(dailyConsumption.toFixed(6))), String(Number(averageDailyConsumption.toFixed(6))), String(days), String(batterySafetyFactor), String(tempCorrectionFactor), String(Number(batteryCapacity.toFixed(6))), String(batteryCount), String(singlePanelPower), String(sunlightHours), String(Number(deviceDailyConsumption.toFixed(6))), String(Number(singlePanelDailySupply.toFixed(6))), String(solarPanelCount) ]; solarEnergy.solarEnergyData = { headers: solarHeaders, rows: [solarRow] }; try { const data = slopePrompts['电力系统(太阳能)'].getManifest([solarPanelCount, batteryCount]) || []; const controllerLoad = (200 / 18) * solarPanelCount; const controllerModel = controllerLoad < 10 ? '12V10A' : controllerLoad < 30 ? '12V30A' : '12V50A'; const controllerCount = Math.ceil((200 * solarPanelCount) / 900); const controllerDesc = '太阳能控制器:根据太阳能控制器选型计算公式从“12V10A、12V30A、12V50A”中选择一个合适的型号。默认太阳能板功率为200,太阳能控制器选型计算公式:(太阳能板功率/18)*太阳能板数量,根据该公式的计算结果选择对应型号:如果计算结果<10,选择太阳能控制器12V10A;如果计算结果>=10且<30选择太阳能控制器12V30A;如果计算结果>=30选择太阳能控制器12V50A。太阳能控制器数量=太阳能板功率*太阳能板数量/900(向上取整)。'; for (let k = 0; k < data.length; k++) { const it = data[k]; if (String(it?.name) === '太阳能控制器') { it.model = controllerModel; it.count = controllerCount; } } request5Data = { data }; } catch (error) { console.error('电力系统(太阳能) calculation failed', error); } } const allListData = request1Data.concat([request2Data, request3Data]); if (request5Data) { allListData.push(request5Data) } // 工期核算清单 const totalCountMap = {}; let xlNum = 0 for (const item of allListData) { if (item && item.data && item.data.length > 0) { for (const sensor of item.data) { const k = sensor.name; const v = Number(sensor.count || 0); if (sensor.name === '接地电缆' || sensor.name === '通信线缆' || sensor.name === '接地线缆') { xlNum += v } totalCountMap[k] = (totalCountMap[k] || 0) + v; } } } const cableSum = Object.entries(totalCountMap).reduce((s, [k, v]) => s + ((String(k).includes('通信电缆') || String(k).includes('接地电缆')) ? Number(v || 0) : 0), 0); 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, '全站仪': 0.3, '钢尺水位计': 2, '一体化裂缝计(成品)': 2.7, '一体化地灾(加速度计)': 4, '一体化地灾倾角计': 4, 'GNSS接收机': 0.9, '4G球机': 2, '管式含水率仪': 4, '分布式节点': 4, '称重系统(1套2车道)': 0.3, '拼接屏(4*6)': 0.3, '太阳能供电': 1, '监控中心(3天)': 3.2, '墩座1': 4, '墩座2': 4, '墩座3': 2.7, '墩座4': 2, '基础1(具备挖机、吊车)': 0.7, '基础2': 2.7, '基础3': 2.7, '基础4': 2, '地网': 1, '线缆(h/100米)': 8.4, '桥架1(h/100米)': 1.3, '桥架2(h/100米)': 1.3, 'PVC管1(h/100米)': 5.3, 'PVC管2(h/100米)': 5.3 }; const inputQtyMap = { '表面式应变计': Number(totalCountMap['表面式应变计'] || 0), '内埋式应变计': Number(totalCountMap['内埋式应变计'] || 0), '钢筋计': Number(totalCountMap['钢筋计'] || 0), '土压力计': Number(totalCountMap['土压力计(单膜)'] || 0), '孔隙水压计': Number(totalCountMap['孔隙水压计'] || 0), '轴力计': Number(totalCountMap['轴力计'] || 0), '锚索计': Number(totalCountMap['锚索计'] || 0), '温度传感器': Number(totalCountMap['温度传感器'] || 0), '温湿度传感器': Number(totalCountMap['温湿度传感器'] || 0), '土壤温湿度传感器': Number(totalCountMap['土壤温湿度传感器'] || 0), '闭环磁通量标定': 0, '开环磁通量绕线(小)': 0, '开环磁通量绕线(中)': 0, '开环磁通量绕线(大)': 0, '人工磁通量温补': 0, '盒式固定测斜仪': Number(totalCountMap['盒式固定测斜仪'] || 0), '串联式固定测斜仪': Number(totalCountMap['串联式固定测斜仪'] || 0), '裂缝计': Number(totalCountMap['裂缝计'] || 0), '静力水准仪': Number(totalCountMap['静力水准仪'] || 0), '物位计': Number(totalCountMap['物位计'] || 0), '雷达液位计': Number(totalCountMap['雷达水位计'] || 0), '拉线位移传感器': Number(totalCountMap['拉线位移传感器'] || 0), '多点位移计(振弦式)': Number(totalCountMap['多点位移计(振弦式)'] || 0), '加速度计': Number(totalCountMap['加速度计'] || 0), '激光测距仪': Number(totalCountMap['激光测距仪'] || 0), '投入式水位计': Number(totalCountMap['投入式水位计'] || 0), '超声波水位计': Number(totalCountMap['超声波水位计'] || 0), '雨量计': Number(totalCountMap['雨量计'] || 0), '超声波风速风向仪': Number(totalCountMap['超声波风速风向仪'] || 0), '光电挠度仪': Number(totalCountMap['光电挠度仪'] || 0), '全站仪': Number(totalCountMap['全站仪'] || 0), '钢尺水位计': Number(totalCountMap['钢尺水位计'] || 0), '一体化裂缝计(成品)': Number(totalCountMap['一体化裂缝计(成品)'] || 0), '一体化地灾(加速度计)': Number(totalCountMap['一体化地灾(加速度计)'] || 0), '一体化地灾倾角计': Number(totalCountMap['一体化地灾倾角计'] || 0), 'GNSS接收机': Number(totalCountMap['测地型GNSS接收机'] || 0), '4G球机': Number(totalCountMap['4G网络球机'] || 0), '管式含水率仪': Number(totalCountMap['管式含水率仪'] || 0), '分布式节点': Number(totalCountMap['分布式节点'] || 0), '称重系统(1套2车道)': 0, '拼接屏(4*6)': 0, '太阳能供电': Number(totalCountMap['太阳能控制器'] || 0), '监控中心(3天)': 0, '墩座1': 0, '墩座2': 0, '墩座3': 0, '墩座4': 0, '基础1(具备挖机、吊车)': 0, '基础2': 0, '基础3': 0, '基础4': 0, '地网': Number(totalCountMap['接地桩(镀锌角铁)'] || 0), '线缆(h/100米)': Number(xlNum || 0), '桥架1(h/100米)': Number(totalCountMap['桥架'] || 0), '桥架2(h/100米)': 0, 'PVC管1(h/100米)': Number(PVCNum || 0), 'PVC管2(h/100米)': 0 }; const sensorsForDebug = [ '表面式应变计', '内埋式应变计', '温度传感器', '温湿度传感器', '盒式固定测斜仪', '裂缝计', '静力水准仪', '拉线位移传感器', '超声波风速风向仪', '光电挠度仪', 'GNSS接收机', '4G球机' ]; const sensorTotal = sensorsForDebug.reduce((sum, k) => sum + Number(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 headers6 = ['序号', '分类', '事项', '耗时(小时/人)', '安装数量/个(2人8小时计)', '备注', '数量输入', '工期(2人天)']; 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米)', '系统调试', '外包(天)', '验收、培训', '项目风险天数(设备转场、开路、技术难点等)', '项目经理(天)' ]; function 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 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', }; function getHoursPerPerson(name) { return hoursPerPersonMap[name] ?? ''; } 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天', }; function getInstallDisplay(name, installQty, qty) { // if (name.endsWith('(h/100米)')) { // const v = Number(((installQty || 0) * (qty || 0) / 100).toFixed(3)); // return String(v); // } 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 ?? ''); } function installRuleText(name) { if (name === '地网') return '安装数量×数量输入÷3'; if (name.endsWith('(h/100米)')) return '安装数量×数量输入÷100'; if (name === '监控中心(3天)') return '固定3天'; if (name === '系统调试') return '总数≤50→2;≤100→3;>100→5'; if (name === '验收、培训') return '总数≤100→3;>100→5'; if (name === '项目风险天数(设备转场、开路、技术难点等)') return '总数≤100→2;>100→3'; return '数量输入÷安装数量'; } function 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(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 rows6 = []; 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); rows6.push([ String(i + 1), getCategory(name), displayName, getHoursPerPerson(name), installDisplay, remarksMap[name] ?? '', (name === '外包(天)' || name === '项目经理(天)') ? '' : String(Number((qty || 0).toFixed(3))), String(durationVal) ]); } const epiboleLimitIdx3 = orderedItems.indexOf('系统调试') + 1; const epiboleDays = rows6.reduce((acc, row) => { const idx = parseInt(row[0], 10); if (isNaN(idx) || idx > epiboleLimitIdx3) return acc; return acc + Number(row[row.length - 1] || 0); }, 0); const managerHoursPerDay = Number(userText['工期核算清单']?.['项目经理每日工时'] || 8); const projectManagerDays = Number((((epiboleDays + acceptanceTrainingDays + projectRiskDays) * 8) / (managerHoursPerDay || 8)).toFixed(6)); const durationData = { headers: headers6, rows: rows6 }; // 核算要点 let hasSolar = false let request7Power = ''; let hasFiber = false; for (const item of allListData) { if (item && item.data && item.data.length > 0) { for (const sensor of item.data) { if (sensor.name.includes('太阳能')) { // request7Power = ',太阳能供电'; hasSolar = true; } if (sensor.name.includes('光纤')) { // request7Power = ',光纤通信'; hasFiber = true; break; } if (hasSolar && hasFiber) { } } } if (hasFiber) break; } const mainPointData = getMainPointData(userText, epiboleDays, hasSolar, hasFiber); let sheet1Data = request1Data; let sheet1Key = request1Key; if (userText['机箱配套']) { sheet1Data.push(request2Data); sheet1Key.push('机箱配套'); } if (userText['通信系统']) { sheet1Data.push(request3Data); sheet1Key.push('通信系统'); } if (request5Data) { sheet1Data.push(request5Data); sheet1Key.push('电力系统(太阳能)'); } /********** 2.生成excel **********/ const tableData = generationXlsxData({ materials, sheet1Data, sheet1Key, solarEnergy: userText['电力系统(太阳能)'] ? solarEnergy : null, duration: { durationData, epiboleDays, projectManagerDays }, mainPointData }); /********** 3.保存数据 **********/ await models.SchemeList.update( { status: 'success', updateAt: new Date(), tableData, }, { where: { id: schemeId } } ); ctx.logger.log(`方案清单数据生成成功,schemeId:${schemeId}`); await transaction.commit(); } catch (error) { await transaction.rollback(); ctx.logger.log(error); try { await models.SchemeList.update( { status: 'fail' }, { where: { id: schemeId } } ); } catch (err) { console.log(`修改状态失败,scheme.id=${schemeId}`); ctx.logger.log(err); } } } /** * 生成excel数据 * @returns {Array} sheets: 所有工作表数据 * [{ name: 'sheet1', data: [], merges: [], // 合并单元格 }] */ function generationXlsxData({ materials = [], sheet1Data, sheet1Key, solarEnergy, duration: { durationData, epiboleDays, projectManagerDays } = {}, mainPointData }) { const tableData = []; // 通用单元格信息 const cellBorder = { top: { style: "thin" }, right: { style: "thin" }, bottom: { style: "thin" }, left: { style: "thin" } }; const alignmentCenter = { horizontal: "center", vertical: "center" }; const commonStyle = { font: { name: '宋体', sz: 11 }, border: cellBorder, alignment: alignmentCenter }; const commonBoldStyle = { font: { name: '宋体', sz: 11, bold: true }, border: cellBorder, alignment: alignmentCenter }; const emptyCell = { v: '', t: 's', s: {} }; const emptyBorderCell = { v: '', t: 's', s: { border: cellBorder } }; /********** sheet 桥梁监测设备清单 **********/ /** * 单元格合并信息 * r: 行索引, c: 列索引, s: 起始位置, e: 结束位置 */ let manifestMerges = [ { s: { r: 0, c: 0 }, e: { r: 1, c: 10 } }, { s: { r: 2, c: 0 }, e: { r: 2, c: 4 } }, { s: { r: 2, c: 5 }, e: { r: 2, c: 10 } }, { s: { r: 3, c: 0 }, e: { r: 3, c: 4 } }, { s: { r: 3, c: 5 }, e: { r: 3, c: 10 } }, { s: { r: 4, c: 0 }, e: { r: 4, c: 4 } }, { s: { r: 4, c: 5 }, e: { r: 4, c: 10 } }, { s: { r: 5, c: 0 }, e: { r: 5, c: 4 } }, { s: { r: 5, c: 5 }, e: { r: 5, c: 10 } }, { s: { r: 6, c: 0 }, e: { r: 6, c: 4 } }, { s: { r: 6, c: 5 }, e: { r: 6, c: 10 } }, ]; // 固定头部 let manifestData = [ [ { v: "江西飞尚科技有限公司 报价单", t: "s", s: { font: { name: '宋体', bold: true, sz: 20 }, border: cellBorder, alignment: { horizontal: "center", vertical: "center" } } }, emptyBorderCell, emptyBorderCell, emptyBorderCell, emptyBorderCell, emptyBorderCell, emptyBorderCell, emptyBorderCell, emptyBorderCell, emptyBorderCell, emptyBorderCell ], new Array(10).fill(emptyBorderCell), ]; for (let i = 0; i < 5; i++) { manifestData.push([ { v: i === 0 ? `TO:` : i === 1 ? `收件人:` : i === 2 ? `电话:` : i === 3 ? `邮件:` : i === 4 ? `项目名称:` : '', t: "s", s: { font: { name: '宋体', bold: true, sz: 12 }, border: cellBorder } }, emptyBorderCell, emptyBorderCell, emptyBorderCell, emptyBorderCell, { v: i === 0 ? `FROM:江西飞尚科技有限公司` : i === 1 ? `报价人:` : i === 2 ? `电话:` : i === 3 ? `邮件: @free-sun.com.cn` : i === 4 ? `报价日期:${moment().format('YYYY')}年 月 日` : '', t: "s", s: { font: { name: '宋体', bold: true, sz: 12 }, border: cellBorder } }, emptyBorderCell, emptyBorderCell, emptyBorderCell, emptyBorderCell, emptyBorderCell, ]); } manifestData.push(['序号', '类型', '名称', '规格/型号', '物料代码', '单位', '单价', '数量', '总价', '线长(传感器定制出厂长度)', '配置说明'].map(title => ({ v: title, t: "s", s: { font: { name: '宋体', bold: true, sz: 11 }, border: cellBorder, } }))); // 数据 for (let i = 0; i < sheet1Data.length; i++) { const title = sheet1Key[i]; const data = sheet1Data[i].data; manifestData.push([{ v: `${i + 1} ${title}`, t: "s", s: { font: { name: '宋体', bold: true, sz: 16, color: { rgb: "FFFFFF" } }, border: cellBorder, alignment: { horizontal: "center", vertical: "center" }, fill: { fgColor: { rgb: "4472c4" } } } }].concat(new Array(9).fill(emptyBorderCell))); manifestMerges.push({ s: { r: manifestData.length - 1, c: 0 }, e: { r: manifestData.length - 1, c: 10 } }); if (!data || data.length === 0) { manifestData.push(new Array(11).fill(emptyCell)); continue; } for (let j = 0; j < data.length; j++) { const item = data[j]; let material = materials .filter(m => { if (title === '索力监测(磁通量)' || title === '索力监测(加速度计)') { return m.factor === '索力监测'; } if (title === '环境温湿度监测' || title === '风速风向监测') { return m.factor === '环境监测'; } return m.factor === title }) .find(m => { if (m.model && item.model) { if (m.name == item.name && m.model === item.model) return true; // name不同,model相同也算匹配 return m.name === item.name && m.model === item.model; } else { return m.name === item.name; } }); if (!material) { // 有些情况ai返回的name是type导致匹配不成功,待解决 material = {}; } manifestData.push([ { v: j + 1, t: "s", s: { font: { name: '宋体', sz: 11 }, border: cellBorder } }, { v: material.type || item.type || '', t: "s", s: { font: { name: '宋体', sz: 11 }, border: cellBorder } }, { v: material.name || item.name, t: "s", s: { font: { name: '宋体', sz: 11 }, border: cellBorder } }, { v: material.model || item.model, t: "s", s: { font: { name: '宋体', sz: 11 }, border: cellBorder } }, { v: material.code || item.code || '', t: "s", s: { font: { name: '宋体', sz: 11 }, border: cellBorder } }, { v: material.unit || item.unit || '', t: "s", s: { font: { name: '宋体', sz: 11 }, border: cellBorder } }, { v: (material.price ?? item.price) ?? '', t: "s", s: { font: { name: '宋体', sz: 11 }, border: cellBorder } }, { v: item.count ?? '', t: "s", s: { font: { name: '宋体', sz: 11 }, border: cellBorder } }, { v: (() => { const p = material.price ?? item.price; return p ? p * item.count : ''; })(), t: "s", s: { font: { name: '宋体', sz: 11 }, border: cellBorder } }, { v: (material.lineLength ?? item.lineLength) ?? '', t: "s", s: { font: { name: '宋体', sz: 11 }, border: cellBorder } }, { // v: material.description || '', v: '', t: "s", s: { font: { name: '宋体', sz: 11 }, border: cellBorder } } ]); } if (data.length > 0) { manifestData.push([{ v: `${data.length + 1}`, t: "s", s: { font: { name: '宋体', sz: 11 }, border: cellBorder } }, { v: '小计', t: "s", s: { font: { name: '宋体', sz: 11 }, border: cellBorder, } }].concat(new Array(9).fill(emptyBorderCell))); manifestMerges.push({ s: { r: manifestData.length - 1, c: 1 }, e: { r: manifestData.length - 1, c: 2 } }); } } // 固定结尾 manifestData.push([{ v: `${sheet1Data.length + 1} 费用总计`, t: "s", s: { font: { name: '宋体', bold: true, sz: 16, color: { rgb: "FFFFFF" } }, border: cellBorder, alignment: { horizontal: "center", vertical: "center" }, fill: { fgColor: { rgb: "4472c4" } } } }].concat(new Array(9).fill(emptyBorderCell))); manifestMerges.push({ s: { r: manifestData.length - 1, c: 0 }, e: { r: manifestData.length - 1, c: 10 } }); const costRows = ['硬件总费用', '软件总费用', '土建费', '机械费用', '系统集成费', '运维费用', '费用总计']; for (let i = 0; i < costRows.length; i++) { manifestData.push([{ v: i + 1, t: "s", s: { font: { name: '宋体', sz: 11 }, border: cellBorder } }, { v: costRows[i], t: "s", s: { font: { name: '宋体', sz: 11 }, border: cellBorder } }].concat(new Array(8).fill(emptyBorderCell))); } const remarksRows = [ '备注:', '1、以上报价含税含运费', ' 2、报价有效期:自报价之日起1个月内有效', ' 3、产品质保期:1年', ' 4、付款方式:签订合同后预付合同总额的40%,货到现场支付合同总额的40%,安装调试完成后支付剩余20 % ', ' 5、交货期:合同签订后30天内' ]; for (let i = 0; i < remarksRows.length; i++) { manifestData.push([{ v: remarksRows[i], t: "s", s: { font: { name: '宋体', sz: 14 }, border: cellBorder } }].concat(new Array(9).fill(emptyBorderCell))); manifestMerges.push({ s: { r: manifestData.length - 1, c: 0 }, e: { r: manifestData.length - 1, c: 10 } }); } // 添加到表格数据 tableData.push({ name: '桥梁监测设备清单', data: manifestData, merges: manifestMerges, cols: [ { wch: 6 }, { wch: 13 }, { wch: 30 }, { wch: 20 }, { wch: 20 }, { wch: 5 }, { wch: 9 }, { wch: 6 }, { wch: 9 }, { wch: 39 }, { wch: 39 }, ] }); /********** sheet 太阳能计算 **********/ if (solarEnergy) { const { solarEnergyTitle = '', solarEnergyData = {}, powerTitle = '', powerData = {}, } = solarEnergy; let solarEnergyCalcMerges = []; let solarEnergyCalcData = [[{ v: solarEnergyTitle, t: "s", s: { font: { name: '宋体', sz: 11 }, alignment: { vertical: 'center', horizontal: 'center' }, fill: { fgColor: { rgb: "ffff00" } }, } }]]; // 太阳能数据表 if (solarEnergyData.headers?.length && solarEnergyData.rows?.length) { solarEnergyCalcData.push(solarEnergyData.headers.map(header => ({ v: header, t: "s", s: { font: { name: '宋体', sz: 11 }, border: cellBorder, alignment: { horizontal: "center", vertical: "center" } } }))); solarEnergyCalcData.push(solarEnergyData.rows[0].map(row => ({ v: row, t: "s", s: { font: { name: '宋体', sz: 11 }, border: cellBorder, alignment: { horizontal: "center", vertical: "center" } } }))); } // 说明 solarEnergyCalcData.push([], [{ v: '说明:太阳能板功率可选,蓄电池和太阳能板太阳能板需冗余,建议乘以1.5的系数,连续阴雨天可根据当地的实际连续阴雨天填写。', t: "s", s: { font: { name: '宋体', sz: 11 }, fill: { fgColor: { rgb: "ffff00" } }, } }], []); solarEnergyCalcMerges.push({ s: { r: solarEnergyCalcData.length - 2, c: 0 }, e: { r: solarEnergyCalcData.length - 2, c: 13 } }); // 功率统计表 solarEnergyCalcData.push([{ v: powerTitle, t: "s", s: { font: { name: '宋体', sz: 16, bold: true }, border: cellBorder, alignment: { vertical: 'center', horizontal: 'center' }, } }].concat(new Array(6).fill(emptyBorderCell))); solarEnergyCalcMerges.push({ s: { r: solarEnergyCalcData.length - 1, c: 0 }, e: { r: solarEnergyCalcData.length - 1, c: 6 } }); if (powerData.headers?.length && powerData.rows?.length) { solarEnergyCalcData.push(powerData.headers.map(header => ({ v: header, t: "s", s: { font: { name: '宋体', sz: 11 }, border: cellBorder, alignment: { horizontal: "center", vertical: "center" } } }))); for (let i = 0; i < powerData.rows.length; i++) { const row = powerData.rows[i]; solarEnergyCalcData.push(row.map(cell => ({ v: cell, t: "s", s: { font: { name: '宋体', sz: 11 }, border: cellBorder, alignment: { horizontal: "center", vertical: "center" } } }))); } } tableData.push({ name: '太阳能计算', data: solarEnergyCalcData, merges: solarEnergyCalcMerges, cols: [ { wch: 12 }, { wch: 20 }, { wch: 24 }, { wch: 12 }, { wch: 24 }, { wch: 15 }, { wch: 15 }, { wch: 20 }, { wch: 20 }, { wch: 15 }, { wch: 20 }, { wch: 15 }, { wch: 15 }, { wch: 15 }, ] }); } /********** sheet 工期核算清单 **********/ let durationMerges = []; let durationTabelData = []; if (durationData.headers?.length && durationData.rows?.length) { durationTabelData.push(durationData.headers.map(header => ({ v: header, t: "s", s: { font: { name: '宋体', sz: 11, bold: true }, border: cellBorder, alignment: { horizontal: "center", vertical: "center" } } }))); for (let i = 0; i < durationData.rows.length; i++) { const row = durationData.rows[i]; if (row[1] === '外包(天)') { row[row.length - 1] = epiboleDays; durationTabelData.push(row.map(cell => ({ v: cell, t: "s", s: { font: { name: '宋体', sz: 11 }, border: cellBorder, alignment: { horizontal: "center", vertical: "center" } } }))); durationTabelData[durationTabelData.length - 1][2].s.alignment.horizontal = 'right'; durationMerges.push({ s: { r: durationTabelData.length - 1, c: 2 }, e: { r: durationTabelData.length - 1, c: 6 } }); } else if (row[1] === '项目经理(天)') { row[row.length - 1] = projectManagerDays; durationTabelData.push(row.map(cell => ({ v: cell, t: "s", s: { font: { name: '宋体', sz: 11 }, border: cellBorder, alignment: { horizontal: "center", vertical: "center" } } }))); durationTabelData[durationTabelData.length - 1][2].s.alignment.horizontal = 'right'; durationMerges.push({ s: { r: durationTabelData.length - 1, c: 2 }, e: { r: durationTabelData.length - 1, c: 6 } }); } else { durationTabelData.push(row.map(cell => ({ v: cell, t: "s", s: { font: { name: '宋体', sz: 11 }, border: cellBorder, alignment: { horizontal: "center", vertical: "center" } } }))); } } } tableData.push({ name: '工期核算清单', data: durationTabelData, merges: durationMerges, cols: [ { wch: 8 }, { wch: 20 }, { wch: 50 }, { wch: 20 }, { wch: 30 }, { wch: 60 }, { wch: 12 }, { wch: 20 }, ] }); /********** sheet 核算要点 **********/ let mainPointTabelData = []; let mainPointMerges = []; const mpBoldCell = (v) => ({ v, t: "s", s: { font: { name: '宋体', sz: 11, bold: true }, border: cellBorder, alignment: alignmentCenter } }); const mpCell = (v) => ({ v, t: "s", s: { font: { name: '宋体', sz: 11 }, border: cellBorder, alignment: alignmentCenter } }); const topHeaderRowIndex = mainPointTabelData.push([ mpBoldCell('明细'), emptyBorderCell, mpBoldCell('具体内容'), emptyBorderCell, emptyBorderCell, mpBoldCell('需明确'), mpBoldCell('常见错误') ]) - 1; mainPointMerges.push({ s: { r: topHeaderRowIndex, c: 0 }, e: { r: topHeaderRowIndex, c: 1 } }); mainPointMerges.push({ s: { r: topHeaderRowIndex, c: 2 }, e: { r: topHeaderRowIndex, c: 4 } }); const topRows = [ ['清单编制人', '', '', ''], ['清单审核人(姓名)', '', '', ''], ['项目特殊环境', '常规', '', ''], ['监测项数量', '', '几个监测项', ''], ]; for (const r of topRows) { const rowIndex = mainPointTabelData.push([ mpCell(r[0]), emptyBorderCell, mpCell(r[1]), emptyBorderCell, emptyBorderCell, mpCell(r[2]), mpCell(r[3]) ]) - 1; mainPointMerges.push({ s: { r: rowIndex, c: 0 }, e: { r: rowIndex, c: 1 } }); if (r[0] === '项目特殊环境' && r[1] === '常规') { mainPointMerges.push({ s: { r: rowIndex, c: 2 }, e: { r: rowIndex, c: 5 } }); } else { mainPointMerges.push({ s: { r: rowIndex, c: 2 }, e: { r: rowIndex, c: 4 } }); } } const basicInfoRows = [ ['', '', '项目名称', ''], ['', '', '客户全称(主要看是否二次销售)', ''], ['', '', '竞争对手情况', ''], ['', '', '项目地点,明确到省市(必填)', ''], ]; let firstBasicInfoRowIndex = null; for (let i = 0; i < basicInfoRows.length; i++) { const r = basicInfoRows[i]; const rowIndex2 = mainPointTabelData.push([ i === 0 ? mpBoldCell('基本信息') : emptyBorderCell, emptyBorderCell, mpCell(r[1]), emptyBorderCell, emptyBorderCell, mpCell(r[2]), mpCell(r[3]) ]) - 1; if (i === 0) firstBasicInfoRowIndex = rowIndex2; mainPointMerges.push({ s: { r: rowIndex2, c: 2 }, e: { r: rowIndex2, c: 4 } }); } if (firstBasicInfoRowIndex != null) { mainPointMerges.push({ s: { r: firstBasicInfoRowIndex, c: 0 }, e: { r: firstBasicInfoRowIndex + basicInfoRows.length - 1, c: 0 } }); } for (let k = 0; k < 4; k++) { mainPointTabelData.push(new Array(7).fill(emptyCell)); } // 数据 if (mainPointData?.headers?.length && mainPointData?.rows?.length) { mainPointTabelData.push(mainPointData.headers.map(header => ({ v: header, t: "s", s: { font: { name: '宋体', sz: 11, bold: true }, border: cellBorder, alignment: { horizontal: "center", vertical: "center" } } }))); for (let i = 0; i < mainPointData.rows.length; i++) { const row = mainPointData.rows[i]; mainPointTabelData.push(row.map(cell => ({ v: cell, t: "s", s: { font: { name: '宋体', sz: 11 }, border: cellBorder, alignment: { horizontal: "center", vertical: "center" } } }))); } } tableData.push({ name: '核算要点', data: mainPointTabelData, merges: mainPointMerges, cols: [ { wch: 50 }, { wch: 40 }, { wch: 15 }, { wch: 24 }, { wch: 12 }, { wch: 60 }, { wch: 15 }, ] }); return tableData; } module.exports.generationXlsxData = generationXlsxData; module.exports.putScheme = async (ctx, next) => { try { const { models } = ctx.app.fs.dc; const schemeId = ctx.params.schemeId; const body = ctx.request.body; if (!schemeId) { throw '缺少参数' }; await models.SchemeList.update( { ...body }, { where: { id: schemeId } } ); ctx.status = 204; } catch (error) { ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error == 'string' ? error : '修改方案失败' }; } } module.exports.delScheme = async (ctx, next) => { try { const { models } = ctx.app.fs.dc; const schemeId = ctx.params.schemeId; if (!schemeId) { throw '缺少参数' }; await models.SchemeList.destroy({ where: { id: schemeId }, }); ctx.status = 204; } catch (error) { ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error == 'string' ? error : '删除方案失败' }; } } module.exports.exportScheme = async (ctx, next) => { try { const { models } = ctx.app.fs.dc; const schemeId = ctx.params.schemeId; if (!schemeId) { throw '缺少参数' }; const scheme = await models.SchemeList.findOne({ where: { id: schemeId }, raw: true, }); if (!scheme) { throw '方案不存在' }; if (scheme.status !== 'success') { throw '方案未完成'; } const wb = XLSXS.utils.book_new(); // 创建工作簿 for (let sheetData of scheme.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; } } // 生成xlsx文件返回 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 : '导出方案失败' }; } } function safeParse(jsonString) { // 将所有的 \* 替换成 * const fixed = jsonString.replace(/\\(?=\*)/g, ''); return JSON.parse(fixed); } function getMainPointData(userText, epiboleDays, hasSolar, hasFiber) { // const hasSolar = (() => { // if (userText && userText['电力系统(太阳能)']) return true; // if (!userText || typeof userText !== 'object') return false; // for (const k of Object.keys(userText)) { // if (String(k).includes('太阳能')) return true; // const v = userText[k]; // if (typeof v === 'string' && v.includes('太阳能')) return true; // if (v && typeof v === 'object') { // for (const kk of Object.keys(v)) { // const vv = v[kk]; // if (typeof vv === 'string' && vv.includes('太阳能')) return true; // } // } // } // return false; // })(); let wbNum = epiboleDays + 2 const electricSource = hasSolar ? '太阳能' : '市电'; const electricLine = hasFiber ? '有线' : '无线'; const headers = ['类别', '明细', '具体内容', '人数/类型', '天数/米数/个数/趟数', '承担方', '需明确']; const rows = [ ['施工成本', '项目经理', '项目经理工资费用', '1', wbNum, '我司', '几个项目经理多长时间'], ['施工成本', '项目经理', '天窗点系数', '1', wbNum, '我司', '考虑天窗时间需要备注天窗点时长;\n结合封路、天窗时长考虑'], ['施工成本', '项目经理', '项目经理住宿及补贴', '1', wbNum, '我司', '食宿由谁承担'], ['施工成本', '项目经理', '差旅', '1', '1', '我司', '来回差旅费用几趟(往返算一趟)'], ['施工成本', '项目经理', '交通(主要指现场用车打车等)', '1', '1', '我司', '现场是否需要用车'], ['施工成本', '机械使用(主要指桥检车及臂长、登高车、脚手架、船只等)', '桁架桥检车 桁架长16m,跨宽2.5m,跨高3.7', '/', '', '我司', '工作量,由谁承担'], ['施工成本', '机械使用(主要指桥检车及臂长、登高车、脚手架、船只等)', '曲臂式登高车 臂长13.5m', '/', '', '我司', '结合封路、天窗时长考虑'], ['施工成本', '机械使用(主要指桥检车及臂长、登高车、脚手架、船只等)', '移动脚手架', '/', '', '我司', '移动脚手架一副尺寸长宽高为2*0.8*1.2,前面写幅数,此处备注写使用天数,且叠加高度不得超过6米'], ['施工成本', '机械使用(主要指桥检车及臂长、登高车、脚手架、船只等)', '小船(渔船类)', '/', '', '我司', ''], ['施工成本', '机械使用(主要指桥检车及臂长、登高车、脚手架、船只等)', '钢管脚手架(立方米)', '/', '', '我司', '写明需要搭设的长宽高的尺寸,计算出立方数后填入\n此项若有,请写明规格等信息,若无写0'], ['施工成本', '土建等', '熔纤', '/', '', '我司', '熔纤次数'], ['施工成本', '土建等', '基坑沙石', '/', '', '我司', '打孔,岩质/土质/工作量,由谁承担,注明是否工勘过且工勘后需尽可能详细的提供地质报告、现场环境情况、是否可以直接机械运上去、取水取电点是否有'], ['施工成本', '土建等', 'GNSS混凝土墩', '/', '', '我司', 'GNSS钢立柱基础:600*600*800mm'], ['施工成本', '土建等', '立杆基础浇筑(2米)', '/', '', '我司', '测斜保护墩:400*400*400mm'], ['施工成本', '土建等', '立杆基础浇筑(4米)', '/', '', '我司', '3米/4米视频立杆:600*600*800mm'], ['施工成本', '土建等', '立杆基础浇筑(6米)', '/', '', '我司', '12米抓拍立杆:按照1800*1800*2000mm计算'], ['施工成本', '土建等', '落地机箱底座', '/', '', '我司', '1号采集箱底座基础:700*450*200mm'], ['施工成本', '外包', '工资', '2', wbNum, '我司', '若有静力水准仪,需间隔3个月进行补液,每次核算1人/次外包差旅'], ['施工成本', '外包', '住宿补贴', '2', wbNum, '我司', ''], ['施工成本', '外包', '差旅', '2', wbNum, '我司', ''], ['施工成本', '外包', '用车', '2', '2', '我司', '来回差旅费用几趟(往返算一趟)'], ['施工成本', '外包', '临时用电', '2', '0', '我司', '几台发电机,使用几天'], ['施工成本', '外包', '其他:机械', '挖掘机 标准斗容量0.1L', '0', '我司', '填写台班数'], ['施工成本', '外包', '交通管制(封路)', '', '', '我司', '封几个车道,封几天'], ['施工成本', '外包高空作业(人员)', '工资', '', '', '我司', '投入几个外包人员,多长工期'], ['施工成本', '外包高空作业(人员)', '住宿补贴', '', '', '我司', ''], ['施工成本', '外包高空作业(人员)', '差旅', '', '', '我司', ''], ['施工成本', '外包高空作业(人员)', '用车', '', '', '我司', '来回差旅费用几趟(往返算一趟)'], ['施工成本', '特种作业(人员)', '工资', '', '', '我司', '明缺什么工种,投入几个多长工期'], ['施工成本', '特种作业(人员)', '住宿补贴', '', '', '我司', ''], ['施工成本', '特种作业(人员)', '差旅', '', '', '我司', ''], ['施工成本', '特种作业(人员)', '用车', '', '', '我司', '来回差旅费用几趟(往返算一趟)'], ['施工成本', '用电', '用电', '/', electricSource, '客户', '用电由谁解决'], ['施工成本', '通讯方式', '通讯方式', '/', electricLine, '客户', '是否涉及光纤铺线'], ['施工成本', '其他', '其他', '/', '', '/', '例如投标等'], ['施工成本', '资金占用', '资金占用', '/', '', '/', '回款方式'], ['软件', '软件', '软件', '我司软件开发', '否', '/', '是否需要软件开发,若是走评估流程,时长软件会传递商务'], ['运维', '项目经理', '项目经理工资费用', '0', '0', '我司', '几个项目经理多长时间'], ['运维', '项目经理', '天窗点系数', '0', '0', '我司', '考虑天窗时间需要备注天窗点时长;\n结合封路、天窗时长考虑'], ['运维', '项目经理', '项目经理住宿及补贴', '0', '0', '我司', '食宿由谁承担'], ['运维', '项目经理', '差旅', '0', '0', '我司', '来回差旅费用几趟(往返算一趟)'], ['运维', '项目经理', '交通(主要指现场用车打车等)', '0', '0', '我司', '现场是否需要用车'], ['运维', '外包', '工资', '0', '0', '我司', ''], ['运维', '外包', '住宿补贴', '0', '0', '我司', ''], ['运维', '外包', '差旅', '0', '0', '我司', ''], ['运维', '外包', '用车', '0', '0', '我司', '来回差旅费用几趟(往返算一趟)'], ['运维机械', '运维机械', '运维机械', '桁架桥检车 桁架长16m,跨宽2.5m,跨高3.7', '', '客户', ''], ['运维交通管制(封路)', '运维交通管制(封路)', '运维交通管制(封路)', '0', '', '客户', ''], ['硬件运维(含质保)', '硬件运维(含质保)', '硬件运维(含质保)', '/', '', '我司', '几年\n1、静力水准仪运维,如客户自行加液,防冻液数量中就不体现(在运维中备注客户自行加液)\n2、如需我们加液,防冻液的数量统计在清单。在运维中备注,防冻液加液去几趟,第一年不算。'], ['软件运维', '软件运维', '软件运维', '/', '', '我司', '几年'], ['风险告知', '风险告知', '风险告知(示例,具体看项目有无进行增减):\n1、取电位置无法明确,涉及后期增加线缆辅材及人工;\n2、工勘过程无法到达,传感器安装位置及走线无法确认;\n3、交通不便,无法确定机械能否到达现场;\n4、青苗费,当地居民索要费用的;\n5、某项工作界限没有明确费用承担方,但又是实施必备项;\n6、施工机械需要客户配合提供,后期无法提供存在风险。\n7、其他涉及项目实施、影响实施成本内容;', '/', '', '/', ''] ]; return { headers, rows }; } module.exports.getMainPointData = getMainPointData;