字数:13996

引文

有一阵子没有写文章了。回想过去这段时间里,对项目管理上,特别是任务管理上的思考与尝试,值得总结一下。为什么要做任务管理,回答这个问题 很简单:

  • 项目上有多少事情,需要拆分成可执行的碎片;

  • 每个小伙伴对事情碎片有分工

  • 结合每个碎片,能大致计算出需要的工时

  • 需要对上层汇报整体性的时间计划

那么如何管理这些任务呢?比如任务拆分合理性、任务的关联性、任务执行过程优化、任务有哪些属性需要汇总,等等。这个是本文尝试触及的 内容,当然不会太全面和深沉,毕竟这是随笔记录一下过去的尝试。

1 用甘特图

甘特图自不必多说,一个非常强大的任务管理软件,在任务编辑、时间计划和进度呈现上都非常地方便,比如上一个项目,我列了一个甘特管理计划,如下图——>

甘特图管理计划

2 自己开发程序

促使我想干这件事,有几个原因:

  • 在上个项目中,有小伙在某个具体任务开发上,过程有点不堪回首,对业务的流程\程序的逻辑没有梳理清楚,即便你给他讲解之后,表面上沟通顺畅, 也没有什么疑问,但在做的过程中又丢三落地,这样原本两三天做完的事,他可以反复几次超出一倍的计划时间。

  • 项目任务重,时间紧,程序员水平或多或少有差别。如果标识出程序员的高光时间,不再出现那种干多干少一个样,干好干的一般一个样的局面,所以我想记录。

  • 在项目整体把控上,首先我能直观计划出总的人天,其次,我结合现有延误比例的可能性,也能推算出可能失准的人天。

于是,花了几个小时整了一个原型,不过,没有人力去开发,后台接口之外还需要简单的UI设计和前端开发配合。相关的原型录屏如下——>

3 用脑图来梳理任务

如何产出任务,这方面,又回顾了一个项目该有哪些文档。本人认为,在确定产品的需求后,需要对需求进行业务场景化,用脑图来串联这些 场景,这个过程如下——>

4 用excel来管理任务吧

4.1 为什么用excel

感觉又回到了石器时代,怎么会又想到用excel呢?主要是考虑到在开发阶段,如何对任务进行管控呢?即达到第2章里的初衷,另外还有一些需求:

  • 如何对任务关联方的工作开展进行对比?

  • 如何做一些任务其它信息的记录与关联,比如接口开发完了,接口的相关信息能不能快速找到,接口所有信息集中在一起。

这样,在自定义程序还没开发的情况下,利用Excel记录信息,如果要统计,简单的可以做到,详细的可以用ETL工具来分析。这个excel大致如下——>

4.2 excel任务统计分析

  • 首先表头是这样的:

表头

  • 记录的关键元素:

关键记录

  • 用ETL工具,比如Talend TOS DI,将excel数据导入到mysql,用ETL工具做的task如下:

etljob

我用的是 Talend Open Studio for Data Integration 6.5,task链接在这里,导入然后修改一下参数即可用。

  • 用MYSQL查询语句进行统计分析
/*第1步:将/置为NULL*/
update task_static set ui_start = null where ui_start = '/';
update task_static set ui_start_actual = null where ui_start_actual = '/';
update task_static set ui_days_delay = null where ui_days_delay = '/';
update task_static set ui_days_plan = null where ui_days_plan = '/';
update task_static set ui_days_actual = null where ui_days_actual = '/';
update task_static set ui_memo = null where ui_memo = '/';
update task_static set ui_status = null where ui_status = '/';
update task_static set back_start = null where back_start = '/';
update task_static set back_start_actual = null where back_start_actual = '/';
update task_static set back_days_delay = null where back_days_delay = '/';
update task_static set back_days_plan = null where back_days_plan = '/';
update task_static set back_days_actual = null where back_days_actual = '/';
update task_static set back_memo = null where back_memo = '/';
update task_static set back_status = null where back_status = '/';
update task_static set wx_start = null where wx_start = '/';
update task_static set wx_start_actual = null where wx_start_actual = '/';
update task_static set wx_days_delay = null where wx_days_delay = '/';
update task_static set wx_days_plan = null where wx_days_plan = '/';
update task_static set wx_days_actual = null where wx_days_actual = '/';
update task_static set wx_memo = null where wx_memo = '/';
update task_static set wx_status = null where wx_status = '/';
update task_static set mgr_start = null where mgr_start = '/';
update task_static set mgr_start_actual = null where mgr_start_actual = '/';
update task_static set mgr_days_delay = null where mgr_days_delay = '/';
update task_static set mgr_days_plan = null where mgr_days_plan = '/';
update task_static set mgr_days_actual = null where mgr_days_actual = '/';
update task_static set mgr_memo = null where mgr_days_plan = '/';
update task_static set mgr_status = null where mgr_status = '/';
/*第2步:查询时间段内任务详情*/
set @start = 20190301, @end = 20190314;
SELECT task,ui_start,ui_start_actual,ui_days_delay,ui_days_plan,ui_days_actual,ui_memo,ui_status FROM `task_static` where ui_days_actual> 0 and ui_start_actual >= @start and ui_start_actual <= @end order by ui_start_actual asc;

