From c4d7079ac30b4ed0393c2dc831627825c7a545d6 Mon Sep 17 00:00:00 2001
From: wangrong <wangrong@shsening.com>
Date: 星期一, 10 八月 2026 18:51:37 +0800
Subject: [PATCH] 增加监控点每日抓图
---
src/modules/push/entity/isapientity.ts | 50 +++
uploads/capture/20260810/GV7474259_1786354585367.jpg | 0
uploads/capture/20260810/GV6662505_1786354596817.jpg | 0
src/modules/monitor/service/capturetask.ts | 382 +++++++++++++++++++++++++
src/modules/push/entity/isapieventtype.ts | 17 +
uploads/capture/20260810/GT7568358_1786354599954.jpg | 0
uploads/capture/20260810/GV6662514_1786354589019.jpg | 0
src/modules/monitor/entity/capturetask.ts | 34 ++
uploads/capture/20260810/GV7474275_1786354579899.jpg | 0
uploads/capture/20260810/GU7398831_1786354575450.jpg | 0
uploads/capture/20260810/GV7474291_1786354592342.jpg | 0
src/modules/basicdata/service/iot.ts | 1
src/modules/monitor/service/ys7.ts | 314 ++++++++++++++++++++
src/modules/push/service/open.ts | 40 ++
uploads/capture/20260810/GT7568358_1786354561815.jpg | 0
src/modules/monitor/controller/admin/capturetask.ts | 20 +
16 files changed, 845 insertions(+), 13 deletions(-)
diff --git a/src/modules/basicdata/service/iot.ts b/src/modules/basicdata/service/iot.ts
index a267df3..8617333 100644
--- a/src/modules/basicdata/service/iot.ts
+++ b/src/modules/basicdata/service/iot.ts
@@ -114,5 +114,4 @@
return result;
}
-
}
\ No newline at end of file
diff --git a/src/modules/monitor/controller/admin/capturetask.ts b/src/modules/monitor/controller/admin/capturetask.ts
new file mode 100644
index 0000000..84b36d3
--- /dev/null
+++ b/src/modules/monitor/controller/admin/capturetask.ts
@@ -0,0 +1,20 @@
+import { Inject } from '@midwayjs/core';
+import { CoolController, BaseController } from '@cool-midway/core';
+import { PushCaptureTaskEntity } from '../../entity/capturetask';
+import { CapturetaskService } from '../../service/capturetask';
+
+@CoolController({
+ api: ['add', 'delete', 'update', 'info', 'list', 'page'],
+ entity: PushCaptureTaskEntity,
+ service: CapturetaskService,
+ pageQueryOp: {
+ keyWordLikeFields: ['a.deviceSerial', 'a.captureTaskId'],
+ fieldEq: ['a.status', 'a.captureDate']
+ }
+})
+export class AdminPushCaptureTaskController extends BaseController {
+ @Inject()
+ capturetaskService: CapturetaskService;
+
+
+}
\ No newline at end of file
diff --git a/src/modules/monitor/entity/capturetask.ts b/src/modules/monitor/entity/capturetask.ts
new file mode 100644
index 0000000..e9e4862
--- /dev/null
+++ b/src/modules/monitor/entity/capturetask.ts
@@ -0,0 +1,34 @@
+// src/modules/push/entity/capturetask.ts
+import { BaseEntity } from '../../base/entity/base';
+import { Column, Entity, Index } from 'typeorm';
+
+@Entity('t_push_capture_task')
+export class PushCaptureTaskEntity extends BaseEntity {
+ @Index()
+ @Column({ comment: '璁惧搴忓垪鍙�' })
+ deviceSerial: string;
+
+ @Column({ comment: '閫氶亾鍙�', type: 'int', default: 0 })
+ channelNo: number;
+
+ @Column({ comment: '鎶撳浘浠诲姟ID(钀ょ煶杩斿洖)', nullable: true })
+ captureTaskId: string;
+
+ @Column({ comment: '浜戠┖闂磗paceId', nullable: true })
+ spaceId: string;
+
+ @Column({ comment: '鍥剧墖鍦ㄤ簯绌洪棿鐨刄RL', type: 'text', nullable: true })
+ cloudUrl: string;
+
+ @Column({ comment: '鍥剧墖鍦ㄦ湰鍦扮殑URL', type: 'text', nullable: true })
+ localUrl: string;
+
+ @Column({ comment: '鏈湴瀛樺偍璺緞', type: 'text', nullable: true })
+ localPath: string;
+
+ @Column({ comment: '鎶撴媿鐘舵��', dict: ['PENDING','SUCCESS','FAILED'], default: 'PENDING' })
+ status: string;
+
+ @Column({ comment: '浠诲姟鎵ц鏃ユ湡', nullable: true })
+ captureDate: string;
+}
\ No newline at end of file
diff --git a/src/modules/monitor/service/capturetask.ts b/src/modules/monitor/service/capturetask.ts
new file mode 100644
index 0000000..b13464b
--- /dev/null
+++ b/src/modules/monitor/service/capturetask.ts
@@ -0,0 +1,382 @@
+import { Inject, Provide } from '@midwayjs/core';
+import { BaseService } from '@cool-midway/core';
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { Repository } from 'typeorm';
+import { BasicdataIotEntity } from '../../basicdata/entity/iot';
+import { BaseSysParamEntity } from '../../base/entity/sys/param';
+import * as _ from 'lodash';
+import { PushCaptureTaskEntity } from '../../monitor/entity/capturetask';
+import * as fs from 'fs';
+import * as path from 'path';
+import * as http from 'http';
+import * as https from 'https';
+import { Readable } from 'stream';
+
+/**
+ * 鏁版嵁婧愬畾涔変俊鎭湇鍔�
+ */
+@Provide()
+export class CapturetaskService extends BaseService {
+ @InjectEntityModel(BasicdataIotEntity)
+ basicdataIotEntity: Repository<BasicdataIotEntity>;
+
+ @InjectEntityModel(BaseSysParamEntity)
+ baseSysParamEntity: Repository<BaseSysParamEntity>;
+
+ @InjectEntityModel(PushCaptureTaskEntity)
+ pushCaptureTaskEntity: Repository<PushCaptureTaskEntity>;
+
+ /**
+ * 缁欐墍鏈夎悿鐭充簯鍥哄畾鐩戞帶璁惧锛氬嵆鏃舵姄鍥�(captureType=1) 鈫� 涓嬭浇鍒版湰鍦� 鈫� 钀藉簱 鈫� 鍒犻櫎浜戠鍥剧墖
+ */
+ async captureForAllDevices() {
+ console.log('[Capture] ===== 寮�濮� captureForAllDevices =====');
+
+ // 1. token
+ const tokenEntity = await this.baseSysParamEntity.findOneBy({ keyName: 'ys7.accessToken' });
+ console.log('[Capture] tokenEntity=', tokenEntity?.id, tokenEntity?.keyName);
+ if (!tokenEntity?.data) {
+ console.error('[Capture] 鉂� 钀ょ煶 accessToken 涓嶅瓨鍦紝璇峰厛鑾峰彇 Token');
+ return;
+ }
+ const accessToken = tokenEntity.data as string;
+ console.log('[Capture] accessToken 瀛樺湪锛岄暱搴�=', accessToken.length);
+
+ // 2. projectId
+ let projectId: string;
+ try {
+ projectId = await this.getProjectId();
+ console.log('[Capture] projectId=', projectId);
+ } catch (e) {
+ console.error('[Capture] 鉂� 鑾峰彇 projectId 澶辫触', e);
+ return;
+ }
+
+ // 3. 鏌ヨ澶�
+ const devices = await this.basicdataIotEntity.find({
+ where: { iotTypeCode: '钀ょ煶浜戝浐瀹氱洃鎺�' },
+ });
+ console.log(`[Capture] 鏌ヨ鍒拌澶囨暟閲�=${devices.length}`);
+
+ if (devices.length === 0) {
+ console.warn('[Capture] 鈿狅笍 娌℃湁钀ょ煶浜戝浐瀹氱洃鎺ц澶囷紝鐩存帴缁撴潫');
+ return;
+ }
+
+ for (const [index, device] of devices.entries()) {
+ console.log(`\n[Capture] ===== 璁惧 ${index + 1}/${devices.length} =====`);
+ const deviceSerial = device.iotCode;
+ const fileId = `cap_${deviceSerial}_${Date.now()}`;
+ try {
+ console.log('[Capture] 鍑嗗璋冪敤钀ょ煶鎶撳浘鎺ュ彛');
+ const params = new URLSearchParams({
+ accessToken,
+ deviceSerial,
+ channelNo: '1',
+ projectId,
+ fileId,
+ captureType: '1',
+ });
+
+ console.log('[Capture] 璇锋眰URL=https://open.ys7.com/api/open/cloud/v1/capture/save');
+ console.log('[Capture] 璇锋眰鍙傛暟=', params.toString());
+
+ const resp = await fetch('https://open.ys7.com/api/open/cloud/v1/capture/save', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ body: params.toString(),
+ });
+
+ console.log('[Capture] HTTP STATUS=', resp.status);
+
+ const json: any = await resp.json();
+ console.log('[Capture] 钀ょ煶杩斿洖=', JSON.stringify(json));
+
+ if (json.meta?.code !== 200) {
+ console.error(
+ `[Capture] 鉂� 鎶撳浘澶辫触 ${deviceSerial} code=${json.meta?.code} msg=${json.meta?.message}`
+ );
+ continue;
+ }
+
+ const picUrl: string = json.data;
+ if (!picUrl) {
+ console.error('[Capture] 鉂� 钀ょ煶杩斿洖 picUrl 涓虹┖');
+ continue;
+ }
+ console.log('[Capture] 鎶撳浘鎴愬姛 picUrl=', picUrl);
+
+ // 4. 鏈湴鐩綍
+ const dateDir = new Date().toISOString().slice(0, 10).replace(/-/g, '');
+ const localDir = path.join(process.cwd(), 'uploads', 'capture', dateDir);
+ fs.mkdirSync(localDir, { recursive: true });
+ console.log('[Capture] 鏈湴鐩綍=', localDir);
+
+ const ext = path.extname(new URL(picUrl).pathname) || '.jpg';
+ const fileName = `${deviceSerial}_${Date.now()}${ext}`;
+ const dest = path.join(localDir, fileName);
+ console.log('[Capture] 寮�濮嬩笅杞藉浘鐗� dest=', dest);
+
+ await this.downloadToFile(picUrl, dest);
+ console.log('[Capture] 鍥剧墖涓嬭浇瀹屾垚');
+
+ const relativePath = path
+ .relative(path.join(process.cwd(), 'uploads'), dest)
+ .replace(/\\/g, '/');
+ const localUrl = `/uploads/${relativePath}`;
+ console.log('[Capture] localUrl=', localUrl);
+
+ const entity = {
+ deviceSerial,
+ channelNo: '1',
+ captureTaskId: fileId,
+ cloudUrl: picUrl,
+ localPath: dest,
+ localUrl,
+ spaceId: projectId,
+ status: 'SUCCESS',
+ captureDate: dateDir,
+ } as any;
+
+ console.log('[Capture] 鍑嗗钀藉簱 entity=', JSON.stringify(entity));
+
+ await this.pushCaptureTaskEntity.save(entity);
+ console.log('[Capture] 鉁� 钀藉簱鎴愬姛');
+
+ // 5. 鍒犻櫎浜戠
+ try {
+ await this.deleteCloudCapture(projectId, fileId);
+ console.log('[Capture] 鉁� 浜戠鍥剧墖宸插垹闄�');
+ } catch (delErr) {
+ console.warn('[Capture] 鈿狅笍 浜戠鍒犻櫎澶辫触', delErr);
+ }
+
+ console.log('[Capture] 绛夊緟 1500ms 鍚庡鐞嗕笅涓�鍙拌澶�');
+ await new Promise(r => setTimeout(r, 1500));
+
+ } catch (err) {
+ console.error(`[Capture] 鉂� 璁惧寮傚父 ${deviceSerial}`, err);
+ }
+ }
+
+ console.log('[Capture] ===== captureForAllDevices 鎵ц缁撴潫 =====');
+ }
+
+ async deleteCloudCapture(projectId: string, fileId: string) {
+ const tokenEntity = await this.baseSysParamEntity.findOneBy({ keyName: 'ys7.accessToken' });
+ if (!tokenEntity?.data) {
+ throw new Error('钀ょ煶 accessToken 涓嶅瓨鍦紝璇峰厛鑾峰彇 Token');
+ }
+ const accessToken = tokenEntity.data as string;
+ const url = `https://open.ys7.com/api/open/cloud/v1/file?accessToken=${accessToken}&projectId=${projectId}&fileId=${encodeURIComponent(fileId)}`;
+ const resp = await fetch(url, { method: 'DELETE' });
+ const json: any = await resp.json();
+ if (json.meta?.code !== 200) {
+ throw new Error('鍒犻櫎浜戞姄鎷嶅け璐�: ' + JSON.stringify(json));
+ }
+ }
+
+ /**
+ * 1. 鑾峰彇鎵�鏈夎悿鐭充簯鍥哄畾鐩戞帶璁惧鐨刬otCode(iotCode灏辨槸deviceSerial) 2. 缁欐瘡涓�涓澶囧垱寤烘姄鍥句换鍔�(涓嬪彂璁惧鎶撴媿) 3. 涓嬭浇鍒版湰鍦板苟钀藉簱
+ * 鍦ㄦ暣涓嚱鏁颁腑瀹屾垚鎵�鏈夌殑鎿嶄綔锛岀‘淇濇瘡涓楠ら兘鑳芥纭墽琛岋紝骞跺湪鍑虹幇閿欒鏃惰繘琛岄�傚綋鐨勫鐞嗗拰鏃ュ織璁板綍銆�
+ */
+ async dailyCaptureForAllDevices() {
+ const devices = await this.basicdataIotEntity.find({
+ where: {
+ iotTypeCode: '钀ょ煶浜戝浐瀹氱洃鎺�',
+ },
+ });
+ for (const device of devices) {
+ const deviceSerial = device.iotCode;
+ const channelNo = Number(device.iotChanel ?? 1);
+ try {
+ const existing = await this.pushCaptureTaskEntity.findOne({
+ where: {
+ deviceSerial,
+ status: 'TASK_CREATED',
+ },
+ });
+ let taskId: string;
+ if (existing) {
+ taskId = existing.captureTaskId;
+ console.log(`[Capture] 浠诲姟宸插瓨鍦� ${deviceSerial} taskId=${taskId}`);
+ } else {
+ const { taskId: newTaskId } = await this.createDeviceCaptureTask(
+ deviceSerial,
+ channelNo,
+ );
+ taskId = newTaskId;
+ await this.pushCaptureTaskEntity.save({
+ deviceSerial,
+ channelNo,
+ captureTaskId: taskId,
+ spaceId: await this.getSpaceID(),
+ status: 'TASK_CREATED',
+ captureDate: new Date().toISOString().slice(0, 10).replace(/-/g, ''),
+ });
+ console.log(`[Capture] 鍒涘缓浠诲姟鎴愬姛 ${deviceSerial} taskId=${taskId}`);
+ }
+ await this.downloadAndSaveCapture(deviceSerial, channelNo);
+ } catch (err) {
+ console.error(`[Capture] 璁惧澶辫触 ${deviceSerial}`, err);
+ }
+ }
+ }
+
+ // ---------- 1. 鍒涘缓鎶撳浘浠诲姟(涓嬪彂璁惧鎶撴媿) ----------
+ async createDeviceCaptureTask(deviceSerial: string, channelNo = 1) {
+ const spaceId = await this.getSpaceID();
+ const tokenEntity = await this.baseSysParamEntity.findOneBy({ keyName: 'ys7.accessToken' });
+ if (!tokenEntity?.data) {
+ throw new Error('钀ょ煶 accessToken 涓嶅瓨鍦紝璇峰厛鑾峰彇 Token');
+ }
+ const accessToken = tokenEntity.data as string;
+ const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
+ const taskName = `daily_${deviceSerial}_${today}`;
+ // 钀ょ煶銆屾壒閲忔姄鍥句换鍔°�嶆帴鍙�(缁撴灉鍐欎簯绌洪棿)
+ const params = new URLSearchParams({
+ accessToken,
+ taskName,
+ deviceSerial,
+ channelNo: String(channelNo),
+ captureType: '1',
+ resultSpaceId: spaceId,
+ schedule: '0 0 8,12,18,20 * * ?',
+ expireDays: '7',
+ });
+ const resp = await fetch('https://open.ys7.com/api/service/cloudrecord/pic/capture/task', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ body: params.toString(),
+ });
+ const json: any = await resp.json();
+ if (json.meta?.code !== 200) throw new Error('create capture task fail: ' + JSON.stringify(json));
+ return { taskId: json.data?.taskId, spaceId };
+ }
+
+ // ---------- 2. 鏌ヨ浠诲姟浜х敓鐨勫浘鐗�(濯掕祫鍒楄〃) ----------
+ async queryCaptureImages(deviceSerial: string, spaceId: string) {
+ const tokenEntity = await this.baseSysParamEntity.findOneBy({ keyName: 'ys7.accessToken' });
+ if (!tokenEntity?.data) {
+ throw new Error('钀ょ煶 accessToken 涓嶅瓨鍦紝璇峰厛鑾峰彇 Token');
+ }
+ const accessToken = tokenEntity.data as string;
+ const params = new URLSearchParams({
+ accessToken,
+ deviceSerial,
+ spaceId,
+ startTime: this.dayStartTimestamp(),
+ endTime: this.dayEndTimestamp(),
+ });
+ const resp = await fetch('https://open.ys7.com/api/open/cloud/v1/capture/list?' + params.toString());
+ const json: any = await resp.json();
+ return (json.data?.list || []) as Array<{ picUrl: string; captureTime: number; picId: string }>;
+ }
+
+ // ---------- 3. 涓嬭浇鍒版湰鍦板苟钀藉簱 ----------
+ async downloadAndSaveCapture(deviceSerial: string, channelNo = 0) {
+ const spaceId = await this.getSpaceID();
+ const imgs = await this.queryCaptureImages(deviceSerial, spaceId);
+ const dateDir = new Date().toISOString().slice(0, 10).replace(/-/g, '');
+ const localDir = path.join(process.cwd(), 'uploads', 'capture', dateDir);
+ fs.mkdirSync(localDir, { recursive: true });
+
+ for (const img of imgs) {
+ const ext = path.extname(new URL(img.picUrl).pathname) || '.jpg';
+ const fileName = `${deviceSerial}_${img.captureTime}${ext}`;
+ const dest = path.join(localDir, fileName);
+ try {
+ await this.downloadToFile(img.picUrl, dest);
+ await this.pushCaptureTaskEntity.save({
+ deviceSerial,
+ channelNo,
+ cloudUrl: img.picUrl,
+ localPath: dest,
+ spaceId,
+ status: 'SUCCESS',
+ captureDate: dateDir,
+ } as any);
+ } catch (e) {
+ console.error('涓嬭浇/钀藉簱澶辫触:', img.picUrl, e);
+ }
+ }
+ }
+
+ async getProjectId(): Promise<string> {
+ const e = await this.baseSysParamEntity.findOneBy({ keyName: 'ys7.captureProjectId' });
+ if (e?.data) return e.data as string;
+ // 娌℃湁灏辨煡鍒楄〃鍙栫涓�涓紝鍒嚜鍔ㄥ缓
+ const tokenEntity = await this.baseSysParamEntity.findOneBy({ keyName: 'ys7.accessToken' });
+ if (!tokenEntity?.data) {
+ throw new Error('钀ょ煶 accessToken 涓嶅瓨鍦紝璇峰厛鑾峰彇 Token');
+ }
+ const accessToken = tokenEntity.data as string;
+ const url = `https://open.ys7.com/api/open/cloud/v1/projects?accessToken=${encodeURIComponent(accessToken)}&pageNumber=0&pageSize=10`;
+ console.log('[Capture] 璇锋眰 projects 鎺ュ彛');
+ const r = await fetch(url);
+ const j: any = await r.json();
+ console.log('[Capture] projects 杩斿洖=', JSON.stringify(j));
+ const pid = j.data?.[0]?.projectId;
+ if (!pid) throw new Error('浜戞姄鎷嶉」鐩笉瀛樺湪锛岃鎺у埗鍙板垱寤�');
+ await this.baseSysParamEntity.save({ keyName: 'ys7.captureProjectId', data: pid, updateTime: new Date() });
+ return pid;
+ }
+
+ async getSpaceID(): Promise<string> {
+ const entity = await this.baseSysParamEntity.findOneBy({ keyName: 'ys7.storageSpaceID' });
+ if (!entity ||
+ !entity.data) {
+ console.error('[Ys7Service] ys7.spaceID 鏈厤缃�');
+ throw new Error('ys7.spaceID 鏈厤缃�');
+ }
+ return entity!.data as string;
+ }
+
+ // ---------- 宸ュ叿锛欸ET 涓嬭浇鏂囦欢鍒版湰鍦� ----------
+ private downloadToFile(url: string, destPath: string): Promise<void> {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const resp = await fetch(url, { redirect: 'follow' });
+ if (!resp.ok || !resp.body) {
+ reject(new Error(`download HTTP ${resp.status}`));
+ return;
+ }
+ const fileStream = fs.createWriteStream(destPath);
+ const nodeStream = Readable.from(resp.body as any);
+ nodeStream.on('error', reject);
+ fileStream.on('finish', () => fileStream.close(() => resolve()));
+ fileStream.on('error', reject);
+ nodeStream.pipe(fileStream);
+ } catch (err) {
+ reject(err);
+ }
+ });
+ }
+
+ private dayStartTimestamp(): string {
+ const now = new Date();
+ const start = new Date(Date.UTC(
+ now.getFullYear(),
+ now.getMonth(),
+ now.getDate(),
+ 0,
+ 0,
+ 0,
+ ));
+ return Math.floor((start.getTime() + 8 * 60 * 60 * 1000) / 1000).toString();
+ }
+
+ private dayEndTimestamp(): string {
+ const now = new Date();
+ const end = new Date(Date.UTC(
+ now.getFullYear(),
+ now.getMonth(),
+ now.getDate(),
+ 23,
+ 59,
+ 59,
+ ));
+ return Math.floor((end.getTime() + 8 * 60 * 60 * 1000) / 1000).toString();
+ }
+
+}
\ No newline at end of file
diff --git a/src/modules/monitor/service/ys7.ts b/src/modules/monitor/service/ys7.ts
index 91037e3..bbdfaff 100644
--- a/src/modules/monitor/service/ys7.ts
+++ b/src/modules/monitor/service/ys7.ts
@@ -5,6 +5,7 @@
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
+
interface Ys7TokenData {
accessToken: string;
/** 钀ょ煶杩斿洖鐨勬绉掔骇杩囨湡鏃堕棿鎴� */
@@ -37,13 +38,11 @@
private async fetchAndSaveRemote(): Promise<Ys7TokenData> {
const appKey_result = await this.baseSysParamEntity.findOneBy({ keyName: 'ys7.appKey' });
const appSecret_result = await this.baseSysParamEntity.findOneBy({ keyName: 'ys7.appSecret' });
-
if (!appKey_result || !appSecret_result) {
throw new Error(
'钀ょ煶浜� appKey/appSecret 鏈厤缃紝璇峰湪銆愮郴缁熷弬鏁般�戜腑缁存姢 ys7.appKey / ys7.appSecret'
);
}
-
const { data: res } = await axios.post(
'https://open.ys7.com/api/lapp/token/get',
new URLSearchParams({ appKey: appKey_result.data, appSecret: appSecret_result.data }),
@@ -52,7 +51,6 @@
timeout: 8000,
}
);
-
if (String(res.code) !== '200') {
throw new Error(`钀ょ煶浜戣幏鍙� Token 澶辫触锛�${res.msg}锛坈ode: ${res.code}锛塦);
}
@@ -61,8 +59,6 @@
accessToken: res.data.accessToken,
expireTime: res.data.expireTime,
};
-
- // 鍐欏洖绯荤粺鍙傛暟琛紙Cool-Admin 鑷甫 upsert锛�
const token = await this.baseSysParamEntity.findOneBy({ keyName: 'ys7.accessToken' });
this.baseSysParamEntity.save({
...token,
@@ -70,7 +66,313 @@
updateTime: new Date(),
}).catch(() => {
// 蹇界暐鍐欏洖澶辫触
- });
+ });
return tokenData;
}
+
+ async getSpaceID(): Promise<string> {
+ const entity = await this.baseSysParamEntity.findOneBy({ keyName: 'ys7.storageSpaceID' });
+ if (!entity ||
+ !entity.updateTime ||
+ Date.now() - new Date(entity.updateTime).getTime() >
+ 6 * 24 * 60 * 60 * 1000) {
+ const spaceId = await this.createCaptureSpace();
+ await this.baseSysParamEntity.save({
+ id: entity?.id,
+ keyName: 'ys7.storageSpaceID',
+ data: spaceId,
+ updateTime: new Date(),
+ });
+ return spaceId;
+ }
+ return entity!.data as string;
+ }
+
+ /**
+ * 鍒涘缓钀ょ煶浜戙�愯澶囨姄鍥俱�戝瓨鍌ㄧ┖闂�
+ * - spaceName 鍙栬嚜绯荤粺鍙傛暟 ys7.spaceName
+ * - 杩囨湡澶╂暟榛樿 7 澶�
+ * - 鍒涘缓鎴愬姛鍚� storageSpaceID 鍐欏叆 ys7.storageSpaceID
+ * - 璋冪敤钀ょ煶浜戠┖闂村垱寤烘帴鍙o細https://open.ys7.com/api/service/open/storage/engine/space
+ * - 鎺ュ彛璇存槑鍦板潃锛歨ttps://open.ys7.com/help/5236
+ * - 灏嗗垱寤哄ソ鐨勫瓨鍌ㄧ┖闂碔D瀛樺叆鍙傛暟锛歽s7.storageSpaceID
+ */
+ async createCaptureSpace(expireDays = 7): Promise<string> {
+ // 1. accessToken
+ const tokenEntity = await this.baseSysParamEntity.findOneBy({ keyName: 'ys7.accessToken' });
+ if (!tokenEntity?.data) {
+ console.error('[Ys7Service] ys7.accessToken 涓虹┖');
+ throw new Error('ys7.accessToken 涓虹┖');
+ }
+ const accessToken = String(tokenEntity.data).trim();
+
+ // 2. spaceName
+ const spaceEntity = await this.baseSysParamEntity.findOneBy({ keyName: 'ys7.spaceName' });
+ if (!spaceEntity?.data) {
+ console.error('[Ys7Service] ys7.spaceName 鏈厤缃�');
+ throw new Error('ys7.spaceName 鏈厤缃�');
+ }
+ const spaceName = String(spaceEntity.data).trim();
+ // 4. 钀ょ煶渚ф槸鍚﹀凡瀛樺湪鍚屽悕绌洪棿
+ const existId = await this.getSpaceIdByName(accessToken, spaceName);
+ if (existId) {
+ console.log('[Ys7Service] 钀ょ煶渚у凡瀛樺湪鍚屽悕绌洪棿锛宻paceId=%s', existId);
+ await this.saveStorageSpaceID(existId);
+ return existId;
+ }
+ // 5. 鐪熸鍒涘缓
+ const params = new URLSearchParams({
+ bizType: 'capture',
+ expireDays: String(expireDays),
+ spaceName,
+ storageType: '1',
+ });
+
+ let res: any;
+ try {
+ const resp = await axios.post(
+ 'https://open.ys7.com/api/service/open/storage/engine/space',
+ params,
+ {
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ accessToken,
+ },
+ timeout: 8000,
+ }
+ );
+ res = resp.data;
+ } catch (err: any) {
+ console.error('[Ys7Service] 钀ょ煶鎺ュ彛璇锋眰澶辫触');
+ if (err.response) {
+ console.error('[Ys7Service] RESPONSE BODY=%s', JSON.stringify(err.response.data));
+ } else {
+ console.error('[Ys7Service] ERROR MSG=%s', err.message);
+ }
+ throw err;
+ }
+ // 6. 鏍¢獙钀ょ煶涓氬姟杩斿洖
+ if (!res || res.meta?.code !== 200) {
+ console.error('[Ys7Service] 钀ょ煶鍒涘缓绌洪棿涓氬姟澶辫触: %s', JSON.stringify(res?.meta));
+ throw new Error(`钀ょ煶鍒涘缓绌洪棿澶辫触: ${JSON.stringify(res?.meta)}`);
+ }
+ const storageSpaceID = String(res.data); // data 灏辨槸 spaceId
+ // 7. 淇濆瓨鍒扮郴缁熷弬鏁�
+ await this.saveStorageSpaceID(storageSpaceID);
+ return storageSpaceID;
+ }
+
+ /**
+ * 淇敼钀ょ煶浜戙�愯澶囨姄鍥俱�戝瓨鍌ㄧ┖闂�
+ * - spaceId 鍙栬嚜 ys7.storageSpaceID
+ * - accessToken 鍙栬嚜 ys7.accessToken
+ * - 榛樿杩囨湡澶╂暟 7 澶�
+ */
+ async updateCaptureSpace(expireDays = 7): Promise<boolean> {
+ // 1. accessToken
+ const tokenEntity = await this.baseSysParamEntity.findOneBy({ keyName: 'ys7.accessToken' });
+ if (!tokenEntity?.data) {
+ throw new Error('钀ょ煶 accessToken 涓嶅瓨鍦紝璇峰厛鑾峰彇 Token');
+ }
+ const accessToken = tokenEntity.data as string;
+
+ // 2. spaceId
+ const spaceEntity = await this.baseSysParamEntity.findOneBy({ keyName: 'ys7.storageSpaceID' });
+ if (!spaceEntity?.data) {
+ throw new Error('钀ょ煶 storageSpaceID 涓嶅瓨鍦紝璇峰厛鍒涘缓鎶撳浘绌洪棿');
+ }
+ const spaceId = Number(spaceEntity.data);
+
+ // 3. 缁勮鍙傛暟锛圥UT 浣跨敤 URLSearchParams锛�
+ const params = new URLSearchParams();
+ params.append('spaceId', String(spaceId));
+ params.append('expireDays', String(expireDays));
+
+ const url = 'https://open.ys7.com/api/service/open/storage/engine/space';
+ const { data: res } = await axios.put(url, params, {
+ headers: {
+ accessToken,
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ timeout: 8000,
+ });
+ if (res.meta?.code !== 200) {
+ throw new Error(
+ `钀ょ煶浜戜慨鏀瑰瓨鍌ㄧ┖闂村け璐ワ細${res.meta?.message}锛坈ode: ${res.meta?.code}锛塦
+ );
+ }
+ if (res.data) {
+ this.baseSysParamEntity.save({
+ ...spaceEntity,
+ updateTime: new Date(),
+ }).catch(() => {
+ // 蹇界暐鍐欏洖澶辫触
+ });
+ }
+ return !!res.data;
+ }
+
+ /**
+ * 鍒犻櫎钀ょ煶浜戙�愯澶囨姄鍥俱�戝瓨鍌ㄧ┖闂�
+ * - spaceId 鍙栬嚜 ys7.storageSpaceID
+ * - accessToken 鍙栬嚜 ys7.accessToken
+ * - 鍒犻櫎鎴愬姛鍚庯紝娓呯┖ ys7.storageSpaceID
+ */
+ async deleteCaptureSpace(): Promise<boolean> {
+ // 1. accessToken
+ const tokenEntity = await this.baseSysParamEntity.findOneBy({ keyName: 'ys7.accessToken' });
+ if (!tokenEntity?.data) {
+ throw new Error('钀ょ煶 accessToken 涓嶅瓨鍦紝璇峰厛鑾峰彇 Token');
+ }
+ const accessToken = tokenEntity.data as string;
+
+ // 2. spaceId
+ const spaceEntity = await this.baseSysParamEntity.findOneBy({ keyName: 'ys7.storageSpaceID' });
+ if (!spaceEntity?.data) {
+ throw new Error('钀ょ煶 storageSpaceID 涓嶅瓨鍦紝鏃犳硶鍒犻櫎');
+ }
+ const spaceId = Number(spaceEntity.data);
+
+ // 3. DELETE 璇锋眰锛坆ody 浣跨敤 URLSearchParams锛�
+ const params = new URLSearchParams();
+ params.append('spaceId', String(spaceId));
+
+ const url = 'https://open.ys7.com/api/service/open/storage/engine/space';
+ const { data: res } = await axios.delete(url, {
+ headers: {
+ accessToken,
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ data: params.toString(),
+ timeout: 8000,
+ });
+ if (res.meta?.code !== 200) {
+ throw new Error(
+ `钀ょ煶浜戝垹闄ゅ瓨鍌ㄧ┖闂村け璐ワ細${res.meta?.message}锛坈ode: ${res.meta?.code}锛塦
+ );
+ }
+ // 4. 娓呯悊鏈湴鍙傛暟
+ spaceEntity.data = '';
+ spaceEntity.updateTime = new Date();
+ await this.baseSysParamEntity.save(spaceEntity);
+
+ return !!res.data;
+ }
+
+ /**
+ * 鏍规嵁 spaceName 鏌ヨ钀ょ煶鏄惁宸插瓨鍦ㄣ�愯澶囨姄鍥俱�戠┖闂�
+ * 浣跨敤瀹樻柟 listById 鎺ュ彛锛屾敮鎸佺炕椤�
+ * 杩斿洖 storageSpaceID 鎴� null
+ */
+ private async getSpaceIdByName(
+ accessToken: string,
+ spaceName: string
+ ): Promise<string | null> {
+
+ const url = 'https://open.ys7.com/api/service/open/storage/engine/space/listById';
+
+ // 鏌ヨ鏃堕棿绐楀彛锛氭渶杩� 30 澶╋紙钀ょ煶寮哄埗瑕佹眰锛�
+ const endTime = new Date();
+ const startTime = new Date(endTime.getTime() - 30 * 24 * 60 * 60 * 1000);
+
+ const format = (d: Date) =>
+ d.getFullYear() +
+ '-' +
+ String(d.getMonth() + 1).padStart(2, '0') +
+ '-' +
+ String(d.getDate()).padStart(2, '0') +
+ ' ' +
+ String(d.getHours()).padStart(2, '0') +
+ ':' +
+ String(d.getMinutes()).padStart(2, '0') +
+ ':' +
+ String(d.getSeconds()).padStart(2, '0');
+
+ const queryParams = new URLSearchParams({
+ startTime: format(startTime),
+ endTime: format(endTime),
+ pageSize: '50',
+ bizTypeList: 'capture', // 鍙煡璁惧鎶撳浘绌洪棿
+ });
+
+ let lastId: number | null = null;
+ let page = 1;
+
+ try {
+ while (true) {
+ if (lastId !== null) {
+ queryParams.set('lastSpaceId', String(lastId));
+ }
+
+ const fullUrl = `${url}?${queryParams.toString()}`;
+
+ const resp = await axios.get(fullUrl, {
+ headers: {
+ accessToken,
+ },
+ timeout: 8000,
+ });
+ const res = resp.data;
+ if (res?.meta?.code !== 200) {
+ console.error('[Ys7Service] 鏌ヨ绌洪棿鍒楄〃澶辫触: %s', JSON.stringify(res.meta));
+ throw new Error(`鏌ヨ钀ょ煶绌洪棿澶辫触: ${JSON.stringify(res.meta)}`);
+ }
+
+ const data = res.data || {};
+ const list = data.result || [];
+
+ // 绮剧‘鍖归厤 spaceName
+ const match = list.find(
+ (item: any) => item.spaceName === spaceName && item.bizType === 'capture'
+ );
+
+ if (match) {
+ console.log(
+ '[Ys7Service] 鉁� 鎵惧埌宸插瓨鍦ㄧ殑绌洪棿锛歴paceId=%s, spaceName=%s, bizType=%s',
+ match.spaceId,
+ match.spaceName,
+ match.bizType
+ );
+ return String(match.spaceId);
+ }
+
+ // 娌℃湁涓嬩竴椤碉紝缁堟
+ if (!data.hasNext) {
+ break;
+ }
+
+ lastId = data.lastId;
+ page++;
+ }
+ return null;
+ } catch (err: any) {
+ console.error('[Ys7Service] 鏌ヨ钀ょ煶绌洪棿寮傚父');
+ if (err.response) {
+ console.error('[Ys7Service] HTTP STATUS=%d', err.response.status);
+ console.error('[Ys7Service] RESPONSE=%s', JSON.stringify(err.response.data));
+ } else {
+ console.error('[Ys7Service] ERROR=%s', err.message);
+ }
+ throw err;
+ }
+ }
+
+ /** 鍏叡淇濆瓨鏂规硶 */
+ private async saveStorageSpaceID(spaceId: string) {
+ let e = await this.baseSysParamEntity.findOneBy({ keyName: 'ys7.storageSpaceID' });
+ if (!e) {
+ e = this.baseSysParamEntity.create({
+ keyName: 'ys7.storageSpaceID',
+ name: '钀ょ煶浜戞姄鍥剧┖闂碔D',
+ dataType: 1,
+ data: spaceId,
+ updateTime: new Date(),
+ });
+ } else {
+ e.data = spaceId;
+ e.updateTime = new Date();
+ }
+ await this.baseSysParamEntity.save(e);
+ }
+
}
\ No newline at end of file
diff --git a/src/modules/push/entity/isapientity.ts b/src/modules/push/entity/isapientity.ts
new file mode 100644
index 0000000..8cbfbaa
--- /dev/null
+++ b/src/modules/push/entity/isapientity.ts
@@ -0,0 +1,50 @@
+// entity/push/isapi.raw.entity.ts
+import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm';
+import { IsapiEventType } from './isapieventtype';
+
+@Entity('t_push_isapi_raw')
+export class PushIsapiRawEntity {
+ @PrimaryGeneratedColumn()
+ id: number;
+
+ /** 钀ょ煶 webhook messageId锛堝箓绛夛級 */
+ @Column({ unique: true, comment: '娑堟伅ID' })
+ messageId: string;
+
+ /** 璁惧搴忓垪鍙� */
+ @Column({ comment: '璁惧搴忓垪鍙�' })
+ deviceId: string;
+
+ /** 閫氶亾鍙� */
+ @Column({ comment: '閫氶亾鍙�' })
+ channelNo: number;
+
+ /** ISAPI 浜嬩欢绫诲瀷锛堟灇涓剧害鏉燂級 */
+ @Column({
+ type: 'enum',
+ enum: IsapiEventType,
+ default: IsapiEventType.UNKNOWN,
+ comment: 'ISAPI 浜嬩欢绫诲瀷',
+ })
+ eventType: IsapiEventType;
+
+ /** 浜嬩欢鐘舵�侊細active / inactive */
+ @Column({ comment: '浜嬩欢鐘舵��' })
+ eventState: string;
+
+ /** 娴峰悍 ISAPI 鍘熷 payload锛堝瓧绗︿覆锛� */
+ @Column({ type: 'text', comment: '鍘熷 ISAPI payload' })
+ rawPayload: string;
+
+ /** 瑙f瀽鍚庣殑 JSON锛堟柟渚垮悗鏈熸墿灞曪級 */
+ @Column({ type: 'json', nullable: true, comment: '瑙f瀽鍚庣殑 payload' })
+ parsed: any;
+
+ /** 璁惧涓婃姤鏃堕棿 */
+ @Column({ type: 'datetime', comment: '浜嬩欢鏃堕棿' })
+ eventTime: Date;
+
+ /** 鍏ュ簱鏃堕棿 */
+ @CreateDateColumn({ comment: '鍒涘缓鏃堕棿' })
+ createTime: Date;
+}
\ No newline at end of file
diff --git a/src/modules/push/entity/isapieventtype.ts b/src/modules/push/entity/isapieventtype.ts
new file mode 100644
index 0000000..a9fc8e6
--- /dev/null
+++ b/src/modules/push/entity/isapieventtype.ts
@@ -0,0 +1,17 @@
+// enum/isapi-event.enum.ts
+export enum IsapiEventType {
+ /** 璁惧鐘舵�侊紙鐢甸噺 / 澶槼鑳� / 浼戠湢锛� */
+ DEVICE_STATUS = 'deviceStatus',
+
+ /** GPS 浣嶇疆 */
+ GPS = 'GPS',
+
+ /** IO 杈撳叆 */
+ IO = 'IO',
+
+ /** 鏅鸿兘浜嬩欢 */
+ SMART = 'smart',
+
+ /** 鏈煡 */
+ UNKNOWN = 'unknown',
+}
\ No newline at end of file
diff --git a/src/modules/push/service/open.ts b/src/modules/push/service/open.ts
index b6421f5..7bc185d 100644
--- a/src/modules/push/service/open.ts
+++ b/src/modules/push/service/open.ts
@@ -9,6 +9,8 @@
import { PushVehicleGpsEntity } from '../entity/vehiclegps';
import { PushDeviceOnoffEntity } from '../entity/deviceonoff';
import { PushDevicealarmEntity } from '../entity/devicealarm';
+import { PushIsapiRawEntity } from '../entity/isapientity';
+import { IsapiEventType } from '../entity/isapieventtype';
import { PluginService } from '../../plugin/service/info';
import { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';
@@ -37,6 +39,9 @@
@InjectEntityModel(PushAuditlogEntity)
pushAuditlogEntity: Repository<PushAuditlogEntity>;
+
+ @InjectEntityModel(PushIsapiRawEntity)
+ pushIsapiRawEntity: Repository<PushIsapiRawEntity>;
@Inject()
pluginService: PluginService;
@@ -199,12 +204,35 @@
// type: 'ys.open.isapi'
// }
// }
- // processCameraGpsAsync(body)
- // await this.pushIsapiRepo.save({
- // messageId,
- // raw: JSON.stringify(body),
- // createTime: new Date(),
- // });
+ const header = body.header;
+ const payloadStr = body.body?.payload;
+ if (!payloadStr) {
+ console.warn('ISAPI payload missing');
+ break;
+ }
+ let parsed: any;
+ try {
+ parsed = JSON.parse(payloadStr);
+ } catch (e) {
+ console.error('ISAPI payload parse error', e);
+ break;
+ }
+ const eventType =
+ Object.values(IsapiEventType).includes(parsed.eventType)
+ ? parsed.eventType
+ : IsapiEventType.UNKNOWN;
+ await this.pushIsapiRawEntity.save({
+ messageId: header.messageId,
+ deviceId: header.deviceId,
+ channelNo: header.channelNo ?? 0,
+ eventType,
+ eventState: parsed.eventState,
+ rawPayload: payloadStr,
+ parsed,
+ eventTime: parsed.dateTime
+ ? new Date(parsed.dateTime)
+ : new Date(header.messageTime),
+ });
break;
}
default:
diff --git a/uploads/capture/20260810/GT7568358_1786354561815.jpg b/uploads/capture/20260810/GT7568358_1786354561815.jpg
new file mode 100644
index 0000000..0f795e7
--- /dev/null
+++ b/uploads/capture/20260810/GT7568358_1786354561815.jpg
Binary files differ
diff --git a/uploads/capture/20260810/GT7568358_1786354599954.jpg b/uploads/capture/20260810/GT7568358_1786354599954.jpg
new file mode 100644
index 0000000..88efa42
--- /dev/null
+++ b/uploads/capture/20260810/GT7568358_1786354599954.jpg
Binary files differ
diff --git a/uploads/capture/20260810/GU7398831_1786354575450.jpg b/uploads/capture/20260810/GU7398831_1786354575450.jpg
new file mode 100644
index 0000000..b131238
--- /dev/null
+++ b/uploads/capture/20260810/GU7398831_1786354575450.jpg
Binary files differ
diff --git a/uploads/capture/20260810/GV6662505_1786354596817.jpg b/uploads/capture/20260810/GV6662505_1786354596817.jpg
new file mode 100644
index 0000000..411bf8b
--- /dev/null
+++ b/uploads/capture/20260810/GV6662505_1786354596817.jpg
Binary files differ
diff --git a/uploads/capture/20260810/GV6662514_1786354589019.jpg b/uploads/capture/20260810/GV6662514_1786354589019.jpg
new file mode 100644
index 0000000..ede51d4
--- /dev/null
+++ b/uploads/capture/20260810/GV6662514_1786354589019.jpg
Binary files differ
diff --git a/uploads/capture/20260810/GV7474259_1786354585367.jpg b/uploads/capture/20260810/GV7474259_1786354585367.jpg
new file mode 100644
index 0000000..71dac7e
--- /dev/null
+++ b/uploads/capture/20260810/GV7474259_1786354585367.jpg
Binary files differ
diff --git a/uploads/capture/20260810/GV7474275_1786354579899.jpg b/uploads/capture/20260810/GV7474275_1786354579899.jpg
new file mode 100644
index 0000000..c1552f6
--- /dev/null
+++ b/uploads/capture/20260810/GV7474275_1786354579899.jpg
Binary files differ
diff --git a/uploads/capture/20260810/GV7474291_1786354592342.jpg b/uploads/capture/20260810/GV7474291_1786354592342.jpg
new file mode 100644
index 0000000..74da95c
--- /dev/null
+++ b/uploads/capture/20260810/GV7474291_1786354592342.jpg
Binary files differ
--
Gitblit v1.9.1