项目整体搭建
整体介绍
基础功能
中州养老系统为养老院量身定制开发专业的养老管理软件产品;涵盖来访管理、入退管理、在住管理、服务管理、财务管理等功能模块,涉及从来访参观到退住办理的完整流程。
中州养老项目分为两端,一个是管理后台,另外一个是家属端
- 管理后台:养老院员工使用,入住、退住,给老人服务记录等
- 家属端:养老院的老人家属使用,查看老人信息,缴费,下订单等
技术架构
- 前端主要使用的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)范围 | 描述 |
---|---|---|---|---|
TINYINT | 1byte | (-128,127) | (0,255) | 小整数值 |
SMALLINT | 2bytes | (-32768,32767) | (0,65535) | 大整数值 |
MEDIUMINT | 3bytes | (-8388608,8388607) | (0,16777215) | 大整数值 |
INT/INTEGER | 4bytes | (-2147483648,2147483647) | (0,4294967295) | 大整数值 |
BIGINT | 8bytes | (-263,263-1) | (0,2^64-1) | 极大整数值 |
FLOAT | 4bytes | (-3.402823466 E+38,3.402823466351 E+38) | 0 和 (1.175494351 E-38,3.402823466 E+38) | 单精度浮点数值 |
DOUBLE | 8bytes | (-1.7976931348623157 E+308,1.7976931348623157 E+308) | 0 和 (2.2250738585072014 E-308,1.7976931348623157 E+308) | 双精度浮点数值 |
DECIMAL | 依赖于M(精度)和D(标度)的值 | 依赖于M(精度)和D(标度)的值 | 小数值(精确定点数) |
字符串类型
类型 | 大小 | 描述 |
---|---|---|
CHAR | 0-255 bytes | 定长字符串(需要指定长度) |
VARCHAR | 0-65535 bytes | 变长字符串(需要指定长度) |
TINYBLOB | 0-255 bytes | 不超过255个字符的二进制数据 |
TINYTEXT | 0-255 bytes | 短文本字符串 |
BLOB | 0-65 535 bytes | 二进制形式的长文本数据 |
TEXT | 0-65 535 bytes | 长文本数据 |
MEDIUMBLOB | 0-16 777 215 bytes | 二进制形式的中等长度文本数据 |
MEDIUMTEXT | 0-16 777 215 bytes | 中等长度文本数据 |
LONGBLOB | 0-4 294 967 295 bytes | 二进制形式的极大文本数据 |
LONGTEXT | 0-4 294 967 295 bytes | 极大文本数据 |
char是定长字符串, 指定长度多长就会占用多少个字符, 和字段值的长度无关
varchar是变长字符串, 指定的长度为最大占用长度 , 相对来说, char的性能会更高些
日期时间类型
类型 | 大小 | 范围 | 格式 | 描述 |
---|---|---|---|---|
DATE | 3 | 1000-01-01 至 9999-12-31 | YYYY-MM-DD | 日期值 |
TIME | 3 | -838:59:59 至 838:59:59 | HH:MM:SS | 时间值或持续时间 |
YEAR | 1 | 1901 至 2155 | YYYY | 年份值 |
DATETIME | 8 | 1000-01-01 00:00:00 至 9999-12-31 23:59:59 | YYYY-MM-DD HH:MM:SS | 混合日期和时间值 |
TIMESTAMP | 4 | 1970-01-01 00:00:01 至 2038-01-19 03:14:07 | YYYY-MM-DD HH:MM:SS | 混合日期和时间值,时间戳 |
Mybatis字段自动填充拦截器
在执行新增操作的时候, 我们并没有单独设置创建人和创建时间和修改时间的字段, 但是在表中存储的数据中, 这三个字段是有值的, 这是因为提供了Mybatis字段自动填充拦截器
首先在 intercept 包下创建了 AutoFillInterceptor 类 , 用来拦截mapper层对数据的update和insert操作
@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,过多的使用会导致代码的可读性变差,且容易引发线程安全问题。
定时任务
概述
任务调度是指系统为了自动完成特定任务,在约定的特定时刻去执行任务的过程。有了任务调度即可解放更多的人力,而是由系统自动去执行任务。
实现任务调度的方式
- 多线程方式,结合sleep
- JDK提供的API,例如:Timer、ScheduledExecutor
- 框架,例如Quartz ,它是一个功能强大的任务调度框架,可以满足更多更复杂的调度需求
- 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运行一次任务 |
注意事项
一般使用的时候 , 年的位置省略 , 星期位置使用 ? 标识
项目使用
- 导入maven坐标 spring-context
目前项目中只要导入了springboot相关依赖会自动导入,这一步无需操作
- 自定义定时任务类
一般在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接口,具体分类如下:
方法 | 类型 |
---|---|
ValueOperations | string数据操作 |
SetOperations | set类型数据操作 |
ZSetOperations | zset类型数据操作 |
HashOperations | hash类型的数据操作 |
ListOperations | list类型的数据操作 |
在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;
}