> Spring 系列 · 综合实战篇(电商扩展)
> 在 Session 登录、MyBatis 分层、软删除、分页等基础上,扩展为完整宠物电商 :分类、购物车、地址、下单。
> 技术环境:Java 17+ · Spring Boot 3.4.x · MyBatis 3.5.x · MySQL 8.x
> 示例项目:spring-petshop(./spring-petshop/)
系列文章目录

提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
[- 引言:从校园 Demo 到电商闭环(#引言从校园-demo-到电商闭环)
- 一、项目概述(#一项目概述)
- 二、数据库设计(#二数据库设计)
- 2.1 ER 关系(#21-er-关系)
- 2.2 购物车中间表(#22-购物车中间表)
- 2.3 订单主子表(#23-订单主子表)
- 三、下单事务(核心逻辑)(#三下单事务核心逻辑)
- 四、API 一览(#四api-一览)
- 五、前端页面(#五前端页面)
- 六、快速启动(#六快速启动)
- 七、自测清单(#七自测清单)
- 八、与 campus-demo 的对比(#八与-campus-demo-的对比)
- 扩展练习(#扩展练习)](#引言从校园-demo-到电商闭环) - 一、项目概述 - 二、数据库设计 - 2.1 ER 关系 - 2.2 购物车中间表 - 2.3 订单主子表 - 三、下单事务(核心逻辑) - 四、API 一览 - 五、前端页面 - 六、快速启动 - 七、自测清单 - 八、与 campus-demo 的对比 - 扩展练习)
前言
综合篇 已经把企业开发的「骨架」搭好了:
-
Session 登录 + 拦截器
-
MyBatis Controller → Service → Mapper 分层
-
软删除 (`delete_flag`)
-分页查询 (`LIMIT offset, pageSize`)
- 统一响应`ResponseResult`
但真实电商不止「列表 + 消息」。用户还要:按分类逛商品 → 加购物车 → 管地址 → 一键下单 → 看订单。这些能力会引入更典型的数据关系与事务:

本文在综合篇 为 宠物用品电商 (spring-petshop(./spring-petshop/)):端口改用8081 ,;核心新增点是购物车中间表 与下单事务。建议边看本文边跑项目,对照自测清单验证闭环。
提示:以下是本篇文章正文内容,下面案例可供参考
一、项目概述
Spring-petshop是一个宠物用品电商Demo,覆盖从浏览到下单的完整链路:
登录 → 浏览商品(分类+分页)→ 加购 → 选地址 → 提交订单 → 查看订单
↘ 客服消息(软删除)

二、数据库设计
完整脚本见
sql
-- Spring 系列综合案例 · 宠物电商系统
CREATE DATABASE IF NOT EXISTS petshop_demo DEFAULT CHARACTER SET utf8mb4;
USE petshop_demo;
DROP TABLE IF EXISTS order_item;
DROP TABLE IF EXISTS order_info;
DROP TABLE IF EXISTS cart_item;
DROP TABLE IF EXISTS address_info;
DROP TABLE IF EXISTS product_info;
DROP TABLE IF EXISTS category_info;
DROP TABLE IF EXISTS message_info;
DROP TABLE IF EXISTS user_info;
CREATE TABLE user_info (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名',
password VARCHAR(100) NOT NULL COMMENT '密码(示例明文,生产请加密)',
nickname VARCHAR(50) COMMENT '昵称',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) COMMENT '用户表';
CREATE TABLE category_info (
id INT PRIMARY KEY AUTO_INCREMENT,
category_name VARCHAR(50) NOT NULL COMMENT '分类名称',
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序'
) COMMENT '商品分类表';
CREATE TABLE product_info (
id INT PRIMARY KEY AUTO_INCREMENT,
category_id INT NOT NULL COMMENT '分类ID',
product_name VARCHAR(100) NOT NULL COMMENT '商品名称',
description VARCHAR(500) COMMENT '描述',
price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '价格',
stock INT NOT NULL DEFAULT 0 COMMENT '库存',
image_url VARCHAR(255) COMMENT '图片URL',
status TINYINT NOT NULL DEFAULT 1 COMMENT '0-下架 1-上架',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_product_category FOREIGN KEY (category_id) REFERENCES category_info(id)
) COMMENT '宠物商品表';
CREATE TABLE address_info (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL COMMENT '用户ID',
receiver VARCHAR(50) NOT NULL COMMENT '收货人',
phone VARCHAR(20) NOT NULL COMMENT '手机号',
province VARCHAR(50) NOT NULL COMMENT '省',
city VARCHAR(50) NOT NULL COMMENT '市',
detail VARCHAR(200) NOT NULL COMMENT '详细地址',
is_default TINYINT NOT NULL DEFAULT 0 COMMENT '0-否 1-默认',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_address_user FOREIGN KEY (user_id) REFERENCES user_info(id)
) COMMENT '收货地址表';
CREATE TABLE cart_item (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL COMMENT '用户ID',
product_id INT NOT NULL COMMENT '商品ID',
quantity INT NOT NULL DEFAULT 1 COMMENT '数量',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_product (user_id, product_id),
CONSTRAINT fk_cart_user FOREIGN KEY (user_id) REFERENCES user_info(id),
CONSTRAINT fk_cart_product FOREIGN KEY (product_id) REFERENCES product_info(id)
) COMMENT '购物车表';
CREATE TABLE order_info (
id INT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '订单号',
user_id INT NOT NULL COMMENT '用户ID',
address_id INT NOT NULL COMMENT '地址ID',
total_amount DECIMAL(10,2) NOT NULL COMMENT '总金额',
status TINYINT NOT NULL DEFAULT 1 COMMENT '1-待发货 2-已发货 3-已完成 0-已取消',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES user_info(id),
CONSTRAINT fk_order_address FOREIGN KEY (address_id) REFERENCES address_info(id)
) COMMENT '订单主表';
CREATE TABLE order_item (
id INT PRIMARY KEY AUTO_INCREMENT,
order_id INT NOT NULL COMMENT '订单ID',
product_id INT NOT NULL COMMENT '商品ID',
product_name VARCHAR(100) NOT NULL COMMENT '商品名快照',
price DECIMAL(10,2) NOT NULL COMMENT '单价快照',
quantity INT NOT NULL COMMENT '数量',
CONSTRAINT fk_order_item_order FOREIGN KEY (order_id) REFERENCES order_info(id),
CONSTRAINT fk_order_item_product FOREIGN KEY (product_id) REFERENCES product_info(id)
) COMMENT '订单明细表';
CREATE TABLE message_info (
id INT PRIMARY KEY AUTO_INCREMENT,
from_user VARCHAR(50) NOT NULL COMMENT '发送方',
to_user VARCHAR(50) NOT NULL COMMENT '接收方',
message VARCHAR(500) NOT NULL COMMENT '消息内容',
delete_flag TINYINT NOT NULL DEFAULT 0 COMMENT '0-未删 1-已删',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) COMMENT '客服消息表';
INSERT INTO user_info (username, password, nickname) VALUES
('admin', '123456', '管理员'),
('zhangsan', '123456', '张三');
INSERT INTO category_info (category_name, sort_order) VALUES
('猫粮', 1), ('狗粮', 2), ('宠物玩具', 3), ('清洁用品', 4);
INSERT INTO product_info (category_id, product_name, description, price, stock, image_url, status) VALUES
(1, '皇家幼猫粮 2kg', '适合 12 月龄以下幼猫', 128.00, 50, '', 1),
(1, '渴望六种鱼 1.8kg', '全龄猫主粮', 368.00, 30, '', 1),
(1, '网易严选猫粮 1.8kg', '均衡营养配方', 89.00, 80, '', 1),
(2, '冠能中型犬成犬粮 2.5kg', '适合中型成犬', 198.00, 40, '', 1),
(2, '伯纳天纯狗粮 10kg', '天然无谷配方', 299.00, 25, '', 1),
(2, '麦富迪双拼粮 2kg', '牛肉粒双拼', 69.00, 60, '', 1),
(3, '逗猫棒套装', '羽毛逗猫棒 3 件套', 19.90, 100, '', 1),
(3, '狗狗磨牙骨', '天然牛皮磨牙', 29.90, 70, '', 1),
(3, '宠物飞盘', '软胶安全飞盘', 35.00, 45, '', 1),
(4, '宠物湿巾 80 片', '温和无刺激', 15.90, 120, '', 1),
(4, '猫砂 6L', '豆腐猫砂', 25.00, 90, '', 1),
(4, '宠物沐浴露 500ml', '抑菌除臭', 39.00, 55, '', 0);
INSERT INTO address_info (user_id, receiver, phone, province, city, detail, is_default) VALUES
(2, '张三', '13800001111', '广东省', '深圳市', '南山区科技园南路 88 号', 1),
(2, '张三', '13800002222', '广东省', '广州市', '天河区体育西路 100 号', 0);
INSERT INTO cart_item (user_id, product_id, quantity) VALUES
(2, 1, 2),
(2, 7, 1);
INSERT INTO message_info (from_user, to_user, message) VALUES
('admin', 'zhangsan', '欢迎来到宠物电商,新用户首单包邮!'),
('admin', 'zhangsan', '您关注的猫粮已补货'),
('zhangsan', 'admin', '请问狗粮什么时候发货?');

2.1 购物车中间表
sql
CREATE TABLE cart_item (
user_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL DEFAULT 1,
UNIQUE KEY uk_user_product (user_id, product_id)
);
同一用户对同一商品只有一条记录,再次加购则累加 `quantity`。
2.3 订单主子表
- order_info :订单号、用户、地址、总金额、状态
-order_item:商品快照(名称、单价、数量),避免商品改价影响历史订单。
三、下单事务(核心逻辑)
`OrderServiceImpl.checkout()` 使用 `@Transactional` 保证原子性:
校验购物车非空
校验地址归属当前用户
校验每件商品库存充足
INSERT order_info
对每个 cart_item:
UPDATE product_info SET stock = stock - quantity
INSERT order_item(价格快照)
- DELETE cart_item WHERE user_id = ?
任一步失败,全部回滚。
四、API一览

统一响应:`ResponseResult`(code: 200 / -1 / -2 / 400 / 500)。
五、前端页面

六、自测清单

七、项目代码
后端代码
启动类
java
package com.example.springpetshop;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringPetshopApplication {
public static void main(String[] args) {
SpringApplication.run(SpringPetshopApplication.class, args);
}
}
common类
java
package com.example.springpetshop.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class PageResult <T> {
private List<T> records;
private Long total;
}
java
package com.example.springpetshop.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ResponseResult<T> {
public static final int CODE_SUCCESS =200;
public static final int CODE_NOT_LOGIN =-1;
public static final int CODE_SESSION_EXPIRED =-2;
private int code;
private String errMsg;
private T data;
public static <T> ResponseResult<T> success(T data){
return new ResponseResult<>(CODE_SUCCESS,"success",data);
}
public static <T> ResponseResult<T> success(){
return success(null);
}
public static <T> ResponseResult<T> fail(int code,String errMsg){
return new ResponseResult<>(code,errMsg,null);
}
public static <T> ResponseResult<T> notLogin(){
return fail(CODE_NOT_LOGIN,"用户未登录");
}
public static <T> ResponseResult<T> sessionExpired(){
return fail(CODE_SESSION_EXPIRED,"登录已过期,请重新登录");
}
}
java
package com.example.springpetshop.common;
public class SessionConstants {
public static final String LOGIN_USER = "loginUser";
private SessionConstants(){
}
}
controller类
java
package com.example.springpetshop.controller;
import com.example.springpetshop.common.ResponseResult;
import com.example.springpetshop.common.SessionConstants;
import com.example.springpetshop.pojo.UserInfo;
import com.example.springpetshop.service.UserService;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/login")
public ResponseResult<UserInfo> login(@RequestBody Map<String ,String> body, HttpSession session){
UserInfo userInfo = userService.login(body.get("username"),body.get("password"));
if (userInfo == null){
return ResponseResult.fail(400,"用户名或密码错误");
}
session.setAttribute(SessionConstants.LOGIN_USER,userInfo);
return ResponseResult.success(userInfo);
}
@GetMapping("/logout")
public ResponseResult<UserInfo> logout(HttpSession session){
session.invalidate();
return ResponseResult.success();
}
@GetMapping("/current")
public ResponseResult<UserInfo> current(HttpSession session){
UserInfo userInfo = (UserInfo) session.getAttribute(SessionConstants.LOGIN_USER);
if (userInfo == null){
return ResponseResult.notLogin();
}
return ResponseResult.success(userInfo);
}
}
java
package com.example.springpetshop.controller;
import com.example.springpetshop.common.PageResult;
import com.example.springpetshop.common.ResponseResult;
import com.example.springpetshop.pojo.ProductInfo;
import com.example.springpetshop.service.ProductService;
import org.apache.ibatis.annotations.Param;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/page")
public ResponseResult<PageResult<ProductInfo>> listByPage(@RequestParam(required = false) Integer categoryId,
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "6")int pageSize){
return ResponseResult.success(productService.listByPage(categoryId,pageNum,pageSize));
}
@GetMapping("{id}")
public ResponseResult<ProductInfo> getById(@PathVariable int id){
return ResponseResult.success(productService.getById(id));
}
}
java
package com.example.springpetshop.controller;
import com.example.springpetshop.common.ResponseResult;
import com.example.springpetshop.common.SessionConstants;
import com.example.springpetshop.pojo.MessageInfo;
import com.example.springpetshop.pojo.UserInfo;
import com.example.springpetshop.service.MessageService;
import jakarta.servlet.http.HttpSession;
import org.apache.ibatis.annotations.Param;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/message")
public class MessageController {
@Autowired
private MessageService messageService;
@GetMapping("/inbox")
public ResponseResult<List<MessageInfo>> inbox(HttpSession session){
UserInfo userInfo = currentUser(session);
return ResponseResult.success(messageService.listInbox(userInfo.getUsername()));
}
@PostMapping("/send")
public ResponseResult<MessageInfo> send(@RequestBody MessageInfo messageInfo,HttpSession session){
UserInfo userInfo = currentUser(session);
messageInfo.setFromUser(userInfo.getUsername());
return ResponseResult.success(messageService.send(messageInfo));
}
@DeleteMapping("/{id}")
public ResponseResult<Void> softDelete(@PathVariable int id, HttpSession session){
UserInfo userInfo = currentUser(session);
messageService.softDelete(id,userInfo.getUsername());
return ResponseResult.success();
}
private UserInfo currentUser(HttpSession session) {
return (UserInfo) session.getAttribute(SessionConstants.LOGIN_USER);
}
}
java
package com.example.springpetshop.controller;
import com.example.springpetshop.common.ResponseResult;
import com.example.springpetshop.common.SessionConstants;
import com.example.springpetshop.pojo.AddressInfo;
import com.example.springpetshop.pojo.UserInfo;
import com.example.springpetshop.service.AddressService;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/address")
public class AddressController {
@Autowired
private AddressService addressService;
@GetMapping("/list")
public ResponseResult<List<AddressInfo>> list(HttpSession session){
UserInfo userInfo = currentUser(session);
return ResponseResult.success(addressService.lisByUser(userInfo.getId()));
}
@PostMapping("/create")
public ResponseResult<AddressInfo> create(@RequestBody AddressInfo address,HttpSession session){
UserInfo userInfo = currentUser(session);
return ResponseResult.success(addressService.create(userInfo.getId(),address));
}
@PutMapping("/{id}")
public ResponseResult<AddressInfo> update(@PathVariable int id, @RequestBody AddressInfo addressInfo, HttpSession session){
UserInfo userInfo = currentUser(session);
return ResponseResult.success(addressService.update(userInfo.getId(),id,addressInfo));
}
@DeleteMapping("/{id}")
public ResponseResult <Void> delete(@PathVariable int id, HttpSession session){
UserInfo userInfo = currentUser(session);
addressService.delete(userInfo.getId(),id);
return ResponseResult.success();
}
@PutMapping("/{id}/default")
public ResponseResult<Void> setDefault(@PathVariable int id,HttpSession session){
UserInfo userInfo = currentUser(session);
addressService.setDefault(userInfo.getId(),id);
return ResponseResult.success();
}
private UserInfo currentUser(HttpSession session) {
return (UserInfo) session.getAttribute(SessionConstants.LOGIN_USER);
}
}
java
package com.example.springpetshop.controller;
import com.example.springpetshop.common.ResponseResult;
import com.example.springpetshop.common.SessionConstants;
import com.example.springpetshop.pojo.CartItem;
import com.example.springpetshop.pojo.UserInfo;
import com.example.springpetshop.service.CartItemService;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/cartItem")
public class CartItemController {
@Autowired
private CartItemService cartItemService;
@GetMapping("/list")
public ResponseResult<List<CartItem>> list(HttpSession session){
UserInfo userInfo = currentUser(session);
return ResponseResult.success(cartItemService.list(userInfo.getId()));
}
@PostMapping("/add")
public ResponseResult <Void> add(@RequestBody Map<String,Integer> body, HttpSession session){
UserInfo userInfo = currentUser(session);
cartItemService.add(userInfo.getId(),body.get("productId"),body.getOrDefault("quantity",1));
return ResponseResult.success();
}
@PutMapping("/{productId}")
public ResponseResult <Void> update(@PathVariable int productId,
@RequestBody Map<String ,Integer> body,
HttpSession session){
UserInfo userInfo = currentUser(session);
cartItemService.updateQuantity(userInfo.getId(),productId,body.get("quantity"));
return ResponseResult.success();
}
@DeleteMapping("/{productId}")
public ResponseResult<Void> remove(@PathVariable int productId,HttpSession session){
UserInfo userInfo = currentUser(session);
cartItemService.remove(userInfo.getId(),productId);
return ResponseResult.success();
}
private UserInfo currentUser(HttpSession session) {
return (UserInfo) session.getAttribute(SessionConstants.LOGIN_USER);
}
}
java
package com.example.springpetshop.controller;
import com.example.springpetshop.common.ResponseResult;
import com.example.springpetshop.pojo.CategoryInfo;
import com.example.springpetshop.service.CategoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
@GetMapping("/list")
public ResponseResult<List<CategoryInfo>> list(){
return ResponseResult.success(categoryService.listAll());
}
}
java
package com.example.springpetshop.controller;
import com.example.springpetshop.common.ResponseResult;
import com.example.springpetshop.common.SessionConstants;
import com.example.springpetshop.pojo.OrderInfo;
import com.example.springpetshop.pojo.UserInfo;
import com.example.springpetshop.service.OrderService;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/checkout")
public ResponseResult<OrderInfo> checkout(@RequestBody Map<String ,Integer> body, HttpSession session){
UserInfo userInfo = currentUser(session);
Integer addressId = body.get("addressId");
if (addressId == null){
return ResponseResult.fail(400,"请选择收获地址");
}
return ResponseResult.success(orderService.checkout(userInfo.getId(),addressId));
}
@GetMapping("/list")
public ResponseResult<List<OrderInfo>> list(HttpSession session){
UserInfo userInfo = currentUser(session);
return ResponseResult.success(orderService.listByUser(userInfo.getId()));
}
@GetMapping("/{id}")
public ResponseResult<OrderInfo> detail(@PathVariable int id, HttpSession session){
UserInfo userInfo = currentUser(session);
return ResponseResult.success(orderService.getDetail(userInfo.getId(),id));
}
private UserInfo currentUser(HttpSession session) {
return (UserInfo) session.getAttribute(SessionConstants.LOGIN_USER);
}
}
service类
java
package com.example.springpetshop.service;
import com.example.springpetshop.pojo.AddressInfo;
import java.util.List;
public interface AddressService {
List<AddressInfo> lisByUser(Integer id);
AddressInfo create(Integer id, AddressInfo address);
AddressInfo update(int userId, int id, AddressInfo addressInfo);
void setDefault(Integer userId, int id);
void delete(Integer userId, int id);
}
java
package com.example.springpetshop.service;
import com.example.springpetshop.pojo.CartItem;
import java.util.List;
public interface CartItemService {
List<CartItem> list(int userId);
void add(int userId, Integer productId, Integer quantity);
void updateQuantity(int userId, int productId, Integer quantity);
void remove(int userId, int productId);
}
java
package com.example.springpetshop.service;
import com.example.springpetshop.pojo.CategoryInfo;
import java.util.List;
public interface CategoryService {
List<CategoryInfo> listAll();
}
java
package com.example.springpetshop.service;
import com.example.springpetshop.pojo.MessageInfo;
import java.util.List;
public interface MessageService {
List<MessageInfo> listInbox(String username);
MessageInfo send(MessageInfo messageInfo);
void softDelete(int userId, String username);
}
java
package com.example.springpetshop.service;
import com.example.springpetshop.pojo.OrderInfo;
import java.util.List;
public interface OrderService {
OrderInfo checkout(Integer id, Integer addressId);
List<OrderInfo> listByUser(Integer id);
OrderInfo getDetail(int userId,int orderId );
}
java
package com.example.springpetshop.service;
import com.example.springpetshop.common.PageResult;
import com.example.springpetshop.mapper.ProductMapper;
import com.example.springpetshop.pojo.ProductInfo;
import org.springframework.beans.factory.annotation.Autowired;
public interface ProductService {
PageResult<ProductInfo> listByPage(Integer categoryId, int pageNum, int pageSize);
ProductInfo getById(int id);
}
java
package com.example.springpetshop.service;
import com.example.springpetshop.pojo.UserInfo;
public interface UserService {
UserInfo login(String username, String password);
}
java
package com.example.springpetshop.service.impl;
import com.example.springpetshop.common.ResponseResult;
import com.example.springpetshop.mapper.UserInfoMapper;
import com.example.springpetshop.pojo.UserInfo;
import com.example.springpetshop.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
private UserInfoMapper userInfoMapper;
/**
* 【练习 2 - 入门】用户登录
* 要求:
* 1. 根据 username 查询用户
* 2. 校验用户名和密码(密码不匹配或用户不存在时返回 null,并 warn 日志)
* 3. 登录成功:info 日志,清空 password 字段后返回用户(避免泄露密码)
*/
@Override
public UserInfo login(String username, String password) {
// TODO: 在此编写业务逻辑
UserInfo userInfo = userInfoMapper.selectByUsername(username);
if(userInfo == null || password == null || !password.equals(userInfo.getPassword())){
log.warn("登录失败:username={}",username);
return null;
}
log.info("用户登录成功:username={}",username);
userInfo.setPassword(null);
return userInfo;
}
}
java
package com.example.springpetshop.service.impl;
import com.example.springpetshop.mapper.AddressMapper;
import com.example.springpetshop.mapper.OrderMapper;
import com.example.springpetshop.pojo.AddressInfo;
import com.example.springpetshop.service.AddressService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class AddressServiceImpl implements AddressService {
@Autowired
private AddressMapper addressMapper;
@Autowired
private OrderMapper orderMapper;
/**
* 【练习 8 - 基础】查询用户地址列表
*/
@Override
public List<AddressInfo> lisByUser(Integer userId) {
return addressMapper.selectListAll(userId);
}
/**
* 【练习 9 - 进阶】新增地址
* 要求:
* 1. 设置 userId;isDefault 为 null 时设为 0
* 2. 若设为默认地址(isDefault==1),先清除该用户其他默认地址
* 3. 插入并返回 address
*/
@Override
public AddressInfo create(Integer userId, AddressInfo address) {
address.setUserId(userId);
if (address.getIsDefault() == null){
address.setIsDefault(0);
}
if (address.getIsDefault() == 1){
addressMapper.clearDefault(userId);
}
addressMapper.insert(address);
return address;
}
/**
* 【练习 10 - 进阶】更新地址
* 要求:
* 1. 校验地址归属(requireOwend)
* 2. 保留 id、userId;isDefault 为 null 时沿用原值
* 3. 若设为默认,先 clearDefault;更新后重新查询返回
*/
@Override
public AddressInfo update(int userId, int id, AddressInfo addressInfo) {
AddressInfo existing=requireOwend(userId,id);
addressInfo.setId(id);
addressInfo.setUserId(userId);
if (addressInfo.getIsDefault() == null){
addressInfo.setIsDefault(existing.getIsDefault());
}
if (addressInfo.getIsDefault() == 1){
addressMapper.clearDefault(userId);
}
addressMapper.update(addressInfo);
return addressMapper.selectById(id);
}
/**
* 【练习 11 - 进阶】设为默认地址
* 要求:校验归属 → clearDefault → setDefault
*/
@Override
@Transactional
public void setDefault(Integer userId, int id) {
requireOwend(userId,id);
addressMapper.clearDefault(userId);
addressMapper.setDefault(id,userId);
}
/**
* 【练习 12 - 进阶】删除地址
* 要求:
* 1. 校验归属
* 2. 若地址已被订单引用则不可删除
* 3. 否则删除
*/
@Override
public void delete(Integer userId, int id) {
if (orderMapper.countByAddressId(id) >0){
throw new IllegalArgumentException("该地址已被订单使用,无法删除");
}
requireOwend(userId,id);
addressMapper.delete(id,userId);
}
/**
* 【练习 13 - 进阶】私有方法:校验地址存在且属于当前用户
* 要求:不存在或 userId 不匹配时抛 IllegalArgumentException
*/
private AddressInfo requireOwend(int userId, int id) {
AddressInfo addressInfo = addressMapper.selectById(id);
if (addressInfo ==null || addressInfo.getUserId() == null || addressInfo.getUserId() !=userId){
throw new IllegalArgumentException("不存在userId或不匹配");
}
return addressInfo;
}
}
java
package com.example.springpetshop.service.impl;
import com.example.springpetshop.mapper.MessageMapper;
import com.example.springpetshop.pojo.MessageInfo;
import com.example.springpetshop.service.MessageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Slf4j
@Service
public class MessageServiceImpl implements MessageService {
@Autowired
private MessageMapper messageMapper;
/**
* 【练习 5 - 基础】查询收件箱
* 要求:返回指定用户未删除的消息列表
*/
@Override
public List<MessageInfo> listInbox(String toUser) {
return messageMapper.selectActiveByToUser(toUser);
}
/**
* 【练习 6 - 基础】发送消息
* 要求:
* 1. 插入消息到数据库
* 2. 记录 info 日志(含 from、to)
* 3. 返回消息对象
*/
@Override
@Transactional
public MessageInfo send(MessageInfo messageInfo) {
messageMapper.insert(messageInfo);
log.info("发送消息:from={},to={}",messageInfo.getFromUser(),messageInfo.getToUser());
return messageInfo;
}
/**
* 【练习 7 - 进阶】软删除消息
* 要求:
* 1. 校验 currentUser 的收件箱中是否包含 id 对应的消息(无权则抛异常)
* 2. 调用 mapper 软删除,影响行数为 0 则抛异常
* 3. 成功则 info 日志
*/
@Override
@Transactional
public void softDelete(int userId, String currentUser) {
List<MessageInfo> inbox = messageMapper.selectActiveByToUser(currentUser);
boolean owned = inbox.stream().anyMatch(m->m.getId() == userId);
if (!owned){
throw new IllegalArgumentException("消息不存在或无权删除");
}
int rows =messageMapper.softDeleteById(userId);
if (rows ==0){
throw new IllegalArgumentException("消息不存在或已删除");
}
log.info("软删除消息,userId={},operator={}",userId,currentUser);
}
}
java
package com.example.springpetshop.service.impl;
import com.example.springpetshop.mapper.CartItemMapper;
import com.example.springpetshop.mapper.ProductMapper;
import com.example.springpetshop.pojo.CartItem;
import com.example.springpetshop.pojo.ProductInfo;
import com.example.springpetshop.service.CartItemService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Slf4j
public class CartItemServiceImpl implements CartItemService {
@Autowired
private CartItemMapper cartItemMapper;
@Autowired
private ProductMapper productMapper;
/**
* 【练习 14 - 基础】查询用户购物车
*/
@Override
public List<CartItem> list(int userId) {
return cartItemMapper.selectListById(userId);
}
/**
* 【练习 15 - 核心】加入购物车
* 要求:
* 1. quantity 至少为 1
* 2. 校验商品存在且上架
* 3. 购物车无该商品 → 新建 CartItem 并 insert
* 4. 已有该商品 → 累加数量,不能超过 stock
* 5. 记录 info 日志
*/
@Override
@Transactional
public void add(int userId, Integer productId, Integer quantity) {
if (quantity<1){
throw new IllegalArgumentException("数量至少为1");
}
ProductInfo productInfo = productMapper.selectById(productId);
if (productInfo == null){
throw new IllegalArgumentException("产品库存数量不足或已下架");
}
CartItem existing = cartItemMapper.selectByUserAndProduct(userId, productId);
if (existing == null){
CartItem cartItem = new CartItem();
cartItem.setUserId(userId);
cartItem.setProductId(productId);
cartItem.setQuantity(quantity);
cartItemMapper.insert(cartItem);
}else {
int newQty = existing.getQuantity() + quantity;
if (newQty > productInfo.getStock()){
throw new IllegalArgumentException("库存数量不足,当前库存。"+productInfo.getStock());
}
cartItemMapper.updateQuantity(userId,productId,newQty);
}
log.info("加购成功: userId={} productId={} quantity={}",userId,productId,quantity);
}
/**
* 【练习 16 - 核心】修改购物车数量
* 要求:
* 1. quantity 至少为 1
* 2. 购物车中必须有该商品
* 3. 不能超过 cartItem 中的 stock
* 4. 更新数量
*/
@Override
@Transactional
public void updateQuantity(int userId, int productId, Integer quantity) {
if(quantity<1){
throw new IllegalArgumentException("数量至少为1");
}
CartItem existing = cartItemMapper.selectByUserAndProduct(userId,productId);
if (existing==null){
throw new IllegalArgumentException("不存在该商品");
}
if (existing.getStock() < quantity){
throw new IllegalArgumentException("库存不足,当前库存: " + existing.getStock());
}
cartItemMapper.updateQuantity(userId,productId,quantity);
}
/**
* 【练习 17 - 基础】移除购物车商品
*/
@Override
@Transactional
public void remove(int userId, int productId) {
cartItemMapper.deleteByUserAndProduct(userId,productId);
}
}
java
package com.example.springpetshop.service.impl;
import com.example.springpetshop.mapper.AddressMapper;
import com.example.springpetshop.mapper.CartItemMapper;
import com.example.springpetshop.mapper.OrderMapper;
import com.example.springpetshop.mapper.ProductMapper;
import com.example.springpetshop.pojo.AddressInfo;
import com.example.springpetshop.pojo.CartItem;
import com.example.springpetshop.pojo.OrderInfo;
import com.example.springpetshop.pojo.OrderItem;
import com.example.springpetshop.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Autowired
private AddressMapper addressMapper;
@Autowired
private ProductMapper productMapper;
@Autowired
private CartItemMapper cartItemMapper;
@Autowired
private OrderMapper orderMapper;
/**
* 【练习 18 - 核心】下单结算
* 要求:
* 1. 购物车不能为空
* 2. 校验收货地址存在且属于当前用户
* 3. 遍历购物车:校验库存,累加 total(用 item.subtotal())
* 4. 创建 OrderInfo(orderNo、userId、addressId、totalAmount、status=1),insertOrder
* 5. 逐条:扣减库存(失败抛异常)、insertOrderItem
* 6. 清空用户购物车
* 7. info 日志,返回 getDetail 结果
*/
@Override
@Transactional
public OrderInfo checkout(Integer userId, Integer addressId) {
List<CartItem> cartItem = cartItemMapper.selectListById(userId);
if (cartItem.isEmpty()){
throw new IllegalArgumentException("购物车为空,无法下单");
}
AddressInfo addressInfo = addressMapper.selectById(addressId);
if (addressInfo == null || addressInfo.getUserId() == null || !addressInfo.getUserId().equals(userId)){
throw new IllegalArgumentException("收货地址不存在或无权使用");
}
BigDecimal total = BigDecimal.ZERO;
for(CartItem item : cartItem){
if (item.getStock() < item.getQuantity()){
throw new IllegalArgumentException("商品「" + item.getProductName() + "」库存不足");
}
total =total.add(item.subtotal());
}
OrderInfo orderInfo = new OrderInfo();
orderInfo.setOrderNo(generateOrderNo());
orderInfo.setUserId(userId);
orderInfo.setAddressId(addressId);
orderInfo.setTotalAmount(total);
orderInfo.setStatus(1);
orderMapper.insertOrder(orderInfo);
for (CartItem item :cartItem){
int rows = productMapper.decreaseStock(item.getProductId(), item.getQuantity());
if (rows ==0){
throw new IllegalArgumentException("商品「" + item.getProductName() + "」扣减库存失败");
}
OrderItem orderItem = new OrderItem();
orderItem.setOrderId(orderInfo.getId());
orderItem.setProductId(item.getProductId());
orderItem.setProductName(item.getProductName());
orderItem.setPrice(item.getPrice());
orderItem.setQuantity(item.getQuantity());
orderMapper.insertOrderItem(orderItem);
}
cartItemMapper.deleteByUserId(userId);
log.info("下单成功: userId={} orderNo={} total={}", userId, orderInfo.getOrderNo(), total);
return getDetail(userId, orderInfo.getId());
}
/**
* 【练习 19 - 基础】查询用户订单列表
*/
@Override
public List<OrderInfo> listByUser(Integer userId) {
return orderMapper.selectByUserId(userId);
}
/**
* 【练习 20 - 进阶】订单详情
* 要求:
* 1. 按 orderId + userId 查询,不存在抛异常
* 2. 填充 address 和 item 列表
* 3. 返回完整 OrderInfo
*/
@Override
public OrderInfo getDetail(int userId, int orderId) {
OrderInfo orderInfo = orderMapper.selectById(orderId,userId);
if (orderInfo == null){
throw new IllegalArgumentException("订单不存在");
}
orderInfo.setAddress(addressMapper.selectById(orderInfo.getAddressId()));
orderInfo.setItem(orderMapper.selectItemByOrderId(orderId));
return orderInfo;
}
/**
* 【练习 21 - 辅助】生成订单号
* 格式:PO + yyyyMMddHHmmss + 4位随机数(1000~9999)
*/
private String generateOrderNo() {
String time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
int random = ThreadLocalRandom.current().nextInt(1000, 9999);
return "PO" + time + random;
}
}
java
package com.example.springpetshop.service.impl;
import com.example.springpetshop.common.PageResult;
import com.example.springpetshop.common.ResponseResult;
import com.example.springpetshop.mapper.ProductMapper;
import com.example.springpetshop.pojo.ProductInfo;
import com.example.springpetshop.service.ProductService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@Slf4j
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductMapper productMapper;
/**
* 【练习 3 - 基础】分页查询商品
* 要求:
* 1. pageNum 最小为 1,pageSize 最小为 10(非法值需修正)
* 2. 计算 offset,调用 mapper 分页查询 + 统计总数
* 3. 封装 PageResult 返回,记录 info 日志(含 categoryId、pageNum、total)
*/
@Override
public PageResult<ProductInfo> listByPage(Integer categoryId, int pageNum, int pageSize) {
// TODO: 在此编写业务逻辑
if(pageNum <1){
pageNum = 1;
}
if (pageSize<1){
pageSize = 10;
}
int offset = pageSize * (pageNum-1);
List<ProductInfo> records = productMapper.selectByPage(categoryId,offset,pageSize);
Long total = productMapper.countActive(categoryId);
log.info("分页查询商品:categoryId={},pageNum={},total={}",categoryId,pageNum,total);
return new PageResult<>(records,total);
}
/**
* 【练习 4 - 基础】根据 ID 查询商品详情
* 要求:
* 1. 调用 mapper 查询
* 2. 不存在则抛 IllegalArgumentException,消息含商品 id
* 3. 存在则返回
*/
@Override
public ProductInfo getById(int id) {
// TODO: 在此编写业务逻辑
ProductInfo productInfo = productMapper.selectById(id);
if (productInfo == null){
throw new IllegalArgumentException("商品不存在或已下架 id = "+id);
}
return productInfo;
}
}
java
package com.example.springpetshop.service.impl;
import com.example.springpetshop.mapper.CategoryMapper;
import com.example.springpetshop.pojo.CategoryInfo;
import com.example.springpetshop.service.CategoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@Slf4j
public class CategoryServiceImpl implements CategoryService {
@Autowired
private CategoryMapper categoryMapper;
/**
* 【练习 1 - 入门】查询所有商品分类
* 要求:
* 1. 调用 categoryMapper 查询全部分类
* 2. 记录 info 级别日志
* 3. 返回查询结果
*/
@Override
public List<CategoryInfo> listAll() {
// TODO: 在此编写业务逻辑
log.info("开始查询");
return categoryMapper.selectAll();
// throw new UnsupportedOperationException("待实现:listAll");
}
}
pojo类
java
package com.example.springpetshop.pojo;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class AddressInfo {
private Integer id;
private Integer userId;
private String receiver;
private String phone;
private String province;
private String city;
private String detail;
private Integer isDefault;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
java
package com.example.springpetshop.pojo;
import lombok.Data;
import lombok.extern.java.Log;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
public class CartItem {
private Integer id;
private Integer userId;
private Integer productId;
private Integer quantity;
private String productName;
private String imageUrl;
private String categoryName;
private BigDecimal price;
private Integer stock;
private LocalDateTime createTime;
private LocalDateTime updateTime;
public BigDecimal subtotal() {
if (price == null || quantity == null) {
return BigDecimal.ZERO;
}
return price.multiply(BigDecimal.valueOf(quantity));
}
}
java
package com.example.springpetshop.pojo;
import lombok.Data;
@Data
public class CategoryInfo {
private Integer id;
private String categoryName;
private Integer sortOrder;
}
java
package com.example.springpetshop.pojo;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class MessageInfo {
private Integer id;
private String fromUser;
private String toUser;
private String message;
private Integer deleteFlag;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
java
package com.example.springpetshop.pojo;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Data
public class OrderInfo {
private Integer id;
private String orderNo;
private Integer userId;
private Integer addressId;
private BigDecimal totalAmount;
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
private AddressInfo address;
private List<OrderItem> item;
}
java
package com.example.springpetshop.pojo;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class OrderItem {
private Integer id;
private Integer orderId;
private Integer productId;
private String productName;
private BigDecimal price;
private Integer quantity;
}
java
package com.example.springpetshop.pojo;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
public class ProductInfo {
private Integer id;
private Integer categoryId;
private String categoryName;
private String productName;
private String description;
private BigDecimal price;
private Integer stock;
private String imageUrl;
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
java
package com.example.springpetshop.pojo;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.Date;
@Data
public class UserInfo {
private Integer id;
private String username;
private String password;
private String nickname;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
mapper类
java
package com.example.springpetshop.mapper;
import com.example.springpetshop.pojo.AddressInfo;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface AddressMapper {
@Select("select * from address_info where user_id =#{userId} order by is_default desc,id desc")
List<AddressInfo> selectListAll(@Param("userId") Integer id);
@Select("select * from address_info where id =#{id}")
AddressInfo selectById(@Param("id") int id);
@Update("update address_info ser is_default = 0 where user_id = #{userId}")
void clearDefault(Integer userId);
@Insert("insert into address_info(user_id,receiver,phone,province,city,detail,is_default) values (#{userId}," +
"#{receiver},#{phone},#{province},#{city},#{detail},#{isDefault})")
void insert(AddressInfo address);
@Update("update address_info set receiver=#{receiver},phone = #{phone},province=#{province},city = #{city},detail=#{detail}." +
"is_default = #{isDefault} where id =#{id} and user_id = #{userId}")
void update(AddressInfo addressInfo);
@Update("update address_info set is_default= 1 where id = #{id} and user_id = #{userId} ;")
void setDefault(@Param("id") int id, Integer userId);
@Delete("delete from address_info where id = #{id} and user_id = #{userId}" )
void delete(@Param("id") int id, Integer userId);
}
java
package com.example.springpetshop.mapper;
import com.example.springpetshop.pojo.CartItem;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface CartItemMapper {
@Select(" SELECT c.id, c.user_id, c.product_id, c.quantity, c.create_time, c.update_time,\n" +
" p.product_name, p.image_url, cat.category_name, p.price, p.stock\n" +
" FROM cart_item c\n" +
" INNER JOIN product_info p ON c.product_id = p.id\n" +
" LEFT JOIN category_info cat ON p.category_id = cat.id\n" +
" WHERE c.user_id = #{userId}\n" +
" AND p.status != 0\n" +
" ORDER BY c.id DESC")
List<CartItem> selectListById(@Param("userId") int userId);
@Select("SELECT c.id, c.user_id, c.product_id, c.quantity, c.create_time, c.update_time, " +
"p.product_name, p.image_url, cat.category_name, p.price, p.stock " +
"FROM cart_item c " +
"INNER JOIN product_info p ON c.product_id = p.id " +
"LEFT JOIN category_info cat ON p.category_id = cat.id " +
"WHERE c.user_id = #{userId} AND c.product_id = #{productId} AND p.status != 0")
CartItem selectByUserAndProduct(@Param("userId") int userId, @Param("productId") Integer productId);
@Insert("insert into cart_item(user_id,product_id,quantity) values (#{userId},#{productId},#{quantity})")
void insert(CartItem item);
@Update("update cart_item set quantity = #{quantity} where user_id = #{userId} and product_id = #{productId}")
void updateQuantity(@Param("userId")int userId, @Param("productId")Integer productId, @Param("quantity") int quantity);
@Delete("delete from cart_item where user_id = #{userId} and product_id = #{productId}")
void deleteByUserAndProduct(@Param("userId")int userId, @Param("productId")int productId);
@Delete("delete from cart_item where user_id = #{userId}")
void deleteByUserId(@Param("userId") int userId);
}
java
package com.example.springpetshop.mapper;
import com.example.springpetshop.pojo.CategoryInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface CategoryMapper {
@Select("select * from category_info order by sort_order,id")
List<CategoryInfo> selectAll();
}
java
package com.example.springpetshop.mapper;
import com.example.springpetshop.pojo.MessageInfo;
import org.apache.ibatis.annotations.*;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
@Mapper
public interface MessageMapper {
@Select("select * from message_info where delete_flag = 0 and to_user = #{toUser}")
List<MessageInfo> selectActiveByToUser(@Param("toUser") String toUser);
@Insert("insert into message_info(from_user,to_user,message,delete_flag) values (#{fromUser},#{toUser},#{message},0)")
void insert(MessageInfo messageInfo);
@Update("update message_info set delete_flag =1 whereid = #{userId} and delete_flag = 0;")
int softDeleteById(@Param("userId") int userId);
}
java
package com.example.springpetshop.mapper;
import com.example.springpetshop.pojo.OrderInfo;
import com.example.springpetshop.pojo.OrderItem;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface OrderMapper {
@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert("insert into order_info (order_no, user_id, address_id, total_amount, status) " +
"values (#{orderNo}, #{userId}, #{addressId}, #{totalAmount}, #{status})")
void insertOrder(OrderInfo orderInfo);
@Insert("insert into order_item (order_id, product_id, product_name, price, quantity) " +
"values (#{orderId}, #{productId}, #{productName}, #{price}, #{quantity})")
void insertOrderItem(OrderItem orderItem);
@Select("select * from order_info where user_id = #{userId} order by create_time desc")
List<OrderInfo> selectByUserId(@Param("userId") int userId);
@Select("select * from order_info where id = #{id} and user_id = #{userId} ")
OrderInfo selectById(@Param("id") int orderId, @Param("userId") int userId);
@Select("select * from order_item where order_id = #{orderId}")
List<OrderItem> selectItemByOrderId(@Param("orderId") int orderId);
@Select("select count(*) from order_info where address_id = #{addressId}")
int countByAddressId(@Param("addressId") int addressId);
}
java
package com.example.springpetshop.mapper;
import com.example.springpetshop.pojo.ProductInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List;
@Mapper
public interface ProductMapper {
List<ProductInfo> selectByPage(@Param("categoryId") Integer categoryId,
@Param("offset")int offset,
@Param("pageSize")int pageSize);
Long countActive(@Param("categoryId")Integer categoryId);
ProductInfo selectById(@Param("id")int id);
@Update("update product_info set stock = stock -#{quantity} where id =#{id} and stock >= #{quantity} and status !=0 ")
int decreaseStock(@Param("id") int id, @Param("quantity") Integer quantity);
}
java
package com.example.springpetshop.mapper;
import com.example.springpetshop.pojo.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface UserInfoMapper {
@Select("select * from user_info where username = #{username}")
UserInfo selectByUsername(String username);
}
XML
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.springpetshop.mapper.ProductMapper">
<resultMap id="ProductInfoMap" type="com.example.springpetshop.pojo.ProductInfo">
<id property="id" column="id"/>
<result property="categoryId" column="category_id"/>
<result property="categoryName" column="category_name"/>
<result property="productName" column="product_name"/>
<result property="description" column="description"/>
<result property="price" column="price"/>
<result property="stock" column="stock"/>
<result property="imageUrl" column="image_url"/>
<result property="status" column="status"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<sql id="productColumns">
select p.id,p.category_id,c.category_name,p.product_name,p.description,p.price,p.stock,p.image_url,
p.status,p.create_time,p.update_time
</sql>
<sql id="productFromJoin">
from product_info p left join category_info c on p.category_id = c.id
</sql>
<select id="selectByPage" resultMap="ProductInfoMap">
<include refid="productColumns"/>
<include refid="productFromJoin"/>
where p.status != 0
<if test="categoryId != null">
and p.category_id = #{categoryId}
</if>
order by p.id limit #{offset}, #{pageSize}
</select>
<select id="countActive" resultType="long">
select count(*)
from product_info p where p.status !=0
<if test="categoryId !=null">
and p.category_id = #{categoryId}
</if>
</select>
<select id="selectById" resultMap="ProductInfoMap">
<include refid="productColumns"/>
<include refid="productFromJoin"/>
where p.status != 0 and p.id = #{id}
</select>
</mapper>
interceptor类
java
package com.example.springpetshop.interceptor;
import com.example.springpetshop.common.ResponseResult;
import com.example.springpetshop.common.SessionConstants;
import com.example.springpetshop.pojo.UserInfo;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import tools.jackson.databind.ObjectMapper;
@Component
public class LoginInterceptor implements HandlerInterceptor {
private final ObjectMapper objectMapper;
public LoginInterceptor(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession(false);
if (session == null){
writeJson(response, ResponseResult.notLogin());
return false;
}
UserInfo userInfo = (UserInfo) session.getAttribute(SessionConstants.LOGIN_USER);
if (userInfo == null){
writeJson(response,ResponseResult.sessionExpired());
return false;
}
return true;
}
private void writeJson(HttpServletResponse response, ResponseResult<?> result) throws Exception {
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/json;charset=UTF-8");
objectMapper.writeValue(response.getWriter(), result);
}
}
config类
java
package com.example.springpetshop.config;
import com.example.springpetshop.interceptor.LoginInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@Slf4j
public class WebMvcConfig implements WebMvcConfigurer {
private final LoginInterceptor loginInterceptor;
public WebMvcConfig(LoginInterceptor loginInterceptor) {
this.loginInterceptor = loginInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns(
"/user/**",
"/product/**",
"/category/**",
"/cartItem/**",
"/address/**",
"/order/**",
"/message/**"
)
.excludePathPatterns("/user/login", "/user/logout");
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("forward:/login.html");
}
}
配置类
java
spring:
application:
name: spring-petshop-demo
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/petshop_demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
username: root
password: wm127369#
server:
port: 8081
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.petshop.pojo
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
logging:
level:
com.example.petshop: debug
com.example.petshop.mapper: debug
pom文件
XML
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.8</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.example</groupId>
<artifactId>spring-petshop</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-petshop</name>
<description>spring-petshop</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>4.0.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.example.springpetshop.SpringPetshopApplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
总结
本文在 Session 登录、MyBatis 分层、软删除、分页查询等骨架基础上,构建了完整的宠物用品电商项目,打通「登录、分类浏览、购物车、地址管理、下单、订单查询、客服消息」业务闭环。核心难点在于数据关系与事务处理:购物车中间表以用户和商品唯一约束实现重复加购自动累加;订单主子表采用地址引用与商品快照,隔离历史订单价格;OrderServiceImpl.checkout() 通过 @Transactional 串联购物车校验、库存扣减、订单插入与清空购物车,保障原子性。全项目代码按 Controller、Service、Mapper、POJO、拦截器、配置类完整分层,建议结合自测清单逐步验证,真正理解企业级电商的典型设计思路。