Zhangbw пре 3 месеци
родитељ
комит
f72349c9df

+ 3 - 0
src/main/java/com/yingpai/gupiao/config/WebMvcConfig.java

@@ -30,6 +30,9 @@ public class WebMvcConfig implements WebMvcConfigurer {
                 // 排除不需要认证的路径
                 .excludePathPatterns("/v1/auth/**")
                 .excludePathPatterns("/v1/stock/**")
+                .excludePathPatterns("/v1/order/notify/**")
+                .excludePathPatterns("/v1/order/config/**")
+                .excludePathPatterns("/v1/agreement/**")
                 .excludePathPatterns("/api/**")
                 .excludePathPatterns("/uploads/**");
         log.info("认证拦截器已注册");

+ 84 - 0
src/main/java/com/yingpai/gupiao/controller/AgreementController.java

@@ -0,0 +1,84 @@
+package com.yingpai.gupiao.controller;
+
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+/**
+ * 协议控制器(小程序端访问)
+ */
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/v1/agreement")
+public class AgreementController {
+
+    private final JdbcTemplate jdbcTemplate;
+
+    private static final String USER_AGREEMENT_KEY = "miniapp.user.agreement";
+    private static final String PRIVACY_POLICY_KEY = "miniapp.privacy.policy";
+
+    /**
+     * 获取协议HTML页面(供web-view使用)
+     */
+    @GetMapping("/page")
+    public void getAgreementPage(@RequestParam String type, HttpServletResponse response) throws IOException {
+        String key = "user".equals(type) ? USER_AGREEMENT_KEY : PRIVACY_POLICY_KEY;
+        String content = getConfigValue(key);
+        
+        if (content == null || content.isEmpty()) {
+            content = "<p style='text-align:center;padding:50px;color:#999;'>暂无内容</p>";
+        }
+        
+        // 解码HTML实体(如果内容被转义了)
+        content = decodeHtmlEntities(content);
+        
+        String html = "<!DOCTYPE html>" +
+            "<html>" +
+            "<head>" +
+            "<meta charset=\"UTF-8\">" +
+            "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no\">" +
+            "<style>*{margin:0;padding:0;box-sizing:border-box;}</style>" +
+            "</head>" +
+            "<body>" + content + "</body>" +
+            "</html>";
+        
+        response.setContentType("text/html;charset=UTF-8");
+        response.setCharacterEncoding("UTF-8");
+        PrintWriter writer = response.getWriter();
+        writer.write(html);
+        writer.flush();
+    }
+    
+    /**
+     * 解码HTML实体
+     */
+    private String decodeHtmlEntities(String content) {
+        if (content == null) return null;
+        return content
+            .replace("&lt;", "<")
+            .replace("&gt;", ">")
+            .replace("&amp;", "&")
+            .replace("&quot;", "\"")
+            .replace("&#39;", "'")
+            .replace("&nbsp;", " ");
+    }
+
+    /**
+     * 从sys_config表获取配置值
+     */
+    private String getConfigValue(String key) {
+        try {
+            String sql = "SELECT config_value FROM sys_config WHERE config_key = ? LIMIT 1";
+            return jdbcTemplate.queryForObject(sql, String.class, key);
+        } catch (Exception e) {
+            log.warn("获取配置失败: key={}", key);
+            return "";
+        }
+    }
+}

+ 17 - 6
src/main/java/com/yingpai/gupiao/controller/OrderController.java

@@ -178,15 +178,26 @@ public class OrderController {
     }
     
     /**
-     * 模拟支付成功(仅测试用
+     * 继续支付(对已有待支付订单重新获取支付参数
      */
-    @PostMapping("/mock-pay")
-    public Result<Void> mockPay(@RequestParam String orderNo) {
+    @PostMapping("/repay")
+    public Result<WxPayVO> repayOrder(@RequestHeader("Authorization") String authorization,
+                                       @RequestParam String orderNo) {
         try {
-            orderService.handlePaySuccess(orderNo, "MOCK_" + System.currentTimeMillis());
-            return Result.success(null);
+            String token = authorization.replace("Bearer ", "");
+            Long userId = jwtUtil.getUserIdFromToken(token);
+            
+            User user = userMapper.selectById(userId);
+            if (user == null || user.getOpenid() == null) {
+                return Result.error("用户信息不完整");
+            }
+            
+            WxPayVO payVO = orderService.repayOrder(userId, user.getOpenid(), orderNo);
+            return Result.success(payVO);
         } catch (Exception e) {
-            return Result.error("模拟支付失败:" + e.getMessage());
+            log.error("继续支付失败", e);
+            return Result.error(e.getMessage());
         }
     }
+    
 }

+ 5 - 0
src/main/java/com/yingpai/gupiao/domain/vo/WxPayVO.java

@@ -1,5 +1,6 @@
 package com.yingpai.gupiao.domain.vo;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
@@ -31,4 +32,8 @@ public class WxPayVO {
     
     /** 签名 */
     private String paySign;
+    
+    /** 支付金额(单位:分) */
+    @JsonProperty("total_fee")
+    private Integer totalFee;
 }

+ 9 - 0
src/main/java/com/yingpai/gupiao/service/OrderService.java

@@ -82,4 +82,13 @@ public interface OrderService {
      * @return 处理结果
      */
     String verifyAndHandleNotify(String serialNumber, String nonce, String timestamp, String signature, String body);
+    
+    /**
+     * 继续支付已有订单
+     * @param userId 用户ID
+     * @param openid 用户openid
+     * @param orderNo 订单号
+     * @return 微信支付参数
+     */
+    WxPayVO repayOrder(Long userId, String openid, String orderNo);
 }

+ 6 - 0
src/main/java/com/yingpai/gupiao/service/WxPayConfigService.java

@@ -14,4 +14,10 @@ public interface WxPayConfigService {
     String getNotifyUrl();
     
     String getPrivateKeyPath();
+    
+    /** 微信支付公钥路径(新商户需要) */
+    String getPublicKeyPath();
+    
+    /** 微信支付公钥ID(新商户需要) */
+    String getPublicKeyId();
 }

+ 7 - 0
src/main/java/com/yingpai/gupiao/service/WxPayService.java

@@ -35,6 +35,13 @@ public interface WxPayService {
      */
     String queryPaymentStatus(String orderNo);
     
+    /**
+     * 根据商户订单号查询微信订单,返回微信支付订单号
+     * @param orderNo 商户订单号
+     * @return 微信支付订单号(transaction_id),未支付返回null
+     */
+    String queryOrderByOutTradeNo(String orderNo);
+    
     /**
      * 申请退款
      * @param orderNo 订单号

+ 45 - 14
src/main/java/com/yingpai/gupiao/service/impl/OrderServiceImpl.java

@@ -65,17 +65,10 @@ public class OrderServiceImpl implements OrderService {
         orderMapper.insert(order);
         log.info("创建订单,orderNo: {}, userId: {}, poolType: {}, amount: {}", 
                 orderNo, userId, config.getPoolType(), config.getPrice());
-        
-        // 测试模式:直接返回订单号,不调用微信支付
-        // 正式环境请改为:return wxPayService.createPrepayOrder(orderNo, openid, amountFen, config.getPoolName());
-        return WxPayVO.builder()
-                .orderNo(orderNo)
-                .timeStamp(String.valueOf(System.currentTimeMillis() / 1000))
-                .nonceStr(java.util.UUID.randomUUID().toString().replace("-", ""))
-                .packageValue("prepay_id=test_" + orderNo)
-                .signType("RSA")
-                .paySign("test_sign")
-                .build();
+
+        // 调用微信支付接口获取支付参数
+        String description = config.getPoolName() + "订阅";
+        return wxPayService.createPrepayOrder(orderNo, openid, amountFen, description);
     }
     
     @Override
@@ -107,7 +100,7 @@ public class OrderServiceImpl implements OrderService {
     /**
      * 创建订阅
      * 超短池:到当日24点
-     * 强势池:1年
+     * 强势池:到当年最后一天的23:59:59
      */
     private void createSubscription(PaymentOrder order) {
         LocalDateTime now = LocalDateTime.now();
@@ -117,8 +110,8 @@ public class OrderServiceImpl implements OrderService {
             // 超短池:到当日24点
             expireTime = LocalDateTime.of(LocalDate.now(), LocalTime.MAX);
         } else {
-            // 强势池:到当年最后一天的12:00
-            expireTime = LocalDateTime.of(now.getYear(), 12, 31, 12, 0, 0);
+            // 强势池:到当年最后一天的23:59:59(晚上12点)
+            expireTime = LocalDateTime.of(now.getYear(), 12, 31, 23, 59, 59);
         }
         
         // 创建新订阅
@@ -278,4 +271,42 @@ public class OrderServiceImpl implements OrderService {
             default -> "未知";
         };
     }
+    
+    @Override
+    public WxPayVO repayOrder(Long userId, String openid, String orderNo) {
+        PaymentOrder order = orderMapper.selectOne(new LambdaQueryWrapper<PaymentOrder>()
+                .eq(PaymentOrder::getOrderNo, orderNo)
+                .eq(PaymentOrder::getUserId, userId));
+        
+        if (order == null) {
+            throw new RuntimeException("订单不存在");
+        }
+        
+        if (order.getOrderStatus() == PaymentOrder.STATUS_PAID) {
+            throw new RuntimeException("订单已支付");
+        }
+        
+        if (order.getOrderStatus() == PaymentOrder.STATUS_CANCELLED) {
+            throw new RuntimeException("订单已取消");
+        }
+        
+        // 重新调用微信支付接口获取支付参数
+        String description = order.getPoolName() + "订阅";
+        log.info("继续支付订单,orderNo: {}, userId: {}", orderNo, userId);
+        
+        try {
+            return wxPayService.createPrepayOrder(orderNo, openid, order.getAmountFen(), description);
+        } catch (Exception e) {
+            // 如果微信返回订单已支付,说明回调还没处理,主动查询并更新
+            if (e.getMessage() != null && e.getMessage().contains("ORDERPAID")) {
+                log.info("微信返回订单已支付,主动查询并更新状态,orderNo: {}", orderNo);
+                String transactionId = wxPayService.queryOrderByOutTradeNo(orderNo);
+                if (transactionId != null) {
+                    handlePaySuccess(orderNo, transactionId);
+                }
+                throw new RuntimeException("订单已支付,请刷新页面查看");
+            }
+            throw e;
+        }
+    }
 }

+ 12 - 0
src/main/java/com/yingpai/gupiao/service/impl/WxPayConfigServiceImpl.java

@@ -23,6 +23,8 @@ public class WxPayConfigServiceImpl implements WxPayConfigService {
     private static final String CERT_PATH_KEY = "payment.cert.path";
     private static final String NOTIFY_URL_KEY = "payment.notify.url";
     private static final String PRIVATE_KEY_PATH_KEY = "payment.private.key.path";
+    private static final String PUBLIC_KEY_PATH_KEY = "payment.public.key.path";
+    private static final String PUBLIC_KEY_ID_KEY = "payment.public.key.id";
     
     @Override
     public String getMchId() {
@@ -49,6 +51,16 @@ public class WxPayConfigServiceImpl implements WxPayConfigService {
         return getConfigValue(PRIVATE_KEY_PATH_KEY);
     }
     
+    @Override
+    public String getPublicKeyPath() {
+        return getConfigValue(PUBLIC_KEY_PATH_KEY);
+    }
+    
+    @Override
+    public String getPublicKeyId() {
+        return getConfigValue(PUBLIC_KEY_ID_KEY);
+    }
+    
     private String getConfigValue(String key) {
         SysConfig config = sysConfigMapper.selectOne(
             new LambdaQueryWrapper<SysConfig>()

+ 79 - 9
src/main/java/com/yingpai/gupiao/service/impl/WxPayServiceImpl.java

@@ -2,6 +2,7 @@ package com.yingpai.gupiao.service.impl;
 
 import com.wechat.pay.java.core.Config;
 import com.wechat.pay.java.core.RSAAutoCertificateConfig;
+import com.wechat.pay.java.core.RSAPublicKeyConfig;
 import com.wechat.pay.java.core.notification.NotificationConfig;
 import com.wechat.pay.java.core.notification.NotificationParser;
 import com.wechat.pay.java.core.notification.RequestParam;
@@ -52,7 +53,9 @@ public class WxPayServiceImpl implements WxPayService {
         String apiV3Key = wxPayConfigService.getApiV3Key();
         String privateKeyPath = wxPayConfigService.getPrivateKeyPath();
         String certPath = wxPayConfigService.getCertPath();
-        return mchId + "|" + apiV3Key + "|" + privateKeyPath + "|" + certPath;
+        String publicKeyPath = wxPayConfigService.getPublicKeyPath();
+        String publicKeyId = wxPayConfigService.getPublicKeyId();
+        return mchId + "|" + apiV3Key + "|" + privateKeyPath + "|" + certPath + "|" + publicKeyPath + "|" + publicKeyId;
     }
     
     /**
@@ -73,8 +76,10 @@ public class WxPayServiceImpl implements WxPayService {
             String apiV3Key = wxPayConfigService.getApiV3Key();
             String privateKeyPath = wxPayConfigService.getPrivateKeyPath();
             String certPath = wxPayConfigService.getCertPath();
+            String publicKeyPath = wxPayConfigService.getPublicKeyPath();
+            String publicKeyId = wxPayConfigService.getPublicKeyId();
             
-            if (mchId.isEmpty() || apiV3Key.isEmpty() || privateKeyPath.isEmpty() || certPath.isEmpty()) {
+            if (mchId.isEmpty() || apiV3Key.isEmpty() || privateKeyPath.isEmpty()) {
                 log.warn("微信支付配置不完整,跳过初始化");
                 config = null;
                 jsapiService = null;
@@ -89,13 +94,30 @@ public class WxPayServiceImpl implements WxPayService {
             String mchSerialNo = loadCertSerialNo(certPath);
             log.info("从证书读取序列号: {}", mchSerialNo);
             
-            // 构建配置(自动获取微信平台证书)
-            config = new RSAAutoCertificateConfig.Builder()
-                    .merchantId(mchId)
-                    .privateKey(privateKey)
-                    .merchantSerialNumber(mchSerialNo)
-                    .apiV3Key(apiV3Key)
-                    .build();
+            // 判断使用公钥模式还是自动证书模式
+            if (!publicKeyPath.isEmpty() && !publicKeyId.isEmpty()) {
+                // 使用微信支付公钥模式(新商户)
+                log.info("使用微信支付公钥模式初始化,公钥路径: {}", publicKeyPath);
+                String publicKey = loadPublicKey(publicKeyPath);
+                
+                config = new RSAPublicKeyConfig.Builder()
+                        .merchantId(mchId)
+                        .privateKey(privateKey)
+                        .merchantSerialNumber(mchSerialNo)
+                        .apiV3Key(apiV3Key)
+                        .publicKey(publicKey)
+                        .publicKeyId(publicKeyId)
+                        .build();
+            } else {
+                // 使用自动下载平台证书模式(老商户)
+                log.info("使用自动证书模式初始化");
+                config = new RSAAutoCertificateConfig.Builder()
+                        .merchantId(mchId)
+                        .privateKey(privateKey)
+                        .merchantSerialNumber(mchSerialNo)
+                        .apiV3Key(apiV3Key)
+                        .build();
+            }
             
             // 初始化JSAPI服务
             jsapiService = new JsapiServiceExtension.Builder().config(config).build();
@@ -154,6 +176,7 @@ public class WxPayServiceImpl implements WxPayService {
                 .packageValue(response.getPackageVal())
                 .signType(response.getSignType())
                 .paySign(response.getPaySign())
+                .totalFee(amount)
                 .build();
     }
     
@@ -207,6 +230,32 @@ public class WxPayServiceImpl implements WxPayService {
         return transaction.getTradeState().name();
     }
     
+    @Override
+    public String queryOrderByOutTradeNo(String orderNo) {
+        log.info("查询微信订单,orderNo: {}", orderNo);
+        
+        // 确保SDK已初始化
+        ensureInitialized();
+        
+        if (jsapiService == null) {
+            throw new RuntimeException("微信支付未初始化");
+        }
+        
+        QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();
+        request.setMchid(wxPayConfigService.getMchId());
+        request.setOutTradeNo(orderNo);
+        
+        try {
+            Transaction transaction = jsapiService.queryOrderByOutTradeNo(request);
+            if (transaction.getTradeState() == Transaction.TradeStateEnum.SUCCESS) {
+                return transaction.getTransactionId();
+            }
+        } catch (Exception e) {
+            log.error("查询微信订单失败,orderNo: {}", orderNo, e);
+        }
+        return null;
+    }
+    
     @Override
     public boolean refund(String orderNo, String refundNo, Integer totalAmount,
                           Integer refundAmount, String reason) {
@@ -234,6 +283,27 @@ public class WxPayServiceImpl implements WxPayService {
         }
     }
     
+    /**
+     * 加载微信支付公钥文件
+     */
+    private String loadPublicKey(String path) {
+        try {
+            if (path.startsWith("classpath:")) {
+                String resourcePath = path.replace("classpath:", "");
+                ClassPathResource resource = new ClassPathResource(resourcePath);
+                try (BufferedReader reader = new BufferedReader(
+                        new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
+                    return reader.lines().collect(Collectors.joining("\n"));
+                }
+            } else {
+                return java.nio.file.Files.readString(java.nio.file.Paths.get(path));
+            }
+        } catch (Exception e) {
+            log.error("加载公钥失败: {}", path, e);
+            throw new RuntimeException("加载公钥失败", e);
+        }
+    }
+    
     /**
      * 从证书文件读取序列号
      */

+ 2 - 2
src/main/resources/application.yml

@@ -3,8 +3,8 @@ server:
 
 # 微信配置
 wx:
-  appid: wxcf9eec0da6a6b696
-  secret: 2808024de706bfb74d0fe1a111dfa00d
+  appid: wx9d7e6e3592830447
+  secret: f79f4bf879df37aff9e12fc81f3ff901
 
 # 文件存储配置
 file: