项目初始化

parents
.idea
/out
/node_modules
/test
/public
/logs
/video
/files
*test*
*.logs
*.zip
/dist
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "启动程序",
"program": "${workspaceFolder}/src/main.ts",
"outFiles": [
"${workspaceFolder}/**/*.js"
]
}
]
}
\ No newline at end of file
{
"compile-hero.disable-compile-files-on-did-save-code": true
}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "screen",
"version": "1.0.0",
"description": "",
"main": "main.ts",
"dependencies": {
"@alicloud/sms-sdk": "^1.1.6",
"@types/node": "^10.12.18",
"bson": "^6.1.0",
"compression": "^1.7.4",
"connect-history-api-fallback": "^2.0.0",
"express": "^4.21.2",
"express-async-handler": "^1.1.4",
"express-history-api-fallback": "^2.2.1",
"formidable": "^1.2.1",
"iconv-lite": "^0.7.0",
"log4js": "^6.6.1",
"lru-cache": "^4.1.5",
"md5": "^2.2.1",
"moment": "^2.24.0",
"mongoose": "^7.6.0",
"mysql": "^2.18.1",
"mysql2": "^3.6.0",
"node-xlsx": "^0.16.1",
"nodemailer": "^6.1.1",
"qs": "^6.11.0",
"request": "^2.88.0",
"sequelize": "^6.32.1",
"ssh2": "^1.17.0",
"svg-captcha": "^1.3.12",
"tencentcloud-sdk-nodejs": "^4.0.562",
"winston": "^3.17.0",
"ws": "^5.2.2",
"xml2js": "^0.4.23",
"xmlrpc": "^1.3.2"
},
"devDependencies": {
"@types/express": "^5.0.0"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "cjj",
"license": "ISC",
"bin": "./out/main.js",
"pkg": {
"scripts": "out/**/*.js",
"assets": [
"public/**/*",
"res/**/*",
"images/**/*",
"video/**/*"
],
"outputPath": "dist"
}
}
<config>
<port>13268</port>
<sign></sign>
<img>http://192.168.0.71:13268</img>
<mysqldb>
<!-- 本地mysql配置 -->
<mysqlHost>127.0.0.1</mysqlHost>
<mysqlPort>3306</mysqlPort>
<mysqlUser>root</mysqlUser>
<mysqlPwd>123456</mysqlPwd>
<dataBase>beermuseumplatform</dataBase>
<!-- 服务器mysql配置 -->
<!-- <mysqlHost>127.0.0.1</mysqlHost>
<mysqlPort>3306</mysqlPort>
<mysqlUser>root</mysqlUser>
<mysqlPwd>qaz123456</mysqlPwd>
<dataBase>unicarbon_platform</dataBase> -->
</mysqldb>
</config>
import { selectOneDataByParam, selectDataListByParam, selectPaginatedDataWithOrder } from "../data/findData";
import { TABLENAME } from "../config/dbEnum";
import { getMySqlMs } from "../tools/systemTools";
import { BizError } from "../util/bizError";
import { ERRORENUM } from "../config/errorEnum";
import { addData } from "../data/addData";
import { updateManyData } from "../data/updateData";
/**
* 注册或更新设备信息
* @param deviceId 设备唯一标识(必填)
* @param regionKey 区域编号(必填),对应 region 表的 id
* @param deviceType 设备类型(必填),如“灯光”“空调”
* @param deviceName 设备名称(可选),如“主照明灯”
* @param controlParams 设备支持的参数定义(可选),JSON对象
* @returns {Promise<{success: boolean}>}
* @throws 如果 regionKey 对应的区域不存在,则抛出错误
*/
export async function registerOrUpdateDevice( deviceId: string, regionKey: number, deviceType: string, deviceName?: string | null, controlParams?: any ) {
// 校验区域是否存在
let region = await selectOneDataByParam(TABLENAME.区域表, { id: regionKey });
if (!region.data) {
throw new BizError(ERRORENUM.参数错误, `区域编号 ${regionKey} 不存在`);
}
let exist = await selectOneDataByParam(TABLENAME.设备表, { device_id: deviceId });
let now = new Date().toISOString().slice(0, 19).replace('T', ' ');
if (Object.keys(exist.data).length) {
await updateManyData(TABLENAME.设备表, { device_id: deviceId }, {
region_key: regionKey,
device_type: deviceType,
device_name: deviceName,
control_params: controlParams || null,
updated_at: now,
});
} else {
await addData(TABLENAME.设备表, {
device_id: deviceId,
region_key: regionKey,
device_type: deviceType,
device_name: deviceName,
control_params: controlParams || null,
created_at: now,
updated_at: now,
});
}
return { success: true };
}
/**
* 处理设备商推送的数据
* @param deviceId 设备唯一标识(必填)
* @param data 设备上报的JSON数据(必填),例如:{ "power": "on", "brightness": 80 }
* @param deviceTime 设备本地时间(可选),格式:YYYY-MM-DD HH:MM:SS
* @returns {Promise<{recordId: number}>} 返回新插入记录的ID
* @throws 如果设备未在 device 表中注册,则抛出错误(需先调用注册接口)
*/
export async function handleDevicePush(deviceId: string, data: any, deviceTime?: string) {
if (!deviceId || !data) {
throw new BizError(ERRORENUM.参数错误, "deviceId和data不能为空");
}
// 检查设备是否存在,不存在则抛出错误(要求先注册)
let device = await selectOneDataByParam(TABLENAME.设备表, { device_id: deviceId });
if (!device.data) {
throw new BizError(ERRORENUM.未找到数据, `设备 ${deviceId} 未注册,请先调用注册接口`);
}
let nowMs = getMySqlMs();
let receivedTime = new Date(nowMs).toISOString().slice(0, 19).replace('T', ' ');
await addData(TABLENAME.设备数据表, {
device_id: deviceId,
data: data,
received_time: receivedTime,
device_time: deviceTime ? new Date(deviceTime).toISOString().slice(0, 19).replace('T', ' ') : null,
created_at: receivedTime,
});
return { success: true };
}
/**
* 处理设备商推送的故障数据
* @param deviceId 设备唯一标识(必填)
* @param faultType 故障类型(必填),如“离线”“数据异常”“硬件故障”
* @param faultDescription 故障详细描述(可选)
* @param occurredTime 故障发生时间(可选),格式:YYYY-MM-DD HH:MM:SS,不传则使用服务器时间
* @returns {Promise<{faultId: number}>} 返回新插入故障记录的ID
* @throws 如果设备未注册,则抛出错误
*/
export async function handleDeviceFaultPush( deviceId: string, faultType: string, faultDescription?: string, occurredTime?: string ) {
if (!deviceId || !faultType) {
throw new BizError(ERRORENUM.参数错误, "deviceId 和 faultType 不能为空");
}
// 检查设备是否存在
let device = await selectOneDataByParam(TABLENAME.设备表, { device_id: deviceId });
if (!device.data) {
throw new BizError(ERRORENUM.未找到数据, `设备 ${deviceId} 未注册,请先调用注册接口`);
}
let nowMs = getMySqlMs();
let nowStr = new Date(nowMs).toISOString().slice(0, 19).replace('T', ' ');
let faultTime = occurredTime
? new Date(occurredTime).toISOString().slice(0, 19).replace('T', ' ')
: nowStr;
let insertResult: any = await addData(TABLENAME.设备故障表, {
device_id: deviceId,
fault_type: faultType,
fault_description: faultDescription || null,
occurred_time: faultTime,
status: 0, // 0=未处理
resolved_time: null,
created_at: nowStr,
updated_at: nowStr,
});
return { faultId: insertResult.insertId, success: true };
}
/**
* 运行分析 todu(待测试)
* @returns 大屏所需的所有指标数据
*/
export async function getRunAnalysis() {
// 1. 获取所有三相电表及电能监测设备的用电数据(用于能耗管理)
let energyDevices = await selectDataListByParam(TABLENAME.设备表, {
device_type: { "%in%": ["三相电表", "电能监测"] } // 使用 %in% 操作符
}, ["device_id"]);
let energyDeviceIds = energyDevices.data.map((d: any) => d.device_id);
// 2. 获取今日、昨日、本月、上月用电量
let todayStart = new Date(); todayStart.setHours(0, 0, 0, 0);
let todayEnd = new Date();
let yesterdayStart = new Date(Date.now() - 86400000); yesterdayStart.setHours(0,0,0,0);
let yesterdayEnd = new Date(Date.now() - 86400000); yesterdayEnd.setHours(23,59,59,999);
let currentMonthStart = new Date(); currentMonthStart.setDate(1); currentMonthStart.setHours(0,0,0,0);
let lastMonthStart = new Date(); lastMonthStart.setMonth(lastMonthStart.getMonth() - 1); lastMonthStart.setDate(1); lastMonthStart.setHours(0,0,0,0);
let lastMonthEnd = new Date(currentMonthStart.getTime() - 1);
// 辅助函数:查询某个时间段内的总用电量(单位 kwh)
async function getTotalEnergy(deviceIds: string[], startTime: Date, endTime: Date): Promise<number> {
if (deviceIds.length === 0) return 0;
let records = await selectDataListByParam(TABLENAME.设备数据表, {
device_id: { "%in%": deviceIds },
received_time: { "%gte%": startTime.toISOString().slice(0,19).replace('T',' '), "%lte%": endTime.toISOString().slice(0,19).replace('T',' ') },
"%orderAsc%": "received_time"
}, ["device_id", "data", "received_time"]);
let total = 0;
let deviceEnergyMap = new Map();
for (let rec of records.data) {
let data = rec.data;
let energy = data.energy ?? data.total_energy;
if (energy === undefined) continue;
if (!deviceEnergyMap.has(rec.device_id)) {
deviceEnergyMap.set(rec.device_id, { min: energy, max: energy });
} else {
let cur = deviceEnergyMap.get(rec.device_id);
if (energy < cur.min) cur.min = energy;
if (energy > cur.max) cur.max = energy;
}
}
for (let { min, max } of deviceEnergyMap.values()) {
total += (max - min);
}
return total;
}
let todayEnergy = await getTotalEnergy(energyDeviceIds, todayStart, todayEnd);
let yesterdayEnergy = await getTotalEnergy(energyDeviceIds, yesterdayStart, yesterdayEnd);
let currentMonthEnergy = await getTotalEnergy(energyDeviceIds, currentMonthStart, new Date());
let previousMonthEnergy = await getTotalEnergy(energyDeviceIds, lastMonthStart, lastMonthEnd);
let dailyYearOnYear = yesterdayEnergy === 0 ? '0%' : `${(((todayEnergy - yesterdayEnergy) / yesterdayEnergy) * 100).toFixed(2)}%`;
let monthlyYearOnYear = previousMonthEnergy === 0 ? '0%' : `${(((currentMonthEnergy - previousMonthEnergy) / previousMonthEnergy) * 100).toFixed(2)}%`;
// 3. 用电趋势:过去24小时(按小时)、过去7天、过去30天、过去一年(按月)
let now = new Date();
let last24hStart = new Date(now.getTime() - 24 * 3600000);
let hourlyEnergy = await getEnergyTrend(energyDeviceIds, last24hStart, now, 'hour');
let last7DaysStart = new Date(now.getTime() - 7 * 86400000);
let dailyEnergy7 = await getEnergyTrend(energyDeviceIds, last7DaysStart, now, 'day');
let last30DaysStart = new Date(now.getTime() - 30 * 86400000);
let dailyEnergy30 = await getEnergyTrend(energyDeviceIds, last30DaysStart, now, 'day');
let lastYearStart = new Date(now.getFullYear() - 1, now.getMonth(), 1);
let monthlyEnergyYear = await getEnergyTrend(energyDeviceIds, lastYearStart, now, 'month');
// 4. 设备表格:按设备类型统计数量和耗电情况
let allDevices = await selectDataListByParam(TABLENAME.设备表, {}, ["device_type", "device_id"]);
let typeCountMap = new Map();
for (let dev of allDevices.data) {
let type = dev.device_type;
typeCountMap.set(type, (typeCountMap.get(type) || 0) + 1);
}
let deviceTable = [];
for (let [type, count] of typeCountMap.entries()) {
// 耗电比例示例(实际可关联用电数据)
let powerRatio = '30%';
if (type === '空调') powerRatio = '97%';
else if (type === '灯光') powerRatio = '45%';
deviceTable.push({
deviceName: type,
quantity: count,
powerConsumptionRatio: powerRatio
});
}
// 5. 汇总数据(简化)
let totalDevices = allDevices.data.length;
let runningDevices = totalDevices; // 或根据在线数量计算,在线即运行
// 查询未解决的故障设备数量(去重,一个设备可能有多个故障,取 status != 2 的)
let faultRecords = await selectDataListByParam(
TABLENAME.设备故障表,
{ status: { "%ne%": 2 } }, // 未解决的状态不是2(已解决)
["device_id"]
);
let faultDeviceIds = new Set(faultRecords.data.map((r: any) => r.device_id));
let faultDevices = faultDeviceIds.size;
let normalRate = totalDevices === 0 ? '100%' : (((totalDevices - faultDevices) / totalDevices) * 100).toFixed(2) + '%';
// 6. 设备监测情况:在线/离线(根据最后数据时间是否超过30分钟)
let nowTime = new Date();
let deviceLatestMap = new Map();
let deviceIds = allDevices.data.map(d => d.device_id);
for (let devId of deviceIds) {
let rec = await selectDataListByParam(TABLENAME.设备数据表,
{ device_id: devId, "%orderDesc%": "received_time", "%limit%": 1 },
["received_time"]);
if (rec.data.length) {
let lastTime = new Date(rec.data[0].received_time);
let diffMinutes = (nowTime.getTime() - lastTime.getTime()) / 60000;
deviceLatestMap.set(devId, { lastTime, isOnline: diffMinutes <= 30 });
} else {
deviceLatestMap.set(devId, { lastTime: null, isOnline: false });
}
}
let onlineCount = Array.from(deviceLatestMap.values()).filter(v => v.isOnline).length;
let offlineCount = totalDevices - onlineCount;
let faultCount = faultRecords.data.length; // 所有未解决的故障单数量(含同一设备多个故障)
// 7. 监测设备趋势:过去24小时每小时在线数量
let hourlyOnlineTrend = [];
for (let i = 0; i < 24; i++) {
let hourStart = new Date(nowTime.getTime() - (23 - i) * 3600000);
let hourEnd = new Date(hourStart.getTime() + 3600000);
let onlineInHour = 0;
for (let { lastTime } of deviceLatestMap.values()) {
if (lastTime && lastTime >= hourStart && lastTime <= hourEnd) onlineInHour++;
}
hourlyOnlineTrend.push({ time: `${i}时`, value: onlineInHour.toString() });
}
// 8. 预警处理状态和预警工单(模拟数据,待接入真实告警表)
let alertStatus = {
totalAlerts: 48,
resolved: 20,
responded: 24,
unresolved: 4
};
let alertWorkOrders = [
{ riskContent: "空调压缩机异常", alertTime: "26/02/01", status: "已解决" },
{ riskContent: "温度传感器离线", alertTime: "26/02/01", status: "已响应" },
{ riskContent: "湿度超标", alertTime: "26/02/02", status: "未处理" }
];
// 9. 空气质量相关数据
let airQualityDevices = await selectDataListByParam(TABLENAME.设备表, { device_type: "空气质量传感" }, ["device_id"]);
let airDeviceIds = airQualityDevices.data.map(d => d.device_id);
let qualityIndex = 300;
let co2Trend = [];
let currentCo2 = '21ppm';
let currentTemperature = '21℃';
let temperatureTrend = [];
let currentHumidity = '47%';
let humidityTrend = [];
let currentPm25 = '21μg/m³';
let pm25Trend = [];
let co2TrendDetail = [];
if (airDeviceIds.length) {
// 最新数据
let latestAir = await selectDataListByParam(TABLENAME.设备数据表, {
device_id: { "%in%": airDeviceIds },
"%orderDesc%": "received_time",
"%limit%": 1
}, ["data"]);
if (latestAir.data.length) {
let airData = latestAir.data[0].data;
currentTemperature = `${airData.temperature ?? 21}`;
currentHumidity = `${airData.humidity ?? 47}%`;
currentPm25 = `${airData.pm25 ?? 21}μg/m³`;
currentCo2 = `${airData.co2 ?? 400}ppm`;
qualityIndex = 500 - (airData.pm25 ?? 0);
}
// 过去24小时趋势
let last24h = new Date(nowTime.getTime() - 24*3600000);
let airHistory = await selectDataListByParam(TABLENAME.设备数据表, {
device_id: { "%in%": airDeviceIds },
received_time: { "%gte%": last24h.toISOString().slice(0,19).replace('T',' ') }
}, ["data", "received_time"]);
let hourlyData = new Map();
for (let rec of airHistory.data) {
let d = new Date(rec.received_time);
let hourKey = `${d.getHours()}`;
let data = rec.data;
if (!hourlyData.has(hourKey)) {
hourlyData.set(hourKey, { temp: data.temperature, hum: data.humidity, pm: data.pm25, co2: data.co2 });
}
}
for (let i = 0; i < 24; i++) {
let key = `${i}`;
let val = hourlyData.get(key);
temperatureTrend.push({ time: `${i}:00`, value: val?.temp?.toString() ?? '21' });
humidityTrend.push({ time: `${i}:00`, value: val?.hum?.toString() ?? '47' });
pm25Trend.push({ time: `${i}:00`, value: val?.pm?.toString() ?? '21' });
let co2Val = val?.co2 ?? 400;
co2TrendDetail.push({ time: `${i}:00`, value: (co2Val / 100).toFixed(1) });
co2Trend.push({ time: `${i}`, value: co2Val.toString() });
}
}
// 10. 机房环境监控和功率监控
let roomEnvironment = [
{ key: "温度", value: currentTemperature },
{ key: "湿度", value: currentHumidity },
{ key: "配置", value: "烟感" },
{ key: "常开", value: "水浸" }
];
let roomPowerTrend = hourlyOnlineTrend.slice(0, 24).map(item => ({ time: item.time.replace('时',':00'), value: item.value }));
// 最终组装返回数据
return {
energyManagement: {
todayElectricity: `${todayEnergy.toFixed(0)}kwh`,
yesterdayElectricity: `${yesterdayEnergy.toFixed(0)}kwh`,
dailyYearOnYear,
currentMonthElectricity: `${currentMonthEnergy.toFixed(0)}kwh`,
previousMonthElectricity: `${previousMonthEnergy.toFixed(0)}kwh`,
monthlyYearOnYear
},
electricityTrend: {
last24Hours: hourlyEnergy,
last7Days: dailyEnergy7,
last30Days: dailyEnergy30,
lastYear: monthlyEnergyYear
},
deviceTable,
summaryData: {
runningDevices,
normalRate,
faultDevices
},
deviceMonitoring: {
totalMonitored: totalDevices,
offlineCount,
onlineCount,
faultCount
},
monitoringDeviceTrend: hourlyOnlineTrend,
alertStatus,
alertWorkOrders,
qualityIndex,
co2Trend,
currentTemperature,
temperatureTrend,
currentHumidity,
humidityTrend,
currentPm25,
pm25Trend,
currentCo2,
co2TrendDetail,
roomEnvironment,
roomPowerTrend
};
}
// 辅助函数:获取用电趋势(按间隔分组)
async function getEnergyTrend(deviceIds: string[], startTime: Date, endTime: Date, granularity: 'hour' | 'day' | 'month') {
if (deviceIds.length === 0) return [];
let records = await selectDataListByParam(TABLENAME.设备数据表, {
device_id: { "%in%": deviceIds },
received_time: { "%gte%": startTime.toISOString().slice(0,19).replace('T',' '), "%lte%": endTime.toISOString().slice(0,19).replace('T',' ') },
"%orderAsc%": "received_time"
}, ["device_id", "data", "received_time"]);
let intervalMap = new Map(); // key: 时间字符串, value: { deviceMin, deviceMax }
for (let rec of records.data) {
let data = rec.data;
let energy = data.energy ?? data.total_energy;
if (energy === undefined) continue;
let intervalKey: string;
let d = new Date(rec.received_time);
if (granularity === 'hour') {
intervalKey = `${d.getFullYear()}-${d.getMonth()+1}-${d.getDate()} ${d.getHours()}`;
} else if (granularity === 'day') {
intervalKey = `${d.getFullYear()}/${d.getMonth()+1}/${d.getDate()}`;
} else {
intervalKey = `${d.getFullYear()}-${d.getMonth()+1}`;
}
if (!intervalMap.has(intervalKey)) {
intervalMap.set(intervalKey, { deviceMin: new Map(), deviceMax: new Map() });
}
let entry = intervalMap.get(intervalKey);
if (!entry.deviceMin.has(rec.device_id)) {
entry.deviceMin.set(rec.device_id, energy);
entry.deviceMax.set(rec.device_id, energy);
} else {
let curMin = entry.deviceMin.get(rec.device_id);
let curMax = entry.deviceMax.get(rec.device_id);
if (energy < curMin) entry.deviceMin.set(rec.device_id, energy);
if (energy > curMax) entry.deviceMax.set(rec.device_id, energy);
}
}
let result = [];
for (let [key, { deviceMin, deviceMax }] of intervalMap.entries()) {
let total = 0;
for (let [deviceId, minVal] of deviceMin.entries()) {
let maxVal = deviceMax.get(deviceId);
total += (maxVal - minVal);
}
result.push({ key, value: total.toFixed(0) });
}
result.sort((a,b) => a.key.localeCompare(b.key));
return result;
}
/**
* 设备类型中文到英文Key的映射
*/
let deviceTypeKeyMap: { [key: string]: string } = {
"灯光": "lighting",
"窗帘": "curtain",
"推窗器": "windowOpener",
"空调": "hvac",
"新风": "freshAir",
"音乐": "music",
"人体传感": "occupancySensor",
"人体传感器": "occupancySensor",
"空气质量传感": "airSensor",
"燃气表": "gasMeter",
"水表": "waterMeter",
"三相电表": "electricityMeter",
"电表": "electricityMeter",
"电能监测": "electricityMeter"
};
// 辅助函数:将字符串或 Date 转换为 Date 对象
function toDate(dateInput: string | Date): Date {
if (dateInput instanceof Date) return dateInput;
return new Date(dateInput);
}
// 辅助函数:将 Date 对象转换为数据库存储的字符串格式 YYYY-MM-DD HH:MM:SS
function toDateTimeStr(date: Date): string {
return date.toISOString().slice(0, 19).replace('T', ' ');
}
/**
* 获取运行分析区域弹窗
*/
export async function getRunAnalysisPop(regionKey: number) {
let devices = await selectDataListByParam(
TABLENAME.设备表,
{ region_key: regionKey },
["device_id", "device_type", "device_name"]
);
let deviceList = devices.data;
let deviceIds = deviceList.map((d: any) => d.device_id);
if (deviceIds.length === 0) {
return {
deviceOnlineStatus: {},
deviceStatusMonitoring: { totalDevices: "0", online: "0", offline: "0", fault: "0" },
deviceStatusTrend: [],
energyConsumptionMonitoring: []
};
}
// 获取每个设备的最新数据(用于判定业务在线状态)
let deviceLatestMap = new Map<string, { isOnline: boolean; latestData: any }>();
for (let devId of deviceIds) {
let rec = await selectDataListByParam(
TABLENAME.设备数据表,
{ device_id: devId, "%orderDesc%": "received_time", "%limit%": 1 },
["data"]
);
let isOnline = false;
let latestData = null;
if (rec.data.length) {
latestData = rec.data[0].data // 已解析的对象
let devType = deviceList.find((d: any) => d.device_id === devId)?.device_type || "";
isOnline = isDeviceOnlineByStatus(devType, latestData);
}
deviceLatestMap.set(devId, { isOnline, latestData });
}
// 设备在线状态(按类型聚合)
let deviceOnlineStatus: { [key: string]: boolean } = {};
for (let dev of deviceList) {
let cnType = dev.device_type;
let enKey = deviceTypeKeyMap[cnType];
if (enKey) {
let isOnline = deviceLatestMap.get(dev.device_id)?.isOnline ?? false;
if (deviceOnlineStatus[enKey] === undefined) {
deviceOnlineStatus[enKey] = isOnline;
} else {
deviceOnlineStatus[enKey] = deviceOnlineStatus[enKey] || isOnline;
}
}
}
let totalDevices = deviceIds.length;
let onlineCount = Array.from(deviceLatestMap.values()).filter(v => v.isOnline).length;
let offlineCount = totalDevices - onlineCount;
// 故障设备数量
let faultRecords = await selectDataListByParam(
TABLENAME.设备故障表,
{ device_id: { "%in%": deviceIds }, status: { "%ne%": 2 } },
["device_id"]
);
let faultDeviceIds = new Set(faultRecords.data.map((r: any) => r.device_id));
let faultCount = faultDeviceIds.size;
let nowStr = getNowStr();
let hourlyStatusTrend = await getDeviceStatusTrend(deviceList, nowStr);
let hourlyEnergyTrend = await getEnergyTrendByRegion(deviceIds, nowStr);
return {
deviceOnlineStatus,
deviceStatusMonitoring: {
totalDevices: totalDevices.toString(),
online: onlineCount.toString(),
offline: offlineCount.toString(),
fault: faultCount.toString()
},
deviceStatusTrend: hourlyStatusTrend,
energyConsumptionMonitoring: hourlyEnergyTrend
};
}
// 辅助函数:将 Date 转换为本地时间字符串 YYYY-MM-DD HH:MM:SS
function toLocalDateTimeStr(date: Date): string {
let year = date.getFullYear();
let month = String(date.getMonth() + 1).padStart(2, '0');
let day = String(date.getDate()).padStart(2, '0');
let hours = String(date.getHours()).padStart(2, '0');
let minutes = String(date.getMinutes()).padStart(2, '0');
let seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
// 获取当前本地时间字符串
function getNowStr(): string {
return toLocalDateTimeStr(new Date());
}
/**
* 获取过去24小时内每小时的设备在线统计(基于业务状态)
* @param deviceList 设备列表
* @param nowStr 当前时间字符串(本地时间)
* @returns 数组,从最早到最晚,每个元素包含 key(整点小时)、online、offline
*/
async function getDeviceStatusTrend(
deviceList: any[],
nowStr: string
) {
let deviceIds = deviceList.map((d: any) => d.device_id);
let totalDevices = deviceIds.length;
let now = new Date(nowStr);
// 计算起始时间:24小时前,并取整到小时(向下取整)
let startDate = new Date(now.getTime() - 24 * 3600000);
startDate.setMinutes(0, 0, 0);
// 结束时间:当前小时整点(不含当前小时区间,因为未结束)
let endDate = new Date(now);
endDate.setMinutes(0, 0, 0);
// 生成小时区间列表(从 startDate 开始,每小时递增,直到 < endDate)
let hourRanges: { start: Date; end: Date; key: string }[] = [];
let current = new Date(startDate);
while (current < endDate) {
let next = new Date(current.getTime() + 3600000);
hourRanges.push({
start: current,
end: next,
key: `${current.getHours()}:00`
});
current = next;
}
// 查询所有设备在时间范围内的数据
let startStr = toLocalDateTimeStr(startDate);
let endStr = toLocalDateTimeStr(now); // 使用当前时间作为结束,但实际查询时用 < endStr
let records = await selectDataListByParam(
TABLENAME.设备数据表,
{
device_id: { "%in%": deviceIds },
received_time: { "%gte%": startStr, "%lte%": endStr },
"%orderAsc%": "received_time"
},
["device_id", "data", "received_time"]
);
// 按设备分组
let deviceDataMap = new Map<string, Array<{ timeStr: string; data: any }>>();
for (let rec of records.data) {
if (!deviceDataMap.has(rec.device_id)) {
deviceDataMap.set(rec.device_id, []);
}
deviceDataMap.get(rec.device_id)!.push({
timeStr: rec.received_time,
data: rec.data
});
}
// 对每个小时区间统计在线设备数
let result = [];
for (let range of hourRanges) {
let startStrRange = toLocalDateTimeStr(range.start);
let endStrRange = toLocalDateTimeStr(range.end);
let onlineCount = 0;
for (let dev of deviceList) {
let devId = dev.device_id;
let devType = dev.device_type;
let dataPoints = deviceDataMap.get(devId) || [];
// 查找该小时内最后一条数据
let lastInRange: { timeStr: string; data: any } | null = null;
for (let point of dataPoints) {
if (point.timeStr >= startStrRange && point.timeStr < endStrRange) {
lastInRange = point;
}
}
if (lastInRange && isDeviceOnlineByStatus(devType, lastInRange.data)) {
onlineCount++;
}
}
result.push({
key: range.key,
online: onlineCount,
offline: totalDevices - onlineCount,
fault: 0
});
}
return result;
}
/**
* 获取过去24小时每小时的能耗趋势
* @param deviceIds 设备ID列表
* @param nowStr 当前时间字符串
*/
async function getEnergyTrendByRegion(deviceIds: string[], nowStr: string) {
let nowTime = toDate(nowStr).getTime();
let startTime = new Date(nowTime - 24 * 3600000);
let startStr = toDateTimeStr(startTime);
let records = await selectDataListByParam(
TABLENAME.设备数据表,
{
device_id: { "%in%": deviceIds },
received_time: { "%gte%": startStr },
"%orderAsc%": "received_time"
},
["device_id", "data", "received_time"]
);
// 按设备分组提取能耗值
let deviceEnergySeries = new Map<string, Array<{ timestamp: number; energy: number }>>();
for (let rec of records.data) {
let dataObj = rec.data;
let energy = dataObj.energy ?? dataObj.total_energy;
if (energy === undefined) continue;
let ts = toDate(rec.received_time).getTime();
if (!deviceEnergySeries.has(rec.device_id)) {
deviceEnergySeries.set(rec.device_id, []);
}
deviceEnergySeries.get(rec.device_id)!.push({ timestamp: ts, energy });
}
let hourlyEnergy = new Array(24).fill(0);
for (let i = 0; i < 24; i++) {
let hourStart = new Date(nowTime - (23 - i) * 3600000);
let hourEnd = new Date(hourStart.getTime() + 3600000);
let startTs = hourStart.getTime();
let endTs = hourEnd.getTime();
for (let series of deviceEnergySeries.values()) {
let firstInHour: { timestamp: number; energy: number } | null = null;
let lastInHour: { timestamp: number; energy: number } | null = null;
for (let point of series) {
if (point.timestamp >= startTs && point.timestamp < endTs) {
if (!firstInHour || point.timestamp < firstInHour.timestamp) firstInHour = point;
if (!lastInHour || point.timestamp > lastInHour.timestamp) lastInHour = point;
}
}
if (firstInHour && lastInHour && lastInHour.energy > firstInHour.energy) {
hourlyEnergy[i] += (lastInHour.energy - firstInHour.energy);
}
}
}
let result = [];
for (let i = 0; i < 24; i++) {
let hourPoint = new Date(nowTime - (23 - i) * 3600000);
let hourKey = `${hourPoint.getHours()}:00`;
result.push({ key: hourKey, energy: Number(hourlyEnergy[i].toFixed(2)) });
}
return result;
}
function isDeviceOnlineByStatus(deviceType: string, latestData: any): boolean {
if (!latestData) return false;
switch (deviceType) {
case "灯光":
case "空调":
case "新风":
case "音乐":
case "水景":
case "雾森":
case "灌溉":
case "驱蚊":
case "增压泵":
case "智慧庭院-水景": // 兼容旧数据
case "智慧庭院-雾森": // 兼容旧数据
case "智慧庭院-灌溉": // 兼容旧数据
case "智慧庭院-驱蚊": // 兼容旧数据
case "智慧庭院-增压泵": // 兼容旧数据
return latestData.power === "on" || latestData.power === true;
case "窗帘":
case "推窗器":
return latestData.action !== undefined && latestData.action !== null;
case "人体传感":
case "人体传感器":
return true;
default:
// 空气质量传感、土壤传感器、气象设备、电能监测等默认只要有数据即视为激活
return true;
}
}
/**
* 获取智能监控弹窗数据
* @param regionKey 区域编号(region.id)
* @returns 设备在线状态、空气质量监测、设备监测详情
*/
export async function getSmartMonitorPop(regionKey: number) {
// 1. 获取该区域下所有设备
let devices = await selectDataListByParam(
TABLENAME.设备表,
{ region_key: regionKey },
["device_id", "device_type", "device_name"]
);
let deviceList = devices.data;
if (deviceList.length === 0) {
return {
deviceOnlineStatus: {},
airQualityMonitoring: {
airQualityLevel: "无数据",
temperature: "--",
humidity: "--",
pm25: "--",
co2: "--",
tvoc: "--"
},
deviceMonitoring: {}
};
}
// 2. 获取每个设备的最新数据(按设备ID分组取最新一条)
let deviceIds = deviceList.map(d => d.device_id);
let latestDataMap = new Map<string, any>(); // key: device_id, value: { data, received_time }
for (let devId of deviceIds) {
let result = await selectDataListByParam(
TABLENAME.设备数据表,
{ device_id: devId, "%orderDesc%": "received_time", "%limit%": 1 },
["data", "received_time"]
);
if (result.data.length > 0) {
latestDataMap.set(devId, {
data: JSON.parse(result.data[0].data),
received_time: result.data[0].received_time
});
}
}
let nowTime = new Date();
let deviceOnlineStatus: { [key: string]: boolean } = {};
let deviceMonitoring: { [key: string]: any } = {};
// 辅助:判断在线状态(30分钟内有数据视为在线)
function isOnline(receivedTime: string | undefined): boolean {
if (!receivedTime) return false;
let lastTime = new Date(receivedTime);
let diffMinutes = (nowTime.getTime() - lastTime.getTime()) / 60000;
return diffMinutes <= 30;
}
// 遍历设备列表,按类型聚合
for (let dev of deviceList) {
let cnType = dev.device_type;
let enKey = deviceTypeKeyMap[cnType];
if (!enKey) continue; // 跳过未映射的设备类型
let latest = latestDataMap.get(dev.device_id);
let online = isOnline(latest?.received_time);
// 设备在线状态:同类型只要有一个在线即为 true(按业务需求可调整)
if (deviceOnlineStatus[enKey] === undefined) {
deviceOnlineStatus[enKey] = online;
} else {
deviceOnlineStatus[enKey] = deviceOnlineStatus[enKey] || online;
}
// 设备监测详情:每种类型只保留最新一个设备的数据(若同类型多设备,可改为取最后上报的设备)
if (!deviceMonitoring[enKey] && latest) {
let data = latest.data;
switch (cnType) {
case "灯光":
deviceMonitoring[enKey] = {
power: data.power === true || data.power === "on",
brightness: data.brightness ?? 0,
colorTemperature: data.colorTemperature ?? 3000
};
break;
case "窗帘":
case "推窗器":
let position = data.position ?? 0;
deviceMonitoring[enKey] = {
position: position,
action: position >= 50 ? "open" : "close"
};
break;
case "空调":
deviceMonitoring[enKey] = {
power: data.power === true || data.power === "on",
targetTemperature: data.targetTemperature ?? 22.0,
roomTemperature: data.roomTemperature ?? 21.0,
mode: data.mode ?? "auto",
fanSpeed: data.fanSpeed ?? "auto"
};
break;
case "新风":
deviceMonitoring[enKey] = {
power: data.power === true || data.power === "on",
fanSpeed: data.fanSpeed ?? "auto"
};
break;
case "音乐":
deviceMonitoring[enKey] = {
power: data.power === true || data.power === "on",
volume: data.volume ?? 0
};
break;
case "人体传感":
case "人体传感器":
deviceMonitoring[enKey] = {
occupied: data.occupied === true
};
break;
// 空气质量传感、电表等不在 deviceMonitoring 中展示,跳过
default:
break;
}
}
}
// 3. 空气质量监测数据(从空气质量传感设备获取)
let airQualityMonitoring = {
airQualityLevel: "无数据",
temperature: "--",
humidity: "--",
pm25: "--",
co2: "--",
tvoc: "--"
};
let airDevices = deviceList.filter(d => d.device_type === "空气质量传感");
for (let airDev of airDevices) {
let latestAir = latestDataMap.get(airDev.device_id);
if (latestAir) {
let data = latestAir.data;
let temperature = data.temperature !== undefined ? `${data.temperature}℃` : undefined;
let humidity = data.humidity !== undefined ? `${data.humidity}%` : undefined;
let pm25 = data.pm25 !== undefined ? `${data.pm25}μg/m³` : undefined;
let co2 = data.co2 !== undefined ? `${data.co2}ppm` : undefined;
let tvoc = data.tvoc !== undefined ? `${data.tvoc}` : "0";
// 空气质量等级:根据 PM2.5 简单划分(也可根据 AQI)
let level = "优";
if (pm25) {
let val = data.pm25;
if (val <= 35) level = "优";
else if (val <= 75) level = "良";
else if (val <= 115) level = "轻度";
else if (val <= 150) level = "中度";
else level = "重度";
}
airQualityMonitoring = {
airQualityLevel: level,
temperature: temperature ?? "--",
humidity: humidity ?? "--",
pm25: pm25 ?? "--",
co2: co2 ?? "--",
tvoc: tvoc
};
break; // 取第一个空气质量设备的数据
}
}
return {
deviceOnlineStatus,
airQualityMonitoring,
deviceMonitoring
};
}
/**
* 获取所有区域列表(供前端下拉选择)
* @returns 区域列表,按 sort_order 排序
*/
export async function getRegionList() {
let result = await selectDataListByParam(
TABLENAME.区域表,
{ "%orderAsc%": "sort_order" },
["id", "name", "sort_order"]
);
return {
list: result.data.map((r: any) => ({
regionKey: r.id,
regionName: r.name,
sortOrder: r.sort_order,
})),
};
}
import { selectOneDataByParam, selectDataListByParam, selectPaginatedDataWithOrder } from "../data/findData";
import { TABLENAME } from "../config/dbEnum";
import { getMySqlMs } from "../tools/systemTools";
import { BizError } from "../util/bizError";
import { ERRORENUM } from "../config/errorEnum";
import { addData } from "../data/addData";
import { updateManyData } from "../data/updateData";
/**
* 注册或更新设备信息
* @param deviceId 设备唯一标识(必填)
* @param regionKey 区域编号(必填),对应 region 表的 id
* @param deviceType 设备类型(必填),如“灯光”“空调”
* @param deviceName 设备名称(可选),如“主照明灯”
* @param controlParams 设备支持的参数定义(可选),JSON对象
* @returns {Promise<{success: boolean}>}
* @throws 如果 regionKey 对应的区域不存在,则抛出错误
*/
export async function registerOrUpdateDevice( deviceId: string, regionKey: number, deviceType: string, deviceName?: string | null, controlParams?: any ) {
// 校验区域是否存在
let region = await selectOneDataByParam(TABLENAME.区域表, { id: regionKey });
if (!region.data) {
throw new BizError(ERRORENUM.参数错误, `区域编号 ${regionKey} 不存在`);
}
let exist = await selectOneDataByParam(TABLENAME.设备表, { device_id: deviceId });
let now = new Date().toISOString().slice(0, 19).replace('T', ' ');
if (Object.keys(exist.data).length) {
await updateManyData(TABLENAME.设备表, { device_id: deviceId }, {
region_key: regionKey,
device_type: deviceType,
device_name: deviceName,
control_params: controlParams || null,
updated_at: now,
});
} else {
await addData(TABLENAME.设备表, {
device_id: deviceId,
region_key: regionKey,
device_type: deviceType,
device_name: deviceName,
control_params: controlParams || null,
created_at: now,
updated_at: now,
});
}
return { success: true };
}
/**
* 处理设备商推送的数据
* @param deviceId 设备唯一标识(必填)
* @param data 设备上报的JSON数据(必填),例如:{ "power": "on", "brightness": 80 }
* @param deviceTime 设备本地时间(可选),格式:YYYY-MM-DD HH:MM:SS
* @returns {Promise<{recordId: number}>} 返回新插入记录的ID
* @throws 如果设备未在 device 表中注册,则抛出错误(需先调用注册接口)
*/
export async function handleDevicePush(deviceId: string, data: any, deviceTime?: string) {
if (!deviceId || !data) {
throw new BizError(ERRORENUM.参数错误, "deviceId和data不能为空");
}
// 检查设备是否存在,不存在则抛出错误(要求先注册)
let device = await selectOneDataByParam(TABLENAME.设备表, { device_id: deviceId });
if (!device.data) {
throw new BizError(ERRORENUM.未找到数据, `设备 ${deviceId} 未注册,请先调用注册接口`);
}
let nowMs = getMySqlMs();
let receivedTime = new Date(nowMs).toISOString().slice(0, 19).replace('T', ' ');
await addData(TABLENAME.设备数据表, {
device_id: deviceId,
data: data,
received_time: receivedTime,
device_time: deviceTime ? new Date(deviceTime).toISOString().slice(0, 19).replace('T', ' ') : null,
created_at: receivedTime,
});
return { success: true };
}
/**
* 处理设备商推送的故障数据
* @param deviceId 设备唯一标识(必填)
* @param faultType 故障类型(必填),如“离线”“数据异常”“硬件故障”
* @param faultDescription 故障详细描述(可选)
* @param occurredTime 故障发生时间(可选),格式:YYYY-MM-DD HH:MM:SS,不传则使用服务器时间
* @returns {Promise<{faultId: number}>} 返回新插入故障记录的ID
* @throws 如果设备未注册,则抛出错误
*/
export async function handleDeviceFaultPush( deviceId: string, faultType: string, faultDescription?: string, occurredTime?: string ) {
if (!deviceId || !faultType) {
throw new BizError(ERRORENUM.参数错误, "deviceId 和 faultType 不能为空");
}
// 检查设备是否存在
let device = await selectOneDataByParam(TABLENAME.设备表, { device_id: deviceId });
if (!device.data) {
throw new BizError(ERRORENUM.未找到数据, `设备 ${deviceId} 未注册,请先调用注册接口`);
}
let nowMs = getMySqlMs();
let nowStr = new Date(nowMs).toISOString().slice(0, 19).replace('T', ' ');
let faultTime = occurredTime
? new Date(occurredTime).toISOString().slice(0, 19).replace('T', ' ')
: nowStr;
let insertResult: any = await addData(TABLENAME.设备故障表, {
device_id: deviceId,
fault_type: faultType,
fault_description: faultDescription || null,
occurred_time: faultTime,
status: 0, // 0=未处理
resolved_time: null,
created_at: nowStr,
updated_at: nowStr,
});
return { faultId: insertResult.insertId, success: true };
}
/**
* 智能管控-运行分析
* @param regionName 区域名称(可选),如“A馆1F”,如果提供则返回该区域的分析数据
* @param regionType 区域类型(可选),如“A馆”,如果提供则返回该类型下所有区域的分析数据
* @returns 大屏所需的所有指标数据
*/
export async function getRunAnalysis(regionName: String, regionType: String) {
// 根据区域类型和名称筛选设备ID列表(如果提供了区域信息)
let regionKey = [];
if (regionType) {
let regionInfo: any = await selectDataListByParam(TABLENAME.区域表, {
type: regionType
}, ["id"]);
regionKey = regionInfo.data.map((r: any) => r.id);
}
if (regionName) {
let regionInfo: any = await selectDataListByParam(TABLENAME.区域表, {
name: regionName
}, ["id"]);
regionKey = regionInfo.data.map((r: any) => r.id);
}
console.log("筛选区域Key:", regionKey);
}
/**
* 获取所有区域列表(供前端下拉选择)
* @returns 区域列表,按 sort_order 排序
*/
export async function getRegionList() {
let regionList = await selectDataListByParam(
TABLENAME.区域表, { }
);
let regionMap: any = {};
regionList.data.forEach((r: any) => {
let regionArr = [];
if (regionMap[r.type]) {
regionArr = regionMap[r.type];
}
regionArr.push(r.name);
regionMap[r.type] = regionArr;
});
console.log("区域列表:", regionMap);
return {
list: regionMap
};
}
\ No newline at end of file
import { selectOneDataByParam } from "../data/findData";
import { TABLENAME } from "../config/dbEnum";
import { getMySqlMs } from "../tools/systemTools";
import { BizError } from "../util/bizError";
import { ERRORENUM } from "../config/errorEnum";
import { updateManyData } from "../data/updateData";
import { addData } from "../data/addData";
import { getToken, getPwdMd5 } from "../tools/system";
/**
* 注册
* @param loginId
* @param pwd
*/
export async function registerUser(loginId:string, pwd:string, name:string) {
let filesList = ["loginId", "id", "pwd", "name"];
let adminUserInfo : any = await selectOneDataByParam(TABLENAME.用户信息表, {loginId}, filesList);
if (!adminUserInfo) {
throw new BizError(ERRORENUM.用户已存在);
}
await addData(TABLENAME.用户信息表, {loginId, pwd: getPwdMd5(loginId, pwd), name: name});
return { success: true };
}
/**
* 登录
* @param loginId
* @param pwd
* @returns
*/
export async function adminLogin(loginId:string, pwd:string) {
let filesList = ["name", "id", "pwd", "pwd"];
let adminUserInfo : any = await selectOneDataByParam(TABLENAME.用户信息表, {loginId}, filesList);
console.log(adminUserInfo.pwd);
if (!adminUserInfo || !adminUserInfo.id) {
throw new BizError(ERRORENUM.账号或密码错误);
}
const encryptedPwd = getPwdMd5(adminUserInfo.id, pwd);
if (adminUserInfo.pwd != encryptedPwd) {
throw new BizError(ERRORENUM.账号或密码错误);
}
let updateUserInfo = {
token : getToken(adminUserInfo.loginId),
tokenMs : getMySqlMs()
};
await updateManyData(TABLENAME.用户信息表, updateUserInfo, {id:adminUserInfo.id});
let userInfo = {
userId:adminUserInfo.id,
userName:adminUserInfo.name,
token:updateUserInfo.token,
};
return {dataInfo:userInfo};
}
\ No newline at end of file
/**
* 表名
*/
export enum TABLENAME {
设备表 = 'device',
设备数据表 = 'device_data',
区域表 = 'region',
设备故障表 = 'device_fault',
用户信息表 = 'user_info'
};
/**
* 表ID前缀枚举(用于randomId生成)
*/
export enum TABLEID {
设备表 = 'device',
设备数据表 = 'device_data',
区域表 = 'region',
设备故障表 = 'device_fault',
用户信息表 = 'user_info'
};
/**
* 校验表单参数配置 【平台】
* 使用场景: 验证客户端请求参数
* 限制: 1.使用端不同不能共用一份配置
* 2.需要搭配 util/verificationParam -> eccFormParam() 方法使用
* 说明: notMustHave = true 时说明该字段是非必填的;不配该字段说明是必填的
*/
/**
* 使用端: 管理后台
* 场景: 新增/修改导师配置
* 备注:
*/
export const TeacherConfigAddConfig = {
teacher_id:{type:"String"}, //导师ID
start_time:{type:"Number", notMustHave:true}, //可选学生开始时间
end_time:{type:"Number", notMustHave:true}, //可选学生结束时间
max_students:{type:"Number"}, //可选学生数量上限
gender_limit:{type:"Number"}, //性别限制(1-无,2-只限男生,3-只限女生)
restrict_to_class:{type:"Boolean"} //是否限制只能带本班级学生
};
export enum DIRNAMEENUM {
系统上传 = 1
}
export enum OPERATIONTYPEENUM {
= 1,
,
,
}
/**上传文件类型 */
export enum FILETYPE {
word = 1,
pdf,
图片,
视频,
多类型
}
export enum TYPEENUM {
string = 1,
number,
object,
array,
boolean,
}
export enum ERRORENUM {
不存在表 = 1,
身份验证失败,
缺少必要参数_表名,
数据表不存在,
参数错误,
添加时数据对象为空,
修改时数据对象为空,
该方法仅可进行数据操作,
数据操作失败,
该方法仅可进行查询操作,
分页请设置当前页数,
数据查询失败,
该方法仅可进行联合查询操作,
数据联合查询失败,
INVALID_REQUEST,
INTERNAL_SERVER_ERROR,
文件不存在,
该身份证号码重复,
用户已存在,
账号或密码错误,
请求参数错误,
您的登录已失效,
答题记录不存在,
系统繁忙请稍后重试,
网络连接失败,
服务不可用,
请求超时,
获取用户信息失败,
非法登录,
重复答题,
数据不存在,
未找到数据,
数据已存在,
权限不足,
重复操作,
操作失败,
数据错误,
密码错误,
请完善信息,
地址数据不完整,
操作不允许,
数据被引用,
空文件失败,
文件上传失败,
只能上传docdocxexcelpngjpg图片,
该时间段已有预约
}
/**
* 只用做code码定义
*/
export enum ERRORCODEENUM {
身份验证失败 = 5001,
缺少必要参数_表名 = 5002,
数据表不存在,
}
let bizErrorMsgMap = {};
for (let key in ERRORENUM) {
bizErrorMsgMap[ERRORENUM[key]] = key;
}
export function getBizMsg(param) {
return bizErrorMsgMap[param];
}
\ No newline at end of file
/**
* 表统一管理
*/
export const TablesConfig = {}
export let EccTableConfig = {};
function initEccTableConfig() {
for(let tableChName in TablesConfig) {
let {tableName, schema} = TablesConfig[tableChName];
EccTableConfig[tableName] ={};
for (let filesName in schema) {
let valueType = typeof schema[filesName];
let value = schema[filesName];
EccTableConfig[tableName][filesName] = {type:'', notMustHave:false};
if (valueType == "function") {
EccTableConfig[tableName][filesName].type = value.name;
} else if (valueType == "object") {
if (!value.type) {
EccTableConfig[tableName][filesName].type = `[${value[0].name}]`;
} else {
if (typeof value.type == "function") {
EccTableConfig[tableName][filesName].type = value.type.name
} else EccTableConfig[tableName][filesName].type = `[${value.type[0].name}]`;
if (value.index) EccTableConfig[tableName][filesName].notMustHave = true;
}
}
}
}
console.log('table eccConfig init success');
}
initEccTableConfig();
\ No newline at end of file
const { Sequelize, DataTypes } = require('sequelize');
export const TablesConfig = [
// 区域配置表
{
tableNameCn: '区域配置表',
tableName: 'region',
schema: {
id: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
autoIncrement: true,
comment: '区域编号(regionKey)'
},
name: {
type: DataTypes.STRING(100),
allowNull: false,
comment: '楼层'
},
type: {
type: DataTypes.STRING(100),
allowNull: false,
comment: '所属楼栋'
},
sort_order: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
comment: '排序序号'
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '创建时间'
},
updated_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '更新时间'
}
},
association: [
{ type: "hasMany", target: "device", foreignKey: "region_key" }
]
},
// 设备主表(关联区域表)
{
tableNameCn: '设备表',
tableName: 'device',
schema: {
id: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
autoIncrement: true,
comment: '自增主键'
},
device_id: {
type: DataTypes.STRING(50),
allowNull: false,
unique: true,
comment: '设备唯一标识(由设备商提供)'
},
region_key: {
type: DataTypes.INTEGER,
allowNull: false,
comment: '区域编号,关联region.id'
},
device_type: {
type: DataTypes.STRING(50),
allowNull: false,
comment: '设备类型,如“空调”“出风口”'
},
device_name: {
type: DataTypes.STRING(100),
allowNull: true,
comment: '设备名称(可选)'
},
control_params: {
type: DataTypes.JSON,
allowNull: true,
comment: '该设备支持的参数定义,JSON格式{"power":["on","off"]}'
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '创建时间'
},
updated_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '更新时间'
}
},
association: [
{ type: "belongsTo", target: "region", foreignKey: "region_key" },
{ type: "hasMany", target: "device_data", foreignKey: "device_id" }
]
},
// 设备数据记录表(存储推送的实时数据)
{
tableNameCn: '设备数据表',
tableName: 'device_data',
schema: {
id: {
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true,
autoIncrement: true,
comment: '自增主键'
},
device_id: {
type: DataTypes.STRING(50),
allowNull: false,
comment: '设备ID,关联device.device_id'
},
data: {
type: DataTypes.JSON,
allowNull: false,
comment: '推送的数据,如{"power":"on","temperature":24,"mode":"cool","fan_speed":"medium","auto_control":"on"}'
},
received_time: {
type: DataTypes.DATE,
allowNull: false,
comment: '数据接收时间(服务器时间)'
},
device_time: {
type: DataTypes.DATE,
allowNull: true,
comment: '设备上报的时间戳(若有)'
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '记录创建时间'
}
},
association: [
{ type: "belongsTo", target: "device", foreignKey: "device_id" }
]
},
// 设备故障表
{
tableNameCn: '设备故障表',
tableName: 'device_fault',
schema: {
id: {
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true,
autoIncrement: true,
comment: '自增主键'
},
device_id: {
type: DataTypes.STRING(50),
allowNull: false,
comment: '设备ID,关联device.device_id'
},
fault_type: {
type: DataTypes.STRING(50),
allowNull: false,
comment: '故障类型,如“离线”“数据异常”“硬件故障”'
},
fault_description: {
type: DataTypes.TEXT,
allowNull: true,
comment: '故障详细描述'
},
occurred_time: {
type: DataTypes.DATE,
allowNull: false,
comment: '故障发生时间'
},
status: {
type: DataTypes.TINYINT,
allowNull: false,
defaultValue: 0,
comment: '处理状态:0=未处理,1=处理中,2=已解决'
},
resolved_time: {
type: DataTypes.DATE,
allowNull: true,
comment: '故障解决时间'
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '记录创建时间'
},
updated_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '记录更新时间'
}
},
association: [
{ type: "belongsTo", target: "device", foreignKey: "device_id" }
]
},
// 人员信息表
{
tableNameCn: '用户信息表',
tableName: 'user_info',
schema: {
id: {
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true,
autoIncrement: true,
comment: '自增主键'
},
loginId: {
type: Sequelize.STRING(255),
allowNull: false,
Comment: '账户,唯一'
},
pwd: {
type: Sequelize.STRING(255),
Comment: '密码'
},
token: {
type: Sequelize.STRING(255),
Comment: 'token'
},
tokenMs: {
type: Sequelize.DATE,
Comment: 'token有效期'
},
name: {
type: Sequelize.STRING(255),
Comment: '用户名称'
}
}
}
];
const path = require('path');
import * as fs from "fs";
import { BizError } from "../util/bizError";
import { analysisXml } from "../util/myXML";
import { ServerConfig } from "../config/systemClass";
export let systemConfig = new ServerConfig();
const ConfigName = "serverConfig.xml";
export async function initConfig() {
try {
let buff = fs.readFileSync(path.join(__dirname.substring(0, __dirname.indexOf("out")), ConfigName));
let configStr = buff.toString();
let configInfo: any = await analysisXml(configStr);
if (!configInfo || !configInfo.config) throw new BizError('xml中无配置');
let { port, sign, img, mysqldb } = configInfo.config;
// 基本配置
systemConfig.port = parseInt(port[0]);
systemConfig.sign = sign[0];
systemConfig.img = img[0];
// MySQL配置
if (mysqldb) {
let dbConfigInfo = mysqldb[0];
systemConfig.mysqldb = { host: '', port: 0, user: '', pwd: '', dataBase: '' };
if (dbConfigInfo.mysqlHost && dbConfigInfo.mysqlPort && dbConfigInfo.mysqlUser && dbConfigInfo.dataBase) {
systemConfig.mysqldb.host = dbConfigInfo.mysqlHost[0];
systemConfig.mysqldb.port = parseInt(dbConfigInfo.mysqlPort[0]);
systemConfig.mysqldb.user = dbConfigInfo.mysqlUser[0];
systemConfig.mysqldb.pwd = dbConfigInfo.mysqlPwd[0] || "";
systemConfig.mysqldb.dataBase = dbConfigInfo.dataBase[0];
}
}
} catch(err) {
console.log('ERROR => 服务器配置解析错误 请检查根目录下 serverConfig.xml 文件是否正确');
console.log(err);
throw new BizError("服务器配置解析错误 请检查根目录下 serverConfig.xml 文件是否正确");
}
}
/**
* 系统配置类
*
*/
export class ServerConfig {
/**系统配置 */
port:number;
sign:string;
img:string;
mysqldb:{
host:string,
port:number,
user:string,
pwd:string,
dataBase:string,
}
}
\ No newline at end of file
import { mysqlModelMap } from "../model/sqlModelBind";
/**
* 添加数据
* @param tableModel
* @param data
* @returns
*/
export async function addData(tableName:string, data:any) {
let tableModel = mysqlModelMap[tableName];
let dataArray = [];
if (!Array.isArray(data)) {
dataArray.push(data);
} else dataArray = data;
await tableModel.bulkCreate(dataArray);
return { isSuccess:true };
}
\ No newline at end of file
import { mysqlModelMap } from "../model/sqlModelBind";
export async function delData(tableName, param) {
let tableModel = mysqlModelMap[tableName];
await tableModel.destroy({where:param});
return {isSuccess:true};
}
\ No newline at end of file
import { Op, Sequelize, Model, WhereOptions, Order, IncludeOptions } from "sequelize";
import { ERRORENUM } from "../config/errorEnum";
import { mysqlModelMap } from "../model/sqlModelBind";
import { BizError } from "../util/bizError";
// 定义类型接口
interface QueryParam {
[key: string]: any;
"%orderDesc%"?: string;
"%orderAsc%"?: string;
"%limit%"?: number;
"%group%"?: string;
"%literal%"?: string;
"%or%"?: Array<QueryParam>;
}
interface AnalysisResult {
where: WhereOptions;
attributes?: string[];
order?: Order;
limit?: number;
group?: string;
}
interface IncludeConfig {
[tableName: string]: {
where?: QueryParam;
column?: string[];
};
}
/**
* where条件查询参数解析
* 支持的操作符:
* %or%:或者条件 [{"列名": 条件}, {"列名": 条件}]
* %like%:模糊查询 {列名: {"%like%": "值"}}
* %gt%:大于 {列名: {"%gt%": 值}}
* %gte%:大于等于 {列名: {"%gte%": 值}}
* %lt%:小于 {列名: {"%lt%": 值}}
* %lte%:小于等于 {列名: {"%lte%": 值}}
* %between%:查询范围内数据 {列名: {"%between%": ["开始", "结束"]}}
* %notBetween%:查询不在范围内数据 {列名: {"%notBetween%": ["开始", "结束"]}}
* %in%:IN查询 {列名: {"%in%": [值1, 值2]}}
* %notIn%:NOT IN查询 {列名: {"%notIn%": [值1, 值2]}}
* %ne%:不等于 {列名: {"%ne%": 值}}
* %regexp%:正则表达式 {列名: {"%regexp%": "模式"}}
* %orderDesc%: 降序排序 {"%orderDesc%": "列名"}
* %orderAsc%: 升序排序 {"%orderAsc%": "列名"}
* %limit%: 限制数量 {"%limit%": 数量}
* %group%: 分组 {"%group%": "列名"}
* %literal%: 原生SQL {"%literal%": "SQL语句"}
*/
function analysisParamToWhere(param: QueryParam, column?: string[]): AnalysisResult {
const where: any = {};
const order: Order = [];
let group: string | undefined;
let limit: number | undefined;
for (const key in param) {
const value = param[key];
// 处理 %or% 条件
if (key === "%or%") {
if (Array.isArray(value) && value.length > 0) {
const orConditions = value.map((condition: QueryParam) => {
// 递归处理 or 中的每个条件
const result = analysisParamToWhere(condition);
return result.where;
}).filter(cond => cond && Object.keys(cond).length > 0);
if (orConditions.length > 0) {
where[Op.or] = orConditions;
}
}
continue;
}
// 处理普通条件和操作符
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
// 处理操作符对象
const operatorObj: any = {};
let hasOperator = false;
for (const opKey in value) {
const opValue = value[opKey];
switch (opKey) {
case "%like%":
operatorObj[Op.like] = `%${opValue}%`;
hasOperator = true;
break;
case "%gt%":
operatorObj[Op.gt] = opValue;
hasOperator = true;
break;
case "%gte%":
operatorObj[Op.gte] = opValue;
hasOperator = true;
break;
case "%lt%":
operatorObj[Op.lt] = opValue;
hasOperator = true;
break;
case "%lte%":
operatorObj[Op.lte] = opValue;
hasOperator = true;
break;
case "%between%":
operatorObj[Op.between] = opValue;
hasOperator = true;
break;
case "%notBetween%":
operatorObj[Op.notBetween] = opValue;
hasOperator = true;
break;
case "%in%":
operatorObj[Op.in] = opValue;
hasOperator = true;
break;
case "%notIn%":
operatorObj[Op.notIn] = opValue;
hasOperator = true;
break;
case "%ne%":
operatorObj[Op.ne] = opValue;
hasOperator = true;
break;
case "%regexp%":
operatorObj[Op.regexp] = opValue;
hasOperator = true;
break;
}
}
if (hasOperator) {
where[key] = operatorObj;
} else {
// 普通对象,直接赋值
where[key] = value;
}
} else {
// 处理特殊键和普通值
switch (key) {
case "%orderDesc%":
order.push([Sequelize.col(value), "DESC"]);
break;
case "%orderAsc%":
order.push([Sequelize.col(value), "ASC"]);
break;
case "%limit%":
limit = Number(value);
break;
case "%group%":
group = value;
break;
case "%literal%":
where[Op.and] = Sequelize.literal(value);
break;
default:
where[key] = value;
}
}
}
const result: AnalysisResult = { where };
if (column && column.length) result.attributes = column;
if (order.length) result.order = order;
if (limit) result.limit = limit;
if (group) result.group = group;
return result;
}
/**
* 获取表模型
*/
function getTableModel(tableName: string): typeof Model {
let tableModel = mysqlModelMap[tableName];
if (!tableModel) {
throw new BizError(ERRORENUM.不存在表, `表 ${tableName} 不存在`);
}
return tableModel;
}
/**
* 查询单个数据
*/
export async function selectOneDataByParam(tableName: string, param: QueryParam, column?: string[]) {
let tableModel = mysqlModelMap[tableName];
const selectParam = analysisParamToWhere(param, column);
const data = await tableModel.findOne(selectParam);
return { data: data || {} };
}
/**
* 查询多个数据
*/
export async function selectDataListByParam(tableName: string, param: QueryParam, column?: string[]) {
let tableModel = mysqlModelMap[tableName];
const selectParam = analysisParamToWhere(param, column);
const data = await tableModel.findAll(selectParam);
return { data };
}
/**
* 分页查询
*/
export async function selectDataListToPageByParam(tableName: string, param: QueryParam, column: string[], pageNumber: number, pageSize: number ) {
let tableModel = mysqlModelMap[tableName];
const selectParam: any = analysisParamToWhere(param, column);
selectParam.limit = pageSize || 10;
selectParam.offset = (pageNumber - 1) * pageSize;
const data = await tableModel.findAll(selectParam);
return { data };
}
/**
* 查询数据总数
*/
export async function selectDataCountByParam(tableName: string, param: QueryParam) {
let tableModel = mysqlModelMap[tableName];
const selectParam: any = analysisParamToWhere(param, []);
const count = await tableModel.count(selectParam);
return { data: count };
}
/**
* 关联查询
*/
export async function associationSelect(tableName: string, param: any) {
let model = mysqlModelMap[tableName];
if (!model) throw new BizError(ERRORENUM.不存在表);
const data = await model.aggragateData(param);
return { data };
}
/**
* 构建关联查询的include配置
*/
function buildIncludeOptions(includeConf: IncludeConfig): IncludeOptions[] {
return Object.entries(includeConf).map(([tableName, config]) => {
const tableModel = mysqlModelMap[tableName];
if (!tableModel) {
throw new BizError(ERRORENUM.不存在表, `尝试进行多表联查,但是不存在 ${tableName}`);
}
const includeOptions: IncludeOptions = {
model: tableModel,
...analysisParamToWhere(config.where || {}, config.column)
};
return includeOptions;
});
}
/**
* 多表联查 - 列表
*/
export async function selectDataToTableAssociation(tableName: string, includeConf: IncludeConfig, param: QueryParam, column?: string[]) {
let tableModel = mysqlModelMap[tableName];
const include = buildIncludeOptions(includeConf);
const selectParam: any = analysisParamToWhere(param, column);
selectParam.include = include;
const data = await tableModel.findAll(selectParam);
return { data };
}
/**
* 多表联查 - 分页
*/
export async function selectDataToTableAssociationToPage(tableName:string, includeConf:IncludeConfig, param:QueryParam, column:string[], pageNumber:number, pageSize:number) {
let tableModel = mysqlModelMap[tableName];
const include = buildIncludeOptions(includeConf);
const selectParam: any = analysisParamToWhere(param, column);
selectParam.include = include;
selectParam.limit = pageSize || 10;
selectParam.offset = (pageNumber - 1) * pageSize;
const data = await tableModel.findAll(selectParam);
return { data };
}
/**
* 多表联查 - 单个
*/
export async function selectOneDataToTableAssociation(tableName:string,includeConf:IncludeConfig,param:QueryParam,column?:string[]) {
let tableModel = mysqlModelMap[tableName];
const include = buildIncludeOptions(includeConf);
const selectParam: any = analysisParamToWhere(param, column);
selectParam.include = include;
const data = await tableModel.findOne(selectParam);
return { data: data || {} };
}
// ==================== 新增的通用方法 ====================
/**
* 查询并计数(返回数据和总数)
*/
export async function selectDataWithCount( tableName: string, param: QueryParam, column?: string[] ) {
let tableModel = mysqlModelMap[tableName];
const selectParam = analysisParamToWhere(param, column);
const result = await tableModel.findAndCountAll(selectParam);
return { data: result.rows, count: result.count };
}
/**
* 分页查询(增强版,返回数据、总数和分页信息)
*/
export async function selectPaginatedData( tableName: string, param: QueryParam, column: string[], pageNumber: number, pageSize: number ) {
let tableModel = mysqlModelMap[tableName];
const selectParam: any = analysisParamToWhere(param, column);
selectParam.limit = pageSize;
selectParam.offset = (pageNumber - 1) * pageSize;
const result = await tableModel.findAndCountAll(selectParam);
return {
data: result.rows,
pagination: {
page: pageNumber,
pageSize,
total: result.count,
totalPages: Math.ceil(result.count / pageSize)
}
};
}
/**
* 自定义排序查询
* @param orders 排序数组,格式: [['column1', 'DESC'], ['column2', 'ASC']]
*/
export async function selectDataWithCustomOrder( tableName: string, param: QueryParam, column: string[], orders: [string, 'ASC' | 'DESC'][] ) {
let tableModel = mysqlModelMap[tableName];
const selectParam: any = analysisParamToWhere(param, column);
// 添加自定义排序
selectParam.order = orders.map(([column, direction]) =>
[Sequelize.col(column), direction]
);
const data = await tableModel.findAll(selectParam);
return { data };
}
/**
* 查询最大值
*/
export async function selectMaxValue( tableName: string, column: string, whereParam?: QueryParam ) {
let tableModel = mysqlModelMap[tableName];
const where = whereParam ? analysisParamToWhere(whereParam, []).where : {};
const result = await tableModel.max(column, { where });
return { max: result };
}
/**
* 查询最小值
*/
export async function selectMinValue( tableName: string, column: string, whereParam?: QueryParam ) {
let tableModel = mysqlModelMap[tableName];
const where = whereParam ? analysisParamToWhere(whereParam, []).where : {};
const result = await tableModel.min(column, { where });
return { min: result };
}
/**
* 查询平均值
*/
export async function selectAvgValue( tableName: string, column: string, whereParam?: QueryParam ) {
let tableModel = mysqlModelMap[tableName];
const where = whereParam ? analysisParamToWhere(whereParam, []).where : {};
const result = await tableModel.average(column, { where });
return { average: result };
}
/**
* 查询求和
*/
export async function selectSumValue( tableName: string, column: string, whereParam?: QueryParam ) {
let tableModel = mysqlModelMap[tableName];
const where = whereParam ? analysisParamToWhere(whereParam, []).where : {};
const result = await tableModel.sum(column, { where });
return { sum: result };
}
/**
* 分页查询(支持自定义排序)
* @param tableName 表名
* @param param 查询参数
* @param column 查询列
* @param pageNumber 页码
* @param pageSize 每页大小
* @param orders 排序数组,格式: [['column1', 'DESC'], ['column2', 'ASC']]
*/
export async function selectPaginatedDataWithOrder( tableName:string, param:QueryParam, column:string[], pageNumber:number, pageSize:number, orders:[string, 'ASC' | 'DESC'][] = []) {
let tableModel = mysqlModelMap[tableName];
const selectParam: any = analysisParamToWhere(param, column);
selectParam.limit = pageSize;
selectParam.offset = (pageNumber - 1) * pageSize;
// 添加自定义排序
if (orders && orders.length > 0) {
selectParam.order = orders.map(([column, direction]) =>
[Sequelize.col(column), direction]
);
}
const result = await tableModel.findAndCountAll(selectParam);
return {
data: result.rows,
pagination: {
page: pageNumber,
pageSize,
total: result.count,
totalPages: Math.ceil(result.count / pageSize)
}
};
}
import { Op, Sequelize } from "sequelize";
import { mysqlModelMap } from "../model/sqlModelBind";
/**
* where条件查询参数
* @param param
* %like%:模糊查询 {列名: {"%like%": }}
* %gt%:大于 {列名: {"%gt%": }}
* %gte%:大于等于 {列名: {"%gte%": }}
* %lt%:小于 {列名: {"%lt%": }}
* %lte%:小于等于 {列名: {"%lte%": }}
* %between%:查询范围内数据 {列名: {"%between%": ["开始参数", "结束参数"]}} ---BETWEEN 开始参数 AND 结束参数
* %notBetween%:查询不在范围内数据 {列名: {"%notBetween%": ["开始参数", "结束参数"]}} ---NOT BETWEEN 开始参数 AND 结束参数
* %orderDesc%: order by DESC {"%orderDesc%": "列名"}
* %limit%: {"%limit%": 数量}
* @returns
*/
function analysisParamToWhere(param) {
let where = {};
let order = [];
let limit = 0;
for (let key in param) {
if (typeof param[key] == "object") {
where[key] = {};
for (let whereKey in param[key]){
switch(whereKey) {
case "%like%":
where[key][Op.like] = `%${param[key]["%like%"]}%`;
break;
case "%gt%":
where[key][Op.gt] = param[key]["%gt%"];
break;
case "%gte%":
where[key][Op.gte] = param[key]["%gte%"];
break;
case "%lt%":
where[key][Op.lt] = param[key]["%lt%"];
break;
case "%lte%":
where[key][Op.lte] = param[key]["%lte%"];
break;
case "%between%":
where[key][Op.between] = param[key]["%between%"];
break;
case "%notBetween%":
where[key][Op.notBetween] = param[key]["%notBetween%"];
break;
case "%in%":
where[key][Op.in] = param[key]["%in%"];
break;
case "%notIn%":
where[key][Op.notIn] = param[key]["%notIn%"];
break;
}
}
}else {
switch (key) {
case "%orderDesc%":
order = [[Sequelize.col(param[key]), "DESC"]];
break;
case "%orderAsc%":
order = [[Sequelize.col(param[key]), "ASC"]];
break;
case "%limit%":
limit = param[key];
break;
default: where[key] = param[key];
}
}
}
let selectParam:any = {where};
if (order && order.length) selectParam.order = order;
if (limit) selectParam.limit = limit;
return selectParam;
}
export async function updateManyData(tableName, param:object, data:object) {
let tableModel = mysqlModelMap[tableName];
let where = analysisParamToWhere(param);
await tableModel.update(data, where);
return {isSuccess:true};
}
\ No newline at end of file
import { systemConfig } from "../config/serverConfig";
//导入sequelize模块
const Sequelize = require('sequelize');
var mysqlDB;
export async function initMysqlDB() {
mysqlDB = new Sequelize(systemConfig.mysqldb.dataBase,systemConfig.mysqldb.user,systemConfig.mysqldb.pwd,{
host:systemConfig.mysqldb.host,
port:systemConfig.mysqldb.port,
dialect:'mysql', //数据库类型
pool:{ //数据库连接池
max:20, //最大连接对象的个数
min:5, //最小连接对象的个数
idle:1000 //最长等待时间,单位为毫秒
},
timezone: '+08:00', //东八时区
dialectOptions: {
dateStrings: true,
typeCast: true
},
});
}
export { mysqlDB };
\ No newline at end of file
import { initConfig, systemConfig} from "./config/serverConfig";
import * as mysqlDB from "./db/mysqlInit";
import { initMysqlModel } from "./model/sqlModelBind";
import { httpServer } from "./net/http_server";
async function lanuch() {
/**初始化配置解析 */
await initConfig();
/**初始化sql */
await mysqlDB.initMysqlDB();
await initMysqlModel();
/**创建http服务 */
httpServer.createServer(systemConfig.port);
console.log('This indicates that the server is started successfully.');
}
lanuch();
import { DIRNAMEENUM } from "../config/enum";
import { ERRORENUM } from "../config/errorEnum";
import { BizError } from "../util/bizError";
var formidable = require("formidable");
const path = require('path');
export async function parseFormParam(req, res, next) {
let upType = DIRNAMEENUM.系统上传;
var form = new formidable.IncomingForm({
uploadDir: path.join(__dirname.substring(0,__dirname.indexOf("out")),'files', `${upType}`),
maxFildsSize: 10*1024*1024,
keepExtensions: true
});
form.parse(req, (err, fields, files)=>{
if (err) {
return next(err);
} else if (!files || !Object.keys(files).length) {
return next(new BizError(ERRORENUM.空文件失败));
}
else {
req.fields = fields;
req.files = {};
if (!files.formData && !files.file ) return next(new BizError(ERRORENUM.文件上传失败) );
if (files.formData) {
req.files.formData = files.formData;
req.files.formData.upType = upType;
}
else {
req.files.formData = files.file;
req.files.formData.upType = upType;
}
if (!req.files.formData || !req.files.formData.name) {
return next(new BizError(ERRORENUM.文件上传失败) );
}
console.log(req.files.formData.type);
//获取文件扩展名
const fileExtension = req.files.formData.name.split('.').pop().toLowerCase();
if ( req.files.formData.type == 'image/png') {
req.fileType = '.png';
return next();
} else if (req.files.formData.type == 'image/jpg' || req.files.formData.type == 'image/jpeg') {
req.fileType = '.jpg';
return next();
} else if (req.files.formData.type == "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || fileExtension == 'docx') {
req.fileType = '.docx';
return next();
} else if (req.files.formData.type == "application/msword") {
req.fileType = '.doc';
return next();
} else if (req.files.formData.type == "application/pdf") {
req.fileType = '.pdf';
return next();
} else if (req.files.formData.type == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"){
req.fileType = '.xlsx';
return next();
} else if (req.files.formData.type == "application/vnd.ms-excel"){
req.fileType = '.xls';
return next();
} else {
return next(new BizError(ERRORENUM.只能上传docdocxexcelpngjpg图片) )
}
}
})
}
import { bizlive } from "tencentcloud-sdk-nodejs";
import { ERRORCODEENUM } from "../config/errorEnum";
/**
* 中间件 错误返回
* @param err
* @param req
* @param res
* @param next
*/
export function httpErrorHandler(err, req, res, next) {
console.log("in httpErrorHandler");
console.log(err);
//todo 自定义错误编码
if (err) {
if ( ERRORCODEENUM[err.message] ) {
res.success({success:false, msg:err.message, code:ERRORCODEENUM[err.message]});
next();
}
else {
res.success({success:false, msg:err.message, code:500});
next();
}
}
}
\ No newline at end of file
import { ERRORENUM } from "../config/errorEnum";
import { systemConfig } from "../config/serverConfig";
import { EccTableConfig } from "../config/mongoTableConfig";
import { BizError } from "../util/bizError";
import { mysqlModelMap } from "../model/sqlModelBind";
import { selectOneDataByParam } from "../data/findData";
import { TABLENAME } from "../config/dbEnum";
/**
* 中间件 校验连接对象token
* @param req
* @param res
* @param next
* @returns
*/
export async function checkMongoSign(req, res, next) {
if (!req.headers) req.headers = {};
let sign = req.headers.sign;
let table = req.headers.table;
if (sign != systemConfig.sign) return next( new BizError(ERRORENUM.身份验证失败, `传入的sign值为:${sign}`) );
if (!table) return next( new BizError(ERRORENUM.缺少必要参数_表名, `传入的table值为:${table}`) );
if (!EccTableConfig[table]) return next( new BizError(ERRORENUM.不存在表, `传入的table值为:${table}`) );
next();
}
/**
* 中间件 校验连接对象token
* @param req
* @param res
* @param next
* @returns
*/
export async function checkMySqlSign(req, res, next) {
if (!req.headers) req.headers = {};
let sign = req.headers.sign;
let table = req.headers.table;
if (sign != systemConfig.sign) return next( new BizError(ERRORENUM.身份验证失败, `传入的sign值为:${sign}`) );
if (!table) return next( new BizError(ERRORENUM.缺少必要参数_表名, `传入的table值为:${table}`) );
if (!mysqlModelMap[table]) return next( new BizError(ERRORENUM.不存在表, `传入的table值为:${table}`) );
req.tableModel = mysqlModelMap[table];
next();
}
/**
* 中间件 校验用户登录token
* @param req
* @param res
* @param next
* @returns
*/
export async function checkUser(req, res, next) {
if (!req.headers) req.headers = {};
const userId = req.headers.userid || "";
const reqToken = req.headers.token || "";
const roleToken = req.headers.roletoken || "";
if (!userId) return next(new BizError(ERRORENUM.身份验证失败, `userId:${userId} token:${reqToken}`));
let userProfileData: any = await selectOneDataByParam(TABLENAME.用户信息表, {
user_id: userId,
token: reqToken
}, ["user_id", "roles", "current_role", "role_token", "permission", "token", "token_ms"]);
if (!userProfileData || !userProfileData.data || !userProfileData.data.user_id) {
return next(new BizError(ERRORENUM.非法登录, `userId:${userId} token:${reqToken}`));
}
if (userProfileData.data.token != reqToken) {
return next(new BizError(ERRORENUM.身份验证失败, `userId:${userId}`));
}
// 如果有roleToken,验证角色token
if (roleToken && userProfileData.data.role_token != roleToken) {
return next(new BizError(ERRORENUM.身份验证失败, `角色token无效`));
}
req.userInfo = {
userId: userId,
userName: req.headers.username || "",
token: reqToken,
roleToken: roleToken,
roles: userProfileData.data.roles || "",
currentRole: userProfileData.data.current_role || null,
permission: userProfileData.data.permission || 0
};
next();
}
/**
* 中间件 校验管理员权限(简化版)
*/
export async function checkAdmin(req, res, next) {
try {
const userInfo = req.userInfo;
if (!userInfo || !userInfo.userId) {
return next(new BizError(ERRORENUM.身份验证失败, '用户未登录'));
}
// 检查permission字段
if (userInfo.permission !== 1) {
return next(new BizError(ERRORENUM.权限不足, '需要管理员权限'));
}
next();
} catch (error) {
next(new BizError(ERRORENUM.系统繁忙请稍后重试, error.message));
}
}
export function watch(req, res, next) {
res.success = success.bind({res:res, req:req});
return next();
}
/**
* 中间件正确返回方法
* @param data
*/
function success(data) {
let resultPack;
if (data ) {
if ( data.success === undefined || data.success === true ) {
resultPack = {data, success:true, code:200};
}
else {
resultPack = data;
}
}else {
resultPack = {code:500, success:false, msg:'result is null'};
}
this.res.send(resultPack);
}
/**
* mysql 数据层
*/
import { TablesConfig } from "../config/mysqlTableConfig";
import { mysqlDB } from "../db/mysqlInit";
let mysqlModelMap = {};
export async function initMysqlModel() {
console.log('🚀 开始初始化 MySQL 模型...');
// 检查 TablesConfig
if (!TablesConfig || !Array.isArray(TablesConfig)) {
console.error('❌ TablesConfig 未定义或不是数组');
return;
}
if (TablesConfig.length === 0) {
console.warn('⚠️ TablesConfig 是空数组');
return;
}
console.log(`📊 共发现 ${TablesConfig.length} 个表配置`);
/**第一步:初始化所有表 */
for (let i = 0; i < TablesConfig.length; i++) {
let { tableName, schema } = TablesConfig[i];
if (!tableName) {
console.warn(`⚠️ 第 ${i} 个表配置缺少 tableName,跳过`);
continue;
}
if (!schema) {
console.warn(`⚠️ 表 ${tableName} 缺少 schema 配置,跳过`);
continue;
}
console.log(`🔄 正在初始化表: ${tableName}`);
let schemaConf = {
freezeTableName: true,
timestamps: false
};
try {
let model = mysqlDB.define(tableName, schema, schemaConf);
mysqlModelMap[tableName] = await model.sync({});
console.log(`✅ 表 ${tableName} 初始化成功`);
} catch (error) {
console.error(`❌ 表 ${tableName} 初始化失败:`, error);
}
}
console.log(`📊 表初始化完成,成功初始化 ${Object.keys(mysqlModelMap).length} 个表`);
/**第二步:初始化表关联 */
for (let i = 0; i < TablesConfig.length; i++) {
let { tableName, association } = TablesConfig[i];
// 检查表是否初始化成功
if (!mysqlModelMap[tableName]) {
console.warn(`⚠️ 表 ${tableName} 未成功初始化,跳过关联配置`);
continue;
}
// 检查关联配置
if (!association || !Array.isArray(association) || association.length === 0) {
console.log(`ℹ️ 表 ${tableName} 没有关联配置,跳过`);
continue;
}
console.log(`🔄 正在初始化表 ${tableName} 的关联关系,共 ${association.length} 个关联`);
association.forEach((item: any, index: number) => {
if (!item) {
console.warn(`⚠️ 表 ${tableName} 的第 ${index} 个关联配置为空,跳过`);
return;
}
let { type, check, foreignKey } = item;
if (!type || !check) {
console.warn(`⚠️ 表 ${tableName} 的关联配置缺少 type 或 check 字段`, item);
return;
}
// 检查关联表是否初始化成功
if (!mysqlModelMap[check]) {
console.warn(`⚠️ 关联表 ${check} 未初始化,跳过关联`);
return;
}
try {
console.log(` 处理关联: ${tableName} -> ${check}, 类型: ${type}`);
if (type == "hasOne") {
mysqlModelMap[check].hasOne(mysqlModelMap[tableName]);
} else if (type == "hasMany") {
mysqlModelMap[tableName].hasMany(mysqlModelMap[check], { foreignKey });
}
mysqlModelMap[check].belongsTo(mysqlModelMap[tableName], { foreignKey });
console.log(` ✅ 关联成功: ${tableName} <-> ${check}`);
} catch (error) {
console.error(` ❌ 关联失败: ${tableName} -> ${check}`, error);
}
});
}
console.log('🎉 MySQL 模型初始化完成');
console.log('📋 已初始化的表:', Object.keys(mysqlModelMap).join(', '));
}
export { mysqlModelMap };
\ No newline at end of file
import express = require('express');
import bodyParser = require('body-parser');
import routers = require('../routers/router');
import compression = require('compression');
import { watch } from '../middleware/watch';
import { httpErrorHandler } from '../middleware/httpErrorHandler';
import * as path from "path";
import fallback from 'express-history-api-fallback';
import historyFallback from 'express-history-api-fallback'; // 正确导入默认导出的函数
export class httpServer {
static createServer(port: number) {
var httpServer = express();
httpServer.all('*', (req, res, next) => {
res.header('Access-Control-Allow-Origin', req.headers.origin);
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header('Access-Control-Allow-Headers', 'Content-Type,request-origin,userid,token,username');
res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
res.header('Access-Control-Allow-Credentials', 'true');
res.header("X-Powered-By", ' 3.2.1');
next();
});
httpServer.use(express.static('public'));
httpServer.use(express.static('img'));
httpServer.use(compression());
httpServer.use(watch);
httpServer.use(bodyParser.json({ limit: "500kb" }));
routers.setRouter(httpServer);
httpServer.use(httpErrorHandler);
const root = path.join(__dirname, "../../public");
httpServer.use(express.static(root))
// 使用 fallback 函数
httpServer.use(fallback('index.html', { root }));
httpServer.use(historyFallback('index.html', { root }));
console.log('web listen on port:' + port);
httpServer.listen(port);
console.log('server listen on port:' + port);
return httpServer;
}
}
\ No newline at end of file
/**
* 零碳展厅设备商数据推送接口(无需登录)
*/
import asyncHandler from 'express-async-handler';
import * as deviceBiz from '../biz/device';
import * as regionBiz from '../biz/region';
import { eccReqParamater } from '../util/verificationParam';
import { checkUser } from '../middleware/user';
export function setRouter(httpServer) {
/** 注册/更新设备信息 */
httpServer.post('/api/zc/device/register', asyncHandler(registerDevice));
httpServer.post('/api/zc/region/regionList', asyncHandler(getRegionList));
/** 设备商推送实时数据 */
httpServer.post('/api/zc/device/push', asyncHandler(deviceDataPush));
/** 设备商推送故障数据 */
httpServer.post('/api/zc/device/fault/push', asyncHandler(deviceFaultPush));
/** 运行分析 */
httpServer.post('/api/zc/run/analysis', asyncHandler(getRunAnalysis));
/**运行分析-弹窗 */
httpServer.post('/api/zc/run/analysis/pop', asyncHandler(getRunAnalysisPop));
/** 智能监控弹窗 */
httpServer.post('/api/zc/smart/monitor/pop', asyncHandler(getSmartMonitorPop));
}
/**
* 注册或更新设备信息
*/
async function registerDevice(req, res) {
let reqConf = {deviceId:'String', regionKey:'Number', deviceType:'String', deviceName:'String', controlParams:'Object'};
const NotMustHaveKeys = [];
let { deviceId, regionKey, deviceType, deviceName, controlParams } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await deviceBiz.registerOrUpdateDevice(deviceId, regionKey, deviceType, deviceName, controlParams);
res.success(result);
}
async function getRegionList(req, res) {
const result = await regionBiz.getRegionList();
res.success(result);
}
/**
* 设备商推送数据
*/
async function deviceDataPush(req, res) {
let reqConf = {deviceId:'String', data:'Object', deviceTime:'String'};
const NotMustHaveKeys = [];
let { deviceId, data, deviceTime } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await deviceBiz.handleDevicePush(deviceId, data, deviceTime);
res.success(result);
}
/**
* 设备商推送故障数据
*/
async function deviceFaultPush(req, res) {
let reqConf = {deviceId:'String', faultType:'String', faultDescription:'String', occurredTime:'String'};
const NotMustHaveKeys = ["faultDescription"];
let { deviceId, faultType, faultDescription, occurredTime } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await deviceBiz.handleDeviceFaultPush(deviceId, faultType, faultDescription, occurredTime);
res.success(result);
}
/**
* 运行分析
*/
async function getRunAnalysis(req, res) {
const { } = req.body;
const result = await deviceBiz.getRunAnalysis();
res.success(result);
}
/**
* 运行分析-楼栋
*/
async function getRunAnalysisByBuilding(req, res) {
let reqConf = {regionType:'String'};
const NotMustHaveKeys = [];
let { type } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await regionBiz.getRunAnalysis("", type);
res.success(result);
}
/**
* 运行分析-楼层
*/
async function getRunAnalysisByFloor(req, res) {
let reqConf = {regionName:'String'};
const NotMustHaveKeys = [];
let { name } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await regionBiz.getRunAnalysis(name, "");
res.success(result);
}
/**
* 运行分析-弹窗
*/
async function getRunAnalysisPop(req, res) {
let reqConf = {regionKey:'Number'};
const NotMustHaveKeys = [];
let { regionKey } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await deviceBiz.getRunAnalysisPop(regionKey);
res.success(result);
}
/**
* 智能监控弹窗
*/
async function getSmartMonitorPop(req, res) {
let reqConf = {regionKey:'Number'};
const NotMustHaveKeys = [];
let { regionKey } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await deviceBiz.getSmartMonitorPop(regionKey);
res.success(result);
}
/**
* 总路由入口
*/
import * as deviceRouter from './device';
export function setRouter(httpServer) {
deviceRouter.setRouter(httpServer);
}
/**
* 青岛博物馆用户注册登录接口
*/
import asyncHandler from 'express-async-handler';
import * as usersBiz from '../biz/users';
import { eccReqParamater } from '../util/verificationParam';
export function setRouter(httpServer) {
/** 用户注册 */
httpServer.post('/api/zc/user/register', asyncHandler(registerUser));
/** 用户登录 */
httpServer.post('/api/zc/user/login', asyncHandler(login));
/**
* 注册信息
*/
async function registerUser(req, res) {
let reqConf = {username:'String', password:'String', name:'String'};
const NotMustHaveKeys = [];
let { username, password, name } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await usersBiz.registerUser(username, password, name);
res.success(result);
}
/**
* 登录信息
*/
async function login(req, res) {
let reqConf = {username:'String', password:'String'};
const NotMustHaveKeys = [];
let { username, password } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await usersBiz.adminLogin(username, password);
res.success(result);
}
}
\ No newline at end of file
import { ERRORENUM } from "../config/errorEnum";
import { BizError } from "../util/bizError";
/**
* 校验value是否符合传入的枚举
* @param name 被掉用名称 用于输出异常日志
* @param key 目标字段 用于输出异常日志
* @param enumConf 目标枚举
* @param value 目标值
* 无返回 有异常直接报错
*/
export function eccEnumValue(name:string, key:string, enumConf, value:any) {
let eccSuccess = true;
if ( typeof value == 'number' ) {
if (!enumConf[value] ) eccSuccess = false;
} else if (Array.isArray(value)) {
value.forEach(item => {
if ( !enumConf[item] ) eccSuccess = false;
});
}
if (!eccSuccess) throw new BizError(ERRORENUM.文件不存在, `${name} 下的 ${key} 字段值为 ${value} 不满足枚举范围`);
}
/**
* 将枚举值转换成对应的枚举名(key)
* @param enumConf 目标枚举
* @param value 目标值
* @returns string 返回字符串 如果传入多个枚举值,就拼接字符串
*/
export function changeEnumValue(enumConf, value:any) {
if (!value) return '';
if ( typeof value == 'number' ) {
let str = enumConf[value];
/**特化处理 */
if(/_dou/.test(str)) str = str.replace(/_dou/gm, ",");
if(/_zyh/.test(str)) str = str.replace(/_zyh/gm, "“");
if(/_yyh/.test(str)) str = str.replace(/_yyh/gm, "”");
if(/_dun/.test(str)) str = str.replace(/_dun/gm, "、");
if(/_ju/.test(str)) str = str.replace(/_ju/gm, "。");
return str
} else if (typeof value == 'string') {
try {//兼容数据库 '[1,2,3]'
value = JSON.parse(value);
}catch(err) {
return enumConf[parseInt(value)];
}
}
let str = "";
value.forEach((item, index) => {
let subStr = enumConf[item];
/**特化处理 */
if(/_dou/.test(subStr)) subStr = subStr.replace(/_dou/gm, ",");
if(/_zyh/.test(subStr)) subStr = subStr.replace(/_zyh/gm, "“");
if(/_yyh/.test(subStr)) subStr = subStr.replace(/_yyh/gm, "”");
if(/_dun/.test(subStr)) subStr = subStr.replace(/_dun/gm, "、");
if(/_ju/.test(subStr)) subStr = subStr.replace(/_ju/gm, "。");
str += subStr;
if (index == value.length-1) str+="";
else str += ","
});
return str;
}
\ No newline at end of file
import { ERRORENUM } from "../config/errorEnum";
import { BizError } from "../util/bizError";
import { TYPEENUM } from "../config/enum";
/**
* 根据conf配置校验请求参数
* @param conf 配置
* @param param 表单
* @param skipKeys []不必填的字段
*/
export function eccReqParamater(conf:object, param, skipKeys?) {
skipKeys = skipKeys || [];
let skipMap = {};
skipKeys.forEach(keyName => {
skipMap[keyName] = 1;
});
/**校验多余字段 */
for (let key in param) {
if (!conf[key]) throw new BizError(ERRORENUM.参数错误, `多余${key}字段`);
}
/**校验必填和缺失字段 */
for (let key in conf) {
let confType = conf[key];
let value = param[key];
let valueType = typeof value;
if ( value == null || value == undefined ) {
if (!skipMap[key]) throw new BizError(ERRORENUM.参数错误, `缺失${key}字段`);
} else {
let isError = false;
let errorStr = "";
switch(confType) {
case 'Number':
if ( confType.toLowerCase() != valueType ) isError = true;
else {
if ((""+param[key]).indexOf('.') > -1) {
param[key] = parseInt(`${param[key] *100}`)/100;
}
}
break;
case 'String':
case 'Boolean':
case 'Object':
if ( confType.toLowerCase() != valueType ) isError = true;
break;
case '[Number]':
if ( !Array.isArray(param[key]) ) isError = true;
for (let i =0; i < param[key].length; i++) {
let item = param[key][i];
if ( typeof item != 'number' ) {
isError = true;
errorStr = `${key}应是number型数组其中下标${i}${typeof item}`;
}
}
break;
case '[Object]':
if ( !Array.isArray(param[key]) ) isError = true;
for (let i =0; i < param[key].length; i++) {
let item = param[key][i];
if ( typeof item != 'object' ) {
isError = true;
errorStr = `${key}应是object型数组其中下标${i}${typeof item}`;
}
}
break;
case '[String]':
if ( !Array.isArray(param[key]) ) isError = true;
for (let i =0; i < param[key].length; i++) {
let item = param[key][i];
if ( typeof item != 'string' ) {
isError = true;
errorStr = `${key}应是String型数组其中下标${i}${typeof item}`;
}
}
break;
// case 'Address':
// /**地址类型 基本数据类型为数组字符串但是要判断层级关系 */
// if ( !Array.isArray(param[key]) ) {
// isError = true;
// errorStr = `${key}应是数组形`;
// }
// if ( param[key].length != 4) {
// isError = true;
// errorStr = `${key}超过特定长度4 目前长度 ${param[key].length}`;
// }
// for (let i =0; i < param[key].length; i++) {
// let item = param[key][i];
// if ( typeof item != 'string' ) {
// isError = true;
// errorStr = `${key}应是string型数组其中下标${i}是${typeof item}`;
// }
// }
// /** 不符合规则的 */
// let nullIndex = -1;
// for (let i = 0; i < param[key].length; i++) {
// if (nullIndex != -1) {//出现过空 第一次出现后的位置 都不能有值
// if (param[key]) {
// //做一个特化
// throw new BizError(ERRORENUM.地址数据不完整, `${key} 下标 ${nullIndex} 为空 `);
// }
// }
// if (nullIndex == -1 && !param[key][i]) {
// /**按顺序第一次赋值 */
// nullIndex = i;
// }
// }
// break;
}
errorStr = isError && errorStr == "" ? `${key}应该是${confType}型 而不是${valueType}`: errorStr;
if (isError) throw new BizError(ERRORENUM.参数错误, errorStr);
}
}
return param;
}
//对象判空
export function objectKeyIsNull(obj, ...keyNames) {
let isNull = false;
for (let i = 0; i < keyNames.length; i++) {
let keyStr = keyNames[i];
let moreKeyList = keyStr.split(".");
let lastObj;
for (let j = 0; j < moreKeyList.length; j++) {
lastObj = obj[moreKeyList[j]];
if (!lastObj) {
isNull = true;
break;
}
}
if (isNull) break;
}
return isNull;
}
/**
* 校验类型
* @param target 目标值
* @param type TYPEENUM枚举值
* @returns 通过 = true 不通过 = false
*/
export function checkType(target, type) {
if (target == undefined || target == null) return false;
switch (type) {
case TYPEENUM.string:
if (typeof target == 'string') {
return true;
}
break;
case TYPEENUM.number:
if (typeof target == 'number') {
return true;
}
break;
case TYPEENUM.object:
if (typeof target == 'object' && !Array.isArray(target) ) {
return true;
}
break;
case TYPEENUM.array:
if (typeof target == 'object' && Array.isArray(target) ) {
return true;
}
break;
case TYPEENUM.boolean:
if (typeof target == 'boolean') {
return true;
}
break;
};
return false;
}
export function checkStrLeng(str, length?) {
length = length ? length : 40;
let result = true;
if (str.length < length) result = false;
return result;
}
const { parseString } = require('xml2js');
import * as iconv from 'iconv-lite';
/**
* 解析用户信息XML响应
*/
export async function parseUserNameInfoXml(xmlData: string): Promise<any> {
return new Promise((resolve, reject) => {
// 先统一转换编码
const utf8XmlData = convertEncoding(xmlData, 'gbk');
parseString(utf8XmlData, {
explicitArray: false,
trim: true
}, (err, result) => {
if (err) {
console.error('XML解析错误:', err);
return resolve(null);
}
try {
// 解析XML结构并提取用户信息
const response = result.methodResponse;
if (!response || !response.params || !response.params.param) {
console.error('无效的XML响应结构');
return resolve(null);
}
// 提取用户信息数组
const params = Array.isArray(response.params.param) ?
response.params.param : [response.params.param];
if (params.length > 0 && params[0].value && params[0].value.array) {
const data = params[0].value.array.data;
if (data && data.value && data.value.length > 0) {
// 提取第一个用户信息
const userValue = data.value[0];
if (userValue.struct) {
const member = userValue.struct.member;
const userInfo: any = {};
// 处理成员字段
(Array.isArray(member) ? member : [member]).forEach((m: any) => {
if (m.name && m.value) {
const key = m.name;
const value = m.value.string || m.value.int || m.value.double || '';
userInfo[key] = value; // 不再需要单独转换编码
}
});
console.log('成功提取用户信息:', userInfo);
return resolve(userInfo);
}
}
}
console.error('未找到用户信息在响应中');
resolve(null);
} catch (parseError) {
console.error('响应解析错误:', parseError);
resolve(null);
}
});
});
}
/**
* 解析用户权限信息XML响应
*/
export async function parseUserInfoXml(xmlData: string): Promise<any> {
return new Promise((resolve, reject) => {
// 先统一转换编码
const utf8XmlData = convertEncoding(xmlData, 'gbk');
parseString(utf8XmlData, {
explicitArray: false,
trim: true,
// 添加值处理器来处理不同类型的数据
valueProcessors: [
(value: string, name: string) => {
// 处理整型数据
if (name === 'int') {
return parseInt(value, 10);
}
// 处理字符串数据(不再需要编码转换)
if (name === 'string') {
return value;
}
return value;
}
]
}, (err, result) => {
if (err) {
console.error('XML解析错误:', err);
return resolve(null);
}
try {
// 解析XML结构
const response = result.methodResponse;
if (!response || !response.params || !response.params.param) {
console.error('无效的XML响应结构');
return resolve(null);
}
// 提取参数值
const param = response.params.param;
const value = param.value;
if (value && value.array && value.array.data) {
const data = value.array.data;
// 检查是否有值
if (data.value && data.value.struct) {
const struct = data.value.struct;
// 提取成员信息
if (struct.member) {
const members = Array.isArray(struct.member) ?
struct.member : [struct.member];
// 构建用户信息对象
const userInfo: any = {};
members.forEach((member: any) => {
if (member.name && member.value) {
const key = member.name;
let value:any = '';
// 根据值类型提取数据
if (member.value.string) {
value = member.value.string; // 不再需要转换编码
} else if (member.value.int) {
value = parseInt(member.value.int, 10);
} else if (member.value.double) {
value = parseFloat(member.value.double);
} else if (member.value.boolean) {
value = member.value.boolean === '1' ||
member.value.boolean === 'true';
} else {
// 尝试直接获取值
value = member.value;
}
userInfo[key] = value;
}
});
console.log('成功提取用户信息:', userInfo);
return resolve(userInfo);
}
}
}
console.error('未找到用户信息在响应中');
resolve(null);
} catch (parseError) {
console.error('响应解析错误:', parseError);
resolve(null);
}
});
});
}
/**
* 更简化的解析方法(针对您提供的XML结构)
*/
export async function parseUserInfoXmlSimple(xmlData: string): Promise<any> {
return new Promise((resolve, reject) => {
// 先统一转换编码
const utf8XmlData = convertEncoding(xmlData, 'gbk');
parseString(utf8XmlData, {
explicitArray: false,
trim: true
}, (err, result) => {
if (err) {
console.error('XML解析错误:', err);
return resolve(null);
}
try {
// 简化解析逻辑
const userInfo: any = {};
// 提取所有成员
const members = result?.methodResponse?.params?.param?.value?.array?.data?.value?.struct?.member;
if (!members) {
console.error('未找到用户信息在响应中');
return resolve(null);
}
// 处理单个或多个成员
const memberList = Array.isArray(members) ? members : [members];
memberList.forEach((member: any) => {
if (member.name && member.value) {
const key = member.name;
let value: any;
// 根据值类型处理
if (member.value.string !== undefined) {
value = member.value.string; // 不再需要转换编码
} else if (member.value.int !== undefined) {
value = parseInt(member.value.int, 10);
} else if (member.value.double !== undefined) {
value = parseFloat(member.value.double);
} else if (member.value.boolean !== undefined) {
value = member.value.boolean === '1' ||
member.value.boolean === 'true';
} else {
value = member.value;
}
userInfo[key] = value;
}
});
console.log('成功提取用户信息:', userInfo);
resolve(userInfo);
} catch (parseError) {
console.error('响应解析错误:', parseError);
resolve(null);
}
});
});
}
/**
* 统一的编码转换函数 (GBK -> UTF-8)
*/
// export function convertEncoding(text: any, from: string = 'gbk'): string {
// if (!text) return '';
// try {
// if (Buffer.isBuffer(text)) {
// return iconv.decode(text, from);
// }
// if (typeof text === 'string') {
// // 如果是字符串,先转换为Buffer再解码
// const buffer = Buffer.from(text, 'binary');
// return iconv.decode(buffer, from);
// }
// return String(text);
// } catch (error) {
// console.warn('编码转换失败:', error);
// return typeof text === 'string' ? text : String(text);
// }
// }
/**
* 增强的编码转换函数
*/
export function convertEncoding(data: any, from: string = 'gbk'): string {
if (!data) return '';
try {
if (Buffer.isBuffer(data)) {
// 直接解码Buffer
return iconv.decode(data, from);
}
if (typeof data === 'string') {
// 检查是否已经是正确编码
if (!hasGarbledCharacters(data)) {
return data;
}
// 尝试修复可能的双重编码
const fixed = fixDoubleEncoding(data);
if (!hasGarbledCharacters(fixed)) {
return fixed;
}
// 如果修复失败,尝试将字符串转换为Buffer再解码
const buffer = Buffer.from(data, 'binary');
return iconv.decode(buffer, from);
}
return String(data);
} catch (error) {
console.warn('增强编码转换失败:', error);
return typeof data === 'string' ? data : String(data);
}
}
/**
* 检测字符串是否包含乱码字符
*/
export function hasGarbledCharacters(str: string): boolean {
if (!str || typeof str !== 'string') return false;
// 检查常见的乱码模式
const garbledPatterns = [
/\\u[0-9a-fA-F]{4}/g, // Unicode转义序列
/�/g, // Unicode替换字符
/[^\x00-\x7F\u4E00-\u9FFF]/g // 非ASCII和非中文字符
];
return garbledPatterns.some(pattern => pattern.test(str));
}
/**
* 修复可能的双重编码问题
*/
export function fixDoubleEncoding(str: string): string {
if (!str || typeof str !== 'string') return str || '';
try {
// 尝试将字符串视为UTF-8编码的GBK数据
// 1. 先将字符串转换为Buffer(假设它是UTF-8)
const utf8Buffer = Buffer.from(str, 'utf8');
// 2. 然后将这个Buffer作为GBK解码
return iconv.decode(utf8Buffer, 'gbk');
} catch (error) {
console.warn('双重编码修复失败:', error);
return str;
}
}
/**
* 解析用户身份代码
* 1:是
* 0:否
* @param userTypeString
* @returns
*/
export function parseUserTypeEnhanced(userTypeString) {
// 配置项:身份类型定义
const ROLE_CONFIG = [
{ key: 'generalUser', label: '一般注册用户' },
{ key: 'administrator', label: '行政管理人员' },
{ key: 'staff', label: '教职员工' },
{ key: 'student', label: '学生' },
{ key: 'parent', label: '家长' }
];
// 验证输入
if (typeof userTypeString !== 'string') {
throw new Error('输入必须是一个字符串');
}
if (userTypeString.length !== 5) {
throw new Error('字符串长度必须为5位');
}
if (!/^[01]+$/.test(userTypeString)) {
throw new Error('字符串只能包含0和1');
}
// 构建结果对象
const result = {};
ROLE_CONFIG.forEach((role, index) => {
result[role.label] = parseInt(userTypeString[index], 10);
});
return result;
}
/**
* 系统中使用的工具
* 包括 草稿箱id生成规则
* 包括 密码加密规则
*/
import moment = require("moment");
import { selectOneDataByParam } from "../data/findData";
import { TABLENAME } from "../config/dbEnum";
import { systemConfig } from "../config/serverConfig";
const md5 = require("md5");
/**
* 根据入学年份获取当前年级名称
* @param enrollmentYear 入学年份,如2024
* @returns 年级名称,如:高一、高二、高三 或 已毕业
*/
export function getGradeName(enrollmentYear: number): string {
const currentYear = new Date().getFullYear();
const yearDiff = currentYear - enrollmentYear;
if (yearDiff === 0) return '高一';
if (yearDiff === 1) return '高二';
if (yearDiff === 2) return '高三';
if (yearDiff >= 3) return '已毕业';
return '未知';
}
/**
* 生成任务草稿箱Id
* @param uscc
* @returns
*/
export function getDraftId(uscc:string) {
return md5(`${uscc}${new Date().valueOf()}${Math.ceil(Math.random()*1000)}`);
}
/**
* 判断查询结果是否有数据
*/
export function hasData(result: any): boolean {
return result.data && Object.keys(result.data).length > 0;
}
/**
* 密码加密
* @param uscc 信用代码
* @param pwd 密码
* @returns md5后的密码
*/
export function getPwdMd5(uscc:string, pwd:string) {
return md5(uscc+pwd);
}
/**
* md5加密
* @param pwd
* @returns pwd 加密后密码
*/
export function md5PwdStr(pwd:string) {
return md5(pwd);
}
/**
* 获取token
* @param uscc 统一信用代码
*/
export function getToken(uscc:string) {
return md5(`${uscc}${new Date().valueOf()}${Math.ceil(Math.random() *100)}`);
}
/**
* 校验uscc是否合法
* @param uscc
* @returns true/false
*/
export function eccUscc(uscc:string) {
let isSuccess = false;
if (uscc.search(/^[A-Za-z0-9]{16}$/) > -1) isSuccess = true;
else if (uscc.search(/^[A-Za-z0-9]{18}$/) > -1) isSuccess = true;
return isSuccess;
}
/**
* 获取绑定id
* @param uscc 孵化器统一信用代码
* @param bindUscc 被绑定的孵化器统一信用代码
* @returns
*/
export function getBindId(uscc:string, bindUscc:string) {
return md5(`${uscc}${bindUscc}${Math.ceil(Math.random() *100)}`);
}
/**
* 获取今天开始时刻的时间戳 0时0分
* @returns
*/
export function getTodayMs() {
let t =`${ moment().format("YYYY-MM-DD")} 00:00:00`;
return new Date(t).valueOf();
}
/**
* 获取这个月的开始时刻的时间戳 0时0分
* @returns
*/
export function getThisMonthMs() {
let t =`${ moment().format("YYYY-MM")}-01 00:00:00`;
return new Date(t).valueOf();
}
/**
* 获取code的id
* @param uscc 发送人的uscc
* @param todaySendCount 今日发送次数
* @returns ''
*/
export function getSMSCodeId(uscc:string, todaySendCount:number) {
return md5(`${uscc}${todaySendCount}${new Date().valueOf()}`);
}
/**
* 获取一个随机6位数的验证码
* @returns
*/
export function getSMSCode() {
let code = ``;
for (let i =0; i < 6; i++) {
code += Math.floor(Math.random() * 10)
}
return code;
}
/**
* 生成融资id
* @param uscc
* @returns
*/
export function getFinancingId(uscc) {
return md5(`${uscc}${new Date().valueOf()}${Math.ceil(Math.random() * 1000)}`);
}
/**
* 生成创始团队成员id
* @param uscc
* @param name
* @returns
*/
export function getInitialTeamMemberId(uscc:string, name:string) {
return md5(`${uscc}${name}${Math.ceil(Math.ceil(Math.random() * 1000000))}`);
}
/**
* 生成 资讯id
* @returns
*/
export function getInformationId() {
return md5(`${Math.ceil(Math.ceil(Math.random() * 1000000))}${new Date().valueOf() }${Math.ceil(Math.ceil(Math.random() * 1000000))}`);
}
/**
* 获取上一个季度 年和季度
* @returns declarationYear:数据填报年 declarationQuarter:数据填报季度
*/
export function getLastQuarter() {
let thisYear = new Date().getFullYear();
let thisQuarter = moment().quarter();//当月填报季度
if ( (thisQuarter - 1) < 1 ) {
thisYear = moment().subtract(1, 'years').year();
thisQuarter = 4;
} else thisQuarter = thisQuarter - 1;
return {year:thisYear, quarter:thisQuarter};
}
export function getI18nRegisterId(uscc:string) {
return md5(`${uscc}${new Date().valueOf() }${Math.ceil(Math.ceil(Math.random() * 1000000))}`);
}
export function getPolicyBGImgId() {
return `bgImg_${md5(`bgImg${new Date().valueOf()}${Math.ceil(Math.ceil(Math.random() * 1000000))}`)}`
}
/**
* 获取随机id
* @param uscc
*/
export function randomId(tableName:string) {
let randomStr = `${new Date().valueOf()}_${Math.ceil(Math.random()*100000)}`;
return `${tableName}_${md5(randomStr)}`;
}
export function changeAddToString(address) {
if (!address) return "";
let str = "";
address.forEach((item, index) => {
if (index == 0 && item == "上海市") return;
str += item;
});
return str;
}
export function getLabelId() {
return `${md5(`label${new Date().valueOf()}${Math.ceil(Math.ceil(Math.random() * 10000))}${Math.ceil(Math.ceil(Math.random() * 10000))}`)}`
}
/**
* 匹配企查查注册地址规则
* @param original
* @returns
*/
export function formatAddress(original) {
// 匹配省/直辖市(支持"北京市"/"上海"等格式)
const cityMatch = original.match(/^(.*?(?:省|市|自治区|特别行政区))/);
const city = cityMatch ? cityMatch[1] : original.split(/[市区县]/)[0] + '市';
// 匹配区级(支持"浦东新区"/"朝阳区"等格式)
const districtMatch = original.match(/(?:省|市)(.*?(?:区|县|市|旗))/);
const district = districtMatch ? districtMatch[1] : '';
// 处理详细地址(自动保留原格式,仅修正明显错误)
let detail = original.replace(city, '').replace(district, '')
.replace(/(\d+)[幢栋](\d+)层/g, (_, num1, num2) => {
const chineseNums = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
return `${chineseNums[parseInt(num1)] || num1}${num2}${num2.length === 1 ? '室' : ''}`;
})
.trim();
return [city, city, district || city, detail || '地址不详'];
}
/**
* 更完善的注册资本格式化函数
* @param {string|number} value 注册资本值
* @returns {string} 格式化后的注册资本字符串
*/
export function formatRegisteredCapital(value) {
if (value === null || value === undefined) {
return '';
}
// 如果输入是数字,转换为字符串
if (typeof value === 'number') {
value = value.toString();
}
// 确保是字符串类型
if (typeof value !== 'string') {
return String(value);
}
// 去除首尾空格
value = value.trim();
// 空字符串直接返回
if (value === '') {
return value;
}
// 特殊情况:如果是"-"或其他占位符,直接返回
if (value === '-' || value === '--' || value === 'N/A' || value === 'null' || value === 'undefined') {
return value;
}
// 判断是否已经包含单位
// 定义常见单位列表
const units = [
'万元', '万', 'w', 'W',
'元', '¥',
'亿', '亿元',
'美元', '美金', 'usd', 'USD',
'人民币', 'RMB', 'CNY',
'欧元', 'EUR', 'euro',
'日元', 'JPY', 'yen',
'港元', '港币', 'HKD',
'英镑', 'GBP', 'pound'
];
// 检查是否已包含单位(不区分大小写)
const lowerValue = value.toLowerCase();
for (const unit of units) {
if (lowerValue.endsWith(unit.toLowerCase()) ||
lowerValue.includes(unit.toLowerCase() + ' ') ||
lowerValue.includes(' ' + unit.toLowerCase())) {
return value; // 已包含单位,直接返回
}
}
// 检查常见的外币符号
const currencySymbols = ['¥', '¥', '$', '€', '£', '¥'];
for (const symbol of currencySymbols) {
if (value.includes(symbol)) {
return value; // 包含货币符号,直接返回
}
}
// 尝试提取纯数字部分
// 移除所有非数字字符(除了小数点、逗号和负号)
const numericString = value.replace(/[^0-9.,-]/g, '');
// 检查是否包含有效数字
if (numericString && /^-?\d[\d,]*\.?\d*$/.test(numericString)) {
// 移除逗号并转换为数字
const numericValue = parseFloat(numericString.replace(/,/g, ''));
// 根据数值大小选择合适的单位
if (!isNaN(numericValue)) {
// 如果数字大于等于10000万(即1亿),可以考虑使用"亿元"
if (numericValue >= 100000000) {
const billionValue = (numericValue / 100000000).toFixed(2);
// 去除末尾的.00
const formatted = billionValue.replace(/\.00$/, '');
return formatted + '亿元';
}
// 如果数字大于等于1000,使用"万元"
else if (numericValue >= 1000) {
const tenThousandValue = (numericValue / 10000).toFixed(2);
const formatted = tenThousandValue.replace(/\.00$/, '');
return formatted + '万元';
}
// 否则直接添加"万元"
else {
// 如果是整数,不显示小数位
if (Number.isInteger(numericValue)) {
return numericValue + '万元';
} else {
return numericValue.toFixed(2).replace(/\.00$/, '') + '万元';
}
}
}
}
// 如果无法解析为数字,但看起来像是金额(包含数字)
if (/\d/.test(value)) {
return value + '万元';
}
// 其他情况返回原值
return value;
}
export function getFilesId(upType:number, type:string, fileName:string) {
return `${fileName.replace(type, "")}${moment().format("YYYYMMDD_HHmmSS")}`;
}
/**
* 格式化附件(将 JSON 字符串转换为数组)
* @param attachment 附件 JSON 字符串,格式:'["path1", "path2"]'
* @returns 附件数组
*/
export function formatAttachment(attachment: string | null): string[] {
if (!attachment) {
return [];
}
try {
const parsed = JSON.parse(attachment);
if (Array.isArray(parsed)) {
return parsed;
}
return [];
} catch (e) {
// 兼容旧的逗号分隔格式
if (typeof attachment === 'string' && attachment.includes(',')) {
return attachment.split(',').map(item => item.trim()).filter(item => item);
}
return attachment ? [attachment] : [];
}
}
/**
* 将附件数组转换为 JSON 字符串存储
* @param attachments 附件数组或字符串
* @returns JSON 字符串
*/
export function stringifyAttachments(attachments: string | string[] | null): string | null {
if (!attachments) {
return null;
}
let attachmentArray: string[] = [];
if (Array.isArray(attachments)) {
attachmentArray = attachments;
} else if (typeof attachments === 'string') {
// 如果是字符串,尝试判断是否为 JSON 格式
try {
const parsed = JSON.parse(attachments);
if (Array.isArray(parsed)) {
attachmentArray = parsed;
} else {
attachmentArray = [attachments];
}
} catch (e) {
// 不是 JSON 格式,当作单个文件处理
attachmentArray = [attachments];
}
}
// 过滤空值
attachmentArray = attachmentArray.filter(item => item && item.trim());
if (attachmentArray.length === 0) {
return null;
}
return JSON.stringify(attachmentArray);
}
/**
* 格式化附件(将 JSON 字符串转换为数组,并提取文件名和完整 URL)
* @param attachment 附件 JSON 字符串,格式:'["path1", "path2"]'
* @returns 包含 name 和 url 的对象数组
*/
export function formatAttachmentNameUrl(attachment: string | null): Array<{ name: string, url: string }> {
if (!attachment) {
return [];
}
let paths: string[] = [];
try {
const parsed = JSON.parse(attachment);
if (Array.isArray(parsed)) {
paths = parsed;
}
} catch (e) {
// 兼容旧的逗号分隔格式
if (typeof attachment === 'string' && attachment.includes(',')) {
paths = attachment.split(',').map(item => item.trim()).filter(item => item);
} else if (attachment) {
paths = [attachment];
}
}
return paths.map(path => {
// 提取文件名(从路径中获取最后一个 / 后面的部分)
const fileName = path.split('/').pop() || path;
return {
name: fileName,
url: `${systemConfig.img}${path}`
};
});
}
import moment = require("moment");
import { ERRORENUM } from "../config/errorEnum";
import { BizError } from "../util/bizError";
import { FILETYPE } from "../config/enum";
const md5 = require("md5");
export function randomId(tableName:string) {
let randomStr = `${new Date().valueOf()}_${Math.ceil(Math.random()*100000)}`;
return `${tableName}_${md5(randomStr)}`;
}
export function getUserToken(loginId:string) {
return md5(`${loginId}_${Math.ceil(Math.random()*1000)}${new Date().valueOf()}`);
}
/**
* 密码加密
* @param uscc 信用代码
* @param pwd 密码
* @returns md5后的密码
*/
export function getPwdMd5(uscc:string, pwd:string) {
return md5(uscc+pwd);
}
export function getMySqlMs(time?) {
time = time || new Date().valueOf();
// time += (8*3600*1000);
return moment(time).format("YYYY-MM-DD HH:mm:ss");
}
export function getClientMs(time) {
if (!time) return new Date().valueOf();
return new Date(time).valueOf();
}
export function getPartyMemberId(param) {
return md5(`${param}-${new Date().valueOf()}-${Math.ceil(Math.random() * 10000)}`);
}
export function getDefPwd(phone:string) {
return md5(`${phone.slice(5, 11)}`);
}
export function getFileType(fileName) {
let fileType = 0;
fileName.forEach(info => {
let repList = info.split(".");
let type = repList[repList.length-1];
if (!type) throw new BizError(ERRORENUM.文件不存在, `文件名 ${info}`);
let typeNum = 0;
switch(type) {
case 'pdf': typeNum = FILETYPE.pdf; break;
case 'doc':
case 'docx': typeNum = FILETYPE.word; break;
case 'jpg':
case 'png': typeNum = FILETYPE.图片; break;
};
if (typeNum) {
if (!fileType) fileType = typeNum;
else if (fileType != typeNum) fileType = FILETYPE.多类型;
}
});
return fileType;
}
const xlsx = require('node-xlsx');
const path = require('path');
/**
* onceSheetBecomeOfblockData 将excel文件的指定sheet解析成数据块数据
* @param fileName 文件名称
* @param sheetName 表名称
* @returns [ {blockData:数据块(二维数组), blockTitle:"数据标题"}]
*/
export function onceSheetBecomeOfblockData(fileName, sheetName) {
let {sheetMap} = getExcel( path.join(__dirname.substring(0,__dirname.indexOf("out")), "res", fileName ));
// return sheetMap;
let thisBlockData = getBlockData(sheetMap[sheetName]);
return thisBlockData;
}
/**
* excelBecomeOfBlockData 将excel所有的sheet解析成数据块
* @param fileName 文件名称
* @returns {"sheetName1":[ {blockData:数据块(二维数组), blockTitle:"数据标题"}], ...}
*/
export function excelBecomeOfBlockData(fileName) {
let {sheetMap} = getExcel( path.join(__dirname.substring(0,__dirname.indexOf("out")), "res", fileName ));
let result = {};
for (let sheetName in sheetMap) {
result[sheetName] = getBlockData(sheetMap[sheetName]);
}
return result;
}
/**
* planaryArrayBecomeOfBlockData 将符合excel规则的sheet二维数组转为 数据块
* @param dataList excel解出来的数据
* @returns thisBlockData 返回数据块集合 格式:blockList = [ {blockData:数据块(二维数组), blockTitle:"数据标题"}]
*/
export function planaryArrayBecomeOfBlockData(planaryArray) {
return getBlockData(planaryArray);;
}
//===
/**
* getBlockData 数据分块
* @param dataList 解析出来的excel二维数组
* @returns 返回数据块集合 格式:blockList = [ {blockData:数据块(二维数组), blockTitle:"数据标题"}]
*/
function getBlockData(dataList) {
let blockList = [];
for (let i = 0; i < 999; i++) {
let {blockData, blockTitle, notItem, delDataList} = checkBlock(dataList);
if (notItem) break;
dataList = delDataList;
if (blockTitle) blockList.push({blockData, blockTitle});
}
return blockList;
}
function getListFristNotNullItemIndex(list) { //获取起始坐标
if (!list.length) return null;
for (let i = 0; i < list.length; i++) {
if (list[i]) return i;
}
}
function getListFirstNullItemIndex(startX, list) { //获取第一个为空的坐标
if (!list.length) return null;
let checkItem = false;
let firstItemIndex = 0;
for (let i = startX; i <= list.length; i++) {
let item = list[i];
if (!checkItem && item) checkItem = true;
if (checkItem && !item) {
firstItemIndex = i;
break;
}
}
return firstItemIndex;
}
function listRegionIsNull(list, startX, endX) { //指定区间内数据是否未空
let isNull = true;
if ( !list.length ) return isNull;
for (let i = startX; i < endX; i++) {
let item = list[i];
if (item) {
isNull = false;
break;
}
}
return isNull;
}
function thisListNotItem(list) {
for (let i = 0; i < list.length; i++) {
if (list[i]) return false;
}
return true
}
function checkBlock(dataList) {
//纵向有效起始点
let startY = 0;
let startX = 0;
let isNotBlockTitle = false; //没有块标题
let isLook = false;
let endX = 0;//x轴最长结束下标 【包括下标】
let blockTitle = ''; //标题块名称
let notItem = true;
for (let i = 0; i < dataList.length; i++) {
let childList = dataList[i] || [];
if (!thisListNotItem(childList)) {
if ( !isLook ) {
let thisRoowStartX = getListFristNotNullItemIndex(childList);
let thisRoowLastItem = childList[thisRoowStartX + 1];
let LastList = dataList[i+1] || [];
// let lastRoowStartX = getListFristNotNullItemIndex(LastList);
let lastRoowHaveItem = LastList[thisRoowStartX];
if ( thisRoowLastItem || (LastList.length && lastRoowHaveItem) ) {
if (lastRoowHaveItem && thisRoowLastItem ) {
isNotBlockTitle = true; //不存在标题块
blockTitle = `${thisRoowStartX}_${i}`;
startY = i;
startX = thisRoowStartX;
}
else {
blockTitle = dataList[i][thisRoowStartX];
dataList[i][thisRoowStartX] = null;
if ( thisRoowLastItem ) { // 同行存在元素 标题在y轴上
startY = i;
startX = thisRoowStartX + 1;
} else { // 同行存在元素 标题在x轴上
startY = i + 1;
startX = thisRoowStartX;
}
}
isLook = true;
} else { //只有标题 无内容
console.log(dataList[i][thisRoowStartX]);
dataList[i][thisRoowStartX] = null;
}
} else {
//测量最大连续长度
let firstNullX = getListFirstNullItemIndex(startX, childList);
if (firstNullX) endX = Math.max(endX, firstNullX-1);
break;
}
notItem = false;
}
}
let endY = 0;//y轴连续下标 【包括下标】
let yInfoStart = false;
let yInfoEnd = false;
for (let y = startY; y < dataList.length; y++) {
//纵向找连续性
let thisRoow = dataList[y];
let regionIsNull = listRegionIsNull(thisRoow, startX, endX);
if (!regionIsNull) {
endY = y;
if (!yInfoStart) yInfoStart = true;
}
if (yInfoStart && regionIsNull) yInfoEnd = true;
if (yInfoEnd) break;
}
let blockData = [];
for (let y = startY; y <= endY; y++) {
let onceList = [];
for (let x = startX; x <= endX; x++) {
onceList.push(dataList[y][x]);
dataList[y][x] = null;
}
blockData.push(onceList);
}
return {blockData, blockTitle, delDataList:dataList,notItem};
}
//获取单个excel文件的数据
function getExcel(filePath) {
const workSheetsFromFile = xlsx.parse(filePath);
let sheetMap = {};
let sheetList = [];
for (let i = 0; i < workSheetsFromFile.length; i++) {
let sheetInfo = workSheetsFromFile[i];
sheetMap[sheetInfo.name] = sheetInfo.data;
sheetList.push(sheetInfo);
}
return {sheetMap, sheetList}
}
\ No newline at end of file
/**
* 异常类
* 需要和log4js共同使用
*/
import { getBizMsg } from "../config/errorEnum";
import { logError } from "./log";
export class BizError extends Error {
statusCode(statusCode: any) {
throw new Error('Method not implemented.');
}
constructor(...msgs) {
let reqErrorMsg = '';
let logErrorMsg = '';
for (let i = 0; i <msgs.length; i++) {
if (!i) {
let msg = getBizMsg(msgs[i]);
reqErrorMsg = msg;
logErrorMsg = msg;
} else {
logErrorMsg += ` | ${msgs[i]} `;
}
}
logError(logErrorMsg);
super(reqErrorMsg);
}
}
/**
* 日志类
* 包括错误日志 普通日志
* 日志存放在根目录的logs内
*/
let log4js = require('log4js');
let path = require('path');
//log路径
export const systemLogPath = {
errorLogFile:"error",
errorLogDir:"error",
handleLogFile:"handle",
handleLogDir:"handle"
}
//日志根目录
// let baseLogPath = path.resolve(__dirname.substring(0, __dirname.indexOf("out")), 'logs');
let baseLogPath = path.resolve('./', 'logs');
let errFile = path.resolve(baseLogPath, systemLogPath.errorLogDir, systemLogPath.errorLogFile);
let handFile =path.resolve(baseLogPath, systemLogPath.handleLogDir, systemLogPath.handleLogFile);
let config = {
appenders:
{
"rule-console": {"type": "console"},
"errorLogger": {
"type": "dateFile", // 日志类型
"filename": errFile, // 输出文件名
"pattern": "yyyy-MM-dd.log", // 后缀
"alwaysIncludePattern": true, // 上面两个参数是否合并
"encoding": "utf-8", // 编码格式
"maxLogSize": 1000, // 最大存储内容
"numBackups": 3, // 当文件内容超过文件存储空间时,备份文件的数量
"path": `/${systemLogPath.errorLogDir}`
},
"handleLogger": {
"type": "dateFile",
"filename": handFile,
"pattern": "yyyy-MM-dd.log",
"alwaysIncludePattern": true,
"encoding": "utf-8",
"maxLogSize": 1000,
"numBackups": 3,
"path": `/${systemLogPath.handleLogDir}`
}
},
categories: {
"default": {"appenders": ["rule-console"], "level": "all"}, //这个配置一定要有
"errorLogger": {"appenders": ["errorLogger"], "level": "error"},
"handleLogger": {"appenders": ["handleLogger"], "level": "all"}
},
"baseLogPath": path.resolve(baseLogPath, systemLogPath.handleLogDir, systemLogPath.handleLogFile)
};
log4js.configure(config); //加载配置文件
//调用预先定义的日志名称
let errorLogger = log4js.getLogger("errorLogger");
let handleLogger = log4js.getLogger("handleLogger");
let consoleLogger = log4js.getLogger("rule-console");
//错误日志
export function logError(...errStrs) {
let str = "";
errStrs.forEach(item => {
str += item + " | ";
});
errorLogger.error(`errorInfo => ${str}`);
}
//普通日志
export function logHandle(msgStr:string) {
handleLogger.info(`logInfo => ${msgStr}`);
}
//输出日志
export function logConsole(logStr:string) {
consoleLogger.info(`logInfo => ${logStr}`);
}
/**
* 解析xml
*/
var xml2js = require("xml2js");
/**
*
* @param str 需要解析的xml文本
* @returns 解析好的对象
*/
export function analysisXml(str) {
return new Promise( (resolve, reject) => {
xml2js.parseString(str, (err, result) => {
if (err) return reject(err);
return resolve(result);
});
});
}
\ No newline at end of file
/**
* 零碎的通用工具
*/
import moment = require("moment");
/**
* 匹配新旧对象变化
* 将newObj 与 oldObj 比对,将newObj中发生变化的key返回
* 使用前需要校验对象中的内容
* @param newObj 新对象
* @param oldObj 旧对象
* @returns [key] 发生变化的key
*/
export function checkChange(newObj, oldObj) {
let changeKeyList = [];
for (let newKey in newObj) {
if (`${newObj[newKey]}` != `${oldObj[newKey]}`) changeKeyList.push(newKey);
}
return changeKeyList;
}
/**
* 根据conf截取data中的数据
* @param conf
* @param data
* @returns
*/
export function extractData(conf, data, isAdmin) {
let result = {};
for (let key in conf) {
let confInfo = conf[key];
if (confInfo.changeDate) {
if (isAdmin) result[key] = data[key] ? moment(data[key]).format("YYYY-MM-DD") : '-';
else result[key] = data[key] || 0;
} else if (confInfo.isAdd && isAdmin) {
let addStr = "";
data[key].forEach(str => {
addStr += str;
});
result[key] = addStr;
}
else {
result[key] = data[key];
if (typeof result[key] == 'string' && !result[key]) result[key] = '';
}
}
return result;
}
/**
* 校验数据对象是否有空
* @param data
* @param sensitive 敏感校验 true时 0 和 ""会校验失败 false时 校验成功
* @returns true/false true = 有空值 false=无空值
*/
export function checkDataHaveNull(data:object, sensitive:boolean) {
if (Array.isArray(data)) return data.length == 0;
if (Object.keys(data).length == 0) return true;
let success = false;
for (let key in data) {
if (data[key] == null || data[key] == undefined) success = true;
if (sensitive) {
if (data[key] === 0 || data[key] === "" ) success = true;
}
}
return success;
}
\ No newline at end of file
/**
* 请求工具
*/
import request from 'request';
import { BizError } from './bizError';
/**
* 请求接口(get)
* @param url 路由
* @param query 请求参数
* @param headers 请求头
* @returns
*/
export function get(url:string, query?, headers?) {
if (!url || (url.search(/http:/) && url.search(/https:/)) ) throw new BizError(!url ? "请求地址为空" : "请求地址错误");
return new Promise((resolve, reject)=>{
let paramater:any = { url, json:true };
if (query) paramater.qs = query;
if (headers) paramater.headers = headers;
request.get(paramater, function (err, r, body) {
if (err) return reject(err);
if (r && r.statusCode != 200) return reject(new Error('httpError:'+r.statusCode));
resolve(body);
});
})
}
export function post(url, body, headers) {
if (!url || (url.search(/http:/) && url.search(/https:/)) ) throw new BizError(!url ? "请求地址为空" : "请求地址错误");
let header = {"content-type": "application/json"};
return new Promise((resolve, reject)=>{
request({
url: url,
method: "POST",
json: true,
headers: Object.assign(header, headers),
body: body
}, function(error, response, body) {
if (!error && response.statusCode == 200) {
resolve(body);
}
else {
// reject(error)
}
});
})
}
export function postForm(url, body, headers) {
if (!url || (url.search(/http:/) && url.search(/https:/)) ) throw new BizError(!url ? "请求地址为空" : "请求地址错误");
return new Promise((resolve, reject)=>{
request({
url: url,
method: "POST",
json: true,
form:body
}, function(error, response, res) {
if (!error) {
resolve(res);
}
else {
reject(error)
}
});
})
}
/**
* 校验枚举工具
*
*/
import { ERRORENUM } from "../config/errorEnum";
import { BizError } from "./bizError";
/**
* 校验value是否符合传入的枚举
* @param name 被掉用名称 用于输出异常日志
* @param key 目标字段 用于输出异常日志
* @param enumConf 目标枚举
* @param value 目标值
* 无返回 有异常直接报错
*/
export function eccEnumValue(name:string, key:string, enumConf, value:any) {
let eccSuccess = true;
if ( typeof value == 'number' ) {
if (!enumConf[value] ) eccSuccess = false;
} else if (Array.isArray(value)) {
value.forEach(item => {
if ( !enumConf[item] ) eccSuccess = false;
});
}
if (!eccSuccess) throw new BizError(ERRORENUM.请完善信息, `${name} 下的 ${key} 字段值为 ${value} 不满足枚举范围`);
}
/**
* 将枚举值转换成对应的枚举名(key)
* @param enumConf 目标枚举
* @param value 目标值
* @returns string 返回字符串 如果传入多个枚举值,就拼接字符串
*/
export function changeEnumValue(enumConf, value:any) {
if (!value) return '';
if ( typeof value == 'number' ) {
let str = enumConf[value];
/** 特化处理 中文引号在枚举中不适用*/
if (str == "_投资__孵化_类型") {
str = str.replace("__","+");
str = str.replace("_","“");
str = str.replace("_","”");
}
if (str == "经营成本过高_场地成本或人员成本_" || str == "办公空间拓展_无合适办公空间_") {
str = str.replace("_","(");
str = str.replace("_",")");
}
if (str == "迁出孵化器_仍在张江" || str == "迁出张江_仍在浦东" || str == "迁出浦东_仍在上海") {
str = str.replace("_",",");
}
if (str == "科技金融_风险投资_" || str == "科技金融_其他_" || str == "技术专家_法律专家_") {
str = str.replace("_","(");
str = str.replace("_",")");
}
return str
}
let str = "";
value.forEach((item, index) => {
let subStr = enumConf[item];
/** 特化处理 中文引号在枚举中不适用*/
if (subStr == "_投资__孵化_类型") {
subStr = subStr.replace("__","+");
subStr = subStr.replace("_","“");
subStr = subStr.replace("_","”");
}
if (subStr == "经营成本过高_场地成本或人员成本_" || subStr == "办公空间拓展_无合适办公空间_") {
subStr = subStr.replace("_","(");
subStr = subStr.replace("_",")");
}
if (subStr == "迁出孵化器_仍在张江" || subStr == "迁出张江_仍在浦东" || subStr == "迁出浦东_仍在上海") {
subStr = subStr.replace("_",",");
}
if (subStr == "科技金融_风险投资_" || subStr == "科技金融_其他_" || subStr == "技术专家_法律专家_") {
subStr = subStr.replace("_","(");
subStr = subStr.replace("_",")");
}
str += subStr;
if (index == value.length-1) str+="";
else str += ","
});
return str;
}
export function changeEnumManyValue(enumConf, value:any) {
let list = [];
value.forEach((item, index) => {
let subStr = enumConf[item];
/** 特化处理 中文引号在枚举中不适用*/
if (subStr == "_投资__孵化_类型") {
subStr = subStr.replace("__","+");
subStr = subStr.replace("_","“");
subStr = subStr.replace("_","”");
}
if (subStr == "经营成本过高_场地成本或人员成本_" || subStr == "办公空间拓展_无合适办公空间_") {
subStr = subStr.replace("_","(");
subStr = subStr.replace("_",")");
}
if (subStr == "迁出孵化器_仍在张江" || subStr == "迁出张江_仍在浦东" || subStr == "迁出浦东_仍在上海") {
subStr = subStr.replace("_",",");
}
if (subStr == "科技金融_风险投资_" || subStr == "科技金融_其他_" || subStr == "技术专家_法律专家_") {
subStr = subStr.replace("_","(");
subStr = subStr.replace("_",")");
}
list.push(subStr);
});
return list;
}
/**
* 根据枚举值反向获取名称
* @param enumConf
* @param code
* @returns
*/
export function changeEnumNameByCode(enumConf, code: string) {
for (const [name, value] of Object.entries(enumConf)) {
if (value === code) {
return name;
}
}
return '未知行业';
}
/**
* 校验参数工具
*
*/
import { ERRORENUM } from "../config/errorEnum";
import { BizError } from "./bizError";
/**
* 通过config校验表单参数param
* 包括类型 String, Number, Boolean, [Number], [Object]
* 参数是必填
* 方法会校验表单中存在的多余字段
* @param name 被调用的方法名
* @param config 校验配置
* @param param 需要校验的参数
* @returns true 无需关注返回
*/
export function eccFormParam(name:string, keyTypeConf:object, param:object) {
/**校验多余字段 */
for (let key in param) {
if (!keyTypeConf[key]) throw new BizError(ERRORENUM.参数错误, name, `多余${key}字段`);
}
/**校验已填参数 */
for (let key in keyTypeConf ) {
let {type, notMustHave} = keyTypeConf[key];
let isError = false; //校验是否异常
let errorStr = "";//异常说明
let value = param[key];
let valueType = typeof value;
if ( value == null || value == undefined ) {
if (!notMustHave) throw new BizError(ERRORENUM.参数错误, name, `缺失${key}字段`);
} else {
switch(type) {
case 'Number':
if ( type.toLowerCase() != valueType ) {
isError = true;
} else {
if ((""+param[key]).indexOf('.') > -1) {
param[key] = parseInt(`${param[key] *1000}`)/1000;
}
}
break;
case 'String':
case 'Boolean':
if ( type.toLowerCase() != valueType ) isError = true;
break;
case '[Number]':
if ( !Array.isArray(param[key]) ) isError = true;
for (let i =0; i < param[key].length; i++) {
let item = param[key][i];
if ( typeof item != 'number' ) {
isError = true;
errorStr = `${key}应是number型数组其中下标${i}${typeof item}`;
}
}
break;
case '[Object]':
if ( !Array.isArray(param[key]) ) isError = true;
for (let i =0; i < param[key].length; i++) {
let item = param[key][i];
if ( typeof item != 'object' ) {
isError = true;
errorStr = `${key}应是object型数组其中下标${i}${typeof item}`;
}
}
break;
case 'Address':
/**地址类型 基本数据类型为数组字符串但是要判断层级关系 */
if ( !Array.isArray(param[key]) ) {
isError = true;
errorStr = `${key}应是数组形`;
}
if ( param[key].length != 4) {
isError = true;
errorStr = `${key}超过特定长度4 目前长度 ${param[key].length}`;
}
for (let i =0; i < param[key].length; i++) {
let item = param[key][i];
if ( typeof item != 'string' ) {
isError = true;
errorStr = `${key}应是string型数组其中下标${i}${typeof item}`;
}
}
/** 不符合规则的 */
let nullIndex = -1;
for (let i = 0; i < param[key].length; i++) {
if (nullIndex != -1) {//出现过空 第一次出现后的位置 都不能有值
if (param[key]) {
//做一个特化
throw new BizError(ERRORENUM.地址数据不完整, name, `${key} 下标 ${nullIndex} 为空 `);
}
}
if (nullIndex == -1 && !param[key][i]) {
/**按顺序第一次赋值 */
nullIndex = i;
}
}
break;
}
errorStr = isError && errorStr == "" ? `${key}应该是${type}型 而不是${valueType}`: errorStr;
if ( isError ) throw new BizError(ERRORENUM.请完善信息, name, errorStr);
}
}
return true;
}
/**
* 根据conf配置校验请求参数
* @param conf 配置
* @param param 表单
* @param skipKeys []不必填的字段
*/
export function eccReqParamater(conf:object, param, skipKeys?) {
skipKeys = skipKeys || [];
let skipMap = {};
skipKeys.forEach(keyName => {
skipMap[keyName] = 1;
});
/**校验多余字段 */
for (let key in param) {
if (!conf[key]) throw new BizError(ERRORENUM.参数错误, `多余${key}字段`);
}
/**校验必填和缺失字段 */
for (let key in conf) {
let confType = conf[key];
let value = param[key];
let valueType = typeof value;
if ( value == null || value == undefined ) {
if (!skipMap[key]) throw new BizError(ERRORENUM.参数错误, `缺失${key}字段`);
} else {
let isError = false;
let errorStr = "";
switch(confType) {
case 'Number':
if ( confType.toLowerCase() != valueType ) isError = true;
else {
if ((""+param[key]).indexOf('.') > -1) {
param[key] = parseInt(`${param[key] *100}`)/100;
}
}
break;
case 'String':
case 'Boolean':
if ( confType.toLowerCase() != valueType ) isError = true;
break;
case '[Number]':
if ( !Array.isArray(param[key]) ) isError = true;
for (let i =0; i < param[key].length; i++) {
let item = param[key][i];
if ( typeof item != 'number' ) {
isError = true;
errorStr = `${key}应是number型数组其中下标${i}${typeof item}`;
}
}
break;
case '[Object]':
if ( !Array.isArray(param[key]) ) isError = true;
for (let i =0; i < param[key].length; i++) {
let item = param[key][i];
if ( typeof item != 'object' ) {
isError = true;
errorStr = `${key}应是object型数组其中下标${i}${typeof item}`;
}
}
break;
case 'Address':
/**地址类型 基本数据类型为数组字符串但是要判断层级关系 */
if ( !Array.isArray(param[key]) ) {
isError = true;
errorStr = `${key}应是数组形`;
}
if ( param[key].length != 4) {
isError = true;
errorStr = `${key}超过特定长度4 目前长度 ${param[key].length}`;
}
for (let i =0; i < param[key].length; i++) {
let item = param[key][i];
if ( typeof item != 'string' ) {
isError = true;
errorStr = `${key}应是string型数组其中下标${i}${typeof item}`;
}
}
/** 不符合规则的 */
let nullIndex = -1;
for (let i = 0; i < param[key].length; i++) {
if (nullIndex != -1) {//出现过空 第一次出现后的位置 都不能有值
if (param[key]) {
//做一个特化
throw new BizError(ERRORENUM.地址数据不完整, `${key} 下标 ${nullIndex} 为空 `);
}
}
if (nullIndex == -1 && !param[key][i]) {
/**按顺序第一次赋值 */
nullIndex = i;
}
}
break;
}
errorStr = isError && errorStr == "" ? `${key}应该是${confType}型 而不是${valueType}`: errorStr;
if (isError) throw new BizError(ERRORENUM.参数错误, errorStr);
}
}
return param;
}
\ No newline at end of file
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2017",
"sourceMap": true,
"rootDir":"./src",
"outDir":"./out",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": false,
"noImplicitAny": false,
"strictNullChecks": false,
"types": ["node"]
},
"exclude": [
"node_modules",
"public"
]
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment