小程序手写电子签名&&身份证照片到远程服务器

2,099 阅读8分钟

小程序手写电子签名&&身份证照片到远程服务器

  • 前言
又来写一篇小程序的技能文章
主要是对工作的经验梳理和积累, 现在的公司平台比较小,属于初创型公司
我目前用到的技术栈主要是uniapp (vue2) 负责了两个TOC的小程序 及他们各自的后端代码(仅限于维护)
工作还是比较轻松的,已经在考虑未来的发展和规划了 

好了废话不多说 正式开始

1.0 先说需求

之前做的功能模块是老拉新活动,分别给新老用户一定的优惠,优惠的发放方式是这样: 
将新用户拉进企业微信群,群的二维码由我们自己生成,放到OSS文件列表,前端只负责获取二维码链接

这样的方式显然比较不合规,比较老套。主要这样处理对用户来说,并不直观,积极性也很差,所以这次需求有所改动
  • 新的发放方式是这样,公司对接了易税平台,
  • 我们需要获取用户的真实姓名、手机号(银行卡预留手机号)、银行卡号,及用户的电子签名,
  • 让用户和易税签订一个合同,这样可以直接打款到用户的银行卡。也就是所谓的返现。

前端遇到的问题并不多,我遇到比较麻烦的问题就是上传

在这里看一下最终的实现效果吧

注意: 目前在用电脑的模拟器在上传(图片和身份证本质上一样,在前端不会做身份证的校验,都是上传图片到OSS列表) 且上传身份证的话 涉及到个人隐私 不打码不合适 等过几天公司项目正式测试的时候 我就换成测试的版本

动画 (1).gif

1.1 电子签名

知道需求之后,我回到工位,就开始百度、csdn、掘金等各个平台先看看有没有先例,
结果大多是驴唇不对马嘴,我就知道可能要自己努力努力了

在网上还是找到了很多类似的例子,看了N个人的实现效果之后,我也开始了自己的代码编写(主要是cv ,取其精华,弃其糟粕。 hhhhh 说出来真的不怕你们笑话)

1.1.1 思路

  • 思路大概是这样给一个小片区域用来放图片(预览效果),和一个提交按钮
  • 点击外部盒子的时候,激活page-container组件 ,这个组件是小程序内部封装好的 简单说就是充满全屏。
  • 然后再page-container组件里分别放上 取消、重新签名、完成签名这三个按钮 (还没有做到撤销,那么高端的功能)。
  • 其他区域进行绘制(其实就是签名,做成了横板签名的效果,因为本人的笔画很多,竖屏实在是困难,加上我看到公司的用户里有少数民族,其中见过迪丽热巴这个姓氏,不知道她要怎么签名,坐等客服跟我反馈!)
  • 写好名字点击提交时,将图片的地址发给服务器就可以了
  • page-container文档 微信和支付宝一样

1.1.2 代码实现

<view class="image-container" @click="cli">
    <image mode="aspectFit" :src="sign" />
    <text v-if="!sign" class="text-center">签名区域</text>
