From 0b0ad442a0e89050b4f84651a35a325cd35e7e6c Mon Sep 17 00:00:00 2001 From: AIGC Developer Date: Tue, 4 Nov 2025 18:18:49 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BB=A3=E7=A0=81=EF=BC=9A?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=94=AF=E4=BB=98=E5=8A=9F=E8=83=BD=E3=80=81?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E9=98=9F=E5=88=97=E7=B3=BB=E7=BB=9F=E5=92=8C?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- demo/ALIPAY_SETUP_GUIDE.md | 1 + demo/CODE_REVIEW_REPORT.md | 110 +++++++ demo/EMAIL_VERIFICATION_LOGIN_GUIDE.md | 1 + demo/IJPAY_USAGE_GUIDE.md | 229 ++++++++++++++ demo/IMAGE_TO_VIDEO_API_README.md | 1 + demo/POINTS_FREEZE_SYSTEM_README.md | 1 + demo/PasswordChecker.java | 1 + demo/TENCENT_SES_STARTUP_CHECKLIST.md | 1 + demo/TEXT_TO_VIDEO_API_README.md | 1 + demo/USER_WORKS_SYSTEM_README.md | 1 + demo/frontend/README.md | 1 + demo/frontend/src/App-backup.vue | 1 + demo/frontend/src/api/userWorks.js | 1 + demo/frontend/src/components/Footer.vue | 1 + demo/frontend/src/components/PaymentModal.vue | 44 ++- .../com/example/demo/DemoApplication.java | 6 + .../example/demo/config/PaymentConfig.java | 293 +++++++++++++++++- .../example/demo/config/SecurityConfig.java | 3 +- .../demo/controller/PaymentController.java | 66 +++- .../com/example/demo/dto/MailMessage.java | 1 + .../demo/model/PointsFreezeRecord.java | 1 + .../com/example/demo/model/TaskQueue.java | 1 + .../com/example/demo/model/TaskStatus.java | 1 + .../demo/repository/TaskStatusRepository.java | 1 + .../security/PlainTextPasswordEncoder.java | 1 + .../example/demo/service/AlipayService.java | 187 +++++++---- .../main/resources/application-dev.properties | 3 +- .../migration/V3__Create_Task_Queue_Table.sql | 1 + .../V4__Add_Points_Freeze_System.sql | 1 + .../V6__Create_Task_Status_Table.sql | 1 + demo/src/main/resources/payment.properties | 17 +- .../resources/templates/orders/admin.html | 1 + .../resources/templates/orders/detail.html | 1 + .../main/resources/templates/orders/form.html | 1 + demo/start-image-to-video-test.bat | 1 + demo/test-api-connection.java | 1 + demo/test_api.py | 1 + demo/test_jwt.py | 1 + demo/test_payment.py | 1 + test_alipay_connection.py | 1 + test_api.py | 1 + 41 files changed, 904 insertions(+), 85 deletions(-) create mode 100644 demo/CODE_REVIEW_REPORT.md create mode 100644 demo/IJPAY_USAGE_GUIDE.md diff --git a/demo/ALIPAY_SETUP_GUIDE.md b/demo/ALIPAY_SETUP_GUIDE.md index 6ddfb99..0cf1689 100644 --- a/demo/ALIPAY_SETUP_GUIDE.md +++ b/demo/ALIPAY_SETUP_GUIDE.md @@ -212,3 +212,4 @@ ngrok http 8080 + diff --git a/demo/CODE_REVIEW_REPORT.md b/demo/CODE_REVIEW_REPORT.md new file mode 100644 index 0000000..84b31ee --- /dev/null +++ b/demo/CODE_REVIEW_REPORT.md @@ -0,0 +1,110 @@ +# 代码检查报告 + +## 发现的问题 + +### 1. IJPay配置问题 ⚠️ + +**问题**:`AlipayService` 使用 IJPay 的 `AliPayApi`,但 IJPay 可能没有正确配置。 + +**位置**:`AlipayService.java` + +**说明**: +- `AlipayService` 使用 `@Value` 注入配置,但 IJPay 的 `AliPayApi` 可能不会自动读取这些配置 +- IJPay 的 `AliPayApi` 需要显式配置或通过系统属性/配置文件读取 + +**建议**: +- 检查 IJPay 是否需要初始化配置 +- 或者使用 `PaymentConfig.AliPayConfig` 来配置 IJPay + +### 2. 配置文件不一致 ⚠️ + +**问题**:`payment.properties` 中有模板值,但实际配置在 `application-dev.properties` 中。 + +**位置**: +- `payment.properties` - 包含模板值("您的APPID") +- `application-dev.properties` - 包含实际配置 + +**说明**: +- `PaymentConfig` 使用 `@PropertySource("classpath:payment.properties")` +- 但 `AlipayService` 使用 `@Value` 从 `application-dev.properties` 读取配置 +- 这可能导致配置不一致 + +**建议**: +- 统一配置文件路径 +- 或者在 `payment.properties` 中也配置实际值 + +### 3. 异步通知参数获取 ⚠️ + +**问题**:`PaymentController.alipayNotify()` 中参数获取方式可能不正确。 + +**位置**:`PaymentController.java` 第 100-117 行 + +**说明**: +- 使用 `request.getParameterMap()` 获取参数 +- 但支付宝异步通知可能使用 POST 请求体,而不是 URL 参数 +- 应该使用 `AliPayApi.toMap(request)` 来获取参数(如 `AlipayController` 中所示) + +**建议**: +- 使用 IJPay 的 `AliPayApi.toMap(request)` 方法获取参数 +- 或者检查支付宝异步通知的实际参数格式 + +### 4. 未使用的变量警告 ⚠️ + +**问题**:`AlipayService` 中的 `privateKey` 变量未使用。 + +**位置**:`AlipayService.java` 第 33 行 + +**说明**: +- `privateKey` 被注入但未在代码中直接使用 +- IJPay 内部可能需要使用它,但当前代码中未显式使用 + +**建议**: +- 如果 IJPay 内部使用,可以保留 +- 或者添加注释说明 + +### 5. 类型转换警告 ⚠️ + +**问题**:JSON 解析时存在类型转换警告。 + +**位置**:`AlipayService.java` 第 136-137 行 + +**说明**: +- 使用 `objectMapper.readValue(responseBody, Map.class)` 时存在类型转换警告 +- 应该使用 `TypeReference` 或 `MapType` 来避免警告 + +**建议**: +- 使用 `TypeReference` 或 `MapType` 来明确类型 + +## 修复建议 + +### 1. 修复 IJPay 配置 + +检查 IJPay 是否需要显式配置,如果需要,可以创建一个配置类来初始化 IJPay。 + +### 2. 统一配置文件 + +确保所有配置文件中的配置一致,或者统一使用一个配置文件。 + +### 3. 修复异步通知参数获取 + +使用 IJPay 的 `AliPayApi.toMap(request)` 方法获取参数。 + +### 4. 修复类型转换警告 + +使用 `TypeReference` 或 `MapType` 来明确类型。 + +## 代码质量 + +### 优点 +- 代码结构清晰 +- 有良好的日志记录 +- 有重试机制和错误处理 +- 使用了 IJPay 封装 + +### 需要改进 +- 配置文件一致性 +- IJPay 配置初始化 +- 异步通知参数获取方式 +- 类型转换警告 + + diff --git a/demo/EMAIL_VERIFICATION_LOGIN_GUIDE.md b/demo/EMAIL_VERIFICATION_LOGIN_GUIDE.md index c5b6485..c356bcc 100644 --- a/demo/EMAIL_VERIFICATION_LOGIN_GUIDE.md +++ b/demo/EMAIL_VERIFICATION_LOGIN_GUIDE.md @@ -273,3 +273,4 @@ if (result.success) { + diff --git a/demo/IJPAY_USAGE_GUIDE.md b/demo/IJPAY_USAGE_GUIDE.md new file mode 100644 index 0000000..d134911 --- /dev/null +++ b/demo/IJPAY_USAGE_GUIDE.md @@ -0,0 +1,229 @@ +# IJPay 使用指南 + +## 一、IJPay 简介 + +IJPay 是一个简洁易用的 Java 聚合支付 SDK,支持微信支付、支付宝支付、银联支付等多种支付方式。 + +项目已集成 IJPay-AliPay 模块,用于支付宝支付功能。 + +## 二、项目中的 IJPay 使用方式 + +### 1. 依赖配置 + +在 `pom.xml` 中已添加 IJPay 依赖: + +```xml + + com.github.javen205 + IJPay-AliPay + 2.9.12.1 + +``` + +### 2. 代码使用方式 + +项目中主要通过 `AlipayController.java` 使用 IJPay 的 `AliPayApi` 类进行支付操作。 + +#### 2.1 扫码支付(QR码支付) + +```java +@PostMapping("/qr-pay") +public ResponseEntity> qrPay(...) { + AlipayTradePrecreateModel model = new AlipayTradePrecreateModel(); + model.setOutTradeNo(outTradeNo); + model.setTotalAmount(totalAmount); + model.setSubject(subject); + model.setBody(body); + + // 使用 IJPay 的 AliPayApi 调用预创建接口 + String qrCode = AliPayApi.tradePrecreatePayToResponse(model, notifyUrl).getBody(); + // 返回二维码字符串 +} +``` + +#### 2.2 PC网页支付 + +```java +@PostMapping("/pc-pay") +public void pcPay(...) { + AlipayTradePagePayModel model = new AlipayTradePagePayModel(); + model.setOutTradeNo(outTradeNo); + model.setProductCode("FAST_INSTANT_TRADE_PAY"); + model.setTotalAmount(totalAmount); + model.setSubject(subject); + + // 使用 IJPay 的 AliPayApi 进行页面跳转 + AliPayApi.tradePage(response, model, notifyUrl, returnUrl); +} +``` + +#### 2.3 手机网页支付 + +```java +@PostMapping("/wap-pay") +public void wapPay(...) { + AlipayTradeWapPayModel model = new AlipayTradeWapPayModel(); + model.setOutTradeNo(outTradeNo); + model.setProductCode("QUICK_WAP_PAY"); + model.setTotalAmount(totalAmount); + + // 使用 IJPay 的 AliPayApi 进行手机支付 + AliPayApi.wapPay(response, model, returnUrl, notifyUrl); +} +``` + +#### 2.4 APP支付 + +```java +@PostMapping("/app-pay") +public ResponseEntity> appPay(...) { + AlipayTradeAppPayModel model = new AlipayTradeAppPayModel(); + model.setOutTradeNo(outTradeNo); + model.setProductCode("QUICK_MSECURITY_PAY"); + model.setTotalAmount(totalAmount); + + // 使用 IJPay 的 AliPayApi 获取 APP 支付订单信息 + String orderInfo = AliPayApi.appPayToResponse(model, notifyUrl).getBody(); + // 返回订单信息给 APP +} +``` + +#### 2.5 订单查询 + +```java +@GetMapping("/query") +public ResponseEntity> queryOrder(...) { + AlipayTradeQueryModel model = new AlipayTradeQueryModel(); + model.setOutTradeNo(outTradeNo); + + // 使用 IJPay 的 AliPayApi 查询订单 + String result = AliPayApi.tradeQueryToResponse(model).getBody(); + // 返回查询结果 +} +``` + +#### 2.6 退款 + +```java +@PostMapping("/refund") +public ResponseEntity> refund(...) { + AlipayTradeRefundModel model = new AlipayTradeRefundModel(); + model.setOutTradeNo(outTradeNo); + model.setRefundAmount(refundAmount); + model.setRefundReason(refundReason); + + // 使用 IJPay 的 AliPayApi 发起退款 + String result = AliPayApi.tradeRefundToResponse(model).getBody(); + // 返回退款结果 +} +``` + +#### 2.7 异步通知处理 + +```java +@PostMapping("/notify") +public String notifyUrl(HttpServletRequest request) { + // 使用 IJPay 的 AliPayApi 将请求参数转换为 Map + Map 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>() {} + ); + Map precreateResponse = (Map) responseMap.get("alipay_trade_precreate_response"); + + if (precreateResponse != null) { + String code = (String) precreateResponse.get("code"); + String msg = (String) precreateResponse.get("msg"); + + if ("10000".equals(code)) { + qrCode = (String) precreateResponse.get("qr_code"); + if (qrCode != null && !qrCode.isEmpty()) { + logger.info("支付宝二维码生成成功(IJPay),订单号:{},二维码:{}", payment.getOrderId(), qrCode); + break; // 成功则跳出循环 + } else { + throw new RuntimeException("二维码为空,响应消息:" + msg); + } + } else { + String subCode = (String) precreateResponse.get("sub_code"); + String subMsg = (String) precreateResponse.get("sub_msg"); + throw new RuntimeException("二维码生成失败:" + msg + (subMsg != null ? " - " + subMsg : "") + " (code: " + code + (subCode != null ? ", sub_code: " + subCode : "") + ")"); + } + } else { + throw new RuntimeException("无法解析响应,响应体:" + responseBody); + } + } catch (com.fasterxml.jackson.core.JsonProcessingException e) { + logger.error("JSON解析失败", e); + throw new RuntimeException("JSON解析失败:" + e.getMessage() + ",响应体:" + responseBody); + } + } catch (Exception e) { retryCount++; String errorType = e.getClass().getSimpleName(); String errorMessage = e.getMessage(); - logger.warn("支付宝API调用失败,第{}次重试", retryCount); + logger.warn("支付宝API调用失败(IJPay),第{}次尝试失败", retryCount); logger.warn("错误类型: {}", errorType); logger.warn("错误信息: {}", errorMessage); - // 根据错误类型提供诊断建议 + // 根据错误类型提供诊断建议和决定重试策略 + boolean isTimeoutError = false; if (errorMessage != null) { - if (errorMessage.contains("Read timed out") || errorMessage.contains("timeout")) { + if (errorMessage.contains("Read timed out") || errorMessage.contains("timeout") + || errorMessage.contains("SocketTimeoutException")) { + isTimeoutError = true; logger.error("=== 网络超时错误诊断 ==="); logger.error("可能的原因:"); logger.error("1. 网络连接不稳定或延迟过高"); - logger.error("2. 支付宝沙箱环境响应慢(openapi.alipaydev.com)"); + logger.error("2. 支付宝沙箱环境响应慢(openapi-sandbox.dl.alipaydev.com)"); logger.error("3. 防火墙或代理服务器阻止连接"); logger.error("4. ngrok隧道可能已过期或不可用"); logger.error("解决方案:"); - logger.error("1. 检查网络连接,尝试ping openapi.alipaydev.com"); + logger.error("1. 检查网络连接,尝试ping openapi-sandbox.dl.alipaydev.com"); logger.error("2. 检查ngrok是否正常运行: {}", notifyUrl); logger.error("3. 考虑使用代理服务器或VPN"); - logger.error("4. 增加超时时间或重试次数"); } else if (errorMessage.contains("Connection refused") || errorMessage.contains("ConnectException")) { logger.error("=== 连接拒绝错误诊断 ==="); logger.error("无法连接到支付宝服务器: {}", gatewayUrl); @@ -170,12 +248,16 @@ public class AlipayService { } if (retryCount >= maxRetries) { - logger.error("支付宝API调用失败,已达到最大重试次数({}次)", maxRetries); + logger.error("支付宝API调用失败(IJPay),已达到最大重试次数({}次)", maxRetries); logger.error("最终失败原因: {}", errorMessage); - throw new RuntimeException("支付宝API调用失败:" + errorMessage); + throw new RuntimeException("支付宝API调用失败(IJPay),已重试" + maxRetries + "次:" + errorMessage); } + + // 根据错误类型决定等待时间:超时错误等待更长时间 + int waitTime = isTimeoutError ? 5000 : 3000; // 超时错误等待5秒,其他错误等待3秒 + logger.info("等待{}秒后重试...", waitTime / 1000); try { - Thread.sleep(2000); // 等待2秒后重试 + Thread.sleep(waitTime); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new RuntimeException("重试被中断", ie); @@ -183,29 +265,20 @@ public class AlipayService { } } - logger.info("支付宝API响应状态:{}", response.isSuccess()); - logger.info("支付宝API响应码:{}", response.getCode()); - logger.info("支付宝API响应消息:{}", response.getMsg()); - logger.info("支付宝API响应子码:{}", response.getSubCode()); - logger.info("支付宝API响应子消息:{}", response.getSubMsg()); - - if (response.isSuccess()) { - String qrCode = response.getQrCode(); - logger.info("支付宝二维码生成成功,订单号:{},二维码:{}", payment.getOrderId(), qrCode); - - Map result = new HashMap<>(); - result.put("qrCode", qrCode); - result.put("outTradeNo", response.getOutTradeNo()); - result.put("success", true); - - return result; - } else { - logger.error("支付宝二维码生成失败,订单号:{},错误信息:{},子码:{},子消息:{}", - payment.getOrderId(), response.getMsg(), response.getSubCode(), response.getSubMsg()); + // 检查二维码是否为空 + if (qrCode == null || qrCode.isEmpty()) { + logger.error("支付宝API调用失败(IJPay),二维码为空"); payment.setStatus(PaymentStatus.FAILED); paymentRepository.save(payment); - throw new RuntimeException("二维码生成失败:" + response.getMsg() + " - " + response.getSubMsg()); + throw new RuntimeException("支付宝API调用失败(IJPay),二维码为空"); } + + Map result = new HashMap<>(); + result.put("qrCode", qrCode); + result.put("outTradeNo", payment.getOrderId()); + result.put("success", true); + + return result; } diff --git a/demo/src/main/resources/application-dev.properties b/demo/src/main/resources/application-dev.properties index cac5335..13e4f95 100644 --- a/demo/src/main/resources/application-dev.properties +++ b/demo/src/main/resources/application-dev.properties @@ -46,7 +46,8 @@ ai.image.api.key=sk-jp1O4h5lfN3WWZReF48SDa2osm0o9alC9qetkgq3M7XUjJ4R alipay.app-id=9021000157616562 alipay.private-key=MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCH7wPeptkJlJuoKwDqxvfJJLTOAWVkHa/TLh+wiy1tEtmwcrOwEU3GuqfkUlhij71WJIZi8KBytCwbax1QGZA/oLXvTCGJJrYrsEL624X5gGCCPKWwHRDhewsQ5W8jFxaaMXxth8GKlSW61PZD2cOQClRVEm2xnWFZ+6/7WBI7082g7ayzGCD2eowXsJyWyuEBCUSbHXkSgxVhqj5wUGIXhr8ly+pdUlJmDX5K8UG2rjJYx+0AU5UZJbOAND7d3iyDsOulHDvth50t8MOWDnDCVJ2aAgUB5FZKtOFxOmzNTsMjvzYldFztF0khbypeeMVL2cxgioIgTvjBkUwd55hZAgMBAAECggEAUjk3pARUoEDt7skkYt87nsW/QCUECY0Tf7AUpxtovON8Hgkju8qbuyvIxokwwV2k72hkiZB33Soyy9r8/iiYYoR5yGfKmUV7R+30df03ivYmamD48BCE138v8GZ31Ufv+hEY7MADSCpzihGrbNtaOdSlslfVVmyWKHHfvy9EyD6yHJGYswLpHXC/QX1TuLRRxk6Uup8qENOG/6zjGWMfxoRZFwTt80ml1mKy32YZGyJqDaQpJcdYwAHOPcnJl1emw4E+oVjiLyksl643npuTkgnZXs1iWcWSS8ojF1w/0kVDzcNh9toLg+HDuQlIHOis01VQ7lYcG4oiMOnhX1QHIQKBgQC9fgBuILjBhuCI9fHvLRdzoNC9heD54YK7xGvEV/mv90k8xcmNx+Yg5C57ASaMRtOq3b7muPiCv5wOtMT4tUCcMIwSrTNlcBM6EoTagnaGfpzOMaHGMXO4vbaw+MIynHnvXFj1rZjG1lzkV/9K36LAaHD9ZKVJaBQ9mK+0CIq/3QKBgQC3pL5GbvXj6/4ahTraXzNDQQpPGVgbHxcOioEXL4ibaOPC58puTW8HDbRvVuhl/4EEOBRVX81BSgkN8XHwTSiZdih2iOqByg+o9kixs7nlFn3Iw9BBP2/g+Wqiyi2N+9g17kfWXXVOKYz/eMXLBeOo4KhQE9wqNGyZldYzX2ywrQKBgApJmvBfqmgnUG1fHOFlS06lvm9ro0ktqxFSmp8wP4gEHt/DxSuDXMUQXk2jRFp9ReSS4VhZVnSSvoA15DO0c2uHXzNsX8v0B7cxZjEOwCyRFyZCn4vJB4VSF2cIOlLRF/Wcx9+eqxqwbJ6hAGUqOwXDJc879ZVEp0So03EsvYupAoGAAnI+Wp/VxLB7FQ1bSFdmTmoKYh1bUBks7HOp3o4yiqduCUWfK7L6XKSxF56Xv+wUYuMAWlbJXCpJTpc9xk6w0MKDLXkLbqkrZjvJohxbyJJxIICDQKtAqUWJRxvcWXzWV3mSGWfrTRw+lZSdReQRMUm01EQ/dYx3OeCGFu8Zeo0CgYAlH5YSYdJxZSoDCJeoTrkxUlFoOg8UQ7SrsaLYLwpwcwpuiWJaTrg6jwFocj+XhjQ9RtRbSBHz2wKSLdl+pXbTbqECKk85zMFl6zG3etXtTJU/dD750Ty4i8zt3+JGhvglPrQBY1CfItgml2oXa/VUVMnLCUS0WSZuPRmPYZD8dg== alipay.public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAksEwzuR3ASrKtTzaANqdQYKOoD44itA1TWG/6onvQr8PHNEMgcguLuJNrdeuT2PDg23byzZ9qKfEM2D5U4zbpt0/uCYLfZQyAnAWWyMvnKPoSIgrtBjnxYK6HE6fuQV3geJTcZxvP/z8dGZB0V0s6a53rzbKSLh0p4w0hWfVXlQihq3Xh4vSKB+ojdhEkIblhpWPT42NPbjVNdwPzIhUGpRy3/nsgNqVBu+ZacQ5/rCvzXU1RE0allBbjcvjymKQTS7bAE0i1Mgo1eX8njvElsfQUv5P7xQdrvZagqtIuTdP19cmsSNGdIC9Z5Po3j0z3KWPR7MrKgDuJfzkWtJR4wIDAQAB -alipay.gateway-url=https://openapi.alipaydev.com/gateway.do +alipay.server-url=https://openapi-sandbox.dl.alipaydev.com/gateway.do +alipay.gateway-url=https://openapi-sandbox.dl.alipaydev.com/gateway.do alipay.charset=UTF-8 alipay.sign-type=RSA2 alipay.notify-url=https://curtly-aphorismatic-ginger.ngrok-free.dev/api/payments/alipay/notify diff --git a/demo/src/main/resources/db/migration/V3__Create_Task_Queue_Table.sql b/demo/src/main/resources/db/migration/V3__Create_Task_Queue_Table.sql index 5944169..f4f2feb 100644 --- a/demo/src/main/resources/db/migration/V3__Create_Task_Queue_Table.sql +++ b/demo/src/main/resources/db/migration/V3__Create_Task_Queue_Table.sql @@ -29,3 +29,4 @@ CREATE TABLE IF NOT EXISTS task_queue ( + diff --git a/demo/src/main/resources/db/migration/V4__Add_Points_Freeze_System.sql b/demo/src/main/resources/db/migration/V4__Add_Points_Freeze_System.sql index 5d20961..a0294ff 100644 --- a/demo/src/main/resources/db/migration/V4__Add_Points_Freeze_System.sql +++ b/demo/src/main/resources/db/migration/V4__Add_Points_Freeze_System.sql @@ -28,3 +28,4 @@ CREATE TABLE IF NOT EXISTS points_freeze_records ( + diff --git a/demo/src/main/resources/db/migration/V6__Create_Task_Status_Table.sql b/demo/src/main/resources/db/migration/V6__Create_Task_Status_Table.sql index d570831..a55507f 100644 --- a/demo/src/main/resources/db/migration/V6__Create_Task_Status_Table.sql +++ b/demo/src/main/resources/db/migration/V6__Create_Task_Status_Table.sql @@ -31,3 +31,4 @@ CREATE TABLE task_status ( + diff --git a/demo/src/main/resources/payment.properties b/demo/src/main/resources/payment.properties index 07f4698..7d63c41 100644 --- a/demo/src/main/resources/payment.properties +++ b/demo/src/main/resources/payment.properties @@ -3,16 +3,13 @@ alipay.app-id=您的APPID alipay.private-key=您的应用私钥 alipay.public-key=支付宝公钥 -alipay.server-url=https://openapi.alipaydev.com/gateway.do -alipay.domain=http://您的域名:8080 +alipay.server-url=https://openapi-sandbox.dl.alipaydev.com/gateway.do +alipay.gateway-url=https://openapi-sandbox.dl.alipaydev.com/gateway.do +alipay.domain=https://curtly-aphorismatic-ginger.ngrok-free.dev +alipay.charset=UTF-8 +alipay.sign-type=RSA2 +alipay.notify-url=https://curtly-aphorismatic-ginger.ngrok-free.dev/api/payments/alipay/notify +alipay.return-url=https://curtly-aphorismatic-ginger.ngrok-free.dev/api/payments/alipay/return alipay.app-cert-path=classpath:cert/alipay/appCertPublicKey.crt alipay.ali-pay-cert-path=classpath:cert/alipay/alipayCertPublicKey_RSA2.crt alipay.ali-pay-root-cert-path=classpath:cert/alipay/alipayRootCert.crt - -# PayPal支付配置 -paypal.client-id=your_paypal_client_id_here -paypal.client-secret=your_paypal_client_secret_here -paypal.mode=sandbox -paypal.return-url=http://localhost:8080/api/payments/paypal/return -paypal.cancel-url=http://localhost:8080/api/payments/paypal/cancel -paypal.domain=http://localhost:8080 diff --git a/demo/src/main/resources/templates/orders/admin.html b/demo/src/main/resources/templates/orders/admin.html index e0f9b71..e78e10d 100644 --- a/demo/src/main/resources/templates/orders/admin.html +++ b/demo/src/main/resources/templates/orders/admin.html @@ -575,5 +575,6 @@ + diff --git a/demo/src/main/resources/templates/orders/detail.html b/demo/src/main/resources/templates/orders/detail.html index 1fbda52..728a874 100644 --- a/demo/src/main/resources/templates/orders/detail.html +++ b/demo/src/main/resources/templates/orders/detail.html @@ -491,5 +491,6 @@ + diff --git a/demo/src/main/resources/templates/orders/form.html b/demo/src/main/resources/templates/orders/form.html index 51d58eb..4831f5e 100644 --- a/demo/src/main/resources/templates/orders/form.html +++ b/demo/src/main/resources/templates/orders/form.html @@ -530,5 +530,6 @@ + diff --git a/demo/start-image-to-video-test.bat b/demo/start-image-to-video-test.bat index 28dfcc9..2809331 100644 --- a/demo/start-image-to-video-test.bat +++ b/demo/start-image-to-video-test.bat @@ -32,3 +32,4 @@ pause > nul + diff --git a/demo/test-api-connection.java b/demo/test-api-connection.java index 36647d0..39ac868 100644 --- a/demo/test-api-connection.java +++ b/demo/test-api-connection.java @@ -59,3 +59,4 @@ public class TestApiConnection { + diff --git a/demo/test_api.py b/demo/test_api.py index e7c40ea..d161190 100644 --- a/demo/test_api.py +++ b/demo/test_api.py @@ -18,3 +18,4 @@ except Exception as e: + diff --git a/demo/test_jwt.py b/demo/test_jwt.py index 3257c96..add08db 100644 --- a/demo/test_jwt.py +++ b/demo/test_jwt.py @@ -26,3 +26,4 @@ else: + diff --git a/demo/test_payment.py b/demo/test_payment.py index 34ef309..35dc881 100644 --- a/demo/test_payment.py +++ b/demo/test_payment.py @@ -62,3 +62,4 @@ if __name__ == "__main__": + diff --git a/test_alipay_connection.py b/test_alipay_connection.py index c96ad83..695aa5a 100644 --- a/test_alipay_connection.py +++ b/test_alipay_connection.py @@ -83,3 +83,4 @@ if __name__ == "__main__": + diff --git a/test_api.py b/test_api.py index e7c40ea..d161190 100644 --- a/test_api.py +++ b/test_api.py @@ -18,3 +18,4 @@ except Exception as e: +