日程管理接口

parent 18e87f61
......@@ -96,7 +96,11 @@ export async function processDeviceData(message) {
["name"]
);
// 调用空调控制(非文创区域才允许自动控温)
if (regionRow?.data && !regionRow.data.name?.includes("文创")) {
if (regionRow?.data && !regionRow.data.name?.includes("文创")
&& !regionRow.data.name?.includes("原料厅")
&& !regionRow.data.name?.includes("社会责任厅")
&& !regionRow.data.name?.includes("新包装")
) {
await controlAcByEnvironment(devEUI, data);
}
}
......
......@@ -70,3 +70,76 @@ export async function getRegionAllList( regionGroup: string ) {
list: regionMap
};
}
/**
* 获取区域设备树
* @returns 区域树
*/
export async function getRegionDevices() {
// 查询所有区域
let where: any = { "%orderAsc%": "sort_order" };
let regionList = await selectDataListByParam(
TABLENAME.区域表,
where,
["id", "room_id", "name", "type", "groups"],
);
const regions: any[] = regionList.data || [];
if (regions.length === 0) return [];
// 通过区域查询所有空调设备
const regionIds = regions.map((r: any) => r.id);
const deviceList = await selectDataListByParam(
TABLENAME.设备表,
{ region_key: { "%in%": regionIds }, device_type: "空调" },
["device_id", "device_name", "region_key", "control_params"],
);
const devices: any[] = deviceList.data || [];
// 将设备列表转成map形式:{regionKey:[{设备1},{设备2}]}
const deviceMap: { [regionKey: number]: any[] } = {};
for (const d of devices) {
const rk = d.region_key;
if (!deviceMap[rk]) deviceMap[rk] = [];
deviceMap[rk].push({
deviceId: d.device_id,
deviceName: d.device_name || d.device_id,
controlParams: d.control_params || null,
});
}
// 循环区域,将区域转成树结构,并通过设备map将设备加入区域
const groupMap: { [group: string]: { key: string; value: string; typeMap: { [type: string]: { key: string; value: string; sub: any[] } } } } = {};
for (const r of regions) {
const groupName = r.groups;
const typeName = r.type;
if (!groupMap[groupName]) {
groupMap[groupName] = { key: groupName, value: groupName, typeMap: {} };
}
if (!groupMap[groupName].typeMap[typeName]) {
groupMap[groupName].typeMap[typeName] = { key: typeName, value: typeName, sub: [] };
}
groupMap[groupName].typeMap[typeName].sub.push({
regionKey: r.id,
roomId: r.room_id,
regionName: r.name,
devices: deviceMap[r.id] || [],
});
}
// 返回区域树
const result: any[] = [];
const sortedGroups = Object.keys(groupMap).sort();
for (const groupName of sortedGroups) {
const group = groupMap[groupName];
const typeArr: any[] = [];
const sortedTypes = Object.keys(group.typeMap).sort();
for (const typeName of sortedTypes) {
typeArr.push(group.typeMap[typeName]);
}
result.push({ key: group.key, value: group.value, sub: typeArr });
}
return result;
}
\ No newline at end of file
/**
* 周报告模块
* - 查询上周各馆(A/B/C馆)的设备监测数据
* - 查询最近七天各馆(A/B/C馆)的设备监测数据
* - 提供周报告 JSON 数据接口
*/
......@@ -47,20 +47,18 @@ function formatDateStr(date: Date): string {
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
}
/** 计算指定周的起止时间(周一~周日) */
/** 计算最近七天的起止时间(今天往前推6天) */
function calcWeekRange(weekOffset: number = 0): { start: Date; end: Date; timer: string } {
const now = new Date();
const dayOfWeek = now.getDay();
const daysToMonday = dayOfWeek === 0 ? 8 : dayOfWeek + 6; // 回溯到上周一
const monday = new Date(now);
monday.setDate(now.getDate() - daysToMonday + weekOffset * 7);
monday.setHours(0, 0, 0, 0);
const sunday = new Date(monday);
sunday.setDate(monday.getDate() + 6);
sunday.setHours(23, 59, 59, 999);
const timer = `${formatChineseDate(monday)}${formatChineseDate(sunday)}`;
return { start: monday, end: sunday, timer };
const end = new Date(now);
end.setDate(now.getDate() + weekOffset * 7);
end.setHours(23, 59, 59, 999);
const start = new Date(now);
start.setDate(now.getDate() - 6 + weekOffset * 7);
start.setHours(0, 0, 0, 0);
const timer = `${formatChineseDate(start)}${formatChineseDate(end)}`;
return { start, end, timer };
}
/** 格式化数字,不足时返回 "——" */
......@@ -217,8 +215,8 @@ function calcMeterDeviceDaily(records: any[]): number {
}
/**
* 查询周报告 JSON 数据
* @param weekOffset 0=上周(默认), -1=前一周, ...
* 查询周报告 JSON 数据(最近七天)
* @param weekOffset 0=最近七天(默认), -1=再往前七天, ...
*/
export async function getWeeklyReportData(weekOffset: number = 0): Promise<any> {
const { start: monday, end: sunday, timer } = calcWeekRange(weekOffset);
......@@ -374,3 +372,189 @@ export async function getWeeklyReportData(weekOffset: number = 0): Promise<any>
return { timer, dataTotal, runDatas };
}
// ======================= 九合一环境监测指标 7 天均值 =======================
/** 九合一核心指标(去掉 pir 人体感应 和 o3 臭氧) */
const ENV_INDICATOR_KEYS = [
{ key: "co2", name: "二氧化碳", unit: "ppm" },
{ key: "pm10", name: "PM10", unit: "μg/m³" },
{ key: "pm2_5", name: "PM2.5", unit: "μg/m³" },
{ key: "hcho", name: "甲醛", unit: "mg/m³" },
{ key: "tvoc", name: "TVOC", unit: "mg/m³" },
{ key: "temperature", name: "温度", unit: "℃" },
{ key: "humidity", name: "湿度", unit: "%" },
{ key: "pressure", name: "气压", unit: "hPa" },
{ key: "light_level", name: "光照度", unit: "lux" },
];
/**
* 查询九合一环境监测指标的 7 天均值(A馆 / B馆 / C馆)
* 对每个馆的环境监测设备,取近 7 天所有上报数据的算术平均
*/
export async function getEnvIndicatorsWeeklyAvg(): Promise<any> {
// 1. 计算七天时间范围
const now = new Date();
const end = new Date(now);
end.setHours(23, 59, 59, 999);
const start = new Date(now);
start.setDate(now.getDate() - 6);
start.setHours(0, 0, 0, 0);
const from = formatLocalTime(start);
const to = formatLocalTime(end);
const timer = `${formatChineseDate(start)}${formatChineseDate(end)}`;
console.log(`[九合一七日均值] 查询范围: ${timer}`);
// 2. 并行查询三馆区域
const hallRegionMap = await queryHallRegions();
// 3. 并行查询三馆的环境监测设备
const hallDeviceGroups = await Promise.all(
HALL_NAMES.map(h => queryHallDeviceGroup(hallRegionMap.get(h) || []))
);
// 4. 并行查询三馆近 7 天的设备数据
const hallResults = await Promise.all(
hallDeviceGroups.map(async (group) => {
const envDeviceIds = group.envDevices.map(d => d.device_id);
if (envDeviceIds.length === 0) return null;
const dayDataRes = await selectDataListByParam(TABLENAME.设备数据表, {
device_id: { "%in%": envDeviceIds },
device_time: { "%gte%": from, "%lte%": to },
}, ["device_id", "data"]);
return dayDataRes.data || [];
})
);
// 5. 计算每个馆的各指标均值
const result: any = { timer };
for (let idx = 0; idx < HALL_NAMES.length; idx++) {
const hallName = HALL_NAMES[idx];
const records = hallResults[idx];
const indicators: any = {};
if (!records || records.length === 0) {
// 无数据填 null
for (const ik of ENV_INDICATOR_KEYS) {
indicators[ik.key] = { name: ik.name, value: null, unit: ik.unit };
}
} else {
// 统计各指标累加值和计数
const sums: { [key: string]: number } = {};
const counts: { [key: string]: number } = {};
for (const ik of ENV_INDICATOR_KEYS) {
sums[ik.key] = 0;
counts[ik.key] = 0;
}
for (const rec of records) {
const d = rec.data || {};
for (const ik of ENV_INDICATOR_KEYS) {
const v = d[ik.key];
if (v !== undefined && v !== null && typeof v === "number") {
sums[ik.key] += v;
counts[ik.key]++;
}
}
}
for (const ik of ENV_INDICATOR_KEYS) {
const avg = counts[ik.key] > 0
? Math.round((sums[ik.key] / counts[ik.key]) * 100) / 100
: null;
indicators[ik.key] = { name: ik.name, value: avg, unit: ik.unit };
}
}
result[hallName] = { deviceCount: hallDeviceGroups[idx].envDevices.length, recordCount: records?.length || 0, indicators };
}
console.log(`[九合一七日均值] 完成`);
return result;
}
// ======================= 近 7 天故障信息(按 fault_code 分组) =======================
const STATUS_MAP: { [key: number]: string } = { 0: "未处理", 1: "处理中", 2: "已解决" };
const LEVEL_MAP: { [key: number]: string } = { 1: "一级", 2: "二级", 3: "三级" };
/**
* 查询近 7 天故障信息,按 fault_code 分组
* 每组展示:故障代码、风险内容、报警时间、处理状态、报警设备
*/
export async function getFaultInfoLast7Days(): Promise<any> {
const now = new Date();
const end = new Date(now);
end.setHours(23, 59, 59, 999);
const start = new Date(now);
start.setDate(now.getDate() - 6);
start.setHours(0, 0, 0, 0);
const from = formatLocalTime(start);
const to = formatLocalTime(end);
const timer = `${formatChineseDate(start)}${formatChineseDate(end)}`;
console.log(`[近7天故障] 查询范围: ${timer}`);
// 1. 查询近 7 天故障记录(按发生时间倒序)
const faultRes = await selectDataListByParam(TABLENAME.设备故障表, {
occurred_time: { "%gte%": from, "%lte%": to },
"%orderDesc%": "occurred_time",
});
const faultRecords = faultRes.data || [];
if (faultRecords.length === 0) {
return { timer, totalCount: 0, groups: [] };
}
// 2. 收集所有 device_id,查询设备名称
const deviceIds = [...new Set(faultRecords.map((f: any) => f.device_id).filter(Boolean))];
let deviceNameMap: Map<string, string> = new Map();
if (deviceIds.length > 0) {
const devRes = await selectDataListByParam(TABLENAME.设备表, {
device_id: { "%in%": deviceIds },
}, ["device_id", "device_name"]);
for (const d of (devRes.data || [])) {
deviceNameMap.set(d.device_id, d.device_name || d.device_id);
}
}
// 3. 按 fault_code 分组
const groupMap: { [code: string]: any[] } = {};
for (const f of faultRecords) {
const code = f.fault_code || "未分类";
if (!groupMap[code]) groupMap[code] = [];
groupMap[code].push({
id: f.id,
faultType: f.fault_type || "",
faultCode: code,
faultDesc: f.fault_description || "",
alarmTime: f.occurred_time
? (typeof f.occurred_time === "string" ? f.occurred_time : formatLocalTime(new Date(f.occurred_time)))
: "",
status: f.status ?? 0,
statusText: STATUS_MAP[f.status] ?? "未知",
level: f.level ?? 0,
levelText: LEVEL_MAP[f.level] ?? "未知",
deviceId: f.device_id,
deviceName: deviceNameMap.get(f.device_id) || f.device_id || "",
});
}
// 4. 构建分组结果
const groups = Object.entries(groupMap)
.sort((a, b) => b[1].length - a[1].length) // 按故障数量降序
.map(([code, items]) => ({
faultCode: code,
count: items.length,
items,
}));
const totalCount = faultRecords.length;
console.log(`[近7天故障] 共 ${totalCount} 条,${groupMap.size} 种故障码`);
return { timer, totalCount, groupCount: Object.keys(groupMap).length, groups };
}
......@@ -606,11 +606,13 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
for (let devId of acDeviceIds) {
let rec = await selectDataListByParam(TABLENAME.设备数据表,
{ device_id: devId, "%orderDesc%": "device_time", "%limit%": 1 },
["device_time"]);
["device_time", "data"]);
if (rec.data.length) {
let lastTime = new Date(rec.data[0].device_time);
let lastData = rec.data[0].data;
let power = lastData.power ?? '';
let diffMinutes = (nowTime.getTime() - lastTime.getTime()) / 60000;
deviceLatestMap.set(devId, { lastTime, isOnline: diffMinutes <= 30 });
deviceLatestMap.set(devId, { lastTime, isOnline: power==='on' });
} else {
deviceLatestMap.set(devId, { lastTime: null, isOnline: false });
}
......@@ -710,11 +712,13 @@ export async function getRunAnalysis(regionGroup: string, regionType: string) {
for (let devId of deviceIds) {
let rec = await selectDataListByParam(TABLENAME.设备数据表,
{ device_id: devId, "%orderDesc%": "device_time", "%limit%": 1 },
["device_time"]);
["device_time", "data"]);
if (rec.data.length) {
let lastTime = new Date(rec.data[0].device_time);
let lastData = rec.data[0].data;
let power = lastData.power ?? '';
let diffMinutes = (nowTime.getTime() - lastTime.getTime()) / 60000;
deviceLatestMap.set(devId, { lastTime, isOnline: diffMinutes <= 30 });
deviceLatestMap.set(devId, { lastTime, isOnline: power === 'on' });
} else {
deviceLatestMap.set(devId, { lastTime: null, isOnline: false });
}
......@@ -1135,8 +1139,21 @@ async function controlAcRunningApi(acDevices?: any, workMode?: number, tempSet?:
return controlResults;
}
/**
* 设置开关机
* @param regionGroup
* @param regionFloor
* @param regionKey
* @param deviceId
* @param powerStatus
*/
export async function scheduledPowerOn(regionGroup:string, regionType:string, regionKey:number, deviceId:string, powerStatus:number) {
// 根据regionGroup\regionType\regionKey查询区域信息
// 根据deviceId查询空调设备
// 无传参时,查询region表,通过region表查询device表中device_type=空调的数据
// 循环查询的空调设备,组装空调控制需要参数
// 调用空调控制的第三方接口并记录device_log日志表
}
/**
* 数据集成 - 启动定时任务
......
/**
* 日程管理 业务逻辑
*/
import { selectDataListByParam, selectDataListToPageByParam, selectDataCountByParam } from "../data/findData";
import { addData } from "../data/addData";
import { delData } from "../data/delData";
import { updateManyData } from "../data/updateData";
import { TABLENAME } from "../config/dbEnum";
// ======================= 公共工具 =======================
/** 格式化为本地时间字符串 YYYY-MM-DD HH:mm:ss */
function formatLocalTime(d: Date): string {
const pad = (n: number) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
// ======================= 日程列表 =======================
/**
* 查询日程列表(分页 + 按有效期/区域过滤)
*/
export async function getScheduleList(params: {
pageNumber?: number;
pageSize?: number;
startDate?: string;
endDate?: string;
regionKey?: number;
}): Promise<any> {
const pageNumber = params.pageNumber || 1;
const pageSize = params.pageSize || 10;
const { startDate, endDate, regionKey } = params;
console.log(`[日程列表] page=${pageNumber}, size=${pageSize}, startDate=${startDate}, endDate=${endDate}, regionKey=${regionKey}`);
// 构建日程表查询条件
const scheduleWhere: any = { "%orderDesc%": "id" };
// 有效期过滤:日程有效时间与查询区间有交集
if (startDate) {
scheduleWhere.begin_validity = { "%lte%": startDate };
}
if (endDate) {
scheduleWhere.end_validity = { "%gte%": endDate };
}
// 如果按区域过滤,先查出该区域下的 schedule_id 集合
let filteredScheduleIds: number[] | null = null;
if (regionKey) {
const sdRes = await selectDataListByParam(TABLENAME.日程设备关联表, {
region_key: regionKey,
}, ["schedule_id"]);
filteredScheduleIds = (sdRes.data || []).map((r: any) => r.schedule_id);
if (filteredScheduleIds.length === 0) {
return { total: 0, list: [] };
}
scheduleWhere.id = { "%in%": filteredScheduleIds };
}
// 分页查询
const columns = ["id", "title", "ac_controls", "schedule_time", "begin_validity", "end_validity", "created_at"];
const listRes = await selectDataListToPageByParam(
TABLENAME.日程表, scheduleWhere, columns, pageNumber, pageSize
);
const rows: any[] = listRes.data || [];
// 总数
const countRes = await selectDataCountByParam(TABLENAME.日程表, scheduleWhere);
const total = countRes.data || 0;
// 收集所有日程 id
const scheduleIds = rows.map(r => r.id);
// 批量查关联的设备关联记录
let deviceMap: { [sid: number]: any[] } = {};
if (scheduleIds.length > 0) {
const sdRes = await selectDataListByParam(TABLENAME.日程设备关联表, {
schedule_id: { "%in%": scheduleIds },
}, ["schedule_id", "device_id", "region_key"]);
const sdRows = sdRes.data || [];
for (const sd of sdRows) {
if (!deviceMap[sd.schedule_id]) deviceMap[sd.schedule_id] = [];
deviceMap[sd.schedule_id].push(sd);
}
}
// 批量查区域名称(需要去重 region_key)
const regionKeySet = new Set<number>();
for (const sid of scheduleIds) {
for (const sd of (deviceMap[sid] || [])) {
regionKeySet.add(sd.region_key);
}
}
let regionNameMap: { [rk: number]: string } = {};
if (regionKeySet.size > 0) {
const regionRes = await selectDataListByParam(TABLENAME.区域表, {
id: { "%in%": [...regionKeySet] },
}, ["id", "name"]);
for (const r of (regionRes.data || [])) {
regionNameMap[r.id] = r.name;
}
}
// 组装返回列表
const list = rows.map(row => {
const devices = deviceMap[row.id] || [];
const regionNames = [...new Set(devices.map((d: any) => regionNameMap[d.region_key] || ""))].filter(Boolean);
let validity = "";
if (row.begin_validity || row.end_validity) {
const from = row.begin_validity
? (typeof row.begin_validity === "string" ? row.begin_validity.split(" ")[0] : formatLocalTime(new Date(row.begin_validity)).split(" ")[0])
: "";
const to = row.end_validity
? (typeof row.end_validity === "string" ? row.end_validity.split(" ")[0] : formatLocalTime(new Date(row.end_validity)).split(" ")[0])
: "";
validity = `${from}${to}`;
}
return {
id: row.id,
title: row.title,
acControls: row.ac_controls,
scheduleTime: row.schedule_time || "",
validity,
deviceCount: devices.length,
regionNames,
createdAt: row.created_at
? (typeof row.created_at === "string" ? row.created_at : formatLocalTime(new Date(row.created_at)))
: "",
};
});
console.log(`[日程列表] total=${total}, rows=${list.length}`);
return { total, list };
}
// ======================= 新增日程 =======================
/**
* 新增日程(含区域设备关联)
*/
export async function addSchedule(params: {
title: string;
schedule_time: string;
ac_controls: string;
begin_validity?: string;
end_validity?: string;
regionKeys: number[];
}): Promise<any> {
const { title, schedule_time, ac_controls, begin_validity, end_validity, regionKeys } = params;
console.log(`[新增日程] title=${title}, time=${schedule_time}, action=${ac_controls}, regions=${regionKeys}`);
// 1. 插入 schedule 主表
const scheduleRes = await addData(TABLENAME.日程表, {
title,
schedule_time,
ac_controls,
begin_validity: begin_validity || null,
end_validity: end_validity || null,
});
// 2. 通过 addData 无法直接获取自增 id,需要反向查询
const inserted = await selectDataListByParam(TABLENAME.日程表, {
title,
schedule_time,
ac_controls,
"%orderDesc%": "id",
"%limit%": 1,
}, ["id"]);
const scheduleId = (inserted.data && inserted.data[0]) ? inserted.data[0].id : null;
if (!scheduleId) {
console.warn("[新增日程] 无法获取插入的 schedule id");
return { isSuccess: false, msg: "日程创建失败" };
}
// 3. 查这些区域下的所有空调设备
const deviceRes = await selectDataListByParam(TABLENAME.设备表, {
region_key: { "%in%": regionKeys },
device_type: "空调",
}, ["device_id", "region_key"]);
const devices = deviceRes.data || [];
// 4. 批量插入 schedule_device 关联记录
if (devices.length > 0) {
const sdRecords = devices.map((d: any) => ({
schedule_id: scheduleId,
device_id: d.device_id,
region_key: d.region_key,
}));
await addData(TABLENAME.日程设备关联表, sdRecords);
console.log(`[新增日程] 关联设备 ${sdRecords.length} 条`);
} else {
// 无设备时也插入一条区域占位记录(允许区域下暂时无设备)
for (const rk of regionKeys) {
await addData(TABLENAME.日程设备关联表, {
schedule_id: scheduleId,
device_id: "",
region_key: rk,
});
}
console.log(`[新增日程] 区域 ${regionKeys.length} 个,暂无设备`);
}
return { isSuccess: true, scheduleId };
}
// ======================= 日程详情 =======================
/**
* 获取日程详情(含关联区域/设备)
*/
export async function getScheduleDetail(scheduleId: number): Promise<any> {
console.log(`[日程详情] scheduleId=${scheduleId}`);
// 1. 查日程主表
const scheduleRes = await selectDataListByParam(TABLENAME.日程表, {
id: scheduleId,
});
const rows = scheduleRes.data || [];
if (rows.length === 0) {
return null;
}
const row = rows[0];
// 2. 查关联设备
const sdRes = await selectDataListByParam(TABLENAME.日程设备关联表, {
schedule_id: scheduleId,
}, ["device_id", "region_key"]);
const sdRows = sdRes.data || [];
const selectedRegions = [...new Set(sdRows.map((r: any) => r.region_key))];
const selectedDevices = sdRows.filter((r: any) => r.device_id).map((r: any) => r.device_id);
return {
id: row.id,
title: row.title,
scheduleTime: row.schedule_time || "",
acControls: row.ac_controls,
beginValidity: row.begin_validity
? (typeof row.begin_validity === "string" ? row.begin_validity.split(" ")[0] : formatLocalTime(new Date(row.begin_validity)).split(" ")[0])
: "",
endValidity: row.end_validity
? (typeof row.end_validity === "string" ? row.end_validity.split(" ")[0] : formatLocalTime(new Date(row.end_validity)).split(" ")[0])
: "",
selectedRegions,
selectedDevices,
};
}
// ======================= 编辑日程 =======================
/**
* 编辑日程(更新主表 + 重建关联)
*/
export async function editSchedule(params: {
scheduleId: number;
title: string;
schedule_time: string;
ac_controls: string;
begin_validity?: string;
end_validity?: string;
regionKeys: number[];
}): Promise<any> {
const { scheduleId, title, schedule_time, ac_controls, begin_validity, end_validity, regionKeys } = params;
console.log(`[编辑日程] id=${scheduleId}, title=${title}`);
// 1. 更新 schedule 主表
await updateManyData(TABLENAME.日程表, { id: scheduleId }, {
title,
schedule_time,
ac_controls,
begin_validity: begin_validity || null,
end_validity: end_validity || null,
});
// 2. 删除旧的关联记录
await delData(TABLENAME.日程设备关联表, { schedule_id: scheduleId });
// 3. 查区域下的空调设备
const deviceRes = await selectDataListByParam(TABLENAME.设备表, {
region_key: { "%in%": regionKeys },
device_type: "空调",
}, ["device_id", "region_key"]);
const devices = deviceRes.data || [];
// 4. 重建关联
if (devices.length > 0) {
const sdRecords = devices.map((d: any) => ({
schedule_id: scheduleId,
device_id: d.device_id,
region_key: d.region_key,
}));
await addData(TABLENAME.日程设备关联表, sdRecords);
console.log(`[编辑日程] 重建关联设备 ${sdRecords.length} 条`);
} else {
for (const rk of regionKeys) {
await addData(TABLENAME.日程设备关联表, {
schedule_id: scheduleId,
device_id: "",
region_key: rk,
});
}
console.log(`[编辑日程] 区域 ${regionKeys.length} 个,暂无设备`);
}
return { isSuccess: true };
}
// ======================= 删除日程 =======================
/**
* 删除日程(含关联记录)
*/
export async function deleteSchedule(scheduleId: number): Promise<any> {
console.log(`[删除日程] id=${scheduleId}`);
// 1. 删除关联记录
await delData(TABLENAME.日程设备关联表, { schedule_id: scheduleId });
// 2. 删除主记录
await delData(TABLENAME.日程表, { id: scheduleId });
return { isSuccess: true };
}
......@@ -8,7 +8,9 @@ export enum TABLENAME {
区域表 = 'region',
设备故障表 = 'device_fault',
用户信息表 = 'user_info',
设备日志表 = 'device_logs'
设备日志表 = 'device_logs',
日程表 = 'schedule',
日程设备关联表 = 'schedule_device'
};
/**
......
......@@ -337,5 +337,118 @@ export const TablesConfig = [
association: [
]
},
// 日程表
{
tableNameCn: '日程表',
tableName: 'schedule',
schema: {
id: {
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true,
autoIncrement: true,
comment: '自增主键'
},
title: {
type: Sequelize.STRING(255),
allowNull: false,
comment: '日程标题'
},
schedule_date: {
type: DataTypes.DATE,
allowNull: false,
comment: '日期'
},
ac_controls: {
type: Sequelize.STRING(50),
allowNull: false,
comment: '操作状态:off关on开'
},
todo_item: {
type: DataTypes.JSON,
allowNull: true,
comment: '待办事项,除了开关以外可以设定模式、温度等。例:{"温度":"24.5","模式":"制冷","风速":"低风"}'
},
schedule_time: {
type: Sequelize.STRING(50),
allowNull: true,
comment: '定时时间,如08:30:00'
},
set_conditions: {
type: DataTypes.JSON,
allowNull: true,
comment: '设定条件,除了定时以外可以通过条件开启或关闭设备。例:{"温度":"≤26&≥22"}'
},
begin_validity: {
type: DataTypes.DATE,
allowNull: true,
comment: '有效时间起,不设置则为空'
},
end_validity: {
type: DataTypes.DATE,
allowNull: true,
comment: '有效时间止,不设置则为空'
},
outward_time: {
type: Sequelize.STRING(200),
allowNull: true,
comment: '有效除外时间,多个通过逗号分隔'
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: Sequelize.NOW,
comment: '创建时间'
},
updated_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: Sequelize.NOW,
comment: '修改时间'
}
},
association: [
{ type: "hasMany", target: "schedule_device", foreignKey: "schedule_id" }
]
},
// 日程设备关联表
{
tableNameCn: '日程设备关联表',
tableName: 'schedule_device',
schema: {
id: {
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true,
autoIncrement: true,
comment: '自增主键'
},
schedule_id: {
type: DataTypes.BIGINT,
allowNull: false,
comment: '日程ID,关联schedule.id'
},
device_id: {
type: DataTypes.STRING(50),
allowNull: false,
comment: '设备ID,关联device.device_id'
},
region_key: {
type: DataTypes.BIGINT,
allowNull: false,
comment: '区域key,关联region.id,表示设备所属区域'
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '创建时间'
}
},
association: [
{ type: "hasMany", target: "device", foreignKey: "device_id" },
{ type: "hasMany", target: "region", foreignKey: "region_key" }
]
},
];
/**
* 后端管理系统 路由
*/
import asyncHandler from "express-async-handler";
import * as reportBiz from "../biz/report";
import * as regionBiz from "../biz/region";
import * as scheduleBiz from "../biz/schedule";
export function setRouter(httpServer) {
/** 获取周报告JSON数据 */
httpServer.get("/api/qdm/report/weekly/data", asyncHandler(getReportData));
/** 获取九合一环境监测指标 7 天均值(A/B/C馆) */
httpServer.get("/api/qdm/report/env/weekly/avg", asyncHandler(getEnvWeeklyAvg));
/** 获取近 7 天故障信息(按 fault_code 分组) */
httpServer.get("/api/qdm/report/fault/weekly", asyncHandler(getFaultInfo));
/** 区域设备树 */
httpServer.get("/api/qdm/admin/region/device", asyncHandler(getRegionDevices));
/** 日程管理列表 */
httpServer.get("/api/qdm/admin/schedule/list", asyncHandler(getScheduleList));
/** 日程管理详情 */
httpServer.get("/api/qdm/admin/schedule/detail", asyncHandler(getScheduleDetail));
/** 日程管理新增 */
httpServer.post("/api/qdm/admin/schedule/add", asyncHandler(addSchedule));
/** 日程管理编辑 */
httpServer.post("/api/qdm/admin/schedule/edit", asyncHandler(editSchedule));
/** 日程管理删除 */
httpServer.post("/api/qdm/admin/schedule/delete", asyncHandler(deleteSchedule));
}
/**
* GET /api/qdm/report/weekly/data?weekOffset=0
* 返回周报告 JSON 数据供前端页面渲染
*/
async function getReportData(req, res) {
const weekOffset = Number(req.query?.weekOffset) || 0;
const data = await reportBiz.getWeeklyReportData(weekOffset);
res.success(data);
}
/**
* GET /api/qdm/report/env/weekly/avg
* 返回九合一环境监测指标 7 天均值(A馆 / B馆 / C馆)
*/
async function getEnvWeeklyAvg(req, res) {
const data = await reportBiz.getEnvIndicatorsWeeklyAvg();
res.success(data);
}
/**
* GET /api/qdm/report/fault/weekly
* 返回近 7 天故障信息,按 fault_code 分组
*/
async function getFaultInfo(req, res) {
const data = await reportBiz.getFaultInfoLast7Days();
res.success(data);
}
// ======================= 区域设备 =======================
/**
* GET /api/qdm/admin/region/device
* 获取区域设备树(馆 → 层 → 区域 → 设备)
*/
async function getRegionDevices(req, res) {
const data = await regionBiz.getRegionDevices();
res.success(data);
}
// ======================= 日程管理 =======================
/**
* GET /api/qdm/admin/schedule/list
* 日程列表(分页 + 有效期 + 区域过滤)
* query: pageNumber, pageSize, startDate, endDate, regionKey
*/
async function getScheduleList(req, res) {
const data = await scheduleBiz.getScheduleList({
pageNumber: Number(req.query?.pageNumber) || 1,
pageSize: Number(req.query?.pageSize) || 10,
startDate: req.query?.startDate,
endDate: req.query?.endDate,
regionKey: req.query?.regionKey ? Number(req.query.regionKey) : undefined,
});
res.success(data);
}
/**
* GET /api/qdm/admin/schedule/detail
* 日程详情(含关联区域/设备)
* query: scheduleId
*/
async function getScheduleDetail(req, res) {
const scheduleId = Number(req.query?.scheduleId);
if (!scheduleId) {
res.fail("缺少参数 scheduleId");
return;
}
const data = await scheduleBiz.getScheduleDetail(scheduleId);
if (!data) {
res.fail("日程不存在");
return;
}
res.success(data);
}
/**
* POST /api/qdm/admin/schedule/add
* 新增日程
* body: title, scheduleTime, acControls, beginValidity, endValidity, regionKeys
*/
async function addSchedule(req, res) {
const { title, scheduleTime, acControls, beginValidity, endValidity, regionKeys } = req.body || {};
if (!title || !scheduleTime || !acControls) {
res.fail("缺少必填参数:title, scheduleTime, acControls");
return;
}
if (!regionKeys || !Array.isArray(regionKeys) || regionKeys.length === 0) {
res.fail("至少选择一个区域");
return;
}
const data = await scheduleBiz.addSchedule({
title,
schedule_time: scheduleTime,
ac_controls: acControls,
begin_validity: beginValidity || undefined,
end_validity: endValidity || undefined,
regionKeys,
});
res.success(data);
}
/**
* POST /api/qdm/admin/schedule/edit
* 编辑日程
* body: scheduleId, title, scheduleTime, acControls, beginValidity, endValidity, regionKeys
*/
async function editSchedule(req, res) {
const { scheduleId, title, scheduleTime, acControls, beginValidity, endValidity, regionKeys } = req.body || {};
if (!scheduleId || !title || !scheduleTime || !acControls) {
res.fail("缺少必填参数:scheduleId, title, scheduleTime, acControls");
return;
}
if (!regionKeys || !Array.isArray(regionKeys) || regionKeys.length === 0) {
res.fail("至少选择一个区域");
return;
}
const data = await scheduleBiz.editSchedule({
scheduleId: Number(scheduleId),
title,
schedule_time: scheduleTime,
ac_controls: acControls,
begin_validity: beginValidity || undefined,
end_validity: endValidity || undefined,
regionKeys,
});
res.success(data);
}
/**
* POST /api/qdm/admin/schedule/delete
* 删除日程
* body: scheduleId
*/
async function deleteSchedule(req, res) {
const { scheduleId } = req.body || {};
if (!scheduleId) {
res.fail("缺少参数 scheduleId");
return;
}
const data = await scheduleBiz.deleteSchedule(Number(scheduleId));
res.success(data);
}
/**
* 周报告 API 路由
*/
import asyncHandler from "express-async-handler";
import * as reportBiz from "../biz/report";
export function setRouter(httpServer) {
/** 获取周报告JSON数据 */
httpServer.get("/api/qdm/report/weekly/data", asyncHandler(getReportData));
}
/**
* GET /api/qdm/report/weekly/data?weekOffset=0
* 返回周报告 JSON 数据供前端页面渲染
*/
async function getReportData(req, res) {
const weekOffset = Number(req.query?.weekOffset) || 0;
const data = await reportBiz.getWeeklyReportData(weekOffset);
res.success(data);
}
......@@ -5,13 +5,13 @@
import * as usersRouter from './users';
import * as deviceRouter from './device';
import * as feiyiClientRouter from './feiyiClient';
import * as reportRouter from './report';
import * as adminRouter from './admin';
export function setRouter(httpServer) {
usersRouter.setRouter(httpServer);
deviceRouter.setRouter(httpServer);
feiyiClientRouter.setRouter(httpServer);
reportRouter.setRouter(httpServer);
adminRouter.setRouter(httpServer);
}
......
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