万字超详细苍穹外卖学习笔记4

七、day07

1、缓存菜品

1.1、实现简单缓存

原方法都是直接对数据库进行操作,当访问量大时,同时对数据库进行大量操作,数据库压力过大可能会导致问题,相较于每次对数据库的操作(磁盘io操作),使用redis(内存操作)不仅效率高而且可以有效减少数据库的压力

现在则是在对数据库进行查询操作之前,先去redis中进行查询,如果查到了则直接返回redis中的数据而不去数据库中进行再次操作;如果没有查到则去数据库中操作后再添加进redis,方便下次查询

java 复制代码
@RestController("userDishController")
// 由于 nginx 的反向代理配置,已将初始路径设置为:http://localhost/api/ 或者也可以设置为 http://localhost:8080/
@RequestMapping("/user/dish")
@Slf4j
public class DishController {
    @Autowired
    private DishService dishService;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 根据分类 id查询菜品
    @GetMapping("/list")
    public Result<List<DishVO>> list(Long categoryId) {
        String key = "dish_" + categoryId;
        // 先从 redis 中查询是否有缓存数据
        List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
        if (list != null && !list.isEmpty()) {
            // 有缓存数据,直接返回
            return Result.success(list);
        }

        // 如果 redis 中没有缓存数据,则从数据库中查询
        Dish dish = new Dish();
        dish.setCategoryId(categoryId);
        dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品
        // 将起售菜品中带有分类 id 的实体类菜品携带口味数据和分类名称封装成 vo 类返回
        list = dishService.listWithFlavor(dish);
        // 在数据库中查询完后再添加到 redis 中方便下次直接从 redis 中查询
        redisTemplate.opsForValue().set(key, list);
        return Result.success(list);
    }
}
1.2、实现缓存清理

即为了确保 redis中缓存的数据要和数据库中的数据保持一致,当数据库中的数据进行了更新、删除、起售停售等操作------数据发生变化则需要对 redis中的缓存数据进行清理,之后添加进新的数据

对数据库的操作是管理端才可进行的,用户端无法直接对数据库进行操作,故进行缓存清理应该在管理端的controller层进行操作

java 复制代码
@RestController("adminDishController")
// 由于 nginx 的反向代理配置,已将初始路径设置为:http://localhost/api/ 或者也可以设置为 http://localhost:8080/
@RequestMapping("/dish")
@Slf4j
// 菜品相关接口
public class DishController {
    @Autowired
    private DishService dishService;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @PostMapping
    // 新增菜品
    // 根据接口文档可见将请求参数(json格式)封装为 DishDTO 对象,故需要使用 @RequestBody 注解
    // 请求参数默认是 @RequestParam(可写可不写)
    public Result save(@RequestBody DishDTO dishDTO) {
        log.info("新增菜品");
        dishService.saveWithFlavor(dishDTO);
        // 清理缓存数据
        cleanCache("dish_" + dishDTO.getCategoryId());
        return Result.success();
    }

    @GetMapping("/page")
    // 分页查询菜品
    // 根据接口文档可将请求参数封装为一个 DishPageQueryDTO 对象
    // 由于请求参数不是json格式,而是地址栏参数,所以不需要 @RequestBody 注解
    public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO) {
        log.info("分页查询菜品");
        PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
        return Result.success(pageResult);
    }

    @DeleteMapping
    // 菜品批量删除
    public Result delete(@RequestParam List<Long> ids) {
        dishService.deleteBatch(ids);
        // 清理缓存数据
        cleanCache("dish_*" );
        return Result.success();
    }

    @GetMapping("/{id}")
    // 根据id查询菜品
    // 由接口文档可见路径参数是 id,所以需要使用 @PathVariable 注解
    // 参数名要与路径参数名一致,这样即可省略写成 @PathVariable Long id
    public Result<DishVO> getById(@PathVariable("id") Long id) {
        DishVO dishVO = dishService.getByIdWithFlavor(id);
        return Result.success(dishVO);
    }

    @PutMapping
    // 更新菜品以及其对应口味
    // 根据接口文档可见将请求参数(json格式)封装为 DishDTO 对象,故需要使用 @RequestBody 注解
    // 请求参数默认是 @RequestParam(可写可不写)
    public Result update(@RequestBody DishDTO dishDTO) {
        dishService.updateWithFlavor(dishDTO);
        // 清理缓存数据
        cleanCache("dish_*" );
        return Result.success();
    }

    // 起售、停售菜品
    @PostMapping("/status/{status}")
    public Result updateStatus(@PathVariable("status") Integer status,Long id){
        dishService.startOrStop(status,id);
        // 清理缓存数据
        cleanCache("dish_*" );
        return Result.success();
    }

    // 根据 id 查询菜品
    @GetMapping("/list")
    public Result<List<Dish>> list(Long categoryId){
        List<Dish> list = dishService.list(categoryId);
        return Result.success(list);
    }
    // 清理缓存数据
    private void cleanCache(String pattern){
        Set<String> keys = redisTemplate.keys(pattern);
        redisTemplate.delete(keys);
    }
}


