Skip to content
20-236 edited this page Oct 15, 2026 · 10 revisions

在线购物系统(MVP)设计文档

项目 内容
文档名称 在线购物系统(最小可行产品)设计文档
版本 V1.3
日期 2026-10-14
上游文档 《在线购物系统(MVP)需求分析文档 V2.2》《需求规格说明书 V1.4》
技术栈 Java 17 + Spring Boot 3.4;数据持久化:SQLite 数据库;前端:原生 HTML/CSS/JavaScript(无框架);开发环境:IntelliJ IDEA / VS Code

修订记录(V1.2 → V1.3)

  1. 上游文档引用统一为《需求规格说明书 V1.4》。
  2. §3.2 设计原则修正:明确"输入字段前端预校验"职责——业务规则以后端服务层为唯一权威,前端对价格、图片、密码、字段长度等输入约束做预校验以提升体验(对齐 SRS V1.4 与实际代码)。
  3. §5.2、§7.2、§8 补充发布表单价格/图片、密码强度、字段长度等前端预校验设计。

修订记录(V1.1 → V1.2)

  1. 技术栈由 Python 3 + Flask + JSON 文件切换为 Java 17 + Spring Boot 3.4 + SQLite(Java 重构版),业务设计不变。
  2. §4 数据设计由 data.json 结构改写为 SQLite 表结构(新增 §4.3 表结构 DDL、§4.4 ORM 映射),字段含义、枚举取值、位次计算规则与 V1.1 完全一致。
  3. §2.2 关键技术决策、§2.3 部署与运行、§2.4 开发工具、§3 模块划分、§6.4 并发控制、§6.5 持久化按新栈改写。
  4. 保留 V1.1 的架构分层、模块职责边界、API 契约、状态机、队列与口令码机制;新增 §11 数据迁移设计。

1. 引言

1.1 编写目的

本文档依据《需求分析文档 V2.2》与《需求规格说明书 V1.4》给出系统的技术设计方案,覆盖架构、模块、数据、接口、核心机制与界面设计,作为编码实现与评审的依据。文中所有设计均可向上追溯到需求文档的功能编号(FR)、业务规则(BR)与非功能需求(NFR),追溯关系见第 9 节。

1.2 设计约束

  • 单商品、单卖家、买家免注册;线下交易,无支付/物流。
  • 交付形态为单机可运行的课程作业:一个 Java 进程同时托管静态页面与 API,数据落本地 SQLite 文件,无需安装数据库服务、无需账号密码。
  • 可移植性硬约束:把项目源码拷到另一台机器,只要该机装了 JDK 17,即可直接运行,无需安装依赖、数据库或配置连接串。为此依赖 Maven Wrapper(自带构建工具)与 SQLite(进程内数据库)。
  • 商品五态状态机(在售/冻结/交易失败/恢复在售/下架)与"提交不冻结、先到先得、失败自动递补、口令码贯穿买家侧"是本期设计核心。

2. 总体设计

2.1 架构概述

四层单体架构,单进程运行(见架构图):

层 组成 职责
页面层 买家首页 index.html、口令码管理(首页内视图)、卖家后台 admin.html 原生 HTML/JS,通过 fetch 调 API,无前端框架
API 层 Spring MVC @RestController 参数校验、鉴权(HandlerInterceptor)、调用服务层、统一错误格式(@ControllerAdvice)
服务层 ShoppingService:认证、商品状态机、意向与队列 全部业务规则的唯一落点(状态机、队列、口令码、递补)
持久层 Spring Data JPA 仓储 + SQLite + uploads/ 实体映射、派生查询、事务边界;图片文件存储

2.2 关键技术决策

