2个文件已删除
18个文件已添加
10个文件已修改
| | |
| | | <version>2.0.10</version> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>com.squareup.okhttp3</groupId> |
| | | <artifactId>okhttp</artifactId> |
| | | <version>4.9.3</version> |
| | | </dependency> |
| | | |
| | | </dependencies> |
| | | |
| | | <build> |
| | |
| | | wx: |
| | | conf: |
| | | appId: |
| | | secretId: |
| | | secretId: |
| | | payment: |
| | | wx: |
| | | # 微信appid |
| | | appId: |
| | | # 微信商户号 |
| | | mchId: |
| | | # 秘钥 |
| | | secretId: |
| | | v3: |
| | | # 加签串 |
| | | apiKey: |
| | | # 加签证书地址 |
| | | privateKeyPath: |
| | | # 微信支付公钥id |
| | | publicKeyId: |
| | | # 微信支付公钥证书 |
| | | publicKeyPath: |
| | | # 证书序列号 |
| | | mchSerialNo: |
| | | # 支付成功回调地址 |
| | | notifyPayUrl: |
| | | # 支付退款回调地址 |
| | | notifyRefundUrl: |
| | |
| | | <scope>test</scope> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>com.squareup.okhttp3</groupId> |
| | | <artifactId>okhttp</artifactId> |
| | | <version>4.9.3</version> |
| | | </dependency> |
| | | |
| | | </dependencies> |
| | | |
| | | <build> |
| New file |
| | |
| | | package com.ruoyi.web.controller.api; |
| | | |
| | | import com.alibaba.fastjson2.JSONObject; |
| | | import com.ruoyi.common.core.domain.R; |
| | | import com.ruoyi.framework.web.service.TokenService; |
| | | import com.ruoyi.system.wxPay.enums.TradeStateEnum; |
| | | import com.ruoyi.system.wxPay.model.WeixinPayProperties; |
| | | import com.ruoyi.system.wxPay.utils.WxV3Pay; |
| | | import com.wechat.pay.contrib.apache.httpclient.util.AesUtil; |
| | | import io.swagger.annotations.Api; |
| | | import io.swagger.annotations.ApiOperation; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.bind.annotation.*; |
| | | |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import java.io.BufferedReader; |
| | | import java.io.PrintWriter; |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 微信相关接口 |
| | | */ |
| | | @Slf4j |
| | | @RestController |
| | | @CrossOrigin |
| | | @RequestMapping("/wx/") |
| | | @Api(tags = {"微信支付相关接口"}) |
| | | public class WxPayController { |
| | | @Autowired |
| | | private WxV3Pay wxV3Pay; |
| | | @Autowired |
| | | private TokenService tokenService; |
| | | @Autowired |
| | | private WeixinPayProperties weixinPayProperties; |
| | | |
| | | |
| | | |
| | | /** |
| | | * 按实际修改 |
| | | */ |
| | | @ApiOperation("订单支付") |
| | | @PostMapping("orderPay") |
| | | public R<Map<String, Object>> orderPay(@RequestParam(value = "inspectionId") String inspectionId) { |
| | | |
| | | // TSysOtherConfig sysOtherConfig = sysOtherConfigService.getById(1); |
| | | // if (Objects.isNull(sysOtherConfig.getAiPrice())) { |
| | | // return R.fail("系统未配置AI支付价格"); |
| | | // } |
| | | // |
| | | // String userId = tokenService.getLoginUserApplet().getUserId(); |
| | | // TSysAppUser sysAppUser = sysAppUserService.getById(userId); |
| | | // // 价格 |
| | | // Long totalPrice = sysOtherConfig.getAiPrice().multiply(new BigDecimal(100)).longValue(); |
| | | // // 生成订单号 |
| | | // String orderNo = CodeGenerateUtils.generateOrderSn(); |
| | | // // 查询用户信息 用户openid |
| | | // String openId = sysAppUser.getOpenId(); |
| | | // // 存储支付记录 |
| | | // sysPayRecordService.saveData(orderNo, userId, sysOtherConfig.getAiPrice(), 1); |
| | | // // 调用支付方法 |
| | | // Map<String, Object> result = wxV3Pay.jsApi(orderNo, totalPrice, openId,"AI检测报告支付",""); |
| | | // log.info("支付参数:{}", result); |
| | | return R.ok(); |
| | | } |
| | | |
| | | /** |
| | | * 支付回调 |
| | | */ |
| | | @PostMapping("pay/notify") |
| | | @ApiOperation("订单回调") |
| | | public void payNotify(HttpServletRequest request, HttpServletResponse response) throws Exception { |
| | | try { |
| | | System.err.println("微信回调"); |
| | | BufferedReader reader = request.getReader(); |
| | | StringBuilder requestBody = new StringBuilder(); |
| | | String line; |
| | | while ((line = reader.readLine()) != null) { |
| | | requestBody.append(line); |
| | | } |
| | | System.err.println("全部请求体" + requestBody); |
| | | JSONObject jsonObject = JSONObject.parseObject(requestBody.toString()); |
| | | JSONObject resource = jsonObject.getJSONObject("resource"); |
| | | |
| | | AesUtil aesUtil = new AesUtil(weixinPayProperties.getV3().getApiKey().getBytes(StandardCharsets.UTF_8)); |
| | | String decryptedData = aesUtil.decryptToString(resource.getString("associated_data").getBytes(StandardCharsets.UTF_8), resource.getString("nonce").getBytes(StandardCharsets.UTF_8), |
| | | resource.getString("ciphertext")); |
| | | System.err.println("微信解密的字符串信息" + decryptedData); |
| | | JSONObject jsonInfo = JSONObject.parse(decryptedData); |
| | | String out_trade_no = jsonInfo.getString("out_trade_no"); |
| | | String transaction_id = jsonInfo.getString("transaction_id"); |
| | | String trade_state = jsonInfo.getString("trade_state"); |
| | | String success_time = jsonInfo.getString("success_time"); |
| | | // 查询订单 |
| | | // TSysPayRecord sysPayRecord = sysPayRecordService.getOne(Wrappers.lambdaQuery(TSysPayRecord.class) |
| | | // .eq(TSysPayRecord::getOrderNo, out_trade_no).last("LIMIT 1")); |
| | | if (trade_state.equals(TradeStateEnum.SUCCESS.name())) { |
| | | log.info("回调成功"); |
| | | // 订单号查询订单 |
| | | // sysPayRecord.setPayState(2); |
| | | // sysPayRecord.setPayTime(WxTimeUtils.dateToLocalDateTime(WxTimeUtils.toRfc3339Date(success_time))); |
| | | // sysPayRecord.setTransactionId(transaction_id); |
| | | // sysPayRecordService.updateById(sysPayRecord); |
| | | // |
| | | // // 处理检测报告为可见 |
| | | // TSysInspection sysInspection = sysInspectionService.getById(sysPayRecord.getInspectionId()); |
| | | // sysInspection.setIsPay(1); |
| | | // sysInspectionService.updateById(sysInspection); |
| | | |
| | | PrintWriter out = response.getWriter(); |
| | | out.print("SUCCESS"); |
| | | out.flush(); |
| | | out.close(); |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("支付回调异常:{}", e, e); |
| | | wxV3Pay.ack(false, e.getMessage()); |
| | | } |
| | | } |
| | | } |
| | |
| | | package com.ruoyi.web.controller.system; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.baomidou.mybatisplus.core.toolkit.Wrappers; |
| | | import com.ruoyi.common.constant.Constants; |
| | | import com.ruoyi.common.core.domain.AjaxResult; |
| | | import com.ruoyi.common.core.domain.R; |
| | | import com.ruoyi.common.core.domain.entity.SysMenu; |
| | | import com.ruoyi.common.core.domain.entity.SysUser; |
| | | import com.ruoyi.common.core.domain.entity.TAppUserResp; |
| | | import com.ruoyi.common.core.domain.model.LoginAppBody; |
| | | import com.ruoyi.common.core.domain.model.LoginUserApplet; |
| | | import com.ruoyi.common.core.redis.RedisCache; |
| | | import com.ruoyi.common.redis.service.RedisService; |
| | | import com.ruoyi.common.utils.SecurityUtils; |
| | | import com.ruoyi.framework.web.service.SysLoginService; |
| | | import com.ruoyi.framework.web.service.SysPermissionService; |
| | | import com.ruoyi.framework.web.service.TokenService; |
| | | import com.ruoyi.system.model.TAppUser; |
| | | import com.ruoyi.system.service.ISysMenuService; |
| | | import com.ruoyi.system.service.ISysRoleService; |
| | | import com.ruoyi.system.service.TAppUserService; |
| | | import com.ruoyi.system.utils.wx.body.resp.Code2SessionRespBody; |
| | | import com.ruoyi.system.utils.wx.body.resq.Code2SessionResqBody; |
| | | import com.ruoyi.system.utils.wx.model.WeixinProperties; |
| | | import com.ruoyi.system.utils.wx.pojo.AppletUserDecodeData; |
| | | import com.ruoyi.system.utils.wx.pojo.AppletUserEncrypteData; |
| | | import com.ruoyi.system.utils.wx.tools.WxAppletTools; |
| | | import com.ruoyi.system.utils.wx.tools.WxUtils; |
| | | import com.ruoyi.web.controller.tool.MsgUtils; |
| | | import io.swagger.annotations.Api; |
| | | import io.swagger.annotations.ApiOperation; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.BeanUtils; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.util.StringUtils; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.PostMapping; |
| | | import org.springframework.web.bind.annotation.RequestBody; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | import org.springframework.web.client.RestTemplate; |
| | | |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | /** |
| | |
| | | * |
| | | * @author ruoyi |
| | | */ |
| | | @Slf4j |
| | | @Api(tags = "登录") |
| | | @RestController |
| | | public class SysLoginController |
| | |
| | | private ISysRoleService roleService; |
| | | @Autowired |
| | | private MsgUtils msgUtils; |
| | | |
| | | @Autowired |
| | | private RestTemplate wxRestTemplate; |
| | | @Autowired |
| | | private WeixinProperties wxConfig; |
| | | @Autowired |
| | | private RedisService redisService; |
| | | @Autowired |
| | | private TAppUserService appUserService; |
| | | /** |
| | | * 账号密码登录 |
| | | * |
| | |
| | | * @return 结果 |
| | | */ |
| | | @ApiOperation(value = "账号密码登录",notes = "账号密码登录") |
| | | @PostMapping("/api/loginApp") |
| | | @PostMapping("/loginApp") |
| | | public AjaxResult login(@RequestBody String param) |
| | | { |
| | | LoginAppBody loginAppBody = JSON.parseObject(param, LoginAppBody.class); |
| | |
| | | LoginUserApplet loginUser = loginService.loginCodeApplet(loginAppBody.getAccount(), loginAppBody.getPassword()); |
| | | ajax.put(Constants.TOKEN, tokenService.createTokenApplet(loginUser)); |
| | | ajax.put("data",loginUser); |
| | | |
| | | return ajax; |
| | | } |
| | | |
| | | @ApiOperation(value = "通过code获得openid,获取用户信息",tags = {"微信小程序登录"}) |
| | | @PostMapping("/openIdByJsCode") |
| | | public R<Map<String, Object>> openIdByJsCode(@RequestBody AppletUserEncrypteData data) { |
| | | log.info("<<<<<<<<换取openid开始<<<<<<<<:{}", data.getCode()); |
| | | WxAppletTools appletTools = new WxAppletTools(wxRestTemplate, wxConfig, redisService); |
| | | Code2SessionRespBody body = appletTools.getOpenIdByJscode2session(new Code2SessionResqBody().build(data.getCode())); |
| | | String openid = body.getOpenid(); |
| | | String sessionKey = body.getSessionKey(); |
| | | // 用户信息解密 数据验签 |
| | | // if (StringUtils.isNotBlank(data.getSignature())) { |
| | | // WxUtils.verifySignature(data.getRawData(), sessionKey, data.getSignature()); |
| | | // } |
| | | if(StringUtils.isEmpty(data.getEncryptedData()) || StringUtils.isEmpty(data.getIv())){ |
| | | return R.fail("已拒绝授权"); |
| | | } |
| | | AppletUserDecodeData appletUserDecodeData = WxUtils.encryptedData(data.getEncryptedData(), sessionKey, data.getIv()); |
| | | appletUserDecodeData.setOpenId(openid); |
| | | // 先使用openId和当前手机号进行查询 |
| | | TAppUser sysAppUser = appUserService.getOne(Wrappers.lambdaQuery(TAppUser.class) |
| | | .and(e->e.eq(TAppUser::getOpenId, appletUserDecodeData.getOpenId()).or() |
| | | .eq(TAppUser::getPhoneNumber, appletUserDecodeData.getPhoneNumber()))); |
| | | if (sysAppUser==null){ |
| | | sysAppUser = new TAppUser(); |
| | | sysAppUser.setPhoneNumber(appletUserDecodeData.getPhoneNumber()); |
| | | sysAppUser.setOpenId(appletUserDecodeData.getOpenId()); |
| | | // 手机号中间四位替换为* |
| | | sysAppUser.setNickName(appletUserDecodeData.getPhoneNumber().replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")); |
| | | appUserService.save(sysAppUser); |
| | | }else { |
| | | // 判断账号是否被冻结 |
| | | if (sysAppUser.getStatus() == 2){ |
| | | return R.fail("账号已冻结,请联系管理员"); |
| | | } |
| | | // 手机号相同但openId为空的情况 |
| | | sysAppUser.setOpenId(appletUserDecodeData.getOpenId()); |
| | | appUserService.updateById(sysAppUser); |
| | | } |
| | | LoginUserApplet loginUserApplet = new LoginUserApplet(); |
| | | TAppUserResp sysUserResp = new TAppUserResp(); |
| | | BeanUtils.copyProperties(sysAppUser, sysUserResp); |
| | | loginUserApplet.setUser(sysUserResp); |
| | | loginUserApplet.setUserId(sysAppUser.getId()); |
| | | Map<String, Object> tokenInfos = new HashMap<>(); |
| | | tokenInfos.put("token",tokenService.createTokenApplet(loginUserApplet)); |
| | | tokenInfos.put("info",loginUserApplet); |
| | | return R.ok(tokenInfos); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 获取用户信息 |
| | |
| | | wx: |
| | | conf: |
| | | appId: |
| | | secretId: |
| | | secretId: |
| | | payment: |
| | | wx: |
| | | # 微信appid |
| | | appId: |
| | | # 微信商户号 |
| | | mchId: |
| | | # 秘钥 |
| | | secretId: |
| | | v3: |
| | | # 加签串 |
| | | apiKey: |
| | | # 加签证书地址 |
| | | privateKeyPath: |
| | | # 微信支付公钥id |
| | | publicKeyId: |
| | | # 微信支付公钥证书 |
| | | publicKeyPath: |
| | | # 证书序列号 |
| | | mchSerialNo: |
| | | # 支付成功回调地址 |
| | | notifyPayUrl: |
| | | # 支付退款回调地址 |
| | | notifyRefundUrl: |
| | |
| | | |
| | | <!-- https://mvnrepository.com/artifact/jakarta.servlet/jakarta.servlet-api --> |
| | | |
| | | <dependency> |
| | | <groupId>com.squareup.okhttp3</groupId> |
| | | <artifactId>okhttp</artifactId> |
| | | <version>4.9.3</version> |
| | | </dependency> |
| | | |
| | | </dependencies> |
| | | |
| | | </project> |
| | |
| | | import lombok.Data; |
| | | import lombok.EqualsAndHashCode; |
| | | |
| | | import java.time.LocalDate; |
| | | |
| | | /** |
| | | * <p> |
| | | * 租户 |
| | |
| | | private String nickName; |
| | | |
| | | @ApiModelProperty(value = "手机号") |
| | | private String phone; |
| | | private String phoneNumber; |
| | | |
| | | @ApiModelProperty(value = "登录账号") |
| | | private String account; |
| | | @ApiModelProperty(value = "头像") |
| | | private String avatar; |
| | | |
| | | @ApiModelProperty(value = "密码") |
| | | private String password; |
| | | @ApiModelProperty(value = "性别 1=男 2=女") |
| | | private Integer gender; |
| | | |
| | | @ApiModelProperty(value = "分队名称 注册提交时使用") |
| | | private String teamName; |
| | | |
| | | @ApiModelProperty(value = "分队id") |
| | | private String teamId; |
| | | @ApiModelProperty(value = "生日") |
| | | private LocalDate birthDate; |
| | | |
| | | @ApiModelProperty(value = "状态 1=启用 2=禁用") |
| | | private Integer status; |
| | | |
| | | @ApiModelProperty(value = "实训次数") |
| | | private Integer practicalTrainCount; |
| | | |
| | | @ApiModelProperty(value = "其他次数") |
| | | private Integer otherCount; |
| | | |
| | | @ApiModelProperty(value = "审核状态 0=待审核 1=通过 2=拒绝") |
| | | private Integer state; |
| | | |
| | | } |
| | |
| | | @Override |
| | | public String getPassword() |
| | | { |
| | | return user.getPassword(); |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | |
| | | <groupId>com.fasterxml.jackson.datatype</groupId> |
| | | <artifactId>jackson-datatype-jsr310</artifactId> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>com.github.wechatpay-apiv3</groupId> |
| | | <artifactId>wechatpay-java-core</artifactId> |
| | | <version>0.2.12</version> |
| | | <scope>compile</scope> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>com.github.wechatpay-apiv3</groupId> |
| | | <artifactId>wechatpay-apache-httpclient</artifactId> |
| | | <version>0.4.9</version> |
| | | </dependency> |
| | | </dependencies> |
| | | |
| | | </project> |
| | |
| | | package com.ruoyi.system.utils.wx.tools; |
| | | |
| | | import com.ruoyi.system.utils.wx.pojo.AppletUserDecodeData; |
| | | import com.ruoyi.common.utils.sign.Base64; |
| | | import com.ruoyi.system.utils.wx.pojo.AppletUserDecodeData; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.commons.lang3.CharEncoding; |
| | | import org.bouncycastle.jce.provider.BouncyCastleProvider; |
| | |
| | | import javax.crypto.Cipher; |
| | | import javax.crypto.spec.IvParameterSpec; |
| | | import javax.crypto.spec.SecretKeySpec; |
| | | import javax.servlet.ServletInputStream; |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import java.io.BufferedReader; |
| | | import java.io.IOException; |
| | | import java.io.InputStream; |
| | | import java.io.InputStreamReader; |
| | | import java.security.AlgorithmParameters; |
| | | import java.security.SecureRandom; |
| | | import java.security.Security; |
| | | import java.util.Arrays; |
| | | import java.util.Random; |
| | | |
| | | /** |
| | | * @Description 获取用户信息工具类 |
| | |
| | | */ |
| | | @Slf4j |
| | | public class WxUtils { |
| | | /** |
| | | * 随机字符 |
| | | */ |
| | | private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; |
| | | |
| | | private static final Random RANDOM = new SecureRandom(); |
| | | /** |
| | | * 微信小程序API 用户数据的解密 |
| | | * |
| | |
| | | * @return |
| | | */ |
| | | public static String streamBodyByReceive(HttpServletRequest request) throws IOException { |
| | | log.info("微信异步回调地址:{}", request.getRequestURL()); |
| | | StringBuffer buffer = new StringBuffer(); |
| | | InputStream inputStream = request.getInputStream(); |
| | | InputStreamReader reader = new InputStreamReader(inputStream); |
| | | BufferedReader bufferedReader = new BufferedReader(reader); |
| | | String body = null; |
| | | while ((body = bufferedReader.readLine()) != null) { |
| | | buffer.append(body); |
| | | BufferedReader reader = null; |
| | | StringBuffer sb = new StringBuffer(); |
| | | try { |
| | | ServletInputStream stream = request.getInputStream(); |
| | | // 获取响应 |
| | | reader = new BufferedReader(new InputStreamReader(stream)); |
| | | String line; |
| | | while ((line = reader.readLine()) != null) { |
| | | sb.append(line); |
| | | } |
| | | } catch (IOException e) { |
| | | throw new RuntimeException("读取微信支付接口数据流出现异常!"); |
| | | } finally { |
| | | reader.close(); |
| | | WxUtils.info(sb.toString()); |
| | | } |
| | | String data = buffer.toString(); |
| | | reader.close(); |
| | | inputStream.close(); |
| | | log.info("微信异步回调数据:{}", data); |
| | | return data; |
| | | return sb.toString(); |
| | | } |
| | | |
| | | /** |
| | | * 获取随机字符串 Nonce Str |
| | | * |
| | | * @return String 随机字符串 |
| | | */ |
| | | public static String generateNonceStr() { |
| | | char[] nonceChars = new char[32]; |
| | | for (int index = 0; index < nonceChars.length; ++index) { |
| | | nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length())); |
| | | } |
| | | return new String(nonceChars); |
| | | } |
| | | |
| | | /** |
| New file |
| | |
| | | package com.ruoyi.system.wxPay.config; |
| | | |
| | | import com.ruoyi.system.wxPay.model.WeixinPayProperties; |
| | | import com.ruoyi.system.wxPay.utils.WxV3Pay; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.context.annotation.Configuration; |
| | | |
| | | /** |
| | | * 项目中需继承此类 |
| | | * |
| | | * @author lihen |
| | | */ |
| | | @Configuration |
| | | public class WxConfig { |
| | | |
| | | private final WeixinPayProperties weixinPayProperties; |
| | | |
| | | @Autowired |
| | | public WxConfig(WeixinPayProperties weixinPayProperties) { |
| | | this.weixinPayProperties = weixinPayProperties; |
| | | } |
| | | |
| | | @Bean |
| | | @ConditionalOnMissingBean(name = "wxV3Pay") |
| | | public WxV3Pay wxSpV3Pay() { |
| | | return new WxV3Pay(weixinPayProperties); |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | package com.ruoyi.system.wxPay.enums; |
| | | |
| | | import lombok.Getter; |
| | | |
| | | import java.util.stream.Stream; |
| | | |
| | | /** |
| | | * @author xiaochen |
| | | * @ClassName ProfitSharingEnum |
| | | * @Description |
| | | * @date 2021-11-21 11:15 |
| | | */ |
| | | public enum RefundEnum { |
| | | /** |
| | | * 退款成功 |
| | | */ |
| | | SUCCESS("SUCCESS", "退款成功"), |
| | | /** |
| | | * 退款关闭 |
| | | */ |
| | | CLOSED("CLOSED", "退款关闭"), |
| | | /** |
| | | * 退款处理中 |
| | | */ |
| | | PROCESSING("PROCESSING", "退款处理中"), |
| | | /** |
| | | * 退款异常 |
| | | */ |
| | | ABNORMAL("ABNORMAL", "退款异常"), |
| | | ; |
| | | |
| | | @Getter |
| | | private String code; |
| | | @Getter |
| | | private String desc; |
| | | |
| | | RefundEnum(String code, String desc) { |
| | | this.code = code; |
| | | this.desc = desc; |
| | | } |
| | | |
| | | /** |
| | | * 通过交易类型执行具体的交易方法 |
| | | * |
| | | * @param code |
| | | * @return |
| | | */ |
| | | public static RefundEnum fromValue(String code) { |
| | | return Stream.of(RefundEnum.values()).filter(fileType -> |
| | | fileType.getCode().toLowerCase().equals(code.toLowerCase()) |
| | | ).findFirst().orElse(null); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.ruoyi.system.wxPay.enums; |
| | | |
| | | import lombok.Getter; |
| | | |
| | | import java.util.stream.Stream; |
| | | |
| | | /** |
| | | * @author xiaochen |
| | | * @ClassName AliTradeStateEnum |
| | | * @Description |
| | | * @date 2022-01-07 11:56 |
| | | */ |
| | | public enum TradeStateEnum { |
| | | SUCCESS("支付成功"), |
| | | RETURN("已分账回退"), |
| | | FAIL("已失败"), |
| | | PROCESSING("处理中"), |
| | | FINISHED("分账完成"), |
| | | REFUND("转入退款"), |
| | | PAYERROR("支付失败(其他原因,如银行返回失败)"), |
| | | USERPAYING("用户支付中"), |
| | | CLOSED("已关闭"), |
| | | NOTPAY("未支付"), |
| | | UNKNOWN("未知"), |
| | | DONE("服务订单完成"), |
| | | // ... |
| | | ; |
| | | @Getter |
| | | private String desc; |
| | | |
| | | TradeStateEnum(String desc) { |
| | | this.desc = desc; |
| | | } |
| | | |
| | | public static TradeStateEnum tradeState(String code) { |
| | | return Stream.of(TradeStateEnum.values()).filter(fileType -> |
| | | fileType.name().equals(code) |
| | | ).findFirst().orElse(UNKNOWN); |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | package com.ruoyi.system.wxPay.exception; |
| | | |
| | | /** |
| | | * @author lihen |
| | | */ |
| | | public class WxException extends RuntimeException { |
| | | |
| | | private final static int OK = 0; |
| | | private final static int ValidateSignatureError = -40001; |
| | | private final static int ParseXmlError = -40002; |
| | | public final static int ComputeSignatureError = -40003; |
| | | private final static int IllegalAesKey = -40004; |
| | | private final static int ValidateAppidError = -40005; |
| | | private final static int EncryptAESError = -40006; |
| | | private final static int DecryptAESError = -40007; |
| | | private final static int IllegalBuffer = -40008; |
| | | |
| | | private int code; |
| | | |
| | | private static String getMessage(int code) { |
| | | switch (code) { |
| | | case ValidateSignatureError: |
| | | return "签名验证错误"; |
| | | case ParseXmlError: |
| | | return "xml解析失败"; |
| | | case ComputeSignatureError: |
| | | return "sha加密生成签名失败"; |
| | | case IllegalAesKey: |
| | | return "SymmetricKey非法"; |
| | | case ValidateAppidError: |
| | | return "appid校验失败"; |
| | | case EncryptAESError: |
| | | return "aes加密失败"; |
| | | case DecryptAESError: |
| | | return "aes解密失败"; |
| | | case IllegalBuffer: |
| | | return "解密后得到的buffer非法"; |
| | | default: |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | public int getCode() { |
| | | return code; |
| | | } |
| | | |
| | | public WxException(int code) { |
| | | super(getMessage(code)); |
| | | this.code = code; |
| | | } |
| | | |
| | | public WxException(String message) { |
| | | super(message); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.ruoyi.system.wxPay.model; |
| | | |
| | | import lombok.Data; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.poi.util.IOUtils; |
| | | |
| | | import java.io.ByteArrayInputStream; |
| | | import java.io.FileInputStream; |
| | | import java.io.IOException; |
| | | import java.io.InputStream; |
| | | |
| | | /** |
| | | * @author xiaochen |
| | | * @ClassName V3 |
| | | * @Description |
| | | */ |
| | | @Slf4j |
| | | @Data |
| | | public class V3 { |
| | | /** |
| | | * 获取 API 密钥 |
| | | * |
| | | * @return API密钥 |
| | | */ |
| | | private String apiKey; |
| | | /** |
| | | * 秘钥路径,apiclient_key.pem |
| | | */ |
| | | private String privateKeyPath; |
| | | /** |
| | | * 微信支付公钥id |
| | | */ |
| | | private String publicKeyId; |
| | | /** |
| | | * 微信支付公钥证书 apiclient_cert.pem |
| | | */ |
| | | private String publicKeyPath; |
| | | /** |
| | | * 商户证书序列号 |
| | | */ |
| | | private String mchSerialNo; |
| | | |
| | | /** |
| | | * 支付回调地址 |
| | | * |
| | | * @return |
| | | */ |
| | | private String notifyPayUrl; |
| | | |
| | | /** |
| | | * 退款回调地址 |
| | | * |
| | | * @return |
| | | */ |
| | | private String notifyRefundUrl; |
| | | |
| | | /** |
| | | * 退款回调地址 |
| | | */ |
| | | private String notifyTravelRefundUrl; |
| | | |
| | | |
| | | public InputStream getPrivateKeyStream() { |
| | | // 需要证书释放 |
| | | byte[] certData; |
| | | // InputStream certStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(this.privateKeyPath); |
| | | InputStream certStream = null; |
| | | try { |
| | | certStream = new FileInputStream(this.privateKeyPath); |
| | | certData = IOUtils.toByteArray(certStream); |
| | | } catch (IOException e) { |
| | | e.printStackTrace(); |
| | | throw new RuntimeException("私钥文件未找到"); |
| | | }finally { |
| | | if(null != certStream){ |
| | | try { |
| | | certStream.close(); |
| | | } catch (IOException e) { |
| | | log.error("私钥流关闭异常"); |
| | | } |
| | | } |
| | | } |
| | | return new ByteArrayInputStream(certData); |
| | | } |
| | | |
| | | public InputStream getPublicKeyStream() { |
| | | // 需要证书释放 |
| | | byte[] certData; |
| | | // InputStream certStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(this.privateKeyPath); |
| | | InputStream certStream = null; |
| | | try { |
| | | certStream = new FileInputStream(this.publicKeyPath); |
| | | certData = IOUtils.toByteArray(certStream); |
| | | } catch (IOException e) { |
| | | e.printStackTrace(); |
| | | throw new RuntimeException("公钥文件未找到"); |
| | | }finally { |
| | | if(null != certStream){ |
| | | try { |
| | | certStream.close(); |
| | | } catch (IOException e) { |
| | | log.error("公钥流关闭异常"); |
| | | } |
| | | } |
| | | } |
| | | return new ByteArrayInputStream(certData); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.ruoyi.system.wxPay.model; |
| | | |
| | | import lombok.ToString; |
| | | import org.springframework.boot.context.properties.ConfigurationProperties; |
| | | import org.springframework.boot.context.properties.NestedConfigurationProperty; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | /** |
| | | * @author xiaochen |
| | | * @ClassName WeixinProperties |
| | | * @Description |
| | | */ |
| | | @ToString |
| | | @Component |
| | | @ConfigurationProperties(prefix = "payment.wx") |
| | | public class WeixinPayProperties { |
| | | /** |
| | | * 默认开启 |
| | | */ |
| | | private boolean enabled = true; |
| | | /** |
| | | * 获取 App ID |
| | | * |
| | | * @return App ID |
| | | */ |
| | | private String appId; |
| | | /** |
| | | * 获取 Mch ID |
| | | * |
| | | * @return Mch ID |
| | | */ |
| | | private String mchId; |
| | | |
| | | /** |
| | | * 获取 secret ID |
| | | * |
| | | * @return secret ID |
| | | */ |
| | | private String secretId; |
| | | /** |
| | | * 回调地址 |
| | | */ |
| | | private String callBackUrl; |
| | | |
| | | public String getSecretId() { |
| | | return secretId; |
| | | } |
| | | |
| | | public void setSecretId(String secretId) { |
| | | this.secretId = secretId; |
| | | } |
| | | |
| | | /** |
| | | * v3 |
| | | */ |
| | | @NestedConfigurationProperty |
| | | private V3 v3; |
| | | |
| | | public boolean isEnabled() { |
| | | return enabled; |
| | | } |
| | | |
| | | public void setEnabled(boolean enabled) { |
| | | this.enabled = enabled; |
| | | } |
| | | |
| | | public V3 getV3() { |
| | | return v3; |
| | | } |
| | | |
| | | public void setV3(V3 v3) { |
| | | this.v3 = v3; |
| | | } |
| | | |
| | | /** |
| | | * HTTP(S) 连接超时时间,单位毫秒 |
| | | * |
| | | */ |
| | | public int getHttpConnectTimeoutMs() { |
| | | return 6 * 1000; |
| | | } |
| | | |
| | | /** |
| | | * HTTP(S) 读数据超时时间,单位毫秒 |
| | | */ |
| | | public int getHttpReadTimeoutMs() { |
| | | return 8 * 1000; |
| | | } |
| | | |
| | | public String getAppId() { |
| | | return appId; |
| | | } |
| | | |
| | | public void setAppId(String appId) { |
| | | this.appId = appId; |
| | | } |
| | | |
| | | public String getMchId() { |
| | | return mchId; |
| | | } |
| | | |
| | | public void setMchId(String mchId) { |
| | | this.mchId = mchId; |
| | | } |
| | | |
| | | public String getCallBackUrl() { |
| | | return callBackUrl; |
| | | } |
| | | |
| | | public void setCallBackUrl(String callBackUrl) { |
| | | this.callBackUrl = callBackUrl; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.ruoyi.system.wxPay.model; |
| | | |
| | | import lombok.*; |
| | | |
| | | /** |
| | | * @author xiaochen |
| | | * @ClassName WxPaymentRefundModel |
| | | * @Description |
| | | */ |
| | | @Builder |
| | | @AllArgsConstructor |
| | | @NoArgsConstructor |
| | | @Getter |
| | | @Setter |
| | | @ToString |
| | | public class WxCloseOrderModel { |
| | | private String mchid; |
| | | |
| | | private String out_trade_no; |
| | | } |
| New file |
| | |
| | | package com.ruoyi.system.wxPay.model; |
| | | |
| | | import lombok.*; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * @author xiaochen |
| | | * @ClassName WxPaymentInfoModel |
| | | * @Description |
| | | */ |
| | | @Builder |
| | | @AllArgsConstructor |
| | | @NoArgsConstructor |
| | | @Getter |
| | | @Setter |
| | | @ToString |
| | | public class WxPaymentInfoModel { |
| | | /** |
| | | * 合单商户appid |
| | | */ |
| | | private String combine_appid; |
| | | /** |
| | | * 合单商户号 |
| | | */ |
| | | private String combine_mchid; |
| | | /** |
| | | * 合单商户订单号 |
| | | */ |
| | | private String combine_out_trade_no; |
| | | /** |
| | | * 合单--子单信息 |
| | | */ |
| | | private List<SubOrders> sub_orders; |
| | | /** |
| | | * 合单--支付者 |
| | | */ |
| | | private CombinePayerInfo combine_payer_info; |
| | | |
| | | private String appid; |
| | | private String sp_appid; |
| | | private String mchid; |
| | | private String sp_mchid; |
| | | private String sub_appid; |
| | | private String sub_mchid; |
| | | private String description; |
| | | private String out_trade_no; |
| | | private String time_expire; |
| | | private String attach; |
| | | private String notify_url; |
| | | private String goods_tag; |
| | | private SettleInfo settle_info; |
| | | private Amount amount; |
| | | private Payer payer; |
| | | private Detail detail; |
| | | private SceneInfo scene_info; |
| | | |
| | | |
| | | @Builder |
| | | @AllArgsConstructor |
| | | @NoArgsConstructor |
| | | @Getter |
| | | @Setter |
| | | @ToString |
| | | public static class SettleInfo { |
| | | private Boolean profit_sharing; |
| | | private Integer subsidy_amount; |
| | | } |
| | | |
| | | @Builder |
| | | @AllArgsConstructor |
| | | @NoArgsConstructor |
| | | @Getter |
| | | @Setter |
| | | @ToString |
| | | public static class Amount { |
| | | private Integer total; |
| | | /** |
| | | * 合单支付时需要 |
| | | */ |
| | | private Integer total_amount; |
| | | @Builder.Default |
| | | private String currency = "CNY"; |
| | | } |
| | | |
| | | @Builder |
| | | @AllArgsConstructor |
| | | @NoArgsConstructor |
| | | @Getter |
| | | @Setter |
| | | @ToString |
| | | public static class Payer { |
| | | private String openid; |
| | | private String sp_openid; |
| | | private String sub_openid; |
| | | } |
| | | |
| | | |
| | | @Builder |
| | | @AllArgsConstructor |
| | | @NoArgsConstructor |
| | | @Getter |
| | | @Setter |
| | | @ToString |
| | | public static class Detail { |
| | | private int cost_price; |
| | | private String invoice_id; |
| | | private List<GoodsDetail> goods_detail; |
| | | |
| | | @Builder |
| | | @AllArgsConstructor |
| | | @NoArgsConstructor |
| | | @Getter |
| | | @Setter |
| | | @ToString |
| | | public static class GoodsDetail { |
| | | private String merchant_goods_id; |
| | | private String wechatpay_goods_id; |
| | | private String goods_name; |
| | | private int quantity; |
| | | private int unit_price; |
| | | } |
| | | } |
| | | |
| | | @Builder |
| | | @AllArgsConstructor |
| | | @NoArgsConstructor |
| | | @Getter |
| | | @Setter |
| | | @ToString |
| | | public static class SceneInfo { |
| | | private String payer_client_ip; |
| | | private String device_id; |
| | | private StoreInfo store_info; |
| | | private H5Info h5_info; |
| | | |
| | | @Builder |
| | | @AllArgsConstructor |
| | | @NoArgsConstructor |
| | | @Getter |
| | | @Setter |
| | | @ToString |
| | | public static class StoreInfo { |
| | | private String id; |
| | | private String name; |
| | | private String area_code; |
| | | private String address; |
| | | } |
| | | |
| | | @Builder |
| | | @AllArgsConstructor |
| | | @NoArgsConstructor |
| | | @Getter |
| | | @Setter |
| | | @ToString |
| | | public static class H5Info { |
| | | private String type; |
| | | private String app_name; |
| | | private String app_url; |
| | | private String bundle_id; |
| | | private String package_name; |
| | | } |
| | | } |
| | | |
| | | |
| | | @Builder |
| | | @AllArgsConstructor |
| | | @NoArgsConstructor |
| | | @Getter |
| | | @Setter |
| | | @ToString |
| | | public static class SubOrders { |
| | | |
| | | private String out_trade_no; |
| | | |
| | | |
| | | private Amount amount; |
| | | |
| | | private String mchid; |
| | | |
| | | private String sub_mchid; |
| | | |
| | | private String attach; |
| | | |
| | | private String description; |
| | | private String goods_tag; |
| | | |
| | | private SettleInfo settle_info; |
| | | } |
| | | |
| | | @Builder |
| | | @AllArgsConstructor |
| | | @NoArgsConstructor |
| | | @Getter |
| | | @Setter |
| | | @ToString |
| | | public static class CombinePayerInfo { |
| | | private String openid; |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | package com.ruoyi.system.wxPay.model; |
| | | |
| | | import lombok.*; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * @author xiaochen |
| | | * @ClassName WxPaymentRefundModel |
| | | * @Description |
| | | */ |
| | | @Builder |
| | | @AllArgsConstructor |
| | | @NoArgsConstructor |
| | | @Getter |
| | | @Setter |
| | | @ToString |
| | | public class WxPaymentRefundModel { |
| | | /** |
| | | * 子商户,二级商户号 |
| | | */ |
| | | private String sub_mchid; |
| | | /** |
| | | * 电商平台APPID |
| | | */ |
| | | private String sp_appid; |
| | | private String transaction_id; |
| | | private String out_trade_no; |
| | | /** |
| | | * 商户退款单号 |
| | | */ |
| | | private String out_refund_no; |
| | | /** |
| | | * 退款原因 |
| | | */ |
| | | private String reason; |
| | | private String notify_url; |
| | | /** |
| | | * 资金账户,否 |
| | | */ |
| | | private String funds_account; |
| | | /** |
| | | * 退款金额信息 |
| | | */ |
| | | private RefundAmount amount; |
| | | /** |
| | | * 退款商品 |
| | | */ |
| | | private List<RefundGoodsDetail> goods_detail; |
| | | |
| | | @Builder |
| | | @AllArgsConstructor |
| | | @NoArgsConstructor |
| | | @Getter |
| | | @Setter |
| | | @ToString |
| | | public static class RefundAmount { |
| | | /** |
| | | * 原订单金额 |
| | | */ |
| | | private int total; |
| | | @Builder.Default |
| | | private String currency = "CNY"; |
| | | /** |
| | | * 退款金额 |
| | | */ |
| | | private int refund; |
| | | } |
| | | |
| | | @Builder |
| | | @AllArgsConstructor |
| | | @NoArgsConstructor |
| | | @Getter |
| | | @Setter |
| | | @ToString |
| | | public static class RefundGoodsDetail { |
| | | private String merchant_goods_id; |
| | | private String wechatpay_goods_id; |
| | | private String goods_name; |
| | | private int unit_price; |
| | | private int refund_amount; |
| | | private int refund_quantity; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.ruoyi.system.wxPay.pojo; |
| | | |
| | | import lombok.Data; |
| | | |
| | | /** |
| | | * @author xiaochen |
| | | * @ClassName AppletUserDecodeData |
| | | * @Description |
| | | * 用户主体信息部分 |
| | | * { |
| | | * "openId": "OPENID", |
| | | * "nickName": "NICKNAME", |
| | | * "gender": GENDER, |
| | | * "city": "CITY", |
| | | * "province": "PROVINCE", |
| | | * "country": "COUNTRY", |
| | | * "avatarUrl": "AVATARURL", |
| | | * "unionId": "UNIONID", |
| | | * "watermark": |
| | | * { |
| | | * "appid":"APPID", |
| | | * "timestamp":TIMESTAMP |
| | | * } |
| | | * } |
| | | * 电话部分 |
| | | * { |
| | | * "phoneNumber": "13580006666", |
| | | * "purePhoneNumber": "13580006666", |
| | | * "countryCode": "86", |
| | | * "watermark": |
| | | * { |
| | | * "appid":"APPID", |
| | | * "timestamp": TIMESTAMP |
| | | * } |
| | | * } |
| | | * |
| | | */ |
| | | @Data |
| | | public class AppletUserDecodeData { |
| | | private String openId; |
| | | private String unionId; |
| | | private String nickName; |
| | | private int gender; |
| | | private String city; |
| | | private String province; |
| | | private String country; |
| | | private String avatarUrl; |
| | | private Watermark watermark; |
| | | private String phoneNumber; |
| | | private String purePhoneNumber; |
| | | private String countryCode; |
| | | } |
| New file |
| | |
| | | package com.ruoyi.system.wxPay.pojo; |
| | | |
| | | import lombok.Data; |
| | | |
| | | @Data |
| | | public class Watermark { |
| | | private String appid; |
| | | private String timestamp; |
| | | } |
| New file |
| | |
| | | package com.ruoyi.system.wxPay.resp; |
| | | |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.Builder; |
| | | import lombok.Data; |
| | | import lombok.NoArgsConstructor; |
| | | |
| | | import java.io.Serializable; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * @author xiaochen |
| | | * @ClassName FacilV3PayNotifyRespBody |
| | | * @Description |
| | | */ |
| | | @Builder |
| | | @NoArgsConstructor |
| | | @AllArgsConstructor |
| | | @Data |
| | | public class NotifyV3PayDecodeRespBody implements Serializable { |
| | | // 合单--开始 |
| | | private String combine_appid; |
| | | private String combine_mchid; |
| | | private String combine_out_trade_no; |
| | | private List<SubOrders> sub_orders; |
| | | // 合单--结束 |
| | | /** |
| | | * 服务商应用ID |
| | | */ |
| | | private String sp_appid; |
| | | /** |
| | | * 服务商户号 |
| | | */ |
| | | private String sp_mchid; |
| | | /** |
| | | * 商户号 |
| | | */ |
| | | private String mchid; |
| | | /** |
| | | * 子商户应用ID |
| | | */ |
| | | private String sub_appid; |
| | | /** |
| | | * 子商户号 |
| | | */ |
| | | private String sub_mchid; |
| | | /** |
| | | * 商户订单号 |
| | | */ |
| | | private String out_trade_no; |
| | | /** |
| | | * 交易状态描述 |
| | | */ |
| | | private String trade_state_desc; |
| | | /** |
| | | * 交易类型,枚举值: |
| | | * JSAPI:公众号支付 |
| | | * NATIVE:扫码支付 |
| | | * APP:APP支付 |
| | | * MICROPAY:付款码支付 |
| | | * MWEB:H5支付 |
| | | * FACEPAY:刷脸支付 |
| | | */ |
| | | private String trade_type; |
| | | /** |
| | | * 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用 |
| | | */ |
| | | private String attach; |
| | | /** |
| | | * 微信支付订单号 |
| | | */ |
| | | private String transaction_id; |
| | | /** |
| | | * 交易状态,枚举值: |
| | | * SUCCESS:支付成功 |
| | | * REFUND:转入退款 |
| | | * NOTPAY:未支付 |
| | | * CLOSED:已关闭 |
| | | * REVOKED:已撤销(付款码支付) |
| | | * USERPAYING:用户支付中(付款码支付) |
| | | * PAYERROR:支付失败(其他原因,如银行返回失败) |
| | | */ |
| | | private String trade_state; |
| | | /** |
| | | * 银行类型,采用字符串类型的银行标识。银行标识请参考《银行类型对照表》 |
| | | * https://pay.weixin.qq.com/wiki/doc/apiv3_partner/terms_definition/chapter1_1_3.shtml#part-6 |
| | | */ |
| | | private String bank_type; |
| | | /** |
| | | * 支付完成时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE, |
| | | * YYYY-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒, |
| | | * TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。 |
| | | * 例如:2015-05-20T13:29:35+08:00表示,北京时间2015年5月20日 13点29分35秒。 |
| | | * 示例值:2018-06-08T10:34:56+08:00 |
| | | */ |
| | | private String success_time; |
| | | /** |
| | | * 支付者信息 |
| | | */ |
| | | private Payer payer; |
| | | |
| | | /** |
| | | * 支付者 |
| | | */ |
| | | private Payer combine_payer_info; |
| | | |
| | | /** |
| | | * 订单金额信息 |
| | | */ |
| | | private Amount amount; |
| | | /** |
| | | * 场景信息 |
| | | */ |
| | | private SceneInfo scene_info; |
| | | /** |
| | | * 优惠功能,享受优惠时返回该字段 |
| | | */ |
| | | private List<PromotionDetail> promotion_detail; |
| | | |
| | | @Builder |
| | | @NoArgsConstructor |
| | | @AllArgsConstructor |
| | | @Data |
| | | public static class Amount implements Serializable{ |
| | | /** |
| | | * 用户支付金额 |
| | | */ |
| | | private int payer_total; |
| | | /** |
| | | * 总金额 |
| | | */ |
| | | private int total; |
| | | /** |
| | | * 标价金额 |
| | | */ |
| | | private int total_amount; |
| | | /** |
| | | * 现金支付金额 |
| | | */ |
| | | private int payer_amount; |
| | | private String currency; |
| | | private String payer_currency; |
| | | } |
| | | |
| | | @Builder |
| | | @NoArgsConstructor |
| | | @AllArgsConstructor |
| | | @Data |
| | | public static class GoodsDetail implements Serializable{ |
| | | private String goods_id; |
| | | private int quantity; |
| | | private int unit_price; |
| | | private int discount_amount; |
| | | private String goods_remark; |
| | | |
| | | } |
| | | |
| | | @Builder |
| | | @NoArgsConstructor |
| | | @AllArgsConstructor |
| | | @Data |
| | | public static class Payer implements Serializable{ |
| | | private String openid; |
| | | private String sp_openid; |
| | | private String sub_openid; |
| | | } |
| | | |
| | | @Builder |
| | | @NoArgsConstructor |
| | | @AllArgsConstructor |
| | | @Data |
| | | public static class PromotionDetail implements Serializable{ |
| | | private String coupon_id; |
| | | private String name; |
| | | private String scope; |
| | | private String type; |
| | | private int amount; |
| | | private String stock_id; |
| | | private int wechatpay_contribute; |
| | | |
| | | private int merchant_contribute; |
| | | private int other_contribute; |
| | | private String currency; |
| | | |
| | | private List<GoodsDetail> goods_detail; |
| | | |
| | | } |
| | | |
| | | @Builder |
| | | @NoArgsConstructor |
| | | @AllArgsConstructor |
| | | @Data |
| | | public static class SceneInfo implements Serializable{ |
| | | /** |
| | | * 商户端设备号 |
| | | */ |
| | | private String device_id; |
| | | } |
| | | |
| | | |
| | | @Builder |
| | | @NoArgsConstructor |
| | | @AllArgsConstructor |
| | | @Data |
| | | public static class SubOrders implements Serializable{ |
| | | private String mchid; |
| | | private String trade_type; |
| | | private String trade_state; |
| | | private String trade_state_desc; |
| | | private String bank_type; |
| | | private String attach; |
| | | private String success_time; |
| | | private String transaction_id; |
| | | private String out_trade_no; |
| | | private String sub_mchid; |
| | | private Amount amount; |
| | | /** |
| | | * 优惠功能,享受优惠时返回该字段 |
| | | */ |
| | | private List<PromotionDetail> promotion_detail; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.ruoyi.system.wxPay.utils; |
| | | |
| | | |
| | | import com.google.gson.annotations.SerializedName; |
| | | import okhttp3.*; |
| | | |
| | | import java.io.IOException; |
| | | import java.io.UncheckedIOException; |
| | | import java.security.PrivateKey; |
| | | import java.security.PublicKey; |
| | | |
| | | /** |
| | | * JSAPI下单 |
| | | */ |
| | | public class JsapiPrepay { |
| | | private static String HOST = "https://api.mch.weixin.qq.com"; |
| | | private static String METHOD = "POST"; |
| | | private static String PATH = "/v3/pay/transactions/jsapi"; |
| | | private final String mchId; |
| | | private final String certificateSerialNo; |
| | | private final PrivateKey privateKey; |
| | | private final String wechatPayPublicKeyId; |
| | | private final PublicKey wechatPayPublicKey; |
| | | |
| | | public JsapiPrepay(String mchId, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) { |
| | | this.mchId = mchId; |
| | | this.certificateSerialNo = certificateSerialNo; |
| | | this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath); |
| | | this.wechatPayPublicKeyId = wechatPayPublicKeyId; |
| | | this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath); |
| | | } |
| | | |
| | | public DirectAPIv3JsapiPrepayResponse run(DirectAPIv3JsapiPrepayRequest request) { |
| | | String uri = PATH; |
| | | String reqBody = WXPayUtility.toJson(request); |
| | | |
| | | Request.Builder reqBuilder = new Request.Builder().url(HOST + uri); |
| | | reqBuilder.addHeader("Accept", "application/json"); |
| | | reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId); |
| | | reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchId, certificateSerialNo,privateKey, METHOD, uri, reqBody)); |
| | | reqBuilder.addHeader("Content-Type", "application/json"); |
| | | RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody); |
| | | reqBuilder.method(METHOD, requestBody); |
| | | Request httpRequest = reqBuilder.build(); |
| | | |
| | | // 发送HTTP请求 |
| | | OkHttpClient client = new OkHttpClient.Builder().build(); |
| | | try (Response httpResponse = client.newCall(httpRequest).execute()) { |
| | | String respBody = WXPayUtility.extractBody(httpResponse); |
| | | if (httpResponse.code() >= 200 && httpResponse.code() < 300) { |
| | | // 2XX 成功,验证应答签名 |
| | | WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey, |
| | | httpResponse.headers(), respBody); |
| | | |
| | | // 从HTTP应答报文构建返回数据 |
| | | return WXPayUtility.fromJson(respBody, DirectAPIv3JsapiPrepayResponse.class); |
| | | } else { |
| | | throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers()); |
| | | } |
| | | } catch (IOException e) { |
| | | throw new UncheckedIOException("Sending request to " + uri + " failed.", e); |
| | | } |
| | | } |
| | | public static class DirectAPIv3JsapiPrepayRequest { |
| | | @SerializedName("appid") |
| | | public String appid; |
| | | |
| | | @SerializedName("mchid") |
| | | public String mchid; |
| | | |
| | | @SerializedName("description") |
| | | public String description; |
| | | |
| | | @SerializedName("out_trade_no") |
| | | public String outTradeNo; |
| | | |
| | | @SerializedName("time_expire") |
| | | public String timeExpire; |
| | | |
| | | @SerializedName("attach") |
| | | public String attach; |
| | | |
| | | @SerializedName("notify_url") |
| | | public String notifyUrl; |
| | | |
| | | @SerializedName("goods_tag") |
| | | public String goodsTag; |
| | | |
| | | @SerializedName("support_fapiao") |
| | | public Boolean supportFapiao; |
| | | |
| | | @SerializedName("amount") |
| | | public CommonAmountInfo amount; |
| | | |
| | | @SerializedName("payer") |
| | | public JsapiReqPayerInfo payer; |
| | | |
| | | } |
| | | |
| | | public static class DirectAPIv3JsapiPrepayResponse { |
| | | @SerializedName("prepay_id") |
| | | public String prepayId; |
| | | } |
| | | |
| | | public static class CommonAmountInfo { |
| | | @SerializedName("total") |
| | | public Long total; |
| | | |
| | | @SerializedName("currency") |
| | | public String currency; |
| | | } |
| | | |
| | | public static class JsapiReqPayerInfo { |
| | | @SerializedName("openid") |
| | | public String openid; |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | package com.ruoyi.system.wxPay.utils; |
| | | |
| | | import com.google.gson.*; |
| | | import com.google.gson.annotations.Expose; |
| | | import com.wechat.pay.java.core.util.GsonUtil; |
| | | import okhttp3.Headers; |
| | | import okhttp3.Response; |
| | | import okio.BufferedSource; |
| | | |
| | | import javax.crypto.BadPaddingException; |
| | | import javax.crypto.Cipher; |
| | | import javax.crypto.IllegalBlockSizeException; |
| | | import javax.crypto.NoSuchPaddingException; |
| | | import java.io.IOException; |
| | | import java.io.UncheckedIOException; |
| | | import java.io.UnsupportedEncodingException; |
| | | import java.net.URLEncoder; |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.nio.file.Files; |
| | | import java.nio.file.Paths; |
| | | import java.security.*; |
| | | import java.security.spec.InvalidKeySpecException; |
| | | import java.security.spec.PKCS8EncodedKeySpec; |
| | | import java.security.spec.X509EncodedKeySpec; |
| | | import java.time.DateTimeException; |
| | | import java.time.Duration; |
| | | import java.time.Instant; |
| | | import java.util.Base64; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | |
| | | public class WXPayUtility { |
| | | private static final Gson gson = new GsonBuilder() |
| | | .disableHtmlEscaping() |
| | | .addSerializationExclusionStrategy(new ExclusionStrategy() { |
| | | @Override |
| | | public boolean shouldSkipField(FieldAttributes fieldAttributes) { |
| | | final Expose expose = fieldAttributes.getAnnotation(Expose.class); |
| | | return expose != null && !expose.serialize(); |
| | | } |
| | | |
| | | @Override |
| | | public boolean shouldSkipClass(Class<?> aClass) { |
| | | return false; |
| | | } |
| | | }) |
| | | .addDeserializationExclusionStrategy(new ExclusionStrategy() { |
| | | @Override |
| | | public boolean shouldSkipField(FieldAttributes fieldAttributes) { |
| | | final Expose expose = fieldAttributes.getAnnotation(Expose.class); |
| | | return expose != null && !expose.deserialize(); |
| | | } |
| | | |
| | | @Override |
| | | public boolean shouldSkipClass(Class<?> aClass) { |
| | | return false; |
| | | } |
| | | }) |
| | | .create(); |
| | | private static final char[] SYMBOLS = |
| | | "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); |
| | | private static final SecureRandom random = new SecureRandom(); |
| | | |
| | | /** |
| | | * 将 Object 转换为 JSON 字符串 |
| | | */ |
| | | public static String toJson(Object object) { |
| | | return gson.toJson(object); |
| | | } |
| | | |
| | | /** |
| | | * 将 JSON 字符串解析为特定类型的实例 |
| | | */ |
| | | public static <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException { |
| | | return gson.fromJson(json, classOfT); |
| | | } |
| | | |
| | | /** |
| | | * 从公私钥文件路径中读取文件内容 |
| | | * |
| | | * @param keyPath 文件路径 |
| | | * @return 文件内容 |
| | | */ |
| | | private static String readKeyStringFromPath(String keyPath) { |
| | | try { |
| | | return new String(Files.readAllBytes(Paths.get(keyPath)), StandardCharsets.UTF_8); |
| | | } catch (IOException e) { |
| | | throw new UncheckedIOException(e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 读取 PKCS#8 格式的私钥字符串并加载为私钥对象 |
| | | * |
| | | * @param keyString 私钥文件内容,以 -----BEGIN PRIVATE KEY----- 开头 |
| | | * @return PrivateKey 对象 |
| | | */ |
| | | public static PrivateKey loadPrivateKeyFromString(String keyString) { |
| | | try { |
| | | keyString = keyString.replace("-----BEGIN PRIVATE KEY-----", "") |
| | | .replace("-----END PRIVATE KEY-----", "") |
| | | .replaceAll("\\s+", ""); |
| | | return KeyFactory.getInstance("RSA").generatePrivate( |
| | | new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyString))); |
| | | } catch (NoSuchAlgorithmException e) { |
| | | throw new UnsupportedOperationException(e); |
| | | } catch (InvalidKeySpecException e) { |
| | | throw new IllegalArgumentException(e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 从 PKCS#8 格式的私钥文件中加载私钥 |
| | | * |
| | | * @param keyPath 私钥文件路径 |
| | | * @return PrivateKey 对象 |
| | | */ |
| | | public static PrivateKey loadPrivateKeyFromPath(String keyPath) { |
| | | return loadPrivateKeyFromString(readKeyStringFromPath(keyPath)); |
| | | } |
| | | |
| | | /** |
| | | * 读取 PKCS#8 格式的公钥字符串并加载为公钥对象 |
| | | * |
| | | * @param keyString 公钥文件内容,以 -----BEGIN PUBLIC KEY----- 开头 |
| | | * @return PublicKey 对象 |
| | | */ |
| | | public static PublicKey loadPublicKeyFromString(String keyString) { |
| | | try { |
| | | keyString = keyString.replace("-----BEGIN PUBLIC KEY-----", "") |
| | | .replace("-----END PUBLIC KEY-----", "") |
| | | .replaceAll("\\s+", ""); |
| | | return KeyFactory.getInstance("RSA").generatePublic( |
| | | new X509EncodedKeySpec(Base64.getDecoder().decode(keyString))); |
| | | } catch (NoSuchAlgorithmException e) { |
| | | throw new UnsupportedOperationException(e); |
| | | } catch (InvalidKeySpecException e) { |
| | | throw new IllegalArgumentException(e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 从 PKCS#8 格式的公钥文件中加载公钥 |
| | | * |
| | | * @param keyPath 公钥文件路径 |
| | | * @return PublicKey 对象 |
| | | */ |
| | | public static PublicKey loadPublicKeyFromPath(String keyPath) { |
| | | return loadPublicKeyFromString(readKeyStringFromPath(keyPath)); |
| | | } |
| | | |
| | | /** |
| | | * 创建指定长度的随机字符串,字符集为[0-9a-zA-Z],可用于安全相关用途 |
| | | */ |
| | | public static String createNonce(int length) { |
| | | char[] buf = new char[length]; |
| | | for (int i = 0; i < length; ++i) { |
| | | buf[i] = SYMBOLS[random.nextInt(SYMBOLS.length)]; |
| | | } |
| | | return new String(buf); |
| | | } |
| | | |
| | | /** |
| | | * 使用公钥按照 RSA_PKCS1_OAEP_PADDING 算法进行加密 |
| | | * |
| | | * @param publicKey 加密用公钥对象 |
| | | * @param plaintext 待加密明文 |
| | | * @return 加密后密文 |
| | | */ |
| | | public static String encrypt(PublicKey publicKey, String plaintext) { |
| | | final String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; |
| | | |
| | | try { |
| | | Cipher cipher = Cipher.getInstance(transformation); |
| | | cipher.init(Cipher.ENCRYPT_MODE, publicKey); |
| | | return Base64.getEncoder().encodeToString(cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8))); |
| | | } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { |
| | | throw new IllegalArgumentException("The current Java environment does not support " + transformation, e); |
| | | } catch (InvalidKeyException e) { |
| | | throw new IllegalArgumentException("RSA encryption using an illegal publicKey", e); |
| | | } catch (BadPaddingException | IllegalBlockSizeException e) { |
| | | throw new IllegalArgumentException("Plaintext is too long", e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 使用私钥按照指定算法进行签名 |
| | | * |
| | | * @param message 待签名串 |
| | | * @param algorithm 签名算法,如 SHA256withRSA |
| | | * @param privateKey 签名用私钥对象 |
| | | * @return 签名结果 |
| | | */ |
| | | public static String sign(String message, String algorithm, PrivateKey privateKey) { |
| | | byte[] sign; |
| | | try { |
| | | Signature signature = Signature.getInstance(algorithm); |
| | | signature.initSign(privateKey); |
| | | signature.update(message.getBytes(StandardCharsets.UTF_8)); |
| | | sign = signature.sign(); |
| | | } catch (NoSuchAlgorithmException e) { |
| | | throw new UnsupportedOperationException("The current Java environment does not support " + algorithm, e); |
| | | } catch (InvalidKeyException e) { |
| | | throw new IllegalArgumentException(algorithm + " signature uses an illegal privateKey.", e); |
| | | } catch (SignatureException e) { |
| | | throw new RuntimeException("An error occurred during the sign process.", e); |
| | | } |
| | | return Base64.getEncoder().encodeToString(sign); |
| | | } |
| | | |
| | | /** |
| | | * 使用公钥按照特定算法验证签名 |
| | | * |
| | | * @param message 待签名串 |
| | | * @param signature 待验证的签名内容 |
| | | * @param algorithm 签名算法,如:SHA256withRSA |
| | | * @param publicKey 验签用公钥对象 |
| | | * @return 签名验证是否通过 |
| | | */ |
| | | public static boolean verify(String message, String signature, String algorithm, |
| | | PublicKey publicKey) { |
| | | try { |
| | | Signature sign = Signature.getInstance(algorithm); |
| | | sign.initVerify(publicKey); |
| | | sign.update(message.getBytes(StandardCharsets.UTF_8)); |
| | | return sign.verify(Base64.getDecoder().decode(signature)); |
| | | } catch (SignatureException e) { |
| | | return false; |
| | | } catch (InvalidKeyException e) { |
| | | throw new IllegalArgumentException("verify uses an illegal publickey.", e); |
| | | } catch (NoSuchAlgorithmException e) { |
| | | throw new UnsupportedOperationException("The current Java environment does not support" + algorithm, e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 根据微信支付APIv3请求签名规则构造 Authorization 签名 |
| | | * |
| | | * @param mchid 商户号 |
| | | * @param certificateSerialNo 商户API证书序列号 |
| | | * @param privateKey 商户API证书私钥 |
| | | * @param method 请求接口的HTTP方法,请使用全大写表述,如 GET、POST、PUT、DELETE |
| | | * @param uri 请求接口的URL |
| | | * @param body 请求接口的Body |
| | | * @return 构造好的微信支付APIv3 Authorization 头 |
| | | */ |
| | | public static String buildAuthorization(String mchid, String certificateSerialNo, |
| | | PrivateKey privateKey, |
| | | String method, String uri, String body) { |
| | | String nonce = createNonce(32); |
| | | long timestamp = Instant.now().getEpochSecond(); |
| | | |
| | | String message = String.format("%s\n%s\n%d\n%s\n%s\n", method, uri, timestamp, nonce, |
| | | body == null ? "" : body); |
| | | |
| | | String signature = sign(message, "SHA256withRSA", privateKey); |
| | | |
| | | return String.format( |
| | | "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",signature=\"%s\"," + |
| | | "timestamp=\"%d\",serial_no=\"%s\"", |
| | | mchid, nonce, signature, timestamp, certificateSerialNo); |
| | | } |
| | | |
| | | /** |
| | | * 对参数进行 URL 编码 |
| | | * |
| | | * @param content 参数内容 |
| | | * @return 编码后的内容 |
| | | */ |
| | | public static String urlEncode(String content) { |
| | | try { |
| | | return URLEncoder.encode(content, StandardCharsets.UTF_8.name()); |
| | | } catch (UnsupportedEncodingException e) { |
| | | throw new RuntimeException(e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 对参数Map进行 URL 编码,生成 QueryString |
| | | * |
| | | * @param params Query参数Map |
| | | * @return QueryString |
| | | */ |
| | | public static String urlEncode(Map<String, Object> params) { |
| | | if (params == null || params.isEmpty()) { |
| | | return ""; |
| | | } |
| | | |
| | | int index = 0; |
| | | StringBuilder result = new StringBuilder(); |
| | | for (Map.Entry<String, Object> entry : params.entrySet()) { |
| | | result.append(entry.getKey()) |
| | | .append("=") |
| | | .append(urlEncode(entry.getValue().toString())); |
| | | index++; |
| | | if (index < params.size()) { |
| | | result.append("&"); |
| | | } |
| | | } |
| | | return result.toString(); |
| | | } |
| | | |
| | | /** |
| | | * 从应答中提取 Body |
| | | * |
| | | * @param response HTTP 请求应答对象 |
| | | * @return 应答中的Body内容,Body为空时返回空字符串 |
| | | */ |
| | | public static String extractBody(Response response) { |
| | | if (response.body() == null) { |
| | | return ""; |
| | | } |
| | | |
| | | try { |
| | | BufferedSource source = response.body().source(); |
| | | return source.readUtf8(); |
| | | } catch (IOException e) { |
| | | throw new RuntimeException(String.format("An error occurred during reading response body. Status: %d", response.code()), e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 根据微信支付APIv3应答验签规则对应答签名进行验证,验证不通过时抛出异常 |
| | | * |
| | | * @param wechatpayPublicKeyId 微信支付公钥ID |
| | | * @param wechatpayPublicKey 微信支付公钥对象 |
| | | * @param headers 微信支付应答 Header 列表 |
| | | * @param body 微信支付应答 Body |
| | | */ |
| | | public static void validateResponse(String wechatpayPublicKeyId, PublicKey wechatpayPublicKey, |
| | | Headers headers, |
| | | String body) { |
| | | String timestamp = headers.get("Wechatpay-Timestamp"); |
| | | try { |
| | | Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp)); |
| | | // 拒绝过期请求 |
| | | if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) { |
| | | throw new IllegalArgumentException( |
| | | String.format("Validate http response,timestamp[%s] of httpResponse is expires, " |
| | | + "request-id[%s]", |
| | | timestamp, headers.get("Request-ID"))); |
| | | } |
| | | } catch (DateTimeException | NumberFormatException e) { |
| | | throw new IllegalArgumentException( |
| | | String.format("Validate http response,timestamp[%s] of httpResponse is invalid, " + |
| | | "request-id[%s]", timestamp, |
| | | headers.get("Request-ID"))); |
| | | } |
| | | String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"), |
| | | body == null ? "" : body); |
| | | String serialNumber = headers.get("Wechatpay-Serial"); |
| | | if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) { |
| | | throw new IllegalArgumentException( |
| | | String.format("Invalid Wechatpay-Serial, Local: %s, Remote: %s", wechatpayPublicKeyId, |
| | | serialNumber)); |
| | | } |
| | | String signature = headers.get("Wechatpay-Signature"); |
| | | |
| | | boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey); |
| | | if (!success) { |
| | | throw new IllegalArgumentException( |
| | | String.format("Validate response failed,the WechatPay signature is incorrect.%n" |
| | | + "Request-ID[%s]\tresponseHeader[%s]\tresponseBody[%.1024s]", |
| | | headers.get("Request-ID"), headers, body)); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 微信支付API错误异常,发送HTTP请求成功,但返回状态码不是 2XX 时抛出本异常 |
| | | */ |
| | | public static class ApiException extends RuntimeException { |
| | | private static final long serialVersionUID = 2261086748874802175L; |
| | | |
| | | private final int statusCode; |
| | | private final String body; |
| | | private final Headers headers; |
| | | private final String errorCode; |
| | | private final String errorMessage; |
| | | |
| | | public ApiException(int statusCode, String body, Headers headers) { |
| | | super(String.format("微信支付API访问失败,StatusCode: [%s], Body: [%s], Headers: [%s]", statusCode, body, headers)); |
| | | this.statusCode = statusCode; |
| | | this.body = body; |
| | | this.headers = headers; |
| | | |
| | | if (body != null && !body.isEmpty()) { |
| | | JsonElement code; |
| | | JsonElement message; |
| | | |
| | | try { |
| | | JsonObject jsonObject = GsonUtil.getGson().fromJson(body, JsonObject.class); |
| | | code = jsonObject.get("code"); |
| | | message = jsonObject.get("message"); |
| | | } catch (JsonSyntaxException ignored) { |
| | | code = null; |
| | | message = null; |
| | | } |
| | | this.errorCode = code == null ? null : code.getAsString(); |
| | | this.errorMessage = message == null ? null : message.getAsString(); |
| | | } else { |
| | | this.errorCode = null; |
| | | this.errorMessage = null; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 获取 HTTP 应答状态码 |
| | | */ |
| | | public int getStatusCode() { |
| | | return statusCode; |
| | | } |
| | | |
| | | /** |
| | | * 获取 HTTP 应答包体内容 |
| | | */ |
| | | public String getBody() { |
| | | return body; |
| | | } |
| | | |
| | | /** |
| | | * 获取 HTTP 应答 Header |
| | | */ |
| | | public Headers getHeaders() { |
| | | return headers; |
| | | } |
| | | |
| | | /** |
| | | * 获取 错误码 (错误应答中的 code 字段) |
| | | */ |
| | | public String getErrorCode() { |
| | | return errorCode; |
| | | } |
| | | |
| | | /** |
| | | * 获取 错误消息 (错误应答中的 message 字段) |
| | | */ |
| | | public String getErrorMessage() { |
| | | return errorMessage; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.ruoyi.system.wxPay.utils; |
| | | |
| | | import com.fasterxml.jackson.core.type.TypeReference; |
| | | import com.ruoyi.system.utils.wx.tools.WebUtils; |
| | | import com.ruoyi.system.utils.wx.tools.WxJsonUtils; |
| | | import com.ruoyi.system.utils.wx.tools.WxUtils; |
| | | import com.ruoyi.system.wxPay.model.WxCloseOrderModel; |
| | | import com.ruoyi.system.wxPay.model.WxPaymentInfoModel; |
| | | import com.ruoyi.system.wxPay.model.WxPaymentRefundModel; |
| | | import com.ruoyi.system.wxPay.resp.NotifyV3PayDecodeRespBody; |
| | | import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner; |
| | | import com.wechat.pay.contrib.apache.httpclient.auth.Verifier; |
| | | import com.wechat.pay.contrib.apache.httpclient.notification.Notification; |
| | | import com.wechat.pay.contrib.apache.httpclient.notification.NotificationHandler; |
| | | import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.http.client.config.RequestConfig; |
| | | import org.apache.http.client.methods.*; |
| | | import org.apache.http.conn.ConnectTimeoutException; |
| | | import org.apache.http.entity.StringEntity; |
| | | import org.apache.http.impl.client.CloseableHttpClient; |
| | | import org.apache.http.util.EntityUtils; |
| | | import org.springframework.util.StringUtils; |
| | | |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import java.io.IOException; |
| | | import java.io.PrintWriter; |
| | | import java.net.SocketTimeoutException; |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | import java.util.stream.Collectors; |
| | | import java.util.stream.Stream; |
| | | |
| | | import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*; |
| | | |
| | | /** |
| | | * @author xiaochen |
| | | * @ClassName WxWifiV3Pay |
| | | * @Description |
| | | * @date 2021-11-13 21:10 |
| | | */ |
| | | @Slf4j |
| | | public abstract class WxAbstractPay { |
| | | /** |
| | | * 请求成功相应码 |
| | | */ |
| | | private static final int STATUS_CODE = 200; |
| | | /** |
| | | * 请求成功相应码 |
| | | */ |
| | | private static final int OTHER_STATUS_CODE = 204; |
| | | /** |
| | | * 请求根地址 |
| | | */ |
| | | private static final String HOST = "https://api.mch.weixin.qq.com"; |
| | | |
| | | private static RuntimeException parameterError(String message, Object... args) { |
| | | message = String.format(message, args); |
| | | return new IllegalArgumentException("parameter error: " + message); |
| | | } |
| | | |
| | | /** |
| | | * 封装基础数据 |
| | | * |
| | | * @param requestBody |
| | | * @param notifyUrl |
| | | * @return |
| | | */ |
| | | protected String buildBaseParam(WxPaymentInfoModel requestBody, String notifyUrl) { |
| | | // 封装基础数据 |
| | | requestBody.setNotify_url(notifyUrl + requestBody.getNotify_url()); |
| | | String reqBody = WxJsonUtils.toJsonString(requestBody); |
| | | return reqBody; |
| | | } |
| | | |
| | | /** |
| | | * 微信调起支付参数 |
| | | * 返回参数如有不理解 请访问微信官方文档 |
| | | * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_1_4.shtml |
| | | * |
| | | * @param prepayId 微信下单返回的prepay_id |
| | | * @param appId 应用ID(appid) |
| | | * @return 当前调起支付所需的参数 |
| | | * @throws Exception |
| | | */ |
| | | protected Map<String, Object> wxTuneUp(PrivateKeySigner privateKeySigner, String appId, String prepayId) { |
| | | String timeStamp = String.valueOf(System.currentTimeMillis() / 1000); |
| | | String nonceStr = WxUtils.generateNonceStr(); |
| | | String packageStr = "prepay_id=" + prepayId; |
| | | //加载签名 |
| | | String signStr = Stream.of(appId, timeStamp, nonceStr, packageStr).collect(Collectors.joining("\n", "", "\n")); |
| | | String packageSign = privateKeySigner.sign(signStr.getBytes(StandardCharsets.UTF_8)).getSign(); |
| | | Map<String, Object> map = new HashMap<>(6); |
| | | map.put("appId", appId); |
| | | map.put("timeStamp", timeStamp); |
| | | map.put("nonceStr", nonceStr); |
| | | map.put("package", packageStr); |
| | | map.put("signType", "RSA"); |
| | | map.put("paySign", packageSign); |
| | | return map; |
| | | } |
| | | |
| | | /** |
| | | * 构建方法请求 |
| | | * |
| | | * @param uri |
| | | * @param socketTimeout |
| | | * @param connectTimeout |
| | | * @return |
| | | */ |
| | | protected HttpGet requestGet(String uri, int socketTimeout, int connectTimeout) { |
| | | //请求URL |
| | | HttpGet httpGet = new HttpGet(HOST + uri); |
| | | RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout) |
| | | .setConnectTimeout(connectTimeout).build(); |
| | | httpGet.setConfig(requestConfig); |
| | | httpGet.setHeader("Content-type", "application/json"); |
| | | httpGet.setHeader("Accept", "application/json"); |
| | | return httpGet; |
| | | } |
| | | |
| | | /** |
| | | * 构建方法请求 |
| | | * |
| | | * @param uri |
| | | * @param socketTimeout |
| | | * @param connectTimeout |
| | | * @param reqdata |
| | | * @return |
| | | */ |
| | | protected HttpPost requestPost(String uri, int socketTimeout, int connectTimeout, String reqdata) { |
| | | //请求URL |
| | | HttpPost httpPost = new HttpPost(HOST + uri); |
| | | RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout) |
| | | .setConnectTimeout(connectTimeout).build(); |
| | | httpPost.setConfig(requestConfig); |
| | | StringEntity entity = new StringEntity(reqdata, StandardCharsets.UTF_8); |
| | | entity.setContentType("application/json"); |
| | | httpPost.setEntity(entity); |
| | | httpPost.setHeader("Accept", "application/json"); |
| | | return httpPost; |
| | | } |
| | | |
| | | public abstract <T> T verifyNotify(HttpServletRequest request, TypeReference<T> valueTypeRef) throws Exception; |
| | | |
| | | /** |
| | | * 接收回调 |
| | | * |
| | | * @param request |
| | | * @return |
| | | * @throws Exception |
| | | */ |
| | | public <T> T verifyNotify(HttpServletRequest request, Verifier verifier, String apiKey, TypeReference<T> valueTypeRef) throws Exception { |
| | | String body = WxUtils.streamBodyByReceive(request); |
| | | String requestId = request.getHeader(REQUEST_ID); |
| | | String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP}; |
| | | String value; |
| | | // 验签必须参数检验 |
| | | for (String headerName : headers) { |
| | | value = request.getHeader(headerName); |
| | | if (value == null || "".equals(value)) { |
| | | throw parameterError("empty [%s], request-id=[%s]", headerName, requestId); |
| | | } |
| | | } |
| | | String serial = request.getHeader(WECHAT_PAY_SERIAL); |
| | | String signature = request.getHeader(WECHAT_PAY_SIGNATURE); |
| | | String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP); |
| | | String nonce = request.getHeader(WECHAT_PAY_NONCE); |
| | | // 构建request,传入必要参数 |
| | | NotificationRequest notificationRequest = new NotificationRequest.Builder().withSerialNumber(serial) |
| | | .withNonce(nonce) |
| | | .withTimestamp(timestamp) |
| | | .withSignature(signature) |
| | | .withBody(body) |
| | | .build(); |
| | | NotificationHandler handler = new NotificationHandler(verifier, apiKey.getBytes(StandardCharsets.UTF_8)); |
| | | // 验签和解析请求体 |
| | | Notification notification = handler.parse(notificationRequest); |
| | | assert notification != null; |
| | | T respBody = WxJsonUtils.parseObject(notification.getDecryptData(), valueTypeRef); |
| | | return respBody; |
| | | } |
| | | |
| | | /** |
| | | * 订单查询 |
| | | * |
| | | * @param httpClient |
| | | * @param socketTimeout |
| | | * @param connectTimeout |
| | | * @param url |
| | | * @return com.abl.biz.center.payment.wx.v3.NotifyV3PayDecodeRespBody |
| | | * @author xiaochen |
| | | * @date 2021-12-20 17:12 |
| | | */ |
| | | protected NotifyV3PayDecodeRespBody query(CloseableHttpClient httpClient, int socketTimeout, int connectTimeout, String url) { |
| | | //请求URL |
| | | HttpGet httpGet = requestGet( |
| | | url |
| | | , socketTimeout |
| | | , connectTimeout); |
| | | CloseableHttpResponse response = null; |
| | | try { |
| | | response = httpClient.execute(httpGet); |
| | | int statusCode = response.getStatusLine().getStatusCode(); |
| | | String respBodyStr = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); |
| | | if (WxUtils.getLogger().isDebugEnabled()) { |
| | | WxUtils.debug("请求成功:{}", respBodyStr); |
| | | } |
| | | if(404 == statusCode){ |
| | | return null; |
| | | } |
| | | // 成功相应 |
| | | if (STATUS_CODE == statusCode || OTHER_STATUS_CODE == statusCode ) { |
| | | NotifyV3PayDecodeRespBody body = WxJsonUtils.parseObject(respBodyStr, NotifyV3PayDecodeRespBody.class); |
| | | return body; |
| | | } else { |
| | | WxUtils.error("failed,resp code = {},return body = {}", statusCode, respBodyStr); |
| | | throw new RuntimeException(respBodyStr); |
| | | } |
| | | } catch (ConnectTimeoutException e) { |
| | | e.printStackTrace(); |
| | | throw new RuntimeException("接口超时"); |
| | | } catch (SocketTimeoutException e) { |
| | | e.printStackTrace(); |
| | | throw new RuntimeException("读取接口数据超时"); |
| | | } catch (IOException e) { |
| | | e.printStackTrace(); |
| | | throw new RuntimeException("接口请求失败,请尝试检查网络环境或请求接口是否能正常访问"); |
| | | } finally { |
| | | // 关闭响应 |
| | | try { |
| | | if (response != null) { |
| | | //关闭结果集 |
| | | response.getEntity().getContent().close(); |
| | | response.close(); |
| | | } |
| | | } catch (IOException e) { |
| | | throw new RuntimeException("关闭流异常"); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 子级实现 |
| | | * |
| | | * @param out_trade_no |
| | | * @return |
| | | */ |
| | | public abstract NotifyV3PayDecodeRespBody query(String out_trade_no); |
| | | |
| | | |
| | | /** |
| | | * 订单退款 |
| | | * |
| | | * @param refundModel |
| | | * @return |
| | | */ |
| | | public abstract Map<String, Object> refund(WxPaymentRefundModel refundModel); |
| | | |
| | | |
| | | |
| | | public abstract String close(String out_trade_no); |
| | | |
| | | /** |
| | | * 订单退款 |
| | | * |
| | | * @param httpClient |
| | | * @param uri |
| | | * @param httpReadTimeoutMs |
| | | * @param httpConnectTimeoutMs |
| | | * @param refundModel |
| | | * @return |
| | | */ |
| | | public Map<String, Object> refund(CloseableHttpClient httpClient, |
| | | String uri, |
| | | int httpReadTimeoutMs, |
| | | int httpConnectTimeoutMs, |
| | | WxPaymentRefundModel refundModel) { |
| | | String reqBody = WxJsonUtils.toJsonString(refundModel); |
| | | //请求URL |
| | | HttpEntityEnclosingRequestBase httpPost = requestPost( |
| | | uri |
| | | , httpReadTimeoutMs |
| | | , httpConnectTimeoutMs, reqBody); |
| | | String repBody = result(httpClient, httpPost); |
| | | Map<String, Object> body = WxJsonUtils.parseObject(repBody, Map.class); |
| | | return body; |
| | | } |
| | | |
| | | /** |
| | | * 请求结果 |
| | | * |
| | | * @param request |
| | | * @return |
| | | */ |
| | | protected String result(CloseableHttpClient httpClient, HttpRequestBase request) { |
| | | CloseableHttpResponse response = null; |
| | | try { |
| | | response = httpClient.execute(request); |
| | | int statusCode = response.getStatusLine().getStatusCode(); |
| | | String respBodyStr = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); |
| | | if (WxUtils.getLogger().isDebugEnabled()) { |
| | | WxUtils.debug("请求成功:{}", respBodyStr); |
| | | } |
| | | // 成功相应 |
| | | if (STATUS_CODE == statusCode || OTHER_STATUS_CODE == statusCode) { |
| | | return respBodyStr; |
| | | } else { |
| | | WxUtils.error("failed,resp code = {},return body = {}", statusCode, respBodyStr); |
| | | throw new RuntimeException(respBodyStr); |
| | | } |
| | | } catch (ConnectTimeoutException e) { |
| | | e.printStackTrace(); |
| | | throw new RuntimeException("接口超时"); |
| | | } catch (SocketTimeoutException e) { |
| | | e.printStackTrace(); |
| | | throw new RuntimeException("读取接口数据超时"); |
| | | } catch (IOException e) { |
| | | e.printStackTrace(); |
| | | throw new RuntimeException("接口请求失败,请尝试检查网络环境或请求接口是否能正常访问"); |
| | | } finally { |
| | | // 关闭响应 |
| | | try { |
| | | if (response != null) { |
| | | //关闭结果集 |
| | | response.getEntity().getContent().close(); |
| | | response.close(); |
| | | } |
| | | } catch (IOException e) { |
| | | throw new RuntimeException("关闭流异常"); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 微信结果确认应答 |
| | | * 支付通知http应答码为200或204才会当作正常接收,当回调处理异常时,应答的HTTP状态码应为500,或者4xx。 |
| | | * |
| | | * @param |
| | | * @throws IOException |
| | | */ |
| | | public void ack() throws IOException { |
| | | ack(true, null); |
| | | } |
| | | |
| | | /** |
| | | * 微信结果确认应答 |
| | | * 支付通知http应答码为200或204才会当作正常接收,当回调处理异常时,应答的HTTP状态码应为500,或者4xx。 |
| | | * |
| | | * @param |
| | | * @throws IOException |
| | | */ |
| | | public void ack(boolean ackSucc, String erroMsg) throws IOException { |
| | | HttpServletResponse response = WebUtils.response(); |
| | | PrintWriter writer = response.getWriter(); |
| | | if (ackSucc) { |
| | | log.info("响应微信回调成功!"); |
| | | response.setStatus(200); |
| | | writer.write("{\"code\": \"SUCCESS\",\"message\": \"成功\"}"); |
| | | |
| | | } else { |
| | | log.info("响应微信回调失败:{}!", erroMsg); |
| | | response.setStatus(500); |
| | | writer.write("{\"code\": \"FAIL\",\"message\": " + (StringUtils.hasLength(erroMsg) ? erroMsg : "业务处理失败") + "}"); |
| | | } |
| | | // 关闭流 |
| | | if (Objects.nonNull(writer)) { |
| | | writer.close(); |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 关闭订单 |
| | | * @param httpClient |
| | | * @param uri |
| | | * @param httpReadTimeoutMs |
| | | * @param httpConnectTimeoutMs |
| | | * @param closeModel |
| | | * @return |
| | | */ |
| | | public String close(CloseableHttpClient httpClient, |
| | | String uri, |
| | | int httpReadTimeoutMs, |
| | | int httpConnectTimeoutMs, |
| | | WxCloseOrderModel closeModel) { |
| | | String reqBody = WxJsonUtils.toJsonString(closeModel); |
| | | //请求URL |
| | | HttpEntityEnclosingRequestBase httpPost = requestPost( |
| | | uri |
| | | , httpReadTimeoutMs |
| | | , httpConnectTimeoutMs, reqBody); |
| | | String repBody = result(httpClient, httpPost); |
| | | return repBody; |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | package com.ruoyi.system.wxPay.utils; |
| | | |
| | | import org.joda.time.DateTime; |
| | | import org.joda.time.DateTimeZone; |
| | | import org.springframework.util.StringUtils; |
| | | |
| | | import java.time.Instant; |
| | | import java.time.LocalDateTime; |
| | | import java.time.ZoneId; |
| | | import java.time.ZonedDateTime; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.Date; |
| | | import java.util.Objects; |
| | | import java.util.TimeZone; |
| | | |
| | | /** |
| | | * @author xiaochen |
| | | * @ClassName WxTimeUtils |
| | | * @Description |
| | | * @date 2021-12-16 16:07 |
| | | */ |
| | | public class WxTimeUtils { |
| | | /** |
| | | * 系统默认时区 |
| | | */ |
| | | private static final ZoneId ZONE = ZoneId.systemDefault(); |
| | | /** |
| | | * yyyy-MM-dd'T'HH:mm:ssxxx 比如:2020-05-23T17:06:30+08:00 0时区时末尾 为+00:00 |
| | | */ |
| | | public static final DateTimeFormatter YYYY_MM_DD_T_HH_MM_SS_XXX_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssxxx"); |
| | | /** |
| | | * yyyy-MM-dd HH:mm:ss 比如:2020-05-23 17:06:30 |
| | | */ |
| | | public static final DateTimeFormatter YYYY_MM_DD_HH_MM_SS_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZONE); |
| | | |
| | | /** |
| | | * 时间转 TimeZone |
| | | * |
| | | * @param date |
| | | * @return |
| | | * @throws Exception |
| | | */ |
| | | public static String dateToTimeZone(Date date) throws Exception { |
| | | String time; |
| | | if (date == null) { |
| | | throw new Exception("date is not null"); |
| | | } |
| | | ZonedDateTime zonedDateTime = toZonedDateTime(date); |
| | | time = format(zonedDateTime, YYYY_MM_DD_T_HH_MM_SS_XXX_FMT); |
| | | return time; |
| | | } |
| | | |
| | | /** |
| | | * Date转ZonedDateTime,时区为系统默认时区 |
| | | * |
| | | * @param date Date |
| | | * @return ZonedDateTime |
| | | */ |
| | | public static ZonedDateTime toZonedDateTime(Date date) { |
| | | Objects.requireNonNull(date, "date"); |
| | | return Instant.ofEpochMilli(date.getTime()).atZone(ZoneId.systemDefault()); |
| | | } |
| | | |
| | | /** |
| | | * 根据 formatter格式化 zonedDateTime |
| | | * |
| | | * @param zonedDateTime ZonedDateTime |
| | | * @param formatter DateTimeFormatter |
| | | * @return String |
| | | */ |
| | | public static String format(ZonedDateTime zonedDateTime, DateTimeFormatter formatter) { |
| | | Objects.requireNonNull(zonedDateTime, "zonedDateTime"); |
| | | Objects.requireNonNull(formatter, "formatter"); |
| | | return zonedDateTime.format(formatter); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * TimeZone 时间转标准时间 |
| | | * |
| | | * @param date |
| | | * @return |
| | | * @throws Exception |
| | | */ |
| | | public static String toTimeZoneStr(String date) { |
| | | String time; |
| | | if (!StringUtils.hasLength(date)) { |
| | | throw new RuntimeException("str is not null"); |
| | | } |
| | | ZonedDateTime zonedDateTime = parseToZonedDateTime(date, YYYY_MM_DD_T_HH_MM_SS_XXX_FMT); |
| | | if (zonedDateTime == null) { |
| | | throw new RuntimeException("str to zonedDateTime fail"); |
| | | } |
| | | time = zonedDateTime.format(YYYY_MM_DD_HH_MM_SS_FMT); |
| | | return time; |
| | | } |
| | | |
| | | /** |
| | | * 转date |
| | | * |
| | | * @param date |
| | | * @return |
| | | * @throws Exception |
| | | */ |
| | | public static Date toDate(String date) { |
| | | String time; |
| | | if (!StringUtils.hasLength(date)) { |
| | | throw new RuntimeException("str is not null"); |
| | | } |
| | | ZonedDateTime zonedDateTime = parseToZonedDateTime(date, YYYY_MM_DD_T_HH_MM_SS_XXX_FMT); |
| | | if (zonedDateTime == null) { |
| | | throw new RuntimeException("str to zonedDateTime fail"); |
| | | } |
| | | return Date.from(zonedDateTime.toInstant()); |
| | | } |
| | | |
| | | /** |
| | | * str --> Date |
| | | * |
| | | * @param date |
| | | * @return java.util.Date |
| | | * @author xiaochen |
| | | * @date 2022-01-20 18:20 |
| | | */ |
| | | public static Date toRfc3339Date(String date) { |
| | | DateTime dt2 = new DateTime(date); |
| | | return dt2.toDate(); |
| | | } |
| | | |
| | | /** |
| | | * 将 Date 转为 LocalDateTime |
| | | * |
| | | * @param date |
| | | * @return java.time.LocalDateTime; |
| | | */ |
| | | public static LocalDateTime dateToLocalDateTime(Date date) { |
| | | return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * str --> Date |
| | | * |
| | | * @param date |
| | | * @return java.util.Date |
| | | * @author xiaochen |
| | | * @date 2022-01-20 18:20 |
| | | */ |
| | | public static String toRfc3339Str(Date date) { |
| | | DateTime dt1 = new DateTime(new Date(), DateTimeZone.forTimeZone(TimeZone.getTimeZone("Asia/Shanghai"))); |
| | | return dt1.toString(); |
| | | } |
| | | |
| | | /** |
| | | * 根据 formatter解析为 ZonedDateTime |
| | | * |
| | | * @param text 待解析字符串 |
| | | * @param formatter DateTimeFormatter |
| | | * @return ZonedDateTime |
| | | */ |
| | | public static ZonedDateTime parseToZonedDateTime(String text, DateTimeFormatter formatter) { |
| | | return ZonedDateTime.parse(text, formatter); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.ruoyi.system.wxPay.utils; |
| | | |
| | | import com.fasterxml.jackson.core.type.TypeReference; |
| | | import com.ruoyi.system.utils.wx.tools.WxUtils; |
| | | import com.ruoyi.system.wxPay.model.WeixinPayProperties; |
| | | import com.ruoyi.system.wxPay.model.WxCloseOrderModel; |
| | | import com.ruoyi.system.wxPay.model.WxPaymentRefundModel; |
| | | import com.ruoyi.system.wxPay.resp.NotifyV3PayDecodeRespBody; |
| | | import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder; |
| | | import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner; |
| | | import com.wechat.pay.contrib.apache.httpclient.auth.Verifier; |
| | | import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator; |
| | | import com.wechat.pay.contrib.apache.httpclient.util.PemUtil; |
| | | import lombok.Getter; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.http.impl.client.CloseableHttpClient; |
| | | |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import java.security.PrivateKey; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * @author xiaochen |
| | | * @ClassName WxWifiV3Pay |
| | | * @Description |
| | | * @date 2021-11-13 21:10 |
| | | */ |
| | | @Slf4j |
| | | public class WxV3Pay extends WxAbstractPay { |
| | | @Getter |
| | | private WeixinPayProperties config; |
| | | @Getter |
| | | private Verifier verifier; |
| | | private WechatPayHttpClientBuilder builder; |
| | | @Getter |
| | | private CloseableHttpClient httpClient; |
| | | private PrivateKeySigner privateKeySigner; |
| | | private WechatPay2Validator validator; |
| | | @Getter |
| | | private PrivateKey privateKey; |
| | | |
| | | /** |
| | | * 初始化 |
| | | * |
| | | * @param config |
| | | */ |
| | | public WxV3Pay(WeixinPayProperties config) { |
| | | // 检查v3支付配置信息 |
| | | if (WxUtils.getLogger().isDebugEnabled()) { |
| | | WxUtils.debug("开始检查v3支付配置信息...."); |
| | | } |
| | | try { |
| | | this.config = config; |
| | | checkWxConfig(); |
| | | this.privateKey = PemUtil.loadPrivateKey(config.getV3().getPrivateKeyStream()); |
| | | this.privateKeySigner = new PrivateKeySigner(config.getV3().getMchSerialNo(), privateKey); |
| | | this.validator = new WechatPay2Validator(verifier); |
| | | this.builder = WechatPayHttpClientBuilder.create() |
| | | .withMerchant(config.getMchId(), config.getV3().getMchSerialNo(), this.privateKey) |
| | | .withValidator(this.validator); |
| | | this.httpClient = this.builder.build(); |
| | | } catch (Exception e) { |
| | | // 打印异常信息 |
| | | e.printStackTrace(); |
| | | WxUtils.warn("检查v3支付配置信息出现错误,直连商户商户号:{},{}", config.getMchId(), e.getMessage()); |
| | | return; |
| | | } |
| | | if (WxUtils.getLogger().isDebugEnabled()) { |
| | | WxUtils.debug("检查v3支付配置信息完成,未出现异常...."); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 检查支付配置信息 |
| | | * |
| | | * @throws Exception |
| | | */ |
| | | private void checkWxConfig() throws Exception { |
| | | if (this.config == null) { |
| | | throw new Exception("config is null"); |
| | | } |
| | | if (config.getMchId() == null || config.getMchId().trim().length() == 0) { |
| | | throw new Exception("MchID in config is empty"); |
| | | } |
| | | if (config.getV3().getMchSerialNo() == null) { |
| | | throw new Exception("mchSerialNo in config is empty"); |
| | | } |
| | | if (config.getV3().getPrivateKeyStream() == null) { |
| | | throw new Exception("cert stream in config is empty"); |
| | | } |
| | | if (this.config.getHttpConnectTimeoutMs() < 10) { |
| | | throw new Exception("http connect timeout is too small"); |
| | | } |
| | | if (this.config.getHttpReadTimeoutMs() < 10) { |
| | | throw new Exception("http read timeout is too small"); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * jsApi下单 |
| | | * |
| | | * @param tradeNo 订单号 |
| | | * @param amount 金额 分 |
| | | * @param openid openid |
| | | * @param description 订单描述 |
| | | * @return java.util.Map<java.lang.String, java.lang.Object> |
| | | * @author xiaochen |
| | | * @date 2022-03-22 12:47 |
| | | */ |
| | | public Map<String, Object> jsApi(String tradeNo, Long amount, String openid, String description,String attach) { |
| | | JsapiPrepay client = new JsapiPrepay( |
| | | this.config.getMchId(), // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756 |
| | | this.config.getV3().getMchSerialNo(), // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053 |
| | | this.config.getV3().getPrivateKeyPath(), // 商户API证书私钥文件路径,本地文件路径 |
| | | this.config.getV3().getPublicKeyId(), // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816 |
| | | this.config.getV3().getPublicKeyPath() // 微信支付公钥文件路径,本地文件路径 |
| | | ); |
| | | JsapiPrepay.DirectAPIv3JsapiPrepayRequest request = new JsapiPrepay.DirectAPIv3JsapiPrepayRequest(); |
| | | request.appid = this.config.getAppId(); |
| | | request.mchid = this.config.getMchId(); |
| | | request.description = description; |
| | | request.outTradeNo = tradeNo; |
| | | // request.timeExpire = "2018-06-08T10:34:56+08:00"; |
| | | request.attach = attach; |
| | | request.notifyUrl = this.config.getCallBackUrl(); |
| | | request.goodsTag = "WXG"; |
| | | request.supportFapiao = false; |
| | | request.amount = new JsapiPrepay.CommonAmountInfo(); |
| | | request.amount.total = amount; |
| | | request.amount.currency = "CNY"; |
| | | request.payer = new JsapiPrepay.JsapiReqPayerInfo(); |
| | | request.payer.openid = openid; |
| | | try { |
| | | JsapiPrepay.DirectAPIv3JsapiPrepayResponse response = client.run(request); |
| | | return wxTuneUp(this.privateKeySigner, this.config.getAppId(), response.prepayId); |
| | | } catch (WXPayUtility.ApiException e) { |
| | | throw new RuntimeException("获取支付数据错误!"); |
| | | } |
| | | } |
| | | // /** |
| | | // * jsApi下单 |
| | | // * |
| | | // * @param tradeNo 订单号 |
| | | // * @param amount 金额 分 |
| | | // * @param openid openid |
| | | // * @param description 订单描述 |
| | | // * @return java.util.Map<java.lang.String, java.lang.Object> |
| | | // * @author xiaochen |
| | | // * @date 2022-03-22 12:47 |
| | | // */ |
| | | // public Map<String, Object> jsApi(String tradeNo, Integer amount, String openid, String description) { |
| | | // WxPaymentInfoModel requestBody = WxPaymentInfoModel.builder() |
| | | // .mchid(this.config.getMchId()) |
| | | // .appid(this.config.getAppId()) |
| | | // .description(description) |
| | | // .out_trade_no(tradeNo) |
| | | //// .attach("") |
| | | // .amount(WxPaymentInfoModel.Amount.builder().total(amount).build()) |
| | | // .payer(WxPaymentInfoModel.Payer.builder().openid(openid).build()) |
| | | // .build(); |
| | | // // 封装基础数据 |
| | | // String reqBody = buildBaseParam(requestBody |
| | | // , this.config.getV3().getNotifyPayUrl()); |
| | | // //请求URL |
| | | // HttpEntityEnclosingRequestBase httpPost = requestPost( |
| | | // "/v3/pay/transactions/jsapi" |
| | | // , this.config.getHttpReadTimeoutMs() |
| | | // , this.config.getHttpConnectTimeoutMs() |
| | | // , reqBody); |
| | | // String repBody = result(httpClient, httpPost); |
| | | // ObjectMapper om = new ObjectMapper(); |
| | | // try { |
| | | // JsonNode rootNode = om.readTree(repBody); |
| | | // String prepayId = rootNode.path("prepay_id").asText(); |
| | | // return wxTuneUp(this.privateKeySigner, requestBody.getAppid(), prepayId); |
| | | // } catch (JsonProcessingException e) { |
| | | // throw new RuntimeException("获取支付数据错误!"); |
| | | // } |
| | | // } |
| | | |
| | | |
| | | /** |
| | | * 接收回调 |
| | | * |
| | | * @param request |
| | | * @return |
| | | * @throws Exception |
| | | */ |
| | | @Override |
| | | public <T> T verifyNotify(HttpServletRequest request, TypeReference<T> valueTypeRef) throws Exception { |
| | | return verifyNotify(request, this.verifier, this.config.getV3().getApiKey(), valueTypeRef); |
| | | } |
| | | |
| | | /** |
| | | * 订单查询 |
| | | * |
| | | * @param out_trade_no |
| | | * @return com.abl.biz.center.payment.wx.v3.NotifyV3PayDecodeRespBody |
| | | * @author xiaochen |
| | | * @date 2021-12-20 16:47 |
| | | */ |
| | | @Override |
| | | public NotifyV3PayDecodeRespBody query(String out_trade_no) { |
| | | String url = |
| | | String.format("/v3/pay/transactions/out-trade-no/%s", out_trade_no) + String.format("?mchid=%s", this.getConfig().getMchId()); |
| | | return query(this.httpClient, this.config.getHttpReadTimeoutMs(), this.config.getHttpConnectTimeoutMs(), url); |
| | | } |
| | | |
| | | /** |
| | | * 退款 |
| | | * |
| | | * @param refundModel |
| | | * @return java.util.Map<java.lang.String, java.lang.Object> |
| | | * @author xiaochen |
| | | */ |
| | | @Override |
| | | public Map<String, Object> refund(WxPaymentRefundModel refundModel) { |
| | | refundModel.setNotify_url(this.config.getV3().getNotifyRefundUrl() + refundModel.getNotify_url()); |
| | | return refund(this.httpClient, "/v3/refund/domestic/refunds", this.config.getHttpReadTimeoutMs(), this.config.getHttpConnectTimeoutMs(), refundModel); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 关闭订单 |
| | | * @param out_trade_no |
| | | * @return |
| | | */ |
| | | @Override |
| | | public String close(String out_trade_no) { |
| | | String uri = String.format("/v3/pay/transactions/out-trade-no/%s/close", out_trade_no); |
| | | WxCloseOrderModel wxCloseOrderModel = new WxCloseOrderModel(); |
| | | wxCloseOrderModel.setMchid(this.config.getMchId()); |
| | | return close(this.httpClient, uri, this.config.getHttpReadTimeoutMs(), this.config.getHttpConnectTimeoutMs(), wxCloseOrderModel); |
| | | } |
| | | } |