Browse Source

超时关单业务完成

wangzhengliang 3 years ago
parent
commit
cc56d88618

+ 6 - 0
pom.xml

@@ -92,6 +92,12 @@
             <artifactId>spring-boot-starter-cache</artifactId>
             <artifactId>spring-boot-starter-cache</artifactId>
         </dependency>
         </dependency>
 
 
+        <!--AMQP-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-amqp</artifactId>
+        </dependency>
+
         <dependency>
         <dependency>
             <groupId>mysql</groupId>
             <groupId>mysql</groupId>
             <artifactId>mysql-connector-java</artifactId>
             <artifactId>mysql-connector-java</artifactId>

+ 2 - 0
src/main/java/com/chuanghai/ihotel/common/exception/BizCodeEnume.java

@@ -28,7 +28,9 @@ public enum BizCodeEnume {
     PARAMETER_ERROR(11003, "参数异常"),
     PARAMETER_ERROR(11003, "参数异常"),
     REPEAT_SUBMIT(11004, "重复提交表单"),
     REPEAT_SUBMIT(11004, "重复提交表单"),
     ORDER_SUBMIT_ERROR(11005, "订单提交失败"),
     ORDER_SUBMIT_ERROR(11005, "订单提交失败"),
+    MQ_CONSUME_EXCEPTION(11006,"消费者消费异常"),
     THIRD_PARTY_SERVICE_CALL_FAILED(11007, "第三方服务调用失败"),
     THIRD_PARTY_SERVICE_CALL_FAILED(11007, "第三方服务调用失败"),
+    JXNXS_ORDER_QUERY_ERROR(11008, "农商行订单状态查询失败"),
     DATA_IS_EXIST(16000, "数据已存在"),
     DATA_IS_EXIST(16000, "数据已存在"),
     DATA_NOT_EXIST(16000, "数据不存在"),
     DATA_NOT_EXIST(16000, "数据不存在"),
     FILE_IS_NOT_EXIT(16006, "临时文件不存在"),
     FILE_IS_NOT_EXIT(16006, "临时文件不存在"),

+ 133 - 0
src/main/java/com/chuanghai/ihotel/component/PayComponent.java

@@ -0,0 +1,133 @@
+package com.chuanghai.ihotel.component;
+
+import com.chuanghai.ihotel.common.exception.BizCodeEnume;
+import com.chuanghai.ihotel.common.exception.RRException;
+import com.chuanghai.ihotel.config.JXNXSPayConfig;
+import com.chuanghai.ihotel.service.HotelOrderService;
+import com.chuanghai.ihotel.util.JXNXSPayUtil;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.RestTemplate;
+
+import javax.servlet.http.HttpServletRequest;
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * @Author: codingliang
+ * @Description: 支付组件
+ * @Date: 2022-07-29 15:11
+ * @Version: V1.0
+ **/
+@Slf4j
+@Component
+public class PayComponent {
+
+    @Autowired
+    private JXNXSPayConfig jxnxsPayConfig;
+    @Autowired
+    private HotelOrderService hotelOrderService;
+
+    /**
+     * 支异步结果通知
+     * @param request
+     * @return
+     */
+    public boolean notify(HttpServletRequest request) {
+        Map<String, String> params = new HashMap<>();
+        Enumeration<String> parameterNames = request.getParameterNames();
+        while (parameterNames.hasMoreElements()) {
+            String parameterName = parameterNames.nextElement();
+            params.put(parameterName, request.getParameter(parameterName));
+        }
+
+        ObjectMapper mapper = new ObjectMapper();
+        try {
+            log.info("江西农商行支付异步回调【{}】", mapper.writeValueAsString(params));
+        } catch (JsonProcessingException e) {}
+
+        // 验签
+        Boolean verifySign = JXNXSPayUtil.verifySign(params, jxnxsPayConfig.getOpenKey());
+
+        // {"ord_no":"798465321471","out_no":"798465321471",
+        // "rand_str":"","sign":"","pay_time":"",
+        // "status":"1","timestamp":"1629255539", "amount": ""}
+        boolean isHandleSuccess = false;
+        if (verifySign) {
+            String orderNo = params.get("out_no");
+            String status = params.get("status");
+            String amount = params.get("amount"); // 分
+
+            if ("1".equals(status)) {
+                isHandleSuccess = true;
+                String realPayAmount = new BigDecimal(amount).divide(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_UP).toString(); // 分转成元
+                hotelOrderService.orderPaySuccess(null, orderNo, realPayAmount, null);
+            }
+        }
+
+        return isHandleSuccess;
+    }
+
+    /**
+     * 支付结果查询
+     * @param orderNo 订单编号
+     * @return
+     */
+    public Map<String, String> queryOrderStatus(String orderNo) {
+        try {
+            // 构建请求参数
+            Map<String, String> postParam = new LinkedHashMap<>();
+            postParam.put("open_id", jxnxsPayConfig.getOpenId());
+            postParam.put("timestamp", new Date().getTime() / 1000 + "");
+            Map<String, String> dataMap = new LinkedHashMap<>();    // data参数
+            dataMap.put("out_no", orderNo);
+            dataMap.put("ord_no", "");
+            ObjectMapper mapper = new ObjectMapper();
+            String data = JXNXSPayUtil.handleEncrypt(mapper.writeValueAsString(dataMap), jxnxsPayConfig.getOpenKey());
+            postParam.put("data", data);
+            postParam.put("open_key", jxnxsPayConfig.getOpenKey());
+            String sign = JXNXSPayUtil.sign(postParam);
+            postParam.put("sign", sign);
+
+            MultiValueMap<String,String> params = new LinkedMultiValueMap<>();
+            params.setAll(postParam);
+            // 发送请求
+            HttpHeaders headers = new HttpHeaders();
+            MediaType type = MediaType.parseMediaType("application/x-www-form-urlencoded");
+            headers.setContentType(type);
+
+            HttpEntity<MultiValueMap<String,String>> formEntity = new HttpEntity<>(params, headers);
+
+            RestTemplate client = new RestTemplate();
+            String url = "https://api.jxnxs.com/mct1/paystatus";
+
+            ResponseEntity<String> resp = client.postForEntity(url, formEntity, String.class);
+            String body = resp.getBody();
+
+            Map<String, String> map = mapper.readValue(body, new TypeReference<HashMap<String, String>>() {});
+            if (JXNXSPayUtil.verifySign(map, jxnxsPayConfig.getOpenKey())) {
+                // data 解密
+                String reposData = JXNXSPayUtil.decrypt(map.get("data"), jxnxsPayConfig.getOpenKey());
+                return mapper.readValue(reposData, new TypeReference<HashMap<String, String>>() {});
+            } else {
+                return map;
+            }
+        } catch (Exception e) {
+            throw new RRException(BizCodeEnume.JXNXS_ORDER_QUERY_ERROR);
+        }
+    }
+}

+ 114 - 0
src/main/java/com/chuanghai/ihotel/config/RabbitMQConfig.java

@@ -0,0 +1,114 @@
+package com.chuanghai.ihotel.config;
+
+import com.chuanghai.ihotel.constant.TimeConstant;
+import lombok.Data;
+import org.springframework.amqp.core.Binding;
+import org.springframework.amqp.core.Exchange;
+import org.springframework.amqp.core.Queue;
+import org.springframework.amqp.core.TopicExchange;
+import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
+import org.springframework.amqp.support.converter.MessageConverter;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ *  自定义消息队列配置
+ *  发送 关单消息-》延迟exchange-》order.close.delay.queue-》死信exchange-》order.close.queue
+ **/
+@Configuration
+@Data
+public class RabbitMQConfig {
+
+    /**
+     * 交换机
+     */
+    private String orderEventExchange="order.event.exchange";
+
+    /**
+     * 延迟队列
+     */
+    private String orderCloseDelayQueue = "order.close.delay.queue";
+
+    /**
+     * 关单队列
+     */
+    private String orderCloseQueue = "order.close.queue";
+
+    /**
+     * 进入到延迟队列的routingKey
+     */
+    private String orderCloseDelayRoutingKey = "order.close.delay.routing.key";
+
+    /**
+     * 进入死信队列的routingKey,消息过期进入死信队列的key
+     */
+    private String orderCloseRoutingKey = "order.close.delay.key";
+
+    /**
+     * 过期时间,毫秒单位,临时改为1分钟过期
+     */
+    private Integer ttl = TimeConstant.ORDER_PAY_TIMEOUT_MILLS;
+
+    /**
+     * 消息转换器
+     * @return
+     */
+    @Bean
+    public MessageConverter messageConverter(){
+        return new Jackson2JsonMessageConverter();
+    }
+
+    /**
+     * 创建交换机,topic类型,一般一个业务一个交换机
+     * @return
+     */
+    @Bean
+    public Exchange orderEventExchange(){
+        return new TopicExchange(orderEventExchange,true,false);
+    }
+
+    /**
+     * 延迟队列
+     * @return
+     */
+    @Bean
+    public Queue orderCloseDelayQueue(){
+        Map<String,Object> args = new HashMap<>(3);
+        args.put("x-dead-letter-exchange", orderEventExchange);
+        args.put("x-dead-letter-routing-key", orderCloseRoutingKey);
+        args.put("x-message-ttl", ttl);
+        return new Queue(orderCloseDelayQueue,true,false,false,args);
+    }
+
+    /**
+     * 死信队列
+     * @return
+     */
+    @Bean
+    public Queue orderCloseQueue(){
+        return new Queue(orderCloseQueue,true,false,false);
+    }
+
+    /**
+     * 延迟队列和交换机建立绑定关系
+     * @return
+     */
+    @Bean
+    public Binding orderCloseDelayBinding(){
+        return new Binding(orderCloseDelayQueue,
+                Binding.DestinationType.QUEUE,orderEventExchange,orderCloseDelayRoutingKey,null);
+    }
+
+    /**
+     * 死信队列和死信交换机绑定关系
+     * @return
+     */
+    @Bean
+    public Binding orderCloseBinding(){
+        return new Binding(orderCloseQueue,
+                Binding.DestinationType.QUEUE,orderEventExchange,orderCloseRoutingKey,null);
+    }
+}

+ 82 - 0
src/main/java/com/chuanghai/ihotel/config/RabbitMQErrorConfig.java

@@ -0,0 +1,82 @@
+package com.chuanghai.ihotel.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.amqp.core.Binding;
+import org.springframework.amqp.core.BindingBuilder;
+import org.springframework.amqp.core.Queue;
+import org.springframework.amqp.core.TopicExchange;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.amqp.rabbit.retry.MessageRecoverer;
+import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 重试队列
+ **/
+@Configuration
+@Slf4j
+public class RabbitMQErrorConfig {
+
+    /**
+     * 异常交换机
+     */
+    private String orderErrorExchange = "order.error.exchange";
+
+    /**
+     * 异常队列
+     */
+    private String orderErrorQueue = "order.error.queue";
+
+    /**
+     * 异常routing.key
+     */
+    private String orderErrorRoutingKey = "order.error.routing.key";
+
+
+    @Autowired
+    private RabbitTemplate rabbitTemplate;
+
+    /**
+     * 创建异常交换机
+     * @return
+     */
+    @Bean
+    public TopicExchange errorTopicExchange(){
+        return new TopicExchange(orderErrorExchange,true,false);
+    }
+
+    /**
+     * 创建异常队列
+     * @return
+     */
+
+    @Bean
+    public Queue errorQueue(){
+        return new Queue(orderErrorQueue,true);
+    }
+
+    /**
+     * 建立绑定关系
+     * @return
+     */
+    @Bean
+    public Binding bindingErrorQueueAndExchange(){
+
+        return BindingBuilder.bind(errorQueue()).to(errorTopicExchange()).with(orderErrorRoutingKey);
+    }
+
+
+    /**
+     * 配置  RepublishMessageRecoverer
+     *
+     * 消费消息重试一定次数后,用特定的routingKey转发到指定的交换机中,方便后续排查和告警
+     *
+     * @return
+     */
+    @Bean
+    public MessageRecoverer messageRecoverer(){
+        return new RepublishMessageRecoverer(rabbitTemplate,orderErrorExchange,orderErrorRoutingKey);
+    }
+}

+ 6 - 2
src/main/java/com/chuanghai/ihotel/constant/TimeConstant.java

@@ -1,14 +1,18 @@
 package com.chuanghai.ihotel.constant;
 package com.chuanghai.ihotel.constant;
 
 
+import java.time.format.DateTimeFormatter;
+
 /**
 /**
  * 时间常量
  * 时间常量
  */
  */
 public class TimeConstant {
 public class TimeConstant {
 
 
+    public static final DateTimeFormatter DEFAULT_DTF = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
     /**
     /**
-     * 默认30分钟超时未支付,单位毫秒
+     * 默认10分钟超时未支付,单位毫秒
      */
      */
-    public static final long ORDER_PAY_TIMEOUT_MILLS =1000 * 60 * 30;
+    public static final int ORDER_PAY_TIMEOUT_MILLS = 1000 * 60 * 10;
 
 
     /**
     /**
      * 最早到店时间
      * 最早到店时间

+ 56 - 0
src/main/java/com/chuanghai/ihotel/dto/EventMessageDTO.java

@@ -0,0 +1,56 @@
+package com.chuanghai.ihotel.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * @Author: codingliang
+ * @Description: 事件消息
+ * @Date: 2022-07-29 14:51
+ * @Version: V1.0
+ **/
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class EventMessageDTO implements Serializable {
+    private static final long serialVersionUID = 3279055428067063169L;
+
+    /**
+     * 消息队列的消息id
+     */
+    private String messageId;
+
+
+    /**
+     * 事件类型
+     */
+    private String eventMessageType;
+
+
+    /**
+     * 业务id
+     */
+    private Long bizId;
+
+
+    /**
+     * 账号
+     */
+    private String accountNo;
+
+
+    /**
+     * 消息体
+     */
+    private String content;
+
+    /**
+     * 备注
+     */
+    private String remark;
+}

+ 14 - 0
src/main/java/com/chuanghai/ihotel/enums/EventMessageTypeEnum.java

@@ -0,0 +1,14 @@
+package com.chuanghai.ihotel.enums;
+
+/**
+ * @Author: codingliang
+ * @Description: 消息类型
+ * @Date: 2022-07-29 14:54
+ * @Version: V1.0
+ **/
+public enum EventMessageTypeEnum {
+    /**
+     * 新建订单
+     */
+    PRODUCT_ORDER_NEW
+}

+ 39 - 0
src/main/java/com/chuanghai/ihotel/listener/ProductOrderMQListener.java

@@ -0,0 +1,39 @@
+package com.chuanghai.ihotel.listener;
+
+import com.chuanghai.ihotel.common.exception.BizCodeEnume;
+import com.chuanghai.ihotel.common.exception.RRException;
+import com.chuanghai.ihotel.dto.EventMessageDTO;
+import com.chuanghai.ihotel.service.HotelOrderService;
+import com.rabbitmq.client.Channel;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.amqp.core.Message;
+import org.springframework.amqp.rabbit.annotation.Queue;
+import org.springframework.amqp.rabbit.annotation.RabbitHandler;
+import org.springframework.amqp.rabbit.annotation.RabbitListener;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * 消息监听
+ **/
+@Component
+@Slf4j
+@RabbitListener(queuesToDeclare = {@Queue("order.close.queue")})
+public class ProductOrderMQListener {
+
+    @Autowired
+    private HotelOrderService hotelOrderService;
+
+
+    @RabbitHandler
+    public void productOrderHandler(EventMessageDTO eventMessageDTO, Message message, Channel channel){
+        log.info("监听到消息ProductOrderMQListener message消息内容:{}",message);
+        try{
+            hotelOrderService.handleOrderMessage(eventMessageDTO);
+        }catch (Exception e){
+            log.error("消费者失败:{}",eventMessageDTO);
+            throw new RRException(BizCodeEnume.MQ_CONSUME_EXCEPTION);
+        }
+        log.info("消费成功:{}", eventMessageDTO);
+    }
+}

+ 33 - 0
src/main/java/com/chuanghai/ihotel/service/HotelOrderService.java

@@ -5,10 +5,13 @@ import com.chuanghai.ihotel.common.utils.PageUtils;
 import com.chuanghai.ihotel.common.utils.PageParam;
 import com.chuanghai.ihotel.common.utils.PageParam;
 import com.chuanghai.ihotel.controller.request.ConfrimOrderRequest;
 import com.chuanghai.ihotel.controller.request.ConfrimOrderRequest;
 import com.chuanghai.ihotel.controller.request.SubmitOrderRequest;
 import com.chuanghai.ihotel.controller.request.SubmitOrderRequest;
+import com.chuanghai.ihotel.dto.EventMessageDTO;
 import com.chuanghai.ihotel.entity.HotelOrderEntity;
 import com.chuanghai.ihotel.entity.HotelOrderEntity;
 import com.chuanghai.ihotel.vo.ConfirmOrderVO;
 import com.chuanghai.ihotel.vo.ConfirmOrderVO;
 import com.chuanghai.ihotel.vo.OrderSubmitVO;
 import com.chuanghai.ihotel.vo.OrderSubmitVO;
 
 
+import java.time.LocalDateTime;
+
 /**
 /**
  * 酒店订单 
  * 酒店订单 
  *
  *
@@ -20,6 +23,8 @@ public interface HotelOrderService extends IService<HotelOrderEntity> {
 
 
     PageUtils queryPage(PageParam pageParam);
     PageUtils queryPage(PageParam pageParam);
 
 
+    HotelOrderEntity findByOrderNo(String orderNo);
+
     /**
     /**
      * 确认订单
      * 确认订单
      * @param request
      * @param request
@@ -28,11 +33,39 @@ public interface HotelOrderService extends IService<HotelOrderEntity> {
     ConfirmOrderVO confirmOrder(ConfrimOrderRequest request);
     ConfirmOrderVO confirmOrder(ConfrimOrderRequest request);
 
 
     /**
     /**
+     * 订单支付成功
+     * @param orderId 订单id
+     * @param orderNo 订单号
+     * @param realPayAmount 支付金额
+     * @param payTime 支付时间
+     */
+    void orderPaySuccess(Long orderId, String orderNo, String realPayAmount, LocalDateTime payTime);
+
+    /**
      * 提交订单
      * 提交订单
      * @param userToken 用户token
      * @param userToken 用户token
      * @param request 请求参数
      * @param request 请求参数
      * @return
      * @return
      */
      */
     OrderSubmitVO submitOrder(String userToken, SubmitOrderRequest request);
     OrderSubmitVO submitOrder(String userToken, SubmitOrderRequest request);
+
+    /**
+     * 处理订单消息
+     * @param eventMessageDTO 消息体
+     */
+    void handleOrderMessage(EventMessageDTO eventMessageDTO);
+
+    /**
+     * 关闭订单
+     * @param eventMessageDTO
+     */
+    void closeOrder(EventMessageDTO eventMessageDTO);
+
+    /**
+     * 取消订单
+     * @param userFlag 用户标识
+     * @param orderId 订单id
+     */
+    boolean cancelOrder(String userFlag, Long orderId);
 }
 }
 
 

+ 6 - 0
src/main/java/com/chuanghai/ihotel/service/RoomRealtimeStatuService.java

@@ -37,5 +37,11 @@ public interface RoomRealtimeStatuService extends IService<RoomRealtimeStatuEnti
      * @return 锁定的房间id
      * @return 锁定的房间id
      */
      */
     Long lockRoomByRoomTypeId(LockRoomDTO lockRoomDTO);
     Long lockRoomByRoomTypeId(LockRoomDTO lockRoomDTO);
+
+    /**
+     * 房态锁定释放
+     * @param bizId 业务id
+     */
+    void releaseByBizId(Long bizId);
 }
 }
 
 

