feat: 新增智电改造线路台账功能(ICC拉取同步)

- 数据库:新增迁移脚本20261011_icc_line_ledger.sql(electric_line/line_room/icc_sync_log三表、菜单61、role_menu(1,61)、定时任务icc_line_sync),同步db_seed.sql
- 后端:新增ElectricLine/IccSyncLog实体、查询DTO、线路台账服务与ICC拉取同步服务、/api/electric-lines控制器,接入RBAC(device:list)与定时任务
- 前端:新增智电改造线路台账页面及路由,API封装electricLineApi
- ICC同步采用本系统主动拉取模式,失败仅记日志不抛错,接口统一code/message/data格式兼容APP
This commit is contained in:
wk committed 2026-10-12 15:57:02 +08:00
1 parent f72644c24f
commit 41765cdf44
21 files changed
+1363 -6

No files matched your search

+51
View File
@@ -0,0 +1,51 @@
/**
* ICC 线路数据字段映射(集中配置,便于按 ICC 平台实际返回结构调整)
*
* ICC 返回的原始字段名 → 本系统 electric_line 字段名。当前为占位映射,
* 接入真实 ICC 平台时仅需调整本文件与 fetch 路径即可,业务层无需改动。
*/
export interface IccLineRawRecord {
[field: string]: any;
}
/** 原始记录关键字段名(ICC 平台约定,占位) */
export const ICC_LINE_FIELDS = {
lineCode: "lineCode", // ICC 线路编码(唯一键)
iccDeviceCode: "deviceCode", // ICC 智电设备编号
equipCode: "equipCode", // 关联的客户设备编码(用于反查 equip_id)
lineName: "lineName", // 线路名称
customerNo: "customerNo", // 关联客户编号(用于反查 customer_id)
buildingName: "buildingName", // 建筑名称(占位,反查 building_id)
floorNo: "floorNo", // 所在楼层
location: "location", // 所在位置
status: "status" // 1在线 0离线
} as const;
/** 渲染列表路径与 token 鉴权头(占位,接入真实平台时配置 .env)。 */
export const ICC_LINE_ENDPOINT = "/api/grid/lines";
/** 将 ICC 原始记录映射为本系统 electric_line 落库字段(不含联表外键)。 */
export function mapIccLineRecord(raw: IccLineRawRecord): {
lineCode: string;
iccDeviceCode: string;
equipCode?: string;
lineName?: string;
customerNo?: string;
buildingName?: string;
floorNo?: string;
location?: string;
status: number;
} {
const f = ICC_LINE_FIELDS;
return {
lineCode: String(raw[f.lineCode] ?? "").trim(),
iccDeviceCode: String(raw[f.iccDeviceCode] ?? "").trim(),
equipCode: raw[f.equipCode] ? String(raw[f.equipCode]).trim() : undefined,
lineName: raw[f.lineName] ? String(raw[f.lineName]).trim() : undefined,
customerNo: raw[f.customerNo] ? String(raw[f.customerNo]).trim() : undefined,
buildingName: raw[f.buildingName] ? String(raw[f.buildingName]).trim() : undefined,
floorNo: raw[f.floorNo] ? String(raw[f.floorNo]).trim() : undefined,
location: raw[f.location] ? String(raw[f.location]).trim() : undefined,
status: Number(raw[f.status]) === 0 ? 0 : 1
};
}
+17
View File
@@ -0,0 +1,17 @@
import type { BuiltinJob } from "./job_registry";
/**
* 智电线路同步任务:从 ICC 平台拉取智电改造线路并按 line_code 幂等 upsert,
* 写 icc_sync_log。默认每 10 分钟执行一次。
*/
export const iccLineSyncJob: BuiltinJob = {
jobKey: "icc_line_sync",
jobName: "智电线路同步",
description: "从 ICC 平台拉取智电改造线路并按 line_code 幂等 upsert(写 icc_sync_log)",
defaultCron: "*/10 * * * *",
defaultParams: {},
paramFields: [],
handler: async (_params, ctx) => {
return ctx.runtime.syncIccLines();
}
};
+4 -1
View File
@@ -1,6 +1,7 @@
import { demoHeartbeatJob } from "./demo_heartbeat.task";
import { logCleanJob } from "./log_clean.task";
import { noticeScanJob } from "./notice_scan.task";
import { iccLineSyncJob } from "./icc_line_sync.task";
export type JobParamType = "number" | "string";
@@ -29,6 +30,8 @@ export interface JobRuntime {
cleanJobLogs(keepDays: number): Promise<number>;
/** 执行全部启用的扫描式通知规则,返回本次执行摘要 */
runNoticeRules(): Promise<string>;
/** 从 ICC 平台拉取智电改造线路并按 line_code 幂等 upsert,返回执行摘要 */
syncIccLines(): Promise<string>;
}
export interface JobHandlerContext {
@@ -55,7 +58,7 @@ export interface BuiltinJob {
handler: JobHandler;
}
export const BUILTIN_JOBS: BuiltinJob[] = [demoHeartbeatJob, logCleanJob, noticeScanJob];
export const BUILTIN_JOBS: BuiltinJob[] = [demoHeartbeatJob, logCleanJob, noticeScanJob, iccLineSyncJob];
export function getBuiltinJob(jobKey: string): BuiltinJob | undefined {
return BUILTIN_JOBS.find(job => job.jobKey === jobKey);