苍穹外卖

zy123
2025-03-21 /  0 评论 /  0 点赞 /  1 阅读 /  11524 字
最近更新于 04-26

苍穹外卖

踩坑总结

项目简介

整体介绍

本项目(苍穹外卖)是专门为餐饮企业(餐厅、饭店)定制的一款软件产品,包括 系统管理后台小程序端应用 两部分。其中系统管理后台主要提供给餐饮企业内部员工使用,可以对餐厅的分类、菜品、套餐、订单、员工等进行管理维护,对餐厅的各类数据进行统计,同时也可进行来单语音播报功能。小程序端主要提供给消费者使用,可以在线浏览菜品、添加购物车、下单、支付、催单等。

image-20221106194424735

1). 管理端功能

员工登录/退出 , 员工信息管理 , 分类管理 , 菜品管理 , 套餐管理 , 菜品口味管理 , 订单管理 ,数据统计,来单提醒。

2). 用户端功能

微信登录 , 收件人地址管理 , 用户历史订单查询 , 菜品规格查询 , 购物车功能 , 下单 , 支付、分类及菜品浏览。

技术选型

image-20221106185646994

1). 用户层

本项目中在构建系统管理后台的前端页面,我们会用到H5、Vue.js、ElementUI、apache echarts(展示图表)等技术。而在构建移动端应用时,我们会使用到微信小程序。

2). 网关层

Nginx是一个服务器,主要用来作为Http服务器,部署静态资源,访问性能高。在Nginx中还有两个比较重要的作用: 反向代理和负载均衡, 在进行项目部署时,要实现Tomcat的负载均衡,就可以通过Nginx来实现。

3). 应用层

SpringBoot: 快速构建Spring项目, 采用 "约定优于配置" 的思想, 简化Spring项目的配置开发。

SpringMVC:SpringMVC是spring框架的一个模块,springmvc和spring无需通过中间整合层进行整合,可以无缝集成。

Spring Task: 由Spring提供的定时任务框架。

httpclient: 主要实现了对http请求的发送。

Spring Cache: 由Spring提供的数据缓存框架

JWT: 用于对应用程序上的用户进行身份验证的标记。

阿里云OSS: 对象存储服务,在项目中主要存储文件,如图片等。

Swagger: 可以自动的帮助开发人员生成接口文档,并对接口进行测试。

POI: 封装了对Excel表格的常用操作。

WebSocket: 一种通信网络协议,使客户端和服务器之间的数据交换更加简单,用于项目的来单、催单功能实现。

4). 数据层

MySQL: 关系型数据库, 本项目的核心业务数据都会采用MySQL进行存储。

Redis: 基于key-value格式存储的内存数据库, 访问速度快, 经常使用它做缓存。

Mybatis: 本项目持久层将会使用Mybatis开发。

pagehelper: 分页插件。

spring data redis: 简化java代码操作Redis的API。

5). 工具

git: 版本控制工具, 在团队协作中, 使用该工具对项目中的代码进行管理。

maven: 项目构建工具。

junit:单元测试工具,开发人员功能实现完毕后,需要通过junit对功能进行单元测试。

postman: 接口测工具,模拟用户发起的各类HTTP请求,获取对应的响应结果。

准备工作

//待完善,最后写一套本地java开发、nginx部署前端;服务器docker部署的方案!!!

前端环境搭建

1.构建和打包前端项目

npm run build

2.将构建文件复制到指定目录

Nginx 默认的静态文件根目录通常是 /usr/share/nginx/html,你可以选择将打包好的静态文件拷贝到该目录

或者使用自定义目录/var/www/my-frontend,并修改 Nginx 配置文件来指向这个目录。

3.配置 Nginx

打开 Nginx 的配置文件,通常位于 /etc/nginx/nginx.conf

以下是一个使用自定义目录 /var/www/my-frontend 作为站点根目录的示例配置:

