Files
bj_power/bj_power_mes/系统设计方案.md
T

28 KiB
Raw Blame History

工业自动化生产线管控系统 — 详细设计方案

文档信息

  • 版本V0.6(全配置化流程引擎)
  • 日期2026-08-10
  • 状态:与代码同步
  • 说明:新增全配置化流程引擎(internal/engine/),引擎作为纯执行器,所有业务逻辑通过数据库配置驱动。详细设计见 流程文档.md

目录

  1. 系统架构总览
  2. 核心组件职责
  3. 启动流程
  4. Actor 模型
  5. 工艺配方系统
  6. 信号路由系统
  7. 事件系统
  8. 调度器
  9. 数据流与写入路径
  10. 断电恢复
  11. 数据库设计
  12. PLC 信号映射
  13. 关键技术选型

1. 系统架构总览

本系统是一个单体应用,内部按职责分层模块化,打包为单个 Go 可执行文件。

┌──────────────────────────────────────────────────────────┐
│                   人工终端 (React)                        │
└──────────────────────┬───────────────────────────────────┘
                       │ SSE / HTTP
┌──────────────────────▼───────────────────────────────────┐
│               go-zero HTTP Server (API层)                 │
│  ┌──────────┐  ┌───────────┐  ┌──────────────────────┐  │
│  │ 工单管理  │  │ 看板/监控  │  │ 人工交互(暂停/恢复)│  │
│  └────┬─────┘  └─────┬─────┘  └──────────┬───────────┘  │
│       │              │                   │               │
│  ┌────▼──────────────▼───────────────────▼────────────┐  │
│  │                 业务逻辑层                           │  │
│  │  ┌────────────┐  ┌───────────┐  ┌──────────────┐  │  │
│  │  │ 工单处理器  │  │ 事件循环   │  │ 调度器       │  │  │
│  │  │ (processor) │  │(eventloop)│  │ (scheduler)  │  │  │
│  │  └────────────┘  └───────────┘  └──────────────┘  │  │
│  └────────────────────────┬──────────────────────────┘  │
│                           │                              │
│  ┌────────────────────────▼──────────────────────────┐  │
│  │                 基础设施层                           │  │
│  │  ┌──────────┐  ┌──────────┐  ┌────────────────┐  │  │
│  │  │ 事件总线  │  │ PLC 管理  │  │ 机器人管理     │  │  │
│  │  │(eventbus)│  │(plc)     │  │(robot)        │  │  │
│  │  └──────────┘  └──────────┘  └────────────────┘  │  │
│  │  ┌──────────┐  ┌──────────┐  ┌────────────────┐  │  │
│  │  │ 设备代理  │  │ 信号路由  │  │ SSE 推送       │  │  │
│  │  │ (actor)  │  │(signal)  │  │(sse)          │  │  │
│  │  └──────────┘  └──────────┘  └────────────────┘  │  │
│  └───────────────────────────────────────────────────┘  │
│                                                          │
│     PostgreSQL (持久化)                                     │
└──────────────────────────────────────────────────────────┘

分层职责

职责 关键约束
API 层 HTTP 入口,请求校验,响应组装 不写业务逻辑
业务逻辑层 工单管理、调度决策、事件处理 核心状态机,所有决策在此
基础设施层 事件总线、PLC 通信、机器人控制 可替换实现,不包含业务逻辑
设备代理层 Actor 管理槽位状态,处理设备信号 不写数据库,不调度

2. 核心组件职责

2.1 事件循环 (EventLoop) — 产线大脑

文件位置: internal/processor/eventloop/

职责:

  • 处理所有生产事件:PLC 信号、调度结果、前端指令
  • 驱动工件状态迁移(Job 状态机)
  • 协调调度器下发任务
  • 统一写入数据库(唯一写 DB 的入口)

启动方式: 独立 goroutinego eventLoop.Run(ctx)

关键行为:

  • 接收 PLC 信号 → 更新对应 Actor 槽位状态
  • 触发调度器 → 决策下一步动作
  • 下发动作指令 → PlcWorker 执行
  • 处理执行结果 → 更新 Job 状态

2.2 工单处理器 (JobProcessor) — 订单管家

文件位置: internal/processor/

