Gqingci 1 неделя назад
Родитель
Сommit
d55f4a7a10

+ 12 - 4
ruoyi-admin/src/main/resources/application-dev.yml

@@ -280,10 +280,18 @@ justauth:
       client-secret: 1f7d08**********5b7**********29e
       redirect-uri: ${justauth.address}/social-callback?source=gitea
 
-amap:
-  geo:
-    # 企业入驻办公地址转经纬度所需的高德 Web 服务 Key
-    key: "e0e8c3c7bdb74c82e805ed71e134ef5a"
+tencent:
+  map:
+    geo:
+      # 企业入驻办公地址转经纬度所需的腾讯位置服务 WebService Key
+      key: "LA5BZ-BDQL7-NHIXK-P1H5G-EIVSH-GPBQY"
 
 manage:
   jumpTo: http://localhost:90/
+
+withdraw:
+  auditMode: auto
+  autoAuditRule:
+    maxAmount: 10000
+    maxDailyCount: 10
+    maxDailyAmount: 50000

+ 5 - 4
ruoyi-admin/src/main/resources/application-prod.yml

@@ -271,7 +271,8 @@ justauth:
       client-secret: 1f7d08**********5b7**********29e
       redirect-uri: ${justauth.address}/social-callback?source=gitea
 
-amap:
-  geo:
-    # 企业入驻办公地址转经纬度所需的高德 Web 服务 Key
-    key: ""
+tencent:
+  map:
+    geo:
+      # 企业入驻办公地址转经纬度所需的腾讯位置服务 WebService Key
+      key: "LA5BZ-BDQL7-NHIXK-P1H5G-EIVSH-GPBQY"

+ 4 - 4
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/config/AmapGeoProperties.java → ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/config/TencentMapGeoProperties.java

@@ -6,16 +6,16 @@ import org.springframework.stereotype.Component;
 
 @Data
 @Component
-@ConfigurationProperties(prefix = "amap.geo")
-public class AmapGeoProperties {
+@ConfigurationProperties(prefix = "tencent.map.geo")
+public class TencentMapGeoProperties {
 
     /**
-     * Web 服务 Key
+     * 腾讯位置服务 WebService Key
      */
     private String key;
 
     /**
      * 地址解析接口地址
      */
-    private String geocodeUrl = "https://restapi.amap.com/v3/geocode/geo";
+    private String geocodeUrl = "https://apis.map.qq.com/ws/geocoder/v1/";
 }

+ 48 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/config/WithdrawConfig.java

@@ -0,0 +1,48 @@
+package org.dromara.main.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+
+/**
+ * 提现配置
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "withdraw")
+public class WithdrawConfig {
+
+    /**
+     * 审核模式:auto=自动审核,manual=人工审核
+     */
+    private String auditMode = "manual";
+
+    /**
+     * 自动审核规则
+     */
+    private AutoAuditRule autoAuditRule = new AutoAuditRule();
+
+    @Data
+    public static class AutoAuditRule {
+        /**
+         * 单笔最大自动审核金额
+         */
+        private BigDecimal maxAmount = new BigDecimal("10000");
+
+        /**
+         * 单日最大提现次数
+         */
+        private Integer maxDailyCount = 10;
+
+        /**
+         * 单日最大提现总额
+         */
+        private BigDecimal maxDailyAmount = new BigDecimal("50000");
+    }
+
+    public boolean isAutoAudit() {
+        return "auto".equalsIgnoreCase(auditMode);
+    }
+}

+ 0 - 75
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/AmapCompanyGeoServiceImpl.java

@@ -1,75 +0,0 @@
-package org.dromara.main.service.impl;
-
-import cn.hutool.http.HttpUtil;
-import cn.hutool.json.JSONArray;
-import cn.hutool.json.JSONObject;
-import cn.hutool.json.JSONUtil;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.dromara.common.core.exception.ServiceException;
-import org.dromara.common.core.utils.StringUtils;
-import org.dromara.main.config.AmapGeoProperties;
-import org.dromara.main.domain.dto.GeoPointDto;
-import org.dromara.main.service.CompanyGeoService;
-import org.springframework.stereotype.Service;
-
-import java.math.BigDecimal;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * 基于高德 Web 服务地理编码接口,将地址转换为经纬度。
- */
-@Slf4j
-@Service
-@RequiredArgsConstructor
-public class AmapCompanyGeoServiceImpl implements CompanyGeoService {
-
-    private final AmapGeoProperties amapGeoProperties;
-
-    @Override
-    public GeoPointDto geocode(String address) {
-        if (StringUtils.isBlank(address)) {
-            throw new ServiceException("办公地址不能为空");
-        }
-        if (StringUtils.isBlank(amapGeoProperties.getKey())) {
-            throw new ServiceException("未配置高德地图地理编码 Key");
-        }
-
-        Map<String, Object> params = new HashMap<>(4);
-        params.put("key", amapGeoProperties.getKey());
-        params.put("address", address.trim());
-        params.put("output", "JSON");
-
-        try {
-            String response = HttpUtil.get(amapGeoProperties.getGeocodeUrl(), params);
-            JSONObject result = JSONUtil.parseObj(response);
-            if (!"1".equals(result.getStr("status"))) {
-                String info = result.getStr("info");
-                String infocode = result.getStr("infocode");
-                log.error("高德地理编码失败,address={}, info={}, infocode={}", address, info, infocode);
-                throw new ServiceException(StringUtils.format("地址解析失败: {}", StringUtils.blankToDefault(info, "高德接口调用失败")));
-            }
-
-            JSONArray geocodes = result.getJSONArray("geocodes");
-            if (geocodes == null || geocodes.isEmpty()) {
-                throw new ServiceException("地址解析失败: 未匹配到经纬度,请检查办公地址");
-            }
-
-            String location = geocodes.getJSONObject(0).getStr("location");
-            if (StringUtils.isBlank(location) || !location.contains(",")) {
-                throw new ServiceException("地址解析失败: 返回坐标格式异常");
-            }
-
-            String[] parts = location.split(",");
-            BigDecimal longitude = new BigDecimal(parts[0].trim());
-            BigDecimal latitude = new BigDecimal(parts[1].trim());
-            return new GeoPointDto(latitude, longitude);
-        } catch (ServiceException ex) {
-            throw ex;
-        } catch (Exception ex) {
-            log.error("调用高德地理编码接口异常,address={}", address, ex);
-            throw new ServiceException("地址解析失败,请稍后重试");
-        }
-    }
-}

+ 69 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/TencentMapCompanyGeoServiceImpl.java

@@ -0,0 +1,69 @@
+package org.dromara.main.service.impl;
+
+import cn.hutool.http.HttpUtil;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.main.config.TencentMapGeoProperties;
+import org.dromara.main.domain.dto.GeoPointDto;
+import org.dromara.main.service.CompanyGeoService;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 基于腾讯位置服务地址解析接口,将地址转换为经纬度。
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class TencentMapCompanyGeoServiceImpl implements CompanyGeoService {
+
+    private final TencentMapGeoProperties tencentMapGeoProperties;
+
+    @Override
+    public GeoPointDto geocode(String address) {
+        if (StringUtils.isBlank(address)) {
+            throw new ServiceException("办公地址不能为空");
+        }
+        if (StringUtils.isBlank(tencentMapGeoProperties.getKey())) {
+            throw new ServiceException("未配置腾讯地图地理编码 Key");
+        }
+
+        Map<String, Object> params = new HashMap<>(4);
+        params.put("key", tencentMapGeoProperties.getKey());
+        params.put("address", address.trim());
+        params.put("output", "json");
+
+        try {
+            String response = HttpUtil.get(tencentMapGeoProperties.getGeocodeUrl(), params);
+            JSONObject result = JSONUtil.parseObj(response);
+            Integer status = result.getInt("status");
+            if (status == null || status != 0) {
+                String message = result.getStr("message");
+                log.error("腾讯地图地理编码失败,address={}, status={}, message={}", address, status, message);
+                throw new ServiceException(StringUtils.format("地址解析失败: {}", StringUtils.blankToDefault(message, "腾讯地图接口调用失败")));
+            }
+
+            JSONObject resultNode = result.getJSONObject("result");
+            JSONObject location = resultNode == null ? null : resultNode.getJSONObject("location");
+            if (location == null) {
+                throw new ServiceException("地址解析失败: 未匹配到经纬度,请检查办公地址");
+            }
+
+            BigDecimal latitude = BigDecimal.valueOf(location.getDouble("lat"));
+            BigDecimal longitude = BigDecimal.valueOf(location.getDouble("lng"));
+            return new GeoPointDto(latitude, longitude);
+        } catch (ServiceException ex) {
+            throw ex;
+        } catch (Exception ex) {
+            log.error("调用腾讯地图地理编码接口异常,address={}", address, ex);
+            throw new ServiceException("地址解析失败,请稍后重试");
+        }
+    }
+}

+ 73 - 6
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/WithdrawServiceImpl.java

@@ -17,6 +17,7 @@ import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.main.config.WithdrawConfig;
 import org.dromara.main.domain.CompanyAccountFlow;
 import org.dromara.main.domain.PaymentConfig;
 import org.dromara.main.domain.Withdraw;
