在平时开发时，不管是前端、后端或者小程序端，为了节省带宽及存储空间，我们一般都会对上传的图片进行压缩。本文我们介绍一下使用uniapp开发小程序时，基于uni-file-picker组件进行图片压缩的方法。开启uni-file-picker自带的压缩配置uni-file-picker 组件通过配置 sizeType可以开启自带的压缩功能。sizeType: { type: Array, default () { return ['original', 'compressed'] } },'original'代表原始文件'compressed'代表启用压缩。使用也比较简单，配置一下就行了。<uni-file-picker return-type="object" fileMediatype="image" mode="grid" :sizeType="sizeType" :auto-upload="false" @select="selectImage" @delete="deleteImage"/>通过以上设置，便可实现对图片进行压缩，一般能够实现对半压缩的，比如10M的图片压缩成5M左右这样的。当然这个不是绝对的，只是个约莫的压缩率。如果需要测试，需要使用手机进行真机调试，才可以看出来文件压缩后的大小。如果对图片大小没有太大限制 ，直接这样压缩就可以了，但是有的项目会限制对图片的大小必须小于1M，这时候，光有这个设置，就满足不了需求了，这时候我们可以再采取一点措施。自定义图片压缩当uni-file-picker自带的压缩功能不能满足我们需要时，我们可以自己对图片进行压缩，自定义压缩图片时，我们可以指定压缩率或循环压缩到指定大小。当然，也需要注意压缩后的图片质量。一、创建公共压缩方法创建公共方法imageCompress，传入file进行压缩。// 图片压缩递归，小于1M跳出 export function imageCompress(file){ return new Promise((resolve, reject)=>{ let { size,path } = file let type = path.split(".")[1] //大于1M进行压缩， if(size< (1024*1024)){ resolve(file) return false } uni.compressImage({ src: path, quality: 80, success: res => { let newPath = res.tempFilePath+type let newName = res.tempFilePath.split("/")[res.tempFilePath.split("/").length-1]+type uni.getFileInfo({ filePath:res.tempFilePath, success:async (info)=>{ let newFile = {...file,size:info.size,path:newPath,name:newName,tempFilePath:res.tempFilePath} resolve(await imageCompress(newFile)) } }) } }) }) }二、修改uni-file-picker上传方法在uni-file-picker上传方法时，先调用公共方法imageCompress进行压缩，压缩完成后在进行上传。import { imageCompress } from "@/utils/leeframe.js" import { uploadImageCommon } from "@/common/api.js" export default { data() { return{ sizeType:['compressed'], //设置图片压缩 } }, onLoad(option) { this.workId = option.workId }, methods:{ //选择照片 selectImage(e){ this.timeSeting() if(e.tempFilePaths&&e.tempFiles){ this.file = e.tempFiles[0].file this.type = 'mentou' this.uploadImage() } }, // 删除照片 deleteImage(e){ this.mentouValue = {} }, // 上传照片 async uploadImage(){ // 压缩图片 this.file = await imageCompress(this.file) // 要传的参数 let params = { file:this.file } // 上传图片到相依的接口 uni.uploadFile({ url: uploadImageCommon, //后台上传地址 filePath: this.file.tempFilePath?this.file.tempFilePath:this.file.path, fileType: "image", formData:{...params}, name: 'file', header: { "content-type": "multipart/form-data", "Authorization": uni.getStorageSync('token') }, success: uploadFileRes => { let imageName = JSON.parse(uploadFileRes.data).data // 这里可以对返回的参数进行处理了 uni.showToast({ title: '上传成功', icon: "success" }); }, fail(err) { uni.showToast({ title: '上传失败', icon: "error" }); } }) }, } }
具体实现效果就是上面这个图片。里面的元素都是自己根据自己实际情况拼接进去的。在开发小程序时，我们经常会遇到【分享】这个功能，在实现分享功能时，我们一般会基于当前页面，生成一个海报进行分享。在实现时，基本上是通过canvas画一个图片，然后生成一个图片，具体的可以看下面的代码。视图代码 <view :style="{marginTop: '30rpx'}"> <view class="container-price"> <view class="container-price-left"> <view style="display: flex;flex-direction: column;align-items: center;margin-left: 30rpx;" @click="shareing"> <view> <image src="../../static/img/share.svg" style="height: 46rpx;width: 46rpx;"></image> </view> <view style="font-size: 16rpx;height: 30rpx;">分享</view> </view> <view style="display: flex;flex-direction: column;align-items: center;margin-left: 30rpx;" @click="subscribeMsg4Bid"> <view> <image src="../../static/img/subscribe.svg" style="height: 46rpx;width: 46rpx;"> </image> </view> <view style="font-size: 16rpx;height: 30rpx;">订阅</view> </view> </view> </view> </view> <uni-popup ref="popupPosters" type="bottom" border-radius="10px 10px 0 0"> <view class="popup-posters-wrap"> <image :src="posterImg" mode="aspectFill" :style="{height:'420px',width:'100%'}"> </image> <view class="popup-footer" :style="{marginBottom : (0-safeAreaHeight) + 'px'}"> <view style="text-align: center;font-size: 20rpx;">立即分享</view> <view style="display: flex;flex-direction: row;font-size: 20rpx;margin: 0 20rpx;"> <view style="display: flex;flex-direction: column;align-items: center;"> <u-icon size="80" color="#03de6d" name="weixin-circle-fill"></u-icon> <view>微信好友</view> </view> <view style="display: flex;flex-direction: column;align-items: center;margin-left: 20rpx;"> <view @click="saveToLocal"> <u-icon size="80" color="#18a5f0" name="photo-fill"></u-icon> </view> <view>保存海报</view> </view> </view> <view :style="{bottom:(safeAreaHeight+5)+'px',left:'50%',position:'fixed',fontSize:'24rpx',transform:'translate(-50%,-50%)',padding:'5rpx 15rpx'}" @click="this.$refs.popupPosters.close()"> <view>取消</view> </view> </view> </view> </uni-popup>js代码//生成海报--微信端 createPoster() { let _this = this const canvasId = "myCanvas" let ctx = uni.createCanvasContext(canvasId, _this) // 自定义组件中 一定要传this ，这里一开始没加，困惑很久，一定要写一下 // 填充背景 ctx.setFillStyle('#FFFFFF') ctx.fillRect(0, 0, _this.pupopWidth, _this.pupopHeight); ctx.save() //生成车辆图册 uni.getImageInfo({ src: _this.imageFileList[0], //这里的banner是展示的商品图 success(image) { let bannerW = _this.pupopWidth let bannerH = 400 * _this.pixelRatio let bannerX = 0 * _this.pixelRatio let bannerY = 20 * _this.pixelRatio // 将banner到画布上 ctx.drawImage(image.path, bannerX, bannerY, bannerW, bannerH) ctx.restore() ctx.save() //车辆描述 ctx.setFontSize(18 * _this.pixelRatio) ctx.setFillStyle("#000") let bannerTextX = 20 * _this.pixelRatio let bannerTextY = bannerY + bannerH + 20 * _this.pixelRatio + 20 let title = _this.inquiry.vehicleBrand + _this.inquiry.vehicleSeries + _this.inquiry.vehicleYear + _this.inquiry.vehicleModel if (title.length > 20) { title = title.slice(0, 20) + '...' } ctx.fillText(title, bannerTextX, bannerTextY, _this.pupopWidth); // 第二行文字 ctx.setFontSize(14 * _this.pixelRatio) ctx.setFillStyle("#cb171d") //事故类型 let titleAccidentType = _this.inquiry.accidentTypeName ctx.fillText(titleAccidentType, bannerTextX, bannerTextY + 30 * _this.pixelRatio, _this .pupopWidth); let accidentTypeWidth = ctx.measureText(titleAccidentType).width //所有人性质 let vehicleOwnerNature = _this.inquiry.vehicleOwnerNatureName ctx.fillText(vehicleOwnerNature, bannerTextX + accidentTypeWidth + 5 * _this.pixelRatio, bannerTextY + 30 * _this.pixelRatio, _this .pupopWidth); let vehicleOwnerNatureWidth = ctx.measureText(vehicleOwnerNature).width // 使用性质 let vehicleUsageNature = _this.inquiry.vehicleUsageNatureName ctx.fillText(vehicleUsageNature, bannerTextX + accidentTypeWidth + 5 * _this.pixelRatio + vehicleOwnerNatureWidth + 5 * _this.pixelRatio, bannerTextY + 30 * _this.pixelRatio, _this .pupopWidth); let vehicleUsageNatureWidth = ctx.measureText(vehicleUsageNature).width // 初登日期及地方位置 ctx.setFontSize(12 * _this.pixelRatio) ctx.setFillStyle("#b3b3b3") let vehicleRegisterDate = '初登日期 ' + _this.inquiry.vehicleRegisterDate let position = '停放地 ' + _this.inquiry.parkingPositionProvinceName + _this.inquiry .parkingPositionCityName let vehicleRegisterDateWithPosition = vehicleRegisterDate + " / " + position ctx.fillText(vehicleRegisterDateWithPosition, bannerTextX, bannerTextY + 30 * _this.pixelRatio + 20 * _this.pixelRatio, _this.pupopWidth); let vehicleRegisterDateWithPositionWidth = ctx.measureText(vehicleRegisterDateWithPosition) .width //画间隔线 ctx.moveTo(40 * _this.pixelRatio, bannerTextY + 20 * _this.pixelRatio + 20 * _this.pixelRatio + 20 * _this.pixelRatio); //设置起点状态 ctx.lineTo(_this.pupopWidth - 40 * _this.pixelRatio, bannerTextY + 20 * _this.pixelRatio + 20 * _this.pixelRatio + 20 * _this.pixelRatio); //设置末端状态 ctx.lineWidth = 1 * _this.pixelRatio; //设置线宽状态 ctx.strokeStyle = '#EEEEEE'; //设置线的颜色状态 ctx.stroke(); //二维码 uni.getImageInfo({ src: 'https://oss.guochewang.cn/banner/app/wechat_app.jpg', success(res) { // 画当前页面的二维码 const img_x = 20 * _this.pixelRatio const img_w = 60 * _this.pixelRatio const img_y = bannerTextY + 20 * _this.pixelRatio + 20 * _this.pixelRatio + 20 * _this.pixelRatio + 20 * _this.pixelRatio ctx.drawImage(res.path, img_x, img_y, img_w, img_w) //画提示文字 const tiptextX = img_x + img_w + 20 * _this.pixelRatio const tiptext1Y = img_y + 20 * _this.pixelRatio const tiptext2Y = img_y + 20 * _this.pixelRatio + 20 * _this .pixelRatio + 12 * _this.pixelRatio const tiptext1 = '国车网 ' const tiptext1_1 = '一个专注事故车的交易平台 ' const tiptext2 = '识别二维码查看车辆更多详情' ctx.setFontSize(16 * _this.pixelRatio) ctx.setFillStyle("#cb171d") ctx.fillText(tiptext1, tiptextX, tiptext1Y); ctx.stroke(); let tiptext1Width = ctx.measureText(tiptext1).width ctx.setFontSize(12 * _this.pixelRatio) ctx.setFillStyle("#333333") ctx.fillText(tiptext1_1, tiptextX + tiptext1Width, tiptext1Y); ctx.stroke(); ctx.setFontSize(12 * _this.pixelRatio) ctx.setFillStyle("#333333") ctx.fillText(tiptext2, tiptextX, tiptext2Y); ctx.stroke(); ctx.draw(false, () => { uni.canvasToTempFilePath({ width: _this.pupopWidth, height: _this.pupopHeight, destWidth: _this.pupopWidth, destHeight: _this.pupopHeight, canvasId: canvasId, fileType: 'png', quality: 1, success: function(res) { _this.posterImg = res .tempFilePath; //最终将canvas转换为图片 _this.$refs.popupPosters .open(); uni.hideLoading() _this.ifGeneratingPosters = false }, fail(error) { console.log('4', error) uni.showToast({ title: '生成海报失败，请稍后重试！' }) setTimeout(() => { uni.hideLoading() _this.ifGeneratingPosters = false }, 2000) } }, _this) }) }, fail(error) { console.log('获取二维码失败', error) uni.showToast({ title: '生成海报失败，获取二维码失败' }) setTimeout(() => { uni.hideLoading() _this.ifGeneratingPosters = false }, 2000) } }) }, fail(error) { console.log('生成商品图失败', error) uni.showToast({ title: '生成海报失败，获取商品图失败' }) setTimeout(() => { uni.hideLoading() _this.ifGeneratingPosters = false }, 2000) } }); }, //将图片保存到本地相册 saveToLocal() { //#ifdef MP-WEIXIN uni.saveImageToPhotosAlbum({ filePath: this.posterImg, success: () => { uni.showToast({ icon: 'success', title: '保存到相册成功' }) this.$refs.popupPosters.close() }, fail: (err) => { console.log("保存到相册失败", err) } }); //#endif },
油耗笔记OilNote集成腾讯地图实现载在图选择加油站充电站功能
油耗笔记OilNote经过两轮迭代，将地图服务由高德地图切换到百度地图，最终定格到了腾讯地图。在前面，我们已经实现了逆地址解析及周边搜索功能。接下来我们要实现打开地图，在图直接选择加油站功能。我们使用腾讯地图提供的微信小程序JavaScript SDK实现在线地图功能。一、微信小程序JavaScript SDK介绍腾讯位置服务为微信小程序提供了基础的标点能力、线和圆的绘制接口等地图组件和位置展示、地图选点等地图API位置服务能力支持，使得开发者可以自由地实现自己的微信小程序产品。 在此基础上，腾讯位置服务微信小程序JavaScript SDK是专为小程序开发者提供的LBS数据服务工具包，可以在小程序中调用腾讯位置服务的POI检索、关键词输入提示、地址解析、逆地址解析、行政区划和距离计算等数据服务，让您的小程序更强大！微信小程序JavaScript SDK开发文档二、微信小程序JavaScript SDK使用2.1、小程序域名设置在小程序管理后台 -> 开发 -> 开发管理 -> 开发设置 -> “服务器域名” 中设置request合法域名，添加https://apis.map.qq.com2.2、引入SDK下载微信小程序JavaScriptSDK，微信小程序JavaScriptSDK v1.1 JavaScriptSDK v1.2，并将解压后的.js文件引入工程中。2.3、页面引入2.3.1、在页面引入脚本文件 // 引入SDK核心类，js文件根据自己业务，位置可自行放置 var QQMapWX = require('../../common/utils/qqmap-wx-jssdk.min.js'); //引入下载好的sdk var qqmapsdk; // 实例化API核心类 qqmapsdk = new QQMapWX({ key: LocationUtils.TENCENT_MAP_KEY });2.3.2、查询加油站//页面加载 onLoad(options) { let that = this; this.latitude = uni.getStorageSync('latitude'); this.longitude = uni.getStorageSync('longitude'); this.oilType = options.oilType === '电' ? '充电站' : '加油站' let iconPath = 'https://oss.lisen.cc/oilnote/images/chargingPile.png'; if (options.oilType !== '电') { iconPath = 'https://oss.lisen.cc/oilnote/images/oilStation.png'; } // 调用接口 qqmapsdk.search({ keyword: encodeURI(that.oilType), location: { latitude: that.latitude, longitude: that.longitude }, page_size: 20, auto_extend: '1', success: function(res) { var selfLocation = { id: -1, latitude: that.latitude, longitude: that.longitude, width: 20, height: 20, iconPath: 'https://oss.lisen.cc/oilnote/images/location.png', title: "我的位置", callout: { content: '我的位置', color: '#adec9c', display: 'BYCLICK', borderRadius: 5, } }; that.makers.push(selfLocation); if (res.status === 0) { res.data.forEach((item, index) => { let obj = { id: index, latitude: item.location.lat, longitude: item.location.lng, width: 20, height: 20, iconPath: iconPath, title: item.title, callout: { content: item.title, bgColor: '#adec9c', display: 'ALWAYS', borderRadius: 5, } }; that.makers.push(obj); }) } }, fail: function(res) { console.log(res); }, }) }markers数组就是我们要标记的坐标点，数组中对象的属性id，表示标记点id，类型为Number，必填项，marker点击事件回调会返回此id，建议为每个marker设置上Number类型id，保证更新marker时有更好的性能。latitude，纬度，类型Number，必填项，浮点数，范围 -90 ~ 90longitude，经度，类型Number，必填项，浮点数，范围 -180 ~ 180title，标注点名，类型String，不是必填，点击时显示，callout存在时将被忽略iconPath，显示的图标，类型String，必填项，项目目录下的图片路径rotate，旋转角度，类型Number，不是必填，顺时针旋转的角度，范围 0 ~ 360，默认为 0alpha，标注的透明度，类型Number，不是必填，默认1，无透明，范围 0 ~ 1width，标注图标宽度，类型Number，不是必填，默认为图片实际宽度height，标注图标高度，类型Number，不是必填，默认为图片实际高度callout，自定义标记点上方的气泡窗口，类型Object，不是必填 - 可识别换行符label，为标记点旁边增加标签，类型Object，不是必填 - 可识别换行符anchor，经纬度在标注图标的锚点，默认底边中点，不是必填，{x, y}，x表示横向(0-1)，y表示竖向(0-1)。{x: .5, y: 1} 表示底边中点。marker上的气泡callout（Object类型）, marker数组上属性callout对象使用属性：content，文本，Stringcolor，文本颜色，StringfontSize，文字大小，NumberborderRadius，callout边框圆角，NumberbgColor，背景色，Stringpadding，文本边缘留白，Numberdisplay，'BYCLICK':点击显示; 'ALWAYS':常显，StringtextAlign，文本对齐方式。有效值: left, right, center，Stringmarker上的标签label（Object类型）content，文本，Stringcolor，文本颜色，StringfontSize，文字大小，Numberx，label的坐标，原点是 marker 对应的经纬度，Numbery，label的坐标，原点是 marker 对应的经纬度，NumberborderWidth，边框宽度，NumberborderColor，边框颜色，StringborderRadius，边框圆角，NumberbgColor，背景色，Stringpadding，文本边缘留白，NumbertextAlign，文本对齐方式。有效值: left, right, center，String2.3.3、界面展示获取到数据之后，就是通过marker属性进行展示 2.3.3.1、地图组件的属性：longitude（类型为Number，没有默认值，表示中心经度）latitude（类型为Number，没有默认值，表示中心纬度）scale（类型为Number，默认值为16，缩放级别取值范围为5-18）markers（类型为Array数组，类型为数组即表示地图上可以有多个，没有默认值，表示标记点）polyline（类型为Array数组，没有默认值，表示路线，数组上的所有点连成线）circles（类型Array数组，表示圆）controls（类型Array数组，表示控件）include-points（类型Array数组，表示缩放视野已包含所有给定的坐标点）enable-3D（类型为Boolean，默认值为false，表示是否显示3D搂块）show-compass（类型为Boolean，默认值为false，表示为是否显示指南针）enable-overlooking（类型为Boolean，默认值为false，表示为是否开启俯视）enable-satellite（类型为Boolean，默认值为false，表示为是否开启卫星图）enable-traffic（类型为Boolean，默认值为false，表示为是否开启实时路况）show-location（类型为Boolean，表示显示带有方向的当前定位点）polygons（类型Array，表示为多边形）2.3.3.2、组件方法@markertap-表示点击标记点时触发，e.detail={markerId}@labeltap-表示点击label时触发，e.detail = {markerId}@callouttap-表示点击标记点对应的气泡时触发，e.detail = {markerId}@controltap-表示点击控件时触发，e.detail = {controlId}@regionchange-表示视野发生变化时触发@tap-表示点击地图时触发; App-nuve、微信小程序2.9支持返回经纬度@updated-表示在地图渲染更新完成时触发
油耗笔记OilNote周边搜索能力由百度地图切换成腾讯地图
油耗笔记OilNote其实对地图搜索能力，其实就是查询周边加油站的功能进行过一次升级，上次升级是将地图搜索能力由高德地图切换成了百度地图，切换原因也非常简单，高德地图无耻的将每日额度改成了100，根本没法用了。具体切换过程，感兴趣的可以看看。油耗笔记OilNote周边搜索能力由高德地图切换成百度地图 - 李森的博客 (lisen.cc)一、为什么又不用百度地图了1.1、对搜索结果不满意其实，从高德地图切换到百度地图也不过一天的时间。之所以不使用百度地图，最根本的原因，是百度地图所谓的圆形搜索能力真的太差劲了。好多东西根本就搜不到。而且作为记录油耗的一个小程序，搜索周边加油站其实是最基础最基本的一个功能，但是百度地图的圆形搜索能力简直就弱爆了。明明旁边就有加油站，死活就是搜索不到。但是百度地图自己却能搜索到。比如这个地图是当时使用百度地图的搜索加油站的结果下面这个是切换到腾讯地图在相同位置的搜索加油站的结果但是，百度地图搜索的充电站数量好像也不少，跟腾讯地图也有差不多。1.2、坐标转换比较麻烦使用uni.getLocation()方法虽然获取到的坐标是gcj02，但是在百度地图下，得使用百度的坐标，必须转换一次，不然坐标误差是非常大的。1.3、官方文档写的太乱百度地图的官方文档写的，真的是一言难尽。1.4、资料很少貌似很多人都用高德的地图API，网上一搜很多，但是百度地图的相对就比较少了。虽然腾讯地图网上资料比较少，但是官网的开发文档还是很清晰明了的，这在一定程度上弥补了网上资料少的缺点。二、腾讯地图额度信息腾讯地图也是比较良心的，每日额度是三家里面最多的10000次/天，是高德地图的100倍，百度地图的2倍。缺点就是并发比较少，只有5次/秒。对于我们现阶段也是很充足的了。三、微信公众平台配置如果使用腾讯地图，必须提前把腾讯地图Api放到域名白名单里面。微信小程序后台，依次定位到开发管理→开发设置→服务器域名，在request合法域名里面加上百度地图的网址https://apis.map.qq.com/如果微信开发者工具提示域名不合法，记得刷新一下开发者工具的域名信息。刷新后记得重新编译项目。四、获取附近加油站功能改造其实三家地图的API都是类似的，我这里主要用了两个API，逆地址解析geocoder还有地点搜索search4.1、逆地址解析用于将uniapp获取到的经纬度解析成实际位置。let getLocation = function(radius = 1000, successFn) { var latitude = '' //纬度 var longitude = '' //经度 if (uni.getStorageSync('position') == '') { uni.getLocation({ geocode: true, type: 'gcj02', altitude: true, accuracy: 'best', isHighAccuracy: true, success: (res) => { console.log('位置是', res) latitude = res.latitude longitude = res.longitude uni.setStorageSync('latitude', latitude) uni.setStorageSync('longitude', longitude) uni.request({ url: 'https://apis.map.qq.com/ws/geocoder/v1/?key=' + TENCENT_MAP_KEY + '&location=' + latitude + ',' + longitude + '&get_poi=1&output=json', success: function(res) { let result = res.data.result let province = result.address_component.province let city = result.address_component.city // 省份名称 uni.setStorageSync('province', province) // 城市名称 uni.setStorageSync('city', city) // 城市编号 uni.setStorageSync('citycode', result.ad_info.city_code) successFn(result, province, city) }, fail(err) { console.log('获取位置信息失败：' + JSON.stringify(err)) } }) }, fail: (err) => { console.log('获取位置信息失败：' + JSON.stringify(err)) } }) } }接口示例https://apis.map.qq.com/ws/geocoder/v1/?location=39.984154,116.307490&key=OB4BZ-D4W3U-B7VVO-4PJWW-6TKDJ-WPB77&get_poi=1请求参数参数必填说明示例key是开发密钥（Key）key=OB4BZ-D4W3U-B7VVO-4PJWW-6TKDJ-*location是经纬度（GCJ02坐标系），格式： location=lat<纬度>,lng<经度>location= 39.984154,116.307490get_poi否是否返回周边地点（POI）列表，可选值： 0 不返回(默认) 1 返回get_poi=1poi_options否周边POI列表控制参数： 1 poi_options=address_format=short 返回短地址，缺省时返回长地址 2 poi_options=radius=5000 半径，取值范围 1-5000（米） 3 poi_options=policy=1/2/3/4/5 控制返回场景， policy=1[默认] 以地标+主要的路+近距离POI为主，着力描述当前位置； policy=2 到家场景：筛选合适收货的POI，并会细化收货地址，精确到楼栋； policy=3 出行场景：过滤掉车辆不易到达的POI(如一些景区内POI)，增加道路出入口、交叉口、大区域出入口类POI，排序会根据真实API大用户的用户点击自动优化。 policy=4 社交签到场景，针对用户签到的热门 地点进行优先排序。 policy=5 位置共享场景，用户经常用于发送位置、位置分享等场景的热门地点优先排序 4 注：policy=1/2/3最多返回10条周边POI，policy=4/5最多返回20条， 如需更多请参见地点搜索-周边推荐【单个参数写法示例】： poi_options=address_format=short 【多个参数英文分号间隔，写法示例】： poi_options=address_format=short;radius=5000;policy=2output否返回格式：支持JSON/JSONP，默认JSONoutput=jsoncallback否JSONP方式回调函数callback=function1响应结果名称类型必填说明 statusnumber是状态码，0为正常，其它为异常，详细请参阅状态码说明 messagestring是状态说明 request_idstring是本次请求的唯一标识 resultobject是逆地址解析结果 addressstring是以行政区划+道路+门牌号等信息组成的标准格式化地址formatted_addressesobject否结合知名地点形成的描述性地址，更具人性化特点 recommendstring否推荐使用的地址描述，描述精确性较高roughstring否粗略位置描述 address_componentobject是地址部件，address不满足需求时可自行拼接 nationstring是国家provincestring是省 citystring是市，如果当前城市为省直辖县级区划，city与district字段均会返回此城市 注：省直辖县级区划adcode第3和第4位分别为9、0，如济源市adcode为419001 districtstring否区，可能为空字串 streetstring否街道，可能为空字串 street_numberstring否门牌，可能为空字串 ad_infoobject是行政区划信息 nation_codestring是国家代码（ISO3166标准3位数字码） adcodestring是行政区划代码，规则详见：行政区划代码说明 city_codestring是城市代码，由国家码+行政区划代码（提出城市级别）组合而来，总共为9位namestring是行政区划名称 locationobject是行政区划中心点坐标 latnumber是纬度lngnumber是经度 nationstring是国家 provincestring是省 / 直辖市 citystring是市 / 地级区 及同级行政区划，如果当前城市为省直辖县级区划，city与district字段均会返回此城市 注：省直辖县级区划adcode第3和第4位分别为9、0，如济源市adcode为419001 districtstring否区 / 县级市 及同级行政区划 address_referenceobject否坐标相对位置参考 famous_areaobject否知名区域，如商圈或人们普遍认为有较高知名度的区域 idstring是地点唯一标识titlestring否名称/标题 locationobject否坐标 latnumber否纬度lngnumber否经度 _distancenumber否此参考位置到输入坐标的直线距离 _dir_descstring否此参考位置到输入坐标的方位关系，如：北、南、内 business_areaobject否商圈，目前与famous_area一致 townobject否乡镇街道 idstring是地点唯一标识titlestring否名称/标题 locationobject否坐标 latnumber否纬度lngnumber否经度 _distancenumber否此参考位置到输入坐标的直线距离 _dir_descstring否此参考位置到输入坐标的方位关系，如：北、南、内 landmark_l1object否一级地标，可识别性较强、规模较大的地点、小区等 【注】对象结构同 famous_area landmark_l2object否二级地标，较一级地标更为精确，规模更小 【注】：对象结构同 famous_area streetobject否街道 【注】：对象结构同 famous_area street_numberobject否门牌 【注】：对象结构同 famous_area crossroadobject否交叉路口 【注】：对象结构同 famous_area waterobject否水系 【注】：对象结构同 famous_area poi_countnumber 查询的周边poi的总数，仅在传入参数get_poi=1时返回 poisarray否周边地点（POI）数组，数组中每个子项为一个POI对象 idstring否地点（POI）唯一标识titlestring否名称 addressstring否地址 categorystring否地点分类信息 locationobject否提示所述位置坐标 latnumber否纬度lngnumber否经度 ad_infoobject否行政区划信息 adcodenumber是行政区划代码provincestring是省，如果当前城市为直辖市，返回空 citystring是市，如果当前城市为省直辖县级区划，city与district字段均会返回此城市 注：省直辖县级区划adcode第3和第4位分别为9、0，如济源市adcode为419001 districtstring是区 _distancenumber否该POI到逆地址解析传入的坐标的直线距离 4.2、获取附近加油站let getOilStation = function(stationType = '加油站', radius = 1000, successFn) { let latitude = uni.getStorageSync('latitude') //纬度 let longitude = uni.getStorageSync('longitude') //经度 uni.request({ url: 'https://apis.map.qq.com/ws/place/v1/search?key=' + TENCENT_MAP_KEY + '&boundary=nearby(' + latitude + ',' + longitude + ',' + radius + ',0)' + '&keyword=' + encodeURI(stationType) + '&page_size=20&page_index=1' + '&orderby=_distance&output=json', success: function(res) { successFn(res) } }) }请求URLhttps://apis.map.qq.com/ws/place/v1/search请求参数请求方式（Method）： GET参数必填说明示例key是开发密钥（Key）key=OB4BZ-D4W3U-*keyword是搜索关键字，长度最大96个字节，注：keyword仅支持检索一个。 （API采用UTF-8字符编码，1个英文字符占用1个字节， 1个中文字符占3个字节，具体请参阅相关技术资料）keyword=酒店，注意键值要进行URL编码（推荐encodeURI），如 keyword=%e9%85%92%e5%ba%97boundary是格式： boundary=nearby(lat,lng,radius[, auto_extend]) 子参数： lat,lng：搜索中心点的经纬度，格式顺序为纬度在前，经度在后 radius：搜索半径，单位：米，取值范围：10到1000 auto_extend：[可选] 当前范围无结果时，是否自动扩大范围，取值： 0 不扩大 1 [默认] 自动扩大范围（依次按照按1公里、2公里、5公里， 最大到全城市范围搜索）boundary=nearby(28.681114,115.918377,1000,1)get_subpois否是否返回子地点，如大厦停车场、出入口等取值： 0 [默认]不返回 1 返回get_subpois=1filter否筛选条件 1. 指定分类筛选，语句格式为： category=分类名1,分类名2 最多支持5个分类词（支持的分类请参考：POI分类表） 2. 排除指定分类，语句格式为： category<>分类名1,分类名2 最多支持5个分类词（支持的分类请参考：POI分类表） 3. 筛选有电话的地点：tel<>null搜索指定分类 filter=category=公交站 搜索多个分类 filter=category=大学,中学 排除指定分类 filter=category<>商务楼宇 （注意参数值要进行url编码）orderby否排序，支持按距离由近到远排序，取值：_distance 说明： 1. 周边搜索默认排序会综合考虑距离、权重等多方面因素 2. 设置按距离排序后则仅考虑距离远近，一些低权重的地点可能因距离近排在前面，导致体验下降orderby=_distancepage_size否每页条目数，最大限制为20条，默认为10条page_size=10page_index否第x页，默认第1页page_index=2output否返回格式： 支持JSON/JSONP，默认JSONoutput=jsoncallback否JSONP方式回调函数callback=function1响应结果名称类型必填说明 statusnumber是状态码，0为正常，其它为异常，详细请参阅状态码说明 messagestring是状态说明 countnumber是本次搜索结果总数，另外本服务限制最多返回200条数据(data)， 翻页（page_index）超过搜索结果总数 或 最大200条限制时，将返回最后一页数据。 request_idstring是本次请求的唯一标识，由系统自动生成，用于追查结果有异常时使用 dataarray是搜索结果POI（地点）数组，每项为一个POI（地点）对象 idstring是POI（地点）唯一标识 titlestring是POI（地点）名称 addressstring是地址 telstring是电话 categorystring是POI（地点）分类 typenumber是POI类型，值说明：0:普通POI / 1:公交车站 / 2:地铁站 / 3:公交线路 / 4:行政区划 locationobject是坐标 latnumber是纬度 lngnumber是经度 _distancenumber是距离，单位： 米，在周边搜索、城市范围搜索传入定位点时返回 ad_infoobject是行政区划信息 adcodenumber是行政区划代码，详见：行政区划代码说明 provincestring是省 citystring是市，如果当前城市为省直辖县级区划，此字段会返回为空，由district字段返回。 注：省直辖县级区划adcode第3和第4位分别为9、0，如济源市adcode为419001 districtstring是区sub_poisarray否子地点列表，仅在输入参数get_subpois=1时返回 parent_idstring是主地点ID，对应data中的地点IDidstring是地点唯一标识 titlestring是地点名称 telstring是电话 categorystring是POI（地点）分类 typenumber是POI类型，值说明：0:普通POI / 1:公交车站 / 2:地铁站 / 3:公交线路 / 4:行政区划 addressstring是地址 locationobject是坐标 latnumber是纬度lngnumber是经度 ad_infoobject是行政区划信息 adcodenumber是行政区划代码，详见：行政区划代码说明 provincestring是省 citystring是市，如果当前城市为省直辖县级区划，此字段会返回为空，由district字段返回。 注：省直辖县级区划adcode第3和第4位分别为9、0，如济源市adcode为419001 districtstring是区五、使用腾讯地图一些问题目前切换到腾讯地图之后遇到的一些问题不能显示加油站或充电站图片，百度地图是有个参数photo_show，如果photo_show开启，会提示授权失败。具体原因不清楚，没搜到，但是腾讯地图是直接就没有这个参数。并发确实少点。
油耗笔记OilNote周边搜索能力由高德地图切换成百度地图
一、交代下背景以前油耗笔记OilNote检索周边加油站功能，都是用的高德地图，起初，也不知道高德Api限制多少访问量，反正一直也没提示过超额，但是今天，莫名的收到了两条超额的短信。一开始收到80%超额的时候也没太当回事，觉得可能这个月改版，调试用的数量比较大，但是紧接着就收到了超额100%的提示，就感觉到不太对劲了，于是登录了高德后台，查询了一下配额。结果真是乖了个乖，搜索服务每日限额100次，这简直就是坑了，100次跟没有没啥区别，高德这是直接劝退个人开发者的节奏。查了下站内信，2022年10月26日发的站内信（没短信），通知2022年10月27日调整限额，连给你调整的时间都不给。坑你没商量呀。二、百度地图为什么选择百度地图呢？介个，国内好像除了高德就是百度了，不用也没办法。因为我就用到了地点检索，百度地图目前还算比较良心的，个人开发者每日限额是5000次。是高德地图的50倍。并发限制都是一样的30QPS。百度地图创建应用的时候，记得选择微信小程序，否则逆地理编码是不能用的。三、微信公众平台配置如果使用百度地图，必须提前把百度地图Api放到域名白名单里面。微信小程序后台，依次定位到开发管理→开发设置→服务器域名，在request合法域名里面加上百度地图的网址https://api.map.baidu.com如果微信开发者工具提示域名不合法，记得刷新一下开发者工具的域名信息。刷新后记得重新编译项目。三、功能改造既然决定了用百度地图，那么剩下的就是如何对程序进行改造了。因为我的小程序是使用uniapp开发的，所以这里就介绍一下uniapp的整个的改造过程。为了不影响在线版本，暂时先不删除高德域名。3.1、manifest.json改造manifest.json用于配置地图的Key，因为我之前使用的是高德的地图，切换到百度后，需要将相关配置改成百度地图的。其实我现在只是用了微信小程序，AK我直接在代码里面写死了，这里配置的appkey我感觉没啥用，对uniapp理解不是很深入，这块可能更多的是给App用的吧。我们Maps需要勾选百度地图并取消高德地图。具体的key，可以在百度开放平台，个人创建的应用中找到。3.2、获取附近加油站功能改造其实改造也比较简单，高德跟百度Api还是比较类似的。3.2.1、原来高德获取周边加油站的代码let getLocation = function(radius = 1000, successFn) { var latitude = '' //纬度 var longitude = '' //经度 if (uni.getStorageSync('position') == '') { uni.getLocation({ geocode: true, type: 'gcj02', altitude: true, accuracy: 'best', isHighAccuracy: true, success: (res) => { console.log('位置是', res) latitude = res.latitude longitude = res.longitude uni.setStorageSync('latitude', latitude) uni.setStorageSync('longitude', longitude) uni.request({ url: 'https://restapi.amap.com/v3/geocode/regeo?key=AK' + '&location=' + longitude + ',' + latitude + '&poitype=010100&radius=' + radius + '&extensions=all&batch=false&roadlevel=0', success: function(res) { var regeocode = res.data.regeocode // 省份名称 uni.setStorageSync('province', regeocode.addressComponent.province) // 城市名称 uni.setStorageSync('city', regeocode.addressComponent.city) // 城市编号 uni.setStorageSync('citycode', regeocode.addressComponent.citycode) successFn(regeocode) }, fail(err) { console.log('获取加油站信息失败：' + JSON.stringify(err)) } }) }, fail: (err) => { console.log('获取加油站信息失败：' + JSON.stringify(err)) } }) } }3.2.2、百度地图获取逆地址编码（根据坐标获取位置）let getLocation = function(radius = 1000, successFn) { var latitude = '' //纬度 var longitude = '' //经度 uni.getLocation({ geocode: true, type: 'gcj02', altitude: true, accuracy: 'best', isHighAccuracy: true, success: (res) => { console.log('位置是', res) latitude = res.latitude longitude = res.longitude uni.request({ url: 'https://api.map.baidu.com/geoconv/v1/?' + 'ak=您的AK（类型是服务端的）' + '&coords=' + longitude + ',' + latitude + '&from=3&to=5&output=json', success: function(res) { debugger if (res.data.status === 0) { longitude = res.data.result[0].x latitude = res.data.result[0].y uni.setStorageSync('latitude', latitude) uni.setStorageSync('longitude', longitude) uni.request({ url: 'https://api.map.baidu.com/reverse_geocoding/v3/?' + 'ak=您的AK（类型是小程序的）&output=json' + '&coord_type=gcj02ll' + '&location=' + latitude + ',' + longitude + '&radius=' + radius, success: function(res) { if (res.data.status === 0) { // 省份名称 uni.setStorageSync('province', res.data .result.addressComponent .province) // 城市名称 uni.setStorageSync('city', res.data .result.addressComponent .city) // 城市编号 uni.setStorageSync('citycode', res.data .result.cityCode) successFn(res.data, res.data.result .addressComponent.province, res.data.result .addressComponent.city) } }, fail(err) { console.log('获取位置信息失败：' + JSON.stringify(err)) } }) } }, fail(err) { console.log('获取位置信息失败：' + JSON.stringify(err)) } }) }, fail: (err) => { console.log('获取位置信息失败：' + JSON.stringify(err)) } }) }需要注意，uni.getLocation获取gcj02时的坐标适用于高德等地图，但是不适用百度地图，需要调用百度地图坐标转换接口geoconv转换成百度地图的坐标，不然会有较大的误差，同时地图转换接口使用的AK需要是服务端类型的应用的AK，也就是说我们实际上使用了两种AK，一种是服务端的，一种是小程序的。3.2.3、百度地图获取附近加油站let getOilStation = function(radius = 1000, successFn) { let latitude = uni.getStorageSync('latitude') //纬度 let longitude = uni.getStorageSync('longitude') //经度 uni.request({ url: 'https://api.map.baidu.com/place/v2/search?' + 'query=充电站$加油站&ak=您的AK（类型是小程序的）&output=json' + '&scope=2' + '&coord_type=2' + '&page_size=20' + '&location=' + latitude + ',' + longitude + '&radius=' + radius, success: function(res) { successFn(res) } }) }改造完成后验证一下四、使用百度地图一些问题目前切换到百度地图之后遇到的一些问题搜索功能显示不全，我家附近其实就有一个中国石化，但是百度地图搜索不到，充电站搜索还算比较全的不能显示图片，如果photo_show开启，会提示授权失败。
解决小程序报错the api need to be declared in the requiredPrivateInfos field in app.json
一、报错信息从报错信息可以大概看到，应该是缺少授权导致的。PS:这里真的想吐槽一下微信的接口，真的是三天两头的变，发个版就得调整一次，心累。二、微信官方文档说明打开微信开发文档查看配置信息。为了开发者能够正常使用获取模糊地理位置等接口，以及后续对于代码提审环节的优化，自 **2022 年 7 月 14 日起，开发者在使用地理位置相关接口时（共计 8 个），需要提前在 app.json 中进行配置。在代码中使用的地理位置相关接口（共计 8 个），开发者均需要在 app.json 中 requiredPrivateInfos 配置项中声明，代码格式如下：地理位置接口新增与相关流程调整 | 微信开放社区三、解决方法因为我是用uni-app框架开发的，所以是在manifest.json里面配置，如果是用其他框架开发的话，一般都是在app.json里面配置，不过原理都是一样的。
微信小程序获取用户头像后上传到七牛云
一、事情起因【油耗笔记OilNote】小程序好久没有升级了，最近打算对代码进行一些优化，但是新版本突然发现无法获取到用户微信头像及微信昵称了。查阅官方文档才知道，官方有对getUserProfile接口进行调整了。自 2022 年 10 月 25 日 24 时后（以下统称 “生效期” ），用户头像昵称获取规则将进行如下调整：自生效期起，小程序 wx.getUserProfile 接口将被收回：生效期后发布的小程序新版本，通过 wx.getUserProfile 接口获取用户头像将统一返回默认灰色头像，昵称将统一返回 “微信用户”。生效期前发布的小程序版本不受影响，但如果要进行版本更新则需要进行适配。自生效期起，插件通过 wx.getUserInfo 接口获取用户昵称头像将被收回：生效期后发布的插件新版本，通过 wx.getUserInfo 接口获取用户头像将统一返回默认灰色头像，昵称将统一返回 “微信用户”。生效期前发布的插件版本不受影响，但如果要进行版本更新则需要进行适配。通过 wx.login 与 wx.getUserInfo 接口获取 openId、unionId 能力不受影响。「头像昵称填写能力」支持获取用户头像昵称：如业务需获取用户头像昵称，可以使用「头像昵称填写能力」（基础库 2.21.2 版本开始支持，覆盖iOS与安卓微信 8.0.16 以上版本），具体实践可见下方《最佳实践》。小程序 wx.getUserProfile 与插件 wx.getUserInfo 接口兼容基础库 2.27.1 以下版本的头像昵称获取需求：对于来自低版本的基础库与微信客户端的访问，小程序通过 wx.getUserProfile 接口将正常返回用户头像昵称，插件通过 wx.getUserInfo 接口将正常返回用户头像昵称，开发者可继续使用以上能力做向下兼容。现在只要是发布的新版本，默认都需要调整，不然就显示下面灰色头像，已经发布的版本不受影响。既然官方调整了，那么我们也只有被动接受的份。二、油耗笔记的开发框架油耗笔记OilNote不是直接使用微信开发者工具开发的，而是使用UniApp开发的，后端是SpringBoot。三、改进思路3.1、获取用户头像由于官方指导意见是，使用button组件 open-type 的值设置为 chooseAvatar，当用户选择需要使用的头像之后，可以通过 bindchooseavatar 事件回调获取到头像信息的临时路径。从官方的指导我们可以看到，微信并没有给我们返回一个具体的路径，只是返回了一个临时的路径，因此我们就必须自己获取到这个临时的文件，然后存储起来。3.2、使用七牛云由于使用的腾讯云的低配服务器，带宽、存储都比较捉襟见肘，所以我打算把头像都存储到七牛云上，既能减轻带宽压力也能节省服务器空间。四、具体改进4.1、UniApp页面改进当用户通过微信登录时，此时获取用户头像信息，如果头像存在，登录之后跳转到首页，否则跳转到个人信息界面，让用户维护头像及昵称，此方法适用于新用户，同时也适用于老用户重新登录。//微信授权登录 getUserInfo(e) { let that = this; var p = this.getSetting(); p.then(function(isAuth) { console.log('是否已经授权', isAuth); if (isAuth) { console.log('用户信息，加密数据', e); //eData 包括//微信头像//微信名称 还有加密的数据. // let eData = JSON.parse(e.detail.rawData); uni.getUserProfile({ desc: 'Wexin', // 这个参数是必须的 success: function(infoRes) { //接下来就是访问接口. that.$request( 'wechat/authCode2Session?code=' + that.weChatCode, 'POST' ).then(function(res) { if (res.code == 200) { //将接口返回的数据保存在全局变量中. let userInfo = {} // 用户id userInfo.id = res.data.id userInfo.username = res.data.username userInfo.tel = res.data.tel userInfo.email = res.data.email userInfo.wechatOpenId = res.data.wechatOpenId userInfo.nickName = res.data.nickName userInfo.avatarUrl = res.data.avatarUrl ? res.data .avatarUrl : infoRes.userInfo.avatarUrl userInfo.gender = infoRes.userInfo.gender userInfo.password = '' if (!userInfo.province) { userInfo.province = uni.getStorageSync('province') } if (!userInfo.city) { userInfo.city = uni.getStorageSync('city') } uni.setStorageSync('userInfo', userInfo); if (!res.data.avatarUrl) { //没有头像时，跳转到用户信息维护界面 uni.redirectTo({ url: '/pages/profile/profile' }) } else { uni.redirectTo({ url: '/pages/index/index' }) } } }, function(err) { uni.showToast({ title: '授权登录失败！', mask: true, icon: 'none' }) } ) } }); } else { uni.showToast({ title: '授权失败，请确认授权已开启', mask: true, icon: 'none' }) } }); },4.1.1、登录界面改造4.1.2、个人信息界面改造油耗笔记之前有一个【个人信息】页面，因此我打算把修改头像的功能发放到里面。在合适的位置放入选择头像的按钮 <view class="cu-avatar xl round margin-center" :style="{backgroundImage:'url('+userInfo.avatarUrl+')'}"></view> 增加回调方法用户选择微信头像之后，会回调chooseavatar方法，因此我们增加一个chooseavatar用于用户选择头像之后上传到服务器（进一步上传到七牛）//选择头像回调 onChooseAvatar(e) { const that = this; this.$set(this.userInfo, "avatarUrl", e.detail.avatarUrl); uni.uploadFile({ url: operate.api + 'user/uploadAvatar/', //上传接口 header: { token: that.userInfo.id ? that.userInfo.id : '', }, formData: { 'userInfo': JSON.stringify(that.userInfo) }, filePath: e.detail.avatarUrl, name: 'file', success: (uploadFileRes) => { uni.hideLoading(); const back = JSON.parse(uploadFileRes.data); if (back.code == 200) { that.$set(that.userInfo, 'avatarUrl', back.data.avatarUrl) } else { uni.showToast(back.msg) } }, fail: (error) => { uni.hideLoading(); uni.showToast("图片上传失败，请联系开发！") }, complete: function() { uni.hideLoading(); } }); }4.2、后端改造我们首先在后端增加一个方法，用于接受前端传递的附件及其他参数（我这里主要传递的是用户信息，用户更新用户表，记录头像地址）。4.2.1、Controller增加接受用户上传头像的Api，具体的实现我们稍后在说。/** * 上传头像 * * @param multipartFile 文件信息 * @return 用户信息 */ @PostMapping("uploadAvatar") public AjaxResult uploadAvatar(@RequestParam("file") MultipartFile multipartFile,@RequestParam("userInfo") String oilUser) { return AjaxResult.success(oilUserService.uploadAvatar(multipartFile)); }4.2.2、增加七牛云依赖经过上面改造，我们已经可以将用户头像上传到我们的后端了，接下来的任务就是将头像上传到我们的七牛云了。现在pom.xml中增加七牛云的依赖 <dependency> <groupId>com.qiniu</groupId> <artifactId>qiniu-java-sdk</artifactId> <version>7.2.28</version> </dependency>4.2.3、增加七牛云配置为了方便使用，我们将七牛云的一些配置信息放入yaml文件中，方便维护。# ========================== ↓↓↓↓↓↓ 七牛云配置 ↓↓↓↓↓↓ ========================== qiniu: accessKey: XXX # Key secretKey: XXX # 密钥 bucket: XXX # 空间名称 domain: XXX # 访问域名 dir: XXX/ # 目录参数说明：accessKey：AK，在七牛云，个人中心，密钥管理中可以看到secretKey：SK，在七牛云，个人中心，密钥管理中可以看到bucket：空间名称，根据自己创建的空间填写domain：访问域名，根据控件绑定的域名实际填写dir：存储路径，因为七牛云默认是直接存储到根目录，为了方便管理，我们可以创建子目录，比如avatar，可以填写avatar/4.2.4、增加配置类为了方便使用，我们将yaml的值，映射到配置类上。/** * 七牛云实体 */ @Component @ConfigurationProperties(prefix = "qiniu") public class QiNiuConfig { /** * Key */ private static String accessKey; /** * 密钥 */ private static String secretKey; /** * 空间名称 */ private static String bucket; /** * 访问域名 */ private static String domain; /** * 目录 */ private static String dir; public static String getAccessKey() { return accessKey; } public void setAccessKey(String accessKey) { QiNiuConfig.accessKey = accessKey; } public static String getSecretKey() { return secretKey; } public void setSecretKey(String secretKey) { QiNiuConfig.secretKey = secretKey; } public static String getBucket() { return bucket; } public void setBucket(String bucket) { QiNiuConfig.bucket = bucket; } public static String getDomain() { return domain; } public void setDomain(String domain) { QiNiuConfig.domain = domain; } public static String getDir() { return dir; } public void setDir(String dir) { QiNiuConfig.dir = dir; } } 4.2.5、封装公共方法为了方便调用，我们将上传、删除等方法封装到单独的服务中。接口/** * 七牛接口 */ public interface IQiNiuService { /** * 以文件的形式上传 * * @param file * @param fileName: * @return: java.lang.String */ String uploadFile(File file, String fileName) throws QiniuException; /** * 以流的形式上传 * * @param inputStream * @param fileName: * @return: java.lang.String */ String uploadFile(InputStream inputStream, String fileName) throws QiniuException; /** * 删除文件 * * @param key: * @return: java.lang.String */ String delete(String key) throws QiniuException; } 实现@Service public class QiNiuServiceImpl implements IQiNiuService, InitializingBean { // 七牛文件上传管理器 private final Configuration cfg; private final Auth auth; public QiNiuServiceImpl() { // //构造一个带指定 Region 对象的配置类 cfg = new Configuration(Region.huadong()); auth = Auth.create(QiNiuConfig.getAccessKey(), QiNiuConfig.getSecretKey()); } /** * 定义七牛云上传的相关策略 */ private StringMap putPolicy; @Override public String uploadFile(File file, String fileName) throws QiniuException { if (!StringUtils.isEmpty(QiNiuConfig.getDir())) { fileName = QiNiuConfig.getDir() + fileName; } UploadManager uploadManager = new UploadManager(cfg); Response response = uploadManager.put(file, fileName, getUploadToken()); int retry = 0; while (response.needRetry() && retry < 3) { response = uploadManager.put(file, fileName, getUploadToken()); retry++; } if (response.statusCode == 200) { return "http://" + QiNiuConfig.getDomain() + "/" + fileName; } return "上传失败!"; } @Override public String uploadFile(InputStream inputStream, String fileName) throws QiniuException { if (!StringUtils.isEmpty(QiNiuConfig.getDir())) { fileName = QiNiuConfig.getDir() + fileName; } UploadManager uploadManager = new UploadManager(cfg); Response response = uploadManager.put(inputStream, fileName, getUploadToken(), null, null); int retry = 0; while (response.needRetry() && retry < 3) { response = uploadManager.put(inputStream, fileName, getUploadToken(), null, null); retry++; } if (response.statusCode == 200) { return "http://" + QiNiuConfig.getDomain() + "/" + fileName; } return "上传失败!"; } @Override public String delete(String key) throws QiniuException { BucketManager bucketManager = new BucketManager(auth, cfg); Response response = bucketManager.delete(QiNiuConfig.getBucket(), key); int retry = 0; while (response.needRetry() && retry++ < 3) { response = bucketManager.delete(QiNiuConfig.getBucket(), key); } return response.statusCode == 200 ? "删除成功!" : "删除失败!"; } @Override public void afterPropertiesSet() throws Exception { this.putPolicy = new StringMap(); putPolicy.put("insertOnly", 0); } /** * 获取上传凭证 */ private String getUploadToken() { return this.auth.uploadToken(QiNiuConfig.getBucket(), null, 3600, putPolicy); } } 有几个需要注意的点：在构造函数中，构造Configuration时，需要指定区域，因为我是华东区域的，因此使用的是Region.huadong()，如果使用的其他区域的，需要根据自己实际区域指定。在指定策略时，因为我一个用户只允许一个头像，因此上传时，如果存在我们直接覆盖的，所以在afterPropertiesSet方法中，设置上传策略时，直接指定的putPolicy.put("insertOnly", 0);，即如果存在就覆盖，如果不想覆盖，可以设置putPolicy.put("insertOnly", 1);，但是此时需要注意，如果上传重名文件，会返回异常。4.2.6、完善后用户上传头像方法用户上传头像后，更新用户实体（但是此时不更新数据库），将更新后的实体返回到前端，点击保存时，再更新数据库。 @Override public OilUser uploadAvatar(MultipartFile multipartFile, OilUser oilUser) throws IOException { String originalFilename = multipartFile.getOriginalFilename(); if (originalFilename == null || !originalFilename.contains(".")) { throw new CustomException("文件名不正确"); } String fileName = "avatar" + oilUser.getId() + originalFilename.substring(originalFilename.lastIndexOf(".")); String avatarUrl = qiNiuService.uploadFile(multipartFile.getInputStream(), fileName); oilUser.setAvatarUrl(avatarUrl); // LambdaUpdateWrapper<OilUser> userUpdateWrapper = new LambdaUpdateWrapper<>(); // userUpdateWrapper.set(OilUser::getAvatarUrl, oilUser.getAvatarUrl()); // userUpdateWrapper.eq(OilUser::getId, oilUser.getId()); // oilUserMapper.update(null, userUpdateWrapper); return oilUser; }4.2.7、用户保存方法分改造用户保存方法主要增加userUpdateWrapper.set(OilUser::getAvatarUrl, user.getAvatarUrl());,当用户有头像时，同步更新用户的头像信息。/** * 新增或保存用户 * * @param user 用户 * @return 结果 */ public OilUser saveUser(OilUser user) { if (user == null) { throw new CustomException("用户信息不能为空"); } if (user.getId() == null) { user.setId(""); } if (checkUserNameExist(user)) { throw new CustomException("用户名已存在"); } if (StringUtils.isEmpty(user.getId())) { user.setId(UUID.randomUUID().toString()); if (!StringUtils.isEmpty(user.getPassword())) { user.setPassword(passwordEncoder.encode(user.getPassword())); } oilUserMapper.insert(user); } else { LambdaUpdateWrapper<OilUser> userUpdateWrapper = new LambdaUpdateWrapper<>(); userUpdateWrapper.set(OilUser::getUsername, user.getUsername()); userUpdateWrapper.set(OilUser::getNickName, user.getNickName()); userUpdateWrapper.set(OilUser::getTel, user.getTel()); userUpdateWrapper.set(OilUser::getEmail, user.getEmail()); if (!StringUtils.isEmpty(user.getPassword())) { userUpdateWrapper.set(OilUser::getPassword, passwordEncoder.encode(user.getPassword())); } if (!StringUtils.isEmpty(user.getProvince())) { userUpdateWrapper.set(OilUser::getProvince, user.getProvince()); } if (!StringUtils.isEmpty(user.getCity())) { userUpdateWrapper.set(OilUser::getCity, user.getCity()); } if (!StringUtils.isEmpty(user.getAvatarUrl())) { userUpdateWrapper.set(OilUser::getAvatarUrl, user.getAvatarUrl()); } userUpdateWrapper.eq(OilUser::getId, user.getId()); oilUserMapper.update(null, userUpdateWrapper); user.setPassword(""); } return user; }五、效果微信用户登录后，如果没有上传过头像，会自动跳转到【个人信息】页面在个人信息上传头像后，自动跳转到首页。六、其他注意事项七牛云域名需要配置HTTPS小程序域名白名单uploadFile合法域名需要配置后台上传附件的域名。
在微信公众号开发之公众号基础配置一文中，我们介绍了如何对微信公众号进行基础配置。下面基于李森的博客的一个需求说明一下如何实现公众号用户的回复开发。需求描述用户给公众号发送消息时，我们查询博客的内容，然后回复给用户。代码实现其实用户回复的请求，跟微信公众号开发之公众号基础配置中配置的URL是一致的。区别在于我们在微信公众号开发之公众号基础配置中配置的请求是GET请求，用户回复的时候，微信会通过POST请求到后台。定义Controller相应微信请求POST请求定义其实没啥特别的，代码如下 @PostMapping("official") public void post(HttpServletRequest request, HttpServletResponse response) { try { request.setCharacterEncoding("UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } response.setCharacterEncoding("UTF-8"); // 调用核心业务类接收消息、处理消息 // String respMessage = weixinPost(request); String respMessage = messageService.newMessageRequest(request); // 响应消息 PrintWriter out = null; try { out = response.getWriter(); out.print(respMessage); } catch (IOException e) { e.printStackTrace(); logger.error(e.getMessage()); } finally { out.close(); out = null; } }封装实体封装消息基础实体BaseMessage.java/** * 微信自动回复消息封装 */ @Data public class BaseMessage { // 开发者微信号 private String ToUserName; // 发送方帐号（一个OpenID） private String FromUserName; // 消息创建时间 （整型） private long CreateTime; // 消息类型（text/image/location/link） private String MsgType; // 消息id，64位整型 private long MsgId; /** * 位0x0001被标志时，星标刚收到的消息 */ private int FuncFlag; }封装普通文本消息实体TextMessage.java@EqualsAndHashCode(callSuper = true) @Data public class TextMessage extends BaseMessage{ // 消息内容 private String Content; }封装图文消息实体Article.java@Data public class Article { /** * 图文消息描述 */ private String Description; /** * 图片链接，支持JPG、PNG格式， * 较好的效果为大图640*320，小图80*80 */ private String PicUrl; /** * 图文消息名称 */ private String Title; /** * 点击图文消息跳转链接 */ private String Url; }封装多条图文消息实体'NewsMessage.java'@EqualsAndHashCode(callSuper = true) @Data public class NewsMessage extends BaseMessage{ /** * 图文消息个数，限制为10条以内 */ private Integer ArticleCount; /** * 多条图文消息信息，默认第一个item为大图 */ private List<Article> Articles; }封装工具类我们需要将实体转换成xml结构，微信消息都是通过xml格式进行数据交互的。public class MessageUtil { /** * 返回消息类型：文本 */ public static final String RESP_MESSAGE_TYPE_TEXT = "text"; /** * 返回消息类型：音乐 */ public static final String RESP_MESSAGE_TYPE_MUSIC = "music"; /** * 返回消息类型：图文 */ public static final String RESP_MESSAGE_TYPE_NEWS = "news"; /** * 请求消息类型：文本 */ public static final String REQ_MESSAGE_TYPE_TEXT = "text"; /** * 请求消息类型：图片 */ public static final String REQ_MESSAGE_TYPE_IMAGE = "image"; /** * 请求消息类型：链接 */ public static final String REQ_MESSAGE_TYPE_LINK = "link"; /** * 请求消息类型：地理位置 */ public static final String REQ_MESSAGE_TYPE_LOCATION = "location"; /** * 请求消息类型：音频 */ public static final String REQ_MESSAGE_TYPE_VOICE = "voice"; /** * 请求消息类型：推送 */ public static final String REQ_MESSAGE_TYPE_EVENT = "event"; /** * 事件类型：subscribe(订阅) */ public static final String EVENT_TYPE_SUBSCRIBE = "subscribe"; /** * 事件类型：unsubscribe(取消订阅) */ public static final String EVENT_TYPE_UNSUBSCRIBE = "unsubscribe"; /** * 事件类型：CLICK(自定义菜单点击事件) */ public static final String EVENT_TYPE_CLICK = "CLICK"; /** * xml转换为map * * @param request * @return * @throws IOException */ public static Map<String, String> xmlToMap(HttpServletRequest request) throws IOException { Map<String, String> map = new HashMap<String, String>(); SAXReader reader = new SAXReader(); InputStream ins = null; try { ins = request.getInputStream(); } catch (IOException e1) { e1.printStackTrace(); } Document doc = null; try { doc = reader.read(ins); Element root = doc.getRootElement(); List<Element> list = root.elements(); for (Element e : list) { map.put(e.getName(), e.getText()); } return map; } catch (DocumentException e1) { e1.printStackTrace(); } finally { ins.close(); } return null; } /** * @param @param request * @param @return * @param @throws Exception * @Description: 解析微信发来的请求（XML） */ public static Map<String, String> parseXml(HttpServletRequest request) throws Exception { // 将解析结果存储在HashMap中 Map<String, String> map = new HashMap<String, String>(); // 从request中取得输入流 InputStream inputStream = request.getInputStream(); // 读取输入流 SAXReader reader = new SAXReader(); Document document = reader.read(inputStream); // 得到xml根元素 Element root = document.getRootElement(); // 得到根元素的所有子节点 List<Element> elementList = root.elements(); // 遍历所有子节点 for (Element e : elementList) map.put(e.getName(), e.getText()); // 释放资源 inputStream.close(); inputStream = null; return map; } // public static XStream xstream = new XStream(); /** * 文本消息对象转换成xml * * @param textMessage 文本消息对象 * @return xml */ public static String textMessageToXml(TextMessage textMessage) { // XStream xstream = new XStream(); xstream.alias("xml", textMessage.getClass()); return xstream.toXML(textMessage); } /** * @param @param newsMessage * @param @return * @Description: 图文消息对象转换成xml */ public static String newsMessageToXml(NewsMessage newsMessage) { xstream.alias("xml", newsMessage.getClass()); xstream.alias("item", new Article().getClass()); return xstream.toXML(newsMessage); } /** * 对象到xml的处理 */ private static XStream xstream = new XStream(new XppDriver() { public HierarchicalStreamWriter createWriter(Writer out) { return new PrettyPrintWriter(out) { // 对所有xml节点的转换都增加CDATA标记 boolean cdata = true; @SuppressWarnings("rawtypes") public void startNode(String name, Class clazz) { super.startNode(name, clazz); } protected void writeText(QuickWriter writer, String text) { if (cdata) { writer.write(" "); writer.write(text); writer.write(" "); } else { writer.write(text); } } }; } }); }封装服务层 /** * 微信公众号处理 * * @param request * @return */ @Override public String newMessageRequest(HttpServletRequest request) { String respMessage = null; try { // xml请求解析 Map<String, String> requestMap = MessageUtil.xmlToMap(request); // 发送方帐号（open_id） String fromUserName = requestMap.get("FromUserName"); // 公众帐号 String toUserName = requestMap.get("ToUserName"); // 消息类型 String msgType = requestMap.get("MsgType"); // 用户发送的消息消息内容 String content = requestMap.get("Content"); logger.info("FromUserName is:" + fromUserName + ", ToUserName is:" + toUserName + ", MsgType is:" + msgType + ",content:" + content); // 文本消息 if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) { logger.info("进入方法内"); QueryWrapper<TypechoContents> queryWrapper = new QueryWrapper<>(); queryWrapper.lambda() .like(TypechoContents::getTitle, content) .or() .like(TypechoContents::getText, content); List<TypechoContents> typechoContentsList = typechoContentsMapper.selectTypechoContentsList(content); logger.info("数据查询完成：" + typechoContentsList.size()); if (typechoContentsList.size() <= 0) { //自动回复 TextMessage text = new TextMessage(); String string = "未找到要查询的内容，请换个关键字试试"; text.setContent(string); text.setToUserName(fromUserName); text.setFromUserName(toUserName); text.setCreateTime(new Date().getTime()); text.setMsgType(msgType); respMessage = MessageUtil.textMessageToXml(text); return respMessage; } else if (typechoContentsList.size() > 1) { TextMessage text = new TextMessage(); StringBuilder stringBuilder = new StringBuilder("搜索到以下内容：\r\n"); for (TypechoContents typechoContents : typechoContentsList) { stringBuilder.append("").append(typechoContents.getTitle()).append(""); stringBuilder.append("\r\n"); } text.setContent(stringBuilder.toString()); text.setToUserName(fromUserName); text.setFromUserName(toUserName); text.setCreateTime(new Date().getTime()); text.setMsgType(msgType); respMessage = MessageUtil.textMessageToXml(text); return respMessage; } else { //一条回复图文消息 NewsMessage newsMessage = new NewsMessage(); newsMessage.setArticles(new ArrayList<>()); newsMessage.setArticleCount(typechoContentsList.size()); newsMessage.setToUserName(fromUserName); newsMessage.setFromUserName(toUserName); newsMessage.setCreateTime(new Date().getTime()); newsMessage.setMsgType("news"); for (TypechoContents typechoContents : typechoContentsList) { Article article = new Article(); article.setUrl("https://lisen.cc/" + typechoContents.getCategory() + "/" + typechoContents.getSlug() + ".html"); article.setDescription(typechoContents.getTitle()); List<String> imageUrlList = MessageUtil.getMatchString(typechoContents.getText()); if (imageUrlList.size() <= 0) { article.setPicUrl("https://lisen.cc/usr/themes/lisen/assets/img/logo.png"); } else { logger.info(imageUrlList.get(0)); article.setPicUrl(imageUrlList.get(0)); } article.setTitle(typechoContents.getTitle()); newsMessage.getArticles().add(article); } respMessage = MessageUtil.newsMessageToXml(newsMessage); return respMessage; } } // 事件推送 else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_EVENT)) { String eventType = requestMap.get("Event");// 事件类型 // 订阅 if (eventType.equals(MessageUtil.EVENT_TYPE_SUBSCRIBE)) { //文本消息 TextMessage text = new TextMessage(); logger.info(LiSenConfig.getResp()); text.setContent(LiSenConfig.getResp().replace(" ","\n")); text.setToUserName(fromUserName); text.setFromUserName(toUserName); text.setCreateTime(new Date().getTime()); text.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT); respMessage = MessageUtil.textMessageToXml(text); return respMessage; } // 取消订阅后用户再收不到公众号发送的消息，因此不需要回复消息 else if (eventType.equals(MessageUtil.EVENT_TYPE_UNSUBSCRIBE)) {// 取消订阅 } } } catch (Exception e) { logger.error("error......"); } return respMessage; }
/** * Description: 微信小程序工具类 * * @author : laughing * DateTime: 2021-03-16 11:10 */ @Slf4j public class WeChatUtils { public static String httpRequest(String requestUrl, String requestMethod, String output) { try { URL url = new URL(requestUrl); HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); connection.setDoOutput(true); connection.setDoInput(true); connection.setUseCaches(false); connection.setRequestMethod(requestMethod); if (null != output) { OutputStream outputStream = connection.getOutputStream(); outputStream.write(output.getBytes(StandardCharsets.UTF_8)); outputStream.close(); } // 从输入流读取返回内容 InputStream inputStream = connection.getInputStream(); InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String str; StringBuilder buffer = new StringBuilder(); while ((str = bufferedReader.readLine()) != null) { buffer.append(str); } bufferedReader.close(); inputStreamReader.close(); inputStream.close(); connection.disconnect(); return buffer.toString(); } catch (Exception e) { e.printStackTrace(); } return ""; } public static String decryptData(String encryptDataB64, String sessionKeyB64, String ivB64) { log.info("encryptDataB64:" + encryptDataB64); log.info("sessionKeyB64:" + sessionKeyB64); log.info("ivB64:" + ivB64); return new String( decryptOfDiyIv( Base64.decode(encryptDataB64), Base64.decode(sessionKeyB64), Base64.decode(ivB64) ) ); } private static final String KEY_ALGORITHM = "AES"; private static final String ALGORITHM_STR = "AES/CBC/PKCS7Padding"; private static Key key; private static Cipher cipher; private static void init(byte[] keyBytes) { // 如果密钥不足16位，那么就补足. 这个if 中的内容很重要 int base = 16; if (keyBytes.length % base != 0) { int groups = keyBytes.length / base + 1; byte[] temp = new byte[groups * base]; Arrays.fill(temp, (byte) 0); System.arraycopy(keyBytes, 0, temp, 0, keyBytes.length); keyBytes = temp; } // 初始化 Security.addProvider(new BouncyCastleProvider()); // 转化成JAVA的密钥格式 key = new SecretKeySpec(keyBytes, KEY_ALGORITHM); try { // 初始化cipher cipher = Cipher.getInstance(ALGORITHM_STR, "BC"); } catch (Exception e) { e.printStackTrace(); } } /** * 解密方法 * * @param encryptedData 要解密的字符串 * @param keyBytes 解密密钥 * @param ivs 自定义对称解密算法初始向量 iv * @return 解密后的字节数组 */ private static byte[] decryptOfDiyIv(byte[] encryptedData, byte[] keyBytes, byte[] ivs) { byte[] encryptedText = null; init(keyBytes); try { cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(ivs)); encryptedText = cipher.doFinal(encryptedData); } catch (Exception e) { e.printStackTrace(); } return encryptedText; } /** * 向指定 URL 发送POST方法的请求 * * @param url 发送请求的 URL * @param json 请求参数，请求参数应该是 json 的形式。 * @return 所代表远程资源的响应结果 */ public static String httpPost(String url, JSONObject json) { PrintWriter out = null; BufferedReader in = null; String result = ""; try { URL realUrl = new URL(url); // 打开和URL之间的连接 URLConnection conn = realUrl.openConnection(); // 设置通用的请求属性 conn.setRequestProperty("accept", "*/*"); conn.setRequestProperty("connection", "Keep-Alive"); conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); // 发送POST请求必须设置如下两行 conn.setDoOutput(true); conn.setDoInput(true); // 获取URLConnection对象对应的输出流 out = new PrintWriter(conn.getOutputStream()); // 发送请求参数 out.print(json); // flush输出流的缓冲 out.flush(); // 定义BufferedReader输入流来读取URL的响应 in = new BufferedReader( new InputStreamReader(conn.getInputStream())); String line; while ((line = in.readLine()) != null) { result=result.concat(line); } } catch (Exception e) { System.out.println("发送 POST 请求出现异常！" + e); e.printStackTrace(); } //使用finally块来关闭输出流、输入流 finally { try { if (out != null) { out.close(); } if (in != null) { in.close(); } } catch (IOException ex) { ex.printStackTrace(); } } return result; }
关于微信小程序使用wxParse报错： thirdScriptError console.dir is not a function
最近在开发小程序，有一个详细展示的页面，为了展示html代码，调用了wxParse的代码，当时在模拟器上面模拟是没有问题的，但是在我安卓手机（坚果Pro2）上面进行调试时，文章展示html页面，始终是一片空白。当时感觉不对劲，然后拿着我老婆的苹果手机试了一下，没有任何问题。此时，果断打开手机的调试功能，看到错误信息thirdScriptError console.dir is not a function。百度了一下，终于找到了解决办法。解决办法把wxParse插件下的html2json.js里的console.dir注释掉或者替换成console.log。