决策点 选择 理由
数据库 SQLite(sqlite-jdbc + hibernate-community-dialects) 进程内嵌入式数据库,数据库即单个 .db 文件:零安装、零账号密码、零连接配置,拷源码即可运行,直接满足可移植性硬约束;仍支持唯一约束与事务,比 JSON 文件更可靠
表结构创建 Hibernate ddl-auto=update 自动建表 无手写 DDL,首次启动即建表;实体注解即数据字典,避免文档与实现漂移
ORM Spring Data JPA + Hibernate 6 派生查询方法表达"按口令码查""按商品+阶段排序""计数"等需求,业务代码不拼 SQL;@Enumerated(STRING) 保证枚举可读
事务与并发 ShoppingService 方法标 @Transactional + 关键转移方法 synchronized 校验与变更在同一事务内完成,异常回滚;JVM 内单写者语义满足 NFR-01"防一货两卖"
密码哈希 Spring Security SCryptPasswordEncoder(随机盐) 满足 BR-18 加密存储;已验证兼容旧格式 scrypt:<salt>:<hex>,迁移无需重置密码
口令码 SecureRandom 生成 8 位字母数字(去除易混淆字符 0/O、1/I) 空间约 32⁸ ≈ 2⁴⁰,唯一且不可枚举(NFR-03);唯一性由数据库唯一约束兜底
会话 内存 ConcurrentHashMap + HttpOnly Cookie 简单;重启后重新登录即可(买家本无会话)
价格存储 整数"分"(如 ¥9.90 存 990) 避免浮点误差;入参校验 >0 且最多两位小数,展示层格式化

2.3 部署与运行

  • 目录:pom.xml、mvnw / mvnw.cmd、src/main/java/、src/main/resources/、public/(静态页面)、data/shop.db(运行时自动生成)、uploads/(运行时自动生成)。
  • 启动:项目根目录执行 mvnw.cmd spring-boot:run(Windows)或 ./mvnw spring-boot:run,监听 127.0.0.1:3000;买家访问 /,卖家访问 /admin.html。
  • 前置条件只有 JDK 17:Maven 由 Wrapper 自动下载,SQLite 驱动随依赖打包,无需安装数据库服务;data/ 与 uploads/ 由应用启动时自动创建,无需手工建目录(见 §6.6)。首次运行需联网下载依赖,之后可离线。
  • 打包:mvnw.cmd clean package → java -jar target/shopping-system-1.0.0.jar。

2.4 开发工具(按课程要求声明)

类别 工具 说明
编程语言 Java 17 后端业务与 API;前端为浏览器原生 HTML/CSS/JavaScript
集成开发环境 IntelliJ IDEA / VS Code 编码、调试与运行
构建工具 Maven(Maven Wrapper,版本 3.9.16) 项目自带 mvnw,目标机器无需预装 Maven
持续集成 不使用 MVP 规模未引入 Jenkins
数据库 SQLite(嵌入式) 无需安装数据库服务;不使用 MySQL / PostgreSQL
测试工具 自写 HTTP 回归脚本 按 SRS 第 8 章验收;未使用 JUnit / Selenium / JMeter

3. 模块设计

3.1 模块划分

模块 职责 对应需求
控制器层 controller 路由与参数校验、统一错误响应(@RestController + @ControllerAdvice) FR 全项
auth 登录校验、会话签发与校验、修改密码(验原密码、强度校验、scrypt 哈希) FR-S-01、FR-S-09
product 发布商品(含图片上传校验)、查询当前商品、手动冻结/解冻、历史列表 FR-S-02、FR-S-05、FR-S-08、FR-B-01
trade 开始交易(强制取队首)、标记成功、标记失败(作废/重新排队 + 自动递补) FR-S-04、FR-S-06、FR-S-07
intent 提交意向(发口令码)、口令码查询/修改/撤销、队列与位次计算 FR-B-02~05、FR-S-03
ShoppingService 上述业务规则的唯一落点;方法标 @Transactional,关键转移加 synchronized 全部 BR / NFR-01
repository(3 个仓储) 数据访问;以派生查询表达队首、位次计数、口令码查找等 NFR-02
domain(3 实体 + 3 枚举) 表结构映射;枚举以 AttributeConverter 存小写字面量 §4
DataMigrationRunner 一次性导入旧 data.json;无卖家时创建默认账号 NFR-02、可移植性
ShoppingApplication 启动期自举 data/、uploads/ 目录(§6.6) 可移植性
装饰器 require_auth 拦截未登录的后台请求 FR-S-01

