互联网应用开发技术课程作业 技术栈:React 19 + React Router 7 + Ant Design 6 + Vite 7(前端) · Spring Boot 3.3.5 + Spring Data JPA + Spring Security + MySQL 8(后端) · Fetch API(前后端通信) · JUnit 5 + Mockito(测试)
答辩演示账号:
demo/123456| 一键跑测试:cd backend && mvn test(76 用例)
当前新增REST分页/新增/库存接口、OpenAPI 3.0规范、真实DeepSeek库存与模拟竞价助手、真实中文向量政策检索、双工具购书向导;新数据库种子16本书。按作业要求分功能完成本地前后端验收并提交推送。入口、证据与提交文件见 作业1~3验收记录;精简源码包见 docs/assignments/提交/。本轮仅验收桌面,未检测手机比例。
在迭代一基础上,把"前端展示的所有数据都来自后端数据库、前端的所有操作都反映回数据库"这条主线打通。
- 后端:新增登录、购物车、订单 三个领域;表结构、实体、Repository、Service(接口 + 实现)、Controller 一应俱全。
- 前端:登录态接入、购物车数据源切到后端、下单调真接口、订单历史页。
- 前后端通信:使用 Fetch API;后端所有响应都用统一
ApiResponse<T>包装;前端request()统一解包。 - 数据库:所有持久化由 Spring Data JPA 负责,定制 SQL 用
@Query展示。
- Spring Security + BCrypt:密码不再明文——注册时
encode()加盐哈希、登录时matches()比对;SecurityConfig保守放行/api/**,不影响既有功能。 - 图书搜索:
GET /api/v1/books?keyword=按标题/作者模糊查询(派生查询 + JPQL 两种写法对照);前端列表页新增搜索框,与分类过滤叠加。 - 原迭代三阶段JUnit测试:当时
backend/src/test/下26个用例(当前为76项)(Service 层 Mockito + Repository 层@DataJpaTest),mvn test全绿,用 H2 内存库、不依赖 MySQL。 - 详情页接真接口:
BookDetailPage改为useEffect调GET /api/books/{id},优先展示数据库数据。
采用米白背景、墨绿主色和宋体标题,通过 CSS 变量与 Ant Design ConfigProvider 统一颜色、字号、按钮和表单。
- 导航与选书:顶部品牌、全站搜索与横向导航;书籍封面完整显示,分类和价格排序配合真实搜索结果。
- 购买流程:详情页分区展示书籍信息;购物车在桌面采用商品列表与右侧金额摘要,手机改为逐件商品布局;订单保留日期与书名筛选。
- 账户与管理:登录注册、个人信息、消费统计、书籍管理和用户管理沿用同一套样式,保留现有接口和操作。
- 手机适配:导航抽屉、自动换行的筛选与按钮、完整可操作的表单,以及可横向滚动的数据表格。
线上书城系统/
├── README.md ← 本文档
├── package.json ← 前端依赖与脚本
├── vite.config.js ← Vite 代理 /api → :8080
├── embedding-service/ ← 中文BGE模型、分块、向量检索与版本缓存
├── index.html ← Vite 入口
├── public/ ← 静态资源
├── src/ ← 前端源代码
│ ├── main.jsx
│ ├── App.jsx ← 路由 + 全局状态(用户态、购物车)
│ ├── api/
│ │ └── bookstoreApi.js ← Fetch HTTP 封装:解包 ApiResponse
│ ├── services/ ← 业务服务层
│ │ ├── authService.js
│ │ └── cartService.js
│ ├── components/ ← 通用组件
│ │ ├── Layout.jsx ← 顶部搜索与导航、账户、手机导航抽屉
│ │ ├── BookCard.jsx
│ │ ├── HeroBanner.jsx
│ │ └── CategoryFilter.jsx
│ ├── pages/ ← 路由级页面
│ │ ├── BookListPage.jsx
│ │ ├── BookDetailPage.jsx
│ │ ├── AssistantPage.jsx ← ISBN库存与模拟竞价
│ │ ├── PolicyPage.jsx ← 完整条款与中文向量检索
│ │ ├── GuidePage.jsx ← 书目+政策双工具导购
│ │ ├── CartPage.jsx
│ │ └── ProfilePage.jsx ← 登录 / 注册 / 订单历史
│ ├── utils/ ← 纯函数工具
│ │ ├── auth.js ← localStorage 用户态读写
│ │ └── formatter.js ← formatPrice / formatDateTime
│ ├── data/Data.json ← 离线降级数据(后端不可达时用)
│ └── styles.css
└── backend/ ← Spring Boot 后端
├── pom.xml
├── database/
│ └── bookstore.sql ← 完整 DDL + 种子数据
├── src/main/
├── java/com/homework/bookstore/
│ ├── BookstoreBackendApplication.java
│ ├── controller/ ← 控制层:HTTP 入口
│ │ ├── BookController.java
│ │ ├── UserController.java
│ │ ├── CartController.java
│ │ ├── OrderController.java
│ │ └── GlobalExceptionHandler.java
│ ├── service/ ← 服务层:业务编排(接口)
│ │ ├── UserService.java
│ │ ├── CartService.java
│ │ ├── OrderService.java
│ │ ├── exception/BusinessException.java
│ │ └── impl/ ← 服务层实现
│ │ ├── UserServiceImpl.java
│ │ ├── CartServiceImpl.java
│ │ └── OrderServiceImpl.java
│ ├── repository/ ← 数据访问层
│ │ ├── BookRepository.java
│ │ ├── UserRepository.java
│ │ ├── CartItemRepository.java
│ │ ├── OrderRepository.java
│ │ └── OrderItemRepository.java
│ ├── entity/ ← 实体层(@Entity)
│ │ ├── Book.java
│ │ ├── User.java
│ │ ├── CartItem.java
│ │ ├── Order.java
│ │ ├── OrderItem.java
│ │ └── OrderStatus.java
│ ├── dto/ ← 出入参对象
│ │ ├── ApiResponse.java
│ │ ├── LoginRequest.java
│ │ ├── RegisterRequest.java
│ │ ├── UserResponse.java
│ │ ├── AddToCartRequest.java
│ │ ├── UpdateCartItemRequest.java
│ │ ├── CartItemDto.java
│ │ ├── OrderDto.java
│ │ └── OrderItemDto.java
│ └── config/
│ ├── WebConfig.java ← CORS 跨域配置
│ └── SecurityConfig.java ← 迭代三:Spring Security 过滤器链 + BCrypt 编码器
└── resources/
├── application.yml ← 数据源 + JPA 配置
├── application-local.yml
└── data.sql ← 启动时自动执行的种子数据(demo 用户为 BCrypt 密文)
└── src/test/ ← 迭代三新增:JUnit 单元测试
├── java/com/homework/bookstore/service/impl/ ← 4 个 Service 测试类(Mockito 单测)
├── java/com/homework/bookstore/repository/ ← BookRepositoryTest(@DataJpaTest 切片)
└── resources/application.yml ← 测试专用 H2 内存库配置
| 目录 | 职责 | 设计理由(对应评分 D.i) |
|---|---|---|
components/ |
UI 组件,不含业务逻辑 | 复用与可测试 |
pages/ |
路由级页面,承载交互逻辑 | 与 React Router 1:1 对应 |
api/ |
纯 HTTP 封装 | 把"发请求"与"做业务"解耦:换接口前缀只改一处 |
services/ |
业务封装层 | 隐藏 fetch 细节、保留扩展空间(缓存、防抖、本地与远程合并) |
utils/ |
纯函数工具 | 与组件解耦,便于单测 |
data/Data.json |
离线降级 | 后端不可达时仍能演示前端 UI |
| 层 | 包名 | 职责 | 设计理由(对应评分 D.ii / D.iii) |
|---|---|---|---|
| 控制层 | controller/ |
解析 HTTP 请求、调 service、包装 ApiResponse |
只关心 HTTP,不做业务 |
| 服务层 | service/ (接口) + service/impl/ (实现) |
业务编排、事务 | 接口与实现分离:单测好做、可换实现,对应评分 D.iii |
| 数据访问层 | repository/ |
通过 JPA 与数据库交互 | 隔离持久化技术细节 |
| 实体层 | entity/ |
@Entity 映射数据库表 |
评分标准 A.iii:"抽象成实体类再做处理" |
| DTO | dto/ |
出入参对象 | 隔离实体与外部契约,避免脏字段(如 password)泄露 |
| 工具 | 版本 | 验证命令 |
|---|---|---|
| JDK | 17 及以上(编译目标为 17,实测 17–26 均可运行) | java -version |
| Maven | 3.8+(或用 IDE 内置) | mvn -version |
| Node.js | 20.19+(20.x)或 22.12+ | node --version |
| MySQL | 8.x(服务需已启动) | mysqladmin ping |
| curl / Python | 模型下载 / 精简打包需要 | curl --version / python3 --version |
关于 JDK 版本:
pom.xml编译目标是 Java 17。若你的机器是 JDK 21+,跑测试所需的 Mockito / ByteBuddy 版本已在pom.xml里处理好兼容,mvn test可直接运行,无需切换 JDK。
后端默认连接 localhost:3306/bookstore,用户名 root,密码默认留空。若你的 MySQL root 设了密码,用环境变量覆盖(见第 2 步),不必改任何代码。启动顺序固定为:MySQL → Embedding服务 → 后端 → 前端 → 浏览器操作。
建库有两种等价方式,任选其一:
- 方式 A(推荐,零操作):什么都不用做。后端首次启动时 JPA 的
ddl-auto: update会自动建库建表,data.sql自动插入 16 本书 + demo 用户。连接串带了createDatabaseIfNotExist=true,库不存在也会自动创建。 - 方式 B(手动导入 DDL):
mysql -uroot < backend/database/bookstore.sql # root 无密码 mysql -uroot -p < backend/database/bookstore.sql # root 有密码(回车后输入)
若使用政策检索与购书向导,先在 embedding-service 执行 npm ci、npm run download-model、npm start;等待 http://127.0.0.1:8091/status 为ready。复制 backend/.env.example 为忽略的 .env,配置DeepSeek密钥与MySQL。完整三服务说明见 作业3运行说明。
cd backend
mvn spring-boot:run # ① root 无密码,直接启动
MYSQL_PASSWORD=你的密码 mvn spring-boot:run # ② root 有密码,用环境变量覆盖(不改文件)可覆盖的环境变量(均有默认值):MYSQL_HOST(localhost)、MYSQL_PORT(3306)、MYSQL_DATABASE(bookstore)、MYSQL_USER(root)、MYSQL_PASSWORD(空)。
看到日志 Started BookstoreBackendApplication 即启动成功。这个终端需要一直开着,前端请求会通过 Vite 代理转到这里。快速自检:
curl localhost:8080/api/v1/books # 应返回 16 本书的 JSON
curl 'localhost:8080/api/v1/books?keyword=代码' # 迭代三搜索:应只返回《代码整洁之道》# 注意:在项目根目录执行,不是 backend/
npm install # 首次运行需要;已装过可跳过
npm run dev浏览器打开 http://localhost:5173(或终端提示的实际地址)。Vite 已配代理,/api 请求自动转发到 8080,开发时无跨域问题。前端终端也需要保持运行。
cd backend
mvn test # 当前76项:Service Mockito + Repository H2 + Controller契约测试测试用 H2 内存数据库,不需要 MySQL、不污染开发库。控制台会打印 Hibernate 生成的真实 SQL(已开 org.hibernate.SQL: debug),可现场演示派生查询/JPQL 翻译结果。
npm run build # 前端产物 → dist/
cd backend && mvn clean package # 后端产物 → backend/target/*.jar,可 java -jar 运行- 主页
/books:来自数据库的 16 本书;顶部搜索框输入「代码」或「norman」(迭代三新增,作者名忽略大小写)→ 结果实时过滤,可与分类 Tab 叠加; /profile:用demo / 123456登录(密码在库中是 BCrypt 密文,登录时matches比对);- 进任意书详情页(数据来自
GET /api/books/{id}真实接口,F12 Network 可见)→「加入购物车」; /cart:改数量、删除;点「提交订单」→ 自动跳/profile看到新订单;- 持久化校验:退出登录再登录,订单仍在,购物车(若没下单)仍在。
users (1) ─── (N) cart_items (N) ─── (1) books
users (1) ─── (N) orders (1) ─── (N) order_items (N) ─── (1) books
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | BIGINT | PK, AUTO_INCREMENT | 主键 |
| username | VARCHAR(60) | NOT NULL, UNIQUE | 登录名 |
| password | VARCHAR(120) | NOT NULL | 密码(迭代三起为 BCrypt 加盐哈希,$2a$10$... 60 字符密文) |
| VARCHAR(120) | NOT NULL, UNIQUE | 邮箱 | |
| phone | VARCHAR(30) | NULL | 手机号 |
| created_at | DATETIME | NOT NULL | 注册时间 |
主键 id 是字符串(如 clean-code、three-body),其余字段为标题、作者、价格、分类、图片、富文本简介等共 16 列。
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | BIGINT | PK | 主键 |
| user_id | BIGINT | NOT NULL, FK→users | 所属用户 |
| book_id | VARCHAR(80) | NOT NULL, FK→books | 书籍 |
| quantity | INT | NOT NULL, default 1 | 数量 |
| created_at | DATETIME | NOT NULL | |
| updated_at | DATETIME | NOT NULL | |
| UK | (user_id, book_id) | UNIQUE | 同一用户同一本书只有一条记录,再次加车数量累加 |
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | BIGINT | PK | |
| user_id | BIGINT | NOT NULL, FK→users | 下单用户 |
| total_amount | DECIMAL(10,2) | NOT NULL | 订单总额 |
| status | VARCHAR(20) | NOT NULL | PENDING / PAID |
| created_at | DATETIME | NOT NULL |
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | BIGINT | PK | |
| order_id | BIGINT | NOT NULL, FK→orders ON DELETE CASCADE | |
| book_id | VARCHAR(80) | NOT NULL, FK→books | |
| book_title | VARCHAR(120) | NOT NULL | 快照:避免书名后续被改影响历史订单 |
| book_image | VARCHAR(255) | NULL | 同上,封面快照 |
| unit_price | DECIMAL(10,2) | NOT NULL | 快照:成交价 |
| quantity | INT | NOT NULL | 数量 |
为什么订单明细要存快照? 因为书的价格、书名以后可能改,如果订单不存快照、用户三年后回头看订单,会发现金额对不上当时支付的款。
- 建立连接:
application.yml配置 datasource URL / username / password;Spring Boot 启动时通过HikariCP创建连接池,懒加载第一次访问数据库时才建立物理连接。 - DDL:
spring.jpa.hibernate.ddl-auto=update让 Hibernate 在启动时扫描所有@Entity类,若数据库里没有对应表则建表、字段缺失则加列(不会删字段)。 - 种子数据:
spring.sql.init.mode=always+defer-datasource-initialization=true让data.sql在 JPA 建完表后执行,幂等地插入 books 和 demo 用户。 - 请求时:Controller → Service(带
@Transactional)→ Repository 方法 → JPA 生成 JPQL/SQL → JDBC 执行 → 结果通过 Hibernate 反射映射回实体对象。 - 事务:以
OrderServiceImpl.placeOrder为例,整个方法在一个事务里,期间任何一步抛异常都会触发回滚,保证"订单已生成但购物车没清空"这种坏数据不会出现。
所有接口返回体统一为:
{
"code": 0, // 0 = 成功;非 0 = 业务错误
"message": "ok",
"data": { ... } // 业务数据
}评分标准 C.i:JSON 数据格式设计合理——使用统一外壳后,前端只需写一段
unwrap()即可处理所有响应,不用为每个接口单独判错。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/v1/books |
列出所有书籍 |
| GET | /api/v1/books?keyword={关键字} |
按书名或作者模糊搜索,忽略大小写 |
| GET | /api/books/{id} |
获取书籍详情 |
示例:
GET /api/v1/books
{
"code": 0,
"message": "ok",
"data": [
{ "id": "clean-code", "title": "代码整洁之道", "price": 79.00, ... }
]
}| 方法 | 路径 | 请求体 | 说明 |
|---|---|---|---|
| POST | /api/v1/users/register |
{username, password, email, phone} |
注册 |
| POST | /api/v1/users/login |
{username, password} |
登录 |
POST /api/v1/users/login
Body: { "username": "demo", "password": "123456" }
{
"code": 0,
"message": "登录成功",
"data": { "id": 1, "username": "demo", "email": "demo@bookstore.com", "phone": "13800000000", "createdAt": "2026-05-23T22:00:00" }
}| 方法 | 路径 | 请求 | 说明 |
|---|---|---|---|
| GET | /api/v1/cart?userId={id} |
— | 查询用户购物车 |
| POST | /api/v1/cart/items |
{userId, bookId, quantity} |
加入购物车(已存在则累加) |
| PUT | /api/v1/cart/items/{id} |
{quantity} |
修改某条数量 |
| DELETE | /api/v1/cart/items/{id} |
— | 删除某条 |
| DELETE | /api/v1/cart?userId={id} |
— | 清空 |
GET /api/v1/cart?userId=1
{
"code": 0,
"data": [
{
"id": 8,
"bookId": "clean-code",
"title": "代码整洁之道",
"image": "/images/代码整洁之道.JPG",
"price": 79.00,
"originalPrice": 99.00,
"quantity": 2,
"subtotal": 158.00
}
]
}| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/v1/orders?userId={id} |
下单(事务:读购物车 → 生成订单 → 清空购物车) |
| GET | /api/v1/orders?userId={id} |
用户订单列表 |
| GET | /api/v1/orders/{id} |
订单详情 |
POST /api/v1/orders?userId=1
{
"code": 0,
"message": "下单成功",
"data": {
"id": 7,
"userId": 1,
"totalAmount": 158.00,
"status": "PAID",
"createdAt": "2026-05-23T23:00:00",
"items": [ { "bookId": "clean-code", "bookTitle": "代码整洁之道", "unitPrice": 79.00, "quantity": 2, "subtotal": 158.00 } ]
}
}对应评分标准 C.ii:详述"前端 JS 发请求 → 后端 Java 处理 → 访问数据库 → 抽象组装数据 → 发回前端 → 前端刷新"全过程。 选最长的一条链路:用户在购物车点"提交订单"。
src/pages/CartPage.jsx 的"提交订单"按钮 onClick={onSubmitOrder} 触发 src/App.jsx 中的 handleSubmitOrder():
async function handleSubmitOrder() {
if (!user) { message.warning("请先登录"); return false; }
if (!cart.length) return false;
try {
await checkout(user.id); // ← cartService.checkout
await refreshCart();
message.success("结算成功!已生成订单。");
navigate("/profile");
return true;
} catch (err) {
message.error(err.message || "下单失败");
return false;
}
}checkout 来自 src/services/cartService.js,最终调到 src/api/bookstoreApi.js:
export function placeOrder(userId) {
return request(`/orders?userId=${encodeURIComponent(userId)}`, {
method: "POST",
});
}request() 用 fetch 发送:
const response = await fetch(`/api/v1${path}`, {
headers: { "Content-Type": "application/json" },
method: "POST",
});Vite dev server 看到 /api 前缀,按 vite.config.js 的 proxy 转发到 http://localhost:8080。
@PostMapping
public ApiResponse<OrderDto> placeOrder(@RequestParam Long userId) {
return ApiResponse.success("下单成功", orderService.placeOrder(userId));
}Controller 只做一件事:把 HTTP 参数交给 Service,并用 ApiResponse 包装返回。
private static final BigDecimal FREE_SHIPPING_THRESHOLD = new BigDecimal("99.00");
private static final BigDecimal SHIPPING_FEE = new BigDecimal("12.00");
@Override
@Transactional // 整体事务
public OrderDto placeOrder(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new BusinessException(40401, "用户不存在"));
List<CartItem> cartItems = cartItemRepository
.findByUser_IdOrderByCreatedAtAsc(userId); // ① 读购物车
if (cartItems.isEmpty()) {
throw new BusinessException(40001, "购物车为空,无法下单");
}
Order order = new Order();
order.setUser(user);
order.setStatus(OrderStatus.PAID);
BigDecimal subtotal = BigDecimal.ZERO;
for (CartItem cartItem : cartItems) { // ② 构建明细 + 累计商品小计
OrderItem oi = new OrderItem();
oi.setBook(cartItem.getBook());
oi.setBookTitle(cartItem.getBook().getTitle()); // 价格、书名快照
oi.setBookImage(cartItem.getBook().getImage());
oi.setUnitPrice(cartItem.getBook().getPrice());
oi.setQuantity(cartItem.getQuantity());
order.addItem(oi);
subtotal = subtotal.add(cartItem.getBook().getPrice()
.multiply(BigDecimal.valueOf(cartItem.getQuantity())));
}
BigDecimal shipping = subtotal.signum() > 0
&& subtotal.compareTo(FREE_SHIPPING_THRESHOLD) < 0
? SHIPPING_FEE : BigDecimal.ZERO;
order.setTotalAmount(subtotal.add(shipping));
Order saved = orderRepository.save(order); // ③ 落库(含级联保存 items)
cartItemRepository.deleteByUserId(userId); // ④ 清空购物车
return OrderDto.from(saved); // ⑤ 抽象成 DTO
}事务保证:①~④ 任何一步失败,整个事务回滚,不会出现"订单已建但购物车没清"或"购物车清了但订单丢失"。
订单应付总额为商品小计加运费:商品小计大于零且不足 99 元时收 12 元,满 99 元或零元免运费。例如三体 49 元 × 2 本,商品小计 98 元,应付并保存 110 元。规则由后端在下单事务中计算,不接受前端传入的金额。历史订单保留已保存的总额;个人总金额与用户消费榜按订单总额汇总,图书销售额和购书明细金额按商品价格快照汇总,不含运费。
orderRepository.save(order) 由 Spring Data JPA 实现:
- Hibernate 通过反射读取
@Entity注解,生成 SQLINSERT INTO orders ... - 因为
Order的items是@OneToMany(cascade=ALL),会自动INSERT INTO order_items ...N 行 cartItemRepository.deleteByUserId(userId)走的是 CartItemRepository 里的@Query("delete from CartItem c where c.user.id = :userId")自定义 JPQL,对应一次DELETE FROM cart_items WHERE user_id = ?
OrderDto.from(saved) 把 JPA 实体 Order 转成对外的 OrderDto,剥离 password / 双向引用 等内部字段。Jackson 自动序列化为 JSON。最终经过 Controller 包成:
{ "code": 0, "message": "下单成功", "data": { "id": 7, "totalAmount": 158.00, ... } }回到 bookstoreApi.js 的 request():
if (payload && Object.prototype.hasOwnProperty.call(payload, "code")) {
if (payload.code !== 0) throw new Error(payload.message || "...");
return payload.data; // 拆掉外壳,只返回 data
}App.jsx 的 handleSubmitOrder 拿到结果后:
await refreshCart()重新拉一次购物车(已是空数组);message.success(...)顶部弹提示;navigate("/profile")跳到个人中心;- ProfilePage.jsx 中的
<OrderHistory />在useEffect里调getOrders(user.id),新订单出现在列表顶部。
整个链路前端 5 个文件、后端 6 个类协同完成,没有一处使用本地缓存数据。
- 关注点分离:每层只对相邻层负责。Controller 不直接碰 Repository,Service 不直接碰 HTTP。
- 接口与实现分离(评分 D.iii):每个 Service 都写成
interface XxxService+XxxServiceImpl,调用方依赖接口;以后想换实现(如换成调用第三方支付)只换实现类即可,调用方零修改。 - 实体抽象(评分 A.iii):数据库表 →
@Entity;前端契约 → DTO。中间转换由XxxDto.from(entity)显式做。
- HTTP 与业务解耦:
api/只负责发请求,services/包装业务,组件不直接 fetch。 - 状态最小化:用户态只存在 localStorage + App.jsx 顶层 state;购物车只在登录态变化时拉取,避免重复请求。
- 离线降级:书籍列表后端不可达时回落到
data/Data.json,不让作业演示在断网时直接白屏。
backend/src/main/java/com/homework/bookstore/config/WebConfig.java 通过 addCorsMappings 允许 http://127.0.0.1:5173 与 http://localhost:5173。开发时 Vite 走 proxy 直接转发,浏览器看到的同源是 5173,不会触发 CORS;但保留 CORS 配置便于在生产把前端独立部署。
本表按《课程大作业迭代3要求细则.pdf》的 30 分口径整理,方便验收前逐项核对。
| 评分项 | 分值 | 实现情况 | 关键位置 |
|---|---|---|---|
| A. 功能实现 | 10 | 已实现 | 登录 / 注册、书籍列表、书籍详情、搜索、购物车、下单、订单历史均走后端真实接口 |
| 登录与注册 | — | 已实现 | UserController、UserServiceImpl、ProfilePage |
| 书籍列表、详情、搜索 | — | 已实现 | BookController、BookServiceImpl、BookListPage、BookDetailPage |
| 购物车与订单 | — | 已实现 | CartController、OrderController、CartPage |
| B. 技术方案 | 10 | 已实现 | 前端组件化、后端分层、接口与实现分离、Spring JPA / ORM |
| 前端组件化开发 | 4 | 已实现 | components/、services/、pages/、utils/ 分包;HTTP 封装在 bookstoreApi.js |
| 后端分层架构 | 4 | 已实现 | controller/、service/、service/impl/、repository/、entity/、dto/;详见第 2 节与第 7 节 |
| 接口与实现分离 + 依赖注入 | — | 已实现 | BookService / BookServiceImpl、UserService / UserServiceImpl、CartService / CartServiceImpl、OrderService / OrderServiceImpl,通过构造器注入 Repository |
| ORM / Spring JPA | 2 | 已实现 | entity/ 与 repository/;Order 到 OrderItem 使用级联保存 |
| C. 代码质量 | 5 | 已实现 | 命名、分层、封装、测试与必要注释 |
| 项目结构、命名、封装、测试 | 3 | 已实现 | 前后端目录清晰;后端 src/test 含76个JUnit / Mockito / DataJpaTest用例 |
| 必要注释 | 2 | 已实现 | pom.xml、配置类、关键业务方法、README 与架构文档补充了答辩说明 |
| D. 界面友好 | 5 | 已实现 | Ant Design 页面、电子商务常见操作路径、响应式布局 |
| 操作习惯 | 2 | 已实现 | 书籍浏览 → 详情 → 加购 → 购物车 → 下单 → 个人中心订单历史 |
| 界面美观 | 2 | 已实现 | 米白与墨绿主题、完整书封、分类筛选、详情分区、响应式购物车、订单卡片及统一账户与管理页面 |
| 体验完整度 | 1 | 已实现 | 登录态持久化、后端不可达时图书列表降级展示、操作反馈使用 Ant Design message |
后端启动后,按顺序执行可一次性验证主链路:
# 1. 登录
curl -X POST localhost:8080/api/v1/users/login \
-H 'Content-Type: application/json' \
-d '{"username":"demo","password":"123456"}'
# → 返回 { code:0, data:{ id:1, username:"demo", ... } }
# 记下 data.id,下面用 USER_ID 代替
# 2. 查书籍列表
curl localhost:8080/api/v1/books
# 3. 加入购物车(demo userId=1)
curl -X POST localhost:8080/api/v1/cart/items \
-H 'Content-Type: application/json' \
-d '{"userId":1,"bookId":"clean-code","quantity":2}'
# 4. 再加另一本
curl -X POST localhost:8080/api/v1/cart/items \
-H 'Content-Type: application/json' \
-d '{"userId":1,"bookId":"three-body","quantity":1}'
# 5. 查购物车
curl "localhost:8080/api/v1/cart?userId=1"
# 6. 下单
curl -X POST "localhost:8080/api/v1/orders?userId=1"
# 7. 查订单
curl "localhost:8080/api/v1/orders?userId=1"
# 8. 再查购物车,应为空
curl "localhost:8080/api/v1/cart?userId=1"MySQL 端校验:
USE bookstore;
SELECT * FROM cart_items; -- 下单后应为空
SELECT * FROM orders; -- 至少 1 行
SELECT * FROM order_items; -- 至少 2 行当前提交材料位于 docs/assignments/提交/:作业1模板报告、作业2精简源码包、作业3精简源码包。完整验收记录及运行说明见 作业1~3。
作业2和3分别执行 python3 scripts/assignments/build_submission.py 2 或 3 生成对应源码包。脚本使用明确清单,排除密钥、依赖目录、编译产物和模型缓存;核验ZIP CRC与逐文件SHA256。作业3包含真实复杂案例与公开行动链,运行前需按README准备Embedding模型。已有验收包应先保留,再有意重建。