中州养老 第一章-快速搞定新项目
中州养老 第一章-快速搞定新项目
今日目标
- 掌握中州养老项目核心业务流程、系统架构、技术架构
- 掌握快速搞定新项目的环境搭建
- 掌握快速熟悉一个新项目的方式
- 掌握如何在新项目中进行接口开发
- 掌握如何快速生成在线接口文档
- 掌握项目中的异常处理流程和方式
1. 项目介绍
项目介绍
行业背景
- 中国老龄化程度加深,我国老龄事业和养老服务体系的发展得到了国家的高度重视,
- 在国家政策的支持下,我国智慧养老产业主体持续增多,产业链不断整合,发展前景较好。
- 我国正在形成一个多元化“互联网 + 养老” 的智慧老年护理服务系统,智慧养老是我国的必然趋势
市场规模及预测
2022年中国养老产业市场规模达到10.3万亿元,同比增长16.7%。
预计2023-2027年中国养老产业迎来较快速增长。
预计2027年中国养老产业市场规模达21.1万亿元
整体业务流程
中州养老系统首字为养老院量身定制开发专业的养老管理软件产品;
涵盖来访管理、入退管理、在住管理、服务管理、财务管理等功能模块,涉及从来访参观到退住办理的完整流程。

项目原型地址:https://rp-java.itheima.net/zhyl/
中州养老项目分为两端,一个是管理后台,另外一个是家属端
- 管理后台:养老院员工使用,入住、退住,给老人服务记录等等
- 家属端:养老院的老人家属使用,查看老人信息,缴费,下订单等等

项目演示地址:https://zhyl-admin.itheima.net/
默认账号:admin@qq.comm 密码:888itcast.CN764%...

技术架构
下图展现了中州养老项目主要使用的技术:

- 前端主要使用的 Vue3+TS
- 后端主要使用的是 Springboot 作为基础架构,当然后端也集成了很多其他的技术,比如有 Mybatis、Swagger、Spring cache、Spring Security、Xxl-job、Activiti7
- 数据存储主要使用到了 MySQL 和 Redis
- 使用了 nginx 来作为反向代理和前端的静态服务器
- 其他技术:阿里云物联网平台 IOT、对象存储 OSS、微信登录、AI 工具辅助开发等
总结
课堂作业
- 中州养老项目解决了什么社会需求?🎤
- 中州养老项目的技术架构是什么,流利的说出来!
2. 如何快速完成新项目的搭建
如何快速完成新项目的搭建
为什么是 1-2 的项目
通常小伙伴入职一家新公司之后,接手的项目有两种情况,第一个是新项目 ,第二个是老项目继续开发。
根据已毕业学员的调研结果说明,95% 的学员会在老项目的基础上进行继续开发新的功能。这个就是 1-2 的项目。
简单说就是,项目已经开发了一部分,我们需要在已有的项目中进行再次开发。
与之对应的0-1的项目,从头开始的新项目

所以,训练大家在已有项目中进行开发是一个非常有必要的能力,那么,我们如何在老项目中进行开发呢?
其实,别管是什么项目,我们有两个问题需要先解决
- 1️⃣如何快速完成新项目的环境搭建
- 2️⃣如何快速熟悉一个新的项目
养老项目就是一个 1-2 的项目,在功能开发之前,我们需要先解决上述的两个问题。那么接下来我们先来搞定第一个问题:如何快速完成新项目的环境搭建
服务器说明
一般企业中服务器都是部署了一些项目中公用的基础环境,比如 MySQL、Redis 等这些。这些服务都是部署在 linux 服务器中,这些服务可能是部署在公司的一个机房中,也有可能是云服务器(中小企业居多)。
在我们教学的过程中,为了还原更真实的企业环境,我们可以在 VM Ware 软件中来部署 linux 服务器,当做我们开发中的公用服务器,具体的 VM Ware 的服务配置,请参考如下链接:
后端初始代码
在当前资料中找到后端的初始代码,使用 idea 打开之后的效果如下:

├── zzyl //父工程,聚合项目其他模块,统一管理依赖
│ ├── zzyl-common //通用的模块,比如,统一的异常、工具类、常量,配置类等等
│ ├── zzyl-service //业务模块,编写业务层代码
│ ├── zzyl-web //web模块,对外提供接口
基础环境要求
现在绝大部分的项目,都是前后端的项目,所以环境搭建中也包含了两部分, 一个是后端,一个是前端, 并且在项目中都会有指定的环境要求部署文档,养老项目的部署文档如下:
总结
课堂作业
- 实际企业项目中,是0-1的项目,还是1-2类型的多一些?针对这两种项目类型,需要做哪些准备工作?🎤
- 参考上述的链接文档,简要的阅读文档,到时候能快速定位资料的位置!
- 参考上述资料,将前后端代码跑起来!!🎯
3. 如何快速熟悉项目中的一个模块
如何快速熟悉项目中的一个模块
熟悉模块的方式
现在前后端代码,已经能够正常跑起来了,我们现在的目标就是来熟悉项目,不过我们并不能一下子对项目全部熟悉,而是需要逐步进行拆解分析,直至熟悉。
在一开始,我们可以找到项目中的一个模块来进行熟悉,也就说先找到一个切入口。那么熟悉项目的方式有很多种渠道,我们需要通过各个渠道来深入了解。
下面列了一些比较常见的熟悉项目的方式: 👇
- 阅读原型文档 + 需求文档 PRD(Product Requirements Document:产品需求文档)
- 阅读表结构(数据库)
- 页面点击访问感受(UI页面)
- 阅读对应模块代码,熟悉代码风格和规范(源代码)

熟悉房型设置模块操作
下面,我们可以先熟悉已有代码中开发完的一个小模块,房型设置
阅读原型图和 PRD
项目原型地址:https://rp-java.itheima.net/zhyl/
导航: 后台原型图-> 在住管理-> 床位管理-> 房型设置

通过阅读文档,我们可以得知: 👇
- 房型设置是养老院中管理不同的房间类型,比如有:四人间、三人间、普通单人间、特护房等等
- 这里面共涉及到了 7 个接口,分别是:新增、编辑、删除、分页查询、图片上传、启用禁用、根据 id 查询
一般原型文档,阅读到什么程度呢?
- 了解产品的背景和定位,熟悉业务流程
- 完整且仔细的了解功能描述,和系统流程
- 确认需求理解没有偏差(与产品经理沟通)
阅读表结构
我们可以到数据库中找到对应的表结构,从名字上,我们也可以判断出,叫做:roomtype(房间类型)
知道了是哪张表以后,我们就要详细的看一下,里面涉及到了哪些字段,字段的含义是什么,哪个是主键、有没有关联、有没有约束等这些信息,最直观的方式就是查看表结构的 DDL(数据定义语言)
一般的数据链接工具,都可以查看表的 DDL,如下图

roomtype 表的 DDL 如下:
CREATE TABLE "roomtype" (
"id" bigint NOT NULL AUTOINCREMENT COMMENT '主键ID',
"name" varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb40900aici NOT NULL COMMENT '房型名称',
"bedcount" int NOT NULL DEFAULT '0' COMMENT '床位数量',
"price" decimal(10,2) NOT NULL COMMENT '床位费用',
"introduction" varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb40900aici DEFAULT NULL COMMENT '介绍',
"photo" varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb40900aici DEFAULT NULL COMMENT '照片',
"status" tinyint NOT NULL COMMENT '状态,0:禁用,1:启用',
"createtime" datetime NOT NULL COMMENT '创建时间',
"updatetime" datetime DEFAULT NULL COMMENT '更新时间',
"createby" bigint DEFAULT NULL COMMENT '创建人id',
"updateby" bigint DEFAULT NULL COMMENT '更新人id',
"remark" varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb40900aici DEFAULT NULL COMMENT '备注',
PRIMARY KEY ("id") USING BTREE,
UNIQUE KEY "name" ("name") USING BTREE
) ENGINE=InnoDB AUTOINCREMENT=97 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb40900aici ROWFORMAT=DYNAMIC COMMENT='房型表';
通过以上的 DDL 的阅读,我们可以清楚的知道里面包含的字段,以及字段代表了什么含义,其中主键为 ID
除了以上的基本信息外,我们还需要额外注意表的其他信息:
- id:主键,使用了 AUTOINCREMENT 修饰,代表的是主键自增
- photo:照片,是 varchar 类型,说明存储的图片的 URL 地址
- status:状态,是 tinyint 类型,有两个值:0 表示禁用,1 表示启用
- name:房型名称,在 DDL 的最后使用了 UNIQUE KEY 进行了修饰,表示添加了唯一约束,存储的内容必须唯一
页面点击访问感受
我们现在启动前后端代码,按照原型图中的导航找到房型设置,自己可以感受一下,可以进行一系列的操作

项目演示地址:https://zhyl-admin.itheima.net/
默认账号:admin@qq.comm 密码:888itcast.CN764%...
阅读对应模块代码,熟悉代码风格和规范
通过刚才的原型分析,当前模块中涉及到了 7 个接口,我们可以一个接口一个接口的阅读。
我们可以通过浏览器中发送的 HTTP 请求来熟悉接口的信息,比如分页列表查询
1.查看浏览器 HTTP 请求
在浏览器中按 F12 调出开发者模式,通过网络可以查看接口的请求响应信息

在这里我们可以清楚的看到接口的路径、请求方式、参数、响应的数据
其中在访问路径中我们需要从 api 后面获取真实的请求 url,其中的 api 以及前面的路径都是 nginx 代理的路径或者是 node 服务启动之后的路径
2.控制层 Controller
找到后端代码对应的接口,我们通过名字来找到对应的 controller,房间设置对应的是:RoomTypeController
在这个类中定义了 6 个方法,如下图

