一个完整的待办事项 Todo List (java可上线)

这是一个最小可行性生产级" (Minimum Viable Production-Ready) 的 Java 项目模板。

它虽然功能简单(一个待办事项 Todo List API),但包含了上线所需的所有核心骨架:统一返回、全局异常、参数校验、日志规范、Docker 部署

你可以直接复制这个结构,作为你未来所有项目的起点。

一.项目结构

bash 复制代码
todo-api/
├── src/main/java/com/example/todo/
│   ├── common/             # 公共模块
│   │   ├── Result.java     # 统一响应封装
│   │   └── BusinessException.java # 自定义业务异常
│   ├── config/             # 配置类
│   │   └── GlobalExceptionHandler.java # 全局异常处理
│   ├── controller/         # 控制层
│   │   ── TodoController.java
│   ├── service/            # 业务层
│   │   ├── TodoService.java
│   │   ── impl/TodoServiceImpl.java
│   ├── mapper/             # 数据访问层
│   │   └── TodoMapper.java
│   ├── entity/             # 数据库实体
│   │   └── Todo.java
│   ├── dto/                # 请求参数对象
│   │   └── CreateTodoDTO.java
│   ── TodoApplication.java # 启动类
── src/main/resources/
│   ├── application.yml     # 配置文件
│   └── schema.sql          # 建表语句
├── Dockerfile              # 镜像构建文件
└── pom.xml                 # Maven 依赖

二. 核心代码实现

A. pom.xml (关键依赖)

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>todo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- MyBatis Plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.7</version>
        </dependency>

        <!-- MySQL Driver -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- Validation -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!-- Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

📂 src/main/java/com/example/todo/

Main.java(启动类)

java 复制代码
package com.example.todo;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.example.todo.mapper") // 扫描 Mapper 接口
public class Main {
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
}

B. common/Result.java (统一返回格式)

这是生产项目的门面,前端只认这个格式。

Result.java(统一响应封装)

java 复制代码
package com.example.todo.common;

import lombok.Data;

@Data
public class Result<T> {
    private int code;
    private String message;
    private T data;

    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMessage("success");
        result.setData(data);
        return result;
    }

    public static <T> Result<T> error(int code, String message) {
        Result<T> result = new Result<>();
        result.setCode(code);
        result.setMessage(message);
        return result;
    }
}

C. common/BusinessException.java (自定义异常)

BusinessException.java(自定义业务异常)

java 复制代码
package com.example.todo.common;

public class BusinessException extends RuntimeException {
    private final int code;

    public BusinessException(int code, String message) {
        super(message);
        this.code = code;
    }

    public int getCode() {
        return code;
    }
}

D. config/GlobalExceptionHandler.java (全局异常捕获)

这是生产项目的安全网,确保用户永远看不到堆栈信息。

GlobalExceptionHandler.java(全局异常处理)

java 复制代码
package com.example.todo.config;

