(十三)点餐项目记录-买家与卖家端连通

一、卖家端Service层的开发

上篇文章已经把卖家信息表dao层开发完毕,并且创建完成接口,接下来实现第二次Service层的开发,首先创建SellerService接口。

// An highlighted block
public interface SellerService {
    /**
     * 查询卖家信息
     * @param openid
     * @return
     */
    SellerInfo findSellerInfoByOpenid(String openid);
}

然后实现这个接口SellerServiceImpl

@Service
public class SellerServiceImpl implements SellerService {
    @Autowired
    private SellerInfoRepository repository;

    public SellerInfo findSellerInfoByOpenid(String openid){
        return repository.findByOpenid(openid);
    }
}

二、登陆

1.获取卖家openid,这是在微信开放平台获取的,由于这里我们没有企业资格,所以这里的openid我们设定为一个固定值

2.设置登录页面

3.创建SellerUserController类,里面两个方法,登录和登出

// An highlighted block
@Controller
@RequestMapping("/seller")
public class SellerUserController {

    @Autowired
    private SellerService sellerService;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private ProjectUrlConfig projectUrlConfig;

    @GetMapping("/login")
    public ModelAndView login(@RequestParam("openid") String openid,
                              HttpServletResponse response,
                              Map<String, Object> map) {

        //1. openid去和数据库里的数据匹配
        SellerInfo sellerInfo = sellerService.findSellerInfoByOpenid(openid);
        if (sellerInfo == null) {
            map.put("msg", ResultEnum.LOGIN_FAIL.getMessage());
            map.put("url", "/sell/seller/order/list");
            return new ModelAndView("common/error");
        }

        //2. 设置token至redis
        String token = UUID.randomUUID().toString();
        Integer expire = RedisConstant.EXPIRE;

        redisTemplate.opsForValue().set(String.format(RedisConstant.TOKEN_PREFIX, token), openid, expire, TimeUnit.SECONDS);

        //3. 设置token至cookie
        CookieUtil.set(response, CookieConstant.TOKEN, token, expire);

        return new ModelAndView("redirect:" + projectUrlConfig.getSell() + "/sell/seller/order/list");

    }

    @GetMapping("/logout")
    public ModelAndView logout(HttpServletRequest request,
                       HttpServletResponse response,
                       Map<String, Object> map) {
        //1. 从cookie里查询
        Cookie cookie = CookieUtil.get(request, CookieConstant.TOKEN);
        if (cookie != null) {
            //2. 清除redis
            redisTemplate.opsForValue().getOperations().delete(String.format(RedisConstant.TOKEN_PREFIX, cookie.getValue()));

            //3. 清除cookie
            CookieUtil.set(response, CookieConstant.TOKEN, null, 0);
        }

        map.put("msg", ResultEnum.LOGOUT_SUCCESS.getMessage());
        map.put("url", "/sell/seller/order/list");
        return new ModelAndView("common/success", map);
    }
}

4.下载Redis Desktop Manager 可视化工具
在这里插入图片描述
5.引入redis依赖,配置redis

6.设置一个redis常量,创建包constant,创建接口RedisConstant

7.设置openid到cookie,创建一个cookie工具类,保存和获取cookie,设置cookie常量

这里补充一下我对Token,cookies,session的理解
1.tooken是服务端生成的一串字符串(相当于身份证号),作为客户端进行请求的一个令牌,(用于登陆验证身份的凭证)。
2.cookies是记录你访问网页的记录
3.session与cookie功能效果相同。Session与Cookie的区别在于Session是记录在服务端的,而Cookie是记录在客户端的。

解释session:当访问服务器否个网页的时候,会在服务器端的内存里开辟一块内存,这块内存就叫做session,而这个内存是跟浏览器关联在一起的。这个浏览器指的是浏览器窗口,或者是浏览器的子窗口,意思就是,只允许当前这个session对应的浏览器访问,就算是在同一个机器上新启的浏览器也是无法访问的。而另外一个浏览器也需要记录session的话,就会再启一个属于自己的session

