正在显示
15 个修改的文件
包含
1521 行增加
和
0 行删除
| @@ -234,6 +234,20 @@ | @@ -234,6 +234,20 @@ | ||
| 234 | <type>pom</type> | 234 | <type>pom</type> |
| 235 | </dependency> | 235 | </dependency> |
| 236 | 236 | ||
| 237 | + <!-- 微信支付 --> | ||
| 238 | + <dependency> | ||
| 239 | + <groupId>com.github.wechatpay-apiv3</groupId> | ||
| 240 | + <artifactId>wechatpay-java</artifactId> | ||
| 241 | + <version>0.2.17</version> | ||
| 242 | + </dependency> | ||
| 243 | + | ||
| 244 | + <!-- 支付宝支付 --> | ||
| 245 | + <dependency> | ||
| 246 | + <groupId>com.alipay.sdk</groupId> | ||
| 247 | + <artifactId>alipay-sdk-java</artifactId> | ||
| 248 | + <version>4.40.354.ALL</version> | ||
| 249 | + </dependency> | ||
| 250 | + | ||
| 237 | <!-- Test --> | 251 | <!-- Test --> |
| 238 | <dependency> | 252 | <dependency> |
| 239 | <groupId>org.springframework.boot</groupId> | 253 | <groupId>org.springframework.boot</groupId> |
| 1 | +package com.aigeo.entity; | ||
| 2 | + | ||
| 3 | +import lombok.Data; | ||
| 4 | + | ||
| 5 | +import java.io.Serializable; | ||
| 6 | + | ||
| 7 | +@Data | ||
| 8 | +public class ApiConfig implements Serializable { | ||
| 9 | + /** | ||
| 10 | + * 主键id | ||
| 11 | + */ | ||
| 12 | + private Integer id; | ||
| 13 | + | ||
| 14 | + /** | ||
| 15 | + * 接口类型(1.文档转换 2.文档扫描 3.图片识别 4.智能翻译 5.短信 6.微信 7.邮件 8.EDM) | ||
| 16 | + */ | ||
| 17 | + private Integer apiType; | ||
| 18 | + | ||
| 19 | + /* | ||
| 20 | + * 接口类型子模块 | ||
| 21 | + * */ | ||
| 22 | + private Integer apiClassify; | ||
| 23 | + | ||
| 24 | + /** | ||
| 25 | + * 接口提供方 | ||
| 26 | + */ | ||
| 27 | + private String apiName; | ||
| 28 | + | ||
| 29 | + /** | ||
| 30 | + * 企业id | ||
| 31 | + */ | ||
| 32 | + private Integer companyId; | ||
| 33 | + | ||
| 34 | + /** | ||
| 35 | + * API的ID号 | ||
| 36 | + */ | ||
| 37 | + private String apiId; | ||
| 38 | + | ||
| 39 | + /** | ||
| 40 | + * API的key | ||
| 41 | + */ | ||
| 42 | + private String apiKey; | ||
| 43 | + | ||
| 44 | + /** | ||
| 45 | + * API的code值 | ||
| 46 | + */ | ||
| 47 | + private String apiCode; | ||
| 48 | + | ||
| 49 | + /** | ||
| 50 | + * API的host | ||
| 51 | + */ | ||
| 52 | + private String apiUrl; | ||
| 53 | + | ||
| 54 | + /** | ||
| 55 | + * API的方法名 | ||
| 56 | + */ | ||
| 57 | + private String apiPath; | ||
| 58 | + | ||
| 59 | + /* | ||
| 60 | + * Api回调地址 | ||
| 61 | + * */ | ||
| 62 | + private String apiRedirectUri; | ||
| 63 | + | ||
| 64 | + /** | ||
| 65 | + * API其它 | ||
| 66 | + */ | ||
| 67 | + private String apiTest; | ||
| 68 | + | ||
| 69 | + /** | ||
| 70 | + * API的更新日期 | ||
| 71 | + */ | ||
| 72 | + private String updateTime; | ||
| 73 | + | ||
| 74 | + /** | ||
| 75 | + * 备注说明 | ||
| 76 | + */ | ||
| 77 | + private String remarks; | ||
| 78 | + | ||
| 79 | + /* | ||
| 80 | + * 系统标识 | ||
| 81 | + * */ | ||
| 82 | + private String code; | ||
| 83 | + | ||
| 84 | + /* | ||
| 85 | + * 是否启用(0:否 1:是) | ||
| 86 | + * */ | ||
| 87 | + private Integer isEnable; | ||
| 88 | + | ||
| 89 | + //--------------------20250804新增以下微信支付相关配置字段---------------------- | ||
| 90 | + /** | ||
| 91 | + * 微信支付私钥路径 | ||
| 92 | + */ | ||
| 93 | + private String privateKeyPath; | ||
| 94 | + /** | ||
| 95 | + * 微信支付公钥路径 | ||
| 96 | + */ | ||
| 97 | + private String publicKeyPath; | ||
| 98 | + /** | ||
| 99 | + * 微信退款回调地址 | ||
| 100 | + */ | ||
| 101 | + private String refundRedirectUrl; | ||
| 102 | + /** | ||
| 103 | + * 商户号 | ||
| 104 | + */ | ||
| 105 | + private String merchantId; | ||
| 106 | + //--------------------20250804新增以上微信支付相关配置字段---------------------- | ||
| 107 | + | ||
| 108 | + private static final long serialVersionUID = 1L; | ||
| 109 | +} |
src/main/java/com/aigeo/entity/PayDto.java
0 → 100644
| 1 | +package com.aigeo.entity; | ||
| 2 | + | ||
| 3 | +import lombok.Data; | ||
| 4 | + | ||
| 5 | +@Data | ||
| 6 | +public class PayPalDTO { | ||
| 7 | + private String intent; | ||
| 8 | + private PaymentSource payment_source; | ||
| 9 | + private PurchaseUnit[] purchase_units; | ||
| 10 | + | ||
| 11 | + @Data | ||
| 12 | + public static class PaymentSource { | ||
| 13 | + private Paypal paypal; | ||
| 14 | + | ||
| 15 | + @Data | ||
| 16 | + public static class Paypal { | ||
| 17 | + private ExperienceContext experience_context; | ||
| 18 | + | ||
| 19 | + @Data | ||
| 20 | + public static class ExperienceContext { | ||
| 21 | + private String return_url; | ||
| 22 | + private String cancel_url; | ||
| 23 | + } | ||
| 24 | + } | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + @Data | ||
| 28 | + public static class PurchaseUnit { | ||
| 29 | + private String custom_id; | ||
| 30 | + private Amount amount; | ||
| 31 | + | ||
| 32 | + @Data | ||
| 33 | + public static class Amount { | ||
| 34 | + private String currency_code; | ||
| 35 | + | ||
| 36 | + private String value; | ||
| 37 | + } | ||
| 38 | + } | ||
| 39 | +} | ||
| 40 | + |
| 1 | +package com.aigeo.entity; | ||
| 2 | + | ||
| 3 | +import lombok.Data; | ||
| 4 | + | ||
| 5 | +@Data | ||
| 6 | +public class PayPalOrderRecord { | ||
| 7 | + private Long id; | ||
| 8 | + private Integer companyId; | ||
| 9 | + private String paypalId; | ||
| 10 | + private String payerId; | ||
| 11 | + private String orderId; | ||
| 12 | + private String createTime; | ||
| 13 | + private String updateTime; | ||
| 14 | + private Integer state; | ||
| 15 | +} |
| 1 | +package com.aigeo.entity; | ||
| 2 | + | ||
| 3 | +import lombok.Data; | ||
| 4 | + | ||
| 5 | +@Data | ||
| 6 | +public class PayPalResponse { | ||
| 7 | + private String id; | ||
| 8 | + private String status; | ||
| 9 | + private ResMsg[] links; | ||
| 10 | + | ||
| 11 | + @Data | ||
| 12 | + public static class ResMsg{ | ||
| 13 | + private String href; | ||
| 14 | + private String rel; | ||
| 15 | + private String method; | ||
| 16 | + } | ||
| 17 | +} |
src/main/java/com/aigeo/util/pay/AliPay.java
0 → 100644
| 1 | +package com.aigeo.util.pay; | ||
| 2 | + | ||
| 3 | +import com.aigeo.entity.ApiConfig; | ||
| 4 | +import com.aigeo.entity.PayDto; | ||
| 5 | +import com.alipay.api.AlipayClient; | ||
| 6 | +import com.alipay.api.AlipayConfig; | ||
| 7 | +import com.alipay.api.DefaultAlipayClient; | ||
| 8 | +import com.alipay.api.diagnosis.DiagnosisUtils; | ||
| 9 | +import com.alipay.api.domain.AlipayTradePrecreateModel; | ||
| 10 | +import com.alipay.api.request.AlipayTradePrecreateRequest; | ||
| 11 | +import com.alipay.api.response.AlipayTradePrecreateResponse; | ||
| 12 | + | ||
| 13 | +import lombok.extern.slf4j.Slf4j; | ||
| 14 | +import org.springframework.stereotype.Component; | ||
| 15 | + | ||
| 16 | +import java.io.File; | ||
| 17 | +import java.io.IOException; | ||
| 18 | +import java.nio.charset.StandardCharsets; | ||
| 19 | +import java.nio.file.Files; | ||
| 20 | +import java.nio.file.Paths; | ||
| 21 | +import java.util.HashMap; | ||
| 22 | +import java.util.Map; | ||
| 23 | + | ||
| 24 | +@Slf4j | ||
| 25 | +@Component | ||
| 26 | +public class AliPay implements PaymentStrategy{ | ||
| 27 | + | ||
| 28 | + // 环境切换开关,"sandbox" 或 "prod",根据实际环境设置 | ||
| 29 | + private static final String environment = "sandbox"; | ||
| 30 | + // 定义环境配置 | ||
| 31 | + public static Map<String, Map<String, String>> CONFIG_MAG = new HashMap<String, Map<String, String>>(); | ||
| 32 | + | ||
| 33 | + static { | ||
| 34 | + // 沙箱环境配置 | ||
| 35 | + Map<String, String> sandbox = new HashMap<String, String>(); | ||
| 36 | + // AppId,对应配置项为“应用信息”。 | ||
| 37 | + sandbox.put("appId", "9021000123615361"); | ||
| 38 | + // 支付宝沙箱网关地址(sandboxAlipayGateway),对应配置项为“支付宝网关”。 | ||
| 39 | + sandbox.put("gatewayUrl", "https://openapi-sandbox.dl.alipaydev.com/gateway.do"); | ||
| 40 | + // 支付宝沙箱公钥(sandboxAlipayPublicKey),对应配置项为“密钥配置”。 | ||
| 41 | + sandbox.put("alipayPublicKey", "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkksZzpOGWoGMz9lz7y79DtaXuvsOs4d4ChdYir3/FSRHbYn/4G25LYw+7h00s8JCXIPhLoozWi9ZFN3hEMUHreT0/UPk0LgX7AS43VQHmR0cLhOOf2o5zxpzN9SjU/zK3ZQT0rEuNyoikPaY/UK4fqHvySBc7OfeKefDqdS/v/BchwzlzefEqXg4imz0uUMhn8micUuAeT7iF72NxlqCBmRXzOBx/CaUCOA2BMn2b3KDUix2qiy8FzkPQRj5dXQbDu8JFsHDAU/T9as8JY1ZRDI3wPgHverb5SuyFydeQsIEHSmC89k+TySa4bqbQRZ5+LOFzbkyrCXXQ9y1V/4jgQIDAQAB"); | ||
| 42 | + // 应用沙箱私钥(sandboxAppPrivateKey),对应配置项为“密钥配置”。 | ||
| 43 | + sandbox.put("appPrivateKey", "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCl7rLc1vU1YMij5nGna2Tiqw08TgCEoaviLgHaenBJi8JfUDndm01XAZ6Vfia53F4yvj8woPkiOzn/HYEfqs3+SAPbdQF/KyZ6GocAxwZBAhXTLD+c/D3pRX9lrOo+krw6H8JoJeVrDD2v4N1fWJBfh8U4x+pMcysYnDZbS9xquGNaPIpEnGZ7FHCvQo5Uq2xDv4iApwmiWdwNcgN9xatCy5deM9ot08cd+rL9NcnFnuWdxeQVBLvYS6w28kJUKKOIIUXKhWkpVTspZksUJRqKevlgw+wL2ORFB0CiAV9l7wugdt0V2KqLeroUNuOYkh9fBTwXTEv/NHZ+mUjmECETAgMBAAECggEAFdmm5/nOdGb4RMz1rmwv7sFDjutemhiH/9K7H7/7xVOIFvEDjbEDsC9gaKMiCluAdAIlZAvugpl1+6gw9rCRS6jECUQRHGPcPzv7BxkuM6VIpnQvNEbrCCrEqejpN7Au5SCFgx1qZnxIKz7bJ9ELweipCd8ZdVO9GxKG/eKxdcP3ZntujxkcSsgaYLKp7KozWmFYTFphGTsRjrJYofg6KpIONxDIigQfNWpyBHCre13lAsP+nxccOCw1Lx2/m1Xda0U7XWDeRePiutrsOV2KPZa59qv15tezjsIs0yhQnTPmDLMRQ2a5oJLRuPRreLLJwDGsG0FZXNUEQK6mWnD4gQKBgQDqXsNwmgTEPW/jcq2Myh4iLlY4Ag3kR7iVOsFn4PbZrqf+B52wSepp4uT9+10z6nuKADMWyUMS+jwPAeoWlhygUkhdB8L50KDcXztsz9NGQ+4LX9OGJncp616Lw3pdIFMkptPuCvjUISi/xy/24VbkgLzosfr2a1rIf7QwHpwI8wKBgQC1PwXaw1eEstzwK/+ov0ptIRQKQlw7frJHx3rRPFCu94G6pbNOLN2FhzZ5D0sIAHlQFhkUR34qOTw/bgS1WduUHWKH+pSGhW7g/hb2G6/gKsGgTUvdW8/jVQoTdS++iKJkI6q08lAdZG769c+PH1gb/RdArhHk979OTwUpmIuPYQKBgQC3/jE8ow1+6iR+0TbCQqKpfusdCS/SuSOFtBzF6mygtGcaHOyEy2KGQiQ36DBhY4Ic8s8o4lQP//dKaNxXYv2SN0asEj3VVR4UI4dJg1Z/4TBuKYqr1GLUu/z7iDj2Bzx+l6HlTSPPlUCiXkOfc4R92ztzCtbUwa8BnI/e/wjMVQKBgE1J+conIOwShgrZCGXCZQ+SWoRhdglc5tObKjGNoe5q12RrgsHkefaWCVGohtt7sD/JnJo8Pn3s+FcoKFPZZLyNp1XLMokG35iMYehDOEXEldsw+xgDLeyIeAqCH4EFrRI872/IIQfXq0fTiNZEjIMq8z5+vFzClU7kfZQWW/UhAoGBANjKR0NJKOMBTGJhSuSL4mL96ayoUoiYDXkQMVvP0oGAt6jdq34lyl+e4n02yhd5IHIDOXrIdx2xnd+HAOBI1YPAYMJ4gHC8WQM9dbjxtqyyNGSQnpVLCAdxvVInCrJdYR82+26oPIUbMtOHVkjtCr47wXlSHb82Ax9UgL5Bhl7N"); | ||
| 44 | + // 线上环境配置 | ||
| 45 | + Map<String, String> prod = new HashMap<String, String>(); | ||
| 46 | + // AppId,对应配置项为“应用信息”。 | ||
| 47 | + prod.put("appId", "${appId}"); | ||
| 48 | + // 支付宝线上网关地址(prodAlipayGateway),对应配置项为“支付宝网关”。 | ||
| 49 | + prod.put("gatewayUrl", "${prodAlipayGateway}"); | ||
| 50 | + // 支付宝线上公钥(prodAlipayPublicKey),对应配置项为“密钥配置”。 | ||
| 51 | + prod.put("alipayPublicKey", "${prodAlipayPublicKey}"); | ||
| 52 | + // 应用线上私钥(prodAppPrivateKey),需要替换为线上环境使用的应用私钥,对应配置项为“密钥配置”,注意 Java 语言需要 PKCS8 格式的密钥。 | ||
| 53 | + prod.put("appPrivateKey", "${prodAppPrivateKey}"); | ||
| 54 | + CONFIG_MAG.put("sandbox", sandbox); | ||
| 55 | + CONFIG_MAG.put("prod", prod); | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + @Override | ||
| 59 | + public String pay(PayDto payDto) { | ||
| 60 | + try { | ||
| 61 | + AlipayConfig alipayConfig = new AlipayConfig(); | ||
| 62 | + ApiConfig config = payDto.getApiConfig(); | ||
| 63 | + alipayConfig.setServerUrl(config.getApiUrl()); | ||
| 64 | + alipayConfig.setAppId(config.getApiId()); | ||
| 65 | + | ||
| 66 | + String path = this.getClass().getResource("/").getPath(); | ||
| 67 | + path = path.replace("/", "//"); | ||
| 68 | + path = path.replace("//target//classes//", ""); | ||
| 69 | + path = path.replace("//WEB-INF//classes//", ""); | ||
| 70 | + //获取秘钥内容 | ||
| 71 | + String publicKey = getKey(path+config.getPublicKeyPath()); | ||
| 72 | + String privateKey = getKey(path+config.getPrivateKeyPath()); | ||
| 73 | + | ||
| 74 | + alipayConfig.setPrivateKey(privateKey); | ||
| 75 | + alipayConfig.setAlipayPublicKey(publicKey); | ||
| 76 | + | ||
| 77 | + alipayConfig.setFormat("json"); | ||
| 78 | + alipayConfig.setCharset("UTF-8"); | ||
| 79 | + alipayConfig.setSignType("RSA2"); | ||
| 80 | + // 初始化SDK | ||
| 81 | + AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig); | ||
| 82 | + // 构造请求参数以调用接口 | ||
| 83 | + AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest(); | ||
| 84 | + AlipayTradePrecreateModel model = new AlipayTradePrecreateModel(); | ||
| 85 | + | ||
| 86 | + // 设置商户订单号 | ||
| 87 | + model.setOutTradeNo(payDto.getOrderId()); | ||
| 88 | + | ||
| 89 | + model.setProductCode("QR_CODE_OFFLINE"); | ||
| 90 | + // 设置订单总金额 | ||
| 91 | + model.setTotalAmount(payDto.getAmount()); | ||
| 92 | + // 设置订单标题 | ||
| 93 | + model.setSubject(payDto.getOrderId()); | ||
| 94 | + // 设置商户的原始订单号 | ||
| 95 | + model.setMerchantOrderNo(payDto.getOrderId()); | ||
| 96 | + | ||
| 97 | + request.setBizModel(model); | ||
| 98 | + request.setNotifyUrl(config.getApiRedirectUri()+"/"+payDto.getCompanyId()); | ||
| 99 | + AlipayTradePrecreateResponse response = alipayClient.execute(request); | ||
| 100 | + System.out.println(response.getBody()); | ||
| 101 | + if (response.isSuccess()) { | ||
| 102 | + log.info("订单号:{}请求支付宝支付",response.getOutTradeNo()); | ||
| 103 | + return response.getQrCode(); | ||
| 104 | + } else { | ||
| 105 | + System.out.println(response.getSubMsg()); | ||
| 106 | + System.out.println("调用失败"); | ||
| 107 | + String diagnosisUrl = DiagnosisUtils.getDiagnosisUrl(response); | ||
| 108 | + System.out.println(diagnosisUrl); | ||
| 109 | + return response.getSubMsg(); | ||
| 110 | + } | ||
| 111 | + | ||
| 112 | + } catch (Exception e) { | ||
| 113 | + e.printStackTrace(); | ||
| 114 | + return null; | ||
| 115 | + } | ||
| 116 | + } | ||
| 117 | + | ||
| 118 | + @Override | ||
| 119 | + public void closeTrade(String orderId, String mchId, ApiConfig apiConfig) { | ||
| 120 | + | ||
| 121 | + } | ||
| 122 | + | ||
| 123 | + @Override | ||
| 124 | + public void refund() { | ||
| 125 | + | ||
| 126 | + } | ||
| 127 | + | ||
| 128 | + @Override | ||
| 129 | + public String getChannel() { | ||
| 130 | + return "alipay"; | ||
| 131 | + } | ||
| 132 | + | ||
| 133 | + private String getKey(String filePath) throws IOException{ | ||
| 134 | + String fixed = filePath | ||
| 135 | + .replaceAll("^/+([A-Za-z]):/+", "$1:/") // 去掉前导 // 并保留盘符 | ||
| 136 | + .replace('/', File.separatorChar); // 把 / 换成平台分隔符 | ||
| 137 | + | ||
| 138 | + // 2. 读取 | ||
| 139 | + byte[] bytes = Files.readAllBytes(Paths.get(fixed)); | ||
| 140 | + return new String(bytes, StandardCharsets.UTF_8); | ||
| 141 | + } | ||
| 142 | +} |
| 1 | +package com.aigeo.util.pay; | ||
| 2 | + | ||
| 3 | + | ||
| 4 | +import javax.crypto.*; | ||
| 5 | +import javax.crypto.spec.GCMParameterSpec; | ||
| 6 | +import javax.crypto.spec.SecretKeySpec; | ||
| 7 | +import java.nio.charset.StandardCharsets; | ||
| 8 | +import java.nio.file.Files; | ||
| 9 | +import java.nio.file.Paths; | ||
| 10 | +import java.security.*; | ||
| 11 | +import java.security.spec.X509EncodedKeySpec; | ||
| 12 | +import java.util.Base64; | ||
| 13 | + | ||
| 14 | +/** | ||
| 15 | + * 项目名称: 微信支付回调解密 | ||
| 16 | + * 版权所有: 深圳市科飞时速网络技术有限公司(0755-88843776) | ||
| 17 | + * 程序版本: V3.0 | ||
| 18 | + * 技术支持: info@21gmail.com | ||
| 19 | + * 单元名称: 支付接口 | ||
| 20 | + * 开始时间: 2025/08/04 15:13 | ||
| 21 | + * 最后修改: 2025/08/04 15:13 | ||
| 22 | + * 开发人员: 温志锋 | ||
| 23 | + * 备 注: 如需修改请联系开发人员 | ||
| 24 | + */ | ||
| 25 | +public class CheckUtil { | ||
| 26 | + /** | ||
| 27 | + * 验签 | ||
| 28 | + * @param wechatPublicKeyPath 公钥文件路径 | ||
| 29 | + * @param timestamp 时间戳 | ||
| 30 | + * @param nonce 验签的随机字符串 | ||
| 31 | + * @param body 请求体 | ||
| 32 | + * @param signature 签名值 | ||
| 33 | + * @return 验签成功 | ||
| 34 | + */ | ||
| 35 | + public static boolean verifySignature(String wechatPublicKeyPath, String timestamp, String nonce, String body, String signature){ | ||
| 36 | + try{ | ||
| 37 | + // 1. 构建验签串 | ||
| 38 | + String signMessage = buildSignMessage(timestamp, nonce, body); | ||
| 39 | + // 2. 解码Base64签名 | ||
| 40 | + byte[] signatureBytes = Base64.getDecoder().decode(signature); | ||
| 41 | + // 3. 加载微信支付公钥 | ||
| 42 | + PublicKey publicKey = loadPublicKey(wechatPublicKeyPath); | ||
| 43 | + // 4. 执行验签 | ||
| 44 | + Signature signer = Signature.getInstance("SHA256withRSA"); | ||
| 45 | + | ||
| 46 | + signer.initVerify(publicKey); | ||
| 47 | + | ||
| 48 | + signer.update(signMessage.getBytes(StandardCharsets.UTF_8)); | ||
| 49 | + | ||
| 50 | + return signer.verify(signatureBytes); | ||
| 51 | + }catch(Exception e){ | ||
| 52 | + e.printStackTrace(); | ||
| 53 | + System.out.println("---------验签异常---------"+e); | ||
| 54 | + return false; | ||
| 55 | + } | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + /** | ||
| 59 | + * 解密方法 | ||
| 60 | + * @param ciphertext 数据密文 | ||
| 61 | + * @param associatedData 附加数据 | ||
| 62 | + * @param nonce 随机串 | ||
| 63 | + * @param vkey v3key | ||
| 64 | + * @return 解密数据 | ||
| 65 | + * @throws NoSuchPaddingException | ||
| 66 | + * @throws NoSuchAlgorithmException | ||
| 67 | + * @throws InvalidAlgorithmParameterException | ||
| 68 | + * @throws InvalidKeyException | ||
| 69 | + * @throws BadPaddingException | ||
| 70 | + * @throws IllegalBlockSizeException | ||
| 71 | + */ | ||
| 72 | + public static String decryptData(String ciphertext, String associatedData, String nonce,String vkey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { | ||
| 73 | + byte[] keyBytes = vkey.getBytes(StandardCharsets.UTF_8); | ||
| 74 | + SecretKey key = new SecretKeySpec(keyBytes, "AES"); | ||
| 75 | + | ||
| 76 | + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); | ||
| 77 | + GCMParameterSpec spec = new GCMParameterSpec(128, nonce.getBytes(StandardCharsets.UTF_8)); | ||
| 78 | + cipher.init(Cipher.DECRYPT_MODE, key, spec); | ||
| 79 | + | ||
| 80 | + if (associatedData != null) { | ||
| 81 | + cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8)); | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(ciphertext)); | ||
| 85 | + return new String(decrypted, StandardCharsets.UTF_8); | ||
| 86 | + } | ||
| 87 | + | ||
| 88 | + | ||
| 89 | + | ||
| 90 | + /** | ||
| 91 | + * 构建验签串 | ||
| 92 | + * 格式: timestamp + "\n" + nonce + "\n" + body + "\n" | ||
| 93 | + */ | ||
| 94 | + private static String buildSignMessage(String timestamp, String nonce, String body) { | ||
| 95 | + // 处理空body情况(如204响应) | ||
| 96 | + if (body == null) { | ||
| 97 | + body = ""; | ||
| 98 | + } | ||
| 99 | + return timestamp + "\n" + nonce + "\n" + body + "\n"; | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + /** | ||
| 103 | + * 从PEM格式字符串加载公钥 | ||
| 104 | + */ | ||
| 105 | + private static PublicKey loadPublicKey(String filePath) throws GeneralSecurityException { | ||
| 106 | + try { | ||
| 107 | + String pemContent = new String(Files.readAllBytes(Paths.get(filePath)), StandardCharsets.UTF_8); | ||
| 108 | + //1. 移除PEM头尾标记和空白字符 | ||
| 109 | + String publicKeyPEM = pemContent | ||
| 110 | + .replace("-----BEGIN PUBLIC KEY-----", "") | ||
| 111 | + .replace("-----END PUBLIC KEY-----", "") | ||
| 112 | + .replaceAll("\\s", ""); | ||
| 113 | + System.out.println("----公钥:"+publicKeyPEM); | ||
| 114 | + | ||
| 115 | + // 2. Base64解码 | ||
| 116 | + byte[] encoded = Base64.getDecoder().decode(publicKeyPEM); | ||
| 117 | + | ||
| 118 | + // 3. 创建KeyFactory | ||
| 119 | + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); | ||
| 120 | + | ||
| 121 | + // 4. 生成公钥 | ||
| 122 | + return keyFactory.generatePublic(new X509EncodedKeySpec(encoded)); | ||
| 123 | + //byte[] certBytes = Files.readAllBytes(Paths.get(filePath)); | ||
| 124 | + //CertificateFactory cf = CertificateFactory.getInstance("X.509"); | ||
| 125 | + //X509Certificate cert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certBytes)); | ||
| 126 | + //return cert.getPublicKey(); | ||
| 127 | + } catch (Exception e) { | ||
| 128 | + e.printStackTrace(); | ||
| 129 | + throw new GeneralSecurityException("加载微信支付公钥失败", e); | ||
| 130 | + } | ||
| 131 | + } | ||
| 132 | +} |
| 1 | +package com.aigeo.util.pay; | ||
| 2 | + | ||
| 3 | + | ||
| 4 | +import org.apache.http.HttpEntity; | ||
| 5 | +import org.apache.http.HttpResponse; | ||
| 6 | +import org.apache.http.ParseException; | ||
| 7 | +import org.apache.http.client.ClientProtocolException; | ||
| 8 | +import org.apache.http.client.HttpClient; | ||
| 9 | +import org.apache.http.client.methods.HttpGet; | ||
| 10 | +import org.apache.http.client.methods.HttpPost; | ||
| 11 | +import org.apache.http.entity.StringEntity; | ||
| 12 | +import org.apache.http.impl.client.DefaultHttpClient; | ||
| 13 | +import org.apache.http.params.CoreConnectionPNames; | ||
| 14 | +import org.apache.http.protocol.HTTP; | ||
| 15 | +import org.apache.http.util.EntityUtils; | ||
| 16 | + | ||
| 17 | +import javax.net.ssl.HttpsURLConnection; | ||
| 18 | +import javax.net.ssl.KeyManagerFactory; | ||
| 19 | +import javax.net.ssl.SSLContext; | ||
| 20 | +import javax.net.ssl.TrustManagerFactory; | ||
| 21 | +import java.io.*; | ||
| 22 | +import java.net.HttpURLConnection; | ||
| 23 | +import java.net.URL; | ||
| 24 | +import java.security.*; | ||
| 25 | +import java.security.cert.Certificate; | ||
| 26 | +import java.security.cert.CertificateException; | ||
| 27 | +import java.security.cert.CertificateFactory; | ||
| 28 | +import java.util.HashMap; | ||
| 29 | +import java.util.Map; | ||
| 30 | + | ||
| 31 | + | ||
| 32 | +public class HttpClientUtils | ||
| 33 | +{ | ||
| 34 | + public static final String SunX509 = "SunX509"; | ||
| 35 | + public static final String JKS = "JKS"; | ||
| 36 | + public static final String PKCS12 = "PKCS12"; | ||
| 37 | + public static final String TLS = "TLS"; | ||
| 38 | + | ||
| 39 | + public static HttpURLConnection getHttpURLConnection(String strUrl) | ||
| 40 | + throws IOException | ||
| 41 | + { | ||
| 42 | + URL url = new URL(strUrl); | ||
| 43 | + HttpURLConnection httpURLConnection = (HttpURLConnection)url | ||
| 44 | + .openConnection(); | ||
| 45 | + return httpURLConnection; | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + | ||
| 49 | + | ||
| 50 | + /** | ||
| 51 | + * 发送HTTP_GET请求 | ||
| 52 | + * | ||
| 53 | + * @see | ||
| 54 | + * @param reqURL | ||
| 55 | + * 请求地址(含参数) | ||
| 56 | + * @param decodeCharset | ||
| 57 | + * 解码字符集,解析响应数据时用之,其为null时默认采用UTF-8解码 | ||
| 58 | + * @return 远程主机响应正文 | ||
| 59 | + */ | ||
| 60 | + public static String sendGetRequest(String reqURL, String decodeCharset) { | ||
| 61 | + long responseLength = 0; // 响应长度 | ||
| 62 | + String responseContent = null; // 响应内容 | ||
| 63 | + HttpClient httpClient = new DefaultHttpClient(); // 创建默认的httpClient实例 | ||
| 64 | + HttpGet httpGet = new HttpGet(reqURL); // 创建org.apache.http.client.methods.HttpGet | ||
| 65 | + try { | ||
| 66 | + HttpResponse response = httpClient.execute(httpGet); // 执行GET请求 | ||
| 67 | + HttpEntity entity = response.getEntity(); // 获取响应实体 | ||
| 68 | + if (null != entity) { | ||
| 69 | + responseLength = entity.getContentLength(); | ||
| 70 | + responseContent = EntityUtils.toString(entity, decodeCharset == null ? "UTF-8" : decodeCharset); | ||
| 71 | + EntityUtils.consume(entity); // Consume response content | ||
| 72 | + } | ||
| 73 | + System.out.println("请求地址: " + httpGet.getURI()); | ||
| 74 | + System.out.println("响应状态: " + response.getStatusLine()); | ||
| 75 | + System.out.println("响应长度: " + responseLength); | ||
| 76 | + System.out.println("响应内容: " + responseContent); | ||
| 77 | + } catch (ClientProtocolException e) { | ||
| 78 | + System.out.println("该异常通常是协议错误导致,比如构造HttpGet对象时传入的协议不对(将'http'写成'htp')或者服务器端返回的内容不符合HTTP协议要求等,堆栈信息如下"); | ||
| 79 | + } catch (ParseException e) { | ||
| 80 | + System.out.println(e.getMessage()); | ||
| 81 | + } catch (IOException e) { | ||
| 82 | + System.out.println("该异常通常是网络原因引起的,如HTTP服务器未启动等,堆栈信息如下"); | ||
| 83 | + } finally { | ||
| 84 | + httpClient.getConnectionManager().shutdown(); // 关闭连接,释放资源 | ||
| 85 | + } | ||
| 86 | + return responseContent; | ||
| 87 | + } | ||
| 88 | + | ||
| 89 | + public static String sendPostRequest(String reqURL, String sendData, boolean isEncoder) { | ||
| 90 | + return sendPostRequest(reqURL, sendData, isEncoder, null, null,"application/json"); | ||
| 91 | + } | ||
| 92 | + | ||
| 93 | + public static String sendPostRequest(String reqURL, String sendData, boolean isEncoder, String encodeCharset, | ||
| 94 | + String decodeCharset,String contentType) { | ||
| 95 | + String responseContent = null; | ||
| 96 | + HttpClient httpClient = new DefaultHttpClient(); | ||
| 97 | + | ||
| 98 | + HttpPost httpPost = new HttpPost(reqURL); | ||
| 99 | + // httpPost.setHeader(HTTP.CONTENT_TYPE, | ||
| 100 | + // "application/x-www-form-urlencoded; charset=UTF-8"); | ||
| 101 | + httpPost.setHeader(HTTP.CONTENT_TYPE, contentType); | ||
| 102 | + try { | ||
| 103 | + if (isEncoder) { | ||
| 104 | + httpPost.setEntity(new StringEntity(sendData, encodeCharset == null ? "UTF-8" : encodeCharset)); | ||
| 105 | + } else { | ||
| 106 | + httpPost.setEntity(new StringEntity(sendData)); | ||
| 107 | + } | ||
| 108 | + // 设置请求超时时间 | ||
| 109 | + httpClient.getParams().setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 5000); | ||
| 110 | + HttpResponse response = httpClient.execute(httpPost); | ||
| 111 | + HttpEntity entity = response.getEntity(); | ||
| 112 | + if (null != entity) { | ||
| 113 | + responseContent = EntityUtils.toString(entity, decodeCharset == null ? "UTF-8" : decodeCharset); | ||
| 114 | + EntityUtils.consume(entity); | ||
| 115 | + } | ||
| 116 | + } catch (Exception e) { | ||
| 117 | + System.out.println("与[" + reqURL + "]通信过程中发生异常,堆栈信息如下"); | ||
| 118 | + e.printStackTrace(); | ||
| 119 | + } finally { | ||
| 120 | + httpClient.getConnectionManager().shutdown(); | ||
| 121 | + } | ||
| 122 | + return responseContent; | ||
| 123 | + } | ||
| 124 | + | ||
| 125 | + | ||
| 126 | + public static HttpsURLConnection getHttpsURLConnection(String strUrl) | ||
| 127 | + throws IOException | ||
| 128 | + { | ||
| 129 | + URL url = new URL(strUrl); | ||
| 130 | + HttpsURLConnection httpsURLConnection = (HttpsURLConnection)url | ||
| 131 | + .openConnection(); | ||
| 132 | + return httpsURLConnection; | ||
| 133 | + } | ||
| 134 | + | ||
| 135 | + | ||
| 136 | + | ||
| 137 | + | ||
| 138 | + public static String getURL(String strUrl) | ||
| 139 | + { | ||
| 140 | + if (strUrl != null) { | ||
| 141 | + int indexOf = strUrl.indexOf("?"); | ||
| 142 | + if (-1 != indexOf) { | ||
| 143 | + return strUrl.substring(0, indexOf); | ||
| 144 | + } | ||
| 145 | + | ||
| 146 | + return strUrl; | ||
| 147 | + } | ||
| 148 | + | ||
| 149 | + return strUrl; | ||
| 150 | + } | ||
| 151 | + | ||
| 152 | + | ||
| 153 | + | ||
| 154 | + public static String getQueryString(String strUrl) | ||
| 155 | + { | ||
| 156 | + if (strUrl != null) { | ||
| 157 | + int indexOf = strUrl.indexOf("?"); | ||
| 158 | + if (-1 != indexOf) { | ||
| 159 | + return strUrl.substring(indexOf + 1, strUrl.length()); | ||
| 160 | + } | ||
| 161 | + | ||
| 162 | + return ""; | ||
| 163 | + } | ||
| 164 | + | ||
| 165 | + return strUrl; | ||
| 166 | + } | ||
| 167 | + | ||
| 168 | + | ||
| 169 | + | ||
| 170 | + public static Map queryString2Map(String queryString) | ||
| 171 | + { | ||
| 172 | + if ((queryString == null) || ("".equals(queryString))) { | ||
| 173 | + return null; | ||
| 174 | + } | ||
| 175 | + | ||
| 176 | + Map m = new HashMap(); | ||
| 177 | + String[] strArray = queryString.split("&"); | ||
| 178 | + for (int index = 0; index < strArray.length; index++) { | ||
| 179 | + String pair = strArray[index]; | ||
| 180 | + putMapByPair(pair, m); | ||
| 181 | + } | ||
| 182 | + | ||
| 183 | + return m; | ||
| 184 | + } | ||
| 185 | + | ||
| 186 | + | ||
| 187 | + | ||
| 188 | + public static void putMapByPair(String pair, Map m) | ||
| 189 | + { | ||
| 190 | + if ((pair == null) || ("".equals(pair))) { | ||
| 191 | + return; | ||
| 192 | + } | ||
| 193 | + | ||
| 194 | + int indexOf = pair.indexOf("="); | ||
| 195 | + if (-1 != indexOf) { | ||
| 196 | + String k = pair.substring(0, indexOf); | ||
| 197 | + String v = pair.substring(indexOf + 1, pair.length()); | ||
| 198 | + if ((k != null) && (!"".equals(k))) { | ||
| 199 | + m.put(k, v); | ||
| 200 | + } | ||
| 201 | + } else { | ||
| 202 | + m.put(pair, ""); | ||
| 203 | + } | ||
| 204 | + } | ||
| 205 | + | ||
| 206 | + | ||
| 207 | + | ||
| 208 | + | ||
| 209 | + public static String bufferedReader2String(BufferedReader reader) | ||
| 210 | + throws IOException | ||
| 211 | + { | ||
| 212 | + StringBuffer buf = new StringBuffer(); | ||
| 213 | + String line = null; | ||
| 214 | + while ((line = reader.readLine()) != null) { | ||
| 215 | + buf.append(line); | ||
| 216 | + buf.append("\r\n"); | ||
| 217 | + } | ||
| 218 | + | ||
| 219 | + return buf.toString(); | ||
| 220 | + } | ||
| 221 | + | ||
| 222 | + | ||
| 223 | + | ||
| 224 | + | ||
| 225 | + public static void doOutput(OutputStream out, byte[] data, int len) | ||
| 226 | + throws IOException | ||
| 227 | + { | ||
| 228 | + int dataLen = data.length; | ||
| 229 | + int off = 0; | ||
| 230 | + while (off < dataLen) { | ||
| 231 | + if (len >= dataLen) { | ||
| 232 | + out.write(data, off, dataLen); | ||
| 233 | + } else { | ||
| 234 | + out.write(data, off, len); | ||
| 235 | + } | ||
| 236 | + | ||
| 237 | + | ||
| 238 | + out.flush(); | ||
| 239 | + | ||
| 240 | + off += len; | ||
| 241 | + | ||
| 242 | + dataLen -= len; | ||
| 243 | + } | ||
| 244 | + } | ||
| 245 | + | ||
| 246 | + | ||
| 247 | + | ||
| 248 | + | ||
| 249 | + public static SSLContext getSSLContext(FileInputStream trustFileInputStream, String trustPasswd, FileInputStream keyFileInputStream, String keyPasswd) | ||
| 250 | + throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException, UnrecoverableKeyException, KeyManagementException | ||
| 251 | + { | ||
| 252 | + TrustManagerFactory tmf = | ||
| 253 | + TrustManagerFactory.getInstance("SunX509"); | ||
| 254 | + KeyStore trustKeyStore = KeyStore.getInstance("JKS"); | ||
| 255 | + trustKeyStore.load(trustFileInputStream, | ||
| 256 | + str2CharArray(trustPasswd)); | ||
| 257 | + tmf.init(trustKeyStore); | ||
| 258 | + | ||
| 259 | + char[] kp = str2CharArray(keyPasswd); | ||
| 260 | + KeyManagerFactory kmf = | ||
| 261 | + KeyManagerFactory.getInstance("SunX509"); | ||
| 262 | + KeyStore ks = KeyStore.getInstance("PKCS12"); | ||
| 263 | + ks.load(keyFileInputStream, kp); | ||
| 264 | + kmf.init(ks, kp); | ||
| 265 | + | ||
| 266 | + SecureRandom rand = new SecureRandom(); | ||
| 267 | + SSLContext ctx = SSLContext.getInstance("TLS"); | ||
| 268 | + ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), rand); | ||
| 269 | + | ||
| 270 | + return ctx; | ||
| 271 | + } | ||
| 272 | + | ||
| 273 | + | ||
| 274 | + | ||
| 275 | + | ||
| 276 | + | ||
| 277 | + | ||
| 278 | + public static Certificate getCertificate(File cafile) | ||
| 279 | + throws CertificateException, IOException | ||
| 280 | + { | ||
| 281 | + CertificateFactory cf = CertificateFactory.getInstance("X.509"); | ||
| 282 | + FileInputStream in = new FileInputStream(cafile); | ||
| 283 | + Certificate cert = cf.generateCertificate(in); | ||
| 284 | + in.close(); | ||
| 285 | + return cert; | ||
| 286 | + } | ||
| 287 | + | ||
| 288 | + | ||
| 289 | + | ||
| 290 | + | ||
| 291 | + | ||
| 292 | + | ||
| 293 | + public static char[] str2CharArray(String str) | ||
| 294 | + { | ||
| 295 | + if (str == null) { | ||
| 296 | + return null; | ||
| 297 | + } | ||
| 298 | + return str.toCharArray(); | ||
| 299 | + } | ||
| 300 | + | ||
| 301 | + | ||
| 302 | + | ||
| 303 | + | ||
| 304 | + | ||
| 305 | + | ||
| 306 | + | ||
| 307 | + public static void storeCACert(Certificate cert, String alias, String password, OutputStream out) | ||
| 308 | + throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException | ||
| 309 | + { | ||
| 310 | + KeyStore ks = KeyStore.getInstance("JKS"); | ||
| 311 | + | ||
| 312 | + ks.load(null, null); | ||
| 313 | + | ||
| 314 | + ks.setCertificateEntry(alias, cert); | ||
| 315 | + | ||
| 316 | + | ||
| 317 | + ks.store(out, str2CharArray(password)); | ||
| 318 | + } | ||
| 319 | + | ||
| 320 | + public static InputStream String2Inputstream(String str) | ||
| 321 | + { | ||
| 322 | + return new ByteArrayInputStream(str.getBytes()); | ||
| 323 | + } | ||
| 324 | +} |
src/main/java/com/aigeo/util/pay/PayKit.java
0 → 100644
| 1 | +package com.aigeo.util.pay; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.codec.Base64; | ||
| 4 | +import cn.hutool.core.io.FileUtil; | ||
| 5 | +import cn.hutool.core.util.CharsetUtil; | ||
| 6 | +import cn.hutool.core.util.IdUtil; | ||
| 7 | +import cn.hutool.core.util.StrUtil; | ||
| 8 | + | ||
| 9 | +import javax.crypto.BadPaddingException; | ||
| 10 | +import javax.crypto.Cipher; | ||
| 11 | +import javax.crypto.IllegalBlockSizeException; | ||
| 12 | +import javax.crypto.NoSuchPaddingException; | ||
| 13 | +import java.io.InputStream; | ||
| 14 | +import java.io.UnsupportedEncodingException; | ||
| 15 | +import java.net.URLEncoder; | ||
| 16 | +import java.nio.charset.StandardCharsets; | ||
| 17 | +import java.security.InvalidKeyException; | ||
| 18 | +import java.security.NoSuchAlgorithmException; | ||
| 19 | +import java.security.PrivateKey; | ||
| 20 | +import java.security.cert.*; | ||
| 21 | +import java.util.*; | ||
| 22 | + | ||
| 23 | +public class PayKit { | ||
| 24 | + | ||
| 25 | + /** | ||
| 26 | + * 获取证书 | ||
| 27 | + * | ||
| 28 | + * @param inputStream 证书文件 | ||
| 29 | + * @return {@link X509Certificate} 获取证书 | ||
| 30 | + */ | ||
| 31 | + public static X509Certificate getCertificate(InputStream inputStream) { | ||
| 32 | + try { | ||
| 33 | + CertificateFactory cf = CertificateFactory.getInstance("X509"); | ||
| 34 | + X509Certificate cert = (X509Certificate) cf.generateCertificate(inputStream); | ||
| 35 | + cert.checkValidity(); | ||
| 36 | + return cert; | ||
| 37 | + } catch (CertificateExpiredException e) { | ||
| 38 | + throw new RuntimeException("证书已过期", e); | ||
| 39 | + } catch (CertificateNotYetValidException e) { | ||
| 40 | + throw new RuntimeException("证书尚未生效", e); | ||
| 41 | + } catch (CertificateException e) { | ||
| 42 | + throw new RuntimeException("无效的证书", e); | ||
| 43 | + } | ||
| 44 | + } | ||
| 45 | + /** | ||
| 46 | + * 简化的UUID,去掉了横线,使用性能更好的 ThreadLocalRandom 生成UUID | ||
| 47 | + * | ||
| 48 | + * @return 简化的 UUID,去掉了横线 | ||
| 49 | + */ | ||
| 50 | + public static String generateStr() { | ||
| 51 | + return IdUtil.fastSimpleUUID(); | ||
| 52 | + } | ||
| 53 | + | ||
| 54 | + /** | ||
| 55 | + * 构造签名串 | ||
| 56 | + * | ||
| 57 | + * @param method {@link} GET,POST,PUT等 | ||
| 58 | + * @param url 请求接口 /v3/certificates | ||
| 59 | + * @param timestamp 获取发起请求时的系统当前时间戳 | ||
| 60 | + * @param nonceStr 随机字符串 | ||
| 61 | + * @param body 请求报文主体 | ||
| 62 | + * @return 待签名字符串 | ||
| 63 | + */ | ||
| 64 | + public static String buildSignMessage(String method, String url, String timestamp, String nonceStr, String body) { | ||
| 65 | + ArrayList<String> arrayList = new ArrayList<>(); | ||
| 66 | + arrayList.add(method); | ||
| 67 | + arrayList.add(url); | ||
| 68 | + arrayList.add(String.valueOf(timestamp)); | ||
| 69 | + arrayList.add(nonceStr); | ||
| 70 | + arrayList.add(body); | ||
| 71 | + return buildSignMessage(arrayList); | ||
| 72 | + } | ||
| 73 | + /** | ||
| 74 | + * 构造签名串 | ||
| 75 | + * | ||
| 76 | + * @param signMessage 待签名的参数 | ||
| 77 | + * @return 构造后带待签名串 | ||
| 78 | + */ | ||
| 79 | + public static String buildSignMessage(ArrayList<String> signMessage) { | ||
| 80 | + if (signMessage == null || signMessage.size() <= 0) { | ||
| 81 | + return null; | ||
| 82 | + } | ||
| 83 | + StringBuilder sbf = new StringBuilder(); | ||
| 84 | + for (String str : signMessage) { | ||
| 85 | + sbf.append(str).append("\n"); | ||
| 86 | + } | ||
| 87 | + System.out.println("待签名字符串内容:" + sbf.toString()); | ||
| 88 | + System.out.println("待签名字符串长度:" + sbf.toString().length()); | ||
| 89 | + return sbf.toString(); | ||
| 90 | + } | ||
| 91 | + /** | ||
| 92 | + * v3 接口创建签名 | ||
| 93 | + * | ||
| 94 | + * @param signMessage 待签名的参数 | ||
| 95 | + * @param keyPath key.pem 证书路径 | ||
| 96 | + * @return 生成 v3 签名 | ||
| 97 | + * @throws Exception 异常信息 | ||
| 98 | + */ | ||
| 99 | + public static String createSign(String signMessage, String keyPath) throws Exception { | ||
| 100 | + if (StrUtil.isEmpty(signMessage)) { | ||
| 101 | + return null; | ||
| 102 | + } | ||
| 103 | + // 获取商户私钥 | ||
| 104 | + PrivateKey privateKey = PayKit.getPrivateKey(keyPath); | ||
| 105 | + // 生成签名 | ||
| 106 | + return RsaKit.encryptByPrivateKey(signMessage, privateKey); | ||
| 107 | + } | ||
| 108 | + | ||
| 109 | + /** | ||
| 110 | + * 获取商户私钥 | ||
| 111 | + * | ||
| 112 | + * @param keyPath 商户私钥证书路径 | ||
| 113 | + * @return {@link PrivateKey} 商户私钥 | ||
| 114 | + * @throws Exception 异常信息 | ||
| 115 | + */ | ||
| 116 | + public static PrivateKey getPrivateKey(String keyPath) throws Exception { | ||
| 117 | + String originalKey = FileUtil.readUtf8String(keyPath); | ||
| 118 | + String privateKey = originalKey | ||
| 119 | + .replace("-----BEGIN PRIVATE KEY-----", "") | ||
| 120 | + .replace("-----END PRIVATE KEY-----", "") | ||
| 121 | + .replaceAll("\\s+", ""); | ||
| 122 | + | ||
| 123 | + return RsaKit.loadPrivateKey(privateKey); | ||
| 124 | + } | ||
| 125 | + | ||
| 126 | + /** | ||
| 127 | + * 获取授权认证信息 | ||
| 128 | + * | ||
| 129 | + * @param mchId 商户号 | ||
| 130 | + * @param serialNo 商户API证书序列号 | ||
| 131 | + * @param nonceStr 请求随机串 | ||
| 132 | + * @param timestamp 时间戳 | ||
| 133 | + * @param signature 签名值 | ||
| 134 | + * @param authType 认证类型,目前为WECHATPAY2-SHA256-RSA2048 | ||
| 135 | + * @return 请求头 Authorization | ||
| 136 | + */ | ||
| 137 | + public static String getAuthorization(String mchId, String serialNo, String nonceStr, String timestamp, String signature, String authType) { | ||
| 138 | + Map<String, String> params = new HashMap<>(5); | ||
| 139 | + params.put("mchid", mchId); | ||
| 140 | + params.put("serial_no", serialNo); | ||
| 141 | + params.put("nonce_str", nonceStr); | ||
| 142 | + params.put("timestamp", timestamp); | ||
| 143 | + params.put("signature", signature); | ||
| 144 | + return authType.concat(" ").concat(createLinkString(params, ",", false, true)); | ||
| 145 | + } | ||
| 146 | + /** | ||
| 147 | + * @param params 需要排序并参与字符拼接的参数组 | ||
| 148 | + * @param encode 是否进行URLEncoder | ||
| 149 | + * @return 拼接后字符串 | ||
| 150 | + */ | ||
| 151 | + public static String createLinkString(Map<String, String> params, boolean encode) { | ||
| 152 | + return createLinkString(params, "&", encode); | ||
| 153 | + } | ||
| 154 | + /** | ||
| 155 | + * @param params 需要排序并参与字符拼接的参数组 | ||
| 156 | + * @param connStr 连接符号 | ||
| 157 | + * @param encode 是否进行URLEncoder | ||
| 158 | + * @return 拼接后字符串 | ||
| 159 | + */ | ||
| 160 | + public static String createLinkString(Map<String, String> params, String connStr, boolean encode) { | ||
| 161 | + return createLinkString(params, connStr, encode, false); | ||
| 162 | + } | ||
| 163 | + | ||
| 164 | + public static String createLinkString(Map<String, String> params, String connStr, boolean encode, boolean quotes) { | ||
| 165 | + List<String> keys = new ArrayList<>(params.keySet()); | ||
| 166 | + Collections.sort(keys); | ||
| 167 | + StringBuilder content = new StringBuilder(); | ||
| 168 | + for (int i = 0; i < keys.size(); i++) { | ||
| 169 | + String key = keys.get(i); | ||
| 170 | + String value = params.get(key); | ||
| 171 | + // 拼接时,不包括最后一个&字符 | ||
| 172 | + if (i == keys.size() - 1) { | ||
| 173 | + if (quotes) { | ||
| 174 | + content.append(key).append("=").append('"').append(encode ? urlEncode(value) : value).append('"'); | ||
| 175 | + } else { | ||
| 176 | + content.append(key).append("=").append(encode ? urlEncode(value) : value); | ||
| 177 | + } | ||
| 178 | + } else { | ||
| 179 | + if (quotes) { | ||
| 180 | + content.append(key).append("=").append('"').append(encode ? urlEncode(value) : value).append('"').append(connStr); | ||
| 181 | + } else { | ||
| 182 | + content.append(key).append("=").append(encode ? urlEncode(value) : value).append(connStr); | ||
| 183 | + } | ||
| 184 | + } | ||
| 185 | + } | ||
| 186 | + return content.toString(); | ||
| 187 | + } | ||
| 188 | + | ||
| 189 | + /** | ||
| 190 | + * URL 编码 | ||
| 191 | + * | ||
| 192 | + * @param src 需要编码的字符串 | ||
| 193 | + * @return 编码后的字符串 | ||
| 194 | + */ | ||
| 195 | + public static String urlEncode(String src) { | ||
| 196 | + try { | ||
| 197 | + return URLEncoder.encode(src, CharsetUtil.UTF_8).replace("+", "%20"); | ||
| 198 | + } catch (UnsupportedEncodingException e) { | ||
| 199 | + e.printStackTrace(); | ||
| 200 | + return null; | ||
| 201 | + } | ||
| 202 | + } | ||
| 203 | + | ||
| 204 | + /** | ||
| 205 | + * 构造签名串 | ||
| 206 | + * | ||
| 207 | + * @param timestamp 应答时间戳 | ||
| 208 | + * @param nonceStr 应答随机串 | ||
| 209 | + * @param body 应答报文主体 | ||
| 210 | + * @return 应答待签名字符串 | ||
| 211 | + */ | ||
| 212 | + public static String buildSignMessage(String timestamp, String nonceStr, String body) { | ||
| 213 | + ArrayList<String> arrayList = new ArrayList<>(); | ||
| 214 | + arrayList.add(timestamp); | ||
| 215 | + arrayList.add(nonceStr); | ||
| 216 | + arrayList.add(body); | ||
| 217 | + return buildSignMessage(arrayList); | ||
| 218 | + } | ||
| 219 | + | ||
| 220 | + /** | ||
| 221 | + * 公钥加密 | ||
| 222 | + * | ||
| 223 | + * @param data 待加密数据 | ||
| 224 | + * @param certificate 平台公钥证书 | ||
| 225 | + * @return 加密后的数据 | ||
| 226 | + * @throws Exception 异常信息 | ||
| 227 | + */ | ||
| 228 | + public static String rsaEncryptOAEP(String data, X509Certificate certificate) throws Exception { | ||
| 229 | + try { | ||
| 230 | + Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding"); | ||
| 231 | + cipher.init(Cipher.ENCRYPT_MODE, certificate.getPublicKey()); | ||
| 232 | + | ||
| 233 | + byte[] dataByte = data.getBytes(StandardCharsets.UTF_8); | ||
| 234 | + byte[] cipherData = cipher.doFinal(dataByte); | ||
| 235 | + return Base64.encode(cipherData); | ||
| 236 | + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { | ||
| 237 | + throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", e); | ||
| 238 | + } catch (InvalidKeyException e) { | ||
| 239 | + throw new IllegalArgumentException("无效的证书", e); | ||
| 240 | + } catch (IllegalBlockSizeException | BadPaddingException e) { | ||
| 241 | + throw new IllegalBlockSizeException("加密原串的长度不能超过214字节"); | ||
| 242 | + } | ||
| 243 | + } | ||
| 244 | + | ||
| 245 | + /** | ||
| 246 | + * 私钥解密 | ||
| 247 | + * | ||
| 248 | + * @param cipherText 加密字符 | ||
| 249 | + * @param privateKey 私钥 | ||
| 250 | + * @return 解密后的数据 | ||
| 251 | + * @throws Exception 异常信息 | ||
| 252 | + */ | ||
| 253 | + public static String rsaDecryptOAEP(String cipherText, PrivateKey privateKey) throws Exception { | ||
| 254 | + try { | ||
| 255 | + Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding"); | ||
| 256 | + cipher.init(Cipher.DECRYPT_MODE, privateKey); | ||
| 257 | + byte[] data = Base64.decode(cipherText); | ||
| 258 | + return new String(cipher.doFinal(data), StandardCharsets.UTF_8); | ||
| 259 | + } catch (NoSuchPaddingException | NoSuchAlgorithmException e) { | ||
| 260 | + throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", e); | ||
| 261 | + } catch (InvalidKeyException e) { | ||
| 262 | + throw new IllegalArgumentException("无效的私钥", e); | ||
| 263 | + } catch (BadPaddingException | IllegalBlockSizeException e) { | ||
| 264 | + throw new BadPaddingException("解密失败"); | ||
| 265 | + } | ||
| 266 | + } | ||
| 267 | +} |
src/main/java/com/aigeo/util/pay/PayPal.java
0 → 100644
| 1 | +package com.aigeo.util.pay; | ||
| 2 | + | ||
| 3 | +import com.aigeo.entity.*; | ||
| 4 | +import com.alibaba.fastjson.JSON; | ||
| 5 | +import com.alibaba.fastjson.JSONArray; | ||
| 6 | +import com.alibaba.fastjson.JSONObject; | ||
| 7 | +import lombok.extern.slf4j.Slf4j; | ||
| 8 | +import okhttp3.*; | ||
| 9 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 10 | +import org.springframework.stereotype.Component; | ||
| 11 | + | ||
| 12 | +import java.io.IOException; | ||
| 13 | +import java.text.SimpleDateFormat; | ||
| 14 | +import java.util.Base64; | ||
| 15 | +import java.util.Date; | ||
| 16 | +import java.util.Objects; | ||
| 17 | +import java.util.UUID; | ||
| 18 | + | ||
| 19 | +@Slf4j | ||
| 20 | +@Component | ||
| 21 | +public class PayPal implements PaymentStrategy{ | ||
| 22 | + | ||
| 23 | + private static final String TOKEN_URL = "/v1/oauth2/token"; | ||
| 24 | + | ||
| 25 | + private static final String ORDER_URL = "/v2/checkout/orders"; | ||
| 26 | + | ||
| 27 | +// @Autowired | ||
| 28 | +// private PaypalOrderRecordDao paypalOrderRecordDao; | ||
| 29 | + | ||
| 30 | + @Override | ||
| 31 | + public String pay(PayDto payDto) throws IOException { | ||
| 32 | + ApiConfig config = payDto.getApiConfig(); | ||
| 33 | + String tokenUrl=config.getApiUrl()+TOKEN_URL; | ||
| 34 | + String token = getAccessToken(tokenUrl,config.getApiId(),config.getApiKey()); | ||
| 35 | + OkHttpClient client = new OkHttpClient(); | ||
| 36 | + String url=config.getApiUrl()+ORDER_URL; | ||
| 37 | + | ||
| 38 | + PayPalDTO palDTO = new PayPalDTO(); | ||
| 39 | + palDTO.setIntent("CAPTURE"); | ||
| 40 | + PayPalDTO.PaymentSource paymentSource = new PayPalDTO.PaymentSource(); | ||
| 41 | + PayPalDTO.PaymentSource.Paypal paypal = new PayPalDTO.PaymentSource.Paypal(); | ||
| 42 | + | ||
| 43 | + PayPalDTO.PaymentSource.Paypal.ExperienceContext experienceContext = new PayPalDTO.PaymentSource.Paypal.ExperienceContext(); | ||
| 44 | + experienceContext.setReturn_url(config.getApiRedirectUri()+"/"+payDto.getCompanyId()); | ||
| 45 | + experienceContext.setCancel_url(config.getApiRedirectUri()); | ||
| 46 | + | ||
| 47 | + paypal.setExperience_context(experienceContext); | ||
| 48 | + paymentSource.setPaypal(paypal); | ||
| 49 | + palDTO.setPayment_source(paymentSource); | ||
| 50 | + | ||
| 51 | + PayPalDTO.PurchaseUnit.Amount amount = new PayPalDTO.PurchaseUnit.Amount(); | ||
| 52 | + amount.setCurrency_code("USD"); | ||
| 53 | + amount.setValue(payDto.getAmount()); | ||
| 54 | + | ||
| 55 | + PayPalDTO.PurchaseUnit purchaseUnit = new PayPalDTO.PurchaseUnit(); | ||
| 56 | + purchaseUnit.setCustom_id(payDto.getOrderId()); | ||
| 57 | + purchaseUnit.setAmount(amount); | ||
| 58 | + PayPalDTO.PurchaseUnit[] list = new PayPalDTO.PurchaseUnit[]{purchaseUnit}; | ||
| 59 | + palDTO.setPurchase_units(list); | ||
| 60 | + | ||
| 61 | + String toJson = JSONObject.toJSONString(palDTO); | ||
| 62 | + RequestBody body = RequestBody.create(toJson, MediaType.parse("application/json; charset=utf-8")); | ||
| 63 | + String string = UUID.randomUUID().toString(); | ||
| 64 | + Request request = new Request.Builder() | ||
| 65 | + .url(url) | ||
| 66 | + .post(body) | ||
| 67 | + .addHeader("Content-Type", "application/json") | ||
| 68 | + .addHeader("PayPal-Request-Id", string) | ||
| 69 | + .addHeader("Authorization", "Bearer "+token) | ||
| 70 | + .build(); | ||
| 71 | + | ||
| 72 | + try (Response response = client.newCall(request).execute()) { | ||
| 73 | + if (response.isSuccessful() && response.body() != null) { | ||
| 74 | + String res = Objects.requireNonNull(response.body()).string(); | ||
| 75 | + log.info("响应内容:{}",res); | ||
| 76 | + PayPalResponse payPalResponse = JSONObject.parseObject(res, PayPalResponse.class); | ||
| 77 | + PayPalResponse.ResMsg[] link = payPalResponse.getLinks(); | ||
| 78 | + log.info("订单:{}支付地址:{}",payDto.getOrderId(),link[1].getHref()); | ||
| 79 | + Date now = new Date(); | ||
| 80 | + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); | ||
| 81 | + String formattedTime = formatter.format(now); | ||
| 82 | + PayPalOrderRecord orderRecord = new PayPalOrderRecord(); | ||
| 83 | + orderRecord.setCompanyId(payDto.getCompanyId()); | ||
| 84 | + orderRecord.setOrderId(payDto.getOrderId()); | ||
| 85 | + orderRecord.setPaypalId(payPalResponse.getId()); | ||
| 86 | + orderRecord.setState(0); | ||
| 87 | + orderRecord.setCreateTime(formattedTime); | ||
| 88 | + orderRecord.setUpdateTime(formattedTime); | ||
| 89 | +// paypalOrderRecordDao.addPojo(orderRecord); | ||
| 90 | + return link[1].getHref(); | ||
| 91 | + } else { | ||
| 92 | + System.out.println("Request failed: " + response.code()); | ||
| 93 | + System.out.println("Response: " + response.body().string()); | ||
| 94 | + return null; | ||
| 95 | + } | ||
| 96 | + } catch (IOException e) { | ||
| 97 | + e.printStackTrace(); | ||
| 98 | + } | ||
| 99 | + return null; | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + @Override | ||
| 103 | + public void closeTrade(String orderId, String mchId, ApiConfig apiConfig) { | ||
| 104 | + | ||
| 105 | + } | ||
| 106 | + | ||
| 107 | + @Override | ||
| 108 | + public void refund() { | ||
| 109 | + | ||
| 110 | + } | ||
| 111 | + | ||
| 112 | + public static String getAccessToken(String tokenUrl,String clientId,String clientSecret) throws IOException { | ||
| 113 | + OkHttpClient client = new OkHttpClient(); | ||
| 114 | + | ||
| 115 | + // 创建请求体 | ||
| 116 | + RequestBody body = new FormBody.Builder() | ||
| 117 | + .add("grant_type", "client_credentials") | ||
| 118 | + .build(); | ||
| 119 | + | ||
| 120 | + // 创建请求 | ||
| 121 | + Request request = new Request.Builder() | ||
| 122 | + .url(tokenUrl) | ||
| 123 | + .post(body) | ||
| 124 | + .addHeader("Content-Type", "application/x-www-form-urlencoded") | ||
| 125 | + .addHeader("Authorization", "Basic " + getBasicAuth(clientId,clientSecret)) | ||
| 126 | + .build(); | ||
| 127 | + | ||
| 128 | + // 发送请求 | ||
| 129 | + try (Response response = client.newCall(request).execute()) { | ||
| 130 | + if (response.isSuccessful() && response.body() != null) { | ||
| 131 | + // 解析响应 | ||
| 132 | + String responseBody = response.body().string(); | ||
| 133 | + System.out.println("*-*--**--*-*-*-*-**--**-*-"+parseAccessToken(responseBody)); | ||
| 134 | + return parseAccessToken(responseBody); | ||
| 135 | + } else { | ||
| 136 | + throw new IOException("Failed to get access token: " + response.code()); | ||
| 137 | + } | ||
| 138 | + } | ||
| 139 | + } | ||
| 140 | + | ||
| 141 | + private static String getBasicAuth(String clientId,String clientSecret) { | ||
| 142 | + return Base64.getEncoder().encodeToString((clientId + ":" + clientSecret).getBytes()); | ||
| 143 | + } | ||
| 144 | + | ||
| 145 | + private static String parseAccessToken(String responseBody) { | ||
| 146 | + // 简单解析 JSON 响应 | ||
| 147 | + String[] parts = responseBody.split(","); | ||
| 148 | + for (String part : parts) { | ||
| 149 | + if (part.contains("access_token")) { | ||
| 150 | + return part.split(":")[1].trim().replace("\"", ""); | ||
| 151 | + } | ||
| 152 | + } | ||
| 153 | + throw new IllegalArgumentException("Access token not found in response: " + responseBody); | ||
| 154 | + } | ||
| 155 | + | ||
| 156 | + public static String extractPayerActionUrl(String jsonResponse) { | ||
| 157 | + // 将 JSON 字符串解析为 JSONObject | ||
| 158 | + JSONObject jsonObject = JSON.parseObject(jsonResponse); | ||
| 159 | + | ||
| 160 | + // 获取 links 数组 | ||
| 161 | + JSONArray links = jsonObject.getJSONArray("links"); | ||
| 162 | + | ||
| 163 | + // 遍历 links 数组 | ||
| 164 | + for (int i = 0; i < links.size(); i++) { | ||
| 165 | + JSONObject link = links.getJSONObject(i); | ||
| 166 | + // 检查 rel 是否为 payer-action | ||
| 167 | + if ("payer-action".equals(link.getString("rel"))) { | ||
| 168 | + // 返回 href 值 | ||
| 169 | + return link.getString("href"); | ||
| 170 | + } | ||
| 171 | + } | ||
| 172 | + | ||
| 173 | + // 如果没有找到 payer-action 链接,返回 null | ||
| 174 | + return null; | ||
| 175 | + } | ||
| 176 | + | ||
| 177 | + @Override | ||
| 178 | + public String getChannel() { | ||
| 179 | + return "paypal"; | ||
| 180 | + } | ||
| 181 | +} |
| 1 | +package com.aigeo.util.pay; | ||
| 2 | + | ||
| 3 | + | ||
| 4 | +import com.aigeo.entity.ApiConfig; | ||
| 5 | +import com.aigeo.entity.PayDto; | ||
| 6 | +import com.alipay.api.AlipayApiException; | ||
| 7 | + | ||
| 8 | +import java.io.IOException; | ||
| 9 | + | ||
| 10 | +public interface PaymentStrategy { | ||
| 11 | + //支付 | ||
| 12 | + String pay(PayDto payDto) throws AlipayApiException, IOException; | ||
| 13 | + | ||
| 14 | + //根据系统订单号查询订单 | ||
| 15 | + | ||
| 16 | + //根据微信支付订单号查询订单 | ||
| 17 | + | ||
| 18 | + //关闭订单 | ||
| 19 | + void closeTrade(String orderId, String mchId, ApiConfig apiConfig); | ||
| 20 | + //退款申请 | ||
| 21 | + void refund(); | ||
| 22 | + //查询退款申请 | ||
| 23 | + | ||
| 24 | + // 支持的支付渠道标识 | ||
| 25 | + String getChannel(); | ||
| 26 | +} |
| 1 | +package com.aigeo.util.pay; | ||
| 2 | + | ||
| 3 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 4 | +import org.springframework.stereotype.Component; | ||
| 5 | + | ||
| 6 | +import javax.annotation.PostConstruct; | ||
| 7 | +import java.util.List; | ||
| 8 | +import java.util.Map; | ||
| 9 | +import java.util.concurrent.ConcurrentHashMap; | ||
| 10 | + | ||
| 11 | +@Component | ||
| 12 | +public class PaymentStrategyFactory { | ||
| 13 | + @Autowired | ||
| 14 | + private List<PaymentStrategy> strategies; | ||
| 15 | + | ||
| 16 | + private final Map<String, PaymentStrategy> strategyMap = new ConcurrentHashMap<>(); | ||
| 17 | + | ||
| 18 | + @PostConstruct | ||
| 19 | + public void init() { | ||
| 20 | + strategies.forEach(strategy -> strategyMap.put(strategy.getChannel(), strategy)); | ||
| 21 | + } | ||
| 22 | + | ||
| 23 | + public PaymentStrategy getStrategy(String channel) { | ||
| 24 | + PaymentStrategy strategy = strategyMap.get(channel); | ||
| 25 | + if (strategy == null) { | ||
| 26 | + throw new IllegalArgumentException("Unsupported payment channel: " + channel); | ||
| 27 | + } | ||
| 28 | + return strategy; | ||
| 29 | + } | ||
| 30 | +} |
src/main/java/com/aigeo/util/pay/RsaKit.java
0 → 100644
| 1 | +package com.aigeo.util.pay; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.codec.Base64; | ||
| 4 | +import cn.hutool.core.util.StrUtil; | ||
| 5 | + | ||
| 6 | +import java.nio.charset.StandardCharsets; | ||
| 7 | +import java.security.KeyFactory; | ||
| 8 | +import java.security.NoSuchAlgorithmException; | ||
| 9 | +import java.security.PrivateKey; | ||
| 10 | +import java.security.PublicKey; | ||
| 11 | +import java.security.spec.InvalidKeySpecException; | ||
| 12 | +import java.security.spec.PKCS8EncodedKeySpec; | ||
| 13 | + | ||
| 14 | +public class RsaKit { | ||
| 15 | + | ||
| 16 | + /** | ||
| 17 | + * 加密算法RSA | ||
| 18 | + */ | ||
| 19 | + private static final String KEY_ALGORITHM = "RSA"; | ||
| 20 | + | ||
| 21 | + /** | ||
| 22 | + * 私钥签名 | ||
| 23 | + * | ||
| 24 | + * @param data 需要加密的数据 | ||
| 25 | + * @param privateKey 私钥 | ||
| 26 | + * @return 加密后的数据 | ||
| 27 | + * @throws Exception 异常信息 | ||
| 28 | + */ | ||
| 29 | + public static String encryptByPrivateKey(String data, PrivateKey privateKey) throws Exception { | ||
| 30 | + java.security.Signature signature = java.security.Signature.getInstance("SHA256WithRSA"); | ||
| 31 | + signature.initSign(privateKey); | ||
| 32 | + signature.update(data.getBytes(StandardCharsets.UTF_8)); | ||
| 33 | + byte[] signed = signature.sign(); | ||
| 34 | + return StrUtil.str(Base64.encode(signed)); | ||
| 35 | + } | ||
| 36 | + | ||
| 37 | + /** | ||
| 38 | + * 从字符串中加载私钥<br> | ||
| 39 | + * 加载时使用的是PKCS8EncodedKeySpec(PKCS#8编码的Key指令)。 | ||
| 40 | + * | ||
| 41 | + * @param privateKeyStr 私钥 | ||
| 42 | + * @return {@link PrivateKey} | ||
| 43 | + * @throws Exception 异常信息 | ||
| 44 | + */ | ||
| 45 | + public static PrivateKey loadPrivateKey(String privateKeyStr) throws Exception { | ||
| 46 | + try { | ||
| 47 | + byte[] buffer = Base64.decode(privateKeyStr); | ||
| 48 | + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(buffer); | ||
| 49 | + KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); | ||
| 50 | + return keyFactory.generatePrivate(keySpec); | ||
| 51 | + } catch (NoSuchAlgorithmException e) { | ||
| 52 | + throw new Exception("无此算法"); | ||
| 53 | + } catch (InvalidKeySpecException e) { | ||
| 54 | + throw new Exception("私钥非法"); | ||
| 55 | + } catch (NullPointerException e) { | ||
| 56 | + throw new Exception("私钥数据为空"); | ||
| 57 | + } | ||
| 58 | + } | ||
| 59 | + /** | ||
| 60 | + * 公钥验证签名 | ||
| 61 | + * | ||
| 62 | + * @param data 需要加密的数据 | ||
| 63 | + * @param sign 签名 | ||
| 64 | + * @param publicKey 公钥 | ||
| 65 | + * @return 验证结果 | ||
| 66 | + * @throws Exception 异常信息 | ||
| 67 | + */ | ||
| 68 | + public static boolean checkByPublicKey(String data, String sign, PublicKey publicKey) throws Exception { | ||
| 69 | + java.security.Signature signature = java.security.Signature.getInstance("SHA256WithRSA"); | ||
| 70 | + signature.initVerify(publicKey); | ||
| 71 | + signature.update(data.getBytes(StandardCharsets.UTF_8)); | ||
| 72 | + return signature.verify(Base64.decode(sign.getBytes(StandardCharsets.UTF_8))); | ||
| 73 | + } | ||
| 74 | +} |
| 1 | +package com.aigeo.util.pay; | ||
| 2 | + | ||
| 3 | +import com.aigeo.entity.ApiConfig; | ||
| 4 | +import com.aigeo.entity.PayDto; | ||
| 5 | +import com.wechat.pay.java.core.Config; | ||
| 6 | +import com.wechat.pay.java.core.RSAAutoCertificateConfig; | ||
| 7 | +import com.wechat.pay.java.service.payments.nativepay.NativePayService; | ||
| 8 | +import com.wechat.pay.java.service.payments.nativepay.model.Amount; | ||
| 9 | +import com.wechat.pay.java.service.payments.nativepay.model.PrepayRequest; | ||
| 10 | +import com.wechat.pay.java.service.payments.nativepay.model.PrepayResponse; | ||
| 11 | +import org.springframework.stereotype.Component; | ||
| 12 | + | ||
| 13 | +import java.math.BigDecimal; | ||
| 14 | +import java.math.RoundingMode; | ||
| 15 | +import java.time.ZoneId; | ||
| 16 | +import java.time.ZonedDateTime; | ||
| 17 | +import java.time.format.DateTimeFormatter; | ||
| 18 | + | ||
| 19 | +@Component | ||
| 20 | +public class WechatPay implements PaymentStrategy{ | ||
| 21 | + /** 全局单例配置 */ | ||
| 22 | + //private static volatile Config globalConfig; | ||
| 23 | + | ||
| 24 | + //@Autowired | ||
| 25 | + //private ApiConfigDao apiConfigDao; | ||
| 26 | + | ||
| 27 | + private static String HOST = "https://api.mch.weixin.qq.com"; | ||
| 28 | + private static String METHOD = "POST"; | ||
| 29 | + private static String PATH = "/v3/pay/partner/transactions/out-trade-no/{out_trade_no}/close"; | ||
| 30 | + | ||
| 31 | + /** | ||
| 32 | + * 初始化完毕(所有 @Value 已注入)后,构造一次并缓存 | ||
| 33 | + */ | ||
| 34 | + //@PostConstruct | ||
| 35 | + //public void initConfig() throws Exception { | ||
| 36 | + // if (globalConfig == null) { | ||
| 37 | + // synchronized (WechatPay.class) { | ||
| 38 | + // if (globalConfig == null) { | ||
| 39 | + // ApiConfig wechatapy = apiConfigDao.findByApiCode(7,"wechatpay"); | ||
| 40 | + // if(wechatapy==null){ | ||
| 41 | + // throw new RuntimeException("微信支付初始化失败!"); | ||
| 42 | + // } | ||
| 43 | + // globalConfig = new RSAAutoCertificateConfig.Builder() | ||
| 44 | + // .merchantId(wechatapy.getMerchantId()) | ||
| 45 | + // .privateKeyFromPath(wechatapy.getPrivateKeyPath()) | ||
| 46 | + // .merchantSerialNumber(wechatapy.getApiCode()) | ||
| 47 | + // .apiV3Key(wechatapy.getApiKey()) | ||
| 48 | + // .build(); | ||
| 49 | + // } | ||
| 50 | + // } | ||
| 51 | + // } | ||
| 52 | + //} | ||
| 53 | + | ||
| 54 | + | ||
| 55 | + @Override | ||
| 56 | + public String pay(PayDto payDto) { | ||
| 57 | + ApiConfig apiConfig = payDto.getApiConfig(); | ||
| 58 | + String path = this.getClass().getResource("/").getPath(); | ||
| 59 | + path = path.replace("/", "//"); | ||
| 60 | + path = path.replace("//target//classes//", ""); | ||
| 61 | + path = path.replace("//WEB-INF//classes//", ""); | ||
| 62 | + Config globalConfig=new RSAAutoCertificateConfig.Builder() | ||
| 63 | + .merchantId(apiConfig.getMerchantId()) | ||
| 64 | + .privateKeyFromPath(path+apiConfig.getPrivateKeyPath()) | ||
| 65 | + .merchantSerialNumber(apiConfig.getApiCode()) | ||
| 66 | + .apiV3Key(apiConfig.getApiKey()) | ||
| 67 | + .build(); | ||
| 68 | + NativePayService service = new NativePayService.Builder() | ||
| 69 | + .config(globalConfig) // 复用全局配置 | ||
| 70 | + .build(); | ||
| 71 | + // request.setXxx(val)设置所需参数,具体参数可见Request定义 | ||
| 72 | + PrepayRequest request = new PrepayRequest(); | ||
| 73 | + Amount amount = new Amount(); | ||
| 74 | + | ||
| 75 | + BigDecimal money = new BigDecimal(payDto.getAmount()).multiply(new BigDecimal("100")); | ||
| 76 | + amount.setTotal(money.setScale(0, RoundingMode.FLOOR).intValueExact()); | ||
| 77 | + request.setAmount(amount); | ||
| 78 | + request.setAppid(payDto.getApiConfig().getApiId()); | ||
| 79 | + request.setMchid(payDto.getApiConfig().getMerchantId()); | ||
| 80 | + request.setDescription(payDto.getDesc()); | ||
| 81 | + request.setNotifyUrl(payDto.getApiConfig().getApiRedirectUri()+"/"+payDto.getCompanyId()); | ||
| 82 | + request.setOutTradeNo(payDto.getOrderId()); | ||
| 83 | + request.setTimeExpire(getTime()); | ||
| 84 | + // 调用下单方法,得到应答 | ||
| 85 | + PrepayResponse response = service.prepay(request); | ||
| 86 | + // 使用微信扫描 code_url 对应的二维码,即可体验Native支付 | ||
| 87 | + System.out.println("**-**-*--*-*-**--*-*-*-*-*-*-*"+response); | ||
| 88 | + System.out.println("*-*-*-*-*-*-*-*-*-*-*-"+response.getCodeUrl()+"-*-**-*-*--**--*-*"); | ||
| 89 | + return response.getCodeUrl(); | ||
| 90 | + } | ||
| 91 | + | ||
| 92 | + /** | ||
| 93 | + * 关闭订单 | ||
| 94 | + * @param orderId 订单号 | ||
| 95 | + * @param mchId 商户号 | ||
| 96 | + */ | ||
| 97 | + @Override | ||
| 98 | + public void closeTrade(String orderId, String mchId,ApiConfig apiConfig) { | ||
| 99 | + | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + /** | ||
| 103 | + * 退款申请 | ||
| 104 | + */ | ||
| 105 | + @Override | ||
| 106 | + public void refund() { | ||
| 107 | + | ||
| 108 | + } | ||
| 109 | + | ||
| 110 | + | ||
| 111 | + private String getTime(){ | ||
| 112 | + // 获取当前时间 | ||
| 113 | + ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault()); | ||
| 114 | + | ||
| 115 | + // 加上十分钟 | ||
| 116 | + ZonedDateTime thirtyMinutesLater = now.plusMinutes(10); | ||
| 117 | + | ||
| 118 | + // 定义目标格式 | ||
| 119 | + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX"); | ||
| 120 | + | ||
| 121 | + // 格式化为字符串 | ||
| 122 | + String formattedTime = thirtyMinutesLater.format(formatter); | ||
| 123 | + | ||
| 124 | + System.out.println("订单结束时间:"+formattedTime); | ||
| 125 | + | ||
| 126 | + // 输出结果 | ||
| 127 | + return formattedTime; | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + @Override | ||
| 131 | + public String getChannel() { | ||
| 132 | + return "wechatpay"; | ||
| 133 | + } | ||
| 134 | +} | ||
| 135 | + |
-
请 注册 或 登录 后发表评论