拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Python标准库手写记账软件:SQLite存储与命令行报表实战

Python标准库手写记账软件:SQLite存储与命令行报表实战

简介:基于Python的简易记账软件设计源码,面向个人用户与小型企业,用于解决日常收支记录、流水统计和基础财务台账管理问题。项目采用Python后端与HTML/CSS/JavaScript前端,整体分为Controller、Dao、Service三层,涵盖用户管理、订单/流水记录、登录与历史明细等功能,模块划分清晰,便于二次开发与学习。

整个压缩包共37个文件,大小约848KB;核心代码以20个Python脚本为主,包含业务逻辑、数据访问和接口处理,前端由4个HTML、4个CSS与5个JavaScript文件构成,另有映射、图标及Git忽略规则等配套文件,结构完整可直接运行或改造参考。

已有717人学习/下载。项目特别适合Python初学者、课程设计及小型项目实践者,既能理解软件分层思路,也能通过实际案例掌握表单交互、数据库操作与后端接口设计,是一款轻量但完整的记账软件入门范本。

1. 记账软件自己写,值不值当

翻了下三个月的支付账单,发现每一笔几十块的零碎消费加起来比房租还高。想做个统计,Excel 里折腾半天格式乱了,市面上记账 App 又有广告还怕数据隐私。干脆用 Python 写一个简易记账软件——命令行录入、SQLite 存储、按月出报表,源码全部自己掌控。这个标题背后其实是一类需求:用 Python 标准库实现一个能真实使用的记账工具,不需要 Web 框架、不需要前端,重点在数据模型、增删改查逻辑和报表统计。适合 Python 刚入门想做一个完整项目的同学,也适合想把自己账目握在手里的开发者。简易不等于简陋,单表设计、金额用分存储、查询用参数化 SQL,这些习惯以后写大项目照样用得上。

2. 先把运行环境立起来:Python 版本、虚拟环境与项目骨架

2.1 为什么只依赖 Python 标准库

记账软件的核心是数据录入、查询、统计,这些活儿 Python 的 sqlite3、csv、datetime、argparse 标准库就能干完。常见做法是先用标准库把逻辑跑通,后期要加图表再引入 pandas、matplotlib,要加界面再上 tkinter 或 PyQt。这样做的直接好处是别人拿到的源码只要装了 Python 就能跑,不用先 pip install 一堆依赖。很多人一上来就装 pandas,其实对于一个月几千条流水的记账工具,SQLite 的 GROUP BY 已经够快了。

环境选择上,我建议装 Python 3.10 以上版本,原因有两个:一是 sqlite3 模块内置,不需要单独装数据库;二是 3.10 以后的语法特性比如 match-case、更清晰的报错信息,写源码时省心。Windows 上去 python.org 下载安装包,安装时勾选 Add Python to PATH;macOS 上可以用 brew install python,或者直接官网装。装完在命令行敲 python --version,能输出版本号就算成了。

2.2 用虚拟环境隔离项目依赖

即使只用标准库,也建议建一个虚拟环境,这个习惯能避免以后引入第三方库时污染全局 Python。命令如下:

# Windows python -m venv .venv .venv\Scripts\activate # macOS / Linux python3 -m venv .venv source .venv/bin/activate

激活后命令行前面会出现 (.venv) 前缀,此时 pip 安装的包都只在这个项目里生效。如果用 VS Code 写代码,打开项目文件夹后按 Ctrl+Shift+P,选择 Python: Select Interpreter,指向 .venv 里的解释器,这样编辑器里的语法提示和运行环境保持一致。这一步跳过也没事,但以后项目多了,依赖互相打架的时候就会想起虚拟环境这个后悔药。

源码的文件结构按功能拆开,而不是全塞进一个 main.py。我一般这样组织:

simple_accounting/ ├── app.py # 入口,命令行分发 ├── storage.py # SQLite 连接与建表 ├── models.py # 数据校验与记录对象 ├── operations.py # 记账、查询、统计核心逻辑 ├── reports.py # 月度报表与汇总输出 └── requirements.txt # 当前为空,等引入第三方库再填

这个拆法把存储、模型、操作、报表分开,以后想给某个模块加功能不用牵连其他文件。新手最容易翻车的地方是把所有代码写在一个文件里,几百行之后改一个函数要上下找半天,所以入口只做参数解析和路由,具体逻辑全部丢给对应模块。

