架构设计与数据库表结构 - xiaochu-dev/online-shopping-group5 GitHub Wiki

在线购物系统 架构设计与数据库表结构(MVP)

项目 内容
文档名称 03-架构设计与数据库表结构
项目名称 在线购物系统(挑战升级)
版本 MVP(第 1 次迭代基线版本)
项目组号 第 5 组
编制人 胡成溯(组长)
编制日期 2026-10-06
文档版本 v1.0
依据文档 《需求规格说明书-第5组》v1.0、《需求澄清结论-第5组》(2026-10-04)

文档说明:本文件是组长「架构设计、数据库表结构」职责的署名成果物,描述 MVP 版本的技术选型、分层架构、两个状态机、队列递补算法、数据库表结构与 REST 接口,作为三人并行开发的实现依据。


1 技术选型与理由

1.1 选型清单

层面 选型 版本 理由
语言运行时 Python 3.12 三人均熟悉,标准库覆盖 sqlite3 / hashlib / secrets,无编译环节
Web 框架 FastAPI 最新稳定版 路由声明简洁,自带表单解析与文件上传(FR-02 图片),开发效率高
应用服务器 uvicorn 最新稳定版 FastAPI 官方配套 ASGI 服务器,单进程即可承载课程演示负载
数据库 SQLite(标准库 sqlite3) Python 内置 见 1.2 节论证,零安装、零运维、文件级持久化
模板引擎 Jinja2 最新稳定版 FastAPI 原生集成,服务端渲染即可满足「仅 Web 端」(NFR-01),无需前后端分离
会话 签名 cookie(itsdangerous) — 单卖家场景无需服务端会话存储,签名 cookie 防篡改即可支撑 FR-01-3

选型总原则:3 人小组、单次迭代 2 周、目标是可运行可验收的基线 MVP。全部组件均为轻量级、零外部服务依赖,pip install 一次即可在任意 Windows 机器上跑通,最大化开发与演示效率,避免运维开销挤占编码时间。

1.2 SQLite 满足非功能需求论证

NFR 需求内容 SQLite 满足方式
NFR-02 数据一致性 同一时间在售商品唯一;队列位次在撤销/递补后正确重排 单文件库 + 单写锁:同一时刻只允许一个写事务,状态流转(冻结、标记结果、递补)在单个事务内完成后才释放锁,不存在中间状态被并发读取;「在售唯一」用单条 UPDATE ... WHERE status='on_sale' + 事务约束保证
NFR-03 并发处理 多人同时提交意向时,入库顺序严格反映提交时间先后 SQLite 的单写锁将写入串行化:多个提交请求按到达数据库的顺序逐个拿锁写入,created_at 在写入事务内取值,因此入库顺序即提交顺序;排序键以毫秒时间戳 + 自增 id 兜底(见 5.1 节),保证严格先到先得
NFR-04 数据持久化 商品、意向、历史记录、口令码重启不丢失 数据全部落在单个 .db 文件,每次事务提交即落盘(WAL/日志由 SQLite 管理);进程重启后重新打开同一文件即恢复全部数据,无需任何额外持久化机制

结论:MVP 的数据规模(单商品、数百意向)与并发量(课程演示级别)远低于 SQLite 能力上限,引入 MySQL/PostgreSQL 属于过度设计。SQLite 是本场景的最优解。


2 系统分层架构

2.1 分层图

