feat(release): close handler and microphone gaps
This commit is contained in:
+33
@@ -8,6 +8,10 @@ import org.dromara.aihr.direct.AihrDirectDto.AdminFeedbackPage;
|
||||
import org.dromara.aihr.direct.AihrDirectDto.ChannelItem;
|
||||
import org.dromara.aihr.direct.AihrDirectDto.FeedbackItem;
|
||||
import org.dromara.aihr.direct.AihrDirectDto.FeedbackPage;
|
||||
import org.dromara.aihr.direct.AihrDirectDto.HandlerAssignRequest;
|
||||
import org.dromara.aihr.direct.AihrDirectDto.HandlerConfigResponse;
|
||||
import org.dromara.aihr.direct.AihrDirectDto.HandlerRemoveRequest;
|
||||
import org.dromara.aihr.direct.AihrDirectDto.HandlerUserItem;
|
||||
import org.dromara.aihr.direct.AihrDirectDto.ReplyRequest;
|
||||
import org.dromara.aihr.direct.AihrDirectDto.SubmitRequest;
|
||||
import org.dromara.common.core.domain.R;
|
||||
@@ -66,4 +70,33 @@ public class AihrDirectController {
|
||||
public R<AdminFeedbackItem> reply(@PathVariable Long id, @Valid @RequestBody ReplyRequest request) {
|
||||
return R.ok(directService.reply(id, request));
|
||||
}
|
||||
|
||||
@GetMapping("/admin/handlers")
|
||||
public R<HandlerConfigResponse> handlers() {
|
||||
return R.ok(directService.handlers());
|
||||
}
|
||||
|
||||
@GetMapping("/admin/handlers/{channelCode}/candidates")
|
||||
public R<List<HandlerUserItem>> handlerCandidates(
|
||||
@PathVariable String channelCode,
|
||||
@RequestParam(required = false) String keyword
|
||||
) {
|
||||
return R.ok(directService.handlerCandidates(channelCode, keyword));
|
||||
}
|
||||
|
||||
@PostMapping("/admin/handlers/{channelCode}")
|
||||
public R<HandlerConfigResponse> assignHandlers(
|
||||
@PathVariable String channelCode,
|
||||
@Valid @RequestBody HandlerAssignRequest request
|
||||
) {
|
||||
return R.ok(directService.assignHandlers(channelCode, request));
|
||||
}
|
||||
|
||||
@PostMapping("/admin/handlers/{channelCode}/remove")
|
||||
public R<HandlerConfigResponse> removeHandler(
|
||||
@PathVariable String channelCode,
|
||||
@Valid @RequestBody HandlerRemoveRequest request
|
||||
) {
|
||||
return R.ok(directService.removeHandler(channelCode, request));
|
||||
}
|
||||
}
|
||||
|
||||
+44
@@ -1,6 +1,8 @@
|
||||
package org.dromara.aihr.direct;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
@@ -82,4 +84,46 @@ public final class AihrDirectDto {
|
||||
List<AdminFeedbackItem> rows
|
||||
) {
|
||||
}
|
||||
|
||||
public record HandlerUserItem(
|
||||
Long userId,
|
||||
String userName,
|
||||
String nickName
|
||||
) {
|
||||
}
|
||||
|
||||
public record HandlerChannelItem(
|
||||
String channelCode,
|
||||
String channelName,
|
||||
String roleKey,
|
||||
boolean roleAvailable,
|
||||
List<HandlerUserItem> users
|
||||
) {
|
||||
}
|
||||
|
||||
public record HandlerConfigResponse(
|
||||
String tenantId,
|
||||
int minimumHandlers,
|
||||
List<HandlerChannelItem> channels
|
||||
) {
|
||||
}
|
||||
|
||||
public record HandlerAssignRequest(
|
||||
@NotEmpty(message = "请选择处理人")
|
||||
@Size(max = 20, message = "一次最多添加 20 名处理人")
|
||||
List<@NotNull(message = "处理人编号不能为空") Long> userIds,
|
||||
@NotBlank(message = "当前租户不能为空")
|
||||
@Size(max = 64, message = "当前租户不能超过 64 个字符")
|
||||
String expectedTenantId
|
||||
) {
|
||||
}
|
||||
|
||||
public record HandlerRemoveRequest(
|
||||
@NotNull(message = "处理人编号不能为空")
|
||||
Long userId,
|
||||
@NotBlank(message = "当前租户不能为空")
|
||||
@Size(max = 64, message = "当前租户不能超过 64 个字符")
|
||||
String expectedTenantId
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
+180
@@ -6,6 +6,11 @@ import org.dromara.aihr.direct.AihrDirectDto.AdminFeedbackPage;
|
||||
import org.dromara.aihr.direct.AihrDirectDto.ChannelItem;
|
||||
import org.dromara.aihr.direct.AihrDirectDto.FeedbackItem;
|
||||
import org.dromara.aihr.direct.AihrDirectDto.FeedbackPage;
|
||||
import org.dromara.aihr.direct.AihrDirectDto.HandlerAssignRequest;
|
||||
import org.dromara.aihr.direct.AihrDirectDto.HandlerChannelItem;
|
||||
import org.dromara.aihr.direct.AihrDirectDto.HandlerConfigResponse;
|
||||
import org.dromara.aihr.direct.AihrDirectDto.HandlerRemoveRequest;
|
||||
import org.dromara.aihr.direct.AihrDirectDto.HandlerUserItem;
|
||||
import org.dromara.aihr.direct.AihrDirectDto.ReplyRequest;
|
||||
import org.dromara.aihr.direct.AihrDirectDto.SubmitRequest;
|
||||
import org.dromara.aihr.knowledge.domain.AihrKnowledgePrincipal;
|
||||
@@ -44,6 +49,8 @@ public class AihrDirectService {
|
||||
|
||||
private static final int DEFAULT_PAGE_SIZE = 20;
|
||||
private static final int MAX_PAGE_SIZE = 100;
|
||||
private static final int MINIMUM_HANDLERS = 2;
|
||||
private static final int MAX_HANDLER_CANDIDATES = 50;
|
||||
private static final Pattern REQUEST_ID_PATTERN = Pattern.compile("[A-Za-z0-9._:-]+");
|
||||
private static final List<DirectChannel> CHANNELS = List.of(
|
||||
new DirectChannel("PRESIDENT", "总裁直达", "direct_president", true),
|
||||
@@ -206,6 +213,86 @@ public class AihrDirectService {
|
||||
return adminDetail(tenantId, feedbackId, channelCodes);
|
||||
}
|
||||
|
||||
public HandlerConfigResponse handlers() {
|
||||
AihrKnowledgePrincipal principal = principalResolver.current();
|
||||
requireHandlerAdmin(principal);
|
||||
return handlerConfig(adminTenant(principal));
|
||||
}
|
||||
|
||||
public List<HandlerUserItem> handlerCandidates(String rawChannelCode, String rawKeyword) {
|
||||
AihrKnowledgePrincipal principal = principalResolver.current();
|
||||
requireHandlerAdmin(principal);
|
||||
String tenantId = adminTenant(principal);
|
||||
DirectRole role = directRole(tenantId, channel(rawChannelCode));
|
||||
String keyword = clean(rawKeyword);
|
||||
String like = "%" + keyword + "%";
|
||||
return jdbcTemplate.query("""
|
||||
select u.user_id, u.user_name, u.nick_name
|
||||
from sys_user u
|
||||
where binary u.tenant_id = binary ?
|
||||
and u.user_type <> ?
|
||||
and u.status = '0' and u.del_flag = '0'
|
||||
and not exists (
|
||||
select 1
|
||||
from sys_user_role assigned
|
||||
where assigned.user_id = u.user_id and assigned.role_id = ?
|
||||
)
|
||||
and not exists (
|
||||
select 1
|
||||
from sys_user_role elevated
|
||||
join sys_role elevated_role on elevated_role.role_id = elevated.role_id
|
||||
where elevated.user_id = u.user_id
|
||||
and binary elevated_role.tenant_id = binary u.tenant_id
|
||||
and elevated_role.role_key = ?
|
||||
and elevated_role.status = '0' and elevated_role.del_flag = '0'
|
||||
)
|
||||
and (? = '' or u.user_name like ? or u.nick_name like ?)
|
||||
order by u.nick_name, u.user_name, u.user_id
|
||||
limit ?
|
||||
""", (rs, rowNum) -> new HandlerUserItem(
|
||||
rs.getLong("user_id"), rs.getString("user_name"), rs.getString("nick_name")
|
||||
), tenantId, UserType.APP_USER.getUserType(), role.roleId(),
|
||||
TenantConstants.SUPER_ADMIN_ROLE_KEY, keyword, like, like, MAX_HANDLER_CANDIDATES);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public HandlerConfigResponse assignHandlers(String rawChannelCode, HandlerAssignRequest request) {
|
||||
AihrKnowledgePrincipal principal = principalResolver.current();
|
||||
requireHandlerAdmin(principal);
|
||||
String tenantId = adminTenant(principal);
|
||||
requireExpectedTenant(request.expectedTenantId(), tenantId);
|
||||
DirectRole role = directRole(tenantId, channel(rawChannelCode));
|
||||
List<Long> userIds = request.userIds().stream().distinct().toList();
|
||||
for (Long userId : userIds) {
|
||||
requireEligibleHandler(tenantId, userId);
|
||||
jdbcTemplate.update("""
|
||||
insert into sys_user_role (user_id, role_id)
|
||||
select ?, ?
|
||||
where not exists (
|
||||
select 1 from sys_user_role where user_id = ? and role_id = ?
|
||||
)
|
||||
""", userId, role.roleId(), userId, role.roleId());
|
||||
}
|
||||
return handlerConfig(tenantId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public HandlerConfigResponse removeHandler(String rawChannelCode, HandlerRemoveRequest request) {
|
||||
AihrKnowledgePrincipal principal = principalResolver.current();
|
||||
requireHandlerAdmin(principal);
|
||||
String tenantId = adminTenant(principal);
|
||||
requireExpectedTenant(request.expectedTenantId(), tenantId);
|
||||
DirectRole role = directRole(tenantId, channel(rawChannelCode));
|
||||
jdbcTemplate.update("""
|
||||
delete assignment
|
||||
from sys_user_role assignment
|
||||
join sys_user u on u.user_id = assignment.user_id
|
||||
where assignment.role_id = ? and assignment.user_id = ?
|
||||
and binary u.tenant_id = binary ?
|
||||
""", role.roleId(), request.userId(), tenantId);
|
||||
return handlerConfig(tenantId);
|
||||
}
|
||||
|
||||
private FeedbackItem replay(AihrKnowledgePrincipal principal, String requestKey, String requestHash) {
|
||||
List<Replay> rows = jdbcTemplate.query("""
|
||||
select id, submit_request_hash
|
||||
@@ -297,6 +384,92 @@ public class AihrDirectService {
|
||||
return allowed;
|
||||
}
|
||||
|
||||
private HandlerConfigResponse handlerConfig(String tenantId) {
|
||||
List<HandlerChannelItem> items = CHANNELS.stream().map(channel -> {
|
||||
DirectRole role = optionalDirectRole(tenantId, channel);
|
||||
if (role == null) {
|
||||
return new HandlerChannelItem(channel.code(), channel.name(), channel.role(), false, List.of());
|
||||
}
|
||||
List<HandlerUserItem> users = jdbcTemplate.query("""
|
||||
select u.user_id, u.user_name, u.nick_name
|
||||
from sys_user_role assignment
|
||||
join sys_user u on u.user_id = assignment.user_id
|
||||
where assignment.role_id = ?
|
||||
and binary u.tenant_id = binary ?
|
||||
and u.user_type <> ?
|
||||
and u.status = '0' and u.del_flag = '0'
|
||||
and not exists (
|
||||
select 1
|
||||
from sys_user_role elevated
|
||||
join sys_role elevated_role on elevated_role.role_id = elevated.role_id
|
||||
where elevated.user_id = u.user_id
|
||||
and binary elevated_role.tenant_id = binary u.tenant_id
|
||||
and elevated_role.role_key = ?
|
||||
and elevated_role.status = '0' and elevated_role.del_flag = '0'
|
||||
)
|
||||
order by u.nick_name, u.user_name, u.user_id
|
||||
""", (rs, rowNum) -> new HandlerUserItem(
|
||||
rs.getLong("user_id"), rs.getString("user_name"), rs.getString("nick_name")
|
||||
), role.roleId(), tenantId, UserType.APP_USER.getUserType(), TenantConstants.SUPER_ADMIN_ROLE_KEY);
|
||||
return new HandlerChannelItem(channel.code(), channel.name(), channel.role(), true, users);
|
||||
}).toList();
|
||||
return new HandlerConfigResponse(tenantId, MINIMUM_HANDLERS, items);
|
||||
}
|
||||
|
||||
private DirectRole directRole(String tenantId, DirectChannel channel) {
|
||||
DirectRole role = optionalDirectRole(tenantId, channel);
|
||||
if (role == null) {
|
||||
throw new ServiceException(channel.name() + "处理角色不存在或已停用", HttpStatus.CONFLICT);
|
||||
}
|
||||
return role;
|
||||
}
|
||||
|
||||
private DirectRole optionalDirectRole(String tenantId, DirectChannel channel) {
|
||||
List<DirectRole> roles = jdbcTemplate.query("""
|
||||
select role_id, role_key
|
||||
from sys_role
|
||||
where binary tenant_id = binary ? and role_key = ?
|
||||
and status = '0' and del_flag = '0'
|
||||
order by role_id
|
||||
limit 1
|
||||
""", (rs, rowNum) -> new DirectRole(rs.getLong("role_id"), rs.getString("role_key")),
|
||||
tenantId, channel.role());
|
||||
return roles.isEmpty() ? null : roles.get(0);
|
||||
}
|
||||
|
||||
private void requireEligibleHandler(String tenantId, Long userId) {
|
||||
if (userId == null || userId < 1) {
|
||||
throw new ServiceException("处理人编号无效", HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
Integer count = jdbcTemplate.queryForObject("""
|
||||
select count(*)
|
||||
from sys_user u
|
||||
where binary u.tenant_id = binary ? and u.user_id = ?
|
||||
and u.user_type <> ?
|
||||
and u.status = '0' and u.del_flag = '0'
|
||||
and not exists (
|
||||
select 1
|
||||
from sys_user_role elevated
|
||||
join sys_role elevated_role on elevated_role.role_id = elevated.role_id
|
||||
where elevated.user_id = u.user_id
|
||||
and binary elevated_role.tenant_id = binary u.tenant_id
|
||||
and elevated_role.role_key = ?
|
||||
and elevated_role.status = '0' and elevated_role.del_flag = '0'
|
||||
)
|
||||
""", Integer.class, tenantId, userId, UserType.APP_USER.getUserType(),
|
||||
TenantConstants.SUPER_ADMIN_ROLE_KEY);
|
||||
if (count == null || count != 1) {
|
||||
throw new ServiceException("只能选择当前租户启用的非超级管理员后台用户", HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireHandlerAdmin(AihrKnowledgePrincipal principal) {
|
||||
if (UserType.APP_USER.getUserType().equals(principal.userType())
|
||||
|| !principal.roles().contains(TenantConstants.SUPER_ADMIN_ROLE_KEY)) {
|
||||
throw new ServiceException("仅超级管理员可配置直达处理人", HttpStatus.FORBIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
private static Query adminQuery(String tenantId, List<String> channelCodes, String status) {
|
||||
String placeholders = String.join(",", Collections.nCopies(channelCodes.size(), "?"));
|
||||
String statusFilter = status == null ? "" : " and status = ?";
|
||||
@@ -410,6 +583,10 @@ public class AihrDirectService {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static String clean(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private static String adminTenant(AihrKnowledgePrincipal principal) {
|
||||
String tenantId = TenantHelper.getTenantId();
|
||||
return tenantId == null || tenantId.isBlank() ? principal.tenantId() : tenantId.trim();
|
||||
@@ -433,6 +610,9 @@ public class AihrDirectService {
|
||||
private record DirectChannel(String code, String name, String role, boolean defaultAnonymous) {
|
||||
}
|
||||
|
||||
private record DirectRole(long roleId, String roleKey) {
|
||||
}
|
||||
|
||||
private record Replay(long id, String requestHash) {
|
||||
}
|
||||
|
||||
|
||||
+30
@@ -2,6 +2,7 @@ package org.dromara.aihr.direct;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckLogin;
|
||||
import org.dromara.aihr.direct.AihrDirectDto.AdminFeedbackItem;
|
||||
import org.dromara.aihr.direct.AihrDirectDto.HandlerUserItem;
|
||||
import org.dromara.aihr.knowledge.domain.AihrKnowledgePrincipal;
|
||||
import org.dromara.common.core.constant.HttpStatus;
|
||||
import org.dromara.common.core.constant.TenantConstants;
|
||||
@@ -62,6 +63,12 @@ class AihrDirectSecurityContractTest {
|
||||
assertTrue(AihrDirectController.class.isAnnotationPresent(SaCheckLogin.class));
|
||||
assertTrue(AihrDirectService.class.getMethod("reply", Long.class, AihrDirectDto.ReplyRequest.class)
|
||||
.isAnnotationPresent(Transactional.class));
|
||||
assertTrue(AihrDirectService.class.getMethod(
|
||||
"assignHandlers", String.class, AihrDirectDto.HandlerAssignRequest.class)
|
||||
.isAnnotationPresent(Transactional.class));
|
||||
assertTrue(AihrDirectService.class.getMethod(
|
||||
"removeHandler", String.class, AihrDirectDto.HandlerRemoveRequest.class)
|
||||
.isAnnotationPresent(Transactional.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -73,6 +80,29 @@ class AihrDirectSecurityContractTest {
|
||||
assertFalse(source.contains("o.ext_party_id = binary u.user_name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlerConfigurationRejectsNonSuperadminsAndNeverReturnsPhoneNumbers() throws Exception {
|
||||
Method guard = AihrDirectService.class.getDeclaredMethod(
|
||||
"requireHandlerAdmin", AihrKnowledgePrincipal.class);
|
||||
guard.setAccessible(true);
|
||||
|
||||
InvocationTargetException handler = assertThrows(InvocationTargetException.class,
|
||||
() -> guard.invoke(null, principal("sys_user", Set.of("direct_audit"))));
|
||||
assertEquals(HttpStatus.FORBIDDEN, ((ServiceException) handler.getCause()).getCode());
|
||||
|
||||
List<String> fields = List.of(HandlerUserItem.class.getRecordComponents()).stream()
|
||||
.map(component -> component.getName().toLowerCase())
|
||||
.toList();
|
||||
assertFalse(fields.contains("phone"));
|
||||
assertFalse(fields.contains("phonenumber"));
|
||||
|
||||
String source = Files.readString(Path.of(
|
||||
"src/main/java/org/dromara/aihr/direct/AihrDirectService.java"));
|
||||
assertTrue(source.contains("u.user_type <> ?"));
|
||||
assertTrue(source.contains("elevated_role.role_key = ?"));
|
||||
assertTrue(source.contains("只能选择当前租户启用的非超级管理员后台用户"));
|
||||
}
|
||||
|
||||
private static List<?> allowedChannels(AihrKnowledgePrincipal principal) throws Exception {
|
||||
Method method = AihrDirectService.class.getDeclaredMethod("allowedChannels", AihrKnowledgePrincipal.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
Reference in New Issue
Block a user