项目学习 鱼皮 API 开放平台 stateful-backend 项目总结

时间:2023-08-26
本文章向大家介绍项目学习 鱼皮 API 开放平台 stateful-backend 项目总结,主要内容包括项目介绍、用户中心、功能介绍、数据库表、标准 CRUD、机制介绍、前端友好响应机制、使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

项目介绍

用户中心

功能介绍

提供了一套基于 Session 的用户中心,提供以下功能

  • 登入
  • 登出
  • 注册
  • 用户信息管理
      • 登录用户查询
      • ID 查询
      • 列表查询
      • 分页查询
  • 用户态记录
数据库表
create table user
(
    id           bigint auto_increment comment 'id'
        primary key,
    userAccount  varchar(256)                           not null comment '账号',
    userRole     varchar(256) default 'user'            not null comment '用户角色:user / admin',
    userPassword varchar(512)                           not null comment '密码',
  
    createTime   datetime     default CURRENT_TIMESTAMP not null comment '创建时间',
    updateTime   datetime     default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
    isDelete     tinyint      default 0                 not null comment '是否删除',
    constraint uni_userAccount
        unique (userAccount)
)
    comment '用户';

标准 CRUD

功能介绍

提供了一套标准的 CRUD 解决方案

  • Sample 信息管理
      • ID 查询
      • 列表查询
      • 分页查询
  • 接口鉴权
数据库表
(
    id           bigint                             not null comment 'id'
        primary key,
    sampleTest   varchar(256)                       not null comment '样例文本',
    sampleStatus int      default 0                 not null comment '样例状态',
  
    createTime   datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    updateTime   datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
    isDelete     tinyint  default 0                 not null comment '是否删除'
)
    comment '样例';

机制介绍

前端友好响应机制

机制原理
  • 抽象出统一的响应形式 → 规范前端响应的格式
  • 将响应流程封装成方法 → 增强后端代码可读性
  • 抽象出规范错误码描述 → 规范错误描述的格式
机制实现
统一响应
/**
 * 通用返回对象
 *
 * @param <T>
 */
@Data
public class BaseResponse<T> implements Serializable {
    private int code;
    private T data;
    private String message;
    private String description;

    public BaseResponse(int code, T data, String messgae, String description) {
        this.code = code;
        this.data = data;
        this.message = messgae;
        this.description = description;
    }

    public BaseResponse(int code, T data) {
        this(code, data, "", "");
    }

    public BaseResponse(ErrorCode errorCode) {
        this(errorCode.getCode(), null, errorCode.getMessage(), errorCode.getDescription());
    }

    public BaseResponse(ErrorCode errorCode, String message, String description) {
        this(errorCode.getCode(), null, message, description);
    }

    public BaseResponse(ErrorCode errorCode, String description) {
        this(errorCode.getCode(), null, errorCode.getMessage(), description);
    }

    public BaseResponse(int errorCode, String message, String description) {
        this(errorCode, null, message, description);
    }
}
封装方法
/**
 * 返回工具类
 *
 */
public class ResultUtils {
    /**
     * 成功
     *
     * @param data
     * @param <T>
     * @return
     */
    public static <T> BaseResponse<T> success(T data){
        return new BaseResponse<T>(ErrorCode.SUCCESS.getCode(),data,ErrorCode.SUCCESS.getMessage(),"");
    }

    /**
     * 失败
     *
     * @param errorCode
     * @return
     */
    public static BaseResponse error(ErrorCode errorCode){
        return new BaseResponse(errorCode);
    }

    /**
     * 失败
     *
     * @param errorCode
     * @param message
     * @param description
     * @return
     */
    public static BaseResponse error(ErrorCode errorCode, String message, String description) {
        return new BaseResponse(errorCode,message,description);
    }

    /**
     * 失败
     *
     * @param errorCode
     * @param message
     * @param description
     * @return
     */
    public static BaseResponse error(int errorCode, String message, String description) {
        return new BaseResponse(errorCode,message,description);
    }
}
错误码
/**
 * 错误码
 */
public enum ErrorCode {
    SUCCESS(200, "ok",""),
    // 客户端错误(请求相关)
    PARAMS_ERROR(40000, "请求参数错误",""),
    PARAMS_NULL(40001,"请求参数为空",""),
    REQUEST_NULL(40002,"请求为空",""),
    NOT_FOUND_ERROR(40400, "请求数据不存在",""),
    // 客户端错误(权限相关)
    NOT_LOGIN_ERROR(40100, "未登录",""),
    NO_AUTH_ERROR(40101, "无权限",""),
    FORBIDDEN_ERROR(40300, "禁止访问",""),
    // 服务端错误
    SYSTEM_ERROR(50000, "系统内部异常",""),
    OPERATION_ERROR(50001, "操作失败","");

