Bootstrap

中州养老项目总结

项目整体搭建

整体介绍

基础功能

中州养老系统为养老院量身定制开发专业的养老管理软件产品;涵盖来访管理、入退管理、在住管理、服务管理、财务管理等功能模块,涉及从来访参观到退住办理的完整流程。

中州养老项目分为两端,一个是管理后台,另外一个是家属端

  • 管理后台:养老院员工使用,入住、退住,给老人服务记录等
  • 家属端:养老院的老人家属使用,查看老人信息,缴费,下订单等

技术架构

  • 前端主要使用的Vue3+TS
  • 后端主要使用的是Springboot作为基础架构,当然后端也集成了很多其他的技术,比如有Mybatis、Swagger、Spring cache、Spring Security、Xxl-job、Activiti7
  • 数据存储主要使用到了MySQL和Redis
  • 使用了nginx来作为反向代理和前端的静态服务器
  • 其他技术:阿里云物联网平台IOT、对象存储OSS、微信登录、AI工具辅助开发等

DTO 、VO

接收对象和返回对象分别使用了DTO和VO

  • DTO:Data Transfer Object 数据传输对象:xxxDto或者xxxDTO,xxx为业务领域相关的名称, 用于接口的入参
  • VO:Value Object展示对象:xxxVO或者xxxVo,xxx一般为网页名称, 用于接口的出参

接口四要素

搞明白需求之后,我们下面就可以来设计接口了,一个接口包含了四个基本要素,分别是:请求路径、请求方式、接口入参、接口出参

请求路径 命名:以模块名称进行区分(英文)

请求方式(需要符合restFul风格)

  • 查询 GET
  • 新增 POST
  • 修改 PUT
  • 删除 DELETE

接口入参

  • 路径参数
    • 问号传参---->后端形参接收
    • path传参---->后端PathVariable注解接收
  • 请求体参数
    • 前端:json对象
    • 后端:对象接收,DTO

接口出参

  • 统一格式 {code:200,msg:"成功",data:{}}
  • 数据封装,一般为VO
    • 敏感数据过滤
    • 整合数据

床位接口增删改查

新增床位为例

BedController层

@PostMapping("/create")
public ResponseResult createBed(@RequestBody BedDto bedDto){
    bedService.addBed(bedDto);
    return ResponseResult.success();
}

BedService层

 @Override
public void addBed(BedDto bedDto) {
    Bed bed = new Bed();
    //使用了BeanUtils中的copyProerties方法 
    //也可以使用Bed bed = BeanUtil.toBean(bedDto, Bed.class);
    BeanUtils.copyProperties(bedDto, bed);  
    bed.setCreateTime(LocalDateTime.now());
    bed.setCreateBy(1L);
    bed.setBedStatus(0);
    try {
        bedMapper.addBed(bed);
    }catch (Exception e){
        throw new BaseException(BasicEnum.BED_INSERT_FAIL);
    }
}

BedMapper层

<insert id="addBed" parameterType="com.zzyl.entity.Bed">
    insert into bed(bed_number, sort, bed_status, room_id, create_by, remark, create_time)
    values (#{bedNumber}, #{sort}, #{bedStatus}, #{roomId}, #{createBy}, #{remark}, #{createTime})
</insert>

Swagger

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

Spring已经将Swagger纳入自身的标准,建立了Spring-swagger项目,现在叫Springfox。通过在项目中引入Springfox ,即可非常简单快捷的使用Swagger。

knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案,前身是swagger-bootstrap-ui,取名knife4j是希望它能像一把匕首一样小巧,轻量,并且功能强悍!

目前,一般都使用knife4j框架。

项目中集成

导入 knife4j 的maven坐标(注意:由于knife4j是基于swagger的,所以也会自动导入swagger的依赖)

<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>

配置类

在配置类中加入 knife4j 相关配置,可以使knife4j在全局生效,目的就是项目中的所有接口都生成在线接口文档

@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: ant_path_matcher
zzyl:
  framework:
    swagger:
      swagger-path: com.zzyl.controller
      title: 项目名称
      description: 具体描述
      contact-name: 组织名称
      contact-url: 联系网址
      contact-email: 联系邮箱

静态资源映射

如果想要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示例 , 改造完可以直接访问在线接口文档

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

全局异常处理

全局异常处理逻辑

一般项目开发有两种异常:

  • 预期异常(程序员手动抛出)
  • 运行时异常

BaseException

基础异常,如果业务中需要手动抛出异常,则需要抛出该异常

@Getter
@Setter
public class BaseException extends RuntimeException {

    private BasicEnum basicEnum;

    public BaseException(BasicEnum basicEnum) {
        this.basicEnum = basicEnum;
    }
}

其中BaseException中的参数为一个枚举,可以在BasicEnum自定义业务中涉及到的异常

@Getter
@AllArgsConstructor
public enum BasicEnum implements IBasicEnum {

    SUCCEED(200, "操作成功"),
    SECURITY_ACCESSDENIED_FAIL(401, "权限不足!"),
    LOGIN_FAIL(401, "用户登录失败"),
    LOGIN_LOSE_EFFICACY(401, "登录状态失效,请重新登录"),
    SYSYTEM_FAIL(500, "系统运行异常"),
    
    //编码
    public final int code;
    //信息
    public final String msg;
}

GlobalExceptionHandler

全局异常处理器

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

}

