main主干首次提交,包含用户认证模块

This commit is contained in:
wh
2026-04-23 11:59:31 +08:00
commit cbef58aee5
65 changed files with 2335 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
package com.labelsys.backend.service;
import com.labelsys.backend.common.ResultCode;
import com.labelsys.backend.common.exception.BusinessException;
import com.labelsys.backend.common.exception.UnauthorizedException;
import com.labelsys.backend.context.LoginUser;
import com.labelsys.backend.dto.request.ChangePasswordRequest;
import com.labelsys.backend.dto.request.LoginRequest;
import com.labelsys.backend.dto.response.CompanyOptionResponse;
import com.labelsys.backend.dto.response.LoginResponse;
import com.labelsys.backend.entity.SysCompany;
import com.labelsys.backend.entity.SysUser;
import com.labelsys.backend.enums.CompanyStatus;
import com.labelsys.backend.enums.UserStatus;
import com.labelsys.backend.mapper.SysCompanyMapper;
import com.labelsys.backend.mapper.SysUserMapper;
import com.labelsys.backend.service.session.TokenSessionRepository;
import java.time.Duration;
import java.util.List;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class AuthService {
private final SysCompanyMapper sysCompanyMapper;
private final SysUserMapper sysUserMapper;
private final PasswordEncoder passwordEncoder;
private final TokenSessionRepository tokenSessionRepository;
@Value("${labelsys.session.ttl:PT2H}")
private Duration sessionTtl;
public List<CompanyOptionResponse> listAvailableCompanies(String phone) {
return sysCompanyMapper.findEnabledCompaniesByPhone(phone).stream()
.map(CompanyOptionResponse::from)
.toList();
}
public LoginResponse login(LoginRequest request) {
SysCompany company = loadEnabledCompany(request.companyCode());
SysUser user = loadEnabledUser(company.getId(), request.phone());
if (!passwordEncoder.matches(request.password(), user.getPasswordHash())) {
throw new UnauthorizedException("手机号、公司或密码错误");
}
LoginUser loginUser = LoginUser.from(user, company);
String token = UUID.randomUUID().toString();
tokenSessionRepository.save(token, loginUser, sessionTtl);
return LoginResponse.from(token, loginUser, company);
}
public LoginUser getCurrentUser(String token) {
return tokenSessionRepository.find(token)
.orElseThrow(() -> new UnauthorizedException("未登录或登录已过期"));
}
@Transactional
public void changePassword(LoginUser currentUser, ChangePasswordRequest request) {
if (!request.newPassword().equals(request.confirmPassword())) {
throw new BusinessException(ResultCode.BAD_REQUEST, "两次输入的新密码不一致");
}
SysUser user = sysUserMapper.findByIdAndCompanyId(currentUser.userId(), currentUser.companyId());
if (user == null || user.getStatus() != UserStatus.ENABLED) {
throw new UnauthorizedException("未登录或登录已过期");
}
if (!passwordEncoder.matches(request.oldPassword(), user.getPasswordHash())) {
throw new BusinessException(ResultCode.BAD_REQUEST, "旧密码错误");
}
sysUserMapper.updatePassword(user.getId(), user.getCompanyId(), passwordEncoder.encode(request.newPassword()), false);
sysUserMapper.bumpSessionVersion(user.getId(), user.getCompanyId());
tokenSessionRepository.removeAll(user.getId());
}
public void logout(String token) {
tokenSessionRepository.remove(token);
}
private SysCompany loadEnabledCompany(String companyCode) {
SysCompany company = sysCompanyMapper.findByCompanyCode(companyCode);
if (company == null || company.getStatus() != CompanyStatus.ENABLED) {
throw new UnauthorizedException("手机号、公司或密码错误");
}
return company;
}
private SysUser loadEnabledUser(Long companyId, String phone) {
SysUser user = sysUserMapper.findByCompanyIdAndPhone(companyId, phone);
if (user == null || user.getStatus() != UserStatus.ENABLED) {
throw new UnauthorizedException("手机号、公司或密码错误");
}
return user;
}
}

