From 5882ba8863d77fdcc168b176c3ffb30e8a20556a Mon Sep 17 00:00:00 2001
From: longz <longzhaoz@qq.com>
Date: 星期一, 13 七月 2026 17:54:25 +0800
Subject: [PATCH] 缩短二维码原始参数
---
backend/src/main/java/com/sinata/appointment/service/MiniAuthApplicationService.java | 156 ++++++++++++++++++++++++++++++++++++++++++++++++++--
1 files changed, 150 insertions(+), 6 deletions(-)
diff --git a/backend/src/main/java/com/sinata/appointment/service/MiniAuthApplicationService.java b/backend/src/main/java/com/sinata/appointment/service/MiniAuthApplicationService.java
index 42d87ab..905f21e 100644
--- a/backend/src/main/java/com/sinata/appointment/service/MiniAuthApplicationService.java
+++ b/backend/src/main/java/com/sinata/appointment/service/MiniAuthApplicationService.java
@@ -6,18 +6,24 @@
import com.sinata.appointment.constants.MpStaffConstants;
import com.sinata.appointment.config.MiniStpUtil;
import com.sinata.appointment.dto.MiniLoginResponse;
+import com.sinata.appointment.dto.MiniOfficialLoginResponse;
import com.sinata.appointment.dto.MiniProfileUpdateRequest;
import com.sinata.appointment.dto.MiniWxLoginRequest;
import com.sinata.appointment.entity.MpUser;
import com.sinata.appointment.entity.SysDept;
import com.sinata.appointment.exception.BizException;
import com.sinata.appointment.integration.WechatMiniGateway;
+import com.sinata.appointment.integration.WechatOfficialAccountGateway;
+import com.sinata.appointment.integration.WxOfficialOAuthResult;
import com.sinata.appointment.integration.WxCode2SessionResult;
import com.sinata.appointment.mapper.SysDeptMapper;
import com.sinata.appointment.mapper.MpUserMapper;
+import java.time.Duration;
import java.util.List;
import java.util.Objects;
+import java.util.UUID;
import com.sinata.appointment.util.PhoneNormalize;
+import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
@@ -28,17 +34,31 @@
@Service
public class MiniAuthApplicationService {
+ private static final String OFFICIAL_BIND_TOKEN_PREFIX = "official:bind:";
+ private static final Duration OFFICIAL_BIND_TOKEN_TTL = Duration.ofMinutes(10);
+ private static final String OFFICIAL_OPEN_ID_PLACEHOLDER_PREFIX = "OA_PLACEHOLDER_";
+
private final MpUserMapper mpUserMapper;
private final SysDeptMapper sysDeptMapper;
private final WechatMiniGateway wechatMiniGateway;
+ private final WechatOfficialAccountGateway wechatOfficialAccountGateway;
+ private final StringRedisTemplate stringRedisTemplate;
public MiniAuthApplicationService(
MpUserMapper mpUserMapper,
SysDeptMapper sysDeptMapper,
- WechatMiniGateway wechatMiniGateway) {
+ WechatMiniGateway wechatMiniGateway,
+ WechatOfficialAccountGateway wechatOfficialAccountGateway,
+ StringRedisTemplate stringRedisTemplate) {
this.mpUserMapper = mpUserMapper;
this.sysDeptMapper = sysDeptMapper;
this.wechatMiniGateway = wechatMiniGateway;
+ this.wechatOfficialAccountGateway = wechatOfficialAccountGateway;
+ this.stringRedisTemplate = stringRedisTemplate;
+ }
+
+ public String buildOfficialOAuthUrl(String redirectUri, String state) {
+ return wechatOfficialAccountGateway.buildSilentOAuthUrl(redirectUri, state);
}
@Transactional(rollbackFor = Exception.class)
@@ -61,6 +81,45 @@
}
assertUserCanLogin(user);
syncUnionIdIfNeeded(user, session.getUnionId());
+ MiniStpUtil.login(user.getId());
+ return buildResponse(user);
+ }
+
+ @Transactional(rollbackFor = Exception.class)
+ public MiniOfficialLoginResponse loginByOfficialCode(String code) {
+ WxOfficialOAuthResult session = wechatOfficialAccountGateway.exchangeCode(code);
+ MpUser user =
+ mpUserMapper.selectOne(
+ new LambdaQueryWrapper<MpUser>()
+ .eq(MpUser::getOfficialOpenId, session.getOpenId())
+ .eq(MpUser::getDelFlag, 0)
+ .last("limit 1"));
+ if (user == null) {
+ return MiniOfficialLoginResponse.builder()
+ .mobileRequired(true)
+ .bindToken(createOfficialBindToken(session.getOpenId(), session.getUnionId()))
+ .login(null)
+ .build();
+ }
+ assertUserCanLogin(user);
+ syncUnionIdIfNeeded(user, session.getUnionId());
+ MiniStpUtil.login(user.getId());
+ return MiniOfficialLoginResponse.builder()
+ .mobileRequired(false)
+ .bindToken(null)
+ .login(buildResponse(user))
+ .build();
+ }
+
+ @Transactional(rollbackFor = Exception.class)
+ public MiniLoginResponse bindOfficialPhoneAndLogin(String bindToken, String mobileRaw) {
+ OfficialBindSession session = consumeOfficialBindToken(bindToken);
+ String phone = PhoneNormalize.mainland(mobileRaw);
+ if (!phone.matches("^\\d{11}$")) {
+ throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "手机号格式不正确");
+ }
+ MpUser user = resolveUserByPhoneForOfficialLogin(session.openId(), session.unionId(), phone, mobileRaw);
+ assertUserCanLogin(user);
MiniStpUtil.login(user.getId());
return buildResponse(user);
}
@@ -121,6 +180,38 @@
throw new BizException(BizErrorCode.MINI_STAFF_NOT_BOUND, "员工未分配部门");
}
bindOpenIdIfNeeded(user, openId, unionId, phoneRaw);
+ return mpUserMapper.selectById(user.getId());
+ }
+
+ private MpUser resolveUserByPhoneForOfficialLogin(
+ String officialOpenId, String unionId, String normalizedPhone, String phoneRaw) {
+ List<MpUser> users =
+ mpUserMapper.selectList(
+ new LambdaQueryWrapper<MpUser>()
+ .eq(MpUser::getDelFlag, 0));
+ MpUser user =
+ users.stream()
+ .filter(Objects::nonNull)
+ .filter(u -> normalizedPhone.equals(PhoneNormalize.mainland(u.getMobile())))
+ .sorted(this::compareByLoginPriority)
+ .findFirst()
+ .orElse(null);
+ if (user == null) {
+ MpUser created = new MpUser();
+ created.setOpenId(OFFICIAL_OPEN_ID_PLACEHOLDER_PREFIX + UUID.randomUUID());
+ created.setOfficialOpenId(officialOpenId);
+ created.setUnionId(unionId);
+ created.setUserType(MiniUserType.VISITOR);
+ created.setMobile(phoneRaw);
+ created.setStatus(1);
+ mpUserMapper.insert(created);
+ return mpUserMapper.selectById(created.getId());
+ }
+ assertAccountEnabled(user);
+ if (MiniUserType.EMPLOYEE.equals(user.getUserType()) && user.getDeptId() == null) {
+ throw new BizException(BizErrorCode.MINI_STAFF_NOT_BOUND, "员工未分配部门");
+ }
+ bindOfficialOpenIdIfNeeded(user, officialOpenId, unionId, phoneRaw);
return mpUserMapper.selectById(user.getId());
}
@@ -199,6 +290,61 @@
if (needUpdate) {
mpUserMapper.updateById(user);
}
+ }
+
+ private void bindOfficialOpenIdIfNeeded(
+ MpUser user, String officialOpenId, String unionId, String phoneRaw) {
+ if (user == null) {
+ return;
+ }
+ String currentOfficialOpenId = user.getOfficialOpenId();
+ if (StringUtils.hasText(currentOfficialOpenId)
+ && !Objects.equals(currentOfficialOpenId, officialOpenId)) {
+ throw new BizException(BizErrorCode.AUTH_FORBIDDEN, "该手机号已绑定其他微信账号");
+ }
+ boolean needUpdate = false;
+ if (!Objects.equals(currentOfficialOpenId, officialOpenId)) {
+ user.setOfficialOpenId(officialOpenId);
+ needUpdate = true;
+ }
+ if (!StringUtils.hasText(user.getUnionId()) && StringUtils.hasText(unionId)) {
+ user.setUnionId(unionId);
+ needUpdate = true;
+ }
+ if (!StringUtils.hasText(user.getMobile()) && StringUtils.hasText(phoneRaw)) {
+ user.setMobile(phoneRaw);
+ needUpdate = true;
+ }
+ if (needUpdate) {
+ mpUserMapper.updateById(user);
+ }
+ }
+
+ private String createOfficialBindToken(String openId, String unionId) {
+ String token = UUID.randomUUID().toString();
+ String value = openId + "\n" + (unionId == null ? "" : unionId);
+ stringRedisTemplate.opsForValue()
+ .set(OFFICIAL_BIND_TOKEN_PREFIX + token, value, OFFICIAL_BIND_TOKEN_TTL);
+ return token;
+ }
+
+ private OfficialBindSession consumeOfficialBindToken(String bindToken) {
+ if (!StringUtils.hasText(bindToken)) {
+ throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "绑定凭证不能为空");
+ }
+ String key = OFFICIAL_BIND_TOKEN_PREFIX + bindToken.trim();
+ String value = stringRedisTemplate.opsForValue().get(key);
+ if (!StringUtils.hasText(value)) {
+ throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "绑定凭证已失效,请重新登录");
+ }
+ stringRedisTemplate.delete(key);
+ String[] parts = value.split("\\n", 2);
+ String openId = parts.length > 0 ? parts[0] : "";
+ String unionId = parts.length > 1 ? parts[1] : null;
+ if (!StringUtils.hasText(openId)) {
+ throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "绑定凭证无效,请重新登录");
+ }
+ return new OfficialBindSession(openId, StringUtils.hasText(unionId) ? unionId : null);
}
private void assertUserCanLogin(MpUser user) {
@@ -301,17 +447,13 @@
SysDept dept = sysDeptMapper.selectById(user.getDeptId());
deptName = dept == null ? null : dept.getDeptName();
}
- Boolean leader =
- MiniUserType.EMPLOYEE.equals(user.getUserType())
- ? Boolean.TRUE.equals(user.getDeptLeaderFlag())
- : null;
return MiniLoginResponse.builder()
.token(MiniStpUtil.getTokenValue())
.mpUserId(user.getId())
.userType(user.getUserType())
.deptId(user.getDeptId())
.deptName(deptName)
- .deptLeader(leader)
+ .deptLeader(MiniUserType.EMPLOYEE.equals(user.getUserType()) ? false : null)
.avatarUrl(user.getAvatarUrl())
.companyName(user.getCompanyName())
.displayName(name)
@@ -333,4 +475,6 @@
}
return trimmed;
}
+
+ private record OfficialBindSession(String openId, String unionId) {}
}
--
Gitblit v1.7.1