代码语言

知识点思维导图

29 个知识节点

Java(30) - 实战 —— 从零开发一个完整接口(字典管理)

读完后,你应能完成以下任务:

  • 绘制“Java(30) - 实战 —— 从零开发一个完整接口(字典管理) / 需求分析(写代码前先想清楚)”的关键对象与数据流,解释“后端也一样,先把"接口清单"列出来。”,并用源码位置、日志或 Trace 标注证据。
  • 为“Java(30) - 实战 —— 从零开发一个完整接口(字典管理) / 设计表结构(数据库是地基)”设计正常与异常输入,验证“【前端类比】这相当于你在 TS 里定义一个 interface DictItem,只不过它落在 MySQL 里,是"持久化的类型定义"。”,输出首个偏差位置与回归测试结果。
  • 实现“Java(30) - 实战 —— 从零开发一个完整接口(字典管理) / demo 的分层架构(先看清全貌)”的最小代码或配置,检验“记住这个分工,后面每写一层你都知道"它该干嘛、不该干嘛"。”,输出命令、结果与 Diff,并说明不适用边界。

前 29 课讲的都是"零件",这一课我们把它们拼成一台能跑的车:手把手从需求到联调,完整开发一个"字典管理"接口。看完你就能独立交付一个 demo 风格的接口了。

前面我们学了环境、语法、HTTP 生命周期(见第 4 课)、类与对象、集合、异常、注解。这些知识单独看都懂,但真到工作里,你要面对的是"产品给了个需求,你怎么从空白文件夹一路写到前端能调通"。这一课就完整走一遍。

我们要做的需求很常见:字典管理。所谓"字典",就是系统里那些可枚举的配置项,比如"车辆类型:1=货车 2=挂车 3=厢式车""维保状态:1=待审 2=审核中 3=已完成"。前端下拉框、状态标签全靠它。


一、需求分析(写代码前先想清楚)

【前端类比】这一步就像你接到一个页面需求,先不急着写 <template>,而是先问:要展示什么字段?要调几个接口?数据结构长啥样?后端也一样,先把"接口清单"列出来。

字典管理最基本的四件事,就是经典的 CRUD(增删改查):

操作 前端动作 接口 HTTP 方法
查列表 进页面拉取字典项 /dict/list GET
查详情 点某一项看详情 /dict/getById GET
新增 填表单点保存 /dict/add POST
修改 编辑后保存 /dict/update POST
删除 点删除按钮 /dict/delete POST

数据长什么样?一条字典项至少需要这些字段:

字典编码 dictCode   —— 属于哪个分组,如 "vehicle_type"(车辆类型)
字典键   dictKey    —— 值,如 "1"
字典名   dictName   —— 显示文本,如 "货车"
排序     sort       —— 下拉框里的顺序
状态     status     —— 1启用 0禁用

需求清楚了,开始设计表。


二、设计表结构(数据库是地基)

【前端类比】这相当于你在 TS 里定义一个 interface DictItem,只不过它落在 MySQL 里,是"持久化的类型定义"。

