diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 80a6b6c..5d8be1d 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -12,7 +12,12 @@ ruoyi: addressEnabled: false # 验证码类型 math 数字计算 char 字符验证 captchaType: math - +wechat: + pay: + app-id: wx73d0202b3c8a6d68 + mch-id: 1672571923 + api-key: sssssssssssssssssssssssssssssssS + cert-path: wechat/apiclient_cert.p12 # 七牛云配置 qiniu: # 是否启用七牛云上传 true-启用七牛云 false-使用本地上传 diff --git a/ruoyi-admin/src/main/resources/wechat/apiclient_cert.p12 b/ruoyi-admin/src/main/resources/wechat/apiclient_cert.p12 new file mode 100644 index 0000000..69c249d Binary files /dev/null and b/ruoyi-admin/src/main/resources/wechat/apiclient_cert.p12 differ diff --git a/ruoyi-admin/src/main/resources/wechat/apiclient_cert.pem b/ruoyi-admin/src/main/resources/wechat/apiclient_cert.pem new file mode 100644 index 0000000..199193f --- /dev/null +++ b/ruoyi-admin/src/main/resources/wechat/apiclient_cert.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIELjCCAxagAwIBAgIUSSFh2nX1a5drX17lBR7WCwwoi7kwDQYJKoZIhvcNAQEL +BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT +FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg +Q0EwHhcNMjQwNDAzMDEzNTM5WhcNMjkwNDAyMDEzNTM5WjCBhzETMBEGA1UEAwwK +MTY3MjU3MTkyMzEbMBkGA1UECgwS5b6u5L+h5ZWG5oi357O757ufMTMwMQYDVQQL +DCropb/lronljY7lupzkurrlrrboo4XppbDlt6XnqIvmnInpmZDlhazlj7gxCzAJ +BgNVBAYTAkNOMREwDwYDVQQHDAhTaGVuWmhlbjCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBANvnZX1T6o0xR1yOkQy17upOeNsGGE8pn5istcvc5/UTtcg8 +5aH39oAZ0wXruE41wR9bEM6c3FB27w5FcnQonUYgBZvU/jHlIrbogZK1UhvoQMqX +SnKbiQx2oqLxiFXzZjWBz5g8E9Mss4xErRT8XmuE5alWnIc5ljGGYAUNt9qi5bpc +wawSqoBYR1nJM1WtGwHWNUTCi2XYL2H7olPnJw/vv0dpQC3kNF/0bNALzM9ulyf3 +sESbFu6wOJhYqZRseiUQHyXRFGQOQ665rho7YJ4j6y51kfbUAIUp2U4yMf8009pQ +du9XYId9TxjRt/tNxwkRVEIVukAgv90mZwALvyMCAwEAAaOBuTCBtjAJBgNVHRME +AjAAMAsGA1UdDwQEAwID+DCBmwYDVR0fBIGTMIGQMIGNoIGKoIGHhoGEaHR0cDov +L2V2Y2EuaXRydXMuY29tLmNuL3B1YmxpYy9pdHJ1c2NybD9DQT0xQkQ0MjIwRTUw +REJDMDRCMDZBRDM5NzU0OTg0NkMwMUMzRThFQkQyJnNnPUhBQ0M0NzFCNjU0MjJF +MTJCMjdBOUQzM0E4N0FEMUNERjU5MjZFMTQwMzcxMA0GCSqGSIb3DQEBCwUAA4IB +AQASunlG7V6cH05d3z4pthRixzU4Rn5xSW8d09gshq1XA6BBCu6I7glqiEKotrai +oLWrwHDKddCRSRuV3KKC0//fAwS/8j0pkFDI/Aad5TKS361uA38HaHda0bJQnKoB +m1WinL+GsfsLr+SL17ZGZAJqPBeP/Vz/c41cUX2gMAXjkem7DGbtytQiVkyXxocL +/qhlF3Hy3eiMI186ki5ctKkp6WISCRKHWjc1du8ri/T6fvnNv/Rls/GAtpxVBm7V +uFem8oe5heDDe4/RlqPv7sY2Oybkq/9iUfFwYvUNv/yQfb7TiGBQkasAt+PVgI5g +djd8S7m1zDTFWl4LeNBIYaS2 +-----END CERTIFICATE----- diff --git a/ruoyi-admin/src/main/resources/wechat/apiclient_key.pem b/ruoyi-admin/src/main/resources/wechat/apiclient_key.pem new file mode 100644 index 0000000..4555a72 --- /dev/null +++ b/ruoyi-admin/src/main/resources/wechat/apiclient_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDb52V9U+qNMUdc +jpEMte7qTnjbBhhPKZ+YrLXL3Of1E7XIPOWh9/aAGdMF67hONcEfWxDOnNxQdu8O +RXJ0KJ1GIAWb1P4x5SK26IGStVIb6EDKl0pym4kMdqKi8YhV82Y1gc+YPBPTLLOM +RK0U/F5rhOWpVpyHOZYxhmAFDbfaouW6XMGsEqqAWEdZyTNVrRsB1jVEwotl2C9h ++6JT5ycP779HaUAt5DRf9GzQC8zPbpcn97BEmxbusDiYWKmUbHolEB8l0RRkDkOu +ua4aO2CeI+sudZH21ACFKdlOMjH/NNPaUHbvV2CHfU8Y0bf7TccJEVRCFbpAIL/d +JmcAC78jAgMBAAECggEAQBF82jTWw2g2GFtZG1DuWMmgIH/4GShRXVQEa27jDd4+ +Oiaz6Kqr4MqlVrZErlSu/Ym0rd25up/MKmZFYF5s4/90YuB9ZuARayT2i13UnFjg +d6X4hMeX+E+gNsUUKaaOgsxdRUdLp10Aur9Phiu4/q5rkxiu5h77CVbTJuXGSSZe +VEtlUfXsyl5nLqnViqOam0cbD3vM0tKnAtHHootIcz3Bn5a9QGfBA7JKHqWpQIZp +zkd0XcfbZdOLwk+I+HAenFQYK8mOpg999iNVOFOI0egsj9tES9cxkDMezUrYjr/W ++I5KkggwigF0tjgvqcklqNdGIIlLak+jBDfWA2vwgQKBgQD3Sx6C/YafiEv30Phw +K+kmWMT5X+bTJBF8uie62Rlzz6PQs9p7K7iHcWLPtAO93JR6O25/tWIeYWGVyWOF +PyJLNoch+3aDw2e7K9gn14xMb9/rlAMEtKsM1g+W78TJfzGaeH4gGTkzBQYxaoV1 +Zi9LXGEXurpgIRgCYA+2mxIeAwKBgQDjpWmcAXEA6NQBfUWCxlI3xAv5R0G4Adtf +ziKnJZnTxTbe7OZBMugAwxWtuNjJESg4RAPwUlmkaWO+pN4yu6dZsfphsgNUs6kG +1x2LXB72E0bVo0xyMANkeYtgh6Vz9JIfm54CNHEbpf6udTKFAL4QmoW43v68FKSK +6wR6j3ogYQKBgFBwqP4nBdGC/mMgIJAsWGzW10ve/DCWPPH7g8ztra/UTQ8oUdta +/21fXqr9fXIW/F0+U9VK5R3Iw3t7xNmlDby1ggN1zrlRqL5jpq5fGQ4MHiR9QvYW +5sUIJI4OdE6vBy2eIDjEu6xy5+7PHZZwpNW4uQSFAvceDLdJFBNsIdqNAoGAPRmK +jPq+D9ZuALBU1dLRAnK2HAQKGY82CDLVjjNDZFmMbqz66Aj3yCyURvpUBTN7rg5j +WD9iHATFZsftc4R2WFays0IKLtPBzDo0jdD95mIqIvEfdY55eqA8zn3Z5JOCHMWx +1TmqGhelkcGW+6fFW8N9nK8NhAGuYDLQEu0rhEECgYAdsN/WQcG+IcHv/90AajQi +a4gnybw1tuC7PlBDTtrSYbd3YuEZ3ocLTKINvADqh9/rgTaOHs6n4wS8av02XWw+ +AOUF2N2Qvb1nk5FDV87wBjeWDWR1wy8o5njBU45+mnMdelubpKHemkrqkMd0sd7B +Pcd6BtcwZjvzB4UisErFqA== +-----END PRIVATE KEY----- diff --git a/ruoyi-admin/src/main/resources/wechat/pay.pem b/ruoyi-admin/src/main/resources/wechat/pay.pem new file mode 100644 index 0000000..996e99a --- /dev/null +++ b/ruoyi-admin/src/main/resources/wechat/pay.pem @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEFDCCAvygAwIBAgIUSfMroWK5C2gDN/vbxA/Igoszko0wDQYJKoZIhvcNAQEL +BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT +FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg +Q0EwHhcNMjQwNDAzMDEzNTM4WhcNMjkwNDAyMDEzNTM4WjBuMRgwFgYDVQQDDA9U +ZW5wYXkuY29tIHNpZ24xEzARBgNVBAoMClRlbnBheS5jb20xHTAbBgNVBAsMFFRl +bnBheS5jb20gQ0EgQ2VudGVyMQswCQYDVQQGEwJDTjERMA8GA1UEBwwIU2hlblpo +ZW4wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCu+HNPatzFYBeTBflC +fsbpLAz/5RgxMriwVxUsy7i2IdlcCX60ZM9W0a4ZpaR0o61njR0s67dTGDW17Kd5 +bQ8rJBParLSVh4sBn7kRcJFyBzcnjGWECDR5kUpuvV2MW7HzlogGZxWqD55/5ZHz +n4QPi35axPRv/ZCx9KDz2iK86FBad9+CZeYBJ0KvDZDDDq4Y3N9b4e0LNNw5Pq4l +IpuHU1Q8LPytf84UhRdyLh7ofgQmulIwISeqPy+IXB01brIYwh76affri90w8UyS +cDraOC8cgjgWsMc5ZJiRjiPmmWTa2jFbUeh2mHoC4ik3CBGxHNOXtg7fEbYtxVCn +qdQLAgMBAAGjgbkwgbYwCQYDVR0TBAIwADALBgNVHQ8EBAMCA/gwgZsGA1UdHwSB +kzCBkDCBjaCBiqCBh4aBhGh0dHA6Ly9ldmNhLml0cnVzLmNvbS5jbi9wdWJsaWMv +aXRydXNjcmw/Q0E9MUJENDIyMEU1MERCQzA0QjA2QUQzOTc1NDk4NDZDMDFDM0U4 +RUJEMiZzZz1IQUNDNDcxQjY1NDIyRTEyQjI3QTlEMzNBODdBRDFDREY1OTI2RTE0 +MDM3MTANBgkqhkiG9w0BAQsFAAOCAQEAB1Dw7Gr+bmGtvL++lNY7KEeIywSTuHz2 +264LhBxFEX5fKaKdq/cy7Kw6n9c7Jbcu1aIbNb08eZ0h1veJGp8oJzDxpIE8m/g2 +AQHRfkOJidHEed+UvLb6dKD20nh/vVfdOKoV1NEvQZw6n3YuRjH+YzaZAeKA6T2E +CxBF7apLC/FQ06hBZruZDr8GgaxzubkAZu8CHdbCDk4K3OuEOJjJT9fxgbiVhih5 +wtlupVbpnGKpXm+xqeti90EKUGh/rFv/3pz+mD4n5xioYcQrvOxNUYEGEAcNSEmK +mEc2D13rJf7pBRm+nhjVee3qckW5cdQfj1hxgB2CQOY05Xt5Zba8KA== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controller/AppletController.java b/ruoyi-system/src/main/java/com/ruoyi/system/controller/AppletController.java index 35e7b4f..6eb9d80 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/controller/AppletController.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/controller/AppletController.java @@ -6212,4 +6212,63 @@ public class AppletController extends BaseController { return AppletControllerUtil.appletError("操作失败"); } } + + + + + /** + * 云信交互式语音通知呼叫结果推送回调接口 + * 用于接收云信平台推送的交互式语音通知呼叫结果。 + * @param requestBody 云信平台推送的JSON字符串 + * @return 必须返回{"resultCode":"200"},否则云信认为推送失败 + */ + @PostMapping("/api/voice/interactNotify/resultCallback") + public Map voiceInteractNotifyResultCallback(@RequestBody String requestBody) { + // 解析请求体(可根据业务需要保存或处理字段) + try { + JSONObject json = JSONObject.parseObject(requestBody); + // 可提取字段:accountId、callId、calleeNumber、startCallTime、endTime、duration等 + // String accountId = json.getString("accountId"); + // String callId = json.getString("callId"); + // ... 其他字段 + // 这里可以根据业务需求进行日志记录、数据库保存等操作 + } catch (Exception e) { + // 解析异常可记录日志 + } + // 必须返回{"resultCode":"200"},否则云信会认为推送失败 + Map resp = new java.util.HashMap<>(); + resp.put("resultCode", "200"); + return resp; + } + + + + + + /** + * 云信交互式小号呼叫结果推送结果推送回调接口 + * 用于接收云信平台推送的交互式语音通知呼叫结果。 + * @param requestBody 云信平台推送的JSON字符串 + * @return 必须返回{"resultCode":"200"},否则云信认为推送失败 + */ + @PostMapping("/api/voice/middleNumberAXB/resultCallback") + public Map voiceInteractmiddleNumberAXBCallback(@RequestBody String requestBody) { + // 解析请求体(可根据业务需要保存或处理字段) + try { + JSONObject json = JSONObject.parseObject(requestBody); + // 可提取字段:accountId、callId、calleeNumber、startCallTime、endTime、duration等 + // String accountId = json.getString("accountId"); + // String callId = json.getString("callId"); + // ... 其他字段 + // 这里可以根据业务需求进行日志记录、数据库保存等操作 + } catch (Exception e) { + // 解析异常可记录日志 + } + // 必须返回{"resultCode":"200"},否则云信会认为推送失败 + Map resp = new java.util.HashMap<>(); + resp.put("resultCode", "200"); + return resp; + } + + } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controllerUtil/AppletControllerUtil.java b/ruoyi-system/src/main/java/com/ruoyi/system/controllerUtil/AppletControllerUtil.java index d7f472c..3346a24 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/controllerUtil/AppletControllerUtil.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/controllerUtil/AppletControllerUtil.java @@ -2696,4 +2696,38 @@ public class AppletControllerUtil { dataPage.put("total", pageInfo.getTotal()); return dataPage; } + + /** + * 云信交互式语音通知回调接口 + *

