针对初学者最佳学习路线:先拉取本Java Spring boot 项目,能运行起来,然后照着本篇文档一步步实现:
1、学习Mysql数据库,了解sql语法,了解索引。
2、创建一个空的Java Spring boot 项目。
3、拉取我的项目,对着文档一步步敲一遍。
-
登录生成token
-
圆形验证码生成
-
登录拦截器实现、API加白
-
Lombok 注解的应用
-
统一Response返回格式
-
统一异常处理 全局错误拦截器
-
MyBatisPlus 集成使用
-
字段注解检验
-
用户表的增删改查的实现
-
文件上传的处理
经过近2周利用空闲时间又学习Java Spring boot
,基本算是入门了。至少能实现JWT接口拦截、API加白放行、圆形验证码存储session验证是否正确、文件导入、表的增删改查 等等功能,技术栈:jdk23 + mybatisplus + lombok + mysql
。
博主本人在最原来写过一段时间PHP
(现在基本忘记完了),对mysql
数据库停留在会用的阶段。也算是熟悉nodejs
,使用nodejs
的express
框架写过一些demo接口
,Pm2
管理进程这些,也使用过eggjs、nestjs
这些nodejs
的框架,但是也就停留在会用阶段,不能说深入吧。
说下为啥我又突然想学习Java
了呢,是的我在上篇文章中接到了一个朋友的私活前端(两个小程序+pc管理端),后面就想变成全栈 ,接更多的私活用!!!
我在去年12月份 的时候,跟着 韩顺平 零基础30天学会Java 敲了点java
代码,韩顺平老师 讲的太细了 ,真的特别适合老手 也来巩固学习,有点类似前端的重学前端 ,各种原理细节分析 ,900 多章,我看了450来章 (也有快进,但是没有跳章),后面因为一些事情也就搁置了。也算是了解了基础语法以及应用。 可能等我在写一段时间java
我会重新看一遍。
既然要写java
了,数据库的表设计要学习下吧,还有索引字段,我在掘金上找了几篇关于mysql索引的文章,(四)MySQL之索引初识篇:索引机制、索引分类、索引使用与管理综述 深入学习了下。初步了解到了索引的作用,数据在多的时候,建立索引能更快的查询到数据。索引建立就像是中华字典的目录 ,可以通过建立索引直接翻到该页码,当然更快了。常见的索引有:主键索引、全文索引、联合索引、前缀索引、唯一索引等等
,到这里我觉得我的mysql
就够用了,等我真处理千万级别的数据再仔细研究具体方案。
我又找时间把我们上个洗车应用的后台管理的数据表给设计了一下,先看功能如下(没有基础的可以直接略过看下面哈):
-
- 登录模块 (若依)
-
- 用户角色模块、字典、部门(若依)
-
- 订单管理
- 3.1 订单列表
- 3.2 美团核销
- 3.3 抖音核销
-
- 会员管理
- 4.1 会员列表
-
- 营销管理
- 5.1 充值规则
-
- 门店管理
- 6.1 门店列表
- 6.2 股东提现记录
- 6.3 门店未消费余额
-
- 分润管理
- 7.1 分润配置
- 7.2 分润记录
- 7.3 银行卡信息
-
- 设备管理 (门店的洗车24小时设备)
然后我的表设计如下:
sql
-- 会员用户表
CREATE TABLE member_user (
user_id CHAR(255) PRIMARY KEY COMMENT '会员用户ID',
user_name CHAR(255) NOT NULL COMMENT '用户名',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
branch_id CHAR(255) COMMENT '归属网点ID',
source ENUM('0', '1', '2') DEFAULT '0' NOT NULL COMMENT '用户来源:小程序注册、门店扫码、系统导入',
unused_recharge_money DECIMAL(10,2) DEFAULT 0.00 NOT NULL COMMENT '剩余充值金额',
unused_recharge_gift_money DECIMAL(10,2) DEFAULT 0.00 NOT NULL COMMENT '剩余充值赠送金额',
-- 积分
FoREIGN KEY (branch_id) REFERENCES branch(branch_id),
)
-- 订单记录表
CREATE TABLE user_recharge_money (
uuid CHAR(255) PRIMARY KEY COMMENT 'UUID',
user_id CHAR(255) COMMENT '会员用户ID',
recharge_money DECIMAL(10,2) DEFAULT 0.00 NOT NULL COMMENT '充值金额',
recharge_gift_money DECIMAL(10,2) DEFAULT 0.00 NOT NULL COMMENT '充值赠送金额',
--字段: 充值前金额、充值前赠送金额、充值后金额、充值后赠送金额
--字段: 消费前金额、消费前赠送金额、消费后金额、消费后赠送金额
--字段: 退款金额、退款赠送金额
order_type ENUM('0', '1', '2',) DEFAULT '0' NOT NULL COMMENT '订单类型:充值、消费、退款',
branch_id CHAR(255) PRIMARY KEY COMMENT '网点ID',
FOREIGN KEY (user_id) REFERENCES member_user(user_id)
);
-- 设备表
CREATE TABLE device {
uuid CHAR(255) PRIMARY KEY COMMENT '设备ID',
name CHAR(255) NOT NULL COMMENT '设备名称',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
branch_id CHAR(255) NOT NULL COMMENT '网点ID',
-- 设备其他信息
FoREIGN KEY (branch_id) REFERENCES branch(branch_id)
}
-- 网点表
CREATE TABLE branch {
branch_id CHAR(255) PRIMARY KEY COMMENT '网点ID',
branch_name CHAR(255) NOT NULL COMMENT '网点名称',
-- 网点其他信息
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
}
-- 股东提现记录表
CREATE TABLE withdraw_record {
uuid CHAR(255) PRIMARY KEY COMMENT '股东提现ID',
user_id INT NOT NULL COMMENT '会员用户ID',
amount DECIMAL(10, 2) NOT NULL COMMENT '提现金额',
status ENUM('pending', 'approved', 'rejected') DEFAULT 'pending' NOT NULL COMMENT '提现状态',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
}
-- 银行卡表
CREATE TABLE bank_card {
bank_uuid CHAR(255) PRIMARY KEY COMMENT '银行卡ID',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
}
-- 分润配置表 profit
CREATE TABLE profit_config {
config_key CHAR(255) PRIMARY KEY COMMENT '分润配置KEY: CROSS_STORE_KEY',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
config_value CHAR(255) NOT NULL COMMENT '分润配置VALUE: 70',
}
-- 网点分润配置管理表
CREATE TABLE profit_config_management {
uuid CHAR(255) PRIMARY KEY COMMENT '分润配置详情ID,用来查询具体的分润列表',
branch_id CHAR(255) PRIMARY KEY COMMENT '网点ID',
details CHAR(255) NOT NULL COMMENT '分润详情文本',
FoREIGN KEY (branch_id) REFERENCES branch(branch_id)
}
-- 网点分润人配置表
CREATE TABLE branch_profit_person_management {
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '分润ID',
profit_uuid CHAR(255) COMMENT '分润配置详情ID,用来查询具体的分润列表',
branch_id CHAR(255) PRIMARY KEY COMMENT '网点ID',
bank_card_id CHAR(255) PRIMARY KEY COMMENT '银行卡ID',
profit_account CHAR(255) PRIMARY KEY COMMENT '分润账户',
profit_rate DECIMAL(5,2) DEFAULT 0.00 NOT NULL COMMENT '分润比例',
FoREIGN KEY (profit_uuid) REFERENCES profit_config_management(uuid)
}
-- 充值规则表
CREATE TABLE recharge_rule {
id INT AUTO_INCREMENT COMMENT '序号',
uuid CHAR(255) PRIMARY KEY COMMENT '充值规则ID',
branch_id CHAR(255) PRIMARY KEY COMMENT '网点ID',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
recharge_money DECIMAL(10,2) DEFAULT 0.00 NOT NULL COMMENT '充值金额',
recharge_gift_money DECIMAL(10,2) DEFAULT 0.00 NOT NULL COMMENT '充值赠送金额',
visible_to_new_user_check BOOLEAN DEFAULT FALSE NOT NULL COMMENT '是否对新用户可见',
FoREIGN KEY (branch_id) REFERENCES branch(branch_id)
}
-- 订单列表:
CREATE TABLE order {
id INT AUTO_INCREMENT COMMENT '序号',
uuid CHAR(255) PRIMARY KEY COMMENT '订单ID',
branch_id CHAR(255) PRIMARY KEY COMMENT '网点ID',
user_id INT PRIMARY KEY COMMENT '会员用户ID',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
FoREIGN KEY (user_id) REFERENCES member_user(user_id)
}
-- 核销表
CREATE TABLE coupon_list {
id INT AUTO_INCREMENT COMMENT '序号',
uuid CHAR(255) PRIMARY KEY COMMENT '核销ID',
branch_id CHAR(255) PRIMARY KEY COMMENT '网点ID',
user_id INT PRIMARY KEY COMMENT '会员用户ID',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
type ENUM('0', '1') DEFAULT '0' NOT NULL COMMENT '核销类型: 美团、抖音',
FoREIGN KEY (user_id) REFERENCES member_user(user_id)
}
-- 二维码管理
CREATE TABLE qrcode {
id INT AUTO_INCREMENT COMMENT '序号',
uuid CHAR(255) PRIMARY KEY COMMENT '二维码ID',
branch_id CHAR(255) COMMENT '网点ID',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
qrcode_url CHAR(255) NOT NULL COMMENT '二维码图片地址',
device_id CHAR(255) NOT NULL COMMENT '设备ID',
FOREIGN KEY (device_id) REFERENCES device(device_id)
}
-- 设备管理: 查询管理[设备表] device
-- 网点管理
-- 网点信息 : 查询管理[网点表] branch
SELECT * FROM branch;
-- 股东提现记录: 查询[股东提现表] withdraw_record
SELECT * FROM withdraw_record;
-- 网点未消费余额: 查询用[会员表] grounp by branch_id
SELECT
mu.branch_id AS `网点ID`,
b.branch_name AS `网点名称`,
SUM(mu.unused_recharge_money) AS `网点未消费充值金额`,
SUM(mu.unused_recharge_gift_money) AS `网点未消费充值赠送金额`
FROM
member_user mu
LEFT JOIN
branch b ON mu.branch_id = b.branch_id
GROUP BY
mu.branch_id, b.branch_name;
LIMIT 10;
OFFSET 10;
-- 分润管理
-- 分润配置
-- 跨店分润配置 : 分润配置表 profit
SELECT * FROM profit_config where config_key = 'CROSS_STORE_KEY';
UPDATE profit_config SET config_value = '80' WHERE config_key = 'CROSS_STORE_KEY';
-- 网点分润配置管理表: 网点分润配置管理表 profit_config_management
SELECT * FROM profit_config_management;
-- 详情: 网点分润配置管理表 + 网点分润人配置表 branch_profit_person_management
SELECT
pcm.uuid,
b.branch_id,
b.branch_name,
JSON_ARRAYAGG(
JSON_OBJECT(
'profit_account', bpp.profit_account,
'profit_rate', bpp.profit_rate,
'bank_card_id', bpp.bank_card_id
)
) AS sharing_config_details
FROM
profit_config_management pcm
JOIN
branch b ON pcm.branch_id = b.branch_id
JOIN
branch_profit_person_management bpp ON pcm.uuid = bpp.profit_uuid
WHERE
pcm.uuid = 'your-uuid-here'
GROUP BY
pcm.uuid, b.branch_id, b.branch_name;
-- 分润记录:充值和跨店消费都会产生
-- 银行卡信息:查询管理【银行卡】表 bank_card
SELECT * FROM bank_card;
-- 营销管理
-- 充值规则:recharge_rule
SELECT * FROM recharge_rule;
-- 会员管理
-- 会员列表:member_user
SELECT * FROM member_user;
-- 订单管理
-- 订单列表:order
SELECT * FROM order;
-- 美团核销: coupon
SELECT * FROM coupon where type = '0';
-- 抖音核销: coupon
SELECT * FROM coupon where type = '1';
创建Java项目
注意:java
项目和 Spring Boot
不一样,有点类似于原声js
跟框架的区别。直接选择创建 Spring Boot
,选择Maven
(这里有点类似是使用npm、cnpm、pnpm、yarn)进行安装。 到这里你就成功创建了一个Java Spring Boot
项目了。

