项目
内容
文档名称
03-架构设计与数据库表结构
项目名称
在线购物系统(挑战升级)
版本
MVP(第 1 次迭代基线版本)
项目组号
第 5 组
编制人
胡成溯(组长)
编制日期
2026-10-06
文档版本
v1.0
依据文档
《需求规格说明书-第5组》v1.0、《需求澄清结论-第5组》(2026-10-04)
文档说明 :本文件是组长「架构设计、数据库表结构」职责的署名成果物,描述 MVP 版本的技术选型、分层架构、两个状态机、队列递补算法、数据库表结构与 REST 接口,作为三人并行开发的实现依据。
层面
选型
版本
理由
语言运行时
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 机器上跑通,最大化开发与演示效率,避免运维开销挤占编码时间。
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 是本场景的最优解。
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
层
职责
禁止事项
路由层
解析请求、校验登录会话、调用服务层、选择模板渲染响应
不写业务规则、不写 SQL
服务层
承载全部业务规则:状态流转合法性判断、队列排序与递补、口令码生成校验
不直接操作 HTTP 对象
数据层
提供三表的 CRUD 与事务封装,保证单写锁下的事务原子性
不做业务判断
模板层
页面呈现与操作反馈(NFR-06 成功/失败提示)
不含业务逻辑
状态值
中文名
买家可浏览
可提交新意向
说明
on_sale
在售
✅
✅
上架可售;同一时间全系统仅此一件
frozen
已冻结
✅(显示「商品交易中」)
❌
进入交易自动冻结 / 卖家手动冻结,同一状态不区分 (澄清结论第六节)
delisted
已下架
仅历史记录中可见
❌
终态,转入历史,不可逆
on_sale(恢复语义)
已恢复在售
✅
✅
语义标签,与「在售」同一状态值 :经历「冻结→解冻」或「标记失败恢复」路径后重新在售;库中不新增状态值,代码零改动(第 1 次评审助教建议显式化,v1.1 增补)
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
#
触发动作
操作角色
前置状态
后置状态
对应需求
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 状态下卖家只能选择标记成功 / 标记失败 / 手动解冻,不能直接手动下架 。
状态值
中文名
说明
pending
排队中
提交成功后的初始状态,在队列中占位
trading
交易中
队首意向被卖家选定进入线下交易
success
交易成功
该意向对应买家完成交易
failed
交易失败
队首交易失败、或商品成交后队列中其余未处理意向
cancelled
已撤销
买家凭口令码主动撤销
#
触发动作
操作角色
前置状态
后置状态
连带效应
对应需求
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 一致)
队列不落物理位次字段 ,位次始终由查询时排序计算,撤销后天然前移,无更新风暴:
主排序键:created_at(毫秒级时间戳,INTEGER)
兜底键:id(自增主键;同一毫秒内多次提交时保证入库顺序即队列顺序)
即 ORDER BY created_at ASC, id ASC,与复合索引 intent(product_id, status, created_at) 配合(见 6.3 节)
# ── 查询当前商品队列(服务层函数)──────────────────────
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
算法要点 :
位次不落库,ORDER BY created_at, id 即队列,撤销后位次自动前移(FR-04-6)。
失败递补严格按「商品回在售 → 取新队首 → 意向转 trading → 商品再冻结」四步,在同一事务内完成,中间状态不可见(NFR-02)。
队列为空时商品停在 on_sale,等待新买家提交(FR-04-7 的自然结果)。
成交时批量将剩余 pending 意向转 failed,保证历史详情页每人交易结果字段有值(FR-08-5)。
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);
表 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)
索引
定义
支撑场景
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 数据量下不额外建索引。
方法
路径
权限
功能
对应 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。
依据《团队分工方案-第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 节接口清单。
#
决策
依据
状态
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 架构基线,三人开发均以本文件为实现依据;后续迭代在此文件基础上增补设计章节。