feat(case): persist source audio for learning detail
This commit is contained in:
+4
-2
@@ -10,7 +10,8 @@ public final class AihrCaseDto {
|
|||||||
public record UploadRequest(String fileName, String projectExtOrgId) {
|
public record UploadRequest(String fileName, String projectExtOrgId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public record UploadResponse(String caseId, String fileName, String transcript, List<TagResponse> tags, String source) {
|
public record UploadResponse(String caseId, String fileName, String transcript, List<TagResponse> tags, String source,
|
||||||
|
String mediaUrl, Long mediaOssId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public record OrganizeRequest(String caseId) {
|
public record OrganizeRequest(String caseId) {
|
||||||
@@ -38,6 +39,7 @@ public final class AihrCaseDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public record DetailResponse(String caseId, String fileName, String title, String type, String status,
|
public record DetailResponse(String caseId, String fileName, String title, String type, String status,
|
||||||
String time, String owner, String transcript, String summary) {
|
String time, String owner, String transcript, String summary,
|
||||||
|
String mediaUrl, Long mediaOssId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-10
@@ -13,6 +13,8 @@ import org.dromara.aihr.domain.AihrCaseDto.RecordResponse;
|
|||||||
import org.dromara.aihr.domain.AihrCaseDto.SummaryResponse;
|
import org.dromara.aihr.domain.AihrCaseDto.SummaryResponse;
|
||||||
import org.dromara.aihr.domain.AihrCaseDto.TagResponse;
|
import org.dromara.aihr.domain.AihrCaseDto.TagResponse;
|
||||||
import org.dromara.aihr.domain.AihrCaseDto.UploadResponse;
|
import org.dromara.aihr.domain.AihrCaseDto.UploadResponse;
|
||||||
|
import org.dromara.system.domain.vo.SysOssVo;
|
||||||
|
import org.dromara.system.service.ISysOssService;
|
||||||
import org.springframework.dao.DataAccessException;
|
import org.springframework.dao.DataAccessException;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -40,6 +42,7 @@ public class AihrCaseService {
|
|||||||
private final AihrModelSeedService modelService;
|
private final AihrModelSeedService modelService;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final JdbcTemplate jdbcTemplate;
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private final ISysOssService ossService;
|
||||||
private final Map<String, CaseState> cases = new ConcurrentHashMap<>();
|
private final Map<String, CaseState> cases = new ConcurrentHashMap<>();
|
||||||
private volatile boolean tableReady;
|
private volatile boolean tableReady;
|
||||||
|
|
||||||
@@ -54,11 +57,13 @@ public class AihrCaseService {
|
|||||||
String transcript = speechService.transcribe(file.getBytes(), fileName, contentType)
|
String transcript = speechService.transcribe(file.getBytes(), fileName, contentType)
|
||||||
.orElseThrow(() -> new IllegalStateException("ASR 未配置或转写失败"));
|
.orElseThrow(() -> new IllegalStateException("ASR 未配置或转写失败"));
|
||||||
transcript = maskSensitiveText(transcript);
|
transcript = maskSensitiveText(transcript);
|
||||||
|
MediaRef media = storeSourceAudio(file);
|
||||||
String caseId = "case-" + UUID.randomUUID();
|
String caseId = "case-" + UUID.randomUUID();
|
||||||
CaseState state = new CaseState(caseId, fileName, scopedProject, transcript, tagsFromText(transcript), null);
|
CaseState state = new CaseState(caseId, fileName, scopedProject, transcript, tagsFromText(transcript), null,
|
||||||
|
media.ossId(), media.url());
|
||||||
cases.put(caseId, state);
|
cases.put(caseId, state);
|
||||||
saveCase(state, "已转写", caseTitle(List.of(), fileName), transcript);
|
saveCase(state, "已转写", caseTitle(List.of(), fileName), transcript);
|
||||||
return new UploadResponse(caseId, fileName, transcript, state.tags(), "real-asr");
|
return new UploadResponse(caseId, fileName, transcript, state.tags(), "real-asr", media.url(), media.ossId());
|
||||||
} catch (IllegalStateException e) {
|
} catch (IllegalStateException e) {
|
||||||
throw e;
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -70,7 +75,8 @@ public class AihrCaseService {
|
|||||||
public OrganizeResponse organize(OrganizeRequest request, List<String> projectScopes) {
|
public OrganizeResponse organize(OrganizeRequest request, List<String> projectScopes) {
|
||||||
CaseState state = requireCase(request == null ? null : request.caseId(), projectScopes);
|
CaseState state = requireCase(request == null ? null : request.caseId(), projectScopes);
|
||||||
CaseSummary summary = organizeWithModel(state).orElseGet(() -> localSummary(state));
|
CaseSummary summary = organizeWithModel(state).orElseGet(() -> localSummary(state));
|
||||||
CaseState next = new CaseState(state.id(), state.fileName(), state.projectExtOrgId(), state.transcript(), summary.tags(), summary.summary());
|
CaseState next = new CaseState(state.id(), state.fileName(), state.projectExtOrgId(), state.transcript(), summary.tags(), summary.summary(),
|
||||||
|
state.mediaOssId(), state.mediaUrl());
|
||||||
cases.put(state.id(), next);
|
cases.put(state.id(), next);
|
||||||
saveCase(next, "已整理", caseTitle(summary.summary(), state.fileName()), summary.aiSummary());
|
saveCase(next, "已整理", caseTitle(summary.summary(), state.fileName()), summary.aiSummary());
|
||||||
return new OrganizeResponse(state.id(), summary.summary(), summary.tags(), summary.aiSummary(), summary.source());
|
return new OrganizeResponse(state.id(), summary.summary(), summary.tags(), summary.aiSummary(), summary.source());
|
||||||
@@ -142,7 +148,7 @@ public class AihrCaseService {
|
|||||||
args.addAll(projectScopes);
|
args.addAll(projectScopes);
|
||||||
}
|
}
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT case_id, file_name, title, type, status, update_time, owner, transcript, summary
|
SELECT case_id, file_name, title, type, status, update_time, owner, transcript, summary, media_url, media_oss_id
|
||||||
FROM aihr_case_record
|
FROM aihr_case_record
|
||||||
WHERE tenant_id = '000000' AND case_id = ?
|
WHERE tenant_id = '000000' AND case_id = ?
|
||||||
""" + projectWhere + " LIMIT 1";
|
""" + projectWhere + " LIMIT 1";
|
||||||
@@ -155,7 +161,9 @@ public class AihrCaseService {
|
|||||||
rs.getTimestamp("update_time") == null ? "" : rs.getTimestamp("update_time").toLocalDateTime().format(TIME_FORMAT),
|
rs.getTimestamp("update_time") == null ? "" : rs.getTimestamp("update_time").toLocalDateTime().format(TIME_FORMAT),
|
||||||
firstNonBlank(rs.getString("owner"), "培训组"),
|
firstNonBlank(rs.getString("owner"), "培训组"),
|
||||||
firstNonBlank(rs.getString("transcript"), ""),
|
firstNonBlank(rs.getString("transcript"), ""),
|
||||||
firstNonBlank(rs.getString("summary"), "")
|
firstNonBlank(rs.getString("summary"), ""),
|
||||||
|
firstNonBlank(rs.getString("media_url"), ""),
|
||||||
|
rs.getObject("media_oss_id", Long.class)
|
||||||
), args.toArray()).stream().findFirst().orElse(null);
|
), args.toArray()).stream().findFirst().orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,8 +218,8 @@ public class AihrCaseService {
|
|||||||
ensureCaseTable();
|
ensureCaseTable();
|
||||||
jdbcTemplate.update("""
|
jdbcTemplate.update("""
|
||||||
INSERT INTO aihr_case_record
|
INSERT INTO aihr_case_record
|
||||||
(tenant_id, case_id, file_name, project_ext_org_id, title, type, status, transcript, summary, owner, create_time, update_time)
|
(tenant_id, case_id, file_name, project_ext_org_id, title, type, status, transcript, summary, media_oss_id, media_url, owner, create_time, update_time)
|
||||||
VALUES ('000000', ?, ?, ?, ?, ?, ?, ?, ?, '培训组', now(), now())
|
VALUES ('000000', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '培训组', now(), now())
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
file_name = VALUES(file_name),
|
file_name = VALUES(file_name),
|
||||||
project_ext_org_id = VALUES(project_ext_org_id),
|
project_ext_org_id = VALUES(project_ext_org_id),
|
||||||
@@ -220,6 +228,8 @@ public class AihrCaseService {
|
|||||||
status = VALUES(status),
|
status = VALUES(status),
|
||||||
transcript = VALUES(transcript),
|
transcript = VALUES(transcript),
|
||||||
summary = VALUES(summary),
|
summary = VALUES(summary),
|
||||||
|
media_oss_id = VALUES(media_oss_id),
|
||||||
|
media_url = VALUES(media_url),
|
||||||
update_time = now()
|
update_time = now()
|
||||||
""",
|
""",
|
||||||
state.id(),
|
state.id(),
|
||||||
@@ -229,7 +239,9 @@ public class AihrCaseService {
|
|||||||
primaryTag(state.tags()),
|
primaryTag(state.tags()),
|
||||||
status,
|
status,
|
||||||
truncate(maskSensitiveText(state.transcript()), 4000),
|
truncate(maskSensitiveText(state.transcript()), 4000),
|
||||||
truncate(maskSensitiveText(summary), 1000)
|
truncate(maskSensitiveText(summary), 1000),
|
||||||
|
state.mediaOssId(),
|
||||||
|
truncate(state.mediaUrl(), 500)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,6 +265,8 @@ public class AihrCaseService {
|
|||||||
`status` varchar(30) DEFAULT '已转写' COMMENT '状态',
|
`status` varchar(30) DEFAULT '已转写' COMMENT '状态',
|
||||||
`transcript` text DEFAULT NULL COMMENT 'ASR转写',
|
`transcript` text DEFAULT NULL COMMENT 'ASR转写',
|
||||||
`summary` varchar(1000) DEFAULT NULL COMMENT '摘要',
|
`summary` varchar(1000) DEFAULT NULL COMMENT '摘要',
|
||||||
|
`media_oss_id` bigint DEFAULT NULL COMMENT '原始音频OSS文件ID',
|
||||||
|
`media_url` varchar(500) DEFAULT NULL COMMENT '原始音频访问地址',
|
||||||
`owner` varchar(80) DEFAULT '培训组' COMMENT '负责人',
|
`owner` varchar(80) DEFAULT '培训组' COMMENT '负责人',
|
||||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||||
@@ -261,6 +275,8 @@ public class AihrCaseService {
|
|||||||
KEY `idx_aihr_case_record_status` (`tenant_id`, `status`, `update_time`)
|
KEY `idx_aihr_case_record_status` (`tenant_id`, `status`, `update_time`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 案例沉淀记录';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 案例沉淀记录';
|
||||||
""");
|
""");
|
||||||
|
addCaseColumn("media_oss_id", "ALTER TABLE aihr_case_record ADD COLUMN media_oss_id bigint DEFAULT NULL COMMENT '原始音频OSS文件ID'");
|
||||||
|
addCaseColumn("media_url", "ALTER TABLE aihr_case_record ADD COLUMN media_url varchar(500) DEFAULT NULL COMMENT '原始音频访问地址'");
|
||||||
tableReady = true;
|
tableReady = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -368,6 +384,7 @@ public class AihrCaseService {
|
|||||||
ensureCaseTable();
|
ensureCaseTable();
|
||||||
List<CaseState> persisted = jdbcTemplate.query("""
|
List<CaseState> persisted = jdbcTemplate.query("""
|
||||||
SELECT case_id, file_name, project_ext_org_id, transcript, type
|
SELECT case_id, file_name, project_ext_org_id, transcript, type
|
||||||
|
, media_oss_id, media_url
|
||||||
FROM aihr_case_record
|
FROM aihr_case_record
|
||||||
WHERE tenant_id = '000000' AND case_id = ?
|
WHERE tenant_id = '000000' AND case_id = ?
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
@@ -377,7 +394,9 @@ public class AihrCaseService {
|
|||||||
firstNonBlank(rs.getString("project_ext_org_id"), ""),
|
firstNonBlank(rs.getString("project_ext_org_id"), ""),
|
||||||
firstNonBlank(rs.getString("transcript"), ""),
|
firstNonBlank(rs.getString("transcript"), ""),
|
||||||
tagsFromText(firstNonBlank(rs.getString("type"), "现场案例")),
|
tagsFromText(firstNonBlank(rs.getString("type"), "现场案例")),
|
||||||
null
|
null,
|
||||||
|
rs.getObject("media_oss_id", Long.class),
|
||||||
|
firstNonBlank(rs.getString("media_url"), "")
|
||||||
), normalizedCaseId);
|
), normalizedCaseId);
|
||||||
state = persisted.stream().findFirst().orElse(null);
|
state = persisted.stream().findFirst().orElse(null);
|
||||||
if (state != null) {
|
if (state != null) {
|
||||||
@@ -397,6 +416,23 @@ public class AihrCaseService {
|
|||||||
return String.join(",", Collections.nCopies(size, "?"));
|
return String.join(",", Collections.nCopies(size, "?"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private MediaRef storeSourceAudio(MultipartFile file) {
|
||||||
|
// Unit tests can construct this service without the system OSS bean; production always wires it.
|
||||||
|
if (ossService == null) {
|
||||||
|
return new MediaRef(null, "");
|
||||||
|
}
|
||||||
|
SysOssVo oss = ossService.upload(file);
|
||||||
|
return new MediaRef(oss.getOssId(), firstNonBlank(oss.getUrl(), ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addCaseColumn(String column, String sql) {
|
||||||
|
try {
|
||||||
|
jdbcTemplate.execute(sql);
|
||||||
|
} catch (DataAccessException e) {
|
||||||
|
log.debug("case table column already exists or cannot be altered: {}", column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static List<TagResponse> tagsFromText(String text) {
|
private static List<TagResponse> tagsFromText(String text) {
|
||||||
String source = clean(text);
|
String source = clean(text);
|
||||||
List<TagResponse> tags = new ArrayList<>();
|
List<TagResponse> tags = new ArrayList<>();
|
||||||
@@ -477,10 +513,15 @@ public class AihrCaseService {
|
|||||||
String projectExtOrgId,
|
String projectExtOrgId,
|
||||||
String transcript,
|
String transcript,
|
||||||
List<TagResponse> tags,
|
List<TagResponse> tags,
|
||||||
List<SummaryResponse> summary
|
List<SummaryResponse> summary,
|
||||||
|
Long mediaOssId,
|
||||||
|
String mediaUrl
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record MediaRef(Long ossId, String url) {
|
||||||
|
}
|
||||||
|
|
||||||
private record CaseSummary(List<SummaryResponse> summary, List<TagResponse> tags, String aiSummary, String source) {
|
private record CaseSummary(List<SummaryResponse> summary, List<TagResponse> tags, String aiSummary, String source) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-4
@@ -2,6 +2,8 @@ package org.dromara.aihr.service;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.dromara.aihr.domain.AihrCaseDto.UploadResponse;
|
import org.dromara.aihr.domain.AihrCaseDto.UploadResponse;
|
||||||
|
import org.dromara.system.domain.vo.SysOssVo;
|
||||||
|
import org.dromara.system.service.ISysOssService;
|
||||||
import org.junit.jupiter.api.Tag;
|
import org.junit.jupiter.api.Tag;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
@@ -32,7 +34,7 @@ class AihrCaseServiceTest {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
CaseJdbcTemplate jdbcTemplate = new CaseJdbcTemplate();
|
CaseJdbcTemplate jdbcTemplate = new CaseJdbcTemplate();
|
||||||
AihrCaseService service = new AihrCaseService(speechService, null, new ObjectMapper(), jdbcTemplate);
|
AihrCaseService service = new AihrCaseService(speechService, null, new ObjectMapper(), jdbcTemplate, null);
|
||||||
|
|
||||||
UploadResponse response = service.upload(new MockMultipartFile("file", "case.webm", "audio/webm", new byte[]{1}), "P1", null);
|
UploadResponse response = service.upload(new MockMultipartFile("file", "case.webm", "audio/webm", new byte[]{1}), "P1", null);
|
||||||
|
|
||||||
@@ -44,11 +46,36 @@ class AihrCaseServiceTest {
|
|||||||
assertFalse(response.transcript().contains("2-304"));
|
assertFalse(response.transcript().contains("2-304"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Tag("dev")
|
||||||
|
void uploadPersistsSourceAudioReferenceWhenOssIsAvailable() {
|
||||||
|
AihrSpeechService speechService = new AihrSpeechService(null, new ObjectMapper()) {
|
||||||
|
@Override
|
||||||
|
public Optional<String> transcribe(byte[] audio, String filename, String contentType) {
|
||||||
|
return Optional.of("现场投诉已完成跟进");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
SysOssVo oss = new SysOssVo();
|
||||||
|
oss.setOssId(123L);
|
||||||
|
oss.setUrl("https://oss.local/cases/case.webm");
|
||||||
|
ISysOssService ossService = mock(ISysOssService.class);
|
||||||
|
when(ossService.upload(org.mockito.ArgumentMatchers.any(org.springframework.web.multipart.MultipartFile.class))).thenReturn(oss);
|
||||||
|
CaseJdbcTemplate jdbcTemplate = new CaseJdbcTemplate();
|
||||||
|
AihrCaseService service = new AihrCaseService(speechService, null, new ObjectMapper(), jdbcTemplate, ossService);
|
||||||
|
|
||||||
|
UploadResponse response = service.upload(new MockMultipartFile("file", "case.webm", "audio/webm", new byte[]{1}), "P1", null);
|
||||||
|
|
||||||
|
assertEquals(123L, response.mediaOssId());
|
||||||
|
assertEquals("https://oss.local/cases/case.webm", response.mediaUrl());
|
||||||
|
assertEquals(123L, jdbcTemplate.updateArgs[8]);
|
||||||
|
assertEquals("https://oss.local/cases/case.webm", jdbcTemplate.updateArgs[9]);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Tag("dev")
|
@Tag("dev")
|
||||||
void curateCanReloadCaseAfterServiceRestart() {
|
void curateCanReloadCaseAfterServiceRestart() {
|
||||||
PersistentCaseJdbcTemplate jdbcTemplate = new PersistentCaseJdbcTemplate();
|
PersistentCaseJdbcTemplate jdbcTemplate = new PersistentCaseJdbcTemplate();
|
||||||
AihrCaseService service = new AihrCaseService(null, null, new ObjectMapper(), jdbcTemplate);
|
AihrCaseService service = new AihrCaseService(null, null, new ObjectMapper(), jdbcTemplate, null);
|
||||||
|
|
||||||
var response = service.curate(new org.dromara.aihr.domain.AihrCaseDto.CurateRequest(
|
var response = service.curate(new org.dromara.aihr.domain.AihrCaseDto.CurateRequest(
|
||||||
"persisted-case", "可复用培训案例", 1
|
"persisted-case", "可复用培训案例", 1
|
||||||
@@ -65,7 +92,7 @@ class AihrCaseServiceTest {
|
|||||||
@Tag("dev")
|
@Tag("dev")
|
||||||
void detailReturnsPersistedCaseForAuthorizedProject() {
|
void detailReturnsPersistedCaseForAuthorizedProject() {
|
||||||
PersistentCaseJdbcTemplate jdbcTemplate = new PersistentCaseJdbcTemplate();
|
PersistentCaseJdbcTemplate jdbcTemplate = new PersistentCaseJdbcTemplate();
|
||||||
AihrCaseService service = new AihrCaseService(null, null, new ObjectMapper(), jdbcTemplate);
|
AihrCaseService service = new AihrCaseService(null, null, new ObjectMapper(), jdbcTemplate, null);
|
||||||
|
|
||||||
var detail = service.detail("persisted-case", List.of("P1"));
|
var detail = service.detail("persisted-case", List.of("P1"));
|
||||||
|
|
||||||
@@ -86,7 +113,7 @@ class AihrCaseServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
@Tag("dev")
|
@Tag("dev")
|
||||||
void systemUploadRequiresExplicitProject() {
|
void systemUploadRequiresExplicitProject() {
|
||||||
AihrCaseService service = new AihrCaseService(null, null, new ObjectMapper(), new CaseJdbcTemplate());
|
AihrCaseService service = new AihrCaseService(null, null, new ObjectMapper(), new CaseJdbcTemplate(), null);
|
||||||
|
|
||||||
assertThrows(IllegalArgumentException.class, () -> service.upload(
|
assertThrows(IllegalArgumentException.class, () -> service.upload(
|
||||||
new MockMultipartFile("file", "case.webm", "audio/webm", new byte[]{1}), "", null
|
new MockMultipartFile("file", "case.webm", "audio/webm", new byte[]{1}), "", null
|
||||||
|
|||||||
@@ -1,6 +1,27 @@
|
|||||||
-- 物业 AI HR 对练记录最小表结构。
|
-- 物业 AI HR 对练记录最小表结构。
|
||||||
-- ponytail: one session table covers history, review list and profile aggregation for MVP.
|
-- ponytail: one session table covers history, review list and profile aggregation for MVP.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `aihr_case_record` (
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
|
||||||
|
`case_id` varchar(80) NOT NULL COMMENT '案例ID',
|
||||||
|
`file_name` varchar(255) DEFAULT NULL COMMENT '来源文件',
|
||||||
|
`project_ext_org_id` varchar(100) DEFAULT NULL COMMENT '项目外部组织ID',
|
||||||
|
`title` varchar(120) DEFAULT NULL COMMENT '标题',
|
||||||
|
`type` varchar(80) DEFAULT NULL COMMENT '案例类型',
|
||||||
|
`status` varchar(30) DEFAULT '已转写' COMMENT '状态',
|
||||||
|
`transcript` text DEFAULT NULL COMMENT 'ASR转写',
|
||||||
|
`summary` varchar(1000) DEFAULT NULL COMMENT '摘要',
|
||||||
|
`media_oss_id` bigint DEFAULT NULL COMMENT '原始音频OSS文件ID',
|
||||||
|
`media_url` varchar(500) DEFAULT NULL COMMENT '原始音频访问地址',
|
||||||
|
`owner` varchar(80) DEFAULT '培训组' COMMENT '负责人',
|
||||||
|
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_aihr_case_record_case` (`tenant_id`, `case_id`),
|
||||||
|
KEY `idx_aihr_case_record_status` (`tenant_id`, `status`, `update_time`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 案例沉淀记录';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `aihr_practice_session` (
|
CREATE TABLE IF NOT EXISTS `aihr_practice_session` (
|
||||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
|
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
| 三角色对练 `/train/practice` | `POST /api/train/practice/start`、`/turn`、`/finish` | 已接入编排 API;数据库启用 chat 模型后,`/turn` 客户回复按人设走真 LLM 生成(seed 剧本作剧情锚点),`/finish` 走单次 temperature=0 结构化评分(4 维分+导师改写+点评);模型未配置或调用失败自动回退 seed,契约不变 |
|
| 三角色对练 `/train/practice` | `POST /api/train/practice/start`、`/turn`、`/finish` | 已接入编排 API;数据库启用 chat 模型后,`/turn` 客户回复按人设走真 LLM 生成(seed 剧本作剧情锚点),`/finish` 走单次 temperature=0 结构化评分(4 维分+导师改写+点评);模型未配置或调用失败自动回退 seed,契约不变 |
|
||||||
| 正式试点数据导出 | `GET /api/train/practice/export?startDate=YYYY-MM-DD&endDate=YYYY-MM-DD` | 起止日期必填且包含结束日;只统计窗口内能通过唯一手机号或外部 ID 映射到在职组织快照的正式会话,排除重复手机号和身份碰撞。完训定义为每人至少 10 次,校准必须关联同一窗口内正式会话;CSV 同时给出校准命中数、SOP 可用数、满意度响应数/平均分,以及明细级 AI 分、人工校准分、校准人、校准时间、最终采用分、满意度分和意见,避免用四舍五入后的比率反推门禁状态;汇总和明细均携带正式人员及项目口径,不混入历史 seed/开发身份 |
|
| 正式试点数据导出 | `GET /api/train/practice/export?startDate=YYYY-MM-DD&endDate=YYYY-MM-DD` | 起止日期必填且包含结束日;只统计窗口内能通过唯一手机号或外部 ID 映射到在职组织快照的正式会话,排除重复手机号和身份碰撞。完训定义为每人至少 10 次,校准必须关联同一窗口内正式会话;CSV 同时给出校准命中数、SOP 可用数、满意度响应数/平均分,以及明细级 AI 分、人工校准分、校准人、校准时间、最终采用分、满意度分和意见,避免用四舍五入后的比率反推门禁状态;汇总和明细均携带正式人员及项目口径,不混入历史 seed/开发身份 |
|
||||||
| 对练语音 | `POST /api/ai/asr`(multipart 字段 `file`,≤5MB)、`POST /api/ai/tts`(JSON `{text≤300字, voice}`,返回 `{audioUrl}` base64 dataURL)、`GET /api/aihr/mobile/oss/{ossId}` | 走 OpenAI-compatible audio 接口(如硅基流动 SenseVoice/CosyVoice2);模型管理需启用 `category=asr/tts` 配置;移动端优先用浏览器录音,`getUserMedia/MediaRecorder` 不可用或麦克风权限失败时,用 `audio/*` file input 选择/录制音频后继续调同一 ASR 接口;ASR/TTS 未配置或失败返回 fail,前端降级文本;音频下载按员工本人或主管项目范围授权,系统管理端保持后台访问 |
|
| 对练语音 | `POST /api/ai/asr`(multipart 字段 `file`,≤5MB)、`POST /api/ai/tts`(JSON `{text≤300字, voice}`,返回 `{audioUrl}` base64 dataURL)、`GET /api/aihr/mobile/oss/{ossId}` | 走 OpenAI-compatible audio 接口(如硅基流动 SenseVoice/CosyVoice2);模型管理需启用 `category=asr/tts` 配置;移动端优先用浏览器录音,`getUserMedia/MediaRecorder` 不可用或麦克风权限失败时,用 `audio/*` file input 选择/录制音频后继续调同一 ASR 接口;ASR/TTS 未配置或失败返回 fail,前端降级文本;音频下载按员工本人或主管项目范围授权,系统管理端保持后台访问 |
|
||||||
| 案例沉淀 `/knowledge/cases` | `POST /api/knowledge/case/upload`、`/organize`、`/curate`、`GET /records`、`GET /records/{caseId}` | `/upload` 改为 multipart 真实语音上传并走 ASR;`/organize` 用真实转写调 chat 模型整理案例,未配置模型时按真实 transcript 本地结构化;APP 用户的项目范围从 `aihr_org_snapshot` 登录身份解析,上传、整理、入库、列表和详情均按项目范围校验,未完成正式组织映射时安全拒绝,不接受前端伪造项目范围;移动端可查看持久化转写/摘要详情;管理端系统用户保留全局运营视图;视频样片、学习点和主管点评仍未接入 |
|
| 案例沉淀 `/knowledge/cases` | `POST /api/knowledge/case/upload`、`/organize`、`/curate`、`GET /records`、`GET /records/{caseId}` | `/upload` 改为 multipart 真实语音上传并走 ASR,成功后原始音频写入 `sys_oss`,案例记录保存 `mediaOssId/mediaUrl`;`/organize` 用真实转写调 chat 模型整理案例,未配置模型时按真实 transcript 本地结构化;APP 用户的项目范围从 `aihr_org_snapshot` 登录身份解析,上传、整理、入库、列表和详情均按项目范围校验,未完成正式组织映射时安全拒绝,不接受前端伪造项目范围;移动端可查看持久化转写/摘要并回放原始音频;管理端系统用户保留全局运营视图;预渲染视频样片、学习点和主管点评仍未接入 |
|
||||||
| SOP知识库 `/knowledge/sop` | `POST /api/knowledge/search`、`POST /api/knowledge/answer-feedback`、`GET /api/knowledge/position-sop`、`POST /api/knowledge/doc/upload` | 已接入 MySQL Fulltext + Qdrant 混合召回、岗位学习适配摘要、OSS-first 文档上传、txt/md/PDF/Word/Excel/PPT 解析和 embedding 写入,失败回退 seed;搜索返回 `reviewId` 与 `promptVersion`,员工反馈回传并保存该评审批次,SOP 人工评审记录同时保留答案生成提示词版本,管理端可继续复核;`position-sop` 目前只返回一期生活顾问学习导航,不代表正式上岗资格 |
|
| SOP知识库 `/knowledge/sop` | `POST /api/knowledge/search`、`POST /api/knowledge/answer-feedback`、`GET /api/knowledge/position-sop`、`POST /api/knowledge/doc/upload` | 已接入 MySQL Fulltext + Qdrant 混合召回、岗位学习适配摘要、OSS-first 文档上传、txt/md/PDF/Word/Excel/PPT 解析和 embedding 写入,失败回退 seed;搜索返回 `reviewId` 与 `promptVersion`,员工反馈回传并保存该评审批次,SOP 人工评审记录同时保留答案生成提示词版本,管理端可继续复核;`position-sop` 目前只返回一期生活顾问学习导航,不代表正式上岗资格 |
|
||||||
| 资料处理 `/knowledge/processing` | `GET /api/knowledge/processing/overview`、`POST /api/knowledge/doc/upload-async`、`GET /api/knowledge/doc/upload-items`、`POST /api/knowledge/doc/upload-items/{id}/retry`、`POST /api/knowledge/doc/import-local-task`、`GET /api/knowledge/doc/import-tasks`、`POST /api/knowledge/doc/import-tasks/{id}/cancel` | 已接入解析任务状态聚合;**批量上传走异步队列**:接口只暂存+入队即秒回,后台 worker(并发 2)逐条解析/归类/向量化;ZIP 在 worker 内安全解压后把支持的子文件继续入同一批次队列,页面按批次轮询进度、失败可单文件重试;服务端目录导入、进度轮询和任务取消保留,失败回退 seed |
|
| 资料处理 `/knowledge/processing` | `GET /api/knowledge/processing/overview`、`POST /api/knowledge/doc/upload-async`、`GET /api/knowledge/doc/upload-items`、`POST /api/knowledge/doc/upload-items/{id}/retry`、`POST /api/knowledge/doc/import-local-task`、`GET /api/knowledge/doc/import-tasks`、`POST /api/knowledge/doc/import-tasks/{id}/cancel` | 已接入解析任务状态聚合;**批量上传走异步队列**:接口只暂存+入队即秒回,后台 worker(并发 2)逐条解析/归类/向量化;ZIP 在 worker 内安全解压后把支持的子文件继续入同一批次队列,页面按批次轮询进度、失败可单文件重试;服务端目录导入、进度轮询和任务取消保留,失败回退 seed |
|
||||||
| 组织人员同步 | `POST /api/aihr/org/sync` | 从开放组织同步系统的 `/api/open/v1/sync/snapshot` 拉取 `company/department/employee` 快照,分页参数使用 `limit`;员工手机号只落 `person_phone` 用于移动端身份映射,不在组织列表响应暴露;岗位识别 `position/job_title/post/job_name/role/title` 等字段。默认 `replaceExisting=true`,写入前必须先用 `{"dryRun":true}`;dry-run 不访问本地快照表、不执行 DDL/写库,返回 `phoneLinked/maskedPhone/suspectText/warnings` 且 `syncedCount=0`。非 dry-run 覆盖写入遇到员工被跳过、手机号不完整、脱敏手机号或疑似乱码时默认拒绝,只有确认 dry-run 结果后显式传 `allowPartialReplace=true` 才允许覆盖;重复 `ext_party_id` 始终拒绝写入,因为数据库唯一键会折叠重复身份;`replaceExisting=false` 不触发不完整快照覆盖闸门,但仍拒绝重复身份。2026-07-10 源接口实测 3474 人仅 1 个可用手机号、3473 个脱敏手机号、8 条疑似乱码,因此未执行覆盖同步,正式试点需上游先开放至少 20 名试点人员手机号 |
|
| 组织人员同步 | `POST /api/aihr/org/sync` | 从开放组织同步系统的 `/api/open/v1/sync/snapshot` 拉取 `company/department/employee` 快照,分页参数使用 `limit`;员工手机号只落 `person_phone` 用于移动端身份映射,不在组织列表响应暴露;岗位识别 `position/job_title/post/job_name/role/title` 等字段。默认 `replaceExisting=true`,写入前必须先用 `{"dryRun":true}`;dry-run 不访问本地快照表、不执行 DDL/写库,返回 `phoneLinked/maskedPhone/suspectText/warnings` 且 `syncedCount=0`。非 dry-run 覆盖写入遇到员工被跳过、手机号不完整、脱敏手机号或疑似乱码时默认拒绝,只有确认 dry-run 结果后显式传 `allowPartialReplace=true` 才允许覆盖;重复 `ext_party_id` 始终拒绝写入,因为数据库唯一键会折叠重复身份;`replaceExisting=false` 不触发不完整快照覆盖闸门,但仍拒绝重复身份。2026-07-10 源接口实测 3474 人仅 1 个可用手机号、3473 个脱敏手机号、8 条疑似乱码,因此未执行覆盖同步,正式试点需上游先开放至少 20 名试点人员手机号 |
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
| 候选人面试与 HR 复核 | `mobile-uni/src/pages/candidate/*`、`frontend/src/views/recruit/interview.vue` | 工程闭环已在位;候选人资料移动端上传/列表现在只接受 APP 候选人本人身份,HR 继续通过独立审核接口处理;需继续保留人工复核证据 |
|
| 候选人面试与 HR 复核 | `mobile-uni/src/pages/candidate/*`、`frontend/src/views/recruit/interview.vue` | 工程闭环已在位;候选人资料移动端上传/列表现在只接受 APP 候选人本人身份,HR 继续通过独立审核接口处理;需继续保留人工复核证据 |
|
||||||
| SOP/RAG/资料处理 | `frontend/src/views/knowledge/*`、`backend/.../AihrSopSeedService.java` | 搜索、引用、反馈、异步处理和评审入口已在位 |
|
| SOP/RAG/资料处理 | `frontend/src/views/knowledge/*`、`backend/.../AihrSopSeedService.java` | 搜索、引用、反馈、异步处理和评审入口已在位 |
|
||||||
| 对练/每日题/主管带教 | `backend/.../AihrPracticeSeedService.java`、`mobile-uni/src/pages/user/practice` | 每日三题现支持文字或录音转写,语音 `audioUrl/ossId` 随作业提交并持久化;普通陪练、即时反馈、低分再练、错题归因和主管带教已在位,正式窗口仍需验证真实音频与低分样本 |
|
| 对练/每日题/主管带教 | `backend/.../AihrPracticeSeedService.java`、`mobile-uni/src/pages/user/practice` | 每日三题现支持文字或录音转写,语音 `audioUrl/ossId` 随作业提交并持久化;普通陪练、即时反馈、低分再练、错题归因和主管带教已在位,正式窗口仍需验证真实音频与低分样本 |
|
||||||
| 案例上传/整理/筛选 | `backend/.../AihrCaseService.java`、`mobile-uni/src/pages/user/cases`、`frontend/src/views/knowledge/cases.vue` | 上传与整理已在位,案例状态在内存缺失时可从 `aihr_case_record` 回读恢复;APP 用户的读取按项目范围校验,写入接口已增加“主管/项目经理/系统用户”门禁,普通员工不能提交、整理或入库案例;本轮补上管理端上传的项目选择与后端无项目拒绝,避免案例落成员工不可见的无范围记录;当前仓库仍没有可用预渲染视频样片资产,也没有稳定的案例详情/学习点模型,不能用假播放器宣称案例学习闭环完成 |
|
| 案例上传/整理/筛选 | `backend/.../AihrCaseService.java`、`mobile-uni/src/pages/user/cases`、`frontend/src/views/knowledge/cases.vue` | 上传与整理已在位,案例状态在内存缺失时可从 `aihr_case_record` 回读恢复;APP 用户的读取按项目范围校验,写入接口已增加“主管/项目经理/系统用户”门禁,普通员工不能提交、整理或入库案例;原始音频现在上传到 `sys_oss` 并在 `aihr_case_record.media_oss_id/media_url` 留痕,员工详情可回放;本轮补上管理端上传的项目选择与后端无项目拒绝,避免案例落成员工不可见的无范围记录;当前仓库仍没有可用预渲染视频样片资产,也没有稳定的学习点/主管点评模型,不能用音频回放冒充完整案例学习闭环 |
|
||||||
|
|
||||||
## 3. 未完成项与优先级
|
## 3. 未完成项与优先级
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
| 2 | uni-app 三端 `https://wygj-mobile-uni.localhost/h5/#/pages/user/today/index`、`/pages/candidate/index/index`、`/pages/supervisor/index/index` | 打开后先手机号登录;员工端从今日页进入问师傅、练习、学习、案例素材和个人页;候选首页按真实状态进入独立 `/interview/index`、`/materials/index`、`/progress/index`、`/study/index`;主管首页进入独立团队画像、指派、复盘列表和复盘详情 | 员工端显示训练评分、历史和画像;候选面试页可真实拉题/答题/评分,资料页显示审核三态,进度页展示办理步骤,预习页从正式知识库返回依据;主管端待复盘列表、复盘详情、专项指派和团队范围均为独立页面且普通员工无主管权限 |
|
| 2 | uni-app 三端 `https://wygj-mobile-uni.localhost/h5/#/pages/user/today/index`、`/pages/candidate/index/index`、`/pages/supervisor/index/index` | 打开后先手机号登录;员工端从今日页进入问师傅、练习、学习、案例素材和个人页;候选首页按真实状态进入独立 `/interview/index`、`/materials/index`、`/progress/index`、`/study/index`;主管首页进入独立团队画像、指派、复盘列表和复盘详情 | 员工端显示训练评分、历史和画像;候选面试页可真实拉题/答题/评分,资料页显示审核三态,进度页展示办理步骤,预习页从正式知识库返回依据;主管端待复盘列表、复盘详情、专项指派和团队范围均为独立页面且普通员工无主管权限 |
|
||||||
| 3 | AI面试 `/recruit/interview` | 生成题目 → 填满参考回答或输入真实回答 → 完成评分 | 配置 chat 模型时真实生成题目和评分;未配置时用本地 Rubric 估分,不返回固定候选人分数;应看到新增面试记录 |
|
| 3 | AI面试 `/recruit/interview` | 生成题目 → 填满参考回答或输入真实回答 → 完成评分 | 配置 chat 模型时真实生成题目和评分;未配置时用本地 Rubric 估分,不返回固定候选人分数;应看到新增面试记录 |
|
||||||
| 4 | 三角色对练 `/train/practice` | 开始对练 → 填入参考回复或输入真实回复 → 继续一轮 → 结束并评分 | 已完成闭环、导师改写、新增对练记录;数据库已启用 chat 模型时,客户回复与评分为真实 LLM 生成(回复不再是固定台词,分数随话术变化),未配置时使用本地剧本兜底 |
|
| 4 | 三角色对练 `/train/practice` | 开始对练 → 填入参考回复或输入真实回复 → 继续一轮 → 结束并评分 | 已完成闭环、导师改写、新增对练记录;数据库已启用 chat 模型时,客户回复与评分为真实 LLM 生成(回复不再是固定台词,分数随话术变化),未配置时使用本地剧本兜底 |
|
||||||
| 5 | 案例沉淀 `/knowledge/cases` | 上传真实语音 → ASR 转写 → AI 整理 → 送审 → 入库 → 查看案例详情 | 已完成语音转写、整理、入库和按项目权限查看持久化转写/摘要详情;稳定媒体 URL、视频样片、学习点和主管点评尚未形成,不把“查看样片”作为当前完成证据 |
|
| 5 | 案例沉淀 `/knowledge/cases` | 上传真实语音 → ASR 转写 → AI 整理 → 送审 → 入库 → 查看案例详情/回放原始音频 | 已完成语音转写、原始音频 OSS 留痕、整理、入库和按项目权限查看持久化转写/摘要详情;预渲染视频样片、学习点和主管点评尚未形成,不把音频回放当作“查看样片”完成证据 |
|
||||||
| 6 | SOP知识库 `/knowledge/sop` | 上传 txt/md/PDF/Word/Excel/PPT 文档或使用 seed 文档 → 检索 → 生成训练题 | 已完成闭环、引用 SOP 原文片段、训练题已生成 |
|
| 6 | SOP知识库 `/knowledge/sop` | 上传 txt/md/PDF/Word/Excel/PPT 文档或使用 seed 文档 → 检索 → 生成训练题 | 已完成闭环、引用 SOP 原文片段、训练题已生成 |
|
||||||
| 7 | 资料处理 `/knowledge/processing` | 打开页面 → 查看解析任务 → 查看处理链路 → 可选点“服务端导入”导入 `.data/import` 下少量样例 | 可看到资料总量、等待/解析中/完成/失败、片段数、向量化状态和事件列表 |
|
| 7 | 资料处理 `/knowledge/processing` | 打开页面 → 查看解析任务 → 查看处理链路 → 可选点“服务端导入”导入 `.data/import` 下少量样例 | 可看到资料总量、等待/解析中/完成/失败、片段数、向量化状态和事件列表 |
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,11 @@
|
|||||||
<text class="case-content">{{ caseDetail.transcript || '暂无转写内容' }}</text>
|
<text class="case-content">{{ caseDetail.transcript || '暂无转写内容' }}</text>
|
||||||
<text class="detail-label">案例摘要</text>
|
<text class="detail-label">案例摘要</text>
|
||||||
<text class="case-note">{{ caseDetail.summary || '暂无案例摘要' }}</text>
|
<text class="case-note">{{ caseDetail.summary || '暂无案例摘要' }}</text>
|
||||||
<text class="case-note">当前版本提供转写、摘要和状态详情;视频样片、学习点与主管点评待正式媒体和业务字段接入。</text>
|
<view v-if="caseDetail.mediaUrl" class="case-media-block">
|
||||||
|
<text class="detail-label">原始音频</text>
|
||||||
|
<audio class="case-audio" :src="caseDetail.mediaUrl" controls />
|
||||||
|
</view>
|
||||||
|
<text class="case-note">当前版本已保留原始音频回放;预渲染视频样片、学习点与主管点评待正式媒体和业务字段接入。</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -369,6 +373,16 @@ onShow(() => {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.case-media-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-audio {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.tag-row {
|
.tag-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -386,6 +386,8 @@ export interface CaseDetailResponse {
|
|||||||
owner: string;
|
owner: string;
|
||||||
transcript: string;
|
transcript: string;
|
||||||
summary: string;
|
summary: string;
|
||||||
|
mediaUrl?: string;
|
||||||
|
mediaOssId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CaseUploadResponse {
|
export interface CaseUploadResponse {
|
||||||
@@ -394,6 +396,8 @@ export interface CaseUploadResponse {
|
|||||||
transcript: string;
|
transcript: string;
|
||||||
tags: CaseTag[];
|
tags: CaseTag[];
|
||||||
source: string;
|
source: string;
|
||||||
|
mediaUrl?: string;
|
||||||
|
mediaOssId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CaseOrganizeResponse {
|
export interface CaseOrganizeResponse {
|
||||||
|
|||||||
@@ -452,6 +452,10 @@ contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service
|
|||||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "aihr_knowledge_gap"
|
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "aihr_knowledge_gap"
|
||||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCaseController.java "仅项目负责人或主管可提交案例素材"
|
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCaseController.java "仅项目负责人或主管可提交案例素材"
|
||||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java "position_level IN ('主管', '项目经理')"
|
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java "position_level IN ('主管', '项目经理')"
|
||||||
|
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java "media_oss_id"
|
||||||
|
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java "storeSourceAudio"
|
||||||
|
contains mobile-uni/src/pages/user/cases/index.vue "case-audio"
|
||||||
|
contains backend/script/sql/aihr_practice_mysql8.sql "aihr_case_record"
|
||||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "prompt_version"
|
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "prompt_version"
|
||||||
contains backend/script/sql/aihr_practice_mysql8.sql "prompt_version"
|
contains backend/script/sql/aihr_practice_mysql8.sql "prompt_version"
|
||||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "review_id"
|
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "review_id"
|
||||||
|
|||||||
Reference in New Issue
Block a user