+ * 用于接收云信平台推送的语音通知结果,解析callId、result、message等字段。 + * 该方法可直接作为Controller层的回调接口方法使用。 + *

+ * @param requestBody 云信平台推送的JSON字符串 + * @return 标准响应结果 + */ + public static Map voiceInteractNotifyCallback(String requestBody) { + Map resultMap = new HashMap<>(); + try { + // 解析JSON请求体 + com.alibaba.fastjson2.JSONObject json = com.alibaba.fastjson2.JSONObject.parseObject(requestBody); + String callId = json.getString("callId"); + String result = json.getString("result"); + String message = json.getString("message"); + + // 业务处理:可根据callId、result、message进行自定义逻辑 + // 例如:记录日志、更新数据库、通知业务系统等 + + // 返回标准响应 + resultMap.put("code", 200); + resultMap.put("msg", "回调接收成功"); + resultMap.put("callId", callId); + resultMap.put("result", result); + resultMap.put("message", message); + } catch (Exception e) { + resultMap.put("code", 500); + resultMap.put("msg", "回调处理异常: " + e.getMessage()); + } + return resultMap; + } } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controllerUtil/WechatPayUtil.java b/ruoyi-system/src/main/java/com/ruoyi/system/controllerUtil/WechatPayUtil.java index 884fc91..644714c 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/controllerUtil/WechatPayUtil.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/controllerUtil/WechatPayUtil.java @@ -6,6 +6,8 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; @@ -35,22 +37,35 @@ import java.util.*; * @date 2025-01-03 * @version 1.0 */ +@Component public class WechatPayUtil { /** - * 微信支付配置常量 - * 注意:实际使用时需要在配置文件中配置这些值 + * 微信支付配置(通过Spring配置注入) + * 仅支持小程序支付(JSAPI) + * 建议在application.yml或application.properties中配置如下: + * wechat.pay.app-id=xxx + * wechat.pay.mch-id=xxx + * wechat.pay.api-key=xxx + * wechat.pay.cert-path=xxx */ - private static final String WECHAT_APP_ID = "your_wechat_appid"; - private static final String WECHAT_MCH_ID = "your_merchant_id"; - private static final String WECHAT_API_KEY = "your_api_key"; - private static final String WECHAT_CERT_PATH = "/path/to/cert.p12"; // 微信支付证书路径 + @Value("${wechat.pay.app-id}") + private String wechatAppId; + + @Value("${wechat.pay.mch-id}") + private String wechatMchId; + + @Value("${wechat.pay.api-key}") + private String wechatApiKey; + + @Value("${wechat.pay.cert-path}") + private String wechatCertPath; /** - * 微信支付API地址 + * 微信支付API地址(仅保留小程序相关接口) */ - private static final String WECHAT_PAY_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder"; - private static final String WECHAT_QUERY_URL = "https://api.mch.weixin.qq.com/pay/orderquery"; + private static final String WECHAT_PAY_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder"; // 统一下单 + private static final String WECHAT_QUERY_URL = "https://api.mch.weixin.qq.com/pay/orderquery"; // 订单查询 private static final String WECHAT_REFUND_URL = "https://api.mch.weixin.qq.com/secapi/pay/refund"; private static final String WECHAT_TRANSFER_URL = "https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers"; // 企业付款 @@ -68,109 +83,101 @@ public class WechatPayUtil { private static final RestTemplate restTemplate = new RestTemplate(); /** - * 统一下单 - 微信小程序支付 - * - * @param orderInfo 订单信息 - * @return 支付参数Map + * 小程序统一下单(JSAPI) + * @param openid 用户openid + * @param orderNo 商户订单号(唯一) + * @param totalFee 支付金额(单位:分,整型) + * @param body 商品描述 + * @param notifyUrl 支付回调通知地址 + * @param attach 附加数据(可选) + * @return 统一下单结果,包含小程序端调起支付的参数 */ - public static Map unifiedOrder(Map orderInfo) { + public Map unifiedOrder(String openid, String orderNo, int totalFee, String body, String notifyUrl, String attach) { Map result = new HashMap<>(); - try { - // 1. 参数验证 - String validationError = validateOrderParams(orderInfo); - if (validationError != null) { - result.put("success", false); - result.put("message", validationError); - return result; + // 1. 参数校验 + if (openid == null || openid.trim().isEmpty()) { + return failResult("用户openid不能为空"); + } + if (orderNo == null || orderNo.trim().isEmpty()) { + return failResult("订单号不能为空"); + } + if (totalFee <= 0) { + return failResult("支付金额必须大于0"); + } + if (body == null || body.trim().isEmpty()) { + return failResult("商品描述不能为空"); + } + if (notifyUrl == null || notifyUrl.trim().isEmpty()) { + return failResult("回调通知地址不能为空"); + } + // 2. 构建参数 + Map params = new HashMap<>(); + params.put("appid", wechatAppId); + params.put("mch_id", wechatMchId); + params.put("nonce_str", generateNonceStr()); + params.put("body", body); + params.put("out_trade_no", orderNo); + params.put("total_fee", String.valueOf(totalFee)); // 单位:分 + params.put("spbill_create_ip", "127.0.0.1"); + params.put("notify_url", notifyUrl); + params.put("trade_type", TRADE_TYPE_JSAPI); + params.put("openid", openid); + if (attach != null && !attach.trim().isEmpty()) { + params.put("attach", attach); } - - // 2. 构建统一下单参数 - Map params = buildUnifiedOrderParams(orderInfo); - // 3. 生成签名 - String sign = generateSign(params, WECHAT_API_KEY); + String sign = generateSign(params, wechatApiKey); params.put("sign", sign); - // 4. 发送请求 String xmlRequest = mapToXml(params); ResponseEntity response = restTemplate.postForEntity(WECHAT_PAY_URL, xmlRequest, String.class); - - // 5. 解析响应 Map responseMap = xmlToMap(response.getBody()); - - if (SUCCESS_CODE.equals(responseMap.get("return_code")) && - SUCCESS_CODE.equals(responseMap.get("result_code"))) { - - // 6. 构建小程序支付参数 + // 5. 处理响应 + if (SUCCESS_CODE.equals(responseMap.get("return_code")) && SUCCESS_CODE.equals(responseMap.get("result_code"))) { + // 6. 构建小程序端调起支付参数 Map payParams = buildMiniProgramPayParams(responseMap.get("prepay_id")); - result.put("success", true); result.put("payParams", payParams); result.put("prepayId", responseMap.get("prepay_id")); result.put("message", "统一下单成功"); - } else { - result.put("success", false); - result.put("message", "统一下单失败:" + - (responseMap.get("err_code_des") != null ? responseMap.get("err_code_des") : - responseMap.get("return_msg"))); + result = failResult("统一下单失败:" + (responseMap.get("err_code_des") != null ? responseMap.get("err_code_des") : responseMap.get("return_msg"))); } - } catch (Exception e) { - result.put("success", false); - result.put("message", "统一下单异常:" + e.getMessage()); + result = failResult("统一下单异常:" + e.getMessage()); } - return result; } /** - * 查询订单状态 - * - * @param orderNo 订单号 - * @param transactionId 微信订单号(可选,与orderNo二选一) - * @return 查询结果 + * 查询订单状态(小程序支付) + * @param orderNo 商户订单号 + * @param transactionId 微信订单号(可选) + * @return 查询结果,包含订单状态等信息 */ - public static Map queryOrder(String orderNo, String transactionId) { + public Map queryOrder(String orderNo, String transactionId) { Map result = new HashMap<>(); - try { - // 1. 参数验证 - if ((orderNo == null || orderNo.trim().isEmpty()) && - (transactionId == null || transactionId.trim().isEmpty())) { - result.put("success", false); - result.put("message", "订单号和微信订单号不能同时为空"); - return result; + if ((orderNo == null || orderNo.trim().isEmpty()) && (transactionId == null || transactionId.trim().isEmpty())) { + return failResult("订单号和微信订单号不能同时为空"); } - - // 2. 构建查询参数 Map params = new HashMap<>(); - params.put("appid", WECHAT_APP_ID); - params.put("mch_id", WECHAT_MCH_ID); + params.put("appid", wechatAppId); + params.put("mch_id", wechatMchId); params.put("nonce_str", generateNonceStr()); - if (orderNo != null && !orderNo.trim().isEmpty()) { params.put("out_trade_no", orderNo.trim()); } if (transactionId != null && !transactionId.trim().isEmpty()) { params.put("transaction_id", transactionId.trim()); } - - // 3. 生成签名 - String sign = generateSign(params, WECHAT_API_KEY); + String sign = generateSign(params, wechatApiKey); params.put("sign", sign); - - // 4. 发送请求 String xmlRequest = mapToXml(params); ResponseEntity response = restTemplate.postForEntity(WECHAT_QUERY_URL, xmlRequest, String.class); - - // 5. 解析响应 Map responseMap = xmlToMap(response.getBody()); - - if (SUCCESS_CODE.equals(responseMap.get("return_code")) && - SUCCESS_CODE.equals(responseMap.get("result_code"))) { - + if (SUCCESS_CODE.equals(responseMap.get("return_code")) && SUCCESS_CODE.equals(responseMap.get("result_code"))) { Map orderInfo = new HashMap<>(); orderInfo.put("tradeState", responseMap.get("trade_state")); orderInfo.put("tradeStateDesc", responseMap.get("trade_state_desc")); @@ -179,125 +186,44 @@ public class WechatPayUtil { orderInfo.put("totalFee", responseMap.get("total_fee")); orderInfo.put("cashFee", responseMap.get("cash_fee")); orderInfo.put("timeEnd", responseMap.get("time_end")); - result.put("success", true); result.put("orderInfo", orderInfo); result.put("message", "查询订单成功"); - } else { - result.put("success", false); - result.put("message", "查询订单失败:" + - (responseMap.get("err_code_des") != null ? responseMap.get("err_code_des") : - responseMap.get("return_msg"))); + result = failResult("查询订单失败:" + (responseMap.get("err_code_des") != null ? responseMap.get("err_code_des") : responseMap.get("return_msg"))); } - } catch (Exception e) { - result.put("success", false); - result.put("message", "查询订单异常:" + e.getMessage()); + result = failResult("查询订单异常:" + e.getMessage()); } - return result; } /** - * 找朋友代付功能 - 生成代付订单 - * - * @param payForInfo 代付信息 - * @return 代付订单结果 + * 支付回调通知处理(小程序支付) + * @param request HttpServletRequest + * @return 处理结果,包含是否成功、支付信息等 */ - public static Map createPayForOrder(Map payForInfo) { + public Map handlePayNotify(HttpServletRequest request) { Map result = new HashMap<>(); - - try { - // 1. 参数验证 - String validationError = validatePayForParams(payForInfo); - if (validationError != null) { - result.put("success", false); - result.put("message", validationError); - return result; - } - - // 2. 生成代付订单号 - String payForOrderNo = generatePayForOrderNo((String) payForInfo.get("orderNo")); - - // 3. 构建代付订单信息 - Map payForOrderInfo = new HashMap<>(); - payForOrderInfo.put("orderNo", payForOrderNo); - payForOrderInfo.put("openid", payForInfo.get("payerOpenid")); - payForOrderInfo.put("totalFee", payForInfo.get("totalFee")); - payForOrderInfo.put("body", "代付-" + payForInfo.get("body")); - payForOrderInfo.put("notifyUrl", payForInfo.get("notifyUrl")); - payForOrderInfo.put("attach", buildPayForAttach(payForInfo)); - - // 4. 设置30分钟过期时间 - Calendar calendar = Calendar.getInstance(); - calendar.add(Calendar.MINUTE, 30); - SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); - payForOrderInfo.put("timeExpire", sdf.format(calendar.getTime())); - - // 5. 调用统一下单 - Map unifiedResult = unifiedOrder(payForOrderInfo); - - if ((Boolean) unifiedResult.get("success")) { - result.put("success", true); - result.put("payForOrderNo", payForOrderNo); - result.put("payParams", unifiedResult.get("payParams")); - result.put("prepayId", unifiedResult.get("prepayId")); - result.put("expireTime", payForOrderInfo.get("timeExpire")); - result.put("message", "代付订单创建成功"); - } else { - result.put("success", false); - result.put("message", "代付订单创建失败:" + unifiedResult.get("message")); - } - - } catch (Exception e) { - result.put("success", false); - result.put("message", "创建代付订单异常:" + e.getMessage()); - } - - return result; - } - - /** - * 支付结果通知回调处理 - * - * @param request HTTP请求对象 - * @return 处理结果 - */ - public static Map handlePayNotify(HttpServletRequest request) { - Map result = new HashMap<>(); - try { // 1. 读取请求数据 StringBuilder xmlData = new StringBuilder(); - BufferedReader reader = new BufferedReader( - new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8)); + BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8)); String line; while ((line = reader.readLine()) != null) { xmlData.append(line); } reader.close(); - // 2. 解析XML数据 Map notifyData = xmlToMap(xmlData.toString()); - // 3. 验证签名 - if (!verifySign(notifyData, WECHAT_API_KEY)) { - result.put("success", false); - result.put("responseXml", buildNotifyResponse(FAIL_CODE, "签名验证失败")); - result.put("message", "签名验证失败"); - return result; + if (!verifySign(notifyData, wechatApiKey)) { + return failResult("签名验证失败"); } - // 4. 检查支付结果 - if (!SUCCESS_CODE.equals(notifyData.get("return_code")) || - !SUCCESS_CODE.equals(notifyData.get("result_code"))) { - result.put("success", false); - result.put("responseXml", buildNotifyResponse(FAIL_CODE, "支付失败")); - result.put("message", "支付失败:" + notifyData.get("err_code_des")); - return result; + if (!SUCCESS_CODE.equals(notifyData.get("return_code")) || !SUCCESS_CODE.equals(notifyData.get("result_code"))) { + return failResult("支付失败:" + notifyData.get("err_code_des")); } - // 5. 构建支付信息 Map paymentInfo = new HashMap<>(); paymentInfo.put("transactionId", notifyData.get("transaction_id")); @@ -307,23 +233,12 @@ public class WechatPayUtil { paymentInfo.put("timeEnd", notifyData.get("time_end")); paymentInfo.put("attach", notifyData.get("attach")); paymentInfo.put("openid", notifyData.get("openid")); - - // 6. 判断是否为代付订单 - String orderNo = notifyData.get("out_trade_no"); - boolean isPayFor = isPayForOrder(orderNo); - result.put("success", true); - result.put("responseXml", buildNotifyResponse(SUCCESS_CODE, "OK")); result.put("paymentInfo", paymentInfo); - result.put("isPayFor", isPayFor); result.put("message", "支付通知处理成功"); - } catch (Exception e) { - result.put("success", false); - result.put("responseXml", buildNotifyResponse(FAIL_CODE, "处理异常")); - result.put("message", "处理支付通知异常:" + e.getMessage()); + result = failResult("处理支付通知异常:" + e.getMessage()); } - return result; } @@ -333,7 +248,7 @@ public class WechatPayUtil { * @param refundInfo 退款信息 * @return 退款结果 */ - public static Map refund(Map refundInfo) { + public Map refund(Map refundInfo) { Map result = new HashMap<>(); try { @@ -349,7 +264,7 @@ public class WechatPayUtil { Map params = buildRefundParams(refundInfo); // 3. 生成签名 - String sign = generateSign(params, WECHAT_API_KEY); + String sign = generateSign(params, wechatApiKey); params.put("sign", sign); // 4. 发送请求(需要证书) @@ -394,7 +309,7 @@ public class WechatPayUtil { * @param transferInfo 付款信息 * @return 付款结果 */ - public static Map transferToUser(Map transferInfo) { + public Map transferToUser(Map transferInfo) { Map result = new HashMap<>(); try { @@ -410,7 +325,7 @@ public class WechatPayUtil { Map params = buildTransferParams(transferInfo); // 3. 生成签名 - String sign = generateSign(params, WECHAT_API_KEY); + String sign = generateSign(params, wechatApiKey); params.put("sign", sign); // 4. 发送请求(需要证书) @@ -448,35 +363,149 @@ public class WechatPayUtil { return result; } - // ========== 私有工具方法 ========== + // ===================== 工具方法和签名相关 ===================== /** - * 验证统一下单参数 + * 构建小程序端调起支付参数 + * @param prepayId 预支付交易会话标识 + * @return 小程序端调起支付参数Map */ - private static String validateOrderParams(Map orderInfo) { - if (orderInfo == null) return "订单信息不能为空"; - if (orderInfo.get("orderNo") == null || orderInfo.get("orderNo").toString().trim().isEmpty()) { - return "订单号不能为空"; + private Map buildMiniProgramPayParams(String prepayId) { + Map payParams = new HashMap<>(); + String timeStamp = String.valueOf(System.currentTimeMillis() / 1000); + String nonceStr = generateNonceStr(); + String packageStr = "prepay_id=" + prepayId; + String signType = "MD5"; + // 构建签名参数 + Map signParams = new HashMap<>(); + signParams.put("appId", wechatAppId); + signParams.put("timeStamp", timeStamp); + signParams.put("nonceStr", nonceStr); + signParams.put("package", packageStr); + signParams.put("signType", signType); + String paySign = generateSign(signParams, wechatApiKey); + payParams.put("timeStamp", timeStamp); + payParams.put("nonceStr", nonceStr); + payParams.put("package", packageStr); + payParams.put("signType", signType); + payParams.put("paySign", paySign); + return payParams; + } + + /** + * 生成签名 + * @param params 参数Map + * @param key API密钥 + * @return 签名字符串 + */ + private String generateSign(Map params, String key) { + try { + Map sortedParams = new TreeMap<>(params); + StringBuilder stringBuilder = new StringBuilder(); + for (Map.Entry entry : sortedParams.entrySet()) { + if (entry.getValue() != null && !entry.getValue().isEmpty() && !"sign".equals(entry.getKey())) { + stringBuilder.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); + } + } + stringBuilder.append("key=").append(key); + MessageDigest md5 = MessageDigest.getInstance("MD5"); + byte[] digest = md5.digest(stringBuilder.toString().getBytes(StandardCharsets.UTF_8)); + StringBuilder result = new StringBuilder(); + for (byte b : digest) { + result.append(String.format("%02X", b)); + } + return result.toString(); + } catch (Exception e) { + throw new RuntimeException("生成签名失败", e); } - if (orderInfo.get("openid") == null || orderInfo.get("openid").toString().trim().isEmpty()) { - return "用户openid不能为空"; + } + + /** + * 验证签名 + * @param params 参数Map + * @param key API密钥 + * @return 是否验证通过 + */ + private boolean verifySign(Map params, String key) { + String sign = params.get("sign"); + if (sign == null || sign.isEmpty()) { + return false; } - if (orderInfo.get("totalFee") == null) { - return "支付金额不能为空"; + Map paramsWithoutSign = new HashMap<>(params); + paramsWithoutSign.remove("sign"); + String generatedSign = generateSign(paramsWithoutSign, key); + return sign.equals(generatedSign); + } + + /** + * Map转XML + * @param params 参数Map + * @return XML字符串 + */ + private String mapToXml(Map params) { + StringBuilder xml = new StringBuilder(); + xml.append(""); + for (Map.Entry entry : params.entrySet()) { + xml.append("<").append(entry.getKey()).append(">"); } - if (orderInfo.get("body") == null || orderInfo.get("body").toString().trim().isEmpty()) { - return "商品描述不能为空"; + xml.append(""); + return xml.toString(); + } + + /** + * XML转Map(简化实现) + * @param xml XML字符串 + * @return Map + */ + private Map xmlToMap(String xml) { + Map map = new HashMap<>(); + try { + xml = xml.replaceAll("", "").replaceAll("", ""); + String[] elements = xml.split(""); + for (String element : elements) { + if (element.trim().isEmpty()) continue; + int startTag = element.indexOf("<"); + int endTag = element.indexOf(">"); + if (startTag >= 0 && endTag > startTag) { + String key = element.substring(startTag + 1, endTag); + String value = element.substring(endTag + 1); + if (value.startsWith("")) { + value = value.substring(9, value.length() - 3); + } + map.put(key, value); + } + } + } catch (Exception e) { + throw new RuntimeException("XML解析失败", e); } - if (orderInfo.get("notifyUrl") == null || orderInfo.get("notifyUrl").toString().trim().isEmpty()) { - return "回调地址不能为空"; - } - return null; + return map; + } + + /** + * 生成随机字符串 + * @return 随机字符串 + */ + private String generateNonceStr() { + return UUID.randomUUID().toString().replace("-", "").substring(0, 32); + } + + /** + * 统一失败返回结构 + * @param msg 失败信息 + * @return 标准失败结构 + */ + private Map failResult(String msg) { + Map result = new HashMap<>(); + result.put("success", false); + result.put("message", msg); + return result; } /** * 验证代付参数 */ - private static String validatePayForParams(Map payForInfo) { + private String validatePayForParams(Map payForInfo) { if (payForInfo == null) return "代付信息不能为空"; if (payForInfo.get("orderNo") == null || payForInfo.get("orderNo").toString().trim().isEmpty()) { return "原订单号不能为空"; @@ -502,7 +531,7 @@ public class WechatPayUtil { /** * 验证退款参数 */ - private static String validateRefundParams(Map refundInfo) { + private String validateRefundParams(Map refundInfo) { if (refundInfo == null) return "退款信息不能为空"; if (refundInfo.get("orderNo") == null || refundInfo.get("orderNo").toString().trim().isEmpty()) { return "订单号不能为空"; @@ -522,7 +551,7 @@ public class WechatPayUtil { /** * 验证企业付款参数 */ - private static String validateTransferParams(Map transferInfo) { + private String validateTransferParams(Map transferInfo) { if (transferInfo == null) return "付款信息不能为空"; if (transferInfo.get("partnerTradeNo") == null || transferInfo.get("partnerTradeNo").toString().trim().isEmpty()) { return "商户订单号不能为空"; @@ -540,38 +569,53 @@ public class WechatPayUtil { } /** - * 构建统一下单参数 + * 构建代付附加数据 */ - private static Map buildUnifiedOrderParams(Map orderInfo) { - Map params = new HashMap<>(); - params.put("appid", WECHAT_APP_ID); - params.put("mch_id", WECHAT_MCH_ID); - params.put("nonce_str", generateNonceStr()); - params.put("body", orderInfo.get("body").toString()); - params.put("out_trade_no", orderInfo.get("orderNo").toString()); - params.put("total_fee", orderInfo.get("totalFee").toString()); - params.put("spbill_create_ip", "127.0.0.1"); - params.put("notify_url", orderInfo.get("notifyUrl").toString()); - params.put("trade_type", TRADE_TYPE_JSAPI); - params.put("openid", orderInfo.get("openid").toString()); - - if (orderInfo.get("attach") != null) { - params.put("attach", orderInfo.get("attach").toString()); + private String buildPayForAttach(Map payForInfo) { + JSONObject attach = new JSONObject(); + attach.put("type", "payfor"); + attach.put("originalOrderNo", payForInfo.get("orderNo")); + attach.put("payerOpenid", payForInfo.get("payerOpenid")); + attach.put("payeeOpenid", payForInfo.get("payeeOpenid")); + if (payForInfo.get("remark") != null) { + attach.put("remark", payForInfo.get("remark")); } - if (orderInfo.get("timeExpire") != null) { - params.put("time_expire", orderInfo.get("timeExpire").toString()); - } - - return params; + return attach.toString(); + } + + /** + * 生成代付订单号 + */ + private String generatePayForOrderNo(String originalOrderNo) { + return "PF" + originalOrderNo + System.currentTimeMillis(); + } + + /** + * 判断是否为代付订单 + */ + private boolean isPayForOrder(String orderNo) { + return orderNo != null && orderNo.startsWith("PF"); + } + + /** + * 构建通知响应XML + */ + private String buildNotifyResponse(String returnCode, String returnMsg) { + StringBuilder xml = new StringBuilder(); + xml.append(""); + xml.append(""); + xml.append(""); + xml.append(""); + return xml.toString(); } /** * 构建退款参数 */ - private static Map buildRefundParams(Map refundInfo) { + private Map buildRefundParams(Map refundInfo) { Map params = new HashMap<>(); - params.put("appid", WECHAT_APP_ID); - params.put("mch_id", WECHAT_MCH_ID); + params.put("appid", wechatAppId); + params.put("mch_id", wechatMchId); params.put("nonce_str", generateNonceStr()); params.put("out_trade_no", refundInfo.get("orderNo").toString()); params.put("out_refund_no", refundInfo.get("refundNo").toString()); @@ -591,10 +635,10 @@ public class WechatPayUtil { /** * 构建企业付款参数 */ - private static Map buildTransferParams(Map transferInfo) { + private Map buildTransferParams(Map transferInfo) { Map params = new HashMap<>(); - params.put("mch_appid", WECHAT_APP_ID); - params.put("mchid", WECHAT_MCH_ID); + params.put("mch_appid", wechatAppId); + params.put("mchid", wechatMchId); params.put("nonce_str", generateNonceStr()); params.put("partner_trade_no", transferInfo.get("partnerTradeNo").toString()); params.put("openid", transferInfo.get("openid").toString()); @@ -610,178 +654,4 @@ public class WechatPayUtil { return params; } - - /** - * 构建小程序支付参数 - */ - private static Map buildMiniProgramPayParams(String prepayId) { - Map payParams = new HashMap<>(); - String timeStamp = String.valueOf(System.currentTimeMillis() / 1000); - String nonceStr = generateNonceStr(); - String packageStr = "prepay_id=" + prepayId; - String signType = "MD5"; - - // 构建签名参数 - Map signParams = new HashMap<>(); - signParams.put("appId", WECHAT_APP_ID); - signParams.put("timeStamp", timeStamp); - signParams.put("nonceStr", nonceStr); - signParams.put("package", packageStr); - signParams.put("signType", signType); - - String paySign = generateSign(signParams, WECHAT_API_KEY); - - payParams.put("timeStamp", timeStamp); - payParams.put("nonceStr", nonceStr); - payParams.put("package", packageStr); - payParams.put("signType", signType); - payParams.put("paySign", paySign); - - return payParams; - } - - /** - * 构建代付附加数据 - */ - private static String buildPayForAttach(Map payForInfo) { - JSONObject attach = new JSONObject(); - attach.put("type", "payfor"); - attach.put("originalOrderNo", payForInfo.get("orderNo")); - attach.put("payerOpenid", payForInfo.get("payerOpenid")); - attach.put("payeeOpenid", payForInfo.get("payeeOpenid")); - if (payForInfo.get("remark") != null) { - attach.put("remark", payForInfo.get("remark")); - } - return attach.toString(); - } - - /** - * 生成代付订单号 - */ - private static String generatePayForOrderNo(String originalOrderNo) { - return "PF" + originalOrderNo + System.currentTimeMillis(); - } - - /** - * 判断是否为代付订单 - */ - private static boolean isPayForOrder(String orderNo) { - return orderNo != null && orderNo.startsWith("PF"); - } - - /** - * 构建通知响应XML - */ - private static String buildNotifyResponse(String returnCode, String returnMsg) { - StringBuilder xml = new StringBuilder(); - xml.append(""); - xml.append(""); - xml.append(""); - xml.append(""); - return xml.toString(); - } - - /** - * 生成随机字符串 - */ - private static String generateNonceStr() { - return UUID.randomUUID().toString().replace("-", "").substring(0, 32); - } - - /** - * 生成签名 - */ - private static String generateSign(Map params, String key) { - try { - // 1. 排序参数 - Map sortedParams = new TreeMap<>(params); - - // 2. 拼接参数 - StringBuilder stringBuilder = new StringBuilder(); - for (Map.Entry entry : sortedParams.entrySet()) { - if (entry.getValue() != null && !entry.getValue().isEmpty() && !"sign".equals(entry.getKey())) { - stringBuilder.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); - } - } - stringBuilder.append("key=").append(key); - - // 3. MD5加密 - MessageDigest md5 = MessageDigest.getInstance("MD5"); - byte[] digest = md5.digest(stringBuilder.toString().getBytes(StandardCharsets.UTF_8)); - - // 4. 转换为大写16进制字符串 - StringBuilder result = new StringBuilder(); - for (byte b : digest) { - result.append(String.format("%02X", b)); - } - - return result.toString(); - - } catch (Exception e) { - throw new RuntimeException("生成签名失败", e); - } - } - - /** - * 验证签名 - */ - private static boolean verifySign(Map params, String key) { - String sign = params.get("sign"); - if (sign == null || sign.isEmpty()) { - return false; - } - - Map paramsWithoutSign = new HashMap<>(params); - paramsWithoutSign.remove("sign"); - - String generatedSign = generateSign(paramsWithoutSign, key); - return sign.equals(generatedSign); - } - - /** - * Map转XML - */ - private static String mapToXml(Map params) { - StringBuilder xml = new StringBuilder(); - xml.append(""); - for (Map.Entry entry : params.entrySet()) { - xml.append("<").append(entry.getKey()).append(">"); - } - xml.append(""); - return xml.toString(); - } - - /** - * XML转Map(简化实现) - */ - private static Map xmlToMap(String xml) { - Map map = new HashMap<>(); - try { - // 简化的XML解析实现 - xml = xml.replaceAll("", "").replaceAll("", ""); - String[] elements = xml.split(""); - - for (String element : elements) { - if (element.trim().isEmpty()) continue; - - int startTag = element.indexOf("<"); - int endTag = element.indexOf(">"); - if (startTag >= 0 && endTag > startTag) { - String key = element.substring(startTag + 1, endTag); - String value = element.substring(endTag + 1); - - // 处理CDATA - if (value.startsWith("")) { - value = value.substring(9, value.length() - 3); - } - - map.put(key, value); - } - } - } catch (Exception e) { - throw new RuntimeException("XML解析失败", e); - } - return map; - } } \ No newline at end of file