package com.nq.utils.pay; import com.alipay.api.AlipayApiException; import com.alipay.api.AlipayClient; import com.alipay.api.AlipayConfig; import com.alipay.api.DefaultAlipayClient; import com.alipay.api.domain.AlipayTradeWapPayModel; import com.alipay.api.internal.util.AlipaySignature; import com.alipay.api.request.AlipayTradeWapPayRequest; import com.alipay.api.response.AlipayTradeWapPayResponse; import com.nq.utils.PropertiesUtil; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.http.HttpServletRequest; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.security.KeyFactory; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Base64; import java.util.HashMap; import java.util.Map; /** * 支付宝手机网站支付(alipay.trade.wap.pay),对接官方 SDK 示例写法。 */ public final class AlipayPayUtil { private static final Logger log = LoggerFactory.getLogger(AlipayPayUtil.class); /** 手机网站支付产品码 */ private static final String PRODUCT_CODE = "QUICK_WAP_WAY"; private static final String SIGN_TYPE = "RSA2"; private static final String FORMAT = "json"; private static final String CHARSET = "UTF-8"; private AlipayPayUtil() { } public static boolean isConfigured() { return getConfigError() == null; } /** @return 配置错误说明,null 表示配置有效 */ public static String getConfigError() { if (StringUtils.isBlank(PropertiesUtil.getProperty("alipay.app-id"))) { return "请填写 alipay.app-id"; } if (StringUtils.isBlank(PropertiesUtil.getProperty("alipay.alipay-public-key"))) { return "请填写 alipay.alipay-public-key(支付宝公钥)"; } String path = PropertiesUtil.getProperty("alipay.private-key-path"); String inlineKey = PropertiesUtil.getProperty("alipay.private-key"); if (StringUtils.isAllBlank(path, inlineKey)) { return "请填写 alipay.private-key(应用私钥 PKCS8)或 alipay.private-key-path(私钥文件路径)"; } try { toSdkPrivateKey(loadPrivateKeyMaterial()); return null; } catch (IllegalArgumentException e) { return e.getMessage(); } catch (Exception e) { return "应用私钥读取失败:" + e.getMessage(); } } public static String buildWapPayForm(String orderSn, String amount, String subject, String notifyUrl, String returnUrl, String quitUrl) throws AlipayApiException { AlipayClient client = createClient(); AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest(); request.setNotifyUrl(notifyUrl); request.setReturnUrl(returnUrl); AlipayTradeWapPayModel model = new AlipayTradeWapPayModel(); model.setOutTradeNo(orderSn); model.setTotalAmount(amount); model.setSubject(subject); model.setProductCode(PRODUCT_CODE); if (StringUtils.isNotBlank(quitUrl)) { model.setQuitUrl(quitUrl); } request.setBizModel(model); AlipayTradeWapPayResponse response = client.pageExecute(request, "POST"); if (response == null || StringUtils.isBlank(response.getBody())) { throw new AlipayApiException("支付宝返回为空"); } if (!response.isSuccess()) { log.error("支付宝 WAP 下单失败 code={} msg={} subCode={} subMsg={}", response.getCode(), response.getMsg(), response.getSubCode(), response.getSubMsg()); throw new AlipayApiException(StringUtils.defaultIfBlank(response.getSubMsg(), response.getMsg())); } return response.getBody(); } public static Map parseRequestParams(HttpServletRequest request) { Map params = new HashMap<>(); Map requestParams = request.getParameterMap(); for (Map.Entry entry : requestParams.entrySet()) { String[] values = entry.getValue(); if (values == null || values.length == 0) { continue; } StringBuilder valueStr = new StringBuilder(); for (int i = 0; i < values.length; i++) { if (i > 0) { valueStr.append(","); } valueStr.append(values[i]); } params.put(entry.getKey(), valueStr.toString()); } return params; } public static boolean verifyNotify(Map params) { try { String alipayPublicKey = normalizePublicKey(PropertiesUtil.getProperty("alipay.alipay-public-key")); return AlipaySignature.rsaCheckV1(params, alipayPublicKey, CHARSET, SIGN_TYPE); } catch (Exception e) { log.error("支付宝回调验签失败", e); return false; } } private static AlipayClient createClient() throws AlipayApiException { try { return new DefaultAlipayClient(buildAlipayConfig()); } catch (IllegalArgumentException e) { throw new AlipayApiException(e.getMessage(), e); } catch (Exception e) { throw new AlipayApiException("支付宝私钥配置错误,请检查 alipay.private-key", e); } } private static AlipayConfig buildAlipayConfig() throws Exception { AlipayConfig config = new AlipayConfig(); config.setServerUrl(StringUtils.defaultIfBlank( PropertiesUtil.getProperty("alipay.gateway-url"), "https://openapi.alipay.com/gateway.do")); config.setAppId(PropertiesUtil.getProperty("alipay.app-id")); config.setPrivateKey(toSdkPrivateKey(loadPrivateKeyMaterial())); config.setFormat(FORMAT); config.setAlipayPublicKey(normalizePublicKey(PropertiesUtil.getProperty("alipay.alipay-public-key"))); config.setCharset(CHARSET); config.setSignType(SIGN_TYPE); return config; } private static String loadPrivateKeyMaterial() { String path = PropertiesUtil.getProperty("alipay.private-key-path"); if (StringUtils.isNotBlank(path)) { try { return new String(Files.readAllBytes(Paths.get(path.trim())), StandardCharsets.UTF_8); } catch (Exception e) { throw new IllegalArgumentException("读取 alipay.private-key-path 失败: " + path, e); } } return PropertiesUtil.getProperty("alipay.private-key"); } static String toSdkPrivateKey(String raw) throws Exception { if (StringUtils.isBlank(raw)) { throw new IllegalArgumentException("alipay.private-key 不能为空"); } String compact = extractKeyBody(raw); if (isLikelyPublicKey(compact)) { throw new IllegalArgumentException( "alipay.private-key 配置错误:当前填入的是公钥。请填写「应用私钥 PKCS8」,不要填应用公钥或支付宝公钥。"); } if (compact.length() < 800) { throw new IllegalArgumentException( "alipay.private-key 长度异常(" + compact.length() + "),应用私钥通常远长于公钥,请确认已完整粘贴 PKCS8 私钥。"); } try { KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(compact))); return compact; } catch (Exception e) { throw new IllegalArgumentException( "应用私钥格式不正确,Java 需使用 PKCS8 格式。请用支付宝开放平台「密钥生成器」生成并选择 PKCS8(Java适用)。", e); } } private static String extractKeyBody(String raw) { String material = raw.trim().replace("\\n", "\n"); return material .replace("-----BEGIN RSA PRIVATE KEY-----", "") .replace("-----END RSA PRIVATE KEY-----", "") .replace("-----BEGIN PRIVATE KEY-----", "") .replace("-----END PRIVATE KEY-----", "") .replace("-----BEGIN PUBLIC KEY-----", "") .replace("-----END PUBLIC KEY-----", "") .replaceAll("\\s+", ""); } private static String normalizePublicKey(String key) { if (StringUtils.isBlank(key)) { return key; } return extractKeyBody(key); } private static boolean isLikelyPublicKey(String compactKey) { return compactKey.startsWith("MIIBIjAN") || compactKey.startsWith("MIGfMA0G"); } }