七、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 用户端的核心差异
| 维度 | 管理端 | 用户端 |
|---|---|---|
| 数据实时性要求 | 极高(需要立即看到修改效果) | 可接受短暂延迟(最终一致) |
| 查询模式 | 高频修改、低频查询 | 低频修改、超高并发查询 |
| 数据敏感性 | 涉及业务操作敏感数据 | 公开数据 |
Ⅱ、管理端缓存策略建议
- 管理员需要实时看到数据变更
- 管理端查询量通常不大,数据库可承受
- 避免缓存与数据库同步的复杂度
Ⅲ、原则
- 管理端原则 :
- 默认不缓存,保证实时性
- 必要时采用超短时间缓存(≤30秒)
- 所有写操作必须清理用户端缓存
- 用户端原则 :
- 全面缓存,特别是高频访问数据
- 采用多级缓存架构
- 设置合理的过期时间(通常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时的远程访问
- 本地开发时的外部测试
- 跨网络设备互联
②、典型应用场景
- 开发测试:微信/支付宝回调调试
- 远程办公:访问公司内网OA系统
- 智能家居:外网控制家中设备
- 演示展示:临时分享本地运行的项目
③、主流实现方式对比
| 类型 | 代表方案 | 特点 |
|---|---|---|
| 第三方服务 | Ngrok/花生壳 | 开箱即用,适合临时需求 |
| 自建服务 | frp/nps | 可控性强,适合长期使用 |
| P2P穿透 | ZeroTier | 点对点直连,延迟低 |
| 云商方案 | AWS/Aliyun NAT | 企业级解决方案,成本较高 |
3.2、订单支付
订单支付我们通过使用微信官方提供的第三方 API 即可,只需要了解流程即可
①、获取微信支付平台证书、商户私钥
- 通过访问微信支付商户平台获取平台证书
-
下载微信支付提供的 证书生成工具(商户平台→API安全→API证书→下载工具)。
使用微信支付官方工具
- 运行工具生成密钥对,自动保存为 apiclient_key.pem(已符合PKCS#8格式)。
②、获取公网 IP
使用内网穿透工具进行获取即可
③、配置文件
④、导入代码
九、day09
1、任务
完成用户端历史订单模块、商家端订单管理模块相关业务新功能开发和已有功能优化,具体任务列表如下:
1.1、新功能开发
用户端历史订单模块:
- 查询历史订单
- 查询订单详情
- 取消订单
- 再来一单
商家端订单管理模块:
- 订单搜索
- 各个状态的订单数量统计
- 查询订单详情
- 接单
- 拒单
- 取消订单
- 派送订单
- 完成订单
1.2、已有功能优化
优化用户下单功能,加入校验逻辑,如果用户的收货地址距离商家门店超出配送范围(配送范围为5公里内),则下单失败。
提示:
1. 基于百度地图开放平台实现(https://lbsyun.baidu.com/)
2. 注册账号--->创建应用获取AK(服务端应用)--->调用接口
-
相关接口
https://lbsyun.baidu.com/index.php?title=webapi/guide/webservice-geocoding
https://lbsyun.baidu.com/index.php?title=webapi/directionlite-v1
-
商家门店地址可以配置在配置文件中,例如:
yamlsky: shop: address: 北京市海淀区上地十街10号
2、实现
详见对应的参考答案或gitee中查看