全栈开发:小程序生成海报功能的思考

发布于 2021-05-13 08:26 ,所属分类:区块连和PHP开发学习资料

全栈开发:小程序生成海报功能的思考

一、背景

业务团队人员需要每天都有发朋友圈的素材,运营团队手工画的海报中小程序码为统一码,没有渠道属性,导致无法帮助业务人员进行锁客。

因此提出需要在小程序内部一键生成推广海报的能力,海报中需要有销售人员的个人属性,客户通过识别该ErWeiMa成为新用户后,可以追溯到是哪个业务人员推广的新用户,进一步用于核算业绩。

Tip: 所有的流程设计应当仔细思考参与人员的利益,否则做出来也不会有人用。

二、需求分析

1. 工作流程

2. 点

  • 专属推广码的生成量可能会比较大,可以使用小程序提供的wxacode.getUnlimited接口生成。

    该接口生成的ErWeiMa可以设定scene参数,我们可以在这个scene参数里面带上推广人的身份信息,这样小程序在打开的时候就知道是来自谁的分享了。

  • 合成海报的能力可以放在后台也可以放在前端

    1. 放后台的好处是参数配置全部后端可控,想改就改,前端开发简单,不存在设备兼容的问题。坏处是合成图片是CPU密集型操作,后台负载会变高,而且为了不block主业务的api,建议独立架设图片服务器。
    2. 放前端的好处是合成计算用的是手机的CPU,后端可以做得非常轻,但弊端是需要考虑多端场景。可以使用canvas进行合成。

  • 最终我们选择前端生成图片的方案,首先在创业者面前,涉及到钱问题都需要慎重。其次在架构面前,多一个模块就多一份故障的可能,也多一份维护的工作量。




    • 核心难点:运营人员出的图片会涉及多种版式,如果写在固定位置放ErWeiMa就会给运营带来很多不便利,所以就需要提供自定义显示的能力,运营提供一张海报之后,可以自定义ErWeiMa放在哪里,这样才是最灵活的。

    三、功能设计

    经过思考,为了提供更强的运营能力(避免运营后续再提需求),对海报功能进行了分析,我们需要提供以下元素的合成能力:

    1. 图片:需要支持以下能力

      • 专属ErWeiMa
      • 用户头像
      • 其他网络图片,如品牌Logo
      • 支持设置图片大小,坐标,是否裁剪为圆形
    2. 文本:需要支持以下能力

      • 自定义文本
      • 用户昵称等动态内容
      • 支持设置字体大小、坐标、颜色、对齐方式(居左、居右、居中)

    如下图所示

    相信实现这些元素能力后,能在很长一段时间满足运营的需求了。

    Tip: 开发需要帮业务团队补位,想他们没想到的地方,做他们不知道能做事情。这样才能从根本上减少需求变更。

    四、开发

    配置内容

    将海报的合成步骤,以json形式生成配置,前端获取配置后进行合成

    1. 主配置

    设置画布大小。items是一个数组,按绘制顺序配置各组件的展示属性

    {
    "width":750,
    "height":950,
    "items":[]
    }

    2. 图片组件

    {
    "type":"image",
    "url":"https://9niu.com/10034/images/poster/bg1.jpg",
    "offset_x":0,
    "offset_y":0,
    "width":750,
    "height":950,
    "circle":false
    }
    • offset_x, offset_y 代表左上角的起始坐标
    • width, height 代表输出图片的尺寸
    • circle 代表是否剪成圆形

    3. 头像组件

    {
    "type":"headerImage",
    "width":70,
    "height":70,
    "offset_x":30,
    "offset_y":30,
    "circle":true
    }

    头像其实是继承于图片组件,唯一的区别是图片地址会从用户属性里面取

    4. 小程序码组件

    {
    "type":"MpQrCode",
    "page":"pages/index/index",
    "scene":{},
    "width":100,
    "height":100,
    "line_color":"{\"r\":0,\"g\":0,\"b\":0}",
    "is_hyaline":false,
    "circle":true,
    "offset_x":585,
    "offset_y":770
    }

    小程序码也是继承于图片组件,line_color、is_hyaline、page、scene是用来控制小程序码的内容,参考wxacode.getUnlimited接口 scene会在程序中统一重新生成,加上用户id信息,以实现推广能力。

    5. 文本组件

    {
    "type":"text",
    "font":"18pxArial",
    "color":"#dedede",
    "content":"长按识别ErWeiMa",
    "offset_x":635,
    "offset_y":900,
    "textAlign":"center"
    }
    • font 字体大小
    • color 字体颜色
    • content 内容
    • textAlign 代表对齐方式,支持left, center, right
    • offset_x, offset_y代表组件的位置:
      • 当textAlign=left时,代表左上角位置。
      • 当textAlign=center时,代表中间点位置。
      • 当textAlign=right时,代表右上角位置。

    6. 用户昵称组件

    {
    "type":"userNick",
    "font":"28pxArial",
    "color":"#ffffff",
    "offset_x":115,
    "offset_y":60,
    "textAlign":"left"
    }

    用户昵称组件继承自文本组件,只是content是从用户属性中读取。

    前端核心代码

    1. 初始化canvas画布

    componentDidMount(){
    Taro.nextTick(()=>{
    Taro.showLoading({title:"获取海报信息"})
    var{id}=getCurrentInstance().router.params
    MallApi.getPosterConfig(id).then(res=>{
    this.setState({poster:JSON.parse(res.config)},()=>{
    Taro.hideLoading()
    //获取canvas节点
    constquery=Taro.createSelectorQuery()
    query.select('#poster').fields({node:true,size:true}).exec((res)=>{
    constcanvas=res[0].node
    varctx=canvas.getContext('2d')
    this.ctx=ctx
    this.canvas=canvas
    ctx.scale(this.dpr,this.dpr)
    this.drawPoster()
    })
    })
    })
    })
    }

    注:在taro的componentDidMount直接用query.select('#poster')查找canvas节点会因节点尚未加载而找不到节点,所以必须要在Taro.nextTick事件中处理

    2. 画图主控制流程

    drawPoster(){
    Taro.showLoading({title:"海报生成中"})
    const{canvas}=this
    const{poster}=this.state
    canvas.width=poster.width
    canvas.height=poster.height

    constforLoop=async_=>{
    console.log('Start')
    const{items}=poster
    //遍历组件,一个一个画出来
    for(letindex=0;index<items.length;index++){
    console.log("startdrawitem",index)
    awaitthis.drawItem(items[index])
    console.log("finishdrawitem",index)
    }
    }
    forLoop()
    }

    核心点:因为绘制有严格的顺序要求,不然可能会出现被覆盖的情况。比如必须先画背景图,再画用户头像。如果反过来就会出现头像被盖掉的问题。所以我们在遍历的过程中加了async

    3. 图片组件

    drawPicture=(item)=>{
    returnnewPromise((resolve,reject)=>{
    const{ctx,canvas}=this

    //如果需要裁剪成圆形
    if(item.circle){
    ctx.save();
    ctx.beginPath();//开始绘制
    ctx.arc(item.width/2+item.offset_x,item.width/2+item.offset_y,item.width/2,0,Math.PI*2,false);
    ctx.clip();
    }

    varimg=canvas.createImage()
    img.onload=()=>{
    console.log('image.loadfinish')
    varwidth=item.width?item.width:img.width
    varheight=item.height?item.height:img.height
    //通过drawImage把图片绘制到指定区域
    ctx.drawImage(img,item.offset_x,item.offset_y,width,height)
    ctx.restore();
    console.log('image.drawfinish')
    resolve()
    }

    //加载图片
    Taro.getImageInfo({
    src:item.url,
    success:(imgres)=>{
    console.log('getImageInfo',imgres)
    img.src=imgres.path
    console.log('getImageInfofinish')
    },
    fail:(res)=>{console.log('getImageInfofail',res)}
    })
    })
    }

    核心是ctx.drawImage方法,可以参考canvas文档

    最重点的是这张图

    4. 文本组件

    drawText=(item)=>{
    returnnewPromise((resolve,reject)=>{
    const{ctx,canvas}=this
    //设置样式
    ctx.font=item.font
    ctx.fillStyle=item.color
    ctx.textAlign=item.textAlign
    //绘制
    ctx.fillText(item.content,item.offset_x,item.offset_y)
    this.setState({canvas:canvas},()=>{ctx.restore();resolve()})
    })
    }

    后端核心代码

    1. 获取小程序码图片

    @mp_qr_router.get('/unlimited')
    defget_unlimited(
    app_id:str,sid:str,
    page:Optional[str]=None,
    scene:Optional[str]='',
    width:Optional[int]=330,
    auto_color:Optional[bool]=False,
    line_color:Optional[str]='{"r":0,"g":0,"b":0}',
    is_hyaline:Optional[bool]=False
    )
    :


    res=MpQRService.get_unlimited(
    auth_appid=app_id,
    page=page,
    scene=scene,
    width=width,
    auto_color=auto_color,
    line_color=line_color,
    is_hyaline=is_hyaline,
    sid=sid
    )

    withtempfile.NamedTemporaryFile(mode="w+b",suffix=".png",delete=False)asFOUT:
    FOUT.write(res)
    returnFileResponse(FOUT.name,media_type="image/png")

    注:开发框架为FastAPI,侧反馈的是字节流,未想到直接转发的玩法,所以引入了tempfile的方式。

    2. 海报配置表

    CREATETABLE`m_poster`(
    `id`int(16)NOTNULLAUTO_INCREMENTCOMMENT'编号',
    `title`varchar(128)NOTNULLDEFAULT''COMMENT'标题',
    `status`tinyint(1)NOTNULLDEFAULT'0'COMMENT'状态:0-草稿,1-生效,2-下架',
    `cover`varchar(256)NOTNULLDEFAULT''COMMENT'封面',
    `config`varchar(2048)NOTNULLDEFAULT''COMMENT'配置(json)',
    `create_time`timestampNOTNULLDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',
    `last_update_time`timestampNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'最近一次修改时间',
    PRIMARYKEY(`id`)
    )ENGINE=InnoDBAUTO_INCREMENT=11DEFAULTCHARSET=utf8mb4COMMENT='海报配置表';

    3. 核心接口

    • 获取海报列表接口(缓存 600秒)
    • 获取海报信息接口(缓存 3600秒)

    五、成果

    至此已经实现了基本上可以自定义的海报配置能力,运营日常就可以提供更多样性的海报了,避免千篇一律的布局导致审美疲劳。


    后续将增加管理平台功能,实现模板化和可视化的配置能力。


    六、最后


    牛仔的公司在茅台镇生产了一批纯正粮食酿造的酱香酒,品质非常好,不论醉成什么样,第二天绝对不头疼。招待、送礼佳品。


    各位读者如有需要点击上面小程序直接购买,这个链接已经配置了专属活动,在结算的时候价格会发生变化。


    如果有什么要要了解的欢迎“牛仔说”gongzhong号与我直接互动。


    相关资源