浏览代码

微信支付功能新增

Zhangbw 3 月之前
父节点
当前提交
c2df56bcab
共有 37 个文件被更改,包括 1592 次插入384 次删除
  1. 7 18
      pom.xml
  2. 24 0
      src/main/java/com/yingpai/gupiao/config/FileStorageConfig.java
  3. 26 23
      src/main/java/com/yingpai/gupiao/config/WebMvcConfig.java
  4. 32 0
      src/main/java/com/yingpai/gupiao/config/WxPayConfig.java
  5. 6 11
      src/main/java/com/yingpai/gupiao/controller/FileUploadController.java
  6. 192 0
      src/main/java/com/yingpai/gupiao/controller/OrderController.java
  7. 6 6
      src/main/java/com/yingpai/gupiao/controller/StockSearchController.java
  8. 13 0
      src/main/java/com/yingpai/gupiao/domain/dto/CreateOrderDTO.java
  9. 67 0
      src/main/java/com/yingpai/gupiao/domain/po/PaymentOrder.java
  10. 0 49
      src/main/java/com/yingpai/gupiao/domain/po/SysOssConfig.java
  11. 57 0
      src/main/java/com/yingpai/gupiao/domain/po/UserSubscription.java
  12. 0 34
      src/main/java/com/yingpai/gupiao/domain/vo/ApiResponse.java
  13. 45 0
      src/main/java/com/yingpai/gupiao/domain/vo/OrderVO.java
  14. 30 0
      src/main/java/com/yingpai/gupiao/domain/vo/PaymentConfigVO.java
  15. 3 0
      src/main/java/com/yingpai/gupiao/domain/vo/StockDetailResponse.java
  16. 42 0
      src/main/java/com/yingpai/gupiao/domain/vo/SubscriptionVO.java
  17. 34 0
      src/main/java/com/yingpai/gupiao/domain/vo/WxPayVO.java
  18. 5 5
      src/main/java/com/yingpai/gupiao/handler/GlobalExceptionHandler.java
  19. 2 5
      src/main/java/com/yingpai/gupiao/mapper/PaymentOrderMapper.java
  20. 18 0
      src/main/java/com/yingpai/gupiao/mapper/SysConfigMapper.java
  21. 19 0
      src/main/java/com/yingpai/gupiao/mapper/UserSubscriptionMapper.java
  22. 4 4
      src/main/java/com/yingpai/gupiao/service/FileStorageService.java
  23. 85 0
      src/main/java/com/yingpai/gupiao/service/OrderService.java
  24. 16 0
      src/main/java/com/yingpai/gupiao/service/PaymentConfigService.java
  25. 17 0
      src/main/java/com/yingpai/gupiao/service/WxPayConfigService.java
  26. 48 0
      src/main/java/com/yingpai/gupiao/service/WxPayService.java
  27. 98 0
      src/main/java/com/yingpai/gupiao/service/impl/LocalFileStorageServiceImpl.java
  28. 258 0
      src/main/java/com/yingpai/gupiao/service/impl/OrderServiceImpl.java
  29. 0 229
      src/main/java/com/yingpai/gupiao/service/impl/OssServiceImpl.java
  30. 54 0
      src/main/java/com/yingpai/gupiao/service/impl/PaymentConfigServiceImpl.java
  31. 54 0
      src/main/java/com/yingpai/gupiao/service/impl/WxPayConfigServiceImpl.java
  32. 274 0
      src/main/java/com/yingpai/gupiao/service/impl/WxPayServiceImpl.java
  33. 6 0
      src/main/resources/application.yml
  34. 28 0
      src/main/resources/sql/payment_order.sql
  35. 22 0
      src/main/resources/sql/user_subscription.sql
  36. 二进制
      uploads/2025/12/29/bc4f6a5f-46a0-4c4c-b912-803cb16837dd.jpeg
  37. 二进制
      uploads/2025/12/29/fa44cdc6-5def-4615-be4f-7271a7b10d69.jpeg

+ 7 - 18
pom.xml

@@ -29,7 +29,6 @@
     <properties>
         <java.version>17</java.version>
         <mybatis-plus.version>3.5.8</mybatis-plus.version>
-        <aws-sdk.version>2.25.60</aws-sdk.version>
     </properties>
     <dependencies>
         <!-- 基础 Spring Boot -->
@@ -77,6 +76,13 @@
             <optional>true</optional>
         </dependency>
 
+        <!-- 微信支付 SDK -->
+        <dependency>
+            <groupId>com.github.wechatpay-apiv3</groupId>
+            <artifactId>wechatpay-java</artifactId>
+            <version>0.2.14</version>
+        </dependency>
+
         <!-- JWT 用于生成和验证token -->
         <dependency>
             <groupId>io.jsonwebtoken</groupId>
@@ -95,23 +101,6 @@
             <version>0.11.5</version>
             <scope>runtime</scope>
         </dependency>