import com.example.todo.common.BusinessException;
import com.example.todo.common.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(BusinessException.class)
    public Result<?> handleBusinessException(BusinessException e) {
        log.warn("Business Exception: {}", e.getMessage());
        return Result.error(e.getCode(), e.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<?> handleValidationException(MethodArgumentNotValidException e) {
        String errorMsg = e.getBindingResult().getFieldError().getDefaultMessage();
        return Result.error(400, errorMsg);
    }

    @ExceptionHandler(Exception.class)
    public Result<?> handleException(Exception e) {
        log.error("System Error: ", e);
        return Result.error(500, "系统繁忙,请稍后重试");
    }
}

E. entity/Todo.java

Todo.java(数据库实体)

java 复制代码
package com.example.todo.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;

@Data
@TableName("todos")
public class Todo {
    @TableId(type = IdType.AUTO)
    private Long id;

    private String title;

    private Boolean completed;

    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @TableField(fill = FieldFill.UPDATE)
    private LocalDateTime updateTime;
}

F. service/impl/TodoServiceImpl.java (业务逻辑)

TodoServiceImpl.java(服务实现)

java 复制代码
package com.example.todo.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.todo.common.BusinessException;
import com.example.todo.dto.CreateTodoDTO;
import com.example.todo.dto.UpdateTodoDTO;
import com.example.todo.entity.Todo;
import com.example.todo.mapper.TodoMapper;
import com.example.todo.service.TodoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class TodoServiceImpl implements TodoService {

    @Autowired
    private TodoMapper todoMapper;

    @Override
    public Todo createTodo(CreateTodoDTO dto) {
        if (dto.getTitle() == null || dto.getTitle().trim().isEmpty()) {
            throw new BusinessException(400, "标题不能为空");
        }
        if (dto.getTitle().length() > 100) {
            throw new BusinessException(400, "标题过长(最大100字符)");
        }

        Todo todo = new Todo();
        todo.setTitle(dto.getTitle());
        todo.setCompleted(false);
        todoMapper.insert(todo);
        return todo;
    }

    @Override
    public Todo updateTodo(Long id, UpdateTodoDTO dto) {
        Todo todo = todoMapper.selectById(id);
        if (todo == null) {
            throw new BusinessException(404, "待办事项不存在");
        }

        if (dto.getTitle() != null && !dto.getTitle().trim().isEmpty()) {
            if (dto.getTitle().length() > 100) {
                throw new BusinessException(400, "标题过长");
            }
            todo.setTitle(dto.getTitle());
        }

        if (dto.getCompleted() != null) {
            todo.setCompleted(dto.getCompleted());
        }

        todoMapper.updateById(todo);
        return todo;
    }

    @Override
    public void deleteTodo(Long id) {
        Todo todo = todoMapper.selectById(id);
        if (todo == null) {
            throw new BusinessException(404, "待办事项不存在");
        }
        todoMapper.deleteById(id);
    }

    @Override
    public Todo getTodoById(Long id) {
        Todo todo = todoMapper.selectById(id);
        if (todo == null) {
            throw new BusinessException(404, "待办事项不存在");
        }
        return todo;
    }

    @Override
    public List<Todo> getAllTodos() {
        return todoMapper.selectList(new QueryWrapper<>());
    }

    @Override
    public List<Todo> getCompletedTodos() {
        return todoMapper.selectList(new QueryWrapper<Todo>().eq("completed", true));
    }

    @Override
    public List<Todo> getPendingTodos() {
        return todoMapper.selectList(new QueryWrapper<Todo>().eq("completed", false));
    }
}

G. controller/TodoController.java

TodoController.java(控制器)

java 复制代码
package com.example.todo.controller;

import com.example.todo.common.Result;
import com.example.todo.dto.CreateTodoDTO;
import com.example.todo.dto.UpdateTodoDTO;
import com.example.todo.entity.Todo;
import com.example.todo.service.TodoService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/todos")
public class TodoController {

    @Autowired
    private TodoService todoService;

    @PostMapping
    public Result<Todo> create(@Valid @RequestBody CreateTodoDTO dto) {
        Todo todo = todoService.createTodo(dto);
        return Result.success(todo);
    }

    @PutMapping("/{id}")
    public Result<Todo> update(@PathVariable Long id, @Valid @RequestBody UpdateTodoDTO dto) {
        Todo todo = todoService.updateTodo(id, dto);
        return Result.success(todo);
    }

    @DeleteMapping("/{id}")
    public Result<Void> delete(@PathVariable Long id) {
        todoService.deleteTodo(id);
        return Result.success(null);
    }

    @GetMapping("/{id}")
    public Result<Todo> getById(@PathVariable Long id) {
        Todo todo = todoService.getTodoById(id);
        return Result.success(todo);
    }

    @GetMapping
    public Result<List<Todo>> getAll() {
        List<Todo> todos = todoService.getAllTodos();
        return Result.success(todos);
    }

    @GetMapping("/completed")
    public Result<List<Todo>> getCompleted() {
        List<Todo> todos = todoService.getCompletedTodos();
        return Result.success(todos);
    }

    @GetMapping("/pending")
    public Result<List<Todo>> getPending() {
        List<Todo> todos = todoService.getPendingTodos();
        return Result.success(todos);
    }
}

📂 mapper/
TodoMapper.java(数据访问层)

java 复制代码
package com.example.todo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.todo.entity.Todo;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface TodoMapper extends BaseMapper<Todo> {
    // MyBatis-Plus 已提供常用 CRUD,无需额外方法
}

📂 service/
TodoService.java(服务接口)

java 复制代码
package com.example.todo.service;

import com.example.todo.dto.CreateTodoDTO;
import com.example.todo.dto.UpdateTodoDTO;
import com.example.todo.entity.Todo;

import java.util.List;

public interface TodoService {
    Todo createTodo(CreateTodoDTO dto);
    Todo updateTodo(Long id, UpdateTodoDTO dto);
    void deleteTodo(Long id);
    Todo getTodoById(Long id);
    List<Todo> getAllTodos();
    List<Todo> getCompletedTodos();
    List<Todo> getPendingTodos();
}

CreateTodoDTO.java

java 复制代码
package com.example.todo.dto;

import jakarta.validation.constraints.NotBlank;
import lombok.Data;

@Data
public class CreateTodoDTO {
    @NotBlank(message = "标题不能为空")
    private String title;
}

UpdateTodoDTO.java

java 复制代码
package com.example.todo.dto;

import lombok.Data;

@Data
public class UpdateTodoDTO {
    private String title;
    private Boolean completed;
}

application.yml(配置文件)

java 复制代码
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/todo_db?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
    username: root
    password: secret
    driver-class-name: com.mysql.cj.jdbc.Driver

  jackson:
    time-zone: GMT+8
    date-format: yyyy-MM-dd HH:mm:ss

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

server:
  port: 8080

schema.sql(建表语句 ------ 放在 resources 下)

sql 复制代码
-- src/main/resources/schema.sql
CREATE DATABASE IF NOT EXISTS todo_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE todo_db;

CREATE TABLE IF NOT EXISTS todos (
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
    title VARCHAR(255) NOT NULL COMMENT '待办标题',
    completed BOOLEAN DEFAULT FALSE COMMENT '是否完成',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='待办事项表';

三. 部署配置 (Docker)

A. Dockerfile

放在项目根目录。使用多阶段构建或简单的 JRE 镜像。

dockerfile 复制代码
# 使用轻量级的 Eclipse Temurin (OpenJDK) 运行时
FROM eclipse-temurin:17-jre-alpine

# 设置工作目录
WORKDIR /app

# 复制打包好的 jar 包 (假设 mvn package 生成在 target/)
COPY target/todo-api-0.0.1-SNAPSHOT.jar app.jar

# 暴露端口
EXPOSE 8080

# 启动命令,指定生产环境 profile
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "/app/app.jar"]FROM eclipse-temurin:17-jre-alpine

WORKDIR /app

COPY target/*.jar app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "/app/app.jar"]

B. docker-compose.yml (一键启动 DB + App)

yaml 复制代码
version: '3.8'

services:
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: todo_db
    volumes:
      - ./src/main/resources/schema.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "3306:3306"
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/todo_db?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
      SPRING_DATASOURCE_USERNAME: root
      SPRING_DATASOURCE_PASSWORD: secret
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

四. 如何运行和上线?

本地开发:

  • 修改: application.yml 连接本地 MySQL。
  • 运行: mvn spring-boot:run。
  • 测试:curl -XPOST http://localhost:8080/api/todos -H "Content-Type:
    application/json" -d '{"title":"Learn Java"}'
    Docker 一键部署:
bash 复制代码
mvn clean package -DskipTests
docker-compose up --build

访问:http://localhost:8080/api/todos

生产上线:

  • 打包:mvn clean package -DskipTests
  • 构建镜像:docker build -t my-todo-app .
  • 启动:docker-compose up -d
    下一步建议
    在这个基础上,你可以逐步添加:
    Redis:缓存热点 Todo。
    Spring Security + JWT:实现用户登录认证。
    Actuator:添加 /actuator/health 健康检查接口。
    GitHub Actions:实现代码提交后自动构建 Docker 镜像。

功能清单:

相关推荐
overmind4 小时前
oeasy Python 102 集合_运算_交集_并集_差集_对称差集
开发语言·python
han_hanker4 小时前
mybatis-plus 常用方法
java·tomcat·mybatis
爱吃提升4 小时前
Python自动驾驶图像识别完整实战教程(OpenCV+YOLOv8,附可直接运行源码)
python·opencv·自动驾驶
长不胖的路人甲4 小时前
CAS讲解
java
我是唐青枫5 小时前
Java SLF4J 实战指南:从日志门面到 Logback、MDC 和链路追踪
java·开发语言·logback
statistican_ABin5 小时前
2026 FIFA 世界杯比赛与球队数据探索性分析
人工智能·python·数据挖掘·数据分析
qetfw5 小时前
yt-dlp:下载公开视频、字幕、封面和音频的实用命令
python·音视频·开源项目·效率工具
QH_ShareHub5 小时前
GPU 完整运行过程:驱动、CUDA Toolkit、Conda、Python PyTorch 与 R torch
pytorch·python·conda
XS0301065 小时前
JavaEE 核心知识
java·java-ee
AbsoluteLogic5 小时前
Python——必学内置模块 Sys
python