Python银行管理系统源码实战:面向对象、JSON持久化与安全存储
发布时间:2026/10/15 12:48:18 锦皓数字建站

简介这是一份基于Python开发的银行管理系统完整源码专为计算机相关专业毕业设计、课程设计场景准备特别适合需要快速搭建可运行项目的中级学习者。资源包含主程序bank_main.py与工具模块bank_tooks.py配合bank.csv、userInfo.csv等数据文件覆盖账户管理、交易记录、用户信息维护等核心功能xml与iml为项目配置信息便于在PyCharm等IDE中直接导入运行。源码内部模块划分清晰主程序与工具函数分离数据结构与业务逻辑分层明确可帮助读者理解文件读写、CSV数据操作、菜单交互等常见Python编程要点。全部源码经严格测试环境配置后即可运行省略繁琐调试过程。压缩包共11个文件整体仅6KB轻量易部署。目前已有369人学习下载适合希望参考完整银行系统项目结构并在此基础上二次开发的毕业生或开发者。1. 一个“玩具”系统凭什么值得你写先说清这是什么“python实现银行管理系统源码.zip”这类标题在很多课程作业、简历项目清单和自学路径里都很常见。你可能会觉得一个控制台里跑菜单的银行系统太“玩具”但恰恰是这个体量的项目把 Python 里最值钱的三样东西一次全占了面向对象设计、文件持久化、异常与输入校验。比它更大的项目你写不动比它更小的项目学不到东西。这套系统要解决的真实问题也很具体怎么用纯 Python 模拟一个多账户的银行核心操作——开户、登录、存款、取款、转账、销户、余额查询并且做到重启程序后数据不丢、密码不裸奔、非法输入不崩溃。它适合三类人刚学完 Python 语法、想要一个“完整项目”练手的自学者需要交课程设计、又不想纯抄网上的现成代码的同学以及想通过一个通俗业务反向理解“对象模型”和“数据持久化”的初级开发者。后面所有章节都围绕这套系统的一条真实开发主线展开先拆需求再定类然后写代码最后踩坑和进阶。2. 从业务到类设计银行系统最怕把代码写成串行脚本2.1 业务需求拆解先把“银行要干什么”列成一张表再动手我见过不少同学拿到“银行管理系统”第一反应是直接开写结果写到一半发现存款和转账逻辑互相打架、销户不知道要不要清余额、密码不知道存什么格式。这种返工完全没必要因为银行系统哪怕再简化它的业务边界也是清晰的。你应该先站在“使用者”的角度把整个系统拆成角色和操作。先拆角色。一个最小可用的银行管理系统至少要有两类使用者普通客户和管理员。普通客户登录后只能操作自己的账户存款、取款、转账、查余额、改密码、销户。管理员则能查看全部账户列表、按账号检索账户、强制冻结或注销异常账户。很多初学者把这两类角色混在一个入口里导致权限校验写得乱七八糟。我的习惯是先画出角色-操作矩阵再动代码。操作清单按优先级排列开户客户填写姓名、密码、初始存款系统生成唯一账号。登录用账号 密码认证失败次数要有限制一般三次锁定本次会话。存款余额增加必须有金额 0 的校验。取款余额减少必须校验余额充足。转账从 A 账户扣款、B 账户入账两个操作必须同时成功或同时失败。销户账户余额必须为 0 才能销户否则提示先取款。管理员查询遍历账户列表按账号或姓名筛选。还有一组隐藏约束比操作本身更重要约束项规则原因余额任何时刻不为负资金类系统的基本底线账号全局唯一、数字格式、不可复用业务主键销户后也不能重新分配密码不能明文存储数据文件泄露时不能连带泄露密码金额单位内部用“分”存储展示时再换算避免浮点数误差文件写入不能写一半断电导致文件损坏数据是唯一的程序可以重写把这些约束先列出来后面代码就是按表实现不会东改一处西补一处。尤其是“金额用分存储”这一条几乎决定了这个项目后面会不会因为 0.1 0.2 的问题翻车第 5 章会专门展开。2.2 对象模型设计Account 与 Bank 各管什么、怎么分工业务拆清楚了类设计就顺理成章。这个系统核心就两个类Account账户和 Bank银行门面。Account 负责单账户的状态和行为Bank 负责账户集合、持久化和操作分发。很多初学者把这两个混成一个类结果是 Bank 里塞满了单个账户的业务方法代码一长就乱。Account 类的字段设计我一般固定为这几项字段类型说明account_idstr账号只读不修改namestr客户姓名password_hashstr密码哈希值不存原文balance_centsint余额单位是分不是元created_atstr开户时间ISO 格式is_activebool账户是否可用销户后置 False行为方法则严格对外暴露最小集合deposit(amount)、withdraw(amount)、transfer_to(target, amount)、change_password(old, new)、check_balance()。注意 deposit 和 withdraw 只做单账户的余额变更和合法性校验不涉及文件存储。文件读写是 Bank 的职责这样分层的好处是以后把存储从 JSON 换成数据库Account 类一行都不用改。Bank 类的职责更聚焦持有 accounts 字典key 是 account_idvalue 是 Account 对象提供 create_account、authenticate、get_account、save、load 等门面方法。账户数据的变更统一走 Bank 方法先改内存对象再统一触发 save。下面是 Bank 类和 Account 类的骨架设计先看结构第 3 章会给出可直接运行的完整实现class Account: def __init__(self, account_id, name, password_hash, balance_cents): self.account_id account_id self.name name self.password_hash password_hash self.balance_cents balance_cents self.is_active True def deposit(self, amount_cents: int): if amount_cents 0: raise ValueError(deposit amount must be positive) self.balance_cents amount_cents def withdraw(self, amount_cents: int): if amount_cents 0: raise ValueError(withdraw amount must be positive) if amount_cents self.balance_cents: raise ValueError(balance not enough) self.balance_cents - amount_cents这段骨架已经体现了两个关键设计getter 方法统一校验业务规则不直接暴露字段赋值金额类型是 int 而不是 float。最后的 ValueError 会被上层捕获并翻译成用户能看懂的中文提示而不是让程序直接崩溃。2.3 为什么不用函数式写法这个项目最大的设计点就在“状态”有一个问题值得停下来想清楚同样功能用函数 字典也能写为什么要用类因为银行系统的本质是“带状态的对象”账户余额是账户这个对象的内部状态操作天然依赖这个状态。你可以在一个全局字典里存 balance然后用函数去改它但这种写法有两个后果第一所有操作函数都要第一个参数传 account_id然后内部再查字典代码冗余第二账户的合法性校验比如 is_active散落在每个函数开头漏一处就出 bug。用类状态和行为绑定在一起Account 保证“任何合法操作后的余额一定不为负”这个不变量由类的内部逻辑保证外部调用方无法绕过。特别是转账场景如果转出和转入是同一个人操作同一个账户用函数式写法很容易出现先扣款后入账、中间抛异常导致余额丢失的情况。把扣款和入账封装成 Account 的方法后转账逻辑就是“先 withdraw再 deposit”顺序明确异常也不会跳过。类设计这一节没有多余动作。我见过一个模拟项目X最初用函数式写了 500 行后来重构成了 Account 和 Bank 两个类行数减到 350 行还多了销户和转账功能。行数本身不是目的重要的是业务规则的归属清晰了单个账户的规则在 Account系统级规则在 Bank。3. 核心代码落地账户操作与主循环怎么一次写对3.1 Account 类完整实现把校验逻辑封死在方法内部跳过骨架直接给出可以在本地跑通的 Account 类代码。这里把密码校验也放到 Account 里后面 Bank 登录时直接调用 verify_password避免 Bank 里去比对哈希的细节泄露。import hashlib import os import time class Account: def __init__(self, account_id, name, password, balance_cents, created_atNone): self.account_id account_id self.name name self.balance_cents balance_cents self.is_active True self.created_at created_at or time.strftime(%Y-%m-%d %H:%M:%S) # 密码加盐哈希salt 随机生成和哈希一起存储 salt os.urandom(16).hex() self.password_salt salt self.password_hash self._hash_password(password, salt) def _hash_password(self, password, salt): return hashlib.pbkdf2_hmac( sha256, password.encode(utf-8), bytes.fromhex(salt), 100_000 ).hex() def verify_password(self, password): return self._hash_password(password, self.password_salt) self.password_hash def deposit(self, amount_cents): if amount_cents 0: raise ValueError(存款金额必须为正) self.balance_cents amount_cents return self.balance_cents def withdraw(self, amount_cents): if amount_cents 0: raise ValueError(取款金额必须为正) if amount_cents self.balance_cents: raise ValueError(余额不足) self.balance_cents - amount_cents return self.balance_cents def change_password(self, old_password, new_password): if not self.verify_password(old_password): raise ValueError(原密码不正确) self.password_salt os.urandom(16).hex() self.password_hash self._hash_password(new_password, self.password_salt)执行逻辑说明init接收的是“分”为单位的余额创建时就直接计算密码哈希不保存任何明文。_hash_password 使用 PBKDF2 做 10 万次迭代这个参数是安全和性能的折中点10 万次在普通笔记本上每次校验大约几十毫秒完全可接受如果你追求更强的安全性可以调到 30 万次但注意老机器可能会慢到 200 毫秒以上。参数说明balance_cents 是整数且允许为 0不允许负数deposit 和 withdraw 里的 ValueError 字符串可以直接在 UI 层原样提示给用户减少一层翻译。change_password 里最容易被忽略的是换了新密码后必须重新生成 salt否则两个账户用相同密码会得到完全相同的哈希值容易被字典攻击。3.2 Bank 类账号生成、开户、登录与转账编排Bank 类是系统的门面它负责账号生成、开户落库、登录校验和转账编排。账号生成是最容易写错的地方有人用随机数可能碰撞有人用自增整数但重启后要从文件里恢复计数麻烦。我一般用时间戳的后 9 位加一个内存内自增序号拼成 12 位账号既保证同一次运行内不重复又保证格式统一。import json import os class Bank: def __init__(self, data_pathbank_data.json): self.data_path data_path self.accounts {} self._seq 0 # 内存内自增序号仅用于同一进程内避免账号碰撞 self.load() def _generate_account_id(self): self._seq 1 ts_part str(time.time_ns())[-9:] seq_part f{self._seq:03d} return ts_part seq_part def create_account(self, name, password, initial_balance_yuan): if initial_balance_yuan 0: raise ValueError(开户金额不能为负) balance_cents int(initial_balance_yuan * 100) account_id self._generate_account_id() acct Account(account_id, name, password, balance_cents) self.accounts[account_id] acct self.save() return account_id def authenticate(self, account_id, password): acct self.accounts.get(account_id) if acct is None or not acct.is_active: return None if acct.verify_password(password): return acct return None def transfer(self, from_id, to_id, amount_cents): src self.accounts.get(from_id) dst self.accounts.get(to_id) if src is None or dst is None: raise ValueError(转账账户不存在) # 先扣款再入账扣款成功但入账失败时回滚 src.withdraw(amount_cents) try: dst.deposit(amount_cents) except Exception: src.deposit(amount_cents) # 回滚 raise self.save()参数说明transfer 方法里 amount_cents 是整数“分”调用方在 UI 层拿到用户输入的“元”用 Decimal 转成“分”再传入。回滚逻辑很关键——如果 dst.deposit 因为某种原因抛异常必须把 src 的扣款补回去这一步不能省否则账就平不了。create_account 里 initial_balance_yuan 是浮点数元单位在入口处立即转成 int 分后续所有计算都不再出现浮点数。3.3 主循环与菜单一个 while 循环扛起整个系统的内外交互核心类写完后还需要一个入口把控制权交给用户。这个入口用最常见的 while True 菜单 input 分支实现。要注意的点是登录失败次数限制、转账对方账号不存在时的提示、以及所有输入都用 try/except 兜住避免用户输入一个非数字直接让程序崩掉。def main(): bank Bank(bank_data.json) while True: print(\n 银行管理系统 ) print(1. 开户 2. 登录 3. 管理员查询 4. 退出) try: choice input(请选择: ).strip() except (EOFError, KeyboardInterrupt): print(\n再见) break if choice 1: name input(客户姓名: ).strip() password input(设置密码: ).strip() try: init_yuan float(input(初始存款(元): ).strip()) if init_yuan 0: raise ValueError(开户金额不能为负数) acct_id bank.create_account(name, password, init_yuan) print(f开户成功账号是 {acct_id}请务必牢记) except ValueError as e: print(f开户失败: {e}) elif choice 2: acct_id input(账号: ).strip() password input(密码: ).strip() acct bank.authenticate(acct_id, password) if acct is None: print(账号或密码错误或账户已注销) continue print(f欢迎{acct.name}) while True: # 客户子菜单存款、取款、转账、查余额、改密码、销户、返回 print(1.存款 2.取款 3.转账 4.查余额 5.改密码 6.销户 7.退出登录) sub input(选择: ).strip() if sub 1: try: amount float(input(存款金额(元): ).strip()) cents int(round(amount * 100)) acct.deposit(cents) bank.save() print(f存款成功当前余额 {acct.balance_cents / 100:.2f} 元) except ValueError as e: print(f存款失败: {e}) elif sub 3: target_id input(转入账号: ).strip() amount float(input(转账金额(元): ).strip()) cents int(round(amount * 100)) try: bank.transfer(acct.account_id, target_id, cents) print(转账成功) except ValueError as e: print(f转账失败: {e}) elif sub 6: if acct.balance_cents ! 0: print(请先取清余额再销户) else: acct.is_active False bank.save() print(销户成功) break elif sub 7: break elif choice 3: for acc in bank.accounts.values(): status 正常 if acc.is_active else 已注销 print(f{acc.account_id} {acc.name} 余额:{acc.balance_cents/100:.2f}元 {status}) elif choice 4: bank.save() print(数据已保存) break这段循环最关键的两个设计点一是退出程序前主动调用 bank.save()把“数据是否持久化”的决定权交给流程而不是依赖文件自动写二是子菜单里的销户操作必须在循环内 break回到上一级菜单而不是把整个程序退出。如果你用 input() 直接读金额记得用 float() 包一层转换并用 ValueError 捕获非数字输入——这几乎是控制台项目崩溃的最高频原因。4. 持久化是项目分水岭JSON 存储的加载、保存与兜底4.1 三种存储方案对比为什么默认选 JSON 而不是 CSV 或文本银行管理系统绕不开一个核心问题程序退出后账号数据怎么保住常见的初学者方案有三种纯文本文件按行拼接、CSV、JSON。纯文本的毛病是字段分隔符容易和内容冲突姓名里如果有竖线就解析错位CSV 对嵌套结构不友好账户里存的是对象CSV 行只能展平。JSON 是这里的最优解Python 内置 json 模块直接支持字典序列化结构嵌套天然贴合 Account 对象的字段且读写都是标准的。JSON 方案在数据量上也有明显边界这个系统单文件存几千个账户毫无压力每个账户的 JSON 记录大约是几百字节一万个账户也就是几十 MB控制台程序完全扛得住。但如果你的系统未来要支持并发读写、多实例部署或者日增账户超过几万就应该换 SQLite。到第 6 章我会给替换方案但当前阶段 JSON 是对的。4.2 load 与 save 的完整实现原子写入解决文件写一半的隐患先定义存储文件的结构。accounts 是一个 JSON 对象key 是账号value 是 Account 对象的所有字段的字典表示{ 870030511234001: { account_id: 870030511234001, name: 张三, balance_cents: 12345, is_active: true, created_at: 2025-01-01 10:00:00, password_salt: a3f2..., password_hash: 9b4d2... } }下面是 Bank 类里完整的 load 和 save 实现注意我用了一个“写临时文件再原子替换”的技巧这是整个持久化章节最值钱的一段代码def save(self): # 先把数据写入临时文件再用 os.replace 原子替换正式文件 tmp_path self.data_path .tmp payload {} for acc_id, acct in self.accounts.items(): payload[acc_id] { account_id: acct.account_id, name: acct.name, balance_cents: acct.balance_cents, is_active: acct.is_active, created_at: acct.created_at, password_salt: acct.password_salt, password_hash: acct.password_hash, } with open(tmp_path, w, encodingutf-8) as f: json.dump(payload, f, ensure_asciiFalse, indent2) os.replace(tmp_path, self.data_path) def load(self): if not os.path.exists(self.data_path): return try: with open(self.data_path, r, encodingutf-8) as f: payload json.load(f) for acc_id, data in payload.items(): acct Account.__new__(Account) acct.account_id data[account_id] acct.name data[name] acct.balance_cents data[balance_cents] acct.is_active data[is_active] acct.created_at data[created_at] acct.password_salt data[password_salt] acct.password_hash data[password_hash] self.accounts[acc_id] acct except (json.JSONDecodeError, KeyError): # 文件损坏时备份坏文件不覆盖用户原文件 backup_path self.data_path .broken. time.strftime(%Y%m%d%H%M%S) os.replace(self.data_path, backup_path) print(f数据文件损坏已备份到 {backup_path}开始使用空数据)参数说明save 里的 ensure_asciiFalse 非常关键不写它的话“张三”会变成 \u5f20\u4e09虽然能读但没法人工排查indent2 是为了让文件可读方便你调试时直接用编辑器打开看。load 里遇到 JSON 损坏时我的处理是先把坏文件改名备份而不是原地删除——这样你能保留现场排查不至于数据彻底丢失后连原因都不知道。4.3 密码哈希存储明文密码是这类系统最常见的安全翻车点如果数据文件里存的是明文密码那这个系统等于没有锁。保存到 JSON 里的 password_hash 和 password_salt在登录时通过 Account.verify_password() 做比对用户输入的密码永远不落盘。这一点在 3.1 的代码里已经实现但我在实际维护这类项目时发现很多人在写 load 时会把密码字段直接存成明文因为他们觉得“反正本地程序没人在乎”。这里给一个判断标准如果系统用于自己学习写明文密码不算严重事故但如果这个代码被拿去交作业、进简历甚至部署给真实用户明文密码就是重大缺陷。第 6 章会给进一步升级方案但 PBKDF2 随机盐的形式已经是当前阶段性价比最高的安全投入——实现成本只有几行代码却能避免“数据文件一泄露、所有账户密码一起曝光”的惨剧。5. 常见问题与避坑排查资金类系统的五个翻车点5.1 金额精度翻车0.1 0.2 不等于 0.3现象存款 0.1 元两次后余额显示 0.30000000000000004 元或者取款 0.3 元时提示余额不足。原因Python 的 float 遵循 IEEE 754二进制无法精确表示 0.1 和 0.2累加后产生误差。银行系统只要涉及小数金额这个是必踩的坑。解决入口处把“元”乘以 100 转成整数“分”内部所有计算只用整数。上面代码里 create_account 和子菜单存款、取款、转账操作都是先 int(round(amount * 100)) 再运算展示时除以 100 格式化保留两位小数。注意 round 要用在内置函数上不要用 int(amount * 100)因为 0.29 * 100 在浮点数里可能是 28.999999999999996直接 int 截断就变成 28少了一分钱。5.2 数据文件写一半损坏程序断电后 JSON 加载失败现象程序在写文件过程中被强制终止下次启动时 load 抛出 JSONDecodeError系统无法启动。原因open write 是非原子操作写入过程中断会留下半截内容。特别是账户数据量增大后一次 json.dump 的时间变长写一半的概率明显上升。解决采用 4.2 里的临时文件 os.replace 原子替换方案。os.replace 在同一个文件系统内是原子的要么旧文件完整保留要么新文件完整生效不存在中间状态。load 里做的损坏检测 备份策略是第二道保险。5.3 账号重复生成时间戳后 9 位撞车现象两个账户在同毫秒内开户时账号完全一样后开户的覆盖了先开户的。原因只用 time.time_ns() 的后 9 位虽然纳秒级理论上很难重复但在循环创建多个账户时如果两个开户请求落在这个时间点的同一纳秒窗口后 9 位完全可能相同。解决在时间戳后面拼一个内存内自增序号保证同一进程内绝不重复。重启后 _seq 归于 1但因为时间戳部分已经变化跨进程碰撞概率极低。如果你想更严格可以在 load 时扫描现有账号把 _seq 设置为当前最大自增序号但实际项目中这个复杂度不太必要。5.4 密码明文落盘数据文件一打开全是密码现象打开 bank_data.json看到 password: 123456 直接暴露。原因写 save 方法时直接把 Account 对象的 password 属性序列化进去了没有做哈希转换。解决把 password 字段拆成 password_salt 和 password_hashAccount 的init里直接生成盐并计算哈希外部代码根本无法获取明文。load 时也不还原 password 属性只还原盐和哈希。5.5 菜单循环里输入非数字直接崩溃现象用户在主菜单输入 abc 或直接回车时程序抛出 ValueError 退出。原因input() 返回字符串int() 或 float() 转换失败的异常没有被捕获。解决每个 input 转换外都用 try/except ValueError 包一层提示“输入格式不正确请重新输入”。更彻底的做法是写一个通用的读输入函数比如 get_int(prompt) 内部循环直到输入合法数字再返回这样主循环清爽很多。我这里为了展示核心逻辑没有抽公共函数实际项目里建议抽出来。6. 从能跑到跑稳三个进阶方向和一组验证用例系统的核心功能跑通后往上走有三个收益最高的方向。第一个是换存储层把 JSON 换成 SQLite本质是改写 save 和 load 两个方法Account 类完全不用动这能直接验证你当初分层设计的好处。SQLite 的好处是支持并发读写、不需要手动 os.replace 防损坏但代价是引入 SQL 语句和 let 语句的拼接风险。第二个方向是操作日志。给 Bank 加一个 transactions 列表每次存款、取款、转账都记录一条时间、账户、金额、余额快照然后追加写入单独的日志文件。这是真实资金系统必备的能力而且实现成本很低对你理解“审计”这个需求有直接的帮助。第三个方向是单元测试。下面这组验证用例可以直接用 unittest 跑覆盖了系统最容易出 bug 的四个边界用例名输入期望结果验证点test_deposit_positive存款 100 分余额 100正常路径test_deposit_zero存款 0 分抛 ValueError零金额拦截test_withdraw_over_balance余额 100 取 200抛 ValueError余额不足拦截test_transfer_rollback转出账户余额不足双方余额都不变转账原子性最后说一个我自己的习惯每次写完一个类似的管理系统我都会做一次“删库重建”测试——删掉数据文件从零开始开户、存款、转账、销户然后重启程序确认数据还在、状态正确。这一步能暴露大部分持久化和初始化的问题比写十个功能快得多。当初我第一次写完银行系统时就是因为没测“数据文件不存在时启动”结果 load 方法里没做存在判断程序一启动就崩溃。后来所有项目我都先跑一遍冷启动流程。这个教训让我后来写任何带持久化的工具第一件事都是处理“文件不存在”和“文件已损坏”两个分支希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。