131 lines
6.4 KiB
Python
131 lines
6.4 KiB
Python
# 第 4-2 课练习:使用事务完成安全转账
|
||
#
|
||
# 本文件只提供题目,不包含导入、代码骨架、测试数据或参考答案。
|
||
# 本练习会创建 course_wallet_account 表,并只操作 PRACTICE- 前缀的数据。
|
||
# 请勿把表名改成现有业务表,也不要删除不属于本练习的数据。
|
||
|
||
|
||
# 第一部分:准备配置和异常
|
||
# 1. 导入 Decimal、Path、tomllib 和 psycopg。
|
||
# 2. 从 psycopg 导入 Connection。
|
||
# 3. 使用 Path(__file__).with_name("config.toml") 定义 CONFIG_PATH。
|
||
# 4. 定义 WalletError(Exception),类体只写 pass。
|
||
# 5. 实现 load_database_config(config_path),要求与第一课相同:
|
||
# - 文件不存在时抛出明确的 RuntimeError;
|
||
# - 使用 tomllib 读取 [postgresql];
|
||
# - 配置节不是字典时抛出明确的 RuntimeError;
|
||
# - 返回数据库配置字典。
|
||
|
||
# 第二部分:定义 WalletRepository
|
||
# 1. __init__(self, connection) 保存 Connection,但不在 Repository 中创建连接。
|
||
# 2. create_table() 执行以下建表逻辑:
|
||
# - 表名 course_wallet_account;
|
||
# - wallet_no VARCHAR(30) 主键;
|
||
# - owner_name VARCHAR(50) 非空;
|
||
# - balance NUMERIC(12, 2) 非空并且大于等于 0;
|
||
# - 使用 CREATE TABLE IF NOT EXISTS。
|
||
# 3. reset_practice_wallets() 只删除 wallet_no LIKE 'PRACTICE-%' 的记录,
|
||
# 模式字符串必须作为参数传入,不得拼接 SQL。
|
||
# 4. add_wallets(wallets) 使用 with connection.cursor() 创建游标,
|
||
# 再调用 cursor.executemany() 批量新增。
|
||
# 5. find_balance_for_update(wallet_no) 使用参数化 SQL 和 FOR UPDATE:
|
||
# - 找不到时抛出 WalletError("钱包不存在:{wallet_no}");
|
||
# - 找到时返回 Decimal 余额。
|
||
# 6. change_balance(wallet_no, amount) 使用 balance = balance + %s 更新余额;
|
||
# - cursor.rowcount 不等于 1 时抛出钱包不存在异常;
|
||
# - 本方法不调用 commit() 或 rollback()。
|
||
# 7. find_practice_wallets() 查询 PRACTICE- 前缀记录,按 wallet_no 排序并返回结果。
|
||
#
|
||
# Repository 边界提醒:
|
||
# - Repository 负责 SQL 和结果转换;
|
||
# - 不要在 add_wallets()、change_balance() 内提交事务;
|
||
# - 多个 Repository 操作需要由外层业务事务统一提交或回滚。
|
||
|
||
# 第三部分:定义 WalletTransferService
|
||
# 1. __init__(self, repository) 保存 WalletRepository。
|
||
# 2. transfer(self, source_no, target_no, amount) 按顺序执行:
|
||
# - amount <= 0 时抛出 WalletError("转账金额必须大于 0。");
|
||
# - 调用 find_balance_for_update(source_no),保存 source_balance;
|
||
# - 调用 find_balance_for_update(target_no),确认目标钱包存在并锁定;
|
||
# - source_balance < amount 时抛出 WalletError("钱包余额不足。");
|
||
# - 调用 change_balance(source_no, -amount);
|
||
# - 调用 change_balance(target_no, amount)。
|
||
# 3. transfer() 不调用 commit() 或 rollback()。
|
||
|
||
# 第四部分:准备练习数据
|
||
# 1. 定义 prepare_data(database_config)。
|
||
# 2. 使用 with psycopg.connect(**database_config) as connection 管理事务。
|
||
# 3. 创建 WalletRepository(connection)。
|
||
# 4. 依次调用 create_table() 和 reset_practice_wallets()。
|
||
# 5. 调用 add_wallets() 批量新增:
|
||
# - ("PRACTICE-W001", "张三", Decimal("800.00"))
|
||
# - ("PRACTICE-W002", "李四", Decimal("300.00"))
|
||
# 6. 正常离开 with,让初始化事务自动提交。
|
||
|
||
# 第五部分:完成成功事务
|
||
# 1. 定义 run_successful_transfer(database_config)。
|
||
# 2. 使用一个连接上下文创建 Repository 和 Service。
|
||
# 3. 调用:
|
||
# service.transfer("PRACTICE-W001", "PRACTICE-W002", Decimal("150.00"))
|
||
# 4. 正常离开 with,不要手工调用 commit()。
|
||
# 5. 成功后两个余额应分别为 650.00 和 450.00。
|
||
|
||
# 第六部分:完成失败事务并验证回滚
|
||
# 1. 定义 run_failed_transfer(database_config)。
|
||
# 2. 在 try 中创建连接上下文和 Repository。
|
||
# 3. 先调用:
|
||
# repository.change_balance("PRACTICE-W001", Decimal("-50.00"))
|
||
# 4. 紧接着抛出 WalletError("模拟入账失败。")。
|
||
# 5. 在连接上下文外捕获 WalletError,并输出:
|
||
# “失败事务已回滚:模拟入账失败。”
|
||
# 6. 不要在 except 前捕获并吞掉异常,否则连接上下文无法自动回滚。
|
||
# 7. 回查后余额必须仍为 650.00 和 450.00,而不是 600.00 和 450.00。
|
||
|
||
# 第七部分:查询与 main() 输出
|
||
# 1. 定义 query_wallets(database_config),使用独立连接查询并返回练习钱包。
|
||
# 2. 定义 print_wallets(title, wallets),按以下格式逐行输出:
|
||
# “PRACTICE-W001|张三|余额:800.00”。
|
||
# 3. main() 依次调用:
|
||
# - load_database_config(CONFIG_PATH);
|
||
# - prepare_data(database_config);
|
||
# - print_wallets("初始余额:", query_wallets(database_config));
|
||
# - run_successful_transfer(database_config);
|
||
# - print_wallets("成功转账 150 元后:", query_wallets(database_config));
|
||
# - run_failed_transfer(database_config);
|
||
# - print_wallets("失败事务回滚后:", query_wallets(database_config))。
|
||
# 4. 分别捕获配置异常和 psycopg.Error,并输出中文场景说明。
|
||
# 5. 添加程序入口判断并调用 main()。
|
||
# 预期关键输出:
|
||
# 初始余额:
|
||
# PRACTICE-W001|张三|余额:800.00
|
||
# PRACTICE-W002|李四|余额:300.00
|
||
# 成功转账 150 元后:
|
||
# PRACTICE-W001|张三|余额:650.00
|
||
# PRACTICE-W002|李四|余额:450.00
|
||
# 失败事务已回滚:模拟入账失败。
|
||
# 失败事务回滚后:
|
||
# PRACTICE-W001|张三|余额:650.00
|
||
# PRACTICE-W002|李四|余额:450.00
|
||
|
||
|
||
# 自查清单:
|
||
# 1. Repository 是否复用外部传入的 Connection?
|
||
# 2. Repository 和 Service 是否都没有自行提交事务?
|
||
# 3. 扣款和入账是否处于同一个连接上下文?
|
||
# 4. 失败异常是否离开连接 with 后才被捕获?
|
||
# 5. 是否使用 FOR UPDATE 锁定待修改账户?
|
||
# 6. 批量初始化是否使用 executemany()?
|
||
# 7. 所有值是否通过参数化查询传入?
|
||
# 8. 是否只清理 PRACTICE- 前缀的练习数据?
|
||
|
||
|
||
# 最终验收标准:
|
||
# 1. practice.py 通过语法检查并能重复运行;
|
||
# 2. 初始化、成功转账和失败回滚的余额符合预期;
|
||
# 3. 失败事务没有保留第一条扣款更新;
|
||
# 4. Repository 只负责数据访问,Service 负责业务规则;
|
||
# 5. 外层连接上下文控制事务提交和回滚;
|
||
# 6. SQL 全部参数化,不拼接业务数据;
|
||
# 7. 只操作本课专用表和 PRACTICE- 前缀数据;
|
||
# 8. config.toml 与真实连接信息没有进入 Git。
|