微信小程序 结合公众号前后端全栈开发微信优惠卡券
微信小程序面世几年了,相关的功能也都开发的很完善。 可当下,开发者在遇到某些项目需求的时候,还是会遇到很多很多的坑,一来微信开发者文档里面的解释经常弄得云里雾里,无从下手,二来网上各种博客写得相关教程或者内容都不太完整,或者没有进行校验核对,甚至直接copy过来得。就拿优惠券开发这块来说,微信开发者文档解释很简陋,并且还要引用关联公众号的相关配置。刚好上个项目开发了微信小程序卡券功能,所以萌生了写这么一个教程的想法,一来担心自己久了遗忘,记录一下未来用到的时候可供翻阅,二来也能提供给需要的朋友,希望能够帮助的到大家。
本人水平有限,编写中有错在所难免,如果有错误的地方望各位批评指正。
废话不多说: 先来看看实际效果图
- 开发使用的架构为:
- 前端: 微信小程序uniapp(原生通用 )
- 后端: koa
- 架构结构图:
先看看微信文档里面的卡券流程图:
可以看到, 官网流程真的苦涩难懂
注: 此次开发使用的API调用,而非公众号制作卡券, 开发进行API调用相对公众号上制作卡券更加的灵活和高度定制化
根据官网通俗理解流程如下:
- 开发准备:
- 开发者须有一个有卡券权限的公众号(服务号)和认证后的小程序账号【前提】
- 准备好卡券的店铺logo,店铺门面图,制作哪种类型的卡券资料等等
- 需要打通微信小程序和微信公众号。 打通方法是使用微信开放平台,进行注册认证。
- access_token的获取、node服务器的搭建、小程序的开发等其他功能不在本文讨论范围
- 认真阅读官方文档中制作卡券接口说明。(小程序中只有调用卡券的接口说明) 制卡官方链接文档 、 微信小程序调用卡券链接文档
- 开发调用:
- 上传logo素材到微信服务器:
- node服务器
- 注意:1. 上传的图片限制文件大小限制1MB,仅支持JPG、PNG格式。2.调用接口获取图片url仅支持在微信相关业务下使用。
function uploadwxlogo( token ,files) {
return new Promise ((resolve , reject) =>{
request.post({
url: `https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=${token}`, //微信上传卡券素材的接口地址
formData:{
buffer:{
// value: fs.readFileSync(path.join(__dirname,"./070.png")) ,
value: files.data, //必须使用formData格式进行buffer流上传
options :{
filename: files.name,
contentType: files.type
}
}
}
},(err , res) =>{
if(err){
reject(err)
}try{
const reData = JSON.parse(res.body)
resolve(reData)
}catch(e){
// console.log(e)
}
})
})}
token提示: 只能使用公众号的access_token ,而非小程序的 access_token
获取到素材回调微信服务的url,记录保存
- 上传门面素材到微信服务器(只能临时地址):
- node服务器
function uploadwxtemp( token ,files) {
// var formData = new formData()
return new Promise ((resolve , reject) =>{
request.post({
url: `https://api.weixin.qq.com/cgi-bin/media/upload?access_token=${token}&type=image`, //Url临时路径
formData:{
buffer:{
// value: fs.readFileSync(path.join(__dirname,"./070.png")) ,
value: files.data, //图片文件流
options :{
filename: files.name,
contentType: files.type
}
}
}
},(err , res) =>{
if(err){
reject(err)
}try{
const reData = JSON.parse(res.body)
resolve(reData)
}catch(e){
// console.log(e)
}
})
})}
记录微信回调的上传到微信服务器的回调url,进行卡券制作:
- 先上代码:
使用原生的 HTTP请求方式: POSTURL: https://api.weixin.qq.com/card/create?access_token=ACCESS_TOKEN 在post传递data数据给微信端的时候总是报错json格式不对,使用egg.js上传就没有问题,由于架构已定,所以退而使用的第三方的卡券npm包。 npm install wechat-cards --save
const wechatcard = require('wechat-cards')
async function postaddCard( ctx ) {
const {
token,card_type,merchant_id,logo,brand_name,CODE_TYPE_TEXT,title,color,notice,description,quantity,get_limit,bind_openid,center_title,center_app_brand_user_name
,center_app_brand_pass,promotion_app_brand_user_name,promotion_app_brand_pass,activate_app_brand_user_name,activate_app_brand_pass ,abstract ,icon_url_list ,service_phone,condition ,begin, end ,shop_id
}= ctx.request.body
wechatcard.setConfig({
accessTokenService: {
"access_token": token,
"expires_in": 11199 //token的过期时间
}
})
let ok = (begin/1000).toString() //单位为秒 。。。 重点啊 !!!! 转换为字符串 优惠券有效期开始
let ends = (end/1000).toString() //单位为秒 。。。 重点啊 !!!! 转换为字符串 优惠券有效期截至
let card = {
"card_type" : card_type, //我们传入通用券, 就是优惠券
"base_info" : {
"sub_merchant_info":{
"merchant_id": merchant_id, //可选,如果有子商铺,可以定义子商铺的id(需要审核),如果不需要,可以不用这个参数
},
"logo_url":logo , //卡券的商户logo,建议像素为300*300 //直接调用商场的logo ? 先传递好再说
"brand_name":brand_name, // "微信餐厅",商户名字,字数上限为12个汉字
"code_type":CODE_TYPE_TEXT, // 码型: "CODE_TYPE_TEXT"文 本 ; "CODE_TYPE_BARCODE"一维码 "CODE_TYPE_QRCODE"二维码
"title": title, // 卡券名,字数上限为9个汉字。(建议涵盖卡券属性、服务及金额)。
"color" :color,
// "color":color, //"Color010",卡券背景颜色
"notice":notice, //卡券使用提醒,字数上限为16个汉字。
"service_phone":service_phone, //****非必填字段 || 客服电话。
"description":"可与其他优惠同享", //卡券使用说明,字数上限为1024个汉字。
"date_info": { //使用日期,有效期的信息。
"type": "DATE_TYPE_FIX_TIME_RANGE", //"DATE_TYPE_FIX_TIME_RANGE" , DATE_TYPE_FIX TIME_RANGE 表示固定日期区间,DATE_TYPE FIX_TERM 表示固定时长 (自领取后按天算。
"begin_timestamp": ok, //type为DATE_TYPE_FIX_TIME_RANGE时专用,表示起用时间。从1970年1月1日00:00:00至起用时间的秒数,最终需转换为字符串形态传入。(东八区时间,UTC+8,单位为秒)
"end_timestamp": ends //结束时间戳 ****||表示结束时间 , 建议设置为截止日期的23:59:59过期 。 ( 东八区时间,UTC+8,单位为秒 )
// "type":"DATE_TYPE_FIX_TERM",
// "fixed_term" : 2,
// "fixed_begin_term": 1
},
"sku": { //商品信息
"quantity":quantity //卡券库存的数量,上限为100000000
},
"use_limit":get_limit, //每人可核销的数量限制,不填写默认为50。
"get_limit": get_limit, //每人可领券的数量限制,不填写默认为50。
"use_custom_code":false, //是否自定义Code码 。填写true或false,默认为false。 通常自有优惠码系统的开发者选择 自定义Code码,并在卡券投放时带入 Code码,详情见 是否自定义Code码 。
"bind_openid":false, //是否指定用户领取,填写true或false 。默认为false。通常指定特殊用户群体 投放卡券或防止刷券时选择指定用户领取。
"can_share":false, // 卡券领取页面是否可分享。*****||非必填
"can_give_friend":false,
"use_all_locations":false, //卡券是否可转赠。*****||非必填
// "location_id_list": [ //*****||非必填||门店位置poiid。 调用 POI门店管理接 口 获取门店位置poiid。具备线下门店 的商户为必填。
// 123,
// 12321,
// 345345
// ],
//"center_title":center_title, //卡券顶部居中的按钮,仅在卡券状 态正常(可以核销)时显示*****||非必填
// "center_sub_title": center_sub_title, //显示在入口下方的提示语 ,仅在卡券状态正常(可以核销)时显示。*****||非必填
// "source": "大众点评" //???
// "fixed_term": //type为DATE_TYPE_FIX_TERM时专用,表示自领取后多少天内有效,不支持填写0。||如果填写为:DATE_TYPE FIX_TERM, 则必填
// fixed_begin_term: //type为DATE_TYPE_FIX_TERM时专用,表示自领取后多少天开始生效,领取后当天生效填写0。(单位为天)
// end_time stamp: //可用于DATE_TYPE_FIX_TERM时间类型,表示卡券统一过期时间 , 建议设置为截止日期的23:59:59过期 。 ( 东八区时间,UTC+8,单位为秒 ),设置了fixed_term卡券,当时间达到end_timestamp时卡券统一过期
// "center_app_brand_user_name":center_app_brand_user_name, //自定义使用入口跳转小程序的user_name,格式为原始id+@app
//" center_app_brand_pass" :center_app_brand_pass, //
"custom_url_name": center_title, //自定义跳转的URL。
"custom_url": "http://www.qq.com", //自定义跳转外链的入口名字。
"custom_app_brand_user_name": center_app_brand_user_name,
"custom_app_brand_pass":center_app_brand_pass,
//"custom_url_sub_title": "6个汉字tips", //显示在入口右侧的提示语。
//"promotion_url_name": "更多优惠", //营销场景的自定义入口名称。
// "promotion_url": "http://www.qq.com", //营销入口跳转外链的地址链接。
// "promotion_app_brand_user_name":promotion_app_brand_user_name, //卡券跳转的小程序的user_name,仅可跳转该 公众号绑定的小程序 。格式:gh_86a091e50ad4@app
//"promotion_app_brand_pass":promotion_app_brand_pass, //promotion _app_brand_pass .格式:"API/cardPa
},
"advanced_info": { //创建优惠券特有的高级字段
"use_condition": { //使用门槛(条件)字段,若不填写使用条件则在券面拼写 :无最低消费限制,全场通用,不限品类;并在使用说明显示: 可与其他优惠共享
// "accept_category": accept_category, //指定可用的商品类目,仅用于代金券类型 ,填入后将在券面拼写适用于xxx "鞋类"
// "reject_category": reject_category, //指定不可用的商品类目,仅用于代金券类型 ,填入后将在券面拼写不适用于xxxx ,"阿迪达斯"
"can_use_with_other_discount": true, //不可以与其他类型共享门槛 ,填写false时系统将在使用须知里 拼写“不可与其他优惠共享”, 填写true时系统将在使用须知里 拼写“可与其他优惠共享”, 默认为true
},
"abstract": { //封面摘要结构体名称
"abstract":abstract, //封面摘要简介
"icon_url_list": [
icon_url_list //封面图片列表,仅支持填入一 个封面图片链接, 上传图片接口 上传获取图片获得链接,填写 非CDN链接会报错,并在此填入。 建议图片尺寸像素850*350 "http://mmbiz.qpic.cn/mmbiz/p98FjXy8LacgHxp3sJ3vn97bGLz0ib0Sfz1bjiaoOYA027iasqSG0sjpiby4vce3AtaPu6cIhBHkt6IjlkY9YnDsfw/0"
]
},
// "text_image_list": [ //图文列表,显示在详情内页 ,优惠券券开发者须至少传入 一组图文列表
// {
// "image_url": image_url, //图片链接,必须调用 上传图片接口 上传图片获得链接,并在此填入, 否则报错
// "text": text //图文描述
// }],
"time_limit": [ //使用时段限制,包含以下字段
// {
// "type": "MONDAY", //限制类型枚举值:支持填入 MONDAY 周一 TUESDAY 周二 WEDNESDAY 周三 THURSDAY 周四 FRIDAY 周五 SATURDAY 周六 SUNDAY 周日 此处只控制显示, 不控制实际使用逻辑,不填默认不显示
// "begin_hour":0, // 当前type类型下的起始时间(小时) ,如当前结构体内填写了MONDAY, 此处填写了10,则此处表示周一 10:00可用
// "end_hour":10, //当前type类型下的结束时间(小时) ,如当前结构体内填写了MONDAY, 此处填写了20, 则此处表示周一 10:00-20:00可用
// // "begin_minute":10,
// // "end_minute":59
// },
{
"type": "HOLIDAY"
}
],
"business_service": [ //商家服务类型: BIZ_SERVICE_DELIVER 外卖服务; BIZ_SERVICE_FREE_PARK 停车位; BIZ_SERVICE_WITH_PET 可带宠物; BIZ_SERVICE_FREE_WIFI 免费wifi, 可多选
"BIZ_SERVICE_FREE_WIFI",
"BIZ_SERVICE_WITH_PET",
"BIZ_SERVICE_FREE_PARK",
"BIZ_SERVICE_DELIVER"
]
},
// "deal_detail" : "123"
"default_detail" : description
}
const p = await wechatcard.card.createCard(card)
.then(result => {
return result
})
.catch(e => console.log('创建失败', e))
if(p.card_id){
ctx.body={
code:200,
p
}
}
}
获取到了微信卡券回调到的card_id号 !!!注意: 不是code号码!
card_id 类似卡券是谁的卡, 比如门禁卡, 上班卡等等, code 类似卡的字面数字, 比如身份证号码, 上面进行的是制卡的过程, 发卡的过程才会有code
如果需要使用api来查看微信卡券能用的卡片背景色,需要调用代码(node端):
const wechatcard = require('wechat-cards')
async function getCardColor (ctx) {
const token = ctx.query.token
wechatcard.setConfig({
accessTokenService: {
"access_token": token,
"expires_in": 17199
}
})
const result = await wechatcard.basic.getColorList()
let data = result.colors
if(result.errmsg == 'ok'){
ctx.body={
code: 200,
msg:'success!',
data
}
}
}
如果知道卡券背景色的编号, 当然也可以不需要调用上面的代码: 调用的就是color010 color020。。。这个值
拿到card_id 后, 微信小程序调用卡券,实际就是发卡的过程了。
又是一个文档的坑点,官方文档, 网上基本都没有详细介绍这一块,微信卡券的api-ticket用于投放卡券时候必须参与的加密签名的参数,调用卡券的加密签名必须携带api-ticket, 应该怎么获取呢?(node端)
调用api获取卡券api-ticket:
使用的官方接口为:https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${token}&type=wx_card
API-ticket也有时效性, 可以根据自己需要进行数据保存
/*
获取卡券API-TICKET
*/
async function getApiTicketToken (ctx) {
const { token} = ctx.request.body
var url = `https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${token}&type=wx_card`
//JSON格式的ticket值数据包
var TempTicket = ''
//说明:随时调用的没失效的ticket的值
var curTicket=''
var cardTicket = await mysql("cardTicket").select('*').first() //后端数据库调用查看ticket数据, 没有过期就调用数据库中的数据
if(cardTicket !== undefined) { //判断数据库是否存在这个数据,这个是有这个数据。
//判断是否过期
var oldTime = cardTicket.create_time
var p = oldTime
oldTime = new Date(oldTime).getTime()
var newTime = new Date().getTime()
var resut = parseInt((newTime - oldTime) / 1000 / 60 / 60 )
if ( resut > 1 ) {
//重新获取access_token的值
TempTicket = await getHttpToken(url)
//存储MYSQL数据库中
if(TempTicket.errcode != 42001) {
await mysql('cardTicket').update({
'ticket ': TempTicket.ticket,
}).where('id',cardTicket.id)
curTicket = TempTicket.ticket,
ctx.state.data = {
code : 200 ,
curTicket ,
update: cardTicket.create_time,
msg:'更新成功!'
}
return
}else {
ctx.state.data = {
code : -1 ,
msg:'获取失败!'
}
return
}
}else {
//没有过期,继续使用数据库中的access_token的值
curTicket = cardTicket.ticket
ctx.state.data = {
code: 200 ,
curTicket ,
update: cardTicket.create_time,
msg:'获取成功!'
}
return
}
}else {
//如果数据库里面没有access_token的这条记录,就是第一次获取的情况。重新请求并插入一条新的数据
TempTicket = await getHttpToken(url)
if(TempTicket) {
await mysql('cardTicket').insert({
'ticket ' : TempTicket.ticket
})
curTicket = TempTicket.ticket
ctx.state.data = {
curTicket ,
code: 200 ,
msg:'插入成功!'
}
}else {
ctx.state.data = {
code : -1 ,
msg:'获取失败!'
}
}
}
}
此时进行发卡前的微信领券过程, 根据上面获取到的api_ticket参数,回调给小程序前端,进行领券。代码如下(node端):
进行微信领券和核销使用的API签名接口
@param :
1.card_id:
2.timestamp:
3.随机字符串
*/
async function ticketSignature (ctx){
const { card_id ,api_ticket } = ctx.request.body
const nonce_str = "rainbowstar" // 随机字符串, 欢迎关注瑞宝星工作室
let timestamp = Date.parse(new Date());
timestamp = timestamp / 1000 + ''; //注意 时间戳也是字符串类型
const data = [api_ticket,timestamp,card_id ,nonce_str] //需要按照顺序进行排列
let result = await wechatcard.basic.getSignature(data) //以sha1进行编码,这个就是signature标签, 回调给微信小程序端
if(result){
ctx.body={
code:200 ,
result,
timestamp,
nonce_str
}
}
}
获取到signatur标签。回调小程序端, 小程序使用wx.addcard接口开始领取优惠券:
代码如下:(uniapp编写的小程序端js部分, 原生一样)
getCouponCard(res) {
let that = this
let cardlist={
cardId:that.benefit.card_id,
cardExt:JSON.stringify({timestamp:res.timestamp,nonce_str:res.nonce_str,signature:res.result}) //timestamp 回调的时间戳(服务器端统一获取), nonce_str: 服务器端传递的随机字符串, signature : 服务器根据api-ticket等参数获取回调的加密签名
}
wx.addCard({
cardList: [cardlist], //代表领取的是cardlist这个优惠? 团购卡券
success(re){
let Code =''
let encry_code={
token: that.tokens, //注意,此token一样是携带的公众号的token
encry_code: re.cardList[0].code //微信回调固定写法
}
})
// encry_code 也是加密code 还需要解密后才可以领取。
}
加密的encry_code 再次传递给后端, 调用微信的api进行解密获取真实的code ,再次回调给微信,才真正实现了领取成功微信卡券
代码如下:(node端 进行解密)
再次强调token是公众号里面的token, 不是小程序的
//===============================
/*
解码加密code,获得真实的code
*/
//================
async function postTrueCode(ctx){
const { token , encry_code } = ctx.request.body
const url = `https://api.weixin.qq.com/card/code/decrypt?access_token=${token}`
const temp ={
"encrypt_code": encry_code
}
let code = await Post(url, {form:JSON.stringify(temp)})
if(code.errcode !=0){
ctx.body={
code:-1,
msg:'fail',
code
}
}else{
ctx.body={
code:200,
msg:'成功',
code
}
}
}