资讯动态

Python记账系统源码解析:从分层架构到运行避坑

发布时间:2026/10/3 14:44:54 来源:尧图企业网站定制
简介基于Python的简易记账软件设计源码面向个人及小型企业用户解决日常收支记录与基础财务统计问题适合Python开发者和前端初学者作为综合练习项目。压缩包共37个文件约848KB其中20个Python脚本负责业务逻辑与数据访问4个HTML配合4个CSS构建界面5个JavaScript脚本增强页面交互另附说明文档、图标及Git忽略规则文件源码按controller、dao、service分层组织从入口文件到各模块调用链清晰覆盖登录、首页、添加记录、历史查询等页面。目前已有717人学习/下载项目体量适中便于快速通读与二次开发。读者可从中学习分层架构思想、Python数据库操作、基于Bootstrap的页面搭建、JavaScript交互增强等技能也能直接运行源码作为简易记账工具或在其基础上扩展多用户、报表统计等功能为同类财务软件开发提供有益参考。1. 记账软件源码拿到手先别急着跑先把这张“地图”读懂个人记账这件事听起来简单真正做起来比想象中麻烦得多。Excel 表格用久了卡顿手机 App 数据又不在自己手里。这份基于 Python 的简易记账软件源码属于典型的“教科书式工程结构落地”项目——29 个文件后端的 controller、service、dao 分层清晰前端的 HTML、CSS、JavaScript 也都给你配齐了。它的价值不在于功能多花哨而在于代码组织方式完全照搬了企业级项目的分层思路适合拿来练手、二次开发或者作为 Python 课程设计的参考模板。很多人拿到源码第一反应是直接跑 test.py结果报错一堆其实问题多半出在对项目结构缺少整体认知。这篇笔记我会从目录结构讲起拆到每个文件干什么、层与层之间怎么调用、数据库操作怎么写最后把常见的运行坑逐个点名。整个流程走完你能达到的水平是不仅能跑通还能自己动手加功能。2. 拆解目录结构29 个文件背后的分层逻辑2.1 从上到下认识整个项目骨架先整体看一眼目录树。整个项目分成六个核心包controller控制层、service业务层、dao数据访问层、model实体模型、html前端页面、filter过滤器再加上 static 静态资源和一个 test.py 入口。upload.zip ├── main.py ├── controller/ │ ├── __init__.py │ ├── usercontroller.py │ └── ordercontroller.py ├── service/ │ ├── __init__.py │ ├── userservice.py │ └── orderservice.py ├── dao/ │ ├── __init__.py │ ├── userdao.py │ ├── orderdao.py │ └── dbhelper.py ├── model/ │ ├── __init__.py │ ├── order.py │ └── userinfo.py ├── html/ │ ├── login.html │ ├── index.html │ ├── add.html │ └── history.html ├── filter/ │ ├── __init__.py │ └── filter.py └── static/ ├── js/ ├── css/ └── images/这个结构不是一个摆设它对应的是后端开发中经典的三层架构controller 不直接碰数据库service 不写 SQLdao 只做数据读写model 定义表结构对应的实体类。分层的意义在于每一层只干自己那一件事改任何一层都不影响其他层。2.2 controller、service、dao 三层各自扮演什么角色学过 Spring MVC 的朋友看到这三个文件夹会相当亲切。controller 负责接收请求、调用 service、返回页面或数据service 层负责业务逻辑比如登录校验、余额计算dao 层负责和数据库打交道包括增删改查。main.py 是程序入口启动服务后所有请求先经过 main.py 分发再到 controller。从包名上看usercontroller 和 ordercontroller 就是两个核心业务的入口。用户管理一个订单记账记录管理一个。这个命名习惯在真实项目里非常常见——按业务模块划分 controller而不是按功能划分比如按功能拆的话你可能会写出 logincontroller 和 addcontroller但按业务拆就是 usercontroller 和 ordercontroller后者更利于扩展。项目虽小但在设计思路上已经向主流的软件工程实践看齐。2.3 model 和 dbhelper被很多人忽略却最关键的两个文件model 文件夹里放着 order.py 和 userinfo.py这两个文件在 Java 项目里叫 POJO 或 Entity在 Python 里就是普通类定义了用户和订单的数据结构。dao/dbhelper.py 是数据库辅助工具负责建立连接、执行 SQL、关闭连接。好多同学拿到项目后直接去看 controller 或 service跳过 model 和 dbhelper结果后面越看越糊涂因为不知道数据从哪来、字段叫什么。提示读源码的顺序建议是 model → dbhelper → dao → service → controller。从底层往上读调用关系会非常清楚因为每上一层就是一次方法的调用而不是在高层代码里不停地猜数据从哪来。3. 把核心代码拆开看从登录校验到账单分页的完整链路3.1 userdao 与 usercontroller先搞清楚用户登录是怎么走通的用户相关的核心代码链路是这样login.html 页面提交表单到 usercontrollercontroller 调用 userserviceservice 调用 userdao 里的查询方法dao 通过 dbhelper 查询数据库最后把结果一层层返回决定登录成功还是失败。我以“登录校验”为场景手写一遍这个链路的典型实现方便你对照源码阅读# dao/userdao.py import sqlite3 class UserDao: def __init__(self, db_path): self.db_path db_path def find_by_username(self, username): conn sqlite3.connect(self.db_path) cursor conn.cursor() sql SELECT id, username, password FROM userinfo WHERE username ? cursor.execute(sql, (username,)) row cursor.fetchone() cursor.close() conn.close() return row这段代码的逻辑不复杂但两个细节值得记住第一SQL 使用的是参数化查询username 用 ? 占位这是防止 SQL 注入的基本做法新手写代码很容易用字符串拼接 SQL源码里这种用法值得学习第二每次连接用完就关闭避免连接泄漏在 Python 的 sqlite3 模块里不主动 close 的话连接会一直占用文件句柄程序跑久了就会报“database is locked”。dao 层只负责查询不判断密码对不对判断逻辑交给了 service 层。# service/userservice.py import hashlib class UserService: def __init__(self, user_dao): self.user_dao user_dao def login(self, username, password): row self.user_dao.find_by_username(username) if row is None: return None # 注意源码里的密码存储方式请以实际为准这里演示常见做法 hashed_password hashlib.sha256(password.encode(utf-8)).hexdigest() if row[2] hashed_password: return {id: row[0], username: row[1]} return Noneservice 层做了一个重要的事把 data 层的原始查询结果转换成业务层需要的字典对象同时承担密码校验逻辑。这样 controller 拿到的就是一个干净的登录结果不需要关心数据库字段的索引位置。参数说明username 是登录表单传来的账号password 是明文密码经过哈希后和数据库存储的密文比较返回字典时只携带 id 和 username敏感信息不下传。3.2 orderdao 的增删改查与分页查询实现订单账单模块是记账软件的核心orderdao 里应该有新增、删除、修改、按用户查询账单、分页查询等方法。记账软件特有的需求是“按用户 ID 查所有账单”和“按时间范围筛选”。以前端页面 history.html 展示账单历史为例查询逻辑的大致样子如下# dao/orderdao.py class OrderDao: def __init__(self, db_path): self.db_path db_path def find_by_user_id(self, user_id, page, page_size): conn sqlite3.connect(self.db_path) cursor conn.cursor() offset (page - 1) * page_size sql SELECT id, amount, category, note, create_time FROM orders WHERE user_id ? ORDER BY create_time DESC LIMIT ? OFFSET ? cursor.execute(sql, (user_id, page_size, offset)) rows cursor.fetchall() cursor.close() conn.close() return rows这里的分页查询是典型做法LIMIT 控制一页显示多少条OFFSET 控制跳过前面多少条。page 从 1 开始第 1 页 OFFSET 为 0第 2 页 OFFSET 为 page_size。这是一个顺序表查询的经典实现也是真实项目中列表页最常见的翻页逻辑。源码中 orderdao 的核心方法大概率包括 find_all、save、delete、update 这几个基础方法找到它们对照读就能理清整个模块。3.3 filter.py 做登录拦截源码里一个容易被忽略的“守门员”filter 包下的 filter.py 在项目里负责登录状态的检查。访问 index.html、add.html、history.html 这类页面之前filter 会先确认当前会话是否已登录没有登录就跳回 login.html。这个机制在 Flask 中通常用 before_request 钩子实现在纯 Python 的 socket 项目中可能是手动在分发前做判断。作用都是同一个防止未登录用户直接通过 URL 访问内部页面。它的工作流程是浏览器请求 → main.py 接收 → filter 检查 session → 放行或重定向。你看到所有内部页面都可以在浏览器地址栏直接输入 URL 访问就是因为 filter 这个“拦截器”兜底。带过项目的人都知道这种机制一开始没有的话后面补非常麻烦。因此拿到手的第一件事其实建议先看 filter.py 的实现细节确认 session 存储的位置和过期策略这决定了整个项目的安全边界。4. 前端页面的配合逻辑四个 HTML 是怎么和 Python 后端对上的4.1 login.html、index.html、add.html、history.html 的功能分工前端虽然只有四个 HTML 文件但覆盖了记账软件最核心的完整路径。login.html 是登录页负责收集用户名和密码index.html 是主面板展示统计摘要、近期账单add.html 是记账表单页填写金额、分类、备注history.html 是历史账单列表支持分页查看。这套页面设计覆盖了一个记账工具的刚需闭环“登录 → 看总览 → 记一笔 → 翻历史”。对应的交互路径也很清晰登录成功后跳 index.html从 index.html 点“新增”跳 add.html点“历史记录”跳 history.html。每个页面提交后的去向又不同——add.html 提交表单新增订单提交完成后回到 index.html 刷新数据history.html 的分页点击会触发查询请求带 page 参数重新拉数据。所以四个页面不是孤立的它们通过 controller 接口串成了一条完整的业务流。4.2 静态资源 mappingbootstrap、jquery 文件被谁调用了static 目录下面放了 bootstrap.min.css、bootstrap.min.js、jquery.min.js 和几个工具类 JS。这部分文件是典型的“拿来即用”型资源前端页面在 HTML 中通过相对路径引用它们。项目里 js 文件夹中的 ie10-viewport-bug-workaround.js 是一个老经典——专门修复 IE10 在 Bootstrap 3 下的 viewport 兼容问题看到这个文件大概能推断出前端是基于 Bootstrap 3 构建的。理解静态资源配置的关键在于页面上的样式、交互效果、响应式布局全部由 static 目录下的文件支撑。如果你准备换主题只需要替换 css 文件并保持文件名不变页面就能自动生效不用改动 HTML 结构。如果遇到页面完全没样式的情况十有八九是静态资源路径没配对报错会在浏览器控制台的 Network 标签里显示为 404。4.3 前后端数据是怎么传的form 表单和 URL 参数的配合这个项目是典型的前后端分离初级阶段——HTML 负责页面展示Python 后端负责逻辑处理数据交换通过 HTTP 请求完成。登录表单、记账表单通过 POST 方法提交数据分页、筛选这类轻量操作通过 GET 参数传递。比如 history.html 的分页链接通常长这样a href/order/list?page2page_size10下一页/a后端通过解析请求对象拿到 page 和 page_size 参数传给 orderdao 的分页方法。新手最容易困惑的是为什么有的页面跳转是在 Python 代码里完成而不是在 HTML 里用 a 标签 因为登录、新增这类操作需要先经过后端校验校验通过后才能跳转这个跳转动作叫做重定向redirect。重定向由后端返回一个 302 状态码加 Location 头信息浏览器收到后自动访问新地址。这种方式保证了业务逻辑不被绕过。提示在后端代码里搜索 redirect 或者 url_for 之类的关键词能快速定位页面跳转逻辑比满项目找 HTML 页面的互相引用高效得多。5. 避坑与排查从拿到源码到跑通页面的那些血泪经验5.1 坑一运行报错 No module named controller现象在项目根目录直接执行python main.py立刻报错显示找不到 controller 模块。原因Python 的模块导入路径默认是当前目录如果你的命令是在 upload 目录的上一层执行的解释器就找不到 controller 这个包。这也是最经典的 Python 新手翻车点。解决先cd upload进入项目根目录再执行python main.py或者用python -m main的方式运行。验证方法是在 Python 交互环境里执行import controller不报错就是路径对了。5.2 坑二页面能打开但是没样式所有排版乱成一团现象浏览器访问页面后内容是出来了但完全是纯文本排列没有表格线、没有按钮样式。原因HTML 正确加载但 css 文件 404通常是静态文件路径配置错误。在本地开发环境跑纯 Python 项目时static 目录往往需要手动配置为可访问的静态路径缺一行配置就全部失效。解决打开浏览器开发者工具F12切到 Network 标签刷新页面找红色 404 请求看具体是哪个 css 文件加载失败。然后检查 main.py 里的静态目录指向是否与文件夹名一致常见错误是代码里写的是static/但目录名实际叫public/或者大小写不一致。5.3 坑三登录报错提示数据库不存在现象输入任何账号密码都提示无法连接或表不存在。原因dbhelper 里的数据库文件路径是相对路径如果你不在项目根目录运行程序它会在当前目录创建一个空的新数据库文件里面没有 userinfo 表于是报错。解决检查项目里是否有 .db 或 .sqlite 后缀的数据文件。如果有确认 dbhelper.py 里连的是不是这个文件名如果没有请先执行项目里建表相关的 SQL 脚本。解决更稳妥的做法是把数据库路径和文件名的定义放在 main.py 顶部统一配置避免每次都去代码里挖路径。5.4 坑四添加账单后刷新页面数据丢失现象add.html 里成功添加了一条记录跳回 index.php 能看到但重启程序后再看数据没了。原因数据被写入了内存或临时文件而不是持久化到数据库文件。解决检查 service 层是否有 save 方法的调用确认 add 流程走的是controller → service → dao完整链路而不是临时把数据存在了 session 或全局变量里。另一个可能是数据库文件路径不一致程序启动时用的库和运行时写入的库不是同一个文件。在 dao 层打印实际使用的 db_path 可以很快定位问题。5.5 坑五新增功能后页面跳转报 500现象往项目里加了代码运行到某个接口时报 500 Internal Server Error。原因多半是代码运行异常但没有被捕获Python 默认会输出完整错误堆栈到控制台。解决切回终端看报错信息定位到具体的行号。最常见的问题是 SQL 语句里表名字段名写错其次是函数参数个数不对。如果控制台没有输出检查 main.py 里的异常处理逻辑是否把错误吞掉了可以临时在 controller 入口处加一个 try-except 把异常信息打出来。6. 进阶玩法在现有结构上快速加一个“月度统计”功能理解了分层逻辑之后你会发现自己动手加功能其实并不难。我以“月度收支统计”为例手把手演示如何在现有源码上扩展新功能这是你在任何课程作业答辩中都能拿出手的亮点。先确定功能需求在 index.html 首页上显示本月的总收入、总支出和结余。按照分层结构来加需要改动 model 层加一个统计结果的数据结构、dao 层加一个聚合查询方法、service 层加业务计算、controller 层加接口、最后改 index.html 展示。# dao/orderdao.py 新增方法 def summary_by_month(self, user_id, year, month): conn sqlite3.connect(self.db_path) cursor conn.cursor() sql SELECT SUM(CASE WHEN type income THEN amount ELSE 0 END) as total_income, SUM(CASE WHEN type expense THEN amount ELSE 0 END) as total_expense FROM orders WHERE user_id ? AND strftime(%Y-%m, create_time) ? cursor.execute(sql, (user_id, f{year}-{month})) row cursor.fetchone() cursor.close() conn.close() return {income: row[0] or 0, expense: row[1] or 0}这里用了 SQL 的条件聚合一行查询同时算出收入和支出合计。strftime(%Y-%m, create_time)是 SQLite 里把时间字段格式化成“年-月”字符串的写法这样无论 create_time 存的是 datetime 还是字符串类型都可以统一匹配。SQL 里CASE WHEN是条件判断符合条件就累加金额否则记为 0。or 0是为了防止返回 None 导致后续计算报错。# service/orderservice.py 新增方法 def get_monthly_summary(self, user_id, year, month): summary self.order_dao.summary_by_month(user_id, year, month) summary[balance] summary[income] - summary[expense] return summaryservice 层在拿到 dao 查询结果后做一次减法得到结余。这样做的好处是dao 层保持“只做查询”的纯粹性业务计算放在 service 层以后如果要加“可用余额 结余 - 预算”之类的逻辑只需要改 service 一个地方。然后在 controller 层加一个/summary接口接收 user_id 和月份参数调用 service 方法返回 JSON 数据。前端 index.html 在页面加载时通过 JavaScript 发起一次 fetch 请求把统计数字填充到页面对应的 DOM 元素上。整个功能的改动量在四人分队里你一个人最多半天就能完成而且完全没有破坏原有的三层结构。从那以后我每次接到一个新功能需求都强制自己走“确认表结构 → 写 dao → 写 service → 接 controller → 改页面”这条流程而不是随手写一个函数一把梭。这套流程对个人开发者可能显得繁琐但一旦项目长大到 50 个文件以上你会感谢当初的结构克制。希望这份拆解笔记帮到你源码里的细节远比我写出来的多拿着目录边读边跑收获会更快。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