实现流程成登录类似,也是MVC的3层实现。
前端部分,不详细说明。根据链接、表单的提交路径,编写对应路径的controller方法接口,model携带数据返回时,key需要对应前端的thymeleaf变量接收的名字
这里把功能全部列出来,当时笔者练习时,跟着oneStar的博客这样做,到后面业务熟悉时,可以一点点地看前端知道需要什么接口,自己写出来。
实际开发肯定是要根据前端写对应的接口,但是我用的是已有的模板。
若想了解关于接口功能的详细分析,推荐看oneStar老哥的博客。
笔者博客写来是做为一个记录,就不搬别人的分析过来了。
分类管理基本的增删改查,并判断分类重复。
1.DAO持久层
在dao
包下创建TypeDao
接口
@Repository
@Mapper
public interface TypeDao {
//新增保存分类
int saveType(Type type);
//根据id查询分类
Type getType(Long id);//编辑时可传入对应分类
//查询所有分类
List<Type> getAllType();
//根据分类名称 查询分类
Type getTypeByName(String name);//阻止 重类
//编辑修改
int updateType(Type type);
//删除分类
void deleteType(Long id);
}
2.分类管理mapper
在mapper
文件夹下创建TypeDao.xml
文件
当Dao接口传递的参数不是基本类型或String时,需要用parameterType标识其类型,又由于在框架搭建时,使用了别名配置。entity包下的实体类都可以直接用类名来填入,否则就要写
包路径+类名
。
<?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.hxj.dao.TypeDao">
<!--新增,保存分类-->
<insert id="saveType" parameterType="Type">
insert into t_type (id, name)
values (#{id},#{name});
</insert>
<!--根据id查询分类-->
<select id="getType" resultType="Type">
select *
from t_type where id=#{id};
</select>
<!--查询所有分类-->
<select id="getAllType" resultType="Type">
select *
from t_type;
</select>
<!--根据分类名 查询分类-->
<select id="getTypeByName" resultType="Type">
select *
from t_type where name = #{name};
</select>
<!--编辑修改 分类-->
<update id="updateType" parameterType="Type">
update t_type
set name = #{name}
where id = #{id};
</update>
<!--删除分类-->
<delete id="deleteType" >
delete
from t_type
where id=#{id};
</delete>
</mapper>
3.分类管理Service业务层
在service
包创建用户业务层接口UserService
。
public interface TypeService {
//新增保存分类
int saveType(Type type);
//根据id查询分类
Type getType(Long id);
//查询所有分类
List<Type> getAllType();
//根据分类名称 查询分类
Type getTypeByName(String name);//阻止 重类
//编辑修改
int updateType(Type type);
//删除分类
void deleteType(Long id);
}
接口实现类impl
在impl
子包,在其中创建用户业务层接口实现类UserServiceImpl
。
原本oneStar老哥在每个方法都使用了
@Transactional
开启事务,但是我发现开和不开对项目来说好像没有什么大影响,用户可以自己判断。(仅供参考)
@Service
public class TypeServiceImpl implements TypeService {
@Autowired
private TypeDao typeDao;
@Transactional
@Override
public int saveType(Type type) {
return typeDao.saveType(type);
}
@Transactional
@Override
public Type getType(Long id) {
return typeDao.getType(id);
}
@Transactional
@Override
public List<Type> getAllType() {
return typeDao.getAllType();
}
@Override
public Type getTypeByName(String name) {
return typeDao.getTypeByName(name);
}
@Transactional
@Override
public int updateType(Type type) {
return typeDao.updateType(type);
}
@Transactional
@Override
public void deleteType(Long id) {
typeDao.deleteType(id);
}
}
4.分类管理Controller控制器
- 使用
PageHepler
进行分页显示。
页显示使用PageHelper插件具体使用可以参考oneStar的:SpringBoot引入Pagehelper分页插件
重点注意:一定要在
pagehelper.startPage()
紧接着写我们的数据库操作(即调用Service),不然数据读取也无法分页。
添加PageHelper分页插件,在pom.xml中添加:
<!--引入分页插件-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.1</version>
</dependency>
然后在application.yml
中添加如下配置。
pagehelper:
helper-dialect: mysql
params: count=countSql
reasonable: true
support-methods-arguments: true
排错:引入后出现循环依赖的bug(用户根据情况自行调整)
我的版本不兼容,提高pagehelper版本 改成
1.4.1
在controller.admin
包下,创建TypeController
,代码如下:
@Controller
@RequestMapping("/admin")
public class TypeController {
@Autowired
private TypeService typeService;
//获得所有分类,查询
@GetMapping("/types")
public String list(Model model, @RequestParam(value ="pageNum",defaultValue = "1") Integer pageNum){
//按照id排序
String orderBy = "id desc";
PageHelper.startPage(pageNum,10,orderBy);//紧跟在startpage后面的sql语句才会被执行
List<Type> allType = typeService.getAllType();
PageInfo<Type> pageInfo = new PageInfo<>(allType); //PageInfo用来封装页面信息,返回给前台界面,一些PageInfo的参数
System.out.println(pageInfo.getList());
model.addAttribute("pageInfo",pageInfo);
return "admin/types";
}
@PostMapping("/refreshType")
public String refreshType(Model model, @RequestParam(value ="pageNum",defaultValue = "1") Integer pageNum){
//按照id排序
String orderBy = "id desc";
PageHelper.startPage(pageNum,10,orderBy);//紧跟在startpage后面的sql语句才会被执行
List<Type> allType = typeService.getAllType();
PageInfo<Type> pageInfo = new PageInfo<>(allType); //PageInfo用来封装页面信息,返回给前台界面,一些PageInfo的参数
System.out.println(pageInfo.getList());
model.addAttribute("pageInfo",pageInfo);
return "admin/types :: types-list";
}
//进入新增界面
@GetMapping("/types/input")
public String input(Model model){
model.addAttribute("type",new Type());
return "admin/types-input";
}
//新增 点击提交 type
@PostMapping("/types")
public String post(@Validated Type type,BindingResult result ,RedirectAttributes attributes){
Type type1 = typeService.getTypeByName(type.getName());//是否重名
if(type1 != null){
attributes.addFlashAttribute("message","不能添加重复的分类");
return "redirect:/admin/types/input";
}
if(result.hasErrors()){
//校验出来,发现问题,把message传前端
return "admin/types-input";
}
int t = typeService.saveType(type);//保存(到数据库)
if(t==0){
attributes.addFlashAttribute("message","新增失败");
}else {
attributes.addFlashAttribute("message","新增成功");
}
return "redirect:/admin/types"; //回到列表展示
}
//跳转到"修改"分类页面
@GetMapping("/types/{id}/input")
public String editInput(@PathVariable Long id,Model model){
model.addAttribute("type",typeService.getType(id)); //把要修改的 分类传
//此时id不为空, 提交会走 不同 post请求 /admin/types/{id}(id=*{id})
return "admin/types-input";
}
//编辑分类
@PostMapping("/types/{id}")
public String editPost(@Validated Type type,BindingResult result, RedirectAttributes attributes){
Type type1 = typeService.getTypeByName(type.getName());
if(type1!=null){
attributes.addFlashAttribute("message","不能添加重复的分类");
return "redirect:/admin/types/{id}/input"; //回到新增(有id还是进 编辑)页面
}
if(result.hasErrors()){
//校验出来,发现问题,把message传前端
return "admin/types-input";
}
int t = typeService.updateType(type);
if(t==0){
attributes.addFlashAttribute("message","编辑失败");
}else {
attributes.addFlashAttribute("message","编辑成功");
}
return "redirect:/admin/types"; //回到列表展示
}
@GetMapping("/types/{id}/delete")
public String delete(@PathVariable Long id,RedirectAttributes attributes){
typeService.deleteType(id);
attributes.addFlashAttribute("message","删除成功");
return "redirect:/admin/types";
}
}
5.前后端交互
这里引用oneStar的内容
1.新增:
<a href="#" th:href="@{/admin/types/input}">
<button type="button" class="ui teal button m-mobile-wide m-margin-top"><i class="pencil icon"></i>新增</button>
</a>
2.编辑删除:
<a href="#" th:href="@{/admin/types/{id}/input(id=${type.id})}" class="ui mini teal basic button">编辑</a>
<a href="#" th:href="@{/admin/types/{id}/delete(id=${type.id})}" onclick="return confirm('确定要删除该分类吗?三思啊! 删了可就没了!')" class="ui mini red basic button">删除</a>
3.查询分类列表:
<a href="#" th:href="@{/admin/types}" class="teal active item">列表</a>
分页是实现,我自己进行了改动,可以看下面的内容部分。(看不懂也可以先跟着oneStar老哥来实现,当做到后面就会懂得了这些要怎么改了。)
实现分页的局部刷新
这里是笔者自己改动的功能,因为原本oneStar老哥的项目中使用到分页并不是局部刷新,在全查询的时候选择的是只传递pageNum来达到跳页的操作(体验效果一般)。
但是在笔者自己后面的一些功能功能需要配合一些查询条件时,这个时候单单传递pageNum可不能实现,于是找了一些其他人的项目,发现他们实现了局部刷新的功能,于是就把全部分页都尽量使用了局部刷新。
(自己的水平有限,实现的时候用了比较笨的方法,代码并没有复用,不过还是实现了局部刷新分页。)
当时参考的是这篇博客,如果是看oneStar的已经做了它的不是局部刷新分类管理。到后面的博客管理的分页,也可以再回来看,如果学会了,用户也可以自行改动了(实现起来并不算难)。
https://blog.csdn.net/qq369392973/article/details/108257393
这篇简单明了:
https://blog.csdn.net/aawmx123/article/details/87862304
这两篇先看
(虽然这是对后面博客管理的分页,不过如果看懂了,那么后续其余的页面的同类改动也不是问题了)
1.改前端
- 先使用theamleaf将需要局部动态更新的区域定义成一个
fragment:
注意: 一定要定义一个 隐藏域,用来存pageNum
- 分页部分的改动
虽然看得懂,自己写不出来,但是会看着别人的改就行。
这里的改动方法,可以沿用到后续一切要局部分页刷新的操作。(有其他条件则可以多写data-xx参数)
<div class="two wide column" align="center">
<a class="item" style="cursor: pointer" onclick="page(this)" th:attr="data-page=1" th:unless="${pageInfo.isFirstPage}">首页</a>
</div>
<div class="two wide column" align="center">
<a class="item" style="cursor: pointer" onclick="page(this)" th:attr="data-page=${pageInfo.hasPreviousPage}?${pageInfo.prePage}:1" th:unless="${pageInfo.isFirstPage}">上一页</a>
</div>
<div class="eight wide column" align="center">
<p>第 <span th:text="${pageInfo.pageNum}"></span> 页,共 <span th:text="${pageInfo.pages}"></span> 页,有 <span th:text="${pageInfo.total}"></span> 篇文章</p>
</div>
<div class="two wide column " align="center">
<a class="item" style="cursor: pointer" onclick="page(this)" th:attr="data-page=${pageInfo.hasNextPage}?${pageInfo.nextPage}:${pageInfo.pages}" th:unless="${pageInfo.isLastPage}">下一页</a>
</div>
<div class="two wide column " align="center">
<a class="item" style="cursor: pointer" onclick="page(this)" th:attr="data-page=${pageInfo.pages}" th:unless="${pageInfo.isLastPage}">尾页</a>
</div>
- 前端定义ajax提交的方法
这里目前不需要传别的条件内容,所以传一个PageNum即可。
function page(obj) {
$("[name='page']").val($(obj).data("page"));
//把a标签中的data-page赋值给隐藏域
loaddata();
}
function loaddata() {
$("#types-container").load(/*[[@{/admin/refreshType}]]*/
"/admin/refreshType",{
pageNum : $("[name='page']").val()
//拿刚刚存到隐藏域的page值做为pageNum传过去
});
}
ajax提交是post方式提交
2.后端接口
(保留了/types
,它不再负责跳转页了,只能作为第一次进入页面的有用)
代码没有复用,但是功能实现了,最大的差别是这里:return "admin/blogs :: types-list";
。
点击跳转页的时候,就会实现ajax的局部动态刷新页面,将搜索条件提交到后台,后台查到对应的Page对象传回前台,在对应部位局部刷新数据。
关于后端校验
视频中虽然说做后台校验是为了防止用户绕过前端js校验向后端发数据,才做这个后端校验。但是oneStar的博客中,没有把这个写清楚(实际它那样写是没有任何作用的样子)
当时看了别人的 才知道原来应该是这样写的
https://blog.csdn.net/weixin_44341916/article/details/124307452
这里就简单说一下后端校验的步骤
- 改实体类
使用相应注解,上面的博客链接中有相关使用说明。
- 改controller控制器
在controller的接口上加上@Validated或@valid注解。
@Validated Type type,BindingResult result
,想把错误信息传递到前端显示需要BindingResult result
,接收校验的结果。(切记,@validated的参数一定要和BindingResult 紧跟着写,否者会出现错误,实际测试过)
在type-input页面中写这一段,就可以把后端校验信息返回到前端了。
(这是视频中老师找的方法,这里直接粘过来用,变量名到时候改改就可以在其他地方用了)
就是当后端传递了某个属性的错误信息,检测到时就会直接显示出来。(想测试时,可以先注掉前端js 的校验才行)
实现效果 : 没有输入直接提交时(前端js不生效的情况)