多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Python+SQLite药物管理系统实战:数据库设计、Tkinter界面与打包部署

Python+SQLite药物管理系统实战:数据库设计、Tkinter界面与打包部署 简介这是一份基于Python的药物管理系统完整工程内置数据库文件与Web界面相关资源适合有Python基础、希望系统掌握Web应用和数据库整合开发的学习者也可直接用于药店、医院药房或课程设计中的药品信息管理场景。压缩包共51个文件约23.73MB主要包含HTML模板、XML配置、Python后端脚本和SQL数据库脚本分别承担页面渲染、项目配置、业务逻辑处理与数据存储初始化等职责。该资源已有108人学习下载。通过研读源码读者能够了解从数据库建模、表结构设计到后端路由分发、页面模板渲染的完整开发流程并掌握药品信息录入、查询、更新、删除等常见功能的实现思路同时工程目录结构清晰模板与配置可复用便于在此基础上扩展库存预警、分类统计等功能是巩固Python全栈开发能力的实用素材。1. 药物管理系统到底是什么一个PythonSQLite就能跑起来的实用项目很多人第一次接触到“基于python的药物管理系统含数据库文件.zip”这个压缩包时第一反应是“又一个课程设计”。但真正打开它、把数据库文件扔进SQLite浏览器里看一眼你会发现这个项目的价值远不止应付答辩。它本质上是Python SQLite组合下的一个完整业务系统药品信息录入、库存变动、过期预警、供应商管理全部落在一张张关系表里而不是躺在内存里的临时字典。这类项目能解决的实际问题很明确小药房、诊所或家庭药箱需要一个能离线运行、不依赖网络的“进销存台账”而Python和SQLite恰好用最轻量的方式把它实现出来。适合谁动手做如果你正在学Python但只会写爬虫和数据分析脚本还没有碰过“带界面的业务系统”这个项目是最好的过渡如果你是刚入职的初级开发需要快速理解数据库文件在应用里的角色它也比空谈理论直观得多。我见过不少人拿到这个压缩包后直接双击主脚本结果报错ModuleNotFoundError然后就开始怀疑源码有问题——其实问题往往出在环境或数据库路径上。接下来我会把这个系统的表结构、核心代码、运行流程以及最容易翻车的细节全部拆开讲你可以直接照着复现。2. 数据库文件是整个系统的地基SQLite表结构设计与初始化脚本2.1 为什么是SQLite而不是MySQL单文件、零配置、自带加密争议药物管理系统这类项目最常见的数据库选择有三种MySQL、SQLite和直接写JSON/CSV。MySQL功能强但你要先装服务端、建账号、配端口对单机运行的药房管理系统来说属于杀鸡用牛刀。JSON/CSV虽然简单但并发写入会损坏文件也没法做复杂的关联查询。SQLite恰好站在中间它把整个数据库放进一个.db文件里Python标准库自带sqlite3模块不需要额外安装任何数据库服务端。热词里有一条“sqlite数据库文件能否加密”这是很多新手拿到含数据库文件的压缩包后第一个想问的。SQLite本身默认不加密文件用文本编辑器打开能看到表结构和部分数据。如果你要保护药品采购价、供应商联系方式常见做法是使用SQLCipher这种编译版或者对敏感字段单独加密。但在这个课程设计级别的项目里我建议先不折腾加密把表结构设计对比换密码更重要。项目压缩包里那个“数据库文件”通常命名为drug.db或medicine.db。它里面一般会包含三到五张核心表。我基于最常见的从业方案给你拆一个标准的表结构设计。2.2 核心表结构药品表、库存表、供应商表怎么建才不冗余我一般会把一张宽表拆成三张窄表避免以后改供应商名称时要把药品表翻个底朝天。下面这份建表SQL是这类项目里最通用的模板-- 供应商表一个供应商对应多个药品 CREATE TABLE IF NOT EXISTS supplier ( supplier_id INTEGER PRIMARY KEY AUTOINCREMENT, supplier_name TEXT NOT NULL UNIQUE, contact TEXT, phone TEXT ); -- 药品信息表只存药品固有属性不存库存数量 CREATE TABLE IF NOT EXISTS drug ( drug_id INTEGER PRIMARY KEY AUTOINCREMENT, drug_code TEXT NOT NULL UNIQUE, drug_name TEXT NOT NULL, spec TEXT, unit TEXT DEFAULT 盒, purchase_price REAL, sale_price REAL, supplier_id INTEGER, expiry_date TEXT, -- 格式 YYYY-MM-DD FOREIGN KEY (supplier_id) REFERENCES supplier(supplier_id) ); -- 库存变动表每次入库、出库、报损都记录一条 CREATE TABLE IF NOT EXISTS stock_log ( log_id INTEGER PRIMARY KEY AUTOINCREMENT, drug_id INTEGER NOT NULL, change_qty INTEGER NOT NULL, -- 正数入库负数出库 log_time TEXT DEFAULT (datetime(now, localtime)), remark TEXT, FOREIGN KEY (drug_id) REFERENCES drug(drug_id) ); -- 当前库存表冗余设计查询速度快但要注意和stock_log保持一致 CREATE TABLE IF NOT EXISTS stock ( drug_id INTEGER PRIMARY KEY, current_qty INTEGER NOT NULL DEFAULT 0, lower_limit INTEGER NOT NULL DEFAULT 10, FOREIGN KEY (drug_id) REFERENCES drug(drug_id) );这段SQL里的逻辑说明supplier表独立出来是为了消除药品表里重复的供应商文本stock_log表是流水账每次库存变动都追加一条方便日后追溯“这批药去哪了”stock表存当前数量属于冗余字段否则每次查库存都要SUM日志表数据量大时会变慢。参数说明expiry_date用文本类型存ISO格式日期虽然不如DATE类型严谨但在SQLite里日期函数支持字符串比较WHERE expiry_date 2025-12-31是能直接跑的。lower_limit是最低库存预警线一般设为10或20具体看药房日均消耗量。2.3 用Python初始化数据库写一个setup_db.py一次性建表如果你不想打开SQLite命令行手敲SQL把上面的脚本塞进Python文件里执行更省事。我通常会在压缩包里提供一个init_db.py作用是“如果数据库不存在就创建表如果存在就跳过”。下面是核心写法import sqlite3 from pathlib import Path DB_PATH Path(__file__).parent / drug.db def init_database(): # connect 会自动创建文件但不会自动创建表 conn sqlite3.connect(DB_PATH) cursor conn.cursor() # 执行一段多语句SQL用三引号包起来 schema_sql CREATE TABLE IF NOT EXISTS supplier (...); CREATE TABLE IF NOT EXISTS drug (...); CREATE TABLE IF NOT EXISTS stock_log (...); CREATE TABLE IF NOT EXISTS stock (...); cursor.executescript(schema_sql) # 给常用查询字段建索引避免全表扫描 cursor.execute(CREATE INDEX IF NOT EXISTS idx_drug_name ON drug(drug_name)) cursor.execute(CREATE INDEX IF NOT EXISTS idx_stock_log_drug ON stock_log(drug_id)) conn.commit() conn.close() print(f数据库初始化完成{DB_PATH}) if __name__ __main__: init_database()这段代码的逻辑说明用executescript而不是execute是因为SQLite的Python驱动允许一次执行多条以分号分隔的语句但execute只会跑第一条。IF NOT EXISTS是幂等设计的关键重复运行不会报错适合新手反复调试。参数说明DB_PATH用Path(__file__).parent拼出脚本同目录下的路径这样双击运行或从命令行启动都不会因为“当前工作目录不对”而找不到数据库文件。这是最常见的坑之一后面避坑章节会再次提到。3. 用Python把增删改查做出来Tkinter界面与业务逻辑写法3.1 界面选型Tkinter、PyQt还是Web前端药物管理系统的从业方案大致分两派一派用Tkinter/PyQt做桌面窗口另一派用Flask/FastAPI做本地Web页面。热词里“python可视化界面”搜得很猛但具体到本项目压缩包里的界面多数是Tkinter——原因很简单Tkinter是Python标准库不需要额外装PyQt5那套体积庞大的依赖对新手和课程设计更友好。Tkinter的缺点是界面丑但你可以用ttk::Style换主题PyQt界面现代但打包exe体积会多出几十MB。如果你只需要药品列表、搜索框、表单弹窗和库存统计Tkinter完全够用。下面的代码我先给你一个能跑的药品列表骨架再讲按钮和表单怎么写。3.2 药品列表与搜索用Treeview展示数据库查询结果药品列表是系统的门面。用户最常干的动作是“输入药名搜索”和“双击查看详情”。下面是一个最小可运行的列表窗口代码import sqlite3 import tkinter as tk from tkinter import ttk, messagebox DB_PATH drug.db def search_drugs(keyword): conn sqlite3.connect(DB_PATH) cursor conn.cursor() # LIKE 模糊匹配药名或药品编码% 是通配符 if keyword: cursor.execute( SELECT drug_code, drug_name, spec, sale_price FROM drug WHERE drug_name LIKE ? OR drug_code LIKE ?, (f%{keyword}%, f%{keyword}%) ) else: cursor.execute( SELECT drug_code, drug_name, spec, sale_price FROM drug ) rows cursor.fetchall() conn.close() return rows def refresh_tree(tree, keyword): # 删除旧行再插入新行避免重复 for item in tree.get_children(): tree.delete(item) for row in search_drugs(keyword): tree.insert(, end, valuesrow) def on_search_click(tree, entry_var): refresh_tree(tree, entry_var.get()) def build_main_window(): root tk.Tk() root.title(药物管理系统 - 列表) root.geometry(600x400) top_frame ttk.Frame(root, padding8) top_frame.pack(fillx) # 搜索输入框和按钮 entry_var tk.StringVar() search_entry ttk.Entry(top_frame, textvariableentry_var, width30) search_entry.pack(sideleft, padx5) search_btn ttk.Button( top_frame, text搜索, commandlambda: on_search_click(tree, entry_var) ) search_btn.pack(sideleft) # 表格显示5列最后一列 stretch 为真 columns (drug_code, drug_name, spec, sale_price) tree ttk.Treeview(root, columnscolumns, showheadings) tree.heading(drug_code, text药品编码) tree.heading(drug_name, text药品名称) tree.heading(spec, text规格) tree.heading(sale_price, text零售价) tree.column(drug_code, width120) tree.column(drug_name, width150) tree.column(spec, width100) tree.column(sale_price, width80) tree.pack(fillboth, expandTrue) refresh_tree(tree) root.mainloop() if __name__ __main__: build_main_window()这段代码的逻辑说明search_drugs函数把SQL查询和界面分离这是所有可靠业务系统的共同习惯——界面只负责取数据和显示不直接拼SQL字符串。参数化查询用?占位符能防SQL注入千万不要用fSELECT ... WHERE drug_name LIKE {keyword}这种拼法。参数说明ttk.Treeview的columns必须是元组showheadings表示只显示列头不显示第一列树状图标。tree.column(drug_code, width120)控制列宽如果不设置中文列名可能被截断这是Tkinter的常见小毛病。3.3 新增与编辑药品表单弹窗里的数据校验搜索列表只是第一步药物管理系统真正核心的操作是新增、修改和删除。下面这个add_drug_window函数是一个典型的弹窗表单包含基本的数据校验def add_drug_window(root, refresh_callback): win tk.Toplevel(root) win.title(新增药品) win.geometry(360x280) fields [药品编码, 药品名称, 规格, 单位, 进价, 售价, 保质期] entries {} for i, label_text in enumerate(fields): ttk.Label(win, textlabel_text).grid(rowi, column0, padx8, pady4, stickye) entry ttk.Entry(win, width25) entry.grid(rowi, column1, padx8, pady4) entries[label_text] entry def save(): drug_code entries[药品编码].get().strip() drug_name entries[药品名称].get().strip() sale_price entries[售价].get().strip() # 必填字段校验 if not drug_code or not drug_name: messagebox.showerror(错误, 药品编码和药品名称不能为空) return # 价格必须是数字这里可以用 float() 捕获异常 try: sale_price_val float(sale_price) except ValueError: messagebox.showerror(错误, 售价必须是数字) return # 写入数据库 conn sqlite3.connect(DB_PATH) cursor conn.cursor() try: cursor.execute( INSERT INTO drug (drug_code, drug_name, spec, unit, purchase_price, sale_price, expiry_date) VALUES (?, ?, ?, ?, ?, ?, ?), ( drug_code, drug_name, entries[规格].get().strip(), entries[单位].get().strip(), float(entries[进价].get() or 0), sale_price_val, entries[保质期].get().strip() or None ) ) conn.commit() except sqlite3.IntegrityError as e: # 唯一键冲突药品编码重复 messagebox.showerror(错误, f药品编码重复或数据不合法{e}) conn.rollback() return finally: conn.close() messagebox.showinfo(成功, 药品已添加) win.destroy() refresh_callback() ttk.Button(win, text保存, commandsave).grid(rowlen(fields), column1, pady12) win.transient(root) # 让弹窗位于主窗口之上 win.grab_set() # 模式对话框必须先关掉弹窗才能操作主窗口逻辑说明win.transient(root)和win.grab_set()组合起来让这个新增窗口变成模态对话框防止用户开着弹窗又去点主窗口的按钮造成数据不同步。sqlite3.IntegrityError专门捕获唯一键冲突比如药品编码重复——表结构里drug_code是UNIQUE重复插入时数据库会抛这个异常代码里做了回滚不会把半截数据写进去。参数说明float(entries[进价].get() or 0)的意思是进价留空时按0处理or 0这个写法能优雅处理空字符串。如果用户输入“12.5元”这种带单位的文本float()会直接抛ValueError所以更严谨的做法是用正则re.match(r^\d(\.\d)?$, text)先判断格式。3.4 库存出库入库事务与数量校验药物管理系统不能只有药品档案还得有库存流水。出库时要先查当前库存够不够入库时要把数量累加。这两步必须放在同一个数据库事务里否则会出现“扣了库存但没写流水”或者“写了流水但库存没变”的问题。下面这个change_stock函数就是标准写法def change_stock(drug_id, delta_qty, remark): conn sqlite3.connect(DB_PATH) cursor conn.cursor() try: # 开启事务sqlite3 默认在 execute DML 前自动开启 # 第一步锁定药品行防止并发修改 cursor.execute(SELECT current_qty FROM stock WHERE drug_id ?, (drug_id,)) row cursor.fetchone() if row is None: raise ValueError(f药品ID {drug_id} 没有库存记录) current_qty row[0] new_qty current_qty delta_qty # 出库时校验负数库存 if new_qty 0: raise ValueError(f库存不足当前{current_qty}试图出库{-delta_qty}) # 第二步更新库存表 cursor.execute( UPDATE stock SET current_qty ? WHERE drug_id ?, (new_qty, drug_id) ) # 第三步写入流水表 cursor.execute( INSERT INTO stock_log (drug_id, change_qty, remark) VALUES (?, ?, ?), (drug_id, delta_qty, remark) ) conn.commit() return new_qty except ValueError as ve: conn.rollback() raise ve except sqlite3.Error as se: conn.rollback() raise se finally: conn.close()这段代码的关键点sqlite3模块在execute执行增删改时默认会开启事务直到你调用commit()才真正落盘。如果中途抛异常rollback()会把前面的UPDATE和INSERT全部撤销这就是原子性。注意这里我没有用BEGIN手动控制是因为Python的sqlite3在isolation_level默认情况下够用但如果你用了conn.isolation_level None就必须手动写BEGIN和COMMIT。参数说明delta_qty正数表示入库负数表示出库。调用方可以这样设计——入库按钮传正值出库按钮传负值并在点击前弹一个二次确认框。remark字段记录“入库单号”“报损”“退货”等文字方便以后查账。4. 从代码到可运行程序环境配置、打包exe与数据库文件分发4.1 环境搭建Python安装到pycharm配置一步都不能省热词里“python安装教程”“pycharm配置python环境”出现频率很高说明很多网友卡在这一步。拿到压缩包后第一件事不是去看代码而是确认解释器。这个项目依赖tkinter和sqlite3它们都是Python标准库所以只要你的Python是官方安装包不需要额外pip install任何东西。但如果你的系统是精简版Linux或者用的是python3-tk没装的发行版运行时会报ModuleNotFoundError: No module named tkinter。我一般建议这么装环境Windows用户去官网下载Python 3.10或3.12安装包勾选“Add Python to PATH”Linux用户用sudo apt install python3-tk补上Tkinter。然后在PyCharm里新建项目时 interpreter选择python.exe而不是某个虚拟环境的空壳。注意一个细节PyCharm默认给新项目创建venv但venv里的Python同样自带tkinter所以不用额外装包。如果你用的是VSCode可能需要额外配置终端环境变量。最常见的报错是python was not found; run without arguments to install from the microsoft store这是Windows没把Python加入PATH的表现解决方法是重新运行安装包并勾选“Add Python to PATH”或者去系统设置手动把Python根目录和Scripts目录加进PATH。4.2 用PyInstaller把脚本打包成exe参数与常见坑做完系统后你不可能让每个药房工作人员都装Python所以要把项目打包成独立的exe。PyInstaller是这条路最常用的工具命令非常简单pip install pyinstaller pyinstaller -D main.py --name DrugManageSystem --noconsole参数说明-D表示生成文件夹模式包含exe和一堆dll这个模式启动速度快适合调试如果你想要一个单文件exe用-F但启动时会先解压到临时目录第一次打开慢而且杀毒软件误报概率高。--noconsole是去掉黑窗口因为Tkinter程序不需要控制台保留它反而会在系统托盘露出一个难看的大黑框。逻辑说明PyInstaller默认会把import到的模块全部打包进去。Tkinter作为标准库会被自动收集但有个坑——如果程序运行时用到了外部的drug.dbPyInstaller不会帮你把数据库文件放进exe包里你需要手动在打包命令后面加--add-data drug.db;.Linux和macOS用冒号。更稳妥的做法是把数据库文件放在exe同目录下运行时用sys.executable所在目录来定位。4.3 数据库文件的分发策略首次启动自动初始化很多人拿到“含数据库文件”的压缩包以为只要把.db文件发给用户就行。实际上一旦用户改乱了数据你没法远程帮他恢复。我更喜欢在代码里做“首次启动检测”import os import sys from pathlib import Path def get_db_path(): # 打包成exe后__file__ 指向临时解压目录要用 sys.executable 的目录 if getattr(sys, frozen, False): base_dir Path(sys.executable).parent else: base_dir Path(__file__).parent return base_dir / drug.db DB_PATH get_db_path() def ensure_database(): if not DB_PATH.exists(): # 从备份文件复制一份干净数据库 import shutil backup_path Path(__file__).parent / drug_blank.db if backup_path.exists(): shutil.copy(backup_path, DB_PATH) else: init_database_from_scratch()这里的关键是sys.frozen判断当程序被PyInstaller打包后__file__会指向PyInstaller的临时解压目录_MEIxxxx那个目录在你关闭程序后就被删了数据库写在里面等于白写。所以必须用sys.executable拿到exe的真实路径再取其父目录作为数据目录。这段代码是这类软件“后悔药”的本质——用户乱删数据库后重启程序还能生成一个新的不会整个系统崩溃。5. 落地避坑指南5个让新手翻车的数据库与编码问题5.1 数据库文件被占用导致“database is locked”现象程序运行一段时间后点击操作报错sqlite3.OperationalError: database is locked。原因上一轮操作没有关闭connection或者两个窗口同时写同一个数据库。SQLite对写操作加锁如果某个连接没commit也没close锁就一直持有。解决在所有conn sqlite3.connect(...)之后务必用try/finally或with语句确保conn.close()执行。另外不要把连接作为全局变量一直开着每次操作短连、用完即走SQLite对这种场景更友好。如果确实需要并发可以在connect时加timeout10参数让等待锁的时间更长。5.2 中文乱码不是数据库问题是终端编码问题现象数据能存进去但命令行打印出来全是Î÷Ò©或者Tkinter界面上显示方框。原因Windows控制台默认GBK编码而SQLite存入的是UTF-8Tkinter本身用UTF-8但PyCharm运行窗口的编码设置不对。解决在Python文件第一行加# -*- coding: utf-8 -*-Python3默认已是UTF-8这一步实际多余但提醒自己最关键的是在连接时指定conn.text_factory str让SQLite直接返回Python字符串而不是bytes。界面显示方框通常是字体问题把Tkinter默认字体改成微软雅黑或SimHei即可import tkinter as tk root tk.Tk() root.option_add(*Font, (Microsoft YaHei, 10))5.3 日期筛选失效字符串格式不统一现象过期预警查不出数据明明有一批药下个月过期但WHERE expiry_date 2025-12-31返回空。原因录入时有人用2025/12/31有人用2025年12月31日还有人漏了前导零写成2025-5-1。SQLite的字符串比较是按字典序的2025-5-1排在2025-12-31前面因为字符5小于1不对字典序比较中2025-5与2025-1比较5比1大所以2025-5-1实际排后面但2025-1-5会被误判。解决强制统一格式。在录入界面用tkcalendar控件选择日期或者用正则校验^\d{4}-\d{2}-\d{2}$。我一般会在save()里加一行import re date_str entries[保质期].get().strip() if date_str and not re.match(r^\d{4}-\d{2}-\d{2}$, date_str): messagebox.showerror(错误, 日期格式应为 YYYY-MM-DD) return5.4 打包exe后找不到数据库或图标现象源码运行一切正常pyinstaller -F main.py打包出的exe双击就闪退或者报“no such table: drug”。原因单文件模式的临时目录和程序实际运行目录不是同一个。后面“坑5.3”那节提到过数据库写到临时目录里程序退出就没了另外图标文件或配置文件在打包时没被包含进去。解决用--add-data把所有静态资源加进包然后在代码里用sys._MEIPASS读取只读资源图标、默认数据库用sys.executable目录读写用户数据。示例def resource_path(relative_path): try: base_path sys._MEIPASS except AttributeError: base_path Path(__file__).parent return os.path.join(base_path, relative_path)5.5 删除药品时外键约束报错现象删除某个药品时报错FOREIGN KEY constraint failed明明这个药品没有库存记录。原因药品表里有stock_log流水数据关联了它的drug_id而SQLite默认外键是开启的在Python sqlite3中默认关闭但某些打包工具或手工PRAGMA会开启。如果开着外键删除有流水的药品会直接失败。解决业务上应该禁止删除有流水记录的药品改成“停用”逻辑。最优雅的做法是在药品表加一个is_active字段0表示停用查询时默认WHERE is_active 1。如果非要硬删可以用外键级联删除建表时写ON DELETE CASCADE但这会连带删除所有历史流水药房账目就没了不推荐。6. 给药房场景加分的进阶技巧过期预警、库存统计与数据备份做到这里系统已经能日常使用了但离“让使用者真正依赖”还差几个功能点。第一个是过期预警不只是提示哪些药过期而是提前90天开始提醒。实现上可以用一个简单的查询SELECT drug_name, expiry_date, CAST(julianday(expiry_date) - julianday(now) AS INTEGER) AS days_left FROM drug WHERE expiry_date IS NOT NULL AND days_left BETWEEN 0 AND 90 ORDER BY days_left;julianday是SQLite内置日期函数能算出距离天数。把这个查询封装成get_expiring_drugs()每次启动程序时弹一个窗口列出近90天到期药品。注意expiry_date必须是YYYY-MM-DD格式否则julianday解析失败返回NULL。第二个是库存统计报表。药房老板最关心的是“哪些药快卖完了”“哪些药占资金多”。你可以在Tkinter里画一个简单的柱状图但更省事的是生成CSV导出到Excelimport csv def export_stock_report(): rows get_stock_joined_drug_info() # 联合查询 stock drug with open(stock_report.csv, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([药品名称, 规格, 当前库存, 最低库存, 零售价, 库存金额]) for r in rows: writer.writerow([r[1], r[2], r[3], r[4], r[5], r[3]*r[5]])utf-8-sig编码是Excel能直接打开中文CSV的关键不加-sig的话Excel默认按ANSI解析中文全变乱码。这个坑我踩过好多次每次导出报表前都提醒自己。第三个是数据备份。SQLite的备份不是简单复制文件因为可能有人正在写入。用Python内置的sqlite3备份API最安全import sqlite3 src sqlite3.connect(drug.db) dst sqlite3.connect(backup_20250601.db) src.backup(dst) dst.close() src.close()这个操作会生成一个一致性快照不会把写了一半的数据拷过来。建议每天关闭程序时自动执行一次备份文件名加日期保留最近30份。我自己的习惯是再写一个restore_backup.py把备份文件拖到程序目录改名为drug.db就能恢复这是用户最需要的“后悔药”。作为做过好几个类似系统的人我想说一句药物管理系统这类项目的核心根本不在于界面多华丽而在于数据库文件的可靠性和操作的可追溯性。哪怕你用最丑的Tkinter只要做到“每一个扣减都有流水每一次过期都能预警”这个系统就比市面上很多形式主义的管理软件更有价值。希望这些落地细节和踩坑记录能帮你把那个zip包真正变成自己能维护、能交付的实用工具。本文还有配套的精品资源点击获取
返回列表