
简介人脸识别作为生物识别技术的重要分支在安防、金融、办公等领域广泛应用。传统机器学习算法如LBPH局部二值模式直方图凭借轻量、离线、低耗的特点成为小型场景下的高性价比选择。LBPH通过提取局部纹理特征生成直方图向量结合距离度量实现身份判别其原理简洁、训练快速非常适合嵌入式或桌面级应用。在实际工程中C结合OpenCV可高效完成人脸检测与识别配合Qt构建交互界面SQLite存储考勤数据即可搭建完整的签到系统。这类方案不仅满足企业考勤、实验室门禁等场景的实时性需求还能帮助开发者深入理解图像处理与特征工程。本文基于一个完整源码项目从环境配置到核心代码实现剖析LBPH模型参数调优、实时识别流程及常见坑点为C开发者提供一套可落地的工程实践参考。 前阵子在整理本地资料库翻出一个 C 人脸识别签到考勤系统源码.zip顺手解压跑了一遍。说实话这个项目比我想象中完整——不是那种随便贴两个 OpenCV 接口的 demo而是从摄像头采集、人脸样本训练、实时识别到考勤记录落库整条链路都串起来了。对于想入门人脸识别同时又想练一练 C 工程能力的开发者来说这是一个非常合适的实战项目。这个系统解决的是传统考勤的真实痛点早上排长队按指纹、忘带工牌找人代打、高峰期闸机口卡顿。用摄像头对准人脸系统自动识别身份、自动写入签到记录整个流程一两秒就完成不需要任何主动动作。核心链路就是 OpenCV 做人脸检测和识别Qt 做桌面界面SQLite 存考勤数据。我基于实际项目经验把整套逻辑拆一遍包括关键代码、参数调优和踩过的坑尽量给到可以直接“抄作业”的程度。1. 项目整体设计与技术方案选型1.1 为什么选 C 而不是 Python很多人接触人脸识别第一反应是用 Python 加 dlib 或 face_recognition因为 API 封装得足够友好几行代码就能出效果。但如果你真的想把一个考勤系统部署到公司前台、工厂闸机或者实验室门口Python 方案往往在几个环节上会卡住环境依赖太多numpy、scikit-learn、opencv-python、dlib、face_recognition 本身还要装 CMake 和编译器打包体积动不动几百兆识别速度虽然够用但离“毫秒级”还有距离更不用说后续要对接门禁硬件或者嵌入到现有登记系统时C/C 的接口优势会非常明显。这个项目选择 C 是合理的OpenCV 本身就是 C 写的C 调用原生接口性能损耗最小编译产物可以做成单个 exe 加几个 DLL 和模型文件拷到任何一台 Windows 电脑上就能跑对于校招、社招面试场景C 版本的人脸识别项目也能侧面证明候选人具备图像处理、数据结构、内存管理、多线程等综合能力比纯脚本语言写的版本更有说服力。当然要承认 C 生态更繁琐普通 Python demo 可能 30 分钟能跑通人脸识别换成 C 你至少得花一个晚上处理环境配置和编译问题。但这个“繁琐”本身就是学习的一部分把工程链路打通之后你再回头看 Python 版本会发现自己对底层原理的理解深了一个层次。1.2 人脸识别的两条技术路线怎么选人脸识别领域的主流方案现在基本分两大类第一类是传统机器学习算法OpenCV 内置的 LBPH局部二值模式直方图、Eigenfaces特征脸、Fisherfaces线性判别分析都属于这一类。LBPH 在小型离线场景下非常实用训练只需要几十张样本模型文件就几百 KB识别速度在普通 PC 上可以达到毫秒级不依赖 GPU也不依赖联网。缺点是对姿态、光照变化比较敏感人脸规模大了之后准确率会下降但对于一个二三十人的小型团队考勤场景它的性价比极高。第二类是深度学习方法比如 FaceNet、ArcFace、CosFace 这些通过卷积神经网络提取人脸特征向量再用余弦相似度或欧氏距离判断身份。准确率确实高戴口罩、侧脸、暗光条件下也能有不错表现但代价是模型文件动辄几十 MB 到上百 MB推理时需要较多计算资源如果集成到 C 工程里通常要用 OpenCV DNN 模块加载 ONNX 模型或者接 TensorRT、ONNX Runtime 这些推理框架工程复杂度明显上升。多数网上下载的类似源码项目核心识别算法会选 LBPH因为这个项目追求的是“源码可读、逻辑清晰、零成本复现”而不是极限精度。如果你后续想升级OpenCV DNN 的接口是兼容 C 的模型换掉、特征提取和比对逻辑改一下整体框架可以复用这也是我建议先跑通这套基础版本的原因。1.3 系统模块划分与数据流一个合格的考勤系统至少包含这几个模块人脸样本采集、模型训练、实时人脸识别、考勤记录写入、员工信息管理、考勤统计查询。实际运行时的数据流是这样的摄像头采集视频帧OpenCV 从每一帧里做人脸检测框出人脸区域如果当前处于“录入模式”就把裁剪归一化后的人脸图片保存到样本目录如果处于“识别模式”就把人脸图片送入训练好的 LBPH 模型做预测得到员工 ID 和置信度置信度在阈值内就认为识别成功然后查询员工表拿到姓名、部门等信息再写入考勤表界面端通过 Qt 的定时器不断刷新画面把实时帧和识别结果显示在窗口上。这个数据流看起来很直白但工程实现时有一些细节值得注意。比如样本采集环节如果只是简单截取视频帧很可能会存下大量模糊、闭眼、角度过偏的废图直接影响后续训练效果。所以正经一点的做法是在采集时做清晰度判断计算拉普拉斯方差过滤掉太模糊的帧同时要求用户左右转头、上下点头保证样本覆盖多个角度。这在源码里不一定实现了但你自己加也就十几行代码效果差别很大。1.4 数据库设计SQLite 还是 MySQL项目源码里我用过的版本中数据库基本是 SQLite 或者 MySQL 二选一。SQLite 的单文件、零配置特性对本地单机部署特别友好不用装服务、不用配账号密码直接把数据库文件放在程序目录下就行MySQL 的优势在于多人并发读写、跨机器共享数据适合多台考勤机把数据汇总到一台服务器上的场景。如果是学习项目或者小团队内部用建议选 SQLite省去一堆运维麻烦。表结构一般就两张核心表员工信息表和考勤记录表。员工表至少要有 id、工号、姓名、部门考勤表要有 id、员工 id、签到时间、签退时间、考勤状态。如果你想加人脸照片字段也可以直接在员工表里存图片路径或者存 base64 编码但考虑到 LBPH 模型是把所有训练数据压缩到一个 yml 文件里的照片路径只是留存备查不必强求入库。2. 环境准备与核心原理2.1 VSCode 配置 C 和 OpenCV 开发环境在 Windows 上做这个项目最省力的组合是 Visual Studio打开 CMake 项目直接编译。但很多学习者习惯用 VSCode这里也完全可以。你需要准备 OpenCV 4.x 的 Windows 预编译包解压后会有 build 和 sources 两个目录。关键在于编译器的版本要和 OpenCV 预编译库对应OpenCV 官方 Windows 包默认是用 MSVC 编译的所以用 MinGW 的话经常会遇到 ABI 不兼容的链接错误。VSCode 里需要配置三处。第一处是 c_cpp_properties.json把 OpenCV 的 include 路径加进去第二处是 tasks.json写编译命令用 g 的话大概是这样g main.cpp -o attendance.exe -I D:/opencv/build/include -L D:/opencv/build/x64/vc15/lib -lopencv_world480注意这里 -I 和 -L 路径要和你实际解压目录一致lib 文件名也可能因为 OpenCV 版本不同而有差异。第三处是配置环境变量把 D:/opencv/build/x64/vc15/bin 加到系统 PATH 里否则运行时找不到 opencv_world480.dll。我见过太多人编译通过了双击 exe 却弹窗报 DLL 缺失十有八九是 PATH 没配好。另一个坑是 OpenCV 和编译器的位数必须一致。64 位程序只能配 x64 版本的 OpenCV32 位程序要配 x86 版本混用的话链接阶段就会报一堆无法解析的外部符号。建议统一用 x64现在绝大多数电脑都支持。2.2 人脸检测Haar 级联检测器细节调优OpenCV 做人脸检测最经典的方式是 Haar 级联分类器加载 haarcascade_frontalface_default.xml 文件后调用 detectMultiScale。核心参数有四个scaleFactor、minNeighbors、minSize、maxSize。scaleFactor 控制图像金字塔的缩放比例每次缩放越大检测速度越快但容易漏检一般取 1.1 比较稳。minNeighbors 控制一个候选框周围至少要有多少个检测框才认为它是人脸值越大误检越少但也可能漏掉侧脸建议 5 左右。minSize 设为人脸最小尺寸如果摄像头离人比较远可以把 minSize 调小到 80x80否则检测不到小脸反过来如果人脸离镜头很近minSize 太大就会把脸切掉一半。实际项目里同样的灯光条件下正脸检测率能到 95% 以上但一旦有人低头看手机、侧头说话Haar 就会频繁漏检。为了缓解这个问题可以在代码里加一个状态判断如果连续 10 帧都没有检测到人脸再判断为“无人”如果只是单帧漏检不要立刻清除识别结果这样界面上的识别框不会闪来闪去。还有一个性能技巧检测时为了快可以先缩小图像再做检测检测到区域后映射回原图坐标。比如原来图像是 1280x720先缩放到 640x360检测到人脸框之后再乘回比例。这个操作能把 CPU 占用率降下来不少尤其是低配机器上否则视频流处理会非常吃力。2.3 LBPH 算法原理与 confidence 含义LBPH 的全称是 Local Binary Pattern Histogram局部二值模式直方图。它的原理分三步第一步把图像转成灰度图对每个像素取周围 3x3 邻域内的像素值如果邻域像素大于中心像素记为 1否则记为 0得到一个 8 位二进制数这就是该像素的 LBP 值。第二步把整张图片划分成若干小格子比如 8x8在每个格子里统计 LBP 值的直方图。第三步把所有格子的直方图拼接起来就是这张人脸的特征向量。训练模型的时候LBPH 会把每个员工的所有样本特征向量存储在模型文件里并记录对应的标签。识别时计算当前人脸的特征向量然后和模型里的所有特征向量比较距离距离最小的那个就是匹配结果。OpenCV 返回的 confidence 其实就是这个距离值越小代表越相似。所以判定的逻辑是 confidence 小于某个阈值才认为识别成功而不是只要 label 匹配就通过。阈值怎么定我用过两套标准如果训练样本每人大约 30 张光照条件稳定阈值设 80 比较合适如果样本量少或者光照不稳定阈值适当放宽到 100。设得太严本来认识的人会识别失败设得太松陌生人会被误识成某人考勤就会出现大问题。建议你写个小工具批量跑测试图片统计不同阈值下的通过率和误识别率再选一个平衡点。2.4 实时视频与 Qt 界面刷新界面端如果选 Qt Widgets最常规的做法是用一个 QTimer 定时器驱动摄像头帧抓取。摄像头用 OpenCV 的 VideoCapture(0) 打开每次 timer 触发时读取一帧转成 RGB 的 QImage 后 setPixmap 到 QLabel 上。一个要注意的细节是VideoCapture 读帧和界面刷新如果都在主线程遇到复杂识别逻辑容易卡界面所以最好把识别线程独立出来主线程只负责显示和交互。我在做这个项目时是开了一个专门的识别线程用 while 循环读帧、做检测和识别识别结果通过信号槽机制发到主线程。这样即使识别占用了大量 CPU界面缩放也不会卡死。线程之间如果要传递图像推荐用 QMutex 加锁保护共享 Mat避免同时读写导致的内存错误这类问题在 C 里一旦出现排查起来相当痛苦。3. 实操核心代码实现与关键步骤3.1 人脸样本采集模块采集环节的核心是把摄像头画面中的人脸区域截取下来预处理后保存成统一的尺寸。代码如下思路是每检测到一帧人脸就裁剪灰度图并 resize 到 200x200然后写入 faces 目录文件名带上员工 ID 和序号方便后面训练时读取标签。void collectFaceSamples(int employeeId) { cv::CascadeClassifier faceCascade; if (!faceCascade.load(haarcascade_frontalface_default.xml)) { std::cerr fail to load cascade file std::endl; return; } cv::VideoCapture cap(0); if (!cap.isOpened()) { std::cerr fail to open camera std::endl; return; } int count 0; while (count 50) { cv::Mat frame; cap frame; cv::Mat gray; cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY); std::vectorcv::Rect faces; faceCascade.detectMultiScale(gray, faces, 1.1, 5, 0, cv::Size(100, 100)); for (auto face : faces) { cv::Mat faceROI gray(face); cv::Mat resized; cv::resize(faceROI, resized, cv::Size(200, 200)); std::string filename faces/ std::to_string(employeeId) _ std::to_string(count) .jpg; cv::imwrite(filename, resized); cv::rectangle(frame, face, cv::Scalar(0, 255, 0), 2); count; } cv::imshow(collecting, frame); if (cv::waitKey(30) 27) break; } cv::destroyAllWindows(); }这个代码的优点是简单直接但生产环境上会有几个需要优化的点。一是要加清晰度检测计算 Laplacian 算子的方差低于某个阈值就跳过这一帧二是要避免画面里同时出现多张脸时反复把别人的脸也存进去最好在采集时提示用户保持单人出现在画面中央三是采集数量不要固定 50 张可以等画面里出现人脸并且静止超过一定帧数再采集这样样本多样性更好。这些都是可以后续迭代的增强项不是必须一开始就做。3.2 模型训练从样本到 yml 模型文件训练代码的核心是读取 faces 目录下的所有图片从文件名里解析出员工 ID 作为标签然后调用 LBPH 的 train 接口训练模型。void trainLBPHModel() { std::vectorcv::Mat images; std::vectorint labels; for (auto entry : std::filesystem::directory_iterator(faces)) { std::string filename entry.path().filename().string(); int label std::stoi(filename.substr(0, filename.find(_))); cv::Mat img cv::imread(entry.path().string(), cv::IMREAD_GRAYSCALE); if (!img.empty()) { images.push_back(img); labels.push_back(label); } } if (images.empty()) return; auto model cv::face::LBPHFaceRecognizer::create(); model-setRadius(1); model-setNeighbors(8); model-setGridX(8); model-setGridY(8); model-train(images, labels); model-save(face_model.yml); }LBPH 的几个参数值得解释一下。setRadius 是 LBP 算子比较邻域像素的半径半径越大能捕捉的纹理范围越广但也会引入更多噪声默认 1 即可。setNeighbors 是采样点数配合半径使用默认 8 没问题。setGridX 和 setGridY 是把图像划分成多少个格子格子越细直方图对局部特征越敏感但特征维度也会变高训练时间变长8x8 是兼顾精度和速度的选择。如果你发现模型对光照敏感的可以先把图像做直方图均衡化再训练效果会有明显提升。训练完成后生成的 face_model.yml 是核心资产部署的时候程序目录里除了可执行文件必须有这个 yml 模型文件和级联分类器 xml 文件缺一个都无法运行。建议在代码里对模型文件是否存在做校验如果不存在就给用户明确提示去执行训练流程否则很容易在识别阶段崩溃。3.3 识别签到核心实时循环识别模块是系统最核心的部分。读取摄像头每一帧转灰度、做直方图均衡化然后用 CascadeClassifier 检测人脸检测到的人脸区域送入 LBPH 模型预测得到 label 和 confidence。当 confidence 低于阈值时认为识别成功根据 label 到员工表里查姓名再写入考勤记录。void recognizeAndSign() { cv::CascadeClassifier faceCascade; faceCascade.load(haarcascade_frontalface_default.xml); auto model cv::face::LBPHFaceRecognizer::create(); model-read(face_model.yml); cv::VideoCapture cap(0); if (!cap.isOpened()) return; cv::Mat frame, gray; while (true) { cap frame; if (frame.empty()) continue; cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY); cv::equalizeHist(gray, gray); std::vectorcv::Rect faces; faceCascade.detectMultiScale(gray, faces, 1.1, 5, 0, cv::Size(100, 100)); for (auto face : faces) { cv::Mat roi gray(face); cv::resize(roi, roi, cv::Size(200, 200)); int label -1; double confidence 0; model-predict(roi, label, confidence); if (label 0 confidence 80.0) { signIn(label); // 写入考勤表 } cv::rectangle(frame, face, cv::Scalar(0, 255, 0), 2); } cv::imshow(attendance, frame); if (cv::waitKey(30) 27) break; } }需要注意的是每次调用 model-predict 都会做全库比对如果员工数目上百识别耗时可能会有几十毫秒再加上检测耗时整体帧率会下降。优化思路有两种一是降低检测帧率比如每两帧检测一次中间一帧直接复用上一次的检测结果二是把识别结果做滑动窗口投票连续 N 帧认为同一个 label 才算识别成功防止因为某一帧的噪声导致误签到。第二种策略在实际场景中非常有用因为摄像头画质差时偶尔一帧会突然识别成另一个员工如果一个帧就签一次到很容易把记录写错。3.4 考勤规则迟到、早退、缺勤判断识别成功之后签到记录只是时间戳还需要根据设定的上下班时间判断考勤状态。假设公司上班时间是 9:00下班时间 18:00迟到容忍时间 30 分钟那么状态判断逻辑可以封装成一个函数std::string getAttendanceStatus(const std::string checkTime) { int hour std::stoi(checkTime.substr(11, 2)); int minute std::stoi(checkTime.substr(14, 2)); int totalMinutes hour * 60 minute; if (totalMinutes 540) return 正常; // 9:00 前 if (totalMinutes 570) return 迟到; // 9:00 - 9:30 return 缺勤; // 9:30 后 }这里有个业务逻辑很容易忽略如果员工上午没刷脸下午来上班系统怎么处理比较常见的做法是签到记录写入时先检查当天是否已有记录如果没有就写为签到如果已经有签到记录就把当前时间更新为签退时间。这样就不需要做“签到/签退”双按钮操作减少人为干扰。数据库设计上可以在 attendance_record 表里建一个唯一索引约束 (employee_id, date)然后插入时用 ON CONFLICT 或者先查后插的方式判断。3.5 编译运行避坑DLL 缺失与模型路径编译期最常见的报错是链接阶段找不到 opencv_world480.lib或者大量 LNK2019 无法解析的外部符号。前者一般是 lib 路径配置不对后者基本是编译器位数或者运行时库版本不匹配。如果你用的 OpenCV 4.5.5lib 文件名可能叫 opencv_world455.lib建议打开 lib 目录看一眼实际文件名再写链接参数。运行期最常见的问题有两个。第一个是缺少 opencv_world*.dll解决方法是把 D:/opencv/build/x64/vc15/bin 加到系统 PATH或者直接把 DLL 复制到 exe 同目录。第二个问题是模型文件和级联 xml 文件路径不对程序在别的目录启动时找不到相对路径下的文件。稳妥的做法是在代码里用绝对路径或者做成配置文件程序启动时读取配置文件中的路径。我自己习惯在程序执行目录下固定放 models 文件夹把模型文件都丢进去启动时校验一遍缺失就给用户明确提示。4. 常见问题与排查技巧实录4.1 识别率低、总认错人识别率低的问题绝大多数出在样本质量和光照这两个因素上。先说样本质量如果你采集时只存正脸几十张模型没见过侧脸、低头、戴眼镜这些变化一到实际场景就容易翻车。解决方法是采集时让用户左右转头、抬头低头每个角度都存一些一张脸凑够三五十张多样化样本。光照问题是传统人脸识别的硬伤。同一个人的脸在左侧光和右侧光下LBP 直方图差异可能比不同人的差异还要大。这也是为什么我上面的代码在识别前先做了直方图均衡化它能在一定程度上拉平光线差异。如果你测试时发现同一个位置、同一个角度早中晚识别结果都不一样大概率是光照变了。你可以尝试在固定位置加补光灯降低环境光变化的影响。4.2 单人识别成多个员工如果你发现画面里只有一个人但识别结果在几个 label 之间跳来跳去多半是阈值设定太宽松或者两个员工的样本太相似。排查方法是打印出每次预测的 confidence 值观察稳定匹配时的 confidence 大概是多少误匹配时的 confidence 又大概是多少然后取两者之间的中间值作为阈值。如果两个员工的 confidence 总是非常接近那说明样本采集时没有区分好需要补录特征明显的样本或者考虑升级到深度学习特征提取方案。我在实际项目里还加了一个“最近帧投票”机制维护一个固定长度的窗口比如最近 15 帧的识别结果取出现次数最多的 label只有当它连续两轮稳居第一且置信度足够时才触发签到。这个机制看着朴素但非常有效直接消除了大部分单帧误判。4.3 摄像头打开失败或画面黑屏摄像头打不开先检查是不是被别的软件占用了比如 Zoom、钉钉、腾讯会议挂后台就会独占摄像头。OpenCV 的 VideoCapture 打开失败时不会抛异常而是 isOpened() 返回 false很多人没检查这一句就直接读帧结果程序在黑屏状态下跑了好几天。另外有些笔记本带红外摄像头和 RGB 摄像头两个设备默认 index 0 可能是红外画面出来是一团黑白的红外图。如果遇到这种情况可以尝试把 VideoCapture 的 index 改成 1 或者 2或者用 VideoCapture CAP_DSHOW 后端OpenCV 在 Windows 上默认的 MSMF 后端对某些摄像头兼容性不好切换成 DirectShow 常常能解决问题。cv::VideoCapture cap(0, cv::CAP_DSHOW); if (!cap.isOpened()) { // 尝试下一个 index cap.open(1, cv::CAP_DSHOW); }4.4 常见问题排查速查表问题现象可能原因解决办法编译报无法打开 opencv_world.liblib 路径没配或库版本不对检查实际的 lib 文件名确认和 OpenCV 版本一致运行报缺少 DLLbin 目录未加入 PATH把 OpenCV 的 bin 路径加入 PATH或复制 DLL 到 exe 目录检测不到人脸minSize 太大、光照太暗把 minSize 调到 80x80加补光必要时做直方图均衡化识别结果跳变阈值太宽松、单帧误判使用滑动窗口投票合理调整 confidence 阈值摄像头黑屏摄像头被占用或 index 错误切换 CAP_DSHOW尝试不同 index数据库写入失败表不存在或磁盘只读检查数据库文件路径首次运行建表训练时样本为空faces 目录没生成或文件名解析失败检查目录路径确认文件名包含员工 ID 前缀Qt 界面中文乱码编码不一致统一使用 UTF-8 或使用 QString::fromLocal8Bit 转换4.5 部署到其他机器时的注意点把程序拷到另一台电脑上运行最容易踩的坑是环境依赖不完整。除了 exe 本身你要确保以下文件都随程序一起拷贝OpenCV 的 DLLopencv_world480.dll 等、OpenCV 的级联分类器 xml 文件、训练好的 face_model.yml 模型文件、SQLite 数据库文件如果已经建好表。如果目标机器没有安装 Visual C Redistributable可能还会提示缺少 VCRUNTIME140.dll这种情况下需要先安装对应的 VC 运行库。如果你想让程序更像一个正经软件可以做一个启动自检逻辑程序启动时检查模型文件、数据库文件、摄像头是否可用并用日志记录每一步的状态。这样部署出去之后即使现场出了问题也能通过日志快速定位不用反复跑现场。5. 后续功能扩展与进阶方向基础版本跑通之后如果还有精力我建议按以下方向逐步升级。第一把 LBPH 换成深度学习模型用 OpenCV DNN 加载一个轻量级的人脸检测模型比如 YuNet和人脸识别模型比如 MobileFaceNet 转 ONNX识别精度会提升一个档次。第二增加考勤统计报表按天、按月汇总每个员工的出勤天数、迟到次数、缺勤时长导出到 Excel 或 PDF。第三打通企业微信或钉钉通知员工签到后自动推送消息给管理员异常考勤自动提醒。第四如果你要对接门禁硬件可以把识别结果通过串口或网络协议发送给单片机联动闸机开合。这几个方向的难度是递进的。第一个方向需要你理解 ONNX 模型加载和特征向量比对第二个方向需要熟悉 Qt 的表格控件和文档导出第三个方向需要了解 webhook 和开放平台 API第四个方向涉及硬件通信协议。每一个拎出来都能单独写一篇长文但前提是你把当前这套基础链路彻底吃透知道瓶颈在哪里、为什么需要升级后续做任何改动心里都有底。我个人的经验是做这种 C 实战项目最忌讳一上来就追求大而全的架构。先把一条最短路跑通让摄像头打开、人脸能识别、考勤能落地再考虑加缓冲、加日志、加多线程。工程能力是在不断踩坑和修 bug 的过程中练出来的不是读几篇架构文章就能会的。这套人脸识别签到考勤系统源码作为练手项目非常合适你可以一边看代码一边改把每个模块都亲手重写一遍收获会比单纯跑通一次大得多。本文还有配套的精品资源点击获取