项目介绍

这是一个专门为餐饮企业(餐厅、饭店)定制的一款外卖软件,分为管理端和用户端。管理端供外卖商家使用,用户端实现用户点餐。

管理端:

用户端:

业务功能模块

   

技术选型

技术选型:展示项目中使用到的技术框架和中间件等

用户层(直接与用户交互):

  负责前端界面展示、用户操作处理,是用户接触系统的 “入口”。

  • node.js:基于 Chrome V8 引擎的 JavaScript 运行时,可用于前端工程化(如构建工具)或服务端逻辑。
  • Vue.js:前端框架,通过 “数据驱动视图” 简化界面开发,实现组件化编程。
  • ElementUI:基于 Vue 的 UI 组件库,提供预制的按钮、表格、弹窗等组件,加速前端页面开发。
  • 微信小程序:微信生态内的轻应用开发框架,无需下载即可运行,面向微信用户提供服务。
  • Apache ECharts:数据可视化库,用于生成折线图、柱状图、饼图等,展示数据分析结果。

网关层(流量入口与分发):

作为用户请求的统一入口,负责路由、负载均衡、安全控制等,隔离用户层与应用层,保障系统稳定性。

  • Nginx:高性能 HTTP 服务器与反向代理工具,可实现:
    • 请求路由(将不同 URL 转发到对应后端服务);
    • 负载均衡(分摊多台服务器的请求压力);
    • 静态资源托管(直接返回图片、CSS 等文件)。

应用层(业务逻辑核心):

负责业务规则处理、流程控制、服务编排,是系统的 “大脑”,协调数据层与用户层的交互。

  • Spring Boot:简化 Spring 应用开发的框架,内置服务器、自动配置等,快速搭建生产级服务。
  • Spring MVC:Spring 的 Web 框架,基于 MVC(模型 - 视图 - 控制器)模式,处理 HTTP 请求、封装响应。
  • Spring Task:Spring 的定时任务工具,用于执行周期性操作(如定时统计、数据同步)。
  • HttpClient:用于发送 HTTP 请求,实现 “服务间远程调用”(如调用第三方 API、其他微服务)。
  • Spring Cache:Spring 的缓存抽象,简化缓存逻辑(如缓存数据库查询结果),提升性能。
  • JWT(JSON Web Token):身份认证与授权标准,用户登录后生成令牌,后续请求通过令牌验证身份(实现 “无状态会话”)。
  • 阿里云 OSS:对象存储服务,用于存储非结构化数据(如图片、视频、文档),实现海量数据的持久化存储。
  • Swagger:API 文档工具,自动生成 RESTful 接口的文档,方便前后端协作与接口测试。
  • POI:操作 Microsoft Office 的工具库,实现 Excel/Word 的 “数据导入导出”(如报表导出为 Excel)。
  • WebSocket:双向通信协议,实现 “服务器 - 客户端实时交互”(如即时聊天、实时数据推送)。

数据层(数据存储与访问):

负责数据的持久化、缓存、高效访问,是系统的 “数据底座”。

  • MySQL:关系型数据库,存储结构化数据(如用户信息、订单表),支持复杂 SQL 查询。
  • Redis:内存数据库,用于:
    • 缓存 “热点数据”(减少 MySQL 查询压力);
    • 实现分布式锁、临时会话存储等。
  • MyBatis:持久层框架,简化 JDBC 操作,通过 “XML / 注解” 将 Java 对象与数据库表映射(实现数据 CRUD)。
  • PageHelper:MyBatis 的分页插件,简化 “数据库查询结果的分页逻辑”。
  • Spring Data Redis:Spring 对 Redis 的封装,提供统一模板类,简化 Redis 操作(如存取值、发布订阅)。

工具:

  • Git:分布式版本控制系统,管理代码版本(如提交、分支、合并),支持多人协作开发。
  • Maven:项目构建与依赖管理工具,统一管理项目依赖(如 jar 包),并完成编译、打包。
  • Junit:单元测试框架,用于编写 “代码单元的自动化测试用例”,验证逻辑正确性。
  • Postman:API 测试工具,用于发送 HTTP 请求,测试后端接口的功能、参数、返回结果。

后端环境搭建

前端项目已经搭建好 直接导入 启动nginx服务 双击nginx.exe即可 注:nginx必须放在没有中文的目录下才能启动

后端工程基于maven进行项目构建 并进行分模块开发

sky-take-out  

maven父工程 统一管理依赖版本 聚合其他子模块