flowchart TD
    BROWSER["浏览器(买家 / 卖家)"]
    BROWSER -->|"HTTP(127.0.0.1:8000)"| ROUTE

    subgraph ROUTE["路由层 · FastAPI + uvicorn"]
        direction LR
        R1["买家页路由<br/>/product"]
        R2["意向路由<br/>/intents"]
        R3["登录路由<br/>/login"]
        R4["后台路由<br/>/admin/*"]
    end

    ROUTE -->|"会话校验(签名 cookie)· 表单/文件解析 · 重定向"| SVC

    subgraph SVC["服务层(业务逻辑,纯 Python 函数)"]
        direction LR
        S1["auth 认证服务<br/>登录校验 · 改密验旧密码<br/>签名 cookie"]
        S2["商品状态机服务<br/>发布唯一性校验 · 冻结/解冻/下架<br/>进入交易自动冻结 · 标记成功/失败"]
        S3["意向队列服务<br/>排序(时间+id)· 撤销(口令码)<br/>递补(失败→队首)· 成交批量转 failed"]
    end

    SVC -->|"事务边界:每个状态流转 = 一个事务 · PRAGMA foreign_keys = ON"| DATA

    subgraph DATA["数据层 · 标准库 sqlite3(单文件 shop.db)"]
        direction LR
        T1["seller 卖家"]
        T2["product 商品"]
        T3["intent 意向"]
    end

    DATA -.->|"渲染上下文"| TPL

    subgraph TPL["模板层 · Jinja2(服务端渲染)"]
        direction LR
        P1["卖家后台模板<br/>发布/意向人/冻结"]
        P2["买家页模板<br/>商品详情/口令码页"]
        P3["历史记录模板<br/>历史列表/历史详情"]
    end
Loading

2.2 层职责说明

层 职责 禁止事项
路由层 解析请求、校验登录会话、调用服务层、选择模板渲染响应 不写业务规则、不写 SQL
服务层 承载全部业务规则:状态流转合法性判断、队列排序与递补、口令码生成校验 不直接操作 HTTP 对象
数据层 提供三表的 CRUD 与事务封装,保证单写锁下的事务原子性 不做业务判断
模板层 页面呈现与操作反馈(NFR-06 成功/失败提示) 不含业务逻辑

3 商品状态机

3.1 状态定义

状态值 中文名 买家可浏览 可提交新意向 说明
on_sale 在售 ✅ ✅ 上架可售;同一时间全系统仅此一件
frozen 已冻结 ✅(显示「商品交易中」) ❌ 进入交易自动冻结 / 卖家手动冻结,同一状态不区分(澄清结论第六节)
delisted 已下架 仅历史记录中可见 ❌ 终态,转入历史,不可逆
on_sale(恢复语义) 已恢复在售 ✅ ✅ 语义标签,与「在售」同一状态值:经历「冻结→解冻」或「标记失败恢复」路径后重新在售;库中不新增状态值,代码零改动(第 1 次评审助教建议显式化,v1.1 增补)

3.2 状态流转图

stateDiagram-v2
    direction LR
    state "在售 on_sale" as S
    state "已冻结 frozen" as F
    state "已下架 delisted(终态)" as D
    [*] --> S: 卖家发布(on_sale 唯一性校验通过)
    S --> F: ① 进入交易(系统自动冻结);② 卖家手动冻结
    F --> S: ③ 手动解冻;④ 标记「失败」恢复在售(已恢复在售语义态 + 递补)
    F --> D: ⑤ 标记「成功」(进入历史)
    S --> D: ⑥ 手动下架(仅在售状态可执行)
    D --> [*]
Loading

3.3 商品状态流转表

# 触发动作 操作角色 前置状态 后置状态 对应需求
1 发布商品 卖家 (无,且当前无在售商品) on_sale FR-02-1/2/7
2 进入交易(卖家选定队首买家线下谈) 系统(自动) on_sale frozen FR-06-1
3 手动冻结 卖家 on_sale frozen FR-06-2
4 手动解冻 卖家 frozen on_sale FR-06-6/7
5 标记交易成功 卖家 frozen delisted(trade_result=success) FR-07-5
6 标记交易失败(恢复在售 + 触发递补) 卖家 frozen on_sale(已恢复在售语义态) FR-07-6/7
7 手动下架(仅允许在售状态执行) 卖家 on_sale delisted(trade_result=manual_delist) 澄清结论第一节