3.2 设计原则

  • 状态变更只走服务层:API 路由不直接改数据,所有状态转移经"状态机转移函数"统一入口,前置校验非法即拒绝(返回 409),保证 BR 不被绕过。
  • 业务规则集中:先到先得、冻结停收意向、失败自动递补、口令码失效判定等只在服务层出现一次,前端不做业务规则判断、只做展示。
  • 输入字段前端预校验:价格(>0、两位小数)、图片(JPG/PNG、≤5MB)、新密码(≥8 位、含字母数字)、意向字段长度(姓名 ≤64、电话 ≤32)、口令码(8 位 + trim/大写规范化)等输入约束先在浏览器端校验(HTML 约束属性 + 提交前脚本检查),不通过的请求不发往服务端;服务层保留同一约束的完整校验并作为最终权威(对齐 SRS V1.4 §6.2)。

4. 数据设计

存储介质为 SQLite 数据库(单文件 <项目>/data/shop.db),表结构由 Hibernate 按实体类自动创建(ddl-auto=update),无需手写 DDL。三张表:seller(卖家账号)、product(商品)、intent(购买意向)。表结构即数据字典,字段与 V1.1 的 data.json 一一对应。

4.1 表结构

seller — 卖家账号(单条记录)

列名 类型 约束 说明
id integer PK, 自增 主键
username varchar(64) NOT NULL, UNIQUE(uk_seller_username) 卖家账号名
password_hash varchar(255) NOT NULL 密码哈希,绝不存明文(BR-18)
password_changed_at varchar(32) 可空 改密时间(BR-19)

product — 商品

列名 类型 约束 说明
id integer PK, 自增 主键
name varchar(128) NOT NULL 商品名称
description varchar(2000) NOT NULL 商品描述
image varchar(255) NOT NULL 形如 /uploads/p<时间戳>-<随机>.jpg
price_cents bigint NOT NULL 价格以"分"存储(BR-17)
status varchar(16) NOT NULL active / frozen / resumed / sold
trading_intent_id bigint 可空,无外键 交易中的意向 ID;null 且 frozen 即"手动冻结"
created_at varchar(32) NOT NULL 发布时间(BR-19)
frozen_at varchar(32) 可空 进入冻结的时间(BR-19)
result varchar(32) 可空 交易结果,成功时为 success
result_at varchar(32) 可空 标记交易结果的时间(BR-19)

索引:idx_product_status (status) —— 支撑"当前唯一未售出商品"的查询。

intent — 购买意向

列名 类型 约束 说明
id integer PK, 自增 主键
product_id bigint NOT NULL 所属商品(无数据库外键,见 §4.3)
name varchar(64) NOT NULL 买家姓名(可凭口令码改,BR-12)
phone varchar(32) NOT NULL 联系电话(可凭口令码改,BR-12)
passcode varchar(16) NOT NULL, UNIQUE(列级,内联进建表语句) 8 位口令码,买家唯一凭证(BR-13)
submitted_at varchar(32) NOT NULL 提交时间,决定位次,不可变
stage varchar(16) NOT NULL queued / trading / closed
outcome varchar(16) 可空 success / voided / requeued / cancelled / unsold

索引:idx_intent_product (product_id);idx_intent_queue (product_id, stage, submitted_at, id) —— 后者直接服务队首选取与位次计数。

字段说明:status 取 active 在售 / frozen 冻结 / resumed 恢复在售 / sold 下架;stage 取 queued 排队中 / trading 交易中 / closed 已结束;outcome 取 success 成功 / voided 作废 / requeued 重新排队 / cancelled 撤销 / unsold 未成交。价格以"分"存储(BR-17),密码只存哈希(BR-18),各时间戳字段满足 BR-19。

4.2 设计说明

  1. 五态与落库:"交易失败"是过渡状态、不停留(V2.2 §3.1),在标记失败的同一次请求内完成意向处置与分流,因此不落库;持久化的状态枚举为 active / frozen / resumed / sold 四值。resumed 与 active 行为等价,单列只为保留"失败后重新上架"的来历。
  2. 交易中判定:status=frozen 且 tradingIntentId≠null → 自动冻结(交易中);tradingIntentId=null → 手动冻结。同一时刻至多一条 stage=trading 的意向(NFR-01)。
  3. 队列与位次:不存位次字段。某意向位次 = 同商品下 stage=queued 且 (submittedAt, id) 排序在前的条数 + 1;队首 = 其中排在最小者。由 countByProductIdAndStage 与 findFirstBy...OrderBySubmittedAtAscIdAsc 两条派生查询完成,无需全表加载。
  4. 重新排队落库(V2.2 §9-1):旧意向置 stage=closed, outcome=requeued,旧口令码随之失效;新建意向记录(复制最终姓名/电话、新口令码、submittedAt=当前时间)自然排到队尾,新口令码由卖家线下告知买家。
  5. 历史留痕边界(BR-16):意向记录只在修改时覆盖写姓名/电话,不留旧值;意向无修改/撤销时间戳字段;商品与账号仅保留 4.1 列出的时间戳字段。
  6. 会话不持久化:登录会话存内存 ConcurrentHashMap,重启失效需重新登录(范围外功能"退出登录"不做)。
  7. 图片:随机文件名防覆盖与猜测;商品删除不存在,图片随历史永久保留。

