中州养老 第一章-快速搞定新项目

YangeIT大约 30 分钟中州养老web开发HTMLCSSJavaScript

中州养老 第一章-快速搞定新项目

今日目标

  • 掌握中州养老项目核心业务流程、系统架构、技术架构
  • 掌握快速搞定新项目的环境搭建
  • 掌握快速熟悉一个新项目的方式
  • 掌握如何在新项目中进行接口开发
  • 掌握如何快速生成在线接口文档
  • 掌握项目中的异常处理流程和方式

1. 项目介绍

项目介绍

行业背景

  1. 中国老龄化程度加深,我国老龄事业和养老服务体系的发展得到了国家的高度重视,
  2. 在国家政策的支持下,我国智慧养老产业主体持续增多,产业链不断整合,发展前景较好。
  3. 我国正在形成一个多元化“互联网 + 养老” 的智慧老年护理服务系统,智慧养老是我国的必然趋势

市场规模及预测image

2022年中国养老产业市场规模达到10.3万亿元,同比增长16.7%。

预计2023-2027年中国养老产业迎来较快速增长。

预计2027年中国养老产业市场规模达21.1万亿元

总结

课堂作业

  1. 中州养老项目解决了什么社会需求?🎤
  2. 中州养老项目的技术架构是什么,流利的说出来!

2. 如何快速完成新项目的搭建

如何快速完成新项目的搭建

为什么是 1-2 的项目

通常小伙伴入职一家新公司之后,接手的项目有两种情况,第一个是新项目 ,第二个是老项目继续开发。

根据已毕业学员的调研结果说明,95% 的学员会在老项目的基础上进行继续开发新的功能。这个就是 1-2 的项目。

简单说就是,项目已经开发了一部分,我们需要在已有的项目中进行再次开发。

与之对应的0-1的项目,从头开始的新项目

image
image

所以,训练大家在已有项目中进行开发是一个非常有必要的能力,那么,我们如何在老项目中进行开发呢?

其实,别管是什么项目,我们有两个问题需要先解决

  • 1️⃣如何快速完成新项目的环境搭建
  • 2️⃣如何快速熟悉一个新的项目

养老项目就是一个 1-2 的项目,在功能开发之前,我们需要先解决上述的两个问题。那么接下来我们先来搞定第一个问题:如何快速完成新项目的环境搭建

总结

课堂作业

  1. 实际企业项目中,是0-1的项目,还是1-2类型的多一些?针对这两种项目类型,需要做哪些准备工作?🎤
  2. 参考上述的链接文档,简要的阅读文档,到时候能快速定位资料的位置!
  3. 参考上述资料,将前后端代码跑起来!!🎯

3. 如何快速熟悉项目中的一个模块

如何快速熟悉项目中的一个模块

熟悉模块的方式

  1. 现在前后端代码,已经能够正常跑起来了,我们现在的目标就是来熟悉项目,不过我们并不能一下子对项目全部熟悉,而是需要逐步进行拆解分析,直至熟悉。

  2. 在一开始,我们可以找到项目中的一个模块来进行熟悉,也就说先找到一个切入口。那么熟悉项目的方式有很多种渠道,我们需要通过各个渠道来深入了解。

下面列了一些比较常见的熟悉项目的方式: 👇

  • 阅读原型文档 + 需求文档 PRD(Product Requirements Document:产品需求文档)
  • 阅读表结构(数据库
  • 页面点击访问感受(UI页面
  • 阅读对应模块代码,熟悉代码风格和规范(源代码
image
image

熟悉房型设置模块操作

下面,我们可以先熟悉已有代码中开发完的一个小模块,房型设置

阅读原型图和 PRD

项目原型地址:https://rp-java.itheima.net/zhyl/open in new window

导航: 后台原型图-> 在住管理-> 床位管理-> 房型设置

通过阅读文档,我们可以得知: 👇

  1. 房型设置是养老院中管理不同的房间类型,比如有:四人间、三人间、普通单人间、特护房等等
  2. 这里面共涉及到了 7 个接口,分别是:新增、编辑、删除、分页查询、图片上传、启用禁用、根据 id 查询

一般原型文档,阅读到什么程度呢?

  1. 了解产品的背景和定位,熟悉业务流程
  2. 完整且仔细的了解功能描述,和系统流程
  3. 确认需求理解没有偏差(与产品经理沟通)

总结

课堂作业

基于我们刚才熟悉模块的步骤,大家可以自行熟悉一个新的模块----> 床位房型 🎯

给大家半个小时时间,需要回答以下问题:

写在文档里面,也是晚上的作业!

  • 描述一下床位房型模块的业务,有什么作用?
  • 描述一下这个模块涉及到了哪些接口,并能说明接口的特点(请求方式、参数,返回)
  • 描述一下这个模块涉及到了哪些表,表与表之间是什么关系
  • 梳理一下,这里使用到了哪些工具类,有什么作用

4. 如何在项目中开发一个接口

如何在项目中开发一个接口

相关信息

我们现在已经有能力去熟悉模块业务了,现在有了新的任务,在已有的代码的基础上,我们来开发几个功能接口,这次涉及到的是房间模块中的床位相关的接口。

不过呢,在我们开发之前,我们现在需要搞清楚几个问题

    1. 在前后端项目开发中,后端的开发流程是什么
    1. 如何设计一个接口,有哪些规则或规范
    1. 开发过程中会产生什么样的问题
    1. 开发完成的接口如何测试

那接下来呢,我们依次搞定这些问题。

后端开发流程

在开发接口之前呢,我们需要先熟悉一下,后端开发的流程,如下:

  • 需求分析(基于原型和 PRD)
  • 开发计划(工期评估)
  • 表结构设计(基于原型和 PRD)
  • 接口设计(基于原型和 PRD)
  • 功能实现(基于接口设计 + 原型 +PRD)
  • 前后端联调
  • 测试提 bug
  • 前后端优化,再联调
  • 测试回归 bug
  • 功能验收

代码操作

创建床位

(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)测试

我们可以直接打开前端项目进行测试,是否能保存床位成功

总结

课堂作业

  1. 根据上述接口文档提示,和代码提示,完成床位的开发🎤

5. 如何快速搞定在线接口文档

如何快速搞定在线接口文档

相关信息

刚才我们使用 ApiFox 工具测试,都是基于在特别熟悉接口的路径,请求方式,出参和入参的情况下测试的。现在如果我们与前端对接,需要提供详细的接口文档才行,不过在开发的过程中,接口文档可能不能及时的提供,或者是更新不及时,就会造成信息闭塞,造成不必要的效率降低。

所以,一般的前后端分离的项目,都会采用在线的接口工具进行调试,可以实时的展示接口的详细数据

Swagger 介绍

Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务

官网:https://swagger.io/autolinkopen in new window

它的主要作用是:

  • 使得前后端分离开发更加方便,有利于团队协作
  • 接口的文档在线自动生成,降低后端开发人员编写接口文档的负担
  • 功能测试

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/");
}

总结

课堂作业

  1. 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());
    }

}