项目背景:
项目业务介绍:
**技术架构图:**
开发配置环境、git等常规配置此处跳过
页面UI分析:
课程id
、课程名称、任务数、创建时间、是否付费、审核状态、类型,操作因为是分页查询所以查询结果中还要包括总记录数、当前页、每页显示记录数
。创建数据库表与PO类:
接口设计分析:
确定协议:
通常协议采用HTTP,查询类接口通常为get或post,查询条件较少的使用get,较多的使用post
分析响应结果:
。 根据前边对数据模型的分析,响应结果为数据列表加一些分页信息
(总记录数、当前页、每页显示记录数)。
。 数据列表中数据的属性包括:课程id、课程名称、任务数、创建时间、审核状态、类型
。 根据分析的响应结果定义模型类,如工作中常见的命名如AjaxResult、ResponseResult类
POST /content/course/list?pageNo=2&pageSize=1
Content-Type: application/json
{
"auditStatus": "202002",
"courseName": "",
"publishStatus":""
}
###成功响应结果
{
"items": [
{
"id": 26,
"companyId": 1232141425,
"companyName": null,
"name": "spring cloud实战",
"users": "所有人",
"tags": null,
"mt": "1-3",
"mtName": null,
"st": "1-3-2",
"stName": null,
"grade": "200003",
"teachmode": "201001",
"description": "本课程主要从四个章节进行讲解: 1.微服务架构入门 2.spring cloud 基础入门 3.实战Spring Boot 4.注册中心eureka。",
"pic": "https://cdn.educba.com/academy/wp-content/uploads/2018/08/Spring-BOOT-Interview-questions.jpg",
"createDate": "2019-09-04 09:56:19",
"changeDate": "2021-12-26 22:10:38",
"createPeople": null,
"changePeople": null,
"auditStatus": "202002",
"auditMind": null,
"auditNums": 0,
"auditDate": null,
"auditPeople": null,
"status": 1,
"coursePubId": null,
"coursePubDate": null
}
],
"counts": 23,
"page": 2,
"pageSize": 1
}
分页查询模型类
由于分页查询这一类的接口在项目较多,这里针对分页查询的参数(当前页码、每页显示记录数)单独在xuecheng-plus-base
基础工程中定义。
package com.xuecheng.base.model;
import lombok.Data;
import lombok.ToString;
import lombok.extern.java.Log;
/**
* @description 分页查询通用参数
*/
@Data
@ToString
public class PageParams {
//当前页码
private Long pageNo = 1L;
//每页记录数默认值
private Long pageSize =10L;
public PageParams(){
}
public PageParams(long pageNo,long pageSize){
this.pageNo = pageNo;
this.pageSize = pageSize;
}
}
定义j接收前端的查询结果模型类,即dto
package com.xuecheng.content.model.dto;
import lombok.Data;
import lombok.ToString;
/**
* @description 课程查询参数Dto
*/
@Data
@ToString
public class QueryCourseParamsDto {
//审核状态
private String auditStatus;
//课程名称
private String courseName;
//发布状态
private String publishStatus;
}
定义返回给前端的
响应模型类
所有分页查询的返回结果是一个固定的格式,定义一个响应类在base下,方便以后复用
package com.xuecheng.base.model;
import lombok.Data;
import lombok.ToString;
import java.io.Serializable;
import java.util.List;
/**
* @description 分页查询结果模型类
*/
@Data
@ToString
public class PageResult<T> implements Serializable {
// 数据列表
private List<T> items;
//总记录数
private long counts;
//当前页码
private long page;
//每页记录数
private long pageSize;
public PageResult(List<T> items, long counts, long page, long pageSize) {
this.items = items;
this.counts = counts;
this.page = page;
this.pageSize = pageSize;
}
}
数据这里使用泛型,以后查询返回的是User对象,则泛型用User,查询返回的是Book对象,则填Book
@RestController
public class CourseBaseInfoController {
@PostMapping("/course/list")
public PageResult<CourseBase> list(PageParams pageParams, @RequestBody(required=false) QueryCourseParamsDto queryCourseParams){
return null;
}
}
说明:pageParams分页参数通过url的key/value传入,queryCourseParams通过json数据传入,使用@RequestBody注解将json转成QueryCourseParamsDto对象。
@RequestBody后添加(required=false)表示此参数不是必填项(required默认为true,即必填) ,不加这个注解,当过滤条件为空,即没有传递json内容时,会导致400错误
dto、po、vo的解释:
DTO用于接口层向业务层之间传输数据
,即controller层的形参常为一个DTO,传给Service层PO用于业务层与持久层之间传输数据
,即service层的形参常为PO,调用mapper层的时候,传给mapper层VO对象用在前端与接口层之间传输数据
,查询到的结果封装VO对象为data,加上code、message后封装成一个AjaxResult结果类,由controller层返回给前端场景:
。 手机查询:查询结果只要课程名称和课程状态
。 PC查询:可以根据课程名称、课程状态、课程审核状态等条件查询,查询显示的结果也比手机查询结果内容多
。 此时,Service业务层尽量提供一个业务接口,即使两个前端接口需要的数据不一样,Service可以提供一个最全查询结果,由Controller进行数据整合。
接口文档生成工具—swapper
<dependency>
<groupId>com.spring4allgroupId>
<artifactId>swagger-spring-boot-starterartifactId>
dependency>
@Api(value = "课程信息编辑接口",tags = "课程信息编辑接口")
@RestController
public class CourseBaseInfoController {
@ApiOperation("课程查询接口")
@PostMapping("/course/list")
public PageResult<CourseBase> list(PageParams pageParams, @RequestBody(required=false) QueryCourseParamsDto queryCourseParams){
//....
}
}
启动服务,工程启动起来,访问http://localhost:63040/content/swagger-ui.html查看接口信息
@Api:修饰整个类,描述Controller的作用
@ApiOperation:描述一个类的一个方法,或者说一个接口
@ApiParam:单个参数描述
@ApiModel:用对象来接收参数
@ApiModelProperty:用对象接收参数时,描述对象的一个字段
@ApiResponse:HTTP响应其中1个描述
@ApiResponses:HTTP响应整体描述
@ApiIgnore:使用该注解忽略这个API
@ApiError :发生错误返回的信息
@ApiImplicitParam:一个请求参数
@ApiImplicitParams:多个请求参数
接口定义出来以后,接下来先mapper层,再写service层:
直接使用插件生成实体类和Mapper接口以及Mapper.xml或者手写Mapper接口,再继承BaseMapper
分页查询
(select * from table where a) 转换为 (select count(*) from table where a)和(select * from table where a limit ,)
测试Mapper
@SpringBootTest
class CourseBaseMapperTests {
@Autowired
CourseBaseMapper courseBaseMapper;
@Test
void testCourseBaseMapper() {
CourseBase courseBase = courseBaseMapper.selectById(74L);
Assertions.assertNotNull(courseBase);
//测试查询接口
LambdaQueryWrapper<CourseBase> queryWrapper = new LambdaQueryWrapper<>();
//查询条件
QueryCourseParamsDto queryCourseParamsDto = new QueryCourseParamsDto();
queryCourseParamsDto.setCourseName("java");
queryCourseParamsDto.setAuditStatus("202004");
queryCourseParamsDto.setPublishStatus("203001");
//拼接查询条件
//根据课程名称模糊查询 name like '%名称%'
queryWrapper.like(StringUtils.isNotEmpty(queryCourseParamsDto.getCourseName()),CourseBase::getName,queryCourseParamsDto.getCourseName());
//根据课程审核状态
queryWrapper.eq(StringUtils.isNotEmpty(queryCourseParamsDto.getAuditStatus()),CourseBase::getAuditStatus,queryCourseParamsDto.getAuditStatus());
//使用之前定义的分页模型类,分页参数
PageParams pageParams = new PageParams();
pageParams.setPageNo(1L);//页码
pageParams.setPageSize(3L);//每页记录数
Page<CourseBase> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
//分页查询E page 分页参数, @Param("ew") Wrapper queryWrapper 查询条件
Page<CourseBase> pageResult = courseBaseMapper.selectPage(page, queryWrapper);
//数据
List<CourseBase> items = pageResult.getRecords();
//总记录数
long total = pageResult.getTotal();
//准备返回数据 List items, long counts, long page, long pageSize
PageResult<CourseBase> courseBasePageResult = new PageResult<>(items, total, pageParams.getPageNo(), pageParams.getPageSize());
System.out.println(courseBasePageResult);
}
}
queryWrapper.like(StringUtils.isNotEmpty(queryCourseParamsDto.getCourseName()),CourseBase::getName,queryCourseParamsDto.getCourseName())
;
如果在课程table中新增一个字段,似乎也可以实现,但当用户需求变更,如想改"审核未通过"为"未通过",每次去改成千上万行数据,可维护性太差。
和审核状态同类的有好多这样的信息,比如:课程状态、课程类型、用户类型等等,这一类数据有一个共同点就是它有一些分类项,且这些分类项较为固定
。针对这些数据,为了提高系统的可扩展性,专门定义数据字典表去维护
。
服务层是controller层来调,所以service层接口定义时,关于方法的返回值类型,思路有:
看controller层的返回值类型,如我们这个练习中的PageResult<>,那service层方法返回同一个类型也好,此时controller直接 return service.method();
即可
或者返回一个VO对象,再调用公司的统一结果类,比如若依的AjaxResult,传入VO对象做为data,此时service层方法返回值类型为VO对应的类型
返回其他类型,此时和controller层类型不一样,在controller层不能直接return,可通过set、get或者其他common类中的方法包装成需要的类型来return
关于形参:
/**
* @description 课程基本信息管理业务接口
*/
public interface CourseBaseInfoService {
/*
* @description 课程查询接口
* @param pageParams 分页参数
* @param queryCourseParamsDto 条件条件
*/
PageResult<CourseBase> queryCourseBaseList(PageParams pageParams, QueryCourseParamsDto queryCourseParamsDto);
}
}
写接口的实现类
:!!!!!!!!!!!!!!!
/**
* @description 课程信息管理业务接口实现类
*/
@Service
public class CourseBaseInfoServiceImpl implements CourseBaseInfoService {
@Autowired
CourseBaseMapper courseBaseMapper;
@Override
public PageResult<CourseBase> queryCourseBaseList(PageParams pageParams, QueryCourseParamsDto queryCourseParamsDto) {
//构建查询条件对象
LambdaQueryWrapper<CourseBase> queryWrapper = new LambdaQueryWrapper<>();
//构建查询条件,根据课程名称查询
queryWrapper.like(StringUtils.isNotEmpty(queryCourseParamsDto.getCourseName()),CourseBase::getName,queryCourseParamsDto.getCourseName());
//构建查询条件,根据课程审核状态查询
queryWrapper.eq(StringUtils.isNotEmpty(queryCourseParamsDto.getAuditStatus()),CourseBase::getAuditStatus,queryCourseParamsDto.getAuditStatus());
//构建查询条件,根据课程发布状态查询
queryWrapper.eq(StringUtils.isNotEmpty(queryCourseParamDto.getPublishStatus()),CourseBase:getPublishStatus,queryCourseParamDto.getPublishStatus());
//分页对象
Page<CourseBase> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
// 查询数据内容获得结果
Page<CourseBase> pageResult = courseBaseMapper.selectPage(page, queryWrapper);
// 获取数据列表
List<CourseBase> list = pageResult.getRecords();
// 获取数据总数
long total = pageResult.getTotal();
// 构建结果集
PageResult<CourseBase> courseBasePageResult = new PageResult<>(list, total, pageParams.getPageNo(), pageParams.getPageSize());
return courseBasePageResult;
}
}
@SpringBootTest
class CourseBaseInfoServiceTests {
@Autowired
CourseBaseInfoService courseBaseInfoService;
@Test
void testCourseBaseInfoService() {
//查询条件,相当于前端筛选框输入后点查询传过来的
QueryCourseParamsDto queryCourseParamsDto = new QueryCourseParamsDto();
queryCourseParamsDto.setCourseName("java");
queryCourseParamsDto.setAuditStatus("202004");
queryCourseParamsDto.setPublishStatus("203001");
//分页参数
PageParams pageParams = new PageParams();
pageParams.setPageNo(1L);//页码
pageParams.setPageSize(3L);//每页记录数
PageResult<CourseBase> courseBasePageResult = courseBaseInfoService.queryCourseBaseList(pageParams, queryCourseParamsDto);
System.out.println(courseBasePageResult);
}
}
在开发完mapper层和service层后,controller层不再用return null来占位,自动注入service层对象,调用service层方法。
@RestController
public class CourseBaseInfoController {
@Autowire
CourseBaseInfoService courseBaseInfoService;
@PostMapping("/course/list")
public PageResult<CourseBase> list(PageParams pageParams, @RequestBody(required=false) QueryCourseParamsDto queryCourseParams){
//service层这里返回的本来就是PageResult类型,直接return就行
return courseBaseInfoService.queryCourseBaseList(pageParams,queryCourseParams);
}
}
到此,接口开发完成,总结下以上的流程
:
安装:
Swagger是一个在线接口文档,虽然使用它也能测试但需要浏览器进入Swagger,最关键的是它并不能保存测试数据,可使用IDEA中的插件HttpClient:
用法:
优化:
{
"dev": {
"access_token": "",
"gateway_host": "localhost:63010",
"content_host": "localhost:63040",
"system_host": "localhost:63110",
"media_host": "localhost:63050",
"search_host": "localhost:63080",
"auth_host": "localhost:63070",
"checkcode_host": "localhost:63075",
"learning_host": "localhost:63020"
}
}
此时:再回到xc-content-api.http文件,将http://localhost:63040 用变量代替
实现设计澄清后,前后端开始照着接口文档同时开发前后端,此时前端开发人员会使用mock数据(假数据)进行开发。后端开发完成后,前端工程师将mock数据改为请求后端接口获取,前端代码请求后端服务测试接口是否正常,这个过程是前后端联调
。
拷贝前端工程,并使用IDEA启动:从前端工程拷贝project-xczx2-portal-vue-ts.zip到代码目录并解压,并使用IDEA或VS Code打开project-xczx2-portal-vue-ts目录
如果存在问题通过以下命令启动:
--------------------
1、cmd进入工程根目录
2、运行以下命令
npm install -g cnpm --registry=https://registry.npm.taobao.org
cnpm i
npm run serve
在浏览器通过http://localhost:8601/地址访问前端工程,有个接口报错:
即System服务异常,这个接口是前端请求后端获取数据字典数据的接口
进入system模块,找到resources下的application.yml修改数据库连接参数,系统服务的端口是63110。启动系统管理服务,启动成功,在浏览器请求:http://localhost:63110/system/dictionary/all
跨域报错
在浏览器通过http://localhost:8601/地址访问前端工程。
Access to XMLHttpRequest at 'http://localhost:63110/system/dictionary/all'
from origin 'http://localhost:8601'
has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
firefox浏览器报错如下:
已拦截跨源请求:同源策略禁止读取位于
http://localhost:63110/system/dictionary/all 的远程资源。
(原因:CORS 头缺少 'Access-Control-Allow-Origin')。状态码:200。
报错分析:
从http://localhost:8601访问http://localhost:63110/system/dictionary/all被CORS policy阻止,因为没有Access-Control-Allow-Origin 头信息。CORS全称是 cross origin resource share 表示跨域资源共享。
出这个提示的原因是基于浏览器的同源策略
,去判断是否跨域请求,同源策略是浏览器的一种安全机制,从一个地址请求另一个地址,如果协议、主机、端口三者全部一致则不属于跨域
,否则有一个不一致就是跨域请求。
如:
解决思路1:
服务器收到请求判断这个Origin是否允许跨域,如果允许则在响应头中说明允许该来源的跨域请求:
Access-Control-Allow-Origin:http://localhost:8601
如果允许任何域名来源的跨域请求,则响应如下:
Access-Control-Allow-Origin:*
解决思路2:JSONP
解决思路3:Nginx做代理
由于服务端之间没有跨域,浏览器通过nginx去访问跨域地址
解决跨域报错
使用上面的方式一来解决:在内容管理的api工程config包下编写GlobalCorsConfig.java
/**
* @description 跨域过虑器
*/
@Configuration
public class GlobalCorsConfig {
/**
* 允许跨域调用的过滤器
*/
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
//允许白名单域名进行跨域调用
config.addAllowedOrigin("*");
//允许跨越发送cookie
config.setAllowCredentials(true);
//放行全部原始头信息
config.addAllowedHeader("*");
//允许所有请求方法跨域调用
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
此配置类实现了跨域过虑器,在响应头添加Access-Control-Allow-Origin。