@@ -37,6 +38,8 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.ZoneId;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
@@ -57,6 +60,7 @@ public class WithdrawServiceImpl implements IWithdrawService {
     private final ISysTenantService tenantService;
     private final SysTenantMapper sysTenantMapper;
     private final IPaymentConfigService paymentConfigService;
+    private final WithdrawConfig withdrawConfig;
 
     @Override
     public WithdrawVo queryById(Long id) {
@@ -126,6 +130,14 @@ public class WithdrawServiceImpl implements IWithdrawService {
         insertFlow(company.getId(), 2, amount, 1, availableBalance, availableBalance.subtract(amount), 5, withdraw.getId(), withdraw.getWithdrawNo(), "申请提现:" + withdraw.getWithdrawNo());
         insertFlow(company.getId(), 1, amount, 3, withdrawingBalance, withdrawingBalance.add(amount), 5, withdraw.getId(), withdraw.getWithdrawNo(), "申请提现(冻结):" + withdraw.getWithdrawNo());
 
+        if (withdrawConfig.isAutoAudit() && shouldAutoAudit(company.getId(), amount)) {
+            try {
+                auditAndTransfer(withdraw.getId(), "系统自动审核通过");
+            } catch (Exception ignored) {
+                // 自动审核失败时保留待审核状态,转人工处理
+            }
+        }
+
         return withdraw.getId();
     }
 
@@ -147,8 +159,7 @@ public class WithdrawServiceImpl implements IWithdrawService {
         withdrawMapper.updateById(withdraw);
 
         try {
-            // 联调阶段先跳过支付宝打款,直接模拟成功流水号,后续余额与状态流转保持不变。
-            String tradeNo = generateMockTradeNo(withdraw);
+            String tradeNo = transferToAlipay(withdraw);
 
             SysTenant tenant = sysTenantMapper.selectById(withdraw.getCompanyId());
             if (tenant == null) {
@@ -355,6 +366,48 @@ public class WithdrawServiceImpl implements IWithdrawService {
             availableBalance.add(withdraw.getWithdrawAmount()), 7, withdraw.getId(), withdraw.getWithdrawNo(), remark);
     }
 
+    private boolean shouldAutoAudit(Long companyId, BigDecimal amount) {
+        WithdrawConfig.AutoAuditRule rule = withdrawConfig.getAutoAuditRule();
+        if (rule == null) {
+            return false;
+        }
+
+        if (amount.compareTo(defaultAmount(rule.getMaxAmount())) > 0) {
+            return false;
+        }
+
+        DailyLimit dailyLimit = checkDailyLimit(companyId);
+        Integer maxDailyCount = rule.getMaxDailyCount() == null ? 0 : rule.getMaxDailyCount();
+        if (maxDailyCount > 0 && dailyLimit.getCount() >= maxDailyCount) {
+            return false;
+        }
+
+        BigDecimal maxDailyAmount = defaultAmount(rule.getMaxDailyAmount());
+        BigDecimal totalAmount = dailyLimit.getAmount().add(amount);
+        return maxDailyAmount.compareTo(BigDecimal.ZERO) <= 0 || totalAmount.compareTo(maxDailyAmount) <= 0;
+    }
+
+    private DailyLimit checkDailyLimit(Long companyId) {
+        LocalDate today = LocalDate.now();
+        Date startOfDay = Date.from(today.atStartOfDay(ZoneId.systemDefault()).toInstant());
+        Date endOfDay = Date.from(today.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
+
+        List<Withdraw> todayWithdraws = withdrawMapper.selectList(
+            Wrappers.<Withdraw>lambdaQuery()
+                .eq(Withdraw::getCompanyId, companyId)
+                .ge(Withdraw::getCreateTime, startOfDay)
+                .lt(Withdraw::getCreateTime, endOfDay)
+        );
+
+        int count = todayWithdraws.size();
+        BigDecimal totalAmount = todayWithdraws.stream()
+            .map(Withdraw::getWithdrawAmount)
+            .filter(Objects::nonNull)
+            .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+        return new DailyLimit(count, totalAmount);
+    }
+
     private String transferToAlipay(Withdraw withdraw) throws Exception {
         PaymentConfig config = paymentConfigService.getEnabledTransferConfig();
         if (config == null) {
@@ -429,11 +482,25 @@ public class WithdrawServiceImpl implements IWithdrawService {
         return "WD" + DateUtil.format(new Date(), "yyyyMMddHHmmss") + RandomUtil.randomNumbers(4);
     }
 
-    private String generateMockTradeNo(Withdraw withdraw) {
-        return "MOCK_" + withdraw.getWithdrawNo();
-    }
-
     private String defaultIfBlank(String value, String defaultValue) {
         return value == null || value.isBlank() ? defaultValue : value;
     }
+
+    private static class DailyLimit {
+        private final int count;
+        private final BigDecimal amount;
+
+        private DailyLimit(int count, BigDecimal amount) {
+            this.count = count;
+            this.amount = amount;
+        }
+
+        public int getCount() {
+            return count;
+        }
+
+        public BigDecimal getAmount() {
+            return amount;
+        }
+    }
 }