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