Java 篇-项目实战-苍穹外卖-笔记汇总

java 篇: 1.基础地基 2.设计原理 3.项目实战


开头总结:

秒速达智慧订餐系统包括管理后台和移动端应用两部分。管理后台是给餐厅企业内部的员工使用的,用于实现员工、菜品分类、菜品、套餐、订单等模块的管理。移动端应用面向消费者,主要实现了菜品浏览、购物车、下单、催单等功能。

员工登录认证,是员工在登录页面登录,发送请求,携带用户名和密码信息,进入拦截器,拦截器放行所有登录页面的请求,进入三层架构,查看用户名和密码是否正确,如果都正确,服务器生成 JWT 令牌,返回给前端,前端后续执行请求,就会在请求头中携带 JWT,就是放在 Authorization 头部的 Bearer token 中嘛。然后服务器去校验 JWT 令牌,看令牌是否有效,有效的话就放行执行相应的请求。

那 JWT 分为三部分,头部,负载跟签名,头部存放令牌类型和签名算法,负载就放一些用户信息,权限,用户名,密码这类的,签名是头部和负载用服务器的密钥加密生成,就是通过这里的签名判断数据是否被篡改,令牌是否有效。

这里有个细节就是在身份认证通过之后,会将员工 ID 放到 ThreadLocal 中。这样保证了线程的安全性,因为 ThreadLocal 给每个线程设了一个存储空间,每个线程在自己的数据副本上进行操作,起到了线程隔离的作用。然后也更加方便,在请求处理的整个过程中可以方便地获取当前员工 ID,就不需要在方法参数中传递或在各个层级之间传递。ThreadLocal 也避免了内存泄漏,如果用全局变量存储当前员工 ID,如果请求处理完后,没有清理该变量,会导致内存泄漏的问题。那 ThreadLocal 中的数据只在当前线程生命周期内有效,线程结束就会自动释放。

定时任务:比如用户下单后,一直不支付,或者用户收到货后,一直不点确认收货。这时候就需要定时任务。这里使用 Spring Task,Spring Task 是 Spring 框架提供的任务调度工具,可以在约定的时间自动执行某个代码逻辑。

具体实现是在启动类添加 @EnableScheduling 注解开启任务调度,在任务类的每个任务方法上带 @Scheduled 注解,注解里面是 cron 表达式,用来设定预定时间的字符串。可直接使用在线生成器。

nginx 接收客户端的请求,并将这些请求转发到内部多个服务器上,实现负载均衡。反向代理的代理对象是服务器,正向代理的对象是客户端。反向代理隐藏后端服务器真实的 IP 地址和信息,从而增加了服务器的安全性。

来单提醒,采用了 WebSocket,WebSocket 是基于 TCP 的一种新的网络协议,专门为实时通信设计的,它实现了浏览器与服务器全双工通信,浏览器和服务器只需要完成一次握手,两者就可以创立永久性的链接,并进行双向数据传输。

和 HTTP 对比:

HTTP 是短连接,是单向的,基于请求响应模型。而 WebSocket 是长连接,支持双向通信。但底层都是 TCP 连接。

当客户支付后,调用 WebSocket 的相关 API 实现服务端向客户端推送消息,客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示,规定服务端发送给客户端浏览器的数据为 JSON,字段包括:type(消息类型:1 来单提醒,2 客户催单),orderId,content

Maven 聚合:一个父项目(父 POM)包括多个子模块

模块拆分:将一个大型项目按功能拆分成多个小项目

依赖管理:在父 POM 中统一管理所有子模块的依赖版本,避免版本冲突。

一.配置与框架:

1.启动类 `SkyApplication.java`

2.配置文件 `application.yml` + `application-dev.yml`

3.WebMvcConfiguration.java

4.Result<T> 统一返回格式

扩展:Result 类-黑马点评 vs 苍穹外卖

5.PageResult 分页结果

二.员工登录完整链路:

1.流程图

2.具体实现

SQL 注入是一种攻击技术,攻击者通过在输入框中插入恶意的 SQL 代码

@Builder 注解

3.JWT

JWT 令牌的生成过程可以这样理解:前端传过来的参数中,真正有用的只有员工 ID,这个 ID 会被放进 JWT 的 Payload 部分作为自定义数据。而生成令牌所需的密钥和有效期都是从配置文件加载进来的,比如在苍穹外卖项目中,密钥是"itcast",有效期是 720000000000 毫秒。

