Преглед на файлове

Merge branch 'dev'

# Conflicts:
#	src/main/java/com/izouma/nineth/service/CacheService.java
#	src/main/java/com/izouma/nineth/service/OrderService.java
#	src/main/java/com/izouma/nineth/service/SysConfigService.java
#	src/main/vue/src/router.js
xiongzhu преди 4 години
родител
ревизия
d866e75701
променени са 70 файла, в които са добавени 2848 реда и са изтрити 792 реда
  1. 12 0
      pom.xml
  2. 8 0
      src/main/java/com/izouma/nineth/annotations/RedisLock.java
  3. 36 18
      src/main/java/com/izouma/nineth/aspect/RedisLockAspect.java
  4. 22 0
      src/main/java/com/izouma/nineth/config/Bucket4jConfig.java
  5. 4 4
      src/main/java/com/izouma/nineth/config/CacheConfig.java
  6. 23 1
      src/main/java/com/izouma/nineth/config/Constants.java
  7. 2 0
      src/main/java/com/izouma/nineth/config/RedisKeys.java
  8. 142 0
      src/main/java/com/izouma/nineth/config/RedissonBasedProxyManager.java
  9. 3 0
      src/main/java/com/izouma/nineth/domain/User.java
  10. 6 0
      src/main/java/com/izouma/nineth/domain/UserBalance.java
  11. 38 0
      src/main/java/com/izouma/nineth/domain/WithdrawApply.java
  12. 30 0
      src/main/java/com/izouma/nineth/dto/PayQuery.java
  13. 1 0
      src/main/java/com/izouma/nineth/enums/BalanceType.java
  14. 10 0
      src/main/java/com/izouma/nineth/enums/PayStatus.java
  15. 7 0
      src/main/java/com/izouma/nineth/enums/WithdrawStatus.java
  16. 1 0
      src/main/java/com/izouma/nineth/event/OrderNotifyEvent.java
  17. 12 4
      src/main/java/com/izouma/nineth/listener/OrderNotifyListener.java
  18. 2 0
      src/main/java/com/izouma/nineth/repo/AssetRepo.java
  19. 4 0
      src/main/java/com/izouma/nineth/repo/BalanceRecordRepo.java
  20. 4 0
      src/main/java/com/izouma/nineth/repo/MintOrderRepo.java
  21. 3 1
      src/main/java/com/izouma/nineth/repo/OrderRepo.java
  22. 3 0
      src/main/java/com/izouma/nineth/repo/TokenHistoryRepo.java
  23. 0 1
      src/main/java/com/izouma/nineth/repo/UserBalanceRepo.java
  24. 23 0
      src/main/java/com/izouma/nineth/repo/WithdrawApplyRepo.java
  25. 2 0
      src/main/java/com/izouma/nineth/security/WebSecurityConfig.java
  26. 4 0
      src/main/java/com/izouma/nineth/service/CacheService.java
  27. 0 17
      src/main/java/com/izouma/nineth/service/GiftOrderService.java
  28. 63 9
      src/main/java/com/izouma/nineth/service/HMPayService.java
  29. 13 5
      src/main/java/com/izouma/nineth/service/IdentityAuthService.java
  30. 8 24
      src/main/java/com/izouma/nineth/service/MintOrderService.java
  31. 89 7
      src/main/java/com/izouma/nineth/service/OrderCancelService.java
  32. 143 75
      src/main/java/com/izouma/nineth/service/OrderPayService.java
  33. 85 79
      src/main/java/com/izouma/nineth/service/OrderService.java
  34. 72 6
      src/main/java/com/izouma/nineth/service/PayEaseService.java
  35. 108 225
      src/main/java/com/izouma/nineth/service/SandPayService.java
  36. 70 2
      src/main/java/com/izouma/nineth/service/SysConfigService.java
  37. 65 44
      src/main/java/com/izouma/nineth/service/UserBalanceService.java
  38. 1 0
      src/main/java/com/izouma/nineth/service/UserBankCardService.java
  39. 33 3
      src/main/java/com/izouma/nineth/service/UserService.java
  40. 179 0
      src/main/java/com/izouma/nineth/service/WithdrawApplyService.java
  41. 1 0
      src/main/java/com/izouma/nineth/utils/JpaUtils.java
  42. 36 0
      src/main/java/com/izouma/nineth/web/CacheController.java
  43. 9 9
      src/main/java/com/izouma/nineth/web/HmPayController.java
  44. 4 1
      src/main/java/com/izouma/nineth/web/MintOrderController.java
  45. 13 12
      src/main/java/com/izouma/nineth/web/OrderPayController.java
  46. 13 5
      src/main/java/com/izouma/nineth/web/OrderPayControllerV2.java
  47. 32 42
      src/main/java/com/izouma/nineth/web/PayChannelMgmtController.java
  48. 11 17
      src/main/java/com/izouma/nineth/web/PayEaseController.java
  49. 16 19
      src/main/java/com/izouma/nineth/web/SandPayController.java
  50. 17 3
      src/main/java/com/izouma/nineth/web/SysConfigController.java
  51. 21 2
      src/main/java/com/izouma/nineth/web/TestClassController.java
  52. 5 0
      src/main/java/com/izouma/nineth/web/UserController.java
  53. 65 0
      src/main/java/com/izouma/nineth/web/WithdrawApplyController.java
  54. 1 0
      src/main/resources/genjson/WithdrawApply.json
  55. 53 18
      src/main/vue/src/router.js
  56. 50 0
      src/main/vue/src/views/Cache.vue
  57. 263 0
      src/main/vue/src/views/PayMgmt.vue
  58. 10 15
      src/main/vue/src/views/SysConfigList.vue
  59. 117 0
      src/main/vue/src/views/WithdrawApplyEdit.vue
  60. 212 0
      src/main/vue/src/views/WithdrawApplyList.vue
  61. 23 0
      src/test/java/com/izouma/nineth/Bucket4jTest.java
  62. 16 0
      src/test/java/com/izouma/nineth/CommonTest.java
  63. 20 1
      src/test/java/com/izouma/nineth/HMPayTest.java
  64. 45 13
      src/test/java/com/izouma/nineth/PayEaseTest.java
  65. 59 0
      src/test/java/com/izouma/nineth/PayOrderTest.java
  66. 46 0
      src/test/java/com/izouma/nineth/TestRedissonLock.java
  67. 84 18
      src/test/java/com/izouma/nineth/repo/UserPropertyRepoTest.java
  68. 53 1
      src/test/java/com/izouma/nineth/service/AirDropServiceTest.java
  69. 159 71
      src/test/java/com/izouma/nineth/service/MintOrderServiceTest.java
  70. 63 20
      src/test/java/com/izouma/nineth/service/SandPayServiceTest.java

+ 12 - 0
pom.xml

@@ -469,6 +469,18 @@
             <artifactId>sdk</artifactId>
             <version>1.0.0</version>
         </dependency>
+
+        <dependency>
+            <groupId>org.redisson</groupId>
+            <artifactId>redisson-spring-boot-starter</artifactId>
+            <version>3.17.3</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.vladimir-bukhtoyarov</groupId>
+            <artifactId>bucket4j-core</artifactId>
+            <version>7.5.0</version>
+        </dependency>
     </dependencies>
 
 </project>

+ 8 - 0
src/main/java/com/izouma/nineth/annotations/RedisLock.java

@@ -12,4 +12,12 @@ public @interface RedisLock {
     long expire() default 10;
 
     TimeUnit unit() default TimeUnit.SECONDS;
+
+    Behavior behavior() default Behavior.THROW;
+
+    enum Behavior {
+        WAIT,
+        THROW
+    }
+
 }

+ 36 - 18
src/main/java/com/izouma/nineth/aspect/RedisLockAspect.java

@@ -8,6 +8,8 @@ import org.aspectj.lang.annotation.Around;
 import org.aspectj.lang.annotation.Aspect;
 import org.aspectj.lang.annotation.Pointcut;
 import org.aspectj.lang.reflect.MethodSignature;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.core.DefaultParameterNameDiscoverer;
 import org.springframework.data.redis.core.BoundValueOperations;
@@ -26,10 +28,13 @@ import java.util.Optional;
 @Slf4j
 public class RedisLockAspect {
 
-    private DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
+    private final DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
 
-    @Autowired
-    private RedisTemplate<String, Object> redisTemplate;
+    private final RedissonClient redissonClient;
+
+    public RedisLockAspect(RedissonClient redissonClient) {
+        this.redissonClient = redissonClient;
+    }
 
     @Pointcut("@annotation(com.izouma.nineth.annotations.RedisLock)")
     public void redisLockPointCut() {
@@ -51,24 +56,37 @@ public class RedisLockAspect {
             key = Optional.ofNullable(parser.parseExpression(redisLock.value()).getValue(context))
                     .map(Object::toString)
                     .orElse("default");
-        } catch (Exception e) {
+        } catch (Exception ignored) {
+        }
+        log.info("enter redisLock aspect key: {}", key);
+        RLock lock = redissonClient.getLock(key);
+        if (redisLock.behavior() == RedisLock.Behavior.WAIT) {
+            lock.lock(redisLock.expire(), redisLock.unit());
+            log.info("get redisLock success");
+        } else {
+            if (!lock.tryLock()) {
+                log.info("get redisLock fail");
+                throw new BusinessException("获取锁失败");
+            }
+            log.info("get redisLock success");
         }
-        BoundValueOperations<String, Object> ops = redisTemplate.boundValueOps(key);
-        Boolean success = ops.setIfAbsent(1, redisLock.expire(), redisLock.unit());
-        if (Boolean.TRUE.equals(success)) {
-            Object res = null;
+        Object res = null;
+        try {
+            res = joinPoint.proceed();
             try {
-                res = joinPoint.proceed();
-            } catch (Throwable e) {
-                redisTemplate.delete(key);
-                throw new RuntimeException(e);
+                lock.unlock();
+            } catch (Exception ignored) {
             }
-            redisTemplate.delete(key);
-            return res;
-        } else {
-            log.info("redis locked, key:{}", key);
+        } catch (Throwable e) {
+            try {
+                lock.unlock();
+            } catch (Exception ignored) {
+            }
+            if (e instanceof BusinessException) {
+                throw (BusinessException) e;
+            }
+            throw new RuntimeException(e);
         }
-        throw new BusinessException("发生错误,请稍后再试");
+        return res;
     }
-
 }

+ 22 - 0
src/main/java/com/izouma/nineth/config/Bucket4jConfig.java

@@ -0,0 +1,22 @@
+package com.izouma.nineth.config;
+
+import io.github.bucket4j.distributed.proxy.ProxyManager;
+import org.redisson.api.RedissonClient;
+import org.redisson.command.CommandSyncService;
+import org.redisson.config.ConfigSupport;
+import org.redisson.connection.ConnectionManager;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.time.Duration;
+
+@Configuration
+public class Bucket4jConfig {
+
+    @Bean
+    ProxyManager<String> proxyManager(RedissonClient client) {
+        ConnectionManager connectionManager = ConfigSupport.createConnectionManager(client.getConfig());
+        CommandSyncService commandSyncService = new CommandSyncService(connectionManager, null);
+        return new RedissonBasedProxyManager(commandSyncService, Duration.ofMinutes(10));
+    }
+}

+ 4 - 4
src/main/java/com/izouma/nineth/config/CacheConfig.java

@@ -125,10 +125,6 @@ public class CacheConfig {
                 .entryTtl(Duration.ofHours(1))
                 .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer())));
 
-        cacheNamesConfigurationMap.put("sandPayGift", RedisCacheConfiguration.defaultCacheConfig()
-                .entryTtl(Duration.ofHours(1))
-                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer())));
-
         RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                 .entryTtl(Duration.ofDays(7))
                 .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer()));
@@ -141,6 +137,10 @@ public class CacheConfig {
                 .entryTtl(Duration.ofHours(1))
                 .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer())));
 
+        cacheNamesConfigurationMap.put("payTmp", RedisCacheConfiguration.defaultCacheConfig()
+                .entryTtl(Duration.ofMinutes(15))
+                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer())));
+
 
         RedisCacheManager redisCacheManager = RedisCacheManager.builder()
                 .cacheWriter(RedisCacheWriter.nonLockingRedisCacheWriter(redisTemplate.getConnectionFactory()))

+ 23 - 1
src/main/java/com/izouma/nineth/config/Constants.java

