黑马程序员苍穹外卖--学习笔记(苍穹外卖万字总结—重点知识,面试常见问题)超全
前言
最近复习一下黑马程序员苍穹外卖这个项目,在总结一下吧,毕竟是暑假做的项目,因为博主最近再投简历了✌️✌️✌️,其中也会写一些常见面试题哈哈哈哈哈哈(我是结合JavaGuide和小林Coding看的)。其实就是把我前面写的总结到一起再补充些面试题🤪,写的不好不要喷我啊(本人菜菜的)
苍穹外卖知识点
一、Nginx
1.Nginx能干啥?
静态资源
能高效地提供 HTML、CSS、JavaScript、图片等静态文件,减少后端应用服务器的压力。(你网站的图片、HTML 页面,Nginx 能飞快地发给用户)
正向/反向代理
-
反向代理是指服务器接受客户端的请求,然后将请求转发给后端服务器,并将后端服务器的响应返回给客户端。反向代理隐藏了服务器的真实身份和位置信息,客户端只知道与反向代理进行通信,而不知道真正的服务器。
-
正向代理是客户端发送请求后代理服务器访问目标服务器,代理服务器代表客户端发送请求并将相应返回给客户端。正向代理隐藏了客户端的真实身份和位置信息,为客户端提供代理访问互联网的功能。
负载均衡
把大量的请求按照、我们指定的指定的方式均衡的分配给集群中的每台服务器。
举个例子简单解释一下Nginx的作用
场景:你家开了个杂货铺(后端服务器),但顾客(用户)太多,门口挤得水泄不通。于是你在街口设了个“代收点”(Nginx),顾客只和代收点打交道。
🔍 核心作用:
- 附加服务
代收点还能顺手做打包(Gzip压缩)、验货(SSL加密)、暂存热门商品(缓存),比如顾客要买畅销酱油,直接从代收点拿,不用跑仓库。 - 隐藏真实店铺
顾客不知道杂货铺具体在哪(IP地址被隐藏),代收点统一收货、发货,防止小偷盯上店铺(防攻击)。 - 统一分发任务
顾客要买米、买菜、退货,代收点自动分给仓库、收银员、售后员(后端不同服务),顾客只用找代收点一个地方。
nginx 反向代理的好处
提高访问速度
因为nginx本身可以进行缓存,如果访问的同一接口,并且做了数据缓存,nginx就直接可把数据返回,不需要真正访问服务端,从而提高访问速度
进行负载均衡
所谓负载均衡,就是把大量的请求按照我们指定的方式均衡的分配给集群中的每台服务器。
保证后端服务安全
因为一般后台服务地址不会暴露,所以浏览器不能直接访问,可以把nginx 作为请求访问的入口;请求到达nginx 后转发到具体的服务器中,从而保证后端服务的安全。
💡提示: 上面都是我在ai上问的那种专业的概念, 看不懂很正常, 我刚开始听课的时候也不懂很懵, 直到我自己用阿里云服务器部署我自己的项目, 就懂了它大概是干什么, 哦对如果了解前端路由那块的知识, 也有很大帮助哦! ! !
3、📦 配置示例(用nginx部署项目的完整流程)
- 放置前端文件:
- 把前端打包后的所有文件(通常在dist目录下)复制到 Nginx 的html目录(默认路径:/usr/share/nginx/html)
- 比如:如果打包后的文件在本地./dist,执行命令:cp -r ./dist/* /usr/share/nginx/html/
- 修改配置文件:
- 找到 Nginx 的配置文件(通常在/etc/nginx/conf.d/default.conf或/etc/nginx/nginx.conf)
- 替换成你需要的配置
- 访问验证:
- 浏览器访问http://localhost(80 端口),就能看到你的前端页面
- 前端调用/api/xxx接口时,会自动转发到localhost:8081/xxx
也就是说整个nginx你只需要注意这两个文件夹html目录和Nginx 的配置文件(通常在/etc/nginx/conf.d/default.conf或/etc/nginx/nginx.conf): 其中最重要的也是这个配置文件, 上面提到的nginx的工作原理和好处都是它实现的
server {
# Nginx对外监听80端口(你浏览器访问的端口)
listen 80;
# 访问域名,localhost即可
server_name localhost;
# 上传文件大小限制,保持10MB即可
client_max_body_size 10m;
# 前端页面配置(核心)
location / {
# 前端打包后的dist文件存放目录(关键!需要改成你实际的路径)
# 示例:如果你的前端打包文件放在 /usr/share/nginx/html/dist ,就写 root /usr/share/nginx/html/dist;
# 如果直接放在Nginx默认的html目录,就写 root html;
root html;
# 前端首页文件
index index.html index.htm;
# 解决Vue/React等前端路由刷新404的问题(必须保留)
try_files $uri $uri/ /index.html;
}
# 后端接口转发配置(修改为你的8081端口)
location ^~ /api/ {
# 去掉/api前缀(如果后端接口本身就带/api,可删除这行)
rewrite ^/api/(.*)$ /$1 break;
# 转发到你的后端服务地址:localhost:8081
proxy_pass http://localhost:8081;
# 补充必要的proxy配置(避免后端获取不到正确的请求信息)
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 错误页面配置(保持不变)
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
nginx监听的是80端口,服务器名是本地(http://localhost:80)。如果匹配到api字符(http://localhost:80/api/xxx),就转发到proxy_pass对应的地址(http://localhost:8081/xxx)。
要注意的是这段配置:
location ^~ /api/ {//匹配 location ^~ /api/
rewrite ^/api/(.*)$ /$1 break;//执行 rewrite:/api/report/empGenderData → 变成 /report/empGenderData
proxy_pass http://localhost:8081;//转发到后端:http://localhost:8081/report/empGenderData
}
它的意思是:
① 当前端http://localhost:80/api/xxx,去掉 /api 前缀
/api/student/countByDept
→ 变成
/student/countByDept
② 转发给后端 8081
Nginx 自己去访问:
http://localhost:8081/student/countByDept
🧩扩展: 你可能会有这样的疑问:我现在有好几个项目在我的服务器, 我是不是要有要好多nginx?
我们完全不用新建多个 Nginx 容器, 因为80 端口是服务器的 “大门”,只能被一个 Nginx 容器占用 —— 只需要在同一个 Nginx 容器里配置多个 server 块,用「域名 / 路径区分不同项目」即可。
🎯 方案 1:用「不同域名」区分不同项目(推荐,生产环境常用)
如果你的服务器有多个域名(或想解析多个二级域名),比如:
- 项目 1:project1.xxx.com → 对应前端 1 + 后端 1
- 项目 2:project2.xxx.com → 对应前端 2 + 后端 2
修改后的 nginx.conf 示例:
user nobody;
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
# 项目1:project1.xxx.com
server {
listen 80;
server_name project1.xxx.com; # 你的第一个域名/二级域名
# 项目1前端静态资源
location / {
root /usr/share/nginx/html/project1; # 前端1打包文件放在这个目录
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
# 项目1后端API转发
location ^~ /api/ {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://project1-server:8080; # 项目1后端容器名
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
# 项目2:project2.xxx.com
server {
listen 80;
server_name project2.xxx.com; # 你的第二个域名/二级域名
# 项目2前端静态资源
location / {
root /usr/share/nginx/html/project2; # 前端2打包文件放在这个目录
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
# 项目2后端API转发
location ^~ /api/ {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://project2-server:8080; # 项目2后端容器名
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
🎯 方案 2:用「不同路径」区分不同项目(无域名时用)
如果只有一个公网 IP / 域名,比如:
- 项目 1:http://你的公网IP/project1 → 对应前端 1 + 后端 1
- 项目 2:http://你的公网IP/project2 → 对应前端 2 + 后端 2
修改后的 nginx.conf 示例
user nobody;
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name 你的公网IP; # 只有一个IP/域名
# 项目1:路径 /project1
location /project1/ {
root /usr/share/nginx/html; # 前端1打包文件放在 /usr/share/nginx/html/project1
index index.html index.htm;
try_files $uri $uri/ /project1/index.html; # 注意路径补全
}
# 项目1API:路径 /project1/api
location ^~ /project1/api/ {
rewrite ^/project1/api/(.*)$ /$1 break;
proxy_pass http://project1-server:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 项目2:路径 /project2
location /project2/ {
root /usr/share/nginx/html; # 前端2打包文件放在 /usr/share/nginx/html/project2
index index.html index.htm;
try_files $uri $uri/ /project2/index.html;
}
# 项目2API:路径 /project2/api
location ^~ /project2/api/ {
rewrite ^/project2/api/(.*)$ /$1 break;
proxy_pass http://project2-server:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
二、Git
Git原理
Git是一个分布式版本控制系统,广泛用于软件开发和版本控制。它允许团队成员协同工作,同时保持项目历史记录的完整性和可追溯性。
工作流程:
1.提交本地仓库:
工作区:这是你当前正在工作的目录,包含了从仓库checkout出来的代码副本。
暂存区:当你修改了工作区中的文件后,可以使用git add命令把这些修改添加到暂存区。暂存区保存了下次提交时要包含的所有更改。
提交(Commit):一旦你对暂存区满意,就可以执行git commit来创建一个新的commit对象。这个commit会记录下所有的改动,并且自动成为当前分支的最新commit。
2.提交到远程仓库:

Git提交代码具体操作
1. 首先先学一点linux常用命令
ls [-al] [dir] 作用: 显示指定目录下的内容
cd [dirName] 作用: 用于切换当前工作目录,即进入指定目录
touch 作用:创建文件
2.提交本地仓库
2.1.执行 git init //生成一个隐藏的 .git 目录 表明这是一个git本地仓库
2.2.Git工作目录下对于文件的修改(增加、删除、更新)会存在工作区,暂存区,本地仓库个状态,这些修改的状态会随着我们执行Git的命令而发生变化,下面来看一下具体操作(以file01.txt文件为例):
- git add (工作区 --> 暂存区)(git add . /“file01.txt”)
- git commit (暂存区 --> 本地仓库) (git commit -m ‘注释’ //-m表示修饰注释)
这样我们的代码就提交到本地仓库了(这里我觉得只要了解就好,我们实践中代码提交到码云都是在idea上进行的)
3.提交远程仓库(码云gitee)
3.1.在gitee上注册并创建仓库
3.2.添加远程仓库:
git remote add <远端名称> <仓库路径>
3.3.将本地仓库代码提交([ ] 中内容可以省略 )
git push [-f] [–set-upstream] [远端名称 ]
来实操:
- 创建本地 Git 仓库:在开发工具(如 IDEA)中,点击顶部菜单栏 VCS -> Create Git Repository…,选择项目目录(示例中为sky-take-out),点击 OK 完成本地 Git 仓库的初始化。


- 提交代码到本地仓库:点击工具栏中的 Commit 按钮(或使用快捷键 Ctrl+K),在弹出的提交窗口中,勾选所有未版本控制的文件(Unversioned Files),填写提交信息(Commit Message),点击 Commit 完成代码的本地提交。


3. 配置远程仓库地址:点击工具栏中的 Push 按钮(或使用快捷键 Ctrl+Shift+K),在弹出的推送窗口中选择 Define remote,在 URL 输入框中填入 Gitee 远程仓库的地址(示例为 https://gitee.com/hi_ma_yun/sky-take-out.git),点击 OK 完成远程仓库地址的绑定。

4. 推送代码到远程仓库:完成远程仓库地址绑定后,确认推送分支(默认为master),执行推送操作,将本地提交的代码上传至 Gitee 远程仓库。

三,登陆界面的实现
先来看看接口文档:
登陆界面的后端实现大致可以分为两部分登录功能和登录校验,其中登陆功能的实现是基于 SpringBoot+Mybatis 按照三层构架的思想,采用分层解耦的方式实现员工登录的基本功能
3.1 先来简单实现登录功能
3.1.1根据接口文档准备登陆时传递的数据模型和登陆成功后返回值的实体类:
注:本项目的实体类在:sky-pojo模块中
package com.sky.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
@Data
@ApiModel(description = "员工登录时传递的数据模型")
public class EmployeeLoginDTO implements Serializable {
@ApiModelProperty("用户名")
private String username;
@ApiModelProperty("密码")
private String password;
}
package com.sky.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(description = "员工登录返回的数据格式")
public class EmployeeLoginVO implements Serializable {
@ApiModelProperty("主键值")
private Long id;
@ApiModelProperty("用户名")
private String userName;
@ApiModelProperty("姓名")
private String name;
@ApiModelProperty("jwt令牌")
private String token;
}
3.1.2.接着是Controller层接收和响应数据
package com.sky.controller.admin;
/**
* 员工管理
*/
@RestController
@RequestMapping("/admin/employee")
@Slf4j
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
@Autowired
private JwtProperties jwtProperties;
/**
* 登录
*
* @param employeeLoginDTO
* @return
*/
@PostMapping("/login")
public Result<Employee> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
log.info("员工登录:{}", employeeLoginDTO);
Employee employee = employeeService.login(employeeLoginDTO);
return Result.success(employee);
}
/**
* 退出
*
* @return
*/
@PostMapping("/logout")
public Result<String> logout() {
return Result.success();
}
}
3.1.3.然后是Service层进行逻辑处理
(这里是用用户名来查找密码,进而处理密码的各种异常情况如:用户名不存在、密码不对、账号被锁定)先只实现这些简单的功能,再进一步优化
public Employee login(EmployeeLoginDTO employeeLoginDTO) {
String username = employeeLoginDTO.getUsername();
String password = employeeLoginDTO.getPassword();
//1、根据用户名查询数据库中的数据
Employee employee = employeeMapper.getByUsername(username);
//2、处理各种异常情况(用户名不存在、密码不对、账号被锁定)
if (employee == null) {
//账号不存在
throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
}
//密码比对
// TODO 后期需要进行md5加密,然后再进行比对
if (!password.equals(employee.getPassword())) {
//密码错误
throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
}
if (employee.getStatus() == StatusConstant.DISABLE) {
//账号被锁定
throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);
}
//3、返回实体对象
return employee;
}
3.1.4.最后调用mapper层进行数据处理(这个就是一个很简单的SQL语句)
public interface EmployeeMapper {
/**
* 根据用户名查询员工
* @param username
* @return
*/
@Select("select * from employee where username = #{username}")
Employee getByUsername(String username);
}
3.2. 再加上拦截器登录校验,优化一下登录功能
其中登陆校验的实现是基于令牌JWT技术来实现会话追踪,在后续的请求当中,服务端使用**Interceptor(拦截器)**统一拦截所有的请求,从而判断是否携带的有合法的JWT令牌。
3.2.1先来简单介绍一下JWT令牌吧
JWT是一种在Web应用程序,简单且安全地处理用户身份验证和信息交换的技术,简单来说就是用户登陆成功过后,肯定要再进行别的操作,JWT令牌就是一个身份证,告诉后端我已经成功登录过楼,让我进行接下来的操作吧首先我们应该了解JWT的组成才能用代码实现它。
JWT令牌由三个部分组成:
- 第一部分:Header(头), 记录令牌类型、签名算法等。 例如:{“alg”:“HS256”,“type”:“JWT”}
- 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{“id”:“1”,“username”:“Tom”}
- 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。
3.2.2 JWT令牌实现(令牌生成)
(Controller方法登录成功后,再加上JWT令牌,用来标识用户的身份,已经登陆过喽✌️✌️✌️)
接下来我们来看总的代码:
- 引入JWT的依赖:
<!-- JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
@Autowired
private JwtProperties jwtProperties;
/**
* 登录
*
* @param employeeLoginDTO
* @return
*/
@PostMapping("/login")
@ApiOperation(value = "员工的登录")
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
log.info("员工登录:{}", employeeLoginDTO);
Employee employee = employeeService.login(employeeLoginDTO);
//登录成功后,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(),
jwtProperties.getAdminTtl(),
claims);
EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
.id(employee.getId())
.userName(employee.getUsername())
.name(employee.getName())
.token(token)
.build();
return Result.success(employeeLoginVO);
}
我用AI给它注释了一下,可以更清楚地看到生成各部分的内容
// 登录成功后,生成 JWT 令牌
// Step 1: 构建 Payload(有效载荷),也就是我们常说的 Claims(声明)
Map<String, Object> claims = new HashMap<>();
// 将员工ID放入claims中,作为用户身份的一部分
// JwtClaimsConstant.EMP_ID 是自定义的键名,例如 "empId"
claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
// Step 2: 调用工具类生成 JWT 令牌
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(), // 签名使用的密钥(用于生成和验证签名)
jwtProperties.getAdminTtl(), // 令牌有效期(单位通常是毫秒)
claims // 自定义的 payload 数据(即上面构造的 claims)
);
1.JwtClaimsConstant.EMP_ID 是自定义的键名
它对应ID这个值,这里创建一个专门的类来存放 JWT 所需的各种 claim 键名:
package com.sky.constant;
public class JwtClaimsConstant {
public static final String EMP_ID = "empId";
public static final String USER_ID = "userId";
public static final String PHONE = "phone";
public static final String USERNAME = "username";
public static final String NAME = "name";
}
这个类只是一个 常量类,它的作用是统一管理 JWT 中使用的各种字段名称,避免硬编码和拼写错误。
- @Autowired
private JwtProperties jwtProperties;
我们来看看它对应的类
@Component
@ConfigurationProperties(prefix = "sky.jwt")
@Data
public class JwtProperties {
/**
* 管理端员工生成jwt令牌相关配置
*/
private String adminSecretKey;
private long adminTtl;
private String adminTokenName;
/**
* 用户端微信用户生成jwt令牌相关配置
*/
private String userSecretKey;
private long userTtl;
private String userTokenName;
}
这里(@ConfigurationProperties(prefix = “sky.jwt”))用到了Spring Boot 提供的一个注解(用于 将配置文件中以指定前缀开头的一组属性,自动绑定(映射)到一个 Java Bean 对象的字段上。),我们来看看其丢应的application.yml 文件:
sky:
jwt:
# 设置jwt签名加密时使用的秘钥
admin-secret-key: itcast
# 设置jwt过期时间
admin-ttl: 7200000
# 设置前端传递过来的令牌名称
admin-token-name: token
Spring Boot 会自动把这些配置值注入到 JwtProperties 类的相应字段中
admin-secret-key--->adminSecretKey
admin-ttl--->adminTtl
token--->adminTokenName
3.2.3 令牌校验
已经有JWT令牌,那我们怎么验证这个令牌是正确的呢?(不能随便一个人带个令牌就能进入我们的系统吧)
获取要校验的令牌(Interceptor)+校验令牌
Interceptor是什么?干啥的?
Interceptor(过滤器) 就像是公司门口的保安和安检门。
你在进公司之前,保安要检查你的身份(比如刷卡),安检门要检查你有没有带违禁品。Interceptor就是在服务器中检查客户端发来的请求的(比如你发出修改菜品的请求,服务器肯定要验证你是不是员工)。
下面来看看代码吧
1.配置拦截路径( 拦截什么方法校验是否登录 )
/**
* 注册自定义拦截器
*
* @param registry
*/
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器...");
registry.addInterceptor(jwtTokenAdminInterceptor)
.addPathPatterns("/admin/**")
.excludePathPatterns("/admin/employee/login");
registry.addInterceptor(jwtTokenUserInterceptor)
.addPathPatterns("/user/**")
.excludePathPatterns("/user/user/login")
.excludePathPatterns("/user/shop/status");
}
- 创建 JwtTokenAdminInterceptor 包( 校验令牌 )
package com.sky.interceptor;
/**
* jwt令牌校验的拦截器
*/
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProperties;
/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("当前线程的ID"+Thread.currentThread().getId());
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前员工id:", empId);
BaseContext.setCurrentId(empId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}
注意⚠️:在拦截过程中:BaseContext 类,保存了当前线程上下文信息(如用户ID) 。它的核心是使用了 ThreadLocal 来在线程内部安全地存储和传递数据。
BaseContext.setCurrentId(empId);
BaseContext类:
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();
}
}
ThreadLocal 是什么,?有什么用?这个面试考并发有可能问到💡
它是 Java 提供的一个类,用于在每个线程中保存一份独立的变量副本。不同线程之间互不干扰,非常适合在 Web 应用中保存“当前请求用户”的上下文信息。
在 Web 应用(我们这个Spring Boot项目)中,一个请求通常由一个线程处理。我们经常需要在整个请求链路中传递一些上下文信息,比如:
- 当前登录用户的 ID
- 请求的 traceId(用于日志追踪)
- 租户 ID(多租户系统)
如果不用 ThreadLocal,你可能要:
- 把用户 ID 作为参数一层层传下去(很麻烦)
- 或者用一个全局静态变量(但多个请求会互相覆盖!)
👉 ThreadLocal 解决了“线程间数据隔离”的问题,让每个线程拥有自己的上下文副本,既方便又线程安全。
ThreadLocal 的内部原理🙌🙌🙌
可以把 ThreadLocal 想象成一个“地图”:
- 每个 Thread 对象内部都有一个 ThreadLocalMap(键是 ThreadLocal 实例,值是你存的数据)
- 当你调用 threadLocal.set(value),实际上是:
Thread.currentThread().threadLocals.put(this, value);
- 当你调用 get(),就是从当前线程的 map 里取值
5.3 最后我们的代码还有两个问题
问题一:用户登录的密码是明文存储,安全性太低。
🛠️先来看看什么是MD5:
MD5:是一种常用的哈希算法,用于生成数据的“指纹”对密码加密。
🙌在来看看其在项目中的使用:
因为是对密码的加密,所以更改数据库中的密码改为加密后的:
123456—> e10adc3949ba59abbe56e057f20f883e
我们用它来改一下Service层登陆界面的实现代码:
@Service
public class EmployeeServiceImpl implements EmployeeService {
@Autowired
private EmployeeMapper employeeMapper;
public Employee login(EmployeeLoginDTO employeeLoginDTO) {
String username = employeeLoginDTO.getUsername();
String password = employeeLoginDTO.getPassword();
//1、根据用户名查询数据库中的数据
Employee employee = employeeMapper.getByUsername(username);
//2、处理各种异常情况(用户名不存在、密码不对、账号被锁定)
if (employee == null) {
//账号不存在
throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
}
//密码比对
// TODO 后期需要进行md5加密,然后再进行比对
password = DigestUtils.md5DigestAsHex(password.getBytes());
if (!password.equals(employee.getPassword())) {
//密码错误
throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
}
if (employee.getStatus() == StatusConstant.DISABLE) {
//账号被锁定
throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);
}
//3、返回实体对象
return employee;
}
🧠 把它拉出来分步解析:我们来逐行解释这段代码
password = DigestUtils.md5DigestAsHex(password.getBytes());
- password.getBytes()
作用:把字符串(比如 “123456”)转换成字节数组(byte[]),因为加密算法操作的是二进制数据。
"hello".getBytes() → [104, 101, 108, 108, 111]
- DigestUtils.md5DigestAsHex(…)
作用:使用 Apache Commons Codec 提供的 DigestUtils 工具类,对传入的字节数组进行 MD5 哈希计算,并将结果转为 16 进制字符串
String password = "123456";
String md5 = DigestUtils.md5DigestAsHex(password.getBytes());
System.out.println(md5); // 输出:e10adc3949ba59abbe56e057f20f883e
问题二:登录的时候如果用户名为空,要抛出异常
1. 🙌先来说Java异常的基本实现(基础知识点可以跳过):
📚先来看看Java异常主要需要掌握的知识点:
- Java异常的基本结构:
Throwable(所有异常的祖宗)
├─ Error(严重错误,比如内存不够,一般不处理)
└─ Exception(我们主要处理的异常)
├─ RuntimeException(运行时异常,比如空指针、数组越界)
└─ 受检异常(比如读写文件出错)
✅异常的顶级父类为Throwable,其中子类Exception分为非受检异常(RuntimeException:不强制你处理)和受检异常(必须处理,否则编译报错)两种异常类型
我还有一个详细的图可以看到常用异常类型(JavaGuide截的):
- 异常处理的关键字
try:包裹可能出错的代码。
catch:捕获并处理异常。
finally:无论是否出错,都会执行(通常用来关闭资源)。
throw:手动抛出一个异常。//自定义异常
throws:在方法上声明这个方法可能会抛出哪些异常
💡 来看一下代码:
//1.非受检异常(RuntimeException)
public class DivisionExample {
public static void main(String[] args) {
try {
int result = divide(10, 0);
System.out.println("Result: " + result);
} catch (ArithmeticException e) {
System.out.println("Error: " + e.getMessage());
}
}
public static int divide(int a, int b) {
return a / b; // 如果b为0,则会抛出ArithmeticException
}
}
import java.io.*;
public class FileReaderExample {
public static void main(String[] args) {
try {
readFromFile("nonexistentfile.txt");
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}
}
public static void readFromFile(String filePath) throws IOException {
File file = new File(filePath);
FileReader fr = new FileReader(file); // 如果文件不存在,则会抛出FileNotFoundException
BufferedReader br = new BufferedReader(fr);
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
br.close();
}
}
但在做项目时有很多异常是编译器检测不到的,需要我们自定义:
✨自定义异常:
//自己创建一个异常类,继承 Exception 或 RuntimeException:
public class InvalidAgeException extends Exception {
public InvalidAgeException(String message) {
super(message);
}
}
//使用:
public class AgeValidator {
public static void main(String[] args) {
try {
validateAge(-5);
} catch (InvalidAgeException e) {
System.out.println("Caught exception: " + e.getMessage());
}
}
public static void validateAge(int age) throws InvalidAgeException {
if (age < 0) {
throw new InvalidAgeException("Age cannot be negative.");
} else {
System.out.println("Age is valid.");
}
}
}
2. 🔍再来看看Java异常在我们的项目中是怎么实现的:
Java异常在项目的运用主要是在Service层的自定义异常(这里以登陆时:用户名为空的异常为例子)
为了方便理解我把代码给了ai,让ai帮我写了一个思维导图(先整体看一下不理解没关系一会会给出解释):
登陆时:用户名为空,前端界面弹出提示框:
Controller 层调用 login 方法
↓
login 方法中:
Employee employee = employeeMapper.getByUsername(username);
if (employee == null) {
throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
}
↓
抛出 AccountNotFoundException 异常
↓
由于没有 try-catch,异常继续向上传播
↓
Spring 框架检测到 Controller 抛出异常
↓
查找全局异常处理器 GlobalExceptionHandler
↓
匹配到 exceptionHandler(BaseException ex)
↓
记录日志,并返回 Result.error("账号不存在")
↓
前端收到 JSON 格式的错误信息:
{
"code": 500,
"message": "账号不存在",
"data": null
}
🙌再来看看代码:
- service层判断用户名:
...
@Service
public class EmployeeService {
@Autowired
private EmployeeMapper employeeMapper;
public Employee login(EmployeeLoginDTO employeeLoginDTO) {
String username = employeeLoginDTO.getUsername();
String password = employeeLoginDTO.getPassword();
Employee employee = employeeMapper.getByUsername(username);
if (employee == null) {
throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
}
...
}
}
- 发现异常后抛给的自定义账号不存在异常:
package com.sky.exception;
/**
* 业务异常
*/
public class BaseException extends RuntimeException {
public BaseException() {
}
public BaseException(String msg) {
super(msg);
}
}
/**
* 账号不存在异常
*/
package com.sky.exception;
public class AccountNotFoundException extends BaseException {
public AccountNotFoundException() {
}
public AccountNotFoundException(String msg) {
super(msg);
}
}
- 没有捕获,抛给自定义全局异常处理器:
...
/**
* 全局异常处理器,处理项目中抛出的业务异常
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 捕获业务异常
* @param ex
* @return
*/
@ExceptionHandler
public Result exceptionHandler(BaseException ex){
log.error("异常信息:{}", ex.getMessage());
return Result.error(ex.getMessage());
}
...
}
✨其中需要注意:
**@RestControllerAdvice:**表示这是一个全局的控制器增强类,可以处理所有 Controller 中抛出的异常。
**@ExceptionHandler(…):**注解指定该方法用于处理 指定类型的异常,没有指定具体异常类型时,默认会匹配所有异常(但在这里只匹配 BaseException 及其子类)。
**exceptionHandler(BaseException ex):**这个方法专门用来处理你自定义的业务异常。
**Result.error():**封装成统一格式的响应结果返回给前端。
4. 总结一下:
Controller 中写了:
@GetMapping("/user")
public String getUser() {
throw new AccountNotFoundException("账号不存在");
}
- 请求进来,执行到 throw new AccountNotFoundException(…);
- 异常向上抛出;
- Spring MVC 检测到未被捕获的异常;
- 查找 @RestControllerAdvice 中是否有能处理该异常的 @ExceptionHandler;
- 发现 exceptionHandler(BaseException ex) 方法的参数类型 BaseException 是 AccountNotFoundException 的父类;
- 于是调用该方法,传入实际的 AccountNotFoundException 实例;
- 日志打印错误信息,并返回 Result.error(…) 给前端。
OK到这里员工登陆界面就圆满结束了🎉🎉🎉🎉🎉
四. Swagger——进行前后端测试
4.1 🙌什么是Swagger?
其实讲的时候我也很懵,讲的课件的原话是:“你只需要按照规范定义接口,生成接口文档,以及在线接口调试页面就行”,我让ai解释了一下:Swagger 是一个流行的 API 开发工具,它帮助开发者设计、构建、记录以及使用 RESTful 风格的 Web 服务。(反正我也没太看懂😵💫)
我根据我学web的时候用的Apifox(这个真的很好用)来看他俩应该差不多,但Swagger能根据我们写的代码自动生成测试。
🛠️ Swagger 的三大功能:
自动生成 API 文档
你写了一个接口,Swagger 自动把它变成网页版的文档。
网页上清晰地列出所有接口:路径、方法、参数、返回值等。
支持在线测试接口
不用手动写 Postman 请求,直接在 Swagger 页面上点击按钮就能调用接口。
接口规范统一
所有接口都有统一格式,方便前后端沟通协作。
🔍再来看看使用:
1. 导入knife4j的maven坐标
<!-- Knife4j UI 替代默认的 Swagger UI 页面 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.5.0</version> <!-- 注意版本匹配 Spring Boot 版本 -->
</dependency>
2. 在配置类中假如 knife4j 相关配置
/**
* 通过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. 设置静态资源映射,否则接口文档页面无法访问
/**
* 设置静态资源映射
* @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始进行静态资源映射...");
registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
🙌还有一些常见注释,主要是使生成的前端界面更丰富好用

这是课件上的解释,下面是用法:
@Api:用于描述整个控制器的功能
@ApiOperation:用于描述一个具体的 API 操作
来看代码(主要在controller层运用):
...
/**
* 员工管理
*/
@RestController
@RequestMapping("/admin/employee")
@Slf4j
@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) {
log.info("员工登录:{}", employeeLoginDTO);
Employee employee = employeeService.login(employeeLoginDTO);
//登录成功后,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(),
jwtProperties.getAdminTtl(),
claims);
EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
.id(employee.getId())
.userName(employee.getUsername())
.name(employee.getName())
.token(token)
.build();
return Result.success(employeeLoginVO);
}
/**
* 退出
*
* @return
*/
@PostMapping("/logout")
@ApiOperation("员工退出")
public Result<String> logout() {
return Result.success();
}
}
五. 完成员工的登录我们来看看其他模块的开发
5.1 新增员工信息
1. 在EmployeeController中新建一个方法save
/**
* 新增员工
* @param employeeDTO
* @return
*/
@PostMapping
@ApiOperation("新增员工")
public Result save(@RequestBody EmployeeDTO employeeDTO) {
System.out.println("当前线程的ID"+Thread.currentThread().getId());
log.info("新增员工,员工数据:{}", employeeDTO);
employeeService.save(employeeDTO);
return Result.success();
}
2. 在EmployeeService中编写如下代码
/**
* 新增员工
* @param employeeDTO
*/
@Override
public void save(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
//对象属性拷贝
BeanUtils.copyProperties(employeeDTO, employee);
//设置账号状态
employee.setStatus(StatusConstant.ENABLE);
//设置默认密码
employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
//设置创建时间
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
//设置创建人
employee.setCreateUser(BaseContext.getCurrentId());
employee.setUpdateUser(BaseContext.getCurrentId());
employeeMapper.insert(employee);
}
3. 在EmployeeMapper中编写SQL语句
/**
* 插入员工数据
* @param employee
*/
@Insert("insert into sky_take_out.employee (name,username, password, phone, sex, id_number, status, create_time, update_time, create_user, update_user) " +
"values" +
"(#{name}, #{username}, #{password}, #{phone}, #{sex}, #{idNumber}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
void insert(Employee employee);
其中有几个要注意的小知识点:
1. 前端的提交数据为什么用EmployeeDTO来封装,而插入数据时有用 Employee:
这是因为前端提交的数据和实体类中对应的属性差别较大时(也就是实体类中会有多余的属性),因此我们再新建一个类
DTO:
package com.sky.dto;
import lombok.Data;
import java.io.Serializable;
@Data
public class EmployeeDTO implements Serializable {
private Long id;
private String username;
private String name;
private String phone;
private String sex;
private String idNumber;
}
Employee:
package com.sky.entity;
import ...
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String name;
private String password;
private String phone;
private String sex;
private String idNumber;
private Integer status;
//@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
//@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
private Long createUser;
private Long updateUser;
}
2. 创建人ID是怎么得到的?
我们之前在拦截界面的代码,已经把线程的信息封装在ThreadLocal里面了,只需通过BaseContext类调用方法取出ID就行了:
package com.sky.context;
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();
}
}
//设置创建人
employee.setCreateUser(BaseContext.getCurrentId());
employee.setUpdateUser(BaseContext.getCurrentId());
5.2 分页查询员工信息
先来实现一个简单的功能(不是苍穹外卖哦,只是举个简单的例子)来了解一下分页查询
预备知识
- ✌️先看看接口文档(之前一直没看过接口文档,接口文档开发是一定要会看的),它不会直接告诉你要进行分页查询,而是要看请求参数

