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(), 'upload', '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(), 'upload'), dest)
|
.replace(/\\/g, '/');
|
const localUrl = `/upload/${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. 获取所有萤石云固定监控设备的iotCode(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(), 'upload', '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;
|
}
|
|
// ---------- 工具:GET 下载文件到本地 ----------
|
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();
|
}
|
|
}
|