+ 131 - 2
src/main/java/com/chuanghai/ihotel/service/impl/HotelOrderServiceImpl.java

@@ -3,15 +3,19 @@ package com.chuanghai.ihotel.service.impl;
 import com.baomidou.mybatisplus.core.toolkit.IdWorker;
 import com.baomidou.mybatisplus.core.toolkit.IdWorker;
 import com.chuanghai.ihotel.common.exception.BizCodeEnume;
 import com.chuanghai.ihotel.common.exception.BizCodeEnume;
 import com.chuanghai.ihotel.common.exception.RRException;
 import com.chuanghai.ihotel.common.exception.RRException;
+import com.chuanghai.ihotel.component.PayComponent;
 import com.chuanghai.ihotel.config.JXNXSPayConfig;
 import com.chuanghai.ihotel.config.JXNXSPayConfig;
+import com.chuanghai.ihotel.config.RabbitMQConfig;
 import com.chuanghai.ihotel.constant.TimeConstant;
 import com.chuanghai.ihotel.constant.TimeConstant;
 import com.chuanghai.ihotel.controller.request.ConfrimOrderRequest;
 import com.chuanghai.ihotel.controller.request.ConfrimOrderRequest;
 import com.chuanghai.ihotel.controller.request.SubmitOrderRequest;
 import com.chuanghai.ihotel.controller.request.SubmitOrderRequest;
