328 lines
10 KiB
TypeScript
328 lines
10 KiB
TypeScript
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
|||
|
|
import { ConfigService } from '@nestjs/config';
|
||
|
|
import { InjectDataSource } from '@nestjs/typeorm';
|
||
|
|
import crypto from 'crypto';
|
||
|
|
import fs from 'fs-extra';
|
||
|
|
import path from 'path';
|
||
|
|
import { DataSource } from 'typeorm';
|
||
|
|
import { AlarmRule } from './services/alarm-rule/alarm-rule.entity';
|
||
|
|
import { User } from './services/user/user.entity';
|
||
|
|
import { Camera } from './services/camera/camera.entity';
|
||
|
|
import { AlarmEvent } from './services/alarm-event/alarm-event.entity';
|
||
|
|
import { MapConfig } from './services/map-config/map-config.entity';
|
||
|
|
|
||
|
|
const SHARED_SECRET = 'smart-parking-platform';
|
||
|
|
|
||
|
|
const hashPassword = (plain: string): string =>
|
||
|
|
crypto
|
||
|
|
.createHmac('sha256', SHARED_SECRET)
|
||
|
|
.update(plain)
|
||
|
|
.digest('base64');
|
||
|
|
|
||
|
|
interface CameraSeed {
|
||
|
|
name: string;
|
||
|
|
latitude: number;
|
||
|
|
longitude: number;
|
||
|
|
model: string;
|
||
|
|
rtspUrl: string;
|
||
|
|
ruleNames: string[];
|
||
|
|
}
|
||
|
|
|
||
|
|
// 12 路摄像头:覆盖园区出入口、地下车库、车位区、通道、坡道与屋顶
|
||
|
|
const CAMERA_SEEDS: CameraSeed[] = [
|
||
|
|
{
|
||
|
|
name: '东门入口摄像机',
|
||
|
|
latitude: 36.6625,
|
||
|
|
longitude: 117.0368,
|
||
|
|
model: 'HIKVISION DS-CD2T46',
|
||
|
|
rtspUrl: 'rtsp://demo-camera/east-gate',
|
||
|
|
ruleNames: ['人员异常聚集', '车辆违停检测', '高峰期车流监测'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: '地下车库A区摄像机',
|
||
|
|
latitude: 36.6608,
|
||
|
|
longitude: 117.0342,
|
||
|
|
model: 'DAHUA IPC-HFW5442',
|
||
|
|
rtspUrl: 'rtsp://demo-camera/garage-a',
|
||
|
|
ruleNames: ['车辆违停检测'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: '中央车位区摄像机',
|
||
|
|
latitude: 36.6614,
|
||
|
|
longitude: 117.0351,
|
||
|
|
model: 'HIKVISION DS-2CD2386',
|
||
|
|
rtspUrl: 'rtsp://demo-camera/center-lot',
|
||
|
|
ruleNames: ['车辆违停检测', '高峰期车流监测'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: '西门出口摄像机',
|
||
|
|
latitude: 36.6619,
|
||
|
|
longitude: 117.0329,
|
||
|
|
model: 'UNV IPC2124SR3',
|
||
|
|
rtspUrl: 'rtsp://demo-camera/west-exit',
|
||
|
|
ruleNames: ['人员异常聚集', '高峰期车流监测'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: '非机动车通道摄像机',
|
||
|
|
latitude: 36.6631,
|
||
|
|
longitude: 117.0357,
|
||
|
|
model: 'DAHUA IPC-HDW3541',
|
||
|
|
rtspUrl: 'rtsp://demo-camera/non-motor-lane',
|
||
|
|
ruleNames: ['人员异常聚集'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: '南门访客入口摄像机',
|
||
|
|
latitude: 36.6602,
|
||
|
|
longitude: 117.0361,
|
||
|
|
model: 'HIKVISION DS-2CD2T86',
|
||
|
|
rtspUrl: 'rtsp://demo-camera/south-gate',
|
||
|
|
ruleNames: ['人员异常聚集', '车辆违停检测'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: '地下车库B区摄像机',
|
||
|
|
latitude: 36.6605,
|
||
|
|
longitude: 117.0335,
|
||
|
|
model: 'DAHUA IPC-HFW3441',
|
||
|
|
rtspUrl: 'rtsp://demo-camera/garage-b',
|
||
|
|
ruleNames: ['车辆违停检测', '高峰期车流监测'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: '车库电梯厅摄像机',
|
||
|
|
latitude: 36.661,
|
||
|
|
longitude: 117.033,
|
||
|
|
model: 'HIKVISION DS-2CD2346',
|
||
|
|
rtspUrl: 'rtsp://demo-camera/garage-lobby',
|
||
|
|
ruleNames: ['人员异常聚集'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: '北侧员工通道摄像机',
|
||
|
|
latitude: 36.6636,
|
||
|
|
longitude: 117.0346,
|
||
|
|
model: 'UNV IPC3228SR3',
|
||
|
|
rtspUrl: 'rtsp://demo-camera/north-staff',
|
||
|
|
ruleNames: ['人员异常聚集'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: '东侧坡道入口摄像机',
|
||
|
|
latitude: 36.662,
|
||
|
|
longitude: 117.0375,
|
||
|
|
model: 'HIKVISION DS-2CD3T46',
|
||
|
|
rtspUrl: 'rtsp://demo-camera/east-ramp',
|
||
|
|
ruleNames: ['车辆违停检测', '高峰期车流监测'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: '中央收费岗亭摄像机',
|
||
|
|
latitude: 36.6616,
|
||
|
|
longitude: 117.0348,
|
||
|
|
model: 'DAHUA IPC-HFW5442',
|
||
|
|
rtspUrl: 'rtsp://demo-camera/toll-booth',
|
||
|
|
ruleNames: ['人员异常聚集', '车辆违停检测'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: '屋顶停车场摄像机',
|
||
|
|
latitude: 36.6628,
|
||
|
|
longitude: 117.034,
|
||
|
|
model: 'HIKVISION DS-2CD2T87',
|
||
|
|
rtspUrl: 'rtsp://demo-camera/roof-lot',
|
||
|
|
ruleNames: ['车辆违停检测', '高峰期车流监测'],
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
// 报警抓拍种子图(源文件 → public 下固定名)
|
||
|
|
const ALARM_PIC_SEEDS: Record<string, string> = {
|
||
|
|
'alarm-crowd.jpg': 'seed-alarm-crowd.jpg',
|
||
|
|
'alarm-parking.jpg': 'seed-alarm-parking.jpg',
|
||
|
|
'alarm-peak.jpg': 'seed-alarm-peak.jpg',
|
||
|
|
'alarm-bicycle.jpg': 'seed-alarm-bicycle.jpg',
|
||
|
|
};
|
||
|
|
|
||
|
|
@Injectable()
|
||
|
|
export class DatabaseSeeder implements OnModuleInit {
|
||
|
|
private readonly logger = new Logger(DatabaseSeeder.name);
|
||
|
|
|
||
|
|
constructor(
|
||
|
|
@InjectDataSource() private readonly dataSource: DataSource,
|
||
|
|
private readonly configService: ConfigService,
|
||
|
|
) {}
|
||
|
|
|
||
|
|
async onModuleInit(): Promise<void> {
|
||
|
|
const publicDir = this.configService.getOrThrow<string>(
|
||
|
|
'PUBLIC_DIR_ABSOLUTE_PATH',
|
||
|
|
);
|
||
|
|
|
||
|
|
await this.dataSource.transaction(async (manager) => {
|
||
|
|
const userRepo = manager.getRepository(User);
|
||
|
|
const ruleRepo = manager.getRepository(AlarmRule);
|
||
|
|
const cameraRepo = manager.getRepository(Camera);
|
||
|
|
const eventRepo = manager.getRepository(AlarmEvent);
|
||
|
|
const mapRepo = manager.getRepository(MapConfig);
|
||
|
|
|
||
|
|
if ((await userRepo.count()) === 0) {
|
||
|
|
await userRepo.save([
|
||
|
|
userRepo.create({
|
||
|
|
username: 'admin',
|
||
|
|
nickname: '系统管理员',
|
||
|
|
role: 'admin',
|
||
|
|
password: hashPassword('admin123'),
|
||
|
|
email: 'admin@parking.local',
|
||
|
|
tel: '13800000001',
|
||
|
|
}),
|
||
|
|
userRepo.create({
|
||
|
|
username: 'operator',
|
||
|
|
nickname: '停车运营员',
|
||
|
|
role: 'user',
|
||
|
|
password: hashPassword('123456'),
|
||
|
|
email: 'operator@parking.local',
|
||
|
|
tel: '13800000002',
|
||
|
|
}),
|
||
|
|
]);
|
||
|
|
this.logger.log('已创建默认用户');
|
||
|
|
}
|
||
|
|
|
||
|
|
// 规则幂等补齐(按名称)
|
||
|
|
const allDays = [1, 2, 3, 4, 5, 6, 7];
|
||
|
|
const ruleDefs = [
|
||
|
|
{
|
||
|
|
name: '人员异常聚集',
|
||
|
|
enabled: true,
|
||
|
|
algorithmType: 'body' as const,
|
||
|
|
triggerDayOfWeek: allDays,
|
||
|
|
triggerTimeStart: '00:00:00',
|
||
|
|
triggerTimeEnd: '23:59:59',
|
||
|
|
triggerCountMin: 3,
|
||
|
|
triggerCountMax: -1,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: '车辆违停检测',
|
||
|
|
enabled: true,
|
||
|
|
algorithmType: 'vehicle' as const,
|
||
|
|
triggerDayOfWeek: allDays,
|
||
|
|
triggerTimeStart: '00:00:00',
|
||
|
|
triggerTimeEnd: '23:59:59',
|
||
|
|
triggerCountMin: 1,
|
||
|
|
triggerCountMax: -1,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: '高峰期车流监测',
|
||
|
|
enabled: true,
|
||
|
|
algorithmType: 'vehicle' as const,
|
||
|
|
triggerDayOfWeek: [1, 2, 3, 4, 5],
|
||
|
|
triggerTimeStart: '07:30:00',
|
||
|
|
triggerTimeEnd: '09:30:00',
|
||
|
|
triggerCountMin: 5,
|
||
|
|
triggerCountMax: -1,
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
const ruleByName = new Map<string, AlarmRule>();
|
||
|
|
for (const def of ruleDefs) {
|
||
|
|
let rule = await ruleRepo.findOne({ where: { name: def.name } });
|
||
|
|
if (!rule) {
|
||
|
|
rule = await ruleRepo.save(ruleRepo.create(def));
|
||
|
|
this.logger.log(`已创建报警规则:${def.name}`);
|
||
|
|
}
|
||
|
|
ruleByName.set(def.name, rule);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 摄像头幂等补齐到 12 路
|
||
|
|
for (const seed of CAMERA_SEEDS) {
|
||
|
|
let camera = await cameraRepo.findOne({
|
||
|
|
where: { name: seed.name },
|
||
|
|
relations: { alarmRules: true },
|
||
|
|
});
|
||
|
|
const alarmRules = seed.ruleNames
|
||
|
|
.map((n) => ruleByName.get(n))
|
||
|
|
.filter((r): r is AlarmRule => Boolean(r));
|
||
|
|
|
||
|
|
if (!camera) {
|
||
|
|
await cameraRepo.save(
|
||
|
|
cameraRepo.create({
|
||
|
|
name: seed.name,
|
||
|
|
latitude: seed.latitude,
|
||
|
|
longitude: seed.longitude,
|
||
|
|
model: seed.model,
|
||
|
|
rtspUrl: seed.rtspUrl,
|
||
|
|
online: false,
|
||
|
|
alarmRules,
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
// 已存在则补齐规则关联(不覆盖在线状态)
|
||
|
|
camera.alarmRules = alarmRules;
|
||
|
|
await cameraRepo.save(camera);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
this.logger.log('摄像头清单已同步(12 路)');
|
||
|
|
|
||
|
|
// 复制报警抓拍图到 public 目录
|
||
|
|
const seedAssetDir = path.join(__dirname, '..', 'seed-assets');
|
||
|
|
await fs.ensureDir(publicDir);
|
||
|
|
const publicPicPaths: string[] = [];
|
||
|
|
for (const [srcName, destName] of Object.entries(ALARM_PIC_SEEDS)) {
|
||
|
|
const dest = path.join(publicDir, destName);
|
||
|
|
try {
|
||
|
|
await fs.copy(path.join(seedAssetDir, srcName), dest, {
|
||
|
|
overwrite: true,
|
||
|
|
});
|
||
|
|
} catch {
|
||
|
|
this.logger.warn(`种子抓拍图缺失:${srcName}`);
|
||
|
|
}
|
||
|
|
if (await fs.pathExists(dest)) publicPicPaths.push(dest);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 历史报警事件:仅在无任何事件时播种,避免重复
|
||
|
|
if ((await eventRepo.count()) === 0 && publicPicPaths.length > 0) {
|
||
|
|
const cameras = await cameraRepo.find();
|
||
|
|
const rules = [...ruleByName.values()];
|
||
|
|
const events: AlarmEvent[] = [];
|
||
|
|
const now = Date.now();
|
||
|
|
const DAY = 24 * 60 * 60 * 1000;
|
||
|
|
|
||
|
|
// 过去 7 天约 42 条事件,时间升序
|
||
|
|
for (let i = 0; i < 42; i += 1) {
|
||
|
|
const offsetMin = Math.floor((42 - i) * (7 * 24 * 60) / 42);
|
||
|
|
const createdAt = new Date(now - offsetMin * 60 * 1000);
|
||
|
|
const camera = cameras[i % cameras.length];
|
||
|
|
const rule = rules[i % rules.length];
|
||
|
|
const picFilePath =
|
||
|
|
publicPicPaths[
|
||
|
|
rule.algorithmType === 'body'
|
||
|
|
? i % 2 === 0
|
||
|
|
? 0
|
||
|
|
: 3
|
||
|
|
: i % 2 === 0
|
||
|
|
? 1
|
||
|
|
: 2
|
||
|
|
] ?? publicPicPaths[i % publicPicPaths.length];
|
||
|
|
|
||
|
|
events.push(
|
||
|
|
eventRepo.create({
|
||
|
|
resolved: i % 10 < 7, // 约 70% 已处理,保留部分未处理
|
||
|
|
picFilePath,
|
||
|
|
createdAt,
|
||
|
|
sourceCamera: camera,
|
||
|
|
alarmRule: rule,
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
await eventRepo.save(events);
|
||
|
|
this.logger.log('已播种过去 7 天历史报警事件 42 条');
|
||
|
|
}
|
||
|
|
|
||
|
|
if ((await mapRepo.count()) === 0) {
|
||
|
|
await mapRepo.save(
|
||
|
|
mapRepo.create({
|
||
|
|
layerType: 'tileLayer',
|
||
|
|
layerUrlOrPath: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||
|
|
mapCenter: [36.6615, 117.035],
|
||
|
|
mapZoom: 17,
|
||
|
|
minZoom: 3,
|
||
|
|
maxZoom: 19,
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
this.logger.log('已创建默认地图配置');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|