4.3 外键与约束策略

约束 是否建立 理由
intent.passcode UNIQUE ✅ 建立且实测生效 口令码是买家唯一凭证,数据库层兜底防重复发放(BR-13)。用列级 unique = true 而非 @Table(uniqueConstraints=…):后者在 Hibernate 6 + SQLite 下会走"建唯一索引"路径,而该索引不被 UniqueConstraintSchemaGenerator 识别,导致建表后又被 DROP 掉,约束形同虚设。列级写法直接把 unique 内联进 CREATE TABLE。
product / seller 上的其他 UNIQUE ❌ 不建 单卖家、单未售出商品由服务层前置校验保证,业务上不会并发插入同用户名
intent.product_id → product.id FK ❌ 刻意不建 SQLite 的 JDBC 驱动默认 foreign_keys=OFF,外键不参与实际执行,反而让 ddl-auto=update 每轮启动都去比对约束。一致性由服务层保证:意向只会挂在已存在的未售出商品上,且商品永不被删除
product.trading_intent_id → intent.id FK ❌ 刻意不建 同样原因;且"标记失败"过程中存在"商品仍指向已终结意向"的合法中间态(SRS §3.3),硬外键会挡住它
枚举列 CHECK 约束 ❌ 刻意不建 Hibernate 为枚举生成 check (status in (...)),而 SQLite 不支持 ALTER TABLE ADD CONSTRAINT,后续增删枚举值会使 ddl-auto=update 失败。改以 AttributeConverter 存字符串,取值范围由 Java 枚举保证

上表两次"实战踩坑"结论均以实际数据库文件为准:@Table(uniqueConstraints=…) 生成的约束会被 Hibernate 6 自己删除;@ManyToOne @JoinColumn 生成的 FK 对 SQLite 无实际意义。两者都已改为"服务层保证 + 必要处用列级约束"。

4.4 对象-关系映射

实体类 表 关键映射
ProductEntity product @Enumerated 替换为 ProductStatus.Persister(AttributeConverter);枚举只存 varchar(16) 字面量,无 CHECK
IntentEntity intent productId 为普通 Long 列(不建关联、不建外键);passcode 用列级 unique = true
SellerEntity seller passwordHash 映射 password_hash

仓储接口(Spring Data JPA 派生查询):

仓储 方法 用途
ProductRepository findFirstByStatusNotOrderByIdDesc(sold) 取当前唯一未售出商品
existsByStatusNot(sold) 发布前置校验(至多一件未售出)
findByStatusOrderByIdDesc(sold) 历史商品列表(含 sold)
IntentRepository findByPasscode 口令码查询(唯一约束支撑)
findByProductIdOrderBySubmittedAtAscIdAsc 队列按先到先得排序
findByProductIdAndStageOrderBySubmittedAtAscIdAsc 队首选取
existsByPasscode 生成口令码时防重
countByProductIdAndStage 位次计算
SellerRepository findByUsername / findTheSeller() 卖家账号读取

5. 接口设计

统一约定:JSON 请求/响应;错误返回 { "error": "<错误码>" };状态冲突返回 HTTP 409,参数错误 400,未登录 401,口令码无效 403。

5.1 买家侧 API(免登录)

