黑马---苍穹外卖总结下:

1.小程序端

1.1 HttpClient
1.1.1 介绍

HttpClient 是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。

1.1.2 HttpClient作用
  • 发送HTTP请求

  • 接收响应数据

1.1.3 如何使用

HttpClient的maven坐标: 在之前在使用OSS时导入相关依赖,其中已经包含了HttpClient相关依赖,所以这部分可导可不导

java 复制代码
<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方法发送请求

1.1.4 GET方式请求实现
  1. 创建HttpClient对象

  2. 创建请求对象

  3. 发送请求,接受响应结果

  4. 解析结果

  5. 关闭资源

    编写测试代码:

java 复制代码
@SpringBootTest
public class HttpClientTest {
    /*
     *测试通过httpclient发送GET请求
     */
    @Test
    public void testGET() throws IOException {
        //创建httpclient对象
        /*CloseableHttpClient是一个实现类,实现了接口httpClient中的方法*/
        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();
    }
1.1.5 POST方式请求实现

在HttpClientTest中添加POST方式请求方法,相比GET请求来说,POST请求若携带参数需要封装请求体对象,并将该对象设置在请求对象中。

实现步骤:

  1. 创建HttpClient对象

  2. 创建请求对象

  3. 发送请求,接收响应结果

  4. 解析响应结果

  5. 关闭资源

编写测试代码:

java 复制代码
/*
     *测试通过httpclient发送POST请求
     */
    @Test
    public void testPOST() throws IOException {
        //创建httpclient对象
        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 = httpPost.getEntity();
        String body = EntityUtils.toString(entity1);
        System.out.println("响应数据"+body);
​
        //关闭资源
        response.close();
        httpClient.close();
    }
1.2 微信小程序开发
1.2.1介绍

**首先,**在进行小程序开发时,需要先去注册一个小程序,在注册的时候,它实际上又分成了不同的注册的主体。我们可以以个人的身份来注册一个小程序,当然,也可以以企业政府、媒体或者其他组织的方式来注册小程序。那么,不同的主体注册小程序,最终开放的权限也是不一样的。比如以个人身份来注册小程序,是无法开通支付权限的。若要提供支付功能,必须是企业、政府或者其它组织等。所以,不同的主体注册小程序后,可开发的功能是不一样的。

**然后,**微信小程序我们提供的一些开发的支持,实际上微信的官方是提供了一系列的工具来帮助开发者快速的接入 并且完成小程序的开发,提供了完善的开发文档,并且专门提供了一个开发者工具,还提供了相应的设计指南,同时也提供了一些小程序体验DEMO,可以快速的体验小程序实现的功能。

**最后,**开发完一个小程序要上线,也给我们提供了详细地接入流程。

1.2.2 小程序目录结构

小程序包含一个描述整体程序的 app 和多个描述各自页面的 page。主体部分由三个文件组成,必须放在项目的根目录。

文件说明:

  • app.js : 必须存在,主要存放小程序的逻辑代码

  • app.json :必须存在,小程序配置文件,主要存放小程序的公共配置

  • app.wxss: 非必须存在,主要存放小程序公共样式表,类似于前端的CSS样式

  • js文件:必须存在,存放页面业务逻辑代码,编写的js代码。

  • json文件:非必须,存放页面相关的配置。

  • wxml 文件:必须存在,存放页面结构,主要是做页面布局,页面效果展示的,类似于HTML页面

  • wxss文件:非必须,存放页面样式表,相当于CSS文件。

1.2.3 微信登录流程
  1. 小程序端,调用wx.login()获取code,就是授权码。

  2. 小程序端,调用wx.request()发送请求并携带code,请求开发者服务器(自己编写的后端服务)。

  3. 开发者服务端,通过HttpClient向微信接口服务发送请求,并携带appId+appsecret+code三个参数。

  4. 开发者服务端,接收微信接口服务返回的数据,session_key+opendId等。opendId是微信用户的唯一标识。

  5. 开发者服务端,自定义登录态,生成令牌(token)和openid等数据返回给小程序端,方便后绪请求身份校验。

  6. 小程序端,收到自定义登录态,存储storage。

  7. 小程序端,后绪通过wx.request()发起业务请求时,携带token。

  8. 开发者服务端,收到请求后,通过携带的token,解析当前登录用户的id。

