修改小程序登录逻辑

This commit is contained in:
sh
2025-12-23 17:58:44 +08:00
parent 497e4f5001
commit 4ff76a3100
7 changed files with 395 additions and 40 deletions

View File

@@ -0,0 +1,10 @@
package com.ruoyi.cms.domain.vo;
import lombok.Data;
@Data
public class WechatAuthVO {
private String openid;
private String unionid;
private String sessionKey;
}

View File

@@ -8,6 +8,7 @@ import com.ruoyi.common.core.domain.entity.AppUserShow;
import com.ruoyi.common.core.domain.entity.MyChart; import com.ruoyi.common.core.domain.entity.MyChart;
import com.ruoyi.common.core.domain.entity.AppUser; import com.ruoyi.common.core.domain.entity.AppUser;
import com.ruoyi.common.core.domain.entity.SysUser; import com.ruoyi.common.core.domain.entity.SysUser;
import org.apache.ibatis.annotations.Param;
/** /**
* APP用户Mapper接口 * APP用户Mapper接口
@@ -26,15 +27,15 @@ public interface AppUserMapper extends BaseMapper<AppUser>
List<AppUser> selectByJobId(Long jobId); List<AppUser> selectByJobId(Long jobId);
AppUser selectByOpenid(String openid); AppUser selectByOpenid(@Param("openid")String openid, @Param("userType") String userType);
int insertSysUserRole(Map<String,Object> map); int insertSysUserRole(Map<String,Object> map);
int insertSysUser(SysUser sysUser); int insertSysUser(SysUser sysUser);
MyChart getMyTj(Long userId); MyChart getMyTj(@Param("userId") Long userId);
SysUser selectSysUserIdcard(String idCard); SysUser selectSysUserIdcard(@Param("idCard") String idCard);
List<AppUserShow> selectUserApplyList(AppUser appUser); List<AppUserShow> selectUserApplyList(AppUser appUser);
} }

View File

@@ -58,7 +58,11 @@ public interface IAppUserService
public AppUser getPhone(String phone); public AppUser getPhone(String phone);
AppUser selectByOpenid(String openid); public AppUser getPhoneAndNoRole(String phone);
public AppUser getPhoneAndUserType(String phone,String userType);
AppUser selectByOpenid(String openid,String userType);
public AppUser registerAppUser(RegisterBody registerBody); public AppUser registerAppUser(RegisterBody registerBody);

View File

@@ -185,8 +185,20 @@ public class AppUserServiceImpl extends ServiceImpl<AppUserMapper,AppUser> imple
} }
@Override @Override
public AppUser selectByOpenid(String openid) { public AppUser getPhoneAndNoRole(String phone) {
return appUserMapper.selectByOpenid(openid); return appUserMapper.selectOne(new LambdaQueryWrapper<AppUser>()
.eq(AppUser::getPhone, phone).eq(AppUser::getDelFlag,"0").isNull(AppUser::getIsCompanyUser).orderByDesc(AppUser::getUpdateTime).last("LIMIT 1"));
}
@Override
public AppUser getPhoneAndUserType(String phone,String userType) {
return appUserMapper.selectOne(new LambdaQueryWrapper<AppUser>()
.eq(AppUser::getPhone, phone).eq(AppUser::getIsCompanyUser,userType).eq(AppUser::getDelFlag,"0").orderByDesc(AppUser::getUpdateTime).last("LIMIT 1"));
}
@Override
public AppUser selectByOpenid(String openid,String userType) {
return appUserMapper.selectByOpenid(openid,userType);
} }
@Override @Override

View File

@@ -68,7 +68,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select> </select>
<select id="selectByOpenid" resultType="com.ruoyi.common.core.domain.entity.AppUser"> <select id="selectByOpenid" resultType="com.ruoyi.common.core.domain.entity.AppUser">
<include refid="selectAppUserVo"/> WHERE DEL_FLAG = '0' and openid=#{openid} LIMIT 1 <include refid="selectAppUserVo"/> WHERE DEL_FLAG = '0' and openid=#{openid} and is_company_user=#{userType} LIMIT 1
</select> </select>
<insert id="insertSysUserRole" parameterType="java.util.Map"> <insert id="insertSysUserRole" parameterType="java.util.Map">

View File

@@ -0,0 +1,211 @@
package com.ruoyi.common.core.redis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/**
* JDK 1.8 兼容版分布式锁工具类(基于 Redis
*/
@Component
public class DistributedLockUtil {
@Autowired
private RedisCache redisCache;
// 锁默认配置
private static final long DEFAULT_EXPIRE_SECONDS = 30;
private static final long DEFAULT_ACQUIRE_TIMEOUT_SECONDS = 5;
private static final long RENEW_INTERVAL_SECONDS = DEFAULT_EXPIRE_SECONDS / 3;
// 续期线程池
private final ScheduledExecutorService renewExecutor = Executors.newScheduledThreadPool(
Runtime.getRuntime().availableProcessors(),
new ThreadFactory() {
private final AtomicBoolean init = new AtomicBoolean(false);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "distributed-lock-renewer");
thread.setDaemon(true);
if (init.compareAndSet(false, true)) {
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
renewExecutor.shutdown();
}
}));
}
return thread;
}
}
);
// 释放锁 Lua 脚本(原子操作)
private static final String RELEASE_LOCK_LUA_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
private final DefaultRedisScript<Long> releaseScript = new DefaultRedisScript<>();
// 初始化 Lua 脚本
{
releaseScript.setScriptText(RELEASE_LOCK_LUA_SCRIPT);
releaseScript.setResultType(Long.class);
}
/**
* 获取锁(带自动续期)
*/
public String acquireLockWithRenewal(String lockKey) {
return acquireLockWithRenewal(lockKey, DEFAULT_EXPIRE_SECONDS, DEFAULT_ACQUIRE_TIMEOUT_SECONDS);
}
/**
* 自定义参数获取锁
*/
public String acquireLockWithRenewal(String lockKey, long expireSeconds, long acquireTimeoutSeconds) {
String identifier = UUID.randomUUID().toString();
long endTime = System.currentTimeMillis() + acquireTimeoutSeconds * 1000;
while (System.currentTimeMillis() < endTime) {
boolean locked = tryLockOnce(lockKey, identifier, expireSeconds);
if (locked) {
startRenewal(lockKey, identifier, expireSeconds);
return identifier;
}
// 指数退避重试
try {
long sleepMs = calculateBackoffSleep(System.currentTimeMillis() - (endTime - acquireTimeoutSeconds * 1000));
Thread.sleep(sleepMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
return null;
}
/**
* 原子释放锁
*/
public boolean releaseLockSafely(String lockKey, String identifier) {
if (identifier == null) {
return false;
}
Long result = (Long) redisCache.redisTemplate.execute(
releaseScript,
Collections.singletonList(lockKey),
identifier
);
return result != null && result > 0;
}
/**
* 尝试获取一次锁
*/
private boolean tryLockOnce(String lockKey, String identifier, long expireSeconds) {
try {
if (!redisCache.hasKey(lockKey)) {
redisCache.setCacheObject(lockKey, identifier, (int) expireSeconds, TimeUnit.SECONDS);
String storedId = redisCache.getCacheObject(lockKey);
return identifier.equals(storedId);
}
return false;
} catch (Exception e) {
return false;
}
}
/**
* 启动锁自动续期
*/
private void startRenewal(final String lockKey, final String identifier, final long expireSeconds) {
final AtomicReference<ScheduledFuture<?>> futureRef = new AtomicReference<>();
Runnable renewalTask = new Runnable() {
@Override
public void run() {
try {
String storedId = redisCache.getCacheObject(lockKey);
if (identifier.equals(storedId)) {
redisCache.expire(lockKey, expireSeconds);
} else {
cancelFuture();
}
} catch (Exception e) {
cancelFuture();
}
}
private void cancelFuture() {
ScheduledFuture<?> future = futureRef.get();
if (future != null && !future.isCancelled()) {
future.cancel(false);
}
}
};
ScheduledFuture<?> future = renewExecutor.scheduleAtFixedRate(
renewalTask,
RENEW_INTERVAL_SECONDS,
RENEW_INTERVAL_SECONDS,
TimeUnit.SECONDS
);
futureRef.set(future);
}
/**
* 指数退避算法(计算重试间隔)
*/
private long calculateBackoffSleep(long elapsedMs) {
int retryCount = (int) (elapsedMs / 100);
long sleepMs = 100L * (1 << Math.min(retryCount, 10));
return Math.min(sleepMs, 1000);
}
/**
* 自动释放锁工具(支持 try-with-resources
*/
public static class AutoReleaseLock implements AutoCloseable {
private final DistributedLockUtil lockUtil;
private final String lockKey;
private final String identifier;
public AutoReleaseLock(DistributedLockUtil lockUtil, String lockKey, String identifier) {
this.lockUtil = lockUtil;
this.lockKey = lockKey;
this.identifier = identifier;
}
public boolean isLocked() {
return identifier != null;
}
@Override
public void close() {
if (isLocked()) {
lockUtil.releaseLockSafely(lockKey, identifier);
}
}
}
/**
* 简化锁使用
*/
public AutoReleaseLock tryLock(String lockKey) {
String identifier = acquireLockWithRenewal(lockKey);
return new AutoReleaseLock(this, lockKey, identifier);
}
}

View File

@@ -4,6 +4,7 @@ import javax.annotation.Resource;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSON;
import com.ruoyi.cms.domain.vo.WechatAuthVO;
import com.ruoyi.cms.service.IAppUserService; import com.ruoyi.cms.service.IAppUserService;
import com.ruoyi.cms.util.StringUtil; import com.ruoyi.cms.util.StringUtil;
import com.ruoyi.cms.util.WechatUtil; import com.ruoyi.cms.util.WechatUtil;
@@ -12,6 +13,7 @@ import com.ruoyi.common.core.domain.entity.AppUser;
import com.ruoyi.common.core.domain.model.LoginBody; import com.ruoyi.common.core.domain.model.LoginBody;
import com.ruoyi.common.core.domain.model.LoginSiteUser; import com.ruoyi.common.core.domain.model.LoginSiteUser;
import com.ruoyi.common.core.domain.model.RegisterBody; import com.ruoyi.common.core.domain.model.RegisterBody;
import com.ruoyi.common.core.redis.DistributedLockUtil;
import com.ruoyi.common.utils.*; import com.ruoyi.common.utils.*;
import com.ruoyi.framework.web.exception.ParamErrorConstants; import com.ruoyi.framework.web.exception.ParamErrorConstants;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -69,6 +71,9 @@ public class SysLoginService
WechatUtil wechatUtil; WechatUtil wechatUtil;
@Autowired @Autowired
private IAppUserService appUserService; private IAppUserService appUserService;
@Autowired
private DistributedLockUtil distributedLockUtil;
/** /**
* 登录验证 * 登录验证
* *
@@ -258,7 +263,7 @@ public class SysLoginService
return AjaxResult.error("微信授权失败"); return AjaxResult.error("微信授权失败");
} }
//验证是否登录过 //验证是否登录过
AppUser existingUser=appUserService.selectByOpenid(openid); AppUser existingUser=appUserService.selectByOpenid(openid,dto.getUserType());
if(existingUser!=null){ if(existingUser!=null){
if(StringUtils.isEmpty(existingUser.getIsCompanyUser())){ if(StringUtils.isEmpty(existingUser.getIsCompanyUser())){
updateAppUserCommon(existingUser,openid,unionid,dto.getUserType()); updateAppUserCommon(existingUser,openid,unionid,dto.getUserType());
@@ -316,44 +321,37 @@ public class SysLoginService
* 小程序登录主逻辑 * 小程序登录主逻辑
*/ */
public AjaxResult appLoginNew(LoginBody dto) { public AjaxResult appLoginNew(LoginBody dto) {
AjaxResult validateResult = validateLoginParam(dto, false); //1.验证基础参数
AjaxResult validateResult = validateBaseParam(dto);
if (validateResult != null) { if (validateResult != null) {
return validateResult; return validateResult;
} }
try { try {
JSONObject sessionInfo = wechatUtil.code2Session(dto.getCode()); //2. 微信授权获取OpenID/UnionID/SessionKey
String openid = sessionInfo.getString("openid"); WechatAuthVO wechatAuthVO = getWechatAuthInfo(dto.getCode());
String unionid = sessionInfo.getString("unionid"); if (wechatAuthVO == null) {
String sessionKey = sessionInfo.getString("session_key");
if (openid == null) {
return AjaxResult.error("微信授权失败"); return AjaxResult.error("微信授权失败");
} }
String openid = wechatAuthVO.getOpenid();
String unionid = wechatAuthVO.getUnionid();
String sessionKey = wechatAuthVO.getSessionKey();
AppUser existingUser = appUserService.selectByOpenid(openid); String userType = dto.getUserType();
//3. 优先匹配「OpenID+角色」的老用户
AppUser existingUser = appUserService.selectByOpenid(openid,userType);
if (existingUser != null) { if (existingUser != null) {
return handleExistingUser(existingUser, dto.getUserType()); return handleExistingUser(existingUser, userType);
} }
validateResult = validateLoginParam(dto, true); // 4. 解密获取手机号(含二次校验)
if (validateResult != null) { String phone = decryptPhone(dto, sessionKey);
return validateResult;
}
JSONObject phoneInfo = wechatUtil.decryptPhoneNumber(dto.getEncryptedData(), sessionKey, dto.getIv());
String phone = phoneInfo.getString("phoneNumber");
if (phone == null) { if (phone == null) {
return AjaxResult.error("获取手机号失败"); return AjaxResult.error("获取手机号失败");
} }
AppUser phoneUser = appUserService.getPhone(phone); // 5. 处理用户匹配与注册(核心逻辑拆分到独立方法)
if (phoneUser != null) { return handleUserMatchAndRegister(openid, unionid, phone, userType);
return handlePhoneBoundUser(phoneUser, openid, unionid, dto.getUserType());
} else {
return handleNewUser(openid, unionid, phone, dto.getUserType());
}
} catch (Exception e) { } catch (Exception e) {
System.err.println("登录失败:" + e.getMessage()); System.err.println("登录失败:" + e.getMessage());
return AjaxResult.error("登录失败,请稍后重试"); return AjaxResult.error("登录失败,请稍后重试");
@@ -361,9 +359,11 @@ public class SysLoginService
} }
/** /**
* 参数校验方法仅返回错误信息补充userType校验 * 1-基础参数校验原validateLoginParam false场景
* @param dto
* @return
*/ */
private AjaxResult validateLoginParam(LoginBody dto, boolean needDecryptPhone) { private AjaxResult validateBaseParam(LoginBody dto) {
if (dto == null) { if (dto == null) {
return AjaxResult.error(ParamErrorConstants.PARAM_NULL_MSG); return AjaxResult.error(ParamErrorConstants.PARAM_NULL_MSG);
} }
@@ -376,17 +376,134 @@ public class SysLoginService
!StringUtil.IS_JOB_REQUEST_USER.equals(userType)) { !StringUtil.IS_JOB_REQUEST_USER.equals(userType)) {
return AjaxResult.error(ParamErrorConstants.USER_TYPE_INVALID_MSG); return AjaxResult.error(ParamErrorConstants.USER_TYPE_INVALID_MSG);
} }
if (needDecryptPhone) { return null;
}
/**
* 2-微信授权信息获取封装code2Session逻辑
* @param code
* @return
*/
private WechatAuthVO getWechatAuthInfo(String code) {
JSONObject sessionInfo = wechatUtil.code2Session(code);
if (sessionInfo == null || sessionInfo.getString("openid") == null) {
return null;
}
WechatAuthVO authVO = new WechatAuthVO();
authVO.setOpenid(sessionInfo.getString("openid"));
authVO.setUnionid(StringUtils.isBlank(sessionInfo.getString("unionid")) ? "" : sessionInfo.getString("unionid"));
authVO.setSessionKey(sessionInfo.getString("session_key"));
return authVO;
}
/**
* 3-手机号解密(含二次参数校验)
* @param dto
* @param sessionKey
* @return
*/
private String decryptPhone(LoginBody dto, String sessionKey) {
// 二次校验(解密相关参数)
AjaxResult validateResult = validateDecryptParam(dto);
if (validateResult != null) {
return null;
}
// 解密手机号
JSONObject phoneInfo = wechatUtil.decryptPhoneNumber(dto.getEncryptedData(), sessionKey, dto.getIv());
String phone = phoneInfo.getString("phoneNumber");
if (phone == null || phone.trim().isEmpty()) {
return null;
}
return phone.trim();
}
/**
* 4-解密参数校验原validateLoginParam true场景
* @param dto
* @return
*/
private AjaxResult validateDecryptParam(LoginBody dto) {
if (StringUtils.isEmpty(dto.getEncryptedData())) { if (StringUtils.isEmpty(dto.getEncryptedData())) {
return AjaxResult.error(ParamErrorConstants.ENCRYPTED_DATA_EMPTY_MSG); return AjaxResult.error(ParamErrorConstants.ENCRYPTED_DATA_EMPTY_MSG);
} }
if (StringUtils.isEmpty(dto.getIv())) { if (StringUtils.isEmpty(dto.getIv())) {
return AjaxResult.error(ParamErrorConstants.IV_EMPTY_MSG); return AjaxResult.error(ParamErrorConstants.IV_EMPTY_MSG);
} }
}
return null; return null;
} }
/**
* 5-用户匹配与注册(含锁逻辑、无角色数据处理)
* @param openid
* @param unionid
* @param phone
* @param userType
* @return
*/
private AjaxResult handleUserMatchAndRegister(String openid, String unionid, String phone, String userType) {
// 匹配「手机号+角色」的用户
AppUser phoneRoleUser = appUserService.getPhoneAndUserType(phone, userType);
if (phoneRoleUser != null) {
return handlePhoneBoundUser(phoneRoleUser, openid, unionid, userType);
}
// 匹配无角色历史数据
AppUser noRoleUser = appUserService.getPhoneAndNoRole(phone);
if (noRoleUser != null) {
return handleNoRoleUserBinding(openid, unionid, phone, userType, noRoleUser);
}
// 全新用户注册
return handleNewUserRegistration(openid, unionid, phone, userType);
}
/**
* 6-无角色用户绑定(含分布式锁)
* @param openid
* @param unionid
* @param phone
* @param userType
* @param noRoleUser
* @return
*/
private AjaxResult handleNoRoleUserBinding(String openid, String unionid, String phone, String userType, AppUser noRoleUser) {
String lockKey = "login_no_role_bind_" + phone + "_" + userType;
try (DistributedLockUtil.AutoReleaseLock lock = distributedLockUtil.tryLock(lockKey)) {
if (!lock.isLocked()) {
return AjaxResult.error("登录请求过于频繁,请稍后重试");
}
// 双重检查
AppUser doubleCheck = appUserService.getPhoneAndUserType(phone, userType);
if (doubleCheck != null) {
return handlePhoneBoundUser(doubleCheck, openid, unionid, userType);
}
return handlePhoneBoundUser(noRoleUser, openid, unionid, userType);
}
}
/**
* 7-新用户注册(含分布式锁)
* @param openid
* @param unionid
* @param phone
* @param userType
* @return
*/
private AjaxResult handleNewUserRegistration(String openid, String unionid, String phone, String userType) {
String createLockKey = "login_create_" + phone + "_" + userType;
try (DistributedLockUtil.AutoReleaseLock lock = distributedLockUtil.tryLock(createLockKey)) {
if (!lock.isLocked()) {
return AjaxResult.error("登录请求过于频繁,请稍后重试");
}
// 双重检查
AppUser checkNew = appUserService.getPhoneAndUserType(phone, userType);
if (checkNew != null) {
return handlePhoneBoundUser(checkNew, openid, unionid, userType);
}
return handleNewUser(openid, unionid, phone, userType);
}
}
/** /**
* 处理老用户登录日志用println * 处理老用户登录日志用println
*/ */