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