在具体的生成过程中,Header 部分是由系统自动设置的,里面放的是预先指定的签名算法 HS256,还会自动添加一个 typ 字段标明令牌类型是 JWT。Payload 部分则包含两部分内容:一部分是我们传进去的员工 ID,另一部分是根据配置文件中的有效期计算出来的过期时间。这两部分各自经过 Base64Url 编码后,用点号连接起来,就形成了待签名的字符串。

最后一步是签名,它是对前面拼接好的 Header 和 Payload 部分,使用配置文件中的密钥进行 HMAC-SHA256 哈希计算得出的。这个签名的作用是确保令牌的完整性------如果有人篡改了 Header 或 Payload 的内容,签名验证就会失败。最终,JWT 令牌就是由 Header、Payload 和签名这三部分用点号连接而成的完整字符串。

简单来说,整个 JWT 就是用配置的密钥和有效期,把员工 ID 封装进去,通过哈希签名生成的一个安全令牌,它既能携带必要的用户身份信息,又能防止被篡改。

具体数据变化:

claims 当中只传了用户 id,最小化原则,token 当中只存必要信息。

JJWT 库自动添加了这个字段"typ"

`compact()` 方法负责将构建好的 JWT 对象压缩成最终的字符串格式。

第 2 步当中具体是怎么哈希的,先不管了,有点复杂。

4.JWT 验证

`handler` 是 Spring MVC 拦截器中的一个重要参数,它代表了被拦截的目标处理器,通常是一个 Controller 方法。`handler` 就像是告诉门卫"来的是谁",门卫根据身份决定

解析验证方法:将前端传来的 JWT 令牌字符串解析验证,还原成包含用户信息的 `Claims` 对象。

Token 自带防伪标识(签名),服务端每次都用密钥重新计算验证,无需存储任何状态。(每次传过来的 Token 自带检验要素就是那个签名,拿密钥将头和负载重新哈希算下,看是不是和签名匹配)

密钥是 JWT 防伪的命脉,只要密钥不泄露,Token 就是安全的!

就没法进行纂改,把负载当中的用户信息修改。

5.异常处理

第一部分设置不同的异常类(基类继承运行时异常,不同异常又继承基类)

第二部分就是全局异常捕捉,当出现运行时异常就跑到 control 层被 baseException 捕获,如果时 sql 受检异常,照样有对应的异常类捕获

第三部分是异常信息常量,用到输出

  • 第一层:定义异常类型(业务异常、系统异常)
  • 第二层:统一捕获处理(`@RestControllerAdvice` + `@ExceptionHandler`)
  • 第三层:统一消息管理(常量类、资源文件)

三.AOP 自动填充机制:

这是一个自定义注解,用于自动填充公共字段(如创建时间、更新时间、创建人、更新人等),避免在每个方法中重复编写赋值代码。

AOP 切面详解:

综合意思: 拦截 `mapper` 包下所有有 `@AutoFill` 注解的方法。

方法签名是方法的唯一标识,包含了方法的名称、参数列表和返回类型等信息。

方法签名 = 方法名 + 参数列表(不包括返回类型)

复制代码
@Aspect
@Component
public class AutoFillAspect {
    
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint) {
        // 1. 获取方法签名
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        
        // 2. 获取方法名
        String methodName = signature.getName();
        System.out.println("方法名: " + methodName);
        // 输出: insert 或 update
        
        // 3. 获取声明类
        Class<?> declaringClass = signature.getDeclaringType();
        System.out.println("声明类: " + declaringClass.getName());
        // 输出: com.sky.mapper.EmployeeMapper
        
        // 4. 获取方法对象(最重要)
        Method method = signature.getMethod();
        System.out.println("完整方法: " + method);
        // 输出: public int com.sky.mapper.EmployeeMapper.insert(Employee)
        
        // 5. 获取参数类型
        Class<?>[] parameterTypes = signature.getParameterTypes();
        System.out.println("参数类型: " + Arrays.toString(parameterTypes));
        // 输出: [class com.sky.entity.Employee]
        
        // 6. 获取返回类型
        Class<?> returnType = signature.getReturnType();
        System.out.println("返回类型: " + returnType);
        // 输出: int
        
        // 7. 获取参数名(需要编译时保留参数名)
        String[] parameterNames = signature.getParameterNames();
        System.out.println("参数名: " + Arrays.toString(parameterNames));
        // 输出: [employee]
        
        // 8. 获取方法上的注解
        AutoFill autoFill = method.getAnnotation(AutoFill.class);
        System.out.println("注解值: " + autoFill.value());
        // 输出: INSERT 或 UPDATE
    }
}

为什么用 `joinPoint.getArgs()` 而不是 `signature` 获取参数?

1.反射

`instanceof` 是 Java 中的类型检查运算符,用于判断一个对象是否属于某个类或其子类。

