目录
生成原始会议数据
一、数据结构
二、添加测试数据
查询会议列表分页数据(后端)
一、业务分析
二、编写持久层代码
三、编辑业务层代码
四、编写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_meeting
SET 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 {
@Autowired
private TbMeetingDao meetingDao;
@Override
public 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 {
@Autowired
private MeetingService meetingService;
@Test
void 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用户ID
meeting.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">
SELECT
m.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 hour
FROM tb_meeting m
JOIN tb_user u1 ON JSON_CONTAINS(m.members,CAST(u1.id AS CHAR))
JOIN tb_user u2 ON m.creator_id=u2.id
WHERE u1.id = #{userId} AND u1.status = 1 AND u2.status = 1
AND m.status IN(3,4)
ORDER BY m.date,m.start,m.id
LIMIT #{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 {
……
@Override
public 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 {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private 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">
SELECT
d.id,
d.dept_name AS deptName,
COUNT(u.id) AS count
FROM 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">
SELECT
d.id AS deptId,
d.dept_name AS deptName,
u.id AS userId,
u.name
FROM tb_dept d JOIN tb_user u ON u.dept_id=d.id
WHERE 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 {
……
@Override
public 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,photo
FROM tb_user
WHERE status = 1
AND 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 {
……
@Override
public ArrayList<HashMap> searchMembers(List param) {
ArrayList<HashMap> list=userDao.searchMembers(param);
return list;
}
}
创建 SearchMembersForm.java 类,接收移动端提交的数据
@Data
@ApiModel
public class SearchMembersForm {
@NotBlank
private 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 bash
cd /root/workflow
nohup java -jar emos-workflow.jar >> out.log 2>&1 &
三、工作流BPMN
会议审批流程如上图,因为无论审批结果是什么,都要向emos-wx-api 项目发送HTTP请求,告知审批结果。
- 如果创建会议的是总经理,那么不需要审批,该会议直接通过;如果创建会议的不是总经理,那么必须要经过审批。
- 如果参会人都不是同一个部门的,需要先由发起人所在部门的经理审批,然后由总经理审批;如果参会人都是一个部门的,那就只需要该部门的经理审批即可。
开启会议审批工作流(持久层&业务层)
<select id="searchUserInfo" parameterType="int" resultType="HashMap">
SELECT
u.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.status
WHEN 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 roles
FROM tb_user u
LEFT JOIN tb_dept d ON u.dept_id = d.id
WHERE u.id = #{userId} AND u.status = 1
</select>
<select id="searchDeptManagerId" parameterType="int" resultType="int">
SELECT
u2.id
FROM tb_user u1 JOIN tb_user u2 ON u1.dept_id=u2.dept_id
JOIN 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">
SELECT
u.id
FROM tb_user u
JOIN 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">
SELECT
IF(COUNT(DISTINCT u.dept_id)=1,TRUE,FALSE ) AS bool
FROM tb_meeting m
JOIN 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 {
@NotBlank
private 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;
@NotBlank
private String members;
@NotBlank
private 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">
SELECT
m.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 instanceId
FROM tb_meeting m
JOIN tb_user u ON m.creator_id = u.id
WHERE m.id =#{id} AND u.status = 1
</select>
<select id="searchMeetingMembers" parameterType="int" resultType="HashMap">
SELECT
u.id,
u.name,
u.photo
FROM tb_meeting m
JOIN tb_user u ON JSON_CONTAINS ( m.members, CAST( u.id AS CHAR ) )
WHERE m.id=#{id} AND u.status = 1
</select>
编辑会议重新发起工作流(持久层&业务层)
@Override
public 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 在线会议房间号。
删除会议和工作流(移动端)
……