news 2026/10/29 6:21:42

python实现淘宝购物系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
python实现淘宝购物系统

前言


先说清楚一个前提:标题里的「淘宝购物系统」指的不是去对接淘宝,而是照着电商的通用业务模型,写一个自己的模拟商城。真实的淘宝开放平台需要商家资质、应用审核和官方授权接口才能读写交易数据,个人项目里没法也不应该去模仿它。所以本文实现的是一个教学用的、跑在本地的电商核心:商品、购物车、库存、下单、支付、取消。


第二个需要纠正的常见说法是「金额用浮点数存就行,反正只保留两位小数」。这是电商系统里最贵的一个坑。IEEE 754 双精度浮点数无法精确表示 0.1 这类十进制小数,0.1 + 0.2的结果是0.30000000000000004。金额一旦参与累加、打折、拆分,误差会累积,对账时就会凭空多出几分钱。Python 标准库给出的答案是decimal.Decimal,它做的是十进制运算,并且可以精确控制舍入。


第三个误解是「订单就是一张表,插一条记录就完了」。实际上订单是不可变的快照:商品当时叫什么名、卖多少钱,必须定格在订单里。如果订单只存 SKU,那么商家后天改价,昨天的订单金额会跟着变,账目直接错乱。


本文用纯标准库把这套模型跑通,不依赖任何第三方库,代码复制进.py文件即可执行。


一、领域模型:商品、购物车、订单


电商的核心其实就是三个对象之间的流转:




对象生命周期是否可变



商品 Product长期存在,库存不断变化可变(库存、价格)

购物车 Cart会话级,随时增删可变

订单 Order下单那一刻定格,之后只改状态内容不可变,状态可变



关键设计是「购物车 → 订单」这一步要做一次深拷贝式的固化:把Product展开成OrderItem,把当时的单价和名称复制进去。这样即便原商品后续改价、改名、甚至下架,历史订单仍然自洽。


订单状态用enum.Enum表达,而不是裸字符串。字符串写错一个字母不会报错,只会在比较时静默为假;枚举成员写错名字会立刻AttributeError。


二、金额必须用 Decimal


Decimal有三个必须记住的规矩:



  1. 从字符串构造。Decimal("19.90")得到精确的 19.90;Decimal(19.90)会先把 float 的二进制近似值原样搬进来,得到一长串奇怪的小数。

  2. 精度由上下文决定。默认上下文有 28 位有效数字,普通商品金额完全够用。

  3. 舍入方式要显式指定。round()和Decimal.quantize()的默认舍入都是ROUND_HALF_EVEN(银行家舍入),遇到.005时可能舍到偶数那一侧。要按「四舍五入」处理,必须传rounding=ROUND_HALF_UP。


# 适用于 Python 3.8+
from decimal import Decimal, ROUND_HALF_UP

print(Decimal("0.1") + Decimal("0.2")) # 0.3
print(0.1 + 0.2) # 0.30000000000000004

print(round(Decimal("2.5")), round(Decimal("3.5"))) # 2 4(银行家舍入)
print(Decimal("2.5").quantize(Decimal("1"), rounding=ROUND_HALF_UP)) # 3

quantize的第一个参数是目标刻度,Decimal("0.01")表示保留到分。


三、库存扣减要「先全部校验,再统一扣减」


下单最容易写错的地方是库存。如果边校验边扣减,第一件商品扣完发现第二件缺货再抛异常,第一件的库存就永远回不来了。正确顺序是两趟扫描:第一趟只检查,只要有任意一件不满足就直接失败;第二趟才真正扣减。这样整个过程要么全成功,要么什么都没动。


真实的多用户环境里,这段逻辑还必须放进数据库事务,并用UPDATE ... WHERE stock >= ?这种带条件的更新语句来避免超卖。本文的对象模型演示的是同一套「先校验后提交」的思路。


四、完整实现


# 适用于 Python 3.8+
from dataclasses import dataclass, field
from decimal import Decimal, ROUND_HALF_UP
from enum import Enum


class OrderStatus(Enum):
CREATED = "created"
PAID = "paid"
CANCELLED = "cancelled"


@dataclass
class Product:
sku: str
name: str
price: Decimal
stock: int = 0


@dataclass
class CartItem:
product: Product
qty: int


@dataclass
class Cart:
items: dict = field(default_factory=dict) # sku -> CartItem