不过该清理缓存的方法太过粗糙,直接就是对缓存进行全部的清理而没有做到精确清理

2、缓存套餐

2.1、Spring Cache

Spring Cache 是 Spring Framework 提供的声明式缓存抽象,让你无需修改业务代码即可为方法添加缓存能力。它通过 AOP 拦截方法调用,根据注解决定何时读取、更新或删除缓存。

①、常用注释
注解 作用 典型场景
@EnableCaching 启用缓存功能 配置类上开启
@Cacheable 方法执行前先查缓存,命中则返回,否则执行方法并缓存结果 查询操作
@CachePut 总是执行方法,并用返回值更新缓存 更新操作后刷新缓存
@CacheEvict 执行方法后删除缓存 删除操作后清理缓存
@Caching 组合多个缓存操作 复杂场景
②、添加依赖
xml 复制代码
<!-- Spring Boot Starter Cache -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<!-- 使用Redis作为缓存实现 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
③、业务层使用

在使用前要先在启动类中添加 @EnableCaching 启用缓存功能

java 复制代码
@Service
public class UserService {
    
    /**
     * 查询用户:先查缓存,不存在则执行方法并缓存
     * key: 支持 SpEL 表达式
     * unless: 条件判断,当结果为 null 时不缓存(防止缓存穿透)
     */
    // value(即cacheNames)和 key 指定了缓存名称,完整的缓存键 = cacheNames::key
    @Cacheable(value = "user", key = "#id", unless = "#result == null")
    public User getUserById(Long id) {
        System.out.println("查询数据库: " + id);
        return userMapper.selectById(id);
    }
    
    /**
     * 更新用户:更新数据库后同步更新缓存
     */
  	// 此处 key 指传入的参数的 id
    @CachePut(value = "user", key = "#user.id")
    public User updateUser(User user) {
        userMapper.updateById(user);
        return user;  // 返回的对象会被缓存
    }
    
    /**
     * 删除用户:删除数据库后清理缓存
     * allEntries = true: 清空整个缓存(慎用)
     * beforeInvocation: 方法执行前清空(防止方法异常导致缓存未清理)
     */
    @CacheEvict(value = "user", key = "#id")
    public void deleteUser(Long id) {
        userMapper.deleteById(id);
    }
    
    /**
     * 复杂场景:组合操作
     */
    @Caching(
        cacheable = {@Cacheable(value = "user", key = "#name")},
        evict = {@CacheEvict(value = "userList", allEntries = true)}
    )
    public User getUserByName(String name) {
        return userMapper.selectByName(name);
    }
}

Spring Cache 的注解可在业务层添加,也可在Controller 层添加

④、简单对比
方式 优点 缺点 适用场景
@CachePut 原子性操作,保证缓存最新 可能短暂存在脏数据(如果更新失败) 强一致性要求不高,追求性能
@CacheEvict + @Cacheable 更安全,先清空保证下次读取最新 多一次查询开销 强一致性场景
2.2、缓存套餐
①、开启缓存功能
java 复制代码
@SpringBootApplication
@EnableTransactionManagement //开启注解方式的事务管理
@Slf4j
@MapperScan("com.sky.mapper") //扫描mapper接口
@EnableCaching  // 开启缓存功能
public class SkyApplication {
    public static void main(String[] args) {
        SpringApplication.run(SkyApplication.class, args);
        log.info("server started");
    }
}
②、添加注解
java 复制代码
@RestController("userSetmealController")
@RequestMapping("/user/setmeal")
public class SetmealController {
    @Autowired
    private SetmealService setmealService;