方法 路径 请求体 成功响应 关键规则
GET /api/product — 当前商品(名称/描述/图片/价格)+ status + buyable 冻结时也返回(前端置灰并标识);无未售出商品返回 { "product": null }
POST /api/intents {name, phone} {passcode} 仅 buyable(active/resumed)时接受,否则 409;两项必填;生成独立记录+口令码
POST /api/intents/query {passcode} {stage, position} 位次按 4.2-3 计算;trading 时返回"交易中";意向 closed 或商品 sold → 403 口令码已失效
PUT /api/intents {passcode, name, phone} {ok:true} 仅口令码有效且意向未结束;只改两项、不动 submittedAt(BR-12)
POST /api/intents/cancel {passcode} {ok:true} 置 closed/cancelled,位次前移自动成立;不改商品状态(BR-09)

5.2 卖家侧 API(除登录外均需 require_auth)

方法 路径 请求体 成功响应 关键规则
POST /api/auth/login {username, password} 种 Cookie scrypt 校验
POST /api/auth/password {oldPassword, newPassword} {ok:true} 验原密码;新密码 ≥8 位且含字母+数字,否则 400;更新 passwordChangedAt
POST /api/products multipart:name/description/price/image {id} 前端先预校验价格(>0 两位小数)与图片(JPG/PNG ≤5MB),不合格不发请求(§7.2);服务端前置:无未售出商品,否则 409;图片仅 JPG/PNG ≤5MB 一张;价格 >0 两位小数转分;status=active
GET /api/admin/product — 当前商品 + 交易中意向详情 供后台仪表盘
GET /api/admin/intents — 当前商品队列(位次/姓名/电话/提交时间/stage),含已撤销交易中买家的标记 FR-S-03
POST /api/admin/freeze — {ok:true} active/resumed→frozen(tradingIntentId=null)
POST /api/admin/unfreeze — {ok:true} frozen→active;不动任何意向(BR-06)
POST /api/admin/trade/start — {intentId, name, phone} 系统强制取队首,不可指定;商品→frozen 且绑定 tradingIntentId;队列空则 409
POST /api/admin/trade/success — {ok:true} 需存在 trading 意向;商品→sold;trading 意向→success;其余未结束意向→closed/unsold;全部口令码因 closed/sold 自然失效;不可撤销,前端二次确认
POST /api/admin/trade/fail {disposition:"void"|"requeue"} {ok:true},requeue 时附 {newPasscode} 处置当前 trading 意向;随后队列非空→队首自动转 trading(自动递补)、商品保持 frozen 并更新 frozenAt;队空→商品 resumed;记 resultAt;若当前 trading 意向已被买家撤销,按 void 处理
GET /api/admin/history — sold 商品列表,各附完整意向名单(姓名/电话/提交时间/outcome) FR-S-08、BR-15

5.3 交易核心时序(标记失败为例)

  1. 校验:商品 frozen 且存在 trading 意向,否则 409。
  2. 处置:void → 该意向 closed/voided;requeue → 该意向 closed/requeued,新建记录发新口令码。
  3. 分流:取新队首——存在 → 转 trading、商品保持 frozen;不存在 → 商品 resumed。
  4. 记 resultAt,save_db() 原子落盘,一次性返回结果。

第 2~4 步在同一请求处理过程内顺序完成,是"交易失败"过渡态的实现形态(4.2-1)。


6. 核心机制设计

6.1 状态机实现

  • 单一转移函数 transition(product, event, payload):内部分支校验"当前状态 × 事件"合法性,非法即抛 409;合法则修改状态、联动意向、写时间戳、落盘。
  • 事件集:PUBLISH / SUBMIT_INTENT / START_TRADE / FREEZE / UNFREEZE / MARK_SUCCESS / MARK_FAIL / CANCEL_INTENT。
  • SUBMIT_INTENT、CANCEL_INTENT 不改变商品状态(BR-01、BR-09),仅操作意向表。

6.2 先到先得与自动递补

  • "开始交易"接口不接受指定买家参数,从数据源上杜绝跳选(BR-04)。
  • 递补与"开始交易"复用同一段"取队首并绑定"逻辑,保证两种入口行为一致(BR-05)。

6.3 口令码

  • 生成:SecureRandom 在 32 字符字母表(去除 0/O、1/I)取 8 位,生成后查重(BR 唯一性),数据库列级 UNIQUE 兜底。
  • 失效判定不集中维护:查询/修改/撤销时实时检查"意向 closed 或所属商品 sold"即拒绝,自然覆盖 BR-13 全部失效时机(成功/作废/撤销/未成交/下架/重新排队)。
  • 页面只展示一次(提交成功页),响应中不回显队列里他人的任何信息(NFR-03)。

