Merge remote-tracking branch 'origin/main'

This commit is contained in:
chenshaohua
2026-07-23 17:24:01 +08:00
7 changed files with 586 additions and 18 deletions

View File

@@ -221,10 +221,10 @@ public class AppOutdoorFairController extends BaseController
return AjaxResult.error("招聘会不存在");
}
if (fair.getHoldTime() == null) {
return AjaxResult.error("招聘会开始时间未设置,暂不能签到");
return AjaxResult.error("招聘会还没有开始,暂不能签到");
}
if (!hasFairStarted(fair, new Date())) {
return AjaxResult.error("招聘会尚未开始,暂不能签到");
return AjaxResult.error("招聘会还没有开始,暂不能签到");
}
// 同一招聘会下同手机号已签到则不重复记录
Long exists = outdoorFairAttendeeService.count(new LambdaQueryWrapper<OutdoorFairAttendee>()

View File

@@ -42,14 +42,15 @@ public class WeChatMiniProgramApiClient
return new AccessTokenResult(accessToken, expiresIn == null ? 7200 : expiresIn);
}
public QrCodeImage getUnlimitedQrCode(String accessToken, String scene, String page)
public QrCodeImage getUnlimitedQrCode(String accessToken, String scene, String page,
String envVersion)
{
JSONObject requestBody = new JSONObject();
requestBody.put("scene", scene);
requestBody.put("page", page);
// 新页面尚未随小程序版本发布时也允许先生成码;扫码仍会进入正式版小程序。
// 新页面尚未随小程序版本发布时也允许先生成码;扫码进入配置指定的小程序版本
requestBody.put("check_path", false);
requestBody.put("env_version", "release");
requestBody.put("env_version", envVersion);
requestBody.put("width", 430);
HttpResponse response = null;

View File

@@ -18,13 +18,15 @@ import java.util.function.LongSupplier;
public class WeChatMiniProgramQrCodeService
{
public static final String OUTDOOR_FAIR_PAGE = "packageA/pages/outdoorFair/detail";
public static final String DEFAULT_ENV_VERSION = "trial";
private static final long TOKEN_REFRESH_AHEAD_MILLIS = 300_000L;
private final WeChatMiniProgramApiClient apiClient;
private final String appId;
private final String appSecret;
private final String envVersion;
private final LongSupplier currentTimeMillis;
private final Map<Long, String> qrCodeDataUrlCache = new ConcurrentHashMap<>();
private final Map<String, String> qrCodeDataUrlCache = new ConcurrentHashMap<>();
private volatile String accessToken;
private volatile long accessTokenExpiresAt;
@@ -32,17 +34,26 @@ public class WeChatMiniProgramQrCodeService
@Autowired
public WeChatMiniProgramQrCodeService(WeChatMiniProgramApiClient apiClient,
@Value("${wx.appid}") String appId,
@Value("${wx.secret}") String appSecret)
@Value("${wx.secret}") String appSecret,
@Value("${wx.env-version:trial}") String envVersion)
{
this(apiClient, appId, appSecret, System::currentTimeMillis);
this(apiClient, appId, appSecret, envVersion, System::currentTimeMillis);
}
WeChatMiniProgramQrCodeService(WeChatMiniProgramApiClient apiClient, String appId,
String appSecret, LongSupplier currentTimeMillis)
{
this(apiClient, appId, appSecret, DEFAULT_ENV_VERSION, currentTimeMillis);
}
WeChatMiniProgramQrCodeService(WeChatMiniProgramApiClient apiClient, String appId,
String appSecret, String envVersion,
LongSupplier currentTimeMillis)
{
this.apiClient = apiClient;
this.appId = appId;
this.appSecret = appSecret;
this.envVersion = normalizeEnvVersion(envVersion);
this.currentTimeMillis = currentTimeMillis;
}
@@ -52,16 +63,17 @@ public class WeChatMiniProgramQrCodeService
throw new ServiceException("招聘会参数不正确");
}
String scene = "fairId=" + fairId;
String imageData = qrCodeDataUrlCache.get(fairId);
String cacheKey = envVersion + ":" + fairId;
String imageData = qrCodeDataUrlCache.get(cacheKey);
if (imageData == null) {
synchronized (qrCodeDataUrlCache) {
imageData = qrCodeDataUrlCache.get(fairId);
imageData = qrCodeDataUrlCache.get(cacheKey);
if (imageData == null) {
WeChatMiniProgramApiClient.QrCodeImage image = apiClient.getUnlimitedQrCode(
getValidAccessToken(), scene, OUTDOOR_FAIR_PAGE);
getValidAccessToken(), scene, OUTDOOR_FAIR_PAGE, envVersion);
imageData = "data:" + image.getContentType() + ";base64,"
+ Base64.getEncoder().encodeToString(image.getContent());
qrCodeDataUrlCache.put(fairId, imageData);
qrCodeDataUrlCache.put(cacheKey, imageData);
}
}
}
@@ -70,6 +82,7 @@ public class WeChatMiniProgramQrCodeService
data.put("fairId", fairId);
data.put("page", OUTDOOR_FAIR_PAGE);
data.put("scene", scene);
data.put("envVersion", envVersion);
data.put("imageData", imageData);
// getUnlimitedQRCode 生成的小程序码长期有效;需要轮换的是 access token而不是已经生成的码。
data.put("permanent", true);
@@ -77,6 +90,20 @@ public class WeChatMiniProgramQrCodeService
return data;
}
private String normalizeEnvVersion(String value)
{
String normalized = value == null ? "" : value.trim().toLowerCase();
if (normalized.isEmpty()) {
return DEFAULT_ENV_VERSION;
}
if (!"trial".equals(normalized)
&& !"release".equals(normalized)
&& !"develop".equals(normalized)) {
throw new ServiceException("微信小程序版本配置不正确,仅支持 trial、release 或 develop");
}
return normalized;
}
private String getValidAccessToken()
{
long now = currentTimeMillis.getAsLong();

View File

@@ -44,7 +44,22 @@ class AppOutdoorFairControllerTest
AjaxResult result = controller.checkIn(attendee(12L));
assertEquals(500, result.get(AjaxResult.CODE_TAG));
assertEquals("招聘会尚未开始,暂不能签到", result.get(AjaxResult.MSG_TAG));
assertEquals("招聘会还没有开始,暂不能签到", result.get(AjaxResult.MSG_TAG));
verify(attendeeService, never()).save(any(OutdoorFairAttendee.class));
}
@Test
void fairWithoutStartTimeUsesNotStartedMessage() throws Exception
{
IOutdoorFairService fairService = mock(IOutdoorFairService.class);
IOutdoorFairAttendeeService attendeeService = mock(IOutdoorFairAttendeeService.class);
when(fairService.getById(12L)).thenReturn(fairAt(null));
AppOutdoorFairController controller = controllerWith(fairService, attendeeService);
AjaxResult result = controller.checkIn(attendee(12L));
assertEquals(500, result.get(AjaxResult.CODE_TAG));
assertEquals("招聘会还没有开始,暂不能签到", result.get(AjaxResult.MSG_TAG));
verify(attendeeService, never()).save(any(OutdoorFairAttendee.class));
}

