From cb4f78a233488c6867fb65d4e0a504f37b489ef3 Mon Sep 17 00:00:00 2001 From: George Date: Sat, 30 May 2026 12:56:07 +0800 Subject: [PATCH] =?UTF-8?q?20260530:=E5=9B=9E=E6=BB=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 10 + .../06-platform-integration-design.md | 1238 ----------------- .../06_1-platform-intergration-design.md | 1013 -------------- docs/knowledge-base/README.md | 1 - docs/platform/thinks.png | Bin 0 -> 64407 bytes 5 files changed, 10 insertions(+), 2252 deletions(-) create mode 100644 .claude/settings.local.json delete mode 100644 docs/knowledge-base/06-platform-integration-design.md delete mode 100644 docs/knowledge-base/06_1-platform-intergration-design.md create mode 100644 docs/platform/thinks.png diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..1422d80c --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(python -c \"import pdfplumber; pdf = pdfplumber.open\\('docs/platform/DCP接口文档\\(轴力计\\).pdf'\\); [print\\(p.extract_text\\(\\)\\) for p in pdf.pages[:15]]\")", + "Bash(python3 -c \"import pdfplumber; pdf = pdfplumber.open\\('docs/platform/DCP接口文档\\(轴力计\\).pdf'\\); [print\\(p.extract_text\\(\\)\\) for p in pdf.pages[:15]]\")", + "Bash(pip install *)", + "Bash(python -c ' *)" + ] + } +} diff --git a/docs/knowledge-base/06-platform-integration-design.md b/docs/knowledge-base/06-platform-integration-design.md deleted file mode 100644 index 56e2ab13..00000000 --- a/docs/knowledge-base/06-platform-integration-design.md +++ /dev/null @@ -1,1238 +0,0 @@ -# 平台对接需求与技术设计 - -## 1. 背景 - -需求目录 `docs/platform/` 中目前包含 DCP 雨量计、倾角、静力水准、轴力计接口文档。DCP 是当前优先落地的平台,但平台对接能力不能只服务一个平台、一个租户。平台一旦完成接入,后续其他租户应复用同一套平台定义、接口定义、流程编排、适配器和数据交换执行能力;租户只维护自己的第三方账号/用户、项目、设备、字段映射和交换任务。 - -平台与平台之间的数据交换本质上分三类: - -| 方向 | FastBee 角色 | 触发方 | 说明 | -| --------------- | ---------- | ------- | ---------------------------------------- | -| OUTBOUND_PUSH | Producer | FastBee | FastBee 主动把本系统数据推送给第三方。 | -| INBOUND_PULL | Consumer | FastBee | FastBee 主动从第三方拉取数据,但消费方仍是 FastBee。 | -| INBOUND_RECEIVE | Consumer | 第三方 | 第三方主动调用 FastBee 接口或消息入口,FastBee 接收并消费数据。 | - -因此本方案不再以“推送”为中心,而是以更普适的 **数据交换 Exchange** 为中心: - -```text -平台对接 = 平台模板 + 租户接入 + 资源映射 + 流程编排 + 数据交换任务 + 数据交换记录 -``` - -当前 FastBee 已具备以下基础能力: - -- 设备、产品、物模型:设备通过 `iot_device` 管理,物模型通过产品和设备编号查询。 -- 设备上报处理:设备数据解析后进入原有入库和规则处理链路。 -- 历史数据查询:数据中心接口支持按设备、时间窗口和物模型标识符查询历史数据。 -- 数据桥接:现有 `bridge`、`iot_script`、`iot_script_bridge` 支持 HTTP、MQTT、数据库桥接,可作为轻量转发能力保留。 -- 定时任务:系统已有 Quartz 任务体系,可复用为 OUTBOUND_PUSH、INBOUND_PULL、RESOURCE_SYNC 等任务触发器。 -- 多租户:IoT 主要业务表已有 `tenant_id`、`tenant_name` 约定,平台对接也必须按租户隔离业务映射和交换数据。 - -## 2. 设计目标 - -平台对接需要同时满足“平台共用”“租户隔离”“方向清晰”“数据边界清晰”: - -- 平台接入人员只维护一次第三方平台模板:认证方式、接口、字段、流程、适配器。 -- 多个租户可启用同一个平台模板,不重复配置接口和流程。 -- 每个租户维护自己的第三方账号/用户授权、项目映射、设备绑定、字段映射和交换任务。 -- 支持三类数据方向:主动推送、主动拉取、被动接收。 -- 主动拉取和被动接收都属于 INBOUND,FastBee 是 consumer,最终要落到 FastBee 的设备、物模型、事件、告警、资源映射或业务表。 -- 主动推送属于 OUTBOUND,FastBee 是 producer,重点是可靠投递、重试、重推和审计。 -- 支持幂等、失败重试、手动重试、补偿、日志追踪。 -- DCP 等复杂平台通过专用适配器处理 token、项目、测点、设备、传感器、雨量递增等平台规则。 -- DCP 必须遵守 **一个测点对应一个 FastBee 设备**,不能将测点设计为平台级或产品级共享资源。 - -## 3. 总体架构 - -平台对接按四层拆分。 - -```text -平台模板层(全局/系统级,可复用) - -> 平台定义、接口定义、字段定义、流程编排、适配器 - -租户接入层(租户级,隔离) - -> 租户平台实例、第三方账号/用户、项目映射、资源映射、设备绑定、字段映射 - -数据交换层(租户级,隔离) - -> 数据交换任务、交换记录、交换尝试、幂等、重试、补偿 - -业务落点层(FastBee 内部边界) - -> 设备数据、设备日志、物模型值、事件、告警、资源映射、业务表 -``` - -### 3.1 平台模板层 - -平台模板层由平台管理员维护,配置一次、多租户复用。 - -包含: - -- 第三方平台类型,例如 DCP、通用 HTTP、通用 MQTT、通用数据库。 -- 认证方式,例如无认证、固定 Token、Basic、Bearer、OAuth2、自定义 Token 接口。 -- 接口定义,例如获取用户、获取项目、创建测点、创建设备、绑定传感器、推送数据、查询数据、接收数据。 -- 接口字段定义,用于页面展示和字段映射。 -- 流程编排,例如“获取用户 -> 获取项目 -> 确保测点 -> 确保设备 -> 推送数据”。 -- 适配器编码,例如 `genericHttp`、`dcp`。 - -平台模板不保存租户账号、项目、设备和业务数据。 - -### 3.2 租户接入层 - -租户接入层由租户管理员或实施人员维护。 - -包含: - -- 租户启用哪个平台模板。 -- 租户自己的第三方账号、token、clientId/clientSecret 等授权配置。 -- 租户第三方用户和项目映射。 -- FastBee 设备与第三方资源的绑定。 -- FastBee 物模型字段与第三方接口字段的映射。 -- 接收入口密钥、签名配置、白名单等租户级安全配置。 - -同一个平台模板下,不同租户的数据必须隔离:授权配置、项目、用户、资源映射、设备绑定、交换任务、交换记录都必须带 `tenant_id`。 - -### 3.3 数据交换层 - -数据交换层是运行时核心。它不只处理推送,也处理拉取和接收。 - -```text -OUTBOUND_PUSH - FastBee 内部数据 -> 第三方平台 - -INBOUND_PULL - 第三方平台 -> FastBee 主动拉取 -> FastBee 内部业务落点 - -INBOUND_RECEIVE - 第三方主动提交 -> FastBee 接收入口 -> FastBee 内部业务落点 -``` - -三种方向共用平台模板、租户接入、认证、字段映射、流程编排、资源映射和交换记录,但使用不同触发器和执行器。 - -### 3.4 业务落点层 - -INBOUND 数据必须明确落点,避免平台对接层无限扩张。 - -推荐落点类型: - -| sink_type | 说明 | -| ---------------- | ------------------- | -| DEVICE_PROPERTY | 写入设备属性/物模型数据。 | -| DEVICE_EVENT | 写入设备事件。 | -| ALERT_EVENT | 写入告警或告警日志。 | -| RESOURCE_MAPPING | 写入用户、项目、测点、设备等资源映射。 | -| BUSINESS_TABLE | 写入明确的业务表,需要白名单。 | -| RAW_ONLY | 只记录原始数据,不进入业务表。 | - -平台对接层负责接入、鉴权、映射、转换、幂等和审计;具体业务落库应通过明确的 sink adapter 完成,不允许任意 SQL 或任意代码写入。 - -## 4. 三类方向的边界 - -### 4.1 OUTBOUND_PUSH:推送数据 - -FastBee 是 producer,第三方平台是 consumer。 - -```text -设备上报 / 历史补推 / 业务事件 - -> 构造标准出站数据 PlatformExchangeMessage - -> 匹配租户平台实例和设备绑定 - -> 字段映射和资源补齐 - -> 执行 PUSH 流程 - -> 调用第三方接口/MQTT/DB - -> 写 exchange_record / exchange_attempt -``` - -边界: - -- 数据来源是 FastBee 内部可信业务数据。 -- 失败可重试,可手动重推。 -- 需要 outbox,避免阻塞设备上报主链路。 -- 幂等 key 应基于本地来源、绑定、时间和 payload hash。 - -### 4.2 INBOUND_PULL:拉取数据 - -FastBee 主动发起请求,但 FastBee 仍是 consumer。 - -```text -Quartz / 手动触发 - -> 加载租户平台实例和授权配置 - -> 执行 PULL / RESOURCE_SYNC 流程 - -> 调第三方查询接口 - -> 处理分页、游标、时间窗口 - -> 响应字段映射 - -> 识别本地租户、项目、设备、资源 - -> 写入业务落点 - -> 写 exchange_record / exchange_attempt -``` - -边界: - -- 数据来源是第三方平台,必须按外部输入处理。 -- 必须有游标、时间窗口或分页边界,禁止无界拉取。 -- 必须定义 sink_type,明确拉取结果落到哪里。 -- 必须支持幂等,避免同一第三方数据重复入库。 -- 拉取状态建议保存在任务游标中,例如 `cursor_json`、`last_success_time`、`last_success_external_id`。 - -### 4.3 INBOUND_RECEIVE:接收数据 - -第三方主动调用 FastBee,FastBee 是 consumer。 - -```text -第三方 HTTP/MQTT 调用 FastBee - -> 接收入口鉴权/签名/限流 - -> 匹配租户平台实例和 RECEIVE 流程 - -> 提取幂等 key - -> 请求字段映射 - -> 识别本地租户、项目、设备、资源 - -> 写入业务落点 - -> 写 exchange_record / exchange_attempt - -> 返回处理结果 -``` - -边界: - -- 数据来源是第三方平台,必须按外部输入处理。 -- 接收入口必须具备租户识别、鉴权、签名校验、幂等、限流和日志。 -- 同步响应只代表 FastBee 是否接收/处理成功,不代表后续所有异步流程都完成。 -- 如果需要异步处理,接收入口应先写 exchange_record,再由 worker 处理。 -- 不建议复用旧 `/bridge/**` 作为主入口,可保留旧桥接用于轻量场景。 - -## 5. 与现有系统功能的关系 - -| 现有能力 | 复用方式 | -| ----------------- | ----------------------------------------------------------------- | -| 设备管理 `iot_device` | 设备绑定引用 `device_id`、`serial_number`、`product_id`、`tenant_id`。 | -| 产品物模型 | 绑定和字段映射页面通过现有物模型接口展示字段,使用 `identifier`。 | -| 设备实时上报链路 | OUTBOUND_PUSH 可在原有数据处理完成后生成交换记录,不改变协议解析逻辑。 | -| 数据中心历史查询 | OUTBOUND_PUSH 补推可按设备、时间窗口、identifier 取历史数据。 | -| 规则引擎和数据桥接 | 保留给轻量 HTTP/MQTT/DB 转发;平台对接面向多步骤、多租户、可审计场景。 | -| Quartz 定时任务 | INBOUND_PULL、OUTBOUND_PUSH 定时补偿、RESOURCE_SYNC 可复用 sys_job/Quartz。 | -| Redis/Redisson | 缓存 token,资源创建时做并发锁,接收入口可做幂等和限流。 | -| 若依权限体系 | 继续使用菜单、角色、`@PreAuthorize` 权限控制。 | - -现有数据桥接仍保留。它适合“固定 URL + 固定模板 + 脚本触发”的轻量转发;平台对接用于第三方平台长期集成,需要多租户账号、项目、设备映射、流程编排、资源幂等、数据落点、失败重试和审计。 - -## 6. 前端功能设计 - -一级菜单:**平台对接**。 - -建议菜单: - -```text -平台对接 - 平台模板 - 接口配置 - 流程编排 - 租户接入 - 资源映射 - 设备绑定 - 数据交换任务 - 数据交换记录 -``` - -### 6.1 平台模板 - -平台模板是系统级可复用配置。 - -主要字段: - -| 字段 | 说明 | -| ------ | ----------------------------------------- | -| 平台名称 | 用户可读名称,例如 DCP。 | -| 平台编码 | 全局唯一,例如 `DCP`。 | -| 平台类型 | DCP、GENERIC_HTTP、GENERIC_MQTT、GENERIC_DB。 | -| 适配器编码 | dcp、genericHttp 等。 | -| 基础地址模板 | 可作为租户实例默认 baseUrl。 | -| 认证方式 | OAuth2、自定义 Token、固定 Token 等。 | -| 默认请求配置 | 超时、重试、Content-Type 等。 | -| 启用状态 | 启用、停用。 | - -### 6.2 接口配置 - -接口配置属于平台模板,可被所有租户复用。 - -接口类型: - -| 类型 | 说明 | -| --------------- | ----------------------- | -| TOKEN | 获取或刷新 token。 | -| QUERY | 查询第三方基础数据,如用户、项目、设备、数据。 | -| RECEIVE | FastBee 接收入口定义。 | -| RESOURCE_CREATE | 创建外部资源,如测点、设备、传感器。 | -| RESOURCE_BIND | 绑定外部资源。 | -| PUSH | 推送设备数据。 | -| CALLBACK | 回调第三方处理结果,可选。 | - -配置项: - -- 请求方法:GET、POST、PUT、DELETE。 -- URL 模板:支持 `${变量}`。 -- Header、Query、Body 模板。 -- Content-Type。 -- 成功条件:HTTP 状态码、响应 JSONPath。 -- 响应变量提取:例如 `$.id -> pointId`。 -- 超时和重试策略。 -- RECEIVE 接口还需要配置入口路径、鉴权方式、签名规则、幂等 key 提取规则。 - -### 6.3 接口字段 - -接口字段用于页面展示和字段映射。 - -| 字段 | 说明 | -| ---- | ----------------------------------- | -| 接口 | 所属接口。 | -| 方向 | REQUEST、RESPONSE。 | -| 字段路径 | JSONPath 或表单字段名。 | -| 字段名称 | 展示名称。 | -| 数据类型 | string、number、boolean、array、object。 | -| 是否必填 | 是否必填。 | -| 默认值 | 默认值。 | -| 示例值 | 示例值。 | - -### 6.4 流程编排 - -流程属于平台模板,用于将多个接口和适配器方法组成可复用业务流程。 - -流程类型: - -| flow_type | 说明 | -| --------------- | --------------------- | -| OUTBOUND_PUSH | FastBee 推送数据到第三方。 | -| INBOUND_PULL | FastBee 从第三方拉取并消费数据。 | -| INBOUND_RECEIVE | FastBee 接收第三方提交并消费数据。 | -| RESOURCE_SYNC | 同步第三方用户、项目、设备、测点等资源。 | -| CALLBACK | 回调第三方处理结果,可选。 | - -流程步骤类型: - -| step_type | 说明 | -| --------- | ---------------- | -| API | 调用平台接口配置。 | -| ADAPTER | 调用专用适配器方法。 | -| CONDITION | 条件判断。 | -| TRANSFORM | 数据转换。 | -| SINK | 写入 FastBee 业务落点。 | - -流程模板可以被多个租户复用;步骤执行时使用当前租户的授权配置、项目映射、设备绑定、资源映射和字段映射。 - -### 6.5 租户接入 - -租户接入表示某个租户启用某个平台模板。 - -主要字段: - -| 字段 | 说明 | -| ------- | -------------------------------- | -| 租户 | 当前租户。 | -| 平台模板 | 选择 DCP 等平台模板。 | -| 实例名称 | 租户内展示名。 | -| baseUrl | 可覆盖模板默认地址。 | -| 授权配置 | 租户自己的账号、clientId、secret、token 等。 | -| 默认第三方用户 | DCP userId 或其他平台用户。 | -| 默认项目 | DCP projectId 或其他平台项目。 | -| 接收安全配置 | webhook token、签名密钥、IP 白名单等。 | -| 启用状态 | 启用、停用。 | - -租户接入配置完成后,该租户才能建立资源映射、设备绑定、交换任务和接收入口。 - -### 6.6 资源映射 - -资源映射是平台对接的核心边界之一,表示 FastBee 本地对象和第三方对象的对应关系。 - -常见资源类型: - -| resource_type | 说明 | -| ------------- | -------- | -| USER | 第三方用户。 | -| PROJECT | 第三方项目。 | -| DEVICE | 第三方设备。 | -| POINT | 第三方测点。 | -| SENSOR | 第三方传感器。 | -| MODULE | 第三方模块。 | -| VIRTUAL | 第三方虚拟资源。 | -| CUSTOM | 其他平台资源。 | - -资源映射必须带 `tenant_id` 和 `tenant_channel_id`。除平台模板定义外,第三方资源 id 不允许全局共享。 - -### 6.7 设备绑定 - -设备绑定是租户级配置。 - -绑定内容: - -```text -租户平台实例 + FastBee 设备 + 平台流程 + 第三方用户/项目/资源 + 字段映射 + 交换模式 -``` - -绑定页面左侧使用现有 FastBee 产品、设备、物模型;右侧使用平台模板的接口字段和租户的第三方用户/项目/资源。 - -绑定支持:新增、修改、启用、停用、解绑、初始化资源、查看资源映射、查看交换记录。 - -DCP 绑定规则: - -```text -FastBee 一个设备 = DCP 一个测点 = DCP 一个设备资源 -``` - -DCP 的 `pointId`、`dcpDeviceId`、sensor/module/virtual 等必须按租户设备绑定保存,不能平台级共享。 - -### 6.8 字段映射 - -字段映射按设备绑定维护,也可以支持从平台模板复制默认映射。 - -映射来源: - -| source_type | 说明 | -| ----------- | ------------------------------------------- | -| THING_MODEL | 物模型字段,例如 rainfall。 | -| SYSTEM | 系统字段,例如 serialNumber、deviceName、reportTime。 | -| CONSTANT | 常量。 | -| RESOURCE | 外部资源映射,例如 pointId、dcpDeviceId。 | -| JSONPATH | 从 rawPayload 或第三方响应中取值。 | -| EXPRESSION | 受限表达式,后续增强。 | - -字段映射同时服务 OUTBOUND 和 INBOUND: - -- OUTBOUND:FastBee 内部字段映射到第三方字段。 -- INBOUND:第三方字段映射到 FastBee 标准消息或业务落点字段。 - -表达式必须受限,不能执行任意代码。 - -### 6.9 数据交换任务与记录 - -数据交换任务是租户级配置。 - -任务类型: - -| task_type | 说明 | -| ------------- | ----------------------- | -| OUTBOUND_PUSH | 定时推送或补推 FastBee 数据到第三方。 | -| INBOUND_PULL | 定时拉取第三方数据并消费。 | -| RESOURCE_SYNC | 定时同步第三方用户、项目、设备、测点等资源。 | - -INBOUND_RECEIVE 一般不依赖定时任务,而是通过接收入口触发;如果接收后异步处理,也会生成交换记录。 - -数据交换记录展示业务层面的交换状态: - -- 按租户、平台、方向、流程、设备、状态、时间筛选。 -- 查看原始输入、标准消息、最终 payload、幂等 key、请求响应、错误信息。 -- 失败重试、批量重试、成功记录强制重放。 - -## 7. 标准数据模型 - -平台对接内部统一使用标准数据交换消息,避免直接依赖原始 MQTT/HTTP payload 或第三方响应结构。 - -```json -{ - "direction": "OUTBOUND_PUSH", - "tenantId": 1, - "tenantChannelId": 10, - "flowId": 100, - "bindingId": 1001, - "sourceType": "DEVICE_PROPERTY", - "sourceRefId": "device_log_id_or_external_message_id", - "eventTime": "2026-05-27 10:00:00", - "device": { - "productId": 10, - "deviceId": 1001, - "serialNumber": "ABC001", - "deviceName": "雨量计-001" - }, - "values": { - "rainfall": { - "value": 12.3, - "unit": "mm", - "ts": "2026-05-27 10:00:00" - } - }, - "resources": { - "projectId": "P001", - "pointId": "PT001", - "externalDeviceId": "D001" - }, - "rawPayload": {} -} -``` - -说明: - -- OUTBOUND 的 `rawPayload` 通常是 FastBee 原始上报或业务事件。 -- INBOUND 的 `rawPayload` 是第三方原始请求或响应。 -- `values` 的 key 优先使用 FastBee 物模型 `identifier`。 -- `resources` 来自租户级资源映射。 -- INBOUND 处理完成后必须进入明确的 `sink_type`,不能停留在平台对接层。 - -## 8. DCP 适配设计 - -DCP 通过平台模板 + 流程编排 + 专用适配器落地。DCP 当前主要覆盖 OUTBOUND_PUSH 和 RESOURCE_SYNC,也可以扩展 INBOUND_PULL 同步项目、测点或历史数据。 - -### 8.1 DCP 平台模板 - -DCP 平台模板包含: - -- token 接口:`POST /connect/token`。 -- 用户接口:获取 userId。 -- 项目接口:按 userId 获取项目。 -- 资源接口:创建测点、创建设备、创建/绑定传感器等。 -- 数据接口:雨量、倾角、静力水准、轴力计数据推送;如 DCP 提供查询接口,也可配置为 INBOUND_PULL。 -- DCP 专用适配器:处理 token 缓存、资源幂等、payload 构造、特殊规则。 - -这些配置由系统维护一次,所有租户复用。 - -### 8.2 DCP 租户映射 - -每个租户需要维护自己的 DCP 映射: - -```text -租户 - -> DCP 租户平台实例 - -> DCP 账号/用户 userId - -> DCP 项目 projectId - -> FastBee 设备绑定 DCP 测点 pointId - -> FastBee 设备绑定 DCP 设备 dcpDeviceId - -> 设备附属 sensor/module/virtual -``` - -同一个 DCP 平台模板下,不同租户可以使用不同 DCP 账号、用户和项目。资源映射必须按 `tenant_id` 隔离。 - -### 8.3 DCP 一测点一设备约束 - -DCP 资源映射必须按“一测点一设备”建模: - -```text -FastBee Device(serialNumber) - -> DCP Project(projectId) - -> DCP Point(pointId) - -> DCP Device(dcpDeviceId) - -> DCP Sensor/Module/Virtual(按设备类型可选) -``` - -规则: - -- `PROJECT` 可以是租户平台实例级默认资源,也可以绑定级覆盖。 -- `USER`、`PROJECT` 是租户级或绑定级映射,不是全局共享。 -- `POINT` 必须是设备级资源,一个 FastBee 设备对应一个 DCP 测点。 -- `DEVICE` 必须是设备级资源,一个 FastBee 设备对应一个 DCP 设备资源。 -- `SENSOR`、`MODULE`、`VIRTUAL` 是设备附属资源,按绑定保存。 -- 静力水准 payload 中可包含多条 `sensorData`,但仍归属同一个 FastBee 设备对应的 DCP 测点和 DCP 设备下。 - -### 8.4 DCP OUTBOUND_PUSH 流程 - -```text -设备上报或补推任务 - -> 匹配租户 DCP 设备绑定 - -> 加载租户 DCP 授权配置 - -> 获取/刷新 token - -> 加载 userId/projectId 映射 - -> 加载 pointId/dcpDeviceId/sensor 映射 - -> 资源缺失时按流程懒加载或手动初始化 - -> 构造对应设备类型 payload - -> 调用 DCP 数据接口 - -> 写 exchange_record 和 exchange_attempt -``` - -资源创建必须幂等。并发上报同一设备时,使用 Redis/Redisson 锁: - -```text -platform:dcp:resource:{tenantId}:{tenantChannelId}:{serialNumber} -``` - -### 8.5 DCP 特殊规则 - -- 雨量 `value` 为累计降雨量,DCP 要求新值大于历史值。系统使用绑定的 `last_success_value` 或资源扩展字段记录最近成功值;不满足时记录为 `SKIPPED`,不反复重试。 -- 倾角需要同一时间的 x、y、z 和温度,必须按一次设备快照组包,不能拆成多个单字段推送。 -- 静力水准的 `sensorData` 可包含多条传感器数据,但这些数据属于同一个 FastBee 设备对应的 DCP 设备资源。 -- 轴力计按独立设备类型处理,字段映射和 payload 由 `DcpPlatformAdapter` 中对应 builder 负责。 -- DCP token 缓存在 Redis,key 建议:`platform:dcp:token:{tenantId}:{tenantChannelId}`。 -- 401 或 token 过期时刷新 token 后重试一次,再失败则记录错误。 - -## 9. 表设计 - -表名前缀使用 `iot_platform_`,避免和现有 `iot_social_platform` 混淆。 - -### 9.1 表清单 - -| 表名 | 层级 | 说明 | -| ------------------------------- | ----- | ------------------------- | -| `iot_platform_template` | 平台模板层 | 平台模板,全局可复用。 | -| `iot_platform_api` | 平台模板层 | 第三方接口定义,含调用接口和接收入口。 | -| `iot_platform_api_field` | 平台模板层 | 接口字段定义。 | -| `iot_platform_flow` | 平台模板层 | 流程模板。 | -| `iot_platform_flow_step` | 平台模板层 | 流程步骤。 | -| `iot_platform_tenant_channel` | 租户接入层 | 租户启用的平台实例、授权和接收安全配置。 | -| `iot_platform_resource_mapping` | 租户接入层 | 用户、项目、测点、设备、传感器等资源映射。 | -| `iot_platform_binding` | 租户接入层 | FastBee 设备与平台流程/项目/资源绑定。 | -| `iot_platform_field_mapping` | 租户接入层 | 绑定级字段映射。 | -| `iot_platform_exchange_task` | 数据交换层 | 定时推送、主动拉取、资源同步任务。 | -| `iot_platform_task_binding` | 数据交换层 | 交换任务与绑定关系。 | -| `iot_platform_exchange_record` | 数据交换层 | 数据交换记录,兼 outbox/inbox 审计。 | -| `iot_platform_exchange_attempt` | 数据交换层 | 每次交换尝试日志。 | - -### 9.2 iot_platform_template - -平台模板,全局可复用。 - -| 字段 | 类型 | 说明 | -| ----------------- | ------------ | ---------------------------------------- | -| id | bigint | 主键 | -| platform_code | varchar(64) | 平台编码,全局唯一 | -| platform_name | varchar(128) | 平台名称 | -| platform_type | varchar(32) | DCP、GENERIC_HTTP、GENERIC_MQTT、GENERIC_DB | -| adapter_code | varchar(64) | dcp、genericHttp 等 | -| base_url_template | varchar(512) | 默认基础地址模板 | -| auth_type | varchar(32) | 认证类型 | -| auth_schema | json | 授权配置字段定义,用于租户填写 | -| request_config | json | 默认超时、重试、TLS 等 | -| status | tinyint | 0停用,1启用 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| remark | varchar(500) | 备注 | - -建议索引: - -```text -uk_platform_code(platform_code) -idx_platform_type(platform_type) -``` - -### 9.3 iot_platform_api - -第三方接口定义,属于平台模板。对 OUTBOUND/PULL 是要调用的第三方接口;对 RECEIVE 是 FastBee 暴露的接收入口定义。 - -| 字段 | 类型 | 说明 | -| ------------------ | ------------ | --------------------------------------------------------------- | -| id | bigint | 主键 | -| template_id | bigint | 平台模板 id | -| api_code | varchar(64) | 接口编码 | -| api_name | varchar(128) | 接口名称 | -| api_type | varchar(32) | TOKEN、QUERY、RECEIVE、PUSH、RESOURCE_CREATE、RESOURCE_BIND、CALLBACK | -| method | varchar(16) | GET、POST、PUT、DELETE | -| path_template | varchar(512) | URL 路径模板或接收入口路径模板 | -| content_type | varchar(64) | application/json、form-urlencoded | -| header_template | json | Header 模板 | -| query_template | json | Query 模板 | -| body_template | json | Body 模板 | -| auth_config | json | 接口级认证或接收校验配置 | -| idempotency_config | json | 幂等 key 提取规则 | -| success_condition | json | 成功条件 | -| response_mapping | json | 响应变量提取规则 | -| retry_policy | json | 接口级重试策略 | -| timeout_ms | int | 超时时间 | -| status | tinyint | 0停用,1启用 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | - -建议索引: - -```text -uk_template_api_code(template_id, api_code) -idx_template_api_type(template_id, api_type) -``` - -### 9.4 iot_platform_api_field - -接口字段定义,用于页面展示和字段映射。 - -| 字段 | 类型 | 说明 | -| ------------- | ------------ | ---------------------------------- | -| id | bigint | 主键 | -| api_id | bigint | 接口 id | -| direction | varchar(16) | REQUEST、RESPONSE | -| field_path | varchar(256) | JSONPath 或表单字段名 | -| field_name | varchar(128) | 字段名称 | -| data_type | varchar(32) | string、number、boolean、array、object | -| required | tinyint | 是否必填 | -| default_value | varchar(512) | 默认值 | -| example_value | varchar(512) | 示例值 | -| description | varchar(500) | 说明 | -| sort_order | int | 排序 | - -建议索引: - -```text -idx_api_direction(api_id, direction) -``` - -### 9.5 iot_platform_flow - -流程模板,属于平台模板。 - -| 字段 | 类型 | 说明 | -| ------------ | ------------ | ----------------------------------------------------------------- | -| id | bigint | 主键 | -| template_id | bigint | 平台模板 id | -| flow_code | varchar(64) | 流程编码 | -| flow_name | varchar(128) | 流程名称 | -| flow_type | varchar(32) | OUTBOUND_PUSH、INBOUND_PULL、INBOUND_RECEIVE、RESOURCE_SYNC、CALLBACK | -| adapter_code | varchar(64) | 可覆盖平台适配器 | -| sink_type | varchar(32) | INBOUND 数据落点类型,OUTBOUND 可为空 | -| status | tinyint | 0停用,1启用 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| remark | varchar(500) | 备注 | - -建议索引: - -```text -uk_template_flow_code(template_id, flow_code) -idx_template_flow_type(template_id, flow_type) -``` - -### 9.6 iot_platform_flow_step - -流程步骤。 - -| 字段 | 类型 | 说明 | -| -------------- | ------------ | ------------------------------------ | -| id | bigint | 主键 | -| flow_id | bigint | 流程 id | -| step_code | varchar(64) | 步骤编码 | -| step_name | varchar(128) | 步骤名称 | -| step_type | varchar(32) | API、ADAPTER、CONDITION、TRANSFORM、SINK | -| api_id | bigint | API 步骤引用接口 id | -| adapter_method | varchar(128) | 适配器方法,例如 ensurePoint | -| input_mapping | json | 步骤入参映射 | -| output_mapping | json | 步骤输出变量映射 | -| condition_expr | varchar(512) | 执行条件 | -| on_fail | varchar(32) | STOP、SKIP、RETRY | -| sort_order | int | 执行顺序 | -| status | tinyint | 0停用,1启用 | - -建议索引: - -```text -idx_flow_sort(flow_id, sort_order) -``` - -### 9.7 iot_platform_tenant_channel - -租户启用平台模板后的平台实例。授权配置、baseUrl、默认用户/项目、接收安全配置属于租户级。 - -| 字段 | 类型 | 说明 | -| ------------------ | ------------ | ------------------------- | -| id | bigint | 主键 | -| tenant_id | bigint | 租户 id | -| tenant_name | varchar(64) | 租户名称 | -| template_id | bigint | 平台模板 id | -| channel_code | varchar(64) | 租户内平台实例编码 | -| channel_name | varchar(128) | 租户内平台实例名称 | -| base_url | varchar(512) | 实际基础地址 | -| auth_config | json | 租户授权配置,敏感值加密 | -| receive_config | json | 接收安全配置,token、签名密钥、IP 白名单等 | -| default_user_id | varchar(128) | 默认第三方用户 id | -| default_project_id | varchar(128) | 默认第三方项目 id | -| config_json | json | 租户级扩展配置 | -| status | tinyint | 0停用,1启用 | -| del_flag | char(1) | 0正常,2删除 | -| create_by | varchar(64) | 创建者 | -| create_time | datetime | 创建时间 | -| update_by | varchar(64) | 更新者 | -| update_time | datetime | 更新时间 | -| remark | varchar(500) | 备注 | - -建议索引: - -```text -uk_tenant_channel_code(tenant_id, channel_code, del_flag) -idx_tenant_template(tenant_id, template_id) -``` - -### 9.8 iot_platform_resource_mapping - -外部资源映射,租户级隔离。 - -| 字段 | 类型 | 说明 | -| ----------------- | ------------ | ------------------------------------------------------ | -| id | bigint | 主键 | -| tenant_id | bigint | 租户 id | -| tenant_channel_id | bigint | 租户平台实例 id | -| binding_id | bigint | 绑定 id,用户/项目可为空 | -| resource_scope | varchar(32) | TENANT、PROJECT、DEVICE、BINDING | -| resource_type | varchar(64) | USER、PROJECT、POINT、DEVICE、SENSOR、MODULE、VIRTUAL、CUSTOM | -| local_key | varchar(128) | 本地唯一键,设备级使用 serialNumber | -| external_id | varchar(128) | 外部资源 id | -| external_code | varchar(128) | 外部资源编码 | -| external_name | varchar(128) | 外部资源名称 | -| resource_payload | json | 外部资源详情 | -| status | tinyint | 0失效,1有效 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | - -建议索引: - -```text -uk_resource(tenant_channel_id, binding_id, resource_type, local_key) -idx_resource_tenant_type(tenant_id, tenant_channel_id, resource_type) -idx_external_id(tenant_channel_id, resource_type, external_id) -``` - -### 9.9 iot_platform_binding - -设备绑定,租户级配置。 - -| 字段 | 类型 | 说明 | -| -------------------- | ------------ | ----------------------------------------------- | -| id | bigint | 主键 | -| tenant_id | bigint | 租户 id | -| tenant_channel_id | bigint | 租户平台实例 id | -| flow_id | bigint | 平台流程 id | -| product_id | bigint | 产品 id | -| product_name | varchar(128) | 产品名称冗余 | -| device_id | bigint | FastBee 设备 id | -| device_name | varchar(128) | FastBee 设备名称 | -| serial_number | varchar(64) | FastBee 设备编号 | -| external_user_id | varchar(128) | 绑定级第三方用户 id,可为空 | -| external_project_id | varchar(128) | 绑定级第三方项目 id,可为空 | -| external_device_type | varchar(32) | 第三方设备类型,例如 DCP RAIN、INCLINO | -| binding_name | varchar(128) | 绑定名称 | -| exchange_mode | varchar(32) | OUTBOUND_PUSH、INBOUND_PULL、INBOUND_RECEIVE、BOTH | -| config_json | json | 绑定级配置 | -| last_success_time | datetime | 最近成功交换时间 | -| last_success_value | varchar(128) | 最近成功值,雨量递增等场景使用 | -| status | tinyint | 0停用,1启用 | -| del_flag | char(1) | 0正常,2删除 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | - -建议索引: - -```text -idx_binding_device(tenant_id, product_id, serial_number) -idx_binding_channel_flow(tenant_channel_id, flow_id) -uk_binding(tenant_channel_id, flow_id, serial_number, del_flag) -``` - -### 9.10 iot_platform_field_mapping - -字段映射,按绑定维护。 - -| 字段 | 类型 | 说明 | -| ---------------- | ------------ | --------------------------------------------------------------------- | -| id | bigint | 主键 | -| tenant_id | bigint | 租户 id | -| binding_id | bigint | 绑定 id | -| direction | varchar(32) | OUTBOUND、INBOUND | -| target_path | varchar(256) | 目标字段路径 | -| target_name | varchar(128) | 目标字段名称 | -| source_type | varchar(32) | THING_MODEL、SYSTEM、CONSTANT、JSONPATH、EXPRESSION、RESOURCE | -| source_key | varchar(256) | 物模型 identifier、系统字段、表达式等 | -| source_name | varchar(128) | 来源字段名称 | -| transform_type | varchar(32) | DIRECT、DATE_FORMAT、UNIT_CONVERT、ENUM、NUMBER_SCALE、TEMPLATE、EXPRESSION | -| transform_config | json | 转换配置 | -| default_value | varchar(512) | 默认值 | -| required | tinyint | 是否必填 | -| sort_order | int | 排序 | -| status | tinyint | 0停用,1启用 | - -建议索引: - -```text -idx_mapping_binding(binding_id, direction) -``` - -### 9.11 iot_platform_exchange_task - -数据交换任务,租户级。用于定时推送、主动拉取、资源同步。 - -| 字段 | 类型 | 说明 | -| ----------------- | ------------ | ----------------------------------------- | -| id | bigint | 主键 | -| tenant_id | bigint | 租户 id | -| tenant_channel_id | bigint | 租户平台实例 id | -| flow_id | bigint | 流程 id | -| task_name | varchar(128) | 任务名称 | -| task_type | varchar(32) | OUTBOUND_PUSH、INBOUND_PULL、RESOURCE_SYNC | -| cron_expression | varchar(128) | Cron 表达式 | -| window_type | varchar(32) | LAST_SUCCESS、RECENT_MINUTES、CUSTOM、CURSOR | -| window_config | json | 时间窗口配置 | -| cursor_json | json | 拉取游标、分页位置、最近外部 id 等 | -| sink_type | varchar(32) | INBOUND 数据落点类型 | -| batch_size | int | 批次大小 | -| max_retry | int | 最大重试次数 | -| status | tinyint | 0停用,1启用 | -| last_run_time | datetime | 最近执行时间 | -| next_run_time | datetime | 下次执行时间 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | - -### 9.12 iot_platform_task_binding - -交换任务与设备绑定关系。 - -| 字段 | 类型 | 说明 | -| ---------- | ------- | ------- | -| id | bigint | 主键 | -| task_id | bigint | 任务 id | -| binding_id | bigint | 绑定 id | -| status | tinyint | 0停用,1启用 | - -建议索引: - -```text -uk_task_binding(task_id, binding_id) -``` - -### 9.13 iot_platform_exchange_record - -数据交换记录。OUTBOUND 时是 outbox;INBOUND 时是 inbox/消费审计。 - -| 字段 | 类型 | 说明 | -| ------------------- | ------------- | --------------------------------------------------------------------------------- | -| id | bigint | 主键 | -| tenant_id | bigint | 租户 id | -| tenant_channel_id | bigint | 租户平台实例 id | -| flow_id | bigint | 流程 id | -| binding_id | bigint | 绑定 id,可为空 | -| task_id | bigint | 任务 id,接收入口可为空 | -| direction | varchar(32) | OUTBOUND_PUSH、INBOUND_PULL、INBOUND_RECEIVE | -| source_type | varchar(32) | DEVICE_PROPERTY、DEVICE_EVENT、ALERT_EVENT、RESOURCE、RAW 等 | -| sink_type | varchar(32) | DEVICE_PROPERTY、DEVICE_EVENT、ALERT_EVENT、RESOURCE_MAPPING、BUSINESS_TABLE、RAW_ONLY | -| source_device_id | bigint | FastBee 设备 id,可为空 | -| serial_number | varchar(64) | FastBee 设备编号,可为空 | -| external_message_id | varchar(128) | 第三方消息 id,可为空 | -| source_ref_id | varchar(128) | 本地来源或外部来源 id | -| source_time | datetime | 数据时间 | -| time_window_start | datetime | 批量窗口开始 | -| time_window_end | datetime | 批量窗口结束 | -| raw_payload | json | 原始输入或原始响应 | -| standard_payload | json | 标准数据交换消息 | -| request_payload | json | 最终请求 payload,OUTBOUND 使用 | -| response_payload | json | 响应或处理结果 | -| payload_hash | varchar(64) | payload 哈希 | -| idempotency_key | varchar(128) | 幂等 key | -| status | varchar(32) | PENDING、PROCESSING、SUCCESS、FAILED、RETRY_WAIT、SKIPPED、CANCELLED | -| retry_count | int | 已重试次数 | -| max_retry | int | 最大重试次数 | -| next_retry_time | datetime | 下次重试时间 | -| last_error | varchar(1000) | 最近错误 | -| locked_by | varchar(64) | worker 锁 | -| locked_time | datetime | 锁定时间 | -| success_time | datetime | 成功时间 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | - -建议索引: - -```text -uk_idempotency(idempotency_key) -idx_record_poll(status, next_retry_time) -idx_record_direction(tenant_channel_id, direction, source_time) -idx_record_device(tenant_channel_id, serial_number, source_time) -idx_record_binding(binding_id, source_time) -``` - -### 9.14 iot_platform_exchange_attempt - -数据交换尝试日志。 - -| 字段 | 类型 | 说明 | -| --------------- | ------------- | ------------------------------------------ | -| id | bigint | 主键 | -| tenant_id | bigint | 租户 id | -| record_id | bigint | 交换记录 id | -| attempt_no | int | 第几次尝试 | -| direction | varchar(32) | OUTBOUND_PUSH、INBOUND_PULL、INBOUND_RECEIVE | -| request_url | varchar(1024) | 请求地址或接收入口 | -| request_headers | json | 请求头,敏感信息脱敏 | -| request_payload | json | 请求体 | -| response_status | int | HTTP 状态码或处理状态码 | -| response_body | mediumtext | 响应内容 | -| success | tinyint | 是否成功 | -| error_msg | varchar(1000) | 错误信息 | -| duration_ms | int | 耗时 | -| create_time | datetime | 创建时间 | - -建议索引: - -```text -idx_attempt_record(record_id, attempt_no) -idx_attempt_tenant(tenant_id, create_time) -``` - -## 10. 技术实现建议 - -### 10.1 模块与包 - -后端先放在现有 IoT 服务和 open-api 中: - -```text -fastbee-open-api/src/main/java/com/fastbee/controller/platform -fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform -fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/platform -``` - -前端目录: - -```text -yack-web/src/views/iot/platformConnect -yack-web/src/api/iot/platformConnect.js -``` - -权限前缀建议使用 `iot:platformConnect`,避免和现有第三方登录 `iot:platform` 混淆。 - -### 10.2 核心接口 - -```java -public interface PlatformAdapter { - String adapterCode(); - void beforeExecute(PlatformContext context); - void ensureResources(PlatformContext context); - Object transform(PlatformContext context); - ExchangeResult execute(PlatformContext context, Object payload); -} -``` - -```java -public interface PlatformFlowExecutor { - ExchangeResult execute(Long tenantChannelId, Long flowId, PlatformExchangeMessage message, Long bindingId); -} -``` - -```java -public interface PlatformExchangeRecordService { - void createOutboundRecords(PlatformExchangeMessage message); - void createPullRecords(Long taskId, Date beginTime, Date endTime); - void createReceiveRecord(Long tenantChannelId, Long flowId, Object rawPayload); - void retry(Long recordId, boolean force); - void retryBatch(List recordIds, boolean force); -} -``` - -### 10.3 OUTBOUND_PUSH 接入点 - -设备上报原流程保持不变,在设备数据入库或规则处理附近增加平台交换事件: - -```text -设备数据解析 - -> 原有设备日志/历史数据处理 - -> 构造 PlatformExchangeMessage(direction=OUTBOUND_PUSH) - -> PlatformExchangeRecordService.createOutboundRecords(message) - -> 匹配 tenant_id + device_id 的启用绑定 - -> 生成 PENDING 交换记录 - -> worker 异步执行流程 -``` - -### 10.4 INBOUND_PULL 调度 - -优先复用 Quartz/sys_job: - -```text -Quartz 触发 - -> 读取 iot_platform_exchange_task - -> direction=INBOUND_PULL 或 RESOURCE_SYNC - -> 根据 window_type/cursor_json 计算拉取范围 - -> 调第三方查询接口 - -> 分页/游标处理 - -> 生成或直接处理 exchange_record - -> 写入 sink_type 对应业务落点 - -> 更新 cursor_json -``` - -### 10.5 INBOUND_RECEIVE 接收入口 - -建议新增统一接收入口,不把旧 `/bridge/**` 作为平台对接主入口。 - -```text -POST /platform/receive/{tenantChannelCode}/{flowCode} -``` - -处理流程: - -```text -解析 tenantChannelCode + flowCode - -> 加载租户平台实例和 RECEIVE 流程 - -> 校验 token/签名/IP 白名单 - -> 提取幂等 key - -> 创建 INBOUND_RECEIVE exchange_record - -> 同步处理或交给 worker 异步处理 - -> 返回接收结果 -``` - -### 10.6 worker 处理 - -worker 轮询: - -```text -status in (PENDING, RETRY_WAIT) -and next_retry_time <= now -``` - -抢占记录时使用数据库乐观更新,避免多实例重复消费: - -```sql -UPDATE iot_platform_exchange_record -SET status = 'PROCESSING', locked_by = ?, locked_time = NOW() -WHERE id = ? - AND status IN ('PENDING', 'RETRY_WAIT') -``` - -处理结果:成功写 `SUCCESS`,可重试失败写 `RETRY_WAIT`,超过次数写 `FAILED`,业务跳过写 `SKIPPED`。 - -## 11. 幂等、游标与重试 - -### 11.1 OUTBOUND_PUSH 幂等 - -实时数据优先使用原始日志 id: - -```text -tenantChannelId + flowId + bindingId + OUTBOUND_PUSH + sourceRefId + payloadHash -``` - -没有 `sourceRefId` 时使用: - -```text -tenantChannelId + flowId + bindingId + OUTBOUND_PUSH + sourceTime + payloadHash -``` - -### 11.2 INBOUND_PULL 幂等 - -优先使用第三方数据 id: - -```text -tenantChannelId + flowId + INBOUND_PULL + externalMessageId -``` - -没有第三方 id 时使用: - -```text -tenantChannelId + flowId + INBOUND_PULL + sourceTime + payloadHash -``` - -拉取任务必须维护 `cursor_json` 或时间窗口,避免重复和无界拉取。 - -### 11.3 INBOUND_RECEIVE 幂等 - -优先从第三方请求中提取 messageId、eventId、deviceCode + time 等业务幂等键: - -```text -tenantChannelId + flowId + INBOUND_RECEIVE + externalMessageId -``` - -如果第三方没有幂等字段,可使用请求体 hash,但需要注意同一业务重复发送和相同 payload 的边界。 - -### 11.4 外部资源幂等 - -资源创建前先查 `iot_platform_resource_mapping`。 - -DCP 设备级资源必须按 `tenantChannelId + bindingId + serialNumber + resourceType` 唯一。 - -```text -查 mapping - -> 存在则复用 - -> 不存在则加 Redis 锁 - -> 再查一次 - -> 调第三方创建接口或适配器方法 - -> 保存 mapping -``` - -DCP 锁 key: - -```text -platform:dcp:resource:{tenantId}:{tenantChannelId}:{serialNumber} -``` - -### 11.5 重试和重放 - -- OUTBOUND_PUSH:失败可自动重试和手动重推。 -- INBOUND_PULL:失败可重试当前窗口;成功后再推进游标。 -- INBOUND_RECEIVE:同步处理失败由第三方重试;异步处理失败可由 FastBee 内部重试。 -- 成功记录强制重放:新建 MANUAL/REPLAY 记录,幂等 key 追加手动批次号。 - -## 12. 权限建议 - -权限前缀:`iot:platformConnect`。 - -```text -iot:platformConnect:template:list/query/add/edit/remove -iot:platformConnect:api:list/add/edit/remove -iot:platformConnect:flow:list/add/edit/remove -iot:platformConnect:tenantChannel:list/query/add/edit/remove -iot:platformConnect:resource:list/query/sync -iot:platformConnect:binding:list/query/add/edit/unbind/initResource -iot:platformConnect:task:list/add/edit/remove/run -iot:platformConnect:record:list/query/retry/replay -``` - -平台模板、接口、流程建议仅平台管理员可维护;租户管理员主要维护租户接入、资源映射、设备绑定、交换任务和交换记录。 - -## 13. 实施计划 - -### 阶段一:平台模板与租户接入骨架 - -- 新增平台模板、接口、字段、流程、流程步骤表。 -- 新增租户平台实例表。 -- 建立 DCP 平台模板和 DCP OUTBOUND_PUSH/RESOURCE_SYNC 基础流程。 -- 实现租户级授权配置、接收安全配置、默认用户/项目映射。 - -### 阶段二:资源映射、设备绑定与字段映射 - -- 新增资源映射、设备绑定、字段映射页面和接口。 -- 实现 DCP 一设备一测点、一设备一 DCP 设备资源建模。 -- 实现 DCP token、userId、projectId、pointId、dcpDeviceId、sensor/module/virtual 资源初始化。 - -### 阶段三:OUTBOUND_PUSH 数据交换闭环 - -- 新增 exchange_task、exchange_record、exchange_attempt。 -- 实现设备上报生成 OUTBOUND_PUSH 交换记录。 -- 实现 worker、重试、幂等和手动重放。 -- 支持雨量计实时推送闭环。 - -### 阶段四:INBOUND_PULL 和 RESOURCE_SYNC - -- 实现平台交换任务的定时拉取。 -- 支持分页、游标、时间窗口。 -- 支持用户、项目、设备、测点等资源同步。 -- 支持拉取数据写入明确 sink_type。 - -### 阶段五:INBOUND_RECEIVE 接收能力 - -- 新增统一接收入口。 -- 支持 token、签名、IP 白名单、幂等 key 提取。 -- 支持同步处理和异步 exchange_record 处理。 -- 支持接收数据写入设备属性、事件、告警、资源映射或 RAW_ONLY。 - -### 阶段六:通用平台增强 - -- 通用 HTTP 平台模板。 -- 接口调试。 -- 模板导入导出。 -- MQTT/DB 适配器按需扩展。 -- 日志归档和敏感信息脱敏策略。 - -## 14. 风险与约束 - -- 平台模板是共享基础能力,租户授权、项目、资源、设备绑定、交换记录必须按 `tenant_id` 隔离。 -- INBOUND_PULL 和 INBOUND_RECEIVE 都是外部输入,必须做鉴权、校验、幂等和明确落点。 -- DCP 一测点一设备是核心建模约束,不能把测点设计成产品级或平台级共享资源。 -- OUTBOUND_PUSH 必须异步,不影响设备上报吞吐。 -- token、密码、请求头、签名密钥等敏感信息需要加密保存和脱敏展示。 -- 交换日志增长较快,需要后续增加归档或清理策略。 -- 字段映射表达式必须受限,不能执行任意代码。 -- INBOUND 写业务表必须通过白名单 sink adapter,不能开放任意 SQL。 -- 定时拉取和补推要限制窗口、批次大小和并发数,避免压垮第三方平台或本系统。 -- 雨量累计值不递增时应跳过并记录原因,不应无限重试。 - -## 15. 结论 - -平台对接应定位为可复用的数据交换/iPaaS Connector 能力,而不是单个平台或单一“推送”能力。 - -推荐模型: - -```text -平台模板(共享) - -> 接口配置(共享) - -> 流程编排(共享) - -> 适配器(共享) - -> 租户平台实例(租户级) - -> 用户/项目/资源映射(租户级) - -> 设备绑定和字段映射(租户级) - -> 数据交换任务(租户级) - -> 数据交换记录和尝试日志(租户级) -``` - -其中数据方向明确分为: - -```text -OUTBOUND_PUSH:FastBee 作为 producer 推送数据 -INBOUND_PULL:FastBee 主动拉取,但作为 consumer 消费第三方数据 -INBOUND_RECEIVE:FastBee 被动接收,并作为 consumer 消费第三方数据 -``` - -DCP 是第一批落地模板和适配器,必须保留“一测点一设备”的资源约束;其他租户复用 DCP 平台模板后,只需配置自己的 DCP 账号、用户、项目、设备绑定、字段映射和数据交换策略。 diff --git a/docs/knowledge-base/06_1-platform-intergration-design.md b/docs/knowledge-base/06_1-platform-intergration-design.md deleted file mode 100644 index 7dbe09c2..00000000 --- a/docs/knowledge-base/06_1-platform-intergration-design.md +++ /dev/null @@ -1,1013 +0,0 @@ -# 平台对接需求与技术设计 - -## 1. 背景 - -需求目录 `docs/platform/` 中目前包含 DCP 雨量计、倾角、静力水准、轴力计接口文档。DCP 是当前优先落地的平台,但平台对接能力不能只服务一个平台、一个租户。平台一旦完成接入,后续其他租户应复用同一套平台定义、接口定义、流程编排、适配器和推送执行能力;租户只需要维护自己的第三方账号/用户、项目、设备、字段映射和推送任务。 - -当前 FastBee 已具备以下基础能力: - -- 设备、产品、物模型:设备通过 `iot_device` 管理,物模型通过产品和设备编号查询。 -- 设备上报处理:设备数据解析后进入原有入库和规则处理链路。 -- 历史数据查询:数据中心接口支持按设备、时间窗口和物模型标识符查询历史数据。 -- 数据桥接:现有 `bridge`、`iot_script`、`iot_script_bridge` 支持 HTTP、MQTT、数据库桥接。 -- 定时任务:系统已有 Quartz 任务体系,可复用为定时推送和补偿任务触发器。 -- 多租户:IoT 主要业务表已有 `tenant_id`、`tenant_name` 约定,平台对接也应按租户隔离业务映射和推送数据。 - -因此推荐建设系统级 **平台对接 / iPaaS Connector** 能力:平台基础接入可复用,租户业务映射可隔离,DCP 等复杂平台通过专用适配器补充规则。 - -## 2. 核心目标 - -平台对接需要同时满足“平台共用”和“租户隔离”: - -- 平台接入人员只需维护一次第三方平台模板:认证方式、接口、字段、流程、适配器。 -- 多个租户可启用同一个平台模板,不重复配置接口和流程。 -- 每个租户维护自己的第三方账号/用户授权、项目映射、设备绑定、字段映射和推送任务。 -- 设备上报后按租户和设备绑定匹配推送目标,生成推送记录并异步执行。 -- 支持定时推送和按时间范围补推,复用现有历史数据查询能力。 -- 支持幂等、失败重试、手动重推、推送日志和审计。 -- DCP 这类平台保留专用适配器,处理 token、项目、测点、设备、传感器、雨量递增等平台规则。 -- DCP 必须遵守 **一个测点对应一个 FastBee 设备**,不能将测点设计为平台级或产品级共享资源。 - -## 3. 分层模型 - -平台对接按三层拆分: - -```text -平台模板层(全局/系统级,可复用) - -> 平台定义、接口定义、字段定义、流程编排、适配器 - -租户接入层(租户级,隔离) - -> 租户平台实例、第三方账号/用户、项目映射、资源映射、设备绑定、字段映射、推送任务 - -执行审计层(租户级,隔离) - -> 推送记录 outbox、推送尝试日志、重试、重推、补偿 -``` - -### 3.1 平台模板层 - -平台模板层由平台管理员维护,配置一次、多租户复用。 - -包含: - -- 第三方平台类型,例如 DCP、通用 HTTP、通用 MQTT、通用数据库。 -- 认证方式,例如无认证、固定 Token、Basic、Bearer、OAuth2、自定义 Token 接口。 -- 接口定义,例如获取用户、获取项目、创建测点、创建设备、绑定传感器、推送数据。 -- 接口字段定义,用于页面展示和字段映射。 -- 流程编排,例如“获取用户 -> 获取项目 -> 确保测点 -> 确保设备 -> 推送数据”。 -- 适配器编码,例如 `genericHttp`、`dcp`。 - -### 3.2 租户接入层 - -租户接入层由租户管理员或实施人员维护。 - -包含: - -- 租户启用哪个平台模板。 -- 租户自己的第三方账号、token、clientId/clientSecret 等授权配置。 -- 租户第三方用户和项目映射。 -- FastBee 设备与第三方资源的绑定。 -- FastBee 物模型字段与第三方接口字段的映射。 -- 实时/定时推送任务和补推策略。 - -同一个平台模板下,不同租户的数据必须隔离:授权配置、项目、用户、资源映射、设备绑定、推送记录都必须带 `tenant_id`。 - -### 3.3 执行审计层 - -执行审计层面向运行时。 - -```text -设备上报 / Quartz 定时任务 / 手动补推 - -> 标准上报快照 PlatformReport - -> 按 tenant_id + device_id 匹配启用绑定 - -> 加载租户平台实例和流程 - -> 字段映射与数据转换 - -> 资源检查/懒加载 - -> 生成推送记录 iot_platform_push_record - -> worker 异步执行 - -> 写推送尝试日志 iot_platform_push_attempt -``` - -## 4. 与现有系统功能的关系 - -| 现有能力 | 复用方式 | -| --- | --- | -| 设备管理 `iot_device` | 设备绑定引用 `device_id`、`serial_number`、`product_id`、`tenant_id`。 | -| 产品物模型 | 绑定页面通过现有物模型接口展示字段,字段映射使用物模型 `identifier`。 | -| 设备实时上报链路 | 在原有数据处理完成后发布平台推送事件,不改变协议解析逻辑。 | -| 数据中心历史查询 | 定时推送和手动补推按设备、时间窗口、identifier 取历史数据。 | -| 规则引擎和数据桥接 | 保留给轻量 HTTP/MQTT/DB 转发;平台对接面向多步骤、多租户、可重推场景。 | -| Quartz 定时任务 | 平台推送任务可复用 sys_job/Quartz 触发。 | -| Redis/Redisson | 缓存 token,资源创建时做并发锁。 | -| 若依权限体系 | 继续使用菜单、角色、`@PreAuthorize` 权限控制。 | - -现有数据桥接仍保留。它适合“固定 URL + 固定模板 + 脚本触发”的轻量转发;平台对接用于第三方平台长期集成,需要多租户账号、项目、设备映射、流程编排、资源幂等、失败重试和审计。 - -## 5. 前端功能设计 - -一级菜单:**平台对接**。 - -建议菜单: - -```text -平台对接 - 平台模板 - 接口配置 - 流程编排 - 租户接入 - 设备绑定 - 推送任务 - 推送记录 -``` - -### 5.1 平台模板 - -平台模板是系统级可复用配置。 - -主要字段: - -| 字段 | 说明 | -| --- | --- | -| 平台名称 | 用户可读名称,例如 DCP。 | -| 平台编码 | 全局唯一,例如 `DCP`。 | -| 平台类型 | DCP、GENERIC_HTTP、GENERIC_MQTT、GENERIC_DB。 | -| 适配器编码 | dcp、genericHttp 等。 | -| 基础地址模板 | 可作为租户实例默认 baseUrl。 | -| 认证方式 | OAuth2、自定义 Token、固定 Token 等。 | -| 默认请求配置 | 超时、重试、Content-Type 等。 | -| 启用状态 | 启用、停用。 | - -### 5.2 接口配置 - -接口配置属于平台模板,可被所有租户复用。 - -接口类型: - -| 类型 | 说明 | -| --- | --- | -| TOKEN | 获取或刷新 token。 | -| QUERY | 查询第三方基础数据,如用户、项目。 | -| RESOURCE_CREATE | 创建外部资源,如测点、设备、传感器。 | -| RESOURCE_BIND | 绑定外部资源。 | -| PUSH | 推送设备数据。 | - -配置项: - -- 请求方法:GET、POST、PUT、DELETE。 -- URL 模板:支持 `${变量}`。 -- Header、Query、Body 模板。 -- Content-Type。 -- 成功条件:HTTP 状态码、响应 JSONPath。 -- 响应变量提取:例如 `$.id -> pointId`。 -- 超时和重试策略。 - -### 5.3 接口字段 - -接口字段用于设备绑定页面右侧展示,帮助租户配置字段映射。 - -| 字段 | 说明 | -| --- | --- | -| 接口 | 所属接口。 | -| 方向 | REQUEST、RESPONSE。 | -| 字段路径 | JSONPath 或表单字段名。 | -| 字段名称 | 展示名称。 | -| 数据类型 | string、number、boolean、array、object。 | -| 是否必填 | 是否必填。 | -| 默认值 | 默认值。 | -| 示例值 | 示例值。 | - -### 5.4 流程编排 - -流程属于平台模板,用于将多个接口和适配器方法组成可复用业务流程。 - -示例:DCP 数据推送流程。 - -```text -获取/刷新 token - -> 获取用户 userId - -> 获取/选择项目 projectId - -> 确保测点 pointId - -> 确保设备 dcpDeviceId - -> 确保传感器/附属资源 - -> 构造 payload - -> 推送数据 -``` - -流程步骤类型: - -| 步骤类型 | 说明 | -| --- | --- | -| API | 调用平台接口配置。 | -| ADAPTER | 调用专用适配器方法。 | -| CONDITION | 条件判断。 | -| TRANSFORM | 数据转换。 | - -流程模板可以被多个租户复用;步骤执行时使用当前租户的授权配置、项目映射、设备绑定和资源映射。 - -### 5.5 租户接入 - -租户接入表示某个租户启用某个平台模板。 - -主要字段: - -| 字段 | 说明 | -| --- | --- | -| 租户 | 当前租户。 | -| 平台模板 | 选择 DCP 等平台模板。 | -| 实例名称 | 租户内展示名。 | -| baseUrl | 可覆盖模板默认地址。 | -| 授权配置 | 租户自己的账号、clientId、secret、token 等。 | -| 默认第三方用户 | DCP userId 或其他平台用户。 | -| 默认项目 | DCP projectId 或其他平台项目。 | -| 启用状态 | 启用、停用。 | - -租户接入配置完成后,该租户才能建立设备绑定、推送任务和资源映射。 - -### 5.6 设备绑定 - -设备绑定是租户级配置。 - -绑定内容: - -```text -租户平台实例 + FastBee 设备 + 平台流程 + 第三方用户/项目 + 字段映射 + 推送模式 -``` - -绑定页面左侧使用现有 FastBee 产品、设备、物模型;右侧使用平台模板的接口字段和租户的第三方用户/项目/资源。 - -绑定支持:新增、修改、启用、停用、解绑、初始化资源、查看资源映射、查看推送记录。 - -DCP 绑定规则: - -```text -FastBee 一个设备 = DCP 一个测点 = DCP 一个设备资源 -``` - -DCP 的 `pointId`、`dcpDeviceId`、sensor/module/virtual 等必须按租户设备绑定保存,不能平台级共享。 - -### 5.7 字段映射 - -字段映射按设备绑定维护,也可以支持从平台模板复制默认映射。 - -映射来源: - -| 来源类型 | 说明 | -| --- | --- | -| THING_MODEL | 物模型字段,例如 rainfall。 | -| SYSTEM | 系统字段,例如 serialNumber、deviceName、reportTime。 | -| CONSTANT | 常量。 | -| RESOURCE | 外部资源映射,例如 pointId、dcpDeviceId。 | -| JSONPATH | 从 rawPayload 中取值。 | -| EXPRESSION | 受限表达式,后续增强。 | - -第一阶段转换能力建议支持:直接映射、时间格式化、单位换算、数值倍率、枚举转换、默认值。表达式必须受限,不能执行任意代码。 - -### 5.8 推送任务与推送记录 - -推送任务是租户级配置,支持: - -- 实时推送:设备上报后生成推送记录。 -- 定时推送:按 Quartz/sys_job 触发,从历史数据按窗口生成推送记录。 -- 手动补推:按设备和时间范围生成推送记录。 - -推送记录展示业务层面的推送状态: - -- 按租户、平台、流程、设备、状态、时间筛选。 -- 查看最终 payload、幂等 key、请求响应、错误信息。 -- 失败重推、批量重推、成功记录强制重推。 - -## 6. 标准上报快照 - -字段映射不要直接依赖原始 MQTT/HTTP payload。系统应先把设备数据整理成统一快照。 - -```json -{ - "tenantId": 1, - "productId": 10, - "deviceId": 1001, - "serialNumber": "ABC001", - "deviceName": "雨量计-001", - "reportTime": "2026-05-27 10:00:00", - "sourceRefId": "device_log_id_or_history_id", - "values": { - "rainfall": { - "value": 12.3, - "unit": "mm", - "ts": "2026-05-27 10:00:00" - }, - "temperature": { - "value": 22.1, - "unit": "℃", - "ts": "2026-05-27 10:00:00" - } - }, - "rawPayload": {} -} -``` - -`values` 的 key 使用物模型 `identifier`,便于复用现有物模型和历史数据查询能力。 - -## 7. DCP 适配设计 - -DCP 通过平台模板 + 流程编排 + 专用适配器落地。 - -### 7.1 DCP 平台模板 - -DCP 平台模板包含: - -- token 接口:`POST /connect/token`。 -- 用户接口:获取 userId。 -- 项目接口:按 userId 获取项目。 -- 资源接口:创建测点、创建设备、创建/绑定传感器等。 -- 推送接口:雨量、倾角、静力水准、轴力计数据推送。 -- DCP 专用适配器:处理 token 缓存、资源幂等、payload 构造、特殊规则。 - -这些配置由系统维护一次,所有租户复用。 - -### 7.2 DCP 租户映射 - -每个租户需要维护自己的 DCP 映射: - -```text -租户 - -> DCP 租户平台实例 - -> DCP 账号/用户 userId - -> DCP 项目 projectId - -> FastBee 设备绑定 DCP 测点 pointId - -> FastBee 设备绑定 DCP 设备 dcpDeviceId - -> 设备附属 sensor/module/virtual -``` - -同一个 DCP 平台模板下,不同租户可以使用不同 DCP 账号、用户和项目。资源映射必须按 `tenant_id` 隔离。 - -### 7.3 DCP 一测点一设备约束 - -DCP 资源映射必须按“一测点一设备”建模: - -```text -FastBee Device(serialNumber) - -> DCP Project(projectId) - -> DCP Point(pointId) - -> DCP Device(dcpDeviceId) - -> DCP Sensor/Module/Virtual(按设备类型可选) -``` - -规则: - -- `PROJECT` 可以是租户平台实例级默认资源,也可以绑定级覆盖。 -- `USER`、`PROJECT` 是租户级或绑定级映射,不是全局共享。 -- `POINT` 必须是设备级资源,一个 FastBee 设备对应一个 DCP 测点。 -- `DEVICE` 必须是设备级资源,一个 FastBee 设备对应一个 DCP 设备资源。 -- `SENSOR`、`MODULE`、`VIRTUAL` 是设备附属资源,按绑定保存。 -- 静力水准 payload 中可包含多条 `sensorData`,但仍归属同一个 FastBee 设备对应的 DCP 测点和 DCP 设备下。 - -### 7.4 DCP 运行流程 - -```text -设备上报或定时任务 - -> 匹配租户 DCP 设备绑定 - -> 加载租户 DCP 授权配置 - -> 获取/刷新 token - -> 加载 userId/projectId 映射 - -> 加载 pointId/dcpDeviceId/sensor 映射 - -> 资源缺失时按流程懒加载或手动初始化 - -> 构造对应设备类型 payload - -> 调用 DCP 数据接口 - -> 写推送记录和尝试日志 -``` - -资源创建必须幂等。并发上报同一设备时,使用 Redis/Redisson 锁: - -```text -platform:dcp:resource:{tenantId}:{tenantChannelId}:{serialNumber} -``` - -### 7.5 DCP 特殊规则 - -- 雨量 `value` 为累计降雨量,DCP 要求新值大于历史值。系统使用绑定的 `last_success_value` 或资源扩展字段记录最近成功值;不满足时记录为 `SKIPPED`,不反复重试。 -- 倾角需要同一时间的 x、y、z 和温度,必须按一次设备快照组包,不能拆成多个单字段推送。 -- 静力水准的 `sensorData` 可包含多条传感器数据,但这些数据属于同一个 FastBee 设备对应的 DCP 设备资源。 -- 轴力计按独立设备类型处理,字段映射和 payload 由 `DcpPlatformAdapter` 中对应 builder 负责。 -- DCP token 缓存在 Redis,key 建议:`platform:dcp:token:{tenantId}:{tenantChannelId}`。 -- 401 或 token 过期时刷新 token 后重试一次,再失败则记录错误。 - -## 8. 表设计 - -表名前缀使用 `iot_platform_`,避免和现有 `iot_social_platform` 混淆。 - -### 8.1 表清单 - -| 表名 | 层级 | 说明 | -| --- | --- | --- | -| `iot_platform_template` | 平台模板层 | 平台模板,全局可复用。 | -| `iot_platform_api` | 平台模板层 | 第三方接口定义。 | -| `iot_platform_api_field` | 平台模板层 | 接口字段定义。 | -| `iot_platform_flow` | 平台模板层 | 流程模板。 | -| `iot_platform_flow_step` | 平台模板层 | 流程步骤。 | -| `iot_platform_tenant_channel` | 租户接入层 | 租户启用的平台实例和授权配置。 | -| `iot_platform_resource_mapping` | 租户接入层 | 用户、项目、测点、设备、传感器等资源映射。 | -| `iot_platform_binding` | 租户接入层 | FastBee 设备与平台流程/项目/资源绑定。 | -| `iot_platform_field_mapping` | 租户接入层 | 绑定级字段映射。 | -| `iot_platform_push_task` | 租户接入层 | 定时推送任务。 | -| `iot_platform_task_binding` | 租户接入层 | 推送任务与绑定关系。 | -| `iot_platform_push_record` | 执行审计层 | 推送记录,兼 outbox。 | -| `iot_platform_push_attempt` | 执行审计层 | 每次推送尝试日志。 | - -### 8.2 iot_platform_template - -平台模板,全局可复用。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint | 主键 | -| platform_code | varchar(64) | 平台编码,全局唯一 | -| platform_name | varchar(128) | 平台名称 | -| platform_type | varchar(32) | DCP、GENERIC_HTTP、GENERIC_MQTT、GENERIC_DB | -| adapter_code | varchar(64) | dcp、genericHttp 等 | -| base_url_template | varchar(512) | 默认基础地址模板 | -| auth_type | varchar(32) | 认证类型 | -| auth_schema | json | 授权配置字段定义,用于租户填写 | -| request_config | json | 默认超时、重试、TLS 等 | -| status | tinyint | 0停用,1启用 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| remark | varchar(500) | 备注 | - -建议索引: - -```text -uk_platform_code(platform_code) -idx_platform_type(platform_type) -``` - -### 8.3 iot_platform_api - -第三方接口定义,属于平台模板。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint | 主键 | -| template_id | bigint | 平台模板 id | -| api_code | varchar(64) | 接口编码 | -| api_name | varchar(128) | 接口名称 | -| api_type | varchar(32) | TOKEN、QUERY、PUSH、RESOURCE_CREATE、RESOURCE_BIND | -| method | varchar(16) | GET、POST、PUT、DELETE | -| path_template | varchar(512) | URL 路径模板 | -| content_type | varchar(64) | application/json、form-urlencoded | -| header_template | json | Header 模板 | -| query_template | json | Query 模板 | -| body_template | json | Body 模板 | -| success_condition | json | 成功条件 | -| response_mapping | json | 响应变量提取规则 | -| retry_policy | json | 接口级重试策略 | -| timeout_ms | int | 超时时间 | -| status | tinyint | 0停用,1启用 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | - -建议索引: - -```text -uk_template_api_code(template_id, api_code) -idx_template_api_type(template_id, api_type) -``` - -### 8.4 iot_platform_api_field - -接口字段定义,用于页面展示和字段映射。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint | 主键 | -| api_id | bigint | 接口 id | -| direction | varchar(16) | REQUEST、RESPONSE | -| field_path | varchar(256) | JSONPath 或表单字段名 | -| field_name | varchar(128) | 字段名称 | -| data_type | varchar(32) | string、number、boolean、array、object | -| required | tinyint | 是否必填 | -| default_value | varchar(512) | 默认值 | -| example_value | varchar(512) | 示例值 | -| description | varchar(500) | 说明 | -| sort_order | int | 排序 | - -建议索引: - -```text -idx_api_direction(api_id, direction) -``` - -### 8.5 iot_platform_flow - -流程模板,属于平台模板。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint | 主键 | -| template_id | bigint | 平台模板 id | -| flow_code | varchar(64) | 流程编码 | -| flow_name | varchar(128) | 流程名称 | -| flow_type | varchar(32) | PUSH、QUERY、RESOURCE_SYNC | -| adapter_code | varchar(64) | 可覆盖平台适配器 | -| status | tinyint | 0停用,1启用 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | -| remark | varchar(500) | 备注 | - -建议索引: - -```text -uk_template_flow_code(template_id, flow_code) -``` - -### 8.6 iot_platform_flow_step - -流程步骤。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint | 主键 | -| flow_id | bigint | 流程 id | -| step_code | varchar(64) | 步骤编码 | -| step_name | varchar(128) | 步骤名称 | -| step_type | varchar(32) | API、ADAPTER、CONDITION、TRANSFORM | -| api_id | bigint | API 步骤引用接口 id | -| adapter_method | varchar(128) | 适配器方法,例如 ensurePoint | -| input_mapping | json | 步骤入参映射 | -| output_mapping | json | 步骤输出变量映射 | -| condition_expr | varchar(512) | 执行条件 | -| on_fail | varchar(32) | STOP、SKIP、RETRY | -| sort_order | int | 执行顺序 | -| status | tinyint | 0停用,1启用 | - -建议索引: - -```text -idx_flow_sort(flow_id, sort_order) -``` - -### 8.7 iot_platform_tenant_channel - -租户启用平台模板后的平台实例。授权配置、baseUrl、默认用户/项目属于租户级。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint | 主键 | -| tenant_id | bigint | 租户 id | -| tenant_name | varchar(64) | 租户名称 | -| template_id | bigint | 平台模板 id | -| channel_code | varchar(64) | 租户内平台实例编码 | -| channel_name | varchar(128) | 租户内平台实例名称 | -| base_url | varchar(512) | 实际基础地址 | -| auth_config | json | 租户授权配置,敏感值加密 | -| default_user_id | varchar(128) | 默认第三方用户 id | -| default_project_id | varchar(128) | 默认第三方项目 id | -| config_json | json | 租户级扩展配置 | -| status | tinyint | 0停用,1启用 | -| del_flag | char(1) | 0正常,2删除 | -| create_by | varchar(64) | 创建者 | -| create_time | datetime | 创建时间 | -| update_by | varchar(64) | 更新者 | -| update_time | datetime | 更新时间 | -| remark | varchar(500) | 备注 | - -建议索引: - -```text -uk_tenant_channel_code(tenant_id, channel_code, del_flag) -idx_tenant_template(tenant_id, template_id) -``` - -### 8.8 iot_platform_resource_mapping - -外部资源映射,租户级隔离。DCP 的 user、project、point、device、sensor 等都保存在这里。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint | 主键 | -| tenant_id | bigint | 租户 id | -| tenant_channel_id | bigint | 租户平台实例 id | -| binding_id | bigint | 绑定 id,用户/项目可为空 | -| resource_scope | varchar(32) | TENANT、PROJECT、DEVICE、BINDING | -| resource_type | varchar(64) | USER、PROJECT、POINT、DEVICE、SENSOR、MODULE、VIRTUAL | -| local_key | varchar(128) | 本地唯一键,设备级使用 serialNumber | -| external_id | varchar(128) | 外部资源 id | -| external_code | varchar(128) | 外部资源编码 | -| external_name | varchar(128) | 外部资源名称 | -| resource_payload | json | 外部资源详情 | -| status | tinyint | 0失效,1有效 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | - -建议索引: - -```text -uk_resource(tenant_channel_id, binding_id, resource_type, local_key) -idx_resource_tenant_type(tenant_id, tenant_channel_id, resource_type) -idx_external_id(tenant_channel_id, resource_type, external_id) -``` - -### 8.9 iot_platform_binding - -设备绑定,租户级配置。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint | 主键 | -| tenant_id | bigint | 租户 id | -| tenant_channel_id | bigint | 租户平台实例 id | -| flow_id | bigint | 平台流程 id | -| product_id | bigint | 产品 id | -| product_name | varchar(128) | 产品名称冗余 | -| device_id | bigint | FastBee 设备 id | -| device_name | varchar(128) | FastBee 设备名称 | -| serial_number | varchar(64) | FastBee 设备编号 | -| external_user_id | varchar(128) | 绑定级第三方用户 id,可为空 | -| external_project_id | varchar(128) | 绑定级第三方项目 id,可为空 | -| external_device_type | varchar(32) | 第三方设备类型,例如 DCP RAIN、INCLINO | -| binding_name | varchar(128) | 绑定名称 | -| push_mode | varchar(16) | REALTIME、SCHEDULED、BOTH | -| config_json | json | 绑定级配置 | -| last_success_time | datetime | 最近成功推送时间 | -| last_success_value | varchar(128) | 最近成功值,雨量递增等场景使用 | -| status | tinyint | 0停用,1启用 | -| del_flag | char(1) | 0正常,2删除 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | - -建议索引: - -```text -idx_binding_device(tenant_id, product_id, serial_number) -idx_binding_channel_flow(tenant_channel_id, flow_id) -uk_binding(tenant_channel_id, flow_id, serial_number, del_flag) -``` - -### 8.10 iot_platform_field_mapping - -字段映射,按绑定维护。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint | 主键 | -| tenant_id | bigint | 租户 id | -| binding_id | bigint | 绑定 id | -| target_path | varchar(256) | 外部目标字段路径 | -| target_name | varchar(128) | 外部字段名称 | -| source_type | varchar(32) | THING_MODEL、SYSTEM、CONSTANT、JSONPATH、EXPRESSION、RESOURCE | -| source_key | varchar(256) | 物模型 identifier、系统字段、表达式等 | -| source_name | varchar(128) | 内部字段名称 | -| transform_type | varchar(32) | DIRECT、DATE_FORMAT、UNIT_CONVERT、ENUM、NUMBER_SCALE、TEMPLATE、EXPRESSION | -| transform_config | json | 转换配置 | -| default_value | varchar(512) | 默认值 | -| required | tinyint | 是否必填 | -| sort_order | int | 排序 | -| status | tinyint | 0停用,1启用 | - -建议索引: - -```text -idx_mapping_binding(binding_id) -``` - -### 8.11 iot_platform_push_task - -定时推送任务,租户级。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint | 主键 | -| tenant_id | bigint | 租户 id | -| tenant_channel_id | bigint | 租户平台实例 id | -| flow_id | bigint | 流程 id | -| task_name | varchar(128) | 任务名称 | -| cron_expression | varchar(128) | Cron 表达式 | -| window_type | varchar(32) | LAST_SUCCESS、RECENT_MINUTES、CUSTOM | -| window_config | json | 时间窗口配置 | -| batch_size | int | 批次大小 | -| max_retry | int | 最大重试次数 | -| status | tinyint | 0停用,1启用 | -| last_run_time | datetime | 最近执行时间 | -| next_run_time | datetime | 下次执行时间 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | - -### 8.12 iot_platform_task_binding - -定时任务与设备绑定关系。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint | 主键 | -| task_id | bigint | 任务 id | -| binding_id | bigint | 绑定 id | -| status | tinyint | 0停用,1启用 | - -建议索引: - -```text -uk_task_binding(task_id, binding_id) -``` - -### 8.13 iot_platform_push_record - -推送记录,租户级 outbox。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint | 主键 | -| tenant_id | bigint | 租户 id | -| tenant_channel_id | bigint | 租户平台实例 id | -| flow_id | bigint | 流程 id | -| binding_id | bigint | 绑定 id | -| task_id | bigint | 定时任务 id,实时推送为空 | -| push_mode | varchar(16) | REALTIME、SCHEDULED、MANUAL | -| source_device_id | bigint | FastBee 设备 id | -| serial_number | varchar(64) | FastBee 设备编号 | -| source_ref_id | varchar(128) | 来源日志或历史数据 id | -| source_time | datetime | 数据时间 | -| time_window_start | datetime | 批量窗口开始 | -| time_window_end | datetime | 批量窗口结束 | -| request_payload | json | 待推送 payload | -| payload_hash | varchar(64) | payload 哈希 | -| idempotency_key | varchar(128) | 幂等 key | -| status | varchar(32) | PENDING、PROCESSING、SUCCESS、FAILED、RETRY_WAIT、SKIPPED、CANCELLED | -| retry_count | int | 已重试次数 | -| max_retry | int | 最大重试次数 | -| next_retry_time | datetime | 下次重试时间 | -| last_error | varchar(1000) | 最近错误 | -| locked_by | varchar(64) | worker 锁 | -| locked_time | datetime | 锁定时间 | -| success_time | datetime | 成功时间 | -| create_time | datetime | 创建时间 | -| update_time | datetime | 更新时间 | - -建议索引: - -```text -uk_idempotency(idempotency_key) -idx_record_poll(status, next_retry_time) -idx_record_device(tenant_channel_id, serial_number, source_time) -idx_record_binding(binding_id, source_time) -``` - -### 8.14 iot_platform_push_attempt - -推送尝试日志。 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| id | bigint | 主键 | -| tenant_id | bigint | 租户 id | -| record_id | bigint | 推送记录 id | -| attempt_no | int | 第几次尝试 | -| request_url | varchar(1024) | 请求地址 | -| request_headers | json | 请求头,敏感信息脱敏 | -| request_payload | json | 请求体 | -| response_status | int | HTTP 状态码 | -| response_body | mediumtext | 响应内容 | -| success | tinyint | 是否成功 | -| error_msg | varchar(1000) | 错误信息 | -| duration_ms | int | 耗时 | -| create_time | datetime | 创建时间 | - -建议索引: - -```text -idx_attempt_record(record_id, attempt_no) -idx_attempt_tenant(tenant_id, create_time) -``` - -## 9. 技术实现建议 - -### 9.1 模块与包 - -后端先放在现有 IoT 服务和 open-api 中: - -```text -fastbee-open-api/src/main/java/com/fastbee/controller/platform -fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform -fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/platform -``` - -前端目录: - -```text -yack-web/src/views/iot/platformConnect -yack-web/src/api/iot/platformConnect.js -``` - -权限前缀建议使用 `iot:platformConnect`,避免和现有第三方登录 `iot:platform` 混淆。 - -### 9.2 核心接口 - -```java -public interface PlatformAdapter { - String adapterCode(); - void beforeExecute(PlatformContext context); - void ensureResources(PlatformContext context); - Object buildPayload(PlatformContext context); - PushResult push(PlatformContext context, Object payload); -} -``` - -```java -public interface PlatformFlowExecutor { - PushResult execute(Long tenantChannelId, Long flowId, PlatformReport report, Long bindingId); -} -``` - -```java -public interface PlatformPushRecordService { - void createRealtimeRecords(PlatformReport report); - void createScheduledRecords(Long taskId, Date beginTime, Date endTime); - void retry(Long recordId, boolean force); - void retryBatch(List recordIds, boolean force); -} -``` - -### 9.3 实时推送接入点 - -设备上报原流程保持不变,在设备数据入库或规则处理附近增加平台推送事件: - -```text -设备数据解析 - -> 原有设备日志/历史数据处理 - -> 构造 PlatformReport - -> PlatformPushRecordService.createRealtimeRecords(report) - -> 匹配 tenant_id + device_id 的启用绑定 - -> 生成 PENDING 推送记录 - -> worker 异步执行流程 -``` - -如果短期不改主链路,也可先通过现有规则引擎脚本触发平台推送服务,作为过渡方案。 - -### 9.4 定时推送 - -优先复用 Quartz/sys_job: - -```text -Quartz 触发 - -> 读取 iot_platform_push_task - -> 根据 window_type 计算时间窗口 - -> 查询任务关联绑定 - -> 调用现有历史数据服务按设备取数 - -> 构造 PlatformReport - -> 批量生成推送记录 - -> worker 执行流程 -``` - -不建议第一阶段新增独立调度框架。 - -### 9.5 worker 处理 - -worker 轮询: - -```text -status in (PENDING, RETRY_WAIT) -and next_retry_time <= now -``` - -抢占记录时使用数据库乐观更新,避免多实例重复消费: - -```sql -UPDATE iot_platform_push_record -SET status = 'PROCESSING', locked_by = ?, locked_time = NOW() -WHERE id = ? - AND status IN ('PENDING', 'RETRY_WAIT') -``` - -处理结果:成功写 `SUCCESS`,可重试失败写 `RETRY_WAIT`,超过次数写 `FAILED`,业务跳过写 `SKIPPED`。 - -## 10. 幂等与重推 - -### 10.1 推送记录幂等 - -实时数据优先使用原始日志 id: - -```text -tenantChannelId + flowId + bindingId + pushMode + sourceRefId + payloadHash -``` - -没有 `sourceRefId` 时使用: - -```text -tenantChannelId + flowId + bindingId + pushMode + sourceTime + payloadHash -``` - -定时窗口使用: - -```text -tenantChannelId + flowId + bindingId + windowStart + windowEnd + sourceTime + payloadHash -``` - -`idempotency_key` 必须唯一。 - -### 10.2 外部资源幂等 - -资源创建前先查 `iot_platform_resource_mapping`。 - -DCP 设备级资源必须按 `tenantChannelId + bindingId + serialNumber + resourceType` 唯一。 - -```text -查 mapping - -> 存在则复用 - -> 不存在则加 Redis 锁 - -> 再查一次 - -> 调第三方创建接口或适配器方法 - -> 保存 mapping -``` - -DCP 锁 key: - -```text -platform:dcp:resource:{tenantId}:{tenantChannelId}:{serialNumber} -``` - -### 10.3 手动重推 - -- 失败记录重推:原记录状态重置为 `PENDING`,保留历史 attempt。 -- 成功记录强制重推:新建 `MANUAL` 记录,幂等 key 追加手动批次号。 -- 按设备时间范围补推:查询历史数据后生成新的 `MANUAL` 推送记录。 - -## 11. 权限建议 - -权限前缀:`iot:platformConnect`。 - -```text -iot:platformConnect:template:list/query/add/edit/remove -iot:platformConnect:api:list/add/edit/remove -iot:platformConnect:flow:list/add/edit/remove -iot:platformConnect:tenantChannel:list/query/add/edit/remove -iot:platformConnect:binding:list/query/add/edit/unbind/initResource -iot:platformConnect:task:list/add/edit/remove -iot:platformConnect:record:list/query/retry -``` - -平台模板、接口、流程建议仅平台管理员可维护;租户管理员主要维护租户接入、设备绑定、推送任务和推送记录。 - -## 12. 实施计划 - -### 阶段一:平台模板与租户接入骨架 - -- 新增平台模板、接口、字段、流程、流程步骤表。 -- 新增租户平台实例表。 -- 建立 DCP 平台模板和 DCP 基础流程。 -- 实现租户级授权配置和默认用户/项目映射。 - -### 阶段二:设备绑定与 DCP 资源映射 - -- 新增设备绑定、字段映射、资源映射页面和接口。 -- 实现 DCP 一设备一测点、一设备一 DCP 设备资源建模。 -- 实现 DCP token、userId、projectId、pointId、dcpDeviceId、sensor/module/virtual 资源初始化。 - -### 阶段三:推送执行与审计 - -- 新增推送记录和推送尝试日志。 -- 实现实时推送记录生成。 -- 实现 worker、重试、幂等和手动重推。 -- 支持雨量计实时推送闭环。 - -### 阶段四:补齐 DCP 设备类型与定时补偿 - -- 倾角 payload 组包。 -- 静力水准 sensorData 配置和组包。 -- 轴力计 payload 组包。 -- 雨量递增校验和 `SKIPPED` 状态。 -- 接入 Quartz/sys_job 定时推送和按时间范围补推。 - -### 阶段五:通用平台增强 - -- 通用 HTTP 平台模板。 -- 接口调试。 -- 模板导入导出。 -- MQTT/DB 适配器按需扩展。 -- 日志归档和敏感信息脱敏策略。 - -## 13. 风险与约束 - -- 平台模板是共享基础能力,租户授权、项目、设备绑定、推送记录必须按 `tenant_id` 隔离。 -- DCP 一测点一设备是核心建模约束,不能把测点设计成产品级或平台级共享资源。 -- 实时推送必须异步,不影响设备上报吞吐。 -- token、密码、请求头等敏感信息需要加密保存和脱敏展示。 -- 推送日志增长较快,需要后续增加归档或清理策略。 -- 字段映射表达式必须受限,不能执行任意代码。 -- 定时补推要限制窗口、批次大小和并发数,避免压垮第三方平台。 -- 雨量累计值不递增时应跳过并记录原因,不应无限重试。 - -## 14. 结论 - -平台对接应定位为可复用的 iPaaS/Connector 能力,而不是单个平台的硬编码适配。 - -推荐模型: - -```text -平台模板(共享) - -> 接口配置(共享) - -> 流程编排(共享) - -> 适配器(共享) - -> 租户平台实例(租户级) - -> 用户/项目/资源映射(租户级) - -> 设备绑定和字段映射(租户级) - -> 推送任务、推送记录、推送日志(租户级) -``` - -DCP 是第一批落地模板和适配器,必须保留“一测点一设备”的资源约束;其他租户复用 DCP 平台模板后,只需配置自己的 DCP 账号、用户、项目、设备绑定、字段映射和推送策略。 diff --git a/docs/knowledge-base/README.md b/docs/knowledge-base/README.md index 4708428a..5d145ec5 100644 --- a/docs/knowledge-base/README.md +++ b/docs/knowledge-base/README.md @@ -9,7 +9,6 @@ 3. [数据库与表关系](./03-data-model.md):表分组、核心实体、业务关联、真实外键和约定关联。 4. [数据库关系图](./04-er-diagrams.md):按业务域拆分的 Mermaid ER 图。 5. [数据桥接对接文档](./05-data-bridge-integration.md):设备/物模型查询、设备上报数据推送和第三方主动查询。 -6. [平台对接需求与技术设计](./06-platform-integration-design.md):可复用 iPaaS/数据交换能力,覆盖 OUTBOUND_PUSH、INBOUND_PULL、INBOUND_RECEIVE,支持租户级资源映射、设备绑定、字段映射、交换任务与记录,明确 DCP 一测点一设备约束。 ## 当前结论 diff --git a/docs/platform/thinks.png b/docs/platform/thinks.png new file mode 100644 index 0000000000000000000000000000000000000000..5cf788348a1866183f1c313503794e769f4fbf0a GIT binary patch literal 64407 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

ns07gG#MriGOD~qoFsisR4fft`t`ZPeKb18 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 V=zi~L15SZoFmYM2ED^02{|ndz65s#; literal 0 HcmV?d00001