对象 instanceof 类名/接口名

返回 `true` 或 `false`

2.AOP

四.菜品管理

关键点: 一个菜品可以有多个口味,所以是一对多关系。

1.文件上传流程

复制代码
@PostMapping("/upload")
@ApiOperation("文件上传")
public Result<String> upload(MultipartFile file){
    log.info("文件上传:{}", file);
    
    try {
        _// 1. 获取原始文件名_
        String originalFilename = file.getOriginalFilename();
        _// originalFilename = "菜品.png"_
        
        _// 2. 截取文件后缀_
        String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
        _// extension = ".png"_
        
        _// 3. 生成新文件名(避免重名)_
        String objectName = UUID.randomUUID().toString() + extension;
        _// objectName = "a1b2c3d4-e5f6-7890-abcd-ef1234567890.png"_
        
        _// 4. 上传到阿里云 OSS_
        String filePath = aliOssUtil.upload(file.getBytes(), objectName);
        _// filePath = "https://bucket-name.oss-cn-beijing.aliyuncs.com/a1b2c3d4-e5f6-7890-abcd-ef1234567890.png"_
        
        _// 5. 返回文件 URL_
        return Result.success(filePath);
    } catch (IOException e) {
        log.error("文件上传失败:{}", e);
    }
    
    return Result.error(MessageConstant.UPLOAD_FAILED);
}

AliOssUtil 上传到阿里云

复制代码
public String upload(byte[] bytes, String objectName) {
    // 1. 创建 OSS 客户端
    OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
    
    try {
        // 2. 上传文件
        ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
    } catch (OSSException oe) {
        // 处理 OSS 异常
    } catch (ClientException ce) {
        // 处理客户端异常
    } finally {
        // 3. 关闭连接
        if (ossClient != null) {
            ossClient.shutdown();
        }
    }
    
    // 4. 构造文件访问 URL
    StringBuilder stringBuilder = new StringBuilder("https://");
    stringBuilder
            .append(bucketName)
            .append(".")
            .append(endpoint)
            .append("/")
            .append(objectName);
    

    
    log.info("文件上传到:{}", stringBuilder.toString());
    
    
    return stringBuilder.toString();
}

2.新增菜品

流程:

获取口味列表

判断是否为空

为每个口味设置菜品 ID(外键)

批量插入口味表

3.修改菜品(多表更新)

4.删除菜品

5.查询菜品(多表关联查询)

6.缓存清理

7.DTO、Entity、VO 的区别

8.CRUD 用户端下单主链路(购物车 → 提交订单 → 支付/回调):

1. 购物车

添加:

Service

复制代码
public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {
    // 第1步:将 DTO 转换成 Entity
    ShoppingCart shoppingCart = new ShoppingCart();
    BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
    
    // 第2步:获取当前用户 ID(从 ThreadLocal)
    Long userId = BaseContext.getCurrentId();
    shoppingCart.setUserId(userId);
    
    // 第3步:查询购物车中是否已存在相同商品
    List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
    
    // 第4步:判断是否存在
    if(list != null && list.size() > 0){
        // 存在:数量 +1
        ShoppingCart cart = list.get(0);
        cart.setNumber(cart.getNumber() + 1);
        shoppingCartMapper.updateNumberById(cart);
    }else {
        // 不存在:新增一条记录
        
        // 第5步:判断是菜品还是套餐
        Long dishId = shoppingCartDTO.getDishId();
        if(dishId != null){
            // 是菜品:从菜品表查询名称、图片、价格
            Dish dish = dishMapper.getById(dishId);
            shoppingCart.setName(dish.getName());
            shoppingCart.setImage(dish.getImage());
            shoppingCart.setAmount(dish.getPrice());
        }else{
            // 是套餐:从套餐表查询名称、图片、价格
            Long setmealId = shoppingCartDTO.getSetmealId();
            Setmeal setmeal = setmealMapper.getById(setmealId);
            shoppingCart.setName(setmeal.getName());
            shoppingCart.setImage(setmeal.getImage());
            shoppingCart.setAmount(setmeal.getPrice());
        }
        
        // 第6步:设置初始数量和创建时间
        shoppingCart.setNumber(1);
        shoppingCart.setCreateTime(LocalDateTime.now());
        
        // 第7步:插入购物车表
        shoppingCartMapper.insert(shoppingCart);
    }
}

查询:

删除:

清空购物车:

2.提交订单
复制代码
前端请求
  ↓
POST /user/order/submit
{
  "addressBookId": 1,
  "payMethod": 1,
  "amount": 86.00,
  ...
}
  ↓