server {
    listen       80;
    server_name  your-domain.com;  # 如果没有域名可以使用 _ 或 localhost

    root  /var/www/my-frontend;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

4.启动或重启 Nginx

sudo nginx -t    # 检查配置是否正确
sudo systemctl restart nginx    # 重启 Nginx 服务

5.访问前端项目

在浏览器中输入你配置的域名或服务器 IP 地址

后端环境搭建

image-20250410161050451

工程的每个模块作用说明:

序号 名称 说明
1 sky-take-out maven父工程,统一管理依赖版本,聚合其他子模块
2 sky-common 子模块,存放公共类,例如:工具类、常量类、异常类等
3 sky-pojo 子模块,存放实体类、VO、DTO等
4 sky-server 子模块,后端服务,存放配置文件、Controller、Service、Mapper等

分析sky-common模块的每个包的作用:

名称 说明
constant 存放相关常量类
context 存放上下文类
enumeration 项目的枚举类存储
exception 存放自定义异常类
json 处理json转换的类
properties 存放SpringBoot相关的配置属性类
result 返回结果类的封装
utils 常用工具类

分析sky-pojo模块的每个包的作用:

名称 说明
Entity 实体,通常和数据库中的表对应
DTO 数据传输对象,通常用于程序中各层之间传递数据(接收从web来的数据)
VO 视图对象,为前端展示数据提供的对象(响应给web)
POJO 普通Java对象,只有属性和对应的getter和setter

分析sky-server模块的每个包的作用:

名称 说明
config 存放配置类
controller 存放controller类
interceptor 存放拦截器类
mapper 存放mapper接口
service 存放service类
SkyApplication 启动类

数据库初始化

执行sky.sql文件

序号 数据表名 中文名称
1 employee 员工表
2 category 分类表
3 dish 菜品表
4 dish_flavor 菜品口味表
5 setmeal 套餐表
6 setmeal_dish 套餐菜品关系表
7 user 用户表
8 address_book 地址表
9 shopping_cart 购物车表
10 orders 订单表
11 order_detail 订单明细表

Nginx

1.静态资源托管

直接高效地托管前端静态文件(HTML/CSS/JS/图片等)。

server {
    root /var/www/html;
    index index.html;
    location / {
        try_files $uri $uri/ /index.html;  # 支持前端路由(如 React/Vue)
    }
}
  • try_files:按顺序尝试多个文件或路径,直到找到第一个可用的为止。
    • $uri:尝试直接访问请求的路径对应的文件(例如 /css/style.css)。
    • $uri/:尝试将路径视为目录(例如 /blog/ 会查找 /blog/index.html)。
    • /index.html:如果前两者均未找到,最终返回前端入口文件 index.html

2.nginx 反向代理:

反向代理的好处:

  • 提高访问速度

    因为nginx本身可以进行缓存,如果访问的同一接口,并且做了数据缓存,nginx就直接可把数据返回,不需要真正地访问服务端,从而提高访问速度。

  • 保证后端服务安全

    因为一般后台服务地址不会暴露,所以使用浏览器不能直接访问,可以把nginx作为请求访问的入口,请求到达nginx后转发到具体的服务中,从而保证后端服务的安全。

  • 统一入口解决跨域问题(无需后端配置 CORS)。

nginx 反向代理的配置方式:

server{
    listen 80;
    server_name localhost;
    
    location /api/{
        proxy_pass http://localhost:8080/admin/; #反向代理
    }
}

监听80端口号, 然后当我们访问 http://localhost:80/api/../..这样的接口的时候,它会通过 location /api/ {} 这样的反向代理到 http://localhost:8080/admin/上来

3.负载均衡配置(默认是轮询)

将流量分发到多个后端服务器,提升系统吞吐量和容错能力。

upstream webservers{
    server 192.168.100.128:8080;
    server 192.168.100.129:8080;
}
server{
    listen 80;
    server_name localhost;
    
    location /api/{
        proxy_pass http://webservers/admin;#负载均衡
    }
}

完整流程示例

  1. 用户访问:浏览器打开 http://yourdomain.com
  2. Nginx 返回静态文件:返回 index.html 和前端资源。
  3. 前端发起 API 请求:前端代码调用 /api/data
  4. Nginx 代理请求:将 /api/data 转发到 http://backend_server:3000/api/data
  5. 后端响应:处理请求并返回数据,Nginx 将结果传回前端。

windows下更新nginx配置:先cd到nginx安装目录

nginx -t
nginx -s reload 

APIFox

使用APIFox管理、测试接口、导出接口文档...

优势:

1.多格式支持 APIFox 能够导入包括 YApi 格式在内的多种接口文档,同时支持导出为 OpenAPI、Postman Collection、Markdown 和 HTML 等格式,使得接口文档在不同工具间无缝迁移和使用。

2.接口调试与 Mock 内置接口调试工具可以直接发送请求、查看返回结果,同时内置 Mock 服务功能,方便前后端联调和接口数据模拟,提升开发效率。

3.易用性与团队协作 界面直观、操作便捷,支持多人协作,通过分支管理和版本控制,团队成员可以并行开发并进行变更管理,确保接口维护有序。

迭代分支功能:

新建迭代分支,新增的待测试的接口在这里充分测试,没问题之后合并回主分支。

image-20250410180705866

导出接口文档:

推荐导出数据格式为OpenAPI Spec,它是一种通用的 API 描述标准,Postman和APIFox都支持。

image-20250410182110385

Swagger

  1. 使得前后端分离开发更加方便,有利于团队协作

  2. 接口的文档在线自动生成,降低后端开发人员编写接口文档的负担

  3. 功能测试

使用:

1.导入 knife4j 的maven坐标

在pom.xml中添加依赖

<dependency>
   <groupId>com.github.xiaoymin</groupId>
   <artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>

2.在配置类中加入 knife4j 相关配置

WebMvcConfiguration.java

/**
     * 通过knife4j生成接口文档
     * @return
*/
    @Bean
    public Docket docket() {
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("苍穹外卖项目接口文档")
                .version("2.0")
                .description("苍穹外卖项目接口文档")
                .build();
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }

3.设置静态资源映射,否则接口文档页面无法访问

WebMvcConfiguration.java

/**
     * 设置静态资源映射
     * @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}

4.访问测试

接口文档访问路径为 http://ip:port/doc.html ---> http://localhost:8080/doc.html

这是根据后端 Java 代码(通常是注解)自动生成接口文档,访问是通过后端服务的端口,这些文档最终会以静态文件的形式存在于 jar 包内,通常存放在 META-INF/resources/

常用注解

通过注解可以控制生成的接口文档,使接口文档拥有更好的可读性,常用注解如下:

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

EmployeeLoginDTO.java

@Data
@ApiModel(description = "员工登录时传递的数据模型")
public class EmployeeLoginDTO implements Serializable {

    @ApiModelProperty("用户名")
    private String username;

    @ApiModelProperty("密码")
    private String password;

}

image-20240327170247852

EmployeeController.java

@Api(tags = "员工相关接口")
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;
    @Autowired
    private JwtProperties jwtProperties;
    
    /**
     * 登录
     *
     * @param employeeLoginDTO
     * @return
     */
    @PostMapping("/login")
    @ApiOperation(value = "员工登录")
    public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) 		{
        //..............     
	}
}

实战开发

分页查询