View File

@@ -26,7 +26,7 @@ class WeChatMiniProgramQrCodeServiceTest
when(client.getAccessToken("app-id", "app-secret"))
.thenReturn(new WeChatMiniProgramApiClient.AccessTokenResult("token-1", 7200));
when(client.getUnlimitedQrCode("token-1", "fairId=12",
WeChatMiniProgramQrCodeService.OUTDOOR_FAIR_PAGE))
WeChatMiniProgramQrCodeService.OUTDOOR_FAIR_PAGE, "trial"))
.thenReturn(new WeChatMiniProgramApiClient.QrCodeImage(
"image-content".getBytes(StandardCharsets.UTF_8), "image/png"));
@@ -38,13 +38,14 @@ class WeChatMiniProgramQrCodeServiceTest
assertEquals(12L, first.get("fairId"));
assertEquals("fairId=12", first.get("scene"));
assertEquals("packageA/pages/outdoorFair/detail", first.get("page"));
assertEquals("trial", first.get("envVersion"));
assertTrue((Boolean) first.get("permanent"));
assertFalse((Boolean) first.get("rotationRequired"));
assertTrue(String.valueOf(first.get("imageData")).startsWith("data:image/png;base64,"));
assertEquals(first.get("imageData"), second.get("imageData"));
verify(client, times(1)).getAccessToken("app-id", "app-secret");
verify(client, times(1)).getUnlimitedQrCode(
"token-1", "fairId=12", WeChatMiniProgramQrCodeService.OUTDOOR_FAIR_PAGE);
"token-1", "fairId=12", WeChatMiniProgramQrCodeService.OUTDOOR_FAIR_PAGE, "trial");
}
@Test
@@ -55,7 +56,7 @@ class WeChatMiniProgramQrCodeServiceTest
when(client.getAccessToken("app-id", "app-secret"))
.thenReturn(new WeChatMiniProgramApiClient.AccessTokenResult("token-1", 7200))
.thenReturn(new WeChatMiniProgramApiClient.AccessTokenResult("token-2", 7200));
when(client.getUnlimitedQrCode(anyString(), anyString(), anyString()))
when(client.getUnlimitedQrCode(anyString(), anyString(), anyString(), anyString()))
.thenReturn(new WeChatMiniProgramApiClient.QrCodeImage(new byte[]{1, 2, 3}, "image/png"));
WeChatMiniProgramQrCodeService service = new WeChatMiniProgramQrCodeService(
@@ -66,9 +67,9 @@ class WeChatMiniProgramQrCodeServiceTest
verify(client, times(2)).getAccessToken("app-id", "app-secret");
verify(client).getUnlimitedQrCode(
"token-1", "fairId=1", WeChatMiniProgramQrCodeService.OUTDOOR_FAIR_PAGE);
"token-1", "fairId=1", WeChatMiniProgramQrCodeService.OUTDOOR_FAIR_PAGE, "trial");
verify(client).getUnlimitedQrCode(
"token-2", "fairId=2", WeChatMiniProgramQrCodeService.OUTDOOR_FAIR_PAGE);
"token-2", "fairId=2", WeChatMiniProgramQrCodeService.OUTDOOR_FAIR_PAGE, "trial");
}
@Test
@@ -83,4 +84,17 @@ class WeChatMiniProgramQrCodeServiceTest
assertEquals("微信小程序配置不完整,请联系管理员", exception.getMessage());
}
@Test
void rejectsUnsupportedEnvironmentVersion()
{
WeChatMiniProgramApiClient client = mock(WeChatMiniProgramApiClient.class);
ServiceException exception = assertThrows(ServiceException.class,
() -> new WeChatMiniProgramQrCodeService(
client, "app-id", "app-secret", "unknown", () -> 1_000L));
assertEquals("微信小程序版本配置不正确,仅支持 trial、release 或 develop",
exception.getMessage());
}
}