2.🙌想进行分页查询,我们还要了解数据库语言,使用LIMIT关键字,格式为:limit 开始索引 每页显示的条数。
第一页,显示10条数据:
select * from emp limit 0,10;
原始方式(后端代码实现)
还是先看一下前端的页面了解我们要实现的功能
这里以一个员工管理分页查询员工的功能为例子
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageResult {
private Long total; //总记录数
private List rows; //当前页数据列表
}
1). EmpController
@Slf4j
@RequestMapping("/emps")
@RestController
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={}", page, pageSize);
PageResult pageResult = empService.page(page, pageSize);
return Result.success(pageBean);
}
}
2). EmpService
public interface EmpService {
/**
* 分页查询
* @param page 页码
* @param pageSize 每页记录数
*/
PageResult page(Integer page, Integer pageSize);
}
3). EmpServiceImp
@Service
public class EmpServiceImpl implements EmpService {
@Autowired
private EmpMapper empMapper;
@Override
public PageResult page(Integer page, Integer pageSize) {
//1. 获取总记录数
Long total = empMapper.count();
//2. 获取结果列表
Integer start = (page - 1) * pageSize;
List<Emp> empList = empMapper.list(start, pageSize);
//3. 封装结果
return new PageResult(total, empList);
}
}
4). EmpMapper
@Mapper
public interface EmpMapper {
/**
* 查询总记录数
*/
@Select("select count(*) from emp e left join dept d on e.dept_id = d.id ")
public Long count();
/**
* 查询所有的员工及其对应的部门名称
*/
@Select("select e.*, d.name deptName from emp as e left join dept as d on e.dept_id = d.id limit #{start}, #{pageSize}")
public List<Emp> list(Integer start , Integer pageSize);
}
🙌🙌🙌用PageHelper分页插件优化
PageHelper是第三方提供的Mybatis框架中的一款功能强大、方便易用的分页插件,支持任何形式的单标、多表的分页查询。
1). 在pom.xml引入依赖
<!--分页插件PageHelper-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.7</version>
</dependency>
2). EmpMapper
PageHelper实现分页查询之后,只需要编写一条SQL语句,而且不需要考虑分页操作,就是一条正常的查询语句。
/**
* 查询所有的员工及其对应的部门名称
*/
@Select("select e.*, d.name deptName from emp as e left join dept as d on e.dept_id = d.id")
public List<Emp> list();
3). EmpServiceImpl
@Override
public PageResult page(Integer page, Integer pageSize) {
//1. 设置分页参数
PageHelper.startPage(page,pageSize);//无需手动计算起始索引,直接告诉PageHelper需要查询那一页的数据,每页展示多少条记录即可。
//2. 执行查询
List<Emp> empList = empMapper.list();
Page<Emp> p = (Page<Emp>) empList;//将查询到的总记录数,与数据列表封装到了 Page<Emp> 对象中。
//3. 封装结果
return new PageResult(p.getTotal(), p.getResult());
}
- PageHelper实现分页查询时,SQL语句的结尾一定一定一定不要加分号;
- PageHelper只会对紧跟在其后的第一条SQL语句进行分页处理。
✨我们可以对比一下,其实使用PageHelper分页插件进行分页是对原始方式的改善(简单了好多)

