场景
用户在参加展会时会互相交换名片,这些名片后续需要录入到系统中,作为一个客户或者供应商存在。如果使用人工手动输入上传会比较耗时,因而需要一个将名片拍照后进行批量识别并直接导入到系统的功能。
解决思路

1、文件上传
拍好的名片照片需要进行上传,可以通过单张上传或者打成压缩包上传。若采用单张上传,那么文件的上传需要做成支持多选的功能,这样的话就会面临需要上传的文件会有几千张,如果做多选的话,我们的上传控件为了考虑性能问题,一定会做选择文件个数的限制,这样就会导致用户需要频繁操作,同时可能会造成很多的请求。鉴于以上原因此处选择压缩包分片上传,防止单个压缩包过大导致上传失败。
在技术上,此处选择便宜云服务器的 对象存储OSS 来处理打包文件的分片上传,以下为主要代码:
📎aliyun-oss-sdk-5.3.1.min.js📎es6-promise.min.js📎md5.js📎uploadFile.js
<!DOCTYPE html>
<html>
<head>
<title></title>
<script type="text/javascript" src="/aliyun-oss-sdk-5.3.1.min.js"></script>
<script type="text/javascript" src="/es6-promise.min.js"></script>
<script type="text/javascript" src="/jSignature/md5.js"></script>
<script type="text/javascript" src="/mumu.file.js"></script>
</head>
<body>
<input type="file" name="file" title="上传文件" text="上传文件" allowexts="zip" onchange="">
<p><button type="button" name="uploadFile" class="hide">上传文件</button></p>
<script type="text/javascript">
var myFiles = [];
var saveFileUrl = "/add/upload/file/url";
MyFile.stsTokenObj = {
region: "oss-cn-qingdao",
accessKeyId: "your-AccessKeyId",
accessKeySecret: "your-AccessKeySecret",
stsToken: "your-sts-Token",
bucket:"your-bucket-name",
secure:true
};
MyFile.uploadFilePath = "/my/file/path/";
$("body").delegate("input[type=file]", "change", function () {
for (var i = 0; i < this.files.length; i++) {
myFiles.push(this.files[i]);
}
});
$("button[name=uploadFile]").click(function () {
$(this).attr("disabled", "disabled");
let curButton = $(this);
var upLoadedCount = 0;
var fileUploadPromise = (file, index) => {
let promise = new Promise((resolve, reject) => {
var callback = function (res) {
var form = new FormData();
form.append("file", res.name);
form.append("originName", res.originName);
$.ajax({
type: "post",
url: saveFileUrl,
data: form,
processData: false,
contentType: false,
success: function (res) {
curButton.removeAttr("disabled");
if (res.success) {
alert("上传成功");
}
else{
alert("上传失败");
}
},
error: function (res) {
curButton.removeAttr("disabled");
alert("上传失败");
}
});
};
var uploadProgress = function (p) {
console.log(p);
return function (done) {
done();
}
};
MyFile.applyTokenDo(MyFile.uploadFile, file, callback, uploadProgress);
});
return promise;
};
var handleFiles = (files) => {
let filesPromise = files.map((file, index) => fileUploadPromise(file, index));
Promise.all(filesPromise)
.then(data => {
data.map((image, index) => console.log("success"))
})
};
handleFiles(myFiles);
});
</script>
</body>
</html>
这里分片上传文件到 OSS 使用了 OSS 的官方sdk,大家可参考便宜云服务器官网上的 OSS SDK下载最新的开发包。上传时的 AccessKey 和 AccessSecret 为使用 STS 方式获取的临时 token,这样可以保证原密钥的安全性。这里就不再说明了。
2、保存文件数据、发送队列
在文件上传成功后,可将上传的文件信息保存到数据库中,比如,可设计一个文件记录表:
CREATE TABLE `tbfile` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`file_path` varchar(300) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '文件路径',
`createtime` datetime DEFAULT NULL COMMENT '上传时间',
`origin_name` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '原始名',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='上传文件'
;

考虑到图片识别的速度会很慢,如果立即识别可能会导致用户的浏览器连接超时,当压缩包上传成功后,此处使用异步发送 MQ 的方式进行名片识别。
将刚插入数据库的数据 ID 作为消息体发送,可使用 ActiveMQ、Rocket MQ、Alimns等消息队列处理。
3、接收队列、进行识别
在服务端新建一个 Service 进行队列接收,并根据消息内容从第二步的数据表中获取上传文件的 OSS 地址,将该压缩包下载解压后,依次识别每个名片信息。识别成功后可将识别的数据插入到目标数据表,并同时通过企业微信、钉钉、短信等通知上传用户。
名片的识别可使用便宜云服务器 视觉智能开放平台>文字识别>名片识别 或者 其他名片识别。此处以 Laravel 框架展示主要代码内容:
/**
* 处理名片信息
* @param fileUrl 压缩文件的下载地址
*/
private function dealCardInfo($fileUrl){
//下载文件
$filePath = storage_path('downloads/mycards.zip');
$downloadFile = file_get_contents("compress.zlib://". $fileUrl);
file_put_contents($filePath, $downloadFile);
//解压文件
shell_exec("unzip -d ".storage_path("downloads/mycards/").' -o '.$filePath);
//循环文件
$allFiles = \Storage::disk('download')->allFiles("mycards/");
foreach ($allFiles as $curFile) {
$curImagePath = storage_path("downloads/".$curFile);
$curImage = base64_encode(file_get_contents($curImagePath));
$ret = $this->getCardInfo($curImage);
if (!empty($ret)) {
$curContent = [
"name" => $ret["name"],
"company" => $ret["company"],
"department" => $ret["department"],
"title" => $ret["title"],
"tel_cell" => $ret["tel_cell"],
"tel_work" => $ret["tel_work"],
"addr" => $ret["addr"],
"email" => $ret["email"]
];
app("db")->connection("mysql")->table("tbcontact")->insert($curContent);
}
//删除文件
unlink($curImagePath);
}
//发送消息提醒
//删除下载文件
unlink($filePath);
}
/**
* 识别名片
* @param imageInfo 图片base64 或者图片url
*/
private function getCardInfo($imageInfo){
$url = "https://bizcard.market.alicloudapi.com/rest/160601/ocr/ocr_business_card.json";
$method = "POST";
$appcode = "your app code";
$headers = [
"Authorization:APPCODE " . $appcode,
"Content-Type".":"."application/json; charset=UTF-8"
];
//图片二进制数据的base64编码或者图片url
$bodys = "{\"image\":\"".$imageInfo."\"}";
$ret = "";
try {
$curl = curl_init();
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_FAILONERROR, false);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_HEADER, false);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($curl, CURLOPT_POSTFIELDS, $bodys);
$ret = curl_exec($curl);
curl_close($curl);
} catch(\Exception $ex){
}
$retArr = json_decode($ret,true);
if (empty($retArr)) {
return [];
}
if (!$retArr["success"]) {
return [];
}
return $retArr;
}