简介:这是一份基于统计过程控制(SPC)的在线质量监控系统毕业设计源码,适合计算机、自动化专业学生用于课程设计、毕业课题或技术参考。系统基于Visual Studio 2013与SQL Server 2012开发,涵盖用户身份验证、人员与产品信息管理、工艺流程设定、设备资料维护、异常判定参数配置、任务处理状态管理等功能,并实现了Xbar-R、Xmedian-R、X-Rs等多种控制图、分布直方图及过程能力指数图,能够完成实时监控与异常状态标识。压缩包共199个文件,包含36个C#源码文件、79个界面预览截图、12个资源文件、33个数据库备份(zbak),以及配置文件、可执行程序等,整体约3.08MB,结构完整。已有71人学习浏览,源码附带登录信息(admin/admin),便于直接运行与二次开发,可作为学习SPC质量控制与WinForm开发的完整参考。
1. SPC在线质量监控系统:为什么说它是批量生产企业的“后悔药”
我在调试一条电机装配线时,遇到过最典型的场景:巡检员两小时抽一次样,等发现尺寸超差,已经连续生产了300件。返工费用不算,更麻烦的是客户那边已经装机的批次要召回。后来把SPC(统计过程控制)做成实时在线监控,控制图直接在工位屏上闪烁报警的那一刻,我知道这个方向值得做。
基于SPC技术的在线质量监控系统,本质是把休哈特控制图从纸面搬到程序里,用C#写上位机,实时采集测量值、计算控制限、按判异规则报警,把事后检验变成过程预防。适合三类人:要解决批量不良的工艺工程师、做上位机开发的程序员、以及想用低成本方案替代昂贵SPC软件的工厂管理者。
2. SPC控制图与C#落地:先搞懂三个公式,再写第一行代码
2.1 控制图为什么能“看到”过程漂移:均值极差图的最小概念集
SPC不是一套算命工具,它建立在“过程正常时输出服从正态分布”这个假设上。只要过程稳定,任何单个测量值落在±3σ之外的概率只有0.27%。所以你不需要等不良品出现,只要发现有数据点越过控制限,就能推断过程中出现了特殊原因——比如刀具磨损、材料批次变了、操作员换人。这个推断逻辑是控制图的全部意义。在线监控系统的第一优先级,不是把图画得多漂亮,而是把控制限算准、把判异规则跑对。
对于计量型数据,比如轴径、扭矩、重量,常用的是均值-极差图(Xbar-R图)。它由两张图组成:上面一张画每个子组的均值,下面一张画每个子组的极差(最大值减最小值)。为什么要用子组而不是单个数据点?因为单点波动可能只是随机噪声,而子组均值能过滤掉一部分随机噪声,更容易暴露出过程位置的漂移。子组怎么取?常见做法是按时间顺序连续取3到5件为一个子组,然后每隔20到30分钟再取下一组。这个参数要按你的节拍来定,取件太密会让控制限计算失真,太疏又会漏掉短期变化。
记住三个核心公式。设第i个子组的均值为x̄_i,极差为R_i,共有k个子组,则总均值x̄̄ = Σx̄_i / k,平均极差R̄ = ΣR_i / k。均值图控制限为:UCL = x̄̄ + A2 × R̄,LCL = x̄̄ - A2 × R̄;极差图控制限为:UCL = D4 × R̄,LCL = D3 × R̄。A2、D3、D4是根据子组大小n查表得到的常数。比如n=5时,A2=0.577,D3=0,D4=2.114;n=3时,A2=1.023,D3=0,D4=2.574。这些常数不是经验值,是统计学上按“无偏估计”推导出来的。如果你的代码里写死了一个常数,一旦换子组大小就会算错,所以一定要做成查表函数。
控制限算出来之后,还要配判异规则。最基础的三条:点子超出控制限;连续7个点子出现在中心线同一侧;连续7个点子递增或递减。碰到任何一种,都算过程异常,系统要报警并锁存当前数据。你可以在代码里把规则做成可配置的开关,因为不同客户对SPC的审核要求不一样,有些汽车行业客户还要求执行GB/T 4091或类似标准的八条判异准则。但不要一上来就全开,先用三条规则跑一个月,看虚警率能不能接受。虚警率太高,现场会时不常“狼来了”,最后对报警麻木。
2.2 为什么选C#做在线监控:上位机生态、Access存储与状态机思路
在这个领域,常见的技术选型是C# + WinForms/WPF + Access/SQLite。原因很直接:大多数工厂的测量设备自带串口或Modbus TCP通信,C#的SerialPort和Modbus库成熟,而且做上位机的老朋友多,遇到问题能问到人。相比用Python写后台服务再配前端,C#桌面方案部署简单,一台工控机双击就能跑,不需要装数据库服务。如果你只需要单机或几台工位机上传到一台服务器,用Access就能扛住;数据量大了再迁MySQL也容易,因为C#的ADO.NET接口是统一的。
在软件结构上,我一般会引入一个简单的状态机来管理监控流程。别把“采集、计算、报警、确认”这四个状态散落在多个按钮的事件里,那样改一个逻辑要动三处。C#里用枚举定义状态,用字典或switch做状态转移,代码会清晰很多。这也是所谓“C#状态机”的常见用法。举例说,系统启动后进入“待采集”状态;收到一组数据后,如果子组未满,就继续累积;子组满了就进入“计算”状态;计算完看是否触发判异,触发则进入“报警”,人工确认后回到“待采集”。状态机里每个转移都只做一件事,出了问题优先看状态变量。
Access在这里的作用不只是存原始数据,还要存控制限历史、报警记录和参数配置。我见过很多工程师把SPC参数写在代码里,改一次要重新编译。更好的做法是建一张Param表,把子组大小、采样间隔、判异规则开关、报警上下限都放在里面。程序启动时读一次,在界面上留一个“参数维护”入口。这样现场调参就不需要你出差。
数据库的表结构至少要三张:MeasureHistory存每个子组的原始测量值,字段有ID、DeviceID、SubgroupNo、Value、Time;SubgroupSummary存每个子组的均值、极差、控制限和时间;AlarmLog存报警类型、触发值、确认人和确认时间。后面分析OEE或改善过程能力时,都从这三张表取数。
另外,很多工厂还在用Excel做SPC报表,但Excel没法实时报警,也容易有人为改动数据。C#上位机的好处是数据一进来就直接入库,人工改不了历史数据,审核时更有说服力。而Access的选择,是因为它在局域网共享模式下够用,并且与C#的OleDb连接池配合稳定。需要注意Access的数据库版本要选2007及以上格式,老版本的jet驱动在新系统上容易出现“未找到提供程序”的报错,这是后面避坑章节会提到的一个细节。
3. 在线监控系统的三层架构:从设备读数到数据库落地的完整代码
3.1 采集层:用SerialPort和Modbus协议把测量值读进C#上位机
先明确一点:SPC在线监控系统的输入是数据,不是“事件”。所以采集层的首要任务,是把测量设备吐出来的每一帧数据解析成可用的数值,并且打上正确的时间戳。针对不同设备,我见过三种协议:串口ASCII帧、Modbus RTU、Modbus TCP。C#里串口通信用System.IO.Ports.SerialPort类,Modbus则可以直接引用第三方库,也可以自己拼报文。这里给出一个最常用的串口读取示例,数据帧格式假设为“起始符STX + 4字节测量值 + 结束符ETX”:
// SerialDataReceiver.cs using System.IO.Ports; public class SerialDataReceiver { private SerialPort _port; private StringBuilder _frameBuffer = new StringBuilder(); public SerialDataReceiver(string portName, int baudRate) { _port = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One); _port.DataReceived += DataReceivedHandler; } private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e) { string chunk = _port.ReadExisting(); foreach (char c in chunk) { if (c == (char)0x02) // STX 起始符 { _frameBuffer.Clear(); } else if (c == (char)0x03) // ETX 结束符 { string frame = _frameBuffer.ToString(); ProcessFrame(frame); // 解析出测量值并触发后续SPC逻辑 } else { _frameBuffer.Append(c); } } } private void ProcessFrame(string frame) { // 示例:帧内容为 "MEAS:001234" 表示测量值 12.34 if (frame.StartsWith("MEAS:")) { double value = double.Parse(frame.Substring(5)) / 100.0; OnMeasurementReceived?.Invoke(value); } } public event Action<double> OnMeasurementReceived; public void Start() => _port.Open(); public void Stop() => _port.Close(); }逻辑说明:这个接收器把串口数据当作无边界字符流,用STX和ETX做帧边界识别。为什么不用按字节长度固定读取?因为你没法保证一次ReadExisting正好读到一帧完整数据,可能读到一半,也可能一次读了两帧。用状态缓冲拆帧是串口编程的基本功。注意ProcessFrame里把字符串转double时要处理InvariantCulture,因为不同工位机系统区域设置不同,小数点可能是逗号,导致解析翻车。
参数说明:串口参数必须与设备匹配。大多数测量设备出厂默认9600或115200,数据位8,无校验,1停止位。如果你发现读到的数据全是乱码,先检查波特率,然后检查帧格式。我一般会在界面上留一个“发送测试命令”的按钮,用来向设备请求一次数据,验证通信链路。对于Modbus RTU设备,你还需要知道设备地址和寄存器地址,读取的数据是两个字节的高低位组合,C#里用BitConverter.ToInt16或ToUInt16转换时要区分大小端。这属于协议细节,每家设备可能不同,建议先用手持调试助手确认寄存器地址和值范围,再写代码。
若设备支持Modbus TCP,则用TcpClient连接,报文格式是MBAP头加PDU,网上有很多现成类,但要注意事务ID和协议ID的赋值,不同批次请求之间事务ID要递增,否则设备侧可能丢弃响应。在线监控系统里,采集层往往不只接一台设备,我会做一个IDevice接口,让串口设备和TCP设备都实现Start/Stop/ReadValue,这样上层只依赖接口,不会因为换设备重写业务逻辑。
3.2 数据层:C#与Access数据库的写入、查询与按时间分表
采集层拿到测量值后,下一步不是直接计算,而是先把原始值落库。原因很简单:如果控制逻辑算错了,至少还能从原始数据重算;如果没有原始数据,一切复盘都是空谈。使用C#与Access配合时,我习惯用OleDbConnection连接,连接串里写Provider为Microsoft.ACE.OLEDB.12.0。写库要注意三点:连接是否及时关闭、参数化查询防止SQL注入、批量写入提升性能。下面这个函数是最小可用的写库代码:
// MeasurementStore.cs using System.Data.OleDb; public class MeasurementStore { private readonly string _connString = @"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=D:\spc_data.accdb;Persist Security Info=False;"; public bool AppendMeasurement(string deviceId, int subgroupNo, double value) { string sql = "INSERT INTO MeasureHistory (DeviceID, SubgroupNo, Value, Time) VALUES (?, ?, ?, ?)"; using (var conn = new OleDbConnection(_connString)) using (var cmd = new OleDbCommand(sql, conn)) { cmd.Parameters.AddWithValue("DeviceID", deviceId); cmd.Parameters.AddWithValue("SubgroupNo", subgroupNo); cmd.Parameters.AddWithValue("Value", value); cmd.Parameters.AddWithValue("Time", DateTime.Now); conn.Open(); cmd.ExecuteNonQuery(); } return true; } }逻辑说明:这里用了四个问号作为参数占位符,而不是拼接字符串。OleDb参数不支持@变量名,只支持问号,顺序必须与SQL中的位置一一对应。AddWithValue虽然方便,但要注意它不会校验长度,如果字段是数字类型,传string进去会触发类型转换异常,所以我明确传double和int。
参数说明:连接串里Data Source要写绝对路径。如果MS Office没装Access驱动,会报“未在本地计算机上注册Microsoft.ACE.OLEDB.12.0”的错。解决方式有两种:装Access Database Engine 2016 Redistributable(x86/x64对应你的程序编译目标),或者改连接串用旧版Jet。我强烈建议用ACE,并确认你的C#项目平台目标设为x86或x64,不能设AnyCPU,否则在64位系统上会找不到32位驱动。
当数据量上来以后,单表写入会慢。常见做法是按天或按月分表,比如表名带上日期后缀MeasureHistory_20250614。程序里可以用一个表映射函数,根据DateTime.Now生成表名,然后动态拼接SQL。但注意动态表名不能用参数绑定,只能用白名单方式拼接,避免SQL注入。另一个实际问题是Access数据库文件大小上限为2GB(新版是更大的),但一般到1GB左右性能就会急剧下降。如果你预计每天产生数以万计的记录,建议预留迁移SQL Server的接口,在写库层做抽象,不要直接到处new OleDbConnection。
数据层还有一个容易被忽略的点:控制限历史的存储。SPC有个特点是控制限会随数据积累不断重算,但一旦过程稳定,控制限就固定下来,进入监控阶段。我会把每次重算后的UCL、LCL、中心线值存到SubgroupSummary表,同时在AlarmLog里记录哪一次计算触发了报警。这样审计时能知道当时用的控制限是多少,而不是用今天的控制限去评判昨天的点。
到这里,三层架构中的采集层和数据层就落地了。下一章进入核心算法:把Xbar-R图的计算和判异规则写成真正能跑的C#代码。
4. 核心算法实现:控制限计算、判异规则与Cpk的C#代码
4.1 写一个XbarRChart类:均值、极差、控制限一次算完
控制图计算的正确性决定整个系统的可信度。我见过不少项目把控制限公式写错,最常见的问题是没有用平均极差R̄去估计σ,而是直接用样本标准差s,导致控制限过窄,现场天天误报警。在Xbar-R图里,我们用的是极差法估计σ:σ̂ = R̄ / d2,其中d2也是查表常数。A2 = 3 / (d2 × sqrt(n)),所以均值图控制限的完整形式是x̄̄ ± 3σ̂/sqrt(n),简化后就是x̄̄ ± A2 × R̄。代码里我建议直接实现原始公式而不是查A2,因为这样逻辑更清晰,也方便以后扩展其他图。
以下是一个完整的XbarRChart类,包含分组、计算、控制限生成、判异和Cpk计算:
// XbarRChart.cs using System.Collections.Generic; using System.Linq; public class XbarRChart { public int SubgroupSize { get; private set; } // 每个子组样本数 n public List<double> OverallMean { get; private set; } // 每个子组的均值 public List<double> OverallRange { get; private set; }// 每个子组的极差 public double CenterLineX { get; private set; } // x̄̄ public double AverageRange { get; private set; } // R̄ public double UclX { get; private set; } public double LclX { get; private set; } public double UclR { get; private set; } public double LclR { get; private set; } public void Compute(IEnumerable<double> rawValues, int subgroupSize) { SubgroupSize = subgroupSize; var values = rawValues.ToList(); int k = values.Count / subgroupSize; // 完整子组个数 OverallMean = new List<double>(); OverallRange = new List<double>(); for (int i = 0; i < k; i++) { var group = values.Skip(i * subgroupSize).Take(subgroupSize).ToList(); double mean = group.Average(); double range = group.Max() - group.Min(); OverallMean.Add(mean); OverallRange.Add(range); } CenterLineX = OverallMean.Average(); AverageRange = OverallRange.Average(); double A2 = Constants.A2For(subgroupSize); double D3 = Constants.D3For(subgroupSize); double D4 = Constants.D4For(subgroupSize); UclX = CenterLineX + A2 * AverageRange; LclX = CenterLineX - A2 * AverageRange; UclR = D4 * AverageRange; LclR = D3 * AverageRange; } public bool IsOutOfControl(out string reason) { // 规则1:均值图有点超出控制限 for (int i = 0; i < OverallMean.Count; i++) { if (OverallMean[i] > UclX || OverallMean[i] < LclX) { reason = $"均值图第 {i + 1} 点超出控制限"; return true; } } // 规则2:连续7点在中心线同一侧 int sideCount = 0; for (int i = 0; i < OverallMean.Count; i++) { bool above = OverallMean[i] > CenterLineX; if (i > 0 && above == (OverallMean[i - 1] > CenterLineX)) { sideCount++; } else { sideCount = 1; } if (sideCount >= 7) { reason = $"连续 {sideCount} 点在中心线同一侧"; return true; } } reason = null; return false; } public double CalculateCpk(double upperSpec, double lowerSpec) { // 用极差估计标准差:sigma = R̄ / d2 double d2 = Constants.D2For(SubgroupSize); double sigma = AverageRange / d2; double cpu = (upperSpec - CenterLineX) / (3 * sigma); double cpl = (CenterLineX - lowerSpec) / (3 * sigma); return System.Math.Min(cpu, cpl); } }逻辑说明:Compute方法先根据子组大小把原始数据切分成k个完整子组,注意末尾不足一组的样本直接丢弃,如果保留一部分会导致该组样本量偏小,控制限计算失真。接着算总均值和平均极差,再查常数表得到控制限。缓存每个子组的均值和极差是为了画图和后续判异。
参数说明:SubgroupSize通常取3到5,具体取决于生产节拍。n越小,控制限越宽,虚警率越低但检出偏移的能力也弱;n越大则相反。我建议先从n=5开始,因为多数SPC参考书的常数表都是以n=5作为示例,好对照验证。Constants这个类要自己实现,用switch或数组存储A2、D3、D4、d2值。注意n=2时D3=0,极差图只有上控制限,这是数学上的结果,代码里不必特判。
4.2 判异规则与过程能力:当心Cpk的隐藏前提
上面的类里只实现了两条判异规则:超出控制限和连续7点同侧。真实生产现场还可以加入“连续7点递增或递减”“连续14点交替上下”等规则,但算法本质都是对点的位置和趋势做模式识别。我建议把每条规则封装成一个独立方法,用委托或接口让系统可灵活组合。不要写一个几百行的大if-else,那样后面增加规则会很痛苦。
Cpk计算有一个隐藏前提:过程必须处于统计受控状态。如果当前数据显示过程已经失控,那么计算出的Cpk没有意义,因为σ估计值被特殊原因污染了。所以正确的流程是先判稳,再计算能力。如果你的系统在报警状态下依然把Cpk算给业务方看,建议在界面上用灰色显示并注明“过程失控,Cpk仅供参考”。另外,规格上下限是工程要求,不是从数据算出来的。我见过有人把规格限设成数据的最大值和最小值,那样算出来Cpk永远是1.0左右,一点参考价值都没有。规格限要来自产品图纸或客户标准,建议单独存储,并在界面上只读展示。
判异规则实际运行中还有误报问题。按3σ原则,过程正常时每个点落在控制限外的概率约0.27%,但若同时启用多条规则,整体虚警率会上升。所以很多企业在导入初期只启用“超限”一条规则,运行两三个月后再逐步开放趋势规则。这个策略值得在系统配置里体现,做成规则启用开关,而不是程序员改代码。
核心算法写完后,下一步是最容易被忽略的环节——上线后的稳定性。下一章我把自己在这些项目里踩过的坑按“现象-原因-解决”的格式写出来,希望能让你少走弯路。
5. 上线后最容易翻车的5个坑:现象、原因、解决
5.1 数据采集延迟导致子组错位
现象:控制图上的点偶尔出现“跳大神”,明明设备测量值稳定,均值却突然远离中心线,随后又恢复正常。
原因:采集层使用了异步事件,但时间戳是事件到达时刻,而不是设备实际测量完成时刻。当系统繁忙或串口缓冲积压时,一帧数据可能延迟几百毫秒,这些延迟数据被打到下个子组里,子组内样本失去了时间连续性。
解决:在设备端尽量启用硬件时间戳,或至少让设备帧里包含测量时间。如果设备不支持,则保证每个子组内数据是连续采集的,不要跨过多个时钟间隔。另外在采集层加入计时器:如果两帧数据间隔超过3倍采样周期,就把当前子组强制结束,从下一帧重新开始。这个参数可以做成配置项。
5.2 控制限随样本增多反复跳动
现象:系统刚上线时控制限每天变化很大,今天算出来的UCL是10.05,明天变成10.02,再过几天又变回10.04,现场怀疑系统不稳定。
原因:控制限是随着累积数据重算的。样本量越少,估计的R̄越不稳定,所以前期控制限自然波动大。这不是bug,而是统计规律。但如果每次样本量增加就全部重算,控制限会一直变,没法让现场适应。
解决:采用“分析用控制图”和“监控用控制图”两段式管理。先在试运行阶段收集至少25个子组(约125个样本),用这阶段的数据计算控制限并固定下来,之后进入监控阶段,控制限不再随新数据变化。只有当过程发生重大变更(换材料、换设备)时,才由质量工程师手动触发重新计算。这个逻辑要在系统里做一个“冻结控制限”按钮,并记录冻结时间。
5.3 Access并发写入冲突
现象:多台工位机同时向同一个Access数据库写入,偶尔报“文件正在使用中”或“操作被锁定”。
原因:Access不是为高并发设计的,默认共享模式下,两个进程同时写一个表会触发锁冲突。尤其当某个连接没有及时释放时,锁会一直占着。
解决:从设计上减少并发写。可以让每台工位机先写本地SQLite或CSV文件,每5分钟由服务器统一导入Access;或者直接换用SQL Server Express。如果一定要用Access,把连接串里增加“Pooling=true;”并确保OleDbConnection用完即关,代码里使用using块。另外把数据库文件放在本机磁盘,不要放在被杀的U盘或慢速网络共享上。我们项目里最终是在服务器上开了个“收集器”Windows服务,工位机通过TCP把数据发给收集器,由收集器单连接写Access,彻底避开锁冲突。
5.4 日期时间格式在不同工位机上不一致
现象:一台工位机显示的报警时间是6/14/2025,另一台显示14/06/2025,导致日报表排序错乱。
原因:系统默认使用操作系统的区域设置,有的工位机是美式日期,有的是中式,解析字符串时就会错位。
解决:所有时间字段统一用DateTime类型存储,不要存字符串;显示时全局指定CultureInfo.InvariantCulture或固定格式“yyyy-MM-dd HH:mm:ss”。在连接串或代码里设置线程的CurrentCulture为一致区域。更狠的做法是把时间戳统一转成Unix毫秒数存储,需要显示时再转换。我们项目里最终用了UTC时间戳,所有报表再按北京时间加8小时,一劳永逸。
5.5 UI线程卡死与跨线程访问崩溃
现象:点击“开始采集”后,窗口时不时卡死几秒,甚至直接报“跨线程访问控件无效”。
原因:串口DataReceived事件在工作线程触发,而直接在事件里刷新界面控件会违反WinForms线程模型。卡顿是因为读取设备或写库等耗时才在主线程执行。
解决:在事件处理器里只做数据累积和入队,使用线程安全队列(ConcurrentQueue),然后由UI定时器(如500ms一次)从队列取数据并刷新图表和数值。写库操作放到后台线程或直接用Task.Run。务必不要让SerialPort的DataReceived事件里做任何重计算和数据库插入,否则串口缓冲区会积压,造成延迟。
这些坑没有一件需要高深理论,但每一个都足以让项目在验收现场翻车。如果你正在开发或部署SPC系统,建议把这章做成一份内部检查表,逐条核对。
6. 用历史数据回放验证你的SPC参数:再小的系统也要能自检
新系统上线前,最有价值的验证不是看界面动画,而是用真实历史数据回放。做法很简单:把过去一个月测量值按时间顺序导入系统,在“回放模式”下以10倍速度重跑一遍,对比系统报警时间和人工记录的异常时间点是否吻合。如果系统早于人工很久报警,说明你设定的控制限可能偏严;如果漏掉了人工都看出的异常,则判异规则或子组划分可能有问题。这个回放功能不复杂,就是用代码控制数据源从“串口”切换成“文件”,业务层完全不用动。
我一般还会做一次“故意破坏”测试:挑选一段稳定数据,把中间20个点加上一个固定偏移(比如全部加上0.5mm),再回放。系统应该在偏移发生后第二到第三个子组的均值图上出现超限。如果到第五个子组都没报警,优先检查控制限是否被冻结、子组大小是否太大、判异规则是否只开了“连续7点同侧”而没开“超出控制限”。这些验证手段比对着公式排查快得多。
关于报警记录,这里分享一个进阶用法:在报警发生时自动调用C#的Word COM组件,把当前控制图截图、控制限值、超限数据点和最近20个子组的历史数据生成一份Word报告,文件名带时间戳。这样质量会议上有凭有据,也方便发给客户。代码核心就两行:用Word.Application打开文档,插入图表位图和表格,保存后关闭。但要注意COM对象务必释放,否则WinWord进程会常驻后台占内存。我们曾经因为漏掉ReleaseComObject,导致工控机运行一周后内存爆满,后来改成每次生成报告后重启用程序来解决,治标不治本;正确做法是young GC之后调用Marshal.FinalReleaseComObject。
最后提醒一句:SPC系统不是装上就能产出效益的,关键在你是否真的每日查看报警记录并驱动现场改进。我最早做的版本跑了一个月,报警500多次,但没有任何人行动,后来改成“报警后2小时内必须确认处置措施”的强制流程,才真正录得不良率下降。希望帮到你。
本文还有配套的精品资源,点击获取