综合案例:宠物电商系统

> Spring 系列 · 综合实战篇(电商扩展)

> 在 Session 登录、MyBatis 分层、软删除、分页等基础上,扩展为完整宠物电商 :分类、购物车、地址、下单。

> 技术环境:Java 17+ · Spring Boot 3.4.x · MyBatis 3.5.x · MySQL 8.x

> 示例项目:spring-petshop(./spring-petshop/)

系列文章目录


提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

[- 引言:从校园 Demo 到电商闭环(#引言从校园-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` 保证原子性:

  1. 校验购物车非空

  2. 校验地址归属当前用户

  3. 校验每件商品库存充足

  4. INSERT order_info

  5. 对每个 cart_item:

  • UPDATE product_info SET stock = stock - quantity

  • INSERT order_item(价格快照)

  1. 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&lt;UserInfo&gt; logout(HttpSession session){
    session.invalidate();
    return  ResponseResult.success();
}

@GetMapping("/current")
public  ResponseResult&lt;UserInfo&gt; 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&lt;?&gt; 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>
	&lt;dependency&gt;
		&lt;groupId&gt;com.mysql&lt;/groupId&gt;
		&lt;artifactId&gt;mysql-connector-j&lt;/artifactId&gt;
		&lt;scope&gt;runtime&lt;/scope&gt;
	&lt;/dependency&gt;
	&lt;dependency&gt;
		&lt;groupId&gt;org.projectlombok&lt;/groupId&gt;
		&lt;artifactId&gt;lombok&lt;/artifactId&gt;
		&lt;optional&gt;true&lt;/optional&gt;
	&lt;/dependency&gt;
	&lt;dependency&gt;
		&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
		&lt;artifactId&gt;spring-boot-starter-validation-test&lt;/artifactId&gt;
		&lt;scope&gt;test&lt;/scope&gt;
	&lt;/dependency&gt;
	&lt;dependency&gt;
		&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
		&lt;artifactId&gt;spring-boot-starter-webmvc-test&lt;/artifactId&gt;
		&lt;scope&gt;test&lt;/scope&gt;
	&lt;/dependency&gt;
	&lt;dependency&gt;
		&lt;groupId&gt;org.mybatis.spring.boot&lt;/groupId&gt;
		&lt;artifactId&gt;mybatis-spring-boot-starter-test&lt;/artifactId&gt;
		&lt;version&gt;4.0.1&lt;/version&gt;
		&lt;scope&gt;test&lt;/scope&gt;
	&lt;/dependency&gt;
&lt;/dependencies&gt;

&lt;build&gt;
	&lt;plugins&gt;
		&lt;plugin&gt;
			&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
			&lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
			&lt;configuration&gt;
				&lt;mainClass&gt;com.example.springpetshop.SpringPetshopApplication&lt;/mainClass&gt;
			&lt;/configuration&gt;
		&lt;/plugin&gt;
	&lt;/plugins&gt;
&lt;/build&gt;
</project>

总结

本文在 Session 登录、MyBatis 分层、软删除、分页查询等骨架基础上,构建了完整的宠物用品电商项目,打通「登录、分类浏览、购物车、地址管理、下单、订单查询、客服消息」业务闭环。核心难点在于数据关系与事务处理:购物车中间表以用户和商品唯一约束实现重复加购自动累加;订单主子表采用地址引用与商品快照,隔离历史订单价格;OrderServiceImpl.checkout() 通过 @Transactional 串联购物车校验、库存扣减、订单插入与清空购物车,保障原子性。全项目代码按 Controller、Service、Mapper、POJO、拦截器、配置类完整分层,建议结合自测清单逐步验证,真正理解企业级电商的典型设计思路。

相关推荐
大文说跨境2 小时前
亚马逊多店铺的图片素材怎么批量去重:EXIF 与哈希的处理流程
开发语言·php
VimorphTech2 小时前
一段字幕太长,怎样拆句并安排显示时间?
java·spring boot
万联WANFLOW2 小时前
Kimi K3.1 Pro 接口接入指南:模型选型、费用计算与零数据保留
java·spring boot·python·安全·spring·大模型
织码weavecodes2 小时前
新闻资讯模块设计:内容发布、阅读计数与附件鉴权
java·服务器·数据库
几何心凉2 小时前
论文解读|智元 GO-1:VLA 进化到 ViLLA,中间多出来的那个“L“到底有什么用?
机器人·1024程序员节·具身智能·启元机器人
学到老活到老2 小时前
Spring 事务怎样生效?为什么加了 @Transactional 仍不回滚?
java·spring
学到老活到老2 小时前
Spring 怎样创建 Bean?@Autowired 在哪一步生效?
java·spring
用户112936112192 小时前
我扫了项目里 739 个 Controller,发现最危险的越权漏洞不是没认证
java
小羊没烦恼!2 小时前
分布式事务 | 使用DTM 的Saga 模式
java·linux·运维·数据库·wpf