Maven == npm
,你如果是一名前端你应该能看的明白,根目录下的pom.xml
就相当于是npm
的package.json
,所有的第三方的插件都放在这里进行管理。
我在这里有个误区,就是我想深入学习Maven 、pom.xml
,我要把他像npm
一样熟悉,很难东西太多了。我现在是个新手,先保持能用就行了。深入的先不关心 。
Maven
也是需要安装的去官网下载,然后放到本地配置环境变量。
bash
#MAC 的放到 .zshrc文件就行 MAVEN_HOME
export MAVEN_HOME=/Users/hejingyuan/tools/apache-maven-3.9.11 #Maven文件夹的路径
export PATH=$PATH:$MAVEN_HOME/bin
打开Java Spring Boot
项目 IDEA
默认会执行 pom.xml
的文件。相比较前端的项目,不需要执行命令安装了。
注意:pom.xml
的文件相比较package.json
他的插件信息需要你手动复制进文件里面。然后点击Maven
的图标刷新按钮就行了。

是的Maven
的一些命令都是IDEA
执行的。我到今天还不知道安装执行什么命令,等有需要再去查命令就行,先会启动项目。
先写一个接口HelloWordController
:
java
package com.car.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class HelloWordController {
@GetMapping("hello")
public String hello() {
return "hello world";
}
}
直接点击Application main
启动就行了,这个也是创建项目生成的。然后访问http://localhost:8080/api/hello
就能看到hello world
了。