新模块全栈开发

MySQL常见数据类型

MySQL中的数据类型有很多,主要分为三类:数值类型、字符串类型、日期时间类型。

数值类型

类型大小有符号(SIGNED)范围无符号(UNSIGNED)范围描述
TINYINT1byte(-128,127)(0,255)小整数值
SMALLINT2bytes(-32768,32767)(0,65535)大整数值
MEDIUMINT3bytes(-8388608,8388607)(0,16777215)大整数值
INT/INTEGER4bytes(-2147483648,2147483647)(0,4294967295)大整数值
BIGINT8bytes(-263,263-1)(0,2^64-1)极大整数值
FLOAT4bytes(-3.402823466 E+38,3.402823466351 E+38)0 和 (1.175494351 E-38,3.402823466 E+38)单精度浮点数值
DOUBLE8bytes(-1.7976931348623157 E+308,1.7976931348623157 E+308)0 和 (2.2250738585072014 E-308,1.7976931348623157 E+308)双精度浮点数值
DECIMAL依赖于M(精度)和D(标度)的值依赖于M(精度)和D(标度)的值小数值(精确定点数)

字符串类型

类型大小描述
CHAR0-255 bytes定长字符串(需要指定长度)
VARCHAR0-65535 bytes变长字符串(需要指定长度)
TINYBLOB0-255 bytes不超过255个字符的二进制数据
TINYTEXT0-255 bytes短文本字符串
BLOB0-65 535 bytes二进制形式的长文本数据
TEXT0-65 535 bytes长文本数据
MEDIUMBLOB0-16 777 215 bytes二进制形式的中等长度文本数据
MEDIUMTEXT0-16 777 215 bytes中等长度文本数据
LONGBLOB0-4 294 967 295 bytes二进制形式的极大文本数据
LONGTEXT0-4 294 967 295 bytes极大文本数据

char是定长字符串, 指定长度多长就会占用多少个字符, 和字段值的长度无关

varchar是变长字符串, 指定的长度为最大占用长度 , 相对来说, char的性能会更高些

日期时间类型

类型大小范围格式描述
DATE31000-01-01 至 9999-12-31YYYY-MM-DD日期值
TIME3-838:59:59 至 838:59:59HH:MM:SS时间值或持续时间
YEAR11901 至 2155YYYY年份值
DATETIME81000-01-01 00:00:00 至 9999-12-31 23:59:59YYYY-MM-DD HH:MM:SS混合日期和时间值
TIMESTAMP41970-01-01 00:00:01 至 2038-01-19 03:14:07YYYY-MM-DD HH:MM:SS混合日期和时间值,时间戳

Mybatis字段自动填充拦截器

在执行新增操作的时候, 我们并没有单独设置创建人和创建时间和修改时间的字段, 但是在表中存储的数据中, 这三个字段是有值的, 这是因为提供了Mybatis字段自动填充拦截器

首先在 intercept 包下创建了 AutoFillInterceptor 类 , 用来拦截mapper层对数据的updateinsert操作