+import com.chuanghai.ihotel.dto.EventMessageDTO;
 import com.chuanghai.ihotel.dto.LockRoomDTO;
 import com.chuanghai.ihotel.dto.LockRoomDTO;
 import com.chuanghai.ihotel.entity.HotelUserEntity;
 import com.chuanghai.ihotel.entity.HotelUserEntity;
 import com.chuanghai.ihotel.entity.RoomEntity;
 import com.chuanghai.ihotel.entity.RoomEntity;
 import com.chuanghai.ihotel.entity.RoomTypeEntity;
 import com.chuanghai.ihotel.entity.RoomTypeEntity;
 import com.chuanghai.ihotel.entity.SystemSettingEntity;
 import com.chuanghai.ihotel.entity.SystemSettingEntity;
+import com.chuanghai.ihotel.enums.EventMessageTypeEnum;
 import com.chuanghai.ihotel.enums.OrderBillStatuEnum;
 import com.chuanghai.ihotel.enums.OrderBillStatuEnum;
 import com.chuanghai.ihotel.enums.OrderStatuEnum;
 import com.chuanghai.ihotel.enums.OrderStatuEnum;
 import com.chuanghai.ihotel.enums.UserIdentityTypeEnum;
 import com.chuanghai.ihotel.enums.UserIdentityTypeEnum;
@@ -25,9 +29,12 @@ import com.chuanghai.ihotel.util.CommonUtil;
 import com.chuanghai.ihotel.vo.ConfirmOrderVO;
 import com.chuanghai.ihotel.vo.ConfirmOrderVO;
 import com.chuanghai.ihotel.vo.LoginUserVO;
 import com.chuanghai.ihotel.vo.LoginUserVO;
 import com.chuanghai.ihotel.vo.OrderSubmitVO;
 import com.chuanghai.ihotel.vo.OrderSubmitVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Component;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.core.metadata.IPage;
@@ -40,6 +47,8 @@ import com.chuanghai.ihotel.dao.HotelOrderDao;
 import com.chuanghai.ihotel.entity.HotelOrderEntity;
 import com.chuanghai.ihotel.entity.HotelOrderEntity;
 import com.chuanghai.ihotel.service.HotelOrderService;
 import com.chuanghai.ihotel.service.HotelOrderService;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+import sun.plugin2.message.EventMessage;
 
 
 import javax.validation.constraints.Min;
 import javax.validation.constraints.Min;
 import javax.validation.constraints.NotNull;
 import javax.validation.constraints.NotNull;
@@ -47,8 +56,10 @@ import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalDateTime;
 import java.time.temporal.ChronoUnit;
 import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalAccessor;
