Browse Source

房型预约业务开发完成

wangzhengliang 3 years ago
parent
commit
a31e701765
53 changed files with 1749 additions and 203 deletions
  1. 10 21
      pom.xml
  2. 37 0
      src/main/java/com/chuanghai/ihotel/anno/RepeatSubmit.java
  3. 3 0
      src/main/java/com/chuanghai/ihotel/aop/ParamCheckAspect.java
  4. 112 0
      src/main/java/com/chuanghai/ihotel/aop/RepeatSubmitAspect.java
  5. 2 0
      src/main/java/com/chuanghai/ihotel/common/exception/BizCodeEnume.java
  6. 2 1
      src/main/java/com/chuanghai/ihotel/common/exception/MyExceptionHandler.java
  7. 0 141
      src/main/java/com/chuanghai/ihotel/common/utils/JwtOperator.java
  8. 28 0
      src/main/java/com/chuanghai/ihotel/config/InterceptorConfig.java
  9. 22 0
      src/main/java/com/chuanghai/ihotel/config/JXNXSPayConfig.java
  10. 43 0
      src/main/java/com/chuanghai/ihotel/config/RedisTemplateConfiguration.java
  11. 56 0
      src/main/java/com/chuanghai/ihotel/config/RedissionConfiguration.java
  12. 21 0
      src/main/java/com/chuanghai/ihotel/constant/RedisKey.java
  13. 21 0
      src/main/java/com/chuanghai/ihotel/constant/TimeConstant.java
  14. 122 20
      src/main/java/com/chuanghai/ihotel/controller/HotelOrderController.java
  15. 35 0
      src/main/java/com/chuanghai/ihotel/controller/request/ConfrimOrderRequest.java
  16. 61 0
      src/main/java/com/chuanghai/ihotel/controller/request/SubmitOrderRequest.java
  17. 17 0
      src/main/java/com/chuanghai/ihotel/dao/HotelUserDao.java
  18. 3 0
      src/main/java/com/chuanghai/ihotel/dao/RoomRealtimeStatuDao.java
  19. 37 0
      src/main/java/com/chuanghai/ihotel/dto/LockRoomDTO.java
  20. 8 7
      src/main/java/com/chuanghai/ihotel/entity/HotelOrderEntity.java
  21. 44 0
      src/main/java/com/chuanghai/ihotel/entity/HotelUserEntity.java
  22. 2 1
      src/main/java/com/chuanghai/ihotel/entity/RoomRealtimeStatuEntity.java
  23. 8 0
      src/main/java/com/chuanghai/ihotel/entity/RoomTypeEntity.java
  24. 8 0
      src/main/java/com/chuanghai/ihotel/entity/SystemSettingEntity.java
  25. 22 0
      src/main/java/com/chuanghai/ihotel/enums/OrderBillStatuEnum.java
  26. 24 0
      src/main/java/com/chuanghai/ihotel/enums/OrderStatuEnum.java
  27. 3 3
      src/main/java/com/chuanghai/ihotel/enums/RoomTypeEnum.java
  28. 25 0
      src/main/java/com/chuanghai/ihotel/enums/UserIdentityTypeEnum.java
  29. 84 0
      src/main/java/com/chuanghai/ihotel/interceptor/LoginInterceptor.java
  30. 19 0
      src/main/java/com/chuanghai/ihotel/service/HotelOrderService.java
  31. 17 0
      src/main/java/com/chuanghai/ihotel/service/HotelUserService.java
  32. 9 0
      src/main/java/com/chuanghai/ihotel/service/RoomRealtimeStatuService.java
  33. 10 0
      src/main/java/com/chuanghai/ihotel/service/RoomService.java
  34. 2 0
      src/main/java/com/chuanghai/ihotel/service/RoomTypeService.java
  35. 6 0
      src/main/java/com/chuanghai/ihotel/service/SystemSettingService.java
  36. 180 1
      src/main/java/com/chuanghai/ihotel/service/impl/HotelOrderServiceImpl.java
  37. 22 0
      src/main/java/com/chuanghai/ihotel/service/impl/HotelUserServiceImpl.java
  38. 86 0
      src/main/java/com/chuanghai/ihotel/service/impl/RoomRealtimeStatuServiceImpl.java
  39. 15 0
      src/main/java/com/chuanghai/ihotel/service/impl/RoomServiceImpl.java
  40. 8 1
      src/main/java/com/chuanghai/ihotel/service/impl/RoomTypeServiceImpl.java
  41. 12 4
      src/main/java/com/chuanghai/ihotel/service/impl/SystemSettingServiceImpl.java
  42. 38 0
      src/main/java/com/chuanghai/ihotel/util/BigDecimalSerializer.java
  43. 181 0
      src/main/java/com/chuanghai/ihotel/util/CommonUtil.java
  44. 80 0
      src/main/java/com/chuanghai/ihotel/util/JWTUtil.java
  45. 86 0
      src/main/java/com/chuanghai/ihotel/vo/ConfirmOrderVO.java
  46. 24 0
      src/main/java/com/chuanghai/ihotel/vo/LoginUserVO.java
  47. 25 0
      src/main/java/com/chuanghai/ihotel/vo/OrderSubmitTokenVO.java
  48. 24 0
      src/main/java/com/chuanghai/ihotel/vo/OrderSubmitVO.java
  49. 4 3
      src/main/java/com/chuanghai/ihotel/vo/RoomTypeShortDescVO.java
  50. 16 0
      src/main/resources/application.yml
  51. 14 0
      src/main/resources/mapper/ihotel/HotelUserDao.xml
  52. 9 0
      src/main/resources/mapper/ihotel/RoomRealtimeStatuDao.xml
  53. 2 0
      src/main/resources/mapper/ihotel/SystemSettingDao.xml

+ 10 - 21
pom.xml

@@ -41,29 +41,11 @@
             <version>3.3.1</version>
         </dependency>
 
-        <!-- jwt -->
+        <!-- JWT相关 -->
         <dependency>
             <groupId>io.jsonwebtoken</groupId>
-            <artifactId>jjwt-api</artifactId>
-            <version>0.10.7</version>
-        </dependency>
-        <dependency>
-            <groupId>io.jsonwebtoken</groupId>
-            <artifactId>jjwt-impl</artifactId>
-            <version>0.10.7</version>
-            <scope>runtime</scope>
-        </dependency>
-        <dependency>
-            <groupId>io.jsonwebtoken</groupId>
-            <artifactId>jjwt-jackson</artifactId>
-            <version>0.10.7</version>
-            <scope>runtime</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.assertj</groupId>
-            <artifactId>assertj-core</artifactId>
-            <version>3.16.1</version>
-            <scope>compile</scope>
+            <artifactId>jjwt</artifactId>
+            <version>0.7.0</version>
         </dependency>
 
         <!-- 密码加密 -->
@@ -97,6 +79,13 @@
             <artifactId>jedis</artifactId>
         </dependency>
 
+        <!--redisson分布式锁-->
+        <dependency>
+            <groupId>org.redisson</groupId>
+            <artifactId>redisson</artifactId>
+            <version>3.12.0</version>
+        </dependency>
+
         <!-- Spring Cache -->
         <dependency>
             <groupId>org.springframework.boot</groupId>

+ 37 - 0
src/main/java/com/chuanghai/ihotel/anno/RepeatSubmit.java

@@ -0,0 +1,37 @@
+package com.chuanghai.ihotel.anno;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 防重提交
+ * @author codingliang
+ */
+@Documented
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RepeatSubmit {
+
+
+    /**
+     * 防重提交,支持两种,一个是方法参数,一个是令牌
+     */
+    enum Type { PARAM, TOKEN }
+
+    /**
+     * 默认防重提交,是方法参数
+     * @return
+     */
+    Type limitType() default Type.PARAM;
+
+
+    /**
+     * 加锁过期时间,默认是5秒
+     * @return
+     */
+    long lockTime() default 5;
+
+}

+ 3 - 0
src/main/java/com/chuanghai/ihotel/aop/ParamCheckAspect.java

@@ -9,6 +9,8 @@ import org.aspectj.lang.annotation.Around;
 import org.aspectj.lang.annotation.Aspect;
 import org.aspectj.lang.reflect.MethodSignature;
 import org.hibernate.validator.HibernateValidator;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
 import org.springframework.stereotype.Component;
 
 import javax.validation.ConstraintViolation;
@@ -25,6 +27,7 @@ import java.util.Set;
  */
 @Aspect
 @Component
+@Order(0)
 public class ParamCheckAspect {
 
     /**

+ 112 - 0
src/main/java/com/chuanghai/ihotel/aop/RepeatSubmitAspect.java

@@ -0,0 +1,112 @@
+package com.chuanghai.ihotel.aop;
+
+import com.chuanghai.ihotel.anno.RepeatSubmit;
+import com.chuanghai.ihotel.common.exception.BizCodeEnume;
+import com.chuanghai.ihotel.common.exception.RRException;
+import com.chuanghai.ihotel.constant.RedisKey;
+import com.chuanghai.ihotel.interceptor.LoginInterceptor;
+import com.chuanghai.ihotel.util.CommonUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.annotation.Order;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import java.lang.reflect.Method;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 防重提交拦截
+ * @Description
+ * @Author codingliang
+ * @Version 1.0
+ **/
+
+@Aspect
+@Component
+@Slf4j
+@Order(1)
+public class RepeatSubmitAspect {
+
+
+    @Autowired
+    private StringRedisTemplate redisTemplate;
+    @Autowired
+    private RedissonClient redissonClient;
+
+
+    /**
+     * 切点
+     */
+    @Pointcut("@annotation(repeatSubmit)")
+    public void pointCutNoRepeatSubmit(RepeatSubmit repeatSubmit) {
+
+    }
+
+    /**
+     * 切入通知
+     * @param joinPoint
+     * @param repeatSubmit
+     * @return
+     * @throws Throwable
+     * @Around 可以用来在调用一个具体方法前和调用后来完成一些具体的任务。
+     * 方式一:加锁 固定时间内不能重复提交 <p>
+     * 方式二:先请求获取token,这边再删除token,删除成功则是第一次提交
+     */
+    @Around("pointCutNoRepeatSubmit(repeatSubmit)")
+    public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {
+
+        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
+
+        String cardNumber = LoginInterceptor.threadLocal.get().getCardNumber();
+
+        // 用于记录成功或者失败
+        boolean res;
+
+        // 防重提交类型
+        String type = repeatSubmit.limitType().name();
+        if (type.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())) { // 方式一,参数形式防重提交
+            String ipAddr = CommonUtil.getIpAddr(request);
+            MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
+            Method method = methodSignature.getMethod();
+            String className = method.getDeclaringClass().getName();
+
+            String keyId = CommonUtil.MD5(String.format("%s-%s-%s-%s",ipAddr,className,method,cardNumber));
+            String key = String.format(RedisKey.SUBMIT_ORDER_TOKEN_KEY_FOR_PARAM, keyId);
+
+            RLock lock = redissonClient.getLock(key);
+            // 尝试加锁,最多等待0秒,上锁以后5秒自动解锁 [lockTime默认为5s, 可以自定义]
+            // 加锁成功返回true,否则返回false
+            res = lock.tryLock(0, repeatSubmit.lockTime(), TimeUnit.SECONDS);
+        } else { // 方式二,令牌形式防重提交
+            String requestToken = request.getHeader("request_token");
+            if (!StringUtils.hasText(requestToken)) {
+                throw new RRException(BizCodeEnume.PARAMETER_ERROR, "访问令牌缺失");
+            }
+
+            String key = String.format(RedisKey.SUBMIT_ORDER_TOKEN_KEY_FOR_TOKEN, cardNumber, requestToken);
+
+            /**
+             * 删除锁,删除成功返回true
+             */
+            res = redisTemplate.delete(key);
+
+        }
+        if (!res) {
+            throw new RRException(BizCodeEnume.REPEAT_SUBMIT);
+        } else {
+            return joinPoint.proceed();
+        }
+    }
+}

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

@@ -26,6 +26,8 @@ public enum BizCodeEnume {
     INVALID_PARAM(11001,"参数格式校验失败"),
     BODY_IS_EMPTY(11002, "body为空"),
     PARAMETER_ERROR(11003, "参数异常"),
+    REPEAT_SUBMIT(11004, "重复提交表单"),
+    ORDER_SUBMIT_ERROR(11005, "订单提交失败"),
     THIRD_PARTY_SERVICE_CALL_FAILED(11007, "第三方服务调用失败"),
     DATA_IS_EXIST(16000, "数据已存在"),
     DATA_NOT_EXIST(16000, "数据不存在"),

+ 2 - 1
src/main/java/com/chuanghai/ihotel/common/exception/MyExceptionHandler.java

@@ -96,7 +96,8 @@ public class MyExceptionHandler {
      * @return
      */
     @ExceptionHandler(HttpMessageNotReadableException.class)
-    public CommonResult handleMessageNotReadableException() {
+    public CommonResult handleMessageNotReadableException(HttpMessageNotReadableException e) {
+        e.printStackTrace();
         return CommonResult.fail(Integer.toString(BizCodeEnume.BODY_IS_EMPTY.getCode()), BizCodeEnume.BODY_IS_EMPTY.getMsg());
     }
 

+ 0 - 141
src/main/java/com/chuanghai/ihotel/common/utils/JwtOperator.java

@@ -1,141 +0,0 @@
-package com.chuanghai.ihotel.common.utils;
-
-import com.chuanghai.ihotel.common.exception.BizCodeEnume;
-import com.chuanghai.ihotel.common.exception.RRException;
-import io.jsonwebtoken.Claims;
-import io.jsonwebtoken.Jwts;
-import io.jsonwebtoken.SignatureAlgorithm;
-import io.jsonwebtoken.security.Keys;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Component;
-
-import javax.crypto.SecretKey;
-import java.util.Date;
-import java.util.Map;
-
-/**
- * @Author: codingliang
- * @Description: JWT操作类
- * @Date: 2020-06-03 11:54
- * @Version: V1.0
- **/
-@Slf4j
-@RequiredArgsConstructor
-@Component
-public class JwtOperator {
-
-    /**
-     * 秘钥
-     * - 默认aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt
-     */
-    @Value("${jwt.secret:aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt}")
-    private String secret;
-
-    /**
-     * 有效期,单位秒
-     * - 默认2周
-     */
-    @Value("${jwt.expire-time-in-second:1209600}")
-    private Long expirationTimeInSecond;
-
-    /**
-     * 获取过期时间
-     * @return
-     */
-    public Long getExpirationTimeInSecond() {
-        return this.expirationTimeInSecond;
-    }
-
-    /**
-     * 从token中获取claim
-     *
-     * @param token token
-     * @return claim
-     */
-    public Claims getClaimsFromToken(String token) throws RRException {
-        try {
-            return Jwts.parser()
-                    .setSigningKey(this.secret.getBytes())
-                    .parseClaimsJws(token)
-                    .getBody();
-        } catch (Exception e) {
-            throw new RRException(BizCodeEnume.TOKEN_INVALID);
-        }
-    }
-
-    /**
-     * 获取token中的信息
-     * @param token token
-     * @param name 属性名称
-     * @return
-     */
-    public Long getInfoFromToken(String token, String name) {
-        Claims claims = getClaimsFromToken(token);
-        return claims.get(name, Long.class);
-    }
-
-    /**
-     * 获取token的过期时间
-     *
-     * @param token token
-     * @return 过期时间
-     */
-    public Date getExpirationDateFromToken(String token) throws RRException {
-        return getClaimsFromToken(token)
-                .getExpiration();
-    }
-
-    /**
-     * 判断token是否过期
-     *
-     * @param token token
-     * @return 已过期返回true,未过期返回false
-     */
-    private Boolean isTokenExpired(String token) throws RRException {
-        Date expiration = getExpirationDateFromToken(token);
-        return expiration.before(new Date());
-    }
-
-    /**
-     * 计算token的过期时间
-     *
-     * @return 过期时间
-     */
-    private Date getExpirationTime() {
-        return new Date(System.currentTimeMillis() + getExpirationTimeInSecond() * 1000);
-    }
-
-    /**
-     * 为指定用户生成token
-     *
-     * @param claims 用户信息
-     * @return token
-     */
-    public String generateToken(Map<String, Object> claims) {
-        Date createdTime = new Date();
-        Date expirationTime = this.getExpirationTime();
-
-
-        byte[] keyBytes = secret.getBytes();
-        SecretKey key = Keys.hmacShaKeyFor(keyBytes);
-
-        return Jwts.builder()
-                .setClaims(claims)
-                .setIssuedAt(createdTime)
-                .setExpiration(expirationTime)
-                .signWith(key, SignatureAlgorithm.HS256)
-                .compact();
-    }
-
-    /**
-     * 判断token是否非法
-     *
-     * @param token token
-     * @return 未过期返回true,否则返回false
-     */
-    public Boolean validateToken(String token) throws RRException {
-        return !isTokenExpired(token);
-    }
-}

+ 28 - 0
src/main/java/com/chuanghai/ihotel/config/InterceptorConfig.java

@@ -0,0 +1,28 @@
+package com.chuanghai.ihotel.config;
+
+import com.chuanghai.ihotel.interceptor.LoginInterceptor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * 拦截器配置
+ **/
+@Configuration
+@Slf4j
+public class InterceptorConfig implements WebMvcConfigurer {
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+
+        registry.addInterceptor(new LoginInterceptor())
+                // 添加拦截路径
+                .addPathPatterns("/**")
+                // 不拦截路径
+                .excludePathPatterns("/ihotel/roomType/list", "/ihotel/roomType/info/**");
+
+
+
+    }
+}

+ 22 - 0
src/main/java/com/chuanghai/ihotel/config/JXNXSPayConfig.java

@@ -0,0 +1,22 @@
+package com.chuanghai.ihotel.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @Author: codingliang
+ * @Description: 江西农商行支付配置
+ * @Date: 2021-08-18 10:55
+ * @Version: V1.0
+ **/
+@Configuration
+@ConfigurationProperties(prefix = "jxnxs")
+@Data
+public class JXNXSPayConfig {
+
+    private String openId;
+    private String openKey;
+    private String notifyUrl;
+    private String payUrl;
+}

+ 43 - 0
src/main/java/com/chuanghai/ihotel/config/RedisTemplateConfiguration.java

@@ -0,0 +1,43 @@
+package com.chuanghai.ihotel.config;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+/**
+ * RedisTemplate 配置
+ **/
+@Configuration
+public class RedisTemplateConfiguration {
+
+    @Bean
+    public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
+
+        RedisTemplate<Object,Object> redisTemplate = new RedisTemplate<>();
+
+        redisTemplate.setConnectionFactory(redisConnectionFactory);
+
+        // 配置序列化规则
+        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
+
+        ObjectMapper objectMapper = new ObjectMapper();
+        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
+
+        // 设置key-value序列化规则
+        redisTemplate.setKeySerializer(new StringRedisSerializer());
+        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
+
+        // 设置hash-value序列化规则
+        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
+        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
+
+        return redisTemplate;
+    }
+}

+ 56 - 0
src/main/java/com/chuanghai/ihotel/config/RedissionConfiguration.java

@@ -0,0 +1,56 @@
+package com.chuanghai.ihotel.config;
+
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Redission 配置
+ **/
+@Configuration
+public class RedissionConfiguration {
+
+    @Value("${spring.redis.host}")
+    private String redisHost;
+    @Value("${spring.redis.port}")
+    private String redisPort;
+//    @Value("${spring.redis.password}")
+//    private String redisPwd;
+
+    /**
+     * 配置分布式锁的redisson
+     * @return
+     */
+    @Bean
+    public RedissonClient redissonClient(){
+        Config config = new Config();
+
+        //单机方式
+        config.useSingleServer()
+//                .setPassword(redisPwd)
+                .setAddress("redis://"+redisHost+":"+redisPort);
+
+        //集群
+        //config.useClusterServers().addNodeAddress("redis://192.31.21.1:6379","redis://192.31.21.2:6379")
+
+        RedissonClient redissonClient = Redisson.create(config);
+        return redissonClient;
+    }
+
+    /**
+     * 集群模式
+     * 备注:可以用"rediss://"来启用SSL连接
+     */
+    /*@Bean
+    public RedissonClient redissonClusterClient() {
+        Config config = new Config();
+        config.useClusterServers().setScanInterval(2000) // 集群状态扫描间隔时间,单位是毫秒
+              .addNodeAddress("redis://127.0.0.1:7000")
+              .addNodeAddress("redis://127.0.0.1:7002");
+        RedissonClient redisson = Redisson.create(config);
+        return redisson;
+    }*/
+}

+ 21 - 0
src/main/java/com/chuanghai/ihotel/constant/RedisKey.java

@@ -0,0 +1,21 @@
+package com.chuanghai.ihotel.constant;
+
+/**
+ * Redis key
+ * @Description
+ * @Version 1.0
+ **/
+public class RedisKey {
+    /**
+     * 提交订单令牌的缓存key param方式
+     */
+    public static final String SUBMIT_ORDER_TOKEN_KEY_FOR_PARAM = "order:submit:param:%s";
+    /**
+     * 提交订单令牌的缓存key token方式
+     */
+    public static final String SUBMIT_ORDER_TOKEN_KEY_FOR_TOKEN = "order:submit:token:%s:%s";
+    /**
+     * 锁定房源
+     */
+    public static final String LOCK_ROOM_BY_ROOM_TYPE = "lock:room:roomType:%s";
+}

+ 21 - 0
src/main/java/com/chuanghai/ihotel/constant/TimeConstant.java

@@ -0,0 +1,21 @@
+package com.chuanghai.ihotel.constant;
+
+/**
+ * 时间常量
+ */
+public class TimeConstant {
+
+    /**
+     * 默认30分钟超时未支付,单位毫秒
+     */
+    public static final long ORDER_PAY_TIMEOUT_MILLS =1000 * 60 * 30;
+
+    /**
+     * 最早到店时间
+     */
+    public static final String INTO_TIME = "14:00:00";
+    /**
+     * 最晚离店时间
+     */
+    public static final String OUT_TIME = "12:00:00";
+}

+ 122 - 20
src/main/java/com/chuanghai/ihotel/controller/HotelOrderController.java

@@ -1,23 +1,42 @@
 package com.chuanghai.ihotel.controller;
 
-import java.util.Arrays;
-
+import com.chuanghai.ihotel.anno.ParamCheck;
+import com.chuanghai.ihotel.anno.RepeatSubmit;
+import com.chuanghai.ihotel.common.exception.BizCodeEnume;
+import com.chuanghai.ihotel.common.exception.RRException;
+import com.chuanghai.ihotel.common.utils.CommonResult;
+import com.chuanghai.ihotel.common.utils.PageParam;
+import com.chuanghai.ihotel.common.utils.PageUtils;
+import com.chuanghai.ihotel.constant.RedisKey;
+import com.chuanghai.ihotel.controller.request.ConfrimOrderRequest;
+import com.chuanghai.ihotel.controller.request.SubmitOrderRequest;
+import com.chuanghai.ihotel.entity.HotelOrderEntity;
+import com.chuanghai.ihotel.interceptor.LoginInterceptor;
+import com.chuanghai.ihotel.service.HotelOrderService;
+import com.chuanghai.ihotel.util.CommonUtil;
+import com.chuanghai.ihotel.vo.ConfirmOrderVO;
+import com.chuanghai.ihotel.vo.LoginUserVO;
+import com.chuanghai.ihotel.vo.OrderSubmitTokenVO;
+import com.chuanghai.ihotel.vo.OrderSubmitVO;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.PutMapping;
 import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
 import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RestController;
 
-import com.chuanghai.ihotel.entity.HotelOrderEntity;
-import com.chuanghai.ihotel.service.HotelOrderService;
-import com.chuanghai.ihotel.common.utils.PageUtils;
-import com.chuanghai.ihotel.common.utils.CommonResult;
-import com.chuanghai.ihotel.common.utils.PageParam;
-
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
 
 /**
  * 酒店订单 
@@ -29,11 +48,73 @@ import com.chuanghai.ihotel.common.utils.PageParam;
 @RestController
 @RequestMapping("hotelOrder")
 public class HotelOrderController {
+
+    @Autowired
+    private StringRedisTemplate redisTemplate;
     @Autowired
     private HotelOrderService hotelOrderService;
 
     /**
-     * 列表
+     * 获取请求令牌
+     * @param userToken 用户token
+     * @return
+     */
+    @GetMapping("submit/token")
+    public CommonResult<OrderSubmitTokenVO> getOrderToken(@RequestHeader("user_token") String userToken){
+        LoginUserVO loginUserVO = LoginInterceptor.threadLocal.get();
+        String cardNumber = loginUserVO.getCardNumber();
+        String token = CommonUtil.getStringNumRandom(32);
+        String key = String.format(RedisKey.SUBMIT_ORDER_TOKEN_KEY_FOR_TOKEN, cardNumber, token);
+
+        // 令牌有效时间是30分钟
+        redisTemplate.opsForValue().set(key, String.valueOf(Thread.currentThread().getId()),30, TimeUnit.MINUTES);
+
+        OrderSubmitTokenVO tokenVO = OrderSubmitTokenVO.builder()
+                .submitToken(token)
+                .expire(LocalDateTime.now().plusMinutes(30).toInstant(ZoneOffset.ofHours(8)).toEpochMilli())
+                .build();
+        return CommonResult.ok().setResult(tokenVO);
+    }
+
+    /**
+     * 确认订单
+     * @param request 预定参数
+     * @return
+     */
+    @GetMapping("confirm/order")
+    @ParamCheck
+    public CommonResult<ConfirmOrderVO> confirmOrder(ConfrimOrderRequest request) {
+        // 时间校验
+        timeCheck(CommonUtil.localDateToTime(request.getStartTime(), "00:00:00"),
+                CommonUtil.localDateToTime(request.getEndTime(), "00:00:00"));
+
+        ConfirmOrderVO vo = hotelOrderService.confirmOrder(request);
+
+        return CommonResult.ok().setResult(vo);
+    }
+
+    /**
+     * 提交订单
+     * @param userToken 用户卡号
+     * @param requestToken 请求令牌,从【获取请求令牌】接口获取
+     * @param request 请求参数
+     * @return
+     */
+    @PostMapping("submit/order")
+    @ParamCheck(index = 3)
+//    @RepeatSubmit(limitType = RepeatSubmit.Type.TOKEN)
+    public CommonResult<OrderSubmitVO> submitOrder(@RequestHeader("user_token") String userToken,
+                                    @RequestHeader("request_token") String requestToken,
+                                    @RequestBody SubmitOrderRequest request) {
+        // 时间校验
+        timeCheck(request.getEnableStartTime(), request.getEnableEndTime());
+
+        OrderSubmitVO vo = hotelOrderService.submitOrder(userToken, request);
+        return CommonResult.ok().setResult(vo);
+    }
+
+    /**
+     * 订单列表
      */
     @GetMapping("/list")
     public CommonResult<PageUtils<HotelOrderEntity>> list(PageParam pageParam){
@@ -54,16 +135,6 @@ public class HotelOrderController {
     }
 
     /**
-     * 保存
-     */
-    @PostMapping("/save")
-    public CommonResult<String> save(@RequestBody HotelOrderEntity hotelOrder){
-		hotelOrderService.save(hotelOrder);
-
-        return CommonResult.ok();
-    }
-
-    /**
      * 修改
      */
     @PutMapping("/update")
@@ -91,4 +162,35 @@ public class HotelOrderController {
         }
     }
 
+    private void timeCheck(LocalDateTime startTime, LocalDateTime endTime) {
+        // 校验参数
+        LocalDateTime currentTime = LocalDateTime.now();
+        LocalDate currentDay = currentTime.toLocalDate();
+        LocalDate startDay = startTime.toLocalDate();
+        LocalDate endDay = endTime.toLocalDate();
+
+        if (startDay.isBefore(currentDay)) {
+            long between = ChronoUnit.DAYS.between(startDay, currentDay);
+            if (between > 1) {
+                throw new RRException(BizCodeEnume.PARAMETER_ERROR, "预定日期错误,不能预定以前的房源");
+            }
+
+            // 当天上午10点之前可以预定前一天的房源
+            if (currentTime.getHour() >= 10) {
+                throw new RRException(BizCodeEnume.PARAMETER_ERROR, "预定日期错误,10点之后不可预定前一天房源");
+            }
+        }
+
+        if (startDay.isEqual(endDay)) {
+            throw new RRException(BizCodeEnume.PARAMETER_ERROR, "入住时间和离店时间不能为同一天");
+        }
+
+        if (endTime.isBefore(startTime)) {
+            throw new RRException(BizCodeEnume.PARAMETER_ERROR, "离店时间必须晚于入住时间");
+        }
+
+        if (ChronoUnit.DAYS.between(startDay, endDay) > 7) {
+            throw new RRException(BizCodeEnume.PARAMETER_ERROR, "最多预定7天");
+        }
+    }
 }

