C#上位机串口Modbus温湿度采集与MySQL存储实战

📅 发布时间:2026/9/13 15:00:42
C#上位机串口Modbus温湿度采集与MySQL存储实战
简介一套基于Modbus协议的上位机串口通信与MySQL存储的完整工程方案面向工业监控、环境数据采集及QT上位机开发者解决温湿度数据实时采集、动态曲线展示与历史存储查询等问题。压缩包共15个文件包含5个C源文件负责业务逻辑与通信、4个头文件声明接口与数据结构、4个QT界面文件可调整显示布局以及工程配置与用户设置文件整体仅24KB结构紧凑便于阅读。工程覆盖串口参数配置、Modbus读写命令封装、MySQL建表与插入查询、实时曲线绘制等关键模块并设计了按时间段检索历史数据的功能代码量精炼且逻辑完整可直接编译运行或在此基础上二次扩展。目前已有197人学习下载适合具备一定C/QT基础、需要快速搭建同类监控系统的开发者参考。1. 上位机、串口、Modbus 与 MySQL 组成的温湿度采集链路很多做上位机开发的人第一次接触 Modbus 就是在温湿度采集场景里。下位机是带 RS-485 或 RS-232 的温湿度变送器上位机用 C# 写窗体程序通过串口每隔几秒读一次寄存器再把数据写进 MySQL。整条链路拆开其实是三段串口要保证帧不丢不乱Modbus RTU 要处理 CRC 与寄存器地址映射MySQL 要设计成能存一年以上的时序数据还不卡。动态曲线只是最上面一层表现。下面按这三段展开从协议解析、表结构设计、曲线联动一路到历史回放与排错适合正在做设备监控、产线数据采集上位机或者被客户要求留三年历史数据的工程师。2. 串口上跑 Modbus RTU从报文结构到温湿度寄存器的解析实现2.1 Modbus RTU 帧结构与时序为什么 CRC 必须自己算Modbus RTU 是主从问答式协议上位机永远作为主机发起请求温湿度变送器作为从机被动应答。一条读请求固定 8 字节从站地址 1 字节、功能码 1 字节、起始寄存器地址 2 字节、寄存器数量 2 字节、CRC16 校验 2 字节。从站地址决定总线上哪个设备回答功能码区分读什么类型的寄存器CRC 则是整个报文里最容易抄错的部分。字节位置内容长度说明调试示例0从站地址1拨码开关或模块配置0x011功能码10x03 读保持寄存器0x04 读输入寄存器0x042-3起始地址2寄存器地址高字节在前0x01014-5寄存器数量2本次一次读几个寄存器0x00026-7CRC162低字节在前计算得到温湿度变送器的寄存器地址随厂家而异常见做法是把温度放在 0x0101、湿度放在 0x0102用 0x04 功能码读输入寄存器。也有不少模组把数据放到保持寄存器里这时要把功能码从 0x04 换成 0x03地址不变。很多人在这一步栽跟头用 Modbus 调试工具能读到数据换成上位机就超时往往就是功能码和寄存器类型没对上。RTU 还有个容易被忽略的时序要求报文之间至少要空闲 3.5 个字符时间9600 波特率下大约是 4 毫秒这个间隔串口驱动会负责但连续高频查询时如果间隔太短从站会把两个请求粘成一帧。2.2 用 C# 构建读请求从站地址、功能码与寄存器地址对齐2.2.1 读请求代码与 CRC 校验实现CRC16 在 .NET 的串口 API 里不会自动生成必须自己算。标准 CRC-16/MODBUS 的算法是初始值 0xFFFF把帧里每个字节异或进去再对每个 bit 按多项式 0xA001 移位处理。static ushort ComputeCrc16(byte[] data, int length) { ushort crc 0xFFFF; // CRC-16/MODBUS 初始值 for (int i 0; i length; i) { crc ^ data[i]; for (int bit 0; bit 8; bit) { crc (crc 0x0001) ! 0 ? (ushort)((crc 1) ^ 0xA001) // 多项式 0xA001 : (ushort)(crc 1); } } return crc; }这段函数从字节 0 到 length-1 逐步计算返回的是 16 位无符号整数。注意返回值不能直接强转成 byte发送时要拆成两个字节并且低字节在前。static byte[] BuildReadRequest(byte slaveId, ushort startRegister, ushort registerCount) { byte[] frame new byte[8]; frame[0] slaveId; frame[1] 0x04; // 读输入寄存器对保持寄存器改 0x03 frame[2] (byte)(startRegister 8); // 地址高字节在前 frame[3] (byte)(startRegister 0xFF); frame[4] (byte)(registerCount 8); frame[5] (byte)(registerCount 0xFF); ushort crc ComputeCrc16(frame, 6); frame[6] (byte)(crc 0xFF); // CRC 低字节在前 frame[7] (byte)(crc 8); return frame; }startRegister 就是 0x0101 这种具体寄存器地址registerCount 是要读的寄存器个数。如果温度和湿度各占一个寄存器count 传 2一次请求拿回 4 字节数据比发两次请求省一半交互时间。请求帧通过 SerialPort.Write 发出去前提是初始化参数已经配对数据位 8、停止位 1、无校验即 8N1是绝大多数温湿度变送器的默认组合波特率出厂多为 9600与下位机不一致的后果是收不到任何响应。2.2.2 响应帧解析与小数位还原从站的正常响应是 9 字节地址 1、功能码 1、字节数 1、数据 4、CRC 2。温度和湿度在小端机器上的拼装方式和手册里的“高字节在前”相反需要手动把两个字节拼成 16 位整数。static bool TryParseResponse(byte[] resp, byte slaveId, out double temp, out double hum) { temp 0; hum 0; if (resp.Length 9) return false; // 长度 9 为正常帧5 为异常帧 if (resp[0] ! slaveId) return false; if (resp[1] 0x84) return false; // 异常功能码最高位置 1 if (resp[1] ! 0x04) return false; ushort crcRecv (ushort)(resp[resp.Length - 2] | (resp[resp.Length - 1] 8)); if (ComputeCrc16(resp, resp.Length - 2) ! crcRecv) return false; short rawTemp (short)((resp[3] 8) | resp[4]); // 温度寄存器高字节在前 short rawHum (short)((resp[5] 8) | resp[6]); // 湿度寄存器 temp rawTemp / 10.0; hum rawHum / 10.0; return true; }响应里的数值多数按 0.1 精度放大存储2551 表示 25.5 摄氏度所以解析完要除以 10。用 short 而不是 int 是为了保留符号位冷库的负温度靠这个符号位还原。CRC 比较必须在拼值之前做否则脏帧会把温度显示成异常大数甚至把故障数据写进历史库。提示寄存器数量一变正常响应长度就变计算方式是5 2 * registerCount。代码里按读 2 个寄存器固定 9 字节处理改成读 4 个寄存器时记得同步调整长度判断。2.3 用 Modbus Slave 模拟温湿度从站不接硬件也能联调手头没有传感器时用 Modbus Slave 这类从站模拟器加上一对虚拟串口就能把整条通信链路跑起来。虚拟串口软件会把 COM3 和 COM4 组成一对上位机打开 COM3Modbus Slave 监听 COM4两边就像用一条串口线连着。在 Slave 里设置从站地址为 1在 0x0101 和 0x0102 两个输入寄存器里分别填入 255 和 520上位机每隔一秒读一次显示区应该能看到 25.5℃ 和 52.0%RH 跟随变化。这一步把协议解析和硬件调试隔离先证明上位机程序没问题再去查现场的接线、干扰和波特率。想反过来验证上位机的请求字节对不对可以把 Modbus Poll 这类主机工具挂在同一对虚拟串口的另一端直接对照请求帧的十六进制内容。读寄存器类型必须和功能码对应Slave 里填输入寄存器上位机就要用 0x04 读填保持寄存器上位机则必须用 0x03两边类型不一致调试界面上全是超时异常。3. MySQL 历史数据表设计与批量写入策略3.1 时序数据表结构不设外键按时间精确到毫秒温湿度数据是典型的时序数据和业务表不同它几乎只做追加写入和时间范围查询很少更新单行。表结构设计要按这个读写特点来。CREATE TABLE temp_hum ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, device_id VARCHAR(32) NOT NULL COMMENT 从站地址或设备编号, temperature DECIMAL(5,2) NOT NULL COMMENT 温度单位℃, humidity DECIMAL(5,2) NOT NULL COMMENT 湿度单位%RH, sample_time DATETIME(3) NOT NULL COMMENT 采集时间毫秒精度, raw_data VARCHAR(64) DEFAULT NULL COMMENT 原始寄存器字节排障用, PRIMARY KEY (id), KEY idx_device_time (device_id, sample_time), KEY idx_time (sample_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT温湿度历史表;字段类型上有两个容易被忽略的点。第一temperature 用 DECIMAL(5,2) 而不用 FLOAT。浮点类型在 MySQL 里存 25.5 看着没问题但做 AVG、SUM 聚合时会出现 25.499999 这类尾巴DECIMAL 是定点数聚合结果能和界面显示值对上。第二sample_time 用 DATETIME(3) 而不是 DATETIME采样周期到 1 秒以内时毫秒能区分同一秒内多条记录排序更可靠也不会因为相邻记录时间完全相同把曲线画成竖直跳变。这张表不建任何外键也不和设备档案表做 JOIN。理由很直白时序表的写入要快外键约束带来额外开销而且历史查询通常只需要 device_id 和时间范围两个条件索引已经够用。设备名、安装位置这类静态信息放到另一张表查询时在应用层合并结果比数据库里做 JOIN 压力小。3.2 采样周期、数据量与按月份分区表结构确定后要估算数据量。假设采样周期 1 秒一天产生 86400 条记录按单条约 60 字节算单日约 5MB一个月 150 万条左右一年接近 3150 万条。这个量级对 MySQL 单表不是不能跑但查询会明显变慢备份和清理也不好做。常见做法是按月做 RANGE 分区注意 InnoDB 分区表的唯一键必须包含分区键所以建表时主键要写成 (id, sample_time)。CREATE TABLE temp_hum_part ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, device_id VARCHAR(32) NOT NULL, temperature DECIMAL(5,2) NOT NULL, humidity DECIMAL(5,2) NOT NULL, sample_time DATETIME(3) NOT NULL, raw_data VARCHAR(64) DEFAULT NULL, PRIMARY KEY (id, sample_time), KEY idx_device_time (device_id, sample_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 PARTITION BY RANGE COLUMNS(sample_time) ( PARTITION p202501 VALUES LESS THAN (2025-02-01), PARTITION p202502 VALUES LESS THAN (2025-03-01), PARTITION p202503 VALUES LESS THAN (2025-04-01), PARTITION pMax VALUES LESS THAN (MAXVALUE) );分区以后删除过期数据直接ALTER TABLE temp_hum_part DROP PARTITION p202503;秒级完成不需要发出几百万行的 DELETE。查询时 MySQL 自动做分区裁剪只扫描命中时间段的分区。已经在线运行的单表想改成分区表常规做法是新建分区表、INSERT 迁移、业务切表避免直接 ALTER TABLE 触发长时间锁表和主键重建。注意最后的 pMax 分区要保留否则时间超出分区范围的数据会写入失败。3.3 批量写入与连接池配置从一条一条插到每 2 秒一批3.3.1 批量 INSERT 代码上位机采集是高频写入每条都开连接再 COMMIT 一次每秒几十条数据时 MySQL 很容易到连接上限。常见做法是采集线程只解析数据把记录放进内存队列每 2 秒取一批做批量 INSERT。async Task BatchInsertAsync(List(string dev, double t, double h, DateTime dt) rows) { if (rows.Count 0) return; var values new StringBuilder(); for (int i 0; i rows.Count; i) { if (i 0) values.Append(,); var r rows[i]; values.Append($({r.dev},{r.t:0.00},{r.h:0.00},{r.dt:yyyy-MM-dd HH:mm:ss.fff})); } using var conn new MySqlConnection(connectionString); await conn.OpenAsync(); using var tx await conn.BeginTransactionAsync(); string sql INSERT INTO temp_hum(device_id,temperature,humidity,sample_time) VALUES values.ToString(); using var cmd new MySqlCommand(sql, conn, tx) { CommandTimeout 10 }; await cmd.ExecuteNonQueryAsync(); await tx.CommitAsync(); }这是千万行级数据量下最简单的批量写法一条 SQL 插入所有行事务包住保证要么全成功要么全失败。温度、湿度用0.00格式化成两位小数再放进 SQL避免系统区域设置把小数点变成逗号导致语法错误。CommandTimeout 设 10 秒数据库短暂不可用时宁可这次失败也不能让采集队列越积越大。批量行数控制在 100 到 500 条之间比较合适太大会超过 max_allowed_packet 默认值。注意批量 SQL 的 VALUES 顺序必须和 INSERT 目标列严格一致。临时加字段后最常出现的问题不是 SQL 报错而是数值错位。3.3.2 连接串参数与常见误区connectionString 里值得逐个确认的参数有下面几个。参数建议值说明Server数据库地址上位机本机部署用 127.0.0.1Poolingtrue连接池默认开启不要手工关闭Max Pool Size202 秒一批写入20 足够ConnectionTimeout5数据库不可达时快速失败避免 UI 卡顿SslModeNone仅限内网采集场景公网部署必须按合规要求配置Charsetutf8mb4与表字符集一致避免中文设备名乱码常见误区是把连接串写在循环里每次都 new MySqlConnection。局部 using 本身没问题问题在于频繁开关连接会让连接池反复收缩表现为运行半小时后偶发超时。稳妥做法是让 BatchInsertAsync 里的连接实例在整个程序生命周期内被复用或者至少配置 Min Pool Size2 减少收缩抖动。4. 实时温湿度显示与动态变化曲线4.1 串口数据进入 UI 的线程模型生产者-消费者C# 上位机里 SerialPort.DataReceived 事件运行在串口后台线程绝对不能在这个事件里直接给 TextBox、Chart 控件赋值。WinForms 里这样做会抛跨线程异常WPF 里会随机闪烁或直接崩溃。正确处理是把事件当生产者把解析好的数据点放进线程安全队列UI 的窗体计时器作为消费者定时取出。BlockingCollection(double temp, double hum, DateTime ts) dataQueue new(4096); void OnDataReceived(object sender, SerialDataReceivedEventArgs e) { var sp (SerialPort)sender; int available sp.BytesToRead; byte[] buffer new byte[available]; sp.Read(buffer, 0, available); receiveBuffer.AddRange(buffer); // 当前按读 2 个寄存器、响应 9 字节切分 while (receiveBuffer.Count 9) { byte[] frame receiveBuffer.Take(9).ToArray(); receiveBuffer.RemoveRange(0, 9); if (TryParseResponse(frame, slaveId, out double temp, out double hum)) { if (!dataQueue.TryAdd((temp, hum, DateTime.Now))) { dataQueue.TryTake(out _); // 队列满时丢最旧 dataQueue.TryAdd((temp, hum, DateTime.Now)); } } } }TryParseResponse 直接复用第 2 章的解析逻辑但串口底层可能把一个完整帧拆成两次到达所以用累积缓冲区只有凑够 9 字节并校验 CRC 通过才认为拿到完整帧。队列容量 4096按 1 秒采样足够满时先丢最旧再入队避免上位机长时间卡顿导致内存上涨。UI 侧用 System.Windows.Forms.Timer间隔 100 到 500 毫秒从队列批量取数据。private void UiTimer_Tick(object sender, EventArgs e) { while (dataQueue.TryTake(out var point)) { AppendToChart(point.ts, point.temp, point.hum); dbBuffer.Add(point); } if (dbBuffer.Count 100 || (dbBuffer.Count 0 DateTime.UtcNow - lastDbFlushUtc TimeSpan.FromSeconds(2))) { var snapshot dbBuffer.ToList(); dbBuffer.Clear(); lastDbFlushUtc DateTime.UtcNow; _ Task.Run(() BatchInsertAsync(snapshot)); } }100 毫秒轮询在 UI 线程上消耗极小。这个模型把串口接收、界面显示、MySQL 写入三个环节完全解耦任何一环慢都不会阻塞串口读取这是上位机长时间运行不卡的基础。界面刷新率由计时器决定而不是由传感器采集速度决定调速度时只改计时器间隔即可。4.2 Chart 控件画双轴动态曲线滑动窗口与坐标自适应实时曲线推荐用 WinForms 自带的 Chart 控件。.NET Framework 工程里直接在工具箱引用 System.Windows.Forms.DataVisualization.NET 6 及以上要从 NuGet 安装对应包。图表里加两个 Series系列名ChartType对应轴单位温度LineAxisY 主 Y 轴℃湿度LineAxisY2 次 Y 轴%RH湿度范围通常在 0 到 100温度可能是 -20 到 60两个量级不同。把湿度挂到次 Y 轴两条曲线都能撑满各自高度否则湿度会被压成一条接近直线的平线。开双轴和鼠标缩放都在窗体 Load 里设置一次。this.chart1.ChartAreas[0].CursorX.IsUserEnabled true; this.chart1.ChartAreas[0].CursorX.IsUserSelectionEnabled true; this.chart1.Series[温度].YAxisType AxisType.Primary; this.chart1.Series[湿度].YAxisType AxisType.Secondary;追加数据点时动态曲线最核心的问题是老点什么时候移出视线。只 AddXY 不清理跑一天后图上有几万个点CPU 和内存双双失控。常见做法是限制最多显示最近 600 个点超出就从索引 0 开始移除。const int maxPointsOnChart 600; void AppendToChart(DateTime ts, double temp, double hum) { pointCount; chart1.Series[温度].Points.AddXY(ts, temp); chart1.Series[湿度].Points.AddXY(ts, hum); while (pointCount maxPointsOnChart) { chart1.Series[温度].Points.RemoveAt(0); chart1.Series[湿度].Points.RemoveAt(0); pointCount--; } chart1.ChartAreas[0].RecalculateAxesScale(); }RecalculateAxesScale 会在移除后重新计算坐标范围不必自己维护 X 轴最小值。600 个点对应 1 秒采样下的 10 分钟窗口是一个便于观察变化的时间窗。补充一个环境问题VS2019 里建的 C# 上位机工程目标框架如果是 net5.0 或 net6.0VS2015 打不开要跨版本共用把目标框架降到 .NET Framework 4.6.2 再重新生成解决方案。4.3 显示刷新频率、曲线采样点数与缓冲区上限的配合串口接收、界面显示、数据库写入三者频率要错开不能绑在同一个心跳上。环节频率理由串口读取与帧解析DataReceived 触发串口来数据就要收否则缓冲区溢出曲线刷新100-500 ms 一次 Timer太快浪费 UI 线程太慢曲线卡顿MySQL 批量写入每 2 秒或积满 100 条减少提交次数降低连接压力这三组参数之间没有必须严格相等的关系。界面可以只显示最近 600 个点数据库则永远全量保存。曲线上的点不代表历史数据的全部只代表屏幕目前容纳的窗口。MySQL 写入缓冲的容量要比曲线窗口大得多建议至少能容纳 2 个小时的数据这样数据库短暂不可用时恢复后还能把积压补写进去。5. 历史数据查询与回放把 MySQL 数据还原成曲线5.1 原始数据查询与分页先按时间过滤再排序历史数据查询的目的不外乎两个在表格里看明细或者把某段时间重新画成曲线。最常写的查询是按设备和时间过滤。SELECT device_id, temperature, humidity, sample_time FROM temp_hum WHERE device_id 01 AND sample_time BETWEEN 2025-06-01 00:00:00 AND 2025-06-01 23:59:59 ORDER BY sample_time LIMIT 5000;这里有两个容易踩的性能点。第一ORDER BY 配合 LIMIT 时尽量用联合索引 idx_device_time让排序直接走索引避免文件排序。第二翻页不要用LIMIT 400000, 5000这种大偏移MySQL 会先读 40 万行再丢弃前 40 万行越翻越慢。正确做法是记录上一页最后一条的 sample_time下一页用WHERE sample_time 上次值继续。对温湿度历史表一次拉回超过 5000 条也没有意义界面上 5000 个点挤在一起什么也看不出来精确时间段加限量数据才是历史表的正确打开方式。5.2 按小时/按天聚合统计避免一年数据全量拉取要回答“过去三十天每天的最高温度是多少”这类问题应该让数据库先聚合而不是把所有原始记录拉回上位机在内存里算。SELECT DATE_FORMAT(sample_time, %Y-%m-%d) AS stat_day, ROUND(AVG(temperature), 2) AS avg_temp, ROUND(MAX(temperature), 2) AS max_temp, ROUND(MIN(temperature), 2) AS min_temp, ROUND(AVG(humidity), 2) AS avg_hum FROM temp_hum WHERE sample_time DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY stat_day ORDER BY stat_day;这个查询的核心是让 MySQL 把一个月的数据压缩成 30 行传输量和显示量都大幅降低。代价是 DATE_FORMAT 包裹了 sample_time 后索引无法用于分组排序。这条 SQL 成为报表高频查询时可以加一列 date_int如 20250601并在应用层同步维护配合普通索引速度差距明显。MySQL 8.0 的函数索引也能解决但会增大写入开销先量化再决定。这类统计不需要 MySQL 存储过程一条 SELECT 就能完成存储过程只会让上位机和数据维护之间的沟通成本变高。5.3 回放实现定时器逐点推进 DataGridView 联动高亮历史回放的常见做法是把查询结果放进内存列表界面计时器每 500 毫秒往曲线里推一个点同时把表格当前行选中并滚动到可见位置。ListTempHumRecord history; int playIndex 0; void PlaybackTimer_Tick(object sender, EventArgs e) { if (playIndex history.Count) { playbackTimer.Stop(); return; } var row history[playIndex]; AppendToChart(row.SampleTime, row.Temperature, row.Humidity); grid.Rows[playIndex - 1].Selected true; if (grid.FirstDisplayedScrollingRowIndex playIndex - 1) grid.FirstDisplayedScrollingRowIndex playIndex - 1; }回放时间和真实时间的比例由计时器间隔控制500 毫秒推一个点适合观察变化想快进就每次推 5 个点进度条提供倍速选择。AppendToChart 里 600 个点数上限对回放同样生效长历史回放到后半段时曲线窗口会自动滑动这正是“最近 600 个点”窗口语义的复用。表格高亮行号要在历史查询结果里维护不能拿数据库自增 id 当表格行号用。走到这一步历史数据查询已经从“能查到”变成了“能演示”整条链路的功能闭环就完整了。6. 串口、Modbus、MySQL 联合排错与运行期验证6.1 常用故障现象与对应处理思路现象原因处理串口打开失败CH340 驱动未装好或端口被其他程序占用设备管理器确认 COM 口存在先关闭占用程序串口烧写失败上位机持续占用端口下载器无法独占烧写前退出上位机监听下位机重新上电请求总是超时波特率、从站地址、寄存器类型不匹配用串口调试助手或 Modbus 调试工具逐项核对温度显示异常大数响应帧没先做 CRC 和长度校验先校验再拼值按5 2 * count计算帧长写 MySQL 偶发超时Max Pool Size 太小或端口被系统策略拦截按第 3 章配置连接池确认采集机到数据库的 3306 连通性历史曲线有空洞采集进程重启期间采样丢失用 6.2 的 SQL 定位缺口补断点续采逻辑排错保持先后顺序先确认串口层通再确认 Modbus 能读到寄存器最后查 MySQL 写入。顺序颠倒会浪费大量时间CRC 还没算对就去排查数据库连接串是最常见的无效操作。6.2 用一条 SQL 验证写入链路是否完整判断采集系统隔夜运行是否正常不需要盯着界面看一夜。用一条聚合 SQL 就能评估昨晚的写入完整性。SELECT COUNT(*) AS actual_rows FROM temp_hum WHERE sample_time 2025-06-01 00:00:00 AND sample_time 2025-06-02 00:00:00 AND device_id 01;如果配置是 1 秒采样一次实际行数应该是 86400允许一次重启导致的少量丢失。偏差过大时再按小时分组定位缺失时间段。用 MySQL Workbench 跑这条 SQL 并看执行计划比命令行直观也能确认索引有没有被正确使用。6.3 长期运行的验收手段最后一个经验是上位机程序不要只在开发机上跑两小时就上线。至少做一次 24 小时连续运行用 6.2 的 SQL 检查写入量再观察内存占用是否随运行时间持续上涨确认队列没有在没人注意时偷偷打满。用串口调试助手挂在边上观察是否有持续的解码异常异常帧率应该低于千分之一。再把下位机断电再上电模拟现场断电验证上位机在串口重连后能继续采集不需要人工重启。把“下位机断电再上电”写进运维手册现场人员照着做一次比任何说明都管用。本文还有配套的精品资源点击获取