原理:HTTP协议是非连接性的,取完当前浏览器的内容,然后关闭浏览器后,链接就断开了,而没有任何机制去记录取出后的信息。而当需要访问同一个网站的另外一个页面时(就好比如在第一个页面选择购买的商品后,跳转到第二个页面去进行付款)这个时候取出来的信息,就读不出来了。所以必须要有一种机制让页面知道原理页面的session内容。

问题:如何知道浏览器和这个服务器中的session是一一对应的呢?又如何保证不会去访问其它的session呢?

原理解答:就是当访问一个页面的时候给浏览器创建一个独一无二的号码,也给同时创建的session赋予同样的号码。这样就可以在打开同一个网站的第二个页面时获取到第一个页面中session保留下来的对应信息(理解:当访问第二个页面时将号码同时传递到第二个页面。找到对应的session。)。这个号码也叫sessionID,session的ID号码,session的独一无二号码。

session的两种实现方式(也就是传递方式):第一种通过cookies实现。第二种通过URL重写来实现

第一种方式的理解:就是把session的id 放在cookie里面(为什么是使用cookies存放呢,因为cookie有临时的,也有定时的,临时的就是当前浏览器什么时候关掉即消失,也就是说session本来就是当浏览器关闭即消失的,所以可以用临时的cookie存放。保存再cookie里的sessionID一定不会重复,因为是独一无二的。),当允许浏览器使用cookie的时候,session就会依赖于cookies,当浏览器不支持cookie后,就可以通过第二种方式获取session内存中的数据资源。

第二种方式的理解:在客户端不支持cookie的情况下使用。为了以防万一,也可以同时使用。

如果不支持cookie,必须自己编程使用URL重写的方式实现。

如何重写URL:通过response.encodeURL()方法

encodeURL()的两个作用

**第一个作用:**转码(说明:转中文的编码,或者一些其他特殊的编码。就好比如网页的链接中存在中文字符,就会转换成为一些百分号或者其他的符号代替。)

**第二个作用:**URL后面加入sessionID,当不支持cookie的时候,可以使用encodeURL()方法,encodeUTL()后面跟上sessionID,这样的话,在禁用cookie的浏览器中同时也可以使用session了。但是需要自己编程,只要链接支持,想用session就必须加上encodeURL()。

**提示:**若想程序中永远支持session,那就必须加上encodeURL(),当别人禁用了cookie,一样可以使用session。

简单的代码例子:在没有使用encodeURL()方法前的代码

在这里插入图片描述

在使用encodeURL()方法后的代码

在这里插入图片描述

看下图,当重写URL 的时候,每一次访问的时候都会将sessionID传过来,传过来了,就没有必要再在cookie里读了。

在这里插入图片描述

规则:

如果浏览器支持cookie,创建session多大的时候,会被sessionID保存再cookie里。只要允许cookie,session就不会改变,如果不允许使用cookie,每刷新一次浏览器就会换一个session(因为浏览器以为这是一个新的链接)
如果不支持cookie,必须自己编程使用URL重写的方式实现session
Session不像cookie一样拥有路径访问的问题,同一个application下的servlet/jsp都可以共享同一个session,前提下是同一个客户端窗口。

在登陆的过程中,根据token这个name查到其对用的value,也就是一串字符串,然后再把这个value作为key在redis中进行查找,查到对应的value,也就是openid。如果有这个id,就说明用户已经登陆了。

8.创建ProductUrlConfig类,获取配置文件中的路径

9.登陆接口”/sell/seller/login”,先把拿到的openid和数据库匹配,

然后把用户的token设置到redis,下次验证方便。

然后把token设置到cookies中,也就是本地也记录下来。

最后再跳转到登陆成功的页面。

