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