@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class AutoFillInterceptor implements Interceptor {

    private static final String CREATE_BY = "createBy";
    private static final String UPDATE_BY = "updateBy";

    private static final String CREATE_TIME = "createTime";
    private static final String UPDATE_TIME = "updateTime";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        // 获取用于描述SQL语句的映射信息
        MappedStatement ms = (MappedStatement) args[0];
        SqlCommandType sqlCommandType = ms.getSqlCommandType();

        // 获取sql参数实体 ParamMap
        Object parameter = args[1];

        if (parameter != null && sqlCommandType != null) {
            // 获取用户ID
            Long userId = loadUserId();

            if (SqlCommandType.INSERT.equals(sqlCommandType)) {
                // 插入操作
                if (parameter instanceof MapperMethod.ParamMap) {
                    // 批量插入的情况
                    MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap) parameter;
                    ArrayList list = (ArrayList) paramMap.get("list");
                    list.forEach(v -> {
                        // 设置创建人和创建时间字段值
                        setFieldValByName(CREATE_BY, userId, v);
                        setFieldValByName(CREATE_TIME, LocalDateTime.now(), v);
                        setFieldValByName(UPDATE_TIME, LocalDateTime.now(), v);
                    });
                    paramMap.put("list", list);
                } else {
                    // 单条插入的情况
                    // 设置创建人和创建时间字段值
                    setFieldValByName(CREATE_BY, userId, parameter);
                    setFieldValByName(CREATE_TIME, LocalDateTime.now(), parameter);
                    setFieldValByName(UPDATE_TIME, LocalDateTime.now(), parameter);
                }
            } else if (SqlCommandType.UPDATE.equals(sqlCommandType)) {
                // 更新操作
                // 设置更新人和更新时间字段值
                setFieldValByName(UPDATE_BY, userId, parameter);
                setFieldValByName(UPDATE_TIME, LocalDateTime.now(), parameter);
            }
        }

        // 继续执行原始方法
        return invocation.proceed();
    }

    /**
     * 通过反射设置实体的字段值
     * @param fieldName 字段名
     * @param fieldVal  字段值
     * @param parameter 实体对象
     */
    private void setFieldValByName(String fieldName, Object fieldVal, Object parameter) {
        MetaObject metaObject = SystemMetaObject.forObject(parameter);
        if (fieldName.equals(CREATE_BY)) {
            Object value = metaObject.getValue(fieldName);
            if (ObjectUtil.isNotEmpty(value)) {
                return;
            }
        }

        if (metaObject.hasSetter(fieldName)) {
            metaObject.setValue(fieldName, fieldVal);
        }
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            // 对目标对象进行包装,返回代理对象
            return Plugin.wrap(target, this);
        }
        // 非 Executor 类型的对象,直接返回原始对象
        return target;
    }

    @Override
    public void setProperties(Properties properties) {
        // 读取配置文件中的属性,此处没有使用
    }

    /**
     * 获取当前用户的ID,用于填充创建人和更新人字段的值
     *
     * @return 当前用户ID
     */
    public static Long loadUserId() {
        // 从 ThreadLocal 中获取用户ID
        Long userId = UserThreadLocal.getUserId();
        // 如果 ThreadLocal 中不存在用户ID,则从管理用户ID中获取
        if (ObjectUtil.isNotEmpty(userId)) {
            return userId;
        }
        userId = UserThreadLocal.getMgtUserId();
        // 如果管理用户ID也不存在,则默认返回ID为1的用户
        if (!EmptyUtil.isNullOrEmpty(userId)) {
            return userId;
        }
        return 1L;
    }
}

然后再 config 包下创建了 MybatisConfig 类 , 使 AutoFillInterceptor拦截器生效

/**
 *  webMvc高级配置
 */
@Configuration
public class MybatisConfig {
    /***
     *  自动填充拦截器
     */
    @Bean
    public AutoFillInterceptor autoFillInterceptor(){
        return new AutoFillInterceptor();
    }
}

在数据库的建表语句中 , 也可以加入对创建时间和更新时间的设置 ( 只是举例方法 )

create table nursing_project
(
    id                  bigint auto_increment comment '编号'  primary key,
    create_time         datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    update_time         datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间',
)