@PostMapping("/login")
public ModelAndView login(@RequestParam("openid") String openid,
                          Map<String, Object> map,
                          HttpServletResponse response) {
    //1.openid和数据库匹配
    SellerInfo sellerInfo = sellerService.findSellerInfoByOpenid(openid);
    if (sellerInfo == null) {
        map.put("msg", ResultEnum.LOGIN_FAIL.getMessage());
        map.put("url", "/sell/seller/toLogin");
        return new ModelAndView("common/error", map);
    }
    //2.设置token到redis中
    String token = UUID.randomUUID().toString();
    Integer expire = RedisConstant.EXPORE;
    redisTemplate.opsForValue().set(String.format(RedisConstant.TOKEN_PREFIX, token, openid), openid, expire, TimeUnit.SECONDS);
    //3.设置token到cookie
    CookieUtil.set(response, CookieConstant.TOKEN, token, CookieConstant.EXPORE);

    //页面跳转
    return new ModelAndView("redirect:" + projectUrlConfig.getSell() + "/sell/seller/order/list");
}

退出

退出接口”/sell/seller/logout”,

这里补充一点:HttpServletRequest request和HttpServletResponse response。这两个对象,一个是请求是前端发起的向后端,一个是响应是后端响应前端

一.HttpServletRequest对象代表客户端的请求,当客户端通过HTTP协议访问服务器时,HTTP请求头中的所有信息都封装在这个对象中。

request常用的方法和操作:

1.获得客户端的信息
getRequestURL方法返回客户端发出请求时的完整URL。

getRequestURI方法返回请求行中的资源名部分,去掉主机名的部分。

getRemoteAddr方法返回发出请求的客户机的IP地址

getRemoteHost方法返回发出请求的客户机的完整主机名

getRemotePort方法返回客户机所使用的端口号

getLocalAddr方法返回WEB服务器的IP地址。

getLocalName方法返回WEB服务器的主机名

getMethod得到客户机请求方式,如GET,POST

2.获得请求头的一些方法
getHead(name)方法

getHeaders(String name)方法

getHeaderNames方法

3.获得请求参数,也就是客户端提交的数据的一些方法。
getParameter(name)方法

getParameterValues(String name)方法

getParameterNames方法

getParameterMap方法

4.HttpServletRequest实现转发
请求转发指一个web资源收到客户端请求后,通知服务器去调用另外一个web资源进行处理。request对象提供了一个getRequestDispatcher方法,该方法返回一个RequestDispatcher对象,调用这个对象的forward方法可以实现请求转发。
5. request域
request对象同时也是一个域对象,我们通过request对象在实现转发时,可以把数据通过request对象带给其它web资源处理。下面是常用的一些对域中的属性的操作的方法:

setAttribute方法

getAttribute方法

removeAttribute方法

getAttributeNames方法
6. Request的getParameter和getAttribute方法的区别。

由于request也是一个域对象,所以既可以从它获得参数,即Parameter。也可以获得域中的属性。但是他们的意义是完全不一样的。

getParameter(String name)获得客户端传送给服务器的参数值,该参数是由name指定的,通常是表单中的参数。而且参数只能是字符串形式的键值对。

getAttribute(String name):返回有name 指定的属性值,如果指定的属性值不存在,则会返回一个null值。这里存放的也是一个键值对,不同的是,这里的值可以是任意的类型。

二.HttpServletResponse则是对服务器的响应对象。这个对象中封装了向客户端发送数据、发送响应头,发送响应状态码的方法。

response常用的方法和操作:

1.常用的方法
addCookie(Cookie cookie) 向客户端写入Cookie

addHeader(Java.lang.String name, java.lang.String value) 写入给定的响应头

encodeURL(java.lang.Stringurl) 默认cookie中包含Session ID,如果客户端不支持 Cookie,就在参数 url 中加入 Session ID 信息,可以解决用户禁用cookie的问题。

