SpringBoot项目生成二维码并统计扫描次数
二维码是什么
二维码又称二维条码,常见的二维码为QR Code,QR全称Quick Response,是一个近几年来移动设备上超流行的一种编码方式,它比传统的Bar Code条形码能存更多的信息,也能表示更多的数据类型。
二维条码/二维码(2-dimensional bar code)是用某种特定的几何图形按一定规律在平面(二维方向上)分布的、黑白相间的、记录数据符号信息的图形;在代码编制上巧妙地利用构成计算机内部逻辑基础的“0”、“1”比特流的概念,使用若干个与二进制相对应的几何形体来表示文字数值信息,通过图象输入设备或光电扫描设备自动识读以实现信息自动处理:它具有条码技术的一些共性:每种码制有其特定的字符集;每个字符占有一定的宽度;具有一定的校验功能等。同时还具有对不同行的信息自动识别功能、及处理图形旋转变化点。
通俗的来说,二维码是将携带的内容进行编码形成图形,使用二维码识别软件进行扫描即可识别其携带的内容。
代码实现
一 引入依赖
google提供了生成二维码的实现类,maven依赖如下
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.0</version>
</dependency>
二 生成二维码的工具类
package com.cube.share.qrcode.utils;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
/**
* @author cube.li
* @date 2021/3/28 14:02
* @description 二维码生成工具类
*/
public class QRCodeUtil {
private static final String DEFAULT_CHARSET = "utf-8";
/**
* 二维码默认空白区域大小
*/
private static final Integer DEFAULT_MARGIN = 1;
/**
* 生成二维码图片
*
* @param content 二维码内容
* @param width 二维码宽度
* @param height 二维码高度
* @param margin 二维码空白区域大小
* @return BufferedImage
* @throws Exception 异常
*/
public static BufferedImage createQrCodeImage(String content, Integer width, Integer height, Integer margin) throws Exception {
if (width < 0 || height < 0) {
throw new Exception("The width and height of the QR code must be greater than zero");
}
//二维码属性设置
Map<EncodeHintType, Object> hints = new HashMap<>(4);
//纠错等级
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
hints.put(EncodeHintType.CHARACTER_SET, DEFAULT_CHARSET);
//设置二维码空白区域大小
hints.put(EncodeHintType.MARGIN, Optional.of(margin).orElse(DEFAULT_MARGIN));
//生成比特矩阵
BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints);
//构造BufferedImage
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
//填充图片像素值
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);
}
}
return image;
}
public static BufferedImage createQrCodeImage(String content, Integer width, Integer height) throws Exception {
return createQrCodeImage(content, width, height, DEFAULT_MARGIN);
}
}
三 MVC
本文重点在与介绍二维码的生成的扫描次数统计,就不引入数据库里,数据直接放在内存中。
- 模拟数据
package com.cube.share.qrcode.mock;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
/**
* @author cube.li
* @date 2021/3/28 14:53
* @description 模拟数据
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MockData implements Serializable {
private static final long serialVersionUID = -5736958265069712442L;
private String id;
@NotBlank(message = "内容不可为空")
private String content;
/**
* 记录此条数据生成二维码后被扫描的次数
*/
private Integer count;
}
- Service
package com.cube.share.qrcode.service;
import com.cube.share.qrcode.mock.MockData;
import com.cube.share.qrcode.utils.IdGenerator;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author cube.li
* @date 2021/3/28 14:57
* @description
*/
@Service
public class MockDataService {
/**
* 存放模拟数据,实际应当存放在数据库中
*/
private static Map<String, MockData> mockDataMap = new ConcurrentHashMap<>();
static {
String id = IdGenerator.generatorStringId();
mockDataMap.put(id, MockData.builder().id(id).content("欢迎参加挥泪大甩卖活动").count(0).build());
String iD = IdGenerator.generatorStringId();
mockDataMap.put(iD, MockData.builder().id(iD).content("这里是二维码的内容").count(0).build());
}
public void save(MockData data) {
String id = IdGenerator.generatorStringId();
data.setId(id);
mockDataMap.put(id, data);
}
public MockData get(String id) {
return mockDataMap.get(id);
}
public void updateScanCount(String id) {
MockData existData = mockDataMap.get(id);
mockDataMap.put(id, MockData.builder().id(id)
.content(existData.getContent())
.count(existData.getCount() + 1).build());
}
}
在内存中直接存放了一条数据,代表与业务场景有关的内容。
- Controller
package com.cube.share.qrcode.controller;
import com.cube.share.base.templates.ApiResult;
import com.cube.share.base.utils.IAssert;
import com.cube.share.qrcode.mock.MockData;
import com.cube.share.qrcode.service.MockDataService;
import com.cube.share.qrcode.utils.QRCodeUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.awt.image.BufferedImage;
/**
* @author cube.li
* @date 2021/3/28 15:02
* @description
*/
@RestController
@RequestMapping("/data")
@Slf4j
public class MockController {
@Resource
private MockDataService dataService;
@Resource
private HttpServletResponse response;
private static final String QR_CODE_SCAN_URL = "localhost:8080/data/scan?id=";
@Value("${cube.qr-code-scan-url}")
private String qrCodeScanUrl;
@PostMapping("/save")
public ApiResult save(@RequestBody @Valid MockData data) {
dataService.save(data);
return ApiResult.success();
}
@GetMapping("/qrCode")
public void qrCode(@RequestParam("id") String id) throws Exception {
MockData data = dataService.get(id);
IAssert.notNull(data, "未查找到对应数据");
//将二维码图片响应到前端
//BufferedImage image = QRCodeUtil.createQrCodeImage(qrCodeScanUrl + id, 256, 256);
BufferedImage image = QRCodeUtil.createQrCodeImage(data.getContent(), 256, 256);
ImageIO.write(image, "png", response.getOutputStream());
}
@RequestMapping("scan")
public ApiResult qrScanCount(@RequestParam("id") String id) {
MockData data = dataService.get(id);
IAssert.notNull(data, "未查找到对应数据");
log.info("活动id = {},活动二维码扫描次数 = {}", id, data.getCount());
dataService.updateScanCount(id);
return ApiResult.success(data);
}
}
演示
1. 二维码生成
项目启动时看下控制台打印日志,可以找到两条数据的主键id
2021-03-28 16:51:15.865 INFO 13184 --- [ main] com.cube.share.qrcode.utils.IdGenerator : generate String Id, id = 6a26de75e37441a587bbf408a7222498
2021-03-28 16:51:15.866 INFO 13184 --- [ main] com.cube.share.qrcode.utils.IdGenerator : generate String Id, id = b4ef702939424149aca5f703d06efee1
访问二维码查看接口,生成的二维码会直接在浏览器展示。
生成二维码.png
通过手机扫描此二维码,可以直接得到二维码的内容
文字内容.jpg
如果此二维码的内容是一个url链接,则扫描后会直接跳转至此链接。
先保存一条内容是有道词典网页链接的模拟数据
保存二维码内容.png
在浏览器中查看刚插入模拟数据内容的二维码
扫描二维码跳转.png
手机扫描后跳转界面
url.jpg
由此,我们知道了扫描二维码后会显示二维码的内容,如果二维码的内容是一个url链接,则会直接跳转到该页面,这里的url链接跳转是我们实现二维码扫描次数的关键。
2. 统计二维码的扫描次数
假设我们的业务场景里需要创建一个大甩卖活动,活动创建后可以将该条活动通过二维码分享。活动参与者通过扫描二维码可以直接进入到我们的活动页面。
借助这个具体的业务场景我们梳理下整个过程:
- 在前端创建活动,创建完成后在后端生成此次活动的具体信息。
- 前端展示/下载二维码,这里的二维码内容应该是此条活动详情的url链接,这个url链接中应该包含具体的参数(活动id),用户扫描后会直接跳转至这个链接展示活动的具体信息。
- 用户扫描跳转页面后实际也就是向后端发送一次请求,在这次请求中后端即可进行扫描次数的统计与更新。
再重点看下Controller里的这两个方法:
@GetMapping("/qrCode")
public void qrCode(@RequestParam("id") String id) throws Exception {
MockData data = dataService.get(id);
IAssert.notNull(data, "未查找到对应数据");
//将二维码图片响应到前端
BufferedImage image = QRCodeUtil.createQrCodeImage(qrCodeScanUrl + id, 256, 256);
//BufferedImage image = QRCodeUtil.createQrCodeImage(data.getContent(), 256, 256);
ImageIO.write(image, "png", response.getOutputStream());
}
@RequestMapping("scan")
public ApiResult qrScanCount(@RequestParam("id") String id) {
MockData data = dataService.get(id);
IAssert.notNull(data, "未查找到对应数据");
log.info("活动id = {},活动二维码扫描次数 = {}", id, data.getCount());
dataService.updateScanCount(id);
return ApiResult.success(data);
}
将二维码响应到前端的代码注释掉,使用这一行BufferedImage image = QRCodeUtil.createQrCodeImage(qrCodeScanUrl + id, 256, 256);
,这里的qrCodeScanUrl
是我配置的扫描二维码后的跳转地址:
cube:
qr-code-scan-url: http://litb.natapp1.cc/data/scan?id=
我这里采用了内网穿透工具将http://litb.natapp1.cc
域名映射到本地的8080端口,是为了能够生成在公网上访问的url,扫描二维码后会直接进入到我本地的 qrScanCount(@RequestParam("id") String id)
方法,扫描二维码的跳转链接是http://litb.natapp1.cc/data/scan?id=5c8aa821b23e4fcba299c8a5ef2a0221
,配置了内网穿透后,这次跳转会向我本地发起一次请求。
重启项目在控制台查看模拟数据的id并利用其查看二维码,使用手机扫描在控制台可以看到其如下信息:
2021-03-28 17:24:16.360 INFO 2920 --- [nio-8080-exec-5] c.c.s.qrcode.controller.MockController : 活动id = 5c8aa821b23e4fcba299c8a5ef2a0221,活动二维码扫描次数 = 1
2021-03-28 17:24:21.112 INFO 2920 --- [nio-8080-exec-8] c.c.s.qrcode.controller.MockController : 活动id = 5c8aa821b23e4fcba299c8a5ef2a0221,活动二维码扫描次数 = 2
2021-03-28 17:24:26.635 INFO 2920 --- [nio-8080-exec-1] c.c.s.qrcode.controller.MockController : 活动id = 5c8aa821b23e4fcba299c8a5ef2a0221,活动二维码扫描次数 = 3
可以看出每次扫描二维码都会对我本地进行一次请求。
手机上则展示如下信息:
{"code":200,"msg":null,"data":{"id":"5c8aa821b23e4fcba299c8a5ef2a0221","content":"欢迎参加挥泪大甩卖活动","count":3}}
可以看出已经成功的获取了此次活动的内容,这里因为是没有前端渲染所以直接展示了响应数据。在实际开发中扫描二维码后应该直接跳转到前端的某一个页面,前端利用请求中的参数再向后端发出请求后得到数据进行页面渲染后进行展示。