6年前端学习Java Spring boot 要怎么学?

针对初学者最佳学习路线:先拉取本Java Spring boot 项目,能运行起来,然后照着本篇文档一步步实现:

1、学习Mysql数据库,了解sql语法,了解索引。

2、创建一个空的Java Spring boot 项目。

3、拉取我的项目,对着文档一步步敲一遍。

Java代码仓库地址

  • 登录生成token

  • 圆形验证码生成

  • 登录拦截器实现、API加白

  • Lombok 注解的应用

  • 统一Response返回格式

  • 统一异常处理 全局错误拦截器

  • MyBatisPlus 集成使用

  • 字段注解检验

  • 用户表的增删改查的实现

  • 文件上传的处理

经过近2周利用空闲时间又学习Java Spring boot,基本算是入门了。至少能实现JWT接口拦截、API加白放行、圆形验证码存储session验证是否正确、文件导入、表的增删改查 等等功能,技术栈:jdk23 + mybatisplus + lombok + mysql

博主本人在最原来写过一段时间PHP(现在基本忘记完了),对mysql数据库停留在会用的阶段。也算是熟悉nodejs,使用nodejsexpress框架写过一些demo接口Pm2管理进程这些,也使用过eggjs、nestjs这些nodejs的框架,但是也就停留在会用阶段,不能说深入吧。

说下为啥我又突然想学习Java了呢,是的我在上篇文章中接到了一个朋友的私活前端(两个小程序+pc管理端),后面就想变成全栈 ,接更多的私活用!!!

我在去年12月份 的时候,跟着 韩顺平 零基础30天学会Java 敲了点java代码,韩顺平老师 讲的太细了 ,真的特别适合老手 也来巩固学习,有点类似前端的重学前端 ,各种原理细节分析900 多章,我看了450来章 (也有快进,但是没有跳章),后面因为一些事情也就搁置了。也算是了解了基础语法以及应用。 可能等我在写一段时间java我会重新看一遍。

既然要写java了,数据库的表设计要学习下吧,还有索引字段,我在掘金上找了几篇关于mysql索引的文章,(四)MySQL之索引初识篇:索引机制、索引分类、索引使用与管理综述 深入学习了下。初步了解到了索引的作用,数据在多的时候,建立索引能更快的查询到数据。索引建立就像是中华字典的目录 ,可以通过建立索引直接翻到该页码,当然更快了。常见的索引有:主键索引、全文索引、联合索引、前缀索引、唯一索引等等,到这里我觉得我的mysql就够用了,等我真处理千万级别的数据再仔细研究具体方案。

我又找时间把我们上个洗车应用的后台管理的数据表给设计了一下,先看功能如下(没有基础的可以直接略过看下面哈):

    1. 登录模块 (若依)
    1. 用户角色模块、字典、部门(若依)
    1. 订单管理
    • 3.1 订单列表
    • 3.2 美团核销
    • 3.3 抖音核销
    1. 会员管理
    • 4.1 会员列表
    1. 营销管理
    • 5.1 充值规则
    1. 门店管理
    • 6.1 门店列表
    • 6.2 股东提现记录
    • 6.3 门店未消费余额
    1. 分润管理
    • 7.1 分润配置
    • 7.2 分润记录
    • 7.3 银行卡信息
    1. 设备管理 (门店的洗车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就相当于是npmpackage.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.ymlapplication.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真的太好用了,也很简单就跟jslodash一样,结合本项目你去看看就会了。MyBatisPlus中文文档,只需要看前几章就行啦。(代码生成器、持久层接口、条件构造器)

使用IDEA插件MybaitsX直接就能根据数据库表生成ServiceMapperEntity,直接写一些简单的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() {}
}
    1. @Data:自动生成getter、setter、toString、equals、hashCode方法。
    1. @AllArgsConstructor:自动生成全参构造方法。
    1. @NoArgsConstructor:自动生成无参构造方法。
    1. @RequiredArgsConstructor:自动生成有参构造方法。
    1. @Getter:自动生成getter方法。
    1. @Setter:自动生成setter方法。
    1. @ToString:自动生成toString方法。
    1. @EqualsAndHashCode:自动生成equals和hashCode方法。
    1. @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;
}

在使用MybatisplusOrmsave没有添加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 然后实现两个方法createJWTTokengetUserByToken就基本够用了。

请直接参考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就行。

相关推荐
白龙马云行技术团队2 分钟前
性能治理之页面LongTask优化
前端
Nayana2 分钟前
Clean Code JavaScript小记(二)
javascript
就是帅我不改2 分钟前
告别996!高可用低耦合架构揭秘:SpringBoot + RabbitMQ 让订单系统不再崩
java·后端·面试
Ankkaya3 分钟前
开发小结(08.11-08.16)
前端·uni-app
Hilaku3 分钟前
前端监控实战:从性能指标到用户行为,我是如何搭建监控体系的
前端·javascript·性能优化
咖啡の猫4 分钟前
Shell脚本-影响shell程序的内置命令
前端·chrome·bash
hhzz15 分钟前
Maven项目中settings.xml终极优化指南
java·jdk·maven
sorryhc17 分钟前
【AI解读源码系列】ant design mobile——Avatar头像
前端·javascript·react.js
Mintopia25 分钟前
🎭 一场浏览器里的文艺复兴
前端·javascript·aigc
Mintopia25 分钟前
🎬《Next 全栈 CRUD 的百老汇》
前端·后端·next.js