【业务场景】
在小程序做真机测试的时候,面临了很尴尬的问题。就是双击提交按钮后,数据居然重复提交了。
【想当然地尝试】
作为一个后端程序猿,首先想到是通过后端解决问题啦。
根据抄袭cdsn大神的思路,我的思路基本是这样的:
1.第一次请求会根据ip地址+请求路径,在redis服务器上面留下key=ipAdress+路径,value等于对这个key在redis使用increment函数的count值。也就是说第一次value从零自增到1。
2.第二次访问会根据ip+请求路径访问同理,但每次访问都会判断count值。count值大于1就不是第一次访问了。
结果是即使双击提交也能够成功拦截下来的:
【结尾(假的)】
通过这个功能的开发,我深刻地......咳咳.....可能有的大佬看到这里已经开始吐痰了。
因为这么做......可能功能的健壮性不够,万一ip地址获取出问题整个程序也跑不下去。
【真正的思路】
那么,这么简单的程序怎么实现呢?由我,慕课网第一C(opy)V(iscidity)侠带大家来捋一捋。
========== 无齿的分割线 ==========
【实现小程序获取token】
1. 我需要在mysql有一张表,专门保存用过我这个小程序的用户的简单信息。不然什么妖艳贱货都来获取我的token。
2. 我们先强迫每一个用户在使用我这个小程序的第一刻必须登陆
-- 小程序登陆
onLoad: function() { // 获取当前位置 var me = this; me.reverseGeocoderSetLocation(); var openid = common.getOpenid(); },
common.js
var config = require('../libs/config.js'); function getOpenid() { var openid = wx.getStorageSync('openid'); if (openid == '' || openid == null || openid == undefined) { wx.login({ success: function (res) { var code = res.code; wx.request({ url: config.Config.serverUrl + '/wechat/code2session', method: "POST", data: { code: code }, header: { "content-type": "application/x-www-form-urlencoded" }, success: function (res) { var data = res.data; if (data.data != undefined && data.data.openid != undefined) { wx.setStorageSync('openid', data.data.openid); console.log('openid:' + data.data.openid +'保存到缓存'); return data.data.openid; } } }); } }) } else { return openid; } } module.exports = { getOpenid: getOpenid }
-- 后端微信登陆接口,在登陆成功且用户信息不存在时创建用户
@RestController @Api(value = "微信相关业务的接口", tags = {"微信相关业务的controller"}) @RequestMapping("/wechat") public class WechatController { @Autowired private WxMaService wxMaService; @Autowired private UserService userService; @ApiOperation(value = "通过code获取登陆信息", notes = "通过code获取登陆信息的接口") @PostMapping(value = "/code2session") public IMoocJSONResult getOpenIdByCode(String code) { if(StringUtils.isBlank(code)) { return IMoocJSONResult.errorMsg("code不能为空"); } WxMaJscode2SessionResult result = null; try { result = wxMaService.jsCode2SessionInfo(code); //检查用户是否存在,不存在则新增用户。 if(StringUtils.isNotBlank(result.getOpenid()) && !userService.queryOpenidIsExist(result.getOpenid())) { User user = new User(); user.setOpenid(result.getOpenid()); user.setCreateTime(new Date()); userService.saveUser(user); } } catch (Exception e) { return IMoocJSONResult.errorMsg(e.getMessage()); } return IMoocJSONResult.ok(result); } }
运用了StringUtils和WxMaService两个工具类,分别来自以下maven库
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.4</version> </dependency>
<dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-miniapp</artifactId> <version>3.3.0</version> </dependency>
3. 通过openid与数据库校验,才发放token(令牌)
@RestController @Api(value = "令牌相关业务的接口", tags = {"令牌相关业务的controller"}) @RequestMapping("/token") public class TokenController { @Autowired private UserService userService; @Autowired private RedisTemplate<String, String> redisTemplate; private final String API_TOKEN = "api_token"; @ApiOperation(value = "获取令牌", notes = "根据openid校对数据库,获取令牌") @PostMapping(value = "/openid") public IMoocJSONResult getTokenByOpenId(String openid) { //检查用户是否存在,和openid是否为空 if(StringUtils.isNotBlank(openid) && userService.queryOpenidIsExist(openid)) { String key = API_TOKEN + ":" + openid + ":" + UUID.randomUUID().toString(); redisTemplate.opsForValue().set(key, "1"); redisTemplate.expire(key, 30 * 60 * 1000, TimeUnit.MILLISECONDS); return IMoocJSONResult.ok(key); } return IMoocJSONResult.errorMsg("获取令牌出错"); } }
在这里引用了redisTemplate大概跟以下两个maven库有关
当然看到了<parent></parent>就知道,我的后端应用框架是springboot
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>1.5.2.RELEASE</version> </dependency>
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.6.RELEASE</version> <relativePath/> </parent>
【实现提交数据令牌校验】
以下操作大量思路来自csdn各位大佬,勿喷。
1. 声明一个自定义注解
package com.imooc.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.METHOD, ElementType.TYPE}) // 作用到类,方法,接口上等 @Retention(RetentionPolicy.RUNTIME) // 在运行时可以获取 public @interface ApiToken { //前后端传递这个api_token的名字 String name() default "API_TOKEN"; }
2. 定义一个切面(AOP)类,对被注解的类(或者方法)作出处理
package com.imooc.aspect; import com.imooc.annotation.ApiToken; import com.imooc.utils.IMoocJSONResult; import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; @Aspect @Component public class ApiTokenContract { @Autowired private RedisTemplate<String, String> redisTemplate; @Around("@annotation(com.imooc.annotation.ApiToken)" ) public Object around(ProceedingJoinPoint point) throws Throwable { try { Object result = null; //通过SpringBoot提供的RequestContextHolder获得request ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); HttpSession session = request.getSession(); if(request.getMethod().equalsIgnoreCase("get")){ //方法为get //TODO get方法留作请求html时,分配token给页面 } else { //方法为post //获取自定义注解里的值 MethodSignature signature = (MethodSignature) point.getSignature(); ApiToken annotation = signature.getMethod().getAnnotation(ApiToken.class); //从request中取出提交时,携带的token String submitToken = request.getParameter(annotation.name()); if (StringUtils.isNotBlank(submitToken)) { long count = del(submitToken); if (count == 1) { //说明redis数据库里有这个key(token) //执行方法 result = point.proceed(); } else { return IMoocJSONResult.errorMsg( "令牌不正确"); } } else { return IMoocJSONResult.errorMsg( "提交数据请使用令牌"); } } return result; } catch (Exception e) { e.printStackTrace(); return IMoocJSONResult.errorMsg( "执行防止重复提交功能AOP失败,原因:" + e.getMessage()); } } /** * 拓展redisTemplate的删除方法 * 根据redis的特性:redis直接删除某个key,key存在则返回1,不存在返回0 * @param keys */ private long del(final String... keys) { return redisTemplate.execute(new RedisCallback<Long>() { public Long doInRedis(RedisConnection connection) throws DataAccessException { long result = 0; for (int i = 0; i < keys.length; i++) { result = connection.del(keys[i].getBytes()); } return result; } }); } }
3. 使用自定义的注解
在小程序传递参数,在访问接口前事先获取了token
在后端对应接口出使用自定义注解(@ApiToken),因为没有给name传值,所以参数名使用默认的API_TOKEN
【最终测试结果】
疯狂点击提交按钮,结果提交了三次,但只有一次是让数据保存了
【写在最后】
到了这里,终于.....你们以为这样就完了吗?
咳咳......
中国的程序猿永不认输!
当然了,由于产品经理对于我水前端十分不满,给我找了一个帖子。最后给大家贡献以下,前端是怎么样避免重复点击的,原理是完全不懂(CV侠在此!)。
不屑于看我怎么CV的老哥直接看以下帖子:https://www.jianshu.com/p/52ec7ede1200
在commons.js定义一个防止短时间内反复调用一个方法的方法(防抖)
function throttle(fn, gapTime) { if (gapTime == null || gapTime == undefined) { gapTime = 1500 } let _lastTime = null // 返回新的函数 return function () { let _nowTime = + new Date() if (_nowTime - _lastTime > gapTime || !_lastTime) { fn.apply(this, arguments) //将this和参数传给原函数 _lastTime = _nowTime } } }
并且exports这个防抖的方法
module.exports = { getOpenid: getOpenid, throttle: throttle }
最后在小程序声明方法时,把方法嵌套进这个防抖的方法
bindSend: common.throttle(function() { console.log('提交'); }, 2000)
亲测有用!在两秒内只能提交一次!!!
咳咳,我不是程序猿,我只是代码的搬运工(狗头)
共同学习,写下你的评论
评论加载中...
作者其他优质文章