+ 35 - 0
src/main/java/com/chuanghai/ihotel/controller/request/ConfrimOrderRequest.java

@@ -0,0 +1,35 @@
+package com.chuanghai.ihotel.controller.request;
+
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import javax.validation.constraints.NotNull;
+import java.time.LocalDate;
+
+/**
+ * @Author: codingliang
+ * @Description: 确认订单
+ * @Date: 2022-07-28 11:13
+ * @Version: V1.0
+ **/
+@Data
+public class ConfrimOrderRequest {
+
+    /**
+     * 入住时间 yyyy-MM-dd
+     */
+    @NotNull(message = "入住时间不能为空")
+    @DateTimeFormat(pattern = "yyyy-MM-dd")
+    private LocalDate startTime;
+    /**
+     * 离店时间 yyyy-MM-dd HH
+     */
+    @NotNull(message = "离店时间不能为空")
+    @DateTimeFormat(pattern = "yyyy-MM-dd")
+    private LocalDate endTime;
+    /**
+     * 房型id
+     */
+    @NotNull(message = "房型id不能为空")
+    private Long roomTypeId;
+}

+ 61 - 0
src/main/java/com/chuanghai/ihotel/controller/request/SubmitOrderRequest.java

@@ -0,0 +1,61 @@
+package com.chuanghai.ihotel.controller.request;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * @Author: codingliangz
+ * @Description: 提交订单
+ * @Date: 2022-07-28 15:55
+ * @Version: V1.0
+ **/
+@Data
+public class SubmitOrderRequest {
+    /**
+     * 入住时间 yyyy-MM-dd HH:mm:ss
+     */
+    @NotNull(message = "入住时间不能为空")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime enableStartTime;
+    /**
+     * 离店时间 yyyy-MM-dd HH:mm:ss
+     */
+    @NotNull(message = "离店时间不能为空")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime enableEndTime;
+    /**
+     * 房间类型id
+     */
+    @NotNull(message = "房间类型id不能为空")
+    private Long roomTypeId;
+    /**
+     * 需付总金额,用于订单验价
+     */
+    @NotNull(message = "需付总金额不能为空")
+    private BigDecimal payAmount;
+    /**
+     * 联系人姓名
+     */
+    @NotBlank(message = "联系人姓名不能为空")
+    private String userName;
+    /**
+     * 联系人电话
+     */
+    @NotBlank(message = "联系人电话不能为空")
+    private String userPhone;
+    /**
+     * 预计到店时间 yyyy-MM-dd HH:mm:ss
+     */
+    @NotNull(message = "预计到店时间不能为空")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime mayIntoTime;
+    /**
+     * 备注说明
+     */
+    private String remark;
+}

