前言:这是楼主做的第一个项目,有很多不足,权当总结,方便以后回顾。如有不足欢迎指出!
项目背景
tlias智能学习辅助系统是面向管理员操作的一个系统,在这个系统中管理员可以方便的查询学生、学生的班级、班级的班主任、教职工、教职工所在的部门、以及教职工的薪水等…
管理员可以方便快捷的对各种信息进行增删改查操作,每一步操作都会被自动记录在日志内。
技术栈
SpringBoot、Maven、Mybatis、Javaweb
项目结构
项目基于SpringBoot主要分为了controller、service、mapper三层架构,分别负责接受前端请求、逻辑分析、调用数据库。
还有Filter、Aop、Exception等中间层负责token的解析、日志记录、全局异常的监控等
三层架构的具体分析
对于每一个Controller层,都需要定义其负责的请求路径,并根据接口文档规定的请求参数格式,对请求参数进行接受,调用Service层进行逻辑处理
对于每一个Service层,都会有一个Service实现类(Impl),Service接口只负责创建方法,实现方法交给ServiceImpl来处理。在ServiceImpl中根据具体的业务需要,调用不同的Mapper接口,并定义好返回参数,在Mapper接口拿到数据后返回给controller层
对于每一个Mapper层,都有很多方法,这些方法不会实现任何方法,只会根据注解来实现Mybatis封装好的链接数据库的操作,与手动建立联系比起来方便快捷。如果sql语句很复杂,可以用基于反射实现的XML映射器。
各个功能的具体实现流程
页查询
以员工分页查询为例,在分页查询时,前段会传一个{queryString}格式的数据,此时可以定义一个EmpQueryParam来接收数据。数据中包括了此时的页码、每页展示的个数、姓名性别、年月日等筛选信息,除了前面两个是必须携带,后面的筛选条件可以有也可以没有。如果有则按条件查询,如果无,则正常查询。
@Slf4j//日志注解
@RequestMapping("/emps")//定义整个类的请求路径
@RestController//controller层注解
public class EmpController {
....
....
....
@GetMapping
public Result page(EmpQueryParam empQueryParam){//接收信息
log.info("分页查询:{}",empQueryParam);//控制台输出日志
PageResult<Emp> pageResult = empService.page(empQueryParam);//创建一个返回格式
return Result.success(pageResult);//为前端返回数据
}
}
由于返回格式的限制,需要整合一个PageResult来存储其类(类统一定义在pojo层中)定义如下
@Data//数据层注解
@AllArgsConstructor//有参构造
@NoArgsConstructor//无参构造
public class PageResult<T> {
private Long total;//总行数
private List<T> rows;//每一行的数据
}
将数据传入service层后,service调用实现类来实现方法
public PageResult<Emp> page(EmpQueryParam empQueryParam) {
//PageHelper是一个插件,方便快捷的来抓取想要的区间
//设置数据起始位置
PageHelper.startPage(empQueryParam.getPage(),empQueryParam.getPageSize());
//调用mapper接口筛选数据
List<Emp> empList = empMapper.list(empQueryParam);
//将list强转为Page格式(PageHelper提供的),方便封装
Page<Emp> p = (Page<Emp>) empList;
//返回最终值(rows和对应的数据)
//这里返回去的时候由于定义过起始位置和页面大小,PageHelper会自动封装好
return new PageResult<Emp>(p.getTotal(),p.getResult());
}
至此数据进入mapper层
//接口方法
public List<Emp> list(EmpQueryParam empQueryParam);
//以下是定义在映射文件中的代码:
<select id="list" resultType="com.itheima.pojo.Emp">
select e.*,d.name deptName from emp e left join dept d on e.dept_id = d.id
<where>
<if test="name != null and name != ''">//如果不为空则按条件筛选
e.name like concat('%',#{name},'%')//concat是用于拼接字符
</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 e.update_time desc
</select>
最终返回给前端分页查询的结果
删除
删除是比较简单的操作,只需要根据前端传来的id对数据库进行删除就行
@DeleteMapping
public Result delete(@RequestParam List<Integer> ids){//要加@RequestParam将 HTTP 请求参数绑定到控制器方法的参数上
log.info("删除员工:{}", ids);
empService.delete(ids);
return Result.success();
}
在Service层操作如下,由于员工的基本信息和工作经历不在一个表需要额外删除员工的工作经历
@Override
public void delete(List<Integer> ids) {
//1.批量删除员工基本信息
empMapper.deleteByIds(ids);
//2.删除经历信息
empExprMapper.deleteByEmpIds(ids);
}
Mapper层操作如下
<delete id="deleteByIds">
delete from emp where id in
<foreach collection="ids" item = "id" separator="," open="(" close=")">//遍历列表,分割符为,开头自带(结尾带)实现字符串拼接
#{id}
</foreach>
</delete>
查找
查找和前面的逻辑有耦合的地方,简短展示
@GetMapping("/{id}")//接受地址中的数据为id
public Result getInfo(@PathVariable Integer id){//由于参数在地址中,加入@PathVariable
log.info("根据id查询信息:",id);
Emp emp = empService.getInfo(id);
return Result.success(emp);
}
<select id="getById" resultMap="empResultMap">//结果映射,不需要service层额外操作,直接返回就行了
select
e.* ,
ee.id ee_id,
ee.emp_id ee_empid ,
ee.begin ee_begin ,
ee.end ee_end,
ee.company ee_company,
ee.job ee_job
from emp e left join emp_expr ee on e.id = ee.emp_id
where e.id = #{id};
</select>
修改
修改的逻辑略有不同,修改基本信息就是update语句,但是修改经历则是先删后加,根据id将原先的经历删除后,再加入新的经历
@PutMapping
public Result update(@RequestBody Emp emp){
log.info("修改员工:{}",emp);
empService.update(emp);
return Result.success();
}
先删后加的具体实现
public void update(Emp emp) {
//1根据id修改信息
emp.setUpdateTime(LocalDateTime.now());//设置更新时间
empMapper.updateById(emp);//直接更新员工基本信息
//2根据id修改工作经历(先删后加
empExprMapper.deleteByEmpIds(Arrays.asList(emp.getId()));
List<EmpExpr> exprList = emp.getExprList();
if(!CollectionUtils.isEmpty(exprList)){//如果新增的经历不为空就加入到经历表中
exprList.forEach(empExpr -> empExpr.setEmpId(emp.getId()));//为每一条经历赋ID值
empExprMapper.insertBatch(exprList);
}
}
mapper层实现
//1.
<update id="updateById">
UPDATE emp
#会自动生成set关键字,并自动删除掉更新字段后多余的,
//如果不为空就更新
<set>
<if test="username != null and username != '' ">username = #{username},</if>
<if test="password != null and password != ''">password = #{password},</if>
<if test="name != null and name != ''">name = #{name},</if>
<if test="gender != null">gender = #{gender},</if>
<if test="phone != null and phone != ''">phone = #{phone},</if>
<if test="job != null">job = #{job},</if>
<if test="salary != null">salary = #{salary},</if>
<if test="image != null and image != ''">image = #{image},</if>
<if test="entryDate != null">entry_date = #{entryDate},</if>
<if test="deptId != null">dept_id = #{deptId},</if>
<if test="updateTime != null">update_time = #{updateTime}</if>
</set>
WHERE id = #{id}
</update>
//2.
<!-- 批量保存员工工作经历-->
<insert id="insertBatch">
insert into emp_expr(emp_id,begin,end,company,job) values
<foreach collection="exprList" item = "expr" separator=",">
(#{expr.empId},#{expr.begin},#{expr.end},#{expr.company},#{expr.job})
</foreach>
</insert>
<!-- 根据员工ID批量删除员工经历-->
<delete id="deleteByEmpIds">
delete from emp_expr where emp_id in
<foreach collection="empIds" item="empId" separator="," open="(" close=")">
#{empId}
</foreach>
</delete>
增加
增加和更新有耦合的地方,只展示不同的地方
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
empMapper.insert(emp);
//保存员工工作经历信息
List<EmpExpr> exprList = emp.getExprList();
if(!CollectionUtils.isEmpty(exprList)){
//遍历集合
exprList.forEach(empExpr -> {
empExpr.setEmpId(emp.getId());
});
empExprMapper.insertBatch(exprList);
}
在mapper层更新后,抓取自动生成的主键id返回给service
@Options(useGeneratedKeys = true,keyProperty = "id") //获取生成的主键,返回给service层
@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 insert(Emp emp);
统计
通过map来存每一个数据对应的人数,直接在mapper定义resultType为Map即可
<select id="countEmpJobData" resultType="java.util.Map">//用map来存对应的职位对应的人数
select
//当...则...
(case job when 1 then '班主任'
when 2 then '讲师'
when 3 then '学工主管'
when 4 then '教研主管'
when 5 then '咨询师'
else '其他' end) pos,
count(*) num
from emp group by job order by num
</select>
<select id="countEmpGenderData" resultType="java.util.Map">//同理
select if(gender = 1,'男性员工' , '女性员工') name ,
count(*) value
from emp group by gender
</select>
日志记录
日志处理时,要定义切入点,切入方式
@Slf4j
@Aspect//切入注解
@Component//spring在启动时自动扫描该类作为一个bean
public class OperateLogAspect {
@Autowired
private OperateLogMapper operateLogMapper;
// 定义切入点,拦截 com.itheima.controller 包下的所有方法
@Pointcut("execution(* com.itheima.controller.*.*(..))")
public void pointcut() {}
// 环绕通知,记录操作日志
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
// 获取操作人 ID,这里假设从上下文中获取,实际需要根据业务逻辑修改
Integer operateEmpId = getOperateEmpId();
// 操作时间
LocalDateTime operateTime = LocalDateTime.now();
// 目标类的全类名
String className = joinPoint.getTarget().getClass().getName();
// 目标方法的方法名
String methodName = joinPoint.getSignature().getName();
// 方法运行时参数
String methodParams = Arrays.toString(joinPoint.getArgs());
// 返回值
String returnValue = result != null ? result.toString() : "null";
// 方法执行时长
Long costTime = endTime - startTime;
// 创建操作日志对象
OperateLog operateLog = new OperateLog();
operateLog.setOperateEmpId(operateEmpId);
operateLog.setOperateTime(operateTime);
operateLog.setClassName(className);
operateLog.setMethodName(methodName);
operateLog.setMethodParams(methodParams);
operateLog.setReturnValue(returnValue);
operateLog.setCostTime(costTime);
log.info("记录操作日志:{}",operateLog);
// 保存操作日志
operateLogMapper.insert(operateLog);
return result;
}
// 获取操作人 ID 的方法,需要根据实际业务逻辑实现
private Integer getOperateEmpId() {
// 这里简单返回 1,实际需要从上下文中获取当前操作人的 ID
return CurrentHolder.getCurrentId();
}
}
全局异常监控
@Slf4j
@RestControllerAdvice//处理全局异常和统一响应结果。下面从注解的构成、作用、使用场景和示例代码等方面详细介绍。
public class GlobalExceptionHandler {
@ExceptionHandler//定义异常处理机制
//如果抛出的异常和该异常相同则在控制台输出设置好的内容
public Result handleException(Exception e){
log.error("程序出错啦~",e);
return Result.error("出错啦,请联系管理员");
}
@ExceptionHandler
//例如这里,如果说是重复错误,则输出xxx已存在
public Result handleDuplicateKeyException(DuplicateKeyException e){
log.error("出错啦",e);
String message = e.getMessage();
int i = message.indexOf("Duplicate entry");
String errorMessage = message.substring(i);
String[] arr = errorMessage.split(" ");
return Result.error(arr[2] + "已存在");
}
@ExceptionHandler(StudentExistsException.class)
public Result handleStudentExistsException(StudentExistsException e) {
log.error("出错啦", e);
return Result.error(e.getMessage());
}
}
登录+token验证
登录实际上就是查找,看所给的用户名与密码是否存在or匹配,逻辑挺简单,但是如果每次进入网站都需要登录或者这个网站可以直接通过地址绕过登录,那么这个网站就是一个失败的网站。
这里可以设计一个中间层,在用户登陆成功后获得一个token(储存在浏览器中)。对于直接访问网址的请求行为在请求处理前,先验证token是否合法,如果合法,则执行请求,如果非法则自动跳转到登录界面。
@Slf4j
@WebFilter(urlPatterns = "/*")
public class TokenFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//1.获取请求路径
String requestURI = request.getRequestURI();
//2.是否是登录请求,如果路径中包含login,则放行
if(requestURI.contains("login")){
log.info("登录请求,放行");
filterChain.doFilter(request,response);
return;
}
//3.获取请求头token
String token = request.getHeader("token");
//4.判断token是否为空,如果为空,则提示用户登录(401),如果不为空,则放行
if (token == null || token.isEmpty()){
log.info("令牌为空");
response.setStatus(401);
return;
}
//5.如果token不为空,则解析token,如果解析失败,则提示用户登录(401)
try{
Claims claims = JwtUtils.parseJWT(token);
Integer empId = Integer.valueOf(claims.get("id").toString());
CurrentHolder.setCurrentId(empId);
log.info("当前登录员工ID:{},将其存入ThreadLocal",empId);
}catch (Exception e){
log.info("令牌为空");
response.setStatus(401);
return;
}
//6.如果校验通过。放行
log.info("令牌合法,放行");
filterChain.doFilter(request,response);
CurrentHolder.remove();
}
}