准备工作 接口文档:接口文档 - 飞书云文档
开发模式:前后端分离开发(前端项目、后端项目 开发和部署都是分开的)
开发流程:需求分析 ➡ 接口设计 ➡ 前后端并行开发 ➡ 测试 ➡ 测试 ➡ 联调
接口测试工具: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 1 2 @Select("SELECT * FROM dept ORDER BY update_time desc") List<Dept> findAll () ;
如果列名和实体类的属性名不匹配,比如create_time为列名,createTime为属性名,那就无法将获取的数据封装到实体类中
解决方式有三种:
手动结果映射:
1 2 3 4 5 6 @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 () ;
为列名起别名:
1 2 @Select("SELECT id, name, create_time creatTime, update_time updateTime FROM dept ORDER BY update_time desc") List<Dept> findAll () ;
如果是create_time这种蛇形命名法 ,可以通过修改配置自动转换为驼峰形式:
1 2 3 mybatis: configuration: map-underscore-to-camel-case: true
Service 1 2 3 4 @Override public List<Dept> findAll () { return deptMapper.findAll(); }
Controller 1 2 3 4 @GetMapping("/depts") public Result findAll () { return Result.success(deptService.findAll()); }
删除部门 Mapper 1 2 @Delete("DELETE FROM dept WHERE id = #{id}") void deleteById (Integer id) ;
Service 1 2 3 4 @Override public void deleteById (Integer id) { deptMapper.deleteById(id); }
Controller 1 2 3 4 5 @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 1 2 @Insert("INSERT INTO dept VALUES (#{id},#{name},#{createTime},#{updateTime})") void insertDept (Dept dept) ;
Service 1 2 3 4 5 6 7 8 @Override public void insertDept (Dept dept) { dept.setCreateTime(LocalDateTime.now()); dept.setUpdateTime(LocalDateTime.now()); deptMapper.insertDept(dept); }
Controller 1 2 3 4 5 6 @PostMapping("/depts") public Result insertDept (@RequestBody Dept dept) { System.out.println(dept); deptService.insertDept(dept); return Result.success(); }
如果想要获取请求体中的请求参数,可以使用@RequestBody注解
并且可以用实体类对获取的请求参数进行封装,前提是请求参数中的参数名 (键名)要和实体类中的属性名匹配
根据ID查询部门 通常来说,对于修改操作,实现的前提是先通过ID获取需要修改的对象(查询回显),然后再进行修改
Mapper 1 2 @Select("SELECT * FROM dept WHERE id = #{id}") Dept findById (Integer id) ;
Service 1 2 3 4 @Override public Dept findById (Integer id) { return deptMapper.findById(id); }
Controller 1 2 3 4 5 @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,是可以正常接受的,接受方式如下:
1 2 3 4 5 @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 1 2 @Update("UPDATE dept SET name = #{name} , update_time = #{updateTime} WHERE id = #{id}") void updateDept (Dept dept) ;
Service 1 2 3 4 5 @Override public void updateDept (Dept dept) { dept.setUpdateTime(LocalDateTime.now()); deptMapper.updateDept(dept); }
Controller 1 2 3 4 5 6 @PutMapping("/depts") public Result updateDept (@RequestBody Dept dept) { System.out.println(dept); deptService.updateDept(dept); return Result.success(); }
总结 观察部门接口的Controller层代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 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”) 统一声明
这样其他方法上的映射注解就可以省略基础路径,使代码更简洁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 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
非必须
id
|- username
string
非必须
用户名
|- name
string
非必须
姓名
|- password
string
非必须
密码
|- gender
number
非必须
性别 , 1 男 ; 2 女
|- image
string
非必须
图像
|- job
number
非必须
职位, 说明: 1 班主任,2 讲师, 3 学工主管, 4 教研主管, 5 咨询师
|- salary
number
非必须
薪资
|- entryDate
string
非必须
入职日期
|- deptId
number
非必须
部门id
|- deptName
string
非必须
部门名称
|- createTime
string
非必须
创建时间
|- updateTime
string
非必须
更新时间
分页查询,后端要返回总记录数(total)和结果列表(rows)
PageResult实体类如下:
1 2 3 4 5 6 7 8 9 10 11 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实现:
1 2 @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 () ;
原始实现:
1 2 3 4 5 6 7 @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实现(**关注它就行 **):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Override public PageResult<Emp> page (Integer pageNum, Integer pageSize) { PageHelper.startPage(pageNum, pageSize); List<Emp> list = empMapper.list(); Page<Emp> p = (Page<Emp>) list; return new PageResult <Emp>(p.getTotal(), p.getResult()); }
原始实现:
1 2 3 4 5 6 7 8 9 @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实现:
1 2 3 4 5 @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); }
原始实现:
1 2 3 4 5 6 7 @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):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <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:
1 List<Emp> list (EmpQueryParam empQueryParam) ;
Service EmpService:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override public PageResult<Emp> page (EmpQueryParam empQueryParam) { PageHelper.startPage(empQueryParam.getPage(), empQueryParam.getPageSize()); List<Emp> list = empMapper.list(empQueryParam); Page<Emp> p = (Page<Emp>) list; return new PageResult <Emp>(p.getTotal(), p.getResult()); }
Controller EmpController:
1 2 3 4 5 @GetMapping public Result findEmps (EmpQueryParam empQueryParam) { PageResult<Emp> pageResult = empService.page(empQueryParam); return Result.success(pageResult); }
当请求参数较多时,可以使用实体类封装参数
新增员工 Mapper EmpMapper:
1 2 3 4 @Options(useGeneratedKeys = true, keyProperty = "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:
1 void saveExprInfo (List<EmpExpr> exprList) ;
EmpExprMapper.xml:
1 2 3 4 5 6 7 <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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Override public void save (Emp emp) { emp.setCreateTime(LocalDateTime.now()); emp.setUpdateTime(LocalDateTime.now()); empMapper.saveBasicInfo(emp); List<EmpExpr> exprList = emp.getExprList(); if (!CollectionUtils.isEmpty(exprList)) { exprList.forEach(item -> item.setEmpId(emp.getId())); empExprMapper.saveExprInfo(exprList); } }
以上代码中一次操作了两张表,所以涉及到事务管理的问题
产生问题的原因:
第一张表与第二张表是关联关系
当操作第一张表时,没有发生异常情况,数据库正确写入数据
但是当操作第二张表时,发生了异常情况,导致数据库没有正确写入数据
所以,此时第一张表存在数据,第二张表由于异常没有存入数据,这就导致了数据的**不一致性 **
Controller 1 2 3 4 5 @PostMapping public Result saveEmps (@RequestBody Emp emp) { empService.save(emp); return Result.success(); }
员工头像图片保存 具体流程:
创建OSS上传工具类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 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 { EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider(); String dir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM" )); String newFileName = UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf("." )); String objectName = dir + "/" + newFileName; 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 @Slf4j @RestController @RequestMapping("/upload") public class UploadController { private final AliyunOSSOperator aliyunOSSOperator; public UploadController (AliyunOSSOperator aliyunOSSOperator) { this .aliyunOSSOperator = aliyunOSSOperator; } @PostMapping public Result upload (MultipartFile file) throws Exception { log.info("文件上传:{}" , file.getOriginalFilename()); String url = aliyunOSSOperator.upload(file.getBytes(), file.getOriginalFilename()); log.info("文件上传OSS,该文件的URL为:{}" , url); return Result.success(url); } }
删除员工 Mapper EmpMapper.xml:
1 2 3 4 <delete id ="removeEmps" > DELETE FROM emp WHERE id IN <foreach collection ="ids" index ="index" item ="item" open ="(" separator ="," close =")" > #{item}</foreach > </delete >
EmpExprMapper.xml:
1 2 3 4 <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 1 2 3 4 5 6 @Override @Transactional(rollbackFor = {Exception.class}) public void removeEmps (Integer[] ids) { empMapper.removeEmps(ids); empExprMapper.removeEmpExprs(ids); }
删除员工操作包括了员工基本信息和员工经历这两张表
Controller 1 2 3 4 5 6 @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:
1 2 @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:
1 2 @Select("SELECT id, emp_id, begin, end, company, job FROM emp_expr WHERE emp_id = #{id}") List<EmpExpr> findEmpExprById (Integer id) ;
第二种方式 EmpMapper:
1 Emp getEmpById (Integer id) ;
EmpMapper.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <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" /> <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 1 2 3 4 5 6 7 8 9 10 @Override public Emp findEmpById (Integer id) { Emp e = empMapper.getEmpById(id); return e; }
Controller 1 2 3 4 5 @GetMapping("/{id}") public Result findEmpById (@PathVariable Integer id) { Emp e = empService.findEmpById(id); return Result.success(e); }
修改员工 Mapper EmpMapper:
EmpMapper.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <!-- 根据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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override @Transactional public void modifyEmp (Emp emp) { 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); } emp.setUpdateTime(LocalDateTime.now()); empMapper.modify(emp); }
员工经历信息按照先删后增的原则进行修改
员工基本信息直接修改即可
数据统计 员工职位人数统计 通过以下SQL语句,可以对每个职位上的员工数量进行统计:
1 2 3 4 5 6 7 8 9 10 11 12 -- 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:
1 List<Map<String, Object>> countJobs () ;
返回值的类型是List集合,集合的泛型是Map
Map用于保存每一行数据,如:Map[num: 14, pos: 讲师]
List集合就是把每一行集合都保存起来,从结构上就看类似表格
EmpMapper.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <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实体类中:
1 2 3 4 5 6 7 @Data @NoArgsConstructor @AllArgsConstructor public class JobOption { private List jobList; private List dataList; }
ReportServiceImpl:
1 2 3 4 5 6 7 8 9 @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:
1 2 3 4 5 @GetMapping("/empJobData") public Result countJobs () { JobOption j = reportService.countJobs(); return Result.success(j); }
员工性别统计 1 2 { "name" : "男性员工" , "value" : 5 } , { "name" : "女性员工" , "value" : 6 }
和员工人数统计不同的在于,它需要的返回值是一个对象而非数组,以下是员工人数统计返回的结果,用于进行对比:
1 2 "jobList" : ["教研主管" ,"学工主管" ,"其他" ,"班主任" ,"咨询师" ,"讲师" ],"dataList" : [1 ,1 ,2 ,6 ,8 ,13 ]
Mapper EmpMapper:
1 List<GenderOption> countGender () ;
EmpMapper.xml:
1 2 3 4 5 6 7 <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 1 2 3 4 5 6 @Override public List<GenderOption> countGender () { List<GenderOption> list = empMapper.countGender(); log.info("list:{}" ,list.toString()); return list; }
Controller 1 2 3 4 5 @GetMapping("/empGenderData") public Result countGender () { List<GenderOption> g = reportService.countGender(); return Result.success(g); }
登录功能 定义登录成功的实体类:
1 2 3 4 5 6 7 8 9 @Data @NoArgsConstructor @AllArgsConstructor public class LoginInfo { private Integer id; private String username; private String name; private String token; }
全局异常处理 创建一个全局异常处理器 类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @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会优先选择参数类型最精确、最具体的异常处理方法
前端请求设计 请求工具类 在前端项目中,创建一个统一的请求工具类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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,这个文件内存放所有增删改查的具体调用方式:
1 2 3 4 5 6 7 8 9 10 11 import request from "@/utils/request" export const queryAllDept = () => { return request.get("/depts" ) } export const queryDept = (id) => { return request.get(`/depts/${id}`) }
需要使用的时候,调用即可:
1 2 3 4 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()