| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288 |
- package com.xuekairui.user.service;
- import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
- import com.xuekairui.common.BusinessException;
- import com.xuekairui.common.ErrorCode;
- import com.xuekairui.common.ValidateUtil;
- import com.xuekairui.common.event.ActivityEventRedisService;
- import com.xuekairui.user.dto.ChangePhoneRequest;
- import com.xuekairui.user.dto.DashboardResponse;
- import com.xuekairui.user.dto.ExternalTokenRequest;
- import com.xuekairui.user.dto.LoginResponse;
- import com.xuekairui.user.dto.MiniProgramLoginRequest;
- import com.xuekairui.user.dto.MiniProgramPhoneRequest;
- import com.xuekairui.user.dto.PasswordLoginRequest;
- import com.xuekairui.user.dto.RegisterRequest;
- import com.xuekairui.user.dto.SendCodeRequest;
- import com.xuekairui.user.dto.SetPasswordRequest;
- import com.xuekairui.user.dto.SmsLoginRequest;
- import com.xuekairui.user.dto.UserGateStatus;
- import com.xuekairui.user.dto.UserInfoResponse;
- import com.xuekairui.user.dto.UserLevelResponse;
- import com.xuekairui.user.dto.WechatBindRequest;
- import com.xuekairui.user.dto.WechatLoginRequest;
- import com.xuekairui.user.entity.User;
- import com.xuekairui.user.enums.LicenseStatus;
- import com.xuekairui.user.enums.MembershipLevel;
- import com.xuekairui.user.enums.OperationType;
- import com.xuekairui.user.enums.OperatorRole;
- import com.xuekairui.user.event.UserRegisteredEvent;
- import com.xuekairui.user.mapper.UserMapper;
- import com.xuekairui.user.util.JwtUtil;
- import com.xuekairui.user.util.PasswordUtil;
- import io.jsonwebtoken.Claims;
- import lombok.RequiredArgsConstructor;
- import lombok.extern.slf4j.Slf4j;
- import org.slf4j.MDC;
- import org.springframework.context.ApplicationEventPublisher;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
- import org.springframework.transaction.support.TransactionSynchronization;
- import org.springframework.transaction.support.TransactionSynchronizationManager;
- import java.time.LocalDateTime;
- /**
- * 认证服务
- */
- @Slf4j
- @Service
- @RequiredArgsConstructor
- public class AuthService {
- private final UserMapper userMapper;
- private final VerificationCodeService verificationCodeService;
- private final WechatService wechatService;
- private final JwtUtil jwtUtil;
- private final ApplicationEventPublisher eventPublisher;
- private final PasswordUtil passwordUtil;
- private final OperationAuditLogService auditLogService;
- private final MembershipService membershipService;
- private final AdminAuthService adminAuthService;
- private final com.xuekairui.user.security.TokenVersionCache tokenVersionCache;
- private final WechatMiniProgramService wechatMiniProgramService;
- private final ExternalUserSyncService externalUserSyncService;
- private final CertificateSyncService certificateSyncService;
- private final BusinessLicenseService businessLicenseService;
- private final com.xuekairui.user.mapper.PlatformAccountMapper platformAccountMapper;
- private final com.xuekairui.user.mapper.CrawlerUsageLogMapper crawlerUsageLogMapper;
- private final com.xuekairui.user.mapper.SearchRecordMapper searchRecordMapper;
- private final com.xuekairui.user.mapper.PurchaseIntentMapper purchaseIntentMapper;
- private final ActivityEventRedisService activityEventRedisService;
- /**
- * 发送验证码
- */
- public void sendVerificationCode(SendCodeRequest request, String ip) {
- verificationCodeService.sendCode(request.getPhone(), request.getScene(), ip);
- }
- /**
- * 验证码登录/注册
- * 如果手机号已注册则登录,否则自动注册
- */
- @Transactional
- public LoginResponse smsLogin(SmsLoginRequest request, String ip) {
- // 1. 验证验证码(兼容 scene=login 和 scene=register,统一用 login key)
- verificationCodeService.verifyCode(request.getPhone(), request.getCode());
- // 2. 查找或创建用户
- User user = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getPhone, request.getPhone()));
- boolean isNewUser = false;
- if (user == null) {
- isNewUser = true;
- // 自动注册新用户
- user = User.builder()
- .phone(request.getPhone())
- .nickname("用户" + request.getPhone().substring(7))
- .membershipLevel(MembershipLevel.PLUS)
- .loginSource("SMS")
- .status(1)
- .build();
- userMapper.insert(user);
- // 注册即写入 PLUS 会员记录
- membershipService.initPlusMembership(user.getId());
- // 发布用户注册事件(邀请模块监听处理邀请码,赠送会员奖励)
- eventPublisher.publishEvent(new UserRegisteredEvent(this, user.getId(), request.getInviteCode()));
- // 普通会员(PLUS)免费开放,注册即拥有,无需额外发放
- }
- // 3. 更新登录信息
- user.setLastLoginTime(LocalDateTime.now());
- user.setLastLoginIp(ip);
- user.setLoginSource("PC");
- userMapper.updateById(user);
- // 4. 生成Token
- LoginResponse response = buildLoginResponse(user);
- response.setUserId(user.getId());
- response.setNewUser(isNewUser);
- // 记录审计日志
- if (isNewUser) {
- auditLogService.logSuccess(user.getId(), user.getNickname(), OperatorRole.USER.getCode(),
- OperationType.USER_REGISTER, "USER:" + user.getId(), user.getId());
- }
- auditLogService.logSuccess(user.getId(), user.getNickname(), OperatorRole.USER.getCode(),
- OperationType.USER_LOGIN, "USER:" + user.getId(), user.getId());
- return response;
- }
- /**
- * 微信扫码登录
- */
- @Transactional
- public LoginResponse wechatLogin(WechatLoginRequest request, String ip) {
- // 1. 获取微信用户信息
- WechatService.WechatUserInfo wechatInfo = wechatService.getUserInfo(request.getCode());
- // 2. 通过UnionID查找用户
- User user = null;
- if (wechatInfo.getUnionId() != null) {
- user = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getWechatUnionId, wechatInfo.getUnionId()));
- }
- // 3. 如果用户存在且已绑定手机号,直接登录
- if (user != null && user.getPhone() != null) {
- user.setLastLoginTime(LocalDateTime.now());
- user.setLastLoginIp(ip);
- user.setLoginSource("PC");
- userMapper.updateById(user);
- return buildLoginResponse(user);
- }
- // 4. 如果用户存在但未绑定手机号,或新用户,返回需要绑定手机号
- if (user == null) {
- // 创建新用户(未绑定手机号)
- user = User.builder()
- .wechatOpenId(wechatInfo.getOpenId())
- .wechatUnionId(wechatInfo.getUnionId())
- .nickname(wechatInfo.getNickname())
- .avatar(wechatInfo.getHeadImgUrl())
- .membershipLevel(MembershipLevel.PLUS)
- .loginSource("WECHAT")
- .status(1)
- .build();
- userMapper.insert(user);
- membershipService.initPlusMembership(user.getId());
- } else {
- // 更新微信信息
- user.setWechatOpenId(wechatInfo.getOpenId());
- user.setWechatUnionId(wechatInfo.getUnionId());
- if (user.getNickname() == null) {
- user.setNickname(wechatInfo.getNickname());
- }
- if (user.getAvatar() == null) {
- user.setAvatar(wechatInfo.getHeadImgUrl());
- }
- user.setLastLoginTime(LocalDateTime.now());
- user.setLastLoginIp(ip);
- userMapper.updateById(user);
- }
- // 5. 生成临时Token,要求绑定手机号
- String tempToken = jwtUtil.generateTempToken(wechatInfo.getOpenId(), wechatInfo.getUnionId());
- // 确保 MDC 中有 uid(微信登录临时Token场景不走 buildLoginResponse)
- MDC.put("uid", String.valueOf(user.getId()));
- return LoginResponse.builder()
- .needBindPhone(true)
- .tempToken(tempToken)
- .userInfo(convertToUserInfoResponse(user))
- .build();
- }
- /**
- * 微信绑定手机号
- */
- @Transactional
- public LoginResponse wechatBindPhone(WechatBindRequest request, String ip) {
- // 1. 验证临时Token
- Claims claims = jwtUtil.parseToken(request.getTempToken());
- String tokenType = claims.get("type", String.class);
- if (!"temp".equals(tokenType)) {
- throw new BusinessException(ErrorCode.TOKEN_INVALID);
- }
- String openId = claims.get("openId", String.class);
- String unionId = claims.get("unionId", String.class);
- // 2. 验证验证码
- verificationCodeService.verifyCode(request.getPhone(), request.getCode());
- // 3. 检查手机号是否已被其他用户绑定
- User existUser = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getPhone, request.getPhone()));
- User wechatUser = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getWechatOpenId, openId));
- if (wechatUser == null) {
- throw new BusinessException(ErrorCode.WECHAT_LOGIN_FAILED);
- }
- if (existUser != null && !existUser.getId().equals(wechatUser.getId())) {
- // 手机号已被其他账号绑定,合并账号
- existUser.setWechatOpenId(openId);
- existUser.setWechatUnionId(unionId);
- existUser.setLastLoginTime(LocalDateTime.now());
- existUser.setLastLoginIp(ip);
- existUser.setLoginSource("PC");
- userMapper.updateById(existUser);
- // 禁用旧的微信用户(不物理删除,仅改状态)
- wechatUser.setStatus(0);
- userMapper.updateById(wechatUser);
- wechatUser = existUser;
- } else if (existUser == null) {
- // 绑定手机号到微信用户(首次完成注册)
- wechatUser.setPhone(request.getPhone());
- wechatUser.setLoginSource("PC");
- wechatUser.setLastLoginTime(LocalDateTime.now());
- wechatUser.setLastLoginIp(ip);
- userMapper.updateById(wechatUser);
- // 发布用户注册事件,邀请模块可据此建立邀请关系并发放奖励
- eventPublisher.publishEvent(new UserRegisteredEvent(this, wechatUser.getId(), request.getInviteCode()));
- // 普通会员(PLUS)免费开放,注册即拥有,无需额外发放
- }
- // 4. 生成正式Token
- return buildLoginResponse(wechatUser);
- }
- /**
- * 外部系统换取小程序 Token(供小程序后台按手机号签发)
- * <p>查找或自动注册用户,不递增 tokenVersion,不踢 PC。
- */
- @Transactional
- public LoginResponse externalMiniAppToken(ExternalTokenRequest request, String ip) {
- User user = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getPhone, request.getPhone()));
- boolean isNewUser = false;
- if (user == null) {
- isNewUser = true;
- // 优先级:nickname > username > 手机号生成
- String nickname = (request.getNickname() != null && !request.getNickname().isBlank())
- ? request.getNickname()
- : (request.getUsername() != null && !request.getUsername().isBlank())
- ? request.getUsername()
- : "用户" + request.getPhone().substring(request.getPhone().length() - 4);
- user = User.builder()
- .phone(request.getPhone())
- .nickname(nickname)
- .userName(request.getUsername())
- .wechatMiniOpenId(request.getOpenId())
- .wechatUnionId(request.getUnionId())
- .pharmacyName(request.getPharmacyName())
- .province(request.getProvince())
- .city(request.getCity())
- .district(request.getDistrict())
- .pharmacyAddress(request.getPharmacyAddress())
- .contactPerson(request.getContactPerson())
- .contactPhone(request.getContactPhone())
- .businessLicenseNo(request.getBusinessLicenseNo())
- .membershipLevel(MembershipLevel.PLUS)
- .loginSource("MINIAPP")
- .status(1)
- .build();
- userMapper.insert(user);
- membershipService.initPlusMembership(user.getId());
- if (request.getInviteCode() != null) {
- eventPublisher.publishEvent(
- new UserRegisteredEvent(this, user.getId(), request.getInviteCode()));
- }
- } else {
- // 已存在用户:更新第三方传入的信息(非空字段才覆盖,避免清空已有数据)
- // userName 独立存储原始用户名;nickname 优先取 nickname,其次 username
- if (request.getUsername() != null && !request.getUsername().isBlank()) {
- user.setUserName(request.getUsername());
- }
- if (request.getNickname() != null && !request.getNickname().isBlank()) {
- user.setNickname(request.getNickname());
- } else if (request.getUsername() != null && !request.getUsername().isBlank()) {
- user.setNickname(request.getUsername());
- }
- if (request.getPharmacyName() != null && !request.getPharmacyName().isBlank()) {
- user.setPharmacyName(request.getPharmacyName());
- }
- if (request.getProvince() != null && !request.getProvince().isBlank()) {
- user.setProvince(request.getProvince());
- }
- if (request.getCity() != null && !request.getCity().isBlank()) {
- user.setCity(request.getCity());
- }
- if (request.getDistrict() != null && !request.getDistrict().isBlank()) {
- user.setDistrict(request.getDistrict());
- }
- if (request.getPharmacyAddress() != null && !request.getPharmacyAddress().isBlank()) {
- user.setPharmacyAddress(request.getPharmacyAddress());
- }
- if (request.getContactPerson() != null && !request.getContactPerson().isBlank()) {
- user.setContactPerson(request.getContactPerson());
- }
- if (request.getContactPhone() != null && !request.getContactPhone().isBlank()) {
- user.setContactPhone(request.getContactPhone());
- }
- if (request.getBusinessLicenseNo() != null && !request.getBusinessLicenseNo().isBlank()) {
- user.setBusinessLicenseNo(request.getBusinessLicenseNo());
- }
- if (request.getOpenId() != null && !request.getOpenId().isBlank()) {
- user.setWechatMiniOpenId(request.getOpenId());
- }
- if (request.getUnionId() != null && !request.getUnionId().isBlank()) {
- user.setWechatUnionId(request.getUnionId());
- }
- }
- user.setLoginSource("MINIAPP");
- user.setLastLoginTime(LocalDateTime.now());
- user.setLastLoginIp(ip);
- userMapper.updateById(user);
- // 同步入驻信息到 t_business_license(仅当传入入驻相关字段时)
- syncBusinessLicense(request, user.getId());
- LoginResponse response = buildMiniAppLoginResponse(user);
- response.setNewUser(isNewUser);
- auditLogService.logSuccess(user.getId(), user.getNickname(), OperatorRole.USER.getCode(),
- isNewUser ? OperationType.USER_REGISTER : OperationType.USER_LOGIN,
- "USER:" + user.getId(), user.getId());
- return response;
- }
- public LoginResponse heartbeat(Long userId, String role, String loginSource) {
- String roleCode = role != null ? role : "USER";
- Long currentVersion = tokenVersionCache.getVersion(userId, false);
- if (currentVersion == null) {
- User user = userMapper.selectById(userId);
- currentVersion = user != null && user.getTokenVersion() != null
- ? user.getTokenVersion() : 0L;
- if (user != null) {
- tokenVersionCache.putVersion(userId, false, currentVersion);
- }
- }
- String newAccessToken = jwtUtil.generateAccessToken(
- userId, roleCode, loginSource, currentVersion);
- String newRefreshToken = jwtUtil.generateRefreshToken(userId, loginSource);
- User updateUser = new User();
- updateUser.setId(userId);
- updateUser.setRefreshTokenHash(JwtUtil.hashToken(newRefreshToken));
- updateUser.setRefreshTokenStatus("ACTIVE");
- userMapper.updateById(updateUser);
- return LoginResponse.builder()
- .userId(userId)
- .accessToken(newAccessToken)
- .refreshToken(newRefreshToken)
- .expiresIn(jwtUtil.getExpirationInSeconds(roleCode, loginSource))
- .needBindPhone(false)
- .role(roleCode)
- .build();
- }
- /**
- * 获取用户信息
- */
- public UserInfoResponse getUserInfo(Long userId) {
- User user = userMapper.selectById(userId);
- if (user == null) {
- throw new BusinessException(ErrorCode.USER_NOT_FOUND);
- }
- return convertToUserInfoResponse(user);
- }
- /**
- * 获取聚合用户信息(小程序"我的"页面)— 新增,不影响 /api/auth/user/info
- * <p>返回 user + gate(门禁)+ quota(配额)+ platformBindings(平台绑定)。
- */
- public java.util.Map<String, Object> getUserMe(Long userId) {
- UserInfoResponse userInfo = getUserInfo(userId);
- UserGateStatus gate = buildGateStatus(userId);
- java.util.Map<String, Object> quota = buildQuotaSummary(userId);
- java.util.List<?> platforms = getPlatformBindings(userId);
- java.util.Map<String, Object> result = new java.util.LinkedHashMap<>(toMap(userInfo));
- result.put("gate", gate);
- result.put("quota", quota);
- result.put("platformBindings", platforms);
- return result;
- }
- /**
- * 首页概览统计 — 新增
- */
- public DashboardResponse getDashboard(Long userId) {
- // 今日查询次数
- int todayQueryCount = crawlerUsageLogMapper.sumTodayUsage(userId);
- // 累计查询次数
- int totalQueryCount = crawlerUsageLogMapper.sumTotalUsage(userId);
- // 最近查询摘要(最多 3 条)
- java.util.List<DashboardResponse.RecentQueryItem> recent =
- searchRecordMapper.selectRecentByUserId(userId, 3)
- .stream().map(r -> DashboardResponse.RecentQueryItem.builder()
- .drugName(r.getDrugName())
- .searchedAt(r.getSearchedAt() != null ? r.getSearchedAt().toString() : null)
- .lowestPrice(r.getQuoteCount() > 0 ? "有报价" : null)
- .build())
- .toList();
- return DashboardResponse.builder()
- .todayQueryCount(todayQueryCount)
- .todayPurchaseIntentCount(
- purchaseIntentMapper.countTodayByUserId(userId, java.time.LocalDate.now()))
- .totalQueryCount(totalQueryCount)
- .recentQueries(recent)
- .build();
- }
- /**
- * 登出 — 新增,清除 refresh token
- */
- @Transactional
- public void logout(Long userId) {
- User user = new User();
- user.setId(userId);
- user.setRefreshTokenHash(null);
- user.setRefreshTokenStatus(null);
- userMapper.updateById(user);
- log.info("用户 {} 已登出", userId);
- }
- /**
- * 独立注册(小程序填写药店信息后调用) — 新增
- */
- @Transactional
- public LoginResponse register(RegisterRequest request, String ip) {
- // 1. 验证临时 Token
- io.jsonwebtoken.Claims claims = jwtUtil.parseToken(request.getTempToken());
- if (!"temp".equals(claims.get("type", String.class))) {
- throw new BusinessException(ErrorCode.TOKEN_INVALID);
- }
- String openId = claims.get("openId", String.class);
- String unionId = claims.get("unionId", String.class);
- // 2. 查找小程序用户(由 miniappUnifiedLogin 创建的未完成注册的用户)
- User user = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getWechatMiniOpenId, openId));
- if (user == null) {
- // fallback: try unionId
- if (unionId != null) {
- user = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getWechatUnionId, unionId));
- }
- }
- if (user == null) {
- throw new BusinessException(ErrorCode.USER_NOT_FOUND, "请先通过小程序授权登录");
- }
- // 3. 补全药店信息
- user.setPharmacyName(request.getPharmacyName());
- user.setProvince(request.getProvince());
- user.setCity(request.getCity());
- user.setDistrict(request.getDistrict());
- user.setLastLoginTime(LocalDateTime.now());
- user.setLastLoginIp(ip);
- user.setLoginSource("MINIAPP");
- userMapper.updateById(user);
- // 4. 发布注册事件(处理邀请码)
- if (request.getInviteCode() != null) {
- eventPublisher.publishEvent(
- new UserRegisteredEvent(this, user.getId(), request.getInviteCode()));
- }
- // 5. 签发正式 Token
- LoginResponse response = buildMiniAppLoginResponse(user);
- auditLogService.logSuccess(user.getId(), user.getNickname(), OperatorRole.USER.getCode(),
- OperationType.USER_REGISTER, "USER:" + user.getId(), user.getId());
- return response;
- }
- // ====================================
- // 聚合查询辅助方法
- // ====================================
- /**
- * 同步第三方传入的入驻信息到 t_business_license
- * 仅当请求中至少有一个入驻相关字段非空时才执行
- */
- private void syncBusinessLicense(ExternalTokenRequest request, Long userId) {
- String storeName = (request.getStoreName() != null && !request.getStoreName().isBlank())
- ? request.getStoreName() : request.getPharmacyName();
- boolean hasLicenseField = isNotBlank(storeName)
- || isNotBlank(request.getTerminalType())
- || isNotBlank(request.getBusinessLicenseUrl())
- || isNotBlank(request.getDrugLicenseUrl())
- || isNotBlank(request.getMedicalDeviceClass2Url())
- || isNotBlank(request.getMedicalDeviceClass3Url());
- if (!hasLicenseField) {
- return;
- }
- com.xuekairui.user.entity.BusinessLicense existing =
- businessLicenseService.getByUserId(userId);
- // 审核状态:0-审核中→PENDING / 1-审核成功→APPROVED / 2-审核失败→REJECTED(不传默认0)
- Integer status = request.getStatus();
- String reviewStatus;
- if (status == null || status == 0) {
- reviewStatus = LicenseStatus.PENDING.name();
- } else if (status == 1) {
- reviewStatus = LicenseStatus.APPROVED.name();
- } else {
- reviewStatus = LicenseStatus.REJECTED.name();
- }
- if (existing == null) {
- com.xuekairui.user.entity.BusinessLicense license =
- com.xuekairui.user.entity.BusinessLicense.builder()
- .userId(userId)
- .storeName(storeName)
- .terminalType(request.getTerminalType())
- .province(request.getProvince())
- .city(request.getCity())
- .district(request.getDistrict())
- .storeAddress(request.getPharmacyAddress())
- .contactPerson(request.getContactPerson())
- .contactPhone(request.getContactPhone())
- .licenseImageUrl(request.getBusinessLicenseUrl())
- .drugLicenseUrl(request.getDrugLicenseUrl())
- .creditCode(request.getBusinessLicenseNo())
- .medicalDeviceClass2Url(request.getMedicalDeviceClass2Url())
- .medicalDeviceClass3Url(request.getMedicalDeviceClass3Url())
- .reviewStatus(reviewStatus)
- .showVerifiedBadge(false)
- .syncSource("MINIAPP_SYNC")
- .build();
- businessLicenseService.insert(license);
- log.info("第三方用户{}同步入驻信息,创建新记录: licenseId={}, status={}",
- userId, license.getId(), reviewStatus);
- // 第三方已审核通过:赠送30天高级会员 + 推送Redis事件
- if (LicenseStatus.APPROVED.name().equals(reviewStatus)) {
- grantLicenseApprovedReward(userId, license.getId());
- }
- } else {
- if (storeName != null && !storeName.isBlank()) {
- existing.setStoreName(storeName);
- }
- if (isNotBlank(request.getTerminalType())) {
- existing.setTerminalType(request.getTerminalType());
- }
- if (isNotBlank(request.getProvince())) {
- existing.setProvince(request.getProvince());
- }
- if (isNotBlank(request.getCity())) {
- existing.setCity(request.getCity());
- }
- if (isNotBlank(request.getDistrict())) {
- existing.setDistrict(request.getDistrict());
- }
- if (isNotBlank(request.getPharmacyAddress())) {
- existing.setStoreAddress(request.getPharmacyAddress());
- }
- if (isNotBlank(request.getContactPerson())) {
- existing.setContactPerson(request.getContactPerson());
- }
- if (isNotBlank(request.getContactPhone())) {
- existing.setContactPhone(request.getContactPhone());
- }
- if (isNotBlank(request.getBusinessLicenseUrl())) {
- existing.setLicenseImageUrl(request.getBusinessLicenseUrl());
- }
- if (isNotBlank(request.getDrugLicenseUrl())) {
- existing.setDrugLicenseUrl(request.getDrugLicenseUrl());
- }
- if (isNotBlank(request.getBusinessLicenseNo())) {
- existing.setCreditCode(request.getBusinessLicenseNo());
- }
- if (isNotBlank(request.getMedicalDeviceClass2Url())) {
- existing.setMedicalDeviceClass2Url(request.getMedicalDeviceClass2Url());
- }
- if (isNotBlank(request.getMedicalDeviceClass3Url())) {
- existing.setMedicalDeviceClass3Url(request.getMedicalDeviceClass3Url());
- }
- // 已 APPROVED 的不降级(不覆盖);非 APPROVED 状态可随第三方 status 更新(含 PENDING/REJECTED 互转、升级到 APPROVED)
- boolean upgradedToApproved = false;
- if (!LicenseStatus.APPROVED.name().equals(existing.getReviewStatus())) {
- existing.setReviewStatus(reviewStatus);
- upgradedToApproved = LicenseStatus.APPROVED.name().equals(reviewStatus);
- }
- existing.setSyncSource("MINIAPP_SYNC");
- businessLicenseService.updateById(existing);
- // 从非APPROVED升级到APPROVED:赠送30天高级会员 + 推送Redis事件
- if (upgradedToApproved) {
- grantLicenseApprovedReward(userId, existing.getId());
- }
- }
- }
- private boolean isNotBlank(String s) {
- return s != null && !s.isBlank();
- }
- /**
- * 首次入驻审核通过:赠送30天高级会员 + 推送Redis事件供活动模块消费
- * <p>与 BusinessLicenseService.approve() 的奖励逻辑保持一致。
- */
- private void grantLicenseApprovedReward(Long userId, Long licenseId) {
- membershipService.grantOrExtendMembership(
- userId,
- MembershipLevel.PRO,
- 30,
- "LICENSE",
- "入驻信息审核通过赠送30天高级会员");
- log.info("入驻信息审核通过(第三方同步),赠送用户{} 30天高级会员", userId);
- // 事务提交后推送 Redis 事件,供活动模块消费(邀请有礼等)
- TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
- @Override
- public void afterCommit() {
- try {
- activityEventRedisService.pushLicenseApproved(userId, licenseId, "BUSINESS_LICENSE");
- } catch (Exception e) {
- log.warn("存入入驻审核通过事件失败,不影响主流程: userId={}", userId, e);
- }
- }
- });
- }
- private UserGateStatus buildGateStatus(Long userId) {
- // 执照审核状态
- com.xuekairui.user.entity.BusinessLicense license =
- businessLicenseService.getByUserId(userId);
- String licenseStatus = license != null ? license.getReviewStatus() : LicenseStatus.NOT_SUBMITTED.name();
- // 已绑定平台数
- Long boundCount = platformAccountMapper.selectCount(
- new LambdaQueryWrapper<com.xuekairui.user.entity.PlatformAccount>()
- .eq(com.xuekairui.user.entity.PlatformAccount::getUserId, userId)
- .eq(com.xuekairui.user.entity.PlatformAccount::getEnabled, true));
- boolean canQuery = LicenseStatus.APPROVED.name().equals(licenseStatus) && boundCount >= 1;
- java.util.List<String> messages = new java.util.ArrayList<>();
- if (!LicenseStatus.APPROVED.name().equals(licenseStatus)) {
- messages.add("请先提交入驻信息并通过审核");
- }
- if (boundCount == 0) {
- messages.add("请至少绑定1个B2B平台账号");
- }
- return UserGateStatus.builder()
- .canQueryOnDesktop(canQuery)
- .licenseStatus(licenseStatus)
- .platformBoundCount(boundCount.intValue())
- .gateMessages(messages)
- .build();
- }
- private java.util.Map<String, Object> buildQuotaSummary(Long userId) {
- // 今日已用 + 每日上限
- int dailyUsed = crawlerUsageLogMapper.sumTodayUsage(userId);
- java.util.Map<String, Object> quota = new java.util.LinkedHashMap<>();
- quota.put("dailyQueryUsed", dailyUsed);
- quota.put("dailyQueryLimit", 20); // TODO: 从会员等级动态获取
- return quota;
- }
- private java.util.List<java.util.Map<String, Object>> getPlatformBindings(Long userId) {
- java.util.List<com.xuekairui.user.entity.PlatformAccount> accounts =
- platformAccountMapper.selectList(
- new LambdaQueryWrapper<com.xuekairui.user.entity.PlatformAccount>()
- .eq(com.xuekairui.user.entity.PlatformAccount::getUserId, userId));
- return accounts.stream().map(a -> {
- java.util.Map<String, Object> m = new java.util.LinkedHashMap<>();
- m.put("platformCode", a.getPlatformCode());
- m.put("platformName", a.getPlatformName());
- m.put("status", Boolean.TRUE.equals(a.getEnabled()) ? "bound" : "disabled");
- m.put("lastVerifyTime", a.getLastVerifyTime());
- return m;
- }).toList();
- }
- private java.util.Map<String, Object> toMap(UserInfoResponse info) {
- // 简单反射转 Map,避免引入额外依赖
- java.util.Map<String, Object> map = new java.util.LinkedHashMap<>();
- map.put("id", info.getId());
- map.put("phone", info.getPhone());
- map.put("nickname", info.getNickname());
- map.put("avatar", info.getAvatar());
- map.put("wechatBound", info.getWechatBound());
- map.put("createTime", info.getCreateTime());
- map.put("loginSource", info.getLoginSource());
- map.put("pharmacyName", info.getPharmacyName());
- map.put("province", info.getProvince());
- map.put("city", info.getCity());
- map.put("district", info.getDistrict());
- map.put("role", info.getRole());
- map.put("membershipLevel", info.getMembershipLevel());
- map.put("membershipLevelName", info.getMembershipLevelName());
- map.put("inviterId", info.getInviterId());
- map.put("inviterNickname", info.getInviterNickname());
- map.put("hasBoundInviteCode", info.getHasBoundInviteCode());
- map.put("licenseStatus", info.getLicenseStatus());
- map.put("levelCode", info.getLevelCode());
- map.put("version", info.getVersion());
- return map;
- }
- /**
- * Refresh Token 旋转 + 重用检测
- * <p>
- * 每次刷新:旧 Refresh Token 标记为 USED,签发新的一对 Token。
- * 如果检测到已 USED 的 Refresh Token 被再次使用 → 说明被盗 → 踢出所有设备。
- */
- @Transactional
- public LoginResponse refreshToken(String refreshToken) {
- // 1. 验证JWT格式
- String tokenType = jwtUtil.getTokenType(refreshToken);
- if (!"refresh".equals(tokenType)) {
- throw new BusinessException(ErrorCode.REFRESH_TOKEN_INVALID);
- }
- Long userId = jwtUtil.getUserId(refreshToken);
- // 只查 Refresh Token 校验所需的字段,减少DB开销
- User user = userMapper.selectOne(
- new LambdaQueryWrapper<User>()
- .select(User::getId, User::getPhone,
- User::getTokenVersion, User::getRefreshTokenHash,
- User::getRefreshTokenStatus, User::getStatus)
- .eq(User::getId, userId));
- if (user == null) {
- throw new BusinessException(ErrorCode.USER_NOT_FOUND);
- }
- // 2. 计算哈希,比对DB中的记录
- String tokenHash = JwtUtil.hashToken(refreshToken);
- String storedHash = user.getRefreshTokenHash();
- String storedStatus = user.getRefreshTokenStatus();
- // 3. 重用检测:Refresh Token 已被使用过 → 被盗!
- if (tokenHash.equals(storedHash) && "USED".equals(storedStatus)) {
- log.warn("用户 {} 的Refresh Token被重用(疑似被盗),踢出所有设备", userId);
- // 递增 tokenVersion,使所有旧 access token 立即失效
- long newVersion = (user.getTokenVersion() != null ? user.getTokenVersion() : 0L) + 1;
- user.setTokenVersion(newVersion);
- user.setRefreshTokenHash(null);
- user.setRefreshTokenStatus(null);
- userMapper.updateById(user);
- tokenVersionCache.putVersion(userId, false, newVersion);
- throw new BusinessException(ErrorCode.TOKEN_KICKED);
- }
- // 4. 校验:哈希不匹配 → 无效或过期的 Refresh Token
- if (!tokenHash.equals(storedHash) || !"ACTIVE".equals(storedStatus)) {
- throw new BusinessException(ErrorCode.REFRESH_TOKEN_INVALID);
- }
- // 5. 旋转:标记旧 Refresh Token 为 USED,签发新的一对
- user.setRefreshTokenStatus("USED");
- String roleCode = "USER";
- long currentVersion = user.getTokenVersion() != null ? user.getTokenVersion() : 0L;
- String loginSource = jwtUtil.getLoginSource(refreshToken);
- String newAccessToken = jwtUtil.generateAccessToken(
- user.getId(), roleCode, loginSource, currentVersion);
- String newRefreshToken = jwtUtil.generateRefreshToken(user.getId(), loginSource);
- // 存储新 Refresh Token 哈希
- user.setRefreshTokenHash(JwtUtil.hashToken(newRefreshToken));
- user.setRefreshTokenStatus("ACTIVE");
- userMapper.updateById(user);
- log.debug("用户 {} Refresh Token 旋转成功", userId);
- return LoginResponse.builder()
- .userId(user.getId())
- .accessToken(newAccessToken)
- .refreshToken(newRefreshToken)
- .expiresIn(jwtUtil.getExpirationInSeconds(roleCode, loginSource))
- .userInfo(convertToUserInfoResponse(user))
- .needBindPhone(false)
- .newUser(false)
- .role(roleCode)
- .build();
- }
- /** PC 端构建登录响应:
- * 递增 tokenVersion,踢掉旧 PC */
- private LoginResponse buildLoginResponse(User user) {
- return buildLoginResponseInternal(user, true, "PC");
- }
- /** 小程序构建登录响应:不递增 tokenVersion,不踢 PC */
- private LoginResponse buildMiniAppLoginResponse(User user) {
- return buildLoginResponseInternal(user, false, "MINIAPP");
- }
- private LoginResponse buildLoginResponseInternal(User user, boolean incrementVersion, String loginSource) {
- MDC.put("uid", String.valueOf(user.getId()));
- String roleCode = "USER";
- long version;
- if (incrementVersion) {
- long oldVersion = user.getTokenVersion() != null ? user.getTokenVersion() : 0L;
- version = oldVersion + 1;
- user.setTokenVersion(version);
- tokenVersionCache.putVersion(user.getId(), false, version);
- } else {
- version = user.getTokenVersion() != null ? user.getTokenVersion() : 0L;
- }
- String accessToken = jwtUtil.generateAccessToken(
- user.getId(), "USER", loginSource, version);
- String refreshToken = jwtUtil.generateRefreshToken(user.getId(), loginSource);
- user.setRefreshTokenHash(JwtUtil.hashToken(refreshToken));
- user.setRefreshTokenStatus("ACTIVE");
- userMapper.updateById(user);
- return LoginResponse.builder()
- .userId(user.getId())
- .accessToken(accessToken)
- .refreshToken(refreshToken)
- .expiresIn(jwtUtil.getExpirationInSeconds(roleCode, loginSource))
- .userInfo(convertToUserInfoResponse(user))
- .needBindPhone(false)
- .newUser(false)
- .role(roleCode)
- .build();
- }
- /**
- * 转换用户信息响应
- */
- private UserInfoResponse convertToUserInfoResponse(User user) {
- UserLevelResponse levelInfo = null;
- if (user != null && user.getId() != null) {
- try {
- levelInfo = membershipService.getUserLevel(user.getId());
- } catch (Exception e) {
- log.warn("获取用户等级信息失败: userId={}, error={}", user.getId(), e.getMessage());
- }
- }
- // 营业执照审核状态
- String licenseStatus = LicenseStatus.NOT_SUBMITTED.name();
- try {
- assert user != null;
- com.xuekairui.user.entity.BusinessLicense license =
- businessLicenseService.getByUserId(user.getId());
- if (license != null && license.getReviewStatus() != null) {
- licenseStatus = license.getReviewStatus();
- }
- } catch (Exception e) {
- log.warn("查询营业执照状态失败: userId={}, error={}", user.getId(), e.getMessage());
- }
- // 邀请人信息
- Long inviterId = user.getInviterId();
- String inviterNickname = null;
- if (inviterId != null) {
- User inviter = userMapper.selectById(inviterId);
- if (inviter != null) {
- inviterNickname = inviter.getWechatNickname() != null ? inviter.getWechatNickname()
- : inviter.getNickname() != null ? inviter.getNickname()
- : "一位用户";
- }
- }
- return UserInfoResponse.builder()
- .id(user.getId())
- .phone(ValidateUtil.maskPhone(user.getPhone()))
- .nickname(user.getNickname())
- .userName(user.getUserName())
- .avatar(user.getAvatar())
- .wechatBound(user.getWechatOpenId() != null)
- .levelInfo(levelInfo)
- .createTime(user.getCreateTime())
- .loginSource(user.getLoginSource() != null ? user.getLoginSource() : "UNKNOWN")
- .pharmacyName(user.getPharmacyName())
- .province(user.getProvince())
- .city(user.getCity())
- .district(user.getDistrict())
- .role("USER")
- .membershipLevel(levelInfo != null ? levelInfo.getLevelCode() : "PLUS")
- .membershipLevelName(levelInfo != null ? levelInfo.getLevelName() : "普通会员")
- .inviterId(inviterId)
- .inviterNickname(inviterNickname)
- .hasBoundInviteCode(inviterId != null)
- .licenseStatus(licenseStatus)
- .build();
- }
- /**
- * 密码登录
- * 支持手机号或用户名登录:phone 和 username 至少填一个
- */
- @Transactional
- public LoginResponse passwordLogin(PasswordLoginRequest request, String ip) {
- // 1. 校验:phone 和 username 至少填一个
- boolean hasPhone = request.getPhone() != null && !request.getPhone().isBlank();
- boolean hasUsername = request.getUsername() != null && !request.getUsername().isBlank();
- if (!hasPhone && !hasUsername) {
- throw new BusinessException(ErrorCode.PARAM_ERROR, "手机号和用户名至少填写一个");
- }
- // 2. 查找用户(优先手机号,其次用户名/昵称)
- User user;
- if (hasPhone) {
- user = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getPhone, request.getPhone()));
- } else {
- user = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getNickname, request.getUsername()));
- }
- if (user == null) {
- return adminAuthService.passwordLogin(
- hasPhone ? request.getPhone() : request.getUsername(),
- request.getPassword(), ip);
- }
- // 2. 检查是否设置了密码
- if (user.getPassword() == null) {
- throw new BusinessException(ErrorCode.PASSWORD_NOT_SET);
- }
- // 3. 验证密码
- if (!passwordUtil.matches(request.getPassword(), user.getPassword())) {
- throw new BusinessException(ErrorCode.PASSWORD_INCORRECT);
- }
- // 4. 检查用户状态
- if (user.getStatus() != null && user.getStatus() == 0) {
- throw new BusinessException(ErrorCode.USER_DISABLED);
- }
- // 5. 更新登录信息
- user.setLastLoginTime(LocalDateTime.now());
- user.setLastLoginIp(ip);
- user.setLoginSource("PC");
- userMapper.updateById(user);
- // 6. 生成Token
- LoginResponse response = buildLoginResponse(user);
- auditLogService.logSuccess(user.getId(), user.getNickname(), OperatorRole.USER.getCode(),
- OperationType.USER_LOGIN, "USER:" + user.getId(), user.getId());
- return response;
- }
- /**
- * 设置/修改密码
- */
- @Transactional
- public void setPassword(Long userId, SetPasswordRequest request) {
- User user = userMapper.selectById(userId);
- if (user == null) {
- throw new BusinessException(ErrorCode.USER_NOT_FOUND);
- }
- // 如果已有密码,需要验证旧密码
- if (user.getPassword() != null && request.getOldPassword() != null) {
- if (!passwordUtil.matches(request.getOldPassword(), user.getPassword())) {
- throw new BusinessException(ErrorCode.OLD_PASSWORD_INCORRECT);
- }
- } else if (user.getPassword() != null) {
- throw new BusinessException(ErrorCode.OLD_PASSWORD_REQUIRED);
- }
- // 加密并更新密码
- user.setPassword(passwordUtil.encode(request.getNewPassword()));
- userMapper.updateById(user);
- log.info("用户{}设置/修改密码成功", userId);
- auditLogService.logSuccess(userId, user.getNickname(), OperatorRole.USER.getCode(),
- OperationType.USER_UPDATE, "PHONE:" + userId, userId);
- }
- /**
- * 修改手机号
- */
- @Transactional
- public void changePhone(Long userId, ChangePhoneRequest request) {
- User user = userMapper.selectById(userId);
- if (user == null) {
- throw new BusinessException(ErrorCode.USER_NOT_FOUND);
- }
- // 1. 验证验证码(使用新手机号和change场景)
- verificationCodeService.verifyCode(request.getNewPhone(), request.getCode());
- // 2. 检查新手机号是否已被其他用户占用
- User existUser = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getPhone, request.getNewPhone()));
- if (existUser != null && !existUser.getId().equals(userId)) {
- throw new BusinessException(ErrorCode.PHONE_ALREADY_EXISTS);
- }
- // 3. 更新手机号
- String oldPhone = user.getPhone();
- user.setPhone(request.getNewPhone());
- userMapper.updateById(user);
- log.info("用户{}修改手机号成功,旧手机号:{},新手机号:{}", userId, oldPhone, request.getNewPhone());
- auditLogService.logSuccess(userId, user.getNickname(), OperatorRole.USER.getCode(),
- OperationType.USER_UPDATE, "PHONE:" + userId, userId);
- }
- // ====================================
- // 小程序统一登录(处理四种用户身份场景)
- // ====================================
- /**
- * 小程序统一登录
- * <p>
- * 自动处理"小程序老/新用户 × 当前系统老/新用户"四种场景:
- * <ol>
- * <li>外部老 + 本地老 → 直接登录</li>
- * <li>外部老 + 本地新 → 本地注册 + 异步同步证书</li>
- * <li>外部新 + 本地老 → 异步注册外部 + 本地直接登录</li>
- * <li>外部新 + 本地新 → 两端都注册</li>
- * </ol>
- * <p>
- * 关键原则:外部系统操作异步执行,不阻塞登录响应。
- */
- @Transactional
- public LoginResponse miniappUnifiedLogin(MiniProgramLoginRequest request, String ip) {
- // 1. code2Session
- WechatMiniProgramService.MiniProgramSessionInfo session =
- wechatMiniProgramService.code2Session(request.getJsCode());
- // 2. 获取手机号(如果提供了 phoneCode)
- String phone = null;
- if (request.getPhoneCode() != null && !request.getPhoneCode().isBlank()) {
- try {
- phone = wechatMiniProgramService.getPhoneNumber(request.getPhoneCode());
- } catch (Exception e) {
- log.warn("获取小程序手机号失败(不阻断登录): {}", e.getMessage());
- }
- }
- // 3. 并行检查双系统
- User user = findUserByWechatIdentity(session.getUnionId(), session.getOpenId());
- boolean existsExternal = checkExternalUserSafe(session.getUnionId());
- boolean isNewLocal = (user == null);
- log.info("小程序统一登录: unionId={}, isNewLocal={}, existsExternal={}",
- session.getUnionId(), isNewLocal, existsExternal);
- // 4. 场景路由
- if (!isNewLocal && existsExternal) {
- // 场景1: 两端都有 → 更新微信信息,直接登录
- updateWechatMiniInfo(user, session.getOpenId(), session.getUnionId(), phone, ip);
- } else if (isNewLocal && existsExternal) {
- // 场景2: 外部老 + 本地新 → 本地注册 + 异步同步证书
- user = createUserFromMiniProgram(session, phone, request.getInviteCode(), ip);
- final Long newUserId = user.getId();
- final String syncUnionId = session.getUnionId();
- try {
- certificateSyncService.syncCertificatesFromExternal(newUserId, syncUnionId);
- } catch (Exception e) {
- log.warn("触发证书同步失败(不阻断登录): userId={}", newUserId, e);
- }
- } else if (!isNewLocal) {
- // 场景3: 外部新 + 本地老 → 更新微信信息,异步注册外部
- updateWechatMiniInfo(user, session.getOpenId(), session.getUnionId(), phone, ip);
- asyncRegisterInExternal(user, session.getOpenId());
- } else {
- // 场景4: 两端都新 → 本地注册 + 异步注册外部
- user = createUserFromMiniProgram(session, phone, request.getInviteCode(), ip);
- asyncRegisterInExternal(user, session.getOpenId());
- }
- // 5. 没有手机号 → 返回需要绑定
- if (user.getPhone() == null) {
- String tempToken = jwtUtil.generateTempToken(session.getOpenId(), session.getUnionId());
- MDC.put("uid", String.valueOf(user.getId()));
- return LoginResponse.builder()
- .needBindPhone(true)
- .tempToken(tempToken)
- .userInfo(convertToUserInfoResponse(user))
- .build();
- }
- // 6. 更新登录信息并签发 Token
- user.setLastLoginTime(LocalDateTime.now());
- user.setLastLoginIp(ip);
- user.setLoginSource("MINIAPP");
- userMapper.updateById(user);
- LoginResponse response = buildMiniAppLoginResponse(user);
- auditLogService.logSuccess(user.getId(), user.getNickname(), OperatorRole.USER.getCode(),
- isNewLocal ? OperationType.USER_REGISTER : OperationType.USER_LOGIN,
- "USER:" + user.getId(), user.getId());
- return response;
- }
- /**
- * 小程序绑定手机号(needBindPhone=true 时调用)
- */
- @Transactional
- public LoginResponse miniappBindPhone(MiniProgramPhoneRequest request, String ip) {
- // 1. 验证临时 Token
- Claims claims = jwtUtil.parseToken(request.getTempToken());
- if (!"temp".equals(claims.get("type", String.class))) {
- throw new BusinessException(ErrorCode.TOKEN_INVALID);
- }
- String openId = claims.get("openId", String.class);
- String unionId = claims.get("unionId", String.class);
- // 2. 获取手机号
- String phone = wechatMiniProgramService.getPhoneNumber(request.getPhoneCode());
- // 3. 查找小程序用户
- User wechatUser = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getWechatMiniOpenId, openId));
- if (wechatUser == null) {
- throw new BusinessException(ErrorCode.WECHAT_LOGIN_FAILED);
- }
- // 4. 检查手机号是否已被其他用户绑定
- User existUser = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getPhone, phone));
- if (existUser != null && !existUser.getId().equals(wechatUser.getId())) {
- // 手机号已被其他账号绑定 → 合并账号
- existUser.setWechatMiniOpenId(openId);
- if (unionId != null) {
- existUser.setWechatUnionId(unionId);
- }
- existUser.setLastLoginTime(LocalDateTime.now());
- existUser.setLastLoginIp(ip);
- userMapper.updateById(existUser);
- // 禁用旧的小程序用户(不物理删除,仅改状态)
- wechatUser.setStatus(0);
- userMapper.updateById(wechatUser);
- wechatUser = existUser;
- } else if (existUser == null) {
- // 绑定手机号(完成注册)
- wechatUser.setPhone(phone);
- wechatUser.setLoginSource("MINIAPP");
- wechatUser.setLastLoginTime(LocalDateTime.now());
- wechatUser.setLastLoginIp(ip);
- userMapper.updateById(wechatUser);
- eventPublisher.publishEvent(new UserRegisteredEvent(
- this, wechatUser.getId(), request.getInviteCode()));
- }
- return buildMiniAppLoginResponse(wechatUser);
- }
- // ====================================
- // 小程序登录辅助方法
- // ====================================
- /** 通过微信身份标识查找用户(unionId 优先,fallback miniOpenId) */
- private User findUserByWechatIdentity(String unionId, String miniOpenId) {
- if (unionId != null) {
- User user = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getWechatUnionId, unionId));
- if (user != null) {
- return user;
- }
- }
- if (miniOpenId != null) {
- return userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getWechatMiniOpenId, miniOpenId));
- }
- return null;
- }
- /** 安全检查外部用户(不可用时降级为"外部新用户",不阻塞流程) */
- private boolean checkExternalUserSafe(String unionId) {
- try {
- return externalUserSyncService.userExists(unionId);
- } catch (Exception e) {
- log.warn("检查外部用户失败(降级为外部新用户): unionId={}", unionId, e);
- return false;
- }
- }
- /** 从小程序注册新用户 */
- private User createUserFromMiniProgram(
- WechatMiniProgramService.MiniProgramSessionInfo session,
- String phone, String inviteCode, String ip) {
- User user = User.builder()
- .wechatMiniOpenId(session.getOpenId())
- .wechatUnionId(session.getUnionId())
- .phone(phone)
- .nickname("药店用户")
- .membershipLevel(MembershipLevel.PLUS)
- .loginSource("MINIAPP")
- .lastLoginTime(LocalDateTime.now())
- .lastLoginIp(ip)
- .status(1)
- .build();
- userMapper.insert(user);
- membershipService.initPlusMembership(user.getId());
- if (inviteCode != null) {
- eventPublisher.publishEvent(new UserRegisteredEvent(this, user.getId(), inviteCode));
- }
- log.info("小程序新用户注册: userId={}, unionId={}", user.getId(), session.getUnionId());
- return user;
- }
- /** 更新已有用户的微信小程序信息 */
- private void updateWechatMiniInfo(User user, String miniOpenId, String unionId,
- String phone, String ip) {
- boolean changed = false;
- if (miniOpenId != null && !miniOpenId.equals(user.getWechatMiniOpenId())) {
- user.setWechatMiniOpenId(miniOpenId);
- changed = true;
- }
- if (unionId != null && !unionId.equals(user.getWechatUnionId())) {
- user.setWechatUnionId(unionId);
- changed = true;
- }
- if (phone != null && user.getPhone() == null) {
- user.setPhone(phone);
- changed = true;
- }
- user.setLastLoginTime(LocalDateTime.now());
- user.setLastLoginIp(ip);
- user.setLoginSource("MINIAPP");
- if (changed) {
- userMapper.updateById(user);
- }
- }
- /** 异步在外部系统注册用户(不阻塞主流程) */
- private void asyncRegisterInExternal(User user, String miniProgramOpenId) {
- try {
- ExternalUserSyncService.ExternalUserInfo extInfo =
- new ExternalUserSyncService.ExternalUserInfo();
- extInfo.setMiniProgramOpenId(miniProgramOpenId);
- extInfo.setUnionId(user.getWechatUnionId());
- extInfo.setPhone(user.getPhone());
- extInfo.setNickname(user.getNickname());
- extInfo.setAvatar(user.getAvatar());
- String extUserId = externalUserSyncService.registerUser(extInfo);
- if (extUserId != null) {
- user.setExternalUserId(extUserId);
- user.setExternalSyncTime(LocalDateTime.now());
- userMapper.updateById(user);
- log.info("外部系统注册成功: userId={}, extUserId={}", user.getId(), extUserId);
- }
- } catch (Exception e) {
- log.warn("外部系统注册失败(不阻断主流程): userId={}", user.getId(), e);
- }
- }
- }
|