View File

@@ -0,0 +1,54 @@
package com.labelsys.backend.service;
import com.labelsys.backend.common.ResultCode;
import com.labelsys.backend.common.exception.BusinessException;
import com.labelsys.backend.common.exception.ForbiddenException;
import com.labelsys.backend.context.LoginUser;
import com.labelsys.backend.dto.request.CreateCompanyRequest;
import com.labelsys.backend.dto.request.UpdateCompanyStatusRequest;
import com.labelsys.backend.entity.SysCompany;
import com.labelsys.backend.mapper.SysCompanyMapper;
import com.labelsys.backend.util.IdGenerator;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class CompanyService {
private final SysCompanyMapper sysCompanyMapper;
public List<SysCompany> listCompanies(LoginUser currentUser) {
assertPlatformAdmin(currentUser);
return sysCompanyMapper.listAll();
}
public SysCompany createCompany(LoginUser currentUser, CreateCompanyRequest request) {
assertPlatformAdmin(currentUser);
if (sysCompanyMapper.findByCompanyCode(request.companyCode()) != null) {
throw new BusinessException(ResultCode.CONFLICT, "公司编码已存在");
}
SysCompany company = SysCompany.builder()
.id(IdGenerator.nextId())
.companyCode(request.companyCode())
.companyName(request.companyName())
.status(com.labelsys.backend.enums.CompanyStatus.ENABLED)
.build();
sysCompanyMapper.insert(company);
return company;
}
public void updateStatus(LoginUser currentUser, Long companyId, UpdateCompanyStatusRequest request) {
assertPlatformAdmin(currentUser);
if (sysCompanyMapper.updateStatus(companyId, request.status()) == 0) {
throw new BusinessException(ResultCode.NOT_FOUND, "公司不存在");
}
}
private void assertPlatformAdmin(LoginUser currentUser) {
if (!currentUser.isPlatformAdmin()) {
throw new ForbiddenException("仅平台管理员可操作");
}
}
}

View File

@@ -0,0 +1,93 @@
package com.labelsys.backend.service;
import com.labelsys.backend.context.LoginUser;
import com.labelsys.backend.entity.BizDataRecord;
import com.labelsys.backend.enums.UserRole;
import com.labelsys.backend.mapper.BizDataRecordMapper;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class DataPermissionService {
private final BizDataRecordMapper bizDataRecordMapper;
public List<BizDataRecord> listVisibleRecords(LoginUser currentUser) {
return switch (currentUser.role()) {
case EMPLOYEE -> bizDataRecordMapper.listVisibleByEmployee(currentUser.companyId(), currentUser.userId());
case MANAGER -> bizDataRecordMapper.listVisibleByManager(currentUser.companyId());
case ENGINEER -> bizDataRecordMapper.listVisibleByEngineer(currentUser.companyId());
};
}
public boolean canAccessCreator(LoginUser currentUser, Long creatorId, UserRole creatorRole) {
return switch (currentUser.role()) {
case EMPLOYEE -> currentUser.userId().equals(creatorId);
case MANAGER -> creatorRole == UserRole.EMPLOYEE || creatorRole == UserRole.MANAGER;
case ENGINEER -> true;
};
}
/**
* 通用数据过滤方法
*
* @param currentUser 当前登录用户
* @param allRecords 待过滤的全量数据列表
* @param roleExtractor 从数据对象中提取“关联角色”或“创建者角色”的函数
* @param ownerIdExtractor 从数据对象中提取“所有者ID”的函数用于员工只能看自己的情况
* @param <T> 数据类型
* @return 过滤后的数据列表
*/
public <T> List<T> filterByRole(
LoginUser currentUser,
List<T> allRecords,
Function<T, UserRole> roleExtractor,
Function<T, Long> ownerIdExtractor) {
if (allRecords == null || allRecords.isEmpty()) {
return List.of();
}
UserRole currentRole = currentUser.role();
Long currentUserId = currentUser.userId();
return allRecords.stream()
.filter(record -> {
UserRole recordRole = roleExtractor.apply(record);
Long recordOwnerId = ownerIdExtractor.apply(record);
return switch (currentRole) {
case EMPLOYEE ->
// 员工只能查看自己创建/拥有的数据
currentUserId.equals(recordOwnerId);
case MANAGER ->
// 经理可以查看员工和经理的数据,不能查看总工程师的数据
recordRole == UserRole.EMPLOYEE || recordRole == UserRole.MANAGER;
case ENGINEER ->
// 总工程师可以查看所有数据
true;
};
})
.collect(Collectors.toList());
}
/**
* 针对 BizDataRecord 的便捷调用方法
*/
// public List<BizDataRecord> listVisibleRecordsGeneric(LoginUser currentUser, List<BizDataRecord> allRecords) {
// return filterByRole(
// currentUser,
// allRecords,
// BizDataRecord::
// BizDataRecord::getCreatorRole, // 提取创建者角色
// BizDataRecord::getCreatorId // 提取创建者ID
// );
// }
}

