feat(personal): add reviewed enterprise publishing
This commit is contained in:
+15
@@ -8,6 +8,8 @@ import org.dromara.aihr.personal.domain.PersonalAssistantDto.ExportOutlineCreate
|
||||
import org.dromara.aihr.personal.domain.PersonalAssistantDto.ExportOutlineResponse;
|
||||
import org.dromara.aihr.personal.domain.PersonalAssistantDto.ExportOutlineUpdateRequest;
|
||||
import org.dromara.aihr.personal.domain.PersonalAssistantDto.ExportPptRequest;
|
||||
import org.dromara.aihr.personal.domain.PersonalAssistantDto.PublishRequestCreateRequest;
|
||||
import org.dromara.aihr.personal.domain.PersonalAssistantDto.PublishRequestResponse;
|
||||
import org.dromara.aihr.personal.domain.PersonalAssistantDto.ItemCreatedResponse;
|
||||
import org.dromara.aihr.personal.domain.PersonalAssistantDto.ItemResponse;
|
||||
import org.dromara.aihr.personal.domain.PersonalAssistantDto.OcrProgressResponse;
|
||||
@@ -24,6 +26,7 @@ import org.dromara.aihr.personal.service.PersonalCleanupService;
|
||||
import org.dromara.aihr.personal.service.PersonalIngestionService;
|
||||
import org.dromara.aihr.personal.service.PersonalExportService;
|
||||
import org.dromara.aihr.personal.service.PersonalPdfOcrService;
|
||||
import org.dromara.aihr.personal.service.PersonalPublishService;
|
||||
import org.dromara.aihr.personal.service.PersonalRetrievalService;
|
||||
import org.dromara.aihr.personal.service.PersonalSpaceService;
|
||||
import org.dromara.aihr.personal.service.PersonalUrlFetchService;
|
||||
@@ -63,6 +66,7 @@ public class PersonalAssistantController {
|
||||
private final PersonalCleanupService cleanupService;
|
||||
private final PersonalPdfOcrService pdfOcrService;
|
||||
private final PersonalExportService exportService;
|
||||
private final PersonalPublishService publishService;
|
||||
|
||||
@GetMapping("/space")
|
||||
public R<SpaceResponse> space() {
|
||||
@@ -191,6 +195,17 @@ public class PersonalAssistantController {
|
||||
return R.ok(exportService.downloadUrl(owner(), id));
|
||||
}
|
||||
|
||||
@PostMapping("/items/{id}/publish-requests")
|
||||
public R<PublishRequestResponse> requestPublish(@PathVariable long id,
|
||||
@RequestBody PublishRequestCreateRequest request) {
|
||||
return R.ok(publishService.request(owner(), id, request));
|
||||
}
|
||||
|
||||
@GetMapping("/publish-requests")
|
||||
public R<List<PublishRequestResponse>> publishRequests() {
|
||||
return R.ok(publishService.requests(owner()));
|
||||
}
|
||||
|
||||
private PersonalOwner owner() {
|
||||
return ownerProvider.current();
|
||||
}
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package org.dromara.aihr.personal.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckRole;
|
||||
import cn.dev33.satoken.annotation.SaMode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.aihr.personal.domain.PersonalAssistantDto.PublishRequestResponse;
|
||||
import org.dromara.aihr.personal.domain.PersonalAssistantDto.PublishReviewRequest;
|
||||
import org.dromara.aihr.personal.service.PersonalPublishService;
|
||||
import org.dromara.aihr.personal.support.PersonalOwnerProvider;
|
||||
import org.dromara.common.core.constant.TenantConstants;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/api/aihr/personal-assistant/admin/publish-requests")
|
||||
@SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, "hr_operator"}, mode = SaMode.OR)
|
||||
public class PersonalPublishAdminController {
|
||||
|
||||
private final PersonalOwnerProvider ownerProvider;
|
||||
private final PersonalPublishService publishService;
|
||||
|
||||
@GetMapping
|
||||
public R<List<PublishRequestResponse>> pending() {
|
||||
return R.ok(publishService.pending(ownerProvider.current()));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/review")
|
||||
public R<PublishRequestResponse> review(@PathVariable long id, @RequestBody PublishReviewRequest request) {
|
||||
return R.ok(publishService.review(ownerProvider.current(), id, request));
|
||||
}
|
||||
}
|
||||
+12
@@ -110,6 +110,18 @@ public final class PersonalAssistantDto {
|
||||
Long ossId) {
|
||||
}
|
||||
|
||||
public record PublishRequestCreateRequest(String targetScope, String reason) {
|
||||
}
|
||||
|
||||
public record PublishReviewRequest(boolean approved, String comment) {
|
||||
}
|
||||
|
||||
public record PublishRequestResponse(long id, long itemId, String title, String sourceType,
|
||||
String targetScope, String reason, String sanitizedPreview, String status,
|
||||
Long reviewerUserId, String reviewComment,
|
||||
Long publishedKnowledgeId, LocalDateTime createdAt) {
|
||||
}
|
||||
|
||||
private static Long legacyId(String value) {
|
||||
if (value == null || value.isBlank()) return null;
|
||||
try {
|
||||
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
package org.dromara.aihr.personal.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import org.dromara.aihr.personal.domain.PersonalAssistantDto.PublishRequestCreateRequest;
|
||||
import org.dromara.aihr.personal.domain.PersonalAssistantDto.PublishRequestResponse;
|
||||
import org.dromara.aihr.personal.domain.PersonalAssistantDto.PublishReviewRequest;
|
||||
import org.dromara.aihr.personal.support.PersonalOwner;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.function.LongSupplier;
|
||||
|
||||
@Service
|
||||
public class PersonalPublishService {
|
||||
|
||||
private static final String ALLOWED_SCOPE = "POSITION:生活顾问";
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
private final EnterprisePublisher publisher;
|
||||
private final LongSupplier idSupplier;
|
||||
|
||||
@Autowired
|
||||
public PersonalPublishService(JdbcTemplate jdbc) {
|
||||
this(jdbc, new DefaultEnterprisePublisher(jdbc), IdWorker::getId);
|
||||
}
|
||||
|
||||
private PersonalPublishService(JdbcTemplate jdbc, EnterprisePublisher publisher, LongSupplier idSupplier) {
|
||||
this.jdbc = jdbc;
|
||||
this.publisher = publisher;
|
||||
this.idSupplier = idSupplier;
|
||||
}
|
||||
|
||||
public static PersonalPublishService forTest(JdbcTemplate jdbc, EnterprisePublisher publisher,
|
||||
LongSupplier idSupplier) {
|
||||
return new PersonalPublishService(jdbc, publisher, idSupplier);
|
||||
}
|
||||
|
||||
public PublishRequestResponse request(PersonalOwner owner, long itemId, PublishRequestCreateRequest request) {
|
||||
requireOwner(owner);
|
||||
String targetScope = clean(request == null ? null : request.targetScope());
|
||||
String reason = clean(request == null ? null : request.reason());
|
||||
if (!ALLOWED_SCOPE.equals(targetScope) || reason.isEmpty()) {
|
||||
throw new ServiceException("PERSONAL_PUBLISH_REQUEST_INVALID");
|
||||
}
|
||||
List<Map<String, Object>> items = jdbc.queryForList("""
|
||||
select id, title, source_type from aihr_personal_item
|
||||
where binary tenant_id = binary ? and owner_user_id = ? and id = ? and status = 'READY'
|
||||
limit 1
|
||||
""", owner.tenantId(), owner.userId(), itemId);
|
||||
if (items.size() != 1) throw new ServiceException("PERSONAL_ITEM_NOT_FOUND");
|
||||
Integer pending = jdbc.queryForObject("""
|
||||
select count(*) from aihr_personal_publish_request
|
||||
where binary tenant_id = binary ? and applicant_user_id = ? and item_id = ? and status = 'PENDING'
|
||||
""", Integer.class, owner.tenantId(), owner.userId(), itemId);
|
||||
if (pending != null && pending > 0) throw new ServiceException("PERSONAL_PUBLISH_ALREADY_PENDING");
|
||||
long id = positive(idSupplier.getAsLong());
|
||||
int inserted = jdbc.update("""
|
||||
insert into aihr_personal_publish_request
|
||||
(id, tenant_id, item_id, applicant_user_id, target_scope, reason, status,
|
||||
create_time, update_time)
|
||||
values (?, ?, ?, ?, ?, ?, 'PENDING', now(), now())
|
||||
""", id, owner.tenantId(), itemId, owner.userId(), targetScope, truncate(reason, 500));
|
||||
if (inserted != 1) throw new ServiceException("PERSONAL_PUBLISH_CREATE_FAILED");
|
||||
Map<String, Object> item = items.get(0);
|
||||
return new PublishRequestResponse(id, itemId, clean(String.valueOf(item.get("title"))),
|
||||
clean(String.valueOf(item.get("source_type"))), targetScope, reason, null, "PENDING",
|
||||
null, null, null, LocalDateTime.now());
|
||||
}
|
||||
|
||||
public List<PublishRequestResponse> requests(PersonalOwner owner) {
|
||||
requireOwner(owner);
|
||||
return rows(jdbc.queryForList("""
|
||||
select r.*, i.title, i.source_type
|
||||
from aihr_personal_publish_request r
|
||||
join aihr_personal_item i on i.id = r.item_id and binary i.tenant_id = binary r.tenant_id
|
||||
and i.owner_user_id = r.applicant_user_id
|
||||
where binary r.tenant_id = binary ? and r.applicant_user_id = ?
|
||||
order by r.create_time desc, r.id desc limit 100
|
||||
""", owner.tenantId(), owner.userId()));
|
||||
}
|
||||
|
||||
public List<PublishRequestResponse> pending(PersonalOwner reviewer) {
|
||||
requireOwner(reviewer);
|
||||
return rows(jdbc.queryForList("""
|
||||
select r.*, i.title, i.source_type,
|
||||
(select group_concat(pf.content order by pf.idx separator '\n')
|
||||
from aihr_personal_fragment pf
|
||||
where binary pf.tenant_id = binary r.tenant_id
|
||||
and pf.owner_user_id = r.applicant_user_id and pf.item_id = r.item_id) preview_content
|
||||
from aihr_personal_publish_request r
|
||||
join aihr_personal_item i on i.id = r.item_id and binary i.tenant_id = binary r.tenant_id
|
||||
and i.owner_user_id = r.applicant_user_id
|
||||
where binary r.tenant_id = binary ? and r.status = 'PENDING'
|
||||
order by r.create_time, r.id limit 200
|
||||
""", reviewer.tenantId()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PublishRequestResponse review(PersonalOwner reviewer, long requestId, PublishReviewRequest request) {
|
||||
requireOwner(reviewer);
|
||||
String comment = clean(request == null ? null : request.comment());
|
||||
if (requestId <= 0 || comment.isEmpty()) throw new ServiceException("PERSONAL_PUBLISH_REVIEW_INVALID");
|
||||
List<Map<String, Object>> rows = jdbc.queryForList("""
|
||||
select r.id, r.tenant_id, r.item_id, r.applicant_user_id, r.target_scope, r.reason, r.status,
|
||||
i.title, i.source_type
|
||||
from aihr_personal_publish_request r
|
||||
join aihr_personal_item i on i.id = r.item_id and binary i.tenant_id = binary r.tenant_id
|
||||
and i.owner_user_id = r.applicant_user_id
|
||||
where r.id = ? and binary r.tenant_id = binary ? and r.status = 'PENDING'
|
||||
limit 1 for update
|
||||
""", requestId, reviewer.tenantId());
|
||||
if (rows.size() != 1) throw new ServiceException("PERSONAL_PUBLISH_NOT_FOUND");
|
||||
Map<String, Object> row = rows.get(0);
|
||||
if (!request.approved()) {
|
||||
int rejected = jdbc.update("""
|
||||
update aihr_personal_publish_request
|
||||
set status = 'REJECTED', reviewer_user_id = ?, review_comment = ?,
|
||||
review_time = now(), update_time = now()
|
||||
where id = ? and status = 'PENDING'
|
||||
""", reviewer.userId(), truncate(comment, 500), requestId);
|
||||
if (rejected != 1) throw new ServiceException("PERSONAL_PUBLISH_NOT_FOUND");
|
||||
return response(row, "REJECTED", reviewer.userId(), comment, null);
|
||||
}
|
||||
long applicantId = number(row, "applicant_user_id");
|
||||
long itemId = number(row, "item_id");
|
||||
List<Map<String, Object>> fragmentRows = jdbc.queryForList("""
|
||||
select content from aihr_personal_fragment
|
||||
where binary tenant_id = binary ? and owner_user_id = ? and item_id = ?
|
||||
order by idx
|
||||
""", reviewer.tenantId(), applicantId, itemId);
|
||||
List<String> sanitized = new ArrayList<>();
|
||||
for (Map<String, Object> fragment : fragmentRows) {
|
||||
String content = PersonalPromptSanitizer.sanitize(clean(String.valueOf(fragment.get("content"))));
|
||||
if (!content.isEmpty()) sanitized.add(content);
|
||||
}
|
||||
if (sanitized.isEmpty() || sanitized.stream().anyMatch(PersonalPromptSanitizer::containsSensitive)) {
|
||||
throw new ServiceException("PERSONAL_PUBLISH_SANITIZE_FAILED");
|
||||
}
|
||||
String title = PersonalPromptSanitizer.sanitize(clean(String.valueOf(row.get("title"))));
|
||||
String scope = clean(String.valueOf(row.get("target_scope")));
|
||||
if (!ALLOWED_SCOPE.equals(scope)) throw new ServiceException("PERSONAL_PUBLISH_SCOPE_FORBIDDEN");
|
||||
long knowledgeId = publisher.publish(reviewer.tenantId(), reviewer.userId(), requestId,
|
||||
title, scope, List.copyOf(sanitized));
|
||||
int approved = jdbc.update("""
|
||||
update aihr_personal_publish_request
|
||||
set status = 'APPROVED', reviewer_user_id = ?, review_comment = ?,
|
||||
published_knowledge_id = ?, review_time = now(), update_time = now()
|
||||
where id = ? and status = ?
|
||||
""", reviewer.userId(), truncate(comment, 500), knowledgeId, requestId, "PENDING");
|
||||
if (approved != 1) throw new ServiceException("PERSONAL_PUBLISH_NOT_FOUND");
|
||||
return response(row, "APPROVED", reviewer.userId(), comment, knowledgeId);
|
||||
}
|
||||
|
||||
private static List<PublishRequestResponse> rows(List<Map<String, Object>> rows) {
|
||||
return rows.stream().map(row -> response(row, clean(String.valueOf(row.get("status"))),
|
||||
nullableLong(row.get("reviewer_user_id")), cleanNullable(row.get("review_comment")),
|
||||
nullableLong(row.get("published_knowledge_id")))).toList();
|
||||
}
|
||||
|
||||
private static PublishRequestResponse response(Map<String, Object> row, String status, Long reviewerId,
|
||||
String comment, Long knowledgeId) {
|
||||
Object created = row.get("create_time");
|
||||
LocalDateTime createdAt = created instanceof java.sql.Timestamp timestamp
|
||||
? timestamp.toLocalDateTime() : created instanceof LocalDateTime time ? time : null;
|
||||
return new PublishRequestResponse(number(row, "id"), number(row, "item_id"),
|
||||
clean(String.valueOf(row.get("title"))), clean(String.valueOf(row.get("source_type"))),
|
||||
clean(String.valueOf(row.get("target_scope"))), clean(String.valueOf(row.get("reason"))),
|
||||
sanitizedPreview(row.get("preview_content")), status, reviewerId, comment, knowledgeId, createdAt);
|
||||
}
|
||||
|
||||
private static String sanitizedPreview(Object value) {
|
||||
String preview = PersonalPromptSanitizer.sanitize(clean(value == null ? null : String.valueOf(value)));
|
||||
return preview.isEmpty() ? null : truncate(preview, 500);
|
||||
}
|
||||
|
||||
private static void requireOwner(PersonalOwner owner) {
|
||||
if (owner == null || owner.userId() <= 0 || clean(owner.tenantId()).isEmpty()) {
|
||||
throw new ServiceException("PERSONAL_OWNER_REQUIRED");
|
||||
}
|
||||
}
|
||||
|
||||
private static long number(Map<String, Object> row, String key) {
|
||||
Object value = row.get(key);
|
||||
if (!(value instanceof Number number)) throw new ServiceException("PERSONAL_PUBLISH_DATA_INVALID");
|
||||
return number.longValue();
|
||||
}
|
||||
|
||||
private static Long nullableLong(Object value) {
|
||||
return value instanceof Number number ? number.longValue() : null;
|
||||
}
|
||||
|
||||
private static String cleanNullable(Object value) {
|
||||
String text = clean(value == null ? null : String.valueOf(value));
|
||||
return text.isEmpty() ? null : text;
|
||||
}
|
||||
|
||||
private static long positive(long value) {
|
||||
if (value <= 0) throw new ServiceException("PERSONAL_PUBLISH_CREATE_FAILED");
|
||||
return value;
|
||||
}
|
||||
|
||||
private static String clean(String value) {
|
||||
return value == null || "null".equals(value) ? "" : value.trim();
|
||||
}
|
||||
|
||||
private static String truncate(String value, int max) {
|
||||
return value.length() <= max ? value : value.substring(0, max);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface EnterprisePublisher {
|
||||
long publish(String tenantId, long reviewerUserId, long requestId, String title,
|
||||
String targetScope, List<String> sanitizedFragments);
|
||||
}
|
||||
|
||||
private static final class DefaultEnterprisePublisher implements EnterprisePublisher {
|
||||
private final JdbcTemplate jdbc;
|
||||
|
||||
private DefaultEnterprisePublisher(JdbcTemplate jdbc) {
|
||||
this.jdbc = jdbc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long publish(String tenantId, long reviewerUserId, long requestId, String title,
|
||||
String targetScope, List<String> sanitizedFragments) {
|
||||
long knowledgeId = IdWorker.getId();
|
||||
String docId = UUID.randomUUID().toString().replace("-", "");
|
||||
int knowledge = jdbc.update("""
|
||||
insert into aihr_knowledge_info
|
||||
(id, tenant_id, name, description, `separator`, overlap_char, retrieve_limit,
|
||||
similarity_threshold, text_block_size, enable_hybrid, hybrid_alpha, system_prompt,
|
||||
create_by, create_time, update_by, update_time, remark)
|
||||
values (?, ?, ?, '由个人知识审核脱敏后生成', '\n\n', 120, 5, 0.5, 800, 1, 1.0,
|
||||
'回答必须引用已审核片段,不得还原个人敏感信息。', ?, now(), ?, now(), ?)
|
||||
""", knowledgeId, tenantId, truncate("个人沉淀 · " + title, 100), reviewerUserId,
|
||||
reviewerUserId, "personal-publish:" + requestId);
|
||||
if (knowledge != 1) throw new ServiceException("PERSONAL_PUBLISH_ENTERPRISE_FAILED");
|
||||
for (int index = 0; index < sanitizedFragments.size(); index++) {
|
||||
int inserted = jdbc.update("""
|
||||
insert into aihr_knowledge_fragment
|
||||
(tenant_id, knowledge_id, idx, doc_id, content, create_by, create_time,
|
||||
update_by, update_time, remark)
|
||||
values (?, ?, ?, ?, ?, ?, now(), ?, now(), ?)
|
||||
""", tenantId, knowledgeId, index + 1, docId, sanitizedFragments.get(index),
|
||||
reviewerUserId, reviewerUserId, "personal-publish:" + requestId);
|
||||
if (inserted != 1) throw new ServiceException("PERSONAL_PUBLISH_ENTERPRISE_FAILED");
|
||||
}
|
||||
String position = targetScope.substring("POSITION:".length());
|
||||
int acl = jdbc.update("""
|
||||
insert into aihr_knowledge_acl
|
||||
(tenant_id, knowledge_id, access_scope, position_name, classification,
|
||||
enabled, create_time, update_time)
|
||||
values (?, ?, 'POSITION', ?, 'INTERNAL', 1, now(), now())
|
||||
""", tenantId, knowledgeId, position);
|
||||
if (acl != 1) throw new ServiceException("PERSONAL_PUBLISH_ENTERPRISE_FAILED");
|
||||
return knowledgeId;
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -12,6 +12,7 @@ import org.dromara.aihr.personal.service.PersonalCleanupService;
|
||||
import org.dromara.aihr.personal.service.PersonalIngestionService;
|
||||
import org.dromara.aihr.personal.service.PersonalExportService;
|
||||
import org.dromara.aihr.personal.service.PersonalRetrievalService;
|
||||
import org.dromara.aihr.personal.service.PersonalPublishService;
|
||||
import org.dromara.aihr.personal.service.PersonalSpaceService;
|
||||
import org.dromara.aihr.personal.service.PersonalUrlFetchService;
|
||||
import org.dromara.aihr.personal.support.PersonalOwner;
|
||||
@@ -51,12 +52,13 @@ class PersonalAssistantControllerTest {
|
||||
org.dromara.aihr.personal.service.PersonalPdfOcrService pdfOcr =
|
||||
mock(org.dromara.aihr.personal.service.PersonalPdfOcrService.class);
|
||||
PersonalExportService exports = mock(PersonalExportService.class);
|
||||
PersonalPublishService publishes = mock(PersonalPublishService.class);
|
||||
when(urls.fetch("https://example.com/a")).thenReturn(new PersonalUrlFetchService.FetchResult(
|
||||
java.net.URI.create("https://example.com/a"), 200, "text/plain", "a".getBytes(),
|
||||
java.time.Instant.now(), "hash"));
|
||||
|
||||
PersonalAssistantController controller = new PersonalAssistantController(owners, spaces, ingestion, urls,
|
||||
retrieval, answers, cleanup, pdfOcr, exports);
|
||||
retrieval, answers, cleanup, pdfOcr, exports, publishes);
|
||||
controller.space();
|
||||
controller.items(1, 20, null, null, null, null, null);
|
||||
controller.createText(new TextItemRequest("note", "body", null, List.of()));
|
||||
@@ -78,8 +80,12 @@ class PersonalAssistantControllerTest {
|
||||
controller.confirmOutline(7L);
|
||||
controller.enqueuePpt(7L, new ExportPptRequest("standard"));
|
||||
controller.exportDownloadUrl(7L);
|
||||
controller.requestPublish(9L,
|
||||
new org.dromara.aihr.personal.domain.PersonalAssistantDto.PublishRequestCreateRequest(
|
||||
"POSITION:生活顾问", "沉淀经验"));
|
||||
controller.publishRequests();
|
||||
|
||||
verify(owners, times(21)).current();
|
||||
verify(owners, times(23)).current();
|
||||
assertFalse(PersonalAssistantController.class.isAnnotationPresent(SaIgnore.class));
|
||||
for (var method : PersonalAssistantController.class.getDeclaredMethods()) {
|
||||
assertFalse(method.isAnnotationPresent(SaIgnore.class), method.getName());
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package org.dromara.aihr.personal;
|
||||
|
||||
import org.dromara.aihr.personal.domain.PersonalAssistantDto.PublishReviewRequest;
|
||||
import org.dromara.aihr.personal.service.PersonalPublishService;
|
||||
import org.dromara.aihr.personal.service.PersonalPublishService.EnterprisePublisher;
|
||||
import org.dromara.aihr.personal.controller.PersonalPublishAdminController;
|
||||
import org.dromara.aihr.personal.support.PersonalOwner;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.contains;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@Tag("dev")
|
||||
class PersonalPublishServiceTest {
|
||||
|
||||
private static final PersonalOwner REVIEWER = new PersonalOwner("000000", 901L, null);
|
||||
|
||||
@Test
|
||||
void adminControllerRequiresHrOrSuperAdminRole() {
|
||||
var annotation = PersonalPublishAdminController.class.getAnnotation(cn.dev33.satoken.annotation.SaCheckRole.class);
|
||||
assertTrue(annotation != null);
|
||||
assertEquals(cn.dev33.satoken.annotation.SaMode.OR, annotation.mode());
|
||||
assertTrue(List.of(annotation.value()).contains("superadmin"));
|
||||
assertTrue(List.of(annotation.value()).contains("hr_operator"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvalCreatesSanitizedEnterpriseCopyWithoutChangingPersonalOwner() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
EnterprisePublisher publisher = mock(EnterprisePublisher.class);
|
||||
when(jdbc.queryForList(contains("from aihr_personal_publish_request r"), eq(88L), eq("000000")))
|
||||
.thenReturn(List.of(Map.of(
|
||||
"id", 88L, "tenant_id", "000000", "item_id", 1201L, "applicant_user_id", 101L,
|
||||
"title", "投诉复盘", "source_type", "TEXT", "target_scope", "POSITION:生活顾问",
|
||||
"reason", "沉淀优秀经验", "status", "PENDING")));
|
||||
when(jdbc.queryForList(contains("from aihr_personal_fragment"),
|
||||
eq("000000"), eq(101L), eq(1201L))).thenReturn(List.of(Map.of(
|
||||
"content", "联系人:张三,电话13800000000。投诉闭环要明确责任人。")));
|
||||
when(publisher.publish(eq("000000"), eq(901L), eq(88L), eq("投诉复盘"),
|
||||
eq("POSITION:生活顾问"), org.mockito.ArgumentMatchers.anyList()))
|
||||
.thenReturn(7001L);
|
||||
when(jdbc.update(contains("status = 'APPROVED'"), eq(901L), eq("脱敏后可入库"), eq(7001L),
|
||||
eq(88L), eq("PENDING"))).thenReturn(1);
|
||||
PersonalPublishService service = PersonalPublishService.forTest(jdbc, publisher, () -> 5001L);
|
||||
|
||||
var result = service.review(REVIEWER, 88L, new PublishReviewRequest(true, "脱敏后可入库"));
|
||||
|
||||
assertEquals("APPROVED", result.status());
|
||||
assertEquals(7001L, result.publishedKnowledgeId());
|
||||
@SuppressWarnings("unchecked") ArgumentCaptor<List<String>> fragments = ArgumentCaptor.forClass(List.class);
|
||||
verify(publisher).publish(eq("000000"), eq(901L), eq(88L), eq("投诉复盘"),
|
||||
eq("POSITION:生活顾问"), fragments.capture());
|
||||
assertFalse(fragments.getValue().get(0).contains("张三"));
|
||||
assertFalse(fragments.getValue().get(0).contains("13800000000"));
|
||||
verify(jdbc, never()).update(contains("update aihr_personal_item set owner_user_id"),
|
||||
anyLong(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvalRejectsMissingTicketOrSensitiveResidualWithoutPublishing() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
EnterprisePublisher publisher = mock(EnterprisePublisher.class);
|
||||
PersonalPublishService service = PersonalPublishService.forTest(jdbc, publisher, () -> 5001L);
|
||||
|
||||
assertThrows(RuntimeException.class,
|
||||
() -> service.review(REVIEWER, 88L, new PublishReviewRequest(true, "")));
|
||||
|
||||
verify(publisher, never()).publish(anyString(), anyLong(), anyLong(), anyString(), anyString(),
|
||||
org.mockito.ArgumentMatchers.anyList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pendingReviewExposesOnlySanitizedPreview() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
EnterprisePublisher publisher = mock(EnterprisePublisher.class);
|
||||
when(jdbc.queryForList(anyString(), eq("000000"))).thenReturn(List.of(Map.of(
|
||||
"id", 88L, "item_id", 1201L, "title", "投诉复盘", "source_type", "TEXT",
|
||||
"target_scope", "POSITION:生活顾问", "reason", "沉淀优秀经验", "status", "PENDING",
|
||||
"preview_content", "姓名:张三,联系电话:13800000000。投诉闭环要明确责任人。")));
|
||||
PersonalPublishService service = PersonalPublishService.forTest(jdbc, publisher, () -> 5001L);
|
||||
|
||||
var result = service.pending(REVIEWER).get(0);
|
||||
|
||||
ArgumentCaptor<String> sql = ArgumentCaptor.forClass(String.class);
|
||||
verify(jdbc).queryForList(sql.capture(), eq("000000"));
|
||||
assertTrue(sql.getValue().contains("preview_content"));
|
||||
assertEquals("[姓名],联系电话:[手机号]。投诉闭环要明确责任人。", result.sanitizedPreview());
|
||||
assertFalse(result.sanitizedPreview().contains("张三"));
|
||||
assertFalse(result.sanitizedPreview().contains("13800000000"));
|
||||
}
|
||||
}
|
||||
+9
@@ -62,6 +62,7 @@ class PersonalSchemaContractTest {
|
||||
String ocrJob = tableDefinition(sql, "aihr_personal_ocr_job");
|
||||
String ocrPage = tableDefinition(sql, "aihr_personal_ocr_page");
|
||||
String exportTask = tableDefinition(sql, "aihr_personal_export_task");
|
||||
String publishRequest = tableDefinition(sql, "aihr_personal_publish_request");
|
||||
|
||||
for (String definition : new String[] {
|
||||
space, item, fragment, session, message, cleanup, ocrJob, ocrPage, exportTask
|
||||
@@ -152,6 +153,14 @@ class PersonalSchemaContractTest {
|
||||
"key `idx_personal_export_owner` (`tenant_id`, `owner_user_id`, `update_time`)"));
|
||||
assertTrue(exportTask.contains("key `idx_personal_export_status` (`status`, `update_time`)"));
|
||||
|
||||
assertTrue(publishRequest.contains("`item_id` bigint not null"));
|
||||
assertTrue(publishRequest.contains("`applicant_user_id` bigint not null"));
|
||||
assertTrue(publishRequest.contains("`target_scope` varchar(100) not null"));
|
||||
assertTrue(publishRequest.contains("`status` varchar(20) not null default 'pending'"));
|
||||
assertTrue(publishRequest.contains("`published_knowledge_id` bigint default null"));
|
||||
assertTrue(publishRequest.contains(
|
||||
"key `idx_personal_publish_status` (`tenant_id`, `status`, `update_time`)"));
|
||||
|
||||
assertFalse(sql.contains("alter table aihr_knowledge_fragment"),
|
||||
"Personal schema must not mutate enterprise knowledge tables");
|
||||
assertFalse(sql.contains("alter table `aihr_knowledge_fragment`"),
|
||||
|
||||
@@ -186,3 +186,23 @@ CREATE TABLE IF NOT EXISTS `aihr_personal_export_task` (
|
||||
KEY `idx_personal_export_owner` (`tenant_id`, `owner_user_id`, `update_time`),
|
||||
KEY `idx_personal_export_status` (`status`, `update_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='个人AI助理汇报导出任务';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `aihr_personal_publish_request` (
|
||||
`id` bigint NOT NULL COMMENT '发布申请ID',
|
||||
`tenant_id` varchar(20) NOT NULL COMMENT '租户编号',
|
||||
`item_id` bigint NOT NULL COMMENT '个人资料ID',
|
||||
`applicant_user_id` bigint NOT NULL COMMENT '申请人用户ID',
|
||||
`target_scope` varchar(100) NOT NULL COMMENT '目标范围,阶段二仅POSITION:生活顾问',
|
||||
`reason` varchar(500) NOT NULL COMMENT '申请原因',
|
||||
`status` varchar(20) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/APPROVED/REJECTED',
|
||||
`reviewer_user_id` bigint DEFAULT NULL COMMENT '审核人用户ID',
|
||||
`review_comment` varchar(500) DEFAULT NULL COMMENT '审核意见',
|
||||
`review_time` datetime DEFAULT NULL COMMENT '审核时间',
|
||||
`published_knowledge_id` bigint DEFAULT NULL COMMENT '企业知识库副本ID',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_personal_publish_applicant` (`tenant_id`, `applicant_user_id`, `create_time`),
|
||||
KEY `idx_personal_publish_status` (`tenant_id`, `status`, `update_time`),
|
||||
KEY `idx_personal_publish_item` (`tenant_id`, `item_id`, `status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='个人知识申请沉淀企业知识审核';
|
||||
|
||||
Reference in New Issue
Block a user