当然, 对这些字段的拦截操作使用AOP也可以实现, 这里不再举例

TDesign

TDesign 具有统一的价值观,  一致的设计语言和视觉风格, 帮助用户形成连续、统一的体验认知。在此基础上,TDesign 提供了开箱即用的 UI 组件库、设计指南和相关设计资产,以优雅高效的方式将设计和研发从重复劳动中解放出来,同时方便大家在 TDesign 的基础上扩展,更好的的贴近业务需求。

官网地址:https://tdesign.tencent.com/

组件使用可以参考官方文档, 这里不做解释 ( 解释也不会, 前端太菜 : )

微信登录接口对接

微信接口对接

三方接口对接

在项目中对接三方接口, 最重要的参考永远是官方提供的接口文档, 主要看接口的四要素(请求路径、请求方式、入参、出参)

微信登录

官方接口文档 :

https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html

如果从后端调用三方接口,一样也是用的http请求,只是这次请求是从后端到另外一个后端服务,我们需要借助工具才能发起请求

如 HttpClient , OkHttp , RestTemplate , 糊涂工具包(https://doc.hutool.cn/pages/http/),在该项目中使用糊涂工具包

微信登录过程 :

  • 前端在小程序中集成微信相关依赖,当用户请求登录的同时,调用wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
  • 后端服务器调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台账号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台账号) 和 会话密钥 session_key
  • 开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。

封装接口调用

新增WechatService

public interface WechatService {

    /**
     * 获取openid
     * @param code  登录凭证
     * @return
     * @throws IOException
     */
    public String getOpenid(String code) ;

    /**
     * 获取手机号
     * @param code  手机号凭证
     * @return
     * @throws IOException
     */
    public String getPhone(String code);

}

WechatService实现类

@Service
public class WechatServiceImpl implements WechatService {

    // 登录
    private static final String REQUEST_URL = "https://api.weixin.qq.com/sns/jscode2session?grant_type=authorization_code";
    // 获取token
    private static final String TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential";
    // 获取手机号
    private static final String PHONE_REQUEST_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=";
	//获取配置文件中的appId
    @Value("${zzyl.wechat.appId}")
    private String appId;
	//获取配置文件中的secret
    @Value("${zzyl.wechat.appSecret}")
    private String secret;

    /**
     * 获取openid
     *
     * @param code 登录凭证
     * @return
     * @throws IOException
     */
    @Override
    public String getOpenid(String code) throws IOException {

        //封装参数
        Map<String,Object> requestUrlParam = getAppConfig();
        requestUrlParam.put("js_code",code);

        String result = HttpUtil.get(REQUEST_URL, requestUrlParam);

        JSONObject jsonObject = JSONUtil.parseObj(result);
        // 若code不正确,则获取不到openid,响应失败
        if (ObjectUtil.isNotEmpty(jsonObject.getInt("errcode"))) {
            throw new RuntimeException(jsonObject.getStr("errmsg"));
        }

        return jsonObject.getStr("openid");
    }

    /**
     * 封装公共参数
     * @return
     */
    private Map<String, Object> getAppConfig() {
        Map<String, Object> requestUrlParam = new HashMap<>();
        requestUrlParam.put("appid",appId);
        requestUrlParam.put("secret",secret);
        return requestUrlParam;
    }

    /**
     * 获取手机号
     *
     * @param code 手机号凭证
     * @return
     * @throws IOException
     */
    @Override
    public String getPhone(String code) throws IOException {

        //获取access_token
        String token = getToken();
        //拼接请求路径
        String url = PHONE_REQUEST_URL + token;

        Map<String,Object> param = new HashMap<>();
        param.put("code",code);

        String result = HttpUtil.post(url, JSONUtil.toJsonStr(param));
        JSONObject jsonObject = JSONUtil.parseObj(result);
        if (jsonObject.getInt("errcode") != 0) {
            //若code不正确,则获取不到phone,响应失败
            throw new RuntimeException(jsonObject.getStr("errmsg"));
        }
        return jsonObject.getJSONObject("phone_info").getStr("purePhoneNumber");

    }