sky-common模块

子模块,存放公共类

包名 作用
constant 封装常量类 代替硬编码
context 封装和项目上下文相关的类
enumeration 封装枚举类
exception 封装自定义异常类
json 处理json转换的类
properties springboot中的配置属性类 将配置文件的配置项封装为对象
result 封装后端返回结果的类
utils 封装工具类

sky-pojo模块

子模块,存放实体类

POJO:普通java对象 只有属性和对应的getter和setter

包名 作用
dto 数据传输对象 通常用于程序中各层之间传递数据
vo 视图对象 为前端展示数据提供的对象
entity 实体 通常和数据库的表对应

sky-server模块

子模块,后端服务,存放配置文件

包名 作用
acpect 存放各种切面
config 存放各种配置类
controller 存放各种处理前端请求的方法,细分为管理端,用户端,通用端
handler 封装和处理异步任务,事件或消息
interceptor 存放拦截器,按照指定条件拦截前端请求
mapper 存放mapper,是Java与MySQL直接进行交互的包
service 存放各种业务功能的具体实现逻辑方法
task 任务类,存放各种任务
websocket 封装websocket,简化websocket的使用

数据库表

表名 作用
address_book 地址簿
category 菜品及套餐分类
dish 菜品表
dish_falvor 菜品口味关系表
employee 员工信息表
order_detail 订单明细表
orders 订单表
setmeal 套餐表
setmeal_dish 菜品套餐关系表
shopping_cart 购物车表
user 用户信息表

项目技术

1 JWT令牌加密技术

JWT(JSON Web Token)是一种用于身份验证和授权的开放标准。

JWT就是将原始的json数据格式进行了安全的封装,这样就可以直接基于jwt在通信双方安全的进行信息传输了。

JWT定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。

JWT 由Header(头部),Payload(载荷),Signature(签名)组成。签名是由于验证令牌的完整性和可信任性。

◆ Header(头),记录令牌类型、签名算法等。例如:{"alg":"HS256","type":"JWT"}

◆ Payload(有效载荷),携带一些自定义信息、默认信息等。例如:{"id":"1","username":"Tom"}

◆ Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。

而JWT令牌加密技术主要体现在签名算法Payload 加密两个维度,保障令牌的完整性(防篡改)保密性(防偷窥)。

JWT令牌加密技术分为对称加密非对称加密两类:

对称加密:使用相同的密钥加密和解密令牌 简单高效

非对称加密:使用密钥对(私钥+公钥) ,安全性更高

JWT技术在本项目的应用为实现资源请求拦截

当我们知道了访问某个业务操作的路径时 直接访问该路径 就会直接跳转到操作界面 这样是不安全的  我们的需求是只有当员工登录了才可以操作访问管理端 。因此当用户登录成功后 我们为已经登录的用户下发JWT令牌 后端拦截除了登录请求之外的所有请求  前端每一次请求都要携带这个JWT令牌 。后端对请求拦截后,要对请求中携带的JWT令牌进行处理,如果可以校验通过就放行请求,如果没有令牌或者解析错误,就返回登录界面,这样保证了 即使我们知道了访问某个业务操作的路径时 直接访问该路径 也不会跳转到对应的业务操作页面 而是直接跳转到登录页面 防止未登录的人对业务进行篡改。

验证登录:

自定义JWT工具包内部:

jwt加密

jwt解密:

2 Nginx反向代理和负载均衡

Nginx是一个轻量级,高性能的HTTP反向代理web服务器,特点是占有内存少,并发能力强。

1 nginx反向代理就是将前端发送的动态请求有nginx转发到后端服务器

正向代理和反向代理:

正向代理替客户端发起请求,反向代理替服务器接收请求

正向代理是客户端的 “中间人”:客户端知道要访问的目标服务器,但因网络限制(如墙、内网权限)无法直接访问,需通过正向代理间接请求。目标服务器只知道代理的 IP,不知道真实客户端的 IP。

典型工具 / 场景 VPN、爬虫代理池、公司内网代理

反向代理是服务器的 “门面”:客户端不知道真实的后端服务器地址,只知道反向代理的地址;所有请求先发送到代理,再由代理转发给后端服务器。后端服务器隐藏在代理后,无需直接暴露在公网。

典型工具 / 场景  Nginx、Apache、CDN(如阿里云 CDN)

2 nginx不仅可以提高访问速度、保证后端服务安全,还有最重要的一点:负载均衡