+import java.util.Map;
 
 
-
+@Slf4j
 @Service("hotelOrderService")
 @Service("hotelOrderService")
 public class HotelOrderServiceImpl extends ServiceImpl<HotelOrderDao, HotelOrderEntity> implements HotelOrderService {
 public class HotelOrderServiceImpl extends ServiceImpl<HotelOrderDao, HotelOrderEntity> implements HotelOrderService {
 
 
@@ -63,7 +74,13 @@ public class HotelOrderServiceImpl extends ServiceImpl<HotelOrderDao, HotelOrder
     @Autowired
     @Autowired
     private RoomService roomService;
     private RoomService roomService;
     @Autowired
     @Autowired
+    private PayComponent payComponent;
+    @Autowired
     private JXNXSPayConfig jxnxsPayConfig;
     private JXNXSPayConfig jxnxsPayConfig;
+    @Autowired
+    private RabbitTemplate rabbitTemplate;
+    @Autowired
+    private RabbitMQConfig rabbitMQConfig;
 
 
     @Override
     @Override
     public PageUtils queryPage(PageParam pageParam) {
     public PageUtils queryPage(PageParam pageParam) {
@@ -76,6 +93,14 @@ public class HotelOrderServiceImpl extends ServiceImpl<HotelOrderDao, HotelOrder
     }
     }
 
 
     @Override
     @Override
+    public HotelOrderEntity findByOrderNo(String orderNo) {
+        QueryWrapper<HotelOrderEntity> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("order_no", orderNo);
+        queryWrapper.last("limit 1");
+        return this.getOne(queryWrapper);
+    }
+
+    @Override
     public ConfirmOrderVO confirmOrder(ConfrimOrderRequest request) {
     public ConfirmOrderVO confirmOrder(ConfrimOrderRequest request) {
         SystemSettingEntity systemSetting = systemSettingService.get();
         SystemSettingEntity systemSetting = systemSettingService.get();
 
 
@@ -128,6 +153,36 @@ public class HotelOrderServiceImpl extends ServiceImpl<HotelOrderDao, HotelOrder
         return vo;
         return vo;
     }
     }
 
 
+    @Override
+    public void orderPaySuccess(Long orderId, String orderNo, String realPayAmount, LocalDateTime payTime) {
+        HotelOrderEntity order = null;
+        if (orderId != null) {
+            order = this.getById(orderId);
+        } else {
+            if (StringUtils.hasText(orderNo)) {
+                order = this.findByOrderNo(orderNo);
+            }
+        }
+
+        if (order == null) {
+            log.warn("修改订单状态:订单【{}】不存在", orderId);
+            return;
+        }
+        if (OrderStatuEnum.WAIT_PAY.getCode().equalsIgnoreCase(order.getOrderStatu())) {
+            if (Math.abs(order.getPayAmount().subtract(new BigDecimal(realPayAmount)).doubleValue()) < 0.01) {
+                // 订单最终支付金额验证成功
+                if (payTime == null) {
+                    order.setPayTime(LocalDateTime.now());
+                } else {
+                    order.setPayTime(payTime);
+                }
+
+                order.setOrderStatu(OrderStatuEnum.FINISH_PAY.getCode());
+                this.updateById(order);
+            }
+        }
+    }
+
     @Transactional
     @Transactional
     @Override
     @Override
     public OrderSubmitVO submitOrder(String userToken, SubmitOrderRequest request) {
     public OrderSubmitVO submitOrder(String userToken, SubmitOrderRequest request) {
@@ -199,10 +254,84 @@ public class HotelOrderServiceImpl extends ServiceImpl<HotelOrderDao, HotelOrder
         hotelOrder.setDeleteFlag("1"); // 0删除、1正常
         hotelOrder.setDeleteFlag("1"); // 0删除、1正常
         this.save(hotelOrder);
         this.save(hotelOrder);
 
 
-        // 发送mq信息 TODO
+        // 发送延迟消息
+        EventMessageDTO eventMessage = EventMessageDTO.builder()
+                .eventMessageType(EventMessageTypeEnum.PRODUCT_ORDER_NEW.name())
+                .accountNo(loginUserVO.getCardNumber())
+                .bizId(orderId)
+                .build();
 
 
+        rabbitTemplate.convertAndSend(rabbitMQConfig.getOrderEventExchange(), rabbitMQConfig.getOrderCloseDelayRoutingKey(), eventMessage);
+
+        // 返回支付参数
         String payUrl = String.format(jxnxsPayConfig.getPayUrl(), hotelOrder.getOrderNo(), request.getPayAmount());
         String payUrl = String.format(jxnxsPayConfig.getPayUrl(), hotelOrder.getOrderNo(), request.getPayAmount());
         return OrderSubmitVO.builder().orderId(hotelOrder.getId()).payUrl(payUrl).build();
         return OrderSubmitVO.builder().orderId(hotelOrder.getId()).payUrl(payUrl).build();
     }
     }
 
 
+    @Override
+    public void handleOrderMessage(EventMessageDTO eventMessageDTO) {
+        String messageType = eventMessageDTO.getEventMessageType();
+        try {
+            if(EventMessageTypeEnum.PRODUCT_ORDER_NEW.name().equalsIgnoreCase(messageType)) {
+                // 关单消息
+                this.closeOrder(eventMessageDTO);
+            }
+        } catch (Exception e) {
+            log.error("订单消费者消费失败:{}", eventMessageDTO);
+            throw new RRException(BizCodeEnume.MQ_CONSUME_EXCEPTION);
+        }
+    }
+
+    @Override
+    public void closeOrder(EventMessageDTO eventMessageDTO) {
+        Long orderId = eventMessageDTO.getBizId();
+        String cardNumber = eventMessageDTO.getAccountNo();
+        HotelOrderEntity orderEntity = this.getById(orderId);
+        if (orderEntity == null) {
+            log.warn("关单:订单【{}】不存在", orderId);
+            return;
+        }
+
+        // 判断订单状态
+        if (!orderEntity.getOrderStatu().equalsIgnoreCase(OrderStatuEnum.WAIT_PAY.getCode())) {
+            log.info("关单:订单不是初始状态待支付:{}", eventMessageDTO);
+            return;
+        }
+
+        // 未支付,向第三方支付平台查询状态
+        Map<String, String> map = payComponent.queryOrderStatus(orderEntity.getOrderNo());
+        log.info("关单:订单支付状态查询结果,【{}】", map);
+
+        String payStatus = map.get("status");
+        if ("1".equalsIgnoreCase(payStatus)) { // 已经完成支付
+            String amount = map.get("trade_amount");
+            String payTimeStr = map.get("trade_pay_time");
+            LocalDateTime payTime = LocalDateTime.parse(payTimeStr, TimeConstant.DEFAULT_DTF);
+            String realPayAmount = new BigDecimal(amount).divide(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_UP).toString(); // 分转成元
+            this.orderPaySuccess(orderId, orderEntity.getOrderNo(), realPayAmount, payTime);
+        } else { // 未支付,取消订单
+            this.cancelOrder(cardNumber, orderId);
+        }
+    }
+
+    @Override
+    public boolean cancelOrder(String userFlag, Long orderId) {
+        HotelOrderEntity orderEntity = this.getById(orderId);
+        if (orderEntity == null) {
+            log.warn("取消订单:订单【{}】不存在", orderId);
+            return false;
+        }
+        if (!orderEntity.getUserFlag().equalsIgnoreCase(userFlag)) {
+            log.warn("取消订单:操作越权,非法取消订单,订单原用户【】,操作用户【】", orderEntity.getUserFlag(), userFlag);
+            throw new RRException(BizCodeEnume.PERMISSION_DENIED, "不能取消非自己的订单");
+        }
+
+        orderEntity.setOrderStatu(OrderStatuEnum.CANCEL.getCode());
+        this.updateById(orderEntity);
+
+        // 释放库存
+        roomRealtimeStatuService.releaseByBizId(orderEntity.getId());
+        return true;
+    }
+
 }
 }

