0%

扫描二维码后立即通知实现

前言

最近在做微信的扫描支付的时候,遇到一个问题:如何在用户扫码支付完成之后,客户端立即得到通知,进行下一步的跳转?

解决方案

首先想到的策略是客户端轮循查询订单的状态,根据返回的结果进行跳转

这个方案有明显的缺点,轮循时间设置短,频繁发送请求,对服务器以及数据库都会产生压力;轮循时间过长,用户等待时间长,体验很差;

针对这个问题想到了微信网页版的扫码登录(扫描完成后,立即登录),现研究一下它的原理并实现相同的功能

微信扫描登录原理

可以看到图片中,前端二维码页面发送一个网络请求,但是这个请求并没有立即返回

一段时间没有扫描后,后端返回408,前端重新发起一个相同的网络请求,并继续挂起 ( pending )

据此猜测大概实现原理如下:

  1. 进入网站后生成一个 ( 比如UUID )
  2. 跳转到二维码页面 ( 二维码中的链接包含此UUID )
  3. 二维码页面向服务器发起请求,查询二维码是否被扫登录
  4. 服务器收到请求后查询,如果未扫登录,进入等待( wait ),不立即返回
  5. 一旦被扫,立即返回 ( notify )
  6. 页面收到结果,做后续处理
UUID 缓存
1
public static Map<String, ScanPool> cacheMap = new ConcurrentHashMap<String, ScanPool>();

一定要使用 ConcurrentHashMap 否则多线程操作会报错 ConcurrentModificationException

单线程中出现该异常的原因是,对一个集合遍历的同时,又对该集合进行了增删的操作

多线程中更易出现该异常,当你在一个线程中对一数据集合进行遍历,正赶上另外一个线程对该数据集合进行增删操作时便会出现该异常

缓存还要设置自动清理功能,防止增长过大

生成二维码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@RequestMapping("/qrcode/{uuid}")
@ResponseBody
public void createQRCode(@PathVariable String uuid, HttpServletResponse response) {
String text = "http://222.186.174.121:41408/login/" + uuid;
int width = 300;
int height = 300;
String format = "png";
//将UUID放入缓存
ScanPool pool = new ScanPool();
PoolCache.cacheMap.put(uuid, pool);
System.out.println("UUID放入缓存 成功");
try {
Map<EncodeHintType, Object> hints = new HashMap<EncodeHintType, Object>();
hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
//hints.put(EncodeHintType.MARGIN, 1);
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); //容错率
BitMatrix bitMatrix = new MultiFormatWriter().encode(text, BarcodeFormat.QR_CODE, width, height, hints);
MatrixToImageWriter.writeToStream(bitMatrix, format, response.getOutputStream());
} catch (WriterException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

System.out.println("根据UUID生成二维码 成功");
}

生成二维码,并将 UUID放入缓存中

此处需要注意,二维码 url 必须是外网可以访问地址,此处可以使用内网穿透工具

验证是否登录

前端发起请求,验证该二维码是否已被扫登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RequestMapping("/pool")
@ResponseBody
String pool(String uuid) {
System.out.println("检测[" + uuid + "]是否登录");

ScanPool pool = PoolCache.cacheMap.get(uuid);

if (pool == null) {
return "timeout";
}

//使用计时器,固定时间后不再等待扫描结果--防止页面访问超时
new Thread(new ScanCounter(pool)).start();

boolean scanFlag = pool.getScanStatus();

if (scanFlag) {
return "success";
} else {
return "fail";
}
}

获得状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public synchronized boolean getScanStatus() {
try {
if (!isScan()) { //如果还未扫描,则等待
this.wait();
}
if (isScan()) {
return true;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}

public synchronized void notifyPool() {
try {
this.notifyAll();
} catch (Exception e) {
e.printStackTrace();
}
}

新开线程防止页面访问超时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ScanCounter implements Runnable {

public Long timeout = 27000L;

//传入的对象
private ScanPool scanPool;

public ScanCounter(ScanPool scanPool) {
this.scanPool = scanPool;
}

public void run() {
try {
Thread.sleep(timeout);
} catch (InterruptedException e) {
e.printStackTrace();
}
notifyPool(scanPool);
}

public synchronized void notifyPool(ScanPool scanPool) {
scanPool.notifyPool();
}
}

扫码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RequestMapping("/login/{uuid}")
@ResponseBody
String login(@PathVariable String uuid) {

ScanPool pool = PoolCache.cacheMap.get(uuid);

if (pool == null) {
return "timeout,scan fail";
}

pool.scanSuccess();

System.out.println("扫码完成,登录成功");

return "扫码完成,登录成功";
}

扫码成功,设置扫码状态,唤起线程

1
2
3
4
5
6
7
8
public synchronized void scanSuccess() {
try {
setScan(true);
this.notifyAll();
} catch (Exception e) {
e.printStackTrace();
}
}

手机扫码后

-------------The End-------------
坚持原创技术分享,您的支持将鼓励我继续创作!