ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于Python+Tkinter+SQLite的船员信息管理系统设计与实现

基于Python+Tkinter+SQLite的船员信息管理系统设计与实现 最近在整理自己的数据库课程设计项目库翻到一套以前实现的基于Python的船员信息管理系统包含了完整程序、数据库设计和GUI界面正好有朋友问Tkinter加SQLite这种组合怎么落地成完整项目就拿出来详细聊聊。这个系统不是那种只跑一次的控制台脚本而是真正能用的桌面应用船员资料录入、证书到期提醒、培训记录维护、模糊查询和数据导出都做了拿它当课程设计、毕业设计参考或者当作自己学完Python基础后的第一个综合练手项目都很合适。我会把需求、表结构、界面布局和核心代码逻辑都拆开讲最后再放几个我踩过的坑希望能帮大家少走点弯路。1. 项目整体设计与技术选型解析1.1 需求分析船员信息管理到底要管什么很多人在做信息管理系统时容易犯一个错误就是一上来就写代码结果做着做着发现字段不够用、表结构混乱、功能互相打架。我建议第一步先把需求捋清楚。船员信息管理系统的核心用户是船舶公司的人力部门、船队调度人员或者海事学校的管理员他们要管理的数据至少包含以下几类船员的基本身份信息、证书信息、培训记录、任职状态以及登录系统的账号。船员基本身份信息就是姓名、性别、出生日期、国籍、证件号码、联系电话、家庭住址、所属部门、所在船舶、入职日期、当前状态。证书信息是航海行业特别强调的部分因为船员上船需要各类证书比如船员服务簿、海员证、适任证书、培训合格证每张证书都有编号、发证机构、发证日期和有效期。证书过期是非常严重的事故隐患所以系统必须能根据有效期提醒管理人员。培训记录则包括培训项目、培训机构、起止日期和考核结果这部分有利于分析船员的职业成长轨迹。最后还需要一个用户账号表做简单的登录和权限控制。功能层面至少要有船员档案的新增、修改、删除、查询查询可以是精确查询也能模糊查询证书信息要能按船员关联展示并做30天内到期提醒数据要能导出成Excel或CSV方便人事做备份和汇报。从大学课设或者个人项目的角度做到这些已经比“能跑就行”的作业高出一个档次了。如果还想增加亮点可以加入统计图表比如统计各船舶人数、各年龄段分布但核心还是把增删改查和证书管理做扎实。1.2 技术选型Python Tkinter SQLite 为什么这么搭技术选型往往是项目开始前最纠结的事情。我当时选Python有三个理由一是Python语法简单适合快速开发写界面、操作数据库的生态都很成熟二是这个项目要给别人参考Python比Java或C#更容易看懂三是运行环境好搭装个Python解释器就能跑不用折腾编译器和复杂依赖。GUI框架我选了Tkinter而不是PyQt或者PySide。Tkinter是Python标准库自带的不需要额外安装pip安装完Python就能直接使用这对新手非常友好。PyQt虽然界面更现代、组件更丰富但安装包大、学习曲线陡而且涉及商业授权问题课程设计和一般业务系统用Tkinter完全够用。当然Tkinter有一个明显缺点是控件样式偏老旧走的是“经典桌面软件”风格不太适合追求炫酷界面的项目。如果你就是想把界面做得好看一点可以在Tkinter基础上结合ttk主题或者改用PyQt后面我也会讲到哪些地方可以替换。数据库我选SQLite核心原因是零配置、单文件、免安装。SQLite把所有数据存在一个.db文件里拷贝这个文件就等于备份了整个数据库特别适合个人项目、课程设计和中小型桌面应用。如果换成MySQL代码层面需要改的就是连接方式还有SQL语法中个别差异但表结构设计思路完全通用。说句实在话很多单位的船舶管理系统本身也不是高并发系统SQLite性能完全够用。为了保证查询结果可读我在连接时设置了conn.row_factory sqlite3.Row这样拿到的每一行数据可以通过字段名访问不用整天记着“第0列是姓名、第1列是性别”代码可读性一下子高很多。2. 数据库设计与落地从表结构到初始化脚本2.1 四张表怎么设计船员、证书、培训、用户数据库设计是信息管理系统的地基表结构没设计好后面写业务逻辑时处处难受。我把船员信息管理系统的数据库拆成了四张表crew_info、crew_certificate、crew_training、user_account。crew_info表存储船员基本信息。id字段是自增主键name字段不能为空id_card字段加上UNIQUE约束防止重复录入同一个证件号码。为什么证件号码要加唯一约束我遇到过实际案例有人录数据时同一船员录了两遍导致统计人数虚高、证书关联错乱加了这个约束后系统层面就能拦截大部分重复数据。status字段我用文本类型取值限定为“在职”“休假”“离职”这几种代码里再用下拉框控制避免乱填。crew_certificate表是证书信息表和crew_info表是一对多关系。一个船员有多张证书所以通过crew_id外键关联主表。证书编号cert_no也加了UNIQUE约束因为证书编号在发证机构那里就是唯一标识但要注意不同机构可能有不同的编号格式如果你发现录入时经常提示重复可以把唯一约束改成联合唯一比如(crew_id, cert_no)。该表的expire_date字段是整个系统的核心后面做证书到期提醒全靠它。crew_training表记录培训经历同样是船员一对多关系。各字段包括培训名称、培训机构、开始日期、结束日期、考核结果。字段的类型尽量都用TEXT因为日期在SQLite里本质上也是字符串用ISO格式“YYYY-MM-DD”存储不仅能直接排序也方便datetime模块解析。最后一张是user_account表存登录账号、密码哈希值和角色。密码一定不要存明文虽然课设里很多时候老师不强制但你自己写项目时要养成习惯可以用hashlib里的sha256或者加盐哈希处理。权限上我分了admin和operator两个角色admin可以删除数据operator只能增改查。2.2 数据库初始化与连接管理技巧数据库的初始化我单独放在db_manager.py里所有建表语句集中在一起用一个init_db()函数统一执行。这样做的好处是启动程序时只要调用一次就能保证表结构存在不会出现“程序跑着跑着报no such table”的问题。import sqlite3 import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) DB_PATH os.path.join(BASE_DIR, crew_system.db) def get_conn(): conn sqlite3.connect(DB_PATH) conn.row_factory sqlite3.Row return conn def init_db(): conn get_conn() cur conn.cursor() cur.execute( CREATE TABLE IF NOT EXISTS crew_info ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, gender TEXT, birth_date TEXT, nationality TEXT, id_card TEXT UNIQUE, department TEXT, ship_name TEXT, phone TEXT, address TEXT, hire_date TEXT, status TEXT DEFAULT 在职 ) ) cur.execute( CREATE TABLE IF NOT EXISTS crew_certificate ( id INTEGER PRIMARY KEY AUTOINCREMENT, crew_id INTEGER NOT NULL, cert_name TEXT, cert_no TEXT UNIQUE, issuer TEXT, issue_date TEXT, expire_date TEXT, remark TEXT, FOREIGN KEY (crew_id) REFERENCES crew_info(id) ON DELETE CASCADE ) ) cur.execute( CREATE TABLE IF NOT EXISTS crew_training ( id INTEGER PRIMARY KEY AUTOINCREMENT, crew_id INTEGER NOT NULL, training_name TEXT, organization TEXT, start_date TEXT, end_date TEXT, result TEXT, remark TEXT, FOREIGN KEY (crew_id) REFERENCES crew_info(id) ON DELETE CASCADE ) ) cur.execute( CREATE TABLE IF NOT EXISTS user_account ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT DEFAULT operator, created_time TEXT ) ) conn.commit() conn.close()这里有个小细节数据库文件路径不要用相对路径crew_system.db因为程序启动目录可能不是项目根目录我吃过这个亏双击运行脚本时工作目录在桌面结果数据库文件跑到了桌面第二天打开项目数据“丢了”。用os.path.dirname(os.path.abspath(__file__))定位文件所在目录再拼上数据库文件名这样不管从哪里启动数据库文件都会稳定在项目目录下。连接管理我采用“每次操作单独获取连接操作完立即关闭”的方式。你可能会觉得频繁开关连接效率低但对SQLite这种嵌入式数据库来说这个开销可以忽略不计而且能避免多线程环境下连接共用导致的SQLite objects created in a thread can only be used in that same thread错误。GUI程序的事件处理基本都在主线程但保险起见我禁止了跨线程共享连接。关于外键SQLite默认没有开启外键约束如果你想依赖数据库自身来保证引用完整性需要在每次连接后执行一句PRAGMA foreign_keys ON;。不过我在代码里主要靠应用层控制删除船员时先删除证书和培训记录所以即使不开启逻辑上也不会产生孤儿数据。如果你希望数据库层面更严谨可以在get_conn()里加上这条PRAGMA。3. GUI界面设计与核心交互实现3.1 主窗口布局怎么排才顺手GUI界面设计有一句话很重要用户打开软件第一眼就知道怎么用而不是先读使用说明书。我的主窗口布局采用经典的三段式结构顶部是查询区域中间是数据表格底部是表单和操作按钮。整体窗口设置为1000x650太小会导致表格和表单挤在一起太大在普通笔记本上又超出屏幕范围。查询区域放了两个输入框一个是普通关键词输入框一个是状态筛选下拉框。关键词可以匹配姓名、证件号、手机号、部门这样调度人员看到一个名字就能直接搜出来不用纠结到底按哪个字段查。下面用一个ttk.Treeview控件展示船员列表它天生就适合做这种二维表格展示。Treeview我设置了七列ID、姓名、性别、证件号、部门、所在船舶、状态列标题可以直接点击排序其实需要额外绑定命令我没有做但列宽调整和横向滚动条是必须的不然数据一多就看不全。底部表单用ttk.LabelFrame包起来视觉上有明显的分组感。左边是船员基本信息字段右边是证书列表和培训记录的简单查看区。点击表格中的某一行时程序会把该行的数据自动回填到左边的输入框这样修改时就不用重新输入所有内容。操作按钮放在表单下方分别是“新增”“保存修改”“删除”“清空”“导出CSV”“证书到期提醒”。按钮不要放一大堆够用就行多余的功能可以放进菜单栏。整个界面用grid布局管理器控制位置稍微有点繁琐但很直观。我的经验是先把大区域用pack分成上中下三个frame每个frame内部再用grid排小控件这样不至于乱套。如果所有控件都塞在同一个容器里后面想调整间距会非常痛苦。3.2 增删改查与证书到期提醒代码详解我挑几个核心操作的代码出来讲一下。首先是加载数据到表格的功能。这段代码每次切换查询条件后都会调用作用是执行SQL查询、清空Treeview原有数据、把查询结果逐行插入表格。def load_data(self): keyword self.keyword_var.get().strip() status self.status_var.get() rows db_manager.search_crew(keyword, status) for row in self.tree.get_children(): self.tree.delete(row) for row in rows: self.tree.insert(, end, values( row[id], row[name], row[gender], row[id_card], row[department], row[ship_name], row[status] ))这里要注意插入Treeview时传入的values必须是元组顺序一定要和定义的列顺序一致。我在查询函数里返回的是sqlite3.Row对象访问字段时用row[name]清晰明了。如果是自己拼SQL很容易在这里踩坑比如字段名写错不会立即报错而是显示一行空值真正查问题时要花不少时间。然后看新增和修改。新增和修改的逻辑本质上是同一套表单数据处理只是最终执行的SQL不同。我写了一个save_crew方法通过判断当前是否有选中行来决定执行INSERT还是UPDATE。def save_crew(self): data { name: self.name_var.get().strip(), gender: self.gender_var.get(), birth_date: self.birth_var.get().strip(), nationality: self.nation_var.get().strip(), id_card: self.idcard_var.get().strip(), department: self.dept_var.get().strip(), ship_name: self.ship_var.get().strip(), phone: self.phone_var.get().strip(), address: self.addr_var.get().strip(), hire_date: self.hire_var.get().strip(), status: self.status_form_var.get(), } if not data[name] or not data[id_card]: messagebox.showwarning(校验失败, 姓名和证件号码不能为空) return if not self.validate_date(data[birth_date]): messagebox.showwarning(校验失败, 出生日期格式应为 YYYY-MM-DD) return selected self.tree.selection() if selected: crew_id int(self.tree.item(selected[0], values)[0]) db_manager.update_crew(crew_id, data) messagebox.showinfo(提示, 船员信息已更新) else: db_manager.add_crew(data) messagebox.showinfo(提示, 船员信息已添加) self.load_data() self.clear_form()讲到这个功能我想重点说数据校验。很多新手做增删改查时只做“能写入”不做“写得对”。日期字段如果不校验用户输入“2024-13-99”也能进库后面证书到期提醒和统计会全部乱套。我写了一个简单的validate_date函数用datetime.strptime去解析解析成功才算通过。这行代码看起来不起眼却是项目从“玩具”走向“可用”的分水岭。另外证件号码重复的问题除了数据库唯一约束在保存前也可以主动查一次库给用户一个友好提示而不是等数据库抛异常。删除功能也必须谨慎。用户一旦点击删除我会先弹出messagebox.askyesno确认对话框然后通过数据库外键级联删除关联证书和培训记录或者手动先删子表再删主表。def delete_crew(self): selected self.tree.selection() if not selected: messagebox.showwarning(提示, 请先选择要删除的船员) return if not messagebox.askyesno(确认, 确定要删除该船员及其所有证书、培训记录吗): return crew_id int(self.tree.item(selected[0], values)[0]) db_manager.delete_crew(crew_id) self.load_data() self.clear_form()证书到期提醒是船员管理系统最有行业特色的功能。我在主界面的查询区域专门放了一个“90天内到期”的筛选按钮点击后遍历所有船员的证书判断距离当前日期是否少于90天并在结果表格里把所有快到期的船员列出来。这个逻辑用SQL也可以实现比如把expire_date转换成日期后和当前日期比较但在SQLite里日期函数在不同版本中行为有差异我干脆在Python里做保证逻辑清晰。实际效果是人事每天早上打开系统点一下这个按钮就能看到哪些船员证书即将过期提前安排培训或换证。def show_expiring_certs(self, days90): today datetime.now().date() rows db_manager.get_all_crews_with_certificates() expiring [] for row in rows: expire row[expire_date] if expire: try: expire_date datetime.strptime(expire, %Y-%m-%d).date() if 0 (expire_date - today).days days: expiring.append(row) except ValueError: continue self.refresh_tree_with_data(expiring)4. 完整项目结构与运行说明4.1 项目文件划分和环境准备很多课程设计项目喜欢把几百行代码全部塞进一个文件说实话可读性很差也不利于分工。我建议至少按职责拆成四个文件。main.py负责程序入口创建主窗口并进入消息循环db_manager.py负责数据库连接、初始化、所有SQL操作ui_main.py负责窗口、控件和交互逻辑utils.py放日期校验、导出CSV等通用函数。如果后续要增加新功能比如添加一个“培训记录管理”窗口只需要在ui_main.py里加类再在db_manager.py里加对应数据函数改动范围很清晰。环境准备非常简单。Python 3.8及以上版本都可以Tkinter和SQLite都是标准库不需要额外pip install任何第三方包。唯一要注意的是Windows系统自带的Python可能没有Tkinter尤其是某些精简版Python安装时务必勾选“tcl/tk and IDLE”选项。如果导入tkinter时报ModuleNotFoundError: No module named tkinter最简单的解决办法是去Python官网重新安装安装向导里把Tcl/Tk组件勾上。运行命令也很直接python main.py第一次运行时程序会自动创建crew_system.db数据库文件同时自动建好四张表。如果你希望系统自带初始测试数据可以在init_db()后面加一个判断如果crew_info表为空就插入几条示例船员记录这样演示界面时不会空荡荡老师检查项目时观感也好很多。4.2 核心代码逐段拆解我在这里把db_manager.py里几个关键函数拆开讲一下它们是整个系统数据层的核心。查询函数search_crew是我最推荐大家模仿的写法。它没有用简单的字符串拼接而是使用了参数化查询。参数化查询是我要强调的重中之重。def search_crew(keywordNone, statusNone): conn get_conn() cur conn.cursor() sql SELECT * FROM crew_info WHERE 11 params [] if keyword: sql AND (name LIKE ? OR id_card LIKE ? OR phone LIKE ? OR department LIKE ?) like f%{keyword}% params.extend([like, like, like, like]) if status: sql AND status ? params.append(status) cur.execute(sql, params) rows cur.fetchall() conn.close() return rows这里的WHERE 11是一个很实用的小技巧不是性能问题而是方便后面动态拼接查询条件。即使没有任何筛选条件SQL语法也是成立的每多一个条件只需要在SQL字符串后追加一段并往params列表里添加对应参数。?占位符就是参数化的标志由sqlite3库安全转义能有效防止SQL注入。我在演示项目时见过有人直接写cur.execute(fSELECT * FROM crew_info WHERE name LIKE %{keyword}%)这在小课设里可能不暴露问题但一旦数据量上来或者系统上线教训会非常惨痛。新增船员的add_crew函数使用cursor.lastrowid获取刚插入数据的自增ID。这个ID非常重要因为紧接着要添加证书、培训记录时都需要这个crew_id作为外键。代码如下def add_crew(data): conn get_conn() cur conn.cursor() cur.execute( INSERT INTO crew_info ( name, gender, birth_date, nationality, id_card, department, ship_name, phone, address, hire_date, status ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) , ( data[name], data[gender], data[birth_date], data[nationality], data[id_card], data[department], data[ship_name], data[phone], data[address], data[hire_date], data[status] )) conn.commit() new_id cur.lastrowid conn.close() return new_id有一点容易忽略INSERT、UPDATE、DELETE操作之后必须执行conn.commit()否则数据只在内存里生效程序一退出就全丢了。SELECT操作不需要提交但为了代码统一我通常在写操作的函数结尾调用commit。close其实也会隐式提交未提交的事务但显式commit更稳妥也更符合阅读习惯。导出CSV的功能我放在utils.py里。很多同学导出CSV后用Excel打开发现中文乱码核心原因是编码问题。常规open(file.csv, w, encodingutf-8)生成的是UTF-8编码Excel在中国区默认用GBK读取UTF-8文件自然乱成一团。解决办法是写入时用utf-8-sig编码utf-8-sig会在文件开头写入BOM头Excel就能正确识别。下面是一个导出函数示例import csv def export_crew_to_csv(rows, file_path): with open(file_path, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([ID, 姓名, 性别, 出生日期, 证件号, 部门, 船舶, 电话, 状态]) for row in rows: writer.writerow([ row[id], row[name], row[gender], row[birth_date], row[id_card], row[department], row[ship_name], row[phone], row[status] ])4.3 运行演示与数据验证流程程序跑起来后我建议你按下面的流程走一遍既能验证功能完整也能提前发现界面和数据的对接问题。第一步先点“新增”故意什么都不填就点保存系统应该弹出“姓名和证件号码不能为空”的提示这证明校验逻辑在工作。第二步填写一条完整的测试数据比如“张三 / 男 / 1990-05-12 / 中国 / 身份证号XXXX / 轮机部 / 远航号 / 13800138000 / 上海市XX区 / 2018-07-01 / 在职”点保存后表格中应该立刻出现这条记录这证明数据库写入和刷新逻辑正常。第三步修改刚才这条记录把状态从“在职”改成“休假”点保存后表格自动刷新状态列同步变化。第四步在查询框里输入“张”能查到张三再输入身份证中的几个数字也能查到说明模糊查询生效。第五步点“证书到期提醒”检查结果集。最后删除测试数据确认删除前弹窗、删除后列表清空、关联数据也一起删除。我用这套流程测了很多次最常见的问题集中在第二步就是保存后表格不刷新。原因是保存函数里漏调了self.load_data()。很多新手喜欢在另一个窗口里保存数据保存后关闭子窗口父窗口的列表却没有重查一遍数据看起来就像“丢了”。我的方案是尽量让所有操作都在主窗口内完成保存成功后直接调用load_data()彻底绕开窗口通信的复杂度。5. 常见问题与排查技巧实录5.1 表不存在、数据查不到、SQL执行报错sqlite3.OperationalError: no such table: crew_info是我见过频率最高的错误。出现这个错误的原因基本只有一个程序启动时没有调用init_db()。解决办法就是在创建主窗口之前先执行数据库初始化。如果你已经调用了init_db()但还是报错检查数据库文件路径是不是正确直接在命令行打印DB_PATH看看。我遇到过一种情况项目文件夹里明明有crew_system.db但程序又新建了一个空数据库原因就是上面讲的相对路径问题。另外如果你把代码给别的电脑跑但只拷贝了.py文件而没有把.db文件一起拷过去也不用慌只要init_db()执行了程序会自动新建数据库。要注意的是如果旧数据库表结构已经变化比如新加了一个字段CREATE TABLE IF NOT EXISTS不会自动加列。这时候最简单的做法是删除旧的.db文件重新初始化但如果已有正式数据就要编写ALTER TABLE迁移语句。课设阶段重删数据库可以接受但心里得有这根弦。5.2 界面相关中文乱码、Treeview列不显示、窗口卡死Tkinter程序本身用的是Unicode正常不会出现中文乱码但有一种情况很常见Windows系统把Python源文件默认按GBK读取而你的代码保存成了UTF-8一运行就报SyntaxError: Non-UTF-8 code starting with...。解决办法有两个要么在文件第一行加注释声明编码要么干脆把所有源文件统一保存为UTF-8。现代IDE默认都是UTF-8所以这个问题现在少了很多但如果你用记事本改过代码就要格外小心。Treeview列不显示完整信息也是一个高频问题。它有两个原因一是列宽设置太小数据显示成了省略号二是列没有设置stretch属性窗口放大后多余空间不知道给谁。我的经验是ID列设60像素姓名列100像素身份证号列180像素部门列120像素船舶列120像素状态列80像素这样在1000像素宽的窗口里刚好放下。真名、地址这类长文本不建议直接放进表格里鼠标选中行后在下方表单里看即可。窗口卡死这个问题我要单独说。Tkinter是单线程驱动的如果某个按钮的响应函数里做了耗时操作比如导出几千条CSV或者大量数据库查询界面就会像死掉一样。我在第一次写导出功能时就踩了这个坑点击导出后窗口白屏好几秒用户肯定会以为程序崩了。解决办法是给耗时操作单独开一个线程或者至少在处理前把按钮设置为disabled状态并更新界面文字处理完成再恢复。如果只是课设数据量数据量不大暂时不开线程问题不大但要在代码里留个注释提醒自己这里可能会卡。5.3 数据层面的坑忘记提交、类型错误、重复数据保存后数据老是不见先查commit。我在代码注释里反复写了写操作要commit但实际操作中还是会漏。如果发现自己改了数据但重新运行程序后又变回原样八成就是漏掉了这一句。还有一个相关问题是SQL语句执行成功但不报错这也容易让人迷惑其实数据就缓存在数据库连接里没有落盘。类型错误集中在两个地方一个是Entry.get()返回的是字符串而表格里显示的ID在对比时需要转成整数我代码里已经写了int(self.tree.item(selected[0], values)[0])另一个是日期字段如果存库时格式不统一比如有的存了“2024/01/01”有的存了“2024-01-01”到期提醒就会解析失败。我建议所有日期输入都用YYYY-MM-DD格式界面上标注清楚校验函数也只认这个格式。最后说下重复数据。虽然证件号码有唯一约束但我还是建议代码里在保存前增加一次检查。因为数据库的UNIQUE约束只有在插入或更新时才会触发如果用户把张三的证件号改成李四的已有证件号程序会弹出很生硬的数据库异常普通用户根本看不懂。我写了一个辅助函数在保存前根据ID排除自身后查询是否已有相同证件号有就直接弹窗提示体验会好很多。这个系统我前前后后改了三版最大的感触是越是看起来简单的信息管理系统越要把数据校验和异常处理做在前面。证书日期格式错误、重复身份证号、忘记commit导致数据丢失这些都是上课时不太会遇到但实际开发中一定会碰到的问题。如果你也打算拿这个项目当课设或毕设基础我建议不要只满足于“能跑”试着把登录功能真正接到用户表上或者给证书模块加一个新建独立管理窗口再在代码里补一个操作日志表这样一下子就能和普通的增删改查作业拉开差距。
返回列表