def add(self, product, qty=1):
if qty <= 0:
raise ValueError("数量必须为正整数")
item = self.items.get(product.sku)
if item is None:
self.items[product.sku] = CartItem(product, qty)
else:
item.qty += qty

def remove(self, sku, qty=None):
item = self.items.get(sku)
if item is None:
return
if qty is None or qty >= item.qty:
del self.items[sku]
else:
item.qty -= qty

def total(self):
return sum((i.product.price * i.qty for i in self.items.values()),
Decimal("0.00"))

def clear(self):
self.items.clear()


@dataclass
class OrderItem:
sku: str
name: str
unit_price: Decimal
qty: int

@property
def subtotal(self):
return self.unit_price * self.qty


@dataclass
class Order:
order_id: str
user: str
items: list = field(default_factory=list)
discount: Decimal = Decimal("0.00")
status: OrderStatus = OrderStatus.CREATED

@property
def total(self):
amount = sum((i.subtotal for i in self.items), Decimal("0.00"))
amount -= self.discount
if amount < 0:
amount = Decimal("0.00")
return amount.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)


class Shop:
def __init__(self):
self.products = {} # sku -> Product
self.orders = {} # order_id -> Order
self._seq = 0

def add_product(self, sku, name, price, stock):
if sku in self.products:
raise ValueError("SKU 已存在: " + sku)
self.products[sku] = Product(sku, name, Decimal(str(price)), stock)

def _next_order_id(self):
self._seq += 1
return "OD{:08d}".format(self._seq)

def checkout(self, user, cart, discount=Decimal("0.00")):
if not cart.items:
raise ValueError("购物车是空的")

# 第一趟:全部校验,任何一件不够就整体失败
for item in cart.items.values():
if item.qty > item.product.stock:
raise ValueError("库存不足: " + item.product.name)

# 第二趟:统一扣减,并固化订单快照
for item in cart.items.values():
item.product.stock -= item.qty

order = Order(
order_id=self._next_order_id(),
user=user,
items=[OrderItem(i.product.sku, i.product.name,
i.product.price, i.qty)
for i in cart.items.values()],
discount=discount,
)
self.orders[order.order_id] = order
cart.clear()
return order

def pay(self, order_id):
order = self.orders[order_id]
if order.status is not OrderStatus.CREATED:
raise ValueError("当前状态不能支付: " + order.status.value)
order.status = OrderStatus.PAID
return order

def cancel(self, order_id):
order = self.orders[order_id]
if order.status is not OrderStatus.CREATED:
raise ValueError("只有未支付订单可以取消")
for item in order.items: # 回滚库存
self.products[item.sku].stock += item.qty
order.status = OrderStatus.CANCELLED
return order


def main():
shop = Shop()
shop.add_product("A001", "机械键盘", "399.00", 5)
shop.add_product("A002", "无线鼠标", "129.50", 2)

cart = Cart()
cart.add(shop.products["A001"], 1)
cart.add(shop.products["A002"], 2)
print("购物车合计:", cart.total())

order = shop.checkout("alice", cart)
print("订单", order.order_id, "金额", order.total, "状态", order.status.value)
print("键盘剩余库存:", shop.products["A001"].stock)

shop.pay(order.order_id)
print("支付后状态:", order.status.value)


if __name__ == "__main__":
main()

运行后会依次打印购物车合计 658.00、订单快照、剩余库存 4,以及支付后的状态paid。


常见坑点


1. 用float存金额


❌price = 19.9; total += price * 3→ 可能出现59.699999999999996✅price = Decimal("19.9"); total += price * 3→ 精确的59.7金额、利率、库存数量这类精确值一律走Decimal。


2. 用 float 构造 Decimal


❌Decimal(19.90)→ 得到19.89999999999999857891452847979962825775146484375✅Decimal("19.90")或Decimal(str(value))浮点字面量已经是近似值了,再转 Decimal 只是把误差搬了个家。


3.@dataclass里用可变默认值


❌@dataclass加items: dict = {}→ 抛ValueError: mutable default✅items: dict = field(default_factory=dict)就算普通类里写def __init__(self, items={}),所有实例也会共享同一个字典——这是经典的可变默认参数陷阱。


4. 订单里存 Product 的引用


❌Order(items=[cart.items[sku]])直接存CartItem(里面还握着Product) ✅ 展开成OrderItem,把名称和单价复制进去 否则商家改价后,昨天的订单金额会跟着变。


5. 边校验边扣库存


