feat: 添加小程序企业测试授权功能及相关接口

This commit is contained in:
2026-07-23 14:41:55 +08:00
parent 736dade03e
commit 487717986a
6 changed files with 392 additions and 0 deletions

View File

@@ -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"));

View File

@@ -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<CompanyLoginUser> 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;
}
}
}

View File

@@ -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());
}
}

View File

@@ -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<CompanyLoginUser> 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<CompanyLoginUser> 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;
}
}

View File

@@ -23,6 +23,15 @@ public interface SysUserMapper
*/
List<SysUser> selectBackDoorUserList(@Param("keyword") String keyword, @Param("limit") int limit);
/**
* 查询小程序测试授权可选择的正常企业账号。
*
* @param keyword 企业用户 ID、企业名称或统一社会信用代码关键字
* @param limit 最大返回数量
* @return 可登录的企业 AppUser
*/
List<AppUser> selectBackDoorCompanyList(@Param("keyword") String keyword, @Param("limit") int limit);
/**
* 查询指定账号在后门登录规则下的最新有效用户 ID。
*

View File

@@ -111,6 +111,32 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
limit 1
</select>
<!--
企业小程序会话以 app_user 为主体。这里只返回未删除、未停用的企业账号;
列表响应还会在服务层转换为最小化 DTO不直接暴露手机号等个人信息。
-->
<select id="selectBackDoorCompanyList" resultType="com.ruoyi.common.core.domain.entity.AppUser">
select u.user_id,
u.name,
u.id_card,
u.status,
u.del_flag,
u.is_company_user
from app_user u
where u.del_flag = '0'
and u.is_company_user = '0'
and (u.status is null or u.status = '' or u.status = '0')
<if test="keyword != null and keyword != ''">
and (
cast(u.user_id as varchar) like concat('%', cast(#{keyword} as varchar), '%')
or u.name like concat('%', cast(#{keyword} as varchar), '%')
or u.id_card like concat('%', cast(#{keyword} as varchar), '%')
)
</if>
order by u.name asc, u.user_id desc
limit #{limit}
</select>
<sql id="selectUserVo">
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,
d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.status as dept_status,