+ 17 - 0
src/main/java/com/chuanghai/ihotel/dao/HotelUserDao.java

@@ -0,0 +1,17 @@
+package com.chuanghai.ihotel.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.chuanghai.ihotel.entity.HotelUserEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 酒店用户
+ * 
+ * @author codingliang
+ * @email codingliang@gmail.com
+ * @date 2022-07-27 10:02:04
+ */
+@Mapper
+public interface HotelUserDao extends BaseMapper<HotelUserEntity> {
+	
+}

+ 3 - 0
src/main/java/com/chuanghai/ihotel/dao/RoomRealtimeStatuDao.java

@@ -6,6 +6,7 @@ import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
 
 import java.time.LocalDateTime;
+import java.util.List;
 
 /**
  * 实时房态 
@@ -18,4 +19,6 @@ import java.time.LocalDateTime;
 public interface RoomRealtimeStatuDao extends BaseMapper<RoomRealtimeStatuEntity> {
 
     int getBusyNum(@Param("roomTypeId") Long roomTypeId, @Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime);
+
+    List<Long> getBusyRoomId(@Param("roomTypeId") Long roomTypeId, @Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime);
 }

+ 37 - 0
src/main/java/com/chuanghai/ihotel/dto/LockRoomDTO.java

@@ -0,0 +1,37 @@
+package com.chuanghai.ihotel.dto;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * @Author: codingliang
+ * @Description: 锁房dto
+ * @Date: 2022-07-28 17:19
+ * @Version: V1.0
+ **/
+@Data
+@Builder
+public class LockRoomDTO {
+    /**
+     * 锁定开始时间
+     */
+    private LocalDateTime startTime;
+    /**
+     * 锁定结束时间
+     */
+    private LocalDateTime endTime;
+    /**
+     * 业务id
+     */
+    private Long bizId;
+    /**
+     * 房型id
+     */
+    private Long roomTypeId;
+    /**
+     * 房间id
+     */
+    private Long roomId;
+}

+ 8 - 7
src/main/java/com/chuanghai/ihotel/entity/HotelOrderEntity.java

@@ -1,5 +1,6 @@
 package com.chuanghai.ihotel.entity;
 