❌for item in ...: if stock < qty: raise; stock -= qty✅ 先整趟校验,再整趟扣减 扣到一半失败,前面已扣的库存就丢了。


6. 直接用round()做金额四舍五入


❌round(Decimal("2.5"))→2(ROUND_HALF_EVEN 银行家舍入) ✅Decimal("2.5").quantize(Decimal("1"), rounding=ROUND_HALF_UP)→3round()的舍入规则是实现中立的五分取偶,做财务计算必须显式指定。


7. 用字符串表示订单状态


❌order.status = "payed"拼错了也照样跑 ✅ 用OrderStatus.PAID;成员名写错立刻报错,值写错由枚举约束 枚举之间用is比较是官方推荐写法,比==更明确。


8. 用is比较金额或字符串


❌if total is Decimal("0")永远是False✅ 数值比较用==;is只用于None、True、False和枚举成员is比的是对象身份,两个相等的 Decimal 是两个不同对象。


总结




关注点错误做法正确做法



金额类型floatDecimal,从字符串构造

舍入方式直接用round()quantize(..., rounding=ROUND_HALF_UP)

订单内容存商品引用定格为OrderItem快照

库存扣减边校验边扣先整趟校验,再整趟扣减

订单状态裸字符串enum.Enum

可变默认值= {}/= []field(default_factory=...)



购物系统的骨架就是「商品 → 购物车 → 订单」这条流水线,真正决定它是否可用的,是金额用不用Decimal、订单是不是快照、库存有没有原子扣减这几个工程细节。把这三点做到位,再往上面加优惠券、拆单、退款,才不会把地基挖塌。




版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/29 6:20:23

Superpowers 效率工具实战:从安装到能力编排的自动化指南

1. 从“superpowers”这个标题说起&#xff1a;它到底是什么第一次看到“superpowers”这个词&#xff0c;很多人脑子里蹦出来的可能是超级英雄电影里的超能力&#xff0c;或者某个游戏里的技能系统。但如果你最近在技术社区、开源项目或者效率工具圈子里混&#xff0c;你会发现…

作者头像 李华
网站建设 2026/10/29 6:19:44

AI风格迁移技术解析:从《送别》到废土美学的跨模态创作

1. 项目背景与核心创意解析当AI绘画工具Midjourney生成的那张"废土版《送别》"概念图第一次出现在我的屏幕上时&#xff0c;锈迹斑斑的钢琴残骸与远处坍塌的钟楼形成的强烈视觉冲击&#xff0c;让我突然意识到&#xff1a;经典文艺作品与末世美学的碰撞&#xff0c;正…

作者头像 李华
网站建设 2026/10/29 6:19:44

Android广播丢失问题排查与Framework层优化实战

1. 项目背景与问题定位去年在开发一个企业级Android应用时&#xff0c;我们遇到了一个诡异的广播丢失问题&#xff1a;在特定机型上&#xff0c;部分系统广播&#xff08;如网络状态变化、电池电量变化&#xff09;会间歇性无法送达应用。这个问题直接影响了核心业务功能&#…

作者头像 李华
网站建设 2026/10/29 6:19:03

AI时代程序员的职业转型与核心技能重塑

1. 当AI开始写代码&#xff1a;程序员的职业危机与转机第一次看到GitHub Copilot自动补全出我想要的代码时&#xff0c;后背突然一阵发凉。那是个普通的周三下午&#xff0c;我正在写一个Python数据处理脚本&#xff0c;刚输入完函数名&#xff0c;整个函数体就像读心术般出现在…

作者头像 李华
网站建设 2026/10/29 6:19:01

Hermes Agent-main:目标驱动的自我迭代智能体框架

简介&#xff1a;Hermes Agent-main 是一款由 Nous Research 团队开源的自我进化型AI智能体框架&#xff0c;面向AI开发者、大模型学习者及本地AI助手搭建需求者&#xff0c;解决传统智能体记忆不可持续、技能难以复用、部署门槛高等核心痛点。资源包共2000个文件&#xff0c;以…

作者头像 李华
网站建设 2026/10/29 6:18:08

ponytail动效:从马尾辫物理学到可工程化的交互语法

1. “ponytail”不是技术术语&#xff0c;而是一把打开视觉语言解码器的钥匙最近在多个设计协作群、UI评审会和前端复盘现场&#xff0c;反复听到这个词被当作某种隐喻或代号使用——“这个动效得做成ponytail风格”&#xff0c;“图标组的节奏感要像ponytail一样甩起来”&…

作者头像 李华