负载均衡即把大量请求按指定方式均衡分配给集群中的每台服务器

负载均衡策略:

轮询:默认方式,即你一个我一个轮着来,比较平均

weight权重方式 默认为1 权重越高 被分配的客户端请求越多

ip_hash依据ip分配方式,每个访客固定访问一个后端服务

least_conn:依据最少连接方式,把请求优先分配给连接数少的后端url_hash:

url_hash:依据url分配方式,相同的url分配到同一个后端服务

fair:依据响应时间方式,响应时间短的服务优先分配

Nginx的conf配置文件已经配置了负载均衡:

3 MD5加密处理

MD5全名MD5信息摘要算法,一种被广泛使用的密码散列函数,通过MD5加密算法可以将明文的字符串进行加密,且该过程是单向

在员工登录时密码比对前对前端传过来的明文密码加密处理:

4 基于Swagger的Knife4j注解

Swagger 是一套围绕 OpenAPI 规范构建的API 开发工具集,核心作用是自动生成 API 文档、支持在线调试、标准化接口设计。通过代码注解自动生成标准化、可交互的文档。

Knife4j 是一款基于 Swagger增强型 API 文档工具,对Swagger进行封装与增强,对国内开发者更友好。

常用的Knife4j注解:

  • @Api: 用在类上,例如Controller,表示对类的说明
  • @ApiModel  用在类上,例如entity、DTO、VO
  • @ApiModelProperty  用在属性上,描述属性信息
  • @ApiOperation  用在方法上,例如Controller的方法,说明方法的用途、作用

在webMvcConfiguration中配置Knife4j:

在webMvcConfiguration中设置静态资源映射:

配置完成后 启动项目 访问 http://localhost:8080/doc.html#/home 即可访问到接口文档

5 ThreadLocal

ThreadLocal 是 Java 中的一个线程本地变量工具类,核心作用是为每个线程创建独立的变量副本,使得多线程环境下,线程之间的变量互不干扰,从而避免线程安全问题。

核心解决的问题

在多线程场景中,若多个线程共享一个变量,容易出现并发安全问题(如多个线程同时修改同一变量导致数据错乱)。传统解决方案是通过 synchronized 或锁机制控制线程访问顺序,但会降低并发效率。

ThreadLocal 采用另一种思路:不让线程共享变量,而是让每个线程拥有变量的独立副本。线程操作的是自己的副本,无需考虑其他线程的影响,从根本上避免了共享冲突。

工作原理

ThreadLocal 的实现依赖于线程(Thread)内部的一个特殊成员变量 threadLocals(类型为 ThreadLocalMap):

  • ThreadLocalMap 是 ThreadLocal 的静态内部类,本质是一个哈希表,key 是 ThreadLocal 实例本身,value 是当前线程的变量副本
  • 当线程通过 ThreadLocal.set(value) 存值时,实际是向当前线程的 threadLocals 中添加一条记录(key = 当前 ThreadLocal 实例,value = 变量副本)。
  • 当线程通过 ThreadLocal.get() 取值时,是从当前线程的 threadLocals 中,以当前 ThreadLocal 实例为 key,获取对应的 value。
  • 线程结束时,若 threadLocals 被回收,变量副本也会随之释放。

在本项目中的应用

新增员工时 我们需要知道是谁创建和修改了员工 已知Token令牌可以获取当前员工的id  得到这个值 利用ThreadLocal存储从token令牌获取到的员工id 再在需要的地方调用员工id

1 我们使用ThreadLocal时一般都会将其封装成一个工具类 这里将它封装成Basecontext工具类

 2 利用token令牌获取当前员工id 再用ThreadLocal存储该id

3 ThreadLocal调用该变量

这里被注释掉是因为我们在后面的业务中对这种填充字段方法进行了统一处理

6 基于PageHelper的分页查询

PageHelper 是 MyBatis 生态中一款分页插件,核心作用是简化数据库分页查询操作。

工作原理

PageHelper 基于 MyBatis 的拦截器机制实现,核心流程如下:

  1. 当调用 PageHelper.startPage() 时,插件会将分页参数(pageNum、pageSize)存入 ThreadLocal(确保线程安全)。
  2. MyBatis 执行 Mapper 方法时,PageHelper 的拦截器会拦截 SQL 执行过程,从 ThreadLocal 中获取分页参数。
  3. 根据配置的数据库方言(如 MySQL),自动为原始 SQL 添加分页条件(如 SELECT * FROM user → SELECT * FROM user LIMIT 0, 10)。
  4. 同时,插件会自动生成 count 查询 SQL(如 SELECT COUNT(*) FROM (原始SQL)),执行后获取总条数。
  5. 将查询结果包装成 Page 对象(继承自 ArrayList),再通过 PageInfo 计算总页数、上 / 下一页等元数据,并清除ThreadLocal 中的参数。

