ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

任务管理流程最佳实践:用状态机替代CRUD,实现可追溯闭环

任务管理流程最佳实践:用状态机替代CRUD,实现可追溯闭环 每个业务系统做到中期几乎都会遇到同一个需求项目排期表里写着一行字——“18.新增任务管理流程”。这个需求刚出现时很多开发容易低估它。查一下数据库不就是加一张任务表再配上增删改查接口外加一个列表页面吗如果是这种理解半天就能交差。但真正上线后问题反而会集中爆发任务状态没人管、负责人换了没有记录、超期任务没人提醒、取消的任务混进统计口径里。到时候你才会意识到任务管理流程的关键不是“任务”而是“流程”。我在这篇文章里想给出一个更稳妥的判断任务管理流程的正确打开方式不是做 CRUD而是设计好一套状态机让每一次状态变化都可控、可追溯、可统计。文章会围绕一个最小可落地的任务管理流程展开覆盖状态机定义、数据库表设计、Spring Boot 后端实现、超期提醒、接口验证方法以及实际项目里的工程建议。读完你可以直接基于这套思路完成一个“创建任务 - 指派 - 处理 - 完成 - 归档”的闭环同时避开团队里最常见的几个坑。1. 这篇文章真正要解决的问题先说一个比较扎心的现象很多系统里的“任务管理”最后都会退化成一张只能“新增”和“改状态字段”的普通表。用户点一下“完成”前端就把 status 改成 3。再过一个月产品说需要统计这个月完成的任务数量你发现数据对不上因为作废的任务、重复打开的任务、被误操作改成已完成的任务都混在了一起。这就是典型的“只做了功能没设计流程”。任务管理流程真正要解决的是下面三类问题状态失序任务可以从待处理直接跳到已完成也可以从已完成再改回待处理中间没有任何流转规则也没有人知道上一次是谁改的。变更无痕负责人发生变化、截止时间被调整、任务中途暂停这些关键动作都没有历史记录。出了问题只能靠人脑回忆。闭环缺失任务创建之后没有提醒、没有超时机制、没有统计口径的兜底任务躺在列表里“假死”。所以我更倾向于把任务管理流程理解成一个“带规则的业务闭环”而不是一张通用字典表。这篇文章适合谁读后端开发、全栈开发、项目负责人以及对“业务系统功能模块设计”感兴趣的人。如果你正要在自己的系统里接一个“新增任务管理流程”的功能这篇文章可以帮你少走弯路。2. 任务管理流程的核心设计流程、角色与状态机在做任何编码之前最先要确定的是三件事谁在用、任务经历哪些状态、每个状态能流向哪里。这三件事决定了流程是清晰还是混乱。2.1 任务管理流程的典型角色一个轻量任务管理流程至少应该区分三类角色角色核心职责关键操作创建人提出任务内容明确预期结果和截止时间创建任务、填写描述、设定执行人执行人推动任务从“待处理”走向“已完成”开始处理、暂停、完成、补充备注管理员/系统兜底处理负责统计、超期提醒、归档取消异常任务、查看全量记录、触发提醒注意这里没有引入复杂的审批角色。这不是偷懒而是刻意控制复杂度。任务管理流程不等于审批流。如果业务没有明确要求“任务完成后需要经过某个经理审批”就不要为了流程而流程。一个简单的“创建人 - 执行人”双向模型已经能满足大部分团队任务管理场景。2.2 任务状态机定义状态机是任务管理流程里最重要的设计。很多项目的状态字段就是一个没有任何校验的字符串谁都能改改到哪里都行。状态机的价值在于它给状态变化划定了边界。我设计的最小状态集如下状态含义可流转到的状态说明TODO待处理DOING、CANCELED任务创建后的默认状态DOING进行中PAUSED、DONE、CANCELED执行人已开始处理PAUSED已暂停DOING、CANCELED遇到阻塞或调整DONE已完成DOING完成后发现需要重新处理CANCELED已取消无终态不可再流转这里有两个容易被忽略的细节。第一TODO 不能直接流转到 DONE。很多系统允许“直接完成”这个操作看起来很方便但它会破坏执行过程的可追溯性。最少也要经过“待处理 - 进行中 - 已完成”这样你才知道任务确实是有人真正处理过的。第二CANCELED 是终态。取消的任务不能重新打开。如果业务上允许“取消后再次执行”应该重新创建一条任务而不是把原任务激活。这样统计口径才干净。2.3 任务管理流程的完整闭环把状态机串起来完整的任务管理流程应该是这样的创建人新建任务填写标题、描述、执行人、截止时间任务进入 TODO 状态。执行人看到任务后开始处理任务进入 DOING 状态。如果中途遇到阻塞可暂停任务进入 PAUSED 状态之后可恢复为 DOING。执行人完成后任务进入 DONE 状态。如果创建人发现结果有问题可以重新打开任务回到 DOING。系统根据截止时间和当前状态自动扫描超期任务并提醒对应人员。这个闭环的核心价值是任务的生命周期透明化。任一时间点你都能回答“这个任务现在处于什么状态”“它是怎么一步步走到这里的”。3. 表结构设计与数据库脚本流程设计完成之后才开始设计数据库。这里有一个很重要的设计原则业务字段与流程字段分离历史记录独立存储。也就是说任务表只负责存“任务本身的当前事实”比如标题、负责人、当前状态、截止时间。而“跟谁、在什么时候、把状态从 A 改成了 B”这类过程信息必须存到独立的流转记录表里。这样做的好处非常明显以后无论审计、统计、还是排查问题都有完整的痕迹可用。3.1 任务主表设计字段类型说明idbigint主键task_novarchar(32)业务编号唯一titlevarchar(200)任务标题descriptiontext任务描述task_typevarchar(32)任务类型默认 GENERALpriorityvarchar(16)优先级HIGH/MEDIUM/LOWstatusvarchar(32)当前状态默认 TODOcreator_idvarchar(64)创建人 IDassignee_idvarchar(64)执行人 IDproject_idvarchar(64)所属项目 ID可为空due_timedatetime截止时间start_timedatetime开始处理时间finish_timedatetime完成时间versionint乐观锁版本号deletedtinyint逻辑删除标志created_atdatetime创建时间updated_atdatetime更新时间3.2 任务流转记录表设计字段类型说明idbigint主键task_idbigint任务 IDfrom_statusvarchar(32)变更前状态to_statusvarchar(32)变更后状态operator_idvarchar(64)操作人 IDremarkvarchar(500)变更备注created_atdatetime记录时间3.3 数据库初始化脚本下面是一份可直接执行的 MySQL 初始化脚本。注意索引设计任务编号唯一索引、执行人 状态组合索引、任务 ID 索引。这些索引直接对应高频查询场景不要省略。-- 任务表 CREATE TABLE task ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, task_no VARCHAR(32) NOT NULL COMMENT 任务编号, title VARCHAR(200) NOT NULL COMMENT 任务标题, description TEXT COMMENT 任务描述, task_type VARCHAR(32) NOT NULL DEFAULT GENERAL COMMENT 任务类型, priority VARCHAR(16) NOT NULL DEFAULT MEDIUM COMMENT 优先级, status VARCHAR(32) NOT NULL DEFAULT TODO COMMENT 当前状态, creator_id VARCHAR(64) NOT NULL COMMENT 创建人ID, assignee_id VARCHAR(64) DEFAULT NULL COMMENT 执行人ID, project_id VARCHAR(64) DEFAULT NULL COMMENT 所属项目ID, due_time DATETIME DEFAULT NULL COMMENT 截止时间, start_time DATETIME DEFAULT NULL COMMENT 开始处理时间, finish_time DATETIME DEFAULT NULL COMMENT 完成时间, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, deleted TINYINT NOT NULL DEFAULT 0 COMMENT 逻辑删除标志 0正常 1删除, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_task_no (task_no), KEY idx_assignee_status (assignee_id, status), KEY idx_due_time_status (due_time, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT任务表; -- 任务流转记录表 CREATE TABLE task_flow_record ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, task_id BIGINT NOT NULL COMMENT 任务ID, from_status VARCHAR(32) DEFAULT NULL COMMENT 变更前状态, to_status VARCHAR(32) NOT NULL COMMENT 变更后状态, operator_id VARCHAR(64) NOT NULL COMMENT 操作人ID, remark VARCHAR(500) DEFAULT NULL COMMENT 备注, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 记录时间, PRIMARY KEY (id), KEY idx_task_id (task_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT任务流转记录表;这里真正容易踩坑的地方是 deleted 字段和唯一索引的冲突。逻辑删除后如果再次插入相同 task_no会因唯一索引冲突导致失败。所以在生成 task_no 时一定不要用固定业务编号推荐使用“前缀 时间戳 随机数”的方式。4. 环境准备与项目结构任务管理流程作为业务系统的一个模块很适合用主流 Java 技术栈快速落地。下面以 Spring Boot MyBatis-Plus MySQL 为例演示。4.1 技术选型与版本说明本文演示使用的技术组合如下JDK 1.8 或 11Spring Boot 2.7.xMyBatis-Plus 3.5.xMySQL 8.0如果你所在团队已经使用 Spring Boot 3.x代码整体可以复用但需要注意两个地方包名从 javax 迁移为 jakarta以及 MyBatis-Plus 需要选择适配 Spring Boot 3 的 starter 版本。我的建议是不要为了“追新”而盲目升级。任务管理模块本身没有强烈的版本需求应该以团队现有的技术基线为准。4.2 Maven 依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency4.3 application.yml 配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/task_demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0注意 MyBatis-Plus 的逻辑删除配置。上面这段配置的作用是所有查询会自动带上deleted 0条件所有更新会自动带上deleted 0条件删除操作变成逻辑更新。这是生产环境的基本要求不要在任务表上做物理删除。4.4 项目目录结构src/main/java/com/example/taskdemo ├── TaskDemoApplication.java ├── common │ ├── BizException.java │ └── Result.java ├── entity │ ├── Task.java │ └── TaskFlowRecord.java ├── enums │ └── TaskStatus.java ├── mapper │ ├── TaskMapper.java │ └── TaskFlowRecordMapper.java ├── service │ ├── TaskService.java │ └── impl │ └── TaskServiceImpl.java └── task └── TaskExpireRemindTask.java这个结构比较清晰。枚举、实体、Mapper、Service、定时任务分层明确后续如果要加注释、附件、子任务只需要在对应层扩展即可。5. 核心代码实现从创建任务到状态流转这一节是实现的重点。我不会贴全部代码而是挑出最核心的四个部分状态枚举、创建任务、状态流转、超期提醒。5.1 任务状态枚举// 文件路径src/main/java/com/example/taskdemo/enums/TaskStatus.java package com.example.taskdemo.enums; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public enum TaskStatus { TODO(待处理), DOING(进行中), PAUSED(已暂停), DONE(已完成), CANCELED(已取消); private final String desc; TaskStatus(String desc) { this.desc desc; } public String getDesc() { return desc; } // 定义状态机的流转规则 private static final MapTaskStatus, ListTaskStatus TRANSITIONS new ConcurrentHashMap(); static { TRANSITIONS.put(TODO, Arrays.asList(DOING, CANCELED)); TRANSITIONS.put(DOING, Arrays.asList(PAUSED, DONE, CANCELED)); TRANSITIONS.put(PAUSED, Arrays.asList(DOING, CANCELED)); TRANSITIONS.put(DONE, Collections.singletonList(DOING)); TRANSITIONS.put(CANCELED, Collections.emptyList()); } public boolean canTransferTo(TaskStatus target) { return TRANSITIONS.get(this).contains(target); } public static TaskStatus fromCode(String code) { for (TaskStatus status : values()) { if (status.name().equalsIgnoreCase(code)) { return status; } } throw new IllegalArgumentException(未知状态: code); } }这段代码把状态机的校验逻辑收敛到了一个地方。以后如果产品说要增加“重新打开”“待验收”等状态只需要修改这个枚举而不需要改动 Service 里的业务逻辑。这就是状态机的核心价值把规则从业务代码中抽离出来。5.2 创建任务创建任务的接口负责两件事生成业务编号、初始化任务状态。任务创建的入参可以用 DTO这里为了控制代码篇幅直接使用实体接收关键参数。// 文件路径src/main/java/com/example/taskdemo/service/impl/TaskServiceImpl.java package com.example.taskdemo.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.taskdemo.common.BizException; import com.example.taskdemo.entity.Task; import com.example.taskdemo.enums.TaskStatus; import com.example.taskdemo.mapper.TaskMapper; import com.example.taskdemo.service.TaskService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.UUID; Service public class TaskServiceImpl extends ServiceImplTaskMapper, Task implements TaskService { Override Transactional(rollbackFor Exception.class) public Task createTask(Task task) { if (!StringUtils.hasText(task.getTitle())) { throw new BizException(任务标题不能为空); } if (task.getDueTime() ! null task.getDueTime().isBefore(LocalDateTime.now())) { throw new BizException(截止时间不能早于当前时间); } // 生成业务编号避免与逻辑删除后的唯一索引冲突 String taskNo T LocalDateTime.now().format(DateTimeFormatter.ofPattern(yyyyMMddHHmmss)) UUID.randomUUID().toString().substring(0, 6).toUpperCase(); task.setTaskNo(taskNo); task.setStatus(TaskStatus.TODO.name()); task.setDeleted(0); task.setVersion(0); this.save(task); return task; } }这里有个细节值得注意创建任务时不设置 start_time 和 finish_time。这两个字段应该在“真正开始处理”和“真正完成”的时候赋值而不是创建时拍脑袋填。这样后续做耗时统计时数据才准确。5.3 状态流转与乐观锁处理状态流转是这个模块里最容易出并发问题的接口。如果两个请求同时把任务从 DOING 改成 DONE或者一个改成 PAUSED、一个改成 DONE数据库最终状态会被后提交的请求覆盖。解决方案是乐观锁。更新时带上 version 条件如果影响行数为 0说明任务已经被其他请求修改需要让用户刷新后重新操作。// 文件路径src/main/java/com/example/taskdemo/service/impl/TaskServiceImpl.java Override Transactional(rollbackFor Exception.class) public void transition(Long taskId, TaskStatus targetStatus, String operatorId, String remark, Integer version) { Task task this.getById(taskId); if (task null || task.getDeleted() 1) { throw new BizException(任务不存在); } TaskStatus currentStatus TaskStatus.fromCode(task.getStatus()); if (!currentStatus.canTransferTo(targetStatus)) { throw new BizException(非法状态流转: currentStatus.name() - targetStatus.name()); } Task update new Task(); update.setId(task.getId()); update.setStatus(targetStatus.name()); update.setVersion(version 1); // 记录开始处理时间和完成时间 if (targetStatus TaskStatus.DOING currentStatus ! TaskStatus.PAUSED) { update.setStartTime(LocalDateTime.now()); } if (targetStatus TaskStatus.DONE) { update.setFinishTime(LocalDateTime.now()); } // 乐观锁更新只有 version 匹配时才会更新成功 boolean updated this.update(update, new LambdaUpdateWrapperTask() .eq(Task::getId, task.getId()) .eq(Task::getVersion, version) .eq(Task::getStatus, task.getStatus())); if (!updated) { throw new BizException(任务已被他人修改请刷新后重试); } // 记录流转历史 TaskFlowRecord record new TaskFlowRecord(); record.setTaskId(task.getId()); record.setFromStatus(currentStatus.name()); record.setToStatus(targetStatus.name()); record.setOperatorId(operatorId); record.setRemark(remark); taskFlowRecordMapper.insert(record); }这里真正容易踩坑的是LambdaUpdateWrapper中的条件。很多初学者只带上id和version忘了带status。如果没有status条件极端场景下仍可能出现“前端看到的是 DOING实际已经被另外一个人改成 PAUSED然后更新成功”的情况。带上原状态条件才能保证 update 操作基于“操作者所见版本”进行。5.4 超期任务提醒超期提醒是任务管理流程闭环中不可缺少的环节。最简单的方式是使用 Spring 自带的定时任务每分钟扫描一次“未完成且已超期”的任务。// 文件路径src/main/java/com/example/taskdemo/task/TaskExpireRemindTask.java package com.example.taskdemo.task; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.example.taskdemo.entity.Task; import com.example.taskdemo.enums.TaskStatus; import com.example.taskdemo.mapper.TaskMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.time.LocalDateTime; import java.util.Arrays; import java.util.List; Component public class TaskExpireRemindTask { private static final Logger log LoggerFactory.getLogger(TaskExpireRemindTask.class); Resource private TaskMapper taskMapper; /** * 每 5 分钟扫描一次超期未完成任务 */ Scheduled(cron 0 */5 * * * ?) public void remindExpiredTasks() { ListString activeStatusList Arrays.asList( TaskStatus.TODO.name(), TaskStatus.DOING.name(), TaskStatus.PAUSED.name() ); ListTask expiredTasks taskMapper.selectList(new LambdaQueryWrapperTask() .in(Task::getStatus, activeStatusList) .lt(Task::getDueTime, LocalDateTime.now())); for (Task task : expiredTasks) { // 这里替换为真实的站内信、短信、邮件或 IM 通知 log.warn(任务超时提醒, taskNo{}, title{}, assigneeId{}, dueTime{}, task.getTaskNo(), task.getTitle(), task.getAssigneeId(), task.getDueTime()); } } }提醒逻辑不要直接写在任务状态流转的 Service 里。主链路是“创建任务 - 状态流转”提醒属于旁路逻辑。如果提醒服务挂了不能影响任务主流程。定时扫描这种方式虽然有一定的延迟但对于大多数内部任务管理场景已经足够。不要忘记在启动类上加EnableScheduling注解否则定时任务不会生效。// 文件路径src/main/java/com/example/taskdemo/TaskDemoApplication.java package com.example.taskdemo; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableScheduling; EnableScheduling MapperScan(com.example.taskdemo.mapper) SpringBootApplication public class TaskDemoApplication { public static void main(String[] args) { SpringApplication.run(TaskDemoApplication.class, args); } }6. 运行结果与效果验证代码写完如何快速验证整个流程是好的我建议不要先打开前端页面而是用最朴素的方式直接调用后端接口验证状态机的每一跳是否符合预期。6.1 启动项目mvn spring-boot:run启动成功之后日志中会出现 Spring Boot 启动 Banner并且端口监听在 8080。6.2 创建任务curl -X POST http://localhost:8080/api/tasks \ -H Content-Type: application/json \ -d { title: 重构订单状态机, description: 将订单模块的散落状态判断统一收敛到状态机, assigneeId: u_1024, dueTime: 2025-12-31 18:00:00 }预期返回结果类似{ code: 200, message: success, data: { id: 1, taskNo: T20251208120000A1B2C3, status: TODO } }如果返回的任务编号出现过短或重复需要检查 taskNo 生成逻辑。6.3 执行人开始处理curl -X POST http://localhost:8080/api/tasks/1/transition \ -H Content-Type: application/json \ -d { targetStatus: DOING, operatorId: u_1024, remark: 开始处理, version: 0 }预期返回成功后再尝试一个非法流转比如把 TODO 状态直接改成 DONEcurl -X POST http://localhost:8080/api/tasks/1/transition \ -H Content-Type: application/json \ -d { targetStatus: DONE, operatorId: u_1024, remark: 想直接完成, version: 1 }此时接口应该返回业务异常提示“非法状态流转”。这一步很重要它说明状态机已经生效不是谁想怎么改就怎么改。6.4 查询流转记录curl http://localhost:8080/api/tasks/1/records预期返回两条记录{ code: 200, message: success, data: [ { fromStatus: TODO, toStatus: DOING, operatorId: u_1024, remark: 开始处理 } ] }如果记录为空重点排查taskFlowRecordMapper.insert是否在事务里执行以及事务是否被异常拦截回滚。7. 常见问题与排查方法任务管理流程看起来简单实际开发中会遇到不少问题。下面整理了几个最高频的场景。问题现象可能原因排查方式解决方案状态流转提示“非法状态流转”前端按钮与后端状态机定义不一致查看当前任务 status对比状态机规则以后端状态机为准前端按钮根据接口返回渲染并发点击后状态被覆盖更新时未使用乐观锁打印 update 影响行数UPDATE 条件必须包含 id、version、原 status定时提醒重复发送多实例部署导致多个节点同时扫描同时查看多台服务器日志引入分布式锁或使用 xxl-job 等调度平台已完成任务在统计中重复计算统计 SQL 没有过滤 CANCELED 和逻辑删除数据查看统计 SQL 条件统一在统计入口维护“有效任务”条件视图任务删除后流转记录错乱使用了物理删除检查 delete SQL改为逻辑删除deleted 字段置 1任务编号生成重复业务编号过于规则或未加随机数查看 task_no 生成逻辑使用“时间戳 随机数”组合更新任务时影响行数为 0版本号来自旧页面让用户刷新后重新操作前端提示“任务已被他人修改”这里最需要强调的一点遇到任何状态类问题时先查流转记录表再猜业务逻辑。因为流转记录是唯一可信的过程证据。如果记录表里没有数据说明业务代码里很可能直接 update 了 status 字段绕过了统一入口。这也是为什么我们要坚持“所有状态变更必须走 transition 接口”的原因。8. 最佳实践与工程建议代码能跑通只是第一步。放到生产环境里任务管理流程还需要遵循一些工程约束。8.1 状态字段使用字符串不要使用无意义数字很多老系统喜欢用status1、status2这种表示方式结果半年后没人记得 1 到底代表待处理还是已完成。使用TODO、DOING、DONE这样的字符串日志里一查就懂排查问题成本会低很多。8.2 所有状态变更必须有权限控制创建人可以把任务重新打开执行人可以开始处理管理员可以取消异常任务。这些动作不应该对所有用户开放。建议在 transition 层增加操作人校验至少校验“任务执行人才能流转任务”。这不是复杂化而是任务管理流程的基本安全边界。8.3 任务历史记录全量保留任务流转记录不应该设置定期清理策略至少也要保留到任务归档后的一个自然年。任务管理流程的价值很大程度上体现在“追责”和“复盘”上历史记录一旦丢失整个流程的闭环就断了。8.4 统计口径要显式定义做任务完成率、超时率统计时不要直接对 task 表count要先把有效状态集合固定下来。比如“任务完成率”的分母是TODO DOING PAUSED DONE分子是DONE“取消率”单独统计 CANCELED。口径在代码里用枚举集合定义而不是散落在各条 SQL 里。8.5 不要为了任务管理引入重型流程引擎任务管理流程和审批流、工作流是两个不同量级的问题。如果只是“指派、执行、完成、取消”状态机完全够用根本不需要引入 Flowable、Activiti 这类流程引擎。只有当你明确遇到以下需求时才应该考虑流程引擎多级审批、复杂条件分支、会签或签、动态流程编排、流程版本管理。否则维护引擎的成本会远超任务管理模块本身的价值。8.6 接口设计保持版本兼容任务管理流程上线后大概率会持续演进。新增状态、新增字段、新增提醒方式会不断发生。建议所有对外接口从第一天就带上版本信息例如/api/v1/tasks因为状态机的变化很容易导致前端、App、第三方系统的联动改动没有版本约束会非常被动。9. 总结与下一步实践“18.新增任务管理流程”这个需求成熟的做法是把它当作一个可以持续演进的业务模块而不只是一张新表。这篇文章最核心的结论有三点状态机是任务管理流程的灵魂流转留痕是不可突破的底线提醒与统计是流程闭环的加分项。如果你接下来的任务正好是“新增任务管理流程”我的建议是先别急着写代码。花两个小时和产品确认清楚任务会不会被重新打开取消后的任务能不能再次执行超时之后由谁负责这三个问题答案不同状态机的设计就完全不同。建议你先按照本文方式跑通一个最小闭环数据库建表Spring Boot 项目落状态机写一个 transition 接口再补一个定时提醒脚本。跑通之后再根据团队实际需要逐步加上评论、附件、子任务、看板视图这些增强功能。等这些做好之后如果业务真的出现了审批需求再考虑引入流程引擎也不迟。
返回列表