一、卖家端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) 设置响应的状态码。
- getOutputStream和getWriter方法的区别
getOutputStream和getWriter方法分别用于得到输出二进制数据、输出文本数据的ServletOuputStream、Printwriter对象。getOutputStream和getWriter这两个方法互相排斥,调用了其中的任何一个方法后,就不能再调用另一方法。
这两个方法写入的数据会作为响应消息的正文,与响应状态行和各响应头组合后输出到客户端。Serlvet的service方法结束后,web容器将检查getWriter或getOutputStream方法返回的输出流对象是否已经调用过close方法,如果没有,web容器将调用close方法关闭该输出流对象。
- 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(“您有新的订单”);