传统员工分页查询分析:

image-20221215153413290

采用分页插件PageHelper:

image-20221215170038833

在执行empMapper.list()方法时,就是执行:select * from emp 语句,怎么能够实现分页操作呢?

分页插件帮我们完成了以下操作:

  1. 先获取到要执行的SQL语句:

    select  *  from  emp    
    
  2. 为了实现分页,第一步是获取符合条件的总记录数。分页插件将原始 SQL 查询中的 SELECT * 改成 SELECT count(*)

    select count(*) from emp;
    
  3. 一旦知道了总记录数,分页插件会将 SELECT * 的查询语句进行修改,加入 LIMIT 关键字,限制返回的记录数。

    select * from emp limit ?, ?
    

    第一个参数(?)是 起始位置,通常是 (当前页 - 1) * 每页显示的记录数,即从哪一行开始查询。

    第二个参数(?)是 每页显示的记录数,即返回多少条数据。

  4. 执行分页查询,例如,假设每页显示 10 条记录,你请求第 2 页数据,那么 SQL 语句会变成:

    select * from emp limit 10, 10;
    

使用方法:

当使用 PageHelper 分页插件时,无需在 Mapper 中手动处理分页。只需在 Mapper 中编写常规的列表查询。

  • Service 层,调用 Mapper 方法之前,设置分页参数
  • 调用 Mapper 查询后,自动进行分页,并将结果封装到 PageBean 对象中返回。

1、在pom.xml引入依赖

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.4.2</version>
</dependency>

2、EmpMapper

@Mapper
public interface EmpMapper {
    //获取当前页的结果列表
    @Select("select * from emp")
    public List<Emp> list();
}

3、EmpServiceImpl 当调用 PageHelper.startPage(page, pageSize) 时,PageHelper 插件会拦截随后的 SQL 查询,自动修改查询,加入 LIMIT 子句来实现分页功能。

@Override
public PageBean page(Integer page, Integer pageSize) {
    // 设置分页参数
    PageHelper.startPage(page, pageSize);   //page是页号,不是起始索引
    // 执行分页查询
    List<Emp> empList = empMapper.list(); 
    // 获取分页结果
    Page<Emp> p = (Page<Emp>) empList;   
    //封装PageBean
    PageBean pageBean = new PageBean(p.getTotal(), p.getResult()); 
    return pageBean;
}

4、Controller

@Slf4j
@RestController
@RequestMapping("/emps")
public class EmpController {

    @Autowired
    private EmpService empService;

    //条件分页查询
    @GetMapping
    public Result page(@RequestParam(defaultValue = "1") Integer page,
                       @RequestParam(defaultValue = "10") Integer pageSize) {
        //记录日志
        log.info("分页查询,参数:{},{}", page, pageSize);
        //调用业务层分页查询功能
        PageBean pageBean = empService.page(page, pageSize);
        //响应
        return Result.success(pageBean);
    }
}

条件分页查询

思路分析:

image-20221215180528415

<select id="pageQuery" resultType="com.sky.entity.Employee">
        select * from employee
        <where>
            <if test="name != null and name != ''">
                and name like concat('%',#{name},'%')
            </if>
        </where>
        order by create_time desc
/select>

文件上传

阿里云OSS存储

pom文件中添加如下依赖:

<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.15.1</version>
</dependency>
<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.1</version>
</dependency>
<dependency>
    <groupId>javax.activation</groupId>
    <artifactId>activation</artifactId>
    <version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
    <groupId>org.glassfish.jaxb</groupId>
    <artifactId>jaxb-runtime</artifactId>
    <version>2.3.3</version>
</dependency>

上传文件的工具类

/**
 * 阿里云 OSS 工具类
 */
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();
    }
}

自己搭建FileBrowser存储

public class FileBrowserUtil {
    private String domain;
    private String username;
    private String password;

    /**
     * —— 第一步:登录拿 token ——
     * 调用 /api/login 接口,返回纯 JWT 字符串,或 {"token":"..."} 结构
     */
    public String login() throws IOException {
        String url = domain + "/api/login";
        try (CloseableHttpClient client = HttpClients.createDefault()) {
            HttpPost post = new HttpPost(url);
            post.setHeader("Content-Type", "application/json");

            // 构造登录参数
            JSONObject cred = new JSONObject();
            cred.put("username", username);
            cred.put("password", password);
            post.setEntity(new StringEntity(cred.toString(), StandardCharsets.UTF_8));

            try (CloseableHttpResponse resp = client.execute(post)) {
                int status = resp.getStatusLine().getStatusCode();
                String body = EntityUtils.toString(resp.getEntity(), StandardCharsets.UTF_8).trim();

                if (status >= 200 && status < 300) {
                    // 如果返回 JSON 对象,则解析出 token 字段
                    if (body.startsWith("{") && body.endsWith("}")) {
                        JSONObject obj = JSONObject.parseObject(body);
                        String token = obj.getString("token");
                        log.info("登录成功,token={}", token);
                        return token;
                    }
                    // 否则直接当成原始 JWT 返回
                    log.info("登录成功,token={}", body);
                    return body;
                } else {
                    log.error("Login failed: HTTP {} - {}", status, body);
                    throw new IOException("Login failed: HTTP " + status);
                }
            }
        }
    }

