基于Python的个人记账本系统逆向分析与MVC架构重构
软件开发与创新课程设计第一次作业
一、项目来源
这个项目是一个基于Python(Tkinter+SQLite3)的简易个人记账本桌面应用,最初的版本参考了同学曾经做过的和GitHub上的开源记账本项目:https://github.com/data-coach/personal-budget-tracker-python 以及网上一些其他的记账本项目,这里就不一一列举了。
这个开源项目实现了基础的“收入/支出”记录添加与记录列表展示的功能。
二、运行环境与原始运行结果截图
在开始逆向分析之前,我先在本地环境运行了原始代码并将截图附在下面👇。
1 运行环境:
| OS | IDE | Python | GUI框架 | DB |
|---|---|---|---|---|
| Windows 11 | VS Code | python 3.13.0 | Tkinter | SQLite3 |
2 原始运行结果截图:
截图1:原始程序正常运行界面,展示添加记录后的列表效果
截图2:在金额输入框输入非数字“一百块”,程序抛出ValueError的终端报错
3 原始核心代码(伸缩展示):
点击展开原始缺陷代码
# old.py是典型的“面条代码”,把所有东西揉在一起
import tkinter as tk
from tkinter import messagebox
import sqlite3
# 问题1:全局数据库连接,没有正确关闭
conn = sqlite3.connect('my_account.db')
c = conn.cursor()
# 创建表(如果不存在)
c.execute('''CREATE TABLE IF NOT EXISTS records
(id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT, amount REAL, desc TEXT)''')
conn.commit()
def add_record():
# 问题2:没有输入校验。如果amount输入非数字,程序会报错
r_type = type_var.get()
amt = float(amount_entry.get())
desc = desc_entry.get()
# 问题3:UI直接操作DB,高耦合
# 问题4:使用字符串格式化拼接SQL,存在SQL注入风险
sql = f"INSERT INTO records (type, amount, desc) VALUES ('{r_type}', {amt}, '{desc}')"
c.execute(sql)
conn.commit()
messagebox.showinfo("成功", "记录已添加!")
refresh_list()
def refresh_list():
listbox.delete(0, tk.END)
# 问题5:查询时所有数据读内存,数据量大时界面会卡死
c.execute("SELECT * FROM records")
rows = c.fetchall()
for row in rows:
listbox.insert(tk.END, f"[{row[1]}] 金额: {row[2]} - 备注: {row[3]}")
# UI部分
root = tk.Tk()
root.title("简易记账本")
root.geometry("400x400")
# 问题6:用户可以输入任意字符串("支出"、"花费"、"其他"、"未知类型"等),后续统计查询时需要处理各种字符串
tk.Label(root, text="类型 (收入/支出):").pack()
type_var = tk.StringVar(value="支出")
tk.Entry(root, textvariable=type_var).pack()
tk.Label(root, text="金额:").pack()
amount_entry = tk.Entry(root)
amount_entry.pack()
tk.Label(root, text="备注:").pack()
desc_entry = tk.Entry(root)
desc_entry.pack()
tk.Button(root, text="添加记录", command=add_record).pack(pady=10)
listbox = tk.Listbox(root, width=50, height=15)
listbox.pack(pady=10)
refresh_list() # 启动时加载数据
root.mainloop()
三、主要问题列表与重构思路
1 全局数据库连接泄漏
- 问题:conn全局变量,程序异常退出无conn.close()不释放连接。
- 重构思路:DatabaseManager类封装,使用context manager。
2 高耦合面条代码
- 问题:UI+DB+业务逻辑杂乱的放在一起。
- 重构思路:引入MVC架构模式,分为DatabaseManager(Model)、AppWindow(View+Controller),各尽其职。
3 输入异常导致添加记录失败
- 问题:用户在金额输入框输入非数字字符时(比如输入“一百块”、“abc”等),float()转换会抛出异常,导致添加记录失败且无提示。
- 重构思路:Controller层增加try-except捕获异常,且用messagebox.showerror()让界面友好提示“请输入有效数字”。
4 SQL注入风险
- 问题:使用f-string字符串拼接SQL语句,备注输入:吃饭'); DROP TABLE records; --,生成SQL:INSERT ... VALUES('支出', 100, '吃饭'); DROP TABLE records; --')会破坏数据库结构。
- 重构思路:改用参数化查询c.execute("INSERT ... VALUES (?, ?, ?)", (params))。
5 类型输入无约束
- 问题:用户自由输入可能导致数据不规范(“支出”、“消费”、“花钱”),且统计查询困难,SQL注入风险。
- 重构思路:枚举下拉选择框ttk.Combobox(values=["支出", "收入"], state="readonly")。
6 没有数据统计展示功能
- 二次开发:新增实时资产统计面板,用于显示总收入、总支出、账户结余。
7 数据排序不方便用户查看习惯
- 二次开发:添加ORDER BY id DESC(最新记录在最上面)。
四、新代码附上
经过MVC重构和二次开发,新代码结构清晰、功能健壮。
点击展开完整重构代码(MVC架构+二次开发)
import tkinter as tk
from tkinter import messagebox, ttk
import sqlite3
# Model层,负责数据库交互
class DatabaseManager:
def __init__(self, db_name='my_account_v2.db'):
self.conn = sqlite3.connect(db_name)
self.cursor = self.conn.cursor()
self.create_table()
def create_table(self):
self.cursor.execute('''CREATE TABLE IF NOT EXISTS records
(id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT, amount REAL, desc TEXT)''')
self.conn.commit()
def insert_record(self, r_type, amount, desc):
# 使用参数化查询,防止SQL注入
self.cursor.execute("INSERT INTO records (type, amount, desc) VALUES (?, ?, ?)",
(r_type, amount, desc))
self.conn.commit()
def get_all_records(self):
# 按照ID降序排列,最新记录在前
self.cursor.execute("SELECT * FROM records ORDER BY id DESC")
return self.cursor.fetchall()
def clear_all(self):
self.cursor.execute("DELETE FROM records")
self.conn.commit()
def close(self):
self.conn.close()
# View和Controller层,负责界面与业务逻辑
class AppWindow:
def __init__(self, root, db_manager):
self.root = root
self.root.title("简易记账本V2.0")
self.root.geometry("450x550")
self.db = db_manager
self.setup_ui()
self.refresh_list()
def setup_ui(self):
# 输入区
input_frame = tk.LabelFrame(self.root, text="添加新记录", padx=10, pady=10)
input_frame.pack(fill="x", padx=10, pady=5)
tk.Label(input_frame, text="类型:").grid(row=0, column=0)
self.type_var = tk.StringVar(value="支出")
# 枚举约束values=["支出", "收入"],用户只能选择预定义选项
ttk.Combobox(input_frame, textvariable=self.type_var, values=["支出", "收入"], width=10, state="readonly").grid(row=0, column=1)
tk.Label(input_frame, text="金额:").grid(row=1, column=0, pady=5)
self.amount_entry = tk.Entry(input_frame)
self.amount_entry.grid(row=1, column=1)
tk.Label(input_frame, text="备注:").grid(row=2, column=0)
self.desc_entry = tk.Entry(input_frame)
self.desc_entry.grid(row=2, column=1)
tk.Button(input_frame, text="保存记录", command=self.add_record, bg="#4CAF50", fg="white").grid(row=3, column=0, columnspan=2, pady=10)
# 统计面板 (二次开发新功能)
self.stats_var = tk.StringVar()
tk.Label(self.root, textvariable=self.stats_var, font=("Arial", 10, "bold"), fg="blue").pack(pady=5)
# 列表区
self.listbox = tk.Listbox(self.root, width=50, height=12)
self.listbox.pack(padx=10, pady=5)
# 清空按钮
tk.Button(self.root, text="一键清空记录", command=self.clear_data, bg="#f44336", fg="white").pack(pady=5)
def add_record(self):
r_type = self.type_var.get()
desc = self.desc_entry.get()
# 增加异常处理
try:
amt = float(self.amount_entry.get())
if amt <= 0:
raise ValueError("金额必须大于0")
except ValueError:
messagebox.showerror("输入错误", "请输入有效的数字金额!")
return
self.db.insert_record(r_type, amt, desc)
self.amount_entry.delete(0, tk.END)
self.desc_entry.delete(0, tk.END)
self.refresh_list()
def refresh_list(self):
self.listbox.delete(0, tk.END)
records = self.db.get_all_records()
total_in = 0
total_out = 0
for row in records:
r_type, amt, desc = row[1], row[2], row[3]
self.listbox.insert(tk.END, f"【{r_type}】 ¥{amt:.2f} | 备注: {desc}")
if r_type == "收入":
total_in += amt
else:
total_out += amt
# 更新统计信息
balance = total_in - total_out
self.stats_var.set(f"总收入: ¥{total_in:.2f} | 总支出: ¥{total_out:.2f} | 结余: ¥{balance:.2f}")
def clear_data(self):
if messagebox.askyesno("警告", "确定要清空所有账单数据吗?一旦操作将不可逆!"):
self.db.clear_all()
self.refresh_list()
if __name__ == "__main__":
root = tk.Tk()
db = DatabaseManager()
app = AppWindow(root, db)
root.mainloop()
db.close()
五、重构软件的测试截图
截图3:重构后的程序新界面,含二次开发新增的资产统计功能,且约束了记录类型的选项
截图4:在金额框输入“一百块”等非数字时的错误输入提示
截图5:二次开发新增一键清空记录功能,会弹出警告二次确定,如果确定清空则记录将变空
六、总结(逆向软件工程的思考)
我是第一次使用博客园,前几天刚刚注册成功,所以这次也是个很好的练手机会吧。我在设置排版(文字大小、图片大小和排布)的时候发现可以用HTML和CSS语法,进而了解到Markdown本质上是HTML的轻量级简化版,直接嵌入了HTML、CSS,甚至部分平台的Markdown支持简单的JavaScript。
好了,接下来回到正题~
这次对记账本项目的逆向分析与MVC重构,让我学习到怎么把“面条代码”拆分为清晰的MVC架构,更加理解了曾经学习过的类之间职责划分的思想。我还认识到对异常处理的重要性,try-except避免了终端报错和用户添加记录失败,同时配合我重构的弹窗给用户更好的体验感。
在我看到类型既然是由自己自由输入的时候,我就在想这样岂不是容易出现各种各样的类型?没有标准了,而且记账本肯定要有统计数据的功能,至少我记录的目的就是为了更直观地看到我的总支出和结余,所以我也自然把类型原先的自由输入改为了有约束性的选项。
我觉得参数化查询的改进思路算是一个难点吧,花时间上网查询资料并结合项目本身存在的漏洞,理解了为什么有SQL注入风险之后,我选择用参数化查询解决这一问题并成功了,这让我又get了一些新知识。
我更加体会到面向对象的设计在很大程度上提高了新代码的可维护性。对了,良好的架构设计也让新增统计面板功能变得更加简单,这一优势在最开始想改善思路的时候我没想到,二次开发了才发现更加省事了。
这次二次开发这个项目给我最大的启示就是:编写代码时要多考虑到未来的自己或团队的成员,一个优秀的项目其代码不只是要能跑,更要健壮、易读、可扩展。这可能也是我们软件工程专业课程的部分核心价值所在吧。
虽然只是一次作业要求,但这是我第一次在博客园发表博客,所以我觉得也挺有纪念意义,以后还想通过后续课程的学习和作业学会更加熟练的使用它,直到有一天我可以自发的在此发表我的想法或者未来的学习心得之类的。那么读到这也谢谢各位的阅读!祝大家生活愉快,天天开心~😃

浙公网安备 33010602011771号