- md-viewer.html 添加 <base href="../"> 修复子目录相对路径 - screenshot-modals.js 新增 fullPage 长图截取支持,action 改为可选 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
16 KiB
16 KiB
本地服务器独立微服务化方案
项目:智能餐养系统 - 边缘服务节点重构 版本:v1.0 日期:2026-05-26 作者:技术团队 范围:面向技术团队的设计与落地方案 周期:1 个月落地,边跑边迁
一、定位与边界
本地服务器 ≠ 业务系统。本次重构将其重新定位为:部署在每个食堂边缘的独立微服务,只做"硬件 ↔ 云端"的可靠中转桥梁,与现有"健康长庆"业务系统、小程序后端、admin 后台完全解耦。
| 做(In Scope) | 不做(Out of Scope) | 不混用(独立部署) |
|---|---|---|
| 任务化数据中转 | 业务规则、菜品算法 | 不依赖小程序后端 |
| 任务链状态管理与重试 | 用户登录鉴权 | 不调用四合一业务接口 |
| 终端设备接入与协议适配 | 报表/统计/分析 | 不耦合 admin 业务模块 |
| 本地资源缓存(视频/人脸/升级包) | 任何 UI 业务逻辑 | 独立进程、独立升级 |
| 设备远程升级 |
一句话原则:本地服务器只回答两个问题——云端要的数据收齐了吗?云端下发的指令落地了吗? 其他一概不管。
二、总体架构
2.1 系统拓扑
flowchart TB
subgraph Cloud["云端"]
CloudAPI[云端任务网关<br>Spring Boot]
CloudDB[(任务主库<br>PostgreSQL)]
OpsUI[运维后台<br>Vue3 + Ant Design Vue<br>复用 admin 技术栈]
end
subgraph Edge["食堂本地 - 每个食堂一套"]
LS[本地服务器微服务<br>Spring Boot 独立 JAR]
LSDB[(本地任务库<br>SQLite)]
Cache[本地资源缓存<br>视频/人脸库/升级包]
end
subgraph Devices["终端硬件"]
T1[点餐终端]
T2[入库秤/配比秤]
T3[智能货柜/绑盘机]
T4[人脸设备]
T5[显示大屏]
end
OpsUI --> CloudAPI
CloudAPI <-->|MQTT QoS2 持久会话| LS
CloudAPI --- CloudDB
LS --- LSDB
LS --- Cache
LS <-->|HTTP/MQTT 局域网| T1
LS <-->|HTTP/MQTT 局域网| T2
LS <-->|HTTP/MQTT 局域网| T3
LS <-->|HTTP/MQTT 局域网| T4
LS -->|本地 HTTP / RTSP| T5
2.2 通信协议选型(针对弱网)
| 链路 | 协议 | 选型理由 |
|---|---|---|
| 云端 ↔ 本地服务器 | MQTT QoS 2 + Persistent Session(EMQX) | 弱网友好、断线自动续传、消息至少一次且不重复 |
| 大文件下发(视频/升级包) | HTTPS 断点续传(Range) | 弱网重连不重头,降低带宽浪费 |
| 本地服务器 ↔ 终端 | HTTP / MQTT(局域网) | 局域网稳定,简单优先,按设备类型适配 |
| 运维后台 ↔ 云端 | HTTPS REST | 复用 admin 现有 axios + token 鉴权 |
2.3 技术栈
| 模块 | 选型 | 备注 |
|---|---|---|
| 云端任务网关 | Spring Boot 3 + Spring Integration MQTT | 与现有 Java 后端栈一致 |
| 本地服务器微服务 | Spring Boot 3,打包成单 JAR | 一键部署,内存占用控制在 512MB 以内 |
| MQTT Broker | EMQX 5(集群) | 支持 QoS 2、共享订阅、规则引擎 |
| 云端数据库 | PostgreSQL 15 ✅ 推荐 / MySQL 8 备选 | PG 的 JSONB 对 target_scope 等字段更友好,CTE/窗口函数利于运维查询 |
| 本地任务库 | SQLite + WAL 模式 | 零运维,适合边缘场景,单文件备份 |
| 运维后台 | Vue 3 + Vite + TypeScript + Ant Design Vue | 复用现有 admin 工程技术栈与登录体系 |
| 配置中心 / 注册 | 暂不引入,本地服务器走配置文件 + 远程下发 | 边缘节点数量级小,过早引入 Nacos 增加复杂度 |
| 部署 | Docker 镜像 + 一键安装脚本 | 边缘节点需支持离线部署 |
三、核心机制:全链路任务化
3.1 设计原则
每一次数据流转(无论下行还是上行)都是一个有状态、可追溯、可重试的任务。云端与本地各持一份任务记录,通过 task_id 关联,任意时刻任意任务卡在哪一跳一查就知道。
3.2 任务状态机
stateDiagram-v2
[*] --> Created: 云端创建任务
Created --> Dispatched: 推送到本地服务器
Dispatched --> LocalReceived: 本地落库 ACK
LocalReceived --> TerminalDispatched: 分发到终端
TerminalDispatched --> TerminalDone: 终端确认完成
TerminalDone --> CloudReported: 上报云端
CloudReported --> [*]: 任务完成
Dispatched --> RetryPending: 网络中断
LocalReceived --> RetryPending: 终端离线
TerminalDispatched --> RetryPending: 终端执行失败
RetryPending --> Dispatched: 自动指数退避重试
RetryPending --> Failed: 达到最大重试次数
Failed --> Dispatched: 运维后台人工触发重试
重试策略:指数退避 1s → 5s → 30s → 2min → 10min,默认最多 5 次,达到上限转 Failed 并触发告警。
3.3 数据库 Schema(核心三表)
以下 DDL 以 PostgreSQL 为例,MySQL 版本仅类型微调(JSONB → JSON、ENUM → VARCHAR + CHECK)。
-- ============================================
-- 1. 任务主表
-- ============================================
CREATE TABLE task (
task_id UUID PRIMARY KEY,
task_type VARCHAR(32) NOT NULL,
-- 'DISH_DOWN','FACE_DOWN','UPGRADE_DOWN','VIDEO_DOWN','MEAL_UP',...
direction VARCHAR(8) NOT NULL, -- 'DOWN' / 'UP'
target_scope JSONB NOT NULL,
-- {"org_id":1,"canteen_ids":[10,11],"device_ids":["T01","T02"]}
-- 支持二级单位/食堂/设备级定向下发
payload_ref VARCHAR(255), -- 大字段指针(对象存储 key 或外部表 ID)
total_count INT NOT NULL DEFAULT 0,
success_count INT NOT NULL DEFAULT 0,
failed_count INT NOT NULL DEFAULT 0,
state VARCHAR(24) NOT NULL,
-- CREATED/DISPATCHED/LOCAL_RECEIVED/TERMINAL_DISPATCHED/
-- TERMINAL_DONE/CLOUD_REPORTED/RETRY_PENDING/FAILED
retry_count INT NOT NULL DEFAULT 0,
max_retry INT NOT NULL DEFAULT 5,
next_retry_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
finished_at TIMESTAMP,
creator VARCHAR(64)
);
CREATE INDEX idx_task_state_retry ON task(state, next_retry_at);
CREATE INDEX idx_task_target ON task USING GIN (target_scope);
-- ============================================
-- 2. 任务跳转流转表(全链路审计)
-- ============================================
CREATE TABLE task_hop (
hop_id BIGSERIAL PRIMARY KEY,
task_id UUID NOT NULL REFERENCES task(task_id),
hop_name VARCHAR(24) NOT NULL,
-- CLOUD_OUT/LOCAL_IN/LOCAL_OUT/TERMINAL_IN/TERMINAL_DONE/
-- LOCAL_REPORT/CLOUD_IN
status VARCHAR(8) NOT NULL, -- 'OK','FAIL','TIMEOUT'
err_code VARCHAR(32),
err_msg TEXT,
occurred_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_task_hop_task ON task_hop(task_id, occurred_at);
-- ============================================
-- 3. 任务明细表(批量任务的逐条结果)
-- ============================================
CREATE TABLE task_item (
item_id BIGSERIAL PRIMARY KEY,
task_id UUID NOT NULL REFERENCES task(task_id),
biz_key VARCHAR(128) NOT NULL, -- 业务键,例如菜品 ID、人脸 ID
status VARCHAR(8) NOT NULL, -- 'OK','FAIL','PENDING'
err_msg TEXT,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (task_id, biz_key)
);
CREATE INDEX idx_task_item_task ON task_item(task_id, status);
这套表的关键能力:
- 任意任务卡在哪一跳,一查
task_hop就知道 - 1000 条菜品下发,失败的 3 条精准告诉你是哪 3 条(
task_item.status='FAIL') - 本地服务器同样跑这套 Schema(SQLite 简化版),云端拉取本地 hop 状态做对账
3.4 可靠性三大不变量
- 先持久化再前进:每一跳必须本地落库成功后才回 ACK;落库失败 → 让上游重发
- 幂等键 =
task_id+biz_key:重发不会产生重复数据;终端拿到同一个 task_id 直接覆盖原记录 - 任务永不丢:本地服务器即使断电重启,任务从最近持久化的状态继续往下走;运维后台可强制重放
3.5 接口契约(节选)
云端 → 本地:任务下发
Topic: tenant/{org_id}/canteen/{canteen_id}/task/down
QoS: 2
Payload:
{
"task_id": "uuid",
"task_type": "DISH_DOWN",
"target_scope": { "device_ids": ["T01","T02"] },
"payload_ref": "https://oss.../dish-snapshot-2026052601.json",
"items_count": 487,
"issued_at": "2026-05-26T10:00:00Z"
}
本地 → 云端:跳转回执
Topic: tenant/{org_id}/canteen/{canteen_id}/task/hop
Payload:
{
"task_id": "uuid",
"hop_name": "TERMINAL_DONE",
"status": "OK",
"success_count": 485,
"failed_count": 2,
"failed_items": [
{"biz_key":"DISH_1024","err_msg":"image download timeout"}
]
}
本地 → 云端:就餐数据上传
Topic: tenant/{org_id}/canteen/{canteen_id}/task/up
Payload: 同 task 主表结构 + items 数组
四、运维后台
复用现有 admin 工程的 Vue 3 + Vite + TypeScript + Ant Design Vue + Pinia + axios 技术栈,沿用 admin 登录体系与权限模型,作为 admin 工程内的一组独立路由模块(
/edge-ops/*),与业务模块隔离。
4.1 功能模块
运维后台 (admin 工程内 /edge-ops 路由组)
├── 任务总览
│ ├── 实时大盘:各食堂在线状态、今日任务数、失败率、堆积量
│ └── 异常告警:失败率突增、任务堆积超阈值、设备掉线
├── 任务管理
│ ├── 任务列表(按食堂/类型/状态/时间筛选)
│ ├── 任务详情(全链路流转图 + 明细 + 错误堆栈)
│ ├── 一键重试(单任务/批量)
│ └── 手动创建任务(应急下发)
├── 设备管理
│ ├── 食堂/本地服务器健康状态
│ ├── 终端设备清单(在线、版本、最后心跳)
│ └── 远程软件升级触发
├── 资源管理
│ ├── 视频/人脸库/升级包上传与下发
│ └── 各食堂本地缓存命中情况
├── 日志与查询
│ ├── 实时日志流(SSE 推送)
│ └── 数据库可视化查询(只读 SQL + 预设查询模板)
└── 系统配置
├── 重试策略、超时阈值
└── 二级单位/食堂组织树
4.2 关键页面示意:任务详情
[任务 #abc123 · 菜品下发 · 目标:总部食堂 + 5 个分食堂]
状态: ⚠ 部分失败 进度: 487/500 耗时: 2m13s
全链路流转:
✓ 云端创建 10:23:01
✓ 推送本地 10:23:02 (6/6 食堂)
✓ 本地落库 10:23:03 (6/6 食堂)
⚠ 终端分发 10:23:05 (5 食堂成功 / 1 食堂超时,自动重试中)
└─ 失败明细: 分食堂C - 终端 T07 离线
操作: [一键重试] [跳过失败项] [取消任务] [导出日志]
4.3 路由与权限
- 在
src/router/index.ts的modules数组中新增「边缘运维」分组,meta.menuKey = 'edge-ops' - 路由命名:
/edge-ops/dashboard、/edge-ops/tasks、/edge-ops/devices、/edge-ops/resources、/edge-ops/logs、/edge-ops/config - 权限模型:复用 admin 已有
role,新增edge:read、edge:retry、edge:upgrade、edge:config四类细粒度权限 - 仅限内部运维人员访问,不对客户开放
五、1 个月落地计划(边跑边迁)
gantt
title 落地排期(2026-05-26 至 2026-06-22)
dateFormat YYYY-MM-DD
section 第1周 框架
项目骨架/任务模型/DB Schema :a1, 2026-05-26, 3d
云端↔本地通信层(MQTT QoS2) :a2, after a1, 4d
section 第2周 三大功能迁移
菜品/营养数据下发改造 :b1, after a2, 3d
就餐数据上传改造 :b2, after a2, 3d
视频资源下发改造 :b3, after b1, 4d
section 第3周 运维后台 + 联调
运维后台前后端 :c1, after b3, 5d
全链路联调 + 弱网压测 :c2, after b3, 7d
section 第4周 灰度 + 切换
单食堂灰度试运行 :d1, after c2, 3d
分批切换 + 监控 + 回滚预案 :d2, after d1, 4d
边跑边迁策略
- 新老并存:旧 MQ 链路保留,新任务系统双写一段时间(老链路真实下发,新链路影子运行)
- 每日对账:跑对账脚本比对新老链路数据一致性,差异 > 0 立即告警
- 按食堂灰度:第 4 周第 1-3 天选 1-2 个食堂切换,稳定后批量推
- 回滚开关:每个食堂有独立的
use_new_pipeline开关,出问题秒级切回老链路 - 冻结老链路功能:迁移期间老链路只修 P0 bug,不再加新功能
验收标准
| 指标 | 目标 |
|---|---|
| 弱网模拟丢包 30% 下任务最终成功率 | ≥ 99.9% |
| 任务平均端到端延迟(局域网正常) | ≤ 3s |
| 任意失败任务可定位到具体跳转 + 失败明细 | 100% |
| 运维后台一键重试可用率 | 100% |
| 灰度食堂连续 3 天无 P0 故障 | 通过 |
六、风险与对策
| 风险 | 影响 | 对策 |
|---|---|---|
| 弱网导致云端推送积压 | 数据延迟 | MQTT 持久会话 + 本地服务器主动拉取兜底 + 队列水位告警 |
| 本地服务器断电/磁盘损坏 | 任务丢失 | SQLite 启用 WAL + 每日云端快照备份 + 关键任务云端可重放 |
| 终端固件升级失败变砖 | 设备瘫痪 | 双分区 A/B 升级 + 失败自动回滚 + 升级前强制健康检查 |
| 1 个月排期紧张 | 延期 | 第 1 周架构定型不可压缩;运维后台 V1 可砍范围(先保任务列表 + 重试 + 日志,大盘和告警 V1.5 补) |
| 与四合一系统数据不一致 | 业务事故 | 双写期间每日对账脚本,差异 > 0 告警;切换前必须对账连续 3 天为 0 |
| MQTT Broker 单点 | 整体瘫痪 | EMQX 至少双节点集群 + VIP/DNS 漂移 |
| 边缘节点版本不一致 | 兼容问题 | 本地服务器协议层做版本协商,云端兼容旧版本 N-1 |
七、待办与开放问题
- EMQX 部署形态:云端机房自建 vs 公有云托管,需运维侧拉齐
- 对象存储:大文件(视频/升级包)走哪个 OSS,断点续传 SDK 选型
- 告警通道:钉钉机器人 / 企业微信 / 短信,需明确接入哪一种
- 灰度食堂选择:建议选 1 个网络条件好的 + 1 个网络条件差的,覆盖两种典型场景
- 历史任务数据迁移:老 MQ 在途消息如何处理,是否需要补录到新任务表
八、附录
8.1 与现有 admin 工程的接入点
- 新增路由模块:
src/views/edge-ops/(dashboard / tasks / devices / resources / logs / config) - 新增 API 聚合:
src/api/edge-ops.ts,baseURL/api/edge,走云端任务网关 - 菜单配置:
MainLayout.vue新增「边缘运维」一级菜单,key =edge-ops - 权限:复用
src/stores/user.ts的角色与权限校验,新增四个权限位 - 登录态:完全沿用 admin 现有 token + axios 拦截器机制,不新建鉴权体系
8.2 文件清单(新增)
zhican/admin/
├── src/views/edge-ops/
│ ├── dashboard/index.vue
│ ├── tasks/index.vue
│ ├── tasks/detail.vue
│ ├── devices/index.vue
│ ├── resources/index.vue
│ ├── logs/index.vue
│ └── config/index.vue
├── src/api/edge-ops.ts
└── docs/本地服务器独立微服务化方案.md (本文件)
服务端(独立工程):
zhican-edge/
├── edge-cloud-gateway/ 云端任务网关 Spring Boot
├── edge-local-server/ 本地服务器 Spring Boot(单 JAR)
└── edge-common/ 协议/DTO/任务模型共享包