这是任务要求(飞书文档):https://my.feishu.cn/wiki/QYdkw1AMqi2fsokXVSXcnRyZnyc?from=from_copylink
📑 目录导航
- 8.14 - 项目初始化与基础框架搭建
- A. 学生注册
- 8.15 - 注册功能完善与密码加密
- B. 登录
- 8.16 - JWT登录与拦截器
- C. 教师注册
- D. 统一JSON返回值格式
- E. 业务异常与全局异常处理
- F. 修改个人信息
- G. 修改密码
- 8.17 - 管理员功能
- H. 管理员
- I. 做EXCEL导入
- 8.18 - 资源模块开始
- A. 建立资料库
- B. 上传文件的数据库记录
- 8.19 - 资源查询与分页
- C. 文件上传并和资源记录关联
- D. 文件类型校验
- E. 用户重新上传资源和删除资源
- 8.20 - 文件下载与收藏功能
- F. 文件下载
- G. 资源收藏和取消收藏
- H. Swagger API文档
- I. 日志Slf4j
花了十天左右把Java的基础语法过了一遍(所以我已经下过JDK和IDEA了),springboot,mysql,maven啥啥的这些我没听说过的东西打算边做边学,做一点学一点。(之前学的在我的Java学习笔记里面,已发布)
8.14
首先,在IDEA中创建SpringBoot+Maven项目,新建 HelloController,启动项目,在浏览器访问接口并看到 Hello QingZhi。(注意controller为根包的子包)
controller的核心职责是前后端的通信桥梁:接收前端发来的 HTTP 请求,调用后端业务逻辑处理(可以调用service),然后把处理结果返回给前端。调用业务层。
然后安装MySQL,安装成功后在cmd里输入mysql -u root -p,如果让输入密码就没问题了,如果说文件找不到,就是MySQL 的安装路径没有添加到 Windows 的环境变量中,问问AI把它添加一下就欧克了。
在windows里面打开mysql自带的客户端,创建 qingzhi 数据库和第一张 user 表,注意中文兼容性和账号UNIQUE,账号密码NOT NULL
下一步:让 Spring Boot 成功连接 MySQL——去IDEA里在pom.xml里面加入mysql驱动依赖,并在核心配置文件application.properties里进行配置。
如此便打通了:浏览器 → Spring Boot → MySQL通路
接下来尝试在MYSQL里面建立一个学生试试看
然后建立一个entity包,存放所有数据库相关的类,这里有一个小技巧:写好private的诸多属性后,快捷键Alt+insert可以自动补充对应的setter和getter方法
创建一个 mapper 包,存放数据库的各种接口。里面定义继承BaseMapper的接口,这样就能直接调用MyBatis-Plus 已经帮我们写好的操作 User 的常用数据库方法。
紧接着创建测试接口,测试成功,这样最基础的框架就搭建好了。(存用户数据的地方,处理用户的方法)
开始写功能:
A.学生注册(先实现写入,再加密码加密)
此功能的实现用了Service层和Controller层的各一个文件来实现,authcontroller文件负责接受并转换前端传来的信息,并传入authservice方法;而在service层分工:authservice方法负责注册的业务逻辑(调用usermapper),mapper专管把数据传入数据库,userMapper.insert(user) //MyBatis-Plus 会根据 User 实体上的注解,自动生成 INSERT 语句,把数据插入到数据库里面。
注:MyBatis-Plus配置好了会自动调用,不用再引入或者@什么的提示
@RestController:标记为“API 控制器”:
1.标记为控制器:让 Spring 在启动时扫描到这个类,并把它实例化成 Bean,托管到 Spring 容器中。
2.自动返回 JSON:这个注解包含了 @ResponseBody,也就是说,方法里返回的 Java 对象(如 User、Map)会被 Spring Boot 自动转换成 JSON格式的响应体,直接返回给前端。
3.处理 HTTP 请求:配合方法上的 @PostMapping 等注解,就能接收来自客户端的请求。
@RequestBody:可以自动把 JSON 转成 User 对象,示例:public String registerStudent(@RequestBody User user) {}
@service:在service下的文件中,在类的声明前进行注释,用于标记这是一个 Service 层的 Bean,Spring 识别他才会扫描并创建 AuthService 的实例,进而让controller需要注入数据的时候能找到它
写好之后用postman(建议安装一个客户端,别用网页版!!!)测试一下http传输,再去MySQL里看看传进去没有(在命令行里写SELECT * FROM user;),测试成功就ok了。
8.15
下一步加一个业务规则:注册时先检查学号是否已经存在。
标准写法:
// 构建查询数据库需要用的条件(是eq(“account”,user.getaccount)的安全加强版
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getAccount, user.getAccount());
// 按wrapper条件在数据库里面查询信息,若单一则返回对象实体。如果查询到多条数据,会抛出异常;如果查不到,返回 null。
User existingUser = userMapper.selectOne(wrapper);
//其中:wrapper 的数据类型是 LambdaQueryWrapper,这是 MyBatis-Plus 框架提供的一个查询条件构造器类,<>指要操作的是user类型的实体。
注:查询账号用 LambdaQueryWrapper:是因为账号在数据库里,你必须通过条件去硬盘上把它“拽”出来。
而匹配密码直接用 matches( password,user.getPassword())是因为密码(密文)已经被“拽”出来了(在 user 对象里),现在只需要在内存里进行数学运算比对。
注意改完service层之后记得随之改controller层的return。
然后加密码:先在service逻辑里加按密码要求判断,然后把密码从明文存储改成 BCrypt 加密存储:
这里先加一个加密工具的依赖,然后要创建配置类(config)
@Bean:告诉 Spring:这个方法返回的对象需要注册到 Spring 容器中,之后可以就被其他类直接拿来用。
在service层新建密码对象,在insert操作之前加一步加密。这样再注册信息后MySQL里面密码就不显示明文而是乱码了。
!!!注意理解一下在整个过程中密码经历了什么:(整条数据流如下)
① 用户输入
“abc12345”
↓
② HTTP JSON
{
“password”: “abc12345”
}
↓
③ Spring @RequestBody
User user
password = “abc12345”
↓
④ Controller
authService.registerStudent(user)
↓
⑤ Service
user.getPassword()
→ “abc12345”
↓
⑥ BCrypt
encodedPassword
→ “$2a10 1010xxxxx…”
↓
⑦ setPassword (这里并非重新创建一个user而是直接在其基础上进行修改)
user.password
→ “$2a10 1010xxxxx…”
↓
⑧ Mapper
userMapper.insert(user) (也就是说数据库从没见过真正的密码)
↓
⑨ MySQL
password
→ “$2a10 1010xxxxx…”
B登录:
Go on!开始写我们的登录操作: JWT(登录成功后服务器发给用户的临时通行证【一串加密的字符串】——token)
先载入几个JWT的依赖
然后写Util 工具类,这里主要写的是依据用户提供的信息和我们自定义的类密钥加密生成独一无二的token
为什么不把注册功能写成工具类而要把登录功能写成工具类?
答:因为登录功能中有"生成Token"这个通用操作,而注册功能中没有类似的可复用操作。(核心操作:验证身份 → 生成 Token)
PS:(这里语法有点忘了,再复习一下)就是SecretKey KEY 接口名+实例名创建的是其实现类的引用变量
这一块没看懂没看懂没看懂。。。可以参考B站上视频【【极简入门】15分钟学会JWT的使用】https://www.bilibili.com/video/BV1cK4y197EM?vd_source=a46d0971e74a0a8fe32a8fef61e60678
哦!懂了,要先定义一个服务于整个类的公用key,持有key的人才能签发有效的token,那些加密文字都是结合这个自定义的key用算法算出来的,然后在写带个人信息生成token的Jwts.builder()
然后service层controller层也都要做对应修改。
8.16
下一步做 JWT 校验 + 登录拦截器(验token):
先给 JwtUtil 增加两个方法:解析 token 和 判断 token 是否有效
(防篡改防过期机制)先用Jwts.parser()方法得到claims(传入token通过特定方法得到当时输入的键值对集合),只有没有过期,格式正确,未被篡改的才能通过,不能被解码的则在执行该方法时异常,用try-catch抛出并返回false。(这个写在util工具类里面)
接下来写拦截器,新创建interceptor类,利用HandlerInterceptor接口封装好的功能先获取token,先看是否为null以及前缀对不对,然后调用util写好的防伪判断方法进行检验,最终返回True和False决定能不能进入controller
@Component:把当前类注册为 Spring 容器的一个 Bean(组件)(因为后面要在配置类中用 @Autowired 注入这个拦截器,如果没加 @Component,Spring 就找不到它)
最后在config里面加一个WebConfig用来设定所有请求都要先经过interceptor的校验,并设置了几个豁免口,比如登录注册啥的
接下来在controller里面加一个临时接口试试未登录的拦截功能,登陆后再访问要带着登录返回的token,去临时接口网页,key写Authorization,value写token(Bearer 刚刚返回的那一串),跑通了ok!
最后一步:从 token 里取出当前登录用户的 userId,让后端知道“现在是谁在访问”。
这个在拦截器里面写,因为是每次http请求都自己解码一次,然后暂存在给这次HTTP请求分配的HttpServletRequest 对象里面。以供后续controller啥的使用。
最后最后还有一点:在拦截器里写的request.setAttribute,写对应的request.getAttribute,让暂存的id信息能返回前端。
可以回到entity里面把password的声明前面加上一个@JsonIgnore(在把 Java 对象转换成 JSON 时,忽略被标注的字段)——防止敏感信息通过接口泄露给前端。即使 BCrypt 密文不能直接还原,也不应该把密码哈希返回出去。
由此,我的JWT登录功能就写完了!!棒棒哒~
C.教师注册
这一块和学生注册重复的逻辑很多,所以主要涉及代码复用的问题。
思路是:把把注册核心逻辑(账号查重,验证密码,加密)抽成一个私有方法,然后写非常简单的registerStudent()和registerTeacher(),再去去AuthController 增加教师注册接口。
D.把接口返回值统一成 JSON 格式
把状态码(1)、成功消息(“Success!”)和业务数据(data)打包成一个 Result 对象返回。
!!这里result写两遍构造方法,一遍有参,下面写success、error方法时用,一遍无参,支持手动创建+setter以及给 Jackson、MyBatis 等框架在反序列化时使用——很多框架的实现依赖无参构造方法,所以只需养成习惯以后写含参构造方法时都一起写上一个无参构造方法。
无参可传可以写null
为了给开发者提供两种灵活的使用方式,写两个success方法——一种用默认的"Success!"消息,一种用自定义的成功消息。(一种只含data一个参,一种还含一个String message。这样用默认省事且工整,自定义信息的就可以在注册成功,收藏成功,登录成功,修改成功等诸多场景下复用,省代码。
E.业务异常 + 全局异常处理
这一部分在exception包里面写,先写一个
@ExceptionHandler当一个 Controller 方法抛出异常时,由被 @ExceptionHandler 标记的方法来“接住”这个异常,并返回一个友好的响应给前端(就是我们写好的rsult规范格式里面的error()),而不是直接把错误堆栈信息抛给用户。
接下来就是把注册登录接口也改成统一 Result + 异常处理
改service层之前在登录注册等等遇到异常只返回一个String正常运行的那里,那样容易出事;改成一个异常处理,就直接throw出来,强制停止再返回一个标准报错信息error方法。
authcontroller里面的注册方法也可以只留下sucess的返回了,因为error已经在service层就已经被抛出并返回信息了。登录同理。并把返回值都改成result里面的标准格式,比如:
String token = authService.login(account, password);
return Result.success(token);
这样重新分工:
Controller只负责:接请求 + 返回成功结果
Service只负责:业务判断出错则throw BusinessException、GlobalExceptionHandler统一处理错误,不负责注册成功的情况,成功全权交给controller处理。
F.修改个人信息
目标:用户只能改自己的信息,不能通过前端传 id 去改别人。且修改个人信息时不允许修改account password role
1.在service层定义一个含参——id的更新方法,然后在controller里面的更新方法:先取自己request里暂存的拦截器解码出来的id,然后调用更新时强制参数为自己的id,这样就能防他人篡改。
2.不允许修改的直接这样处理: user.setAccount(null);
至于更新操作,先用user.setId(userId);锁定对象,然后直接用现成的:updateById(user):MyBatis-Plus 提供的一个智能更新方法。
优化:之前为了快速写 /users/me,在 Controller 里直接用了:userMapper.selectById(userId);,但是现在写好了UserService,UserController 其实不应该再直接持有 UserMapper,而是只对接UserService。这样在UserService最后再加一个以userid为参,能返回user的方法。
原则:Controller 调 Service,Service 调 Mapper。Controller 通常不要直接操作 Mapper。
注意咱的核心功能是在service层的一个类里面写的,想调用其中方法就得依托人家的对象,所以在controller里面要先声明一个userservice对象,同样,userservice类里面也要声明一个usermapper对象来连上人家。
G.修改密码
这个用Map和键值对来完成:
(参数)@RequestBody Map<String, String> body
String oldPassword = body.get(“oldPassword”);
String newPassword = body.get(“newPassword”);
userService.changePassword(userId, oldPassword, newPassword);
因为要验证旧密码、加密新密码,所以 UserService 也需要 BCryptPasswordEncoder。
8.17
H.管理员
让系统里存在一个固定管理员账号,具有用户管理权限。
先查询管理员,用这一套:
LambdaQueryWrapper wrapper =new LambdaQueryWrapper<>();
wrapper.eq(User::getAccount, “Admin”);
User admin = userMapper.selectOne(wrapper); //零条返回null,一条返回user对象,多条抛出异常
没有的话就创建,初始化管理员账户应该先写死一个初始化密码,管理员不能自己注册,所以先按这个登进去之后可以再连接口修改密码。
当类继承了CommandLineRunner接口表明Spring每启动一次,run都会自动执行。(所以才要先判断有没有,再插入管理员)
然后写管理员权限校验:(目标:普通用户访问 /admin/** 返回“无权限”;管理员访问则放行)
这个从拦截器里调出role,并在拦截器里加一个管理员路径判断,测试没问题
接下来我们要做管理员crud——管理员操作在逻辑上和用户操作是一样的,地位也是平等的。所以就另设一个AdminUserController和AdminUserService(为了省事,我把这个放在userservice里面了,没有单独拎出来,因为那里局部变量的声明是全的)
管理员查询用户信息、管理员新增用户、管理员修改用户信息、管理员删除用户、重置密码
把“查询自己发布资源”和“收藏列表”先预留接口不实现,等资源表建好后补;
I.做EXCEL导入
导入操作属于数据库操作,在entity层。
暂定excel格式:
账号 | 姓名 | 手机号 | 邮箱 | 院系 | 专业 | 角色 | 初始密码
其中角色:STUDENT/TEACHER
public class UserImportRow {
@ExcelProperty(“账号”)
private String account;
public String getAccount() {return account; }
public void setAccount(String account) {this.account = account;}
}
下一步给 Excel 导入补必填校验 + 账号重复校验 + 密码校验 + 角色校验(校验统统在service层)也写在userservice里面。
for (int i = 0; i < rows.size(); i++) {
UserImportRow row = rows.get(i);
// Excel 第一行是表头,所以数据第1行实际是 Excel 第2行
int excelRow = i + 2;
if (row.getAccount() == null || row.getAccount().isBlank()) {
throw new BusinessException(“第” + excelRow + “行:账号不能为空”);
}
}
@Transactional(rollbackFor = Exception.class)(事务回滚):“自动回滚整个数据库操作” = 把这一系列数据库操作当作一个整体,只要有一步失败,前面所有已执行的操作全部撤销,恢复到方法执行前的状态。——确保数据一致性
手机号 / 邮箱格式校验先不做了,假的就假的吧。
——————————————————————————————用户模块完结,资源模块开始
8.18
A.建立资料库,
资料库建立和之前存用户的是一样的,之前是qingzhi里面的user表,现在新建一个resource表,仍是声明表头及填入方式限制等,然后在entity里面建的文件就放一堆变量的声明和setter getter,再让主代码连数据库,建新的mapper则是定义一个继承了BaseMapper,操作表的接口,MyBatis-Plus已经帮我们写好了各种操作。
B.上传文件的数据库记录(纯资源记录 CRUD + 审核状态流转做)
接下来就是进service层和controller层写功能了:service层就是先声明局部变量mapper然后在一堆判断之后insert的方法:上传资源、查询资源、修改自己上传的资源、删除自己上传的资源、已审核未审核资源的可见性、管理员资源审核
分页展示:用MyBatis-Plus的分页插件(在config里面进行配置)
MybatisPlusInterceptor :MyBatis-Plus 的拦截器(插件核心),所有增强功能都在它上面添加。
interceptor.addInnerInterceptor(…) 往拦截器里添加一个内部插件
new PaginationInnerInterceptor(DbType.MYSQL) 创建分页插件,并指定数据库类型为 MySQL(不同数据库的分页语法不同)。
return interceptor; 把配置好的拦截器交给 Spring 管理。
再把ResourceService改成带分页的——先创建一个分页对象Page pageObject =new Page<>(page, pageSize);规定了页码和每页几条。然后正常查询、筛选approved及按时间排序,最后返回resourceMapper.selectPage(pageObject, wrapper);会自动生成分页MySQL。在controller层把那个page和pagesize用@RequestParam(defaultValue = “1”) long page,@RequestParam(defaultValue = “10”) long pageSize放在参数里。
8.19
按发布时间范围查询 + 时间倒序 :
添加参数:@RequestParam(required = false) LocalDate startDate和…
@RequestParam 告诉 Spring:“这个参数的值,需要从 URL 的查询参数中提取”
required = false 这个参数不是必须的,如果 URL 里没有这个参数,Spring 会传 null 进来(而不会报 400 错误)
LocalDate startDate 参数名称是 startDate,类型是 LocalDate(Java 8 引入的日期类型,只有年月日,没有时分秒)
在servicce层也改:如果传了startday如果传了endday,用ge(>=),lt(<=),如下:
if (startDate != null) {
wrapper.ge(
Resource::getCreatedAt,
startDate.atStartOfDay()
);
}
日期格式要求:yyyy-MM-dd
C.文件上传并和资源记录关联
在controller层仅需加上一个 @RequestParam(“file”) MultipartFile file) 参数
在service层:大小检查、防重名覆盖(UUID+originalname)然后创建一个 File 对象,指向一个叫 uploads 的文件夹(路径是相对于当前运行目录的,即项目启动的位置)。但是有可能还没有这个文件夹,if (!uploadDir.exists()) { uploadDir.mkdirs();},在后面用这个检验然后再创建也来得及。然后:
try {
file.transferTo(destination);
} catch (IOException e) {
throw new BusinessException(“文件保存失败”);
} 用这个写入,并抛出异常。
最后就再存数据库信息。
D.文件类型校验
先判断格式对不对,然后取.后的部分,建立一个允许的后缀库,然后据此判断后缀是否合法,就行了
语法:
.toLowerCase();——把字符串里的所有英文字母,全部转换成小写——做字符串适配时若不在意大小写应该先全部转为大写或小写。
字符串操作substring:取…之后的字符串,例如"unhappy".substring(2) returns “happy”
E.用户重新上传资源和删除资源(把update也接入文件操作)
规则:修改资源时文件可选。没传文件就保留旧文件;传了新文件就保存新文件、更新数据库,成功后再删除旧文件。
这一块优化一下,不再一个一个写了,而是把文件和数据库操作隔离,先写一个存文件(savefile)和删除文件的方法,然后接入资源create、update、delete方法中
理解File类型:File 对象本身 不是 物理文件,它在 Java 中只是一个 “路径名的抽象表示”,可以理解为一张写有地址的“便签”或“指针”。
savefile:进行一系列合规检查后,创建唯一文件名,创建文件存储目录(创建失败且无此文件时抛出异常),用File destination =new File(uploadDir, storedFilename);创建一个指向具体文件的 File 对象,把文件真正存入硬盘的操作file.transferTo(destination);放入try-catch里面,最终应该返回return destination.getPath();路径。
deletefile:删文件是用path删掉的:
private void deleteFile(String filePath) {
if (filePath == null || filePath.isBlank()) {
return;
}
File file = new File(filePath);
if (file.exists()) {
boolean deleted = file.delete();
if (!deleted) {
System.out.println(
“文件删除失败:” + filePath
);
}
}
}
8.20
F.文件下载
先用token校验用户,以及查询要下载的文件是否存在(resource里面找一遍file里面找一遍)。
org.springframework.core.io.Resource fileResource = new FileSystemResource(file);把硬盘上的一个物理文件,包装成一个 Spring 框架可以统一处理的“资源对象”。
处理一下文件名中文兼容性:
String fileName = resource.getFileName();
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replaceAll(“\+”, “%20”);
然后是返回文件流:(告诉浏览器弹出下载框)八股:
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, “attachment; filename=”" + encodedFileName + “”")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(fileResource);
G.资源收藏和取消收藏(没收藏就收藏,收藏了就取消收藏)
就是单独建立一个表放收藏的资源
在entity中建立收藏列表,用的表名与类名相同(只有首字母大小写不同)时,可以不加@TableName(“user_favorite”)来说明该实体类和数据库中哪个表关联,但是这里显然类名表名不统一所以必须要加。
H.Swagger
Swagger: API 文档生成工具,通过在代码里加注解,自动生成可视化、可在线调试的 API 文档,彻底告别手写文档,保证代码和文档永远同步。(这个文档是给前端看的)
首先加入 SpringDoc 的 Starter 依赖,
添加基础配置(可选,用于定制文档标题与描述)——写在application.properties
在 Controller 中加上友好的注解(可选)——为了让生成的接口文档更加清晰规范,可以在你的 ResourceController 或实体类上适当加一些 OpenAPI 注解,如:@Tag(name = “资源管理模块”, description = “资源的上传、下载、查询、修改与删除”)
别忘了写config配置文件以及把这个http从webconfig的登陆名单里加上豁免金牌。
http://localhost:8080/swagger-ui/index.html浏览器访问这个就可获得漂亮的可视化页面:
I.日志Slf4j
就是在需要日志的地方在return之前加一个log,让黑盒变成明盒。(这也是一个写好的功能,加一个依赖一个import再加一个@Slf4j就行了)
在exception、service层、JWT拦截器加(一定要放在return之前,放在return后面永远都执行不了)
然后改controller层
Service:记录业务结果(上传成功、删除成功)
Controller:记录用户请求入口
所以 Controller 不要大量加日志,否则一个页面查询会产生大量无意义日志。所以只记录发送请求不记录成功。
最后还需要在application.properties里面配置日志输出文件就能让 Spring Boot 把日志保存到文件了