6.4 防一货两卖(NFR-01)

  • 至多一件未售出商品:发布前置校验(existsByStatusNot(sold))+ 售出后才允许再发布。
  • 至多一条 trading:绑定前校验当前无 trading。
  • 所有"校验 + 变更"在同一 @Transactional 方法内顺序完成,关键转移方法加 synchronized 保证 JVM 内单写者语义,请求间无交错执行。

6.5 持久化

  • 启动:Hibernate 按实体自动建表(ddl-auto=update);若数据库为空则 DataMigrationRunner 创建默认卖家账号。
  • 每次成功写操作在同一事务内提交落库;异常回滚,不产生半成品状态。
  • 重启后商品、意向、口令码、历史全部恢复;仅会话需重新登录。

6.6 启动期目录自举(可移植性关键)

SQLite 驱动只创建数据库文件,不创建其父目录;目录不存在会直接抛 path to './data/shop.db': '...\data' does not exist 并导致启动失败。全新克隆的项目里没有 data/,因此启动时必须自行创建。

实现要点:

  • 监听 ApplicationEnvironmentPreparedEvent,在 JPA EntityManagerFactory 构建之前创建目录。 不可放在 SpringApplication.run(...) 之后——run() 在上下文刷新阶段就已完成 DDL 执行,晚于此的一切都来不及。
  • 目录从已解析的配置读取,因此 SHOP_DB_FILE / SHOP_UPLOAD_DIR 指定的自定义路径同样生效。
  • 同时创建数据库文件父目录与图片上传目录;:memory: 等非落盘 URL 跳过。

7. 界面设计

7.1 买家首页(/)

三个视图区域:

  1. 商品区:主图、名称、描述、价格(¥xx.xx)。
    • 在售/恢复在售:显示"我要购买"表单(姓名、联系电话,均必填)。
    • 冻结:商品正常展示 + "已被预定/暂停接受意向"标识,表单隐藏(V2.2 §9-4)。
    • 无商品:提示"暂无在售商品"。
  2. 提交成功区:大号展示口令码 + "请立即保存,遗失无法找回"提示;提供"复制"按钮。
  3. 口令码管理区:输入口令码 → 显示排队位次或"交易中";提供修改姓名/电话表单与"撤销意向"按钮(撤销前弹确认)。

7.2 卖家后台(/admin.html)

  • 登录视图:用户名 + 密码;失败提示不区分账号错还是密码错。
  • 仪表盘(登录后):
    • 当前商品卡片:信息 + 状态徽章(在售/冻结-交易中/冻结-手动/恢复在售)+ 按状态显示的操作按钮(发布/开始交易/手动冻结/手动解冻/标记成功/标记失败)。
    • "标记失败"弹出二选一:作废 / 重新排队;选重新排队则结果页显著展示新口令码,提示卖家线下转交买家。
    • "标记成功"二次确认弹窗,文案明示"不可撤销"(FR-S-06)。
    • 交易中买家若已自行撤销,队列中该行置灰标记"买家已撤销"。
    • 意向队列表:位次、姓名、电话、提交时间、状态。
    • 历史商品:可展开列表,每个商品附完整意向名单(姓名/电话/提交时间/结局)。
    • 发布商品表单:价格输入用 type=number(min 0.01 / step 0.01)+ 提交前脚本校验(>0、最多两位小数、上限 9999999.99);图片选择用 accept 过滤格式 + 提交前脚本校验类型与大小(JPG/PNG、≤5MB);名称/描述按数据字典限长(128/2000)。校验不通过即时提示且不发请求(SRS V1.4 §6.2)。
    • 修改密码表单:新密码输入加 minlength + pattern(≥8 位、含字母数字),提交前再做脚本预校验;服务端校验为最终权威。

8. 安全设计

