本文是《企业级自动化测试实战》系列的第 5 篇,也是 Python 基础篇中最关键的一篇。上一篇我们编写了 DataStore 数据存储层,已经提前接触了类的概念。本篇将系统讲解面向对象编程,然后用面向对象的方式为 MallLite 搭建完整的 FastAPI 后端。学完这篇,MallLite 就从一堆脚本变成了一个真正可运行的 Web 应用。
前言
自动化测试框架的核心设计模式——POM(Page Object Model),本质就是面向对象。每个页面是一个类,每个操作是类的方法。如果面向对象没学好,后面 POM 模式根本写不动。
同时,后续的 Playwright UI 测试和接口测试都需要一个可运行的目标系统。本篇会把 MallLite 后端搭起来,后续所有自动化测试都对着它打。
今天的任务:
- 理解面向对象的核心思想:类、对象、属性、方法
- 掌握 init 构造方法和 self 关键字
- 掌握继承机制
- 理解模块和包的导入方式
- 用面向对象的方式搭建 MallLite FastAPI 后端
- 搭建完整的 Web 页面(HTML 模板)
- 实现所有 API 接口和页面路由
一、为什么需要面向对象
1.1 用函数写代码的问题
假设我们要管理商品,用函数的方式:
# 用函数管理商品
def create_product(name, price, stock):
return {"name": name, "price": price, "stock": stock}
def get_product_info(product):
return f"{product['name']}(¥{product['price']})"
def is_in_stock(product):
return product["stock"] > 0
# 使用
p = create_product("iPhone", 8999, 100)
print(get_product_info(p))
看起来没什么问题,但如果商品越来越多,操作越来越复杂,函数会越来越多,而且"数据"和"操作"是分开的,不好管理。
1.2 用面向对象的方式
把数据和操作绑在一起:
class Product:
"""商品类"""
def __init__(self, name, price, stock):
"""构造方法:创建对象时自动调用"""
self.name = name
self.price = price
self.stock = stock
def get_info(self):
"""获取商品信息"""
return f"{self.name}(¥{self.price})"
def is_in_stock(self):
"""是否有库存"""
return self.stock > 0
def reduce_stock(self, quantity):
"""扣减库存"""
if quantity > self.stock:
print(f"库存不足:需要 {quantity},剩余 {self.stock}")
return False
self.stock -= quantity
return True
# 使用
p = Product("iPhone 15 Pro", 8999, 100)
print(p.get_info()) # iPhone 15 Pro(¥8999)
print(p.is_in_stock()) # True
p.reduce_stock(2)
print(p.stock) # 98
面向对象的核心优势:数据(属性)和操作(方法)绑在同一个对象里,代码更有组织性。
二、类和对象
2.1 什么是类和对象
类(Class)是蓝图,对象(Object)是根据蓝图造出来的实物。
| 类 | 建筑图纸 | class Product 定义了商品长什么样、能做什么 |
| 对象 | 根据图纸造出来的楼 | p = Product("iPhone", 8999, 100) 造出了一个具体的商品 |
| 属性 | 楼的特征(几层、面积) | p.name、p.price、p.stock |
| 方法 | 楼的功能(能住、能办公) | p.get_info()、p.reduce_stock() |
2.2 定义类和创建对象
class User:
"""用户类"""
def __init__(self, username, password, role="user"):
"""
构造方法
创建对象时自动调用,用于初始化属性
参数:
self: 代表对象自身(创建出来的那个实例)
username: 用户名
password: 密码
role: 角色(默认 "user")
"""
self.username = username
self.password = password
self.role = role
self.is_active = True
def get_display_name(self):
"""获取显示名称"""
return f"{self.username}({self.role})"
def check_password(self, password):
"""验证密码"""
return self.password == password
def deactivate(self):
"""停用账号"""
self.is_active = False
def __str__(self):
"""定义 print() 时的输出格式"""
return f"User({self.username}, role={self.role})"
# 创建对象(实例化)
admin = User("admin", "admin123", "admin")
guest = User("guest", "guest123")
# 访问属性
print(admin.username) # admin
print(admin.role) # admin
print(guest.role) # user(默认值)
# 调用方法
print(admin.get_display_name()) # admin(admin)
print(admin.check_password("123")) # False
print(admin.check_password("admin123")) # True
# print 对象
print(admin) # User(admin, role=admin)
2.3 self 是什么
self 是面向对象中最让新手困惑的概念。其实很简单:
class Dog:
def __init__(self, name):
self.name = name # 把传入的 name 存到对象身上
def bark(self):
print(f"{self.name}:汪汪!") # 从对象身上取出 name
a = Dog("旺财")
b = Dog("来福")
a.bark() # 旺财:汪汪! (self 指向 a)
b.bark() # 来福:汪汪! (self 指向 b)
self 就是"我自己"——哪个对象调用方法,self 就指向谁。你不用手动传它,Python 自动处理。
2.4 类变量 vs 实例变量
class Product:
# 类变量:所有对象共享
tax_rate = 0.13
def __init__(self, name, price):
# 实例变量:每个对象各自独立
self.name = name
self.price = price
def price_with_tax(self):
"""含税价格"""
return self.price * (1 + Product.tax_rate)
p1 = Product("iPhone", 8999)
p2 = Product("AirPods", 1899)
print(p1.price_with_tax()) # 10168.87
print(p2.price_with_tax()) # 2145.87
print(Product.tax_rate) # 0.13(通过类名访问类变量)
三、继承
3.1 为什么需要继承
假设 MallLite 有两种用户:普通用户和管理员。大部分行为相同,但管理员多了一些权限。
# 不用继承:大量重复代码
class NormalUser:
def __init__(self, username, password):
self.username = username
self.password = password
def login(self):
print(f"{self.username} 登录")
def browse_products(self):
print(f"{self.username} 浏览商品")
class AdminUser:
def __init__(self, username, password):
self.username = username # 重复
self.password = password # 重复
def login(self):
print(f"{self.username} 登录") # 重复
def browse_products(self):
print(f"{self.username} 浏览商品") # 重复
def manage_products(self): # 管理员独有
print(f"{self.username} 管理商品")
3.2 用继承消除重复
class BaseUser:
"""基础用户类(父类)"""
def __init__(self, username, password):
self.username = username
self.password = password
def login(self):
print(f"{self.username} 登录")
def browse_products(self):
print(f"{self.username} 浏览商品")
class AdminUser(BaseUser):
"""管理员用户(子类),继承 BaseUser"""
def manage_products(self):
print(f"{self.username} 管理商品")
def manage_orders(self):
print(f"{self.username} 管理订单")
class VipUser(BaseUser):
"""VIP 用户(子类),继承 BaseUser"""
def __init__(self, username, password, vip_level=1):
# 调用父类的 __init__
super().__init__(username, password)
self.vip_level = vip_level
def get_discount(self):
"""VIP 独有:获取折扣"""
discounts = {1: 0.95, 2: 0.90, 3: 0.85}
return discounts.get(self.vip_level, 1.0)
# 使用
admin = AdminUser("admin", "admin123")
admin.login() # admin 登录(继承自 BaseUser)
admin.browse_products() # admin 浏览商品(继承自 BaseUser)
admin.manage_products() # admin 管理商品(AdminUser 自己的)
vip = VipUser("vip001", "vip123", vip_level=2)
vip.login() # vip001 登录(继承自 BaseUser)
print(vip.get_discount()) # 0.9(VipUser 自己的)
3.3 方法重写
子类可以重写(覆盖)父类的方法:
class BaseUser:
def __init__(self, username, password):
self.username = username
self.password = password
def get_role_info(self):
return f"用户:{self.username},角色:普通用户"
class AdminUser(BaseUser):
def get_role_info(self):
"""重写父类方法"""
return f"用户:{self.username},角色:管理员(拥有全部权限)"
admin = AdminUser("admin", "admin123")
print(admin.get_role_info()) # 用户:admin,角色:管理员(拥有全部权限)
3.4 继承关系在 POM 中的应用
这是后续 Playwright 自动化框架的核心结构:
# 后面 Playwright POM 模式的核心结构就是这样:
class BasePage:
"""所有页面的基类"""
def __init__(self, page):
self.page = page
def click(self, locator):
self.page.click(locator)
def fill(self, locator, text):
self.page.fill(locator, text)
def get_text(self, locator):
return self.page.text_content(locator)
class LoginPage(BasePage):
"""登录页面,继承 BasePage"""
def login(self, username, password):
self.fill("#username", username)
self.fill("#password", password)
self.click("#login-btn")
class CartPage(BasePage):
"""购物车页面,继承 BasePage"""
def get_total_price(self):
return self.get_text(".total-price")
四、模块和包
4.1 什么是模块
一个 .py 文件就是一个模块。把代码分散到多个文件中,就是模块化。
mall-lite/
├── models.py ← 一个模块
├── services.py ← 一个模块
└── main.py ← 一个模块
4.2 import 导入方式
# 方式一:导入整个模块
import json
json.load(f)
# 方式二:从模块中导入指定内容
from json import load, dump
load(f)
# 方式三:导入并起别名
import json as j
j.load(f)
# 方式四:导入全部(不推荐,容易命名冲突)
from json import *
4.3 从自己的文件中导入
假设项目结构:
mall-lite/
├── models.py
├── services.py
└── main.py
在 main.py 中导入其他文件:
# 从 models.py 导入 Product 类
from models import Product, User
# 从 services.py 导入 ProductService
from services import ProductService
4.4 包(Package)
当模块太多时,用文件夹把它们组织起来,就是包。包里面必须有一个 __init__.py 文件(可以是空的)。
mall-lite/
├── app/ ← 这是一个包
│ ├── __init__.py ← 标识这是一个包(可以为空)
│ ├── models.py ← 包内的模块
│ ├── services.py
│ └── routers/
│ ├── __init__.py
│ ├── user.py
│ ├── product.py
│ └── order.py
└── main.py
导入包内的模块:
from app.models import Product
from app.services import ProductService
from app.routers.user import user_router
五、动手实战:搭建 MallLite FastAPI 后端
接下来用面向对象的方式,把 MallLite 从一个脚本集合变成一个真正的 Web 应用。
5.1 安装 FastAPI
pip install uvicorn jinja2 python-multipart
pip install fastapi==0.109.0
各依赖的用途:
| fastapi | Web 框架,处理 HTTP 请求 |
| uvicorn | ASGI 服务器,运行 FastAPI 应用 |
| jinja2 | HTML 模板引擎,渲染页面 |
| python-multipart | 处理表单提交(登录表单等) |
5.2 最终项目结构
mall-lite/
├── app/
│ ├── __init__.py
│ ├── main.py ← FastAPI 入口
│ ├── models.py ← 数据模型类
│ ├── data_store.py ← 数据存储层(P04 的改进版)
│ └── routers/
│ ├── __init__.py
│ ├── pages.py ← 页面路由(返回 HTML)
│ └── api.py ← API 路由(返回 JSON)
├── templates/ ← HTML 模板
│ ├── base.html
│ ├── home.html
│ ├── login.html
│ ├── product_detail.html
│ ├── cart.html
│ ├── orders.html
│ └── profile.html
├── static/
│ └── css/
│ └── style.css
├── data/
│ ├── products.json
│ ├── categories.json
│ ├── users.json
│ ├── orders.json
│ ├── carts.json
│ └── config.json
├── run.py ← 启动脚本
└── requirements.txt
5.3 数据模型类 – app/models.py
"""
MallLite 数据模型
定义系统中所有数据对象的类
"""
from datetime import datetime
class User:
"""用户模型"""
def __init__(self, id, username, password, phone="", email="", role="user", nickname=""):
self.id = id
self.username = username
self.password = password
self.phone = phone
self.email = email
self.role = role
self.nickname = nickname or username
def to_dict(self, include_password=False):
"""转为字典(用于 JSON 响应)"""
data = {
"id": self.id,
"username": self.username,
"phone": self.phone,
"email": self.email,
"role": self.role,
"nickname": self.nickname,
}
if include_password:
data["password"] = self.password
return data
def check_password(self, password):
return self.password == password
@staticmethod
def from_dict(data):
"""从字典创建 User 对象"""
return User(**data)
class Product:
"""商品模型"""
def __init__(self, id, name, price, category, stock, description="", is_new=False, rating=0.0):
self.id = id
self.name = name
self.price = price
self.category = category
self.stock = stock
self.description = description
self.is_new = is_new
self.rating = rating
def to_dict(self):
return {
"id": self.id,
"name": self.name,
"price": self.price,
"category": self.category,
"stock": self.stock,
"description": self.description,
"is_new": self.is_new,
"rating": self.rating,
}
def is_in_stock(self, quantity=1):
return self.stock >= quantity
def reduce_stock(self, quantity):
if not self.is_in_stock(quantity):
return False
self.stock -= quantity
return True
@staticmethod
def from_dict(data):
return Product(**data)
class CartItem:
"""购物车项"""
def __init__(self, product_id, name, price, quantity):
self.product_id = product_id
self.name = name
self.price = price
self.quantity = quantity
@property
def subtotal(self):
return self.price * self.quantity
def to_dict(self):
return {
"product_id": self.product_id,
"name": self.name,
"price": self.price,
"quantity": self.quantity,
"subtotal": self.subtotal,
}
class Order:
"""订单模型"""
def __init__(self, order_no, user_id, items, total_price, status="待支付"):
self.order_no = order_no
self.user_id = user_id
self.items = items # list of CartItem
self.total_price = total_price
self.status = status
self.created_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.updated_at = self.created_at
def to_dict(self):
return {
"order_no": self.order_no,
"user_id": self.user_id,
"items": [item.to_dict() if isinstance(item, CartItem) else item for item in self.items],
"total_price": self.total_price,
"total_quantity": sum(
item.quantity if isinstance(item, CartItem) else item["quantity"]
for item in self.items
),
"item_count": len(self.items),
"status": self.status,
"created_at": self.created_at,
"updated_at": self.updated_at,
}
def pay(self):
if self.status != "待支付":
return False
self.status = "已支付"
self.updated_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return True
def cancel(self):
if self.status in ["已取消", "已完成"]:
return False
self.status = "已取消"
self.updated_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return True
5.4 数据存储层 – app/data_store.py
"""
MallLite DataStore – 改进版
在 P04 基础上改为面向对象模型,内部使用 Product / User / Order 等类
"""
import json
import os
from pathlib import Path
from datetime import datetime
from app.models import User, Product, CartItem, Order
class DataStore:
"""数据存储层"""
def __init__(self, data_dir="data"):
self.data_dir = Path(data_dir)
self.data_dir.mkdir(parents=True, exist_ok=True)
self._files = {
"products": self.data_dir / "products.json",
"categories": self.data_dir / "categories.json",
"users": self.data_dir / "users.json",
"orders": self.data_dir / "orders.json",
"carts": self.data_dir / "carts.json",
}
# 初始化空文件
for name, filepath in self._files.items():
if not filepath.exists() and name in ["orders", "carts"]:
self._write_json(filepath, [] if name == "orders" else {})
self._order_counter = 0
# ====== 底层文件操作 ======
def _read_json(self, filepath):
filepath = Path(filepath)
if not filepath.exists():
return None
try:
with open(filepath, "r", encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, PermissionError) as e:
print(f"读取文件失败 {filepath}: {e}")
return None
def _write_json(self, filepath, data):
filepath = Path(filepath)
filepath.parent.mkdir(parents=True, exist_ok=True)
try:
with open(filepath, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
except (PermissionError, TypeError) as e:
print(f"写入文件失败 {filepath}: {e}")
# ====== 用户操作 ======
def get_users(self):
data = self._read_json(self._files["users"]) or []
return [User.from_dict(u) for u in data]
def get_user_by_username(self, username):
for user in self.get_users():
if user.username == username:
return user
return None
def verify_login(self, username, password):
if not username:
return {"success": False, "message": "请输入用户名", "user": None}
if not password:
return {"success": False, "message": "请输入密码", "user": None}
user = self.get_user_by_username(username)
if user is None:
return {"success": False, "message": "用户不存在", "user": None}
if not user.check_password(password):
return {"success": False, "message": "密码错误", "user": None}
return {"success": True, "message": "登录成功", "user": user.to_dict()}
# ====== 商品操作 ======
def get_products(self):
data = self._read_json(self._files["products"]) or []
return [Product.from_dict(p) for p in data]
def get_product_by_id(self, product_id):
for p in self.get_products():
if p.id == product_id:
return p
return None
def search_products(self, keyword):
keyword_lower = keyword.lower()
return [
p for p in self.get_products()
if keyword_lower in p.name.lower()
or keyword_lower in p.description.lower()
]
def get_products_by_category(self, category):
return [p for p in self.get_products() if p.category == category]
# ====== 分类操作 ======
def get_categories(self):
return self._read_json(self._files["categories"]) or []
# ====== 购物车操作 ======
def get_cart(self, user_id):
carts = self._read_json(self._files["carts"]) or {}
cart_data = carts.get(str(user_id), [])
return [CartItem(**item) for item in cart_data]
def add_to_cart(self, user_id, product_id, quantity=1):
product = self.get_product_by_id(product_id)
if product is None:
return {"success": False, "message": "商品不存在"}
if not product.is_in_stock(quantity):
return {"success": False, "message": f"{product.name} 库存不足"}
carts = self._read_json(self._files["carts"]) or {}
user_cart = carts.get(str(user_id), [])
for item in user_cart:
if item["product_id"] == product_id:
item["quantity"] += quantity
carts[str(user_id)] = user_cart
self._write_json(self._files["carts"], carts)
return {"success": True, "message": f"{product.name} 数量更新为 {item['quantity']}"}
user_cart.append({
"product_id": product_id,
"name": product.name,
"price": product.price,
"quantity": quantity,
})
carts[str(user_id)] = user_cart
self._write_json(self._files["carts"], carts)
return {"success": True, "message": f"{product.name} 已加入购物车"}
def update_cart_quantity(self, user_id, product_id, quantity):
carts = self._read_json(self._files["carts"]) or {}
user_cart = carts.get(str(user_id), [])
for item in user_cart:
if item["product_id"] == product_id:
if quantity <= 0:
user_cart.remove(item)
else:
item["quantity"] = quantity
carts[str(user_id)] = user_cart
self._write_json(self._files["carts"], carts)
return {"success": True, "message": "购物车已更新"}
return {"success": False, "message": "商品不在购物车中"}
def remove_from_cart(self, user_id, product_id):
return self.update_cart_quantity(user_id, product_id, 0)
def clear_cart(self, user_id):
carts = self._read_json(self._files["carts"]) or {}
carts[str(user_id)] = []
self._write_json(self._files["carts"], carts)
def get_cart_total(self, user_id):
cart = self.get_cart(user_id)
total = sum(item.price * item.quantity for item in cart)
count = sum(item.quantity for item in cart)
return {"total_price": total, "total_quantity": count, "item_count": len(cart)}
# ====== 订单操作 ======
def get_orders(self):
data = self._read_json(self._files["orders"]) or []
return data
def get_order_by_no(self, order_no):
for order in self.get_orders():
if order["order_no"] == order_no:
return order
return None
def create_order(self, user_id, cart_items):
if not cart_items:
return {"success": False, "message": "购物车为空", "order": None}
self._order_counter += 1
date_str = datetime.now().strftime("%Y%m%d")
order_no = f"ORD{date_str}–{str(self._order_counter).zfill(6)}"
order_items = []
total_price = 0
for item in cart_items:
product = self.get_product_by_id(item.product_id)
if product is None:
return {"success": False, "message": f"商品不存在", "order": None}
if not product.is_in_stock(item.quantity):
return {"success": False, "message": f"{product.name} 库存不足", "order": None}
subtotal = product.price * item.quantity
order_items.append({
"product_id": product.id,
"name": product.name,
"price": product.price,
"quantity": item.quantity,
"subtotal": subtotal,
})
total_price += subtotal
product.reduce_stock(item.quantity)
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
order = {
"order_no": order_no,
"user_id": user_id,
"items": order_items,
"total_price": total_price,
"total_quantity": sum(i["quantity"] for i in order_items),
"item_count": len(order_items),
"status": "待支付",
"created_at": now,
"updated_at": now,
}
orders = self.get_orders()
orders.append(order)
self._write_json(self._files["orders"], orders)
# 保存更新后的库存
self._write_json(self._files["products"], [p.to_dict() for p in self.get_products()])
# 清空购物车
self.clear_cart(user_id)
return {"success": True, "message": f"订单 {order_no} 创建成功", "order": order}
def update_order_status(self, order_no, new_status):
orders = self.get_orders()
for order in orders:
if order["order_no"] == order_no:
order["status"] = new_status
order["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self._write_json(self._files["orders"], orders)
return True
return False
def get_orders_by_user(self, user_id):
return [o for o in self.get_orders() if o["user_id"] == user_id]
5.5 API 路由 – app/routers/api.py
"""
MallLite API 路由
提供 RESTful JSON 接口,供自动化测试调用
"""
from fastapi import APIRouter, Request, HTTPException
router = APIRouter(prefix="/api")
def get_store(request: Request):
"""从 app state 获取 DataStore 实例"""
return request.app.state.store
# ====== 用户接口 ======
@router.post("/login")
async def api_login(request: Request):
body = await request.json()
store = get_store(request)
result = store.verify_login(body.get("username", ""), body.get("password", ""))
if not result["success"]:
return {"code": 401, "message": result["message"], "data": None}
return {"code": 200, "message": result["message"], "data": result["user"]}
@router.post("/register")
async def api_register(request: Request):
body = await request.json()
store = get_store(request)
username = body.get("username", "")
password = body.get("password", "")
if not username or not password:
return {"code": 400, "message": "用户名和密码不能为空", "data": None}
if store.get_user_by_username(username):
return {"code": 400, "message": "用户名已存在", "data": None}
return {"code": 200, "message": "注册成功", "data": {"username": username}}
# ====== 商品接口 ======
@router.get("/products")
async def api_products(request: Request, page: int = 1, page_size: int = 10,
category: str = None, keyword: str = None):
store = get_store(request)
if keyword:
products = store.search_products(keyword)
elif category:
products = store.get_products_by_category(category)
else:
products = store.get_products()
total = len(products)
start = (page – 1) * page_size
items = [p.to_dict() for p in products[start:start + page_size]]
return {
"code": 200,
"data": {
"total": total,
"page": page,
"page_size": page_size,
"total_pages": (total + page_size – 1) // page_size,
"items": items,
}
}
@router.get("/products/search")
async def api_search(request: Request, q: str = ""):
store = get_store(request)
if not q:
return {"code": 200, "data": {"items": [], "total": 0}}
results = store.search_products(q)
return {
"code": 200,
"data": {"items": [p.to_dict() for p in results], "total": len(results)}
}
@router.get("/products/{product_id}")
async def api_product_detail(request: Request, product_id: int):
store = get_store(request)
product = store.get_product_by_id(product_id)
if product is None:
raise HTTPException(status_code=404, detail="商品不存在")
return {"code": 200, "data": product.to_dict()}
@router.get("/categories")
async def api_categories(request: Request):
store = get_store(request)
return {"code": 200, "data": store.get_categories()}
# ====== 购物车接口 ======
@router.get("/cart")
async def api_get_cart(request: Request, user_id: int = 2):
store = get_store(request)
cart = store.get_cart(user_id)
summary = store.get_cart_total(user_id)
return {
"code": 200,
"data": {
"items": [item.to_dict() for item in cart],
**summary,
}
}
@router.post("/cart")
async def api_add_to_cart(request: Request):
body = await request.json()
store = get_store(request)
result = store.add_to_cart(
user_id=body.get("user_id", 2),
product_id=body.get("product_id"),
quantity=body.get("quantity", 1),
)
code = 200 if result["success"] else 400
return {"code": code, "message": result["message"]}
@router.put("/cart/{product_id}")
async def api_update_cart(request: Request, product_id: int):
body = await request.json()
store = get_store(request)
result = store.update_cart_quantity(
user_id=body.get("user_id", 2),
product_id=product_id,
quantity=body.get("quantity", 1),
)
code = 200 if result["success"] else 400
return {"code": code, "message": result["message"]}
@router.delete("/cart/{product_id}")
async def api_remove_from_cart(request: Request, product_id: int, user_id: int = 2):
store = get_store(request)
result = store.remove_from_cart(user_id, product_id)
code = 200 if result["success"] else 400
return {"code": code, "message": result["message"]}
# ====== 订单接口 ======
@router.get("/orders")
async def api_orders(request: Request, user_id: int = 2, status: str = None):
store = get_store(request)
orders = store.get_orders_by_user(user_id)
if status:
orders = [o for o in orders if o["status"] == status]
return {"code": 200, "data": {"items": orders, "total": len(orders)}}
@router.post("/orders")
async def api_create_order(request: Request):
body = await request.json()
store = get_store(request)
user_id = body.get("user_id", 2)
cart = store.get_cart(user_id)
result = store.create_order(user_id, cart)
code = 200 if result["success"] else 400
return {"code": code, "message": result["message"], "data": result.get("order")}
@router.get("/orders/{order_no}")
async def api_order_detail(request: Request, order_no: str):
store = get_store(request)
order = store.get_order_by_no(order_no)
if order is None:
raise HTTPException(status_code=404, detail="订单不存在")
return {"code": 200, "data": order}
@router.put("/orders/{order_no}/cancel")
async def api_cancel_order(request: Request, order_no: str):
store = get_store(request)
success = store.update_order_status(order_no, "已取消")
if success:
return {"code": 200, "message": f"订单 {order_no} 已取消"}
return {"code": 400, "message": "取消失败"}
5.6 页面路由 – app/routers/pages.py
"""
MallLite 页面路由
返回 HTML 页面,供 Playwright UI 测试使用
"""
from fastapi import APIRouter, Request, Form
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from pathlib import Path
router = APIRouter()
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent.parent / "templates"))
def get_store(request: Request):
return request.app.state.store
# ====== 首页 ======
@router.get("/", response_class=HTMLResponse)
async def home(request: Request, page: int = 1, category: str = None, keyword: str = None):
store = get_store(request)
if keyword:
products = store.search_products(keyword)
elif category:
products = store.get_products_by_category(category)
else:
products = store.get_products()
page_size = 8
total = len(products)
total_pages = max((total + page_size – 1) // page_size, 1)
page = max(1, min(page, total_pages))
start = (page – 1) * page_size
page_products = products[start:start + page_size]
categories = store.get_categories()
return templates.TemplateResponse("home.html", {
"request": request,
"products": page_products,
"categories": categories,
"current_category": category,
"keyword": keyword or "",
"page": page,
"total_pages": total_pages,
"total": total,
})
# ====== 登录页 ======
@router.get("/login", response_class=HTMLResponse)
async def login_page(request: Request, error: str = None):
return templates.TemplateResponse("login.html", {
"request": request,
"error": error,
})
@router.post("/login", response_class=HTMLResponse)
async def login_submit(request: Request, username: str = Form(...), password: str = Form(...)):
store = get_store(request)
result = store.verify_login(username, password)
if result["success"]:
response = RedirectResponse("/", status_code=302)
response.set_cookie("username", username)
response.set_cookie("user_id", str(result["user"]["id"]))
return response
return templates.TemplateResponse("login.html", {
"request": request,
"error": result["message"],
"username": username,
})
# ====== 商品详情页 ======
@router.get("/products/{product_id}", response_class=HTMLResponse)
async def product_detail(request: Request, product_id: int):
store = get_store(request)
product = store.get_product_by_id(product_id)
if product is None:
return HTMLResponse("<h1>商品不存在</h1>", status_code=404)
return templates.TemplateResponse("product_detail.html", {
"request": request,
"product": product,
})
# ====== 购物车页 ======
@router.get("/cart", response_class=HTMLResponse)
async def cart_page(request: Request):
store = get_store(request)
user_id = int(request.cookies.get("user_id", "2"))
cart = store.get_cart(user_id)
summary = store.get_cart_total(user_id)
return templates.TemplateResponse("cart.html", {
"request": request,
"cart": cart,
"summary": summary,
})
@router.post("/cart/add/{product_id}", response_class=HTMLResponse)
async def cart_add(request: Request, product_id: int):
store = get_store(request)
user_id = int(request.cookies.get("user_id", "2"))
store.add_to_cart(user_id, product_id, 1)
return RedirectResponse("/cart", status_code=302)
@router.post("/cart/remove/{product_id}", response_class=HTMLResponse)
async def cart_remove(request: Request, product_id: int):
store = get_store(request)
user_id = int(request.cookies.get("user_id", "2"))
store.remove_from_cart(user_id, product_id)
return RedirectResponse("/cart", status_code=302)
# ====== 订单页 ======
@router.get("/orders", response_class=HTMLResponse)
async def orders_page(request: Request):
store = get_store(request)
user_id = int(request.cookies.get("user_id", "2"))
orders = store.get_orders_by_user(user_id)
return templates.TemplateResponse("orders.html", {
"request": request,
"orders": orders,
})
@router.post("/orders/create", response_class=HTMLResponse)
async def create_order(request: Request):
store = get_store(request)
user_id = int(request.cookies.get("user_id", "2"))
cart = store.get_cart(user_id)
store.create_order(user_id, cart)
return RedirectResponse("/orders", status_code=302)
# ====== 个人中心 ======
@router.get("/profile", response_class=HTMLResponse)
async def profile_page(request: Request):
store = get_store(request)
username = request.cookies.get("username", "")
user = store.get_user_by_username(username)
return templates.TemplateResponse("profile.html", {
"request": request,
"user": user,
})
@router.get("/logout")
async def logout(request: Request):
response = RedirectResponse("/", status_code=302)
response.delete_cookie("username")
response.delete_cookie("user_id")
return response
5.7 FastAPI 入口 – app/main.py
"""
MallLite 应用入口
"""
import sys
from pathlib import Path
# 确保项目根目录在 Python 路径中
sys.path.insert(0, str(Path(__file__).parent.parent))
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from app.data_store import DataStore
from app.routers import pages, api
def create_app():
"""创建 FastAPI 应用"""
app = FastAPI(title="MallLite", version="1.0.0", description="MallLite 电商系统")
# 初始化数据存储
store = DataStore("data")
app.state.store = store
# 挂载静态文件
app.mount("/static", StaticFiles(directory="static"), name="static")
# 注册路由
app.include_router(pages.router)
app.include_router(api.router, tags=["API"])
return app
app = create_app()
5.8 启动脚本 – run.py
"""
MallLite 启动脚本
用法:python run.py
"""
import uvicorn
if __name__ == "__main__":
print("=" * 50)
print(" MallLite 电商系统启动中…")
print(" 访问地址:http://localhost:8000")
print(" API 文档:http://localhost:8000/docs")
print("=" * 50)
uvicorn.run(
"app.main:app",
host="0.0.0.0",
port=8000,
reload=True,
)
5.9 app/init.py 和 app/routers/init.py
# app/__init__.py
# 空文件,标识这是一个 Python 包
# app/routers/__init__.py
# 空文件
5.10 requirements.txt
fastapi==0.109.0
uvicorn==0.27.0
jinja2==3.1.3
python-multipart==0.0.6
pyyaml==6.0.1
5.11 HTML 模板
创建 templates/base.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}MallLite{% endblock %}</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<header class="header">
<div class="header-inner">
<a href="/" class="logo">MallLite</a>
<div class="search-bar">
<form action="/" method="get">
<input type="text" id="search-input" name="keyword" placeholder="搜索商品" value="{{ keyword if keyword else '' }}">
<button type="submit" id="search-btn">搜索</button>
</form>
</div>
<nav class="nav-links">
<a href="/cart" id="cart-link">购物车</a>
<a href="/orders" id="orders-link">订单</a>
<a href="/profile" id="profile-link">个人中心</a>
<a href="/login" id="login-link">登录</a>
</nav>
</div>
</header>
<main class="main-content">
{% block content %}{% endblock %}
</main>
<footer class="footer">
<p>MallLite 电商系统 – 自动化测试练习项目</p>
</footer>
</body>
</html>
创建 templates/home.html:
{% extends "base.html" %}
{% block title %}MallLite – 首页{% endblock %}
{% block content %}
<div class="container">
<nav class="category-nav" id="category-nav">
<a href="/" class="category-link {% if not current_category %}active{% endif %}">全部</a>
{% for cat in categories %}
<a href="/?category={{ cat.name }}" class="category-link {% if current_category == cat.name %}active{% endif %}" data-category="{{ cat.name }}">{{ cat.name }}</a>
{% endfor %}
</nav>
<div class="product-list" id="product-list">
{% for product in products %}
<div class="product-card" data-id="{{ product.id }}">
<a href="/products/{{ product.id }}">
<div class="product-img-placeholder">{{ product.category }}</div>
<h3 class="product-name">{{ product.name }}</h3>
<span class="product-price">¥{{ product.price }}</span>
{% if product.is_new %}
<span class="badge-new">新品</span>
{% endif %}
<span class="product-rating">{{ product.rating }}分</span>
</a>
<form action="/cart/add/{{ product.id }}" method="post">
<button type="submit" class="add-cart-btn" id="add-cart-{{ product.id }}">加入购物车</button>
</form>
</div>
{% endfor %}
</div>
{% if total_pages > 1 %}
<div class="pagination" id="pagination">
{% if page > 1 %}
<a href="/?page={{ page – 1 }}{% if current_category %}&category={{ current_category }}{% endif %}" class="page-link page-prev">上一页</a>
{% endif %}
<span class="page-info" id="page-info">第 {{ page }}/{{ total_pages }} 页(共 {{ total }} 件商品)</span>
{% if page < total_pages %}
<a href="/?page={{ page + 1 }}{% if current_category %}&category={{ current_category }}{% endif %}" class="page-link page-next">下一页</a>
{% endif %}
</div>
{% endif %}
</div>
{% endblock %}
创建 templates/login.html:
{% extends "base.html" %}
{% block title %}MallLite – 登录{% endblock %}
{% block content %}
<div class="container">
<div class="form-container" id="login-form">
<h2>用户登录</h2>
{% if error %}
<div class="error-msg" id="error-msg">{{ error }}</div>
{% endif %}
<form method="post" action="/login">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" name="username" placeholder="请输入用户名"
value="{{ username if username else '' }}" required>
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" placeholder="请输入密码" required>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="remember" name="remember"> 记住密码
</label>
</div>
<button type="submit" id="login-btn" class="btn-primary">登录</button>
</form>
<p class="form-footer">测试账号:admin / admin123</p>
</div>
</div>
{% endblock %}
创建 templates/product_detail.html:
{% extends "base.html" %}
{% block title %}{{ product.name }} – MallLite{% endblock %}
{% block content %}
<div class="container">
<div class="product-detail" id="product-detail">
<div class="product-detail-left">
<div class="product-img-placeholder large">{{ product.category }}</div>
</div>
<div class="product-detail-right">
<h1 class="product-title" id="product-title">{{ product.name }}</h1>
<div class="product-meta">
<span class="product-price" id="product-price">¥{{ product.price }}</span>
<span class="product-stock" id="product-stock">库存:{{ product.stock }}件</span>
<span class="product-rating">评分:{{ product.rating }}</span>
</div>
<p class="product-desc" id="product-desc">{{ product.description }}</p>
<div class="product-actions">
<div class="quantity-selector">
<button class="qty-btn qty-minus" id="qty-minus">–</button>
<input type="number" class="qty-input" id="qty-input" value="1" min="1" max="{{ product.stock }}">
<button class="qty-btn qty-plus" id="qty-plus">+</button>
</div>
<form action="/cart/add/{{ product.id }}" method="post" style="display:inline;">
<button type="submit" class="btn-primary add-to-cart-btn" id="add-to-cart-btn">加入购物车</button>
</form>
</div>
{% if product.is_new %}
<span class="badge-new large">新品</span>
{% endif %}
</div>
</div>
</div>
{% endblock %}
创建 templates/cart.html:
{% extends "base.html" %}
{% block title %}购物车 – MallLite{% endblock %}
{% block content %}
<div class="container">
<h2>我的购物车</h2>
{% if cart %}
<div class="cart-list" id="cart-list">
{% for item in cart %}
<div class="cart-item" data-id="{{ item.product_id }}">
<div class="item-info">
<span class="item-name">{{ item.name }}</span>
<span class="item-price">¥{{ item.price }}</span>
</div>
<div class="item-quantity">
数量:{{ item.quantity }}
</div>
<span class="item-subtotal">¥{{ item.price * item.quantity }}</span>
<form action="/cart/remove/{{ item.product_id }}" method="post" style="display:inline;">
<button type="submit" class="btn-delete delete-btn" id="delete-{{ item.product_id }}">删除</button>
</form>
</div>
{% endfor %}
</div>
<div class="cart-footer" id="cart-footer">
<span class="total-price" id="total-price">合计:¥{{ summary.total_price }}</span>
<span class="total-quantity">共 {{ summary.total_quantity }} 件</span>
<form action="/orders/create" method="post" style="display:inline;">
<button type="submit" class="btn-primary checkout-btn" id="checkout-btn">去结算</button>
</form>
</div>
{% else %}
<div class="empty-cart" id="empty-cart">
<p>购物车是空的</p>
<a href="/" class="btn-primary">去逛逛</a>
</div>
{% endif %}
</div>
{% endblock %}
创建 templates/orders.html:
{% extends "base.html" %}
{% block title %}我的订单 – MallLite{% endblock %}
{% block content %}
<div class="container">
<h2>我的订单</h2>
{% if orders %}
<div class="order-list" id="order-list">
{% for order in orders %}
<div class="order-card" data-order-no="{{ order.order_no }}">
<div class="order-header">
<span class="order-no" id="order-no-{{ loop.index }}">{{ order.order_no }}</span>
<span class="order-status status-{{ order.status }}">{{ order.status }}</span>
<span class="order-time">{{ order.created_at }}</span>
</div>
<div class="order-items">
{% for item in order["items"] %}
<div class="order-item">
<span class="item-name">{{ item.name }}</span>
<span>¥{{ item.price }} × {{ item.quantity }}</span>
<span class="item-subtotal">¥{{ item.subtotal }}</span>
</div>
{% endfor %}
</div>
<div class="order-footer">
<span class="order-total">合计:¥{{ order.total_price }}</span>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-orders" id="empty-orders">
<p>暂无订单</p>
<a href="/" class="btn-primary">去购物</a>
</div>
{% endif %}
</div>
{% endblock %}
创建 templates/profile.html:
{% extends "base.html" %}
{% block title %}个人中心 – MallLite{% endblock %}
{% block content %}
<div class="container">
<h2>个人中心</h2>
{% if user %}
<div class="profile-card" id="profile-card">
<div class="profile-info">
<p><strong>用户名:</strong><span id="profile-username">{{ user.username }}</span></p>
<p><strong>昵称:</strong><span id="profile-nickname">{{ user.nickname }}</span></p>
<p><strong>手机号:</strong><span id="profile-phone">{{ user.phone }}</span></p>
<p><strong>邮箱:</strong><span id="profile-email">{{ user.email }}</span></p>
<p><strong>角色:</strong><span id="profile-role">{{ user.role }}</span></p>
</div>
<div class="profile-actions">
<a href="/orders" class="btn-secondary">查看订单</a>
<a href="/logout" class="btn-delete" id="logout-btn">退出登录</a>
</div>
</div>
{% else %}
<div class="login-prompt">
<p>请先 <a href="/login">登录</a></p>
</div>
{% endif %}
</div>
{% endblock %}
创建 static/css/style.css:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: #f5f5f5;
color: #333;
line-height: 1.6;
}
/* Header */
.header {
background: #fff;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
position: sticky;
top: 0;
z-index: 100;
}
.header-inner {
max-width: 1200px;
margin: 0 auto;
padding: 12px 24px;
display: flex;
align-items: center;
gap: 24px;
}
.logo {
font-size: 24px;
font-weight: 700;
color: #e4393c;
text-decoration: none;
white-space: nowrap;
}
.search-bar {
flex: 1;
display: flex;
}
.search-bar form {
display: flex;
width: 100%;
}
.search-bar input {
flex: 1;
padding: 8px 16px;
border: 2px solid #e4393c;
border-right: none;
border-radius: 4px 0 0 4px;
font-size: 14px;
outline: none;
}
.search-bar button {
padding: 8px 20px;
background: #e4393c;
color: #fff;
border: 2px solid #e4393c;
border-radius: 0 4px 4px 0;
cursor: pointer;
font-size: 14px;
}
.search-bar button:hover {
background: #c1272d;
}
.nav-links {
display: flex;
gap: 16px;
white-space: nowrap;
}
.nav-links a {
color: #666;
text-decoration: none;
font-size: 14px;
}
.nav-links a:hover {
color: #e4393c;
}
/* Main */
.main-content {
max-width: 1200px;
margin: 24px auto;
padding: 0 24px;
min-height: 60vh;
}
.container {
background: #fff;
border-radius: 8px;
padding: 24px;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
}
/* Category Nav */
.category-nav {
display: flex;
gap: 12px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #eee;
flex-wrap: wrap;
}
.category-link {
padding: 6px 16px;
background: #f0f0f0;
border-radius: 20px;
text-decoration: none;
color: #666;
font-size: 14px;
}
.category-link:hover,
.category-link.active {
background: #e4393c;
color: #fff;
}
/* Product List */
.product-list {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 24px;
}
.product-card {
background: #fff;
border: 1px solid #eee;
border-radius: 8px;
padding: 16px;
transition: box-shadow 0.2s;
}
.product-card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.product-card a {
text-decoration: none;
color: inherit;
}
.product-img-placeholder {
height: 120px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 14px;
margin-bottom: 12px;
}
.product-img-placeholder.large {
height: 300px;
font-size: 20px;
}
.product-name {
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.product-price {
color: #e4393c;
font-size: 18px;
font-weight: 700;
}
.product-rating {
color: #999;
font-size: 12px;
margin-left: 8px;
}
.badge-new {
display: inline-block;
padding: 2px 8px;
background: #ff6600;
color: #fff;
border-radius: 3px;
font-size: 12px;
margin-left: 8px;
}
.badge-new.large {
font-size: 14px;
padding: 4px 12px;
margin-top: 12px;
}
.add-cart-btn {
width: 100%;
margin-top: 12px;
padding: 8px;
background: #e4393c;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.add-cart-btn:hover {
background: #c1272d;
}
/* Pagination */
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
padding: 16px 0;
}
.page-link {
padding: 6px 16px;
background: #e4393c;
color: #fff;
text-decoration: none;
border-radius: 4px;
}
.page-link:hover {
background: #c1272d;
}
.page-info {
color: #666;
font-size: 14px;
}
/* Product Detail */
.product-detail {
display: flex;
gap: 40px;
}
.product-detail-left {
flex: 0 0 400px;
}
.product-detail-right {
flex: 1;
}
.product-title {
font-size: 24px;
margin-bottom: 16px;
}
.product-meta {
display: flex;
align-items: baseline;
gap: 16px;
margin-bottom: 16px;
padding: 16px;
background: #fafafa;
border-radius: 6px;
}
.product-meta .product-price {
font-size: 28px;
}
.product-meta .product-stock,
.product-meta .product-rating {
color: #666;
font-size: 14px;
}
.product-desc {
color: #666;
margin-bottom: 24px;
line-height: 1.8;
}
.product-actions {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 16px;
}
.quantity-selector {
display: flex;
align-items: center;
border: 1px solid #ddd;
border-radius: 4px;
}
.qty-btn {
width: 36px;
height: 36px;
background: #f5f5f5;
border: none;
cursor: pointer;
font-size: 18px;
}
.qty-input {
width: 60px;
height: 36px;
border: none;
border-left: 1px solid #ddd;
border-right: 1px solid #ddd;
text-align: center;
font-size: 14px;
}
.add-to-cart-btn {
padding: 10px 32px;
font-size: 16px;
}
/* Cart */
.cart-list {
margin-bottom: 24px;
}
.cart-item {
display: flex;
align-items: center;
gap: 16px;
padding: 16px 0;
border-bottom: 1px solid #eee;
}
.item-info {
flex: 1;
}
.item-name {
font-weight: 500;
display: block;
margin-bottom: 4px;
}
.item-price {
color: #999;
}
.item-subtotal {
color: #e4393c;
font-weight: 700;
font-size: 16px;
min-width: 80px;
text-align: right;
}
.cart-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 24px;
padding: 20px;
background: #fafafa;
border-radius: 8px;
}
.total-price {
font-size: 20px;
color: #e4393c;
font-weight: 700;
}
.total-quantity {
color: #666;
}
.empty-cart,
.empty-orders {
text-align: center;
padding: 60px 0;
color: #999;
}
.empty-cart p,
.empty-orders p {
font-size: 18px;
margin-bottom: 16px;
}
/* Orders */
.order-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.order-card {
border: 1px solid #eee;
border-radius: 8px;
overflow: hidden;
}
.order-header {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 16px;
background: #fafafa;
border-bottom: 1px solid #eee;
}
.order-no {
font-weight: 500;
}
.order-status {
padding: 2px 10px;
border-radius: 12px;
font-size: 12px;
}
.status-待支付 { background: #fff3cd; color: #856404; }
.status-已支付 { background: #d4edda; color: #155724; }
.status-已取消 { background: #f8d7da; color: #721c24; }
.status-已完成 { background: #cce5ff; color: #004085; }
.order-time {
color: #999;
font-size: 13px;
margin-left: auto;
}
.order-items {
padding: 12px 16px;
}
.order-item {
display: flex;
align-items: center;
gap: 16px;
padding: 8px 0;
}
.order-item .item-name {
flex: 1;
}
.order-footer {
display: flex;
justify-content: flex-end;
padding: 12px 16px;
background: #fafafa;
border-top: 1px solid #eee;
}
.order-total {
font-weight: 700;
color: #e4393c;
}
/* Profile */
.profile-card {
max-width: 500px;
}
.profile-info p {
padding: 12px 0;
border-bottom: 1px solid #eee;
}
.profile-actions {
display: flex;
gap: 12px;
margin-top: 24px;
}
/* Form */
.form-container {
max-width: 400px;
margin: 40px auto;
}
.form-container h2 {
text-align: center;
margin-bottom: 24px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: 500;
font-size: 14px;
}
.form-group input[type="text"],
.form-group input[type="password"] {
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
outline: none;
}
.form-group input:focus {
border-color: #e4393c;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 6px;
font-weight: normal;
}
.error-msg {
padding: 10px 14px;
background: #fef2f2;
color: #dc2626;
border: 1px solid #fecaca;
border-radius: 4px;
margin-bottom: 16px;
font-size: 14px;
}
.form-footer {
text-align: center;
margin-top: 16px;
color: #999;
font-size: 13px;
}
/* Buttons */
.btn-primary {
display: inline-block;
padding: 10px 24px;
background: #e4393c;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
text-decoration: none;
text-align: center;
}
.btn-primary:hover {
background: #c1272d;
}
.btn-secondary {
display: inline-block;
padding: 10px 24px;
background: #fff;
color: #333;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
text-decoration: none;
text-align: center;
}
.btn-delete {
display: inline-block;
padding: 6px 14px;
background: #fff;
color: #dc2626;
border: 1px solid #dc2626;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
text-decoration: none;
text-align: center;
}
.btn-delete:hover {
background: #dc2626;
color: #fff;
}
/* Login Prompt */
.login-prompt {
text-align: center;
padding: 60px 0;
font-size: 16px;
}
.login-prompt a {
color: #e4393c;
}
/* Footer */
.footer {
text-align: center;
padding: 24px;
color: #999;
font-size: 13px;
margin-top: 40px;
}
5.12 验证目录结构
确保 templates/ 和 static/css/ 目录存在:
mkdir -p templates static/css
5.13 启动项目
python run.py
看到以下输出说明启动成功:
==================================================
MallLite 电商系统启动中…
访问地址:http://localhost:8000
API 文档:http://localhost:8000/docs
==================================================
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO: Started reloader process
5.14 访问验证
在浏览器中打开以下地址,逐一验证:
| http://localhost:8000 | 首页,商品列表 |
| http://localhost:8000/?category=手机 | 手机分类 |
| http://localhost:8000/?keyword=Pro | 搜索 Pro |
| http://localhost:8000/login | 登录页 |
| http://localhost:8000/products/1 | 商品详情 |
| http://localhost:8000/cart | 购物车 |
| http://localhost:8000/orders | 订单列表 |
| http://localhost:8000/profile | 个人中心 |
| http://localhost:8000/docs | API 文档(FastAPI 自动生成) |
六、API 接口文档
FastAPI 自动生成了交互式 API 文档(Swagger UI),访问 http://localhost:8000/docs 可以直接在页面上测试每个接口。
以下是完整的 API 列表:
| POST | /api/login | 用户登录 |
| POST | /api/register | 用户注册 |
| GET | /api/products | 商品列表(支持分页、分类、搜索) |
| GET | /api/products/search?q=xxx | 搜索商品 |
| GET | /api/products/{id} | 商品详情 |
| GET | /api/categories | 分类列表 |
| GET | /api/cart | 获取购物车 |
| POST | /api/cart | 添加到购物车 |
| PUT | /api/cart/{product_id} | 修改购物车数量 |
| DELETE | /api/cart/{product_id} | 删除购物车商品 |
| GET | /api/orders | 订单列表 |
| POST | /api/orders | 创建订单 |
| GET | /api/orders/{order_no} | 订单详情 |
| PUT | /api/orders/{order_no}/cancel | 取消订单 |
用 curl 测试接口:
# 登录
curl -X POST http://localhost:8000/api/login \\
-H "Content-Type: application/json" \\
-d '{"username":"admin","password":"admin123"}'
# 获取商品列表
curl http://localhost:8000/api/products?page=1&page_size=5
# 搜索商品
curl http://localhost:8000/api/products/search?q=Pro
# 添加购物车
curl -X POST http://localhost:8000/api/cart \\
-H "Content-Type: application/json" \\
-d '{"user_id":2,"product_id":1,"quantity":1}'
# 查看购物车
curl http://localhost:8000/api/cart?user_id=2
# 创建订单
curl -X POST http://localhost:8000/api/orders \\
-H "Content-Type: application/json" \\
-d '{"user_id":2}'
七、面向对象在本项目中的应用总结
7.1 类的设计
| User | id, username, password, role | check_password(), to_dict() | 用户数据模型 |
| Product | id, name, price, stock | is_in_stock(), reduce_stock() | 商品数据模型 |
| CartItem | product_id, name, price, quantity | subtotal(属性) | 购物车项模型 |
| Order | order_no, items, status | pay(), cancel(), to_dict() | 订单数据模型 |
| DataStore | data_dir, _files | get_products(), add_to_cart() | 数据存储服务 |
7.2 和后续 POM 模式的对照
本篇的 OOP 思想直接对应后续 Playwright 的 POM 模式:
| class Product | class ProductPage(页面对象) |
| Product.to_dict() | ProductPage.get_title()(页面操作方法) |
| DataStore 管理所有数据 | BasePage 封装所有通用操作 |
| User 继承关系 | LoginPage 继承 BasePage |
| 路由分离(pages / api) | 用例分离(POM 用例 / KDT 用例) |
八、课后练习
练习 1:添加商品管理 API
# 在 api.py 中添加
@router.post("/products")
async def api_create_product(request: Request):
body = await request.json()
store = get_store(request)
products = store.get_products()
new_id = max(p.id for p in products) + 1 if products else 1
# 添加到 products.json
# …
return {"code": 200, "message": "商品添加成功", "data": {"id": new_id}}
练习 2:添加用户注册页面
创建 templates/register.html 并在 pages.py 中添加路由,实现完整的注册功能。
练习 3:添加订单详情页面
创建 templates/order_detail.html,显示订单的完整信息(商品列表、总价、状态、时间等)。
九、今日成果总结
今天完成了什么:
- 系统学习了面向对象编程(类、对象、属性、方法、self、继承)
- 学习了模块和包的导入方式
- 搭建了完整的 MallLite FastAPI 后端
- 实现了 14 个 API 接口
- 实现了 7 个页面路由(首页、登录、商品详情、购物车、订单、个人中心、退出)
- 创建了完整的 HTML 模板和 CSS 样式
- 项目已可运行,浏览器访问 http://localhost:8000 可看到完整的电商页面
当前项目结构:
mall-lite/
├── app/
│ ├── __init__.py
│ ├── main.py ← FastAPI 入口
│ ├── models.py ← 数据模型(User, Product, CartItem, Order)
│ ├── data_store.py ← 数据存储层
│ └── routers/
│ ├── __init__.py
│ ├── pages.py ← 页面路由(返回 HTML)
│ └── api.py ← API 路由(返回 JSON)
├── templates/ ← HTML 模板(7 个文件)
│ ├── base.html
│ ├── home.html
│ ├── login.html
│ ├── product_detail.html
│ ├── cart.html
│ ├── orders.html
│ └── profile.html
├── static/
│ └── css/
│ └── style.css
├── data/ ← 数据文件(JSON)
├── generate_data.py ← P01:数据生成
├── mall_functions.py ← P02:核心函数
├── order_functions.py ← P03:订单与统计
├── data_store.py ← P04:DataStore(独立版)
├── run.py ← 启动脚本
├── requirements.txt
└── venv/
十、下篇预告
P06 – 自动化测试常用技巧
Python 基础篇的最后一篇。将学习正则表达式、装饰器、logging 日志、datetime 日期处理、os/pathlib 路径操作等自动化测试中高频使用的 Python 技巧。学完 P06,Python 基础篇正式结束,你将拥有一个完整的电商项目和扎实的 Python 基础,正式进入自动化测试框架的学习。
系列导航
| P01 | Python 环境搭建与第一行代码 | ✅ 已发布 |
| P02 | 流程控制与函数 | ✅ 已发布 |
| P03 | 数据结构与常用操作 | ✅ 已发布 |
| P04 | 文件操作与异常处理 | ✅ 已发布 |
| P05 | 面向对象与模块 | ✅ 本文 |
| P06 | 自动化测试常用技巧 | 下一篇 |
| 01 | Web UI 自动化项目总览与环境搭建 | 待更新 |
| 02 | 框架基础层搭建 | 待更新 |