Mapper接口层:
- 原始的分页查询功能中,我们需要在Mapper接口中定义两条SQL语句。
- PageHelper实现分页查询之后,只需要编写一条SQL语句,而且不需要考虑分页操作,就是一条正常的查询语句。
Service层:
- 需要根据页码、每页展示记录数,手动的计算起始索引。
- 无需手动计算起始索引,直接告诉PageHelper需要查询那一页的数据,每页展示多少条记录即可。
员工分页查询后端代码开发
- 定义俩个实体类来封装是数据:
package com.sky.result;
/**
* 封装分页查询结果
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {
private long total; //总记录数
private List records; //当前页数据集合
}
package com.sky.dto;
@Data
public class EmployeePageQueryDTO implements Serializable {
//员工姓名
private String name;
//页码
private int page;
//每页显示记录数
private int pageSize;
}
- EmployeeController:
/**
*分页查询
* @param employeePageQueryDTO
*/
@GetMapping("/page")
@ApiOperation("员工分页查询")
public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO) {
log.info("员工分页查询,参数为{}",employeePageQueryDTO);
PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);
return Result.success(pageResult);
}
- EmployeeService接口
/**
* 分页查询
* @param employeePageQueryDTO
* @return
*/
PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
- EmployeeService
/**
* 分页查询
* @param employeePageQueryDTO
* @return
*/
@Override
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
//开始分页查询
PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());
Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);
long total = page.getTotal();
List<Employee> records = page.getResult();
return new PageResult(total, records);
}
- EmployeeMappe:
/**
* 分页查询
* @param employeePageQueryDTO
* @return
*/
Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
在application.yml配置文件中扫描了EmployeeMapper.xml配置文件:
mybatis:
#mapper配置文件
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.sky.entity
configuration:
#开启驼峰命名
map-underscore-to-camel-case: true
1. mapper-locations: classpath:mapper/*.xml
- 作用:指定 MyBatis 的 Mapper XML 映射文件 所在的位置。
- 说明:
- classpath: 表示从类路径(即 src/main/resources)下查找。
- mapper/*.xml 表示加载 resources/mapper/ 目录下所有以 .xml 结尾的文件。
- 这些 XML 文件通常包含 SQL 语句(如 , 等),与 Mapper 接口方法对应。
- EmployeeMapper.xml
<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>
这里面还有一个问题(查询返回的时间的格式怎么办?有两种方法)
方法一:用@JsonFormat注解,格式化时间
Employee类:
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
方法二:在Spring MVC 中用自定义或扩展 HTTP 消息转换器(HttpMessageConverter) 实现日期格式化
/**
* 扩展 MVC消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建一个消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用Jackson将Java对象序列化转为json
converter.setObjectMapper(new JacksonObjectMapper());
//将自己的转换器加入容器中
converters.add(0,converter);
}
创建自定义的 JSON 转换器:MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
MappingJackson2HttpMessageConverter 是 Spring 提供的、基于 Jackson 库 的消息转换器。
package com.sky.json;
import ...
/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
*/
public class JacksonObjectMapper extends ObjectMapper {
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
//public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
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);
}
}
5.3 修改员工状态
- EmployeeController
/**
* 修改员工状态
* @param status
* @param id
* @return
*/
@PostMapping("/status/{status}")
@ApiOperation("员工状态禁用/启用")
public Result startOrStop(@PathVariable Integer status, Long id) {
log.info("员工状态禁用/启用:{},{}", status,id);
employeeService.startOrStop(status, id);
return Result.success();
}
- EmployeeService接口
/**
* 启用禁用员工账号
* @param status
* @param id
*/
void startOrStop(Integer status, Long id);
- EmployeeServiceImpl
/**
* 启用禁用员工账号
* @param status
* @param id
*/
@Override
public void startOrStop(Integer status, Long id) {
/*
Employee employee = new Employee();
employee.setStatus(status);
employee.setId(id);
*/
Employee employee = Employee.builder()
.status(status)
.id(id)
.build();
employeeMapper.update(employee);
}
- 在EmployeeMapper:
/**
* 修改员工信息
* @param
* @return
*/
@AutoFill(value = OperationType.UPDATE)
void update(Employee employee);
- Employee Mapper.XML
<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>
5.4 修改员工信息(主要分为两个部分数据回显和员工信息修改)
5.4.1 数据回显:
EmployeeController中用id查询员工信息作为数据回显的方法
/**
*
* 根据id查询员工信息
* @param id
*/
@GetMapping("/{id}")
@ApiOperation("根据id查询员工信息")
public Result<Employee> getById(@PathVariable Long id) {
log.info("查询员工信息,员工id为{}", id);
Employee employee = employeeService.getById(id);
return Result.success(employee);
}
EmployeeService接口:
/**
* 根据id查询员工信息
* @param id
* @return
*/
@Override
public Employee getById(Long id) {
Employee employee = employeeMapper.getById(id);
employee.setPassword("****");
return employee;
}
EmployeeMapper方法:
/**
* 根据id查询员工
* @param id
* @return
*/
@Select("select * from sky_take_out.employee where id = #{id}")
Employee getById(Long id);
5.4.2 员工信息修改
- EmployeeController
/**
* 编辑员工信息
* @param employeeDTO
* @return
*/
@PutMapping
@ApiOperation("编辑员工信息")
public Result update(@Valid @RequestBody EmployeeDTO employeeDTO) {
log.info("编辑员工信息:{}", employeeDTO);
System.out.println("当前线程的ID"+Thread.currentThread().getId());
employeeService.update(employeeDTO);
return Result.success();
}
/**
* 修改员工信息
* @param employeeDTO
*/
@Override
public void update(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
BeanUtils.copyProperties(employeeDTO, employee);
employee.setUpdateTime(LocalDateTime.now());
//拦截器get出来
employee.setUpdateUser(BaseContext.getCurrentId());
employeeMapper.update(employee);
}
/**
* 修改员工信息
* @param
* @return
*/
void update(Employee employee);
<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>
OK了,员工的增删改查终于写完了,快写鼠博主了🤪
六, 公共字段填充(AOP,反射,注解)
6.1 为什么要使用公共字段填充
前面我们写员工的增删改查代码时,会发现有好多功能之间有好多重复的字段(比如:设置员工信息修改创建时间这种),可以用公共字段填充的方式,单领出来解决,因为这个方法涉及到了AOP,反射,注解等这种面试常考的方式,所以我单领出来总结总结
不知道什么是AOP的可以看看我这篇文章: 链接: Spring AOP
业务中常用的字段(这些我们都可以但领出来注入到我们的业务方法中):
对应的代码:
实现思路:
- 自定义注解AutoFill,用于标识需要进行公共字段填充的方法
- 自定义切面类AutoFillAspect,统一拦截加入AutoFill注解方法,通过反射为公共字段赋值。
- 在Mapper的方法上加入AutoFill注解
6.2 先来看看整体的配置代码:
实现思路:
- 自定义注解AutoFill,用于标识需要进行公共字段填充的方法
- 自定义切面类AutoFillAspect,统一拦截加入AutoFill注解方法,通过反射为公共字段赋值。
- 在Mapper的方法上加入AutoFill注解
1. 自定义注解AutoFill(注解)
package com.sky.annotation;
import com.sky.enumeration.OperationType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解,用于标识某个方法需要进行功能字段自动填充和处理
*/
@Target(ElementType.METHOD)// 表示该注解用于方法
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
// 用于指定当前方法对应的数据库操作类型 insert update
OperationType value();
📌 第一步:定义注解 @AutoFill
@Target(ElementType.METHOD) // 只能用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时保留,可通过反射读取
public @interface AutoFill {
OperationType value(); // 必须指定是 INSERT 还是 UPDATE
}
注意💡(关键点):
@Target(ElementType.METHOD):表示注解只能写在 方法 上。
@Retention(RetentionPolicy.RUNTIME):程序运行时可以通过反射拿到它(非常重要!否则切面无法识别)。
OperationType value():使用注解时必须传一个值,比如 @AutoFill(OperationType.INSERT)
2. 自定义切面类AutoFillAspect(AOP )
💡注意:这里使用到了AOP如果看不懂可以看我这篇文章:
package com.sky.aspect;
import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
/**
* 自定义切面类,用于公共字段自动填充处理逻辑
*/
@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("开始进行公共字段填充");
//获取当前被拦截的方法上的数据库操作类型
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) {
//为四个字段赋值
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) {
//为update_time和update_user字段赋值
try {
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
// 通过反射为对象属性赋值
setUpdateTime.invoke(entity, now);
setUpdateUser.invoke(entity, currentId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
🔍🔍🔍AutoFillAspect切面类:(拦截所有加了 @AutoFill 注解的 Mapper 方法,在执行前自动填充字段)
1️⃣ 定义切入点(Pointcut):
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){}
拦截 com.sky.mapper 包下 所有类的所有方法(.(…))并且这些方法上 必须有 @AutoFill 注解
2️⃣ 前置通知(Before Advice):真正干活的地方(这个代码好长下面分开慢慢解释)
💡注意:这里使用到了AOP如果看不懂可以看我这篇文章:
/**
* 前置通知实现方法,在通知中为公共字段赋值
*
*/
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint) {
log.info("开始进行公共字段填充");
//获取当前被拦截的方法上的数据库操作类型
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) {
//为四个字段赋值
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) {
//为update_time和update_user字段赋值
try {
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
// 通过反射为对象属性赋值
setUpdateTime.invoke(entity, now);
setUpdateUser.invoke(entity, currentId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 使用AOP拦截所有加了 @AutoFill 的 Mapper 方法,在方法执行 之前(@Before)自动填充字段
@Aspect
@Component
public class AutoFillAspect {
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){}
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint) { ... }
}
- 反射:因为我们知道都有 setCreateTime(LocalDateTime) 等方法,所以通过反射 动态调用 setter 方法,实现通用填充。
// 获取实体类的 setCreateTime 方法
Method setCreateTime = entity.getClass().getDeclaredMethod("setCreateTime", LocalDateTime.class);
// 调用该方法,传入 now
setCreateTime.invoke(entity, now);
更深入了解反射可以看看我写的这篇文章:链接: Java基础面试题(10)—Java(反射使用,常见应用场景)
3.OperationType枚举类型的使用
package com.sky.enumeration;
/**
* 数据库操作类型
*/
public enum OperationType {
/**
* 更新操作
*/
UPDATE,
/**
* 插入操作
*/
INSERT
}
枚举是一种特殊的数据类型,用于定义一组固定的常量。比 int 或 String 更安全、可读性强。
🧪 使用示例(新增员工)
这里EmployeeServiceImpl的save方法里的公共字段就可以注释掉了
/**
* 新增员工
* @param employeeDTO
*/
@Override
public void save(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
//对象属性拷贝
BeanUtils.copyProperties(employeeDTO, employee);
//设置账号状态
employee.setStatus(StatusConstant.ENABLE);
//设置默认密码
employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
//设置创建时间
// employee.setCreateTime(LocalDateTime.now());
// employee.setUpdateTime(LocalDateTime.now());
// //设置创建人
// employee.setCreateUser(BaseContext.getCurrentId());
// employee.setUpdateUser(BaseContext.getCurrentId());
employeeMapper.insert(employee);
}
直接在mapper层加上注解@AutoFill(value = OperationType.INSERT)就好了
/**
* 插入员工数据
* @param employee
*/
@Insert("insert into sky_take_out.employee (name,username, password, phone, sex, id_number, status, create_time, update_time, create_user, update_user) " +
"values" +
"(#{name}, #{username}, #{password}, #{phone}, #{sex}, #{idNumber}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
@AutoFill(value = OperationType.INSERT)
void insert(Employee employee);
还有update方法都可以加上这里就不一 一举例了
七,菜品开发
7.1 新增菜品
先来看看接口文档:
要插入这么多数据(菜品图片,口味,菜品的分类ID等):
7.1.1 文件上传,JavaIO,阿里云OSS插入菜品图片
✌️✌️✌️在新增菜品的功能中我们需要通过文件上传的功能来实现添加菜品的图片
因为是上传,首先还是先看一下JavaIO基础😎(这个面试虽然不常考,但还是了解一下)详细可以看我这篇文章: Java(IO操作文件内容,File管理文件/目录的元信息和结构)
JavaIO基础😎
📚IO 即 Input/Output,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出。
流的分类(IO 流有40 多个类,但都是从这几个基类中派生出来的):
| 类型 | 常见类 |
|---|---|
| 输入流(所有输入流基类) | InputStream(字节输入流), Reader(字符输入流) |
| 输出流(所有输出流基类) | OutputStream(字节输出流), Writer(字符输出流) |
🧐下面来看看几个简单的代码:
- InputStream(字节输入流):
//读取文件
FileInputStream fis = new FileInputStream("input.txt");
int data;
while ((data = fis.read()) != -1) { // 每次读一个字节
System.out.print((char) data); // 转成字符输出
}
fis.close();
- OutputStream(字节输出流)
//写入文件
FileOutputStream fos = new FileOutputStream("output.txt");
String text = "Hello Java IO!";
fos.write(text.getBytes()); // 把字符串转成字节写入文件
fos.close();
- Reader(字符输入流)
//读取文本
FileReader fr = new FileReader("input.txt");
int ch;
while ((ch = fr.read()) != -1) {
System.out.print((char) ch);
}
fr.close();
- Writer(字符输出流)
//写入文本
FileWriter fw = new FileWriter("output.txt");
fw.write("你好,Java IO!");
fw.close();
✨这里举个例子,主要是来看看JavaIO在具体项目中怎么用:
用它来实现文件上传最基本的功能,将接收到的文件保存在本地服务器的磁盘目录中了,客户端访问时可以显示出来
结合前端看看具体功能(提交姓名性别头像):

来看后端代码:
...
@Slf4j
@RestController
public class UploadController {
private static final String UPLOAD_DIR = "D:/images/";
/**
* 上传文件 - 参数名file
*/
@PostMapping("/upload")
public Result upload(MultipartFile file) throws Exception {
log.info("上传文件:{}, {}, {}", username, age, file);
if (!file.isEmpty()) {
// 生成唯一文件名(防止上传的文件名相同,后面上传的会覆盖前面文件)
String originalFilename = file.getOriginalFilename();
String extName = originalFilename.substring(originalFilename.lastIndexOf("."));
String uniqueFileName = UUID.randomUUID().toString().replace("-", "") + extName;
// 拼接完整的文件路径
File targetFile = new File(UPLOAD_DIR + uniqueFileName);
// 如果目标目录不存在,则创建它
if (!targetFile.getParentFile().exists()) {
targetFile.getParentFile().mkdirs();
}
// 保存文件
file.transferTo(targetFile);
}
return Result.success();
}
}
🙌其中有几个点不好理解(我让ai解释了一下):
1.MultipartFile:Spring中提供了一个API,使用这个API就可以来接收到上传的文件
String getOriginalFilename(); MultipartFile 常见方法,获取原始文件名
2.file.transferTo(…) InputStream 常用方法:作用是将上传的临时文件写入目标位置。
调用 transferTo() 方法后,Spring 会使用 Java I/O 流的方式将这个临时文件复制到指定的目标路径。
其本质是调用了类似下面的逻辑(真的很方便):
InputStream inputStream = file.getInputStream();
OutputStream outputStream = new FileOutputStream(targetFile);
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
上面说的是把文件存储在服务器短的C盘里,但在实际开发中,这样做是很不安全,容易丢失的,一般使用云服务阿里云OSS来存储。
🔍阿里云OSS
账号注册和开通阿里云账号就不说了可以看链接:黑马程序员的JavaWeb 这里只说说🙌苍穹外卖的后端文件上传代码实现:
主要是分为三步(视频里讲的不是很详细,但是之前学JavaWeb有讲过)我根据自己😎的理解分为三步来说:
1. 首先就是根据阿里云OSS官方所提供的sdk示例,改造引入阿里云OSS上传文件工具类以及引入依赖(这个视频配套资料都存在的)
可以查看一下自己idea上的package com.sky.utils和pom.xml文件:
<!--阿里云OSS依赖-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.17.4</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>
package com.sky.utils;
...
@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();
}
}
2. 配置参数,阿里云OSS上传文件工具类中生成的文件路径中的配置项中是需要我们改成自己的
//文件访问路径规则 https://BucketName.Endpoint/ObjectName
StringBuilder stringBuilder = new StringBuilder("https://");
stringBuilder
.append(bucketName)
.append(".")
.append(endpoint)
.append("/")
.append(objectName);
✅这几个值在你的阿里云上都可以找到:
endpoint:阿里云OSS中的bucket对应的域名
bucketName:Bucket名称
objectName:对象名称,在Bucket中存储的对象的名称
region:bucket所属区域
application.yml
alioss:
endpoint: ${sky.alioss.endpoint}
access-key-id: ${sky.alioss.access-key-id}
access-key-secret: ${sky.alioss.access-key-secret}
bucket-name: ${sky.alioss.bucket-name}
application-dev.yml 这个要自己填一下:
alioss:
bucket-name:
access-key-secret:
access-key-id:
endpoint:
package com.sky.config;
import com.sky.properties.AliOssProperties;
import com.sky.utils.AliOssUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 配置类用于创建阿里云OSSUtill对象
*/
@Configuration
@Slf4j
public class OssConfiguration {
@Bean
@ConditionalOnMissingBean
public AliOssUtil aliOssUtil (AliOssProperties aliOssProperties){
log.info("开始创建阿里云文件上传工具类对象 {}",aliOssProperties);
return new AliOssUtil(aliOssProperties.getEndpoint(),
aliOssProperties.getAccessKeyId(),
aliOssProperties.getAccessKeySecret(),
aliOssProperties.getBucketName());
}
}
注:
因为这个苍穹外卖项目课程是前几年的,所以代码不是很先进,想我学JavaWeb是的项目就不是这么配置的(实现功能都一样)。
本来还想解释一下每行代码的功能,有感觉没必要只要会用就行,知道干啥的就行(不过其中涉及到SpringBoot的原理配置感觉面试的时候会问,我以后再写🤪)
- 最后修改一下Controller层:让这一层调用阿里云OSS上传文件工具类上传文件到阿里云,并返回可以查看图片的阿里云URL:

单独新建一个类Controller类
package com.sky.controller.admin;
...
/**
* 通用接口
*/
@Slf4j
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
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();
//获取文件名后缀
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);
}
}
7.1.2 新增菜品和对应口味
在DishController:
/**
* 新增菜品
* @param dishDTO
* @return
*/
@PostMapping
@ApiOperation("新增菜品")
public Result save(@RequestBody DishDTO dishDTO){
log.info("新增菜品:{}", dishDTO);
dishService.saveWithFlavor(dishDTO);
return Result.success();
}
DishService接口:
/**
* 新增菜品和对应的口味数据
* @param dishDTO
*/
public void saveWithFlavor(DishDTO dishDTO);
/**
* 新增菜品,同时保存对应的口味数据
*
* @param dishDTO
*/
@Override
@Transactional
public void saveWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish);
//向菜品表插入一条数据
dishMapper.insert(dish);
// 获取insert的自增主键
Long dishId = dish.getId();
//向口味表插入n条数据
List<DishFlavor> flavors = dishDTO.getFlavors();
if (flavors != null && flavors.size() > 0) {
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishId);
});
//批量插入
dishFlavorMapper.insertBatch(flavors);
}
}
注意💡: 方法上要加@Transactional注解,启动类上添加@EnableTransactionManagement注解
因为方法执行了两个数据库操作:
- 插入一条菜品记录(Dish)到 dish 表;
- 根据传入的口味列表(flavors),批量插入多条记录到 dish_flavor 表。
如果其中任意一步失败,整个操作都应该回滚,避免出现 “有菜品但没有口味”或“口味指向一个不存在的菜品” 的脏数据,才加上注解的。
🙌🙌🙌@Transactional 的作用:
- 原子性(Atomicity):确保上述两个操作要么全部成功,要么全部失败。
- 一致性(Consistency):防止数据库处于中间不一致状态。
- 异常回滚:如果在插入口味时发生异常(比如数据库连接中断、违反约束等),Spring 会自动回滚之前插入的 dish 记录。
- 简化手动事务管理:不用自己写 try-catch + 手动 commit/rollback
DishMapper类,写入insert方法的代码(先插入菜品数据):
/**
* 插入菜品数据
* @param dish
*/
@AutoFill(value = OperationType.INSERT)
void insert(Dish dish);
哎呀,加上@AutoFill(value = OperationType.INSERT) 注解了,用到了前面的公共字段填充。
DishMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishMapper">
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into dish (name,category_id,price,image,description,status,create_time,update_time,create_user,update_user)
values (#{name},#{categoryId},#{price},#{image},#{description},#{status},#{createTime},#{updateTime},#{createUser},#{updateUser})
</insert>
</mapper>
DishFlavorMapper类(再插入口味数据):
/**
* 批量插入口味数据
* @param flavors
*/
void insertBatch(List<DishFlavor> flavors);
DishFlavorMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishFlavorMapper">
<insert id="insertBatch">
insert into dish_flavor (dish_id, name, value) values
<foreach collection="flavors" item="df" separator=",">
(#{df.dishId},#{df.name},#{df.value})
</foreach>
</insert>
</mapper>
7.2 菜品查询
7.2.1 分页查询
- DishController类:
/**
* 菜品分页查询
* @param dishPageQueryDTO
* @return
*/
@GetMapping("/page")
@ApiOperation("菜品分页查询")
public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){
log.info("菜品分页查询:{}", dishPageQueryDTO);
PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
return Result.success(pageResult);
}
- DishService 接口
/**
* 菜品分页查询
* @param dishPageQueryDTO
* @return
*/
PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);
- DishServiceImpl类
/**
* 菜品分页查询
*
* @param dishPageQueryDTO
* @return
*/
@Override
public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());
Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
return new PageResult(page.getTotal(), page.getResult());
}
- DishMapper
/**
* 分页查询
* @param dishPageQueryDTO
* @return
*/
Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);
- DishMapper.xml
<select id="pageQuery" resultType="com.sky.vo.DishVO">
select d.*,c.name as categoryName from dish d left join category c on d.category_id = c.id
<where>
<if test="name != null and name != ''">
and d.name like concat('%',#{name},'%')
</if>
<if test="categoryId != null">
and d.category_id = #{categoryId}
</if>
<if test="status != null">
and d.status = #{status}
</if>
</where>
order by d.create_time desc
</select>
7.2.2 根据Id查询菜品信息,用于数据回显
- DishController
/**
* 根据Id查询菜品信息,用于数据回显
* @param id
* @return
*/
@GetMapping("/{id}")
@ApiOperation("根据id查询菜品")
public Result<DishVO> getById(@PathVariable Long id){
log.info(" 根据id查询菜品:id {}", id);
DishVO dishVO = dishService.getByIdWithFlavors(id);
return Result.success(dishVO);
}
- DishService
/**
* 根据id查询菜品和对应的口味数据
* @param id
* @return
*/
DishVO getByIdWithFlavors(Long id);
- DishServiceImpl
/**
* 根据id查询菜品和对应的口味
*
* @param id
* @return
*/
@Override
public DishVO getByIdWithFlavors(Long id) {
//根据Id查询菜品数据
Dish dish = dishMapper.getById(id);
//根据Id查询口味数据
List<DishFlavor> dishflavors = dishFlavorMapper.getByDishId(id);
//组装返回结果
DishVO dishVO = new DishVO();
BeanUtils.copyProperties(dish, dishVO);
dishVO.setFlavors(dishflavors);
return dishVO;
}
- DishMappper
/**
* 根据id查询菜品和对应的口味数据
* @param id
* @return
*/
@Select("select * from dish where id = #{id}")
Dish getById(Long id);
- DishFlavorMapper
/**
* 根据菜品id查询对应的口味数据
* @param dishid
* @return
*/
@Select("select * from dish_flavor where dish_id = #{diahid}")
List<DishFlavor> getByDishId(Long dishid);
7.3 修改菜品
- DishController
/**
* 修改菜品
* @param dishDTO
* @return
*/
@PutMapping
@ApiOperation("修改菜品")
public Result update(@RequestBody DishDTO dishDTO){
log.info("修改菜品:{}", dishDTO);
dishService.updateWithFlavor(dishDTO);
//清理所有菜品缓存数据
clearCache("dish_*");
return Result.success();
}
- DishService
/**
* 修改菜品基本信息和口味数据
* @param dishDTO
*/
void updateWithFlavor(DishDTO dishDTO);
- DishServiceImpl
/**
* 修改id修改菜品基本信息和口味数据
*
* @param dishDTO
*/
@Override
public void updateWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish);
//修改菜品表基本信息
dishMapper.update(dish);
//删除原有的口味数据
dishFlavorMapper.deleteByDishId(dishDTO.getId());
//添加新的口味数据
List<DishFlavor> flavors = dishDTO.getFlavors();
if (flavors != null && flavors.size() > 0) {
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishDTO.getId());
});
//批量插入
dishFlavorMapper.insertBatch(flavors);
}
}
- dishMapper
/**
* 根据id动态修改菜品
* @param dish
*/
@AutoFill( value = OperationType.UPDATE)
void update(Dish dish);
- dishMapper.xml
<update id="update">
update dish
<set>
<if test="name != null">
name = #{name},
</if>
<if test="categoryId != null">
category_id = #{categoryId},
</if>
<if test="price != null">
price = #{price},
</if>
<if test="image != null">
image = #{image},
</if>
<if test="description != null">
description = #{description},
</if>
<if test="status != null">
status = #{status},
</if>
<if test="updateTime != null">
update_time = #{updateTime},
</if>
<if test="updateUser != null">
update_user = #{updateUser},
</if>
</set>
where id = #{id}
</update>
7.4 删除菜品
注⚠️: 这里主要是删除菜品的时候,连带着口味,套餐都要考虑到
- DishController类中添加如下代码:
/**
* 菜品批量删除
* @param ids
*/
@DeleteMapping
@ApiOperation("菜品批量删除")
public Result delete(@RequestParam List<Long> ids){
log.info(" 菜品批量删除:ids {}", ids);
dishService.deleteBatch(ids);
return Result.success();
}
- DishService类:
/**
* 菜品批量删除
* @param ids
*/
void deleteBatch(List<Long> ids);
- DishServiceImpl类:
/**
* 菜品批量删除
*
* @param ids
*/
@Transactional//添加事务保证代码的一致性
@Override
public void deleteBatch(List<Long> ids) {
//判断菜品是否可以删除,是否是起售中
for (Long id : ids) {
Dish dish = dishMapper.getById(id);
if (dish.getStatus() == StatusConstant.ENABLE) {
//菜品起售中,不能删除
throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
}
}
//判断菜品是否跟套餐关联,关联不能删除
List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
if (setmealIds != null && setmealIds.size() > 0) {
//菜品关联了套餐
throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
}
//删除菜品表中的菜品数据
for (Long id : ids) {
dishMapper.deleteById(id);
//删除菜品表中的口味数据
dishFlavorMapper.deleteByDishId(id);
}
}
- mapper层
DishMapper:
/**
* 根据ID删除菜品
* @param id
*/
@Delete("delete from dish where id = #{id}")
void deleteById(Long id);
DishFlavorMapper:
/**
* 根据菜品id删除对应的口味数据
* @param dishid
*/
@Delete("delete from dish_flavor where dish_id = #{diahid}")
void deleteByDishId(Long dishid);
🙌🙌🙌这里还可以优化一下,优化成批量删除
DishMapper
/**
* 批量删除菜品
* @param ids
*/
void deleteByIds(List<Long> ids);
DishMapper.xml
<delete id="deleteByIds">
delete from dish where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delete>
DishFlavorMapper
/**
* 根据菜品id批量删除对应的口味数据
* @param dishids
*/
void deleteByDishIds(List<Long> dishids);
DishFlavorMapper.xml
<delete id="deleteByDishIds">
delete from dish_flavor where dish_id in
<foreach collection="list" item="dishId" separator="," open="(" close=")">
#{dishId}
</foreach>
</delete>
批量删除Service就不用加上for循环了
/**
* 菜品批量删除
*
* @param ids
*/
@Transactional//添加事务保证代码的一致性
@Override
public void deleteBatch(List<Long> ids) {
//判断菜品是否可以删除,是否是起售中
for (Long id : ids) {
Dish dish = dishMapper.getById(id);
if (dish.getStatus() == StatusConstant.ENABLE) {
//菜品起售中,不能删除
throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
}
}
//判断菜品是否跟套餐关联,关联不能删除
List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
if (setmealIds != null && setmealIds.size() > 0) {
//菜品关联了套餐
throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
}
//根据菜品id集合批量删除菜品数据
dishMapper.deleteByIds(ids);
//删除菜品表和口味表关联的数据
dishFlavorMapper.deleteByDishIds(ids);
}
7.5 修改菜品状态
- DishController
/**
* 修改菜品状态
* @param status
* @param id
* @return
*/
@PostMapping("/status/{status}")
@ApiOperation("菜品的起售/禁用")
public Result startOrStop(@PathVariable Integer status, Long id) {
log.info("菜品的起售/禁用:{},{}", status,id);
dishService.startOrStop(status, id);
return Result.success();
}
- DishService接口
/**
*
* @param status
* @param id
*/
void startOrStop(Integer status, Long id);
- DishServiceImpl
@Override
public void startOrStop(Integer status, Long id) {
Dish dish = Dish.builder()
.status(status)
.id(id)
.build();
dishMapper.update(dish);
}
mapper层就是前面的update方法
OK啦!!! 菜品的功能就基本实现了
Redis
这个面试常考,我总结了一些面试题,可以看看这篇文章:
📚Redis基本概念
Redis 是一个开源的、基于内存的数据结构存储系统,它可以用作数据库(这个和Mysql差不多,存储的信息少些)、缓存和消息中间件。(🙌可以把它理解成一个“超级快的临时记事本”。它把数据存在内存里,所以读写速度非常快,常用于做缓存、排行榜,验证码存储等需要快速访问的场景。)
🌟Redis的存储
Redis存储是Key-value结构的数据,其中key是字符串类型,value有5中常用的数据类型:
- 字符串(String):就是普通字符串,(比如:用户登录后的 token、用户的昵称、访问次数(计数器)等)。
- 哈希(Hash):类似于对象,适合保存一个用户的多个信息(类似于Java中的HashMap结构,(比如:保存用户的信息(id、姓名、年龄等)在一个 key 里)。
- 列表(List):就是一个有序的字符串集合,可以在头尾添加/删除,(比如:消息队列、最近浏览记录)。
- 集合(Set):不重复的字符串集合,没有顺序,(抽奖活动中的唯一中奖用户、标签系统)。
- 有序集合(Sorted Set / ZSet):带分数排序的集合,可以按分数排名,(比如:游戏排行榜、热度榜单)。
我在黑马程序员讲义里截了张图,会更有助于理解这几种数据类型的存储结构

🔍Redis常用命令(注:不区分大小写)
Redis 字符串类型常用命令:
SET key value 设置指定key的值
Get Key 获取指定key的值
SETEX key seconds value 设置key的值,并把时间设置为seconds秒
SETNX key value key 不存在时设置key的值
Redis 哈希操作命令:
HASET key field value 将哈希表key 中的字段field的值设为value
HGET key field 获取存储在哈希表中指定字段的值
HGET key field 删除存储在哈希表中指定字段
HKEYS key 获取哈希表中所有字段
HVALS key 获取哈希表中所有值
Redis 列表操作命令:
LPUSH key value1 [value2] 将一个或多个值插入到列表头部
LRANGE key start stop 获取列表指定范围内的元素
RPOP key 移除并获取列表最后一个元素
LLEN key 获取列表长度
Redis集合操作命令
SADD key member1 [member2] 向集合添加一个或者多个成员
SMENMBER key 返回集合中的所有成员
SCARD key 获取集合的成员数
SINTER key1 [key2] 返回给定所有集合的交集
SUNION key1 [key2] 返回所有给定集合的并集
SREM key member1 [member2] 删除集合中一个或多个成员
Redis有序集合操作命令
ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员
ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合中指定区间内的成员
ZINCRBY key increment member 有序集合中对指定成员分数加上增量 increment
ZREM key member [member …] 移除有序集合中的一个或多个成员
Redis的通用命令:
KEYS pattern 查找所有符合给定模式的key
EXISTS key 检查给定key 是否存在
TYPE key 返回key所储存的值的类型
DEL key 该命令用于在key存在时删除key
✨后面我们的项目都是用Java写的,通过一个Java测试类,来看这些语句转成Java中怎么配置使用的(功能都是一样的,都是将数据储存在Redis中):
Java中操作Redis(入门)

1. 导入Maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2. 配置Redis数据源
在application-dev.yml配置
spring:
redis:
host: localhost
port: 6379
database: 0
在application.yml中配置引用
redis:
host: ${sky.redis.host}
port: ${sky.redis.port}
password: ${sky.redis.password}
database: ${sky.redis.database}
3. 编写配置类
config包下创建RedisConfiguration类
package com.sky.config;
import ...
@Configuration
@Slf4j
public class RedisConfiguration {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
log.info("开始创建RedisTemplate对象...");
RedisTemplate redisTemplate = new RedisTemplate();
//设置Redis连接工厂对象
redisTemplate.setConnectionFactory(redisConnectionFactory);
//设置Redis key的序列化器
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
4. 通过Redis Templet 操作对象操作Redis
@SpringBootTest
public class SpringDataRedisTest {
@Autowired
private RedisTemplate redisTemplate;
@Test
public void testRedisTemplate() {
System.out.println(redisTemplate);
ValueOperations valueOperations = redisTemplate.opsForValue();
HashOperations hashOperations = redisTemplate.opsForHash();
ListOperations listOperations =redisTemplate.opsForList();
SetOperations setOperations = redisTemplate.opsForSet();
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
}
/**
* 操作字符串类型的数据
*/
@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);
redisTemplate.opsForValue().setIfAbsent("lock", "1");
redisTemplate.opsForValue().setIfAbsent("lock", "2");
}
}
将代码运行,通过Redis图形化界面,可以清楚的看到数据在Redis中的存储状态
简单的学习过后,直接看Redis在苍穹外卖项目中的使用🧐:
首先来根据前端来看看具体要实现的功能
- 设置营业状态: 营业状态只有两种(营业和打样)存储的内容较少因此不用数据库Mysql存储:
- 查询营业状态: 不仅商家端要查, 顾客也要查

