核心数据结构:任务(Task)的定义、属性与状态机模型

好,咱们进入第二章。上一章我们聊了任务规划器能干什么,这一章得落地了——聊聊任务这个最核心的数据结构。

说白了,整个规划器就是围绕「任务」转的。你创建它、修改它、推进它、完成它。如果任务的定义没想清楚,后面所有功能都会跟着歪。我见过不少项目,一开始图省事,任务就存个标题和状态,结果做到一半发现缺字段,回头补数据,那叫一个痛苦。

2.1 任务的核心属性

一个任务,到底需要哪些字段?我个人的习惯是:够用就好,别过度设计。但有几个属性是跑不掉的。

属性 类型 说明
ID 字符串 / UUID 唯一标识,用于查找、关联、引用
名称 字符串 任务的简短标题,一眼能看懂
描述 文本 详细说明、验收标准、备注
优先级 枚举 P0 ~ P3 或 紧急/高/中/低
状态 枚举 待办、进行中、已完成、已取消
截止日期 日期 / 时间戳 最晚完成时间,可为空

嗯,这里要注意:ID 不要用自增整数。我在一个老项目里吃过亏,任务 ID 用自增主键,结果后来要做数据合并、跨系统同步,全乱套了。现在统一用 UUID,虽然长一点,但省心。

2.2 优先级:别让所有任务都是「紧急」

优先级这个字段,看着简单,用起来最容易出问题。你想想看,如果团队里每个人都把自己的任务标成「紧急」,那这个字段就废了。

我建议用四档制:

  • P0:阻塞性任务,不做就卡住整个流程
  • P1:重要任务,有明确截止日期
  • P2:常规任务,按计划推进
  • P3:低优先级,有空再做
小技巧: 在代码里,优先级建议用数字存储(0-3),排序时直接按数字排。显示的时候再映射成文字。这样排序逻辑干净,扩展也方便。

2.3 状态机模型:任务的生命周期

任务不是静止的。它从创建到完成,会经历一系列状态变化。这个变化路径,就是状态机。

我曾经见过一个项目,任务状态搞了十几个,什么「待评审」「评审中」「待测试」「测试中」「待发布」……看着很细,实际上没人记得住当前该用哪个状态。最后大家统一用「进行中」糊弄过去。

所以我的建议是:状态越少越好。核心就四个:

  • 待办(Todo):任务已创建,还没开始做
  • 进行中(In Progress):正在处理
  • 已完成(Done):任务完成,验收通过
  • 已取消(Cancelled):不再需要,或废弃

状态之间的转换,也不是随便跳的。比如「已完成」不能直接回到「待办」,得先变成「进行中」再重新处理。这个约束,必须在代码里写死。

核心原则: 状态转换必须是有向的、可控的。不要让用户随意乱跳,否则数据会变得不可追溯。

2.4 状态机流程图

下面这张图,就是任务状态机的标准流转。我建议你把它贴在项目文档里,团队所有人都能看懂。

任务状态机流转图 任务状态机模型 待办 进行中 已完成 已取消 开始处理 完成 重新打开 取消 取消 重新打开 注意 已完成 → 待办 不允许直接跳转
避坑指南: 我曾经在一个项目里允许「已完成」直接改回「待办」,结果统计报表里的完成率忽高忽低,被领导质疑数据造假。后来我强制要求:只有「进行中」才能回到「待办」,已完成的任务必须重新打开才能修改。这个规则,一定要在代码层面做校验。

2.5 代码实现:一个干净的任务模型

理论说完了,咱们看代码。下面是一个 TypeScript 定义,我尽量保持简洁:

// 任务状态枚举
enum TaskStatus {
  Todo = 'todo',
  InProgress = 'in_progress',
  Done = 'done',
  Cancelled = 'cancelled'
}

// 优先级枚举
enum Priority {
  P0 = 0,  // 紧急
  P1 = 1,  // 高
  P2 = 2,  // 中
  P3 = 3   // 低
}

// 任务接口
interface Task {
  id: string;           // UUID
  name: string;         // 任务名称
  description: string;  // 详细描述
  priority: Priority;   // 优先级
  status: TaskStatus;   // 当前状态
  dueDate: Date | null; // 截止日期,可为空
  createdAt: Date;      // 创建时间
  updatedAt: Date;      // 最后更新时间
}

// 状态转换规则
const validTransitions: Record<TaskStatus, TaskStatus[]> = {
  [TaskStatus.Todo]: [TaskStatus.InProgress, TaskStatus.Cancelled],
  [TaskStatus.InProgress]: [TaskStatus.Done, TaskStatus.Todo, TaskStatus.Cancelled],
  [TaskStatus.Done]: [TaskStatus.InProgress],  // 只能重新打开
  [TaskStatus.Cancelled]: [TaskStatus.Todo]    // 取消后可以恢复
};

// 校验状态转换
function canTransition(from: TaskStatus, to: TaskStatus): boolean {
  return validTransitions[from]?.includes(to) ?? false;
}

这段代码里,validTransitions 就是状态机的核心。你想想看,如果以后要加新状态,比如「审核中」,只需要在这里加一行映射就行,其他地方不用动。这就是状态机的好处——变化集中,影响可控。

2.6 截止日期:别让它变成摆设

截止日期这个字段,很多人就是填个日期,然后就不管了。但实际用起来,有几个细节要注意:

  • 允许为空:不是所有任务都有明确截止日期,别强制必填
  • 过期提醒:状态不是「已完成」且当前时间超过截止日期,应该标记为「已逾期」
  • 时间精度:大部分场景精确到天就够了,别搞到时分秒,徒增复杂度
我的习惯: 在任务列表里,我会把已逾期的任务用红色高亮显示。这样一打开页面,哪些任务需要优先处理,一目了然。这个功能实现起来很简单,但用户体验提升很明显。

2.7 小结

这一章我们聊了任务的核心数据结构。记住几个关键点:

  • ID 用 UUID,别用自增整数
  • 优先级分四档,别搞太复杂
  • 状态机四个状态,转换规则写死在代码里
  • 截止日期允许为空,但过期要能提醒