  9. 开发者服务端,身份校验通过后,继续相关的业务逻辑处理,最终返回业务数据。

说明:

1.调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。

2.调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台帐号下的唯一标识UnionID (若当前小程序已绑定到微信开放平台帐号) 和 会话密钥 session_key

之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。

1.2.4 配置微信登录所需配置项

注意!!!:为微信用户生成jwt令牌时使用的配置项:

java 复制代码
sky:
  jwt:
    #user端
    #设置jwt签名加密时使用的秘钥
    user-secret-key: itheima
    # 设置jwt过期时间
    user-ttl: 144000000
    # 设置前端传递过来的令牌名称
    user-token-name: authentication

配置user端拦截器:

java 复制代码
@Component
@Slf4j
public class JwtTokenUserInterceptor 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;
        }
        //System.out.println("当前线程id为:"+Thread.currentThread().getId());
        //1、从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getUserTokenName());
​
        //2、校验令牌
        try {
            //log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
            Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
            log.info("当前用户id:", userId);
​
            //3、通过,放行
            BaseContext.setCurrentId(userId);
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}

在配置文件中配置拦截路径:

java 复制代码
protected void addInterceptors(InterceptorRegistry registry) {
        log.info("开始注册自定义拦截器...");
        registry.addInterceptor(jwtTokenUserInterceptor)
                .addPathPatterns("/user/**")
                .excludePathPatterns("/user/user/login")
                .excludePathPatterns("/user/shop/status");
    }
1.2.5 订单支付
1.2.5.1 微信支付相关接口:

JSAPI下单:商户系统调用该接口在微信支付服务后台生成预支付交易单(对应时序图的第5步)

下单请求示例:

XML 复制代码
{
    "mchid": "1900006XXX",
    "out_trade_no": "1217752501201407033233368318",
    "appid": "wxdace645e0bc2cXXX",
    "description": "Image形象店-深圳腾大-QQ公仔",
    "notify_url": "https://www.weixin.qq.com/wxpay/pay.php",
    "amount": {
        "total": 1,
        "currency": "CNY"
    },
    "payer": {
        "openid": "o4GgauInH_RCEdvrrNGrntXDuXXX"
    }
}

微信小程序调起支付:通过JSAPI下单接口获取到发起支付的必要参数prepay_id,然后使用微信支付提供的小程序方法调起小程序支付(对应时序图的第10步)

调起支付请求示例:

复制代码
XML 复制代码
wx.requestPayment
(
    {
        "timeStamp": "1414561699",
        "nonceStr": "5K8264ILTKCH16CQ2502SI8ZNMTM67VS",
        "package": "prepay_id=wx201410272009395522657a690389285100",
        "signType": "RSA",
        "paySign": "oR9d8PuhnIc+YZ8cBHFCwfgpaK9gd7vaRvkYD7rthRAZ\/X+QBhcCYL21N7cHCTUxbQ+EAt6Uy+lwSN22f5YZvI45MLko8Pfso0jm46v5hqcVwrk6uddkGuT+Cdvu4WBqDzaDjnNa5UK3GfE1Wfl2gHxIIY5lLdUgWFts17D4WuolLLkiFZV+JSHMvH7eaLdT9N5GBovBwu5yYKUR7skR8Fu+LozcSqQixnlEZUfyE55feLOQTUYzLmR9pNtPbPsu6WVhbNHMS3Ss2+AehHvz+n64GDmXxbX++IOBvm2olHu3PsOUGRwhudhVf7UcGcunXt8cqNjKNqZLhLw4jq\/xDg==",
        "success":function(res){},
        "fail":function(res){},
        "complete":function(res){}
    }
)
1.2.5.2 完成微信支付有两个关键的步骤:
  • 第一个 就是需要在商户系统当中调用微信后台的一个下单接口,就是生成预支付交易单。

  • 第二个 就是支付成功之后微信后台会给推送消息

    解决:微信提供的方式就是对数据进行加密、解密、签名多种方式。要完成数据加密解密,需要提前准备相应的一些文件,其实就是一些证书。

1.2.5.3 调用到商户系统

微信后台会调用到商户系统给推送支付的结果 ,在这里我们就会遇到一个问题,就是微信后台怎么就能调用到我们这个商户系统呢?因为这个调用过程,其实本质上也是一个HTTP请求

目前,商户系统它的ip地址就是当前自己电脑的ip地址,只是一个局域网内的ip地址,微信后台无法调用到