    /**
     * —— 第二步:上传文件 ——
     * POST {domain}/api/resources/{encodedPath}?override=true
     * Header: X-Auth: token
     */
    public String uploadFile(byte[] fileBytes, String fileName) throws IOException {
        String token = login();
        String remotePath = "store/" + fileName;
        String encodedPath = URLEncoder
                .encode(remotePath, StandardCharsets.UTF_8)
                .replace("%2F", "/");

        // 根据后缀猜 MIME 类型
        String mimeType = URLConnection.guessContentTypeFromName(fileName);
        if (mimeType == null) {
            mimeType = "application/octet-stream";
        }

        String uploadUrl = domain + "/api/resources/" + encodedPath + "?override=true";
        try (CloseableHttpClient client = HttpClients.createDefault()) {
            HttpPost post = new HttpPost(uploadUrl);
            post.setHeader("X-Auth", token);
            post.setEntity(new ByteArrayEntity(fileBytes, ContentType.create(mimeType)));

            try (CloseableHttpResponse resp = client.execute(post)) {
                int status = resp.getStatusLine().getStatusCode();
                String respBody = EntityUtils.toString(resp.getEntity(), StandardCharsets.UTF_8);
                if (status < 200 || status >= 300) {
                    log.error("文件上传失败: HTTP {} - {}", status, respBody);
                    throw new IOException("Upload failed: HTTP " + status);
                }
                log.info("文件上传成功,remotePath={}", remotePath);
                return remotePath;
            }
        }
    }

    /**
     * 第三步:生成公开分享链接
     * 模拟浏览器的 POST /api/share/{encodedPath} 请求,body 为 "{}"
     */
    public String createShareLink(String remotePath) throws IOException {
        String token = login();

        // URL encode 并保留斜杠
        String encodedPath = URLEncoder
                .encode(remotePath, StandardCharsets.UTF_8)
                .replace("%2F", "/");

        String shareUrl = domain + "/api/share/" + encodedPath;
        log.info("准备创建分享链接,POST {}", shareUrl);

        try (CloseableHttpClient client = HttpClients.createDefault()) {
            HttpPost post = new HttpPost(shareUrl);
            post.setHeader("Cookie", "auth=" + token);
            post.setHeader("X-Auth", token);
            post.setHeader("Content-Type", "text/plain;charset=UTF-8");
            post.setEntity(new StringEntity("{}", StandardCharsets.UTF_8));

            try (CloseableHttpResponse resp = client.execute(post)) {
                int status = resp.getStatusLine().getStatusCode();
                String body = EntityUtils.toString(resp.getEntity(), StandardCharsets.UTF_8).trim();

                if (status < 200 || status >= 300) {
                    log.error("创建分享失败 HTTP {} - {}", status, body);
                    throw new IOException("Share failed: HTTP " + status);
                }

                // ========= 这里改为先检测是对象还是数组 =========
                JSONObject json;
                if (body.startsWith("[")) {
                    // 如果真返回数组(老版本可能是 [{...}])
                    json = JSONArray.parseArray(body).getJSONObject(0);
                } else if (body.startsWith("{")) {
                    // 当前版本直接返回对象
                    json = JSONObject.parseObject(body);
                } else {
                    throw new IOException("Unexpected share response: " + body);
                }

                String hash = json.getString("hash");
                String publicUrl = domain + "/api/public/dl/" + hash + "/" + remotePath;
                log.info("创建分享链接成功:{}", publicUrl);
                return publicUrl;
            }
        }
    }

    /**
     * —— 整合示例:上传并立即返回分享链接 ——
     */
    public String uploadAndGetUrl(byte[] fileBytes, String fileName) throws IOException {
        String remotePath = uploadFile(fileBytes, fileName);
        return createShareLink(remotePath);
    }
}

数据库密码加密

加密存储确保即使数据库泄露,攻击者也不能轻易获取用户原始密码。

spring security中提供了一个加密类BCryptPasswordEncoder。

它采用哈希算法 SHA-256 +随机盐+密钥对密码进行加密。加密算法是一种可逆的算法,而哈希算法是一种不可逆的算法。

因为有随机盐的存在,所以相同的明文密码经过加密后的密码是不一样的,盐在加密的密码中是有记录的,所以需要对比的时候,springSecurity是可以从中获取到盐的

  • 添加 spring-security-crypto 依赖,无需引入Spring Security 的认证、授权、过滤器链等其它安全组件!
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-crypto</artifactId>
</dependency>
  • 添加配置
@Configuration
public class SecurityConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        // 参数 strength 为工作因子,默认为 10,这里可以根据需要进行调整
        return new BCryptPasswordEncoder(10);
    }
}
  • 用户注册、加密 encode
@Autowired
private PasswordEncoder passwordEncoder;
// 对密码进行加密
String encodedPassword = passwordEncoder.encode(rawPassword);
  • 验证密码 matches
// 使用 matches 方法来对比明文密码和存储的哈希密码
boolean judge= passwordEncoder.matches(rawPassword, user.getPassword());

BaseContext

如何获得当前登录的用户id?

方法:ThreadLocal

ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。

每次请求代表一个线程!!!注:请求可以先经过拦截器,再经过controller=>service=>mapper,都是在一个线程里。而即使同一个用户,先用两次请求/login、 /upload,它们也不处于同一线程中!

public class BaseContext {

    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id) {
        threadLocal.set(id);
    }

    public static Long getCurrentId() {
        return threadLocal.get();
    }

    public static void removeCurrentId() {
        threadLocal.remove();
    }

}

实现方式:登录成功 -> 生成jwt令牌 (claims中存userId)->前端浏览器保存

后续每次请求携带jwt -> 拦截器检查jwt令牌 -> BaseContext.setCurrentId(jwt中取出的userId); ->

BaseContext.getCurrentId(); //service层中获取当前userId

全局异常处理

新增员工时的问题

录入的用户名已存,抛出的异常后没有处理。

