Fork me on GitHub
余鸢

微信公众号开发(十):微信小程序支付和订单以及打包编译

小程序

从后端接收来自客户端发过来的code,通过code获取openid和sessionKey,根据openid和sessionKey生成用户专属的token存入到用户数据库里,也可以存入Redis或者MongooDb里,同时把token返回给小程序。当用户再次打开小程序时会把之前的token或者sessionKey来识别用户,把用户从Redis或者MongooDb里检索出来,来锁定并维持用户的的登录状态

登录

新建mina.js用来专门存放小程序的路由。小程序可以通过微信官方提供的登录能力方便地获取微信提供的用户身份标识,快速建立小程序内的用户体系。

server/routes/mina.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import { controller, get, post, required } from '../decorator/router'
import config from '../config'
@controller('/mina')
export class MinaController {
@get('/login')
@required({ body: ['code', 'avatarUrl', 'nickName'] })
async login (ctx, next) {
const { code, avatarUrl, nickName } = ctx.request.body
try {
const { openid, unionid } = await openidAndSessionKey(code)
let user = await user.findOne({
openid
}).exec()
if (!user) {
user = new user({
openid: [openid],
nickName: nickName,
unionid,
avatarUrl
})
user = await user.save()
} else {
user.avatarUrl = avatarUrl
user.nickName = nickName
user = await user.save()
}
ctx.body = {
success: true,
data: {
nickName: nickName,
avatarUrl: avatarUrl
}
}
} catch (e) {
ctx.body = {
success: false,
err: e
}
}
}
}

注意:只开发微信网页的话用openid就可以,如果是开发小程序或多个产品时需要通过开放平台绑定公众号、小程序拿到unionid,unionid是跨平台的,一个用户在各个产品下都是一个unionid,如果是通过openid来辨识的话,openid是会变化的

使用code获取 session_key 和 openid

构建小程序函数。接收参数code,声明配置参数opts,发送请求传入opts返回数据

server/wechat-lib/mina.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import config from '../config'
import rp from 'request-promise'
export const openidAndSessionKey = async code => {
let opts = {
uri: 'https://api.weixin.qq.com/sns/jscode2session',
qs: {
appid: config.mina.appid,
secret: config.mina.secret,
grant_type: 'authorization_code'
},
json: true
}
opts.qs.js_code = code
let res = await rp(opts)
return res
}

获取sessionKey

server/routers/mina.js

1
2
3
4
5
6
7
8
9
10
11
@get('codeAndSessionKey')
@required({ query: ['code']})
async getCodeAndSessionKey (ctx, next) {
const { code } = ctx.query
let res = await openidAndSessionKey(code)
ctx.body = {
success: true,
data: res
}
}

获取用户资料

通过openidAndSessionKey(code)拿到用户资料,在数据里查到user,对拿到的数据进行加密解密

server/routers/mina.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@get('user')
@required({ query: ['code', 'userInfo'] })
async getUser (ctx, next) {
const { code, userInfo } = ctx.query
const minaUser = await openidAndSessionKey(code)
let user = await User.findOne({openid}).exec()
if (!user) {
let pc = new WXBizDataCrypt(minaUser.sessionKey)
let data = pc.decryptData(userInfo.encryptedData, userInfo.iv)
try {
user = await User.findOne({openid: data.openId})
if (!user) {
let _userData = userInfo.userInfo
user = new User({
avatarUrl: _userData.avatarUrl,
nickName: _userData.nickName,
unionid: data.unionid,
openid: [minaUser.openid],
sex: _userData.gender,
country: _userData.country,
province: _userData.province,
city: _userData.city
})
await user.save()
}
} catch (e) {
return (ctx.body = {
success: false,
err: e
})
}
}
ctx.body = {
success: true,
data: {
nickName: user.nickName,
avatarUrl: user.avatarUrl,
sex: user.sex
}
}
}

解密算法

