Просмотр исходного кода

Merge branch 'dev' of http://git.izouma.com/xiongzhu/9th into dev

panhui 4 лет назад
Родитель
Сommit
cb30ec6ff3
50 измененных файлов с 1327 добавлено и 454 удалено
  1. 43 13
      src/main/comos/src/views/Submit.vue
  2. 3 0
      src/main/java/com/izouma/nineth/TokenHistory.java
  3. 12 0
      src/main/java/com/izouma/nineth/annotations/Debounce.java
  4. 71 0
      src/main/java/com/izouma/nineth/aspect/DebounceAspect.java
  5. 0 188
      src/main/java/com/izouma/nineth/aspect/OperLogAspect.java
  6. 33 0
      src/main/java/com/izouma/nineth/aspect/debounce/DebounceTask.java
  7. 2 0
      src/main/java/com/izouma/nineth/config/GeneralProperties.java
  8. 21 0
      src/main/java/com/izouma/nineth/config/RedisKeys.java
  9. 5 2
      src/main/java/com/izouma/nineth/config/SnowflakeIdWorkerConfig.java
  10. 7 0
      src/main/java/com/izouma/nineth/domain/Asset.java
  11. 2 3
      src/main/java/com/izouma/nineth/domain/Banner.java
  12. 89 0
      src/main/java/com/izouma/nineth/domain/BaseEntityNoID.java
  13. 6 3
      src/main/java/com/izouma/nineth/domain/BlindBoxItem.java
  14. 7 1
      src/main/java/com/izouma/nineth/domain/Collection.java
  15. 24 0
      src/main/java/com/izouma/nineth/domain/ErrorOrder.java
  16. 3 1
      src/main/java/com/izouma/nineth/domain/Like.java
  17. 4 0
      src/main/java/com/izouma/nineth/domain/MerProperty.java
  18. 14 2
      src/main/java/com/izouma/nineth/domain/Order.java
  19. 6 0
      src/main/java/com/izouma/nineth/domain/SmsRecord.java
  20. 10 0
      src/main/java/com/izouma/nineth/domain/User.java
  21. 2 0
      src/main/java/com/izouma/nineth/dto/UserBankCard.java
  22. 2 0
      src/main/java/com/izouma/nineth/dto/UserRegister.java
  23. 28 0
      src/main/java/com/izouma/nineth/event/CreateOrderEvent.java
  24. 21 0
      src/main/java/com/izouma/nineth/event/OrderNotifyEvent.java
  25. 52 0
      src/main/java/com/izouma/nineth/listener/CreateOrderListener.java
  26. 27 0
      src/main/java/com/izouma/nineth/listener/OrderNotifyListener.java
  27. 28 0
      src/main/java/com/izouma/nineth/listener/UpdateSaleListener.java
  28. 27 0
      src/main/java/com/izouma/nineth/listener/UpdateStockListener.java
  29. 5 0
      src/main/java/com/izouma/nineth/repo/BlindBoxItemRepo.java
  30. 18 0
      src/main/java/com/izouma/nineth/repo/CollectionRepo.java
  31. 7 0
      src/main/java/com/izouma/nineth/repo/ErrorOrderRepo.java
  32. 2 0
      src/main/java/com/izouma/nineth/repo/UserRepo.java
  33. 77 14
      src/main/java/com/izouma/nineth/service/CollectionService.java
  34. 291 152
      src/main/java/com/izouma/nineth/service/OrderService.java
  35. 128 22
      src/main/java/com/izouma/nineth/service/UserService.java
  36. 28 0
      src/main/java/com/izouma/nineth/utils/AdapayUtils.java
  37. 11 7
      src/main/java/com/izouma/nineth/web/AssetController.java
  38. 2 2
      src/main/java/com/izouma/nineth/web/AuthenticationController.java
  39. 19 2
      src/main/java/com/izouma/nineth/web/OrderController.java
  40. 24 6
      src/main/java/com/izouma/nineth/web/OrderNotifyController.java
  41. 13 7
      src/main/java/com/izouma/nineth/web/OrderPayController.java
  42. 8 0
      src/main/java/com/izouma/nineth/web/UserController.java
  43. 43 13
      src/main/nine-space/src/views/Submit.vue
  44. 14 3
      src/main/resources/application.yaml
  45. 16 0
      src/main/resources/templates/PayError.ftlh
  46. 1 1
      src/main/vue/src/mixins/pageableTable.js
  47. 31 6
      src/main/vue/src/views/UserList.vue
  48. 30 4
      src/test/java/com/izouma/nineth/service/AdapayTest.java
  49. 0 2
      src/test/java/com/izouma/nineth/service/OrderServiceTest.java
  50. 10 0
      src/test/java/com/izouma/nineth/service/UserServiceTest.java

+ 43 - 13
src/main/comos/src/views/Submit.vue

@@ -109,7 +109,8 @@ export default {
             enable_wx_pub: false,
             launchName: '',
             launchPath: '',
-            iosPrice: 0
+            iosPrice: 0,
+            createOrderTimer: null
         };
     },
     computed: {
@@ -132,6 +133,9 @@ export default {
     },
     beforeRouteLeave(to, from, next) {
         console.log(to);
+        if (this.createOrderTimer) {
+            clearInterval(this.createOrderTimer);
+        }
         if (to.path !== '/couponList') {
             this.$store.commit('setCouponInfo', null);
         }
@@ -250,22 +254,48 @@ export default {
                     });
                 });
         },