SELECT task,back_start,back_start_actual,back_days_delay,back_days_plan,back_days_actual,back_memo,back_status FROM `task_static` where back_days_actual> 0 and  back_start_actual >= @start and back_start_actual <= @end order by back_start_actual asc;

select task,wx_start,wx_start_actual,wx_days_delay,wx_days_plan,wx_days_actual,wx_memo,wx_status FROM `task_static` where wx_days_actual> 0 and  wx_start_actual >= @start and wx_start_actual <= @end order by wx_start_actual asc;

select task,mgr_start,mgr_start_actual,mgr_days_delay,mgr_days_plan,mgr_days_actual,mgr_memo,mgr_status FROM `task_static` where mgr_days_actual> 0 and mgr_start_actual >= @start and mgr_start_actual <= @end order by mgr_start_actual asc;
/*1.1 插入式任务:个数、实际人天(周期内)*/

set @start = 20190301, @end = 20190314;
SELECT count(*), sum(ui_days_actual) FROM `task_static` where ui_days_plan = 0 and ui_start_actual >= @start and ui_start_actual <= @end;
SELECT count(*), sum(back_days_actual) FROM `task_static` where back_days_plan = 0 and back_start_actual >= @start and back_start_actual <= @end;
SELECT count(*), sum(wx_days_actual) FROM `task_static` where wx_days_plan = 0 and wx_start_actual >= @start and wx_start_actual <= @end;
SELECT count(*), sum(mgr_days_actual) FROM `task_static` where mgr_days_plan = 0 and mgr_start_actual >= @start and mgr_start_actual <= @end;

/*1.2 插入式任务:个数、实际人天(总计)*/
SELECT count(*), sum(ui_days_actual) FROM `task_static` where ui_days_plan = 0;
SELECT count(*), sum(back_days_actual) FROM `task_static` where back_days_plan = 0;
SELECT count(*), sum(wx_days_actual) FROM `task_static` where wx_days_plan = 0;
SELECT count(*), sum(mgr_days_actual) FROM `task_static` where mgr_days_plan = 0; 

/*1.3 放弃的计划任务:个数、计划人天(总计)*/
SELECT count(*), sum(ui_days_plan) FROM `task_static` where locate('放弃', ui_memo) = 1;
SELECT count(*), sum(back_days_plan) FROM `task_static` where locate('放弃', back_memo) = 1;
SELECT count(*), sum(wx_days_plan) FROM `task_static` where locate('放弃', wx_memo) = 1;
SELECT count(*), sum(mgr_days_plan) FROM `task_static` where locate('放弃', mgr_memo) = 1;

/*2.1 提前启动任务个数/延时启动任务个数(周期内)*/
set @start = 20180215, @end = 20190214;
select count(*) FROM `task_static` where ui_days_delay < 0 and ui_start_actual >= @start and ui_start_actual <= @end;
select count(*) FROM `task_static` where ui_days_delay > 0 and ui_start_actual >= @start and ui_start_actual <= @end;
select count(*) FROM `task_static` where back_days_delay < 0 and back_start_actual >= @start and back_start_actual <= @end;
select count(*) FROM `task_static` where back_days_delay > 0 and back_start_actual >= @start and back_start_actual <= @end;
select count(*) FROM `task_static` where wx_days_delay < 0 and wx_start_actual >= @start and wx_start_actual <= @end;
select count(*) FROM `task_static` where wx_days_delay > 0 and wx_start_actual >= @start and wx_start_actual <= @end;
select count(*) FROM `task_static` where mgr_days_delay < 0 and mgr_start_actual >= @start and mgr_start_actual <= @end;
select count(*) FROM `task_static` where mgr_days_delay > 0 and mgr_start_actual >= @start and mgr_start_actual <= @end;