核心功能:

  1. 自动分页查询只需在执行 Mapper 方法前调用 PageHelper.startPage(pageNum, pageSize) 方法,即可自动拦截后续的 SQL 查询,为其添加数据库适配的分页条件(如 MySQL 的 LIMIT、Oracle 的 ROWNUM 等),返回分页后的结果。

  2. 封装分页信息分页查询的结果会被封装到 Page 对象中(继承自 ArrayList),该对象包含当前页数据、总记录数、总页数、当前页码、每页条数等关键分页信息,方便前端展示分页控件(如页码导航、总条数显示等)。

  3. 支持排序功能可以通过 PageHelper.orderBy("字段名 排序方式") 或在 startPage 中指定排序参数,自动为 SQL 添加排序条件(ORDER BY),无需手动在 SQL 中编写。

  4. 适配多种数据库内置对主流数据库(如 MySQL、Oracle、SQL Server、PostgreSQL 等)的分页语法支持,无需根据不同数据库修改分页逻辑,插件会自动适配。

  5. 灵活的分页参数处理支持对分页参数(如页码、每页条数)进行默认值处理(如页码为 0 或负数时自动调整为 1),避免因参数错误导致的查询异常。

在pom文件中引入PageHelper的maven坐标:

controller层:

service层:

mapper层动态sql:

7 基于消息转换器对时间进行格式化

对时间格式化有2种方法

方法1:在属性上方加注解

@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss") 但只对一个属性有效  建议使用自定义消息转换器

方法2:使用自定义消息转换器

消息转换器(HttpMessageConverter)是 Spring MVC 体系的核心组件,主要负责 HTTP 请求体与 Java 对象Java 对象与 HTTP 响应体 之间的格式转换,比如将前端传的 JSON 转成 Java Bean,或把 Java Bean 转成 JSON/XML 返回给前端。

核心功能

消息转换器的核心作用是解耦 “数据格式处理” 与 “业务逻辑”,避免手动解析请求 / 组装响应的重复工作,主要功能分三类:

  1. 请求体转换(入参):配合 @RequestBody 注解,将前端发送的请求体(如 JSON、XML)自动解析为 Controller 方法的 Java 参数对象(如 UserList<User>)。
  2. 响应体转换(出参):配合 @ResponseBody 或 @RestController 注解,将 Controller 方法返回的 Java 对象(如 String、Java Bean、集合)自动转为指定格式(默认 JSON)的响应体。
  3. 多格式支持:内置对 JSON、XML、字符串、表单数据(x-www-form-urlencoded)等常见格式的支持,也可扩展自定义格式(如 Protobuf、CSV)。

在webMvcConfiguration中自定义消息转换器:

* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]

8 基于注解和AOP的公共字段填充功能

在进行增删改查时 并不是所有操作都需要改createTime /updateTime /createUser/ updateUser 而在需要insert和update时 对以上四个字段的操作是相同的 因此为避免代码冗余 多了一些重复繁琐的操作 我们打算将这些公共字段优化  利用AOP面向切面编程 在用到这些字段的方法时统一拦截 再在 通知 中为其赋值

整体思路为:通过注解的方式标记方法,利用AOP思想创建一个切面,再切面中实现对标记方法中字段的填充 再运行原方法

1 自定义注解AutoFill 用于标识需要进行公共字段自动填充的方法

2 自定义切面类AutoFillAspect 统一拦截加入了AutoFill注解的方法 通过反射为公共字段赋值

