fix(aihr): mask sensitive case transcripts

This commit is contained in:
2026-07-09 23:11:02 +08:00
parent 6cec08f4cb
commit 0acbea7058
2 changed files with 70 additions and 4 deletions
@@ -48,6 +48,7 @@ public class AihrCaseService {
try {
String transcript = speechService.transcribe(file.getBytes(), fileName, contentType)
.orElseThrow(() -> new IllegalStateException("ASR 未配置或转写失败"));
transcript = maskSensitiveText(transcript);
String caseId = "case-" + UUID.randomUUID();
CaseState state = new CaseState(caseId, fileName, projectExtOrgId, transcript, tagsFromText(transcript), null);
cases.put(caseId, state);
@@ -127,8 +128,8 @@ public class AihrCaseService {
truncate(title, 120),
primaryTag(state.tags()),
status,
truncate(state.transcript(), 4000),
truncate(summary, 1000)
truncate(maskSensitiveText(state.transcript()), 4000),
truncate(maskSensitiveText(summary), 1000)
);
}
@@ -183,7 +184,7 @@ public class AihrCaseService {
if (summaryNode.isArray()) {
for (JsonNode item : summaryNode) {
String label = clean(item.path("label").asText(""));
String text = clean(item.path("text").asText(""));
String text = maskSensitiveText(item.path("text").asText(""));
if (!label.isBlank() && !text.isBlank()) {
summary.add(new SummaryResponse(label, truncate(text, 120)));
}
@@ -205,7 +206,7 @@ public class AihrCaseService {
if (tags.isEmpty()) {
tags = tagsFromText(root.path("aiSummary").asText(""));
}
return Optional.of(new CaseSummary(summary, tags, truncate(root.path("aiSummary").asText(""), 180), "real-llm"));
return Optional.of(new CaseSummary(summary, tags, truncate(maskSensitiveText(root.path("aiSummary").asText("")), 180), "real-llm"));
} catch (Exception e) {
log.warn("case summary parse failed, uses local transcript summary: {}", e.getMessage());
return Optional.empty();
@@ -294,6 +295,18 @@ public class AihrCaseService {
return value == null ? "" : value.trim();
}
private static String maskSensitiveText(String text) {
if (text == null || text.isBlank()) {
return clean(text);
}
String masked = text;
masked = masked.replaceAll("(?<!\\d)(1[3-9]\\d{2})\\d{3}(\\d{4})(?!\\d)", "$1****$2");
masked = masked.replaceAll("(?<!\\d)(\\d{1,2})(栋|幢|号楼|单元)(\\d{3,4})(?!\\d)", "$1$2****");
masked = masked.replaceAll("(?<!\\d)(\\d{1,2})[--](\\d{3,4})(?!\\d)", "$1-****");
masked = masked.replaceAll("([\\u4e00-\\u9fa5])([\\u4e00-\\u9fa5])(先生|女士|经理)", "$1*$3");
return clean(masked);
}
private record CaseState(
String id,
String fileName,
@@ -0,0 +1,53 @@
package org.dromara.aihr.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.dromara.aihr.domain.AihrCaseDto.UploadResponse;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.web.MockMultipartFile;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
class AihrCaseServiceTest {
@Test
@Tag("dev")
void uploadMasksSensitiveTranscriptBeforeReturningAndSaving() {
AihrSpeechService speechService = new AihrSpeechService(null, new ObjectMapper()) {
@Override
public Optional<String> transcribe(byte[] audio, String filename, String contentType) {
return Optional.of("张三先生住在12栋1201,手机号13900001111,说2-304漏水");
}
};
CaseJdbcTemplate jdbcTemplate = new CaseJdbcTemplate();
AihrCaseService service = new AihrCaseService(speechService, null, new ObjectMapper(), jdbcTemplate);
UploadResponse response = service.upload(new MockMultipartFile("file", "case.webm", "audio/webm", new byte[]{1}), "P1");
assertEquals("张*先生住在12栋****,手机号1390****1111,说2-****漏水", response.transcript());
assertEquals(response.transcript(), jdbcTemplate.updateArgs[6]);
assertEquals(response.transcript(), jdbcTemplate.updateArgs[7]);
assertFalse(response.transcript().contains("13900001111"));
assertFalse(response.transcript().contains("1201"));
assertFalse(response.transcript().contains("2-304"));
}
private static final class CaseJdbcTemplate extends JdbcTemplate {
private Object[] updateArgs = new Object[0];
@Override
public void execute(String sql) {
// Table DDL is not relevant for this unit test.
}
@Override
public int update(String sql, Object... args) {
this.updateArgs = args;
return 1;
}
}
}