Files
smart_grid_iot/server/src/service/icc_gateway_sync.service.ts
T

199 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Provide } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import { Repository } from "sequelize-typescript";
import { Op } from "sequelize";
import { Context } from "@midwayjs/koa";
import ElectricGateway from "../entity/electric_gateway.entity";
import IccSyncLog from "../entity/icc_sync_log.entity";
import Customer from "../entity/customer.entity";
import { getIccAccessToken, iccFetch } from "../task/icc_customer_auth";
import { ICC_GATEWAY_ENDPOINT, mapIccGatewayRecord, parseGatewayPage, IccGatewayRawRecord } from "../task/icc_gateway_mapper";
export interface IccGatewaySyncResult {
/** ICC 拉取到的原始设备条数 */
pulled: number;
/** 成功落库的设备条数(新增+更新) */
synced: number;
/** 新增的设备条数 */
inserted: number;
/** 更新的设备条数 */
updated: number;
/** 跳过的条数(缺 imei/deviceCode / orgCode 匹配不到客户) */
skipped: number;
}
const SYNC_TYPE = "electric_gateway";
/** 分页拉取参数(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 智电改造网关设备拉取同步:
* 1) 复用 icc_customer_auth 的真实 OAuth2 密码模式鉴权(getIccAccessToken / iccFetch),
* 从 .env 读取 ICC_BASE_URL / ICC_VERSION
* 2) POST 分页请求 ICC 设备列表(/device/subsystem/page,pageNum/pageSize=50,全量拉取)
* 3) 预取 orgCode→customer.id 映射(customer.icc_code),避免 N+1 查询客户
* 4) 按 imei/deviceCode 幂等 upsert 写入 electric_gateway,更新 synced_at
* 5) 每次同步写一条 icc_sync_log;拉取失败仅记录 error,不抛业务错误
*/
@Provide()
export class IccGatewaySyncService {
@InjectRepository(ElectricGateway)
gatewayModel!: Repository<ElectricGateway>;
@InjectRepository(IccSyncLog)
syncLogModel!: Repository<IccSyncLog>;
@InjectRepository(Customer)
customerModel!: Repository<Customer>;
async syncFromIcc(ctx?: Context): Promise<IccGatewaySyncResult> {
const startTime = new Date();
const baseUrl = process.env.ICC_BASE_URL?.trim();
const result: IccGatewaySyncResult = { pulled: 0, synced: 0, inserted: 0, updated: 0, skipped: 0 };
// 基础配置缺失视为拉取失败:记录日志,不抛业务错误
if (!baseUrl) {
await this.writeLog(startTime, 0, SYNC_TYPE, 0, "未配置 ICC_BASE_URL,请检查 server/.env");
return result;
}
try {
const token = await getIccAccessToken();
const url = `${baseUrl.replace(/\/$/, "")}${ICC_GATEWAY_ENDPOINT.replace("{version}", process.env.ICC_VERSION?.trim() || DEFAULT_VERSION)}`;
const allRaw: IccGatewayRawRecord[] = [];
let pageNum = 1;
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 }), // 不传 typeCode,全量拉取
signal: AbortSignal.timeout(HTTP_TIMEOUT_MS)
});
if (!res.ok) {
throw new Error(`ICC 网关设备接口返回异常:HTTP ${res.status}`);
}
const payload: any = await res.json();
const page = parseGatewayPage(payload);
allRaw.push(...page.list);
result.pulled += page.list.length;
// 短页判定:本页不满 pageSize 视为无下一页;另有最大页数保护
if (page.list.length < PAGE_SIZE) break;
pageNum++;
}
const { inserted, updated, skipped } = await this.batchUpsert(allRaw);
result.inserted = inserted;
result.updated = updated;
result.synced = inserted + updated;
result.skipped = skipped;
await this.writeLog(startTime, 1, SYNC_TYPE, result.synced, null);
return result;
} catch (err: any) {
const message = err instanceof Error ? err.message : String(err);
await this.writeLog(startTime, 0, SYNC_TYPE, result.pulled, message);
return result;
}
}
/** 按 imei/deviceCode 幂等 upsert(幂等),customer_id 解析失败的行跳过。 */
private async batchUpsert(records: IccGatewayRawRecord[]): Promise<{ inserted: number; updated: number; skipped: number }> {
let inserted = 0;
let updated = 0;
let skipped = 0;
if (records.length === 0) return { inserted, updated, skipped };
// 预取 orgCode → customer.id 映射,避免 N+1 查询客户
const orgCodes = [...new Set(records.map((r) => mapIccGatewayRecord(r).orgCode).filter(Boolean))];
const customers = orgCodes.length
? await this.customerModel.findAll({ where: { iccCode: { [Op.in]: orgCodes } }, attributes: ["id", "iccCode"] })
: [];
const customerIdByOrgCode = new Map(customers.map((c) => [c.iccCode, c.id]));
const now = new Date();
for (const raw of records) {
const mapped = mapIccGatewayRecord(raw);
// imei 与 deviceCode 都为空则无同步匹配键,跳过
if (!mapped.imei && !mapped.deviceCode) {
skipped++;
continue;
}
// customer_id NOT NULL:orgCode 必须能解析到客户,否则跳过
if (!mapped.orgCode) {
skipped++;
continue;
}
const customerId = customerIdByOrgCode.get(mapped.orgCode);
if (!customerId) {
skipped++;
continue;
}
const payload = {
iccImei: mapped.imei || null,
deviceCode: mapped.deviceCode || null,
deviceName: mapped.deviceName || null,
customerId,
orgCode: mapped.orgCode || null,
orgName: mapped.orgName || null,
manufacturerCode: mapped.manufacturerCode || null,
manufacturerName: mapped.manufacturerName || null,
typeCode: mapped.typeCode || null,
typeName: mapped.typeName || null,
productModel: mapped.productModel || null,
isOnline: mapped.isOnline,
syncedAt: now
} as any;
// 幂等 upsert:按非空键匹配(仅包含非空键)
const matchWhere: any[] = [];
if (mapped.imei) matchWhere.push({ iccImei: mapped.imei });
if (mapped.deviceCode) matchWhere.push({ deviceCode: mapped.deviceCode });
const existing = await this.gatewayModel.findOne({ where: { [Op.or]: matchWhere } });
if (existing) {
await existing.update({ ...payload, syncedAt: now });
updated++;
} else {
await this.gatewayModel.create(payload);
inserted++;
}
}
return { inserted, updated, skipped };
}
private async writeLog(startTime: Date, status: number, syncType: string, recordCount: number, errorMessage: string | null): Promise<void> {
try {
await this.syncLogModel.create({
syncType,
startTime,
endTime: new Date(),
recordCount,
status,
errorMessage
} as any);
} catch (err) {
console.error("[ICC同步] 同步日志写入失败:", err);
}
}
}