微服务对接沙箱支付宝支付,并实现回调数据修改
在前文中,我们完成了vip题目与权限的设计,于是自然而然的引出了我们怎么成为vip这个问题,传统的c端系统以及游戏,往往采用对接支付接口的方式,本文选取了对接支付宝接口的形式进行支付并获取vip权限。
扫码付款的接入流程
支付宝支付的逻辑过程
支付成功后的消息回调
有了此处基础上,通过研究支付宝沙箱支付原内容,我们可以很好的上手
进入支付宝沙箱系统网站(沙箱应用 - 开放平台 (alipay.com))
拿到这些信息
由于测试环境是本地运行,我们通过内网穿透获得一个公网地址,这里我用的内网穿透工具为natapp工具
配置页
<groupId>com.alipay.sdk</groupId> <artifactId>alipay-sdk-java</artifactId> <version>4.9.9</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency> <!--方式二 https://mvnrepository.com/artifact/com.alipay.sdk/alipay-easysdk --> <dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-easysdk</artifactId> <version>2.2.0</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.8</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.google.zxing</groupId> <artifactId>core</artifactId> <version>3.3.3</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.5</version> </dependency>
/** * @Description: * @Version: V1.0 */ @Configuration @Data @ConfigurationProperties(prefix = "alipay.easy") public class AliPayProperties { //请求协议 private String protocol; // 请求网关 private String gatewayHost; // 签名类型 RSA2 private String signType; // 应用ID private String appId; // 应用私钥 private String merchantPrivateKey; // 支付宝公钥 private String alipayPublicKey; // 异步通知接收服务地址 private String notifyUrl; // 设置AES密钥 private String encryptKey; }
@Configuration @Data public class AlipayConfig { @Bean public Config config(AliPayProperties payProperties) { Config config = new Config(); config.protocol = payProperties.getProtocol(); config.gatewayHost = payProperties.getGatewayHost(); config.signType = payProperties.getSignType(); config.appId = payProperties.getAppId(); config.merchantPrivateKey = payProperties.getMerchantPrivateKey(); config.alipayPublicKey = payProperties.getAlipayPublicKey(); //可设置异步通知接收服务地址(可选) config.notifyUrl = payProperties.getNotifyUrl(); config.encryptKey = ""; return config; } }
@RestController @Slf4j @AllArgsConstructor public class EasyPayController { private final Config config; @GetMapping("pay") public String pay() throws Exception { Factory.setOptions(config); //调用支付宝接口 AlipayTradePrecreateResponse response = Factory.Payment.FaceToFace().preCreate("iphone15 promax 1T", "12138881313134", "12999"); //解析结果 String httpBody = response.getHttpBody(); System.out.println(httpBody); //转JSON对象 JSONObject jsonObject = JSONObject.parseObject(httpBody); try { String qrUrl = jsonObject.getJSONObject("alipay_trade_precreate_response").get("qr_code").toString(); QrCodeUtil.generate(qrUrl,300,300,new File("E://qr2.jpg")); }catch (Exception e){ return "支付失败"; } //生成二维码 return httpBody; } @PostMapping("/notify") public String notify(HttpServletRequest request){ log.info("收到支付成功通知"); System.out.println("支付成功"); String out_trade_no = request.getParameter("out_trade_no"); log.info("流水号:{}",out_trade_no); System.out.println("流水号"+out_trade_no); //TODO 后续业务流程 发货 //回调了多次怎么办? 考虑接口的幂等性 分布式锁 乐观锁 先判断再处理 return "success"; } @GetMapping("/query") public String query() throws Exception { Factory.setOptions(config); AlipayTradeQueryResponse response = Factory.Payment.Common().query("1213131313131"); return response.getHttpBody(); } }
上述代码实现了一个简单的模拟支付的流程,下面引出我们刷题系统的支付流程管理。
问题:
系统是微服务,支付功能放在哪里?
系统怎么确定某个用户是否支付?
怎么提升系统复用性?
一开始的设计之初,笔者是想把支付接口与微信服务写在一起,因为二者都需要内网穿透映射在公网,同时都是对外对接的服务,但是尝试过后笔者发现设计之初时,我们的微信服务是无业务关联的,他只负责登陆,这就导致了我们将支付系统引入微信服务会导致原代码大量重构,后面笔者又想将支付接口写入auth用户微服务中,但是因为支付接口异步消息回调的原因,将用户服务暴露在公网对系统安全也是不利的,同时内网穿透本身网速就慢,这样对系统而言影响较大。
考虑到系统后期可能不止一种服务支持方式,也不止仅仅充值vip这一个服务,为了更好的服务于其他功能,我们决定将支付服务单独独立部署出来。
新建订单表
@Override public String created(Orders order) throws Exception { // 创建订单预支付请求 AlipayTradePrecreateResponse response = Factory.Payment.FaceToFace().preCreate(order.getOrderName(), order.getOrderId(), order.getOrderPrice()); // 解析结果 String httpBody = response.getHttpBody(); System.out.println(httpBody); // 转JSON对象 JSONObject jsonObject = JSONObject.parseObject(httpBody); try { // 检查是否订单已支付 if ("40004".equals(jsonObject.getJSONObject("alipay_trade_precreate_response").getString("code"))) { return "订单已支付,请勿重复支付"; } // 获取二维码URL String qrUrl = jsonObject.getJSONObject("alipay_trade_precreate_response").getString("qr_code"); // 生成二维码并保存到本地文件 String filePath = "codeimg.png"; qrCodeGenerator.generateQRCode(qrUrl, filePath); // 构建MinIO中的对象名称 String uniqueObjectName = OBJECT_NAME + "/" + Paths.get(filePath).getFileName(); // 将生成的二维码上传到MinIO Path path = Paths.get(filePath); minioClient.putObject( PutObjectArgs.builder() .bucket(BUCKET_NAME) .object(uniqueObjectName) // 使用唯一的对象名称 .stream(Files.newInputStream(path), Files.size(path), -1) .contentType("image/png") .build() ); // 生成预签名URL String url = URL + "/" + BUCKET_NAME + "/" + uniqueObjectName; // 删除本地文件 Files.delete(path); OrderVip orderVip = new OrderVip(); orderVip.setOrderId(Integer.valueOf(order.getOrderId())); orderVip.setOrderName(order.getOrderName()); orderVip.setOrderPrice(order.getOrderPrice()); orderVip.setStatus(0L); String loginId = LoginUtil.getLoginId(); orderVip.setUserId(loginId); orderVip.setIsDeleted(0); orderVipService.insert(orderVip); return url; } catch (Exception e) { // 记录异常信息 e.printStackTrace(); return "上传失败"; } }
业务分析:用户调用支付接口传入订单id,订单名称,订单金额字段
通过阿里云面对面支付生成预创建订单
AlipayTradePrecreateResponse response = Factory.Payment.FaceToFace().preCreate(order.getOrderName(), order.getOrderId(), order.getOrderPrice());
用户打开支付宝扫码完成支付
异步消息回调这里有个问题我们要主要下,由于消息是异步回调通知,所以我们回调的消息内容在threadlocal中是不包含用户数据信息的,所以我们需要回调消息的订单id去订单库中提取用户信息,
public AuthUserRoleDTO updated(String out_trade_no,String loginId) throws Exception { AuthUserRoleDTO authUserRoleDTO = new AuthUserRoleDTO();
//获取用户信息 OrderVip orderVip =orderVipService.queryByOrderId(Integer.valueOf(out_trade_no)); orderVip.setStatus(1L); orderVipService.update(orderVip); //改为vip角色 authUserRoleDTO.setRoleId(4L); // Long userId = Long.valueOf(loginId); authUserRoleDTO.setUserId(orderVip.getUserId()); AuthUserRoleDTO authUserRole=userRpc.updateRole(authUserRoleDTO); return authUserRole; }
feign传送数据至下文修改用户角色为vip角色
public AuthUserRoleDTO updateRole(AuthUserRoleDTO authUserRole) { AuthUserRoleDTO result = userFeignService.updateRole(authUserRole).getData(); return result; }
@RequestMapping("updateRole") public Result<AuthUserRole> updateRole(@RequestBody AuthUserRoleDTO authUserRoleDTO) { try { if (log.isInfoEnabled()) { log.info("RoleController.update.dto:{}", JSON.toJSONString(authUserRoleDTO)); } // Preconditions.checkNotNull(authUserRole.getUserId(), "用户id不能为空"); Preconditions.checkNotNull(authUserRoleDTO.getRoleId(), "角色id不能为空"); String loginId = authUserRoleDTO.getUserId(); log.info("loginId:{}",loginId); AuthUserBO authUserBO = new AuthUserBO(); authUserBO.setUserName(loginId); AuthUserBO userInfo =authUserDomainService.getUserInfo(authUserBO); AuthUserRole authrole = new AuthUserRole(); authrole.setUserId(userInfo.getId()); authrole.setRoleId(authUserRoleDTO.getRoleId()); AuthUserRole authUserRole1 =authUserRoleService.updateUserId(authrole); return Result.ok(authUserRole1); } catch (Exception e) { log.error("RoleController.update.error:{}", e.getMessage(), e); return Result.fail("更新角色信息失败"); } }
整体效果: