【业务场景】
在小程序做真机测试的时候,面临了很尴尬的问题。就是双击提交按钮后,数据居然重复提交了。
【想当然地尝试】
作为一个后端程序猿,首先想到是通过后端解决问题啦。
根据抄袭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)亲测有用!在两秒内只能提交一次!!!
咳咳,我不是程序猿,我只是代码的搬运工(狗头)
共同学习,写下你的评论
评论加载中...
作者其他优质文章