小程序通过getUserInfo拿到用户资料和敏感数据,需要对敏感数据解密

server/wechat-lib/mina.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
export class WXBizDataCrypt {
constructor (sessionKey) {
this.appId = config.mina.appid
this.sessionKey = sessionKey
}
decryptData (encryptedData, iv) {
// base64 decode
let decoded
let sessionKey = new Buffer(this.sessionKey, 'base64')
encryptedData = new Buffer(encryptedData, 'base64')
iv = new Buffer(iv, 'base64')
try {
// 解密
let decipher = crypto.createDecipheriv('aes-128-cbc', sessionKey, iv)
// 设置自动 padding 为 true,删除填充补位
decipher.setAutoPadding(true)
decoded = decipher.update(encryptedData, 'binary', 'utf8')
decoded += decipher.final('utf8')
decoded = JSON.parse(decoded)
} catch (err) {
throw new Error('Illegal Buffer')
}
if (decoded.watermark.appid !== this.appId) {
throw new Error('Illegal Buffer')
}
return decoded
}
}

输入根路径进入index.vue页面跳转到二跳页面auth.vue拿到code,用code换token,再通过token拿到unionId和openid获取用户资料

公众号商城支付

增加中间件控制

pages/deal/index.vuepage/shopping/index.vue 都各自加上中间件

1
middleware: 'wechat-auth',

添加购买/取消动画框

点击购买后跳出弹出框

pages/deal/index.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
transition(name='slide-top')
payment-modal(v-if='showInfo')
.payment-modal-header
span 准备购买
span(@click='showInfo = false') 取消
.payment-modal-body
.info-item
img(:src='imageCDN2 + product.images[0]')
div
p {{ product.title }}
p 价格 ¥{{ product.price }}
.info-item
span 收件人
input(v-model.trim='info.name' placeholder='你的名字')
.info-item
span 电话
input(v-model.trim='info.phoneNumber' type='tel' placeholder='你的电话')
.info-item
span 地址
input(v-model.trim='info.address' type='tel' placeholder='收货地址是?')
.payment-modal-footer(@click='handPayment') 确认支付
transition(name='fade')
span.modal(v-if='modal.visible') {{ modal.content}}

添加控制数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
data () {
return {
// 是否弹出信息框
showInfo: false,
info: {
name: '',
phoneNumber: '',
address: ''
},
modal: {
visible: false,
content: '成功',
timer: null
}
}
}

添加控制modal方法

1
2
3
4
5
6
7
8
function toggleModal(obj, content) {
clearTimeout(obj.timer)
obj.visible = true
obj.content = content
obj.timer = setTimeout(() => {
obj.visible = fade
}, 1500)
}

唤起支付方法

拿到用户信息,判断字段是否填写正确,接着创建订单,处理数据成功后的处理,调用微信支付方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
methods: {
async handPayment(item) {
const that = this
const { name, address, phoneNumber } = this.info
if (!name || !address || !phoneNumber) {
toggleModal(this.modal, '收获信息忘填了哦~')
return
}
// 创建订单
const res = await this.$store.dispatch('createOrder', {
productId: this.product.id
name: name,
address: address,
phoneNumber: phoneNumber
})
const data = res.data
if (!data || !data.success) {
toggleModal(this.modal, '服务器异常,请等待后重新尝试')
return
}
// 调用微信支付
window.wx.chooseWXPay({
})
}
}

支付接口调用

简要的说明下微信支付业务流程:

  1. 从客户端拿到预付订单
  2. 用户通过js-sdk发起微信支付的请求
  3. 对支付成功或失败的信息做处理

添加中间件

在页面初始化时,跟服务器端做加密的动作,让我们在微信网页的环境下把当前的域名包括页面注册到微信环境里,允许我们调用接口。