解决: 内网穿透**。通过** cpolar软件可以获得一个临时域名,而这个临时域名是一个公网ip**,这样,微信后台就可以请求到商户系统了。

1.2.6 内网穿透
1.2.6.1 cpolar指定authtoken

复制authtoken:

cpolar - secure introspectable tunnels to localhost

--->注册登录 --->验证---->复制自己的隧道 Authtoken

1.2.6.2 执行命令:

命令行输入: cpolar.exe authtoken +你的隧道authtoken

获取临时域名

执行命令:cpolar.exe http 8080

将得到的公网ip复制,放到配置文件中

java 复制代码
sky:
  wechat:
    appid: 
    secret: 
    mchid : 1561414331
    mchSerialNo: 4B3B3DC35414AD50B1B755BAF8DE9CC7CF407606
    privateKeyFilePath: D:\apiclient_key.pem
    apiV3Key: CZBK51236435wxpay435434323FFDuv3
    weChatPayCertFilePath: D:\wechatpay_166D9公网ip+6F876F45C7D07CE98952A96EC980368ACFC.pem
    #支付成功的回调地址
    notifyUrl: 公网ip/notify/paySuccess 
    #退款成功的回调地址
    refundNotifyUrl: 公网ip/notify/refundSuccess 

验证临时域名有效性

启动项目,访问接口文档:http://localhost:8080/doc.html 使用临时域名访问,证明临时域名生效

1.2.7 微信支付代码

WeChatProperties.java:读取配置

java 复制代码
@Component
@ConfigurationProperties(prefix = "sky.wechat")
@Data
public class WeChatProperties {

    private String appid; //小程序的appid
    private String secret; //小程序的秘钥
    private String mchid; //商户号
    private String mchSerialNo; //商户API证书的证书序列号
    private String privateKeyFilePath; //商户私钥文件
    private String apiV3Key; //证书解密的密钥
    private String weChatPayCertFilePath; //平台证书
    private String notifyUrl; //支付成功的回调地址
    private String refundNotifyUrl; //退款成功的回调地址
}

在OrderServiceImpl.java中实现payment和paySuccess两个方法

java 复制代码
/*
     * 订单支付
     * @param ordersPaymentDTO
     * @return
     */
    public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {
        // 当前登录用户id
        Long userId = BaseContext.getCurrentId();
        User user = userMapper.getById(userId);
/*        //调用微信支付接口,生成预支付交易单
        JSONObject jsonObject = weChatPayUtil.pay(
                ordersPaymentDTO.getOrderNumber(), //商户订单号
                new BigDecimal(0.01), //支付金额,单位 元
                "苍穹外卖订单", //商品描述
                user.getOpenid() //微信用户的openid
        );

        if (jsonObject.getString("code") != null && jsonObject.getString("code").equals("ORDERPAID")) {
            throw new OrderBusinessException("该订单已支付");
        }
*/
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("code","ORDERPAID");
        OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);
        vo.setPackageStr(jsonObject.getString("package"));
        Integer OrderPaidStatus = Orders.PAID;//支付状态,已支付
        Integer OrderStatus = Orders.TO_BE_CONFIRMED;  //订单状态,待接单
        LocalDateTime check_out_time = LocalDateTime.now();//更新支付时间
        orderMapper.updateStatus(OrderStatus, OrderPaidStatus, check_out_time, this.orders.getId());
        return vo;
    }

    /*
     * 支付成功,修改订单状态
     *
     * @param outTradeNo
     */
    public void paySuccess(String outTradeNo) {
            // 当前登录用户id
            Long userId = BaseContext.getCurrentId();

            // 根据订单号查询当前用户的订单
            Orders ordersDB = orderMapper.getByNumberAndUserId(outTradeNo, userId);

            // 根据订单id更新订单的状态、支付方式、支付状态、结账时间
            Orders orders = Orders.builder()
                    .id(ordersDB.getId())
                    .status(Orders.TO_BE_CONFIRMED)
                    .payStatus(Orders.PAID)
                    .checkoutTime(LocalDateTime.now())
                    .build();
            orderMapper.update(orders);
    }

注意:!!! 由于小程序支付需要商家注册,所以我把小程序调用微信支付接口进行支付相关代码注释掉,然后重新把订单状态进行修改,使其支付成功。或者,我们直接操作数据库,改变订单状态。

支付回调相关接口:

