一般來(lái)說(shuō)支付功能都是通過(guò)公司申請(qǐng)賬戶進(jìn)行開發(fā)、測(cè)試。如果是個(gè)人想了解學(xué)習(xí)一下,可以使用支付寶提供的沙箱功能,做一些基礎(chǔ)功能的學(xué)習(xí)和測(cè)試。我查了一圈沒(méi)發(fā)現(xiàn)微信支付有類似的功能,如果誰(shuí)知道請(qǐng)說(shuō)一下。
準(zhǔn)備工作
登錄支付寶的開發(fā)者中心控制臺(tái),如圖:
設(shè)置密鑰:
生成密鑰的工具,如圖:
下面還有安卓版的支付寶(沙箱版),配合使用。
代碼
接下來(lái)我們就可以著手進(jìn)行開發(fā)。文檔很豐富,大家可以根據(jù)自己的情況選用,我這里就是用Spring Boot,采用老版的支付寶服務(wù)端SDK,簡(jiǎn)單地實(shí)現(xiàn)一下當(dāng)面付、PC網(wǎng)頁(yè)掃碼付、支付寶回調(diào)通知接口(無(wú)外網(wǎng)環(huán)境下如何測(cè)試)、基于AOP的驗(yàn)簽、基于hibernate-validator的參數(shù)校驗(yàn)以及全局異常捕獲。因?yàn)橹皇菍W(xué)習(xí)功能,所以代碼寫的不是那么規(guī)整,而且不涉及數(shù)據(jù)庫(kù)。
之前在工作中,支付作為一個(gè)基礎(chǔ)服務(wù),是用Dubbo服務(wù)提供對(duì)外接口的,隨著后續(xù)的發(fā)展,出現(xiàn)了一系列的問(wèn)題:
- 一開始是沒(méi)有消息驗(yàn)簽的。
- 回調(diào)業(yè)務(wù)系統(tǒng)的接口是HTTP,因?yàn)橹Ц斗?wù)不可能也用Dubbo Service的方式調(diào)業(yè)務(wù)系統(tǒng)的接口,這得引入多少業(yè)務(wù)系統(tǒng)的jar啊。這樣一來(lái),就很別扭了,你調(diào)我接口Dubbo Service,我調(diào)你接口HTTP請(qǐng)求。
- 業(yè)務(wù)系統(tǒng)都要引入支付服務(wù)的jar包。
所以后來(lái)改成HTTP服務(wù)了。
先看一下結(jié)構(gòu)圖:
pom.xml引入jar包:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/com.google.guava/guava --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>29.0-jre</version> </dependency> <!-- https://mvnrepository.com/artifact/com.google.code.gson/gson --> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/com.alipay.sdk/alipay-sdk-java --> <dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-sdk-java</artifactId> <version>4.10.145.ALL</version> </dependency> <!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator --> <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
在實(shí)際工作中,微服務(wù)的背景下,要分配給每個(gè)業(yè)務(wù)系統(tǒng)一個(gè)clientId、一個(gè)驗(yàn)證消息簽名用的密鑰,還有支付渠道ID——支付寶當(dāng)面付、掃碼支付、APP支付,微信小程序支付等等,不同的channelId對(duì)應(yīng)不同的處理策略。
我們?cè)诠ぷ髦兄Ц读鞒淌牵?/p>
1、用戶發(fā)起支付,業(yè)務(wù)系統(tǒng)搜集訂單信息,通過(guò)HTTP請(qǐng)求至支付服務(wù)(這里可以是異步也可以是同步)。
2、支付服務(wù)對(duì)消息進(jìn)行驗(yàn)簽,校驗(yàn)參數(shù)合法性,入庫(kù),轉(zhuǎn)發(fā)請(qǐng)求至支付寶或微信(這里可以是異步也可以是同步)。
3、如果是同步,則直接將返回的信息回傳給業(yè)務(wù)系統(tǒng),例如PC網(wǎng)頁(yè)掃碼支付,支付寶就返回了<form>標(biāo)簽的HTML代碼,業(yè)務(wù)系統(tǒng)的前端頁(yè)面要嵌進(jìn)去。如果是當(dāng)面付這種,其實(shí)是可以走異步的,按需處理吧。
4、用戶完成支付后,支付寶會(huì)有兩個(gè)回調(diào)return_url、notify_url告知支付狀態(tài),支付服務(wù)接到結(jié)果后再回調(diào)業(yè)務(wù)系統(tǒng)的接口,回寫支付狀態(tài)。
驗(yàn)簽的AOP代碼:
@Aspect@Componentpublic class SignAspect { @Pointcut("execution(* org.leo.demo.controller..PayController.*(..))") public void executionPay() { } @Around("executionPay()") public Object doAround(ProceedingJoinPoint pjp) throws Throwable { ServletrequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = Objects.requireNonNull(attributes).getRequest(); // 獲取請(qǐng)求頭 Enumeration<String> enumeration = request.getHeaderNames(); Map<String, String> headerMap = Maps.newHashMap(); while (enumeration.hasMoreElements()) { String name = enumeration.nextElement(); String value = request.getHeader(name); headerMap.put(name, value); } Gson gson = new Gson(); String json = gson.toJson(pjp.getArgs()[0]); String signFromHeader = headerMap.get("sign"); // 每個(gè)客戶端的salt應(yīng)該是不同的,此處可以根據(jù)clientId去DB或Redis中取,順便也校驗(yàn)一下clientId是否存在 String signFronEncrypt = SignUtils.encrypt(json, SignUtils.salt); System.out.print("n請(qǐng)求明文:" json); System.out.print("n請(qǐng)求簽名:" signFromHeader); System.out.println("n加密簽名:" signFronEncrypt); if (signFromHeader.equals(signFronEncrypt)) { Object result = pjp.proceed(); return result; } else { DefaultResult<PayResult> result = new DefaultResult<PayResult>(); result.setCode(999); result.setMsg("驗(yàn)簽錯(cuò)誤"); return result; } }}
我在這里使用了AOP而非Filter,僅僅是因?yàn)槲也幌矚gFilter獲取消息體時(shí),inputStream無(wú)法傳入Controller,還要額外處理一下。公司里面倒是用Filter?的多,不知道大家在實(shí)際工作中是如何處理的。?
消息加密方法:
/** * 真實(shí)環(huán)境中,鹽應(yīng)該是一個(gè)客戶端分配一個(gè) */ public static final String salt = "111111"; public static String encrypt(String data, String salt) { // 因?yàn)橹皇球?yàn)簽,沒(méi)必要解密。MD5已經(jīng)被廢棄 return Hashing.sha256().newHasher().putString(data salt, Charsets.UTF_8).hash().toString(); }
消息加密的方法有很多種,而且每個(gè)客戶分配的密鑰也必須不同,我這里只是為了展示一下功能,采用了Guava的Hashing.sha256方法,實(shí)際工作中要按照要求進(jìn)行修改。
HTTP請(qǐng)求除了放在body的json消息體外,還要在header上放入固定的sign,postman請(qǐng)求如圖:
下面是PayController:
@RestController@Validatedpublic class PayController { @Autowired private AliPayFace2FaceService aliPayFace2FaceService; @Autowired private AliPayScan2PayService aliPayScan2PayService; @PostMapping("/pay.do") @ResponseBody public DefaultResult<PayResult> pay(@Valid @RequestBody PayParam param) { System.out.println("Controller:" param.toString()); if (param.getChannelId() == 1) { return aliPayFace2FaceService.pay(param); } else if (param.getChannelId() == 2) { return aliPayScan2PayService.pay(param); } else { DefaultResult<PayResult> result = new DefaultResult<PayResult>(); result.setCode(222); result.setMsg("支付渠道錯(cuò)誤"); return result; } } @RequestMapping("/alipaynotify.do") @ResponseBody public String alipayNotify(HttpServletRequest req) throws AlipayApiException { Map<String, String> params = new HashMap<String, String>(); Map<String, String[]> requestParams = req.getParameterMap(); for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext();) { String name = (String) iter.next(); String[] values = (String[]) requestParams.get(name); String valueStr = ""; for (int i = 0; i < values.length; i ) { valueStr = (i == values.length - 1) ? valueStr values[i] : valueStr values[i] ","; } params.put(name, valueStr); } boolean signVerified = AlipaySignature.rsaCheckV1(params, AlipayConfig.alipay_public_key, AlipayConfig.charset, AlipayConfig.sign_type); if (signVerified) { System.out.println("支付寶回調(diào)驗(yàn)簽通過(guò)"); System.out.println(params.toString()); return "success"; } else { System.out.println("支付寶回調(diào)驗(yàn)簽失敗"); return "error"; } }}
alipayNotify這個(gè)方法我們后面再說(shuō)。支付渠道應(yīng)該有個(gè)枚舉類,這里省略了。
Controller里的pay方法有點(diǎn)像策略模式,根據(jù)不同的支付渠道ID,使用對(duì)應(yīng)的處理Service。只是我們這里省下了Context類。
對(duì)參數(shù)的校驗(yàn),這里放在Controller了,大家可以按照公司的開發(fā)規(guī)范,放在Service也可以。處理類代碼如下:
@ControllerAdvicepublic class GlobalExceptionHandler { @ExceptionHandler(value = MethodArgumentNotValidException.class) @ResponseBody public DefaultResult<List<Map<String, String>>> validParam(MethodArgumentNotValidException e) { // 按需重新封裝需要返回的錯(cuò)誤信息 List<Map<String, String>> errorMsgs = Lists.newArrayList(); // 解析原錯(cuò)誤信息,封裝后返回,此處返回非法的字段名稱,原始值,錯(cuò)誤信息 for (FieldError error : e.getBindingResult().getFieldErrors()) { Map<String, String> errorMap = Maps.newLinkedHashMap(); errorMap.put("字段", error.getField()); errorMap.put("消息", error.getDefaultMessage()); errorMap.put("傳入值", error.getRejectedValue().toString()); errorMsgs.add(errorMap); } DefaultResult<List<Map<String, String>>> result = new DefaultResult<List<Map<String, String>>>(); result.setCode(444); result.setMsg("參數(shù)校驗(yàn)錯(cuò)誤"); result.setData(errorMsgs); return result; }}
IPayService這個(gè)沒(méi)什么好說(shuō)的,就是通用的方法,本例就寫了一個(gè)支付接口。
下面是當(dāng)面付的Service,因?yàn)槭菍W(xué)習(xí)測(cè)試用,所以省略了訂單入庫(kù)的代碼:
@Servicepublic class AliPayFace2FaceService implements IPayService { @Override public DefaultResult<PayResult> pay(PayParam param) { System.out.println("Service:" param.toString()); // 應(yīng)從相關(guān)配置和數(shù)據(jù)庫(kù)拿這些參數(shù) AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.gatewayUrl, AlipayConfig.app_id, AlipayConfig.merchant_private_key, "json", AlipayConfig.charset, AlipayConfig.alipay_public_key, AlipayConfig.sign_type); AlipayTradePayRequest request = new AlipayTradePayRequest(); AlipayTradePayModel model = new AlipayTradePayModel(); request.setBizModel(model); model.setOutTradeNo(param.getOrderNo()); model.setSubject(param.getSubject()); // 計(jì)算金額應(yīng)有專門的工具類實(shí)現(xiàn) BigDecimal b1 = new BigDecimal(param.getTotalAmount()); BigDecimal b2 = new BigDecimal(100); BigDecimal bdResult = b1.divide(b2, 2, RoundingMode.DOWN); model.setTotalAmount(bdResult.toString()); model.setAuthCode(param.getAuthCode());// 沙箱錢包中的付款碼 model.setScene("bar_code"); AlipayTradePayResponse response = null; try { response = alipayClient.execute(request); System.out.println(response.getBody()); System.out.println(response.getTradeNo()); DefaultResult<PayResult> result = new DefaultResult<PayResult>(); PayResult payResult = new PayResult(); if (response.getCode().equals("10000")) { payResult.setPayOrderNo(response.getTradeNo()); } else { result.setCode(Integer.valueOf(response.getCode())); result.setMsg(response.getMsg() "。" response.getSubMsg()); } return result; } catch (AlipayApiException e) { DefaultResult<PayResult> result = new DefaultResult<PayResult>(); result.setCode(753); result.setMsg("支付寶異常"); result.setData(null); return result; } }}
其中AlipayConfig里面就是之前我們?cè)谥Ц秾毶吓渲玫墓€、私鑰、網(wǎng)關(guān)地址、APPID、還有我們的回調(diào)接口,這些信息應(yīng)該從配置文件或者DB里面獲取。
支付寶接收的金額是元,小數(shù)點(diǎn)后兩位,也就是只到分了。而我們?cè)诠ぷ髦?,?shí)際上金額存的都是long型,沒(méi)有小數(shù)點(diǎn),直接到分,這里要寫一個(gè)專門的工具類處理一下,本例省略了。
付款碼就是沙箱支付寶APP中,“付款”-“查看數(shù)字”。測(cè)試的時(shí)候要快,因?yàn)檫@段數(shù)字會(huì)變。
下面是PC網(wǎng)頁(yè)掃碼付的Service:
@Servicepublic class AliPayScan2PayService implements IPayService { @Override public DefaultResult<PayResult> pay(PayParam param) { System.out.println("Service:" param.toString()); AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.gatewayUrl, AlipayConfig.app_id, AlipayConfig.merchant_private_key, "json", AlipayConfig.charset, AlipayConfig.alipay_public_key, AlipayConfig.sign_type); AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest(); alipayRequest.setReturnUrl(AlipayConfig.return_url); alipayRequest.setNotifyUrl(AlipayConfig.notify_url); // 付款金額 BigDecimal b1 = new BigDecimal(param.getTotalAmount()); BigDecimal b2 = new BigDecimal(100); BigDecimal bdResult = b1.divide(b2, 2, RoundingMode.DOWN); // 最晚付款時(shí)間。m分鐘,h小時(shí),d天,1c-當(dāng)天(0點(diǎn)關(guān)閉)。 String timeoutExpress = "15m"; String body = ""; DefaultResult<PayResult> result = new DefaultResult<PayResult>(); PayResult payResult = new PayResult(); alipayRequest.setBizContent("{"out_trade_no":"" param.getOrderNo() ""," ""total_amount":"" bdResult ""," ""subject":"" param.getSubject() ""," ""body":"" body ""," ""timeout_express":"" timeoutExpress ""," ""product_code":"FAST_INSTANT_TRADE_PAY"}"); try { String htmlStr = alipayClient.pageExecute(alipayRequest).getBody(); System.out.println(htmlStr); payResult.setHtmlStr(htmlStr); } catch (AlipayApiException e) { result.setCode(753); result.setMsg("支付寶異常"); result.setData(null); } return result; }}
這里返回的是一段HTML代碼(微信支付返回的是一個(gè)二維碼),我們拿到之后,可以新建一個(gè)HTML文件放進(jìn)去,直接掃碼支付。
掃碼支付后,支付寶會(huì)回調(diào)我們提供的return_url和notify_url接口,告知結(jié)果,但是作為個(gè)人開發(fā),如果沒(méi)有外網(wǎng)IP,該如何驗(yàn)證一下呢?
先說(shuō)一下return_url和notify_url的區(qū)別。
掃碼支付完成后,支付網(wǎng)頁(yè)會(huì)同步跳轉(zhuǎn)到return_url,展示一些信息,這是個(gè)get請(qǐng)求,僅發(fā)一次。
而notify_url是由支付寶后端發(fā)起的post請(qǐng)求,如果我們不返回success消息,支付寶會(huì)進(jìn)行重發(fā)。
所以從工作中來(lái)說(shuō),我們一般是把return_url做一個(gè)臨時(shí)展示,而在notify_url中進(jìn)行一些邏輯處理,例如回寫訂單狀態(tài)等操作。
在測(cè)試開發(fā)中,因?yàn)槲覀儧](méi)有外網(wǎng)IP,網(wǎng)頁(yè)會(huì)彈出一個(gè)信息框,告知地址無(wú)法訪問(wèn),這時(shí)候我們就可以把整個(gè)url復(fù)制下來(lái),自己在瀏覽器上訪問(wèn)一下,從而可以驗(yàn)證我們的回調(diào)接口是否正常。
版權(quán)聲明:本文內(nèi)容由互聯(lián)網(wǎng)用戶自發(fā)貢獻(xiàn),該文觀點(diǎn)僅代表作者本人。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如發(fā)現(xiàn)本站有涉嫌抄襲侵權(quán)/違法違規(guī)的內(nèi)容, 請(qǐng)發(fā)送郵件至 舉報(bào),一經(jīng)查實(shí),本站將立刻刪除。