feat(line): 智电改造线路台账接入 ICC 消防探测器真实同步
- 重建 electric_line 表结构(迁移20261024_icc_line_rebuild.sql并同步db_init/db_seed):删building_id/floor_no/location/icc_device_code占位字段,equip_id改可空,新增imei/gateway_id/type_code/type_name/fire_system_code/fire_system_name/part_unit_loop_code/channel_no/channel_sn/produce_date,gateway_id外键→electric_gateway - 同步服务IccLineSyncService复用OAuth2真实鉴权分页拉取探测器(POST /detector/subsystem/page,pageSize=50短页停止),按line_code(detectorCode)幂等upsert写icc_sync_log;customer_id由orgCode→customer.icc_code解析、gateway_id由parentCode→electric_gateway.device_code匹配、equip_id同步不填充 - 列表接口返回gateway/type/imei等新字段,keyword检索含imei;前端台账页更新为新列(IMEI/所属网关/探测器类型/回路通道/消防系统/生产日期),保留手动同步按钮与筛选 - 更新系统设计文档electric_line表定义与spec(add-icc-line-sync)
This commit is contained in:
1 parent
cf0a0d4d31
commit
160ad85051
13 files changed
+510
-200
No files matched your search
@@ -1,51 +1,109 @@
|
||||
/**
|
||||
* ICC 线路数据字段映射(集中配置,便于按 ICC 平台实际返回结构调整)
|
||||
* ICC 智电改造线路(消防探测器)数据字段映射(集中配置,便于按 ICC 平台实际返回结构调整)。
|
||||
*
|
||||
* ICC 返回的原始字段名 → 本系统 electric_line 字段名。当前为占位映射,
|
||||
* 接入真实 ICC 平台时仅需调整本文件与 fetch 路径即可,业务层无需改动。
|
||||
* ICC 返回的原始字段名 → 本系统 electric_line 字段。接入真实 ICC 平台时
|
||||
* 仅需调整本文件与 fetch 路径即可,业务层无需改动。纯映射逻辑,无 SQL 无 DOM。
|
||||
*/
|
||||
export interface IccLineRawRecord {
|
||||
[field: string]: any;
|
||||
}
|
||||
|
||||
/** 原始记录关键字段名(ICC 平台约定,占位) */
|
||||
/** 探测器分页列表接口路径({version} 由调用方按 ICC_VERSION 替换) */
|
||||
export const ICC_LINE_ENDPOINT = "/evo-apigw/evo-fdbu/{version}/detector/subsystem/page";
|
||||
|
||||
/** 原始记录关键字段映射:本系统字段 → 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离线
|
||||
lineCode: "detectorCode", // 探测器编码(唯一幂等键)
|
||||
lineName: "detectorName", // 探测器名称
|
||||
imei: "imei", // 探测器 IMEI
|
||||
orgCode: "orgCode", // 所属组织编码(→ customer.icc_code 反查客户,不落库)
|
||||
parentCode: "parentCode", // 上级网关编码(→ electric_gateway.device_code 反查网关,不落库)
|
||||
typeCode: "typeCode", // 探测器类型编码
|
||||
typeName: "typeName", // 探测器类型名称
|
||||
fireSystemCode: "fireSystemCode", // 消防系统编码
|
||||
fireSystemName: "fireSystemName", // 消防系统名称
|
||||
partUnitLoopCode: "partUnitLoopCode", // 部件/单元/回路编码
|
||||
channelNo: "channelNo", // 通道号
|
||||
channelSn: "channelSn", // 通道序列号
|
||||
produceDate: "produceDate", // 生产日期
|
||||
detectorStatus: "detectorStatus" // 上报状态 01=在线 02=离线
|
||||
} as const;
|
||||
|
||||
/** 渲染列表路径与 token 鉴权头(占位,接入真实平台时配置 .env)。 */
|
||||
export const ICC_LINE_ENDPOINT = "/api/grid/lines";
|
||||
/** 离线标记集合:命中即归一化为离线(0),其余(含空)均视为在线(1) */
|
||||
export const OFFLINE_MARKERS = new Set(["02", "0", "false", "off", "离线"]);
|
||||
|
||||
/** 将 ICC 原始记录映射为本系统 electric_line 落库字段(不含联表外键)。 */
|
||||
/** 字符串 trim;null / undefined / 空串统一归一为 undefined。 */
|
||||
export function toOptionalString(value: unknown): string | undefined {
|
||||
if (value === undefined || value === null) return undefined;
|
||||
const s = String(value).trim();
|
||||
return s === "" ? undefined : s;
|
||||
}
|
||||
|
||||
/** 将 ICC 原始探测器记录映射为本系统 electric_line 落库字段(不含联表外键)。 */
|
||||
export function mapIccLineRecord(raw: IccLineRawRecord): {
|
||||
lineCode: string;
|
||||
iccDeviceCode: string;
|
||||
equipCode?: string;
|
||||
lineName?: string;
|
||||
customerNo?: string;
|
||||
buildingName?: string;
|
||||
floorNo?: string;
|
||||
location?: string;
|
||||
imei?: string;
|
||||
orgCode?: string;
|
||||
parentCode?: string;
|
||||
typeCode?: string;
|
||||
typeName?: string;
|
||||
fireSystemCode?: string;
|
||||
fireSystemName?: string;
|
||||
partUnitLoopCode?: string;
|
||||
channelNo?: string;
|
||||
channelSn?: string;
|
||||
produceDate?: 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
|
||||
const result: Record<string, any> = {
|
||||
// 匹配键保持空串语义,由调用方判跳
|
||||
lineCode: String(raw[f.lineCode] ?? "").trim()
|
||||
};
|
||||
}
|
||||
// 可选字符串字段
|
||||
for (const key of ["lineName", "imei", "orgCode", "parentCode", "typeCode", "typeName", "fireSystemCode", "fireSystemName", "partUnitLoopCode", "channelNo", "channelSn", "produceDate"] as const) {
|
||||
result[key] = toOptionalString(raw[f[key]]);
|
||||
}
|
||||
// 上报状态归一化:离线标记集合 → 0,其余(含空)→ 1
|
||||
const statusRaw = String(raw[f.detectorStatus] ?? "").trim();
|
||||
result.status = OFFLINE_MARKERS.has(statusRaw) ? 0 : 1;
|
||||
return result as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析探测器分页响应(兼容三种形态):
|
||||
* - payload 为数组 → 直接作为列表
|
||||
* - payload.data.pageData 为数组 → 取 pageData
|
||||
* - payload.data.list 为数组 → 取 list
|
||||
* - 结构不匹配或解析异常 → 返回空列表
|
||||
*/
|
||||
export function parseLinePage(payload: any): {
|
||||
list: IccLineRawRecord[];
|
||||
nextPage?: number;
|
||||
totalCount?: number;
|
||||
} {
|
||||
try {
|
||||
if (Array.isArray(payload)) {
|
||||
return { list: payload };
|
||||
}
|
||||
const page = payload?.data;
|
||||
if (page && Array.isArray(page.pageData)) {
|
||||
return {
|
||||
list: page.pageData,
|
||||
nextPage: Number(page.nextPage) > 0 ? Number(page.nextPage) : undefined,
|
||||
totalCount: Number(page.totalCount) > 0 ? Number(page.totalCount) : undefined
|
||||
};
|
||||
}
|
||||
if (page && Array.isArray(page.list)) {
|
||||
return {
|
||||
list: page.list,
|
||||
nextPage: Number(page.nextPage) > 0 ? Number(page.nextPage) : undefined,
|
||||
totalCount: Number(page.totalCount) > 0 ? Number(page.totalCount) : undefined
|
||||
};
|
||||
}
|
||||
return { list: [] };
|
||||
} catch {
|
||||
return { list: [] };
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user