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 = { '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 { const publicDir = this.configService.getOrThrow( '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(); 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('已创建默认地图配置'); } }); } }