    // 根据分类 id查询套餐
    @GetMapping("/list")
    @Cacheable(value = "setmealCache", key = "#categoryId")
    public Result<List<Setmeal>> list(Long categoryId) {
        Setmeal setmeal = new Setmeal();
        setmeal.setCategoryId(categoryId);
        setmeal.setStatus(StatusConstant.ENABLE);
        // 将起售菜品中带有分类 id 的实体类套餐返回
        List<Setmeal> list = setmealService.list(setmeal);
        return Result.success(list);
    }

    //根据套餐 id查询包含的菜品列表
    @GetMapping("/dish/{id}")
    public Result<List<DishItemVO>> dishList(@PathVariable("id") Long id) {
        List<DishItemVO> list = setmealService.getDishItemById(id);
        return Result.success(list);
    }
}
java 复制代码
@RestController("adminSetmealController")
@RequestMapping("/setmeal")
@Slf4j
public class SetmealController {
    @Autowired
    private SetmealService setmealService;

    // 新增套餐
    @PostMapping
    @CacheEvict(value = "setmealCache", key = "#setmealDTO.categoryId")     // 删除指定分类 id 的套餐缓存
    public Result save(@RequestBody SetmealDTO setmealDTO){
        setmealService.saveWithDish(setmealDTO);
        return Result.success();
    }

    // 套餐分页查询
    @GetMapping("/page")
    // 分页查询返回的结果是 total和 records
    public Result<PageResult> page(SetmealPageQueryDTO setmealPageQueryDTO){
        PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO);
        return Result.success(pageResult);
    }

    // 删除套餐
    @DeleteMapping
    @CacheEvict(value = "setmealCache", allEntries = true)  // 删除所有套餐缓存
    // 也可写成 @RequestParam List<Long> ids
    public Result delete(List<Long> ids){
        setmealService.deleteBatch(ids);
        return Result.success();
    }

    // 修改套餐
    @PutMapping
    @CacheEvict(value = "setmealCache", allEntries = true)  // 删除所有套餐缓存
    public Result update(@RequestBody SetmealDTO setmealDTO){
        setmealService.update(setmealDTO);
        return Result.success();
    }

    // 起售、停售套餐
    @PostMapping("/status/{status}")
    @CacheEvict(value = "setmealCache", allEntries = true)  // 删除所有套餐缓存
    public Result updateStatus(@PathVariable("status") Integer status,Long id){
        setmealService.startOrStop(status,id);
        return Result.success();
    }

    // 根据套餐 id 查询套餐和其中关联的菜品
    @GetMapping("/{id}")
    public Result<SetmealVO> getById(@PathVariable("id") Long id){
        SetmealVO setmealVO = setmealService.getByIdWithDish(id);
        return Result.success(setmealVO);
    }
}
③、测试
④、缓存策略(个人分析)

为什么管理端不去使用或很少使用 Cacheable和 CachePut,而是用户端多使用

Ⅰ、管理端 vs 用户端的核心差异
维度 管理端 用户端
数据实时性要求 极高(需要立即看到修改效果) 可接受短暂延迟(最终一致)
查询模式 高频修改、低频查询 低频修改、超高并发查询
数据敏感性 涉及业务操作敏感数据 公开数据
Ⅱ、管理端缓存策略建议
  • 管理员需要实时看到数据变更
  • 管理端查询量通常不大,数据库可承受
  • 避免缓存与数据库同步的复杂度
Ⅲ、原则
  1. 管理端原则
    • 默认不缓存,保证实时性
    • 必要时采用超短时间缓存(≤30秒)
    • 所有写操作必须清理用户端缓存
  2. 用户端原则
    • 全面缓存,特别是高频访问数据
    • 采用多级缓存架构
    • 设置合理的过期时间(通常5-30分钟)

3、添加、查看、删除购物车

①、controller层
java 复制代码
@RestController("userShoppingCartController")
@RequestMapping("/user/shoppingCart")
public class ShoppingCartController {
    @Autowired
    private ShoppingCartService shoppingCartService;