- addRoomType 添加房型
- removeRoomType 删除
- modifyRoomType 修改
- findRoomTypeById 根据 id 查询
- findRoomTypeList 查询房型列表(无参)
- enableOrDisable 启用和禁用
其中的上传图片的接口,属于通用接口,是在 CommonController,我们后面在分析
具体的代码如下:
package com.zzyl.controller;
@RestController
@RequestMapping("/roomTypes")
@Api(tags = "房型管理")
public class RoomTypeController extends BaseController {
@Autowired
private RoomTypeService roomTypeService;
@PostMapping
@ApiOperation("添加房型")
public ResponseResult addRoomType(@RequestBody RoomTypeDto roomTypeDTO) {
roomTypeService.addRoomType(roomTypeDTO);
return success();
}
@DeleteMapping("/{id}")
@ApiOperation("删除房型")
public ResponseResult removeRoomType(
@ApiParam(value = "房型ID", required = true) @PathVariable Long id) {
roomTypeService.removeRoomType(id);
return success();
}
@PutMapping("/{id}")
@ApiOperation("修改房型")
public ResponseResult modifyRoomType(
@ApiParam(value = "房型ID", required = true) @PathVariable Long id,
@RequestBody RoomTypeDto roomTypeDTO) {
roomTypeService.modifyRoomType(id, roomTypeDTO);
return success();
}
@GetMapping("/{id}")
@ApiOperation("根据ID查询房型")
public ResponseResult<RoomTypeVo> findRoomTypeById(
@ApiParam(value = "房型ID", required = true) @PathVariable Long id) {
RoomTypeVo roomTypeVO = roomTypeService.findRoomTypeById(id);
return success(roomTypeVO);
}
@GetMapping
@ApiOperation("查询所有房型")
public ResponseResult<List<RoomTypeVo>> findRoomTypeList() {
List<RoomTypeVo> roomTypeVoList = roomTypeService.findRoomTypeList();
return success(roomTypeVoList);
}
@PutMapping("/{id}/status/{status}")
@ApiOperation("启用/禁用房型")
public ResponseResult enableOrDisable(
@ApiParam(value = "房型ID", required = true) @PathVariable Long id,
@ApiParam(value = "房型状态", required = true) @PathVariable Integer status) {
roomTypeService.enableOrDisable(id, status);
return success();
}
}
通过阅读以上代码,我们可知:
接口的定义是符合 RESTful 风格 的
- GET 查询
- POST 新增
- PUT 修改
- DELETE 删除
所有的接口返回值为 ResponseResult ,有一种通用的响应格式
{
"code": 200,
"msg": "操作成功",
"data": 接口返回的数据(可以是任何类型)
}
- 当前RoomTypeController继承了BaseController