OrderController.submit()
  ↓
OrderService.submitOrder()
  ├─ 1. 检查地址簿
  │   └─ SELECT * FROM address_book WHERE id = 1
  │      ├─ 存在 ✓
  │      └─ 不存在 ✗ → 抛异常
  │
  ├─ 2. 检查购物车
  │   └─ SELECT * FROM shopping_cart WHERE user_id = 1
  │      ├─ 有商品 ✓
  │      └─ 无商品 ✗ → 抛异常
  │
  ├─ 3. 插入订单表
  │   └─ INSERT INTO orders (number, status, user_id, ...) VALUES (...)
  │      └─ orders.id = 100(自动回写)
  │
  ├─ 4. 插入订单明细表
  │   └─ INSERT INTO order_detail (order_id, dish_id, number, ...) VALUES (...)
  │      ├─ order_id = 100
  │      ├─ dish_id = 10, number = 2
  │      └─ dish_id = 11, number = 1
  │
  ├─ 5. 清空购物车
  │   └─ DELETE FROM shopping_cart WHERE user_id = 1
  │
  └─ 6. 返回结果
      └─ OrderSubmitVO {
           id: 100,
           orderNumber: "1711000000000",
           orderTime: "2026-03-20 10:30:45",
           orderAmount: 86.00
         }
  ↓
前端收到
{
  "code": 1,
  "msg": null,
  "data": {
    "id": 100,
    "orderNumber": "1711000000000",
    "orderTime": "2026-03-20 10:30:45",
    "orderAmount": 86.00
  }
}

Service

复制代码
@Transactional  // ← 事务注解,保证原子性
public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {
    
    // ========== 第1步:业务校验 ==========
    
    // 1.1 检查地址簿是否存在
    AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());
    if(addressBook == null){
        throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
    }
    
    // 1.2 检查购物车是否为空
    Long userId = BaseContext.getCurrentId();
    ShoppingCart shoppingCart = new ShoppingCart();
    shoppingCart.setUserId(userId);
    List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart);
    
    if(shoppingCartList == null || shoppingCartList.size() == 0){
        throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);
    }
    
    // ========== 第2步:插入订单表 ==========
    
    Orders orders = new Orders();
    BeanUtils.copyProperties(ordersSubmitDTO, orders);
    
    // 设置订单基本信息
    orders.setOrderTime(LocalDateTime.now());           // 下单时间
    orders.setPayStatus(Orders.UN_PAID);               // 支付状态:未支付
    orders.setStatus(Orders.PENDING_PAYMENT);          // 订单状态:待付款
    orders.setNumber(String.valueOf(System.currentTimeMillis()));  // 订单号:时间戳
    
    // 从地址簿获取收货信息
    orders.setAddress(addressBook.getDetail());        // 详细地址
    orders.setPhone(addressBook.getPhone());           // 手机号
    orders.setConsignee(addressBook.getConsignee());   // 收货人
    
    // 设置用户信息
    orders.setUserId(userId);
    
    // 插入订单表
    orderMapper.insert(orders);
    // 此时 orders.id 已经被自动回写
    
    // ========== 第3步:插入订单明细表 ==========
    
    List<OrderDetail> orderDetailList = new ArrayList<>();
    
    for (ShoppingCart cart : shoppingCartList) {
        OrderDetail orderDetail = new OrderDetail();
        
        // 从购物车复制数据
        BeanUtils.copyProperties(cart, orderDetail);
        
        // 设置订单 ID(关键!)
        orderDetail.setOrderId(orders.getId());
        
        orderDetailList.add(orderDetail);
    }
    
    // 批量插入订单明细
    orderDetailMapper.insertBatch(orderDetailList);
    
    // ========== 第4步:清空购物车 ==========
    
    shoppingCartMapper.deleteByUserId(userId);
    
    // ========== 第5步:返回结果 ==========
    
    OrderSubmitVO orderSubmitVO = OrderSubmitVO.builder()
            .id(orders.getId())
            .orderTime(orders.getOrderTime())
            .orderNumber(orders.getNumber())
            .orderAmount(orders.getAmount())
            .build();
    
    return orderSubmitVO;
}
3.订单支付与微信支付回调
复制代码
用户点击"支付"
  ↓
前端调用 payment() 接口
  ↓
后端调用微信支付 API(生成预支付交易单)
  ↓
后端返回支付参数给前端
  ↓
前端调用微信支付 SDK(唤起支付界面)
  ↓
用户在微信中完成支付
  ↓
微信服务器回调后端 /notify/paySuccess
  ↓
后端验证签名、解密数据
  ↓
