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);
}
}
如果对你有帮助的话,请点赞,关注,收藏。热爱可抵一切!👍 ❤️ 🔥