diff --git a/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java b/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java index f28a1ed..91fbc5a 100644 --- a/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java +++ b/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java @@ -58,6 +58,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { "/api/sendEmailCaptcha", "/api/loginBySms", "/api/loginBySuperAdminSms", + "/api/loginBySelectTenant", "/api/loginByDeveloperSms", "/api/system/user/regByPhone", "/api/parseToken/*", diff --git a/src/main/java/com/gxwebsoft/common/system/controller/MainController.java b/src/main/java/com/gxwebsoft/common/system/controller/MainController.java index f41a7a5..06c816c 100644 --- a/src/main/java/com/gxwebsoft/common/system/controller/MainController.java +++ b/src/main/java/com/gxwebsoft/common/system/controller/MainController.java @@ -41,6 +41,7 @@ import com.gxwebsoft.common.system.param.UpdatePasswordParam; import com.gxwebsoft.common.system.param.UserParam; import com.gxwebsoft.common.system.result.CaptchaResult; import com.gxwebsoft.common.system.result.LoginResult; +import com.gxwebsoft.common.system.result.TenantOption; import com.gxwebsoft.common.system.result.AccountInfoResult; import com.gxwebsoft.common.system.result.CheckPhoneResult; import com.gxwebsoft.common.system.service.*; @@ -64,6 +65,7 @@ import java.time.Instant; import java.time.LocalDateTime; import java.util.HashMap; import java.util.List; +import java.util.stream.Collectors; import java.util.concurrent.TimeUnit; import java.util.concurrent.ThreadLocalRandom; @@ -964,37 +966,111 @@ public class MainController extends BaseController { return fail(message, null); } - User user = userService.getLastLoginSuperAdminByPhone(phone); - if (user == null) { + // 查询该手机号关联的所有超级管理员记录(跨租户) + List superAdmins = userService.getSuperAdminsByPhone(phone); + if (superAdmins == null || superAdmins.isEmpty()) { String message = "用户不存在"; loginRecordService.saveAsync(phone, LoginRecord.TYPE_ERROR, message, null, request); return fail(message, null); } + + // 单租户:直接登录(现有逻辑不变,单租户用户无感知) + if (superAdmins.size() == 1) { + User user = superAdmins.get(0); + if (!Boolean.TRUE.equals(user.getIsSuperAdmin())) { + String message = "非超级管理员账号不允许登录"; + loginRecordService.saveAsync(phone, LoginRecord.TYPE_ERROR, message, user.getTenantId(), request); + return fail(message, null); + } + if (!user.getStatus().equals(0)) { + String message = "账号被冻结"; + loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_ERROR, message, user.getTenantId(), request); + return fail(message, null); + } + + String access_token = generateToken(user); + loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request); + return success("登录成功", new LoginResult(access_token, user)); + } + + // 多租户:返回租户列表,不生成 token,前端展示选择列表 + List tenants = superAdmins.stream() + .map(u -> new TenantOption( + u.getTenantId(), + u.getTenantName(), + u.getUserId(), + u.getUsername(), + u.getNickname(), + null, + u.getAvatar() + )) + .collect(Collectors.toList()); + return success("请选择要登录的租户", new LoginResult(tenants)); + } + + @Operation(summary = "选择租户后登录(多租户场景)") + @PostMapping("/loginBySelectTenant") + public ApiResult loginBySelectTenant(@RequestBody LoginParam param, HttpServletRequest request) { + if (param == null) { + return fail("参数不能为空", null); + } + final String phone = param.getPhone(); + if (!CommonUtil.isValidPhoneNumber(phone)) { + return fail("请输入有效的手机号码", null); + } + if (param.getTenantId() == null) { + return fail("请选择租户", null); + } + String code = param.getCode(); + if (StrUtil.isBlank(code)) { + code = param.getSmsCode(); + } + if (StrUtil.isBlank(code)) { + return fail("验证码不能为空", null); + } + + String smsCode = redisUtil.get("code:" + phone); + String devSmsCode = redisUtil.get(CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS); + if (!StrUtil.equals(code, smsCode) && !StrUtil.equals(code, devSmsCode)) { + String message = "验证码不正确"; + loginRecordService.saveAsync(phone, LoginRecord.TYPE_ERROR, message, null, request); + return fail(message, null); + } + + // 查询该手机号在指定租户下的超级管理员 + User user = userService.getSuperAdminByPhoneAndTenantId(phone, param.getTenantId()); + if (user == null) { + String message = "该租户下不存在此账号"; + loginRecordService.saveAsync(phone, LoginRecord.TYPE_ERROR, message, param.getTenantId(), request); + return fail(message, null); + } if (!Boolean.TRUE.equals(user.getIsSuperAdmin())) { String message = "非超级管理员账号不允许登录"; loginRecordService.saveAsync(phone, LoginRecord.TYPE_ERROR, message, user.getTenantId(), request); return fail(message, null); } - if (!user.getStatus().equals(0)) { - String message = "账号被冻结"; - loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_ERROR, message, user.getTenantId(), request); - return fail(message, null); - } + String access_token = generateToken(user); + loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request); + return success("登录成功", new LoginResult(access_token, user)); + } + + /** + * 生成 JWT token 并存入 Redis + */ + private String generateToken(User user) { Long tokenExpireTime = configProperties.getTokenExpireTime(); final JSONObject register = cacheClient.getSettingInfo("register", user.getTenantId()); if (register != null) { - final String ExpireTime = register.getString("tokenExpireTime"); - if (ExpireTime != null) { - tokenExpireTime = Long.valueOf(ExpireTime); + final String expireTime = register.getString("tokenExpireTime"); + if (expireTime != null) { + tokenExpireTime = Long.valueOf(expireTime); } } - - loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request); String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()), tokenExpireTime, configProperties.getTokenKey()); redisUtil.set("access_token:" + user.getUserId(), access_token, tokenExpireTime, TimeUnit.SECONDS); - return success("登录成功", new LoginResult(access_token, user)); + return access_token; } @Operation(summary = "开发者短信验证码登录") diff --git a/src/main/java/com/gxwebsoft/common/system/mapper/UserMapper.java b/src/main/java/com/gxwebsoft/common/system/mapper/UserMapper.java index cdea5cd..5a7a518 100644 --- a/src/main/java/com/gxwebsoft/common/system/mapper/UserMapper.java +++ b/src/main/java/com/gxwebsoft/common/system/mapper/UserMapper.java @@ -126,4 +126,25 @@ public interface UserMapper extends BaseMapper { @InterceptorIgnore(tenantLine = "true") User selectAdminByPhoneCrossTenant(@Param("phone") String phone); + /** + * 根据手机号查询所有超级管理员账号(跨租户,忽略租户隔离) + * 用于多租户登录:手机号关联多个租户时返回列表供用户选择 + * + * @param phone 手机号 + * @return List + */ + @InterceptorIgnore(tenantLine = "true") + List selectSuperAdminsByPhone(@Param("phone") String phone); + + /** + * 根据手机号和租户ID查询超级管理员账号(跨租户,忽略租户隔离) + * 用于多租户登录:用户选择租户后,查询该租户下的超级管理员 + * + * @param phone 手机号 + * @param tenantId 租户ID + * @return User + */ + @InterceptorIgnore(tenantLine = "true") + User selectSuperAdminByPhoneAndTenantId(@Param("phone") String phone, @Param("tenantId") Integer tenantId); + } diff --git a/src/main/java/com/gxwebsoft/common/system/mapper/xml/UserMapper.xml b/src/main/java/com/gxwebsoft/common/system/mapper/xml/UserMapper.xml index 5d0a5ad..38a859e 100644 --- a/src/main/java/com/gxwebsoft/common/system/mapper/xml/UserMapper.xml +++ b/src/main/java/com/gxwebsoft/common/system/mapper/xml/UserMapper.xml @@ -461,4 +461,33 @@ LIMIT 1 + + + + + + diff --git a/src/main/java/com/gxwebsoft/common/system/result/LoginResult.java b/src/main/java/com/gxwebsoft/common/system/result/LoginResult.java index 940d8e9..5908aad 100644 --- a/src/main/java/com/gxwebsoft/common/system/result/LoginResult.java +++ b/src/main/java/com/gxwebsoft/common/system/result/LoginResult.java @@ -2,12 +2,11 @@ package com.gxwebsoft.common.system.result; import com.gxwebsoft.common.system.entity.User; import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; +import java.util.List; /** * 登录返回结果 @@ -17,7 +16,6 @@ import java.io.Serializable; */ @Data @NoArgsConstructor -@AllArgsConstructor @Schema(description = "登录返回结果") public class LoginResult implements Serializable { private static final long serialVersionUID = 1L; @@ -28,4 +26,16 @@ public class LoginResult implements Serializable { @Schema(description = "用户信息") private User user; + @Schema(description = "租户列表(多租户场景下返回,此时不返回access_token)") + private List tenants; + + public LoginResult(String access_token, User user) { + this.access_token = access_token; + this.user = user; + } + + public LoginResult(List tenants) { + this.tenants = tenants; + } + } diff --git a/src/main/java/com/gxwebsoft/common/system/result/TenantOption.java b/src/main/java/com/gxwebsoft/common/system/result/TenantOption.java new file mode 100644 index 0000000..f7d11a5 --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/result/TenantOption.java @@ -0,0 +1,52 @@ +package com.gxwebsoft.common.system.result; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; + +/** + * 租户选项(多租户登录时返回,供前端展示选择列表) + * + * @author WebSoft + */ +@Data +@Schema(description = "租户选项") +public class TenantOption implements Serializable { + private static final long serialVersionUID = 1L; + + @Schema(description = "租户ID") + private Integer tenantId; + + @Schema(description = "租户名称") + private String tenantName; + + @Schema(description = "用户ID") + private Integer userId; + + @Schema(description = "用户名") + private String username; + + @Schema(description = "昵称") + private String nickname; + + @Schema(description = "角色名称") + private String roleName; + + @Schema(description = "头像") + private String avatar; + + public TenantOption() { + } + + public TenantOption(Integer tenantId, String tenantName, Integer userId, + String username, String nickname, String roleName, String avatar) { + this.tenantId = tenantId; + this.tenantName = tenantName; + this.userId = userId; + this.username = username; + this.nickname = nickname; + this.roleName = roleName; + this.avatar = avatar; + } +} diff --git a/src/main/java/com/gxwebsoft/common/system/service/UserService.java b/src/main/java/com/gxwebsoft/common/system/service/UserService.java index 7dd41c1..63561f2 100644 --- a/src/main/java/com/gxwebsoft/common/system/service/UserService.java +++ b/src/main/java/com/gxwebsoft/common/system/service/UserService.java @@ -160,6 +160,25 @@ public interface UserService extends IService, UserDetailsService { */ User getByPhoneAndAdmin(String phone); + /** + * 根据手机号查询所有超级管理员账号(跨租户,忽略租户隔离) + * 用于多租户登录:手机号关联多个租户时返回列表供用户选择 + * + * @param phone 手机号 + * @return List + */ + List getSuperAdminsByPhone(String phone); + + /** + * 根据手机号和租户ID查询超级管理员账号(跨租户,忽略租户隔离) + * 用于多租户登录:用户选择租户后,查询该租户下的超级管理员 + * + * @param phone 手机号 + * @param tenantId 租户ID + * @return User + */ + User getSuperAdminByPhoneAndTenantId(String phone, Integer tenantId); + List pageAll(UserParam param); User getByUserId(String userId); diff --git a/src/main/java/com/gxwebsoft/common/system/service/impl/UserServiceImpl.java b/src/main/java/com/gxwebsoft/common/system/service/impl/UserServiceImpl.java index 82cd3df..19bc01a 100644 --- a/src/main/java/com/gxwebsoft/common/system/service/impl/UserServiceImpl.java +++ b/src/main/java/com/gxwebsoft/common/system/service/impl/UserServiceImpl.java @@ -460,6 +460,22 @@ public class UserServiceImpl extends ServiceImpl implements Us return baseMapper.selectAdminByPhoneCrossTenant(phone); } + @Override + public List getSuperAdminsByPhone(String phone) { + if (StrUtil.isBlank(phone)) { + return null; + } + return baseMapper.selectSuperAdminsByPhone(phone); + } + + @Override + public User getSuperAdminByPhoneAndTenantId(String phone, Integer tenantId) { + if (StrUtil.isBlank(phone) || tenantId == null) { + return null; + } + return baseMapper.selectSuperAdminByPhoneAndTenantId(phone, tenantId); + } + @Override public List pageAll(UserParam param) { return baseMapper.pageRelAll(param);