java 复制代码
/**
 * 支付回调相关接口
 */
@RestController
@RequestMapping("/notify")
@Slf4j
public class PayNotifyController {
    @Autowired
    private OrderService orderService;
    @Autowired
    private WeChatProperties weChatProperties;

    /**
     * 支付成功回调
     *
     * @param request
     */
    @RequestMapping("/paySuccess")
    public void paySuccessNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
        //读取数据
        String body = readData(request);
        log.info("支付成功回调:{}", body);

        //数据解密
        String plainText = decryptData(body);
        log.info("解密后的文本:{}", plainText);

        JSONObject jsonObject = JSON.parseObject(plainText);
        String outTradeNo = jsonObject.getString("out_trade_no");//商户平台订单号
        String transactionId = jsonObject.getString("transaction_id");//微信支付交易号

        log.info("商户平台订单号:{}", outTradeNo);
        log.info("微信支付交易号:{}", transactionId);

        //业务处理,修改订单状态、来单提醒
        orderService.paySuccess(outTradeNo);

        //给微信响应
        responseToWeixin(response);
    }

    /**
     * 读取数据
     *
     * @param request
     * @return
     * @throws Exception
     */
    private String readData(HttpServletRequest request) throws Exception {
        BufferedReader reader = request.getReader();
        StringBuilder result = new StringBuilder();
        String line = null;
        while ((line = reader.readLine()) != null) {
            if (result.length() > 0) {
                result.append("\n");
            }
            result.append(line);
        }
        return result.toString();
    }

    /**
     * 数据解密
     *
     * @param body
     * @return
     * @throws Exception
     */
    private String decryptData(String body) throws Exception {
        JSONObject resultObject = JSON.parseObject(body);
        JSONObject resource = resultObject.getJSONObject("resource");
        String ciphertext = resource.getString("ciphertext");
        String nonce = resource.getString("nonce");
        String associatedData = resource.getString("associated_data");

        AesUtil aesUtil = new AesUtil(weChatProperties.getApiV3Key().getBytes(StandardCharsets.UTF_8));
        //密文解密
        String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
                nonce.getBytes(StandardCharsets.UTF_8),
                ciphertext);

        return plainText;
    }

    /**
     * 给微信响应
     * @param response
     */
    private void responseToWeixin(HttpServletResponse response) throws Exception{
        response.setStatus(200);
        HashMap<Object, Object> map = new HashMap<>();
        map.put("code", "SUCCESS");
        map.put("message", "SUCCESS");
        response.setHeader("Content-type", ContentType.APPLICATION_JSON.toString());
        response.getOutputStream().write(JSONUtils.toJSONString(map).getBytes(StandardCharsets.UTF_8));
        response.flushBuffer();
    }
}
1.3 定时任务SpringTask
1.3.1介绍

Spring Task是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑

定位:定时任务框架 作用:定时自动执行某段Java代码

应用场景:

1). 信用卡每月还款提醒

2). 银行贷款每月还款提醒

3). 火车票售票系统处理未支付订单

4). 入职纪念日为用户发送通知

强调:只要是需要定时处理的场景都可以使用Spring Task

1.3.2 cron表达式
  • cron其实就是一个字符串,通过cron表达式可以定义任务触发的时间

  • 构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义:秒、分钟、小时、日、月、周、年(可选),每部分的含义如下表所示:


cron表达式在线生成器:在线Cron表达式生成器

1.3.3 开发步骤

1). 导入maven坐标 spring-context

2). 启动类添加注解 @EnableScheduling 开启任务调度

3). 自定义定时任务类

在本项目中,利用Spring Task技术主要是为了解决订单一直处于未支付状态user未下单、订单一直处于派送中状态商家未完成订单两个问题。

java 复制代码
/*
 *定时任务类,定时处理订单状态
 */
@Slf4j
@Component
public class OrderTask {
    @Autowired
    private OrderMapper orderMapper;

