大美最近在项目管理上遇到一些问题:

  • 资源协调效率低。真的是我看你不忙,其实你很忙;
  • 人员任务饱和度难评估。可谓是旱的旱死,涝的涝死,有时还不自知;
  • 可用资源不透明。全局调人难难难啊……

哎,头大。。。

团队在用的三方项目管理平台,从单个项目来看,还挺好用,但在全局维度略显不友好,为满足现有需要,实现高效管理,大美就在想:如果任务创建规范,状态更新及时,来个全局的可视化资源日历,岂不是电灯照雪–明明白白…..咦~想想都可美。

尝试一把?

说干就干!!!

一番讨论后,初版流程出炉:

一打听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页实现页面的展示,小编其实是有苦难言,作为测试,俺们正在努力学习编码知识,奈何前端知识储备不足,只好结合现有储备,先干了再说啦。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注