法一:每次新增员工前查询一遍数据库,保证无重复username再插入。

  • 法二:插入后系统报“Duplicate entry”再处理。 「乐观策略」减少不必要的查询,只有在冲突时才抛错。

解决方法:定义全局异常处理器

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 捕获业务异常
     * @param ex
     * @return
     */
    @ExceptionHandler
    public Result exceptionHandler(BaseException ex){
        log.error("异常信息:{}", ex.getMessage());
        return Result.error(ex.getMessage());
    }
    @ExceptionHandler
    public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
        //Duplicate entry 'zhangsan' for key 'employee.idx_username'
        String message = ex.getMessage();
        log.info(message);
        if(message.contains("Duplicate entry")){
            String[] split = message.split(" ");
            String username = split[2];
            String msg = username + MessageConstant.ALREADY_EXISTS;
            return Result.error(msg);
        }else{
            return Result.error(MessageConstant.UNKNOWN_ERROR);
        }
    }
}

SQLIntegrityConstraintViolationException用来捕获各种 完整性约束冲突,如唯一/主键约束冲突(向一个设置了 UNIQUE 或者 PRIMARY KEY 的字段重复插入相同的值。)。可以捕捉username重复异常。并以Result.error(msg)返回通用响应。

另外,自定义一个异常BaseException与若干个业务层异常,

public class BaseException extends RuntimeException {
    public BaseException() {
    }
    public BaseException(String msg) {
        super(msg);
    }
}

image-20250418124757147

业务层抛异常:

throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);

这样抛出异常之后可以被全局异常处理器 exceptionHandler(BaseException ex) 捕获。

SpringMVC的消息转换器(处理日期)

Jackson 是一个用于处理 JSON 数据 的流行 Java 库,主要用于:

  1. 序列化:将 Java 对象转换为 JSON 字符串(例如:Java对象 → {"name":"Alice"})。Controller 返回值上带有 @ResponseBody 或者使用 @RestController
  2. 反序列化:将 JSON 字符串解析为 Java 对象(例如:{"name":"Alice"} → Java对象)。方法参数上标注了 @RequestBody

Spring Boot默认集成了Jackson

1). 方式一

在属性上加上注解,对日期进行格式化

image-20221112103501581

但这种方式,需要在每个时间属性上都要加上该注解,使用较麻烦,不能全局处理。

2). 方式二(推荐 )

WebMvcConfiguration中扩展SpringMVC的消息转换器,统一对LocalDateTime、LocalDate、LocalTime进行格式处理

	/**
     * 扩展Spring MVC框架的消息转化器
     * @param converters
     */
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("扩展消息转换器...");
        //创建一个消息转换器对象
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        //需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
        converter.setObjectMapper(new JacksonObjectMapper());
        //将自己的消息转化器加入容器中,确保覆盖默认的 Jackson 行为
        converters.add(0,converter);
    }

JacksonObjectMapper()文件:

//直接复用 Jackson 的核心功能,仅覆盖或扩展特定行为。
public class JacksonObjectMapper extends ObjectMapper {

    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";  //LocalDate
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; //LocalDateTime
//    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss"; //LocalTime

    public JacksonObjectMapper() {
        super();
        //收到未知属性时不报异常
        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

        //反序列化时,属性不存在的兼容处理
        this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

        SimpleModule simpleModule = new SimpleModule()
                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

        //注册功能模块 例如,可以添加自定义序列化器和反序列化器
        this.registerModule(simpleModule);
    }
}

数据库操作代码复用

为提高代码复用率,在 Mapper 层统一定义一个通用的 update 方法,利用 MyBatis 的动态 SQL,根据传入的 Employee 对象中非空字段生成对应的 SET 子句。这样:

  • 启用/禁用员工:只需在业务层调用(如 startOrStop),传入带有 idstatusEmployee 实例,底层自动只更新 status 字段。
  • 更新员工信息:调用(如 updateEmployee)时,可传入包含多个属性的 Employee 实例,自动更新那些非空字段。

Controller 层和 Service 层的方法命名可根据不同业务场景进行区分,底层均复用同一个 update方法

在 EmployeeMapper 接口中声明 update 方法:

	/**
     * 根据主键动态修改属性
     * @param employee
     */
    void update(Employee employee);

在 EmployeeMapper.xml 中编写SQL:

<update id="update" parameterType="Employee">
        update employee
        <set>
            <if test="name != null">name = #{name},</if>
            <if test="username != null">username = #{username},</if>
            <if test="password != null">password = #{password},</if>
            <if test="phone != null">phone = #{phone},</if>
            <if test="sex != null">sex = #{sex},</if>
            <if test="idNumber != null">id_Number = #{idNumber},</if>
            <if test="updateTime != null">update_Time = #{updateTime},</if>
            <if test="updateUser != null">update_User = #{updateUser},</if>
            <if test="status != null">status = #{status},</if>
        </set>
        where id = #{id}
    </update>

操作多表时的规范操作

功能:实现批量删除套餐操作,只能删除'非起售中'的套餐,关联表有套餐表和套餐菜品表。

代码1:

@Transactional
    public void deleteBatch(Long[] ids) {
        for(Long id:ids){
            Setmeal setmeal = setmealMapper.getById(id);
            if(StatusConstant.ENABLE == setmeal.getStatus()){
                //起售中的套餐不能删除
                throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE);
            }
            else{
                Long setmealId=id;
                setmealMapper.deleteById(id);
                setmeal_dishMapper.deleteByDishId(id);
            }
        }
    }

代码2:

