简介:基于Python+Flask与OpenCV深度学习技术的人脸识别签到系统,适配软件工程、人工智能、通信工程等计算机相关专业的在校学生、老师或企业员工,用于毕业设计、课程设计及项目演示,针对传统人工签到效率低、易代签等问题,给出从算法到Web端的完整落地实现。压缩包共28个文件,涵盖8个Python核心源码、7个HTML页面模板,以及SQLite数据库、数据文件与说明文档,整体约101.47MB,按功能模块划分,目录清晰,便于按需检索与二次开发。目前已有341人学习下载。代码经过运行测试,功能正常,可直接部署使用;附带的详细文档覆盖环境配置、功能设计与实现思路,既能支撑毕业答辩与项目演示,也可作为深度学习、OpenCV人脸识别及Flask开发入门的参考素材,适合在此基础之上扩展考勤统计、权限管理等其他功能。
1. 人脸识别签到系统:把“谁来了”从人工核对变成一条可审计记录
教室或实验室的签到,最怕的就是代签和补签。基于 Python + Flask + OpenCV 的人脸识别签到系统,本质是把“摄像头抓脸 → 深度学习特征提取 → 比对注册库 → 写入签到表”这条链路串起来,让“谁来了、几点来的”自动沉淀成可查询的记录。毕业设计选这个题目很常见,因为一套系统里同时包含了 Web 后端、图像处理、模型调用和数据存储,工程量适中,展示效果好,答辩时有得聊。这套方案也适合想快速搭一个局域网签到原型的工程师。需要先泼一盆冷水:它不是装好就能 100% 可靠的东西,光线一变、角度一偏就可能翻车;提前知道坑在哪,比闷头多写几千行代码更重要。
2. 技术方案拆解:Flask、OpenCV、深度学习各管哪一段
先把三样技术栈在系统里的分工说清楚。Flask 负责对外提供 Web 服务,管理签到页面和接口;OpenCV 负责图像采集、人脸检测、图像预处理;深度学习部分负责把一张人脸变成可以比对的向量特征。三者通过 Python 粘在一起,是一个典型的数据处理流水线:摄像头读帧,OpenCV 找到人脸,深度学习模型提取特征,Flask 把比对结果写到数据库里。
2.1 Flask vs FastAPI vs 桌面程序:签到后端为什么优先选 Flask
写这类系统,很多人会在 Flask、FastAPI、PyQt 桌面程序之间犹豫。我的选择标准很简单:看你的运行环境和交付方式。
| 方案 | 优势 | 代价 | 适合场景 |
|---|---|---|---|
| Flask Web 服务 | 上手快、模板和路由简单、教程多 | 同步 IO,极端并发能力弱 | 毕业设计、局域网签到原型 |
| FastAPI | 异步、自动生成 API 文档、类型校验好 | 需要额外配模板/静态方案,入门曲线高一点 | 后续要接移动端、高并发 API |
| 纯桌面(PyQt + OpenCV) | 实时性最稳,不依赖网络 | 每台机器要装 Python 环境,界面开发量大 | 固定工位闸机、离线签到 |
Flask 在这个场景里的核心价值不是性能,而是“可访问性”。浏览器打开http://服务器IP:5000就能签到,答辩时一台电脑装好服务,其他同学用手机或笔记本访问同一页面,效果比每台机器都装一遍 Python 环境好得多。FastAPI 虽然异步性能和自动文档更好,但毕业设计和快速原型场景下,Flask 的资料密度和排错难度更有优势。桌面端更适合识别延迟要求极高的闸机场景,对大多数签到打卡需求反而是杀鸡用牛刀。
2.2 检测与特征提取的模型分工:OpenCV 是眼,深度学习是脑
人脸识别链路里,最容易误解的地方是把“深度学习”和“OpenCV”对立起来。实际上它们是不同层级的组件:OpenCV 负责找脸、裁剪、缩放、画框这些图像操作;深度学习负责把对齐后的人脸转换成特征向量。
识别方案我一般推荐“预训练 Embedding 模型 + 距离比对”,而不是自己训练 CNN 分类器。原因很现实:毕设场景没有几万张同人的照片,也没有多卡训练环境,自己训分类器容易过拟合到数据集上,现场换个人就废。预训练模型把一张脸映射成 128 维浮点向量,系统只需要注册时保存每个人的向量,签到时算当前人脸向量和注册库的距离,小于阈值就算匹配成功。这样数据量要求低,而且换人注册不需要重新训练模型。ArcFace 在这类任务上精度更高,但模型获取和推理依赖更重;dlib 的 128 维描述子模型在 Windows 上最容易装通,也足够应付签到场景。
2.3 从打包文件到最小可跑工程:先跑通摄像头页面再说
手头这种“源码+数据集+详细文档”的打包文件,第一件事不是急着跑完整系统,而是先做环境体检。打开压缩包先找三个东西:requirements.txt、模型权重文件(通常以.dat或.h5结尾)、数据分析或预处理脚本。常见翻车现场是:文档里写 Python 3.7,代码用 3.10 跑不通;数据集路径被写死成绝对路径;模型文件缺失导致 import 报错。所以第一步永远是建干净环境,按依赖清单装库。
python -m venv venv venv\Scripts\activate # Windows 下激活虚拟环境 pip install -r requirements.txt然后在同目录写一个最小骨架,先证明“摄像头 + Flask 能一起工作”,再往里面加识别逻辑。这样可以隔离问题:如果连视频流都出不来,后面所有识别代码都是空转。
# app.py —— 最小骨架:先证明摄像头和 Flask 能同时跑 from flask import Flask, Response import cv2 app = Flask(__name__) def gen_frames(): cap = cv2.VideoCapture(0) # 0 表示默认摄像头 while True: ok, frame = cap.read() if not ok: break ret, jpeg = cv2.imencode('.jpg', frame) if ret: yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + jpeg.tobytes() + b'\r\n') @app.route('/video') def video(): return Response(gen_frames(), mimetype='multipart/x-mixed-replace; boundary=frame') if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False)这段代码做了三件事:持续读摄像头帧、压缩成 JPEG、以 multipart 流的方式推给浏览器。host='0.0.0.0'意味着监听所有网卡,局域网内其他设备可以通过http://本机IP:5000/video看到画面,这是 Flask 部署最常见的配置点,写127.0.0.1就只有本机能访问。debug=False一定要保持关闭,Flask 调试模式会双进程运行,摄像头设备在 Windows 上常常因此打不开。
3. 识别链路核心:检测、对齐、128 维特征与比对
从这一章开始进入系统的核心。整个识别流程可以拆成四步:检测人脸位置、对齐姿态、提取特征、计算距离。每一步都有可调的参数和容易踩的坑,尤其是“一个在现场识别不了的人,特征库里到底该存什么照片”,这比调参更重要。
3.1 dlib HOG 检测 vs OpenCV 级联检测:检测器怎么选、参数怎么设
人脸检测器的选择直接决定后续识别的输入质量。常见有两套方案:dlib 的 HOG 检测器和 OpenCV 的 Haar 级联检测器。Haar 级联的优势是轻量,老机器上也能跑;dlib HOG 对侧脸和小脸的容忍度略好,但速度稍慢。签到场景里人脸一般离摄像头不远,两种都能用。
dlib 检测器用法很直接,返回的是可迭代的dlib.rectangle对象:
import dlib detector = dlib.get_frontal_face_detector() # HOG + 线性分类器 def detect_faces(gray): # 第二个参数 1 表示金字塔上采样一次,小脸更容易被召回,代价是速度变慢 faces = detector(gray, 1) return [(int(f.left()), int(f.top()), int(f.right()), int(f.bottom())) for f in faces]OpenCV 级联的写法则是detectMultiScale,它的参数对结果的影响非常明显:
import cv2 face_cascade = cv2.CascadeClassifier('haarcascade_frontalface_default.xml') def detect_faces_haar(gray): faces = face_cascade.detectMultiScale( gray, scaleFactor=1.1, # 每轮缩放比例,越小越容易检出小脸,但更慢 minNeighbors=5, # 邻域内最少命中次数,越大误检越少,但容易漏检 minSize=(64, 64) # 小于该尺寸的框直接忽略 ) return [(x, y, x + w, y + h) for x, y, w, h in faces]参数的意义一句话讲透:scaleFactor是扫描金字塔的步长,1.1比1.3召回率高但耗时翻倍;minNeighbors是误检过滤器,调到8以上会开始漏掉侧脸;minSize决定系统离多远能认出人。签到场景里人脸占画面比例不会太小,minSize=(64, 64)足够,还能显著降低 CPU 压力。采样时优先用灰度图,因为这两个检测器都不依赖颜色信息,转换一次颜色空间能省不少耗时。
3.2 68 点对齐与 128 维特征:姿态漂移如何拉回来
检测出人脸框之后不能直接做特征提取。同一个人的脸,如果歪着头拍照,特征向量会明显偏离注册时的值。所以要先做人脸对齐,让输入模型的人脸尽量是“正脸”。
最常用的对齐参考是两只眼睛的位置。通过 68 点关键点模型找到左右眼中心,然后以两眼的连线为水平参考,做旋转仿射变换。
import cv2 import numpy as np import dlib predictor = dlib.shape_predictor('shape_predictor_68_face_landmarks.dat') def align_face(gray, face_rect): shape = predictor(gray, face_rect) left_eye = np.array([shape.part(36).x, shape.part(36).y]) # 左眼外眼角 right_eye = np.array([shape.part(45).x, shape.part(45).y]) # 右眼外眼角 eye_center = ((left_eye + right_eye) / 2.0).astype(np.float32) dx = right_eye[0] - left_eye[0] dy = right_eye[1] - left_eye[1] angle = np.degrees(np.arctan2(dy, dx)) # 计算两眼中线的倾斜角 M = cv2.getRotationMatrix2D(tuple(eye_center), angle, 1.0) aligned = cv2.warpAffine(gray, M, (gray.shape[1], gray.shape[0])) # 按人脸框裁剪并缩放到模型要求的输入尺寸 x, y, w, h = face_rect.left(), face_rect.top(), face_rect.width(), face_rect.height() aligned_face = aligned[y:y + h, x:x + w] aligned_face = cv2.resize(aligned_face, (160, 160)) return aligned_face这里 160 不是固定值,取决于你用的特征提取模型。dlib 官方例子里常用 150×150,FaceNet 常用 160×160,具体以权重文件训练时的输入尺寸为准。cv2.warpAffine的第三个参数是输出图像尺寸,如果写成(0, 0)就会报 OpenCV 相关错误,这是新手高频翻车点。
对齐之后送入特征模型,得到 128 维特征向量:
face_net = dlib.face_recognition_model_v1('dlib_face_recognition_resnet_model_v1.dat') def compute_feature(aligned_face): # 输入的 aligned_face 必须是 RGB 色彩空间 rgb = cv2.cvtColor(aligned_face, cv2.COLOR_GRAY2RGB) return np.array(face_net.compute_feature(rgb))注意 dlib 的特征模型输入要求 RGB 图像,喂灰度图会导致特征数值漂移。比对时用 L2 欧氏距离即可,dlib 官方阈值经验值在0.5左右,但实际部署必须用自己的注册照片重新标定。
def match(feature, db_features, threshold=0.5): distances = [np.linalg.norm(feature - db) for db in db_features] idx = int(np.argmin(distances)) if distances[idx] < threshold: return idx, distances[idx] return -1, distances[idx]argmin找到库里距离最小的那个人,如果最小距离都超过阈值,就认为“不认识”。阈值调大,放松匹配,但误认增加;阈值调小,误认减少,但本人也容易被拒。这个值的标定方法会在最后一章细说。
3.3 特征库构建与数据集质量:一个人 20 张照片才够
特征库是整个系统的数据库前置资产。打包里的数据集一般结构是“按人建文件夹,每张照片是一个人脸”,类似:
dataset/ wang/ 1.jpg 2.jpg zhang/ 1.jpg构建特征库时,不应该只取一张照片的特征,而是把同一个人的多张特征向量做平均,得到一个更稳定的“中心向量”。
import os import pickle db = {} # 结构: { 'wang': np.ndarray(128,) } for name in os.listdir('dataset'): feats = [] face_dir = os.path.join('dataset', name) for img_name in os.listdir(face_dir): img_path = os.path.join(face_dir, img_name) gray = cv2.imread(img_path, 0) if gray is None: continue faces = detector(gray, 1) if len(faces) == 0: print(f'跳过无脸图片: {img_path}') continue aligned = align_face(gray, faces[0]) feats.append(compute_feature(aligned)) if feats: db[name] = np.array(feats).mean(axis=0) else: print(f'警告: 用户 {name} 没有任何可用照片') with open('features.pkl', 'wb') as f: pickle.dump(db, f)这里的逻辑值得说明:检测不到人脸的图片直接跳过,说明它要么模糊、要么角度太大,硬塞进均值里反而污染特征;每个成员至少保证 20 张以上照片,覆盖正脸、左右各 15 度转头、抬头低头、不同光照。注册照片里一个人只有一张自拍,现场必然识别不稳,这是绝大多数识别失败的根本原因,不是模型不够好。
很多打包数据集的真实情况是每人只有 10 张左右,而且光线单一。拿到后建议先用视频抽帧扩充:对着摄像头慢慢左右转头,按每秒 2 帧抽取,去掉模糊帧,保留 20~30 张,比单纯复制粘贴图片有用得多。
4. 把识别结果变成签到记录:Flask 路由与 SQLite
识别链路跑通之后,系统还剩最后一块拼图:把“这个人是谁”变成一条可以查询、统计、导出的签到记录。这里有两个设计重点,一个是前后端数据交互方式,一个是数据库的防重逻辑。
4.1 路由与识别请求设计:前端截帧后 POST 给后端
常见做法是浏览器端用canvas.toDataURL()截取当前摄像头画面,转成 base64 字符串,POST 给 Flask 接口。这样做的好处是识别逻辑全部集中在服务端,浏览器只负责采集画面。后端接收到图片后解码、检测、对齐、提取特征、比对,返回结果。
@app.route('/api/checkin', methods=['POST']) def checkin(): data = request.get_json() b64 = data['image'].split(',')[1] # 去掉 data:image/jpeg;base64 前缀 img_bytes = base64.b64decode(b64) np_arr = np.frombuffer(img_bytes, np.uint8) frame = cv2.imdecode(np_arr, cv2.IMREAD_COLOR) gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = detector(gray, 1) if len(faces) == 0: return jsonify(success=False, message='未检测到人脸') aligned = align_face(gray, faces[0]) feature = compute_feature(aligned) idx, dist = match(feature, list(db.values())) if idx == -1: return jsonify(success=False, message='未注册人员') person_name = list(db.keys())[idx] ok = insert_sign(person_name, person_name) if ok: return jsonify(success=True, message=f'签到成功: {person_name}') return jsonify(success=False, message='今日已签到')接口设计上建议返回结构化 JSON,而不是渲染一个 HTML 页面。前端拿到success和message后自己决定显示什么文字,这样接口可以被后续的移动端复用。data['image']是完整 base64 字符串,形如data:image/jpeg;base64,/9j/...,所以必须先分割取后半段,否则b64decode会报错。
4.2 SQLite 签到表与防重逻辑:同一张脸在视频流里出现 30 帧怎么处理
签到系统最容易在设计上翻车的地方是重复签到。摄像头是流式的,同一个人站在镜头前 10 秒,画面会产生几十帧人脸,如果每一帧都触发一次写入,数据库里就会刷出几十条记录。正确做法是让“同一人同一天只能有一条记录”。
SQLite 建表时通过联合唯一约束从数据库层面兜底:
CREATE TABLE IF NOT EXISTS sign_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, person_id TEXT NOT NULL, person_name TEXT NOT NULL, sign_date TEXT NOT NULL, checkin_time TEXT NOT NULL, UNIQUE(person_id, sign_date) );写入逻辑用两层防重:先查一次今天的记录,如果存在直接返回“已签到”;不存在则插入,利用唯一约束兜底。这样即使前端重复提交、或者两个客户端同时发起请求,数据库也不会写入重复记录。
import sqlite3 from datetime import datetime def insert_sign(person_id, person_name): now = datetime.now() sign_date = now.strftime('%Y-%m-%d') checkin_time = now.strftime('%Y-%m-%d %H:%M:%S') conn = sqlite3.connect('sign.db') cur = conn.cursor() try: cur.execute(''' INSERT INTO sign_record (person_id, person_name, sign_date, checkin_time) VALUES (?, ?, ?, ?) ''', (person_id, person_name, sign_date, checkin_time)) conn.commit() return True except sqlite3.IntegrityError: return False # UNIQUE 约束生效,当天已签过 finally: conn.close()这里更推荐用应用层先查再插的方式返回“今日已签到”的提示,而不是每次都等到IntegrityError才反映。因为用户看到“已签到”和看到“失败”是两种完全不同的体验,前者明确告诉他系统已记录了他的打卡。另外checkin_time不要直接依赖数据库当前时间,统一用 Python 的datetime.now(),这样后续如果要切换不同时区或者做测试,时间逻辑都在代码里可查。
4.3 前端页面与浏览器安全限制:getUserMedia 为什么在局域网 IP 上被拦
前端页面不需要太复杂,一个视频流、一个签到按钮、一个结果提示就够。关键是浏览器对摄像头权限的限制非常严格,这块的坑比后端还多。
<video id="cam" autoplay playsinline></video> <canvas id="frame" style="display:none"></canvas> <button onclick="checkin()">签到</button> <b id="result"></b>async function checkin() { const video = document.getElementById('cam'); const canvas = document.getElementById('frame'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; canvas.getContext('2d').drawImage(video, 0, 0); const dataUrl = canvas.toDataURL('image/jpeg', 0.9); const resp = await fetch('/api/checkin', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({image: dataUrl}) }); const data = await resp.json(); document.getElementById('result').textContent = data.message; } navigator.mediaDevices.getUserMedia({video: true, audio: false}) .then(stream => document.getElementById('cam').srcObject = stream) .catch(err => console.error('摄像头被拒绝', err));drawImage截取当前帧,toDataURL('image/jpeg', 0.9)的第二个参数是 JPEG 压缩质量,0.9 够清晰,再低会损失人脸细节影响识别。最麻烦的是浏览器安全策略:getUserMedia只允许在localhost或 HTTPS 环境下调用。你用http://192.168.1.100:5000访问页面,控制台会报getUserMedia() must be called in secure context。开发时最省事的办法是在本机用http://localhost:5000访问,演示时再给电脑配一个自签名 HTTPS 证书,或者用 Chrome 的 insecure origin 白名单参数启动浏览器。
5. 从装库到摄像头占用:人脸签到落地常见的 5 个坑
人脸签到系统的代码量不一定大,真正卡人的都是环境问题和数据问题。下面这五条是按踩坑频率排的序,基本覆盖从零开始搭建的完整路径。
5.1 ModuleNotFoundError: No module named 'cv2'
现象:运行python app.py,第一行 import 就报错。
原因:大多数情况是 Python 版本太新,老版本opencv-python轮子还没适配;或者依赖装到了全局环境,虚拟环境里没有。还有一部分人错装了opencv-python-headless,它在服务器上能用,但缺少 GUI 相关模块,如果你后续调用cv2.imshow()会直接报错。
解决:进入项目虚拟环境执行pip install opencv-python,如果系统是 Python 3.12 以上,优先安装最新版 opencv。部署到服务器跑识别时,再考虑替换成opencv-python-headless,两者不能混装,否则有版本冲突。
5.2 cv2.error: OpenCV(4.4.0) 之类的参数报错
现象:程序能启动,但跑到某一帧突然中断,报出类似cv2.error: OpenCV(4.4.0) ...的一长串信息。
原因:按经验看绝大多数是图像尺寸或通道数不匹配。比如cv2.warpAffine的 dsize 传了(0, 0),或者把灰度图送进了期望 RGB 输入的特征模型。
解决:先看报错信息末尾的函数名,warpAffine就查输出尺寸,cvtColor就查通道数。建议在关键函数入口打印img.shape,确认是(h, w)还是(h, w, 3)。灰度图是二维数组,BGR 图是三维数组,混用是最常见的问题源。
5.3 摄像头打不开:cap.read() 一直返回 False
现象:页面能打开,但视频是黑的,后端日志显示cap.read()一直返回False。
原因:Windows 下 Flask debug 模式会启动两个进程,两个进程同时抢占摄像头设备;或者摄像头被其他软件占用;虚拟机里没有把宿主机的摄像头设备挂载进来。
解决:把app.run(debug=False)写死,并检查任务管理器里是否有残留 Python 进程。如果多人共用一个 USB 摄像头,前一个人没释放设备,后一个人就打不开。重启电脑是最快的排错手段,能解决八成摄像头占用问题。
5.4 识别率低:注册库照片只有一张自拍
现象:注册时用手机拍了一张正脸照,现场测试时怎么测都不识别。调大阈值能认出来,但陌生人也被误认成已注册人员。
原因:这是数据集质量问题的典型表现。单张照片只覆盖了一个角度和一种光线,现场姿态、明暗稍有变化,特征向量就会漂移出阈值范围。
解决:按第 3 章的方法采集 20 张以上照片,覆盖左右转头、抬头低头、室内灯光和自然光。如果原始打包数据集不够用,可以写脚本按视频帧抽取注册照片。这段是纯数据工程,不值得省时间。
5.5 同一人一分钟内签到了三次:数据库没有防重
现象:测试时发现签到表同一人出现多条记录,而且时间间隔只有几秒。
原因:视频流连续产生人脸帧,前端自动触发多次 POST,而后端没有当天防重逻辑。
解决:用第 4 章的表结构,UNIQUE(person_id, sign_date)约束加上应用层先查再插。另外建议前端设一个状态位,点击签到后按钮置灰,直到收到后端响应再恢复,从源头减少重复请求。
6. 验证与进阶:先做小样本测试,再提活体和防刷
系统能跑通只是第一步,能不能交付看的是阈值标定和边界场景处理。这里介绍一套我习惯用的验证流程和三个值得做的进阶点。
6.1 用小样本验证集把阈值找出来
dlib 官方给的0.5只是经验值,每个注册库的成员构成、摄像头型号、现场光线都会改变最优阈值。正确做法是留出验证集做阈值扫描:注册时每人存 20 张,另留 10 张完全不参与特征库构建,专门用来测试误识率(FAR)和拒识率(FRR)。
for thr in [0.40, 0.45, 0.50, 0.55, 0.60]: far = 0 frr = 0 total_far = 0 total_frr = 0 for name in test_names: for img_path in test_imgs[name]: gray = cv2.imread(img_path, 0) faces = detector(gray, 1) if not faces: continue aligned = align_face(gray, faces[0]) feat = compute_feature(aligned) idx, dist = match(feat, db) if dist < thr: if list(db.keys())[idx] != name: far += 1 else: frr += 1 total_far += 1 if list(db.keys())[idx] != name else 0 total_frr += 1 print(f'thr={thr:.2f} FAR={far/total_far:.3f} FRR={frr/total_frr:.3f}')扫描结果的判读规则简单直接:FAR 是“不是他的人被认成他”,FRR 是“他是本人却没认出来”。签到系统里 FAR 比 FRR 危险,因为误认会导致别人能帮你签到。我一般选“FAR 为 0 且 FRR 尽可能低”的那个阈值,而不是硬套 0.5。
6.2 三个有价值的进阶:活体检测、注册模式、签到时间窗
静态照片攻击是签到系统最容易被挑战的点,一张 A4 纸打印的照片就能骗过基础识别。最简单的活体判断是用关键点算眼睛睁开程度:连续 5 帧中检测到眨眼动作才认为“人是活的”。用 68 点关键点里上眼睑和下眼睑的坐标比值就能实现,不需要额外模型,代码量也不大,但答辩时说到“防照片攻击”会非常加分。
注册模式也值得做。与其手工整理数据集,不如在系统里加一个“注册页”:人站在摄像头前,自动采样 30 帧,剔除模糊帧后取特征均值入库。这样换人登记就不需要碰代码。签到时间窗可以配合考勤规则:超过早上 9 点签到标记“迟到”,写入独立字段。SQLite 表加一列status即可,逻辑不复杂但让系统从“签到工具”变成“考勤系统”,实用性提升不少。
这些功能加完后,记得重新过一遍验证集。阈值不是调好一次就不变的,每加一个人进特征库,整个人脸特征分布都会变化,阈值可能有几毫的偏移。我自己的习惯是每新增一批注册数据就重跑一次阈值扫描,哪怕结果没变,也让心里有底。用固定套路反复验证,比临时瞎调省心得多。希望帮到你。
本文还有配套的精品资源,点击获取