运动管理系统后端学习笔记
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/ # 模板文件| 盒子 | 全名 | 面向谁 | 核心任务 | 典型字段差异 | 改动代价 |
|---|---|---|---|---|---|
| Entity | Persistence Entity / ORM 实体 | 数据库 | 1:1 映射表结构,保证 ACID、索引、关联、生命周期都与 DB 同步 | 字段名=列名,类型=SQL 类型,外键对象/注解齐全 | 牵一发动全身——加列要发 DDL,影响整个系统 |
| DTO | Data Transfer Object | 进程/服务之间 | 把数据“原封不动”搬个家,解决网络序列化、内部保密、懒加载等问题 | 可能砍掉敏感字段(password)、把懒加载集合展平成 ID 列表 | 只影响调用双方,可随时加字段兼容 |
| VO | View Object / Value Object | 前端界面 | 按 UI 需要“剪枝+化妆”——只留要的、转单位、格式化、加中文文本 | 金额分→元,Date→yyyy-MM-dd,status→“已支付”,甚至拼好 URL、图标 | 纯展示,想改就改,与 DB 无关 |
1.Mapper与Service层的逻辑
| 层级 | 关注点 | 例子 |
|---|---|---|
| Mapper | 与表一一对应的 物理 SQL | SELECT * 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:#f1f8e91. 实体类(Entity层)- 数据模型
SysUser.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的基础MapperBaseMapper提供了基本的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.系统菜单管理模块相关
扁平菜单列表变成树形结构
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(() -> Integer.MAX_VALUE);orElseThrow()方法
orElseThrow() 方法,它和 get 方法非常类似,它们遭遇 Optional 对象为空时都会抛出一个异常,但是使用 orElseThrow 可以定制希望抛出的异常类型:
optional.orElseThrow(() -> new RuntimeException("student不存在!"));ifPresent(Consumer<? super T> consumer)方法
ifPresent(Consumer<? super T> consumer) 方法,它让能在变量值存在时执行一个作为参数传入的方法,否则就不进行任何操作:
optional.ifPresent(o -> o.setAge(18));Optional对象中值的提取和转换
map()方法
map() 方法,如果值存在,就对该值执行提供的 mapping 函数调用,如果值不存在,则返回一个空的 Optional 对象。
引入 Optional 以前:
String name = null;
if(insurance != null){
name = insurance.getName();
}
引入 Optional 以后:
Optional<String> 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<Person> optPerson = Optional.of(person);
Optional<String> 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> person) {
return person.flatMap(Person::getCar)
.flatMap(Car::getInsurance)
.map(Insurance::getName)
.orElse("Unknown"); // 如果Optional的结果 值为空设置默认值
}Optional对象其他方法
isPresent()方法
可以使用 isPresent() 方法检查 Optional 对象是否包含非空值,例如:
Optional<String> 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<Insurance> optInsurance = ...;
optInsurance.filter(insurance -> "CambridgeInsurance".equals(insurance.getName()))
.ifPresent(x -> System.out.println("ok"));collect()方法
.collect(Collectors.toList()) 是终端操作,核心作用是将流(Stream)中的元素收集并转换为一个 List 集合,是流处理的 “收尾” 步骤。
List<String> collect = Optional.ofNullable(menuList).orElse(new ArrayList<>())
.stream()
// 是 Java 8 Stream 流中的映射操作,核心作用是将流中的每个 SysMenu 对象转换为其 code
//现从 “菜单对象” 到 “权限标识字符串” 的转换
.map(item -> item.getCode())
.filter(item -> item != null)
//collect(Collectors.toList()) 是终端操作,核心作用是将流(Stream)中的元素收集并转换为一个 List 集合,是流处理的 “收尾” 步骤。
//Collectors.toList():java.util.stream.Collectors 工具类提供的静态方法,返回一个收集器(Collector),
// 该收集器的作用是将流中的元素按顺序存入一个 List 集合中
.collect(Collectors.toList());假设流经过 map 和 filter 后包含以下元素:
["user:view", "user:add", "menu:edit"]调用 .collect(Collectors.toList()) 后,会得到一个 List 集合:
List<String> 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
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 > 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 > 0) {
//删除角色
QueryWrapper<MemberRole> query = new QueryWrapper<>();
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 > 0) {
//删除角色
QueryWrapper MemberRole query = new QueryWrapper<>();
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);
}
}
}相关业务代码的解读
@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 > 0 等价于“成功插入了至少一行”,所以后续才会执行设置角色等逻辑。
if(insert>0){
MemberRole role = new MemberRole();
role.setMemberId(member.getMemberId());
role.setRoleId(member.getRoleId());
memberRoleService.save(role);
}
}业务顺序
执行 insert 插入会员 → 返回 insert 行数,并把自增的 memberId 回填到 member 对象。
if (insert > 0) 成立 → 表示会员插入成功才继续。
new MemberRole() → 构造关联实体。
setMemberId(member.getMemberId())、setRoleId(member.getRoleId()) → 用前面插入成功后得到的 memberId 和传入的 roleId 组装关系数据。
memberRoleService.save(role) → 这时才“落表”,向 member_role 表插入一行关联记录。
所以 memberRoleService.save(role) 必须在 insert 成功、且 role 的两个字段都已设置后执行;方法整体受 @Transactional 保护,任一步失败会整体回滚。
MemberMapper
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> {
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 version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.doc.web.member.mapper.MemberMapper">
<update id="addMoney">
update member set money = money + #{parm.money} where member_id = #{parm.memberId}
</update>
</mapper>where member_id = ?:更新/查询时只作用于满足条件的那一行(主键等于给定值的会员
MemberController
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> query = new QueryWrapper<>();
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> query = new QueryWrapper<>();
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> query = new QueryWrapper<>();
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> page = new Page<>(pageParam.getCurrentPage(), pageParam.getPageSize());
//构造查询条件
QueryWrapper<Member> query = new QueryWrapper<>();
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> list = memberService.page(page, query);
return ResultUtils.success("查询成功",list);
}
//根据会员ID查询角色ID(兼容老路径)
@GetMapping({"/getRole","/getRoleByMemberId"})
public ResultVo getRoleByMemberId(@RequestParam("memberId") Long memberId) {
QueryWrapper<MemberRole> query = new QueryWrapper<>();
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> query = new QueryWrapper<>();
query.lambda().eq(MemberCard::getStatus,"1");
List<MemberCard> 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 变量
@GetMapping("/api/member/{memberId}")
public ResultVo get(@PathVariable Long memberId) { ... }
// 自定义名称映射
@GetMapping("/api/{type}/{id}")
public ResultVo get(@PathVariable("id") Long memberId, @PathVariable String type) { ... }查询参数 / 表单参数
@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)
@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。
查询参数写法:
@GetMapping("/api/member/getRoleByMemberId")
public ResultVo getRole(@RequestParam("memberId") Long memberId) { ... }路径变量写法
@GetMapping("/api/member/{memberId}/role")
public ResultVo getRole(@PathVariable("memberId") Long memberId) { ... }4.登录相关后端代码
loginController
用户登录流程:
- 前端请求验证码 → 2. 输入用户名/密码/验证码 → 3. 后端验证 →4. 生成JWT Token → 5. 返回给前端 → 6. 后续请求携带Token
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> query = new QueryWrapper<>();
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> map = new HashMap<>();
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> query = new QueryWrapper<>();
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> map = new HashMap<>();
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
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.write、outputStream.toByteArray()等 IO 操作可能抛出的IOException进行捕获。 e.printStackTrace():打印异常堆栈信息(包含异常类型、出错位置、调用链路),方便开发时排查问题(比如图像格式错误、流操作失败等)。
finally 块:强制释放资源
finally块的特性是无论 try 块是否正常执行、catch 是否捕获到异常,都会执行,确保资源不会泄漏。- 逻辑:判断
outputStream(内存字节流)是否为 null,非 null 则调用close()关闭流,释放其占用的内存资源。
JWT令牌
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> map){
//设置令牌的过期时间 日历类 获取一个 Calendar 对象
//用来处理日期和时间
Calendar instance = Calendar.getInstance();
//设置失效时间
instance.add(Calendar.MINUTE, expiration);
//创建JWT Bulier
JWTCreator.Builder builder = JWT.create();
//payload
map.forEach((k,v) -> {;
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()进行对比
@PostMapping("/joinCourse")
public ResultVo joinCourse(@RequestBody MemberCourse memberCourse) {
//查询是否已报名
QueryWrapper<MemberCourse> query = new QueryWrapper<>();
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.订单管理
/**
* - 用户验证 :根据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> list = parm.getOrderList();
List<GoodsOrder> orderList = new ArrayList<>();
// 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() > 0) {
//saveBatch 是 MyBatis-Plus 框架提供的一个批量保存方法
goodsOrderService.saveBatch(orderList);
}
return ResultUtils.success("下单成功");
}
}7.主页数据统计
<select id="hotGoods" resultType="com.doc.web.home.entity.EchartItem">
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>
<select id="hotCard" resultType="com.doc.web.home.entity.EchartItem">
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>
<select id ="hotCourse" resultType="com.doc.web.home.entity.EchartItem">
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>JOIN 类型选择的核心原则
| JOIN 类型 | 包含数据 | 适用场景 |
|---|---|---|
| INNER JOIN | 只包含匹配的记录 | 只关心有关联数据的情况 |
| LEFT JOIN | 包含左表所有记录 | 需要保留左表完整数据 |
| RIGHT JOIN | 包含右表所有记录 | 需要保留右表完整数据 |
业务分析
商品销量排行 ( hotGoods ):
只关心 有销量的商品
没销量的商品不需要显示
→ 用 INNER JOIN
课程热度统计 ( hotCourse ):
需要显示 所有课程
包括 没人选的课程 (选课人数为0)
→ 用 LEFT JOIN
会员卡类型统计 ( hotCard ):
数据都在一张表
→ 不需要 JOIN