wechatInit中要做的工作其实是让微信知道在当前url能调用某些微信api接口的能力,发送getWechatSignature事件,在服务器端针对url进行加密,把签名值返回给前端。拿到全局微信sdk对象,通过config接口注入权限验证配置,通过ready接口处理成功验证,初始化微信按钮

static/mixins/wechat.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
export default {
methods: {
async wechatInit(url) {
const res = await this.$store.dispatch('getWechatSignature', url)
const { data, success } = res.data
if (!success) throw new Error('不能成功获取服务器签名!')
const wx = window.wx
wx.config({
// 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
debug: true,
appId: data.appId, // 必填,公众号的唯一标识
timestamp: data.timestamp, // 必填,生成签名的时间戳
nonceStr: data.noncestr, // 必填,生成签名的随机串
signature: data.signature,// 必填,签名,见附录1
jsApiList: [
'previewImage',
'hideAllNonBaseMenuItem',
'showMenuItems',
'onMenuShareTimeline',
'onMenuShareAppMessage',
'chooseWXPay'
] // 必填,需要使用的 JS 接口列表,所有JS接口列表见附录2
})
wx.ready(() => {
// this.wechatSetMenu()
})
}
}
}

添加实际支付行为

在deal页面中获取到当前商品后初始化网页端的微信支付行为,引入wechat中间件

pages/deal/index.vue

1
2
3
import wechat from '../static/mixins/wechat.js'
mixins: [wechat],

发起支付请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
window.wx.chooseWXPay({
timestamp: data.timestamp,
nonceStr: data.nonceStr,
package: data.package,
signType: data.signType,
paySign: data.paySign,
success: (response) => {
try {
window.WeixinJSBridge.log(response.err_msg)
} catch (e) {
console.error(e)
}
// 支付成功
if (response.err_msg === 'get_brand_wcpay_request:ok') {
toggleModal(that.modal, '支付成功')
}
}
})

注意:

到这里可能要止步了,没有支付权限,原因是没有认证过的服务号才可以申请微信支付功能,如果是订阅号,认证和不认证都不能用!

微信支付流程

微信支付分为下面几个步骤:
1、申请一个微信公众服务号
2、认证微信公众服务号
3、认证之后才可以做微信支付模块下在公众平台下微信支付功能权限的申请,申请后才有权限
4、在微信商户里设置网页授权的域名、js安全接口域名、业务域名等等,并且这个域名需要是备案过的,否则不能使用微信的支付功能
5、到商户平台->产品中心->开发配置,添加公众号支付授权目录
6、下载官方的示例代码,基于这个代码选择你要用的语言代码拷贝到你的项目目录
7、到商户平台下载API证书,设置API密钥(需要在服务器端基于证书和密钥在服务器上生成预支付订单,这两个没有设置也是不能使用微信的支付功能)

订单

订单数据

定义订单数据字段,需要关联两个对象user和product,payType表示支付方式(比如:支付宝、网银)

server/database/schema/payment.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const mongoose = require('mongoose')
const { Schema } = mongoose
const Mixed = Schema.Types.Mixed
const ObjectId = Schema.Types.ObjectId
const PaymentSchema = new Schema({
user: {
type: ObjectId,
ref: 'User'
},
product: {
type: ObjectId,
ref: 'Product'
},
payType: String,
totalFee: Number,
name: String,
phoneNumber: [String],
address: String,
description: String,
order: Mixed,
success: {
type: Number,
default: 0
},
meta: {
createdAt: {
type: Date,
default: Date.now()
},
updatedAt: {
type: Date,
default: Date.now()
}
}
})
PaymentSchema.pre('save', function (next) {
if (this.isNew) {
this.meta.createdAt = this.meta.updatedAt = Date.now()
} else {
this.meta.updatedAt = Date.now()
}
next()
})
mongoose.model('Payment', PaymentSchema)

在存储数据之前判断,如果是新建数据的话同步更新时间。

创建订单

server/routes/wechat.js

1
2
3
4
5
@post('/wx-pay')
@required({ body: ['productId', 'name', 'phoneNumber', 'address' ]})
async createdOrder (ctx, next) {
await wechatPay(ctx, next)
}

向微信支付系统发起支付请求

有时候在本地做域名代理时可能会带::ffff:类似于这样的标识,替换成''即可。

用户初次登录公众号网页时会进行授权,授权后就持有用户的信息,就可以持久化session。通过productId获得product,判断它的状态。

通过session.user.unionid查找到user,判断如果不存在,创建User存入数据库。

定义订单参数,生成预支付订单,继而生成订单。

server/controllers/wechat.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
export async function wechatPay (code) {
const ip = ctx.ip.replace('::ffff:', '')
const session = ctx.session
const { productId, name, phoneNumber, address } = ctx.request.body
const product = await api.product.findProduct(productId)
if (!product) {
return (ctx.body = {
success: false,
err: '这个宝贝不在了'
})
}
try {
let user = await api.user.findUserByUnionId(session.user.unionid).exec()
if (!user) {
user = await api.user.saveFromSession(session)
}
const orderParams = {
body: product.title,
attach: '公众号周边手办支付',
out_trade_no: 'Product' + (+new Date),
spbill_create_ip: ip,
// total_fee: product.price * 100,
total_fee: 0.01 * 100,
openid: session.user.unionid,
trade_type: 'JSAPI'
}
// 生成预支付订单
const order = await getParamsAsync(orderParams)
// 生成订单
const payment = await api.payment.create(user, product, order, '公众号', {
name,
address,
phoneNumber
})
ctx.body = {
success: true,
data: payment.order
}
} catch (e) {
ctx.body = {
success: false,
err: e
}
}
}

支付方法

引入第三方库wechat-pay支付方法

server/wechat-lib/pay.js

获取预支付订单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import fs from 'fs'
import config from '../config'
import wechatPay from 'wechat-pay'
import path from 'path'
const cert = path.resolve(__dirname, '../', 'config/cert/apiclient_cert.p12')
const paymentConfig = {
appId: config.shop.appId,
partnerKey: config.shop.partnerKey,
mchId: config.shop.mchId,
notifyUrl: config.shop.notifyUrl,
ptx: fs.readFileSync(cert)
}
const Payment = wechatPay.Payment
const payment = new Payment(paymentConfig || {})
export const getParamsAsync = (order) => {
return new Promise((resolve, reject) => {
payment.getBrandWCPayRequestParams(order, (err, payargs) => {
if (err) reject(err)
else resolve(payargs)
})
})
}

获取订单数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export const getPayDataAsync = (req) => {
return new Promise((resolve, reject) => {
let data = ''
req.setEncoding('utf8')
req.on('data', chunk => {
data += chunk
})
req.on('end', () => {
req.rawBody = data
resolve(data)
})
})
}

订单微信推送通知

1
2
3
4
5
6
7
8
export const getNoticeAsync = (rawBody) => {
return new Promise((resolve, reject) => {
payment.validate(rawBody, (err, message) => {
if (err) reject(err)
else resolve(message)
})
})
}

获取所有订单列表

1
2
3
4
5
6
7
8
9
10
11
export const getBillAsync = (date) => {
return new Promise((resolve, reject) => {
payment.downloadBill({
bill_date: date,
bill_type: 'ALL'
}, (err, data) => {
if (err) reject(err)
else resolve(data)
})
})
}

获取订单

1
2
3
4
5
6
7
8
export const getOrdersAsync = (params) => {
return new Promise((resolve, reject) => {
payment.orderQuery(params, (err, data) => {
if (err) reject(err)
else resolve(data)
})
})
}

失败信息

1
2
3
4
5
6
export const buildFailXML = (err) => {
return payment.buildXml({
return_code: 'FAIL',
return_msg: err.name
})
}

成功信息

1
2
3
4
5
6
7
export const buildSuccessXML = (err) => {
if (err) return buildFailXML(err)
return payment.buildXml({
return_code: 'SUCCESS'
})
}