    // 删除购物车中一个商品
    @PostMapping("/sub")
    public Result sub(@RequestBody ShoppingCartDTO shoppingCartDTO) {
        shoppingCartService.subShoppingCart(shoppingCartDTO);
        return Result.success();
    }

    // 单次操作添加购物车
    @PostMapping("/add")
    public Result add(@RequestBody ShoppingCartDTO shoppingCartDTO) {
        shoppingCartService.add(shoppingCartDTO);
        return Result.success();
    }

    // 查看购物车
    @GetMapping("/list")
    public Result<List<ShoppingCart>> list() {
        return Result.success(shoppingCartService.list());
    }

    // 清空购物车
    @DeleteMapping("/clean")
    public Result clean() {
        shoppingCartService.clean();
        return Result.success();
    }
}
②、service层
java 复制代码
public interface ShoppingCartService {
    // 删除购物车中一个商品
    void subShoppingCart(ShoppingCartDTO shoppingCartDTO);

    // 添加购物车
    void add(ShoppingCartDTO shoppingCartDTO);

    // 查看购物车
    List<ShoppingCart> list();

    // 清空购物车
    void clean();
}
java 复制代码
@Service
public class ShoppingCartServiceImpl implements ShoppingCartService {
    @Autowired
    private ShoppingCartMapper shoppingCartMapper;
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private SetmealMapper setmealMapper;

    // 删除购物车中一个商品
    public void subShoppingCart(ShoppingCartDTO shoppingCartDTO) {
        ShoppingCart shoppingCart = new ShoppingCart();
        BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
        Long userId = BaseContext.getCurrentId();
        // 通过 ThreadLocal 来获取当前用户的 id
        shoppingCart.setUserId(userId);
        List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
        ShoppingCart cart = list.get(0);
        // 判断商品数量是否为 1
        if(cart.getNumber() == 1){
            shoppingCartMapper.deleteById(cart.getId());
        }else {
            // 不为 1 则数量减 1 即可
            cart.setNumber(cart.getNumber() - 1);
            shoppingCartMapper.updateNumberById(cart);
        }
    }

    // 单次操作添加购物车
    // 即要么是添加一个菜品,要么是添加一个套餐
    public void add(ShoppingCartDTO shoppingCartDTO) {
        // 1、判断当前购物车中的商品是否存在
        ShoppingCart shoppingCart = new ShoppingCart();
        BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
        Long userId = BaseContext.getCurrentId();
        // 通过 ThreadLocal 来获取当前用户的 id
        shoppingCart.setUserId(userId);
        List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
        if (list != null && !list.isEmpty()) {
            // 如果商品已经存在,数量加 1
            ShoppingCart cart = list.get(0);
            cart.setNumber(cart.getNumber() + 1);
            shoppingCartMapper.updateNumberById(cart);
        } else {
            // 如果不存在,新增
            // 2、判断本次操作添加的是菜品还是套餐
            if (shoppingCart.getDishId() != null) {
                // 如果添加的是菜品,则去菜品表查询菜品信息
                // 从菜品表查询菜品信息补全实体类的其他属性
                Dish dish = dishMapper.getById(shoppingCart.getDishId());
                shoppingCart.setName(dish.getName());
                shoppingCart.setAmount(dish.getPrice());
                shoppingCart.setImage(dish.getImage());
            } else if (shoppingCart.getSetmealId() != null) {
                // 如果添加的是套餐,则去套餐表查询套餐信息
                // 从套餐表查询套餐信息补全实体类的其他属性
                Setmeal setmeal = setmealMapper.getById(shoppingCart.getSetmealId());
                shoppingCart.setName(setmeal.getName());
                shoppingCart.setAmount(setmeal.getPrice());
                shoppingCart.setImage(setmeal.getImage());
            }
            // 相同代码提取到这里
            shoppingCart.setNumber(1);
            shoppingCart.setCreateTime(LocalDateTime.now());
            shoppingCartMapper.insert(shoppingCart);
        }
    }

    // 查看购物车
    public List<ShoppingCart> list() {
        Long userId = BaseContext.getCurrentId();
        ShoppingCart shoppingCart = new ShoppingCart();
        shoppingCart.setUserId(userId);
        // 通过当前用户 id 动态查询购物车
        return shoppingCartMapper.list(shoppingCart);
    }