@Transactional
    public void deleteBatch(List<Long> ids) {
        ids.forEach(id -> {
            Setmeal setmeal = setmealMapper.getById(id);
            if (StatusConstant.ENABLE == setmeal.getStatus()) {
                //起售中的套餐不能删除
                throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE);
            }
        });

        ids.forEach(setmealId -> {
            //删除套餐表中的数据
            setmealMapper.deleteById(setmealId);
            //删除套餐菜品关系表中的数据
            setmealDishMapper.deleteBySetmealId(setmealId);
        });
    }

代码2更好,因为:

1.把「验证」逻辑和「删除」逻辑分成了两段,职责更单一,读代码的时候一目了然

2.避免不必要的删除操作,第一轮只做 getById 校验,碰到 起售中 马上抛异常,从不执行任何删除 SQL,效率更高。

@Transactional 最典型的场景就是:在同一个业务方法里要执行多条数据库操作(增删改),而且这些操作必须保证“要么都成功、要么都失败” 时,用它来把这些 SQL 语句包裹在同一个事务里,遇到运行时异常就回滚,避免出现“删到一半、中途抛错”导致的数据不一致。也就是说,同时操作多表时,都在方法上加下这个注解!

公共字段自动填充——AOP编程

在数据库操作中,通常需要为某些公共字段(如创建时间、更新时间等)自动赋值。采用AOP:

  1. 统一管理这些字段的赋值逻辑
  2. 避免在业务代码中重复设置
  3. 确保数据一致性
序号 字段名 含义 数据类型 操作类型
1 create_time 创建时间 datetime insert
2 create_user 创建人id bigint insert
3 update_time 修改时间 datetime insert、update
4 update_user 修改人id bigint insert、update

实现步骤:

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

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

客户端 → Service → Mapper接口方法(带@AutoFill) 
    ↓ 切面触发 Before 通知(AutoFillAspect.autoFill)
[1] 读取注解,确定 INSERT/UPDATE  
[2] 从 BaseContext 拿到 currentId  
[3] 反射调用 entity.setXxx()  
    ↓ 切面执行完毕,回到原方法  
Mapper 执行动态 SQL,将已填充的字段写入数据库

3). 在 需要统一填充的Mapper 的方法上加入 AutoFill 注解

**技术点:**枚举、注解、AOP、反射

HttpClient

HttpClient作用:

  • 在Java程序中发送HTTP请求
  • 接收响应数据

HttpClient的maven坐标:

<dependency>
	<groupId>org.apache.httpcomponents</groupId>
	<artifactId>httpclient</artifactId>
	<version>4.5.13</version>
</dependency>

HttpClient的核心API:

  • HttpClient:Http客户端对象类型,使用该类型对象可发起Http请求。
  • HttpClients:可认为是构建器,可创建HttpClient对象。
  • CloseableHttpClient:实现类,实现了HttpClient接口。
  • HttpGet:Get方式请求类型。
  • HttpPost:Post方式请求类型。

HttpClient发送请求步骤:

  • 创建HttpClient对象
  • 创建Http请求对象
  • 调用HttpClient的execute方法发送请求

测试用例

@SpringBootTest
public class HttpClientTest {

    /**
     * 测试通过httpclient发送GET方式的请求
     */
    @Test
    public void testGET() throws Exception{
        //创建httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();

        //创建请求对象
        HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");

        //发送请求,接受响应结果
        CloseableHttpResponse response = httpClient.execute(httpGet);

        //获取服务端返回的状态码
        int statusCode = response.getStatusLine().getStatusCode();
        System.out.println("服务端返回的状态码为:" + statusCode);

        HttpEntity entity = response.getEntity();
        String body = EntityUtils.toString(entity);
        System.out.println("服务端返回的数据为:" + body);

        //关闭资源
        response.close();
        httpClient.close();
    }
    @Test
    public void testPOST() throws Exception{
        // 创建httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();

        //创建请求对象
        HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("username","admin");
        jsonObject.put("password","123456");

        StringEntity entity = new StringEntity(jsonObject.toString());
        //指定请求编码方式
        entity.setContentEncoding("utf-8");
        //数据格式
        entity.setContentType("application/json");
        httpPost.setEntity(entity);

        //发送请求
        CloseableHttpResponse response = httpClient.execute(httpPost);

        //解析返回结果
        int statusCode = response.getStatusLine().getStatusCode();
        System.out.println("响应码为:" + statusCode);

        HttpEntity entity1 = response.getEntity();
        String body = EntityUtils.toString(entity1);
        System.out.println("响应数据为:" + body);

        //关闭资源
        response.close();
        httpClient.close();
    }
}

微信小程序

image-20221204211800753

步骤分析:

  1. 小程序端,调用wx.login()获取code,就是授权码。
  2. 小程序端,调用wx.request()发送请求并携带code,请求开发者服务器(自己编写的后端服务)。
  3. 开发者服务端,通过HttpClient向微信接口服务发送请求,并携带appId+appsecret+code三个参数。
  4. 开发者服务端,接收微信接口服务返回的数据,session_key+opendId等。opendId是微信用户的唯一标识。
  5. 开发者服务端,自定义登录态,生成令牌(token)和openid等数据返回给小程序端,方便后绪请求身份校验。
  6. 小程序端,收到自定义登录态,存储storage。
  7. 小程序端,后绪通过wx.request()发起业务请求时,携带token。
  8. 开发者服务端,收到请求后,通过携带的token,解析当前登录用户的id(无需获取openai,因为token中存了userid可以确认用户身份)。
  9. 开发者服务端,身份校验通过后,继续相关的业务逻辑处理,最终返回业务数据。

缓存功能

用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大。

image-20221208180228667

实现思路

