From 487717986a71949f5eaaf7a4a15f92713fd52645 Mon Sep 17 00:00:00 2001 From: lapuda <577732344@qq.com> Date: Thu, 23 Jul 2026 14:41:55 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20=E6=B7=BB=E5=8A=A0=E5=B0=8F?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E4=BC=81=E4=B8=9A=E6=B5=8B=E8=AF=95=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E5=8A=9F=E8=83=BD=E5=8F=8A=E7=9B=B8=E5=85=B3=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../system/BackDoorLoginController.java | 57 +++++++ .../web/service/BackDoorLoginService.java | 142 ++++++++++++++++++ .../system/BackDoorLoginControllerTest.java | 74 +++++++++ .../web/service/BackDoorLoginServiceTest.java | 84 +++++++++++ .../ruoyi/system/mapper/SysUserMapper.java | 9 ++ .../resources/mapper/system/SysUserMapper.xml | 26 ++++ 6 files changed, 392 insertions(+) diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/BackDoorLoginController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/BackDoorLoginController.java index 4eb492e..fa67154 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/BackDoorLoginController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/BackDoorLoginController.java @@ -12,6 +12,7 @@ import com.ruoyi.common.exception.ServiceException; import com.ruoyi.web.config.BackDoorLoginProperties; import com.ruoyi.web.service.BackDoorLoginService; import com.ruoyi.web.service.BackDoorLoginService.AppLoginSession; +import com.ruoyi.web.service.BackDoorLoginService.CompanyLoginSession; import com.ruoyi.web.service.BackDoorLoginService.LoginSession; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -150,6 +151,62 @@ public class BackDoorLoginController } } + /** + * 小程序企业测试授权列表,只返回可登录企业的最小化展示字段。 + */ + @PostMapping("/app/backDoor/companies") + public AjaxResult appBackDoorCompanies(@RequestBody(required = false) JSONObject body) + { + if (!matchesAppBackDoorPassword(body)) + { + return AjaxResult.error("测试授权密码错误"); + } + try + { + String keyword = body == null ? null : body.getString("keyword"); + return AjaxResult.success( + backDoorLoginService.listAvailableCompanies(keyword, properties.getMaxUsers())); + } + catch (Exception e) + { + log.error("加载小程序测试授权企业列表失败", e); + return AjaxResult.error("加载企业列表失败,请稍后重试"); + } + } + + /** + * 小程序企业测试授权登录。服务层会再次确认 AppUser 仍为正常企业账号。 + */ + @PostMapping("/app/backDoor/company/login") + public AjaxResult appBackDoorCompanyLogin(@RequestBody(required = false) JSONObject body) + { + if (!matchesAppBackDoorPassword(body)) + { + return AjaxResult.error("测试授权密码错误"); + } + Long appUserId = body == null ? null : body.getLong("appUserId"); + try + { + CompanyLoginSession session = backDoorLoginService.loginCompany(appUserId); + AjaxResult ajax = AjaxResult.success(); + ajax.put(Constants.TOKEN, session.getToken()); + ajax.put("appUserId", session.getAppUser().getUserId()); + ajax.put("isCompanyUser", session.getAppUser().getIsCompanyUser()); + ajax.put("companyName", session.getAppUser().getName()); + ajax.put("companyCode", session.getAppUser().getIdCard()); + return ajax; + } + catch (ServiceException e) + { + return AjaxResult.error(e.getMessage()); + } + catch (Exception e) + { + log.error("小程序企业测试授权登录失败,appUserId={}", appUserId, e); + return AjaxResult.error("企业授权登录失败,请稍后重试"); + } + } + private boolean matchesAppBackDoorPassword(JSONObject body) { return properties.matchesPassword(body == null ? null : body.getString("password")); diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/service/BackDoorLoginService.java b/ruoyi-admin/src/main/java/com/ruoyi/web/service/BackDoorLoginService.java index 259b5a9..9d4a864 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/service/BackDoorLoginService.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/service/BackDoorLoginService.java @@ -29,6 +29,7 @@ public class BackDoorLoginService private static final Logger log = LoggerFactory.getLogger(BackDoorLoginService.class); private static final int MAX_KEYWORD_LENGTH = 50; private static final int MAX_USER_LIMIT = 200; + private static final String COMPANY_USER_TYPE = "0"; private final SysUserMapper userMapper; private final UserDetailsServiceImpl userDetailsService; @@ -74,6 +75,21 @@ public class BackDoorLoginService .collect(Collectors.toList()); } + /** + * 返回当前可用于小程序测试授权登录的企业账号。 + * + * 企业登录态的主体是 AppUser,因此不能从 Company 表直接签发令牌。列表只 + * 包含正常的企业 AppUser,并转换为最小化展示对象。 + */ + public List listAvailableCompanies(String keyword, int limit) + { + String normalizedKeyword = normalizeKeyword(keyword); + int safeLimit = normalizeLimit(limit); + return userMapper.selectBackDoorCompanyList(normalizedKeyword, safeLimit).stream() + .map(CompanyLoginUser::new) + .collect(Collectors.toList()); + } + public LoginSession login(Long userId) { SysUser user = getEffectiveUser(userId); @@ -112,6 +128,64 @@ public class BackDoorLoginService return new AppLoginSession(user, appUser, token); } + /** + * 仅供测试环境小程序使用的企业后门授权。 + * + * 即使账号来自刚加载的列表,这里仍重新查询并校验角色、删除状态和停用状态, + * 防止列表加载后账号状态变化,或调用方绕过列表直接提交任意 AppUser ID。 + */ + public CompanyLoginSession loginCompany(Long appUserId) + { + if (appUserId == null || appUserId <= 0) + { + throw new ServiceException("企业用户 ID 必须是正整数"); + } + + AppUser appUser = sysUserService.selectAppUserById(appUserId); + validateCompanyUser(appUser); + + String token = loginService.loginUserIdApp(appUser); + log.warn("测试环境小程序企业后门授权:appUserId={}, companyName={}", + appUser.getUserId(), appUser.getName()); + return new CompanyLoginSession(appUser, token); + } + + private void validateCompanyUser(AppUser appUser) + { + if (appUser == null) + { + throw new ServiceException("企业账号不存在"); + } + if (!UserStatus.OK.getCode().equals(appUser.getDelFlag())) + { + throw new ServiceException("企业账号已删除,不能登录"); + } + if (StringUtils.isNotBlank(appUser.getStatus()) + && !UserStatus.OK.getCode().equals(appUser.getStatus())) + { + throw new ServiceException("企业账号已停用,不能登录"); + } + if (!COMPANY_USER_TYPE.equals(appUser.getIsCompanyUser())) + { + throw new ServiceException("所选账号不是企业账号"); + } + } + + private String normalizeKeyword(String keyword) + { + String normalizedKeyword = StringUtils.trimToNull(keyword); + if (normalizedKeyword != null && normalizedKeyword.length() > MAX_KEYWORD_LENGTH) + { + return normalizedKeyword.substring(0, MAX_KEYWORD_LENGTH); + } + return normalizedKeyword; + } + + private int normalizeLimit(int limit) + { + return Math.max(1, Math.min(limit, MAX_USER_LIMIT)); + } + private SysUser getEffectiveUser(Long userId) { if (userId == null || userId <= 0) @@ -248,4 +322,72 @@ public class BackDoorLoginService return token; } } + + /** 企业选择页最小化展示字段,不返回手机号、完整信用代码或浪潮用户标识。 */ + public static class CompanyLoginUser + { + private final Long appUserId; + private final String companyName; + private final String companyCodeMasked; + + public CompanyLoginUser(AppUser appUser) + { + this.appUserId = appUser.getUserId(); + this.companyName = StringUtils.defaultIfBlank(appUser.getName(), + "企业账号 " + appUser.getUserId()); + this.companyCodeMasked = maskCompanyCode(appUser.getIdCard()); + } + + public Long getAppUserId() + { + return appUserId; + } + + public String getCompanyName() + { + return companyName; + } + + public String getCompanyCodeMasked() + { + return companyCodeMasked; + } + + private static String maskCompanyCode(String companyCode) + { + String normalized = StringUtils.trimToNull(companyCode); + if (normalized == null) + { + return null; + } + if (normalized.length() <= 8) + { + return "****"; + } + return normalized.substring(0, 4) + "**********" + + normalized.substring(normalized.length() - 4); + } + } + + public static class CompanyLoginSession + { + private final AppUser appUser; + private final String token; + + public CompanyLoginSession(AppUser appUser, String token) + { + this.appUser = appUser; + this.token = token; + } + + public AppUser getAppUser() + { + return appUser; + } + + public String getToken() + { + return token; + } + } } diff --git a/ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/BackDoorLoginControllerTest.java b/ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/BackDoorLoginControllerTest.java index 176a93a..5bf4f42 100644 --- a/ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/BackDoorLoginControllerTest.java +++ b/ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/BackDoorLoginControllerTest.java @@ -10,6 +10,8 @@ import com.ruoyi.web.config.BackDoorLoginProperties; import com.ruoyi.web.service.BackDoorLoginService; import com.ruoyi.web.service.BackDoorLoginService.AppLoginUser; import com.ruoyi.web.service.BackDoorLoginService.AppLoginSession; +import com.ruoyi.web.service.BackDoorLoginService.CompanyLoginSession; +import com.ruoyi.web.service.BackDoorLoginService.CompanyLoginUser; import com.ruoyi.web.service.BackDoorLoginService.LoginSession; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -155,4 +157,76 @@ class BackDoorLoginControllerTest assertEquals("测试授权密码错误", response.get("msg")); verify(loginService, never()).loginApp(org.mockito.ArgumentMatchers.any()); } + + @Test + void appBackDoorCompaniesRequiresPasswordBeforeLoadingData() + { + JSONObject body = new JSONObject(); + body.put("keyword", "测试企业"); + body.put("password", "wrong-password"); + + AjaxResult response = controller.appBackDoorCompanies(body); + + assertEquals(500, response.get("code")); + assertEquals("测试授权密码错误", response.get("msg")); + verify(loginService, never()).listAvailableCompanies(org.mockito.ArgumentMatchers.any(), + org.mockito.ArgumentMatchers.anyInt()); + } + + @Test + void appBackDoorCompaniesReturnsMinimalOptionsAfterAuthorization() + { + AppUser company = new AppUser(); + company.setUserId(66L); + company.setName("测试企业"); + company.setIdCard("91659001MA77ABCD12"); + when(loginService.listAvailableCompanies("测试企业", 100)) + .thenReturn(Collections.singletonList(new CompanyLoginUser(company))); + JSONObject body = new JSONObject(); + body.put("keyword", "测试企业"); + body.put("password", "zkr2024"); + + AjaxResult response = controller.appBackDoorCompanies(body); + + assertEquals(200, response.get("code")); + assertEquals(1, ((java.util.List) response.get("data")).size()); + verify(loginService).listAvailableCompanies("测试企业", 100); + } + + @Test + void appBackDoorCompanyLoginReturnsCompanySiteToken() + { + AppUser company = new AppUser(); + company.setUserId(66L); + company.setName("测试企业"); + company.setIdCard("91659001MA77ABCD12"); + company.setIsCompanyUser("0"); + when(loginService.loginCompany(66L)) + .thenReturn(new CompanyLoginSession(company, "company-site-token")); + JSONObject body = new JSONObject(); + body.put("appUserId", 66L); + body.put("password", "zkr2024"); + + AjaxResult response = controller.appBackDoorCompanyLogin(body); + + assertEquals(200, response.get("code")); + assertEquals("company-site-token", response.get("token")); + assertEquals(66L, response.get("appUserId")); + assertEquals("0", response.get("isCompanyUser")); + assertEquals("测试企业", response.get("companyName")); + } + + @Test + void appBackDoorCompanyLoginRequiresPasswordBeforeCreatingToken() + { + JSONObject body = new JSONObject(); + body.put("appUserId", 66L); + body.put("password", "wrong-password"); + + AjaxResult response = controller.appBackDoorCompanyLogin(body); + + assertEquals(500, response.get("code")); + assertEquals("测试授权密码错误", response.get("msg")); + verify(loginService, never()).loginCompany(org.mockito.ArgumentMatchers.any()); + } } diff --git a/ruoyi-admin/src/test/java/com/ruoyi/web/service/BackDoorLoginServiceTest.java b/ruoyi-admin/src/test/java/com/ruoyi/web/service/BackDoorLoginServiceTest.java index 5279b8e..ac31a3a 100644 --- a/ruoyi-admin/src/test/java/com/ruoyi/web/service/BackDoorLoginServiceTest.java +++ b/ruoyi-admin/src/test/java/com/ruoyi/web/service/BackDoorLoginServiceTest.java @@ -13,6 +13,8 @@ import com.ruoyi.framework.web.service.UserDetailsServiceImpl; import com.ruoyi.system.mapper.SysUserMapper; import com.ruoyi.system.service.ISysUserService; import com.ruoyi.web.service.BackDoorLoginService.AppLoginSession; +import com.ruoyi.web.service.BackDoorLoginService.CompanyLoginSession; +import com.ruoyi.web.service.BackDoorLoginService.CompanyLoginUser; import com.ruoyi.web.service.BackDoorLoginService.LoginSession; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -153,6 +155,77 @@ class BackDoorLoginServiceTest verify(sysLoginService, never()).loginUserIdApp(org.mockito.ArgumentMatchers.any()); } + @Test + void listAvailableCompaniesReturnsMinimalMaskedOptions() + { + AppUser company = activeCompany(66L); + company.setIdCard("91659001MA77ABCD12"); + when(userMapper.selectBackDoorCompanyList("测试企业", 200)) + .thenReturn(Collections.singletonList(company)); + + List companies = service.listAvailableCompanies(" 测试企业 ", 500); + + assertEquals(1, companies.size()); + assertEquals(66L, companies.get(0).getAppUserId()); + assertEquals("测试企业", companies.get(0).getCompanyName()); + assertEquals("9165**********CD12", companies.get(0).getCompanyCodeMasked()); + verify(userMapper).selectBackDoorCompanyList("测试企业", 200); + } + + @Test + void listAvailableCompaniesUsesAccountIdWhenCompanyNameIsMissing() + { + AppUser company = activeCompany(67L); + company.setName(null); + when(userMapper.selectBackDoorCompanyList(null, 100)) + .thenReturn(Collections.singletonList(company)); + + List companies = service.listAvailableCompanies(null, 100); + + assertEquals("企业账号 67", companies.get(0).getCompanyName()); + } + + @Test + void loginCompanyCreatesSiteTokenForActiveCompany() + { + AppUser company = activeCompany(66L); + when(sysUserService.selectAppUserById(66L)).thenReturn(company); + when(sysLoginService.loginUserIdApp(company)).thenReturn("company-site-token"); + + CompanyLoginSession session = service.loginCompany(66L); + + assertSame(company, session.getAppUser()); + assertEquals("company-site-token", session.getToken()); + verify(sysLoginService).loginUserIdApp(company); + verify(tokenService, never()).createTokenHourTwo(org.mockito.ArgumentMatchers.any()); + } + + @Test + void loginCompanyRejectsNonCompanyAccount() + { + AppUser jobSeeker = activeCompany(66L); + jobSeeker.setIsCompanyUser("1"); + when(sysUserService.selectAppUserById(66L)).thenReturn(jobSeeker); + + ServiceException exception = assertThrows(ServiceException.class, () -> service.loginCompany(66L)); + + assertEquals("所选账号不是企业账号", exception.getMessage()); + verify(sysLoginService, never()).loginUserIdApp(org.mockito.ArgumentMatchers.any()); + } + + @Test + void loginCompanyRejectsDisabledCompany() + { + AppUser company = activeCompany(66L); + company.setStatus("1"); + when(sysUserService.selectAppUserById(66L)).thenReturn(company); + + ServiceException exception = assertThrows(ServiceException.class, () -> service.loginCompany(66L)); + + assertEquals("企业账号已停用,不能登录", exception.getMessage()); + verify(sysLoginService, never()).loginUserIdApp(org.mockito.ArgumentMatchers.any()); + } + private SysUser activeUser(Long userId) { SysUser user = new SysUser(); @@ -163,4 +236,15 @@ class BackDoorLoginServiceTest user.setDelFlag("0"); return user; } + + private AppUser activeCompany(Long userId) + { + AppUser company = new AppUser(); + company.setUserId(userId); + company.setName("测试企业"); + company.setIsCompanyUser("0"); + company.setStatus("0"); + company.setDelFlag("0"); + return company; + } } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java index 229cb98..ba9d566 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java @@ -23,6 +23,15 @@ public interface SysUserMapper */ List selectBackDoorUserList(@Param("keyword") String keyword, @Param("limit") int limit); + /** + * 查询小程序测试授权可选择的正常企业账号。 + * + * @param keyword 企业用户 ID、企业名称或统一社会信用代码关键字 + * @param limit 最大返回数量 + * @return 可登录的企业 AppUser + */ + List selectBackDoorCompanyList(@Param("keyword") String keyword, @Param("limit") int limit); + /** * 查询指定账号在后门登录规则下的最新有效用户 ID。 * diff --git a/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml index bed93cd..a764fb3 100644 --- a/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml @@ -110,6 +110,32 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" order by u.create_time desc nulls last, u.user_id desc limit 1 + + + select u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark,