| | |
| | | import { InjectEntityModel } from '@midwayjs/typeorm'; |
| | | import { Repository } from 'typeorm'; |
| | | |
| | | |
| | | interface Ys7TokenData { |
| | | accessToken: string; |
| | | /** 萤石返回的毫秒级过期时间戳 */ |
| | |
| | | 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 }), |
| | |
| | | timeout: 8000, |
| | | } |
| | | ); |
| | | |
| | | if (String(res.code) !== '200') { |
| | | throw new Error(`萤石云获取 Token 失败:${res.msg}(code: ${res.code})`); |
| | | } |
| | |
| | | accessToken: res.data.accessToken, |
| | | expireTime: res.data.expireTime, |
| | | }; |
| | | |
| | | // 写回系统参数表(Cool-Admin 自带 upsert) |
| | | const token = await this.baseSysParamEntity.findOneBy({ keyName: 'ys7.accessToken' }); |
| | | this.baseSysParamEntity.save({ |
| | | ...token, |
| | |
| | | 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); |
| | | } |
| | | |
| | | } |