万字超详细苍穹外卖学习笔记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中查看

相关推荐
日更嵌入式的打工仔2 小时前
C内存布局
笔记
努力写代码的熊大2 小时前
c++异常和智能指针
java·开发语言·c++
卡布叻_星星2 小时前
达梦数据库笔记之使用教程以及不推荐迁移选择小写
笔记
山岚的运维笔记2 小时前
SQL Server笔记 -- 第15章:INSERT INTO
java·数据库·笔记·sql·microsoft·sqlserver
Yvonne爱编码2 小时前
JAVA数据结构 DAY5-LinkedList
java·开发语言·python
孞㐑¥2 小时前
算法—队列+宽搜(bfs)+堆
开发语言·c++·经验分享·笔记·算法
代码游侠2 小时前
学习笔记——Linux字符设备驱动开发
linux·arm开发·驱动开发·单片机·嵌入式硬件·学习·算法
charlie1145141912 小时前
嵌入式C++教程——ETL(Embedded Template Library)
开发语言·c++·笔记·学习·嵌入式·etl
小王不爱笑1322 小时前
LangChain4J 整合多 AI 模型核心实现步骤
java·人工智能·spring boot