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