diff --git a/src/main/java/com/example/mini_program/controller/WxController.java b/src/main/java/com/example/mini_program/controller/WxController.java new file mode 100644 index 0000000..8b50d98 --- /dev/null +++ b/src/main/java/com/example/mini_program/controller/WxController.java @@ -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 getWxacode(@RequestBody Map 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; + } + } +} diff --git a/src/main/java/com/example/mini_program/service/WxService.java b/src/main/java/com/example/mini_program/service/WxService.java new file mode 100644 index 0000000..711d1fc --- /dev/null +++ b/src/main/java/com/example/mini_program/service/WxService.java @@ -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; + } + } +} diff --git a/src/main/java/com/example/mini_program/util/HttpUtil.java b/src/main/java/com/example/mini_program/util/HttpUtil.java index 2562fd0..f461045 100644 --- a/src/main/java/com/example/mini_program/util/HttpUtil.java +++ b/src/main/java/com/example/mini_program/util/HttpUtil.java @@ -3,6 +3,7 @@ package com.example.mini_program.util; import lombok.extern.slf4j.Slf4j; import java.io.BufferedReader; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; 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 { int code = conn.getResponseCode(); try (BufferedReader reader = new BufferedReader(new InputStreamReader( diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 07af8ac..6874b39 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -16,6 +16,7 @@ wx: appid: wx50fe0c5c28dd3060 secret: e82fa407fad13a9df35503f2d176e5a4 subscribe-template-id: Csf_dJU7DhvVFt_03sphPPBCGlnmcWQSPhgqfxHZ5RQ + env: develop # 环境版本: release(正式版), trial(体验版), develop(开发版) corp: # 企业ID corpid: ww257614cff8a1b61b # 应用Secret