    /*
     *处理超时订单的方法
     */
    @Scheduled(cron = "0 * * * * ? ")//每分钟触发一次
    public void processTimeoutOrder(){
        log.info("定时处理超时订单:{}", LocalDateTime.now());
        //处理超时订单---select from orders where status=未支付 and  order_time >15min
        //time是现在时间的15分钟前
        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.setCancelReason("支付超时,自动取消");
                orders.setCancelTime(LocalDateTime.now());
                orderMapper.update(orders);
            }
        }
    }

    /*
     * 处理"派送中"状态的订单
     */
    @Scheduled(cron = "0 0 1 * * ?")//每天凌晨1点执行一次
    public void processDeliveryOrder(){
        log.info("处理"派送中"状态的订单:{}", LocalDateTime.now());
        //处理"派送中"状态的订单---可以利用getByStatusAndOrdertimeLT方法查询,查询的内容是零点之前
        LocalDateTime time=LocalDateTime.now().plusMinutes(-60);
        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);
            }
        }
    }
}
1.4 WebSocket全双工通信
1.4.1介绍

WebSocket 是基于 TCP 的一种新的网络协议 。它实现了浏览器与服务器全双工通信------浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性 的连接, 并进行双向数据传输。

HTTP协议和WebSocket协议对比:

  • HTTP是短连接

  • WebSocket是长连接

  • HTTP通信是单向 的,基于请求响应模式(一次请求--一次响应)

  • WebSocket支持双向通信

  • HTTP和WebSocket底层都是TCP连接

WebSocket缺点:

服务器长期维护长连接需要一定的成本 各个浏览器支持程度不一 WebSocket 是长连接,受网络限制比较大,需要处理好重连

结论: WebSocket并不能完全取代HTTP,它只适合在特定的场景下使用

WebSocket应用场景:

1). 直播弹幕

2). 网页聊天

3). 体育实况更新

4). 股票基金报价实时更新

1.4.2入门案例

下面通过一个入门案例来详细了解WebSocket

  • 需求:实现浏览器与服务器全双工通信。浏览器既可以向服务器发送消息,服务器也可主动向浏览器推送消息。

实现步骤:

1). 直接使用websocket.html页面作为WebSocket客户端

2). 导入WebSocket的maven坐标

3). 导入WebSocket服务端组件WebSocketServer,用于和客户端通信

4). 导入配置类WebSocketConfiguration,注册WebSocket的服务端组件

5). 导入定时任务类WebSocketTask,定时向客户端推送数据

1.4.2.1 定义websocket.html页面
html 复制代码
<!DOCTYPE HTML>
<html>
<head>
    <meta charset="UTF-8">
    <title>WebSocket Demo</title>
</head>
<body>
    <input id="text" type="text" />
    <button onclick="send()">发送消息</button>
    <button onclick="closeWebSocket()">关闭连接</button>
    <div id="message">
    </div>
</body>
<script type="text/javascript">
    
    function abc() {
        //code
    }
    
    var websocket = null;
    //Math.random()返回一个0(包含)到 1(不包含)的浮点数
    //toString(32): 转成字符串
    //substr(): 从指定位置开始截取字符串
    var clientId = Math.random().toString(36).substr(2);

    //判断当前浏览器是否支持WebSocket
    if('WebSocket' in window){
        //连接WebSocket节点
        websocket = new WebSocket("ws://localhost:8080/ws/"+clientId);
    }else{
        alert('Not support websocket')
    }

    //连接发生错误的回调方法
    websocket.onerror = function(){
        setMessageInnerHTML("error");
    };

    //连接成功建立的回调方法
    websocket.onopen = function(){
        setMessageInnerHTML("连接成功");
    }

    //接收到消息的回调方法
    websocket.onmessage = function(event){
        setMessageInnerHTML(event.data); //event.data是服务器返回的数据
    }

    //连接关闭的回调方法
    websocket.onclose = function(){
        setMessageInnerHTML("close");
    }

    //监听窗口关闭事件:
    //当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function(){
        websocket.close();
    }

    //将消息显示在网页上
    function setMessageInnerHTML(innerHTML){
        document.getElementById('message').innerHTML += innerHTML + '<br/>';
    }

    //发送消息
    function send(){
        var message = document.getElementById('text').value;
        websocket.send(message);
    }
	
	//关闭连接
    function closeWebSocket() {
        websocket.close();
    }
</script>
</html>
1.4.2.2 定义WebSocket服务端组件
java 复制代码
/*
 * 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();
            }
        }
    }
}    
1.4.2.3 定义配置类,注册WebSocket的服务端组件
java 复制代码
/*
 * WebSocket配置类,用于注册WebSocket的Bean
 */