-
-        <!-- AWS S3 SDK (兼容阿里云/腾讯云/MinIO等) -->
-        <dependency>
-            <groupId>software.amazon.awssdk</groupId>
-            <artifactId>s3</artifactId>
-            <version>${aws-sdk.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>software.amazon.awssdk</groupId>
-            <artifactId>s3-transfer-manager</artifactId>
-            <version>${aws-sdk.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>software.amazon.awssdk</groupId>
-            <artifactId>netty-nio-client</artifactId>
-            <version>${aws-sdk.version}</version>
-        </dependency>
     </dependencies>
 
     <build>

+ 24 - 0
src/main/java/com/yingpai/gupiao/config/FileStorageConfig.java

@@ -0,0 +1,24 @@
+package com.yingpai.gupiao.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 本地文件存储配置
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "file.storage")
+public class FileStorageConfig {
+    
+    /**
+     * 文件存储根路径
+     */
+    private String path = "./uploads";
+    
+    /**
+     * 文件访问URL前缀
+     */
+    private String urlPrefix = "/uploads";
+}

+ 26 - 23
src/main/java/com/yingpai/gupiao/config/WebMvcConfig.java

@@ -1,37 +1,40 @@
 package com.yingpai.gupiao.config;
 
-import com.yingpai.gupiao.interceptor.AuthInterceptor;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.context.annotation.Configuration;
-import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
+import java.nio.file.Paths;
+
 /**
- * Web MVC配置类
- * 配置拦截器等
+ * Web MVC配置 - 静态资源映射
  */
+@Slf4j
 @Configuration
 @RequiredArgsConstructor
 public class WebMvcConfig implements WebMvcConfigurer {
-    
-    private final AuthInterceptor authInterceptor;
-    
-    /**
-     * 添加拦截器配置
-     */
+
+    private final FileStorageConfig fileStorageConfig;
+
     @Override
-    public void addInterceptors(InterceptorRegistry registry) {
-        registry.addInterceptor(authInterceptor)
-                .addPathPatterns("/**")
-                .excludePathPatterns(
-                        "/v1/auth/**",
-                        "/auth/sys/miniapp/custom/**",
-                        "/v1/stock/suggestion",
-                        "/v1/stock/search",
-                        "/api/stock/**",
-                        "/error",
-                        "/favicon.ico",
-                        "/static/**"
-                );
+    public void addResourceHandlers(ResourceHandlerRegistry registry) {
+        // 获取绝对路径,统一使用/作为分隔符
+        String absolutePath = Paths.get(fileStorageConfig.getPath())
+                .toAbsolutePath()
+                .normalize()
+                .toString()
+                .replace("\\", "/");
+        
+        if (!absolutePath.endsWith("/")) {
+            absolutePath = absolutePath + "/";
+        }
+        
+        String location = "file:///" + absolutePath;
+        log.info("静态资源映射: /uploads/** -> {}", location);
+        
+        registry.addResourceHandler("/uploads/**")
+                .addResourceLocations(location);
     }
 }

+ 32 - 0
src/main/java/com/yingpai/gupiao/config/WxPayConfig.java

@@ -0,0 +1,32 @@
+package com.yingpai.gupiao.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 微信支付配置类
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "wx.pay")
+public class WxPayConfig {
+    
+    /** 商户号 */
+    private String mchId;
+    
+    /** 商户证书文件路径 */
+    private String certPath;
+    
+    /** 商户私钥文件路径 */
+    private String privateKeyPath;
+    
+    /** APIv3密钥 */
+    private String apiV3Key;
+    
+    /** 支付回调通知地址 */
+    private String notifyUrl;
+    
+    /** 退款回调通知地址 */
+    private String refundNotifyUrl;
+}

+ 6 - 11
src/main/java/com/yingpai/gupiao/controller/FileUploadController.java

@@ -1,7 +1,7 @@
 package com.yingpai.gupiao.controller;
 
 import com.yingpai.gupiao.domain.vo.Result;
-import com.yingpai.gupiao.service.OssService;
+import com.yingpai.gupiao.service.FileStorageService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
@@ -12,7 +12,7 @@ import java.util.Map;
 
 /**
  * 文件上传控制器
- * 使用OSS存储(读取RuoYi后台的OSS配置)
+ * 使用本地存储
  */
 @Slf4j
 @RestController
@@ -20,7 +20,7 @@ import java.util.Map;
 @RequestMapping("/v1/file")
 public class FileUploadController {
 
-    private final OssService ossService;
+    private final FileStorageService fileStorageService;
 
     private static final String[] ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"};
     private static final long MAX_FILE_SIZE = 5 * 1024 * 1024;
@@ -35,7 +35,7 @@ public class FileUploadController {
         try {
             validateFile(file);
 
-            String fileUrl = ossService.upload(file);
+            String fileUrl = fileStorageService.upload(file);
             String originalFilename = file.getOriginalFilename();
 
             Map<String, String> result = new HashMap<>();
@@ -50,13 +50,8 @@ public class FileUploadController {
             log.error("文件验证失败: {}", e.getMessage());
             return Result.error(e.getMessage());
         } catch (Exception e) {
-            log.warn("OSS上传失败,返回默认头像: {}", e.getMessage());
-            // OSS上传失败,返回默认头像
-            Map<String, String> result = new HashMap<>();
-            result.put("url", "/static/images/head.png");
-            result.put("filename", "head.png");
-            result.put("originalFilename", file.getOriginalFilename());
-            return Result.success(result);
+            log.error("文件上传失败: {}", e.getMessage());
+            return Result.error("文件上传失败: " + e.getMessage());
         }
     }
 

+ 192 - 0
src/main/java/com/yingpai/gupiao/controller/OrderController.java

@@ -0,0 +1,192 @@
+package com.yingpai.gupiao.controller;
+
+import com.yingpai.gupiao.domain.dto.CreateOrderDTO;
+import com.yingpai.gupiao.domain.po.User;
+import com.yingpai.gupiao.domain.vo.*;
+import com.yingpai.gupiao.mapper.UserMapper;
+import com.yingpai.gupiao.service.OrderService;
+import com.yingpai.gupiao.util.JwtUtil;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.BufferedReader;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 订单控制器
+ */
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/v1/order")
+public class OrderController {
+    
+    private final OrderService orderService;
+    private final UserMapper userMapper;
+    private final JwtUtil jwtUtil;
+    
+    /**
+     * 获取支付配置
+     */
+    @GetMapping("/config")
+    public Result<PaymentConfigVO> getPaymentConfig(@RequestParam Integer poolType) {
+        try {
+            PaymentConfigVO config = orderService.getPaymentConfig(poolType);
+            return Result.success(config);
+        } catch (Exception e) {
+            return Result.error(e.getMessage());
+        }
+    }
+    
+    /**
+     * 创建订单
+     */
+    @PostMapping("/create")
+    public Result<WxPayVO> createOrder(@RequestHeader("Authorization") String authorization,
+                                        @RequestBody CreateOrderDTO dto) {
+        try {
+            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.createOrder(userId, user.getOpenid(), dto);
+            return Result.success(payVO);
+        } catch (Exception e) {
+            log.error("创建订单失败", e);
+            return Result.error("创建订单失败:" + e.getMessage());
+        }
+    }
+    
+    /**
+     * 微信支付回调
+     */
+    @PostMapping("/notify")
+    public Map<String, String> payNotify(HttpServletRequest request) {
+        // 微信支付返回规范
+        Map<String, String> response = new HashMap<>();
+        try {
+            // 读取请求体
+            StringBuilder sb = new StringBuilder();
+            try (BufferedReader reader = request.getReader()) {
+                String line;
+                while ((line = reader.readLine()) != null) {
+                    sb.append(line);
+                }
+            }
+            String body = sb.toString();
+            log.info("收到支付回调: {}", body);
+            
+            // 获取回调头信息
+            String serialNumber = request.getHeader("Wechatpay-Serial");
+            String nonce = request.getHeader("Wechatpay-Nonce");
+            String timestamp = request.getHeader("Wechatpay-Timestamp");
+            String signature = request.getHeader("Wechatpay-Signature");
+            
+            // 验签并解密
+            String result = orderService.verifyAndHandleNotify(serialNumber, nonce, timestamp, signature, body);
+            
+            log.info("支付回调处理完成: {}", result);
+            response.put("code", "SUCCESS");
+            response.put("message", "成功");
+        } catch (Exception e) {
+            log.error("处理支付回调异常", e);
+            response.put("code", "FAIL");
+            response.put("message", e.getMessage());
+        }
+        return response;
+    }
+    
+    /**
+     * 查询订单
+     */
+    @GetMapping("/query")
+    public Result<OrderVO> queryOrder(@RequestParam String orderNo) {
+        OrderVO order = orderService.queryOrder(orderNo);
+        return order != null ? Result.success(order) : Result.error("订单不存在");
+    }
+    
+    /**
+     * 获取用户订单列表
+     */
+    @GetMapping("/list")
+    public Result<List<OrderVO>> getUserOrders(@RequestHeader("Authorization") String authorization) {
+        try {
+            String token = authorization.replace("Bearer ", "");
+            Long userId = jwtUtil.getUserIdFromToken(token);
+            return Result.success(orderService.getUserOrders(userId));
+        } catch (Exception e) {
+            return Result.error("获取订单列表失败");
+        }
+    }
+    
+    /**
+     * 获取用户订阅列表
+     */
+    @GetMapping("/subscriptions")
+    public Result<List<SubscriptionVO>> getUserSubscriptions(@RequestHeader("Authorization") String authorization) {
+        try {
+            String token = authorization.replace("Bearer ", "");
+            Long userId = jwtUtil.getUserIdFromToken(token);
+            return Result.success(orderService.getUserSubscriptions(userId));
+        } catch (Exception e) {
+            return Result.error("获取订阅列表失败");
+        }
+    }
+    
+    /**
+     * 检查订阅状态
+     */
+    @GetMapping("/check-subscription")
+    public Result<Map<String, Object>> checkSubscription(@RequestHeader("Authorization") String authorization,
+                                                          @RequestParam Integer poolType) {
+        try {
+            String token = authorization.replace("Bearer ", "");
+            Long userId = jwtUtil.getUserIdFromToken(token);
+            
+            boolean hasActive = orderService.hasActiveSubscription(userId, poolType);
+            Map<String, Object> data = new HashMap<>();
+            data.put("hasSubscription", hasActive);
+            data.put("poolType", poolType);
+            return Result.success(data);
+        } catch (Exception e) {
+            return Result.error("检查订阅状态失败");
+        }
+    }
+    
+    /**
+     * 取消订单
+     */
+    @PostMapping("/cancel")
+    public Result<Void> cancelOrder(@RequestHeader("Authorization") String authorization,
+                                     @RequestParam String orderNo) {
+        try {
+            String token = authorization.replace("Bearer ", "");
+            Long userId = jwtUtil.getUserIdFromToken(token);
+            orderService.cancelOrder(userId, orderNo);
+            return Result.success(null);
+        } catch (Exception e) {
+            return Result.error("取消订单失败:" + e.getMessage());
+        }
+    }
+    
+    /**
+     * 模拟支付成功(仅测试用)
+     */
+    @PostMapping("/mock-pay")
+    public Result<Void> mockPay(@RequestParam String orderNo) {
+        try {
+            orderService.handlePaySuccess(orderNo, "MOCK_" + System.currentTimeMillis());
+            return Result.success(null);
+        } catch (Exception e) {
+            return Result.error("模拟支付失败:" + e.getMessage());
+        }
+    }
+}

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

@@ -2,7 +2,7 @@ package com.yingpai.gupiao.controller;
 
 import com.yingpai.gupiao.domain.dto.StockSearchRequest;
 import com.yingpai.gupiao.domain.dto.StockSuggestionDTO;
-import com.yingpai.gupiao.domain.vo.ApiResponse;
+import com.yingpai.gupiao.domain.vo.Result;
 import com.yingpai.gupiao.domain.vo.StockDetailResponse;
 import com.yingpai.gupiao.service.StockSearchService;
 import org.springframework.web.bind.annotation.*;
@@ -25,12 +25,12 @@ public class StockSearchController {
      * POST /v1/stock/search
      */
     @PostMapping("/search")
-    public ApiResponse<StockDetailResponse> search(@RequestBody StockSearchRequest request) {
+    public Result<StockDetailResponse> search(@RequestBody StockSearchRequest request) {
         StockDetailResponse result = stockSearchService.search(request);
         if (result == null) {
-            return new ApiResponse<>(1001, "未查询到该股票评分数据", null);
+            return Result.error("未查询到该股票评分数据");
         }
-        return new ApiResponse<>(0, "success", result);
+        return Result.success(result);
     }
 
     /**
@@ -39,9 +39,9 @@ public class StockSearchController {
      * GET /v1/stock/suggestion
      */
     @GetMapping("/suggestion")
-    public ApiResponse<List<StockSuggestionDTO>> suggestion(@RequestParam String keyword) {
+    public Result<List<StockSuggestionDTO>> suggestion(@RequestParam String keyword) {
         List<StockSuggestionDTO> list = stockSearchService.getSuggestion(keyword);
-        return new ApiResponse<>(0, "success", list);
+        return Result.success(list);
     }
 }
 

+ 13 - 0
src/main/java/com/yingpai/gupiao/domain/dto/CreateOrderDTO.java

@@ -0,0 +1,13 @@
+package com.yingpai.gupiao.domain.dto;
+
+import lombok.Data;
+
+/**
+ * 创建订单请求DTO
+ */
+@Data
+public class CreateOrderDTO {
+    
+    /** 池类型:1-超短池,2-强势池 */
+    private Integer poolType;
+}

+ 67 - 0
src/main/java/com/yingpai/gupiao/domain/po/PaymentOrder.java

@@ -0,0 +1,67 @@
+package com.yingpai.gupiao.domain.po;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 支付订单实体类
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@TableName("payment_order")
+public class PaymentOrder {
+    
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    
+    /** 订单号 */
+    private String orderNo;
+    
+    /** 用户ID */
+    private Long userId;
+    
+    /** 用户openid */
+    private String openid;
+    
+    /** 池类型:1-超短池,2-强势池 */
+    private Integer poolType;
+    
+    /** 池名称 */
+    private String poolName;
+    
+    /** 订单金额(元) */
+    private BigDecimal amount;
+    
+    /** 订单金额(分) */
+    private Integer amountFen;
+    
+    /** 订单状态:0-待支付,1-已支付,2-已取消 */
+    private Integer orderStatus;
+    
+    /** 微信支付订单号 */
+    private String transactionId;
+    
+    /** 支付时间 */
+    private LocalDateTime payTime;
+    
+    /** 取消时间 */
+    private LocalDateTime cancelTime;
+    
+    private LocalDateTime createTime;
+    private LocalDateTime updateTime;
+    
+    // 订单状态常量
+    public static final int STATUS_PENDING = 0;    // 待支付
+    public static final int STATUS_PAID = 1;       // 已支付
+    public static final int STATUS_CANCELLED = 2;  // 已取消
+}

+ 0 - 49
src/main/java/com/yingpai/gupiao/domain/po/SysOssConfig.java

@@ -1,49 +0,0 @@
-package com.yingpai.gupiao.domain.po;
-
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
-
-/**
- * 对象存储配置(读取RuoYi的sys_oss_config表)
- */
-@Data
-@TableName("sys_oss_config")
-public class SysOssConfig {
-
-    @TableId(value = "oss_config_id")
-    private Long ossConfigId;
-
-    /** 配置key */
-    private String configKey;
-
-    /** accessKey */
-    private String accessKey;
-
-    /** 秘钥 */
-    private String secretKey;
-
-    /** 桶名称 */
-    private String bucketName;
-
-    /** 前缀 */
-    private String prefix;
-
-    /** 访问站点 */
-    private String endpoint;
-
-    /** 自定义域名 */
-    private String domain;
-
-    /** 是否https(0否 1是) */
-    private String isHttps;
-
-    /** 域 */
-    private String region;
-
-    /** 是否默认(0=是,1=否) */
-    private String status;
-
-    /** 桶权限类型(0private 1public 2custom) */
-    private String accessPolicy;
-}

+ 57 - 0
src/main/java/com/yingpai/gupiao/domain/po/UserSubscription.java

@@ -0,0 +1,57 @@
+package com.yingpai.gupiao.domain.po;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 用户订阅实体类
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@TableName("user_subscription")
+public class UserSubscription {
+    
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    
+    /** 用户ID */
+    private Long userId;
+    
+    /** 池类型:1-超短池,2-强势池 */
+    private Integer poolType;
+    
+    /** 关联订单ID */
+    private Long orderId;
+    
+    /** 关联订单号 */
+    private String orderNo;
+    
+    /** 支付金额 */
+    private BigDecimal amount;
+    
+    /** 订阅开始时间 */
+    private LocalDateTime startTime;
+    
+    /** 订阅到期时间 */
+    private LocalDateTime expireTime;
+    
+    /** 状态:1-有效,0-已过期 */
+    private Integer status;
+    
+    private LocalDateTime createTime;
+    private LocalDateTime updateTime;
+    
+    // 状态常量
+    public static final int STATUS_ACTIVE = 1;   // 有效
+    public static final int STATUS_EXPIRED = 0;  // 已过期
+}

+ 0 - 34
src/main/java/com/yingpai/gupiao/domain/vo/ApiResponse.java

@@ -1,34 +0,0 @@
-package com.yingpai.gupiao.domain.vo;
-
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-/**
- * 统一返回结构
- *
- * {
- *   "code": 0,
- *   "message": "ok",
- *   "data": {...}
- * }
- */
-@Data
-@NoArgsConstructor
-@AllArgsConstructor
-public class ApiResponse<T> {
-
-    private int code;
-    private String message;
-    private T data;
-
-    public static <T> ApiResponse<T> success(T data) {
-        return new ApiResponse<>(0, "success", data);
-    }
-
-    public static <T> ApiResponse<T> error(int code, String message) {
-        return new ApiResponse<>(code, message, null);
-    }
-}
-
-

+ 45 - 0
src/main/java/com/yingpai/gupiao/domain/vo/OrderVO.java

@@ -0,0 +1,45 @@
+package com.yingpai.gupiao.domain.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+/**
+ * 订单响应VO
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class OrderVO {
+    
+    /** 订单ID */
+    private Long orderId;
+    
+    /** 订单号 */
+    private String orderNo;
+    
+    /** 池类型 */
+    private Integer poolType;
+    
+    /** 池名称 */
+    private String poolName;
+    
+    /** 订单金额 */
+    private BigDecimal amount;
+    
+    /** 订单状态 */
+    private Integer orderStatus;
+    
+    /** 订单状态名称 */
+    private String orderStatusName;
+    
+    /** 创建时间 */
+    private String createTime;
+    
+    /** 支付时间 */
+    private String payTime;
+}

+ 30 - 0
src/main/java/com/yingpai/gupiao/domain/vo/PaymentConfigVO.java

@@ -0,0 +1,30 @@
+package com.yingpai.gupiao.domain.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+/**
+ * 支付配置VO
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class PaymentConfigVO {
+    
+    /** 池类型 */
+    private Integer poolType;
+    
+    /** 池名称 */
+    private String poolName;
+    
+    /** 价格 */
+    private BigDecimal price;
+    
+    /** 描述 */
+    private String description;
+}

+ 3 - 0
src/main/java/com/yingpai/gupiao/domain/vo/StockDetailResponse.java

@@ -6,6 +6,9 @@ import lombok.NoArgsConstructor;
 
 import java.util.List;
 
+/**
+ * 打分查询股票信息
+ */
 @Data
 public class StockDetailResponse {
 

+ 42 - 0
src/main/java/com/yingpai/gupiao/domain/vo/SubscriptionVO.java

@@ -0,0 +1,42 @@
+package com.yingpai.gupiao.domain.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+/**
+ * 用户订阅信息VO
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class SubscriptionVO {
+    
+    /** 订阅ID */
+    private Long id;
+    
+    /** 池类型 */
+    private Integer poolType;
+    
+    /** 池名称 */
+    private String poolName;
+    
+    /** 订单金额 */
+    private BigDecimal amount;
+    
+    /** 开始时间 */
+    private String startTime;
+    
+    /** 到期时间 */
+    private String expireTime;
+    
+    /** 是否有效 */
+    private Boolean isActive;
+    
+    /** 剩余天数 */
+    private Integer remainDays;
+}

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

@@ -0,0 +1,34 @@
+package com.yingpai.gupiao.domain.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 微信支付参数VO(返回给小程序调起支付)
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class WxPayVO {
+    
+    /** 订单号 */
+    private String orderNo;
+    
+    /** 时间戳 */
+    private String timeStamp;
+    
+    /** 随机字符串 */
+    private String nonceStr;
+    
+    /** 预支付交易会话标识(prepay_id=xxx) */
+    private String packageValue;
+    
+    /** 签名类型 */
+    private String signType;
+    
+    /** 签名 */
+    private String paySign;
+}

+ 5 - 5
src/main/java/com/yingpai/gupiao/handler/GlobalExceptionHandler.java

@@ -1,6 +1,6 @@
 package com.yingpai.gupiao.handler;
 
-import com.yingpai.gupiao.domain.vo.ApiResponse;
+import com.yingpai.gupiao.domain.vo.Result;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.http.HttpStatus;
@@ -20,9 +20,9 @@ public class GlobalExceptionHandler {
      * 处理参数异常
      */
     @ExceptionHandler(IllegalArgumentException.class)
-    public ResponseEntity<ApiResponse<Void>> handleIllegalArgumentException(IllegalArgumentException ex) {
+    public ResponseEntity<Result<Void>> handleIllegalArgumentException(IllegalArgumentException ex) {
         log.warn("参数错误: {}", ex.getMessage());
-        ApiResponse<Void> body = ApiResponse.error(1003, ex.getMessage() != null ? ex.getMessage() : "参数错误");
+        Result<Void> body = Result.error(400, ex.getMessage() != null ? ex.getMessage() : "参数错误");
         return new ResponseEntity<>(body, HttpStatus.OK);
     }
 
@@ -30,9 +30,9 @@ public class GlobalExceptionHandler {
      * 处理所有未捕获的异常
      */
     @ExceptionHandler(Exception.class)
-    public ResponseEntity<ApiResponse<Void>> handleException(Exception ex) {
+    public ResponseEntity<Result<Void>> handleException(Exception ex) {
         log.error("服务器内部错误", ex);
-        ApiResponse<Void> body = ApiResponse.error(5000, "服务器内部错误");
+        Result<Void> body = Result.error(500, "服务器内部错误");
         return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
     }
 }

+ 2 - 5
src/main/java/com/yingpai/gupiao/mapper/SysOssConfigMapper.java → src/main/java/com/yingpai/gupiao/mapper/PaymentOrderMapper.java

@@ -1,12 +1,9 @@
 package com.yingpai.gupiao.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.yingpai.gupiao.domain.po.SysOssConfig;
+import com.yingpai.gupiao.domain.po.PaymentOrder;
 import org.apache.ibatis.annotations.Mapper;
 
-/**
- * OSS配置Mapper
- */
 @Mapper
-public interface SysOssConfigMapper extends BaseMapper<SysOssConfig> {
+public interface PaymentOrderMapper extends BaseMapper<PaymentOrder> {
 }

+ 18 - 0
src/main/java/com/yingpai/gupiao/mapper/SysConfigMapper.java

@@ -0,0 +1,18 @@
+package com.yingpai.gupiao.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+/**
+ * 系统配置Mapper(只读取配置值)
+ */
+@Mapper
+public interface SysConfigMapper {
+    
+    /**
+     * 根据配置key获取配置值
+     */
+    @Select("SELECT config_value FROM sys_config WHERE config_key = #{configKey} LIMIT 1")
+    String getConfigValue(@Param("configKey") String configKey);
+}

+ 19 - 0
src/main/java/com/yingpai/gupiao/mapper/UserSubscriptionMapper.java

@@ -0,0 +1,19 @@
+package com.yingpai.gupiao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.yingpai.gupiao.domain.po.UserSubscription;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.time.LocalDateTime;
+
+@Mapper
+public interface UserSubscriptionMapper extends BaseMapper<UserSubscription> {
+    
+    /**
+     * 查询用户某个池的有效订阅
+     */
+    @Select("SELECT * FROM user_subscription WHERE user_id = #{userId} AND pool_type = #{poolType} AND status = 1 AND expire_time > #{now} ORDER BY expire_time DESC LIMIT 1")
+    UserSubscription findActiveSubscription(@Param("userId") Long userId, @Param("poolType") Integer poolType, @Param("now") LocalDateTime now);
+}

+ 4 - 4
src/main/java/com/yingpai/gupiao/service/OssService.java → src/main/java/com/yingpai/gupiao/service/FileStorageService.java

@@ -3,19 +3,19 @@ package com.yingpai.gupiao.service;
 import org.springframework.web.multipart.MultipartFile;
 
 /**
- * OSS服务接口
+ * 文件存储服务接口
  */
-public interface OssService {
+public interface FileStorageService {
 
     /**
-     * 上传文件到OSS
+     * 上传文件
      * @param file 文件
      * @return 文件访问URL
      */
     String upload(MultipartFile file);
 
     /**
-     * 删除OSS文件
+     * 删除文件
      * @param url 文件URL
      */
     void delete(String url);

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

@@ -0,0 +1,85 @@
+package com.yingpai.gupiao.service;
+
+import com.yingpai.gupiao.domain.dto.CreateOrderDTO;
+import com.yingpai.gupiao.domain.vo.OrderVO;
+import com.yingpai.gupiao.domain.vo.PaymentConfigVO;
+import com.yingpai.gupiao.domain.vo.SubscriptionVO;
+import com.yingpai.gupiao.domain.vo.WxPayVO;
+
+import java.util.List;
+
+/**
+ * 订单服务接口
+ */
+public interface OrderService {
+    
+    /**
+     * 获取支付配置
+     * @param poolType 池类型
+     * @return 支付配置
+     */
+    PaymentConfigVO getPaymentConfig(Integer poolType);
+    
+    /**
+     * 创建订单并获取支付参数
+     * @param userId 用户ID
+     * @param openid 用户openid
+     * @param dto 创建订单请求
+     * @return 微信支付参数
+     */
+    WxPayVO createOrder(Long userId, String openid, CreateOrderDTO dto);
+    
+    /**
+     * 支付成功处理
+     * @param orderNo 订单号
+     * @param transactionId 微信支付订单号
+     */
+    void handlePaySuccess(String orderNo, String transactionId);
+    
+    /**
+     * 查询订单状态
+     * @param orderNo 订单号
+     * @return 订单信息
+     */
+    OrderVO queryOrder(String orderNo);
+    
+    /**
+     * 获取用户订单列表
+     * @param userId 用户ID
+     * @return 订单列表
+     */
+    List<OrderVO> getUserOrders(Long userId);
+    
+    /**
+     * 获取用户订阅列表
+     * @param userId 用户ID
+     * @return 订阅列表
+     */
+    List<SubscriptionVO> getUserSubscriptions(Long userId);
+    
+    /**
+     * 检查用户是否有某个池的有效订阅
+     * @param userId 用户ID
+     * @param poolType 池类型
+     * @return 是否有效订阅
+     */
+    boolean hasActiveSubscription(Long userId, Integer poolType);
+    
+    /**
+     * 取消订单
+     * @param userId 用户ID
+     * @param orderNo 订单号
+     */
+    void cancelOrder(Long userId, String orderNo);
+    
+    /**
+     * 验证并处理支付回调
+     * @param serialNumber 证书序列号
+     * @param nonce 随机串
+     * @param timestamp 时间戳
+     * @param signature 签名
+     * @param body 请求体
+     * @return 处理结果
+     */
+    String verifyAndHandleNotify(String serialNumber, String nonce, String timestamp, String signature, String body);
+}

+ 16 - 0
src/main/java/com/yingpai/gupiao/service/PaymentConfigService.java

@@ -0,0 +1,16 @@
+package com.yingpai.gupiao.service;
+
+import com.yingpai.gupiao.domain.vo.PaymentConfigVO;
+
+/**
+ * 支付配置服务(从sys_config读取)
+ */
+public interface PaymentConfigService {
+    
+    /**
+     * 获取支付配置
+     * @param poolType 池类型:1-超短池,2-强势池
+     * @return 支付配置
+     */
+    PaymentConfigVO getConfig(Integer poolType);
+}

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

@@ -0,0 +1,17 @@
+package com.yingpai.gupiao.service;
+
+/**
+ * 微信支付配置服务(从sys_config读取商户配置)
+ */
+public interface WxPayConfigService {
+    
+    String getMchId();
+    
+    String getApiV3Key();
+    
+    String getCertPath();
+    
+    String getNotifyUrl();
+    
+    String getPrivateKeyPath();
+}

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

@@ -0,0 +1,48 @@
+package com.yingpai.gupiao.service;
+
+import com.yingpai.gupiao.domain.vo.WxPayVO;
+
+/**
+ * 微信支付服务接口
+ */
+public interface WxPayService {
+    
+    /**
+     * 创建预支付订单(JSAPI下单)
+     * @param orderNo 订单号
+     * @param openid 用户openid
+     * @param amount 金额(分)
+     * @param description 商品描述
+     * @return 支付参数
+     */
+    WxPayVO createPrepayOrder(String orderNo, String openid, Integer amount, String description);
+    
+    /**
+     * 验证并解密回调数据
+     * @param serialNumber 证书序列号
+     * @param nonce 随机串
+     * @param timestamp 时间戳
+     * @param signature 签名
+     * @param body 请求体
+     * @return 解密后的数据
+     */
+    String verifyAndDecryptNotify(String serialNumber, String nonce, String timestamp, String signature, String body);
+    
+    /**
+     * 查询订单支付状态
+     * @param orderNo 订单号
+     * @return 支付状态
+     */
+    String queryPaymentStatus(String orderNo);
+    
+    /**
+     * 申请退款
+     * @param orderNo 订单号
+     * @param refundNo 退款单号
+     * @param totalAmount 订单总金额(分)
+     * @param refundAmount 退款金额(分)
+     * @param reason 退款原因
+     * @return 是否成功
+     */
+    boolean refund(String orderNo, String refundNo, Integer totalAmount, Integer refundAmount, String reason);
+}

+ 98 - 0
src/main/java/com/yingpai/gupiao/service/impl/LocalFileStorageServiceImpl.java

@@ -0,0 +1,98 @@
+package com.yingpai.gupiao.service.impl;
+
+import com.yingpai.gupiao.config.FileStorageConfig;
+import com.yingpai.gupiao.service.FileStorageService;
+import jakarta.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.UUID;
+
+/**
+ * 本地文件存储服务实现
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class LocalFileStorageServiceImpl implements FileStorageService {
+
+    private final FileStorageConfig config;
+    
+    private Path rootPath;
+
+    @PostConstruct
+    public void init() {
+        rootPath = Paths.get(config.getPath()).toAbsolutePath().normalize();
+        try {
+            Files.createDirectories(rootPath);
+            log.info("[FileStorage] 本地存储初始化成功,路径: {}", rootPath);
+        } catch (IOException e) {
+            log.error("[FileStorage] 创建存储目录失败: {}", e.getMessage());
+            throw new RuntimeException("无法创建文件存储目录", e);
+        }
+    }
+
+    @Override
+    public String upload(MultipartFile file) {
+        try {
+            String originalFilename = file.getOriginalFilename();
+            String suffix = "";
+            if (originalFilename != null && originalFilename.contains(".")) {
+                suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
+            }
+
+            // 生成文件路径:yyyy/MM/dd/uuid.ext
+            String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
+            String uuid = UUID.randomUUID().toString();
+            String fileName = uuid + suffix;
+            String relativePath = datePath + "/" + fileName;
+
+            // 创建目录
+            Path targetDir = rootPath.resolve(datePath);
+            Files.createDirectories(targetDir);
+
+            // 保存文件
+            Path targetPath = rootPath.resolve(relativePath);
+            file.transferTo(targetPath.toFile());
+
+            // 返回访问URL
+            String url = config.getUrlPrefix() + "/" + relativePath;
+            log.info("[FileStorage] 文件上传成功: {}", url);
+            return url;
+
+        } catch (IOException e) {
+            log.error("[FileStorage] 文件上传失败: {}", e.getMessage());
+            throw new RuntimeException("文件上传失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public void delete(String url) {
+        try {
+            if (url == null || !url.startsWith(config.getUrlPrefix())) {
+                log.warn("[FileStorage] URL不匹配: {}", url);
+                return;
+            }
+
+            String relativePath = url.replace(config.getUrlPrefix() + "/", "");
+            Path filePath = rootPath.resolve(relativePath);
+
+            if (Files.exists(filePath)) {
+                Files.delete(filePath);
+                log.info("[FileStorage] 文件删除成功: {}", url);
+            } else {
+                log.warn("[FileStorage] 文件不存在: {}", url);
+            }
+        } catch (IOException e) {
+            log.error("[FileStorage] 文件删除失败: {}", e.getMessage());
+        }
+    }
+}

+ 258 - 0
src/main/java/com/yingpai/gupiao/service/impl/OrderServiceImpl.java

@@ -0,0 +1,258 @@
+package com.yingpai.gupiao.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.yingpai.gupiao.domain.dto.CreateOrderDTO;
+import com.yingpai.gupiao.domain.po.PaymentOrder;
+import com.yingpai.gupiao.domain.po.UserSubscription;
+import com.yingpai.gupiao.domain.vo.*;
+import com.yingpai.gupiao.mapper.PaymentOrderMapper;
+import com.yingpai.gupiao.mapper.UserSubscriptionMapper;
+import com.yingpai.gupiao.service.OrderService;
+import com.yingpai.gupiao.service.PaymentConfigService;
+import com.yingpai.gupiao.service.WxPayService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+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.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class OrderServiceImpl implements OrderService {
+    
+    private final PaymentConfigService configService;
+    private final PaymentOrderMapper orderMapper;
+    private final UserSubscriptionMapper subscriptionMapper;
+    private final WxPayService wxPayService;
+    
+    private static final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+    
+    @Override
+    public PaymentConfigVO getPaymentConfig(Integer poolType) {
+        return configService.getConfig(poolType);
+    }
+    
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public WxPayVO createOrder(Long userId, String openid, CreateOrderDTO dto) {
+        PaymentConfigVO config = configService.getConfig(dto.getPoolType());
+        
+        String orderNo = generateOrderNo();
+        int amountFen = config.getPrice().multiply(new BigDecimal("100")).intValue();
+
+        PaymentOrder order = PaymentOrder.builder()
+                .orderNo(orderNo)
+                .userId(userId)
+                .openid(openid)
+                .poolType(config.getPoolType())
+                .poolName(config.getPoolName())
+                .amount(config.getPrice())
+                .amountFen(amountFen)
+                .orderStatus(PaymentOrder.STATUS_PENDING)
+                .createTime(LocalDateTime.now())
+                .build();
+        
+        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();
+    }
+    
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void handlePaySuccess(String orderNo, String transactionId) {
+        PaymentOrder order = orderMapper.selectOne(new LambdaQueryWrapper<PaymentOrder>()
+                .eq(PaymentOrder::getOrderNo, orderNo));
+        
+        if (order == null) {
+            log.error("订单不存在: {}", orderNo);
+            return;
+        }
+        
+        if (order.getOrderStatus() == PaymentOrder.STATUS_PAID) {
+            log.info("订单已支付,跳过: {}", orderNo);
+            return;
+        }
+        
+        order.setOrderStatus(PaymentOrder.STATUS_PAID);
+        order.setTransactionId(transactionId);
+        order.setPayTime(LocalDateTime.now());
+        order.setUpdateTime(LocalDateTime.now());
+        orderMapper.updateById(order);
+        
+        createSubscription(order);
+        log.info("支付成功,orderNo: {}", orderNo);
+    }
+    
+    /**
+     * 创建订阅
+     * 超短池:到当日24点
+     * 强势池:1年
+     */
+    private void createSubscription(PaymentOrder order) {
+        LocalDateTime now = LocalDateTime.now();
+        LocalDateTime expireTime;
+        
+        if (order.getPoolType() == 1) {
+            // 超短池:到当日24点
+            expireTime = LocalDateTime.of(LocalDate.now(), LocalTime.MAX);
+        } else {
+            // 强势池:到当年最后一天的12:00
+            expireTime = LocalDateTime.of(now.getYear(), 12, 31, 12, 0, 0);
+        }
+        
+        // 创建新订阅
+        UserSubscription sub = UserSubscription.builder()
+                .userId(order.getUserId())
+                .poolType(order.getPoolType())
+                .orderId(order.getId())
+                .orderNo(order.getOrderNo())
+                .amount(order.getAmount())
+                .startTime(now)
+                .expireTime(expireTime)
+                .status(UserSubscription.STATUS_ACTIVE)
+                .createTime(now)
+                .build();
+        subscriptionMapper.insert(sub);
+        log.info("创建订阅,userId: {}, poolType: {}, expireTime: {}", 
+                order.getUserId(), order.getPoolType(), expireTime);
+    }
+
+    
+    @Override
+    public OrderVO queryOrder(String orderNo) {
+        PaymentOrder order = orderMapper.selectOne(new LambdaQueryWrapper<PaymentOrder>()
+                .eq(PaymentOrder::getOrderNo, orderNo));
+        return order != null ? toOrderVO(order) : null;
+    }
+    
+    @Override
+    public List<OrderVO> getUserOrders(Long userId) {
+        return orderMapper.selectList(new LambdaQueryWrapper<PaymentOrder>()
+                .eq(PaymentOrder::getUserId, userId)
+                .orderByDesc(PaymentOrder::getCreateTime))
+                .stream().map(this::toOrderVO).collect(Collectors.toList());
+    }
+    
+    @Override
+    public List<SubscriptionVO> getUserSubscriptions(Long userId) {
+        LocalDateTime now = LocalDateTime.now();
+        // 返回所有订阅记录,优先显示生效中的(按状态降序、创建时间降序)
+        return subscriptionMapper.selectList(new LambdaQueryWrapper<UserSubscription>()
+                .eq(UserSubscription::getUserId, userId)
+                .orderByDesc(UserSubscription::getStatus)  // 生效中的优先
+                .orderByDesc(UserSubscription::getCreateTime))
+                .stream()
+                .map(sub -> toSubscriptionVO(sub, now))
+                .collect(Collectors.toList());
+    }
+    
+    @Override
+    public boolean hasActiveSubscription(Long userId, Integer poolType) {
+        return subscriptionMapper.findActiveSubscription(userId, poolType, LocalDateTime.now()) != null;
+    }
+    
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void cancelOrder(Long userId, 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_PENDING) 
+            throw new RuntimeException("订单状态不允许取消");
+        
+        order.setOrderStatus(PaymentOrder.STATUS_CANCELLED);
+        order.setCancelTime(LocalDateTime.now());
+        order.setUpdateTime(LocalDateTime.now());
+        orderMapper.updateById(order);
+        log.info("取消订单,orderNo: {}", orderNo);
+    }
+    
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public String verifyAndHandleNotify(String serialNumber, String nonce, String timestamp, String signature, String body) {
+        // 调用支付服务验签解密
+        String result = wxPayService.verifyAndDecryptNotify(serialNumber, nonce, timestamp, signature, body);
+        
+        // 解析结果:orderNo|transactionId|tradeState
+        String[] parts = result.split("\\|");
+        String orderNo = parts[0];
+        String transactionId = parts[1];
+        String tradeState = parts[2];
+        
+        if ("SUCCESS".equals(tradeState)) {
+            handlePaySuccess(orderNo, transactionId);
+        } else {
+            log.warn("支付未成功,orderNo: {}, state: {}", orderNo, tradeState);
+        }
+        
+        return orderNo;
+    }
+    
+    private String generateOrderNo() {
+        String ts = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
+        String uuid = UUID.randomUUID().toString().replace("-", "").substring(0, 8).toUpperCase();
+        return "GP" + ts + uuid;
+    }
+    
+    private OrderVO toOrderVO(PaymentOrder order) {
+        return OrderVO.builder()
+                .orderId(order.getId())
+                .orderNo(order.getOrderNo())
+                .poolType(order.getPoolType())
+                .poolName(order.getPoolName())
+                .amount(order.getAmount())
+                .orderStatus(order.getOrderStatus())
+                .orderStatusName(getStatusName(order.getOrderStatus()))
+                .createTime(order.getCreateTime().format(DTF))
+                .payTime(order.getPayTime() != null ? order.getPayTime().format(DTF) : null)
+                .build();
+    }
+    
+    private SubscriptionVO toSubscriptionVO(UserSubscription sub, LocalDateTime now) {
+        boolean isActive = sub.getStatus() == 1 && sub.getExpireTime().isAfter(now);
+        int remainDays = isActive ? (int) ChronoUnit.DAYS.between(now, sub.getExpireTime()) + 1 : 0;
+        
+        return SubscriptionVO.builder()
+                .id(sub.getId())
+                .poolType(sub.getPoolType())
+                .poolName(sub.getPoolType() == 1 ? "超短池" : "强势池")
+                .amount(sub.getAmount())
+                .startTime(sub.getStartTime().format(DTF))
+                .expireTime(sub.getExpireTime().format(DTF))
+                .isActive(isActive)
+                .remainDays(remainDays)
+                .build();
+    }
+    
+    private String getStatusName(Integer status) {
+        return switch (status) {
+            case 0 -> "待支付";
+            case 1 -> "已支付";
+            case 2 -> "已取消";
+            default -> "未知";
+        };
+    }
+}

+ 0 - 229
src/main/java/com/yingpai/gupiao/service/impl/OssServiceImpl.java

@@ -1,229 +0,0 @@
-package com.yingpai.gupiao.service.impl;
-
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.yingpai.gupiao.domain.po.SysOssConfig;
-import com.yingpai.gupiao.mapper.SysOssConfigMapper;
-import com.yingpai.gupiao.service.OssService;
-import jakarta.annotation.PostConstruct;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-import org.springframework.web.multipart.MultipartFile;
-import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
-import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
-import software.amazon.awssdk.core.sync.RequestBody;
-import software.amazon.awssdk.regions.Region;
-import software.amazon.awssdk.services.s3.S3Client;
-import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
-import software.amazon.awssdk.services.s3.model.PutObjectRequest;
-
-import java.net.URI;
-import java.time.LocalDate;
-import java.time.format.DateTimeFormatter;
-import java.util.UUID;
-
-/**
- * OSS服务实现(读取RuoYi的sys_oss_config配置)
- */
-@Slf4j
-@Service
-@RequiredArgsConstructor
-public class OssServiceImpl implements OssService {
-
-    private final SysOssConfigMapper ossConfigMapper;
-
-    private S3Client s3Client;
-    private SysOssConfig currentConfig;
-
-    /**
-     * 初始化OSS客户端
-     */
-    @PostConstruct
-    public void init() {
-        refreshConfig();
-    }
-
-    /**
-     * 刷新OSS配置
-     */
-    private void refreshConfig() {
-        // 查询默认启用的OSS配置(status=0表示默认)
-        LambdaQueryWrapper<SysOssConfig> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(SysOssConfig::getStatus, "0");
-        SysOssConfig config = ossConfigMapper.selectOne(wrapper);
-
-        if (config == null) {
-            log.warn("[OSS] 未找到默认OSS配置,文件上传将使用本地存储");
-            return;
-        }
-
-        // 配置未变化则不重建客户端
-        if (currentConfig != null && isSameConfig(currentConfig, config)) {
-            return;
-        }
-
-        currentConfig = config;
-        buildS3Client(config);
-        log.info("[OSS] 初始化OSS客户端成功,configKey={}", config.getConfigKey());
-    }
-
-    /**
-     * 构建S3客户端
-     */
-    private void buildS3Client(SysOssConfig config) {
-        try {
-            // trim所有配置值,防止数据库中有空白字符
-            String accessKey = config.getAccessKey() != null ? config.getAccessKey().trim() : "";
-            String secretKey = config.getSecretKey() != null ? config.getSecretKey().trim() : "";
-            String endpoint = config.getEndpoint() != null ? config.getEndpoint().trim() : "";
-            String regionStr = config.getRegion() != null ? config.getRegion().trim() : "";
-
-            StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
-                    AwsBasicCredentials.create(accessKey, secretKey)
-            );
-
-            String endpointUrl = getEndpoint(config);
-            Region region = regionStr != null && !regionStr.isEmpty()
-                    ? Region.of(regionStr)
-                    : Region.US_EAST_1;
-
-            // 判断是否为云服务商(阿里云、腾讯云等使用虚拟主机样式,MinIO使用路径样式)
-            boolean isPathStyle = !isCloudService(endpoint);
-
-            this.s3Client = S3Client.builder()
-                    .credentialsProvider(credentialsProvider)
-                    .endpointOverride(URI.create(endpointUrl))
-                    .region(region)
-                    .forcePathStyle(isPathStyle)
-                    .build();
-
-        } catch (Exception e) {
-            log.error("[OSS] 构建S3客户端失败: {}", e.getMessage());
-            this.s3Client = null;
-        }
-    }
-
-    @Override
-    public String upload(MultipartFile file) {
-        // 每次上传前刷新配置(支持后台动态修改)
-        refreshConfig();
-
-        if (s3Client == null || currentConfig == null) {
-            throw new RuntimeException("OSS未配置或配置错误");
-        }
-
-        try {
-            String originalFilename = file.getOriginalFilename();
-            String suffix = originalFilename != null && originalFilename.contains(".")
-                    ? originalFilename.substring(originalFilename.lastIndexOf("."))
-                    : "";
-
-            // 生成文件路径:prefix/yyyy/MM/dd/uuid.ext
-            String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
-            String uuid = UUID.randomUUID().toString().replace("-", "");
-            String key = (currentConfig.getPrefix() != null && !currentConfig.getPrefix().isEmpty()
-                    ? currentConfig.getPrefix() + "/" : "")
-                    + datePath + "/" + uuid + suffix;
-
-            // 上传文件
-            PutObjectRequest putRequest = PutObjectRequest.builder()
-                    .bucket(currentConfig.getBucketName())
-                    .key(key)
-                    .contentType(file.getContentType())
-                    .build();
-
-            s3Client.putObject(putRequest, RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
-
-            // 返回访问URL
-            String url = getAccessUrl(currentConfig) + "/" + key;
-            log.info("[OSS] 文件上传成功: {}", url);
-            return url;
-
-        } catch (Exception e) {
-            log.error("[OSS] 文件上传失败: {}", e.getMessage());
-            throw new RuntimeException("文件上传失败: " + e.getMessage());
-        }
-    }
-
-    @Override
-    public void delete(String url) {
-        if (s3Client == null || currentConfig == null) {
-            log.warn("[OSS] OSS未配置,无法删除文件");
-            return;
-        }
-
-        try {
-            String baseUrl = getAccessUrl(currentConfig) + "/";
-            if (!url.startsWith(baseUrl)) {
-                log.warn("[OSS] URL不匹配当前OSS配置: {}", url);
-                return;
-            }
-
-            String key = url.replace(baseUrl, "");
-            DeleteObjectRequest deleteRequest = DeleteObjectRequest.builder()
-                    .bucket(currentConfig.getBucketName())
-                    .key(key)
-                    .build();
-
-            s3Client.deleteObject(deleteRequest);
-            log.info("[OSS] 文件删除成功: {}", url);
-
-        } catch (Exception e) {
-            log.error("[OSS] 文件删除失败: {}", e.getMessage());
-        }
-    }
-
-    /**
-     * 获取endpoint(带协议)
-     */
-    private String getEndpoint(SysOssConfig config) {
-        String protocol = "1".equals(config.getIsHttps()) ? "https://" : "http://";
-        return protocol + config.getEndpoint();
-    }
-
-    /**
-     * 获取访问URL
-     */
-    private String getAccessUrl(SysOssConfig config) {
-        String protocol = "1".equals(config.getIsHttps()) ? "https://" : "http://";
-
-        // 如果配置了自定义域名
-        if (config.getDomain() != null && !config.getDomain().isEmpty()) {
-            String domain = config.getDomain();
-            if (domain.startsWith("http://") || domain.startsWith("https://")) {
-                return domain;
-            }
-            return protocol + domain;
-        }
-
-        // 云服务商使用虚拟主机样式:bucket.endpoint
-        if (isCloudService(config.getEndpoint())) {
-            return protocol + config.getBucketName() + "." + config.getEndpoint();
-        }
-
-        // MinIO等使用路径样式:endpoint/bucket
-        return protocol + config.getEndpoint() + "/" + config.getBucketName();
-    }
-
-    /**
-     * 判断是否为云服务商
-     */
-    private boolean isCloudService(String endpoint) {
-        if (endpoint == null) return false;
-        return endpoint.contains("aliyuncs.com")
-                || endpoint.contains("myqcloud.com")
-                || endpoint.contains("qiniucs.com")
-                || endpoint.contains("amazonaws.com");
-    }
-
-    /**
-     * 判断配置是否相同
-     */
-    private boolean isSameConfig(SysOssConfig c1, SysOssConfig c2) {
-        return c1.getOssConfigId().equals(c2.getOssConfigId())
-                && c1.getAccessKey().equals(c2.getAccessKey())
-                && c1.getSecretKey().equals(c2.getSecretKey())
-                && c1.getBucketName().equals(c2.getBucketName())
-                && c1.getEndpoint().equals(c2.getEndpoint());
-    }
-}

+ 54 - 0
src/main/java/com/yingpai/gupiao/service/impl/PaymentConfigServiceImpl.java

@@ -0,0 +1,54 @@
+package com.yingpai.gupiao.service.impl;
+
+import com.yingpai.gupiao.domain.vo.PaymentConfigVO;
+import com.yingpai.gupiao.mapper.SysConfigMapper;
+import com.yingpai.gupiao.service.PaymentConfigService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+
+/**
+ * 支付配置服务实现(从sys_config读取)
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class PaymentConfigServiceImpl implements PaymentConfigService {
+    
+    private final SysConfigMapper sysConfigMapper;
+    
+    // 配置key
+    private static final String SHORT_PRICE_KEY = "payment.short.price";
+    private static final String STRONG_PRICE_KEY = "payment.strong.price";
+    
+    @Override
+    public PaymentConfigVO getConfig(Integer poolType) {
+        if (poolType == 1) {
+            // 超短池:到当日24点
+            String priceStr = sysConfigMapper.getConfigValue(SHORT_PRICE_KEY);
+            BigDecimal price = priceStr != null ? new BigDecimal(priceStr) : new BigDecimal("18");
+            
+            return PaymentConfigVO.builder()
+                    .poolType(1)
+                    .poolName("超短池")
+                    .price(price)
+                    .description("有效期至当日24:00")
+                    .build();
+        } else if (poolType == 2) {
+            // 强势池:1年
+            String priceStr = sysConfigMapper.getConfigValue(STRONG_PRICE_KEY);
+            BigDecimal price = priceStr != null ? new BigDecimal(priceStr) : new BigDecimal("998");
+            
+            return PaymentConfigVO.builder()
+                    .poolType(2)
+                    .poolName("强势池")
+                    .price(price)
+                    .description("有效期1年")
+                    .build();
+        }
+        
+        throw new RuntimeException("无效的池类型");
+    }
+}

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

@@ -0,0 +1,54 @@
+package com.yingpai.gupiao.service.impl;
+
+import com.yingpai.gupiao.mapper.SysConfigMapper;
+import com.yingpai.gupiao.service.WxPayConfigService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+/**
+ * 微信支付配置服务实现(从sys_config读取商户配置)
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class WxPayConfigServiceImpl implements WxPayConfigService {
+    
+    private final SysConfigMapper sysConfigMapper;
+    
+    private static final String MCH_ID_KEY = "payment.mch.id";
+    private static final String API_V3_KEY = "payment.api.v3.key";
+    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";
+    
+    @Override
+    public String getMchId() {
+        return getConfigValue(MCH_ID_KEY);
+    }
+    
+    @Override
+    public String getApiV3Key() {
+        return getConfigValue(API_V3_KEY);
+    }
+    
+    @Override
+    public String getCertPath() {
+        return getConfigValue(CERT_PATH_KEY);
+    }
+    
+    @Override
+    public String getNotifyUrl() {
+        return getConfigValue(NOTIFY_URL_KEY);
+    }
+    
+    @Override
+    public String getPrivateKeyPath() {
+        return getConfigValue(PRIVATE_KEY_PATH_KEY);
+    }
+    
+    private String getConfigValue(String key) {
+        String value = sysConfigMapper.getConfigValue(key);
+        return value != null ? value : "";
+    }
+}

+ 274 - 0
src/main/java/com/yingpai/gupiao/service/impl/WxPayServiceImpl.java

@@ -0,0 +1,274 @@
+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.notification.NotificationConfig;
+import com.wechat.pay.java.core.notification.NotificationParser;
+import com.wechat.pay.java.core.notification.RequestParam;
+import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
+import com.wechat.pay.java.service.payments.jsapi.model.*;
+import com.wechat.pay.java.service.payments.model.Transaction;
+import com.yingpai.gupiao.config.WxConfig;
+import com.yingpai.gupiao.domain.vo.WxPayVO;
+import com.yingpai.gupiao.service.WxPayConfigService;
+import com.yingpai.gupiao.service.WxPayService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.stereotype.Service;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.stream.Collectors;
+
+/**
+ * 微信支付服务实现(微信支付SDK)
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class WxPayServiceImpl implements WxPayService {
+    
+    private final WxConfig wxConfig;
+    private final WxPayConfigService wxPayConfigService;
+    
+    private Config config;
+    private JsapiServiceExtension jsapiService;
+    private NotificationParser notificationParser;
+    
+    // 缓存当前配置的hash,用于检测配置变化
+    private String currentConfigHash = "";
+    
+    /**
+     * 获取配置hash,用于检测配置是否变化
+     */
+    private String getConfigHash() {
+        String mchId = wxPayConfigService.getMchId();
+        String apiV3Key = wxPayConfigService.getApiV3Key();
+        String privateKeyPath = wxPayConfigService.getPrivateKeyPath();
+        String certPath = wxPayConfigService.getCertPath();
+        return mchId + "|" + apiV3Key + "|" + privateKeyPath + "|" + certPath;
+    }
+    
+    /**
+     * 确保SDK已初始化,配置变化时自动重新初始化
+     */
+    private synchronized void ensureInitialized() {
+        String newHash = getConfigHash();
+        if (!newHash.equals(currentConfigHash)) {
+            log.info("检测到支付配置变化,重新初始化SDK");
+            init();
+            currentConfigHash = newHash;
+        }
+    }
+    
+    private void init() {
+        try {
+            String mchId = wxPayConfigService.getMchId();
+            String apiV3Key = wxPayConfigService.getApiV3Key();
+            String privateKeyPath = wxPayConfigService.getPrivateKeyPath();
+            String certPath = wxPayConfigService.getCertPath();
+            
+            if (mchId.isEmpty() || apiV3Key.isEmpty() || privateKeyPath.isEmpty() || certPath.isEmpty()) {
+                log.warn("微信支付配置不完整,跳过初始化");
+                config = null;
+                jsapiService = null;
+                notificationParser = null;
+                return;
+            }
+            
+            // 读取私钥
+            String privateKey = loadPrivateKey(privateKeyPath);
+            
+            // 从证书文件自动读取序列号
+            String mchSerialNo = loadCertSerialNo(certPath);
+            log.info("从证书读取序列号: {}", mchSerialNo);
+            
+            // 构建配置(自动获取微信平台证书)
+            config = new RSAAutoCertificateConfig.Builder()
+                    .merchantId(mchId)
+                    .privateKey(privateKey)
+                    .merchantSerialNumber(mchSerialNo)
+                    .apiV3Key(apiV3Key)
+                    .build();
+            
+            // 初始化JSAPI服务
+            jsapiService = new JsapiServiceExtension.Builder().config(config).build();
+            
+            // 初始化回调解析器
+            notificationParser = new NotificationParser((NotificationConfig) config);
+            
+            log.info("微信支付SDK初始化成功,商户号: {}", mchId);
+        } catch (Exception e) {
+            log.error("微信支付SDK初始化失败", e);
+            config = null;
+            jsapiService = null;
+            notificationParser = null;
+        }
+    }
+
+    @Override
+    public WxPayVO createPrepayOrder(String orderNo, String openid, Integer amount, String description) {
+        log.info("创建预支付订单,orderNo: {}, openid: {}, amount: {}分", orderNo, openid, amount);
+        
+        // 确保SDK已初始化,配置变化时自动重新初始化
+        ensureInitialized();
+        
+        if (jsapiService == null) {
+            throw new RuntimeException("微信支付未初始化,请检查配置");
+        }
+        
+        String notifyUrl = wxPayConfigService.getNotifyUrl();
+        
+        // 构建下单请求
+        PrepayRequest request = new PrepayRequest();
+        request.setAppid(wxConfig.getAppid());
+        request.setMchid(wxPayConfigService.getMchId());
+        request.setDescription(description);
+        request.setOutTradeNo(orderNo);
+        request.setNotifyUrl(notifyUrl);
+        
+        Amount amountObj = new Amount();
+        amountObj.setTotal(amount);
+        amountObj.setCurrency("CNY");
+        request.setAmount(amountObj);
+        
+        Payer payer = new Payer();
+        payer.setOpenid(openid);
+        request.setPayer(payer);
+        
+        // 调用JSAPI下单并获取调起支付参数
+        PrepayWithRequestPaymentResponse response = jsapiService.prepayWithRequestPayment(request);
+        
+        log.info("预支付成功,orderNo: {}", orderNo);
+        
+        return WxPayVO.builder()
+                .orderNo(orderNo)
+                .timeStamp(response.getTimeStamp())
+                .nonceStr(response.getNonceStr())
+                .packageValue(response.getPackageVal())
+                .signType(response.getSignType())
+                .paySign(response.getPaySign())
+                .build();
+    }
+    
+    @Override
+    public String verifyAndDecryptNotify(String serialNumber, String nonce, String timestamp,
+                                          String signature, String body) {
+        log.info("验证支付回调,serialNumber: {}", serialNumber);
+        
+        // 确保SDK已初始化
+        ensureInitialized();
+        
+        if (notificationParser == null) {
+            throw new RuntimeException("微信支付未初始化");
+        }
+        
+        // 构建回调参数
+        RequestParam requestParam = new RequestParam.Builder()
+                .serialNumber(serialNumber)
+                .nonce(nonce)
+                .timestamp(timestamp)
+                .signature(signature)
+                .body(body)
+                .build();
+        
+        // 解析并验证回调(SDK自动验签和解密)
+        Transaction transaction = notificationParser.parse(requestParam, Transaction.class);
+        
+        log.info("回调验证成功,orderNo: {}, transactionId: {}, state: {}",
+                transaction.getOutTradeNo(), transaction.getTransactionId(), transaction.getTradeState());
+        
+        return transaction.getOutTradeNo() + "|" + transaction.getTransactionId() + "|" + transaction.getTradeState();
+    }
+    
+    @Override
+    public String queryPaymentStatus(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);
+        
+        Transaction transaction = jsapiService.queryOrderByOutTradeNo(request);
+        
+        return transaction.getTradeState().name();
+    }
+    
+    @Override
+    public boolean refund(String orderNo, String refundNo, Integer totalAmount,
+                          Integer refundAmount, String reason) {
+        throw new UnsupportedOperationException("不支持退款");
+    }
+    
+    /**
+     * 加载私钥文件
+     */
+    private String loadPrivateKey(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);
+        }
+    }
+    
+    /**
+     * 从证书文件读取序列号
+     */
+    private String loadCertSerialNo(String certPath) {
+        try {
+            String certContent;
+            if (certPath.startsWith("classpath:")) {
+                String resourcePath = certPath.replace("classpath:", "");
+                ClassPathResource resource = new ClassPathResource(resourcePath);
+                try (BufferedReader reader = new BufferedReader(
+                        new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
+                    certContent = reader.lines().collect(Collectors.joining("\n"));
+                }
+            } else {
+                certContent = java.nio.file.Files.readString(java.nio.file.Paths.get(certPath));
+            }
+            
+            // 解析PEM格式证书
+            String certPem = certContent
+                    .replace("-----BEGIN CERTIFICATE-----", "")
+                    .replace("-----END CERTIFICATE-----", "")
+                    .replaceAll("\\s", "");
+            
+            byte[] certBytes = java.util.Base64.getDecoder().decode(certPem);
+            CertificateFactory cf = CertificateFactory.getInstance("X.509");
+            try (InputStream is = new ByteArrayInputStream(certBytes)) {
+                X509Certificate cert = (X509Certificate) cf.generateCertificate(is);
+                // 获取序列号并转为16进制大写
+                String serialNo = cert.getSerialNumber().toString(16).toUpperCase();
+                log.info("证书序列号: {}", serialNo);
+                return serialNo;
+            }
+        } catch (Exception e) {
+            log.error("读取证书序列号失败: {}", certPath, e);
+            throw new RuntimeException("读取证书序列号失败", e);
+        }
+    }
+}

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

@@ -6,6 +6,12 @@ wx:
   appid: wxcf9eec0da6a6b696
   secret: 2808024de706bfb74d0fe1a111dfa00d
 
+# 文件存储配置
+file:
+  storage:
+    path: ./uploads                    # 文件存储路径(相对或绝对路径)
+    url-prefix: /uploads               # 文件访问URL前缀
+
 spring:
   application:
     name: gupiao

+ 28 - 0
src/main/resources/sql/payment_order.sql

@@ -0,0 +1,28 @@
+
+-- ----------------------------
+-- 订单表
+-- ----------------------------
+DROP TABLE IF EXISTS `payment_order`;
+CREATE TABLE `payment_order` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单ID',
+    `order_no` VARCHAR(32) NOT NULL COMMENT '订单号',
+    `user_id` BIGINT NOT NULL COMMENT '用户ID',
+    `openid` VARCHAR(64) NOT NULL COMMENT '用户openid',
+    `pool_type` TINYINT NOT NULL COMMENT '池类型:1-超短池,2-强势池',
+    `pool_name` VARCHAR(50) NOT NULL COMMENT '池名称',
+    `amount` DECIMAL(10, 2) NOT NULL COMMENT '订单金额(元)',
+    `amount_fen` INT NOT NULL COMMENT '订单金额(分)',
+    `order_status` TINYINT DEFAULT 0 COMMENT '订单状态:0-待支付,1-已支付,2-已取消',
+    `transaction_id` VARCHAR(64) DEFAULT NULL COMMENT '微信支付订单号',
+    `pay_time` DATETIME DEFAULT NULL COMMENT '支付时间',
+    `cancel_time` DATETIME DEFAULT NULL COMMENT '取消时间',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_order_no` (`order_no`),
+    KEY `idx_user_id` (`user_id`),
+    KEY `idx_pool_type` (`pool_type`),
+    KEY `idx_order_status` (`order_status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付订单表';
+
+

+ 22 - 0
src/main/resources/sql/user_subscription.sql

@@ -0,0 +1,22 @@
+-- ----------------------------
+-- 用户订阅表
+-- ----------------------------
+DROP TABLE IF EXISTS `user_subscription`;
+CREATE TABLE `user_subscription` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '订阅ID',
+    `user_id` BIGINT NOT NULL COMMENT '用户ID',
+    `pool_type` TINYINT NOT NULL COMMENT '池类型:1-超短池,2-强势池',
+    `order_id` BIGINT NOT NULL COMMENT '关联订单ID',
+    `order_no` VARCHAR(32) NOT NULL COMMENT '关联订单号',
+    `amount` DECIMAL(10, 2) NOT NULL COMMENT '支付金额',
+    `start_time` DATETIME NOT NULL COMMENT '订阅开始时间',
+    `expire_time` DATETIME NOT NULL COMMENT '订阅到期时间',
+    `status` TINYINT DEFAULT 1 COMMENT '状态:1-有效,0-已过期',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    KEY `idx_user_id` (`user_id`),
+    KEY `idx_pool_type` (`pool_type`),
+    KEY `idx_expire_time` (`expire_time`),
+    KEY `idx_user_pool` (`user_id`, `pool_type`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户订阅表';

二进制
uploads/2025/12/29/bc4f6a5f-46a0-4c4c-b912-803cb16837dd.jpeg


二进制
uploads/2025/12/29/fa44cdc6-5def-4615-be4f-7271a7b10d69.jpeg