2.3 入口文件先做一个能跑的骨架

app.py 的任务很简单:接收命令行参数,分发给对应函数。先用 argparse 做一个最小骨架,确认环境没问题再继续写业务逻辑:

import argparse from operations import add_transaction, list_transactions, monthly_report def main(): parser = argparse.ArgumentParser(description="简易记账软件") sub = parser.add_subparsers(dest="command") add_parser = sub.add_parser("add", help="记一笔账") add_parser.add_argument("--type", choices=["income", "expense"], required=True) add_parser.add_argument("--amount", type=float, required=True) add_parser.add_argument("--category", required=True) add_parser.add_argument("--note", default="") list_parser = sub.add_parser("list", help="查流水") list_parser.add_argument("--days", type=int, default=7) report_parser = sub.add_parser("report", help="月报") report_parser.add_argument("--month", help="YYYY-MM 格式") args = parser.parse_args() if args.command == "add": add_transaction(args) elif args.command == "list": list_transactions(args) elif args.command == "report": monthly_report(args) if __name__ == "__main__": main()

argparse 的好处是自动生成帮助信息,输入 python app.py add --help 就能看到参数的用法说明。子命令的设计让 add、list、report 之间的参数互不干扰,这也是记账软件这类命令行工具最常见的交互方式。参数解释一下:--type 限制只能是 income 或 expense,--amount 记 float 是为了人输入方便,内部存储时再换成整数分,--days 控制查近几天的流水。

跑一下 python app.py add --help,能看到子命令的参数说明,说明环境没问题,可以进入数据层设计。

3. 数据模型先行:单表设计、金额存储与 SQLite 建表

3.1 一条账目记录需要哪些字段

记账需求拆到最简,一条记录至少要回答四个问题:钱从哪来/花到哪去、多少金额、什么时候、备注是什么。对应到表字段,我设计了这样一张表:

CREATE TABLE IF NOT EXISTS transactions ( id INTEGER PRIMARY KEY AUTOINCREMENT, trade_type TEXT NOT NULL CHECK(trade_type IN ('income', 'expense')), amount_cents INTEGER NOT NULL CHECK(amount_cents > 0), category TEXT NOT NULL, note TEXT DEFAULT '', trade_date TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')) );

trade_type 用字符串 income/expense 而不是 0/1,是因为直接可读,查数据时不用脑子里再翻译一遍。amount_cents 是金额乘以 100 后的整数,这是整个设计里最关键的一个决定。举个实际例子,早饭 7.9 元记成 790 分,午饭 28.5 元记成 2850 分,月底汇总时直接用整数相加,不会出现 0.1+0.2=0.30000000000000004 这种浮点误差。所有统计报表的金额输出时再除以 100 转回十进制字符串。

category 字段是自由文本还是预置列表?我的建议是预置一批常用分类,同时允许用户在录入时输入任意值。预置的好处是月度分类统计时能看到干净的聚合结果,自由输入的好处是比如临时记一笔“宠物看病”,不至于找不到分类而放弃记账。折中做法是提供一个默认分类建议,但程序不做强校验,源码里留给用户自己改。

trade_date 单独存 TEXT 而不是用 created_at,是因为用户可能补录几天前的账,记账时间和交易时间不是一回事。存储格式统一用 YYYY-MM-DD,这样用 BETWEEN 查询时字符串比较正好是按日期排序的,不需要 date() 函数转换。

3.2 SQLite 与 CSV 的选型边界

有人会把数据存在 CSV 文件里,代码更简单,Excel 也能直接打开。CSV 方案在账目量小于几百条、单用户不并发写入时确实够用,但遇到两个场景就难受了:一是程序崩溃或断电,CSV 写到一半文件损坏,整份账目可能全丢;二是月末要按分类汇总时,得用 Python 循环遍历所有行自己累加。SQLite 单文件数据库,事务机制保证写入的原子性,一条 SELECT ... GROUP BY 就能完成分类汇总,重复打开关闭也不会损坏数据。代价是文件是二进制格式,不能直接用 Excel 打开,但可以用命令行导出 CSV。

存储层我用 sqlite3 标准库,连接方式如下:

import sqlite3 from pathlib import Path DB_PATH = Path(__file__).parent / "accounting.db" def get_connection(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row conn.execute("PRAGMA journal_mode=WAL;") return conn

conn.row_factory = sqlite3.Row 让查询结果可以用 row["amount_cents"] 这样的字段名访问,代码里不用写 row[0]、row[1] 这种没人看得懂的索引。PRAGMA journal_mode=WAL 是优化并发读写的,后面避坑章节还会再提到,这里先埋个伏笔。

3.3 建表与初始化:首次运行自动创建

为了不让用户手动执行 SQL 文件,我在 storage.py 里加一个 init_db 函数,在入口 main() 第一行调用。首次运行自动建表,文件不存在则自动创建,这是源码开箱即用的关键设计:

def init_db(): with get_connection() as conn: conn.execute( """ CREATE TABLE IF NOT EXISTS transactions ( id INTEGER PRIMARY KEY AUTOINCREMENT, trade_type TEXT NOT NULL CHECK(trade_type IN ('income', 'expense')), amount_cents INTEGER NOT NULL CHECK(amount_cents > 0), category TEXT NOT NULL, note TEXT DEFAULT '', trade_date TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')) ); """ ) create_monthly_report_view()

这里有个易忽略的细节:with get_connection() as conn 语法不会自动关闭连接,sqlite3 的 connect 对象是上下文管理器,只提交事务但不关闭连接。时间长了程序反复运行会有句柄泄漏的隐患。我一般会在函数末尾显式调用 conn.close(),或者用 contextlib.closing 包一层。为了让源码看起来更整洁,最直接的方式是每次取连接后用完手动关,下面的代码都按这个习惯处理:

conn = get_connection() try: # 数据库操作 pass finally: conn.close()

这个习惯在多线程环境下尤其重要,连接不关会导致文件锁一直占着,Windows 上甚至会报数据库被占用。

3.4 初始化时写一条测试数据验证链路

建表函数写完后,先别急着写业务逻辑,插入一条测试数据验证整个链路是通的。这一步相当关键,可以省去后面排查代码问题时还要怀疑建表没成功的烦恼:

conn = get_connection() try: conn.execute( "INSERT INTO transactions (trade_type, amount_cents, category, note, trade_date) VALUES (?, ?, ?, ?, ?)", ("expense", 790, "餐饮", "早饭", "2024-05-20"), ) conn.commit() row = conn.execute("SELECT * FROM transactions WHERE id = last_insert_rowid()").fetchone() print(dict(row)) finally: conn.close()

代码里的 ? 占位符是参数化查询,values 由 Python 变量传入,避免拼接 SQL 字符串造成的注入风险和引号转义问题。last_insert_rowid() 拿的是刚插入那一行的自增 ID,拿回来再查一遍,确认能读到自己刚写入的数据。如果这里翻车,多半是 DB_PATH 指向的目录没有写入权限,换成项目目录下的绝对路径即可。

4. 核心记账逻辑:增删改查、参数校验与统计报表

4.1 记账操作:先校验再落库

记账入口的 add_transaction 函数要做三层检查:金额必须大于 0、类型必须是 income/expense、日期格式合法。这三层过了再写数据库,缺一不可:

from datetime import datetime def parse_amount_to_cents(amount: float) -> int: """金额转分,规避浮点误差""" if amount <= 0: raise ValueError("金额必须大于 0") return int(round(amount * 100)) def parse_date(date_str: str) -> str: """校验日期格式,返回 YYYY-MM-DD""" try: dt = datetime.strptime(date_str, "%Y-%m-%d") return dt.strftime("%Y-%m-%d") except ValueError: raise ValueError("日期格式应为 YYYY-MM-DD,例如 2024-05-20") def add_transaction(args): amount_cents = parse_amount_to_cents(args.amount) trade_date = parse_date(args.date) if hasattr(args, "date") and args.date else datetime.now().strftime("%Y-%m-%d") conn = get_connection() try: conn.execute( "INSERT INTO transactions (trade_type, amount_cents, category, note, trade_date) VALUES (?, ?, ?, ?, ?)", (args.type, amount_cents, args.category.strip(), args.note.strip(), trade_date), ) conn.commit() print(f"记账成功:{args.type} {args.amount} 元,分类 {args.category},日期 {trade_date}") finally: conn.close()

parse_amount_to_cents 里先 round 再 int,是为了避免 19.99*100 得到 1998.9999999999998 这样的浮点结果。这个坑在记账软件里非常经典,金额汇总差一分钱时,先怀疑这里。注意 category.strip() 和 note.strip(),用户手滑输入的前后空格如果不处理,查询“餐饮”会漏掉“ 餐饮”分类的数据。

4.2 改账与删账:软删除还是物理删除

日常使用中最大的需求其实是“记错了”要改。有两种思路:UPDATE 原记录,或者把原记录标记为删除再插入新记录。简易记账软件用 UPDATE 就够,直接改字段、保留 id,不会破坏关联关系。删除时我推荐物理删除,因为单表没有外键关联,而且账本数据量小,不存在性能压力:

def update_transaction(trans_id: int, **fields): allowed = {"trade_type", "amount_cents", "category", "note", "trade_date"} updates = {k: v for k, v in fields.items() if k in allowed} if not updates: raise ValueError("没有可更新的字段") set_clause = ", ".join(f"{col} = ?" for col in updates) values = list(updates.values()) + [trans_id] conn = get_connection() try: conn.execute(f"UPDATE transactions SET {set_clause} WHERE id = ?", values) conn.commit() finally: conn.close() def delete_transaction(trans_id: int): conn = get_connection() try: conn.execute("DELETE FROM transactions WHERE id = ?", (trans_id,)) conn.commit() print(f"已删除记录 #{trans_id}") finally: conn.close()

注意 set_clause 不能直接拼用户输入,这里用的是白名单过滤,只把 allowed 集合里的字段拼进 SQL,其余的字段即使传入也被忽略。源码里这个模式叫动态 UPDATE 的白名单构造,是防止 SQL 注入的关键手段。UPDATE 和 DELETE 执行后要检查 cursor.rowcount,如果等于 0 说明这个 id 不存在,给用户一个明确提示而不是静默成功。

4.3 流水查询与分类汇总

查询近 N 天流水、按月汇总这两个是记账软件里最常用的功能。近 N 天流水用 trade_date 和 date('now', '-N days') 比较:

def list_transactions(days: int = 7, limit: int = 50): conn = get_connection() try: rows = conn.execute( """ SELECT id, trade_type, amount_cents, category, note, trade_date FROM transactions WHERE trade_date >= date('now', ?) ORDER BY trade_date DESC, id DESC LIMIT ? """, (f"-{days} days", limit), ).fetchall() for row in rows: amount = row["amount_cents"] / 100 sign = "+" if row["trade_type"] == "income" else "-" note = f" ({row['note']})" if row["note"] else "" print(f"{row['trade_date']} {sign}{amount:8.2f} [{row['category']}]{note}") finally: conn.close()

date('now', ?) 的参数传负数字符串,SQLite 会自动计算日期偏移,这比在 Python 里用 datetime 加减再格式化更省事,也避免了时区问题。LIMIT ? 防止查询结果太多刷屏,命令行工具输出超过几十行阅读体验就很差了。

分类汇总用 GROUP BY,这是 SQLite 的强项,也体现了把金额存为整数分的另一个好处——SUM 的结果精确,不需要担心浮点累加误差:

def category_summary(month: str): conn = get_connection() try: rows = conn.execute( """ SELECT category, SUM(amount_cents) AS total_cents, COUNT(*) AS cnt FROM transactions WHERE strftime('%Y-%m', trade_date) = ? AND trade_type = 'expense' GROUP BY category ORDER BY total_cents DESC """, (month,), ).fetchall() print(f"{month} 支出分类排行") for row in rows: print(f"{row['category']:<10} {row['total_cents']/100:>10.2f} 元 {row['cnt']} 笔") finally: conn.close()

strftime('%Y-%m', trade_date) 能从 YYYY-MM-DD 里取出年月,字符串比较正好按月份过滤。ORDER BY total_cents DESC 让花钱多的分类排在头上,一眼看出钱被谁吃掉。这个查询放在 SQL 里做要比把所有记录拉到 Python 里再循环累加简洁得多,也是“用数据库思维解决问题”的一个范例。

4.4 月度收支总览

报表函数把收入、支出、结余一起算出来,这是用户月底最关心的数字。实现上不用写复杂的子查询,分别汇总就行:

def monthly_report(month: str): conn = get_connection() try: income = conn.execute( "SELECT COALESCE(SUM(amount_cents), 0) FROM transactions WHERE strftime('%Y-%m', trade_date) = ? AND trade_type = 'income'", (month,), ).fetchone()[0] expense = conn.execute( "SELECT COALESCE(SUM(amount_cents), 0) FROM transactions WHERE strftime('%Y-%m', trade_date) = ? AND trade_type = 'expense'", (month,), ).fetchone()[0] balance = income - expense print(f"{month} 收入:{income/100:.2f} 元") print(f"{month} 支出:{expense/100:.2f} 元") print(f"{month} 结余:{balance/100:.2f} 元") finally: conn.close()

COALESCE 把没有记录时的 NULL 转成 0,否则 SUM 没数据时返回的 None 直接参与减法会抛出类型错误。这笔处理看似小,但恰恰是新手最容易踩的坑。收入、支出各自单独查询,逻辑直白,以后要加预算、年度对比,在这个基础上叠加即可。

5. 避坑记录:记账软件常见的五个翻车现场

5.1 中文路径导致数据库无法打开

现象:项目放在 C:\Users\张三\Documents\记账软件\ 下,运行 init_db 时报 sqlite3.OperationalError: unable to open database file。

原因:SQLite 打开中文路径在某些 Windows 环境变量编码下会失败,尤其是控制台代码页为 GBK、Python 字符串 path 传进 C API 时转换异常。这不是 Python 的 bug,而是操作系统本地化设置与 Python 的 UTF-8 默认编码不一致导致的。

解决:DB_PATH 不要拼在用户目录的中文文件夹下,源码里改为项目所在目录的相对路径,并确保项目目录本身是纯英文。如果必须支持中文路径,可以在程序启动时把路径编码方式显示指定,但最简单的方案是约定项目路径只含英文,下载源码后解压到英文目录再运行。

5.2 SQLite 并发写入报 database is locked

现象:开了两个终端同时向同一数据库插入数据,第二个终端报 sqlite3.OperationalError: database is locked。

原因:SQLite 是文件级锁,同一时刻只允许一个写事务。WAL 模式允许读写并发,但两个写操作还是会互相阻塞,默认超时时间是 0 秒,立刻抛错。

解决:连接时加上 timeout 参数,让等待锁的时间拉长:

conn = sqlite3.connect(DB_PATH, timeout=10) conn.execute("PRAGMA journal_mode=WAL;")

timeout=10 表示拿不到写锁时最多等 10 秒,多数情况等前一个事务提交完就能继续。如果是长时间持锁的事务,要么减小事务粒度,要么用排队逻辑避免并发写。个人记账场景基本不会撞上,但如果以后改成多人共用一个小型记账服务,这里会第一个暴露问题。

5.3 金额汇总差一分钱:浮点存储的坑

现象:记了三笔 0.1 元、0.2 元、0.3 元的账,报表却显示 0.60 元,跟手算的 0.6 元对不上。更隐蔽的是 SUM 结果出现 0.6000000000000001。

原因:float 在计算机里是二进制小数,0.1 在二进制下是无限循环,相加时的舍入误差会累积。直接存 float 到 SQLite 再 SUM,误差会一直在。

解决:从源头解决,录入时乘 100 转 int,存储、计算都用整数分,只在展示时除以 100。代码里 parse_amount_to_cents 用了 round(amount * 100),这是处理用户输入浮点数的最后一层防线。这个坑在记账软件里出现过太多次,凡是涉及钱的计算,一律 int,不做任何例外。

5.4 PyInstaller 打包后找不到数据库文件

现象:源码在 Python 环境里运行正常,用 PyInstaller 打成 exe 后,换到别的电脑上提示 unable to open database file。

原因:打包后程序的当前工作目录是 exe 所在目录或系统临时目录,而代码里用相对路径创建的 accounting.db 被放到了临时目录,用户看不到数据,再次启动时数据丢失。

解决:数据库路径要固定放在用户数据目录,而不是当前工作目录:

import os from pathlib import Path APP_NAME = "SimpleAccounting" data_dir = Path(os.environ.get("APPDATA", Path.home())) / APP_NAME data_dir.mkdir(parents=True, exist_ok=True) DB_PATH = data_dir / "accounting.db"

Windows 上 %APPDATA% 是 C:\Users\用户名\AppData\Roaming,文件放在这里不会因为 exe 位置变化而丢失。打包前还要在 PyInstaller 的 spec 文件里排除不必要的模块,减少生成的 exe 体积。这一步虽然看起像“进阶操作”,但只要你想把记账软件发给朋友用,就一定会撞上。

5.5 Windows 控制台打印中文乱码

现象:print("记账成功") 在 Windows 命令行下输出乱码,在 VS Code 终端里却正常。

原因:Windows 控制台默认代码页是 GBK,Python 的标准输出编码跟随系统区域设置,如果源码文件是 UTF-8 保存,print 输出的字符串编码与终端解码方式不匹配。

解决:在入口 main() 最前面加上重配置:

import sys if sys.platform == "win32": sys.stdout.reconfigure(encoding="utf-8")

sys.stdout.reconfigure 是 Python 3.7 之后提供的方法,把输出流改为 UTF-8,同时把控制台代码页切到 UTF-8 通常也能解决,但改终端设置只影响当前窗口,重开终端又恢复,所以代码里强制配置更可靠。乱码问题不影响数据正确性,但影响使用心情,一个 print 就劝退用户很不值。

6. 进阶用法:给记账软件加一个数据自检与月度趋势输出

账记到第三个月的时候,最怕的是数据错了自己不知道。我后来加了一个自检函数,每次跑月报之前先做一次交叉验证,确保报表数字可信。思路很简单:报表里的总支出金额,必须等于明细表里所有 expense 记录的 sum,如果不等,说明有数据被误改或者浮点误差混进来了:

def validate_ledger(month: str): conn = get_connection() try: detail_sum = conn.execute( "SELECT COALESCE(SUM(amount_cents), 0) FROM transactions WHERE strftime('%Y-%m', trade_date) = ? AND trade_type = 'expense'", (month,), ).fetchone()[0] report_sum = conn.execute( "SELECT COALESCE(SUM(amount_cents), 0) FROM expense_summary WHERE month = ?", (month,), ).fetchone()[0] if detail_sum != report_sum: raise ValueError(f"{month} 报表对不上:明细 {detail_sum} vs 汇总 {report_sum}") print(f"{month} 数据校验通过:共 {detail_sum} 分") finally: conn.close()

这个自检函数要想跑通,需要提前建一个 expense_summary 视图,按月份和分类预先聚合。每月月底跑一次校验,数字一致才能把报表发给家人看。数据自检的意义不在于防黑客,而是防止自己几次手滑输错金额后,报表还显示得很正常,实际账目已经不对了。

月度趋势输出用一张简单的文本表格就够了,不需要上图表。输出近 6 个月每月的收入、支出、结余,用字符串格式化对齐:

def trend_six_months(): conn = get_connection() try: rows = conn.execute( """ SELECT strftime('%Y-%m', trade_date) AS month, SUM(CASE WHEN trade_type = 'income' THEN amount_cents ELSE 0 END) AS inc, SUM(CASE WHEN trade_type = 'expense' THEN amount_cents ELSE 0 END) AS exp FROM transactions WHERE trade_date >= date('now', '-6 months') GROUP BY month ORDER BY month """ ).fetchall() print(f"{'月份':<8} {'收入':>12} {'支出':>12} {'结余':>12}") for r in rows: balance = r["inc"] - r["exp"] print(f"{r['month']:<10} {r['inc']/100:>10.2f} {r['exp']/100:>10.2f} {balance/100:>10.2f}") finally: conn.close()

这段代码的价值在于展示了一种思路:命令行工具不需要花哨图表,也可以用对齐的文本表格把趋势呈现清楚。SUM 配合 CASE WHEN 实现按条件累加,一次 SQL 查询就把收入和支出同时算出来,比两条 SQL 拼起来效率更高,代码也更紧凑。

记账软件做到这一步,功能已经覆盖日常使用:记账、查账、分类汇总、月度报表、数据自检、趋势查看。回头看整个过程,最值得沉淀的不是代码技巧,而是把“金额用整数分存储”和“参数化 SQL 防止注入”这两个习惯刻进肌肉记忆。后来的项目不管是写订单系统还是库存管理,这两个原则都直接复用。如果你照着这个结构写一版自己的记账源码,跑通后再加个 GUI 界面或者导出 CSV 功能,会比现在更清楚自己每一笔钱的去向。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表