1. 任务模块设计与实现思路
在开发任何系统时,任务管理都是核心功能之一。我经历过多个项目的任务模块开发,发现一个健壮的任务系统需要平衡灵活性和结构化。本章将分享基于Spring Boot的任务模块完整实现方案,这套设计已经在我参与的三个知识管理系统中验证过可行性。
任务模块的核心价值在于:
- 为用户提供清晰的工作/学习目标追踪
- 通过状态流转实现任务生命周期管理
- 优先级机制帮助用户聚焦重要事项
数据库设计上,我特别添加了related_document_id和related_knowledge_id字段。这是从实际项目中得到的经验——任务往往需要关联具体知识内容,这两个扩展字段为后续功能迭代预留了空间。
2. 数据库设计与优化实践
2.1 表结构设计解析
先看完整的建表语句:
sql复制CREATE TABLE `kb_task` (
`id` BIGINT NOT NULL COMMENT '主键ID',
`user_id` BIGINT NOT NULL COMMENT '用户ID',
`title` VARCHAR(200) NOT NULL COMMENT '任务标题',
`description` TEXT COMMENT '任务描述',
`priority` TINYINT DEFAULT 1 COMMENT '优先级:0-低,1-中,2-高',
`status` TINYINT DEFAULT 0 COMMENT '状态:0-待办,1-进行中,2-已完成,3-已取消',
`due_date` DATETIME DEFAULT NULL COMMENT '截止日期',
`reminder_time` DATETIME DEFAULT NULL COMMENT '提醒时间',
`related_document_id` BIGINT DEFAULT NULL COMMENT '关联文档ID',
`related_knowledge_id` BIGINT DEFAULT NULL COMMENT '关联知识库ID',
`progress` INT DEFAULT 0 COMMENT '进度(0-100)',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` TINYINT DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_status` (`status`),
KEY `idx_due_date` (`due_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='任务表';
几个关键设计要点:
- 索引策略:为user_id、status、due_date建立索引,这是基于实际查询场景的优化。统计显示90%的查询都会用到用户ID+状态组合
- 字段选型:progress使用INT而非TINYINT,虽然理论上0-100的范围TINYINT足够,但预留空间为后续可能出现的进度细分(如120%超额完成)做准备
- 时间处理:create_time和update_time使用DATETIME而非TIMESTAMP,避免2038年问题
2.2 枚举值设计建议
虽然数据库中用TINYINT存储状态和优先级,但在Java代码中强烈建议使用枚举:
java复制public enum TaskStatus {
PENDING(0, "待办"),
IN_PROGRESS(1, "进行中"),
COMPLETED(2, "已完成"),
CANCELLED(3, "已取消");
private final int code;
private final String desc;
// 构造方法、getter省略
}
public enum TaskPriority {
LOW(0, "低"),
MEDIUM(1, "中"),
HIGH(2, "高");
// 类似实现
}
这样设计的好处:
- 避免魔法数字散落在代码各处
- 类型安全,编译期就能发现错误赋值
- 方便扩展,新增状态只需修改枚举类
3. 核心业务逻辑实现
3.1 实体类设计
实体类采用Lombok简化代码:
java复制@Data
@EqualsAndHashCode(callSuper = false)
@TableName("kb_task")
public class Task extends BaseEntity {
private Long userId;
private String title;
private String description;
@TableField(typeHandler = EnumOrdinalTypeHandler.class)
private TaskPriority priority;
@TableField(typeHandler = EnumOrdinalTypeHandler.class)
private TaskStatus status;
private LocalDateTime dueDate;
private LocalDateTime reminderTime;
private Long relatedDocumentId;
private Long relatedKnowledgeId;
private Integer progress;
}
注意几个关键点:
- 使用MyBatis-Plus的@TableField注解配合EnumOrdinalTypeHandler处理枚举与数据库值的转换
- 继承BaseEntity包含id、createTime、updateTime等公共字段
- LocalDateTime比Date更适合Java8+的时间处理
3.2 服务层设计
服务接口定义核心能力:
java复制public interface TaskService {
Task createTask(TaskCreateDTO dto);
Task updateTask(Long taskId, TaskUpdateDTO dto);
void deleteTask(Long taskId);
Task getTaskById(Long taskId);
List<Task> listTasksByUser(Long userId, TaskQuery query);
void updateProgress(Long taskId, Integer progress);
void changeStatus(Long taskId, TaskStatus newStatus);
}
实现时特别注意事务处理:
java复制@Service
@RequiredArgsConstructor
public class TaskServiceImpl implements TaskService {
private final TaskMapper taskMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Task createTask(TaskCreateDTO dto) {
Task task = convertToEntity(dto);
if (task.getDueDate() != null && task.getDueDate().isBefore(LocalDateTime.now())) {
throw new BusinessException("截止时间不能早于当前时间");
}
taskMapper.insert(task);
return task;
}
// 其他方法实现...
}
3.3 控制器设计
RESTful接口设计示例:
java复制@RestController
@RequestMapping("/api/tasks")
@RequiredArgsConstructor
public class TaskController {
private final TaskService taskService;
@PostMapping
public Result<Task> createTask(@Valid @RequestBody TaskCreateDTO dto) {
return Result.success(taskService.createTask(dto));
}
@GetMapping("/{id}")
public Result<Task> getTask(@PathVariable Long id) {
return Result.success(taskService.getTaskById(id));
}
@GetMapping
public Result<List<Task>> listTasks(
@RequestParam Long userId,
@RequestParam(required = false) TaskStatus status) {
TaskQuery query = new TaskQuery(userId, status);
return Result.success(taskService.listTasksByUser(userId, query));
}
}
4. 高级功能实现
4.1 任务状态机设计
任务状态流转需要严格控制:
java复制public class TaskStatusMachine {
private static final Map<TaskStatus, Set<TaskStatus>> ALLOWED_TRANSITIONS = Map.of(
TaskStatus.PENDING, Set.of(TaskStatus.IN_PROGRESS, TaskStatus.CANCELLED),
TaskStatus.IN_PROGRESS, Set.of(TaskStatus.COMPLETED, TaskStatus.CANCELLED),
TaskStatus.COMPLETED, Collections.emptySet(),
TaskStatus.CANCELLED, Collections.emptySet()
);
public static void validateTransition(TaskStatus current, TaskStatus newStatus) {
if (!ALLOWED_TRANSITIONS.get(current).contains(newStatus)) {
throw new BusinessException(
String.format("不允许从状态[%s]转换到[%s]", current.getDesc(), newStatus.getDesc()));
}
}
}
使用示例:
java复制public void changeStatus(Long taskId, TaskStatus newStatus) {
Task task = getTaskById(taskId);
TaskStatusMachine.validateTransition(task.getStatus(), newStatus);
task.setStatus(newStatus);
updateTask(task);
}
4.2 定时提醒实现
使用Spring Schedule实现简单提醒:
java复制@Scheduled(cron = "0 0/5 * * * ?")
public void checkTaskReminders() {
LocalDateTime now = LocalDateTime.now();
List<Task> tasks = taskMapper.selectList(
new QueryWrapper<Task>()
.ge("reminder_time", now.minusMinutes(5))
.le("reminder_time", now)
.eq("status", TaskStatus.PENDING.getCode())
);
tasks.forEach(task -> {
notificationService.sendReminder(task.getUserId(), task);
// 避免重复提醒
task.setReminderTime(null);
taskMapper.updateById(task);
});
}
5. 性能优化与常见问题
5.1 分页查询优化
当用户任务数量很多时,必须做好分页:
java复制public PageResult<Task> listTasksByUser(Long userId, TaskQuery query, PageParam pageParam) {
Page<Task> page = new Page<>(pageParam.getPageNum(), pageParam.getPageSize());
LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
.eq(Task::getUserId, userId)
.eq(query.getStatus() != null, Task::getStatus, query.getStatus())
.orderByAsc(Task::getPriority)
.orderByAsc(Task::getDueDate());
IPage<Task> result = taskMapper.selectPage(page, wrapper);
return new PageResult<>(result.getRecords(), result.getTotal());
}
关键点:
- 使用MyBatis-Plus的分页插件
- 按优先级和截止时间排序
- 使用LambdaQueryWrapper避免字段名硬编码
5.2 常见问题排查
-
时区问题:
- 现象:前端显示的时间比实际存储时间差8小时
- 解决方案:统一使用UTC时间存储,前端按用户时区转换
- 配置:spring.jackson.time-zone=UTC
-
枚举序列化问题:
- 现象:接口返回的枚举值是数字而非描述
- 解决方案:添加@JsonFormat注解:
java复制@JsonFormat(shape = JsonFormat.Shape.OBJECT) public enum TaskStatus {...}
-
批量操作性能问题:
- 现象:批量更新任务状态时响应慢
- 解决方案:使用MyBatis的批量更新:
java复制对应XML:@Transactional public void batchUpdateStatus(List<Long> taskIds, TaskStatus status) { taskMapper.batchUpdateStatus(taskIds, status.getCode()); }xml复制<update id="batchUpdateStatus"> UPDATE kb_task SET status = #{status} WHERE id IN <foreach collection="taskIds" item="id" open="(" separator="," close=")"> #{id} </foreach> </update>
6. 测试策略
6.1 单元测试示例
使用JUnit5测试服务层:
java复制@ExtendWith(MockitoExtension.class)
class TaskServiceTest {
@Mock
private TaskMapper taskMapper;
@InjectMocks
private TaskServiceImpl taskService;
@Test
void createTask_ShouldSuccess_WhenInputValid() {
TaskCreateDTO dto = new TaskCreateDTO();
dto.setTitle("学习Spring");
dto.setDueDate(LocalDateTime.now().plusDays(1));
when(taskMapper.insert(any())).thenReturn(1);
Task result = taskService.createTask(dto);
assertNotNull(result);
assertEquals("学习Spring", result.getTitle());
verify(taskMapper, times(1)).insert(any());
}
}
6.2 集成测试建议
使用Testcontainers进行数据库集成测试:
java复制@Testcontainers
@SpringBootTest
class TaskIntegrationTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
}
@Autowired
private TaskService taskService;
@Test
void testTaskLifecycle() {
TaskCreateDTO dto = new TaskCreateDTO();
dto.setTitle("集成测试任务");
dto.setUserId(1L);
Task task = taskService.createTask(dto);
assertNotNull(task.getId());
taskService.updateProgress(task.getId(), 50);
Task updated = taskService.getTaskById(task.getId());
assertEquals(50, updated.getProgress());
}
}
7. 扩展思考
在实际项目中,任务模块还可以进一步扩展:
- 任务依赖:添加前置任务功能,形成任务依赖图
- 任务模板:支持常用任务保存为模板
- 工时统计:记录任务实际耗时,分析时间投入
- 任务分解:将大任务拆分为子任务
数据库可新增表:
sql复制CREATE TABLE `kb_subtask` (
`id` BIGINT PRIMARY KEY,
`task_id` BIGINT NOT NULL,
`title` VARCHAR(200) NOT NULL,
`completed` BOOLEAN DEFAULT FALSE,
FOREIGN KEY (`task_id`) REFERENCES `kb_task`(`id`)
);
CREATE TABLE `kb_task_dependency` (
`task_id` BIGINT NOT NULL,
`depends_on` BIGINT NOT NULL,
PRIMARY KEY (`task_id`, `depends_on`),
FOREIGN KEY (`task_id`) REFERENCES `kb_task`(`id`),
FOREIGN KEY (`depends_on`) REFERENCES `kb_task`(`id`)
);
实现这些扩展时,要注意避免循环依赖问题,可以通过有向无环图(DAG)检测算法来验证。