View File

@@ -0,0 +1,26 @@
package com.labelsys.backend.service;
import com.labelsys.backend.context.LoginUser;
import com.labelsys.backend.dto.response.MenuResponse;
import com.labelsys.backend.mapper.SysMenuMapper;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class MenuService {
private final SysMenuMapper sysMenuMapper;
public List<MenuResponse> listCurrentMenus(LoginUser currentUser) {
List<String> positionCodes = java.util.Arrays.stream(com.labelsys.backend.enums.UserPosition.values())
.filter(position -> currentUser.position().canAccess(position))
.map(Enum::name)
.toList();
return sysMenuMapper.listCurrentMenus(currentUser.companyId(), positionCodes)
.stream()
.map(MenuResponse::from)
.toList();
}
}

View File

@@ -0,0 +1,134 @@
package com.labelsys.backend.service;
import com.labelsys.backend.common.ResultCode;
import com.labelsys.backend.common.exception.BusinessException;
import com.labelsys.backend.common.exception.ForbiddenException;
import com.labelsys.backend.context.LoginUser;
import com.labelsys.backend.dto.request.CreateCompanyAdminRequest;
import com.labelsys.backend.dto.request.CreateUserRequest;
import com.labelsys.backend.dto.request.UpdateUserAssignmentRequest;
import com.labelsys.backend.dto.request.UpdateUserStatusRequest;
import com.labelsys.backend.entity.SysCompany;
import com.labelsys.backend.entity.SysUser;
import com.labelsys.backend.enums.CompanyStatus;
import com.labelsys.backend.enums.UserPosition;
import com.labelsys.backend.enums.UserRole;
import com.labelsys.backend.enums.UserStatus;
import com.labelsys.backend.mapper.SysCompanyMapper;
import com.labelsys.backend.mapper.SysUserMapper;
import com.labelsys.backend.service.session.TokenSessionRepository;
import com.labelsys.backend.util.IdGenerator;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class UserService {
private static final String DEFAULT_PASSWORD = "123456";
private final SysUserMapper sysUserMapper;
private final SysCompanyMapper sysCompanyMapper;
private final PasswordEncoder passwordEncoder;
private final TokenSessionRepository tokenSessionRepository;
public List<SysUser> listCompanyUsers(LoginUser currentUser) {
assertCompanyAdmin(currentUser);
return sysUserMapper.listByCompanyId(currentUser.companyId());
}
public List<SysUser> listCompanyAdmins(LoginUser currentUser, Long companyId) {
assertPlatformAdmin(currentUser);
return sysUserMapper.listCompanyAdmins(companyId);
}
public SysUser createCompanyAdmin(LoginUser currentUser, CreateCompanyAdminRequest request) {
assertPlatformAdmin(currentUser);
ensureEnabledCompany(request.companyId());
return createUser(
request.companyId(),
new CreateUserRequest(request.phone(), request.username(), request.realName(), UserRole.EMPLOYEE, UserPosition.ADMIN)
);
}
public SysUser createCompanyUser(LoginUser currentUser, CreateUserRequest request) {
assertCompanyAdmin(currentUser);
return createUser(currentUser.companyId(), request);
}
public SysUser createCompanyUser(LoginUser currentUser, Long companyId, CreateUserRequest request) {
assertPlatformAdmin(currentUser);
ensureEnabledCompany(companyId);
return createUser(companyId, request);
}
@Transactional
public void updateAssignment(LoginUser currentUser, Long userId, UpdateUserAssignmentRequest request) {
assertCompanyAdmin(currentUser);
if (sysUserMapper.updateAssignment(userId, currentUser.companyId(), request.role(), request.position()) == 0) {
throw new BusinessException(ResultCode.NOT_FOUND, "用户不存在");
}
tokenSessionRepository.removeAll(userId);
}
@Transactional
public void updateStatus(LoginUser currentUser, Long userId, UpdateUserStatusRequest request) {
assertCompanyAdmin(currentUser);
if (sysUserMapper.updateStatus(userId, currentUser.companyId(), request.status()) == 0) {
throw new BusinessException(ResultCode.NOT_FOUND, "用户不存在");
}
tokenSessionRepository.removeAll(userId);
}
@Transactional
public void updateCompanyAdminStatus(LoginUser currentUser, Long companyId, Long userId, UpdateUserStatusRequest request) {
assertPlatformAdmin(currentUser);
if (sysUserMapper.updateStatus(userId, companyId, request.status()) == 0) {
throw new BusinessException(ResultCode.NOT_FOUND, "用户不存在");
}
tokenSessionRepository.removeAll(userId);
}
private SysUser createUser(Long companyId, CreateUserRequest request) {
if (sysUserMapper.findByCompanyIdAndPhone(companyId, request.phone()) != null) {
throw new BusinessException(ResultCode.CONFLICT, "同一公司内手机号已存在");
}
SysUser user = SysUser.builder()
.id(IdGenerator.nextId())
.companyId(companyId)
.phone(request.phone())
.username(request.username())
.realName(request.realName())
.role(request.role())
.position(request.position())
.passwordHash(passwordEncoder.encode(DEFAULT_PASSWORD))
.mustChangePassword(true)
.status(UserStatus.ENABLED)
.sessionVersion(1)
.build();
sysUserMapper.insert(user);
return user;
}
private void ensureEnabledCompany(Long companyId) {
SysCompany company = sysCompanyMapper.findById(companyId);
if (company == null || company.getStatus() != CompanyStatus.ENABLED) {
throw new BusinessException(ResultCode.NOT_FOUND, "公司不存在或已禁用");
}
}
private void assertPlatformAdmin(LoginUser currentUser) {
if (!currentUser.isPlatformAdmin()) {
throw new ForbiddenException("仅平台管理员可操作");
}
}
private void assertCompanyAdmin(LoginUser currentUser) {
if (currentUser.isPlatformAdmin() || currentUser.position() != UserPosition.ADMIN) {
throw new ForbiddenException("仅公司管理员可操作");
}
}
}

