1.项目架构
2. 项目搭建
2.1 搭建父工程
删除父工程中的src目录,在pom中导入maven依赖
2.2 搭建common父工程
2.3 搭建core工程
2.4 搭建interface工程
2.5 搭建admin后台管理工程
2.6 ssm整合
2.7 admin测试
2.8 maven插件
admin中的pom.xml配置jetty
<build>
<!--jetty插件-->
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>9.4.34.v20201102</version>
<configuration>
<httpConnector>
<port>8081</port>
</httpConnector>
<!--配置项目的访问根路径-->
<webApp>
<contextPath>/admin</contextPath>
</webApp>
</configuration>
</plugin>
<build>
2.9 搭建前端工程
nginx作为静态资源服务器
3. 科室管理
3.1 需求和表设计
3.2 lombok
maven导入依赖jar包,idea下载Lombok插件即可使用
使用举例:
@Data //自动生成get,set,tostring方法
@Builder //建造者模式
@AllArgsConstructor//可以使用有参构造器
@NoArgsConstructor//可以使用无参构造器
public class Department implements Serializable {
Long id;
String name;
Integer type; //1门诊,0非门诊
String description;
Integer recommended; //1首页,0非首页
Date createTime;
Date updateTime;
}
使用建造者注解后测试:
Department department = new Department();
Department build = Department.builder().name("kunkun").build();
System.out.println(build);
3.3 mybatisplus整合
applicationContext-dao.xml
<!--根据工厂类SqlSessionFactoryBean对SqlSessionFactory进行ioc和di-->
<bean class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
<!--注入连接池对象-->
<property name="dataSource" ref="dataSource"/>
<!--注入pojo实体类存放的位置-->
<property name="typeAliasesPackage" value="com.furong.pojo.entity"/>
</bean>
3.3.1 mybatisplus中常用的注解
@TableName("tableName")//用在与数据库单表对应的bean实体上面,tableName为与实体类对应的单表表名
@TableId(type = IdType.AUTO) //用在实体类中与数据库表主键对应的属性上,告诉MybatisPlus主键对应的属性,且属性已在数据库中设置自动增长
@TableField(value="field_name",exist=true)//用在与数据库字段名不一致的属性上,value为属性在数据库表中对应的字段名,exist指定当前属性是否是数据库表中字段,默认为true,若有属性不是数据库字段时,这里要配置false
@TableLogic//用在实体类中的逻辑删除标记上面,只要实体类中配置了这个注解,mybatisplus在实现删除的时候都是假删,不配置就是真删
使用举例:
@TableName("fr_department") //指定该entity对应的表名
public class Department implements Serializable {
@TableId(type = IdType.AUTO) //告诉MybatisPlus主键对应的属性,且属性是数据库自动增长的
Long id;
String name;
Integer type;
String description;
Integer recommended;
@TableField("created_time") //属性名和数据库字段名不一致
Date createTime;
@TableField("update_time")
Date updateTime;
}
3.3.2 在mapper中使用mybatisplus
//会在底层使用动态代理自动生成单表crud的代码
public interface DepartmentMapper extends BaseMapper<Department> {}
测试
public class TestDepartmentMapper {
@Autowired
DepartmentMapper departmentMapper;
@Test
public void test(){
//查询所有
List<Department> departments = departmentMapper.selectList(null);
//根据id查询
Department department2 = departmentMapper.selectById(1);
//根据id范围查询
departmentMapper.selectBatchIds(Arrays.asList(1,2,3));
//根据map集合中的条件进行条件查询
Map<String,Object> map = new HashMap<>();
map.put("name","kunkun");
List<Department> departments1 = departmentMapper.selectByMap(map);
//insert插入,update修改,delete删除
}
}
3.3.3 在service中使用mybatisplus
service接口类
//service中使用使用到的mybatisplus只会提供接口,下面的接口实现类还需要继承一个抽象类才能正常使用
public interface DepartmentService extends IService<Department> {}
service实现类
//在applicationContext.xml中配置扫描包即可,自动生成service层的代码
@Service
public class DepartmentServiceImpl extends ServiceImpl<DepartmentMapper, Department> implements DepartmentService { }
测试
public class TestDepartmentService {
@Autowired
DepartmentService departmentService;
@Test
public void test(){
Department department = Department.builder().id(1L).name("kumkum").build();
//查询所有
List<Department> list = departmentService.list();
//根据id查询,底层就是调用MybatisPlus的BaseMapper接口中的selectById方法
Department byId = departmentService.getById(2);
//根据id修改
departmentService.updateById(department);
//根据id范围查询
List<Department> departments = departmentService.listByIds(Arrays.asList(1, 2, 3));
//根据map集合中的条件进行条件查询
Map<String,Object> map = new HashMap<>();
map.put("name","kunkun");
List<Department> departments1 = departmentService.listByMap(map);
//根据id删除
departmentService.removeById(1);
//插入,底层调用的就是mybatisplus在实现mapper层时使用的insert
departmentService.save(department);
}
}
3.3.4 使用mybatisplus实现逻辑删除
在实体类和数据库表中添加删除标记属性,0可用,1删除,之后在实体类的删除标记对应属性上使用**@TableLogic**注解,在使用mybatisplus中的删除方法时,就是向数据库中发送sql设置对应数据的删除标记字段为1,只要实体类中配置了这个注解,mybatisplus在实现删除的时候都是假删,不配置就是真删,使用举例:
//执行后数据库中对应id为5的数据的删除标记字段值就为1
departmentService.removeById(5);
3.3.5 使用mybatisplus实现分页
在mysql中实现分页时,要使用limit关键字,后面有两个参数
第一个参数代表当前页面第一条数据在数据库中序号
第二个参数为为分页大小
如limit 0,5代表从第1条数据开始查询,查询5条数据
如limit 5,5代表从第6条数据开始查询,查询5条数据
在实现分页时,前端会传入当前页数和分页大小
后端会根据**(当前页数-1)* 分页大小** 计算得到查询得到本次查询的第一条数据在数据库中的序号,查询后将对应的数据返回给前端,同时也会将总数据条数返回给前端
下面将使用mybatisplus来简化分页查询
首先在applicationContext-dao.xml中对分页插件进行ioc,并在MybatisSqlSessionFactoryBean中注入分页插件
<bean class="com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor">
<constructor-arg name="dbType" value="MYSQL"/>
</bean>
<!--根据工厂类SqlSessionFactoryBean对SqlSessionFactory进行ioc和di-->
<bean class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
<!--注入连接池对象-->
<property name="dataSource" ref="dataSource"/>
<!--注入pojo实体类存放的位置-->
<property name="typeAliasesPackage" value="com.furong.pojo.entity"/>
<!--配置mybatisplus的分页插件-->
<property name="plugins">
<array>
<bean class="com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor">
<property name="interceptors" ref="paginationInnerInterceptor"/>
</bean>
</array>
</property>
</bean>
测试
@Test
public void testPage(){
//分页条件,参数1是开始页数,参数2是分页大小
Page<Department> page = new Page<>(1,5);
//根据分页条件进行分页查询
Page<Department> page1 = departmentMapper.selectPage(page, null);
//获得分页查询集合结果
List<Department> records = page1.getRecords();
//获得数据库总数据条数
long total = page1.getTotal();
}
3.3.6 实现条件查询+分页查询
//测试条件+分页查询
@Test
public void testQueryPage(){
//分页条件,参数1是开始页数,参数2是分页大小
Page<Department> page = new Page<>(1,5);
//创建查询条件对象queryWrapper
QueryWrapper<Department> queryWrapper = new QueryWrapper<>();
//设置查询条件对象queryWrapper中的条件,queryWrapper支持链式编程,eq代表等于,like代表模糊查询
queryWrapper.eq("name","外科").like("description","xxx");
//使用分页查询方法,并将查询条件对象传入第二个参数
Page<Department> page1 = departmentService.page(page, queryWrapper);
}
3.3.7 使用UpdateWrapper进行修改
@Test
public void testUpdate(){
UpdateWrapper<Department> wrapper = new UpdateWrapper<>();
//设置想要修改的字段值,最后跟上id
wrapper.set("name","ikun").set("description","xxx").eq("id",2);
departmentService.update(wrapper);
}
3.4 分页插件pagehelper
pom.xml导入依赖
<!--分页插件-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.3</version>
</dependency>
在appliactionContext.xml中的SqlSessionFactory中注入分页插件
<!--根据工厂类SqlSessionFactoryBean对SqlSessionFactory进行ioc和di-->
<bean class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
<!--注入连接池对象-->
<property name="dataSource" ref="dataSource"/>
<property name="plugins">
<array>
<!--配置pagehelper的分页插件-->
<bean class="com.github.pagehelper.PageInterceptor">
<property name="properties">
<value>
param1=value1;
</value>
</property>
</bean>
</array>
</property>
</bean>
测试
@Test
public void testPageHelper(){
//设置分页条件,参数1为当前页数,参数2为分页大小
PageHelper.startPage(1,5);
//查询数据,底层分页插件会拦截下面的sql语句,拼接上分页的limit条件
List<Department> list = departmentService.list();
//获取分页信息,其中有数据库总条数
PageInfo<Department> departmentPageInfo = new PageInfo<>(list);
}
3.5 实现查询科室列表功能
1、PO (Persistent Object)entity实体 里的每一个字段,与数据库表的字段相对应,
2、VO:值对象(Value Object) 通常用于业务层之间的数据传递VO (View Object)表现层对象,主要对应展示界面显示的数据对象,用一个VO对象来封装整个界面展示所需要的对象数据。
3、DTO 数据传输对象(Data Transfer Object)是一种设计模式之间传输数据的软件应用系统。
3.5.1 编写公用的分页dto
@Data
public class BasePageDto implements Serializable {
//当前页数
Integer page = 1;
//分页大小
Integer limit = 5;
}
3.5.2 编写科室查询的DTO
@Data
public class DepartmentQueryDto extends BasePageDto{
//添加科室表的特有查询条件
//科室名查询
String name;
//科室类型查询
Integer type; //1门诊,0非门诊
//推荐级别查询
Integer recommended; //1推荐,0不推荐
}
3.5.3 编写controller接口
layui需要接收的返回数据格式
{
"code": 0,
"msg": "...",
"count": 3,
"data": [{
"id": "1",
"code": "0x0001",
"name": "阳光智慧小区",
"address": "成都",
"totalBuildings": 30,
"totalHouseholds": 90,
"image": "aaa",
"estateCompany": "奥利给物业",
"created": "2021-06-20"
}, {
"id": "2",
"code": "0x0001",
"name": "阳光智慧小区",
"address": "成都",
"totalBuildings": 30,
"totalHouseholds": 90,
"image": "aaa",
"estateCompany": "奥利给物业",
"created": "2021-06-20"
}]
}
@RequestMapping("/department")
@RestController //进行ioc并且方法都返回json格式,等于Controller注解+ResponseBody注解
public class DepartmentController {
@Autowired
DepartmentService departmentService;
@RequestMapping(value = "list",method = RequestMethod.GET)
public Map list(DepartmentQueryDto dto){
//组装分页条件
PageHelper.startPage(dto.getPage(),dto.getLimit());
//组装查询条件
QueryWrapper<Department> wrapper = new QueryWrapper<>();
//若name不为空,使用name模糊查询
if(!StringUtils.isEmpty(dto.getName()))
wrapper.like("name",dto.getName());
if(dto.getType()!=null)
wrapper.eq("type",dto.getType());
if(dto.getRecommended()!=null)
wrapper.like("recommended",dto.getRecommended());
//查询结果
List<Department> list = departmentService.list(wrapper);
//获取分页信息
PageInfo<Department> departmentPageInfo = new PageInfo<>(list);
Map map = new HashMap();
map.put("code","0");
map.put("msg","success");
map.put("count",departmentPageInfo.getTotal());
map.put("data",list);
return map;
}
}
3.5.4 测试
启动jetty,postiman测试
接口地址8081/admin在配置jetty的pom依赖时设置
3.6 跨域
域名,协议,端口只要有一个不同,就是跨域
解决跨域
1.服务器告诉浏览器允许跨域, 后端解决
编写过滤器类,Filter从servlet中引入
public class CORSFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException, IOException {
HttpServletRequest request= (HttpServletRequest) req;
HttpServletResponse response= (HttpServletResponse) res;
/* 允许跨域的主机地址 */
response.setHeader("Access-Control-Allow-Origin", "*");
/* 允许跨域的请求方法GET, POST, HEAD 等 */
response.setHeader("Access-Control-Allow-Methods", "*");
/* 重新预检验跨域的缓存时间 (s) */
response.setHeader("Access-Control-Max-Age", "3600");
/* 允许跨域的请求头 */
response.setHeader("Access-Control-Allow-Headers", "*");
/* 是否携带cookie */
response.setHeader("Access-Control-Allow-Credentials", "true");
//判断前端发送的是否是预请求,如果是,直接返回,并且告诉他允许的请求方式
if("OPTIONS".equalsIgnoreCase(request.getMethod()))
return;
chain.doFilter(request,response);
}
@Override
public void destroy() {
}
}
配置过滤器
web.xml
<!--配置解决跨域过滤器-->
<filter>
<filter-name>CORSFilter</filter-name>
<filter-class>com.furong.admin.filter.CORSFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CORSFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
2.nginx配置在同一域
3.7 controller层crud接口
@RequestMapping("/department")
@RestController //进行ioc并且方法都返回json格式,等于Controller注解+ResponseBody注解
public class DepartmentController {
@Autowired
DepartmentService departmentService;
@RequestMapping(value = "/list",method = RequestMethod.GET)
public Map list(DepartmentQueryDto dto){
//组装分页条件
PageHelper.startPage(dto.getPage(),dto.getLimit());
//组装查询条件
QueryWrapper<Department> wrapper = new QueryWrapper<>();
//若name不为空,使用name模糊查询
if(!StringUtils.isEmpty(dto.getName()))
wrapper.like("name",dto.getName());
if(dto.getType()!=null)
wrapper.eq("type",dto.getType());
if(dto.getRecommended()!=null)
wrapper.like("recommended",dto.getRecommended());
//查询结果
List<Department> list = departmentService.list(wrapper);
//获取分页信息
PageInfo<Department> departmentPageInfo = new PageInfo<>(list);
Map map = new HashMap();
map.put("code","0");
map.put("msg","success");
map.put("count",departmentPageInfo.getTotal());
map.put("data",list);
return map;
}
@RequestMapping(value = "/{id}",method = RequestMethod.GET)
public Map find(@PathVariable("id")Long id){
//查询数据
Department byId = departmentService.getById(id);
//封装返回结果
Map map = new HashMap();
map.put("code","0");
map.put("msg","success");
map.put("data",byId);
return map;
}
@RequestMapping(value = "/add",method = RequestMethod.POST)
public Map add(@RequestBody Department department){
//调用service添加
boolean b = departmentService.save(department);
Map map = new HashMap();
if(b){
map.put("code","0");
map.put("msg","success");
}else {
map.put("code","5000");
map.put("msg","failed");
}
return map;
}
//不带分页查询所有
@RequestMapping(value = "/findAll",method = RequestMethod.GET)
public Map findAll(DepartmentQueryDto dto){
//查询结果
List<Department> list = departmentService.list();
Map map = new HashMap();
map.put("code","0");
map.put("msg","success");
map.put("data",list);
return map;
}
@RequestMapping(value = "/update",method = RequestMethod.POST)
public Map update(@RequestBody Department department){
//调用service添加
boolean b = departmentService.updateById(department);
Map map = new HashMap();
if(b){
map.put("code","0");
map.put("msg","success");
}else {
map.put("code","5000");
map.put("msg","failed");
}
return map;
}
@RequestMapping(value = "/deleteById",method = RequestMethod.GET)
public Map deleteById(@RequestParam Long id){
//调用service添加
boolean b = departmentService.removeById(id);
Map map = new HashMap();
if(b){
map.put("code","0");
map.put("msg","success");
}else {
map.put("code","5000");
map.put("msg","failed");
}
return map;
}
}
3.8 对接前端
3.8.1 添加
<body>
<!--html代码中使输入框input标签的name属性与实体类bean中对应属性名一致-->
</body>
<script>
layui.use(['form','jquery','upload'],function(){
let form = layui.form;
let $ = layui.jquery;
let upload = layui.upload;
form.on('submit(department-save)', function(data){
$.ajax({
url:'http://localhost:8081/admin/department/add',
data:JSON.stringify(data.field),
dataType:'json',
contentType:'application/json',
type:'post',
success:function(result){
if(result.code==0){
layer.msg(result.msg,{icon:1,time:1000},function(){
parent.layer.close(parent.layer.getFrameIndex(window.name));//关闭当前页
window.parent.location.reload()
});
}else{
layer.msg(result.msg,{icon:2,time:1000});
}
}
})
return false;
});
})
</script>
3.8.2 删除
window.remove = function(obj){
layer.confirm('确定要删除该科室吗?', {icon: 3, title:'提示'}, function(index){
layer.close(index);
let loading = layer.load();
$.ajax({
url: "http://localhost:8081/admin/department/deleteById",
data:{
id:obj.data['id']
},
success:function(result){
layer.close(loading);
if(result.code==0){
layer.msg(result.msg,{icon:1,time:1000},function(){
obj.del();
});
}else{
layer.msg(result.msg,{icon:2,time:1000});
}
}
})
});
}
3.8.3 修改
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="../../../component/pear/css/pear.css" />
<style type="text/css">
input.layui-input.layui-unselect {width:200px}
</style>
</head>
<body>
<form class="layui-form" lay-filter="layItem" action="">
<div class="mainBox">
<div class="main-container">
<div class="main-container">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">科室名称</label>
<div class="layui-input-block">
<input type="text" name="name" lay-verify="title" autocomplete="off" placeholder="" class="layui-input">
</div>
</div>
</div>
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">科室类型</label>
<div class="layui-input-block">
<input type="radio" name="type" value="1" title="门诊">
<input type="radio" name="type" value="0" title="非门诊" checked>
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">推荐级别</label>
<div class="layui-input-block">
<input type="radio" name="recommended" value="1" title="首页推荐">
<input type="radio" name="recommended" value="0" title="不推荐" checked>
</div>
</div>
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">科室介绍</label>
<div class="layui-input-block">
<textarea name="description" placeholder="请输入介绍" class="layui-textarea" style="width: 500px"></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="bottom">
<div class="button-container">
<button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit="" lay-filter="department-save">
<i class="layui-icon layui-icon-ok"></i>
提交
</button>
<button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">
<i class="layui-icon layui-icon-refresh"></i>
重置
</button>
</div>
</div>
</form>
<script src="../../../component/layui/layui.js"></script>
<script src="../../../component/pear/pear.js"></script>
<script type="text/javascript" src="../../../component/layui/base.js"></script>
<script>
layui.use(['form','jquery','upload'],function(){
let form = layui.form;
let $ = layui.jquery;
let upload = layui.upload;
//获取url后面携带的参数
let id=getQueryVariable("id");
//回显数据
$.ajax({
url:'http://localhost:8081/admin/department/'+id,
method:"GET",
success:function(obj){
let data=obj.data;
//layui表单赋值回显
form.val("layItem", { //formTest 即 class="layui-form" 所在元素属性 lay-filter="" 对应的值
id:id,
name:data.name,
type:data.type,
recommended:data.recommended,
description:data.description
});
}
});
form.on('submit(department-save)', function(data){
//设置修改的id
data.field.id=id;
$.ajax({
url:'http://localhost:8081/admin/department/update',
data:JSON.stringify(data.field),
dataType:'json',
contentType:'application/json',
type:'post',
success:function(result){
if(result.code==0){
layer.msg(result.msg,{icon:1,time:1000},function(){
parent.layer.close(parent.layer.getFrameIndex(window.name));//关闭当前页
window.parent.location.reload()
});
}else{
layer.msg(result.msg,{icon:2,time:1000});
}
}
})
return false;
});
})
</script>
<script>
</script>
</body>
</html>
3.8.4 查询
table.render({
elem: '#department-table',
url: 'http://localhost:8081/admin/department/list',
page: true ,
cols: cols ,
skin: 'line',
toolbar: '#role-toolbar',
defaultToolbar: [{
layEvent: 'refresh',
icon: 'layui-icon-refresh',
}, 'filter', 'print', 'exports']
});
3.8.5 批量删除
编写dto接收批量删除的数据
@Data
public class BaseDeleteDto implements Serializable {
/**
* 批量删除
*/
List<Integer> ids;
}
编写controller
@RequestMapping(value = "/deleteByIds",method = RequestMethod.POST)
public Result deleteByIds(@RequestBody BaseDeleteDto dto) {
boolean b = consultingRoomService.removeByIds(dto.getIds());
return ResultUtils.judge(b);
}
前端对接
<script>
//上方html批量删除按钮的lay-event="batchRemove"
window.batchRemove = function(obj){
let data = table.checkStatus(obj.config.id).data;
if(data.length === 0){
layer.msg("未选中数据",{icon:3,time:1000});
return false;
}
//构建批量删除的id的json数据
let json = {
ids:[]
}
for(let i = 0;i < data.length;i++){
json.ids.push(data[i].id)
}
layer.confirm('确定要删除这些诊室', {icon: 3, title:'提示'}, function(index){
layer.close(index);
let loading = layer.load();
$.ajax({
url: "http://localhost:8081/admin/consult/deleteByIds",
data:JSON.stringify(json),
dataType:'json',
contentType:'application/json',
type:'post',
success:function(result){
layer.close(loading);
if(result.success){
layer.msg(result.msg,{icon:1,time:1000},function(){
table.reload('user-table');
});
}else{
layer.msg(result.msg,{icon:2,time:1000});
}
}
})
});
}
</script>
3.8.6 完整代码
department.html页面
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>科室管理</title>
<link href="../../component/pear/css/pear.css" rel="stylesheet" />
</head>
<body class="pear-container">
<div class="layui-card">
<div class="layui-card-body">
<form class="layui-form" action="">
<div class="layui-form-item">
<div class="layui-form-item layui-inline">
<label class="layui-form-label">科室名称</label>
<div class="layui-input-inline">
<input type="text" name="name" placeholder="" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<select name="type" lay-filter="aihao" >
<option value="">请选择类型</option>
<option value="0">非门诊</option>
<option value="1">门诊</option>
</select>
</div>
<div class="layui-form-item layui-inline">
<select name="recommended" lay-filter="aihao" >
<option value="">请选择类型</option>
<option value="0">不推荐</option>
<option value="1">首页推荐</option>
</select>
</div>
<div class="layui-form-item layui-inline">
<button class="pear-btn pear-btn-md pear-btn-primary" lay-submit lay-filter="department-query">
<i class="layui-icon layui-icon-search"></i>
查询
</button>
<button type="reset" class="pear-btn pear-btn-md">
<i class="layui-icon layui-icon-refresh"></i>
重置
</button>
</div>
</div>
</form>
</div>
</div>
<div class="layui-card">
<div class="layui-card-body">
<table id="department-table" lay-filter="department-table"></table>
</div>
</div>
<script type="text/html" id="role-toolbar">
<button class="pear-btn pear-btn-primary pear-btn-md" lay-event="add">
<i class="layui-icon layui-icon-add-1"></i>
新增
</button>
<button class="pear-btn pear-btn-danger pear-btn-md" lay-event="batchRemove">
<i class="layui-icon layui-icon-delete"></i>
删除
</button>
</script>
<script type="text/html" id="role-bar">
<button class="pear-btn pear-btn-primary pear-btn-sm" lay-event="edit"><i class="layui-icon layui-icon-edit"></i></button>
<button class="pear-btn pear-btn-danger pear-btn-sm" lay-event="remove"><i class="layui-icon layui-icon-delete"></i></button>
</script>
<script type="text/html" id="role-enable">
<input type="checkbox" name="enable" value="{{d.id}}" lay-skin="switch" lay-text="上架|下架" lay-filter="user-enable" checked = "{{ d.id == 10003 ? 'true' : 'false' }}">
</script>
<script src="../../component/layui/layui.js"></script>
<script src="../../component/pear/pear.js"></script>
<script>
layui.use(['table','form','jquery'],function () {
let table = layui.table;
let form = layui.form;
let $ = layui.jquery;
let MODULE_PATH = "./department/";
let cols = [
[
//field名字与上方table字段名一致
{type:'checkbox'},
{title: '序号', field: 'id', align:'center'},
{title: '科室名称', field: 'name', align:'center'},
{title: '科室类型', field: 'type', align:'center',templet : function(d) {
var p = d.type;
if(p==0){
return '非门诊'
}else if(p==1){
return '门诊'
}
}
},
{title: '推荐级别', field: 'recommended', align:'center',templet : function(d) {
var p = d.recommended;
if(p==0){
return '不推荐'
}else if(p==1){
return '首页推荐'
}
}
},
{title: '操作', toolbar: '#role-bar', align:'center'}
]
]
//查询全部
table.render({
elem: '#department-table',
url: 'http://localhost:8081/admin/department/list',
page: true ,
cols: cols ,
skin: 'line',
toolbar: '#role-toolbar',
defaultToolbar: [{
layEvent: 'refresh',
icon: 'layui-icon-refresh',
}, 'filter', 'print', 'exports']
});
table.on('tool(department-table)', function(obj){
if(obj.event === 'remove'){
window.remove(obj);
} else if(obj.event === 'edit'){
window.edit(obj);
} else if(obj.event === 'power'){
window.power(obj);
}
});
table.on('toolbar(department-table)', function(obj){
if(obj.event === 'add'){
window.add();
} else if(obj.event === 'refresh'){
window.refresh();
} else if(obj.event === 'batchRemove'){
window.batchRemove(obj);
}
});
//条件查询
form.on('submit(department-query)', function(data){
table.reload('department-table',{where:data.field})
return false;
});
form.on('switch(role-enable)', function(obj){
layer.tips(this.value + ' ' + this.name + ':'+ obj.elem.checked, obj.othis);
});
window.add = function(){
layer.open({
type: 2,
title: '新增',
shade: 0.1,
maxmin: true,
area: ['600px', '500px'],
content: MODULE_PATH + 'add.html'
});
}
window.edit = function(obj){
layer.open({
type: 2,
title: '修改',
shade: 0.1,
area: ['600px', '500px'],
content: MODULE_PATH +'edit.html?id='+obj.data.id
});
}
//删除
window.remove = function(obj){
layer.confirm('确定要删除该科室吗?', {icon: 3, title:'提示'}, function(index){
layer.close(index);
let loading = layer.load();
$.ajax({
url: "http://localhost:8081/admin/department/deleteById",
data:{
id:obj.data['id']
},
success:function(result){
layer.close(loading);
if(result.code==0){
layer.msg(result.msg,{icon:1,time:1000},function(){
obj.del();
});
}else{
layer.msg(result.msg,{icon:2,time:1000});
}
}
})
});
}
window.batchRemove = function(obj){
let data = table.checkStatus(obj.config.id).data;
if(data.length === 0){
layer.msg("未选中数据",{icon:3,time:1000});
return false;
}
let ids = "";
for(let i = 0;i<data.length;i++){
ids += data[i].userId+",";
}
ids = ids.substr(0,ids.length-1);
layer.confirm('确定要删除这些用户', {icon: 3, title:'提示'}, function(index){
layer.close(index);
let loading = layer.load();
$.ajax({
url: MODULE_PATH+"batchRemove/"+ids,
dataType:'json',
type:'delete',
success:function(result){
layer.close(loading);
if(result.success){
layer.msg(result.msg,{icon:1,time:1000},function(){
table.reload('user-table');
});
}else{
layer.msg(result.msg,{icon:2,time:1000});
}
}
})
});
}
window.refresh = function(){
table.reload('department-table');
}
})
</script>
</body>
</html>
add.html页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="../../../component/pear/css/pear.css" />
<style type="text/css">
input.layui-input.layui-unselect {width:200px}
</style>
</head>
<body>
<form class="layui-form" action="">
<div class="mainBox">
<div class="main-container">
<div class="main-container">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">科室名称</label>
<div class="layui-input-block">
<input type="text" name="name" lay-verify="title" autocomplete="off" placeholder="" class="layui-input">
</div>
</div>
</div>
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">科室类型</label>
<div class="layui-input-block">
<input type="radio" name="type" value="1" title="门诊">
<input type="radio" name="type" value="0" title="非门诊" checked>
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">推荐级别</label>
<div class="layui-input-block">
<input type="radio" name="recommended" value="1" title="首页推荐">
<input type="radio" name="recommended" value="0" title="不推荐" checked>
</div>
</div>
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">科室介绍</label>
<div class="layui-input-block">
<textarea name="description" placeholder="请输入介绍" class="layui-textarea" style="width: 500px"></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="bottom">
<div class="button-container">
<button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit="" lay-filter="department-save">
<i class="layui-icon layui-icon-ok"></i>
提交
</button>
<button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">
<i class="layui-icon layui-icon-refresh"></i>
重置
</button>
</div>
</div>
</form>
<script src="../../../component/layui/layui.js"></script>
<script src="../../../component/pear/pear.js"></script>
<script>
layui.use(['form','jquery','upload'],function(){
let form = layui.form;
let $ = layui.jquery;
let upload = layui.upload;
form.on('submit(department-save)', function(data){
$.ajax({
url:'http://localhost:8081/admin/department/add',
data:JSON.stringify(data.field),
dataType:'json',
contentType:'application/json',
type:'post',
success:function(result){
if(result.code==0){
layer.msg(result.msg,{icon:1,time:1000},function(){
parent.layer.close(parent.layer.getFrameIndex(window.name));//关闭当前页
window.parent.location.reload()
});
}else{
layer.msg(result.msg,{icon:2,time:1000});
}
}
})
return false;
});
})
</script>
<script>
</script>
</body>
</html>
edit.html页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="../../../component/pear/css/pear.css" />
<style type="text/css">
input.layui-input.layui-unselect {width:200px}
</style>
</head>
<body>
<form class="layui-form" lay-filter="layItem" action="">
<div class="mainBox">
<div class="main-container">
<div class="main-container">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">科室名称</label>
<div class="layui-input-block">
<input type="text" name="name" lay-verify="title" autocomplete="off" placeholder="" class="layui-input">
</div>
</div>
</div>
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">科室类型</label>
<div class="layui-input-block">
<input type="radio" name="type" value="1" title="门诊">
<input type="radio" name="type" value="0" title="非门诊" checked>
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">推荐级别</label>
<div class="layui-input-block">
<input type="radio" name="recommended" value="1" title="首页推荐">
<input type="radio" name="recommended" value="0" title="不推荐" checked>
</div>
</div>
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">科室介绍</label>
<div class="layui-input-block">
<textarea name="description" placeholder="请输入介绍" class="layui-textarea" style="width: 500px"></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="bottom">
<div class="button-container">
<button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit="" lay-filter="department-save">
<i class="layui-icon layui-icon-ok"></i>
提交
</button>
<button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">
<i class="layui-icon layui-icon-refresh"></i>
重置
</button>
</div>
</div>
</form>
<script src="../../../component/layui/layui.js"></script>
<script src="../../../component/pear/pear.js"></script>
<script type="text/javascript" src="../../../component/layui/base.js"></script>
<script>
layui.use(['form','jquery','upload'],function(){
let form = layui.form;
let $ = layui.jquery;
let upload = layui.upload;
//获取url后面携带的参数
let id=getQueryVariable("id");
//回显数据
$.ajax({
url:'http://localhost:8081/admin/department/'+id,
method:"GET",
success:function(obj){
let data=obj.data;
//layui表单赋值回显
form.val("layItem", { //formTest 即 class="layui-form" 所在元素属性 lay-filter="" 对应的值
id:id,
name:data.name,
type:data.type,
recommended:data.recommended,
description:data.description
});
}
});
form.on('submit(department-save)', function(data){
//设置修改的id
data.field.id=id;
$.ajax({
url:'http://localhost:8081/admin/department/update',
data:JSON.stringify(data.field),
dataType:'json',
contentType:'application/json',
type:'post',
success:function(result){
if(result.code==0){
layer.msg(result.msg,{icon:1,time:1000},function(){
parent.layer.close(parent.layer.getFrameIndex(window.name));//关闭当前页
window.parent.location.reload()
});
}else{
layer.msg(result.msg,{icon:2,time:1000});
}
}
})
return false;
});
})
</script>
<script>
</script>
</body>
</html>
3.9 封装结果集
3.9.1 编写结果集类
这里定义的结果集类对应后端传递到前端的响应结果的json类型, 之后后端在响应前端请求时都使用该结果集, 传递到前端后自动将该类对象转换为json格式
@Data
public class Result<T> {
Integer code; //响应状态码,0成功
String msg; //提示
Long count; //总记录条数,分页查询时携带该参数
T data; //后端响应的实际数据
}
3.9.2 编写结果集工具类
public class ResultUtils {
/**
* 构建带分页的查询返回
* @param data
* @param count
* @return
*/
public static Result buildSuccess(Object data,Long count){
Result result = new Result();
result.setCode(0);
result.setMsg("操作成功");
result.setData(data);
result.setCount(count);
return result;
}
/**
* 构建单个查询结果的返回
* @param data
* @return
*/
public static Result buildSuccess(Object data){
return buildSuccess(data,null);
}
/**
* 构建增删改的成功返回
* @return
*/
public static Result buildSuccess(){
return buildSuccess(null);
}
/**
* 构建增删改的失败返回
* @return
*/
public static Result buildFailed(Integer code,String msg){
Result result = new Result();
result.setCode(code);
result.setMsg(msg);
return result;
}
}
3.9.3 对controller进行优化
@RequestMapping("/department")
@RestController //进行ioc并且方法都返回json格式,等于Controller注解+ResponseBody注解
public class DepartmentController {
@Autowired
DepartmentService departmentService;
@RequestMapping(value = "/list",method = RequestMethod.GET)
public Result list(DepartmentQueryDto dto){
//组装分页条件
PageHelper.startPage(dto.getPage(),dto.getLimit());
//组装查询条件
QueryWrapper<Department> wrapper = new QueryWrapper<>();
//若name不为空,使用name模糊查询
if(!StringUtils.isEmpty(dto.getName()))
wrapper.like("name",dto.getName());
if(dto.getType()!=null)
wrapper.eq("type",dto.getType());
if(dto.getRecommended()!=null)
wrapper.like("recommended",dto.getRecommended());
//查询结果
List<Department> list = departmentService.list(wrapper);
//获取分页信息
PageInfo<Department> departmentPageInfo = new PageInfo<>(list);
return ResultUtils.buildSuccess(list,departmentPageInfo.getTotal());
}
@RequestMapping(value = "/{id}",method = RequestMethod.GET)
public Result find(@PathVariable("id")Long id){
//查询数据
Department byId = departmentService.getById(id);
//封装返回结果
return ResultUtils.buildSuccess(byId);
}
@RequestMapping(value = "/add",method = RequestMethod.POST)
public Result add(@RequestBody Department department){
//调用service添加
boolean b = departmentService.save(department);
if(b){
return ResultUtils.buildSuccess();
}else {
return ResultUtils.buildFailed(1,"操作失败");
}
}
//不带分页查询所有
@RequestMapping(value = "/findAll",method = RequestMethod.GET)
public Result findAll(DepartmentQueryDto dto){
//查询结果
List<Department> list = departmentService.list();
return ResultUtils.buildSuccess(list);
}
@RequestMapping(value = "/update",method = RequestMethod.POST)
public Result update(@RequestBody Department department){
//调用service添加
boolean b = departmentService.updateById(department);
if(b){
return ResultUtils.buildSuccess();
}else {
return ResultUtils.buildFailed(1,"操作失败");
}
}
@RequestMapping(value = "/deleteById",method = RequestMethod.GET)
public Result deleteById(@RequestParam Long id){
//调用service添加
boolean b = departmentService.removeById(id);
if(b){
return ResultUtils.buildSuccess();
}else {
return ResultUtils.buildFailed(1,"操作失败");
}
}
}
3.10 多表一对一列表查询
应该显示科室名
3.10.1 编写Dto
3.10.2 编写sql
SELECT
cr.id,
cr.`name`,
d.`name`as depName,
cr.address,
cr.create_time as createTime
FROM
consulting_room cr
LEFT JOIN
fr_department d ON cr.department_id = d.id
<where>
<if test="name!=null and name!=''">
AND cr.`name` LIKE CONCAT('%',#{name},'%')
</if>
<if test="departmentId!=null">
AND cr.department_id=#{departmentId}
</if>
<if test="address!=null">
AND cr.address LIKE CONCAT('%',#{address},'%')
</if>
AND cr.is_deleted=0
</where>
3.10.3 编写vo实体类接收查询结果
@Data
public class ConsultingRoomListVo implements Serializable {
Integer id;
String name;
String depName;
String address;
Date createTime;
}
3.10.4 编写mapper
3.10.4 .1 编写接口
public interface ConsultingRoomMapper extends BaseMapper<ConsultingRoom> {
/**
* 带条件查询
* @param dto
* @return
*/
List<ConsultingRoom> findConsultingRoomByQueryDto(ConsultingRoomQueryDto dto);
}
3.10.4 .1 编写xml
ConsultingRoomMapper.xml放在resources/mapper文件夹下
<?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">
<!--namespace和mapper接口全限定名一致才能关联到mapper-->
<mapper namespace="com.furong.admin.mapper.ConsultingRoomMapper">
<select id="findConsultingRoomByQueryDto" parameterType="com.furong.pojo.dto.ConsultingRoomQueryDto" resultType="com.furong.pojo.vo.ConsultingRoomListVo">
SELECT
cr.id,
cr.`name`,
d.`name`as depName,
cr.address,
cr.create_time as createTime
FROM
consulting_room cr
LEFT JOIN
fr_department d ON cr.department_id = d.id
<where>
<if test="name!=null and name!=''">
AND cr.`name` LIKE CONCAT('%',#{name},'%')
</if>
<if test="departmentId!=null">
AND cr.department_id=#{departmentId}
</if>
<if test="address!=null">
AND cr.address LIKE CONCAT('%',#{address},'%')
</if>
AND cr.is_deleted=0
</where>
</select>
</mapper>
因为mapper接口和xml文件不在同一目录下,所以需要在applicationContext-dao.xml的SqlSessionFactory对象中注入mapper文件的加载位置
<!--根据工厂类SqlSessionFactoryBean对SqlSessionFactory进行ioc和di-->
<bean class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
<!--加载mapper.xml配置文件的位置-->
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
<!--注入连接池对象-->
<property name="dataSource" ref="dataSource"/>
<!--注入pojo实体类存放的位置-->
<property name="typeAliasesPackage" value="com.furong.pojo.entity"/>
<property name="plugins">
<array>
<!--配置mybatisplus的分页插件-->
<bean class="com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor">
<property name="interceptors" ref="paginationInnerInterceptor"/>
</bean>
<!--配置pagehelper的分页插件-->
<bean class="com.github.pagehelper.PageInterceptor">
<property name="properties">
<value>
param1=value1;
</value>
</property>
</bean>
</array>
</property>
</bean>
3.10.4 编写service
service接口
public interface ConsultingRoomService extends IService<ConsultingRoom> {
List<ConsultingRoomListVo> findConsultingRoomByQueryDto(ConsultingRoomQueryDto consultingRoomQueryDto);
}
service实现
@Service
public class ConsultingRoomServiceImpl extends ServiceImpl<ConsultingRoomMapper, ConsultingRoom> implements ConsultingRoomService {
@Override
public List<ConsultingRoomListVo> findConsultingRoomByQueryDto(ConsultingRoomQueryDto dto) {
//Mapper通过在dao.xml配置扫描包进行了ioc,ServiceImpl中注入了对应的mapper,所以getBaseMapper能直接返回spring容器中的mapper
return getBaseMapper().findConsultingRoomByQueryDto(dto);
}
}
3.10.5 编写controller
public class ConsultingRoomController {
@Autowired
ConsultingRoomService consultingRoomService;
@RequestMapping(value = "/list",method = RequestMethod.GET)
public Result list(ConsultingRoomQueryDto dto){
//组装分页条件
PageHelper.startPage(dto.getPage(),dto.getLimit());
//查询结果,用专用的多表查询结果实体vo接收
List<ConsultingRoomListVo> list = consultingRoomService.findConsultingRoomByQueryDto(dto);
//获取分页信息
PageInfo<ConsultingRoomListVo> departmentPageInfo = new PageInfo<>(list);
return ResultUtils.buildSuccess(list,departmentPageInfo.getTotal());
}
3.11 动态显示下拉菜单
这里的隶属科室下拉菜单应该从数据库的科室表读取数据后动态生成
<div class="layui-form-item">
<label class="layui-form-label">隶属科室</label>
<div class="layui-input-block">
<!--name字段应和后端接收对象的科室id-->
<select name="departmentId" id="departmentCheckBox" lay-filter="aihao" >
</select>
</div>
</div>
<script>
layui.use(['form','jquery','upload'],function(){
//动态添加科室列表
$.ajax({
type:"get",
dataType:"json",
//不带分页获取所有科室的接口
url:"http://localhost:8081/admin/department/findAll",
success:function(res){
var data = res.data;
$.each(data,function(index,department){
//departmentCheckBox为下拉元素id
$("#departmentCheckBox").append("<option value='"+department.id+"'>"+department.name+"</option>");
});
layui.form.render("select")
}
})
</script>
3.11 多表一对一删除
诊室的科室id引用自科室表,当科室被删除时,诊室一方会有不同的处理方法
1.禁止被诊室关联的科室的删除
2.可以删除,但解除诊室方记录的科室id
3.13 一对多查询xml代码
doctor表中这四个字段存放的都是对应表中的id,这里需要多表查询出来其对应的名字并进行显示
<select id="findDoctorByQueryDto" parameterType="com.furong.pojo.dto.DoctorQueryDto" resultType="com.furong.pojo.vo.DoctorListVo">
SELECT
doctor.id,
doctor.name,
doctor.sex,
doctor.phone,
dep.`name` AS depName,
consult.name AS consultName,
itemPost.name AS postName,
itemEdu.name AS eduName
FROM
fr_doctor doctor
LEFT JOIN fr_department dep ON doctor.department_id = dep.id
LEFT JOIN consulting_room consult ON doctor.consulting_id = consult.id
LEFT JOIN fr_dict_item itemPost ON doctor.post = itemPost.id
LEFT JOIN fr_dict_item itemEdu ON doctor.edu = itemEdu.id
<where>
<if test="name!=null and name!=''">
AND doctor.name LIKE CONCAT('%',#{name},'%')
</if>
<if test="departmentId!=null">
AND doctor.department_id=#{departmentId}
</if>
<if test="post!=null">
AND doctor.post=#{post}
</if>
AND doctor.is_deleted=0
</where>
</select>
3.12 图片上传
异步图片上传流程
3.12.1 对接腾讯云存储
导入依赖
<dependency>
<groupId>com.qcloud</groupId>
<artifactId>cos_api</artifactId>
<version>5.6.89</version>
</dependency>
上传图片controller接口
@RestController
@RequestMapping("/upload")
public class UploadController {
private org.slf4j.Logger log = LoggerFactory.getLogger(UploadController.class);
//配置图片格式
private static final List<String> EXT_LIST = Arrays.asList(".jpg",".png",".gif");
@RequestMapping("/image")
public Result upload(@RequestParam MultipartFile file) throws IOException {
//获取原名字
String originalFilename = file.getOriginalFilename();
log.debug("originalFilename:{}",originalFilename);
//获取后缀
String extName = originalFilename.substring(originalFilename.lastIndexOf("."));;
log.debug("后缀为:{}",extName);
//判断类型是否合法
if(!EXT_LIST.contains(extName)){
log.error("上传图片不合法");
return ResultUtils.buildFailed(20001,"格式错误");
}
//生成临时名字
String picName = UUID.randomUUID().toString().replace("-", "") + extName;
log.debug("新名字:{}",picName);
//创建对象存放图片
File tempFile = File.createTempFile("image-",picName);
file.transferTo(tempFile);
//上传到腾讯云中的hospital文件夹中,其中会格式化名字
String fileName = TencentCOS.uploadfile(tempFile, "hospital");
//删除临时图片对象
if(tempFile.exists()){
tempFile.delete();
}
//返回图片访问地址
return ResultUtils.buildSuccess("https://hospital-1317500932.cos.ap-chengdu.myqcloud.com/"+fileName);
}
}
前端接口
//普通图片上传
var uploadInst = upload.render({
elem: '#test1'
,url: 'http://localhost:8081/admin/upload/image' //改成您自己的上传接口
,field:"file"//上传文件的字段名,这里必须要和controller层方法形参名一致
,before: function(obj){
//预读本地文件示例,不支持ie8
obj.preview(function(index, file, result){
$('#demo1').attr('src', result); //图片链接(base64)
});
}
,done: function(res){
//如果上传失败
if(res.code > 0){
return layer.msg('上传失败');
}
$('#demo1').attr('src',res.data);
//上传成功 前端需要把图片存储在隐藏域中
$(".layui-form").append("<input class='image' type='hidden' name='image' value='"+res.data+"'>");
}
});
测试
4. 进阶知识
4.1 异常处理
项目中分为预期异常和不可预期异常
4.1.1 编写自定义异常
public class CustomException extends RuntimeException{
String msg;
public CustomException(String msg){
super(msg);
this.msg = msg;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
4.1.2 编写异常处理器
public class CustomExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception exception) {
String msg;
if(exception instanceof CustomException){
CustomException customException = (CustomException) exception;
msg = customException.getMsg();
}else {
exception.printStackTrace();
msg="系统繁忙";
}
Result result = ResultUtils.buildFailed(5000,msg);
//将结果转换为json发给前端
ResponseUtils.responseToJson(JsonUtils.objectToJson(result),response);
return new ModelAndView();
}
}
4.1.3 配置异常处理器
springmvc.xml
<!--配置异常处理器-->
<bean class="com.furong.core.exception.CustomExceptionResolver"/>
5. 日志
5.1 配置文件
### direct log messages to stdout ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
### set log levels - for more verbose logging change 'info' to 'debug' ###
log4j.appender.D = org.apache.log4j.DailyRollingFileAppender
log4j.appender.D.File = logs/log.log
log4j.appender.D.Append = true
log4j.appender.D.Threshold = DEBUG
log4j.appender.D.layout = org.apache.log4j.PatternLayout
log4j.appender.D.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n
####
<!--显示debug级别以上的日志,在stdout和D中配置的位置保存-->
log4j.rootLogger=debug, stdout,D
6. swagger和nginx配置反向代理集群
6.1 nginx配置反向代理集群
修改nginx.conf文件
7. 认证
用户访问时通过拦截器判断是否携带token的登录信息,若未携带则禁止访问,拦截器用来拦截除了登录的所有请求
7.1 使用Lambda实现条件查询
测试
@Test
public void test(){
LambdaQueryWrapper<Admin> lambdaQueryWrapper = new LambdaQueryWrapper<>();
//将第二个参数作为name字段的查询条件
lambdaQueryWrapper.like(Admin::getName,"打")
//将第二个字段作为username字段的查询条件
.eq(Admin::getUsername,"123");
//list查询方法接收dto类型参数
List<Admin> list = adminService.list(lambdaQueryWrapper);
System.out.println(list);
}
实际在controller中使用
@ApiOperation("根据条件分页查询")
@RequestMapping(value = "/list",method = RequestMethod.GET)
public Result list(AdminQueryDto dto){
//组装分页条件
PageHelper.startPage(dto.getPage(),dto.getLimit());
LambdaQueryWrapper<Admin> lambdaQueryWrapper = new LambdaQueryWrapper<>();
//根据前端是否传递了查询添加生成条件查询对象
lambdaQueryWrapper.like(!StringUtils.isEmpty(dto.getName()),Admin::getName,dto.getName())
.eq(!StringUtils.isEmpty(dto.getUsername()),Admin::getUsername,dto.getUsername())
.eq(!StringUtils.isEmpty(dto.getPhone()),Admin::getPhone,dto.getPhone());
//进行查询
List<Admin> list = adminService.list(lambdaQueryWrapper);
//获取分页信息
PageInfo<Admin> adminPageInfo = new PageInfo<>(list);
return ResultUtils.buildSuccess(list,adminPageInfo.getTotal());
}
7.1 添加管理员
7.1.1 添加前后端校验用户名是否已存在及密码加密
引入加密依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.9</version>
</dependency>
service接口
public interface AdminService extends IService<Admin> {
/**
* 添加管理员
* @param dto
* @return
*/
Boolean insertAdmin(AdminAddUpdateDto dto);
}
service实现
@Service
@Slf4j //生成日志对象log
public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements AdminService {
@Override
public Boolean insertAdmin(AdminAddUpdateDto dto) {
//判断用户名,邮箱,电话是否重复
checkUserName(dto.getUsername());
checkEmail(dto.getEmail());
checkPhone(dto.getPhone());
//拷贝数据到entity
Admin admin = new Admin();
BeanUtils.copyProperties(dto,admin);
//生成言
String salt = UUID.randomUUID().toString();
//密码加密
Digester md5 = new Digester(DigestAlgorithm.MD5);
String mdPwd = md5.digestHex(admin.getPassword()+salt);
//存储言和加密后密码
admin.setPassword(mdPwd);
admin.setSalt(salt);
//保存到数据库,调用ServiceImpl中的save
return this.save(admin);
}
//根据用户名查询信息,若查询成功证明已经存在
private void checkUserName(String username) {
LambdaQueryWrapper<Admin> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(Admin::getUsername,username);
if(getBaseMapper().selectOne(lambdaQueryWrapper)!=null){
log.info("用户名已存在{}",username);
throw new CustomException("用户名已存在");
}
}
//根据邮箱查询信息,若查询成功证明已经存在
private void checkEmail(String email) {
LambdaQueryWrapper<Admin> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(Admin::getEmail,email);
if(getBaseMapper().selectOne(lambdaQueryWrapper)!=null){
log.info("邮箱已存在{}",email);
throw new CustomException("邮箱已存在");
}
}
//根据电话查询信息,若查询成功证明已经存在
private void checkPhone(String phone) {
LambdaQueryWrapper<Admin> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(Admin::getPhone,phone);
if(getBaseMapper().selectOne(lambdaQueryWrapper)!=null){
log.info("电话已存在{}",phone);
throw new CustomException("电话已存在");
}
}
}
7.1.2 校验
使用springmvc的校验
导入依赖
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.7.Final</version>
</dependency>
常用注解
常用在@Pattern中的正则
一、校验数字的表达式
1 数字:^[0-9]*$
2 n位的数字:^\d{n}$
3 至少n位的数字:^\d{n,}$
4 m-n位的数字:^\d{m,n}$
5 零和非零开头的数字:^(0|[1-9][0-9]*)$
6 非零开头的最多带两位小数的数字:^([1-9][0-9]*)+(.[0-9]{1,2})?$
7 带1-2位小数的正数或负数:^(\-)?\d+(\.\d{1,2})?$
8 正数、负数、和小数:^(\-|\+)?\d+(\.\d+)?$
9 有两位小数的正实数:^[0-9]+(.[0-9]{2})?$
10 有1~3位小数的正实数:^[0-9]+(.[0-9]{1,3})?$
11 非零的正整数:^[1-9]\d*$ 或 ^([1-9][0-9]*){1,3}$ 或 ^\+?[1-9][0-9]*$
12 非零的负整数:^\-[1-9][]0-9"*$ 或 ^-[1-9]\d*$
13 非负整数:^\d+$ 或 ^[1-9]\d*|0$
14 非正整数:^-[1-9]\d*|0$ 或 ^((-\d+)|(0+))$
15 非负浮点数:^\d+(\.\d+)?$ 或 ^[1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0$
16 非正浮点数:^((-\d+(\.\d+)?)|(0+(\.0+)?))$ 或 ^(-([1-9]\d*\.\d*|0\.\d*[1-9]\d*))|0?\.0+|0$
17 正浮点数:^[1-9]\d*\.\d*|0\.\d*[1-9]\d*$ 或 ^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$
18 负浮点数:^-([1-9]\d*\.\d*|0\.\d*[1-9]\d*)$ 或 ^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$
19 浮点数:^(-?\d+)(\.\d+)?$ 或 ^-?([1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0)$
二、校验字符的表达式
1 汉字:^[\u4e00-\u9fa5]{0,}$
2 英文和数字:^[A-Za-z0-9]+$ 或 ^[A-Za-z0-9]{4,40}$
3 长度为3-20的所有字符:^.{3,20}$
4 由26个英文字母组成的字符串:^[A-Za-z]+$
5 由26个大写英文字母组成的字符串:^[A-Z]+$
6 由26个小写英文字母组成的字符串:^[a-z]+$
7 由数字和26个英文字母组成的字符串:^[A-Za-z0-9]+$
8 由数字、26个英文字母或者下划线组成的字符串:^\w+$ 或 ^\w{3,20}$
9 中文、英文、数字包括下划线:^[\u4E00-\u9FA5A-Za-z0-9_]+$
10 中文、英文、数字但不包括下划线等符号:^[\u4E00-\u9FA5A-Za-z0-9]+$ 或 ^[\u4E00-\u9FA5A-Za-z0-9]{2,20}$
11 可以输入含有^%&',;=?$\"等字符:[^%&',;=?$\x22]+
12 禁止输入含有~的字符:[^~\x22]+
三、特殊需求表达式
验证Email
^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$
email地址,格式:zhangsan@zuidaima.com,zhangsan@xxx.com.cn ----- "\\w+@\\w+\\.[a-z]+(\\.[a-z]+)?"
IP地址
\d+\.\d+\.\d+\.\d+ (提取IP地址时有用)
域名
[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/.?
中国邮政编码
[1-9]\d{5}(?!\d) (中国邮政编码为6位数字)
验证身份证号码
居民身份证号码15位或18位,最后一位可能是数字或字母
手机号码
^((13[0-9])|(19[1-9])|(15[^4,\\D])|(18[0,5-9]))\\d{8}$
"[1-9]\\d{13,16}[a-zA-Z0-9]{1}"
15或18位身份证:^\d{15}|\d{18}$
15位身份证:^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$
18位身份证:^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{4}$
封装校验类
@Slf4j
public class WebUtils {
public static Result getResult(BindingResult result) {
log.info("校验失败");
//获取所有错误信息
List<ObjectError> allErrors = result.getAllErrors();
StringBuilder sb = new StringBuilder();
for(int i = 0;i < allErrors.size();i++){
sb.append(allErrors.get(i).getDefaultMessage()+",");
}
log.info("失败原因:{}",sb.toString());
return ResultUtils.buildFailed(50000, sb.toString());
}
}
定义添加校验字段注解的dto
@Data
@ApiModel("管理员dto")
public class AdminAddUpdateDto implements Serializable {
@ApiModelProperty("管理员姓名")
private String name;
@ApiModelProperty("拥有角色")
private String role;
@Pattern(regexp = "^((13[0-9])|(19[1-9])|(15[^4,\\D])|(18[0,5-9]))\\d{8}$",message = "手机号不合法")
@ApiModelProperty("电话")
private String phone;
@ApiModelProperty("邮箱")
@Email(message = "邮箱不合法")
private String email;
@NotBlank(message = "密码不能为空")
@ApiModelProperty("密码")
private String password;
@NotBlank(message = "头像不能为空")
@ApiModelProperty("头像")
private String avatar;
@Pattern(regexp = "^\\w{3,20}$",message = "用户名不合法")
@ApiModelProperty("用户名")
private String username;
}
controller中调用
@ApiOperation("管理员添加")
@RequestMapping(value = "/add",method = RequestMethod.POST)
//使用@Validated注解就能对后面的dto进行校验,dto中有使用校验注解的字段就会进行校验
public Result add(@RequestBody @Validated AdminAddUpdateDto dto, BindingResult result){
//判断校验是否通过,BindingResult是使用校验后的返回结果
if(result.hasErrors()) {
//当出现错误时调用上面定义的WebUtils工具类中的获取错误信息的方法
return WebUtils.getResult(result);
}
//校验通过后调用service添加
boolean b = adminService.insertAdmin(dto);
return ResultUtils.judge(b);
}
接口测试
当传入的数据格式不正确时
8. 分布式session
8.1 jwt方案
属于无状态的登录,服务器端不存储登录信息,客户端存储登录状态的技术,保证登录状态不能被篡改
jwt工作流程
8.1.1 token与cookie相比的优点
-
token支持跨域访问,cookie不支持跨域访问
-
无状态化,服务器无需存储token,只需要验证token信息是否正确,session需要在服务器存储,且通过cookie中的sessionID在服务器端查找对应的session
-
更适用于移动端,如小程序等,这种平台不支持cookie,因为每次请求都是不同的会话
8.1.2 jwt组成
一个jwt就是个token字符串,由头部,载荷,签名组成
头部(header):声明jwt最基本的信息,如该jwt签名部分用的加密算法,头部的信息会使用BASE64算法进行编码
如{“typ”:“JWT”,“alg”:“HS256”}
载荷(payload):存放有效信息的部分,一般用于存放用户的用户名和与业务相关的信息,但不包括敏感信息,因为这部分也使用BASE64进行编码,客户端可以进行解密
如{“sub”:“1234567890”,“name”:“John Doe”,“admin”:true}
签名:由header(base64后)+payload(base64后)+secret(密钥)三部分组合,再将组合的结果利用头部声明的加密算法进行加密。其中secret是存放在服务器端的不对外公开的密钥
8.1.3 token作用的全流程
-
客户端发起登录请求,先后验证用户名是否存在,用户名加盐加密后密码是否和数据库对应密码一致,通过登录验证后服务器准备生成token
-
将后续业务可能会用到的非隐私数据放入到map中作为token的载荷
-
调用JWTUtil.createToken方法,传入载荷map及服务器端密钥secret,得到对应的token
-
将token返回给客户端,由客户端进行存储
-
后续每次都会携带token进行请求,由服务器端拦截所有除了访问登录的所有请求,检查是否携带token,没有携带token时转到登录,携带token时调用JWT.of(token).setKey(SECRET.getBytes()).verify()进行签名验证,secret为后端保存的密钥
-
验证通过时使用JWT jwt = JWT.of(token)从token中获取载荷数据,并将其放入当前线程中,方便后续业务使用
8.1.4 管理员登录
下面通过管理员登录实现在服务器生成token并返沪给客户端
编写专门用于登录的dto
@Data
@ApiModel("管理员登录dto")
public class AdminLoginDto implements Serializable {
@NotBlank(message = "用户名不能为空")
String username;
@NotBlank(message = "密码不能为空")
String pwd;
}
登录service接口
String login(AdminLoginDto dto);
编写Token相关工具类
public class TokenUtils {
//token加密密钥
static final String SECRET="sajkbdkjwqkjhksdiou2309kjlkajsd";
/**
* 生成jwt的token
* @param map
* @return
*/
public static String createToken(Map<String,Object> map){
//设置token过期时间为2小时
map.put("expire_time",System.currentTimeMillis()+1000*60*60*2);
String token = JWTUtil.createToken(map, SECRET.getBytes());
return token;
}
/**
* 验证token
* @param token
* @return
*/
public static Boolean checkToken(String token){
return JWT.of(token).setKey(SECRET.getBytes()).verify();
}
}
登录service实现
@Override
public String login(AdminLoginDto dto) {
//根据用户名查询用户
LambdaQueryWrapper<Admin> lambdaQueryWrapper = new LambdaQueryWrapper<>();
//这里的.eq(Admin::getUsername,dto.getUsername())等价于新建一个Admin对象,将Admin对象的username字段赋值为dto.getUsername()的值
lambdaQueryWrapper.eq(Admin::getUsername,dto.getUsername());
Admin admin = getBaseMapper().selectOne(lambdaQueryWrapper);
if(admin == null){
log.info("用户名不存在{}",dto.getUsername());
throw new CustomException("用户名或密码错误");
}
//获取言
String salt = admin.getSalt();
//再次加盐加密
Digester digester = new Digester(DigestAlgorithm.MD5);
String md5pwd = digester.digestHex(dto.getPwd()+salt);
//加密后和数据库中密码对比
if(!admin.getPassword().equals(md5pwd)){
log.info("密码错误{}",dto.getPwd());
throw new CustomException("用户名或密码错误");
}
//准备载荷数据
Map<String,Object> map = new HashMap<>();
map.put("id",admin.getId());
map.put("username",admin.getUsername());
map.put("name",admin.getName());
map.put("phone",admin.getPhone());
map.put("avatar",admin.getAvatar());
//生成token并返回
String token = TokenUtils.createToken(map);
return token;
}
controller
@ApiOperation("管理员登录")
@RequestMapping(value = "/login",method = RequestMethod.POST)
public Result add(@RequestBody @Validated AdminLoginDto dto, BindingResult result){
//判断校验是否通过,BindingResult是使用校验后的返回结果
if(result.hasErrors()) {
//当客户端传来的AdminLoginDto校验不通过时,返回错误信息
return WebUtils.getResult(result);
}
//调用service验证登录信息
String token = adminService.login(dto);
//返回token给客户端
return ResultUtils.buildSuccess(token);
}
测试
8.1.5 认证
用户每次请求非登录接口时,先判断是否携带token,且token信息是否正确
线程工具类,用于隐式传参,将合法token中的载荷取出并放入线程中,后续业务需要时直接取出
public class AdminThreadLocal {
static ThreadLocal<Admin> threadLocal = new ThreadLocal<>();
/**
* 绑定管理员到当前线程
*/
public static void setAdmin(Admin admin){
threadLocal.set(admin);
}
/**
* 获取当前线程绑定的管理员
* @return
*/
public static Admin get(){
return threadLocal.get();
}
/**
* 移出当前线程绑定的管理员
*/
public static void remove(){
threadLocal.remove();
}
}
拦截器,用于拦截除了访问登录接口和swagger的所有请求
@Slf4j
//实现认证的拦截器
public class AuthInterceptor implements HandlerInterceptor {
@Override
//处理器之前调用
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.debug("认证拦截器被调用了");
//获取jwt令牌
String token = request.getHeader("token");
if(token == null || token.equals("null")){
token = request.getParameter("token");
if(token==null || token.equals("null")){
log.debug("没有携带token:{}",request.getRemoteAddr());
String json = JsonUtils.objectToJson(ResultUtils.buildFailed(20003, "没有携带token"));
ResponseUtils.responseToJson(json,response);
return false;
}
}
//验签
if(!TokenUtils.checkToken(token)){
log.debug("验签失败{}",token);
String json = JsonUtils.objectToJson(ResultUtils.buildFailed(20003,"无效token"));
ResponseUtils.responseToJson(json,response);
return false;
}
//获取载荷数据
JWT jwt = JWT.of(token);
Admin admin = new Admin();
admin.setId((Integer) jwt.getPayload("id"));
admin.setUsername((String) jwt.getPayload("username"));
admin.setName((String) jwt.getPayload("name"));
admin.setPhone((String) jwt.getPayload("phone"));
admin.setAvatar((String) jwt.getPayload("avatar"));
//绑定载荷数据到当前线程
AdminThreadLocal.setAdmin(admin);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
AdminThreadLocal.remove();
}
}
springmvc.xml中配置拦截器
<!--配置拦截器-->
<mvc:interceptors>
<mvc:interceptor>
<!--配置拦截路径-->
<mvc:mapping path="/**"/>
<!--配置不拦截登录-->
<mvc:exclude-mapping path="/admin/login"/>
<!--配置拦截器不拦截swagger资源-->
<mvc:exclude-mapping path="/swagger-resources/**"/>
<mvc:exclude-mapping path="/webjars/**"/>
<mvc:exclude-mapping path="/v2/**"/>
<mvc:exclude-mapping path="/swagger-ui.html/**"/>
<bean class="com.furong.admin.interceptor.AuthInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
测试不携带token请求
测试携带token
8.1.6 前端认证处理
使用localStorage和sessionStorage,local会把数据永久保存,session浏览器关闭后清除
存储token,这里result.data就是后端登录接口返回的token字符串
发起除登录外所有ajax请求前携带token
退出登录时清除token
9、权限
认证:判断客户端请求是否有资格访问后端接口
权限:不同账号拥有不同接口的访问权限,查询请求是否有访问接口的权限
实现权限也需要一个拦截器,用于判断请求是否有访问资源的权限,若没有对应权限则拦截
9.1 权限表设计
RBAC权限表设计结构
9.2 针对permission表和role表多对多关系的三表联合查询
需求:根据表中每个角色id到角色权限关联表中查询其拥有的权限id,再根据权限id到权限表中查询对应权限名,将其所拥有的所有权限名以逗号连接放到表中进行显示
9.2.1 编写sql
SELECT
role.id,
role.name,
GROUP_CONCAT(permission.name) as pName,
role.description
FROM
fr_role role
LEFT JOIN fr_role_permission rolePermission ON role.id=rolePermission.role_id
LEFT JOIN fr_permission permission ON permission.id=rolePermission.permission_id
WHERE
role.name LIKE CONCAT('%','主','%')
GROUP BY
role.id
9.2.2 编写条件查询dto实体类
只能根据角色名模糊查询
@Data
@ApiModel("角色查询dto")
public class RoleQueryDto extends BasePageDto {
@ApiModelProperty("角色名")
String name;
}
9.2.3 编写多表查询结果vo实体类
@Data
public class RoleListVo {
Integer id;
String name;
String description;
//当前角色拥有的权限名
String pName;
}
9.2.4 编写mapper接口
public interface RoleMapper extends BaseMapper<Role> {
/**
* 根据角色名模糊查询角色
* @param dto
* @return
*/
List<RoleListVo> findRoleByQueryDto(RoleQueryDto dto);
}
9.2.5 编写mapper的xml文件
<mapper namespace="com.furong.admin.mapper.RoleMapper">
<select id="findRoleByQueryDto" resultType="com.furong.pojo.vo.RoleListVo" parameterType="com.furong.pojo.dto.RoleQueryDto">
SELECT
role.id,
role.name,
GROUP_CONCAT(permission.name) as pName,
role.description
FROM
fr_role role
LEFT JOIN fr_role_permission rolePermission ON role.id=rolePermission.role_id
LEFT JOIN fr_permission permission ON permission.id=rolePermission.permission_id
<where>
<if test="name!=null and name !=''">
role.name LIKE CONCAT('%',#{name},'%')
</if>
</where>
GROUP BY
role.id
</select>
</mapper>
9.2.6 编写service接口
public interface RoleService extends IService<Role> {
List<RoleListVo> findRoleByQueryDto(RoleQueryDto dto);
}
9.2.7 编写service实现类
@Override
public List<RoleListVo> findRoleByQueryDto(RoleQueryDto dto) {
return getBaseMapper().findRoleByQueryDto(dto);
}
9.2.8 编写controller接口
@RequestMapping(value = "/list",method = RequestMethod.GET)
@ApiOperation("待条件查询权限")
public Result list(RoleQueryDto dto){
//组装分页条件
PageHelper.startPage(dto.getPage(),dto.getLimit());
//进行查询
List<RoleListVo> list = roleService.findRoleByQueryDto(dto);
//获取分页信息
PageInfo<RoleListVo> rolePageInfo = new PageInfo<>(list);
return ResultUtils.buildSuccess(list,rolePageInfo.getTotal());
}
测试成功显示结果
9.3 针对permission表和role表多对多关系的三表联合增删改
需求:当需要对role表角色进行增加或修改时(包括为角色增加权限或减少权限)时,实际应该在中间表role_permission中进行增加或删除
9.3.1 添加角色
针对添加角色来说,如果添加时有选择的权限,则将新增角色id和所选择的全部权限id添加到role_permission表中;
9.3.1.1 编写中间表role_permission的mapper
public interface RolePermissionMapper extends BaseMapper<RolePermission> {
}
9.3.1.2 编写role表的service接口
public interface RoleService extends IService<Role> {
Boolean addRole(RoleAddDto dto);
}
9.3.1.3 编写role表service实现
@Override
public Boolean addRole(RoleAddDto dto) {
//调用mapper先进行role表的单表添加
Role role = new Role();
BeanUtils.copyProperties(dto,role);
int row = getBaseMapper().insert(role);
//当前端没有选择任何权限时,就不用向中间表进行添加,直接返回
if(dto.getPermissionIds().equals(""))
return true;
//前端返回的权限信息是字符串格式的权限id,每个id间用逗号分隔
String[] rowIds = dto.getPermissionIds().split(",");
List<Integer> permissionId = new ArrayList<>();
//取得Integer格式的所有权限id
for(int i = 0;i <rowIds.length;i++){
permissionId.add(Integer.parseInt(rowIds[i]));
}
RolePermission rolePermission = new RolePermission();
rolePermission.setRoleId(role.getId());
for(int i = 0;i < permissionId.size();i++){
rolePermission.setPermissionId(permissionId.get(i));
//这里的rolePermissionMapper作为类中对象在前面进行了注入,直接循环调用insert方法循环插入
rolePermissionMapper.insert(rolePermission);
}
return true;
}
9.3.1.4 编写controller接口
@ApiOperation("添加角色")
@RequestMapping(value = "/add",method = RequestMethod.POST)
public Result add(@RequestBody RoleAddDto dto){
boolean b = roleService.addRole(dto);
return ResultUtils.judge(b);
}
9.3.2 修改角色
针对修改角色来说,每次提交修改请求时前端将记录所选择的权限传回后端,后端先根据修改用户的id删除所有role_permission表中对应的记录,再添加用户所选择的权限即可
9.3.2.1 编写修改角色dto
@Data
@ApiModel("角色修改dto")
public class RoleUpdateDto {
@ApiModelProperty("角色id")
Integer id;
@ApiModelProperty("角色名")
String name;
@ApiModelProperty("角色描述")
String description;
@ApiModelProperty("角色拥有的权限")
String permissionIds;
}
9.3.2.2 编写service接口
public interface RoleService extends IService<Role> {
Boolean updateRole(RoleUpdateDto dto);
}
9.3.2.3 编写service实现
@Override
public Boolean updateRole(RoleUpdateDto dto){
//先修改角色单表中的信息
Role role = new Role();
BeanUtils.copyProperties(dto,role);
getBaseMapper().updateById(role);
//再根据角色id删除角色权限中间表中角色对应的所有权限
LambdaQueryWrapper<RolePermission> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(RolePermission::getRoleId,dto.getId());
rolePermissionMapper.delete(lambdaQueryWrapper);
//当前端选择的权限不为空时才进行中间表的插入
if(dto.getPermissionIds() == "")
return true;
String[] stringIds = dto.getPermissionIds().split(",");
List<Integer> integerIds = new ArrayList<>();
//获取更新的权限id集合
for(String stringId:stringIds){
integerIds.add(Integer.parseInt(stringId));
}
if(integerIds.size() > 0){
//循环向角色权限中间表插入数据
for(Integer permissionId:integerIds){
RolePermission rolePermission = new RolePermission();
rolePermission.setRoleId(dto.getId());
rolePermission.setPermissionId(permissionId);
rolePermissionMapper.insert(rolePermission);
}
}
return true;
}
9.3.2.4 编写controller接口
@RequestMapping(value = "/update",method = RequestMethod.POST)
@ApiOperation("修改角色")
public Result update(@RequestBody RoleUpdateDto dto){
//前端将选择的权限的id拼接成字符串permissionIds返回给后端
boolean b = roleService.updateRole(dto);
return ResultUtils.judge(b);
}
10. EasyExcel
10.1 导入依赖
<!-- execl -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.0.5</version>
<exclusions>
<exclusion>
<artifactId>commons-codec</artifactId>
<groupId>commons-codec</groupId>
</exclusion>
<exclusion>
<artifactId>slf4j-api</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
10.2 编写导入导出实体类
@Data
public class PermissionData implements Serializable {
//注解为导出的列名
@ExcelProperty("权限名")
String name;
@ExcelProperty("权限uri")
String uri;
@ExcelProperty("权限标记")
String tag;
}
10.3 实现导出excel
编写controller接口
@RequestMapping(value = "/download",method = RequestMethod.GET)
public void download(HttpServletResponse response) throws IOException {
// 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("权限数据", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
//查询所有权限
List<Permission> permissionList = permissionService.list();
//2.把List<Permission>拷贝到 List<PermissionData>集合中
List<PermissionData> permissionDataList=new ArrayList<>();
for(Permission permission:permissionList){
PermissionData permissionData=new PermissionData();
BeanUtils.copyProperties(permission,permissionData);
permissionDataList.add(permissionData);
}
EasyExcel.write(response.getOutputStream(), PermissionData.class).sheet("权限").doWrite(permissionDataList);
}
10.3 实现导入excel
下载模板
定义导入监听器
11. 权限鉴定
11. 1 权限鉴定的实现(基于uri绑定)
基于拦截器+uri
方法1.将用户权限信息写入到jwt令牌中,由客户端保存,但当权限很多时,jwt中可能存不下,这时就要存入redis中
方法2.存入redis中
方法1思路:用户登陆成功后,为用户生成载荷时,先根据用户名查询用户所拥有的所有权限的uri地址,将所有uri权限地址组合成一个json字符串,并将这个字符串放入token的载荷中返回给客户端,客户端在发起后续请求时,先在登录状态拦截器中取出这个载荷中的uri地址字符串,并将其绑定到ThreadLocal当前线程中,然后在uri权限拦截器中直接从当前线程中取出这个字符串,看字符串中是否包含用户想要访问的uri,若包含则放行,否则拦截,下面是方法1具体实现
编写sql,根据管理员用户名查询其所有的权限uri
--使用DISTINCT保证不会查询出多条同样的结果
SELECT DISTINCT
permission.id,
permission.NAME,
permission.uri
FROM
fr_admin admin,
fr_admin_role adminRole,
fr_role_permission rolePermission,
fr_permission permission
WHERE
admin.id = adminRole.adnin_id
AND adminRole.role_id = rolePermission.role_id
AND rolePermission.permission_id = permission.id
--这里的adminName是动态获取的
AND admin.username = 'adminName'
查询过程:
编写mapper接口,使用注解的方式进行sql映射
public interface PermissionMapper extends BaseMapper<Permission> {
/**
* 根据用户名查询用户权限
* @param username
* @return
*/
@Select("SELECT DISTINCT permission.id,permission.name,permission.uri FROM fr_admin admin,fr_admin_role adminRole,fr_role_permission rolePermission,fr_permission permission WHERE admin.id=adminRole.adnin_id AND adminRole.role_id = rolePermission.role_id AND rolePermission.permission_id=permission.id AND admin.username=#{username}")
List<Permission> findPermissionByUsername(String username);
}
编写service接口和实现
public interface PermissionService extends IService<Permission> {
List<Permission> findPermissionByUsername(String username);
}
@Service
@Slf4j //生成日志对象log
public class PermissionServiceImpl extends ServiceImpl<PermissionMapper, Permission> implements PermissionService {
@Override
public List<Permission> findPermissionByUsername(String username) {
return getBaseMapper().findPermissionByUsername(username);
}
}
在用户登录的service层实现方法中查询用户所有的uri权限,并放入载荷中
@Override
public String login(AdminLoginDto dto) {
//code。。。。
//查询用户权限存放到token的载荷中,后期可以存储到redis中
List<Permission> permissionList = permissionService.findPermissionByUsername(admin.getUsername());
List<String> permissionUris = permissionList.stream().map(permission -> permission.getUri()).collect(Collectors.toList());
//重新分配权限后用户要重新登录,更新token中的权限uri,才能生效
map.put("permissionUris", JsonUtils.objectToJson(permissionUris));
//生成token并返回
String token = TokenUtils.createToken(map);
return token;
{
线程工具类
public class AdminThreadLocal {
static ThreadLocal<Admin> threadLocal = new ThreadLocal<>();
/**
* 绑定管理员到当前线程
*/
public static void setAdmin(Admin admin){
threadLocal.set(admin);
}
/**
* 获取当前线程绑定的管理员
* @return
*/
public static Admin get(){
return threadLocal.get();
}
/**
* 移出当前线程绑定的管理员
*/
public static void remove(){
threadLocal.remove();
}
}
在登录状态拦截器中取出载荷中的权限uri并放入当前线程中
@Slf4j
//实现登录状态验证的拦截器
public class AuthInterceptor implements HandlerInterceptor {
@Override
//处理器之前调用
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//code。。。
//获取载荷数据
JWT jwt = JWT.of(token);
Admin admin = new Admin();
//获取载荷中权限uri内容
admin.setPermissionUris((String) jwt.getPayload("permissionUris"));
//绑定载荷数据到当前线程
AdminThreadLocal.setAdmin(admin);
return true;
}
//code。。。
}
当用户发起后续请求时,在权限鉴定拦截器中取出当前线程中的uri,并判断请求的uri是否包含在其中
@Slf4j
public class PermissionInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取当前用户访问的uri
//如 /项目名/doctor/list
String requestURI = request.getRequestURI();
//使用hutool的方法去掉用户请求uri前缀的项目名
requestURI = StrUtil.removePrefix(requestURI,request.getContextPath());
//超级管理员直接放行
if(AdminThreadLocal.get().getUsername().equals("admin")){
log.debug("超级管理员放行");
return true;
}
//查询用户拥有的权限,从当前线程获取用户权限信息
String permissionUris = AdminThreadLocal.get().getPermissionUris();
//判断用户拥有的权限是否包含用户当前访问的uri
Boolean isOk = false;
if(permissionUris.contains(requestURI)){
isOk = true;
}
if(!isOk){
log.debug("用户没有对应权限{}",requestURI);
String json = JsonUtils.objectToJson(ResultUtils.buildFailed(ResultEnum.NO_PERMISSION.getCode(), ResultEnum.NO_PERMISSION.getMessage()));
ResponseUtils.responseToJson(json,response);
return false;
}
return true;
}
//code。。。
}
为用户分配/doctor/list接口访问权限并测试访问,能够成功访问
11. 2 实现权限鉴定(基于注解)
使用自定义注解+aop
定义自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Permission {
String value() default "";
}
使用权限注解,在controller接口上使用
//当访问该注解下接口时先获得该注解中的权限tag。并将其与当前用户拥有的权限tag比较,若包含当前接口的权限tag则放行,否则拦截,
//当方法上没有该注解时代表该方法不用权限鉴定
@Permission("doctor:list")
@RequestMapping(value = "/list",method = RequestMethod.GET)
public Result list(DoctorQueryDto dto){
//code。。。
}
改造登录接口,登陆成功后根据用户名查询其所有的权限tags并放入token中
@Override
public String login(AdminLoginDto dto) {
//根据用户名查询用户
LambdaQueryWrapper<Admin> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(Admin::getUsername,dto.getUsername());
Admin admin = getBaseMapper().selectOne(lambdaQueryWrapper);
if(admin == null){
log.info("用户名不存在{}",dto.getUsername());
throw new CustomException("用户名或密码错误");
}
//获取言
String salt = admin.getSalt();
//再次加盐加密
Digester digester = new Digester(DigestAlgorithm.MD5);
String md5pwd = digester.digestHex(dto.getPwd()+salt);
//加密后和数据库中密码对比
if(!admin.getPassword().equals(md5pwd)){
log.info("密码错误{}",dto.getPwd());
throw new CustomException("用户名或密码错误");
}
//准备载荷数据
Map<String,Object> map = new HashMap<>();
map.put("id",admin.getId());
map.put("username",admin.getUsername());
map.put("name",admin.getName());
map.put("phone",admin.getPhone());
map.put("avatar",admin.getAvatar());
//根据用户名查询用户权限tag存放到token的载荷中,后期可以存储到redis中
List<Permission> permissionList = permissionService.findPermissionByUsername(admin.getUsername());
List<String> permissionUris = permissionList.stream().map(permission -> permission.getTag()).collect(Collectors.toList());
//将权限tags转换为json字符串后放入载荷中
map.put("tags", JsonUtils.objectToJson(permissionUris));
//生成token并返回,由于权限相关信息存放在token中,所以当为用户重新分配权限后,需要对应重新登录,生成新的token才能正常使用新权限
String token = TokenUtils.createToken(map);
return token;
}
重点:
改造Admin实体添加权限tags字段
@Data
@TableName("fr_admin") //指定该entity对应的表名
public class Admin {
@TableId(type = IdType.AUTO)
private Integer id;
private String name;
private String phone;
private String email;
private Date createTime;
private Date updateTime;
private String password;
private String avatar;
private String username;
@TableLogic
private Integer isDeleted;
private String salt;
//不是数据库表中的字段
@TableField(exist = false)
private String permissionUris;
@TableField(exist = false)
private String tags;
}
改造登录认证拦截器,在每次请求时将请求中的token中的权限tags取出并绑定到当前线程中
@Slf4j
//实现登录认证的拦截器
public class AuthInterceptor implements HandlerInterceptor {
@Override
//处理器之前调用
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.debug("认证拦截器被调用了");
//获取jwt令牌
String token = request.getHeader("token");
if(token == null || token.equals("null")){
token = request.getParameter("token");
if(token==null || token.equals("null")){
log.debug("没有携带token:{}",request.getRemoteAddr());
String json = JsonUtils.objectToJson(ResultUtils.buildFailed(20003, "没有携带token"));
ResponseUtils.responseToJson(json,response);
return false;
}
}
//验签
if(!TokenUtils.checkToken(token)){
log.debug("验签失败{}",token);
String json = JsonUtils.objectToJson(ResultUtils.buildFailed(20003,"无效token"));
ResponseUtils.responseToJson(json,response);
return false;
}
//获取载荷数据
JWT jwt = JWT.of(token);
Admin admin = new Admin();
admin.setId((Integer) jwt.getPayload("id"));
admin.setUsername((String) jwt.getPayload("username"));
admin.setName((String) jwt.getPayload("name"));
admin.setPhone((String) jwt.getPayload("phone"));
admin.setAvatar((String) jwt.getPayload("avatar"));
admin.setTags((String) jwt.getPayload("tags"));
//绑定载荷数据到当前线程
AdminThreadLocal.setAdmin(admin);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
AdminThreadLocal.remove();
}
}
编写aop权限鉴定通知,其中从线程中获取用户所有权限tags,并判断其中是否包含访问的controller上Permission注解中的权限tag
@Aspect//切面
@Slf4j
public class PermissionAdvice {
//需要放行,所以用环绕通知,拦截所有controller所有方法
@Around("execution(* com.furong.admin.controller.*Controller.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//获取目标方法上是否有权限鉴定注解,如果有注解则代表需要权限鉴定,没有则代表不需要权限鉴定,直接放行
//通过jointPoint获取目标方法
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
Method method = methodSignature.getMethod();
//获取目标方法上的注解
Permission permission = method.getAnnotation(Permission.class);
//若没有权限注解则直接放行
if(permission == null){
log.debug("{}方法不需要权限鉴定",method.getName());
return joinPoint.proceed();
}
//如果当前访问用户是超级管理员直接放行
if(AdminThreadLocal.get().getUsername().equals("admin")){
log.debug("超级管理员直接放行");
return joinPoint.proceed();
}
//获取permission注解中标记的权限,如得到的是doctor:list
String tag = permission.value();
//查询用户拥有的所有权限标记,从当前线程获取用户的所有权限标记
String tags = AdminThreadLocal.get().getTags();
List<String> tagsString = JsonUtils.jsonToList(tags, String.class);
//权限标志
Boolean isOk = false;
for(String t:tagsString){
if(t.equals(tag)){
isOk = true;
break;
}
}
//不包含权限
if(!isOk){
log.debug("没有{}权限",tag);
throw new CustomException(ResultEnum.NO_PERMISSION);
}
//有权限直接放行
log.debug("有权限,放行");
return joinPoint.proceed();
}
}
配置aop
springmvc.xml
<!--配置开启注解权限鉴定aop-->
<aop:aspectj-autoproxy/>
<!--配置通知-->
<bean class="com.furong.admin.permission.PermissionAdvice"></bean>
12. 排班信息
13.轮播管理
轮播管理使用远程服务器上的mongodb数据库进行操作
13.1 整合mongodb
导入依赖
<!--MongoDB驱动包-->
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
<version>3.1.0</version>
</dependency>
<!--MongoDB核心包-->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
<version>1.8.2.RELEASE</version>
</dependency>
编写applicationContext-mongodb.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:mongo="http://www.springframework.org/schema/data/mongo"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/data/mongo http://www.springframework.org/schema/data/mongo/spring-mongo.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
<!--服务器连接信息,host为远程服务器地址,credentials中分别为访问数据库的账户,密码和数据库名-->
<mongo:mongo-client host="101.43.251.178" port="27017" credentials="gxa:gxa123456@hospital">
</mongo:mongo-client>
<!--创建mongoTemplate模板-->
<bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
<constructor-arg ref="mongo"/>
<!-- value设置对哪个数据库进行操作-->
<constructor-arg name="databaseName" value="hospital"/>
</bean>
<!--对后面编写的轮播图BannerDao进行ioc-->
<bean class="com.furong.admin.dao.impl.BannerDaoImpl"/>
</beans>
在主配置文件applicationContext.xml中引入applicationContext-mongodb.xm
<import resource="applicationContext-mongodb.xml"/>
13.2 编写entity
编写entity
@Data
//spring针对使用mongodb的注解
@Document(collection = "fr_banner")
public class Banner implements Serializable {
@Id //标记主键
String id;
@Field("title") //配置表字段和实体映射关系,一致时不用配
String title;
//图片地址
String image;
//跳转连接
String url;
//广告位置
Integer position;
Date createTime;
//首页顶部
public static final Integer INDEX_TOP=0;
//首页中部
public static final Integer INDEX_CENTER=0;
}
编写后台管理页面的查询dto
@Data
public class BannerQueryDto extends BasePageDto{
/**
* 广告标题
*/
String title;
/**
* 广告位置
*/
Integer position;
}
13.2 编写dao接口
public interface BannerDao {
/**
* 插入广告
*/
void insertBanner(Banner banner);
/**
* 修改广告
*/
void updateBanner(Banner banner);
/**
* 删除广告
*/
void deleteBannerById(String id);
/**
* 根据id查询广告
*/
Banner findBannerById(String id);
/**
* 后台管理根据条件查询
* @return
*/
List<Banner> findBannerByQueryDto(BannerQueryDto dto);
/**
* 后台管理根据条件查询总记录数
* @param dto
* @return
*/
Long findBannerCountByQueryDto(BannerQueryDto dto);
/**
* 用户端根据位置查询指定轮播图
* @param position
* @return
*/
List<Banner> findBannerByPosition(Integer position);
}
13.3 编写dao接口实现类
public class BannerDaoImpl implements BannerDao {
@Autowired //注入spring容器中管理的mongodbTemplate
MongoTemplate mongoTemplate;
@Override
public void insertBanner(Banner banner) {
mongoTemplate.save(banner);
}
@Override
public void updateBanner(Banner banner) {
//当存在时修改
mongoTemplate.save(banner);
}
@Override
public void deleteBannerById(String id) {
//构建删除条件
Query query = new Query(Criteria.where("id").is(id));
mongoTemplate.remove(query,Banner.class);
}
@Override
public Banner findBannerById(String id) {
return mongoTemplate.findById(id,Banner.class);
}
/**
* 后台管理根据条件进行分页查询
* @param dto
* @return
*/
@Override
public List<Banner> findBannerByQueryDto(BannerQueryDto dto) {
//构建查询条件,动态查询
Query query = new Query();
//判断当前是否携带对应dto查询条件,根据位置或标题进行模糊查询
if(dto.getPosition() != null){
query.addCriteria(Criteria.where("position").is(dto.getPosition()));
}
if(!StringUtils.isEmpty(dto.getTitle())){
//构建mongo中实现模糊查询的正则
Pattern pattern = Pattern.compile("^.*"+dto.getTitle()+".*$", Pattern.CASE_INSENSITIVE);
query.addCriteria(Criteria.where("title").regex(pattern));
}
//设置分页查询开始序号
query.skip((dto.getPage()-1)*dto.getLimit());
//设置分页大小
query.limit(dto.getLimit());
//查询页面数据
List<Banner> bannerList = mongoTemplate.find(query, Banner.class);
return bannerList;
}
/**
* 后台管理根据条件查询匹配的总记录数
* @param dto
* @return
*/
@Override
public Long findBannerCountByQueryDto(BannerQueryDto dto) {
//构建查询条件,动态查询
Query query = new Query();
//判断当前是否携带对应dto查询条件,根据位置或标题进行模糊查询
if(dto.getPosition() != null){
query.addCriteria(Criteria.where("position").is(dto.getPosition()));
}
if(!StringUtils.isEmpty(dto.getTitle())){
Pattern pattern = Pattern.compile("^.*"+dto.getTitle()+".*$", Pattern.CASE_INSENSITIVE);
query.addCriteria(Criteria.where("title").regex(pattern));
}
//查询总记录数
long count = mongoTemplate.count(query, Banner.class);
return count;
}
@Override
public List<Banner> findBannerByPosition(Integer position) {
//构建删除条件
Query query = new Query(Criteria.where("position").is(position));
//执行查询
List<Banner> bannerList = mongoTemplate.find(query, Banner.class);
return bannerList;
}
}
(编写完成后在applicationContext-mongodb.xml中进行ioc)
13.4 测试dao
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring/applicationContext.xml")
public class TestBannerDao {
@Autowired
BannerDao bannerDao;
@Test
public void testInsert(){
for(int i = 0;i < 100;i++){
Banner banner = Banner.builder().title("首页广告"+i).position(1).image("xxx").createTime(new Date()).build();
bannerDao.insertBanner(banner);
}
}
@Test
public void testUpdate(){
Banner banner = Banner.builder().id("642b924077270e265c079655").title("ikun").position(1).image("xxx").build();
bannerDao.updateBanner(banner);;
}
@Test
public void testQuery(){
Banner banner = bannerDao.findBannerById("642b924077270e265c079655");
System.out.println(banner);
}
@Test
public void testDelete(){
bannerDao.deleteBannerById("642b924077270e265c079655");
}
@Test
public void testFindPage(){
BannerQueryDto bannerQueryDto = new BannerQueryDto();
bannerQueryDto.setPage(1);
bannerQueryDto.setTitle("首");
bannerQueryDto.setLimit(5);
bannerQueryDto.setPosition(1);
System.out.println(bannerDao.findBannerByQueryDto(bannerQueryDto));
System.out.println(bannerDao.findBannerCountByQueryDto(bannerQueryDto));
}
@Test
public void testFindByPosition(){
List<Banner> bannerByPosition = bannerDao.findBannerByPosition(2);
System.out.println(bannerByPosition);
}
}
13.5 编写service接口及实现
public interface BannerService {
/**
* 插入广告
*/
void insertBanner(Banner banner);
/**
* 修改广告
*/
void updateBanner(Banner banner);
/**
* 删除广告
*/
void deleteBannerById(String id);
/**
* 根据id查询广告
*/
Banner findBannerById(String id);
/**
* 后台管理根据条件查询
* @return
*/
List<Banner> findBannerByQueryDto(BannerQueryDto dto);
/**
* 后台管理根据条件查询总记录数
* @param dto
* @return
*/
Long findBannerCountByQueryDto(BannerQueryDto dto);
/**
* 用户端根据位置查询指定轮播图
* @param position
* @return
*/
List<Banner> findBannerByPosition(Integer position);
}
@Service
public class BannerServiceImpl implements BannerService {
@Autowired
BannerDao dao;
@Override
public void insertBanner(Banner banner) {
dao.insertBanner(banner);
}
@Override
public void updateBanner(Banner banner) {
dao.updateBanner(banner);
}
@Override
public void deleteBannerById(String id) {
dao.deleteBannerById(id);
}
@Override
public Banner findBannerById(String id) {
return dao.findBannerById(id);
}
@Override
public List<Banner> findBannerByQueryDto(BannerQueryDto dto) {
return dao.findBannerByQueryDto(dto);
}
@Override
public Long findBannerCountByQueryDto(BannerQueryDto dto) {
return dao.findBannerCountByQueryDto(dto);
}
@Override
public List<Banner> findBannerByPosition(Integer position) {
return dao.findBannerByPosition(position);
}
}
13.6 编写controller接口
@RequestMapping("/banner")
@RestController //进行ioc并且方法都返回json格式,等于Controller注解+ResponseBody注解
public class BannerController {
@Autowired
BannerService bannerService;
@RequestMapping(value = "/list",method = RequestMethod.GET)
public Result list(BannerQueryDto dto){
//查询页面数据
List<Banner> bannerList = bannerService.findBannerByQueryDto(dto);
//查询总记录数
Long count = bannerService.findBannerCountByQueryDto(dto);
return ResultUtils.buildSuccess(bannerList,count);
}
@RequestMapping(value = "/{id}",method = RequestMethod.GET)
public Result find(@PathVariable("id")String id){
//查询数据
Banner byId = bannerService.findBannerById(id);
return ResultUtils.buildSuccess(byId);
}
@RequestMapping(value = "/add",method = RequestMethod.POST)
public Result add(@RequestBody Banner banner){
//调用service添加
bannerService.insertBanner(banner);
return ResultUtils.buildSuccess();
}
@RequestMapping(value = "/update",method = RequestMethod.POST)
public Result update(@RequestBody Banner banner){
//调用service添加
bannerService.updateBanner(banner);
return ResultUtils.buildSuccess();
}
@RequestMapping(value = "/deleteById",method = RequestMethod.GET)
public Result deleteById(@RequestParam String id){
//调用service添加
bannerService.deleteBannerById(id);
return ResultUtils.buildSuccess();
}
}
14. 注册中心
14.1 搭建患者端
和admin管理员项目在同一级中
14.2 Zookeeper的demo
解压
修改配置文件名
更改数据目录
双击启动,若没有闪退则启动成功
14.2.1 使用Java操作zookeeper
导包
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.7</version>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
<version>0.1</version>
</dependency>
14.2.2 在单元测试中测试zk操作API
public class TestZK {
@Test
public void createPersistentNode(){
//连接zk
ZkClient zkClient = new ZkClient("127.0.0.1:2181");
//创建持久化节点
zkClient.createPersistent("/aa","哈哈");
//读取
Object o = zkClient.readData("/aa");
//修改
zkClient.writeData("/aa","呵呵");
Object o1 = zkClient.readData("/aa");
System.out.println(o1);
}
@Test
public void createEphNode(){
//连接zk
ZkClient zkClient = new ZkClient("127.0.0.1:2181");
//创建临时节点
zkClient.createEphemeral("/aa/ww","xxx");
Object o = zkClient.readData("/aa/ww");
System.out.println(o);
}
@Test
public void testChildren(){
//连接zk
ZkClient zkClient = new ZkClient("127.0.0.1:2181");
//创建持久化节点
// zkClient.createPersistent("/aa/aa","a1");
// zkClient.createPersistent("/aa/bb","b1");
// zkClient.createPersistent("/aa/cc","c1");
//获取子节点名
List<String> children = zkClient.getChildren("/aa");
System.out.println(children);
//获取子节点值
children.forEach(name->{
String path = "/aa/"+name;
Object o = zkClient.readData(path);
System.out.println(o);
});
}
//判断节点是否存在,若存在删除节点
@Test
public void testDelete(){
ZkClient zkClient = new ZkClient("127.0.0.1:2181");
//判断节点是否存在
if(zkClient.exists("/aa/aa"))
//删除节点
zkClient.delete("/aa/aa");
}
}
14.2.3 zookeeper监听机制
zookeeper实现服务器集群的原理
每台服务器启动时去zk的服务器列表中创建一个临时节点,节点的值就是服务器的ip+端口号;
客户端中有一个缓存专门用于存放服务器列表中的每一个临时节点中存放的对应的服务器地址,同时设置一个监听器,监听zk中服务器节点列表的变化,当变化时立即更新缓存中存放的服务器地址,当客户端发出请求时,根据不同的负载均衡策略,将请求发送到不同的后端服务器中。
14.2.4 dubbo注册中心原理
每当一个服务启动方在启动时去zookeeper的持久化节点下创建对应的临时节点,节点的值为自己的ip+端口号,使用临时节点是因为当某台服务提供方宕机后,能够自动断开与zookeeper的连接,并且自动删除zookeeper中对应的临时节点。
服务调用方在调用时去持久化节点下获取所有临时节点中服务提供方的ip,并将其缓存起来。同时会设置一个监听器,当监听器监听到zookeeper中存放的临时节点变化时立即更新缓存中的所有服务提供方ip。在调用服务时会根据特定的负载均衡算法在缓存中进行服务调用
服务提供端代码
public class Server {
public static void main(String[] args) throws Exception {
//可以更改不同的port值多次执行此main方法开启不同的服务提供端线程,客户端可以通过这个端口号访问到这个对应服务提供端
int port=20002;
// 1.创建绑定到特定端口的服务器套接字。
ServerSocket ss = new ServerSocket(port);
//向zk注册服务器信息
register(port);
while(true) {
// 2.监听客户端的请求,获取客户端的Socket对象
// 侦听并接受到此套接字的连接,当没有接收到客户端请求时被阻塞
Socket socket = ss.accept();
System.out.println(port+"-----------");
//创建server线程并开启
new SocketThread(socket).start();
}
}
/**
* 向zk服务器注册自己的ip地址和端口号
* @param port
*/
private static void register(int port) {
//连接zk服务器
ZkClient zkClient = new ZkClient("127.0.0.1:2181");
//获取存放所有服务端临时节点的持久化父节点名
String parentPath = "/dubbo";
//判断持久化节点是否存在,不存在则创建
if(!zkClient.exists(parentPath)){
//创建持久化节点
zkClient.createPersistent(parentPath);
}
//每台服务器连接时创建临时节点
try {
//获取当前服务端ip地址
InetAddress inetAddress = InetAddress.getLocalHost();
String ip = inetAddress.getHostAddress();
//准备临时节点数据,作为临时节点中的值
String server = ip+":"+port;
//准备临时节点完整路径,作为临时节点的名字
String lpath = parentPath+"/"+server;
//判断临时节点是否存在
if(zkClient.exists(lpath)){
zkClient.delete(lpath);
}
//创建临时节点
zkClient.createEphemeral(lpath,server);
}catch (Exception e){
e.printStackTrace();
throw new RuntimeException("注册临时节点失败");
}
}
}
服务提供端多线程类代码
public class SocketThread extends Thread {
//保存服务端的socket
private Socket socket;
public SocketThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//3.获取客户端的数据,从服务端角度是读取数据,所以使用inputStream解析数据
InputStream in = socket.getInputStream();
InetAddress inetAddress = socket.getInetAddress();
byte[] buffer=new byte[1024];
int len=in.read(buffer);
System.out.println("ip"+inetAddress.getHostAddress()+new String(buffer, 0, len));
//4.服务端向客户端发送响应数据,从服务端角度是发送数据,所以使用outputStream解析数据
OutputStream out = socket.getOutputStream();
out.write("奥利给,今天天气很好,我们去爬山".getBytes());
//5.关闭资源
socket.close();
}catch (Exception e) {
e.printStackTrace();
}
}
}
服务调用端代码
public class Client {
//存储服务提供方列表
static List<String> servers = new LinkedList<>();
//记录轮询的均衡策略时访问次数
static int count = 0;
/**
* 初始化服务提供方列表,并建立列表变化的监听
*/
static void initServer(){
//连接zookeeper服务器
ZkClient zkClient = new ZkClient("127.0.0.1:2181");
//获取zk中存放服务端列表的持久化节点的名字
String parentPath = "/dubbo";
//当列表中节点不存在
if(!zkClient.exists(parentPath)){
throw new RuntimeException("没有提供方");
}
//获取持久化节点所有的临时节点的ip,缓存到链表中,其存放的格式是ip值:端口号,如127.0.0.1:20001
List<String> children = zkClient.getChildren(parentPath);
for(String name:children){
String value = zkClient.readData(parentPath + "/" + name);
servers.add(value);
}
System.out.println("初始化服务器列表成功");
//注册监听节点变化,zk会开一个单独线程监听其变化,并自动调用handleChildChange回调
zkClient.subscribeChildChanges(parentPath, new IZkChildListener() {
@Override
public void handleChildChange(String parentPath, List<String> currentChilds) throws Exception {
//清除服务器列表
servers.clear();
//重新获取新的服务器列表
for(String name:currentChilds){
String value = zkClient.readData(parentPath + "/" + name);
servers.add(value);
}
System.out.println("服务器提供方:"+servers);;
}
});
}
public static void main(String[] args) throws Exception, IOException {
initServer();
// 1.创建Socket对象
while (true) {
try {
//getServer所返回的是类似于127.0.0.1:20001格式的字符串,这里将IP地址和端口号分割
String[] servers = getServer().split(":");
Socket socket = new Socket(servers[0], Integer.valueOf(servers[1]));
// 2.发送数据
// 获取字节输出流,向服务端发送数据
OutputStream out = socket.getOutputStream();
//键盘录入数据输出给服务器的socket
Scanner sc = new Scanner(System.in);
String str = sc.next();
out.write(str.getBytes());
// 3.获取服务器端响应的数据
InputStream in = socket.getInputStream();
//读取并显示服务器端响应的数据
byte[] buffer = new byte[1024];
int len = in.read(buffer);
System.out.println(new String(buffer, 0, len));
socket.close();
}catch (Exception e) {
e.printStackTrace();
System.out.println("暂时没有服务器,请稍等!");
}
}
}
//其中定义负载均衡策略
public static String getServer(){
//随机
//int index = new Random().nextInt(servers.size());
//轮询
int index = count%servers.size();
count++;
return servers.get(index);
}
}
14.3 项目整合zookeeper
原理:patient工程中需要调用admin工程中service接口实现类的方法,所以admin作为zookeeper的服务提供方,patient作为zookeeper的服务调用方,每当一个admin服务器启动时,都会到zk中注册对应的临时节点,节点中存放自己的访问ip和端口号,patient可以动态获取zk中的服务提供方列表,并根据不同的负载均衡算法选择特定的admin服务器传递调用信息,admin服务器在接收到调用信息后执行对应的方法,执行完毕后将结果再返回调用方patient端。
对于服务提供方admin来说需要暴露两个端口,一个用于暴露controller层接口被客户端访问,一个用于暴露service层接口提供被调用方patient访问
14.3.1 项目顶级依赖中导入
<!--dubbo开始-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.5.3</version>
<exclusions>
<exclusion>
<artifactId>spring</artifactId>
<groupId>org.springframework</groupId>
</exclusion>
<exclusion>
<groupId>org.jboss.netty</groupId>
<artifactId>netty</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.7</version>
<exclusions>
<exclusion>
<artifactId>spring</artifactId>
<groupId>org.springframework</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
<version>0.1</version>
<exclusions>
<exclusion>
<artifactId>spring</artifactId>
<groupId>org.springframework</groupId>
</exclusion>
</exclusions>
</dependency>
<!--dubbo结束-->
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.20.0-GA</version>
</dependency>
14.3.2 编写服务提供方admin的applicationContext-dubbo.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!-- 自定义提供方应用信息,用于计算依赖关系 -->
<dubbo:application name="hospital-admin" />
<!-- 使用zookeeper广播注册中心暴露服务地址 -->
<dubbo:registry address="zookeeper://127.0.0.1:2181" />
<!-- 用dubbo协议在20001端口暴露service接口,调用方通过这个端口调用提供方的service服务 -->
<dubbo:protocol name="dubbo" port="20001" />
<!--配置服务的提供方 Service实现类交给spring管理 如果使用注解配置,这里不需要再配置-->
<!-- 声明需要暴露的服务接口,ref是放在spring容器中的接口实现类对象的名字-->
<dubbo:service interface="com.furong.service.BannerService" ref="bannerServiceImpl" />
</beans>
14.3.3 编写服务调用方patient的applicationContext-dubbo.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!-- 自定义调用方的名字,用于计算依赖关系 -->
<dubbo:application name="hospital-patient" />
<!-- 使用zookeeper广播注册中心暴露服务地址,在进行rpc时根据这个地址到rpc中获得提供方ip+端口号 -->
<dubbo:registry address="zookeeper://127.0.0.1:2181" />
<!--配置使用接口生成代理对象
id为该代理对象在spring容器中的名字,controller层在注入时名字要和这个id一致
interface指定接口类
loadbalance属性配置负载均衡算法-->
<dubbo:reference id="bannerService" interface="com.furong.service.BannerService"/>
<!--服务提供方启动检查,设置为false时,若提供方没有启动,调用方也能启动-->
<dubbo:consumer check="false"></dubbo:consumer>
</beans>
14.3.4 编写服务调用方patient的controller层接口,在其中进行rpc
@RestController
@RequestMapping("/banner")
@Api(tags = "轮播接口")
public class BannerController {
//这里注入的是在xml中配置的代理对象
@Autowired
BannerService bannerService;
@ApiOperation("根据位置查询轮播图")
@RequestMapping(value = "/findBannerByPosition",method = RequestMethod.GET)
public Result findBannerByPosition(@RequestParam Integer position){
List<Banner> bannerByPosition = bannerService.findBannerByPosition(position);
return ResultUtils.buildSuccess(bannerByPosition);
}
}
之后就能通过访问findBannerByPosition直接调用admin中的service实现类
14.3.5 dubbo的配置注意事项
- 传递参数时若是bean类型,则必须实现序列化接口
- 服务的提供方和调用方的service接口要完全一致,包括全限定名和其中每个方法的声明,所以一般会将service的接口放在项目的公共项目中,双方依赖同一公共项目,保证两者一致性
14.3.6 dubbo中的负载均衡算法
14.4 编写科室诊室患者端查询
14.4.1 提供方admin提供服务配置
applicationContext-dubbo.xml(admin)
<dubbo:service interface="com.furong.service.DepartmentService" ref="departmentServiceImpl" />
<dubbo:service interface="com.furong.service.ConsultingRoomService" ref="consultingRoomServiceImpl" />
14.4.2 调用方patient调用服务配置
applicationContext-dubbo.xml(patient)
<dubbo:reference id="departmentService" interface="com.furong.service.DepartmentService"/>
<dubbo:reference id="consultingRoomService" interface="com.furong.service.ConsultingRoomService"/>
14.4.3 编写调用方和提供方公共接口
DepartmentService.java
public interface DepartmentService extends IService<Department> {
public List<Department> findDepartmentAll();
}
ConsultingRoomService,java
public interface ConsultingRoomService extends IService<ConsultingRoom> {
List<ConsultingRoom> findConByDepId(Integer depId);
}
14.4.4 编写提供方service接口实现类
DepartmentServiceImpl.java
@Service
public class DepartmentServiceImpl extends ServiceImpl<DepartmentMapper, Department> implements DepartmentService {
@Override
public List<Department> findDepartmentAll() {
LambdaQueryWrapper<Department> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(Department::getType,1);
return this.list(lambdaQueryWrapper);
}
}
ConsultingRoomServiceImpl.java
@Service
public class ConsultingRoomServiceImpl extends ServiceImpl<ConsultingRoomMapper, ConsultingRoom> implements ConsultingRoomService {
@Override
public List<ConsultingRoom> findConByDepId(Integer depId) {
LambdaQueryWrapper<ConsultingRoom> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(ConsultingRoom::getDepartmentId,depId);
return this.list(lambdaQueryWrapper);
}
}
14.4.5 编写调用方controller接口
@Api("科室诊室相关结构")
@RestController
@RequestMapping("/con")
public class ConController {
@Autowired
DepartmentService departmentService;
@Autowired
ConsultingRoomService consultingRoomService;
//查询所有门诊科室
@ApiOperation("查询所有门诊科室")
@RequestMapping(value = "/findDepartmentAll",method = RequestMethod.GET)
public Result findDepartmentAll(){
List<Department> list = departmentService.findDepartmentAll();
return ResultUtils.buildSuccess(list);
}
@ApiOperation("查询指定科室下门诊")
@RequestMapping(value = "/findConByDepId",method = RequestMethod.GET)
public Result findConByDepId(@RequestParam Integer depId){
List<ConsultingRoom> list = consultingRoomService.findConByDepId(depId);
return ResultUtils.buildSuccess(list);
}
}
15. redis
15.1 项目整合redis
引入依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
编写redis工具类
public class RedisTemplate {
//set方法注入
private JedisPool jedisPool;
/**
* 设置String类型的值
* @param key
* @param value
* @return
*/
public String set(String key,String value){
Jedis jedis = jedisPool.getResource();
String result = jedis.set(key, value);
jedis.close();
return result;
}
/**
* 获取string类型的值
* @param key
* @return
*/
public String get(String key){
Jedis jedis = jedisPool.getResource();
String result = jedis.get(key);
jedis.close();
return result;
}
/**
* 设置hash的值
* @param key
* @param filed
* @param value
* @return
*/
public Long hset(String key,String filed,String value){
Jedis jedis = jedisPool.getResource();
Long result = jedis.hset(key, filed, value);
jedis.close();
return result;
}
/**
* 获取hash的值
* @param key
* @param filed
* @return
*/
public String hget(String key,String filed){
Jedis jedis = jedisPool.getResource();
String result = jedis.hget(key,filed);
jedis.close();
return result;
}
/**
* 获取整合hash的map
* @param key
* @return
*/
public Map<String,String> hgetAll(String key){
Jedis jedis = jedisPool.getResource();
Map<String, String> result = jedis.hgetAll(key);
jedis.close();
return result;
}
/**
* 设置失效时间
* @param key
* @param seconds
* @return
*/
public Long expire(String key,Integer seconds){
Jedis jedis = jedisPool.getResource();
Long result = jedis.expire(key, seconds);
jedis.close();
return result;
}
/**
* 删除整个key
* @param key
* @return
*/
public Long del(String key){
Jedis jedis = jedisPool.getResource();
Long result = jedis.del(key);
jedis.close();
return result;
}
/**
* 删除hash某个filed
* @param key
* @param filed
* @return
*/
public Long hdel(String key,String filed){
Jedis jedis = jedisPool.getResource();
Long result = jedis.hdel(key,filed);
jedis.close();
return result;
}
/**
* 递增
* @param key
* @return
*/
public Long incr(String key){
Jedis jedis = jedisPool.getResource();
Long result = jedis.incr(key);
jedis.close();
return result;
}
/**
* 递减
* @param key
* @return
*/
public Long decr(String key){
Jedis jedis = jedisPool.getResource();
Long result = jedis.decr(key);
jedis.close();
return result;
}
public JedisPool getJedisPool() {
return jedisPool;
}
public void setJedisPool(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
}
spring整合redis及工具类
applicationContext-redis.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
">
<!--配置连接池的配置类-->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxTotal" value="9"/>
<property name="minIdle" value="5"/>
<property name="maxWaitMillis" value="3000"/>
</bean>
<!--配置连接池-->
<bean id="jedisPool" class="redis.clients.jedis.JedisPool">
<constructor-arg name="host" value="localhost"/>
<constructor-arg name="port" value="6379"/>
<constructor-arg name="poolConfig" ref="jedisPoolConfig"/>
</bean>
<!--配置redis工具类的ioc-->
<bean id="redisTemplate" class="com.furong.patient.template.RedisTemplate">
<property name="jedisPool" ref="jedisPool"/>
</bean>
</beans>
测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring/applicationContext.xml")
public class testString {
@Autowired
RedisTemplate redisTemplate;
@Test
public void test(){
redisTemplate.set("kk","11");
System.out.println(redisTemplate.get("kk"));
}
}
15.2 缓存的实现
缓存逻辑
15.2.1 编写患者端service接口及实现类
public interface ConService {
/**
* 查询所有科室的缓存逻辑
* @return
*/
List<Department> findDepartmentAll();
/**
* 查询指定科室下门诊的逻辑
* @param depId
* @return
*/
List<ConsultingRoom> findConByDepId(Integer depId);
}
@Service
@Slf4j
public class ConServiceImpl implements ConService {
@Autowired
DepartmentService departmentService;
@Autowired
ConsultingRoomService consultingRoomService;
//科室缓存在redis中的key
public static final String DEPART_CACHE="cache:department";
//每个诊室缓存在redis中的key
public static final String CON_CACHE="cache:con:depId:";
//缓存失效事件
public static final Integer DEPART_CACHE_EXPIRE=60*60*24*2;
public static final Integer CON_CACHE_EXPIRE=60*60*24*2;
@Autowired
RedisTemplate redisTemplate;
@Override
public List<Department> findDepartmentAll() {
//先在redis中查询是否有数据,若有直接返回
String result = redisTemplate.get(DEPART_CACHE);
if(!StringUtils.isEmpty(result)){
log.debug("缓存查询到数据{}",result);
return JsonUtils.jsonToList(result,Department.class);
}
//如果没有就查询数据库
List<Department> departmentList = departmentService.findDepartmentAll();
if(CollectionUtils.isEmpty(departmentList)){
log.error("没有数据");
throw new CustomException(ResultEnum.NO_DATA);
}
//写入redis缓存
String json = JsonUtils.objectToJson(departmentList);
redisTemplate.set(DEPART_CACHE,json);
//设置失效时间
redisTemplate.expire(DEPART_CACHE,DEPART_CACHE_EXPIRE);
return departmentList;
}
@Override
public List<ConsultingRoom> findConByDepId(Integer depId) {
String result = redisTemplate.get(CON_CACHE + depId);
if(!StringUtils.isEmpty(result)){
log.debug("缓存查询到数据{}",result);
return JsonUtils.jsonToList(result,ConsultingRoom.class);
}
List<ConsultingRoom> conList = consultingRoomService.findConByDepId(depId);
if(CollectionUtils.isEmpty(conList)){
log.error("没有数据");
throw new CustomException(ResultEnum.NO_DATA);
}
String json = JsonUtils.objectToJson(conList);
redisTemplate.set(CON_CACHE+depId,json);
redisTemplate.expire(CON_CACHE+depId,CON_CACHE_EXPIRE);
return conList;
}
}
15.2.2 改造患者端controller接口
@Api("科室诊室相关结构")
@RestController
@RequestMapping("/con")
public class ConController {
@Autowired
ConService conService;
//查询所有门诊科室
@ApiOperation("查询所有门诊科室")
@RequestMapping(value = "/findDepartmentAll",method = RequestMethod.GET)
public Result findDepartmentAll(){
List<Department> list = conService.findDepartmentAll();
return ResultUtils.buildSuccess(list);
}
@ApiOperation("查询指定科室下门诊")
@RequestMapping(value = "/findConByDepId",method = RequestMethod.GET)
public Result findConByDepId(@RequestParam Integer depId){
List<ConsultingRoom> list = conService.findConByDepId(depId);
return ResultUtils.buildSuccess(list);
}
}
问题:当患者端第一次通过远程调用查询到数据并写入缓存后,此时若数据库中存放的数据发生变化,患者端再次查询时,会因为缓存中已经写入了前一次查询的数据而返回未更新的查询结果,出现缓存双写不一致情况
15.2.3 解决缓存双写不一致
在患者端编写删除缓存的controller接口,当管理端在更新数据时,通过rpc远程调用患者端的删除contoller接口,患者端之后再进行查询时就会去数据库重新进行查询并将新数据写入缓存中
15.2.3.1 删除缓存代码service接口和实现
public interface ConService {
//code。。。
/**
* 根据科室id删除在redis中的缓存
* @param id
*/
void deleteConCacheByDepId(Integer id);
}
@Service
@Slf4j
public class ConServiceImpl implements ConService {
@Autowired
DepartmentService departmentService;
@Autowired
ConsultingRoomService consultingRoomService;
//科室缓存在redis中的key
public static final String DEPART_CACHE="cache:department";
//每个诊室缓存在redis中的key
public static final String CON_CACHE="cache:con:depId:";
//缓存失效事件
public static final Integer DEPART_CACHE_EXPIRE=60*60*24*2;
public static final Integer CON_CACHE_EXPIRE=60*60*24*2;
@Autowired
RedisTemplate redisTemplate;
//code。。
@Override
public void deleteConCacheByDepId(Integer id) {
log.debug("删除缓存{}",CON_CACHE+id);
redisTemplate.del(CON_CACHE+id);
}
}
15.2.3.2 删除缓存代码controller接口实现
@Api("科室诊室相关结构")
@RestController
@RequestMapping("/con")
public class ConController {
@Autowired
ConService conService;
//查询所有门诊科室
@ApiOperation("查询所有门诊科室")
@RequestMapping(value = "/findDepartmentAll",method = RequestMethod.GET)
public Result findDepartmentAll(){
List<Department> list = conService.findDepartmentAll();
return ResultUtils.buildSuccess(list);
}
@ApiOperation("查询指定科室下门诊")
@RequestMapping(value = "/findConByDepId",method = RequestMethod.GET)
public Result findConByDepId(@RequestParam Integer depId){
List<ConsultingRoom> list = conService.findConByDepId(depId);
return ResultUtils.buildSuccess(list);
}
@ApiOperation("删除指定科室下诊室缓存")
@RequestMapping(value = "/deleteConCacheByDepId",method = RequestMethod.POST)
public Result deleteConCacheByDepId(Integer depId){
conService.deleteConCacheByDepId(depId);
return ResultUtils.buildSuccess();
}
}
15.2.3.3 利用HttpClient远程调用controller
之前都是使用rpc远程调用service,下面实现管理端远程调用患者端的删除科室缓存接口
导包
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.3.5</version>
</dependency>
演示HttpClient的get请求调用
@Test
public void testGet() throws Exception {
//创建httpClient,模拟客户端
CloseableHttpClient httpClient = HttpClients.createDefault();
URI uri = new URIBuilder("http://localhost:8080/patient/con/findConByDepId").setParameter("depId","1").build();
//创建get请求
HttpGet httpGet = new HttpGet(uri);
//发起get请求,获取响应体
CloseableHttpResponse httpResponse = httpClient.execute(httpGet);
HttpEntity responseEntity = httpResponse.getEntity();
System.out.println(EntityUtils.toString(responseEntity));
}
演示HttpClient的post请求调用
@Test
public void testPost() throws Exception{
//创建httpClient,模拟客户端
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建post请求
HttpPost httpPost = new HttpPost("http://localhost:8080/patient/con/deleteConCacheByDepId");
//准备表单输入项
List<NameValuePair> pairList = new ArrayList<>();
pairList.add(new BasicNameValuePair("depId","1"));
//准备表单
UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(pairList);
//准备post请求的请求体,将表单传入请求体
httpPost.setEntity(urlEncodedFormEntity);
//发起post请求,获取响应体
CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
HttpEntity responseEntity = httpResponse.getEntity();
System.out.println(EntityUtils.toString(responseEntity));
}
编写HttpClient工具类
public class HttpClientUtil {
public static String doGet(String url, Map<String, String> param) {
// 创建Httpclient对象
CloseableHttpClient httpclient = HttpClients.createDefault();
String resultString = "";
CloseableHttpResponse response = null;
try {
// 创建uri
URIBuilder builder = new URIBuilder(url);
if (param != null) {
for (String key : param.keySet()) {
builder.addParameter(key, param.get(key));
}
}
URI uri = builder.build();
// 创建http GET请求
HttpGet httpGet = new HttpGet(uri);
// 执行请求
response = httpclient.execute(httpGet);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == 200) {
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (response != null) {
response.close();
}
httpclient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
public static String doGet(String url) {
return doGet(url, null);
}
public static String doPost(String url, Map<String, String> param) {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 创建参数列表
if (param != null) {
List<NameValuePair> paramList = new ArrayList<NameValuePair>();
for (String key : param.keySet()) {
paramList.add(new BasicNameValuePair(key, param.get(key)));
}
// 模拟表单
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList);
httpPost.setEntity(entity);
}
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "utf-8");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
response.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return resultString;
}
public static String doPost(String url) {
return doPost(url, null);
}
public static String doPostJson(String url, String json) {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 创建请求内容
StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
httpPost.setEntity(entity);
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "utf-8");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
response.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return resultString;
}
}
管理端修改诊室con时调用患者端的删除缓存controller接口
@RequestMapping(value = "/update",method = RequestMethod.POST)
public Result update(@RequestBody ConsultingRoom consultingRoom){
//调用service修改
boolean b = consultingRoomService.updateById(consultingRoom);
//修改、添加、删除操作时,都要删除对应的redis缓存
Map<String,String> param = new HashMap<>();
ConsultingRoom byId = consultingRoomService.getById(consultingRoom.getId());
param.put("depId",byId.getDepartmentId()+"");
HttpClientUtil.doPost("http://localhost:8080/patient/con/deleteConCacheByDepId",param);
return ResultUtils.judge(b);
}
16. 实现患者端挂号流程
实现方法:在患者端编写controller接口,其中注入管理端对应的service接口,通过rpc调用管理端的service实现类方法
16.1 接口一:根据诊室id和当前日期查询诊室在最近7天的出诊时间信息
16.1.1在管理端的xml编写sql
<select id="searchCanRegisterInDateRange" parameterType="com.furong.pojo.dto.WorkPlanInQueryDto" resultType="string">
select DISTINCT date from fr_doctor_work_plan where con_id=#{conId}
and date BETWEEN #{startDate} AND #{endDate}
</select>
在查询数据库查询结果如下
16.1.2在管理端编写对应的mapper接口
public interface DoctorWorkPlanMapper extends BaseMapper<DoctorWorkPlan> {
/**
* 用户端根据诊室id和日期查询科室有医生出诊的日期
* @param workPlanInQueryDto
* @return
*/
List<String> searchCanRegisterInDateRange(WorkPlanInQueryDto workPlanInQueryDto);
//code。。。
}
16.1.3 在管理端编写对应的service层接口及实现
/**
* 用户端根据诊室id和日期查询科室有医生出诊的日期
* @param workPlanInQueryDto
* @return
*/
List<Map> searchCanRegisterInDateRange(WorkPlanInQueryDto workPlanInQueryDto);
//返回一个List,其中每个Map元素代表一天,当这一天有出诊时,设置status为出诊,否则设置为无号
@Override
public List<Map> searchCanRegisterInDateRange(WorkPlanInQueryDto workPlanInQueryDto) {
List<String> list = getBaseMapper().searchCanRegisterInDateRange(workPlanInQueryDto);
DateTime startDate = DateUtil.parse(workPlanInQueryDto.getStartDate());
DateTime endDate = DateUtil.parse(workPlanInQueryDto.getEndDate());
DateRange range = DateUtil.range(startDate, endDate, DateField.DAY_OF_MONTH);
ArrayList result = new ArrayList();
while (range.hasNext()) {
String date = range.next().toDateStr();
if (list.contains(date)) {
result.add(new HashMap() {{
put("date", date);
put("status", "出诊");
}});
} else {
result.add(new HashMap() {{
put("date", date);
put("status", "无号");
}});
}
}
return result;
}
16.1.4 配置提供方和调用方的dubbo.xml
16.1.5 编写患者端controller
@Api("出诊相关接口")
@RestController
@RequestMapping("/plan")
public class DoctorWorkPlanController {
@Autowired
DoctorWorkPlanService doctorWorkPlanService;
@RequestMapping(value = "/searchConDateRange",method = RequestMethod.POST)
public Result searchCanRegisterInDateRange(@RequestBody WorkPlanInQueryDto dto){
List<Map> mapList = doctorWorkPlanService.searchCanRegisterInDateRange(dto);
return ResultUtils.buildSuccess(mapList);
}
//code。。。
}
16.2 接口二:查询某天某诊室的所有出诊医生列表
16.2.1 编写xml及sql
<select id="searchDeptSubDoctorPlanInDay" parameterType="com.furong.pojo.dto.WorkPlanScheduleQueryDto" resultType="com.furong.pojo.vo.WorkPlanDateVo">
SELECT
d.id,
d.NAME,
d.avatar as photo,
di.name as job,
d.intro as introduce,
wp.num,
wp.max_num AS maxNum,
d.price
FROM
fr_doctor_work_plan wp
LEFT JOIN fr_doctor d ON wp.doctor_id =d.id
LEFT JOIN fr_dict_item di on d.post=di.id
WHERE
wp.con_id = #{conId}
AND wp.date = #{date}
</select>
在数据库查询id为3的诊室在2023-04-15出诊的医生,结果如下图所示
16.1.2 在管理端编写对应的mapper接口
public interface DoctorWorkPlanMapper extends BaseMapper<DoctorWorkPlan> {
/**
* 用户端查询某诊室某天医生出诊信息
* @param workPlanScheduleQueryDto
* @return
*/
List<WorkPlanDateVo> searchDeptSubDoctorPlanInDay(WorkPlanScheduleQueryDto workPlanScheduleQueryDto);
//code。。。
}
16.1.3 在管理端编写对应的service层接口及实现
/**
* 用户端查询某诊室某天医生出诊信息
* @param workPlanScheduleQueryDto
* @return
*/
List<WorkPlanDateVo> searchDeptSubDoctorPlanInDay(WorkPlanScheduleQueryDto workPlanScheduleQueryDto);
//code。。。
@Override
public List<WorkPlanDateVo> searchDeptSubDoctorPlanInDay(WorkPlanScheduleQueryDto workPlanScheduleQueryDto) {
return getBaseMapper().searchDeptSubDoctorPlanInDay(workPlanScheduleQueryDto);
}
16.1.4 在患者端编写管理端controller
@RequestMapping(value = "/searchDeptSubDoctorPlanInDay",method = RequestMethod.POST)
public Result searchDeptSubDoctorPlanInDay(@RequestBody WorkPlanScheduleQueryDto dto){
List<WorkPlanDateVo> workPlanDateVos = doctorWorkPlanService.searchDeptSubDoctorPlanInDay(dto);
return ResultUtils.buildSuccess(workPlanDateVos);
}
16.3 接口三:根据医生的id查询医生详情信息
16.3.1 编写xml和sql,这里因为医生的职称信息存放在数据字典项表中的,所以需要自己手动编写sql进行多表查询,不能用mybatisplus自带的查询
<select id="findDoctorById" parameterType="Integer" resultType="com.furong.pojo.vo.DoctorVo">
SELECT
d.id,
d.name,
d.avatar as photo,
di.`name` as job,
d.note as remark,
d.intro as introduce,
d.price
FROM
fr_doctor d
LEFT JOIN fr_dict_item di ON d.post = di.id
WHERE
d.id =#{doctorId}
</select>
16.3.2 编写管理端mapper接口
DoctorVo findDoctorById(Integer doctorId);
16.3.3 编写管理端service接口和实现类
DoctorVo findDoctorById(Integer doctorId);
@Override
public DoctorVo findDoctorById(Integer doctorId) {
return getBaseMapper().findDoctorById(doctorId);
}
16.3.4 编写患者端controller接口
public class DoctorController {
@Autowired
DoctorService doctorService;
@RequestMapping(value = "/searchDoctorInfoById",method = RequestMethod.GET)
public Result searchCanRegisterInDateRange(@RequestParam Integer doctorId){
DoctorVo doctor = doctorService.findDoctorById(doctorId);
return ResultUtils.buildSuccess(doctor);
}
}
16.4 接口四:根据日期和医生id查询医生该天上下午具体出诊时间信息
16.4.1 编写xml及sql
<select id="searchDoctorWorkPlanSchedule" parameterType="com.furong.pojo.dto.DoctorScheduleQueryDto" resultType="com.furong.pojo.vo.DoctorScheduleVo">
SELECT
wp.id as workPlanId,
ps.id as scheduleId,
ps.slot,
ps.max_num,
ps.num
FROM
fr_doctor_work_plan wp
LEFT JOIN fr_doctor_work_plan_schedule ps ON wp.id = ps.work_plan_id
WHERE
wp.date = #{date}
AND wp.doctor_id = #{doctorId}
ORDER BY
wp.id
</select>
例如,在数据库中查询id为3的医生在2023-04-15这一天的出诊具体时间信息:
查询出的结果的slot取值可以为1或2,若只有一条1表示医生只在上午出诊,只有一条2表示只在下午出诊,同时有1和2两条表示上下午都出诊
16.4.2 在管理端编写mapper接口
/**
* 根据日期和医生id查询医生出诊时段信息
* @param doctorScheduleQueryDto
* @return
*/
List<DoctorScheduleVo> searchDoctorWorkPlanSchedule(DoctorScheduleQueryDto doctorScheduleQueryDto);
16.4.3 在管理端编写service接口及实现
/**
* 根据日期和医生id查询医生出诊时段信息
* @param doctorScheduleQueryDto
* @return
*/
List<DoctorScheduleVo> searchDoctorWorkPlanSchedule(DoctorScheduleQueryDto doctorScheduleQueryDto);
@Override
public List<DoctorScheduleVo> searchDoctorWorkPlanSchedule(DoctorScheduleQueryDto doctorScheduleQueryDto) {
return getBaseMapper().searchDoctorWorkPlanSchedule(doctorScheduleQueryDto);
}
16.4.4 在患者端编写controller接口
@RequestMapping(value = "/searchDoctorWorkPlanSchedule",method = RequestMethod.POST)
public Result searchDoctorWorkPlanSchedule(@RequestBody DoctorScheduleQueryDto dto){
List<DoctorScheduleVo> workPlanDateVos = doctorWorkPlanService.searchDoctorWorkPlanSchedule(dto);
return ResultUtils.buildSuccess(workPlanDateVos);
}
17. 患者端实现redis缓存预热
监听spring容器的创建,在创建时先去数据库查询对应的数据并设置到redis缓存中
17.1 编写监听器类
@Slf4j
//实现ApplicationListener接口就能监听到spring容器创建,会在spring容器初始化完成后调用
public class CacheListener implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
RedisTemplate redisTemplate;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
log.info("spring容器启动====================");
//获取spring容器
ApplicationContext applicationContext = event.getApplicationContext();
//从spring容器中根据类获取service
DepartmentService departmentService = applicationContext.getBean(DepartmentService.class);
ConsultingRoomService consultingRoomService = applicationContext.getBean(ConsultingRoomService.class);
//查询所有科室
List<Department> departmentList = departmentService.findDepartmentAll();
//迭代所有科室
if(!CollectionUtils.isEmpty(departmentList)){
departmentList.forEach(department -> {
//查询科室下所有诊室列表
List<ConsultingRoom> consultingRoomList = consultingRoomService.findConByDepId(Integer.valueOf(department.getId().toString()));
//做缓存,将每个科室中的所有诊室放入redis中
if(!CollectionUtils.isEmpty(consultingRoomList)){
String json = JsonUtils.objectToJson(consultingRoomList);
redisTemplate.set(ConServiceImpl.CON_CACHE+department.getId(),json);
redisTemplate.expire(ConServiceImpl.CON_CACHE+department.getId(),ConServiceImpl.CON_CACHE_EXPIRE);
}
});
}
}
}
17.2 配置患者端spring容器监听器
applicationContext.xml
<bean class="com.furong.patient.listener.CacheListener"/>
18. dubbo的spi
和Mybatis的插件类似,dubbo底层开放了很多spi接口,这些spi接口可以在运行时针对dubbo中不同的部分进行自定义配置,比如包含了序列化、通信协议、负载均衡等等接口,比如我们要自己编写算法来实现dubbo的负载均衡时:
18.1 编写自定义类实现LoadBalance接口
//通过实现LoadBalance接口来自定义负载均衡算法
public class MyLoadBalance implements LoadBalance {
@Override
//list为调用方在注册中心中的临时节点
public <T> Invoker<T> select(List<Invoker<T>> list, URL url, Invocation invocation) throws RpcException {
//随机
int index = new Random().nextInt(list.size());
log.info("服务提供方{}",index);
return list.get(index);
}
}
18.2 在resources目录下配置
创建名为META-INF/dubbo/com.alibaba.dubbo.rpc.cluster.LoadBalance的file文件,在其中编写xxx1=xxx2,xxx1代表自定义的这个负载均衡名,xxx2代表自定义负载均衡实现类的全限定名
18.3 在服务调用方的标签内配置loadbalance="xxx1"即可
原理:当dubbo启动时会自动检测到LoadBalance文件,并且将其中配置的自定义负载均衡名和负载均衡实现类进行匹配,我们在调用方标签内配置这个自定义负载均衡名时,就能实现自动调用
19. mq消息中间件
19.1 测试生产者和消费者
生产者
public class MessageProduct {
public static void main(String[] args) throws Exception {
//创建连接工长
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ActiveMQConnectionFactory.DEFAULT_USER, ActiveMQConnectionFactory.DEFAULT_PASSWORD, "tcp://127.0.0.1:61616");
//获取连接
Connection connection = activeMQConnectionFactory.createConnection();
//开启通信
connection.start();
//创建session
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//设置发送目标
Destination destination = session.createQueue("banzhang");
//创建消息生产者,指定消息的生产者往哪里发送消息
MessageProducer producer = session.createProducer(destination);
//创建消息
TextMessage textMessage = session.createTextMessage("ikun hello");
//发送消息
producer.send(textMessage);
connection.close();
}
}
消费者
public class MessageReceive {
public static void main(String[] args) throws Exception {
//1.创建连接工厂
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ActiveMQConnectionFactory.DEFAULT_USER,
ActiveMQConnectionFactory.DEFAULT_PASSWORD, "tcp://127.0.0.1:61616");
//2.获取连接
Connection connection = activeMQConnectionFactory.createConnection();
//3.开启通信
connection.start();
//4.创建session
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//5.设置接收目标
Destination destination = session.createQueue("banzhang");
//6.创建消费者
MessageConsumer messageConsumer = session.createConsumer(destination);
while(true){
TextMessage message = (TextMessage) messageConsumer.receive();//阻塞,等待新的消息
System.out.println(message.getText());
}
}
}
19.2 消息丢失
当mq宕机后就会导致其中消息丢失,我们设置消息持久化就能避免,在创建消息生产者时可以指定其持久化保存
//创建消息生产者,指定消息的生产者往哪里发送消息
MessageProducer producer = session.createProducer(destination);
//发送的消息在mq中持久化,避免mq宕机后消息不会丢失
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
19.3 activemq和spring整合
在项目中要实现当管理端在更新诊室信息时,就向mq的conCache通道发送一条消息,其中为被更新诊室所对应的科室id,患者端会随时监听mq中conCache通道中是否有消息,若有消息时将消息中被更新诊室所对应的科室id取出,再去redis中删除这个科室下所有诊室信息的缓存,所以在这里管理端是消息生产者,患者端是消息消费者
19.3.1 导包
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-all</artifactId>
<version>5.9.0</version>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-pool</artifactId>
<version>5.9.0</version>
</dependency>
19.3.2 配置消息生产者(患者端)的配置文件
applicationContext-mq.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
">
<!--1.配置mq的工厂(原厂家工厂)-->
<bean id="activeMQConnectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
<property name="userName" value="admin"/>
<property name="password" value="admin"/>
</bean>
<!--2.配置mq的连接池-->
<bean id="connectionFactory" class="org.apache.activemq.pool.PooledConnectionFactory">
<property name="connectionFactory" ref="activeMQConnectionFactory"/>
<property name="maxConnections" value="10"/>
<property name="expiryTimeout" value="3000"/>
</bean>
<!--3.配置spring mq工厂-->
<bean id="singleConnectionFactory" class="org.springframework.jms.connection.SingleConnectionFactory">
<property name="targetConnectionFactory" ref="connectionFactory"/>
</bean>
<!--配置发送目标(通道)-->
<bean id="conCache" class="org.apache.activemq.command.ActiveMQQueue">
<constructor-arg name="name" value="conCache"/>
</bean>
<!--JmsTemplate工具类-->
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<!--配置工厂-->
<property name="connectionFactory" ref="singleConnectionFactory"/>
<!--设置发送的默认通道目标-->
<property name="defaultDestination" ref="conCache"/>
<!--设置持久化消息-->
<property name="deliveryMode" value="2"/>
</bean>
</beans>
19.3.3 改造消息生产者(患者端)更新诊室代码
@RequestMapping(value = "/update",method = RequestMethod.POST)
public Result update(@RequestBody ConsultingRoom consultingRoom){
//调用service更新
boolean b = consultingRoomService.updateById(consultingRoom);
ConsultingRoom byId = consultingRoomService.getById(consultingRoom.getId());
//向mq发送消息
jmsTemplate.send(new MessageCreator() {
@Override
public Message createMessage(Session session) throws JMSException {
log.info("发送修改的诊室id为{},对应的科室id为{}===============",byId.getId(),byId.getDepartmentId());
return session.createTextMessage(byId.getDepartmentId()+"");
}
});
return ResultUtils.judge(b);
}
19.3.4 编写消息消费方(患者端)消息监听器,监听mq中对应通道中是否有信息,并解析信息,进行redis缓存删除(通过redis解决重复消费问题)
//当mq队列中有更新消息时自动调用清除redis缓存的监听器类
@Slf4j
public class ConMessageListener implements MessageListener {
@Autowired
ConService conService;
@Autowired
RedisTemplate redisTemplate;
@Override
public void onMessage(Message message) {
try {
//获取接收消息
ActiveMQTextMessage activeMQTextMessage = (ActiveMQTextMessage)message;
String msg = activeMQTextMessage.getText();
log.info("接收到的消息为{}",msg);
String jmsMessageID = activeMQTextMessage.getJMSMessageID();
//查询redis中是否已经有key为jmsMessageID的数据,若存在则说明当前消息已经处理过了,只是没有签收
String result = redisTemplate.get(jmsMessageID);
if(!StringUtils.isEmpty(result)){
log.info("已经处理过业务,直接签收");
activeMQTextMessage.acknowledge();
return;
}
//删除redis中科室下的所有缓存
conService.deleteConCacheByDepId(Integer.valueOf(msg));
//删除后在redis中标记,jmsMessageID是消息生产方为每条消息生成的唯一标志,这里将其作为key存入redis中
//放入redis目的是防止在消息已经被处理后,但是手动签收前出现异常,导致消息被重复处理,及mq的幂等性问题(消息重复消费问题)
redisTemplate.set(jmsMessageID,"xxx");
//手动签收
activeMQTextMessage.acknowledge();
}catch (Exception e){
log.error(e.getMessage());
//签收失败,抛异常
throw new RuntimeException(e);
}
}
}
19.3.5 配置消息消费方(患者端)配置文件及配置监听器
applicationContext-mq.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<!--1.配置mq的工厂(原厂家工厂)-->
<bean id="activeMQConnectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
<property name="userName" value="admin"/>
<property name="password" value="admin"/>
</bean>
<!--2.配置mq连接池-->
<bean id="connectionFactory" class="org.apache.activemq.pool.PooledConnectionFactory">
<property name="maxConnections" value="10"/>
<property name="expiryTimeout" value="2000"/>
<property name="connectionFactory" ref="activeMQConnectionFactory"/>
</bean>
<!--3.配置spring mq工厂-->
<bean id="singleConnectionFactory" class="org.springframework.jms.connection.SingleConnectionFactory">
<property name="targetConnectionFactory" ref="connectionFactory"/>
</bean>
<!--配置接收目标(队列)-->
<bean id="conCache" class="org.apache.activemq.command.ActiveMQQueue">
<constructor-arg name="name" value="conCache"/>
</bean>
<bean id="conMessageListener" class="com.furong.patient.listener.ConMessageListener"/>
<!--配置监听处理类-->
<bean class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<!--注入工厂-->
<property name="connectionFactory" ref="singleConnectionFactory"/>
<!--配置监听目标-->
<property name="destination" ref="conCache"/>
<!--配置手动签收-->
<property name="sessionAcknowledgeMode" value="2"/>
<!--配置接收超时时间-->
<property name="receiveTimeout" value="2000"/>
<!--配置监听类-->
<property name="messageListener" ref="conMessageListener"/>
</bean>
</beans>