setStatus(intsc) 设置响应的状态码。

  1. getOutputStream和getWriter方法的区别
    getOutputStream和getWriter方法分别用于得到输出二进制数据、输出文本数据的ServletOuputStream、Printwriter对象。getOutputStream和getWriter这两个方法互相排斥,调用了其中的任何一个方法后,就不能再调用另一方法。

这两个方法写入的数据会作为响应消息的正文,与响应状态行和各响应头组合后输出到客户端。Serlvet的service方法结束后,web容器将检查getWriter或getOutputStream方法返回的输出流对象是否已经调用过close方法,如果没有,web容器将调用close方法关闭该输出流对象。

  1. HttpServletResponse实现重定向
    重定向指的是一个web资源收到客户端请求后,web服务器通知客户端去访问另外一个web资源,这称之为请求重定向。实现方式是调用response.sendRedirect()方法。实现的原理就是给客户端返回了302状态码和location头。

三.转发forward和重定向Redirect的区别

转发是在服务器端实现的。一个web资源收到客户端请求后,通知服务器去调用另外一个web资源进行处理,称之为请求转发。调用RequestDispatcher.forward 方法的请求转发过程结束后,浏览器地址栏保持初始的URL地址不变。

而重定向是在客户端实现的。一个web资源收到客户端请求后,通知客户端的浏览器去访问另外一个web资源,称之为请求重定向。所以调用HttpServletResponse.sendRedirect方法重定向的访问过程结束后,浏览器地址栏中显示的URL会发生改变,由初始的URL地址变成重定向的目标URL。

RequestDispatcher.forward方法只能将请求转发给同一个WEB应用中的其他资源; sendRedirect方法还可以重定向到同一个站点上的其他应用程序中的资源,甚至是使用绝对URL重定向到其他站点的资源。

HttpServletResponse.sendRedirect方法对浏览器的请求直接作出响应,响应的结果就是告诉浏览器去重新发出对另外一个URL的访问请求;RequestDispatcher.forward方法在服务器端将请求转发给另外一个资源,相当过程于对客户端不可见。

RequestDispatcher.forward方法的调用者与被调用者之间共享相同的request对象和response对象,它们属于同一个访问请求和响应过程;而HttpServletResponse.sendRedirect方法调用者与被调用者使用各自的request对象和response对象,它们属于两个独立的访问请求和响应过程。也就是说,重定向生成了新的request对象和response对象。

https://blog.csdn.net/ethan_10/article/details/80700848

在登出的过程中,首先获取到cookies。然后再注销cookies。然后从redis中清除。

@GetMapping("/logout")
public ModelAndView logout(HttpServletRequest request,
                           HttpServletResponse response,
                           Map<String, Object> map) {
    //1.从cookie里面查询
    Cookie cookie = CookieUtil.get(request, CookieConstant.TOKEN);
    if (cookie != null) {
        //2.清除redis
        redisTemplate.opsForValue().getOperations().delete((String.format(RedisConstant.TOKEN_PREFIX, cookie.getValue())));
        //3.清除cookie,设置过期时间为0
        CookieUtil.set(response, CookieConstant.TOKEN, null, 0);
    }
    map.put("msg",ResultEnum.LOGOUT_SUCCESS.getMessage());
    map.put("url","/sell/seller/toLogin");
    return new ModelAndView("common/success",map);
}

AOP实现身份验证
1.创建SellerAuthorizeAspect类,设置拦截点以及操作,有问题则抛出异常

@Aspect
@Component
@Slf4j
public class SellerAuthorizeAspect {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Pointcut("execution(public * com.xiong.sell.controller.Seller*.*(..))" +
            " && !execution(public * com.xiong.sell.controller.SellerUserController.*(..))")
    public void verify() {
    }

