主题
商城后端接口实现
把商城核心接口完整写出来。数据库表结构见第 7 部分,这里直接使用。顺序:用户 → 商品 → 购物车 → 订单。
一、用户模块(Auth)
java
// controller/AuthController.java
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final UserService userService;
public AuthController(UserService userService) {
this.userService = userService;
}
@PostMapping("/register")
public Result<Void> register(@RequestBody @Valid RegisterDTO dto) {
userService.register(dto);
return Result.ok(null);
}
@PostMapping("/login")
public Result<LoginVO> login(@RequestBody @Valid LoginDTO dto) {
return Result.ok(userService.login(dto));
}
}java
// service/UserService.java(核心逻辑)
@Service
public class UserService {
private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder;
public void register(RegisterDTO dto) {
// 1. 校验用户名唯一
Long count = userMapper.selectCount(
new LambdaQueryWrapper<User>().eq(User::getUsername, dto.getUsername()));
if (count > 0) {
throw new BizException("用户名已存在");
}
// 2. 密码加密存储(绝不明文)
User user = new User();
user.setUsername(dto.getUsername());
user.setPassword(passwordEncoder.encode(dto.getPassword()));
user.setNickname(dto.getUsername());
userMapper.insert(user);
}
public LoginVO login(LoginDTO dto) {
User user = userMapper.selectOne(
new LambdaQueryWrapper<User>().eq(User::getUsername, dto.getUsername()));
if (user == null || !passwordEncoder.matches(dto.getPassword(), user.getPassword())) {
throw new BizException("用户名或密码错误");
}
// 签发 JWT(第 6 章详细讲)
String token = JwtUtil.generateToken(user.getId());
LoginVO vo = new LoginVO();
vo.setToken(token);
vo.setNickname(user.getNickname());
return vo;
}
}二、商品模块
java
// controller/ProductController.java
@RestController
@RequestMapping("/api/product")
public class ProductController {
private final ProductService productService;
@GetMapping("/list")
public Result<IPage<ProductVO>> list(
@RequestParam(required = false) String keyword,
@RequestParam(required = false) Long categoryId,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
return Result.ok(productService.pageVO(keyword, categoryId, page, size));
}
@GetMapping("/{id}")
public Result<ProductVO> detail(@PathVariable Long id) {
return Result.ok(productService.detail(id));
}
}三、购物车模块(核心业务)
java
// service/CartService.java
@Service
public class CartService {
private final CartItemMapper cartItemMapper;
private final ProductMapper productMapper;
// 加入购物车:已有则数量 +1,没有则新增
public void add(Long userId, CartAddDTO dto) {
CartItem exist = cartItemMapper.selectOne(new LambdaQueryWrapper<CartItem>()
.eq(CartItem::getUserId, userId)
.eq(CartItem::getProductId, dto.getProductId()));
if (exist != null) {
exist.setCount(exist.getCount() + dto.getCount());
cartItemMapper.updateById(exist);
return;
}
CartItem item = new CartItem();
item.setUserId(userId);
item.setProductId(dto.getProductId());
item.setCount(dto.getCount());
cartItemMapper.insert(item);
}
// 查询购物车(联查商品信息)
public List<CartItemVO> list(Long userId) {
// 查购物车条目
List<CartItem> items = cartItemMapper.selectList(
new LambdaQueryWrapper<CartItem>().eq(CartItem::getUserId, userId));
// 组装商品信息(join product)
return items.stream().map(item -> {
Product product = productMapper.selectById(item.getProductId());
CartItemVO vo = new CartItemVO();
vo.setId(item.getId());
vo.setProductId(product.getId());
vo.setProductName(product.getName());
vo.setPrice(product.getPrice());
vo.setImage(product.getImage());
vo.setStock(product.getStock());
vo.setCount(item.getCount());
return vo;
}).toList();
}
}四、订单模块(事务 + 库存扣减)
下单是最典型的"事务场景":扣库存、建订单必须同时成功或同时失败。
java
// service/OrderService.java
@Service
public class OrderService {
private final OrderMapper orderMapper;
private final OrderItemMapper orderItemMapper;
private final CartItemMapper cartItemMapper;
private final ProductMapper productMapper;
@Transactional // 事务:任何一个环节失败,全部回滚
public Order create(Long userId, OrderCreateDTO dto) {
BigDecimal total = BigDecimal.ZERO;
Order order = new Order();
order.setUserId(userId);
order.setOrderNo(OrderNoUtil.generate()); // 订单号:yyyyMMdd+时间戳+随机
order.setStatus("PENDING"); // 待支付
order.setAddress(dto.getAddress());
// 遍历购物车条目,扣库存 + 生成订单明细
for (Long cartId : dto.getCartIds()) {
CartItem item = cartItemMapper.selectById(cartId);
Product product = productMapper.selectById(item.getProductId());
// 校验库存
if (product.getStock() < item.getCount()) {
throw new BizException(product.getName() + " 库存不足");
}
// 扣库存(乐观锁防超卖,第 7 部分详讲)
int rows = productMapper.reduceStock(product.getId(), item.getCount());
if (rows == 0) {
throw new BizException(product.getName() + " 库存不足");
}
total = total.add(product.getPrice().multiply(BigDecimal.valueOf(item.getCount())));
OrderItem oi = new OrderItem();
oi.setOrderId(order.getId());
oi.setProductId(product.getId());
oi.setProductName(product.getName()); // 快照:改价/下架不影响历史订单
oi.setPrice(product.getPrice());
oi.setCount(item.getCount());
orderItemMapper.insert(oi);
cartItemMapper.deleteById(item.getId()); // 从购物车移除
}
order.setTotalAmount(total);
orderMapper.insert(order);
return order;
}
}乐观锁防超卖
java
// ProductMapper.java
@Update("UPDATE product SET stock = stock - #{count} " +
"WHERE id = #{id} AND stock >= #{count}")
int reduceStock(@Param("id") Long id, @Param("count") Integer count);
WHERE stock >= #{count}是乐观锁思路:更新时校验条件,条件不满足返回 0 行,说明库存不够,抛异常回滚。并发下也不会超卖(面试高频考点)。
五、订单状态流转
java
public void updateStatus(Long orderId, String from, String to) {
int rows = orderMapper.updateStatusWithCheck(orderId, from, to);
if (rows == 0) {
throw new BizException("订单状态已变更,操作失败");
}
}java
// OrderMapper.java
@Update("UPDATE `order` SET status = #{to} WHERE id = #{id} AND status = #{from}")
int updateStatusWithCheck(@Param("id") Long id,
@Param("from") String from,
@Param("to") String to);六、接口验收清单
| 接口 | 方法 | 说明 |
|---|---|---|
/api/auth/register | POST | 注册,密码加密存储 |
/api/auth/login | POST | 登录,返回 token |
/api/product/list | GET | 分页 + 搜索 + 分类 |
/api/product/{id} | GET | 详情 |
/api/cart | GET/POST | 查询 / 加购 |
/api/cart/{id} | PATCH/DELETE | 改数量 / 删除 |
/api/order | POST | 下单(事务 + 扣库存) |
/api/order/list | GET | 我的订单 |
待办
现在接口还没有鉴权——任何请求都能访问任意用户的购物车。第 6 章讲 JWT 后,把"当前登录用户"从 token 里取出来替换 userId 参数。