@Configuration
public class WebSocketConfiguration {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
1.4.2.4 定义定时任务类,定时向客户端推送数据
java 复制代码
@Component
public class WebSocketTask {
    @Autowired
    private WebSocketServer webSocketServer;
    /*
     * 通过WebSocket每隔5秒向客户端发送消息
     */
    @Scheduled(cron = "0/5 * * * * ?")
    public void sendMessageToClient() {
        webSocketServer.sendToAllClient("这是来自服务端的消息:" + LocalTime.now());
    }
}
1.5 注册百度地图服务
1.5.1 注册百度地图服务
复制代码
1.基于百度地图开放平台实现(https://lbsyun.baidu.com/)

2.注册账号--->控制台--->我的应用-->创建应用获取AK(服务端应用)--->调用接口
创建应用时:
类型:选服务端
#IP白名单:0.0.0.0/0 

3.相关接口
地理编码服务:https://lbsyun.baidu.com/index.php?title=webapi/guide/webservice-geocoding

地理编码服务(又名Geocoder)是一类Web API接口服务;
地理编码服务提供将结构化地址数据(如:北京市海淀区上地十街十号)转换为对应坐标点(经纬度)功能;

GET: https://api.map.baidu.com/geocoding/v3/?address=北京市海淀区上地十街10号&output=json&ak=您的ak

在url中传递3个参数即可,返回数据格式如下:Java中将返回的json字符串转成JSONObject

XML 复制代码
{ // JSONObject
    "status": 0, // jsonObject.getIntValue("status")
    "result": { //对象: jsonObject.getJSONObject("result")
        "location": { //对象: jsonObject.getJSONObject("location") 
            "lng": 116.3076223267197, //经度 getString("lng")
            "lat": 40.05682848596073 //纬度 getString("lat")
        },
        "precise": 1,
        "confidence": 80,
        "comprehension": 100,
        "level": "门址"
    }
}

路线规划服务:轻量级路线规划 | 百度地图API SDK (baidu.com)

轻量级路线规划服务(又名DirectionLite API )是一套REST风格的Web服务API,以HTTP/HTTPS形式提供了路线规划服务。相较于Direction API,DirectionLite API更注重服务的高性能和接口的轻便简洁,满足基础的路线规划需求,并不具备Direciton API中的驾车多路线/未来出行和公交跨城规划等高级功能。DirectionLite API支持驾车、骑行、步行、公交路线规划,支持中国大陆地区。

传递4个参数即可--origin、destination、steps_info、ak,返回数据如下:

XML 复制代码
{
    "status": 0, //getIntValue
    "message": "ok",
    "result": { //对象: getJSONObject
        "origin": {
            "lng": 116.33929505188,
            "lat": 40.011157363344
        },
        "destination": {
            "lng": 116.45255341058,
            "lat": 39.936401378723
        },
        "routes": [ //数组: result.getJSONArray("routes");
            {
                "distance": 18129, //getString 得到的两地间的距离
                "duration": 6193
            }
        ]
    }
}

代码如下:

java 复制代码
/*
     * 功能优化
     * 优化用户下单功能,加入校验逻辑,如果用户的收货地址距离商家门店超出配送范围(配送范围为5公里内),则下单失败。
     */
    private void checkOutOfRange(String address){
        Map map = new HashMap();
        map.put("address",shopAddress);
        map.put("output","json");
        map.put("ak",ak);

        //获取店铺的经纬度坐标
        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("店铺地址解析失败");
        }

        //数据解析
        JSONObject location = jsonObject.getJSONObject("result").getJSONObject("location");
        String lat = location.getString("lat");
        String lng = location.getString("lng");
        //店铺经纬度坐标
        String shopLngLat = lat + "," + lng;

        map.put("address",address);
        //获取用户收货地址的经纬度坐标
        String userCoordinate = HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3", map);

        jsonObject = JSON.parseObject(userCoordinate);
        if(!jsonObject.getString("status").equals("0")){
            throw new OrderBusinessException("收货地址解析失败");
        }

        //数据解析
        location = jsonObject.getJSONObject("result").getJSONObject("location");
        lat = location.getString("lat");
        lng = location.getString("lng");
        //用户收货地址经纬度坐标
        String userLngLat = lat + "," + lng;

        map.put("origin",shopLngLat);
        map.put("destination",userLngLat);
        map.put("steps_info","0");

        //路线规划
        //从map集合中自动拿出4个参数进行请求访问---origin、destination、steps_info、ak
        String json = HttpClientUtil.doGet("https://api.map.baidu.com/directionlite/v1/driving", map);

        jsonObject = JSON.parseObject(json);
        if(!jsonObject.getString("status").equals("0")){
            throw new OrderBusinessException("配送路线规划失败");
        }

        //数据解析
        JSONObject result = jsonObject.getJSONObject("result");
        JSONArray jsonArray = (JSONArray) result.get("routes");
        Integer distance = (Integer) ((JSONObject) jsonArray.get(0)).get("distance");

        if(distance > 5000){
            //配送距离超过5000米
            throw new OrderBusinessException("超出配送范围");
        }
    }

最后在用户下单方法中调用checkOutOfRange方法来验证距离是否符合规定

java 复制代码
 //校验收货地址是否超出配送范围
        checkOutOfRange(addressBook.getCityName()+addressBook.getDistrictName()+addressBook.getDetail());
1.6 Apache POI
1.6.1介绍

Apache POI 是一个处理Miscrosoft Office各种文件格式的开源项目。简单来说就是,我们可以使用 POI 在 Java 程序中对Miscrosoft Office各种文件进行读写操作。 一般情况下,POI 都是用于操作 Excel 文件。

Apache POI 的应用场景:

1)银行网银系统导出交易明细

