订单单点登录功能实现
导入shop-sso依赖
开启@EnableDubbo
配置yml文件中的dubbo服务
提供应用信息和zookeeper地址
拦截器和配置拦截器类
因为订单这个系统是没有登录页面的,所以我们拦截器拦截这个订单系统的时候,如果他的ticket为空或者过期那么就跳回到前台系统的登录页面,因为这个是跨系统的,所以我们重定向的时候需要一个完整的路径,那么我们需要拿到前台系统的url,前台系统的url可以在yml文件中配置,通过@Value("${shop.portal.url}")
去获取。
在拦截器中我们根据ticket拦截,如果没有登录则去登录,登录成功后重定向到已经登录过后才可以访问的页面。
代码:Important!!!
单点登录失败,从订单系统重定向前台系统
response.sendRedirect(portalUrl+"login?redirectUrl="+request.getRequestURL());
这是一个JavaWeb中的重定向代码。response.sendRedirect()
是重定向到指定的URL,参数portalUrl是重定向的URL基础地址,login是登录页面的URL,redirectUrl是登录成功后要重定向回来的页面的URL。request.getRequestURL()
是获取当前请求的URL,这里将其作为参数传递给了重定向URL,登录成功后重回当前页面。其中redirectUrl参数可以是任何需要登录后才能访问的页面,例如用户个人中心、订单页面等等。
具体实现
当用户的ticket在订单系统没有通过验证,被拦截器拦截了,就需要跳转到portal系统的login方法那里,传进去一个redirectUrl参数,存到request域中,跳转到登录页面,登录页面有一个隐藏域来存放这个redirectUrl,如果是重定向来的,会给这个隐藏域赋值,反之不赋值,登录页面填写信息之后点击登录,发送请求,如果redirectUrl中有值,重定向到redirectUrl中的地址,反之,重定向到portal首页。注意这里需要重新去写这个login方法,不能用portal里面未登录进入登录页面,登录成功的去向的Controller,因为这里虽然是去了portal的登录页面,但登录成功之后是重定向了原先请求的页面,需要去向不同,我们应该重新进行一个编写。
代码
配置拦截器类:
/**
* MVC配置类
*
* @author zhoubin
* @since 1.0.0
*/
@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private OrderLoginInterceptor loginInterceptor;
@Autowired
private OrderCommonInterceptor commonInterceptor;
/**
* addInterceptor:添加自定义拦截器
* addPathPatterns:添加拦截请求 /**表示拦截所有
* excludePathPatterns:不拦截的请求
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(commonInterceptor)
.addPathPatterns("/**");
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/static/**")
.excludePathPatterns("/login/**")
.excludePathPatterns("/image/**")
.excludePathPatterns("/user/login/**")
.excludePathPatterns("/user/logout/**");
}
/**
* 放行静态资源
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
}
}
拦截器三种方法:
@Component
public class OrderLoginInterceptor implements HandlerInterceptor {
@Reference(interfaceClass = ShopSSOService.class)
private ShopSSOService ssoService;
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Value("${user.ticket}")
private String userTicket;
@Value("${shop.portal.url}")
private String portalUrl;
/**
* 请求处理的方法之前执行
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取用户票据
String ticket = CookieUtil.getCookieValue(request, "userTicket");
if (!StringUtils.isEmpty(ticket)){
//如果票据存在,进行验证
Admin admin = ssoService.validate(ticket);
//将用户信息存入session中,用于页面返显
request.getSession().setAttribute("user",admin);
//重新设置失效时间
ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
valueOperations.set(userTicket+":"+ticket, JsonUtil.object2JsonStr(admin),30, TimeUnit.MINUTES);
return true;
}
//票据不存在或者用户验证失败,重定向至登录页面
response.sendRedirect(portalUrl+"login?redirectUrl="+request.getRequestURL());
return false;
}
/**
* 请求处理的方法之后执行
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
/**
* 处理后执行清理工作
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) throws Exception {
}
}
在portal前台页面中编写login方法
@RequestMapping("login")
public String login(String redirectUrl, Model model){
model.addAttribute("redirectUrl",redirectUrl);
return "login";
}
前台!''
表示如果获取到的值为空,则将其转换为空字符串。
<input type="hidden" id="redirectUrl" value="${redirectUrl!''}"/>
<script type="text/javascript">
// 用户登录
function userLogin() {
$.ajax({
url: "${ctx}/user/login",
type: "POST",
data: $("#formlogin").serialize(),
dataType: "JSON",
success: function (result) {
if (200 == result.code) {
// 如果存在重定向url则重定向至该url
if ($("#redirectUrl").val()) {
location.href = $("#redirectUrl").val();
}else {
location.href = "${ctx}/index";
}
} else {
layer.msg("用户名或密码错误,请重新输入!");
}
},
error: function () {
layer.alert("亲,系统正在升级中,请稍后再试!");
}
});
}
</script>
点击去结算跳转到预订单页面,说明为什么需要定义一个拦截器?@Value?
去结算的那个页面是在前台系统的商品列表页面中,点击按钮之后跳转到订单系统的预订单页面。因此我们把订单系统的url放到前台系统的yml中,通过@Value("${shop.order.url}")
获取订单系统的完整的url
注意这里的小坑
@Value注解是spring容器中的,我们无法在Controller层也就是springmvc容器中获取key中的value值。
原因是controller注册在spring-mvc.xml代表的SpringMVC的容器中,而service则注册在applicationContext.xml代表的Spring的容器中。如果要使用@Value注解,需要在对应的容器中进行注册。
解决方法:我们可以把我们的url放到我们的一个最大的作用域application
,通过拦截器去拿,因为拦截器会使用@Component
注解,这样我们就可以将@Value注解注册到spring容器中了。然后将url放到application域中。
但是我们这个拦截器会不拦截请求,但是每一个请求他都会将url存入到我们的application域中,会造成一个频繁的写操作,因此我们要进行判断,如果这个url已经存过了,那么就不需要再存了。
配置拦截器类
@Autowired
private PortalCommonInterceptor commonInterceptor;
/**
* addInterceptor:添加自定义拦截器
* addPathPatterns:添加拦截请求 /**表示拦截所有
* excludePathPatterns:不拦截的请求
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(commonInterceptor)
.addPathPatterns("/**");
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/cart/**")
.excludePathPatterns("/static/**")
.excludePathPatterns("/login/**")
.excludePathPatterns("/user/login/**")
.excludePathPatterns("/user/logout/**");
}
实现HanlerInterceptor接口
@Component
public class PortalCommonInterceptor implements HandlerInterceptor {
@Value("${shop.order.url}")
private String shopOrderUrl;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取application对象
ServletContext servletContext = request.getSession().getServletContext();
String orderUrl = (String) servletContext.getAttribute("orderUrl");
if (StringUtils.isEmpty(orderUrl)){
servletContext.setAttribute("orderUrl",shopOrderUrl);
}
return true;
}
}
接下来就是实现从前台系统跳转到订单系统了。
此时portal的全局域内有orderUrl,我们点击去结算按钮走重定向的Controller,到达订单系统之后我们走订单系统的Controller,去预订单页面。
前台系统的Controller
/**
* 跳转订单系统
* @return
*/
@RequestMapping("/toPreOrder")
public String toPreOrder(HttpServletRequest request){
//获取ServletContext上下文中的值
String orderUrl = (String) request.getSession().getServletContext().getAttribute("orderUrl");
//重定向
return "redirect:"+orderUrl+"order/preOrder";
}
订单系统的Controller
/**
* 跳转到预订单页面
*
* @return
*/
@RequestMapping("/preOrder")
public String preOrder(Model model, HttpServletRequest request) {
Admin admin = (Admin) request.getSession().getAttribute("user");
model.addAttribute("cartResult", cartService.getCartList(admin));
return "order/preOrder";
}
到达预订单系统应当展示购物车列表和总金额
**怎么获取?**购物车列表和总金额可以调用rpc服务中的CartService获取。要调用这个方法必须要获取用户信息,查看哪个用户的购物车信息。因为在拦截器里面登录的用户信息存放在session中,因此我们从session中获取用户信息。
@RequestMapping("/preOrder")
public String preOrder(Model model, HttpServletRequest request) {
Admin admin = (Admin) request.getSession().getAttribute("user");
model.addAttribute("cartResult", cartService.getCartList(admin));
return "order/preOrder";
}
公共的页面展示出购物车的数量,调用rpc的CatService
@Controller
@RequestMapping("/cart")
public class CartController {
@Reference(interfaceClass = CartService.class)
private CartService cartService;
/**
* 获取购物车数量
* @return
*/
@RequestMapping("/getCartNum")
@ResponseBody
public Integer getCartNum(HttpServletRequest request){
Admin admin = (Admin) request.getSession().getAttribute("user");
return cartService.getCartNum(admin);
}
}
提交订单
点击提交按钮,前端传过来的有CartResult(购物车列表,总金额),
点击提交订单之后,有三件事情需要完成
1、跳转到订单页面
2、删除购物车中的数据
3、将提交的数据变道数据库里面去,为了后续的查看全部支部订单。
返回的是否成功,因为需要渲染页面,把订单和总金额传入前台。
生成订单
要把订单存入到数据库中首先要先生成订单。
通过mybatis-plus进行生成。
有关订单的数据库有t_order,t_order_goods,生成pojo,mapper,xml文件。
new一个order,属性赋值,然后插入到数据库中,如果插入成功,则获取CarResult中List的每一个元素,让他转换为OrderGoods,设置订单商品中的订单号为订单编码,与Order关联起来,然后将OrderGoods放到一个List集合中,插入数据库中,如果插入成功,并未出现异常,则返回成功,否则返回失败,因为Controller需要一个订单编号,所以我们可以将返回的成功的信息里面放入我们的订单编号,在Controller就可以进行存入了。
在这里生成的订单的状态信息会用一个枚举类来进行定义。
注意这里的订单编号是唯一的,我们用的是redis的自增key
@Override
public BaseResult orderSave(Admin admin, CartResult cartResult) {
//创建order对象
Order order = new Order();
//订单编号 shop_年月日时分秒_自增key
String orderSn = "shop_"+ DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now())+"_"+getIncrement(redisOrderIncrement);
order.setOrderSn(orderSn);
//用户id
order.setUserId(admin.getAdminId().intValue());
//订单状态(未确认)
order.setOrderStatus(OrderStatus.no_confirm.getStatus());
//发货状态(未发货)
order.setShippingStatus(SendStatus.no_pay.getStatus());
//支付状态(未支付)
order.setPayStatus(PayStatus.no_pay.getStatus());
//商品总价
order.setGoodsPrice(cartResult.getTotalPrice());
//应付金额
order.setOrderAmount(cartResult.getTotalPrice());
//订单总价
order.setTotalAmount(cartResult.getTotalPrice());
//订单时间
Long addTime = LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8"));
order.setAddTime(addTime.intValue());
int result = orderMapper.insertSelective(order);
//存储成功
if (result>0){
List<OrderGoods> orderGoodsList = new ArrayList<>();
for (CartVo cartVo : cartResult.getCartList()) {
//创建orderGoods对象
OrderGoods orderGoods = new OrderGoods();
//订单id
orderGoods.setOrderId(order.getOrderId());
//商品id
orderGoods.setGoodsId(cartVo.getGoodsId());
//商品名称
orderGoods.setGoodsName(cartVo.getGoodsName());
//商品价格
orderGoods.setGoodsPrice(cartVo.getMarketPrice());
//商品数量
orderGoods.setGoodsNum(cartVo.getGoodsNum().shortValue());
//订单方式
orderGoods.setPromType(PromTypeStatus.normal.getStatus());
//发货状态
orderGoods.setIsSend(SendStatus.no_pay.getStatus());
//添加到订单商品对象列表
orderGoodsList.add(orderGoods);
}
//批量插入
result = orderGoodsMapper.insertOrderGoodsBatch(orderGoodsList);
if (result>0){
BaseResult baseResult = BaseResult.success();
baseResult.setMessage(orderSn);
return baseResult;
}
}
return BaseResult.error();
}
/**
* redis自增key
* @param key
* @return
*/
private Long getIncrement(String key){
RedisAtomicLong entityIdCounter = new RedisAtomicLong(key,redisTemplate.getConnectionFactory());
return entityIdCounter.getAndIncrement();
}
}
跳转到提交页面
1、生成订单
2、清空购物车
3、跳转页面
@RequestMapping("/submitOrder")
public String submitOrder(Model model, HttpServletRequest request) {
Admin admin = (Admin) request.getSession().getAttribute("user");
CartResult cartResult = cartService.getCartList(admin);
//1.存入订单信息
BaseResult baseResult = orderService.orderSave(admin, cartResult);
//2.清除购物车信息
cartService.clearCart(admin);
//总价
model.addAttribute("totalPrice", cartResult.getTotalPrice());
//订单编号
model.addAttribute("orderSn", baseResult.getMessage());
//3.页面跳转
return "/order/submitOrder";
}
订单系统和前台系统的交互
订单系统可以在搜索框中进行搜索,搜索跳转到前台系统。
搜索进入前台页面
@Controller
@RequestMapping("search")
public class SearchController {
/**
* 跳转搜索页面,传过去一个searchStr
* @param request
* @param searchStr
* @param model
* @return
*/
@RequestMapping("index")
public String index(HttpServletRequest request, String searchStr, Model model){
try {
//对输入的内容进行编码,防止中文乱码
searchStr = URLEncoder.encode(searchStr,"UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return "redirect:"+request.getSession().getServletContext().getAttribute("portalUrl")+"search/index?searchStr="+searchStr;
}
}
在订单系统点击登出:
重定向,从应用域中获取数据request.getSession().getServletContext().getAttribute()
@RequestMapping("/index")
public String index(HttpServletRequest request, String searchStr, Model model){
try {
//对输入的内容进行编码,防止中文乱码
searchStr = URLEncoder.encode(searchStr,"UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return "redirect:"+request.getSession().getServletContext().getAttribute("portalUrl")+"search/index?searchStr="+searchStr;
}
}