通过Redis来缓存菜品数据,减少数据库查询操作。

image-20221208180818572

经典缓存实现代码

缓存逻辑分析:

  • 每个分类下的菜品保存一份缓存数据
  • 数据库中菜品数据有变更时清理缓存数据
@Autowired
    private RedisTemplate redisTemplate;
	/**
     * 根据分类id查询菜品
     *
     * @param categoryId
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据分类id查询菜品")
    public Result<List<DishVO>> list(Long categoryId) {

        //构造redis中的key,规则:dish_分类id
        String key = "dish_" + categoryId;

        //查询redis中是否存在菜品数据
        List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
        if(list != null && list.size() > 0){
            //如果存在,直接返回,无须查询数据库
            return Result.success(list);
        }
		////////////////////////////////////////////////////////
        Dish dish = new Dish();
        dish.setCategoryId(categoryId);
        dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品

        //如果不存在,查询数据库,将查询到的数据放入redis中
        list = dishService.listWithFlavor(dish);
        ////////////////////////////////////////////////////////
        redisTemplate.opsForValue().set(key, list);

        return Result.success(list);
    }

为了保证数据库Redis中的数据保持一致,修改管理端接口 DishController 的相关方法,加入清理缓存逻辑。

需要改造的方法:

  • 新增菜品
  • 修改菜品
  • 批量删除菜品
  • 起售、停售菜品

清理缓冲方法:

 private void cleanCache(String pattern){
        Set keys = redisTemplate.keys(pattern);
        redisTemplate.delete(keys);
    }

Spring Cache框架实现缓存

Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-cache</artifactId>  		            		       	 <version>2.7.3</version> 
</dependency>

在SpringCache中提供了很多缓存操作的注解,常见的是以下的几个:

注解 说明
@EnableCaching 开启缓存注解功能,通常加在启动类
@Cacheable 方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据();如果没有缓存数据,调用方法并将方法返回值到缓存中
@CachePut 将方法的返回值到缓存中
@CacheEvict 将一条或多条数据从缓存中删除

1)@CachePut 说明:

​ 作用: 将方法返回值,放入缓存

​ value: 缓存的名称, 每个缓存名称下面可以有很多key

​ key: 缓存的key ----------> 支持Spring的表达式语言SPEL语法

valuecacheNames 属性在用法上是等效的。它们都用来指定缓存区的名称

在Redis中并没有直接的“缓存名”概念,而是通过键(key)来访问数据。Spring Cache通过cacheNames属性来模拟不同的“缓存区”,实际上这是通过将这些名称作为键的一部分来实现的。例如,如果你有一个缓存名为 userCache,那么所有相关的缓存条目的键可能以 "userCache::" 开头。

在save方法上加注解@CachePut

@PostMapping
@CachePut(value = "userCache", key = "#user.id")//key的生成:userCache::1
public User save(@RequestBody User user){
     userMapper.insert(user);
     return user;
}

**说明:**key的写法如下

#user.id : #user指的是方法形参的名称, id指的是user的id属性 , 也就是使用user的id属性作为key ;

#result.id : #result代表方法返回值,该表达式 代表以返回对象的id属性作为key ;

2)@Cacheable 说明:

作用: 在方法执行前,spring先查看缓存中是否有指定的key的数据,如果有数据,则直接返回缓存数据,不执行后续sql操作;若没有数据,调用方法并将方法返回值放到缓存中。

在getById上加注解@Cacheable

@GetMapping
@Cacheable(cacheNames = "userCache",key="#id")
public User getById(Long id){
     User user = userMapper.getById(id);
     return user;
}

3)@CacheEvict 说明:

作用: 清理指定缓存

在 delete 方法上加注解@CacheEvict

	@DeleteMapping
    @CacheEvict(cacheNames = "userCache",key = "#id")//删除某个key对应的缓存数据
    public void deleteById(Long id){
        userMapper.deleteById(id);
    }

	@DeleteMapping("/delAll")
    @CacheEvict(cacheNames = "userCache",allEntries = true)//删除userCache下所有的缓存数据
    public void deleteAll(){
        userMapper.deleteAll();
    }

**总结:**新增数据的时候->添加缓存@CachePut ;

查询的时候->判断有无缓存@Cacheable;

删除的时候->删除缓存@CacheEvict。

Spring Cache是经典缓存的上位替代!!!

注意,如果缓存的是套餐分类,即一个套餐分类中含有多个套餐,那么在新增套餐的时候,需要清除相应的套餐分类缓存,因为当你新增一个属于分类 5 的套餐时,原来缓存里那份「分类 5 的列表」已经不再完整──它少了新加的那个套餐。

但是如果缓存的就是套餐本身,新增套餐的时候就可以直接缓存套餐。不要混淆两者!

下单支付

下单

image-20221214200913654image-20221214200959943

表名 含义 说明
orders 订单表 主要存储订单的基本信息(如: 订单号、状态、金额、支付方式、下单用户、收件地址等)
order_detail 订单明细表 主要存储订单详情信息(如: 该订单关联的套餐及菜品的信息)

微信支付

官方文档:https://pay.weixin.qq.com/static/product/product_index.shtml

image-20221214223910840

商户系统调用微信后台:

**JSAPI下单:**商户系统调用该接口在微信支付服务后台生成预支付交易单(对应时序图的第5步)

image-20221214224409174

微信小程序调起支付:

通过JSAPI下单接口获取到发起支付的必要参数prepay_id,然后使用微信支付提供的小程序方法调起小程序支付(对应时序图的第10步)

image-20221214224551220

内网穿透

微信后台会调用到商户系统给推送支付的结果,在这里我们就会遇到一个问题,就是微信后台怎么就能调用到我们这个商户系统呢?因为这个调用过程,其实本质上也是一个HTTP请求。

目前,商户系统它的ip地址就是当前自己电脑的ip地址,只是一个局域网内的ip地址,微信后台无法调用到。

解决:内网穿透。通过cpolar软件可以获得一个临时域名,而这个临时域名是一个公网ip,这样,微信后台就可以请求到商户系统了。

1)下载地址:https://dashboard.cpolar.com/get-started

2). cpolar指定authtoken

复制authtoken:

image-20240806133753849

执行命令:

注意,cd到cpolar.exe所在的目录打开cmd

输入代码:

cpolar.exe authtoken ZmIwMmQzZDYtZDE2ZS00ZGVjLWE2MTUtOGQ0YTdhOWI2M2Q1

3)获取临时域名

cpolar.exe http 8080 

image-20240806135141280

这里的 https://52ac2ecb.r18.cpolar.top 就是与http://localhost:8080对应的临时域名。

原理:

  1. 客户端向 cpolar 的中转节点发起 出站(outbound)连接,完成身份认证(authtoken),并在连接上报出要映射的本地端口,比如 HTTP 的 8080
  2. 中转节点分配一个公网端点,如abcd1234.cpolar.com
  3. 外部用户 访问 http://abcd1234.cpolar.com,落到 cpolar 的中转节点,中转节点 通过先前建立好的持久隧道,把流量转发到你本地运行的客户端。

Spring Task

Spring Task 是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。

**定位:**定时任务框架

**作用:**定时自动执行某段Java代码

1.2 cron表达式

cron表达式其实就是一个字符串,通过cron表达式可以定义任务触发的时间

**构成规则:**分为6或7个域,由空格分隔开,每个域代表一个含义

每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)

image-20240807141614724

cron表达式在线生成器:https://cron.qqe2.com/

1.3 入门案例

1.3.1 Spring Task使用步骤

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

image-20221218193251182

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

3). 自定义定时任务类

2.订单状态定时处理

2.1 需求分析

用户下单后可能存在的情况:

  • 下单后未支付,订单一直处于**“待支付”**状态
  • 用户收货后管理端未点击完成按钮,订单一直处于**“派送中”**状态

对于上面两种情况需要通过定时任务来修改订单状态,具体逻辑为:

  • 通过定时任务每分钟检查一次是否存在支付超时订单(下单后超过15分钟仍未支付则判定为支付超时订单),如果存在则修改订单状态为“已取消”
  • 通过定时任务每天凌晨1点(打烊后)检查一次是否存在“派送中”的订单,如果存在则修改订单状态为“已完成”
@Component
@Slf4j
public class OrderTask {
    /**
     * 处理下单之后未15分组内支付的超时订单
     */
    @Autowired
    private OrderMapper orderMapper;

    @Scheduled(cron = "0 * * * * ? ")
    public void processTimeoutOrder(){
        log.info("定时处理支付超时订单:{}", LocalDateTime.now());
        LocalDateTime time = LocalDateTime.now().plusMinutes(-15);

        // select * from orders where status = 1 and order_time < 当前时间-15分钟
        List<Orders> ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.PENDING_PAYMENT, time);
        if(ordersList != null && ordersList.size() > 0){
            ordersList.forEach(order -> {
                order.setStatus(Orders.CANCELLED);
                order.setCancelReason("支付超时,自动取消");
                order.setCancelTime(LocalDateTime.now());
                orderMapper.update(order);
            });
        }
    }
    @Scheduled(cron = "0 0 1 * * ?")
    public void processDeliveryOrder() {
        log.info("处理派送中订单:{}", new Date());
        // select * from orders where status = 4 and order_time < 当前时间-1小时
        LocalDateTime time = LocalDateTime.now().plusMinutes(-60);
        List<Orders> ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.DELIVERY_IN_PROGRESS, time);
        if (ordersList != null && ordersList.size() > 0) {
            ordersList.forEach(order -> {
                order.setStatus(Orders.COMPLETED);
                orderMapper.update(order);
            });
        }
    }
}