-        submit() {
+        createOrder() {
             if (!this.payType) {
                 this.$toast('请选择支付方式');
                 return;
             }
-            this.$toast.loading('加载中');
-            let url = '/order/create?collectionId=' + this.$route.query.id + '&qty=1';
-            if (this.couponInfo) {
-                url += '&couponId=' + this.couponInfo.id;
-            }
-            let invitor = sessionStorage.getItem('invitor');
-            if (invitor) {
-                url += '&invitor=' + invitor;
-            }
-            this.$http
-                .post(url)
+            this.$toast.loading('请不要离开当前页面');
+
+            let params = {
+                collectionId: this.$route.query.id,
+                qty: 1,
+                couponId: (this.couponInfo || {}).id || '',
+                invitor: sessionStorage.getItem('invitor')
+            };
+            return this.$http.post('/order/mqCreate', params).then(res => {
+                return new Promise((resolve, reject) => {
+                    let checkOrder = () => {
+                        this.$http
+                            .get('/order/createResult', { id: res.id })
+                            .then(res => {
+                                if (res) {
+                                    clearInterval(this.createOrderTimer);
+                                    this.createOrderTimer = null;
+                                    if (res.success) {
+                                        resolve(res.data);
+                                    } else {
+                                        reject({ error: res.data });
+                                    }
+                                }
+                            })
+                            .catch(e => {
+                                clearInterval(this.createOrderTimer);
+                                this.createOrderTimer = null;
+                                reject(e);
+                            });
+                    };
+                    setTimeout(checkOrder, 500);
+                    this.createOrderTimer = setInterval(checkOrder, 2000);
+                });
+            });
+        },
+        submit() {
+            this.createOrder()
                 .then(res => {
                     if (this.money) {
                         this.$toast.clear();

+ 3 - 0
src/main/java/com/izouma/nineth/TokenHistory.java

@@ -8,10 +8,13 @@ import lombok.NoArgsConstructor;
 
 import javax.persistence.Column;
 import javax.persistence.Entity;
+import javax.persistence.Index;
+import javax.persistence.Table;
 import java.math.BigDecimal;
 
 @Data
 @Entity
+@Table(indexes = {@Index(columnList = "tokenId")})
 @AllArgsConstructor
 @NoArgsConstructor
 @Builder

+ 12 - 0
src/main/java/com/izouma/nineth/annotations/Debounce.java

@@ -0,0 +1,12 @@
+package com.izouma.nineth.annotations;
+
+import java.lang.annotation.*;
+
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Debounce {
+    String key();
+
+    long delay() default 200L;
+}

+ 71 - 0
src/main/java/com/izouma/nineth/aspect/DebounceAspect.java

@@ -0,0 +1,71 @@
+package com.izouma.nineth.aspect;
+
+import com.izouma.nineth.annotations.Debounce;
+import com.izouma.nineth.aspect.debounce.DebounceTask;
+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.springframework.core.DefaultParameterNameDiscoverer;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.ExpressionParser;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+@Aspect
+@Component
+@Slf4j
+public class DebounceAspect {
+
+    private DefaultParameterNameDiscoverer nameDiscoverer  = new DefaultParameterNameDiscoverer();
+    private HashMap<String, Future<Void>>  debounceStore   = new HashMap<>();
+    private ScheduledExecutorService       executorService = Executors.newScheduledThreadPool(10);
+    private Map<String, Long>              debounceCounter = new HashMap<>();
+
+    @Pointcut("@annotation(com.izouma.nineth.annotations.Debounce)")
+    public void debouncePointCut() {
+    }
+
+    @Around(value = "debouncePointCut() && @annotation(debounce)")
+    public synchronized void debounce(ProceedingJoinPoint joinPoint, Debounce debounce) {
+        ExpressionParser parser = new SpelExpressionParser();
+        EvaluationContext context = new StandardEvaluationContext(joinPoint.getSignature());
+        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
+        Method method = methodSignature.getMethod();
+        String[] paramNames = nameDiscoverer.getParameterNames(method);
+        Object[] args = joinPoint.getArgs();
+        for (int i = 0; i < args.length; i++) {
+            context.setVariable(paramNames[i], args[i]);
+        }
+        String key = Optional.ofNullable(parser.parseExpression(debounce.key()).getValue(context)).map(Object::toString)
+                .orElse("default");
+
+        Future<Void> future = debounceStore.get(key);
+        long lastRun = debounceCounter.getOrDefault(key, 0L);
+        if (future != null && !future.isDone()) {
+            if (System.currentTimeMillis() - lastRun > debounce.delay()) {
+                debounceCounter.put(key, System.currentTimeMillis());
+            } else {
+                future.cancel(false);
+            }
+        }
+        debounceStore.put(key, executorService.schedule(new DebounceTask(joinPoint, (Void) -> {
+            debounceCounter.put(key, System.currentTimeMillis());
+            return null;
+        }), debounce.delay(), TimeUnit.MILLISECONDS));
+
+    }
+
+}

+ 0 - 188
src/main/java/com/izouma/nineth/aspect/OperLogAspect.java

@@ -1,188 +0,0 @@
-package com.izouma.nineth.aspect;
-
-import com.alibaba.fastjson.JSON;
-import com.izouma.nineth.annotations.OperLog;
-import com.izouma.nineth.domain.ExceptionLog;
-import com.izouma.nineth.domain.OperationLog;
-import com.izouma.nineth.domain.User;
-import com.izouma.nineth.repo.ExceptionLogRepo;
-import com.izouma.nineth.repo.OperationLogRepo;
-import com.izouma.nineth.utils.IPUtils;
-import com.izouma.nineth.utils.SecurityUtils;
-import org.aspectj.lang.JoinPoint;
-import org.aspectj.lang.annotation.AfterReturning;
-import org.aspectj.lang.annotation.AfterThrowing;
-import org.aspectj.lang.annotation.Aspect;
-import org.aspectj.lang.annotation.Pointcut;
-import org.aspectj.lang.reflect.MethodSignature;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-import org.springframework.web.context.request.RequestAttributes;
-import org.springframework.web.context.request.RequestContextHolder;
-
-import javax.servlet.http.HttpServletRequest;
-import java.lang.reflect.Method;
-import java.time.LocalDateTime;
-import java.util.HashMap;
-import java.util.Map;
-
-@Aspect
-@Component
-public class OperLogAspect {
-
-    @Autowired
-    private OperationLogRepo operationLogRepo;
-
-    @Autowired
-    private ExceptionLogRepo exceptionLogRepo;
-
-    /**
-     * 设置操作日志切入点 记录操作日志 在注解的位置切入代码
-     */
-    @Pointcut("@annotation(com.izouma.nineth.annotations.OperLog)")
-    public void operLogPointCut() {
-    }
-
-    /**
-     * 设置操作异常切入点记录异常日志 扫描所有controller包下操作
-     */
-    @Pointcut("execution(* com.izouma.nineth.web..*.*(..))")
-    public void operExceptionLogPointCut() {
-    }
-
-    /**
-     * 正常返回通知,拦截用户操作日志,连接点正常执行完成后执行, 如果连接点抛出异常,则不会执行
-     *
-     * @param joinPoint 切入点
-     * @param keys      返回结果
-     */
-    @AfterReturning(value = "operLogPointCut()", returning = "keys")
-    public void saveOperLog(JoinPoint joinPoint, Object keys) {
-        // 获取RequestAttributes
-        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
-        // 从获取RequestAttributes中获取HttpServletRequest的信息
-        HttpServletRequest request = (HttpServletRequest) requestAttributes
-                .resolveReference(RequestAttributes.REFERENCE_REQUEST);
-
-        OperationLog operationLog = new OperationLog();
-        try {
-            // 从切面织入点处通过反射机制获取织入点处的方法
-            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
-            // 获取切入点所在的方法
-            Method method = signature.getMethod();
-            // 获取操作
-            OperLog operLog = method.getAnnotation(OperLog.class);
-            if (operLog != null) {
-                operationLog.setName(operLog.value()); // 操作模块
-                operationLog.setType(operLog.type()); // 操作类型
-                operationLog.setDesc(operLog.desc()); // 操作描述
-            }
-            // 获取请求的类名
-            String className = joinPoint.getTarget().getClass().getName();
-            // 获取请求的方法名
-            String methodName = method.getName();
-            methodName = className + "." + methodName;
-
-            operationLog.setReqMethod(methodName);
-
-            // 请求的参数
-            Map<String, String> rtnMap = null;
-            String params = null;
-            if (request != null) {
-                rtnMap = convertMap(request.getParameterMap());
-                params = JSON.toJSONString(rtnMap);
-            }
-
-            operationLog.setReqParams(params);
-            operationLog.setResp(JSON.toJSONString(keys));
-
-            User user = SecurityUtils.getAuthenticatedUser();
-            if (user != null) {
-                operationLog.setUserId(String.valueOf(user.getId()));
-                operationLog.setUsername(user.getUsername());
-            }
-            operationLog.setReqIp(IPUtils.getIpAddr(request));
-            operationLog.setReqUrl(request != null ? request.getRequestURI() : null);
-            operationLog.setTime(LocalDateTime.now());
-            operationLogRepo.save(operationLog);
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
-    }
-
-    /**
-     * 异常返回通知,用于拦截异常日志信息 连接点抛出异常后执行
-     *
-     * @param joinPoint 切入点
-     * @param e         异常信息
-     */
-    @AfterThrowing(pointcut = "operExceptionLogPointCut()", throwing = "e")
-    public void saveExceptionLog(JoinPoint joinPoint, Throwable e) {
-        // 获取RequestAttributes
-        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
-        // 从获取RequestAttributes中获取HttpServletRequest的信息
-        HttpServletRequest request = (HttpServletRequest) requestAttributes
-                .resolveReference(RequestAttributes.REFERENCE_REQUEST);
-
-        ExceptionLog exceptionLog = new ExceptionLog();
-        try {
-            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
-            Method method = signature.getMethod();
-            String className = joinPoint.getTarget().getClass().getName();
-            String methodName = method.getName();
-            methodName = className + "." + methodName;
-            exceptionLog.setReqMethod(methodName);
-
-            Map<String, String> rtnMap = convertMap(request.getParameterMap());
-            String params = JSON.toJSONString(rtnMap);
-            exceptionLog.setReqParams(params);
-
-            exceptionLog.setName(e.getClass().getName());
-            exceptionLog.setMessage(stackTraceToString(e.getClass().getName(), e.getMessage(), e
-                    .getStackTrace()));
-            User user = SecurityUtils.getAuthenticatedUser();
-            if (user != null) {
-                exceptionLog.setUserId(String.valueOf(user.getId()));
-                exceptionLog.setUsername(user.getUsername());
-            }
-
-            exceptionLog.setReqUrl(request.getRequestURI());
-            exceptionLog.setReqIp(IPUtils.getIpAddr(request));
-            exceptionLog.setTime(LocalDateTime.now());
-
-            exceptionLogRepo.save(exceptionLog);
-
-        } catch (Exception e2) {
-            e2.printStackTrace();
-        }
-
-    }
-
-    /**
-     * 转换request 请求参数
-     *
-     * @param paramMap request获取的参数数组
-     */
-    public Map<String, String> convertMap(Map<String, String[]> paramMap) {
-        Map<String, String> rtnMap = new HashMap<>();
-        for (String key : paramMap.keySet()) {
-            rtnMap.put(key, paramMap.get(key)[0]);
-        }
-        return rtnMap;
-    }
-
-    /**
-     * 转换异常信息为字符串
-     *
-     * @param exceptionName    异常名称
-     * @param exceptionMessage 异常信息
-     * @param elements         堆栈信息
-     */
-    public String stackTraceToString(String exceptionName, String exceptionMessage, StackTraceElement[] elements) {
-        StringBuilder strBuff = new StringBuilder();
-        for (StackTraceElement stet : elements) {
-            strBuff.append(stet).append("\n");
-        }
-        return exceptionName + ":" + exceptionMessage + "\n\t" + strBuff;
-    }
-}

+ 33 - 0
src/main/java/com/izouma/nineth/aspect/debounce/DebounceTask.java

@@ -0,0 +1,33 @@
+package com.izouma.nineth.aspect.debounce;
+
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+
+import java.util.concurrent.Callable;
+import java.util.function.Function;
+
+@Slf4j
+public class DebounceTask implements Callable<Void> {
+    private final ProceedingJoinPoint  joinPoint;
+    private final Function<Void, Void> callback;
+
+    public DebounceTask(ProceedingJoinPoint joinPoint, Function<Void, Void> callback) {
+        this.joinPoint = joinPoint;
+        this.callback = callback;
+    }
+
+    @Override
+    public Void call() throws Exception {
+        try {
+            this.joinPoint.proceed();
+            if (this.callback != null) {
+                this.callback.apply(null);
+            }
+        } catch (Throwable e) {
+            e.printStackTrace();
+        }
+
+        return null;
+
+    }
+}

+ 2 - 0
src/main/java/com/izouma/nineth/config/GeneralProperties.java

@@ -24,4 +24,6 @@ public class GeneralProperties {
     private boolean notifyServer;
     private String  updateActivityStockGroup;
     private String  updateActivityStockTopic;
+    private int     dataCenterId;
+    private int     workerId;
 }

+ 21 - 0
src/main/java/com/izouma/nineth/config/RedisKeys.java

@@ -0,0 +1,21 @@
+package com.izouma.nineth.config;
+
+public class RedisKeys {
+    public static final String COLLECTION = "collection::";
+
+    public static final String CREATE_ORDER = "createOrder::";
+
+    public static final String COLLECTION_STOCK = "collectionStock::";
+
+    public static final String COLLECTION_SALE = "collectionSale::";
+
+    public static final String PAY_RECORD = "payRecord::";
+
+    public static final String ORDER_LOCK = "orderLock::";
+
+    public static final String MINT_ACTIVITY_STOCK = "mintActivityStock::";
+
+    public static final String MINT_ORDER_LOCK = "mintOrderLock::";
+
+    public static final String ACTIVITY_PAY_RECORD = "activityPayRecord::";
+}

+ 5 - 2
src/main/java/com/izouma/nineth/config/SnowflakeIdWorkerConfig.java

@@ -1,13 +1,16 @@
 package com.izouma.nineth.config;
 
 import com.izouma.nineth.utils.SnowflakeIdWorker;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
 @Configuration
+@Slf4j
 public class SnowflakeIdWorkerConfig {
     @Bean
-    public SnowflakeIdWorker snowflakeIdWorker() {
-        return new SnowflakeIdWorker(0, 0);
+    public SnowflakeIdWorker snowflakeIdWorker(GeneralProperties generalProperties) {
+        log.info("init snowflakeIdWorker worker={} dataCenter={}", generalProperties.getWorkerId(), generalProperties.getDataCenterId());
+        return new SnowflakeIdWorker(generalProperties.getWorkerId(), generalProperties.getDataCenterId());
     }
 }

+ 7 - 0
src/main/java/com/izouma/nineth/domain/Asset.java

@@ -22,6 +22,13 @@ import java.util.List;
 
 @Data
 @Entity
+@Table(indexes = {
+        @Index(columnList = "userId"),
+        @Index(columnList = "tokenId"),
+        @Index(columnList = "collectionId"),
+        @Index(columnList = "minterId"),
+        @Index(columnList = "ownerId"),
+})
 @AllArgsConstructor
 @NoArgsConstructor
 @Builder

+ 2 - 3
src/main/java/com/izouma/nineth/domain/Banner.java

@@ -9,12 +9,11 @@ import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
-import javax.persistence.Entity;
-import javax.persistence.EnumType;
-import javax.persistence.Enumerated;
+import javax.persistence.*;
 
 @Data
 @Entity
+@Table(indexes = {@Index(columnList = "type")})
 @AllArgsConstructor
 @NoArgsConstructor
 @Builder

+ 89 - 0
src/main/java/com/izouma/nineth/domain/BaseEntityNoID.java

@@ -0,0 +1,89 @@
+package com.izouma.nineth.domain;
+
+import com.alibaba.excel.annotation.ExcelIgnore;
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.hibernate.envers.Audited;
+import org.springframework.data.annotation.CreatedBy;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedBy;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import javax.persistence.EntityListeners;
+import javax.persistence.MappedSuperclass;
+import java.time.LocalDateTime;
+
+@MappedSuperclass
+@Audited
+@EntityListeners(AuditingEntityListener.class)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(value = {"hibernateLazyInitializer"}, ignoreUnknown = true)
+public abstract class BaseEntityNoID {
+    @ExcelIgnore
+    @JsonIgnore
+    @CreatedBy
+    private String createdBy;
+
+    @ExcelProperty("创建时间")
+    @JsonIgnore
+    @CreatedDate
+    private LocalDateTime createdAt;
+
+    @ExcelIgnore
+    @JsonIgnore
+    @LastModifiedBy
+    private String modifiedBy;
+
+    @ExcelIgnore
+    @JsonIgnore
+    @LastModifiedDate
+    private LocalDateTime modifiedAt;
+
+    @ExcelIgnore
+    private boolean del;
+
+    public String getCreatedBy() {
+        return createdBy;
+    }
+
+    public void setCreatedBy(String createdBy) {
+        this.createdBy = createdBy;
+    }
+
+    @JsonProperty("createdAt")
+    public LocalDateTime getCreatedAt() {
+        return createdAt;
+    }
+
+    public void setCreatedAt(LocalDateTime createdAt) {
+        this.createdAt = createdAt;
+    }
+
+    public String getModifiedBy() {
+        return modifiedBy;
+    }
+
+    public void setModifiedBy(String modifiedBy) {
+        this.modifiedBy = modifiedBy;
+    }
+
+    public LocalDateTime getModifiedAt() {
+        return modifiedAt;
+    }
+
+    public void setModifiedAt(LocalDateTime modifiedAt) {
+        this.modifiedAt = modifiedAt;
+    }
+
+    public boolean isDel() {
+        return del;
+    }
+
+    public void setDel(boolean del) {
+        this.del = del;
+    }
+}

+ 6 - 3
src/main/java/com/izouma/nineth/domain/BlindBoxItem.java

@@ -10,14 +10,17 @@ import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
-import javax.persistence.Column;
-import javax.persistence.Convert;
-import javax.persistence.Entity;
+import javax.persistence.*;
 import java.math.BigDecimal;
 import java.util.List;
 
 @Data
 @Entity
+@Table(indexes = {
+        @Index(columnList = "blindBoxId"),
+        @Index(columnList = "collectionId"),
+        @Index(columnList = "minterId"),
+})
 @AllArgsConstructor
 @NoArgsConstructor
 @Builder

+ 7 - 1
src/main/java/com/izouma/nineth/domain/Collection.java

@@ -22,7 +22,13 @@ import java.util.List;
 
 @Data
 @Entity
-@Table(name = "collection_info")
+@Table(name = "collection_info", indexes = {
+        @Index(columnList = "type,source"),
+        @Index(columnList = "minterId"),
+        @Index(columnList = "onShelf"),
+        @Index(columnList = "ownerId"),
+        @Index(columnList = "assetId")
+})
 @AllArgsConstructor
 @NoArgsConstructor
 @Builder

+ 24 - 0
src/main/java/com/izouma/nineth/domain/ErrorOrder.java

@@ -0,0 +1,24 @@
+package com.izouma.nineth.domain;
+
+import com.izouma.nineth.enums.PayMethod;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Entity;
+
+@Data
+@Entity
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class ErrorOrder extends BaseEntity {
+    private Long orderId;
+
+    private String errorMessage;
+
+    private String transactionId;
+
+    private PayMethod payMethod;
+}

+ 3 - 1
src/main/java/com/izouma/nineth/domain/Like.java

@@ -7,11 +7,13 @@ import lombok.Data;
 import lombok.NoArgsConstructor;
 
 import javax.persistence.Entity;
+import javax.persistence.Index;
 import javax.persistence.Table;
 
 @Data
 @Entity
-@Table(name = "like_info")
+@Table(name = "like_info", indexes =
+        {@Index(columnList = "userId"), @Index(columnList = "collectionId")})
 @AllArgsConstructor
 @NoArgsConstructor
 @Builder

+ 4 - 0
src/main/java/com/izouma/nineth/domain/MerProperty.java

@@ -0,0 +1,4 @@
+package com.izouma.nineth.domain;
+
+public class MerProperty {
+}

+ 14 - 2
src/main/java/com/izouma/nineth/domain/Order.java

@@ -23,12 +23,24 @@ import java.util.List;
 
 @Data
 @Entity
-@Table(name = "order_info")
+@Table(name = "order_info", indexes = {
+        @Index(columnList = "userId"),
+        @Index(columnList = "status"),
+        @Index(columnList = "assetId"),
+        @Index(columnList = "collectionId"),
+        @Index(columnList = "transactionId"),
+        @Index(columnList = "minterId"),
+})
 @AllArgsConstructor
 @NoArgsConstructor
 @Builder
 @ApiModel("订单")
-public class Order extends BaseEntity {
+public class Order extends BaseEntityNoID {
+
+    @Id
+//    @GenericGenerator(name = "custom-id", strategy = "com.izouma.nineth.utils.SnowflakeIdGenerator")
+//    @GeneratedValue(strategy = GenerationType.AUTO, generator = "custom-id")
+    private Long id;
 
     @ApiModelProperty("用户ID")
     @Searchable

+ 6 - 0
src/main/java/com/izouma/nineth/domain/SmsRecord.java

@@ -8,10 +8,16 @@ import lombok.Data;
 import lombok.NoArgsConstructor;
 
 import javax.persistence.Entity;
+import javax.persistence.Index;
+import javax.persistence.Table;
 import java.time.LocalDateTime;
 
 @Data
 @Entity
+@Table(indexes = {
+        @Index(columnList = "phone"),
+        @Index(columnList = "expiresAt")
+})
 @AllArgsConstructor
 @NoArgsConstructor
 @Builder

+ 10 - 0
src/main/java/com/izouma/nineth/domain/User.java

@@ -28,6 +28,10 @@ import java.util.Set;
 
 @Data
 @Entity
+@Table(indexes = {
+        @Index(columnList = "phone"),
+        @Index(columnList = "admin"),
+})
 @AllArgsConstructor
 @NoArgsConstructor
 @Builder
@@ -124,4 +128,10 @@ public class User extends BaseEntity implements Serializable {
     private String inviteCode;
 
     private int minterProjectId;
+
+    @ApiModelProperty(value = "邀请得空投")
+    private Long invitor;
+
+    @ApiModelProperty(value = "邀请数量")
+    private int inviteNum;
 }

+ 2 - 0
src/main/java/com/izouma/nineth/dto/UserBankCard.java

@@ -26,4 +26,6 @@ public class UserBankCard extends BaseEntity {
     private String cardTypeDesc;
 
     private String bankNo;
+
+    private String phone;
 }

+ 2 - 0
src/main/java/com/izouma/nineth/dto/UserRegister.java

@@ -46,4 +46,6 @@ public class UserRegister {
     private int minterProjectId;
 
     private String intro;
+
+    private Long invitor;
 }

+ 28 - 0
src/main/java/com/izouma/nineth/event/CreateOrderEvent.java

@@ -0,0 +1,28 @@
+package com.izouma.nineth.event;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class CreateOrderEvent implements Serializable {
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long    id;
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long    userId;
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long    collectionId;
+    private int     qty;
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long    addressId;
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long    userCouponId;
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long    invitor;
+}

+ 21 - 0
src/main/java/com/izouma/nineth/event/OrderNotifyEvent.java

@@ -0,0 +1,21 @@
+package com.izouma.nineth.event;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import com.izouma.nineth.enums.PayMethod;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class OrderNotifyEvent implements Serializable {
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long      orderId;
+    private PayMethod payMethod;
+    private String    transactionId;
+    private long      time;
+}

+ 52 - 0
src/main/java/com/izouma/nineth/listener/CreateOrderListener.java

@@ -0,0 +1,52 @@
+package com.izouma.nineth.listener;
+
+import com.izouma.nineth.config.RedisKeys;
+import com.izouma.nineth.domain.Order;
+import com.izouma.nineth.event.CreateOrderEvent;
+import com.izouma.nineth.exception.BusinessException;
+import com.izouma.nineth.service.OrderService;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.spring.annotation.ConsumeMode;
+import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
+import org.apache.rocketmq.spring.core.RocketMQListener;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+@Service
+@Slf4j
+@AllArgsConstructor
+@RocketMQMessageListener(
+        consumerGroup = "${general.create-order-group}",
+        topic = "${general.create-order-topic}",
+        consumeMode = ConsumeMode.ORDERLY)
+//@ConditionalOnProperty(value = "general.notify-server", havingValue = "false", matchIfMissing = true)
+public class CreateOrderListener implements RocketMQListener<CreateOrderEvent> {
+    private OrderService                  orderService;
+    private RedisTemplate<String, Object> redisTemplate;
+
+    @Override
+    public void onMessage(CreateOrderEvent event) {
+        log.info("收到订单创建消息: {}", event.getId());
+        Map<String, Object> map = new HashMap<>();
+        try {
+            Order order = orderService.create(event.getUserId(), event.getCollectionId(), event.getQty(),
+                    event.getAddressId(), event.getUserCouponId(), event.getInvitor(), event.getId());
+            map.put("success", true);
+            map.put("data", order);
+        } catch (Exception e) {
+            if (e instanceof BusinessException) {
+                log.error("订单创建失败 {}", e.getMessage());
+            } else {
+                log.error("订单创建失败", e);
+            }
+            map.put("success", false);
+            map.put("data", e.getMessage());
+        }
+        redisTemplate.boundValueOps(RedisKeys.CREATE_ORDER + event.getId()).set(map, 1, TimeUnit.DAYS);
+    }
+}

+ 27 - 0
src/main/java/com/izouma/nineth/listener/OrderNotifyListener.java

@@ -0,0 +1,27 @@
+package com.izouma.nineth.listener;
+
+import com.izouma.nineth.event.OrderNotifyEvent;
+import com.izouma.nineth.service.OrderService;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.spring.annotation.ConsumeMode;
+import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
+import org.apache.rocketmq.spring.core.RocketMQListener;
+import org.springframework.stereotype.Service;
+
+@Service
+@Slf4j
+@AllArgsConstructor
+@RocketMQMessageListener(
+        consumerGroup = "${general.order-notify-group}",
+        topic = "${general.order-notify-topic}",
+        consumeMode = ConsumeMode.ORDERLY)
+//@ConditionalOnProperty(value = "general.notify-server", havingValue = "true")
+public class OrderNotifyListener implements RocketMQListener<OrderNotifyEvent> {
+    private OrderService orderService;
+
+    @Override
+    public void onMessage(OrderNotifyEvent e) {
+        orderService.notifyOrder(e.getOrderId(), e.getPayMethod(), e.getTransactionId());
+    }
+}

+ 28 - 0
src/main/java/com/izouma/nineth/listener/UpdateSaleListener.java

@@ -0,0 +1,28 @@
+package com.izouma.nineth.listener;
+
+import com.izouma.nineth.service.CollectionService;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.spring.annotation.ConsumeMode;
+import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
+import org.apache.rocketmq.spring.core.RocketMQListener;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Service;
+
+@Service
+@Slf4j
+@AllArgsConstructor
+@RocketMQMessageListener(
+        consumerGroup = "${general.update-sale-group}",
+        topic = "${general.update-sale-topic}",
+        consumeMode = ConsumeMode.ORDERLY)
+//@ConditionalOnProperty(value = "general.notify-server", havingValue = "false", matchIfMissing = true)
+public class UpdateSaleListener implements RocketMQListener<Long> {
+
+    private CollectionService collectionService;
+
+    @Override
+    public void onMessage(Long id) {
+        collectionService.syncSale(id);
+    }
+}

+ 27 - 0
src/main/java/com/izouma/nineth/listener/UpdateStockListener.java

@@ -0,0 +1,27 @@
+package com.izouma.nineth.listener;
+
+import com.izouma.nineth.service.CollectionService;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.spring.annotation.ConsumeMode;
+import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
+import org.apache.rocketmq.spring.core.RocketMQListener;
+import org.springframework.stereotype.Service;
+
+@Service
+@Slf4j
+@AllArgsConstructor
+@RocketMQMessageListener(
+        consumerGroup = "${general.update-stock-group}",
+        topic = "${general.update-stock-topic}",
+        consumeMode = ConsumeMode.ORDERLY)
+//@ConditionalOnProperty(value = "general.notify-server", havingValue = "false", matchIfMissing = true)
+public class UpdateStockListener implements RocketMQListener<Long> {
+
+    private CollectionService collectionService;
+
+    @Override
+    public void onMessage(Long id) {
+        collectionService.syncStock(id);
+    }
+}

+ 5 - 0
src/main/java/com/izouma/nineth/repo/BlindBoxItemRepo.java

@@ -16,4 +16,9 @@ public interface BlindBoxItemRepo extends JpaRepository<BlindBoxItem, Long>, Jpa
     void softDelete(Long id);
 
     List<BlindBoxItem> findByBlindBoxId(Long blindBoxId);
+
+    @Query("update BlindBoxItem b set b.stock = b.stock - ?2, b.sale = b.sale + ?2 where b.id = ?1")
+    @Modifying
+    @Transactional
+    void decreaseStockAndIncreaseSale(Long id, int num);
 }

+ 18 - 0
src/main/java/com/izouma/nineth/repo/CollectionRepo.java

@@ -107,5 +107,23 @@ public interface CollectionRepo extends JpaRepository<Collection, Long>, JpaSpec
     @Transactional
     void softDeleteByIdIn(java.util.Collection<Long> id);
 
+    @Modifying
+    @Transactional
     void deleteAllByIdIn(java.util.Collection<Long> id);
+
+    @Query("select c.stock from Collection c where c.id = ?1")
+    Integer getStock(Long id);
+
+    @Query("select c.sale from Collection c where c.id = ?1")
+    Integer getSale(Long id);
+
+    @Query("update Collection c set c.stock = ?2 where c.id = ?1")
+    @Transactional
+    @Modifying
+    int updateStock(Long id, int stock);
+
+    @Query("update Collection c set c.sale = ?2 where c.id = ?1")
+    @Transactional
+    @Modifying
+    int updateSale(Long id, int sale);
 }

+ 7 - 0
src/main/java/com/izouma/nineth/repo/ErrorOrderRepo.java

@@ -0,0 +1,7 @@
+package com.izouma.nineth.repo;
+
+import com.izouma.nineth.domain.ErrorOrder;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface ErrorOrderRepo extends JpaRepository<ErrorOrder, Long> {
+}

+ 2 - 0
src/main/java/com/izouma/nineth/repo/UserRepo.java

@@ -158,4 +158,6 @@ public interface UserRepo extends JpaRepository<User, Long>, JpaSpecificationExe
     void setSales(Long userId, int sales);
 
     List<User> findByAuthoritiesContains(Authority authority);
+
+    List<User> findBySettleAccountIdIsNotNull();
 }

+ 77 - 14
src/main/java/com/izouma/nineth/service/CollectionService.java

@@ -1,6 +1,9 @@
 package com.izouma.nineth.service;
 
 import com.alibaba.fastjson.JSON;
+import com.izouma.nineth.annotations.Debounce;
+import com.izouma.nineth.config.GeneralProperties;
+import com.izouma.nineth.config.RedisKeys;
 import com.izouma.nineth.domain.Collection;
 import com.izouma.nineth.domain.*;
 import com.izouma.nineth.dto.CollectionDTO;
@@ -20,14 +23,16 @@ import org.apache.commons.collections.MapUtils;
 import org.apache.commons.lang3.RandomUtils;
 import org.apache.commons.lang3.Range;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
 import org.springframework.beans.BeanUtils;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.PageImpl;
 import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Sort;
 import org.springframework.data.jpa.domain.Specification;
+import org.springframework.data.redis.core.BoundValueOperations;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.scheduling.TaskScheduler;
-import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.PostConstruct;
@@ -37,6 +42,7 @@ import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.util.*;
 import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 @Slf4j
@@ -44,14 +50,17 @@ import java.util.stream.Collectors;
 @AllArgsConstructor
 public class CollectionService {
 
-    private CollectionRepo   collectionRepo;
-    private LikeRepo         likeRepo;
-    private BlindBoxItemRepo blindBoxItemRepo;
-    private AppointmentRepo  appointmentRepo;
-    private UserRepo         userRepo;
-    private TaskScheduler    taskScheduler;
-    private CacheService     cacheService;
-    private AssetRepo        assetRepo;
+    private CollectionRepo                collectionRepo;
+    private LikeRepo                      likeRepo;
+    private BlindBoxItemRepo              blindBoxItemRepo;
+    private AppointmentRepo               appointmentRepo;
+    private UserRepo                      userRepo;
+    private TaskScheduler                 taskScheduler;
+    private CacheService                  cacheService;
+    private AssetRepo                     assetRepo;
+    private RedisTemplate<String, Object> redisTemplate;
+    private RocketMQTemplate              rocketMQTemplate;
+    private GeneralProperties             generalProperties;
 
     private final Map<Long, ScheduledFuture<?>> tasks = new HashMap<>();
 
@@ -261,7 +270,7 @@ public class CollectionService {
                 .build());
     }
 
-    public BlindBoxItem draw(Long collectionId) {
+    public synchronized BlindBoxItem draw(Long collectionId) {
         List<BlindBoxItem> items = blindBoxItemRepo.findByBlindBoxId(collectionId);
 
         Map<BlindBoxItem, Range<Integer>> randomRange = new HashMap<>();
@@ -309,9 +318,11 @@ public class CollectionService {
                 throw new BusinessException("盲盒抽卡失败");
             }
         }
-        winItem.setStock(winItem.getStock() - 1);
-        winItem.setSale(winItem.getSale() + 1);
-        blindBoxItemRepo.save(winItem);
+//        winItem.setStock(winItem.getStock() - 1);
+//        winItem.setSale(winItem.getSale() + 1);
+//        blindBoxItemRepo.saveAndFlush(winItem);
+        blindBoxItemRepo.decreaseStockAndIncreaseSale(winItem.getId(), 1);
+        blindBoxItemRepo.flush();
         return winItem;
     }
 
@@ -342,7 +353,7 @@ public class CollectionService {
         collection.setTotal(collection.getTotal() + number);
     }
 
-//    @Scheduled(cron = "0 0 1 ?")
+    //    @Scheduled(cron = "0 0 1 ?")
     public void delCollection() {
         List<Long> assetIds = assetRepo.findAllByStatus(AssetStatus.TRANSFERRED);
         List<Long> collections = collectionRepo.findAllByDelFalseAndAssetIdIn(assetIds);
@@ -352,4 +363,56 @@ public class CollectionService {
         log.info("定时任务删除未删除藏品:{}", collections);
         collectionRepo.softDeleteByIdIn(collections);
     }
+
+    public synchronized Long increaseStock(Long id, int number) {
+        BoundValueOperations<String, Object> ops = redisTemplate.boundValueOps(RedisKeys.COLLECTION_STOCK + id);
+        if (ops.get() == null) {
+            Boolean success = ops.setIfAbsent(Optional.ofNullable(collectionRepo.getStock(id))
+                    .orElse(0), 7, TimeUnit.DAYS);
+            log.info("创建redis库存:{}", success);
+        }
+        Long stock = ops.increment(number);
+        rocketMQTemplate.convertAndSend(generalProperties.getUpdateStockTopic(), id);
+        return stock;
+    }
+
+    public synchronized Long decreaseStock(Long id, int number) {
+        return increaseStock(id, -number);
+    }
+
+    public synchronized Long increaseSale(Long id, int number) {
+        BoundValueOperations<String, Object> ops = redisTemplate.boundValueOps(RedisKeys.COLLECTION_SALE + id);
+        if (ops.get() == null) {
+            Boolean success = ops.setIfAbsent(Optional.ofNullable(collectionRepo.getSale(id))
+                    .orElse(0), 7, TimeUnit.DAYS);
+            log.info("创建redis销量:{}", success);
+        }
+        Long sale = ops.increment(number);
+        rocketMQTemplate.convertAndSend(generalProperties.getUpdateSaleTopic(), id);
+        return sale;
+    }
+
+    public synchronized Long decreaseSale(Long id, int number) {
+        return increaseSale(id, -number);
+    }
+
+    @Debounce(key = "#id", delay = 500)
+    public void syncStock(Long id) {
+        Integer stock = (Integer) redisTemplate.opsForValue().get(RedisKeys.COLLECTION_STOCK + id);
+        if (stock != null) {
+            log.info("同步库存信息{}", id);
+            collectionRepo.updateStock(id, stock);
+            cacheService.clearCollection(id);
+        }
+    }
+
+    @Debounce(key = "#id", delay = 500)
+    public void syncSale(Long id) {
+        Integer sale = (Integer) redisTemplate.opsForValue().get(RedisKeys.COLLECTION_SALE + id);
+        if (sale != null) {
+            log.info("同步销量信息{}", id);
+            collectionRepo.updateSale(id, sale);
+            cacheService.clearCollection(id);
+        }
+    }
 }

+ 291 - 152
src/main/java/com/izouma/nineth/service/OrderService.java

@@ -15,20 +15,21 @@ import com.github.binarywang.wxpay.service.WxPayService;
 import com.huifu.adapay.core.exception.BaseAdaPayException;
 import com.huifu.adapay.model.AdapayCommon;
 import com.huifu.adapay.model.Payment;
-import com.izouma.nineth.config.AdapayProperties;
-import com.izouma.nineth.config.AlipayProperties;
-import com.izouma.nineth.config.GeneralProperties;
-import com.izouma.nineth.config.WxPayProperties;
+import com.huifu.adapay.model.Refund;
+import com.izouma.nineth.config.*;
 import com.izouma.nineth.domain.Collection;
 import com.izouma.nineth.domain.*;
 import com.izouma.nineth.dto.PageQuery;
 import com.izouma.nineth.enums.*;
 import com.izouma.nineth.event.CreateAssetEvent;
+import com.izouma.nineth.event.CreateOrderEvent;
+import com.izouma.nineth.event.OrderNotifyEvent;
 import com.izouma.nineth.event.TransferAssetEvent;
 import com.izouma.nineth.exception.BusinessException;
 import com.izouma.nineth.repo.*;
 import com.izouma.nineth.security.Authority;
 import com.izouma.nineth.utils.JpaUtils;
+import com.izouma.nineth.utils.SecurityUtils;
 import com.izouma.nineth.utils.SnowflakeIdWorker;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -37,9 +38,14 @@ import org.apache.commons.codec.net.URLCodec;
 import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.collections.MapUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.rocketmq.client.producer.SendResult;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
+import org.springframework.cache.annotation.Cacheable;
 import org.springframework.context.event.EventListener;
 import org.springframework.core.env.Environment;
 import org.springframework.data.domain.Page;
+import org.springframework.data.redis.core.BoundSetOperations;
+import org.springframework.data.redis.core.BoundValueOperations;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
@@ -51,6 +57,7 @@ import java.math.RoundingMode;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
+import java.util.concurrent.TimeUnit;
 
 @Service
 @AllArgsConstructor
@@ -76,108 +83,135 @@ public class OrderService {
     private CommissionRecordRepo          commissionRecordRepo;
     private AdapayProperties              adapayProperties;
     private GeneralProperties             generalProperties;
+    private SnowflakeIdWorker             snowflakeIdWorker;
+    private RocketMQTemplate              rocketMQTemplate;
+    private ErrorOrderRepo                errorOrderRepo;
 
     public Page<Order> all(PageQuery pageQuery) {
         return orderRepo.findAll(JpaUtils.toSpecification(pageQuery, Order.class), JpaUtils.toPageRequest(pageQuery));
     }
 
+    public String mqCreate(Long userId, Long collectionId, int qty, Long addressId, Long userCouponId, Long invitor) {
+
+        Long id = snowflakeIdWorker.nextId();
+        SendResult result = rocketMQTemplate.syncSend(generalProperties.getCreateOrderTopic(),
+                new CreateOrderEvent(id, userId, collectionId, qty, addressId, userCouponId, invitor), 100000);
+        log.info("发送订单到队列: {}, result={}", id, result);
+        return String.valueOf(id);
+    }
+
     @Transactional
-    public Order create(Long userId, Long collectionId, int qty, Long addressId, Long userCouponId, Long invitor) {
-        if (qty <= 0) throw new BusinessException("数量必须大于0");
-        User user = userRepo.findByIdAndDelFalse(userId).orElseThrow(new BusinessException("用户不存在"));
-        Collection collection = collectionRepo.findById(collectionId).orElseThrow(new BusinessException("藏品不存在"));
-        User minter = userRepo.findById(collection.getMinterId()).orElseThrow(new BusinessException("铸造者不存在"));
-        UserCoupon coupon = null;
-        if (userCouponId != null) {
-            coupon = userCouponRepo.findById(userCouponId).orElseThrow(new BusinessException("兑换券不存在"));
-            if (coupon.isUsed()) {
-                throw new BusinessException("该兑换券已使用");
+    public Order create(Long userId, Long collectionId, int qty, Long addressId, Long userCouponId, Long invitor, Long id) {
+        qty = 1;
+        long t = System.currentTimeMillis();
+        int stock = Optional.ofNullable(collectionService.decreaseStock(collectionId, qty))
+                .map(Math::toIntExact)
+                .orElseThrow(new BusinessException("很遗憾,藏品已售罄"));
+
+        try {
+            if (stock < 0) {
+                throw new BusinessException("很遗憾,藏品已售罄");
             }
-            if (coupon.isLimited() && !coupon.getCollectionIds().contains(collectionId)) {
-                throw new BusinessException("该兑换券不可用");
+
+            User user = userRepo.findByIdAndDelFalse(userId).orElseThrow(new BusinessException("用户不存在"));
+            Collection collection = collectionRepo.findById(collectionId).orElseThrow(new BusinessException("藏品不存在"));
+            User minter = userRepo.findById(collection.getMinterId()).orElseThrow(new BusinessException("铸造者不存在"));
+            UserCoupon coupon = null;
+            if (userCouponId != null) {
+                coupon = userCouponRepo.findById(userCouponId).orElseThrow(new BusinessException("兑换券不存在"));
+                if (coupon.isUsed()) {
+                    throw new BusinessException("该兑换券已使用");
+                }
+                if (coupon.isLimited() && !coupon.getCollectionIds().contains(collectionId)) {
+                    throw new BusinessException("该兑换券不可用");
+                }
             }
-        }
-        if (collection.isScheduleSale()) {
-            if (collection.getStartTime().isAfter(LocalDateTime.now())) {
-                throw new BusinessException("当前还未开售");
+            if (collection.isScheduleSale()) {
+                if (collection.getStartTime().isAfter(LocalDateTime.now())) {
+                    throw new BusinessException("当前还未开售");
+                }
             }
-        }
-        if (!collection.isOnShelf()) {
-            throw new BusinessException("藏品已下架");
-        }
-        if (qty > collection.getStock()) {
-            throw new BusinessException("库存不足");
-        }
-        if (!collection.isSalable()) {
-            throw new BusinessException("该藏品当前不可购买");
-        }
-        if (collection.getAssetId() != null) {
-            Asset asset = assetRepo.findById(collection.getAssetId()).orElseThrow(new BusinessException("藏品不存在"));
-            if (asset.getStatus() != AssetStatus.NORMAL) {
+            if (!collection.isOnShelf()) {
                 throw new BusinessException("藏品已下架");
             }
-        }
-        UserAddress userAddress = null;
-        if (addressId != null) {
-            userAddress = userAddressRepo.findById(addressId).orElseThrow(new BusinessException("地址信息不存在"));
-        }
+            if (qty > collection.getStock()) {
+                throw new BusinessException("库存不足");
+            }
+            if (!collection.isSalable()) {
+                throw new BusinessException("该藏品当前不可购买");
+            }
+            if (collection.getAssetId() != null) {
+                Asset asset = assetRepo.findById(collection.getAssetId()).orElseThrow(new BusinessException("藏品不存在"));
+                if (asset.getStatus() != AssetStatus.NORMAL) {
+                    throw new BusinessException("藏品已下架");
+                }
+            }
+            UserAddress userAddress = null;
+            if (addressId != null) {
+                userAddress = userAddressRepo.findById(addressId).orElseThrow(new BusinessException("地址信息不存在"));
+            }
 
-        collectionRepo.increaseStock(collectionId, -qty);
-        collectionRepo.increaseSale(collectionId, qty);
-
-        BigDecimal gasFee = sysConfigService.getBigDecimal("gas_fee");
-        Order order = Order.builder()
-                .userId(userId)
-                .collectionId(collectionId)
-                .name(collection.getName())
-                .pic(collection.getPic())
-                .detail(collection.getDetail())
-                .properties(collection.getProperties())
-                .category(collection.getCategory())
-                .canResale(collection.isCanResale())
-                .royalties(collection.getRoyalties())
-                .serviceCharge(collection.getServiceCharge())
-                .type(collection.getType())
-                .source(collection.getSource())
-                .minterId(collection.getMinterId())
-                .minter(minter.getNickname())
-                .minterAvatar(minter.getAvatar())
-                .qty(qty)
-                .price(collection.getPrice())
-                .gasPrice(gasFee)
-                .totalPrice(collection.getPrice().multiply(BigDecimal.valueOf(qty)).add(gasFee))
-                .contactName(Optional.ofNullable(userAddress).map(UserAddress::getName).orElse(null))
-                .contactPhone(Optional.ofNullable(userAddress).map(UserAddress::getPhone).orElse(null))
-                .address(Optional.ofNullable(userAddress).map(u ->
-                                u.getProvinceId() + " " + u.getCityId() + " " + u.getDistrictId() + " " + u.getAddress())
-                        .orElse(null))
-                .status(OrderStatus.NOT_PAID)
-                .assetId(collection.getAssetId())
-                .couponId(userCouponId)
-                .invitor(invitor)
-                .projectId(collection.getProjectId())
-                .build();
-        if (coupon != null) {
-            coupon.setUsed(true);
-            coupon.setUseTime(LocalDateTime.now());
-            if (coupon.isNeedGas()) {
-                order.setTotalPrice(order.getGasPrice());
-            } else {
-                order.setTotalPrice(BigDecimal.ZERO);
+            BigDecimal gasFee = sysConfigService.getBigDecimal("gas_fee");
+            Order order = Order.builder()
+                    .id(Optional.ofNullable(id).orElse(snowflakeIdWorker.nextId()))
+                    .userId(userId)
+                    .collectionId(collectionId)
+                    .name(collection.getName())
+                    .pic(collection.getPic())
+                    .detail(collection.getDetail())
+                    .properties(collection.getProperties())
+                    .category(collection.getCategory())
+                    .canResale(collection.isCanResale())
+                    .royalties(collection.getRoyalties())
+                    .serviceCharge(collection.getServiceCharge())
+                    .type(collection.getType())
+                    .source(collection.getSource())
+                    .minterId(collection.getMinterId())
+                    .minter(minter.getNickname())
+                    .minterAvatar(minter.getAvatar())
+                    .qty(qty)
+                    .price(collection.getPrice())
+                    .gasPrice(gasFee)
+                    .totalPrice(collection.getPrice().multiply(BigDecimal.valueOf(qty)).add(gasFee))
+                    .contactName(Optional.ofNullable(userAddress).map(UserAddress::getName).orElse(null))
+                    .contactPhone(Optional.ofNullable(userAddress).map(UserAddress::getPhone).orElse(null))
+                    .address(Optional.ofNullable(userAddress).map(u ->
+                                    u.getProvinceId() + " " + u.getCityId() + " " + u.getDistrictId() + " " + u.getAddress())
+                            .orElse(null))
+                    .status(OrderStatus.NOT_PAID)
+                    .assetId(collection.getAssetId())
+                    .couponId(userCouponId)
+                    .invitor(invitor)
+                    .projectId(collection.getProjectId())
+                    .build();
+            if (coupon != null) {
+                coupon.setUsed(true);
+                coupon.setUseTime(LocalDateTime.now());
+                if (coupon.isNeedGas()) {
+                    order.setTotalPrice(order.getGasPrice());
+                } else {
+                    order.setTotalPrice(BigDecimal.ZERO);
+                }
+                userCouponRepo.save(coupon);
             }
-        }
 
-        if (collection.getSource() == CollectionSource.TRANSFER) {
-            Asset asset = assetRepo.findById(collection.getAssetId()).orElseThrow(new BusinessException("资产不存在"));
-            asset.setStatus(AssetStatus.TRADING);
-            assetRepo.save(asset);
-            collectionRepo.setOnShelf(collection.getId(), false);
-        }
-        order = orderRepo.save(order);
-        if (order.getTotalPrice().equals(BigDecimal.ZERO)) {
-            notifyOrder(order.getId(), PayMethod.WEIXIN, null);
+            if (collection.getSource() == CollectionSource.TRANSFER) {
+                Asset asset = assetRepo.findById(collection.getAssetId()).orElseThrow(new BusinessException("资产不存在"));
+                asset.setStatus(AssetStatus.TRADING);
+                assetRepo.save(asset);
+                collectionRepo.setOnShelf(collection.getId(), false);
+            }
+            order = orderRepo.save(order);
+            if (order.getTotalPrice().equals(BigDecimal.ZERO)) {
+                notifyOrder(order.getId(), PayMethod.WEIXIN, null);
+            }
+            rocketMQTemplate.syncSend(generalProperties.getUpdateStockTopic(), collectionId, 10000);
+            log.info("订单创建完成, id={}, {}ms", order.getId(), System.currentTimeMillis() - t);
+            return order;
+        } catch (Exception e) {
+            collectionService.increaseStock(collectionId, qty);
+            throw e;
         }
-        return order;
     }
 
     public void payOrderAlipay(Long id, Model model) {
@@ -191,7 +225,7 @@ public class OrderService {
             JSONObject bizContent = new JSONObject();
             bizContent.put("notifyUrl", alipayProperties.getNotifyUrl());
             bizContent.put("returnUrl", alipayProperties.getReturnUrl());
-            bizContent.put("out_trade_no", String.valueOf(new SnowflakeIdWorker(0, 0).nextId()));
+            bizContent.put("out_trade_no", String.valueOf(snowflakeIdWorker.nextId()));
             bizContent.put("total_amount", order.getTotalPrice().stripTrailingZeros().toPlainString());
             bizContent.put("disable_pay_channels", "pcredit,creditCard");
             if (Arrays.stream(env.getActiveProfiles()).noneMatch(s -> s.equals("prod"))) {
@@ -228,7 +262,7 @@ public class OrderService {
 
         WxPayUnifiedOrderRequest request = new WxPayUnifiedOrderRequest();
         request.setBody(order.getName());
-        request.setOutTradeNo(String.valueOf(new SnowflakeIdWorker(1, 1).nextId()));
+        request.setOutTradeNo(String.valueOf(snowflakeIdWorker.nextId()));
         request.setTotalFee(order.getTotalPrice().multiply(BigDecimal.valueOf(100)).intValue());
         if (Arrays.stream(env.getActiveProfiles()).noneMatch(s -> s.equals("prod"))) {
             // 测试环境设为1分
@@ -254,6 +288,7 @@ public class OrderService {
 
     }
 
+    @Cacheable(value = "adapay", key = "#id+'_'+#payChannel")
     public Object payAdapay(Long id, String payChannel, String openId) throws BaseAdaPayException {
         List<String> aliChannels = Arrays.asList("alipay", "alipay_qr", "alipay_wap");
         List<String> wxChannels = Arrays.asList("wx_pub", "wx_lite");
@@ -272,7 +307,7 @@ public class OrderService {
         }
 
         Map<String, Object> paymentParams = new HashMap<>();
-        paymentParams.put("order_no", String.valueOf(new SnowflakeIdWorker(0, 0).nextId()));
+        paymentParams.put("order_no", String.valueOf(snowflakeIdWorker.nextId()));
         paymentParams.put("pay_amt", order.getTotalPrice().setScale(2, RoundingMode.HALF_UP).toPlainString());
         paymentParams.put("app_id", adapayProperties.getAppId());
         paymentParams.put("pay_channel", payChannel);
@@ -331,6 +366,11 @@ public class OrderService {
             response = Payment.create(paymentParams);
             log.info("createOrderResponse {}", JSON.toJSONString(response, SerializerFeature.PrettyFormat));
             AdapayService.checkSuccess(response);
+
+            // 保存adapay的订单id,用于后续取消订单时的查询
+            BoundSetOperations<String, Object> ops = redisTemplate.boundSetOps(RedisKeys.PAY_RECORD + order.getId());
+            ops.add(MapUtils.getString(response, "id"));
+            ops.expire(7, TimeUnit.DAYS);
         }
 
         switch (payChannel) {
@@ -378,46 +418,98 @@ public class OrderService {
         return restAmount.subtract(divAmount);
     }
 
-    @Transactional
     public void notifyOrder(Long orderId, PayMethod payMethod, String transactionId) {
-        Order order = orderRepo.findById(orderId).orElseThrow(new BusinessException("订单不存在"));
-        Collection collection = collectionRepo.findById(order.getCollectionId())
-                .orElseThrow(new BusinessException("藏品不存在"));
-        User user = userRepo.findById(order.getUserId()).orElseThrow(new BusinessException("用户不存在"));
-        if (order.getStatus() == OrderStatus.NOT_PAID) {
-            order.setStatus(OrderStatus.PROCESSING);
-            order.setPayTime(LocalDateTime.now());
-            order.setTransactionId(transactionId);
-            order.setPayMethod(payMethod);
-            if (order.getType() == CollectionType.BLIND_BOX) {
-                BlindBoxItem winItem = collectionService.draw(collection.getId());
-                order.setWinCollectionId(winItem.getCollectionId());
-                orderRepo.save(order);
-                assetService.createAsset(winItem, user, order.getId(), order.getPrice(), "出售",
-                        collectionService.getNextNumber(winItem.getCollectionId()));
-                addSales(winItem.getMinterId());
-            } else {
-                if (collection.getSource() == CollectionSource.TRANSFER) {
-                    Asset asset = assetRepo.findById(collection.getAssetId()).orElse(null);
-                    assetService.transfer(asset, order.getPrice(), user, "转让", order.getId());
-                    List<Long> collectionIds = collectionRepo.findAllByAssetId(collection.getAssetId());
-                    log.info("删除collection {}", collectionIds);
-                    if (CollectionUtils.isNotEmpty(collectionIds)) {
-                        collectionRepo.deleteAllByIdIn(collectionIds);
-                    } else {
-                        collectionRepo.delete(collection);
-                    }
+        log.info("订单回调 orderId: {}, payMethod: {}, transactionId: {}", orderId, payMethod, transactionId);
 
-                } else {
+        // 取消订单与订单回调不能同时进行,需要抢锁
+        if (!getOrderLock(orderId)) {
+            log.info("订单回调失败 orderId: {} redis锁定, 重新发送到队列", orderId);
+            rocketMQTemplate.syncSend(generalProperties.getOrderNotifyTopic(),
+                    new OrderNotifyEvent(orderId, payMethod, transactionId, System.currentTimeMillis()));
+            return;
+        }
+
+        try {
+            Order order = orderRepo.findById(orderId).orElseThrow(new BusinessException("订单不存在"));
+            Collection collection = collectionRepo.findById(order.getCollectionId())
+                    .orElseThrow(new BusinessException("藏品不存在"));
+            User user = userRepo.findById(order.getUserId()).orElseThrow(new BusinessException("用户不存在"));
+            if (order.getStatus() == OrderStatus.NOT_PAID) {
+                order.setStatus(OrderStatus.PROCESSING);
+                order.setPayTime(LocalDateTime.now());
+                order.setTransactionId(transactionId);
+                order.setPayMethod(payMethod);
+                if (order.getType() == CollectionType.BLIND_BOX) {
+                    log.info("开始盲盒抽卡 orderId: {}, collectionId: {}", orderId, collection.getId());
+                    BlindBoxItem winItem = null;
+                    try {
+                        winItem = collectionService.draw(collection.getId());
+                    } catch (BusinessException ignored) {
+                    }
+                    if (winItem == null) {
+                        log.info("抽卡失败退款 orderId: {}", orderId);
+                        order.setStatus(OrderStatus.CANCELLED);
+                        order.setCancelTime(LocalDateTime.now());
+
+                        Map<String, Object> refundParams = new HashMap<>();
+                        refundParams.put("refund_amt", order.getTotalPrice().setScale(2, RoundingMode.HALF_UP)
+                                .toPlainString());
+                        refundParams.put("refund_order_no", String.valueOf(snowflakeIdWorker.nextId()));
+                        try {
+                            Map<String, Object> response = Refund.create(transactionId, refundParams);
+                        } catch (BaseAdaPayException e) {
+                            e.printStackTrace();
+                        }
+                        orderRepo.save(order);
+                        throw new BusinessException("抽卡失败, 已退款 " + orderId);
+                    }
+                    log.info("抽卡成功 orderId: {}, collectionId: {}, winCollectionId: {}", orderId, collection.getId(), winItem.getCollectionId());
+                    order.setWinCollectionId(winItem.getCollectionId());
                     orderRepo.save(order);
-                    assetService.createAsset(collection, user, order.getId(), order.getPrice(), "出售",
-                            collectionService.getNextNumber(order.getCollectionId()));
+                    assetService.createAsset(winItem, user, order.getId(), order.getPrice(), "出售",
+                            winItem.getTotal() > 1 ? collectionService.getNextNumber(winItem.getCollectionId()) : null);
+                } else {
+                    if (collection.getSource() == CollectionSource.TRANSFER) {
+                        orderRepo.save(order);
+                        Asset asset = assetRepo.findById(collection.getAssetId()).orElse(null);
+                        assetService.transfer(asset, order.getPrice(), user, "转让", order.getId());
+                        List<Long> collectionIds = collectionRepo.findAllByAssetId(collection.getAssetId());
+                        log.info("删除collection {}", collectionIds);
+                        if (CollectionUtils.isNotEmpty(collectionIds)) {
+                            collectionRepo.deleteAllByIdIn(collectionIds);
+                        } else {
+                            collectionRepo.delete(collection);
+                        }
+
+                    } else {
+                        orderRepo.save(order);
+                        assetService.createAsset(collection, user, order.getId(), order.getPrice(), "出售",
+                                collection.getTotal() > 1 ? collectionService.getNextNumber(order.getCollectionId()) : null);
+                    }
+                }
+                commission(order);
+                if (collection.getAssetId() == null) {
+                    collectionService.increaseSale(order.getCollectionId(), order.getQty());
                 }
-                addSales(collection.getMinterId());
+            } else {
+                throw new BusinessException("状态错误 " + order.getStatus());
             }
-            commission(order);
-        } else if (order.getStatus() == OrderStatus.CANCELLED) {
+        } catch (Exception e) {
+            ErrorOrder errorOrder = ErrorOrder.builder()
+                    .orderId(orderId)
+                    .transactionId(transactionId)
+                    .payMethod(payMethod)
+                    .build();
+            if (e instanceof BusinessException) {
+                log.error("订单回调出错 orderId: {} {}", orderId, e.getMessage());
+            } else {
+                log.error("订单回调出错 orderId: " + orderId, e);
+            }
+            errorOrder.setErrorMessage(e.getMessage());
+            errorOrderRepo.save(errorOrder);
+
         }
+        releaseOrderLock(orderId);
     }
 
     @EventListener
@@ -459,36 +551,53 @@ public class OrderService {
     }
 
     public void cancel(Order order) {
-        if (order.getStatus() != OrderStatus.NOT_PAID) {
-            throw new BusinessException("已支付订单无法取消");
+        if (!getOrderLock(order.getId())) {
+            log.error("订单取消失败 {}, redis锁了", order.getId());
+            return;
         }
-        Collection collection = collectionRepo.findById(order.getCollectionId())
-                .orElseThrow(new BusinessException("藏品不存在"));
-        User minter = userRepo.findById(collection.getMinterId()).orElseThrow(new BusinessException("铸造者不存在"));
 
-        if (collection.getSource() == CollectionSource.TRANSFER) {
-            Asset asset = assetRepo.findById(collection.getAssetId()).orElse(null);
-            if (asset != null) {
-                asset.setStatus(AssetStatus.NORMAL);
-                assetRepo.save(asset);
+        try {
+            if (order.getStatus() != OrderStatus.NOT_PAID) {
+                throw new BusinessException("已支付订单无法取消");
             }
-            collectionRepo.setOnShelf(collection.getId(), true);
-        }
-        collectionRepo.increaseSale(collection.getId(), -order.getQty());
-        collectionRepo.increaseStock(collection.getId(), order.getQty());
+            Collection collection = collectionRepo.findById(order.getCollectionId())
+                    .orElseThrow(new BusinessException("藏品不存在"));
+
+            CollectionSource source = Optional.ofNullable(order.getSource()).orElseGet(() ->
+                    collectionRepo.findById(order.getCollectionId()).map(Collection::getSource).orElse(null));
+            if (source == CollectionSource.TRANSFER) {
+                Asset asset = assetRepo.findById(order.getAssetId()).orElse(null);
+                if (asset != null) {
+                    log.info("set normal cancelOrder {}", order.getId());
+                    asset.setStatus(AssetStatus.NORMAL);
+                    assetRepo.save(asset);
+                }
+                collectionRepo.setOnShelf(order.getCollectionId(), true);
+            }
+            collectionService.increaseStock(order.getCollectionId(), order.getQty());
 
 
-        order.setStatus(OrderStatus.CANCELLED);
-        order.setCancelTime(LocalDateTime.now());
-        orderRepo.save(order);
+            order.setStatus(OrderStatus.CANCELLED);
+            order.setCancelTime(LocalDateTime.now());
+            orderRepo.save(order);
 
-        if (order.getCouponId() != null) {
-            userCouponRepo.findById(order.getCouponId()).ifPresent(coupon -> {
-                coupon.setUsed(false);
-                coupon.setUseTime(null);
-                userCouponRepo.save(coupon);
-            });
+            if (order.getCouponId() != null) {
+                userCouponRepo.findById(order.getCouponId()).ifPresent(coupon -> {
+                    coupon.setUsed(false);
+                    coupon.setUseTime(null);
+                    userCouponRepo.save(coupon);
+                });
+            }
+            rocketMQTemplate.syncSend(generalProperties.getUpdateStockTopic(), order.getCollectionId(), 10000);
+            log.info("取消订单{}", order.getId());
+        } catch (Exception e) {
+            if (e instanceof BusinessException) {
+                log.error(e.getMessage());
+            } else {
+                log.error("订单取消错误 orderId: " + order.getId(), e);
+            }
         }
+        releaseOrderLock(order.getId());
     }
 
     @Scheduled(fixedRate = 30000)
@@ -572,7 +681,37 @@ public class OrderService {
         request.setTransactionId(order.getTransactionId());
         request.setTotalFee(order.getTotalPrice().multiply(BigDecimal.valueOf(100)).intValue());
         request.setRefundFee(order.getTotalPrice().multiply(BigDecimal.valueOf(100)).intValue());
-        request.setOutRefundNo(String.valueOf(new SnowflakeIdWorker(0, 0).nextId()));
+        request.setOutRefundNo(String.valueOf(snowflakeIdWorker.nextId()));
         wxPayService.refund(request);
     }
+
+    public Object queryCreateOrder(String id) {
+        Object res = redisTemplate.opsForValue().get(RedisKeys.CREATE_ORDER + id);
+        if (res != null) {
+            if (res instanceof Map) {
+                if (MapUtils.getBooleanValue((Map) res, "success", false)) {
+                    Order order = (Order) MapUtils.getObject((Map) res, "data");
+                    if (!SecurityUtils.getAuthenticatedUser().getId().equals(order.getUserId())) {
+                        log.error("queryCreateOrder userId错误 requestUserId={} orderUserId={}",
+                                SecurityUtils.getAuthenticatedUser().getId(), order.getUserId());
+                        return null;
+                    }
+                }
+            }
+        }
+        return res;
+    }
+
+    // 获取订单锁,有效时间1小时
+    public boolean getOrderLock(Long orderId) {
+        BoundValueOperations<String, Object> ops = redisTemplate.boundValueOps(RedisKeys.ORDER_LOCK + orderId);
+        Boolean flag = ops.setIfAbsent(1, 1, TimeUnit.HOURS);
+        return Boolean.TRUE.equals(flag);
+    }
+
+    // 释放订单锁
+    public void releaseOrderLock(Long orderId) {
+        redisTemplate.delete(RedisKeys.ORDER_LOCK + orderId);
+    }
+
 }

+ 128 - 22
src/main/java/com/izouma/nineth/service/UserService.java

@@ -3,13 +3,16 @@ package com.izouma.nineth.service;
 import cn.binarywang.wx.miniapp.api.WxMaService;
 import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
 import cn.binarywang.wx.miniapp.bean.WxMaUserInfo;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.serializer.SerializerFeature;
 import com.huifu.adapay.core.exception.BaseAdaPayException;
+import com.huifu.adapay.model.AdapayCommon;
+import com.huifu.adapay.model.SettleAccount;
+import com.izouma.nineth.config.AdapayProperties;
 import com.izouma.nineth.config.Constants;
-import com.izouma.nineth.domain.Follow;
-import com.izouma.nineth.domain.IdentityAuth;
-import com.izouma.nineth.domain.Invite;
-import com.izouma.nineth.domain.User;
+import com.izouma.nineth.domain.*;
 import com.izouma.nineth.dto.*;
+import com.izouma.nineth.enums.AirDropType;
 import com.izouma.nineth.enums.AuthStatus;
 import com.izouma.nineth.enums.AuthorityName;
 import com.izouma.nineth.event.AccountCreatedEvent;
@@ -30,6 +33,7 @@ import me.chanjar.weixin.common.error.WxErrorException;
 import me.chanjar.weixin.mp.api.WxMpService;
 import me.chanjar.weixin.mp.bean.result.WxMpOAuth2AccessToken;
 import me.chanjar.weixin.mp.bean.result.WxMpUser;
+import org.apache.commons.collections.MapUtils;
 import org.apache.commons.lang3.RandomStringUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.BeanUtils;
@@ -51,23 +55,24 @@ import java.util.stream.Collectors;
 @Slf4j
 @AllArgsConstructor
 public class UserService {
-    private UserRepo          userRepo;
-    private WxMaService       wxMaService;
-    private WxMpService       wxMpService;
-    private SmsService        smsService;
-    private StorageService    storageService;
-    private JwtTokenUtil      jwtTokenUtil;
-    private CaptchaService    captchaService;
-    private FollowService     followService;
-    private FollowRepo        followRepo;
-    private IdentityAuthRepo  identityAuthRepo;
-    private SysConfigService  sysConfigService;
-    private CollectionService collectionService;
-    private AdapayService     adapayService;
-    private UserBankCardRepo  userBankCardRepo;
-    private CacheService      cacheService;
-    private InviteRepo        inviteRepo;
-    private NFTService        nftService;
+    private UserRepo         userRepo;
+    private WxMaService      wxMaService;
+    private WxMpService      wxMpService;
+    private SmsService       smsService;
+    private StorageService   storageService;
+    private JwtTokenUtil     jwtTokenUtil;
+    private CaptchaService   captchaService;
+    private FollowService    followService;
+    private FollowRepo       followRepo;
+    private IdentityAuthRepo identityAuthRepo;
+    private SysConfigService sysConfigService;
+    private AirDropService   airDropService;
+    private AdapayService    adapayService;
+    private UserBankCardRepo userBankCardRepo;
+    private CacheService     cacheService;
+    private InviteRepo       inviteRepo;
+    private NFTService       nftService;
+    private AdapayProperties adapayProperties;
 
     @CacheEvict(value = "user", key = "#user.username")
     public User update(User user) {
@@ -148,7 +153,7 @@ public class UserService {
         return user;
     }
 
-    public User phoneRegister(String phone, String code, String password, String inviteCode) {
+    public User phoneRegister(String phone, String code, String password, String inviteCode, Long invitor) {
         String name = "9th_" + RandomStringUtils.randomAlphabetic(8);
         Invite invite = null;
         if (StringUtils.isNotBlank(inviteCode)) {
@@ -165,10 +170,36 @@ public class UserService {
                 .invitorPhone(Optional.ofNullable(invite).map(Invite::getPhone).orElse(null))
                 .invitorName(Optional.ofNullable(invite).map(Invite::getName).orElse(null))
                 .inviteCode(Optional.ofNullable(invite).map(Invite::getCode).orElse(null))
+                .invitor(invitor)
                 .build());
         if (invite != null) {
             inviteRepo.increaseNum(invite.getId());
         }
+        if (invitor != null) {
+            userRepo.findByIdAndDelFalse(invitor).ifPresent(user1 -> {
+                user1.setInviteNum(user1.getInviteNum() + 1);
+                userRepo.save(user1);
+                if (user1.getInviteNum() >= 15) {
+                    airDropService.create(AirDrop.builder()
+                            .name("宇宙熊-致敬奥运-纯金款")
+                            .type(AirDropType.asset)
+                            .collectionId(8472L)
+                            .phone(List.of(user1.getPhone()))
+                            .userIds(List.of(invitor))
+                            .projectId(1)
+                            .build());
+                } else if (user1.getInviteNum() >= 5) {
+                    airDropService.create(AirDrop.builder()
+                            .name("宇宙熊-致敬奥运-纯黄款")
+                            .type(AirDropType.asset)
+                            .collectionId(8406L)
+                            .phone(List.of(user1.getPhone()))
+                            .userIds(List.of(invitor))
+                            .projectId(1)
+                            .build());
+                }
+            });
+        }
         return user;
     }
 
@@ -480,6 +511,7 @@ public class UserService {
                 .cardType(bankValidate.getCardType())
                 .cardTypeDesc(bankValidate.getCardTypeDesc())
                 .userId(userId)
+                .phone(phone)
                 .build());
     }
 
@@ -544,4 +576,78 @@ public class UserService {
             identityAuthRepo.deleteAll(identityAuthRepo.findByUserIdAndDelFalse(userId));
         }
     }
+
+    public void switchAccount() {
+        switchAccount(adapayProperties.getAppId());
+    }
+
+    public void switchAccount(String appId) {
+        userRepo.findBySettleAccountIdIsNotNull().parallelStream().forEach(user -> {
+            try {
+                IdentityAuth identityAuth = identityAuthRepo.findFirstByUserIdAndStatusAndDelFalseOrderByCreatedAtDesc(user.getId(), AuthStatus.SUCCESS)
+                        .orElseThrow(new BusinessException("用户未认证"));
+                UserBankCard userBankCard = userBankCardRepo.findByUserId(user.getId()).stream().findAny()
+                        .orElseThrow(new BusinessException("未绑卡"));
+                createMember(appId, user.getId().toString(), Optional.ofNullable(userBankCard.getPhone())
+                        .orElse(user.getPhone()), identityAuth.getRealName(), identityAuth.getIdNo());
+                createSettleAccount(appId, user.getId()
+                        .toString(), identityAuth.getRealName(), identityAuth.getIdNo(), Optional.ofNullable(userBankCard.getPhone())
+                        .orElse(user.getPhone()), userBankCard.getBankNo());
+                userBankCard.setPhone(Optional.ofNullable(userBankCard.getPhone()).orElse(user.getPhone()));
+                userBankCardRepo.save(userBankCard);
+            } catch (Exception e) {
+                try {
+                    adapayService.delSettleAccount(user.getMemberId(), user.getSettleAccountId());
+                } catch (Exception ex) {
+                    ex.printStackTrace();
+                }
+                user.setSettleAccountId(null);
+                userRepo.save(user);
+                userBankCardRepo.deleteByUserId(user.getId());
+            }
+        });
+    }
+
+    public void createMember(String appId, String memberId, String tel, String realName, String idno) throws BaseAdaPayException {
+        Map<String, Object> memberParams = new HashMap<>();
+        memberParams.put("adapay_func_code", "members.realname");
+        memberParams.put("member_id", memberId);
+        memberParams.put("app_id", appId);
+        memberParams.put("tel_no", tel);
+        memberParams.put("user_name", realName);
+        memberParams.put("cert_type", "00");
+        memberParams.put("cert_id", idno);
+        Map<String, Object> res = AdapayCommon.requestAdapay(memberParams);
+        log.info("createMember\n{}", JSON.toJSONString(res, SerializerFeature.PrettyFormat));
+        if (!("succeeded".equals(MapUtils.getString(res, "status"))
+                || "member_id_exists".equals(MapUtils.getString(res, "error_code")))) {
+            String errMsg = MapUtils.getString(res, "error_msg");
+            String errCode = MapUtils.getString(res, "error_code");
+            throw new BusinessException(errMsg + "(" + errCode + ")");
+        }
+    }
+
+    public String createSettleAccount(String appId, String memberId, String realName, String idNo, String phone, String bankNo) throws BaseAdaPayException {
+        Map<String, Object> settleCountParams = new HashMap<>();
+        Map<String, Object> accountInfo = new HashMap<>();
+        accountInfo.put("card_id", bankNo);
+        accountInfo.put("card_name", realName);
+        accountInfo.put("cert_id", idNo);
+        accountInfo.put("cert_type", "00");
+        accountInfo.put("tel_no", phone);
+        accountInfo.put("bank_acct_type", "2");
+        settleCountParams.put("member_id", memberId);
+        settleCountParams.put("app_id", appId);
+        settleCountParams.put("channel", "bank_account");
+        settleCountParams.put("account_info", accountInfo);
+        Map<String, Object> res = SettleAccount.create(settleCountParams);
+        log.info("createSettleAccount\n{}", JSON.toJSONString(res, SerializerFeature.PrettyFormat));
+        if (!("succeeded".equals(MapUtils.getString(res, "status"))
+                || "account_exists".equals(MapUtils.getString(res, "error_code")))) {
+            String errMsg = MapUtils.getString(res, "error_msg");
+            String errCode = MapUtils.getString(res, "error_code");
+            throw new BusinessException(errMsg + "(" + errCode + ")");
+        }
+        return MapUtils.getString(res, "id");
+    }
 }

+ 28 - 0
src/main/java/com/izouma/nineth/utils/AdapayUtils.java

@@ -0,0 +1,28 @@
+package com.izouma.nineth.utils;
+
+import com.huifu.adapay.core.exception.BaseAdaPayException;
+import com.huifu.adapay.core.exception.FailureCode;
+import com.izouma.nineth.exception.BusinessException;
+import org.apache.commons.collections.MapUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Map;
+
+public class AdapayUtils {
+    public static String packageRequestUrl(Map<String, Object> requestParams) throws BaseAdaPayException {
+        String adapayFuncCode = (String) requestParams.get("adapay_func_code");
+        if (StringUtils.isBlank(adapayFuncCode)) {
+            throw new BaseAdaPayException(FailureCode.ADAPAY_FUNC_CODE_NOT_BLANK.getFailureCode());
+        }
+        String adapayApiVersion = (String) requestParams.getOrDefault("adapay_api_version", "v1");
+        return "/" + adapayApiVersion + "/" + StringUtils.replace(adapayFuncCode, ".", "/");
+    }
+
+    public static void checkSuccess(Map<String, Object> map) {
+        if (!"succeeded".equals(MapUtils.getString(map, "status"))) {
+            String errMsg = MapUtils.getString(map, "error_msg");
+            String errCode = MapUtils.getString(map, "error_code");
+            throw new BusinessException(errMsg + "(" + errCode + ")");
+        }
+    }
+}

+ 11 - 7
src/main/java/com/izouma/nineth/web/AssetController.java

@@ -1,6 +1,7 @@
 package com.izouma.nineth.web;
 
 import com.izouma.nineth.TokenHistory;
+import com.izouma.nineth.config.GeneralProperties;
 import com.izouma.nineth.domain.Asset;
 import com.izouma.nineth.domain.GiftOrder;
 import com.izouma.nineth.domain.User;
@@ -17,6 +18,7 @@ import com.izouma.nineth.utils.SecurityUtils;
 import com.izouma.nineth.utils.excel.ExcelUtils;
 import io.swagger.annotations.ApiOperation;
 import lombok.AllArgsConstructor;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 import org.springframework.web.bind.annotation.*;
@@ -32,11 +34,13 @@ import java.util.Map;
 @RequestMapping("/asset")
 @AllArgsConstructor
 public class AssetController extends BaseController {
-    private AssetService     assetService;
-    private AssetRepo        assetRepo;
-    private GiftOrderService giftOrderService;
-    private TokenHistoryRepo tokenHistoryRepo;
-    private AssetMintService assetMintService;
+    private AssetService      assetService;
+    private AssetRepo         assetRepo;
+    private GiftOrderService  giftOrderService;
+    private TokenHistoryRepo  tokenHistoryRepo;
+    private AssetMintService  assetMintService;
+    private RocketMQTemplate  rocketMQTemplate;
+    private GeneralProperties generalProperties;
 
     //@PreAuthorize("hasRole('ADMIN')")
     @PostMapping("/save")
@@ -133,9 +137,9 @@ public class AssetController extends BaseController {
     }
 
     @GetMapping("/mint")
-    public String mint( ) {
+    public String mint() {
         for (Asset asset : assetRepo.findByTxHashIsNullAndTokenIdNotNullAndCreatedAtBefore(LocalDateTime.now())) {
-            assetMintService.mint(asset);
+            rocketMQTemplate.syncSend(generalProperties.getMintTopic(), asset.getId());
         }
         return "ok";
     }

+ 2 - 2
src/main/java/com/izouma/nineth/web/AuthenticationController.java

@@ -65,8 +65,8 @@ public class AuthenticationController {
 
     @PostMapping("/phoneRegister")
     @ApiOperation(value = "手机号密码注册")
-    public String phonePwdLogin(String phone, String code, String password, String inviteCode) {
-        User user = userService.phoneRegister(phone, code, password, inviteCode);
+    public String phonePwdLogin(String phone, String code, String password, String inviteCode, Long invitor) {
+        User user = userService.phoneRegister(phone, code, password, inviteCode, invitor);
         return jwtTokenUtil.generateToken(JwtUserFactory.create(user));
     }
 

+ 19 - 2
src/main/java/com/izouma/nineth/web/OrderController.java

@@ -2,6 +2,7 @@ package com.izouma.nineth.web;
 
 import com.github.binarywang.wxpay.exception.WxPayException;
 import com.izouma.nineth.domain.Order;
+import com.izouma.nineth.domain.User;
 import com.izouma.nineth.dto.OrderDTO;
 import com.izouma.nineth.dto.PageQuery;
 import com.izouma.nineth.enums.CollectionType;
@@ -79,8 +80,18 @@ public class OrderController extends BaseController {
                         @RequestParam(required = false) Long addressId,
                         @RequestParam(required = false) Long couponId,
                         @RequestParam(required = false) Long invitor) {
-        return orderService.create(SecurityUtils.getAuthenticatedUser().getId(),
-                collectionId, qty, addressId, couponId, invitor);
+        throw new BusinessException("接口暂不可用,请刷新后重试");
+    }
+
+    @PostMapping("/mqCreate")
+    public HashMap<String, String> mqCreate(@RequestParam Long collectionId, @RequestParam int qty,
+                                            @RequestParam(required = false) Long addressId,
+                                            @RequestParam(required = false) Long couponId,
+                                            @RequestParam(required = false) Long invitor) {
+        final User user = SecurityUtils.getAuthenticatedUser();
+        return new HashMap<>() {{
+            put("id", orderService.mqCreate(user.getId(), collectionId, qty, addressId, couponId, invitor));
+        }};
     }
 
     @PostMapping("/hide")
@@ -111,5 +122,11 @@ public class OrderController extends BaseController {
             orderRepo.save(order);
         });
     }
+
+    @GetMapping("/createResult")
+    public Object createResult(@RequestParam String id) {
+        return orderService.queryCreateOrder(id);
+    }
+
 }
 

+ 24 - 6
src/main/java/com/izouma/nineth/web/OrderNotifyController.java

@@ -11,13 +11,19 @@ import com.github.binarywang.wxpay.service.WxPayService;
 import com.huifu.adapay.core.AdapayCore;
 import com.huifu.adapay.core.util.AdapaySign;
 import com.izouma.nineth.config.AlipayProperties;
+import com.izouma.nineth.config.GeneralProperties;
+import com.izouma.nineth.config.RedisKeys;
 import com.izouma.nineth.enums.PayMethod;
+import com.izouma.nineth.event.OrderNotifyEvent;
 import com.izouma.nineth.service.AssetService;
 import com.izouma.nineth.service.GiftOrderService;
 import com.izouma.nineth.service.OrderService;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections.MapUtils;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
+import org.springframework.data.redis.core.BoundSetOperations;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.*;
 
@@ -25,6 +31,7 @@ import javax.servlet.http.HttpServletRequest;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 import static com.alibaba.fastjson.serializer.SerializerFeature.PrettyFormat;
 
@@ -34,11 +41,14 @@ import static com.alibaba.fastjson.serializer.SerializerFeature.PrettyFormat;
 @AllArgsConstructor
 public class OrderNotifyController {
 
-    private final AlipayProperties alipayProperties;
-    private final OrderService     orderService;
-    private final WxPayService     wxPayService;
-    private final AssetService     assetService;
-    private final GiftOrderService giftOrderService;
+    private final AlipayProperties              alipayProperties;
+    private final OrderService                  orderService;
+    private final WxPayService                  wxPayService;
+    private final AssetService                  assetService;
+    private final GiftOrderService              giftOrderService;
+    private final RedisTemplate<String, Object> redisTemplate;
+    private final RocketMQTemplate              rocketMQTemplate;
+    private final GeneralProperties             generalProperties;
 
     @PostMapping("/order/alipay")
     @ResponseBody
@@ -122,7 +132,15 @@ public class OrderNotifyController {
                     JSONObject jsonObject = JSON.parseObject(data);
                     String channel = jsonObject.getString("pay_channel");
                     String id = jsonObject.getString("id");
-                    orderService.notifyOrder(orderId, channel.startsWith("wx") ? PayMethod.WEIXIN : PayMethod.ALIPAY, id);
+                    PayMethod payMethod = channel.startsWith("wx") ? PayMethod.WEIXIN : PayMethod.ALIPAY;
+
+                    BoundSetOperations<String, Object> listOps = redisTemplate.boundSetOps(RedisKeys.PAY_RECORD + orderId);
+                    listOps.add(id);
+                    listOps.expire(7, TimeUnit.DAYS);
+
+                    rocketMQTemplate.syncSend(generalProperties.getOrderNotifyTopic(),
+                            new OrderNotifyEvent(orderId, payMethod, id, System.currentTimeMillis()));
+
                 }
             }
         } catch (Exception e) {

+ 13 - 7
src/main/java/com/izouma/nineth/web/OrderPayController.java

@@ -44,14 +44,20 @@ public class OrderPayController {
 
     @RequestMapping(value = "/alipay_wx", method = RequestMethod.GET)
     public String payOrderAlipayWx(Long id, Model model) throws BaseAdaPayException {
-        Order order = orderRepo.findById(id).orElseThrow(new BusinessException("订单不存在"));
-        if (order.getStatus() != OrderStatus.NOT_PAID) {
-            return "redirect:/9th/store";
+        try {
+            Order order = orderRepo.findById(id).orElseThrow(new BusinessException("订单不存在"));
+            if (order.getStatus() != OrderStatus.NOT_PAID) {
+                return "redirect:/9th/store";
+            }
+            String payUrl = (String) orderService.payAdapay(id, "alipay_wap", null);
+            model.addAttribute("payUrl", payUrl);
+            model.addAttribute("orderId", id);
+            return "AlipayHtml";
+        } catch (Exception e) {
+            log.error("payOrderAlipayWx", e);
+            model.addAttribute("msg", e.getMessage());
+            return "PayError";
         }
-        String payUrl = (String) orderService.payAdapay(id, "alipay_wap", null);
-        model.addAttribute("payUrl", payUrl);
-        model.addAttribute("orderId", id);
-        return "AlipayHtml";
     }
 
     @RequestMapping(value = "/alipay_qr", method = RequestMethod.GET)

+ 8 - 0
src/main/java/com/izouma/nineth/web/UserController.java

@@ -250,6 +250,14 @@ public class UserController extends BaseController {
                 .getContent();
         ExcelUtils.export(response, data);
     }
+
+
+    @PreAuthorize("hasAnyRole('ADMIN')")
+    @GetMapping("/switchAccount")
+    public String switchAccount() {
+        userService.switchAccount();
+        return "ok";
+    }
 }
 
 

+ 43 - 13
src/main/nine-space/src/views/Submit.vue

@@ -106,7 +106,8 @@ export default {
             enable_wx_lite: false,
             enable_wx_pub: false,
             launchName: '',
-            launchPath: ''
+            launchPath: '',
+            createOrderTimer: null
         };
     },
     computed: {
@@ -125,6 +126,9 @@ export default {
     },
     beforeRouteLeave(to, from, next) {
         console.log(to);
+        if (this.createOrderTimer) {
+            clearInterval(this.createOrderTimer);
+        }
         if (to.path !== '/couponList') {
             this.$store.commit('setCouponInfo', null);
         }
@@ -226,22 +230,48 @@ export default {
                     });
                 });
         },
-        submit() {
+        createOrder() {
             if (!this.payType) {
                 this.$toast('请选择支付方式');
                 return;
             }
-            this.$toast.loading('加载中');
-            let url = '/order/create?collectionId=' + this.$route.query.id + '&qty=1';
-            if (this.couponInfo) {
-                url += '&couponId=' + this.couponInfo.id;
-            }
-            let invitor = sessionStorage.getItem('invitor');
-            if (invitor) {
-                url += '&invitor=' + invitor;
-            }
-            this.$http
-                .post(url)
+            this.$toast.loading('请不要离开当前页面');
+
+            let params = {
+                collectionId: this.$route.query.id,
+                qty: 1,
+                couponId: (this.couponInfo || {}).id || '',
+                invitor: sessionStorage.getItem('invitor')
+            };
+            return this.$http.post('/order/mqCreate', params).then(res => {
+                return new Promise((resolve, reject) => {
+                    let checkOrder = () => {
+                        this.$http
+                            .get('/order/createResult', { id: res.id })
+                            .then(res => {
+                                if (res) {
+                                    clearInterval(this.createOrderTimer);
+                                    this.createOrderTimer = null;
+                                    if (res.success) {
+                                        resolve(res.data);
+                                    } else {
+                                        reject({ error: res.data });
+                                    }
+                                }
+                            })
+                            .catch(e => {
+                                clearInterval(this.createOrderTimer);
+                                this.createOrderTimer = null;
+                                reject(e);
+                            });
+                    };
+                    setTimeout(checkOrder, 500);
+                    this.createOrderTimer = setInterval(checkOrder, 2000);
+                });
+            });
+        },
+        submit() {
+            this.createOrder()
                 .then(res => {
                     if (this.money) {
                         this.$toast.clear();

+ 14 - 3
src/main/resources/application.yaml

@@ -193,12 +193,23 @@ alipay:
   root-cert-path: classpath:cert/alipayRootCert.crt
   notify-url: https://nfttest.9space.vip/notify/order/alipay
   return-url: https://nfttest.9space.vip/9th/home
+#adapay:
+#  app-id: app_f8760acc-f4d8-46f6-8f70-d80e36517075
+#  debug: true
+#  prod: true
+#  api-key: api_live_8818cac1-894b-40bc-ac77-803e02e9c260
+#  mock-key: api_test_0b1b0eb9-30e1-4acd-8e03-10b529de1856
+#  public-key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApyGiKxT+ffKnH/7Nxal8TE461QHr539cOQgDHSanERb9Fr3SxJjUccagGEi5f4kPAMlOMqlcJMGKr5/2hDQYNIkOYeam6MW/E+sZq2S3LZPe04iLglMG3e0OTQRyV8x5WinNadSgb0wiQzMPPrdCYd7AZAb7u5dCj5YhEB7TywQ5Cr8tzQUA+gTMuO5D3q7I0lB+dxwbzaXI3D2RqVF/05i90jZEvfEZKHmxcbqKyuVHWs2Decb2q433GM4koSI1N1iGSW+dgxlMWFk5OrySmayYcOwIViBEKR+6icb5pfwfETEOILh3bad6I8cJBw7WXT1Pa2PIuhB8G2uKjmIKGwIDAQAB
+#  priv-key: MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQCnIaIrFP598qcf/s3FqXxMTjrVAevnf1w5CAMdJqcRFv0WvdLEmNRxxqAYSLl/iQ8AyU4yqVwkwYqvn/aENBg0iQ5h5qboxb8T6xmrZLctk97TiIuCUwbd7Q5NBHJXzHlaKc1p1KBvTCJDMw8+t0Jh3sBkBvu7l0KPliEQHtPLBDkKvy3NBQD6BMy47kPersjSUH53HBvNpcjcPZGpUX/TmL3SNkS98RkoebFxuorK5UdazYN5xvarjfcYziShIjU3WIZJb52DGUxYWTk6vJKZrJhw7AhWIEQpH7qJxvml/B8RMQ4guHdtp3ojxwkHDtZdPU9rY8i6EHwba4qOYgobAgMBAAECggEAT36L5/oAYl+8ZleIAHBxEspS6WYUkvPdJbNN59uus04/60U2rxQSWFulYmeU87h5TmJxs18i2MjF8msfkhpFORfHo4FV+nm0PQEiIIezKRagcfUMhlx/c6eBmdh3mpNDVUN01NWxyb5ovZXXtnjsNikBUZKQwdVcb3d1GnnPO0xtt6/0xwiduCkA2ihS1tgnsYYDhMHgukIdZ3eczn3stRPQ+QyCt1JWS6DDd1nS3S2RyPZw8P9Z1zzJFVKH8z3bGqk3/98Lw7Hw+rKFnKhIA6/H9ZVORKw5OuGC3Ozy6cVbmUn8tuw3sC0NdR7w56dedB+fjJB8od0nahX1Cc6eQQKBgQDckcenslWqjs2PbncwW1wqlw7FdJX9rzJAg7kp9ItpHCoNi/kSgXeLphHXWJmyj7a1BkWynmTGxO48X3dPXUrDPFKJc42fSbxMgAQdtc/A2z+v7Ga/oUpH8jajKfKmcgeRX026R7gd9W0yi0EW+C0WdFhrzNKKY4shvnYy9lc+QwKBgQDB+mHSllqLqYru0bLrtKOKJXaR3N3INxDBZKnRqba4tUKN35IVIexiEMkHmC51jtjoRyA5Y+fc/8P11i9FbuShtRVGHWeyDibKlwff5zrETveSLTpSULBKZ6MsFSm0Fo1krSUC1QTUGG5VX/wwWm9AB2UKJqG5cMDd3i3RiPeDSQKBgBs1ED+rS83iF5Eduy4H1vKZ94R7wRSty7ERjoGSXK/2fWl2Xp7dwXVEYucBUtQnzg2+XFKQHzY1jH19+SWdCF/UzQmPa2S+n6+ACwHvL1VGtjBpJLN2nccKJZsyzW+imTRhYSEdP6TSZUnay4idzFH8v/tsJHxVkw/ygnn+0PwpAn8uOHsWsrzgioWQYmc/wss1H7ghCX/PNU/IxTOxwb7IRGiXZa5pWqv4sgc0yA5J9L+6mTgUdLnK7ybCbUbWRJY18fAfxOHwi26y10oJEA/wtuBG9H/xHUjkcc1vs5s8TiNi2d73zcpYv3mK3lQ5MVNQ7nIk+Q+QIE3UkBxa0UgpAoGBAMDwg0ebzBEZsV2cr/Er2b25LsXteDJ+V67plBNrv+A1/omA9a52sWek4bY0D+Uu6zPTDaLj9BhHC2wJmThYl0eLRKyDKYQslBR3h253Gsn3If6RH9/tSyDsQ88iAEI1f6QH27bGHL9VDrsLGEFg5E7ZEzFQuJPqoUvBOoURNwa6
+#  app-public-key: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCwN6xgd6Ad8v2hIIsQVnbt8a3JituR8o4Tc3B5WlcFR55bz4OMqrG/356Ur3cPbc2Fe8ArNd/0gZbC9q56Eb16JTkVNA/fye4SXznWxdyBPR7+guuJZHc/VW2fKH2lfZ2P3Tt0QkKZZoawYOGSMdIvO+WqK44updyax0ikK6JlNQIDAQAB
+#  wx-app-id:
+#  notify-url: https://nfttest.9space.vip/notify/adapay
 adapay:
-  app-id: app_f8760acc-f4d8-46f6-8f70-d80e36517075
+  app-id: app_843a743d-744e-4d73-bd60-3ebbee15d383
   debug: true
   prod: true
-  api-key: api_live_8818cac1-894b-40bc-ac77-803e02e9c260
-  mock-key: api_test_0b1b0eb9-30e1-4acd-8e03-10b529de1856
+  api-key: api_live_c9db942a-b6d6-4cf7-b1b1-ec1e2873a7e7
+  mock-key: api_test_129dbafc-080a-4a36-94b6-68f3ed2ed217
   public-key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApyGiKxT+ffKnH/7Nxal8TE461QHr539cOQgDHSanERb9Fr3SxJjUccagGEi5f4kPAMlOMqlcJMGKr5/2hDQYNIkOYeam6MW/E+sZq2S3LZPe04iLglMG3e0OTQRyV8x5WinNadSgb0wiQzMPPrdCYd7AZAb7u5dCj5YhEB7TywQ5Cr8tzQUA+gTMuO5D3q7I0lB+dxwbzaXI3D2RqVF/05i90jZEvfEZKHmxcbqKyuVHWs2Decb2q433GM4koSI1N1iGSW+dgxlMWFk5OrySmayYcOwIViBEKR+6icb5pfwfETEOILh3bad6I8cJBw7WXT1Pa2PIuhB8G2uKjmIKGwIDAQAB
   priv-key: MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQCnIaIrFP598qcf/s3FqXxMTjrVAevnf1w5CAMdJqcRFv0WvdLEmNRxxqAYSLl/iQ8AyU4yqVwkwYqvn/aENBg0iQ5h5qboxb8T6xmrZLctk97TiIuCUwbd7Q5NBHJXzHlaKc1p1KBvTCJDMw8+t0Jh3sBkBvu7l0KPliEQHtPLBDkKvy3NBQD6BMy47kPersjSUH53HBvNpcjcPZGpUX/TmL3SNkS98RkoebFxuorK5UdazYN5xvarjfcYziShIjU3WIZJb52DGUxYWTk6vJKZrJhw7AhWIEQpH7qJxvml/B8RMQ4guHdtp3ojxwkHDtZdPU9rY8i6EHwba4qOYgobAgMBAAECggEAT36L5/oAYl+8ZleIAHBxEspS6WYUkvPdJbNN59uus04/60U2rxQSWFulYmeU87h5TmJxs18i2MjF8msfkhpFORfHo4FV+nm0PQEiIIezKRagcfUMhlx/c6eBmdh3mpNDVUN01NWxyb5ovZXXtnjsNikBUZKQwdVcb3d1GnnPO0xtt6/0xwiduCkA2ihS1tgnsYYDhMHgukIdZ3eczn3stRPQ+QyCt1JWS6DDd1nS3S2RyPZw8P9Z1zzJFVKH8z3bGqk3/98Lw7Hw+rKFnKhIA6/H9ZVORKw5OuGC3Ozy6cVbmUn8tuw3sC0NdR7w56dedB+fjJB8od0nahX1Cc6eQQKBgQDckcenslWqjs2PbncwW1wqlw7FdJX9rzJAg7kp9ItpHCoNi/kSgXeLphHXWJmyj7a1BkWynmTGxO48X3dPXUrDPFKJc42fSbxMgAQdtc/A2z+v7Ga/oUpH8jajKfKmcgeRX026R7gd9W0yi0EW+C0WdFhrzNKKY4shvnYy9lc+QwKBgQDB+mHSllqLqYru0bLrtKOKJXaR3N3INxDBZKnRqba4tUKN35IVIexiEMkHmC51jtjoRyA5Y+fc/8P11i9FbuShtRVGHWeyDibKlwff5zrETveSLTpSULBKZ6MsFSm0Fo1krSUC1QTUGG5VX/wwWm9AB2UKJqG5cMDd3i3RiPeDSQKBgBs1ED+rS83iF5Eduy4H1vKZ94R7wRSty7ERjoGSXK/2fWl2Xp7dwXVEYucBUtQnzg2+XFKQHzY1jH19+SWdCF/UzQmPa2S+n6+ACwHvL1VGtjBpJLN2nccKJZsyzW+imTRhYSEdP6TSZUnay4idzFH8v/tsJHxVkw/ygnn+0PwpAn8uOHsWsrzgioWQYmc/wss1H7ghCX/PNU/IxTOxwb7IRGiXZa5pWqv4sgc0yA5J9L+6mTgUdLnK7ybCbUbWRJY18fAfxOHwi26y10oJEA/wtuBG9H/xHUjkcc1vs5s8TiNi2d73zcpYv3mK3lQ5MVNQ7nIk+Q+QIE3UkBxa0UgpAoGBAMDwg0ebzBEZsV2cr/Er2b25LsXteDJ+V67plBNrv+A1/omA9a52sWek4bY0D+Uu6zPTDaLj9BhHC2wJmThYl0eLRKyDKYQslBR3h253Gsn3If6RH9/tSyDsQ88iAEI1f6QH27bGHL9VDrsLGEFg5E7ZEzFQuJPqoUvBOoURNwa6
   app-public-key: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCwN6xgd6Ad8v2hIIsQVnbt8a3JituR8o4Tc3B5WlcFR55bz4OMqrG/356Ur3cPbc2Fe8ArNd/0gZbC9q56Eb16JTkVNA/fye4SXznWxdyBPR7+guuJZHc/VW2fKH2lfZ2P3Tt0QkKZZoawYOGSMdIvO+WqK44updyax0ikK6JlNQIDAQAB

+ 16 - 0
src/main/resources/templates/PayError.ftlh

@@ -0,0 +1,16 @@
+<!doctype html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport"
+          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+</head>
+<body>
+<div style="text-align: center">
+    <div>${msg}</div>
+    <button onclick="window.history.go(-1); return false;">返回</button>
+</div>
+</body>
+</html>

+ 1 - 1
src/main/vue/src/mixins/pageableTable.js

@@ -55,7 +55,7 @@ export default {
                     this.fetchingData = false;
                     this.tableData = res.content;
                     this.totalPages = res.totalPages;
-                    this.totalElements = res.totalElements;
+                    this.totalElements = Number(res.totalElements);
                     if (this.afterGetData) {
                         this.afterGetData(res);
                     }

+ 31 - 6
src/main/vue/src/views/UserList.vue

@@ -42,11 +42,13 @@
                     ></el-image>
                 </template>
             </el-table-column>
-            <el-table-column label="手机" prop="phone"></el-table-column>
-            <el-table-column label="注册时间" prop="createdAt" width="150"></el-table-column>
-            <el-table-column label="操作" align="center" fixed="right" width="200">
+            <el-table-column label="手机" prop="phone" min-width="100"></el-table-column>
+            <el-table-column label="注册时间" prop="createdAt" min-width="150"></el-table-column>
+            <el-table-column label="邀请人数" prop="inviteNum" min-width="80"></el-table-column>
+            <el-table-column label="操作" align="center" fixed="right" width="220">
                 <template slot-scope="{ row }">
-                    <el-button @click="editRow(row)" type="primary" size="mini" plain>编辑</el-button>
+                    <el-button @click="getInviteInfo(row)" type="text" size="mini" plain>邀请列表</el-button>
+                    <el-button @click="editRow(row)" type="text" size="mini" plain>编辑</el-button>
                     <el-button @click="removeCard(row)" size="mini" type="text" :disabled="!row.settleAccountId">
                         解绑卡
                     </el-button>
@@ -74,6 +76,15 @@
             >
             </el-pagination>
         </div>
+
+        <el-dialog :visible.sync="showDialog" title="邀请列表" width="800px" top="10vh">
+            <el-table :data="list" v-loading="dialogLoading" height="60vh">
+                <el-table-column prop="id" label="ID" width="80"></el-table-column>
+                <el-table-column prop="nickname" label="昵称"></el-table-column>
+                <el-table-column prop="phone" label="手机"></el-table-column>
+                <el-table-column prop="createdAt" label="注册时间"></el-table-column>
+            </el-table>
+        </el-dialog>
     </div>
 </template>
 <script>
@@ -89,7 +100,10 @@ export default {
             search: '',
             url: '/user/all',
             downloading: false,
-            createdAt: ''
+            createdAt: '',
+            showDialog: false,
+            dialogLoading: false,
+            list: []
         };
     },
     computed: {
@@ -178,7 +192,7 @@ export default {
                     .then(res => {
                         let el = document.createElement('div');
                         new ClipboardJS(el, {
-                            text: function(trigger) {
+                            text: function (trigger) {
                                 return res;
                             }
                         });
@@ -270,6 +284,17 @@ export default {
                     }
                 }
             }).then(_ => {});
+        },
+        getInviteInfo(row) {
+            this.list = [];
+            this.showDialog = true;
+            this.dialogLoading = true;
+            this.$http
+                .post('/user/all', { size: 10000, sort: 'id,desc', query: { invitor: row.id } }, { body: 'json' })
+                .then(res => {
+                    this.list = res.content;
+                    this.dialogLoading = false;
+                });
         }
     }
 };

+ 30 - 4
src/test/java/com/izouma/nineth/service/AdapayTest.java

@@ -7,10 +7,7 @@ import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.serializer.SerializerFeature;
 import com.huifu.adapay.Adapay;
 import com.huifu.adapay.core.exception.BaseAdaPayException;
-import com.huifu.adapay.model.AdapayCommon;
-import com.huifu.adapay.model.MerConfig;
-import com.huifu.adapay.model.Payment;
-import com.huifu.adapay.model.Refund;
+import com.huifu.adapay.model.*;
 import com.izouma.nineth.utils.SnowflakeIdWorker;
 import com.izouma.nineth.utils.excel.BigIntegerConverter;
 import com.izouma.nineth.utils.excel.LocalDateConverter;
@@ -220,4 +217,33 @@ public class AdapayTest {
         Map<String, Object> map = Payment.query("002112022022011030310341306906502942720");
         System.out.println(JSON.toJSONString(map, SerializerFeature.PrettyFormat));
     }
+
+    @Test
+    public void createmember() throws BaseAdaPayException {
+        Map<String, Object> memberParams = new HashMap<String, Object>(7);
+        memberParams.put("member_id", "member_id_test");
+        memberParams.put("app_id", appId);
+        memberParams.put("location", "上海市徐汇区宜山路700号");
+        memberParams.put("email", "123@163.com");
+        memberParams.put("gender", "MALE");
+        memberParams.put("tel_no", "13153333333");
+        memberParams.put("nickname", "nick_name");
+        Map<String, Object> member = Member.create(memberParams);
+        System.out.println(JSON.toJSONString(member, SerializerFeature.PrettyFormat));
+
+        Map<String, Object> settleCountParams = new HashMap<>();
+        Map<String, Object> accountInfo = new HashMap<>();
+        accountInfo.put("card_id", "6222024301070380165");
+        accountInfo.put("card_name", "熊竹");
+        accountInfo.put("cert_id", "321002199408304614");
+        accountInfo.put("cert_type", "00");
+        accountInfo.put("tel_no", "15077886171");
+        accountInfo.put("bank_acct_type", "2");
+        settleCountParams.put("member_id", "member_id_test");
+        settleCountParams.put("app_id", appId);
+        settleCountParams.put("channel", "bank_account");
+        settleCountParams.put("account_info", accountInfo);
+        Map<String, Object> settleCount = SettleAccount.create(settleCountParams);
+        System.out.println(JSON.toJSONString(member, SerializerFeature.PrettyFormat));
+    }
 }

+ 0 - 2
src/test/java/com/izouma/nineth/service/OrderServiceTest.java

@@ -49,8 +49,6 @@ public class OrderServiceTest extends ApplicationTests {
 
     @Test
     public void create() throws EncoderException, WxPayException {
-        Order order = orderService.create(1110L, 1777L, 1, null, 1896L, null);
-        assert order.getStatus() == OrderStatus.FINISH;
     }
 
     @Test

+ 10 - 0
src/test/java/com/izouma/nineth/service/UserServiceTest.java

@@ -57,4 +57,14 @@ public class UserServiceTest extends ApplicationTests {
     public void removeBankCard() throws BaseAdaPayException {
         userService.removeBankCard(5773L);
     }
+
+    @Test
+    public void switchAccount() {
+        userService.switchAccount();
+    }
+
+    @Test
+    public void test() {
+        userService.phoneRegister("18200001115", "1234", "123456", null, 7834L);
+    }
 }