/*
*
* 自定义切面 实现公共字段自动填充处理逻辑
* */
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
    //切入点
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut(){
    }

    //前置通知
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint){
        log.info("开始进行公共字段填充");
        //获取注解里的操作类型是update还是insert
        MethodSignature signature =(MethodSignature) joinPoint.getSignature();//方法签名对象
        AutoFill autoFill=signature.getMethod().getAnnotation(AutoFill.class);//获得方法上的注解对象
        OperationType operationType = autoFill.value();//获得数据库操作类型

        //获取当前被拦截的方法的参数 --实体对象
        Object[] args = joinPoint.getArgs();
        if(args==null||args.length==0){
            return;
        }

        Object entity = args[0];

        //准备赋值的数据
        LocalDateTime now = LocalDateTime.now();
        Long currentId = BaseContext.getCurrentId();

        //根据不同操作类型 赋值
        if(operationType==OperationType.INSERT){
            //为4个公共字段赋值
            try {
                //方法名定义成常量类
                Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

                //通过反射为对象属性赋值
                setCreateTime.invoke(entity,now);
                setCreateUser.invoke(entity,currentId);
                setUpdateTime.invoke(entity,now);
                setUpdateUser.invoke(entity,currentId);

            } catch (Exception e) {
                e.printStackTrace();
            }

        }else if(operationType==OperationType.UPDATE){
            //为2个公共字段赋值
            try {
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);

                //通过反射为对象属性赋值
                setUpdateTime.invoke(entity,now);
                setUpdateUser.invoke(entity,currentId);

            } catch (Exception e) {
                e.printStackTrace();
            }


        }
    }

3 在Mapper方法上加入AuroFill注解

9 基于阿里云OSS云存储服务上传文件图片

之所以将菜品套餐图片存储到阿里云而不是本地,是因为前端无法回调服务器的本地图片,造成我们只能存图片 不能回显图片  而调用了阿里云云存储服务后 阿里云会返回一个图片url 我们可以通过这个url回调图片

配置阿里云:

application-dev.yml

application.yml

这里有一个开发习惯 不要把配置类写死 我们并不直接把配置写死在application.yml中  而是将配置写在application-dev.yml中 然后在application.yml中应用application-dev.yml的配置 方便后续可能对配置进行修改 

sky-common

读取阿里云OSS的配置  将以"sky.alioss"为前缀的配置项绑定到AliOssProperties对象中

在sky-common模块里创建AliOssUtil工具类 

