ai-query对接新版freesun-agent接口的分支
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

3820 lines
183 KiB

'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<Object>} 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;