✅来看后端代码:
controller.admin.ShopController类:
package com.sky.controller.admin;
...
@RestController("adminShopController")
@RequestMapping("/admin/shop")
@Api(tags = "店铺相关接口")
@Slf4j
public class ShopController {
public static final String KEY = "SHOP_STATUS";
@Autowired
private RedisTemplate redisTemplate;
/**
* 设置营业状态
* @param status
* @return
*/
@PutMapping ("/{status}")
@ApiOperation("设置营业状态")
public Result setStatus(@PathVariable Integer status) {
log.info("查询店铺状态 :{}", status==1 ? "营业中" : "打烊中");
redisTemplate.opsForValue().set(KEY,status);
return Result.success();
}
/**
* 查询营业状态
* @return
*/
@GetMapping("/status")
@ApiOperation("查询营业状态")
public Result<Integer> getStatus() {
Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
log.info("查询店铺状态 :{}", status==1 ? "营业中" : "打烊中");
return Result.success(status);
}
}
controller.user.ShopController
package com.sky.controller.user;
import com.sky.result.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;
@RestController("userShopController")
@RequestMapping("/user/shop")
@Api(tags = "店铺相关接口")
@Slf4j
public class ShopController {
public static final String KEY = "SHOP_STATUS";
@Autowired
private RedisTemplate redisTemplate;
/**
* 查询营业状态
* @return
*/
@GetMapping("/status")
@ApiOperation("查询营业状态")
public Result<Integer> getStatus() {
Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
log.info("查询店铺状态 :{}", status==1 ? "营业中" : "打烊中");
return Result.success(status);
}
}
管理端的功能学习完之后,就进入到了用户端微信小程序的开发,先来看看小程序开发流程以及HttpClient在小程序开发中的运用。
微信小程序开发,HttpClient
🧩首先看一下小程序开发需要掌握的知识
微信小程序开发是一项非常实用的技能,尤其适合想从事前端开发、移动开发或想快速搭建轻量级应用的开发者。(从这个简介可以看出,微信小程序开发其实涉及了好多前端的内容,但是我前端学的不太好,这里就稍微了解一下,以后学好了再更,今天主要讲后端开发流程)
1.打开微信开发者工具(这个图标真的好可爱呀🥰🥰🥰🥰😍😍😍😍😗😗😗🤩🤩🤩🤩🤩🤩😽😽😽😍😍😍😍)
2.小程序目录结构: 如果学过一点前端的会感觉很熟悉,这个跟VSCode有点像:
项目结构:

app.js:全局逻辑
app.json:全局配置(页面路径、窗口样式、tab栏等)
app.wxss:全局样式

js:页面逻辑
json:页面配置
wxml:页面结构
wxss:页面样式表
🧠接下来从实现微信登录这个功能来了解小程序开发整个流程
首先还是从前端的界面来看看,接下来实现的功能:


点击确认后会弹出界面授权信息,点击允许从而创建用户信息登录
🔍登录功能流程图

大致可以概括为:
小程序用户点击登录按钮
↓
调用 wx.login() 获取临时登录凭证 code
↓
将 code 发送到你自己的服务器
↓
服务器用 code 向微信请求换取 openid 和 session_key
↓
服务器生成自己的 token(如 JWT)返回给小程序
↓
小程序保存 token,后续请求带上 token 做身份验证
⚠️注: 在第三步中 “服务器用 code 向微信请求换取 openid 和 session_key” 中我们的服务器需要向微信官方的服务器发送请求 “https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code” 这个功能是HttpClient实现的
🌐 什么是 HttpClient?
你可以把 HttpClient 想象成一个“会联网的浏览器”,但它不是给人用的,而是给程序(代码)用的。
它能让你的 Java 程序像浏览器一样访问网页、发送请求、获取数据。比如你打开淘宝,其实是浏览器在背后做了这些事,而 HttpClient 就是让你的 Java 程序也能做类似的事情。
在Java中通过编码的方式发送HTTP请求:
1.maven:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
2.在key-server/src/test/java/com/sky/test下面创建HttpClientTes测试类通过HttpClient发GET请求
public class HttpClientTest{
@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();
}
}
3.测试通过httpclient发送Post方式请求
@Test
public void testPost() throws Exception {
//创建httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建请求对象
HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");
//创建Json参数
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();
}
}
✨了解完HttpClient,我们继续来看登录功能的代码(顺便来看看HttpClient在项目中的使用)
🥰准备工作:
安装依赖配置:
applicaton-dev.yml:
sky:
wechat:
appid: ...
secret: ...
application.yml:
sky:
wechat:
appid: ${sky.wechat.appit}
secret: ${sky.wechat.secret}
//还要配置jwy,在是sky:jwt:上加上
# 设置前端传递过来的令牌名称
admin-token-name: token
user-secret-key: itcast
user-ttl: 7200000
user-token-name: authentication
🙌代码:
Controller:
package com.sky.controller.user;
...
@RestController
@RequestMapping("/user/user")
@Api(tags = "C端用户接口")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@Autowired
private JwtProperties jwtProperties;
/**
* 微信登录
*/
@RequestMapping("/login")
@ApiOperation("微信登录")
public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO){
log.info("用户登录:{}", userLoginDTO);
// 调用service完成微信登录
User user = userService.weixinlogin(userLoginDTO);
//为微信用户生成JWT令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.USER_ID, user.getId());
String token =JwtUtil.createJWT(jwtProperties.getUserSecretKey(), jwtProperties.getUserTtl(), claims);
UserLoginVO userLoginVO = UserLoginVO.builder()
.id(user.getId())
.openid(user.getOpenid())
.token(token)
.build();
return Result.success(userLoginVO);
}
}
service:
public interface UserService {
//微信登录
User wxLogin(UserLoginDTO userLoginDTO);
}
UserServiceImpl:
package com.sky.service.impl;
...
@Service
@Slf4j
public class UserServiceImpl implements UserService {
//微信公众号的接口地址
public static final String WX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session";
@Autowired
private WeChatProperties weChatProperties;
@Autowired
private UserMapper userMapper;
/**
* 微信登录
* @param userLoginDTO
* @return
*/
@Override
public User weixinlogin(UserLoginDTO userLoginDTO) {
//调用微信接口服务,获取用户的openid
String openid =getOpenid(userLoginDTO.getCode());
//判断当前用户是否为为空,为空表示登陆失败,抛出异常
if (openid == null) {
throw new RuntimeException(MessageConstant.LOGIN_FAILED);
}
//判断不为空,判断是否为新用户
User user = userMapper.getByOpenid(openid);
// 是则自动完成注册
if (user == null) {
user = User.builder()
.openid(openid)
.createTime(LocalDateTime.now())
.build();
userMapper.insert(user);
}
//返回用户对象
return user;
}
/**
* 获取微信用户openid
* @param code
* @return
*/
private String getOpenid(String code) {
//调用微信接口服务,获取用户的openid
HashMap<String, String> map = new HashMap<>();
map.put("appid",weChatProperties.getAppid());
map.put("secret",weChatProperties.getSecret());
map.put("js_code", code);
map.put("grant_type","authorization_code");
String json =HttpClientUtil.doGet(WX_LOGIN, map);
log.info("微信接口返回的json数据:{}",json);
JSONObject jsonObject = JSON.parseObject(json);
String openid = jsonObject.getString("openid");
return openid;
}
}
UserMapper:
package com.sky.mapper;
@Mapper
public interface UserMapper {
/**
* 根据OpenId查询用户
* @param
* @return
*/
@Select("select * from user where openid = #{openid}")
User getByOpenid(String openid);
/**
* 自动完成注册
* @param user
*/
void insert(User user);
}
UserMapper.XML
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.UserMapper">
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into user(openid,name,phone,sex,id_number,avatar,create_time)
values(#{openid},#{name},#{phone},#{sex},#{idNumber},#{avatar},#{createTime})
</insert>
</mapper>
小白啊!!!写的不好轻喷啊🤯如果觉得写的不好,点个赞吧🤪(批评是我写作的动力)
…。。。。。。。。。。。…
…。。。。。。。。。。。…
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)