微信公眾號(hào)開(kāi)發(fā)--支付完整流程

微信公眾號(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ù)流程”逐步完成。
JSAPI支付詳細(xì)

PS.其實(shí)下圖鏈接網(wǎng)頁(yè)授權(quán)獲取用戶openid接口文檔也是真的寫(xiě)的很清楚了磁椒。當(dāng)trade_type=JSAPI時(shí)(即公眾號(hào)支付)堤瘤,openId必傳。
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/myselfhttp://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炫欺。
參數(shù)說(shuō)明

用戶同意授權(quán)后,跳轉(zhuǎn)到redirect_uri并返回code熏兄。
用戶同意授權(quán)
3. 換取access_token

獲取了code之后品洛,以code作為票據(jù)再換access_token。以下是微信文檔的相應(yīng)截圖摩桶,我們只需要了解的就是替換掉鏈接中的appId為自己的appId和code為剛剛后臺(tái)獲取的code(注:code時(shí)效只有5min)桥状。
換取access_token
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。
JSON數(shù)據(jù)包

更多使用蔗崎,可以仔細(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ò)程:

  1. 重定向到 /sell/pay/create,攜帶參數(shù)(orderId:123456returnUrl:http://xxx.com/abc/order/123456
  2. 最后返回到 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ù)滞欠。

之后會(huì)返回如下圖所示的前端代碼古胆,這個(gè)代碼就是最后生成微信支付頁(yè)的部分。

所以在代碼部分筛璧,我們接下來(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)代碼安全性不足嗦锐。在微信異步通知中荚恶,有幾方面需要注意:

  1. 驗(yàn)證簽名(驗(yàn)證一下這個(gè)簽名是不是真正來(lái)自于微信涕滋,不然別人模擬一個(gè)微信驗(yàn)證請(qǐng)求剪菱,我們也會(huì)傻fufu的通過(guò))
  2. 支付的狀態(tài)(雖然會(huì)得到異步通知栖茉,但是消息的內(nèi)容不一定是支付成功罪裹,也有失敗等多種情況)
  3. 支付金額(有可能程序錯(cuò)誤饱普,導(dǎo)致微信回調(diào)之后的金額不夠統(tǒng)一,所以需要校驗(yàn)金額)
  4. 付款人(下單人 == 支付人)(根據(jù)業(yè)務(wù)需要確定下單人和支付人是否一直状共,所以根據(jù)情況可以校驗(yàn)確認(rèn)一下)

由于使用了SDK套耕,所以第1、2點(diǎn)是不需要我們?nèi)プ龅南考獭4a中我們還需要做第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);
    }
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末妒蛇,一起剝皮案震驚了整個(gè)濱河市机断,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌绣夺,老刑警劉巖吏奸,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異乐导,居然都是意外死亡苦丁,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)物臂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)旺拉,“玉大人产上,你說(shuō)我怎么就攤上這事《旯罚” “怎么了晋涣?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)沉桌。 經(jīng)常有香客問(wèn)我谢鹊,道長(zhǎng),這世上最難降的妖魔是什么留凭? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任佃扼,我火速辦了婚禮,結(jié)果婚禮上蔼夜,老公的妹妹穿的比我還像新娘兼耀。我一直安慰自己,他們只是感情好求冷,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布瘤运。 她就那樣靜靜地躺著,像睡著了一般匠题。 火紅的嫁衣襯著肌膚如雪拯坟。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,692評(píng)論 1 305
  • 那天韭山,我揣著相機(jī)與錄音郁季,去河邊找鬼。 笑死掠哥,一個(gè)胖子當(dāng)著我的面吹牛巩踏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播续搀,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼塞琼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了禁舷?” 一聲冷哼從身側(cè)響起彪杉,我...
    開(kāi)封第一講書(shū)人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎牵咙,沒(méi)想到半個(gè)月后派近,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡洁桌,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年渴丸,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡谱轨,死狀恐怖戒幔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情土童,我是刑警寧澤诗茎,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站献汗,受9級(jí)特大地震影響敢订,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜罢吃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一楚午、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧尿招,春花似錦醒叁、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)啊易。三九已至吁伺,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間租谈,已是汗流浹背篮奄。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留割去,地道東北人窟却。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像呻逆,于是被迫代替她去往敵國(guó)和親夸赫。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355