后端修改订单状态为"待接单"
  ↓
后端响应微信"SUCCESS"
  ↓
用户看到支付成功提示

WeChatPayUtil.pay() 做了什么?

复制代码
public JSONObject pay(String orderNum, BigDecimal total, String description, String openid) throws Exception {
    // 1. 调用微信统一下单接口
    String bodyAsString = jsapi(orderNum, total, description, openid);
    
    // 2. 解析返回结果,获取 prepay_id
    JSONObject jsonObject = JSON.parseObject(bodyAsString);
    String prepayId = jsonObject.getString("prepay_id");
    
    // 3. 二次签名(用于前端调起支付)
    String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
    String nonceStr = RandomStringUtils.randomNumeric(32);
    
    // 签名数据
    ArrayList<Object> list = new ArrayList<>();
    list.add(weChatProperties.getAppid());
    list.add(timeStamp);
    list.add(nonceStr);
    list.add("prepay_id=" + prepayId);
    
    // 4. 用私钥签名
    String packageSign = Base64.getEncoder().encodeToString(signature.sign());
    
    // 5. 返回支付参数
    JSONObject jo = new JSONObject();
    jo.put("timeStamp", timeStamp);
    jo.put("nonceStr", nonceStr);
    jo.put("package", "prepay_id=" + prepayId);
    jo.put("signType", "RSA");
    jo.put("paySign", packageSign);
    
    return jo;
}

前端代码(伪代码

复制代码
// 前端收到支付参数后
const paymentData = {
  timeStamp: "1711000000",
  nonceStr: "12345678901234567890123456789012",
  package: "prepay_id=wx2021033100000000000000000000",
  signType: "RSA",
  paySign: "xxx..."
};

// 调用微信支付 SDK
wx.requestPayment({
  timeStamp: paymentData.timeStamp,
  nonceStr: paymentData.nonceStr,
  package: paymentData.package,
  signType: paymentData.signType,
  paySign: paymentData.paySign,
  success: function(res) {
    // 支付成功
    console.log("支付成功");
  },
  fail: function(res) {
    // 支付失败
    console.log("支付失败");
  }
});

Controller 处理回调

复制代码
@RequestMapping("/paySuccess")
public void paySuccessNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
    // 1. 读取微信发来的数据
    String body = readData(request);
    log.info("支付成功回调:{}", body);
    
    // 2. 解密数据
    String plainText = decryptData(body);
    log.info("解密后的文本:{}", plainText);
    
    // 3. 解析 JSON
    JSONObject jsonObject = JSON.parseObject(plainText);
    String outTradeNo = jsonObject.getString("out_trade_no");      // 商户订单号
    String transactionId = jsonObject.getString("transaction_id"); // 微信交易号
    
    log.info("商户订单号:{}", outTradeNo);
    log.info("微信交易号:{}", transactionId);
    
    // 4. 业务处理(修改订单状态)
    orderService.paySuccess(outTradeNo);
    
    // 5. 响应微信
    responseToWeixin(response);
}

五.Redis 缓存 + Spring Cache:

Redis 配置:

复制代码
@Configuration
@Slf4j
public class RedisConfiguration {

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
        log.info("开始创建redis模板对象...");
        RedisTemplate redisTemplate = new RedisTemplate();
        
        // 设置 Redis 连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        
        // 设置 Key 的序列化器为字符串
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        
        return redisTemplate;
    }
}

1.店铺营业状态缓存

管理端:设置营业状态

复制代码
@RestController("adminShopController")
@RequestMapping("/admin/shop")
public class ShopController {
    
    public static final String KEY = "SHOP_STATUS";
    
    @Autowired
    private RedisTemplate redisTemplate;
    
    /**
     * 设置店铺的营业状态
     */
    @PutMapping("/{status}")
    @ApiOperation("设置店铺的营业状态")
    public Result setStatus(@PathVariable Integer status){
        log.info("设置店铺的营业状态为:{}", status == 1 ? "营业中" : "打烊中");
        
        // 存入 Redis
        redisTemplate.opsForValue().set(KEY, status);
        
        return Result.success();
    }
    
    /**
     * 获取店铺的营业状态
     */
    @GetMapping("/status")
    @ApiOperation("获取店铺的营业状态")
    public Result<Integer> getStatus(){
        Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
        log.info("获取到店铺的营业状态为:{}", status == 1 ? "营业中" : "打烊中");
        return Result.success(status);
    }
}

用户端:查询营业状态

复制代码
@RestController("userShopController")
@RequestMapping("/user/shop")
public class ShopController {
    
    public static final String KEY = "SHOP_STATUS";
    
