# 第 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。