微信小程序 结合公众号前后端全栈开发微信优惠卡券

微信小程序面世几年了,相关的功能也都开发的很完善。 可当下,开发者在遇到某些项目需求的时候,还是会遇到很多很多的坑,一来微信开发者文档里面的解释经常弄得云里雾里,无从下手,二来网上各种博客写得相关教程或者内容都不太完整,或者没有进行校验核对,甚至直接copy过来得。就拿优惠券开发这块来说,微信开发者文档解释很简陋,并且还要引用关联公众号的相关配置。刚好上个项目开发了微信小程序卡券功能,所以萌生了写这么一个教程的想法,一来担心自己久了遗忘,记录一下未来用到的时候可供翻阅,二来也能提供给需要的朋友,希望能够帮助的到大家。

本人水平有限,编写中有错在所难免,如果有错误的地方望各位批评指正。

废话不多说: 先来看看实际效果图
ceshi

  • 开发使用的架构为:
  • 前端: 微信小程序uniapp(原生通用 )
  • 后端: koa
  • 架构结构图:
先看看微信文档里面的卡券流程图:

官方架构

可以看到, 官网流程真的苦涩难懂
注: 此次开发使用的API调用,而非公众号制作卡券, 开发进行API调用相对公众号上制作卡券更加的灵活和高度定制化
根据官网通俗理解流程如下:

架构

  • 开发准备:
  • 开发者须有一个有卡券权限的公众号(服务号)和认证后的小程序账号【前提】
  • 公众号1
  • 公众号2
  • 准备好卡券的店铺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用于投放卡券时候必须参与的加密签名的参数,调用卡券的加密签名必须携带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
    }
  }
}
回调回小程序端
就得到了领取成功的逻辑

huidiao

下一步就进行领取后绑定数据的操作

over ! 感兴趣的朋友还可以关注我的微信公众号, 不定时的发布更多原创文章。 比如卡券如何核销

本人原创! 非授权严禁转载!保留追究的权利!

posted @ 2020-04-25 21:53  星辰cd  阅读(1228)  评论(0编辑  收藏  举报