    @Autowired
    private RedisTemplate redisTemplate;
    
    /**
     * 获取店铺的营业状态
     */
    @GetMapping("/status")
    @ApiOperation("获取店铺的营业状态")
    public Result<Integer> getStatus(){
        Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
        log.info("获取到店铺的营业状态为:{}", status == 1 ? "营业中" : "打烊中");
        return Result.success(status);
    }
}

2.菜品缓存

复制代码
@RestController("userDishController")
@RequestMapping("/user/dish")
public class DishController {
    
    @Autowired
    private DishService dishService;
    @Autowired
    private RedisTemplate redisTemplate;
    
    /**
     * 根据分类id查询菜品
     */
    @GetMapping("/list")
    @ApiOperation("根据分类id查询菜品")
    public Result<List<DishVO>> list(Long categoryId) {
        
        // 第1步:构造 Redis Key
        String key = "dish_" + categoryId;
        
        // 第2步:查询 Redis 缓存
        List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
        
        // 第3步:判断缓存是否存在
        if(list != null && list.size() > 0){
            // 缓存命中,直接返回
            return Result.success(list);
        }
        
        // 第4步:缓存未命中,查询数据库
        Dish dish = new Dish();
        dish.setCategoryId(categoryId);
        dish.setStatus(StatusConstant.ENABLE);  // 只查起售中的菜品
        
        list = dishService.listWithFlavor(dish);
        
        // 第5步:存入 Redis 缓存
        redisTemplate.opsForValue().set(key, list);
        
        // 第6步:返回结果
        return Result.success(list);
    }
}

3.缓存清除

4.Spring Cache 注解(进阶)

六.WebSocket 实时通信:

时间线:

复制代码
时刻1:客户端连接
  ↓
前端:ws = new WebSocket("ws://localhost:8081/ws/admin")
  ↓
后端:@OnOpen 被调用
  ├─ sessionMap.put("admin", session)
  └─ 现在 sessionMap = {"admin": Session对象}
  ↓

时刻2:客户端发送消息
  ↓
前端:ws.send("用户催单了")
  ↓
后端:@OnMessage 被调用
  ├─ message = "用户催单了"
  ├─ sid = "admin"
  └─ 处理消息...
  ↓

时刻3:服务器主动推送消息
  ↓
后端:webSocketServer.sendToAllClient("新订单来了")
  ↓
前端:ws.onmessage = function(event) {
        console.log(event.data);  // "新订单来了"
      }
  ↓

时刻4:客户端断开连接
  ↓
前端:ws.close()
  ↓
后端:@OnClose 被调用
  ├─ sessionMap.remove("admin")
  └─ 现在 sessionMap = {}

1.WebSocket 配置类

复制代码
@Configuration
public class WebSocketConfiguration {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}

这个配置类的作用是将 WebSocket 服务器端点暴露给容器,使得 Spring Boot 能够识别和处理带有 `@ServerEndpoint` 注解的 WebSocket 端点类。

2.WebSocket 服务器

