Member Management System 是一个基于 Python + PySide6 + MySQL 的桌面端会员数据管理系统。
主要面向需要通过 Excel 批量维护会员档案的场景,支持:
- 将 Excel 文件拖入窗口即可完成批量导入
- 在表格中直接编辑、删除单条记录
- 一键导出当前视图(或选中行)为 Excel
- 通过点击列头进行 SQL 级排序
Excel 数据源结构(来自项目内 .xlsx 示例文件):
| Excel 列名 | 数据库列名 | 类型 | 说明 |
|---|---|---|---|
| Member | member | INT (PK) | 会员编号(主键) |
| Type | type | VARCHAR | 会员类型 |
| Date in | date_in | DATE | 入会日期 |
| Payment | payment | DATE | 缴费日期 |
| $ | dollar | VARCHAR | 金额/类别标记 |
| Issued | issued | DATE | 证件签发日期 |
| Renewal | renewal | DATE | 到期/续费日期 |
| Last | last | VARCHAR | 姓(Last Name) |
| First | first | VARCHAR | 名(First Name) |
| Country | country | VARCHAR | 国家 |
| Address H | address_h | TEXT | 住宅地址 |
| Address B | address_b | TEXT | 单位地址 |
| VARCHAR | 电子邮件 | ||
| web | web | VARCHAR | 网址 |
| tel | tel | VARCHAR | 电话 |
| Activity | activity | VARCHAR | 活动标记 |
| other | other | TEXT | 备注 |
| 层次 | 技术 | 版本要求 |
|---|---|---|
| 语言 | Python | ≥ 3.10 |
| GUI | PySide6 | ≥ 6.5 |
| ORM / 数据库访问 | SQLAlchemy (Core) | ≥ 2.0 |
| 数据库驱动 | PyMySQL | ≥ 1.1 |
| 数据库 | MySQL | ≥ 5.7 / 8.x |
| Excel 处理 | pandas + openpyxl | pandas ≥ 2.0 |
Members_Manager/
│
├── main.py # 程序入口
├── create_database.py # 独立建库脚本(首次运行)
├── requirements.txt # Python 依赖清单
├── .gitignore
│
├── utils/
│ ├── __init__.py
│ ├── helpers.py # 通用工具函数(列名转换等)
│ └── db_config.py # 数据库连接配置(由 create_database.py 生成)
│
├── database/
│ ├── __init__.py
│ ├── db.py # SQLAlchemy Engine 管理
│ └── dynamic_model.py # 运行时反射表结构,提供列名/类型/主键
│
├── services/
│ ├── __init__.py
│ ├── member_service.py # 数据库 CRUD 操作(含智能排序)
│ ├── excel_service.py # Excel 导入 / 导出逻辑
│ ├── email_service.py # SMTP 配置与邮件发送
│ └── notification_service.py # 到期检测、去重、通知编排
│
└── ui/
├── __init__.py
├── main_window.py # 主窗口(工具栏、表格、拖放)
└── dialogs.py # 新增 / 编辑记录对话框
本项目采用三层架构,各层职责严格分离:
┌─────────────────────────────────────────┐
│ UI 层 (ui/) │
│ main_window.py · dialogs.py │
│ 负责:界面渲染、用户交互、事件响应 │
└────────────────┬────────────────────────┘
│ 调用
┌────────────────▼────────────────────────┐
│ 业务逻辑层 (services/) │
│ member_service.py · excel_service.py│
│ 负责:数据校验、导入/导出规则、CRUD 封装 │
└────────────────┬────────────────────────┘
│ 调用
┌────────────────▼────────────────────────┐
│ 数据层 (database/) │
│ db.py · dynamic_model.py │
│ 负责:连接管理、表结构反射、SQL 执行 │
└─────────────────────────────────────────┘
重要设计原则:
- UI 层不直接访问数据库,所有数据操作经由
services/层中转。 database/dynamic_model.py在启动时通过 SQLAlchemy 反射(MetaData.reflect())自动感知表结构,无需硬编码列名,使系统具备适应不同 Excel 模板的能力。create_database.py与主程序完全独立,仅在初始化阶段运行一次。
create_database.py 使用 utils/helpers.py 中的 sanitize_column_name() 函数,将 Excel 列名自动转换为合法的 SQL 标识符:
转换规则(按优先级):
1. 特殊字符映射:$ → dollar,# → number,% → percent
2. 转为小写
3. 连续空格 → 下划线
4. 非字母数字字符 → 下划线
5. 合并连续下划线
6. 去除首尾下划线
7. 若结果为空或以数字开头,加前缀 col_
示例:Date in → date_in,Address H → address_h,$ → dollar
表的第一列(Member)被自动设为整型主键(INT PRIMARY KEY, autoincrement=False)。主程序通过 get_primary_key_column() 动态获取,无硬编码。
- Python 3.10+
- 已安装并运行的 MySQL 服务(本地或远程)
- 准备好要导入的
.xlsx文件
pip install -r requirements.txtpython create_database.py脚本会交互式询问以下信息:
MySQL host [localhost]: ← 直接回车使用默认值
MySQL port [3306]:
MySQL user [root]:
MySQL password: ← 输入密码(不回显)
Database name [members_db]:
Table name [members]:
脚本执行后:
- 自动创建数据库(如不存在)
- 读取 Excel 文件推断各列类型
- 在终端打印列映射表
- 创建
members数据表 - 将连接配置写入
utils/db_config.py
若需使用不同的 Excel 文件作为模板:
python create_database.py --excel /path/to/your_file.xlsx若需强制删除并重建已存在的表(会清空数据):
python create_database.py --forcepython main.py如果数据库连接失败,程序会弹出错误对话框并提示解决方法,不会崩溃。
- 启动后自动加载全部数据,展示在中央表格中
- 表格为只读,每行末尾提供 Edit 和 Delete 操作按钮
- 双击任意行也可进入编辑模式
点击任意列头即可按该列排序(升序);再次点击同一列头切换为降序。
排序在数据库侧执行(ORDER BY),状态栏右侧会显示当前排序列和方向(▲/▼)。
方式 A:拖放
将一个或多个 .xlsx / .xls 文件直接拖入程序窗口,窗口顶部的提示横条会高亮显示可放置区域。
方式 B:按钮 点击工具栏的 ⬇ Import Excel,通过文件选择对话框选取文件(支持多选)。
导入逻辑:
读取 Excel
↓
列名标准化(sanitize_column_name)
↓
检查必需列是否存在 → 缺失则报错,整文件拒绝
↓
逐行检查主键是否已存在于数据库
↓ 有冲突
列出所有冲突主键值 → 报错,整文件拒绝,不插入任何行
↓ 无冲突
批量 INSERT 所有行
↓
刷新表格,显示成功数量
注意: 主键冲突采用"全拒绝"策略——只要文件中有任意一行的主键已存在,整个文件都不会被导入,保证数据一致性。
点击工具栏的 ⬆ Export Excel,弹出保存文件对话框。
- 选中了行 → 仅导出选中行
- 未选中任何行 → 导出当前视图的全部数据(按当前排序)
导出文件使用人类可读的列标题(如 Date In、Address H),适合直接分发。
点击工具栏的 + Add Record,弹出动态表单。
表单字段根据数据库列结构自动生成。日期字段需填写 YYYY-MM-DD 格式(留空表示 NULL)。主键字段不可为空。
点击行末的 Edit 按钮,或双击该行,弹出预填数据的编辑表单。主键字段在编辑模式下为只读。
点击行末的 Delete 按钮,弹出二次确认对话框,确认后执行数据库删除并刷新表格。
程序启动 2 秒后,会在后台线程自动扫描全部会员,对 30 天内即将到期 的会员发送提醒邮件。同一会员的同一到期日只会发送一次。
renewal 列 |
issued 列 |
生效到期日 |
|---|---|---|
| 不为空 | — | 直接使用 renewal 的值 |
| 为空 | 有值 | issued + 1 年 − 1 天(闰年 2 月 29 日 → 次年 2 月 28 日) |
| 为空 | 也为空 | 跳过,不处理 |
email 字段可能包含多个地址,以任意非邮件字符(逗号、空格、分号、竖线等)分隔。系统使用正则表达式 [a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,} 提取全部有效地址,并同时发送给所有地址。
数据库中有一张辅助表 notification_log,结构如下:
CREATE TABLE notification_log (
id INT AUTO_INCREMENT PRIMARY KEY,
member_id INT NOT NULL,
expiry_date DATE NOT NULL,
sent_at DATETIME NOT NULL,
UNIQUE KEY uq_member_expiry (member_id, expiry_date)
);每次发送成功后写入一条记录。UNIQUE KEY 保证相同的 (member_id, expiry_date) 组合永远不会重复触发,即便程序每天多次启动也不会重复发邮件。
注意:如果某会员的
renewal日期被修改(到期日变了),新的(member_id, new_expiry_date)组合不在日志表中,会重新发送一封通知。
编辑 services/email_service.py,填入 SMTP 配置和邮件正文:
SMTP_HOST = "smtp.gmail.com" # SMTP 服务器
SMTP_PORT = 587 # STARTTLS 端口
SMTP_USER = "yourname@gmail.com" # 发件人地址
SMTP_PASSWORD = "xxxx xxxx xxxx xxxx" # Gmail App Password同文件中的 subject、body_text、body_html 三个变量用于填写邮件主题和正文(支持纯文本和 HTML 双格式)。
若 SMTP 未配置,通知检查会静默跳过(不崩溃、不弹窗),仅在控制台打印提示。
| 文件 | 职责 |
|---|---|
services/email_service.py |
SMTP 配置、邮件构建与发送 |
services/notification_service.py |
扫描到期会员、去重判断、编排发送流程 |
ui/main_window.py |
启动 2 s 后用 QTimer + 守护线程触发检查,结果通过 Signal 回传主线程 |
| 函数 | 说明 |
|---|---|
sanitize_column_name(name) |
将任意字符串转换为合法 SQL 列名 |
db_column_to_display(name) |
将 DB 列名(date_in)转为显示标签(Date In) |
| 函数 | 说明 |
|---|---|
get_engine() |
懒加载并缓存 SQLAlchemy Engine;失败时抛出 RuntimeError(含友好提示) |
dispose_engine() |
释放连接池(程序退出时调用) |
| 函数 | 说明 |
|---|---|
get_table_obj(table_name) |
返回反射后的 SQLAlchemy Table 对象(带缓存) |
get_table_columns() |
返回列名列表(按定义顺序) |
get_primary_key_column() |
返回主键列名 |
get_column_types() |
返回 {列名: 类型字符串} 字典,类型值为 'integer' | 'float' | 'date' | 'text' | 'string' |
invalidate_cache() |
清除反射缓存(表结构变更后调用) |
| 函数 | 说明 |
|---|---|
get_all(order_by, ascending) |
查询全部记录,支持单列排序(字符串列自动 CAST 数值排序) |
get_count() |
返回记录总数 |
member_exists(pk_value) |
判断指定主键的记录是否存在 |
insert_member(data) |
插入一条记录 |
update_member(pk_value, data) |
更新指定主键的记录 |
delete_member(pk_value) |
删除指定主键的记录 |
| 元素 | 说明 |
|---|---|
SMTP_HOST / PORT / USER / PASSWORD |
发件方配置(需手动填写) |
send_expiry_notice(emails, name, expiry) |
构建并发送到期提醒邮件 |
| 函数 | 说明 |
|---|---|
check_and_send_notifications() |
入口函数;返回 (sent_count, errors) |
_extract_emails(raw) |
正则提取邮件地址列表 |
_calc_expiry(issued) |
计算标准到期日(+1年−1天) |
_ensure_notification_table(engine) |
自动建表 notification_log(幂等) |
| 函数 | 说明 |
|---|---|
import_excel(filepath) |
导入单个 Excel 文件;含列校验和主键冲突预检;返回已插入行数 |
export_excel(filepath, data, columns) |
将数据列表导出为 Excel 文件 |
MainWindow(QMainWindow) 主窗口,负责:
- 工具栏(Import / Export / Add / Refresh)
- 拖放事件处理(
dragEnterEvent/dropEvent) - 数据加载与表格渲染(
_load_data/_repaint_table) - 列头点击排序(
_on_header_click) - 调用 dialogs 完成新增 / 编辑流程
AddEditDialog(QDialog) 动态表单对话框,负责:
- 根据列类型自动生成输入控件(
QLineEdit或QTextEdit) - 在保存前进行类型校验(日期格式、整数、浮点数)
- 主键在编辑模式下置为只读
| 场景 | 处理方式 |
|---|---|
| 数据库连接失败 | 启动时弹出错误对话框,提示检查 db_config.py 或重新运行建库脚本 |
| 表不存在 | dynamic_model.get_table_obj() 抛出 RuntimeError,主窗口捕获后弹框提示 |
| Excel 文件无法读取 | import_excel() 抛出 ValueError,主窗口显示错误详情 |
| Excel 缺少必需列 | 同上,列出所有缺失列名 |
| 主键冲突(导入时) | 列出全部冲突值,整文件拒绝 |
| 主键冲突(新增时) | 捕获 IntegrityError,弹出"重复主键"提示 |
| 表单校验失败 | 在对话框内显示警告,不关闭对话框 |
- 运行
python create_database.py --excel new_file.xlsx --force - 重启主程序,表结构自动反射,无需修改任何代码
在 services/member_service.py 中扩展 get_all() 函数,加入 WHERE 子句参数,再在主窗口工具栏添加搜索框触发即可。
当数据量较大时,可在 get_all() 中加入 LIMIT / OFFSET 参数,并在主窗口底部添加翻页控件。
只需将整个项目目录复制到目标机器,在目标机器上运行:
pip install -r requirements.txt
python create_database.py # 指向共享 MySQL 服务器
python main.py多个客户端可同时连接同一个 MySQL 数据库,实现多人协作。
文档版本:1.0 · 最后更新:2026-03-30