    @Before("verify()")
    public void doVerify() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        //查询cookie
        Cookie cookie = CookieUtil.get(request, CookieConstant.TOKEN);
        if (cookie == null) {
            log.warn("【登录校验】 cookie中没有token");
            throw new SellerAuthorizeException();
        }
        //查询redis
        String tokenValue = redisTemplate.opsForValue().get(String.format(RedisConstant.TOKEN_PREFIX, cookie.getValue()));
        if(StringUtils.isEmpty(tokenValue)){
            log.warn("【登录校验】 redis中没有token");
            throw new SellerAuthorizeException();
        }
    }
}

2.创建SellerAuthorizeException类

public class SellerAuthorizeException extends RuntimeException {
}

3.拦截SellerAuthorizeException异常并且给出操作

@ControllerAdvice
public class SellExceptionHandler {

    @Autowired
    private ProjectUrlConfig projectUrlConfig;

    /**
     * 拦截登录异常
     * @return
     */
    @ExceptionHandler(value = SellerAuthorizeException.class)
    public ModelAndView handlerAuthorizeException(){
        return new ModelAndView("redirect:" + projectUrlConfig.getSell() + "/sell/seller/toLogin");
    }
} 

微信模板消息推送

1.创建PushMessage接口service

public interface PushMessage {

    void orderStatus(OrderDTO orderDTO);
}

2.实现PushMassage接口

@Service
@Slf4j
public class PushMessageImpl implements PushMessage {

    @Autowired
    private WxMpService wxMpService;

    @Override
    public void orderStatus(OrderDTO orderDTO) {
        WxMpTemplateMessage templateMessage = new WxMpTemplateMessage();
        templateMessage.setTemplateId("sBkdCQcYxaVaIlhQ2wGuejjr_K1I0Rv2HVCZHIaNXdg");
        templateMessage.setToUser("oIe231KOhNAGPWEIsE52bdPBA910");
        List<WxMpTemplateData> data = new ArrayList<>();
        data.add(new WxMpTemplateData("first","这是标题"));
        data.add(new WxMpTemplateData("keyword1",String.valueOf(orderDTO.getBuyerOpenid())));
        data.add(new WxMpTemplateData("remark","这是结尾"));
        templateMessage.setData(data);
        try{
            wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage);
        }catch (WxErrorException e){
            log.info("【微信模板消息】发送失败,{}",e);
        }
    }
}

3.取消订单则方法调用这个推送

WebSocket接收并且推送新订单消息

1.引入依赖

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

2.修改order/list.ftl页面的Script

<html>
<#include "../common/header.ftl">

<body>
<div id="wrapper" class="toggled">
    <#--边栏sidebar-->
    <#include "../common/nav.ftl">
    <#--主要内容content-->
    <div id="page-content-wrapper">
        <div class="container-fluid">
            <div class="row clearfix">
                <div class="col-md-12 column">
                    <table class="table table-condensed table-hover table-bordered">
                        <thead>
                        <tr>
                            <th>订单id</th>
                            <th>姓名</th>
                            <th>手机号</th>
                            <th>地址</th>
                            <th>金额</th>
                            <th>订单状态</th>
                            <th>支付状态</th>
                            <th>创建时间</th>
                            <th colspan="2">操作</th>
                        </tr>
                        </thead>
                        <tbody>
                        <#list orderDTOPage.content as orderDTO>
                            <tr>
                                <td>${orderDTO.orderId}</td>
                                <td>${orderDTO.buyerName}</td>
                                <td>${orderDTO.buyerPhone}</td>
                                <td>${orderDTO.buyerAddress}</td>
                                <td>${orderDTO.orderAmount}</td>
                                <td>${orderDTO.orderStatusEnum.message}</td>
                                <td>${orderDTO.payStatusEnum.message}</td>
                                <td>${orderDTO.createTime}</td>
                                <td><a href="/sell/seller/order/detail?orderId=${orderDTO.orderId}">详情</a></td>
                                <td>
                                    <#if orderDTO.orderStatusEnum.message == "新订单">
                                        <a href="/sell/seller/order/cancel?orderId=${orderDTO.orderId}">取消</a>
                                    </#if>
                                </td>
                            </tr>
                        </#list>
                        </tbody>
                    </table>
                </div>
                <div class="col-md-12 column">
                    <ul class="pagination pull-right">
                        <#--上一页 小于1则无法显示上一页-->
                        <#if currentPage lte 1>
                            <li class="disabled"><a href="#">上一页</a></li>
                        <#else >
                            <li><a href="/sell/seller/order/list?page=${currentPage-1}&size=${size}">上一页</a></li>
                        </#if>
                        <#list 1..orderDTOPage.totalPages as index>
                            <#if currentPage == index>
                                <li class="disabled"><a href="#">${index}</a></li>
                            <#else>
                                <li><a href="/sell/seller/order/list?page=${index}&size=${size}">${index}</a></li>
                            </#if>
                        </#list>
                        <#--下一页 大于orderDTOPage.totalPages则无法显示下一页-->
                        <#if currentPage gte orderDTOPage.totalPages>
                            <li class="disabled"><a href="#">下一页</a></li>
                        <#else >
                            <li><a href="/sell/seller/order/list?page=${currentPage+1}&size=${size}">下一页</a></li>
                        </#if>
                    </ul>
                </div>
            </div>
        </div>
    </div>