/*2.2 提前启动任务个数/延时启动任务个数(总计)*/
select count(*) FROM `task_static` where ui_days_delay < 0;
select count(*) FROM `task_static` where ui_days_delay > 0;
select count(*) FROM `task_static` where back_days_delay < 0;
select count(*) FROM `task_static` where back_days_delay > 0;
select count(*) FROM `task_static` where wx_days_delay < 0;
select count(*) FROM `task_static` where wx_days_delay > 0;
select count(*) FROM `task_static` where mgr_days_delay < 0;
select count(*) FROM `task_static` where mgr_days_delay > 0;

/*3.1 超期完成:任务个数/超期人天(周期内)*/
set @start = 20180215, @end = 20190214;
select count(*), sum(ui_days_actual - ui_days_plan) FROM `task_static` where ui_days_plan > 0 and ui_days_actual > ui_days_plan and ui_start_actual >= @start and ui_start_actual <= @end;
select count(*), sum(back_days_actual - back_days_plan) FROM `task_static` where back_days_plan > 0 and back_days_actual > back_days_plan and back_start_actual >= @start and back_start_actual <= @end;
select count(*), sum(wx_days_actual - wx_days_plan) FROM `task_static` where wx_days_plan > 0 and wx_days_actual > wx_days_plan and wx_start_actual >= @start and wx_start_actual <= @end;
select count(*), sum(mgr_days_actual - mgr_days_plan) FROM `task_static` where mgr_days_plan > 0 and mgr_days_actual > mgr_days_plan and mgr_start_actual >= @start and mgr_start_actual <= @end;

/*3.2 超期完成:任务数/超期人天(总计)*/
select count(*), sum(ui_days_actual - ui_days_plan) FROM `task_static` where ui_days_plan > 0 and ui_days_actual > ui_days_plan;
select count(*), sum(back_days_actual - back_days_plan) FROM `task_static` where back_days_plan > 0 and back_days_actual > back_days_plan;
select count(*), sum(wx_days_actual - wx_days_plan) FROM `task_static` where wx_days_plan > 0 and wx_days_actual > wx_days_plan;
select count(*), sum(mgr_days_actual - mgr_days_plan) FROM `task_static` where mgr_days_plan > 0 and mgr_days_actual > mgr_days_plan;

/*哪些任务超期(暂时不统计)*/

select * FROM `task_static` where ui_days_plan > 0 and ui_days_actual > ui_days_plan and ui_start_actual >= @start and ui_start_actual <= @end;
select * FROM `task_static` where back_days_plan > 0 and back_days_actual > back_days_plan and back_start_actual >= @start and back_start_actual <= @end;
select * FROM `task_static` where wx_days_plan > 0 and wx_days_actual > wx_days_plan and wx_start_actual >= @start and wx_start_actual <= @end;
select * FROM `task_static` where mgr_days_plan > 0 and mgr_days_actual > mgr_days_plan and mgr_start_actual >= @start and mgr_start_actual <= @end;

/* 4.1 提前完成:任务数,提前人天(周期内)*/
set @start = 20180215, @end = 20190214;
select count(*), sum(ui_days_actual - ui_days_plan) FROM `task_static` where ui_days_actual < ui_days_plan and ui_start_actual >= @start and ui_start_actual <= @end;
select count(*), sum(back_days_actual - back_days_plan) FROM `task_static` where back_days_actual < back_days_plan and back_start_actual >= @start and back_start_actual <= @end;
select count(*), sum(wx_days_actual - wx_days_plan) FROM `task_static` where wx_days_actual < wx_days_plan and wx_start_actual >= @start and wx_start_actual <= @end;
select count(*), sum(mgr_days_actual - mgr_days_plan) FROM `task_static` where mgr_days_actual < mgr_days_plan and mgr_start_actual >= @start and mgr_start_actual <= @end;

/* 4.2 提前完成:任务数,提前人天(总计)*/
select count(*), sum(ui_days_actual - ui_days_plan) FROM `task_static` where ui_days_actual < ui_days_plan;
select count(*), sum(back_days_actual - back_days_plan) FROM `task_static` where back_days_actual < back_days_plan;
select count(*), sum(wx_days_actual - wx_days_plan) FROM `task_static` where wx_days_actual < wx_days_plan;
select count(*), sum(mgr_days_actual - mgr_days_plan) FROM `task_static` where mgr_days_actual < mgr_days_plan;

/* 5 ui计划人天/ui实际人天(含插入式任务人天)...*/
select sum(ui_days_plan), sum(ui_days_actual) FROM `task_static` where locate('放弃', ui_memo) is null;
select sum(back_days_plan), sum(back_days_actual) FROM `task_static` where locate('放弃', back_memo) is null;
select sum(wx_days_plan), sum(wx_days_actual) FROM `task_static` where locate('放弃', back_memo) is null;
select sum(mgr_days_plan),sum(mgr_days_actual) FROM `task_static` where locate('放弃', back_memo)is null;

