import { Provide, Inject } from '@midwayjs/core';
|
import { BaseService } from '@cool-midway/core';
|
import axios from 'axios';
|
import { BaseSysParamEntity } from '../../base/entity/sys/param';
|
import { InjectEntityModel } from '@midwayjs/typeorm';
|
import { Repository } from 'typeorm';
|
|
|
interface Ys7TokenData {
|
accessToken: string;
|
/** 萤石返回的毫秒级过期时间戳 */
|
expireTime: number;
|
}
|
|
@Provide()
|
export class Ys7Service extends BaseService {
|
@InjectEntityModel(BaseSysParamEntity)
|
baseSysParamEntity: Repository<BaseSysParamEntity>;
|
|
/** 内存并发锁 */
|
private refreshing: Promise<Ys7TokenData> | null = null;
|
|
/**
|
* 对外暴露:拿 token(自动读缓存 / 续签)
|
*/
|
async getAccessToken() {
|
const token = await this.baseSysParamEntity.findOneBy({ keyName: 'ys7.accessToken' });
|
if (
|
!token ||
|
!token.updateTime ||
|
Date.now() - new Date(token.updateTime).getTime() > 6 * 24 * 60 * 60 * 1000
|
) {
|
await this.fetchAndSaveRemote();
|
}
|
}
|
|
/** 真正调萤石 */
|
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 }),
|
{
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
timeout: 8000,
|
}
|
);
|
if (String(res.code) !== '200') {
|
throw new Error(`萤石云获取 Token 失败:${res.msg}(code: ${res.code})`);
|
}
|
|
const tokenData: Ys7TokenData = {
|
accessToken: res.data.accessToken,
|
expireTime: res.data.expireTime,
|
};
|
const token = await this.baseSysParamEntity.findOneBy({ keyName: 'ys7.accessToken' });
|
this.baseSysParamEntity.save({
|
...token,
|
data: tokenData.accessToken,
|
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
|
* - 调用萤石云空间创建接口:https://open.ys7.com/api/service/open/storage/engine/space
|
* - 接口说明地址:https://open.ys7.com/help/5236
|
* - 将创建好的存储空间ID存入参数:ys7.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] 萤石侧已存在同名空间,spaceId=%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. 组装参数(PUT 使用 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}(code: ${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 请求(body 使用 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}(code: ${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] ✅ 找到已存在的空间:spaceId=%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: '萤石云抓图空间ID',
|
dataType: 1,
|
data: spaceId,
|
updateTime: new Date(),
|
});
|
} else {
|
e.data = spaceId;
|
e.updateTime = new Date();
|
}
|
await this.baseSysParamEntity.save(e);
|
}
|
|
}
|