feat: 实现微信小程序码生成后端接口
- 新增 WxController 提供 /api/wx-mini/wxacode 接口 - 新增 WxService 实现 access_token 缓存和小程序码生成 - 新增 HttpUtil.postJsonBytes() 方法处理二进制响应 - 配置 application.yml 支持 env 环境参数 主要功能: 1. access_token 自动缓存,5分钟过期缓冲避免频繁调用 2. 支持生成任意页面小程序码,最大宽度 430px 3. 支持环境版本参数(release/trial/develop) 4. 返回 Base64 图片数据,避免服务器文件存储 5. 使用 Jackson ObjectMapper 处理 JSON 序列化 技术细节: - HttpURLConnection 请求微信 API - Base64 编码图片数据 - 场景参数限制 32 字符 - 错误处理和异常捕获
This commit is contained in:
@@ -0,0 +1,61 @@
|
|||||||
|
package com.example.mini_program.controller;
|
||||||
|
|
||||||
|
import com.example.mini_program.common.Result;
|
||||||
|
import com.example.mini_program.service.WxService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/wx-mini")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WxController {
|
||||||
|
|
||||||
|
private final WxService wxService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成小程序码
|
||||||
|
*/
|
||||||
|
@PostMapping("/wxacode")
|
||||||
|
public Result<String> getWxacode(@RequestBody Map<String, String> request) {
|
||||||
|
String scene = request.get("scene");
|
||||||
|
String page = request.get("page");
|
||||||
|
String envVersion = request.get("envVersion");
|
||||||
|
|
||||||
|
if (scene == null || scene.trim().isEmpty()) {
|
||||||
|
return Result.error("scene不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
page = (page != null && !page.trim().isEmpty()) ? page : "pages/scan/result/index";
|
||||||
|
|
||||||
|
Integer width = parseWidth(request.get("width"));
|
||||||
|
|
||||||
|
if (envVersion == null || envVersion.trim().isEmpty()) {
|
||||||
|
envVersion = wxService.getDefaultEnvVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String base64Image = wxService.getUnlimitedWxacode(scene, page, width, envVersion);
|
||||||
|
return Result.success(base64Image);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("生成小程序码失败", e);
|
||||||
|
return Result.error("生成小程序码失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer parseWidth(String widthStr) {
|
||||||
|
if (widthStr == null || widthStr.trim().isEmpty()) {
|
||||||
|
return 430;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
int width = Integer.parseInt(widthStr);
|
||||||
|
return width > 0 ? width : 430;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("width参数格式错误: {}", widthStr);
|
||||||
|
return 430;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package com.example.mini_program.service;
|
||||||
|
|
||||||
|
import com.example.mini_program.config.WxMiniAppConfig;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class WxService {
|
||||||
|
|
||||||
|
private static final String WXACODE_URL = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=%s";
|
||||||
|
private static final String TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
|
||||||
|
private static final int TOKEN_EXPIRE_BUFFER = 300; // access_token提前过期时间(秒)
|
||||||
|
|
||||||
|
private final WxMiniAppConfig wxMiniAppConfig;
|
||||||
|
private final RestTemplate restTemplate;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Value("${wx.miniapp.env:release}")
|
||||||
|
private String defaultEnvVersion;
|
||||||
|
|
||||||
|
private String cachedAccessToken;
|
||||||
|
private long tokenExpireTime;
|
||||||
|
|
||||||
|
public WxService(WxMiniAppConfig wxMiniAppConfig, RestTemplate restTemplate) {
|
||||||
|
this.wxMiniAppConfig = wxMiniAppConfig;
|
||||||
|
this.restTemplate = restTemplate;
|
||||||
|
this.objectMapper = new ObjectMapper();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成小程序码
|
||||||
|
*/
|
||||||
|
public String getUnlimitedWxacode(String scene, String page, int width, String envVersion) throws Exception {
|
||||||
|
// 参数校验
|
||||||
|
if (scene == null || scene.trim().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("scene不能为空");
|
||||||
|
}
|
||||||
|
if (scene.length() > 32) {
|
||||||
|
throw new IllegalArgumentException("scene长度不能超过32个字符");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用传入的环境版本,若为空则使用默认值
|
||||||
|
if (envVersion == null || envVersion.trim().isEmpty()) {
|
||||||
|
envVersion = defaultEnvVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
String accessToken = getAccessToken();
|
||||||
|
String requestBody = objectMapper.writeValueAsString(new WxacodeRequest(scene, page, width, envVersion));
|
||||||
|
|
||||||
|
log.info("生成小程序码: scene={}, page={}, width={}, env={}", scene, page, width, envVersion);
|
||||||
|
|
||||||
|
byte[] response = com.example.mini_program.util.HttpUtil.postJsonBytes(
|
||||||
|
String.format(WXACODE_URL, accessToken),
|
||||||
|
requestBody
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.length > 0 && response[0] == '{') {
|
||||||
|
String errorMsg = new String(response, StandardCharsets.UTF_8);
|
||||||
|
log.error("微信API返回错误: {}", errorMsg);
|
||||||
|
throw new RuntimeException("微信小程序码生成失败: " + errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("小程序码生成成功,大小: {} bytes", response.length);
|
||||||
|
return java.util.Base64.getEncoder().encodeToString(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDefaultEnvVersion() {
|
||||||
|
return defaultEnvVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取微信access_token(带缓存)
|
||||||
|
*/
|
||||||
|
private String getAccessToken() throws Exception {
|
||||||
|
if (cachedAccessToken != null && System.currentTimeMillis() < tokenExpireTime) {
|
||||||
|
return cachedAccessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = String.format(TOKEN_URL, wxMiniAppConfig.getAppid(), wxMiniAppConfig.getSecret());
|
||||||
|
log.info("刷新access_token");
|
||||||
|
|
||||||
|
String response = restTemplate.getForObject(url, String.class);
|
||||||
|
JsonNode json = objectMapper.readTree(response);
|
||||||
|
|
||||||
|
String accessToken = json.get("access_token").asText();
|
||||||
|
int expiresIn = json.get("expires_in").asInt();
|
||||||
|
|
||||||
|
cachedAccessToken = accessToken;
|
||||||
|
tokenExpireTime = System.currentTimeMillis() + (expiresIn - TOKEN_EXPIRE_BUFFER) * 1000L;
|
||||||
|
|
||||||
|
log.info("access_token已更新,有效期: {} 秒", expiresIn);
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序码请求参数
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
private static class WxacodeRequest {
|
||||||
|
private String scene;
|
||||||
|
private String page;
|
||||||
|
private Integer width;
|
||||||
|
private Boolean check_path;
|
||||||
|
private String env_version;
|
||||||
|
private Boolean auto_color;
|
||||||
|
private Boolean is_hyaline;
|
||||||
|
|
||||||
|
WxacodeRequest(String scene, String page, int width, String envVersion) {
|
||||||
|
this.scene = scene;
|
||||||
|
this.page = page;
|
||||||
|
this.width = width;
|
||||||
|
this.check_path = false;
|
||||||
|
this.env_version = envVersion;
|
||||||
|
this.auto_color = false;
|
||||||
|
this.is_hyaline = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package com.example.mini_program.util;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
@@ -55,6 +56,46 @@ public class HttpUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static byte[] postJsonBytes(String urlStr, String jsonBody) throws Exception {
|
||||||
|
HttpURLConnection conn = null;
|
||||||
|
try {
|
||||||
|
URL url = new URL(urlStr);
|
||||||
|
conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
conn.setConnectTimeout(CONNECT_TIMEOUT);
|
||||||
|
conn.setReadTimeout(READ_TIMEOUT);
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
||||||
|
conn.setRequestProperty("Expect", ""); // 禁用 100-continue
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
os.write(jsonBody.getBytes(StandardCharsets.UTF_8));
|
||||||
|
os.flush();
|
||||||
|
}
|
||||||
|
int code = conn.getResponseCode();
|
||||||
|
if (code >= 300) {
|
||||||
|
String errorBody = readStringFromStream(conn.getErrorStream());
|
||||||
|
throw new RuntimeException("HTTP error " + code + ": " + errorBody);
|
||||||
|
}
|
||||||
|
try (InputStream is = conn.getInputStream()) {
|
||||||
|
return is.readAllBytes();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (conn != null) conn.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String readStringFromStream(InputStream is) throws Exception {
|
||||||
|
if (is == null) return "";
|
||||||
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
sb.append(line);
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static String readResponse(HttpURLConnection conn) throws Exception {
|
private static String readResponse(HttpURLConnection conn) throws Exception {
|
||||||
int code = conn.getResponseCode();
|
int code = conn.getResponseCode();
|
||||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ wx:
|
|||||||
appid: wx50fe0c5c28dd3060
|
appid: wx50fe0c5c28dd3060
|
||||||
secret: e82fa407fad13a9df35503f2d176e5a4
|
secret: e82fa407fad13a9df35503f2d176e5a4
|
||||||
subscribe-template-id: Csf_dJU7DhvVFt_03sphPPBCGlnmcWQSPhgqfxHZ5RQ
|
subscribe-template-id: Csf_dJU7DhvVFt_03sphPPBCGlnmcWQSPhgqfxHZ5RQ
|
||||||
|
env: develop # 环境版本: release(正式版), trial(体验版), develop(开发版)
|
||||||
corp: # 企业ID
|
corp: # 企业ID
|
||||||
corpid: ww257614cff8a1b61b
|
corpid: ww257614cff8a1b61b
|
||||||
# 应用Secret
|
# 应用Secret
|
||||||
|
|||||||
Reference in New Issue
Block a user