    private final int code;
    private final String message;
    private final String description;

    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }

    public String getDescription() {
        return description;
    }

    ErrorCode(int code, String message, String description) {
        this.code = code;
        this.message = message;
        this.description = description;
    }

}
机制使用
机制载入

一般配合统一异常处理机制一起使用

复制 common 文件夹下的

  • BaseResponse
  • ErrorCode
  • ResultUtils

即可使用

机制使用

在 Controller 中统一使用 BaseResponse<T> 返回正常响应

@GetMapping("/hello")
public BaseResponse<String> hello() {

    return ResultUtils.success("hello");
}

在 全局异常处理器 或 需要返回错误响应的方法中 中统一使用 BaseResponse<T> 返回异常响应

统一异常处理机制

机制原理
  • 声明自定义异常
  • AOP 统一捕获并处理异常
机制实现
业务异常
/**
 * 自定义异常
 */
public class BusinessException extends RuntimeException {
    private final int code;
    private final String description;

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

    public int getCode() {
        return code;
    }

    public String getDescription() {
        return description;
    }

    public BusinessException(ErrorCode errorCode) {
        super(errorCode.getMessage());
        this.code = errorCode.getCode();
        this.description = errorCode.getDescription();
    }

    public BusinessException(ErrorCode errorCode, String description) {
        super(errorCode.getMessage());
        this.code = errorCode.getCode();
        this.description = description;
    }

}
拦截器配置

全局异常处理器

@RestControllerAdvice
@Slf4j
/**
 *  全局异常处理器
 */
public class GlobalExceptionHandler {
    @ExceptionHandler(BusinessException.class)//过滤,只捕获特定异常
    public BaseResponse<?> businessExceptionHandler(BusinessException e) {
        log.error("businessException:" + e.getMessage(), e);
        return ResultUtils.error(e.getCode(), e.getMessage(), e.getDescription());
    }

    @ExceptionHandler(RuntimeException.class)//过滤,只捕获特定异常
    public BaseResponse<?> runtimeExceptionHandler(HttpServletResponse res, RuntimeException e) {
        log.error("runtimeException", e);
        return ResultUtils.error(ErrorCode.SYSTEM_ERROR, e.getMessage(), "");
    }
}
机制使用
机制载入

统一异常处理机制依赖于前端友好响应机制,需要先载入前端友好响应机制,在此基础上

复制 exception 文件夹下的

  • BusinessException
  • GlobalExceptionHandler
机制使用

在需要抛出业务异常的位置抛出 BusinessException 即可

运行时异常无需额外配置,在该抛出异常的位置正常抛出异常即可

请求响应日志机制

机制原理

AOP

机制实现
拦截器配置
/**
 * 请求响应日志 AOP
 *
 **/
@Aspect
@Component
@Slf4j
public class LogInterceptor {

    /**
     * 拦截
     * 范围:controller 目录下的所有方法
     *
     * @param point
     * @return
     * @throws Throwable
     */
    @Around("execution(* com.example.statefulbackend.controller.*.*(..))")
    public Object logInterceptor(ProceedingJoinPoint point) throws Throwable {
        // 计时
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        // 获取请求路径
        RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
        HttpServletRequest httpServletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
        // 生成请求唯一 id
        String requestId = UUID.randomUUID().toString();
        String url = httpServletRequest.getRequestURI();
        // 获取请求参数
        Object[] args = point.getArgs();
        String reqParam = "[" + StringUtils.join(args, ", ") + "]";
        // 输出请求日志
        log.info("request start,id: {}, path: {}, ip: {}, params: {}", requestId, url,
                httpServletRequest.getRemoteHost(), reqParam);
        // 执行原方法
        Object result = point.proceed();
        // 输出响应日志
        stopWatch.stop();
        long totalTimeMillis = stopWatch.getTotalTimeMillis();
        log.info("request end, id: {}, cost: {}ms", requestId, totalTimeMillis);
        return result;
    }
}
机制使用
机制载入

在 Maven 中导入相关依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>
机制使用

复制 aop 文件夹下的 LogInterceptor 即可

会话注解鉴权机制

机制原理

本质上就是自定义注解

注解 + 注解处理器

