4. 用户账户系统(下)——实现注册、登录、资金存取功能,数据用JSON文件持久化
好,咱们接着聊。上一章我们把用户账户的数据结构搭好了,也把JSON文件的读写封装成了工具函数。这一章,我们要让这个系统真正“活”起来——用户能注册、能登录、能存钱取钱。说白了,就是给我们的交易模拟器装上“门禁”和“金库”。
本章核心目标:实现一个完整的用户账户交互系统,所有数据通过JSON文件持久化存储。
4.1 注册功能——给新用户发“身份证”
注册的逻辑其实很简单:用户输入用户名和密码,我们检查一下有没有重名,没有就存进去。但这里有个坑——密码不能明文存。我在项目中遇到过用户密码泄露的事故,那叫一个惨烈。所以,咱们至少得用哈希处理一下。
Python自带的hashlib库就能搞定。我习惯用SHA-256,虽然不算最强,但对这个模拟器来说足够了。
import hashlib
import json
from pathlib import Path
DATA_DIR = Path("data")
DATA_DIR.mkdir(exist_ok=True)
USER_FILE = DATA_DIR / "users.json"
def load_users():
if USER_FILE.exists():
with open(USER_FILE, "r", encoding="utf-8") as f:
return json.load(f)
return {}
def save_users(users):
with open(USER_FILE, "w", encoding="utf-8") as f:
json.dump(users, f, indent=2, ensure_ascii=False)
def hash_password(password):
return hashlib.sha256(password.encode()).hexdigest()
def register(username, password):
users = load_users()
if username in users:
return False, "用户名已存在"
users[username] = {
"password": hash_password(password),
"balance": 0.0,
"created_at": "2025-01-01" # 实际项目用datetime
}
save_users(users)
return True, "注册成功"
我的小建议:注册成功后,最好直接返回用户信息,而不是只返回一个成功标志。这样前端拿到数据就能直接展示,省得再查一次。我在早期版本里就犯过这个傻,后来改了,舒服多了。
4.2 登录功能——验证你是谁
登录就是注册的逆过程。用户输入用户名和密码,我们把存着的哈希拿出来比一比。注意,比较的时候要用常量时间比较,防止时序攻击。不过咱们这个模拟器嘛,用==也没人黑你,但养成好习惯总没错。
def login(username, password):
users = load_users()
if username not in users:
return False, "用户不存在"
stored_hash = users[username]["password"]
if stored_hash != hash_password(password):
return False, "密码错误"
return True, users[username] # 返回用户数据
嗯,这里要注意:登录成功后,我们返回了整个用户字典。这样调用方可以直接拿到余额等信息,不用再读一次文件。效率嘛,就是一点点抠出来的。
4.3 资金存取——模拟器的“心脏”
存取款是交易模拟器的核心操作。存款简单,加钱就行。取款得检查余额够不够。我曾经在项目里忘了做余额检查,结果用户取款取成负数,整个账本都乱了。从那以后,我每次写取款逻辑都会先画个流程图。
来,先看看我画的这个核心逻辑图:
你看,流程其实不复杂。但就是这种“不复杂”的逻辑,最容易让人掉以轻心。我建议你写代码前,先在脑子里过一遍这个图。
def deposit(username, amount):
if amount <= 0:
return False, "存款金额必须大于0"
users = load_users()
if username not in users:
return False, "用户不存在"
users[username]["balance"] += amount
save_users(users)
return True, f"存款成功,当前余额:{users[username]['balance']}"
def withdraw(username, amount):
if amount <= 0:
return False, "取款金额必须大于0"
users = load_users()
if username not in users:
return False, "用户不存在"
if users[username]["balance"] < amount:
return False, "余额不足"
users[username]["balance"] -= amount
save_users(users)
return True, f"取款成功,当前余额:{users[username]['balance']}"
注意:这里有个并发问题。如果两个请求同时操作同一个账户,后一个保存会覆盖前一个。对于单机模拟器来说问题不大,但如果是生产环境,一定要加锁或使用数据库事务。我曾经在写一个抢票系统时吃过这个亏,数据全乱了,加班到凌晨三点才修好。
4.4 数据持久化——JSON文件的“读写平衡”
我们每次操作都调用了load_users()和save_users()。这意味着每次存取款都要读写一次文件。对于模拟器来说,用户量不大,完全够用。但如果用户量上来了,这种设计就是性能瓶颈。
我个人的做法是:在内存中维护一个用户字典,程序启动时加载,程序退出时保存。中间的所有操作都在内存里完成。这样快得多。但代价是——如果程序崩溃,未保存的数据就丢了。
你想想看,哪种方案更适合你的场景?对于教学模拟器,我建议用简单方案,也就是每次操作都读写文件。逻辑清晰,不容易出bug。
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 每次操作读写文件 | 简单、安全、不易丢数据 | 性能差,频繁IO | 教学、小规模使用 |
| 内存缓存+定时保存 | 性能好,响应快 | 实现复杂,可能丢数据 | 中等规模、可接受少量丢失 |
| 使用数据库 | 高性能、事务支持、并发安全 | 需要安装数据库、学习成本高 | 生产环境、大规模使用 |
4.5 整合测试——让代码跑起来
好了,所有功能都写好了。我们来写个简单的测试脚本,看看能不能跑通。
if __name__ == "__main__":
# 注册
ok, msg = register("alice", "123456")
print(msg) # 注册成功
# 重复注册
ok, msg = register("alice", "654321")
print(msg) # 用户名已存在
# 登录
ok, data = login("alice", "123456")
print(f"登录成功,余额:{data['balance']}") # 登录成功,余额:0.0
# 存款
ok, msg = deposit("alice", 1000)
print(msg) # 存款成功,当前余额:1000.0
# 取款
ok, msg = withdraw("alice", 500)
print(msg) # 取款成功,当前余额:500.0
# 取款超余额
ok, msg = withdraw("alice", 1000)
print(msg) # 余额不足
跑一下,应该能看到预期的输出。如果哪里报错了,多半是文件路径或者JSON格式的问题。检查一下data/users.json文件,看看内容是不是对的。
调试小技巧:我习惯在save_users()里加一行print(f"已保存 {len(users)} 个用户")。这样每次保存都能看到数据量,心里有底。调试完了再删掉,或者改成日志输出。
4.6 避坑指南——我踩过的那些坑
最后,分享几个我实际踩过的坑,希望能帮你省点时间:
- 文件编码问题:JSON文件一定要用
utf-8编码,否则中文用户名会乱码。我一开始用的系统默认编码,结果在Windows上跑得好好的,部署到Linux上全崩了。 - 路径问题:不要硬编码路径。用
pathlib或者os.path来构建路径。我曾经把数据文件放在代码目录下,结果换了台电脑就跑不起来了。 - 密码哈希:虽然我们用SHA-256,但实际项目中建议加盐(salt)。每个用户用不同的盐,这样即使数据库泄露,破解难度也大很多。
- 余额精度:浮点数有精度问题。0.1 + 0.2 不等于 0.3,这个你应该知道。对于金融系统,建议用
Decimal类型。不过模拟器嘛,float凑合用了。
好了,这一章的内容就到这里。代码不多,但都是实打实能用的。你把这些函数组合起来,一个简单的用户账户系统就成型了。下一章,我们会在这个基础上,加入交易记录功能——每一笔存取款、每一次买卖,都要留下痕迹。
公众号:蓝海资料掘金营,微信deep3321