feat(数据库): 新增第四阶段前两课教学内容

This commit is contained in:
zhiye.sun
2026-08-12 17:23:12 +08:00
parent f1d9548646
commit a28c3b3168
12 changed files with 1291 additions and 18 deletions

View File

@@ -0,0 +1,130 @@
# 第 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。