目录
生成原始会议数据
一、数据结构
二、添加测试数据
查询会议列表分页数据(后端)
一、业务分析
二、编写持久层代码
三、编辑业务层代码
四、编写Web层代码
查询成员列表(后端)
一、编写持久层代码
二、编写业务层代码
三、编写Web层代码
实现会议成员的增删
一、查询会议成员信息
部署工作流项目
一、为什么要把工作流独立成项目?
二、部署工作流项目
三、工作流BPMN
开启会议审批工作流(持久层&业务层)
开启会议审批工作流(Web层)
保存会议记录(移动端)
加载现有会议详情
编辑会议重新发起工作流(持久层&业务层)
编辑会议重新发起工作流(Web层&移动端)
删除会议和工作流(后端)
删除会议和工作流(移动端)
生成原始会议数据
一、数据结构
前一个小节,我们把静态的会议列表页面做出来了,很多同学都想把其中的静态数据换成动态数据,所以咱们把后端的Java代码写一下。想要在页面上展示会议记录,那就需要读取tb_meeting 数据表,但是这个表里面并没有初始数据,所以我们来写程序生成原始会议数据。
首先咱们来认识一下会议表(tb_meeting),这个数据表包含了14个字段。其中的 members 字段是JSON格式,记录所有参会人员的ID。 instance_id 字段是工作流实例ID,因为会议创建出来之后,必须经过审批才能正式执行。
注意:数据表中date time类型 映射到 Java pojo包中String类型。
二、添加测试数据
在 TbMeetingDao.xml 文件中添加INSERT语句
<insert id="insertMeeting" parameterType="com.example.emos.wx.db.pojo.TbMeeting">INSERT INTO tb_meetingSET uuid = #{uuid},title = #{title},date = #{date},creator_id = #{creatorId},<if test="place!=null">place = #{place},</if>start = #{start},end = #{end},type = #{type},members = #{members},`desc` = #{desc},instance_id = #{instanceId},status = #{status},create_time = NOW()</insert>
编写 TbMeetingDao.java 接口,添加DAO方法
@Mapper
public interface TbMeetingDao {public int insertMeeting(TbMeeting entity);
}
创建 MeetingService.java 接口,添加抽象方法
public interface MeetingService { public void insertMeeting(TbMeeting entity);
}
创建 MettingServiceImpl.java 类,添加实现方法
@Service
@Slf4j
public class MeetingServiceImpl implements MeetingService {@Autowiredprivate TbMeetingDao meetingDao;@Overridepublic void insertMeeting(TbMeeting entity) {int row = meetingDao.insertMeeting(entity);if (row != 1) {throw new EmosException("会议添加失败");}//开启审批工作流//startMeetingWorkflow(entity.getUuid(), entity.getCreatorId().intValue(), entity.getDate(), entity.getStart());}
}
在 EmosWxApiApplicationTests.java 类中,编写生成原始会议记录的代码
@SpringBootTest
class EmosWxApiApplicationTests {@Autowiredprivate MeetingService meetingService;@Testvoid createMeetingData(){for (int i=1;i<=100;i++){TbMeeting meeting=new TbMeeting();meeting.setId((long)i);meeting.setUuid(IdUtil.simpleUUID());meeting.setTitle("测试会议"+i);meeting.setCreatorId(15L); //ROOT用户IDmeeting.setDate(DateUtil.today());meeting.setPlace("线上会议室");meeting.setStart("08:30");meeting.setEnd("10:30");meeting.setType((short) 1);meeting.setMembers("[15,16]");meeting.setDesc("会议研讨Emos项目上线测试");meeting.setInstanceId(IdUtil.simpleUUID());meeting.setStatus((short)3);meetingService.insertMeeting(meeting);}}
}
查询会议列表分页数据(后端)
一、业务分析
每个员工只能看到自己参与的会议记录,自己不参与的会议是看不到的。而且会议必须是未开始状态,或者进行中状态才能被看到,已经结束的会议、审批中的会议、审批不通过的会议都是无法被看到的。
二、编写持久层代码
在 TbMeetingDao.xml 文件中编写分页查询的SQL语句
<select id="searchMyMeetingListByPage" parameterType="HashMap" resultType="HashMap">SELECTm.id,m.uuid,m.title,u2.name,DATE_FORMAT(m.date,'%Y年%m月%d日') AS date,m.place,DATE_FORMAT(m.start,'%H:%i') AS start,DATE_FORMAT(m.end,'%H:%i') AS end,m.type,m.status,m.desc,u2.photo,TIMESTAMPDIFF(HOUR,CONCAT(m.date," ",m.start),CONCAT(m.date," ",m.end)) AS hourFROM tb_meeting mJOIN tb_user u1 ON JSON_CONTAINS(m.members,CAST(u1.id AS CHAR))JOIN tb_user u2 ON m.creator_id=u2.idWHERE u1.id = #{userId} AND u1.status = 1 AND u2.status = 1AND m.status IN(3,4)ORDER BY m.date,m.start,m.idLIMIT #{start}, #{length}</select>
在 TbMeetingDao.java 接口中定义Dao方法
@Mapper
public interface TbMeetingDao {……public ArrayList<HashMap> searchMyMeetingListByPage(HashMap param);
}
三、编辑业务层代码
在 MeetingService.java 中添加抽象分页方法
public interface MeetingService { ……public ArrayList<HashMap> searchMyMeetingListByPage(HashMap param);
}
在 MeetingServiceImpl.java 中添加分页方法
@Service
@Slf4j
public class MeetingServiceImpl implements MeetingService {……@Overridepublic ArrayList<HashMap> searchMyMeetingListByPage(HashMap param) {ArrayList<HashMap> list = meetingDao.searchMyMeetingListByPage(param);String date = null;ArrayList resultList = new ArrayList();HashMap resultMap = null;JSONArray array = null;for (HashMap map : list) {String temp = map.get("date").toString();if (!temp.equals(date)) {date = temp;resultMap = new HashMap();resultMap.put("date", date);array = new JSONArray();resultMap.put("list", array);resultList.add(resultMap);}array.put(map);}return resultList;}
}
四、编写Web层代码
创建 SearchMyMeetingListByPageForm.java 封装分页请求数据
@Data
@ApiModel
public class SearchMyMeetingListByPageForm {@NotNull@Min(1)private Integer page;@NotNull@Range(min = 1,max = 40)private Integer length;}
创建 MeetingController.java 类,添加分页查询Web方法
@RestController
@RequestMapping("/meeting")
@Slf4j
public class MeetingController {@Autowiredprivate JwtUtil jwtUtil;@Autowiredprivate MeetingService meetingService;@PostMapping("/searchMyMeetingListByPage")@ApiOperation("查询会议列表分页数据")public R searchMyMeetingListByPage(@Valid @RequestBody SearchMyMeetingListByPageForm form,@RequestHeader("token") String token){int userId=jwtUtil.getUserId(token);int page=form.getPage();int length=form.getLength();long start=(page-1)*length;HashMap map=new HashMap();map.put("userId",userId);map.put("start",start);map.put("length",length);ArrayList list=meetingService.searchMyMeetingListByPage(map);return R.ok().put("result",list);}
}
查询成员列表(后端)
一、编写持久层代码
在TbDeptDao.xml文件中编写SQL语句
<select id="searchDeptMembers" parameterType="String" resultType="HashMap">SELECTd.id,d.dept_name AS deptName,COUNT(u.id) AS countFROM tb_dept d LEFT JOIN tb_user u ON u.dept_id=d.id AND u.status=1<if test="keyword!=null">WHERE u.name LIKE '%${keyword}%'</if>GROUP BY d.id</select>
在 TbDeptDao.java 接口中的DAO方法
@Mapper
public interface TbDeptDao {public ArrayList<HashMap> searchDeptMembers(String keyword);
}
在 TbUserDao.xml 文件中编写SQL语句
<select id="searchUserGroupByDept" parameterType="String" resultType="HashMap">SELECTd.id AS deptId,d.dept_name AS deptName,u.id AS userId,u.nameFROM tb_dept d JOIN tb_user u ON u.dept_id=d.idWHERE u.status=1<if test="keyword!=null">AND u.name LIKE '%${keyword}%'</if>ORDER BY d.id, u.id;</select>
编写 TbUserDao.java 接口中的DAO方法
@Mapper
public interface TbUserDao {……public ArrayList<HashMap> searchUserGroupByDept(String keyword);
}
二、编写业务层代码
在 UserService.java 中创建编写抽象方法
public interface UserService {……public ArrayList<HashMap> searchUserGroupByDept(String keyword);
}
在 UserServiceImpl.java 中实现抽象方法
@Service
@Slf4j
@Scope("prototype")
public class UserServiceImpl implements UserService {……@Overridepublic ArrayList<HashMap> searchUserGroupByDept(String keyword) {ArrayList<HashMap> list_1=deptDao.searchDeptMembers(keyword);ArrayList<HashMap> list_2=userDao.searchUserGroupByDept(keyword);for(HashMap map_1:list_1){long deptId=(Long)map_1.get("id");ArrayList members=new ArrayList();for(HashMap map_2:list_2){long id=(Long) map_2.get("deptId");if(deptId==id){members.add(map_2);}}map_1.put("members",members);}return list_1;}
}// 数据表中 int 类型 可能映射为 Java中 Long 类型
三、编写Web层代码
创建 SearchUserGroupByDeptForm.java 类,接收移动端提交的数据
@Data
@ApiModel
public class SearchUserGroupByDeptForm {@Pattern(regexp = "^[\\u4e00-\\u9fa5]{1,15}$")private String keyword;
}
编写 UserController.java 的Web方法,接收移动端请求
@RestController
@RequestMapping("/user")
@Api("用户模块Web接口")
public class UserController {……@PostMapping("/searchUserGroupByDept")@ApiOperation("查询员工列表,按照部门分组排列")@RequiresPermissions(value = {"ROOT","EMPLOYEE:SELECT"},logical = Logical.OR)public R searchUserGroupByDept(@Valid @RequestBody SearchUserGroupByDeptForm form){ArrayList<HashMap> list=userService.searchUserGroupByDept(form.getKeyword());return R.ok().put("result",list);}
}
实现会议成员的增删
一、查询会议成员信息
编写 TBUserDao.xml 文件中的SQL语句
<select id="searchMembers" parameterType="list" resultType="HashMap">SELECT id,name,photoFROM tb_userWHERE status = 1AND id IN<foreach collection="list" item="one" separator="," open="(" close=")">#{one}</foreach></select>
在 TbUserDao.java 中声明DAO方法
@Mapper
public interface TbUserDao {……public ArrayList<HashMap> searchMembers(List param);
}
在 UserService.java 接口中声明抽象方法
public interface UserService {……public ArrayList<HashMap> searchMembers(List param);
}
在 UserServiceImpl.java 类中实现抽象方法
@Service
@Slf4j
@Scope("prototype")
public class UserServiceImpl implements UserService {……@Overridepublic ArrayList<HashMap> searchMembers(List param) {ArrayList<HashMap> list=userDao.searchMembers(param);return list;}
}
创建 SearchMembersForm.java 类,接收移动端提交的数据
@Data
@ApiModel
public class SearchMembersForm {@NotBlankprivate String members;
}
编写 UserController.java 中的Web方法
@RestController
@RequestMapping("/user")
@Api("用户模块Web接口")
public class UserController {……@PostMapping("/searchMembers")@ApiOperation("查询成员")@RequiresPermissions(value = {"ROOT", "MEETING:INSERT", "MEETING:UPDATE"},logical = Logical.OR)public R searchMembers(@Valid @RequestBody SearchMembersForm form){if(!JSONUtil.isJsonArray(form.getMembers())){throw new EmosException("members不是JSON数组");}List param=JSONUtil.parseArray(form.getMembers()).toList(Integer.class);ArrayList list=userService.searchMembers(param);return R.ok().put("result",list);}
}// JSONUtil.isJsonArray()
// JSONUtil.parseArray().toList(Integer.class)
// 对应前端AJAX提交数据 {members: JSON.stringify(members)}
部署工作流项目
一、为什么要把工作流独立成项目?
本课程的工作流部分已经剥离成独立的项目,跟emos-wx-api项目形成分布式调用关系。为什么要把工作流独立出来有三点考虑:
- 最新的Activiti7工作流引擎需要JDK1.8以上的环境,很多同学的JDK都还是1,8的,所以独立出来可以单独部署在高版本JDK的Docker容器里面,并不影响emos-wx-api项目。
- 因为工作流引擎搭建和BPMN绘制较为麻烦,所以方便大家使用,我就把该部分内容打成JAR文件了,直接部署就可以使用工作流引擎。
- 独立出来的工作流项目部署在其他的主机上面,容易获得更好的性能。如果所有的功能都整合到一个项目中,运行的时候主机压力较高,而且无法分拆。
二、部署工作流项目
大家在GIT 项目上面下载 jdk.tar.gz 镜像文件,把该镜像上传到centos系统,导入Docker环境
docker load < jdk.tar.gz
然后执行命令,创建JDK容器
docker run -it -d --name=workflow -p 9090:9090 -v /root/workflow:/root/workflow jdk
在Navicat上面上面执行 工作流.sql 文件,导入工作流依赖的各种数据表
用好压软件(推荐此款压缩软件)打开 emos-workflow.jar文件。找到application.yml 文件,点击右键,选择用内部查看器打开。修改其中的MYSQL连接信息和Redis连接信息,并且保存该文件。好压软件会弹出对话框是否更新该压缩文件,选择确定。
把 emos-workflow. jar 文件上传到Linux的 /root/workflow 文件夹,然后进入到Docker容器,运行工作流程序
// 注意先配置好 java 和 nohup 环境变量docker exec -it workflow bashcd /root/workflownohup java -jar emos-workflow.jar >> out.log 2>&1 &
三、工作流BPMN
会议审批流程如上图,因为无论审批结果是什么,都要向emos-wx-api 项目发送HTTP请求,告知审批结果。
- 如果创建会议的是总经理,那么不需要审批,该会议直接通过;如果创建会议的不是总经理,那么必须要经过审批。
- 如果参会人都不是同一个部门的,需要先由发起人所在部门的经理审批,然后由总经理审批;如果参会人都是一个部门的,那就只需要该部门的经理审批即可。
开启会议审批工作流(持久层&业务层)
<select id="searchUserInfo" parameterType="int" resultType="HashMap">SELECTu.open_id AS openId,u.nickname,u.name,u.photo,u.sex,u.tel,u.email,d.dept_name AS dept,u.hiredate,CASE u.statusWHEN 1 THEN "在职"WHEN 2 THEN "离职"END AS status,( SELECT GROUP_CONCAT( role_name separator "," ) FROM tb_role WHERE JSON_CONTAINS ( u.role, CONVERT ( id, CHAR ) ) ) AS rolesFROM tb_user uLEFT JOIN tb_dept d ON u.dept_id = d.idWHERE u.id = #{userId} AND u.status = 1</select>
<select id="searchDeptManagerId" parameterType="int" resultType="int">SELECTu2.idFROM tb_user u1 JOIN tb_user u2 ON u1.dept_id=u2.dept_idJOIN tb_role r ON JSON_CONTAINS(u2.role, CAST(r.id AS CHAR))WHERE u1.id=#{id} AND r.id=2 AND u1.status = 1 AND u2.status = 1</select><select id="searchGmId" resultType="int">SELECTu.idFROM tb_user uJOIN tb_role r ON JSON_CONTAINS(u.role, CAST(r.id AS CHAR))WHERE r.id=1 AND u.status = 1</select>
<select id="searchMeetingMembersInSameDept" parameterType="String" resultType="boolean">SELECTIF(COUNT(DISTINCT u.dept_id)=1,TRUE,FALSE ) AS boolFROM tb_meeting mJOIN tb_user u ON JSON_CONTAINS ( m.members, CAST( u.id AS CHAR ) )WHERE m.uuid=#{uuid} AND u.status = 1</select>
private void startMeetingWorkflow(String uuid, int creatorId, String date, String start) {HashMap info = userDao.searchUserInfo(creatorId);JSONObject json = new JSONObject();json.set("url", recieveNotify);json.set("uuid", uuid);json.set("openId", info.get("openId"));json.set("code", code);json.set("date", date);json.set("start", start);String[] roles = info.get("roles").toString().split(",");if (!ArrayUtil.contains(roles, "总经理")) {Integer managerId = userDao.searchDeptManagerId(creatorId);json.set("managerId", managerId);Integer gmId = userDao.searchGmId();json.set("gmId", gmId);boolean bool = meetingDao.searchMeetingMembersInSameDept(uuid);json.set("sameDept", bool);}String url = workflow + "/workflow/startMeetingProcess";HttpResponse resp = HttpRequest.post(url).header("Content-Type", "application/json").body(json.toString()).execute();if (resp.getStatus() == 200) {json = JSONUtil.parseObj(resp.body());String instanceId = json.getStr("instanceId");HashMap param = new HashMap();param.put("uuid", uuid);param.put("instanceId", instanceId);int row = meetingDao.updateMeetingInstanceId(param);if (row != 1) {throw new EmosException("保存会议工作流实例ID失败");}}}
开启会议审批工作流(Web层)
@Data
@ApiModel
public class InsertMeetingForm {@NotBlankprivate String title;@NotNull@Pattern(regexp = "^((((1[6-9]|[2-9]\\d)\\d{2})-(0?[13578]|1[02])-(0?[1-9]|[12]\\d|3[01]))|(((1[6-9]|[2-9]\\d)\\d{2})-(0?[13456789]|1[012])-(0?[1-9]|[12]\\d|30))|(((1[6-9]|[2-9]\\d)\\d{2})-0?2-(0?[1-9]|1\\d|2[0-8]))|(((1[6-9]|[2-9]\\d)(0[48]|[2468][048]|[13579][26])|((16|[2468][048]|[3579][26])00))-0?2-29-))$")private String date;private String place;@NotNull@Pattern(regexp = "^([01]?[0-9]|2[0-3]):[0-5][0-9]$")private String start;@NotNull@Pattern(regexp = "^([01]?[0-9]|2[0-3]):[0-5][0-9]$")private String end;@Range(min = 1,max = 2)private Byte type;@NotBlankprivate String members;@NotBlankprivate String desc;}
@PostMapping("/insertMeeting")@ApiOperation("添加会议")@RequiresPermissions(value = {"ROOT", "MEETING:INSERT"},logical = Logical.OR)public R insertMeeting(@Valid @RequestBody InsertMeetingForm form, @RequestHeader("token") String token){if(form.getType()==2&&(form.getPlace()==null||form.getPlace().length()==0)){throw new EmosException("线下会议地点不能为空");}DateTime d1= DateUtil.parse(form.getDate()+" "+form.getStart()+":00");DateTime d2= DateUtil.parse(form.getDate()+" "+form.getEnd()+":00");if(d2.isBeforeOrEquals(d1)){throw new EmosException("结束时间必须大于开始时间");}if(!JSONUtil.isJsonArray(form.getMembers())){throw new EmosException("members不是JSON数组");}TbMeeting entity=new TbMeeting();entity.setUuid(UUID.randomUUID().toString(true));entity.setTitle(form.getTitle());entity.setCreatorId((long)jwtUtil.getUserId(token));entity.setDate(form.getDate());entity.setPlace(form.getPlace());entity.setStart(form.getStart() + ":00");entity.setEnd(form.getEnd() + ":00");entity.setType((short)form.getType());entity.setMembers(form.getMembers());entity.setDesc(form.getDesc());entity.setStatus((short)1);meetingService.insertMeeting(entity);return R.ok().put("result","success");}
保存会议记录(移动端)
JwtUtil int > entity long > dao int > 数据表 int
前端JSON.stringify > entity Object > dao Object > 数据表 json
加载现有会议详情
<select id="searchMeetingById" parameterType="int" resultType="HashMap">SELECTm.uuid,m.creator_id AS creatorId,m.title,u.name,DATE_FORMAT( m.date, '%Y-%m-%d' ) AS date,m.place,DATE_FORMAT( m.START, '%H:%i' ) AS start,DATE_FORMAT( m.END, '%H:%i' ) AS end,m.type,m.status,m.desc,m.instance_id AS instanceIdFROM tb_meeting mJOIN tb_user u ON m.creator_id = u.idWHERE m.id =#{id} AND u.status = 1</select>
<select id="searchMeetingMembers" parameterType="int" resultType="HashMap">SELECTu.id,u.name,u.photoFROM tb_meeting mJOIN tb_user u ON JSON_CONTAINS ( m.members, CAST( u.id AS CHAR ) )WHERE m.id=#{id} AND u.status = 1</select>
编辑会议重新发起工作流(持久层&业务层)
@Overridepublic void updateMeetingInfo(HashMap param) {int id = (int) param.get("id");String date = param.get("date").toString();String start = param.get("start").toString();String instanceId = param.get("instanceId").toString();HashMap oldMeeting = meetingDao.searchMeetingById(id);String uuid = oldMeeting.get("uuid").toString();Integer creatorId = Integer.parseInt(oldMeeting.get("creatorId").toString());int row = meetingDao.updateMeetingInfo(param);if (row != 1) {throw new EmosException("会议更新失败");}JSONObject json = new JSONObject();json.set("instanceId", instanceId);json.set("reason", "会议被修改");json.set("uuid", uuid);json.set("code", code);String url = workflow + "/workflow/deleteProcessById";HttpResponse resp = HttpRequest.post(url).header("content-type", "application/json").body(json.toString()).execute();if (resp.getStatus() != 200) {log.error("删除工作流失败");throw new EmosException("删除工作流失败");}startMeetingWorkflow(uuid, creatorId, date, start);}
编辑会议重新发起工作流(Web层&移动端)
……
删除会议和工作流(后端)
DateTime date = DateUtil.parse(meeting.get("date") + " " + meeting.get("start"));
DateTime now = DateUtil.date();
if (now.isAfterOrEquals(date.offset(DateField.MINUTE, -20))) {throw new EmosException("距离会议开始不足20分钟,不能删除会议");
}
int row = meetingDao.deleteMeetingById(id);
if (row != 1) {throw new EmosException("会议删除失败");
}
UUID 在线会议房间号。
删除会议和工作流(移动端)
……