2025-03-18 12:46:59 +08:00
|
|
|
|
# 苍穹外卖
|
|
|
|
|
|
2025-04-17 18:55:50 +08:00
|
|
|
|
## 踩坑总结
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-04-13 14:09:55 +08:00
|
|
|
|
## 项目简介
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
2025-04-13 14:09:55 +08:00
|
|
|
|
### 整体介绍
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
2025-04-13 14:09:55 +08:00
|
|
|
|
本项目(苍穹外卖)是专门为餐饮企业(餐厅、饭店)定制的一款软件产品,包括 **系统管理后台** 和 **小程序端应用** 两部分。其中系统管理后台主要提供给餐饮企业内部员工使用,可以对餐厅的分类、菜品、套餐、订单、员工等进行管理维护,对餐厅的各类数据进行统计,同时也可进行来单语音播报功能。小程序端主要提供给消费者使用,可以在线浏览菜品、添加购物车、下单、支付、催单等。
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
**1). 管理端功能**
|
|
|
|
|
|
|
|
|
|
员工登录/退出 , 员工信息管理 , 分类管理 , 菜品管理 , 套餐管理 , 菜品口味管理 , 订单管理 ,数据统计,来单提醒。
|
|
|
|
|
|
|
|
|
|
**2). 用户端功能**
|
|
|
|
|
|
|
|
|
|
微信登录 , 收件人地址管理 , 用户历史订单查询 , 菜品规格查询 , 购物车功能 , 下单 , 支付、分类及菜品浏览。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 技术选型
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
**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.构建和打包前端项目
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
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` 作为站点根目录的示例配置:
|
|
|
|
|
|
|
|
|
|
```nginx
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo nginx -t # 检查配置是否正确
|
|
|
|
|
sudo systemctl restart nginx # 重启 Nginx 服务
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
5.访问前端项目
|
|
|
|
|
|
|
|
|
|
在浏览器中输入你配置的域名或服务器 IP 地址
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### 后端环境搭建
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
工程的每个模块作用说明:
|
|
|
|
|
|
|
|
|
|
| **序号** | **名称** | **说明** |
|
|
|
|
|
| -------- | ------------ | ------------------------------------------------------------ |
|
|
|
|
|
| 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 | 常用工具类 |
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
分析sky-pojo模块的每个包的作用:
|
|
|
|
|
|
|
|
|
|
| **名称** | **说明** |
|
|
|
|
|
| -------- | ------------------------------------------------------------ |
|
|
|
|
|
| Entity | 实体,通常和数据库中的表对应 |
|
|
|
|
|
| DTO | 数据传输对象,通常用于程序中各层之间传递数据(接收从web来的数据) |
|
|
|
|
|
| VO | 视图对象,为前端展示数据提供的对象(响应给web) |
|
|
|
|
|
| POJO | 普通Java对象,只有属性和对应的getter和setter |
|
|
|
|
|
|
2025-04-13 14:09:55 +08:00
|
|
|
|
分析sky-server模块的每个包的作用:
|
|
|
|
|
|
|
|
|
|
| 名称 | 说明 |
|
|
|
|
|
| -------------- | ---------------- |
|
|
|
|
|
| config | 存放配置类 |
|
|
|
|
|
| controller | 存放controller类 |
|
|
|
|
|
| interceptor | 存放拦截器类 |
|
|
|
|
|
| mapper | 存放mapper接口 |
|
|
|
|
|
| service | 存放service类 |
|
|
|
|
|
| SkyApplication | 启动类 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### 数据库初始化
|
|
|
|
|
|
|
|
|
|
执行sky.sql文件
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
| 序号 | 数据表名 | 中文名称 |
|
|
|
|
|
| ---- | ------------- | -------------- |
|
|
|
|
|
| 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 | 订单明细表 |
|
|
|
|
|
|
2025-04-02 18:28:46 +08:00
|
|
|
|
|
|
|
|
|
|
2025-04-13 14:09:55 +08:00
|
|
|
|
#### Nginx
|
2025-03-26 18:16:04 +08:00
|
|
|
|
|
2025-04-13 14:09:55 +08:00
|
|
|
|
**1.静态资源托管**
|
2025-03-26 18:16:04 +08:00
|
|
|
|
|
2025-04-13 14:09:55 +08:00
|
|
|
|
直接高效地托管前端静态文件(HTML/CSS/JS/图片等)。
|
2025-03-26 18:16:04 +08:00
|
|
|
|
|
2025-04-13 14:09:55 +08:00
|
|
|
|
```nginx
|
|
|
|
|
server {
|
|
|
|
|
root /var/www/html;
|
|
|
|
|
index index.html;
|
|
|
|
|
location / {
|
|
|
|
|
try_files $uri $uri/ /index.html; # 支持前端路由(如 React/Vue)
|
|
|
|
|
}
|
2025-03-26 18:16:04 +08:00
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2025-04-13 14:09:55 +08:00
|
|
|
|
- **`try_files`**:按顺序尝试多个文件或路径,直到找到第一个可用的为止。
|
|
|
|
|
- `$uri`:尝试直接访问请求的路径对应的文件(例如 `/css/style.css`)。
|
|
|
|
|
- `$uri/`:尝试将路径视为目录(例如 `/blog/` 会查找 `/blog/index.html`)。
|
|
|
|
|
- `/index.html`:如果前两者均未找到,最终返回前端入口文件 `index.html`。
|
2025-03-26 18:16:04 +08:00
|
|
|
|
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
|
2025-04-13 14:09:55 +08:00
|
|
|
|
**2.nginx 反向代理:**
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
2025-04-13 14:09:55 +08:00
|
|
|
|
**反向代理的好处:**
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
2025-04-13 14:09:55 +08:00
|
|
|
|
- 提高访问速度
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
2025-04-13 14:09:55 +08:00
|
|
|
|
因为nginx本身可以进行缓存,如果访问的同一接口,并且做了数据缓存,nginx就直接可把数据返回,不需要真正地访问服务端,从而提高访问速度。
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
- 保证后端服务安全
|
|
|
|
|
|
|
|
|
|
因为一般后台服务地址不会暴露,所以使用浏览器不能直接访问,可以把nginx作为请求访问的入口,请求到达nginx后转发到具体的服务中,从而保证后端服务的安全。
|
2025-04-13 14:09:55 +08:00
|
|
|
|
|
|
|
|
|
- 统一入口解决跨域问题(无需后端配置 CORS)。
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
2025-04-13 14:09:55 +08:00
|
|
|
|
**nginx 反向代理的配置方式:**
|
|
|
|
|
|
|
|
|
|
```nginx
|
2025-03-18 12:46:59 +08:00
|
|
|
|
server{
|
|
|
|
|
listen 80;
|
|
|
|
|
server_name localhost;
|
|
|
|
|
|
|
|
|
|
location /api/{
|
|
|
|
|
proxy_pass http://localhost:8080/admin/; #反向代理
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2025-04-13 14:09:55 +08:00
|
|
|
|
监听80端口号, 然后当我们访问 http://localhost:80/api/../..这样的接口的时候,它会通过 location /api/ {} 这样的反向代理到 http://localhost:8080/admin/上来。
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
|
2025-04-13 14:09:55 +08:00
|
|
|
|
|
|
|
|
|
**3.负载均衡配置**(默认是轮询)
|
|
|
|
|
|
|
|
|
|
将流量分发到多个后端服务器,提升系统吞吐量和容错能力。
|
|
|
|
|
|
|
|
|
|
```nginx
|
2025-03-18 12:46:59 +08:00
|
|
|
|
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;#负载均衡
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-04-13 14:09:55 +08:00
|
|
|
|
**完整流程示例**
|
|
|
|
|
|
|
|
|
|
1. **用户访问**:浏览器打开 `http://yourdomain.com`。
|
|
|
|
|
2. **Nginx 返回静态文件**:返回 `index.html` 和前端资源。
|
|
|
|
|
3. **前端发起 API 请求**:前端代码调用 `/api/data`。
|
|
|
|
|
4. **Nginx 代理请求**:将 `/api/data` 转发到 `http://backend_server:3000/api/data`。
|
|
|
|
|
5. **后端响应**:处理请求并返回数据,Nginx 将结果传回前端。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-04-15 12:46:07 +08:00
|
|
|
|
跨域问题:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-04-13 14:09:55 +08:00
|
|
|
|
#### APIFox
|
|
|
|
|
|
|
|
|
|
使用APIFox管理、测试接口、导出接口文档...
|
|
|
|
|
|
|
|
|
|
**优势:**
|
|
|
|
|
|
|
|
|
|
1.多格式支持
|
|
|
|
|
APIFox 能够导入包括 YApi 格式在内的多种接口文档,同时支持导出为 OpenAPI、Postman Collection、Markdown 和 HTML 等格式,使得接口文档在不同工具间无缝迁移和使用。
|
|
|
|
|
|
|
|
|
|
2.接口调试与 Mock
|
|
|
|
|
内置接口调试工具可以直接发送请求、查看返回结果,同时内置 Mock 服务功能,方便前后端联调和接口数据模拟,提升开发效率。
|
|
|
|
|
|
|
|
|
|
3.易用性与团队协作
|
|
|
|
|
界面直观、操作便捷,支持多人协作,通过分支管理和版本控制,团队成员可以并行开发并进行变更管理,确保接口维护有序。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**迭代分支功能:**
|
|
|
|
|
|
|
|
|
|
新建迭代分支,新增的待测试的接口在这里充分测试,没问题之后合并回主分支。
|
|
|
|
|
|
|
|
|
|
<img src="https://pic.bitday.top/i/2025/04/10/tvvptv-0.png" alt="image-20250410180705866" style="zoom:67%;" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**导出接口文档:**
|
|
|
|
|
|
|
|
|
|
推荐导出数据格式为OpenAPI Spec,它是一种通用的 API 描述标准,Postman和APIFox都支持。
|
|
|
|
|
|
|
|
|
|
<img src="https://pic.bitday.top/i/2025/04/10/u48k7q-0.png" alt="image-20250410182110385" style="zoom:67%;" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### Swagger
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
1. 使得前后端分离开发更加方便,有利于团队协作
|
|
|
|
|
|
|
|
|
|
2. 接口的文档在线自动生成,降低后端开发人员编写接口文档的负担
|
|
|
|
|
|
|
|
|
|
3. 功能测试
|
|
|
|
|
|
|
|
|
|
**使用:**
|
|
|
|
|
|
2025-04-15 12:46:07 +08:00
|
|
|
|
**1.导入 knife4j 的maven坐标**
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
在pom.xml中添加依赖
|
|
|
|
|
|
2025-03-26 18:16:04 +08:00
|
|
|
|
```java
|
2025-03-18 12:46:59 +08:00
|
|
|
|
<dependency>
|
|
|
|
|
<groupId>com.github.xiaoymin</groupId>
|
|
|
|
|
<artifactId>knife4j-spring-boot-starter</artifactId>
|
|
|
|
|
</dependency>
|
|
|
|
|
```
|
|
|
|
|
|
2025-04-15 12:46:07 +08:00
|
|
|
|
**2.在配置类中加入 knife4j 相关配置**
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
WebMvcConfiguration.java
|
|
|
|
|
|
|
|
|
|
```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;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2025-04-15 12:46:07 +08:00
|
|
|
|
**3.设置静态资源映射,否则接口文档页面无法访问**
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
WebMvcConfiguration.java
|
|
|
|
|
|
|
|
|
|
```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/");
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2025-04-15 12:46:07 +08:00
|
|
|
|
**4.访问测试**
|
|
|
|
|
|
|
|
|
|
接口文档访问路径为 http://ip:port/doc.html ---> http://localhost:8080/doc.html
|
|
|
|
|
|
|
|
|
|
这是根据后端 Java 代码(通常是注解)自动生成接口文档,访问是通过**后端服务的端口**,这些文档最终会以静态文件的形式存在于 jar 包内,通常存放在 `META-INF/resources/`
|
|
|
|
|
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**常用注解**
|
|
|
|
|
|
|
|
|
|
通过注解可以控制生成的接口文档,使接口文档拥有更好的可读性,常用注解如下:
|
|
|
|
|
|
|
|
|
|
| **注解** | **说明** |
|
|
|
|
|
| ----------------- | ---------------------------------------------------------- |
|
|
|
|
|
| @Api | 用在类上,例如Controller,表示对类的说明 |
|
|
|
|
|
| @ApiModel | 用在类上,例如entity、DTO、VO |
|
|
|
|
|
| @ApiModelProperty | 用在属性上,描述属性信息 |
|
|
|
|
|
| @ApiOperation | 用在**方法**上,例如Controller的方法,说明方法的用途、作用 |
|
|
|
|
|
|
|
|
|
|
EmployeeLoginDTO.java
|
|
|
|
|
|
2025-03-26 18:16:04 +08:00
|
|
|
|
```java
|
2025-03-18 12:46:59 +08:00
|
|
|
|
@Data
|
|
|
|
|
@ApiModel(description = "员工登录时传递的数据模型")
|
|
|
|
|
public class EmployeeLoginDTO implements Serializable {
|
|
|
|
|
|
|
|
|
|
@ApiModelProperty("用户名")
|
|
|
|
|
private String username;
|
|
|
|
|
|
|
|
|
|
@ApiModelProperty("密码")
|
|
|
|
|
private String password;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2025-03-19 18:31:37 +08:00
|
|
|
|

|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
2025-04-15 12:46:07 +08:00
|
|
|
|
EmployeeController.java
|
|
|
|
|
|
|
|
|
|
```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) {
|
|
|
|
|
//..............
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-04-16 18:02:24 +08:00
|
|
|
|
## 实战开发
|
|
|
|
|
|
|
|
|
|
### 分页查询
|
|
|
|
|
|
|
|
|
|
传统员工分页查询分析:
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
**采用分页插件PageHelper:**
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
**在执行empMapper.list()方法时,就是执行:select * from emp 语句,怎么能够实现分页操作呢?**
|
|
|
|
|
|
|
|
|
|
分页插件帮我们完成了以下操作:
|
|
|
|
|
|
|
|
|
|
1. 先获取到要执行的SQL语句:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
select * from emp
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
2. 为了实现分页,第一步是获取符合条件的总记录数。分页插件将原始 SQL 查询中的 `SELECT *` 改成 `SELECT count(*)`
|
|
|
|
|
|
|
|
|
|
```mysql
|
|
|
|
|
select count(*) from emp;
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
3. 一旦知道了总记录数,分页插件会将 `SELECT *` 的查询语句进行修改,加入 `LIMIT` 关键字,限制返回的记录数。
|
|
|
|
|
|
|
|
|
|
```mysql
|
|
|
|
|
select * from emp limit ?, ?
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
第一个参数(`?`)是 起始位置,通常是 `(当前页 - 1) * 每页显示的记录数`,即从哪一行开始查询。
|
|
|
|
|
|
|
|
|
|
第二个参数(`?`)是 每页显示的记录数,即返回多少条数据。
|
|
|
|
|
|
|
|
|
|
4. 执行分页查询,例如,假设每页显示 10 条记录,你请求第 2 页数据,那么 SQL 语句会变成:
|
|
|
|
|
|
|
|
|
|
```mysql
|
|
|
|
|
select * from emp limit 10, 10;
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**使用方法:**
|
|
|
|
|
|
|
|
|
|
当使用 **PageHelper** 分页插件时,无需在 Mapper 中手动处理分页。只需在 Mapper 中编写常规的列表查询。
|
|
|
|
|
|
|
|
|
|
- 在 **Service 层**,调用 Mapper 方法之前,**设置分页参数**。
|
|
|
|
|
- 调用 Mapper 查询后,**自动进行分页**,并将结果封装到 `PageBean` 对象中返回。
|
|
|
|
|
|
|
|
|
|
1、在pom.xml引入依赖
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
<dependency>
|
|
|
|
|
<groupId>com.github.pagehelper</groupId>
|
|
|
|
|
<artifactId>pagehelper-spring-boot-starter</artifactId>
|
|
|
|
|
<version>1.4.2</version>
|
|
|
|
|
</dependency>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
2、EmpMapper
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
@Mapper
|
|
|
|
|
public interface EmpMapper {
|
|
|
|
|
//获取当前页的结果列表
|
|
|
|
|
@Select("select * from emp")
|
|
|
|
|
public List<Emp> list();
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
3、EmpServiceImpl
|
|
|
|
|
当调用 `PageHelper.startPage(page, pageSize)` 时,PageHelper 插件会拦截随后的 SQL 查询,自动修改查询,加入 `LIMIT` 子句来实现分页功能。
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
@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
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
@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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 条件分页查询
|
|
|
|
|
|
|
|
|
|
思路分析:
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
<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文件中添加如下依赖:
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
<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>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
上传文件的工具类
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
/**
|
|
|
|
|
* 阿里云 OSS 工具类
|
|
|
|
|
*/
|
2025-04-17 18:55:50 +08:00
|
|
|
|
public class AliOssUtil {
|
2025-04-16 18:02:24 +08:00
|
|
|
|
|
2025-04-17 18:55:50 +08:00
|
|
|
|
private String endpoint;
|
|
|
|
|
private String accessKeyId;
|
|
|
|
|
private String accessKeySecret;
|
|
|
|
|
private String bucketName;
|
2025-04-16 18:02:24 +08:00
|
|
|
|
|
|
|
|
|
/**
|
2025-04-17 18:55:50 +08:00
|
|
|
|
* 文件上传
|
|
|
|
|
*
|
|
|
|
|
* @param bytes
|
|
|
|
|
* @param objectName
|
|
|
|
|
* @return
|
2025-04-16 18:02:24 +08:00
|
|
|
|
*/
|
2025-04-17 18:55:50 +08:00
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-04-16 18:02:24 +08:00
|
|
|
|
|
2025-04-17 18:55:50 +08:00
|
|
|
|
//文件访问路径规则 https://BucketName.Endpoint/ObjectName
|
|
|
|
|
StringBuilder stringBuilder = new StringBuilder("https://");
|
|
|
|
|
stringBuilder
|
|
|
|
|
.append(bucketName)
|
|
|
|
|
.append(".")
|
|
|
|
|
.append(endpoint)
|
|
|
|
|
.append("/")
|
|
|
|
|
.append(objectName);
|
2025-04-16 18:02:24 +08:00
|
|
|
|
|
2025-04-17 18:55:50 +08:00
|
|
|
|
log.info("文件上传到:{}", stringBuilder.toString());
|
2025-04-16 18:02:24 +08:00
|
|
|
|
|
2025-04-17 18:55:50 +08:00
|
|
|
|
return stringBuilder.toString();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
2025-04-16 18:02:24 +08:00
|
|
|
|
|
|
|
|
|
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
### 加密算法
|
|
|
|
|
|
2025-04-15 12:46:07 +08:00
|
|
|
|
加密存储确保即使数据库泄露,攻击者也不能轻易获取用户原始密码。
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
spring security中提供了一个加密类BCryptPasswordEncoder。
|
|
|
|
|
|
|
|
|
|
它采用[哈希算法](https://so.csdn.net/so/search?q=哈希算法&spm=1001.2101.3001.7020) SHA-256 +随机盐+密钥对密码进行加密。加密算法是一种**可逆**的算法,而哈希算法是一种**不可逆**的算法。
|
|
|
|
|
|
2025-04-15 12:46:07 +08:00
|
|
|
|
因为有随机盐的存在,所以**相同的明文密码**经过加密后的密码是**不一样**的,盐在加密的密码中是有记录的,所以需要对比的时候,springSecurity是可以从中获取到盐的
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-04-15 12:46:07 +08:00
|
|
|
|
- 添加 spring-security-crypto 依赖,无需引入Spring Security 的认证、授权、过滤器链等其它安全组件!
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
2025-04-15 12:46:07 +08:00
|
|
|
|
```xml
|
2025-03-18 12:46:59 +08:00
|
|
|
|
<dependency>
|
2025-04-15 12:46:07 +08:00
|
|
|
|
<groupId>org.springframework.security</groupId>
|
|
|
|
|
<artifactId>spring-security-crypto</artifactId>
|
|
|
|
|
</dependency>
|
2025-03-18 12:46:59 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- 添加配置
|
|
|
|
|
|
2025-03-26 18:16:04 +08:00
|
|
|
|
```java
|
2025-03-18 12:46:59 +08:00
|
|
|
|
@Configuration
|
2025-04-15 12:46:07 +08:00
|
|
|
|
public class SecurityConfig {
|
2025-03-18 12:46:59 +08:00
|
|
|
|
@Bean
|
2025-04-15 12:46:07 +08:00
|
|
|
|
public PasswordEncoder passwordEncoder() {
|
|
|
|
|
// 参数 strength 为工作因子,默认为 10,这里可以根据需要进行调整
|
|
|
|
|
return new BCryptPasswordEncoder(10);
|
2025-03-18 12:46:59 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2025-04-15 12:46:07 +08:00
|
|
|
|
- 用户注册、加密 **encode**
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
2025-03-26 18:16:04 +08:00
|
|
|
|
```java
|
2025-03-18 12:46:59 +08:00
|
|
|
|
@Autowired
|
2025-04-15 12:46:07 +08:00
|
|
|
|
private PasswordEncoder passwordEncoder;
|
|
|
|
|
// 对密码进行加密
|
|
|
|
|
String encodedPassword = passwordEncoder.encode(rawPassword);
|
|
|
|
|
```
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
2025-04-15 12:46:07 +08:00
|
|
|
|
- 验证密码 **matches**
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
2025-04-15 12:46:07 +08:00
|
|
|
|
```java
|
|
|
|
|
// 使用 matches 方法来对比明文密码和存储的哈希密码
|
|
|
|
|
boolean judge= passwordEncoder.matches(rawPassword, user.getPassword());
|
2025-03-18 12:46:59 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 新增员工的两个问题
|
|
|
|
|
|
|
|
|
|
**问题1:**
|
|
|
|
|
|
|
|
|
|
录入的用户名已存,抛出的异常后没有处理。
|
|
|
|
|
|
|
|
|
|
法一:每次新增员工前查询一遍数据库,保证无重复username再插入。
|
|
|
|
|
|
|
|
|
|
法二:插入后系统报“Duplicate entry”再处理。
|
|
|
|
|
|
|
|
|
|
**推荐法二,因为发生异常的概率是很小的,每次新增前查询一遍数据库不划算。**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**问题2:**
|
|
|
|
|
|
2025-04-16 18:02:24 +08:00
|
|
|
|
如何获得当前登录的用户id?
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
方法:ThreadLocal
|
|
|
|
|
|
|
|
|
|
ThreadLocal为**每个线程**提供**单独**一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
|
|
|
|
|
|
|
|
|
|
**每次请求代表一个线程**!!!注:请求可以先经过拦截器,再经过controller=>service=>mapper,都是在一个线程里。
|
|
|
|
|
|
2025-03-26 18:16:04 +08:00
|
|
|
|
```java
|
2025-03-18 12:46:59 +08:00
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2025-04-16 18:02:24 +08:00
|
|
|
|
实现方式:登录成功 -> 生成jwt令牌 (claims中存userId)->前端浏览器保存
|
|
|
|
|
|
|
|
|
|
后续每次请求携带jwt -> 拦截器检查jwt令牌 -> BaseContext.setCurrentId(jwt中取出的userId); ->
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
2025-04-16 18:02:24 +08:00
|
|
|
|
BaseContext.getCurrentId(); //service层中获取当前userId
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### SpringMVC的消息转换器(处理日期)
|
|
|
|
|
|
2025-04-16 18:02:24 +08:00
|
|
|
|
**Jackson** 是一个用于处理 **JSON 数据** 的流行 Java 库,主要用于:
|
|
|
|
|
|
|
|
|
|
1. **序列化**:将 Java 对象转换为 JSON 字符串(例如:`Java对象 → {"name":"Alice"}`)。
|
|
|
|
|
2. **反序列化**:将 JSON 字符串解析为 Java 对象(例如:`{"name":"Alice"} → Java对象`)。
|
|
|
|
|
|
|
|
|
|
**Spring Boot**默认集成了Jackson
|
|
|
|
|
|
2025-03-18 12:46:59 +08:00
|
|
|
|
**1). 方式一**
|
|
|
|
|
|
|
|
|
|
在属性上加上注解,对日期进行格式化
|
|
|
|
|
|
2025-03-19 18:31:37 +08:00
|
|
|
|
<img src="https://pic.bitday.top/i/2025/03/19/u7y008-2.png" alt="image-20221112103501581" style="zoom:67%;" />
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
但这种方式,需要在每个时间属性上都要加上该注解,使用较麻烦,不能全局处理。
|
|
|
|
|
|
|
|
|
|
**2). 方式二(推荐 )**
|
|
|
|
|
|
|
|
|
|
在**WebMvcConfiguration**中扩展SpringMVC的消息转换器,统一对日期类型进行格式处理
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
/**
|
|
|
|
|
* 扩展Spring MVC框架的消息转化器
|
|
|
|
|
* @param converters
|
|
|
|
|
*/
|
|
|
|
|
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
|
|
|
|
|
log.info("扩展消息转换器...");
|
|
|
|
|
//创建一个消息转换器对象
|
|
|
|
|
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
|
|
|
|
|
//需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
|
|
|
|
|
converter.setObjectMapper(new JacksonObjectMapper());
|
2025-04-16 18:02:24 +08:00
|
|
|
|
//将自己的消息转化器加入容器中,确保覆盖默认的 Jackson 行为
|
2025-03-18 12:46:59 +08:00
|
|
|
|
converters.add(0,converter);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
JacksonObjectMapper()文件:
|
|
|
|
|
|
2025-03-26 18:16:04 +08:00
|
|
|
|
```java
|
2025-04-16 18:02:24 +08:00
|
|
|
|
//直接复用 Jackson 的核心功能,仅覆盖或扩展特定行为。
|
2025-03-18 12:46:59 +08:00
|
|
|
|
public class JacksonObjectMapper extends ObjectMapper {
|
|
|
|
|
|
2025-04-16 18:02:24 +08:00
|
|
|
|
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
|
2025-03-18 12:46:59 +08:00
|
|
|
|
// public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
|
2025-04-16 18:02:24 +08:00
|
|
|
|
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss"; //LocalTime
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 修改员工信息(复用update方法)
|
|
|
|
|
|
|
|
|
|
代码能复用尽量复用!在mapper类里定义一个**通用的update**接口,即mybatis操作数据库时修改员工信息都调用这个接口。**启用/禁用员工**可能只要修改status,**修改员工**可能大面积修改属性,在**mapper**类中定义一个通用的update方法,但是**controller层和service层**的函数命名可以不一样,以区分两种业务。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
在 EmployeeMapper 接口中声明 update 方法:
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
/**
|
|
|
|
|
* 根据主键动态修改属性
|
|
|
|
|
* @param employee
|
|
|
|
|
*/
|
|
|
|
|
void update(Employee employee);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
在 EmployeeMapper.xml 中编写SQL:
|
|
|
|
|
|
2025-03-26 18:16:04 +08:00
|
|
|
|
```java
|
2025-03-18 12:46:59 +08:00
|
|
|
|
<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>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 公共字段自动填充——AOP编程
|
|
|
|
|
|
2025-04-16 18:02:24 +08:00
|
|
|
|
在数据库操作中,通常需要为某些公共字段(如创建时间、更新时间等)自动赋值。采用AOP:
|
|
|
|
|
|
|
|
|
|
1. 统一管理这些字段的赋值逻辑
|
|
|
|
|
2. 避免在业务代码中重复设置
|
|
|
|
|
3. 确保数据一致性
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
| **序号** | **字段名** | **含义** | **数据类型** | **操作类型** |
|
|
|
|
|
| -------- | ----------- | -------- | ------------ | -------------- |
|
|
|
|
|
| 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 注解的方法,通过反射为公共字段赋值
|
|
|
|
|
|
|
|
|
|
3). 在 Mapper 的方法上加入 AutoFill 注解
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**技术点:**枚举、注解、AOP、反射
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Java中操作Redis
|
|
|
|
|
|
|
|
|
|
### 环境搭建
|
|
|
|
|
|
|
|
|
|
进入到sky-server模块
|
|
|
|
|
|
|
|
|
|
**1). 导入Spring Data Redis的maven坐标(已完成)**
|
|
|
|
|
|
2025-03-26 18:16:04 +08:00
|
|
|
|
```java
|
2025-03-18 12:46:59 +08:00
|
|
|
|
<dependency>
|
|
|
|
|
<groupId>org.springframework.boot</groupId>
|
|
|
|
|
<artifactId>spring-boot-starter-data-redis</artifactId>
|
|
|
|
|
</dependency>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**2). 配置Redis数据源**
|
|
|
|
|
|
|
|
|
|
在application-dev.yml中添加
|
|
|
|
|
|
2025-03-26 18:16:04 +08:00
|
|
|
|
```java
|
2025-03-18 12:46:59 +08:00
|
|
|
|
sky:
|
|
|
|
|
redis:
|
|
|
|
|
host: localhost
|
|
|
|
|
port: 6379
|
|
|
|
|
password: 123456
|
|
|
|
|
database: 10
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**解释说明:**
|
|
|
|
|
|
|
|
|
|
database:指定使用Redis的哪个数据库,Redis服务启动后默认有16个数据库,编号分别是从0到15。
|
|
|
|
|
|
|
|
|
|
可以通过修改Redis配置文件来指定数据库的数量。
|
|
|
|
|
|
|
|
|
|
在application.yml中添加读取application-dev.yml中的相关Redis配置
|
|
|
|
|
|
2025-03-26 18:16:04 +08:00
|
|
|
|
```java
|
2025-03-18 12:46:59 +08:00
|
|
|
|
spring:
|
|
|
|
|
profiles:
|
|
|
|
|
active: dev
|
|
|
|
|
redis:
|
|
|
|
|
host: ${sky.redis.host}
|
|
|
|
|
port: ${sky.redis.port}
|
|
|
|
|
password: ${sky.redis.password}
|
|
|
|
|
database: ${sky.redis.database}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**3). 编写配置类,创建RedisTemplate对象**
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
package com.sky.config;
|
|
|
|
|
|
|
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
import org.springframework.context.annotation.Bean;
|
|
|
|
|
import org.springframework.context.annotation.Configuration;
|
|
|
|
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
|
|
|
|
import org.springframework.data.redis.core.RedisTemplate;
|
|
|
|
|
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
|
|
|
|
|
|
|
|
|
@Configuration
|
|
|
|
|
@Slf4j
|
|
|
|
|
public class RedisConfiguration {
|
|
|
|
|
|
|
|
|
|
@Bean
|
|
|
|
|
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
|
|
|
|
|
log.info("开始创建redis模板对象...");
|
|
|
|
|
RedisTemplate redisTemplate = new RedisTemplate();
|
|
|
|
|
//设置redis的连接工厂对象 连接工厂负责创建与 Redis 服务器的连接
|
|
|
|
|
redisTemplate.setConnectionFactory(redisConnectionFactory);
|
|
|
|
|
//设置redis key的序列化器 这意味着所有通过这个RedisTemplate实例存储的键都将被转换为字符串格式存储在Redis中
|
|
|
|
|
redisTemplate.setKeySerializer(new StringRedisSerializer());
|
|
|
|
|
return redisTemplate;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**解释说明:**
|
|
|
|
|
|
|
|
|
|
当前配置类不是必须的,因为 Spring Boot 框架会自动装配 RedisTemplate 对象,但是默认的key序列化器为
|
|
|
|
|
|
|
|
|
|
JdkSerializationRedisSerializer,**导致我们存到Redis中后的数据和原始数据有差别,故设置为**
|
|
|
|
|
|
|
|
|
|
**StringRedisSerializer序列化器**。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 功能测试
|
|
|
|
|
|
|
|
|
|
**通过RedisTemplate对象操作Redis**
|
|
|
|
|
|
|
|
|
|
在test下新建测试类
|
|
|
|
|
|
|
|
|
|
**字符串测试**
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
@SpringBootTest
|
|
|
|
|
public class SpringDataRedisTest {
|
|
|
|
|
@Autowired
|
|
|
|
|
private RedisTemplate redisTemplate;
|
|
|
|
|
@Test
|
|
|
|
|
public void testRedisTemplate(){
|
|
|
|
|
System.out.println(redisTemplate);
|
|
|
|
|
}
|
|
|
|
|
@Test
|
|
|
|
|
public void testString(){
|
|
|
|
|
//set get setex setnx
|
|
|
|
|
redisTemplate.opsForValue().set("city","北京");
|
|
|
|
|
String city= (String) redisTemplate.opsForValue().get("city");
|
|
|
|
|
System.out.println(city);
|
|
|
|
|
redisTemplate.opsForValue().set("code","1234",3, TimeUnit.MINUTES); //设置code的值为1234,过期时间3min
|
|
|
|
|
redisTemplate.opsForValue().setIfAbsent("lock","1"); //如果不存在该key则创建
|
|
|
|
|
redisTemplate.opsForValue().setIfAbsent("lock","2");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**哈希测试**
|
|
|
|
|
|
2025-03-26 18:16:04 +08:00
|
|
|
|
```java
|
2025-03-18 12:46:59 +08:00
|
|
|
|
/**
|
|
|
|
|
* 操作哈希类型的数据
|
|
|
|
|
*/
|
|
|
|
|
@Test
|
|
|
|
|
public void testHash(){
|
|
|
|
|
//hset hget hdel hkeys hvals
|
|
|
|
|
HashOperations hashOperations = redisTemplate.opsForHash();
|
|
|
|
|
|
|
|
|
|
hashOperations.put("100","name","tom");
|
|
|
|
|
hashOperations.put("100","age","20");
|
|
|
|
|
|
|
|
|
|
String name = (String) hashOperations.get("100", "name");
|
|
|
|
|
System.out.println(name);
|
|
|
|
|
|
|
|
|
|
Set keys = hashOperations.keys("100");
|
|
|
|
|
System.out.println(keys);
|
|
|
|
|
|
|
|
|
|
List values = hashOperations.values("100");
|
|
|
|
|
System.out.println(values);
|
|
|
|
|
|
|
|
|
|
hashOperations.delete("100","age");
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**get获得的是Object类型,keys获得的是set类型,values获得的是List**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**3). 操作列表类型数据**
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
/**
|
|
|
|
|
* 操作列表类型的数据
|
|
|
|
|
*/
|
|
|
|
|
@Test
|
|
|
|
|
public void testList(){
|
|
|
|
|
//lpush lrange rpop llen
|
|
|
|
|
ListOperations listOperations = redisTemplate.opsForList();
|
|
|
|
|
|
|
|
|
|
listOperations.leftPushAll("mylist","a","b","c");
|
|
|
|
|
listOperations.leftPush("mylist","d");
|
|
|
|
|
|
|
|
|
|
List mylist = listOperations.range("mylist", 0, -1);
|
|
|
|
|
System.out.println(mylist);
|
|
|
|
|
|
|
|
|
|
listOperations.rightPop("mylist");
|
|
|
|
|
|
|
|
|
|
Long size = listOperations.size("mylist");
|
|
|
|
|
System.out.println(size);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**4). 操作集合类型数据**
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
/**
|
|
|
|
|
* 操作集合类型的数据
|
|
|
|
|
*/
|
|
|
|
|
@Test
|
|
|
|
|
public void testSet(){
|
|
|
|
|
//sadd smembers scard sinter sunion srem
|
|
|
|
|
SetOperations setOperations = redisTemplate.opsForSet();
|
|
|
|
|
|
|
|
|
|
setOperations.add("set1","a","b","c","d");
|
|
|
|
|
setOperations.add("set2","a","b","x","y");
|
|
|
|
|
|
|
|
|
|
Set members = setOperations.members("set1");
|
|
|
|
|
System.out.println(members);
|
|
|
|
|
|
|
|
|
|
Long size = setOperations.size("set1");
|
|
|
|
|
System.out.println(size);
|
|
|
|
|
|
|
|
|
|
Set intersect = setOperations.intersect("set1", "set2");
|
|
|
|
|
System.out.println(intersect);
|
|
|
|
|
|
|
|
|
|
Set union = setOperations.union("set1", "set2");
|
|
|
|
|
System.out.println(union);
|
|
|
|
|
|
|
|
|
|
setOperations.remove("set1","a","b");
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**5). 操作有序集合类型数据**
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
/**
|
|
|
|
|
* 操作有序集合类型的数据
|
|
|
|
|
*/
|
|
|
|
|
@Test
|
|
|
|
|
public void testZset(){
|
|
|
|
|
//zadd zrange zincrby zrem
|
|
|
|
|
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
|
|
|
|
|
|
|
|
|
|
zSetOperations.add("zset1","a",10);
|
|
|
|
|
zSetOperations.add("zset1","b",12);
|
|
|
|
|
zSetOperations.add("zset1","c",9);
|
|
|
|
|
|
|
|
|
|
Set zset1 = zSetOperations.range("zset1", 0, -1);
|
|
|
|
|
System.out.println(zset1);
|
|
|
|
|
|
|
|
|
|
zSetOperations.incrementScore("zset1","c",10);
|
|
|
|
|
|
|
|
|
|
zSetOperations.remove("zset1","a","b");
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**6). 通用命令操作**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- `*` 匹配零个或多个字符。
|
|
|
|
|
- `?` 匹配任何单个字符。
|
|
|
|
|
- `[abc]` 匹配方括号内的任一字符(本例中为 'a'、'b' 或 'c')。
|
|
|
|
|
- `[^abc]` 或 `[!abc]` 匹配任何不在方括号中的单个字符。
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
/**
|
|
|
|
|
* 通用命令操作
|
|
|
|
|
*/
|
|
|
|
|
@Test
|
|
|
|
|
public void testCommon(){
|
|
|
|
|
//keys exists type del
|
|
|
|
|
Set keys = redisTemplate.keys("*");
|
|
|
|
|
System.out.println(keys);
|
|
|
|
|
|
|
|
|
|
Boolean name = redisTemplate.hasKey("name");
|
|
|
|
|
Boolean set1 = redisTemplate.hasKey("set1");
|
|
|
|
|
|
|
|
|
|
for (Object key : keys) {
|
|
|
|
|
DataType type = redisTemplate.type(key);
|
|
|
|
|
System.out.println(type.name());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
redisTemplate.delete("mylist");
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## HttpClient
|
|
|
|
|
|
|
|
|
|
**HttpClient作用:**
|
|
|
|
|
|
|
|
|
|
- 在Java程序中发送HTTP请求
|
|
|
|
|
- 接收响应数据
|
|
|
|
|
|
2025-04-17 18:55:50 +08:00
|
|
|
|
**HttpClient的maven坐标:**
|
|
|
|
|
|
|
|
|
|
```xml
|
|
|
|
|
<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方式请求类型。
|
|
|
|
|
|
2025-03-18 12:46:59 +08:00
|
|
|
|
**HttpClient发送请求步骤:**
|
|
|
|
|
|
|
|
|
|
- 创建HttpClient对象
|
|
|
|
|
- 创建Http请求对象
|
|
|
|
|
- 调用HttpClient的execute方法发送请求
|
|
|
|
|
|
2025-04-17 18:55:50 +08:00
|
|
|
|
**测试用例**
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
2025-03-26 18:16:04 +08:00
|
|
|
|
```java
|
2025-04-17 18:55:50 +08:00
|
|
|
|
@SpringBootTest
|
|
|
|
|
public class HttpClientTest {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 测试通过httpclient发送GET方式的请求
|
|
|
|
|
*/
|
|
|
|
|
@Test
|
|
|
|
|
public void testGET() throws Exception{
|
|
|
|
|
//创建httpclient对象
|
2025-03-18 12:46:59 +08:00
|
|
|
|
CloseableHttpClient httpClient = HttpClients.createDefault();
|
|
|
|
|
|
2025-04-17 18:55:50 +08:00
|
|
|
|
//创建请求对象
|
|
|
|
|
HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
2025-04-17 18:55:50 +08:00
|
|
|
|
//发送请求,接受响应结果
|
|
|
|
|
CloseableHttpResponse response = httpClient.execute(httpGet);
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
2025-04-17 18:55:50 +08:00
|
|
|
|
//获取服务端返回的状态码
|
|
|
|
|
int statusCode = response.getStatusLine().getStatusCode();
|
|
|
|
|
System.out.println("服务端返回的状态码为:" + statusCode);
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
2025-04-17 18:55:50 +08:00
|
|
|
|
HttpEntity entity = response.getEntity();
|
|
|
|
|
String body = EntityUtils.toString(entity);
|
|
|
|
|
System.out.println("服务端返回的数据为:" + body);
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
2025-04-17 18:55:50 +08:00
|
|
|
|
//关闭资源
|
|
|
|
|
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);
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
2025-04-17 18:55:50 +08:00
|
|
|
|
//发送请求
|
|
|
|
|
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();
|
2025-03-18 12:46:59 +08:00
|
|
|
|
}
|
2025-04-17 18:55:50 +08:00
|
|
|
|
}
|
2025-03-18 12:46:59 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 微信小程序
|
|
|
|
|
|
2025-03-19 18:31:37 +08:00
|
|
|
|

|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 缓存功能
|
|
|
|
|
|
|
|
|
|
用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大。
|
|
|
|
|
|
2025-03-19 18:31:37 +08:00
|
|
|
|

|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 实现思路
|
|
|
|
|
|
|
|
|
|
通过Redis来缓存菜品数据,减少数据库查询操作。
|
|
|
|
|
|
2025-03-19 18:31:37 +08:00
|
|
|
|

|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 经典缓存实现代码
|
|
|
|
|
|
|
|
|
|
**缓存逻辑分析:**
|
|
|
|
|
|
|
|
|
|
- 每个分类下的菜品保存一份缓存数据
|
|
|
|
|
- 数据库中菜品数据有**变更时清理缓存数据**
|
|
|
|
|
|
2025-03-26 18:16:04 +08:00
|
|
|
|
```java
|
2025-03-18 12:46:59 +08:00
|
|
|
|
@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** 的相关方法,加入清理缓存逻辑。
|
|
|
|
|
|
|
|
|
|
需要改造的方法:
|
|
|
|
|
|
|
|
|
|
- 新增菜品
|
|
|
|
|
- 修改菜品
|
|
|
|
|
- 批量删除菜品
|
|
|
|
|
- 起售、停售菜品
|
|
|
|
|
|
|
|
|
|
清理缓冲方法:
|
|
|
|
|
|
2025-03-26 18:16:04 +08:00
|
|
|
|
```java
|
2025-03-18 12:46:59 +08:00
|
|
|
|
private void cleanCache(String pattern){
|
|
|
|
|
Set keys = redisTemplate.keys(pattern);
|
|
|
|
|
redisTemplate.delete(keys);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Spring Cache框架实现缓存
|
|
|
|
|
|
|
|
|
|
Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。
|
|
|
|
|
|
2025-03-26 18:16:04 +08:00
|
|
|
|
```java
|
2025-03-18 12:46:59 +08:00
|
|
|
|
<dependency>
|
|
|
|
|
<groupId>org.springframework.boot</groupId>
|
|
|
|
|
<artifactId>spring-boot-starter-cache</artifactId> <version>2.7.3</version>
|
|
|
|
|
</dependency>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
在SpringCache中提供了很多缓存操作的注解,常见的是以下的几个:
|
|
|
|
|
|
|
|
|
|
| **注解** | **说明** |
|
|
|
|
|
| -------------- | ------------------------------------------------------------ |
|
|
|
|
|
| @EnableCaching | 开启缓存注解功能,通常加在**启动类**上 |
|
|
|
|
|
| @Cacheable | 在**方法**执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据(**取**);如果没有缓存数据,调用方法并将方法返回值**放**到缓存中 |
|
|
|
|
|
| @CachePut | 将方法的返回值**放**到缓存中 |
|
|
|
|
|
| @CacheEvict | 将一条或多条数据从缓存中**删除** |
|
|
|
|
|
|
|
|
|
|
**@CachePut 说明:**
|
|
|
|
|
|
|
|
|
|
作用: 将方法返回值,放入缓存
|
|
|
|
|
|
|
|
|
|
value: 缓存的名称, 每个缓存名称下面可以有很多key
|
|
|
|
|
|
|
|
|
|
key: 缓存的key ----------> 支持Spring的表达式语言SPEL语法
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
`value` 和 `cacheNames` 属性在用法上是等效的。它们都用来指定缓存区的名称
|
|
|
|
|
|
|
|
|
|
在Redis中并没有直接的“缓存名”概念,而是通过键(key)来访问数据。Spring Cache通过`cacheNames`属性来模拟不同的“缓存区”,实际上这是通过将这些名称作为键的一部分来实现的。例如,如果你有一个缓存名为 `userCache`,那么所有相关的缓存条目的键可能以 `"userCache::"` 开头。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-03-26 18:16:04 +08:00
|
|
|
|
```java
|
2025-03-18 12:46:59 +08:00
|
|
|
|
@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 ;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**@Cacheable 说明:**
|
|
|
|
|
|
|
|
|
|
作用: 在方法执行前,spring先查看缓存中是否有**指定的key的**数据,如果有数据,则直接返回缓存数据,不执行后续sql操作;若没有数据,调用方法并将方法返回值放到缓存中。
|
|
|
|
|
|
|
|
|
|
所以,@Cacheable(cacheNames = "userCache",key="#id")中的#id表示的是函数形参中的id,而不能是返回值中的user.id
|
|
|
|
|
|
2025-03-26 18:16:04 +08:00
|
|
|
|
```java
|
2025-03-18 12:46:59 +08:00
|
|
|
|
@GetMapping
|
|
|
|
|
@Cacheable(cacheNames = "userCache",key="#id")
|
|
|
|
|
public User getById(Long id){
|
|
|
|
|
User user = userMapper.getById(id);
|
|
|
|
|
return user;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**@CacheEvict 说明:**
|
|
|
|
|
|
|
|
|
|
作用: 清理指定缓存
|
|
|
|
|
|
2025-03-26 18:16:04 +08:00
|
|
|
|
```java
|
2025-03-18 12:46:59 +08:00
|
|
|
|
@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();
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 微信支付
|
|
|
|
|
|
|
|
|
|
小程序支付
|
|
|
|
|
|
|
|
|
|
https://pay.weixin.qq.com/static/product/product_index.shtml
|
|
|
|
|
|
2025-03-19 18:31:37 +08:00
|
|
|
|

|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**5.商户系统调用微信后台:**
|
|
|
|
|
|
2025-03-19 18:31:37 +08:00
|
|
|
|

|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**10.用户调起微信支付**
|
|
|
|
|
|
2025-03-19 18:31:37 +08:00
|
|
|
|

|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 内网穿透
|
|
|
|
|
|
|
|
|
|
微信后台会调用到商户系统给推送支付的结果,在这里我们就会遇到一个问题,就是微信后台怎么就能调用到我们这个商户系统呢?因为这个调用过程,其实本质上也是一个HTTP请求。
|
|
|
|
|
|
|
|
|
|
目前,商户系统它的ip地址就是当前自己电脑的ip地址,只是一个局域网内的ip地址,微信后台无法调用到。
|
|
|
|
|
|
|
|
|
|
**解决:**内网穿透。通过**cpolar软件**可以获得一个临时域名,而这个临时域名是一个公网ip,这样,微信后台就可以请求到商户系统了。
|
|
|
|
|
|
|
|
|
|
1)下载地址:https://dashboard.cpolar.com/get-started
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**2). cpolar指定authtoken**
|
|
|
|
|
|
|
|
|
|
复制authtoken:
|
|
|
|
|
|
2025-03-19 18:31:37 +08:00
|
|
|
|

|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
执行命令:
|
|
|
|
|
|
|
|
|
|
注意,cd到cpolar.exe所在的目录打开cmd
|
|
|
|
|
|
|
|
|
|
输入代码:
|
|
|
|
|
|
2025-03-26 18:16:04 +08:00
|
|
|
|
```java
|
2025-03-18 12:46:59 +08:00
|
|
|
|
cpolar.exe authtoken ZmIwMmQzZDYtZDE2ZS00ZGVjLWE2MTUtOGQ0YTdhOWI2M2Q1
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
3)获取临时域名
|
|
|
|
|
|
2025-03-26 18:16:04 +08:00
|
|
|
|
```java
|
2025-03-18 12:46:59 +08:00
|
|
|
|
cpolar.exe http 8080
|
|
|
|
|
```
|
|
|
|
|
|
2025-03-19 18:31:37 +08:00
|
|
|
|

|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
这里的 https://52ac2ecb.r18.cpolar.top 就是与http://localhost:8080对应的临时域名。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Spring Task
|
|
|
|
|
|
|
|
|
|
**Spring Task** 是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。
|
|
|
|
|
|
|
|
|
|
**定位:**定时任务框架
|
|
|
|
|
|
|
|
|
|
**作用:**定时自动执行某段Java代码
|
|
|
|
|
|
|
|
|
|
### 1.2 cron表达式
|
|
|
|
|
|
|
|
|
|
**cron表达式**其实就是一个字符串,通过cron表达式可以**定义任务触发的时间**
|
|
|
|
|
|
|
|
|
|
**构成规则:**分为6或7个域,由空格分隔开,每个域代表一个含义
|
|
|
|
|
|
|
|
|
|
每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)
|
|
|
|
|
|
2025-03-19 18:31:37 +08:00
|
|
|
|

|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
cron表达式在线生成器:https://cron.qqe2.com/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 1.3 入门案例
|
|
|
|
|
|
|
|
|
|
#### 1.3.1 Spring Task使用步骤
|
|
|
|
|
|
|
|
|
|
1). 导入maven坐标 spring-context(已存在)
|
|
|
|
|
|
2025-03-19 18:31:37 +08:00
|
|
|
|
<img src="https://pic.bitday.top/i/2025/03/19/u805dw-2.png" alt="image-20221218193251182" style="zoom:50%;" />
|
2025-03-18 12:46:59 +08:00
|
|
|
|
|
|
|
|
|
2). 启动类添加注解 @EnableScheduling 开启任务调度
|
|
|
|
|
|
|
|
|
|
3). 自定义定时任务类
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 2.订单状态定时处理
|
|
|
|
|
|
|
|
|
|
#### 2.1 需求分析
|
|
|
|
|
|
|
|
|
|
用户下单后可能存在的情况:
|
|
|
|
|
|
|
|
|
|
- 下单后未支付,订单一直处于**“待支付”**状态
|
|
|
|
|
- 用户收货后管理端未点击完成按钮,订单一直处于**“派送中”**状态
|
|
|
|
|
|
|
|
|
|
对于上面两种情况需要通过**定时任务**来修改订单状态,具体逻辑为:
|
|
|
|
|
|
|
|
|
|
- 通过定时任务每分钟检查一次是否存在支付超时订单(下单后超过15分钟仍未支付则判定为支付超时订单),如果存在则修改订单状态为“已取消”
|
|
|
|
|
- 通过定时任务每天凌晨1点(打烊后)检查一次是否存在“派送中”的订单,如果存在则修改订单状态为“已完成”
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-03-26 18:16:04 +08:00
|
|
|
|
```java
|
2025-03-18 12:46:59 +08:00
|
|
|
|
@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 为消息内容
|
|
|
|
|
|