Files
smart-parking-platform/backend/src/database-seeder.ts
T

328 lines
10 KiB
TypeScript
Raw Normal View History

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('已创建默认地图配置');
}
});
}
}