大美最近在项目管理上遇到一些问题:
- 资源协调效率低。真的是我看你不忙,其实你很忙;
- 人员任务饱和度难评估。可谓是旱的旱死,涝的涝死,有时还不自知;
- 可用资源不透明。全局调人难难难啊……
哎,头大。。。
团队在用的三方项目管理平台,从单个项目来看,还挺好用,但在全局维度略显不友好,为满足现有需要,实现高效管理,大美就在想:如果任务创建规范,状态更新及时,来个全局的可视化资源日历,岂不是电灯照雪–明明白白…..咦~想想都可美。
尝试一把?
说干就干!!!
一番讨论后,初版流程出炉:
一打听API服务居然收费,还挺贵!看来接口(方案1)是用不了了,只能从页面(方案2)入手了。
那咱就用UI自动化,来解决下数据获取问题吧!
python+selenium模拟人工操作点~点~点~,登录、查询、导出一条龙服务,新鲜热乎的excel文件到手啦,完美!
数据有了,存哪儿呢?
必须数据库啊,现成的测试服务器,安装mysql!
那数据要怎么展示?支持哪些筛选条件呢?一通讨论后,草图搞定,来瞄一眼~
技术方案很快也敲定了,后台使用Spring+SpringBoot+MyBatis+MySQL框架,页面交互使用PyQt5(先不要问为啥不用web页),结合python的plotly_express库来实现任务甘特图的展示,数据后端处理,前端调用。
开工!!!
大壮负责后端,小帅负责前端。
大壮很快完成了接口:
以「按平台获取任务列表」为例,来聊聊:
小帅选择部门、开始时间、结束时间、优先级、状态等条件后,点击刷新数据按钮时,请求两位基友约定好的接口。
# 获取部门的数据
def get_department_data(self, department, startDate, endDate,prioritys,states):
url = self.url + '/teststatistics-app/api/v2/xxxxxx'
data = {
'platform': department,
'startDate': startDate + ' 00:00:00',
'endDate': endDate + ' 23:59:59',
'prioritys':prioritys,
'states': states
}
header = {
'Content-Type': 'application/json'
}
try:
res = requests.post(url, data=json.dumps(data), headers=header)
try:
if res.json()['code'] == 0:
return res.json()
else:
return str(res.json()['msg']) + '错误代码:009'
except:
return str(res.json()['status']) + '---' + str(res.json()['message']) + '错误代码008'
except Exception as e:
return str(e) + '错误代码:007'
大壮收到了小帅呼唤,先去查询了各平台的人员:
/**
* 根据平台名称查询该平台下的人员
* @param platform
* @return
*/
List<String> getUserByPlatform(String platform);
<select id="getUserByPlatform" parameterType="string" resultType="string">
SELECT UserName
FROM t_user
WHERE sid = (SELECT id FROM t_station WHERE Job = #{platform})
AND State = 1
</select>
再根据人员、开始时间、结束时间查询任务列表:
<select id="getUserByPlatform" parameterType="string" resultType="string">
SELECT UserName
FROM t_user
WHERE sid = (SELECT id FROM t_station WHERE Job = #{platform})
AND State = 1
</select>
<select id="getTaskByUserName" parameterType="com.uutest.domain.TaskUser" resultType="com.uutest.domain.Task">
SELECT Title, Priority, Conductor, EstimatedStartTime, EstimatedEndTime, State, Iteration,Progress
FROM t_task
WHERE Conductor LIKE CONCAT('%', #{userName}, '%')
AND EstimatedStartTime <![CDATA[<=]]> #{endDate} AND EstimatedEndTime <![CDATA[ >=]]>
#{startDate}
<if test="prioritys != null and prioritys.size() > 0">
AND Priority in
<foreach item="item" index="index" collection="prioritys" open="(" close=")" separator=",">
#{item}
</foreach>
</if>
<if test="states != null and states.size() > 0">
AND State in
<foreach item="item" index="index" collection="states" open="(" close=")" separator=",">
#{item}
</foreach>
</if>
</select>
大壮将查询到的任务数据处理:
public List<Task> getPlatformTaskList(String platformName, String startDate, String endDate, List<String> prioritys, List<String> states) throws ParseException {
List<String> userList = taskMapper.getUserByPlatform(platformName);
List<Task> list;
List<Task> taskList = new ArrayList<>();
for (String userName : userList) {
list = taskMapper.getTaskByUserName(userName, startDate, endDate, prioritys, states);
String platform = taskMapper.getPlatformByUsername(userName);
if (list.size() < 1) {
Task task = new Task();
task.setTitle("");
task.setPriority("--");
task.setConductor(userName);
task.setEstimatedStartTime(startDate);
task.setEstimatedEndTime("0000-00-00 00:00:00");
task.setState("");
task.setIteration("");
task.setProgress("0");
list.add(task);
}
for (Task task : list) {
task.setConductor(platform + "-" + userName);
task.setProgress(task.getProgress().replace("%", " ").trim());
if (task.getEstimatedStartTime().split(" ")[0].equals("0000-00-00") || task.getEstimatedStartTime().split(" ")[0].equals("1991-01-01")) {
task.setEstimatedStartTime("");
}
if (task.getEstimatedEndTime().split(" ")[0].equals("1990-01-01") || task.getEstimatedEndTime().split(" ")[0].equals("0000-00-00")) {
task.setEstimatedEndTime("");
}
if (!task.getEstimatedStartTime().equals("") && !task.getEstimatedEndTime().equals("")) {
if (task.getEstimatedStartTime().split(" ")[0].equals(task.getEstimatedEndTime().split(" ")[0])) {
String endTime = task.getEstimatedEndTime().split(" ")[0];
String estimatedEndTime = endTime + " " + "18:00:00";
task.setEstimatedEndTime(estimatedEndTime);
}
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = sdf.parse(task.getEstimatedStartTime());
Date date1 = sdf.parse(startDate);
Calendar cal = Calendar.getInstance();
cal.setTime(date);
int month = cal.get(Calendar.MONTH);
Calendar cal1 = Calendar.getInstance();
cal1.setTime(date1);
int month1 = cal.get(Calendar.MONTH);
if (month == month1) {
taskList.add(task);
}
}
}
List<Task> taskList1 = listSort(taskList);
return taskList1;
}
数据处理完了,大壮把数据处理成小帅想要的格式,暴露出他俩约定的接口:
@PostMapping("getPlatformTaskList")
public String getPlatformTaskList(@RequestBody PlatformInfo platformInfo) throws Exception {
logger.info("getPlatformTaskList的接口请求内容是:" + platformInfo);
try {
String platform = platformInfo.getPlatform();
String startDate = platformInfo.getStartDate();
String endDate = platformInfo.getEndDate();
List<String> prioritys = platformInfo.getPrioritys();
List<String> states = platformInfo.getStates();
if (platform.equals("全部")) {
List<Task> allTaskList = taskService.getAllTaskList(startDate, endDate, prioritys, states);
logger.info("接口返回内容:" + allTaskList);
return JSON.toJSONString(JsonData.buildSuccess(allTaskList));
}
List<Task> platformTaskList = taskService.getPlatformTaskList(platform, startDate, endDate, prioritys, states);
logger.info("接口返回内容:" + platformTaskList);
return JSON.toJSONString(JsonData.buildSuccess(platformTaskList));
} catch (Exception e) {
e.printStackTrace();
return JSON.toJSONString(JsonData.buildError("服务器内部错误" + e, 500));
}
}
和大壮之间一次愉快合作后,小帅接收到接口返回的数据,小帅要对对数据进行处理,使数据格式符合plotly_express需要的格式,并进行数据的优化(如多个任务开始时间和结束时间相同时,合并成一个任务)
for i in self.data:
aa = 22
for k, d in enumerate(df):
data = {"处理人": i['conductor'], "开始时间": i['estimatedStartTime'], "结束时间": i['estimatedEndTime']}
aa = assert_dict(data, d)
if aa == 11:
df[k]['任务'] = df[k]['任务'] + '---【' + i['title'] + ' / ' + i['priority'] + ' / ' + i['state'] + ' / ' + i['iteration'] + '】'
df[k]['完成度']=df[k]['完成度']+int(i['progress'])
break # 如果不加break,当数据[2]和[1,2,3,4]数据中的2一致的时候,会继续和后面的数据做对比,就是导致判断用的aa初始值变成22,导致在拼接之后,还会加入到df列表中
if aa == 22:
df.append(
{"处理人": i['conductor'], "开始时间": i['estimatedStartTime'], "结束时间": i['estimatedEndTime'],"综合等级": i['priority'],
"任务": '【' + i['title'] + ' / ' + i['priority'] + ' / ' + i['state'] + ' / ' + i['iteration'] + '】','完成度':int(i['progress'])})
把处理过的数据传给plotly_express,设置显示的格式:
fig = px.timeline(
df2, # 绘图数据
y="处理人", # y轴显示的数据
x_start="开始时间", # 开始时间
x_end="结束时间", # 结束时间
color="完成度", # 颜色设置
opacity=0.7,
title='人员任务分布图',
template='plotly', # 值:plotly_white,plotly,plotly_dark
# color_discrete_map=colors, # 配置自定义颜色
# hover_name='项目名称', # 设置鼠标悬停是显示的内容
hover_data=['任务'], # 设置鼠标悬停是显示的内容
height=height)
接着生成html文件,保存在本地:
fig.write_html('1.html') # 生成html文件
最后把html页面显示在前端:
def showhtml(self, data):
if data == 'finsh':
# 让web控件显示生成的本地的html文件
self.browser.load(QUrl(QFileInfo("./1.html").absoluteFilePath()))
else:
QMessageBox.information(self, '提示', data)
来瞄一瞄最后的小宝贝儿长啥样吧,喏~
任务展示出来了,现在每个人的任务安排都清楚了,任务饱和度一目了然。
但小美发现隔壁王麻子的任务已经逾期好久了还是进行中,大壮处理数据的时候发现有的任务没有处理人、有的没有开始时间、结束时间,新建任务不规范导致数据处理异常,数据统计不准确。无规矩不成方圆,这得规范起来呐!
大美找来大壮,咱们得把逾期的、信息不完整的任务通知给创建人让他及时去调整,新建任务通知给部门主管。大壮思索片刻,已然成竹在胸,这个功能可以如此这般……
以「逾期任务通知」为例:
大壮先查询逾期任务:
/**
* 查询逾期任务
* @param jobName
* @return
*/
List<BatchJob> getOverdueTaskList (String jobName);
<select id="getOverdueTaskList" resultType="com.uutest.domain.BatchJob" parameterType="string">
SELECT t.ID,
t.Title,
t.Conductor,
t.State,
t.Creator,
t.Iteration,
t.demand,
t.EstimatedStartTime,
t.EstimatedEndTime,
u.Mobile,
p.ProjectID,
u.userid,
u.Job
FROM t_task t
LEFT JOIN t_user u ON u.UserName = t.Creator
LEFT JOIN t_project p ON t.AffiliationProject = p.ProjectName
WHERE u.Job = #{jobName}
AND t.State in ('进行中', '未开始')
AND u.State = 1
</select>
调用钉钉工作通知接口:
/**
* 调用钉钉通知接口
*
* @param userIds
* @param msg
* @return
* @throws IOException
*/
public static boolean senDingDingMessage(String userIds, String msg) throws Exception {
String url = "https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2?" + "access_token=" + readAccessToken();
// 设置请求头的值
HashMap<String, String> headermap = new HashMap<>();
headermap.put("Content-Type", "application/x-www-form-urlencoded");
Map dingMsg = new HashMap();
Map msgMap = new HashMap();
dingMsg.put("msgtype", "text");
msgMap.put("content", msg);
dingMsg.put("text", msgMap);
String json = JSONObject.toJSONString(dingMsg);
Map body = new HashMap();
body.put("userid_list", userIds);
body.put("agent_id", "1352487382");
body.put("msg", json);
CloseableHttpResponse closeableHttpResponse = restClient.post(url, body, headermap);
String response = EntityUtils.toString(closeableHttpResponse.getEntity());
logger.info("钉钉通知接口返回结果是:"+response);
String errmsg = JsonPath.read(response, "$.errmsg");
if (errmsg.equalsIgnoreCase("ok")) {
Long task_id = JsonPath.read(response, "$.task_id");
String requestUrl = "https://oapi.dingtalk.com/topapi/message/corpconversation/getsendresult?" + "access_token=" + readAccessToken();
Map parameter = new HashMap();
parameter.put("task_id", task_id);
parameter.put("agent_id", "1352487382");
CloseableHttpResponse result = restClient.post(requestUrl, parameter, headermap);
String res = EntityUtils.toString(result.getEntity());
logger.info("查询钉钉通知结果接口返回结果是:"+res);
String errinfo = JsonPath.read(res, "$.errmsg");
Integer errcode = JsonPath.read(res, "$.errcode");
int[] arr = erroe_code; //钉钉消息错误码
List<Integer> list = Arrays.stream(arr).boxed().collect(Collectors.toList());
if (errinfo.equalsIgnoreCase("ok")) {
return true;
}
if (list.contains(errcode)) {
sendMessageToGroup("调用钉钉获取工作通知消息的发送结果报错了,钉钉返回内容是:" + res);
return false;
}
} else {
sendMessageToGroup("调用钉钉发送工作通知接口报错了,钉钉返回内容是:" + response);
return false;
}
return true;
}
接着发送消息通知:
public void taskOverdueReminder() throws Exception {
List<String> jobNames = taskMapper.getJobName();
jobNames.remove("产品");
Date date = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//当前时间
String today = simpleDateFormat.format(date).split(" ")[0];
String todayDate = today+" "+"00:00:00";
Date todayTime = simpleDateFormat.parse(todayDate);
//获取当前年月日+00:00:00的毫秒值
long thisTime = todayTime.getTime();
String leaderUserId = "";
for (String jobName : jobNames) {
//主管的用户id
Map<String, String> map = taskMapper.getSupervisorUserIdByJob(jobName);
leaderUserId = map.get("UserId");
String userName = map.get("UserName");
StringBuilder sb = new StringBuilder();
sb.append(userName + ",你好:\n");
sb.append("***" + today + "【团队逾期任务】信息如下***\n");
List<BatchJob> batchJobInfoList = taskMapper.getOverdueTaskList(jobName);
if (batchJobInfoList.size()>0){
for (BatchJob batchJob : batchJobInfoList) {
String title = batchJob.getTitle();
String conductor = batchJob.getConductor();
int projectID = batchJob.getProjectID();
String taskId = batchJob.getID();
String empUserid = batchJob.getUserid();
String state = batchJob.getState();
String estimatedEndTime = batchJob.getEstimatedEndTime();
String link = "https://www.tapd.cn/" + projectID + "/prong/tasks/view/" + taskId;
Date estimatedEndDate = simpleDateFormat.parse(estimatedEndTime);
//获取预期结束时间的毫秒值
long time = estimatedEndDate.getTime();
if (!estimatedEndTime.equals("1990-01-01 00:00:00")){
if (time<thisTime){
sb.append("【" + conductor + "】" + "有一个逾期的任务:" + title + ",\n" + "预计结束时间:" + estimatedEndTime + ",地址:" + link + "\n");
sb.append("\n");
// 通知处理人
String msg = conductor + ",你好:" + "\n" + "你有一个逾期的任务," + "任务名称:" + title + ",\n任务预计结束时间:" + estimatedEndTime + ",任务的地址:" + link;
DingDingUtil.senDingDingMessage(empUserid,msg );
taskMapper.saveSendMessageLog(conductor, taskId, title, state, 2, link);
}
}
}
}else {
sb.append("无逾期任务");
}
DingDingUtil.senDingDingMessage(leaderUserId, sb.toString());
}
}
设置定时任务,每天定时查询发送通知:
/**
* 每天早上8:50逾期任务和新建任务消息提醒
* @throws Exception
*/
@Scheduled(cron = "0 50 8 * * ?")
private void configureTasks() throws Exception {
try {
taskService.taskOverdueReminder();
} catch (Exception e) {
e.printStackTrace();
}
}
次日王麻子收到了一个逾期提醒,点击链接一看,哦~昨天做了一半,去处理别的事了,得赶紧处理下,要不提醒都忘了。
一大早,麻子的主管收到了一条团队的逾期提醒,啥?这几个人昨天任务没完成,得问问咋回事儿去。
除了逾期任务,还有不规范任务、逾期缺陷、新建任务等提醒,通过系统自动监控,进行有效信息的精准发送,让简单重复的事情实现自动化,切实解放人力,让主管们可以有更多的精力去做更有意义的事儿。当然工具起到的只是辅助性作用,行为的规范是不能一蹴而就的,需要大家持续养成。
作为一个负责任的开发者,当然少不了自测,大美、大壮、小帅又经过严谨的几轮自测,结结实实地发现了不少bug,作为测试人员对自己开发的工具进行测试,发现bug,解决bug,也体会了一把研发人员的不容易。
话说回来,前文提到为啥不用web页实现页面的展示,小编其实是有苦难言,作为测试,俺们正在努力学习编码知识,奈何前端知识储备不足,只好结合现有储备,先干了再说啦。