苍穹外卖初始工程涉及技术点

苍穹外卖初始工程涉及技术点

1.全局异常处理器

spring mvc架构中各层会出现大量的try{...} catch{...} finally{...}代码块,不仅有大量的冗余代码,而且还影响代码的可读性。这样就需要定义个全局统一异常处理器,以便业务层再也不必处理异常。

@RestControllerAdvice @Slf4j public class GlobalExceptionHandler { /** * 捕获业务异常 * @param ex * @return */ @ExceptionHandler public Result exceptionHandler(BaseException ex){ log.error("异常信息:{}", ex.getMessage()); return Result.error(ex.getMessage()); } }

springboot优雅的做全局异常处理(完整代码+已运行使用)_yuqueglobalexceptionhandler extends commonexceptio-CSDN博客

@Responsebody将对象转换为json返回

2. 定义消息返回接收对象

<T> Result<T><T>放在方法名前,这表示这些方法可以处理任何类型的数据。

@Data public class Result<T> implements Serializable { private Integer Code;//编码,1成功 private String msg;//错误信息 private T data;//数据 public static <T> Result<T> success(){ Result<T> result = new Result<T>(); result.Code = 1 ; return result; } public static <T> Result<T> success(T object){ Result<T> result = new Result<>(); result.Code=1; result.data = object; return result; } public static <T> Result<T> error(String msg){ Result<T> result = new Result<>(); result.Code = 0; result.msg = msg; return result; } }

3.登录校验

3.1 md5加密存储,md5是单向的,不可逆

password = DigestUtils.md5DigestAsHex(password.getBytes());

3.2 jwt

jwt 令牌用于登录校验,相当于一个身份,需要注意,令牌的生成和使用的秘钥需要一致

在登录操作中,需要给前端返回一个jwt

这一块使用jwt携带用户id,下发令牌,前端接收到令牌,保存在浏览器本地(移动端,电脑端均支持)每次前端发起请求,后端通过拦截器拦截,查看是否有id

3.2.1 jwt的使用

JWT 的使用也需要注意安全方面的问题。例如,不应该在 JWT 中存储敏感信息,因为其 payload 部分是可以被解码的

public class JwtUtil { /** * 生成jwt * 使用Hs256算法, 私匙使用固定秘钥 * * @param secretKey jwt秘钥 * @param ttlMillis jwt过期时间(毫秒) * @param claims 设置的信息 * @return */ public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) { // 指定签名的时候使用的签名算法,也就是header那部分 SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; // 生成JWT的时间 long expMillis = System.currentTimeMillis() + ttlMillis; Date exp = new Date(expMillis); // 设置jwt的body JwtBuilder builder = Jwts.builder() // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的 .setClaims(claims) // 设置签名使用的签名算法和签名使用的秘钥 .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8)) // 设置过期时间 .setExpiration(exp); return builder.compact(); } /** * Token解密 * * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个 * @param token 加密后的token * @return */ public static Claims parseJWT(String secretKey, String token) { // 得到DefaultJwtParser Claims claims = Jwts.parser() // 设置签名的秘钥 .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8)) // 设置需要解析的jwt .parseClaimsJws(token).getBody(); return claims; } } //登录成功后,生成jwt令牌 Map<String, Object> claims = new HashMap<>(); claims.put(JwtClaimsConstant.EMP_ID, employee.getId()); String token = JwtUtil.createJWT( jwtProperties.getAdminSecretKey(), jwtProperties.getAdminTtl(), claims);

注意此处的jwtProperties类,使用了

@ConfigurationProperties(prefix = "sky.jwt")将外部配置(如application.yml)里面的数据绑定到该类,employee是从service层返回的结果

3.2.2拦截器的使用

拦截器有两个步骤,一个是定义拦截器的设置,实现 HandlerInterceptor 接口,重写方法

/** * jwt令牌校验的拦截器 */ @Component @Slf4j public class JwtTokenAdminInterceptor implements HandlerInterceptor { @Autowired private JwtProperties jwtProperties; /** * 校验jwt * * @param request * @param response * @param handler * @return * @throws Exception */ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //判断当前拦截到的是Controller的方法还是其他资源 if (!(handler instanceof HandlerMethod)) { //当前拦截到的不是动态方法,直接放行 return true; } //1、从请求头中获取令牌 String token = request.getHeader(jwtProperties.getAdminTokenName());//此处更改为你的token命名 //2、校验令牌 try { log.info("jwt校验:{}", token); Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token); Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString()); log.info("当前员工id:", empId); //3、通过,放行 return true; } catch (Exception ex) { //4、不通过,响应401状态码 response.setStatus(401); return false; } } }

另一个则是注册拦截器

@Configuration @Slf4j public class WebMvcConfiguration extends WebMvcConfigurationSupport { @Autowired private JwtTokenAdminInterceptor jwtTokenAdminInterceptor; /** * 注册自定义拦截器 * * @param registry */ protected void addInterceptors(InterceptorRegistry registry) { log.info("开始注册自定义拦截器..."); registry.addInterceptor(jwtTokenAdminInterceptor) .addPathPatterns("/admin/**")//添加拦截的路径 .excludePathPatterns("/admin/employee/login");//去掉无需拦截的路径 } }

通过这两步就可以实现一个拦截器的基本使用

4.swagger的knife4j

开发阶段使用的框架,帮助后端开发人员进行接口测试

4.1 在配置类中加入Knife4j配置

/** * 通过knife4j生成接口文档 * @return */ @Bean public Docket docket() { ApiInfo apiInfo = new ApiInfoBuilder() .title("苍穹外卖项目接口文档") .version("2.0") .description("苍穹外卖项目接口文档") .build(); Docket docket = new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo) .select() .apis(RequestHandlerSelectors.basePackage("com.sky.controller")) .paths(PathSelectors.any()) .build(); return docket; }

4.2 设置静态资源映射

/** * 设置静态资源映射 * @param registry */ protected void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/"); }

4.3 常用注解

@Api(tags = "员工相关接口")//写在类上 @ApiOperation("员工登录")//写在方法上

5.分页查询

5.1 基础使用

分页查询需要前端传递,查询的用户,页码,每页记录数,返回分页总数,还有每一页信息

这一块使用了mybatics提供的插件pagehelper,

注意此块返回类型必须使用Page<>类型,第一步使用的startPage使用底层的方法在 动态映射中自动添加limit字段

此处mapper的扫描路径应该在application.yml中定义

mybatis: #mapper配置文件 mapper-locations: classpath:mapper/*.xml

5.2 日期序列化

这一块有两种方式,一种是在属性上加入注解@JsonFormat(pattern =

"yyy-MM-dd HH:mm")

第二种则是拓展springmvc 消息转化器,统一处理

//拓展springmvc 消息转化器 protected void extendMessageConverters(List<HttpMessageConverter<?>> converters){ log.info("拓展消息转换器"); //创建一个消息转换器对象 MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); //为消息转换器设置一个对象转换器,对象转换器可以将java对象序列化为json数据 converter.setObjectMapper(new JacksonObjectMapper()); //将自己的消息转化器加入容器 converters.add(0,converter); }
package com.tlias.json; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; /** * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象] * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON] */ 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:ss"; 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); } }

6. redis

6.1 redis基础操作

6.2 redis在java里面的运用(spring data redis)

这里分三步

6.2.1 配置redis数据源

6.2.2 编写配置类,编写RedisTemplate对象

在application.yml中编写

6.2.3 通过RedisTemplate操作redis

需要先确保本地redis已经运行起来

创建配置对象,通过spring容器进行引入即可使用

@Configuration @Slf4j public class RedisConfiguration { @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){ log.info("开始创建redis模板对象。。。"); RedisTemplate redisTemplate = new RedisTemplate(); //设置redis连接工厂对象 redisTemplate.setConnectionFactory(redisConnectionFactory); //设置redis key序列化器 //防止保存到redis里面的键出现看不懂的情况 redisTemplate.setKeySerializer(new StringRedisSerializer()); return redisTemplate; } } @SpringBootTest public class SpringDataRedisTest { @Autowired private RedisTemplate redisTemplate; @Test public void testRedisTemplate(){ ValueOperations valueOperations = redisTemplate.opsForValue(); valueOperations.set("color","white"); String color = (String) valueOperations.get("color"); System.out.println(color); } }

6.3 redis缓存实践

每个菜品下的数据进行缓存

数据库菜品变更进行清除数据

@GetMapping("/list") @ApiOperation("根据分类id查询菜品") public Result<List<DishVO>> list(Long categoryId) { //构造redis中的key dish_分类id String key = "dish_" + categoryId; //查询redis中是否存在菜品数据 List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key); //如果存在,直接返回,无须查询数据库 if (list != null && list.size()>0 ){ return Result.success(list); } Dish dish = new Dish(); dish.setCategoryId(categoryId); dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品 //如果不存在,查询数据库,将查询到的数据放入redis中 list = dishService.listWithFlavor(dish); redisTemplate.opsForValue().set(key,list); return Result.success(list); }
//清理缓存数据 String key ="dish_" + dishDTO.getCategoryId(); cleanCache(key); private void cleanCache(String pattern){ Set keys = redisTemplate.keys(pattern); redisTemplate.delete(keys); }

6.3.1 Spring Cache

是一个框架,基于注解的缓存功能,spring Cache提供了一层抽象,底层可以切换不同的缓存实现

如(redis,caffeine,EHCache)

引入依赖

在启动类加入@EnableCaching 在相应的方法上加入相应注解

@PostMapping @CachePut(cacheNames = "userCache", key=#user.id) // key的生成 userCache::100 //一个冒号代表一层,即树形结构存储,一般用上面这一种方式 @CachePut(cacheNames = "userCache", key = #result.id) public User save(@RequestBody User user){ userMapper.insert(user); return user; } //删除全部数据 @CacheEvict(cacheNames = "userCache" , allEntries = true)

7.httpClient

后端构造http请求,与okhttp类似(个人觉得okhttp比较好用)RestTemplate,相对而言这三个技术都是发送http请求

/** * Http工具类 */ public class HttpClientUtil { static final int TIMEOUT_MSEC = 5 * 1000; /** * 发送GET方式请求 * @param url * @param paramMap * @return */ public static String doGet(String url,Map<String,String> paramMap){ // 创建Httpclient对象 CloseableHttpClient httpClient = HttpClients.createDefault(); String result = ""; CloseableHttpResponse response = null; try{ URIBuilder builder = new URIBuilder(url); if(paramMap != null){ for (String key : paramMap.keySet()) { builder.addParameter(key,paramMap.get(key)); } } URI uri = builder.build(); //创建GET请求 HttpGet httpGet = new HttpGet(uri); //发送请求 response = httpClient.execute(httpGet); //判断响应状态 if(response.getStatusLine().getStatusCode() == 200){ result = EntityUtils.toString(response.getEntity(),"UTF-8"); } }catch (Exception e){ e.printStackTrace(); }finally { try { response.close(); httpClient.close(); } catch (IOException e) { e.printStackTrace(); } } return result; } /** * 发送POST方式请求 * @param url * @param paramMap * @return * @throws IOException */ public static String doPost(String url, Map<String, String> paramMap) throws IOException { // 创建Httpclient对象 CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = null; String resultString = ""; try { // 创建Http Post请求 HttpPost httpPost = new HttpPost(url); // 创建参数列表 if (paramMap != null) { List<NameValuePair> paramList = new ArrayList(); for (Map.Entry<String, String> param : paramMap.entrySet()) { paramList.add(new BasicNameValuePair(param.getKey(), param.getValue())); } // 模拟表单 UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList); httpPost.setEntity(entity); } httpPost.setConfig(builderRequestConfig()); // 执行http请求 response = httpClient.execute(httpPost); resultString = EntityUtils.toString(response.getEntity(), "UTF-8"); } catch (Exception e) { throw e; } finally { try { response.close(); } catch (IOException e) { e.printStackTrace(); } } return resultString; } /** * 发送POST方式请求 * @param url * @param paramMap * @return * @throws IOException */ public static String doPost4Json(String url, Map<String, String> paramMap) throws IOException { // 创建Httpclient对象 CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = null; String resultString = ""; try { // 创建Http Post请求 HttpPost httpPost = new HttpPost(url); if (paramMap != null) { //构造json格式数据 JSONObject jsonObject = new JSONObject(); for (Map.Entry<String, String> param : paramMap.entrySet()) { jsonObject.put(param.getKey(),param.getValue()); } StringEntity entity = new StringEntity(jsonObject.toString(),"utf-8"); //设置请求编码 entity.setContentEncoding("utf-8"); //设置数据类型 entity.setContentType("application/json"); httpPost.setEntity(entity); } httpPost.setConfig(builderRequestConfig()); // 执行http请求 response = httpClient.execute(httpPost); resultString = EntityUtils.toString(response.getEntity(), "UTF-8"); } catch (Exception e) { throw e; } finally { try { response.close(); } catch (IOException e) { e.printStackTrace(); } } return resultString; } private static RequestConfig builderRequestConfig() { return RequestConfig.custom() .setConnectTimeout(TIMEOUT_MSEC) .setConnectionRequestTimeout(TIMEOUT_MSEC) .setSocketTimeout(TIMEOUT_MSEC).build(); } }

这一块是为了小程序请求后端之后,后端发送请求到微信接口服务,获取open_id,这里需要去查看微信官方文档

8.公共字段自动填充

8.1 获取当前用户登录id

客户端发起的每一次请求就是一个线程

ThreadLocal 不是Thread 而是Thread的局部变量

ThreadLocal为每一个线程提供单独存储空间,只有在线程内才能获取到对应的值

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

8.2具体实现

aop和拦截器类似

aop是面向切面编程(面向方法编程) ,比如统计每个业务方法的执行耗时

通知,切入点(对哪些类哪些方法进行拦截)

标识AutoFill注解

//用于标识某个方法需要进行公共字段自动填充,自定义注解 //指定注解加在哪里 @Target(ElementType.METHOD)//只能加在方法上 @Retention(RetentionPolicy.RUNTIME) public @interface AutoFIll { //数据库操作类型,update,insert,枚举类型 OperationType value(); }

自定义切面

package com.sky.aspect; import com.sky.annotation.AutoFIll; import com.sky.constant.AutoFillConstant; import com.sky.context.BaseContext; import com.sky.enumeration.OperationType; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.time.LocalDateTime; //自定义切面类型,实现公共字段自动填充 @Aspect @Component @Slf4j public class AutoFillAspect { //定义切入点 //annotation 同时满足加入AutoFill @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFIll)") public void autoFillPointCut(){} //前置通知,为公共字段赋值 //指定切入点 @Before("autoFillPointCut()") //joinPoint连接点,什么方法被拦截到了,具体参数 public void autoFill(JoinPoint joinPoint) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { log.info("开始进行公共字段的自动填充"); //获取到当前被拦截的方法上的数据库操作类型 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 == OperationType.INSERT){ //为四个公共字段赋值 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); setCreateUser.invoke(entity,currentId); setCreateTime.invoke(entity,now); setUpdateTime.invoke(entity,now); setUpdateUser.invoke(entity,currentId); } else if (operationType ==OperationType.UPDATE) { 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); } } }

9.比较杂的点

这一块使用了第一点的全局异常处理器,处理sql语句异常,应关注里面,split方法的使用

@RestController = @Controller + @ResponseBody

@ResponseBody使用json返回对象 当前端传回来的数据是json时候,在接收对象前使用@RequestBody

路径参数需要多加@PathVariable

@ConfigurationProperties(prefix = )将application.yml绑定到该类中

@Component //该注解将外部配置绑定到当前类 @ConfigurationProperties(prefix = "sky.jwt") @Data public class JwtProperties { /** * 管理端员工生成jwt令牌相关配置 */ private String adminSecretKey; private long adminTtl; private String adminTokenName; /** * 用户端微信用户生成jwt令牌相关配置 */ private String userSecretKey; private long userTtl; private String userTokenName; }

aop面向方法编程,面向特定方法编程,比如统计每个业务方法的执行耗时

//使其符合前端的yyyy-MM-dd
@DateTimeFormat(pattern = "yyyy-MM-dd")

//日期计算,计算指定日期的后一天对应日期
begin =begin.plusDays(1);

LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);精确到分秒

//把datelist集合里面的元素取出来,加上,分隔,变成字符串
.dateList(StringUtils.join(dateList,","))

10.Spring Task

可以按照某个约定的时间自动执行某段程序 ,定时任务框架

cron表达式 直接使用cron表达式在线生成器

解决用户一直待支付状态,以及订单一直处于待派送的状态

@Component @Slf4j public class OrderTask { @Autowired private OrderMapper orderMapper; //处理超时订单方法 @Scheduled(cron = "0 * * * * ?")//每分钟触发一次 public void processTimeoutOrder(){ log.info("定时处理超时订单,{}", LocalDateTime.now()); LocalDateTime time = LocalDateTime.now().plusMinutes(-15); //select * from orders where status = ? and order_time < (当前时间-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.setCancelReason("订单超时,自动取消"); orders.setCancelTime(LocalDateTime.now()); orderMapper.update(orders); } } } //处理一直处于派送中状态的订单 @Scheduled(cron = "0 0 1 * * ?")//每天凌晨一点触发一次 public void processDeliveryOrder(){ LocalDateTime time = LocalDateTime.now().plusMinutes(-60); log.info("定时处理处于派送中的订单,{}",LocalDateTime.now()); List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS, time); if (ordersList != null && ordersList.size()>0){ for (Orders orders : ordersList){ orders.setStatus(Orders.COMPLETED); orderMapper.update(orders); } } } }

11.WebSocket

http为请求响应模式,单向,短连接

websocket为长连接,双向

http和websocket都是基于tcp

使用场景:视频弹幕,网页聊天,实时

在外卖系统中,ws:localhost并没有指定端口号,是因为在naginx里面已经进行了配置

步骤为三步

1.导入maven

2.导入服务端组件webSocketServer

3.配置类WebSocketConfiguration,注册组件

package com.sky.websocket; import org.springframework.stereotype.Component; import javax.websocket.OnClose; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.util.Collection; import java.util.HashMap; import java.util.Map; /** * WebSocket服务 */ @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); } /** * 收到客户端消息后调用的方法 * * @param message 客户端发送过来的消息 */ @OnMessage public void onMessage(String message, @PathParam("sid") String sid) { System.out.println("收到来自客户端:" + sid + "的信息:" + message); } /** * 连接关闭调用的方法 * * @param sid */ @OnClose public void onClose(@PathParam("sid") String sid) { System.out.println("连接断开:" + sid); sessionMap.remove(sid); } /** * 群发 * * @param message */ public void sendToAllClient(String message) { Collection<Session> sessions = sessionMap.values(); for (Session session : sessions) { try { //服务器向客户端发送消息 session.getBasicRemote().sendText(message); } catch (Exception e) { e.printStackTrace(); } } } }
package com.sky.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; /** * WebSocket配置类,用于注册WebSocket的Bean */ //ServerEndPointExporter用于自动注册使用@ServerEndpoint注解的WebSocket端点。使得客户端可以通过WebSocket连接到这个端点。 @Configuration public class WebSocketConfiguration { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }

12.Apache ECharts

基于JavaScript的可视化图表库(用直观的图展示数据)

后端提供符合格式要求的动态数据,响应给前端展示图表

@Autowired private ReportService reportService; //@GetMapping("/turnoverStatistics") @ApiOperation("营业额统计") public Result<TurnoverReportVO> turnoverStatistics( //使其符合前端的yyyy-MM-dd @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin , @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end ){ log.info("营业额数据统计,{},{}",begin,end); return Result.success(reportService.getTurnoverStatics(begin,end)); } public TurnoverReportVO getTurnoverStatics(LocalDate begin, LocalDate end) { //当前集合用于存放从begin到end范围内的每天日期 List<LocalDate> dateList = new ArrayList<>(); dateList.add(begin); while (!begin.equals(end)){ //日期计算,计算指定日期的后一天对应日期 begin =begin.plusDays(1); dateList.add(begin); } List<Double> turnoverList = new ArrayList<>(); for (LocalDate date : dateList) { //查询date日期对应的营业额数据,状态为已完成的订单金额统计 LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN); LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX); //select sum(amount) from orders where order_time > ? and order_time <? and status = 5 Map map = new HashMap(); map.put("begin",beginTime); map.put("end",endTime); map.put("status", Orders.COMPLETED); Double turnover = orderMapper.sumByMap(map); //防止返回的数据是null turnover = turnover == null ? 0.0 : turnover; turnoverList.add(turnover); } return TurnoverReportVO .builder() //把datelist集合里面的元素取出来,加上,分隔,变成字符串 .dateList(StringUtils.join(dateList,",")) .turnoverList(StringUtils.join(turnoverList,",")) .build(); } <select id="sumByMap" resultType="java.lang.Double"> select sum(amount) from orders <where> <if test="begin != null"> and order_time &gt; #{begin} </if> <if test="end != null"> and order_time &lt; #{end} </if> <if test="status != null"> and status = #{status} </if> </where> </select>

13.Apache POI

步骤有 导入maven坐标,创建sheet页,sheet页创建行,创建单元格,setcellvalue写入数据,写入(输出流) ,关闭资源

13.1 导出excel报表

无请求参数,无返回数据,服务端通过输出流进行下载

1 设计模版,存放在resource目录下新建Template

2 查询近30天数据

3 POI写入

4 输出流下载到浏览器

@GetMapping("/export") @ApiOperation("导出运营数据报表") //这一块使用的httpServletResponse是为了获取输出流 public void export(HttpServletResponse response){ reportService.exportBusinessData(response); } public void exportBusinessData(HttpServletResponse response) { //查询数据库,获取营业数据,查询最近30天运营数据 LocalDate dateBegin = LocalDate.now().minusDays(30); LocalDate dateEnd = LocalDate.now().minusDays(1); //查询概览数据 BusinessDataVO businessDataVO = workspaceService.getBusinessData(LocalDateTime.of(dateBegin, LocalTime.MIN), LocalDateTime.of(dateEnd, LocalTime.MAX)); //通过POI将数据写入到excel //类路径中获取资源文件作为输入流 InputStream in = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx"); try{ XSSFWorkbook excel = new XSSFWorkbook(in) ; //获取表格文件的sheet页 XSSFSheet sheet = excel.getSheet("sheet1"); //填充数据--时间 sheet.getRow(1).getCell(1).setCellValue("时间"+dateBegin+"至"+dateEnd); //获得第四行 XSSFRow row = sheet.getRow(3); row.getCell(2).setCellValue(businessDataVO.getTurnover()); row.getCell(4).setCellValue(businessDataVO.getOrderCompletionRate()); row.getCell(6).setCellValue(businessDataVO.getNewUsers()); //获得第五行 row = sheet.getRow(4); row.getCell(2).setCellValue(businessDataVO.getValidOrderCount()); row.getCell(4).setCellValue(businessDataVO.getUnitPrice()); //填充明细数据 for (int i=0;i<30;i++){ LocalDate date = dateBegin.plusDays(1); //查询某一天的营业数据 BusinessDataVO businessData = workspaceService.getBusinessData(LocalDateTime.of(date, LocalTime.MIN), LocalDateTime.of(date, LocalTime.MAX)); row = sheet.getRow(7 + i); row.getCell(1).setCellValue(date.toString()); row.getCell(2).setCellValue(businessData.getTurnover()); row.getCell(3).setCellValue(businessData.getValidOrderCount()); row.getCell(4).setCellValue(businessData.getOrderCompletionRate()); row.getCell(5).setCellValue(businessData.getUnitPrice()); row.getCell(6).setCellValue(businessData.getNewUsers()); } //通过输出流将excel下载到客户端浏览器 ServletOutputStream out = response.getOutputStream(); excel.write(out); //关闭资源 out.close(); excel.close(); }catch (IOException e){ e.printStackTrace(); } }