密码加密
平常密码都是明文保存,安全性太低,我们采用md5对其进行加密之后保存
1
| password = DigestUtils.md5DigestAsHex("要保存的密码".getBytes());
|
同时进行比对时也是采用同样的方法进行加密比对
Swagger
介绍
Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务(https://swagger.io/)。 它的主要作用是:
使得前后端分离开发更加方便,有利于团队协作
接口的文档在线自动生成,降低后端开发人员编写接口文档的负担
功能测试
Spring已经将Swagger纳入自身的标准,建立了Spring-swagger项目,现在叫Springfox。通过在项目中引入Springfox ,即可非常简单快捷的使用Swagger。
knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案,前身是swagger-bootstrap-ui,取名kni4j是希望它能像一把匕首一样小巧,轻量,并且功能强悍!
目前,一般都使用knife4j框架。
使用步骤
导入 knife4j 的maven坐标
在pom.xml中添加依赖
1 2 3 4 5
| <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-openapi2-spring-boot-starter</artifactId> <version>4.1.0</version> </dependency>
|
在配置文件中加入 knife4j 相关配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| knife4j: enable: true openapi: title: 用户管理接口文档 description: "用户管理接口文档" email: email concat: jiahao url: jiahao.love version: v1.0.0 group: default: group-name: default api-rule: package api-rule-resources: - com.example.controller
|
访问测试
接口文档访问路径为 http://ip:port/doc.html —> http://localhost:8080/doc.html
常用注解
注解 |
说明 |
@Api |
用在类上,例如Controller,表示对类的说明 |
@ApiModel |
用在类上,例如entity、DTO、VO |
@ApiModelProperty |
用在属性上,描述属性信息 |
@ApiOperation |
用在方法上,例如Controller的方法,说明方法的用途、作用 |
@RequestPart |
用在传入参数上,可以上传文件,例如:@RequestPart MultipartFile file |
|
|
ThreadLocal
我们该怎么通过某种方式来动态的获取当前登录员工的员工id?
思路:每次访问请求都会携带一个token,而我们的员工id又存储在了token中,所以我们可以通过解析token来获得员工id,但是我们要怎么传递给Service层呢?
答:通过ThreadLocal进行传递。
介绍:
ThreadLocal 并不是一个Thread,而是Thread的局部变量。
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访
问。
常用方法:
- public void set(T value) 设置当前线程的线程局部变量的值
- public T get() 返回当前线程所对应的线程局部变量的值
- public void remove() 移除当前线程的线程局部变量
有了Thread Local我们就能解决上面的问题,当用户请求新增员工时,首先会被拦截器拦截下来,解析token,这个时候可以将
id存入线程,然后解析token通过执行对应的业务方法,Service层就可以获得该id
为了解决这个问题,我们定义一个封装了ThreadLocal操作的工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class BaseContext {
public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id) { threadLocal.set(id); }
public static Long getCurrentId() { return threadLocal.get(); }
public static void removeCurrentId() { threadLocal.remove(); } }
|
在解析令牌时,解析出用户id,存入线程中
1
| BaseContext.setCurrentId(empId);
|
需要用到用户id时,取出
1 2 3 4 5 6 7
|
employee.setCreateUser(BaseContext.getCurrentId()); employee.setUpdateUser(BaseContext.getCurrentId());
BaseContext.removeCurrentId();
|
统一格式化日期类型
由于我们的日期,转化为json字符串传递给前端时,格式会发生改变,所以我们需要统一格式
其实通过每个日期加上注解格式化也可以,但是对于项目而言,这种方法效率太低了
1 2
| @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime updateTime
|
所以我们可以通过SpringMVC的消息转换器来做到统一格式化日期
首先我们设置对象映射器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
public class JacksonObjectMapper extends ObjectMapper {
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm"; public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
public JacksonObjectMapper() { super(); this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule simpleModule = new SimpleModule() .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))) .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))) .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))) .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))) .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))) .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
this.registerModule(simpleModule); } }
|
其次我们在WebMvcConfiguration中扩展SpringMVC的消息转换器,统一对日期类型进行格式处理
1 2 3 4 5 6 7 8 9 10 11 12 13
|
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) { log.info("扩展消息转换器..."); MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); converter.setObjectMapper(new JacksonObjectMapper()); converters.add(0,converter); }
|
这样就完成了日期的统一格式化处理
公共字段自动填充
对于一个项目我们每次新增数据和修改数据的时候都会添加或者修改创建时间,创建人id,最后一次修改时间,修改人id,,而这些字段就属于公共字段,如果我们每次都为其添加,效率太低了,并且代码冗余,下面我们就根据Spring的AOP切面编程结合注解,来为新增和修改功能进行增强。
序号 |
字段名 |
含义 |
数据类型 |
1 |
create_time |
创建时间 |
datetime |
2 |
create_user |
创建人id |
bigint |
3 |
update_time |
修改时间 |
datetime |
4 |
update_user |
修改人id |
bigint |
而针对于这些字段,我们的赋值方式为:
1). 在新增数据时, 将createTime、updateTime 设置为当前时间, createUser、updateUser设置为当前登录用户ID。
2). 在更新数据时, 将updateTime 设置为当前时间, updateUser设置为当前登录用户ID。
实现步骤:
1). 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法(注解中需要传入value,值为枚举类)
2). 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值
3). 在 Mapper 的方法上加入 AutoFill 注解
代码开发:
首先定义枚举类,来标记mapper方法的类型(Insert or Update)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
public enum OperationType {
UPDATE,
INSERT
}
|
其次自定义注解AutoFill
1 2 3 4 5 6 7 8 9
|
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AutoFill { OperationType value(); }
|
最后自定义切面类AutoFillAspect
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
|
@Aspect @Slf4j @Component public class AutoFillAspect { @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)") public void autoFillPointCut(){}
@Before("autoFillPointCut()") public void autoFill(JoinPoint joinPoint){ log.debug("开始填充公共字段");
MethodSignature signature = (MethodSignature) joinPoint.getSignature(); AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class); OperationType operationType = autoFill.value();
Object[] args = joinPoint.getArgs(); if (args == null || args.length == 0){ return; }
Object entity = args[0];
LocalDateTime now = LocalDateTime.now(); Long currentId = BaseContext.getCurrentId();
if (operationType.equals(OperationType.INSERT)){ try { Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class); Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class); Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
setCreateTime.invoke(entity, now); setUpdateTime.invoke(entity, now); setCreateUser.invoke(entity, currentId); setUpdateUser.invoke(entity, currentId);
} catch (Exception e) { throw new RuntimeException(e); } } else if (operationType.equals(OperationType.UPDATE)) { try { Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
setUpdateTime.invoke(entity, now); setUpdateUser.invoke(entity, currentId);
} catch (Exception e) { throw new RuntimeException(e); } } } }
|
最后把注解标记在响应的方法上面,启动测试,即可
1 2 3 4 5 6
|
@AutoFill(OperationType.INSERT) void add(Employee employee);
|
SpringDataRedis
介绍
Spring Data Redis 是 Spring 的一部分,提供了在 Spring 应用中通过简单的配置就可以访问 Redis 服务,对 Redis 底层开发包进行了高度封装。在 Spring 项目中,可以使用Spring Data Redis来简化 Redis 操作。
网址:https://spring.io/projects/spring-data-redis
Spring Boot提供了对应的Starter,maven坐标:
1 2 3 4
| <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类型的数据操作
环境搭建
1). 导入Spring Data Redis的maven坐标
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
|
2). 配置Redis数据源
1 2 3 4 5 6
| spring: redis: host: localhost port: 6379 password: 6379 database: 0
|
3). 编写配置类,创建RedisTemplate对象(非必需)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Configuration @Slf4j public class RedisConfiguration {
@Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){ log.info("开始创建redis模板对象..."); RedisTemplate redisTemplate = new RedisTemplate(); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setKeySerializer(new StringRedisSerializer()); return redisTemplate; } }
|
当前配置类不是必须的,因为 Spring Boot 框架会自动装配 RedisTemplate 对象,但是默认的key序列化器为
JdkSerializationRedisSerializer,导致我们存到Redis中后的数据和原始数据有差别,故设置为
StringRedisSerializer序列化器。
操作常见类型数据
1). 操作字符串类型数据
1 2 3 4 5 6 7 8 9 10 11 12 13
|
@Test public void testString(){ redisTemplate.opsForValue().set("name","小明"); String city = (String) redisTemplate.opsForValue().get("name"); System.out.println(city); redisTemplate.opsForValue().set("code","1234",3, TimeUnit.MINUTES); redisTemplate.opsForValue().setIfAbsent("lock","1"); redisTemplate.opsForValue().setIfAbsent("lock","2"); }
|
2). 操作哈希类型数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
@Test public void testHash(){ HashOperations hashOperations = redisTemplate.opsForHash();
hashOperations.put("100","name","tom"); hashOperations.put("100","age","20");
String name = (String) hashOperations.get("100", "name"); System.out.println(name);
Set keys = hashOperations.keys("100"); System.out.println(keys);
List values = hashOperations.values("100"); System.out.println(values);
hashOperations.delete("100","age"); }
|
3). 操作列表类型数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
@Test public void testList(){ ListOperations listOperations = redisTemplate.opsForList();
listOperations.leftPushAll("mylist","a","b","c"); listOperations.leftPush("mylist","d");
List mylist = listOperations.range("mylist", 0, -1); System.out.println(mylist);
listOperations.rightPop("mylist");
Long size = listOperations.size("mylist"); System.out.println(size); }
|
4). 操作集合类型数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
@Test public void testSet(){ SetOperations setOperations = redisTemplate.opsForSet();
setOperations.add("set1","a","b","c","d"); setOperations.add("set2","a","b","x","y");
Set members = setOperations.members("set1"); System.out.println(members);
Long size = setOperations.size("set1"); System.out.println(size);
Set intersect = setOperations.intersect("set1", "set2"); System.out.println(intersect);
Set union = setOperations.union("set1", "set2"); System.out.println(union);
setOperations.remove("set1","a","b"); }
|
5). 操作有序集合类型数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
@Test public void testZset(){ ZSetOperations zSetOperations = redisTemplate.opsForZSet();
zSetOperations.add("zset1","a",10); zSetOperations.add("zset1","b",12); zSetOperations.add("zset1","c",9);
Set zset1 = zSetOperations.range("zset1", 0, -1); System.out.println(zset1);
zSetOperations.incrementScore("zset1","c",10);
zSetOperations.remove("zset1","a","b"); }
|
6). 通用命令操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
@Test public void testCommon(){ Set keys = redisTemplate.keys("*"); System.out.println(keys);
Boolean name = redisTemplate.hasKey("name"); Boolean set1 = redisTemplate.hasKey("set1");
for (Object key : keys) { DataType type = redisTemplate.type(key); System.out.println(type.name()); }
redisTemplate.delete("mylist"); }
|
HttpClient
介绍
HttpClient 是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。
HttpClient作用:
为什么要在Java程序中发送Http请求?有哪些应用场景呢?
HttpClient应用场景:
当我们在使用扫描支付、查看地图、获取验证码、查看天气等功能时
其实,应用程序本身并未实现这些功能,都是在应用程序里访问提供这些功能的服务,访问这些服务需要发送HTTP请求,并且接收响应数据,可通过HttpClient来实现。
HttpClient的maven坐标:
1 2 3 4 5
| <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.13</version> </dependency>
|
HttpClient的核心API:
- HttpClient:Http客户端对象类型,使用该类型对象可发起Http请求。
- HttpClients:可认为是构建器,可创建HttpClient对象。
- CloseableHttpClient:实现类,实现了HttpClient接口。
- HttpGet:Get方式请求类型。
- HttpPost:Post方式请求类型。
HttpClient发送请求步骤:
- 创建HttpClient对象
- 创建Http请求对象
- 调用HttpClient的execute方法发送请求
入门案例
对HttpClient编程工具包有了一定了解后,那么,我们使用HttpClient在Java程序当中来构造Http的请求,并且把请求发送出去,接下来,就通过入门案例分别发送GET请求和POST请求,具体来学习一下它的使用方法。
GET方式请求
正常来说,首先,应该导入HttpClient相关的坐标,但在项目中,就算不导入,也可以使用相关的API。
因为在项目中已经引入了aliyun-sdk-oss坐标:
1 2 3 4
| <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> </dependency>
|
上述依赖的底层已经包含了HttpClient相关依赖。
故选择导入或者不导入均可。
进入到sky-server模块,编写测试代码,发送GET请求。
实现步骤:
- 创建HttpClient对象
- 创建请求对象
- 发送请求,接受响应结果
- 解析结果
- 关闭资源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| package com.sky.test;
import org.apache.http.HttpEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest public class HttpClientTest {
@Test public void testGET() throws Exception{ CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");
CloseableHttpResponse response = httpClient.execute(httpGet);
int statusCode = response.getStatusLine().getStatusCode(); System.out.println("服务端返回的状态码为:" + statusCode);
HttpEntity entity = response.getEntity(); String body = EntityUtils.toString(entity); System.out.println("服务端返回的数据为:" + body);
response.close(); httpClient.close(); } }
|
在访问http://localhost:8080/user/shop/status请求时,需要提前启动项目。
测试结果:
POST方式请求
在HttpClientTest中添加POST方式请求方法,相比GET请求来说,POST请求若携带参数需要封装请求体对象,并将该对象设置在请求对象中。
实现步骤:
- 创建HttpClient对象
- 创建请求对象
- 发送请求,接收响应结果
- 解析响应结果
- 关闭资源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
@Test public void testPOST() throws Exception{ CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");
JSONObject jsonObject = new JSONObject(); jsonObject.put("username","admin"); jsonObject.put("password","123456");
StringEntity entity = new StringEntity(jsonObject.toString()); entity.setContentEncoding("utf-8"); entity.setContentType("application/json"); httpPost.setEntity(entity);
CloseableHttpResponse response = httpClient.execute(httpPost);
int statusCode = response.getStatusLine().getStatusCode(); System.out.println("响应码为:" + statusCode);
HttpEntity entity1 = response.getEntity(); String body = EntityUtils.toString(entity1); System.out.println("响应数据为:" + body);
response.close(); httpClient.close(); }
|
测试结果:
微信小程序
登录流程
- 小程序端:调用wx.login()获取code,也就是授权码
- 小程序端:通过wx.request()发送请求并携带code,发送到自己编写的服务器
- 开发者服务端:通过自己配置的appid+appsecret+grant_type+传入的code,请求微信接口服务
- 开发者服务端:接收微信接口服务返回的数据,openid是用户的唯一标识
- 开发者服务端:用户如果没有注册,添加到数据库注册,生成token返回给小程序端
- 小程序端:收到token保存到本地
- 小程序端:后续通过wx.request()向后端发起业务请求,携带token
- 开发者服务端:解析token,验证通过返回对应的数据
- 小程序端:访问成功
支付流程
- 用户端:进入小程序并进行下单,首先提交订单
- 小程序端:包装订单信息请求到后台系统(自己开发的系统)
- 后台系统端:将订单信息进行处理写入数据库,并包装结果返回给小程序端
- 小程序端:请求微信支付
- 后台系统端:调用微信官方提供的微信下单接口
- 微信接口端:返回预支付交易标识到后台系统
- 后台系统端:将微信接口端返回的结果进行处理,组合数据返回给小程序端
- 小程序端:接收后台系统返回的支付参数
- 用户端:确认支付
- 小程序端:直接调用微信支付
- 微信接口端:返回支付结果
- 小程序端:显示微信接口端返回的结果
- 微信接口端:推送支付结果到后端系统
- 后台系统端:接收支付结果,更新订单状态
百度地图
由于是外卖项目,我们需要地图服务,这里选择了百度地图,来处理收货地址是否超出配送范围的问题
登录百度地图开放平台:https://lbsyun.baidu.com/
进入控制台,创建应用,获取AK:
相关接口:
获取经纬度坐标的接口,需要获得商家和客户的经纬度
https://lbsyun.baidu.com/index.php?title=webapi/guide/webservice-geocoding
通过商家和客户的经纬度来获取实际距离单位为米
https://lbsyun.baidu.com/index.php?title=webapi/directionlite-v1
下面进入代码的开发:
- 首先我们需要在配置文件中配置外卖店家的店铺地址和百度地图的AK
1 2 3 4
| sky: baidu: address: 北京市海淀区上地十街10号 ak: 你的AK
|
- 创建实体类,读取配置文件的属性
1 2 3 4 5 6 7 8 9
| @Component @Data @ConfigurationProperties(prefix = "sky.baidu") public class BaiduProperties { private String address; private String ak; }
|
- 创建百度地图工具类,提供方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
|
@Component @Slf4j public class BaiduUtils { @Autowired private BaiduProperties baiduProperties;
public void checkOutOfRange(String address){ Map<String, String> map = new HashMap<>(); map.put("address", baiduProperties.getAddress()); map.put("output", "json"); map.put("ak", baiduProperties.getAk());
String shopCoordinate = HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3", map); JSONObject jsonObject = JSON.parseObject(shopCoordinate); if (!jsonObject.getString("status").equals("0")) { throw new OrderBusinessException(MessageConstant.SHOP_NOT_FOUND); }
JSONObject location = jsonObject.getJSONObject("result").getJSONObject("location"); String lng = location.getString("lng"); String lat = location.getString("lat");
String shopLngLat = lat + "," + lng; map.put("address", address);
String userCoordinate = HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3", map); JSONObject userObject = JSON.parseObject(userCoordinate); if (!userObject.getString("status").equals("0")) { throw new OrderBusinessException(MessageConstant.USER_NOT_FOUND); }
JSONObject userLocation = userObject.getJSONObject("result").getJSONObject("location"); String userLngLat = userLocation.getString("lat") + "," + userLocation.getString("lng"); map.put("origin", shopLngLat); map.put("destination", userLngLat); map.put("steps_info", "0");
String json = HttpClientUtil.doGet("https://api.map.baidu.com/directionlite/v1/driving", map); JSONObject jsonResult = JSON.parseObject(json);
if (!jsonResult.getString("status").equals("0")) { throw new OrderBusinessException(MessageConstant.ROUTE_PLANNING_FAILED); }
JSONObject result = jsonResult.getJSONObject("result"); JSONArray jsonArray = (JSONArray) result.get("routes"); Integer distance = Integer.valueOf(((JSONObject) jsonArray.get(0)).getString("distance"));
if (distance > 10000) { throw new OrderBusinessException(MessageConstant.OUT_OF_DELIVERY); } } }
|
注意:这里判断距离传入的商家和客户的经纬度,是按照”纬度,经度”来传入参数的
- 有了工具类之后,我们就可以在订单提交时,进行判断
1 2 3 4 5 6
| @Autowired private BaiduUtils baiduUtils;
baiduUtils.checkOutOfRange(addressBook.getProvinceName() + addressBook.getDistrictName() + addressBook.getDetail());
|
Spring Cache
介绍
Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。
Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,例如:
- EHCache
- Caffeine
- Redis(常用)
应用场景
用户端小程序展示的数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大。
结果:系统响应慢、用户体验差
通过Redis来缓存数据,减少数据库查询操作。
起步依赖
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> <version>2.7.3</version> </dependency>
|
常用注解
在SpringCache中提供了很多缓存操作的注解,常见的是以下的几个:
注解 |
说明 |
@EnableCaching |
开启缓存注解功能,通常加在启动类上 |
@Cacheable |
在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中 |
@CachePut |
将方法的返回值放到缓存中 |
@CacheEvict |
将一条或多条数据从缓存中删除 |
在spring boot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。
例如,使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可。
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @PostMapping
@CachePut(value = "userCache", key = "#user.id") public User save(@RequestBody User user){ userMapper.insert(user); return user; }
@DeleteMapping("/delAll") @CacheEvict(cacheNames = "userCache",allEntries = true) public void deleteAll(){ userMapper.deleteAll(); }
|
思路
我们可以在用户端请求数据的时候,将对应分类菜品的所有数据保存到Redis里面进行缓存,再下一次访问时,就直接访问Redis里面的数据,保证用户的体验
在商家对菜品或者套餐进行新增的时候,我们需要清除Redis里面菜品或套餐所属分类对应的缓存,对整个分类进行更新
由于我们修改和删除,涉及到的分类比较多,情况比较复杂,所以我们直接清除菜品或套餐的所有缓存,对整个菜品或者套餐进行更新数据
Spring Task
Spring Task 是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。
定位:定时任务框架
作用:定时自动执行某段Java代码
背景:
假如用户没有及时支付,我们需要将这些订单改为取消,取消原因是用户未及时支付
还有商家每天凌晨一点休息后,把那些没有及时处理的在派送的订单改为已完成
强调:只要是需要定时处理的场景都可以使用Spring Task
cron表达式
cron表达式其实就是一个字符串,通过cron表达式可以定义任务触发的时间
构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义
每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)
举例:
2022年10月12日上午9点整 对应的cron表达式为:0 0 9 12 10 ? 2022
说明:一般日和周的值不同时设置,其中一个设置,另一个用?表示。
比如:描述2月份的最后一天,最后一天具体是几号呢?可能是28号,也有可能是29号,所以就不能写具体数字。
为了描述这些信息,提供一些特殊的字符。这些具体的细节,我们就不用自己去手写,因为这个cron表达式,它其实有在线生成器。
cron表达式在线生成器:https://cron.qqe2.com/
可以直接在这个网站上面,只要根据自己的要求去生成corn表达式即可。所以一般就不用自己去编写这个表达式。
通配符:
* 表示所有值;
? 表示未说明的值,即不关心它为何值;
- 表示一个指定的范围;
, 表示附加一个可能值;
/ 符号前表示开始时间,符号后表示每次递增的值;
代码开发
我们首先需要导入SpringTask的起步依赖(已存在:因为包含在spring-context)
SpringBoot启动类添加注解@EnableScheduling 开启任务调度
- 自定义定时任务类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
|
@Component @Slf4j public class OrderTask { @Autowired private OrderMapper orderMapper;
@Scheduled(cron = "0 * * * * ?") public void processTimeoutOrder(){ log.info("处理支付超时订单:{}", new Date()); LocalDateTime time = LocalDateTime.now().plusMinutes(-15);
List<Orders> ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.PENDING_PAYMENT, time); if (ordersList != null && ordersList.size() > 0) { for (Orders orders : ordersList) { orders.setStatus(Orders.CANCELLED); orders.setCancelTime(LocalDateTime.now()); orders.setCancelReason(MessageConstant.ORDER_OUT_TIME); orderMapper.update(orders); } }
}
@Scheduled(cron = "0 0 1 * * ?") public void processDeliveryOrder(){ log.info("处理派送中订单:{}", new Date());
LocalDateTime time = LocalDateTime.now(); List<Orders> ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.DELIVERY_IN_PROGRESS, time); for (Orders orders : ordersList) { orders.setStatus(Orders.COMPLETED); orders.setDeliveryTime(time); orderMapper.update(orders); } } }
|
WebSocket
介绍
WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接, 并进行双向数据传输。
HTTP协议和WebSocket协议对比:
- HTTP是短连接
- WebSocket是长连接
- HTTP通信是单向的,基于请求响应模式
- WebSocket支持双向通信
- HTTP和WebSocket底层都是TCP连接
思考:既然WebSocket支持双向通信,功能看似比HTTP强大,那么我们是不是可以基于WebSocket开发所有的业务功能?
WebSocket缺点:
服务器长期维护长连接需要一定的成本
各个浏览器支持程度不一
WebSocket 是长连接,受网络限制比较大,需要处理好重连
结论:WebSocket并不能完全取代HTTP,它只适合在特定的场景下使用
代码开发
这里我们的使用场景就是,当用户下单支付完成之后,需要提醒外卖商家,有新的订单
当订单超过预计时间时,用户可以进行催单,会提醒商家
- 导入Maven坐标
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
|
- 定义WebSocketSerVer,用于和客户端通信
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
|
@Component @ServerEndpoint("/ws/{sid}") public class WebSocketServer {
private static Map<String, Session> sessionMap = new HashMap();
@OnOpen public void onOpen(Session session, @PathParam("sid") String sid) { System.out.println("客户端:" + sid + "建立连接"); sessionMap.put(sid, session); }
@OnMessage public void onMessage(String message, @PathParam("sid") String sid) { System.out.println("收到来自客户端:" + sid + "的信息:" + message); }
@OnClose public void onClose(@PathParam("sid") String sid) { System.out.println("连接断开:" + sid); sessionMap.remove(sid); }
public void sendToAllClient(String message) { Collection<Session> sessions = sessionMap.values(); for (Session session : sessions) { try { session.getBasicRemote().sendText(message); } catch (Exception e) { e.printStackTrace(); } } } }
|
- 定义配置类,注册WebSocket的服务端组件
1 2 3 4 5 6 7 8 9 10 11 12
|
@Configuration public class WebSocketConfiguration {
@Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); }
}
|
- 在订单处理实现类中注入WebSocketServer
1 2
| @Autowired private WebSocketServer webSocketServer;
|
- 完成支付时,向商家发送消息
1 2 3 4 5 6 7
| HashMap map = new HashMap(); map.put("type", 1); map.put("orderId", orders.getId()); map.put("content", "订单号:" + outTradeNo);
webSocketServer.sendToAllClient(JSON.toJSONString(map));
|
- 用户催单时,向商家发送消息Controller
1 2 3 4 5 6
| @GetMapping("/reminder/{id}") @ApiOperation("催单") public Result reminder(@PathVariable Long id){ orderService.reminder(id); return Result.success(); }
|
- Service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
@Override public void reminder(Long id) { Orders orders = orderMapper.getByOrderId(id);
if (LocalDateTime.now().isBefore(orders.getEstimatedDeliveryTime())) { throw new OrderBusinessException(MessageConstant.ORDER_NOT_TIME_OUT); } Duration between = Duration.between(orders.getEstimatedDeliveryTime(), LocalDateTime.now()); Map map = new HashMap(); map.put("type", 2); map.put("orderId", id); map.put("content", "订单号:" + id + "催单,该订单已超时" + between.toHours() + "时" + between.toMinutes() % 60 + "分" + ",请及时处理"); webSocketServer.sendToAllClient(JSON.toJSONString(map)); }
|
Apache POI
Apache POI 是一个处理Miscrosoft Office各种文件格式的开源项目。简单来说就是,我们可以使用 POI 在 Java 程序中对Miscrosoft Office各种文件进行读写操作。
一般情况下,POI 都是用于操作 Excel 文件。
这里我们用来导出营业数据表
首先通过一个入门案例熟悉Apache POI
入门案例
- 导入依赖
1 2 3 4 5 6 7 8 9 10
| <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>3.16</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>3.16</version> </dependency>
|
- 代码开发(写入Excel)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| @Test public void test(){ XSSFWorkbook excel = new XSSFWorkbook();
XSSFSheet sheet = excel.createSheet("sheet1");
XSSFRow row1 = sheet.createRow(0); row1.createCell(0).setCellValue("姓名"); row1.createCell(1).setCellValue("城市");
XSSFRow row2 = sheet.createRow(1); row2.createCell(0).setCellValue("张三"); row2.createCell(1).setCellValue("北京");
XSSFRow row3 = sheet.createRow(2); row3.createCell(0).setCellValue("李四"); row3.createCell(1).setCellValue("杭州"); try ( FileOutputStream out = new FileOutputStream(new File("D:\\13684\\Desktop\\test.xlsx")) ) { excel.write(out); excel.close(); }catch (IOException e) { e.printStackTrace(); } }
|
- 读取Excel文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @Test public void test2(){ try ( FileInputStream inputStream = new FileInputStream(new File("D:\\13684\\Desktop\\test.xlsx")) ) { XSSFWorkbook excel = new XSSFWorkbook(inputStream); XSSFSheet sheet = excel.getSheetAt(0);
int lastRowNum = sheet.getLastRowNum();
for (int i = 0; i <= lastRowNum; i++) { XSSFRow row = sheet.getRow(i); String value = row.getCell(0).getStringCellValue(); String value1 = row.getCell(1).getStringCellValue(); System.out.println(value + " " + value1);
} excel.close();
}catch (IOException ex) { ex.printStackTrace(); } }
|
代码开发
我们需要导出运营数据30天内的表,首先给程序提供模板,其次填入数据即可
接口和Controller不在赘述,直接看ServiceImpl(利用HttpServletResponse得到输出流,向浏览器输出文件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
|
@Override public void export(HttpServletResponse response) { LocalDate begin = LocalDate.now().minusDays(30); LocalDate end = LocalDate.now().minusDays(1); BusinessDataVO businessDataVO = workspaceService.businessData( LocalDateTime.of(begin, LocalTime.MIN), LocalDateTime.of(end, LocalTime.MAX)); try ( InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx") ) { XSSFWorkbook excel = new XSSFWorkbook(inputStream); XSSFSheet sheet = excel.getSheetAt(0); sheet.getRow(1).getCell(1).setCellValue(begin + " - " + end);
XSSFRow row4 = sheet.getRow(3); row4.getCell(2).setCellValue(businessDataVO.getTurnover()); row4.getCell(4).setCellValue(businessDataVO.getOrderCompletionRate()); row4.getCell(6).setCellValue(businessDataVO.getNewUsers());
XSSFRow row5 = sheet.getRow(4); row5.getCell(2).setCellValue(businessDataVO.getValidOrderCount()); row5.getCell(4).setCellValue(businessDataVO.getUnitPrice());
for (int i = 0; i < 30; i++) { LocalDate date = begin.plusDays(i); BusinessDataVO businessDataVODetail = workspaceService.businessData( LocalDateTime.of(date, LocalTime.MIN), LocalDateTime.of(date, LocalTime.MAX)); XSSFRow row = sheet.getRow(7 + i); row.getCell(1).setCellValue(date.toString()); row.getCell(2).setCellValue(businessDataVODetail.getTurnover()); row.getCell(3).setCellValue(businessDataVODetail.getValidOrderCount()); row.getCell(4).setCellValue(businessDataVODetail.getOrderCompletionRate()); row.getCell(5).setCellValue(businessDataVODetail.getUnitPrice()); row.getCell(6).setCellValue(businessDataVODetail.getNewUsers()); } ServletOutputStream outputStream = response.getOutputStream(); excel.write(outputStream); excel.close(); }catch (IOException ex){ ex.printStackTrace(); }
}
|
注意事项
- 如果想获取插入之后的主键值,需要在xml文件里,配置这些
useGeneratedKeys=”true” 用数据库生成的主键值
keyProperty=”id” 将值存入java对象的id属性
1 2 3 4
| <insert id="add" useGeneratedKeys="true" keyProperty="id"> insert into dish (name, category_id, price, image, description, create_time, update_time, create_user,update_user) VALUE (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{createTime},#{updateTime}, #{createUser}, #{updateUser}) </insert>
|
- 如果一个业务要操作多张表,要在service层加事务注解,同成功,同失败
- Apache ECharts 属于前端操控,后端人员秩序返回数据即可
这里运用的StringUtils.join()的函数属于org.apache.commons.lang