From 6acf6357094588946b5528f1ef1ed84a0f1037fd Mon Sep 17 00:00:00 2001 From: huliguo <2023611923@qq.com> Date: 星期五, 13 六月 2025 19:45:27 +0800 Subject: [PATCH] 小程序收付款 --- ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/HttpUtil.java | 98 + ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wechat/PayMoneyUtil.java | 1295 ++++++++++++++++++++ ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/vo/PayResult.java | 16 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/WechatPayService.java | 742 +++++++++++ ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/payment/wx/XMLUtil.java | 52 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/vo/UnifiedOrderResult.java | 38 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/OrderController.java | 61 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/OrderServiceImpl.java | 207 ++ ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/payment/wx/WechatPayService.java | 734 +++++++++++ ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/WechatPayConfig.java | 26 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/payment/wx/PayResult.java | 16 ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/AppUserServiceImpl.java | 10 ruoyi-service/pom.xml | 2 ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/ShopWithdraw.java | 2 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/ShopServiceImpl.java | 10 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/XMLUtil.java | 52 ruoyi-service/ruoyi-order/pom.xml | 21 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/ShopWithdrawController.java | 60 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/PaymentUtil.java | 10 ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/OrderService.java | 8 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/payment/wx/WechatPayConfig.java | 26 ruoyi-service/ruoyi-other/pom.xml | 17 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/payment/wx/RefundCallbackResult.java | 38 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/payment/wx/HttpUtil.java | 99 + ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/vo/RefundCallbackResult.java | 38 25 files changed, 3,557 insertions(+), 121 deletions(-) diff --git a/ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/ShopWithdraw.java b/ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/ShopWithdraw.java index d19a884..b00313e 100644 --- a/ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/ShopWithdraw.java +++ b/ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/ShopWithdraw.java @@ -106,7 +106,7 @@ @TableField("receiverAccountType") private Integer receiverAccountType; - @ApiModelProperty("收款账户联行号") + @ApiModelProperty("收款方开户行编号") @TableField("receiverBankChannelNo") private String receiverBankChannelNo; diff --git a/ruoyi-service/pom.xml b/ruoyi-service/pom.xml index 7d28d6e..9efe6e9 100644 --- a/ruoyi-service/pom.xml +++ b/ruoyi-service/pom.xml @@ -32,7 +32,7 @@ <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> - <version>5.0.3</version> + <version>5.8.25</version> </dependency> <!--log4j--> <dependency> diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/AppUserServiceImpl.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/AppUserServiceImpl.java index e2a7611..d71fa38 100644 --- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/AppUserServiceImpl.java +++ b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/AppUserServiceImpl.java @@ -136,6 +136,10 @@ appUser.setDelFlag(false); appUser.setCreateTime(LocalDateTime.now()); this.save(appUser); + }else { + //从订单导入的,将openid导入 + appUser.setWxOpenid(openid); + this.updateById(appUser); } //账户被冻结,给出提示 if(2 == appUser.getStatus()){ @@ -175,7 +179,7 @@ //查询用户是否注册,没有注册则跳转到注册页面 AppUser appUser = this.getOne(new LambdaQueryWrapper<AppUser>().eq(AppUser::getPhone, mobileLogin.getPhone()) .ne(AppUser::getStatus, 3).eq(AppUser::getDelFlag, 0)); - if(null == appUser){ + if(null == appUser||null ==appUser.getWxOpenid()){ LoginVo loginVo = new LoginVo(); loginVo.setSkipPage(2); loginVo.setPhone(mobileLogin.getPhone()); @@ -304,8 +308,8 @@ if(null != appUser1 && StringUtils.isNotEmpty(appUser1.getWxOpenid())){ return R.fail("手机号已注册,请直接登录!"); } - if(null != appUser1 && appUser1.getStatus() == 1){ - return R.fail("手机号已注册,请直接登录!"); + if(null != appUser1 && appUser1.getStatus() == 2){ + return R.fail("该手机号已被冻结!"); } String avatar = registerAccount.getAvatar(); diff --git a/ruoyi-service/ruoyi-order/pom.xml b/ruoyi-service/ruoyi-order/pom.xml index 1d2c7a2..1123cfc 100644 --- a/ruoyi-service/ruoyi-order/pom.xml +++ b/ruoyi-service/ruoyi-order/pom.xml @@ -15,6 +15,12 @@ </description> <dependencies> + <dependency> + <groupId>com.alipay.sdk</groupId> + <artifactId>alipay-sdk-java</artifactId> + <version>4.8.10.ALL</version> + </dependency> + <!--网易邮件--> <dependency> <groupId>javax.mail</groupId> @@ -170,6 +176,21 @@ <artifactId>geodesy</artifactId> <version>1.1.3</version> </dependency> + <dependency> + <groupId>cn.hutool</groupId> + <artifactId>hutool-http</artifactId> + <version>5.8.25</version> + </dependency> + <dependency> + <groupId>org.bouncycastle</groupId> + <artifactId>bcprov-jdk15on</artifactId> + <version>1.70</version> + </dependency> + <dependency> + <groupId>dom4j</groupId> + <artifactId>dom4j</artifactId> + <version>1.6.1</version> + </dependency> </dependencies> <build> diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/OrderController.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/OrderController.java index c08a457..9594278 100644 --- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/OrderController.java +++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/OrderController.java @@ -24,6 +24,9 @@ import com.ruoyi.order.service.OrderService; import com.ruoyi.order.util.payment.model.RefundCallbackResult; import com.ruoyi.order.util.payment.model.UniPayCallbackResult; +import com.ruoyi.order.util.payment.wechat.PayMoneyUtil; +import com.ruoyi.order.util.payment.wx.WechatPayService; +import com.ruoyi.order.util.payment.wx.vo.PayResult; import com.ruoyi.order.util.vo.MapTrackKD100Vo; import com.ruoyi.order.util.vo.ShopAnalysisVO; import com.ruoyi.order.vo.*; @@ -37,10 +40,12 @@ import com.ruoyi.system.api.model.LoginUser; import io.swagger.annotations.*; import org.apache.ibatis.annotations.Param; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; @@ -82,7 +87,10 @@ @Resource private ShopClient shopClient; - + @Resource + private PayMoneyUtil payMoneyUtil; + @Resource + private WechatPayService wechatPayService; @ResponseBody @@ -104,12 +112,19 @@ * 订单支付回调通知 */ @ResponseBody - @GetMapping("/orderPaymentCallback") - public void orderPaymentCallback(UniPayCallbackResult uniPayCallbackResult, HttpServletResponse response){ - String jsonString = JSONObject.toJSONString(uniPayCallbackResult); + @PostMapping("/orderPaymentCallback") + public void orderPaymentCallback(HttpServletRequest request, HttpServletResponse response){ +// String jsonString = JSONObject.toJSONString(uniPayCallbackResult); + System.err.println("1111111111111"); + PayResult payResult= null; + try { + payResult = wechatPayService.processNotify(request); + } catch (Exception e) { + throw new RuntimeException(e); + } System.out.println("1111111111111111111111"); - System.out.println(jsonString); - R callback = orderService.orderPaymentCallback(uniPayCallbackResult); +// System.out.println(jsonString); + R callback = orderService.orderPaymentCallback(payResult); if(callback.getCode() == 200){ response.setStatus(200); PrintWriter out = null; @@ -406,26 +421,16 @@ */ /** * 订单取消支付回退 - * - * @param refundCallbackResult - * @param response - * @return */ @ResponseBody - @GetMapping("/refundPayMoneyCallback") - public void refundPayMoneyCallback(RefundCallbackResult refundCallbackResult, HttpServletResponse response) { - R callback = orderService.refundPayMoneyCallback(refundCallbackResult); + @PostMapping("/refundPayMoneyCallback") + public String refundPayMoneyCallback( @RequestBody(required = false) String xmlData) { + R callback = orderService.refundPayMoneyCallback(xmlData); if (callback.getCode() == 200) { - response.setStatus(200); - PrintWriter out = null; - try { - out = response.getWriter(); - } catch (IOException e) { - throw new RuntimeException(e); - } - out.println("success"); - out.flush(); - out.close(); + return "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>"; + }else { + System.err.println("支付回退错误:"+callback.getMsg()); + return "<xml><return_code><![CDATA[FAIL]]></return_code></xml>"; } } @@ -778,5 +783,15 @@ return R.ok(orderService.getMap(queryWrapper)); } + /** + * 获取商户RSA加密公钥 + */ + + @GetMapping("/getRsaPublicKey") + public R<Void> getRsaPublicKey(){ + wechatPayService.getRsaPublicKey(); + return R.ok(); + } + } diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/OrderService.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/OrderService.java index 0ea0d30..65464b7 100644 --- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/OrderService.java +++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/OrderService.java @@ -7,12 +7,16 @@ import com.ruoyi.order.model.Order; import com.ruoyi.order.util.payment.model.RefundCallbackResult; import com.ruoyi.order.util.payment.model.UniPayCallbackResult; +import com.ruoyi.order.util.payment.wx.vo.PayResult; import com.ruoyi.order.vo.*; import org.springframework.web.multipart.MultipartFile; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; +import java.util.Map; /** * <p> @@ -72,7 +76,7 @@ * 取消订单后回调处理 * @return */ - R refundPayMoneyCallback(RefundCallbackResult refundCallbackResult); + R refundPayMoneyCallback(String xmlData); /** @@ -121,7 +125,7 @@ /** * 订单支付回调通知 */ - R orderPaymentCallback(UniPayCallbackResult uniPayCallbackResult); + R orderPaymentCallback(PayResult payResult); /** * 定时任务关闭订单 */ diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/OrderServiceImpl.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/OrderServiceImpl.java index f632abb..948d8fa 100644 --- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/OrderServiceImpl.java +++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/OrderServiceImpl.java @@ -33,6 +33,11 @@ import com.ruoyi.order.util.payment.PaymentUtil; import com.ruoyi.order.util.payment.model.*; +import com.ruoyi.order.util.payment.wechat.PayMoneyUtil; +import com.ruoyi.order.util.payment.wx.WechatPayConfig; +import com.ruoyi.order.util.payment.wx.WechatPayService; +import com.ruoyi.order.util.payment.wx.vo.PayResult; +import com.ruoyi.order.util.payment.wx.vo.RefundCallbackResult; import com.ruoyi.order.vo.*; import com.ruoyi.other.api.domain.*; import com.ruoyi.other.api.feignClient.*; @@ -52,6 +57,8 @@ import org.springframework.util.CollectionUtils; import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import java.math.BigDecimal; import java.math.RoundingMode; @@ -122,6 +129,13 @@ private RegionClient regionClient; @Resource private ApplicationEventPublisher applicationEventPublisher; + + + @Resource + private PayMoneyUtil payMoneyUtil; + + @Resource + private WechatPayService wechatPayService; @Override @@ -433,10 +447,16 @@ BigDecimal paymentAmount = order.getPaymentAmount(); if (BigDecimal.ZERO.compareTo(order.getPaymentAmount()) < 0) {//支付的金额是否大于0 //微信退款 - RefundResult refund = PaymentUtil.refund(order.getOrderNumber(), "R" + order.getOrderNumber(), paymentAmount.doubleValue(), "/order/order/refundPayMoneyCallback"); - if (!"100".equals(refund.getRa_Status())) { - return R.fail(refund.getRc_CodeMsg());//退款失败 + Map<String,String> map = wechatPayService.refund(order.getOrderNumber(), order.getOrderNumber(), order.getPaymentAmount().toString(), order.getPaymentAmount().toString(), "退款", "/order/order/refundPayMoneyCallback"); + +// RefundResult refund = PaymentUtil.refund(order.getOrderNumber(), "R" + order.getOrderNumber(), paymentAmount.doubleValue(), "/order/order/refundPayMoneyCallback"); + if (!"SUCCESS".equals(map.get("return_code"))) { + return R.fail(map.get("return_msg"));//退款失败 } + //这里申请成功后先返回,等待微信退款成功再返回积分这些 + order.setRefundStatus(1); + this.updateById(order); + return R.ok(); } //退款成功再回退积分 AppUser appUser = appUserClient.getAppUserById(order.getAppUserId()); @@ -516,16 +536,90 @@ * @return */ @Override - public R refundPayMoneyCallback(RefundCallbackResult refundCallbackResult) { - String code = refundCallbackResult.getR3_RefundOrderNo().substring(1); - Order order = this.getOne(new LambdaQueryWrapper<Order>().eq(Order::getOrderNumber, code)); - if (null == order || order.getPayStatus() == 1 || order.getOrderStatus() == 6) { + public R refundPayMoneyCallback(String xmlData) { + + RefundCallbackResult result = wechatPayService.processRefundCallback(xmlData); + if (!result.isSuccess()) { + return R.fail(result.getMsg()); + } + + Order order = this.getOne(new LambdaQueryWrapper<Order>().eq(Order::getOrderNumber, result.getOrderNo())); + if (null == order || order.getPayStatus() == 1 || order.getRefundStatus() == 2) { return R.ok(); } - order.setRefundCode(refundCallbackResult.getR5_RefundTrxNo()); +// order.setRefundCode(refundCallbackResult.getR5_RefundTrxNo()); + order.setRefundCode(result.getRefundNo()); order.setRefundStatus(2); order.setRefundTime(LocalDateTime.now()); this.updateById(order); + + //退款成功再回退积分 + AppUser appUser = appUserClient.getAppUserById(order.getAppUserId()); + if (order.getPoint()>0) { + if(null==appUser.getCancelPoint()){ + appUser.setCancelPoint(0); + } + //返回订单抵扣积分 + Integer historicalPoint = appUser.getAvailablePoint(); + Integer availablePoint = appUser.getAvailablePoint() + order.getPoint();//可用积分 + Integer cancelPoint = appUser.getCancelPoint() + order.getPoint();//取消订单积分 + + appUser.setAvailablePoint(availablePoint); + appUser.setCancelPoint(cancelPoint); + appUser.setTotalPoint(appUser.getTotalPoint() + order.getPoint()); + appUserClient.editAppUserById(appUser); + //构建积分流水 + UserPoint userPoint = new UserPoint(); + userPoint.setType(16);//取消订单 + userPoint.setHistoricalPoint(historicalPoint); + userPoint.setVariablePoint(order.getPoint()); + userPoint.setBalance(availablePoint); + userPoint.setCreateTime(LocalDateTime.now()); + userPoint.setAppUserId(order.getAppUserId()); + userPoint.setObjectId(order.getId()); + userPointClient.saveUserPoint(userPoint); + } + + order.setRefundStatus(2); + order.setRefundTime(LocalDateTime.now()); + + //商品销售数量 + OrderGood orderGood = orderGoodService.getOne(new LambdaQueryWrapper<OrderGood>().eq(OrderGood::getOrderId, order.getId())); + goodsClient.editGoodsNum(orderGood.getGoodsId(), -1); + //获取商品json + Goods good = JSON.parseObject(orderGood.getGoodJson(), Goods.class); + GoodsSeckill goodsSeckill = JSON.parseObject(orderGood.getSeckillJson(), GoodsSeckill.class); + + //门店减少冻结资金 即减少余额, 冻结资金=余额-可用资金 + Shop shop = shopClient.getShopById(order.getShopId()).getData(); + + BigDecimal historicalBalance=shop.getBalance();//历史余额 + BigDecimal variableAmount=BigDecimal.ZERO;//变动金额 + if (null != goodsSeckill) { + variableAmount=goodsSeckill.getSellingPrice(); + }else { + variableAmount=good.getSellingPrice(); + } + + BigDecimal balance=shop.getBalance().subtract(variableAmount);//变动后余额 + + shop.setBalance(balance); + shopClient.updateShop(shop); + + //门店余额流水记录 + ShopBalanceStatement shopBalanceStatement = new ShopBalanceStatement(); + shopBalanceStatement.setShopId(shop.getId()); + shopBalanceStatement.setShopName(shop.getName()); + shopBalanceStatement.setShopManagerName(shop.getShopManager()); + shopBalanceStatement.setPhone(shop.getPhone()); + shopBalanceStatement.setType(6);//变更类型,订单退款 + shopBalanceStatement.setHistoricalBalance(historicalBalance); + shopBalanceStatement.setVariableAmount(variableAmount); + shopBalanceStatement.setCreateTime(LocalDateTime.now()); + shopBalanceStatement.setBalance(balance); + shopBalanceStatement.setCreateUserId(appUser.getId()); + shopBalanceStatement.setObjectId(order.getId()); + shopBalanceStatementClient.saveShopBalanceStatement(shopBalanceStatement); return R.ok(); } @@ -913,49 +1007,54 @@ if ( BigDecimal.ZERO.compareTo(paymentMoney) < 0){ //调起微信支付 String goodsNames = goods.getName(); - UniPayResult uniPayResult = PaymentUtil.uniPay(order.getOrderNumber(), paymentMoney.doubleValue(), "购买单品商品", - goodsNames, "", "/order/order/orderPaymentCallback", appUser.getWxOpenid(), null); - if(null == uniPayResult || !"100".equals(uniPayResult.getRa_Code())){ - //支付失败,积分回退 ,删除订单 - //检查是否先有过积分抵扣了,是的话要返回订单已抵扣的积分,以及用户积分流水的删除 - if (order.getPoint()>0) { - //返回订单抵扣积分 - AppUser appUser2 = appUserClient.getAppUserById(order.getAppUserId()); - Integer availablePoint = appUser2.getAvailablePoint();//可用积分 - Integer variablePoint = order.getPoint();//变动积分 - Integer balance = appUser2.getAvailablePoint() + order.getPoint();//变动后积分 - Integer cancelPoint = appUser2.getCancelPoint() + order.getPoint();//取消订单积分 - appUser2.setAvailablePoint(availablePoint); - appUser2.setCancelPoint(cancelPoint); - appUser2.setTotalPoint(appUser2.getTotalPoint() + order.getPoint()); + try { + R r = wechatPayService.unifiedOrder(order.getId().toString(), order.getOrderNumber(), paymentMoney.toString(), "购买单品商品",appUser.getWxOpenid(),"/order/order/orderPaymentCallback"); + if (null == r || 200 != r.getCode()){ + //支付失败,积分回退 ,删除订单 + //检查是否先有过积分抵扣了,是的话要返回订单已抵扣的积分,以及用户积分流水的删除 + if (order.getPoint()>0) { + //返回订单抵扣积分 + AppUser appUser2 = appUserClient.getAppUserById(order.getAppUserId()); + Integer availablePoint = appUser2.getAvailablePoint();//可用积分 + Integer variablePoint = order.getPoint();//变动积分 + Integer balance = appUser2.getAvailablePoint() + order.getPoint();//变动后积分 + Integer cancelPoint = appUser2.getCancelPoint() + order.getPoint();//取消订单积分 + appUser2.setAvailablePoint(availablePoint); + appUser2.setCancelPoint(cancelPoint); + appUser2.setTotalPoint(appUser2.getTotalPoint() + order.getPoint()); - //构建积分流水记录 - UserPoint userPoint = new UserPoint(); - userPoint.setType(16);//取消订单 - userPoint.setHistoricalPoint(availablePoint); - userPoint.setVariablePoint(variablePoint); - userPoint.setBalance(balance); - userPoint.setCreateTime(LocalDateTime.now()); - userPoint.setAppUserId(appUser2.getId()); - userPoint.setObjectId(order.getId()); - userPointClient.saveUserPoint(userPoint); + //构建积分流水记录 + UserPoint userPoint = new UserPoint(); + userPoint.setType(16);//取消订单 + userPoint.setHistoricalPoint(availablePoint); + userPoint.setVariablePoint(variablePoint); + userPoint.setBalance(balance); + userPoint.setCreateTime(LocalDateTime.now()); + userPoint.setAppUserId(appUser2.getId()); + userPoint.setObjectId(order.getId()); + userPointClient.saveUserPoint(userPoint); - appUserClient.editAppUserById(appUser2); + appUserClient.editAppUserById(appUser2); + + } //删除订单 order.setDelFlag(1); orderMapper.updateById(order); + //返回报错信息 + return R.fail(null == r ? "支付失败" : r.getMsg()); } - //返回报错信息 - return R.fail(null == uniPayResult ? "支付失败" : uniPayResult.getRb_CodeMsg()); + /*if(null == uniPayResult || !"100".equals(uniPayResult.getRa_Code())){ + + }*/ + //将支付数据添加到redis队列中,便于定时任务去校验是否完成支付,没有完成支付支付,15分钟后关闭订单。 + long second = LocalDateTime.now().plusMinutes(15).toEpochSecond(ZoneOffset.UTC); + redisTemplate.opsForZSet().add("OrderPayment", order.getId(), second); + return r; + }catch (Exception e){ + e.printStackTrace(); } - String rc_result = uniPayResult.getRc_Result(); - JSONObject jsonObject = JSON.parseObject(rc_result); - jsonObject.put("orderId", order.getId().toString()); - //将支付数据添加到redis队列中,便于定时任务去校验是否完成支付,没有完成支付支付,15分钟后关闭订单。 - long second = LocalDateTime.now().plusMinutes(15).toEpochSecond(ZoneOffset.UTC); - redisTemplate.opsForZSet().add("OrderPayment", order.getOrderNumber(), second); - return R.ok(jsonObject.toJSONString()); + } @@ -993,15 +1092,18 @@ shopBalanceStatement.setCreateUserId(appUser.getId()); shopBalanceStatement.setObjectId(order.getId()); shopBalanceStatementClient.saveShopBalanceStatement(shopBalanceStatement); - return R.ok(order.getId().toString()); + Map<String, String> payParams = new HashMap<>(); + payParams.put("payMethod","3");//给前端标识 3-不需要调微信支付 + payParams.put("orderId",order.getId().toString()); + return R.ok(JSON.toJSONString(payParams)); } /** - * 订单支付回调通知 + * 订单支付回调 处理业务逻辑 */ @Override - public R orderPaymentCallback(UniPayCallbackResult uniPayCallbackResult) { - Order order = orderMapper.selectOne(new LambdaQueryWrapper<Order>().eq(Order::getOrderNumber, uniPayCallbackResult.getR2_OrderNo())); + public R orderPaymentCallback(PayResult payResult) { + Order order = orderMapper.selectOne(new LambdaQueryWrapper<Order>().eq(Order::getOrderNumber, payResult.getOrderNumber())); if(null == order || order.getPayStatus() == 2){ return R.ok(); } @@ -1017,7 +1119,7 @@ order.setPayStatus(2); //待使用 order.setOrderStatus(3); - String r7TrxNo = uniPayCallbackResult.getR9_BankTrxNo(); + String r7TrxNo = payResult.getTransactionId(); order.setSerialNumber(r7TrxNo); orderMapper.updateById(order); @@ -1068,18 +1170,17 @@ long second = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC); Set<String> orderPayment = redisTemplate.opsForZSet().rangeByScore("OrderPayment", 0, second); if(orderPayment.size() > 0){ - List<Order> list = orderMapper.selectList(new LambdaQueryWrapper<Order>().in(Order::getOrderNumber, orderPayment)); + List<Order> list = orderMapper.selectList(new LambdaQueryWrapper<Order>().in(Order::getId, orderPayment)); for (Order order : list) { if(null == order || order.getPayStatus() != 1){ redisTemplate.opsForZSet().remove("OrderPayment", order.getOrderNumber()); continue; } //开始执行关闭订单操作 - CloseOrderResult closeOrderResult = PaymentUtil.closeOrder(order.getOrderNumber()); - if((null == closeOrderResult || !closeOrderResult.getRa_Status().equals("100")) && - Arrays.asList("0", "4", "101", "10080000", "10080002", "10083004", "10083005").contains(closeOrderResult.getRb_Code())){ + Map<String, String> map = wechatPayService.closeOrder(order.getOrderNumber()); + if((null == map || !map.get("return_code").equals("SUCCESS"))){ redisTemplate.opsForZSet().add("OrderPayment", order.getOrderNumber(), 0); - log.error("关闭订单失败:{}---->{}", order.getOrderNumber(), JSON.toJSONString(closeOrderResult)); + log.error("关闭订单失败:{}---->{}", order.getOrderNumber(), map.get("return_msg")); } redisTemplate.opsForZSet().remove("OrderPayment", order.getOrderNumber()); //关闭订单后,检查是否先有过积分抵扣了,是的话要返回订单已抵扣的积分,以及用户积分流水的删除, 删除订单 diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/PaymentUtil.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/PaymentUtil.java index 9d0ae2a..776a2c0 100644 --- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/PaymentUtil.java +++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/PaymentUtil.java @@ -22,7 +22,7 @@ /** * 商户密钥 */ - private static final String key = "a2369875124965782f148539657823152"; + private static final String key = "ss369875124965782f148539657826321"; /** * 商户号 */ @@ -280,10 +280,10 @@ public static void main(String[] args) { -// UniPayResult uniPayResult = PaymentUtil.uniPay("852963742", 0.01D, "测试商品", "这是用于对接支付测试的商品描述", -// "", "/order/shopping-cart/shoppingCartPaymentCallback", "ooOrs64zHLuInkZ_GF0LpIN9_Rxc", "777168500885852"); -// PaymentUtil.queryOrder("852963742"); -// PaymentUtil.closeOrder("852963742"); + UniPayResult uniPayResult = PaymentUtil.uniPay("852963742", 0.01D, "测试商品", "这是用于对接支付测试的商品描述", + "", "/order/shopping-cart/shoppingCartPaymentCallback", "ooOrs64zHLuInkZ_GF0LpIN9_Rxc", "777168500885852"); + PaymentUtil.queryOrder("852963742"); + PaymentUtil.closeOrder("852963742"); } } diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wechat/PayMoneyUtil.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wechat/PayMoneyUtil.java new file mode 100644 index 0000000..6948dc8 --- /dev/null +++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wechat/PayMoneyUtil.java @@ -0,0 +1,1295 @@ +package com.ruoyi.order.util.payment.wechat; + +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alipay.api.AlipayApiException; +import com.alipay.api.AlipayClient; +import com.alipay.api.CertAlipayRequest; +import com.alipay.api.DefaultAlipayClient; +import com.alipay.api.domain.AlipayTradeAppPayModel; +import com.alipay.api.domain.AlipayTradeRefundModel; +import com.alipay.api.request.*; +import com.alipay.api.response.*; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.order.util.payment.MD5AndKL; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.dom4j.Document; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.dom4j.io.SAXReader; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.SecretKeySpec; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.servlet.http.HttpServletRequest; +import java.io.*; +import java.math.BigDecimal; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.security.*; +import java.util.*; + +/** + * 第三方支付工具类 + */ +@Component +public class PayMoneyUtil { + + @Value("${alipay.appid}") + private String aliAppid;//支付宝appid + + @Value("${alipay.appPrivateKey}") + private String appPrivateKey;//支付宝开发者应用私钥 + + @Value("${alipay.alipayPublicKey}") + private String alipayPublicKey;//支付宝应用公钥 + + @Value("${alipay.alipay_public_key}") + private String alipay_public_key;//支付宝支付公钥 + + @Value("${wx.appid}") + private String appid;//微信appid + + @Value("${wx.appletsAppid}") + private String appletsAppid;//微信小程序appid + + @Value("${wx.mchId}") + private String mchId;//微信商户号 + + @Value("${wx.key}") + private String key;//微信商户号 + + @Value("${wx.callbackPath}") + private String callbackPath;//支付回调网关地址 + + @Value(("${wx.certPath}")) + private String certPath;//微信证书路径 + + private String app_cert_path = "C:/cert/alipay/driver/app_cert_path.crt";//应用公钥证书路径 + + private String alipay_cert_path = "C:/cert/alipay/driver/alipay_cert_path.crt";//支付宝公钥证书文件路径 + + private String alipay_root_cert_path = "C:/cert/alipay/driver/alipay_root_cert_path.crt";//支付宝CA根证书文件路径 + + + + private Map<String, JSONObject> order = new HashMap<>();//存储支付订单用于主动查询支付结果 + + + /** + * 支付宝支付 + */ + public R alipay(String body, String subject, String passbackParams, String outTradeNo, String amount, String notifyUrl){ + //构造client + CertAlipayRequest certAlipayRequest = new CertAlipayRequest (); + //设置网关地址 + certAlipayRequest.setServerUrl("https://openapi.alipay.com/gateway.do"); + //设置应用Id + certAlipayRequest.setAppId(aliAppid); + //设置应用私钥 + certAlipayRequest.setPrivateKey(appPrivateKey); + //设置请求格式,固定值json + certAlipayRequest.setFormat("json"); + //设置字符集 + certAlipayRequest.setCharset("UTF-8"); + //设置签名类型 + certAlipayRequest.setSignType("RSA2"); + //设置应用公钥证书路径 + certAlipayRequest.setCertPath(app_cert_path); + //设置支付宝公钥证书路径 + certAlipayRequest.setAlipayPublicCertPath(alipay_cert_path); + //设置支付宝根证书路径 + certAlipayRequest.setRootCertPath(alipay_root_cert_path); + //构造client + AlipayClient alipayClient = null; + try { + alipayClient = new DefaultAlipayClient(certAlipayRequest); + } catch (AlipayApiException e) { + e.printStackTrace(); + } + //实例化具体API对应的request类,类名称和接口名称对应,当前调用接口名称:alipay.trade.app.pay + AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest (); + //SDK已经封装掉了公共参数,这里只需要传入业务参数。以下方法为sdk的model入参方式(model和biz_content同时存在的情况下取biz_content)。 + AlipayTradeAppPayModel model = new AlipayTradeAppPayModel (); + model.setBody(body); + model.setSubject (subject); + model.setOutTradeNo (outTradeNo); + model.setTimeoutExpress ("30m" ); + model.setTotalAmount (amount); + model.setProductCode ( "QUICK_MSECURITY_PAY" ); + model.setPassbackParams(passbackParams);//自定义参数 + request.setBizModel ( model ); + request.setNotifyUrl (callbackPath + notifyUrl); + try { + //这里和普通的接口调用不同,使用的是sdkExecute + AlipayTradeAppPayResponse response = alipayClient.sdkExecute(request); + System.out.println(response.getBody());//就是orderString 可以直接给客户端请求,无需再做处理。 + return R.ok(response.getBody()); + } catch (AlipayApiException e ) { + e.printStackTrace(); + } + + +// //实例化客户端 +// AlipayClient alipayClient = new DefaultAlipayClient("https://openapi.alipay.com/gateway.do", aliAppid, appPrivateKey, "json", "UTF-8", alipay_public_key, "RSA2"); +// //实例化具体API对应的request类,类名称和接口名称对应,当前调用接口名称:alipay.trade.app.pay +// AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest(); +// //SDK已经封装掉了公共参数,这里只需要传入业务参数。以下方法为sdk的model入参方式(model和biz_content同时存在的情况下取biz_content)。 +// AlipayTradeAppPayModel model = new AlipayTradeAppPayModel(); +// model.setBody(body);//对一笔交易的具体描述信息。如果是多种商品,请将商品描述字符串累加传给body。 +// model.setSubject(subject);//商品的标题/交易标题/订单标题/订单关键字等。 +// model.setOutTradeNo(outTradeNo);//商户网站唯一订单号 +// model.setTimeoutExpress("30m"); +// model.setTotalAmount(amount);//付款金额 +// model.setProductCode("QUICK_MSECURITY_PAY"); +// model.setPassbackParams(passbackParams);//自定义参数 +// request.setBizModel(model); +// request.setNotifyUrl(callbackPath + notifyUrl); +// try { +// //这里和普通的接口调用不同,使用的是sdkExecute +// AlipayTradeAppPayResponse response = alipayClient.sdkExecute(request); +// Map<String, String> map = new HashMap<>(); +// map.put("orderString", response.getBody()); +// System.out.println(map);//就是orderString 可以直接给客户端请求,无需再做处理。 +// return ResultUtil.success(map); +// } catch (AlipayApiException e) { +// e.printStackTrace(); +// } + return null; + } + + + /** + * 支付宝扫码支付下单 + * @param body + * @param subject + * @param outTradeNo + * @param amount + * @param notifyUrl + * @return + */ + public R aliScanCodePay(String body, String subject, String outTradeNo, String amount, String notifyUrl){ + AlipayClient alipayClient = new DefaultAlipayClient("https://openapi.alipay.com/gateway.do", aliAppid, appPrivateKey, "json", "UTF-8", alipay_public_key, "RSA2"); //获得初始化的AlipayClient + AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();//创建API对应的request类 + request.setBizContent("{" + + " \"out_trade_no\":\"" + outTradeNo + "\"," +//商户订单号 + " \"total_amount\":\"" + 1 + "\"," + + " \"subject\":\"" + subject + "\"," + + " \"notify_url\":\"" + callbackPath + notifyUrl + "\"," + + " \"body\":\"" + body + "\"," + + " \"store_id\":\"NJ_001\"," + + " \"timeout_express\":\"90m\"}");//订单允许的最晚付款时间 + AlipayTradePrecreateResponse response = null; + try { + response = alipayClient.execute(request); + } catch (AlipayApiException e) { + e.printStackTrace(); + } + JSONObject alipay_trade_precreate_response = JSON.parseObject(response.getBody()).getJSONObject("alipay_trade_precreate_response"); + + System.err.print(alipay_trade_precreate_response.getString("qr_code")); + return R.ok(alipay_trade_precreate_response.getString("qr_code")); + } + + + /** + * 支付成功后的回调处理逻辑 + * @param request + */ + public Map<String, String> alipayCallback(HttpServletRequest request){ + //获取支付宝POST过来反馈信息 + Map<String,String> params = new HashMap<String,String>(); + Map requestParams = request.getParameterMap(); + for (Iterator iter = requestParams.keySet().iterator(); iter.hasNext();) { + String name = (String) iter.next(); + String[] values = (String[]) requestParams.get(name); + String valueStr = ""; + for (int i = 0; i < values.length; i++) { + valueStr = (i == values.length - 1) ? valueStr + values[i] + : valueStr + values[i] + "_"; + } + //乱码解决,这段代码在出现乱码时使用。 + //valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8"); + params.put(name, valueStr); + } + //切记alipaypublickey是支付宝的公钥,请去open.alipay.com对应应用下查看。 + //boolean AlipaySignature.rsaCheckV1(Map<String, String> params, String publicKey, String charset, String sign_type) +// try { +// boolean flag = AlipaySignature.rsaCheckV1(params, alipay_public_key, "UTF-8","RSA2"); +// if(flag){ +// Map<String, String> map = new HashMap<>(); +// String out_trade_no = params.get("out_trade_no"); +// String subject = params.get("subject"); +// String total_amount = params.get("total_amount"); +// String trade_no = params.get("trade_no"); +// String passback_params = params.get("passback_params"); +// map.put("out_trade_no", out_trade_no);//商家订单号 +// map.put("subject", subject); +// map.put("total_amount", total_amount); +// map.put("trade_no", trade_no);//支付宝交易号 +// map.put("passback_params", passback_params);//回传参数 +// return map; +// }else{ +// System.err.println("验签失败"); +// } +// +// } catch (AlipayApiException e) { +// e.printStackTrace(); +// } +// return null; + + + Map<String, String> map = new HashMap<>(); + String out_trade_no = params.get("out_trade_no"); + String subject = params.get("subject"); + String total_amount = params.get("total_amount"); + String trade_no = params.get("trade_no"); + String passback_params = params.get("passback_params"); + map.put("out_trade_no", out_trade_no);//商家订单号 + map.put("subject", subject); + map.put("total_amount", total_amount); + map.put("trade_no", trade_no);//支付宝交易号 + map.put("passback_params", passback_params);//回传参数 + return map; + } + + + /** + * 支付宝查询订单支付状态 + * @param out_trade_no + * @return + * @throws Exception + */ + public R queryALIOrder(String out_trade_no) throws Exception{ + AlipayClient alipayClient = new DefaultAlipayClient("https://openapi.alipay.com/gateway.do",aliAppid, appPrivateKey,"json","UTF-8", alipay_public_key,"RSA2"); + AlipayTradeQueryRequest request = new AlipayTradeQueryRequest(); + request.setBizContent("{" + + "\"out_trade_no\":" + out_trade_no + + " }"); + AlipayTradeQueryResponse response = alipayClient.execute(request); + if(response.isSuccess()){ + String tradeStatus = response.getTradeStatus();//交易状态:WAIT_BUYER_PAY(交易创建,等待买家付款)、TRADE_CLOSED(未付款交易超时关闭,或支付完成后全额退款)、TRADE_SUCCESS(交易支付成功)、TRADE_FINISHED(交易结束,不可退款) + return R.ok(tradeStatus); + } else { + return R.fail(response.getMsg()); + } + } + + + + /** + * 微信统一下单 + * @param body 商品描述 + * @param attach 附加数据 + * @param out_trade_no 商户订单号 + * @param total_fee 标价金额 + * @param notify_url 通知地址 + * @param tradeType 交易类型 + * @return + */ + public R weixinpay(String body, String attach, String out_trade_no, String total_fee, String notify_url, String tradeType, String openId) throws Exception{ + int i = new BigDecimal(total_fee).multiply(new BigDecimal("100")).intValue(); + String hostAddress = null; + try { + hostAddress = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + String nonce_str = UUID.randomUUID().toString(); + Map<String, Object> map = new HashMap<>(); + map.put("appid", "APP".equals(tradeType) ? appid : appletsAppid); + map.put("mch_id", mchId); + map.put("nonce_str", nonce_str); + map.put("body", body); + map.put("attach", attach);//存储订单id + map.put("out_trade_no", out_trade_no);//存储的订单code + map.put("total_fee", i); + map.put("spbill_create_ip", hostAddress); + map.put("notify_url", callbackPath + notify_url); + map.put("trade_type", tradeType); + if("JSAPI".equals(tradeType)){ + map.put("openid", openId); + } + String s = this.weixinSignature(map); + map.put("sign", s); + + String url = "https://api.mch.weixin.qq.com/pay/unifiedorder"; + //设置请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_XML); + StringBuffer xmlString = new StringBuffer(); + Set<String> strings = map.keySet(); + String[] keys = {}; + keys = strings.toArray(keys); + Arrays.sort(keys); + xmlString.append("<xml>"); + for(int l = 0; l < keys.length; l++){ + xmlString.append("<" + keys[l] + ">" + map.get(keys[l]) + "</" + keys[l] + ">"); + } + xmlString.append("</xml>"); + + Map<String, String> map1 = null; +// String body1 = httpClientUtil.pushHttpRequsetXml(url, xmlString.toString(), new HashMap<>()); + String body1 = HttpRequest.post(url) + .body(xmlString.toString()) + .contentType("application/xml; charset=UTF-8") + .timeout(5000) // 设置超时时间 + .execute() + .body(); + //将结果xml解析成map + body1 = body1.replaceAll("<!\\[CDATA\\[",""); + body1 = body1.replaceAll("]]>", ""); + try { + map1 = this.xmlToMap(body1, "UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } catch (DocumentException e) { + e.printStackTrace(); + } + String return_code = map1.get("return_code"); + if("SUCCESS".equals(return_code)){ + String result_code = map1.get("result_code"); + if("SUCCESS".equals(result_code)){ + String type = map1.get("trade_type"); + String prepay_id = map1.get("prepay_id"); + switch (type){ + case "JSAPI": + //重新进行签名后返回给前端 + Map<String, Object> map2 = new HashMap<>(); + map2.put("appId", map1.get("appid")); + map2.put("nonceStr", map1.get("nonce_str")); + map2.put("package", "prepay_id=" + prepay_id); + map2.put("signType", "MD5"); + map2.put("timeStamp", new Date().getTime() + ""); + String s2 = this.weixinSignature(map2); + + map2.put("prepay_id", prepay_id); + map2.put("mch_id", map1.get("mch_id")); + map2.put("trade_type", map1.get("trade_type")); + + map2.put("sign", s2); + return R.ok(map2); + case "NATIVE": + String code_url = map1.get("code_url"); + return R.ok(code_url); + case "APP": + //重新进行签名后返回给前端 + Map<String, Object> map3 = new HashMap<>(); + map3.put("appid", appid); + map3.put("noncestr", nonce_str); + map3.put("package", "Sign=WXPay"); + map3.put("partnerid", mchId); + map3.put("prepayid", prepay_id); + map3.put("timestamp", new Date().getTime() / 1000); + String s1 = this.weixinSignature(map3); + map3.put("sign", s1); + System.err.println(map3); + return R.ok(map3); + } + return null; + }else{ + System.err.println(map1.get("err_code_des")); + return R.fail(map1.get("err_code_des")); + } + }else{ + System.err.println(map1.get("return_msg") + appid + "----" + mchId); + return R.fail(map1.get("return_msg")); + } + } + + + + + + /** + * 微信支付成功后的回调处理 + * @param request + */ + public Map<String, String> weixinpayCallback(HttpServletRequest request){ + try { + String param = this.getParam(request); + param = param.replaceAll("<!\\[CDATA\\[",""); + param = param.replaceAll("]]>", ""); + Map<String, String> map = this.xmlToMap(param, "UTF-8"); + String return_code = map.get("return_code"); + if("SUCCESS".equals(return_code)){ + String result_code = map.get("result_code"); + if("SUCCESS".equals(result_code)){ + Map<String, String> map1 = new HashMap<>(); + map1.put("nonce_str", map.get("nonce_str")); + map1.put("out_trade_no", map.get("out_trade_no"));//存储的订单code + map1.put("attach", map.get("attach"));//存储订单id + map1.put("total_fee", map.get("total_fee")); + map1.put("transaction_id", map.get("transaction_id"));//微信支付订单号 + String result = "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>"; + map1.put("result", result); + return map1; + }else{ + System.err.println(map.get("err_code_des")); + } + }else{ + System.err.println(map.get("return_msg")); + } + } catch (IOException e) { + e.printStackTrace(); + } catch (DocumentException e) { + e.printStackTrace(); + } + return null; + } + + + /** + * 微信扫码收款 + * @param body 商品描述 + * @param attach 附加数据 + * @param nonce_str 随机字符串 + * @param out_trade_no 商户订单号 + * @param total_fee 订单金额 + * @param auth_code 授权码 扫码支付授权码,设备读取用户微信中的条码或者二维码信息(注:用户付款码条形码规则:18位纯数字,以10、11、12、13、14、15开头) + * @return + */ + public R wxScanQRCodePay(String body, String attach, String nonce_str, String out_trade_no, String total_fee, String auth_code){ + int i = new BigDecimal(total_fee).multiply(new BigDecimal("100")).intValue(); + String hostAddress = null; + try { + InetAddress address = InetAddress.getLocalHost(); + hostAddress = address.getHostAddress(); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + String randomCode = null; + try { + randomCode = UUID.randomUUID().toString(); + } catch (Exception e) { + e.printStackTrace(); + } + Map<String, Object> map = new HashMap<>(); + map.put("appid", appid); + map.put("mch_id", mchId); + map.put("nonce_str", nonce_str);//存储的支付人员id,员工扫描二维码支付的时候存储的是收款员工id + map.put("body", body); + map.put("attach", attach);//存储的费用月份数据,员工扫描二维码支付的时候存储的是收费项id + map.put("out_trade_no", randomCode + "_" + out_trade_no);//存储的房间id + map.put("total_fee", i); + map.put("spbill_create_ip", hostAddress); + map.put("auth_code", auth_code); + String s = this.weixinSignature(map); + map.put("sign", s); + + String url = "https://api.mch.weixin.qq.com/pay/unifiedorder"; + //设置请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_XML); + StringBuffer xmlString = new StringBuffer(); + Set<String> strings = map.keySet(); + String[] keys = {}; + keys = strings.toArray(keys); + Arrays.sort(keys); + xmlString.append("<xml>"); + for(int l = 0; l < keys.length; l++){ + xmlString.append("<" + keys[l] + ">" + map.get(keys[l]) + "</" + keys[l] + ">"); + } + xmlString.append("</xml>"); + + Map<String, String> map1 = null; +// String body1 = httpClientUtil.pushHttpRequsetXml(url, xmlString.toString(), new HashMap<>()); + String body1 = HttpRequest.post(url) + .body(xmlString.toString()) + .contentType("application/xml; charset=UTF-8") + .timeout(5000) // 设置超时时间 + .execute() + .body(); + + //将结果xml解析成map + body1 = body1.replaceAll("<!\\[CDATA\\[",""); + body1 = body1.replaceAll("]]>", ""); + try { + map1 = this.xmlToMap(body1, "UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } catch (DocumentException e) { + e.printStackTrace(); + } + String return_code = map1.get("return_code"); + if("SUCCESS".equals(return_code)){ + String result_code = map1.get("result_code"); + if("SUCCESS".equals(result_code)){ + String type = map1.get("trade_type"); + switch (type){ + case "JSAPI": + break; + case "NATIVE": + String code_url = map1.get("code_url"); + return R.ok(code_url); + case "APP": + String prepay_id = map1.get("prepay_id"); + //重新进行签名后返回给前端 + Map<String, Object> map2 = new HashMap<>(); + map2.put("appid", appid); + map2.put("noncestr", nonce_str); + map2.put("package", "Sign=WXPay"); + map2.put("partnerid", mchId); + map2.put("prepayid", prepay_id); + map2.put("timestamp", new Date().getTime() + ""); + String s1 = this.weixinSignature(map2); + + map2.put("pac", "Sign=WXPay"); + map2.put("sign", s1); +// System.err.println(map2); + return R.ok(map2); + } + return null; + }else{ +// System.err.println(map1.get("err_code_des")); + return R.fail(map1.get("err_code_des")); + } + }else{ +// System.err.println(map1.get("return_msg") + appid + "----" + mchId); + return R.fail(map1.get("return_msg")); + } + } + + + /** + * 支付宝扫码收款 + * @param data + * @return + */ + public Object aliScanQRCodePay(String data){ + return null; + } + + + /** + * 微信退款申请 + * @param transaction_id 微信订单号。微信生成的订单号,在支付通知中有返回 + * @param out_refund_no 商户退款单号。商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。 + * @param total_fee 订单金额。订单总金额,单位为分,只能为整数 + * @param refund_fee 退款金额。退款总金额,订单总金额,单位为分,只能为整数 + * @param notify_url 退款结果通知url。异步接收微信支付退款结果通知的回调地址,通知URL必须为外网可访问的url,不允许带参数 如果参数中传了notify_url,则商户平台上配置的回调地址将不会生效。 + * @return + */ + public Map<String, String> wxRefund(String transaction_id, String out_refund_no, String total_fee, String refund_fee, String notify_url){ + int tf = new BigDecimal(total_fee).multiply(new BigDecimal("100")).intValue(); + int rf = new BigDecimal(refund_fee).multiply(new BigDecimal("100")).intValue(); + String nonce_str = UUID.randomUUID().toString(); + Map<String, Object> map = new HashMap<>(); + map.put("appid", appid); + map.put("mch_id", mchId); + map.put("nonce_str", nonce_str); + map.put("transaction_id", transaction_id); + map.put("out_refund_no", out_refund_no); + map.put("total_fee", tf); + map.put("refund_fee", rf); + map.put("notify_url", callbackPath + notify_url); + String s = this.weixinSignature(map, key); + map.put("sign", s); + + String url = "https://api.mch.weixin.qq.com/secapi/pay/refund"; + //设置请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_XML); + StringBuffer xmlString = new StringBuffer(); + Set<String> strings = map.keySet(); + String[] keys = {}; + keys = strings.toArray(keys); + Arrays.sort(keys); + xmlString.append("<xml>"); + for(int l = 0; l < keys.length; l++){ + xmlString.append("<" + keys[l] + ">" + map.get(keys[l]) + "</" + keys[l] + ">"); + } + xmlString.append("</xml>"); + + Map<String, String> map1 = null; + String body1 = null; + try { + body1 = sendHttpsRequestWithCert(url, xmlString.toString()); +// String certPath = "E:\\cert\\1523106371_20211206_cert\\apiclient_cert.p12"; +// +// body1 = httpClientUtil.pushHttpsRequsetXml(url, xmlString.toString(), new HashMap<>(), "1717539630", certPath, "PKCS12"); + + } catch (Exception e) { + System.err.println("微信退款请求失败:"+ e.getMessage()); + e.printStackTrace(); + } + System.err.println(body1); + //将结果xml解析成map + body1 = body1.replaceAll("<!\\[CDATA\\[",""); + body1 = body1.replaceAll("]]>", ""); + try { + map1 = this.xmlToMap(body1, "UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } catch (DocumentException e) { + e.printStackTrace(); + } + String return_code = map1.get("return_code"); + Map<String, String> map2 = new HashMap<>(); + if("SUCCESS".equals(return_code)){ + String result_code = map1.get("result_code"); + if("SUCCESS".equals(result_code)){ + map2.put("return_code", result_code); + map2.put("refund_id", String.valueOf(map1.get("refund_id")));//微信退款订单号 + map2.put("refund_fee", String.valueOf(map1.get("refund_fee")));//退款金额 + return map2; + }else{ + map2.put("return_code", result_code); + map2.put("return_msg", map1.get("err_code_des")); + return map2; + } + }else{ + map2.put("return_code", return_code); + map2.put("return_msg", map1.get("return_msg")); + return map2; + } + } + + /** + * 发送带商户证书的 HTTPS 请求 + */ + private String sendHttpsRequestWithCert(String url, String xmlBody) throws Exception { + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + try (FileInputStream fis = new FileInputStream(certPath)) { + keyStore.load(fis, mchId.toCharArray()); // 证书密码为商户号 + } + + // 初始化SSL上下文 + String keyManagerAlgorithm = KeyManagerFactory.getDefaultAlgorithm(); + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(keyManagerAlgorithm); + keyManagerFactory.init(keyStore, mchId.toCharArray()); + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init( + keyManagerFactory.getKeyManagers(), // 客户端密钥管理器(包含商户证书和私钥) + null, // 信任管理器(使用默认信任库) + new SecureRandom() // 安全随机数生成器 + ); + // 获取SSLSocketFactory + SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); + + // 发送请求(此处假设使用Hutool的HttpRequest,但需调整SSLSocketFactory设置) + // 注意:Hutool的HttpRequest仍可使用,但需传入原生的SSLSocketFactory + HttpResponse response = HttpRequest.post(url) + .setSSLSocketFactory(sslSocketFactory) // 传入原生SSLSocketFactory + .body(xmlBody) + .contentType("application/xml; charset=UTF-8") + .timeout(10000) + .execute(); + + + if (response.getStatus() != 200) { + throw new RuntimeException("HTTP 请求失败,状态码:" + response.getStatus()); + } + return response.body(); + } + + /** + * 微信退款成功后的回调处理 + * @param request + * @return + */ + public Map<String, String> wxRefundCallback(HttpServletRequest request){ + try { + String param = this.getParam(request); + param = param.replaceAll("<!\\[CDATA\\[",""); + param = param.replaceAll("]]>", ""); + Map<String, String> map = this.xmlToMap(param, "UTF-8"); + String return_code = map.get("return_code"); + if("SUCCESS".equals(return_code)){ + String req_info = map.get("req_info");//加密信息请用商户秘钥进行解密 + String s = this.wxDecrypt(req_info); + s = s.replaceAll("<!\\[CDATA\\[",""); + s = s.replaceAll("]]>", ""); + map = this.xmlToMap(s, "UTF-8"); + Map<String, String> map1 = new HashMap<>(); + map1.put("refund_id", map.get("refund_id")); + map1.put("out_refund_no", map.get("out_refund_no")); + String result = "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>"; + map1.put("result", result); + return map1; + }else{ +// System.err.println(map.get("return_msg")); + } + } catch (IOException e) { + e.printStackTrace(); + } catch (DocumentException e) { + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (InvalidKeyException e) { + e.printStackTrace(); + } catch (NoSuchPaddingException e) { + e.printStackTrace(); + } catch (BadPaddingException e) { + e.printStackTrace(); + } catch (NoSuchProviderException e) { + e.printStackTrace(); + } catch (IllegalBlockSizeException e) { + e.printStackTrace(); + } + return null; + } + + + /** + * 支付宝退款 + * @param trade_no 支付宝交易号 + * @param refund_amount 退款金额 + * @return + * @throws AlipayApiException + */ + public Map<String, String> aliRefund(String trade_no, String refund_amount) throws AlipayApiException { +// AlipayClient alipayClient = new DefaultAlipayClient("https://openapi.alipay.com/gateway.do", aliAppid, appPrivateKey,"json","UTF-8", alipay_public_key,"RSA2"); +// AlipayTradeRefundRequest request = new AlipayTradeRefundRequest(); +// JSONObject jsonObject = new JSONObject(); +// jsonObject.put("trade_no", trade_no); +// jsonObject.put("refund_amount", refund_amount); +// request.setBizContent(jsonObject.toJSONString()); +// AlipayTradeRefundResponse response = alipayClient.execute(request); +// Map<String, String> map = new HashMap<>(); +// if(response.isSuccess()){ +// System.out.println("调用成功"); +// String outTradeNo = response.getOutTradeNo(); +// map.put("code", response.getCode());//10000 +// map.put("trade_no", response.getTradeNo());//支付宝交易号 +// map.put("out_trade_no", outTradeNo);//商户订单号 +// } else { +// System.out.println("调用失败"); +// map.put("code", response.getCode()); +// map.put("msg", response.getSubMsg()); +// } +// return map; + + + //构造client + CertAlipayRequest certAlipayRequest = new CertAlipayRequest (); + //设置网关地址 + certAlipayRequest.setServerUrl("https://openapi.alipay.com/gateway.do"); + //设置应用Id + certAlipayRequest.setAppId(aliAppid); + //设置应用私钥 + certAlipayRequest.setPrivateKey(appPrivateKey); + //设置请求格式,固定值json + certAlipayRequest.setFormat("json"); + //设置字符集 + certAlipayRequest.setCharset("UTF-8"); + //设置签名类型 + certAlipayRequest.setSignType("RSA2"); + //设置应用公钥证书路径 + certAlipayRequest.setCertPath(app_cert_path); + //设置支付宝公钥证书路径 + certAlipayRequest.setAlipayPublicCertPath(alipay_cert_path); + //设置支付宝根证书路径 + certAlipayRequest.setRootCertPath(alipay_root_cert_path); + //构造client + AlipayClient alipayClient = null; + try { + alipayClient = new DefaultAlipayClient(certAlipayRequest); + } catch (AlipayApiException e) { + e.printStackTrace(); + } + //实例化具体API对应的request类,类名称和接口名称对应,当前调用接口名称:alipay.trade.app.pay + AlipayTradeRefundRequest request = new AlipayTradeRefundRequest (); + //SDK已经封装掉了公共参数,这里只需要传入业务参数。以下方法为sdk的model入参方式(model和biz_content同时存在的情况下取biz_content)。 + AlipayTradeRefundModel model = new AlipayTradeRefundModel (); + model.setTradeNo(trade_no); + model.setRefundAmount(refund_amount); + request.setBizModel ( model ); + try { + //这里和普通的接口调用不同,使用的是sdkExecute + AlipayTradeRefundResponse response = alipayClient.certificateExecute(request); + Map<String, String> map = new HashMap<>(); + if(response.isSuccess()){ + System.out.println("调用成功"); + String outTradeNo = response.getOutTradeNo(); + map.put("code", response.getCode());//10000 + map.put("trade_no", response.getTradeNo());//支付宝交易号 + map.put("out_trade_no", outTradeNo);//商户订单号 + } else { + System.out.println("调用失败"); + map.put("code", response.getCode()); + map.put("msg", response.getSubMsg()); + } + return map; + } catch (AlipayApiException e ) { + e.printStackTrace(); + } + return null; + } + + + /** + * 查询微信支付订单 + * @return + * @throws Exception + */ + public R queryWXOrder() throws Exception{ + String url = "https://api.mch.weixin.qq.com/pay/orderquery"; + String nonce_str = UUID.randomUUID().toString(); + Map<String, Object> map = new HashMap<>(); + map.put("appid", appid); + map.put("mch_id", mchId); + map.put("transaction_id", nonce_str);//微信订单号 + map.put("nonce_str", nonce_str);//随机字符串 + String s = this.weixinSignature(map); + map.put("sign", s); + + //设置请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_XML); + StringBuffer xmlString = new StringBuffer(); + Set<String> strings = map.keySet(); + String[] keys = {}; + keys = strings.toArray(keys); + Arrays.sort(keys); + xmlString.append("<xml>"); + for(int l = 0; l < keys.length; l++){ + xmlString.append("<" + keys[l] + ">" + map.get(keys[l]) + "</" + keys[l] + ">"); + } + xmlString.append("</xml>"); + + Map<String, String> map1 = null; +// String body1 = httpClientUtil.pushHttpRequsetXml(url, xmlString.toString(), new HashMap<>()); + String body1 = HttpRequest.post(url) + .body(xmlString.toString()) + .contentType("application/xml; charset=UTF-8") + .timeout(5000) // 设置超时时间 + .execute() + .body(); + //将结果xml解析成map + body1 = body1.replaceAll("<!\\[CDATA\\[",""); + body1 = body1.replaceAll("]]>", ""); + try { + map1 = this.xmlToMap(body1, "UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } catch (DocumentException e) { + e.printStackTrace(); + } + String return_code = map1.get("return_code"); + if("SUCCESS".equals(return_code)){ + String result_code = map1.get("result_code"); + if("SUCCESS".equals(result_code)){ + String type = map1.get("trade_type"); + switch (type){ + case "JSAPI": + break; + case "NATIVE": + String code_url = map1.get("code_url"); + return R.ok(code_url); + case "APP": + String trade_state = map1.get("trade_state"); + String time_end = map1.get("time_end"); + Map<String, Object> map2 = new HashMap<>(); + map2.put("trade_state", trade_state);//订单状态SUCCESS—支付成功,REFUND—转入退款,NOTPAY—未支付,CLOSED—已关闭,REVOKED—已撤销(刷卡支付),USERPAYING--用户支付中,PAYERROR--支付失败(其他原因,如银行返回失败) + map2.put("time_end", time_end);//订单支付时间,格式为yyyyMMddHHmmss,如2009年12月25日9点10分10秒表示为20091225091010。 + return R.ok(map2); + } + return null; + }else{ + System.err.println(map1.get("err_code_des")); + return R.fail(map1.get("err_code_des")); + } + }else{ + System.err.println(map1.get("return_msg") + appid + "----" + mchId); + return R.fail(map1.get("return_msg")); + } + } + + + + /** + * 微信转账功能(企业付款到零钱) + * @param openid 商户appid下,某用户的openid + * @param desc 企业付款备注,必填。 + * @param total_fee 企业付款金额 + * @param partner_trade_no 商户订单号,需保持唯一性 + * @return + */ + public Map<String, String> wxTransfers(String openid, String desc, String total_fee, String partner_trade_no) throws Exception{ + int amount = new BigDecimal(total_fee).multiply(new BigDecimal("100")).intValue(); + String nonce_str = UUID.randomUUID().toString(); + Map<String, Object> map = new HashMap<>(); + map.put("mch_appid", appid);//申请商户号的appid或商户号绑定的appid + map.put("mchid", mchId);//微信支付分配的商户号 + map.put("nonce_str", nonce_str);//随机字符串,不长于32位 + map.put("partner_trade_no", partner_trade_no);//商户订单号,需保持唯一性 + map.put("openid", openid);//商户appid下,某用户的openid + map.put("check_name", "NO_CHECK");//NO_CHECK:不校验真实姓名 FORCE_CHECK:强校验真实姓名 + map.put("amount", amount);//企业付款金额,单位为分 + map.put("desc", desc);//企业付款备注,必填。 + String s = this.weixinSignature(map, key); + map.put("sign", s); + + String url = "https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers"; + //设置请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_XML); + StringBuffer xmlString = new StringBuffer(); + Set<String> strings = map.keySet(); + String[] keys = {}; + keys = strings.toArray(keys); + Arrays.sort(keys); + xmlString.append("<xml>"); + for(int l = 0; l < keys.length; l++){ + xmlString.append("<" + keys[l] + ">" + map.get(keys[l]) + "</" + keys[l] + ">"); + } + xmlString.append("</xml>"); + + Map<String, String> map1 = null; + + String certPath = "C:\\cert\\1523106371_20211206_cert\\apiclient_cert.p12";//证书地址 + String body1 = sendHttpsRequestWithCert(url, xmlString.toString()); +// String body1 = httpClientUtil.pushHttpsRequsetXml(url, xmlString.toString(), new HashMap<>(), "1523106371", certPath, "PKCS12"); + //将结果xml解析成map + body1 = body1.replaceAll("<!\\[CDATA\\[",""); + body1 = body1.replaceAll("]]>", ""); + try { + map1 = this.xmlToMap(body1, "UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } catch (DocumentException e) { + e.printStackTrace(); + } + String return_code = map1.get("return_code"); + Map<String, String> map2 = new HashMap<>(); + if("SUCCESS".equals(return_code)){ + String result_code = map1.get("result_code"); + if("SUCCESS".equals(result_code)){ + map2.put("return_code", result_code); + map2.put("payment_no", String.valueOf(map1.get("payment_no")));//付款订单号 + map2.put("payment_time", String.valueOf(map1.get("payment_time")));//付款时间 + return map2; + }else{ + map2.put("return_code", result_code); + map2.put("err_code", map1.get("err_code")); + map2.put("err_code_des", map1.get("err_code_des")); + return map2; + } + }else{ + map2.put("return_code", return_code); + map2.put("return_msg", map1.get("return_msg")); + return map2; + } + } + + + /** + * 微信转账功能(企业付款到银行卡) + * @param desc 备注信息 + * @param total_fee 转账金额 + * @param partner_trade_no 订单号 + * @param enc_bank_no 银行卡号 + * @param enc_true_name 收款方用户名 + * @param bankName 银行名称 + * @return + * @throws Exception + */ + public Map<String, String> wxPayBank(String desc, String total_fee, String partner_trade_no, String enc_bank_no, String enc_true_name, String bankName) throws Exception{ + int amount = new BigDecimal(total_fee).multiply(new BigDecimal("100")).intValue(); + String nonce_str = UUID.randomUUID().toString(); + Map<String, Object> map = new HashMap<>(); + map.put("mch_id", mchId);//微信支付分配的商户号 + map.put("nonce_str", nonce_str);//随机字符串,不长于32位 + map.put("partner_trade_no", partner_trade_no);//商户订单号,需保持唯一性 + map.put("enc_bank_no", enc_bank_no);//收款方银行卡号(采用标准RSA算法,公钥由微信侧提供) + map.put("enc_true_name", enc_true_name);//收款方用户名(采用标准RSA算法,公钥由微信侧提供) + map.put("bank_code", findBankCode(bankName));// + map.put("amount", amount);//企业付款金额,单位为分 + map.put("desc", desc);//企业付款备注,必填。 + String s = this.weixinSignature(map, key); + map.put("sign", s); + + String url = "https://api.mch.weixin.qq.com/mmpaysptrans/pay_bank"; + //设置请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_XML); + StringBuffer xmlString = new StringBuffer(); + Set<String> strings = map.keySet(); + String[] keys = {}; + keys = strings.toArray(keys); + Arrays.sort(keys); + xmlString.append("<xml>"); + for(int l = 0; l < keys.length; l++){ + xmlString.append("<" + keys[l] + ">" + map.get(keys[l]) + "</" + keys[l] + ">"); + } + xmlString.append("</xml>"); + + Map<String, String> map1 = null; +// String certPath = "C:\\cert\\1523106371_20211206_cert\\apiclient_cert.p12";//证书地址 +// String body1 = httpClientUtil.pushHttpsRequsetXml(url, xmlString.toString(), new HashMap<>(), "1523106371", certPath, "PKCS12"); + String body1 = sendHttpsRequestWithCert(url, xmlString.toString()); + //将结果xml解析成map + body1 = body1.replaceAll("<!\\[CDATA\\[",""); + body1 = body1.replaceAll("]]>", ""); + try { + map1 = this.xmlToMap(body1, "UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } catch (DocumentException e) { + e.printStackTrace(); + } + String return_code = map1.get("return_code"); + Map<String, String> map2 = new HashMap<>(); + if("SUCCESS".equals(return_code)){ + String result_code = map1.get("result_code"); + if("SUCCESS".equals(result_code)){ + map2.put("return_code", result_code); + map2.put("payment_no", String.valueOf(map1.get("payment_no")));//付款订单号 + map2.put("cmms_amt", String.valueOf(map1.get("cmms_amt")));//手续费金额 RMB:分 + return map2; + }else{ + map2.put("return_code", result_code); + map2.put("err_code", map1.get("err_code")); + map2.put("err_code_des", map1.get("err_code_des")); + return map2; + } + }else{ + map2.put("return_code", return_code); + map2.put("return_msg", map1.get("return_msg")); + return map2; + } + } + + /** + * 微信转账到银行卡不编号 + * @param bankName + * @return + */ + public String findBankCode(String bankName){ + String json = "{\"工商银行 \":1002,\"农业银行\":1005,\"建设银行\":1003,\"中国银行\":1026,\"交通银行 \":1020,\"招商银行 \":1001,\"邮储银行\":1066,\"民生银行 \":1006,\"平安银行 \":1010,\"中信银行\":1021,\"浦发银行 \":1004,\"兴业银行 \":1009,\"光大银行 \":1022,\"广发银行\":1027,\"华夏银行\":1025,\"宁波银行\":1056,\"北京银行\":4836,\"上海银行\":1024,\"南京银行\":1054,\"长子县融汇村镇银行\":4755,\"长沙银行\":4216,\"浙江泰隆商业银行\":4051,\"中原银行 \":4753,\"企业银行(中国)\":4761,\"顺德农商银行 \":4036,\"衡水银行\":4752,\"长治银行\":4756,\"大同银行\":4767,\"河南省农村信用社\":4115,\"宁夏黄河农村商业银行\":4150,\"山西省农村信用社\":4156,\"安徽省农村信用社\":4166,\"甘肃省农村信用社\":4157,\"天津农村商业银行\":4153,\"广西壮族自治区农村信用社\":4113,\"陕西省农村信用社\":4108,\"深圳农村商业银行\":4076,\"宁波鄞州农村商业银行\":4052,\"浙江省农村信用社联合社\":4764,\"江苏省农村信用社联合社\":4217,\"江苏紫金农村商业银行股份有限公司 \":4072,\"北京中关村银行股份有限公司 \":4769,\"星展银行( 中国) 有限公司 \":4778,\"枣庄银行股份有限公司 \":4766,\"海口联合农村商业银行股份有限公司 \":4758,\"南洋商业银行( 中国) 有限公司 \":4763}"; + JSONObject jsonObject = JSON.parseObject(json); + Set<String> strings = jsonObject.keySet(); + for(String key : strings){ + if(key.indexOf(bankName) >= 0){ + return jsonObject.getString(key); + } + } + return ""; + } + + + + /** + * 支付宝转账 + * @param out_biz_no 商家侧唯一订单号,由商家自定义。对于不同转账请求,商家需保证该订单号在自身系统唯一。 + * @param trans_amount 订单总金额,单位为元,精确到小数点后两位 + * @param order_title 转账业务的标题,用于在支付宝用户的账单里显示 + * @param identity 参与方的唯一标识(收款方支付宝账号) + * @param name 参与方真实姓名,如果非空,将校验收款支付宝账号姓名一致性。 + * @param remark 业务备注 + * @return + * @throws Exception + */ + public Map<String, Object> aliTransfer(String out_biz_no, Double trans_amount, String order_title, String identity, String name, String remark) throws Exception{ + CertAlipayRequest certAlipayRequest = new CertAlipayRequest(); + certAlipayRequest.setServerUrl("https://openapi.alipay.com/gateway.do"); //gateway:支付宝网关(固定)https://openapi.alipay.com/gateway.do + certAlipayRequest.setAppId(aliAppid); //APPID 即创建应用后生成,详情见创建应用并获取 APPID + certAlipayRequest.setPrivateKey(appPrivateKey); //开发者应用私钥,由开发者自己生成 + certAlipayRequest.setFormat("json"); //参数返回格式,只支持 json 格式 + certAlipayRequest.setCharset("UTF-8"); //请求和签名使用的字符编码格式,支持 GBK和 UTF-8 + certAlipayRequest.setSignType("RSA2"); //商户生成签名字符串所使用的签名算法类型,目前支持 RSA2 和 RSA,推荐商家使用 RSA2。 + certAlipayRequest.setCertPath(app_cert_path); //应用公钥证书路径(app_cert_path 文件绝对路径) + certAlipayRequest.setAlipayPublicCertPath(alipay_cert_path); //支付宝公钥证书文件路径(alipay_cert_path 文件绝对路径) + certAlipayRequest.setRootCertPath(alipay_root_cert_path); //支付宝CA根证书文件路径(alipay_root_cert_path 文件绝对路径) + AlipayClient alipayClient = new DefaultAlipayClient(certAlipayRequest); + AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest(); + request.setBizContent("{" + + "\"out_biz_no\":\"" + out_biz_no + "\"," + + "\"trans_amount\":" + trans_amount + "," + + "\"product_code\":\"TRANS_ACCOUNT_NO_PWD\"," + + "\"biz_scene\":\"DIRECT_TRANSFER\"," + + "\"order_title\":\"" + order_title + "\"," + + "\"payee_info\":{" + + "\"identity\":\"" + identity + "\"," + + "\"identity_type\":\"ALIPAY_USER_ID\"," + + "\"name\":\"" + name + "\"," + + "}," + + "\"remark\":\"" + remark + "\"" + + "}"); + AlipayFundTransUniTransferResponse response = alipayClient.certificateExecute(request); + Map<String, Object> map = new HashMap<>(); + if(response.isSuccess()){ + String status = response.getStatus(); + if(status.equals("SUCCESS")){//成功 + map.put("code", response.getCode()); + map.put("order_id", response.getOrderId());//支付宝订单号 + map.put("pay_fund_order_id", response.getPayFundOrderId());//支付宝流水号 + }else{ + map.put("code", response.getCode()); + map.put("sub_msg", response.getSubMsg()); + } + } else { + map.put("code", response.getSubCode()); + map.put("sub_msg", response.getSubMsg()); + } + return map; + } + + + /** + * 获取请求内容 + * @param request + * @return + * @throws IOException + */ + private String getParam(HttpServletRequest request) throws IOException { + // 读取参数 + InputStream inputStream; + StringBuilder sb = new StringBuilder(); + inputStream = request.getInputStream(); + String s; + BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); + while ((s = in.readLine()) != null) { + sb.append(s); + } + in.close(); + inputStream.close(); + return sb.toString(); + } + + + /** + * 微信下单的签名算法 + * @param map + * @return + */ + private String weixinSignature(Map<String, Object> map){ + try { + Set<Map.Entry<String, Object>> entries = map.entrySet(); + List<Map.Entry<String, Object>> infoIds = new ArrayList<Map.Entry<String, Object>>(entries); + // 对所有传入参数按照字段名的 ASCII 码从小到大排序(字典序) + Collections.sort(infoIds, new Comparator<Map.Entry<String, Object>>() { + public int compare(Map.Entry<String, Object> o1, Map.Entry<String, Object> o2) { + return (o1.getKey()).toString().compareTo(o2.getKey()); + } + }); + // 构造签名键值对的格式 + StringBuilder sb = new StringBuilder(); + for (Map.Entry<String, Object> item : infoIds) { + if (item.getKey() != null || item.getKey() != "") { + String key = item.getKey(); + Object val = item.getValue(); + if (!(val == "" || val == null)) { + sb.append(key + "=" + val + "&"); + } + } + } + sb.append("key=" + key); + String sign = MD5AndKL.MD5Encode(sb.toString(), "UTF-8").toUpperCase(); //注:MD5签名方式 + return sign; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + + /** + * 微信下单的签名算法 + * @param map + * @return + */ + private String weixinSignature(Map<String, Object> map, String key_){ + try { + Set<Map.Entry<String, Object>> entries = map.entrySet(); + List<Map.Entry<String, Object>> infoIds = new ArrayList<Map.Entry<String, Object>>(entries); + // 对所有传入参数按照字段名的 ASCII 码从小到大排序(字典序) + Collections.sort(infoIds, new Comparator<Map.Entry<String, Object>>() { + public int compare(Map.Entry<String, Object> o1, Map.Entry<String, Object> o2) { + return (o1.getKey()).toString().compareTo(o2.getKey()); + } + }); + // 构造签名键值对的格式 + StringBuilder sb = new StringBuilder(); + for (Map.Entry<String, Object> item : infoIds) { + if (item.getKey() != null || item.getKey() != "") { + String key = item.getKey(); + Object val = item.getValue(); + if (!(val == "" || val == null)) { + sb.append(key + "=" + val + "&"); + } + } + } + sb.append("key=" + key_); + String sign = MD5AndKL.MD5Encode(sb.toString(), "UTF-8").toUpperCase(); //注:MD5签名方式 + return sign; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + + /** + * 微信退款成功后的解密 + * @param req_info + * @return + */ + private String wxDecrypt(String req_info) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, + InvalidKeyException, BadPaddingException, IllegalBlockSizeException { + byte[] decode = Base64.getDecoder().decode(req_info); + String sign = MD5AndKL.MD5Encode(key, "UTF-8").toLowerCase(); + if (Security.getProvider("BC") == null){ + Security.addProvider(new BouncyCastleProvider()); + } + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS7Padding", "BC"); + SecretKeySpec secretKeySpec = new SecretKeySpec(sign.getBytes(), "AES"); + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec); + return new String(cipher.doFinal(decode)); + } + + + public static void main(String[] ages){ +// PayMoneyUtil payMoneyUtil = new PayMoneyUtil(); +// payMoneyUtil.weixinpay("测试", "123", "12.5", ""); + } + + + /** + * xml转map + * @param xml + * @param charset + * @return + * @throws UnsupportedEncodingException + * @throws DocumentException + */ + public static Map<String, String> xmlToMap(String xml, String charset) throws UnsupportedEncodingException, DocumentException { + + Map<String, String> respMap = new HashMap<String, String>(); + + SAXReader reader = new SAXReader(); + Document doc = reader.read(new ByteArrayInputStream(xml.getBytes(charset))); + Element root = doc.getRootElement(); + xmlToMap(root, respMap); + return respMap; + } + + public static Map<String, String> xmlToMap(Element tmpElement, Map<String, String> respMap){ + if (tmpElement.isTextOnly()) { + respMap.put(tmpElement.getName(), tmpElement.getText()); + return respMap; + } + + @SuppressWarnings("unchecked") + Iterator<Element> eItor = tmpElement.elementIterator(); + while (eItor.hasNext()) { + Element element = eItor.next(); + xmlToMap(element, respMap); + } + return respMap; + } +} diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/HttpUtil.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/HttpUtil.java new file mode 100644 index 0000000..19ca6ed --- /dev/null +++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/HttpUtil.java @@ -0,0 +1,98 @@ +package com.ruoyi.order.util.payment.wx; + +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; + +import javax.net.ssl.*; +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * HTTP工具类 + */ +public class HttpUtil { + + /** + * 发送POST请求 + */ + public static String post(String urlStr, String data) throws Exception { + // 设置超时时间(单位:毫秒) + int timeout = 5000; // 5秒 + + // 发送 POST 请求 + try (HttpResponse response = HttpRequest.post(urlStr) + .body(data, "application/xml") // 设置 XML 请求体 + .timeout(timeout) + .execute()) { + + // 检查 HTTP 状态码 + if (!response.isOk()) { + throw new RuntimeException("HTTP请求失败,状态码: " + response.getStatus() + + ", 响应: " + response.body()); + } + return response.body(); + } + } + + /** + * 发送HTTPS请求 + */ + public static String postHttps(String urlStr, String data, String certPath, String certPassword) throws Exception { + // 创建SSL上下文 + SSLContext sslContext = SSLContext.getInstance("SSL"); + TrustManager[] trustManagers = {new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + }}; + sslContext.init(null, trustManagers, new java.security.SecureRandom()); + + // 创建HTTPS连接 + URL url = new URL(urlStr); + HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); + conn.setSSLSocketFactory(sslContext.getSocketFactory()); + conn.setHostnameVerifier(new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession session) { + return true; + } + }); + + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + conn.setDoInput(true); + conn.setUseCaches(false); + conn.setRequestProperty("Content-Type", "application/xml"); + conn.setRequestProperty("Connection", "Keep-Alive"); + conn.setRequestProperty("Charset", "UTF-8"); + + // 发送请求 + OutputStream os = conn.getOutputStream(); + os.write(data.getBytes("UTF-8")); + os.flush(); + os.close(); + + // 获取响应 + StringBuilder result = new StringBuilder(); + BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8")); + String line; + while ((line = br.readLine()) != null) { + result.append(line); + } + br.close(); + + return result.toString(); + } +} diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/WechatPayConfig.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/WechatPayConfig.java new file mode 100644 index 0000000..b1a1cd1 --- /dev/null +++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/WechatPayConfig.java @@ -0,0 +1,26 @@ +package com.ruoyi.order.util.payment.wx; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 微信支付配置类 + */ +@Data +@Component +@ConfigurationProperties(prefix = "wx") +public class WechatPayConfig { + // 小程序APPID + private String appId; + // 商户号 + private String mchId; + // 商户API密钥 + private String key; + // 支付结果通知地址 + private String callbackPath; + // 证书路径 + private String certPath; + // 商户RAS加密公钥路径 + private String RASPath; +} diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/WechatPayService.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/WechatPayService.java new file mode 100644 index 0000000..d8ef90b --- /dev/null +++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/WechatPayService.java @@ -0,0 +1,742 @@ +package com.ruoyi.order.util.payment.wx; + + +import com.alibaba.fastjson2.JSON; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.order.util.payment.MD5AndKL; +import com.ruoyi.order.util.payment.wx.vo.PayResult; +import com.ruoyi.order.util.payment.wx.vo.RefundCallbackResult; +import org.apache.commons.codec.digest.DigestUtils; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.crypto.engines.AESEngine; +import org.bouncycastle.crypto.paddings.PKCS7Padding; +import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.dom4j.DocumentException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import sun.misc.BASE64Decoder; +import sun.security.util.DerInputStream; +import sun.security.util.DerValue; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.URL; +import java.net.URLEncoder; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.*; +import java.security.spec.RSAPublicKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 微信支付服务类 + */ +@Service +public class WechatPayService { + + @Autowired + private WechatPayConfig wechatPayConfig; + private static final String RSA_PUBLIC_KEY_FILENAME = "wechat_rsa_public_key.pem"; + private static final String CERT_FOLDER = "cert/"; + /** + * 统一下单 + * @param orderNumber 订单号 + * @param totalFee 总金额(分) + * @param body 商品描述 + * @param openid 用户openid + * @return 预支付订单信息 + */ + public R unifiedOrder(String orderId,String orderNumber, String totalFee, String body, String openid, String callbackPath) throws Exception { + int i = new BigDecimal(totalFee).multiply(new BigDecimal("100")).intValue(); + String hostAddress = null; + try { + hostAddress = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + // 构建请求参数 + Map<String, String> params = new HashMap<>(); + params.put("appid", wechatPayConfig.getAppId()); + params.put("mch_id", wechatPayConfig.getMchId()); + params.put("nonce_str", generateNonceStr()); + params.put("body", body); + params.put("out_trade_no", orderNumber); + params.put("total_fee", String.valueOf(i) ); + params.put("spbill_create_ip", "221.182.45.100"); // 实际应用中应获取客户端IP + params.put("notify_url", wechatPayConfig.getCallbackPath()+callbackPath); + params.put("trade_type", "JSAPI"); + params.put("openid", openid); + + // 生成签名 + String sign = weixinSignature(params); + params.put("sign", sign); + + // 将参数转换为XML + String xmlParams = XMLUtil.mapToXml(params).replaceFirst("^<\\?xml.+?\\?>\\s*", ""); + + // 发送请求到微信支付统一下单接口 + String url = "https://api.mch.weixin.qq.com/pay/unifiedorder"; + String result = HttpUtil.post(url, xmlParams); + + // 解析返回结果 + Map<String, String> resultMap = XMLUtil.xmlToMap(result); + + // 验证签名 + if (!verifySign(resultMap, wechatPayConfig.getKey())) { + return R.fail("微信支付签名验证失败"); +// throw new Exception("微信支付签名验证失败"); + } + if (!resultMap.get("return_code").equals("SUCCESS")) { + return R.fail(resultMap.get("return_msg")); + } + + // 构建小程序支付所需参数 + Map<String, String> payParams = new HashMap<>(); + payParams.put("appId", wechatPayConfig.getAppId()); + payParams.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000)); + payParams.put("nonceStr", generateNonceStr()); + payParams.put("package", "prepay_id=" + resultMap.get("prepay_id")); + payParams.put("signType", "MD5"); + + // 生成支付签名 + String paySign = weixinSignature(payParams); + payParams.put("paySign", paySign); + + //给前端标识 + payParams.put("payMethod","1"); + payParams.put("orderId", orderId); + return R.ok(JSON.toJSONString(payParams)); + } + /** + * 微信下单的签名算法 + * @param map + * @return + */ + private String weixinSignature(Map<String, String> map){ + try { + Set<Map.Entry<String, String>> entries = map.entrySet(); + List<Map.Entry<String, String>> infoIds = new ArrayList<Map.Entry<String, String>>(entries); + // 对所有传入参数按照字段名的 ASCII 码从小到大排序(字典序) + Collections.sort(infoIds, new Comparator<Map.Entry<String, String>>() { + public int compare(Map.Entry<String, String> o1, Map.Entry<String, String> o2) { + return (o1.getKey()).toString().compareTo(o2.getKey()); + } + }); + // 构造签名键值对的格式 + StringBuilder sb = new StringBuilder(); + for (Map.Entry<String, String> item : infoIds) { + if (item.getKey() != null || item.getKey() != "") { + String key = item.getKey(); + Object val = item.getValue(); + if (!(val == "" || val == null)) { + sb.append(key + "=" + val + "&"); + } + } + } + sb.append("key=" + wechatPayConfig.getKey()); + String sign = MD5AndKL.MD5Encode(sb.toString(), "UTF-8").toUpperCase(); //注:MD5签名方式 + return sign; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + /** + * 处理支付结果通知 + * @param request HTTP请求 + * @return 处理结果 + */ + public PayResult processNotify(HttpServletRequest request) throws Exception { + // 读取请求内容 + BufferedReader br = new BufferedReader(new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8)); + String line; + StringBuilder xml = new StringBuilder(); + while ((line = br.readLine()) != null) { + xml.append(line); + } + br.close(); + + // 解析XML + Map<String, String> resultMap = XMLUtil.xmlToMap(xml.toString()); + + // 验证签名 + if (!verifySign(resultMap, wechatPayConfig.getKey())) { + throw new Exception("微信支付签名验证失败"); + } + + // 验证支付结果 + if (!"SUCCESS".equals(resultMap.get("return_code")) || !"SUCCESS".equals(resultMap.get("result_code"))) { + throw new Exception("微信支付结果异常"); + } + + // 处理业务逻辑 + PayResult payResult = new PayResult(); + payResult.setOrderNumber(resultMap.get("out_trade_no")); + payResult.setTransactionId(resultMap.get("transaction_id")); + payResult.setTotalFee(resultMap.get("total_fee")); + + return payResult; + } + + /** + * 生成随机字符串 + */ + private String generateNonceStr() { + return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32); + } + + /** + * 微信支付API V2签名算法 + * @param params 请求参数 + * @param apiKey 商户API密钥 + * @return 签名结果 + */ + public static String generateSign(Map<String, String> params, String apiKey) throws Exception { + // 1. 过滤空值参数 + Map<String, String> filteredParams = new HashMap<>(); + for (Map.Entry<String, String> entry : params.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + // 排除sign字段和空值字段 + if (!"sign".equals(key) && value != null && !value.isEmpty()) { + filteredParams.put(key, value); + } + } + + // 2. 按照ASCII码排序参数名 + List<String> keys = new ArrayList<>(filteredParams.keySet()); + Collections.sort(keys); + + // 3. 构建签名原始字符串 + StringBuilder sb = new StringBuilder(); + for (String key : keys) { + String value = filteredParams.get(key); + sb.append(key).append("=").append(value).append("&"); + } + + // 4. 添加API密钥 + sb.append("key=").append(apiKey); + + // 5. MD5加密并转为大写 + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] digest = md.digest(sb.toString().getBytes("UTF-8")); + + // 6. 转换为十六进制字符串 + StringBuilder sign = new StringBuilder(); + for (byte b : digest) { + sign.append(String.format("%02x", b & 0xff)); + } + + return sign.toString().toUpperCase(); + } + /** + * 验证签名 + */ + private boolean verifySign(Map<String, String> params, String key) throws Exception { + String sign = params.get("sign"); + if (StringUtils.isEmpty(sign)) { + return false; + } + + // 移除sign字段 + Map<String, String> newParams = new HashMap<>(params); + newParams.remove("sign"); + + // 生成新签名 + String newSign = generateSign(newParams, key); + + return sign.equals(newSign); + } + + /** + * 关闭订单 + */ + public Map<String, String> closeOrder(String orderId){ + // 构建请求参数 + Map<String, String> params = new HashMap<>(); + params.put("appid", wechatPayConfig.getAppId()); + params.put("mch_id", wechatPayConfig.getMchId()); + params.put("nonce_str", generateNonceStr()); + params.put("out_trade_no", orderId); + + // 生成签名 + String sign = weixinSignature(params); + params.put("sign", sign); + + // 将参数转换为XML + String xmlParams = XMLUtil.mapToXml(params); + + // 发送请求到微信支付关闭订单接口 + String url = "https://api.mch.weixin.qq.com/pay/closeorder"; + String result = null; + try { + result = HttpUtil.post(url, xmlParams); + } catch (Exception e) { + throw new RuntimeException(e); + } + + // 解析返回结果 + try { + return XMLUtil.xmlToMap(result); + } catch (DocumentException e) { + throw new RuntimeException(e); + } + } + + /** + * 查询订单 + */ + public Map<String, String> queryOrder(String orderId) throws Exception { + // 构建请求参数 + Map<String, String> params = new HashMap<>(); + params.put("appid", wechatPayConfig.getAppId()); + params.put("mch_id", wechatPayConfig.getMchId()); + params.put("nonce_str", generateNonceStr()); + params.put("out_trade_no", orderId); + + // 生成签名 + String sign = generateSign(params, wechatPayConfig.getKey()); + params.put("sign", sign); + + // 将参数转换为XML + String xmlParams = XMLUtil.mapToXml(params); + + // 发送请求到微信支付查询订单接口 + String url = "https://api.mch.weixin.qq.com/pay/orderquery"; + String result = HttpUtil.post(url, xmlParams); + + // 解析返回结果 + return XMLUtil.xmlToMap(result); + } + + + /** + * 申请退款 - 使用证书 + */ + public Map<String, String> refund(String orderNo, String refundNo, String totalFee, String refundFee, String refundDesc,String callbackPath) { + int i = new BigDecimal(totalFee).multiply(new BigDecimal("100")).intValue(); + int j = new BigDecimal(refundFee).multiply(new BigDecimal("100")).intValue(); + try { + // 构建请求参数 + Map<String, String> params = new HashMap<>(); + params.put("appid", wechatPayConfig.getAppId()); + params.put("mch_id", wechatPayConfig.getMchId()); + params.put("nonce_str", UUID.randomUUID().toString().replaceAll("-", "")); + params.put("out_trade_no", orderNo); + params.put("out_refund_no", refundNo); + params.put("total_fee", String.valueOf(i)); + params.put("refund_fee", String.valueOf(j)); + params.put("refund_desc", refundDesc); + params.put("notify_url", wechatPayConfig.getCallbackPath() + callbackPath); // 退款结果 + + // 生成签名 + String sign = weixinSignature(params); + params.put("sign", sign); + + // 转换为XML + String xmlParams = XMLUtil.mapToXml(params); + + // 使用证书发送请求 + String result = postWithCert("https://api.mch.weixin.qq.com/secapi/pay/refund", xmlParams); + + // 解析结果 + Map<String, String> resultMap = XMLUtil.xmlToMap(result); + System.out.println("申请退款结果"+resultMap); + + // 验证签名 + if (!verifySign(resultMap, wechatPayConfig.getKey())) { + resultMap.put("return_code","FAILED"); + resultMap.put("return_msg","申请退款结果签名验证失败"); + return resultMap; + } + + return resultMap; + } catch (Exception e) { + Map<String, String> resultMap=new HashMap<>(); + resultMap.put("return_code","FAILED"); + resultMap.put("return_msg","申请退款失败"); + return resultMap; + } + } + + /** + * 使用证书发送请求 + */ + private String postWithCert(String url, String xmlData) throws Exception { + // 证书类型为PKCS12 + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + // 获取证书路径 + String certPath = wechatPayConfig.getCertPath(); + + // 如果是classpath路径,使用ClassLoader加载 + if (certPath.startsWith("classpath:")) { + String path = certPath.substring("classpath:".length()); + try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(path)) { + if (inputStream == null) { + throw new FileNotFoundException("证书文件不存在: " + path); + } + keyStore.load(inputStream, wechatPayConfig.getMchId().toCharArray()); + } + } else { + // 传统文件路径 + try (FileInputStream inputStream = new FileInputStream(new File(certPath))) { + keyStore.load(inputStream, wechatPayConfig.getMchId().toCharArray()); + } + } + + // 实例化密钥库 & 初始化密钥工厂 + SSLContext sslContext = SSLContext.getInstance("TLS"); + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, wechatPayConfig.getMchId().toCharArray()); + sslContext.init(kmf.getKeyManagers(), null, new SecureRandom()); + + // 创建HttpsURLConnection对象 + URL httpsUrl = new URL(url); + HttpsURLConnection conn = (HttpsURLConnection) httpsUrl.openConnection(); + conn.setSSLSocketFactory(sslContext.getSocketFactory()); + conn.setDoOutput(true); + conn.setDoInput(true); + conn.setUseCaches(false); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + + // 发送请求 + OutputStream outputStream = conn.getOutputStream(); + outputStream.write(xmlData.getBytes("UTF-8")); + outputStream.flush(); + outputStream.close(); + + // 获取响应 + InputStream inputStream = conn.getInputStream(); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); + StringBuilder result = new StringBuilder(); + String line; + while ((line = bufferedReader.readLine()) != null) { + result.append(line); + } + bufferedReader.close(); + inputStream.close(); + conn.disconnect(); + + return result.toString(); + } + + /** + * 处理退款回调 + */ + public RefundCallbackResult processRefundCallback(String xmlData) { + try { + // 1. 解析回调XML数据 + if (StringUtils.isEmpty(xmlData)) { + return RefundCallbackResult.fail("回调数据为空"); + } + + //2.解析参数 + System.out.println(xmlData); + System.out.println("----------------------------------------"); + Map<String, String> resultMap = XMLUtil.xmlToMap(xmlData); + System.out.println(resultMap.get("req_info")); + // 3. 检查返回状态 + String returnCode = resultMap.get("return_code"); + + if (!"SUCCESS".equals(returnCode)) { + String errMsg = resultMap.get("return_msg"); + return RefundCallbackResult.fail("通信失败:" + errMsg); + } + + //4 使用商户API密钥解密req_info(AES-256-CBC算法) + String decryptData = decrypt(resultMap.get("req_info"), wechatPayConfig.getKey()); + Map<String, String> refundDetail = XMLUtil.xmlToMap(decryptData); + + + // 4. 提取退款信息 + String orderNo = refundDetail.get("out_trade_no"); // 原订单号 + String refundNo = refundDetail.get("out_refund_no"); // 退款订单号 + String refundId = refundDetail.get("refund_id"); // 微信退款ID + System.err.println("退款回调成功,订单号:"+orderNo+",退款号:"+refundNo+",状态:{}"+refundId); + RefundCallbackResult refundCallbackResult = RefundCallbackResult.success(); + refundCallbackResult.setOrderNo(orderNo); + refundCallbackResult.setRefundNo(refundId); + + return refundCallbackResult; + + } catch (Exception e) { + return RefundCallbackResult.fail("系统异常:" + e.getMessage()); + } + } + + private static String wxDecrypt(String req_info, String key) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, + InvalidKeyException, BadPaddingException, IllegalBlockSizeException{ + byte[] decode = Base64.getDecoder().decode(req_info); + System.out.println(Arrays.toString(decode)); + String sign = MD5AndKL.MD5Encode(key, "UTF-8").toLowerCase(); + System.out.println(sign); + if (Security.getProvider("BC") == null){ + Security.addProvider(new BouncyCastleProvider()); + } + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS7Padding", "BC"); + SecretKeySpec secretKeySpec = new SecretKeySpec(sign.getBytes(), "AES"); + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec); + return new String(cipher.doFinal(decode)); + } + + /** + * 获取RSA加密公钥 + */ + public String getRsaPublicKey() { + int maxRetries = 3; + for (int retryCount = 0; retryCount < maxRetries; retryCount++) { + try { + System.out.println("尝试获取RSA公钥,第" + (retryCount + 1) + "次"); + + // 构建请求参数 + Map<String, String> params = new HashMap<>(); + params.put("mch_id", wechatPayConfig.getMchId()); + params.put("sign_type", "MD5"); + params.put("nonce_str", generateNonceStr()); + + // 生成签名 + String sign = weixinSignature(params); + params.put("sign", sign); + + // 转换为XML + String xmlParams = XMLUtil.mapToXml(params); + + // 打印请求参数 + System.out.println("请求参数: " + xmlParams); + + // 使用证书发送请求(关键修改:使用postWithCert而非普通HTTP请求) + String result = postWithCert("https://fraud.mch.weixin.qq.com/risk/getpublickey", xmlParams); + + // 打印响应结果 + System.out.println("响应结果: " + result); + + // 解析结果 + Map<String, String> resultMap = XMLUtil.xmlToMap(result); + System.out.println("获取RSA公钥结果: " + resultMap); + + // 检查返回状态 + if (!"SUCCESS".equals(resultMap.get("return_code"))) { + throw new Exception("RSA公钥获取失败: " + resultMap.get("return_msg")); + } + + // 保存公钥到本地文件 + savePublicKeyToClasspath(resultMap.get("pub_key")); + + return resultMap.get("pub_key"); + } catch (Exception e) { + System.err.println("获取RSA公钥异常: " + e.getMessage() + ", 重试次数: " + (retryCount + 1)); + e.printStackTrace(); + + // 如果是最后一次重试,抛出异常 + if (retryCount == maxRetries - 1) { + return null; + } + + // 重试前等待一段时间 + try { + Thread.sleep(1000 * (retryCount + 1)); // 指数退避 + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + return null; + } + } + } + + return null; + } + + /** + * 保存RSA公钥到文件夹 + * @param publicKey RSA公钥内容 + */ + private static void savePublicKeyToClasspath(String publicKey) throws IOException { + // 创建 cert 目录(如果不存在) + Path certDir = Paths.get(CERT_FOLDER); + if (!Files.exists(certDir)) { + Files.createDirectories(certDir); // 自动创建目录 + } + + // 写入公钥文件(UTF-8 编码) + Path keyFile = certDir.resolve(RSA_PUBLIC_KEY_FILENAME); + Files.write(keyFile, publicKey.getBytes(StandardCharsets.UTF_8)); + } + /** + * 从文件夹加载RSA公钥 + * @return RSA公钥内容 + */ + private static String loadPublicKeyFromClasspath() throws IOException { + Path keyFile = Paths.get(CERT_FOLDER + RSA_PUBLIC_KEY_FILENAME); + byte[] bytes = Files.readAllBytes(keyFile); // 读取所有字节 + return new String(bytes, StandardCharsets.UTF_8); // 转为UTF-8字符串 + } + + /** + * 加载公钥 返回PublicKey对象 + */ + public static PublicKey loadPublicKey(String pemContent) throws Exception { + // 读取PEM文件内容 +// String pemContent = new String(Files.readAllBytes(Paths.get(CERT_FOLDER + RSA_PUBLIC_KEY_FILENAME)), StandardCharsets.UTF_8); + + // 移除PEM头尾标记 + String publicKeyPEM = pemContent + .replace("-----BEGIN RSA PUBLIC KEY-----", "") + .replace("-----END RSA PUBLIC KEY-----", "") + .replaceAll("\\s", ""); // 去除换行/空格 + + // 解码Base64 + byte[] encoded = Base64.getDecoder().decode(publicKeyPEM); + + // 手动解析PKCS#1格式 + DerInputStream derReader = new DerInputStream(encoded); + DerValue[] seq = derReader.getSequence(0); + BigInteger modulus = seq[0].getBigInteger(); + BigInteger exponent = seq[1].getBigInteger(); + + RSAPublicKeySpec keySpec = new RSAPublicKeySpec(modulus, exponent); + return KeyFactory.getInstance("RSA").generatePublic(keySpec); + } + + /** + * 使用RSA-OAEP加密数据 + * @param plaintext 待加密的明文 + * @return Base64编码的密文 + */ + public static String encrypt(String plaintext, PublicKey publicKey) throws Exception { + Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWITHSHA-1ANDMGF1PADDING"); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + + byte[] encryptedBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(encryptedBytes); + } + + + + /** + * 商户付款到银行卡(优先使用本地保存的公钥) + * @param partnerTradeNo 商户订单号 + * @param bankNo 银行卡号 + * @param trueName 银行卡真实姓名 + * @param bankCode 银行编码 + * @param amount 金额(分) + * @param desc 付款说明 + * @return 付款结果 + */ + public Map<String, String> payToBankCard(String partnerTradeNo, String bankNo, String trueName, + String bankCode, BigDecimal amount, String desc) throws Exception { + int i = amount.multiply(new BigDecimal("100")).intValue(); + // 1. 尝试从本地加载RSA公钥 + String pubKey = loadPublicKeyFromClasspath(); + + // 2. 如果本地没有公钥或公钥无效,则从微信获取新公钥 + if (pubKey == null || pubKey.isEmpty()) { + pubKey = getRsaPublicKey(); + + } + //公钥对象 + PublicKey publicKey = loadPublicKey(pubKey); + + // 3. 使用RSA公钥加密银行卡号和真实姓名 + String encryptedBankNo = encrypt(bankNo, publicKey); + String encryptedTrueName = encrypt(trueName, publicKey); + + // 4. 构建请求参数 + Map<String, String> params = new HashMap<>(); + params.put("mch_id", wechatPayConfig.getMchId()); + params.put("partner_trade_no", partnerTradeNo); + params.put("enc_bank_no", encryptedBankNo); + params.put("enc_true_name", encryptedTrueName); + params.put("bank_code", bankCode); + params.put("amount", String.valueOf(i)); + params.put("desc", desc); + params.put("nonce_str", generateNonceStr()); + + // 生成签名 + String sign = weixinSignature(params); + params.put("sign", sign); + + // 将参数转换为XML + String xmlParams = XMLUtil.mapToXml(params); + + // 5. 发送请求到微信支付付款到银行卡接口 + String url = "https://api.mch.weixin.qq.com/mmpaysptrans/pay_bank"; + String result = postWithCert(url, xmlParams); + + // 解析返回结果 + Map<String, String> resultMap = XMLUtil.xmlToMap(result); + + // 验证签名 + if (!verifySign(resultMap, wechatPayConfig.getKey())) { + throw new Exception("付款到银行卡签名验证失败"); + } + + return resultMap; + } + + public static void main(String[] args) throws IOException { + + String info="CjlaS7RVnPn7zzP5ByZDxUN7OrXGp1/DEdO0qahpIqDH/gTNHb/U7VmrVV0S4lXrIa0N8FEREC3CdIeT4XB5P4D0E8TSURu6J/cD01hFu28f0JDRfeips3vSpTgznRGyCfnUBDPYwyrVeP29Wac7WAb3CCcJf7OZWaweOUkaKjaBRa1GzMZcguSZnQJz0cD5Jb4HbTMvM0VAebfCY9aXdfFBIbm+cPYESo3awqwkNTQeT4V+FViw8f8sjkH0TScMgWBiSKmQC837BLD27yIGklqlYkDP2IMeiNw+b12qCAGszfp2vYd3X+HpViXkQQet3PJWYlAm55R+IgvschP7Ub65XzLINfQrJKrQUXiKKO2LwoSRSwZvfDkR8G8E8X59CnU2XvWKeos5Y0q8ckbJb97yI+09nNgMjYyJoVCVjTFgFMDEQ4+e3CpYRhD6V/3RBp+TvBwszldbRav2XEuCXL2kCJyJEAqLPMNyfYBSNF8z1btjyz0+y/xQQcySKlQInZ710FxSE7KwRSBQ92j9nDdlR7UxCrPVCkEd+GrVNSqqnyjNh1J/rPJPHvvGwkPPq72TKiw6ZgaIgIDhy0/lWHTclo4sjYAWuUVfg3CJ8dqkuQwVZ7i0+NiahIl78RtcUph8NR48yUgBkN7WhCcu5wLbg2tu8Qe0SIwHF+RW1x9Yc8akEkNbMd4xzs8lY5MYEU9V16U8RyWJuwPDph3RnmV8HQ+2hfzmjCvPkBwtfR8P5VdK86OIsHfnfQxAcPM2a86tOBBzFXPrLHgd2CRcDKH+MXTw7RSH/bk1PiMUAWF8TQsNDzgUlznJnkjiQxoym/4ZUf4C6072KKQHbp6bgBYkBhJLT2lmjVMNSX5b1SXM9eTQixRfq6MKGw3P8XJnKdofktVv+KtSzWQlW0C8p504NWACiExupF5EII7FG+xCWt7urWUbc4NRI36UFrKToQCLVv6UBCXt/t9iWlvs6SfuZhpCexeMmZWeiIldzRu87U9rXR46Hu7DAL8dZ+0ItsIZYThSIABzZgaLKggXlkjyAcbcPYKO7egrCmDtFhzHuh4uA3VeBylL3/ZLZ4FUedn/8L4e2iAu22Qj46ORlu17W5R8Ez9kubydeAgC9PkWnjptaubPxE0bjPN69tec"; + + String key="fD0JzscfMf295SYtRK3MnPRjSCA4Gahr"; + try { + String decrypted = decrypt(info, key); + System.out.println("解密结果: " + decrypted); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + public static String decrypt(String encryptedStringA, String merchantKey) throws Exception { + // 1. 对加密串A做base64解码,得到加密串B + byte[] decode = Base64.getDecoder().decode(encryptedStringA); + + // 2. 对商户key做md5,得到32位小写key* + String sign = MD5AndKL.MD5Encode(merchantKey, "UTF-8").toLowerCase(); + + // 3. 确保BouncyCastle提供者已添加 + if (Security.getProvider("BC") == null) { + Security.addProvider(new BouncyCastleProvider()); + } + + // 4. 使用AES-256-ECB解密(PKCS7Padding) + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS7Padding", "BC"); + + // 注意:微信要求使用AES-256,所以密钥应为32字节(256位) + // 如果MD5结果是32字节(256位),直接使用 + byte[] aesKey = sign.getBytes(StandardCharsets.UTF_8); + + SecretKeySpec secretKeySpec = new SecretKeySpec(aesKey, "AES"); + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec); + + // 执行解密并指定UTF-8编码 + byte[] decryptedBytes = cipher.doFinal(decode); + return new String(decryptedBytes, StandardCharsets.UTF_8); + } + + +} diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/XMLUtil.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/XMLUtil.java new file mode 100644 index 0000000..68e6585 --- /dev/null +++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/XMLUtil.java @@ -0,0 +1,52 @@ +package com.ruoyi.order.util.payment.wx; + +import org.dom4j.Document; +import org.dom4j.DocumentException; +import org.dom4j.DocumentHelper; +import org.dom4j.Element; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * XML工具类 + */ +public class XMLUtil { + + /** + * 将Map转换为XML字符串 + */ + public static String mapToXml(Map<String, String> params) { + Document document = DocumentHelper.createDocument(); + // 禁用XML声明 + document.setXMLEncoding(null); // 关键设置 + + Element root = document.addElement("xml"); + for (Map.Entry<String, String> entry : params.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue() != null ? entry.getValue() : ""; + root.addElement(key).setText(value); + } + + return document.asXML(); + } + + /** + * 将XML字符串转换为Map + */ + public static Map<String, String> xmlToMap(String xmlStr) throws DocumentException { + Map<String, String> map = new HashMap<>(); + Document document = DocumentHelper.parseText(xmlStr); + Element root = document.getRootElement(); + + for (Iterator<?> iterator = root.elementIterator(); iterator.hasNext();) { + Element element = (Element) iterator.next(); + // 关键修改:获取元素内所有内容(包括CDATA) + String value = element.getStringValue(); // 自动处理CDATA + map.put(element.getName(), value); + } + + return map; + } +} diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/vo/PayResult.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/vo/PayResult.java new file mode 100644 index 0000000..5d0b826 --- /dev/null +++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/vo/PayResult.java @@ -0,0 +1,16 @@ +package com.ruoyi.order.util.payment.wx.vo; + +import lombok.Data; + +/** + * 支付结果VO + */ +@Data +public class PayResult { + // 订单ID + private String orderNumber; + // 微信交易ID + private String transactionId; + // 支付金额 + private String totalFee; +} diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/vo/RefundCallbackResult.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/vo/RefundCallbackResult.java new file mode 100644 index 0000000..4e8df35 --- /dev/null +++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/vo/RefundCallbackResult.java @@ -0,0 +1,38 @@ +package com.ruoyi.order.util.payment.wx.vo; + +import lombok.Data; + +/** + * 退款回调结果实体 + */ +@Data +public class RefundCallbackResult { + private boolean success; // 处理是否成功 + private String msg; // 结果描述 + private String orderNo; // 原订单号 + private String refundNo; // 退款订单号 + private String refundId; // 微信退款ID + private String totalFee; // 原订单金额(分) + private String refundFee; // 退款金额(分) + private String refundStatus; // 退款状态 + + // 成功响应 + public static RefundCallbackResult success() { + return success("处理成功"); + } + + public static RefundCallbackResult success(String msg) { + RefundCallbackResult result = new RefundCallbackResult(); + result.setSuccess(true); + result.setMsg(msg); + return result; + } + + // 失败响应 + public static RefundCallbackResult fail(String msg) { + RefundCallbackResult result = new RefundCallbackResult(); + result.setSuccess(false); + result.setMsg(msg); + return result; + } +} \ No newline at end of file diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/vo/UnifiedOrderResult.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/vo/UnifiedOrderResult.java new file mode 100644 index 0000000..85785c3 --- /dev/null +++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/util/payment/wx/vo/UnifiedOrderResult.java @@ -0,0 +1,38 @@ +package com.ruoyi.order.util.payment.wx.vo; + +import lombok.Data; + +/** + * 统一下单结果VO + */ +@Data +public class UnifiedOrderResult { + // 返回状态码 + private String returnCode; + // 返回信息 + private String returnMsg; + // 业务结果 + private String resultCode; + // 错误代码 + private String errCode; + // 错误代码描述 + private String errCodeDes; + // 公众账号ID + private String appid; + // 商户号 + private String mchId; + // 设备号 + private String deviceInfo; + // 随机字符串 + private String nonceStr; + // 签名 + private String sign; + // 签名类型 + private String signType; + // 交易类型 + private String tradeType; + // 预支付交易会话标识 + private String prepayId; + // 二维码链接 + private String codeUrl; +} diff --git a/ruoyi-service/ruoyi-other/pom.xml b/ruoyi-service/ruoyi-other/pom.xml index e51a5ae..18947f6 100644 --- a/ruoyi-service/ruoyi-other/pom.xml +++ b/ruoyi-service/ruoyi-other/pom.xml @@ -142,6 +142,23 @@ <artifactId>geodesy</artifactId> <version>1.1.3</version> </dependency> + + <!--微信支付--> + <dependency> + <groupId>cn.hutool</groupId> + <artifactId>hutool-http</artifactId> + <version>5.8.25</version> + </dependency> + <dependency> + <groupId>org.bouncycastle</groupId> + <artifactId>bcprov-jdk15on</artifactId> + <version>1.70</version> + </dependency> + <dependency> + <groupId>dom4j</groupId> + <artifactId>dom4j</artifactId> + <version>1.6.1</version> + </dependency> </dependencies> <build> diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/ShopWithdrawController.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/ShopWithdrawController.java index b5f7d13..e98cf90 100644 --- a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/ShopWithdrawController.java +++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/ShopWithdrawController.java @@ -26,6 +26,7 @@ import com.ruoyi.other.util.payment.model.SinglePay; import com.ruoyi.other.util.payment.model.SinglePayCallbackResult; import com.ruoyi.other.util.payment.model.SinglePayResult; +import com.ruoyi.other.util.payment.wx.WechatPayService; import com.ruoyi.system.api.domain.SysUser; import com.ruoyi.system.api.feignClient.SysUserClient; import com.ruoyi.system.api.model.LoginUser; @@ -37,11 +38,13 @@ import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; +import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -75,6 +78,9 @@ private SysUserClient sysUserClient; @Autowired private ShopWithdrawMapper shopWithdrawMapper; + + @Autowired + private WechatPayService wechatPayService; @GetMapping("/getShopById") @@ -269,33 +275,30 @@ Shop shop = shopService.getById(shopWithdraw1.getShopId()); BigDecimal money = shopWithdraw1.getMoney(); if(1 == shopWithdraw.getAuditStatus()){ - //通过 - // 先检查账户余额是否充足 todo 商户编号 - AccountBalanceQueryResult accountBalanceQueryResult = TransferUtil.accountBalanceQuery(); - if(null == accountBalanceQueryResult){ - return R.fail("查询账户余额出错"); - } - Double useAbleSettAmount = accountBalanceQueryResult.getUseAbleSettAmount(); - if(useAbleSettAmount < (shopWithdraw1.getMoney().doubleValue() + 1)){ - return R.fail("账户可用余额不足,请先补充账户余额"); - } - //银行卡转账 - SinglePay singlePay = new SinglePay(); - singlePay.setTradeMerchantNo(TransferUtil.sysTradeMerchantNo); - singlePay.setMerchantOrderNo(shopWithdraw1.getId().toString()); - singlePay.setReceiverAccountNoEnc(shop.getReceiverAccountNoEnc()); - singlePay.setReceiverNameEnc(shop.getReceiverNameEnc()); - singlePay.setReceiverAccountType(shop.getReceiverAccountType()); - singlePay.setReceiverBankChannelNo(shop.getReceiverBankChannelNo()); - singlePay.setPaidAmount(shopWithdraw1.getMoney().doubleValue()); - singlePay.setPaidDesc("账户余额提现"); - singlePay.setPaidUse("208"); - singlePay.setCallbackUrl("/other/shop-withdraw/withdrawalCallback"); - SinglePayResult singlePayResult = TransferUtil.singlePay(singlePay); - if(null == singlePayResult){ - return R.fail("转账失败"); - } - shopWithdraw1.setStatus(1); + //取消商户转账,线下转账 + /* + try { + Map<String, String> map = wechatPayService.payToBankCard(shopWithdraw1.getId().toString(), + shopWithdraw1.getReceiverAccountNoEnc(),shopWithdraw1.getReceiverNameEnc(),shopWithdraw1.getReceiverBankChannelNo(),shopWithdraw1.getMoney(),shopWithdraw.getRemark()); + if (map.get("return_code").equals("SUCCESS")) { + System.out.println("转账申请成功"); + shopWithdraw1.setStatus(1); + }else { + return R.fail("转账申请失败"); + } + } catch (Exception e) { + throw new RuntimeException(e); + }*/ + //到账 + + shopWithdraw1.setStatus(2); + shopWithdraw1.setArrivalTime(LocalDateTime.now()); + shopWithdrawService.updateById(shopWithdraw); + //更新店铺审核中的金额,和提现金额 + shop.setWithdrawAuditMoney(shop.getWithdrawAuditMoney().subtract(shopWithdraw1.getMoney()));//审核中金额 + shop.setWithdrawMoney(shop.getWithdrawMoney().add(shopWithdraw1.getMoney()));//提现金额 + shopService.updateById(shop); + } if(2 == shopWithdraw.getAuditStatus()){ //审核不通过 @@ -330,8 +333,7 @@ return R.ok(); } - - + /** * 提现审核通过后转账回调通知 * @param singlePayCallbackResult diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/ShopServiceImpl.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/ShopServiceImpl.java index cf664fb..968a5ea 100644 --- a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/ShopServiceImpl.java +++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/ShopServiceImpl.java @@ -338,10 +338,12 @@ //商品评价 GoodsEvaluate goodsEvaluateOne = goodsEvaluateMapper.getGoodsEvaluateOne(goods.getId()); - AppUser appUserById = appUserClient.getAppUserById(goodsEvaluateOne.getAppUserId()); - goodsEvaluateOne.setUserName(appUserById.getName()); - goodsEvaluateOne.setAvatar(appUserById.getAvatar()); - goodsVO.setGoodsEvaluate(goodsEvaluateOne); + if(goodsEvaluateOne != null){ + AppUser appUserById = appUserClient.getAppUserById(goodsEvaluateOne.getAppUserId()); + goodsEvaluateOne.setUserName(appUserById.getName()); + goodsEvaluateOne.setAvatar(appUserById.getAvatar()); + goodsVO.setGoodsEvaluate(goodsEvaluateOne); + } return goodsVO; } diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/payment/wx/HttpUtil.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/payment/wx/HttpUtil.java new file mode 100644 index 0000000..4c2310b --- /dev/null +++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/payment/wx/HttpUtil.java @@ -0,0 +1,99 @@ +package com.ruoyi.other.util.payment.wx; + +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; + +import javax.net.ssl.*; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.URL; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * HTTP工具类 + */ +public class HttpUtil { + + /** + * 发送POST请求 + */ + public static String post(String urlStr, String data) throws Exception { + // 设置超时时间(单位:毫秒) + int timeout = 5000; // 5秒 + + // 发送 POST 请求 + try (HttpResponse response = HttpRequest.post(urlStr) + .body(data, "application/xml") // 设置 XML 请求体 + .timeout(timeout) + .execute()) { + + // 检查 HTTP 状态码 + if (!response.isOk()) { + throw new RuntimeException("HTTP请求失败,状态码: " + response.getStatus() + + ", 响应: " + response.body()); + } + return response.body(); + } + } + + /** + * 发送HTTPS请求 + */ + public static String postHttps(String urlStr, String data, String certPath, String certPassword) throws Exception { + // 创建SSL上下文 + SSLContext sslContext = SSLContext.getInstance("SSL"); + TrustManager[] trustManagers = {new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + }}; + sslContext.init(null, trustManagers, new java.security.SecureRandom()); + + // 创建HTTPS连接 + URL url = new URL(urlStr); + HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); + conn.setSSLSocketFactory(sslContext.getSocketFactory()); + conn.setHostnameVerifier(new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession session) { + return true; + } + }); + + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + conn.setDoInput(true); + conn.setUseCaches(false); + conn.setRequestProperty("Content-Type", "application/xml"); + conn.setRequestProperty("Connection", "Keep-Alive"); + conn.setRequestProperty("Charset", "UTF-8"); + + // 发送请求 + OutputStream os = conn.getOutputStream(); + os.write(data.getBytes("UTF-8")); + os.flush(); + os.close(); + + // 获取响应 + StringBuilder result = new StringBuilder(); + BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8")); + String line; + while ((line = br.readLine()) != null) { + result.append(line); + } + br.close(); + + return result.toString(); + } +} diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/payment/wx/PayResult.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/payment/wx/PayResult.java new file mode 100644 index 0000000..244626c --- /dev/null +++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/payment/wx/PayResult.java @@ -0,0 +1,16 @@ +package com.ruoyi.other.util.payment.wx; + +import lombok.Data; + +/** + * 支付结果VO + */ +@Data +public class PayResult { + // 订单ID + private String orderNumber; + // 微信交易ID + private String transactionId; + // 支付金额 + private String totalFee; +} diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/payment/wx/RefundCallbackResult.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/payment/wx/RefundCallbackResult.java new file mode 100644 index 0000000..6c5eb20 --- /dev/null +++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/payment/wx/RefundCallbackResult.java @@ -0,0 +1,38 @@ +package com.ruoyi.other.util.payment.wx; + +import lombok.Data; + +/** + * 退款回调结果实体 + */ +@Data +public class RefundCallbackResult { + private boolean success; // 处理是否成功 + private String msg; // 结果描述 + private String orderNo; // 原订单号 + private String refundNo; // 退款订单号 + private String refundId; // 微信退款ID + private String totalFee; // 原订单金额(分) + private String refundFee; // 退款金额(分) + private String refundStatus; // 退款状态 + + // 成功响应 + public static RefundCallbackResult success() { + return success("处理成功"); + } + + public static RefundCallbackResult success(String msg) { + RefundCallbackResult result = new RefundCallbackResult(); + result.setSuccess(true); + result.setMsg(msg); + return result; + } + + // 失败响应 + public static RefundCallbackResult fail(String msg) { + RefundCallbackResult result = new RefundCallbackResult(); + result.setSuccess(false); + result.setMsg(msg); + return result; + } +} \ No newline at end of file diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/payment/wx/WechatPayConfig.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/payment/wx/WechatPayConfig.java new file mode 100644 index 0000000..3fbd68c --- /dev/null +++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/payment/wx/WechatPayConfig.java @@ -0,0 +1,26 @@ +package com.ruoyi.other.util.payment.wx; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 微信支付配置类 + */ +@Data +@Component +@ConfigurationProperties(prefix = "wx") +public class WechatPayConfig { + // 小程序APPID + private String appId; + // 商户号 + private String mchId; + // 商户API密钥 + private String key; + // 支付结果通知地址 + private String callbackPath; + // 证书路径 + private String certPath; + // 商户RAS加密公钥路径 + private String RASPath; +} diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/payment/wx/WechatPayService.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/payment/wx/WechatPayService.java new file mode 100644 index 0000000..dbf26be --- /dev/null +++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/payment/wx/WechatPayService.java @@ -0,0 +1,734 @@ +package com.ruoyi.other.util.payment.wx; + + +import com.alibaba.fastjson2.JSON; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.other.util.payment.MD5AndKL; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.dom4j.DocumentException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import sun.security.util.DerInputStream; +import sun.security.util.DerValue; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.SecretKeySpec; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.servlet.http.HttpServletRequest; +import java.io.*; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.URL; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.*; +import java.security.spec.RSAPublicKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.*; + +/** + * 微信支付服务类 + */ +@Service +public class WechatPayService { + + @Autowired + private WechatPayConfig wechatPayConfig; + private static final String RSA_PUBLIC_KEY_FILENAME = "wechat_rsa_public_key.pem"; + private static final String CERT_FOLDER = "C:\\cert\\"; + /** + * 统一下单 + * @param orderNumber 订单号 + * @param totalFee 总金额(分) + * @param body 商品描述 + * @param openid 用户openid + * @return 预支付订单信息 + */ + public R unifiedOrder(String orderNumber, String totalFee, String body, String openid, String callbackPath) throws Exception { + int i = new BigDecimal(totalFee).multiply(new BigDecimal("100")).intValue(); + String hostAddress = null; + try { + hostAddress = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + // 构建请求参数 + Map<String, String> params = new HashMap<>(); + params.put("appid", wechatPayConfig.getAppId()); + params.put("mch_id", wechatPayConfig.getMchId()); + params.put("nonce_str", generateNonceStr()); + params.put("body", body); + params.put("out_trade_no", orderNumber); + params.put("total_fee", String.valueOf(i) ); + params.put("spbill_create_ip", "221.182.45.100"); // 实际应用中应获取客户端IP + params.put("notify_url", wechatPayConfig.getCallbackPath()+callbackPath); + params.put("trade_type", "JSAPI"); + params.put("openid", openid); + + // 生成签名 + String sign = weixinSignature(params); + params.put("sign", sign); + + // 将参数转换为XML + String xmlParams = XMLUtil.mapToXml(params).replaceFirst("^<\\?xml.+?\\?>\\s*", ""); + + // 发送请求到微信支付统一下单接口 + String url = "https://api.mch.weixin.qq.com/pay/unifiedorder"; + String result = HttpUtil.post(url, xmlParams); + + // 解析返回结果 + Map<String, String> resultMap = XMLUtil.xmlToMap(result); + + // 验证签名 + if (!verifySign(resultMap, wechatPayConfig.getKey())) { + return R.fail("微信支付签名验证失败"); +// throw new Exception("微信支付签名验证失败"); + } + + // 构建小程序支付所需参数 + Map<String, String> payParams = new HashMap<>(); + payParams.put("appId", wechatPayConfig.getAppId()); + payParams.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000)); + payParams.put("nonceStr", generateNonceStr()); + payParams.put("package", "prepay_id=" + resultMap.get("prepay_id")); + payParams.put("signType", "MD5"); + + // 生成支付签名 + String paySign = weixinSignature(payParams); + payParams.put("paySign", paySign); + + //给前端标识 + payParams.put("payMethod","1"); + return R.ok(JSON.toJSONString(payParams)); + } + /** + * 微信下单的签名算法 + * @param map + * @return + */ + private String weixinSignature(Map<String, String> map){ + try { + Set<Map.Entry<String, String>> entries = map.entrySet(); + List<Map.Entry<String, String>> infoIds = new ArrayList<Map.Entry<String, String>>(entries); + // 对所有传入参数按照字段名的 ASCII 码从小到大排序(字典序) + Collections.sort(infoIds, new Comparator<Map.Entry<String, String>>() { + public int compare(Map.Entry<String, String> o1, Map.Entry<String, String> o2) { + return (o1.getKey()).toString().compareTo(o2.getKey()); + } + }); + // 构造签名键值对的格式 + StringBuilder sb = new StringBuilder(); + for (Map.Entry<String, String> item : infoIds) { + if (item.getKey() != null || item.getKey() != "") { + String key = item.getKey(); + Object val = item.getValue(); + if (!(val == "" || val == null)) { + sb.append(key + "=" + val + "&"); + } + } + } + sb.append("key=" + wechatPayConfig.getKey()); + String sign = MD5AndKL.MD5Encode(sb.toString(), "UTF-8").toUpperCase(); //注:MD5签名方式 + return sign; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + /** + * 处理支付结果通知 + * @param request HTTP请求 + * @return 处理结果 + */ + public PayResult processNotify(HttpServletRequest request) throws Exception { + // 读取请求内容 + BufferedReader br = new BufferedReader(new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8)); + String line; + StringBuilder xml = new StringBuilder(); + while ((line = br.readLine()) != null) { + xml.append(line); + } + br.close(); + + // 解析XML + Map<String, String> resultMap = XMLUtil.xmlToMap(xml.toString()); + + // 验证签名 + if (!verifySign(resultMap, wechatPayConfig.getKey())) { + throw new Exception("微信支付签名验证失败"); + } + + // 验证支付结果 + if (!"SUCCESS".equals(resultMap.get("return_code")) || !"SUCCESS".equals(resultMap.get("result_code"))) { + throw new Exception("微信支付结果异常"); + } + + // 处理业务逻辑 + PayResult payResult = new PayResult(); + payResult.setOrderNumber(resultMap.get("out_trade_no")); + payResult.setTransactionId(resultMap.get("transaction_id")); + payResult.setTotalFee(resultMap.get("total_fee")); + + return payResult; + } + + /** + * 生成随机字符串 + */ + private String generateNonceStr() { + return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32); + } + + /** + * 微信支付API V2签名算法 + * @param params 请求参数 + * @param apiKey 商户API密钥 + * @return 签名结果 + */ + public static String generateSign(Map<String, String> params, String apiKey) throws Exception { + // 1. 过滤空值参数 + Map<String, String> filteredParams = new HashMap<>(); + for (Map.Entry<String, String> entry : params.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + // 排除sign字段和空值字段 + if (!"sign".equals(key) && value != null && !value.isEmpty()) { + filteredParams.put(key, value); + } + } + + // 2. 按照ASCII码排序参数名 + List<String> keys = new ArrayList<>(filteredParams.keySet()); + Collections.sort(keys); + + // 3. 构建签名原始字符串 + StringBuilder sb = new StringBuilder(); + for (String key : keys) { + String value = filteredParams.get(key); + sb.append(key).append("=").append(value).append("&"); + } + + // 4. 添加API密钥 + sb.append("key=").append(apiKey); + + // 5. MD5加密并转为大写 + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] digest = md.digest(sb.toString().getBytes("UTF-8")); + + // 6. 转换为十六进制字符串 + StringBuilder sign = new StringBuilder(); + for (byte b : digest) { + sign.append(String.format("%02x", b & 0xff)); + } + + return sign.toString().toUpperCase(); + } + /** + * 验证签名 + */ + private boolean verifySign(Map<String, String> params, String key) throws Exception { + String sign = params.get("sign"); + if (StringUtils.isEmpty(sign)) { + return false; + } + + // 移除sign字段 + Map<String, String> newParams = new HashMap<>(params); + newParams.remove("sign"); + + // 生成新签名 + String newSign = generateSign(newParams, key); + + return sign.equals(newSign); + } + + /** + * 关闭订单 + */ + public Map<String, String> closeOrder(String orderId){ + // 构建请求参数 + Map<String, String> params = new HashMap<>(); + params.put("appid", wechatPayConfig.getAppId()); + params.put("mch_id", wechatPayConfig.getMchId()); + params.put("nonce_str", generateNonceStr()); + params.put("out_trade_no", orderId); + + // 生成签名 + String sign = weixinSignature(params); + params.put("sign", sign); + + // 将参数转换为XML + String xmlParams = XMLUtil.mapToXml(params); + + // 发送请求到微信支付关闭订单接口 + String url = "https://api.mch.weixin.qq.com/pay/closeorder"; + String result = null; + try { + result = HttpUtil.post(url, xmlParams); + } catch (Exception e) { + throw new RuntimeException(e); + } + + // 解析返回结果 + try { + return XMLUtil.xmlToMap(result); + } catch (DocumentException e) { + throw new RuntimeException(e); + } + } + + /** + * 查询订单 + */ + public Map<String, String> queryOrder(String orderId) throws Exception { + // 构建请求参数 + Map<String, String> params = new HashMap<>(); + params.put("appid", wechatPayConfig.getAppId()); + params.put("mch_id", wechatPayConfig.getMchId()); + params.put("nonce_str", generateNonceStr()); + params.put("out_trade_no", orderId); + + // 生成签名 + String sign = generateSign(params, wechatPayConfig.getKey()); + params.put("sign", sign); + + // 将参数转换为XML + String xmlParams = XMLUtil.mapToXml(params); + + // 发送请求到微信支付查询订单接口 + String url = "https://api.mch.weixin.qq.com/pay/orderquery"; + String result = HttpUtil.post(url, xmlParams); + + // 解析返回结果 + return XMLUtil.xmlToMap(result); + } + + + /** + * 申请退款 - 使用证书 + */ + public Map<String, String> refund(String orderNo, String refundNo, String totalFee, String refundFee, String refundDesc,String callbackPath) { + int i = new BigDecimal(totalFee).multiply(new BigDecimal("100")).intValue(); + int j = new BigDecimal(refundFee).multiply(new BigDecimal("100")).intValue(); + try { + // 构建请求参数 + Map<String, String> params = new HashMap<>(); + params.put("appid", wechatPayConfig.getAppId()); + params.put("mch_id", wechatPayConfig.getMchId()); + params.put("nonce_str", UUID.randomUUID().toString().replaceAll("-", "")); + params.put("out_trade_no", orderNo); + params.put("out_refund_no", refundNo); + params.put("total_fee", String.valueOf(i)); + params.put("refund_fee", String.valueOf(j)); + params.put("refund_desc", refundDesc); + params.put("notify_url", wechatPayConfig.getCallbackPath() + callbackPath); // 退款结果 + + // 生成签名 + String sign = weixinSignature(params); + params.put("sign", sign); + + // 转换为XML + String xmlParams = XMLUtil.mapToXml(params); + + // 使用证书发送请求 + String result = postWithCert("https://api.mch.weixin.qq.com/secapi/pay/refund", xmlParams); + + // 解析结果 + Map<String, String> resultMap = XMLUtil.xmlToMap(result); + System.out.println("申请退款结果"+resultMap); + + // 验证签名 + if (!verifySign(resultMap, wechatPayConfig.getKey())) { + resultMap.put("return_code","FAILED"); + resultMap.put("return_msg","申请退款结果签名验证失败"); + return resultMap; + } + + return resultMap; + } catch (Exception e) { + Map<String, String> resultMap=new HashMap<>(); + resultMap.put("return_code","FAILED"); + resultMap.put("return_msg","申请退款失败"); + return resultMap; + } + } + + /** + * 使用证书发送请求 + */ + private String postWithCert(String url, String xmlData) throws Exception { + // 证书类型为PKCS12 + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + // 获取证书路径 + String certPath = wechatPayConfig.getCertPath(); + + // 如果是classpath路径,使用ClassLoader加载 + if (certPath.startsWith("classpath:")) { + String path = certPath.substring("classpath:".length()); + try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(path)) { + if (inputStream == null) { + throw new FileNotFoundException("证书文件不存在: " + path); + } + keyStore.load(inputStream, wechatPayConfig.getMchId().toCharArray()); + } + } else { + // 传统文件路径 + try (FileInputStream inputStream = new FileInputStream(new File(certPath))) { + keyStore.load(inputStream, wechatPayConfig.getMchId().toCharArray()); + } + } + + // 实例化密钥库 & 初始化密钥工厂 + SSLContext sslContext = SSLContext.getInstance("TLS"); + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, wechatPayConfig.getMchId().toCharArray()); + sslContext.init(kmf.getKeyManagers(), null, new SecureRandom()); + + // 创建HttpsURLConnection对象 + URL httpsUrl = new URL(url); + HttpsURLConnection conn = (HttpsURLConnection) httpsUrl.openConnection(); + conn.setSSLSocketFactory(sslContext.getSocketFactory()); + conn.setDoOutput(true); + conn.setDoInput(true); + conn.setUseCaches(false); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + + // 发送请求 + OutputStream outputStream = conn.getOutputStream(); + outputStream.write(xmlData.getBytes("UTF-8")); + outputStream.flush(); + outputStream.close(); + + // 获取响应 + InputStream inputStream = conn.getInputStream(); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); + StringBuilder result = new StringBuilder(); + String line; + while ((line = bufferedReader.readLine()) != null) { + result.append(line); + } + bufferedReader.close(); + inputStream.close(); + conn.disconnect(); + + return result.toString(); + } + + /** + * 处理退款回调 + */ + public RefundCallbackResult processRefundCallback(String xmlData) { + try { + // 1. 解析回调XML数据 + if (StringUtils.isEmpty(xmlData)) { +// logger.error("退款回调数据为空"); + return RefundCallbackResult.fail("回调数据为空"); + } + + //2.解析参数 + System.err.println(xmlData); + System.out.println("----------------------------------------"); + Map<String, String> resultMap = XMLUtil.xmlToMap(xmlData); + System.out.println(resultMap.get("req_info")); + // 3. 检查返回状态 + String returnCode = resultMap.get("return_code"); + + if (!"SUCCESS".equals(returnCode)) { + String errMsg = resultMap.get("return_msg"); + return RefundCallbackResult.fail("通信失败:" + errMsg); + } + + //4 使用商户API密钥解密req_info(AES-256-CBC算法) + String decryptData = wxDecrypt(resultMap.get("req_info"), wechatPayConfig.getKey()); + Map<String, String> refundDetail = XMLUtil.xmlToMap(decryptData); + + + // 4. 提取退款信息 + String orderNo = refundDetail.get("out_trade_no"); // 原订单号 + String refundNo = refundDetail.get("out_refund_no"); // 退款订单号 + String refundId = refundDetail.get("refund_id"); // 微信退款ID + System.err.println("退款回调成功,订单号:"+orderNo+",退款号:"+refundNo+",状态:{}"+refundId); + RefundCallbackResult refundCallbackResult = RefundCallbackResult.success(); + refundCallbackResult.setOrderNo(orderNo); + refundCallbackResult.setRefundNo(refundId); + + return refundCallbackResult; + + } catch (Exception e) { + return RefundCallbackResult.fail("系统异常:" + e.getMessage()); + } + } + + private static String wxDecrypt(String req_info, String key) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, + InvalidKeyException, BadPaddingException, IllegalBlockSizeException{ + byte[] decode = Base64.getDecoder().decode(req_info); + System.out.println(Arrays.toString(decode)); + String sign = MD5AndKL.MD5Encode(key, "UTF-8").toLowerCase(); + System.out.println(sign); + if (Security.getProvider("BC") == null){ + Security.addProvider(new BouncyCastleProvider()); + } + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS7Padding", "BC"); + SecretKeySpec secretKeySpec = new SecretKeySpec(sign.getBytes(), "AES"); + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec); + return new String(cipher.doFinal(decode)); + } + + /** + * 获取RSA加密公钥 + */ + public String getRsaPublicKey() { + int maxRetries = 3; + for (int retryCount = 0; retryCount < maxRetries; retryCount++) { + try { + System.out.println("尝试获取RSA公钥,第" + (retryCount + 1) + "次"); + + // 构建请求参数 + Map<String, String> params = new HashMap<>(); + params.put("mch_id", wechatPayConfig.getMchId()); + params.put("sign_type", "MD5"); + params.put("nonce_str", generateNonceStr()); + + // 生成签名 + String sign = weixinSignature(params); + params.put("sign", sign); + + // 转换为XML + String xmlParams = XMLUtil.mapToXml(params); + + // 打印请求参数 + System.out.println("请求参数: " + xmlParams); + + // 使用证书发送请求(关键修改:使用postWithCert而非普通HTTP请求) + String result = postWithCert("https://fraud.mch.weixin.qq.com/risk/getpublickey", xmlParams); + + // 打印响应结果 + System.out.println("响应结果: " + result); + + // 解析结果 + Map<String, String> resultMap = XMLUtil.xmlToMap(result); + System.out.println("获取RSA公钥结果: " + resultMap); + + // 检查返回状态 + if (!"SUCCESS".equals(resultMap.get("return_code"))) { + throw new Exception("RSA公钥获取失败: " + resultMap.get("return_msg")); + } + + // 保存公钥到本地文件 + savePublicKeyToClasspath(resultMap.get("pub_key")); + + return resultMap.get("pub_key"); + } catch (Exception e) { + System.err.println("获取RSA公钥异常: " + e.getMessage() + ", 重试次数: " + (retryCount + 1)); + e.printStackTrace(); + + // 如果是最后一次重试,抛出异常 + if (retryCount == maxRetries - 1) { + return null; + } + + // 重试前等待一段时间 + try { + Thread.sleep(1000 * (retryCount + 1)); // 指数退避 + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + return null; + } + } + } + + return null; + } + + /** + * 保存RSA公钥到文件夹 + * @param publicKey RSA公钥内容 + */ + private static void savePublicKeyToClasspath(String publicKey) throws IOException { + // 创建 cert 目录(如果不存在) + Path certDir = Paths.get(CERT_FOLDER); + if (!Files.exists(certDir)) { + Files.createDirectories(certDir); // 自动创建目录 + } + + // 写入公钥文件(UTF-8 编码) + Path keyFile = certDir.resolve(RSA_PUBLIC_KEY_FILENAME); + Files.write(keyFile, publicKey.getBytes(StandardCharsets.UTF_8)); + } + /** + * 从文件夹加载RSA公钥 + * @return RSA公钥内容 + */ + private static String loadPublicKeyFromClasspath() throws IOException { + Path keyFile = Paths.get(CERT_FOLDER + RSA_PUBLIC_KEY_FILENAME); + byte[] bytes = Files.readAllBytes(keyFile); // 读取所有字节 + return new String(bytes, StandardCharsets.UTF_8); // 转为UTF-8字符串 + } + + /** + * 加载公钥 返回PublicKey + */ + public static PublicKey loadPublicKey(String pemContent) throws Exception { + // 读取PEM文件内容 +// String pemContent = new String(Files.readAllBytes(Paths.get(CERT_FOLDER + RSA_PUBLIC_KEY_FILENAME)), StandardCharsets.UTF_8); + + // 移除PEM头尾标记 + String publicKeyPEM = pemContent + .replace("-----BEGIN RSA PUBLIC KEY-----", "") + .replace("-----END RSA PUBLIC KEY-----", "") + .replaceAll("\\s", ""); // 去除换行/空格 + + // 解码Base64 + byte[] encoded = Base64.getDecoder().decode(publicKeyPEM); + + // 手动解析PKCS#1格式 + DerInputStream derReader = new DerInputStream(encoded); + DerValue[] seq = derReader.getSequence(0); + BigInteger modulus = seq[0].getBigInteger(); + BigInteger exponent = seq[1].getBigInteger(); + + RSAPublicKeySpec keySpec = new RSAPublicKeySpec(modulus, exponent); + return KeyFactory.getInstance("RSA").generatePublic(keySpec); + } + + /** + * 使用RSA-OAEP加密数据 + * @param plaintext 待加密的明文 + * @return Base64编码的密文 + */ + public static String encrypt(String plaintext, PublicKey publicKey) throws Exception { + Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWITHSHA-1ANDMGF1PADDING"); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + + byte[] encryptedBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(encryptedBytes); + } + + + + /** + * 商户付款到银行卡(优先使用本地保存的公钥) + * @param partnerTradeNo 商户订单号 + * @param bankNo 银行卡号 + * @param trueName 银行卡真实姓名 + * @param bankCode 银行编码 + * @param amount 金额(分) + * @param desc 付款说明 + * @return 付款结果 + */ + public Map<String, String> payToBankCard(String partnerTradeNo, String bankNo, String trueName, + String bankCode, BigDecimal amount, String desc) throws Exception { + int i = amount.multiply(new BigDecimal("100")).intValue(); + // 1. 尝试从本地加载RSA公钥 + String pubKey = loadPublicKeyFromClasspath(); + + // 2. 如果本地没有公钥或公钥无效,则从微信获取新公钥 + if (pubKey == null || pubKey.isEmpty()) { + pubKey = getRsaPublicKey(); + + } + //公钥对象 + PublicKey publicKey = loadPublicKey(pubKey); + + // 3. 使用RSA公钥加密银行卡号和真实姓名 + String encryptedBankNo = encrypt(bankNo, publicKey); + String encryptedTrueName = encrypt(trueName, publicKey); + + // 4. 构建请求参数 + Map<String, String> params = new HashMap<>(); + params.put("mch_id", wechatPayConfig.getMchId()); + params.put("partner_trade_no", partnerTradeNo); + params.put("enc_bank_no", encryptedBankNo); + params.put("enc_true_name", encryptedTrueName); + params.put("bank_code", bankCode); + params.put("amount", String.valueOf(i)); + params.put("desc", desc); + params.put("nonce_str", generateNonceStr()); + + // 生成签名 + String sign = weixinSignature(params); + params.put("sign", sign); + + // 将参数转换为XML + String xmlParams = XMLUtil.mapToXml(params); + + // 5. 发送请求到微信支付付款到银行卡接口 + String url = "https://api.mch.weixin.qq.com/mmpaysptrans/pay_bank"; + String result = postWithCert(url, xmlParams); + + // 解析返回结果 + Map<String, String> resultMap = XMLUtil.xmlToMap(result); + + // 验证签名 + if (!verifySign(resultMap, wechatPayConfig.getKey())) { + throw new Exception("付款到银行卡签名验证失败"); + } + + return resultMap; + } + + public static void main(String[] args) throws IOException { +/* + try { + // 1. 加载公钥 + PublicKey publicKey = loadPublicKey(); + + // 2. 加密数据 + String sensitiveData = "用户名"; + String encryptedData = encrypt(sensitiveData, publicKey); + + System.out.println("加密结果(Base64):\n" + encryptedData); + } catch (Exception e) { + e.printStackTrace(); + }*/ + + String info="CjlaS7RVnPn7zzP5ByZDxUN7OrXGp1/DEdO0qahpIqDH/gTNHb/U7VmrVV0S4lXrIa0N8FEREC3CdIeT4XB5P4D0E8TSURu6J/cD01hFu2/uJOvcE6EeQH2xiRg/Wir4qcW7c6uTiLoqyirCQXcGzQb3CCcJf7OZWaweOUkaKjaBRa1GzMZcguSZnQJz0cD5jTMx+Tch5+b7jBq5PrTFxtMSH/DAG+kgkRazDFnEzkMeT4V+FViw8f8sjkH0TScMgWBiSKmQC837BLD27yIGklqlYkDP2IMeiNw+b12qCAGszfp2vYd3X+HpViXkQQet3PJWYlAm55R+IgvschP7Ub65XzLINfQrJKrQUXiKKO2LwoSRSwZvfDkR8G8E8X59CnU2XvWKeos5Y0q8ckbJb97yI+09nNgMjYyJoVCVjTGc7ghcYvWKbqanJ8bSFqiBCIqLSXsRR2DmJIxHq9fGE72kCJyJEAqLPMNyfYBSNF8z1btjyz0+y/xQQcySKlQInZ710FxSE7KwRSBQ92j9nDdlR7UxCrPVCkEd+GrVNSqqnyjNh1J/rPJPHvvGwkPPq72TKiw6ZgaIgIDhy0/lWHTclo4sjYAWuUVfg3CJ8dqkuQwVZ7i0+NiahIl78RtcUph8NR48yUgBkN7WhCcu5wLbg2tu8Qe0SIwHF+RW1x9Yc8akEkNbMd4xzs8lY5MYEU9V16U8RyWJuwPDph3RnmV8HQ+2hfzmjCvPkBwtfR8P5VdK86OIsHfnfQxAcPM2a86tOBBzFXPrLHgd2CRcDKH+MXTw7RSH/bk1PiMUAWF8TQsNDzgUlznJnkjiQxoym/4ZUf4C6072KKQHbp6bgBYkBhJLT2lmjVMNSX5b1SXM9eTQixRfq6MKGw3P8XJnKdofktVv+KtSzWQlW0C8p504NWACiExupF5EII7FG+xbTa/s7vxXCP7R98tpcQTGoQCLVv6UBCXt/t9iWlvs6SfuZhpCexeMmZWeiIldzRu87U9rXR46Hu7DAL8dZ+0ItsIZYThSIABzZgaLKggXlkjyAcbcPYKO7egrCmDtFhwN50V7hoXEQB8G5kf/lMuT5+xNE2FRmv7H2a0ttZiv4u17W5R8Ez9kubydeAgC9PkWnjptaubPxE0bjPN69tec"; + String key="fD0JzscfMf295SYtRK3MnPRjSCA4Gahr"; + try { + String decrypted = decrypt(info, key); + System.out.println("解密结果: " + decrypted); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + public static String decrypt(String encryptedStringA, String merchantKey) throws Exception { + try { + byte[] decode = Base64.getDecoder().decode(encryptedStringA); + String sign = MD5AndKL.MD5Encode(merchantKey, "UTF-8").toLowerCase(); + System.out.println("MD5 Key: " + sign); // 调试输出 + + if (Security.getProvider("BC") == null) { + Security.addProvider(new BouncyCastleProvider()); + } + + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS7Padding", "BC"); + byte[] aesKey = Arrays.copyOf(sign.getBytes("UTF-8"), 16); // 明确指定 UTF-8 + SecretKeySpec secretKeySpec = new SecretKeySpec(aesKey, "AES"); + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec); + + byte[] decryptedBytes = cipher.doFinal(decode); + return new String(decryptedBytes, "UTF-8"); // 明确指定 UTF-8 + } catch (Exception e) { + System.err.println("解密失败: " + e.getMessage()); + throw e; + } + } + +} diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/payment/wx/XMLUtil.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/payment/wx/XMLUtil.java new file mode 100644 index 0000000..1ea6998 --- /dev/null +++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/payment/wx/XMLUtil.java @@ -0,0 +1,52 @@ +package com.ruoyi.other.util.payment.wx; + +import org.dom4j.Document; +import org.dom4j.DocumentException; +import org.dom4j.DocumentHelper; +import org.dom4j.Element; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * XML工具类 + */ +public class XMLUtil { + + /** + * 将Map转换为XML字符串 + */ + public static String mapToXml(Map<String, String> params) { + Document document = DocumentHelper.createDocument(); + // 禁用XML声明 + document.setXMLEncoding(null); // 关键设置 + + Element root = document.addElement("xml"); + for (Map.Entry<String, String> entry : params.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue() != null ? entry.getValue() : ""; + root.addElement(key).setText(value); + } + + return document.asXML(); + } + + /** + * 将XML字符串转换为Map + */ + public static Map<String, String> xmlToMap(String xmlStr) throws DocumentException { + Map<String, String> map = new HashMap<>(); + Document document = DocumentHelper.parseText(xmlStr); + Element root = document.getRootElement(); + + for (Iterator<?> iterator = root.elementIterator(); iterator.hasNext();) { + Element element = (Element) iterator.next(); + // 关键修改:获取元素内所有内容(包括CDATA) + String value = element.getStringValue(); // 自动处理CDATA + map.put(element.getName(), value); + } + + return map; + } +} -- Gitblit v1.7.1