简介:简易人事管理系统基于PyQt5与MySQL实现,面向初学桌面端开发与数据库联调的开发者,也适合作为ERP系统中人事模块的参考方案。资源包含完整的员工、部门、薪资、登录验证等管理界面与逻辑,通过Python脚本和SQL文件即可运行与二次开发。压缩包共49个文件,以40个Python程序文件为主,涵盖主界面、增删改查、表格展示及数据库连接等模块,另含7张界面图标、1份SQL建库脚本和1个说明文档,整体约295KB,轻量易部署。目前已吸引2688人学习,适合用来拆解PyQt5的窗体设计、信号槽机制以及MySQL数据交互思路。从内容预览看,目录结构按功能拆分清晰,从登录、员工管理到薪资记录均有对应实现,可帮助读者快速掌握一个迷你人事系统的完整开发路径。
1. 简易人事管理系统(PyQt5+MySQL):中小团队摆脱 Excel 混乱的轻量方案
先说你会在什么场景下需要它:部门负责人发现员工花名册在 Excel 里被改了三版,离职人员的删除逻辑全靠手工;老板想要一张按部门统计的薪资表,行政要加一列学历,整个表格就乱掉。这种规模下没必要上重型的 SaaS 人事系统,用 PyQt5+MySQL 拼一个桌面工具反而是更务实的选择。这套代码是完整可运行的项目,覆盖登录权限、员工档案管理、部门检索、薪资统计和离职管理五大块,新人照着改就能部署上线。适合有 Python 基础的开发者做二次开发,也适合课程设计阶段拿来做功能演示。
2. 项目骨架与数据库表设计:先看清代码是怎么分的层
拿到源码包不要急着 F5,先花十分钟把目录结构读一遍。PyQt5 项目的常见问题是所有代码都堆在 main.py 里,这个资源刻意拆成了 ui、models、utils 三层,后期维护逻辑清楚很多。
2.1 目录结构和分层思路
典型的目录是这样的:
hrms/ ├── main.py # 程序入口,负责启动和异常兜底 ├── db.py # 数据库连接单例 ├── ui/ │ ├── login_window.py # 登录窗口 │ └── main_window.py # 主界面(员工管理、统计、导出) ├── models/ │ ├── employee.py # 员工增删改查、搜索、分页 │ └── statistics.py # 部门统计、薪资聚合 └── utils/ ├── session.py # Session 全局对象,保存登录状态 └── hashers.py # 密码哈希工具我一般会坚持这样分层:UI 文件里只放 PyQt5 的控件创建与事件绑定,models 层放 SQL 与业务逻辑,db.py 是唯一能连接数据库的模块。新手最容易犯的毛病是直接在按钮回调里拼 SQL,等模块一多,参数传错一个就开始互相污染。
2.2 MySQL 三张核心表的设计
这套系统只围绕三个业务主体:部门、员工、用户。部门表和员工表是一对多关系,用户表与员工表一对一绑定。先看建表脚本:
CREATE DATABASE IF NOT EXISTS hrms DEFAULT CHARACTER SET utf8mb4; USE hrms; CREATE TABLE departments ( id INT AUTO_INCREMENT PRIMARY KEY, dept_name VARCHAR(50) NOT NULL UNIQUE, manager VARCHAR(20) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE employees ( emp_no VARCHAR(20) NOT NULL, name VARCHAR(50) NOT NULL, gender TINYINT NOT NULL DEFAULT 1, phone VARCHAR(11), dept_id INT NOT NULL, position VARCHAR(50), salary DECIMAL(10,2), hire_date DATE, status TINYINT NOT NULL DEFAULT 1, PRIMARY KEY (emp_no), KEY idx_dept (dept_id), KEY idx_emp_name (name), CONSTRAINT fk_dept FOREIGN KEY (dept_id) REFERENCES departments(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(30) NOT NULL UNIQUE, password_hash VARCHAR(64) NOT NULL, password_salt CHAR(16) NOT NULL, role TINYINT NOT NULL DEFAULT 2, emp_no VARCHAR(20), CONSTRAINT fk_user_emp FOREIGN KEY (emp_no) REFERENCES employees(emp_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有几个容易被新手忽略的细节。员工表我用 emp_no 做业务主键而不是自增 id,因为工号在业务上天然唯一,比如“EMP001”这类字符串,直接用做主键能避免系统里出现两个同号员工;如果你用自增 id,就还需要额外加一个唯一索引去兜底。
phone 字段从一开始就规划为 VARCHAR(11),而不是 BIGINT。BIGINT 存手机号在两个场景下会翻车:一是员工手机号里有特殊长号或将来升级到 12 位,二是前端展示时想保留 011 开头的区号或 001 的前缀,数值型会把前导零抹掉。
外键有意识地被保留下来。当你尝试删除一个还有在职员工的部门时,MySQL 会直接拒绝并报 1451 错误,这就逼迫业务层先处理好员工转移或离职标记。实际使用中很多系统为了省事去掉了外键,结果部门删了,员工落到孤儿数据,报表对不上账。
2.3 数据库连接驱动选型与 db.py 单例
Python 连 MySQL 的驱动日常是 pymysql、mysql-connector-python 二选一。
| 选型 | 包名 | 优点 | 缺点 |
|---|---|---|---|
| pymysql | pymysql | 纯 Python 实现,pip 安装即用 | 性能比官方 C 扩展略低 |
| mysql-connector-python | mysql-connector-python | MySQL 官方维护,协议兼容度高 | 包体积大,部分机器存在运行时依赖问题 |
这个项目用的是 pymysql,因为典型的中小团队内部系统并发不会超过 20,pymysql 的表现完全够用,而且打包时体积更小。数据库连接模块写成了单例,避免每次点击按钮都新建连接、用完又释放的开销:
# db.py import pymysql from pymysql.cursors import DictCursor class MySQLHelper: _instance = None def __init__(self, host='localhost', port=3306, user='root', password='123456', database='hrms'): self.conn = pymysql.connect( host=host, port=port, user=user, password=password, database=database, charset='utf8mb4', autocommit=False, cursorclass=DictCursor ) @classmethod def get_instance(cls): if cls._instance is None: cls._instance = cls() return cls._instance def query(self, sql, params=None): with self.conn.cursor() as cur: cur.execute(sql, params or ()) return cur.fetchall() def execute(self, sql, params=None): with self.conn.cursor() as cur: affected = cur.execute(sql, params or ()) self.conn.commit() return affected def close(self): if self.conn and self.conn.open: self.conn.close()几点说明:cursorclass 设为 DictCursor,这样每一行结果在 Python 里是一个 dict,按字段名取值,可读性和维护性都比 tuple 好。charset 必须写 utf8mb4 而不是 utf8,否则表情字符或特殊生僻字入库会报错,后面第 5 章会展开说。autocommit=False 是刻意控制的,目的是让“修改员工 + 记录日志”这类强一致的业务可以包在同一个事务里,只要在业务层统一调用 execute 并捕获异常,就能保证要么都成功、要么都回滚。
3. 登录与员工管理:PyQt5 与 MySQL 联动的核心模块
3.1 登录窗口、密码哈希与权限标识
登录窗口在 Qt 里就是 QLineEdit 加两个按钮。关键并不在界面上,而在认证逻辑的写法。这套代码没有把密码明文存库,而是用带盐的 pbkdf2_hmac 做单向哈希:
# utils/hashers.py import hashlib import os SALT_BYTES = 16 ITERATIONS = 100_000 def make_password(password: str) -> tuple: salt = os.urandom(SALT_BYTES) hashed = hashlib.pbkdf2_hmac( 'sha256', password.encode('utf-8'), salt, ITERATIONS ) return hashed.hex(), salt.hex() def check_password(password: str, hashed: str, salt_hex: str) -> bool: salt = bytes.fromhex(salt_hex) trial = hashlib.pbkdf2_hmac( 'sha256', password.encode('utf-8'), salt, ITERATIONS ) return trial.hex() == hashed登录回调里拿用户名去 users 表查记录,查到后调 check_password 做校验。校验通过以后把用户 id、角色写进 utils/session.py 里的全局 Session 对象。Session 对象的核心作用不是“安全认证”,而是给界面层做一个权限开关——比如 role 为 1 的管理员可以看到“删除员工”按钮,普通 HR 只有查询和编辑权限。
新手经常在这里直接把密码做单个 MD5。MD5 的问题是攻击者可以拿彩虹表秒破,而且两个同密码用户的哈希值完全一致,等于把明文写在门牌号上。用 pbkdf2_hmac 迭代到 10 万次,单个调用耗时在几十毫秒,登录体验没有明显差别,安全性却高一截。
登录的边界情况也要处理:连续输错三次是关闭窗口还是轻量冻结?这个项目里用的是界面禁用 30 秒。实际上更好的方案是在 users 表加一个 failed_attempts 字段,但那种程度是后端系统的追求,桌面端做到冻结输入框就够了。
3.2 员工列表加载:用原生 SQL 替代 QSqlTableModel
很多 PyQt5 教程推荐 QSqlTableModel 或 QSqlQueryModel 做表格映射。但这套系统最终没有用它们,原因是员工列表需要 JOIN departments 表拿中文部门名,且还要在加载流程里做状态翻译。用 QSqlTableModel 时写 JOIN 会比较别扭,回头还得靠 setQuery 去拼原始 SQL,既然最终都是拼 SQL,不如直接用 pymysql 取数据然后塞进 QStandardItemModel,控制力更强、代码也更直观。
# models/employee.py from PyQt5.QtGui import QStandardItemModel, QStandardItem from db import MySQLHelper def load_employee_model(keyword=''): db = MySQLHelper.get_instance() if keyword.strip(): sql = """ SELECT e.emp_no, e.name, d.dept_name, e.position, e.salary, e.status FROM employees e LEFT JOIN departments d ON e.dept_id = d.id WHERE e.name LIKE %s OR e.emp_no LIKE %s ORDER BY e.emp_no ASC """ like = f'%{keyword.strip()}%' rows = db.query(sql, (like, like)) else: sql = """ SELECT e.emp_no, e.name, d.dept_name, e.position, e.salary, e.status FROM employees e LEFT JOIN departments d ON e.dept_id = d.id ORDER BY e.emp_no ASC """ rows = db.query(sql) model = QStandardItemModel(len(rows), 6) model.setHorizontalHeaderLabels(['工号', '姓名', '部门', '职位', '薪资', '状态']) status_map = {1: '在职', 0: '离职'} for row_idx, rec in enumerate(rows): model.setItem(row_idx, 0, QStandardItem(rec['emp_no'])) model.setItem(row_idx, 1, QStandardItem(rec['name'])) model.setItem(row_idx, 2, QStandardItem(rec['dept_name'])) model.setItem(row_idx, 3, QStandardItem(rec['position'])) model.setItem(row_idx, 4, QStandardItem(f"{rec['salary']:.2f}")) model.setItem(row_idx, 5, QStandardItem(status_map.get(rec['status'], '未知'))) return model这里要重点看的是 LIKE 查询的参数化写法。我在 WHERE 里用了%s占位符,配合 pymysql 的参数绑定,而不是把 keyword 直接拼进 SQL。直接拼接的后果是:搜索词里出现单引号或分号时,MySQL 会直接把它当作 SQL 执行,属于经典的注入漏洞。
# 错误示范,工程上千万不要这么用 sql = f"SELECT * FROM employees WHERE name LIKE '%{keyword}%'"3.3 新增、编辑与删除员工:表单校验和事务
新增员工在界面层是一个 QDialog,里面放 QLineEdit 和 QComboBox。提交时先把表单数据聚合成一个 dict,然后调 models 层的方法:
# models/employee.py def add_employee(emp_no, name, gender, phone, dept_id, position, salary, hire_date): db = MySQLHelper.get_instance() sql = """ INSERT INTO employees (emp_no, name, gender, phone, dept_id, position, salary, hire_date, status) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, 1) """ try: return db.execute(sql, (emp_no, name, gender, phone, dept_id, position, salary, hire_date)) except pymysql.err.IntegrityError as exc: if exc.args[0] == 1062: # duplicate entry raise ValueError('工号已存在,请检查后重试') if exc.args[0] == 1451: # foreign key constraint fails raise ValueError('部门不存在或已被停用') raise在 UI 回调里捕获 ValueError 并弹 QMessageBox.warning。注意这里没有在每个按钮里分别写连接和提交逻辑,而是统一走 models 层,方便复现和测试。
性别在库里存的是 TINYINT 0/1,界面上显示出来的却是“男/女”,我在新增面板里用 QComboBox 做映射,提交前把 text 转成数字;反过来编辑时要把数字填回 ComboBox 的当前索引。很多二次开发的读者在这一步会直接出问题——setCurrentText('男') 在 ComboBox 里找不到对应 item 并不报错,只有肉眼确认。一个建议是统一封装两个方法:gender_to_text(code)和text_to_gender(text),所有界面调用同一对函数,就不会出现这个窗口显示男、下一个窗口显示 1 的割裂。
3.4 刷新后选中行丢失与排序状态重置
员工列表变更是高频动作,每次新增完都要重新 load。问题来了:刷新后用户正在编辑的位置被顶掉了,数据改完了还要重新翻页找人。常规做法是刷新前记录当前选中行的主键 emp_no,刷新完成后重新定位:
def refresh_and_restore(self): selected = self.tableView.selectionModel().currentIndex() if selected.isValid(): emp_no = self.model.index(selected.row(), 0).data() else: emp_no = None new_model = load_employee_model(self.search_keyword.text()) self.tableView.setModel(new_model) if emp_no is not None: for r in range(new_model.rowCount()): if new_model.index(r, 0).data() == emp_no: self.tableView.selectRow(r) self.tableView.scrollTo(new_model.index(r, 0)) break这一段的逻辑很简单,但几乎是我见过二次开发代码里缺失最多的环节。没有 restore 的话,每次编辑完表格都跳回第一行,用户在 500 条员工数据的场景里操作体验会很糟。这种细节往往影响整个系统的可用性评价。
4. 统计与检索:把人事数据变成决策可用的数字
4.1 部门人数与薪资汇总的 SQL 写法
统计模块是这套系统的加分项。先看 SQL:
SELECT d.dept_name, COUNT(e.emp_no) AS emp_count, SUM(e.salary) AS total_salary, ROUND(AVG(e.salary), 2) AS avg_salary FROM employees e INNER JOIN departments d ON e.dept_id = d.id WHERE e.status = 1 GROUP BY e.dept_id, d.dept_name ORDER BY emp_count DESC;这段 SQL 在 models/statistics.py 里被封装为一个函数,返回 list of dict,界面层接住之后塞进 QTableWidget 展示。需要注意的是 INNER JOIN 与 LEFT JOIN 的选择:这里只统计“在职”员工,所以 INNER JOIN 天然过滤掉已经没有实际人员的空部门;如果你的报表需要把“空部门也展示出来”这种边界需求,就得改成 LEFT JOIN,并在 SELECT 里用 IFNULL(SUM(e.salary), 0) 兜底。
界面展示上,我建议先放一个 QTableWidget 展示数字表格,再做“按部门人数占比”的图形化展示。不少读者想直接上 QChart 的饼状图,就可以用如下代码把它放出来:
# ui/main_window.py 中的片段 from PyQt5.QtChart import QChart, QChartView, QPieSeries def show_dept_pie(self, dept_stats): series = QPieSeries() for rec in dept_stats: if rec['emp_count'] > 0: series.append(f"{rec['dept_name']} ({rec['emp_count']}人)", rec['emp_count']) chart = QChart() chart.addSeries(series) chart.legend().setVisible(True) chart.legend().setAlignment(Qt.AlignBottom) view = QChartView(chart) view.setRenderHint(QPainter.Antialiasing) self.chart_container.setCentralWidget(view)实用性比较强,但必须提醒一点:QChart 的 series 数据里如果混入 None 或 NaN,图会在运行时异常退出,所以从 MySQL 取出的数据先做类型转换,确保 emp_count 是 int。
4.2 分页加载和关键字搜索
随着员工数量增长到几千,一次性把所有行塞进 QStandardItemModel 就会带来肉眼可见的卡顿。真正的原生问题不在 Qt,而在 SQL 的LIMIT/OFFSET分页写法。这个项目实现了 page 参数:
def load_page(self, page=1, page_size=20, keyword=''): offset = (page - 1) * page_size db = MySQLHelper.get_instance() like = f'%{keyword.strip()}%' rows = db.query( """ SELECT e.emp_no, e.name, d.dept_name, e.position, e.salary, e.status FROM employees e LEFT JOIN departments d ON e.dept_id = d.id WHERE e.name LIKE %s OR e.emp_no LIKE %s ORDER BY e.emp_no ASC LIMIT %s OFFSET %s """, (like, like, page_size, offset) ) total = db.query( "SELECT COUNT(*) AS cnt FROM employees WHERE name LIKE %s OR emp_no LIKE %s", (like, like) )[0]['cnt'] return rows, total分页的关键是每次翻页都要重新执行 COUNT 语句,因为上一页数据可能在这期间被另一个客户端修改过。如果省略这一步,页面上“共 5 页”的显示就可能对不上实际数据量。pymysql 的 LIMIT 和 OFFSET 都支持参数化,可以放心绑定。
分页控件在 Qt 里没有现成的组件,常见实现是拿 QSpinBox 加“上一页/下一页”两个按钮自己拼。务必在翻页函数里重新获取总记录数并更新 SpinBox 的最大值,否则用户会从第 1 页一路点“下一页”直到超出最大页才报错。
4.3 导出 Excel 与一键备份
统计完数据,下一步往往是把报表丢给领导。该资源用 pandas + openpyxl 导出:
import pandas as pd def export_employees_to_excel(file_path): db = MySQLHelper.get_instance() sql = """ SELECT e.emp_no, e.name, d.dept_name, e.position, e.salary FROM employees e LEFT JOIN departments d ON e.dept_id = d.id WHERE e.status = 1 """ df = pd.read_sql(sql, db.conn) df.columns = ['工号', '姓名', '部门', '职位', '薪资'] df.to_excel(file_path, index=False, engine='openpyxl')pd.read_sql 直接把 pymysql 的 connection 对象接进去,比逐行 fetch 再排版方便很多。导出后可以在回调里用 QDesktopServices.openUrl 直接打开文件位置。
备份功能我一般不建议在 GUI 线程里调用 mysqldump,因为大库 dump 会阻塞界面 5 到 10 秒。更稳妥的做法是放进 QThread 里执行:
import subprocess def backup_db(out_path): subprocess.run([ 'mysqldump', '-uroot', '-p123456', '--default-character-set=utf8mb4', 'hrms', '--result-file', out_path ], check=True)参数里务必带上--default-character-set=utf8mb4,否则导出的 SQL 文件会在源头丢失部分生僻字。
5. 避坑 / 常见问题排查:五个高频事故现场与解法
这一章是实战里沉淀的,每一个坑我都对应看过真实运行环境中的表现。
5.1 现象:程序放置 8 小时后再点击查询,界面弹出 “Lost connection to MySQL server”
原因:MySQL 服务端有一个 wait_timeout 参数,默认通常是 8 小时(28800 秒)。在这期间该连接没有任何查询操作,服务器会主动断开这个空闲连接。pymysql 自身不会自动重连,于是下一次 query 时直接抛异常。
解决:在 db.py 里给连接增加一个“探测 + 重连”机制:
def ensure_connected(self): try: self.conn.ping(reconnect=True) except Exception: # ping 失败后重建连接 self.__init__()在每次 query 和 execute 之前调用 ensure_connected(),这也是大型桌面应用常见的做法。一个小经验是不要只在程序启动时做一次 ping,因为 MySQL 断开的时间点不可控,所有的数据库操作入口都要先走一遍。
5.2 现象:新增员工时明明提交的是 “001”,查询出来变成 “1”
原因:数据库中 emp_no 字段被定义成了 INT(可能是从 MySQL Workbench 里把字符串手误选成了 int)。数字类型在存储时不会保留前导零。
解决:把表结构改成 VARCHAR,并重新导入数据:
ALTER TABLE employees MODIFY emp_no VARCHAR(20) NOT NULL; UPDATE employees SET emp_no = LPAD(emp_no, 3, '0') WHERE LENGTH(emp_no) < 3;同时在新增表单里校验工号只能输入字母和数字,并限制长度。这类坑往往在系统上线一两周后有员工离职、工号进入两位数时才突然暴露。
5.3 现象:本地运行正常,用 PyInstaller 打包后移到别的电脑上,双击提示 ModuleNotFoundError: No module named 'pymysql'
原因:PyInstaller 不会自动把纯 Python 包之外的动态依赖收集齐全。pymysql 虽是纯 Python,但有时因为打包参数--onefile的处理方式,它没有进入打包图里,或者被当作隐藏导入漏掉了。
解决:在打包命令中显式加上 hidden-import:
pyinstaller -w -F main.py \ --hidden-import pymysql \ --hidden-import openpyxl \ --hidden-import pandas打包完成后在干净的虚拟机或另一台没有安装 Python 环境的机器上冒烟测试。只在原开发机运行成功并不能证明打包产物可用。
5.4 现象:界面上的中文显示正常,但写入数据库后显示为乱码“??”
原因:连接时 charset 写成了 utf8。MySQL 的 utf8 字符集最多支持 3 字节编码,而现代汉字及 emoji 是 4 字节(utf8mb4)。用 utf8 写入时,包含某些特殊字符的数据会被丢弃部分字节,变成问号。
解决:统一改为 utf8mb4。除了代码里的连接参数,还建议检查 MySQL 配置文件:
[mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci改完编码后,已经乱掉的数据要先把表里的旧数据导出,再在 utf8mb4 下重新导入。只改连接参数救不回已乱码的历史数据。
5.5 现象:新增员工很慢,或者在 MySQL 命令行中看到大量 “Waiting for table metadata lock”
原因:员工表或部门表被一个未提交的长事务锁住了。很常见的情况是某人开启了一个事务,执行了查询或修改,但没有 commit 也没有 rollback,就离开了。这个长事务持有的锁会阻塞其他会话的写操作。
解决:在代码里严格按“开启-提交-捕获异常回滚”三明治方式管理事务边界:
try: db.execute("INSERT INTO ...", params) db.execute("INSERT INTO log ...", log_params) db.conn.commit() except Exception: db.conn.rollback() raise另外用下面这条 SQL 排查长时间未提交的事务:
SELECT * FROM information_schema.INNODB_TRX;找到 trx_started 时间特别早的线程,KILL 掉对应的连接后锁就释放了。之前遇到过一次测试窗口开了一整天没关,事务挂了一整天,导致部门表一直删不掉。
6. 打包与部署:交到使用人手里之前的必要准备
把代码跑通只是第一步,真正交付给行政或人事使用时,还得把可执行文件打到目标机器上。
6.1 PyInstaller 打包命令与参数说明
下面是针对 Windows 桌面场景下比较稳妥的打包参数:
pyinstaller -w -F main.py \ --name Hrms \ --hidden-import pymysql \ --hidden-import openpyxl \ --hidden-import pandas \ --collect-all PyQt5.QtChart-w去掉控制台窗口,-F打包成单文件。--collect-all PyQt5.QtChart是为了把 QtChart 的插件和加载器一起收进去,如果漏了这一步,在目标机器上打开统计图表时会报找不到相关插件。打包完成后检查 dist 目录下的文件大小,一般 30-60MB 属于正常区间。如果体积异常小,大概只有 5MB,多半是 Qt 的插件目录没被带进包,启动时会白屏或直接闪退。
6.2 交付前对 MySQL 连接参数的最终配置
桌面软件和 Web 服务不一样,它会长时间挂在后台。建议交付前把 MySQL 的几个超时参数调大一些:
[mysqld] wait_timeout = 28800 interactive_timeout = 28800 max_connections = 200这几个参数配置的是 MySQL 服务端,不随程序打包。如果部署机器上的 MySQL 由其他同事统一管理,请提前沟通好,否则又会连续触发第 5 章那个“闲置一段时间后提示连接断开”的问题。
最后我建议拿到这套源码后的第一次二次开发,别急着改界面,先去跑通登录、建好库、造好数据。能在真实数据库上跑通核心链路之后,你遇到的技术问题就不再是“程序逻辑怎么写”这种宽泛问题,而是能直接定位到某个模块、某个字段、某个参数。每改完一个功能,从头走一遍“登录→新增员工→按部门统计→导出”二十步回归流程。
这已经是我每次交付桌面系统都强制执行的一套流程了,几次大翻车后养成的习惯,希望帮到你。
本文还有配套的精品资源,点击获取