@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {

    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;

    /**
     * 文件上传
     *
     * @param bytes
     * @param objectName
     * @return
     */
    public String upload(byte[] bytes, String objectName) {

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        try {
            // 创建PutObject请求。
            ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }

        //文件访问路径规则 https://BucketName.Endpoint/ObjectName
        StringBuilder stringBuilder = new StringBuilder("https://");
        stringBuilder
                .append(bucketName)
                .append(".")
                .append(endpoint)
                .append("/")
                .append(objectName);

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

        return stringBuilder.toString();
    }

因为阿里云OSS是作用于整个server层 所以我们可以在server模块写一个配置类专门用来初始化阿里云OSS工具类 如下:

阿里云OSS工具类的调用

/*
* 通用接口
* */
@RestController
@RequestMapping("/admin/common")
@Api(tags="通用接口")
@Slf4j
public class CommonController {

    @Autowired
    private AliOssUtil aliOssUtil;

    /*
    * 文件上传
    * @param file
    * @return
    * */
    @PostMapping("/upload")
    @ApiOperation("文件上传")
    public Result<String> upload(MultipartFile file){
        log.info("文件上传:{}",file);

        try {
            //原始文件名
            String originalFilename = file.getOriginalFilename();
            //截取原始文件名后缀  jfdjw.png
            String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
            //构造新文件名称
            String objectName = UUID.randomUUID().toString() + extension;
            //文件的请求路径
            String filePath = aliOssUtil.upload(file.getBytes(), objectName);
            return Result.success(filePath);
        } catch (IOException e) {
            log.error("文件上传失败:{}",e);
        }
        return Result.error(MessageConstant.UPLOAD_FAILED);
    }

上述代码使用了UUID生成文件名:

UUID是一种标准的标识符,用于在分布式环境中唯一标识信息。
作用:生成全局唯一的字符串标识符,避免命名冲突,可以防止因同名文件上传导致的覆盖问题

阿里云图片命名不允许重复 否则会覆盖  因此使用UUID生成一串随机数 为阿里云OSS图片命名 防止图片被覆盖

10 开启事务注解

在进行某一个业务操作时 可能需要对多张表增删改 此为了确保数据库操作的一致性和完整性 我们可以将对这几张表的操作设置为一个事务

具体实现方式:

1 在启动类上加@EnableTransactionManagement 表示开启注解方式的事务管理

2 在捆绑为一个事务的方法(涉及多表操作的方法)上加@Transactional注解

@Transactional

核心属性(常用)

@Transactional 提供了丰富的属性,用于自定义事务行为,以下是开发中最常用的几个:

属性名 作用 常用值示例
propagation 事务传播行为(定义多个事务方法嵌套调用时的事务规则) Propagation.REQUIRED(默认)、REQUIRES_NEW
isolation 事务隔离级别(解决并发问题,如脏读、不可重复读、幻读) Isolation.READ_COMMITTED(默认,跟随数据库)
rollbackFor 指定触发事务回滚的异常类型(默认只回滚 RuntimeException 及其子类) rollbackFor = Exception.class(所有异常回滚)
noRollbackFor 指定不触发回滚的异常类型 noRollbackFor = BusinessException.class
readOnly 是否为只读事务(查询操作建议设为 true,优化数据库性能) readOnly = true
timeout 事务超时时间(超过指定时间未完成则自动回滚,单位:秒) timeout = 30(30 秒超时)
关键属性详解:
  1. propagation(传播行为)控制多个事务方法嵌套时的事务关系,最常用的两种:

    • REQUIRED(默认):如果当前有事务,则加入该事务;如果没有,则创建新事务。例:A()调用B(),若 A 有事务,B 则在 A 的事务中执行(A 或 B 失败,整体回滚)。
    • REQUIRES_NEW:无论当前是否有事务,都创建新事务(新事务与原事务独立)。例:A()调用B(),A 和 B 分别在自己的事务中执行(B 失败不影响 A 的提交)。
  2. isolation(隔离级别)解决并发场景下的事务问题,常用级别:

    • READ_UNCOMMITTED:最低级别,允许读取未提交的数据(可能脏读)。
    • READ_COMMITTED:只能读取已提交的数据(避免脏读,大多数数据库默认级别)。
    • REPEATABLE_READ:保证多次读取同一数据结果一致(避免不可重复读,MySQL 默认级别)。
    • SERIALIZABLE:最高级别,完全串行化执行(避免幻读,性能最低)。
  3. rollbackFor(回滚策略)Spring 默认只对 未捕获的 RuntimeException 及其子类 回滚,对 Checked Exception(如 IOException)不回滚。通过 rollbackFor 可指定需要回滚的异常:

    // 遇到任何Exception(包括Checked Exception)都回滚
    @Transactional(rollbackFor = Exception.class)
    public void updateData() throws IOException {
        // 业务操作,若抛IOException,事务回滚
    }
    

11 Redis缓存数据

1 利用Redis实现店铺营业状态

 店铺状态只需要存储一个简单的状态值(营业中/打烊中),Redis 的 key-value 结构非常适合  Redis 是内存数据库,读取速度极快(微秒级),而 MySQL 是磁盘数据库,响应时间相对较慢 店铺状态变化需要快速生效,Redis 的内存存储特性保证了数据的实时性

关于redis的具体操作

Redis的一些简单操作-CSDN博客

在java中操作redis 一般通过Spring Data Redis

导入依赖

在application-dev.yml和application.yml中配置redis

编写配置类

现在就可以使用redis缓存店铺营业状态了

2 redis缓存菜品

菜品和套餐数据都存放在数据库中 当大量用户来点餐 频繁查询数据库可能导致数据库压力过大  性能下降   用户体验感差 而 Redis是高性能的基于键值对的写入缓存的 内存存储系统  因此利用redis缓存商品数据 无疑是最好的选择 ‘

整体思路为:当用户查询菜品时 如果是第一次查询 redis里没有数据 我们从数据库查询菜品数据返回给用户 同时将菜品数据缓存到redis中 当下次用户再次访问菜品时 redis中已有菜品数据 就可以直接访问redis获取菜品数据

当数据库里有菜品变更时要及时清理缓存数据

当数据库菜品增删改、起售、停售时 及时清理缓存数据

将清理缓存数据抽取成一个方法cleanCache 在这些方法里调用清理方法

12 Spring Cache

Spring Cache 是 Spring 框架提供的缓存抽象层,它通过注解驱动的方式简化缓存操作,屏蔽不同缓存中间件(如 Redis、EhCache、Caffeine 等)的底层差异,让开发者无需直接操作缓存 API,就能快速实现数据缓存功能。

Spring Cache提供了一层抽象 底层可以切换不同的缓存实现 如 EHCache ,Caffeine, Redis

核心注解

Spring Cache 提供了 5 个核心注解,覆盖缓存的 “查、更、删” 场景:

注解 作用场景 核心逻辑
@Cacheable 查询方法(读操作) 方法执行前先查缓存,命中则返回缓存数据;未命中则执行方法,将结果写入缓存。
@CachePut 更新方法(写操作) 执行方法后,将结果写入缓存(常用于更新数据后同步缓存)。
@CacheEvict 删除方法(删操作) 执行方法后,删除缓存中对应的数据(常用于删除数据后清理缓存)。
@Caching 复杂缓存操作 组合多个缓存注解(如同时更新和删除缓存)。
@CacheConfig 类级别缓存配置 统一配置类中所有方法的缓存名称(cacheNames)、Key 生成器等,简化代码。

常用注解属性(以 @Cacheable 为例)

属性名 作用 示例
cacheNames 指定缓存名称(必填,可理解为缓存的 “组名”) @Cacheable(cacheNames = "userCache")
key 缓存的 Key(支持 SpEL 表达式,默认是方法参数组合) @Cacheable(key = "#userId")
condition 缓存生效条件(满足条件才缓存,SpEL 表达式) @Cacheable(condition = "#userId > 0")
unless 缓存排除条件(满足条件则不缓存,在方法执行后判断) @Cacheable(unless = "#result == null")
sync 是否启用同步模式(防止缓存击穿,多线程并发查询时仅一个线程查库) @Cacheable(sync = true)

使用步骤

导入坐标

在启动类或配置类上添加 @EnableCaching 注解,开启 Spring Cache 功能

调用相关注解达到缓存套餐的功能

13 HttpClient

HttpClient一个客户端编程工具包

使用HttpClient可以在java中构造请求和发送请求 在后续的微信登录中用来请求微信的某个接口 来实现微信登录

14 微信登录

小程序调用wx.login()获取到一个code(授权码)发送code 给后端服务器

后端服务器接收到code  传入(appid+appsecret+code)通过HttpClient发送http请求给微信接口服务

 微信接口服务返回相应的数据(session_key+openid)给后端服务器 

openid是微信用户的唯一标识  后端拿到openid后我们自定义登录状态 

 将openid存到数据库 同时为微信用户生成一个token令牌 把令牌返回给小程序 小程序之后发起各种业务请求就都会携带令牌 

15 SpringTask

Spring Task是spring框架提供的任务调度工具  定位为一个定时任务框架 作用是定时自动执行某段Java代码  在此处的应用为 处理外卖订单状态 只要是需要定时处理的场景都可以使用SpringTask

通过Spring Task处理外卖订单状态
1 用户下单后未支付 订单一直处于待支付状态 通过定时任务每分钟检查一次(cron表达式)是否存在支付超时订单 存在则修改订单状态为已取消

2 用户收货后管理端未点击完成按钮 订单一直处于派送中状态 通过定时任务每天凌晨一点(cron表达式)检查一次是否存在派送中订单 存在则修改订单状态为已完成

Spring Task使用步骤:

1 导入maven坐标 spring-context(已存在)

2 启动类添加注解@EnableScheduling开启任务调度

3 自定义定时任务类

16 WebSocket

WebSocket是一种网络协议 实现了浏览器和服务器双向数据传输  只需握手一次就可建立持久性的连接   此处应用WebSocket实现来单提醒和用户催单功能

步骤:

1 导入WebSocket的maven坐标

2 导入WebSocket服务端组件WebSocketServer,用于和客户端通信

/**
 * 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);
    }
 
    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, @PathParam("sid") String sid) {
        System.out.println("收到来自客户端:" + sid + "的信息:" + message);
    }
 
    /**
     * 连接关闭调用的方法
     *
     * @param sid
     */
    @OnClose
    public void onClose(@PathParam("sid") String sid) {
        System.out.println("连接断开:" + sid);
        sessionMap.remove(sid);
    }
 
    /**
     * 群发
     *
     * @param message
     */
    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 导入配置类WebSocketConfiguration,注册WebSocket的服务端组件

当客户支付后,调用WebSocket的相关API实现服务端向客户端推送消息

客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报

 约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type, orderld,content

来单提醒:当用户下单并支付成功后添加以下代码(在OrderControllerImpl.java的paySuccess方法中)

客户催单:

在OrderServiceImpl中实现用户催单功能

17 Apache POI导出运营数据报表

Apache POI 是一个处理Miscrosoft Office各种文件格式的开源项目。简单来说就是,我们可以使用POI在Java 程序中对Miscrosoft Office各种文件进行读写操作

一般情况下,POI都是用于操作Excel文件。

在这里我们不使用Apache POI 建表   而是准备一张模板表 使用Apache POI填充数据就好了

//导出运营数据报表
    @Override
    public void exportBusinessData(HttpServletResponse response) {
        //1 查询数据库获取营业数据--查询最近30天数据
        LocalDate dateBegin = LocalDate.now().minusDays(30);
        LocalDate dateEnd = LocalDate.now().minusDays(1);
        //查询概览数据
        BusinessDataVO businessDataVO = workspaceService.getBusinessData(LocalDateTime.of(dateBegin, LocalTime.MIN), LocalDateTime.of(dateEnd, LocalTime.MAX));
 
        //2 通过POI将数据写入excel文件
        InputStream in = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx");
 
        try {
            //基于模板文件创建一个新的excel文件
            XSSFWorkbook excel=new XSSFWorkbook(in);
 
            //获取表格文件的Sheet页
            XSSFSheet sheet = excel.getSheet("Sheet1");
            //填充数据--时间
            sheet.getRow(1).getCell(1).setCellValue("时间:"+dateBegin+"至"+dateEnd);
 
            //获得第4行
            XSSFRow row = sheet.getRow(3);
            row.getCell(2).setCellValue(businessDataVO.getTurnover());
            row.getCell(4).setCellValue(businessDataVO.getOrderCompletionRate());
            row.getCell(6).setCellValue(businessDataVO.getNewUsers());
 
            //获得第5行
            row = sheet.getRow(4);
            row.getCell(2).setCellValue(businessDataVO.getValidOrderCount());
            row.getCell(4).setCellValue(businessDataVO.getUnitPrice());
 
            //填充明细数据
            for (int i = 0; i < 30; i++) {
                LocalDate date = dateBegin.plusDays(i);
                //查询某一天的营业数据
                BusinessDataVO businessData = workspaceService.getBusinessData(LocalDateTime.of(date, LocalTime.MIN), LocalDateTime.of(date, LocalTime.MAX));
 
                row = sheet.getRow(7 + i);
                row.getCell(1).setCellValue(date.toString());
                row.getCell(2).setCellValue(businessData.getTurnover());
                row.getCell(3).setCellValue(businessData.getValidOrderCount());
                row.getCell(4).setCellValue(businessData.getOrderCompletionRate());
                row.getCell(5).setCellValue(businessData.getUnitPrice());
                row.getCell(6).setCellValue(businessData.getNewUsers());
            }
 
            //3 通过输出流将文件下载到客户端浏览器
            ServletOutputStream out=response.getOutputStream();
            excel.write(out);
 
            //关闭资源
            out.close();;
            excel.close();
 
        } catch (IOException e) {
            e.printStackTrace();
        }

拓展知识点

1 @Builer注解快速构造对象

使用了 Lombok 的 @Builder 注解来创建 Category 对象 

2 <foreach>标签

MyBatis框架中的<foreach>标签,遍历集合中的每个元素,生成相应的sql片段,

主要属性有:

collection:指定要遍历的集合对象

item:集合中每个元素的临时变量名

index:集合中每个元素的索引变量名,对于 List 是数字索引,对于 Map 是键名

open:循环开始前添加的字符串(可选),如 "(" 用于 SQL 的括号开头

close:循环结束后添加的字符串(可选),如 ")" 用于 SQL 的括号结尾

separator:每个元素之间的分隔符(可选),如 "," 用于分隔多个值

3 <insert>标签

MyBatis 中的 <insert> 标签,用于定义插入操作的 SQL 映射。

主要属性包括:

id:指定该 SQL 语句的唯一标识符,对应 Mapper 接口中的方法名

parameterType:指定传入参数的类型,可选(MyBatis 会自动推断)

useGeneratedKeys:布尔值,设置是否使用数据库内部生成的主键(如自增主键)

keyProperty:指定将生成的主键值赋给参数对象的哪个属性

keyColumn:指定数据库表中主键列的名称(当列名与属性名不同时使用)

timeout:设置此语句的超时时间(秒)

databaseId:指定数据库厂商 ID,用于多数据库支持

lang:指定使用的脚本语言(如默认的 OGNL)

4 对象属性拷贝 BeanUtils.copyProperties

这是 Spring Framework 提供的工具类,常用于 DTO 与 Entity 对象之间的转换。

原理:BeanUtils.copyProperties(source, target) 会自动匹配两个对象中同名且类型兼容的属性,并将 source 对象的属性值赋给 target 对象

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