本文是《企业级自动化测试实战》系列的第 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.namep.pricep.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 模式:

本篇的概念 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 框架基础层搭建 待更新
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