微信支付

微信支付,最难的地方不在于技术,而是在于微信有一套自己的技术规范。

建议每位同学都要从官方文档看起。 虽然市面上有一些集成工具,例如 Ping++ , 但是往往这些产品不是特别方便,收费也比较高昂。 出了问题不好调试。

所以,我们对于核心技术,一定要亲自掌握。

申请微信账号和配置

这里就不详述了。 我们假设全部的账号都已经做好了。

1. 添加支付页面的路由

  1. import Pay from '@/components/pay'
  2. Vue.use(Router)
  3. export default new Router({
  4. routes: [
  5. {
  6. path: '/shops/pay',
  7. name: 'Pay',
  8. component: Pay
  9. },
  10. ]
  11. })

2. 添加vue页面

  1. <template>
  2. <div class="background">
  3. <header class="top_bar">
  4. <a onclick="window.history.go(-1)" class="icon_back"></a>
  5. <h3 class="cartname">订单支付</h3>
  6. </header>
  7. <div class="tast_list_bd" style="background-color: #F3F3F3; padding-top: 0; padding-bottom: 80px;">
  8. <div class="goods_detail" style="">
  9. <main class="detail_box">
  10. <span class="divider"></span>
  11. <form style="margin-top: 45px;">
  12. <div class="column is-12">
  13. <label class="label">收货人</label>
  14. <p class="control has-icon has-icon-right">
  15. <input name="name" v-model="mobile_user_name" v-validate="'required|required'" :class="{'input': true, 'is-danger': errors.has('name') }" type="text" placeholder="例如: 张三" autofocus="autofocus"/>
  16. <span v-show="errors.has('name')" class="help is-danger">收货人不能为空</span>
  17. </p>
  18. </div>
  19. <div class="column is-12">
  20. <label class="label">收货地址</label>
  21. <p class="control has-icon has-icon-right">
  22. <input name="url" v-model="mobile_user_address" v-validate="'required|required'" :class="{'input': true, 'is-danger': errors.has('url') }" type="text" placeholder="例如: 北京市朝阳区大望路西西里小区4栋2单元201"/>
  23. <span v-show="errors.has('url')" class="help is-danger">收货地址不能为空</span>
  24. </p>
  25. </div>
  26. <div class="column is-12">
  27. <label class="label">收货电话</label>
  28. <p class="control has-icon has-icon-right">
  29. <input name="phone" v-model="mobile_user_phone" v-validate="'required|numeric'" :class="{'input': true, 'is-danger': errors.has('phone') }" type="text" placeholder="例如: 18888888888"/>
  30. <span v-show="errors.has('phone')" class="help is-danger">电话号码不能为空</span>
  31. </p>
  32. </div>
  33. </form>
  34. <span class="divider"></span>
  35. <section class="product_info clearfix" v-if="single_pay">
  36. <div>
  37. <div class="fu_li_zhuan_qu" >
  38. <img :src="good_images[0]" class="logo_image"/>
  39. <div class="content" >
  40. <div class="title">
  41. {{good.name}}
  42. </div>
  43. <div class="logo_and_shop_name">
  44. <div class="product_pric">
  45. <span></span>
  46. <span class="rel_price">{{good.price}}</span>
  47. <span> &nbsp x {{buy_count}}</span>
  48. </div>
  49. </div>
  50. </div>
  51. </div>
  52. </div>
  53. </section>
  54. <section class="product_info clearfix" v-else v-for="product in cartProducts">
  55. <div>
  56. <div class="fu_li_zhuan_qu" >
  57. <img :src="product.image" class="logo_image"/>
  58. <div class="content" >
  59. <div class="title">
  60. {{product.title}}
  61. </div>
  62. <div class="logo_and_shop_name">
  63. <div class="product_pric">
  64. <span></span>
  65. <span class="rel_price">{{product.price}}</span>
  66. <span> &nbsp x {{product.quantity}}</span>
  67. </div>
  68. </div>
  69. </div>
  70. </div>
  71. </div>
  72. </section>
  73. <section>
  74. <span class="divider" style="height: 15px;"></span>
  75. <div class="extra_cost" style=" ">
  76. <span style="float: left; margin-left: 15px;"> 卖家留言:</span>
  77. <input v-model="guest_remarks" id="extra_charge" type="text" name="cost" placeholder="选填: 对本次交易的说明" style="border: 0; background-color: white;
  78. font-size: 15px; color: #48484b; outline: none; width: 60%;"></input>
  79. </div>
  80. </section>
  81. <section>
  82. <span class="divider"></span>
  83. <div class="extra_cost" style=" ">
  84. <span style="float: left; margin-left: 15px;"> 应付金额:</span>
  85. <div v-if="single_pay" class="rel_price" type="text" name="cost" style="border: 0; background-color: white;
  86. font-size: 20px; color: #ff621a; font-weight: bold; outline: none; text-align: right; padding-right: 20px;"> \{\{ total_cost | currency }}</div>
  87. <div v-else class="rel_price" type="text" name="cost" style="border: 0; background-color: white;
  88. font-size: 20px; color: #ff621a; font-weight: bold; outline: none; text-align: right; padding-right: 20px;"> \{\{ total | currency }}</div>
  89. </div>
  90. </section>
  91. </main>
  92. <span class="divider"></span>
  93. <div style="height: 55px; display: flex; width: 100%; padding: 0px 10px; background-color: #fff;" @click="">
  94. <div style="flex: 1; display: flex;">
  95. <div style="margin-top: 10px;">
  96. <img src="../../assets/微信icon@3x.png" style="width: 35px;"/>
  97. </div>
  98. <span style="margin-top: 8px; font-size: 18px; line-height:40px; margin-left: 10px;">微信支付</span>
  99. </div>
  100. <div style=" padding: 14px 10px;" @click="user_wechat">
  101. <img src="../../assets/选中3x.png" style="width: 28px;"/>
  102. </div>
  103. </div>
  104. </div>
  105. </div>
  106. <div class="shop_layout-scroll-absolute" style="">
  107. <div class="queding" @click="buy">
  108. 立即支付
  109. </div>
  110. </div>
  111. </div>
  112. </template>
  113. <script>
  114. import { go } from '../../libs/router'
  115. import { mapGetters } from 'vuex'
  116. export default{
  117. data(){
  118. return {
  119. good_images: [],
  120. good: "",
  121. buy_count: this.$route.query.buy_count,
  122. good_id: this.$route.query.good_id,
  123. open_id: this.$store.state.userInfo.open_id,
  124. mobile_user_address: '',
  125. mobile_user_name: '',
  126. mobile_user_phone: '',
  127. guest_remarks: '',
  128. is_use_wechat: false,
  129. }
  130. },
  131. watch:{
  132. },
  133. mounted(){
  134. if (this.single_pay) {
  135. this.$http.get(this.$configs.api + 'goods/goods_details?good_id=' + this.good_id).then((response)=>{
  136. console.info(this.good_id)
  137. console.info(response.body)
  138. this.good = response.body.good
  139. this.good_images = response.body.good_images
  140. },(error) => {
  141. console.error(error)
  142. });
  143. }
  144. },
  145. computed: {
  146. total () {
  147. return this.cartProducts.reduce((total, p) => {
  148. return (total + p.price * p.quantity)
  149. }, 0)
  150. },
  151. single_pay () {
  152. return this.good_id && this.buy_count
  153. },
  154. total_cost () {
  155. return this.good.price * this.buy_count
  156. },
  157. ...mapGetters({
  158. cartProducts: 'cartProducts',
  159. checkoutStatus: 'checkoutStatus'
  160. })
  161. },
  162. methods:{
  163. validateBeforeSubmit() {
  164. //拦截异步操作
  165. return new Promise((resolve, reject) => {
  166. this.$validator.validateAll().then(result => {
  167. console.info(result)
  168. if (result) {
  169. console.info("============表单验证成功===")
  170. resolve(true);
  171. } else {
  172. alert('请填写完整的收货信息!');
  173. resolve(false);
  174. }
  175. });
  176. })
  177. },
  178. plus () {
  179. this.buy_count = this.buy_count + 1
  180. },
  181. minus () {
  182. if(this.buy_count > 1) {
  183. this.buy_count = this.buy_count - 1
  184. }
  185. },
  186. user_wechat () {
  187. if (this.is_use_wechat === false) {
  188. this.is_use_wechat = true
  189. } else {
  190. this.is_use_wechat = false
  191. }
  192. },
  193. buy (){
  194. let result = this.validateBeforeSubmit().then((resolve)=>{
  195. if (resolve) {
  196. console.info('true ==== ')
  197. let params
  198. if (this.single_pay) {
  199. params = {
  200. good_id: this.good_id,
  201. buy_count: this.buy_count,
  202. total_cost: this.total_cost,
  203. guest_remarks: this.guest_remarks,
  204. mobile_user_address: this.mobile_user_address,
  205. mobile_user_name: this.mobile_user_name,
  206. mobile_user_phone: this.mobile_user_phone,
  207. open_id: this.open_id
  208. }
  209. } else {
  210. console.info(this.total)
  211. params = {
  212. goods: this.cartProducts,
  213. total_cost: this.total,
  214. guest_remarks: this.guest_remarks,
  215. mobile_user_address: this.mobile_user_address,
  216. mobile_user_name: this.mobile_user_name,
  217. mobile_user_phone: this.mobile_user_phone,
  218. open_id: this.open_id
  219. }
  220. }
  221. this.$http.post(this.$configs.api + 'goods/buy', params
  222. ).then((response) => {
  223. let order_number = response.body.order_number
  224. this.purchase(order_number)
  225. }, (error) => {
  226. console.error(error)
  227. });
  228. } else {
  229. console.info('== 请填写完整的收货信息')
  230. }
  231. });
  232. },
  233. purchase (order_number) {
  234. //调起微信支付界面
  235. if (typeof WeixinJSBridge == "undefined"){
  236. if( document.addEventListener ){
  237. document.addEventListener('WeixinJSBridgeReady', this.onBridgeReady, false);
  238. }else if (document.attachEvent){
  239. document.attachEvent('WeixinJSBridgeReady', this.onBridgeReady);
  240. document.attachEvent('onWeixinJSBridgeReady', this.onBridgeReady);
  241. }
  242. }else{
  243. this.onBridgeReady(order_number);
  244. }
  245. },
  246. onBridgeReady (order_number) {
  247. let that = this
  248. let total_cost
  249. if (this.single_pay) {
  250. total_cost = this.total_cost
  251. } else {
  252. total_cost = this.total
  253. }
  254. this.$http.post(this.$configs.api + 'payments/user_pay',
  255. {
  256. open_id: this.$store.state.userInfo.open_id,
  257. total_cost: total_cost,
  258. order_number: order_number
  259. }).then((response) => {
  260. WeixinJSBridge.invoke(
  261. 'getBrandWCPayRequest', {
  262. "appId": response.data.appId,
  263. "timeStamp": response.data.timeStamp,
  264. "nonceStr": response.data.nonceStr,
  265. "package": response.data.package,
  266. "signType": response.data.signType,
  267. "paySign": response.data.paySign
  268. },
  269. function(res){
  270. // 下面代码仅用于调试
  271. // alert("res.err_msg: " + res.err_msg + ", err_desc: " + res.err_desc)
  272. if(res.err_msg == "get_brand_wcpay_request:ok" ) {
  273. // 使用以上方式判断前端返回,微信团队郑重提示:res.err_msg将在用户支付成功后返回 ok,但并不保证它绝对可靠。
  274. that.$router.push({ path: '/shops/paysuccess?order_id=' + order_number });
  275. } else {
  276. // 显示取消支付或者失败
  277. that.$router.push({ path: '/shops/payfail?order_id=' + order_number });
  278. }
  279. }
  280. );
  281. }, (error) => {
  282. console.error(error)
  283. });
  284. }
  285. },
  286. }
  287. </script>