机制实现
用户态信息获取
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
        implements UserService {


    /**
     * 获取当前登录用户
     *
     * @param request
     * @return
     */
    @Override
    public User getLoginUser(HttpServletRequest request) {
        // 先判断是否已登录
        Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
        User currentUser = (User) userObj;
        if (currentUser == null || currentUser.getId() == null) {
            throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
        }
        // 从数据库查询(追求性能的话可以注释,直接走缓存)
        long userId = currentUser.getId();
        currentUser = this.getById(userId);
        if (currentUser == null) {
            throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
        }
        //返回脱敏对象
        return getSafeUser(currentUser);
    }
  
	...
  
}
注解
/**
 * 权限校验
 *
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthCheck {

    /**
     * 有任何一个角色
     *
     * @return
     */
    String[] hasRole() default "";


    /**
     * 必须有某个角色
     *
     * @return
     */
    String mustRole() default "";

}
注解处理器
/**
 * 权限校验 AOP
 */
@Aspect
@Component
public class AuthInterceptor {

    @Resource
    private UserService userService;

    /**
     * 执行拦截
     *
     * @param joinPoint
     * @param authCheck
     * @return
     */
    @Around("@annotation(authCheck)")
    public Object authCheckInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable {
        //获取注解属性
        //获取 anyRole 数组中的元素
        List<String> anyRole = Arrays
                                .stream(authCheck.hasRole())
                                .filter(StringUtils::isNotBlank)
                                .collect(Collectors.toList());
        //获取 mustRole 数组中的元素
        String mustRole = authCheck.mustRole();

        //获取请求属性对象
        // RequestContextHolder是Spring提供的一个工具类
        // currentRequestAttributes()方法返回当前线程的请求属性对象。
        RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();

        //获取请求
        HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();

        //获取当前登录用户
        User user = userService.getLoginUser(request);

        //配置所需权限中拥有任意权限即通过
        if (CollectionUtils.isNotEmpty(anyRole)) {
            //获取用户拥有的权限
            String userRole = user.getUserRole();
            //若所需权限不包含用户拥有的权限则抛出异常
            if (!anyRole.contains(userRole)) {
                throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
            }
        }

        //配置所需权限中必须有所有权限才通过
        if (StringUtils.isNotBlank(mustRole)) {
            //获取用户拥有的权限
            String userRole = user.getUserRole();
            //若用户权限不为所需权限则抛出异常
            if (!mustRole.equals(userRole)) {
                throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
            }
        }

        // 通过权限校验,放行
        return joinPoint.proceed();
    }
}
机制使用
机制载入

依赖于 userService 中的 用户态信息获取方法,仅本项目可用

机制使用

在需要拦截的方法前加注解,一般放在 Controller 中

@AuthCheck(mustRole = "admin")
@PostMapping("/list/page")
public BaseResponse<Page<SampleVO>> listSampleByPage(SampleQueryRequest sampleQueryRequest, HttpServletRequest request) {
	// 方法体省略
}
@AuthCheck(hasRole = {"admin","user"})
@PostMapping("/list/page")
public BaseResponse<Page<SampleVO>> listSampleByPage(SampleQueryRequest sampleQueryRequest, HttpServletRequest request) {
	// 方法体省略
}

标准增删改查机制(带分页查询)

机制原理
  • 标准化
    • 请求
    • 响应
    • 数据库表
  • 复用 MyBatis Plus 中现成的方法
  • 配置 MyBatis Plus 中的分页插件
  • 实现合法性检验方法
机制实现
请求响应标准化
  • common
    • IdRequest
    • PageRequest
  • controller
    • SampleController
  • model
    • domain
      • Sample
    • dto
      • sample
        • SampleAddRequest
        • SampleQueryRequest
        • SampleUpdateRequest
    • enums
      • SampleStatusEnum
    • vo
      • SampleVO
数据库表标准化
-- 样例表
create table if not exists sample
(
    id           bigint auto_increment comment 'id' primary key,
    sampleTest  varchar(256)                           not null comment '样例文本',

    createTime   datetime     default CURRENT_TIMESTAMP not null comment '创建时间',
    updateTime   datetime     default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
    isDelete     tinyint      default 0                 not null comment '是否删除',
) comment '样例';
增删改查方法实现

在 Controller 中按照以下顺序声明接口方法

  1. 请求判空
  2. 数据获取
  3. 服务调用
    在此处直接使用从 IService 中继承的现有方法
  4. 结果返回
分页插件配置
/**
 * MyBatis Plus 配置
 *
 */
@Configuration
@MapperScan("com.example.statefulbackend.mapper")
public class MyBatisPlusConfig {

