微信公眾號(hào)支付的完整流程伸但,首先需要微信授權(quán)夸政,獲取openId绘沉,因?yàn)閛penid是微信用戶在公眾號(hào)appid下的唯一用戶標(biāo)識(shí)(appid不同,則獲取到的openid就不同)岂昭,可用于永久標(biāo)記一個(gè)用戶以现,同時(shí)也是微信JSAPI支付的必傳參數(shù)。
首先解釋一下微信公眾號(hào)中的一些概念约啊,想要完成支付邑遏,需要被認(rèn)證的公眾號(hào)外,還需要商戶號(hào)恰矩。這兩個(gè)都需要有一定資質(zhì)才能申請(qǐng)记盒。單純擁有公眾號(hào),只能進(jìn)行微信授權(quán)操作外傅,需要公眾號(hào)和商戶號(hào)綁定后才能完成支付操作纪吮。
用個(gè)不夠恰當(dāng)?shù)睦觼?lái)解釋:公眾號(hào)就類比于銀行的前臺(tái),商戶號(hào)就類比于銀行栏豺,前臺(tái)不綁定銀行的話那她就是一個(gè)普通人彬碱,不能完成銀行的各類經(jīng)濟(jì)業(yè)務(wù),前臺(tái)和銀行綁定后才可以操作款項(xiàng)奥洼。而這個(gè)前臺(tái)也要通過(guò)有一定的資質(zhì)認(rèn)證,才能和銀行綁定晚胡。
注:微信支付的接口不只有公眾號(hào)支付一種灵奖,但是無(wú)論哪種支付接口,都需要綁定商戶號(hào)才能進(jìn)行支付操作估盘。
獲取OpenId
獲取OpenId瓷患,有兩種方式,“手工方式”和“利用第三方API”遣妥,最終目的都是一樣的擅编,但是在實(shí)際開(kāi)發(fā)中還是用輪子比較容易。手工方式最主要的是一步一步的了解獲取OpenId的過(guò)程,如果以使用為主爱态,可以直接跳過(guò)“手工方式”谭贪,查看“利用第三方API”。
手工方式
首先锦担,很重要也是很多人懶得去做的事情就是仔細(xì)看看【微信支付】商戶接入文檔俭识,內(nèi)容很多,因?yàn)槭俏⑿殴娞?hào)洞渔,所以我選擇JSAPI支付
打開(kāi)鏈接可以看到JSAPI支付的詳細(xì)內(nèi)容套媚,接下來(lái)的操作都是根據(jù)JSAPI支付中的“業(yè)務(wù)流程”逐步完成。
PS.其實(shí)下圖鏈接網(wǎng)頁(yè)授權(quán)獲取用戶openid接口文檔也是真的寫(xiě)的很清楚了磁椒。當(dāng)trade_type=JSAPI時(shí)(即公眾號(hào)支付)堤瘤,openId必傳。
1. 設(shè)置網(wǎng)頁(yè)授權(quán)域名
按照文檔來(lái)浆熔,在公眾號(hào)中【設(shè)置網(wǎng)頁(yè)授權(quán)域名】本辐,這里接入的是外網(wǎng)地址,通俗但不夠準(zhǔn)確的講蘸拔,就是你程序所在的域名师郑。
填寫(xiě)完域名之后,記得下載文件调窍,因?yàn)橐?guī)定很多宝冕,還需要ICP備案什么的,作為調(diào)試邓萨,我選擇使用了https://natapp.cn/穿透內(nèi)網(wǎng)地梨,就可以使得微信這邊訪問(wèn)到自己的電腦。
在NATAPP中缔恳,注冊(cè)/登錄宝剖,購(gòu)買(mǎi)隧道,免費(fèi)的只能臨時(shí)用一下歉甚,還會(huì)隨便換域名/端口万细,所以我購(gòu)買(mǎi)了9/月的。
查看購(gòu)買(mǎi)到的隧道纸泄,這個(gè)域名是你在購(gòu)買(mǎi)過(guò)程中赖钞,他讓你自己輸入的。
查看教程NATAPP 1分鐘快速圖文教程聘裁,啟動(dòng)NATAPP雪营。啟動(dòng)后會(huì)看到域名映射到當(dāng)前本地端口。
此時(shí)通過(guò)在瀏覽器中衡便,輸入localhost:8080/myself
和 http://my.natapp.com/myself
訪問(wèn)的是同一個(gè)界面則表示成功献起。
再將之前在微信【網(wǎng)頁(yè)授權(quán)域名】中下載的 MP_verify_nxxxxxx.txt文件放到源碼文件中洋访。
注意:這里要求文件的位置,必須是在域名的根目錄下谴餐,在本例中也就是在瀏覽器中輸入http://my.natapp.com/MP_verify_nxxxxxx.txt
后姻政,頁(yè)面不報(bào)錯(cuò)時(shí),在點(diǎn)擊【確認(rèn)】才能在微信網(wǎng)頁(yè)授權(quán)域名中添加成功总寒。
當(dāng)然扶歪,添加成功域名之后,這個(gè)MP_verify_nxxxxxx.txt文件也可以從源碼文件中刪除摄闸。
2. 獲取code
可以查看微信文檔善镰,并了解相應(yīng)參數(shù)說(shuō)明。以下是微信文檔的相應(yīng)截圖年枕,我們只需要了解的就是替換掉鏈接中的appId和redirect_uri炫欺。用戶同意授權(quán)后,跳轉(zhuǎn)到redirect_uri并返回code熏兄。
3. 換取access_token
獲取了code之后品洛,以code作為票據(jù)再換access_token。以下是微信文檔的相應(yīng)截圖摩桶,我們只需要了解的就是替換掉鏈接中的appId為自己的appId和code為剛剛后臺(tái)獲取的code(注:code時(shí)效只有5min)桥状。4. 得到openId
在上一個(gè)步驟中,獲取到網(wǎng)頁(yè)授權(quán)access_token的同時(shí)硝清,也獲取到了openid辅斟,snsapi_base式的網(wǎng)頁(yè)授權(quán)流程即到此為止。請(qǐng)求上述鏈接時(shí)芦拿,正確時(shí)會(huì)返回如下JSON數(shù)據(jù)包士飒。其中就包含了openId。更多使用蔗崎,可以仔細(xì)查看微信文檔=湍弧!文檔寫(xiě)的很細(xì)致的;嚎痢芳撒!
以我自己為例,當(dāng)我手機(jī)微信訪問(wèn)了下面這個(gè)地址(網(wǎng)頁(yè)授權(quán)URL)之后
https://open.weixin.qq.com/connect/oauth2/authorize?appid=myappId&redirect_uri=http://my.natapp.com/myRedirectURL&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
手機(jī)微信會(huì)自動(dòng)跳轉(zhuǎn)到http://my.natapp.com/myRedirectURL/code=xxcodexx
未桥,雖然手機(jī)頁(yè)面是空白番官,但是后臺(tái)已經(jīng)獲得了code的信息。后臺(tái)可以通過(guò)拼接字符串等操作钢属,再發(fā)起請(qǐng)求https://api.weixin.qq.com/sns/oauth2/access_token?appid=myappId&secret=SECRET&code=xxcodexx&grant_type=authorization_code
,之后獲得一個(gè)包含了openId信息的JSON數(shù)據(jù)包门躯。
利用第三方API
直接看Github上的SDK:https://github.com/Wechat-Group/WxJava淆党。里面文檔、工具都非常詳細(xì)。
因?yàn)槲冶镜爻绦蛴玫腗aven染乌,所以直接引用山孔。[pom代碼1]
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>3.5.0</version>
</dependency>
在本地函數(shù)中的使用,主要查看文檔https://github.com/Wechat-Group/WxJava/wiki/MP_OAuth2網(wǎng)頁(yè)授權(quán)
根據(jù)文檔逐步完成本地代碼荷憋。
新建WechatController.class台颠,控制網(wǎng)絡(luò)授權(quán)。[授權(quán)代碼1]
@Controller
@RequestMapping("/wechat")
@Slf4j
public class WechatController {
@Autowired
WxMpService wxMpService = new WxMpServiceImpl();
@GetMapping("/authorize")
public String authorize(@RequestParam("returnUrl") String returnUrl) {
// 1.配置勒庄,項(xiàng)目中配置應(yīng)該是進(jìn)行一個(gè)統(tǒng)一配置串前,供程序各個(gè)部分使用。
// 2.調(diào)用方法实蔽,下面這個(gè)回調(diào)地址 是我自己的地址荡碾,你需要用你自己的
String url = "http://sell35.natapp1.cc/sell/wechat/userInfo";
String redirectUrl = wxMpService.oauth2buildAuthorizationUrl(url, WxConsts.OAUTH2_SCOPE_BASE, URLEncoder.encode(returnUrl));
log.info("【微信網(wǎng)頁(yè)授權(quán)】獲取code, result={}", redirectUrl);
return "redirect:" + redirectUrl;
}
// 獲取用戶信息
@GetMapping("/userInfo")
public String userInfo(@RequestParam("code") String code,
@RequestParam("state") String returnUrl) {
WxMpOAuth2AccessToken wxMpOAuth2AccessToken = new WxMpOAuth2AccessToken();
try {
wxMpOAuth2AccessToken = wxMpService.oauth2getAccessToken(code);
} catch (WxErrorException e) {
log.error("【微信網(wǎng)頁(yè)授權(quán)】{}", e);
throw new SellException(ResultEnum.WECHAT_MP_ERROR.getCode(), e.getError().getErrorMsg());
}
String openId = wxMpOAuth2AccessToken.getOpenId();
return "redirect:" + returnUrl + "?openid=" + openId;
}
}
建立config文件夾,并在下面新建WeChatMpConfig.class局装。將Service作為一個(gè)Bean坛吁、配置也作為Bean。 其中的AppId和AppSecret我們可以從配置文件中讀取铐尚。[授權(quán)代碼2]
@Component
public class WechatMpConfig {
@Autowired
private WechatAccountConfig accountConfig;
@Bean
public WxMpService wxMpService(){
WxMpService wxMpService = new WxMpServiceImpl();
wxMpService.setWxMpConfigStorage(wxMpConfigStorage());
return wxMpService;
}
@Bean
public WxMpConfigStorage wxMpConfigStorage(){
WxMpInMemoryConfigStorage wxMpConfigStorage =new WxMpInMemoryConfigStorage();
wxMpConfigStorage.setAppId(accountConfig.getMpAppId());
wxMpConfigStorage.setSecret(accountConfig.getMpAppSecret());
return wxMpConfigStorage;
}
}
配置文件微信賬號(hào)相關(guān)的部分先寫(xiě)一個(gè)配置文件拨脉。WechatAccountConfig.class[授權(quán)代碼3]
@Data
@Component
@ConfigurationProperties(prefix = "wechat")
public class WechatAccountConfig {
private String mpAppId;
private String mpAppSecret;
}
前端調(diào)試
首先先講一下請(qǐng)求過(guò)程,微信訪問(wèn)sell.com宣增,前端回會(huì)重定向到 /sell/wechat/authorize玫膀,并攜帶returnUrl:http://sell.com/abc。 通過(guò)上一步驟的授權(quán)操作獲取openid统舀,最后后端返回給前端 :http://sell.com/abc?openid=oxfjhaojdnsjcos
所以需要先在前端重定向匆骗,進(jìn)入虛擬機(jī)(前端部署部分)
cd /opt/
cd code/
cd sell_fe_buyer/
cd config/
配置文件
vim index.js
在配置文件中
sellUrl
對(duì)應(yīng)的是項(xiàng)目地址:http://sell.com
openidUrl
獲取openId的地址:http://sell35.natapp.cc/sell/wechat/authorize
wechatPayUrl
支付地址(當(dāng)前主要是配置授權(quán),先無(wú)需配置這一項(xiàng))
配置完成后誉简,回到前端項(xiàng)目的根目錄(cd ..
)碉就。
再構(gòu)建一下(npm run build
)
構(gòu)建好的文件,在dist目錄下闷串,所以需要將構(gòu)建好的文件copy到前端的根目錄下瓮钥,語(yǔ)句如下。
cp -r dist/* /opt/data/wwwroot/sell
但是此時(shí)我們通過(guò)手機(jī)訪問(wèn)"sell.com"是訪問(wèn)不了的烹吵。這是因?yàn)楫?dāng)前目標(biāo)網(wǎng)址“sell.com”是在電腦端碉熄,電腦之所以能訪問(wèn),是因?yàn)楸緳C(jī)設(shè)置了host肋拔,他將域名直接指向虛擬機(jī)地址锈津,所以可以成功訪問(wèn),但是由于手機(jī)無(wú)法更改host凉蜂。所以需要用代理解決這個(gè)問(wèn)題琼梆。將手機(jī)的所有請(qǐng)求轉(zhuǎn)發(fā)到電腦上性誉,此時(shí)就可以訪問(wèn)了。Mac下可以使用Charles茎杂,Windows下可以使用fiddler错览,
通過(guò)終端輸入ifconfig
,得到當(dāng)前電腦ip為 192.168.1.103.
再通過(guò)手機(jī)查詢當(dāng)前手機(jī)的ip為 192.168.1.105.
最好二者接通前在terminal中ping一下煌往。
ping通之后倾哺,在手機(jī)中設(shè)置手動(dòng)代理
服務(wù)器中輸入:電腦ip(103)
端口輸入:8888(因?yàn)镃harles的默認(rèn)端口就8888)
此時(shí)再在手機(jī)端訪問(wèn)sell.com,就可以通過(guò)電腦訪問(wèn)到公眾號(hào)網(wǎng)站刽脖。
微信支付
支付業(yè)務(wù)流程:生成商戶訂單(開(kāi)發(fā)者生成的訂單) —> 調(diào)用統(tǒng)一下單API —> 生成預(yù)付單后會(huì)返回一個(gè)預(yù)付單信息 —> 通過(guò)JSAPI頁(yè)面調(diào)用的支付參數(shù)并簽名(此時(shí)才會(huì)喚起支付) —> 支付完成后等待一個(gè)異步通知結(jié)果 —> 依據(jù)這個(gè)結(jié)果通知更改訂單狀態(tài)為已支付 —> 調(diào)用查詢API羞海,查詢支付結(jié)果(用于對(duì)賬)
選擇SDK,可以選擇之前的那個(gè)SDK曾棕,這里我選擇的是Best Pay SDK扣猫。
請(qǐng)求過(guò)程:
- 重定向到
/sell/pay/create
,攜帶參數(shù)(orderId:123456
和returnUrl:http://xxx.com/abc/order/123456
) - 最后返回到
http://xxx.com/abc/order/123456
翘地。
這里需要注意的是申尤,支付過(guò)程中,只需要傳過(guò)來(lái)訂單ID即可衙耕,至于需要支付多少錢(qián)昧穿,可以通過(guò)訂單ID去數(shù)據(jù)庫(kù)查看。不能將支付金額作為參數(shù)往后傳遞橙喘,因?yàn)檫@樣即便金額不對(duì)时鸵,也能夠支付成功,或者后臺(tái)再校驗(yàn)一邊金額厅瞎,無(wú)論怎樣饰潜,都是多此一舉。
引入SDK和簸,在Pom文件中添加依賴彭雾。[pom代碼2]
<dependency>
<groupId>cn.springboot</groupId>
<artifactId>best-pay-sdk</artifactId>
<version>1.1.0</version>
</dependency>
新建一個(gè)PayController Class,主要完成訂單查詢和支付操作锁保。[該段代碼非最終代碼薯酝,至于此處以便與思考] 。
@Controller
@RequestMapping("/pay")
public class PayController {
@Autowired
private OrderService orderService;
// PayService之后作為服務(wù)新建爽柒,當(dāng)前并不存在吴菠。
@Autowired
private PayService payService;
// 為了重定向,完成請(qǐng)求過(guò)程的第一步
@GetMapping("/create")
public void create(@RequestParam("orderId") String orderId,
@RequestParam("returnUrl") String returnUrl) {
//1. 查詢訂單
OrderDTO orderDTO = orderService.findOne(orderId);
if (orderDTO == null) {
throw new SellException(ResultEnum.PRODUCT_NOT_EXIST);
}
//2. 發(fā)起支付
PayResponse payResponse = payService.create(orderDTO);
}
}
根據(jù)SDK規(guī)則浩村,微信賬戶相關(guān)內(nèi)容需要配置做葵,配置在WechatAccountConfig中。[代碼3]
@Data
@Component
@ConfigurationProperties(prefix = "wechat")
public class WechatAccountConfig {
private String mpAppId;
private String mpAppSecret;
// 商戶號(hào)
private String mchId;
// 商戶密鑰
private String mchKey;
// 商戶證書(shū)路徑
private String keyPath;
// 微信支付異步通知地址
private String notifyUrl;
}
同時(shí)修改給配置文件增加相應(yīng)內(nèi)容配置一下WechatPayConfig()心墅,并把service作為Bean配置進(jìn)去蜂挪。[代碼4]
@Component
public class WechatPayConfig {
@Autowired
private WechatAccountConfig accountConfig;
@Bean
public BestPayServiceImpl bestPayService(){
WxPayH5Config wxPayH5Config=new WxPayH5Config();
wxPayH5Config.setAppId(accountConfig.getMpAppId());
wxPayH5Config.setAppSecret(accountConfig.getMpAppSecret());
wxPayH5Config.setMchId(accountConfig.getMchId());
wxPayH5Config.setMchKey(accountConfig.getMchKey());
wxPayH5Config.setKeyPath(accountConfig.getKeyPath());
wxPayH5Config.setNotifyUrl(accountConfig.getNotifyUrl());
BestPayServiceImpl bestPayService=new BestPayServiceImpl();
bestPayService.setWxPayH5Config(wxPayH5Config);
return bestPayService;
}
支付操作作為一個(gè)服務(wù)重挑,新建PayService,并建立該方法的實(shí)現(xiàn)PayServiceImpl棠涮。[代碼6] 并且將之前BestPayServiceImpl配置好的注入進(jìn)Service。上述的JsonUtil是個(gè)JSON格式化工具類刺覆,已附追在文章末尾严肪。
@Service
@Slf4j
public class PayServiceImpl implements PayService {
private static final String ORDER_NAME = "微信點(diǎn)單訂餐";
@Autowired
private BestPayServiceImpl bestPayService;
@Override
public PayResponse create(OrderDTO orderDTO) {
PayRequest payRequest = new PayRequest();
payRequest.setOpenid(orderDTO.getBuyerOpenid());
payRequest.setOrderAmount(orderDTO.getOrderAmount().doubleValue());
payRequest.setOrderId(orderDTO.getOrderId());
payRequest.setOrderName(ORDER_NAME);
payRequest.setPayTypeEnum(BestPayTypeEnum.WXPAY_H5);
log.info("【微信支付】,發(fā)起支付谦屑,request={}", JsonUtil.toJson(payRequest));
PayResponse payResponse = bestPayService.pay(payRequest);
log.info("【微信支付】驳糯,發(fā)起支付,response={}", JsonUtil.toJson(payResponse));
return payResponse;
}
}
最后返回的 response 內(nèi)容包含了 "appId"
氢橙、"timeStamp"
酝枢、"nonceStr"
、"packAge"
悍手、"signType"
帘睦、"paySign"
的值。
此時(shí)完成了業(yè)務(wù)流程中的:調(diào)用統(tǒng)一下單API坦康,并且返回預(yù)付單信息(prepay_id
在"packAge"
對(duì)應(yīng)的值中)竣付。
下一步我們要做的就是發(fā)起支付。
從網(wǎng)頁(yè)發(fā)起支付
支付操作的詳細(xì)內(nèi)容先仔細(xì)閱讀文檔JSAPI支付開(kāi)發(fā)者文檔
需要向后端先傳遞這些參數(shù)滞欠。
所以在代碼部分筛璧,我們接下來(lái)的工作就是動(dòng)態(tài)構(gòu)造如上圖所示的代碼逸绎。
這里我們選擇模版技術(shù),用到了freemarker這個(gè)組件夭谤,現(xiàn)在pom文件中引入dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
完善之前的PayController Class[代碼5] 棺牧。將返回的參數(shù)從void改成ModelAndView,最后return返回的“pay/create”路徑下的create實(shí)際上就是一個(gè)create.flt文件(模版文件)沮翔。
@Controller
@RequestMapping("/pay")
public class PayController {
@Autowired
private OrderService orderService;
@Autowired
private PayService payService;
// 為了重定向陨帆,完成請(qǐng)求過(guò)程的第一步
@GetMapping("/create")
public ModelAndView create(@RequestParam("orderId") String orderId,
@RequestParam("returnUrl") String returnUrl,
Map<String, Object> map) {
//1. 查詢訂單
OrderDTO orderDTO = orderService.findOne(orderId);
if (orderDTO == null) {
throw new SellException(ResultEnum.PRODUCT_NOT_EXIST);
}
//2. 發(fā)起支付
PayResponse payResponse = payService.create(orderDTO);
map.put("payResponse", payResponse);
map.put("returnUrl", returnUrl);
return new ModelAndView("pay/create");
}
}
create.flt中放的就是微信內(nèi)H5調(diào)起支付
文檔中返回的代碼格式
<script>
function onBridgeReady() {
WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
"appId":"${payResponse.appId}", //公眾號(hào)名稱,由商戶傳入
"timeStamp":"${payResponse.timeStamp}", //時(shí)間戳采蚀,自1970年以來(lái)的秒數(shù)
"nonceStr":"${payResponse.nonceStr}", //隨機(jī)串
"package":"${payResponse.packAge}",
"signType": "MD5", //微信簽名方式:
"paySign":"${payResponse.paySign}" //微信簽名
},
function (res) {
// if (res.err_msg == "get_brand_wcpay_request:ok") {
// } // 使用以上方式判斷前端返回,微信團(tuán)隊(duì)鄭重提示:res.err_msg將在用戶支付成功后返回 ok疲牵,但并不保證它絕對(duì)可靠。
location.href="${returnUrl}"
}
);
}
if (typeof WeixinJSBridge == "undefined") {
if (document.addEventListener) {
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
} else if (document.attachEvent) {
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
} else {
onBridgeReady();
}
</script>
此時(shí)已經(jīng)完成動(dòng)態(tài)注入?yún)?shù)了榆鼠,但是完成支付還需要我們?cè)谇岸宋募信渲靡幌赂侔郑瑓⒖贾暗摹厩岸苏{(diào)試】模塊。記得改完之后妆够,build和拷貝文件识啦。
此時(shí)再去支付负蚊,支付完成后發(fā)現(xiàn)并沒(méi)有得到“支付成功”的通知,這是因?yàn)槲覀儧](méi)有修改訂單狀颓哮。在微信的支付業(yè)務(wù)流程中家妆,我們還沒(méi)有做處理微信異步通知結(jié)果這一步。
所以我們下一步的工作就是:接受微信的異步通知結(jié)果冕茅,并根據(jù)結(jié)果更改訂單的支付狀態(tài)伤极。
微信異步通知
在微信內(nèi)H5調(diào)起支付時(shí),前端也可以接收到一個(gè)是否成功的標(biāo)志姨伤。注意這行注釋哨坪,我們知道不能通過(guò)get_brand_wcpay_request的值去判斷是否支付成功。因?yàn)樵谇岸苏С摯a是有可能被篡改的当编。更安全的方式是根據(jù)后端的異步通知來(lái)確定是否支付成功。
在PayController Class中加入一個(gè)接受微信異步通知的方法notify徒溪。直接使用SDK中的notify處理方法忿偷。[代碼7]
@PostMapping("/notify")
public void notify(@RequestBody String notifyData){
payService.notify(notifyData);
}
將異步通知的邏輯寫(xiě)入PayService、PayServiceImpl词渤。[代碼8]
@Override
public PayResponse notify(String notifyData) {
PayResponse payResponse = bestPayService.asyncNotify(notifyData);
log.info("【微信支付】異步通知牵舱,payResponse={}", payResponse);
return payResponse;
}
同時(shí)需要在配置文件application.yml
文件中配置notify地址。
支付成功后缺虐,需要修改訂單的支付狀態(tài)芜壁。也就是更改一下代碼8中的內(nèi)容饼丘。[代碼9]
@Override
public PayResponse notify(String notifyData) {
PayResponse payResponse = bestPayService.asyncNotify(notifyData);
log.info("【微信支付】異步通知绎速,payResponse={}", payResponse);
// 修改訂單支付狀態(tài)
// 1 先查詢一下當(dāng)前訂單狀態(tài)
OrderDTO orderDTO = orderService.findOne(payResponse.getOrderId());
// 2 修改訂單狀態(tài)
orderService.paid(orderDTO);
return payResponse;
}
此時(shí)卑惜,我們可以發(fā)現(xiàn)代碼安全性不足嗦锐。在微信異步通知中荚恶,有幾方面需要注意:
- 驗(yàn)證簽名(驗(yàn)證一下這個(gè)簽名是不是真正來(lái)自于微信涕滋,不然別人模擬一個(gè)微信驗(yàn)證請(qǐng)求剪菱,我們也會(huì)傻fufu的通過(guò))
- 支付的狀態(tài)(雖然會(huì)得到異步通知栖茉,但是消息的內(nèi)容不一定是支付成功罪裹,也有失敗等多種情況)
- 支付金額(有可能程序錯(cuò)誤饱普,導(dǎo)致微信回調(diào)之后的金額不夠統(tǒng)一,所以需要校驗(yàn)金額)
- 付款人(下單人 == 支付人)(根據(jù)業(yè)務(wù)需要確定下單人和支付人是否一直状共,所以根據(jù)情況可以校驗(yàn)確認(rèn)一下)
由于使用了SDK套耕,所以第1、2點(diǎn)是不需要我們?nèi)プ龅南考獭4a中我們還需要做第3步冯袍。
在判斷金額中,要判斷微信返回金額與系統(tǒng)金額是否一致,不僅需要保證二者的數(shù)據(jù)類型相同康愤,也需要精度一致儡循。所以把判斷金額這個(gè)部分寫(xiě)入了單獨(dú)的utils
MathUtil.class [代碼10]
public class MathUtil {
private static final Double Money_Range = 0.01;
public static Boolean equals(Double d1, Double d2){
Double result = Math.abs(d1 - d2);
if (result < Money_Range){
return true;
} else {
return false;
}
}
}
完成MathUtil.class之后,我們也需要相應(yīng)的更改代碼9征冷。[代碼11]
@Override
public PayResponse notify(String notifyData) {
PayResponse payResponse = bestPayService.asyncNotify(notifyData);
log.info("【微信支付】異步通知择膝,payResponse={}", payResponse);
// 修改訂單支付狀態(tài)
// 1 先查詢一下當(dāng)前訂單
OrderDTO orderDTO = orderService.findOne(payResponse.getOrderId());
// 2 判斷訂單是否存在
if (orderDTO == null) {
log.error("【微信支付】異步通知,訂單不存在资盅。orderId={}", payResponse.getOrderId());
throw new SellException(ResultEnum.ORDER_NOT_EXIST);
}
// 3 判斷金額是否一致(因?yàn)楹芏嗯袛嘀械鏖捎诰鹊牟煌瑫?huì)判斷兩個(gè)金額不一致呵扛,比如0.10和0.1;所以采用相減的方式筐带,寫(xiě)在util工具類中)
if (!MathUtil.equals(payResponse.getOrderAmount(), orderDTO.getOrderAmount().doubleValue())) {
log.error("【微信支付】異步通知今穿,訂單不存在。orderId={}伦籍, 微信通知金額={}蓝晒, 系統(tǒng)金額 ={}", payResponse.getOrderId(), payResponse.getOrderAmount(), orderDTO.getOrderAmount());
throw new SellException(ResultEnum.WXPAY_NOTIFY_MONEY_VERIFY_ERROR);
}
// 4 2、3步都通過(guò)后帖鸦,再修改訂單狀態(tài)
orderService.paid(orderDTO);
return payResponse;
}
根據(jù)微信支付業(yè)務(wù)流程芝薇,在支付成功后需要給微信返回“支付通知”,否則將會(huì)一直回調(diào)PayService 中的notify
作儿。如圖是微信支付成功的API文檔洛二。
在PayController Class中,同發(fā)起支付一樣攻锰,選擇返回
ModelAndView
模版晾嘶,完成微信異步通知,也就是完善代碼5中的代碼娶吞。[代碼12]
@PostMapping("/notify")
public ModelAndView notify(@RequestBody String notifyData) {
payService.notify(notifyData);
// 返回給微信處理結(jié)果
return new ModelAndView("pay/success");
}
到這里垒迂,微信公眾號(hào)支付的流程就全部結(jié)束了。
代碼:JsonUtil
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
public class JsonUtil {
public static String toJson(Object object) {
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.setPrettyPrinting();
Gson gson = gsonBuilder.create();
return gson.toJson(object);
}
}