JDBC 连接数据库
现在都是插件的形式了,JDBC
连接数据库很方便的。只要下载插件如下:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
然后再配置文件application.yml
(application.yml
和application.properties
配置文件都可以)yml是结构化的就是有换行
,中添加如下内容:
yml
# application.yml`
spring:
application:
name: dome
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/api_services?useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=GMT
username: root
password: 123456789
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
http:
encoding:
charset: UTF-8
enabled: true
force: true
propertie
<!-- application.propertie 是一样的 就是写法不一样 -->
spring.application.name=demo
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/api_services?useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=GMT
spring.datasource.username=root
spring.datasource.password=123456789
MyBatisPlus使用
是的现在新JAVA
项目也是使用的ORM
,MyBatisPlus
真的太好用了,也很简单就跟js
的lodash
一样,结合本项目你去看看就会了。MyBatisPlus中文文档,只需要看前几章就行啦。(代码生成器、持久层接口、条件构造器)
使用IDEA
插件MybaitsX
直接就能根据数据库表生成Service
、Mapper
、Entity
,直接写一些简单的Service
方法还都是ORM
提供的。我稍微放点代码。


java
package com.car.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.car.domain.User;
import com.car.service.UserService;
import com.car.mapper.UserMapper;
import org.springframework.stereotype.Service;
import java.io.Serializable;
import java.util.Collection;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
/**
* @author hejingyuan
* @description 针对表【user】的数据库操作Service实现
* @createDate 2025-07-25 13:24:50
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService{
@Override
public IPage<User> queryList(User user, Integer current, Integer size) {
// 创建分页对象
Page<User> page = new Page<>(current == null ? 1 : current, size == null ? 10 : size);
// 构建查询条件
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
if (user != null) {
if (user.getId() != null) {
queryWrapper.eq("id", user.getId());
}
if (user.getUsername() != null && !user.getUsername().isEmpty()) {
queryWrapper.like("username", user.getUsername());
}
}
// 执行分页查询
return super.page(page, queryWrapper);
}
@Override
public boolean save(User entity) {
// 设置创建时间
if (entity.getCreateTime() == null) {
entity.setCreateTime(java.time.LocalDateTime.now());
}
// 如果有更新时间字段,也可以一并设置
if (entity.getUpdateTime() == null) {
entity.setUpdateTime(java.time.LocalDateTime.now());
}
return super.save(entity);
}
@Override
public boolean saveBatch(Collection<User> entityList) {
return super.saveBatch(entityList);
}
@Override
public boolean saveBatch(Collection<User> entityList, int batchSize) {
return super.saveBatch(entityList, batchSize);
}
@Override
public boolean saveOrUpdateBatch(Collection<User> entityList) {
return super.saveOrUpdateBatch(entityList);
}
@Override
public boolean saveOrUpdateBatch(Collection<User> entityList, int batchSize) {
return super.saveOrUpdateBatch(entityList, batchSize);
}
@Override
public boolean removeById(Serializable id) {
return super.removeById(id);
}
@Override
public boolean removeById(Serializable id, boolean useFill) {
return super.removeById(id, useFill);
}
@Override
public boolean removeByIds(Collection<?> idList) {
return super.removeByIds(idList);
}
}
这上面的
Service
的代码其实好多好是通义灵码
给我提示生成的比如removeById
方法,removeByIds
方法,remove
方法等等,这些都不用写逻辑直接操作数据库的,太简单了。
lombok 使用
lombok
是一个开源的注解库,它可以简化Java模版代码,提高开发效率。我先写下用于不用的区别:
不使用lombok
的代码:
java
public class User {
// 成员变量
private String name;
private int age;
// 无参构造函数
public User() {
}
// 带参数的构造函数
public User(String name, int age) {
this.name = name;
this.age = age;
}
// Getter 方法
public String getName() {
return name;
}
public int getAge() {
return age;
}
// Setter 方法
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
// toString 方法
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
java
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data // 自动生成 Getter、Setter、toString 方法
@NoArgsConstructor // 自动生成无参构造函数
@AllArgsConstructor // 自动生成全参构造函数
public class User {
private String name;
private int age;
}
@AllArgsConstructor
全参数构建是指:
java
public class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
@NoArgsConstructor
无参构造是指:
java
public class User {
private String name;
private int age;
public User() {}
}
-
@Data
:自动生成getter、setter、toString、equals、hashCode方法。
-
@AllArgsConstructor
:自动生成全参构造方法。
-
@NoArgsConstructor
:自动生成无参构造方法。
-
@RequiredArgsConstructor
:自动生成有参构造方法。
-
@Getter
:自动生成getter方法。
-
@Setter
:自动生成setter方法。
-
@ToString
:自动生成toString方法。
-
@EqualsAndHashCode
:自动生成equals和hashCode方法。
-
@Slf4j
:自动生成日志对象。
lombok
是不是好用了很多,你就看下我这个示例官网lombok都不用去看就能用了。
统一返回值
返回值肯定要统一的啊,包含状态码、提示信息、数据:
json
{
"code": 200,
"message": "操作成功",
"data": {}
}
咱们放置一个公共类:R
如下:
java
package com.car.common.result;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
@Data
public class R<T> {
private Integer code;
private String message;
private Map<String, T> data = new HashMap<>();
/**
* 构造器私有
*/
private R(){}
/**
* 返回成功
*/
public static R ok(){
R r = new R();
r.setCode(ResultEnum.SUCCESS.getCode());
r.setMessage(ResultEnum.SUCCESS.getMessage());
return r;
}
/**
* 返回失败
*/
public static R error(){
R r = new R();
r.setCode(ResultEnum.ERROR.getCode());
r.setMessage(ResultEnum.ERROR.getMessage());
return r;
}
/**
* 返回失败
*/
public static R error(String errorMessage){
R r = new R();
r.setCode(ResultEnum.ERROR.getCode());
r.setMessage(errorMessage);
return r;
}
public static R error(ResultEnum enumm){
R r = new R();
r.setCode(enumm.getCode());
r.setMessage(enumm.getMessage());
return r;
}
public static R error(ResultEnum enumm, String errorMessage){
R r = new R();
r.setCode(enumm.getCode());
r.setMessage(errorMessage);
return r;
}
/**
* 设置特定结果
*/
public static R setResult(ResultEnum ResultEnum){
R r = new R();
r.setCode(ResultEnum.getCode());
r.setMessage(ResultEnum.getMessage());
return r;
}
public R message(String message){
this.setMessage(message);
return this;
}
public R code(Integer code){
this.setCode(code);
return this;
}
public R<T> data(String key, T value){
this.data.put(key, value);
return this;
}
public R<T> data(T t){
this.data("result", t);
return this;
}
}
使用的时候直接
java
package com.car.controller;
// 引入
import com.car.common.result.R;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class HelloWordController {
@GetMapping("hello")
public R<String> hello() { // 这里返回结构化的结果
return R.ok().data("hello world"); // 这里返回结构化的结果
}
}
格式化时间、uuid生成
前端格式化时间用的是dayjs
,后端直接在Entity
类中添加一个@JsonFormat
注解,指定格式就行了如下:
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
直接就行了,不用循环赋值。
java
package com.car.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
*
* @TableName user
*/
@TableName(value ="user")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class User {
/**
* 注解的形式
* import java.util.UUID;
* UUID.randomUUID().toString()
*
*/
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
/**
*
*/
@TableField(value = "username")
@NotBlank(message = "用户名不能为空")
private String username;
/**
*
*/
@TableField(value = "password")
@NotBlank(message = "密码不能为空")
private String password;
/**
*
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
@TableField(value = "update_time")
private LocalDateTime updateTime;
/**
*
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
@TableField(value = "create_time")
private LocalDateTime createTime;
/**
*
*/
@TableField(value = "user_type")
private Object userType;
}
在使用Mybatisplus
的Orm
的save
没有添加id
字段的时候,会自动生成一个19
位额雪花ID。如果想让他使用uuid
加个注解就行了 @TableId(value = "id", type = IdType.ASSIGN_UUID)
。
或者在新增的时候,手动添加id
字段。import java.util.UUID
然后执行 UUID.randomUUID().toString()
方法就行;
字段校验
java
直接使用@NotBlank
注解就可以实现字段非空校验了。我上面的实体上就有@NotBlank(message = "密码不能为空")
,等价于:
java
// 手动判断用户名是否为空
if (login.getUsername() == null || login.getUsername().trim().isEmpty()) {
throw new BusinessException("用户名不能为空");
}
// 手动判断密码是否为空
if (login.getPassword() == null || login.getPassword().trim().isEmpty()) {
throw new BusinessException("密码不能为空");
}
使用注解的形式需要添加依赖:
xml
<!-- 校验相关-->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
添加依赖后,参数上添加@Valid
注解
java
package com.car.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.car.common.exception.BusinessException;
import com.car.common.mode.LoginRequired;
import com.car.common.result.R;
import com.car.common.utils.JwtUtil;
import com.car.domain.User;
import com.car.service.UserService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Objects;
@RestController
@RequestMapping("/api")
public class LoginController {
@Autowired
UserService userService;
@Autowired
JwtUtil jwtUtil;
@PostMapping("login")
@LoginRequired(required = false)
public R<String> login(@Valid @RequestBody User login) // 注意这里
{
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", login.getUsername())
.eq("password", login.getPassword());
User loginUser = userService.getOne(queryWrapper);
if(Objects.isNull(loginUser)) {
throw new BusinessException("用户不存在");
}
return R.ok().data(jwtUtil.createJWTToken(loginUser));
}
}
其实这个时候还没生效需要有统一的错误拦截才行。太多了 直接去我代码仓库里面看吧。
JWT生成
很方便的直接使用的java的JWT工具类,创建一个类:JwtUtil定义为@Component
然后实现两个方法createJWTToken
和getUserByToken
就基本够用了。
请直接参考git仓库
LoginRequired注解 API放行
第一步定义一个注解:
java
package com.car.common.mode;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* TODO
*
* @author djq
* @create 2024/10/6 23:12
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface LoginRequired {
boolean required() default true;
}
第二步添加登录拦截器
java
package com.car.common.interceptor;
//import com.car.common.exception.BusinessException;
import com.car.common.exception.BusinessException;
import com.car.common.mode.LoginRequired;
import com.car.common.result.ResultEnum;
import com.car.common.utils.JwtUtil;
import com.car.common.utils.ServletUtils;
import com.car.common.utils.ThreadLocalHolder;
import com.car.domain.User;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Objects;
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
/**
* 1、LoginRequire d默认都拦截;
* 2、loginRequired.required() === false 时放行
*/
HandlerMethod handlerMethod = null;
// 检查是否是HandlerMethod类型
if (!(handler instanceof HandlerMethod)) {
return true; // 不是方法处理器,直接放行
}
handlerMethod = (HandlerMethod) handler;
LoginRequired loginRequired = handlerMethod.getMethodAnnotation(LoginRequired.class);
if(Objects.isNull(loginRequired)) {
return verifyToken();
}else {
boolean needLogin = loginRequired.required();
if(!needLogin) {
return true;
}else {
return verifyToken();
}
}
}
private boolean verifyToken() {
String token = ServletUtils.getToken();
User account = null;
if (!token.isBlank()) {
account = jwtUtil.getUser(token);
}else {
throw new BusinessException("未登录");
}
if (Objects.nonNull(account)) {
ThreadLocalHolder.addAccount(account);
}else {
ThreadLocalHolder.remove();
throw new BusinessException("TOKEN无效");
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
ThreadLocalHolder.remove();
}
}
全局错误处理
java
的错误是一个个的 每个类型都不一样,需要一个个的重写,然后给对应的错误返回一个错误码,然后根据错误码返回对应的错误信息。
文件上传
请直接拉代码仓库运行查看,都是一些逻辑代码就不贴了。需要了解的是:
- 创建公共的上传服务 vs 每个模块写自己的上传逻辑那个更好?
身为前端应该有过要么是整个接口是
FormData
格式的,要么是一个个公共的上传接口,然后列表中只是保存了资源路径供下载预览使用。要多少了解下区别
圆形验证码使用
需要注意不要使用固定key,要使用生成的session 做为用户的唯一key,不然多用户就冲突了。
总结
Java Spring Boot
确实要比nodejs
写后台好用,且开发能更快,基本的增删改查功能,基本就一直根据代码提示摁Tab
就行了。
应该也算是初步步入全栈 啦,前端我是资深前端工程师,后端我刚刚入门,还需要多积累。如果你也正在学习
java
, 可以根据我的代码仓库 边跟着文档学习java
。我再练习这个Java Demo
的时候 总共创建了2
个表。直接运行根目录的api_services.sql
就行。