Websocket

WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接, 并进行双向数据传输。

HTTP协议和WebSocket协议对比:

  • HTTP是短连接
  • WebSocket是长连接
  • HTTP通信是单向的,基于请求响应模式
  • WebSocket支持双向通信
  • HTTP和WebSocket底层都是TCP连接

入门案例

实现步骤:

1). 直接使用websocket.html页面作为WebSocket客户端

2). 导入WebSocket的maven坐标

3). 导入WebSocket服务端组件WebSocketServer,用于和客户端通信(比较固定,建立连接、接收消息、关闭连接、发送消息)

4). 导入配置类WebSocketConfiguration,注册WebSocket的服务端组件

它通过Spring的 ServerEndpointExporter 将使用 @ServerEndpoint 注解的类自动注册为WebSocket端点。这样,当应用程序启动时,所有带有 @ServerEndpoint 注解的类就会被Spring容器自动扫描并注册为WebSocket服务器端点,使得它们能够接受和处理WebSocket连接。

5). 导入定时任务类WebSocketTask,定时向客户端推送数据

来单提醒

设计思路:

  • 通过WebSocket实现管理端页面和服务端保持长连接状态
  • 当客户支付后,调用WebSocket的相关API实现服务端向客户端推送消息
  • 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报
  • 约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,content
    • type 为消息类型,1为来单提醒 2为客户催单
    • orderId 为订单id
    • content 为消息内容
© 版权声明
THE END
喜欢就支持一下吧
点赞 0 分享 收藏
评论 抢沙发
取消