    public String getToken(){
        Map<String, Object> requestUrlParam = getAppConfig();

        String result = HttpUtil.get(TOKEN_URL, requestUrlParam);
        //解析
        JSONObject jsonObject = JSONUtil.parseObj(result);
        //如果code不正确,则失败
        if(ObjectUtil.isNotEmpty(jsonObject.getInt("errcode"))){
            throw new RuntimeException(jsonObject.getStr("errmsg"));
        }
        return jsonObject.getStr("access_token");
    }
}

其中jwt相关的配置,我们已经在application.yml文件中定义,主要有两个属性,一个是签名,一个是过期时间

zzyl:
  framework:
    jwt:
      base64-encoded-secret-key: 项目的签名密码
      ttl: 3600000
  wechat:
    appId: 自己账号的appid
    appSecret: 自己账号的secret

读取配置文件的配置类

/**
 * jw配置文件
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "zzyl.framework.jwt")
public class JwtTokenManagerProperties implements Serializable {
	//签名密码
    private String base64EncodedSecretKey;
    //有效时间
    private Integer ttl;
}

微信登录业务开发

接收参数类型UserLoginRequestDto

@Data
public class UserLoginRequestDto {

    @ApiModelProperty("昵称")
    private String nickName;

    @ApiModelProperty("登录临时凭证")
    private String code;

    @ApiModelProperty("手机号临时凭证")
    private String phoneCode;
}

返回类型:LoginVo

@Data
@ApiModel(value = "登录对象")
public class LoginVo {

    @ApiModelProperty(value = "JWT token")
    private String token;

    @ApiModelProperty(value = "昵称")
    private String nickName;
}

Controller层

@RestController
@RequestMapping("/customer/user")
public class CustomerUserController {

    @Autowired
    private MemberService memberService;

    @PostMapping("/login")
    @ApiOperation("登录")
    public ResponseResult<LoginVo> login(@RequestBody UserLoginRequestDto userLoginRequestDto) {
        LoginVo loginVo = memberService.login(userLoginRequestDto);
        return ResponseResult.success(loginVo);
    }

}

Mapper层 , 具体sql语句不做展示

@Mapper
public interface MemberMapper {

    Member getByOpenId(String openId);

    void save(Member member);

    void update(Member member);
}

Service层

public interface MemberService {
    LoginVo login(UserLoginRequestDto userLoginRequestDto);
}

Service实现类

@Service
public class MemberServiceImpl implements MemberService {

    @Autowired
    private MemberMapper memberMapper;
	//微信封装类注入
    @Autowired
    private WechatService wechatService;
	//Jwt工具类
    @Autowired
    private JwtTokenManagerProperties jwtTokenManagerProperties;
	//昵称集合
    static ArrayList DEFAULT_NICKNAME_PREFIX = Lists.newArrayList(
            "生活更美好","大桔大利","日富一日","好柿开花","柿柿如意","一椰暴富","大柚所为","杨梅吐气","天生荔枝"
    );

    @Override
    public LoginVo login(UserLoginRequestDto userLoginRequestDto) {

        // 1.获取小程序传递的code,发起远程调用 获取openId
        String openId = wechatService.getOpenId(userLoginRequestDto.getCode());
        // 2.根据openId 查询 数据库,查询用户
        Member member = memberMapper.getByOpenId(openId);
        // 3.用户为空,构建一个用户对象,赋值openId
        if(ObjectUtil.isEmpty(member)){
            member = Member.builder().openId(openId).build();
        }
        // 4.根据detail.code发起请求获取真实的手机号
        String phone = wechatService.getPhone(userLoginRequestDto.getPhoneCode());
        // 5.根据不同情况 新增/更新
        saveOrUpdate(member,phone);
        // 6.生成token 返回(包含用户ID 和昵称)
        HashMap<String, Object> claims = new HashMap<>();
        claims.put(Constants.JWT_USERID,member.getId());
        claims.put(Constants.JWT_USERNAME, member.getName());
        String token = JwtUtil.createJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(), jwtTokenManagerProperties.getTtl(), claims);
		// 7.封装 LoginVo 对象 返回
        LoginVo loginVo = new LoginVo();
        loginVo.setToken(token);
        loginVo.setNickName(member.getName());
        return loginVo;
    }
	//新增/更新 方法
    private void saveOrUpdate(Member member, String phone) {
		//1.判断取到的手机号与数据库中保存的手机号是否一样
        if(!phone.equals(member.getPhone())){
            member.setPhone(phone);
        }
		//2.判断id存在
        if(!ObjectUtil.isEmpty(member.getId())){
            memberMapper.update(member);
            return;
        }
		//3.保存新的用户
        //随机组装昵称,词组+手机号后四位
        String subPhone = phone.substring(phone.length() - 4);
        Random random = new Random();
        int index = random.nextInt(DEFAULT_NICKNAME_PREFIX.size());
        String nickName = DEFAULT_NICKNAME_PREFIX.get(index) + subPhone;
        member.setName(nickName);
        memberMapper.save(member);
    }
}

ThreadLocal

概述

ThreadLocal是Java中的一个线程局部变量工具类,它提供了一种在多线程环境下,每个线程都可以独立访问自己的变量副本的机制。

ThreadLocal中存储的数据对于每个线程来说都是独立的,互不干扰。

基本用法

// 创建ThreadLocal对象
ThreadLocal<String> threadLocal = new ThreadLocal<>();
// set方法
threadLocal.set("value");
// get方法
String value = threadLocal.get();
// remove方法 清除当前线程中局部变量的值
threadLocal.remove();

注意事项

需要注意ThreadLocal的内存泄漏问题。由于ThreadLocal的生命周期与线程的生命周期相同,如果没有手动清除线程局部变量的值,可能会导致内存泄漏。
避免过多使用ThreadLocal,过多的使用会导致代码的可读性变差,且容易引发线程安全问题。

定时任务

概述

任务调度是指系统为了自动完成特定任务,在约定的特定时刻去执行任务的过程。有了任务调度即可解放更多的人力,而是由系统自动去执行任务。

实现任务调度的方式

  1. 多线程方式,结合sleep
  2. JDK提供的API,例如:Timer、ScheduledExecutor
  3. 框架,例如Quartz ,它是一个功能强大的任务调度框架,可以满足更多更复杂的调度需求
  4. spring task

在这里采用的是Spring task

cron表达式

cron表达式是一个字符串, 用来设置定时规则, 由七部分组成, 每部分中间用空格隔开, 每部分的含义如下表所示:

组成部分含义取值范围
第一部分Seconds (秒)0-59
第二部分Minutes(分)0-59
第三部分Hours(时)0-23
第四部分Day-of-Month(天)1月31日
第五部分Month(月)0-11或JAN-DEC
第六部分Day-of-Week(星期)1-7(1表示星期日)或SUN-SAT
第七部分Year(年) 可选1970-2099

另外, cron表达式还可以包含一些特殊符号来设置更加灵活的定时规则, 如下表所示:

符号含义
?表示不确定的值。当两个子表达式其中一个被指定了值以后,为了避免冲突,需要将另外一个的值设为“?”。例如:想在每月20日触发调度,不管20号是星期几,只能用如下写法:0 0 0 20 * ?,其中最后以为只能用“?”
*代表所有可能的值
,设置多个值,例如”26,29,33”表示在26分,29分和33分各自运行一次任务
-设置取值范围,例如”5-20”,表示从5分到20分钟每分钟运行一次任务
/设置频率或间隔,如"1/15"表示从1分开始,每隔15分钟运行一次任务
L用于每月,或每周,表示每月的最后一天,或每个月的最后星期几,例如"6L"表示"每月的最后一个星期五"
W表示离给定日期最近的工作日,例如"15W"放在每月(day-of-month)上表示"离本月15日最近的工作日"
#表示该月第几个周X。例如”6#3”表示该月第3个周五

这里给出基本示例

cron表达式含义
*/5 * * * * ?每隔5秒运行一次任务
0 0 23 * * ?每天23点运行一次任务
0 0 1 1 * ?每月1号凌晨1点运行一次任务
0 0 23 L * ?每月最后一天23点运行一次任务
0 26,29,33 * * * ?在26分、29分、33分运行一次任务
0 0/30 9-17 * * ?朝九晚五工作时间内每半小时运行一次任务
0 15 10 ? * 6#3每月的第三个星期五上午10:15运行一次任务

注意事项

一般使用的时候 , 年的位置省略 , 星期位置使用 ? 标识

项目使用

  1. 导入maven坐标 spring-context

​ 目前项目中只要导入了springboot相关依赖会自动导入,这一步无需操作

  1. 自定义定时任务类

一般在Task包下创建, 两个注解@Component交给IOC容器和@Scheduled定义cron表达式

@Component
public class MyTask {

    /**
     * 定时任务 每隔5秒触发一次
     */
    @Scheduled(cron = "0/5 * * * * ?")
    public void executeTask(){
        log.info("定时任务开始执行:{}", LocalDateTime.now());
    }
}

启动类添加注解 @EnableScheduling

@SpringBootApplication
@EnableScheduling
public class ZzylApplication {
   public static void main(String[] args) {
      SpringApplication.run(ZzylApplication.class, args);
   }
}

通用权限系统

RBAC模型

在企业系统中,通过配置用户的功能权限可以解决不同的人分管不同业务的需求,基于RBAC模型,RBAC(Role Based Access Control)模型,它的中文是基于角色的访问控制,主要是将功能组合成角色,再将角色分配给用户,也就是说角色是功能的合集

BCrypt密码加密

MD5加密: 长度是32 同一个字符串加密多次都是相同

BCrypt加密 : 长度是64 同一个字符串加密多次都是不同