职责:

  • 工单创建(校验料位、生成 Job
  • 工单暂停/恢复
  • 暂存台补料策略触发
  • 工单完成统计

2.3 调度器 (Scheduler) — 决策者

文件位置: internal/processor/scheduler/

职责:

  • 从就绪工件中选择下一个要处理的
  • 选择目标设备(约束过滤 + 策略排序)
  • 生成搬运任务(取→放)

三层架构:

  1. 任务生成层 — 根据工件当前步骤生成候选动作
  2. 约束过滤层 — 排除不可用设备、暂停工单、冲突等
  3. 策略选择层 — 按优先级排序

2.4 Actor — 设备代理

文件位置: internal/processor/actor/

职责:

  • 管理设备槽位状态(占用/空闲)
  • 处理 PLC 发来的设备信号
  • 不写数据库,不调度

2.5 信号路由 (SignalRouter) — 信号分拣

文件位置: internal/processor/actor/

职责:

  • 每秒轮询 PLC 输入寄存器
  • 检测信号变化(上升沿/下降沿)
  • 根据信号找到对应 Actor,分发处理

2.6 PLC 管理器 (PLC Manager) — 硬件通信

文件位置: internal/plc/

职责:

  • 建立与西门子 S1200 的通信连接
  • 读写 PLC 寄存器(Modbus TCP / OPC-UA
  • 发送动作指令(取料、放料、换料等)

2.7 事件总线 (EventBus) — 消息通道

文件位置: internal/eventbus/

职责:

  • 组件间解耦通信
  • 当前实现:LocalBus(内存 channel

2.8 机器人管理器 (Robot Manager) — 搬运执行

文件位置: internal/robot/

职责:

  • 控制地轨机器人执行取/放动作
  • 管理手持工具(扫码枪、激光打标机)
  • 任务执行结果回传

3. 启动流程

启动顺序分三个阶段,按依赖关系排列:

阶段一:基础设施初始化

1. PostgreSQL 连接           → ent ORM + 原生 SQL 可用
2. 预加载                    → 信号地址表 + 产品类型加载到内存
3. 事件总线创建              → 组件间通信通道(此时未启动,防止事件丢失)
4. 告警服务注册              → 订阅异常事件
6. PLC 管理器创建            → 硬件通信通道建立
7. 机器人管理器创建          → 上下料能力就绪

阶段二:业务组件组装

8. 工艺配方加载              → 各工件型号的加工步骤序列可用
9. 调度器创建                → 决策策略就绪
10. 产线 Actor 构建          → 每个设备对应一个 Actor 管理槽位
11. 工单处理器创建           → 可管理工单全生命周期
12. PLC 动作执行器创建       → 可将动作指令转为 PLC 信号
13. 事件循环创建             → 产线状态机核心(此时未运行)
14. 信号路由绑定事件循环     → PLC 信号可直接驱动状态机
15. 信号轮询启动(goroutine)→ 每秒读取 PLC 信号

阶段三:启动与恢复

16. 事件总线启动             → 之前注册的订阅开始生效
17. 事件日志写入器启动       → 所有事件异步写入数据库
18. 总线全量订阅 → 日志写入  → 审计追溯能力就绪
19. SSE 桥接器创建           → 事件实时推送到前端
20. 断电恢复                 → 从数据库读取未完成工单
    ├─ L1 自动恢复           → CREATED/ON_BUFFER 直接恢复
    ├─ L2 半自动恢复          → PROCESSING 且 PLC 确认完成
    └─ L3 人工确认            → 发布事件通知前端
21. Actor 槽位同步           → 内存状态与 DB 一致
22. 事件循环启动(goroutine)→ 产线状态机开始运行

4. Actor 模型

4.1 什么是 Actor

每个 Actor 对应一台物理设备,管理该设备的所有槽位。Actor 是统一实现MachineActor),通过 Type 字段适配不同设备类型。Actor 不调度、不写数据库,只做两件事:

  • 维护槽位状态(占用/空闲/哪个工件在上面)
  • 处理 PLC 发来的信号(加工完成 → 把槽位状态改为 DONE)

Actor 是 mutex 保护的同步状态结构体,由 EventLoop 同步调用,不启动独立 goroutine。

4.2 设备类型与槽位配置

设备类型 槽位数 支持 Exchange 说明
GRINDER 1 单槽位,支持换料(取成品+放毛坯一次完成)
TURN_TABLE 1 单槽位,支持换料,用于工件换向
CACHE 1 单槽位暂存缓冲,仅由槽位占用状态表达语义
FULL_CHECK 2 双槽位检测台,OK/NG 双结果信号
AIR_BLOW 1 单槽位,气吹清洁
DOCK 84 不建模为 MachineActor,由 PlcExecutor 直接驱动

4.3 Actor 槽位状态机

每个 Actor 的槽位经历以下状态:

EMPTY → OCCUPIED → DONE → EMPTY
  • EMPTY:空闲,可放工件
  • OCCUPIED:有工件,设备正在加工/处理中
  • DONE:处理完成,等待机器人取走

4.4 Actor 与 PlcExecutor 的关系

Actor 只管槽位状态(纯内存),PlcExecutor 管 PLC 信号握手。两者分离:

  • SignalRouter 检测到 PLC 完成信号 → 投递 SignalEvent 到 EventLoop
  • EventLoop.handleMachineSignal → Actor.MarkSlotDone → 更新槽位状态
  • 调度器决定动作 → PlcWorker 直接调用 PlcExecutor 方法执行 PLC 握手

5. 工艺配方系统

5.1 配方结构

每种工件型号(Product Type)对应一份 Recipe(工艺配方),Recipe 包含一组有序的 Step(工序步骤)。

Recipe 表:

recipe {
    id          // 配方编号,如 "R-A6VM107"
    name        // 名称
    version     // 版本号
}

RecipeStep 表:

recipe_step {
    step_id              // 步骤编号,如 "10", "20"
    recipe_id            // 所属配方
    step_name            // 步骤名称
    step_index           // 步骤序号
    step_type            // 工序类型(见下表)
    resource_type        // 设备资源类型
    next_step_default    // 默认下一步
    desc                 // 步骤描述
}

5.2 工序类型

工序类型(step_type)由 agent业务.md 定义,不同项目可不同。框架支持以下通用工序类型:

工序类型 含义 需要什么资源
LOAD 上料并扫码 搬运设备 + 接驳台
BUFFER_STAGE 暂存台等待 暂存台槽位
MACHINING 加工 加工设备
INSPECTION 检测 检测设备
PLACE 下料 搬运设备 + 接驳台

具体工序类型和步骤内容见 agent业务.mddoc/data.sql

5.3 配方加载时机

系统启动时从数据库加载所有 Recipe 到内存(RecipeLoader),运行期间不查库。新增配方需要重启或手动刷新。


6. 信号路由系统

6.1 信号流向

PLC 输入寄存器
    │
    ▼
SignalRouter.Run()  ─── 每秒轮询
    │
    ▼
检测信号变化(上升沿/下降沿)
    │
    ▼
根据信号找到对应 Actor
    │
    ▼
Actor.HandleSignal()  ─── 更新槽位状态,发布事件
    │
    ▼
EventLoop 收到事件 ─── 触发调度器

6.2 信号类型

设备完成信号(PLC → 系统): 每种设备类型可定义自己的完成信号,由 done_signal_name 字段指定。信号按设备类型分组:

设备类型 完成信号 处理方 触发效果
GRINDER GrinderMachiningDone MachineActor 槽位状态 PROCESSING → DONE
TURN_TABLE TurnTableExchangeDone MachineActor 槽位状态 PROCESSING → DONE
AIR_BLOW WashCompleteDone MachineActor 槽位状态 PROCESSING → DONE
FULL_CHECK FullCheckOK / FullCheckNG MachineActor 检测结果分支(OK→继续 / NG→报废)

请求信号(系统 → PLC): 系统通过 PlcExecutor 向 PLC 发送请求信号,信号命名规范为 {设备}{动作}Req,等待对应的 {设备}{动作}Done 完成信号。

6.3 信号 vs 状态

重要原则:PLC 信号是输入,不是状态源。

  • 系统以数据库中的状态为准
  • PLC 信号只用来触发状态迁移
  • 信号可能丢失或重复,系统通过幂等设计保证正确性

7. 事件系统

7.1 事件总线

当前使用 LocalBus(内存实现),基于 Go channel 的发布/订阅模式。

事件结构:

type Event struct {
    ID        string            // UUID
    Type      string            // 事件类型
    Source    string            // 来源
    Timestamp time.Time
    Payload   map[string]interface{}
}

7.2 事件类型

事件类型 发布者 订阅者 作用
设备信号事件 SignalRouter EventLoop 驱动状态机
工单创建事件 前端 API JobProcessor 开始生产
槽位状态变更 Actor EventLoop 触发调度
调度任务下发 EventLoop PlcWorker 执行动作
告警事件 各组件 AlarmService 通知
人工确认事件 RecoveryCoordinator 前端 断电恢复

7.3 事件订阅者

当前所有订阅者:

  • EventLoop — 订阅 PLC 信号和调度结果
  • AlarmService — 订阅告警事件
  • EventLogWriter — 订阅所有事件,写入数据库审计
  • SSEBridge — 订阅所有事件,推送到前端看板

7.4 启动顺序保证

事件总线先创建、后启动:

  1. 创建总线(NewLocalBus)— 此时可以订阅,但不分发事件
  2. 各组件注册订阅
  3. 启动总线(bus.Start)— 开始分发事件

防止"事件发了但没人处理"的竞态条件。


8. 调度器

8.1 调度触发

事件循环收到"槽位状态变化"事件后,触发调度器:

  • 工件变为 ON_BUFFER(暂存台上就绪)
  • 工件变为 WAITING_UNLOAD(设备加工完成待取走)

8.2 决策过程

输入:就绪工件列表
  │
  ▼
任务生成层 ─── 根据工件当前步骤,生成候选动作
  │
  ▼
约束过滤层 ─── 排除不可用的设备和冲突
  │
  ▼
策略选择层 ─── 按优先级排序,选择最优方案
  │
  ▼
输出:搬运任务(从哪取,放到哪)

8.3 优先级策略

  1. 紧急转移 — WAITING_UNLOAD 的工件必须尽快取走,否则堵住设备
  2. 成组装卸 — 清洗机需要凑够 2 件才关门
  3. 设备上料 — 给空闲设备上料
  4. 批量补料 — 暂存台空位 > 4 时触发
  5. 手持作业 — 扫码、打标
  6. 普通搬运 — 常规的暂存台 ↔ 设备搬运

9. 数据流与写入路径

9.1 写入路径

事件源(PLC 信号 / 前端指令 / 调度结果)
    │
    ▼
EventLoop 处理事件
    │
    ├──→ 更新 Actor 内存状态
    │
    ├──→ 触发调度器
    │
    └──→ 写入数据库(唯一写 DB 入口)

关键约束: EventLoop 是唯一写数据库的入口。Actor 不能直接操作数据库。

9.2 数据一致性

EventLoop 处理事件
    │
    ├──1. 更新 PostgreSQL(持久化)
    │
    └──2. 发布事件到总线(通知其他组件)

先写数据库,后发事件。如果写库失败,事件不发布。

9.3 事件日志

所有总线事件通过 EventLogWriter 异步写入 event_log 表:

  • 用于审计追溯
  • 用于断电恢复重放

10. 断电恢复

10.1 恢复分级

级别 条件 处理方式
L1 自动 工件在接驳台(CREATED)、暂存台(ON_BUFFER)、等待资源 直接恢复,无需人工
L2 半自动 设备加工中(PROCESSING),PLC 确认完成 转为 WAITING_UNLOAD,自动恢复
L3 人工确认 机器人操作中(IN_HANDLING)、关门中、位置不明 发布人工确认事件,前端通知操作员

10.2 恢复流程

1. 从数据库读取所有未完成的 Job 和 Task
2. 按分级分类
3. L1 → 直接恢复状态
4. L2 → 查询 PLC 确认设备状态,恢复
5. L3 → 发布 ManualActionCreatedEvent 到总线
6. 前端收到 → 弹出人工确认对话框
7. 操作员确认后 → 恢复 Job

10.3 事件重放

event_log 表按顺序重放事件,版本号和幂等性保证正确性:

  • 版本号校验:事件版本必须等于当前实体版本
  • 幂等:重复事件(相同 last_event_id)直接跳过

11. 数据库设计

11.1 主数据表

表名 用途 备注
product_type 产品类型定义 关联 Recipe
recipe 工艺配方 静态配置
recipe_step 配方步骤 步骤类型、资源类型、默认下一步
equipment 设备定义 来自 PLC 配置
equipment_slot 设备槽位(运行时) 唯一槽位真相源,包含 dock_no 区分左右托盘

11.2 业务数据表

表名 用途 备注
work_order 工单 关联产品类型、数量、状态
job 工件实例 当前步骤、状态、位置、版本
job_step_instance 工序执行记录 每次工序的执行记录
task 搬运任务 机器人执行的任务记录
event_log 事件日志 审计和恢复用

11.3 关键设计

  • equipment_slot 是槽位状态的唯一真相源,包含 statuscurrent_job_idoccupied_at 等字段
  • job 表使用 version 字段实现乐观锁,防止并发冲突
  • job 表使用 last_event_id 字段实现幂等

12. PLC 信号映射

12.1 信号模型

PLC 信号分为两类,全部定义在 doc/signals.sql 中:

状态信号(PLC → 系统): PLC 向系统上报设备状态变化,由 SignalRouter 轮询检测上升沿后投递给 EventLoop。

指令信号(系统 → PLC): 系统向 PLC 下发动作指令,由 PlcExecutor 按握手协议执行(写 Req → 等 Done → 复位 Req)。

具体信号地址和映射关系见 doc/signals.sql,不同项目替换此文件即可。

12.2 信号握手协议

所有 PLC 动作遵循统一的四步握手协议:

1. 写参数字节(如位置号、托盘号),非必须
2. 置位 Req 信号 = true,触发 PLC 执行动作
3. 轮询等待 Done 信号 = true(上升沿检测,间隔 100ms,超时 30s
4. 复位 Req 信号 = false
5. 等待 Done 信号 = falsePLC 看到 Req 为 false 后复位 Done,握手完成)

信号命名规范:{设备名}{动作}Req / {设备名}{动作}Done 成对出现。


13. 关键技术选型

组件 技术 选型理由
后端语言 Go 1.23.4 并发性能好,编译快,单二进制部署
框架 go-zero v1.3.8 轻量级,支持 HTTP/API 代码生成
ORM ent v0.14.5 类型安全,代码生成
数据库 PostgreSQL 稳定可靠,支持 JSONB
Token 存储 内存(InMemoryTokenStore 单进程部署,无需外部依赖
PLC 通信 Snap7(西门子 S7 协议) 工业标准协议
前端 React 19 + Semi Design 企业级 UI 组件库

14. 已知问题与待办

14.1 GrinderMachiningDone 信号初始化

doc/signals.sql 中已定义该信号,但需执行 go run data.go signals 导入到数据库。

14.2 RecipeLoader 调用方

RecipeLoader 提供按产品类型加载工艺路线的缓存能力,目前仅用于 cmd/mockrun 调试工具。核心业务路径(EventLoop / DBState)直接通过 entClient.RecipeStep.Query() 查库。


15. 安全防护架构

15.1 宕机风险防护

事件循环是产线的核心状态机,一旦崩溃将导致生产中断。系统采用多层次防护机制:

┌──────────────────────────────────────────────────────┐
│               应用层防护                              │
│  ├── panic recoverEventLoop.Run + goroutine       │
│  ├── workerBusy 超时保护(5分钟自动重置)              │
│  └── 通道背压保护(非阻塞发送 + 告警)                  │
├──────────────────────────────────────────────────────┤
│               业务层防护                              │
│  ├── WorkerResult 幂等性(taskID 去重)               │
│  ├── Exchange LoadJob 失败同步处理                    │
│  ├── 槽位分配 fail-fast(不 fallback 到槽位1        │
│  └── 全检台 RequestInspect 去重                      │
├──────────────────────────────────────────────────────┤
│               数据层防护                              │
│  ├── 数据库查询超时(context.WithTimeout            │
│  ├── DB 写操作 error 必须处理                        │
│  ├── Exchange 原子化操作                             │
│  └── 断电恢复数据一致性校验                          │
└──────────────────────────────────────────────────────┘

15.2 并发安全设计

场景 风险 防护措施
goroutine 访问共享 map concurrent map read/write 启动前提取局部变量传入
类型断言失败 panic 带 ok 检查的类型断言
JSON 反序列化类型不匹配 panic intFromPayload 兼容 int/int64/float64
map key 不存在 零值静默使用 v, ok := m[key] 模式

15.3 待修复的安全问题

优先级 问题 位置 修复方案
P0 EventLoop.Run() 缺少 panic recover loop.go:274-300 添加 defer recover
P0 dispatchWorker goroutine 缺少 recover worker_dispatch.go:223 添加 defer recover
P0 workerBusy 缺少超时保护 loop.go:64 添加 workerStartedAt + 超时检查
P0 snap 空指针访问 worker_dispatch.go:79 添加 nil 检查
P1 DB 查询缺少超时 scheduler_bridge.go:47 添加 context.WithTimeout
P1 handleWorkerResult 缺少幂等性 worker_dispatch.go:189-220 添加 taskID 去重
P1 Exchange LoadJob 失败未处理 worker_dispatch.go:428-462 检查 LoadJobID > 0
P1 槽位分配失败 fallback 到槽位1 worker_dispatch.go:319-321 改为 fail-fast
P2 全检台 RequestInspect 重复触发 worker_dispatch.go:471-507 添加原子标志去重
P2 全检台容量硬编码为6 worker_dispatch.go:483 从 Actor SlotCount 获取
P2 msgCh 通道满时阻塞 loop.go:199 改为 select + default

16. 代码结构优化建议

16.1 函数拆分

函数 当前行数 问题 拆分建议
dispatchWorker ~130 行 超过 80 行约束 拆分为 fillActionSlots、executeAndSyncActor、buildWorkerResultMessage
trySchedule ~200 行 承担 5 个职责 拆分为 cleanupExpiredSnapshots、refreshRuntimeSnapshots、buildJobViews、buildSystemState、dispatchCandidates
buildSystemState ~150 行 承担 6 个子逻辑 拆分为 buildMachineBusyState、buildMachineSlotState、buildCacheSlotState、buildDockSlotState、buildOrderState、buildExchangePairState
handleMachineDone ~70 行 嵌套过深 拆分为 handleInspectionResult、recordCNCTime、advanceJobStep

16.2 代码质量改进

问题 位置 改进方案
Actor 查找重复 5 次 worker_dispatch.go:102-144 分支前一次取出 Actor
handleWorkerSuccess 分支条件风格不一致 worker_dispatch.go:242-264 统一为 switch payload.Action
tryRequestFullCheckInspect 嵌套过深 worker_dispatch.go:486-497 拆为 isFullCheckFull、isLastPieceOfOrder
handleExchangeLoadJob 混合两条路径 worker_dispatch.go:389-426 拆为 handleExchangeLoadJobForOperation、handleExchangeLoadJobForAdvance
tryRequestFullCheckInspect 文件归属不当 worker_dispatch.go:471-507 移到 candidate_handlers.go 或新建 fullcheck_handler.go
success 判断逻辑分散 worker_dispatch.go:203 在 types.go 增加 func (t MessageType) IsSuccess() bool
UpdateOneID 影响行数被忽略 多处 检查 n == 0 时 slog.Warn

17. BuildProductionLine 详细流程

BuildProductionLine 是产线初始化的核心入口,位于 internal/processor/factory.go

执行步骤

1. buildSignalMaps(ctx, entClient)
   ├── 查询 equipment 表,读取 done_signal_name 字段
   ├── 调用 preload.GetMAddress(signalName) 查找 PLC 地址
   ├── 成功 → 写入 doneSignals[equipmentID] = PLC地址
   └── 失败 → slog.Warn

2. BuildActors(ctx, entClient)
   ├── 查询 equipment 表,遍历每台设备
   ├── NewMachineActor(cfg) → 创建带锁状态结构体
   └── machineActors[eq.ID] = actor

3. NewSignalRouter(plc) → 创建信号路由器
   ├── 遍历 equipment 表,对每台设备注册完成信号
   └── signalRouter.Watch(eq.ID, doneSignals[eq.ID])

4. 返回 ProductionLine{Actors, SignalRouter}

关键设计决策

决策 说明
Actor 不启动 goroutine Actor 是纯状态结构体,由 EventLoop 同步调用
设备 PLC 握手由 PlcExecutor 统一管理 不同设备类型的 PLC 操作通过 PlcExecutor 方法实现,PlcWorker 直接调用,无中间 Station 抽象
SignalRouter 在 Build 时不绑定 sink EventLoop 尚未创建,由 service_context.go 后续绑定
不在此处写 DB 只读 equipment 表,所有写入由 EventLoop 统一完成

本方案与代码保持同步,修改代码前请先更新此文档。