+import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
@@ -23,12 +24,12 @@ public class HotelOrderEntity implements Serializable {
 	/**
 	 * id
 	 */
-	@TableId
+	@TableId(type = IdType.ASSIGN_ID)
 	private Long id;
 	/**
 	 * 订单编号
 	 */
-	private Long orderNo;
+	private String orderNo;
 	/**
 	 * 订单用户标识
 	 */
@@ -62,7 +63,7 @@ public class HotelOrderEntity implements Serializable {
 	 */
 	private BigDecimal payAmount;
 	/**
-	 * 支付方式 1微校支付
+	 * 支付方式 (保留字段,页面暂不显示)
 	 */
 	private String payMethod;
 	/**
@@ -84,19 +85,19 @@ public class HotelOrderEntity implements Serializable {
 	/**
 	 * 最早入住时间 最早可入住时间
 	 */
-	private String enableStartTime;
+	private LocalDateTime enableStartTime;
 	/**
 	 * 最晚离店时间 最晚要离店时间
 	 */
-	private String enableEndTime;
+	private LocalDateTime enableEndTime;
 	/**
 	 * 实际入住时间
 	 */
-	private String realStartTime;
+	private LocalDateTime realStartTime;
 	/**
 	 * 实际离店时间
 	 */
-	private String realEndTime;
+	private LocalDateTime realEndTime;
 	/**
 	 * 预计到店时间 预计到店时间
 	 */

+ 44 - 0
src/main/java/com/chuanghai/ihotel/entity/HotelUserEntity.java

@@ -0,0 +1,44 @@
+package com.chuanghai.ihotel.entity;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 酒店用户表
+ * 
+ * @author codingliang
+ * @email codingliang@gmail.com
+ * @date 2022-07-27 10:02:04
+ */
+@Data
+@TableName("hotel_user")
+public class HotelUserEntity implements Serializable {
+	private static final long serialVersionUID = -1889239169533119725L;
+
+	/**
+	 * id
+	 */
+	@TableId
+	private Long id;
+	/**
+	 * 卡号
+	 */
+	private String cardNumber;
+	/**
+	 * 姓名
+	 */
+	private String name;
+	/**
+	 * 身份类型 0其他、1学生、4教职工、5校友
+	 */
+	private String identityType;
+	/**
+	 * 状态 0拉黑、1正常
+	 */
+	private String statu;
+}

+ 2 - 1
src/main/java/com/chuanghai/ihotel/entity/RoomRealtimeStatuEntity.java

@@ -1,5 +1,6 @@
 package com.chuanghai.ihotel.entity;
 
+import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
@@ -22,7 +23,7 @@ public class RoomRealtimeStatuEntity implements Serializable {
 	/**
 	 * id
 	 */
-	@TableId
+	@TableId(type = IdType.ASSIGN_ID)
 	private Long id;
 	/**
 	 * 房间id

+ 8 - 0
src/main/java/com/chuanghai/ihotel/entity/RoomTypeEntity.java

@@ -3,7 +3,11 @@ package com.chuanghai.ihotel.entity;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.chuanghai.ihotel.util.BigDecimalSerializer;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import lombok.AllArgsConstructor;
 import lombok.Data;
+import lombok.NoArgsConstructor;
 
 import javax.validation.constraints.Min;
 import javax.validation.constraints.NotBlank;
@@ -19,6 +23,8 @@ import java.math.BigDecimal;
  * @date 2022-07-27 10:02:04
  */
 @Data
+@NoArgsConstructor
+@AllArgsConstructor
 @TableName("room_type")
 public class RoomTypeEntity implements Serializable {
 	private static final long serialVersionUID = 1580689328484360501L;
@@ -38,12 +44,14 @@ public class RoomTypeEntity implements Serializable {
 	 */
 	@NotNull(message = "日常价格不能为空")
 	@Min(value = 0, message = "日常价格不能为负数")
+	@JsonSerialize(using = BigDecimalSerializer.class)
 	private BigDecimal usualPrice;
 	/**
 	 * 优惠价格 单位:元/间/晚
 	 */
 	@NotNull(message = "优惠价格不能为空")
 	@Min(value = 0, message = "优惠价格不能为负数")
+	@JsonSerialize(using = BigDecimalSerializer.class)
 	private BigDecimal discountPrice;
 	/**
 	 * 房型简单描述

+ 8 - 0
src/main/java/com/chuanghai/ihotel/entity/SystemSettingEntity.java

@@ -33,6 +33,14 @@ public class SystemSettingEntity implements Serializable {
 	 */
 	private Integer preDay;
 	/**
+	 * 水费 单位:元/吨
+	 */
+	private BigDecimal priceOfWater;
+	/**
+	 * 电费 单位:元/度
+	 */
+	private BigDecimal priceOfElectric;
+	/**
 	 * 水免费额度 单位:吨
 	 */
 	private BigDecimal freeQuotaOfWater;

+ 22 - 0
src/main/java/com/chuanghai/ihotel/enums/OrderBillStatuEnum.java

@@ -0,0 +1,22 @@
+package com.chuanghai.ihotel.enums;
+
+import lombok.Getter;
+
+/**
+ * 订单账单状态
+ */
+@Getter
+public enum OrderBillStatuEnum {
+
+    WAIT_HANDLE("0", "待处理"),
+    REFUND_ING("1", "退款中"),
+    WAIT_PAY("2", "等待支付"),
+    FINISH("3", "已完成(退款或者支付)");
+
+    private String code;
+    private String desc;
+    OrderBillStatuEnum(String code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+}

+ 24 - 0
src/main/java/com/chuanghai/ihotel/enums/OrderStatuEnum.java

@@ -0,0 +1,24 @@
+package com.chuanghai.ihotel.enums;
+
+import lombok.Getter;
+
+/**
+ * 订单状态
+ */
+@Getter
+public enum OrderStatuEnum {
+    CANCEL("0", "已取消"),
+    WAIT_PAY("1", "待支付"),
+    FINISH_PAY("2", "待入住"),
+    HOLD_ON("3", "已入住"),
+    WAIT_BILL("4", "待结账"),
+    FINISH("5", "已完成");
+
+    private String code;
+    private String desc;
+
+    OrderStatuEnum(String code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+}

+ 3 - 3
src/main/java/com/chuanghai/ihotel/enums/RoomTypeEnum.java

@@ -9,17 +9,17 @@ import lombok.Getter;
  * @Version: V1.0
  **/
 @Getter
-public enum RoomTypeEnum {
+public enum RoomStatuEnum {
 
     FREE("1", "空闲"),
     ORDER("2", "预定"),
-    CHECK_IN("3", "入住"),
+    HOLD("3", "入住"),
     DIRTY("4", "脏房"),
     LOCK("5", "锁定");
 
     private String code;
     private String desc;
-    RoomTypeEnum(String code, String desc) {
+    RoomStatuEnum(String code, String desc) {
         this.code = code;
         this.desc = desc;
     }

+ 25 - 0
src/main/java/com/chuanghai/ihotel/enums/UserIdentityTypeEnum.java

@@ -0,0 +1,25 @@
+package com.chuanghai.ihotel.enums;
+
+import lombok.Getter;
+
+/**
+ * @Author: codingliang
+ * @Description: 用户身份
+ * @Date: 2022-07-28 16:46
+ * @Version: V1.0
+ **/
+@Getter
+public enum  UserIdentityTypeEnum {
+
+    OTHER("0", "其他"),
+    STUDENT("1", "学生"),
+    STAFF_OF_TEACHER("4", "教职工"),
+    OTHER_STUDENT("5", "校友");
+
+    private String code;
+    private String typeName;
+    UserIdentityTypeEnum(String code, String typeName) {
+        this.code = code;
+        this.typeName = typeName;
+    }
+}

+ 84 - 0
src/main/java/com/chuanghai/ihotel/interceptor/LoginInterceptor.java

@@ -0,0 +1,84 @@
+package com.chuanghai.ihotel.interceptor;
+
+import com.chuanghai.ihotel.common.exception.BizCodeEnume;
+import com.chuanghai.ihotel.common.utils.CommonResult;
+import com.chuanghai.ihotel.util.CommonUtil;
+import com.chuanghai.ihotel.util.JWTUtil;
+import com.chuanghai.ihotel.vo.LoginUserVO;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.jsonwebtoken.Claims;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.util.StringUtils;
+import org.springframework.web.servlet.HandlerInterceptor;
+import org.springframework.web.servlet.ModelAndView;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * 登录拦截
+ **/
+@Slf4j
+public class LoginInterceptor implements HandlerInterceptor {
+
+    public static ThreadLocal<LoginUserVO> threadLocal = new ThreadLocal<>();
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        if (HttpMethod.OPTIONS.toString().equalsIgnoreCase(request.getMethod())) {
+            response.setStatus(HttpStatus.NO_CONTENT.value());
+            return true;
+        }
+
+        // 获取token信息
+        String token = request.getHeader("user_token");
+        // 验证userToken
+        if (!StringUtils.hasText(token)) {
+            returnErrorResponse(response, BizCodeEnume.TOKEN_IS_EMPTY);
+            return false;
+        }
+
+        // 解析token
+        Claims claims = JWTUtil.checkJWT(token);
+        if (claims == null) { // 未登录
+            returnErrorResponse(response, BizCodeEnume.TOKEN_INVALID);
+            return false;
+        }
+
+        String cardNumber = (String) claims.get("card_number");
+        String identityType = (String) claims.get("identity_type");
+        LoginUserVO loginUserVO = LoginUserVO.builder().cardNumber(cardNumber).identityType(identityType).build();
+        threadLocal.set(loginUserVO);
+
+        return true;
+    }
+
+    @Override
+    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
+
+    }
+
+    @Override
+    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
+        threadLocal.remove();
+    }
+
+    // 返回错误信息
+    public void returnErrorResponse(HttpServletResponse response, BizCodeEnume bizCodeEnume) {
+        try (OutputStream out = response.getOutputStream()) {
+            CommonResult fail = CommonResult.fail(Integer.toString(bizCodeEnume.getCode()), bizCodeEnume.getMsg());
+            ObjectMapper objectMapper = new ObjectMapper();
+
+            response.setCharacterEncoding("utf-8");
+            response.setContentType("text/json");
+            out.write(objectMapper.writeValueAsString(fail).getBytes("utf-8"));
+            out.flush();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+}

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

@@ -3,7 +3,11 @@ package com.chuanghai.ihotel.service;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.chuanghai.ihotel.common.utils.PageUtils;
 import com.chuanghai.ihotel.common.utils.PageParam;
+import com.chuanghai.ihotel.controller.request.ConfrimOrderRequest;
+import com.chuanghai.ihotel.controller.request.SubmitOrderRequest;
 import com.chuanghai.ihotel.entity.HotelOrderEntity;
+import com.chuanghai.ihotel.vo.ConfirmOrderVO;
+import com.chuanghai.ihotel.vo.OrderSubmitVO;
 
 /**
  * 酒店订单 
@@ -15,5 +19,20 @@ import com.chuanghai.ihotel.entity.HotelOrderEntity;
 public interface HotelOrderService extends IService<HotelOrderEntity> {
 
     PageUtils queryPage(PageParam pageParam);
+
+    /**
+     * 确认订单
+     * @param request
+     * @return
+     */
+    ConfirmOrderVO confirmOrder(ConfrimOrderRequest request);
+
+    /**
+     * 提交订单
+     * @param userToken 用户token
+     * @param request 请求参数
+     * @return
+     */
+    OrderSubmitVO submitOrder(String userToken, SubmitOrderRequest request);
 }
 

+ 17 - 0
src/main/java/com/chuanghai/ihotel/service/HotelUserService.java

@@ -0,0 +1,17 @@
+package com.chuanghai.ihotel.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.chuanghai.ihotel.entity.HotelUserEntity;
+
+/**
+ * 酒店用户
+ *
+ * @author codingliang
+ * @email codingliang@gmail.com
+ * @date 2022-07-27 10:02:04
+ */
+public interface HotelUserService extends IService<HotelUserEntity> {
+
+    HotelUserEntity findByCardNumber(String cardNumber);
+}
+

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

@@ -3,7 +3,9 @@ package com.chuanghai.ihotel.service;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.chuanghai.ihotel.common.utils.PageUtils;
 import com.chuanghai.ihotel.common.utils.PageParam;
+import com.chuanghai.ihotel.dto.LockRoomDTO;
 import com.chuanghai.ihotel.entity.RoomRealtimeStatuEntity;
+import com.chuanghai.ihotel.entity.RoomTypeEntity;
 
 import java.time.LocalDateTime;
 
@@ -28,5 +30,12 @@ public interface RoomRealtimeStatuService extends IService<RoomRealtimeStatuEnti
      * @return
      */
     int getBusyNum(Long roomTypeId, LocalDateTime startTime, LocalDateTime endTime);
+
+    /**
+     * 锁定房源
+     * @param lockRoomDTO 锁房参数
+     * @return 锁定的房间id
+     */
+    Long lockRoomByRoomTypeId(LockRoomDTO lockRoomDTO);
 }
 

+ 10 - 0
src/main/java/com/chuanghai/ihotel/service/RoomService.java

@@ -6,6 +6,8 @@ import com.chuanghai.ihotel.common.utils.PageParam;
 import com.chuanghai.ihotel.controller.request.RoomQueryRequest;
 import com.chuanghai.ihotel.entity.RoomEntity;
 
+import java.util.List;
+
 /**
  * 房间 
  *
@@ -20,5 +22,13 @@ public interface RoomService extends IService<RoomEntity> {
     PageUtils queryPageGroup(PageParam pageParam, RoomQueryRequest request);
 
     void mySave(RoomEntity room);
+
+    /**
+     * 获取不在roomIds中的房间id
+     * @param roomTypeId 房间类型id
+     * @param roomIds 房间id列表
+     * @return
+     */
+    Long getRoomIdNotIn(Long roomTypeId, List<Long> roomIds);
 }
 

+ 2 - 0
src/main/java/com/chuanghai/ihotel/service/RoomTypeService.java

@@ -21,5 +21,7 @@ public interface RoomTypeService extends IService<RoomTypeEntity> {
     PageUtils queryPage(PageParam pageParam);
 
     List<RoomTypeShortDescVO> listForClientIndex(RoomTypeQueryRequest request);
+
+    RoomTypeEntity myGetById(Long roomTypeId);
 }
 

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

@@ -15,5 +15,11 @@ import com.chuanghai.ihotel.entity.SystemSettingEntity;
 public interface SystemSettingService extends IService<SystemSettingEntity> {
 
     PageUtils queryPage(PageParam pageParam);
+
+    /**
+     * 获取系统设置
+     * @return
+     */
+    SystemSettingEntity get();
 }
 

+ 180 - 1
src/main/java/com/chuanghai/ihotel/service/impl/HotelOrderServiceImpl.java

@@ -1,5 +1,33 @@
 package com.chuanghai.ihotel.service.impl;
 
+import com.baomidou.mybatisplus.core.toolkit.IdWorker;
+import com.chuanghai.ihotel.common.exception.BizCodeEnume;
+import com.chuanghai.ihotel.common.exception.RRException;
+import com.chuanghai.ihotel.config.JXNXSPayConfig;
+import com.chuanghai.ihotel.constant.TimeConstant;
+import com.chuanghai.ihotel.controller.request.ConfrimOrderRequest;
+import com.chuanghai.ihotel.controller.request.SubmitOrderRequest;
+import com.chuanghai.ihotel.dto.LockRoomDTO;
+import com.chuanghai.ihotel.entity.HotelUserEntity;
+import com.chuanghai.ihotel.entity.RoomEntity;
+import com.chuanghai.ihotel.entity.RoomTypeEntity;
+import com.chuanghai.ihotel.entity.SystemSettingEntity;
+import com.chuanghai.ihotel.enums.OrderBillStatuEnum;
+import com.chuanghai.ihotel.enums.OrderStatuEnum;
+import com.chuanghai.ihotel.enums.UserIdentityTypeEnum;
+import com.chuanghai.ihotel.interceptor.LoginInterceptor;
+import com.chuanghai.ihotel.service.HotelUserService;
+import com.chuanghai.ihotel.service.RoomRealtimeStatuService;
+import com.chuanghai.ihotel.service.RoomService;
+import com.chuanghai.ihotel.service.RoomTypeService;
+import com.chuanghai.ihotel.service.SystemSettingService;
+import com.chuanghai.ihotel.util.CommonUtil;
+import com.chuanghai.ihotel.vo.ConfirmOrderVO;
+import com.chuanghai.ihotel.vo.LoginUserVO;
+import com.chuanghai.ihotel.vo.OrderSubmitVO;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
@@ -11,19 +39,170 @@ import com.chuanghai.ihotel.common.utils.PageParam;
 import com.chuanghai.ihotel.dao.HotelOrderDao;
 import com.chuanghai.ihotel.entity.HotelOrderEntity;
 import com.chuanghai.ihotel.service.HotelOrderService;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
 
 
 @Service("hotelOrderService")
 public class HotelOrderServiceImpl extends ServiceImpl<HotelOrderDao, HotelOrderEntity> implements HotelOrderService {
 
+    @Autowired
+    private SystemSettingService systemSettingService;
+    @Autowired
+    private RoomTypeService roomTypeService;
+    @Autowired
+    private RoomRealtimeStatuService roomRealtimeStatuService;
+    @Autowired
+    private HotelUserService hotelUserService;
+    @Autowired
+    private RoomService roomService;
+    @Autowired
+    private JXNXSPayConfig jxnxsPayConfig;
+
     @Override
     public PageUtils queryPage(PageParam pageParam) {
         IPage<HotelOrderEntity> page = this.page(
                 new MyQuery<HotelOrderEntity>().getPage(pageParam),
-                new QueryWrapper<HotelOrderEntity>()
+                new QueryWrapper<>()
         );
 
         return new PageUtils(page);
     }
 
+    @Override
+    public ConfirmOrderVO confirmOrder(ConfrimOrderRequest request) {
+        SystemSettingEntity systemSetting = systemSettingService.get();
+
+        // 入住日期校验
+        Integer preDay = systemSetting.getPreDay();
+        if (Math.abs(ChronoUnit.DAYS.between(request.getStartTime(), LocalDate.now())) > preDay) {
+            throw new RRException(BizCodeEnume.PARAMETER_ERROR, "只能预定" + preDay + "天之内的房源");
+        }
+
+        // 查询房源是否充足
+        Long roomTypeId = request.getRoomTypeId();
+        RoomTypeEntity roomType = roomTypeService.myGetById(roomTypeId);
+        if (roomType == null) {
+            throw new RRException(BizCodeEnume.PARAMETER_ERROR, "房型不存在");
+        }
+
+        LocalDateTime startTime = CommonUtil.localDateToTime(request.getStartTime(), TimeConstant.INTO_TIME);
+        LocalDateTime endTime = CommonUtil.localDateToTime(request.getEndTime(), TimeConstant.OUT_TIME);
+        long days = ChronoUnit.DAYS.between(request.getStartTime(), request.getEndTime());
+        int busyNum = roomRealtimeStatuService.getBusyNum(roomTypeId, startTime, endTime);
+
+        int enableNum = roomType.getRoomNum() - busyNum;
+
+        // 查询房型价格
+        BigDecimal discountPrice = roomType.getDiscountPrice();
+
+        // 查询押金
+        BigDecimal deposit = systemSetting.getDeposit();
+
+        BigDecimal totalAmount = discountPrice.add(deposit).multiply(new BigDecimal(Long.toString(days)));
+
+        // 封装确认订单vo
+        ConfirmOrderVO vo = new ConfirmOrderVO();
+        vo.setStartTime(startTime);
+        vo.setEndTime(endTime);
+        vo.setNums(Integer.valueOf((int)days));
+        vo.setRoomTypeId(roomTypeId);
+        vo.setRoomTypeName(roomType.getTypeName());
+        vo.setFreeNum(enableNum >= 0 ? enableNum : 0);
+        vo.setDiscountPrice(roomType.getDiscountPrice());
+        vo.setDeposit(systemSetting.getDeposit());
+        vo.setPriceOfWater(systemSetting.getPriceOfWater());
+        vo.setPriceOfElectric(systemSetting.getPriceOfElectric());
+        vo.setFreeQuotaOfElectric(systemSetting.getPriceOfElectric());
+        vo.setFreeQuotaOfWater(systemSetting.getFreeQuotaOfWater());
+        vo.setFreeQuotaOfElectric(systemSetting.getFreeQuotaOfElectric());
+        vo.setFreeTotal(systemSetting.getFreeTotal());
+        vo.setTotalAmount(totalAmount);
+
+        return vo;
+    }
+
+    @Transactional
+    @Override
+    public OrderSubmitVO submitOrder(String userToken, SubmitOrderRequest request) {
+        LoginUserVO loginUserVO = LoginInterceptor.threadLocal.get();
+        String cardNumber = loginUserVO.getCardNumber();
+
+        // 身份校验
+        HotelUserEntity hotelUser = hotelUserService.findByCardNumber(cardNumber);
+        if (hotelUser == null) {
+            throw new RRException(BizCodeEnume.PERMISSION_DENIED, "非法用户");
+        }
+        String identityType = hotelUser.getIdentityType();
+        if (!UserIdentityTypeEnum.STAFF_OF_TEACHER.getCode().equals(identityType)) {
+            throw new RRException(BizCodeEnume.PERMISSION_DENIED, "非法用户-非教职工");
+        }
+
+        SystemSettingEntity systemSetting = systemSettingService.get();
+        // 入住时间校验
+        Integer preDay = systemSetting.getPreDay();
+        if (Math.abs(ChronoUnit.DAYS.between(request.getEnableStartTime().toLocalDate(), LocalDate.now())) > preDay) {
+            throw new RRException(BizCodeEnume.PARAMETER_ERROR, "只能预定" + preDay + "天之内的房源");
+        }
+
+        // 校验房型
+        RoomTypeEntity roomType = roomTypeService.myGetById(request.getRoomTypeId());
+        if (roomType == null) {
+            throw new RRException(BizCodeEnume.PARAMETER_ERROR, "房型不存在");
+        }
+
+        // 金额校验
+        BigDecimal discountPrice = roomType.getDiscountPrice();
+        BigDecimal deposit = systemSetting.getDeposit();
+        long days = ChronoUnit.DAYS.between(request.getEnableStartTime().toLocalDate(), request.getEnableEndTime().toLocalDate());
+        BigDecimal totalAmount = discountPrice.add(deposit).multiply(new BigDecimal(Long.toString(days)));
+        if (Math.abs(totalAmount.subtract(request.getPayAmount()).doubleValue()) > 0.01) {
+            throw new RRException(BizCodeEnume.ORDER_SUBMIT_ERROR, "订单金额校验失败");
+        }
+
+        // 锁定房源
+        long orderId = IdWorker.getId();
+        LockRoomDTO lockRoomDTO = LockRoomDTO.builder()
+                .roomTypeId(roomType.getId())
+                .bizId(orderId)
+                .startTime(request.getEnableStartTime())
+                .endTime(request.getEnableEndTime())
+                .build();
+        Long roomId = roomRealtimeStatuService.lockRoomByRoomTypeId(lockRoomDTO);
+        if (roomId == null) {
+            throw new RRException(BizCodeEnume.ORDER_SUBMIT_ERROR, "【" + roomType.getTypeName() + "】房型房源不足,锁定房源失败");
+        }
+
+        RoomEntity room = roomService.getById(roomId);
+        if (room == null) {
+            throw new RRException(BizCodeEnume.UNKNOW_EXCEPTION, "系统异常,根据id查询房间失败");
+        }
+
+        // 订单落库
+        HotelOrderEntity hotelOrder = new HotelOrderEntity();
+        BeanUtils.copyProperties(request, hotelOrder);
+        hotelOrder.setId(orderId);
+        hotelOrder.setOrderNo(IdWorker.getTimeId());
+        hotelOrder.setUserFlag(cardNumber);
+        hotelOrder.setRoomId(roomId);
+        hotelOrder.setRoomNo(room.getRoomNo());
+        hotelOrder.setRoomTypeName(roomType.getTypeName());
+        hotelOrder.setCreateTime(LocalDateTime.now());
+        hotelOrder.setOrderStatu(OrderStatuEnum.WAIT_PAY.getCode());
+        hotelOrder.setBizStatu(OrderBillStatuEnum.WAIT_HANDLE.getCode());
+        hotelOrder.setDeleteFlag("1"); // 0删除、1正常
+        this.save(hotelOrder);
+
+        // 发送mq信息 TODO
+
+        String payUrl = String.format(jxnxsPayConfig.getPayUrl(), hotelOrder.getOrderNo(), request.getPayAmount());
+        return OrderSubmitVO.builder().orderId(hotelOrder.getId()).payUrl(payUrl).build();
+    }
+
 }

+ 22 - 0
src/main/java/com/chuanghai/ihotel/service/impl/HotelUserServiceImpl.java

@@ -0,0 +1,22 @@
+package com.chuanghai.ihotel.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.chuanghai.ihotel.dao.HotelUserDao;
+import com.chuanghai.ihotel.entity.HotelUserEntity;
+import com.chuanghai.ihotel.service.HotelUserService;
+import org.springframework.stereotype.Service;
+
+
+@Service("hotelUserService")
+public class HotelUserServiceImpl extends ServiceImpl<HotelUserDao, HotelUserEntity> implements HotelUserService {
+
+    @Override
+    public HotelUserEntity findByCardNumber(String cardNumber) {
+        QueryWrapper<HotelUserEntity> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("card_number", cardNumber);
+        queryWrapper.eq("statu", "1"); // 1正常用户,0冻结用户
+        queryWrapper.last("limit 1");
+        return this.getOne(queryWrapper);
+    }
+}

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

@@ -3,20 +3,39 @@ package com.chuanghai.ihotel.service.impl;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.chuanghai.ihotel.common.exception.BizCodeEnume;
+import com.chuanghai.ihotel.common.exception.RRException;
 import com.chuanghai.ihotel.common.utils.MyQuery;
 import com.chuanghai.ihotel.common.utils.PageParam;
 import com.chuanghai.ihotel.common.utils.PageUtils;
+import com.chuanghai.ihotel.constant.RedisKey;
 import com.chuanghai.ihotel.dao.RoomRealtimeStatuDao;
+import com.chuanghai.ihotel.dto.LockRoomDTO;
 import com.chuanghai.ihotel.entity.RoomRealtimeStatuEntity;
+import com.chuanghai.ihotel.entity.RoomTypeEntity;
+import com.chuanghai.ihotel.enums.RoomStatuEnum;
 import com.chuanghai.ihotel.service.RoomRealtimeStatuService;
+import com.chuanghai.ihotel.service.RoomService;
+import com.chuanghai.ihotel.service.RoomTypeService;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.time.LocalDateTime;
+import java.util.List;
 
 
 @Service("roomRealtimeStatuService")
 public class RoomRealtimeStatuServiceImpl extends ServiceImpl<RoomRealtimeStatuDao, RoomRealtimeStatuEntity> implements RoomRealtimeStatuService {
 
+    @Autowired
+    private RedissonClient redissonClient;
+    @Autowired
+    private RoomTypeService roomTypeService;
+    @Autowired
+    private RoomService roomService;
+
     @Override
     public PageUtils queryPage(PageParam pageParam) {
         IPage<RoomRealtimeStatuEntity> page = this.page(
@@ -32,4 +51,71 @@ public class RoomRealtimeStatuServiceImpl extends ServiceImpl<RoomRealtimeStatuD
         return this.getBaseMapper().getBusyNum(roomTypeId, startTime, endTime);
     }
 
+    @Override
+    public Long lockRoomByRoomTypeId(LockRoomDTO lockRoomDTO) {
+        Long roomTypeId = lockRoomDTO.getRoomTypeId();
+        LocalDateTime startTime = lockRoomDTO.getStartTime();
+        LocalDateTime endTime = lockRoomDTO.getEndTime();
+        RoomTypeEntity roomType = roomTypeService.myGetById(roomTypeId);
+        if (roomType == null) {
+            throw new RRException(BizCodeEnume.PARAMETER_ERROR, "房型不存在");
+        }
+
+        String key = String.format(RedisKey.LOCK_ROOM_BY_ROOM_TYPE, roomTypeId);
+        RLock lock = redissonClient.getLock(key);
+        lock.lock();
+        try {
+            // 校验房间数量
+            int busyNum = getBusyNum(roomTypeId, startTime, endTime);
+            if (roomType.getRoomNum() - busyNum <= 0) {
+                return null;
+            }
+
+            // 随机选房
+            Long roomId = randomSelectRoom(lockRoomDTO);
+            if (roomId == null) {
+                return null;
+            }
+
+            // 锁定房间
+            RoomRealtimeStatuEntity roomRealtimeStatu = new RoomRealtimeStatuEntity();
+            roomRealtimeStatu.setRoomId(roomId);
+            roomRealtimeStatu.setRoomTypeId(roomTypeId);
+            roomRealtimeStatu.setStartTime(startTime);
+            roomRealtimeStatu.setEndTime(endTime);
+            roomRealtimeStatu.setBizId(lockRoomDTO.getBizId());
+            roomRealtimeStatu.setStatu(RoomStatuEnum.ORDER.getCode());
+            this.save(roomRealtimeStatu);
+
+            return roomId;
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * 随机根据房型选房
+     * @param lockRoomDTO 锁房参数
+     * @return
+     */
+    private Long randomSelectRoom(LockRoomDTO lockRoomDTO) {
+        Long roomId = lockRoomDTO.getRoomId();
+        Long roomTypeId = lockRoomDTO.getRoomTypeId();
+        // 查询当前时间段内已经被预定的房间id
+        List<Long> busyRoomIds = getBusyRoomId(roomTypeId, lockRoomDTO.getStartTime(), lockRoomDTO.getEndTime());
+        if (roomId != null) { // 房间id不为空,优先预定该房间
+            if (!busyRoomIds.contains(roomId)) {
+                return roomId;
+            }
+        }
+        // 根据房型随机选择,查询不在已预定列表中的房间id
+        roomId = roomService.getRoomIdNotIn(roomTypeId, busyRoomIds);
+
+        return roomId;
+    }
+
+    private List<Long> getBusyRoomId(Long roomTypeId, LocalDateTime startTime, LocalDateTime endTime) {
+        return this.getBaseMapper().getBusyRoomId(roomTypeId, startTime, endTime);
+    }
+
 }

+ 15 - 0
src/main/java/com/chuanghai/ihotel/service/impl/RoomServiceImpl.java

@@ -21,6 +21,7 @@ import com.chuanghai.ihotel.entity.RoomEntity;
 import com.chuanghai.ihotel.service.RoomService;
 import org.springframework.util.StringUtils;
 
+import java.util.List;
 import java.util.Map;
 
 
@@ -83,4 +84,18 @@ public class RoomServiceImpl extends ServiceImpl<RoomDao, RoomEntity> implements
 
         this.save(room);
     }
+
+    @Override
+    public Long getRoomIdNotIn(Long roomTypeId, List<Long> roomIds) {
+        QueryWrapper<RoomEntity> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("room_type_id", roomTypeId);
+        queryWrapper.notIn(roomIds.size()> 0, "id", roomIds);
+        queryWrapper.last("limit 1");
+        RoomEntity room = this.getOne(queryWrapper);
+        if (room != null) {
+            return room.getId();
+        } else {
+            return null;
+        }
+    }
 }

+ 8 - 1
src/main/java/com/chuanghai/ihotel/service/impl/RoomTypeServiceImpl.java

@@ -38,7 +38,7 @@ public class RoomTypeServiceImpl extends ServiceImpl<RoomTypeDao, RoomTypeEntity
         return new PageUtils(page);
     }
 
-    @Cacheable(value = {"roomType"},key = "#root.method.name + '-' + #request.startTime + '-' + #request.endTime")
+    @Cacheable(value = {"roomType"}, key = "#root.method.name + '-' + #request.startTime + '-' + #request.endTime")
     @Override
     public List<RoomTypeShortDescVO> listForClientIndex(RoomTypeQueryRequest request) {
         List<RoomTypeEntity> all = this.list();
@@ -59,4 +59,11 @@ public class RoomTypeServiceImpl extends ServiceImpl<RoomTypeDao, RoomTypeEntity
         return vos;
     }
 
+    @Cacheable(value = {"roomType"}, key = "#root.method.name + '-' + #roomTypeId")
+    @Override
+    public RoomTypeEntity myGetById(Long roomTypeId) {
+        RoomTypeEntity roomType = getById(roomTypeId);
+        return roomType;
+    }
+
 }

+ 12 - 4
src/main/java/com/chuanghai/ihotel/service/impl/SystemSettingServiceImpl.java

@@ -1,29 +1,37 @@
 package com.chuanghai.ihotel.service.impl;
 
-import org.springframework.stereotype.Service;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-import com.chuanghai.ihotel.common.utils.PageUtils;
 import com.chuanghai.ihotel.common.utils.MyQuery;
 import com.chuanghai.ihotel.common.utils.PageParam;
-
+import com.chuanghai.ihotel.common.utils.PageUtils;
 import com.chuanghai.ihotel.dao.SystemSettingDao;
 import com.chuanghai.ihotel.entity.SystemSettingEntity;
 import com.chuanghai.ihotel.service.SystemSettingService;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
 
 
 @Service("systemSettingService")
 public class SystemSettingServiceImpl extends ServiceImpl<SystemSettingDao, SystemSettingEntity> implements SystemSettingService {
 
+
+
     @Override
     public PageUtils queryPage(PageParam pageParam) {
         IPage<SystemSettingEntity> page = this.page(
                 new MyQuery<SystemSettingEntity>().getPage(pageParam),
-                new QueryWrapper<SystemSettingEntity>()
+                new QueryWrapper<>()
         );
 
         return new PageUtils(page);
     }
 
+    @Cacheable(value = {"systemSetting"},key = "#root.method.name")
+    @Override
+    public SystemSettingEntity get() {
+        SystemSettingEntity systemSetting = this.getOne(null);
+        return systemSetting;
+    }
 }

+ 38 - 0
src/main/java/com/chuanghai/ihotel/util/BigDecimalSerializer.java

@@ -0,0 +1,38 @@
+package com.chuanghai.ihotel.util;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.core.type.WritableTypeId;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+/**
+ * @Author: codingliang
+ * @Description: BigDecimal 序列化
+ * @Date: 2022-07-28 15:44
+ * @Version: V1.0
+ **/
+public class BigDecimalSerializer extends JsonSerializer<BigDecimal> {
+    @Override
+    public void serialize(BigDecimal value, JsonGenerator generator, SerializerProvider serializerProvider) throws IOException {
+        if (value != null) {
+            // 保留2位小数,四舍五入
+            BigDecimal number = value.setScale(2, RoundingMode.HALF_UP);
+            generator.writeNumber(number);
+        } else {
+            generator.writeNumber(value);
+        }
+    }
+
+    @Override
+    public void serializeWithType(BigDecimal value, JsonGenerator gen, SerializerProvider serializers, TypeSerializer typeSer) throws IOException {
+        WritableTypeId typeId = typeSer.writeTypePrefix(gen, typeSer.typeId(value, JsonToken.VALUE_STRING));
+        serialize(value, gen, serializers);
+        typeSer.writeTypeSuffix(gen, typeId);
+    }
+}

+ 181 - 0
src/main/java/com/chuanghai/ihotel/util/CommonUtil.java

@@ -0,0 +1,181 @@
+package com.chuanghai.ihotel.util;
+
+import lombok.extern.slf4j.Slf4j;
+import redis.clients.jedis.util.Hashing;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.MessageDigest;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import java.util.UUID;
+
+/**
+ * 公共工具类
+ **/
+@Slf4j
+public class CommonUtil {
+    /**
+     * 获取ip
+     *
+     * @param request
+     * @return
+     */
+    public static String getIpAddr(HttpServletRequest request) {
+        String ipAddress = null;
+        try {
+            ipAddress = request.getHeader("x-forwarded-for");
+            System.out.println("x-forwarded-for="+ipAddress);
+            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+                ipAddress = request.getHeader("Proxy-Client-IP");
+                System.out.println("Proxy-Client-IP="+ipAddress);
+            }
+            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+                ipAddress = request.getHeader("WL-Proxy-Client-IP");
+                System.out.println("WL-Proxy-Client-IP="+ipAddress);
+            }
+            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+                ipAddress = request.getRemoteAddr();
+                System.out.println("getRemoteAddr="+ipAddress);
+                if (ipAddress.equals("127.0.0.1")) {
+                    // 根据网卡取本机配置的IP
+                    InetAddress inet = null;
+                    try {
+                        inet = InetAddress.getLocalHost();
+                    } catch (UnknownHostException e) {
+                        e.printStackTrace();
+                    }
+                    ipAddress = inet.getHostAddress();
+                    System.out.println("getHostAddress="+ipAddress);
+                }
+            }
+            // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
+            if (ipAddress != null && ipAddress.length() > 15) {
+                // "***.***.***.***".length()
+                // = 15
+                if (ipAddress.indexOf(",") > 0) {
+                    ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
+                }
+            }
+        } catch (Exception e) {
+            ipAddress = "";
+        }
+        return ipAddress;
+    }
+
+
+    /**
+     * 获取全部请求头
+     *
+     * @param request
+     * @return
+     */
+    public static Map<String, String> getAllRequestHeader(HttpServletRequest request) {
+        Enumeration<String> headerNames = request.getHeaderNames();
+        Map<String, String> map = new HashMap<>();
+        while (headerNames.hasMoreElements()) {
+            String key = headerNames.nextElement();
+            //根据名称获取请求头的值
+            String value = request.getHeader(key);
+            map.put(key, value);
+        }
+
+        return map;
+    }
+
+
+    /**
+     * MD5加密
+     *
+     * @param data
+     * @return
+     */
+    public static String MD5(String data) {
+        try {
+            MessageDigest md = MessageDigest.getInstance("MD5");
+            byte[] array = md.digest(data.getBytes("UTF-8"));
+            StringBuilder sb = new StringBuilder();
+            for (byte item : array) {
+                sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
+            }
+
+            return sb.toString().toUpperCase();
+        } catch (Exception exception) {
+        }
+        return null;
+    }
+
+
+    /**
+     * 获取验证码随机数
+     *
+     * @param length
+     * @return
+     */
+    public static String getRandomCode(int length) {
+        String sources = "0123456789";
+        Random random = new Random();
+        StringBuilder sb = new StringBuilder();
+        for (int j = 0; j < length; j++) {
+            sb.append(sources.charAt(random.nextInt(9)));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 获取当前时间戳
+     *
+     * @return
+     */
+    public static long getCurrentTimestamp() {
+        return System.currentTimeMillis();
+    }
+
+    /**
+     * 生成uuid
+     *
+     * @return
+     */
+    public static String generateUUID() {
+        return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
+    }
+
+    /**
+     * 获取随机长度的串
+     *
+     * @param length
+     * @return
+     */
+    private static final String ALL_CHAR_NUM = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+
+    public static String getStringNumRandom(int length) {
+        //生成随机数字和字母,
+        Random random = new Random();
+        StringBuilder saltString = new StringBuilder(length);
+        for (int i = 1; i <= length; ++i) {
+            saltString.append(ALL_CHAR_NUM.charAt(random.nextInt(ALL_CHAR_NUM.length())));
+        }
+        return saltString.toString();
+    }
+
+    /**
+     * LocalDate 拼接时间转成 LocalDateTime
+     * @param localDate
+     * @param timeStr HH:mm:ss
+     * @return
+     */
+    public static LocalDateTime localDateToTime(LocalDate localDate, String timeStr) {
+        LocalTime time = LocalTime.parse(timeStr);
+        return LocalDateTime.of(localDate.getYear(), localDate.getMonth(), localDate.getDayOfMonth(),
+                time.getHour(), time.getMinute(), time.getSecond());
+    }
+}

+ 80 - 0
src/main/java/com/chuanghai/ihotel/util/JWTUtil.java

@@ -0,0 +1,80 @@
+package com.chuanghai.ihotel.util;
+
+import com.chuanghai.ihotel.vo.LoginUserVO;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Date;
+
+/**
+ * jwt 工具
+ **/
+@Slf4j
+public class JWTUtil {
+
+    /**
+     * 主题
+     */
+    private static final String SUBJECT = "chuanghai-ihotel";
+
+    /**
+     * 加密密钥
+     */
+    private static final String SECRET = "chuanghai.ihotel";
+
+    /**
+     * 令牌前缀
+     */
+    private static final String TOKEN_PREFIX = "ihotel";
+
+
+    /**
+     * token过期时间,7天
+     */
+    private static final long EXPIRED = 1000 * 60 * 60 * 24 * 7;
+
+
+    /**
+     * 生成token
+     *
+     * @param loginUser
+     * @return
+     */
+    public static String geneJsonWebTokne(LoginUserVO loginUser) {
+
+        if (loginUser == null) {
+            throw new NullPointerException("对象为空");
+        }
+
+        String token = Jwts.builder().setSubject(SUBJECT)
+                //配置payload
+                .claim("card_number", loginUser.getCardNumber())
+                .claim("identity_type", loginUser.getIdentityType())
+                .setIssuedAt(new Date())
+                .setExpiration(new Date(CommonUtil.getCurrentTimestamp() + EXPIRED))
+                .signWith(SignatureAlgorithm.HS256, SECRET).compact();
+
+        token = TOKEN_PREFIX + token;
+        return token;
+    }
+
+    /**
+     * 解密jwt
+     * @param token
+     * @return
+     */
+    public static Claims checkJWT(String token) {
+        try {
+            final Claims claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token.replace(TOKEN_PREFIX, "")).getBody();
+            return claims;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    public static void main(String[] args) {
+        System.out.println(geneJsonWebTokne(LoginUserVO.builder().cardNumber("123456").identityType("1").build()));
+    }
+}

+ 86 - 0
src/main/java/com/chuanghai/ihotel/vo/ConfirmOrderVO.java

@@ -0,0 +1,86 @@
+package com.chuanghai.ihotel.vo;
+
+import com.chuanghai.ihotel.util.BigDecimalSerializer;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * @Author: codingliang
+ * @Description: 确认订单vo
+ * @Date: 2022-07-28 9:41
+ * @Version: V1.0
+ **/
+@Data
+public class ConfirmOrderVO {
+
+    /**
+     * 入住时间 yyyy-MM-dd HH:mm:ss
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime startTime;
+    /**
+     * 离店时间 yyyy-MM-dd HH:mm:ss
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime endTime;
+    /**
+     * 入住天数
+     */
+    private Integer nums;
+    /**
+     * 房间类型id
+     */
+    private Long roomTypeId;
+    /**
+     * 房间类型名称
+     */
+    private String roomTypeName;
+    /**
+     * 可预定房间数量,为0表示不可定
+     */
+    private Integer freeNum;
+    /**
+     * 优惠价格 单位:元
+     */
+    @JsonSerialize(using = BigDecimalSerializer.class)
+    private BigDecimal discountPrice;
+    /**
+     * 押金 单位:元
+     */
+    @JsonSerialize(using = BigDecimalSerializer.class)
+    private BigDecimal deposit;
+    /**
+     * 需付总金额 (优惠价格+押金)*nums
+     */
+    @JsonSerialize(using = BigDecimalSerializer.class)
+    private BigDecimal totalAmount;
+    /**
+     * 水费 单位:元/吨
+     */
+    @JsonSerialize(using = BigDecimalSerializer.class)
+    private BigDecimal priceOfWater;
+    /**
+     * 电费 单位:元/度
+     */
+    @JsonSerialize(using = BigDecimalSerializer.class)
+    private BigDecimal priceOfElectric;
+    /**
+     * 水免费额度 单位:吨
+     */
+    @JsonSerialize(using = BigDecimalSerializer.class)
+    private BigDecimal freeQuotaOfWater;
+    /**
+     * 电免费额度 单位:度
+     */
+    @JsonSerialize(using = BigDecimalSerializer.class)
+    private BigDecimal freeQuotaOfElectric;
+    /**
+     * 水电免费额度总金额 单位:元
+     */
+    @JsonSerialize(using = BigDecimalSerializer.class)
+    private BigDecimal freeTotal;
+}

+ 24 - 0
src/main/java/com/chuanghai/ihotel/vo/LoginUserVO.java

@@ -0,0 +1,24 @@
+package com.chuanghai.ihotel.vo;
+
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * @Author: codingliang
+ * @Description: 登录用户
+ * @Date: 2022-07-28 10:24
+ * @Version: V1.0
+ **/
+@Data
+@Builder
+public class LoginUserVO {
+
+    /**
+     * 卡号
+     */
+    private String cardNumber;
+    /**
+     * 身份
+     */
+    private String identityType;
+}

+ 25 - 0
src/main/java/com/chuanghai/ihotel/vo/OrderSubmitTokenVO.java

@@ -0,0 +1,25 @@
+package com.chuanghai.ihotel.vo;
+
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * @Author: codingliang
+ * @Description: 订单提交token
+ * @Date: 2022-07-28 11:01
+ * @Version: V1.0
+ **/
+@Data
+@Builder
+public class OrderSubmitTokenVO {
+
+    /**
+     * 提交token
+     */
+    private String submitToken;
+
+    /**
+     * 过期时间 单位:ms
+     */
+    private Long expire;
+}

+ 24 - 0
src/main/java/com/chuanghai/ihotel/vo/OrderSubmitVO.java

@@ -0,0 +1,24 @@
+package com.chuanghai.ihotel.vo;
+
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * @Author: codingliang
+ * @Description: 订单提交vo
+ * @Date: 2022-07-28 16:19
+ * @Version: V1.0
+ **/
+@Data
+@Builder
+public class OrderSubmitVO {
+
+    /**
+     * 订单id
+     */
+    private Long orderId;
+    /**
+     * 支付地址
+     */
+    private String payUrl;
+}

+ 4 - 3
src/main/java/com/chuanghai/ihotel/vo/RoomTypeShortDescVO.java

@@ -1,6 +1,7 @@
 package com.chuanghai.ihotel.vo;
 
-import com.fasterxml.jackson.annotation.JsonFormat;
+import com.chuanghai.ihotel.util.BigDecimalSerializer;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
 import lombok.Data;
 
 import java.math.BigDecimal;
@@ -24,12 +25,12 @@ public class RoomTypeShortDescVO {
     /**
      * 日常价格 单位:元/间/晚
      */
-    @JsonFormat(shape = JsonFormat.Shape.STRING)
+    @JsonSerialize(using = BigDecimalSerializer.class)
     private BigDecimal usualPrice;
     /**
      * 优惠价格 单位:元/间/晚
      */
-    @JsonFormat(shape = JsonFormat.Shape.STRING)
+    @JsonSerialize(using = BigDecimalSerializer.class)
     private BigDecimal discountPrice;
     /**
      * 房型简单描述

+ 16 - 0
src/main/resources/application.yml

@@ -6,6 +6,8 @@ server:
 spring:
   application:
     name: ihotel
+  main:
+    allow-circular-references: true
   servlet:
     multipart:
       max-file-size: 30MB
@@ -24,6 +26,13 @@ spring:
       cache-null-values: true
   redis:
     host: 192.168.126.131
+    port: 6379
+    jedis:
+      pool:
+        max-active: 100
+        max-idle: 100
+        min-idle: 100
+        max-wait: 60000
 #  rabbitmq:
 #    host: 192.168.126.131
 #    port: 5672
@@ -49,3 +58,10 @@ aliyun:
     access-key: LTAI5tNSisG8sUJppiMKq2Po
     secret-key: bLAQ2325jIEIm2NOQC0UWFit2QUhUS
 
+# 江西农商行相关配置
+jxnxs:
+  openId: 5494ec3310685daa218382619dd20e27
+  openKey: 611764d05a545bb7bb08cbd457f7dac1
+  notifyUrl: http://chuanghai-dev.natappfree.cc/pay/jxnxs/notify
+  payUrl: https://q.jxnxs.com/newpay?O=${jxnxs.openId}&out_no=%s&amount=%s&appoint_notify=${jxnxs.notifyUrl}
+

+ 14 - 0
src/main/resources/mapper/ihotel/HotelUserDao.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+
+<mapper namespace="com.chuanghai.ihotel.dao.HotelOderRefundDao">
+
+	<!-- 可根据自己的需求,是否要使用 -->
+    <resultMap type="com.chuanghai.ihotel.entity.HotelUserEntity" id="hoteluserMap">
+        <result property="id" column="id"/>
+        <result property="cardNumber" column="card_number"/>
+        <result property="name" column="name"/>
+        <result property="identityType" column="identity_type"/>
+        <result property="statu" column="statu"/>
+    </resultMap>
+</mapper>

+ 9 - 0
src/main/resources/mapper/ihotel/RoomRealtimeStatuDao.xml

@@ -23,4 +23,13 @@
             and statu != '1'
             and not(end_time <![CDATA[<]]> #{startTime} or start_time <![CDATA[>]]> #{endTime})
     </select>
+
+    <select id="getBusyRoomId" resultType="java.lang.Long">
+        select room_id
+        from room_realtime_statu
+        where
+            room_type_id = #{roomTypeId}
+          and statu != '1'
+          and not(end_time <![CDATA[<]]> #{startTime} or start_time <![CDATA[>]]> #{endTime})
+    </select>
 </mapper>

+ 2 - 0
src/main/resources/mapper/ihotel/SystemSettingDao.xml

@@ -8,6 +8,8 @@
         <result property="id" column="id"/>
         <result property="deposit" column="deposit"/>
         <result property="preDay" column="pre_day"/>
+        <result property="priceOfWater" column="price_of_water"/>
+        <result property="priceOfElectric" column="price_of_electric"/>
         <result property="freeQuotaOfWater" column="free_quota_of_water"/>
         <result property="freeQuotaOfElectric" column="free_quota_of_electric"/>
         <result property="freeTotal" column="free_total"/>