使用示例:

public class PasswordTest {

    public static void main(String[] args) {
      	// 对密码进行加密
        String password1 = BCrypt.hashpw("123456", BCrypt.gensalt());
        // 验证密码是否正确
        boolean checkpw = BCrypt.checkpw("123456", "$2a$10$rkB/70Cz5UvsE7F5zsBh8O2EYDoGus3/AnVrEgP5cTpsGLxM8iyG6");
    }
}

Redis基础

Redis是一个基于内存的key-value结构数据库。Redis 是互联网技术领域使用最为广泛的存储中间件

这里主要讲述在项目中的使用 , 并直接使用 Spring Cache 这样只需要简单地加一个注解,就能实现缓存功能

Spring Boot提供了对应的Starter,maven坐标:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Spring Data Redis中提供了一个高度封装的类:RedisTemplate,对相关api进行了归类封装,将同一类型操作封装为operation接口,具体分类如下:

方法类型
ValueOperationsstring数据操作
SetOperationsset类型数据操作
ZSetOperationszset类型数据操作
HashOperationshash类型的数据操作
ListOperationslist类型的数据操作

在application.yml文件,配置redis连接,如下:

spring:
  redis:
    host: localhost
    port: 端口号
    password: redis密码

常用注解 :

注解说明
@EnableCaching开启缓存注解功能,通常加在启动类上
@Cacheable在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中
@CachePut将方法的返回值放到缓存中
@CacheEvict将一条或多条数据从缓存中删除
@Caching缓存的结合体,可以组合以上注解在一个方法中使用,比如有新增,有删除

在Redis中,冒号通常用作键的命名约定,可以创建层次结构,类似于文件系统中的路径结构,提升查找效率

@CachePut

作用: 将方法返回值,放入缓存,一般保存的时候使用该注解

@CachePut(value = "userCache", key = "#user.id")//key的生成:userCache::1
public User insert(User user){
    userMapper.insert(user);
    return user;
}

@CacheEvict

作用: 清理指定缓存

@CacheEvict(cacheNames = "userCache",key = "#id")//删除某个key对应的缓存数据
public void deleteById(Long id){
    userMapper.deleteById(id);
}

@CacheEvict(cacheNames = "userCache",allEntries = true)//删除userCache下所有的缓存数据
public void deleteAll(){
    userMapper.deleteAll();
}

@Caching

作用: 组装其他缓存注解

  • cacheable 组装一个或多个@Cacheable注解
  • put 组装一个或多个@CachePut注解
  • evict 组装一个或多个@CacheEvict注解
@Caching(
         cacheable = {@Cacheable(value = "userCache",key = "#id")},
         put = {@CachePut(value = "userCache",key = "#result.name"),
                 @CachePut(value = "userCache",key = "#result.age")})
 public User insert(User user){
     userMapper.insert(user);
     return user;
 }
;