params = AliPayApi.toMap(request);
+
+ // 验证签名
+ boolean verifyResult = AlipaySignature.rsaCertCheckV1(
+ params, aliPayConfig.getAliPayCertPath(), "UTF-8", "RSA2");
+
+ if (verifyResult) {
+ // 处理支付成功逻辑
+ return "success";
+ } else {
+ return "failure";
+ }
+}
+```
+
+## 三、IJPay 配置说明
+
+### 3.1 配置方式
+
+IJPay 的 `AliPayApi` 类内部会自动读取系统属性或配置文件中的支付宝配置。
+
+需要配置的参数包括:
+- `appId`: 支付宝应用ID
+- `privateKey`: 应用私钥
+- `publicKey`: 支付宝公钥
+- `gatewayUrl`: 支付宝网关地址
+- `charset`: 字符编码(通常为 UTF-8)
+- `signType`: 签名类型(通常为 RSA2)
+
+### 3.2 配置文件
+
+配置在 `application-dev.properties` 或 `application-prod.properties` 中:
+
+```properties
+# 支付宝配置
+alipay.app-id=你的APPID
+alipay.private-key=你的应用私钥
+alipay.public-key=支付宝公钥
+alipay.gateway-url=https://openapi.alipaydev.com/gateway.do
+alipay.charset=UTF-8
+alipay.sign-type=RSA2
+alipay.notify-url=回调通知地址
+alipay.return-url=同步返回地址
+```
+
+### 3.3 IJPay 初始化(可选)
+
+如果需要显式初始化 IJPay,可以在应用启动时配置:
+
+```java
+@Configuration
+public class IJPayConfig {
+
+ @Autowired
+ private AliPayConfig aliPayConfig;
+
+ @PostConstruct
+ public void init() {
+ // IJPay 的 AliPayApi 会自动读取配置
+ // 如果需要显式配置,可以使用 AliPayApiConfigKit
+ // 注意:具体初始化方式取决于 IJPay 版本
+ }
+}
+```
+
+## 四、IJPay vs 原生 Alipay SDK
+
+项目中同时使用了两种方式:
+
+1. **IJPay (AliPayApi)** - 在 `AlipayController.java` 中使用
+ - 优点:封装更简洁,使用更方便
+ - 缺点:需要额外依赖 IJPay 库
+
+2. **原生 Alipay SDK (DefaultAlipayClient)** - 在 `AlipayService.java` 中使用
+ - 优点:官方 SDK,功能完整
+ - 缺点:需要手动创建客户端和处理响应
+
+## 五、常见问题
+
+### Q1: IJPay 如何初始化?
+
+A: IJPay 的 `AliPayApi` 类内部会自动读取配置,通常不需要显式初始化。如果遇到配置问题,可以检查:
+1. 配置文件中的参数是否正确
+2. 系统属性是否设置
+3. 环境变量是否配置
+
+### Q2: 如何切换到 IJPay?
+
+A: 如果当前使用原生 Alipay SDK,可以:
+1. 修改 `AlipayService.java`,使用 `AliPayApi` 替代 `DefaultAlipayClient`
+2. 或者直接使用 `AlipayController` 中的接口
+
+### Q3: IJPay 和原生 SDK 的区别?
+
+A: IJPay 是对原生 SDK 的封装,提供了更简洁的 API。底层实现仍然使用支付宝官方 SDK。
+
+## 六、参考文档
+
+- IJPay 官方文档:https://github.com/Javen205/IJPay
+- IJPay 示例项目:https://github.com/Javen205/IJPay-Demo
+- 支付宝开放平台:https://open.alipay.com
+
+
diff --git a/demo/IMAGE_TO_VIDEO_API_README.md b/demo/IMAGE_TO_VIDEO_API_README.md
index b76c629..e434f9f 100644
--- a/demo/IMAGE_TO_VIDEO_API_README.md
+++ b/demo/IMAGE_TO_VIDEO_API_README.md
@@ -293,3 +293,4 @@ grep "img2vid_abc123def456" logs/application.log
+
diff --git a/demo/POINTS_FREEZE_SYSTEM_README.md b/demo/POINTS_FREEZE_SYSTEM_README.md
index 518383f..170a426 100644
--- a/demo/POINTS_FREEZE_SYSTEM_README.md
+++ b/demo/POINTS_FREEZE_SYSTEM_README.md
@@ -294,3 +294,4 @@ public TaskQueue addTextToVideoTask(String username, String taskId) {
+
diff --git a/demo/PasswordChecker.java b/demo/PasswordChecker.java
index 8510ed2..2005028 100644
--- a/demo/PasswordChecker.java
+++ b/demo/PasswordChecker.java
@@ -37,5 +37,6 @@ public class PasswordChecker {
+
diff --git a/demo/TENCENT_SES_STARTUP_CHECKLIST.md b/demo/TENCENT_SES_STARTUP_CHECKLIST.md
index 2b9b992..564db90 100644
--- a/demo/TENCENT_SES_STARTUP_CHECKLIST.md
+++ b/demo/TENCENT_SES_STARTUP_CHECKLIST.md
@@ -285,3 +285,4 @@ ResourceNotFound.TemplateNotFound
+
diff --git a/demo/TEXT_TO_VIDEO_API_README.md b/demo/TEXT_TO_VIDEO_API_README.md
index 5d1a85c..a239715 100644
--- a/demo/TEXT_TO_VIDEO_API_README.md
+++ b/demo/TEXT_TO_VIDEO_API_README.md
@@ -303,3 +303,4 @@ const startPolling = (taskId) => {
+
diff --git a/demo/USER_WORKS_SYSTEM_README.md b/demo/USER_WORKS_SYSTEM_README.md
index c8c2a88..c884bd3 100644
--- a/demo/USER_WORKS_SYSTEM_README.md
+++ b/demo/USER_WORKS_SYSTEM_README.md
@@ -173,3 +173,4 @@ const updateWork = async (workId, updateData) => {
+
diff --git a/demo/frontend/README.md b/demo/frontend/README.md
index 3be8588..cbef172 100644
--- a/demo/frontend/README.md
+++ b/demo/frontend/README.md
@@ -437,5 +437,6 @@ MIT License
+
diff --git a/demo/frontend/src/App-backup.vue b/demo/frontend/src/App-backup.vue
index a83e595..2d9a1ba 100644
--- a/demo/frontend/src/App-backup.vue
+++ b/demo/frontend/src/App-backup.vue
@@ -35,3 +35,4 @@ console.log('App.vue 加载成功')
+
diff --git a/demo/frontend/src/api/userWorks.js b/demo/frontend/src/api/userWorks.js
index 3536222..e44416e 100644
--- a/demo/frontend/src/api/userWorks.js
+++ b/demo/frontend/src/api/userWorks.js
@@ -61,3 +61,4 @@ export const getWorkStats = () => {
+
diff --git a/demo/frontend/src/components/Footer.vue b/demo/frontend/src/components/Footer.vue
index 3a10c6f..0f53109 100644
--- a/demo/frontend/src/components/Footer.vue
+++ b/demo/frontend/src/components/Footer.vue
@@ -96,5 +96,6 @@
+
diff --git a/demo/frontend/src/components/PaymentModal.vue b/demo/frontend/src/components/PaymentModal.vue
index 2b0422d..3cbc736 100644
--- a/demo/frontend/src/components/PaymentModal.vue
+++ b/demo/frontend/src/components/PaymentModal.vue
@@ -9,6 +9,8 @@
:close-on-press-escape="true"
@close="handleClose"
center
+ :show-close="true"
+ custom-class="payment-modal-dialog"
>
@@ -206,11 +208,46 @@ const showAgreement = () => {
background: #0a0a0a;
}
-.payment-modal :deep(.el-dialog) {
- background: #0a0a0a;
+/* 移除所有可能的白边和边框 */
+.payment-modal :deep(.el-dialog),
+.payment-modal-dialog {
+ background: #0a0a0a !important;
border-radius: 12px;
- border: none;
+ border: none !important;
+ border-width: 0 !important;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
+ padding: 0 !important;
+ margin: 0 !important;
+ outline: none !important;
+ box-sizing: border-box !important;
+ overflow: hidden !important;
+}
+
+.payment-modal :deep(.el-dialog__body) {
+ padding: 0 !important;
+ margin: 0 !important;
+ background: #0a0a0a !important;
+ border: none !important;
+ overflow: hidden !important;
+}
+
+.payment-modal :deep(.el-dialog__wrapper) {
+ background: transparent !important;
+ padding: 0 !important;
+ margin: 0 !important;
+}
+
+.payment-modal :deep(.el-overlay) {
+ background: transparent !important;
+}
+
+.payment-modal :deep(.el-overlay-dialog) {
+ background: transparent !important;
+}
+
+/* 确保遮罩层没有白边 */
+.payment-modal :deep(.el-overlay.is-message-box) {
+ background: transparent !important;
}
.payment-modal :deep(.el-dialog__header) {
@@ -237,6 +274,7 @@ const showAgreement = () => {
padding: 24px;
background: #0a0a0a;
color: white;
+ margin: 0;
}
/* 支付方式选择 */
diff --git a/demo/src/main/java/com/example/demo/DemoApplication.java b/demo/src/main/java/com/example/demo/DemoApplication.java
index 56c0d4b..96552d6 100644
--- a/demo/src/main/java/com/example/demo/DemoApplication.java
+++ b/demo/src/main/java/com/example/demo/DemoApplication.java
@@ -11,6 +11,12 @@ import org.springframework.scheduling.annotation.EnableScheduling;
public class DemoApplication {
public static void main(String[] args) {
+ // 在应用启动时立即设置HTTP超时时间(支付宝API调用可能需要更长时间)
+ // 连接超时:30秒,读取超时:120秒
+ // 必须在SpringApplication.run()之前设置,确保在所有HTTP客户端创建之前生效
+ System.setProperty("sun.net.client.defaultConnectTimeout", "30000");
+ System.setProperty("sun.net.client.defaultReadTimeout", "120000");
+
SpringApplication.run(DemoApplication.class, args);
}
diff --git a/demo/src/main/java/com/example/demo/config/PaymentConfig.java b/demo/src/main/java/com/example/demo/config/PaymentConfig.java
index a0e0857..2756e14 100644
--- a/demo/src/main/java/com/example/demo/config/PaymentConfig.java
+++ b/demo/src/main/java/com/example/demo/config/PaymentConfig.java
@@ -1,17 +1,27 @@
package com.example.demo.config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.PropertySource;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.lang.NonNull;
+
+import com.ijpay.alipay.AliPayApiConfig;
+import com.ijpay.alipay.AliPayApiConfigKit;
/**
* 支付配置类
* 集成IJPay支付模块配置
+ *
+ * 初始化IJPay的AliPayApiConfigKit,确保AliPayApi可以正常使用
*/
@Configuration
-@PropertySource("classpath:payment.properties")
-public class PaymentConfig {
+public class PaymentConfig implements ApplicationListener {
+
+ private static final Logger logger = LoggerFactory.getLogger(PaymentConfig.class);
@Bean
@ConfigurationProperties(prefix = "alipay")
@@ -19,6 +29,263 @@ public class PaymentConfig {
return new AliPayConfig();
}
+ /**
+ * 初始化IJPay配置
+ * 在应用上下文完全初始化后执行,确保所有bean都已创建完成
+ */
+ @Override
+ public void onApplicationEvent(@NonNull ContextRefreshedEvent event) {
+ // 确保只在根上下文触发时初始化,避免子上下文重复触发
+ if (event.getApplicationContext().getParent() != null) {
+ return;
+ }
+
+ // 从ApplicationContext获取已创建的bean(此时所有bean都已创建完成)
+ AliPayConfig aliPayConfig;
+ try {
+ aliPayConfig = event.getApplicationContext().getBean(AliPayConfig.class);
+ } catch (Exception e) {
+ logger.error("无法从ApplicationContext获取AliPayConfig bean", e);
+ return;
+ }
+
+ try {
+ logger.info("=== 初始化IJPay配置 ===");
+
+ // 详细的配置验证
+ boolean configValid = validateAlipayConfig(aliPayConfig);
+ if (!configValid) {
+ logger.error("支付宝配置验证失败,请检查配置");
+ return;
+ }
+
+ logger.info("应用ID: {}", aliPayConfig.getAppId());
+ logger.info("网关地址: {}", aliPayConfig.getServerUrl());
+ logger.info("字符集: {}", aliPayConfig.getCharset());
+ logger.info("签名类型: {}", aliPayConfig.getSignType());
+ logger.info("通知URL: {}", aliPayConfig.getNotifyUrl());
+ logger.info("返回URL: {}", aliPayConfig.getReturnUrl());
+
+ // HTTP超时时间已在应用启动时设置(DemoApplication.main方法中)
+ // 连接超时:30秒,读取超时:120秒
+ logger.info("HTTP超时设置:连接超时30秒,读取超时120秒(已在应用启动时设置)");
+
+ // 构建AliPayApiConfig对象
+ // 注意:DefaultAlipayClient会在build()方法中创建,此时会使用上面设置的系统属性
+ AliPayApiConfig apiConfig = AliPayApiConfig.builder()
+ .setAppId(aliPayConfig.getAppId())
+ .setPrivateKey(aliPayConfig.getPrivateKey())
+ .setAliPayPublicKey(aliPayConfig.getPublicKey())
+ .setServiceUrl(aliPayConfig.getServerUrl())
+ .setCharset(aliPayConfig.getCharset() != null ? aliPayConfig.getCharset() : "UTF-8")
+ .setSignType(aliPayConfig.getSignType() != null ? aliPayConfig.getSignType() : "RSA2")
+ .build();
+
+ // 验证配置中的serviceUrl是否正确
+ logger.info("=== 验证IJPay配置中的网关地址 ===");
+ logger.info("配置中的serviceUrl: {}", apiConfig.getServiceUrl());
+ logger.info("配置中的appId: {}", apiConfig.getAppId());
+
+ // 保存配置到静态变量,供AlipayService使用(备用方案)
+ AliPayApiConfigHolder.setConfig(apiConfig);
+
+ // 设置到AliPayApiConfigKit中,供AliPayApi使用
+ // 根据IJPay源码,正确的方法是 putApiConfig
+ try {
+ AliPayApiConfigKit.putApiConfig(apiConfig);
+ logger.info("IJPay配置已成功设置到AliPayApiConfigKit");
+
+ // 验证从AliPayApiConfigKit获取的配置
+ AliPayApiConfig retrievedConfig = AliPayApiConfigKit.getAliPayApiConfig();
+ logger.info("从AliPayApiConfigKit获取的serviceUrl: {}", retrievedConfig.getServiceUrl());
+ logger.info("从AliPayApiConfigKit获取的appId: {}", retrievedConfig.getAppId());
+
+ logger.info("IJPay配置初始化完成");
+ } catch (Exception e) {
+ logger.error("设置IJPay配置到AliPayApiConfigKit时发生异常", e);
+ // 不抛出异常,允许应用启动,配置将在调用时动态设置
+ logger.warn("IJPay配置设置失败,但应用将继续启动,配置将在调用时动态设置");
+ }
+ } catch (Exception e) {
+ logger.error("IJPay配置初始化失败", e);
+ // 不抛出异常,允许应用启动,配置将在调用时动态设置
+ logger.warn("IJPay配置初始化失败,但应用将继续启动,配置将在调用时动态设置");
+ }
+ }
+
+ /**
+ * 验证支付宝配置
+ * @param config 支付宝配置
+ * @return 验证结果
+ */
+ private boolean validateAlipayConfig(AliPayConfig config) {
+ logger.info("=== 开始验证支付宝配置 ===");
+
+ boolean isValid = true;
+ StringBuilder errors = new StringBuilder();
+
+ // 1. 验证应用ID
+ if (config.getAppId() == null || config.getAppId().isEmpty()) {
+ logger.error("❌ 应用ID (app-id) 为空");
+ errors.append("应用ID为空; ");
+ isValid = false;
+ } else {
+ // 验证appId格式(应该是16位数字)
+ String appId = config.getAppId().trim();
+ if (!appId.matches("^\\d{16}$")) {
+ logger.warn("⚠️ 应用ID格式可能不正确: {} (应该是16位数字)", appId);
+ } else {
+ logger.info("✅ 应用ID格式正确: {}", appId);
+ }
+ }
+
+ // 2. 验证私钥
+ if (config.getPrivateKey() == null || config.getPrivateKey().isEmpty()) {
+ logger.error("❌ 应用私钥 (private-key) 为空");
+ errors.append("应用私钥为空; ");
+ isValid = false;
+ } else {
+ String privateKey = config.getPrivateKey().trim();
+ // 验证私钥格式(RSA私钥通常以BEGIN PRIVATE KEY或BEGIN RSA PRIVATE KEY开头)
+ if (!privateKey.startsWith("MII") && !privateKey.contains("BEGIN")) {
+ logger.warn("⚠️ 私钥格式可能不正确(应该以MII开头或包含BEGIN PRIVATE KEY)");
+ logger.debug("私钥前20个字符: {}", privateKey.substring(0, Math.min(20, privateKey.length())));
+ }
+ // 验证私钥长度(RSA私钥通常至少1000字符)
+ if (privateKey.length() < 500) {
+ logger.error("❌ 私钥长度过短: {} 字符(RSA私钥通常至少500字符)", privateKey.length());
+ errors.append("私钥长度过短; ");
+ isValid = false;
+ } else {
+ logger.info("✅ 私钥长度: {} 字符", privateKey.length());
+ // 隐藏敏感信息,只显示前后部分
+ String maskedKey = privateKey.substring(0, 20) + "..." + privateKey.substring(privateKey.length() - 20);
+ logger.debug("私钥摘要: {}", maskedKey);
+ }
+ }
+
+ // 3. 验证公钥
+ if (config.getPublicKey() == null || config.getPublicKey().isEmpty()) {
+ logger.error("❌ 支付宝公钥 (public-key) 为空");
+ errors.append("支付宝公钥为空; ");
+ isValid = false;
+ } else {
+ String publicKey = config.getPublicKey().trim();
+ // 验证公钥格式(RSA公钥通常以MII开头)
+ if (!publicKey.startsWith("MII") && !publicKey.contains("BEGIN")) {
+ logger.warn("⚠️ 公钥格式可能不正确(应该以MII开头或包含BEGIN PUBLIC KEY)");
+ }
+ // 验证公钥长度(RSA公钥通常至少200字符)
+ if (publicKey.length() < 200) {
+ logger.error("❌ 公钥长度过短: {} 字符(RSA公钥通常至少200字符)", publicKey.length());
+ errors.append("公钥长度过短; ");
+ isValid = false;
+ } else {
+ logger.info("✅ 公钥长度: {} 字符", publicKey.length());
+ // 隐藏敏感信息,只显示前后部分
+ String maskedKey = publicKey.substring(0, 20) + "..." + publicKey.substring(publicKey.length() - 20);
+ logger.debug("公钥摘要: {}", maskedKey);
+ }
+ }
+
+ // 4. 验证网关地址
+ if (config.getServerUrl() == null || config.getServerUrl().isEmpty()) {
+ logger.error("❌ 网关地址 (server-url) 为空");
+ errors.append("网关地址为空; ");
+ isValid = false;
+ } else {
+ String serverUrl = config.getServerUrl().trim();
+ if (!serverUrl.startsWith("https://")) {
+ logger.error("❌ 网关地址格式不正确: {} (应该以https://开头)", serverUrl);
+ errors.append("网关地址格式不正确; ");
+ isValid = false;
+ } else if (!serverUrl.contains("alipay")) {
+ logger.warn("⚠️ 网关地址可能不正确: {} (应该包含alipay)", serverUrl);
+ } else {
+ logger.info("✅ 网关地址: {}", serverUrl);
+ }
+ }
+
+ // 5. 验证字符集
+ String charset = config.getCharset();
+ if (charset == null || charset.isEmpty()) {
+ logger.warn("⚠️ 字符集未设置,将使用默认值: UTF-8");
+ } else if (!charset.equalsIgnoreCase("UTF-8")) {
+ logger.warn("⚠️ 字符集不是UTF-8: {} (建议使用UTF-8)", charset);
+ } else {
+ logger.info("✅ 字符集: {}", charset);
+ }
+
+ // 6. 验证签名类型
+ String signType = config.getSignType();
+ if (signType == null || signType.isEmpty()) {
+ logger.warn("⚠️ 签名类型未设置,将使用默认值: RSA2");
+ } else if (!signType.equalsIgnoreCase("RSA2")) {
+ logger.warn("⚠️ 签名类型不是RSA2: {} (建议使用RSA2)", signType);
+ } else {
+ logger.info("✅ 签名类型: {}", signType);
+ }
+
+ // 7. 验证通知URL
+ if (config.getNotifyUrl() == null || config.getNotifyUrl().isEmpty()) {
+ logger.error("❌ 通知URL (notify-url) 为空");
+ errors.append("通知URL为空; ");
+ isValid = false;
+ } else {
+ String notifyUrl = config.getNotifyUrl().trim();
+ if (!notifyUrl.startsWith("https://")) {
+ logger.error("❌ 通知URL格式不正确: {} (应该以https://开头)", notifyUrl);
+ errors.append("通知URL格式不正确; ");
+ isValid = false;
+ } else {
+ logger.info("✅ 通知URL: {}", notifyUrl);
+ }
+ }
+
+ // 8. 验证返回URL
+ if (config.getReturnUrl() == null || config.getReturnUrl().isEmpty()) {
+ logger.error("❌ 返回URL (return-url) 为空");
+ errors.append("返回URL为空; ");
+ isValid = false;
+ } else {
+ String returnUrl = config.getReturnUrl().trim();
+ if (!returnUrl.startsWith("https://")) {
+ logger.error("❌ 返回URL格式不正确: {} (应该以https://开头)", returnUrl);
+ errors.append("返回URL格式不正确; ");
+ isValid = false;
+ } else {
+ logger.info("✅ 返回URL: {}", returnUrl);
+ }
+ }
+
+ // 总结验证结果
+ if (isValid) {
+ logger.info("=== 支付宝配置验证通过 ===");
+ } else {
+ logger.error("=== 支付宝配置验证失败 ===");
+ logger.error("错误详情: {}", errors.toString());
+ logger.error("请检查 application-dev.properties 或 payment.properties 文件中的支付宝配置");
+ }
+
+ return isValid;
+ }
+
+ /**
+ * IJPay配置持有者
+ * 用于保存配置,供AlipayService在调用时动态设置
+ */
+ public static class AliPayApiConfigHolder {
+ private static AliPayApiConfig config;
+
+ public static void setConfig(AliPayApiConfig config) {
+ AliPayApiConfigHolder.config = config;
+ }
+
+ public static AliPayApiConfig getConfig() {
+ return config;
+ }
+ }
+
/**
* 支付宝配置
*/
@@ -27,7 +294,12 @@ public class PaymentConfig {
private String privateKey;
private String publicKey;
private String serverUrl;
+ private String gatewayUrl;
private String domain;
+ private String charset;
+ private String signType;
+ private String notifyUrl;
+ private String returnUrl;
private String appCertPath;
private String aliPayCertPath;
private String aliPayRootCertPath;
@@ -45,9 +317,24 @@ public class PaymentConfig {
public String getServerUrl() { return serverUrl; }
public void setServerUrl(String serverUrl) { this.serverUrl = serverUrl; }
+ public String getGatewayUrl() { return gatewayUrl; }
+ public void setGatewayUrl(String gatewayUrl) { this.gatewayUrl = gatewayUrl; }
+
public String getDomain() { return domain; }
public void setDomain(String domain) { this.domain = domain; }
+ public String getCharset() { return charset; }
+ public void setCharset(String charset) { this.charset = charset; }
+
+ public String getSignType() { return signType; }
+ public void setSignType(String signType) { this.signType = signType; }
+
+ public String getNotifyUrl() { return notifyUrl; }
+ public void setNotifyUrl(String notifyUrl) { this.notifyUrl = notifyUrl; }
+
+ public String getReturnUrl() { return returnUrl; }
+ public void setReturnUrl(String returnUrl) { this.returnUrl = returnUrl; }
+
public String getAppCertPath() { return appCertPath; }
public void setAppCertPath(String appCertPath) { this.appCertPath = appCertPath; }
diff --git a/demo/src/main/java/com/example/demo/config/SecurityConfig.java b/demo/src/main/java/com/example/demo/config/SecurityConfig.java
index 06f1f58..0dbe6a5 100644
--- a/demo/src/main/java/com/example/demo/config/SecurityConfig.java
+++ b/demo/src/main/java/com/example/demo/config/SecurityConfig.java
@@ -45,7 +45,8 @@ public class SecurityConfig {
.requestMatchers("/login", "/register", "/api/public/**", "/api/auth/**", "/api/verification/**", "/api/email/**", "/api/tencent/**", "/api/test/**", "/api/polling/**", "/api/diagnostic/**", "/api/polling-diagnostic/**", "/api/monitor/**", "/css/**", "/js/**", "/h2-console/**").permitAll()
.requestMatchers("/api/orders/stats").permitAll() // 统计接口允许匿名访问
.requestMatchers("/api/orders/**").authenticated() // 订单接口需要认证
- .requestMatchers("/api/payments/**").authenticated() // 支付接口需要认证
+ .requestMatchers("/api/payments/alipay/notify", "/api/payments/alipay/return").permitAll() // 支付宝回调接口允许匿名访问(外部调用)
+ .requestMatchers("/api/payments/**").authenticated() // 其他支付接口需要认证
.requestMatchers("/api/image-to-video/**").authenticated() // 图生视频接口需要认证
.requestMatchers("/api/text-to-video/**").authenticated() // 文生视频接口需要认证
.requestMatchers("/api/dashboard/**").hasRole("ADMIN") // 仪表盘API需要管理员权限
diff --git a/demo/src/main/java/com/example/demo/controller/PaymentController.java b/demo/src/main/java/com/example/demo/controller/PaymentController.java
index 6d3081e..cc1e781 100644
--- a/demo/src/main/java/com/example/demo/controller/PaymentController.java
+++ b/demo/src/main/java/com/example/demo/controller/PaymentController.java
@@ -101,11 +101,55 @@ public class PaymentController {
@ResponseBody
public String alipayNotify(HttpServletRequest request) {
try {
- Map params = request.getParameterMap().entrySet().stream()
- .collect(java.util.stream.Collectors.toMap(
- Map.Entry::getKey,
- entry -> entry.getValue()[0]
- ));
+ // 支付宝异步通知参数获取
+ // 注意:IJPay的AliPayApi.toMap()使用javax.servlet,但Spring Boot 3使用jakarta.servlet
+ // 所以手动获取参数
+ Map params = new java.util.HashMap<>();
+
+ // 获取URL参数
+ request.getParameterMap().forEach((key, values) -> {
+ if (values != null && values.length > 0) {
+ params.put(key, values[0]);
+ }
+ });
+
+ // 如果是POST请求,尝试从请求体获取参数
+ if ("POST".equalsIgnoreCase(request.getMethod())) {
+ try {
+ // 读取请求体
+ StringBuilder body = new StringBuilder();
+ try (java.io.BufferedReader reader = request.getReader()) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ body.append(line);
+ }
+ }
+
+ // 解析请求体参数(如果存在)
+ if (body.length() > 0) {
+ String bodyStr = body.toString();
+ // 支付宝可能使用form-urlencoded格式
+ if (bodyStr.contains("=")) {
+ String[] pairs = bodyStr.split("&");
+ for (String pair : pairs) {
+ String[] keyValue = pair.split("=", 2);
+ if (keyValue.length == 2) {
+ try {
+ params.put(
+ java.net.URLDecoder.decode(keyValue[0], "UTF-8"),
+ java.net.URLDecoder.decode(keyValue[1], "UTF-8")
+ );
+ } catch (Exception e) {
+ logger.warn("解析参数失败: {}", pair, e);
+ }
+ }
+ }
+ }
+ }
+ } catch (Exception e) {
+ logger.warn("读取请求体失败", e);
+ }
+ }
boolean success = alipayService.handleNotify(params);
return success ? "success" : "fail";
@@ -122,11 +166,13 @@ public class PaymentController {
@GetMapping("/alipay/return")
public String alipayReturn(HttpServletRequest request, Model model) {
try {
- Map params = request.getParameterMap().entrySet().stream()
- .collect(java.util.stream.Collectors.toMap(
- Map.Entry::getKey,
- entry -> entry.getValue()[0]
- ));
+ // 支付宝同步返回参数获取(GET请求,从URL参数获取)
+ Map params = new java.util.HashMap<>();
+ request.getParameterMap().forEach((key, values) -> {
+ if (values != null && values.length > 0) {
+ params.put(key, values[0]);
+ }
+ });
boolean success = alipayService.handleReturn(params);
diff --git a/demo/src/main/java/com/example/demo/dto/MailMessage.java b/demo/src/main/java/com/example/demo/dto/MailMessage.java
index 100ae22..87fdba3 100644
--- a/demo/src/main/java/com/example/demo/dto/MailMessage.java
+++ b/demo/src/main/java/com/example/demo/dto/MailMessage.java
@@ -68,3 +68,4 @@ public class MailMessage {
+
diff --git a/demo/src/main/java/com/example/demo/model/PointsFreezeRecord.java b/demo/src/main/java/com/example/demo/model/PointsFreezeRecord.java
index 04600e9..85f95c1 100644
--- a/demo/src/main/java/com/example/demo/model/PointsFreezeRecord.java
+++ b/demo/src/main/java/com/example/demo/model/PointsFreezeRecord.java
@@ -202,3 +202,4 @@ public class PointsFreezeRecord {
+
diff --git a/demo/src/main/java/com/example/demo/model/TaskQueue.java b/demo/src/main/java/com/example/demo/model/TaskQueue.java
index de91209..cc606cc 100644
--- a/demo/src/main/java/com/example/demo/model/TaskQueue.java
+++ b/demo/src/main/java/com/example/demo/model/TaskQueue.java
@@ -270,3 +270,4 @@ public class TaskQueue {
+
diff --git a/demo/src/main/java/com/example/demo/model/TaskStatus.java b/demo/src/main/java/com/example/demo/model/TaskStatus.java
index e3214d8..fec4bb9 100644
--- a/demo/src/main/java/com/example/demo/model/TaskStatus.java
+++ b/demo/src/main/java/com/example/demo/model/TaskStatus.java
@@ -262,3 +262,4 @@ public class TaskStatus {
+
diff --git a/demo/src/main/java/com/example/demo/repository/TaskStatusRepository.java b/demo/src/main/java/com/example/demo/repository/TaskStatusRepository.java
index 1c1c948..31ff7a0 100644
--- a/demo/src/main/java/com/example/demo/repository/TaskStatusRepository.java
+++ b/demo/src/main/java/com/example/demo/repository/TaskStatusRepository.java
@@ -70,3 +70,4 @@ public interface TaskStatusRepository extends JpaRepository {
+
diff --git a/demo/src/main/java/com/example/demo/security/PlainTextPasswordEncoder.java b/demo/src/main/java/com/example/demo/security/PlainTextPasswordEncoder.java
index 86e1b4a..5799037 100644
--- a/demo/src/main/java/com/example/demo/security/PlainTextPasswordEncoder.java
+++ b/demo/src/main/java/com/example/demo/security/PlainTextPasswordEncoder.java
@@ -39,5 +39,6 @@ public class PlainTextPasswordEncoder implements PasswordEncoder {
+
diff --git a/demo/src/main/java/com/example/demo/service/AlipayService.java b/demo/src/main/java/com/example/demo/service/AlipayService.java
index 0be62f6..44fa668 100644
--- a/demo/src/main/java/com/example/demo/service/AlipayService.java
+++ b/demo/src/main/java/com/example/demo/service/AlipayService.java
@@ -10,16 +10,18 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
-import com.alipay.api.AlipayClient;
-import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.AlipayTradePrecreateModel;
import com.alipay.api.internal.util.AlipaySignature;
-import com.alipay.api.request.AlipayTradePrecreateRequest;
-import com.alipay.api.response.AlipayTradePrecreateResponse;
import com.example.demo.model.Payment;
import com.example.demo.model.PaymentMethod;
import com.example.demo.model.PaymentStatus;
import com.example.demo.repository.PaymentRepository;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.ijpay.alipay.AliPayApi;
+import com.ijpay.alipay.AliPayApiConfig;
+import com.ijpay.alipay.AliPayApiConfigKit;
+import com.example.demo.config.PaymentConfig.AliPayApiConfigHolder;
@Service
public class AlipayService {
@@ -52,8 +54,11 @@ public class AlipayService {
@Value("${alipay.return-url}")
private String returnUrl;
+ private final ObjectMapper objectMapper;
+
public AlipayService(PaymentRepository paymentRepository) {
this.paymentRepository = paymentRepository;
+ this.objectMapper = new ObjectMapper();
}
/**
@@ -88,11 +93,36 @@ public class AlipayService {
}
/**
- * 调用真实的支付宝API
+ * 确保AliPayApiConfigKit中已设置配置
+ * 如果未设置,从AliPayApiConfigHolder获取并设置
+ * 根据IJPay源码,正确的方法是 putApiConfig
+ */
+ private void ensureAliPayConfigSet() {
+ try {
+ // 从AliPayApiConfigHolder获取配置
+ AliPayApiConfig config = AliPayApiConfigHolder.getConfig();
+ if (config != null) {
+ // 根据IJPay源码,使用 putApiConfig 方法设置配置
+ try {
+ AliPayApiConfigKit.putApiConfig(config);
+ logger.debug("IJPay配置已动态设置到AliPayApiConfigKit");
+ } catch (Exception e) {
+ logger.warn("动态设置IJPay配置到AliPayApiConfigKit时发生异常: {}", e.getMessage());
+ }
+ } else {
+ logger.warn("AliPayApiConfigHolder中没有配置,IJPay配置可能未初始化");
+ }
+ } catch (Exception e) {
+ logger.warn("动态设置IJPay配置时发生异常: {}", e.getMessage());
+ }
+ }
+
+ /**
+ * 调用真实的支付宝API(使用IJPay)
*/
private Map callRealAlipayAPI(Payment payment) throws Exception {
// 记录配置信息
- logger.info("=== 支付宝API配置信息 ===");
+ logger.info("=== 使用IJPay调用支付宝API ===");
logger.info("网关地址: {}", gatewayUrl);
logger.info("应用ID: {}", appId);
logger.info("字符集: {}", charset);
@@ -100,64 +130,112 @@ public class AlipayService {
logger.info("通知URL: {}", notifyUrl);
logger.info("返回URL: {}", returnUrl);
- // 设置连接和读取超时时间(60秒),需要在创建客户端之前设置
- System.setProperty("sun.net.client.defaultConnectTimeout", "60000");
- System.setProperty("sun.net.client.defaultReadTimeout", "60000");
- logger.info("超时配置: 连接超时=60秒, 读取超时=60秒");
-
- // 创建支付宝客户端
- AlipayClient alipayClient = new DefaultAlipayClient(
- gatewayUrl, appId, privateKey, "json", charset, publicKey, signType);
-
- // 使用预创建API生成二维码
- AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
- request.setNotifyUrl(notifyUrl);
-
// 设置业务参数
AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
model.setOutTradeNo(payment.getOrderId());
model.setTotalAmount(payment.getAmount().toString());
model.setSubject(payment.getDescription() != null ? payment.getDescription() : "AIGC会员订阅");
model.setBody(payment.getDescription() != null ? payment.getDescription() : "AIGC平台会员订阅服务");
+ model.setTimeoutExpress("5m");
- request.setBizModel(model);
-
- logger.info("调用支付宝预创建API,订单号:{},金额:{},商品名称:{}",
+ logger.info("调用支付宝预创建API(IJPay),订单号:{},金额:{},商品名称:{}",
model.getOutTradeNo(), model.getTotalAmount(), model.getSubject());
- // 调用API,增加重试机制
- AlipayTradePrecreateResponse response = null;
- int maxRetries = 3;
+ // 使用IJPay调用API,增加重试机制
+ String qrCode = null;
+ int maxRetries = 5; // 最大重试次数
int retryCount = 0;
+ // 在调用前确保配置已设置
+ ensureAliPayConfigSet();
+
+ // 验证配置中的网关地址
+ try {
+ AliPayApiConfig config = AliPayApiConfigKit.getAliPayApiConfig();
+ logger.info("=== API调用前配置验证 ===");
+ logger.info("配置中的serviceUrl: {}", config.getServiceUrl());
+ logger.info("配置中的appId: {}", config.getAppId());
+ logger.info("配置中的charset: {}", config.getCharset());
+ logger.info("配置中的signType: {}", config.getSignType());
+ } catch (Exception e) {
+ logger.warn("获取IJPay配置时发生异常: {}", e.getMessage());
+ }
+
while (retryCount < maxRetries) {
try {
- logger.info("正在调用支付宝API... (第{}次尝试)", retryCount + 1);
- response = alipayClient.execute(request);
- break; // 成功则跳出循环
+ logger.info("正在调用支付宝API(IJPay)... (第{}次尝试,共{}次)", retryCount + 1, maxRetries);
+ logger.info("API方法: alipay.trade.precreate (由AlipayTradePrecreateRequest自动设置)");
+ logger.info("通知URL: {}", notifyUrl);
+
+ // 使用IJPay的AliPayApi调用预创建接口
+ // AlipayTradePrecreateRequest会自动设置method参数为"alipay.trade.precreate"
+ String responseBody = AliPayApi.tradePrecreatePayToResponse(model, notifyUrl).getBody();
+
+ if (responseBody == null || responseBody.isEmpty()) {
+ throw new RuntimeException("IJPay API响应为空");
+ }
+
+ logger.info("IJPay API响应: {}", responseBody);
+
+ // 解析JSON响应
+ // IJPay返回的响应体是JSON字符串,格式为: {"alipay_trade_precreate_response":{"code":"10000","msg":"Success","qr_code":"..."},"sign":"..."}
+ try {
+ Map responseMap = objectMapper.readValue(
+ responseBody,
+ new TypeReference