zoukankan      html  css  js  c++  java
  • 微信小程序 结合公众号前后端全栈开发微信优惠卡券

    微信小程序面世几年了,相关的功能也都开发的很完善。 可当下,开发者在遇到某些项目需求的时候,还是会遇到很多很多的坑,一来微信开发者文档里面的解释经常弄得云里雾里,无从下手,二来网上各种博客写得相关教程或者内容都不太完整,或者没有进行校验核对,甚至直接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 ! 感兴趣的朋友还可以关注我的微信公众号, 不定时的发布更多原创文章。 比如卡券如何核销

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

  • 相关阅读:
    解决:com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure(真实有效)
    数据库连接池Druid的介绍,配置分析对比总结
    浅谈mybatis如何半自动化解耦和ORM实现
    IntelliJ Idea14 创建Maven多模块项目,多继承,热部署配置总结(三)
    IntelliJ Idea14 创建Maven多模块项目,多继承,热部署配置总结(二)
    IntelliJ Idea14 创建Maven多模块项目,多继承,热部署配置总结(一)
    IntelliJ IDEA 创建Spring+SpringMVC+mybatis+maven项目
    跨站点请求伪造(CSRF)总结和防御
    移动端网站开发要点-meta设置
    数组去重
  • 原文地址:https://www.cnblogs.com/xochin001/p/12775536.html
Copyright © 2011-2022 走看看