规则:自动冻结(#2)与手动冻结(#3)到达同一个 frozen 状态,库中不做来源标记(澄清结论第六节)。 下架唯一性:delisted 仅有 #5、#6 两条进入路径;frozen 状态下卖家只能选择标记成功 / 标记失败 / 手动解冻,不能直接手动下架。


4 意向状态机

4.1 状态定义

状态值 中文名 说明
pending 排队中 提交成功后的初始状态,在队列中占位
trading 交易中 队首意向被卖家选定进入线下交易
success 交易成功 该意向对应买家完成交易
failed 交易失败 队首交易失败、或商品成交后队列中其余未处理意向
cancelled 已撤销 买家凭口令码主动撤销

4.2 意向状态流转表

# 触发动作 操作角色 前置状态 后置状态 连带效应 对应需求
1 提交意向(姓名+电话) 买家 (无) pending 生成口令码当场显示;按 created_at 入队 FR-03-4~9
2 进入交易 卖家 pending(队首) trading 商品 on_sale → frozen FR-05-4、FR-06-1
3 凭口令码撤销 买家 pending cancelled 后位意向位次自动前移;商品状态不变 FR-04-4~6
4 标记交易成功 卖家 trading success 商品 → delisted;该商品队列中其余 pending 意向全部转 failed FR-07-5
5 标记交易失败 卖家 trading failed 商品 → on_sale;若队列非空,新队首 pending → trading,商品再 → frozen(递补) FR-07-6/7

状态 3 的撤销仅允许 pending 意向:已进入 trading 的意向买家不可自行撤销,须等待卖家标记结果;success / failed / cancelled 均为终态。

4.3 口令码机制(设计补充,对应需求规格待确认事项 1)

项 设计定义
生成算法 Python 标准库 secrets.token_hex(4),生成 8 位十六进制随机串(如 a3f9c2e1)。v1.0 误记为 32 位,已按 services_state.py 实现勘误(v1.1)
生成时机 意向入库的同一事务内生成,提交成功页当场一次性显示
存储方式 intent.token_code 字段持久化存储;唯一性由应用层生成时查重、撞码循环重新生成保证(非数据库 UNIQUE 约束),随机串不可猜测,满足 NFR-05
校验规则 撤销时按 token_code 精确匹配 + 意向状态必须为 pending 双重校验(与 cancel_intent 实现及《口令码与队列规则说明》#8 一致)
有效期 意向离开 pending(进入 trading / cancelled / failed / success)后即失效,不可再用于撤销;不设时间有效期(与《口令码与队列规则说明》#9~11 及规格书 FR-04-8 一致)

5 队列与递补算法

5.1 排序规则

队列不落物理位次字段,位次始终由查询时排序计算,撤销后天然前移,无更新风暴:

  • 主排序键:created_at(毫秒级时间戳,INTEGER)
  • 兜底键:id(自增主键;同一毫秒内多次提交时保证入库顺序即队列顺序)
  • 即 ORDER BY created_at ASC, id ASC,与复合索引 intent(product_id, status, created_at) 配合(见 6.3 节)

5.2 核心算法伪代码

# ── 查询当前商品队列(服务层函数)──────────────────────
function get_queue(product_id):
    return SELECT id, buyer_name, phone, created_at
           FROM intent
           WHERE product_id = product_id AND status = 'pending'
           ORDER BY created_at ASC, id ASC        # 位次 = 结果集行号

# ── 买家撤销意向(一个事务)───────────────────────────
function cancel_intent(token_code):
    BEGIN
        row = SELECT * FROM intent WHERE token_code = token_code
                                   AND status = 'pending'   # 精确匹配 + 仅排队中可撤销
        assert row 存在
        UPDATE intent SET status = 'cancelled' WHERE id = row.id
        # 队列按 ORDER BY 实时计算,删除即前移,无需更新其余行
        # 商品状态不变(on_sale 或 frozen,取决于撤销时机)
    COMMIT

# ── 卖家选定队首进入交易(一个事务)─────────────────────
function start_trade(product_id):
    BEGIN
        assert product.status == 'on_sale'
        head = get_queue(product_id) 的第一行
        assert head 存在
        UPDATE intent SET status = 'trading' WHERE id = head.id
        UPDATE product SET status = 'frozen'      # 自动冻结
    COMMIT

# ── 标记交易失败(一个事务,触发递补)────────────────────
function mark_failed(product_id):
    BEGIN
        cur = SELECT * FROM intent
              WHERE product_id = product_id AND status = 'trading'
        UPDATE intent SET status = 'failed' WHERE id = cur.id
        UPDATE product SET status = 'on_sale'     # ① 商品恢复在售
        head = get_queue(product_id) 的第一行
        IF head 存在 THEN                          # ② 队列非空
            UPDATE intent SET status = 'trading' WHERE id = head.id
            UPDATE product SET status = 'frozen'   # ③ 新队首进入交易,商品再冻结
        ELSE                                       # ④ 队列为空
            商品停留在 on_sale,重新接收新意向
        END IF
    COMMIT

# ── 标记交易成功(一个事务,殃及整个队列)─────────────────
function mark_success(product_id):
    BEGIN
        cur = 当前 trading 意向
        UPDATE intent SET status = 'success' WHERE id = cur.id
        UPDATE intent SET status = 'failed'
              WHERE product_id = product_id AND status = 'pending'
              # 商品已下架,队列中其余排队意向失去意义,全部转 failed
        UPDATE product SET status = 'delisted',
                           trade_result = 'success',
                           traded_at = 当前毫秒时间戳
    COMMIT

算法要点:

  1. 位次不落库,ORDER BY created_at, id 即队列,撤销后位次自动前移(FR-04-6)。
  2. 失败递补严格按「商品回在售 → 取新队首 → 意向转 trading → 商品再冻结」四步,在同一事务内完成,中间状态不可见(NFR-02)。
  3. 队列为空时商品停在 on_sale,等待新买家提交(FR-04-7 的自然结果)。
  4. 成交时批量将剩余 pending 意向转 failed,保证历史详情页每人交易结果字段有值(FR-08-5)。

6 数据库表结构

6.1 建表 DDL

PRAGMA foreign_keys = ON;

-- 卖家账号表(全系统仅一条记录)
CREATE TABLE seller (
    id            INTEGER PRIMARY KEY AUTOINCREMENT,
    username      TEXT    NOT NULL UNIQUE,
    password_hash TEXT    NOT NULL,
    created_at    INTEGER NOT NULL
);

-- 商品表(同一时间最多一条 status = 'on_sale')
CREATE TABLE product (
    id           INTEGER PRIMARY KEY AUTOINCREMENT,
    name         TEXT    NOT NULL,
    description  TEXT    NOT NULL DEFAULT '',
    image_path   TEXT    NOT NULL DEFAULT 'static/placeholder.png',
    price_cents  INTEGER NOT NULL CHECK (price_cents > 0),
    status       TEXT    NOT NULL DEFAULT 'on_sale'
                 CHECK (status IN ('on_sale', 'frozen', 'delisted')),
    published_at INTEGER NOT NULL,
    trade_result TEXT    CHECK (trade_result IN ('success', 'failed', 'manual_delist')),
    traded_at    INTEGER
);

-- 购买意向表
CREATE TABLE intent (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    product_id INTEGER NOT NULL REFERENCES product(id),
    buyer_name TEXT    NOT NULL,
    phone      TEXT    NOT NULL,
    token_code TEXT    NOT NULL,  -- 口令码;唯一性由应用层生成时查重保证(services_state.py)
    status     TEXT    NOT NULL DEFAULT 'pending'
               CHECK (status IN ('pending', 'trading', 'success', 'failed', 'cancelled')),
    created_at INTEGER NOT NULL
);

-- 队列查询复合索引(见 6.3 节)
CREATE INDEX idx_intent_queue ON intent (product_id, status, created_at);

6.2 字段说明

表 1:seller(卖家账号)

字段 类型 约束 业务含义
id INTEGER 主键,自增 卖家记录标识(全表仅一条)
username TEXT NOT NULL,UNIQUE 登录用户名;初始化种子为 admin
password_hash TEXT NOT NULL 密码哈希(sha256 + 盐);改密需验原密码(FR-01-4/5)
created_at INTEGER NOT NULL 账号创建时间,毫秒时间戳

表 2:product(商品)

字段 类型 约束 业务含义
id INTEGER 主键,自增 商品标识,同时用于历史详情路由
name TEXT NOT NULL 商品名称,必填,不设字数上限(FR-02-3)
description TEXT NOT NULL DEFAULT '' 商品描述,可留空(FR-02-4)
image_path TEXT NOT NULL DEFAULT 'static/placeholder.png' 主图相对路径;未上传时使用占位图(FR-02-4)
price_cents INTEGER NOT NULL,CHECK > 0 价格,以「分」为单位的整数(9.90 元存 990),避免浮点误差,天然满足「必须 > 0、保留两位小数」(FR-02-5)
status TEXT NOT NULL,CHECK 三值 商品状态:on_sale / frozen / delisted(见 3.1 节);「已恢复在售」为 on_sale 的恢复语义标签
published_at INTEGER NOT NULL 发布时间,毫秒时间戳
trade_result TEXT CHECK 三值,可 NULL 最终结果:success / failed / manual_delist;在售与冻结期为 NULL
traded_at INTEGER 可 NULL 交易时间(标记成功或手动下架的毫秒时间戳);历史列表倒序依据(FR-08-2/3)

表 3:intent(购买意向)

字段 类型 约束 业务含义
id INTEGER 主键,自增 意向标识;排序兜底键(见 5.1 节)
product_id INTEGER NOT NULL,外键 → product(id) 所属商品;意向随商品进入历史而保留(FR-08-4/5)
buyer_name TEXT NOT NULL 买家姓名,必填,不做格式校验(FR-03-4/5)
phone TEXT NOT NULL 联系电话,必填;不校验格式、不按号码去重(FR-03-5/6)
token_code TEXT NOT NULL(唯一性由应用层查重保证) 口令码,8 位随机十六进制串(secrets.token_hex(4)),凭码撤销(FR-03-8、FR-04-4~10)
status TEXT NOT NULL,CHECK 五值 意向状态:pending / trading / success / failed / cancelled(见 4.1 节)
created_at INTEGER NOT NULL 提交时间,毫秒时间戳;队列主排序键(FR-03-9、FR-04-2)

6.3 索引设计

索引 定义 支撑场景
idx_intent_queue intent (product_id, status, created_at) 复合索引 队列查询 WHERE product_id=? AND status='pending' ORDER BY created_at:三列前缀完全匹配过滤条件,排序键为索引第三列,免 filesort 直接按索引序返回;后台意向列表、递补取队首、成交批量转 failed 均走此索引

其余高频查询均为小表全表扫描(seller 单行、product 按状态过滤、历史列表按 traded_at 排序的记录量受翻页限制),MVP 数据量下不额外建索引。


7 REST 接口清单

方法 路径 权限 功能 对应 FR
GET /product 买家(公开) 买家浏览当前商品(在售完整信息 / 冻结显示「商品交易中」) FR-03-1/2
POST /product/{id}/intents 买家(公开) 提交购买意向(姓名+电话),返回口令码页 FR-03-4~9、FR-04-1
POST /intents/{id}/cancel 买家(公开,凭口令码) 撤销自己的意向,队列位次前移 FR-04-4/5/6
GET /login 公开 卖家登录页 FR-01-1
POST /login 公开 提交用户名密码,校验通过发签名 cookie FR-01-1/3
GET /admin 卖家 后台首页:当前商品 + 意向购买人列表(按提交时间正序) FR-05-1/2/3
GET /admin/publish 卖家 商品发布表单页 FR-02
POST /admin/publish 卖家 提交发布(4 项字段校验 + 在售唯一性校验) FR-02-1~7
POST /admin/products/{id}/trade/start 卖家 选定队首进入交易(自动冻结) FR-05-4、FR-06-1
POST /admin/products/{id}/freeze 卖家 手动冻结 FR-06-2
POST /admin/products/{id}/unfreeze 卖家 手动解冻 FR-06-6/7
POST /admin/products/{id}/delist 卖家 手动下架(仅 on_sale 可调用) 澄清结论第一节
POST /admin/products/{id}/trade/result 卖家 标记交易结果(成功 / 失败+递补) FR-07-5/6/7
GET /admin/history 卖家 历史商品列表(按交易时间倒序,支持翻页) FR-08-1/2
GET /admin/history/{id} 卖家 历史详情:商品级 + 意向人级字段(含每人交易结果) FR-08-3/4/5
POST /admin/password 卖家 修改密码(验原密码,新密码 ≥ 8 位) FR-01-4/5

权限说明:/admin/* 全部经签名 cookie 会话校验,未登录访问重定向至 /login(FR-01-3);本版本不提供退出登录(FR-01-6)。接口均为服务端渲染页面表单,不提供 JSON API。


8 模块划分与署名对照

依据《团队分工方案-第5组》第四节「第 1 次迭代(MVP)模块认领」,与本架构文档的模块划分对照如下:

架构模块 涵盖文件/职责 认领人 覆盖需求 交付标准
公共数据层与状态机骨架 db.py(建表、事务封装)、两个状态机的流转校验骨架、main.py 应用装配 胡成溯(架构设计职责) 全局支撑 三表建表成功,状态流转校验可供其余模块复用
卖家后台 登录/改密(FR-01)、发布表单与校验(FR-02)、意向人管理(FR-05)、结果标记(FR-07)、历史记录(FR-08)的后台路由与模板 胡成溯 FR-01/05/07/08 卖家后台能跑通全流程
商品与意向提交 买家商品展示页、意向提交表单与校验、口令码展示页 黄程宇 FR-02/03 买家能提交,卖家能看到意向购买人
队列与冻结 意向队列排序/撤销/递补算法(第 5 节)、冻结/解冻状态流转(第 3 节) 周到 FR-04/06 多买家按时间正序排队;冻结/恢复状态正确流转

对照说明:本表与需求追溯矩阵(需求规格说明书第 7 节)一致;FR-02 的发布表单与校验归卖家后台(胡成溯),FR-03 的买家浏览与提交归黄程宇,两模块共用 product 表但入口分离,接口边界即第 7 节接口清单。


9 关键设计决策记录

# 决策 依据 状态
D1 自动冻结与手动冻结共用同一个 frozen 状态,库中不区分冻结来源 需求澄清结论第六节:「手动冻结与自动冻结是同一个状态,不区分」 已定稿
D2 下架仅两条路径:交易成功(frozen → delisted)、在售状态手动下架(on_sale → delisted);已冻结状态不可手动下架 第 5 组需求澄清结论第一节、第六节;比规格书 3.3 节状态图的表述更收紧,以澄清结论为准 已定稿
D3 商品发布后信息不可修改,如需修改须重新发布 FR-02-8,基于澄清结论「未提及修改能力」的推断 待与助教复核(规格书待确认事项 2)
D4 历史列表倒序依据交易时间 traded_at(而非发布时间) 规格书待确认事项 3 未定,本设计采用默认口径 默认口径,待确认后如调整仅需改排序字段
D5 价格以「分」为整数存储(price_cents);队列位次不落库、由 ORDER BY created_at, id 实时计算 规避浮点精度问题,满足价格「> 0、两位小数」;位次实时计算使撤销/递补无需批量 UPDATE,天然满足 NFR-02/03 已定稿

变更记录

版本 日期 变更内容 修订人
v1.0 2026-10-06 首版创建:技术选型、分层架构、双状态机、队列递补算法、三表 DDL、接口清单、署名对照、设计决策 胡成溯
v1.1 2026-10-07 依据第 1 次评审助教点评补强:①3.1 状态表显式化「已恢复在售」语义态(复用 on_sale,代码零改动);②口令码描述按代码实现勘误——生成算法 8 位(v1.0 误记 32 位)、字段名 token_code(v1.0 误记 passcode)、唯一性为应用层查重(v1.0 误记 UNIQUE 约束)、撤销校验与实现及《口令码与队列规则说明》对齐 胡成溯

本文件为第 5 组 MVP 架构基线,三人开发均以本文件为实现依据;后续迭代在此文件基础上增补设计章节。

⚠️ **GitHub.com Fallback** ⚠️