会员管理优化

master
zzy 2026-04-21 14:58:49 +08:00
parent 3acdbeeb37
commit d88a9ca74a
6 changed files with 364 additions and 20 deletions

View File

@ -1,17 +1,18 @@
package com.fjrcloud.community.module.member.dal.dataobject.user;
import com.fjrcloud.community.framework.common.enums.CommonStatusEnum;
import com.fjrcloud.community.framework.common.enums.TerminalEnum;
import com.fjrcloud.community.framework.ip.core.Area;
import com.fjrcloud.community.framework.mybatis.core.type.LongListTypeHandler;
import com.fjrcloud.community.framework.tenant.core.db.TenantBaseDO;
import com.fjrcloud.community.module.member.dal.dataobject.group.MemberGroupDO;
import com.fjrcloud.community.module.member.dal.dataobject.level.MemberLevelDO;
import com.fjrcloud.community.module.system.enums.common.SexEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fjrcloud.community.framework.common.enums.CommonStatusEnum;
import com.fjrcloud.community.framework.common.enums.TerminalEnum;
import com.fjrcloud.community.framework.ip.core.Area;
import com.fjrcloud.community.framework.mybatis.core.type.LongListTypeHandler;
import com.fjrcloud.community.framework.tenant.core.aop.TenantIgnore;
import com.fjrcloud.community.framework.tenant.core.db.TenantBaseDO;
import com.fjrcloud.community.module.member.dal.dataobject.group.MemberGroupDO;
import com.fjrcloud.community.module.member.dal.dataobject.level.MemberLevelDO;
import com.fjrcloud.community.module.system.enums.common.SexEnum;
import lombok.*;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@ -26,12 +27,13 @@ import java.util.List;
* @author
*/
@TableName(value = "member_user", autoResultMap = true)
@KeySequence("member_user_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@KeySequence("member_user_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TenantIgnore
public class MemberUserDO extends TenantBaseDO {
// ========== 账号信息 ==========

View File

@ -0,0 +1,44 @@
package com.fjrcloud.community.module.member.dal.dataobject.user;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fjrcloud.community.framework.mybatis.core.dataobject.BaseDO;
import lombok.*;
/**
*
*
* @author
*/
@TableName("member_user_tenant_rel")
@KeySequence("member_user_tenant_rel_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberUserTenantRelDO extends BaseDO {
/**
* ID
*/
@TableId
private Long id;
/**
* ID
*/
private Long userId;
/**
* ID
*/
private Long tenantId;
/**
*
*/
private Boolean defaultTenant;
}

View File

@ -0,0 +1,64 @@
package com.fjrcloud.community.module.member.dal.mysql.user;
import com.fjrcloud.community.framework.mybatis.core.mapper.BaseMapperX;
import com.fjrcloud.community.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.fjrcloud.community.module.member.dal.dataobject.user.MemberUserTenantRelDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* Mapper
*
* @author
*/
@Mapper
public interface MemberUserTenantRelMapper extends BaseMapperX<MemberUserTenantRelDO> {
/**
* ID
*
* @param userId ID
* @return
*/
default List<MemberUserTenantRelDO> selectListByUserId(Long userId) {
return selectList(MemberUserTenantRelDO::getUserId, userId);
}
/**
* IDID
*
* @param userId ID
* @param tenantId ID
* @return
*/
default MemberUserTenantRelDO selectByUserIdAndTenantId(Long userId, Long tenantId) {
return selectOne(new LambdaQueryWrapperX<MemberUserTenantRelDO>()
.eq(MemberUserTenantRelDO::getUserId, userId)
.eq(MemberUserTenantRelDO::getTenantId, tenantId));
}
/**
* ID
*
* @param userId ID
* @return
*/
default int deleteByUserId(Long userId) {
return delete(new LambdaQueryWrapperX<MemberUserTenantRelDO>()
.eq(MemberUserTenantRelDO::getUserId, userId));
}
/**
* IDID
*
* @param userId ID
* @param tenantId ID
* @return
*/
default int deleteByUserIdAndTenantId(Long userId, Long tenantId) {
return delete(new LambdaQueryWrapperX<MemberUserTenantRelDO>()
.eq(MemberUserTenantRelDO::getUserId, userId)
.eq(MemberUserTenantRelDO::getTenantId, tenantId));
}
}

View File

@ -1,20 +1,23 @@
package com.fjrcloud.community.module.member.service.auth;
import cn.hutool.core.lang.Assert;
import com.fjrcloud.community.framework.common.biz.system.oauth2.OAuth2TokenCommonApi;
import com.fjrcloud.community.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCreateReqDTO;
import com.fjrcloud.community.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenRespDTO;
import com.fjrcloud.community.framework.common.enums.CommonStatusEnum;
import com.fjrcloud.community.framework.common.enums.TerminalEnum;
import com.fjrcloud.community.framework.common.enums.UserTypeEnum;
import com.fjrcloud.community.framework.common.util.monitor.TracerUtils;
import com.fjrcloud.community.framework.common.util.servlet.ServletUtils;
import com.fjrcloud.community.framework.tenant.core.context.TenantContextHolder;
import com.fjrcloud.community.module.member.controller.app.auth.vo.*;
import com.fjrcloud.community.module.member.convert.auth.AuthConvert;
import com.fjrcloud.community.module.member.dal.dataobject.user.MemberUserDO;
import com.fjrcloud.community.module.member.dal.dataobject.user.MemberUserTenantRelDO;
import com.fjrcloud.community.module.member.service.user.MemberUserService;
import com.fjrcloud.community.module.member.service.user.MemberUserTenantRelService;
import com.fjrcloud.community.module.system.api.logger.LoginLogApi;
import com.fjrcloud.community.module.system.api.logger.dto.LoginLogCreateReqDTO;
import com.fjrcloud.community.framework.common.biz.system.oauth2.OAuth2TokenCommonApi;
import com.fjrcloud.community.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCreateReqDTO;
import com.fjrcloud.community.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenRespDTO;
import com.fjrcloud.community.module.system.api.sms.SmsCodeApi;
import com.fjrcloud.community.module.system.api.social.SocialClientApi;
import com.fjrcloud.community.module.system.api.social.SocialUserApi;
@ -39,7 +42,7 @@ import static com.fjrcloud.community.framework.web.core.util.WebFrameworkUtils.g
import static com.fjrcloud.community.module.member.enums.ErrorCodeConstants.*;
/**
* Service
* Service
*
* @author
*/
@ -49,22 +52,33 @@ public class MemberAuthServiceImpl implements MemberAuthService {
@Resource
private MemberUserService userService;
@Resource
private MemberUserTenantRelService userTenantRelService;
@Resource
private SmsCodeApi smsCodeApi;
@Resource
private LoginLogApi loginLogApi;
@Resource
private SocialUserApi socialUserApi;
@Resource
private SocialClientApi socialClientApi;
@Resource
private OAuth2TokenCommonApi oauth2TokenApi;
@Override
public AppAuthLoginRespVO login(AppAuthLoginReqVO reqVO) {
// 使用手机 + 密码进行登录
// 使用手机 + 密码进行登录
MemberUserDO user = login0(reqVO.getMobile(), reqVO.getPassword());
// 确保用户与当前租户有关联关系
ensureUserTenantRelation(user.getId());
// 如果 socialType 非空,说明需要绑定社交用户
String openid = null;
if (reqVO.getSocialType() != null) {
@ -79,7 +93,7 @@ public class MemberAuthServiceImpl implements MemberAuthService {
@Override
@Transactional
public AppAuthLoginRespVO smsLogin(AppAuthSmsLoginReqVO reqVO) {
// 校验验证码
// 获得或创建注册用户
String userIp = getClientIP();
// smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.MEMBER_LOGIN.getScene(), userIp));
@ -93,6 +107,9 @@ public class MemberAuthServiceImpl implements MemberAuthService {
throw exception(AUTH_LOGIN_USER_DISABLED);
}
// 确保用户与当前租户有关联关系
ensureUserTenantRelation(user.getId());
// 如果 socialType 非空,说明需要绑定社交用户
String openid = null;
if (reqVO.getSocialType() != null) {
@ -107,7 +124,7 @@ public class MemberAuthServiceImpl implements MemberAuthService {
@Override
@Transactional
public AppAuthLoginRespVO socialLogin(AppAuthSocialLoginReqVO reqVO) {
// 使用 code 授权码进行登录。然后,获得绑定的用户编号
// 使用 code 授权码进行登录,获得绑定的用户编号
SocialUserRespDTO socialUser = socialUserApi.getSocialUserByCode(UserTypeEnum.MEMBER.getValue(), reqVO.getType(),
reqVO.getCode(), reqVO.getState());
if (socialUser == null) {
@ -115,10 +132,10 @@ public class MemberAuthServiceImpl implements MemberAuthService {
}
// 情况一:已绑定,直接读取用户信息
// 情况二:未绑定,注册用户 + 绑定用户
MemberUserDO user;
if (socialUser.getUserId() != null) {
user = userService.getUser(socialUser.getUserId());
// 情况二:未绑定,注册用户 + 绑定用户
} else {
user = userService.createUser(socialUser.getNickname(), socialUser.getAvatar(), getClientIP(), getTerminal());
socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),
@ -128,6 +145,9 @@ public class MemberAuthServiceImpl implements MemberAuthService {
throw exception(USER_NOT_EXISTS);
}
// 确保用户与当前租户有关联关系
ensureUserTenantRelation(user.getId());
// 创建 Token 令牌,记录登录日志
return createTokenAfterLoginSuccess(user, user.getMobile(), LoginLogTypeEnum.LOGIN_SOCIAL, socialUser.getOpenid());
}
@ -139,11 +159,14 @@ public class MemberAuthServiceImpl implements MemberAuthService {
UserTypeEnum.MEMBER.getValue(), reqVO.getPhoneCode());
Assert.notNull(phoneNumberInfo, "获得手机信息失败,结果为空");
// 获得获得注册用户
// 获得或创建注册用户
MemberUserDO user = userService.createUserIfAbsent(phoneNumberInfo.getPurePhoneNumber(),
getClientIP(), TerminalEnum.WECHAT_MINI_PROGRAM.getTerminal());
Assert.notNull(user, "获取用户失败,结果为空");
// 确保用户与当前租户有关联关系
ensureUserTenantRelation(user.getId());
// 绑定社交用户
String openid = socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),
SocialTypeEnum.WECHAT_MINI_PROGRAM.getType(), reqVO.getLoginCode(), reqVO.getState()));
@ -152,6 +175,29 @@ public class MemberAuthServiceImpl implements MemberAuthService {
return createTokenAfterLoginSuccess(user, user.getMobile(), LoginLogTypeEnum.LOGIN_SOCIAL, openid);
}
/**
*
*
*
* @param userId ID
*/
private void ensureUserTenantRelation(Long userId) {
// 获取当前租户ID
Long currentTenantId = TenantContextHolder.getTenantId();
if (currentTenantId == null) {
return;
}
// 检查用户是否已在当前租户下有关联
MemberUserTenantRelDO rel = userTenantRelService.getUserTenantRel(userId, currentTenantId);
// 如果没有关联,自动创建
if (rel == null) {
userTenantRelService.assignUserToTenant(userId, currentTenantId, false);
log.info("[ensureUserTenantRelation][会员({}) 自动关联租户({})]", userId, currentTenantId);
}
}
private AppAuthLoginRespVO createTokenAfterLoginSuccess(MemberUserDO user, String mobile,
LoginLogTypeEnum logType, String openid) {
// 插入登陆日志
@ -169,23 +215,35 @@ public class MemberAuthServiceImpl implements MemberAuthService {
return socialClientApi.getAuthorizeUrl(type, UserTypeEnum.MEMBER.getValue(), redirectUri);
}
/**
* 使 +
*
* @param mobile
* @param password
* @return
*/
private MemberUserDO login0(String mobile, String password) {
final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_MOBILE;
// 校验账号是否存在
MemberUserDO user = userService.getUserByMobile(mobile);
if (user == null) {
createLoginLog(null, mobile, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);
throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
}
// 校验密码是否正确
if (!userService.isPasswordMatch(password, user.getPassword())) {
createLoginLog(user.getId(), mobile, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);
throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
}
// 校验是否禁用
if (CommonStatusEnum.isDisable(user.getStatus())) {
createLoginLog(user.getId(), mobile, logTypeEnum, LoginResultEnum.USER_DISABLED);
throw exception(AUTH_LOGIN_USER_DISABLED);
}
return user;
}
@ -201,6 +259,7 @@ public class MemberAuthServiceImpl implements MemberAuthService {
reqDTO.setUserIp(getClientIP());
reqDTO.setResult(loginResult.getResult());
loginLogApi.createLoginLog(reqDTO);
// 更新最后登录时间
if (userId != null && Objects.equals(LoginResultEnum.SUCCESS.getResult(), loginResult.getResult())) {
userService.updateUserLogin(userId, getClientIP());
@ -220,13 +279,14 @@ public class MemberAuthServiceImpl implements MemberAuthService {
@Override
public void sendSmsCode(Long userId, AppAuthSmsSendReqVO reqVO) {
// 情况 1如果是修改手机场景需要校验新手机号是否已经注册,说明不能使用该手机了
// 情况 1如果是修改手机场景需要校验新手机号是否已经注册
if (Objects.equals(reqVO.getScene(), SmsSceneEnum.MEMBER_UPDATE_MOBILE.getScene())) {
MemberUserDO user = userService.getUserByMobile(reqVO.getMobile());
if (user != null && !Objects.equals(user.getId(), userId)) {
throw exception(AUTH_MOBILE_USED);
}
}
// 情况 2如果是重置密码场景需要校验手机号是存在的
if (Objects.equals(reqVO.getScene(), SmsSceneEnum.MEMBER_RESET_PASSWORD.getScene())) {
MemberUserDO user = userService.getUserByMobile(reqVO.getMobile());
@ -234,10 +294,10 @@ public class MemberAuthServiceImpl implements MemberAuthService {
throw exception(USER_MOBILE_NOT_EXISTS);
}
}
// 情况 3如果是修改密码场景需要查询手机号无需前端传递
if (Objects.equals(reqVO.getScene(), SmsSceneEnum.MEMBER_UPDATE_PASSWORD.getScene())) {
MemberUserDO user = userService.getUser(userId);
// TODO 芋艿:后续 member user 手机非强绑定,这块需要做下调整;
reqVO.setMobile(user.getMobile());
}

View File

@ -0,0 +1,55 @@
package com.fjrcloud.community.module.member.service.user;
import com.fjrcloud.community.module.member.dal.dataobject.user.MemberUserTenantRelDO;
import java.util.List;
/**
* Service
*
* @author
*/
public interface MemberUserTenantRelService {
/**
*
*
* @param userId ID
* @return
*/
List<MemberUserTenantRelDO> getUserTenantRels(Long userId);
/**
*
*
* @param userId ID
* @param tenantId ID
* @return
*/
MemberUserTenantRelDO getUserTenantRel(Long userId, Long tenantId);
/**
*
*
* @param userId ID
* @param tenantId ID
* @param isDefault
*/
void assignUserToTenant(Long userId, Long tenantId, Boolean isDefault);
/**
*
*
* @param userId ID
* @param tenantId ID
*/
void removeUserFromTenant(Long userId, Long tenantId);
/**
*
*
* @param userId ID
* @param tenantIds ID
*/
void batchAssignUserToTenants(Long userId, List<Long> tenantIds);
}

View File

@ -0,0 +1,119 @@
package com.fjrcloud.community.module.member.service.user;
import cn.hutool.core.collection.CollUtil;
import com.fjrcloud.community.module.member.dal.dataobject.user.MemberUserTenantRelDO;
import com.fjrcloud.community.module.member.dal.mysql.user.MemberUserTenantRelMapper;
import com.fjrcloud.community.module.system.dal.dataobject.tenant.TenantDO;
import com.fjrcloud.community.module.system.dal.mysql.tenant.TenantMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
import static com.fjrcloud.community.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.fjrcloud.community.module.system.enums.ErrorCodeConstants.TENANT_NOT_EXISTS;
/**
* Service
*
* @author
*/
@Service
@Slf4j
public class MemberUserTenantRelServiceImpl implements MemberUserTenantRelService {
@Resource
private MemberUserTenantRelMapper memberUserTenantRelMapper;
@Resource
private TenantMapper tenantMapper;
@Override
public List<MemberUserTenantRelDO> getUserTenantRels(Long userId) {
return memberUserTenantRelMapper.selectListByUserId(userId);
}
@Override
public MemberUserTenantRelDO getUserTenantRel(Long userId, Long tenantId) {
return memberUserTenantRelMapper.selectByUserIdAndTenantId(userId, tenantId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void assignUserToTenant(Long userId, Long tenantId, Boolean isDefault) {
// 校验租户是否存在
TenantDO tenant = tenantMapper.selectById(tenantId);
if (tenant == null) {
throw exception(TENANT_NOT_EXISTS);
}
// 检查关联是否已存在,避免重复创建
MemberUserTenantRelDO existRel = memberUserTenantRelMapper.selectByUserIdAndTenantId(userId, tenantId);
if (existRel != null) {
return;
}
// 如果设置为默认租户,需要清除其他默认租户标记
if (Boolean.TRUE.equals(isDefault)) {
clearDefaultTenant(userId);
}
// 创建新的关联关系
MemberUserTenantRelDO rel = new MemberUserTenantRelDO();
rel.setUserId(userId);
rel.setTenantId(tenantId);
rel.setDefaultTenant(isDefault != null ? isDefault : false);
memberUserTenantRelMapper.insert(rel);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void removeUserFromTenant(Long userId, Long tenantId) {
memberUserTenantRelMapper.deleteByUserIdAndTenantId(userId, tenantId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void batchAssignUserToTenants(Long userId, List<Long> tenantIds) {
// 先删除该用户的所有租户关联
memberUserTenantRelMapper.deleteByUserId(userId);
// 如果租户列表为空,直接返回
if (CollUtil.isEmpty(tenantIds)) {
return;
}
// 批量创建新的关联关系
for (Long tenantId : tenantIds) {
// 校验租户是否存在
TenantDO tenant = tenantMapper.selectById(tenantId);
if (tenant == null) {
throw exception(TENANT_NOT_EXISTS);
}
// 创建关联关系
MemberUserTenantRelDO rel = new MemberUserTenantRelDO();
rel.setUserId(userId);
rel.setTenantId(tenantId);
rel.setDefaultTenant(false);
memberUserTenantRelMapper.insert(rel);
}
}
/**
*
*
* @param userId ID
*/
private void clearDefaultTenant(Long userId) {
List<MemberUserTenantRelDO> rels = memberUserTenantRelMapper.selectListByUserId(userId);
for (MemberUserTenantRelDO rel : rels) {
if (Boolean.TRUE.equals(rel.getDefaultTenant())) {
rel.setDefaultTenant(false);
memberUserTenantRelMapper.updateById(rel);
}
}
}
}