-- 字典项表:存储系统所有可枚举配置
CREATE TABLE `sys_dict` (
  `id`        INT          NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `dict_code` VARCHAR(64)  NOT NULL COMMENT '字典编码(分组), 如 vehicle_type',
  `dict_key`  VARCHAR(64)  NOT NULL COMMENT '字典键(值), 如 1',
  `dict_name` VARCHAR(128) NOT NULL COMMENT '字典名(显示文本), 如 货车',
  `sort`      INT          NOT NULL DEFAULT 0 COMMENT '排序号, 越小越靠前',
  `status`    TINYINT      NOT NULL DEFAULT 1 COMMENT '状态 1启用 0禁用',
  `create_time` DATETIME   DEFAULT NULL COMMENT '创建时间',
  `update_time` DATETIME   DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_dict_code` (`dict_code`)  -- 按编码查列表很频繁, 建索引
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='字典项表';

两个关键决策,解释下 WHY:

  • dict_code 建索引:前端最常用的查询是"给我 vehicle_type 这一组的所有项",等于 WHERE dict_code = ?。这种高频条件查询不建索引会全表扫描,数据量大了会慢。
  • 字段命名用下划线 dict_code:MySQL 习惯下划线,Java 习惯驼峰 dictCode。两者的映射 MyBatis-Plus 会自动帮我们做(下面会讲)。

【数据库列名 vs Java 字段名对照】

MySQL(下划线) Java(驼峰) 说明
dict_code dictCode 自动映射
create_time createTime 自动映射
id id 一致

三、demo 的分层架构(先看清全貌)

在 demo 里一个模块(如 demo-basic)通常拆成三个 maven 子模块,对应你前端项目里的"分层目录":

demo-basic-common    ← 放 In(入参) / Out(出参) DTO    ≈ 前端的 types/  接口类型定义
demo-basic-service   ← 放 entity / Mapper / ServiceImpl ≈ 前端的 api/ + store/ 数据层
demo-basic-biz       ← 放 Controller                    ≈ 前端的 router + 页面入口

一个请求进来的完整链路(第 4 课讲过的五站,这里聚焦后端三层):

前端 axios
   │  POST /dict/add  { dictCode, dictKey, dictName }
   ▼
┌──────────────────────────────────────────────┐
│ Controller   收 In, 调 Service, 包成 R 返回      │  demo-basic-biz
│   DictController.add(DictAddIn in)             │
└───────────────────┬──────────────────────────┘
                    ▼
┌──────────────────────────────────────────────┐
│ Service      业务逻辑(校验/组装/事务)            │  demo-basic-service
│   DictServiceImpl.add(...)                     │
└───────────────────┬──────────────────────────┘
                    ▼
┌──────────────────────────────────────────────┐
│ Mapper       只管和 MySQL 打交道                │  demo-basic-service
│   DictMapper extends BaseMapper<Dict>          │
└───────────────────┬──────────────────────────┘
                    ▼
                 MySQL  sys_dict 表

记住这个分工,后面每写一层你都知道"它该干嘛、不该干嘛"。下面从最底层往上写。


四、写 Entity(数据库表的 Java 镜像)

Entity 就是数据库一行记录的 Java 对象。参考 demo 示例的 Organization 实体(demo-basic-service/.../entity/Organization.java),它长这样:

@Data
@EqualsAndHashCode(callSuper = true)
@TableName("organization")               // 对应哪张表
public class Organization extends Model<Organization> {
    @TableId
    private Integer id;
    private Integer groupId;
    private String organizationName;
    // ...
}

照葫芦画瓢,我们的 Dict 实体:

package com.example.platform.basic.service.entity;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;

/**
 * 字典项表实体: 一个对象 = sys_dict 表里的一行记录
 */
@Data                                       // Lombok: 自动生成 getter/setter/toString(见第8课)
@EqualsAndHashCode(callSuper = true)
@TableName("sys_dict")                       // 告诉 MyBatis-Plus 这个类映射到 sys_dict 表
public class Dict extends Model<Dict> {

    /** 主键id */
    @TableId                                 // 标记主键, MyBatis-Plus 据此做更新/删除定位
    private Integer id;

    /** 字典编码(分组), 如 vehicle_type */
    private String dictCode;                 // 注意: 库里是 dict_code, 这里驼峰, 自动映射

    /** 字典键(值), 如 1 */
    private String dictKey;

    /** 字典名(显示文本), 如 货车 */
    private String dictName;

    /** 排序号, 越小越靠前 */
    private Integer sort;

    /** 状态 1启用 0禁用 */
    private Integer status;

    /** 创建时间 */
    private LocalDateTime createTime;

    /** 更新时间 */
    private LocalDateTime updateTime;
}

【前端类比】Entity 约等于你 TS 里贴着数据库的 interface DictRow@TableName 像是告诉 ORM "这个类型对应哪张表",@Data 帮你省掉手写 getter/setter(JS 里属性天生可读写,Java 需要这个)。

注解 作用 前端类比
@TableName("sys_dict") 类 ↔ 表 映射 ORM 的 @Entity({ table })
@TableId 标记主键字段 主键约定
@Data 生成 getter/setter TS 属性默认可读写

五、写 Mapper(数据访问层,最省力的一层)

demo 用 MyBatis-Plus,它的精髓是:让 Mapper 接口继承 BaseMapper<T>,CRUD 的 SQL 它全自动给你生成,你一行 SQL 都不用写。看 demo 示例的 OrganizationMapper

public interface OrganizationMapper extends BaseMapper<Organization> {
    // 只有"非标准"的复杂查询才需要自己声明方法
    IPage<Organization> getOrganizationPage(Page page, @Param("organization") Organization organization);
}

我们的 DictMapper 更简单,CRUD 全靠继承:

package com.example.platform.basic.service.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.platform.basic.service.entity.Dict;

/**
 * 字典 Mapper: 继承 BaseMapper 即白拿一整套 CRUD 方法
 * 不需要写任何 SQL, MyBatis-Plus 运行时自动生成
 */
public interface DictMapper extends BaseMapper<Dict> {
    // 暂时不需要自定义方法, 标准 CRUD 已够用
}

继承 BaseMapper<Dict> 后,你白拿了这些方法(无需实现):

方法 作用 等价 SQL
insert(dict) 插入一行 INSERT INTO sys_dict ...
selectById(id) 按主键查 SELECT * WHERE id=?
selectList(wrapper) 条件查列表 SELECT * WHERE ...
updateById(dict) 按主键改 UPDATE ... WHERE id=?
deleteById(id) 按主键删 DELETE WHERE id=?

【前端类比】这就像你用 Prisma:定义好 model,prisma.dict.findMany() / create() / update() 全都白送,不用手写 SQL。BaseMapper<Dict> 里的泛型 <Dict>(见第 6 课)就是告诉它"我操作的是 Dict 这张表"。

⚠️ 别忘了:要让 Spring 扫到 Mapper,demo 项目入口类上通常有 @MapperScan("com.example.platform.basic.service.mapper")。这是项目级配置,已经配好了,新建 Mapper 放对包就行。


六、定义 In / Out(接口的"出入境"类型)

【前端类比】这就是你和后端约定的 请求体类型响应体类型。前端写 interface AddDictReqinterface DictResp,后端写 InOut。两边对齐,联调才顺。

为什么不直接用 Entity 收发?因为 Entity 是给数据库看的,In/Out 是给前端看的,二者职责不同。比如新增时前端不该传 id(自增)、不该传 createTime(后端生成)。用专门的 In 能精确控制"前端能传什么"。

参考 demo 示例的 OrganizationIndemo-basic-common/.../in/OrganizationIn.java)风格:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class OrganizationIn {
    private List<Integer> ids;
    private Integer groupId;
}

我们定义新增用的入参 DictAddIn

package com.example.platform.basic.common.in;

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

/**
 * 新增字典项入参: 只暴露前端"应该"填的字段
 */
@Data
public class DictAddIn {

    /** 字典编码(分组), 不能为空 */
    @NotBlank(message = "字典编码不能为空")   // 参数校验(见第7课异常), 为空直接拦下
    private String dictCode;

    /** 字典键(值), 不能为空 */
    @NotBlank(message = "字典键不能为空")
    private String dictKey;

    /** 字典名(显示文本), 不能为空 */
    @NotBlank(message = "字典名不能为空")
    private String dictName;

    /** 排序号, 可不传, 默认 0 */
    private Integer sort;
}

出参 DictOut(参考 demo OrganizationOut 风格,扁平的展示对象):

package com.example.platform.basic.common.out;

import lombok.Data;
import java.time.LocalDateTime;

/**
 * 字典项出参: 返回给前端展示的字段
 */
@Data
public class DictOut {
    private Integer id;            // 主键, 前端编辑/删除时回传
    private String dictCode;       // 字典编码
    private String dictKey;        // 字典键
    private String dictName;       // 字典名
    private Integer sort;          // 排序
    private Integer status;        // 状态 1启用 0禁用
    private LocalDateTime createTime; // 创建时间
}
In(入参) Out(出参) Entity
给谁看 前端→后端 后端→前端 后端↔数据库
前端类比 RequestBody 类型 Response 类型 DB row 类型
含 id 吗 新增不含/修改含

七、写 Service(业务逻辑的大脑)

Service 是真正"干活"的地方:参数校验、业务规则、对象转换、调 Mapper 落库。Controller 不写业务,Mapper 不写业务,业务全在这。

先定义接口(demo 习惯接口 + 实现分离):

package com.example.platform.basic.service.service;

import com.example.platform.basic.common.in.DictAddIn;
import com.example.platform.basic.common.out.DictOut;
import java.util.List;

/**
 * 字典业务接口
 */
public interface DictService {

    /** 新增字典项, 返回新记录id */
    Integer add(DictAddIn in);

    /** 按字典编码查列表 */
    List<DictOut> listByCode(String dictCode);
}

再写实现类:

package com.example.platform.basic.service.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.platform.basic.common.in.DictAddIn;
import com.example.platform.basic.common.out.DictOut;
import com.example.platform.basic.service.entity.Dict;
import com.example.platform.basic.service.mapper.DictMapper;
import com.example.platform.basic.service.service.DictService;
import com.example.platform.common.core.exception.BusinessException;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 字典业务实现
 */
@Service                                 // 标记为 Spring Bean(见第5课), 才能被 @Autowired 注入
public class DictServiceImpl implements DictService {

    @Autowired                           // 注入 Mapper, Spring 自动给我们 new 好实例
    private DictMapper dictMapper;

    /**
     * 新增字典项
     * @param in 前端传来的新增参数
     * @return 新记录的主键id
     */
    @Override
    public Integer add(DictAddIn in) {
        // 业务校验: 同一编码下, dictKey 不允许重复(否则前端下拉框会出现两个相同值)
        LambdaQueryWrapper<Dict> dupQuery = new LambdaQueryWrapper<Dict>()
                .eq(Dict::getDictCode, in.getDictCode())
                .eq(Dict::getDictKey, in.getDictKey());
        // 注意: demo-basic 用的 MyBatis-Plus 3.1.0, selectCount 返回 Integer
        // (高版本 3.4+ 才改成返回 Long, 跟着项目实际版本走)
        Integer count = dictMapper.selectCount(dupQuery);  // 查有没有同编码同键的记录
        if (count != null && count > 0) {
            // 抛业务异常(见第7课), 全局异常处理器会包成统一错误响应给前端
            throw new BusinessException("该字典编码下已存在相同的字典键");
        }

        // In → Entity 转换: 拷贝同名属性, 省去逐个 setXxx
        Dict dict = new Dict();
        BeanUtils.copyProperties(in, dict);     // dictCode/dictKey/dictName/sort 自动拷过去
        dict.setStatus(1);                       // 新增默认启用
        dict.setSort(in.getSort() == null ? 0 : in.getSort()); // 排序兜底为0
        dict.setCreateTime(LocalDateTime.now()); // 创建时间后端生成, 不信任前端传值

        dictMapper.insert(dict);                 // 落库, 回填自增id 到 dict.id
        return dict.getId();
    }

    /**
     * 按字典编码查列表
     * @param dictCode 字典编码, 如 vehicle_type
     * @return 该编码下的所有字典项(已转为出参)
     */
    @Override
    public List<DictOut> listByCode(String dictCode) {
        // 构造查询条件: dict_code = ? AND status = 1, 按 sort 升序
        LambdaQueryWrapper<Dict> query = new LambdaQueryWrapper<Dict>()
                .eq(Dict::getDictCode, dictCode)   // 只查这一组
                .eq(Dict::getStatus, 1)            // 只返回启用的
                .orderByAsc(Dict::getSort);        // 按排序号升序, 保证下拉框顺序稳定
        List<Dict> list = dictMapper.selectList(query);

        // Entity → Out 转换: 用 Stream(见第6课) 把每个 Dict 映射成 DictOut
        return list.stream().map(dict -> {
            DictOut out = new DictOut();
            BeanUtils.copyProperties(dict, out);   // 同名属性自动拷贝
            return out;
        }).collect(Collectors.toList());
    }
}

几个值得记住的点:

  • LambdaQueryWrapper:MyBatis-Plus 的条件构造器,.eq(Dict::getDictCode, code) 等价于 SQL WHERE dict_code = ?。用方法引用 Dict::getDictCode 而非字符串 "dict_code",编译期就能查错(字段改名会编译报错),比手写字符串安全。
  • BeanUtils.copyProperties(源, 目标):把同名属性批量拷贝。【前端类比】约等于 Object.assign(target, source){ ...source } 的浅拷贝,省去一行行 set
  • 校验失败抛 BusinessException(第 7 课):不要 return null 或返回错误码字符串,抛异常 + 全局处理器才是 demo 的统一姿势。

【前端类比】Service 就是你 Vue/React 里那个不碰 UI、只处理数据和规则的 store actionservice.ts 函数:校验、调 API、转数据格式,最后把干净的结果交出去。


八、写 Controller(接口的门面)

Controller 是最薄的一层,只做三件事:收参数 → 调 Service → 包成 R 返回。绝不写业务逻辑。

直接对照 demo 示例的 OrganizationControllerdemo-basic-biz/.../controller/OrganizationController.java):

@RestController
@RequestMapping("/organization")
public class OrganizationController extends BaseController {
    @Autowired
    private OrganizationService organizationService;

    @GetMapping("/getById")
    public R<OrganizationOut> getOrganization(@RequestParam("id") int id) {
        return R.ok(organizationService.getOrganization(id));   // 调 service, 包成 R 返回
    }
}

我们的 DictController 一模一样的套路:

package com.example.platform.basic.biz.controller;

import com.example.platform.basic.common.in.DictAddIn;
import com.example.platform.basic.common.out.DictOut;
import com.example.platform.basic.service.service.DictService;
import com.example.platform.common.core.entity.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;

/**
 * 字典管理接口
 */
@RestController                          // = @Controller + @ResponseBody, 返回值自动转JSON(见第8课)
@RequestMapping("/dict")                  // 这个类下所有接口都以 /dict 开头
public class DictController {

    @Autowired                            // 注入业务层
    private DictService dictService;

    /**
     * 新增字典项
     * @param in 请求体(JSON), @Valid 触发 In 里的 @NotBlank 等校验
     * @return R 包裹的新记录id
     */
    @PostMapping("/add")                  // POST, 入参从请求体取
    public R<Integer> add(@Valid @RequestBody DictAddIn in) {  // @RequestBody: JSON→对象(见第8课)
        return R.ok(dictService.add(in));
    }

    /**
     * 按字典编码查列表
     * @param dictCode URL 上的查询参数, 如 ?dictCode=vehicle_type
     * @return R 包裹的字典项列表
     */
    @GetMapping("/list")                  // GET, 入参从 URL 取
    public R<List<DictOut>> list(@RequestParam("dictCode") String dictCode) { // @RequestParam: 取URL参数
        return R.ok(dictService.listByCode(dictCode));
    }
}

约定提醒:demo 的 biz 层 Controller 通常都 extends BaseController(如上面的 OrganizationController),父类封装了取登录用户、分页参数等通用能力。这个示例为聚焦主线省略了它,真实开发时按团队规范补上 extends BaseController

@RequestParam vs @RequestBody 是前端对接时最容易踩的坑,重点记住(第 8 课讲过):

注解 取值来源 前端怎么传 适用
@RequestParam URL 查询串 ?dictCode=x params: { dictCode } GET 查询
@RequestBody 请求体 JSON data: { ... } POST 提交对象

【前端类比】@RequestParam 对应 axios 的 params@RequestBody 对应 axios 的 data。传错了后端就收到 null,这是联调时 80% "参数收不到" 问题的根因。

关于返回值 R<T>:这是 demo 的统一响应包装(第 4 课见过),R.ok(data) 生成的 JSON 长这样:

{ "code": 0, "message": "success", "data": { ... } }

⚠️ 注意字段名:demo 示例的 R<T>demo-common-core/.../entity/R.java)用的是 code(错误码,成功为 0)、message(提示文案)、data(业务数据),不是很多教程里常见的 code/msg/data。对接前看准字段名,这是联调踩坑高发区。

前端永远先看 code(等于 0 才是成功)再取 data,结构稳定,全项目一致。


九、前端对接联调

后端写完,回到你熟悉的主场。前端调用这两个接口:

import request from '@/utils/request' // 项目封装好的 axios 实例

// 1. 查字典列表 —— GET, 参数放 params(对应 @RequestParam)
export function fetchDictList(dictCode: string) {
  return request({
    url: '/dict/list',
    method: 'get',
    params: { dictCode },        // 拼到 URL: /dict/list?dictCode=vehicle_type
  })
}

// 2. 新增字典项 —— POST, 参数放 data(对应 @RequestBody)
export function addDict(payload: {
  dictCode: string
  dictKey: string
  dictName: string
  sort?: number
}) {
  return request({
    url: '/dict/add',
    method: 'post',
    data: payload,               // 作为 JSON 请求体发出
  })
}

页面里用(以 React 为例):

// 进页面拉取"车辆类型"字典, 渲染下拉框
const { data } = await fetchDictList('vehicle_type')
if (data.code === 0) {          // 先判 code(0=成功), 这是 R<T> 的约定
  setOptions(data.data)           // data.data 就是 List<DictOut>
}

// 新增一项
const data = await addDict({ dictCode: 'vehicle_type', dictKey: '4', dictName: '冷藏车' })
if (data.data.code === 0) {
  message.success('新增成功, id=' + data.data.data)
}

【联调自查清单】出问题时按这个顺序排查:

后端收不到参数?
 ├─ GET 接口 → 检查前端是否用了 params (而不是 data)
 ├─ POST 接口 → 检查前端是否用了 data (而不是 params)
 └─ 字段名拼错? dictCode 写成 dictcode? (大小写敏感)

HTTP 200 但 code != 0 ?(demo 业务异常不靠 HTTP 状态码, 全走 code)
 ├─ code=400 → 校验失败, @NotBlank 字段没传或传了空串, 看 message 提示哪个字段
 └─ code=1   → BusinessException, 如"字典键重复", message 就是抛出的提示文案

HTTP 500 ?
 └─ 未被业务捕获的异常(如 NPE/SQL报错), 看后端日志栈追根因

关键认知:demo 用 @RestControllerAdvice 全局异常处理器把 BusinessException、校验失败统一包成 HTTP 200 + code(1 或 400) 返回,所以前端不能只看 HTTP 状态码,必须看 code。只有真正未捕获的系统异常才落到 HTTP 500。

完整链路验证(用 curl 或 Apifox 模拟前端):

# 新增一条
curl -X POST http://localhost:8080/dict/add \
  -H "Content-Type: application/json" \
  -d '{"dictCode":"vehicle_type","dictKey":"1","dictName":"货车"}'
# 返回 {"code":0,"message":"success","data":1}

# 查列表
curl "http://localhost:8080/dict/list?dictCode=vehicle_type"
# 返回 {"code":0,"message":"success","data":[{"id":1,"dictKey":"1","dictName":"货车",...}]}

十、回顾全链路(一张图串起来)

┌─ demo-basic-common ─────────────────────────────┐
│  DictAddIn (入参)        DictOut (出参)          │
└──────────────┬──────────────────▲──────────────┘
               │                  │
┌─ demo-basic-biz ──────────────────────────────────┐
│  DictController                                   │
│    @PostMapping add(@RequestBody DictAddIn)       │
│    @GetMapping  list(@RequestParam dictCode)      │
└──────────────┬──────────────────▲────────────────┘
               │ 调 service        │ 返回 R<T>
┌─ demo-basic-service ──────────────────────────────┐
│  DictServiceImpl  (校验/转换/业务)                 │
│    BeanUtils.copyProperties  In↔Entity↔Out        │
│           │                  ▲                    │
│  DictMapper extends BaseMapper<Dict>  (CRUD自动)   │
│           │                  ▲                    │
│  Dict (Entity) @TableName("sys_dict")             │
└───────────┴──────────────────┴───────────────────┘
                    │           ▲
                    ▼           │
                 MySQL  sys_dict 表

你会发现,写一个接口就是从下往上盖楼:表 → Entity → Mapper → In/Out → Service → Controller → 前端联调。每一层职责单一,互不越界。这套分层一旦形成肌肉记忆,再复杂的接口你也能照这个骨架往里填。


十一、本课小结

  • 开发顺序:需求分析 → 设计表 → Entity → Mapper → In/Out → Service → Controller → 前端联调,从下往上盖楼
  • 分层职责:Controller 只收发不写业务;Service 是业务大脑(校验/转换/事务);Mapper 只管和库打交道(继承 BaseMapper 白拿 CRUD)
  • 三种对象别混:Entity 对数据库、In 对前端请求、Out 对前端响应,靠 BeanUtils.copyProperties 互转
  • MyBatis-Plus 省力extends BaseMapper<Dict> + LambdaQueryWrapper,常见 CRUD 不用写一行 SQL,方法引用 Dict::getDictCode 比字符串更安全
  • 联调最大坑@RequestParam(对 axios params) vs @RequestBody(对 axios data) 别传错;前端先判 Rcode(等于 0 才成功) 再取 data
  • 统一响应 R<T>:demo 示例字段是 code(0=成功) / message / data,不是 code/msg/data;业务异常走全局处理器返回 HTTP 200 + code,前端认 code 不认 HTTP 状态码
  • 参考的 demo 匿名化示例代码OrganizationController(Controller 模式)、Organization(Entity)、OrganizationMapper(BaseMapper 继承)、OrganizationIn/OrganizationOut(In/Out DTO 风格)、R(统一响应)

下一课预告:第 31 课 单元测试入门。接口写完怎么证明它是对的?我们会用 JUnit + Mockito 给这个 DictService 写测试,类比你前端的 Jest/Vitest,让你交付的代码更有底气。

十二、总结

  • 设计表结构(数据库是地基):【前端类比】这相当于你在 TS 里定义一个 interface DictItem,只不过它落在 MySQL 里,是"持久化的类型定义"。
  • 写 Entity(数据库表的 Java 镜像):Entity 就是数据库一行记录的 Java 对象。
  • 写 Mapper(数据访问层,最省力的一层):demo 用 MyBatis-Plus,它的精髓是:让 Mapper 接口继承 BaseMapper ,CRUD 的 SQL 它全自动给你生成,你一行 SQL 都不用写。
  • 定义 In / Out(接口的"出入境"类型):【前端类比】这就是你和后端约定的 请求体类型 和 响应体类型。
  • 写 Service(业务逻辑的大脑):Service 是真正"干活"的地方:参数校验、业务规则、对象转换、调 Mapper 落库。
  • 写 Controller(接口的门面):Controller 是最薄的一层,只做三件事:收参数 → 调 Service → 包成 R 返回。

学完自测

选择所有正确答案;提交后逐项核对判断依据。

1在“实战 —— 从零开发一个完整接口(字典管理)”中,需要同时满足“需求分析(写代码前先想清楚)”与“设计表结构(数据库是地基)”。给定正文约束“数据长什么样?一条字典项至少需要这些字段。”,哪些判断保持了原有处理机制?多选
2“实战 —— 从零开发一个完整接口(字典管理)”出现偏差:“在“实战 —— 从零开发一个完整接口(字典管理) / demo 的分层架构(先看清全貌)”中,即使不满足“在 demo 里一个模块(如 demo-basic)通常拆成三个 maven 子模块,对应你前端项目里的"分层目录"”,结果与副作用仍会保持不变。”已成为实际行为。围绕“demo 的分层架构(先看清全貌)”与“写 Entity(数据库表的 Java 镜像)”,哪些判断能定位被改变的职责或边界?多选
3评审“实战 —— 从零开发一个完整接口(字典管理)”方案时,验收条件包含“让 Mapper 接口继承 BaseMapper,CRUD 的 SQL 它全自动给你生成,你一行 SQL 都不用写。”。关于“写 Mapper(数据访问层,最省力的一层)”与“定义 In / Out(接口的"出入境"类型)”的哪些决策符合正文机制?多选