</div>
<#--弹窗-->
<div class="modal fade" id="myModal" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
                <h4 class="modal-title" id="myModalLabel">
                    提醒
                </h4>
            </div>
            <div class="modal-body">
                你有新的订单
            </div>
            <div class="modal-footer">
                <button onclick="javascript:document.getElementById('notice').pause()" type="button" class="btn btn-default" data-dismiss="modal">关闭</button>
                <button onclick="location.reload()" type="button" class="btn btn-primary">查看新的订单</button>
            </div>
        </div>
    </div>
</div>

<#--播放音乐-->
<audio id="notice" loop="loop">
    <source src="/sell/mp3/song.mp3" type="audio/mpeg" />
</audio>

<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>

<script>
    var websocket = null;
    if('WebSocket' in window) {
        websocket = new WebSocket('ws://localhost:8080/sell/webSocket');
    }else {
        alert('该浏览器不支持websocket!');
    }

    websocket.onopen = function (event) {
        console.log('建立连接');
    }

    websocket.onclose = function (event) {
        console.log('连接关闭');
    }

    websocket.onmessage = function (event) {
        console.log('收到消息:' + event.data)
        //弹窗提醒,
        $('#myModal').modal('show');
        // 播放音乐
        document.getElementById('notice').play();
    }

    websocket.onerror = function () {
        alert('websocket通信发生错误!');
    }

    window.onbeforeunload = function () {
        websocket.close();
    }

</script>
</body>
</html>

3.添加websocket配置

// An highlighted block
@Component
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

4.建立websocket连接

// An highlighted block
@Component
@ServerEndpoint("/webSocket")
@Slf4j
public class WebSocket {

    private Session session;

    private static CopyOnWriteArraySet<WebSocket> webSocketSet = new CopyOnWriteArraySet<>();

    @OnOpen
    public void onOpen(Session session) {
        this.session = session;
        webSocketSet.add(this);
        log.info("【websocket消息】有新的连接, 总数:{}", webSocketSet.size());
    }

    @OnClose
    public void onClose() {
        webSocketSet.remove(this);
        log.info("【websocket消息】连接断开, 总数:{}", webSocketSet.size());
    }

    @OnMessage
    public void onMessage(String message) {
        log.info("【websocket消息】收到客户端发来的消息:{}", message);
    }

    public void sendMessage(String message) {
        for (WebSocket webSocket: webSocketSet) {
            log.info("【websocket消息】广播消息, message={}", message);
            try {
                webSocket.session.getBasicRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

5.推送消息订单创建的service方法中插入下面一句话

webSocket.sendMessage(“您有新的订单”);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值