用过微信的人都知道,日常发表朋友圈图文动态时,可以选择多图片,图片右上角有个灰色的删除图标,可以对即将要发表的照片信息进行预览和删除。今天笔者通过微信js-sdk接口给大家把这个实现过程分享出来。
一、主体思路
微信js-sdk上传接口支持单次最多选择9张图片,我们要实现的就是跟微信发图文朋友圈一样的功能,最多支持9张图片,可以预览,删除。也就是说,如果我们第一次选了8张照片,再次选照片时,最多只能选一张,如果总共选了8张又删了2张,那么我们还可以最多选3张照片。
二、案例效果
本案例应客户方要求,最多上传6张图片(和上传9张图片的过程一样)。图一,上传3张图片时,左下角的图标标明可以继续上传
图二,上传6张图片时,左下角的上传图标不显示,标明图片数量已达上限,不能再选图片上传了,并且每个图片的右上角有个红色的垃圾桶图标,就是删除功能,在提交之前可以随时增删图片,总的图片上传数量上限是6。
三、具体实现过程
1、微信调用手机摄像头的js-sdk版本
<script src="http://res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>
其他的 Bootstrap.js,JQuery.js,Ajax.js可以根据自己的实际情况引入即可。
2、jsp页面定义图片存放容器
这里的图片容器就是class="car"的这个div,下面的id="add-car"这个图片是从手机相册添加图片或者拍照的图标,并且绑定了一个点击方法chooseImg(this),这个方法会调用微信js-sdk的上传图片接口,触发选择手机照片的事件,但是在调用上传接口前,得先获取微信js-sdk的接口权限,获取这个接口权限需要先得到所需的签名参数等。
<div class="form-group">
<div>
<label class="col-xs-10 col-sm-8 control-label">车辆拍照(最多可以上传6张图片)</label>
</div>
<div class="col-xs-12 col-sm-12">
<div id="car-imgs" class="car" style="display:flex;flex-wrap:wrap;align-items:center;margin-top:15px;" data-count="6" data-type="car">
</div>
<img src="/img/addPic.png" id="add-car" onclick="chooseImg(this)">
</div>
</div>
3、获取微信js-sdk接口权限签名参数,通过微信wx.config({options})前台接口获取选择图片接口权限签名参数。
<script type="text/javascript">
var images = {
localId: [],
serverId: []
};
var carPicUrl = [];//存放车辆照片的url
var count = 6; //图片的最大数量
var ios = false; //判断是否是ios或者iphone设备
var $imgContainer; //通过Jquery选择器获取的Dom对象作为图片容器
function chooseImg(obj){
$.ajax({
url:"***/passage/getWxConfig",
dataType:"json",
type:"post",
data:{'openid':$("#openid").val()},
success:function(res){
wx.config({
debug: false,
appId:res.appId, // 必填,公众号的唯一标识
timestamp: res.timestamp, // 必填,生成签名的时间戳
nonceStr: res.noncestr, // 必填,生成签名的随机串
signature: res.signature,// 必填,签名,见附录1
jsApiList: ['chooseImage'] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2});
var ua = navigator.userAgent;
if (/(iPhone|iOS)/i.test(ua)) {
ios = true;
} else {
ios = false;
}
alert("是否是IOS设备"+ios);
wx.ready(function(){
wx.chooseImage({
count: count, // 微信默认9
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: function (res) {
count = count-res.localIds.length;
images.localId = res.localIds;
uploadImage(images.localId,$imgContainer);
},
fail:function(res){
alert("失败的原因2"+JSON.stringify(res));
}
})
});
},
error:function(){
alert("获取JSAPI接口权限参数失败");
}
})
}
</script>
4、后台代码获取微信js-sdk接口权限签名所需参数。
/**
* create by guo bin hui in 2018-10-20
* 获取jsp页面调用微信公众号js-sdk上传图片的config接口所需参数
*/
@RequestMapping(value = "/getWxConfig")
@ResponseBody
public Map getWxConfig(HttpSession seesion, HttpServletRequest request) throws IOException {
Map <String,String> map = new HashMap <String,String> ();
Map data = WxAccessToken.getSavedAccessToken();
map.put("jsapi_ticket", (String)data.get("jsapi_ticket"));
map.put("timestamp",WeiXinUtil.getTimestamp());
map.put("noncestr",WxPayUtil.getNonceStr());
map.put("url", (String)request.getSession().getAttribute("url"));
String str = WxPayUtil.createLinkString(map);
String sign = DigestUtils.sha1Hex(str);
map.put("signature",sign);
map.put("appId",Constants.appID);
return map;
}
关于jsapi_ticket这个参数的获取以及全局缓存笔者已经在上一篇讲到了,以及timestamp,noncestr这2个参数的获取可以点击 《Java开发公众号系列教程(二):公众号开发全局缓存access_token和jsapi_ticket》查看。
这里的url参数指的是从哪个页面所属的接口发起的选择相册照片的接口路径,参数的组合以及加密签名上一篇都有。通过这个接口获取到了jsp页面的wx.cinfig({})接口所需的参数。
5、选择图片接口的使用 wx.ready(function(){})和wx.chooseImage({})
wx.ready(function(){
wx.chooseImage({
count: count, // 微信默认9
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: function (res) {
count = count-res.localIds.length;
images.localId = res.localIds;
uploadImage(images.localId,$imgContainer);
},
fail:function(res){
alert("失败的原因2"+JSON.stringify(res));
}
})
});
6、具体的图片上传业务逻辑,由于微信的图片上传只能单张上传,所以这里写了个上传的递归方法,循环上传,直到把选择的照片全部上传给微信服务器,然后微信服务器返回每张图片的mediaId
function uploadImage(localIds,imgContainer) {
var i = 0,picNum = images.localId.length;
images.serverId = [];
function upload(){
wx.uploadImage({
localId: localIds[i], // 需要上传的图片的本地ID,由chooseImage接口获得
isShowProgressTips: 1, // 默认为1,显示进度提示
success: function (res) {
images.serverId.push(res.serverId);
var localId = localIds[i];
if(ios){
wx.getLocalImgData({
localId: localId,
success: function (res) {
var localData = res.localData; // localData是微信IOS端图片的base64数据,可以用img标签显示
localData = localData.replace('jgp', 'jpeg');
if(imgContainer.attr("data-count") == "1"){//单张图片的处理
imgContainer.append('<div style="height:160px;" class="uploadImg" >' +
'<div class="close" style="position: absolute;top:5px;right:15px;"><img src="/img/delPic.png" onclick="delPic(this);"></div>' +
'<img width="100%" height="100%" src="'+localData+'"/></div>');
imgContainer.next().css("display",'none');
}if(imgContainer.attr("data-count") == "6"){ //多张图片的处理
if(count == 0){
imgContainer.next().css("display",'none');
}
imgContainer.append('<div style="width:50%;height:120px;" class="multiImg"><div class="close"><img src="/img/delPic.png" onclick="delImg(this);"></div><img name = "pic" style="width:100%;height:100%;margin-top:-32px;" src="'+localData+'"/></div>');
}
},
fail: function () {
alert('该图片暂时无法查看');
}
})
}else{
if(imgContainer.attr("data-count") == "1"){//单张图片的处理
imgContainer.append('<div style="height:160px;" class="uploadImg" >' +
'<div class="close" style="position: absolute;top:5px;right:15px;"><img src="/img/delPic.png" onclick="delPic(this);"></div><img width="100%" height="100%" src="'+localId+'"/></div>');
imgContainer.next().css("display",'none');
}if(imgContainer.attr("data-count") == "6"){ //多张图片的处理
if(count == 0){
imgContainer.next().css("display",'none');
}
imgContainer.append('<div style="width:50%;height:120px;padding: 5px 5px" class="multiImg"><div class="close"><img src="/img/delPic.png" onclick="delImg(this);"></div><img name = "pic" style="width:100%;height:100%;margin-top:-32px;" src="'+localId+'"/></div>');
}
}
var mediaId = res.serverId; // 返回图片的服务器端ID,即mediaId
//将获取到的 mediaId 传入后台 方法savePicture
$.post("<%=request.getContextPath()%>/passage/savePicture",{mediaId:mediaId,picType:imgContainer.attr("data-type")},function(res){
if("drivingLicense-front"==imgContainer.attr("class")){
$("input[name='drivingLicense-front-url']").val(res);
}else if("drivingLicense-back"==imgContainer.attr("class")){
$("input[name='drivingLicense-back-url']").val(res);
}else if("idCard-front"==imgContainer.attr("class")){
$("input[name='idCard-front-url']").val(res);
}else if("idCard-back"==imgContainer.attr("class")){
$("input[name='idCard-back-url']").val(res);
}else if("car"==imgContainer.attr("class")){
var multi = imgContainer.find(".multiImg");
$(multi[multi.length-1]).append('<input type="hidden" name="car-url" class="car-url" value="'+res+'" />');
// alert(multi[multi.length-1].innerHTML);
}
i++;
if(i < picNum){//递归方法循环上传图片
upload();
}
})
},
fail: function (res) {
alert('上传失败,请重新上传!'+res);
}
});
}
upload();
}
7、从微信服务器拉取图片保存至开发者服务器或物理磁盘业务的后台代码Controller,Service
@RequestMapping(value = "/savePicture", method= RequestMethod.POST)
@ResponseBody
public static String savePicture(String mediaId,String picType) throws IOException{
String imgPrevPath = WeiXinUtil.saveImageToDisk(mediaId,picType);
return imgPrevPath;
}
这里的还有另外一个思路也可以实现,过程就是通过步骤6上传到微信服务器后,微信返回每个图片的服务端mediaId,然后在jsp页面的回调方法里调用JS-SDK的downloadImage()方法,把刚上传至微信服务器的图片下载下来,再上传到开发者服务器。
笔者这里的代码思路是,通过步骤6上传到微信服务器后,微信返回每个图片的服务端mediaId,然后把这个参数传给自己自定义的方法,从微信服务器通过IOl流拉取下来,保存至开发者服务器,以及图片对应的预览路径的数据库保存,跟上述思路道理是一样的,只不过一个是通过前台js-sdk接口下载下来的,另外一个是自定义后台方法,通过IO流拉取下来保存的。
由于上传至微信服务器的临时素材,微信只保存3天,所以我们要传mediaId参数,通过IO流拉取下来,获取到文件流
/**
* 获取临时素材
*/
private static InputStream getMediaStream(String mediaId)throws IOException {
String url = "https://api.weixin.qq.com/cgi-bin/media/get";
Map map = WxAccessToken.getSavedAccessToken();
String access_token = (String)map.get("access_token");
String params = "access_token=" + access_token + "&media_id=" + mediaId;
InputStream is = null;
try {
String urlNameString = url + "?" + params;
URL urlGet = new URL(urlNameString);
HttpURLConnection http = (HttpURLConnection) urlGet.openConnection();
http.setRequestMethod("GET"); // 必须是get方式请求
http.setRequestProperty("Content-Type","application/x-www-form-urlencoded");
http.setDoOutput(true);
http.setDoInput(true);
http.connect();
// 获取文件转化为byte流
is = http.getInputStream();
} catch (Exception e) {
e.printStackTrace();
}
return is;
}
获取到图片流之后保存至开发者服务器物理磁盘
/**
* 保存图片至服务器
* @param mediaId
* @return 文件名
*/
public static String saveImageToDisk(String mediaId,String picType)throws IOException{
String imgPrevPath = ""; //图片预览路径
InputStream inputStream = getMediaStream(mediaId);
byte[] data = new byte[1024];
int len ;
FileOutputStream fileOutputStream = null;
try {
String prevPath = "";
String uploadPath = "";
//服务器存图路径
UploadPathUtil uploadPathUtil = new UploadPathUtil();
if("car".equals(picType)){
prevPath = uploadPathUtil.getCarPrevPath();
uploadPath = uploadPathUtil.getCarUploadPath();
}else if("idCard".equals(picType)){
prevPath = uploadPathUtil.getIdCardPrevPath();
uploadPath = uploadPathUtil.getIdCardUploadPath();
}else if("drivingLicense".equals(picType)){
prevPath = uploadPathUtil.getVehicleLicensePrevPath();
uploadPath = uploadPathUtil.getVehicleLicenseUploadPath();
}
String filename = mediaId + ".jpg";
fileOutputStream = new FileOutputStream(uploadPath + File.separator+ filename);
imgPrevPath = prevPath + File.separator + filename;
while ((len = inputStream.read(data)) != -1) {
fileOutputStream.write(data, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return imgPrevPath;
}
8、每个图片右上角的删除按钮以及删除图片事件
function delImg(dom){//多张图片删除处理
var $addImg = $(dom).parents('.multiImg').parent().next();
$(dom).parents('.multiImg').remove();
count++;//每次删除一张图片,count就加1,不用笔者在解释
$addImg.css("display","block");
}