+ 12 - 0
src/main/java/com/chuanghai/ihotel/service/impl/RoomRealtimeStatuServiceImpl.java

@@ -93,6 +93,18 @@ public class RoomRealtimeStatuServiceImpl extends ServiceImpl<RoomRealtimeStatuD
         }
         }
     }
     }
 
 
+    @Override
+    public void releaseByBizId(Long bizId) {
+        QueryWrapper<RoomRealtimeStatuEntity> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("biz_id", bizId);
+        queryWrapper.last("limit 1");
+
+        RoomRealtimeStatuEntity one = this.getOne(queryWrapper);
+        if (one != null) {
+            this.removeById(one);
+        }
+    }
+
     /**
     /**
      * 随机根据房型选房
      * 随机根据房型选房
      * @param lockRoomDTO 锁房参数
      * @param lockRoomDTO 锁房参数

+ 163 - 0
src/main/java/com/chuanghai/ihotel/util/JXNXSPayUtil.java

@@ -0,0 +1,163 @@
+package com.chuanghai.ihotel.util;
+
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.util.StringUtils;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.SecretKeySpec;
+import java.security.MessageDigest;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Set;
+
+
+/**
+ * 农商行支付工具类
+ *
+ * Description
+ * Create 2017-03-07 14:01:23
+ *
+ * @author Benny.YEE
+ */
+public class JXNXSPayUtil {
+
+    public static String SHA1(String decript) {
+        try {
+            MessageDigest digest = MessageDigest.getInstance("SHA-1");
+            digest.update(decript.getBytes("UTF-8"));
+            byte[] messageDigest = digest.digest();
+            StringBuilder hexString = new StringBuilder();
+            for (byte message : messageDigest) {
+                String shaHex = Integer.toHexString(message & 0xFF);
+                if (shaHex.length() < 2)
+                    hexString.append(0);
+
+                hexString.append(shaHex);
+            }
+            return hexString.toString();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return "";
+    }
+
+    /**
+     * 签名
+     *
+     * @param data
+     * @return
+     */
+    public static String sign(Map<String, String> data) {
+        String sign = null;
+
+        try {
+            // A~z排序 (加上open_key)
+            Set<String> keySet = data.keySet();
+            String[] keyArray = keySet.toArray(new String[keySet.size()]);
+            Arrays.sort(keyArray);
+            StringBuilder sb = new StringBuilder();
+            for (String k : keyArray) {
+                if (k.equals("sign")) {  // 删除sign节点
+                    continue;
+                }
+                sb.append(k).append("=").append(data.get(k).trim()).append("&");
+            }
+
+            String sbStr = sb.toString();
+            String sortStr = sbStr.substring(0, sbStr.length() - 1);
+
+            // sha1加密(小写)
+            String sha1 = SHA1(sortStr).toLowerCase();
+
+            // md5加密(小写)
+            MessageDigest md5 = MessageDigest.getInstance("MD5");
+            byte[] digest = md5.digest(sha1.getBytes("utf-8"));
+            String resultString = byte2hex(digest);
+
+            sign = resultString.toLowerCase();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        return sign;
+    }
+
+    /**
+     * AES加密,再二进制转十六进制(bin2hex)
+     * @param dataJsonStr 需要加密的字符串
+     * @param key key
+     * @throws Exception
+     */
+    public static String handleEncrypt(String dataJsonStr, String key) throws Exception {
+
+        SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("ASCII"), "AES");
+        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
+        cipher.init(1, skeySpec);
+        byte[] encrypted = cipher.doFinal(dataJsonStr.getBytes("UTF-8"));
+
+        return byte2hex(encrypted);
+    }
+
+    public static String decrypt(String sSrc, String sKey) throws Exception {
+        SecretKeySpec skeySpec = new SecretKeySpec(sKey.getBytes("ASCII"), "AES");
+        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
+        cipher.init(2, skeySpec);
+        byte[] encrypted1 = hex2byte(sSrc);
+        byte[] original = cipher.doFinal(encrypted1);
+        return new String(original, "UTF-8");
+    }
+
+    /**
+     * 验签
+     *
+     * @param params
+     * @param key 农商openKey
+     * @return
+     */
+    public static Boolean verifySign(Map<String, String> params, String key) {
+        String respSign = params.get("sign");
+
+        if (StringUtils.hasText(respSign)) {
+            params.put("open_key", key);
+            String veriSign = sign(params);
+            if (respSign.equals(veriSign)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * byte数组转十六进制字符串
+     */
+    public static String byte2hex(byte[] result) {
+        StringBuffer sb = new StringBuffer(result.length * 2);
+        for (int i = 0; i < result.length; i++) {
+            int hight = ((result[i] >> 4) & 0x0f);
+            int low = result[i] & 0x0f;
+            sb.append(hight > 9 ? (char) ((hight - 10) + 'a') : (char) (hight + '0'));
+            sb.append(low > 9 ? (char) ((low - 10) + 'a') : (char) (low + '0'));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 十六进制字符串转byte数组
+     */
+    public static byte[] hex2byte(String strhex) {
+        if (strhex == null)
+            return null;
+
+        int l = strhex.length();
+        if (l % 2 == 1)
+            return null;
+
+        byte[] b = new byte[l / 2];
+        for (int i = 0; i != l / 2; ++i)
+            b[i] = (byte) Integer.parseInt(strhex.substring(i * 2, i * 2 + 2), 16);
+
+        return b;
+    }
+}

+ 13 - 8
src/main/resources/application.yml

@@ -33,14 +33,19 @@ spring:
         max-idle: 100
         max-idle: 100
         min-idle: 100
         min-idle: 100
         max-wait: 60000
         max-wait: 60000
-#  rabbitmq:
-#    host: 192.168.126.131
-#    port: 5672
-#    virtual-host: /
-#    # 手动ack
-#    listener:
-#      simple:
-#        acknowledge-mode: manual
+  rabbitmq:
+    host: 192.168.126.131
+    port: 5672
+    virtual-host: dev
+    username: guest
+    password: guest
+    # 消息重试机制
+    listener:
+      simple:
+        retry:
+          enabled: true
+          max-attempts: 4
+          initial-interval: 5000
 
 
 mybatis-plus:
 mybatis-plus:
   mapper-locations: classpath:/mapper/ihotel/*.xml
   mapper-locations: classpath:/mapper/ihotel/*.xml