ava后台实现微信小程序不同人员生成不同小程序码并追踪扫码来源,实现推广
下面我将详细介绍如何使用Java后台实现这一功能。
一、整体架构设计
- 前端:微信小程序
- 后端:Java (Spring Boot)
- 数据库:MySQL/其他
- 微信接口:调用微信小程序码生成API
二、数据库设计
1. 推广人员表(promoter)
CREATE TABLE `promoter` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL COMMENT '推广人员姓名',
`mobile` varchar(20) COMMENT '联系电话',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2. 用户-推广关系表(user_promoter_relation)
CREATE TABLE `user_promoter_relation` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` varchar(64) NOT NULL COMMENT '小程序用户openid',
`promoter_id` bigint NOT NULL COMMENT '推广人员ID',
`first_scan_time` datetime NOT NULL COMMENT '首次扫码时间',
`last_scan_time` datetime NOT NULL COMMENT '最近扫码时间',
`scan_count` int NOT NULL DEFAULT '1' COMMENT '扫码次数',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_user_promoter` (`user_id`,`promoter_id`),
KEY `idx_promoter` (`promoter_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
三、Java后端实现
1. 添加微信小程序Java SDK依赖
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-miniapp</artifactId>
<version>4.1.0</version>
</dependency>
2. 配置微信小程序参数
@Configuration
public class WxMaConfiguration {
@Value("${wx.miniapp.appid}")
private String appid;
@Value("${wx.miniapp.secret}")
private String secret;
@Bean
public WxMaService wxMaService() {
WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
config.setAppid(appid);
config.setSecret(secret);
WxMaService service = new WxMaServiceImpl();
service.setWxMaConfig(config);
return service;
}
}
3. 生成带参数的小程序码
@RestController
@RequestMapping("/api/qrcode")
public class QrCodeController {
@Autowired
private WxMaService wxMaService;
@Autowired
private PromoterService promoterService;
/**
* 生成推广二维码
* @param promoterId 推广人员ID
* @return 二维码图片字节流
*/
@GetMapping("/generate")
public void generatePromoterQrCode(@RequestParam Long promoterId,
HttpServletResponse response) throws IOException {
// 验证推广人员是否存在
Promoter promoter = promoterService.getById(promoterId);
if (promoter == null) {
throw new RuntimeException("推广人员不存在");
}
// 生成小程序码
String scene = "promoterId=" + promoterId;
WxMaQrcodeService qrcodeService = wxMaService.getQrcodeService();
File qrCodeFile = qrcodeService.createWxaCodeUnlimit(scene, "pages/index/index", 430, true, null, false);
// 返回图片流
response.setContentType("image/jpeg");
try (InputStream in = new FileInputStream(qrCodeFile);
OutputStream out = response.getOutputStream()) {
byte[] buffer = new byte[1024];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
}
}
}
4. 处理扫码进入事件
@RestController
@RequestMapping("/api/track")
public class TrackController {
@Autowired
private UserPromoterRelationService relationService;
/**
* 记录用户扫码行为
* @param dto 包含用户信息和推广信息
* @return 操作结果
*/
@PostMapping("/scan")
public Result trackScan(@RequestBody ScanTrackDTO dto) {
// 解析scene参数
String scene = dto.getScene();
Map<String, String> sceneParams = parseScene(scene);
String promoterIdStr = sceneParams.get("promoterId");
if (StringUtils.isBlank(promoterIdStr)) {
return Result.fail("缺少推广人员参数");
}
try {
Long promoterId = Long.parseLong(promoterIdStr);
relationService.recordUserScan(dto.getOpenid(), promoterId);
return Result.success();
} catch (NumberFormatException e) {
return Result.fail("推广人员参数格式错误");
}
}
private Map<String, String> parseScene(String scene) {
Map<String, String> params = new HashMap<>();
if (StringUtils.isBlank(scene)) {
return params;
}
String[] pairs = scene.split("&");
for (String pair : pairs) {
String[] kv = pair.split("=");
if (kv.length == 2) {
params.put(kv[0], kv[1]);
}
}
return params;
}
}
5. 用户-推广关系服务
@Service
public class UserPromoterRelationServiceImpl implements UserPromoterRelationService {
@Autowired
private UserPromoterRelationMapper relationMapper;
@Override
@Transactional
public void recordUserScan(String openid, Long promoterId) {
// 查询是否已有记录
UserPromoterRelation relation = relationMapper.selectByUserAndPromoter(openid, promoterId);
Date now = new Date();
if (relation == null) {
// 新建关系记录
relation = new UserPromoterRelation();
relation.setUserId(openid);
relation.setPromoterId(promoterId);
relation.setFirstScanTime(now);
relation.setLastScanTime(now);
relation.setScanCount(1);
relationMapper.insert(relation);
} else {
// 更新已有记录
relation.setLastScanTime(now);
relation.setScanCount(relation.getScanCount() + 1);
relationMapper.updateById(relation);
}
}
}
四、小程序前端处理
在小程序的app.js中处理扫码进入的场景:
App({
onLaunch: function(options) {
// 处理扫码进入的情况
if (options.scene === 1047 || options.scene === 1048 || options.scene === 1049) {
// 这些scene值表示是通过扫码进入
const scene = decodeURIComponent(options.query.scene);
// 上报扫码信息到后端
wx.request({
url: 'https://yourdomain.com/api/track/scan',
method: 'POST',
data: {
scene: scene,
openid: this.globalData.openid // 需要先获取用户openid
},
success: function(res) {
console.log('扫码记录成功', res);
}
});
}
}
})
五、数据统计接口实现
@RestController
@RequestMapping("/api/stat")
public class StatController {
@Autowired
private UserPromoterRelationMapper relationMapper;
/**
* 获取推广人员业绩统计
* @param promoterId 推广人员ID
* @param startDate 开始日期
* @param endDate 结束日期
* @return 统计结果
*/
@GetMapping("/promoter")
public Result getPromoterStats(@RequestParam Long promoterId,
@RequestParam(required = false) @DateTimeFormat(pattern="yyyy-MM-dd") Date startDate,
@RequestParam(required = false) @DateTimeFormat(pattern="yyyy-MM-dd") Date endDate) {
// 构建查询条件
QueryWrapper<UserPromoterRelation> query = new QueryWrapper<>();
query.eq("promoter_id", promoterId);
if (startDate != null) {
query.ge("first_scan_time", startDate);
}
if (endDate != null) {
query.le("first_scan_time", endDate);
}
// 执行查询
int totalUsers = relationMapper.selectCount(query);
List<Map<String, Object>> dailyStats = relationMapper.selectDailyStatsByPromoter(promoterId, startDate, endDate);
// 返回结果
Map<String, Object> result = new HashMap<>();
result.put("totalUsers", totalUsers);
result.put("dailyStats", dailyStats);
return Result.success(result);
}
}
六、安全注意事项
- 参数校验:所有传入的promoterId需要验证是否存在
- 防刷机制:限制同一用户频繁上报扫码记录
- HTTPS:确保所有接口使用HTTPS协议
- 权限控制:推广数据统计接口需要添加权限验证
- 日志记录:记录所有二维码生成和扫码行为
七、扩展功能建议
- 二级分销:可以扩展支持多级推广关系
- 奖励机制:根据扫码用户的活动情况给推广人员奖励
- 实时通知:当有新用户扫码时,实时通知推广人员
- 数据分析:提供更详细的数据分析报表
通过以上Java实现,你可以完整地构建一个支持不同人员生成不同小程序码并能追踪扫码来源的系统。
生成不同环境二维码
https://developers.weixin.qq.com/minigame/dev/api-backend/open-api/qr-code/wxacode.getUnlimited.html
正式版为 release,体验版为 trial,开发版为 develop
直接流输出小程序二维码的实现
以下是改造后的方法,直接将小程序二维码以流的形式输出,而不生成临时文件:
@GetMapping("/generate")
public void generatePromoterQrcode(@RequestParam Long promoterId,
@RequestParam(defaultValue = "release") String envVersion,
HttpServletResponse response) throws IOException {
// 1. 验证推广人员
Promoter promoter = promoterService.getById(promoterId);
if (promoter == null) {
response.sendError(HttpStatus.NOT_FOUND.value(), "推广人员不存在");
return;
}
// 2. 验证环境参数
if (!Arrays.asList("develop", "trial", "release").contains(envVersion)) {
envVersion = "release";
}
// 3. 获取微信access_token
String accessToken = wxMaService.getAccessToken();
if (StringUtils.isBlank(accessToken)) {
response.sendError(HttpStatus.INTERNAL_SERVER_ERROR.value(), "获取微信access_token失败");
return;
}
// 4. 构建请求参数
String scene = "promoterId=" + promoterId;
Map<String, Object> params = new HashMap<>();
params.put("scene", scene);
params.put("page", "pages/index/index");
params.put("width", 430);
params.put("is_hyaline", true);
params.put("env_version", envVersion);
// 5. 调用微信API获取二维码流
String apiUrl = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + accessToken;
// 6. 设置响应头
response.setContentType("image/jpeg");
response.setHeader("Cache-Control", "no-store");
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
// 7. 直接流式输出
try {
RestTemplate restTemplate = new RestTemplate();
restTemplate.execute(
apiUrl,
HttpMethod.POST,
request -> {
request.getHeaders().setContentType(MediaType.APPLICATION_JSON);
String jsonBody = new ObjectMapper().writeValueAsString(params);
request.getBody().write(jsonBody.getBytes(StandardCharsets.UTF_8));
},
responseEntity -> {
InputStream inputStream = responseEntity.getBody();
OutputStream outputStream = response.getOutputStream();
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
return null;
}
);
} catch (Exception e) {
log.error("生成小程序码失败", e);
response.sendError(HttpStatus.INTERNAL_SERVER_ERROR.value(), "生成小程序码失败");
}
}
关键点说明:
-
流式传输优势:
- 不生成临时文件,减少磁盘I/O
- 降低内存消耗
- 响应速度更快
-
响应头设置:
response.setContentType("image/jpeg"); response.setHeader("Cache-Control", "no-store"); response.setHeader("Pragma", "no-cache"); response.setDateHeader("Expires", 0);
这些设置确保浏览器不会缓存二维码图片
-
异常处理:
- 验证推广人员存在性
- 处理微信API调用失败情况
- 捕获并处理流传输中的异常
-
RestTemplate使用:
- 使用
execute
方法直接处理输入输出流 - 避免将整个图片加载到内存
- 使用
替代方案(使用HttpClient):
如果不使用RestTemplate,可以使用HttpClient实现:
// 替换RestTemplate部分代码
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost(apiUrl);
httpPost.setHeader("Content-Type", "application/json");
httpPost.setEntity(new StringEntity(new ObjectMapper().writeValueAsString(params)));
try (CloseableHttpResponse apiResponse = httpClient.execute(httpPost);
InputStream inputStream = apiResponse.getEntity().getContent();
OutputStream outputStream = response.getOutputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
}
性能优化建议:
-
添加缓存:
// 在方法开始时添加缓存检查 String cacheKey = "qrcode:" + promoterId + ":" + envVersion; byte[] cachedImage = redisTemplate.opsForValue().get(cacheKey); if (cachedImage != null) { response.getOutputStream().write(cachedImage); return; } // 在成功生成二维码后添加缓存 ByteArrayOutputStream baos = new ByteArrayOutputStream(); // ...流复制代码... byte[] imageBytes = baos.toByteArray(); redisTemplate.opsForValue().set(cacheKey, imageBytes, 1, TimeUnit.HOURS);
-
添加限流:
// 使用Guava RateLimiter private final RateLimiter rateLimiter = RateLimiter.create(10.0); // 每秒10个 if (!rateLimiter.tryAcquire()) { response.sendError(HttpStatus.TOO_MANY_REQUESTS.value(), "请求过于频繁"); return; }
这样改造后的接口可以直接返回二维码图片流,无需生成临时文件,提高了性能和资源利用率。
原创作者: vipsoft 转载于: https://www.cnblogs.com/vipsoft/p/18910003