@@ -2,7 +2,7 @@ package com.izouma.nineth.config;
 
 public interface Constants {
 
-    public interface Regex {
+    interface Regex {
         String PHONE    = "^1[3-9]\\d{9}$";
         String USERNAME = "^[_.@A-Za-z0-9-]*$";
         String CHINESE  = "^[\\u4e00-\\u9fa5]+$";
@@ -16,4 +16,26 @@ public interface Constants {
     String kmsKey = "ydtg$@WZ9NH&EB2e";
 
     String SMS_TOKEN_SECRET = "rjbcsj39s9mg9r";
+
+    String PAY_ERR_MSG = "绿洲宇宙冷却系统已启动,请稍后支付";
+
+    interface PayChannel {
+        String SAND = "sandPay";
+        String HM   = "hmPay";
+        String PE   = "payEase";
+    }
+
+    interface PayType {
+        String ALIPAY    = "alipay";
+        String QUICK     = "quick";
+        String AGREEMENT = "agreement";
+        String BALANCE   = "balance";
+    }
+
+    interface OrderNotifyType {
+        String ORDER    = "order";
+        String GIFT     = "gift";
+        String MINT     = "mintOrder";
+        String RECHARGE = "recharge";
+    }
 }

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

@@ -38,4 +38,6 @@ public class RedisKeys {
     public static final String JWT_TOKEN = "jwtToken::";
 
     public static final String BIND_CARD = "bindCard::";
+
+    public static final String PAY_TMP = "payTmp::";
 }

+ 142 - 0
src/main/java/com/izouma/nineth/config/RedissonBasedProxyManager.java

@@ -0,0 +1,142 @@
+package com.izouma.nineth.config;
+
+import io.github.bucket4j.distributed.proxy.ClientSideConfig;
+import io.github.bucket4j.distributed.proxy.generic.compare_and_swap.AbstractCompareAndSwapBasedProxyManager;
+import io.github.bucket4j.distributed.proxy.generic.compare_and_swap.AsyncCompareAndSwapOperation;
+import io.github.bucket4j.distributed.proxy.generic.compare_and_swap.CompareAndSwapOperation;
+import io.netty.buffer.ByteBuf;
+import org.redisson.api.RFuture;
+import org.redisson.client.codec.ByteArrayCodec;
+import org.redisson.client.protocol.RedisCommand;
+import org.redisson.client.protocol.RedisCommands;
+import org.redisson.client.protocol.convertor.BooleanNotNullReplayConvertor;
+import org.redisson.command.CommandExecutor;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+
+public class RedissonBasedProxyManager extends AbstractCompareAndSwapBasedProxyManager<String> {
+
+    public static RedisCommand<Boolean> SETPXNX_WORK_ARROUND = new RedisCommand<Boolean>("SET", new BooleanNotNullReplayConvertor());
+
+    private final CommandExecutor commandExecutor;
+    private final long            ttlMillis;
+
+    public RedissonBasedProxyManager(CommandExecutor commandExecutor, Duration ttl) {
+        this(commandExecutor, ClientSideConfig.getDefault(), ttl);
+    }
+
+    public RedissonBasedProxyManager(CommandExecutor commandExecutor, ClientSideConfig clientSideConfig, Duration ttl) {
+        super(clientSideConfig);
+        this.commandExecutor = Objects.requireNonNull(commandExecutor);
+        this.ttlMillis = ttl.toMillis();
+    }
+
+    @Override
+    protected CompareAndSwapOperation beginCompareAndSwapOperation(String key) {
+        List<Object> keys = Collections.singletonList(key);
+        return new CompareAndSwapOperation() {
+            @Override
+            public Optional<byte[]> getStateData() {
+                byte[] persistedState = commandExecutor.read(key, ByteArrayCodec.INSTANCE, RedisCommands.GET, key);
+                return Optional.ofNullable(persistedState);
+            }
+
+            @Override
+            public boolean compareAndSwap(byte[] originalData, byte[] newData) {
+                if (originalData == null) {
+                    // Redisson prohibits the usage null as values, so "replace" must not be used in such cases
+                    RFuture<Boolean> redissonFuture = commandExecutor.writeAsync(key, ByteArrayCodec.INSTANCE, SETPXNX_WORK_ARROUND, key, encodeByteArray(newData), "PX", ttlMillis, "NX");
+                    return commandExecutor.get(redissonFuture);
+                } else {
+                    String script =
+                            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
+                                    "redis.call('psetex', KEYS[1], ARGV[3], ARGV[2]); " +
+                                    "return 1; " +
+                                    "else " +
+                                    "return 0; " +
+                                    "end";
+                    Object[] params = new Object[]{originalData, newData, ttlMillis};
+                    RFuture<Boolean> redissonFuture = commandExecutor.evalWriteAsync(key, ByteArrayCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, script, keys, params);
+                    return commandExecutor.get(redissonFuture);
+                }
+            }
+        };
+    }
+
+
+    @Override
+    protected AsyncCompareAndSwapOperation beginAsyncCompareAndSwapOperation(String key) {
+        List<Object> keys = Collections.singletonList(key);
+        return new AsyncCompareAndSwapOperation() {
+            @Override
+            public CompletableFuture<Optional<byte[]>> getStateData() {
+                RFuture<byte[]> redissonFuture = commandExecutor.readAsync(key, ByteArrayCodec.INSTANCE, RedisCommands.GET, key);
+                return convertFuture(redissonFuture)
+                        .thenApply((byte[] resultBytes) -> Optional.ofNullable(resultBytes));
+            }
+
+            @Override
+            public CompletableFuture<Boolean> compareAndSwap(byte[] originalData, byte[] newData) {
+                if (originalData == null) {
+                    RFuture<Boolean> redissonFuture = commandExecutor.writeAsync(key, ByteArrayCodec.INSTANCE, SETPXNX_WORK_ARROUND, key, encodeByteArray(newData), "PX", ttlMillis, "NX");
+                    return convertFuture(redissonFuture);
+                } else {
+                    String script =
+                            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
+                                    "redis.call('psetex', KEYS[1], ARGV[3], ARGV[2]); " +
+                                    "return 1; " +
+                                    "else " +
+                                    "return 0; " +
+                                    "end";
+                    Object[] params = new Object[]{encodeByteArray(originalData), encodeByteArray(newData), ttlMillis};
+                    RFuture<Boolean> redissonFuture = commandExecutor.evalWriteAsync(key, ByteArrayCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, script, keys, params);
+                    return convertFuture(redissonFuture);
+                }
+            }
+        };
+    }
+
+    @Override
+    public void removeProxy(String key) {
+        RFuture<Object> future = commandExecutor.writeAsync(key, RedisCommands.DEL_VOID, key);
+        commandExecutor.get(future);
+    }
+
+    @Override
+    protected CompletableFuture<Void> removeAsync(String key) {
+        RFuture<?> redissonFuture = commandExecutor.writeAsync(key, RedisCommands.DEL_VOID, key);
+        return convertFuture(redissonFuture).thenApply(bytes -> null);
+    }
+
+    @Override
+    public boolean isAsyncModeSupported() {
+        return true;
+    }
+
+    private <T> CompletableFuture<T> convertFuture(RFuture<T> redissonFuture) {
+        CompletableFuture<T> jdkFuture = new CompletableFuture<>();
+        redissonFuture.whenComplete((result, error) -> {
+            if (error != null) {
+                jdkFuture.completeExceptionally(error);
+            } else {
+                jdkFuture.complete(result);
+            }
+        });
+        return jdkFuture;
+    }
+
+    public ByteBuf encodeByteArray(byte[] value) {
+        try {
+            return ByteArrayCodec.INSTANCE.getValueEncoder().encode(value);
+        } catch (IOException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+}

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

@@ -173,4 +173,7 @@ public class User extends UserBaseEntity implements Serializable {
 
     @Column(columnDefinition = "tinyint unsigned default 0")
     private boolean company = false;
+
+    @Column(columnDefinition = "tinyint unsigned default 0")
+    private boolean walletEnabled = false;
 }

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

@@ -32,4 +32,10 @@ public class UserBalance {
     private String lockReason;
 
     private LocalDateTime lockTime;
+
+    public UserBalance(Long userId) {
+        this.userId = userId;
+        this.balance = BigDecimal.ZERO;
+        this.lastBalance = BigDecimal.ZERO;
+    }
 }

+ 38 - 0
src/main/java/com/izouma/nineth/domain/WithdrawApply.java

@@ -0,0 +1,38 @@
+package com.izouma.nineth.domain;
+
+import com.izouma.nineth.enums.PayMethod;
+import com.izouma.nineth.enums.WithdrawStatus;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@Entity
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class WithdrawApply extends BaseEntity {
+    private Long userId;
+
+    private BigDecimal amount;
+
+    @Enumerated(EnumType.STRING)
+    @Column(length = 20)
+    private WithdrawStatus status;
+
+    private String reason;
+
+    private String withdrawId;
+
+    private String channel;
+
+    private LocalDateTime finishTime;
+}

+ 30 - 0
src/main/java/com/izouma/nineth/dto/PayQuery.java

@@ -0,0 +1,30 @@
+package com.izouma.nineth.dto;
+
+import com.izouma.nineth.enums.PayStatus;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Entity;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class PayQuery {
+    private boolean       exist;
+    private String        msg;
+    private PayStatus     status;
+    private BigDecimal    amount;
+    private LocalDateTime createTime;
+    private LocalDateTime payTime;
+    private String        transactionId;
+    private String        channel;
+
+    public PayQuery(String channel) {
+        this.channel = channel;
+    }
+}

+ 1 - 0
src/main/java/com/izouma/nineth/enums/BalanceType.java

@@ -7,6 +7,7 @@ public enum BalanceType {
     RETURN("失败退回"),
     PAY("支付"),
     RECHARGE("充值"),
+    DENY("拒绝"),
     ;
 
     private final String description;

+ 10 - 0
src/main/java/com/izouma/nineth/enums/PayStatus.java

@@ -0,0 +1,10 @@
+package com.izouma.nineth.enums;
+
+public enum PayStatus {
+    SUCCESS,
+    PENDING,
+    FAIL,
+    CANCEL,
+    REFUNDED,
+    REFUNDING
+}

+ 7 - 0
src/main/java/com/izouma/nineth/enums/WithdrawStatus.java

@@ -0,0 +1,7 @@
+package com.izouma.nineth.enums;
+
+public enum WithdrawStatus {
+    PENDING,
+    SUCCESS,
+    FAIL
+}

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

@@ -14,6 +14,7 @@ public class OrderNotifyEvent implements Serializable {
     public static final String TYPE_ORDER      = "order";
     public static final String TYPE_MINT_ORDER = "mint_order";
     public static final String TYPE_GIFT_ORDER = "gift_order";
+    public static final String TYPE_RECHARGE   = "recharge";
 
     private Long      orderId;
     private PayMethod payMethod;

+ 12 - 4
src/main/java/com/izouma/nineth/listener/OrderNotifyListener.java

@@ -5,6 +5,7 @@ import com.izouma.nineth.event.OrderNotifyEvent;
 import com.izouma.nineth.service.GiftOrderService;
 import com.izouma.nineth.service.MintOrderService;
 import com.izouma.nineth.service.OrderService;
+import com.izouma.nineth.service.UserBalanceService;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.rocketmq.spring.annotation.ConsumeMode;
@@ -19,12 +20,13 @@ import org.springframework.stereotype.Service;
 @RocketMQMessageListener(
         consumerGroup = "${general.order-notify-group}",
         topic = "${general.order-notify-topic}",
-        consumeMode = ConsumeMode.ORDERLY)
+        consumeMode = ConsumeMode.CONCURRENTLY, consumeThreadMax = 2)
 @ConditionalOnProperty(value = "general.notify-server", havingValue = "true")
 public class OrderNotifyListener implements RocketMQListener<OrderNotifyEvent> {
-    private OrderService     orderService;
-    private MintOrderService mintOrderService;
-    private GiftOrderService giftOrderService;
+    private OrderService       orderService;
+    private MintOrderService   mintOrderService;
+    private GiftOrderService   giftOrderService;
+    private UserBalanceService userBalanceService;
 
     @Override
     public void onMessage(OrderNotifyEvent e) {
@@ -35,6 +37,12 @@ public class OrderNotifyListener implements RocketMQListener<OrderNotifyEvent> {
             case OrderNotifyEvent.TYPE_MINT_ORDER:
                 mintOrderService.mintNotify(e.getOrderId(), e.getPayMethod(), e.getTransactionId());
                 break;
+            case OrderNotifyEvent.TYPE_GIFT_ORDER:
+                giftOrderService.giftNotify(e.getOrderId(), e.getPayMethod(), e.getTransactionId());
+                break;
+            case OrderNotifyEvent.TYPE_RECHARGE:
+                userBalanceService.recharge(e.getOrderId(), e.getPayMethod(), e.getTransactionId());
+                break;
             case OrderNotifyEvent.TYPE_ORDER:
             default:
                 orderService.notifyOrder(e.getOrderId(), e.getPayMethod(), e.getTransactionId());

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

@@ -75,4 +75,6 @@ public interface AssetRepo extends JpaRepository<Asset, Long>, JpaSpecificationE
                   String ownerAvatar, String detail);
 
     Page<Asset> findByUserIdAndStatusAndNameLike(Long userId, AssetStatus status, String name, Pageable pageable);
+
+    List<Asset> findAllByUserIdAndCollectionIdAndStatus(Long userId, Long collectionId, AssetStatus status);
 }

+ 4 - 0
src/main/java/com/izouma/nineth/repo/BalanceRecordRepo.java

@@ -1,6 +1,7 @@
 package com.izouma.nineth.repo;
 
 import com.izouma.nineth.domain.BalanceRecord;
+import com.izouma.nineth.enums.BalanceType;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.jpa.repository.JpaRepository;
@@ -8,6 +9,7 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
 
 import java.time.LocalDateTime;
 import java.util.List;
+import java.util.Optional;
 
 public interface BalanceRecordRepo extends JpaRepository<BalanceRecord, Long>, JpaSpecificationExecutor<BalanceRecord> {
     List<BalanceRecord> findByUserIdAndCreatedAtBetweenOrderByCreatedAt(Long userId, LocalDateTime start, LocalDateTime end);
@@ -15,4 +17,6 @@ public interface BalanceRecordRepo extends JpaRepository<BalanceRecord, Long>, J
     List<BalanceRecord> findByUserIdOrderByCreatedAt(Long userId);
 
     Page<BalanceRecord> findByUserId(Long userId, Pageable pageable);
+
+    BalanceRecord findByOrderIdAndType(Long orderId, BalanceType type);
 }

+ 4 - 0
src/main/java/com/izouma/nineth/repo/MintOrderRepo.java

@@ -20,4 +20,8 @@ public interface MintOrderRepo extends JpaRepository<MintOrder, Long>, JpaSpecif
     List<MintOrder> findByStatusAndCreatedAtBeforeAndDelFalse(MintOrderStatus status, LocalDateTime createdAt);
 
     List<MintOrder> findByStatusAndCreatedAtAfter(MintOrderStatus status, LocalDateTime time);
+
+    List<MintOrder> findByStatusAndCreatedAtAfterOrderByCreatedAtDesc(MintOrderStatus status, LocalDateTime time);
+
+    List<MintOrder> findByMintActivityIdAndStatusOrderById(Long id, MintOrderStatus status);
 }

+ 3 - 1
src/main/java/com/izouma/nineth/repo/OrderRepo.java

@@ -10,6 +10,7 @@ import org.springframework.data.jpa.repository.Modifying;
 import org.springframework.data.jpa.repository.Query;
 
 import javax.transaction.Transactional;
+import java.math.BigDecimal;
 import java.time.LocalDateTime;
 import java.util.Collection;
 import java.util.List;
@@ -82,5 +83,6 @@ public interface OrderRepo extends JpaRepository<Order, Long>, JpaSpecificationE
     @Modifying
     int processingOrder(Long id, LocalDateTime payTime, PayMethod payMethod, String transactionId);
 
-
+    @Query(value = "select sum(price) from order_info where user_id = ?1 and status = 'FINISH'", nativeQuery = true)
+    BigDecimal sumUserPrice(Long userId);
 }

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

@@ -14,6 +14,7 @@ import java.time.LocalDateTime;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 
 public interface TokenHistoryRepo extends JpaRepository<TokenHistory, Long>, JpaSpecificationExecutor<TokenHistory> {
     List<TokenHistory> findByTokenIdOrderByCreatedAtDesc(String tokenId);
@@ -48,4 +49,6 @@ public interface TokenHistoryRepo extends JpaRepository<TokenHistory, Long>, Jpa
     @Query(nativeQuery = true, value = "select to_user_id , to_user, to_avatar, sum(price) total from token_history " +
             "where created_at between ?1 and ?2 group by to_user_id order by sum(price) desc limit ?3")
     List<Map<String, Object>> sumPrice(LocalDateTime start, LocalDateTime end, int size);
+
+    Optional<TokenHistory> findByToUserIdAndTokenId(Long toUserId, String tokenId);
 }

+ 0 - 1
src/main/java/com/izouma/nineth/repo/UserBalanceRepo.java

@@ -1,7 +1,6 @@
 package com.izouma.nineth.repo;
 
 import com.izouma.nineth.domain.UserBalance;
-import org.springframework.data.domain.Page;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
 import org.springframework.data.jpa.repository.Modifying;

+ 23 - 0
src/main/java/com/izouma/nineth/repo/WithdrawApplyRepo.java

@@ -0,0 +1,23 @@
+package com.izouma.nineth.repo;
+
+import com.izouma.nineth.domain.WithdrawApply;
+import com.izouma.nineth.enums.WithdrawStatus;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+
+import javax.transaction.Transactional;
+import java.time.LocalDateTime;
+import java.util.List;
+
+public interface WithdrawApplyRepo extends JpaRepository<WithdrawApply, Long>, JpaSpecificationExecutor<WithdrawApply> {
+    @Query("update WithdrawApply t set t.del = true where t.id = ?1")
+    @Modifying
+    @Transactional
+    void softDelete(Long id);
+
+    int countByUserIdAndCreatedAtBetween(Long userId, LocalDateTime start, LocalDateTime end);
+
+    List<WithdrawApply> findByCreatedAtBetweenAndStatus(LocalDateTime start, LocalDateTime end, WithdrawStatus status);
+}

+ 2 - 0
src/main/java/com/izouma/nineth/security/WebSecurityConfig.java

@@ -125,6 +125,8 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
                 .antMatchers("/ossNotify").permitAll()
                 .antMatchers("/priceList/list").permitAll()
                 .antMatchers("/user/collectionInvitorList").permitAll()
+                .antMatchers("/payOrder/v2/**/sandQuick").permitAll()
+                .antMatchers("/pay/v2/**/sandQuick").permitAll()
                 // all other requests need to be authenticated
                 .anyRequest().authenticated().and()
                 // make sure we use stateless session; session won't be used to

+ 4 - 0
src/main/java/com/izouma/nineth/service/CacheService.java

@@ -118,4 +118,8 @@ public class CacheService {
     public void clearFixedTop() {
     }
 
+
+    @CacheEvict(value = "sysConfigGet", allEntries = true)
+    public void clearSysConfigGet() {
+    }
 }

+ 0 - 17
src/main/java/com/izouma/nineth/service/GiftOrderService.java

@@ -350,23 +350,6 @@ public class GiftOrderService {
         }
     }
 
-    @Scheduled(fixedRate = 60000)
-    public void batchCancel() {
-        if (generalProperties.isNotifyServer()) {
-            return;
-        }
-        if (Arrays.asList(env.getActiveProfiles()).contains("dev")) {
-            return;
-        }
-        List<GiftOrder> orders = giftOrderRepo.findByStatusAndCreatedAtBeforeAndDelFalse(OrderStatus.NOT_PAID,
-                LocalDateTime.now().minusMinutes(5));
-        orders.forEach(o -> {
-            try {
-                cancel(o);
-            } catch (Exception ignored) {
-            }
-        });
-    }
 
     public void cancel(GiftOrder order) {
         if (order.getStatus() != OrderStatus.NOT_PAID) {

+ 63 - 9
src/main/java/com/izouma/nineth/service/HMPayService.java

@@ -5,15 +5,21 @@ import com.alibaba.fastjson.JSONObject;
 import com.alipay.api.AlipayApiException;
 import com.alipay.api.internal.util.AlipaySignature;
 import com.github.kevinsawicki.http.HttpRequest;
+import com.izouma.nineth.config.Constants;
 import com.izouma.nineth.config.GeneralProperties;
 import com.izouma.nineth.config.HmPayProperties;
+import com.izouma.nineth.config.RedisKeys;
+import com.izouma.nineth.dto.PayQuery;
+import com.izouma.nineth.enums.PayStatus;
 import com.izouma.nineth.exception.BusinessException;
 import com.izouma.nineth.utils.DateTimeUtils;
 import com.izouma.nineth.utils.SnowflakeIdWorker;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 
 import java.math.BigDecimal;
@@ -21,16 +27,18 @@ import java.math.RoundingMode;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
 import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.*;
+import java.util.concurrent.TimeUnit;
 
 @Service
 @Slf4j
 @AllArgsConstructor
 @EnableConfigurationProperties({HmPayProperties.class})
 public class HMPayService {
-    private final HmPayProperties   hmPayProperties;
-    private final GeneralProperties generalProperties;
-    private final SnowflakeIdWorker snowflakeIdWorker;
+    private final HmPayProperties               hmPayProperties;
+    private final SnowflakeIdWorker             snowflakeIdWorker;
+    private final RedisTemplate<String, Object> redisTemplate;
 
     public String paddingOrderId(String orderId) {
         if (orderId != null && orderId.length() < 12) {
@@ -101,15 +109,11 @@ public class HMPayService {
 
     public JSONObject requestAlipayRaw(String orderId, BigDecimal amount, String subject,
                                        String timeout, String type, String returnUrl) {
-        if (orderId.length() < 12) {
-            for (int i = orderId.length(); i < 12; i++) {
-                orderId = "0" + orderId;
-            }
-        }
+        String pOrderId = paddingOrderId(orderId);
         Map<String, String> params = new HashMap<>();
         params.put("version", "10");
         params.put("mer_no", hmPayProperties.getMerNo());
-        params.put("mer_order_no", orderId);
+        params.put("mer_order_no", pOrderId);
         params.put("create_time", getReqTime());
         params.put("expire_time", timeout);
         params.put("order_amt", amount.setScale(2, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString());
@@ -141,22 +145,72 @@ public class HMPayService {
                                 String timeout, String type, String returnUrl) {
         JSONObject res = requestAlipayRaw(orderId, amount, subject, timeout, type, returnUrl);
         if ("000000".equals(res.getString("ret_code"))) {
+            redisTemplate.opsForValue()
+                    .set(RedisKeys.PAY_TMP + orderId, Constants.PayChannel.HM, 1, TimeUnit.DAYS);
             return res.getJSONObject("data").getString("token_id");
         }
         throw new BusinessException("绿洲宇宙冷却系统已启动,请稍后支付");
     }
 
     public JSONObject query(String orderId) {
+        orderId = paddingOrderId(orderId);
         Map<String, String> bizContent = new HashMap<>();
         bizContent.put("out_order_no", orderId);
         return requestApi("trade.query", bizContent);
     }
 
     public JSONObject refund(String orderId, BigDecimal amount) {
+        orderId = paddingOrderId(orderId);
         Map<String, String> bizContent = new HashMap<>();
         bizContent.put("out_order_no", paddingOrderId(orderId));
         bizContent.put("refund_amount", amount.stripTrailingZeros().toPlainString());
         bizContent.put("refund_request_no", snowflakeIdWorker.nextId() + "");
         return requestApi("trade.refund", bizContent);
     }
+
+    public PayQuery payQuery(String orderId) {
+        JSONObject res = query(orderId);
+        PayQuery query = new PayQuery(Constants.PayChannel.HM);
+        if ("200".equals(res.getString("code"))) {
+            query.setExist(true);
+            JSONObject data = res.getJSONObject("data");
+            String sub_code = data.getString("sub_code");
+            if ("ILLEGAL_ORDER".equals(sub_code)) {
+                query.setExist(false);
+                return query;
+            }
+            if (data.containsKey("total_amount")) {
+                query.setAmount(new BigDecimal(data.getString("total_amount")));
+            }
+            query.setTransactionId(data.getString("plat_trx_no"));
+            switch (sub_code) {
+                case "SUCCESS":
+                    query.setStatus(PayStatus.SUCCESS);
+                    break;
+                case "FINISH":
+                case "FAILED":
+                    query.setStatus(PayStatus.FAIL);
+                    break;
+                case "CREATED":
+                case "WAITING_PAYMENT":
+                    query.setStatus(PayStatus.PENDING);
+                    break;
+                case "CLOSED":
+                case "CANCELED":
+                    query.setStatus(PayStatus.CANCEL);
+                    break;
+            }
+            if (Objects.equals(Boolean.TRUE, data.getBoolean("is_refund"))) {
+                query.setStatus(PayStatus.REFUNDED);
+            }
+            String successTime = data.getString("success_time");
+            if (StringUtils.isNotEmpty(successTime)) {
+                query.setPayTime(LocalDateTime.parse(successTime, DateTimeFormatter.ofPattern("yyyyMMddHHmmss")));
+            }
+        } else {
+            query.setExist(false);
+            query.setMsg(res.getString("msg"));
+        }
+        return query;
+    }
 }

+ 13 - 5
src/main/java/com/izouma/nineth/service/IdentityAuthService.java

@@ -133,13 +133,17 @@ public class IdentityAuthService {
 //            }
 //        }
 //    }
-    public void validate(String name, String phone, String idno) {
-        String body = HttpRequest.post("https://zid.market.alicloudapi.com/idcheck/Post")
+    public static void validate(String name, String phone, String idno) {
+        HttpRequest request = HttpRequest.post("https://zid.market.alicloudapi.com/idcheck/Post")
                 .header("Authorization", "APPCODE b48bc8f6759345a79ae20a951f03dabe")
                 .contentType(HttpRequest.CONTENT_TYPE_FORM)
                 .form("cardNo", idno)
-                .form("realName", name)
-                .body();
+                .form("realName", name);
+        int code = request.code();
+        if (code != 200) {
+            throw new BusinessException("网络异常", "网络异常");
+        }
+        String body = request.body();
         JSONObject jsonObject = JSONObject.parseObject(body);
         log.info("validate {} {} \n{}", name, idno, JSON.toJSONString(jsonObject, SerializerFeature.PrettyFormat));
         if (jsonObject.getInteger("error_code") != 0) {
@@ -221,7 +225,9 @@ public class IdentityAuthService {
             List<IdentityAuth> list = identityAuthRepo.findByStatusAndAutoValidated(AuthStatus.PENDING, false);
             list.parallelStream().forEach(identityAuth -> {
                 Map<String, Object> map = auth(identityAuth);
-                audit(identityAuth.getId(), (AuthStatus) map.get("status"), (String) map.get("reason"));
+                if (map != null) {
+                    audit(identityAuth.getId(), (AuthStatus) map.get("status"), (String) map.get("reason"));
+                }
             });
         } catch (Exception e) {
             log.error("批量自动实名出错", e);
@@ -269,6 +275,8 @@ public class IdentityAuthService {
                         log.error("自动实名出错", e);
                         if ("不匹配".equals(e.getMessage())) {
                             result.put("status", AuthStatus.FAIL);
+                        } else if ("网络异常".equals(e.getMessage())) {
+                            return null;
                         } else {
                             result.put("status", AuthStatus.PENDING);
                         }

+ 8 - 24
src/main/java/com/izouma/nineth/service/MintOrderService.java

@@ -12,10 +12,7 @@ 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.GeneralProperties;
-import com.izouma.nineth.config.RedisKeys;
-import com.izouma.nineth.config.WxPayProperties;
+import com.izouma.nineth.config.*;
 import com.izouma.nineth.domain.*;
 import com.izouma.nineth.domain.Collection;
 import com.izouma.nineth.dto.PageQuery;
@@ -583,24 +580,6 @@ public class MintOrderService {
         }
     }
 
-    @Scheduled(fixedRate = 60000)
-    public void batchCancel() {
-        if (generalProperties.isNotifyServer()) {
-            return;
-        }
-        if (Arrays.asList(env.getActiveProfiles()).contains("dev")) {
-            return;
-        }
-        List<MintOrder> orders = mintOrderRepo.findByStatusAndCreatedAtBeforeAndDelFalse(MintOrderStatus.NOT_PAID,
-                LocalDateTime.now().minusMinutes(5));
-        orders.forEach(o -> {
-            try {
-                cancel(o, false);
-            } catch (Exception ignored) {
-            }
-        });
-    }
-
     public void cancel(MintOrder order, boolean refund) {
         if (!getOrderLock(order.getId())) {
             log.error("订单取消失败 {}, redis锁了", order.getId());
@@ -662,13 +641,18 @@ public class MintOrderService {
                 try {
                     switch (payMethod) {
                         case HMPAY:
-                            orderPayService.refund(order.getId().toString(), order.getGasPrice(), "hmPay");
+                            orderPayService.refund(order.getId().toString(), order.getTransactionId(),
+                                    order.getGasPrice(), Constants.PayChannel.HM);
                             log.info("退款成功");
                             break;
                         case SANDPAY:
-                            orderPayService.refund(order.getId().toString(), order.getGasPrice(), "sandPay");
+                            orderPayService.refund(order.getId().toString(), order.getTransactionId(),
+                                    order.getGasPrice(), Constants.PayChannel.SAND);
                             log.info("退款成功");
                             break;
+                        case PAYEASE:
+                            orderPayService.refund(order.getTransactionId(), order.getTransactionId(),
+                                    order.getGasPrice(), Constants.PayChannel.PE);
                     }
                 } catch (Exception e) {
                     log.error("铸造订单退款失败 {} ", order.getId(), e);

+ 89 - 7
src/main/java/com/izouma/nineth/service/OrderCancelService.java

@@ -1,52 +1,73 @@
 package com.izouma.nineth.service;
 
+import com.izouma.nineth.annotations.RedisLock;
 import com.izouma.nineth.config.GeneralProperties;
+import com.izouma.nineth.config.RedisKeys;
+import com.izouma.nineth.domain.GiftOrder;
+import com.izouma.nineth.domain.MintOrder;
 import com.izouma.nineth.domain.Order;
 import com.izouma.nineth.domain.SysConfig;
+import com.izouma.nineth.dto.PayQuery;
+import com.izouma.nineth.enums.MintOrderStatus;
 import com.izouma.nineth.enums.OrderStatus;
+import com.izouma.nineth.enums.PayStatus;
 import com.izouma.nineth.exception.BusinessException;
+import com.izouma.nineth.repo.GiftOrderRepo;
+import com.izouma.nineth.repo.MintOrderRepo;
 import com.izouma.nineth.repo.OrderRepo;
 import com.izouma.nineth.repo.SysConfigRepo;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.PostConstruct;
 import java.time.LocalDateTime;
+import java.util.Arrays;
 import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 @Service
 @ConditionalOnProperty(value = "general.notify-server", havingValue = "true")
 @Slf4j
 @AllArgsConstructor
 public class OrderCancelService {
-    private final GeneralProperties generalProperties;
-    private final OrderRepo         orderRepo;
-    private final OrderService      orderService;
-    private final SysConfigRepo     sysConfigRepo;
+    private final GeneralProperties             generalProperties;
+    private final OrderRepo                     orderRepo;
+    private final OrderService                  orderService;
+    private final SysConfigRepo                 sysConfigRepo;
+    private final MintOrderRepo                 mintOrderRepo;
+    private final MintOrderService              mintOrderService;
+    private final GiftOrderRepo                 giftOrderRepo;
+    private final GiftOrderService              giftOrderService;
+    private final OrderPayService               orderPayService;
+    private final RedisTemplate<String, Object> redisTemplate;
 
     private static int orderCancelInterval = 210;
 
     public static void setOrderCancelInterval(int orderCancelInterval) {
+        log.info("设置订单取消时间间隔为 {}S", orderCancelInterval);
         OrderCancelService.orderCancelInterval = orderCancelInterval;
     }
 
     @PostConstruct
     public void init() {
-        orderCancelInterval = sysConfigRepo.findByName("order_cancel_interval")
+        orderCancelInterval = sysConfigRepo.findByName("order_cancel_time")
                 .map(SysConfig::getValue).map(Integer::parseInt).orElse(210);
     }
 
-    @Scheduled(fixedRate = 30000)
+    @Scheduled(fixedRate = 30000, initialDelay = 10000)
+    @RedisLock(value = "order_batch_cancel", expire = 3, unit = TimeUnit.MINUTES)
     public void batchCancel() {
         List<Order> orders = orderRepo.findByStatusAndCreatedAtBeforeAndDelFalse(OrderStatus.NOT_PAID,
                 LocalDateTime.now().minusSeconds(orderCancelInterval));
         orders.parallelStream().forEach(o -> {
             try {
                 Order order = orderRepo.findById(o.getId()).orElseThrow(new BusinessException("订单不存在"));
-                if (order.getStatus() == OrderStatus.NOT_PAID) {
+                if (order.getStatus() == OrderStatus.NOT_PAID && canCancel(order.getId().toString())) {
                     orderService.cancel(order);
                 }
             } catch (Exception e) {
@@ -54,4 +75,65 @@ public class OrderCancelService {
             }
         });
     }
+
+    @Scheduled(fixedRate = 30000, initialDelay = 20000)
+    @RedisLock(value = "mint_order_batch_cancel", expire = 3, unit = TimeUnit.MINUTES)
+    public void batchCancelMintOrder() {
+        List<MintOrder> orders = mintOrderRepo.findByStatusAndCreatedAtBeforeAndDelFalse(MintOrderStatus.NOT_PAID,
+                LocalDateTime.now().minusSeconds(orderCancelInterval));
+        orders.forEach(o -> {
+            try {
+                MintOrder order = mintOrderRepo.findById(o.getId()).orElseThrow(new BusinessException("订单不存在"));
+                if (order.getStatus() == MintOrderStatus.NOT_PAID && canCancel(order.getId().toString())) {
+                    mintOrderService.cancel(order, false);
+                }
+            } catch (Exception ignored) {
+            }
+        });
+    }
+
+    @Scheduled(fixedRate = 30000, initialDelay = 30000)
+    @RedisLock(value = "gift_order_batch_cancel", expire = 3, unit = TimeUnit.MINUTES)
+    public void batchCancelGiftOrder() {
+        List<GiftOrder> orders = giftOrderRepo.findByStatusAndCreatedAtBeforeAndDelFalse(OrderStatus.NOT_PAID,
+                LocalDateTime.now().minusSeconds(orderCancelInterval));
+        orders.forEach(o -> {
+            try {
+                GiftOrder order = giftOrderRepo.findById(o.getId()).orElseThrow(new BusinessException("订单不存在"));
+                if (order.getStatus() == OrderStatus.NOT_PAID && canCancel(order.getId().toString())) {
+                    giftOrderService.cancel(order);
+                }
+            } catch (Exception ignored) {
+            }
+        });
+    }
+
+    private boolean canCancel(String id) {
+        String channel = null;
+        Object payTmp = redisTemplate.opsForValue().get(RedisKeys.PAY_TMP + id);
+        log.info("payTmp {}", payTmp);
+        if (payTmp != null) {
+            channel = (String) payTmp;
+        }
+        PayQuery query = new PayQuery();
+        try {
+            query = orderPayService.query(id, channel);
+        } catch (Exception e) {
+            query.setExist(false);
+        }
+        if (query.isExist()) {
+            if (query.getStatus() != null) {
+                switch (query.getStatus()) {
+                    case SUCCESS:
+                    case PENDING:
+                        log.info("订单 {}, 状态 {}, 不能取消", id, query.getStatus().name());
+                        return false;
+                    default:
+                        log.info("订单 {}, 状态 {}, 可以取消", id, query.getStatus().name());
+                        return true;
+                }
+            }
+        }
+        return true;
+    }
 }

+ 143 - 75
src/main/java/com/izouma/nineth/service/OrderPayService.java

@@ -1,8 +1,10 @@
 package com.izouma.nineth.service;
 
 import com.alibaba.fastjson.JSONObject;
+import com.izouma.nineth.config.Constants;
 import com.izouma.nineth.config.GeneralProperties;
 import com.izouma.nineth.domain.*;
+import com.izouma.nineth.dto.PayQuery;
 import com.izouma.nineth.dto.UserBankCard;
 import com.izouma.nineth.enums.MintOrderStatus;
 import com.izouma.nineth.enums.OrderStatus;
@@ -10,7 +12,6 @@ import com.izouma.nineth.enums.PayMethod;
 import com.izouma.nineth.event.OrderNotifyEvent;
 import com.izouma.nineth.exception.BusinessException;
 import com.izouma.nineth.repo.*;
-import com.izouma.nineth.utils.DateTimeUtils;
 import com.izouma.nineth.utils.SnowflakeIdWorker;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -25,12 +26,13 @@ import java.time.LocalDateTime;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.stream.Stream;
 
 @Service
 @Slf4j
 @AllArgsConstructor
 public class OrderPayService {
-    private static String PAY_CHANNEL = "sandPay";
+    private static String PAY_CHANNEL = Constants.PayChannel.SAND;
 
     private final OrderService       orderService;
     private final OrderRepo          orderRepo;
@@ -52,22 +54,12 @@ public class OrderPayService {
     private final UserBankCardRepo   userBankCardRepo;
 
     public static void setPayChannel(String payChannel) {
-        if ("hmPay".equals(payChannel) || "sandPay".equals(payChannel)) {
+        log.info("set pay channel {}", payChannel);
+        if (Constants.PayChannel.HM.equals(payChannel) || Constants.PayChannel.SAND.equals(payChannel)) {
             PAY_CHANNEL = payChannel;
         }
     }
 
-    public String paddingOrderId(String orderId) {
-        if (orderId != null && orderId.length() < 12) {
-            StringBuilder orderIdBuilder = new StringBuilder(orderId);
-            for (int i = orderIdBuilder.length(); i < 12; i++) {
-                orderIdBuilder.insert(0, "0");
-            }
-            orderId = orderIdBuilder.toString();
-        }
-        return orderId;
-    }
-
     @Cacheable(value = "payOrder", key = "'order#'+#orderId")
     public String payOrder(Long orderId) {
         Order order = orderRepo.findById(orderId).orElseThrow(new BusinessException("订单不存在"));
@@ -75,33 +67,34 @@ public class OrderPayService {
             throw new BusinessException("订单状态错误");
         }
         switch (PAY_CHANNEL) {
-            case "sandPay":
-                return sandPayService.requestAlipay(orderId + "", order.getTotalPrice(), order.getName(),
-                        order.getName(), SandPayService.getTimeout(order.getCreatedAt(), 180),
-                        "{\"type\":\"order\",\"id\":\"" + orderId + "\"}");
-            case "hmPay":
+            case Constants.PayChannel.SAND:
+                return sandPayService.pay(orderId + "", order.getName(), order.getTotalPrice(),
+                        order.getCreatedAt().plusMinutes(3), "order");
+            case Constants.PayChannel.HM:
                 return hmPayService.requestAlipay(orderId + "", order.getTotalPrice(), order.getName(),
-                        HMPayService.getTimeout(order.getCreatedAt(), 180),
-                        "order", generalProperties.getHost() + "/9th/orderDetail?id=" + orderId);
+                        HMPayService.getTimeout(order.getCreatedAt(), 180), Constants.OrderNotifyType.ORDER,
+                        generalProperties.getHost() + "/9th/orderDetail?id=" + orderId);
         }
-        throw new BusinessException("绿洲宇宙冷却系统已启动,请稍后支付");
+        throw new BusinessException(Constants.PAY_ERR_MSG);
     }
 
-    public void payOrderBalance(Long orderId, Long userId, String tradeCode) {
+    @Cacheable(value = "payOrder", key = "'order#'+#orderId")
+    public String payOrderQuick(Long orderId) {
         Order order = orderRepo.findById(orderId).orElseThrow(new BusinessException("订单不存在"));
         if (order.getStatus() != OrderStatus.NOT_PAID) {
             throw new BusinessException("订单状态错误");
         }
-        if (!Objects.equals(order.getUserId(), userId)) {
-            throw new BusinessException("订单不属于该用户");
-        }
-        String encodedPwd = userRepo.findTradeCode(userId);
-        if (StringUtils.isEmpty(encodedPwd)) {
-            throw new BusinessException("请先设置交易密码");
-        }
-        if (!passwordEncoder.matches(tradeCode, encodedPwd)) {
-            throw new BusinessException("交易码错误");
+        return sandPayService.payQuick(orderId + "", order.getName(), order.getTotalPrice(),
+                order.getCreatedAt().plusMinutes(3), Constants.OrderNotifyType.ORDER,
+                generalProperties.getHost() + "/9th/home");
+    }
+
+    public void payOrderBalance(Long orderId, Long userId, String tradeCode) {
+        Order order = orderRepo.findById(orderId).orElseThrow(new BusinessException("订单不存在"));
+        if (order.getStatus() != OrderStatus.NOT_PAID) {
+            throw new BusinessException("订单状态错误");
         }
+        checkTradeCode(userId, tradeCode, order.getUserId());
         BalanceRecord record = userBalanceService.balancePay(order.getUserId(), order.getTotalPrice(), orderId, order.getName());
         rocketMQTemplate.syncSend(generalProperties.getOrderNotifyTopic(),
                 new OrderNotifyEvent(orderId, PayMethod.BALANCE, record.getId().toString(),
@@ -122,7 +115,7 @@ public class OrderPayService {
             throw new BusinessException("请先绑定银行卡");
         }
         return payEaseService.pay(order.getName(), orderId.toString(), order.getTotalPrice(),
-                order.getUserId().toString(), bindCardId, "order");
+                order.getUserId().toString(), bindCardId, Constants.OrderNotifyType.ORDER);
     }
 
     public void confirmOrderAgreement(String requestId, String paymentOrderId, String code) {
@@ -146,31 +139,35 @@ public class OrderPayService {
             throw new BusinessException("订单状态错误");
         }
         switch (PAY_CHANNEL) {
-            case "sandPay":
-                return sandPayService.requestAlipay(orderId + "", order.getGasPrice(),
-                        "转赠" + order.getAssetId(), "转赠" + order.getAssetId(),
-                        SandPayService.getTimeout(order.getCreatedAt(), 180),
-                        "{\"type\":\"gift\",\"id\":\"" + orderId + "\"}");
-            case "hmPay":
+            case Constants.PayChannel.SAND:
+                return sandPayService.pay(orderId + "", "转赠" + order.getAssetId(), order.getGasPrice(),
+                        order.getCreatedAt().plusMinutes(3), Constants.OrderNotifyType.GIFT);
+            case Constants.PayChannel.HM:
                 return hmPayService.requestAlipay(orderId + "", order.getGasPrice(),
                         "转赠" + order.getAssetId(),
                         HMPayService.getTimeout(order.getCreatedAt(), 180),
-                        "gift", generalProperties.getHost() + "/9th/home");
+                        Constants.OrderNotifyType.GIFT, generalProperties.getHost() + "/9th/home");
         }
-        throw new BusinessException("绿洲宇宙冷却系统已启动,请稍后支付");
+        throw new BusinessException(Constants.PAY_ERR_MSG);
     }
 
-    public void payGiftBalance(Long orderId, Long userId, String tradeCode) {
+    @Cacheable(value = "payOrder", key = "'gift#'+#orderId")
+    public String payGiftQuick(Long orderId) {
         GiftOrder order = giftOrderRepo.findById(orderId).orElseThrow(new BusinessException("订单不存在"));
         if (order.getStatus() != OrderStatus.NOT_PAID) {
             throw new BusinessException("订单状态错误");
         }
-        if (!Objects.equals(order.getUserId(), userId)) {
-            throw new BusinessException("订单不属于该用户");
-        }
-        if (!Objects.equals(userRepo.findTradeCode(userId), tradeCode)) {
-            throw new BusinessException("交易码错误");
+        return sandPayService.payQuick(orderId + "", "转赠" + order.getAssetId(), order.getGasPrice(),
+                order.getCreatedAt().plusMinutes(3), Constants.OrderNotifyType.GIFT,
+                generalProperties.getHost() + "/9th/home");
+    }
+
+    public void payGiftBalance(Long orderId, Long userId, String tradeCode) {
+        GiftOrder order = giftOrderRepo.findById(orderId).orElseThrow(new BusinessException("订单不存在"));
+        if (order.getStatus() != OrderStatus.NOT_PAID) {
+            throw new BusinessException("订单状态错误");
         }
+        checkTradeCode(userId, tradeCode, order.getUserId());
         BalanceRecord record = userBalanceService.balancePay(order.getUserId(), order.getGasPrice(), orderId, "转赠");
         giftOrderService.giftNotify(orderId, PayMethod.BALANCE, record.getId().toString());
     }
@@ -189,7 +186,7 @@ public class OrderPayService {
             throw new BusinessException("请先绑定银行卡");
         }
         return payEaseService.pay("转赠" + order.getAssetId(), orderId.toString(), order.getGasPrice(),
-                order.getUserId().toString(), bindCardId, "gift");
+                order.getUserId().toString(), bindCardId, Constants.OrderNotifyType.GIFT);
     }
 
     @Cacheable(value = "payOrder", key = "'mintOrder#'+#orderId")
@@ -199,33 +196,50 @@ public class OrderPayService {
             throw new BusinessException("订单状态错误");
         }
         switch (PAY_CHANNEL) {
-            case "sandPay":
-                return sandPayService.requestAlipay(orderId + "", order.getGasPrice(),
-                        "铸造活动:" + order.getMintActivityId(), "铸造活动:" + order.getMintActivityId(),
-                        SandPayService.getTimeout(order.getCreatedAt(), 180),
-                        "{\"type\":\"mintOrder\",\"id\":\"" + orderId + "\"}");
-            case "hmPay":
+            case Constants.PayChannel.SAND:
+                return sandPayService.pay(orderId + "", "铸造活动:" + order.getMintActivityId(),
+                        order.getGasPrice(), order.getCreatedAt().plusMinutes(3), Constants.OrderNotifyType.MINT);
+            case Constants.PayChannel.HM:
                 return hmPayService.requestAlipay(orderId + "", order.getGasPrice(),
                         "铸造活动:" + order.getMintActivityId(),
                         HMPayService.getTimeout(order.getCreatedAt(), 180),
-                        "mintOrder", generalProperties.getHost() + "/9th/home");
+                        Constants.OrderNotifyType.MINT, generalProperties.getHost() + "/9th/home");
         }
         throw new BusinessException("绿洲宇宙冷却系统已启动,请稍后支付");
     }
 
+    @Cacheable(value = "payOrder", key = "'mintOrder#'+#orderId")
+    public String payMintQuick(Long orderId) {
+        MintOrder order = mintOrderRepo.findById(orderId).orElseThrow(new BusinessException("订单不存在"));
+        if (order.getStatus() != MintOrderStatus.NOT_PAID) {
+            throw new BusinessException("订单状态错误");
+        }
+        return sandPayService.payQuick(orderId + "", "铸造活动:" + order.getMintActivityId(),
+                order.getGasPrice(), order.getCreatedAt().plusMinutes(3), Constants.OrderNotifyType.MINT,
+                generalProperties.getHost() + "/9th/home");
+    }
+
     public void payMintOrderBalance(Long orderId, Long userId, String tradeCode) {
         MintOrder order = mintOrderRepo.findById(orderId).orElseThrow(new BusinessException("订单不存在"));
         if (order.getStatus() != MintOrderStatus.NOT_PAID) {
             throw new BusinessException("订单状态错误");
         }
-        if (!Objects.equals(order.getUserId(), userId)) {
+        checkTradeCode(userId, tradeCode, order.getUserId());
+        BalanceRecord record = userBalanceService.balancePay(order.getUserId(), order.getGasPrice(), orderId, "铸造活动");
+        mintOrderService.mintNotify(orderId, PayMethod.BALANCE, record.getId().toString());
+    }
+
+    private void checkTradeCode(Long userId, String tradeCode, Long orderUserId) {
+        if (!Objects.equals(orderUserId, userId)) {
             throw new BusinessException("订单不属于该用户");
         }
-        if (!Objects.equals(userRepo.findTradeCode(userId), tradeCode)) {
+        String encodedPwd = userRepo.findTradeCode(userId);
+        if (StringUtils.isEmpty(encodedPwd)) {
+            throw new BusinessException("请先设置交易密码");
+        }
+        if (!passwordEncoder.matches(tradeCode, encodedPwd)) {
             throw new BusinessException("交易码错误");
         }
-        BalanceRecord record = userBalanceService.balancePay(order.getUserId(), order.getGasPrice(), orderId, "铸造活动");
-        giftOrderService.giftNotify(orderId, PayMethod.BALANCE, record.getId().toString());
     }
 
     @Cacheable(value = "payOrder", key = "'mint#'+#orderId")
@@ -242,7 +256,7 @@ public class OrderPayService {
             throw new BusinessException("请先绑定银行卡");
         }
         return payEaseService.pay("铸造活动:" + order.getMintActivityId(), orderId.toString(), order.getGasPrice(),
-                order.getUserId().toString(), bindCardId, "mintOrder");
+                order.getUserId().toString(), bindCardId, Constants.OrderNotifyType.MINT);
     }
 
     public String recharge(Long userId, BigDecimal amount) {
@@ -261,16 +275,14 @@ public class OrderPayService {
                 .build();
         rechargeOrderRepo.save(order);
         switch (PAY_CHANNEL) {
-            case "sandPay":
-                return sandPayService.requestAlipay(order.getId() + "", order.getAmount(),
-                        "余额充值", "余额充值",
-                        SandPayService.getTimeout(order.getCreatedAt(), 180),
-                        "{\"type\":\"recharge\",\"id\":\"" + order.getId() + "\"}");
-            case "hmPay":
+            case Constants.PayChannel.SAND:
+                return sandPayService.pay(order.getId() + "", "余额充值", order.getAmount(),
+                        order.getCreatedAt().plusMinutes(3), Constants.OrderNotifyType.RECHARGE);
+            case Constants.PayChannel.HM:
                 return hmPayService.requestAlipay(order.getId() + "", order.getAmount(),
                         "余额充值",
                         HMPayService.getTimeout(order.getCreatedAt(), 180),
-                        "recharge", generalProperties.getHost() + "/9th/home");
+                        Constants.OrderNotifyType.RECHARGE, generalProperties.getHost() + "/9th/home");
         }
         throw new BusinessException("绿洲宇宙冷却系统已启动,请稍后支付");
     }
@@ -298,27 +310,83 @@ public class OrderPayService {
             throw new BusinessException("请先绑定银行卡");
         }
         return payEaseService.pay("余额充值", order.getId().toString(), order.getAmount(),
-                order.getUserId().toString(), bindCardId, "recharge");
+                order.getUserId().toString(), bindCardId, Constants.OrderNotifyType.RECHARGE);
+    }
+
+    public String rechargeQuick(Long userId, BigDecimal amount) {
+        BigDecimal minAmount = sysConfigService.getBigDecimal("min_recharge_amount");
+        if (amount.compareTo(minAmount) < 0) {
+            throw new BusinessException("充值金额不能小于" + minAmount);
+        }
+        if (amount.compareTo(new BigDecimal("50000")) > 0) {
+            throw new BusinessException("充值金额不能大于50000");
+        }
+        RechargeOrder order = RechargeOrder.builder()
+                .id(snowflakeIdWorker.nextId())
+                .userId(userId)
+                .amount(amount)
+                .status(OrderStatus.NOT_PAID)
+                .build();
+        rechargeOrderRepo.save(order);
+        return sandPayService.payQuick(order.getId() + "", "余额充值",
+                order.getAmount(), LocalDateTime.now().plusMinutes(3), Constants.OrderNotifyType.RECHARGE,
+                generalProperties.getHost() + "/9th/home");
     }
 
-    public JSONObject refund(String orderId, BigDecimal amount, String channel) {
+    public JSONObject refund(String orderId, String transactionId, BigDecimal amount, String channel) {
         switch (channel) {
-            case "sandPay": {
+            case Constants.PayChannel.SAND: {
                 JSONObject res = sandPayService.refund(orderId, amount);
                 if (!"000000".equals(res.getJSONObject("head").getString("respCode"))) {
-                    throw new BusinessException("退款失败");
+                    String msg = res.getJSONObject("head").getString("respMsg");
+                    throw new BusinessException("退款失败:" + msg);
                 }
-                break;
+                return res;
             }
-            case "hmPay": {
+            case Constants.PayChannel.HM: {
                 JSONObject res = hmPayService.refund(orderId, amount);
                 if (!"REFUND_SUCCESS".equals(res.getString("sub_code"))) {
-                    throw new BusinessException("退款失败");
+                    String msg = res.getString("msg");
+                    throw new BusinessException("退款失败:" + msg);
                 }
-                break;
+                return res;
+            }
+            case Constants.PayChannel.PE: {
+                JSONObject res = payEaseService.refund(orderId, transactionId, amount);
+                String status = res.getString("status");
+                if (!"SUCCESS".equals(status)) {
+                    String error = res.getString("error");
+                    String cause = res.getString("cause");
+                    throw new BusinessException("退款失败:" + error + ";" + cause);
+                }
+                return res;
             }
         }
         throw new BusinessException("退款失败");
     }
 
+    public PayQuery query(String orderId) {
+        return query(orderId, null);
+    }
+
+    public PayQuery query(String orderId, String channel) {
+        if (StringUtils.isNotEmpty(channel)) {
+            switch (channel) {
+                case Constants.PayChannel.SAND:
+                    return sandPayService.payQuery(orderId);
+                case Constants.PayChannel.HM:
+                    return hmPayService.payQuery(orderId);
+                case Constants.PayChannel.PE:
+                    return payEaseService.payQuery(orderId);
+            }
+        }
+        PayQuery query = sandPayService.payQuery(orderId);
+        if (query == null || !query.isExist()) {
+            query = hmPayService.payQuery(orderId);
+        }
+        if (query == null || !query.isExist()) {
+            query = payEaseService.payQuery(orderId);
+        }
+        return Optional.ofNullable(query).orElse(PayQuery.builder().exist(false).build());
+    }
 }

+ 85 - 79
src/main/java/com/izouma/nineth/service/OrderService.java

@@ -13,7 +13,6 @@ import com.github.binarywang.wxpay.constant.WxPayConstants;
 import com.github.binarywang.wxpay.exception.WxPayException;
 import com.github.binarywang.wxpay.service.WxPayService;
 import com.google.common.base.Splitter;
-import com.huifu.adapay.Adapay;
 import com.huifu.adapay.core.exception.BaseAdaPayException;
 import com.huifu.adapay.model.AdapayCommon;
 import com.huifu.adapay.model.Payment;
@@ -33,7 +32,11 @@ import com.izouma.nineth.repo.*;
 import com.izouma.nineth.security.Authority;
 import com.izouma.nineth.service.sms.SmsService;
 import com.izouma.nineth.utils.*;
-import lombok.AllArgsConstructor;
+import io.github.bucket4j.Bandwidth;
+import io.github.bucket4j.Bucket;
+import io.github.bucket4j.BucketConfiguration;
+import io.github.bucket4j.Refill;
+import io.github.bucket4j.distributed.proxy.ProxyManager;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.codec.EncoderException;
 import org.apache.commons.codec.net.URLCodec;
@@ -64,45 +67,89 @@ import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.time.format.DateTimeFormatter;
-import java.time.temporal.ChronoUnit;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
 
 @Service
-@AllArgsConstructor
 @Slf4j
 public class OrderService {
 
-    private OrderRepo orderRepo;
-    private CollectionRepo collectionRepo;
-    private UserAddressRepo userAddressRepo;
-    private UserRepo userRepo;
-    private Environment env;
-    private AlipayClient alipayClient;
-    private AlipayProperties alipayProperties;
-    private WxPayService wxPayService;
-    private WxPayProperties wxPayProperties;
-    private AssetService assetService;
-    private SysConfigService sysConfigService;
-    private AssetRepo assetRepo;
-    private UserCouponRepo userCouponRepo;
-    private CollectionService collectionService;
-    private CommissionRecordRepo commissionRecordRepo;
-    private AdapayProperties adapayProperties;
-    private GeneralProperties generalProperties;
-    private RocketMQTemplate rocketMQTemplate;
-    private RedisTemplate<String, Object> redisTemplate;
-    private SnowflakeIdWorker snowflakeIdWorker;
-    private SmsService smsService;
-    private ErrorOrderRepo errorOrderRepo;
-    private ShowCollectionRepo showCollectionRepo;
-    private ShowroomService showroomService;
-    private CollectionPrivilegeRepo collectionPrivilegeRepo;
-    private UserBankCardRepo userBankCardRepo;
-    private CacheService cacheService;
-    private UserPropertyRepo userPropertyRepo;
+    private final OrderRepo                     orderRepo;
+    private final CollectionRepo                collectionRepo;
+    private final UserAddressRepo               userAddressRepo;
+    private final UserRepo                      userRepo;
+    private final Environment                   env;
+    private final AlipayClient                  alipayClient;
+    private final AlipayProperties              alipayProperties;
+    private final WxPayService                  wxPayService;
+    private final WxPayProperties               wxPayProperties;
+    private final AssetService                  assetService;
+    private final SysConfigService              sysConfigService;
+    private final AssetRepo                     assetRepo;
+    private final UserCouponRepo                userCouponRepo;
+    private final CollectionService             collectionService;
+    private final CommissionRecordRepo          commissionRecordRepo;
+    private final AdapayProperties              adapayProperties;
+    private final GeneralProperties             generalProperties;
+    private final RocketMQTemplate              rocketMQTemplate;
+    private final RedisTemplate<String, Object> redisTemplate;
+    private final SnowflakeIdWorker             snowflakeIdWorker;
+    private final SmsService                    smsService;
+    private final ErrorOrderRepo                errorOrderRepo;
+    private final ShowCollectionRepo            showCollectionRepo;
+    private final ShowroomService               showroomService;
+    private final CollectionPrivilegeRepo       collectionPrivilegeRepo;
+    private final UserBankCardRepo              userBankCardRepo;
+    private final CacheService                  cacheService;
+    private final UserPropertyRepo              userPropertyRepo;
+    private final UserBalanceService            userBalanceService;
+    private final ProxyManager<String>          buckets;
+
+    public OrderService(OrderRepo orderRepo, CollectionRepo collectionRepo, UserAddressRepo userAddressRepo,
+                        UserRepo userRepo, Environment env, AlipayClient alipayClient,
+                        AlipayProperties alipayProperties, WxPayService wxPayService, WxPayProperties wxPayProperties,
+                        AssetService assetService, SysConfigService sysConfigService, AssetRepo assetRepo,
+                        UserCouponRepo userCouponRepo, CollectionService collectionService,
+                        CommissionRecordRepo commissionRecordRepo, AdapayProperties adapayProperties,
+                        GeneralProperties generalProperties, RocketMQTemplate rocketMQTemplate,
+                        RedisTemplate<String, Object> redisTemplate, SnowflakeIdWorker snowflakeIdWorker,
+                        SmsService smsService, ErrorOrderRepo errorOrderRepo, ShowCollectionRepo showCollectionRepo,
+                        ShowroomService showroomService, CollectionPrivilegeRepo collectionPrivilegeRepo,
+                        UserBankCardRepo userBankCardRepo, CacheService cacheService, UserPropertyRepo userPropertyRepo,
+                        UserBalanceService userBalanceService, ProxyManager<String> proxyManager) {
+        this.orderRepo = orderRepo;
+        this.collectionRepo = collectionRepo;
+        this.userAddressRepo = userAddressRepo;
+        this.userRepo = userRepo;
+        this.env = env;
+        this.alipayClient = alipayClient;
+        this.alipayProperties = alipayProperties;
+        this.wxPayService = wxPayService;
+        this.wxPayProperties = wxPayProperties;
+        this.assetService = assetService;
+        this.sysConfigService = sysConfigService;
+        this.assetRepo = assetRepo;
+        this.userCouponRepo = userCouponRepo;
+        this.collectionService = collectionService;
+        this.commissionRecordRepo = commissionRecordRepo;
+        this.adapayProperties = adapayProperties;
+        this.generalProperties = generalProperties;
+        this.rocketMQTemplate = rocketMQTemplate;
+        this.redisTemplate = redisTemplate;
+        this.snowflakeIdWorker = snowflakeIdWorker;
+        this.smsService = smsService;
+        this.errorOrderRepo = errorOrderRepo;
+        this.showCollectionRepo = showCollectionRepo;
+        this.showroomService = showroomService;
+        this.collectionPrivilegeRepo = collectionPrivilegeRepo;
+        this.userBankCardRepo = userBankCardRepo;
+        this.cacheService = cacheService;
+        this.userPropertyRepo = userPropertyRepo;
+        this.userBalanceService = userBalanceService;
+        this.buckets = proxyManager;
+    }
 
     public Page<Order> all(PageQuery pageQuery) {
         return orderRepo.findAll(JpaUtils.toSpecification(pageQuery, Order.class), JpaUtils.toPageRequest(pageQuery));
@@ -150,10 +197,10 @@ public class OrderService {
     }
 
     public void limitReq(Long collectionId) {
-        BoundValueOperations<String, Object> ops = redisTemplate.boundValueOps(RedisKeys.LIMIT_REQ + collectionId);
-        ops.setIfAbsent(3000, Duration.ofSeconds(30));
-        Long val = ops.decrement();
-        if (val == null || val < 0) {
+        Bucket bucket = buckets.builder().build("limit::" + collectionId, () -> (BucketConfiguration.builder()
+                .addLimit(Bandwidth.classic(3000, Refill.intervally(3000, Duration.ofSeconds(3000))))
+                .build()));
+        if (!bucket.tryConsume(1)) {
             throw new BusinessException("前方拥堵,请稍后再试");
         }
     }
@@ -669,6 +716,7 @@ public class OrderService {
                             smsService.sellOut(userRepo.findPhoneById(asset.getUserId()));
                         }
 
+                        userBalanceService.realtimeSettleOrder(order);
                     } else {
                         orderRepo.save(order);
                         //藏品其他信息/是否vip
@@ -758,48 +806,7 @@ public class OrderService {
 
         try {
             if (order.getStatus() != OrderStatus.NOT_PAID) {
-                throw new BusinessException("已支付订单无法取消");
-            }
-
-            // 查询adapay支付记录,如果已经支付,则不能取消
-            Set<Object> transactionIds = redisTemplate.opsForSet().members(RedisKeys.PAY_RECORD + order.getId());
-            if (transactionIds != null && transactionIds.size() > 0) {
-                AtomicInteger succeeded = new AtomicInteger();
-                AtomicInteger pending = new AtomicInteger();
-                transactionIds.parallelStream().forEach(s -> {
-                    String transactionIdStr = Optional.ofNullable(s).map(Object::toString).orElse("");
-                    String transactionId = null;
-                    String merchant = null;
-                    if (transactionIdStr.contains("#")) {
-                        String[] arr = transactionIdStr.split("#");
-                        merchant = arr[0];
-                        transactionId = arr[1];
-                    } else {
-                        merchant = Adapay.defaultMerchantKey;
-                        transactionId = transactionIdStr;
-                    }
-                    try {
-                        Map<String, Object> map = Payment.query(transactionId, merchant);
-                        if ("succeeded".equalsIgnoreCase(MapUtils.getString(map, "status"))) {
-                            succeeded.getAndIncrement();
-                        }
-                        if ("pending".equalsIgnoreCase(MapUtils.getString(map, "status"))) {
-                            pending.getAndIncrement();
-                            // 未支付的订单调用关单接口
-                            Map<String, Object> closeParams = new HashMap<>();
-                            closeParams.put("payment_id", transactionId);
-                            Payment.close(closeParams, merchant);
-                        }
-                    } catch (BaseAdaPayException e) {
-                        log.error("adapay error", e);
-                    }
-                });
-//                if (succeeded.get() + pending.get() > 0) {
-                if (succeeded.get() > 0) {
-                    if (ChronoUnit.MINUTES.between(order.getCreatedAt(), LocalDateTime.now()) < 10) {
-                        throw new BusinessException("订单已经支付成功或待支付,不能取消 " + order.getId());
-                    }
-                }
+                throw new BusinessException("当前订单状态无法取消[" + order.getStatus().name() + "]");
             }
 
             CollectionSource source = Optional.ofNullable(order.getSource()).orElseGet(() ->
@@ -981,7 +988,6 @@ public class OrderService {
         userIds.forEach(userId -> {
             redisTemplate.opsForValue().set(RedisKeys.BLACK_LIST + userId, 1, Duration.ofSeconds(60 * 120));
         });
-
     }
 
     public List<Order> addOrder(Long collectionId, List<Long> userIds, LocalDateTime time, boolean notify) {

+ 72 - 6
src/main/java/com/izouma/nineth/service/PayEaseService.java

@@ -3,13 +3,11 @@ package com.izouma.nineth.service;
 import com.alibaba.fastjson15.JSON;
 import com.alibaba.fastjson15.JSONObject;
 import com.alibaba.fastjson15.parser.Feature;
-import com.izouma.nineth.config.GeneralProperties;
-import com.izouma.nineth.config.HmPayProperties;
-import com.izouma.nineth.config.PayEaseProperties;
-import com.izouma.nineth.config.RedisKeys;
+import com.izouma.nineth.config.*;
 import com.izouma.nineth.dto.BindCardRequest;
+import com.izouma.nineth.dto.PayQuery;
+import com.izouma.nineth.enums.PayStatus;
 import com.izouma.nineth.exception.BusinessException;
-import com.izouma.nineth.utils.BankUtils;
 import com.izouma.nineth.utils.SnowflakeIdWorker;
 import com.upay.sdk.CipherWrapper;
 import com.upay.sdk.ConfigurationUtils;
@@ -22,9 +20,10 @@ import com.upay.sdk.cashier.order.builder.ReceiptPaymentBuilder;
 import com.upay.sdk.entity.Payer;
 import com.upay.sdk.entity.ProductDetail;
 import com.upay.sdk.onlinepay.builder.OrderBuilder;
+import com.upay.sdk.onlinepay.builder.QueryBuilder;
+import com.upay.sdk.onlinepay.builder.RefundBuilder;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.junit.Test;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
@@ -32,9 +31,12 @@ import org.springframework.stereotype.Service;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
+import java.util.concurrent.TimeUnit;
 
 @Service
 @Slf4j
@@ -44,6 +46,7 @@ public class PayEaseService {
 
     private PayEaseProperties             payEaseProperties;
     private RedisTemplate<String, Object> redisTemplate;
+    private SnowflakeIdWorker             snowflakeIdWorker;
 
     public JSONObject request(String url, JSONObject requestData) {
         log.info("requestData: {}", JSON.toJSONString(requestData, true));
@@ -180,6 +183,8 @@ public class PayEaseService {
             String error = response.getString("error");
             throw new BusinessException(error);
         }
+        redisTemplate.opsForValue()
+                .set(RedisKeys.PAY_TMP + orderId, Constants.PayChannel.PE, 1, TimeUnit.DAYS);
         boolean needCaptcha = response.getBoolean("needKaptcha");
         String paymentOrderId = response.getString("paymentOrderId");
         String requestId = response.getString("requestId");
@@ -207,4 +212,65 @@ public class PayEaseService {
             throw new BusinessException(error);
         }
     }
+
+    public com.alibaba.fastjson.JSONObject refund(String orderId, String transactionId, BigDecimal amount) {
+        RefundBuilder builder = new RefundBuilder(payEaseProperties.getMerchantId());
+        builder.setRequestId(orderId)
+                .setAmount(amount.multiply(new BigDecimal("100")).setScale(0, RoundingMode.FLOOR)
+                        .stripTrailingZeros().toPlainString())
+                .setOrderId(transactionId).setNotifyUrl(payEaseProperties.getNotifyUrl());
+        JSONObject response = request(ConfigurationUtils.getOnlinePayRefundUrl(), builder.bothEncryptBuild());
+        return com.alibaba.fastjson.JSON.parseObject(response.toJSONString());
+    }
+
+    public com.alibaba.fastjson.JSONObject query(String requestId) {
+        QueryBuilder builder = new QueryBuilder(payEaseProperties.getMerchantId());
+        builder.setRequestId(requestId);
+        JSONObject response = request(ConfigurationUtils.getOnlinePayQueryUrl(), builder.bothEncryptBuild());
+        return com.alibaba.fastjson.JSON.parseObject(response.toJSONString());
+    }
+
+    public com.alibaba.fastjson.JSONObject refundQuery(String requestId) {
+        RefundBuilder builder = new RefundBuilder(payEaseProperties.getMerchantId());
+        builder.setRequestId(requestId);
+        JSONObject response = request(ConfigurationUtils.getOnlinePayRefundQueryUrl(), builder.bothEncryptBuild());
+        return com.alibaba.fastjson.JSON.parseObject(response.toJSONString());
+    }
+
+    public PayQuery payQuery(String orderId) {
+        com.alibaba.fastjson.JSONObject res = query(orderId);
+        PayQuery query = new PayQuery(Constants.PayChannel.PE);
+        String status = res.getString("status");
+        if ("ERROR".equals(status)) {
+            String error = res.getString("error");
+            query.setExist(false);
+            query.setMsg(error);
+        } else {
+            query.setExist(true);
+            if (res.containsKey("completeDateTime")) {
+                query.setPayTime(LocalDateTime.parse(res.getString("completeDateTime"), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
+            }
+            query.setAmount(new BigDecimal(res.getString("orderAmount")).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP));
+            query.setTransactionId(res.getString("serialNumber"));
+            switch (status) {
+                case "INIT":
+                    query.setStatus(PayStatus.PENDING);
+                    break;
+                case "SUCCESS":
+                    query.setStatus(PayStatus.SUCCESS);
+                    break;
+                case "FAILED":
+                    query.setStatus(PayStatus.FAIL);
+                    break;
+                case "CANCEL":
+                    query.setStatus(PayStatus.CANCEL);
+                    break;
+            }
+            com.alibaba.fastjson.JSONObject res1 = refundQuery(orderId);
+            if ("SUCCESS".equals(res1.getString("status"))) {
+                query.setStatus(PayStatus.REFUNDED);
+            }
+        }
+        return query;
+    }
 }

+ 108 - 225
src/main/java/com/izouma/nineth/service/SandPayService.java

@@ -2,13 +2,12 @@ package com.izouma.nineth.service;
 
 import cn.com.sandpay.cashier.sdk.*;
 import com.alibaba.fastjson.JSONObject;
+import com.izouma.nineth.config.Constants;
 import com.izouma.nineth.config.GeneralProperties;
+import com.izouma.nineth.config.RedisKeys;
 import com.izouma.nineth.config.SandPayProperties;
-import com.izouma.nineth.domain.GiftOrder;
-import com.izouma.nineth.domain.MintOrder;
-import com.izouma.nineth.domain.Order;
-import com.izouma.nineth.enums.MintOrderStatus;
-import com.izouma.nineth.enums.OrderStatus;
+import com.izouma.nineth.dto.PayQuery;
+import com.izouma.nineth.enums.PayStatus;
 import com.izouma.nineth.exception.BusinessException;
 import com.izouma.nineth.repo.GiftOrderRepo;
 import com.izouma.nineth.repo.MintOrderRepo;
@@ -18,34 +17,39 @@ import com.izouma.nineth.utils.SnowflakeIdWorker;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.codec.binary.Base64;
-import org.springframework.cache.annotation.Cacheable;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.data.redis.core.BoundSetOperations;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 
 import java.io.IOException;
 import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.net.URLDecoder;
 import java.nio.charset.StandardCharsets;
 import java.text.DecimalFormat;
 import java.text.DecimalFormatSymbols;
-import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
+import java.util.concurrent.TimeUnit;
 
 @Service
 @AllArgsConstructor
 @Slf4j
 public class SandPayService {
-    private final OrderRepo         orderRepo;
-    private final GiftOrderRepo     giftOrderRepo;
-    private final SandPayProperties sandPayProperties;
-    private final MintOrderRepo     mintOrderRepo;
-    private final SnowflakeIdWorker snowflakeIdWorker;
-    private final GeneralProperties generalProperties;
-
-    public String paddingOrderId(String orderId) {
+    private final OrderRepo                     orderRepo;
+    private final GiftOrderRepo                 giftOrderRepo;
+    private final SandPayProperties             sandPayProperties;
+    private final MintOrderRepo                 mintOrderRepo;
+    private final SnowflakeIdWorker             snowflakeIdWorker;
+    private final GeneralProperties             generalProperties;
+    private final RedisTemplate<String, Object> redisTemplate;
+
+    public static String paddingOrderId(String orderId) {
         if (orderId != null && orderId.length() < 12) {
             StringBuilder orderIdBuilder = new StringBuilder(orderId);
             for (int i = orderIdBuilder.length(); i < 12; i++) {
@@ -56,7 +60,7 @@ public class SandPayService {
         return orderId;
     }
 
-    public String getReqTime() {
+    public static String getReqTime() {
         return DateTimeUtils.format(LocalDateTime.now(), "yyyyMMddHHmmss");
     }
 
@@ -69,12 +73,12 @@ public class SandPayService {
                 .plusSeconds(seconds), "yyyyMMddHHmmss");
     }
 
-    public String convertAmount(BigDecimal amount) {
+    public static String convertAmount(BigDecimal amount) {
         DecimalFormat df = new DecimalFormat("000000000000", DecimalFormatSymbols.getInstance(Locale.US));
         return df.format(amount.multiply(new BigDecimal("100")));
     }
 
-    public JSONObject requestServer(JSONObject header, JSONObject body, String reqAddr) {
+    public static JSONObject requestServer(JSONObject header, JSONObject body, String reqAddr) {
 
         Map<String, String> reqMap = new HashMap<String, String>();
         JSONObject reqJson = new JSONObject();
@@ -137,22 +141,13 @@ public class SandPayService {
         }
     }
 
-    public String requestAlipay(String orderId, BigDecimal amount, String subject, String desc,
-                                String timeout, String extend) {
-        JSONObject res = requestAlipayRaw(orderId, amount, subject, desc, timeout, extend);
-        if ("000000".equals(res.getJSONObject("head").getString("respCode"))) {
-            return "alipays://platformapi/startapp?saId=10000007&qrcode=" + res.getJSONObject("body").getString("qrCode");
-        }
-        throw new BusinessException("绿洲宇宙冷却系统已启动,请稍后支付");
-    }
+    public String pay(String orderId, String subject, BigDecimal amount, LocalDateTime expireAt, String type) {
+        String pOrderId = paddingOrderId(orderId);
+
+        JSONObject extend = new JSONObject();
+        extend.put("type", type);
+        extend.put("orderId", pOrderId);
 
-    public JSONObject requestAlipayRaw(String orderId, BigDecimal amount, String subject, String desc,
-                                       String timeout, String extend) {
-        if (orderId.length() < 12) {
-            for (int i = orderId.length(); i < 12; i++) {
-                orderId = "0" + orderId;
-            }
-        }
         JSONObject header = new JSONObject();
         header.put("version", "1.0");                      //版本号
         header.put("method", "sandpay.trade.precreate");   //接口名称:统一下单并支付
@@ -168,23 +163,34 @@ public class SandPayService {
         body.put("totalAmount", convertAmount(amount));               //订单金额 12位长度,精确到分
         //body.put("limitPay", "5");                                  //限定支付方式 送1-限定不能使用贷记卡	送4-限定不能使用花呗	送5-限定不能使用贷记卡+花呗
         body.put("subject", subject);                                 //订单标题
-        body.put("body", desc);                                       //订单描述
-        body.put("txnTimeOut", timeout);                              //订单超时时间
+        body.put("body", subject);                                    //订单描述
+        body.put("txnTimeOut", getTimeout(expireAt, 0));      //订单超时时间
         body.put("notifyUrl", sandPayProperties.getNotifyUrl());      //异步通知地址
         body.put("bizExtendParams", "");                              //业务扩展参数
         body.put("merchExtendParams", "");                            //商户扩展参数
-        body.put("extend", extend);                                   //扩展域
+        body.put("extend", extend.toJSONString());                    //扩展域
 
-        return requestServer(header, body, "https://cashier.sandpay.com.cn/qr/api/order/create");
-    }
+        JSONObject res = requestServer(header, body, "https://cashier.sandpay.com.cn/qr/api/order/create");
 
-    public JSONObject requestQuick(String orderId, BigDecimal amount, String subject, String desc,
-                                   int timeout, String extend, String frontUrl) {
-        if (orderId.length() < 12) {
-            for (int i = orderId.length(); i < 12; i++) {
-                orderId = "0" + orderId;
-            }
+        String respCode = res.getJSONObject("head").getString("respCode");
+        if ("000000".equals(respCode)) {
+            redisTemplate.opsForValue()
+                    .set(RedisKeys.PAY_TMP + orderId, Constants.PayChannel.SAND, 1, TimeUnit.DAYS);
+            return "alipays://platformapi/startapp?saId=10000007&qrcode="
+                    + res.getJSONObject("body").getString("qrCode");
         }
+        String msg = res.getJSONObject("head").getString("respMsg");
+        throw new BusinessException(Constants.PAY_ERR_MSG, msg);
+    }
+
+    public String payQuick(String orderId, String subject, BigDecimal amount, LocalDateTime expireAt,
+                           String type, String returnUrl) {
+        String pOrderId = paddingOrderId(orderId);
+
+        JSONObject extend = new JSONObject();
+        extend.put("type", type);
+        extend.put("orderId", pOrderId);
+
         JSONObject header = new JSONObject();
         header.put("version", "1.0");                         //版本号
         header.put("method", "sandpay.trade.pay");            //接口名称:统一下单
@@ -198,12 +204,12 @@ public class SandPayService {
         body.put("orderCode", orderId);                                           //商户订单号
         body.put("totalAmount", convertAmount(amount));                           //订单金额
         body.put("subject", subject);                                             //订单标题
-        body.put("body", desc);                                                   //订单描述
-        body.put("txnTimeOut", getTimeout(timeout));                              //订单超时时间
+        body.put("body", subject);                                                //订单描述
+        body.put("txnTimeOut", getTimeout(expireAt, 0));                  //订单超时时间
         body.put("clientIp", "192.168.22.55");                                    //客户端IP
         body.put("limitPay", "");                                                 //限定支付方式	送1-限定不能使用贷记卡送	4-限定不能使用花呗	送5-限定不能使用贷记卡+花呗
         body.put("notifyUrl", sandPayProperties.getNotifyUrl());                  //异步通知地址
-        body.put("frontUrl", frontUrl);                                           //前台通知地址
+        body.put("frontUrl", returnUrl);                                          //前台通知地址
         body.put("storeId", "");                                                  //商户门店编号
         body.put("terminalId", "");                                               //商户终端编号
         body.put("operatorId", "");                                               //操作员编号
@@ -212,13 +218,22 @@ public class SandPayService {
         body.put("riskRateInfo", "");                                             //风控信息域
         body.put("bizExtendParams", "");                                          //业务扩展参数
         body.put("merchExtendParams", "");                                        //商户扩展参数
-        body.put("extend", extend);                                               //扩展域
+        body.put("extend", extend.toJSONString());                                //扩展域
         body.put("payMode", "sand_h5");                                           //支付模式
 
-        return requestServer(header, body, "https://cashier.sandpay.com.cn/gateway/api/order/pay");
+        JSONObject res = requestServer(header, body, "https://cashier.sandpay.com.cn/gateway/api/order/pay");
+        String respCode = res.getJSONObject("head").getString("respCode");
+        if ("000000".equals(respCode)) {
+            redisTemplate.opsForValue()
+                    .set(RedisKeys.PAY_TMP + orderId, Constants.PayChannel.SAND, 1, TimeUnit.DAYS);
+            return res.getJSONObject("body").getString("credential");
+        }
+        String msg = res.getJSONObject("head").getString("respMsg");
+        throw new BusinessException(Constants.PAY_ERR_MSG, msg);
     }
 
     public JSONObject query(String orderId) {
+        orderId = paddingOrderId(orderId);
         JSONObject header = new JSONObject();
         header.put("version", "1.0");                     //版本号
         header.put("method", "sandpay.trade.query");      //接口名称:订单查询
@@ -236,6 +251,7 @@ public class SandPayService {
     }
 
     public JSONObject refund(String orderId, BigDecimal amount) {
+        orderId = paddingOrderId(orderId);
         JSONObject header = new JSONObject();
         header.put("version", "1.0");                           //版本号
         header.put("method", "sandpay.trade.refund");           //接口名称:退货
@@ -249,188 +265,13 @@ public class SandPayService {
         body.put("orderCode", snowflakeIdWorker.nextId());        //商户订单号
         body.put("oriOrderCode", paddingOrderId(orderId));        //原交易订单号
         body.put("refundAmount", convertAmount(amount));          //退货金额
-        body.put("refundReason", "退货测试");                      //退货原因
+        body.put("refundReason", "退款");                         //退货原因
         body.put("notifyUrl", sandPayProperties.getNotifyUrl());  //异步通知地址
         body.put("extend", "");
 
         return requestServer(header, body, "https://cashier.sandpay.com.cn/qr/api/order/refund");
     }
 
-    @Cacheable(value = "sandPay", key = "#orderId")
-    public String payOrder(Long orderId) {
-        Order order = orderRepo.findById(orderId).orElseThrow(new BusinessException("订单不存在"));
-        if (order.getStatus() != OrderStatus.NOT_PAID) {
-            throw new BusinessException("订单状态错误");
-        }
-        JSONObject extend = new JSONObject();
-        extend.put("type", "order");
-        extend.put("id", orderId);
-
-        JSONObject res = requestAlipayRaw(orderId.toString(), order.getTotalPrice(), order.getName(), order.getName(),
-                getTimeout(order.getCreatedAt(), 180), extend.toJSONString());
-        if (res == null)
-            throw new BusinessException("下单失败,请稍后再试");
-
-        if (!"000000".equals(res.getJSONObject("head").getString("respCode"))) {
-            String msg = res.getJSONObject("head").getString("respMsg");
-            if (msg.contains("超限")) {
-                throw new BusinessException("超过商户单日额度");
-            }
-            if (msg.contains("商户状态")) {
-                throw new BusinessException("超过商户单日额度");
-            }
-            throw new BusinessException(msg);
-        }
-
-        return res.getJSONObject("body").getString("qrCode");
-    }
-
-    @Cacheable(value = "sandPayQuick", key = "#orderId")
-    public String payOrderQuick(Long orderId) {
-        Order order = orderRepo.findById(orderId).orElseThrow(new BusinessException("订单不存在"));
-        if (order.getStatus() != OrderStatus.NOT_PAID) {
-            throw new BusinessException("订单状态错误");
-        }
-        JSONObject extend = new JSONObject();
-        extend.put("type", "order");
-        extend.put("id", orderId);
-
-        JSONObject res = requestQuick(orderId.toString(), order.getTotalPrice(), order.getName(), order.getName(),
-                180, extend.toJSONString(), generalProperties.getHost() + "/9th/orderDetail?id=" + orderId);
-        if (res == null)
-            throw new BusinessException("下单失败,请稍后再试");
-
-        if (!"000000".equals(res.getJSONObject("head").getString("respCode"))) {
-            String msg = res.getJSONObject("head").getString("respMsg");
-            if (msg.contains("超限")) {
-                throw new BusinessException("超过商户单日额度");
-            }
-            if (msg.contains("商户状态")) {
-                throw new BusinessException("超过商户单日额度");
-            }
-            throw new BusinessException(msg);
-        }
-        return res.getJSONObject("body").getString("credential");
-    }
-
-    @Cacheable(value = "sandPayQuick", key = "#orderId")
-    public String payGiftQuick(Long orderId) {
-        GiftOrder order = giftOrderRepo.findById(orderId).orElseThrow(new BusinessException("订单不存在"));
-        if (order.getStatus() != OrderStatus.NOT_PAID) {
-            throw new BusinessException("订单状态错误");
-        }
-        JSONObject extend = new JSONObject();
-        extend.put("type", "gift");
-        extend.put("id", orderId);
-
-        JSONObject res = requestQuick(orderId.toString(), order.getGasPrice(), "转增" + order.getAssetId(),
-                "转增" + order.getAssetId(), 180, extend.toJSONString(),
-                generalProperties.getHost() + "/9th/");
-        if (res == null)
-            throw new BusinessException("下单失败,请稍后再试");
-
-        if (!"000000".equals(res.getJSONObject("head").getString("respCode"))) {
-            String msg = res.getJSONObject("head").getString("respMsg");
-            if (msg.contains("超限")) {
-                throw new BusinessException("超过商户单日额度");
-            }
-            if (msg.contains("商户状态")) {
-                throw new BusinessException("超过商户单日额度");
-            }
-            throw new BusinessException(msg);
-        }
-        return res.getJSONObject("body").getString("credential");
-    }
-
-    @Cacheable(value = "sandPayQuick", key = "#orderId")
-    public String payMintQuick(Long orderId) {
-        MintOrder order = mintOrderRepo.findById(orderId).orElseThrow(new BusinessException("订单不存在"));
-        if (order.getStatus() != MintOrderStatus.NOT_PAID) {
-            throw new BusinessException("订单状态错误");
-        }
-        JSONObject extend = new JSONObject();
-        extend.put("type", "mintOrder");
-        extend.put("id", orderId);
-
-        JSONObject res = requestQuick(orderId.toString(), order.getGasPrice(),
-                "铸造活动:" + order.getMintActivityId(), "铸造活动:" + order.getMintActivityId(),
-                180, extend.toJSONString(), generalProperties.getHost() + "/9th/");
-        if (res == null)
-            throw new BusinessException("下单失败,请稍后再试");
-
-        if (!"000000".equals(res.getJSONObject("head").getString("respCode"))) {
-            String msg = res.getJSONObject("head").getString("respMsg");
-            if (msg.contains("超限")) {
-                throw new BusinessException("超过商户单日额度");
-            }
-            if (msg.contains("商户状态")) {
-                throw new BusinessException("超过商户单日额度");
-            }
-            throw new BusinessException(msg);
-        }
-        return res.getJSONObject("body").getString("credential");
-    }
-
-    @Cacheable(value = "sandPay", key = "#orderId")
-    public String payGiftOrder(Long orderId) {
-        GiftOrder order = giftOrderRepo.findById(orderId).orElseThrow(new BusinessException("订单不存在"));
-        if (order.getStatus() != OrderStatus.NOT_PAID) {
-            throw new BusinessException("订单状态错误");
-        }
-        JSONObject extend = new JSONObject();
-        extend.put("type", "gift");
-        extend.put("id", orderId);
-
-        JSONObject res = requestAlipayRaw(orderId.toString(), order.getGasPrice(), "转增" + order.getAssetId(),
-                "转增" + order.getAssetId(),
-                getTimeout(order.getCreatedAt(), 180), extend.toJSONString());
-        if (res == null)
-            throw new BusinessException("下单失败,请稍后再试");
-
-        if (!"000000".equals(res.getJSONObject("head").getString("respCode"))) {
-            String msg = res.getJSONObject("head").getString("respMsg");
-            if (msg.contains("超限")) {
-                throw new BusinessException("超过商户单日额度");
-            }
-            if (msg.contains("商户状态")) {
-                throw new BusinessException("超过商户单日额度");
-            }
-            throw new BusinessException(msg);
-        }
-
-        return res.getJSONObject("body").getString("qrCode");
-    }
-
-    @Cacheable(value = "sandPay", key = "#orderId")
-    public String payMintOrder(Long orderId) {
-        MintOrder order = mintOrderRepo.findById(orderId).orElseThrow(new BusinessException("订单不存在"));
-        if (order.getStatus() != MintOrderStatus.NOT_PAID) {
-            throw new BusinessException("订单状态错误");
-        }
-        JSONObject extend = new JSONObject();
-        extend.put("type", "mintOrder");
-        extend.put("id", orderId);
-
-        JSONObject res = requestAlipayRaw(orderId.toString(), order.getGasPrice(), "铸造活动:" + order.getMintActivityId(),
-                "铸造活动:" + order.getMintActivityId(), getTimeout(order.getCreatedAt(), 180), extend.toJSONString());
-        if (res == null)
-            throw new BusinessException("下单失败,请稍后再试");
-
-        if (!"000000".equals(res.getJSONObject("head").getString("respCode"))) {
-            String msg = res.getJSONObject("head").getString("respMsg");
-            if (msg.contains("超限")) {
-                throw new BusinessException("超过商户单日额度");
-            }
-            if (msg.contains("商户状态")) {
-                throw new BusinessException("超过商户单日额度");
-            }
-            throw new BusinessException(msg);
-        }
-
-        return res.getJSONObject("body").getString("qrCode");
-    }
-
-
     public JSONObject transfer(String id, String name, String bank, BigDecimal amount) {
         JSONObject request = new JSONObject();
         DecimalFormat df = new DecimalFormat("000000000000", DecimalFormatSymbols.getInstance(Locale.US));
@@ -648,4 +489,46 @@ public class SandPayService {
             return null;
         }
     }
+
+    public PayQuery payQuery(String orderId) {
+        JSONObject res = query(orderId);
+        PayQuery query = new PayQuery(Constants.PayChannel.SAND);
+        String respCode = res.getJSONObject("head").getString("respCode");
+
+        if ("000000".equals(respCode)) {
+            query.setExist(true);
+            JSONObject body = res.getJSONObject("body");
+            query.setMsg(body.getString("orderMsg") + " " + body.getString("oriRespMsg"));
+            query.setAmount(new BigDecimal(body.getString("totalAmount"))
+                    .divide(new BigDecimal(100), 2, RoundingMode.HALF_UP));
+            query.setTransactionId(body.getString("payOrderCode"));
+            if (StringUtils.isNotEmpty(body.getString("payTime"))) {
+                query.setPayTime(LocalDateTime.parse(body.getString("payTime"), DateTimeFormatter.ofPattern("yyyyMMddHHmmss")));
+            }
+            switch (body.getString("orderStatus")) {
+                case "00":
+                    query.setStatus(PayStatus.SUCCESS);
+                    break;
+                case "01":
+                    query.setStatus(PayStatus.PENDING);
+                    break;
+                case "02":
+                    query.setStatus(PayStatus.FAIL);
+                    break;
+                case "03":
+                    query.setStatus(PayStatus.CANCEL);
+                    break;
+                case "04":
+                    query.setStatus(PayStatus.REFUNDED);
+                    break;
+                case "05":
+                    query.setStatus(PayStatus.REFUNDING);
+                    break;
+            }
+        } else {
+            query.setExist(false);
+            query.setMsg(res.getString("msg"));
+        }
+        return query;
+    }
 }

+ 70 - 2
src/main/java/com/izouma/nineth/service/SysConfigService.java

@@ -50,6 +50,10 @@ public class SysConfigService {
         return Integer.parseInt(str);
     }
 
+    public String getString(String name) {
+        return sysConfigRepo.findByName(name).map(SysConfig::getValue).orElse(null);
+    }
+
     @PostConstruct
     public void init() {
         List<SysConfig> list = sysConfigRepo.findAll();
@@ -82,7 +86,7 @@ public class SysConfigService {
                     .name("hold_days")
                     .desc("持有满几天可销售")
                     .type(SysConfig.ValueType.NUMBER)
-                    .value("5")
+                    .value("1")
                     .build());
         }
         if (list.stream().noneMatch(i -> i.getName().equals("default_search_mode"))) {
@@ -101,7 +105,7 @@ public class SysConfigService {
                     .value("FALSE")
                     .build());
         }
-        if (list.stream().noneMatch(i -> i.getName().equals("order_cancel_interval"))) {
+        if (list.stream().noneMatch(i -> i.getName().equals("order_cancel_time"))) {
             sysConfigRepo.save(SysConfig.builder()
                     .name("order_cancel_time")
                     .desc("订单自动取消间隔(S)")
@@ -134,6 +138,14 @@ public class SysConfigService {
                     .value("100")
                     .build());
         }
+        if (list.stream().noneMatch(i -> i.getName().equals("max_recharge_amount"))) {
+            sysConfigRepo.save(SysConfig.builder()
+                    .name("max_recharge_amount")
+                    .desc("最大充值金额")
+                    .type(SysConfig.ValueType.NUMBER)
+                    .value("5000")
+                    .build());
+        }
         if (list.stream().noneMatch(i -> i.getName().equals("enable_balance_pay"))) {
             sysConfigRepo.save(SysConfig.builder()
                     .name("enable_balance_pay")
@@ -150,6 +162,62 @@ public class SysConfigService {
                     .value("100")
                     .build());
         }
+        if (list.stream().noneMatch(i -> i.getName().equals("withdraw_charge"))) {
+            sysConfigRepo.save(SysConfig.builder()
+                    .name("withdraw_charge")
+                    .desc("提现手续费(%,最低)")
+                    .type(SysConfig.ValueType.STRING)
+                    .value("8,2")
+                    .build());
+        }
+        if (list.stream().noneMatch(i -> i.getName().equals("enable_withdraw"))) {
+            sysConfigRepo.save(SysConfig.builder()
+                    .name("enable_withdraw")
+                    .desc("允许提现")
+                    .type(SysConfig.ValueType.BOOLEAN)
+                    .value("0")
+                    .build());
+        }
+        if (list.stream().noneMatch(i -> i.getName().equals("enable_recharge"))) {
+            sysConfigRepo.save(SysConfig.builder()
+                    .name("enable_recharge")
+                    .desc("允许充值")
+                    .type(SysConfig.ValueType.BOOLEAN)
+                    .value("0")
+                    .build());
+        }
+        if (list.stream().noneMatch(i -> i.getName().equals("daily_withdraw_limit"))) {
+            sysConfigRepo.save(SysConfig.builder()
+                    .name("daily_withdraw_limit")
+                    .desc("每日提现次数限制")
+                    .type(SysConfig.ValueType.NUMBER)
+                    .value("1")
+                    .build());
+        }
+        if (list.stream().noneMatch(i -> i.getName().equals("enable_wallet"))) {
+            sysConfigRepo.save(SysConfig.builder()
+                    .name("enable_wallet")
+                    .desc("开启钱包功能")
+                    .type(SysConfig.ValueType.BOOLEAN)
+                    .value("0")
+                    .build());
+        }
+        if (list.stream().noneMatch(i -> i.getName().equals("realtime_settle_order"))) {
+            sysConfigRepo.save(SysConfig.builder()
+                    .name("realtime_settle_order")
+                    .desc("开启订单实时结算")
+                    .type(SysConfig.ValueType.BOOLEAN)
+                    .value("0")
+                    .build());
+        }
+        if (list.stream().noneMatch(i -> i.getName().equals("wallet_enable_amount"))) {
+            sysConfigRepo.save(SysConfig.builder()
+                    .name("wallet_enable_amount")
+                    .desc("开启钱包所需最小消费金额")
+                    .type(SysConfig.ValueType.NUMBER)
+                    .value("1")
+                    .build());
+        }
         SearchMode searchMode = SearchMode.valueOf(sysConfigRepo.findByName("default_search_mode").get().getValue());
         JpaUtils.setDefaultSearchMode(searchMode);
 

+ 65 - 44
src/main/java/com/izouma/nineth/service/UserBalanceService.java

@@ -37,7 +37,6 @@ import javax.transaction.Transactional;
 import java.io.*;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
-import java.time.Duration;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
@@ -99,6 +98,10 @@ public class UserBalanceService {
         int c = 0;
         for (Order order : orders) {
             log.info("结算订单 {}/{}, orderId={}", ++c, orders.size(), order.getId());
+            BalanceRecord record = balanceRecordRepo.findByOrderIdAndType(order.getId(), BalanceType.SELL);
+            if (record != null) {
+                continue;
+            }
             Asset asset = assets.stream().filter(i -> i.getId().equals(order.getAssetId()))
                     .findFirst()
                     .orElseThrow(new BusinessException("藏品不存在"));
@@ -468,70 +471,88 @@ public class UserBalanceService {
 
     }
 
-    @RedisLock("#userId")
-    public void withdraw(Long userId) {
+    public BalanceRecord preWithdraw(Long userId, BigDecimal amount) {
         UserBalance userBalance = userBalanceRepo.findById(userId).orElseThrow(new BusinessException("余额不足"));
+        if (amount.compareTo(userBalance.getBalance()) > 0) {
+            throw new BusinessException("余额不足");
+        }
         BigDecimal minWithdrawAmount = sysConfigService.getBigDecimal("min_withdraw_amount");
-        if (userBalance.getBalance().compareTo(minWithdrawAmount) < 0) {
+        if (amount.compareTo(minWithdrawAmount) < 0) {
             throw new BusinessException("最小提现金额为" + minWithdrawAmount);
         }
         UserBankCard userBankCard = userBankCardRepo.findByUserId(userBalance.getUserId())
                 .stream().findFirst().orElseThrow(new BusinessException("请先绑定银行卡"));
 
-        BigDecimal amount = userBalance.getBalance();
         userBalance.setLastBalance(userBalance.getBalance());
-        userBalance.setBalance(BigDecimal.ZERO);
+        userBalance.setBalance(userBalance.getBalance().subtract(amount));
         userBalanceRepo.saveAndFlush(userBalance);
 
-        String withdrawId = snowflakeIdWorker.nextId() + "";
-        balanceRecordRepo.save(BalanceRecord.builder()
+        return balanceRecordRepo.save(BalanceRecord.builder()
                 .time(LocalDateTime.now())
                 .userId(userBalance.getUserId())
                 .amount(amount.negate())
-                .balance(BigDecimal.ZERO)
+                .balance(userBalance.getBalance())
                 .lastBalance(userBalance.getLastBalance())
                 .type(BalanceType.WITHDRAW)
-                .withdrawId(withdrawId)
                 .build());
+    }
 
-        boolean success = false;
-        String msg = null;
-        try {
-            if (Arrays.asList(env.getActiveProfiles()).contains("prod")) {
-                JSONObject res = sandPayService.transfer(withdrawId, userBankCard.getRealName(), userBankCard.getBankNo(), amount);
-                if ("0000".equals(res.getString("respCode"))) {
-                    success = true;
-                } else {
-                    msg = res.getString("respDesc");
-                }
-            } else {
-                success = true;
-            }
-        } catch (Exception e) {
-            msg = e.getMessage();
+    @RedisLock("'modifyBalance::'+#userId")
+    public void modifyBalance(Long userId, BigDecimal amount, BalanceType type,
+                              String reason, boolean lock, String withdrawId) {
+        UserBalance userBalance = userBalanceRepo.findById(userId).orElse(new UserBalance(userId));
+        userBalance.setLastBalance(userBalance.getBalance());
+        userBalance.setBalance(userBalance.getBalance().add(amount));
+        if (lock) {
+            userBalance.setLockTime(LocalDateTime.now());
+            userBalance.setLocked(true);
+            userBalance.setLockReason(reason);
         }
+        userBalanceRepo.saveAndFlush(userBalance);
 
-        if (!success) {
-            userBalance.setLastBalance(userBalance.getBalance());
-            userBalance.setBalance(userBalance.getBalance().add(amount));
-            userBalanceRepo.saveAndFlush(userBalance);
+        balanceRecordRepo.save(BalanceRecord.builder()
+                .time(LocalDateTime.now())
+                .userId(userId)
+                .amount(amount)
+                .balance(userBalance.getBalance())
+                .lastBalance(userBalance.getLastBalance())
+                .type(type)
+                .remark(reason)
+                .withdrawId(withdrawId)
+                .build());
+    }
 
-            balanceRecordRepo.save(BalanceRecord.builder()
-                    .time(LocalDateTime.now())
-                    .userId(userBalance.getUserId())
-                    .amount(amount)
-                    .balance(userBalance.getBalance())
-                    .lastBalance(userBalance.getLastBalance())
-                    .type(BalanceType.RETURN)
-                    .withdrawId(withdrawId)
-                    .remark(msg)
-                    .build());
+    public void realtimeSettleOrder(Order order) {
+        if (!sysConfigService.getBoolean("realtime_settle_order")) {
+            return;
         }
-        if (!success) {
-            userBalance.setLocked(true);
-            userBalance.setLockReason(msg);
-            userBalance.setLockTime(LocalDateTime.now());
-            userBalanceRepo.saveAndFlush(userBalance);
+        BalanceRecord record = balanceRecordRepo.findByOrderIdAndType(order.getId(), BalanceType.SELL);
+        if (record != null) {
+            log.info("此订单已结算 orderId={}", order.getId());
+            return;
         }
+        log.info("结算订单 orderId={}", order.getId());
+        Asset asset = assetRepo.findById(order.getAssetId()).orElse(null);
+
+        UserBalance userBalance = userBalanceRepo.findById(asset.getUserId()).orElse(new UserBalance(asset.getUserId()));
+
+        BigDecimal amount = order.getTotalPrice()
+                .subtract(order.getGasPrice())
+                .multiply(BigDecimal.valueOf(100 - order.getRoyalties() - order.getServiceCharge()))
+                .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
+
+        userBalance.setLastBalance(userBalance.getBalance());
+        userBalance.setBalance(userBalance.getBalance().add(amount));
+        userBalanceRepo.save(userBalance);
+
+        balanceRecordRepo.save(BalanceRecord.builder()
+                .time(LocalDateTime.now())
+                .userId(asset.getUserId())
+                .orderId(order.getId())
+                .amount(amount)
+                .balance(userBalance.getBalance())
+                .lastBalance(userBalance.getLastBalance())
+                .type(BalanceType.SELL)
+                .build());
     }
 }

+ 1 - 0
src/main/java/com/izouma/nineth/service/UserBankCardService.java

@@ -71,6 +71,7 @@ public class UserBankCardService {
             user.setSettleAccountId(request.getBindCardId());
             userService.save(user);
         }
+        userBalanceRepo.unlock(Long.parseLong(request.getUserId()));
     }
 
     public void bindCardCaptcha(String bindCardId) {

+ 33 - 3
src/main/java/com/izouma/nineth/service/UserService.java

@@ -62,6 +62,9 @@ import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.text.SimpleDateFormat;
 import java.time.Duration;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
 import java.util.*;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.regex.Pattern;
@@ -94,6 +97,7 @@ public class UserService {
     private WeakPassRepo                  weakPassRepo;
     private UserBalanceRepo               userBalanceRepo;
     private ContentAuditService           contentAuditService;
+    private OrderRepo                     orderRepo;
 
     public User update(User user) {
         if (!SecurityUtils.hasRole(AuthorityName.ROLE_ADMIN)) {
@@ -119,6 +123,7 @@ public class UserService {
     public User save(User user) {
         if (user.getId() != null) {
             cacheService.clearUserMy(user.getId());
+            cacheService.clearUser(user.getId());
         }
         return userRepo.save(user);
     }
@@ -577,7 +582,7 @@ public class UserService {
             throw new BusinessException("用户不存在或未认证");
         }
         String realName = identityAuthRepo.findFirstByUserIdAndStatusAndDelFalseOrderByCreatedAtDesc(
-                user.getId(), AuthStatus.SUCCESS)
+                        user.getId(), AuthStatus.SUCCESS)
                 .map(IdentityAuth::getRealName).orElse("").replaceAll(".*(?=.)", "**");
         Map<String, Object> map = new HashMap<>();
         map.put("id", user.getId());
@@ -590,8 +595,8 @@ public class UserService {
 
     public Map<String, Object> searchByPhoneAdmin(String phoneStr) {
         List<String> phone = Arrays.stream(phoneStr.replaceAll("\n", " ")
-                .replaceAll("\r\n", " ")
-                .split(" "))
+                        .replaceAll("\r\n", " ")
+                        .split(" "))
                 .map(String::trim)
                 .filter(s -> !StringUtils.isEmpty(s))
                 .collect(Collectors.toList());
@@ -881,4 +886,29 @@ public class UserService {
         result.setCount(BigInteger.valueOf(invitedUserDTOS.size()));
         return result;
     }
+
+    public void enableWallet(Long userId) {
+        User user = userRepo.findById(userId).orElseThrow(new BusinessException("用户不存在"));
+        if (user.isWalletEnabled()) {
+            return;
+        }
+        if (!sysConfigService.getBoolean("enable_wallet")) {
+            throw new BusinessException("绿魔卡功能暂未开启");
+        }
+        IdentityAuth identityAuth = identityAuthRepo.findByUserId(userId).stream().findFirst().orElse(null);
+        if (identityAuth == null) {
+            throw new BusinessException("请先完成实名认证");
+        }
+//        long age = ChronoUnit.YEARS.between(LocalDate.parse(identityAuth.getIdNo().substring(6, 14),
+//                DateTimeFormatter.ofPattern("yyyyMMdd")), LocalDate.now());
+//        if (!((age >= 22 && age <= 55))) {
+//            throw new BusinessException("仅22至55周岁藏家可申请绿魔卡");
+//        }
+//        BigDecimal amount = sysConfigService.getBigDecimal("wallet_enable_amount");
+//        if (Optional.ofNullable(orderRepo.sumUserPrice(userId)).orElse(BigDecimal.ZERO).compareTo(amount) < 0) {
+//            throw new BusinessException("申请绿魔卡需满" + amount + "绿洲石");
+//        }
+        user.setWalletEnabled(true);
+        save(user);
+    }
 }

+ 179 - 0
src/main/java/com/izouma/nineth/service/WithdrawApplyService.java

@@ -0,0 +1,179 @@
+package com.izouma.nineth.service;
+
+import com.alibaba.fastjson.JSONObject;
+import com.izouma.nineth.annotations.RedisLock;
+import com.izouma.nineth.config.Constants;
+import com.izouma.nineth.domain.WithdrawApply;
+import com.izouma.nineth.dto.PageQuery;
+import com.izouma.nineth.dto.UserBankCard;
+import com.izouma.nineth.enums.BalanceType;
+import com.izouma.nineth.enums.WithdrawStatus;
+import com.izouma.nineth.exception.BusinessException;
+import com.izouma.nineth.repo.BalanceRecordRepo;
+import com.izouma.nineth.repo.UserBalanceRepo;
+import com.izouma.nineth.repo.UserBankCardRepo;
+import com.izouma.nineth.repo.WithdrawApplyRepo;
+import com.izouma.nineth.utils.JpaUtils;
+import com.izouma.nineth.utils.SnowflakeIdWorker;
+import lombok.AllArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.core.env.Environment;
+import org.springframework.data.domain.Page;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+@Service
+@AllArgsConstructor
+public class WithdrawApplyService {
+
+    private WithdrawApplyRepo  withdrawApplyRepo;
+    private UserBalanceService userBalanceService;
+    private UserBalanceRepo    userBalanceRepo;
+    private BalanceRecordRepo  balanceRecordRepo;
+    private SysConfigService   sysConfigService;
+    private SandPayService     sandPayService;
+    private UserBankCardRepo   userBankCardRepo;
+    private SnowflakeIdWorker  snowflakeIdWorker;
+    private Environment        env;
+    private RedissonClient     redissonClient;
+
+    public Page<WithdrawApply> all(PageQuery pageQuery) {
+        return withdrawApplyRepo.findAll(JpaUtils.toSpecification(pageQuery, WithdrawApply.class), JpaUtils.toPageRequest(pageQuery));
+    }
+
+    @RedisLock("'withdrawApple'+#userId")
+    public WithdrawApply apply(Long userId, BigDecimal amount) {
+        if (!sysConfigService.getBoolean("enable_withdraw")) {
+            throw new BusinessException("提现功能暂时关闭");
+        }
+        int limit = sysConfigService.getInt("daily_withdraw_limit");
+        if (withdrawApplyRepo.countByUserIdAndCreatedAtBetween(userId,
+                LocalDate.now().atStartOfDay(), LocalDate.now().atTime(LocalTime.MAX)) > limit) {
+            throw new BusinessException("每天只能申请提现" + limit + "次");
+        }
+        userBalanceService.preWithdraw(userId, amount);
+        WithdrawApply withdrawApply = WithdrawApply.builder()
+                .userId(userId)
+                .amount(amount)
+                .status(WithdrawStatus.PENDING)
+                .build();
+        return withdrawApplyRepo.save(withdrawApply);
+    }
+
+    @RedisLock("'finishWithdrawApply'+#id")
+    public WithdrawApply finishWithdrawApply(Long id, boolean approve, String reason) {
+        WithdrawApply apply = withdrawApplyRepo.findById(id).orElseThrow(new BusinessException("提现申请不存在"));
+        if (apply.getStatus() != WithdrawStatus.PENDING) {
+            throw new BusinessException("提现申请已处理");
+        }
+        if (approve) {
+            UserBankCard bankCard = userBankCardRepo.findByUserId(apply.getUserId())
+                    .stream().findFirst()
+                    .orElse(null);
+
+            if (bankCard == null) {
+                apply.setStatus(WithdrawStatus.FAIL);
+                apply.setFinishTime(LocalDateTime.now());
+                apply.setReason(Optional.ofNullable(reason).orElse("用户未绑卡"));
+
+                userBalanceService.modifyBalance(apply.getUserId(), apply.getAmount(), BalanceType.DENY,
+                        "用户未绑卡", false, null);
+            } else {
+                BigDecimal chargeAmount;
+                String withdrawCharge = sysConfigService.getString("withdraw_charge");
+                if (StringUtils.isNotEmpty(withdrawCharge) && Pattern.matches("^(\\d+|\\d+.\\d+),\\d+$", withdrawCharge)) {
+                    String[] arr = withdrawCharge.split(",");
+                    chargeAmount = new BigDecimal(arr[0]);
+                    BigDecimal minChargeAmount = new BigDecimal(arr[1]);
+                    if (chargeAmount.compareTo(minChargeAmount) < 0) {
+                        chargeAmount = minChargeAmount;
+                    }
+                } else {
+                    chargeAmount = BigDecimal.ZERO;
+                }
+                String withdrawId = snowflakeIdWorker.nextId() + "";
+                try {
+                    String msg = "";
+                    boolean success = false;
+
+                    if (Arrays.asList(env.getActiveProfiles()).contains("prod")) {
+                        try {
+                            JSONObject res = sandPayService.transfer(withdrawId, bankCard.getRealName(), bankCard.getBankNo(),
+                                    apply.getAmount().subtract(chargeAmount));
+                            if ("0000".equals(res.getString("respCode"))) {
+                                success = true;
+                            } else {
+                                msg = res.getString("respDesc");
+                            }
+                        } catch (Exception e) {
+                            msg = e.getMessage();
+                        }
+                        if (!success) {
+                            throw new BusinessException(msg);
+                        }
+
+                        apply.setStatus(WithdrawStatus.SUCCESS);
+                        apply.setFinishTime(LocalDateTime.now());
+                        apply.setWithdrawId(withdrawId);
+                        apply.setChannel(Constants.PayChannel.SAND);
+                    } else {
+                        if (Math.random() > 0.5) {
+                            success = true;
+                            apply.setStatus(WithdrawStatus.SUCCESS);
+                            apply.setFinishTime(LocalDateTime.now());
+                            apply.setWithdrawId(withdrawId);
+                            apply.setChannel(Constants.PayChannel.SAND);
+                        } else {
+                            throw new BusinessException("测试服随机失败");
+                        }
+                    }
+                } catch (Exception e) {
+                    apply.setStatus(WithdrawStatus.FAIL);
+                    apply.setFinishTime(LocalDateTime.now());
+                    apply.setReason(e.getMessage());
+
+                    boolean lock = e.getMessage() == null || !e.getMessage().contains("系统繁忙");
+                    userBalanceService.modifyBalance(apply.getUserId(), apply.getAmount(), BalanceType.RETURN,
+                            e.getMessage(), lock, withdrawId);
+                }
+            }
+
+        } else {
+            apply.setStatus(WithdrawStatus.FAIL);
+            apply.setFinishTime(LocalDateTime.now());
+            apply.setReason(Optional.ofNullable(reason).orElse("审核不通过"));
+
+            userBalanceService.modifyBalance(apply.getUserId(), apply.getAmount(), BalanceType.DENY,
+                    apply.getReason(), false, null);
+        }
+        return withdrawApplyRepo.save(apply);
+    }
+
+    @Async
+    @RedisLock(value = "'approveAll'", expire = 1, unit = TimeUnit.HOURS)
+    public void approveAll() throws ExecutionException, InterruptedException {
+        new ForkJoinPool(5).submit(() -> {
+            withdrawApplyRepo.findByCreatedAtBetweenAndStatus(LocalDate.now().minusDays(1).atStartOfDay(),
+                            LocalDate.now().minusDays(1).atTime(LocalTime.MAX), WithdrawStatus.PENDING)
+                    .parallelStream().forEach(withdrawApply -> {
+                        try {
+                            finishWithdrawApply(withdrawApply.getId(), true, null);
+                        } catch (Exception ignored) {
+                        }
+                    });
+        }).get();
+    }
+}

+ 1 - 0
src/main/java/com/izouma/nineth/utils/JpaUtils.java

@@ -31,6 +31,7 @@ public class JpaUtils {
     private static SearchMode defaultSearchMode = SearchMode.PREFIX;
 
     public static void setDefaultSearchMode(SearchMode defaultSearchMode) {
+        log.info("set default search mode to {}", defaultSearchMode.name());
         JpaUtils.defaultSearchMode = defaultSearchMode;
     }
 

+ 36 - 0
src/main/java/com/izouma/nineth/web/CacheController.java

@@ -0,0 +1,36 @@
+package com.izouma.nineth.web;
+
+import com.izouma.nineth.service.CacheService;
+import lombok.AllArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.text.CaseUtils;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+@RestController
+@RequestMapping("/cache")
+@AllArgsConstructor
+public class CacheController {
+
+    private final CacheService cacheService;
+
+    @RequestMapping("/clear")
+    @PreAuthorize("hasRole('ADMIN')")
+    public void clear(String name, String stringParam, Long longParam) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+        Method method;
+        if (stringParam != null) {
+            method = CacheService.class.getMethod("clear" + StringUtils.capitalize((name)), String.class);
+            method.invoke(cacheService, stringParam);
+        } else if (longParam != null) {
+            method = CacheService.class.getMethod("clear" + StringUtils.capitalize((name)), Long.class);
+            method.invoke(cacheService, longParam);
+        } else {
+            method = CacheService.class.getMethod("clear" + StringUtils.capitalize((name)));
+            method.invoke(cacheService);
+        }
+    }
+}

+ 9 - 9
src/main/java/com/izouma/nineth/web/HmPayController.java

@@ -7,7 +7,6 @@ import com.izouma.nineth.config.GeneralProperties;
 import com.izouma.nineth.config.HmPayProperties;
 import com.izouma.nineth.enums.PayMethod;
 import com.izouma.nineth.event.OrderNotifyEvent;
-import com.izouma.nineth.service.*;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
@@ -27,14 +26,9 @@ import java.util.stream.Collectors;
 @Slf4j
 @AllArgsConstructor
 public class HmPayController extends BaseController {
-    private final HMPayService       hmPayService;
     private final HmPayProperties    hmPayProperties;
     private final RocketMQTemplate   rocketMQTemplate;
-    private final OrderService       orderService;
-    private final MintOrderService   mintOrderService;
-    private final GiftOrderService   giftOrderService;
     private final GeneralProperties  generalProperties;
-    private final UserBalanceService userBalanceService;
 
     @GetMapping("/notify/{type}/{id}")
     public String orderNotify(@PathVariable String type, @PathVariable Long id, HttpServletRequest req) throws AlipayApiException {
@@ -60,13 +54,19 @@ public class HmPayController extends BaseController {
                             new OrderNotifyEvent(id, PayMethod.HMPAY, plat_trx_no, System.currentTimeMillis()));
                     break;
                 case "gift":
-                    giftOrderService.giftNotify(id, PayMethod.HMPAY, plat_trx_no);
+                    rocketMQTemplate.syncSend(generalProperties.getOrderNotifyTopic(),
+                            new OrderNotifyEvent(id, PayMethod.SANDPAY, plat_trx_no,
+                                    System.currentTimeMillis(), OrderNotifyEvent.TYPE_GIFT_ORDER));
                     break;
                 case "mintOrder":
-                    mintOrderService.mintNotify(id, PayMethod.HMPAY, plat_trx_no);
+                    rocketMQTemplate.syncSend(generalProperties.getOrderNotifyTopic(),
+                            new OrderNotifyEvent(id, PayMethod.SANDPAY, plat_trx_no,
+                                    System.currentTimeMillis(), OrderNotifyEvent.TYPE_MINT_ORDER));
                     break;
                 case "recharge":
-                    userBalanceService.recharge(id, PayMethod.HMPAY, plat_trx_no);
+                    rocketMQTemplate.syncSend(generalProperties.getOrderNotifyTopic(),
+                            new OrderNotifyEvent(id, PayMethod.SANDPAY, plat_trx_no,
+                                    System.currentTimeMillis(), OrderNotifyEvent.TYPE_RECHARGE));
                     break;
             }
         }

+ 4 - 1
src/main/java/com/izouma/nineth/web/MintOrderController.java

@@ -34,7 +34,7 @@ public class MintOrderController extends BaseController {
     private MintActivityRepo mintActivityRepo;
     private MintMaterialRepo mintMaterialRepo;
 
-    //@PreAuthorize("hasRole('ADMIN')")
+    @PreAuthorize("hasRole('ADMIN')")
     @PostMapping("/save")
     public MintOrder save(@RequestBody MintOrder record) {
         if (record.getId() != null) {
@@ -76,6 +76,7 @@ public class MintOrderController extends BaseController {
         return mintOrder;
     }
 
+    @PreAuthorize("hasRole('ADMIN')")
     @PostMapping("/del/{id}")
     public void del(@PathVariable Long id) {
         mintOrderRepo.softDelete(id);
@@ -95,6 +96,7 @@ public class MintOrderController extends BaseController {
         return mintOrderService.create(SecurityUtils.getAuthenticatedUser(), assetIds, mintActivityId, addressId);
     }
 
+    @PreAuthorize("hasRole('ADMIN')")
     @ApiOperation("导出")
     @PostMapping("/excelPhone")
     public void excelPhone(HttpServletResponse response, @RequestBody PageQuery pageQuery) throws IOException {
@@ -102,6 +104,7 @@ public class MintOrderController extends BaseController {
         ExcelUtils.export(response, data);
     }
 
+    @PreAuthorize("hasRole('ADMIN')")
     @ApiOperation("订单完成")
     @GetMapping("/finish/{id}")
     public void finish(@PathVariable Long id) {

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

@@ -36,12 +36,13 @@ public class OrderPayController {
     private final OrderRepo        orderRepo;
     private final MintOrderService mintOrderService;
     private final SandPayService   sandPayService;
+    private final OrderPayService  orderPayService;
 
     @RequestMapping(value = "/alipay_h5", method = RequestMethod.GET)
     @ResponseBody
     public String payOrderAlipayH5(Long id, Model model) throws BaseAdaPayException {
 //        return (String) orderService.payAdapay(id, "alipay_wap", null);
-        return sandPayService.payOrder(id);
+        return orderPayService.payOrder(id);
     }
 
     @RequestMapping(value = "/alipay_wx", method = RequestMethod.GET)
@@ -60,7 +61,7 @@ public class OrderPayController {
         if (order.getStatus() != OrderStatus.NOT_PAID) {
             return "redirect:/9th/store";
         }
-        String payUrl = sandPayService.payOrder(id);
+        String payUrl = orderPayService.payOrder(id);
         model.addAttribute("payUrl", payUrl);
         model.addAttribute("orderId", id);
         return "AlipayHtml";
@@ -75,7 +76,7 @@ public class OrderPayController {
     @RequestMapping(value = "/alipay_app", method = RequestMethod.GET)
     @ResponseBody
     public String payOrderAlipayApp(Long id, Model model) throws BaseAdaPayException {
-        return sandPayService.payOrder(id);
+        return orderPayService.payOrder(id);
     }
 
     @RequestMapping(value = "/weixin_h5")
@@ -105,7 +106,7 @@ public class OrderPayController {
     @ResponseBody
     public String sandQuick(@RequestParam Long id, Model model) throws BaseAdaPayException {
 //        return (String) orderService.payAdapay(id, "alipay_wap", null);
-        return sandPayService.payOrderQuick(id);
+        return orderPayService.payOrderQuick(id);
     }
 
     @ApiOperation("衫德h5快捷")
@@ -113,20 +114,20 @@ public class OrderPayController {
     @ResponseBody
     public String agreementPay(@RequestParam Long id) {
 //        return (String) orderService.payAdapay(id, "alipay_wap", null);
-        return sandPayService.payOrderQuick(id);
+        return orderPayService.payOrderQuick(id);
     }
 
     @RequestMapping(value = "/gift/alipay_h5", method = RequestMethod.GET)
     @ResponseBody
     public String payGiftOrderAlipayH5(Long id, Model model) throws BaseAdaPayException {
 //        return (String) giftOrderService.payAdapay(id, "alipay_wap", null);
-        return sandPayService.payGiftOrder(id);
+        return orderPayService.payGiftOrder(id);
     }
 
     @RequestMapping(value = "/gift/alipay_wx", method = RequestMethod.GET)
     public String payGiftOrderAlipayWx(Long id, Model model) throws BaseAdaPayException {
 //        String payUrl = (String) giftOrderService.payAdapay(id, "alipay_wap", null);
-        String payUrl = sandPayService.payGiftOrder(id);
+        String payUrl = orderPayService.payGiftOrder(id);
         model.addAttribute("payUrl", payUrl);
         model.addAttribute("orderId", id);
         return "AlipayHtml";
@@ -135,13 +136,13 @@ public class OrderPayController {
     @RequestMapping(value = "/gift/alipay_qr", method = RequestMethod.GET)
     @ResponseBody
     public String payGiftOrderAlipayQR(Long id, Model model) throws BaseAdaPayException {
-        return sandPayService.payGiftOrder(id);
+        return orderPayService.payGiftOrder(id);
     }
 
     @RequestMapping(value = "/gift/alipay_app", method = RequestMethod.GET)
     @ResponseBody
     public String payGiftOrderAlipayApp(Long id, Model model) throws BaseAdaPayException {
-        return sandPayService.payGiftOrder(id);
+        return orderPayService.payGiftOrder(id);
     }
 
     @RequestMapping(value = "/gift/weixin_h5")
@@ -170,13 +171,13 @@ public class OrderPayController {
     @ResponseBody
     public String payMintOrderAlipayH5(Long id, Model model) throws BaseAdaPayException {
 //        return (String) mintOrderService.payAdapay(id, "alipay_wap", null);
-        return sandPayService.payMintOrder(id);
+        return orderPayService.payMintOrder(id);
     }
 
     @RequestMapping(value = "/mint/alipay_wx", method = RequestMethod.GET)
     public String payMintOrderAlipayWx(Long id, Model model) throws BaseAdaPayException {
 //        String payUrl = (String) mintOrderService.payAdapay(id, "alipay_wap", null);
-        String payUrl = sandPayService.payMintOrder(id);
+        String payUrl = orderPayService.payMintOrder(id);
         model.addAttribute("payUrl", payUrl);
         model.addAttribute("orderId", id);
         return "AlipayHtml2";
@@ -192,7 +193,7 @@ public class OrderPayController {
     @ResponseBody
     public String payMintOrderAlipayApp(Long id, Model model) throws BaseAdaPayException {
 //        return (String) mintOrderService.payAdapay(id, "alipay", null);
-        return sandPayService.payMintOrder(id);
+        return orderPayService.payMintOrder(id);
     }
 
     @RequestMapping(value = "/mint/weixin_h5")

+ 13 - 5
src/main/java/com/izouma/nineth/web/OrderPayControllerV2.java

@@ -64,7 +64,7 @@ public class OrderPayControllerV2 {
     @RequestMapping(value = "/sandQuick", produces = "text/html")
     @ResponseBody
     public String sandQuick(@RequestParam Long id) {
-        return sandPayService.payOrderQuick(id);
+        return orderPayService.payOrderQuick(id);
     }
 
     @ApiOperation("首信易协议支付")
@@ -97,6 +97,7 @@ public class OrderPayControllerV2 {
     }
 
     @RequestMapping(value = "/gift/balance")
+    @ResponseBody
     public void payGiftOrderBalance(@RequestParam Long id, @RequestParam String tradeCode) {
         orderPayService.payGiftBalance(id, SecurityUtils.getAuthenticatedUser().getId(), tradeCode);
     }
@@ -104,7 +105,7 @@ public class OrderPayControllerV2 {
     @RequestMapping(value = "/gift/sandQuick", produces = "text/html")
     @ResponseBody
     public String payGiftQuick(@RequestParam Long id) {
-        return sandPayService.payGiftQuick(id);
+        return orderPayService.payGiftQuick(id);
     }
 
     @RequestMapping(value = "/gift/agreement")
@@ -130,10 +131,11 @@ public class OrderPayControllerV2 {
     @RequestMapping(value = "/mint/sandQuick", produces = "text/html")
     @ResponseBody
     public String payMintQuick(@RequestParam Long id) {
-        return sandPayService.payMintQuick(id);
+        return orderPayService.payMintQuick(id);
     }
 
     @RequestMapping(value = "/mint/balance")
+    @ResponseBody
     public void payMintOrderBalance(@RequestParam Long id, @RequestParam String tradeCode) {
         orderPayService.payMintOrderBalance(id, SecurityUtils.getAuthenticatedUser().getId(), tradeCode);
     }
@@ -152,8 +154,14 @@ public class OrderPayControllerV2 {
 
     @RequestMapping(value = "/recharge/agreement")
     @ResponseBody
-    public Map<String, Object> payRechargeAgreement(@RequestParam Long id, @RequestParam BigDecimal amount,
+    public Map<String, Object> payRechargeAgreement(@RequestParam Long userId, @RequestParam BigDecimal amount,
                                                     String bindCardId) {
-        return orderPayService.rechargeAgreement(id, amount, bindCardId);
+        return orderPayService.rechargeAgreement(userId, amount, bindCardId);
+    }
+
+    @RequestMapping(value = "/recharge/sandQuick", produces = "text/html")
+    @ResponseBody
+    public String payRechargeQuick(@RequestParam Long userId, @RequestParam BigDecimal amount) {
+        return orderPayService.rechargeQuick(userId, amount);
     }
 }

+ 32 - 42
src/main/java/com/izouma/nineth/web/PayChannelMgmtController.java

@@ -1,18 +1,20 @@
 package com.izouma.nineth.web;
 
 import com.alibaba.fastjson.JSON;
+import com.izouma.nineth.config.Constants;
+import com.izouma.nineth.dto.PayQuery;
+import com.izouma.nineth.exception.BusinessException;
 import com.izouma.nineth.service.HMPayService;
+import com.izouma.nineth.service.OrderPayService;
+import com.izouma.nineth.service.PayEaseService;
 import com.izouma.nineth.service.SandPayService;
 import com.izouma.nineth.utils.SnowflakeIdWorker;
 import lombok.AllArgsConstructor;
-import lombok.NoArgsConstructor;
 import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
 import java.math.BigDecimal;
+import java.time.LocalDateTime;
 
 @RestController
 @RequestMapping("/payChannelMgmt")
@@ -21,21 +23,30 @@ public class PayChannelMgmtController {
     private final SandPayService    sandPayService;
     private final HMPayService      hmPayService;
     private final SnowflakeIdWorker snowflakeIdWorker;
+    private final PayEaseService    payEaseService;
+    private final OrderPayService   orderPayService;
 
-    @GetMapping(value = "/pay")
+    @PostMapping(value = "/pay")
     @PreAuthorize("hasRole('ADMIN')")
-    public String pay(@RequestParam String channel) {
+    public Object pay(@RequestParam String channel, @RequestParam String orderId, String userId, String bindCardId,
+                      BigDecimal amount) {
+        if (amount == null) {
+            amount = new BigDecimal("0.01");
+        }
         switch (channel) {
-            case "sandPay":
-                return sandPayService.requestAlipay(snowflakeIdWorker.nextId() + "",
-                        new BigDecimal("0.01"), "话费充值", "话费充值",
-                        SandPayService.getTimeout(180), "{\"type\":\"test\",\"id\":1}");
-            case "hmPay":
-                return hmPayService.requestAlipay(snowflakeIdWorker.nextId() + "",
-                        new BigDecimal("0.01"), "话费充值",
+            case "sandQuick":
+                return sandPayService.payQuick(orderId, "测试", amount,
+                        LocalDateTime.now().plusMinutes(3), "test", "https://www.baidu.com");
+            case Constants.PayChannel.SAND:
+                return sandPayService.pay(orderId, "测试", amount,
+                        LocalDateTime.now().plusMinutes(3), "test");
+            case Constants.PayChannel.HM:
+                return hmPayService.requestAlipay(orderId, amount, "测试",
                         HMPayService.getTimeout(180), "test", "https://www.baidu.com");
+            case Constants.PayChannel.PE:
+                return payEaseService.pay("测试", orderId, amount, userId, bindCardId, "test");
         }
-        return null;
+        throw new BusinessException("不支持此渠道");
     }
 
     @GetMapping(value = "/transfer")
@@ -44,38 +55,17 @@ public class PayChannelMgmtController {
         return JSON.toJSONString(sandPayService.transfer(snowflakeIdWorker.nextId() + "", name, bank, amount), true);
     }
 
-    @GetMapping(value = "/refund")
+    @PostMapping(value = "/refund")
     @PreAuthorize("hasRole('ADMIN')")
-    public String refund(@RequestParam String channel, @RequestParam String orderId, @RequestParam BigDecimal amount) {
-        switch (channel) {
-            case "sandPay":
-                if (orderId.length() < 12) {
-                    for (int i = orderId.length(); i < 12; i++) {
-                        orderId = "0" + orderId;
-                    }
-                }
-                return JSON.toJSONString(sandPayService.refund(orderId, amount), true);
-            case "hmPay":
-                if (orderId.length() < 12) {
-                    for (int i = orderId.length(); i < 12; i++) {
-                        orderId = "0" + orderId;
-                    }
-                }
-                return JSON.toJSONString(hmPayService.refund(orderId, amount), true);
-        }
-        return null;
+    public Object refund(@RequestParam String channel, @RequestParam String orderId, String transactionId,
+                         @RequestParam BigDecimal amount) {
+        return orderPayService.refund(orderId, transactionId, amount, channel);
     }
 
     @GetMapping(value = "/query")
     @PreAuthorize("hasRole('ADMIN')")
-    public String query(@RequestParam String channel, @RequestParam String id) {
-        switch (channel) {
-            case "sandPay":
-                return JSON.toJSONString(sandPayService.query(id), true);
-            case "hmPay":
-                return JSON.toJSONString(hmPayService.query(id), true);
-        }
-        return null;
+    public PayQuery query(@RequestParam String orderId) {
+        return orderPayService.query(orderId);
     }
 
     @GetMapping("/queryTransfer")

+ 11 - 17
src/main/java/com/izouma/nineth/web/PayEaseController.java

@@ -5,32 +5,20 @@ import com.alibaba.fastjson15.JSONObject;
 import com.izouma.nineth.config.GeneralProperties;
 import com.izouma.nineth.enums.PayMethod;
 import com.izouma.nineth.event.OrderNotifyEvent;
-import com.izouma.nineth.service.GiftOrderService;
-import com.izouma.nineth.service.MintOrderService;
-import com.izouma.nineth.service.UserBalanceService;
 import com.upay.sdk.CipherWrapper;
-import com.upay.sdk.executer.ResultListenerAdpater;
 import com.upay.sdk.onlinepay.executer.OnlinePayOrderExecuter;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.lang3.StringUtils;
 import org.apache.rocketmq.spring.core.RocketMQTemplate;
 import org.springframework.web.bind.annotation.*;
 
-import javax.servlet.http.HttpServletRequest;
-import java.util.HashMap;
-import java.util.Map;
-
 @RestController
 @RequestMapping("/payease")
 @Slf4j
 @AllArgsConstructor
 public class PayEaseController {
-    private final GeneralProperties  generalProperties;
-    private final RocketMQTemplate   rocketMQTemplate;
-    private final GiftOrderService   giftOrderService;
-    private final MintOrderService   mintOrderService;
-    private final UserBalanceService userBalanceService;
+    private final GeneralProperties generalProperties;
+    private final RocketMQTemplate  rocketMQTemplate;
 
     @PostMapping("/notify/{type}/{id}")
     public String notify(@PathVariable String type, @PathVariable Long id, @RequestHeader String encryptKey,
@@ -51,13 +39,19 @@ public class PayEaseController {
                             new OrderNotifyEvent(id, PayMethod.PAYEASE, serialNumber, System.currentTimeMillis()));
                     break;
                 case "gift":
-                    giftOrderService.giftNotify(id, PayMethod.PAYEASE, serialNumber);
+                    rocketMQTemplate.syncSend(generalProperties.getOrderNotifyTopic(),
+                            new OrderNotifyEvent(id, PayMethod.SANDPAY, serialNumber,
+                                    System.currentTimeMillis(), OrderNotifyEvent.TYPE_GIFT_ORDER));
                     break;
                 case "mintOrder":
-                    mintOrderService.mintNotify(id, PayMethod.PAYEASE, serialNumber);
+                    rocketMQTemplate.syncSend(generalProperties.getOrderNotifyTopic(),
+                            new OrderNotifyEvent(id, PayMethod.SANDPAY, serialNumber,
+                                    System.currentTimeMillis(), OrderNotifyEvent.TYPE_MINT_ORDER));
                     break;
                 case "recharge":
-                    userBalanceService.recharge(id, PayMethod.PAYEASE, serialNumber);
+                    rocketMQTemplate.syncSend(generalProperties.getOrderNotifyTopic(),
+                            new OrderNotifyEvent(id, PayMethod.SANDPAY, serialNumber,
+                                    System.currentTimeMillis(), OrderNotifyEvent.TYPE_RECHARGE));
                     break;
             }
         }

+ 16 - 19
src/main/java/com/izouma/nineth/web/SandPayController.java

@@ -6,21 +6,16 @@ import com.alibaba.fastjson.JSONObject;
 import com.izouma.nineth.config.GeneralProperties;
 import com.izouma.nineth.enums.PayMethod;
 import com.izouma.nineth.event.OrderNotifyEvent;
-import com.izouma.nineth.exception.BusinessException;
-import com.izouma.nineth.service.GiftOrderService;
-import com.izouma.nineth.service.MintOrderService;
-import com.izouma.nineth.service.SandPayService;
-import com.izouma.nineth.service.UserBalanceService;
-import com.izouma.nineth.utils.SnowflakeIdWorker;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.codec.binary.Base64;
 import org.apache.rocketmq.spring.core.RocketMQTemplate;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import java.math.BigDecimal;
 import java.nio.charset.StandardCharsets;
 
 @RestController
@@ -29,13 +24,8 @@ import java.nio.charset.StandardCharsets;
 @AllArgsConstructor
 public class SandPayController {
 
-    private SandPayService     sandPayService;
-    private SnowflakeIdWorker  snowflakeIdWorker;
-    private GeneralProperties  generalProperties;
-    private RocketMQTemplate   rocketMQTemplate;
-    private GiftOrderService   giftOrderService;
-    private MintOrderService   mintOrderService;
-    private UserBalanceService userBalanceService;
+    private GeneralProperties generalProperties;
+    private RocketMQTemplate  rocketMQTemplate;
 
     @PostMapping("/notify")
     public Object notifyOrder(HttpServletRequest req, HttpServletResponse resp) {
@@ -67,16 +57,23 @@ public class SandPayController {
                         switch (type) {
                             case "order":
                                 rocketMQTemplate.syncSend(generalProperties.getOrderNotifyTopic(),
-                                        new OrderNotifyEvent(id, PayMethod.SANDPAY, payOrderCode, System.currentTimeMillis()));
+                                        new OrderNotifyEvent(id, PayMethod.SANDPAY,
+                                                payOrderCode, System.currentTimeMillis()));
                                 break;
                             case "gift":
-                                giftOrderService.giftNotify(id, PayMethod.SANDPAY, payOrderCode);
+                                rocketMQTemplate.syncSend(generalProperties.getOrderNotifyTopic(),
+                                        new OrderNotifyEvent(id, PayMethod.SANDPAY, payOrderCode,
+                                                System.currentTimeMillis(), OrderNotifyEvent.TYPE_GIFT_ORDER));
                                 break;
                             case "mintOrder":
-                                mintOrderService.mintNotify(id, PayMethod.SANDPAY, payOrderCode);
+                                rocketMQTemplate.syncSend(generalProperties.getOrderNotifyTopic(),
+                                        new OrderNotifyEvent(id, PayMethod.SANDPAY, payOrderCode,
+                                                System.currentTimeMillis(), OrderNotifyEvent.TYPE_MINT_ORDER));
                                 break;
                             case "recharge":
-                                userBalanceService.recharge(id, PayMethod.SANDPAY, payOrderCode);
+                                rocketMQTemplate.syncSend(generalProperties.getOrderNotifyTopic(),
+                                        new OrderNotifyEvent(id, PayMethod.SANDPAY, payOrderCode,
+                                                System.currentTimeMillis(), OrderNotifyEvent.TYPE_RECHARGE));
                                 break;
                         }
                     }

+ 17 - 3
src/main/java/com/izouma/nineth/web/SysConfigController.java

@@ -7,11 +7,13 @@ import com.izouma.nineth.domain.SysConfig;
 import com.izouma.nineth.dto.PageQuery;
 import com.izouma.nineth.exception.BusinessException;
 import com.izouma.nineth.repo.SysConfigRepo;
+import com.izouma.nineth.service.CacheService;
 import com.izouma.nineth.service.SysConfigService;
 import com.izouma.nineth.utils.excel.ExcelUtils;
 import lombok.AllArgsConstructor;
 import org.apache.rocketmq.spring.core.RocketMQTemplate;
 import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
 import org.springframework.data.domain.Page;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
@@ -19,7 +21,9 @@ import org.springframework.web.bind.annotation.*;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.math.BigDecimal;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 @RestController
 @RequestMapping("/sysConfig")
@@ -29,12 +33,13 @@ public class SysConfigController extends BaseController {
     private SysConfigRepo     sysConfigRepo;
     private RocketMQTemplate  rocketMQTemplate;
     private GeneralProperties generalProperties;
+    private CacheService      cacheService;
 
     @PreAuthorize("hasRole('ADMIN')")
     @PostMapping("/save")
-    @CacheEvict(value = {"SysConfigServiceGetBigDecimal", "SysConfigServiceGetTime", "SysConfigServiceGetBoolean", "SysConfigServiceGetInt"}, allEntries = true)
     public SysConfig save(@RequestBody SysConfig record) {
         record = sysConfigRepo.save(record);
+        cacheService.clearSysConfigGet();
 
         JSONObject jsonObject = new JSONObject();
         jsonObject.put("name", EventNames.CONFIG_CHANGE);
@@ -54,8 +59,17 @@ public class SysConfigController extends BaseController {
     }
 
     @GetMapping("/get/{id}")
-    public SysConfig get(@PathVariable String id) {
-        return sysConfigRepo.findByName(id).orElseThrow(new BusinessException("无记录"));
+    @Cacheable("sysConfigGet")
+    public Object get(@PathVariable String id) {
+        if (id.contains(",")) {
+            Map<String, SysConfig> map = new HashMap<>();
+            for (String name : id.split(",")) {
+                map.put(name, sysConfigRepo.findByName(name).orElseThrow(null));
+            }
+            return map;
+        } else {
+            return sysConfigRepo.findByName(id).orElseThrow(new BusinessException("无记录"));
+        }
     }
 
     @GetMapping("/getValue/{id}")

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

@@ -7,6 +7,11 @@ import com.izouma.nineth.exception.BusinessException;
 import com.izouma.nineth.repo.TestClassRepo;
 import com.izouma.nineth.service.TestClassService;
 import com.izouma.nineth.utils.excel.ExcelUtils;
+import io.github.bucket4j.Bandwidth;
+import io.github.bucket4j.Bucket;
+import io.github.bucket4j.BucketConfiguration;
+import io.github.bucket4j.Refill;
+import io.github.bucket4j.distributed.proxy.ProxyManager;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.data.domain.Page;
@@ -14,6 +19,7 @@ import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
+import java.time.Duration;
 import java.util.List;
 
 @RestController
@@ -21,8 +27,9 @@ import java.util.List;
 @AllArgsConstructor
 @Slf4j
 public class TestClassController extends BaseController {
-    private TestClassService testClassService;
-    private TestClassRepo    testClassRepo;
+    private TestClassService     testClassService;
+    private TestClassRepo        testClassRepo;
+    private ProxyManager<String> buckets;
 
     //@PreAuthorize("hasRole('ADMIN')")
     @PostMapping("/save")
@@ -60,5 +67,17 @@ public class TestClassController extends BaseController {
         List<TestClass> data = all(pageQuery).getContent();
         ExcelUtils.export(response, data);
     }
+
+    @GetMapping("/test")
+    public String test() {
+        Bucket bucket = buckets.builder().build("test1", () -> (BucketConfiguration.builder()
+                .addLimit(Bandwidth.classic(10, Refill.intervally(10, Duration.ofSeconds(10))))
+                .build()));
+        if (bucket.tryConsume(1)) {
+            return "ok";
+        } else {
+            throw new BusinessException("rate limit");
+        }
+    }
 }
 

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

@@ -313,6 +313,11 @@ public class UserController extends BaseController {
     public InvitorDetailDTO invitorList(@RequestParam Long collectionId, @RequestParam Long userId) {
         return userService.findMyInviteRecord(userId, collectionId);
     }
+
+    @PostMapping("/enableWallet")
+    public void enableWallet() {
+        userService.enableWallet(SecurityUtils.getAuthenticatedUser().getId());
+    }
 }
 
 

+ 65 - 0
src/main/java/com/izouma/nineth/web/WithdrawApplyController.java

@@ -0,0 +1,65 @@
+package com.izouma.nineth.web;
+
+import com.izouma.nineth.domain.WithdrawApply;
+import com.izouma.nineth.service.WithdrawApplyService;
+import com.izouma.nineth.dto.PageQuery;
+import com.izouma.nineth.exception.BusinessException;
+import com.izouma.nineth.repo.WithdrawApplyRepo;
+import com.izouma.nineth.utils.ObjUtils;
+import com.izouma.nineth.utils.SecurityUtils;
+import com.izouma.nineth.utils.excel.ExcelUtils;
+import lombok.AllArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+@RestController
+@RequestMapping("/withdrawApply")
+@AllArgsConstructor
+public class WithdrawApplyController extends BaseController {
+    private WithdrawApplyService withdrawApplyService;
+    private WithdrawApplyRepo    withdrawApplyRepo;
+
+    @PreAuthorize("hasRole('ADMIN')")
+    @PostMapping("/all")
+    public Page<WithdrawApply> all(@RequestBody PageQuery pageQuery) {
+        return withdrawApplyService.all(pageQuery);
+    }
+
+    @PreAuthorize("hasRole('ADMIN')")
+    @GetMapping("/get/{id}")
+    public WithdrawApply get(@PathVariable Long id) {
+        return withdrawApplyRepo.findById(id).orElseThrow(new BusinessException("无记录"));
+    }
+
+    @GetMapping("/excel")
+    @ResponseBody
+    public void excel(HttpServletResponse response, PageQuery pageQuery) throws IOException {
+        List<WithdrawApply> data = all(pageQuery).getContent();
+        ExcelUtils.export(response, data);
+    }
+
+    @PostMapping("/apply")
+    public WithdrawApply apply(@RequestParam BigDecimal amount) {
+        return withdrawApplyService.apply(SecurityUtils.getAuthenticatedUser().getId(), amount);
+    }
+
+    @PreAuthorize("hasRole('ADMIN')")
+    @PostMapping("/finish")
+    public WithdrawApply finish(@RequestParam Long id, @RequestParam boolean approve, String reason) {
+        return withdrawApplyService.finishWithdrawApply(id, approve, reason);
+    }
+
+    @PreAuthorize("hasRole('ADMIN')")
+    @PostMapping("/approveAll")
+    public void approveAll() throws ExecutionException, InterruptedException {
+        withdrawApplyService.approveAll();
+    }
+}
+

+ 1 - 0
src/main/resources/genjson/WithdrawApply.json

@@ -0,0 +1 @@
+{"tableName":"WithdrawApply","className":"WithdrawApply","remark":"提现申请","genTable":true,"genClass":true,"genList":true,"genForm":true,"genRouter":true,"javaPath":"/Users/drew/Projects/Java/raex_back/src/main/java/com/izouma/nineth","viewPath":"/Users/drew/Projects/Java/raex_back/src/main/vue/src/views","routerPath":"/Users/drew/Projects/Java/raex_back/src/main/vue/src","resourcesPath":"/Users/drew/Projects/Java/raex_back/src/main/resources","dataBaseType":"Mysql","fields":[{"name":"userId","modelName":"userId","remark":"userId","showInList":true,"showInForm":true,"formType":"number"},{"name":"amount","modelName":"amount","remark":"amount","showInList":true,"showInForm":true,"formType":"number"},{"name":"status","modelName":"status","remark":"status","showInList":true,"showInForm":true,"formType":"select","apiFlag":"1","optionsValue":"[{\"label\":\"PENDING\",\"value\":\"PENDING\"},{\"label\":\"SUCCESS\",\"value\":\"SUCCESS\"},{\"label\":\"FAIL\",\"value\":\"FAIL\"}]"},{"name":"reason","modelName":"reason","remark":"reason","showInList":true,"showInForm":true,"formType":"singleLineText"}],"readTable":false,"dataSourceCode":"dataSource","genJson":"","subtables":[],"update":false,"basePackage":"com.izouma.nineth","tablePackage":"com.izouma.nineth.domain.WithdrawApply"}

+ 53 - 18
src/main/vue/src/router.js

@@ -621,7 +621,7 @@ const router = new Router({
                     component: () =>
                         import(
                             /* webpackChunkName: "collectionPendingList" */ '@/views/company/CollectionPendingList.vue'
-                        ),
+                            ),
                     meta: {
                         title: '申请中藏品'
                     }
@@ -650,7 +650,7 @@ const router = new Router({
                     component: () =>
                         import(
                             /* webpackChunkName: "companyCollectionEdit" */ '@/views/company/CompanyCollectionEdit.vue'
-                        ),
+                            ),
                     meta: {
                         title: '企业藏品编辑'
                     }
@@ -661,7 +661,7 @@ const router = new Router({
                     component: () =>
                         import(
                             /* webpackChunkName: "companyCollectionShelf" */ '@/views/company/CompanyCollectionShelf.vue'
-                        ),
+                            ),
                     meta: {
                         title: '企业藏品查看'
                     }
@@ -713,39 +713,74 @@ const router = new Router({
                     name: 'TagEdit',
                     component: () => import(/* webpackChunkName: "tagEdit" */ '@/views/TagEdit.vue'),
                     meta: {
-                       title: '标签编辑',
-                    },
+                        title: '标签编辑'
+                    }
                 },
                 {
                     path: '/tagList',
                     name: 'TagList',
                     component: () => import(/* webpackChunkName: "tagList" */ '@/views/TagList.vue'),
                     meta: {
-                       title: '标签',
-                    },
-               },
+                        title: '标签'
+                    }
+                },
                 {
                     path: '/priceListEdit',
                     name: 'PriceListEdit',
                     component: () => import(/* webpackChunkName: "priceListEdit" */ '@/views/PriceListEdit.vue'),
                     meta: {
-                       title: 'pricelist编辑',
-                    },
+                        title: 'pricelist编辑'
+                    }
                 },
                 {
                     path: '/priceListList',
                     name: 'PriceListList',
                     component: () => import(/* webpackChunkName: "priceListList" */ '@/views/PriceListList.vue'),
                     meta: {
-                       title: 'pricelist',
-                    },
-               },
+                        title: 'pricelist'
+                    }
+                },
+                {
+                    path: '/payMgmt',
+                    name: 'PayMgmt',
+                    component: () => import(/* webpackChunkName: "payMgmt" */ '@/views/PayMgmt.vue'),
+                    meta: {
+                        title: '支付管理'
+                    }
+                },
+                {
+                    path: '/withdrawApplyEdit',
+                    name: 'WithdrawApplyEdit',
+                    component: () =>
+                        import(/* webpackChunkName: "withdrawApplyEdit" */ '@/views/WithdrawApplyEdit.vue'),
+                    meta: {
+                        title: '提现申请编辑'
+                    }
+                },
+                {
+                    path: '/withdrawApplyList',
+                    name: 'WithdrawApplyList',
+                    component: () =>
+                        import(/* webpackChunkName: "withdrawApplyList" */ '@/views/WithdrawApplyList.vue'),
+                    meta: {
+                        title: '提现申请'
+                    }
+                },
+                {
+                    path: '/cache',
+                    name: 'cache',
+                    component: () =>
+                        import(/* webpackChunkName: "cache" */ '@/views/Cache.vue'),
+                    meta: {
+                        title: '缓存清理'
+                    }
+                },
                 {
                     path: '/messageEdit',
                     name: 'MessageEdit',
                     component: () => import(/* webpackChunkName: "messageEdit" */ '@/views/MessageEdit.vue'),
                     meta: {
-                       title: '留言编辑',
+                        title: '留言编辑',
                     },
                 },
                 {
@@ -753,9 +788,9 @@ const router = new Router({
                     name: 'MessageList',
                     component: () => import(/* webpackChunkName: "messageList" */ '@/views/MessageList.vue'),
                     meta: {
-                       title: '留言',
+                        title: '留言',
                     },
-               }
+                }
                 /**INSERT_LOCATION**/
             ]
         },
@@ -796,7 +831,7 @@ router.beforeEach((to, from, next) => {
         window.open(url);
         return;
     }
-    if (!store.state.userInfo && to.path !== '/login' && to.path!=='/photoProcessing') {
+    if (!store.state.userInfo && to.path !== '/login' && to.path !== '/photoProcessing') {
         http.axios
             .get('/user/my')
             .then(res => {
@@ -815,4 +850,4 @@ router.beforeEach((to, from, next) => {
     }
 });
 
-export default router;
+export default router;

+ 50 - 0
src/main/vue/src/views/Cache.vue

@@ -0,0 +1,50 @@
+<template>
+    <div class="edit-view">
+        <div class="edit-view__content-wrapper">
+            <div class="edit-view__content-section" v-loading="loading">
+                <el-input v-model="param" placeholder="输入参数"></el-input>
+                <el-button @click="clean('user', null, param)">user</el-button>
+                <el-button @click="clean('user')">all user</el-button>
+                <el-button @click="clean('userMy', null, param)">userMy</el-button>
+                <el-button @click="clean('userMy')">all userMy</el-button>
+                <el-button @click="clean('collection')">all collection</el-button>
+                <el-button @click="clean('recommend')">all recommend</el-button>
+            </div>
+        </div>
+    </div>
+</template>
+<script>
+export default {
+    data() {
+        return {
+            loading: false,
+            param: null
+        };
+    },
+    methods: {
+        clean(name, stringParam, longParam) {
+            this.loading = true;
+            this.$http
+                .post('/cache/clear', { name, stringParam, longParam })
+                .then(res => {
+                    this.loading = false;
+                    this.$message.success('成功');
+                })
+                .catch(e => {
+                    this.loading = false;
+                    this.$message.error(e.error || '失败');
+                });
+        }
+    }
+};
+</script>
+<style lang="less" scoped>
+.el-input {
+    width: 300px;
+    margin-right: 20px;
+    margin-bottom: 20px;
+}
+.el-button {
+    margin-bottom: 20px;
+}
+</style>

+ 263 - 0
src/main/vue/src/views/PayMgmt.vue

@@ -0,0 +1,263 @@
+<template>
+    <div class="edit-view">
+        <page-title>
+            <!-- <el-button @click="onDelete" :disabled="saving" type="danger" v-if="formData.id"> 删除 </el-button>
+            <el-button @click="onSave" :loading="saving" type="primary">保存</el-button> -->
+        </page-title>
+        <div class="edit-view__content-wrapper">
+            <div class="edit-view__content-section" @keyup.enter="query" v-loading="loading">
+                <h4 style="margin-top: 0">订单查询</h4>
+                <el-input v-model="orderId" placeholder="输入订单号" style="width: 350px">
+                    <el-button slot="append" icon="el-icon-search" @click="query"></el-button>
+                </el-input>
+
+                <div v-if="info && info.exist" class="detail">
+                    <div class="items">
+                        <div class="row">
+                            <div class="item-wrapper">
+                                <div class="item">
+                                    <div class="label">支付渠道</div>
+                                    <div class="value">{{ payChannel[info.channel] }}</div>
+                                </div>
+                            </div>
+                            <div class="item-wrapper">
+                                <div class="item">
+                                    <div class="label">金额</div>
+                                    <div class="value">{{ info.amount }}</div>
+                                </div>
+                            </div>
+                            <div class="item-wrapper">
+                                <div class="item">
+                                    <div class="label">状态</div>
+                                    <div class="value">{{ payStatus[info.status] }}</div>
+                                </div>
+                            </div>
+                        </div>
+                        <div class="row">
+                            <div class="item-wrapper">
+                                <div class="item">
+                                    <div class="label">交易ID</div>
+                                    <div class="value">{{ info.transactionId }}</div>
+                                </div>
+                            </div>
+                            <div class="item-wrapper">
+                                <div class="item">
+                                    <div class="label">支付时间</div>
+                                    <div class="value">{{ info.payTime }}</div>
+                                </div>
+                            </div>
+                            <div class="item-wrapper">
+                                <div class="item">
+                                    <div class="label">备注</div>
+                                    <div class="value">{{ info.msg }}</div>
+                                </div>
+                            </div>
+                        </div>
+                        <div class="row"></div>
+                        <div class="item-wrapper"></div>
+                    </div>
+                    <el-button @click="refund" style="margin-top: 20px" type="warning">申请退款</el-button>
+                </div>
+            </div>
+            <div class="edit-view__content-section">
+                <h4 style="margin-top: 0">测试支付</h4>
+                <el-select v-model="testPay.channel" class="test-pay-form-item">
+                    <el-option v-for="(item, key) in payChannel" :label="item" :value="key" :key="key"></el-option>
+                </el-select>
+                <el-input v-model="testPay.orderId" placeholder="订单ID" class="test-pay-form-item">
+                    <el-button
+                        icon="el-icon-refresh"
+                        slot="append"
+                        @click="testPay.orderId = 'TEST' + new Date().getTime()"
+                    ></el-button>
+                </el-input>
+                <el-input
+                    v-model="testPay.bindCardId"
+                    class="test-pay-form-item"
+                    placeholder="绑卡ID"
+                    v-if="testPay.channel === 'payEase'"
+                ></el-input>
+                <el-button @click="pay" class="test-pay-form-item" type="primary" plain :loading="testPayLoading">
+                    发起支付
+                </el-button>
+                <el-input
+                    placeholder="验证码"
+                    class="test-pay-form-item"
+                    style="width: 100px"
+                    v-if="testPay.channel === 'payEase'"
+                ></el-input>
+                <el-button class="test-pay-form-item" type="primary" plain v-if="testPay.channel === 'payEase'">
+                    确认支付
+                </el-button>
+            </div>
+        </div>
+    </div>
+</template>
+<script>
+import VueQrcode from '@chenfengyuan/vue-qrcode';
+export default {
+    components: { VueQrcode },
+    data() {
+        return {
+            orderId: '',
+            info: null,
+            loading: false,
+            payStatus: {
+                SUCCESS: '支付成功',
+                PENDING: '等待支付',
+                FAIL: '支付失败',
+                CANCEL: '已取消',
+                REFUNDED: '已退款',
+                REFUNDING: '退款中'
+            },
+            payChannel: {
+                sandQuick: '衫德快捷',
+                sandPay: '衫德',
+                hmPay: '河马',
+                payEase: '首信易'
+            },
+            testPay: {
+                channel: '',
+                orderId: 'TEST' + new Date().getTime()
+            },
+            testPayLoading: false,
+            payRes: null,
+            qrCode: ''
+        };
+    },
+    methods: {
+        query() {
+            if (!this.orderId) {
+                this.$message.error('请输入订单号');
+                return;
+            }
+            this.info = null;
+            this.loading = true;
+            this.$http
+                .get('/payChannelMgmt/query', { orderId: this.orderId })
+                .then(res => {
+                    this.loading = false;
+                    if (res && res.exist) {
+                        this.info = res;
+                    } else {
+                        this.$message.error('订单不存在');
+                    }
+                })
+                .catch(e => {
+                    this.loading = false;
+                    this.$message.error(e.error || '查询失败');
+                });
+        },
+        refund() {
+            this.$confirm('是否申请退款' + this.info.amount + '元', '确认退款', { type: 'warning' })
+                .then(res => {
+                    this.loading = true;
+                    return this.$http.post('/payChannelMgmt/refund', {
+                        orderId: this.orderId,
+                        transactionId: this.info.transactionId,
+                        amount: this.info.amount,
+                        channel: this.info.channel
+                    });
+                })
+                .then(res => {
+                    this.loading = false;
+                    this.$message.success('退款成功');
+                    this.query();
+                })
+                .catch(e => {
+                    this.loading = false;
+                    if (e !== 'cancel') {
+                        this.$message.error(e.error);
+                    }
+                });
+        },
+        pay() {
+            if (!this.testPay.channel) {
+                this.$message.error('请先选择支付渠道');
+                return;
+            }
+            this.testPayLoading = true;
+            this.$http
+                .post('/payChannelMgmt/pay', this.testPay)
+                .then(res => {
+                    this.orderId = this.testPay.orderId;
+                    this.testPayLoading = false;
+                    if (this.testPay.channel === 'sandQuick') {
+                        var newWindow = window.open('about:blank', '_blank', 'scrollbars=yes,width=1050,height=600');
+                        newWindow.document.write(res);
+                    } else if (this.testPay.channel === 'sandPay' || this.testPay.channel === 'hmPay') {
+                        const h = this.$createElement;
+                        this.$msgbox({
+                            title: '支付宝扫码支付',
+                            message: h('div', { style: { textAlign: 'center' } }, [
+                                h(
+                                    'vue-qrcode',
+                                    {
+                                        props: {
+                                            options: { width: 300, margin: 2 },
+                                            value: res
+                                        }
+                                    },
+                                    []
+                                )
+                            ]),
+                            confirmButtonText: '确定',
+                            cancelButtonText: '取消'
+                        });
+                    }
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.testPayLoading = false;
+                    this.$message.error(e.error);
+                });
+        },
+        confirmPay() {}
+    }
+};
+</script>
+<style lang="less" scoped>
+.items {
+    font-size: 14px;
+    color: @text1;
+    margin-top: 20px;
+    border-top: 1px solid @border1;
+    border-left: 1px solid @border1;
+    font-size: 0;
+    .row {
+        .flex();
+    }
+    .item-wrapper {
+        display: inline-block;
+        flex-basis: 0;
+        flex-grow: 1;
+        .item {
+            .flex();
+            border-right: 1px solid @border1;
+            border-bottom: 1px solid @border1;
+            .label {
+                width: 80px;
+                line-height: 40px;
+                padding: 0 10px;
+                background: @bg;
+                font-size: 14px;
+            }
+            .value {
+                flex-grow: 1;
+                flex-basis: 0;
+                line-height: 40px;
+                padding: 0 10px;
+                font-size: 14px;
+                .ellipsisLn(1);
+            }
+        }
+    }
+}
+.test-pay-form-item {
+    margin-right: 20px;
+    margin-bottom: 20px;
+    &.el-input {
+        width: 300px;
+    }
+}
+</style>

+ 10 - 15
src/main/vue/src/views/SysConfigList.vue

@@ -14,7 +14,7 @@
             </el-button>
         </div>
         <el-table
-            :data="tableData"
+            :data="filterList"
             row-key="id"
             ref="table"
             header-row-class-name="table-header-row"
@@ -33,19 +33,7 @@
                 </template>
             </el-table-column>
         </el-table>
-        <div class="pagination-wrapper">
-            <el-pagination
-                background
-                @size-change="onSizeChange"
-                @current-change="onCurrentChange"
-                :current-page="page"
-                :page-sizes="[10, 20, 30, 40, 50]"
-                :page-size="pageSize"
-                layout="total, sizes, prev, pager, next, jumper"
-                :total="totalElements"
-            >
-            </el-pagination>
-        </div>
+        <div class="pagination-wrapper"></div>
 
         <el-dialog :visible.sync="showDialog" width="500px" title="编辑设置" :close-on-click-modal="false">
             <el-form :model="formData" :rules="rules" ref="form" label-width="52px" label-position="right" size="small">
@@ -230,11 +218,18 @@ export default {
     computed: {
         selection() {
             return this.$refs.table.selection.map(i => i.id);
+        },
+        filterList() {
+            if (this.search) {
+                return this.tableData.filter(i => i.name.indexOf(this.search) > -1 || i.desc.indexOf(this.search) > -1);
+            } else {
+                return this.tableData;
+            }
         }
     },
     methods: {
         beforeGetData() {
-            return { search: this.search, sort: 'createdAt,desc' };
+            return { sort: 'createdAt,desc', size: 10000 };
         },
         toggleMultipleMode(multipleMode) {
             this.multipleMode = multipleMode;

+ 117 - 0
src/main/vue/src/views/WithdrawApplyEdit.vue

@@ -0,0 +1,117 @@
+<template>
+    <div class="edit-view">
+        <page-title>
+            <el-button @click="$router.go(-1)" :disabled="saving">取消</el-button>
+            <el-button @click="onDelete" :disabled="saving" type="danger" v-if="formData.id">
+                删除
+            </el-button>
+            <el-button @click="onSave" :loading="saving" type="primary">保存</el-button>
+        </page-title>
+        <div class="edit-view__content-wrapper">
+            <div class="edit-view__content-section">
+                <el-form :model="formData" :rules="rules" ref="form" label-width="76px" label-position="right"
+                         size="small"
+                         style="max-width: 500px;">
+                        <el-form-item prop="userId" label="userId">
+                                    <el-input-number type="number" v-model="formData.userId"></el-input-number>
+                        </el-form-item>
+                        <el-form-item prop="amount" label="amount">
+                                    <el-input-number type="number" v-model="formData.amount"></el-input-number>
+                        </el-form-item>
+                        <el-form-item prop="status" label="status">
+                                    <el-select v-model="formData.status" clearable filterable placeholder="请选择">
+                                        <el-option
+                                                v-for="item in statusOptions"
+                                                :key="item.value"
+                                                :label="item.label"
+                                                :value="item.value">
+                                        </el-option>
+                                    </el-select>
+                        </el-form-item>
+                        <el-form-item prop="reason" label="reason">
+                                    <el-input v-model="formData.reason"></el-input>
+                        </el-form-item>
+                    <el-form-item class="form-submit">
+                        <el-button @click="onSave" :loading="saving" type="primary">
+                            保存
+                        </el-button>
+                        <el-button @click="onDelete" :disabled="saving" type="danger" v-if="formData.id">
+                            删除
+                        </el-button>
+                        <el-button @click="$router.go(-1)" :disabled="saving">取消</el-button>
+                    </el-form-item>
+                </el-form>
+            </div>
+        </div>
+    </div>
+</template>
+<script>
+    export default {
+        name: 'WithdrawApplyEdit',
+        created() {
+            if (this.$route.query.id) {
+                this.$http
+                    .get('withdrawApply/get/' + this.$route.query.id)
+                    .then(res => {
+                        this.formData = res;
+                    })
+                    .catch(e => {
+                        console.log(e);
+                        this.$message.error(e.error);
+                    });
+            }
+        },
+        data() {
+            return {
+                saving: false,
+                formData: {
+                },
+                rules: {
+                },
+                statusOptions: [{"label":"PENDING","value":"PENDING"},{"label":"SUCCESS","value":"SUCCESS"},{"label":"FAIL","value":"FAIL"}],
+            }
+        },
+        methods: {
+            onSave() {
+                this.$refs.form.validate((valid) => {
+                    if (valid) {
+                        this.submit();
+                    } else {
+                        return false;
+                    }
+                });
+            },
+            submit() {
+                let data = {...this.formData};
+
+                this.saving = true;
+                this.$http
+                    .post('/withdrawApply/save', data, {body: 'json'})
+                    .then(res => {
+                        this.saving = false;
+                        this.$message.success('成功');
+                        this.$router.go(-1);
+                    })
+                    .catch(e => {
+                        console.log(e);
+                        this.saving = false;
+                        this.$message.error(e.error);
+                    });
+            },
+            onDelete() {
+                this.$confirm('删除将无法恢复,确认要删除么?', '警告', {type: 'error'}).then(() => {
+                    return this.$http.post(`/withdrawApply/del/${this.formData.id}`)
+                }).then(() => {
+                    this.$message.success('删除成功');
+                    this.$router.go(-1);
+                }).catch(e => {
+                    if (e !== 'cancel') {
+                        console.log(e);
+                        this.$message.error((e || {}).error || '删除失败');
+                    }
+                })
+            },
+        }
+    }
+</script>
+<style lang="less" scoped></style>

+ 212 - 0
src/main/vue/src/views/WithdrawApplyList.vue

@@ -0,0 +1,212 @@
+<template>
+    <div class="list-view">
+        <page-title>
+            <el-button
+                @click="download"
+                icon="el-icon-upload2"
+                :loading="downloading"
+                :disabled="fetchingData"
+                class="filter-item"
+            >
+                导出
+            </el-button>
+        </page-title>
+        <div class="filters-container">
+            <template v-if="$store.state.userInfo && $store.state.userInfo.username === 'xiong'">
+                <el-button class="filter-item" type="primary" @click="approveAll" size="mini"> 全部通过 </el-button>
+            </template>
+            <el-input
+                placeholder="搜索..."
+                v-model="search"
+                clearable
+                class="filter-item search"
+                @keyup.enter.native="getData"
+            >
+                <el-button @click="getData" slot="append" icon="el-icon-search"> </el-button>
+            </el-input>
+        </div>
+        <el-table
+            :data="tableData"
+            row-key="id"
+            ref="table"
+            header-row-class-name="table-header-row"
+            header-cell-class-name="table-header-cell"
+            row-class-name="table-row"
+            cell-class-name="table-cell"
+            :height="tableHeight"
+            v-loading="fetchingData"
+        >
+            <el-table-column v-if="multipleMode" align="center" type="selection" width="50"> </el-table-column>
+            <el-table-column prop="id" label="ID" width="100"> </el-table-column>
+            <el-table-column prop="createdAt" label="申请时间" min-width="120"></el-table-column>
+            <el-table-column prop="userId" label="用户ID"> </el-table-column>
+            <el-table-column prop="amount" label="金额"> </el-table-column>
+            <el-table-column prop="status" label="状态" :formatter="statusFormatter"> </el-table-column>
+            <el-table-column prop="finishTime" label="处理时间" min-width="120"></el-table-column>
+            <el-table-column prop="reason" label="原因"> </el-table-column>
+            <el-table-column label="操作" align="center" fixed="right" width="150">
+                <template slot-scope="{ row }" v-if="row.status === 'PENDING'">
+                    <el-button @click="finish(row, true)" type="primary" size="mini" plain>通过</el-button>
+                    <el-button @click="finish(row, false)" type="danger" size="mini" plain>拒绝</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+        <div class="pagination-wrapper">
+            <!-- <div class="multiple-mode-wrapper">
+                <el-button v-if="!multipleMode" @click="toggleMultipleMode(true)">批量编辑</el-button>
+                <el-button-group v-else>
+                    <el-button @click="operation1">批量操作1</el-button>
+                    <el-button @click="operation2">批量操作2</el-button>
+                    <el-button @click="toggleMultipleMode(false)">取消</el-button>
+                </el-button-group>
+            </div> -->
+            <el-pagination
+                background
+                @size-change="onSizeChange"
+                @current-change="onCurrentChange"
+                :current-page="page"
+                :page-sizes="[10, 20, 30, 40, 50]"
+                :page-size="pageSize"
+                layout="total, sizes, prev, pager, next, jumper"
+                :total="totalElements"
+            >
+            </el-pagination>
+        </div>
+    </div>
+</template>
+<script>
+import { mapState } from 'vuex';
+import pageableTable from '@/mixins/pageableTable';
+
+export default {
+    name: 'WithdrawApplyList',
+    mixins: [pageableTable],
+    data() {
+        return {
+            multipleMode: false,
+            search: '',
+            url: '/withdrawApply/all',
+            downloading: false,
+            statusOptions: [
+                { label: '待提现', value: 'PENDING' },
+                { label: '成功', value: 'SUCCESS' },
+                { label: '失败', value: 'FAIL' }
+            ]
+        };
+    },
+    computed: {
+        selection() {
+            return this.$refs.table.selection.map(i => i.id);
+        }
+    },
+    methods: {
+        statusFormatter(row, column, cellValue, index) {
+            let selectedOption = this.statusOptions.find(i => i.value === cellValue);
+            if (selectedOption) {
+                return selectedOption.label;
+            }
+            return '';
+        },
+        beforeGetData() {
+            return { search: this.search, query: { del: false } };
+        },
+        toggleMultipleMode(multipleMode) {
+            this.multipleMode = multipleMode;
+            if (!multipleMode) {
+                this.$refs.table.clearSelection();
+            }
+        },
+        addRow() {
+            this.$router.push({
+                path: '/withdrawApplyEdit',
+                query: {
+                    ...this.$route.query
+                }
+            });
+        },
+        editRow(row) {
+            this.$router.push({
+                path: '/withdrawApplyEdit',
+                query: {
+                    id: row.id
+                }
+            });
+        },
+        download() {
+            this.downloading = true;
+            this.$axios
+                .get('/withdrawApply/excel', {
+                    responseType: 'blob',
+                    params: { size: 10000 }
+                })
+                .then(res => {
+                    console.log(res);
+                    this.downloading = false;
+                    const downloadUrl = window.URL.createObjectURL(new Blob([res.data]));
+                    const link = document.createElement('a');
+                    link.href = downloadUrl;
+                    link.setAttribute('download', res.headers['content-disposition'].split('filename=')[1]);
+                    document.body.appendChild(link);
+                    link.click();
+                    link.remove();
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.downloading = false;
+                    this.$message.error(e.error);
+                });
+        },
+        operation1() {
+            this.$notify({
+                title: '提示',
+                message: this.selection
+            });
+        },
+        operation2() {
+            this.$message('操作2');
+        },
+        deleteRow(row) {
+            this.$alert('删除将无法恢复,确认要删除么?', '警告', { type: 'error' })
+                .then(() => {
+                    return this.$http.post(`/withdrawApply/del/${row.id}`);
+                })
+                .then(() => {
+                    this.$message.success('删除成功');
+                    this.getData();
+                })
+                .catch(e => {
+                    if (e !== 'cancel') {
+                        this.$message.error(e.error);
+                    }
+                });
+        },
+        finish(row, approve) {
+            this.$confirm(`是否${approve ? '通过' : '拒绝'}提现申请?`)
+                .then(res => {
+                    return this.$http.post('/withdrawApply/finish', { id: row.id, approve });
+                })
+                .then(res => {
+                    this.$message.success('操作成功');
+                    this.getData();
+                })
+                .catch(e => {
+                    if ('cancel' !== e) {
+                        this.$message.error(e.error || '操作失败');
+                    }
+                });
+        },
+        approveAll() {
+            this.$http
+                .post('/withdrawApply/approveAll')
+                .then(res => {
+                    this.$message.success('操作成功');
+                })
+                .catch(e => {
+                    this.$message.error(e.error || '操作失败');
+                });
+        }
+    }
+};
+</script>
+<style lang="less" scoped>
+</style>

+ 23 - 0
src/test/java/com/izouma/nineth/Bucket4jTest.java

@@ -0,0 +1,23 @@
+package com.izouma.nineth;
+
+import io.github.bucket4j.*;
+import io.github.bucket4j.distributed.proxy.ProxyManager;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.time.Duration;
+
+public class Bucket4jTest extends ApplicationTests {
+    @Autowired
+    private ProxyManager<String> buckets;
+
+    @Test
+    public void test() {
+        Bucket bucket =  buckets.builder().build("test", () -> (BucketConfiguration.builder()
+                .addLimit(Bandwidth.classic(10, Refill.intervally(10, Duration.ofMinutes(10))))
+                .build()));
+        for (int i = 0; i < 20; i++) {
+            System.out.println(bucket.tryConsume(1));
+        }
+    }
+}

+ 16 - 0
src/test/java/com/izouma/nineth/CommonTest.java

@@ -16,6 +16,7 @@ import com.izouma.nineth.domain.*;
 import com.izouma.nineth.dto.PageQuery;
 import com.izouma.nineth.dto.SandPaySettle;
 import com.izouma.nineth.dto.UserWithdraw;
+import com.izouma.nineth.service.IdentityAuthService;
 import com.izouma.nineth.service.UserService;
 import com.izouma.nineth.utils.AESEncryptUtil;
 import com.izouma.nineth.utils.DateTimeUtils;
@@ -92,7 +93,10 @@ import java.security.spec.X509EncodedKeySpec;
 import java.text.DecimalFormat;
 import java.text.DecimalFormatSymbols;
 import java.text.NumberFormat;
+import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
 import java.util.List;
 import java.util.*;
 import java.util.concurrent.ExecutorService;
@@ -699,4 +703,16 @@ public class CommonTest {
     public void decrypt() throws Exception {
         System.out.println(AESEncryptUtil.decrypt("3BBC5669B95729554056D247338D3BAECA4483E429514227058BF3503F2D42A0"));
     }
+
+    @Test
+    public void auth() {
+        IdentityAuthService.validate("熊竹", "15077886171", "321002199408304611");
+        IdentityAuthService.validate("熊竹", "15077886171", "321002199408304614");
+    }
+
+    @Test
+    public void testAge() {
+        System.out.println(ChronoUnit.YEARS.between(LocalDate.parse("321002199408304614".substring(6, 14),
+                DateTimeFormatter.ofPattern("yyyyMMdd")), LocalDate.now()));
+    }
 }

+ 20 - 1
src/test/java/com/izouma/nineth/HMPayTest.java

@@ -125,7 +125,7 @@ public class HMPayTest {
         params.put("method", "trade.query");
         params.put("timestamp", DateTimeUtils.format(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss"));
         params.put("nonce", RandomStringUtils.randomAlphabetic(32));
-        params.put("biz_content", "{\"out_order_no\":\"967114410067890176\"}");
+        params.put("biz_content", "{\"out_order_no\":\"976535925569884160\"}");
         params.put("sign", getSign(params));
         String body = HttpRequest.post("https://hmpay.sandpay.com.cn/gateway/api")
                 .contentType("application/json")
@@ -160,4 +160,23 @@ public class HMPayTest {
         JSONObject data = res.getJSONObject("data");
         System.out.println(JSON.toJSONString(data, true));
     }
+
+    @Test
+    public void queryRefund() {
+        Map<String, String> params = new HashMap<>();
+        params.put("app_id", "664403000025502");
+        params.put("method", "trade.refund.query");
+        params.put("timestamp", DateTimeUtils.format(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss"));
+        params.put("nonce", RandomStringUtils.randomAlphabetic(32));
+        params.put("biz_content", "{\"out_order_no\":\"970096214148124672\",\"refund_request_no\":\"970096214148124672\"}");
+        params.put("sign", getSign(params));
+        String body = HttpRequest.post("https://hmpay.sandpay.com.cn/gateway/api")
+                .contentType("application/json")
+                .send(JSON.toJSONString(params)).body();
+        JSONObject res = JSON.parseObject(body);
+        System.out.println(JSON.toJSONString(res, true));
+
+        JSONObject data = res.getJSONObject("data");
+        System.out.println(JSON.toJSONString(data, true));
+    }
 }

+ 45 - 13
src/test/java/com/izouma/nineth/PayEaseTest.java

@@ -1,7 +1,6 @@
 package com.izouma.nineth;
 
 import com.alibaba.fastjson15.JSON;
-import com.alibaba.fastjson15.JSONArray;
 import com.alibaba.fastjson15.JSONObject;
 import com.alibaba.fastjson15.parser.Feature;
 import com.izouma.nineth.utils.SnowflakeIdWorker;
@@ -16,9 +15,13 @@ import com.upay.sdk.cashier.order.builder.ReceiptPaymentBuilder;
 import com.upay.sdk.entity.Payer;
 import com.upay.sdk.entity.ProductDetail;
 import com.upay.sdk.onlinepay.builder.OrderBuilder;
+import com.upay.sdk.onlinepay.builder.QueryBuilder;
+import com.upay.sdk.onlinepay.builder.RefundBuilder;
 import lombok.extern.slf4j.Slf4j;
 import org.junit.Test;
 
+import java.util.concurrent.atomic.AtomicInteger;
+
 @Slf4j
 public class PayEaseTest {
     String merchantId = "896593123";
@@ -36,15 +39,15 @@ public class PayEaseTest {
     @Test
     public void bindCard() {
         //商户会员id
-        String merchantUserId = "9850";
+        String merchantUserId = "1";
         //	银⾏卡号
-        String bankCardNumber = "6217001370031997059";
+        String bankCardNumber = "6222024301070380165";
         //预留⼿机号
-        String phoneNumber = "13365135976";
+        String phoneNumber = "15077886171";
         //	持卡⼈姓名
-        String name = "潘惠";
+        String name = "熊竹";
         //	身份证号
-        String idCardNum = "321081199602086329";
+        String idCardNum = "321002199408304614";
 
 
         BindCardBuilder builder = new BindCardBuilder(merchantId);
@@ -64,8 +67,8 @@ public class PayEaseTest {
 
     @Test
     public void bindCardConfirm() {
-        String bindCardId = "20220519093153711994335360860160";
-        String kaptchaCode = "366263";
+        String bindCardId = "20220524516958713695754308698112";
+        String kaptchaCode = "959522";
         String merchantUserId = "1";
 
         BindCardConfirmBuilder builder = new BindCardConfirmBuilder(merchantId);
@@ -117,9 +120,9 @@ public class PayEaseTest {
         String remark = "111";
 
         //商户会员Id
-        String merchantUserId = "134613";
+        String merchantUserId = "1";
         //绑卡Id
-        String bindCardId = "20220519032566712013088098238464";
+        String bindCardId = "20220524516958713695754308698112";
         //超时时间
         String timeout = "3";
 
@@ -153,9 +156,9 @@ public class PayEaseTest {
 
     @Test
     public void payConfirm() {
-        String requestId = "12345677";
-        String paymentOrderId = "4c4e5a40e54c48838cea6ab935fae917";
-        String kaptchaCode = "413237";
+        String requestId = "978683558270861312";
+        String paymentOrderId = "f03fc9a74ac3450292cdfc18f6723b9c";
+        String kaptchaCode = "478513";
         ReceiptPaymentBuilder builder = new ReceiptPaymentBuilder(merchantId);
         builder.setRequestId(requestId)
                 .setPaymentOrderId(paymentOrderId)
@@ -163,4 +166,33 @@ public class PayEaseTest {
 
         JSONObject response = request(ConfigurationUtils.getCashierReceiptPaymentUrl(), builder.bothEncryptBuild());
     }
+
+    @Test
+    public void refund() {
+        String orderId = "7f159751086347cbb728e7fd0de8ad1c";
+        String amount = "1";
+        String notifyUrl = "https://www.raex.vip/payease/notify/order";
+        RefundBuilder builder = new RefundBuilder(merchantId);
+        builder.setRequestId("978601560772706304")
+                .setAmount(amount)
+                .setOrderId(orderId)
+                .setNotifyUrl(notifyUrl);
+        JSONObject response = request(ConfigurationUtils.getOnlinePayRefundUrl(), builder.bothEncryptBuild());
+    }
+
+    @Test
+    public void query() {
+        String requestId = "978601560772706304";
+        QueryBuilder builder = new QueryBuilder(merchantId);
+        builder.setRequestId(requestId);
+        JSONObject response = request(ConfigurationUtils.getOnlinePayQueryUrl(), builder.bothEncryptBuild());
+    }
+
+    @Test
+    public void queryRefund() {
+        String requestId = "978601560772706304";
+        RefundBuilder builder = new RefundBuilder(merchantId);
+        builder.setRequestId(requestId);
+        JSONObject response = request(ConfigurationUtils.getOnlinePayRefundQueryUrl(), builder.bothEncryptBuild());
+    }
 }

+ 59 - 0
src/test/java/com/izouma/nineth/PayOrderTest.java

@@ -0,0 +1,59 @@
+package com.izouma.nineth;
+
+
+import com.izouma.nineth.config.Constants;
+import com.izouma.nineth.dto.PayQuery;
+import com.izouma.nineth.service.OrderPayService;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+public class PayOrderTest extends ApplicationTests {
+    @Autowired
+    private OrderPayService orderPayService;
+
+    @Test
+    public void refund() {
+        for (int orderId : new int[]{
+                7587002,
+                7587102,
+                7587101,
+                7587577,
+                7587001,
+                7587100,
+                7587000,
+                7587574,
+                7586999,
+                7587573,
+                7586998,
+                7586997,
+                7587571,
+                7587569,
+                7587570,
+                7587568,
+                7586996,
+                7586995,
+                7586993,
+                7587567,
+                7586916,
+                7587096,
+                7586915,
+                7586992,
+                7587094,
+                7587091,
+                7587093,
+                7587090,
+                7586991,
+                7586914,
+                7586913,
+                7587089,
+                7586990,
+                7587088,
+                7586989
+        }) {
+            PayQuery query = orderPayService.query(orderId + "");
+            if (query.isExist()) {
+                orderPayService.refund(orderId + "", query.getTransactionId(), query.getAmount(), query.getChannel());
+            }
+        }
+    }
+}

+ 46 - 0
src/test/java/com/izouma/nineth/TestRedissonLock.java

@@ -0,0 +1,46 @@
+package com.izouma.nineth;
+
+import org.junit.Test;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.util.concurrent.TimeUnit;
+
+public class TestRedissonLock extends ApplicationTests {
+
+    @Autowired
+    private RedissonClient redissonClient;
+
+    @Test
+    public void testLock() throws InterruptedException {
+        RLock lock = redissonClient.getLock("testLock");
+        Thread t1 = new Thread(() -> {
+            lock.lock(10, TimeUnit.SECONDS);
+            System.out.println("线程1获取锁");
+            try {
+                Thread.sleep(2000);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+            lock.unlock();
+            System.out.println("线程1释放锁");
+        });
+        Thread t2 = new Thread(() -> {
+            lock.lock(10, TimeUnit.SECONDS);
+            System.out.println("线程2获取锁");
+            try {
+                Thread.sleep(2000);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+            lock.unlock();
+            System.out.println("线程2释放锁");
+        });
+
+        t1.start();
+        t2.start();
+        t1.join();
+        t2.join();
+    }
+}

+ 84 - 18
src/test/java/com/izouma/nineth/repo/UserPropertyRepoTest.java

@@ -11,7 +11,6 @@ import com.izouma.nineth.utils.excel.UploadDataListener;
 import org.junit.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 
-import java.io.DataOutput;
 import java.io.File;
 import java.util.*;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -170,23 +169,24 @@ public class UserPropertyRepoTest extends ApplicationTests {
 
     @Test
     public void setById() {
-        File file = new File("/Users/qiufangchao/Desktop/VIP.xlsx");
+        userPropertyRepo.deleteAll();
+        File file = new File("/Users/sunnianwen/Desktop/all1.xlsx");
         UploadDataListener<AirDropExcelDTO> listener = new UploadDataListener<>();
         List<AirDropExcelDTO> dtos = EasyExcel.read(file, AirDropExcelDTO.class, listener)
                 .sheet()
                 .doReadSync();
-        Set<String> userIds = dtos.stream()
-                .map(AirDropExcelDTO::getPhone)
-                .collect(Collectors.toSet());
-        System.out.println(userIds);
-        userIds.forEach(id -> {
+//        Set<String> userIds = dtos.stream()
+//                .map(AirDropExcelDTO::getPhone)
+//                .collect(Collectors.toSet());
+//        System.out.println(userIds);
+        dtos.forEach(id -> {
 
-            UserProperty userProperty = userPropertyRepo.findById(Long.parseLong(id))
-                    .orElse(new UserProperty(Long.parseLong(id), 0));
-            userProperty.setMaxCount(userProperty.getMaxCount() + 1);
-            if (userProperty.getMaxCount() > 6) {
-                userProperty.setMaxCount(6);
-            }
+            UserProperty userProperty = userPropertyRepo.findById(Long.parseLong(id.getPhone()))
+                    .orElse(new UserProperty(Long.parseLong(id.getPhone()), 0));
+            userProperty.setMaxCount(Integer.parseInt(id.getName()));
+//            if (userProperty.getMaxCount() > 6) {
+//                userProperty.setMaxCount(6);
+//            }
             userPropertyRepo.save(userProperty);
         });
     }
@@ -226,14 +226,80 @@ public class UserPropertyRepoTest extends ApplicationTests {
     }
 
     @Test
-    public void test(){
+    public void test() {
         Iterable<UserProperty> all = userPropertyRepo.findAll();
+//        System.out.println(all);
+        AtomicInteger sum = new AtomicInteger();
         all.forEach(p -> {
-            if (p.getMaxCount() > 1) {
-                p.setMaxCount(p.getMaxCount() - 1);
-                userPropertyRepo.save(p);
-            }
+            sum.getAndIncrement();
         });
+        System.out.println(sum.get());
+    }
+
+    @Test
+    public void heji() {
+        Map<String, Integer> map = new HashMap<>();
+        UploadDataListener<AirDropExcelDTO> listener = new UploadDataListener<>();
+
+//        File fileLei = new File("/Users/sunnianwen/Desktop/lei.xlsx");
+//        List<AirDropExcelDTO> leis = EasyExcel.read(fileLei, AirDropExcelDTO.class, listener)
+//                .sheet()
+//                .doReadSync();
+//        leis.forEach(dto -> map.merge(dto.getPhone(), Integer.parseInt(dto.getName()), Integer::sum));
+//
+//        File fileTuan = new File("/Users/sunnianwen/Desktop/tuan.xlsx");
+//        List<AirDropExcelDTO> tuans = EasyExcel.read(fileTuan, AirDropExcelDTO.class, listener)
+//                .sheet()
+//                .doReadSync();
+//        tuans.forEach(dto -> map.merge(dto.getPhone(), Integer.parseInt(dto.getName()), Integer::sum));
+//
+//        File fileDi = new File("/Users/sunnianwen/Desktop/di.xlsx");
+//        List<AirDropExcelDTO> dis = EasyExcel.read(fileDi, AirDropExcelDTO.class, listener)
+//                .sheet()
+//                .doReadSync();
+//        dis.forEach(dto -> map.merge(dto.getPhone(), Integer.parseInt(dto.getName()), Integer::sum));
+//
+//        File fileTop1 = new File("/Users/sunnianwen/Desktop/top20_1.xlsx");
+//        List<AirDropExcelDTO> top1s = EasyExcel.read(fileTop1, AirDropExcelDTO.class, listener)
+//                .sheet()
+//                .doReadSync();
+//        top1s.forEach(dto -> map.merge(dto.getPhone(), Integer.parseInt(dto.getName()), Integer::sum));
+//
+//        File fileTop2 = new File("/Users/sunnianwen/Desktop/top20_2.xlsx");
+//        List<AirDropExcelDTO> top2s = EasyExcel.read(fileTop2, AirDropExcelDTO.class, listener)
+//                .sheet()
+//                .doReadSync();
+//        top2s.forEach(dto -> map.merge(dto.getPhone(), Integer.parseInt(dto.getName()), Integer::sum));
+//
+//        File vipLou = new File("/Users/sunnianwen/Desktop/vip_man_lou.xlsx");
+//        List<AirDropExcelDTO> lou1s = EasyExcel.read(vipLou, AirDropExcelDTO.class, listener)
+//                .sheet()
+//                .doReadSync();
+//        lou1s.forEach(dto -> map.merge(dto.getPhone(), Integer.parseInt(dto.getName()), Integer::sum));
+//
+//        File baiLou = new File("/Users/sunnianwen/Desktop/bai_lou.xlsx");
+//        List<AirDropExcelDTO> lou2s = EasyExcel.read(baiLou, AirDropExcelDTO.class, listener)
+//                .sheet()
+//                .doReadSync();
+//        lou2s.forEach(dto -> map.merge(dto.getPhone(), Integer.parseInt(dto.getName()), Integer::sum));
+//
+//        File vip = new File("/Users/sunnianwen/Desktop/vip.xlsx");
+//        List<AirDropExcelDTO> vips = EasyExcel.read(vip, AirDropExcelDTO.class, listener)
+//                .sheet()
+//                .doReadSync();
+//        vips.forEach(dto -> map.merge(dto.getPhone(), Integer.parseInt(dto.getName()), Integer::sum));
+
+        File all = new File("/Users/sunnianwen/Desktop/all1.xlsx");
+        List<AirDropExcelDTO> alls = EasyExcel.read(all, AirDropExcelDTO.class, listener)
+                .sheet()
+                .doReadSync();
+        alls.forEach(dto -> map.merge(dto.getPhone(), Integer.parseInt(dto.getName()), Integer::sum));
+
+        map.forEach((key, value) -> System.out.println(key + "," + value));
+        AtomicInteger sum = new AtomicInteger();
+        map.forEach((key, value) -> sum.addAndGet(value));
+        System.out.println(sum.get());
     }
 
+
 }

+ 53 - 1
src/test/java/com/izouma/nineth/service/AirDropServiceTest.java

@@ -2,18 +2,37 @@ package com.izouma.nineth.service;
 
 import com.alibaba.excel.EasyExcel;
 import com.izouma.nineth.ApplicationTests;
+import com.izouma.nineth.TokenHistory;
+import com.izouma.nineth.domain.AirDrop;
+import com.izouma.nineth.domain.Asset;
+import com.izouma.nineth.domain.DropTarget;
 import com.izouma.nineth.dto.AirDropExcelDTO;
+import com.izouma.nineth.enums.AssetStatus;
+import com.izouma.nineth.exception.BusinessException;
+import com.izouma.nineth.repo.AirDropRepo;
+import com.izouma.nineth.repo.AssetRepo;
+import com.izouma.nineth.repo.TokenHistoryRepo;
 import com.izouma.nineth.utils.excel.UploadDataListener;
 import org.junit.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 
 import java.io.File;
 import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.List;
 
 public class AirDropServiceTest extends ApplicationTests {
     @Autowired
-    AirDropService airDropService;
+    AirDropService    airDropService;
+    @Autowired
+    AirDropRepo       airDropRepo;
+    @Autowired
+    AssetRepo         assetRepo;
+    @Autowired
+    TokenHistoryRepo  tokenHistoryRepo;
+    @Autowired
+    CollectionService collectionService;
 
     @Test
     public void drop() {
@@ -34,4 +53,37 @@ public class AirDropServiceTest extends ApplicationTests {
 
         });
     }
+
+    @Test
+    public void cancel() {
+        AirDrop airDrop = airDropRepo.findById(7607018L).orElseThrow(new BusinessException(""));
+        List<Asset> resultA = new ArrayList<>();
+        List<TokenHistory> resultT = new ArrayList<>();
+        int total = 0;
+        for (DropTarget target : airDrop.getTargets()) {
+            List<Asset> assets = assetRepo.findAllByUserIdAndCollectionIdAndStatus(target.getUserId(), airDrop.getCollectionId(), AssetStatus.NORMAL);
+            assets.sort((a, b) -> b.getId().compareTo(a.getId()));
+            if (target.getNum() > assets.size()) {
+                throw new BusinessException(target.getUserId() + ":" + target.getNum() + ":" + assets.size());
+            }
+            for (int i = 0; i < target.getNum(); i++) {
+                Asset asset = assets.get(i);
+                resultA.add(asset);
+                TokenHistory tokens = tokenHistoryRepo.findByToUserIdAndTokenId(target.getUserId(), asset.getTokenId())
+                        .orElseThrow(new BusinessException(""));
+                resultT.add(tokens);
+                total++;
+            }
+
+        }
+
+//        System.out.println(total);
+//        System.out.println(airDrop.getTargets().stream().mapToInt(DropTarget::getNum).sum());
+//
+//        assetRepo.deleteAll(resultA);
+//        tokenHistoryRepo.deleteAll(resultT);
+//
+//        collectionService.increaseStock(airDrop.getCollectionId(), total);
+//        collectionService.decreaseSale(airDrop.getCollectionId(), total);
+    }
 }

+ 159 - 71
src/test/java/com/izouma/nineth/service/MintOrderServiceTest.java

@@ -1,16 +1,20 @@
 package com.izouma.nineth.service;
 
 import com.izouma.nineth.ApplicationTests;
+import com.izouma.nineth.config.Constants;
 import com.izouma.nineth.domain.Asset;
 import com.izouma.nineth.domain.MintMaterial;
 import com.izouma.nineth.domain.MintOrder;
 import com.izouma.nineth.domain.User;
+import com.izouma.nineth.dto.PayQuery;
 import com.izouma.nineth.enums.AssetStatus;
 import com.izouma.nineth.enums.MintOrderStatus;
 import com.izouma.nineth.enums.PayMethod;
+import com.izouma.nineth.event.OrderNotifyEvent;
 import com.izouma.nineth.repo.*;
 import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 
@@ -36,6 +40,8 @@ class MintOrderServiceTest extends ApplicationTests {
     private AssetRepo        assetRepo;
     @Autowired
     private OrderPayService  orderPayService;
+    @Autowired
+    private RocketMQTemplate rocketMQTemplate;
 
     @Test
     void exchange() throws InterruptedException {
@@ -43,11 +49,6 @@ class MintOrderServiceTest extends ApplicationTests {
         Thread.sleep(1000);
     }
 
-    @Test
-    void test() {
-        mintOrderService.batchCancel();
-    }
-
     @Test
     public void test1() {
         User user = userRepo.findByIdAndDelFalse(9972L).orElse(null);
@@ -77,86 +78,173 @@ class MintOrderServiceTest extends ApplicationTests {
     @Test
     public void fixDuplicated() throws ExecutionException, InterruptedException {
         new ForkJoinPool(3000).submit(() -> {
-            List<MintOrder> mintOrders = mintOrderRepo.findByStatusAndCreatedAtAfter(MintOrderStatus.AIR_DROP, LocalDateTime.of(2022, 5, 14, 0, 0, 0));
+            List<MintOrder> mintOrders = mintOrderRepo.findByStatusAndCreatedAtAfter(MintOrderStatus.AIR_DROP, LocalDateTime.of(2022, 5, 20, 0, 0, 0));
             List<MintOrder> errOrders = new ArrayList<>();
             mintOrders.parallelStream().forEach(mintOrder -> {
                 mintOrder.setMaterial(mintMaterialRepo.findAllByOrderIdAndDelFalse(mintOrder.getId()));
             });
-            mintOrders.parallelStream().collect(Collectors.groupingBy(MintOrder::getUserId)).forEach((userId, orders) -> {
-                Iterator<MintOrder> iterator = orders.iterator();
-                List<MintOrder> userErrOrders = new ArrayList<>();
-                while (iterator.hasNext()) {
-                    MintOrder next = iterator.next();
-                    List<Long> assetIds = next.getMaterial().stream().map(MintMaterial::getAssetId).collect(Collectors.toList());
-                    for (MintOrder order : orders) {
-                        if (!order.getId().equals(next.getId())) {
-                            List<Long> assetIds1 = order.getMaterial().stream().map(MintMaterial::getAssetId).collect(Collectors.toList());
-                            if (CollectionUtils.intersection(assetIds, assetIds1).size() > 0) {
-                                iterator.remove();
-                                errOrders.add(next);
-                                userErrOrders.add(next);
-                                break;
+            mintOrders.parallelStream().collect(Collectors.groupingBy(MintOrder::getUserId))
+                    .forEach((userId, orders) -> {
+                        Iterator<MintOrder> iterator = orders.iterator();
+                        List<MintOrder> userErrOrders = new ArrayList<>();
+                        while (iterator.hasNext()) {
+                            MintOrder next = iterator.next();
+                            List<Long> assetIds = next.getMaterial().stream().map(MintMaterial::getAssetId)
+                                    .collect(Collectors.toList());
+                            for (MintOrder order : orders) {
+                                if (!order.getId().equals(next.getId())) {
+                                    List<Long> assetIds1 = order.getMaterial().stream().map(MintMaterial::getAssetId)
+                                            .collect(Collectors.toList());
+                                    if (CollectionUtils.intersection(assetIds, assetIds1).size() > 0) {
+                                        iterator.remove();
+                                        errOrders.add(next);
+                                        userErrOrders.add(next);
+                                        break;
+                                    }
+                                }
                             }
                         }
-                    }
-                }
-                if (userErrOrders.size() > 0) {
-                    Set<Long> errAssetIds = userErrOrders.stream().flatMap(mintOrder -> mintOrder.getMaterial().stream())
-                            .map(MintMaterial::getAssetId).collect(Collectors.toSet());
-                    Set<Long> orderAssetIds = orders.stream().flatMap(mintOrder -> mintOrder.getMaterial().stream())
-                            .map(MintMaterial::getAssetId).collect(Collectors.toSet());
-                    Collection toSetNormal = CollectionUtils.subtract(errAssetIds, orderAssetIds);
-                    System.out.println(toSetNormal);
-                    for (MintOrder userErrOrder : userErrOrders) {
-                        userErrOrder.setStatus(MintOrderStatus.CANCELLED);
-                        userErrOrder.setCancelTime(LocalDateTime.now());
-                        mintOrderRepo.save(userErrOrder);
-                        PayMethod payMethod = userErrOrder.getPayMethod();
-                        if (PayMethod.ALIPAY == payMethod) {
-                            if (StringUtils.length(userErrOrder.getTransactionId()) == 28) {
-                                payMethod = PayMethod.HMPAY;
-                            } else if (StringUtils.length(userErrOrder.getTransactionId()) == 30) {
-                                payMethod = PayMethod.SANDPAY;
+                        if (userErrOrders.size() > 0) {
+                            Set<Long> errAssetIds = userErrOrders.stream()
+                                    .flatMap(mintOrder -> mintOrder.getMaterial().stream())
+                                    .map(MintMaterial::getAssetId).collect(Collectors.toSet());
+                            Set<Long> orderAssetIds = orders.stream()
+                                    .flatMap(mintOrder -> mintOrder.getMaterial().stream())
+                                    .map(MintMaterial::getAssetId).collect(Collectors.toSet());
+                            Collection toSetNormal = CollectionUtils.subtract(errAssetIds, orderAssetIds);
+                            System.out.println(toSetNormal);
+                            for (MintOrder userErrOrder : userErrOrders) {
+                                userErrOrder.setStatus(MintOrderStatus.CANCELLED);
+                                userErrOrder.setCancelTime(LocalDateTime.now());
+                                mintOrderRepo.save(userErrOrder);
+                                PayMethod payMethod = userErrOrder.getPayMethod();
+                                if (PayMethod.ALIPAY == payMethod) {
+                                    if (StringUtils.length(userErrOrder.getTransactionId()) == 28) {
+                                        payMethod = PayMethod.HMPAY;
+                                    } else if (StringUtils.length(userErrOrder.getTransactionId()) == 30) {
+                                        payMethod = PayMethod.SANDPAY;
+                                    }
+                                }
+                                try {
+                                    switch (payMethod) {
+                                        case HMPAY:
+                                            orderPayService.refund(userErrOrder.getId()
+                                                    .toString(), userErrOrder.getTransactionId(), userErrOrder.getGasPrice(), "hmPay");
+                                            break;
+                                        case SANDPAY:
+                                            orderPayService.refund(userErrOrder.getId()
+                                                    .toString(), userErrOrder.getTransactionId(), userErrOrder.getGasPrice(), "sandPay");
+                                            break;
+                                        case PAYEASE:
+                                            orderPayService.refund(userErrOrder.getId()
+                                                    .toString(), userErrOrder.getTransactionId(), userErrOrder.getGasPrice(), "payEase");
+                                            break;
+                                    }
+                                } catch (Exception e) {
+                                }
                             }
-                        }
-                        try {
-                            switch (payMethod) {
-                                case HMPAY:
-                                    orderPayService.refund(userErrOrder.getId().toString(), userErrOrder.getGasPrice(), "hmPay");
-                                    break;
-                                case SANDPAY:
-                                    orderPayService.refund(userErrOrder.getId().toString(), userErrOrder.getGasPrice(), "sandPay");
-                                    break;
+                            for (Object assetId : toSetNormal) {
+                                Asset asset = assetRepo.findById((Long) assetId).get();
+                                asset.setStatus(AssetStatus.NORMAL);
+                                asset.getPrivileges().forEach(p -> {
+                                    if (p.getName().contains("铸造")) {
+                                        p.setOpened(false);
+                                        p.setOpenTime(null);
+                                        p.setOpenedBy(null);
+                                    }
+                                });
+                                asset.setOwnerId(asset.getUserId());
+                                assetRepo.saveAndFlush(asset);
+                                userRepo.updateAssetOwner(asset.getUserId());
+                                tokenHistoryRepo.findByTokenIdOrderByCreatedAtDesc(asset.getTokenId()).stream()
+                                        .findFirst()
+                                        .ifPresent(tokenHistory -> {
+                                            if (tokenHistory.getToUserId().equals(1435297L)) {
+                                                tokenHistoryRepo.delete(tokenHistory);
+                                            }
+                                        });
                             }
-                        } catch (Exception e) {
                         }
+                    });
+            for (MintOrder errOrder : errOrders) {
+                System.out.println("remove orderId=" + errOrder.getId() + ", userId=" + errOrder.getUserId() + ", activityId=" + errOrder.getMintActivityId());
+            }
+        }).get();
+
+    }
+
+    @Test
+    public void fixDuplicated1() throws ExecutionException, InterruptedException {
+        new ForkJoinPool(3000).submit(() -> {
+            List<MintOrder> mintOrders = mintOrderRepo.findByStatusAndCreatedAtAfterOrderByCreatedAtDesc(MintOrderStatus.AIR_DROP, LocalDateTime.of(2022, 5, 20, 0, 0, 0));
+            for (MintOrder userErrOrder : mintOrders.subList(0, 100)) {
+                System.out.println(userErrOrder.getId());
+                userErrOrder.setMaterial(mintMaterialRepo.findAllByOrderIdAndDelFalse(userErrOrder.getId()));
+                userErrOrder.setStatus(MintOrderStatus.CANCELLED);
+                userErrOrder.setCancelTime(LocalDateTime.now());
+                mintOrderRepo.save(userErrOrder);
+                PayMethod payMethod = userErrOrder.getPayMethod();
+                if (PayMethod.ALIPAY == payMethod) {
+                    if (StringUtils.length(userErrOrder.getTransactionId()) == 28) {
+                        payMethod = PayMethod.HMPAY;
+                    } else if (StringUtils.length(userErrOrder.getTransactionId()) == 30) {
+                        payMethod = PayMethod.SANDPAY;
                     }
-                    for (Object assetId : toSetNormal) {
-                        Asset asset = assetRepo.findById((Long) assetId).get();
-                        asset.setStatus(AssetStatus.NORMAL);
-                        asset.getPrivileges().forEach(p -> {
-                            if (p.getName().contains("铸造")) {
-                                p.setOpened(false);
-                                p.setOpenTime(null);
-                                p.setOpenedBy(null);
-                            }
-                        });
-                        asset.setOwnerId(asset.getUserId());
-                        assetRepo.saveAndFlush(asset);
-                        userRepo.updateAssetOwner(asset.getUserId());
-                        tokenHistoryRepo.findByTokenIdOrderByCreatedAtDesc(asset.getTokenId()).stream().findFirst()
-                                .ifPresent(tokenHistory -> {
-                                    if (tokenHistory.getToUserId().equals(1435297L)) {
-                                        tokenHistoryRepo.delete(tokenHistory);
-                                    }
-                                });
+                }
+                try {
+                    switch (payMethod) {
+                        case HMPAY:
+                            orderPayService.refund(userErrOrder.getId()
+                                    .toString(), userErrOrder.getTransactionId(), userErrOrder.getGasPrice(), "hmPay");
+                            break;
+                        case SANDPAY:
+                            orderPayService.refund(userErrOrder.getId()
+                                    .toString(), userErrOrder.getTransactionId(), userErrOrder.getGasPrice(), "sandPay");
+                            break;
+                        case PAYEASE:
+                            orderPayService.refund(userErrOrder.getTransactionId(), userErrOrder.getTransactionId(), userErrOrder.getGasPrice(), "payEase");
+                            break;
                     }
+                } catch (Exception e) {
+                }
+                for (MintMaterial material : userErrOrder.getMaterial()) {
+                    Long assetId = material.getAssetId();
+                    Asset asset = assetRepo.findById(assetId).get();
+                    asset.setStatus(AssetStatus.NORMAL);
+                    asset.getPrivileges().forEach(p -> {
+                        if (p.getName().contains("铸造")) {
+                            p.setOpened(false);
+                            p.setOpenTime(null);
+                            p.setOpenedBy(null);
+                        }
+                    });
+                    asset.setOwnerId(asset.getUserId());
+                    assetRepo.saveAndFlush(asset);
+                    userRepo.updateAssetOwner(asset.getUserId());
+                    tokenHistoryRepo.findByTokenIdOrderByCreatedAtDesc(asset.getTokenId()).stream().findFirst()
+                            .ifPresent(tokenHistory -> {
+                                if (tokenHistory.getToUserId().equals(1435297L)) {
+                                    tokenHistoryRepo.delete(tokenHistory);
+                                }
+                            });
                 }
-            });
-            for (MintOrder errOrder : errOrders) {
-                System.out.println("remove orderId=" + errOrder.getId() + ", userId=" + errOrder.getUserId() + ", activityId=" + errOrder.getMintActivityId());
             }
         }).get();
 
     }
+
+    @Test
+    public void fix() {
+        for (MintOrder order : mintOrderRepo.findByMintActivityIdAndStatusOrderById(7584417L, MintOrderStatus.AIR_DROP)
+                .subList(0, 3)) {
+            PayQuery query = orderPayService.query(order.getId().toString());
+            if (query.isExist()) {
+                order.setStatus(MintOrderStatus.NOT_PAID);
+                mintOrderRepo.saveAndFlush(order);
+                rocketMQTemplate.syncSend("order-notify-topic",
+                        new OrderNotifyEvent(order.getId(), Constants.PayChannel.HM.equals(query.getChannel()) ? PayMethod.HMPAY : PayMethod.SANDPAY,
+                                query.getTransactionId(), System.currentTimeMillis(), OrderNotifyEvent.TYPE_MINT_ORDER));
+            }
+        }
+    }
 }

+ 63 - 20
src/test/java/com/izouma/nineth/service/SandPayServiceTest.java

@@ -1,5 +1,6 @@
 package com.izouma.nineth.service;
 
+import cn.com.sandpay.cashier.sdk.CertUtil;
 import com.alibaba.fastjson.JSONObject;
 import com.izouma.nineth.ApplicationTests;
 import com.izouma.nineth.utils.DateTimeUtils;
@@ -8,40 +9,82 @@ import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 
 import java.math.BigDecimal;
+import java.time.LocalDate;
 import java.time.LocalDateTime;
 
-class SandPayServiceTest extends ApplicationTests {
-    @Autowired
-    private SandPayService    sandPayService;
-    @Autowired
-    private SnowflakeIdWorker snowflakeIdWorker;
+class SandPayServiceTest {
+    String mid = "6888806043057";
+
+    static {
+        try {
+            CertUtil.init("classpath:cert/sand.cer", "classpath:cert/6888806043057.pfx", "3edc#EDC");
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
 
     @Test
-    void requestAlipay() {
-        JSONObject jsonObject = sandPayService.requestAlipayRaw(snowflakeIdWorker.nextId() + "",
-                new BigDecimal("0.01"), "话费充值", "话费充值",
-                DateTimeUtils.format(LocalDateTime.now().plusSeconds(3), "yyyyMMddHHmmss"), "");
-        System.out.println(JSONObject.toJSONString(jsonObject, true));
+    void pay() {
+        String orderId = new SnowflakeIdWorker(0, 0).nextId() + "";
+        if (orderId.length() < 12) {
+            for (int i = orderId.length(); i < 12; i++) {
+                orderId = "0" + orderId;
+            }
+        }
+
+        JSONObject extend = new JSONObject();
+        extend.put("type", "test");
+        extend.put("orderId", orderId);
+
+        JSONObject header = new JSONObject();
+        header.put("version", "1.0");                      //版本号
+        header.put("method", "sandpay.trade.precreate");   //接口名称:统一下单并支付
+        header.put("productId", "00000006");               //产品编码
+        header.put("mid", mid);     //商户号
+        header.put("accessType", "1");                     //接入类型设置为普通商户接入
+        header.put("channelType", "07");                   //渠道类型:07-互联网   08-移动端
+        header.put("reqTime", SandPayService.getReqTime());               //请求时间
+
+        JSONObject body = new JSONObject();
+        body.put("payTool", "0401");                                  //支付工具: 固定填写0401
+        body.put("orderCode", orderId);                               //商户订单号
+        body.put("totalAmount", "000000000001");               //订单金额 12位长度,精确到分
+        //body.put("limitPay", "5");                                  //限定支付方式 送1-限定不能使用贷记卡	送4-限定不能使用花呗	送5-限定不能使用贷记卡+花呗
+        body.put("subject", "充值");                                 //订单标题
+        body.put("body", "充值");                                    //订单描述
+        body.put("txnTimeOut", SandPayService.getTimeout(LocalDateTime.now(), 180));      //订单超时时间
+        body.put("notifyUrl", "https://test.raex.vip/sandpay/notify");      //异步通知地址
+        body.put("bizExtendParams", "");                              //业务扩展参数
+        body.put("merchExtendParams", "");                            //商户扩展参数
+        body.put("extend", extend.toJSONString());                    //扩展域
+
+        JSONObject res = SandPayService.requestServer(header, body, "https://cashier.sandpay.com.cn/qr/api/order/create");
     }
 
     @Test
-    public void testQuery() {
-        JSONObject jsonObject = sandPayService.query("964252580806926336");
+    public void query() {
+        String orderId = "980860142826299392";
+        JSONObject header = new JSONObject();
+        header.put("version", "1.0");
+        header.put("method", "sandpay.trade.query");
+        header.put("productId", "00000006");
+        header.put("mid", mid);    //商户号
+        header.put("accessType", "1");
+        header.put("channelType", "07");
+        header.put("reqTime", SandPayService.getReqTime());
+
+        JSONObject body = new JSONObject();
+        body.put("orderCode", orderId);
+        body.put("extend", "");
+
+        SandPayService.requestServer(header, body, "https://cashier.sandpay.com.cn/qr/api/order/query");
     }
 
     @Test
     public void refund() {
-        for (Integer integer : new Integer[]{6014609,
-                6015032,
-                6015680,
-                6016164,
-                6016401}) {
-            sandPayService.refund(String.format("%012d", integer), new BigDecimal("1"));
-        }
     }
 
     @Test
     public void queryTransfer() {
-        sandPayService.queryTransfer("20220420151655", "966356799781339136");
     }
 }