继承以后的controller就可以很方便的使用一些ResponseResult中的方法,比如success、error
接收对象和返回对象分别使用了DTO和VO
- DTO :Data Transfer Object数据传输对象:xxxDto或者xxxDTO,xxx为业务领域相关的名称。接口的入参
- VO :Value Object展示对象:xxxVO或者xxxVo,xxx一般为网页名称。接口的出参
关于api开头的一些注解是在线接口文档的描述,后面我们会专门进行学习
3.业务层Service
业务层:RoomTypeServiceImpl中的代码
package com.zzyl.service.impl;
@Service
public class RoomTypeServiceImpl implements RoomTypeService {
@Autowired
private RoomTypeMapper roomTypeMapper;
@Autowired
private OSSAliyunFileStorageService fileStorageService;
/**
* 添加房间类型
* @param roomTypeDTO 房间类型DTO
*/
@Override
public void addRoomType(RoomTypeDto roomTypeDTO) {
RoomType roomType = BeanUtil.toBean(roomTypeDTO, RoomType.class);
roomTypeMapper.addRoomType(roomType);
}
/**
* 删除房间类型
* @param id 房间类型id
*/
@Override
public void removeRoomType(Long id) {
//先删除图片
RoomType roomType = roomTypeMapper.findRoomTypeById(id);
fileStorageService.delete(roomType.getPhoto());
//删除房间类型
roomTypeMapper.removeRoomType(id);
}
/**
* 修改房间类型
* @param id 房间类型id
* @param roomTypeDTO 房间类型DTO
*/
@Override
public void modifyRoomType(Long id, RoomTypeDto roomTypeDTO) {
RoomType roomType = BeanUtil.toBean(roomTypeDTO, RoomType.class);
roomType.setId(id);
roomTypeMapper.modifyRoomType(roomType);
}
/**
* 根据id查找房间类型
* @param id 房间类型id
* @return 房间类型VO
*/
@Override
public RoomTypeVo findRoomTypeById(Long id) {
RoomType roomType = roomTypeMapper.findRoomTypeById(id);
return BeanUtil.toBean(roomType, RoomTypeVo.class);
}
/**
* 查找所有房间类型
* @return 房间类型VO列表
*/
@Override
public List<RoomTypeVo> findRoomTypeList() {
return roomTypeMapper.findRoomTypeList();
}
@Override
public void enableOrDisable(Long id, Integer status) {
roomTypeMapper.updateStatus(id, status);
}
}
通过阅读以上代码,我们可以得知:
- 所有的数据操作都需要调用 mapper 接口
- 其中有很多对象拷贝,使用的工具 BeanUtil 来进行属性拷贝
RoomType roomType = new RoomType();
//对象属性拷贝,从一个对象中拷贝到另外一个对象中
BeanUtil.copyProperties(roomTypeDTO,roomType);
//把属性拷贝到新创建的对象中
RoomType roomType = BeanUtil.toBean(roomTypeDTO, RoomType.class);
4.持久层 Mapper
持久层:RoomTypeMapper 接口和映射文件
package com.zzyl.mapper;
@Mapper
public interface RoomTypeMapper {
void addRoomType(RoomType roomType);
void removeRoomType(Long id);
void modifyRoomType(RoomType roomType);
RoomType findRoomTypeById(Long id);
List<RoomTypeVo> findRoomTypeList();
void updateStatus(@Param("id") Long id, @Param("status") Integer status);
}
resource/RoomTypeMapper.xml 文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zzyl.mapper.RoomTypeMapper">
<resultMap id="roomTypeMap" type="com.zzyl.vo.RoomTypeVo">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="bedcount" property="bedCount"/>
<result column="price" property="price"/>
<result column="introduction" property="introduction"/>
<result column="photo" property="photo"/>
<result column="status" property="status"/>
<result column="createby" property="createBy"/>
<result column="updateby" property="updateBy"/>
<result column="remark" property="remark"/>
<result column="createtime" property="createTime"/>
<result column="updatetime" property="updateTime"/>
</resultMap>
<insert id="addRoomType">
INSERT INTO roomtype(name, price, introduction, photo, status, createby, updateby, remark, createtime,
updatetime)
VALUES (#{name}, #{price}, #{introduction}, #{photo}, #{status}, #{createBy}, #{updateBy}, #{remark},
#{createTime}, #{updateTime})
</insert>
<delete id="removeRoomType">
DELETE
FROM roomtype
WHERE id = #{id}
</delete>
<update id="modifyRoomType">
UPDATE roomtype
SET name = #{name},
price = #{price},
introduction = #{introduction},
photo = #{photo},
status = #{status},
updatetime = #{updateTime},
updateby = #{updateBy}
WHERE id = #{id}
</update>
<select id="findRoomTypeById" resultMap="roomTypeMap">
SELECT *
FROM roomtype
WHERE id = #{id}
</select>
<select id="findRoomTypeList" resultMap="roomTypeMap">
SELECT t.*, s.realname as creator
FROM roomtype t
LEFT JOIN sysuser s ON t.createby = s.id
order by createtime desc
</select>
<update id="updateStatus">
UPDATE roomtype
SET status = #{status},
updatetime = #{updateTime},
updateby = #{updateBy}
WHERE id = #{id}
</update>
</mapper>
通过阅读以上代码,我们可以得知:
- 所有的操作,sql 语句都写在映射文件 中
- 查询列表的时候,需要返回创建人名称,需要关联查询 sysuser 表来获取用户名称
- 结果封装使用到了 resultMap 标签
总结
课堂作业
基于我们刚才熟悉模块的步骤,大家可以自行熟悉一个新的模块----> 床位房型 🎯

给大家半个小时时间,需要回答以下问题:
写在文档里面,也是晚上的作业!
- 描述一下床位房型模块的业务,有什么作用?
- 描述一下这个模块涉及到了哪些接口,并能说明接口的特点(请求方式、参数,返回)
- 描述一下这个模块涉及到了哪些表,表与表之间是什么关系
- 梳理一下,这里使用到了哪些工具类,有什么作用
4. 如何在项目中开发一个接口
如何在项目中开发一个接口
相关信息
我们现在已经有能力去熟悉模块业务了,现在有了新的任务,在已有的代码的基础上,我们来开发几个功能接口,这次涉及到的是房间模块中的床位相关的接口。
不过呢,在我们开发之前,我们现在需要搞清楚几个问题
- 在前后端项目开发中,后端的开发流程是什么
- 如何设计一个接口,有哪些规则或规范
- 开发过程中会产生什么样的问题
- 开发完成的接口如何测试
那接下来呢,我们依次搞定这些问题。
后端开发流程
在开发接口之前呢,我们需要先熟悉一下,后端开发的流程,如下:

- 需求分析(基于原型和 PRD)
- 开发计划(工期评估)
- 表结构设计(基于原型和 PRD)
- 接口设计(基于原型和 PRD)
- 功能实现(基于接口设计 + 原型 +PRD)
- 前后端联调
- 测试提 bug
- 前后端优化,再联调
- 测试回归 bug
- 功能验收
接口设计规范
需求分析
在接收到任务之后,我们需要查看任务涉及到的模块,需要快速熟悉业务,熟悉业务的参考就是原型文档和 PRD 文档,我们接下来要开发的床位相关的接口,那么就详细查看床位相关的原型图即可。
特殊情况,新需求可能会没有原型图,这时就需要跟产品经理要多沟通,直至搞明白需求为止
接口四要素
搞明白需求之后,我们下面就可以来设计接口了,一个接口包含了四个基本要素,分别是:请求路径、请求方式、接口入参、接口出参
请求路径命名 :以模块名称进行区分(英文)
请求方式(需要符合 restFul 风格)
- 查询 GET
- 新增 POST
- 修改 PUT
- 删除 DELETE
接口入参
- 路径参数
- 问号传参----> 后端形参接收
- path 传参----> 后端 PathVariable 注解接收
- 请求体参数
- 前端:json 对象
- 后端:对象接收,DTO
- 路径参数
接口出参
- 统一格式
{code:200,msg:"成功",data:{}}
- 数据封装,一般为 VO
- 敏感数据过滤
- 整合数据
- 统一格式
接口文档
创建床位
接口地址:/bed/create
请求方式:POST
请求示例:
{
"bedNumber": "901-1",
"sort": 1,
"roomId": "58"
}
响应示例:
{
"code": 0,
"data": {},
"msg": "",
"operationTime": ""
}
根据 ID 查询床位
接口地址:/bed/read/{id}
请求方式:GET
请求示例:
参数名称 | 参数说明 | 是否必须 | 数据类型 |
---|---|---|---|
id | 床位 ID | true | integer(int64) |
响应示例:
{
"code": 200,
"msg": "操作成功",
"data": {
"id": "4",
"bedNumber": "103-1",
"bedStatus": 1,
"roomId": "3",
"sort": 1,
}
}
更新床位
接口地址:/bed/update
请求方式:PUT
请求示例:
{
"bedNumber": "901-1",
"sort": 4,
"id": "170",
"roomId": "66"
}
响应示例:
{
"code": 0,
"data": {},
"msg": "",
"operationTime": ""
}
删除床位
接口地址:/bed/delete/{id}
请求方式:DELETE
请求参数:
参数名称 | 参数说明 | 是否必须 | 数据类型 |
---|---|---|---|
id | 床位 ID | true | integer(int64) |
integer(int64)在后台接收数据类型为 Long
响应示例:
{
"code": 0,
"data": {},
"msg": "",
"operationTime": ""
}
接口测试
测试工具有很多,以下几个是比较常见的接口测试工具
- Postman
- Apifox
- Swagger 在线接口文档
- Knife4j 对 swagger 的增强,可生成离线接口文档
使用 postman 或者 apifox 工具测试接口,需知道明确的接口信息
代码操作
创建床位
(1)接口定义
根据刚才的接口文档,在 BedController 中新增一个方法,如下
@PostMapping("/create")
public ResponseResult createBed(@RequestBody BedDto bedDto) {
return null;
}
BedDto
package com.zzyl.dto;
import com.zzyl.base.BaseDto;
import lombok.Data;
@Data
public class BedDto extends BaseDto {
/**
* 床位编号
*/
private String bedNumber;
/**
* 床位状态: 未入住0, 已入住1
*/
private Integer bedStatus;
/**
* 房间ID
*/
private Long roomId;
/**
* 排序号
*/
private Integer sort;
}
(2)mapper
在 BedMapper 中新增方法
/**
* 增加床位
* @param bed 床位对象
*/
void addBed(Bed bed);
映射文件
<insert id="addBed" parameterType="com.zzyl.entity.Bed">
insert into bed(bednumber, sort, bedstatus, roomid, createby, updateby, remark, createtime, updatetime)
values (#{bedNumber}, #{sort}, #{bedStatus}, #{roomId}, #{createBy}, #{updateBy}, #{remark}, #{createTime},
#{updateTime})
</insert>
(3)业务层
在 BedService 中新增方法
/**
* 添加新的床位
* @param bedDto 床位数据传输对象
*/
void addBed(BedDto bedDto);
实现方法
/**
* 添加新的床位
* @param bedDto 床位数据传输对象
*/
@Override
public void addBed(BedDto bedDto) {
Bed bed = BeanUtil.toBean(bedDto, Bed.class);
bed.setCreateTime(LocalDateTime.now());
bed.setCreateBy(1L);
bed.setBedStatus(0);
bedMapper.addBed(bed);
}
(4)控制层
@PostMapping("/create")
public ResponseResult createBed(@RequestBody BedDto bedDto) {
bedService.addBed(bedDto);
return success();
}
(5)测试
我们可以直接打开前端项目进行测试,是否能保存床位成功
根据 ID 查询床位
(1)接口定义
根据刚才的接口文档,在 BedController 中新增一个方法,如下
@GetMapping("/read/{id}")
public ResponseResult readBed(@PathVariable("id") Long id) {
return null;
}
(2)mapper
在 BedMapper 中新增方法
Bed getBedById(Long id);
mapper 映射文件
<select id="getBedById" resultMap="BedResultMap" parameterType="java.lang.Long">
select id, bednumber, sort, bedstatus, roomid
from bed
where id = #{id}
</select>
(3)业务层
在 BedService 中新增方法
/**
* 通过ID检索床位
* @param id 床位ID
* @return 床位视图对象
*/
BedVo getBedById(Long id);
实现方法:
/**
* 通过ID检索床位
* @param id 床位ID
* @return 床位视图对象
*/
@Override
public BedVo getBedById(Long id) {
return BeanUtil.toBean(bedMapper.getBedById(id), BedVo.class);
}
(4)控制层
补全控制层代码:
@GetMapping("/read/{id}")
public ResponseResult readBed(@PathVariable("id") Long id) {
BedVo bed = bedService.getBedById(id);
return success(bed);
}
(5)测试
打开前端工程页面,是否可以正常查看床位数据
更新床位
(1)接口定义
根据刚才的接口文档,在 BedController 中新增一个方法,如下
@PutMapping("/update")
public ResponseResult updateBed(@RequestBody BedDto bed) {
return null;
}
其中的 BedDto 与新增接口是同一个 Dto
(2)mapper
在 BedMapper 中新增方法
void updateBed(Bed bed);
映射文件
<update id="updateBed" parameterType="com.zzyl.entity.Bed">
update bed
set bednumber = #{bedNumber},
sort = #{sort},
bedstatus = #{bedStatus},
roomid = #{roomId},
updatetime = #{updateTime},
updateby = #{updateBy}
where id = #{id}
</update>
(3)业务层
在 BedService 中新增方法
/**
* 更新现有的床位
* @param bed 床位数据传输对象
*/
void updateBed(BedDto bed);
实现方法
/**
* 更新现有的床位
* @param bedDto 床位数据传输对象
*/
@Override
public void updateBed(BedDto bedDto) {
BedVo bedVo = getBedById(bedDto.getId());
bedVo.setSort(bedDto.getSort());
bedVo.setBedNumber(bedDto.getBedNumber());
if (ObjectUtil.isNotEmpty(bedDto.getBedStatus())) {
bedVo.setBedStatus(bedDto.getBedStatus());
}
Bed bed = BeanUtil.toBean(bedVo, Bed.class);
bed.setCreateTime(LocalDateTime.now());
bed.setCreateBy(1L);
bedMapper.updateBed(bed);
}
(4)控制层
@PutMapping("/update")
public ResponseResult updateBed(@RequestBody BedDto bed) {
bedService.updateBed(bed);
return success();
}
(5)测试
打开前端工程页面,是否可以正常修改床位数据
删除床位
(1)接口定义
根据刚才的接口文档,在 BedController 中新增一个方法,如下
@DeleteMapping("/delete/{id}")
public ResponseResult deleteBed(@PathVariable("id") Long id) {
return null;
}
(2)mapper
在 BedMapper 中新增方法
void deleteBedById(Long id);
映射文件
<delete id="deleteBedById" parameterType="java.lang.Long">
delete
from bed
where id = #{id}
</delete>
(3)业务层
在 BedService 中新增方法
/**
* 通过ID删除床位
* @param id 床位ID
*/
void deleteBedById(Long id);
实现方法
@Override
public void deleteBedById(Long id) {
bedMapper.deleteBedById(id);
}
(4)控制层
@DeleteMapping("/delete/{id}")
public ResponseResult deleteBed(@PathVariable("id") Long id) {
bedService.deleteBedById(id);
return success();
}
(5)测试
打开前端工程页面,是否可以正常删除床位
总结
课堂作业
- 根据上述接口文档提示,和代码提示,完成床位的开发🎤
5. 如何快速搞定在线接口文档
如何快速搞定在线接口文档
相关信息
刚才我们使用 ApiFox 工具测试,都是基于在特别熟悉接口的路径,请求方式,出参和入参的情况下测试的。现在如果我们与前端对接,需要提供详细的接口文档才行,不过在开发的过程中,接口文档可能不能及时的提供,或者是更新不及时,就会造成信息闭塞,造成不必要的效率降低。
所以,一般的前后端分离的项目,都会采用在线的接口工具进行调试,可以实时的展示接口的详细数据
Swagger 介绍
Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务
官网:https://swagger.io/autolink。
它的主要作用是:
- 使得前后端分离开发更加方便,有利于团队协作
- 接口的文档在线自动生成,降低后端开发人员编写接口文档的负担
- 功能测试
Spring 已经将 Swagger 纳入自身的标准,建立了 Spring-swagger 项目,现在叫 Springfox。通过在项目中引入 Springfox ,即可非常简单快捷的使用 Swagger。
knife4j 是为 Java MVC 框架集成 Swagger 生成 Api 文档的增强解决方案,前身是 swagger-bootstrap-ui,取名 knife4j 是希望它能像一把匕首一样小巧,轻量,并且功能强悍!
目前,一般都使用 knife4j 框架。
代码操作
1.导入依赖
导入 knife4j 的 maven 坐标(注意:由于 knife4j 是基于 swagger 的,所以也会自动导入 swagger 的依赖)
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
2.配置类
在配置类中加入 knife4j 相关配置,可以使 knife4j 在全局生效,目的就是项目中的所有接口都生成在线接口文档
在 zzyl-framework 工程中的 config 包下(无需大家编写,固定工具类,直接拷贝即可)
package com.zzyl.config;
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import com.zzyl.properties.SwaggerConfigProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.web.*;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Configuration
@EnableConfigurationProperties(SwaggerConfigProperties.class)
@EnableKnife4j
@Import(BeanValidatorPluginsConfiguration.class)
public class SwaggerConfig {
@Autowired
SwaggerConfigProperties swaggerConfigProperties;
@Bean(value = "defaultApi2")
@ConditionalOnClass(SwaggerConfigProperties.class)
public Docket defaultApi2() {
// 构建API文档 文档类型为swagger2
return new Docket(DocumentationType.SWAGGER_2)
.select()
// 配置 api扫描路径
.apis(RequestHandlerSelectors.basePackage(swaggerConfigProperties.getSwaggerPath()))
// 指定路径的设置 any代表所有路径
.paths(PathSelectors.any())
// api的基本信息
.build().apiInfo(new ApiInfoBuilder()
// api文档名称
.title(swaggerConfigProperties.getTitle())
// api文档描述
.description(swaggerConfigProperties.getDescription())
// api文档版本
.version("1.0") // 版本
// api作者信息
.contact(new Contact(
swaggerConfigProperties.getContactName(),
swaggerConfigProperties.getContactUrl(),
swaggerConfigProperties.getContactEmail()))
.build());
}
/**
* 增加如下配置可解决Spring Boot 6.x 与Swagger 3.0.0 不兼容问题
**/
@Bean
public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(WebEndpointsSupplier webEndpointsSupplier,
ServletEndpointsSupplier servletEndpointsSupplier,
ControllerEndpointsSupplier controllerEndpointsSupplier,
EndpointMediaTypes endpointMediaTypes,
CorsEndpointProperties corsProperties,
WebEndpointProperties webEndpointProperties,
Environment environment) {
List<ExposableEndpoint<?>> allEndpoints = new ArrayList();
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
allEndpoints.addAll(webEndpoints);
allEndpoints.addAll(servletEndpointsSupplier.getEndpoints());
allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
String basePath = webEndpointProperties.getBasePath();
EndpointMapping endpointMapping = new EndpointMapping(basePath);
boolean shouldRegisterLinksMapping = this.shouldRegisterLinksMapping(webEndpointProperties, environment, basePath);
return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes, corsProperties.toCorsConfiguration(),
new EndpointLinksResolver(allEndpoints, basePath), shouldRegisterLinksMapping, null);
}
private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties, Environment environment, String basePath) {
return webEndpointProperties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath) || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT));
}
}
在上述代码中引用了一个配置,用来定制项目中的一些特殊信息,比如扫描的包、项目相关信息等
@Setter
@Getter
@NoArgsConstructor
@ToString
@ConfigurationProperties(prefix = "zzyl.framework.swagger")
public class SwaggerConfigProperties implements Serializable {
/**
* 扫描的路径,哪些接口需要使用在线文档
*/
public String swaggerPath;
/**
* 项目名称
*/
public String title;
/**
* 具体描述
*/
public String description;
/**
* 组织名称
*/
public String contactName;
/**
* 联系网址
*/
public String contactUrl;
/**
* 联系邮箱
*/
public String contactEmail;
}
所以上述代码具体的配置,是在 application.yml
文件中来定义
# 顶格写
spring:
mvc:
pathmatch:
matching-strategy: antpathmatcher
# 顶格写
zzyl:
framework:
swagger:
swagger-path: com.zzyl.controller
title: 智慧养老服务
description: 智慧养老
contact-name: yange研究院
contact-url: www.itheima.com
contact-email: itheima@itcast.cn
注意,在使用 swagger 时候,需要使用 ant 的方式进行匹配路径,需要设置为 antpathmatcher
Ant 是一种风格,简单匹配规则如下:
- ? 匹配一个字符
匹配0个或多个字符
- ** 匹配 0 个或多个目录
3.静态资源映射
如果想要 swagger 生效,还需要设置静态资源映射,否则接口文档页面无法访问,
找到配置类为:WebMvcConfig,添加如下代码:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//支持webjars
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
//支持swagger
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
//支持小刀
registry.addResourceHandler("doc.html")
.addResourceLocations("classpath:/META-INF/resources/");
}
常用注解
通过注解可以控制生成的接口文档,使接口文档拥有更好的可读性,常用注解如下:
注解 | 说明 |
---|---|
@Api | 用在类上,描述 Controller 的作用 |
@ApiOperation | 用在方法上,说明方法的用途、作用 |
@ApiParam | 用在方法的参数上,描述单个形参的含义 |
@ApiImplicitParam | 用在方法上,描述单个形参的含义,与上面相比使用范围更广 |
@ApiModel | 用在类上,用对象来接收参数或者返回参数,描述类的含义 |
@ApiModelProperty | 用在类的属性上,用对象来接收参数或者返回参数,描述字段的含义 |
改造代码
改造我们刚才写的床位的接口 BedController ,改造之后的代码如下:
package com.zzyl.controller;
@RestController
@RequestMapping("/bed")
@Api(tags = "床位管理相关接口")
public class BedController extends BaseController {
@Autowired
private BedService bedService;
@GetMapping("/read/room/{roomId}")
@ApiOperation(value = "根据房间id查询床位", notes = "传入房间id")
public ResponseResult<List<BedVo>> readBedByRoomId(
@ApiParam(value = "房间ID", required = true) @PathVariable("roomId") Long roomId) {
List<BedVo> beds = bedService.getBedsByRoomId(roomId);
return success(beds);
}
@PostMapping("/create")
@ApiOperation(value = "创建床位", notes = "传入床位对象,包括床位号和所属房间号")
public ResponseResult createBed(@RequestBody BedDto bedDto) {
bedService.addBed(bedDto);
return success();
}
@GetMapping("/read/{id}")
@ApiOperation(value = "根据id查询床位", notes = "传入床位id")
public ResponseResult<BedVo> readBed(@ApiParam(value = "床位ID", required = true) @PathVariable("id") Long id) {
BedVo bed = bedService.getBedById(id);
return success(bed);
}
@PutMapping("/update")
@ApiOperation(value = "更新床位", notes = "传入床位对象,包括床位id、床位号、所属房间号等信息")
public ResponseResult updateBed(@RequestBody BedDto bedDto) {
bedService.updateBed(bedDto);
return success();
}
@DeleteMapping("/delete/{id}")
@ApiOperation(value = "删除床位", notes = "传入床位id")
public ResponseResult deleteBed(@ApiParam(value = "床位ID", required = true) @PathVariable("id") Long id) {
bedService.deleteBedById(id);
return success();
}
}
BedDto 改造如下:
package com.zzyl.dto;
@Data
@ApiModel("床位实体类")
public class BedDto extends BaseDto {
/**
* 床位编号
*/
@ApiModelProperty("床位编号")
private String bedNumber;
/**
* 床位状态: 未入住0, 已入住1
*/
@ApiModelProperty(value = "床位状态: 未入住0, 已入住1",example = "0")
private Integer bedStatus;
/**
* 房间ID
*/
@ApiModelProperty("房间ID")
private Long roomId;
/**
* 排序号
*/
@ApiModelProperty(value = "排序号")
private Integer sort;
}
其余的 vo、dto、或者是 controller 都可以使用对应的注解,对接口进行描述
在线文档调试
代码改造完成之后,我们可以启动后端的服务,然后访问在线接口文档, 访问地址:http://ip:port/doc.html

