全栈开发:小程序生成海报功能的思考
发布于 2021-05-13 08:26 ,所属分类:区块连和PHP开发学习资料
全栈开发:小程序生成海报功能的思考
一、背景
业务团队人员需要每天都有发朋友圈的素材,运营团队手工画的海报中小程序码为统一码,没有渠道属性,导致无法帮助业务人员进行锁客。
因此提出需要在小程序内部一键生成推广海报的能力,海报中需要有销售人员的个人属性,客户通过识别该ErWeiMa成为新用户后,可以追溯到是哪个业务人员推广的新用户,进一步用于核算业绩。
Tip: 所有的流程设计应当仔细思考参与人员的利益,否则做出来也不会有人用。
二、需求分析
1. 工作流程
2. 点
专属推广码的生成量可能会比较大,可以使用小程序提供的wxacode.getUnlimited接口生成。
该接口生成的ErWeiMa可以设定scene参数,我们可以在这个scene参数里面带上推广人的身份信息,这样小程序在打开的时候就知道是来自谁的分享了。
合成海报的能力可以放在后台也可以放在前端
放后台的好处是参数配置全部后端可控,想改就改,前端开发简单,不存在设备兼容的问题。坏处是合成图片是CPU密集型操作,后台负载会变高,而且为了不block主业务的api,建议独立架设图片服务器。 放前端的好处是合成计算用的是手机的CPU,后端可以做得非常轻,但弊端是需要考虑多端场景。可以使用canvas进行合成。 最终我们选择前端生成图片的方案,首先在创业者面前,涉及到钱问题都需要慎重。其次在架构面前,多一个模块就多一份故障的可能,也多一份维护的工作量。
核心难点:运营人员出的图片会涉及多种版式,如果写在固定位置放ErWeiMa就会给运营带来很多不便利,所以就需要提供自定义显示的能力,运营提供一张海报之后,可以自定义ErWeiMa放在哪里,这样才是最灵活的。
三、功能设计
经过思考,为了提供更强的运营能力(避免运营后续再提需求),对海报功能进行了分析,我们需要提供以下元素的合成能力:
图片:需要支持以下能力
专属ErWeiMa 用户头像 其他网络图片,如品牌Logo 支持设置图片大小,坐标,是否裁剪为圆形 文本:需要支持以下能力
自定义文本 用户昵称等动态内容 支持设置字体大小、坐标、颜色、对齐方式(居左、居右、居中)
如下图所示
相信实现这些元素能力后,能在很长一段时间满足运营的需求了。
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号与我直接互动。
相关资源