复制代码
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {

    // 存放所有连接的会话
    private static Map<String, Session> sessionMap = new HashMap();

    /**
     * 连接建立成功调用
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) {
        System.out.println("客户端:" + sid + "建立连接");
        sessionMap.put(sid, session);
    }

    /**
     * 收到客户端消息调用
     */
    @OnMessage
    public void onMessage(String message, @PathParam("sid") String sid) {
        System.out.println("收到来自客户端:" + sid + "的信息:" + message);
    }

    /**
     * 连接关闭调用
     */
    @OnClose
    public void onClose(@PathParam("sid") String sid) {
        System.out.println("连接断开:" + sid);
        sessionMap.remove(sid);
    }

    /**
     * 群发消息
     */
    public void sendToAllClient(String message) {
        Collection<Session> sessions = sessionMap.values();
        for (Session session : sessions) {
            try {
                session.getBasicRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

3.实际应用场景

复制代码
@Service
public class OrderServiceImpl implements OrderService {
    
    @Autowired
    private WebSocketServer webSocketServer;
    
    @Transactional
    public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {
        // ... 订单逻辑
        
        // 来单提醒
        Map<String, Object> map = new HashMap<>();
        map.put("type", 1);  // 1 = 来单提醒
        map.put("orderId", orders.getId());
        map.put("orderNumber", orders.getNumber());
        map.put("orderAmount", orders.getAmount());
        
        webSocketServer.sendToAllClient(JSON.toJSONString(map));
        
        return orderSubmitVO;
    }
}
复制代码
const ws = new WebSocket("ws://localhost:8081/ws/admin");

ws.onmessage = function(event) {
    const data = JSON.parse(event.data);
    
    if(data.type === 1) {
        // 来单提醒
        console.log("新订单:" + data.orderNumber);
        // 播放提示音
        playSound();
        // 显示通知
        showNotification(data);
    }
};
复制代码
@OnMessage
public void onMessage(String message, @PathParam("sid") String sid) {
    JSONObject jsonObject = JSON.parseObject(message);
    Integer type = jsonObject.getInteger("type");
    
    if(type == 2) {
        // 用户催单
        Long orderId = jsonObject.getLong("orderId");
        
        // 更新订单状态
        orderService.reminder(orderId);
        
        // 推送给管理端
        Map<String, Object> map = new HashMap<>();
        map.put("type", 2);  // 2 = 用户催单
        map.put("orderId", orderId);
        
        sendToAllClient(JSON.toJSONString(map));
    }
}

4,定时任务推送

WebSocketTask

复制代码
@Component
public class WebSocketTask {
    
    @Autowired
    private WebSocketServer webSocketServer;

    /**
     * 通过 WebSocket 每隔 5 秒向客户端发送消息
     */
    @Scheduled(cron = "0/5 * * * * ?")
    public void sendMessageToClient() {
        webSocketServer.sendToAllClient(
            "这是来自服务端的消息:" + 
            DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now())
        );
    }
}

这段代码是一个 WebSocket 定时推送任务,通过 Spring 的定时任务功能,每隔 5 秒向所有在线客户端推送当前时间。

七.SpringTask 定时任务:

启动定时任务

复制代码
@SpringBootApplication
@EnableScheduling  // ← 开启定时任务
public class SkyApplication {
    public static void main(String[] args) {
        SpringApplication.run(SkyApplication.class, args);
        log.info("server started");
    }
}

定时任务默认单线程,需要配置线程池

定时任务类

复制代码
@Component  // 必须是 Spring Bean
@Slf4j
public class OrderTask {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Scheduled(cron = "0 * * * * ?")  // 每分钟执行一次
    public void processTimeoutOrder(){
        log.info("定时处理超时订单:{}", LocalDateTime.now());
        // 业务逻辑
    }
}

超时订单处理

复制代码
@Component
@Slf4j
public class OrderTask {
    
    @Autowired
    private OrderMapper orderMapper;
    
    /**
     * 处理超时订单
     */
    @Scheduled(cron = "0 * * * * ?")  // 每分钟执行一次
    public void processTimeoutOrder(){
        log.info("定时处理超时订单:{}", LocalDateTime.now());
        
        // 第1步:计算 15 分钟前的时间
        LocalDateTime time = LocalDateTime.now().plusMinutes(-15);
        
        // 第2步:查询所有"待付款"且下单时间超过 15 分钟的订单
        List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(
            Orders.PENDING_PAYMENT,  // 状态:待付款
            time                     // 下单时间 < 15分钟前
        );
        
        // 第3步:遍历订单,更新状态为"已取消"
        if(ordersList != null && ordersList.size() > 0){
            for (Orders orders : ordersList) {
                orders.setStatus(Orders.CANCELLED);           // 状态改为已取消
                orders.setCancelReason("订单超时,自动取消");
                orders.setCancelTime(LocalDateTime.now());
                orderMapper.update(orders);
            }
        }
    }
}

八.AI 客服:

复制代码
package com.sky.service.impl;

import com.alibaba.fastjson.JSON;
import com.sky.context.BaseContext;
import com.sky.dto.ChatMessageDTO;
import com.sky.entity.ChatMessage;
import com.sky.entity.ChatSession;
import com.sky.mapper.ChatMessageMapper;
import com.sky.mapper.ChatSessionMapper;
import com.sky.service.ChatService;
import com.sky.vo.ChatMessageVO;
import com.sky.websocket.WebSocketServer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
@Slf4j
public class ChatServiceImpl implements ChatService {

    @Autowired
    private ChatMessageMapper chatMessageMapper;
    @Autowired
    private ChatSessionMapper chatSessionMapper;
    @Autowired
    private WebSocketServer webSocketServer;
    @Autowired
    private ChatClient chatClient;

    // 统一获取用户ID,测试时默认为1
    private Long getCurrentUserId() {
        Long userId = BaseContext._getCurrentId_();
        return userId != null ? userId : 1L;
    }

    @Transactional
    public ChatMessageVO sendMessage(ChatMessageDTO chatMessageDTO) {
        Long userId = getCurrentUserId();
        _log_.info("用户 {} 发送消息:{}", userId, chatMessageDTO.getContent());

        ChatMessage userMessage = ChatMessage._builder_()
                .userId(userId)
                .sessionId(chatMessageDTO.getSessionId())
                .messageType(1)
                .content(chatMessageDTO.getContent())
                .sender("user")
                .createTime(LocalDateTime._now_())
                .build();
        chatMessageMapper.insert(userMessage);

        String aiReply = callAi(chatMessageDTO.getContent(), chatMessageDTO.getSessionId());

        ChatMessage aiMessage = ChatMessage._builder_()
                .userId(userId)
                .sessionId(chatMessageDTO.getSessionId())
                .messageType(2)
                .content(aiReply)
                .sender("ai")
                .createTime(LocalDateTime._now_())
                .build();
        chatMessageMapper.insert(aiMessage);

        ChatSession session = chatSessionMapper.getById(chatMessageDTO.getSessionId());
        if (session != null) {
            session.setMessageCount(session.getMessageCount() + 2);
            session.setUpdateTime(LocalDateTime._now_());
            chatSessionMapper.update(session);
        }

        Map<String, Object> map = new HashMap<>();
        map.put("type", 3);
        map.put("sender", "ai");
        map.put("content", aiReply);
        map.put("createTime", LocalDateTime._now_());
        webSocketServer.sendToAllClient(JSON._toJSONString_(map));

        _log_.info("AI 回复:{}", aiReply);

        return ChatMessageVO._builder_()
                .id(aiMessage.getId())
                .sender("ai")
                .content(aiReply)
                .createTime(aiMessage.getCreateTime())
                .build();
    }

    private String callAi(String userMessage, Long sessionId) {
        try {
            return chatClient.prompt()
                    .system("你是一个外卖平台的智能客服助手,名叫小天。帮助用户解答关于订单、菜品、配送、退款等问题。请用友好、简洁、专业的语气回答。")
                    .user(userMessage)
                    .call()
                    .content();
        } catch (Exception e) {
            _log_.error("调用 AI 失败", e);
            return "抱歉,我暂时无法回答您的问题,请稍后再试。";
        }
    }

    public List<ChatMessageVO> getHistory(Long sessionId) {
        List<ChatMessage> messages = chatMessageMapper.getBySessionId(sessionId);
        return messages.stream()
                .map(msg -> ChatMessageVO._builder_()
                        .id(msg.getId())
                        .sender(msg.getSender())
                        .content(msg.getContent())
                        .createTime(msg.getCreateTime())
                        .build())
                .collect(Collectors._toList_());
    }

    public List<ChatSession> getSessions() {
        Long userId = getCurrentUserId();
        return chatSessionMapper.getByUserId(userId);
    }

    @Transactional
    public ChatSession createSession() {
        Long userId = getCurrentUserId();
        ChatSession session = ChatSession._builder_()
                .userId(userId)
                .title("新会话")
                .messageCount(0)
                .createTime(LocalDateTime._now_())
                .updateTime(LocalDateTime._now_())
                .build();
        chatSessionMapper.insert(session);
        _log_.info("用户 {} 创建新会话,ID:{}", userId, session.getId());
        return session;
    }

    @Transactional
    public void deleteSession(Long sessionId) {
        chatMessageMapper.deleteBySessionId(sessionId);
        chatSessionMapper.delete(sessionId);
        _log_.info("删除会话:{}", sessionId);
    }
}

如果对你有帮助的话,请点赞,关注,收藏。热爱可抵一切!👍 ❤️ 🔥

相关推荐
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题】【Java基础篇】第22题:HashMap 和 HashSet 有哪些区别
java·开发语言·哈希算法·散列表·hash
juniperhan2 小时前
Flink 系列第21篇:Flink SQL 函数与 UDF 全解读:类型推导、开发要点与 Module 扩展
java·大数据·数据仓库·分布式·sql·flink
ID_180079054732 小时前
Python 实现亚马逊商品详情 API 数据准确性校验(极简可用 + JSON 参考)
java·python·json
c++之路2 小时前
C++23概述
java·c++·c++23
时空系2 小时前
第10篇:继承扩展——面向对象编程进阶 python中文编程
开发语言·python·ai编程
专注API从业者3 小时前
Open Claw 京东商品监控选品实战:一键抓取、实时监控、高效选品
java·服务器·数据库
sakiko_3 小时前
UIKit学习笔记4-使用UITableView制作滚动视图
笔记·学习·ios·swift·uikit
CHANG_THE_WORLD3 小时前
python 批量终止进程exe
开发语言·python
摇滚侠3 小时前
DBeaver 导入数据库 导入 SQL 文件 MySQL 备份恢复
java·数据库·mysql