    // 清空购物车
    public void clean() {
        Long userId = BaseContext.getCurrentId();
        // 清空当前用户 id 的购物车
        shoppingCartMapper.cleanByUserId(userId);
    }
}
③、mapper层
java 复制代码
@Mapper
public interface ShoppingCartMapper {

    // 动态查询购物车
    List<ShoppingCart> list(ShoppingCart shoppingCart);

    // 更新购物车中商品的数量
    @Update("update shopping_cart set number = #{number} where id = #{id}")
    void updateNumberById(ShoppingCart shoppingCart);

    // 添加购物车(插入购物车的数据)
    void insert(ShoppingCart shoppingCart);

    // 清空购物车
    @Delete("delete from shopping_cart where user_id = #{userId}")
    void cleanByUserId(Long userId);

    // 根据商品 id 删除该商品记录
    @Delete("delete from shopping_cart where id = #{id}")
    void deleteById(Long id);
}
xml 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.ShoppingCartMapper">
    <!--   新增购物车     -->
    <insert id="insert" parameterType="com.sky.entity.ShoppingCart">
        insert into shopping_cart (name, user_id, dish_id, setmeal_id, dish_flavor, number, amount,image,create_time)
        values (#{name}, #{userId}, #{dishId}, #{setmealId}, #{dishFlavor}, #{number}, #{amount},#{image},#{createTime})
    </insert>

    <!--   动态查询购物车     -->
    <select id="list" parameterType="com.sky.entity.ShoppingCart" resultType="com.sky.entity.ShoppingCart">
        select * from shopping_cart
        <where>
            <if test="userId != null">
                and user_id = #{userId}
            </if>
            <if test="setmealId != null">
                and setmeal_id = #{setmealId}
            </if>
            <if test="dishId != null">
                and dish_id = #{dishId}
            </if>
            <if test="dishFlavor != null">
                and dish_flavor = #{dishFlavor}
            </if>
        </where>
    </select>
</mapper>
④、测试

八、day08

1、导入地址薄模块

①、controller层
java 复制代码
@RestController
@RequestMapping("/user/addressBook")
public class AddressBookController {
    @Autowired
    private AddressBookService addressBookService;

    // 新增地址
    @PostMapping
    public Result save(@RequestBody AddressBook addressBook) {
        addressBookService.save(addressBook);
        return Result.success();
    }

    // 查询当前登录用户的所有地址信息
    @GetMapping("/list")
    public Result<List<AddressBook>> list() {
        AddressBook addressBook = new AddressBook();
        addressBook.setUserId(BaseContext.getCurrentId());
        List<AddressBook> list = addressBookService.list(addressBook);
        return Result.success(list);
    }

    // 根据地址 id 查询地址
    @GetMapping("/{id}")
    public Result<AddressBook> getById(@PathVariable("id") Long id) {
        return Result.success(addressBookService.getById(id));
    }

    // 根据地址 id 修改地址
    @PutMapping
    public Result update(@RequestBody AddressBook addressBook) {
        addressBookService.update(addressBook);
        return Result.success();
    }

    // 设置默认地址
    @PutMapping("/default")
    public Result setDefault(@RequestBody AddressBook addressBook) {
        addressBookService.setDefault(addressBook);
        return Result.success();
    }

    // 根据 id 删除地址
    @DeleteMapping("/") // 注意此处前端请求地址后有一个 /,接口文档未说明!!!
    public Result deleteById(Long id) {
        addressBookService.deleteById(id);
        return Result.success();
    }

    // 查询默认地址
    @GetMapping("default")
    public Result<AddressBook> getDefault() {
        //SQL:select * from address_book where user_id = ? and is_default = 1
        AddressBook addressBook = new AddressBook();
        addressBook.setIsDefault(1);
        addressBook.setUserId(BaseContext.getCurrentId());
        List<AddressBook> list = addressBookService.list(addressBook);

        if (list != null && list.size() == 1) {
            return Result.success(list.get(0));
        }

        return Result.error("没有查询到默认地址");
    }
}
②、service层
java 复制代码
public interface AddressBookService {
    // 新增地址
    void save (AddressBook addressBook);

    // 查询当前登录用户的所有地址信息
    List<AddressBook> list(AddressBook addressBook);

    // 根据地址 id 查询地址
    AddressBook getById(Long id);

    // 根据地址 id 修改地址
    void update(AddressBook addressBook);

    // 设置默认地址
    void setDefault(AddressBook addressBook);

    // 根据地址 id 删除地址
    void deleteById(Long id);
}
java 复制代码
@Service
public class AddressBookServiceImpl implements AddressBookService {
    @Autowired
    private AddressBookMapper addressBookMapper;

    // 新增地址
    public void save (AddressBook addressBook){
        addressBook.setUserId(BaseContext.getCurrentId());
        // 新增地址时,默认设置为非默认地址
        addressBook.setIsDefault(0);
        addressBookMapper.insert(addressBook);
    }

    // 查询当前登录用户 id 的所有地址信息
    public List<AddressBook> list(AddressBook addressBook){
        addressBook.setUserId(BaseContext.getCurrentId());
        return addressBookMapper.list(addressBook);
    }

    // 根据地址 id 查询地址
    public AddressBook getById(Long id){
        return addressBookMapper.getById(id);
    }

    // 根据地址 id 修改地址
    public void update(AddressBook addressBook){
        addressBookMapper.updateById(addressBook);
    }

    // 设置默认地址
    public void setDefault(AddressBook addressBook){
        // 1.将当前用户 id 的所有地址设置为非默认
        addressBook.setUserId(BaseContext.getCurrentId());
        addressBook.setIsDefault(0);
        addressBookMapper.updateByUserId(addressBook);

        // 2.将当前地址设置为默认
        addressBook.setIsDefault(1);
        addressBookMapper.updateById(addressBook);
    }

    // 根据地址 id 删除地址
    public void deleteById(Long id) {
        addressBookMapper.deleteById(id);
    }

}
③、mapper层
java 复制代码
@Mapper
public interface AddressBookMapper {
    // 新增地址
    void insert(AddressBook addressBook);

    // 查询当前登录用户的所有地址信息
    List<AddressBook> list(AddressBook addressBook);

    // 根据地址 id 查询地址
    AddressBook getById(Long id);

    // 根据地址 id 修改地址
    void updateById(AddressBook addressBook);

    // 根据用户 id 修改地址
    @Update("update address_book set is_default = #{isDefault} where user_id = #{userId}")
    void updateByUserId(AddressBook addressBook);

    // 根据地址 id 删除地址
    @Delete("delete from address_book where id = #{id}")
    void deleteById(Long id);
}
xml 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.AddressBookMapper">
    <!-- 新增地址 -->
    <insert id="insert" parameterType="com.sky.entity.AddressBook">
        insert into address_book (user_id, consignee, phone, sex, province_code, province_name, city_code, city_name, district_code, district_name, detail, label, is_default)
        values (#{userId}, #{consignee}, #{phone}, #{sex}, #{provinceCode}, #{provinceName}, #{cityCode}, #{cityName}, #{districtCode}, #{districtName}, #{detail}, #{label}, #{isDefault})
    </insert>
    <!-- 查询当前登录用户的所有地址信息 -->
    <select id="list" parameterType="com.sky.entity.AddressBook" resultType="com.sky.entity.AddressBook">
        select * from address_book
        <where>
            <if test="userId != null"> and user_id = #{userId} </if>
            <if test="phone != null"> and phone = #{phone} </if>
            <if test="isDefault != null"> and is_default = #{isDefault} </if>
        </where>
    </select>
    <!-- 根据地址 id 查询地址 -->
    <select id="getById" parameterType="java.lang.Long" resultType="com.sky.entity.AddressBook">
        select * from address_book where id = #{id}
    </select>
    <!-- 根据地址 id 更新地址簿信息 -->
    <update id="updateById" parameterType="com.sky.entity.AddressBook">
        update address_book
        <set>
            <if test="consignee != null"> consignee = #{consignee}, </if>
            <if test="sex != null"> sex = #{sex}, </if>
            <if test="phone != null"> phone = #{phone}, </if>
            <if test="detail != null"> detail = #{detail}, </if>
            <if test="label != null"> label = #{label}, </if>
            <if test="isDefault != null"> is_default = #{isDefault}, </if>
        </set>
        where id = #{id}
    </update>

</mapper>
④、测试

2、用户下单

①、controller层
java 复制代码
@RestController("userOrderController")
@RequestMapping("/user/order")
public class OrderController {

    @Autowired
    private OrderService orderService;

    // 用户下单
    @PostMapping("/submit")
    public Result<OrderSubmitVO> submit(@RequestBody OrdersSubmitDTO ordersSubmitDTO) {
        return Result.success(orderService.submitOrder(ordersSubmitDTO));
    }
}
②、service层
java 复制代码
public interface OrderService {
    // 用户下单
    OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO);
}
java 复制代码
@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private OrderDetailMapper orderDetailMapper;
    @Autowired
    private AddressBookMapper addressBookMapper;
    @Autowired
    private ShoppingCartMapper shoppingCartMapper;

    // 用户下单
    // 由于是对多个表进行操作,所以需要添加事务注解
    @Transactional
    public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {
        // 1、处理业务异常
        // 1.1、校验地址簿是否存在
        AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());
        if (addressBook == null) {
            throw new IllegalArgumentException(MessageConstant.ADDRESS_BOOK_IS_NULL);
        }
        // 1.2、校验购物车是否为空
        ShoppingCart shoppingCart = new ShoppingCart();
        shoppingCart.setUserId(addressBook.getUserId());
        List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart);
        if (shoppingCartList == null || shoppingCartList.isEmpty()) {
            throw new IllegalArgumentException(MessageConstant.SHOPPING_CART_IS_NULL);
        }
        // 2、向订单表插入 1 条数据
        Orders order = new Orders();
        BeanUtils.copyProperties(ordersSubmitDTO, order);
        // 除了 DTO 中包含的字段,其他部分字段需要手动设置
        // 如取消原因等不需要设置,因为当前业务是下单,而不是取消
        order.setPhone(addressBook.getPhone());
        order.setAddress(addressBook.getDetail());
        order.setConsignee(addressBook.getConsignee());
        order.setNumber(String.valueOf(System.currentTimeMillis()));
        order.setUserId(addressBook.getUserId());
        order.setStatus(Orders.PENDING_PAYMENT);
        order.setPayStatus(Orders.UN_PAID);
        order.setOrderTime(LocalDateTime.now());
        orderMapper.insert(order);
        // 3、向订单明细表插入 n 条数据
        // 使用 ArrayList 来存储订单明细,避免频繁的扩容操作
        List<OrderDetail> orderDetailList = new ArrayList<>();
        for(ShoppingCart shoppingCartItem : shoppingCartList) {
            OrderDetail orderDetail = new OrderDetail();
            BeanUtils.copyProperties(shoppingCartItem, orderDetail);
            // 设置订单明细的订单 ID
            orderDetail.setOrderId(order.getId());
            orderDetailList.add(orderDetail);
        }
        // 批量插入订单明细
        orderDetailMapper.insertBatch(orderDetailList);
        // 4、清空购物车数据
        shoppingCartMapper.cleanByUserId(addressBook.getUserId());
        // 5、返回 VO
        OrderSubmitVO orderSubmitVO = OrderSubmitVO.builder()
                .id(order.getId())
                .orderNumber(order.getNumber())
                .orderAmount(order.getAmount())
                .orderTime(order.getOrderTime())
                .build();
        return orderSubmitVO;
    }
}
③、mapper层
java 复制代码
@Mapper
public interface OrderDetailMapper {
        // 批量插入订单明细数据
        void insertBatch(List<OrderDetail> orderDetailList);
}
xml 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.OrderDetailMapper">
    <!-- 批量插入订单明细数据 -->
    <!-- useGeneratedKeys="true"代表开启主键自增,keyProperty="id"代表将自增的主键值赋值给实体类的id属性 -->
    <insert id="insertBatch" parameterType="com.sky.entity.OrderDetail" useGeneratedKeys="true" keyProperty="id">
        insert into order_detail (name, image, order_id, dish_id, setmeal_id, dish_flavor, number, amount)
        values
        <foreach collection="orderDetailList" item="orderDetail" separator=",">
            (#{orderDetail.name}, #{orderDetail.image}, #{orderDetail.orderId}, #{orderDetail.dishId}, #{orderDetail.setmealId}, #{orderDetail.dishFlavor}, #{orderDetail.number}, #{orderDetail.amount})
        </foreach>
    </insert>
</mapper>
④、测试

3、订单支付

3.1、补充知识:内网穿透

国内网络运营商是普遍不提供公网 IP 的,我们当前所属为局域网地址(外界无法直接访问),要想使外界可访问我们局域网地址,我们需要有公网 IP ,而**内网穿透(NAT穿透)**就是可让外部访问内部局域网的技术

特性 公网 IP 内网穿透
本质 直接分配全球唯一 IP 地址 通过中转或隧道技术间接暴露服务
网络位置 设备直接暴露在互联网 设备仍在内网,通过中介与外界通信
典型场景 云服务器、企业级网络 家庭宽带、开发调试、临时访问需求
成本 需要购买/租用 免费工具或低成本方案(如 frp+云服务器)
配置复杂度 简单(直接访问) 需搭建中转服务或使用第三方工具

简单来说:我们属于内部局域网,外界要想访问需要一个可供其找到我们的公网 IP,内网穿透就是间接暴露我们的技术

①、核心概念

内网穿透是一种让外部网络访问局域网内设备的技术,解决以下问题:

  • 无公网IP时的远程访问
  • 本地开发时的外部测试
  • 跨网络设备互联
②、典型应用场景
  1. 开发测试:微信/支付宝回调调试
  2. 远程办公:访问公司内网OA系统
  3. 智能家居:外网控制家中设备
  4. 演示展示:临时分享本地运行的项目
③、主流实现方式对比
类型 代表方案 特点
第三方服务 Ngrok/花生壳 开箱即用,适合临时需求
自建服务 frp/nps 可控性强,适合长期使用
P2P穿透 ZeroTier 点对点直连,延迟低
云商方案 AWS/Aliyun NAT 企业级解决方案,成本较高
3.2、订单支付

订单支付我们通过使用微信官方提供的第三方 API 即可,只需要了解流程即可

①、获取微信支付平台证书、商户私钥
  • 通过访问微信支付商户平台获取平台证书
  1. 下载微信支付提供的 证书生成工具(商户平台→API安全→API证书→下载工具)。

使用微信支付官方工具
  1. 运行工具生成密钥对,自动保存为 apiclient_key.pem(已符合PKCS#8格式)。
②、获取公网 IP

使用内网穿透工具进行获取即可

③、配置文件
④、导入代码

九、day09

1、任务

完成用户端历史订单模块、商家端订单管理模块相关业务新功能开发和已有功能优化,具体任务列表如下:

1.1、新功能开发

用户端历史订单模块:

  • 查询历史订单
  • 查询订单详情
  • 取消订单
  • 再来一单

商家端订单管理模块:

  • 订单搜索
  • 各个状态的订单数量统计
  • 查询订单详情
  • 接单
  • 拒单
  • 取消订单
  • 派送订单
  • 完成订单
1.2、已有功能优化

优化用户下单功能,加入校验逻辑,如果用户的收货地址距离商家门店超出配送范围(配送范围为5公里内),则下单失败。

提示:

​ 1. 基于百度地图开放平台实现(https://lbsyun.baidu.com/)

​ 2. 注册账号--->创建应用获取AK(服务端应用)--->调用接口

  1. 相关接口

    https://lbsyun.baidu.com/index.php?title=webapi/guide/webservice-geocoding

    https://lbsyun.baidu.com/index.php?title=webapi/directionlite-v1

  2. 商家门店地址可以配置在配置文件中,例如:

    yaml 复制代码
    sky:
      shop:
        address: 北京市海淀区上地十街10号

2、实现

详见对应的参考答案或gitee中查看

相关推荐
腻害兔4 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社5 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
克里斯蒂亚诺更新5 小时前
ruoyi角色权限的对应关系
笔记
萧瑟余晖6 小时前
JDK 26 新特性详解
java·开发语言
马优晨6 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
a1117768 小时前
2FA 验证码生成器(github登录验证 app)
笔记·学习
我的xiaodoujiao8 小时前
API 接口自动化测试详细图文教程学习系列32--Allure测试报告2
python·学习·测试工具·pytest
维天说9 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json
破碎的南瓜9 小时前
sqli--sql注入过关笔记(1,2,3,4,5,9)
数据库·笔记·sql
Zane199410 小时前
并发 vs 并行:别再傻傻分不清了,一文讲透 Java 并发编程的第一课
java·后端