第5组 · 胡成溯 · 第1次评审详情 - sebestp/coursewise GitHub Wiki
- 报告期次:第1次 | 报告日期:2026-10-08 | 项目组号:第5组 | 项目名称:在线购物系统(挑战升级)
-
已完成工作:搭建项目 GitHub 仓库与组 Wiki,挂接全部文档页
-
已完成工作:架构设计:技术选型论证、分层架构、商品三态与意向五态状态机、队列递补算法、三表数据库设计
-
已完成工作:公共数据层与状态机骨架开发(sqlite3 三表 DDL、商品/意向状态流转、队列排序键设计)
-
已完成工作:卖家后台模块开发(FR-01/05/07/08):登录与会话、验原密码改密、意向购买人列表、进入交易自动冻结、冻结/解冻开关、交易结果标记、历史记录
-
已完成工作:部署交付与质量基线:全流程冒烟测试 30 项自动化用例、部署脚本(run.bat)与部署手册
-
成果物:在线购物系统 部署手册(MVP)
| 项目 | 内容 |
|---|---|
| 文档名称 | 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
| 层 | 职责 | 禁止事项 |
|---|---|---|
| 路由层 | 解析请求、校验登录会话、调用服务层、选择模板渲染响应 | 不写业务规则、不写 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 --> [*]
| # | 触发动作 | 操作角色 | 前置状态 | 后置状态 | 对应需求 |
|---|---|---|---|---|---|
| 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均为终态。
| 项 | 设计定义 |
|---|---|
| 生成算法 | 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 架构基线,三人开发均以本文件为实现依据;后续迭代在此文件基础上增补设计章节。
| 项目 | 内容 |
|---|---|
| 文档名称 | 04-部署手册 |
| 项目名称 | 在线购物系统(挑战升级) |
| 版本 | MVP(第 1 次迭代基线版本) |
| 项目组号 | 第 5 组 |
| 编制人 | 胡成溯(组长) |
| 编制日期 | 2026-10-06 |
| 文档版本 | v1.0 |
| 对齐标准 | 课程 Wiki《系统验收标准》第 4 项「部署」:运行环境说明、安装部署过程、部署脚本 |
文档说明:本手册描述 MVP 版本从零到可访问的完整部署过程。按第 1~4 节顺序执行即可完成部署;第 5 节为常见问题排查;第 6 节为评审现场的一键演示脚本。
| 项 | 要求 | 说明 |
|---|---|---|
| 操作系统 | Windows 10 / 11 | 64 位;Linux / macOS 同样可运行(命令通用),本文以 Windows 为准 |
| Python | ≥ 3.10(开发与测试基线为 3.12) | 安装时勾选「Add Python to PATH」 |
| 数据库 | 无外部数据库依赖 | 使用 Python 标准库 sqlite3,数据库为项目目录下的单文件 shop.db,无需安装任何数据库服务 |
| 浏览器 | Chrome / Edge 现代浏览器 | 访问与演示使用 |
| 网络 | 本机回环即可 | 服务监听 127.0.0.1:8000,无需公网 |
| 第三方依赖 | 仅 4 个 pip 包 | 见 requirements.txt:fastapi、uvicorn、jinja2、python-multipart(表单与文件上传解析) |
从 Git 平台下载 项目简写-版本号.zip 并解压,或直接克隆仓库,得到项目目录:
shop-mvp/
├── main.py # 应用入口(uvicorn 启动)
├── db.py # 建表与种子数据初始化脚本
├── run.bat # Windows 一键启动脚本
├── requirements.txt # 第三方依赖清单
├── app/ # 路由层、服务层代码
├── templates/ # Jinja2 模板(后台 / 买家页 / 历史)
└── static/ # 占位图、上传图片目录
cd shop-mvp
python -m venv .venv
.venv\Scripts\activatepip install -r requirements.txt国内网络环境建议使用清华镜像源加速:
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后可用 pip list 确认 fastapi、uvicorn、jinja2、python-multipart 均已就位。
首次部署必须先执行初始化脚本(自动建表 + 写入种子卖家账号):
python db.py初始化脚本自动完成:
- 在项目根目录创建 SQLite 数据库文件
shop.db; - 创建
seller/product/intent三张表及队列复合索引(DDL 见《03-架构设计与数据库表结构》第 6 节); - 写入种子卖家账号:
| 项 | 值 |
|---|---|
| 用户名 | admin |
| 初始密码 | admin12345 |
⚠️ 安全提示:种子密码仅用于首次登录,评审与日常使用前请登录后台立即修改密码(后台「修改密码」入口,需验证原密码,新密码 ≥ 8 位)。
两种方式任选其一:
方式一:命令行启动
python main.py方式二:双击一键启动脚本
双击项目根目录下的 run.bat
run.bat 内部执行「激活虚拟环境 + python main.py」,用于评审现场免命令行演示。
启动成功后控制台输出:
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
| 页面 | 地址 | 权限 |
|---|---|---|
| 买家页(商品浏览与意向提交) | http://127.0.0.1:8000/product | 公开 |
| 登录页 | http://127.0.0.1:8000/login | 公开 |
| 卖家后台(发布 / 意向人 / 冻结 / 标记结果) | http://127.0.0.1:8000/admin | 需登录 |
| 历史记录 | http://127.0.0.1:8000/admin/history | 需登录 |
首次部署顺序:第 3 节初始化 → 第 4 节启动。未初始化直接启动会提示数据库文件缺失,按第 3 节执行
python db.py后重新启动即可。
| # | 问题 | 原因 | 解决方法 |
|---|---|---|---|
| 1 | 启动报 [Errno 10048] / address already in use,8000 端口被占用 |
8000 端口已被其他进程(或上次未退出的实例)占用 | 换端口启动:python main.py --port 8001,并以 http://127.0.0.1:8001 访问;或先结束占用进程 |
| 2 | 图片上传被拒绝,提示超出大小限制 | FR-02-6:单张图片 ≤ 5MB,且仅接受 JPG / PNG | 压缩图片或换小图。限制定义于发布表单校验逻辑(app/ 下发布校验代码,对应 FR-02-6),修改上限需改该校验常量并重新启动 |
| 3 | 数据想重置回初始状态(演示前清场) | 数据持久化在 shop.db,重启不丢失(NFR-04) |
重置方法:① 停止服务(Ctrl+C 或关闭窗口);② 删除项目根目录下的 shop.db(若启用 WAL 还有 shop.db-wal、shop.db-shm 一并删除);③ 重新执行 python db.py;④ 重新启动。重置后卖家账号恢复为 admin/admin12345
|
| 4 | 访问 /admin 被跳转到登录页 |
FR-01-3:后台须登录后访问 | 正常行为,用卖家账号登录即可 |
| 5 |
pip install 下载缓慢或超时 |
默认源境外网络不稳定 | 使用 2.3 节的清华镜像源命令重试 |
| 6 | 上传的商品图片不显示 |
static/ 目录被移动或改名 |
保持项目目录结构原样,确认 static/ 目录存在且有写入权限 |
以下为第 1 次评审的完整演示动线,覆盖 MVP 验收清单第 1~12 项。演示前建议按 FAQ-3 重置数据库,保证队列干净。演示用买家在多个浏览器窗口(或普通 + 无痕窗口)中打开买家页。
| 步 | 操作 | URL | 预期页面 / 结果 |
|---|---|---|---|
| 1 |
卖家登录:打开登录页,输入 admin / 当前密码 |
http://127.0.0.1:8000/login |
登录成功,跳转后台 /admin,可见「发布商品」入口 |
| 2 |
发布商品:后台点「发布商品」,填名称、描述、上传图片(或留空用占位图)、价格 9.90,提交 |
http://127.0.0.1:8000/admin/publish |
发布成功,商品进入「在售」;打开买家页可见该商品 |
| 3 | 买家甲提交意向:买家窗口 1 打开买家页,填姓名「买家甲」、电话,提交 | http://127.0.0.1:8000/product |
提交成功,当场显示口令码,提示「当前排队第 1 位」 |
| 4 | 买家乙、丙依次提交:买家窗口 2、3 重复操作(乙第 2、丙第 3) | http://127.0.0.1:8000/product |
各自获得独立口令码;后台意向列表按提交时间正序显示甲→乙→丙 |
| 5 | 买家乙撤销:在乙的口令码页(或凭口令码进入撤销页)撤销意向 | 口令码页「撤销意向」入口 | 撤销成功;后台列表变为甲→丙,丙位次自动前移到第 2 位 |
| 6 | 进入交易(自动冻结):后台选中队首「买家甲」进入交易 | http://127.0.0.1:8000/admin |
意向甲转「交易中」,商品转「已冻结」;买家页显示**「商品交易中」**,无法提交新意向 |
| 7 | 标记交易失败(递补):后台对甲标记「交易失败」 | http://127.0.0.1:8000/admin |
甲转「失败」,商品自动回「在售」→ 新队首丙自动转「交易中」→ 商品再次冻结(递补完成);买家页显示「商品交易中」 |
| 8 | 标记交易成功:后台对丙标记「交易成功」 | http://127.0.0.1:8000/admin |
丙转「成功」,商品撤下进入历史;商品已下架,队列其余意向(无)转失败 |
| 9 | 历史核对:后台打开历史列表,点进该商品详情 | http://127.0.0.1:8000/admin/history |
历史列表按交易时间倒序展示、可翻页;详情页可见商品级字段(名称/描述/图片/价格/发布时间/结果/交易时间)与意向人级字段(甲=失败、乙=已撤销、丙=成功),字段完整 |
| 10 | 改密(收尾,可选):后台修改密码,输入原密码与新密码(≥8 位) | http://127.0.0.1:8000/admin |
原密码校验通过方可修改,新密码生效(演示后如需还原按 FAQ-3 重置) |
演示要点提示:步骤 6~7 是「队列与递补」的核心演示点(FR-06-1、FR-07-6/7);步骤 9 可对照验收清单第 12 项逐字段核对。若评审现场 8000 端口冲突,全程将 URL 中的 8000 替换为 FAQ-1 中的备用端口即可。
| 版本 | 日期 | 变更内容 | 修订人 |
|---|---|---|---|
| v1.0 | 2026-10-06 | 首版创建:运行环境、安装步骤、初始化、启动访问、FAQ、评审演示脚本 | 胡成溯 |
本文件对齐课程《系统验收标准》第 4 项「部署」三要素:运行环境说明(第 1 节)、安装部署过程(第 2~4 节)、部署脚本(run.bat 一键启动 + db.py 初始化脚本)。
(评审后填写)