2)各种业务系统导出Excel报表

3)批量导入业务数据

注!!!:开发导出数据--Excel模块时,牢记怎么在Windows上操作excel,就怎么在代码开发中操作。

java 复制代码
/*
     * 导出运营数据报表--要下载到客户端浏览器--需要一个response输出流
     * @param response
     */
    public void exportBusinessData(HttpServletResponse response) {
        LocalDate begin = LocalDate.now().minusDays(30);
        LocalDate end = LocalDate.now().minusDays(1);
        //查询概览运营数据,提供给Excel模板文件
        BusinessDataVO businessData = workspaceService.getBusinessData(LocalDateTime.of(begin,LocalTime.MIN), LocalDateTime.of(end, LocalTime.MAX));
        InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模版.xlsx");
        try {
            //基于提供好的模板文件创建一个新的Excel表格对象
            XSSFWorkbook excel = new XSSFWorkbook(inputStream);
            //获得Excel文件中的一个Sheet页
            XSSFSheet sheet = excel.getSheet("Sheet1");

            sheet.getRow(1).getCell(1).setCellValue(begin + "至" + end);
            //获得第4行
            XSSFRow row = sheet.getRow(3);
            //获取单元格
            row.getCell(2).setCellValue(businessData.getTurnover());
            row.getCell(4).setCellValue(businessData.getOrderCompletionRate());
            row.getCell(6).setCellValue(businessData.getNewUsers());
            //获取第5行
            row = sheet.getRow(4);
            row.getCell(2).setCellValue(businessData.getValidOrderCount());
            row.getCell(4).setCellValue(businessData.getUnitPrice());

            for (int i = 0; i < 30; i++) {
                LocalDate date = begin.plusDays(i);
                //准备明细数据
                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());
            }
            //通过输出流将文件下载到客户端浏览器中
            ServletOutputStream out = response.getOutputStream();
            excel.write(out);
            //关闭资源
            out.flush();
            out.close();
            excel.close();

        }catch (IOException e){
            e.printStackTrace();
        }
    }
相关推荐
缺点内向3 小时前
Java:创建、读取或更新 Excel 文档
java·excel
带刺的坐椅4 小时前
Solon v3.4.7, v3.5.6, v3.6.1 发布(国产优秀应用开发框架)
java·spring·solon
四谎真好看5 小时前
Java 黑马程序员学习笔记(进阶篇18)
java·笔记·学习·学习笔记
桦说编程5 小时前
深入解析CompletableFuture源码实现(2)———双源输入
java·后端·源码
java_t_t5 小时前
ZIP工具类
java·zip
lang201509286 小时前
Spring Boot优雅关闭全解析
java·spring boot·后端
pengzhuofan6 小时前
第10章 Maven
java·maven
百锦再7 小时前
Vue Scoped样式混淆问题详解与解决方案
java·前端·javascript·数据库·vue.js·学习·.net
刘一说7 小时前
Spring Boot 启动慢?启动过程深度解析与优化策略
java·spring boot·后端
壹佰大多7 小时前
【spring如何扫描一个路径下被注解修饰的类】
java·后端·spring