View File

@@ -0,0 +1,68 @@
package com.labelsys.backend.service.session;
import com.labelsys.backend.context.LoginUser;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Repository;
@Repository
@ConditionalOnProperty(prefix = "labelsys.session", name = "store-type", havingValue = "memory")
public class InMemoryTokenSessionRepository implements TokenSessionRepository {
private final Map<String, SessionEntry> sessions = new ConcurrentHashMap<>();
private final Map<Long, Set<String>> userTokens = new ConcurrentHashMap<>();
@Override
public void save(String token, LoginUser loginUser, Duration ttl) {
sessions.put(token, new SessionEntry(loginUser, Instant.now().plus(ttl)));
userTokens.computeIfAbsent(loginUser.userId(), ignored -> ConcurrentHashMap.newKeySet()).add(token);
}
@Override
public Optional<LoginUser> find(String token) {
SessionEntry entry = sessions.get(token);
if (entry == null) {
return Optional.empty();
}
if (entry.expiresAt().isBefore(Instant.now())) {
remove(token);
return Optional.empty();
}
return Optional.of(entry.loginUser());
}
@Override
public void refresh(String token, Duration ttl) {
SessionEntry entry = sessions.get(token);
if (entry != null) {
sessions.put(token, new SessionEntry(entry.loginUser(), Instant.now().plus(ttl)));
}
}
@Override
public void remove(String token) {
SessionEntry removed = sessions.remove(token);
if (removed != null) {
Set<String> tokens = userTokens.get(removed.loginUser().userId());
if (tokens != null) {
tokens.remove(token);
}
}
}
@Override
public void removeAll(Long userId) {
Set<String> tokens = userTokens.remove(userId);
if (tokens != null) {
tokens.forEach(sessions::remove);
}
}
private record SessionEntry(LoginUser loginUser, Instant expiresAt) {
}
}