订单管理页面

pages/admin/payments.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template lang="pug">
.content
.related-products
table.table
thead
tr
th 图片
th 标题
th 价格
th 支付价格
th 姓名
th 电话
th 地址
th 支付方式
tbody
tr(v-for='item in payments')
td
.img(v-for='image in item.images')
img(:src='imageCDN2 + image')
td {{item.product.title}}
td {{item.product.price}}
td {{item.product.totalFee}}
td {{item.name}}
td {{item.phoneNumber}}
td {{item.address}}
td {{item.payType}}
</template>

增加中间件

1
middleware: 'auth',

获取支付数据

1
2
3
async created() {
this.$store.dispatch('fetchPayments')
},

路由

server/routers/admin.js

1
2
3
4
5
6
7
8
@get('payments')
async getPayments (ctx, next) {
const data = await api.payment.fetchPayments()
ctx.body = {
success: true,
data: data
}
}

store/actions.js

1
2
3
4
5
6
async fetchPayments ({ state }) {
let { data } = await Services.getPayments()
state.payments = data.data
return data
},

请求路径

store/services.js

1
2
3
4
getPayments() {
return axios.get(`${baseUrl}/admin/payments`)
}
}

启动服务和代理

将sunny二进制文件复制到项目,另外再创建一个shell文件写入配置,配置就是启动sunny的shell命令

server/bin/ngrok

1
2
3
#!/bin/bash
./server/bin/sunny clientid 你的隧道id

添加服务启动配置,修改package.json

1
"ngrok": "./server/bin/ngrok",

给ngrok文件添加权限

1
chmod u+x ngrok

这样就可以在在同一个目录下通过npm run dev启动本地开发编译环境,同时也可以通过npm run ngrok启动代理端口的工具环境。

编译压缩

修改package.json

1
"build": "nuxt build",

执行编译命令:

1
npm run build

编译后启动命令:

1
NODE_ENV=production node start

分离本地与线上环境

区分本地环境与线上环境,通过NODE_ENV进行文件切换。

新建本地开发环境文件development.json

server/config/development.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"db": "mongodb://localhost/nuxtka",
"SITE_ROOT_URL": "http://nuxtssr.ngrok.xiaomiqiu.cn",
"wechat": {
"appID": "你的appID",
"appSecret": "你的appSecret",
"token": "你的token"
},
"qiniu": {
"AK": "你的七牛云AK",
"SK": "你的七牛云SK"
},
"mina": {
"appid": "你的公众平台的appid",
"secret": "你的公众平台的secret"
},
"shop": {
"appID": "你的 shop ID",
"mchId": "你的 shop mchId",
"notifyUrl": "接收通知的 URL",
"key": ""
}
}

线上文件production.json

server/config/production.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"db": "mongodb://localhost/nuxtka",
"SITE_ROOT_URL": "http://nuxtssr.ngrok.xiaomiqiu.cn",
"wechat": {
"appID": "你的appID",
"appSecret": "你的appSecret",
"token": "你的token"
},
"qiniu": {
"AK": "你的七牛云AK",
"SK": "你的七牛云SK"
},
"mina": {
"appid": "你的公众平台的appid",
"secret": "你的公众平台的secret"
},
"shop": {
"appID": "你的 shop ID",
"mchId": "你的 shop mchId",
"notifyUrl": "接收通知的 URL",
"key": ""
}
}

根据当前nodejs运行时NODE_ENV是开发环境还是线上环境进行切换,修改文件:

server/config/index.js

1
2
3
4
5
6
7
8
9
10
11
import _ from 'lodash'
import { resolve } from 'path'
const host = process.env.HOST || 'localhost'
const env = process.env.NODE_ENV || 'development'
const conf = require(reslove(__dirname, `./${env}.json`))
export default _.assign({
env,
host
}, conf)