</view>
<button style="margin-top: 30rpx;" @click="submitSignature">提交保存</button>
data(){
        return{
            sign: null,
            isSignatureFinish: false,
            moveLength: 0, //移动一定距离后才能提交保存
            showPage: false,
            canvasTempWidth: uni.getSystemInfoSync().screenWidth,
            canvasTempHeight: uni.getSystemInfoSync().screenWidth * 0.5,
            img_1:'', //身份证正面照片
        }
    },
    onShow() {
       //第一个canvas 创建画布
        this.canvasContext = uni.createCanvasContext('canvas');
        console.log('this.canvasContext',this.canvasContext);
        this.initCanvas()
        //第二个canvas
        this.ctx2 = uni.createCanvasContext('canvas-temp');
    },
    methods:{
        // 封装上传到oss列表方法
        async uploadOss(data,type){
            const res = await payRequest({
                path:'/api/yishui/img/upload',
                method:'POST',
                data:{
                    type
                }
            },1)
            const {host , OSSAccessKeyId:ossAccessKeyId, signature,key,policy} = res[1].data
            uni.uploadFile({
                url: host,
                fileType: 'image',
                //  #ifdef MP-ALIPAY 
                fileName: 'file',
                //  #endif 
                //  #ifdef MP-WEIXIN 
                name: 'file',
                //  #endif 
                filePath:data,
                formData: {
                    key,
                    policy,
                    OSSAccessKeyId: ossAccessKeyId,
                    signature,
                    success_action_status: '200',
                },
                success: (res) => {
                // 将上传成功状态码设置为200,默认状态码为204。
                    if (res.statusCode === 200) {
                        console.log('上传成功');
                    }
                        uni.showToast({ title: '上传成功!'});
                        data = `${host}/${key}`// 回填数据 让图片回填到页面
                        console.log(data,this.img_1, 'data,');
                    },
                fail: err => {
                    console.log(err,'err上传失败了');
                }
            })

        },
        // 提交签名
        async submitSignature(){
            if(this.sign){
                this.uploadOss(this.sign,'signImg')
            }else{
                uni.showToast({
                    icon:'none',
                    title: '请先完成电子签名,再提交保存',
                })
            }
        },

        // 打开/关闭电子签名区域
        cli(){
            this.showPage = !this.showPage
            console.log(this.showPage);
        },
        drawBackground (ctx, color = 'white', width = 1000, height = 3000, x = 0, y = 0) {
            ctx.rect(x, y, width, height)
            ctx.setFillStyle(color)
            ctx.fill()
        },
        // 完成签名
        confirmSign() {
            let _this = this
            _this.generateSignImage('canvas').then(res => {
                console.log('图片地址:---------', res)
                uni.getImageInfo({
                    src: res,
                    success(info) {
                        let width = info.width
                        let height = info.height
                        //先重置canvas-temp
                        _this.ctx2.draw(false, ()=>{
                            let dx = _this.canvasTempWidth
                            let dy = _this.canvasTempHeight
                            //把原点移动到中心点位置
                            _this.ctx2.translate(dx / 2, dy / 2)
                            _this.ctx2.rotate(270 * Math.PI / 180)
                            
                            let dWidth = dx / height * width
                            let dHeight = dx
                            //drawImage参数说明
                            //imageResource
                            //imageResource的左上角在目标 canvas 上 x 轴的位置
                            //imageResource的左上角在目标 canvas 上 y 轴的位置
                            //在目标画布上绘制 imageResource 的宽度,允许对绘制的 imageResource 进行缩放
                            //在目标画布上绘制 imageResource 的高度,允许对绘制的 imageResource 进行缩放
                            
                            _this.ctx2.drawImage(res, -dWidth / 2,-dHeight / 2, dWidth, dHeight)
                            //canvas-temp图片绘制完成后
                            _this.ctx2.draw(false, ()=>{
                                _this.generateSignImage('canvas-temp').then(res => {
                                    console.log('图片地址:!!!!!!!', res)
                                    _this.sign= res
                                    _this.showPage = false
                                })
                            })
                        })
                    }
                })
            })
        },
        initCanvas() {
            /* 设置线条颜色 */
            this.canvasContext.setStrokeStyle('#0081ff'); //2A2A2A
            /* 设置线条粗细 */
            this.canvasContext.setLineWidth(2);
            /* 设置线条的结束端点样式 */
            this.canvasContext.setLineCap('round');
        },
        /* 触摸开始 */
        handleTouchStart(e) {
            console.log(e)
            this.drawStartX = e.changedTouches[0].x;
            this.drawStartY = e.changedTouches[0].y;
            this.canvasContext.beginPath();
        },
        /* 触摸移动 */
        handleTouchMove(e) {
            /* 记录当前位置 */
            const tempX = e.changedTouches[0].x;
            const tempY = e.changedTouches[0].y;
            this.moveLength += Math.abs(this.drawStartX - tempX) + Math.abs(this.drawStartY - tempY)
            /* 画线 */
            this.canvasContext.moveTo(this.drawStartX, this.drawStartY);
            this.canvasContext.lineTo(tempX, tempY);
            this.canvasContext.stroke();

            /* 旧版draw方法,新版本不需要draw */
            this.canvasContext.draw(true);

            /* 重新记录起始位置 */
            this.drawStartX = tempX;
            this.drawStartY = tempY;
        },
        /* 触摸结束 */
        handleTouchEnd(e) {
            console.log('触摸结束')
            this.canvasContext.save();
            if(this.moveLength > 100) this.isSignatureFinish = true
        },
        /* 触摸取消 */
        handleTouchCancel(e) {
            console.log('触摸取消')
            this.canvasContext.save();
        },
        /* 清空画布 */
        clearCanvas() {
            console.log('清空画布')
            this.canvasContext.draw()
            this.initCanvas()
            this.moveLength = 0
            this.isSignatureFinish= false

        },
        /* 生成签名图片 */
        generateSignImage(canvasId) {
            console.log('Canvas生成图片')
            return new Promise((resolve, reject) => {
                uni.canvasToTempFilePath({
                    x: 0,
                    y: 0,
                    canvasId: canvasId, // 旧版使用id
                    fileType: 'png',
                    quality: 1,
                    success: res => {
                        resolve(res.tempFilePath)
                    },
                    fail: err => {
                        reject(err);
                    }
                })
            })
        },
    }
  • 代码量还是比较多的 尤其是绘制的时候 实话实说,不是完全自己写的,就不逐行解释了. 各位吴彦祖和刘亦菲们 应该都看得懂!
  • oss上传文件的问题 一会单独说一下