View File

@@ -0,0 +1,82 @@
package com.labelsys.backend.service.session;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.labelsys.backend.common.exception.BusinessException;
import com.labelsys.backend.common.ResultCode;
import com.labelsys.backend.context.LoginUser;
import java.time.Duration;
import java.util.Optional;
import java.util.Set;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
@Repository
@ConditionalOnProperty(prefix = "labelsys.session", name = "store-type", havingValue = "redis", matchIfMissing = true)
public class RedisTokenSessionRepository implements TokenSessionRepository {
private static final String TOKEN_PREFIX = "token:";
private static final String USER_TOKENS_PREFIX = "user:tokens:";
private final StringRedisTemplate redisTemplate;
private final ObjectMapper objectMapper;
public RedisTokenSessionRepository(StringRedisTemplate redisTemplate, ObjectMapper objectMapper) {
this.redisTemplate = redisTemplate;
this.objectMapper = objectMapper;
}
@Override
public void save(String token, LoginUser loginUser, Duration ttl) {
try {
redisTemplate.opsForValue().set(tokenKey(token), objectMapper.writeValueAsString(loginUser), ttl);
redisTemplate.opsForSet().add(userTokensKey(loginUser.userId()), token);
redisTemplate.expire(userTokensKey(loginUser.userId()), ttl);
} catch (JsonProcessingException exception) {
throw new BusinessException(ResultCode.ERROR, "登录态序列化失败");
}
}
@Override
public Optional<LoginUser> find(String token) {
String value = redisTemplate.opsForValue().get(tokenKey(token));
if (value == null) {
return Optional.empty();
}
try {
return Optional.of(objectMapper.readValue(value, LoginUser.class));
} catch (JsonProcessingException exception) {
remove(token);
return Optional.empty();
}
}
@Override
public void refresh(String token, Duration ttl) {
redisTemplate.expire(tokenKey(token), ttl);
}
@Override
public void remove(String token) {
find(token).ifPresent(loginUser -> redisTemplate.opsForSet().remove(userTokensKey(loginUser.userId()), token));
redisTemplate.delete(tokenKey(token));
}
@Override
public void removeAll(Long userId) {
Set<String> tokens = redisTemplate.opsForSet().members(userTokensKey(userId));
if (tokens != null && !tokens.isEmpty()) {
redisTemplate.delete(tokens.stream().map(this::tokenKey).toList());
}
redisTemplate.delete(userTokensKey(userId));
}
private String tokenKey(String token) {
return TOKEN_PREFIX + token;
}
private String userTokensKey(Long userId) {
return USER_TOKENS_PREFIX + userId;
}
}

View File

@@ -0,0 +1,18 @@
package com.labelsys.backend.service.session;
import com.labelsys.backend.context.LoginUser;
import java.time.Duration;
import java.util.Optional;
public interface TokenSessionRepository {
void save(String token, LoginUser loginUser, Duration ttl);
Optional<LoginUser> find(String token);
void refresh(String token, Duration ttl);
void remove(String token);
void removeAll(Long userId);
}