    /**
     * 拦截器配置
     *
     * @return
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}
合法性检验方法
@Service
public class SampleServiceImpl extends ServiceImpl<SampleMapper, Sample>
        implements SampleService {

    @Override
    public void validSample(Sample sample, boolean isAdd) {
        //数据判空
        if (sample == null) {
            throw new BusinessException(ErrorCode.PARAMS_NULL);
        }
        //数据获取
        String sampleTest = sample.getSampleTest();
        Integer sampleStatus = sample.getSampleStatus();
        //新增时验证
        if (isAdd) {
            if (StringUtils.isAnyBlank(sampleTest)|| ObjectUtils.anyNull(sampleStatus)) {
                throw new BusinessException(ErrorCode.PARAMS_NULL);
            }
        }
        //属性合法性验证
        if (StringUtils.isNotBlank(sampleTest) && sampleTest.length() > 8192) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "内容过长");
        }
        if (sampleStatus != null && !SampleStatusEnum.getValues().contains(sampleStatus)) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "状态不符合要求");
        }
    }

}
机制使用
机制载入

复制以下文件

  • common

    • IdRequest
    • PageRequest
  • controller

    • SampleController
  • model

    • domain
      • Sample
    • dto
      • sample
        • SampleAddRequest
        • SampleQueryRequest
        • SampleUpdateRequest
    • enums
      • SampleStatusEnum
    • vo
      • SampleVO
机制使用
  1. 基于标准化数据库表进行客制化改造
  2. 使用 MybatisX-Generator 生成以下内容
    • <Entity>.java
    • <EntityService>.java
    • <EntityServiceImpl>.java
    • <EntityMapper>.java
    • <EntityMapper>.xml
  3. 客制化改造
    • 生成的内容
    • enums、vo
  4. 将 SampleController 和 dto 文件夹下所有文件 中所有 <Sample> 相关内容重命名为 <Entity>

解决方案介绍

后端跨域问题解决方案

解决方案原理

在请求头中加入 Access-Control-Allow-Origin 相关配置信息

解决方案实现
拦截器配置

重写 WebMvcConfigurer 的方式以解决跨域问题

@Configuration
public class WebMvcConfg implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        //设置允许跨域的路径
        registry.addMapping("/**")
                //设置允许跨域请求的域名
                //当**Credentials为true时,**Origin不能为星号,需为具体的ip地址【如果接口不带cookie,ip无需设成具体ip】
                .allowedOrigins("*")
                //是否允许证书 不再默认开启
                .allowCredentials(false)
                //设置允许的方法
                .allowedMethods("*")
                //跨域允许时间
                .maxAge(3600);
    }
}
解决方案使用
  1. 复制上述拦截器配置到项目中
  2. 根据实际需求配置 addCorsMappings(CorsRegistry registry) 方法中的代码

接口文档管理解决方案

解决方案原理

knife4j是一款基于Swagger的自动化接口文档管理工具。它通过解析项目中的Swagger注解,自动生成接口文档,并提供了一系列功能来增强接口文档的可读性、可维护性和可测试性。

knife4j的主要原理是通过解析项目中的Swagger注解,生成接口文档的HTML页面。在项目启动时,knife4j会扫描项目中的Swagger注解,并根据注解的配置信息,自动生成接口文档的相关内容,如API接口列表、接口参数、接口响应等。生成的接口文档可以通过浏览器访问,以便团队成员和其他开发者查看和测试接口。

解决方案实现
拦截器配置
/**
 * Knife4j 接口文档配置
 *
 */
@Configuration
@EnableSwagger2
@Profile("dev")
public class Knife4jConfig {

    @Bean
    public Docket defaultApi2() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(new ApiInfoBuilder()
                        .title("project-backend")
                        .description("project-backend")
                        .version("1.0")
                        .build())
                .select()
                //TODO 指定 Controller 扫描包路径
                .apis(RequestHandlerSelectors.basePackage("com.example.statefulbackend.controller"))
                .paths(PathSelectors.any())
                .build();
    }
}
接口注解配置(可选)
//Knife 配置样例
//启动后,打开 http://localhost:8080/doc.html 查看文档
@Api(tags = "首页模块")
@RestController
public class IndexController {

    @ApiImplicitParam(name = "name",value = "姓名",required = true)
    @ApiOperation(value = "向客人问好")
    @GetMapping("/sayHi")
    public ResponseEntity<String> sayHi(@RequestParam(value = "name")String name){
        return ResponseEntity.ok("Hi:"+name);
    }
}
解决方案使用
  1. 依赖导入

    <dependency>
        <groupId>com.github.xiaoymin</groupId>
        <artifactId>knife4j-spring-boot-starter</artifactId>
        <version>3.0.3</version>
    </dependency>
    
  2. 复制上述拦截器配置到项目中

  3. 根据实际需求配置 defaultApi2() 方法中的代码

项目源码

https://github.com/Ba11ooner/stateful-backend

原文地址:https://www.cnblogs.com/ba11ooner/p/17658535.html