feat(wx): 添加微信小程序码生成功能
- 新增 getQRCodeUnlimited 方法生成小程序码 - 添加 getLocalAccessToken 方法获取微信 access_token - 更新 WxLoginController 以使用新的二维码生成逻辑- 移除 MqttServiceTest 类,增加 WxDev 类用于微信相关测试 - 更新 Dockerfile 和 docker-compose.yml 以适应新的功能需求
This commit is contained in:
14
Dockerfile
14
Dockerfile
@@ -1,21 +1,19 @@
|
|||||||
# 使用OpenJDK 17作为基础镜像
|
# 使用更小的 Alpine Linux + OpenJDK 17 镜像
|
||||||
FROM openjdk:17-jre-alpine
|
FROM openjdk:17-jdk-alpine
|
||||||
|
|
||||||
# 设置工作目录
|
# 设置工作目录
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 创建证书目录
|
|
||||||
RUN mkdir -p /app/certs
|
|
||||||
|
|
||||||
# 创建日志目录
|
# 创建日志目录
|
||||||
RUN mkdir -p /app/logs
|
RUN mkdir -p /app/logs
|
||||||
|
|
||||||
# 创建上传文件目录
|
# 创建上传文件目录
|
||||||
RUN mkdir -p /app/uploads
|
RUN mkdir -p /app/uploads
|
||||||
|
|
||||||
# 添加应用用户(安全考虑)
|
# 安装wget用于健康检查,并添加应用用户(安全考虑)
|
||||||
RUN addgroup -g 1000 appgroup && \
|
RUN apk add --no-cache wget && \
|
||||||
adduser -D -s /bin/sh -u 1000 -G appgroup appuser
|
addgroup -g 1000 appgroup && \
|
||||||
|
adduser -D -u 1000 -G appgroup appuser
|
||||||
|
|
||||||
# 复制jar包到容器
|
# 复制jar包到容器
|
||||||
COPY target/*.jar app.jar
|
COPY target/*.jar app.jar
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ version: '3.8'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
# 应用服务
|
# 应用服务
|
||||||
cms-app:
|
cms-api:
|
||||||
build: .
|
build: .
|
||||||
container_name: cms-java-app
|
container_name: cms-api
|
||||||
ports:
|
ports:
|
||||||
- "9200:9200"
|
- "9200:9200"
|
||||||
environment:
|
environment:
|
||||||
@@ -19,9 +19,6 @@ services:
|
|||||||
- ./uploads:/app/uploads
|
- ./uploads:/app/uploads
|
||||||
networks:
|
networks:
|
||||||
- cms-network
|
- cms-network
|
||||||
depends_on:
|
|
||||||
- mysql
|
|
||||||
- redis
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:9200/actuator/health"]
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:9200/actuator/health"]
|
||||||
@@ -30,58 +27,6 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
start_period: 60s
|
start_period: 60s
|
||||||
|
|
||||||
# MySQL数据库
|
|
||||||
mysql:
|
|
||||||
image: mysql:8.0
|
|
||||||
container_name: cms-mysql
|
|
||||||
environment:
|
|
||||||
MYSQL_ROOT_PASSWORD: root123456
|
|
||||||
MYSQL_DATABASE: modules
|
|
||||||
MYSQL_USER: modules
|
|
||||||
MYSQL_PASSWORD: 8YdLnk7KsPAyDXGA
|
|
||||||
ports:
|
|
||||||
- "3308:3306"
|
|
||||||
volumes:
|
|
||||||
- mysql_data:/var/lib/mysql
|
|
||||||
- ./mysql/conf:/etc/mysql/conf.d
|
|
||||||
- ./mysql/init:/docker-entrypoint-initdb.d
|
|
||||||
networks:
|
|
||||||
- cms-network
|
|
||||||
restart: unless-stopped
|
|
||||||
command: --default-authentication-plugin=mysql_native_password
|
|
||||||
|
|
||||||
# Redis缓存
|
|
||||||
redis:
|
|
||||||
image: redis:6.2-alpine
|
|
||||||
container_name: cms-redis
|
|
||||||
ports:
|
|
||||||
- "16379:6379"
|
|
||||||
volumes:
|
|
||||||
- redis_data:/data
|
|
||||||
- ./redis/redis.conf:/usr/local/etc/redis/redis.conf
|
|
||||||
networks:
|
|
||||||
- cms-network
|
|
||||||
restart: unless-stopped
|
|
||||||
command: redis-server /usr/local/etc/redis/redis.conf
|
|
||||||
|
|
||||||
# Nginx反向代理(可选)
|
|
||||||
nginx:
|
|
||||||
image: nginx:alpine
|
|
||||||
container_name: cms-nginx
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
- "443:443"
|
|
||||||
volumes:
|
|
||||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
|
|
||||||
- ./nginx/conf.d:/etc/nginx/conf.d
|
|
||||||
- ./nginx/ssl:/etc/nginx/ssl
|
|
||||||
- ./uploads:/var/www/uploads
|
|
||||||
networks:
|
|
||||||
- cms-network
|
|
||||||
depends_on:
|
|
||||||
- cms-app
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
cms-network:
|
cms-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|||||||
12
pom.xml
12
pom.xml
@@ -340,6 +340,18 @@
|
|||||||
<version>0.2.5</version>
|
<version>0.2.5</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.squareup.okhttp3</groupId>
|
||||||
|
<artifactId>okhttp</artifactId>
|
||||||
|
<version>4.12.0</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- 可选:用来做内存缓存 access_token -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||||
|
<artifactId>caffeine</artifactId>
|
||||||
|
<version>3.1.8</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.freewayso</groupId>
|
<groupId>com.freewayso</groupId>
|
||||||
<artifactId>image-combiner</artifactId>
|
<artifactId>image-combiner</artifactId>
|
||||||
|
|||||||
@@ -50,6 +50,13 @@ public class CmsAdController extends BaseController {
|
|||||||
return success(ad);
|
return success(ad);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "根据code查询广告位")
|
||||||
|
@GetMapping("/getByCode/{code}")
|
||||||
|
public ApiResult<CmsAd> getByCode(@PathVariable("code") String code) {
|
||||||
|
final CmsAd ad = cmsAdService.getByIdCode(code);
|
||||||
|
return success(ad);
|
||||||
|
}
|
||||||
|
|
||||||
@Operation(summary = "添加广告位")
|
@Operation(summary = "添加广告位")
|
||||||
@PostMapping()
|
@PostMapping()
|
||||||
public ApiResult<?> save(@RequestBody CmsAd cmsAd) {
|
public ApiResult<?> save(@RequestBody CmsAd cmsAd) {
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ public class CmsAd implements Serializable {
|
|||||||
@Schema(description = "类型")
|
@Schema(description = "类型")
|
||||||
private Integer type;
|
private Integer type;
|
||||||
|
|
||||||
|
@Schema(description = "唯一标识")
|
||||||
|
private String code;
|
||||||
|
|
||||||
@Schema(description = "栏目ID")
|
@Schema(description = "栏目ID")
|
||||||
private Integer categoryId;
|
private Integer categoryId;
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
<if test="param.type != null">
|
<if test="param.type != null">
|
||||||
AND a.type = #{param.type}
|
AND a.type = #{param.type}
|
||||||
</if>
|
</if>
|
||||||
|
<if test="param.code != null">
|
||||||
|
AND a.code = #{param.code}
|
||||||
|
</if>
|
||||||
<if test="param.categoryId != null">
|
<if test="param.categoryId != null">
|
||||||
AND a.category_id = #{param.categoryId}
|
AND a.category_id = #{param.categoryId}
|
||||||
</if>
|
</if>
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ public class CmsAdParam extends BaseParam {
|
|||||||
@Schema(description = "类型")
|
@Schema(description = "类型")
|
||||||
private Integer type;
|
private Integer type;
|
||||||
|
|
||||||
|
@Schema(description = "唯一标识")
|
||||||
|
@QueryField(type = QueryType.EQ)
|
||||||
|
private String code;
|
||||||
|
|
||||||
@Schema(description = "栏目ID")
|
@Schema(description = "栏目ID")
|
||||||
@QueryField(type = QueryType.EQ)
|
@QueryField(type = QueryType.EQ)
|
||||||
private Integer categoryId;
|
private Integer categoryId;
|
||||||
|
|||||||
@@ -39,4 +39,10 @@ public interface CmsAdService extends IService<CmsAd> {
|
|||||||
*/
|
*/
|
||||||
CmsAd getByIdRel(Integer adId);
|
CmsAd getByIdRel(Integer adId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据code查询
|
||||||
|
*
|
||||||
|
* @return CmsAd
|
||||||
|
*/
|
||||||
|
CmsAd getByIdCode(String code);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,4 +47,11 @@ public class CmsAdServiceImpl extends ServiceImpl<CmsAdMapper, CmsAd> implements
|
|||||||
return param.getOne(baseMapper.selectListRel(param));
|
return param.getOne(baseMapper.selectListRel(param));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CmsAd getByIdCode(String code) {
|
||||||
|
CmsAdParam param = new CmsAdParam();
|
||||||
|
param.setCode(code);
|
||||||
|
return param.getOne(baseMapper.selectListRel(param));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,4 +127,6 @@ public class WxUtil {
|
|||||||
this.qr_code = jsonObject.getString("qr_code");
|
this.qr_code = jsonObject.getString("qr_code");
|
||||||
this.open_userid = jsonObject.getString("open_userid");
|
this.open_userid = jsonObject.getString("open_userid");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
package com.gxwebsoft.common.system.controller;
|
package com.gxwebsoft.common.system.controller;
|
||||||
|
|
||||||
import cn.hutool.core.io.FileUtil;
|
import cn.hutool.core.io.FileUtil;
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
|
||||||
import cn.hutool.core.util.RandomUtil;
|
import cn.hutool.core.util.RandomUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.hutool.http.HttpRequest;
|
import cn.hutool.http.HttpRequest;
|
||||||
import cn.hutool.http.HttpUtil;
|
import cn.hutool.http.HttpUtil;
|
||||||
import com.alibaba.fastjson.JSON;
|
import com.alibaba.fastjson.JSON;
|
||||||
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.JSONObject;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.gxwebsoft.common.core.config.ConfigProperties;
|
import com.gxwebsoft.common.core.config.ConfigProperties;
|
||||||
import com.gxwebsoft.common.core.exception.BusinessException;
|
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||||
import com.gxwebsoft.common.core.security.JwtSubject;
|
import com.gxwebsoft.common.core.security.JwtSubject;
|
||||||
@@ -25,13 +25,18 @@ import com.gxwebsoft.common.system.result.LoginResult;
|
|||||||
import com.gxwebsoft.common.system.service.*;
|
import com.gxwebsoft.common.system.service.*;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import okhttp3.*;
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@@ -43,6 +48,9 @@ import static com.gxwebsoft.common.core.constants.RedisConstants.ACCESS_TOKEN_KE
|
|||||||
@Tag(name = "微信小程序登录API")
|
@Tag(name = "微信小程序登录API")
|
||||||
public class WxLoginController extends BaseController {
|
public class WxLoginController extends BaseController {
|
||||||
private final StringRedisTemplate redisTemplate;
|
private final StringRedisTemplate redisTemplate;
|
||||||
|
private final OkHttpClient http = new OkHttpClient();
|
||||||
|
private final ObjectMapper om = new ObjectMapper();
|
||||||
|
private volatile long tokenExpireEpoch = 0L; // 过期的 epoch 秒
|
||||||
@Resource
|
@Resource
|
||||||
private SettingService settingService;
|
private SettingService settingService;
|
||||||
@Resource
|
@Resource
|
||||||
@@ -64,6 +72,8 @@ public class WxLoginController extends BaseController {
|
|||||||
@Resource
|
@Resource
|
||||||
private UserRefereeService userRefereeService;
|
private UserRefereeService userRefereeService;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public WxLoginController(StringRedisTemplate redisTemplate) {
|
public WxLoginController(StringRedisTemplate redisTemplate) {
|
||||||
this.redisTemplate = redisTemplate;
|
this.redisTemplate = redisTemplate;
|
||||||
}
|
}
|
||||||
@@ -118,7 +128,6 @@ public class WxLoginController extends BaseController {
|
|||||||
UserParam userParam2 = new UserParam();
|
UserParam userParam2 = new UserParam();
|
||||||
userParam2.setCode(userParam.getAuthCode());
|
userParam2.setCode(userParam.getAuthCode());
|
||||||
JSONObject result = getOpenIdByCode(userParam2);
|
JSONObject result = getOpenIdByCode(userParam2);
|
||||||
System.out.println("userInfo res:" + result);
|
|
||||||
String openid = result.getString("openid");
|
String openid = result.getString("openid");
|
||||||
// String unionid = result.getString("unionid");
|
// String unionid = result.getString("unionid");
|
||||||
userParam.setOpenid(openid);
|
userParam.setOpenid(openid);
|
||||||
@@ -288,7 +297,6 @@ public class WxLoginController extends BaseController {
|
|||||||
String url = apiUrl.concat("?grant_type=client_credential").concat("&appid=").concat(setting.getString("appId")).concat("&secret=").concat(setting.getString("appSecret"));
|
String url = apiUrl.concat("?grant_type=client_credential").concat("&appid=").concat(setting.getString("appId")).concat("&secret=").concat(setting.getString("appSecret"));
|
||||||
// 执行get请求
|
// 执行get请求
|
||||||
String result = HttpUtil.get(url);
|
String result = HttpUtil.get(url);
|
||||||
System.out.println("result = " + result);
|
|
||||||
// 解析access_token
|
// 解析access_token
|
||||||
JSONObject response = JSON.parseObject(result);
|
JSONObject response = JSON.parseObject(result);
|
||||||
if (response.getString("access_token") != null) {
|
if (response.getString("access_token") != null) {
|
||||||
@@ -401,27 +409,96 @@ public class WxLoginController extends BaseController {
|
|||||||
|
|
||||||
@Operation(summary = "获取微信小程序码-订单核销码-数量极多的业务场景")
|
@Operation(summary = "获取微信小程序码-订单核销码-数量极多的业务场景")
|
||||||
@GetMapping("/getOrderQRCodeUnlimited/{orderNo}")
|
@GetMapping("/getOrderQRCodeUnlimited/{orderNo}")
|
||||||
public ApiResult<?> getOrderQRCodeUnlimited(@PathVariable("orderNo") String orderNo) {
|
public void getOrderQRCodeUnlimited(@PathVariable("orderNo") String orderNo, HttpServletResponse response) throws IOException {
|
||||||
String apiUrl = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + getAccessToken();
|
System.out.println("orderNo = " + orderNo);
|
||||||
|
String apiUrl = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + getLocalAccessToken();
|
||||||
final HashMap<String, Object> map = new HashMap<>();
|
final HashMap<String, Object> map = new HashMap<>();
|
||||||
map.put("scene", "orderNo=".concat(orderNo));
|
map.put("scene", orderNo);
|
||||||
map.put("page", "package/admin/order-scan");
|
map.put("page", "pages/index/index");
|
||||||
map.put("env_version", "trial");
|
map.put("env_version", "develop");
|
||||||
|
String jsonBody = JSON.toJSONString(map);
|
||||||
|
System.out.println("请求的 JSON body = " + jsonBody);
|
||||||
// 获取图片 Buffer
|
// 获取图片 Buffer
|
||||||
byte[] qrCode = HttpRequest.post(apiUrl)
|
byte[] qrCode = HttpRequest.post(apiUrl)
|
||||||
.body(JSON.toJSONString(map))
|
.body(JSON.toJSONString(map))
|
||||||
.execute().bodyBytes();
|
.execute().bodyBytes();
|
||||||
System.out.println("qrCode = " + qrCode);
|
|
||||||
|
|
||||||
// 保存的文件名称
|
// 设置响应头
|
||||||
final String fileName = CommonUtil.randomUUID8().concat(".png");
|
response.setContentType("image/png");
|
||||||
// 保存路径
|
response.setHeader("Cache-Control", "no-cache");
|
||||||
String filePath = getUploadDir().concat("qrcode/") + fileName;
|
response.setHeader("Content-Disposition", "inline; filename=encrypted_qrcode.png");
|
||||||
File file = FileUtil.writeBytes(qrCode, filePath);
|
|
||||||
if (file != null) {
|
// 输出图片
|
||||||
return success(config.getFileServer().concat("/qrcode/").concat(fileName));
|
response.getOutputStream().write(qrCode);
|
||||||
|
System.out.println("response = " + response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取微信小程序码-用户ID")
|
||||||
|
@GetMapping("/getQRCodeText")
|
||||||
|
public byte[] getQRCodeText(String scene, String page, Integer width,
|
||||||
|
Boolean isHyaline, String envVersion) throws IOException {
|
||||||
|
HttpUrl url = HttpUrl.parse("https://api.weixin.qq.com/wxa/getwxacodeunlimit")
|
||||||
|
.newBuilder()
|
||||||
|
.addQueryParameter("access_token", getLocalAccessToken())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
System.out.println("page = " + page);
|
||||||
|
// 构造请求 JSON
|
||||||
|
// 注意:scene 仅支持可见字符,长度上限 32,尽量 URL-safe(字母数字下划线等)
|
||||||
|
// page 必须是已发布小程序内的路径(不带开头斜杠也可)
|
||||||
|
var root = om.createObjectNode();
|
||||||
|
root.put("scene", scene);
|
||||||
|
if (page != null) root.put("page", page);
|
||||||
|
if (width != null) root.put("width", width); // 默认 430,建议 280~1280
|
||||||
|
if (isHyaline != null) root.put("is_hyaline", isHyaline);
|
||||||
|
if (envVersion != null) root.put("env_version", envVersion); // release/trial/develop
|
||||||
|
|
||||||
|
okhttp3.RequestBody reqBody = okhttp3.RequestBody.create(
|
||||||
|
root.toString(), MediaType.parse("application/json; charset=utf-8"));
|
||||||
|
Request req = new Request.Builder().url(url).post(reqBody).build();
|
||||||
|
|
||||||
|
try (Response resp = http.newCall(req).execute()) {
|
||||||
|
if (!resp.isSuccessful()) {
|
||||||
|
throw new IOException("HTTP " + resp.code() + " calling getwxacodeunlimit");
|
||||||
|
}
|
||||||
|
MediaType ct = resp.body().contentType();
|
||||||
|
byte[] bytes = resp.body().bytes();
|
||||||
|
// 微信出错时返回 JSON,需要识别一下
|
||||||
|
if (ct != null && ct.subtype() != null && ct.subtype().contains("json")) {
|
||||||
|
String err = new String(bytes);
|
||||||
|
throw new IOException("WeChat error: " + err);
|
||||||
|
}
|
||||||
|
return bytes; // 成功就是图片二进制(PNG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取/刷新 access_token */
|
||||||
|
public String getLocalAccessToken() throws IOException {
|
||||||
|
long now = Instant.now().getEpochSecond();
|
||||||
|
String key ="AccessToken:Local:10550";
|
||||||
|
if (redisUtil.get(key) != null && now < tokenExpireEpoch - 60) {
|
||||||
|
return redisUtil.get(key);
|
||||||
|
}
|
||||||
|
HttpUrl url = HttpUrl.parse("https://api.weixin.qq.com/cgi-bin/token")
|
||||||
|
.newBuilder()
|
||||||
|
.addQueryParameter("grant_type", "client_credential")
|
||||||
|
.addQueryParameter("appid", "wx51962d6ac21f2ed2")
|
||||||
|
.addQueryParameter("secret", "d821f98de8a6c1ba7bc7e0ee84bcbc8e")
|
||||||
|
.build();
|
||||||
|
Request req = new Request.Builder().url(url).get().build();
|
||||||
|
try (Response resp = http.newCall(req).execute()) {
|
||||||
|
String body = resp.body().string();
|
||||||
|
JsonNode json = om.readTree(body);
|
||||||
|
if (json.has("access_token")) {
|
||||||
|
String token = json.get("access_token").asText();
|
||||||
|
long expiresIn = json.get("expires_in").asInt(7200);
|
||||||
|
redisUtil.set(key,token,expiresIn,TimeUnit.SECONDS);
|
||||||
|
tokenExpireEpoch = now + expiresIn;
|
||||||
|
return token;
|
||||||
|
} else {
|
||||||
|
throw new IOException("Get access_token failed: " + body);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return fail("获取失败", null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -72,9 +72,10 @@ public class SettingServiceImpl extends ServiceImpl<SettingMapper, Setting> impl
|
|||||||
@Override
|
@Override
|
||||||
public JSONObject getBySettingKey(String key) {
|
public JSONObject getBySettingKey(String key) {
|
||||||
Setting setting = this.getOne(new QueryWrapper<Setting>().eq("setting_key", key), false);
|
Setting setting = this.getOne(new QueryWrapper<Setting>().eq("setting_key", key), false);
|
||||||
|
System.out.println("setting1 = " + setting);
|
||||||
if(setting == null){
|
if(setting == null){
|
||||||
if ("mp-weixin".equals(key)) {
|
if ("mp-weixin".equals(key)) {
|
||||||
throw new BusinessException("小程序未配置");
|
throw new BusinessException("小程序未配置1");
|
||||||
}
|
}
|
||||||
if ("payment".equals(key)) {
|
if ("payment".equals(key)) {
|
||||||
throw new BusinessException("支付未配置");
|
throw new BusinessException("支付未配置");
|
||||||
|
|||||||
@@ -3,17 +3,19 @@
|
|||||||
# 数据源配置
|
# 数据源配置
|
||||||
spring:
|
spring:
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:mysql://1Panel-mysql-Bqdt:3306/modules?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
|
url: jdbc:mysql://8.134.169.209:13306/modules?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
|
||||||
username: modules
|
username: modules
|
||||||
password: 8YdLnk7KsPAyDXGA
|
password: 8YdLnk7KsPAyDXGA
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
type: com.alibaba.druid.pool.DruidDataSource
|
type: com.alibaba.druid.pool.DruidDataSource
|
||||||
|
druid:
|
||||||
|
remove-abandoned: true
|
||||||
|
|
||||||
# redis
|
# redis
|
||||||
redis:
|
redis:
|
||||||
database: 0
|
database: 0
|
||||||
host: 1Panel-redis-Q1LE
|
host: 8.134.169.209
|
||||||
port: 6379
|
port: 16379
|
||||||
password: redis_WSDb88
|
password: redis_WSDb88
|
||||||
|
|
||||||
# 日志配置
|
# 日志配置
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ shop:
|
|||||||
tenant-configs:
|
tenant-configs:
|
||||||
- tenant-id: 10324
|
- tenant-id: 10324
|
||||||
tenant-name: "百色中学"
|
tenant-name: "百色中学"
|
||||||
timeout-minutes: 60 # 捐款订单给更长的支付时间
|
timeout-minutes: 120 # 捐款订单给更长的支付时间
|
||||||
enabled: true
|
enabled: true
|
||||||
# 可以添加更多租户配置
|
# 可以添加更多租户配置
|
||||||
# - tenant-id: 10550
|
# - tenant-id: 10550
|
||||||
|
|||||||
110
src/test/java/com/gxwebsoft/WxDev.java
Normal file
110
src/test/java/com/gxwebsoft/WxDev.java
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package com.gxwebsoft;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import okhttp3.*;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class WxDev {
|
||||||
|
|
||||||
|
@Value("${wechat.appid}")
|
||||||
|
private String appId;
|
||||||
|
@Value("${wechat.secret}")
|
||||||
|
private String secret;
|
||||||
|
|
||||||
|
private final StringRedisTemplate redisTemplate;
|
||||||
|
|
||||||
|
private final OkHttpClient http = new OkHttpClient();
|
||||||
|
private final ObjectMapper om = new ObjectMapper();
|
||||||
|
|
||||||
|
/** 简单本地缓存 access_token(生产建议放 Redis) */
|
||||||
|
private final AtomicReference<String> cachedToken = new AtomicReference<>();
|
||||||
|
private volatile long tokenExpireEpoch = 0L; // 过期的 epoch 秒
|
||||||
|
|
||||||
|
public WxDev(StringRedisTemplate redisTemplate) {
|
||||||
|
this.redisTemplate = redisTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取/刷新 access_token */
|
||||||
|
public String getAccessToken() throws IOException {
|
||||||
|
long now = Instant.now().getEpochSecond();
|
||||||
|
System.out.println("cachedToken.get = " + cachedToken.get());
|
||||||
|
if (cachedToken.get() != null && now < tokenExpireEpoch - 60) {
|
||||||
|
return cachedToken.get();
|
||||||
|
}
|
||||||
|
HttpUrl url = HttpUrl.parse("https://api.weixin.qq.com/cgi-bin/token")
|
||||||
|
.newBuilder()
|
||||||
|
.addQueryParameter("grant_type", "client_credential")
|
||||||
|
.addQueryParameter("appid", "wx51962d6ac21f2ed2")
|
||||||
|
.addQueryParameter("secret", "d821f98de8a6c1ba7bc7e0ee84bcbc8e")
|
||||||
|
.build();
|
||||||
|
Request req = new Request.Builder().url(url).get().build();
|
||||||
|
try (Response resp = http.newCall(req).execute()) {
|
||||||
|
String body = resp.body().string();
|
||||||
|
JsonNode json = om.readTree(body);
|
||||||
|
if (json.has("access_token")) {
|
||||||
|
String token = json.get("access_token").asText();
|
||||||
|
int expiresIn = json.get("expires_in").asInt(7200);
|
||||||
|
System.out.println("token1 = " + token);
|
||||||
|
cachedToken.set(token);
|
||||||
|
tokenExpireEpoch = now + expiresIn;
|
||||||
|
return token;
|
||||||
|
} else {
|
||||||
|
throw new IOException("Get access_token failed: " + body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 调用 getwxacodeunlimit,返回图片二进制 */
|
||||||
|
public byte[] getUnlimitedCode(String scene, String page, Integer width,
|
||||||
|
Boolean isHyaline, String envVersion) throws IOException {
|
||||||
|
String accessToken = getAccessToken();
|
||||||
|
System.out.println("accessToken = " + accessToken);
|
||||||
|
HttpUrl url = HttpUrl.parse("https://api.weixin.qq.com/wxa/getwxacodeunlimit")
|
||||||
|
.newBuilder()
|
||||||
|
.addQueryParameter("access_token", accessToken)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 构造请求 JSON
|
||||||
|
// 注意:scene 仅支持可见字符,长度上限 32,尽量 URL-safe(字母数字下划线等)
|
||||||
|
// page 必须是已发布小程序内的路径(不带开头斜杠也可)
|
||||||
|
var root = om.createObjectNode();
|
||||||
|
root.put("scene", scene);
|
||||||
|
if (page != null) root.put("page", page);
|
||||||
|
if (width != null) root.put("width", width); // 默认 430,建议 280~1280
|
||||||
|
if (isHyaline != null) root.put("is_hyaline", isHyaline);
|
||||||
|
if (envVersion != null) root.put("env_version", envVersion); // release/trial/develop
|
||||||
|
|
||||||
|
RequestBody reqBody = RequestBody.create(
|
||||||
|
root.toString(), MediaType.parse("application/json; charset=utf-8"));
|
||||||
|
Request req = new Request.Builder().url(url).post(reqBody).build();
|
||||||
|
|
||||||
|
try (Response resp = http.newCall(req).execute()) {
|
||||||
|
if (!resp.isSuccessful()) {
|
||||||
|
throw new IOException("HTTP " + resp.code() + " calling getwxacodeunlimit");
|
||||||
|
}
|
||||||
|
MediaType ct = resp.body().contentType();
|
||||||
|
byte[] bytes = resp.body().bytes();
|
||||||
|
// 微信出错时返回 JSON,需要识别一下
|
||||||
|
if (ct != null && ct.subtype() != null && ct.subtype().contains("json")) {
|
||||||
|
String err = new String(bytes);
|
||||||
|
throw new IOException("WeChat error: " + err);
|
||||||
|
}
|
||||||
|
return bytes; // 成功就是图片二进制(PNG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getQrCode() throws IOException {
|
||||||
|
final byte[] test = getUnlimitedCode("register", "pages/index/index",180,false,"develop");
|
||||||
|
System.out.println("test = " + test);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package com.gxwebsoft.hjm;
|
|
||||||
|
|
||||||
import com.gxwebsoft.hjm.service.MqttService;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
|
||||||
import org.springframework.test.context.ActiveProfiles;
|
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MQTT服务测试类
|
|
||||||
*
|
|
||||||
* @author 科技小王子
|
|
||||||
* @since 2025-07-02
|
|
||||||
*/
|
|
||||||
@SpringBootTest
|
|
||||||
@ActiveProfiles("dev")
|
|
||||||
public class MqttServiceTest {
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private MqttService mqttService;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testMqttConnection() {
|
|
||||||
System.out.println("MQTT连接状态: " + mqttService.isConnected());
|
|
||||||
System.out.println("MQTT客户端信息: " + mqttService.getClientInfo());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testMqttReconnect() {
|
|
||||||
try {
|
|
||||||
mqttService.reconnect();
|
|
||||||
System.out.println("MQTT重连测试完成");
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("MQTT重连测试失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testMqttPublish() {
|
|
||||||
try {
|
|
||||||
if (mqttService.isConnected()) {
|
|
||||||
mqttService.publish("/test/topic", "测试消息");
|
|
||||||
System.out.println("MQTT消息发布测试完成");
|
|
||||||
} else {
|
|
||||||
System.out.println("MQTT未连接,跳过发布测试");
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("MQTT消息发布测试失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user