核心代码如下:

  1. onBridgeReady (order_number) {
  2. //....
  3. this.$http.post(this.$configs.api + 'payments/user_pay',
  4. {
  5. open_id: this.$store.state.userInfo.open_id,
  6. total_cost: total_cost,
  7. order_number: order_number
  8. }).then((response) => {
  9. WeixinJSBridge.invoke(
  10. 'getBrandWCPayRequest', {
  11. "appId": response.data.appId,
  12. "timeStamp": response.data.timeStamp,
  13. //....
  14. },
  15. function(res){
  16. //...
  17. }
  18. );
  19. }, (error) => {
  20. console.error(error)
  21. });
  22. }

上面的代码是用来给页面一准备好( WeixinJSBridge 准备好了)的时候,页面就要调用的。

  1. purchase (order_number) {
  2. //调起微信支付界面
  3. if (typeof WeixinJSBridge == "undefined"){
  4. if( document.addEventListener ){
  5. document.addEventListener('WeixinJSBridgeReady', this.onBridgeReady, false);
  6. }else if (document.attachEvent){
  7. document.attachEvent('WeixinJSBridgeReady', this.onBridgeReady);
  8. document.attachEvent('onWeixinJSBridgeReady', this.onBridgeReady);
  9. }
  10. }else{
  11. this.onBridgeReady(order_number);
  12. }
  13. },

上面的代码用于调用出 “微信支付页面”. 其中的变量 WeixinJSBridge 是微信浏览器自带的变量, 不必声名,直接拿过来用就行。

看效果

微信的支付页面会跳出来 (图略)

总结

可以看到:

  1. 微信支付的细节处理,都交给了后台服务器端。 只要我们H5端把参数准备好,直接访问 http://shopweb.siwei.me/api/payments/user_pay 就可以了。
  2. 微信支付,分成单笔商品支付和多笔商品支付两种情况. 区别就是把参数重新组织一下即可。
  3. 新手对于 WeixinJSBridge 这个变量很难掌握, 一定要多看文档。 这个文档是 “微信公众号内支付”的文档, 不是微信APP , 或者微信普通H5的文档。 一定要梳理好逻辑。 另外,对于后端API的同学这里更难,建议多查多试。
  4. 在微信的后台,要配置不同的支付目录。 安卓和IOS 的粗略是不一样的。 建议大家百度一下。
  5. 微信的支付场景对应的支付方式和实现方式是不一样的。 本例是“微信的公众号内支付”。

微信的官方文档中, 提供的例子都是基于经典的WEB页面(整体刷新的那种)的,目前还没有看到SPA的例子。 但是大家的问题很多。 我的个人博客也记录了一些内容: http://siwei.me/blog/posts/--27

由于篇幅限制,微信相关的内容不再赘述。