Spring入门项目 - Tlias系统
准备工作
接口文档:接口文档 - 飞书云文档
开发模式:前后端分离开发(前端项目、后端项目 开发和部署都是分开的)
开发流程:需求分析 ➡ 接口设计 ➡ 前后端并行开发 ➡ 测试 ➡ 测试 ➡ 联调
接口测试工具:Apifox(浏览器发起的请求默认都是GET请求,如果需要测试POST、PUT、DELETE请求,需要用到专门的测试工具)
开发风格:Restful架构风格
| REST风格URL | 请求方式 | 含义 | 备注 |
|---|---|---|---|
http://localhost:8080/users/1 |
GET | 查询id为1的用户 | URL定位资源 HTTP动词描述操作 简洁、规范、优雅 |
http://localhost:8080/users/1 |
DELETE | 删除id为1的用户 | |
http://localhost:8080/users |
POST | 新增用户 | |
http://localhost:8080/users |
PUT | 修改用户 |
- REST是风格,是约定方式,约定不是规定,可以打破
- 描述功能模块通常使用复数形式(加s),表示此类资源,而非单个资源。如:users、books
工程搭建
- 创建SpringBoot工程,引入起步依赖、Mybatis、Mysql驱动、Lombok
- 创建数据库表dept,并在application.yaml中配置基本信息
- 准备基础代码结构,并引入实体类Dept以及统一的响应结果封装类
部门管理
部门列表查询
Mapper
@Select("SELECT * FROM dept ORDER BY update_time desc")
List<Dept> findAll();
如果列名和实体类的属性名不匹配,比如create_time为列名,createTime为属性名,那就无法将获取的数据封装到实体类中
解决方式有三种:
- 手动结果映射:
@Results({
@Result(column = "create_time", property = "createTime"),
@Result(column = "update_time", property = "updateTIme")
})
@Select("SELECT * FROM dept ORDER BY update_time desc")
List<Dept> findAll();
- 为列名起别名:
@Select("SELECT id, name, create_time creatTime, update_time updateTime FROM dept ORDER BY update_time desc")
List<Dept> findAll();
- 如果是create_time这种蛇形命名法,可以通过修改配置自动转换为驼峰形式:
mybatis:
configuration:
map-underscore-to-camel-case: true
Service
@Override
public List<Dept> findAll() {
return deptMapper.findAll();
}
Controller
@GetMapping("/depts")
public Result findAll() {
return Result.success(deptService.findAll());
}
删除部门
Mapper
@Delete("DELETE FROM dept WHERE id = #{id}")
void deleteById(Integer id);
Service
@Override
public void deleteById(Integer id) {
deptMapper.deleteById(id);
}
Controller
@DeleteMapping("/depts")
public Result deleteById(@RequestParam Integer id) {
deptService.deleteById(id);
return Result.success();
}
如果请求参数是存放在路径中,可以使用@RequestParam(“参数名”)注解修饰形参:
@RequestParam Integer deptId如果请求参数名与形参变量相同,直接定义方法形参即可接收:
@RequestParam Integer id当声明@RequestParam注解后,该参数在请求时**必须传递**,如果不传递会报错
如果希望即使不传递也不报错,可以在注解中将required设置为false:
@RequestParam(required = false)
添加部门
Mapper
@Insert("INSERT INTO dept VALUES (#{id},#{name},#{createTime},#{updateTime})")
void insertDept(Dept dept);
Service
@Override
public void insertDept(Dept dept) {
// 设置Dept对象时间属性
dept.setCreateTime(LocalDateTime.now());
dept.setUpdateTime(LocalDateTime.now());
deptMapper.insertDept(dept);
}
Controller
@PostMapping("/depts")
public Result insertDept(@RequestBody Dept dept) {
System.out.println(dept);// Dept(id=null, name=测试部, createTime=null, updateTime=null)
deptService.insertDept(dept);
return Result.success();
}
如果想要获取请求体中的请求参数,可以使用@RequestBody注解
并且可以用实体类对获取的请求参数进行封装,前提是请求参数中的参数名(键名)要和实体类中的属性名匹配
根据ID查询部门
通常来说,对于修改操作,实现的前提是先通过ID获取需要修改的对象(查询回显),然后再进行修改
Mapper
@Select("SELECT * FROM dept WHERE id = #{id}")
Dept findById(Integer id);
Service
@Override
public Dept findById(Integer id) {
return deptMapper.findById(id);
}
Controller
@GetMapping("/depts/{id}")
public Result findById(@PathVariable Integer id) {
Dept dept = deptService.findById(id);
return Result.success(dept);
}
如果参数是以
路径/参数值的形式存放在请求路径中,获取请求参数的方式与路径?参数名=值不同获取**路径参数**的方式是通过@PathVariable注解,并且路径中用
{名称}给参数起名称@PathVariable(“参数名”)表达的意思和@RequestParam是一样的
如果URL中携带了多个路径,比如:depts/1/0,是可以正常接受的,接受方式如下:
@GetMapping("/depts/{id}/{status}")
public Result findById(@PathVariable("id") Integer id, @PathVariable("status") Integer status) {
Dept dept = deptService.findById(id);
return Result.success(dept);
}
修改部门
完成了根据ID查询部门的接口,就可以正式开发修改部门接口了
Mapper
@Update("UPDATE dept SET name = #{name} , update_time = #{updateTime} WHERE id = #{id}")
void updateDept(Dept dept);
Service
@Override
public void updateDept(Dept dept) {
dept.setUpdateTime(LocalDateTime.now());
deptMapper.updateDept(dept);
}
Controller
@PutMapping("/depts")
public Result updateDept(@RequestBody Dept dept) {
System.out.println(dept);
deptService.updateDept(dept);
return Result.success();
}
总结
观察部门接口的Controller层代码:
package com.norlcyan.controller;
import com.norlcyan.pojo.Dept;
import com.norlcyan.pojo.Result;
import com.norlcyan.service.DeptService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
public class DeptController {
private final DeptService deptService;
@Autowired
public DeptController(DeptService deptService) {
this.deptService = deptService;
}
@GetMapping("/depts")
public Result findAll() {
return Result.success(deptService.findAll());
}
@DeleteMapping("/depts")
public Result deleteById(@RequestParam Integer id) {
deptService.deleteById(id);
return Result.success();
}
@PostMapping("/depts")
public Result insertDept(@RequestBody Dept dept) {
deptService.insertDept(dept);
return Result.success();
}
@GetMapping("/depts/{id}")
public Result findById(@PathVariable Integer id) {
Dept dept = deptService.findById(id);
return Result.success(dept);
}
@PutMapping("/depts")
public Result updateDept(@RequestBody Dept dept) {
System.out.println(dept);
deptService.updateDept(dept);
return Result.success();
}
}
由于所有请求路径都有相同的基础路径 /depts,可以在类上使用 @RequestMapping(“/depts”) 统一声明
这样其他方法上的映射注解就可以省略基础路径,使代码更简洁
package com.norlcyan.controller;
import com.norlcyan.pojo.Dept;
import com.norlcyan.pojo.Result;
import com.norlcyan.service.DeptService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RequestMapping("/depts")
@RestController
public class DeptController {
private final DeptService deptService;
@Autowired
public DeptController(DeptService deptService) {
this.deptService = deptService;
}
@GetMapping
public Result findAll() {
return Result.success(deptService.findAll());
}
@DeleteMapping
public Result deleteById(@RequestParam Integer id) {
deptService.deleteById(id);
return Result.success();
}
@PostMapping
public Result insertDept(@RequestBody Dept dept) {
deptService.insertDept(dept);
return Result.success();
}
@GetMapping("/{id}")
public Result findById(@PathVariable Integer id) {
Dept dept = deptService.findById(id);
return Result.success(dept);
}
@PutMapping
public Result updateDept(@RequestBody Dept dept) {
System.out.println(dept);
deptService.updateDept(dept);
return Result.success();
}
}
像@GetMapping、@PostMapping、@DeleteMapping和@PutMapping都是@RequestMapping的衍生注解
所以,也就说明@RequestMapping注解可以添加在:
- 类上(可选)
- 方法上
- 完整的请求路径为 = 类上 + 方法上的
员工管理
员工分页查询
| 名称 | 类型 | 是否必须 | 备注 |
|---|---|---|---|
| code | number | 必须 | 响应码, 1 成功 , 0 失败 |
| msg | string | 非必须 | 提示信息 |
| data | object | 必须 | 返回的数据 |
| - total | number | 必须 | |
| - rows | object [] | 必须 | |
| - id | number | 非必须 | |
| - username | string | 非必须 | |
| - name | string | 非必须 | |
| - password | string | 非必须 | |
| - gender | number | 非必须 | |
| - image | string | 非必须 | |
| - job | number | 非必须 | |
| - salary | number | 非必须 | |
| - entryDate | string | 非必须 | |
| - deptId | number | 非必须 | |
| - deptName | string | 非必须 | |
| - createTime | string | 非必须 | |
| - updateTime | string | 非必须 |
分页查询,后端要返回
总记录数(total)和结果列表(rows)
PageResult实体类如下:
package com.norlcyan.pojo;
import lombok.Data;
import java.util.List;
@Data
public class PageResult<T>{
private Long total;
private List<T> rows;
}
Mapper
PageHelper实现:
@Select("SELECT emp.*, dept.name as dept_name FROM emp LEFT JOIN dept ON dept_id = dept.id ORDER BY update_time desc")
List<Emp> list();
原始实现:
// 获取结果列表
@Select("SELECT emp.*, dept.name as dept_name FROM emp LEFT JOIN dept ON dept_id = dept.id ORDER BY update_time desc LIMIT #{pageNum},#{pageSize}")
List<Emp> findEmps(Integer pageNum,Integer pageSize);
// 获取总记录数
@Select("SELECT COUNT(emp.id) FROM emp LEFT JOIN dept ON dept_id = dept.id")
Long getTotals();
Service
PageHelper实现(**关注它就行**):
@Override
public PageResult<Emp> page(Integer pageNum, Integer pageSize) {
// 1. 设置分页参数
PageHelper.startPage(pageNum, pageSize);
// 2. 执行查询
// 由于设置了PageHelper分页参数,底层会进行分页处理
// PageHelper通过MyBatis拦截器机制,在执行SQL时自动添加LIMIT子句
List<Emp> list = empMapper.list();
// 3. 解析查询结果并封装
// Page底层继承了ArrayList,所以它属于集合
// Page → ArrayList → List
// 该对象还会存储分页相关元数据,包括总记录数
Page<Emp> p = (Page<Emp>) list;
return new PageResult<Emp>(p.getTotal(), p.getResult());
}
原始实现:
@Override
public List<Emp> findEmps(Integer pageNum, Integer pageSize) {
return empMapper.findEmps((pageSize * (pageNum - 1)),pageSize);
}
@Override
public Long getTotals() {
return empMapper.getTotals();
}
Controller
PageHelper实现:
@GetMapping
public Result findEmps(@RequestParam(defaultValue = "1",name = "page") Integer pageNum,@RequestParam(defaultValue = "10") Integer pageSize) {
PageResult<Emp> pageResult = empService.page(pageNum,pageSize);
return Result.success(pageResult);
}
原始实现:
@GetMapping
public Result findEmps(@RequestParam(defaultValue = "1") Integer pageNum,@RequestParam(defaultValue = "10") Integer pageSize) {
PageResult<Emp> pageResult = new PageResult<>();
pageResult.setTotal(empService.getTotals());
pageResult.setRows(empService.findEmps(pageNum,pageSize));
return Result.success(pageResult);
}
@RequestParam(defaultValue = "1")注解的参数表示的是默认值,如果请求中没有相应的请求参数,就会用默认值替代
员工分页条件查询
Mapper
由于SQL语句相对复杂,所以使用XML文件映射(EmpMapper.xml):
<!-- 动态查询员工列表 -->
<select id="list" parameterType="com.norlcyan.pojo.EmpQueryParam" resultType="com.norlcyan.pojo.Emp">
SELECT
e.*, dept.name dept_name
FROM emp e LEFT JOIN dept ON dept_id = dept.id
<where>
<if test="name != null and name != ''">
AND e.name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="gender != null">
AND e.gender = #{gender}
</if>
<if test="begin != null and end != null">
AND e.entry_date BETWEEN #{begin} AND #{end}
</if>
</where>
ORDER BY update_time DESC
</select>
EmpMapper:
List<Emp> list(EmpQueryParam empQueryParam);
Service
EmpService:
@Override
public PageResult<Emp> page(EmpQueryParam empQueryParam) {
// 1. 设置分页参数
PageHelper.startPage(empQueryParam.getPage(), empQueryParam.getPageSize());
// 2. 执行查询
// 由于设置了PageHelper分页参数,底层会进行分页处理
// PageHelper通过MyBatis拦截器机制,在执行SQL时自动添加LIMIT子句
List<Emp> list = empMapper.list(empQueryParam);
// 3. 解析查询结果并封装
// Page底层继承了ArrayList,所以它属于集合
// Page → ArrayList → List
// 该对象还会存储分页相关元数据,包括总记录数
Page<Emp> p = (Page<Emp>) list;
return new PageResult<Emp>(p.getTotal(), p.getResult());
}
Controller
EmpController:
@GetMapping
public Result findEmps(EmpQueryParam empQueryParam) {
PageResult<Emp> pageResult = empService.page(empQueryParam);
return Result.success(pageResult);
}
当请求参数较多时,可以使用实体类封装参数
新增员工
Mapper
EmpMapper:
@Options(useGeneratedKeys = true, keyProperty = "id") // 主键返回,相当于给Emp对象中的id进行赋值
@Insert("INSERT INTO emp(username, name, gender, phone, job, salary, image, entry_date, dept_id, create_time, update_time) " +
"VALUES (#{username},#{name},#{gender},#{phone},#{job},#{salary},#{image},#{entryDate},#{deptId},#{createTime},#{updateTime})")
void saveBasicInfo(Emp emp);
新增员工时,id是通过数据库自增的,如果想要获取到自增后的id,可以使用MyBatis下的@Options注解
@Options(useGeneratedKeys = true, keyProperty = "id"),第一个参数表示是否使用自动生成的主键,第二个参数表示主键值赋给哪个属性
EmpExprMapper:
void saveExprInfo(List<EmpExpr> exprList);
EmpExprMapper.xml:
<!-- 保存员工经历 -->
<insert id="saveExprInfo">
INSERT INTO emp_expr(emp_id,begin,end,company,job) VALUES
<foreach item="item" index="index" collection="exprList" separator=",">
(#{item.empId},#{item.begin},#{item.end},#{item.company},#{item.job})
</foreach>
</insert>
Service
@Override
public void save(Emp emp) {
// 保存员工基本信息
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
empMapper.saveBasicInfo(emp); // 当存入了员工的基本信息,数据库会自动生成ID,通过MyBatis的Options注解可以将生成的ID返回给传入的对象
// 保存员工经历
List<EmpExpr> exprList = emp.getExprList();
if (!CollectionUtils.isEmpty(exprList)) {
// 返回给emp对象后,需要对集合遍历,将emp中的id赋值给集合中的每个EmpExpr对象下的empId
exprList.forEach(item -> item.setEmpId(emp.getId()));
empExprMapper.saveExprInfo(exprList);
}
}
以上代码中一次操作了两张表,所以涉及到事务管理的问题
产生问题的原因:
- 第一张表与第二张表是关联关系
- 当操作第一张表时,没有发生异常情况,数据库正确写入数据
- 但是当操作第二张表时,发生了异常情况,导致数据库没有正确写入数据
- 所以,此时第一张表存在数据,第二张表由于异常没有存入数据,这就导致了数据的**不一致性**
Controller
@PostMapping
public Result saveEmps(@RequestBody Emp emp) {
empService.save(emp);
return Result.success();
}
员工头像图片保存
具体流程:

创建OSS上传工具类:
package com.norlcyan.utils;
import com.aliyun.oss.*;
import com.aliyun.oss.common.auth.CredentialsProviderFactory;
import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider;
import com.aliyun.oss.common.comm.SignVersion;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
@Component
public class AliyunOSSOperator {
@Value("${aliyun.oss.endpoint}")
private String endpoint;
@Value("${aliyun.oss.bucketName}")
private String bucketName = "norlcyan-javaweb-ai";
@Value("${aliyun.oss.region}")
private String region = "cn-shanghai";
public String upload(byte[] content, String originalFilename) throws Exception {
// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
// 填写Object完整路径,例如202406/1.png。Object完整路径中不能包含Bucket名称。
//获取当前系统日期的字符串,格式为 yyyy/MM
String dir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM"));
//生成一个新的不重复的文件名
String newFileName = UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf("."));
String objectName = dir + "/" + newFileName;
// 创建OSSClient实例。
ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);
OSS ossClient = OSSClientBuilder.create()
.endpoint(endpoint)
.credentialsProvider(credentialsProvider)
.clientConfiguration(clientBuilderConfiguration)
.region(region)
.build();
try {
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content));
} finally {
ossClient.shutdown();
}
return endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + objectName;
}
}
代码由官方提供的Demo改造而来,返回值为上传文件的URL
想要使工具类可以正常使用,还需要在本地配置阿里云的Access Key
Controller
UploadController:
@Slf4j
@RestController
@RequestMapping("/upload")
public class UploadController {
private final AliyunOSSOperator aliyunOSSOperator;
public UploadController(AliyunOSSOperator aliyunOSSOperator) {
this.aliyunOSSOperator = aliyunOSSOperator;
}
/* 本地磁盘存储方式
@PostMapping
public Result upload(String name, Integer age, MultipartFile file) throws IOException {
log.info("接收到的参数:{},{},{}",name,age,file);
// 获取原始文件名
String originalFilename = file.getOriginalFilename();
// 转换原始文件名
String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); // 获取后缀
String newFileName = UUID.randomUUID() + extension; // 利用UUID生成不会重复的文件名称
// 保存文件
file.transferTo(new File("E:\\MyFiles\\" + newFileName));
return Result.success();
}
*/
@PostMapping
public Result upload(MultipartFile file) throws Exception {
log.info("文件上传:{}", file.getOriginalFilename());
// 将文件交给OSS存储管理
String url = aliyunOSSOperator.upload(file.getBytes(), file.getOriginalFilename());
log.info("文件上传OSS,该文件的URL为:{}", url);
return Result.success(url);
}
}
删除员工
Mapper
EmpMapper.xml:
<delete id="removeEmps">
DELETE FROM emp WHERE id IN
<foreach collection="ids" index="index" item="item" open="(" separator="," close=")">#{item}</foreach>
</delete>
EmpExprMapper.xml:
<delete id="removeEmpExprs">
DELETE FROM emp_expr WHERE emp_id IN
<foreach collection="ids" index="index" item="item" open="(" close=")" separator=",">#{item}</foreach>
</delete>
Service
@Override
@Transactional(rollbackFor = {Exception.class})
public void removeEmps(Integer[] ids) {
empMapper.removeEmps(ids);
empExprMapper.removeEmpExprs(ids);
}
删除员工操作包括了员工基本信息和员工经历这两张表
Controller
@DeleteMapping
public Result removeEmps(Integer[] ids) {
log.info("ids:{}", Arrays.toString(ids));
empService.removeEmps(ids);
return Result.success();
}
对于DELETE /emps?ids=1,2,3这样的请求参数,可以直接使用数组的方式接收
或者利用@RequestParam注解让List接收:
public Result delete(@RequestParam List<Integer> ids) {...}
根据ID查询
根据ID查询员工信息,有两种方案:
第一种是操作两次表,将结果封装到Emp实体类中
第二种是通过一次SQL查询同时获取员工基本信息和工作经历,然后让MyBatis自动组装成完整的员工对象
Mapper
第一种方式
EmpMapper:
@Select("SELECT id, username, password, name, gender, phone, job, salary, image, entry_date, dept_id, create_time, update_time FROM emp WHERE id = #{id}")
Emp findEmpById(Integer id);
EmpExprMapper:
@Select("SELECT id, emp_id, begin, end, company, job FROM emp_expr WHERE emp_id = #{id}")
List<EmpExpr> findEmpExprById(Integer id);
第二种方式
EmpMapper:
Emp getEmpById(Integer id);
EmpMapper.xml:
<!-- 自定义结果集ResultMap -->
<resultMap id="empResultMap" type="com.norlcyan.pojo.Emp">
<id column="id" property="id" />
<result column="username" property="username" />
<result column="password" property="password" />
<result column="name" property="name" />
<result column="gender" property="gender" />
<result column="phone" property="phone" />
<result column="job" property="job" />
<result column="salary" property="salary" />
<result column="image" property="image" />
<result column="entry_date" property="entryDate" />
<result column="dept_id" property="deptId" />
<result column="create_time" property="createTime" />
<result column="update_time" property="updateTime" />
<!-- 封装exprList -->
<collection property="exprList" ofType="com.norlcyan.pojo.EmpExpr">
<id column="ee_id" property="id" />
<result column="ee_company" property="company" />
<result column="ee_job" property="job" />
<result column="ee_begin" property="begin" />
<result column="ee_end" property="end" />
<result column="ee_empid" property="empId" />
</collection>
</resultMap>
<select id="getEmpById" resultMap="empResultMap">
SELECT emp.*,
ee.id ee_id,
ee.company ee_company,
ee.job ee_job,
ee.emp_id ee_empid,
ee.begin ee_begin,
ee.end ee_end
FROM emp LEFT JOIN emp_expr ee on emp.id = ee.emp_id WHERE emp.id = #{id};
</select>
注意自定义结果集中的column对应的是查询结果的列名,property对应的是实体类中的成员属性
对于自定义结果集,主要针对需要复用自定义结果集、 字段名和属性名不匹配、复杂的关联查询(一对一/一对多)这种情况
Service
@Override
public Emp findEmpById(Integer id) {
// 第一种方式:通过操作两个数据表,得到结果
// Emp e = empMapper.findEmpById(id);
// e.setExprList(empExprMapper.findEmpExprById(id));
// 第二种方式:通过外连接得到结果
Emp e = empMapper.getEmpById(id);
return e;
}
Controller
@GetMapping("/{id}")
public Result findEmpById(@PathVariable Integer id) {
Emp e = empService.findEmpById(id);
return Result.success(e);
}
修改员工
Mapper
EmpMapper:
void modify(Emp emp);
EmpMapper.xml:
<!-- 根据ID更新员工信息 -->
<update id="modify">
UPDATE emp SET
<if test="username != null and username != ''">username = #{username},</if>
<if test="password != null and password != ''">password = #{password},</if>
<if test="name != null and username != ''">name = #{name},</if>
<if test="gender != null">gender = #{gender},</if>
<if test="phone != null and phone != ''">phone = #{phone},</if>
<if test="image != null and username != ''">image = #{image},</if>
<if test="deptId != null">dept_id = #{deptId},</if>
<if test="entryDate != null and username != ''">entry_date = #{entryDate},</if>
<if test="job != null">job = #{job},</if>
<if test="salary != null">salary = #{salary},</if>
<if test="updateTime != null">update_time = #{updateTime}</if>
WHERE id = #{id}
</update>
以上代码可以防止前端未将所有字段值都传入,而导致某些字段被修改为空值
Service
@Override
@Transactional
public void modifyEmp(Emp emp) {
// 1. 先删除员工所有的工作经历,再添加现有的所有工作经历(先删后增)
empExprMapper.removeEmpExprs(new Integer[]{emp.getId()});
List<EmpExpr> exprList = emp.getExprList();
if (!CollectionUtils.isEmpty(exprList)) {
exprList.forEach(item -> item.setEmpId(emp.getId()));
empExprMapper.saveExprInfo(exprList);
}
// 2. 修改员工基本信息
emp.setUpdateTime(LocalDateTime.now());
empMapper.modify(emp);
}
员工经历信息按照先删后增的原则进行修改
员工基本信息直接修改即可
数据统计
员工职位人数统计
通过以下SQL语句,可以对每个职位上的员工数量进行统计:
-- case函数
SELECT
COUNT(e.id) AS num,
(CASE e.job
WHEN 1 THEN '班主任'
WHEN 2 THEN '讲师'
WHEN 3 THEN '学工主任'
WHEN 4 THEN '教研主管'
WHEN 5 THEN '咨询师'
ELSE '其他'
END) AS pos
FROM emp e GROUP BY e.job ORDER BY num desc;
得到的结果:
| num | pos |
|---|---|
| 14 | 讲师 |
| 7 | 班主任 |
| 7 | 咨询师 |
| 1 | 教研主管 |
| 1 | 学工主任 |
| 1 | 其他 |
Mapper
EmpMapper:
List<Map<String, Object>> countJobs();
返回值的类型是List集合,集合的泛型是Map
Map用于保存每一行数据,如:Map[num: 14, pos: 讲师]
List集合就是把每一行集合都保存起来,从结构上就看类似表格
EmpMapper.xml:
<!-- 统计员工人数 -->
<select id="countJobs" resultType="java.util.Map">
SELECT
COUNT(e.id) AS num,
(CASE e.job
WHEN 1 THEN '班主任'
WHEN 2 THEN '讲师'
WHEN 3 THEN '学工主任'
WHEN 4 THEN '教研主管'
WHEN 5 THEN '咨询师'
ELSE '其他'
END) AS pos
FROM emp e GROUP BY e.job ORDER BY num desc
</select>
Service
这一层主要是把之前的List集合封装到JobOption实体类中:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class JobOption {
private List jobList; // 职位列表
private List dataList; // 数据列表
}
ReportServiceImpl:
@Override
public JobOption countJobs() {
List<Map<String, Object>> maps = empMapper.countJobs();
List<Object> pos = maps.stream().map(dataMap -> dataMap.get("pos")).toList();
log.info("pos:{}",pos.toString());
List<Object> num = maps.stream().map(dataMap -> dataMap.get("num")).toList();
log.info("num:{}",num.toString());
return new JobOption(pos,num);
}
Controller
ReportController:
@GetMapping("/empJobData")
public Result countJobs() {
JobOption j = reportService.countJobs();
return Result.success(j);
}
员工性别统计
{"name": "男性员工","value": 5},
{"name": "女性员工","value": 6}
和员工人数统计不同的在于,它需要的返回值是一个对象而非数组,以下是员工人数统计返回的结果,用于进行对比:
"jobList": ["教研主管","学工主管","其他","班主任","咨询师","讲师"],
"dataList": [1,1,2,6,8,13]
Mapper
EmpMapper:
List<GenderOption> countGender();
EmpMapper.xml:
<!-- 统计员工性别数量 -->
<select id="countGender" resultType="com.norlcyan.pojo.GenderOption">
SELECT
COUNT(e.id) as count,
if (gender = 1, '男性员工', '女性员工') as emp_gender
FROM emp e GROUP BY gender
</select>
Service
@Override
public List<GenderOption> countGender() {
List<GenderOption> list = empMapper.countGender();
log.info("list:{}",list.toString());
return list;
}
Controller
@GetMapping("/empGenderData")
public Result countGender() {
List<GenderOption> g = reportService.countGender();
return Result.success(g);
}
登录功能
定义登录成功的实体类:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginInfo {
private Integer id;
private String username;
private String name;
private String token;
}
全局异常处理
创建一个全局异常处理器类:
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler
public Result handleGlobalException(Exception e) {
log.error("程序发生异常:",e);
return Result.error("发生未知异常,请联系网管");
}
@ExceptionHandler
public Result handleDuplicateKeyException(DuplicateKeyException e) {
log.error("程序发生异常:",e);
String message = e.getMessage();
// 解析数据库错误信息
if (message.contains("emp.username")) {
return Result.error("用户名已存在");
} else if (message.contains("emp.phone")) {
return Result.error("手机号已存在");
}
// 如果上面的内容没有匹配到,就会采用下面的默认处理方式
int i = message.indexOf("Duplicate entry");
String errorMessage = message.substring(i);
String[] arr = errorMessage.split(" ");
return Result.error(arr[2] + "已存在");
}
}
异常会根据最具体匹配优先
最具体匹配优先(Most Specific Match First)是指当发生异常时,Spring会优先选择参数类型最精确、最具体的异常处理方法
前端请求设计
请求工具类
在前端项目中,创建一个统一的请求工具类:
import axios from "axios";
const request = axios.create({
baseURL: "http://localhost:8080",// 请求路径的基本地址
timeout: 60000,
});
request.interceptors.response.use(
(response) => {
return response.data;
},
(error) => {
return Promise.reject(error);
}
);
export default request;
为了方便管理,还需要创建一个Api文件夹,在该文件夹下对每个接口进行分类操作
比如调用部门接口就创建dept.js,这个文件内存放所有增删改查的具体调用方式:
import request from "@/utils/request"
// 部门列表查询
export const queryAllDept = () => {
return request.get("/depts")
}
// 查询回显
export const queryDept = (id) => {
return request.get(`/depts/${id}`)
}
需要使用的时候,调用即可:
import {queryAllDept,queryDept} from '@/api/dept'
const list = queryAllDept()
const dept = queryDept(1)
配置文件设置服务器信息
在上一章中,请求工具类中的baseURL是写死的。项目打包后,如果后期更换了服务器地址,无法直接修改请求工具类
所以,可以利用vite.config.js配置请求服务器信息,具体流程如图:

vite.config.js中的proxy配置意思是:当发送请求时的路径为/api时,将其目标基础地址设置为http://localhost:8080,并重写(移除)掉/api
登录
登录成功时,将服务器返回的用户信息(username、name、token)存储到本地中,可以利用localStorage
- localStorage是浏览器提供的本地存储机制(最大5MB)
- 存储形式为key-value形式,键和值都是字符串形式
- API方法:
- localStorage.setItem(key, value)
- localStorage.getItem(key)
- localStorage.removeItem(key)
- localStorage.clear()