威胁 对策 对应需求
密码泄露 scrypt + 随机盐,只存哈希;登录失败不透露具体原因 BR-18
弱密码 改密强制 ≥8 位且字母+数字(前后端双重校验) FR-S-09
未授权访问后台 全部 /api/admin/*、/api/auth/password、/api/products 经 require_auth;Cookie HttpOnly FR-S-01
口令码被枚举 32⁸ 随机空间 + 全匹配查询 + 无效/失效统一 403 不区分 NFR-03
买家信息泄露 队列与联系方式仅后台接口返回;买家侧接口按口令码只返回本人的位次与阶段 NFR-03
恶意上传 前端预校验(accept 过滤 + 脚本检查类型/大小)+ 服务端限制单文件 ≤5MB、MIME 白名单 JPG/PNG、随机文件名 BR-17
恶意灌意向 本版本按需求方口径不做限制,仅在此登记为已知风险 V2.2 §8 范围外

9. 需求-设计追踪矩阵

需求 设计落点
FR-B-01 浏览商品 GET /api/product;买家首页三态展示(§7.1)
FR-B-02 提交意向 POST /api/intents;口令码生成(§6.3)
FR-B-03/04/05 查/改/撤 intents query/PUT/cancel;失效判定(§6.3)
FR-S-01 登录 auth 模块 + require_auth(§8)
FR-S-02 发布商品 POST /api/products;图片/价格校验(§5.2)
FR-S-03 查看队列 GET /api/admin/intents;位次计算(§4.2-3)
FR-S-04 开始交易 trade/start 强制取队首(§6.2)
FR-S-05 手动冻结/解冻 freeze/unfreeze;不碰意向(§5.2)
FR-S-06 标记成功 trade/success;不可逆 + 落选者 unsold(§5.2)
FR-S-07 标记失败 trade/fail;作废/重新排队 + 自动递补(§5.3)
FR-S-08 历史商品 GET /api/admin/history(BR-15/16)
FR-S-09 修改密码 auth/password;强度规则(§8)
BR-01~19 状态机转移函数(§6.1)+ 各接口"关键规则"列(§5)
NFR-01 防一货两卖 事务内顺序处理 + synchronized + 状态校验(§6.4)
NFR-02 持久化 SQLite 事务提交(§6.5);表由实体自动创建(§4.1)
NFR-03 安全 §8 全表
NFR-04 可用性 免注册三页面流程(§7)

10. 与既有实现的关系

早期原型 shop-mvp 与组员实现 se-course-project 均为 Node.js/Express 技术栈的"提交即冻结"旧模型验证版本;其后有一版 Python + Flask + data.json 实现(本文档 V1.1)。版本 V1.2 按课程工具要求切换为 Java 17 + Spring Boot 3.4 + SQLite,但数据层结构(§4)与接口设计(§5)与语言无关,业务设计保持完全一致:状态机、队列、口令码、递补规则一字未改,前端页面亦未改动(原 /api/* 路径与 JSON 响应结构全量保留)。


11. 数据迁移设计

为让旧版 data.json 数据不丢失,Java 版内置一次性迁移(DataMigrationRunner,实现 ApplicationRunner)。

触发条件(三个条件同时满足才执行)

  1. seller、product、intent 三张表全部为空;
  2. 配置的旧数据文件(默认 ./data.json,可用 SHOP_DATA_FILE 覆盖)存在。

任一条件不满足即跳过,绝不覆盖已有数据,因此该逻辑可安全地留在代码中长期存在。

迁移内容与规则

迁移对象 规则
卖家 passwordHash 原样复制(不重新哈希),因此旧密码继续可用;passwordChangedAt 一并保留
商品 显式指定 id,保持与原文件一致;status 按字面量解析为枚举
意向 显式指定 id 与 productId,保持关联与位次语义不变;stage / outcome 为 null 时正确置空

无旧文件时的初始化

  • 若数据库中不存在任何卖家账号,创建默认账号 admin,密码 admin1234(可用环境变量 DEFAULT_SELLER_PASSWORD 覆盖),口令以 scrypt 哈希存储。

密码兼容性 系统同时支持两种哈希格式的校验,因此无论数据是"迁移来的"还是"新建的"都能登录:

  • 新格式:Spring Security SCryptPasswordEncoder 输出($100801$<salt>$<hash>);
  • 旧格式:data.json 中的 scrypt:<salt>:<hex>(BouncyCastle 参数)。

Clone this wiki locally