1.2 身份证照片上传

  • 需求虽然是身份证照片上传,但我问过产品了,上传的时候,我们公司不会坐识别校验是否合规,易税会统一审核,如果被驳回说明不合规,所以上传的就是一张图片。上边的效果也只是我的测试,等项目上线,会换成公司的项目gif文件

1.2.1 思路

  • 根据我之前上传身份证的经验来说,用户会有两个选择,拍照或者上传(需要获取用户相册权限。如果遭拒,需要提醒)。
  • 将用户上传的文件类型需要做限制,必须是图片且只能上传一张
  • 上传到oss文件列表后,再次拿到图片的url地址做展示

1.2.2 代码实现

<view class="img" style="width:100%;height: 400rpx;">
    <image :src="img_1" style="width:100%;height:100%" mode="aspectFit" />
</view>
<button style="margin-top: 30rpx;" @click="submitCard">提交身份证</button>
data(){
    return{
        img_1:'', //身份证正面照片
    }
}
methods:{
    // 提交身份证正面
    submitCard(){
        //  #ifdef MP-ALIPAY 
        my.chooseImage({
            success: res => {
                console.log(res, '查看res');
                this.img_1 = res.tempFilePaths[0]
                my.previewImage({
                    urls: res.tempFilePaths, // 使用所选中图片的本地临时文件路径列表
                });
                this.uploadOss(this.img_1,'cerFrontImg')
            }
        });
        //  #endif 
        //  #ifdef MP-WEIXIN 
        wx.chooseMedia({
            count:1,
            mediaType:['image'],
            success:res=>{
                console.log(res);
                this.img_1 = res.tempFiles[0].tempFilePath
                this.uploadOss(this.img_1,'cerFrontImg')
            }
        })
        //  #endif 
    },
    // 封装上传到oss列表方法
    async uploadOss(data,type){
        const res = await payRequest({
            path:'/api/yishui/img/upload',
            method:'POST',
            data:{
                type
            }
        },1)
        const {host , OSSAccessKeyId:ossAccessKeyId, signature,key,policy} = res[1].data
        uni.uploadFile({
            url: host,
            fileType: 'image',
            //  #ifdef MP-ALIPAY 
            fileName: 'file',
            //  #endif 
            //  #ifdef MP-WEIXIN 
            name: 'file',
            //  #endif 
            filePath:data,
            formData: {
                key,
                policy,
                OSSAccessKeyId: ossAccessKeyId,
                signature,
                success_action_status: '200',
            },
            success: (res) => {
            // 将上传成功状态码设置为200,默认状态码为204。
                if (res.statusCode === 200) {
                    console.log('上传成功');
                }
                    uni.showToast({ title: '上传成功!'});
                    data = `${host}/${key}`
                    console.log(data,this.img_1, 'data,');
                },
            fail: err => {
                console.log(err,'err上传失败了');
            }
        })
    },
}

1.3 OSS小程序直传实践

这里可能有同学要问 封装的oss上传方法怎么来的
那这里我就告诉你

OSS微信直传实践文档

没有阿里云 RAM账号会禁止访问 ,我在放一下截图吧

Snipaste_2023-05-26_16-00-54.png

  • 前面还有三个步骤,都是用来配置跨域 设置白名单
  • 从文档可以看到,最需要的就是 key,policy,OSSAccessKeyId,signature这几个参数,我和老大沟通之后,他给了我接口,我发请求(签名、身份证正面、身份证反面都会返给我对应的参数),于是我封装的方法,便应运而生了
// 封装上传到oss列表方法  
/*
data是需要上传的数据(图片文件)
type是类型(我老大对签名、身份证正面、身份证反面这三种做了分类)
分别传入调用就可以了
*/
async uploadOss(data,type){
    const res = await payRequest({
        path:'/api/yishui/img/upload',
        method:'POST',
        data:{
            type
        }
    },1)
    const {host , OSSAccessKeyId:ossAccessKeyId, signature,key,policy} = res[1].data
    uni.uploadFile({
        url: host,
        fileType: 'image',
        //  #ifdef MP-ALIPAY 
        fileName: 'file',
        //  #endif 
        //  #ifdef MP-WEIXIN 
        name: 'file',
        //  #endif 
        filePath:data,
        formData: {
            key,
            policy,
            OSSAccessKeyId: ossAccessKeyId,
            signature,
            success_action_status: '200',
        },
        success: (res) => {
        // 将上传成功状态码设置为200,默认状态码为204。
            if (res.statusCode === 200) {
                console.log('上传成功');
            }
                uni.showToast({ title: '上传成功!'});
                data = `${host}/${key}`
                console.log(data,this.img_1, 'data,');
            },
        fail: err => {
            console.log(err,'err上传失败了');
        }
    })
},

最后介绍 几个小程序的api

my.chooseImage:拍照或从本地相册中选择图片的 API(支付宝)

wx.chooseMedia:拍摄或从手机相册中选择图片或视频(微信)

uni.uploadFile:将本地资源上传到开发者服务器(通用)