在这里面,我们可以对接口进行查看,比如接口的四要素信息,同时也可以对接口进行调试
AI 工具协助快速生成
打开 openai,把 controller 类的所有代码都给它,在最后添加一句话:
帮我给上述代码中添加swagger注解,需要使用中文描述

通过以上方式,我们就可以快速给 controller、dto、vo 中生成 swagger 注解,提高我们的开发效率
ai 工具提供的中文描述,可能不太精确,需要大家根据实际业务手动调整
总结
课堂作业
- swagger的作用是什么?参考上述步骤,完成在线接口文档的生成🎤
6. 项目中的全局异常处理
前言
项目中的全局异常是如何处理的?
一般项目开发有两种异常:
- 预期异常(程序员手动抛出)
- 运行时异常

在目前的项目中已经提供了全局异常处理器

代码操作
- BaseException 基础异常,如果业务中需要手动抛出异常,则需要抛出该异常
package com.zzyl.exception;
import com.zzyl.enums.BasicEnum;
import lombok.Getter;
import lombok.Setter;
/**
* BaseException
* @author itheima
**/
@Getter
@Setter
public class BaseException extends RuntimeException {
private BasicEnum basicEnum;
public BaseException(BasicEnum basicEnum) {
this.basicEnum = basicEnum;
}
}
其中 BaseException 中的参数为一个枚举,可以在 BasicEnum 自定义业务中涉及到的异常
package com.zzyl.enums;
import com.zzyl.base.IBasicEnum;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 基础枚举
*
* @author itcast
*/
@Getter
@AllArgsConstructor
public enum BasicEnum implements IBasicEnum {
SUCCEED(200, "操作成功"),
SECURITYACCESSDENIEDFAIL(401, "权限不足!"),
LOGINFAIL(401, "用户登录失败"),
LOGINLOSEEFFICACY(401, "登录状态失效,请重新登录"),
SYSYTEMFAIL(500, "系统运行异常"),
//权限相关异常:1400-1499
DEPTDEPTHUPPERLIMIT(1400, "部门最多4级"),
PARENTDEPTDISABLE(1401, "父级部门为禁用状态,不允许启用"),
DEPTNULLEXCEPTION(1402, "部门不能为空"),
POSITIONDISTRIBUTED(1403, "职位已分配,不允许禁用"),
MENUNAMEDUPLICATEEXCEPTION(1404, "菜单路由重复"),
//业务相关异常:1500-1599
WEBSOCKETPUSHMSGERROR(1500, "websocket推送消息失败"),
CLOSEBALANCEERROR(1501, "关闭余额账户失败"),
MONTHBILLDUPLICATEEXCEPTION(1502, "该老人的月度账单已生成,不可重复生成"),
MONTHOUTCHECKINTERM(1503, "该月不在费用期限内");
/**
* 编码
*/
public final int code;
/**
* 信息
*/
public final String msg;
}
- GlobalExceptionHandler 全局异常处理器
package com.zzyl.exception;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import com.zzyl.base.ResponseResult;
import com.zzyl.enums.BasicEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import java.io.FileNotFoundException;
import java.nio.file.AccessDeniedException;
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 处理自定义异常BaseException。
* 返回自定义异常中的错误代码和错误消息。
*
* @param exception 自定义异常
* @return 响应数据,包含错误代码和错误消息
*/
@ExceptionHandler(BaseException.class)
public ResponseResult<Object> handleBaseException(BaseException exception) {
exception.printStackTrace();
if (ObjectUtil.isNotEmpty(exception.getBasicEnum())) {
log.error("自定义异常处理:{}", exception.getBasicEnum().getMsg());
}
return ResponseResult.error(exception.getBasicEnum());
}
/**
* 处理其他未知异常。
* 返回HTTP响应状态码500,包含错误代码和异常堆栈信息。
*
* @param exception 未知异常
* @return 响应数据,包含错误代码和异常堆栈信息
*/
@ExceptionHandler(Exception.class)
public ResponseResult<Object> handleUnknownException(Exception exception) {
exception.printStackTrace();
if (ObjectUtil.isNotEmpty(exception.getCause())) {
log.error("其他未知异常:{}", exception.getMessage());
}
return ResponseResult.error(500,exception.getMessage());
}
}
1. 程序员手动抛出业务异常
当床位新增失败的时候,可以直接抛出 BaseException
@Override
public void addBed(BedDto bedDto) {
Bed bed = BeanUtil.toBean(bedDto, Bed.class);
bed.setCreateTime(LocalDateTime.now());
bed.setCreateBy(1L);
bed.setBedStatus(0);
try {
bedMapper.addBed(bed);
} catch (Exception e) {
throw new BaseException(BasicEnum.BEDINSERTFAIL);
}
}
测试:
当重复录入床位编号的时候,则会抛出:床位新增失败,而此时的执行逻辑就是走了全局异常处理器

2. 不可知异常处理
比如,在文件上传的接口中,如果上传文件失败,则可以抛出 RuntimeException 异常,由于 RuntimeException 异常不是自定义异常,一旦触发就是走全局异常处理器的未知异常
/**
* 文件上传
*
* @param file 文件
* @return 上传结果
* @throws Exception 异常
*/
@PostMapping("/upload")
@ApiOperation("文件上传")
public ResponseResult<String> upload(
@ApiParam(value = "上传的文件", required = true)
@RequestPart("file") MultipartFile file) throws Exception {
// 校验是否为图片文件
try {
BufferedImage bufferedImage = ImageIO.read(file.getInputStream());
} catch (Exception e) {
throw new RuntimeException("上传图片失败");
}
if (file.getSize() == 0) {
throw new RuntimeException("上传图片不能为空");
}
// 获得原始文件名
String originalFilename = file.getOriginalFilename();
// 获得文件扩展名
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
String fileName = UUID.randomUUID().toString() + extension;
String filePath = fileStorageService.store(fileName, file.getInputStream());
return ResponseResult.success(filePath);
}
结论:
- 一旦文件上传失败,则会走全局异常处理器的未知异常
- 如果系统抛出了其他异常,非 BaseException,都会走未知异常
