全量提交: KMS适配器终检修复+warehouse P0修复+MC4认证修复+网关B路由+接口文档+代码审核报告

This commit is contained in:
2026-06-04 04:07:32 +08:00
parent fa170e55a9
commit 29e12a235a
11 changed files with 1706 additions and 377 deletions
+608
View File
@@ -0,0 +1,608 @@
# warehouse 客户端代码深度审核报告(含完整修复方案)
> 日期: 2026-06-04 | 项目: warehouse/ | 扫描: 38 源文件, ~12,000 行 | 问题: 70 项
---
## 1. `api/http.js` — 9 项
### H1 [🔴] `lang_storage_key` 未定义
**位置**: 第 134 行
```javascript
function setHeaderLang(_header) {
let langType = localStorage.getItem(lang_storage_key) // ← 未定义!
}
```
**后果**: 运行时报错 `lang_storage_key is not defined`,语言功能完全失效。
**修复** — 在函数上方加常量:
```javascript
const lang_storage_key = 'lang'
```
### H2 [🔴] `replaceToken` 未定义
**位置**: 第 107 行
```javascript
function checkResponse(res) { if (res.headers.vol_exp == '1') { replaceToken() } }
```
**后果**: Token 过期后调用未定义函数,静默失败。
**修复** — 在 `checkResponse` 前追加:
```javascript
function replaceToken() {
store.dispatch('clearUserInfo')
window.location.href = '/login'
}
```
**跨文件影响**: 需确认 `store/index.js``clearUserInfo` mutation(已存在)。
### H3 [🔴] `toLogin` 未定义
**位置**: 第 77 行
```javascript
if (error.response.status == '401') { toLogin() }
```
**修复** — 在文件顶部 import router 后追加:
```javascript
import router from '@/router'
function toLogin() { router.push('/login') }
```
**跨文件影响**: 需确认 `router/index.ts` 导出 router 实例。
### H4 [🟠] 降级地址硬编码
**位置**: 第 25-33 行
```javascript
axios.defaults.baseURL = 'http://192.168.3.108:9100/'
dataViewUrl = 'http://192.168.3.108:9200/'
```
**修复**:
```javascript
axios.defaults.baseURL = window.location.origin
dataViewUrl = (window as any).apiConfig?.dataViewUrl || window.location.origin
```
### H5 [🟠] `get()` 参数 `param` 未使用
**位置**: 第 176 行
```javascript
function get(url, param, loading, config) { axios.get(url, config) }
```
**修复**:
```javascript
function get(url, param, loading, config) {
const cfg = { ...config }
if (param) cfg.params = param
// ... 其余不变
axios.get(url, cfg).then(...)
}
```
### H6 [🟡] `closeLoading` 冗余
**位置**: 第 92-101 行
```javascript
if (loadingInstance) loadingInstance.close()
if (loadingStatus) { loadingStatus = false; if (loadingInstance) loadingInstance.close() }
```
**修复**:
```javascript
loadingStatus = false
loadingInstance?.close()
```
### H7 [🟡] `alert()` 弹窗
**位置**: 第 199 行
```javascript
alert('http.js未配置大屏url地址')
```
**修复**:
```javascript
import { ElMessage } from 'element-plus'
ElMessage.error('未配置大屏URL地址')
```
### H8 [🟡] 无类型安全
**修复** — 在文件头部加 JSDoc(不改变运行时):
```typescript
/**
* @template T
* @param {string} url
* @param {object} [params]
* @param {boolean|string} [loading]
* @param {object} [config]
* @returns {Promise<T>}
*/
function post(url, params, loading, config) { ... }
```
### H9 [⚪] 文件臃肿 (409行)
**修复** — 拆为 3 文件:
- `api/http-client.ts` — Axios 实例 + baseURL + 拦截器 (80行)
- `api/http-auth.ts` — getToken/replaceToken/toLogin (40行)
- `api/http-loading.ts` — showLoading/closeLoading (20行)
`api/http.js` 改为:
```javascript
import { createHttpClient } from './http-client'
export default createHttpClient()
```
---
## 2. `api/gateway.ts` — 3 项
### GW1 [🟠] 网关地址硬编码
**位置**: 第 5 行 `const GW_BASE = 'http://192.168.3.108:5100'`
**修复**:
```typescript
const GW_BASE = (window as any).apiConfig?.gatewayUrl || 'http://localhost:5100'
```
### GW2 [🟡] `fetch()` 无超时
**修复** — 完整重写 `gwGet``gwPost` 同理):
```typescript
export async function gwGet(url: string, timeoutMs = 10000): Promise<any> {
const ctrl = new AbortController()
const timer = setTimeout(() => ctrl.abort(), timeoutMs)
try {
const resp = await fetch(`${GW_BASE}${url}`, { signal: ctrl.signal })
if (!resp.ok) throw new Error(`网关请求失败: ${resp.status}`)
return resp.json()
} finally { clearTimeout(timer) }
}
```
### GW3 [🟡] 模型映射混入API层
**修复** — 新建 `warehouse/src/services/cameraService.ts`:
```typescript
import { gwGet, type Camera, type StandardDevice } from '@/api/gateway'
export function toCamera(d: StandardDevice): Camera { ... }
export async function fetchCameras(adapter: string): Promise<Camera[]> { ... }
```
**跨文件影响**: `Live.vue`, `VideoWall.vue`, `History.vue` 的 import 改为 `from '@/services/cameraService'`
---
## 3. `api/buttons.js` — 1 项
### B1 [🟡] Element UI 旧版图标语法
```javascript
icon: 'el-icon-search'
```
Element Plus ≥2.0 已废弃字符串图标。
**修复** — 改为组件引用:
```javascript
import { Search, Plus, Edit, DocumentCopy, Delete, Check, Finished, Top, Bottom, Printer } from '@element-plus/icons-vue'
import { shallowRef } from 'vue'
const buttons = [
{ name:'查询', icon: shallowRef(Search), ... },
{ name:'新建', icon: shallowRef(Plus), ... },
// ... 其余同理
]
```
**跨文件影响**: 使用 buttons 的组件需确认其渲染逻辑支持组件引用(通常通过 `<component :is="btn.icon" />`)。
---
## 4. `api/permission.js` — 2 项
### PE1 [🟠] 权限缺失时静默放行
**位置**: 第 24 行
```javascript
if (!permission) { permission = { permission: ['Search'] } }
```
**修复**:
```javascript
if (!permission) { return [] }
```
空数组使所有按钮不可见(安全默认)。
### PE2 [🟡] `to401` 空实现
**修复**:
```javascript
import router from '@/router'
function to401() { router.push('/401') }
```
---
## 5. `router/index.ts` — 5 项
### R1 [🟠] 40+ 条路由指向同一组件
所有菜单子项渲染 `Index.vue`,用户看到重复空白页。
**修复** — 3 步方案:
1. 为已实现的页面保留独立路由(VideoWall/AlarmRecord/AccessRecord 等已存在)
2. 其余指向一个占位组件:
```typescript
{ path: "/index/goods/list", component: () => import("@/view/Placeholder.vue") }
```
3. `Placeholder.vue`:
```vue
<template><el-empty description="功能开发中" /></template>
```
### R2 [🟡] `/new-dv` 不要求认证
```typescript
{ path:"/new-dv", meta:{ requiresAuth: false } }
```
**修复**: 改为 `requiresAuth: true`
### R3 [🟡] 缺少 beforeEach 守卫
**修复** — 在 `router/index.ts` 末尾追加:
```typescript
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth !== false) {
const token = localStorage.getItem('token')
if (token) next()
else next('/login')
} else { next() }
})
```
### R4 [⚪] `@ts-ignore` 绕过 store 类型
**修复**: 创建 `warehouse/src/types/store.d.ts`:
```typescript
declare module '@/store' {
const store: import('vuex').Store<any>
export default store
}
```
### R5 [⚪] 仓库/货物/出入库路由 20+ 条未使用
**修复**: 删除。若后续需要可从 git 恢复。
---
## 6. `main.ts` — 1 项
### M1 [🟡] 暗色模式写死
```typescript
app.use(ElementPlus, { dark: true })
```
**修复**:
```typescript
const dark = localStorage.getItem('dark-mode') !== 'false'
app.use(ElementPlus, { dark })
```
---
## 7. `view/index.js` — 6 项
### SI1 [🔴] `displayedMessageIds` Set 无限增长
**位置**: 第 8 行 `let displayedMessageIds = new Set()`
**后果**: 运行数天后 Set 包含成千上万条 ID → 内存泄漏。
**修复** — 改为 LRU 缓存(保留最近 500 条):
```javascript
class LruSet {
#set = new Set()
#max = 500
add(v) { if (this.#set.has(v)) return; this.#set.add(v); if (this.#set.size > this.#max) { this.#set.delete(this.#set.values().next().value) } }
has(v) { return this.#set.has(v) }
}
const displayedMessageIds = new LruSet()
```
### SI2 [🟠] 消息队列 3s 延迟堆积
**修复**: 删除 `messageQueue`/`processMessageQueue`,直接调 `receive`:
```javascript
connection.on("ReceiveHomePageMessage", function (data) {
if (displayedMessageIds.has(data.id)) return
displayedMessageIds.add(data.id)
receive(data)
})
```
### SI3 [🟠] connection 启动无重试
`connection.start().catch(...)` 失败后永远不重试。
**修复**:
```javascript
async function startWithRetry(retries = 5) {
for (let i = 0; i < retries; i++) {
try { await connection.start(); return }
catch (e) { console.warn(`SignalR retry ${i+1}/${retries}: ${e.message}`); await new Promise(r => setTimeout(r, 2000)) }
}
console.error('SignalR connection failed after retries')
}
startWithRetry()
```
### SI4 [🟡] `console.log` 残留 6 处
**修复**: 全部替换为 `if (import.meta.env.DEV) console.log(...)`
### SI5 [🟡] `receive` 被双重调用
`processMessageQueue``ReceiveHomePageMessage` 都调了 `receive`
**修复**: SI2 删除了 `processMessageQueue` 后此问题自动消除。
### SI6 [🟡] 用户信息获取失败静默
**修复**:
```javascript
http.post("api/user/GetCurrentUserInfo").then(...).catch(error => {
console.error('获取用户信息失败,SignalR未启动:', error)
})
```
---
## 8-28: 视图层文件(21 个 Vue 文件)
### 通用修复模式(适用于所有 Mock 页面)
以下 17 个页面全 Mock —— 统一修复方案:
```
AccessRecord.vue, AlarmRecord.vue, EmergencyAlarmRecord.vue,
KeyInfo.vue, KeyApply.vue, EnvVarManagement.vue,
PatrolLog.vue, ScheduleManagement.vue, PathManagement.vue,
DroneManagement.vue, dataview.vue, CarApply.vue, CarManager.vue,
DeviceStatus.vue(V), VisitorsManagement.vue, VisitCarManagement.vue
```
**统一修复**: 每个页面增加网关 API 调用骨架,Mock 数据降级为 fallback:
```typescript
// 以 AlarmRecord.vue 为例
import { gwGet } from '@/api/gateway'
const fetchData = async () => {
try {
const data = await gwGet('/api/gateway/alarms/Owl:main?page=1&size=100')
alarmData.value = data.items.map((a: StandardAlarm) => ({
id: a.alarmId, alarmTime: a.occurTime, deviceName: a.title,
location: a.deviceId, status: a.status, imageUrl: '/images/placeholder.png'
}))
} catch {
alarmData.value = getMockAlarmData() // fallback
}
}
```
### 单个页面专项修复
**DataView.vue** (1840行):
- DV1: 告警等级 → 改用 `level` 字段或网关 `StandardAlarm.level`
- DV2: `setTimeout``clearTimeout`:
```typescript
const timers = new Set<number>()
onBeforeUnmount(() => timers.forEach(t => clearTimeout(t)))
// 使用时: timers.add(setTimeout(...))
```
- DV6: `originalData: JSON.parse(JSON.stringify(data))` 避免循环引用
**DeviceInfo.vue** (1300行):
- DI1: 对接网关 B4 获取真实在线率
- DI2: `randomVideoImage` → `gwGet('/api/gateway/streams/.../live')`
- DI3: `handleTurnOn` → `gwPost('/api/gateway/realtime/.../control', { deviceId, pointIndex, value })`
**Live.vue** / **VideoWall.vue** / **History.vue**:
- LV2/VH2: `setInterval` 加清理:
```typescript
const timer = setInterval(updateTime, 1000)
onBeforeUnmount(() => clearInterval(timer))
```
**Main.vue**:
- MA1: icon 改为 `@element-plus/icons-vue` 组件引用
- MA2: Menu 配置提取:
```typescript
const menuItems = [
{ index:'1', icon: VideoCamera, label:'视频监控', children:[
{ index:'/index/video/videowall', label:'视频墙' }
]}
]
```
- MA5: 统一 `import { useMapStore } from '@/stores/mapStore'`
**Map.vue**:
- MP1: `const m = /#\/(\d+)/.exec(location.hash); const mapId = m?.[1] || 'default'`
**Index.vue**:
- IN1: `const mapId = import.meta.env.VITE_MAP_ID || 'default'`
---
## 29-31: 组件层(3 文件)
### Filter.vue [🟡]
**console.log 8 处**: 同 SI4,改为条件输出。
**硬件设备图标映射** 提取为常量:
```typescript
const DEVICE_ICONS: Record<string, string> = {
'摄像头':'/images/dataview/deviceinfo/camera.png',
'门禁':'/images/dataview/deviceinfo/access.png',
// ...
}
```
### Fence.vue [🟡]
**硬编码仓库名**: `['1号库','2号库','12号库']`
**修复**: 从 store 或配置注入:
```typescript
const warehouseNames = inject('warehouseNames', ['1号库'])
const inFencePoints = store.polygonDataAll.filter(p => warehouseNames.includes(p.name))
```
---
## 32-34: 状态管理层(3 文件)
### SM1 [🔴] 两份 `useMapStore` 同名
**文件**: `stores/mapStore.js` 和 `store/useMapStore.js` 都 `defineStore('map', ...)`
**修复** — 选择一份保留(推荐 `stores/mapStore.js` 功能更全),另一份删除:
```bash
git rm warehouse/src/store/useMapStore.js
```
**跨文件影响** — 修改以下文件的 import:
- `Map.vue` line 9: `'../store/useMapStore'` → `'../stores/mapStore'`
- `Index.vue` line 10: `'../stores/mapStore'` (已对)
- `Fence.vue` line 4: `'../store/useMapStore'` → `'../stores/mapStore'`
- `Filter.vue` line 3: `'../store/useMapStore'` → `'../stores/mapStore'`
- `DataView.vue` line 10: `'../stores/mapStore'` (已对)
### ST1 [🟠] `getServiceList` getter 忽略参数
**位置**: `store/index.js`
```javascript
getServiceList: (state) => (path) => { return state.serviceList || [] }
```
**修复**: 如果不需要按 path 过滤则简化为:
```javascript
getServiceList: (state) => state.serviceList || []
```
### ST3 [🟡] `test` mutation 返回 `113344`
**位置**: `store/index.js` line 49
```javascript
test(state) { return 113344 } // 调试代码
```
**修复**: 删除此 mutation。
### ST4 [⚪] `setPermission` 数组 push 会叠加
```javascript
if (data instanceof Array) { state.permission.push(...data) }
```
每次调用追加而非替换。**修复**: 始终替换 `state.permission = data`。
---
## 35: 项目结构 — 4 项
### PS1/PS2 [🟡] 过期副本文件
```bash
git rm warehouse/src/view/DataView\ copy.vue
git rm warehouse/src/view/Map.vue.bak
```
### PS3 [🟡] 文档放在源码目录
```bash
mkdir -p warehouse/doc
mv warehouse/src/view/intercom/TODO_*.md warehouse/doc/
```
### PS4 [🟠] Vuex + Pinia 共存
**修复** — 迁移 Vuex → Pinia:
1. 新建 `stores/authStore.js`(替代 Vuex 的 userInfo/token/permission
2. 迁移 `store/index.js` 中的 `setUserInfo`/`getToken`/`getPermission` 到 Pinia
3. `npm uninstall vuex`
4. 修改 `http.js`/`Login.vue`/`permission.js` 从 `@/stores/authStore` 导入
---
## 36: 全局问题 — 7 项
### G1 [🟠] Mock 覆盖率 86%
22 页面中仅 3 个对接网关。**分 4 阶段对接**:
| 阶段 | 页面 | 网关接口 | 预计 |
|:--:|------|------|:--:|
| 1 | 告警页 (AlarmRecord/EmergencyAlarm) | B8 (GET /alarms) | 2h |
| 2 | 环境变量 (EnvVarManagement) | B4 (GET /realtime) | 2h |
| 3 | 门禁/钥匙 (AccessRecord/KeyInfo) | B2 (GET /devices) + B11 (GET /logs) | 3h |
| 4 | 巡更/无人机/车辆/访客 | B2 设备列表 | 1h |
### G2 [🟡] 硬编码IP散布6+文件
**修复** — 创建 `.env.development`:
```
VITE_GATEWAY_URL=http://localhost:5100
VITE_VOLPRO_URL=http://localhost:9100
```
各文件改为:
```typescript
const GW_BASE = import.meta.env.VITE_GATEWAY_URL || 'http://localhost:5100'
```
### G3 [🟡] JS/TS 混用
**修复**: 7 个 `.js` → `.ts`(逐文件迁移,不改运行时逻辑):
```
api/http.js → api/http.ts
api/buttons.js → api/buttons.ts
api/permission.js → api/permission.ts
view/index.js → view/index.ts
stores/mapStore.js → stores/mapStore.ts
router/viewGird.js → router/viewGird.ts
```
### G4 [🟡] `window.*` 全局变量
**修复**: `window.$map` → Store 管理(已存在 `mapStore.setMap`
```typescript
// Map.vue: 删除 window.$map = map,保留 store.setMap(map)
// 其他文件: 改 window.$map → const { map } = useMapStore()
```
### G5 [🟡] `console.log` 残留 30+ 处
**修复** — 一行全局替换:
```bash
# 将所有 console.log 改为条件输出
find warehouse/src -name "*.vue" -o -name "*.ts" -o -name "*.js" | xargs sed -i 's/console\.log(/if(import\.meta\.env\.DEV)console.log(/g'
```
### G6 [⚪] 无全局错误边界
**修复** — `main.ts`:
```typescript
app.config.errorHandler = (err) => {
console.error('Global error:', err)
ElMessage.error('系统异常,请刷新页面')
}
```
### G7 [⚪] `counter.ts` 模板残留
**修复**: 删除 `warehouse/src/stores/counter.ts`。
---
## 执行优先级
| 批次 | 项 | 文件 | 预计 | 影响 |
|:--:|------|------|:--:|------|
| 🔴P0 | H1+H2+H3+SI1+SM1 | 5 | 2h | 修复运行时错误 |
| 🟠P1 | SI3+PE1+R1+R3+G1(告警) | 6 | 4h | 功能可用性 |
| 🟡P2 | GW1+GW2+H5+H6+DV1+LV2 | 6 | 3h | 代码质量 |
| ⚪P3 | PS+console+G6+CT1 | 5 | 1h | 整洁性 |
> **总计**: 70 项 / 预估 10h。P0 批 5 项必须在联调前完成。
@@ -0,0 +1,424 @@
# VolPro.WebApi 网关相关接口文档
> **版本**: 1.0
> **日期**: 2026-06-04
> **基址**: `http://{host}:{port}`(默认 `http://localhost:9100`
> **内容类型**: `application/json`
> **接口来源**: `api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/`
---
## 目录
1. [A 组 — 网关注册通信](#1-a-组--网关注册通信)
- A1: 网关注册 `POST /api/gateway/register`
- A2: 心跳上报 `POST /api/gateway/heartbeat`
- A3: 设备同步 `POST /api/gateway/sync/devices`
- A4: 告警同步 `POST /api/gateway/sync/alarms`
2. [设备管理](#2-设备管理)
- 区域树 `GET /api/DeviceManager/GetRegionTree`
- 点位设备 `GET /api/DeviceManager/GetDevicesByPoint`
3. [定时任务](#3-定时任务)
- 设备同步任务 `POST /api/task/syncDevices`
- 心跳监控任务 `POST /api/task/heartbeatMonitor`
- 实时轮询任务 `POST /api/task/realtimePoll`
- 规则引擎任务 `POST /api/task/ruleEngine`
4. [错误代码](#4-错误代码)
---
## 1. A 组 — 网关注册通信
> A 组接口是 VolPro 向网关暴露的管理端点,由网关主动调用。所有 A 组使用 `[AllowAnonymous]` + `NodeToken` 二次认证,不走 VolPro JWT 体系。
>
> **实现文件**: `Controllers/Warehouse/Partial/gateway_nodesController.cs`
### A1: 网关注册(Upsert
```
POST /api/gateway/register
```
网关启动时调用,注册自身节点信息。NodeCode 已存在则更新,不存在则插入。返回当前网关的已有设备列表供网关对比差异。
**请求头**: `Content-Type: application/json`
**请求体 (GatewayRegisterRequest)**:
| 字段 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `NodeCode` | string | ✅ | 网关节点编码,如 `gw-31ku` |
| `Token` | string | ✅ | 认证令牌(由环境变量 `SECMPS_GATEWAY_TOKEN` 注入) |
| `AdapterTypes` | string | ✅ | 适配器类型列表(逗号分隔),如 `Owl:main,MC4:31ku` |
| `BaseUrl` | string | ✅ | 网关自身地址,如 `http://192.168.1.10:5100` |
**返回参数**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `nodeId` | int | 网关节点 IDbase_device.NodeId 外键) |
| `devices` | array | 该网关已有的设备列表 |
**devices[] 条目**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `deviceId` | int | 设备自增 ID |
| `deviceName` | string | 设备名称 |
| `adapterCode` | string | 适配器编码 |
| `sourceId` | string | 子系统设备原始 ID |
| `deviceCategory` | string | 设备种类 |
| `deviceGroup` | string | 设备分组 |
| `isParent` | string | 是否父设备("是"/"否" |
| `isOnline` | string | 是否在线("在线"/"离线" |
| `extraData` | string? | 扩展数据 JSON |
**返回示例**:
```json
{
"nodeId": 1,
"devices": [
{ "deviceId": 10, "deviceName": "NVR-1", "adapterCode": "Owl:main", "sourceId": "nvr_001", "deviceCategory": "硬盘录像机", "deviceGroup": "视频设备", "isParent": "是", "isOnline": "在线" }
]
}
```
**错误响应**:
| HTTP | 说明 |
|:---:|------|
| 400 | `NodeCode``Token` 为空 |
| 401 | `NodeToken` 不匹配(已有节点 Token 变更) |
---
### A2: 心跳上报
```
POST /api/gateway/heartbeat
```
网关每 15s 调用一次,更新 `LastHeartbeat` 字段。连续失败 ≥3 次(45s)后网关自动触发 A1+A3 重注册。
**请求体 (GatewayHeartbeatRequest)**:
| 字段 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `NodeCode` | string | ✅ | 网关节点编码 |
| `Token` | string | ✅ | 认证令牌 |
**返回参数**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `status` | string | 固定 `"ok"` |
| `serverTime` | string | 服务器时间 (`yyyy-MM-dd HH:mm:ss`) |
**错误响应**:
| HTTP | 说明 |
|:---:|------|
| 400 | `NodeCode``Token` 为空 |
| 401 | NodeCode+Token 组合不匹配 |
---
### A3: 设备数据同步
```
POST /api/gateway/sync/devices
```
网关每次设备变更后调用,将全量设备列表推送到 VolPro。采用字段分治策略:首次入库写全量,后续只更新网关字段。
**请求体 (SyncDevicesRequest)**:
| 字段 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `NodeCode` | string | ✅ | 网关节点编码 |
| `Token` | string | ✅ | 认证令牌 |
| `Devices` | array | ✅ | 设备列表 |
**Devices[].SyncDeviceItemDto**:
| 字段 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `AdapterCode` | string | ✅ | 适配器编码 |
| `SourceId` | string | ✅ | 子系统设备原始 ID |
| `Name` | string? | ❌ | 设备名称 |
| `Category` | string? | ❌ | 设备种类 |
| `Group` | string? | ❌ | 设备分组 |
| `IsParent` | bool | ❌ | 是否父设备 |
| `ParentSourceId` | string? | ❌ | 父设备 SourceId(用于解析 ParentDeviceId |
| `IsOnline` | bool | ❌ | 是否在线 |
| `IpAddress` | string? | ❌ | IP 地址 |
| `Port` | int? | ❌ | 端口号 |
| `ExtraDataJson` | string? | ❌ | 扩展数据 JSON |
**返回参数**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `added` | int | 新增设备数 |
| `updated` | int | 更新设备数 |
| `removed` | int | 固定 `0`(当前版本不移除下线设备) |
**错误响应**:
| HTTP | 说明 |
|:---:|------|
| 400 | `NodeCode``Token` 为空 |
| 401 | NodeCode+Token 认证失败 |
---
### A4: 告警数据同步
```
POST /api/gateway/sync/alarms
```
网关检测到新告警后调用,推送告警列表到 VolPro。通过 `SourceAlarmId` 去重(同一告警不重复入库)。
**请求体 (SyncAlarmsRequest)**:
| 字段 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `NodeCode` | string | ✅ | 网关节点编码 |
| `Token` | string | ✅ | 认证令牌 |
| `Alarms` | array | ✅ | 告警列表 |
**Alarms[].SyncAlarmItemDto**:
| 字段 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `SourceAlarmId` | string | ✅ | 子系统告警唯一 ID(用于去重) |
| `DeviceSourceId` | string | ✅ | 关联设备 SourceId(用于映射 DeviceId |
| `AdapterCode` | string | ✅ | 适配器编码 |
| `Level` | string | ✅ | 告警等级:`提示`/`普通`/`重要`/`紧急` |
| `Desc` | string | ✅ | 告警描述 |
| `Value` | double? | ❌ | 告警实际值 |
| `StartTime` | string | ✅ | 告警发生时间 |
**返回参数**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `added` | int | 新增告警数 |
**错误响应**:
| HTTP | 说明 |
|:---:|------|
| 400 | `NodeCode``Token` 为空 |
| 401 | NodeCode+Token 认证失败 |
---
## 2. 设备管理
> **实现文件**: `Controllers/Warehouse/Partial/base_deviceController.cs`
### 区域树
```
GET /api/DeviceManager/GetRegionTree
```
返回 区域→点位 的层级结构,供管理端左侧树形控件使用。
**请求参数**: 无
**返回参数**: `TreeNode[]`
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string | 节点 ID`r_{regionId}``p_{pointId}` |
| `label` | string | 节点显示名称 |
| `type` | string | 节点类型:`region`(区域) 或 `point`(点位) |
| `deviceCount` | int | 该节点下的设备数量 |
| `children` | array? | 子节点列表(仅 region 节点有) |
**返回示例**:
```json
[
{
"id": "r_1", "label": "库房A区", "type": "region", "deviceCount": 3,
"children": [
{ "id": "p_10", "label": "温湿度监测点1", "type": "point", "deviceCount": 5 },
{ "id": "p_11", "label": "门禁点1", "type": "point", "deviceCount": 2 }
]
}
]
```
---
### 点位设备列表
```
GET /api/DeviceManager/GetDevicesByPoint?pointId={pointId}&page={page}&size={size}
```
获取指定点位下的设备列表(含子设备),支持分页。
**请求参数**:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|:--:|------|------|
| `pointId` | int | ✅ | — | 点位 ID |
| `page` | int | ❌ | 1 | 页码 |
| `size` | int | ❌ | 20 | 每页条数 |
**返回参数**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `items` | array | 设备列表 |
| `total` | int | 总设备数 |
**items[] 条目**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `deviceId` | int | 设备自增 ID |
| `deviceName` | string | 设备名称 |
| `adapterCode` | string | 适配器编码 |
| `sourceId` | string | 子系统设备原始 ID |
| `deviceCategory` | string | 设备种类 |
| `deviceGroup` | string | 设备分组(`视频设备`/`IoT设备`/`门禁设备` |
| `isParent` | string | 是否父设备("是"/"否" |
| `parentDeviceId` | int? | 父设备 ID |
| `isOnline` | string | 是否在线("在线"/"离线" |
| `ipAddress` | string? | IP 地址 |
| `port` | int? | 端口号 |
| `location` | string? | 位置描述 |
| `extraData` | string? | 扩展数据 JSON |
| `lastSyncTime` | DateTime? | 最后同步时间 |
| `mapModelId` | string? | 3D 地图模型 ID |
| `mapModelScale` | decimal? | 模型缩放比例 |
| `mapModelRotation` | string? | 模型旋转参数 JSON |
| `enable` | string | 启用状态("启用"/"停用" |
---
## 3. 定时任务
> VolPro 框架通过 `Sys_QuartzOptions` 表配置 URL+Cron 定时调用。每个端点加 `[ApiTask]` 属性以允许框架匿名调用。
>
> **实现文件**: `Controllers/Warehouse/TaskController.cs`
### 设备同步任务
```
POST /api/task/syncDevices
```
遍历所有在线网关,触发全量设备同步至 VolPro。
**Cron**: `0 */5 * * * ?`(每 5 分钟)
**请求参数**: 无
**返回参数**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `time` | DateTime | 执行时间 |
| `status` | string | 固定 `"ok"` |
**错误响应**:
| HTTP | 说明 |
|:---:|------|
| 500 | `gateway_nodesService` 未注册 |
---
### 心跳监控任务
```
POST /api/task/heartbeatMonitor
```
扫描心跳超时 ≥30s 的网关节点,标记离线并级联标记该节点下所有设备离线。
**Cron**: `0/15 * * * * ?`(每 15 秒)
**请求参数**: 无
**返回**: 同设备同步任务
---
### 实时轮询任务
```
POST /api/task/realtimePoll
```
轮询所有在线 MC4 IoT 设备的实时值,写入 `iot_devicedata` 表。
**Cron**: `0/10 * * * * ?`(每 10 秒)
**请求参数**: 无
**返回**: 同设备同步任务
---
### 规则引擎任务
```
POST /api/task/ruleEngine
```
加载启用规则 → 从网关批量获取实时值 → 逐规则评估条件 → 触发动作(控制/告警/通知)。
**Cron**: `0/10 * * * * ?`(每 10 秒)
**请求参数**: 无
**返回**: 同设备同步任务
**当前状态**: 桩实现。`RuleEngineService.EvaluateAllAsync()` 抛出 `NotImplementedException`,需先执行 SQL ALTER TABLE + 代码生成器。
---
## 4. 错误代码
### HTTP 状态码
| 状态码 | 含义 | 触发条件 |
|:---:|------|------|
| 200 | OK | 请求成功 |
| 400 | Bad Request | 必填参数缺失(`NodeCode`/`Token`/`pointId` |
| 401 | Unauthorized | A 组接口 NodeToken 认证失败 |
| 500 | Internal Server Error | 服务未注册或内部异常 |
### A 组认证错误
所有 A 组接口在认证失败时返回:
```json
{ "message": "认证失败:Token 无效" }
```
```json
{ "message": "认证失败" }
```
### 定时任务错误
定时任务在服务未注册时返回:
```json
{ "error": "服务未注册: gateway_nodesService" }
```
---
> **接口总数**: 10 个端点(A 组 4 + 设备管理 2 + 定时任务 4)
> **实现位置**: `api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/`
@@ -0,0 +1,550 @@
# IntegrationGateway B 组接口文档
> **版本**: 1.0
> **日期**: 2026-06-04
> **基址**: `http://{host}:{port}`(默认 `http://localhost:5100`
> **内容类型**: `application/json`(除标注外)
> **认证**: 可选 `X-Gateway-Key` 请求头(与 Gateway 段配置一致时生效)
> **通用错误码**: 见 §5
---
## 目录
1. [健康检查](#1-健康检查) — B1
2. [设备管理](#2-设备管理) — B2, B3, B3-sync
3. [视频与流媒体](#3-视频与流媒体) — B6a, B6b, 截图, B7
4. [IoT 实时数据](#4-iot-实时数据) — B4, B4-batch, B5
5. [告警管理](#5-告警管理) — B8, B9-confirm, B9-end
6. [录像查询](#6-录像查询)
7. [设备控制 (通用)](#7-设备控制-通用) — B10
8. [业务记录查询](#8-业务记录查询) — B11
9. [数据同步](#9-数据同步) — B12, B13
10. [错误代码](#10-错误代码)
---
## 1. 健康检查
### B1: 查询所有适配器健康状态
```
GET /api/gateway/health
```
**请求参数**: 无
**返回参数**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `[].adapterCode` | string | 适配器编码,如 `Owl:main``MC4:31ku``KMS:main` |
| `[].displayName` | string | 人类可读的适配器名称 |
| `[].healthy` | bool | `true` = 适配器在线,`false` = 离线或不可达 |
| `[].capabilities` | object | 适配器能力声明 |
**capabilities 字段**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `hasFlatDevices` | bool | 是否支持扁平设备列表 |
| `hasOwnDeviceTree` | bool | 是否支持层级对象树 |
| `hasStreams` | bool | 是否支持视频取流 |
| `hasPoints` | bool | 是否支持 IoT 实时点位 |
| `hasAlarms` | bool | 是否支持告警查询 |
| `hasRecordings` | bool | 是否支持录像查询 |
**返回示例**:
```json
[
{
"adapterCode": "Owl:main",
"displayName": "Owl (Owl:main)",
"healthy": true,
"capabilities": { "hasFlatDevices": true, "hasStreams": true, "hasAlarms": true, "hasRecordings": true }
}
]
```
---
## 2. 设备管理
### B2: 分页获取扁平设备列表
```
GET /api/gateway/devices?adapter={adapterCode}&page={page}&size={size}&keyword={keyword}
```
**请求参数**:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|:--:|------|------|
| `adapter` | string | ✅ | — | 适配器编码,如 `Owl:main` |
| `page` | int | ❌ | 1 | 页码(从 1 开始) |
| `size` | int | ❌ | 20 | 每页条数 |
| `keyword` | string | ❌ | null | 设备名称模糊搜索 |
**返回参数**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `items` | array | 设备列表 |
| `total` | int | 总设备数 |
**items[].StandardDevice**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `sourceId` | string | 子系统设备原始 ID |
| `name` | string | 设备名称 |
| `category` | string | 设备种类(如 `硬盘录像机``摄像头``智能钥匙柜``钥匙位` |
| `group` | string | 设备分组(`视频设备`/`IoT设备`/`门禁设备` |
| `isParent` | bool | 是否父设备(含下级子设备) |
| `parentSourceId` | string? | 上级设备 SourceId |
| `isOnline` | bool | 是否在线 |
| `ipAddress` | string? | IP 地址 |
| `port` | int? | 端口号 |
| `extra` | object? | 子系统特有扩展属性 |
**返回示例**:
```json
{
"items": [
{ "sourceId": "locker_25", "name": "10位智能公共钥匙柜", "category": "智能钥匙柜", "group": "门禁设备", "isParent": true, "isOnline": true },
{ "sourceId": "lockhole_25_1", "name": "仓库大门钥匙", "category": "钥匙位", "group": "门禁设备", "isParent": false, "isOnline": true, "parentSourceId": "locker_25" }
],
"total": 11
}
```
### B3: 获取层级对象树
```
GET /api/gateway/tree?adapter={adapterCode}
```
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `adapter` | string | ✅ | 适配器编码,如 `MC4:31ku` |
**返回参数**: `DeviceTreeNode[]`
| 字段 | 类型 | 说明 |
|------|------|------|
| `sourceId` | string | 节点 ID |
| `name` | string | 节点名称 |
| `tag` | string | 节点标签(如 `区域`/`设备组`/`IoT设备` |
| `type` | int | 节点类型:1=父节点, 0=叶子节点 |
| `children` | array | 子节点列表(递归) |
| `option` | object? | 扩展配置 |
### B3-sync: 手动触发设备同步
```
POST /api/gateway/devices/sync?adapter={adapterCode}
```
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `adapter` | string | ✅ | 适配器编码 |
**返回参数**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `deviceCount` | int | 同步设备数(扁平设备) |
| `nodeCount` | int | 同步节点数(对象树) |
| `message` | string | 同步结果描述 |
---
## 3. 视频与流媒体
### B6a: 获取实时流地址
```
GET /api/gateway/streams/{adapter}/{deviceId}/live
```
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `adapter` | string | ✅ | 适配器编码,如 `Owl:main` |
| `deviceId` | string | ✅ | 通道 SourceId |
**返回参数 (StreamUrls)**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `wsFlv` | string? | WebSocket-FLV 地址(推荐,低延迟) |
| `httpFlv` | string? | HTTP-FLV 地址 |
| `hls` | string? | HLS (m3u8) 地址 |
| `webrtc` | string? | WebRTC 地址 |
| `rtmp` | string? | RTMP 地址 |
### B6b: 获取录像回放地址
```
GET /api/gateway/streams/{adapter}/{deviceId}/playback?start={start}&end={end}
```
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `adapter` | string | ✅ | 适配器编码 |
| `deviceId` | string | ✅ | 通道 SourceId |
| `start` | DateTime | ✅ | 回放起始时间 (ISO 8601) |
| `end` | DateTime | ✅ | 回放结束时间 (ISO 8601) |
**返回**: 同 `StreamUrls`
### 截图: 获取通道实时截图
```
POST /api/gateway/streams/{adapter}/{deviceId}/snapshot
```
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `adapter` | string | ✅ | 适配器编码 |
| `deviceId` | string | ✅ | 通道 SourceId |
**返回**: JPEG 图片 Base64 或 URL
### B7: 云台控制
```
POST /api/gateway/streams/{adapter}/{deviceId}/ptz
```
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `adapter` | string | ✅ | 适配器编码 |
| `deviceId` | string | ✅ | 通道 SourceId |
**请求体 (PtzRequest)**:
| 字段 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `direction` | string | ❌ | 方向:`up`/`down`/`left`/`right`/`zoom_in`/`zoom_out` |
| `action` | string | ✅ | 动作:`continuous`(持续)/`stop`(停止)/`preset`/`patrol` |
| `speed` | float | ❌ | 速度 (0.1~1.0),默认 0.5 |
---
## 4. IoT 实时数据
### B4: 获取设备实时点位值
```
GET /api/gateway/realtime/{adapter}/{deviceId}
```
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `adapter` | string | ✅ | 适配器编码,如 `MC4:31ku` |
| `deviceId` | string | ✅ | 设备 SourceId |
**返回参数**: `PointValue[]`
| 字段 | 类型 | 说明 |
|------|------|------|
| `pointIndex` | int | 点位索引 |
| `pointName` | string | 点位名称 |
| `value` | decimal | 当前值 |
| `unit` | string? | 单位(如 `℃``%` |
| `updateTime` | DateTime | 更新时间 |
| `interval` | int | 采集间隔(秒) |
### B4-batch: 批量获取实时点位值
```
POST /api/gateway/realtime/{adapter}/batch
```
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `adapter` | string | ✅ | 适配器编码 |
**请求体 (BatchRealtimeRequest)**:
| 字段 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `deviceIds` | string[] | ✅ | 设备 SourceId 列表 |
**返回**: `Dictionary<string, PointValue[]>` — 以 deviceId 为键的实时值字典
### B5: 设备反向控制
```
POST /api/gateway/realtime/{adapter}/control
```
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `adapter` | string | ✅ | 适配器编码 |
**请求体 (ControlRequest)**:
| 字段 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `deviceId` | string | ✅ | 设备 SourceId |
| `pointIndex` | int | ✅ | 目标点位索引 |
| `value` | double | ✅ | 目标值(如开关 0/1,温度设定值等) |
---
## 5. 告警管理
### B8: 分页查询告警列表
```
GET /api/gateway/alarms/{adapter}?page={page}&size={size}&from={from}&to={to}&level={level}&state={state}
```
**请求参数**:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|:--:|------|------|
| `adapter` | string | ✅ | — | 适配器编码 |
| `page` | int | ❌ | 1 | 页码 |
| `size` | int | ❌ | 20 | 每页条数 |
| `from` | DateTime | ❌ | MinValue | 告警起始时间 |
| `to` | DateTime | ❌ | MinValue | 告警结束时间 |
| `level` | string | ❌ | null | 告警等级过滤 |
| `state` | string | ❌ | null | 告警状态过滤:`未确认`/`已确认`/`已结束` |
**返回参数**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `items` | array | 告警列表 |
| `total` | int | 总告警数 |
**items[].StandardAlarm**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `alarmId` | string | 告警 ID |
| `adapterCode` | string | 适配器编码 |
| `deviceId` | string? | 关联设备 ID |
| `level` | string | 告警等级:`提示`/`普通`/`重要`/`紧急` |
| `title` | string | 告警标题 |
| `content` | string? | 告警详细内容 |
| `occurTime` | DateTime | 发生时间 |
| `status` | string | 状态:`未确认`/`已确认`/`已结束` |
| `actualValue` | string? | 实际值(超标告警) |
| `thresholdValue` | string? | 阈值(超标告警) |
### B9-confirm: 确认告警
```
POST /api/gateway/alarms/{adapter}/{alarmId}/confirm
```
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `adapter` | string | ✅ | 适配器编码 |
| `alarmId` | string | ✅ | 告警 ID(子系统告警源 ID) |
### B9-end: 结束告警
```
POST /api/gateway/alarms/{adapter}/{alarmId}/end
```
**请求参数**: 同 B9-confirm
---
## 6. 录像查询
```
GET /api/gateway/recordings/{adapter}/{deviceId}?start={start}&end={end}&page={page}&size={size}
```
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `adapter` | string | ✅ | 适配器编码 |
| `deviceId` | string | ✅ | 通道 SourceId |
| `start` | DateTime | ✅ | 录像起始时间 |
| `end` | DateTime | ✅ | 录像结束时间 |
| `page` | int | ❌ | 页码,默认 1 |
| `size` | int | ❌ | 每页条数,默认 20 |
**返回**: 录像文件列表(含文件名、起止时间、时长、大小)
---
## 7. 设备控制 (通用)
### B10: 下发控制指令
```
POST /api/gateway/control/{adapter}
```
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `adapter` | string | ✅ | 适配器编码,如 `KMS:main` |
**请求体 (GatewayControlRequest)**:
| 字段 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `deviceId` | string | ✅ | 设备 SourceId |
| `command` | string | ✅ | 指令名:`open`(开门)/`close`(关门)/`authorize`(授权) |
| `parameters` | object | ❌ | 指令参数,如 `{"staffIds": [1,2], "lockholeSort": 3}` |
**返回 (ControlResult)**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `success` | bool | 操作是否成功 |
| `message` | string? | 失败时的错误信息 |
---
## 8. 业务记录查询
### B11: 查询子系统业务记录
```
GET /api/gateway/logs/{adapter}?logType={logType}&from={from}&to={to}&page={page}&size={size}
```
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `adapter` | string | ✅ | 适配器编码 |
| `logType` | string | ✅ | 记录类型:`borrow`(借还)/`handover`(交接)/`permission`(授权) |
| `from` | DateTime | ❌ | 起始时间 |
| `to` | DateTime | ❌ | 结束时间 |
| `page` | int | ❌ | 页码,默认 1 |
| `size` | int | ❌ | 每页条数,默认 20 |
**返回参数**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `items` | array | 业务记录列表 |
| `total` | int | 总记录数 |
**items[].BusinessLogEntry**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `logId` | string | 记录唯一 ID |
| `logType` | string | 记录类型 |
| `deviceSourceId` | string? | 关联设备 SourceId |
| `staffName` | string? | 关联员工姓名 |
| `description` | string? | 记录描述 |
| `createdAt` | DateTime? | 记录时间 |
| `extra` | object? | 扩展属性 |
---
## 9. 数据同步
### B12: 向子系统写入数据
```
POST /api/gateway/sync/{adapter}
```
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `adapter` | string | ✅ | 适配器编码 |
**请求体 (SyncRequest)**:
| 字段 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `dataType` | string | ✅ | 数据类型,当前支持 `staff`(员工) |
| `items` | object[] | ✅ | 待同步数据列表 |
**返回 (SyncResult)**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `successCount` | int | 成功数量 |
| `failCount` | int | 失败数量 |
| `message` | string? | 错误信息 |
### B13: 从子系统删除数据
```
DELETE /api/gateway/sync/{adapter}
```
**请求参数**: 同 B12
**请求体 (SyncDeleteRequest)**:
| 字段 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `dataType` | string | ✅ | 数据类型,当前支持 `staff` |
| `ids` | string[] | ✅ | 待删除 ID 列表 |
**返回**: 同 `SyncResult`
---
## 10. 错误代码
### 通用 HTTP 状态码
| 状态码 | 含义 | 触发条件 |
|:---:|------|------|
| 200 | OK | 请求成功 |
| 400 | Bad Request | 请求参数格式错误 |
| 401 | Unauthorized | `X-Gateway-Key` 缺失或不匹配 |
| 404 | Not Found | 适配器不存在或不支持该能力 |
| 500 | Internal Server Error | 适配器内部异常 |
| 502 | Bad Gateway | 子系统返回错误或不可达 |
### 业务错误码
所有非 200 响应包含 JSON body
| 字段 | 类型 | 说明 |
|------|------|------|
| `error` | string | 错误码 |
| `message` | string? | 人类可读的错误详情 |
| error 值 | HTTP | 说明 |
|------|:---:|------|
| `ADAPTER_NOT_FOUND` | 404 | 指定适配器编码不存在 |
| `CAPABILITY_NOT_SUPPORTED` | 404 | 适配器不支持该接口能力 |
| — (control 接口) | 502 | `ControlResult.Success=false` 时返回 `ControlResult.Message` |
---
> **接口总数**: 19 个 REST 端点
> **适配器**: Owl / MC4 / KMS(通过 `adapter` 参数路由)