手写点餐系统解决报错难题,面试必问实战
报错堆栈满屏红字,StackTrace 看得人头晕眼花,逻辑断点根本抓不住。这不仅是代码写崩了,更是思维没理清。很多转岗过来的朋友一写复杂业务就卡壳,其实这就是面试必问的底层逻辑缺失。
别慌,今天咱们不背八股文,直接手写一个极简但完整的点餐系统。
为什么选点餐?因为它是典型的 CRUD 加状态流转,能把你最头疼的“对象关系”、“异步回调”、“数据一致性”全串起来。
项目目标:不只是跑通,更要能讲清
咱们不做花里胡哨的前端界面,纯后端逻辑,用 Python 实现。为什么选 Python?语法接近伪代码,适合快速验证逻辑,且类型提示(Type Hints)能帮你在面试时展示工程化思维。
核心目标有三个:
- 解耦:菜单、订单、库存分离,别把逻辑全堆在
main.py里。 - 状态机:订单从“待支付”到“已完成”,状态变更必须受控。
- 异常处理:模拟真实场景,比如库存不足、并发超卖,看看你的代码怎么“体面”地报错,而不是直接崩溃。
痛点直击:
很多新手写代码,报错时只知道 Error: xxx,不知道是哪行代码触发的,更不知道是业务逻辑错了还是数据错了。咱们这次要做的,就是让错误信息“会说话”。
目录结构:像搭乐高一样组织代码
乱糟糟的单文件是调试噩梦。咱们采用标准的模块化结构。新建一个 order_system 文件夹,里面包含以下文件:
models.py:定义核心数据类(商品、订单)。service.py:核心业务逻辑(下单、扣库存、支付)。exceptions.py:自定义异常类,让报错更精准。main.py:入口文件,模拟用户操作。
order_system/
├── models.py
├── service.py
├── exceptions.py
└── main.py
设计原则:
- Models 只存数据,不存逻辑。
- Service 只处理流程,不直接操作数据库(这里用内存字典模拟数据库)。
- Exceptions 专门用来抛出业务错误,区分“系统崩了”和“业务不允许”。
核心代码实现:逐行拆解关键逻辑
1. 定义异常:让报错有“温度”
先写 exceptions.py。别用通用的 Exception,太粗了。
class OrderException(Exception):"""点餐系统基础异常"""passclass InsufficientStockError(OrderException):"""库存不足异常"""def __init__(self, product_name: str, requested: int, available: int):self.product_name = product_nameself.requested = requestedself.available = availablesuper().__init__(f"商品[{product_name}]库存不足: 需要{requested}, 仅有{available}")class PaymentFailedError(OrderException):"""支付失败异常"""pass
关键点:
在 __init__ 里把关键参数(商品名、数量)存下来。这样在日志里,你能一眼看出是哪个商品、缺多少货,而不是光秃秃的一句“库存错误”。
2. 定义模型:数据即真相
models.py 里,我们用 dataclass,简单又干净。
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, Listclass OrderStatus(Enum):PENDING = "pending" # 待支付PAID = "paid" # 已支付COMPLETED = "completed" # 已完成CANCELLED = "cancelled" # 已取消@dataclass
class Product:id: intname: strprice: floatstock: int@dataclass
class OrderItem:product: Productquantity: int@dataclass
class Order:id: intitems: List[OrderItem]status: OrderStatus = OrderStatus.PENDINGtotal_price: float = 0.0def calculate_total(self):"""计算总价,这是业务逻辑的一部分,放在模型里比较合理"""self.total_price = sum(item.product.price * item.quantity for item in self.items)return self.total_price
注意:
calculate_total 放在 Order 里,因为它只依赖订单内部数据。如果在 Service 里算,容易忘记更新状态。
3. 核心服务:逻辑的骨架
这是最容易出 Bug 的地方,也是面试必问的重灾区。service.py:
import uuid
from typing import Dict, List
from models import Product, Order, OrderItem, OrderStatus
from exceptions import InsufficientStockError, PaymentFailedErrorclass OrderService:def __init__(self):# 模拟数据库,Key: product_idself.product_db: Dict[int, Product] = {}# 模拟订单存储,Key: order_idself.order_db: Dict[str, Order] = {}self._order_counter = 0def add_product(self, product: Product):"""初始化菜单"""self.product_db[product.id] = productdef create_order(self, items: List[OrderItem]) -> Order:"""创建订单痛点:这里要检查库存,防止超卖"""# 1. 检查库存self._check_stock(items)# 2. 生成唯一IDself._order_counter += 1order_id = f"ORD{self._order_counter:06d}"# 3. 创建订单对象order = Order(id=self._order_counter, items=items)order.calculate_total()# 4. 持久化(模拟)self.order_db[order_id] = orderreturn orderdef _check_stock(self, items: List[OrderItem]):"""内部方法:校验库存"""for item in items:product = self.product_db.get(item.product.id)if not product:raise ValueError(f"商品ID {item.product.id} 不存在")if product.stock < item.quantity:# 抛出具体异常,带上详细信息raise InsufficientStockError(product.name, item.quantity, product.stock)def pay_order(self, order_id: str, amount: float) -> Order:"""支付订单痛点:状态流转 + 扣减库存 + 金额校验"""order = self.order_db.get(order_id)if not order:raise ValueError("订单不存在")# 状态机校验:只有待支付才能支付if order.status != OrderStatus.PENDING:raise PaymentFailedError(f"订单状态为{order.status.value},无法支付")# 金额校验:防止少付if abs(order.total_price - amount) > 0.01:raise PaymentFailedError(f"支付金额{amount}与订单金额{order.total_price}不符")# 更新状态order.status = OrderStatus.PAID# 扣减库存(注意:这里简化处理,真实场景需事务)for item in order.items:product = self.product_db[item.product.id]product.stock -= item.quantityreturn order
逐行讲解关键点:
_check_stock:在创建订单时就校验,而不是支付时。这叫“前置校验”,能尽早暴露问题。- 状态机:
if order.status != OrderStatus.PENDING这行代码救了无数人。如果没有它,用户可以重复支付,或者对已取消的订单进行支付,导致数据混乱。 - 精度问题:
abs(...) > 0.01,浮点数计算有精度问题,别用==比较金额,这是经典坑。
运行与测试:亲眼见证“报错”的艺术
main.py,模拟一个真实场景:
from models import Product, OrderItem
from service import OrderService
from exceptions import OrderExceptiondef run_demo():service = OrderService()# 1. 初始化菜单burger = Product(id=1, name="牛肉汉堡", price=25.0, stock=10)cola = Product(id=2, name="可乐", price=5.0, stock=100)service.add_product(burger)service.add_product(cola)print("--- 场景1: 正常下单 ---")try:items = [OrderItem(product=burger, quantity=2), OrderItem(product=cola, quantity=1)]order = service.create_order(items)print(f"订单创建成功: {order.id}, 总价: {order.total_price}")# 2. 支付service.pay_order(order.id, order.total_price)print(f"支付成功, 汉堡剩余库存: {burger.stock}")except OrderException as e:print(f"业务异常: {e}")except Exception as e:print(f"系统崩溃: {e}")print("\n--- 场景2: 库存不足 ---")try:# 尝试买11个汉堡,只剩8个items2 = [OrderItem(product=burger, quantity=11)]service.create_order(items2)except InsufficientStockError as e:# 这里捕获具体异常,打印详细信息print(f"详细错误: {e.product_name} - {e}")except Exception as e:print(f"其他错误: {e}")if __name__ == "__main__":run_demo()
运行结果:
--- 场景1: 正常下单 ---
订单创建成功: ORD000001, 总价: 55.0
支付成功, 汉堡剩余库存: 8--- 场景2: 库存不足 ---
详细错误: 牛肉汉堡 - 商品[牛肉汉堡]库存不足: 需要11, 仅有8
看到区别了吗? 第二种报错,直接告诉你哪个商品、需要多少、还剩多少。调试时,你不需要去翻代码猜,直接看日志就能定位。这就是自定义异常的价值。
优化扩展:从“能跑”到“能上线”
现在代码能跑了,但离生产环境还差得远。面试时,如果问到“如何优化”,你可以从以下几个角度展开,显得很有深度:
并发安全: 现在的代码是单线程的。如果两个用户同时买最后一个汉堡,
_check_stock通过后,product.stock -= 1可能都执行了,导致超卖。- 方案:加锁(
threading.Lock)或者用原子操作。在 Python 里,可以用queue或者数据库的行锁。 - 面试话术:“在高并发场景下,我会使用数据库的乐观锁(版本号)或悲观锁来保证库存扣减的原子性。”
- 方案:加锁(
持久化: 现在数据存在内存里,重启就没了。
- 方案:引入 SQLite 或 MySQL。将
Order和Product映射为数据库表。 - 重点:强调事务(Transaction)。支付和扣库存必须在同一个事务里,要么都成功,要么都回滚。
- 方案:引入 SQLite 或 MySQL。将
幂等性: 网络抖动,用户点了两次支付。
- 方案:引入支付流水号(Payment ID)。如果收到相同的支付请求,直接返回成功,不再重复处理。
日志系统: 别用
print。用logging模块。- 配置:区分 INFO(正常流程)、WARNING(潜在问题)、ERROR(业务异常)、CRITICAL(系统崩溃)。
- 结构化日志:输出 JSON 格式,方便 ELK 等日志平台检索。
小结:从报错到掌控
回顾一下,我们从一个“报错一堆看不懂”的状态,通过模块化、自定义异常、状态机控制,把一个点餐系统梳理得清清楚楚。
核心收获:
- 异常不是敌人:它是你理解系统边界的最佳工具。
- 状态必须受控:任何业务对象的状态流转,都要有明确的“门槛”。
- 代码要“会说话”:好的错误信息,能节省 50% 的调试时间。
转岗做开发,最忌讳的就是“黑盒思维”。不要觉得代码跑通了就行,要问自己:如果这里出错,我怎么知道?怎么恢复?怎么避免?
你在项目里踩过这个坑吗? 比如并发超卖、浮点数精度、或者状态流转死循环?评论区聊聊,咱们一起避坑。