/* 6 待完成的ui计划人天...*/
select sum(ui_days_plan) FROM `task_static` where (locate('放弃', ui_memo) is null) and (ui_days_actual is null);
select sum(back_days_plan) FROM `task_static` where (locate('放弃', back_memo) is null) and (back_days_actual is null);
select sum(wx_days_plan) FROM `task_static` where (locate('放弃', back_memo) is null) and (wx_days_actual is null);
select sum(mgr_days_plan) FROM `task_static` where (locate('放弃', back_memo) is null) and (mgr_days_actual is null);

/* 7 ui插入式任务人天...*/
select sum(ui_days_actual) FROM `task_static` where ui_days_plan=0;
select sum(back_days_actual) FROM `task_static` where back_days_plan=0;
select sum(wx_days_actual) FROM `task_static` where wx_days_plan=0;
select sum(mgr_days_actual) FROM `task_static` where mgr_days_plan=0;
  • 统计出来的数据

统计结果

  • 探究

从上述统计可以看到,每个任务的各类计划及其实施情况都可以看出来,另外计划的调整,比如废弃一些、新增一些,也能呈现出来。 这样,理想情况下,一般的项目,基本的人天是能预计和修正的,进度和趋势稍能直观明显一点。 理想之外,实际中要解决以下问题,解决的越彻底效果越好:

(1)任务是很难精确列出来的,特别是产品设计同步开展,因为设计的变数比较大,所以产品要先出来最好;

(2)任务在实施的时候,人天及其开始时间、连续时间,很难精确符合,有的是超期,而且这个超期有点难以衡量,有时大家的工作相互依赖,导致没有按预计时间开始,优先级和配合等因素导致中间还可能要暂停和穿插。所以解决这个的问题就是拆分的足够细,这又加大了第(1)点的难度;

(3)任务实施者主观上有自主性,因为(1)(2)的缘故,这种自主性看似也难以避免,但是在前面两点尽量好的情况下,实施者要配合项目的人天管理

(4)实施者在报告任务实施情况时,描述、颗粒度都要有讲究,尽量有依循和标准化,这需要第(1)点做的够好,直接引用其内容也可;

(5)更新也是一件需要非常细致且繁琐、高频且及时的事情,因此要求严格定时更新,这个还特别依赖第(4)点。

4.3 但是!但是!

excel来管理任务,模型基本是OK的,但还是有一些未解决的问题:

(1)当任务达到数百条时,是很难去查找和管理的

(2)目前是定量到小组,小组内的任务指派没有体现

(3)小伙伴在完成任务时,并不能直接提交和修改,这个上面讨论过,交给项目管理人员严格定时更新,有点浪费,且任务规模一上来,基本没法搞

(4)有些统计并不是最想要的,比如插入式任务,如何才能定义为插入式的?后续补充任务和提出新计划是项目的常态,只要能分段统计出创建的计划就可以了。什么时段创建任务,这个信息是缺漏的

(5)任务拆分的足够细,上面讨论过,但是细成什么样的呢?只要能记录每次执行这个任务的细节就好了,比如接口开发任务A,分步工作可能是:设计表、创建业务接口、测试接口等,细节可以分列,每项多少时间也分列,得出的是最后的整个任务执行时间。 这样在拆分任务上没有压力,在填写上也自然,甚至细节可以定性,比如:改bug,之前有错误完善,之前产品或程序考虑不完整重构,优化程序周边,与任务有关联的一些事情基础,与任务有关联的前期的事情测试,开发后验证新,主体开发等等,只要统计到位,什么问题都能呈现出来。

(6)管理员只负责大任务的创建,细节及状态由成员自己去维护,当完成时,管理员收到邮件或短信通知,岂不更美妙?

(7)关于统计,除了前文讨论过的,且达到了上述几条特点,还可以统计出:每时段新增任务个数、每个任务时间消耗详情。

这样,终极解决方案仍未得,开发一个小规模程序无可避免了。不过这个程序的人机交互快速找任务方便填写这些都是挑战。

5 简易任务管理程序

这个程序基本上是按第4章的模型来设计的,比第2章的产品更简化一点,落地性更好。

看一下表结构,功能就能知道个大概了:

CREATE TABLE `tm_user` (
  `id` bigint(20) unsigned NOT NULL,
  `name` varchar(20) DEFAULT NULL COMMENT '用户名',
  `password` varchar(30) DEFAULT NULL COMMENT '密码',
  `role` enum('观察员','组员','管理员') DEFAULT '观察员' COMMENT '用户角色',
  `work_group` enum('观察','测试','后端','前端','小程序','管理','设计') DEFAULT '前端' COMMENT '工作组',
  `phone` varchar(20) DEFAULT NULL COMMENT '用户手机号',
  `email` varchar(30) DEFAULT NULL COMMENT '邮箱',
  `is_email_verified` tinyint(1) DEFAULT NULL COMMENT '邮箱是否已经激活',
  `time_create` timestamp NULL DEFAULT NULL,
  `time_update` timestamp NULL DEFAULT NULL,
  `is_delete` tinyint(1) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户';
CREATE TABLE `tm_task` (
  `id` bigint(20) unsigned NOT NULL COMMENT '任务编号',
  `name_task` varchar(100) DEFAULT NULL COMMENT '任务名',
  `id_task_parent` bigint(20) unsigned DEFAULT NULL COMMENT '父任务编号,一般不超过3级',
  `level` tinyint(2) DEFAULT '1' COMMENT '级别,无父级则为1,其它依次加1',
  `is_discard` tinyint(1) DEFAULT '0' COMMENT '是否放弃,一般指最小级的升级,上级任务放弃则其子任务自动放弃',
  `priority` tinyint(2) unsigned DEFAULT '1' COMMENT '执行优先级,数字越小优先级越高',
  `time_create` timestamp NULL DEFAULT NULL,
  `time_update` timestamp NULL DEFAULT NULL,
  `is_delete` tinyint(4) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='任务,仅管理员可以写这个表';
CREATE TABLE `tm_task_plan` (
  `id` bigint(20) unsigned NOT NULL,
  `id_task` bigint(20) unsigned DEFAULT NULL COMMENT '关联的任务',
  `work_group` enum('测试','后端','前端','小程序','设计') DEFAULT '后端' COMMENT '任务执行组,一般用作工作性质分类。同一个任务可能有多个小组参与,比如接口开发,必须是后端开发 + 前端对接',
  `id_user_assigned` bigint(20) DEFAULT '0' COMMENT '指派给人,一般一个任务批判给一个人,未指定其值为0',
  `time_start_plan` timestamp NULL DEFAULT NULL,
  `hr_plan` float(4,2) DEFAULT NULL COMMENT '计划工作时,精度0.5小时,每天按8小时算',
  `memo` varchar(100) DEFAULT NULL COMMENT '备注,通常用于任务总结。如果是接口,这里可以备注一下接口编号',
  `is_discard` tinyint(1) DEFAULT '0' COMMENT '是否已经放弃',
  `status_task` enum('已完成','进行中','待开始') DEFAULT '待开始' COMMENT '任务进行状态。最初状态是待开始,如果有开始执行,则将任务状态修改为执行中,已完成表示主动提交为已完成。已完成任务不能再编辑属性,除了备注。执行者可以修改备注和任务状态两项属性',
  `id_user_create` bigint(20) unsigned DEFAULT NULL COMMENT '任务创建者',
  `id_user_update` bigint(20) unsigned DEFAULT NULL COMMENT '任务最后更新者',
  `time_create` timestamp NULL DEFAULT NULL,
  `time_update` timestamp NULL DEFAULT NULL,
  `is_delete` tinyint(1) DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='任务开展计划。放弃的除外';
CREATE TABLE `tm_task_executor` (
  `id` bigint(20) unsigned NOT NULL,
  `id_user` bigint(20) unsigned DEFAULT NULL COMMENT '工作任务执行者,虽然只指定到一个人,但可能有多个人参与',
  `id_task_plan` bigint(20) unsigned DEFAULT NULL COMMENT '关联的任务计划编号',
  `tag` enum('改bug,之前有错误','完善,之前产品或程序考虑不完整','重构,优化程序','周边,与任务有关联的一些事情','基础,与任务有关联的前期的事情','测试,开发后验证','新,主体开发') DEFAULT '测试,开发后验证' COMMENT '一般用于工作定性',
  `time_work_in` timestamp NULL DEFAULT NULL COMMENT '工作发生日,未特别指定一般默认为提交日',
  `hr_spend` float(2,2) DEFAULT '0.00' COMMENT '工作用时,精度0.5小时',
  `memo` varchar(100) DEFAULT NULL COMMENT '任务执行描述,可能分步执行,这里是分步信息描述',
  `time_create` timestamp NULL DEFAULT NULL,
  `time_update` timestamp NULL DEFAULT NULL,
  `is_delete` tinyint(1) DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='任务执行情况';

6 总结

在项目管理上,没有银弹,只有在实践中不断摸索,找到功能、时效与实际情况相当的做法。