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
+494
-184
No files matched your search
@@ -0,0 +1,12 @@
|
|||||||
|
# Checklist
|
||||||
|
|
||||||
|
- [x] electric_line 表结构按 spec 重建:删 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;迁移已执行到生产库且 db_seed.sql/db_init.sql 一致
|
||||||
|
- [x] 设计文档「表结构新增及变更.md」已补充 electric_line 新结构定义
|
||||||
|
- [x] ElectricLine 实体与迁移一致(gatewayId FK→electric_gateway,删除旧占位字段)
|
||||||
|
- [x] icc_line_mapper 按 DetectorVO 映射:lineCode=detectorCode、lineName=detectorName、status 01→1/02→0、orgCode/parentCode 透出作匹配键;parseLinePage 支持 PageVO 分页结构
|
||||||
|
- [x] icc_line_sync.service 复用 getIccAccessToken/iccFetch 真实鉴权,POST 分页 pageSize=50 短页停止全量拉取;按 line_code 幂等 upsert(equip_id 不填);customer_id 由 orgCode 匹配、gateway_id 由 parentCode 匹配,失败跳过;写 icc_sync_log(sync_type=electric_line);拉取失败仅记日志不抛业务错
|
||||||
|
- [x] 定时任务 icc_line_sync(jobKey/cron */10)注册链路可用;POST /api/electric-lines/sync 返回 {pulled,inserted,updated,skipped,synced}
|
||||||
|
- [x] 列表接口 GET /api/electric-lines 返回新字段(含 gateway/type/imei),非 ADMIN 限定 customer_id,keyword 检索含 imei
|
||||||
|
- [x] 前端 /device/line:删除建筑/楼层/位置列,新增 IMEI/探测器类型/回路/通道/消防系统/生产日期/所属网关列;无 v-html;同步按钮调用真实接口
|
||||||
|
- [x] 后端 tsc --noEmit、前端 vue-tsc -b 均通过
|
||||||
|
- [x] 真实同步端到端:admin 登录 → 手动同步 → 探测器数据落库且 customer/gateway 关联正确 → 重跑幂等 updated;浏览器页面新列渲染正常
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# 智电线路真实 ICC 同步 Spec(add-icc-line-sync)
|
||||||
|
|
||||||
|
## Why
|
||||||
|
现有 electric_line(智电改造线路台账)为占位实现:表结构是手工设计的占位字段(building/floor/location 等),同步链路 `icc_line_sync.service.ts` 走假的 `ICC_TOKEN` 配置、无真实鉴权。本次依据 ICC 平台接口文档第三节「消防探测器分页查询」(`POST /evo-apigw/evo-fdbu/{version}/detector/subsystem/page`)将线路台账升级为**真实数据同步**:重建表结构(按 DetectorVO 落库字段 + 与 customer/customer_equipment/electric_gateway 三张表的关联),后端替换为真实 OAuth2 拉取(复用 icc_customer_auth),前端台账页补充新字段展示并保留手动同步按钮。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
- **表结构变更(BREAKING)**:`electric_line` 按 DetectorVO 重建字段集:
|
||||||
|
- 删除:`building_id`、`floor_no`、`location`、`icc_device_code`(占位字段无真实数据源)
|
||||||
|
- `equip_id` 由 `NOT NULL` 改为**可空**(→ customer_equipment,同步不填充,留待手动关联)
|
||||||
|
- 新增:`imei`、`gateway_id`(→ electric_gateway)、`type_code`、`type_name`、`fire_system_code`、`fire_system_name`、`part_unit_loop_code`、`channel_no`、`channel_sn`、`produce_date`
|
||||||
|
- 保留:`line_code`(= detectorCode,唯一幂等键)、`line_name`(= detectorName)、`customer_id`(NOT NULL)、`status`(= detectorStatus:01→1 在线 / 02→0 离线)、`synced_at`、`created_at`
|
||||||
|
- 不落库(仅作关联匹配键):`orgCode`→customer.icc_code 反查 customer_id;`parentCode`→electric_gateway.device_code 反查 gateway_id
|
||||||
|
- **后端同步真实化**:重写 `icc_line_mapper.ts`(DetectorVO 映射 + 分页解析)与 `icc_line_sync.service.ts`(复用 `getIccAccessToken`/`iccFetch`,POST 分页 pageSize=50 短页停止、全量拉取、按 line_code 幂等 upsert、写 icc_sync_log sync_type=electric_line、失败仅记日志不抛)
|
||||||
|
- **关联规则**:customer_id 由 orgCode 匹配;gateway_id 由 parentCode 匹配 device_code;equip_id 同步不填充
|
||||||
|
- **前端台账页更新**:`views/device/line/index.vue` 移除楼层/建筑/位置列,新增探测器类型、IMEI、回路/通道、消防系统、生产日期、网关等列;保留「同步线路」手动按钮与筛选
|
||||||
|
- **设计文档同步**:更新 `系统设计/表结构新增及变更.md` 补充 electric_line 新结构
|
||||||
|
- 定时任务 `icc_line_sync`(jobKey 不变,cron */10)、菜单 61、RBAC `/api/electric-lines → device:list`、前端路由 `/device/line` 均沿用既有,无需新增
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
- Affected specs:add-icc-line-ledger(本 spec 升级其占位实现)、add-icc-gateway-sync(复用其真实同步模式与关联表)
|
||||||
|
- Affected code:
|
||||||
|
- 后端:`server/src/entity/electric_line.entity.ts`、`server/src/task/icc_line_mapper.ts`、`server/src/service/icc_line_sync.service.ts`、`server/src/service/electric_line.service.ts`(decorate 去掉 building、新增 gateway/类型展示)、`server/src/controller/electric_line.controller.ts`(不动)
|
||||||
|
- 迁移:`database/migrations/20261011_icc_line_ledger.sql`(重建 electric_line 结构)或新增增量迁移;`database/db_seed.sql` 同步
|
||||||
|
- 前端:`client/src/views/device/line/index.vue`、`client/src/api/device.ts`(ElectricLine 类型)
|
||||||
|
- 设计文档:`系统设计/表结构新增及变更.md`
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: electric_line 表结构按 DetectorVO 重建
|
||||||
|
系统 SHALL 将 `electric_line` 重建为与 DetectorVO 对应的字段集,且:
|
||||||
|
- `line_code` 存 detectorCode(唯一,幂等键)
|
||||||
|
- `customer_id` NOT NULL,由 orgCode→customer.icc_code 反查
|
||||||
|
- `gateway_id` 可空,由 parentCode→electric_gateway.device_code 反查
|
||||||
|
- `equip_id` 可空,同步不填充
|
||||||
|
|
||||||
|
#### Scenario: 迁移执行成功
|
||||||
|
- **WHEN** 执行迁移到生产库
|
||||||
|
- **THEN** electric_line 包含新字段集,旧字段(building_id/floor_no/location/icc_device_code)被移除,equip_id 可空,索引重建;db_seed.sql 与生产库一致
|
||||||
|
|
||||||
|
### Requirement: 真实 ICC 探测器拉取同步
|
||||||
|
系统 SHALL 复用 `getIccAccessToken`/`iccFetch` 真实鉴权,POST `{baseUrl}/evo-apigw/evo-fdbu/{version}/detector/subsystem/page` 分页全量拉取(pageNum 递增、pageSize=50、短页停止、MAX_PAGES 保护),按 line_code 幂等 upsert,写 icc_sync_log(sync_type=electric_line)。
|
||||||
|
|
||||||
|
#### Scenario: 同步成功
|
||||||
|
- **WHEN** 手动点击「同步线路」或定时任务触发
|
||||||
|
- **THEN** 返回 {pulled, inserted, updated, skipped, synced};新探测器落库、重复 detectorCode 更新;customer_id 解析失败的行跳过;写成功日志
|
||||||
|
|
||||||
|
#### Scenario: 同步失败
|
||||||
|
- **WHEN** ICC 拉取异常(超时/HTTP 错误/鉴权失败)
|
||||||
|
- **THEN** 仅写 icc_sync_log 失败日志,不抛业务错误,接口返回 code 200 与当前计数
|
||||||
|
|
||||||
|
### Requirement: 前端台账页按新字段展示
|
||||||
|
系统 SHALL 更新 `views/device/line/index.vue`:删除建筑/楼层/位置列,新增探测器类型、IMEI、回路/通道、消防系统、生产日期、所属网关列;保留筛选(客户/状态/关键词)与「同步线路」手动按钮。
|
||||||
|
|
||||||
|
#### Scenario: 页面渲染
|
||||||
|
- **WHEN** 登录 admin 打开 /device/line
|
||||||
|
- **THEN** 表格展示新字段列,无 v-html,同步按钮触发 POST /api/electric-lines/sync
|
||||||
|
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: 线路台账列表接口(原有)
|
||||||
|
`GET /api/electric-lines` 保留分页/筛选/RBAC(非 ADMIN 限定 customer_id),decorate 从 building 改为 gateway/类型/IMEI 展示字段,删除 customer_building 关联。
|
||||||
|
|
||||||
|
#### Scenario: 列表返回
|
||||||
|
- **WHEN** 带权限请求 GET /api/electric-lines
|
||||||
|
- **THEN** 返回行包含 lineCode/lineName/imei/customer/gateway/type/loop/channel/fireSystem/produceDate/status/syncedAt
|
||||||
|
|
||||||
|
## REMOVED Requirements
|
||||||
|
|
||||||
|
### Requirement: 占位 ICC 同步实现(原 icc_line_sync.service / icc_line_mapper 假配置映射)
|
||||||
|
**Reason**:无真实鉴权与接口,与 ICC 文档字段不对应。
|
||||||
|
**Migration**:删除 `ICC_TOKEN` 占位用法,改为复用 icc_customer_auth 真实鉴权;映射按 DetectorVO 重写。
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Tasks
|
||||||
|
|
||||||
|
- [x] Task 1: 数据库迁移:重建 `electric_line` 结构(删 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),迁移脚本 `database/migrations/20261024_icc_line_rebuild.sql`,同步更新 `database/db_seed.sql`(fk_line_gateway 幂等补充)与 `database/db_init.sql`(基线建表);`gateway_id` 建 `REFERENCES electric_gateway(id)` 与索引
|
||||||
|
- [x] SubTask 1.1: 核对生产库 electric_line 现状(旧结构迁移而来,无占位数据阻碍),确认 ALTER 方案幂等
|
||||||
|
- [x] SubTask 1.2: 执行迁移到远程生产库并验证字段/索引/外键(columns 18 个、idx_line_gateway/imei、fk_line_gateway 均就绪)
|
||||||
|
- [x] SubTask 1.3: 同步 `系统设计/表结构新增及变更.md`(补充 electric_line 新表结构定义)
|
||||||
|
- [x] Task 2: 后端实体:按新字段重写 `server/src/entity/electric_line.entity.ts`(gatewayId FK→ElectricGateway,删除 building/floor/location/iccDeviceCode)
|
||||||
|
- [x] Task 3: 映射层:重写 `server/src/task/icc_line_mapper.ts`
|
||||||
|
- [x] SubTask 3.1: 定义 `ICC_LINE_ENDPOINT = /evo-apigw/evo-fdbu/{version}/detector/subsystem/page` 与 DetectorVO 字段映射(lineCode=detectorCode、lineName=detectorName、imei、typeCode/typeName、fireSystemCode/Name、partUnitLoopCode、channelNo、channelSn、produceDate、status 归一 01→1/02→0、orgCode、parentCode 作为匹配键透出)
|
||||||
|
- [x] SubTask 3.2: `parseLinePage(payload)` 解析 PageVO{nextPage,totalCount,pageData}(兼容数组形态)
|
||||||
|
- [x] Task 4: 同步服务:重写 `server/src/service/icc_line_sync.service.ts`,复用 `getIccAccessToken`/`iccFetch`,POST 分页 pageSize=50 短页停止全量拉取;预取 orgCode→customer.id(customer.icc_code)与 parentCode→gateway.id(electric_gateway.device_code)映射避免 N+1;按 line_code 幂等 upsert(equip_id 不填);写 icc_sync_log(sync_type=electric_line);拉取失败仅记日志不抛
|
||||||
|
- [x] SubTask 4.1: 定时任务 `icc_line_sync.task.ts`(jobKey/cron 不变)与 job_registry/JobRuntime(job_scheduler.service.ts syncIccLines)链路可用
|
||||||
|
- [x] SubTask 4.2: 控制器 `POST /api/electric-lines/sync` 复用返回结构(pulled/inserted/updated/skipped/synced)
|
||||||
|
- [x] Task 5: 列表服务:`server/src/service/electric_line.service.ts` 更新 decorate(删除 building 反查,新增 gateway/type/imei 展示字段);keyword 检索范围补充 imei
|
||||||
|
- [x] Task 6: 前端:`client/src/api/device.ts` 更新 `ElectricLine` 类型(新字段);`client/src/views/device/line/index.vue` 删除建筑/楼层/位置列,新增 IMEI/探测器类型/回路/通道/消防系统/生产日期/所属网关列,筛选/分页/同步按钮保留
|
||||||
|
- [x] Task 7: 验证:
|
||||||
|
- [x] SubTask 7.1: 后端 `tsc --noEmit`、前端 `vue-tsc -b` 均 0 错误
|
||||||
|
- [x] SubTask 7.2: 真实拉取 ICC 探测器(admin token,拉取 4 条插入 4 条;重跑幂等 updated=4;icc_sync_log 3 条 status=1;列表接口返回新字段)
|
||||||
|
- [x] SubTask 7.3: 浏览器 admin 登录验证 /device/line 页面新列渲染与手动同步按钮(11 列齐全、同步提示「拉取 4 条,新增 0,更新 4」)
|
||||||
|
|
||||||
|
# Task Dependencies
|
||||||
|
- Task 2/3/6 依赖 Task 1(表结构先就绪)
|
||||||
|
- Task 4 依赖 Task 2/3
|
||||||
|
- Task 5 依赖 Task 2
|
||||||
|
- Task 7 依赖全部
|
||||||
@@ -154,9 +154,6 @@ export interface ElectricLine {
|
|||||||
iccDeviceCode?: string;
|
iccDeviceCode?: string;
|
||||||
lineName?: string;
|
lineName?: string;
|
||||||
customerId: number;
|
customerId: number;
|
||||||
buildingId?: number;
|
|
||||||
floorNo?: string;
|
|
||||||
location?: string;
|
|
||||||
status: number;
|
status: number;
|
||||||
syncedAt?: string;
|
syncedAt?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -165,7 +162,19 @@ export interface ElectricLine {
|
|||||||
customerName?: string;
|
customerName?: string;
|
||||||
equipCode?: string;
|
equipCode?: string;
|
||||||
equipName?: string;
|
equipName?: string;
|
||||||
buildingName?: string;
|
// 探测器 / 网关相关字段
|
||||||
|
imei?: string;
|
||||||
|
gatewayId?: number;
|
||||||
|
gatewayCode?: string;
|
||||||
|
gatewayName?: string;
|
||||||
|
typeCode?: string;
|
||||||
|
typeName?: string;
|
||||||
|
fireSystemCode?: string;
|
||||||
|
fireSystemName?: string;
|
||||||
|
partUnitLoopCode?: string;
|
||||||
|
channelNo?: string;
|
||||||
|
channelSn?: string;
|
||||||
|
produceDate?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ElectricLineQuery {
|
export interface ElectricLineQuery {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<span class="main-title">智电改造线路台账</span>
|
<span class="main-title">智电改造线路台账</span>
|
||||||
<span class="sub-title">ELECTRIC LINE LEDGER</span>
|
<span class="sub-title">ELECTRIC LINE LEDGER</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="desc">由 ICC 平台同步的智电改造线路台账,按客户、设备、建筑查看线路及在线状态。</p>
|
<p class="desc">由 ICC 平台同步的智电改造线路台账,按客户、网关查看线路、探测器及在线状态。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
<label class="filter-label">关键词检索</label>
|
<label class="filter-label">关键词检索</label>
|
||||||
<a-input
|
<a-input
|
||||||
v-model:value="searchForm.keyword"
|
v-model:value="searchForm.keyword"
|
||||||
placeholder="线路名称 / 线路编码"
|
placeholder="线路名称 / 编码 / IMEI"
|
||||||
allow-clear
|
allow-clear
|
||||||
@pressEnter="handleSearch"
|
@pressEnter="handleSearch"
|
||||||
>
|
>
|
||||||
@@ -103,16 +103,32 @@
|
|||||||
<span class="line-name">{{ record.lineName || '-' }}</span>
|
<span class="line-name">{{ record.lineName || '-' }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="column.key === 'imei'">
|
||||||
|
<span class="mono">{{ record.imei || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-else-if="column.key === 'customerId'">
|
<template v-else-if="column.key === 'customerId'">
|
||||||
{{ record.customerName ? `${record.customerName}(${record.customerNo})` : '-' }}
|
{{ record.customerName ? `${record.customerName}(${record.customerNo})` : '-' }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="column.key === 'equipId'">
|
<template v-else-if="column.key === 'gatewayId'">
|
||||||
{{ record.equipName ? `${record.equipName}(${record.equipCode})` : '-' }}
|
{{ record.gatewayName ? `${record.gatewayName}(${record.gatewayCode})` : (record.gatewayCode || '-') }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="column.key === 'buildingId'">
|
<template v-else-if="column.key === 'typeName'">
|
||||||
{{ record.buildingName || '-' }}
|
{{ record.typeName || '-' }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="column.key === 'loopChannel'">
|
||||||
|
{{ [record.partUnitLoopCode, record.channelNo].filter(Boolean).join('/') || '-' }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="column.key === 'fireSystemName'">
|
||||||
|
{{ record.fireSystemName || '-' }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="column.key === 'produceDate'">
|
||||||
|
{{ record.produceDate || '-' }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="column.key === 'status'">
|
<template v-else-if="column.key === 'status'">
|
||||||
@@ -172,11 +188,13 @@ const customerOptions = ref<{ label: string; value: number }[]>([]);
|
|||||||
const columns = [
|
const columns = [
|
||||||
{ title: '线路编码', dataIndex: 'lineCode', key: 'lineCode', ellipsis: true, width: 160 },
|
{ title: '线路编码', dataIndex: 'lineCode', key: 'lineCode', ellipsis: true, width: 160 },
|
||||||
{ title: '线路名称', dataIndex: 'lineName', key: 'lineName', ellipsis: true },
|
{ title: '线路名称', dataIndex: 'lineName', key: 'lineName', ellipsis: true },
|
||||||
|
{ title: 'IMEI', dataIndex: 'imei', key: 'imei', ellipsis: true, width: 180 },
|
||||||
{ title: '所属客户', dataIndex: 'customerId', key: 'customerId', ellipsis: true, width: 180 },
|
{ title: '所属客户', dataIndex: 'customerId', key: 'customerId', ellipsis: true, width: 180 },
|
||||||
{ title: '所属设备', dataIndex: 'equipId', key: 'equipId', ellipsis: true, width: 180 },
|
{ title: '所属网关', dataIndex: 'gatewayId', key: 'gatewayId', ellipsis: true, width: 180 },
|
||||||
{ title: '所属建筑', dataIndex: 'buildingId', key: 'buildingId', ellipsis: true, width: 140 },
|
{ title: '探测器类型', dataIndex: 'typeName', key: 'typeName', ellipsis: true, width: 120 },
|
||||||
{ title: '所在楼层', dataIndex: 'floorNo', key: 'floorNo', ellipsis: true, width: 100 },
|
{ title: '回路/通道', dataIndex: 'loopChannel', key: 'loopChannel', ellipsis: true, width: 160 },
|
||||||
{ title: '所在位置', dataIndex: 'location', key: 'location', ellipsis: true },
|
{ title: '消防系统', dataIndex: 'fireSystemName', key: 'fireSystemName', ellipsis: true, width: 140 },
|
||||||
|
{ title: '生产日期', dataIndex: 'produceDate', key: 'produceDate', ellipsis: true, width: 120 },
|
||||||
{ title: '状态', dataIndex: 'status', key: 'status', align: 'center' as const, width: 80 },
|
{ title: '状态', dataIndex: 'status', key: 'status', align: 'center' as const, width: 80 },
|
||||||
{ title: '同步时间', dataIndex: 'syncedAt', key: 'syncedAt', ellipsis: true, width: 170 },
|
{ title: '同步时间', dataIndex: 'syncedAt', key: 'syncedAt', ellipsis: true, width: 170 },
|
||||||
];
|
];
|
||||||
|
|||||||
+30
-18
@@ -616,33 +616,45 @@ DROP TABLE IF EXISTS electric_line CASCADE;
|
|||||||
CREATE TABLE electric_line (
|
CREATE TABLE electric_line (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
line_code VARCHAR(64) NOT NULL UNIQUE,
|
line_code VARCHAR(64) NOT NULL UNIQUE,
|
||||||
equip_id BIGINT NOT NULL REFERENCES customer_equipment(id),
|
equip_id BIGINT,
|
||||||
icc_device_code VARCHAR(128),
|
gateway_id BIGINT,
|
||||||
line_name VARCHAR(128),
|
|
||||||
customer_id BIGINT NOT NULL REFERENCES customer(id),
|
customer_id BIGINT NOT NULL REFERENCES customer(id),
|
||||||
building_id BIGINT REFERENCES customer_building(id),
|
line_name VARCHAR(128),
|
||||||
floor_no VARCHAR(16),
|
imei VARCHAR(64),
|
||||||
location VARCHAR(256),
|
type_code VARCHAR(32),
|
||||||
|
type_name VARCHAR(64),
|
||||||
|
fire_system_code VARCHAR(64),
|
||||||
|
fire_system_name VARCHAR(128),
|
||||||
|
part_unit_loop_code VARCHAR(64),
|
||||||
|
channel_no VARCHAR(64),
|
||||||
|
channel_sn VARCHAR(64),
|
||||||
|
produce_date VARCHAR(16),
|
||||||
status SMALLINT NOT NULL DEFAULT 1,
|
status SMALLINT NOT NULL DEFAULT 1,
|
||||||
synced_at TIMESTAMPTZ,
|
synced_at TIMESTAMPTZ,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
);
|
);
|
||||||
COMMENT ON TABLE electric_line IS '智电改造线路信息表(ICC 同步)';
|
COMMENT ON TABLE electric_line IS '智电改造线路信息表(ICC 探测器同步)';
|
||||||
COMMENT ON COLUMN electric_line.id IS '自增主键';
|
COMMENT ON COLUMN electric_line.line_code IS '探测器编码(ICC detectorCode,唯一幂等键)';
|
||||||
COMMENT ON COLUMN electric_line.line_code IS '线路编码(唯一)';
|
COMMENT ON COLUMN electric_line.line_name IS '探测器名称(ICC detectorName)';
|
||||||
COMMENT ON COLUMN electric_line.equip_id IS '客户设备 ID(→ customer_equipment)';
|
COMMENT ON COLUMN electric_line.imei IS '设备唯一标识码(ICC imei)';
|
||||||
COMMENT ON COLUMN electric_line.icc_device_code IS 'ICC 智电设备编号';
|
COMMENT ON COLUMN electric_line.equip_id IS '客户设备 ID(→ customer_equipment,可空,同步不填充)';
|
||||||
COMMENT ON COLUMN electric_line.line_name IS '线路名称(ICC 同步)';
|
COMMENT ON COLUMN electric_line.gateway_id IS '网关设备 ID(→ electric_gateway,外键由迁移/db_seed 建立)';
|
||||||
COMMENT ON COLUMN electric_line.customer_id IS '客户 ID(逻辑隔离键)';
|
COMMENT ON COLUMN electric_line.customer_id IS '客户 ID(orgCode→customer.icc_code 解析,逻辑隔离键)';
|
||||||
COMMENT ON COLUMN electric_line.building_id IS '所属建筑 ID(→ customer_building)';
|
COMMENT ON COLUMN electric_line.type_code IS '探测器类型编码';
|
||||||
COMMENT ON COLUMN electric_line.floor_no IS '所在楼层';
|
COMMENT ON COLUMN electric_line.type_name IS '探测器类型名称';
|
||||||
COMMENT ON COLUMN electric_line.location IS '所在位置';
|
COMMENT ON COLUMN electric_line.fire_system_code IS '消防系统编码';
|
||||||
COMMENT ON COLUMN electric_line.status IS '状态:1在线 0离线';
|
COMMENT ON COLUMN electric_line.fire_system_name IS '消防系统名称';
|
||||||
|
COMMENT ON COLUMN electric_line.part_unit_loop_code IS '探测器回路号';
|
||||||
|
COMMENT ON COLUMN electric_line.channel_no IS '探测器通道号';
|
||||||
|
COMMENT ON COLUMN electric_line.channel_sn IS '通道唯一标识码';
|
||||||
|
COMMENT ON COLUMN electric_line.produce_date IS '生产日期(yyyy-MM-dd)';
|
||||||
|
COMMENT ON COLUMN electric_line.status IS '探测器状态:1在线 0离线(detectorStatus 01→1 / 02→0)';
|
||||||
COMMENT ON COLUMN electric_line.synced_at IS 'ICC 同步时间';
|
COMMENT ON COLUMN electric_line.synced_at IS 'ICC 同步时间';
|
||||||
COMMENT ON COLUMN electric_line.created_at IS '创建时间';
|
COMMENT ON COLUMN electric_line.created_at IS '创建时间';
|
||||||
CREATE INDEX idx_line_customer ON electric_line (customer_id);
|
CREATE INDEX idx_line_customer ON electric_line (customer_id);
|
||||||
CREATE INDEX idx_line_equip ON electric_line (equip_id);
|
CREATE INDEX idx_line_equip ON electric_line (equip_id);
|
||||||
CREATE INDEX idx_line_icc ON electric_line (icc_device_code);
|
CREATE INDEX idx_line_gateway ON electric_line (gateway_id);
|
||||||
|
CREATE INDEX idx_line_imei ON electric_line (imei);
|
||||||
|
|
||||||
-- 20. 线路所属房间关联表
|
-- 20. 线路所属房间关联表
|
||||||
DROP TABLE IF EXISTS line_room CASCADE;
|
DROP TABLE IF EXISTS line_room CASCADE;
|
||||||
|
|||||||
@@ -493,6 +493,14 @@ CREATE INDEX IF NOT EXISTS idx_gateway_customer ON electric_gateway (customer_id
|
|||||||
CREATE INDEX IF NOT EXISTS idx_gateway_equip ON electric_gateway (equip_id);
|
CREATE INDEX IF NOT EXISTS idx_gateway_equip ON electric_gateway (equip_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_gateway_imei ON electric_gateway (icc_imei);
|
CREATE INDEX IF NOT EXISTS idx_gateway_imei ON electric_gateway (icc_imei);
|
||||||
|
|
||||||
|
-- 智电改造线路表:gateway_id 外键补充(FK 由迁移/本段建立;db_init.sql 中为普通 BIGINT)
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_line_gateway' AND conrelid = 'electric_line'::regclass) THEN
|
||||||
|
ALTER TABLE electric_line ADD CONSTRAINT fk_line_gateway FOREIGN KEY (gateway_id) REFERENCES electric_gateway(id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
-- 菜单 62:设备监控下的智电改造网关台账菜单
|
-- 菜单 62:设备监控下的智电改造网关台账菜单
|
||||||
INSERT INTO sys_menu (id, parent_id, menu_name, menu_path, component, icon, sort_order, menu_type, permission, visible) VALUES
|
INSERT INTO sys_menu (id, parent_id, menu_name, menu_path, component, icon, sort_order, menu_type, permission, visible) VALUES
|
||||||
(62, 4, '智电改造网关台账', '/device/gateway', 'device/gateway/index', 'ApiOutlined', 3, 2, 'device:list', 1)
|
(62, 4, '智电改造网关台账', '/device/gateway', 'device/gateway/index', 'ApiOutlined', 3, 2, 'device:list', 1)
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
-- 智电改造线路台账真实 ICC 同步:重建 electric_line 表结构(按 ICC DetectorVO)
|
||||||
|
-- 1) 增列/删列/改可空(幂等);2) gateway_id 外键 → electric_gateway;3) 索引与注释同步
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE electric_line ADD COLUMN IF NOT EXISTS imei VARCHAR(64);
|
||||||
|
ALTER TABLE electric_line ADD COLUMN IF NOT EXISTS gateway_id BIGINT;
|
||||||
|
ALTER TABLE electric_line ADD COLUMN IF NOT EXISTS type_code VARCHAR(32);
|
||||||
|
ALTER TABLE electric_line ADD COLUMN IF NOT EXISTS type_name VARCHAR(64);
|
||||||
|
ALTER TABLE electric_line ADD COLUMN IF NOT EXISTS fire_system_code VARCHAR(64);
|
||||||
|
ALTER TABLE electric_line ADD COLUMN IF NOT EXISTS fire_system_name VARCHAR(128);
|
||||||
|
ALTER TABLE electric_line ADD COLUMN IF NOT EXISTS part_unit_loop_code VARCHAR(64);
|
||||||
|
ALTER TABLE electric_line ADD COLUMN IF NOT EXISTS channel_no VARCHAR(64);
|
||||||
|
ALTER TABLE electric_line ADD COLUMN IF NOT EXISTS channel_sn VARCHAR(64);
|
||||||
|
ALTER TABLE electric_line ADD COLUMN IF NOT EXISTS produce_date VARCHAR(16);
|
||||||
|
|
||||||
|
ALTER TABLE electric_line DROP COLUMN IF EXISTS icc_device_code;
|
||||||
|
ALTER TABLE electric_line DROP COLUMN IF EXISTS building_id;
|
||||||
|
ALTER TABLE electric_line DROP COLUMN IF EXISTS floor_no;
|
||||||
|
ALTER TABLE electric_line DROP COLUMN IF EXISTS location;
|
||||||
|
|
||||||
|
ALTER TABLE electric_line ALTER COLUMN equip_id DROP NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE electric_line ADD CONSTRAINT fk_line_gateway FOREIGN KEY (gateway_id) REFERENCES electric_gateway(id);
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_line_icc;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_line_gateway ON electric_line (gateway_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_line_imei ON electric_line (imei);
|
||||||
|
|
||||||
|
COMMENT ON TABLE electric_line IS '智电改造线路信息表(ICC 探测器同步)';
|
||||||
|
COMMENT ON COLUMN electric_line.line_code IS '探测器编码(ICC detectorCode,唯一幂等键)';
|
||||||
|
COMMENT ON COLUMN electric_line.line_name IS '探测器名称(ICC detectorName)';
|
||||||
|
COMMENT ON COLUMN electric_line.imei IS '设备唯一标识码(ICC imei)';
|
||||||
|
COMMENT ON COLUMN electric_line.equip_id IS '客户设备 ID(→ customer_equipment,可空,同步不填充)';
|
||||||
|
COMMENT ON COLUMN electric_line.gateway_id IS '网关设备 ID(→ electric_gateway,由 parentCode↔device_code 匹配)';
|
||||||
|
COMMENT ON COLUMN electric_line.customer_id IS '客户 ID(orgCode→customer.icc_code 解析,逻辑隔离键)';
|
||||||
|
COMMENT ON COLUMN electric_line.type_code IS '探测器类型编码';
|
||||||
|
COMMENT ON COLUMN electric_line.type_name IS '探测器类型名称';
|
||||||
|
COMMENT ON COLUMN electric_line.fire_system_code IS '消防系统编码';
|
||||||
|
COMMENT ON COLUMN electric_line.fire_system_name IS '消防系统名称';
|
||||||
|
COMMENT ON COLUMN electric_line.part_unit_loop_code IS '探测器回路号';
|
||||||
|
COMMENT ON COLUMN electric_line.channel_no IS '探测器通道号';
|
||||||
|
COMMENT ON COLUMN electric_line.channel_sn IS '通道唯一标识码';
|
||||||
|
COMMENT ON COLUMN electric_line.produce_date IS '生产日期(yyyy-MM-dd)';
|
||||||
|
COMMENT ON COLUMN electric_line.status IS '探测器状态:1在线 0离线(detectorStatus 01→1 / 02→0)';
|
||||||
|
COMMENT ON COLUMN electric_line.synced_at IS 'ICC 同步时间';
|
||||||
|
COMMENT ON COLUMN electric_line.created_at IS '创建时间';
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
|
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
|
||||||
import CustomerEquipment from "./customer_equipment.entity";
|
import CustomerEquipment from "./customer_equipment.entity";
|
||||||
|
import ElectricGateway from "./electric_gateway.entity";
|
||||||
import Customer from "./customer.entity";
|
import Customer from "./customer.entity";
|
||||||
import CustomerBuilding from "./customer_building.entity";
|
|
||||||
|
|
||||||
@Table({
|
@Table({
|
||||||
tableName: "electric_line",
|
tableName: "electric_line",
|
||||||
@@ -10,41 +10,60 @@ import CustomerBuilding from "./customer_building.entity";
|
|||||||
indexes: [
|
indexes: [
|
||||||
{ name: "idx_line_customer", fields: ["customer_id"] },
|
{ name: "idx_line_customer", fields: ["customer_id"] },
|
||||||
{ name: "idx_line_equip", fields: ["equip_id"] },
|
{ name: "idx_line_equip", fields: ["equip_id"] },
|
||||||
{ name: "idx_line_icc", fields: ["icc_device_code"] }
|
{ name: "idx_line_gateway", fields: ["gateway_id"] },
|
||||||
|
{ name: "idx_line_imei", fields: ["imei"] }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export default class ElectricLine extends Model {
|
export default class ElectricLine extends Model {
|
||||||
@Column({ type: DataType.BIGINT, autoIncrement: true, primaryKey: true })
|
@Column({ type: DataType.BIGINT, autoIncrement: true, primaryKey: true })
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
@Column({ type: DataType.STRING(64), allowNull: false, unique: true, field: "line_code", comment: "线路编码(唯一)" })
|
@Column({ type: DataType.STRING(64), allowNull: false, unique: true, field: "line_code", comment: "探测器编码(detectorCode)唯一幂等键" })
|
||||||
lineCode!: string;
|
lineCode!: string;
|
||||||
|
|
||||||
@ForeignKey(() => CustomerEquipment)
|
@Column({ type: DataType.STRING(128), field: "line_name", comment: "探测器名称(detectorName)" })
|
||||||
@Column({ type: DataType.BIGINT, allowNull: false, field: "equip_id", comment: "客户设备 ID(→ customer_equipment)" })
|
|
||||||
equipId!: number;
|
|
||||||
|
|
||||||
@Column({ type: DataType.STRING(128), field: "icc_device_code", comment: "ICC 智电设备编号" })
|
|
||||||
iccDeviceCode!: string;
|
|
||||||
|
|
||||||
@Column({ type: DataType.STRING(128), field: "line_name", comment: "线路名称(ICC 同步)" })
|
|
||||||
lineName!: string;
|
lineName!: string;
|
||||||
|
|
||||||
|
@Column({ type: DataType.STRING(64), field: "imei", comment: "探测器 IMEI" })
|
||||||
|
imei!: string;
|
||||||
|
|
||||||
|
@ForeignKey(() => CustomerEquipment)
|
||||||
|
@Column({ type: DataType.BIGINT, field: "equip_id", comment: "客户设备 ID(→ customer_equipment,同步不填充)" })
|
||||||
|
equipId!: number;
|
||||||
|
|
||||||
|
@ForeignKey(() => ElectricGateway)
|
||||||
|
@Column({ type: DataType.BIGINT, field: "gateway_id", comment: "网关 ID(→ electric_gateway,由 parentCode↔device_code 匹配)" })
|
||||||
|
gatewayId!: number;
|
||||||
|
|
||||||
@ForeignKey(() => Customer)
|
@ForeignKey(() => Customer)
|
||||||
@Column({ type: DataType.BIGINT, allowNull: false, field: "customer_id", comment: "客户 ID(逻辑隔离键)" })
|
@Column({ type: DataType.BIGINT, allowNull: false, field: "customer_id", comment: "客户 ID(由 orgCode→customer.icc_code 解析)" })
|
||||||
customerId!: number;
|
customerId!: number;
|
||||||
|
|
||||||
@ForeignKey(() => CustomerBuilding)
|
@Column({ type: DataType.STRING(32), field: "type_code", comment: "探测器类型编码(typeCode)" })
|
||||||
@Column({ type: DataType.BIGINT, field: "building_id", comment: "所属建筑 ID(→ customer_building)" })
|
typeCode!: string;
|
||||||
buildingId!: number;
|
|
||||||
|
|
||||||
@Column({ type: DataType.STRING(16), field: "floor_no", comment: "所在楼层" })
|
@Column({ type: DataType.STRING(64), field: "type_name", comment: "探测器类型名称(typeName)" })
|
||||||
floorNo!: string;
|
typeName!: string;
|
||||||
|
|
||||||
@Column({ type: DataType.STRING(256), comment: "所在位置" })
|
@Column({ type: DataType.STRING(64), field: "fire_system_code", comment: "消防系统编码(fireSystemCode)" })
|
||||||
location!: string;
|
fireSystemCode!: string;
|
||||||
|
|
||||||
@Column({ type: DataType.SMALLINT, allowNull: false, defaultValue: 1, comment: "状态:1在线 0离线" })
|
@Column({ type: DataType.STRING(128), field: "fire_system_name", comment: "消防系统名称(fireSystemName)" })
|
||||||
|
fireSystemName!: string;
|
||||||
|
|
||||||
|
@Column({ type: DataType.STRING(64), field: "part_unit_loop_code", comment: "部件/单元/回路编码(partUnitLoopCode)" })
|
||||||
|
partUnitLoopCode!: string;
|
||||||
|
|
||||||
|
@Column({ type: DataType.STRING(64), field: "channel_no", comment: "通道号(channelNo)" })
|
||||||
|
channelNo!: string;
|
||||||
|
|
||||||
|
@Column({ type: DataType.STRING(64), field: "channel_sn", comment: "通道序列号(channelSn)" })
|
||||||
|
channelSn!: string;
|
||||||
|
|
||||||
|
@Column({ type: DataType.STRING(16), field: "produce_date", comment: "生产日期(produceDate)" })
|
||||||
|
produceDate!: string;
|
||||||
|
|
||||||
|
@Column({ type: DataType.SMALLINT, allowNull: false, defaultValue: 1, field: "status", comment: "上报状态:1在线 0离线(detectorStatus 归一)" })
|
||||||
status!: number;
|
status!: number;
|
||||||
|
|
||||||
@Column({ type: DataType.DATE, field: "synced_at", comment: "ICC 同步时间" })
|
@Column({ type: DataType.DATE, field: "synced_at", comment: "ICC 同步时间" })
|
||||||
@@ -53,12 +72,12 @@ export default class ElectricLine extends Model {
|
|||||||
@BelongsTo(() => CustomerEquipment, { foreignKey: "equipId", as: "equipment" })
|
@BelongsTo(() => CustomerEquipment, { foreignKey: "equipId", as: "equipment" })
|
||||||
equipment!: CustomerEquipment;
|
equipment!: CustomerEquipment;
|
||||||
|
|
||||||
|
@BelongsTo(() => ElectricGateway, { foreignKey: "gatewayId", as: "gateway" })
|
||||||
|
gateway!: ElectricGateway;
|
||||||
|
|
||||||
@BelongsTo(() => Customer, { foreignKey: "customerId", as: "customer" })
|
@BelongsTo(() => Customer, { foreignKey: "customerId", as: "customer" })
|
||||||
customer!: Customer;
|
customer!: Customer;
|
||||||
|
|
||||||
@BelongsTo(() => CustomerBuilding, { foreignKey: "buildingId", as: "building" })
|
|
||||||
building!: CustomerBuilding;
|
|
||||||
|
|
||||||
@CreatedAt
|
@CreatedAt
|
||||||
created_at!: Date;
|
created_at!: Date;
|
||||||
}
|
}
|
||||||
@@ -4,9 +4,9 @@ import { Repository } from "sequelize-typescript";
|
|||||||
import { Op } from "sequelize";
|
import { Op } from "sequelize";
|
||||||
import { Context } from "@midwayjs/koa";
|
import { Context } from "@midwayjs/koa";
|
||||||
import ElectricLine from "../entity/electric_line.entity";
|
import ElectricLine from "../entity/electric_line.entity";
|
||||||
|
import ElectricGateway from "../entity/electric_gateway.entity";
|
||||||
import Customer from "../entity/customer.entity";
|
import Customer from "../entity/customer.entity";
|
||||||
import CustomerEquipment from "../entity/customer_equipment.entity";
|
import CustomerEquipment from "../entity/customer_equipment.entity";
|
||||||
import CustomerBuilding from "../entity/customer_building.entity";
|
|
||||||
import SysUserRole from "../entity/sysUserRole.entity";
|
import SysUserRole from "../entity/sysUserRole.entity";
|
||||||
import SysRole from "../entity/sysRole.entity";
|
import SysRole from "../entity/sysRole.entity";
|
||||||
|
|
||||||
@@ -21,8 +21,8 @@ export class ElectricLineService {
|
|||||||
@InjectRepository(CustomerEquipment)
|
@InjectRepository(CustomerEquipment)
|
||||||
equipmentModel!: Repository<CustomerEquipment>;
|
equipmentModel!: Repository<CustomerEquipment>;
|
||||||
|
|
||||||
@InjectRepository(CustomerBuilding)
|
@InjectRepository(ElectricGateway)
|
||||||
buildingModel!: Repository<CustomerBuilding>;
|
gatewayModel!: Repository<ElectricGateway>;
|
||||||
|
|
||||||
@InjectRepository(SysUserRole)
|
@InjectRepository(SysUserRole)
|
||||||
userRoleModel!: Repository<SysUserRole>;
|
userRoleModel!: Repository<SysUserRole>;
|
||||||
@@ -44,12 +44,12 @@ export class ElectricLineService {
|
|||||||
if (query.keyword) {
|
if (query.keyword) {
|
||||||
where[Op.or] = [
|
where[Op.or] = [
|
||||||
{ lineName: { [Op.like]: `%${query.keyword}%` } },
|
{ lineName: { [Op.like]: `%${query.keyword}%` } },
|
||||||
{ lineCode: { [Op.like]: `%${query.keyword}%` } }
|
{ lineCode: { [Op.like]: `%${query.keyword}%` } },
|
||||||
|
{ imei: { [Op.like]: `%${query.keyword}%` } }
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
if (query.customerId !== undefined && query.customerId !== "") where.customerId = Number(query.customerId);
|
if (query.customerId !== undefined && query.customerId !== "") where.customerId = Number(query.customerId);
|
||||||
if (query.equipId !== undefined && query.equipId !== "") where.equipId = Number(query.equipId);
|
if (query.equipId !== undefined && query.equipId !== "") where.equipId = Number(query.equipId);
|
||||||
if (query.buildingId !== undefined && query.buildingId !== "") where.buildingId = Number(query.buildingId);
|
|
||||||
if (query.status !== undefined && query.status !== "" && query.status !== null) where.status = Number(query.status);
|
if (query.status !== undefined && query.status !== "" && query.status !== null) where.status = Number(query.status);
|
||||||
|
|
||||||
// 非管理员按客户归属自动限定数据范围
|
// 非管理员按客户归属自动限定数据范围
|
||||||
@@ -95,15 +95,15 @@ export class ElectricLineService {
|
|||||||
return { code: 200, message: "获取线路台账详情成功", data: decorated[0] };
|
return { code: 200, message: "获取线路台账详情成功", data: decorated[0] };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 补全客户/设备/建筑展示信息,避免 N+1。 */
|
/** 补全客户/设备/网关展示信息,避免 N+1。 */
|
||||||
private async decorate(rows: ElectricLine[]): Promise<any[]> {
|
private async decorate(rows: ElectricLine[]): Promise<any[]> {
|
||||||
if (rows.length === 0) return rows as any;
|
if (rows.length === 0) return rows as any;
|
||||||
|
|
||||||
const customerIds = [...new Set(rows.map((r) => r.customerId).filter(Boolean))];
|
const customerIds = [...new Set(rows.map((r) => r.customerId).filter(Boolean))];
|
||||||
const equipIds = [...new Set(rows.map((r) => r.equipId).filter(Boolean))];
|
const equipIds = [...new Set(rows.map((r) => r.equipId).filter(Boolean))];
|
||||||
const buildingIds = [...new Set(rows.map((r) => r.buildingId).filter(Boolean))];
|
const gatewayIds = [...new Set(rows.map((r) => r.gatewayId).filter(Boolean))];
|
||||||
|
|
||||||
const [customers, equips, buildings] = await Promise.all([
|
const [customers, equips, gateways] = await Promise.all([
|
||||||
customerIds.length
|
customerIds.length
|
||||||
? this.customerModel.findAll({
|
? this.customerModel.findAll({
|
||||||
where: { id: { [Op.in]: customerIds } },
|
where: { id: { [Op.in]: customerIds } },
|
||||||
@@ -116,28 +116,29 @@ export class ElectricLineService {
|
|||||||
attributes: ["id", "equipCode", "equipName"]
|
attributes: ["id", "equipCode", "equipName"]
|
||||||
})
|
})
|
||||||
: Promise.resolve([]),
|
: Promise.resolve([]),
|
||||||
buildingIds.length
|
gatewayIds.length
|
||||||
? this.buildingModel.findAll({
|
? this.gatewayModel.findAll({
|
||||||
where: { id: { [Op.in]: buildingIds } },
|
where: { id: { [Op.in]: gatewayIds } },
|
||||||
attributes: ["id", "buildingName"]
|
attributes: ["id", "deviceCode", "deviceName"]
|
||||||
})
|
})
|
||||||
: Promise.resolve([])
|
: Promise.resolve([])
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const customerMap = new Map(customers.map((c) => [c.id, c]));
|
const customerMap = new Map(customers.map((c) => [c.id, c]));
|
||||||
const equipMap = new Map(equips.map((e) => [e.id, e]));
|
const equipMap = new Map(equips.map((e) => [e.id, e]));
|
||||||
const buildingMap = new Map(buildings.map((b) => [b.id, b]));
|
const gatewayMap = new Map(gateways.map((g) => [g.id, g]));
|
||||||
|
|
||||||
return rows.map((row) => {
|
return rows.map((row) => {
|
||||||
const json: any = row.toJSON();
|
const json: any = row.toJSON();
|
||||||
const customer: any = customerMap.get(row.customerId);
|
const customer: any = customerMap.get(row.customerId);
|
||||||
const equip: any = equipMap.get(row.equipId);
|
const equip: any = row.equipId ? equipMap.get(row.equipId) : undefined;
|
||||||
const building: any = row.buildingId ? buildingMap.get(row.buildingId) : undefined;
|
const gateway: any = row.gatewayId ? gatewayMap.get(row.gatewayId) : undefined;
|
||||||
json.customerNo = customer?.customerNo || "";
|
json.customerNo = customer?.customerNo || "";
|
||||||
json.customerName = customer?.customerName || "";
|
json.customerName = customer?.customerName || "";
|
||||||
json.equipCode = equip?.equipCode || "";
|
json.equipCode = equip?.equipCode || "";
|
||||||
json.equipName = equip?.equipName || "";
|
json.equipName = equip?.equipName || "";
|
||||||
json.buildingName = building?.buildingName || "";
|
json.gatewayCode = gateway?.deviceCode || "";
|
||||||
|
json.gatewayName = gateway?.deviceName || "";
|
||||||
return json;
|
return json;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,31 +6,41 @@ import { Context } from "@midwayjs/koa";
|
|||||||
import ElectricLine from "../entity/electric_line.entity";
|
import ElectricLine from "../entity/electric_line.entity";
|
||||||
import IccSyncLog from "../entity/icc_sync_log.entity";
|
import IccSyncLog from "../entity/icc_sync_log.entity";
|
||||||
import Customer from "../entity/customer.entity";
|
import Customer from "../entity/customer.entity";
|
||||||
import CustomerEquipment from "../entity/customer_equipment.entity";
|
import ElectricGateway from "../entity/electric_gateway.entity";
|
||||||
import CustomerBuilding from "../entity/customer_building.entity";
|
import { getIccAccessToken, iccFetch } from "../task/icc_customer_auth";
|
||||||
import { ICC_LINE_ENDPOINT, mapIccLineRecord, IccLineRawRecord } from "../task/icc_line_mapper";
|
import { ICC_LINE_ENDPOINT, mapIccLineRecord, parseLinePage, IccLineRawRecord } from "../task/icc_line_mapper";
|
||||||
|
|
||||||
export interface IccLineSyncResult {
|
export interface IccLineSyncResult {
|
||||||
/** ICC 拉取到的原始条数 */
|
/** ICC 拉取到的原始探测器条数 */
|
||||||
pulled: number;
|
pulled: number;
|
||||||
/** 解析落库的线路条数(成功 upsert,含新增/更新) */
|
/** 成功落库的线路条数(新增+更新) */
|
||||||
synced: number;
|
synced: number;
|
||||||
/** 新增的线路条数 */
|
/** 新增的线路条数 */
|
||||||
inserted: number;
|
inserted: number;
|
||||||
/** 更新的线路条数 */
|
/** 更新的线路条数 */
|
||||||
updated: number;
|
updated: number;
|
||||||
/** 因缺少合法 line_code 或无法匹配客户/设备而跳过的条数 */
|
/** 跳过的条数(缺 lineCode / orgCode 匹配不到客户) */
|
||||||
skipped: number;
|
skipped: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SYNC_TYPE = "electric_line";
|
const SYNC_TYPE = "electric_line";
|
||||||
|
/** 分页拉取参数(ICC 探测器分页接口约定,短页判定阈值) */
|
||||||
|
const PAGE_SIZE = 50;
|
||||||
|
/** 最大分页数保护,防止异常接口导致无限拉取 */
|
||||||
|
const MAX_PAGES = 100;
|
||||||
|
/** ICC_VERSION 缺省值(与 icc_customer_auth 保持一致) */
|
||||||
|
const DEFAULT_VERSION = "1.0.0";
|
||||||
|
/** HTTP 超时 60s(项目约定) */
|
||||||
|
const HTTP_TIMEOUT_MS = 60000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ICC 智电改造线路拉取同步:
|
* ICC 智电改造线路(消防探测器)拉取同步:
|
||||||
* 1) 从 .env 读取 ICC_BASE_URL / ICC_TOKEN 占位配置
|
* 1) 复用 icc_customer_auth 的真实 OAuth2 密码模式鉴权(getIccAccessToken / iccFetch),
|
||||||
* 2) 请求 ICC 线路列表(路径与字段映射集中在 icc_line_mapper)
|
* 从 .env 读取 ICC_BASE_URL / ICC_VERSION
|
||||||
* 3) 按 line_code 幂等 upsert 写入 electric_line,更新 synced_at
|
* 2) POST 分页请求 ICC 探测器列表(/detector/subsystem/page,pageNum/pageSize=50,全量拉取)
|
||||||
* 4) 每次同步写一条 icc_sync_log;ICC 拉取失败仅记录 error,不抛业务错误
|
* 3) 预取 orgCode→customer.id 映射(customer.icc_code)与 parentCode→gateway.id 映射(electric_gateway.device_code),避免 N+1 查询
|
||||||
|
* 4) 按 line_code(detectorCode)幂等 upsert 写入 electric_line,更新 synced_at
|
||||||
|
* 5) 每次同步写一条 icc_sync_log;拉取失败仅记录 error,不抛业务错误
|
||||||
*/
|
*/
|
||||||
@Provide()
|
@Provide()
|
||||||
export class IccLineSyncService {
|
export class IccLineSyncService {
|
||||||
@@ -43,52 +53,55 @@ export class IccLineSyncService {
|
|||||||
@InjectRepository(Customer)
|
@InjectRepository(Customer)
|
||||||
customerModel!: Repository<Customer>;
|
customerModel!: Repository<Customer>;
|
||||||
|
|
||||||
@InjectRepository(CustomerEquipment)
|
@InjectRepository(ElectricGateway)
|
||||||
equipmentModel!: Repository<CustomerEquipment>;
|
gatewayModel!: Repository<ElectricGateway>;
|
||||||
|
|
||||||
@InjectRepository(CustomerBuilding)
|
|
||||||
buildingModel!: Repository<CustomerBuilding>;
|
|
||||||
|
|
||||||
async syncFromIcc(ctx?: Context): Promise<IccLineSyncResult> {
|
async syncFromIcc(ctx?: Context): Promise<IccLineSyncResult> {
|
||||||
const startTime = new Date();
|
const startTime = new Date();
|
||||||
const baseUrl = process.env.ICC_BASE_URL?.trim();
|
const baseUrl = process.env.ICC_BASE_URL?.trim();
|
||||||
const token = process.env.ICC_TOKEN?.trim();
|
|
||||||
|
|
||||||
const result: IccLineSyncResult = { pulled: 0, synced: 0, inserted: 0, updated: 0, skipped: 0 };
|
const result: IccLineSyncResult = { pulled: 0, synced: 0, inserted: 0, updated: 0, skipped: 0 };
|
||||||
|
|
||||||
// 占位配置缺失视为拉取失败:记录日志,不抛业务错误
|
// 基础配置缺失视为拉取失败:记录日志,不抛业务错误
|
||||||
if (!baseUrl) {
|
if (!baseUrl) {
|
||||||
await this.writeLog(startTime, 0, SYNC_TYPE, 0, "未配置 ICC_BASE_URL,请检查 server/.env");
|
await this.writeLog(startTime, 0, SYNC_TYPE, 0, "未配置 ICC_BASE_URL,请检查 server/.env");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = `${baseUrl.replace(/\/$/, "")}${ICC_LINE_ENDPOINT}`;
|
const token = await getIccAccessToken();
|
||||||
const headers: Record<string, string> = { Accept: "application/json" };
|
const url = `${baseUrl.replace(/\/$/, "")}${ICC_LINE_ENDPOINT.replace("{version}", process.env.ICC_VERSION?.trim() || DEFAULT_VERSION)}`;
|
||||||
if (token) headers.Authorization = `Bearer ${token}`;
|
|
||||||
|
|
||||||
const res = await fetch(url, {
|
const allRaw: IccLineRawRecord[] = [];
|
||||||
method: "GET",
|
let pageNum = 1;
|
||||||
headers,
|
|
||||||
signal: AbortSignal.timeout(60000) // 按项目约定 HTTP 超时 60s
|
while (pageNum <= MAX_PAGES) {
|
||||||
|
const res = await iccFetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
Authorization: `bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ pageNum, pageSize: PAGE_SIZE }), // 不传额外筛选条件,全量拉取
|
||||||
|
signal: AbortSignal.timeout(HTTP_TIMEOUT_MS)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`ICC 线路接口返回异常:HTTP ${res.status}`);
|
throw new Error(`ICC 探测器接口返回异常:HTTP ${res.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload: any = await res.json();
|
const payload: any = await res.json();
|
||||||
// 兼容缓存工具包裹(data/list 字段)返回格式
|
const page = parseLinePage(payload);
|
||||||
const rawList: IccLineRawRecord[] = Array.isArray(payload)
|
allRaw.push(...page.list);
|
||||||
? payload
|
result.pulled += page.list.length;
|
||||||
: Array.isArray(payload.data)
|
|
||||||
? payload.data
|
|
||||||
: Array.isArray(payload.list)
|
|
||||||
? payload.list
|
|
||||||
: [];
|
|
||||||
result.pulled = rawList.length;
|
|
||||||
|
|
||||||
const { inserted, updated, skipped } = await this.batchUpsert(rawList);
|
// 短页判定:本页不满 pageSize 视为无下一页;另有最大页数保护
|
||||||
|
if (page.list.length < PAGE_SIZE) break;
|
||||||
|
pageNum++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { inserted, updated, skipped } = await this.batchUpsert(allRaw);
|
||||||
result.inserted = inserted;
|
result.inserted = inserted;
|
||||||
result.updated = updated;
|
result.updated = updated;
|
||||||
result.synced = inserted + updated;
|
result.synced = inserted + updated;
|
||||||
@@ -97,15 +110,13 @@ export class IccLineSyncService {
|
|||||||
await this.writeLog(startTime, 1, SYNC_TYPE, result.synced, null);
|
await this.writeLog(startTime, 1, SYNC_TYPE, result.synced, null);
|
||||||
return result;
|
return result;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const message = err?.code === "ERR_MODULE_NOT_FOUND" || err?.message
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
? String(err?.message || err)
|
|
||||||
: String(err);
|
|
||||||
await this.writeLog(startTime, 0, SYNC_TYPE, result.pulled, message);
|
await this.writeLog(startTime, 0, SYNC_TYPE, result.pulled, message);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 批量按 line_code upsert(幂等),并做客户/设备/建筑外键反查。 */
|
/** 按 line_code(detectorCode)幂等 upsert,customer_id 解析失败的行跳过。 */
|
||||||
private async batchUpsert(records: IccLineRawRecord[]): Promise<{ inserted: number; updated: number; skipped: number }> {
|
private async batchUpsert(records: IccLineRawRecord[]): Promise<{ inserted: number; updated: number; skipped: number }> {
|
||||||
let inserted = 0;
|
let inserted = 0;
|
||||||
let updated = 0;
|
let updated = 0;
|
||||||
@@ -113,71 +124,66 @@ export class IccLineSyncService {
|
|||||||
|
|
||||||
if (records.length === 0) return { inserted, updated, skipped };
|
if (records.length === 0) return { inserted, updated, skipped };
|
||||||
|
|
||||||
// 预取外键映射,避免 N+1
|
// 预取 orgCode → customer.id 映射,避免 N+1 查询客户
|
||||||
const equipCodes = [...new Set(records.map((r) => mapIccLineRecord(r).equipCode).filter(Boolean))];
|
const orgCodes = [...new Set(records.map((r) => mapIccLineRecord(r).orgCode).filter(Boolean))];
|
||||||
const customerNos = [...new Set(records.map((r) => mapIccLineRecord(r).customerNo).filter(Boolean))];
|
const customers = orgCodes.length
|
||||||
const buildingNames = [...new Set(records.map((r) => mapIccLineRecord(r).buildingName).filter(Boolean))];
|
? await this.customerModel.findAll({ where: { iccCode: { [Op.in]: orgCodes } }, attributes: ["id", "iccCode"] })
|
||||||
|
: [];
|
||||||
|
const customerIdByOrgCode = new Map(customers.map((c) => [c.iccCode, c.id]));
|
||||||
|
|
||||||
const [equips, customers, buildings] = await Promise.all([
|
// 预取 parentCode → gateway.id 映射(electric_gateway.device_code),避免 N+1 查询网关
|
||||||
equipCodes.length
|
const parentCodes = [...new Set(records.map((r) => mapIccLineRecord(r).parentCode).filter(Boolean))];
|
||||||
? this.equipmentModel.findAll({ where: { equipCode: { [Op.in]: equipCodes } }, attributes: ["id", "equipCode", "customerId", "buildingId"] })
|
const gateways = parentCodes.length
|
||||||
: Promise.resolve([]),
|
? await this.gatewayModel.findAll({ where: { deviceCode: { [Op.in]: parentCodes } }, attributes: ["id", "deviceCode"] })
|
||||||
customerNos.length
|
: [];
|
||||||
? this.customerModel.findAll({ where: { customerNo: { [Op.in]: customerNos } }, attributes: ["id", "customerNo"] })
|
const gatewayIdByParentCode = new Map(gateways.map((g) => [g.deviceCode, g.id]));
|
||||||
: Promise.resolve([]),
|
|
||||||
buildingNames.length
|
|
||||||
? this.buildingModel.findAll({ where: { buildingName: { [Op.in]: buildingNames } }, attributes: ["id", "buildingName", "customerId"] })
|
|
||||||
: Promise.resolve([])
|
|
||||||
]);
|
|
||||||
|
|
||||||
const equipByCode = new Map(equips.map((e) => [e.equipCode, e]));
|
|
||||||
const customerByNo = new Map(customers.map((c) => [c.customerNo, c]));
|
|
||||||
const buildingByName = new Map(buildings.map((b) => [b.buildingName, b]));
|
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
for (const raw of records) {
|
for (const raw of records) {
|
||||||
const mapped = mapIccLineRecord(raw);
|
const mapped = mapIccLineRecord(raw);
|
||||||
|
// line_code(detectorCode)为空则无同步匹配键,跳过
|
||||||
if (!mapped.lineCode) {
|
if (!mapped.lineCode) {
|
||||||
skipped++;
|
skipped++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// customer_id NOT NULL:orgCode 必须能解析到客户,否则跳过
|
||||||
const equip = mapped.equipCode ? equipByCode.get(mapped.equipCode) : undefined;
|
if (!mapped.orgCode) {
|
||||||
// equip_id 与 customer_id 为 NOT NULL,无法解析时跳过该行
|
skipped++;
|
||||||
if (!equip) {
|
continue;
|
||||||
|
}
|
||||||
|
const customerId = customerIdByOrgCode.get(mapped.orgCode);
|
||||||
|
if (!customerId) {
|
||||||
skipped++;
|
skipped++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let customerId: number = equip.customerId;
|
// gateway_id 可空:parentCode 匹配不到 electric_gateway.device_code 时置空
|
||||||
if (mapped.customerNo) {
|
const gatewayId = mapped.parentCode ? (gatewayIdByParentCode.get(mapped.parentCode) ?? null) : null;
|
||||||
const byNo = customerByNo.get(mapped.customerNo);
|
|
||||||
if (byNo) customerId = byNo.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
let buildingId: number | null = equip.buildingId || null;
|
|
||||||
if (mapped.buildingName) {
|
|
||||||
const building = buildingByName.get(mapped.buildingName);
|
|
||||||
if (building) buildingId = building.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
lineCode: mapped.lineCode,
|
lineCode: mapped.lineCode,
|
||||||
equipId: equip.id,
|
|
||||||
customerId,
|
|
||||||
buildingId,
|
|
||||||
iccDeviceCode: mapped.iccDeviceCode || null,
|
|
||||||
lineName: mapped.lineName || null,
|
lineName: mapped.lineName || null,
|
||||||
floorNo: mapped.floorNo || null,
|
imei: mapped.imei || null,
|
||||||
location: mapped.location || null,
|
// equipId 同步不填充
|
||||||
|
gatewayId,
|
||||||
|
customerId,
|
||||||
|
typeCode: mapped.typeCode || null,
|
||||||
|
typeName: mapped.typeName || null,
|
||||||
|
fireSystemCode: mapped.fireSystemCode || null,
|
||||||
|
fireSystemName: mapped.fireSystemName || null,
|
||||||
|
partUnitLoopCode: mapped.partUnitLoopCode || null,
|
||||||
|
channelNo: mapped.channelNo || null,
|
||||||
|
channelSn: mapped.channelSn || null,
|
||||||
|
produceDate: mapped.produceDate || null,
|
||||||
status: mapped.status,
|
status: mapped.status,
|
||||||
syncedAt: now
|
syncedAt: now
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
|
// 幂等 upsert:按 line_code 唯一键匹配
|
||||||
const existing = await this.lineModel.findOne({ where: { lineCode: mapped.lineCode } });
|
const existing = await this.lineModel.findOne({ where: { lineCode: mapped.lineCode } });
|
||||||
if (existing) {
|
if (existing) {
|
||||||
await existing.update(payload);
|
await existing.update({ ...payload, syncedAt: now });
|
||||||
updated++;
|
updated++;
|
||||||
} else {
|
} else {
|
||||||
await this.lineModel.create(payload);
|
await this.lineModel.create(payload);
|
||||||
|
|||||||
@@ -1,51 +1,109 @@
|
|||||||
/**
|
/**
|
||||||
* ICC 线路数据字段映射(集中配置,便于按 ICC 平台实际返回结构调整)
|
* ICC 智电改造线路(消防探测器)数据字段映射(集中配置,便于按 ICC 平台实际返回结构调整)。
|
||||||
*
|
*
|
||||||
* ICC 返回的原始字段名 → 本系统 electric_line 字段名。当前为占位映射,
|
* ICC 返回的原始字段名 → 本系统 electric_line 字段。接入真实 ICC 平台时
|
||||||
* 接入真实 ICC 平台时仅需调整本文件与 fetch 路径即可,业务层无需改动。
|
* 仅需调整本文件与 fetch 路径即可,业务层无需改动。纯映射逻辑,无 SQL 无 DOM。
|
||||||
*/
|
*/
|
||||||
export interface IccLineRawRecord {
|
export interface IccLineRawRecord {
|
||||||
[field: string]: any;
|
[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 = {
|
export const ICC_LINE_FIELDS = {
|
||||||
lineCode: "lineCode", // ICC 线路编码(唯一键)
|
lineCode: "detectorCode", // 探测器编码(唯一幂等键)
|
||||||
iccDeviceCode: "deviceCode", // ICC 智电设备编号
|
lineName: "detectorName", // 探测器名称
|
||||||
equipCode: "equipCode", // 关联的客户设备编码(用于反查 equip_id)
|
imei: "imei", // 探测器 IMEI
|
||||||
lineName: "lineName", // 线路名称
|
orgCode: "orgCode", // 所属组织编码(→ customer.icc_code 反查客户,不落库)
|
||||||
customerNo: "customerNo", // 关联客户编号(用于反查 customer_id)
|
parentCode: "parentCode", // 上级网关编码(→ electric_gateway.device_code 反查网关,不落库)
|
||||||
buildingName: "buildingName", // 建筑名称(占位,反查 building_id)
|
typeCode: "typeCode", // 探测器类型编码
|
||||||
floorNo: "floorNo", // 所在楼层
|
typeName: "typeName", // 探测器类型名称
|
||||||
location: "location", // 所在位置
|
fireSystemCode: "fireSystemCode", // 消防系统编码
|
||||||
status: "status" // 1在线 0离线
|
fireSystemName: "fireSystemName", // 消防系统名称
|
||||||
|
partUnitLoopCode: "partUnitLoopCode", // 部件/单元/回路编码
|
||||||
|
channelNo: "channelNo", // 通道号
|
||||||
|
channelSn: "channelSn", // 通道序列号
|
||||||
|
produceDate: "produceDate", // 生产日期
|
||||||
|
detectorStatus: "detectorStatus" // 上报状态 01=在线 02=离线
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/** 渲染列表路径与 token 鉴权头(占位,接入真实平台时配置 .env)。 */
|
/** 离线标记集合:命中即归一化为离线(0),其余(含空)均视为在线(1) */
|
||||||
export const ICC_LINE_ENDPOINT = "/api/grid/lines";
|
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): {
|
export function mapIccLineRecord(raw: IccLineRawRecord): {
|
||||||
lineCode: string;
|
lineCode: string;
|
||||||
iccDeviceCode: string;
|
|
||||||
equipCode?: string;
|
|
||||||
lineName?: string;
|
lineName?: string;
|
||||||
customerNo?: string;
|
imei?: string;
|
||||||
buildingName?: string;
|
orgCode?: string;
|
||||||
floorNo?: string;
|
parentCode?: string;
|
||||||
location?: string;
|
typeCode?: string;
|
||||||
|
typeName?: string;
|
||||||
|
fireSystemCode?: string;
|
||||||
|
fireSystemName?: string;
|
||||||
|
partUnitLoopCode?: string;
|
||||||
|
channelNo?: string;
|
||||||
|
channelSn?: string;
|
||||||
|
produceDate?: string;
|
||||||
status: number;
|
status: number;
|
||||||
} {
|
} {
|
||||||
const f = ICC_LINE_FIELDS;
|
const f = ICC_LINE_FIELDS;
|
||||||
return {
|
const result: Record<string, any> = {
|
||||||
lineCode: String(raw[f.lineCode] ?? "").trim(),
|
// 匹配键保持空串语义,由调用方判跳
|
||||||
iccDeviceCode: String(raw[f.iccDeviceCode] ?? "").trim(),
|
lineCode: String(raw[f.lineCode] ?? "").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
|
|
||||||
};
|
};
|
||||||
|
// 可选字符串字段
|
||||||
|
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: [] };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -15,3 +15,26 @@
|
|||||||
| <br /> | <br /> | <br /> | <br /> |
|
| <br /> | <br /> | <br /> | <br /> |
|
||||||
| <br /> | <br /> | <br /> | <br /> |
|
| <br /> | <br /> | <br /> | <br /> |
|
||||||
|
|
||||||
|
智电改造线路表:electric_line(ICC探测器同步)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 键 | 说明 |
|
||||||
|
| :------------------ | :----------- | :----- | :------------------------------------------------------- |
|
||||||
|
| id | BIGSERIAL | PK | 自增主键 |
|
||||||
|
| line_code | VARCHAR(64) | UNIQUE | 探测器编码(ICC detectorCode,唯一幂等键) |
|
||||||
|
| equip_id | BIGINT | FK | 客户设备 ID(→ customer_equipment,可空,同步不填充) |
|
||||||
|
| gateway_id | BIGINT | FK | 网关设备 ID(→ electric_gateway,由 parentCode↔device_code 匹配) |
|
||||||
|
| customer_id | BIGINT | FK | 客户 ID(orgCode→customer.icc_code 解析,逻辑隔离键) |
|
||||||
|
| line_name | VARCHAR(128) | | 探测器名称(ICC detectorName) |
|
||||||
|
| imei | VARCHAR(64) | | 设备唯一标识码(ICC imei) |
|
||||||
|
| type_code | VARCHAR(32) | | 探测器类型编码 |
|
||||||
|
| type_name | VARCHAR(64) | | 探测器类型名称 |
|
||||||
|
| fire_system_code | VARCHAR(64) | | 消防系统编码 |
|
||||||
|
| fire_system_name | VARCHAR(128) | | 消防系统名称 |
|
||||||
|
| part_unit_loop_code | VARCHAR(64) | | 探测器回路号 |
|
||||||
|
| channel_no | VARCHAR(64) | | 探测器通道号 |
|
||||||
|
| channel_sn | VARCHAR(64) | | 通道唯一标识码 |
|
||||||
|
| produce_date | VARCHAR(16) | | 生产日期(yyyy-MM-dd) |
|
||||||
|
| status | SMALLINT | | 探测器状态:1在线 0离线(detectorStatus 01→1 / 02→0) |
|
||||||
|
| synced_at | TIMESTAMPTZ | | ICC 同步时间 |
|
||||||
|
| created_at | TIMESTAMPTZ | | 创建时间 |
|
||||||
|
|
||||||
Reference in new issue
Block a user