179 lines
8.8 KiB
Python
179 lines
8.8 KiB
Python
# 第4-5课练习:数据库综合项目——库存订单管理
|
||
#
|
||
# 本文件只提供题目,不包含导入、代码骨架、测试数据代码或参考答案。
|
||
# 练习会创建course_shop_product、course_shop_order和course_shop_order_item三张表,
|
||
# 并只操作DBP-商品前缀及DBO-订单前缀的数据。
|
||
# 请勿改用现有业务表,也不要删除不属于本练习的数据。
|
||
|
||
|
||
# 第一部分:导入、配置与基础类型
|
||
# 1. 导入dataclass、Decimal、Path和tomllib。
|
||
# 2. 从sqlalchemy导入CheckConstraint、ForeignKey、Numeric、String、URL、
|
||
# create_engine、delete、func和select。
|
||
# 3. 从sqlalchemy.exc导入SQLAlchemyError。
|
||
# 4. 从sqlalchemy.orm导入DeclarativeBase、Mapped、Session、mapped_column、
|
||
# relationship、selectinload和sessionmaker。
|
||
# 5. 使用Path(__file__).with_name("config.toml")定义CONFIG_PATH。
|
||
# 6. 定义Base(DeclarativeBase)。
|
||
# 7. 定义OrderError(Exception),用于表达商品不存在、库存不足等业务失败。
|
||
# 8. 实现load_database_config(config_path),读取[postgresql]配置。
|
||
# 9. 实现create_database_url(database_config),使用URL.create()创建连接地址。
|
||
|
||
|
||
# 第二部分:定义三个ORM模型
|
||
# 1. 定义Product(Base),表名course_shop_product:
|
||
# - id:int主键;
|
||
# - product_code:最长30字符,唯一且非空;
|
||
# - product_name:最长100字符且非空;
|
||
# - price:NUMERIC(10, 2)且非空;
|
||
# - stock:int且非空;
|
||
# - 使用CheckConstraint保证price和stock都大于等于0;
|
||
# - items:与OrderItem建立双向一对多关系。
|
||
# 2. 定义Order(Base),表名course_shop_order:
|
||
# - id:int主键;
|
||
# - order_no:最长30字符,唯一且非空;
|
||
# - customer_name:最长100字符且非空;
|
||
# - total_amount:NUMERIC(12, 2)且非空;
|
||
# - status:最长20字符且非空;
|
||
# - items:与OrderItem建立双向一对多关系;
|
||
# - 配置cascade="all, delete-orphan"。
|
||
# 3. 定义OrderItem(Base),表名course_shop_order_item:
|
||
# - id:int主键;
|
||
# - order_id:外键引用course_shop_order.id,非空;
|
||
# - product_id:外键引用course_shop_product.id,非空;
|
||
# - quantity:int且非空,使用CheckConstraint保证大于0;
|
||
# - unit_price:NUMERIC(10, 2)且非空;
|
||
# - order:与Order.items互为双向关系;
|
||
# - product:与Product.items互为双向关系。
|
||
|
||
|
||
# 第三部分:定义DTO
|
||
# 1. 使用@dataclass(frozen=True)定义OrderDetailDTO。
|
||
# 2. DTO包含order_no、customer_name、product_code、product_name、quantity、
|
||
# unit_price、line_amount和status。
|
||
# 3. DTO不继承Base,不承担数据库持久化职责。
|
||
# 4. line_amount由查询结果中的unit_price乘以quantity得到。
|
||
|
||
|
||
# 第四部分:实现ProductRepository
|
||
# 1. 构造方法接收并保存外部传入的Session。
|
||
# 2. find_by_code_for_update(product_code):
|
||
# - 使用select(Product).where(...)查询商品;
|
||
# - 调用with_for_update()锁定商品行;
|
||
# - 找不到时抛出OrderError("商品不存在:{product_code}");
|
||
# - 返回Product对象。
|
||
# 3. find_practice_products()查询DBP-前缀商品并按商品编号排序。
|
||
# 4. Repository中不得创建Session,不得调用commit()或rollback()。
|
||
|
||
|
||
# 第五部分:实现OrderRepository
|
||
# 1. 构造方法接收并保存外部传入的Session。
|
||
# 2. exists_by_order_no(order_no)判断订单编号是否已经存在。
|
||
# 3. add(order)调用session.add(order),但不提交事务。
|
||
# 4. find_order_details()使用显式join查询订单、明细和商品:
|
||
# - 只查询DBO-前缀订单;
|
||
# - 只选择DTO所需字段;
|
||
# - 按订单编号和明细ID排序;
|
||
# - 把结果转换成OrderDetailDTO列表。
|
||
# 5. count_orders_by_status()使用func.count()和group_by()统计各状态订单数。
|
||
|
||
|
||
# 第六部分:实现OrderService下单业务
|
||
# 1. 构造方法接收ProductRepository和OrderRepository。
|
||
# 2. 定义place_order(order_no, customer_name, requests),其中requests是
|
||
# “商品编号、购买数量”组成的列表。
|
||
# 3. 订单编号已存在时抛出OrderError("订单已存在:{order_no}")。
|
||
# 4. requests为空时抛出OrderError("订单至少需要一项商品。")。
|
||
# 5. 逐项处理购买请求:
|
||
# - 数量小于等于0时抛出OrderError("购买数量必须大于0。");
|
||
# - 调用find_by_code_for_update()查询并锁定商品;
|
||
# - 库存不足时抛出OrderError("商品库存不足:{product_code}");
|
||
# - 商品库存减去购买数量;
|
||
# - 使用商品当前价格创建OrderItem;
|
||
# - 累加订单总金额。
|
||
# 6. 创建status="CREATED"的Order并关联全部OrderItem。
|
||
# 7. 调用OrderRepository.add(order),不在Service中提交事务。
|
||
|
||
|
||
# 第七部分:准备数据和验证事务
|
||
# 1. 定义reset_and_add_products(session):
|
||
# - 先删除DBO-前缀订单对应的订单明细;
|
||
# - 再删除DBO-前缀订单;
|
||
# - 最后删除DBP-前缀商品;
|
||
# - 新增DBP-001机械键盘,价格399.00,库存10;
|
||
# - 新增DBP-002无线鼠标,价格199.00,库存5;
|
||
# - 全过程不调用commit()。
|
||
# 2. 定义run_successful_order(session_factory):
|
||
# - 使用with session_factory.begin() as session管理事务;
|
||
# - 创建两个Repository和OrderService;
|
||
# - 创建订单DBO-001,客户张三,购买2个DBP-001和1个DBP-002;
|
||
# - 正常离开with,让事务自动提交;
|
||
# - 成功后键盘库存为8,鼠标库存为4,订单金额为997.00。
|
||
# 3. 定义run_failed_order(session_factory):
|
||
# - 在try中使用with session_factory.begin() as session;
|
||
# - 创建订单DBO-002,先购买1个DBP-001,再购买99个DBP-002;
|
||
# - 第二项因库存不足抛出OrderError;
|
||
# - 在事务with外捕获OrderError并输出失败信息;
|
||
# - 整个订单事务必须回滚,键盘库存仍为8,且DBO-002不能存在。
|
||
|
||
|
||
# 第八部分:输出和main()流程
|
||
# 1. 定义print_products(title, products),输出商品编号、名称、价格和库存。
|
||
# 2. 定义print_order_details(details),输出DTO中的订单和明细信息。
|
||
# 3. 定义print_order_counts(counts),输出“状态|订单数量:数字”。
|
||
# 4. main()依次执行:
|
||
# - 读取TOML配置并创建数据库URL;
|
||
# - 创建一次Engine并启用pool_pre_ping;
|
||
# - 使用sessionmaker(engine, expire_on_commit=False)创建Session工厂;
|
||
# - 调用Base.metadata.create_all(engine);
|
||
# - 在一个事务中重置数据并新增练习商品;
|
||
# - 输出初始库存;
|
||
# - 执行成功订单并输出扣减后的库存;
|
||
# - 执行失败订单并输出回滚后的库存;
|
||
# - 查询并输出订单DTO和状态统计;
|
||
# - 分类捕获配置异常、OrderError和SQLAlchemyError;
|
||
# - 在finally中调用engine.dispose()。
|
||
# 5. 添加程序入口判断并调用main()。
|
||
#
|
||
# 预期关键输出:
|
||
# 初始库存:
|
||
# DBP-001|机械键盘|价格:399.00|库存:10
|
||
# DBP-002|无线鼠标|价格:199.00|库存:5
|
||
# 成功订单提交后:
|
||
# DBP-001|机械键盘|价格:399.00|库存:8
|
||
# DBP-002|无线鼠标|价格:199.00|库存:4
|
||
# 失败订单已回滚:商品库存不足:DBP-002
|
||
# 失败订单回滚后:
|
||
# DBP-001|机械键盘|价格:399.00|库存:8
|
||
# DBP-002|无线鼠标|价格:199.00|库存:4
|
||
# 订单明细DTO:
|
||
# DBO-001|张三|DBP-001|机械键盘|数量:2|单价:399.00|小计:798.00|CREATED
|
||
# DBO-001|张三|DBP-002|无线鼠标|数量:1|单价:199.00|小计:199.00|CREATED
|
||
# 订单状态统计:
|
||
# CREATED|订单数量:1
|
||
|
||
|
||
# 自查清单:
|
||
# 1. 三个ORM模型是否建立了真实外键和双向对象关系?
|
||
# 2. 金额是否全部使用Decimal和NUMERIC,而不是float?
|
||
# 3. 商品查询是否使用FOR UPDATE锁定待扣减库存的记录?
|
||
# 4. Repository和Service是否都没有自行提交事务?
|
||
# 5. 一张订单的全部库存扣减和订单新增是否处于同一个事务?
|
||
# 6. 失败订单中第一项库存扣减是否也被回滚?
|
||
# 7. 订单明细查询是否使用join()并转换成DTO?
|
||
# 8. 状态统计是否由数据库完成count()和group_by()?
|
||
# 9. 程序是否只清理DBP-和DBO-前缀的练习数据?
|
||
# 10. 配置是否来自被Git忽略的本地config.toml?
|
||
|
||
|
||
# 最终验收标准:
|
||
# 1. practice.py通过语法检查并能连续运行两次;
|
||
# 2. 三张表的字段、约束、外键和关系映射正确;
|
||
# 3. 成功订单正确保存订单、明细并扣减库存;
|
||
# 4. 失败订单完全回滚,不保存订单且不改变任何库存;
|
||
# 5. DTO联表查询结果和订单状态统计符合预期;
|
||
# 6. Repository负责持久化,Service负责业务规则,调用方负责事务;
|
||
# 7. SQLAlchemy查询使用2.x写法,不使用session.query();
|
||
# 8. 所有练习数据与现有数据安全隔离;
|
||
# 9. config.toml与真实连接信息没有进入Git。
|