Skip to content

运动管理系统后端学习笔记

wechat_server/
├─ pom.xml                           # Maven 项目配置
├─ src/main/java/com/doc/
│  ├─ MediWaitApplication.java       # Spring Boot 启动类
│  ├─ server/
│  │  ├─ controller/                 # HTTP 控制器层
│  │  │  └─ doctor/                  # 医生相关接口
│  │  │     └─ DoctorController.java # 医生控制器
│  │  ├─ service/                    # 业务逻辑层
│  │  │  └─ impl/                    # 服务实现类
│  │  ├─ mapper/                     # MyBatis-Plus 数据访问层
│  │  ├─ config/                     # 配置类
│  │  │  ├─ WebMvcConfiguration.java # Web MVC 配置(拦截器、CORS、Swagger)
│  │  │  ├─ SecurityConfig.java      # Spring Security 安全配置
│  │  │  ├─ MybatisPlusConfig.java   # MyBatis-Plus 配置
│  │  │  └─ DruidConfig.java         # Druid 数据源配置
│  │  ├─ interceptor/                # 拦截器
│  │  │  └─ JwtTokenAdminInterceptor.java # JWT 令牌拦截器
│  │  └─ handler/                    # 全局异常处理器
│  ├─ pojo/                          # 数据传输对象
│  │  ├─ entity/                     # 数据库实体类
│  │  ├─ dto/                        # 数据传输对象(入参)
│  │  └─ vo/                         # 视图对象(出参)
│  └─ common/                        # 公共组件
│     ├─ constant/                   # 常量类
│     │  ├─ MessageConstant.java     # 消息常量
│     │  ├─ StatusConstant.java      # 状态常量
│     │  └─ JwtClaimsConstant.java   # JWT 声明常量
│     ├─ exception/                  # 异常类
│     │  ├─ BaseException.java       # 基础异常类
│     │  ├─ AccountNotFoundException.java # 账户未找到异常
│     │  ├─ AccountLockedException.java   # 账户锁定异常
│     │  └─ PasswordErrorException.java   # 密码错误异常
│     ├─ result/                     # 统一返回结果
│     │  └─ Result.java              # 统一返回体
│     ├─ utils/                      # 工具类
│     │  ├─ JwtUtil.java             # JWT 工具类
│     │  ├─ ResultUtils.java         # 结果工具类
│     │  └─ ResultVo.java            # 结果视图对象
│     ├─ properties/                 # 配置属性类
│     │  └─ JwtProperties.java       # JWT 配置属性
│     ├─ context/                    # 上下文
│     ├─ enumeration/                # 枚举类
│     └─ json/                       # JSON 处理
└─ src/main/resources/
   ├─ application.yml                # 基础配置
   ├─ application-dev.yml            # 开发环境配置
   ├─ mapper/                        # MyBatis XML 映射文件
   │  └─ all_logs_view.sql           # 数据库视图脚本
   ├─ static/                        # 静态资源
   └─ templates/                     # 模板文件
盒子全名面向谁核心任务典型字段差异改动代价
EntityPersistence Entity / ORM 实体数据库1:1 映射表结构,保证 ACID、索引、关联、生命周期都与 DB 同步字段名=列名,类型=SQL 类型,外键对象/注解齐全牵一发动全身——加列要发 DDL,影响整个系统
DTOData Transfer Object进程/服务之间把数据“原封不动”搬个家,解决网络序列化、内部保密、懒加载等问题可能砍掉敏感字段(password)、把懒加载集合展平成 ID 列表只影响调用双方,可随时加字段兼容
VOView Object / Value Object前端界面按 UI 需要“剪枝+化妆”——只留要的、转单位、格式化、加中文文本金额分→元,Date→yyyy-MM-dd,status→“已支付”,甚至拼好 URL、图标纯展示,想改就改,与 DB 无关

1.Mapper与Service层的逻辑

层级关注点例子
Mapper与表一一对应的 物理 SQLSELECT * FROM sys_role WHERE …
Service业务规则、数据加工、权限判断等“若当前用户非超级管理员,则追加部门过滤条件”

Mapper` 只负责“把数据按指定条件从数据库取出来”,不做业务逻辑;真正的逻辑查询(业务规则、数据权限、结果裁剪)放在 Service 层完成

graph TD
    A[前端/客户端] --> B[Controller层 - 控制器]
    B --> C[Service层 - 业务逻辑]
    C --> D[Mapper层 - 数据访问]
    D --> E[数据库]
    
    F[Entity层 - 实体类] --> B
    F --> C
    F --> D
    
    style A fill:#e1f5fe
    style B fill:#f3e5f5
    style C fill:#e8f5e8
    style D fill:#fff3e0
    style E fill:#ffebee
    style F fill:#f1f8e9

1. 实体类(Entity层)- 数据模型

SysUser.java - 用户实体类

java
package com.doc.web.sys_user.entity;

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 lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.math.BigDecimal;
import java.util.Collection;
import java.util.Date;

@Data
@TableName("sys_user")
public class SysUser implements UserDetails {

    //声明“这是主键,并且它的值由数据库自增”。
    @TableId(type = IdType.AUTO)
    private Long userId;
    //表明roleId字段不属于sys_user表,需要排除
    @TableField(exist = false)
    private Long roleId;
    private String username;
    private String password;
    private String phone;
    private String email;
    private String sex;
    private String isAdmin;
    //类型(1:员工 2:教练)
    private String userType;
    //状态 0:停用 1:启用
    private String status;
    private BigDecimal salary;
    private String nickName;
    //创建时间
    private Date createTime;
    //更新时间
    private Date updateTime;
    //帐户是否过期(1 未过期,0已过期)
    private boolean isAccountNonExpired = true;
    //帐户是否被锁定(1 未锁定,0已锁定)
    private boolean isAccountNonLocked = true;
    //密码是否过期(1 未过期,0已过期)
    private boolean isCredentialsNonExpired = true;
    //帐户是否可用(1 可用,0 删除用户)
    private boolean isEnabled = true;
    //用户权限字段的集合 authorities字段不属于sys_user表,需要排除
    //(exist = false) → 注解参数;给注解的 exist 属性赋值 false,告诉框架“这个字段在表里不存在”。
    @TableField(exist = false)
    Collection<? extends GrantedAuthority> authorities;
}
  • @Data:这是Lombok注解,自动生成getter、setter、toString、equals、hashCode方法

  • @TableName("sys_user"):MyBatis-Plus注解,指定对应的数据库表名

  • implements UserDetails:实现Spring Security的用户详情接口,用于权限认证

  • @TableId(type = IdType.AUTO):标记主键,AUTO表示数据库自增

  • @TableField(exist = false):表示该字段不在数据库表中,仅用于业务逻辑

PageParam.java - 分页参数类

package com.doc.web.sys_user.entity;

import lombok.Data;
@Data
public class PageParam {
    private Long currentPage;  // 当前页码
    private Long pageSize;     // 每页大小
    private String phone;      // 查询条件:手机号
    private String nickName;   // 查询条件:昵称
}

2. 数据访问层(Mapper层)

public interface SysUserMapper extends BaseMapper<SysUser> {
}
  • interface:Java接口,定义方法签名但不实现

  • extends BaseMapper:继承MyBatis-Plus的基础Mapper

  • BaseMapper

    提供了基本的CRUD操作:

    • insert() - 插入
    • deleteById() - 根据ID删除
    • updateById() - 根据ID更新
    • selectById() - 根据ID查询
    • selectList() - 查询列表
    • 等等...

3. 业务逻辑层(Service层)

SysUserService.java - 业务接口

package com.doc.web.sys_user.service;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.doc.web.sys_user.entity.PageParam;
import com.doc.web.sys_user.entity.SysUser;
//声明一个用户业务接口,它天生自带 MyBatis-Plus 的全部单表 CRUD 能力,额外方法自己再补
public interface SysUserService extends IService<SysUser> {

    IPage<SysUser> list(PageParam param);

    //根据员工姓名查询员工信息
    SysUser loadUser(String username);

}
  • extends IService:继承MyBatis-Plus的服务接口,获得基础CRUD能力
  • IPage:分页结果接口,包含数据和分页信息

SysUserServiceImpl.java - 业务实现类

处理具体业务逻辑

package com.doc.web.sys_user.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.doc.web.sys_user.entity.PageParam;
import com.doc.web.sys_user.entity.SysUser;
import com.doc.web.sys_user.mapper.SysUserMapper;
import com.doc.web.sys_user.service.SysUserService;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

@Service
//拿到通用 CRUD;泛型 <Mapper, Entity> 指定操作谁
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
    @Override
    public IPage<SysUser> list(PageParam param) {
        // 实现分页查询逻辑,构造分页对象
        // 这里可以使用 MyBatis-Plus 的分页查询方法
        IPage<SysUser> page = new Page<>();
        page.setSize(param.getPageSize());
        page.setCurrent(param.getCurrentPage());
        //构造查询条件
        //动态 SQL 条件构造器 QueryWrapper
        QueryWrapper<SysUser> query = new QueryWrapper<>();
        //Spring 的判空工具
        if(StringUtils.isNotEmpty(param.getNickName())){
            query.lambda().like(SysUser::getNickName, param.getNickName());
        }

        if(StringUtils.isNotEmpty(param.getPhone())){
            query.lambda().like(SysUser::getPhone, param.getPhone());
        }

        return this.baseMapper.selectPage(page, query);
    }

    @Override
    public SysUser loadUser(String username) {
        // 根据用户名查询用户信息
        QueryWrapper<SysUser> query = new QueryWrapper<>();
        query.lambda().eq(SysUser::getUsername, username);
        return this.baseMapper.selectOne(query);
    }
}
  • @Service:Spring注解,标记为服务层组件,会被Spring容器管理

  • abstract class:抽象类,可以有具体实现,也可以有抽象方法

  • ServiceImpl:MyBatis-Plus提供的服务实现基类

  • StringUtils.isNotEmpty():判断字符串非空

  • query.lambda().like():Lambda表达式构造模糊查询条件

  • SysUser::getNickName:方法引用,指向getNickName方法

4. 控制器层(Controller层)

处理HTTP请求和响应

package com.doc.web.sys_user.controller;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.doc.utils.ResultUtils;
import com.doc.utils.ResultVo;
import com.doc.web.sys_role.service.SysRoleService;
import com.doc.web.sys_user.entity.PageParam;
import com.doc.web.sys_user.entity.SysUser;
import com.doc.web.sys_user.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.apache.commons.lang.StringUtils;
import org.springframework.util.DigestUtils;

import java.util.Date;

@RestController
@RequestMapping("/api/user")
public class SysUserController {
    @Autowired
    private SysUserService sysUserService;

    @Autowired
    private SysRoleService sysRoleaService;

    //新增员工
    @PostMapping
    public ResultVo addUser(@RequestBody SysUser sysUser) {
        //判断用户名字是否存在
        QueryWrapper<SysUser> query = new QueryWrapper<>();
        //指定列,指定值
        query.lambda().eq(SysUser::getNickName, sysUser.getNickName());
        SysUser one = sysUserService.getOne(query);
        if (one != null) {
            return ResultUtils.error("员工名称已存在,请重新输入");
        }

        //密码加密
        if (StringUtils.isNotEmpty(sysUser.getPassword())) {
            sysUser.setPassword(DigestUtils.md5DigestAsHex(sysUser.getPassword().getBytes()));

        }
        //设置默认角色
        sysUser.setIsAdmin("0");
        sysUser.setCreateTime(new Date());
        //存储到数据库中
        boolean save = sysUserService.save(sysUser);
        if (save) {
            return ResultUtils.success("新增用户成功");
        }

        return ResultUtils.error("新增用户失败");

    }

    //编辑员工
    @PutMapping
    public ResultVo editUser(@RequestBody SysUser sysUser) {
        QueryWrapper<SysUser> query = new QueryWrapper<>();
        //指定列,指定值
        query.lambda().eq(SysUser::getUsername, sysUser.getUsername());
        SysUser one = sysUserService.getOne(query);
        if(one != null && one.getUserId()!= sysUser.getUserId()){
            return ResultUtils.error("用户名已存在,请重新输入");
        }
        //密码加密
        if (StringUtils.isNotEmpty(sysUser.getPassword())) {
            sysUser.setPassword(DigestUtils.md5DigestAsHex(sysUser.getPassword().getBytes()));

        }
        sysUser.setUpdateTime(new Date());
        //存入数据库
        boolean save = sysUserService.updateById(sysUser);
        if (save) {
            return ResultUtils.success("编辑用户成功");
        }

        return ResultUtils.error("编辑用户失败");

    }

    //删除员工
    @DeleteMapping("/{userId}")
    public ResultVo deleteUser(@PathVariable("userId") Long userId) {
        boolean remove = sysUserService.removeById(userId);
        if (remove) {
            return ResultUtils.success("删除用户成功");
        }
        return ResultUtils.error("删除用户失败");
    }

    //查询用户列表
    @GetMapping("/list")
    public ResultVo getlist(PageParam param) {
        IPage<SysUser> list = sysUserService.list(param);
        //查询出来不显示密码
        list.getRecords().forEach(user -> user.setPassword(null));
        return ResultUtils.success("查询成功", list);
    }
    //根据用户id查询角色id
    @GetMapping("/role")
    public ResultVo getRole(@RequestParam("userId") Long userId) {
        //查询用户角色
        QueryWrapper<SysUser> query = new QueryWrapper<>();
        query.lambda().eq(SysUser::getUserId, userId);
        SysUser sysUser = sysUserService.getOne(query);
        if (sysUser == null) {
            return ResultUtils.error("用户不存在");
        }
        return ResultUtils.success("查询成功", sysUser.getRoleId());
    }

}
  • @RestController:Spring注解,等于@Controller + @ResponseBody,返回JSON数据

  • @RequestMapping("/api/user"):定义基础URL路径

  • @Autowired:Spring自动装配注解,自动注入Service实例

新增用户方法详解:

@PostMapping
public ResultVo addUser(@RequestBody SysUser sysUser) {
    // 检查用户名是否存在
    QueryWrapper<SysUser> query = new QueryWrapper<>();
    query.lambda().eq(SysUser::getNickName, sysUser.getNickName());
    SysUser one = sysUserService.getOne(query);
  • @PostMapping:处理POST请求

  • @RequestBody:将请求体JSON转换为Java对象

  • ResultVo:统一返回结果封装类

  • @PathVariable:获取URL路径中的变量

  • @RequestParam:获取请求参数

@Autowired的核心作用

1. 自动依赖注入

简单来说,@Autowired帮你自动"找到"并"注入"需要的对象,你不需要手动创建。

让我用一个生活中的例子来解释:

// 没有@Autowired的情况(手动创建)
public class SysUserController {
    private SysUserService sysUserService;
    
    public SysUserController() {
        // 你需要手动创建Service对象
        this.sysUserService = new SysUserServiceImpl();
        // 但是SysUserServiceImpl又需要SysUserMapper
        // 你还得手动创建Mapper...
        // 这样一层层创建下去,非常麻烦!
    }
}

创建Controller时

  • Spring看到@Autowired注解
  • 从容器中找到SysUserService类型的实例(就是SysUserServiceImpl
  • 自动注入到sysUserService字段中

业务逻辑思维流程图

┌─────────────────────────────────────┐ │ Controller层 │ │ - 接收HTTP请求 │ │ - 参数验证 │ │ - 调用Service │ │ - 返回统一响应格式 │ └─────────────────┬───────────────────┘ │ 调用 ▼ ┌─────────────────────────────────────┐ │ ServiceImpl层 │ │ - 复杂业务逻辑 │ │ - 事务管理 │ │ - 多表操作 │ │ - 业务规则实现 │ └─────────────────┬───────────────────┘ │ 调用 ▼ ┌─────────────────────────────────────┐ │ Mapper层 │ │ - 数据库操作 │ │ - SQL执行 │ └─────────────────────────────────────┘

2.系统菜单管理模块相关

扁平菜单列表变成树形结构

java
package com.doc.web.sys_menu.entity;

import org.springframework.beans.BeanUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/**
 * 构造菜单和路由数据的实体类
 */
public class MakeMenuTree {

    //构造菜单树
    //扁平菜单列表变成树形结构
    public static List<SysMenu> makeTree(List<SysMenu> menuList,long pid){
        //static:工具方法,直接 MenuTreeUtil.makeTree(...) 调用,无需 new 对象。
       // menuList:从数据库查出来的全部菜单(扁平)。
        //pid:当前要查找的父节点 ID,第一次调用传 0 或 1(代表根节点)。
        List<SysMenu> list = new ArrayList<>();
        Optional.ofNullable(menuList).orElse(new ArrayList<>())
                .stream()
                .filter(item -> item != null && item.getParentId().equals(pid))
                //防止外面把 null 传进来,自动退化成空流,避免 NPE。
                //只保留 parentId == pid 的记录——即“找爸爸等于 pid 的所有儿子”。
                .forEach(item ->{
                    SysMenu menu = new SysMenu(); // 新对象,避免污染原始数据
                    BeanUtils.copyProperties(item,menu);// Spring 工具,把属性拷贝过去
                    //递归查找下级,自己调用自己
                    List<SysMenu> children = makeTree(menuList, item.getMenuId());
                    menu.setChildren(children);// 把儿子们挂到当前节点
                    list.add(menu); // 放入本层结果
                });
        return list;
    }

    //把数据库里扁平的 SysMenu 转成前端 Vue-Router 能直接吃的 RouterVo 树,并且把“一级菜单”特殊处理成 Vue 的 Layout 路由。
    //构造路由数据
    public static  List<RouterVo> makeRouter(List<SysMenu> menuList,Long pid){
        //构建存放路由数据的容器
        List<RouterVo> list = new ArrayList<>();
        Optional.ofNullable(menuList).orElse(new ArrayList<>())
                .stream()
                .filter(item ->item != null && item.getParentId().equals(pid))
                .forEach(item ->{
                    RouterVo router = new RouterVo();
                    router.setName(item.getName());
                    router.setPath(item.getPath());
                    //设置children 递归调用,自己调用自己
                    List<RouterVo> children = makeRouter(menuList, item.getMenuId());
                    router.setChildren(children);
                    if(item.getParentId() == 0L){
                        router.setComponent("Layout");
                        //Vue-Element-Admin 的套路:一级路由永远对应 Layout 组件(侧边栏骨架),真正的页面放在它的 children 里。
//所以 path 可以重复:一级 path:/user 只是侧边栏入口,children 里的 path:/user 才是页面路由。
                        
                        //判断该数据是否是菜单类型
                        if(item.getType().equals("1")){
                            router.setRedirect(item.getPath());
                            //目录型菜单(type=1)再包一层 redirect
                            //对应“点击一级菜单自动跳转到默认子页”的效果。
//代码里又手动 new 了一个 child,把真实组件挂进去,注意这里把之前递归生成的 children 覆盖了,所以 目录节点不会继续向下递归——这是逻辑隐患(目录下其实还能再挂子目录)
                            //菜单也需要设置children
                            List<RouterVo> listChild = new ArrayList<>();
                            RouterVo child = new RouterVo();
                            child.setName(item.getName());
                            child.setPath(item.getPath());
                            child.setComponent(item.getUrl());
                            child.setMeta(child.new Meta(
                                    item.getTitle(),
                                    item.getIcon(),
                                    item.getCode().split(",")
                            ));
                            listChild.add(child);
                            router.setChildren(listChild);
                            router.setPath(item.getPath());
                            router.setName(item.getName() + "parent");
                        }
                    }else {
                        router.setComponent(item.getUrl());
                    }
                    router.setMeta(
                            router.new Meta(
                                    item.getTitle(),
                                    item.getIcon(),
                                    item.getCode().split(",")
                            )
                    );
                    list.add(router);
                });
        return list;
    }
}

Optional java8新特性

Optional 类是 Java 8 才引入的,Optional 是个容器,它可以保存类型 T 的值,或者仅仅保存 null。Optional 提供了很多方法,这样我们就不用显式进行空值检测。Optional 类的引入很好的解决空指针异常

Java8之前的空指针异常判断

Java 在使用对象过程中,访问任何方法或属性都可能导致 NullPointerException:

例如我们通过以下方法,获取存在 student 对象中的 Age 值。

    public String getIsocode (Student student){
        return student.getAge();
    }

在这样的示例中,如果我们想要避免由 student 或 student.age 为空而导致的空指针问题,我们就需要采用防御式检查减少 NullPointerException(在访问每一个值之前对其进行明确地检查):

   public String getIsocode (Student student){
        if (null == student) {
            // doSomething
            return "Unknown";
        }
        if (null = student.getAge()) {
            // doSomething
            return "Unknown";
        }
        return student.getAge();
    }

Java8之后Optional的使用

当需要判断的量多时,此时的这些判断语句可能会导致代码臃肿冗余,为此 Java8 特意推出了 Optional 类来帮助我们去处理空指针异常。

Optional类常用方法总结

Optional对象创建

Optional.empty()方法

使用 Optional.empty() 方法声明一个空的 Optional:

// 通过静态工厂方法 Optional.empty(),创建一个空的 Optional 对象
Optional<Student> optStudent = Optional.empty();

Optional.of(T t)方法

使用 Optional.of(T t) 方法创建一个包含非空值的 Optional 对象 (不推荐):

// 静态工厂方法 Optional.of(T t),依据一个非空值创建一个 Optional 对象
Optional<Student> optStudent = Optional.of(student);

如果 student 为 null,这段代码会立即抛出一个 NullPointerException,而不是等到访问 student 的属性值时才返回一个错误。

Optional.ofNullable(T t)方法

使用 Optional.ofNullable(T t) 方法创建一个包含可能为空的值的 Optional 对象 (推荐):

// 用静态工厂方法 Optional.ofNullable(T t),你可以创建一个允许 null 值的 Optional 对象

Optional<Student> optStudent = Optional.ofNullable(student);

Optional对象获取

get()方法

get() 方法,如果变量存在,它直接返回封装的变量值,否则就抛出一个 NoSuchElementException 异常,不推荐使用:

optional.map(Student::getAge).get()
orElse(T other)方法

orElse(T other) 方法,它允许你在 Optional 对象不包含值时提供一个默认值:

optional.map(Student::getAge).orElse(20));
orElseGet(Supplier<? extends T> other)方法

orElseGet(Supplier<? extends T> other) 方法,它是 orElse 方法的延迟调用版,Supplier 方法只有在 Optional 对象不含值时才执行调用(懒加载):

optional.map(Student::getAge).orElseGet(() -&gt; Integer.MAX_VALUE);
orElseThrow()方法

orElseThrow() 方法,它和 get 方法非常类似,它们遭遇 Optional 对象为空时都会抛出一个异常,但是使用 orElseThrow 可以定制希望抛出的异常类型:

optional.orElseThrow(() -&gt; new RuntimeException("student不存在!"));
ifPresent(Consumer<? super T> consumer)方法

ifPresent(Consumer<? super T> consumer) 方法,它让能在变量值存在时执行一个作为参数传入的方法,否则就不进行任何操作:

optional.ifPresent(o -&gt; o.setAge(18));

Optional对象中值的提取和转换

map()方法

map() 方法,如果值存在,就对该值执行提供的 mapping 函数调用,如果值不存在,则返回一个空的 Optional 对象。

引入 Optional 以前:

	String name = null;
if(insurance != null){
    name = insurance.getName();
}

引入 Optional 以后:

	Optional&lt;String&gt; name = Optional.ofNullable(insurance).map(Insurance::getName);

Optional 的 map 方法和 Java 8 中 Stream 的 map 方法相差无几。

flatMap()方法

flatMap() 方法,对于嵌套式的 Optiona 结构,我们应该使用 flatMap 方法,将两层的 Optional 合并成一个。

我们试着重构以下代码:

public String getCarInsuranceName(Person person) { return person.getCar().getInsurance().getName(); }

由于我们刚刚学习了如何使用 map,我们的第一反应可能是我们可以利用 map 重写之前的代码:

  Optional&lt;Person&gt; optPerson = Optional.of(person);
      Optional&lt;String&gt; name =
          optPerson.map(Person::getCar)
                   .map(Car::getInsurance)
                   .map(Insurance::getName);

不幸的是,这段代码无法通过编译。为什么呢? optPerson 是 Optional<Person> 类型的 变量, 调用 map 方法应该没有问题。但 getCar 返回的是一个 Optional<Car> 类型的对象,这意味着 map 操作的结果是一个 Optional<Optional<Car>> 类型的对象。因此,它对 getInsurance 的调用是非法的。

下面应用 map 和 flatMap 对上述示例进行重写:

  public String getCarInsuranceName(Optional<Person&gt; person) { 
      return person.flatMap(Person::getCar)
                       .flatMap(Car::getInsurance)
                       .map(Insurance::getName)
                       .orElse("Unknown"); // 如果Optional的结果 值为空设置默认值
  }

Optional对象其他方法

isPresent()方法

可以使用 isPresent() 方法检查 Optional 对象是否包含非空值,例如:

Optional&lt;String&gt; optional = Optional.of("Hello World");
if (optional.isPresent()) { 
    System.out.println(optional.get()); 
}
filter()方法

filter() 方法接受一个谓词作为参数。如果 Optional 对象的值存在,并且它符合谓词的条件,filter 方法就返回其值,否则它就返回一个空的 Optional 对象。

比如,你可能需要检查保险公司的名称是否为 “Cambridge-Insurance”。

Insurance insurance = ...;
if(insurance != null && "CambridgeInsurance".equals(insurance.getName())){
		System.out.println("ok");

}

使用 Optional 对象的 filter 方法,这段代码可以重构如下:

Optional&lt;Insurance&gt; optInsurance = ...;
optInsurance.filter(insurance -&gt; "CambridgeInsurance".equals(insurance.getName()))
            .ifPresent(x -&gt; System.out.println("ok"));
collect()方法

.collect(Collectors.toList())终端操作,核心作用是将流(Stream)中的元素收集并转换为一个 List 集合,是流处理的 “收尾” 步骤。

java
         List<String&gt; collect = Optional.ofNullable(menuList).orElse(new ArrayList<&gt;())
                    .stream()
                    // 是 Java 8 Stream 流中的映射操作,核心作用是将流中的每个 SysMenu 对象转换为其 code
                    //现从 “菜单对象” 到 “权限标识字符串” 的转换
                    .map(item -&gt; item.getCode())
                    .filter(item -&gt; item != null)
                    //collect(Collectors.toList()) 是终端操作,核心作用是将流(Stream)中的元素收集并转换为一个 List 集合,是流处理的 “收尾” 步骤。
                    //Collectors.toList():java.util.stream.Collectors 工具类提供的静态方法,返回一个收集器(Collector),
                    // 该收集器的作用是将流中的元素按顺序存入一个 List 集合中
                    .collect(Collectors.toList());

假设流经过 mapfilter 后包含以下元素:

["user:view", "user:add", "menu:edit"]

调用 .collect(Collectors.toList()) 后,会得到一个 List 集合:

List<String&gt; collect = ["user:view", "user:add", "menu:edit"]

扩展:其他收集方式

Collectors 还提供了其他收集器,用于满足不同需求:

  • Collectors.toSet():收集为 Set 集合(去重,无序)。
  • Collectors.toCollection(ArrayList::new):指定具体的 List 实现类(如 LinkedList)。
  • Collectors.joining(","):将元素拼接为字符串(如 "user:view,user:add"

3.会员管理、会员卡管理、会员充值、办卡模块相关

@Transactional 事务注解详解

什么是事务?

事务(Transaction)就像银行转账一样:

  • 要么全部成功(A账户扣钱,B账户加钱)

  • 要么全部失败(A账户不扣钱,B账户不加钱)

  • 不能出现A账户扣了钱,但B账户没加钱的情况

MemberServiceImpl

java
package com.doc.web.member.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.doc.web.member.entity.JoinParam;
import com.doc.web.member.entity.Member;
import com.doc.web.member.entity.RechargeParam;
import com.doc.web.member.mapper.MemberMapper;
import com.doc.web.member.service.MemberService;
import com.doc.web.member_apply.entity.MemberApply;
import com.doc.web.member_apply.mapper.MemberApplyMapper;
import com.doc.web.member_card.entity.MemberCard;
import com.doc.web.member_card.mapper.MemberCardMapper;
import com.doc.web.member_recharge.Service.MemberRechargeService;
import com.doc.web.member_recharge.entity.MemberRecharge;
import com.doc.web.member_role.entity.MemberRole;
import com.doc.web.member_role.service.MemberRoleService;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

@Service
public class MemberServiceImpl extends ServiceImpl MemberMapper,  Member implements MemberService {
    @Resource
    private MemberCardMapper memberCardMapper;

    @Resource
    private MemberApplyMapper memberApplyMapper;

    @Autowired
    MemberRoleService memberRoleService;

    @Autowired
    MemberRechargeService memberRechargeService;

    @Override
    @Transactional
    public void addMember(Member member) {
        //新增会员
        int insert = this.baseMapper.insert(member);
        
        
        //简单示例(等价思路)
 //等价于你手写一个 Mapper 的 insert SQL:
 //INSERT INTO member (name, username, ...) VALUES (# //{name}, #{username}, ...)
 //MyBatis-Plus帮你省去了写 XML/注解 SQL 的工作
        //this:当前 MemberServiceImpl 实例。
//baseMapper:继承自 ServiceImpl 的 Mapper 成员。
//insert(...):BaseMapper 定义的插入方法。
//member:要插入的实体对象(行数据)
        
        //设置会员角色
        
        //insert 是 this.baseMapper.insert(member) 的返回值,类型是 int,表示数据库受影响的行数。
//单条插入时,成功通常返回 1;失败返回 0(比如约束失败、未执行等)。
//insert &gt; 0 等价于“成功插入了至少一行”,所以后续才会执行设置角色等逻辑。
        if(insert小于0){
            MemberRole role = new MemberRole();
            role.setMemberId(member.getMemberId());
            role.setRoleId(member.getRoleId());
            memberRoleService.save(role);
        }
    }


    @Override
    @Transactional
    public void editMember(Member member) {
        int i = this.baseMapper.updateById(member);
        //设置角色 先删除后插入
        if (i &gt; 0) {
            //删除角色
            QueryWrapper<MemberRole&gt; query = new QueryWrapper<&gt;();
            query.lambda().eq(MemberRole::getMemberId, member.getMemberId());
            memberRoleService.remove(query);

            //重新插入
            MemberRole role = new MemberRole();
            role.setMemberId(member.getMemberId());
            role.setRoleId(member.getRoleId());
            memberRoleService.save(role);
        }
    }

    @Override
    @Transactional
    public void deleteMember(Long memberId) {
        //删除
        int i = this.baseMapper.deleteById(memberId);
        if (i &gt; 0) {
            //删除角色
            QueryWrapper MemberRole  query = new QueryWrapper<&gt;();
            query.lambda().eq(MemberRole::getMemberId, memberId);
            memberRoleService.remove(query);
        }
    }

    @Override
    @Transactional
    public void joinApply(JoinParam joinParm) throws ParseException {
        Member select = this.baseMapper.selectById(joinParm.getMemberId());
        //更新会员
        MemberCard card = memberCardMapper.selectById(joinParm.getCardId()); //获取会员卡相关信息
        //根据前端传来的卡种 ID 把卡规则读出来
        Member member = new Member();
        member.setMemberId(joinParm.getMemberId());
        member.setCardType(card.getTitle());
        member.setCardDay(card.getCardDay());
        member.setPrice(card.getPrice());
        //取出会员旧截止日字符串
        String endTime = select.getEndTime();
        //拿到日历工具类实例,用来加减天数
        Calendar calendar = Calendar.getInstance();
        if(StringUtils.isNotEmpty(endTime)){
            //如果会员旧截止日不为空,则更新截止日
            Date date = new SimpleDateFormat("yyyy-MM-dd").parse(select.getEndTime());
            calendar.setTime(date);
            //在旧日期上加上卡天数
            calendar.add(Calendar.DATE, card.getCardDay());
        }else{
            Date date = new Date();
            calendar.setTime(date);
            //如果会员旧截止日为空,则更新截止日为当前日期加上卡天数
            calendar.add(Calendar.DATE, card.getCardDay());
        }
        member.setEndTime(new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime()));
        this.baseMapper.updateById(member);
        //插入明细
        MemberApply memberApply = new MemberApply();
        memberApply.setCardDay(card.getCardDay());
        memberApply.setCardType(card.getTitle());
        memberApply.setMemberId(joinParm.getMemberId());
        memberApply.setPrice(card.getPrice());
        memberApply.setCreateTime(new Date());
        memberApplyMapper.insert(memberApply);
    }

    @Override
    @Transactional
    public void recharge(RechargeParam parm) {
        //生成充值明细
        MemberRecharge recharge = new MemberRecharge();
        recharge.setMemberId(parm.getMemberId());
        recharge.setMoney(parm.getMoney());
        boolean save = memberRechargeService.save(recharge);
        if(save){
            //更新余额
            this.baseMapper.addMoney(parm);
        }
    }
}

相关业务代码的解读

java
    @Override
    @Transactional
    public void addMember(Member member) {
        //新增会员
        int insert = this.baseMapper.insert(member);
        
        
        //简单示例(等价思路)
 //等价于你手写一个 Mapper 的 insert SQL:
 //INSERT INTO member (name, username, ...) VALUES (# //{name}, #{username}, ...)
 //MyBatis-Plus帮你省去了写 XML/注解 SQL 的工作
        //this:当前 MemberServiceImpl 实例。
//baseMapper:继承自 ServiceImpl 的 Mapper 成员。
//insert(...):BaseMapper 定义的插入方法。
//member:要插入的实体对象(行数据)
        
        //设置会员角色
        
        //insert 是 this.baseMapper.insert(member) 的返回值,类型是 int,表示数据库受影响的行数。
//单条插入时,成功通常返回 1;失败返回 0(比如约束失败、未执行等)。
//insert &gt; 0 等价于“成功插入了至少一行”,所以后续才会执行设置角色等逻辑。
        if(insert&gt;0){
            MemberRole role = new MemberRole();
            role.setMemberId(member.getMemberId());
            role.setRoleId(member.getRoleId());
            memberRoleService.save(role);
        }
    }
业务顺序
  1. 执行 insert 插入会员 → 返回 insert 行数,并把自增的 memberId 回填到 member 对象。

  2. if (insert > 0) 成立 → 表示会员插入成功才继续。

  3. new MemberRole() → 构造关联实体。

  4. setMemberId(member.getMemberId())、setRoleId(member.getRoleId()) → 用前面插入成功后得到的 memberId 和传入的 roleId 组装关系数据。

  5. memberRoleService.save(role) → 这时才“落表”,向 member_role 表插入一行关联记录。

所以 memberRoleService.save(role) 必须在 insert 成功、且 role 的两个字段都已设置后执行;方法整体受 @Transactional 保护,任一步失败会整体回滚。

MemberMapper

java
package com.doc.web.member.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.doc.web.member.entity.Member;
import com.doc.web.member.entity.RechargeParam;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface MemberMapper extends BaseMapper<Member&gt; {
    int addMoney(@Param("param")RechargeParam parm);

}

Mapper 原子加余额

  • 接口: MemberMapper.addMoney(@Param("param") RechargeParam parm)

  • 位置: com.doc.web.member.mapper.MemberMapper

  • 作用: 执行一条“在数据库层原子累加余额”的 SQL。典型写法如下(示意):

    sql

    UPDATE member

    SET money = COALESCE(money, 0) + #

    WHERE member_id = #

  • 为什么要自定义这个方法而不用 updateById:

  • 这是“增量更新”(money = money + x),需要数据库原子操作,避免读-改-写并发覆盖。

  • 一条 SQL 完成累加,效率高,且不会出现并发下“余额被覆盖”的问题。

int addMoney(@Param("param")RechargeParam parm)

所在位置:

在 MemberMapper extends BaseMapper<Member> 中,属于 MyBatis/MP 的 Mapper 接口自定义方法。

方法含义:

addMoney(...):给某个会员的余额做“增量累加”(money = money + 传入金额)。

参数 RechargeParam parm:封装了充值所需信息(如 memberId、money、可选 userId)。

返回值 int:受影响的行数。正常成功应为 1;0 表示未更新到任何行(如会员不存在或条件不满足)。

@Param("param") 的作用

给方法参数起一个 SQL 可用的名称“param”,便于在对应的 Mapper XML 或注解 SQL 中通过 #{param.xxx} 取值。

例如:

#

#

SysMembebrMapper.xml

xml
<?xml version="1.0" encoding="UTF-8" ?&gt;
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd"&gt;
<mapper namespace="com.doc.web.member.mapper.MemberMapper"&gt;
    <update id="addMoney"&gt;
            update member set money = money + #{parm.money} where member_id = #{parm.memberId}
    </update&gt;
</mapper&gt;

where member_id = ?:更新/查询时只作用于满足条件的那一行(主键等于给定值的会员

MemberController

java
package com.doc.web.member.controller;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.doc.utils.ResultUtils;
import com.doc.utils.ResultVo;
import com.doc.web.member.entity.JoinParam;
import com.doc.web.member.entity.Member;
import com.doc.web.member.entity.PageParam;
import com.doc.web.member.entity.RechargeParam;
import com.doc.web.member.service.MemberService;
import com.doc.web.member_card.entity.MemberCard;
import com.doc.web.member_card.service.MemberCardService;
import com.doc.web.member_role.entity.MemberRole;
import com.doc.web.member_role.service.MemberRoleService;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.text.ParseException;
import java.util.List;

@RestController
@RequestMapping("/api/member")
public class MemberController {
    @Autowired
    private MemberService memberService;

    @Autowired
    private MemberRoleService memberRoleService;

    //新增
    @PostMapping
    public ResultVo add(@RequestBody Member member){
        //判断卡号是否被占用
        QueryWrapper<Member&gt; query = new QueryWrapper<&gt;();
        query.lambda().eq(Member::getUsername, member.getUsername());
        Member one = memberService.getOne(query);
        if(one!=null){
            return ResultUtils.error("会员卡号已被占用");
        }
        memberService.addMember(member);
        return ResultUtils.success("新增成功");
    }

    //编辑
    @PutMapping
    public ResultVo edit(@RequestBody Member member) {
        //判断卡号是否被占用
        QueryWrapper<Member&gt; query = new QueryWrapper<&gt;();
        query.lambda().eq(Member::getUsername, member.getUsername());
        Member one = memberService.getOne(query);
        if(one != null && !one.getMemberId().equals(member.getMemberId())){
            return ResultUtils.error("会员卡号已被占用");
        }
        memberService.editMember(member);
        return ResultUtils.success("编辑成功");

    }

    //删除
    @DeleteMapping("/{memberId}")
    public ResultVo delete(@PathVariable Long memberId) {
        QueryWrapper<Member&gt; query = new QueryWrapper<&gt;();
        query.lambda().eq(Member::getMemberId, memberId);
        Member one = memberService.getOne(query);
        if(one == null){
            return ResultUtils.error("会员不存在");
        }
        memberService.deleteMember(memberId);
        return ResultUtils.success("删除成功");
    }

    //查询
    @GetMapping("/list")
    public ResultVo get(PageParam pageParam) {
        //构造分页对象
        IPage<Member&gt; page = new Page<&gt;(pageParam.getCurrentPage(), pageParam.getPageSize());

        //构造查询条件
        QueryWrapper<Member&gt; query = new QueryWrapper<&gt;();
        if(StringUtils.isNotEmpty(pageParam.getName())){
            query.lambda().like(Member::getName,pageParam.getName());
        }
        if(StringUtils.isNotEmpty(pageParam.getPhone())){
            query.lambda().like(Member::getPhone,pageParam.getPhone());
        }
        if(StringUtils.isNotEmpty(pageParam.getUsername())){
            query.lambda().like(Member::getUsername,pageParam.getUsername());
        }
        query.lambda().orderByDesc(Member::getJoinTime);
        IPage<Member&gt; list = memberService.page(page, query);
        return ResultUtils.success("查询成功",list);
    }

    //根据会员ID查询角色ID(兼容老路径)
    @GetMapping({"/getRole","/getRoleByMemberId"})
    public ResultVo getRoleByMemberId(@RequestParam("memberId") Long memberId) {
        QueryWrapper<MemberRole&gt; query = new QueryWrapper<&gt;();
        query.lambda().eq(MemberRole::getMemberId,memberId);
        MemberRole one = memberRoleService.getOne(query);
        if(one == null){
            return ResultUtils.error("未找到该会员的角色信息");
        }
        return ResultUtils.success("查询成功",one.getRoleId());
    }


    @Autowired
    private MemberCardService memberCardService;

    //查询会员卡列表
    @GetMapping("/getCardList")
    public ResultVo getCardList() {
        QueryWrapper<MemberCard&gt; query = new QueryWrapper<&gt;();
        query.lambda().eq(MemberCard::getStatus,"1");
        List<MemberCard&gt; list = memberCardService.list(query);
        return ResultUtils.success("查询成功",list);
    }

    //办卡提交
    @PostMapping("/joinApply")
    public ResultVo joinApply(@RequestBody JoinParam joinParm) throws ParseException {
        memberService.joinApply(joinParm);
        return ResultUtils.success("办卡成功");
    }

    //充值
    @PostMapping("/recharge")
    public ResultVo recharge(@RequestBody RechargeParam parm) {
        memberService.recharge(parm);
        return ResultUtils.success("充值成功");
    }
}

@RequestParam、@RequestBody、@PathVariable

@RequestParam("memberId") Long memberId,在代码前面加上了@RequestParam("memberId"),同时代码内容也存在,@RequestBody,@PathVariable 的使用,那学习的过程中应该怎么判断这三个应该在什么场景下进行应用。他们三个的基础语法以及之间的关联又是什么?

  • @PathVariable:URL 路径的一部分,用来标识资源

场景:/api/member/9、/orders/2025/items/100

语义:层级资源定位,REST 风格最常用(GET/PUT/DELETE 常用)

  • @RequestParam:查询字符串或表单键值对里的参数

场景:/api/member/list?page=1&pageSize=10、表单 application/x-www-form-urlencoded

语义:过滤器、分页器、排序器这类“可选筛选项”

  • @RequestBody:请求体中的 JSON/XML 等结构化数据

场景:创建/编辑时提交对象数据(POST/PUT/PATCH),Content-Type: application/json

语义:提交复杂对象或集合(如新增用户、批量操作

Path 变量
java
@GetMapping("/api/member/{memberId}")
public ResultVo get(@PathVariable Long memberId) { ... }

// 自定义名称映射
@GetMapping("/api/{type}/{id}")
public ResultVo get(@PathVariable("id") Long memberId, @PathVariable String type) { ... }
查询参数 / 表单参数
java
@GetMapping("/api/member/list")
public ResultVo list(@RequestParam Integer page, 
                     @RequestParam(defaultValue="10") Integer pageSize,
                     @RequestParam(required=false) String name) { ... }

// 别名映射
@GetMapping("/api/search")
public ResultVo search(@RequestParam("q") String keyword) { ... }
请求体(JSON)
java
@PostMapping("/api/member")
public ResultVo add(@RequestBody Member member) { ... }

@PutMapping("/api/member/{id}")
public ResultVo edit(@PathVariable Long id, @RequestBody Member member) { ... }
如何判断用哪个
  • URL 已经“包含”参数且用于定位单个资源 → 用 @PathVariable

  • 过滤/分页/排序等“可选条件” → 用 @RequestParam

  • 需要提交“结构化对象或数组” → 用 @RequestBody

  • 二者组合常见:路径定位资源 + 体内提交修改数据(@PathVariable + @RequestBody)

常见坑

GET 请求不要放 @RequestBody(多数客户端/网关丢弃或不支持)

@RequestParam + required=true(默认)缺参会 400,可用 required=false 或 defaultValue

@PathVariable 名称需与模板一致或显式指定别名

@RequestBody 必须匹配 Content-Type: application/json 且 JSON 字段与对象属性对齐

@RequestParam("memberId") Long memberId的前面也可以用@PathVariable吗?

可以,但前提是“参数来自路径”而不是“查询串”。

  • 如果你的路由是 /api/member/getRoleByMemberId?memberId=9,参数在查询串,用 @RequestParam("memberId") Long memberId。

  • 如果你把路由设计成 /api/member/getRoleByMemberId/9 或更 REST 的 /api/member/9/role,参数在路径段,用 @PathVariable("memberId") Long memberId。

查询参数写法:

java
@GetMapping("/api/member/getRoleByMemberId")
public ResultVo getRole(@RequestParam("memberId") Long memberId) { ... }

路径变量写法

java
@GetMapping("/api/member/{memberId}/role")
public ResultVo getRole(@PathVariable("memberId") Long memberId) { ... }

4.登录相关后端代码

loginController

用户登录流程:

  1. 前端请求验证码 → 2. 输入用户名/密码/验证码 → 3. 后端验证 →4. 生成JWT Token → 5. 返回给前端 → 6. 后续请求携带Token
java
package com.xq.web.login.controller;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.xq.jwt.JwtUtils;
import com.xq.utils.ResultUtils;
import com.xq.utils.ResultVo;
import com.xq.web.login.entity.InfoParam;
import com.xq.web.login.entity.LoginParm;
import com.xq.web.login.entity.LoginResult;
import com.xq.web.login.entity.UserInfo;
import com.xq.web.member.entity.Member;
import com.xq.web.member.service.MemberService;
import com.xq.web.sys_menu.entity.MakeMenuTree;
import com.xq.web.sys_menu.entity.RouterVo;
import com.xq.web.sys_menu.entity.SysMenu;
import com.xq.web.sys_menu.service.SysMenuService;
import com.xq.web.sys_user.entity.SysUser;
import com.xq.web.sys_user.service.SysUserService;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.*;
import sun.misc.BASE64Encoder;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/login")
public class LoginController {

    @Autowired
    //返回值类型,表示这个方法会返回一个 DefaultKaptcha 对象DefaultKaptcha 是验证码生成器
    private DefaultKaptcha defaultKaptcha;

    //生成验证码
    @PostMapping("/image")
    
    
    public ResultVo imageCode(HttpServletRequest request) throws IOException{
        //获取验证码字符
        String text = defaultKaptcha.createText();
        //将验证码存储到session里面
        HttpSession session = request.getSession();
        session.setAttribute("code",text);
        //生成图片
        //通过配置好的 DefaultKaptcha 实例,根据验证码文本 text 生成对应的验证码图片,并将图片数据存储在 BufferedImage 对象中。
        BufferedImage bufferedImage = defaultKaptcha.createImage(text);
        //这行代码的作用是声明一个 ByteArrayOutputStream 类型的变量 outputStream,并初始化为 null。它通常是在需要使用内存字节流临时存储数据(如图片、文本的字节)时,为后续的流操作做准备
        ByteArrayOutputStream outputStream = null;
        try{
            outputStream = new ByteArrayOutputStream();
          //  ImageIO 是 Java 核心库 javax.imageio 包下的工具类,专门用于图像的读取、写入和处理,也是你代码中验证码图片转 Base64 的关键工具。将 BufferedImage 类型的验证码图片,以 JPG 格式写入到 ByteArrayOutputStream(内存字节流)中。
            ImageIO.write(bufferedImage,"jpg",outputStream);
            //实例化 Base64 编码器,为后续调用 encode 方法做准备。
            BASE64Encoder encoder = new BASE64Encoder();
            //先通过 outputStream.toByteArray(),把存储图像数据的内存字节流(ByteArrayOutputStream)转为 byte[] 数组。再调用 encoder.encode(...) 方法,对字节数组进行 Base64 编码,最终得到 Base64 格式的字符串。
            String base64 = encoder.encode(outputStream.toByteArray());
            //前缀 data:image/jpeg;base64,:是 HTML 中支持的数据 URI 格式,告诉前端 “这是一个 JPEG 格式的 Base64 图片”,让浏览器能直接解析渲染,无需额外请求图片文件。
            //base64.replaceAll("\r\n", ""):去除 Base64 字符串中的换行符(仅针对 sun.misc.BASE64Encoder 编码结果),避免换行符导致前端解析失败。
            String captchaBase64 = "data:image/jpeg;base64," + base64.replaceAll("\r\n", "");
           
            ResultVo result = new ResultVo("生成成功",200,captchaBase64);
            return result;
        }catch (IOException e){
            e.printStackTrace();
        }finally {
            if(outputStream != null){
                outputStream.close();
            }
        }
        return null;
        
    }
    @Autowired
    MemberService memberService;

    @Autowired
    SysUserService sysUserService;

    @Autowired
    JwtUtils jwtUtils;

    //登录
    @PostMapping("/login")
    public ResultVo login(HttpServletRequest request,@RequestBody LoginParm loginParam) {
        //获取session
        HttpSession session = request.getSession();
        //获取验证码
        String code = (String) session.getAttribute("code");
        //判断验证码是否正确
        if (!code.equals((loginParam.getCode()))) {
            return ResultUtils.error("验证码错误");
        }
        //获取用户登录时输入的原始密码(字符串),通过 getBytes() 转为字节数组(MD5 加密的输入是字节流)。
        //DigestUtils 是 Apache Commons Codec 工具类(项目已引入该依赖),提供便捷的加密方法。
        String password = DigestUtils.md5DigestAsHex(loginParam.getPassword().getBytes());

        //用户类型判断
        String token = null;
        if (loginParam.getUserType().equals("1")) { //会员
            //构造查询条件
            QueryWrapper<Member&gt; query = new QueryWrapper<&gt;();
            query.lambda().eq(Member::getUsername, loginParam.getUsername()).eq(Member::getPassword, loginParam.getPassword());
            Member one = memberService.getOne(query);
            //通过 MyBatis-Plus 的 getOne 方法,根据构建的查询条件(query)从数据库中查询唯一一条会员记录
            if (one == null) {
                return ResultUtils.error("用户或密码错误");
            }
            //生成token
            //生成用户身份令牌(JWT Token,并将用户关键信息嵌入令牌中,作为用户后续访问系统的 “身份凭证”
            //作为用户登录成功后的 “通行证”,前端会存储这个令牌,后续请求系统接口时携带令牌,证明 “我是已登录的合法用户
            Map<String, String&gt; map = new HashMap<&gt;();
            map.put("userId", Long.toString(one.getMemberId()));
            map.put("username", one.getUsername());
            //调用 JwtUtils 工具类的 generateToken 方法,传入上面准备的用户信息 map,生成一个完整的 JWT 令牌字符串
            token = jwtUtils.generateToken(map);
            System.out.println("token" + token);
            //返回登录成功信息
            LoginResult result = new LoginResult();
            result.setToken(token);
            result.setUserId(one.getMemberId());
            result.setUername(one.getName());
            result.setUserType(loginParam.getUserType());
            return ResultUtils.success("登陆成功", result);

        } else if (loginParam.getUserType().equals("2")) {
            //员工
            QueryWrapper<SysUser&gt; query = new QueryWrapper<&gt;();
            query.lambda().eq(SysUser::getUsername, loginParam.getUsername()).eq(SysUser::getPassword, password);
            SysUser one = new SysUser();
            if (one == null) {
                return ResultUtils.error("用户名或密码错误!");
            }
            //生成token
            Map<String, String&gt; map = new HashMap<&gt;();
            map.put("userId", Long.toString(one.getUserId()));
            map.put("username", one.getUsername());
            String Token = jwtUtils.generateToken(map);
            System.out.println("token" + token);
            //返回登录信息
            LoginResult result = new LoginResult();
            result.setToken(token);
            result.setUserId(one.getUserId());
            result.setUername(one.getNickName());
            result.setUserType(loginParam.getUserType());
            return ResultUtils.success("登陆成功", result);

        }
        else{
            return ResultUtils.error("用户类型错误!");
        }
    }
}

HttpServletRequest接口

HttpServletRequest 就像一个"快递单"

当用户在浏览器上做任何操作(点击按钮、提交表单等),都会发送一个 HTTP 请求 到服务器。HttpServletRequest 就是服务器收到的这个请求的完整信息

获取会话与属性

获取session
java
HttpSession session = request.getSession();
session.setAttribute("code",text);
  • HttpSession - 返回值类型(会话对象)
  • request.getSession() - 获取会话

Session 就像一个"用户档案夹" 服务器为每个用户创建一个档案夹 用来存储这个用户的临时数据

HttpSession 接口

HttpSession 接口提供了一系列用于管理用户会话状态的核心方法,主要可分为属性操作会话信息查询生命周期管理三大类

获取会话与属性(请求域数据)

void setAttribute(String name, Object value) 向请求域中存储数据(仅在当前请求有效,转发时可共享)request.setAttribute("msg", "登录成功");(在 Servlet 中存数据,JSP 中取)

BufferedImage

是 Java AWT(抽象窗口工具包)中的一个核心类(位于 java.awt.image 包下),用于在内存中存储和操作图像的像素数据。它是 Image 类的子类,提供了直接访问和修改图像像素的能力,是处理静态图像(如验证码、图片编辑、格式转换等)的基础工具。

catch (IOException e):捕获 IO 异常

  • 针对 ImageIO.writeoutputStream.toByteArray() 等 IO 操作可能抛出的 IOException 进行捕获。
  • e.printStackTrace():打印异常堆栈信息(包含异常类型、出错位置、调用链路),方便开发时排查问题(比如图像格式错误、流操作失败等)。

finally 块:强制释放资源

  • finally 块的特性是无论 try 块是否正常执行、catch 是否捕获到异常,都会执行,确保资源不会泄漏。
  • 逻辑:判断 outputStream(内存字节流)是否为 null,非 null 则调用 close() 关闭流,释放其占用的内存资源。

JWT令牌

java
package com.doc.jwt;


import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Calendar;
import java.util.Date;
import java.util.Map;

@Component
@Data
@ConfigurationProperties(prefix = "jwt")
public class JwtUtils {
    //颁发者
    private String issuer;
    //密钥
    private String secret;
    //过期时间
    private int expiration;

    /**
     * 生成token
     * @param map
     * @return
     */

    public String generateToken(Map<String,String&gt; map){
        //设置令牌的过期时间  日历类 获取一个 Calendar 对象
        //用来处理日期和时间
        Calendar instance = Calendar.getInstance();
        //设置失效时间
        instance.add(Calendar.MINUTE, expiration);
        //创建JWT Bulier
        JWTCreator.Builder builder = JWT.create();
        //payload
        map.forEach((k,v) -&gt; {;
            builder.withClaim(k,v);
        });

        //指定令牌的过期时间
        //这行代码是利用 JWTCreator.Builder 链式链式 调用构建并生成 JWT 令牌的核心逻辑,通过连续设置令牌属性并最终签名,完成 Token 的生成
        String token = builder.withIssuer(issuer)
                //设置 iss 标准声明(令牌颁发者,如系统名称,对应你配置中的 jwt.issuer)
                .withIssuedAt(new Date())
                //设置 iat 标准声明(令牌签发时间,当前时间)。
                .withExpiresAt(instance.getTime())
                //设置 exp 标准声明(令牌过期时间,通过 Calendar 计算的未来时间)。
                .sign(Algorithm.HMAC256(secret));
                //生成 Token 字符串
        return token;
    }
    /**
     * 验证令牌是否合法
     * @param token
     * @return
     */
    public boolean verify(String token){
        try {
            JWT.require(Algorithm.HMAC256(secret)).build().verify(token);
        }
        catch (JWTVerificationException e){
            return false;
        }
        return true;
    }

    /**
     * 解析token
     * @param token
     * @return
     */
    public DecodedJWT jwtDecode(String token){
        try {
            return JWT.require(Algorithm.HMAC256(secret)).build().verify(token);
        } catch (SignatureVerificationException e) {
            throw new RuntimeException("token签名错误!");
        } catch (AlgorithmMismatchException e) {
            throw new RuntimeException("token算法不匹配!");
        } catch (TokenExpiredException e) {
            throw new RuntimeException("token过期!");
        } catch (Exception e) {
            throw new RuntimeException("token解析失败!");
        }
    }




}

1、什么是jwt

JWT全称是Json Web Token。JWT由三部分组成Header(头)、Payload(有效载荷)和Signature(签名)。

Header:记录令牌类型、签名算法等 例如:

Payload(有效载荷)作用:携带一些用户信息 例如{"userId":"1","username":"mingzi"};需要注意的是jwt看似是十分复杂的密码,其实可以非常轻松的被破解。所以Payload中存放的用户信息不可以涉及到隐私,比如密码或者手机号等等

Signature(签名)作用:防止JWT被篡改、确保安全性 例如 计算出来的签名,一个字符串

5.购买商品

compareTo()

在进行余额是不是足够的时候,可以用compareTo()进行对比

java
    @PostMapping("/joinCourse")
    public ResultVo joinCourse(@RequestBody MemberCourse memberCourse) {
        //查询是否已报名
        QueryWrapper<MemberCourse&gt; query = new QueryWrapper<&gt;();
        query.lambda().eq(MemberCourse::getMemberId, memberCourse.getMemberId())
                .eq(MemberCourse::getCourseId, memberCourse.getCourseId());
        MemberCourse one = memberCourseService.getOne(query);
        if(one!=null){
            return ResultUtils.error("已报名");
        }
        //判断余额是否充足
        Course course = courseService.getById(memberCourse.getCourseId());
        Member member = memberService.getById(memberCourse.getMemberId());
        int flag =  member.getMoney().compareTo(course.getCoursePrice());
        if(flag==-1){
            return ResultUtils.error("余额不足");
        }
        memberCourseService.joinCourse(memberCourse);
        return ResultUtils.success("报名成功");
    }

6.订单管理

java
/**
 * - 用户验证 :根据userId查询用户信息
 * - 商品处理 :遍历订单中的每个商品项
 * - 价格计算 :单价 × 数量 = 总价(保留2位小数)
 * - 数据组装 :将商品信息、用户信息、价格等组装成订单记录
 * - 批量保存 :使用 saveBatch 方法批量插入订单数据
 */
@RestController
@RequestMapping("/api/goods_order")
public class GoodsOrderController {
    @Autowired
    SysUserService sysUserService;
    @Autowired
    private GoodsService goodsService;

    @Autowired
    private GoodsOrderService goodsOrderService;

    //下单
    @PostMapping("/down")
    public ResultVo down(@RequestBody OrderParm parm) {
        //查询用户信息
        SysUser sysUser = sysUserService.getById(parm.getUserId());

        List<OrderItem&gt; list = parm.getOrderList();
        List<GoodsOrder&gt; orderList = new ArrayList<&gt;();

        // 3. 遍历订单中的每个商品项,创建对应的订单记录
        for (int i = 0; i < list.size(); i++) {
            Long goodsId = list.get(i).getGoodsId();
            Integer num = list.get(i).getNum();

            //查询商品信息
            Goods goods = goodsService.getById(goodsId);
            // 创建订单对象
            GoodsOrder order = new GoodsOrder();
            BeanUtils.copyProperties(goods, order);
            // 复制商品属性到订单
            // 设置订单数量和总价
            order.setNum(num);
            //BigDecimal 是Java中用于 精确小数运算 的类,避免浮点数精度问题
            //价格计算 - 转换为BigDecimal
            BigDecimal number = BigDecimal.valueOf(list.get(i).getNum());
            BigDecimal price = goods.getPrice();
            //数量 × 单价 = 总价
            BigDecimal total = number.multiply(price);
            //设置小数位数和舍入规则
            BigDecimal totalPrice = total.setScale(2, BigDecimal.ROUND_HALF_UP);

            //设置总价到订单对象
            order.setTotalPrice(totalPrice);
            //设置操作用户
            order.setControlUser(sysUser.getNickName());
            //将处理好的订单对象添加到列表中
            orderList.add(order);
        }
        if (orderList.size() &gt; 0) {
            //saveBatch 是 MyBatis-Plus 框架提供的一个批量保存方法
            goodsOrderService.saveBatch(orderList);
        }
        return ResultUtils.success("下单成功");
    }
}

7.主页数据统计

sql
    <select id="hotGoods" resultType="com.doc.web.home.entity.EchartItem"&gt;
        SELECT g.name AS name, SUM(gd.num) AS value
        from goods as g

        INNER JOIN good_order as gd ON og.goods_id = g.goods_id
        GROUP BY og.goods_id
        ORDER BY value DESC
        LIMIT 7
    </select&gt;
    <select id="hotCard" resultType="com.doc.web.home.entity.EchartItem"&gt;
        SELECT ma.card_type AS name, count(ma.apply_id) AS value
        from member_apply as ma
        GROUP BY ma.card_type,ma.card_day
        ORDER BY value DESC
        LIMIT 7
    </select&gt;

    <select id ="hotCourse" resultType="com.doc.web.home.entity.EchartItem"&gt;
       SELECT mc.course_name AS name, count(ma.member_course_id) AS value
       FROM course as c
         LEFT JOIN member_course as mc ON c.course_id = mc.course_id
            GROUP BY c.course_id
    </select&gt;

JOIN 类型选择的核心原则

JOIN 类型包含数据适用场景
INNER JOIN只包含匹配的记录只关心有关联数据的情况
LEFT JOIN包含左表所有记录需要保留左表完整数据
RIGHT JOIN包含右表所有记录需要保留右表完整数据

业务分析

  • 商品销量排行 ( hotGoods ):

    只关心 有销量的商品

    没销量的商品不需要显示

    → 用 INNER JOIN

  • 课程热度统计 ( hotCourse ):

    需要显示 所有课程

    包括 没人选的课程 (选课人数为0)

    → 用 LEFT JOIN

  • 会员卡类型统计 ( hotCard ):

    数据都在一张表

    → 不需要 JOIN