feat(aihr): complete uni-app role workflows
This commit is contained in:
@@ -47,8 +47,8 @@ public class UserActionListener implements SaTokenListener {
|
||||
UserOnlineDTO dto = new UserOnlineDTO();
|
||||
dto.setIpaddr(ip);
|
||||
dto.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
|
||||
dto.setBrowser(userAgent.getBrowser().getName());
|
||||
dto.setOs(userAgent.getOs().getName());
|
||||
dto.setBrowser(userAgent == null ? "Unknown" : userAgent.getBrowser().getName());
|
||||
dto.setOs(userAgent == null ? "Unknown" : userAgent.getOs().getName());
|
||||
dto.setLoginTime(System.currentTimeMillis());
|
||||
dto.setTokenId(tokenValue);
|
||||
String username = (String) loginParameter.getExtra(LoginHelper.USER_NAME_KEY);
|
||||
|
||||
@@ -53,5 +53,11 @@
|
||||
<artifactId>tika-parser-microsoft-module</artifactId>
|
||||
<version>3.2.2</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
+29
-2
@@ -1,11 +1,17 @@
|
||||
package org.dromara.aihr.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckRole;
|
||||
import cn.dev33.satoken.annotation.SaMode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.aihr.service.AihrCandidateMaterialService;
|
||||
import org.dromara.aihr.service.AihrCandidateMaterialService.MaterialResponse;
|
||||
import org.dromara.aihr.service.AihrCandidateMaterialService.MaterialReviewResponse;
|
||||
import org.dromara.aihr.service.AihrCandidateMaterialService.ReviewRequest;
|
||||
import org.dromara.common.core.constant.TenantConstants;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.domain.model.LoginUser;
|
||||
import org.dromara.common.core.enums.UserType;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
@@ -22,6 +28,8 @@ import java.util.List;
|
||||
@RestController
|
||||
public class AihrCandidateMaterialController {
|
||||
|
||||
private static final String HR_OPERATOR_ROLE = "hr_operator";
|
||||
|
||||
private final AihrCandidateMaterialService materialService;
|
||||
|
||||
@PostMapping(value = "/api/aihr/mobile/candidate/materials", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@@ -29,21 +37,40 @@ public class AihrCandidateMaterialController {
|
||||
@RequestParam(value = "candidateId", required = false) String candidateId,
|
||||
@RequestParam(value = "candidateName", required = false) String candidateName,
|
||||
@RequestParam(value = "materialType", required = false) String materialType) {
|
||||
return R.ok(materialService.upload(file, candidateId, candidateName, materialType));
|
||||
String appUsername = currentAppUsername();
|
||||
String boundCandidateId = appUsername.isBlank() ? candidateId : appUsername;
|
||||
String boundCandidateName = appUsername.isBlank() ? candidateName : appCandidateName(appUsername);
|
||||
return R.ok(materialService.upload(file, boundCandidateId, boundCandidateName, materialType));
|
||||
}
|
||||
|
||||
@GetMapping("/api/aihr/mobile/candidate/materials")
|
||||
public R<List<MaterialResponse>> list(@RequestParam(value = "candidateId", required = false) String candidateId) {
|
||||
return R.ok(materialService.list(candidateId));
|
||||
String appUsername = currentAppUsername();
|
||||
return R.ok(materialService.list(appUsername.isBlank() ? candidateId : appUsername));
|
||||
}
|
||||
|
||||
@SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR)
|
||||
@GetMapping("/api/aihr/hr/candidate/materials")
|
||||
public R<List<MaterialReviewResponse>> reviewList(@RequestParam(value = "status", required = false) String status) {
|
||||
return R.ok(materialService.reviewList(status));
|
||||
}
|
||||
|
||||
@SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR)
|
||||
@PostMapping("/api/aihr/hr/candidate/materials/{id}/review")
|
||||
public R<MaterialReviewResponse> review(@PathVariable Long id, @RequestBody ReviewRequest request) {
|
||||
return R.ok(materialService.review(id, request));
|
||||
}
|
||||
|
||||
private static String currentAppUsername() {
|
||||
LoginUser loginUser = LoginHelper.getLoginUser();
|
||||
if (loginUser == null || !UserType.APP_USER.getUserType().equals(loginUser.getUserType())) {
|
||||
return "";
|
||||
}
|
||||
String username = loginUser.getUsername();
|
||||
return username == null ? "" : username.trim();
|
||||
}
|
||||
|
||||
private static String appCandidateName(String username) {
|
||||
return username.matches("1[3-9]\\d{9}") ? "候选人" + username.substring(username.length() - 4) : username;
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -5,10 +5,12 @@ import org.dromara.aihr.domain.AihrCaseDto.CurateRequest;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.CurateResponse;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.OrganizeRequest;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.OrganizeResponse;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.RecordResponse;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.UploadResponse;
|
||||
import org.dromara.aihr.service.AihrCaseService;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -17,6 +19,8 @@ import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 案例语音整理 API。
|
||||
*/
|
||||
@@ -41,4 +45,9 @@ public class AihrCaseController {
|
||||
public R<CurateResponse> curate(@RequestBody CurateRequest request) {
|
||||
return R.ok(caseService.curate(request));
|
||||
}
|
||||
|
||||
@GetMapping("/records")
|
||||
public R<List<RecordResponse>> records(@RequestParam(value = "limit", required = false) Integer limit) {
|
||||
return R.ok(caseService.records(limit));
|
||||
}
|
||||
}
|
||||
|
||||
+31
-3
@@ -10,6 +10,9 @@ import org.dromara.aihr.domain.AihrInterviewDto.StartRequest;
|
||||
import org.dromara.aihr.domain.AihrInterviewDto.StartResponse;
|
||||
import org.dromara.aihr.service.AihrInterviewService;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.domain.model.LoginUser;
|
||||
import org.dromara.common.core.enums.UserType;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
@@ -31,7 +34,7 @@ public class AihrInterviewController {
|
||||
|
||||
@PostMapping("/start")
|
||||
public R<StartResponse> start(@RequestBody StartRequest request) {
|
||||
return R.ok(interviewService.start(request));
|
||||
return R.ok(interviewService.start(bindAppCandidate(request)));
|
||||
}
|
||||
|
||||
@PostMapping("/answer")
|
||||
@@ -45,7 +48,32 @@ public class AihrInterviewController {
|
||||
}
|
||||
|
||||
@GetMapping("/records")
|
||||
public R<List<RecordResponse>> records(@RequestParam(value = "limit", required = false, defaultValue = "20") int limit) {
|
||||
return R.ok(interviewService.records(limit));
|
||||
public R<List<RecordResponse>> records(@RequestParam(value = "limit", required = false, defaultValue = "20") int limit,
|
||||
@RequestParam(value = "candidateId", required = false) String candidateId) {
|
||||
String appUsername = currentAppUsername();
|
||||
return R.ok(interviewService.records(limit, appUsername.isBlank() ? candidateId : appUsername));
|
||||
}
|
||||
|
||||
private static StartRequest bindAppCandidate(StartRequest request) {
|
||||
String appUsername = currentAppUsername();
|
||||
if (appUsername.isBlank()) {
|
||||
return request;
|
||||
}
|
||||
String positionCode = request == null ? null : request.positionCode();
|
||||
String mode = request == null ? null : request.mode();
|
||||
return new StartRequest(appUsername, appCandidateName(appUsername), positionCode, mode);
|
||||
}
|
||||
|
||||
private static String currentAppUsername() {
|
||||
LoginUser loginUser = LoginHelper.getLoginUser();
|
||||
if (loginUser == null || !UserType.APP_USER.getUserType().equals(loginUser.getUserType())) {
|
||||
return "";
|
||||
}
|
||||
String username = loginUser.getUsername();
|
||||
return username == null ? "" : username.trim();
|
||||
}
|
||||
|
||||
private static String appCandidateName(String username) {
|
||||
return username.matches("1[3-9]\\d{9}") ? "候选人" + username.substring(username.length() - 4) : username;
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -1,13 +1,16 @@
|
||||
package org.dromara.aihr.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.aihr.domain.AihrOrgSyncDto.OrgSnapshotResponse;
|
||||
import org.dromara.aihr.domain.AihrOrgSyncDto.SyncRequest;
|
||||
import org.dromara.aihr.domain.AihrOrgSyncDto.SyncResponse;
|
||||
import org.dromara.aihr.service.AihrOrgSyncService;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
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.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@@ -21,4 +24,13 @@ public class AihrOrgSyncController {
|
||||
public R<SyncResponse> sync(@RequestBody(required = false) SyncRequest request) {
|
||||
return R.ok(orgSyncService.sync(request));
|
||||
}
|
||||
|
||||
@GetMapping("/snapshot")
|
||||
public R<OrgSnapshotResponse> snapshot(@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) String projectCode,
|
||||
@RequestParam(required = false) String positionLevel,
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(required = false, defaultValue = "200") Integer limit) {
|
||||
return R.ok(orgSyncService.snapshot(keyword, projectCode, positionLevel, status, limit));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -128,7 +128,7 @@ public class AihrPracticeController {
|
||||
if (username.isBlank()) {
|
||||
return request;
|
||||
}
|
||||
return new StartRequest(username, request.scenarioId(), request.mode());
|
||||
return new StartRequest(username, request.scenarioId(), request.mode(), request.assignmentId());
|
||||
}
|
||||
|
||||
private static String currentAppUsername() {
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ public final class AihrInterviewDto {
|
||||
private AihrInterviewDto() {
|
||||
}
|
||||
|
||||
public record StartRequest(String candidateId, String positionCode, String mode) {
|
||||
public record StartRequest(String candidateId, String candidateName, String positionCode, String mode) {
|
||||
}
|
||||
|
||||
public record StartResponse(String sessionId, String candidateId, String candidateName, String positionCode, List<QuestionResponse> questions, String source) {
|
||||
|
||||
+30
@@ -30,4 +30,34 @@ public final class AihrOrgSyncDto {
|
||||
List<String> warnings
|
||||
) {
|
||||
}
|
||||
|
||||
public record OrgSnapshotResponse(
|
||||
int total,
|
||||
int active,
|
||||
int departed,
|
||||
List<OrgProjectOption> projects,
|
||||
List<String> levels,
|
||||
List<OrgPersonRow> rows
|
||||
) {
|
||||
}
|
||||
|
||||
public record OrgProjectOption(
|
||||
String projectCode,
|
||||
String projectName
|
||||
) {
|
||||
}
|
||||
|
||||
public record OrgPersonRow(
|
||||
Long id,
|
||||
String projectCode,
|
||||
String projectName,
|
||||
String deptName,
|
||||
String extPartyId,
|
||||
String personName,
|
||||
String positionName,
|
||||
String positionLevel,
|
||||
String employmentStatus,
|
||||
String snapshotDate
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@ public final class AihrPracticeDto {
|
||||
private AihrPracticeDto() {
|
||||
}
|
||||
|
||||
public record StartRequest(String extPartyId, String scenarioId, String mode) {
|
||||
public record StartRequest(String extPartyId, String scenarioId, String mode, Long assignmentId) {
|
||||
}
|
||||
|
||||
public record StartResponse(
|
||||
@@ -97,7 +97,7 @@ public final class AihrPracticeDto {
|
||||
) {
|
||||
}
|
||||
|
||||
public record FinishRequest(String sessionId) {
|
||||
public record FinishRequest(String sessionId, Long assignmentId) {
|
||||
}
|
||||
|
||||
public record FinishResponse(
|
||||
@@ -113,7 +113,7 @@ public final class AihrPracticeDto {
|
||||
public record DimensionResponse(String label, Object value, String note) {
|
||||
}
|
||||
|
||||
public record RecordResponse(Long id, String sessionId, String time, String trainee, String scene, Integer score, String status, String summary) {
|
||||
public record RecordResponse(Long id, String sessionId, String time, String trainee, String scene, Integer score, String status, String summary, String extPartyId) {
|
||||
}
|
||||
|
||||
public record DialogueResponse(String role, String label, String text, String audioUrl, Integer emotion, Integer trust, Boolean redFlag, String coachHint) {
|
||||
|
||||
+20
-1
@@ -83,7 +83,26 @@ public class AihrCaseService {
|
||||
summary.stream().map(item -> item.label() + ":" + item.text()).findFirst().orElse(state.transcript())
|
||||
);
|
||||
saveCase(state, "已入库", record.title(), record.summary());
|
||||
return new CurateResponse(List.of(state.id()), List.of(record), "已按真实转写内容生成培训案例,视频仍使用预渲染样片。");
|
||||
return new CurateResponse(List.of(state.id()), List.of(record), "已按真实转写内容生成培训案例,已进入案例库。");
|
||||
}
|
||||
|
||||
public List<RecordResponse> records(Integer limit) {
|
||||
ensureCaseTable();
|
||||
int safeLimit = limit == null ? 10 : Math.max(1, Math.min(20, limit));
|
||||
return jdbcTemplate.query("""
|
||||
SELECT title, type, status, update_time, owner, summary
|
||||
FROM aihr_case_record
|
||||
WHERE tenant_id = '000000'
|
||||
ORDER BY update_time DESC, id DESC
|
||||
LIMIT ?
|
||||
""", (rs, rowNum) -> new RecordResponse(
|
||||
firstNonBlank(rs.getString("title"), "现场案例"),
|
||||
firstNonBlank(rs.getString("type"), "现场案例"),
|
||||
firstNonBlank(rs.getString("status"), "已入库"),
|
||||
rs.getTimestamp("update_time") == null ? "" : rs.getTimestamp("update_time").toLocalDateTime().format(TIME_FORMAT),
|
||||
firstNonBlank(rs.getString("owner"), "培训组"),
|
||||
firstNonBlank(rs.getString("summary"), "")
|
||||
), safeLimit);
|
||||
}
|
||||
|
||||
private void saveCase(CaseState state, String status, String title, String summary) {
|
||||
|
||||
+49
-5
@@ -44,7 +44,12 @@ public class AihrInterviewService {
|
||||
private volatile boolean tableReady;
|
||||
|
||||
public StartResponse start(StartRequest request) {
|
||||
CandidateProfile candidate = resolveCandidate(request == null ? null : request.candidateId(), null);
|
||||
CandidateProfile candidate = resolveCandidate(
|
||||
request == null ? null : request.candidateId(),
|
||||
request == null ? null : request.candidateName(),
|
||||
request == null ? null : request.positionCode(),
|
||||
null
|
||||
);
|
||||
GeneratedQuestions generated = generateQuestions(candidate).orElseGet(() -> new GeneratedQuestions(candidate.questions(), "local-rubric"));
|
||||
String sessionId = "iv-" + UUID.randomUUID();
|
||||
sessions.put(sessionId, new InterviewSession(candidate, generated.questions(), new ConcurrentHashMap<>(), generated.source()));
|
||||
@@ -70,7 +75,7 @@ public class AihrInterviewService {
|
||||
InterviewSession session = sessions.computeIfAbsent(
|
||||
sessionKey,
|
||||
key -> {
|
||||
CandidateProfile candidate = resolveCandidate(null, key);
|
||||
CandidateProfile candidate = resolveCandidate(null, null, null, key);
|
||||
return new InterviewSession(candidate, candidate.questions(), new ConcurrentHashMap<>(), "local-rubric");
|
||||
}
|
||||
);
|
||||
@@ -98,15 +103,27 @@ public class AihrInterviewService {
|
||||
}
|
||||
|
||||
public List<RecordResponse> records(int limit) {
|
||||
return records(limit, null);
|
||||
}
|
||||
|
||||
public List<RecordResponse> records(int limit, String candidateId) {
|
||||
ensureResultTable();
|
||||
int safeLimit = Math.max(1, Math.min(limit <= 0 ? 20 : limit, 100));
|
||||
return jdbcTemplate.query("""
|
||||
StringBuilder sql = new StringBuilder("""
|
||||
SELECT finished_time, candidate_name, position_code, total_score, result, ai_summary
|
||||
FROM aihr_interview_result
|
||||
WHERE tenant_id = '000000'
|
||||
""");
|
||||
List<Object> args = new ArrayList<>();
|
||||
if (!isBlank(candidateId)) {
|
||||
sql.append(" AND candidate_id = ?");
|
||||
args.add(clean(candidateId));
|
||||
}
|
||||
sql.append("""
|
||||
ORDER BY finished_time DESC, id DESC
|
||||
LIMIT %d
|
||||
""".formatted(safeLimit), this::mapRecord);
|
||||
""".formatted(safeLimit));
|
||||
return jdbcTemplate.query(sql.toString(), this::mapRecord, args.toArray());
|
||||
}
|
||||
|
||||
private RecordResponse mapRecord(ResultSet rs, int rowNum) throws SQLException {
|
||||
@@ -330,13 +347,40 @@ public class AihrInterviewService {
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private CandidateProfile resolveCandidate(String candidateId, String sessionId) {
|
||||
private CandidateProfile resolveCandidate(String candidateId, String candidateName, String positionCode, String sessionId) {
|
||||
if (!isBlank(candidateId) && candidates.containsKey(candidateId)) {
|
||||
return candidates.get(candidateId);
|
||||
}
|
||||
if (!isBlank(candidateId)) {
|
||||
return dynamicCandidate(candidateId, candidateName, positionCode);
|
||||
}
|
||||
return candidates.get("zhang-ming");
|
||||
}
|
||||
|
||||
private CandidateProfile dynamicCandidate(String candidateId, String candidateName, String positionCode) {
|
||||
CandidateProfile template = candidates.get("zhang-ming");
|
||||
String cleanId = clean(candidateId);
|
||||
String name = firstNonBlank(candidateName, cleanId.matches("1[3-9]\\d{9}") ? "候选人" + cleanId.substring(cleanId.length() - 4) : cleanId);
|
||||
return new CandidateProfile(
|
||||
cleanId,
|
||||
name,
|
||||
positionName(positionCode),
|
||||
"待分配项目",
|
||||
"候选人补充资料待审核",
|
||||
"需通过结构化面试确认服务场景能力",
|
||||
template.capabilities(),
|
||||
template.questions()
|
||||
);
|
||||
}
|
||||
|
||||
private static String positionName(String positionCode) {
|
||||
String code = clean(positionCode);
|
||||
if (code.isBlank() || "customer-service".equals(code) || "life-consultant".equals(code)) {
|
||||
return "生活顾问";
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
private static Map<String, CandidateProfile> buildCandidates() {
|
||||
Map<String, CandidateProfile> result = new LinkedHashMap<>();
|
||||
result.put("zhang-ming", new CandidateProfile(
|
||||
|
||||
+1
-1
@@ -167,7 +167,7 @@ public class AihrMobileSeedService {
|
||||
new PrimaryTaskResponse(
|
||||
"重点训练",
|
||||
"投诉接待三角色对练",
|
||||
"3分钟语音练习 · 完成后自动评分",
|
||||
"3分钟情景练习 · 完成后自动评分",
|
||||
"1/1",
|
||||
100,
|
||||
"开始训练",
|
||||
|
||||
+117
-11
@@ -4,6 +4,9 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.aihr.domain.AihrOrgSyncDto.OrgPersonRow;
|
||||
import org.dromara.aihr.domain.AihrOrgSyncDto.OrgProjectOption;
|
||||
import org.dromara.aihr.domain.AihrOrgSyncDto.OrgSnapshotResponse;
|
||||
import org.dromara.aihr.domain.AihrOrgSyncDto.SyncRequest;
|
||||
import org.dromara.aihr.domain.AihrOrgSyncDto.SyncResponse;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -23,9 +26,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
@@ -65,7 +66,7 @@ public class AihrOrgSyncService {
|
||||
SyncRequest req = request == null ? new SyncRequest(null, null, null, null, null, null, null) : request;
|
||||
String baseUrl = normalizeBaseUrl(configuredBaseUrl);
|
||||
if (baseUrl.isBlank()) {
|
||||
throw new IllegalArgumentException("请先配置 AIHR_ORG_SYNC_BASE_URL,值为外部开放平台 /open/v1 前缀");
|
||||
throw new IllegalArgumentException("请先配置 AIHR_ORG_SYNC_BASE_URL,值为外部开放平台 /api/open/v1 前缀");
|
||||
}
|
||||
requireSnapshotTable();
|
||||
|
||||
@@ -112,6 +113,39 @@ public class AihrOrgSyncService {
|
||||
);
|
||||
}
|
||||
|
||||
public OrgSnapshotResponse snapshot(String keyword, String projectCode, String positionLevel, String status, Integer limit) {
|
||||
requireSnapshotTable();
|
||||
int safeLimit = clamp(limit, 200, 1, MAX_PAGE_SIZE);
|
||||
List<Object> args = new ArrayList<>();
|
||||
String where = orgSnapshotWhere(keyword, projectCode, positionLevel, status, args);
|
||||
int total = countOrgRows("select count(*) from aihr_org_snapshot" + where, args);
|
||||
int active = countOrgRows("select count(*) from aihr_org_snapshot" + where + " and employment_status = 'active'", args);
|
||||
int departed = countOrgRows("select count(*) from aihr_org_snapshot" + where + " and employment_status = 'departed'", args);
|
||||
List<Object> rowArgs = new ArrayList<>(args);
|
||||
rowArgs.add(safeLimit);
|
||||
List<OrgPersonRow> rows = jdbcTemplate.query("""
|
||||
select id, project_code, project_name, dept_name, ext_party_id, person_name,
|
||||
position_name, position_level, employment_status,
|
||||
date_format(snapshot_date, '%Y-%m-%d') snapshot_date
|
||||
from aihr_org_snapshot
|
||||
""" + where + """
|
||||
order by project_name, dept_name, position_level, person_name
|
||||
limit ?
|
||||
""", (rs, rowNum) -> new OrgPersonRow(
|
||||
rs.getLong("id"),
|
||||
rs.getString("project_code"),
|
||||
rs.getString("project_name"),
|
||||
rs.getString("dept_name"),
|
||||
rs.getString("ext_party_id"),
|
||||
rs.getString("person_name"),
|
||||
rs.getString("position_name"),
|
||||
rs.getString("position_level"),
|
||||
rs.getString("employment_status"),
|
||||
rs.getString("snapshot_date")
|
||||
), rowArgs.toArray());
|
||||
return new OrgSnapshotResponse(total, active, departed, orgProjects(), orgLevels(), rows);
|
||||
}
|
||||
|
||||
private List<JsonNode> fetchOptional(String baseUrl, String token, SyncRequest req, String resourceType,
|
||||
int pageSize, int maxPages, List<String> warnings) {
|
||||
try {
|
||||
@@ -131,7 +165,7 @@ public class AihrOrgSyncService {
|
||||
Map<String, String> query = new TreeMap<>();
|
||||
query.put("resource_type", resourceType);
|
||||
query.put("page", String.valueOf(page));
|
||||
query.put("page_size", String.valueOf(pageSize));
|
||||
query.put("limit", String.valueOf(pageSize));
|
||||
putIfNotBlank(query, "group_id", req.groupId());
|
||||
putIfNotBlank(query, "company_id", req.companyId());
|
||||
putIfNotBlank(query, "department_id", req.departmentId());
|
||||
@@ -190,16 +224,16 @@ public class AihrOrgSyncService {
|
||||
HttpRequest.Builder builder = HttpRequest.newBuilder(uri)
|
||||
.timeout(HTTP_TIMEOUT)
|
||||
.header("Accept", "application/json");
|
||||
String requestBody = body == null ? "" : body;
|
||||
if (!clean(token).isBlank()) {
|
||||
builder.header("Authorization", bearer(token));
|
||||
sign(builder, method, uri.getRawPath(), queryString, requestBody);
|
||||
}
|
||||
String requestBody = body == null ? "" : body;
|
||||
if ("POST".equals(method)) {
|
||||
builder.header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(requestBody));
|
||||
} else {
|
||||
builder.GET();
|
||||
}
|
||||
sign(builder, method, uri.getRawPath(), queryString, requestBody);
|
||||
HttpResponse<String> response = HttpClient.newBuilder()
|
||||
.connectTimeout(HTTP_TIMEOUT)
|
||||
.build()
|
||||
@@ -218,20 +252,21 @@ public class AihrOrgSyncService {
|
||||
}
|
||||
|
||||
private void sign(HttpRequest.Builder builder, String method, String path, String queryString, String body) throws Exception {
|
||||
String signingSecret = clean(configuredSigningSecret);
|
||||
String signingSecret = firstNonBlank(configuredSigningSecret, configuredClientSecret);
|
||||
String clientId = clean(configuredClientId);
|
||||
if (signingSecret.isBlank() || clientId.isBlank()) {
|
||||
return;
|
||||
}
|
||||
String timestamp = Instant.now().toString();
|
||||
String timestamp = String.valueOf(System.currentTimeMillis());
|
||||
String nonce = UUID.randomUUID().toString();
|
||||
String plain = method + "\n" + path + "\n" + queryString + "\n" + sha256(body) + "\n" + timestamp + "\n" + nonce;
|
||||
String plain = method + "\n" + path + "\n" + queryString + "\n" + sha256(signatureBody(body)) + "\n" + timestamp + "\n" + nonce;
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(signingSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
|
||||
builder.header("X-Client-Id", clientId)
|
||||
.header("X-Client-Secret", clean(configuredClientSecret))
|
||||
.header("X-Timestamp", timestamp)
|
||||
.header("X-Nonce", nonce)
|
||||
.header("X-Signature", Base64.getEncoder().encodeToString(mac.doFinal(plain.getBytes(StandardCharsets.UTF_8))));
|
||||
.header("X-Signature", hex(mac.doFinal(plain.getBytes(StandardCharsets.UTF_8))));
|
||||
}
|
||||
|
||||
private void saveRows(List<OrgRow> rows, boolean replaceExisting) {
|
||||
@@ -325,6 +360,69 @@ public class AihrOrgSyncService {
|
||||
return map;
|
||||
}
|
||||
|
||||
private String orgSnapshotWhere(String keyword, String projectCode, String positionLevel, String status, List<Object> args) {
|
||||
StringBuilder where = new StringBuilder(" where tenant_id = ?");
|
||||
args.add(TENANT_ID);
|
||||
String keywordValue = clean(keyword);
|
||||
if (!keywordValue.isBlank()) {
|
||||
String like = "%" + keywordValue + "%";
|
||||
where.append("""
|
||||
and (person_name like ? or ext_party_id like ? or position_name like ?
|
||||
or dept_name like ? or project_name like ?)
|
||||
""");
|
||||
args.add(like);
|
||||
args.add(like);
|
||||
args.add(like);
|
||||
args.add(like);
|
||||
args.add(like);
|
||||
}
|
||||
String project = clean(projectCode);
|
||||
if (!project.isBlank()) {
|
||||
where.append(" and project_code = ?");
|
||||
args.add(project);
|
||||
}
|
||||
String levelValue = clean(positionLevel);
|
||||
if (!levelValue.isBlank()) {
|
||||
where.append(" and position_level = ?");
|
||||
args.add(levelValue);
|
||||
}
|
||||
String statusValue = clean(status);
|
||||
if (!statusValue.isBlank()) {
|
||||
where.append(" and employment_status = ?");
|
||||
args.add(statusValue);
|
||||
}
|
||||
return where.toString();
|
||||
}
|
||||
|
||||
private int countOrgRows(String sql, List<Object> args) {
|
||||
Integer count = jdbcTemplate.queryForObject(sql, Integer.class, args.toArray());
|
||||
return count == null ? 0 : count;
|
||||
}
|
||||
|
||||
private List<OrgProjectOption> orgProjects() {
|
||||
return jdbcTemplate.query("""
|
||||
select project_code, max(project_name) project_name
|
||||
from aihr_org_snapshot
|
||||
where tenant_id = ?
|
||||
group by project_code
|
||||
order by project_name
|
||||
limit 200
|
||||
""", (rs, rowNum) -> new OrgProjectOption(
|
||||
rs.getString("project_code"),
|
||||
rs.getString("project_name")
|
||||
), TENANT_ID);
|
||||
}
|
||||
|
||||
private List<String> orgLevels() {
|
||||
return jdbcTemplate.query("""
|
||||
select distinct position_level
|
||||
from aihr_org_snapshot
|
||||
where tenant_id = ? and position_level is not null and position_level <> ''
|
||||
order by position_level
|
||||
limit 20
|
||||
""", (rs, rowNum) -> rs.getString("position_level"), TENANT_ID);
|
||||
}
|
||||
|
||||
private void requireSnapshotTable() {
|
||||
try {
|
||||
Integer count = jdbcTemplate.queryForObject("""
|
||||
@@ -367,7 +465,7 @@ public class AihrOrgSyncService {
|
||||
private static String text(JsonNode node, String... fields) {
|
||||
for (String field : fields) {
|
||||
JsonNode child = node.path(field);
|
||||
if (child.isValueNode()) {
|
||||
if (child.isValueNode() && !child.isNull()) {
|
||||
String value = clean(child.asText());
|
||||
if (!value.isBlank()) {
|
||||
return value;
|
||||
@@ -417,6 +515,14 @@ public class AihrOrgSyncService {
|
||||
private static String sha256(String value) throws Exception {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] bytes = digest.digest((value == null ? "" : value).getBytes(StandardCharsets.UTF_8));
|
||||
return hex(bytes);
|
||||
}
|
||||
|
||||
private static String signatureBody(String value) {
|
||||
return clean(value).isBlank() ? "{}" : value;
|
||||
}
|
||||
|
||||
private static String hex(byte[] bytes) {
|
||||
StringBuilder hex = new StringBuilder(bytes.length * 2);
|
||||
for (byte b : bytes) {
|
||||
hex.append(String.format("%02x", b));
|
||||
|
||||
+86
-29
@@ -255,7 +255,7 @@ public class AihrPracticeSeedService {
|
||||
String trainee = resolveTrainee(request, scenario);
|
||||
List<String> customerLines = new ArrayList<>();
|
||||
customerLines.add(firstRound.customer());
|
||||
activeSessions.put(sessionId, new ActiveSession(scenario.id(), trainee, resolveExtPartyId(request, trainee), isMobile(request), LocalDateTime.now(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), customerLines, new ArrayList<>()));
|
||||
activeSessions.put(sessionId, new ActiveSession(scenario.id(), trainee, resolveExtPartyId(request, trainee), request == null ? null : request.assignmentId(), isMobile(request), LocalDateTime.now(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), customerLines, new ArrayList<>()));
|
||||
return new StartResponse(
|
||||
sessionId,
|
||||
scenario.id(),
|
||||
@@ -411,8 +411,10 @@ public class AihrPracticeSeedService {
|
||||
ScenarioSeed scenario = resolveScenario(activeSession == null ? null : activeSession.scenarioId(), sessionId);
|
||||
String trainee = activeSession == null ? scenario.trainee() : activeSession.trainee();
|
||||
PracticeResult result = evaluate(activeSession, scenario);
|
||||
RecordResponse record = new RecordResponse(null, sessionId, formatNow(), trainee, scenario.name(), result.total(), "待复盘", result.summary());
|
||||
String extPartyId = activeSession == null ? trainee : activeSession.extPartyId();
|
||||
RecordResponse record = new RecordResponse(null, sessionId, formatNow(), trainee, scenario.name(), result.total(), "待复盘", result.summary(), extPartyId);
|
||||
savePracticeRecord(sessionId, activeSession, scenario, record, result);
|
||||
markAssignmentCompleted(request == null ? null : request.assignmentId(), activeSession);
|
||||
return new FinishResponse(
|
||||
result.total(),
|
||||
result.scores(),
|
||||
@@ -459,7 +461,7 @@ public class AihrPracticeSeedService {
|
||||
int safeLimit = normalizeLimit(limit);
|
||||
if (isBlank(extPartyId)) {
|
||||
return jdbcTemplate.query("""
|
||||
SELECT id, session_id, finished_time, trainee_name, scenario_name, total_score, status, summary
|
||||
SELECT id, session_id, finished_time, trainee_name, scenario_name, total_score, status, summary, ext_party_id
|
||||
FROM aihr_practice_session
|
||||
WHERE tenant_id = ? AND mode = 'mobile'
|
||||
ORDER BY finished_time DESC, id DESC
|
||||
@@ -467,7 +469,7 @@ public class AihrPracticeSeedService {
|
||||
""", this::mapRecord, TENANT_ID, safeLimit);
|
||||
}
|
||||
return jdbcTemplate.query("""
|
||||
SELECT id, session_id, finished_time, trainee_name, scenario_name, total_score, status, summary
|
||||
SELECT id, session_id, finished_time, trainee_name, scenario_name, total_score, status, summary, ext_party_id
|
||||
FROM aihr_practice_session
|
||||
WHERE tenant_id = ? AND mode = 'mobile' AND ext_party_id = ?
|
||||
ORDER BY finished_time DESC, id DESC
|
||||
@@ -478,10 +480,10 @@ public class AihrPracticeSeedService {
|
||||
public List<RecordResponse> pendingReviewRecords(int limit) {
|
||||
ensurePracticeTable();
|
||||
return jdbcTemplate.query("""
|
||||
SELECT id, session_id, finished_time, trainee_name, scenario_name, total_score, status, summary
|
||||
SELECT id, session_id, finished_time, trainee_name, scenario_name, total_score, status, summary, ext_party_id
|
||||
FROM aihr_practice_session
|
||||
WHERE tenant_id = ? AND mode = 'mobile' AND status = '待复盘'
|
||||
ORDER BY finished_time DESC, id DESC
|
||||
ORDER BY CASE WHEN total_score < 80 THEN 0 ELSE 1 END, total_score ASC, finished_time DESC, id DESC
|
||||
LIMIT ?
|
||||
""", this::mapRecord, TENANT_ID, normalizeLimit(limit));
|
||||
}
|
||||
@@ -729,7 +731,9 @@ public class AihrPracticeSeedService {
|
||||
), TENANT_ID, party, party);
|
||||
CompetencySnapshot snapshot = rows.isEmpty() ? new CompetencySnapshot(0, 82, 0, 82, 82, 82, 0) : rows.get(0);
|
||||
int trainingMinutes = snapshot.completed() * 8;
|
||||
int contribution = Math.min(100, snapshot.incentivePoints() + snapshot.completed() * 8 + Math.max(0, 100 - snapshot.pendingReview() * 10) / 5);
|
||||
int contribution = snapshot.completed() == 0 && snapshot.incentivePoints() == 0
|
||||
? 0
|
||||
: Math.min(100, snapshot.incentivePoints() + snapshot.completed() * 8 + Math.max(0, 100 - snapshot.pendingReview() * 10) / 5);
|
||||
int assessmentScore = snapshot.score();
|
||||
String aiLevel = aiLevel(snapshot.completed(), assessmentScore);
|
||||
return new CompetencyResponse(
|
||||
@@ -827,7 +831,11 @@ public class AihrPracticeSeedService {
|
||||
WHERE tenant_id = ?
|
||||
AND (? = '' OR ext_party_id = ?)
|
||||
AND (status = '待训练' OR (source = 'daily' AND DATE(create_time) = CURRENT_DATE()))
|
||||
ORDER BY create_time DESC, id DESC
|
||||
ORDER BY
|
||||
CASE WHEN source = 'daily' THEN 1 ELSE 0 END,
|
||||
CASE WHEN source = 'daily' THEN id ELSE NULL END ASC,
|
||||
create_time DESC,
|
||||
id DESC
|
||||
LIMIT ?
|
||||
""", this::mapAssignment, TENANT_ID, party, party, normalizeLimit(limit));
|
||||
}
|
||||
@@ -844,11 +852,7 @@ public class AihrPracticeSeedService {
|
||||
if (existingToday >= 3) {
|
||||
return;
|
||||
}
|
||||
List<DailyDrillSeed> drills = List.of(
|
||||
new DailyDrillSeed("complaint-water", "每日三题 1:业主投诉车库积水,第一句话先说什么?", "先承接情绪,再确认点位、责任人和首次反馈时间。"),
|
||||
new DailyDrillSeed("fee-collection", "每日三题 2:业主拒缴物业费并质疑服务,如何拆分问题?", "把服务整改和费用沟通分开,说明复核、整改跟进和缴费节点。"),
|
||||
new DailyDrillSeed("repair-leak", "每日三题 3:报修跟进超时,如何给下一步承诺?", "说明已核实的信息、责任人、到场或反馈时间,并主动同步变化。")
|
||||
);
|
||||
List<DailyDrillSeed> drills = dailyDrills();
|
||||
int start = Math.max(0, existingToday);
|
||||
for (int i = start; i < drills.size(); i++) {
|
||||
DailyDrillSeed drill = drills.get(i);
|
||||
@@ -864,7 +868,7 @@ public class AihrPracticeSeedService {
|
||||
extPartyId.matches("\\d{11}") ? "手机用户" + extPartyId.substring(7) : extPartyId,
|
||||
scenario.id(),
|
||||
scenario.name(),
|
||||
drill.question() + " 参考答案:" + drill.referenceAnswer(),
|
||||
drill.question(),
|
||||
Timestamp.valueOf(now),
|
||||
Timestamp.valueOf(now)
|
||||
);
|
||||
@@ -880,15 +884,48 @@ public class AihrPracticeSeedService {
|
||||
String traineeName = firstNonBlank(request == null ? null : request.traineeName(), extPartyId);
|
||||
String source = firstNonBlank(request == null ? null : request.source(), "manual");
|
||||
String reason = firstNonBlank(request == null ? null : request.reason(), "主管派发专项训练");
|
||||
Timestamp createdAt = Timestamp.valueOf(now);
|
||||
jdbcTemplate.update("""
|
||||
INSERT INTO aihr_practice_assignment
|
||||
(tenant_id, ext_party_id, trainee_name, scenario_id, scenario_name, source, reason, status, create_time, update_time)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, '待训练', ?, ?)
|
||||
""", TENANT_ID, extPartyId, traineeName, scenario.id(), firstNonBlank(request == null ? null : request.scenarioName(), scenario.name()), source, reason, Timestamp.valueOf(now), Timestamp.valueOf(now));
|
||||
List<PracticeAssignmentResponse> rows = assignments(extPartyId, 1);
|
||||
""", TENANT_ID, extPartyId, traineeName, scenario.id(), firstNonBlank(request == null ? null : request.scenarioName(), scenario.name()), source, reason, createdAt, createdAt);
|
||||
List<PracticeAssignmentResponse> rows = jdbcTemplate.query("""
|
||||
SELECT id, ext_party_id, trainee_name, scenario_id, scenario_name, source, reason, status, create_time,
|
||||
answer_text, feedback, correct
|
||||
FROM aihr_practice_assignment
|
||||
WHERE tenant_id = ? AND ext_party_id = ? AND scenario_id = ? AND source = ? AND reason = ?
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
""", this::mapAssignment, TENANT_ID, extPartyId, scenario.id(), source, reason);
|
||||
return rows.isEmpty() ? new PracticeAssignmentResponse(null, extPartyId, traineeName, scenario.id(), scenario.name(), source, reason, "待训练", now.format(TIME_FORMATTER), null, null, null, null, null) : rows.get(0);
|
||||
}
|
||||
|
||||
private void markAssignmentCompleted(Long requestAssignmentId, ActiveSession activeSession) {
|
||||
Long assignmentId = requestAssignmentId == null && activeSession != null ? activeSession.assignmentId() : requestAssignmentId;
|
||||
if (assignmentId == null || activeSession == null || isBlank(activeSession.extPartyId())) {
|
||||
return;
|
||||
}
|
||||
ensureAssignmentTable();
|
||||
jdbcTemplate.update("""
|
||||
UPDATE aihr_practice_assignment
|
||||
SET status = '已完成',
|
||||
finished_time = ?,
|
||||
update_time = ?
|
||||
WHERE tenant_id = ?
|
||||
AND id = ?
|
||||
AND ext_party_id = ?
|
||||
AND source <> 'daily'
|
||||
AND status = '待训练'
|
||||
""",
|
||||
Timestamp.valueOf(LocalDateTime.now()),
|
||||
Timestamp.valueOf(LocalDateTime.now()),
|
||||
TENANT_ID,
|
||||
assignmentId,
|
||||
activeSession.extPartyId()
|
||||
);
|
||||
}
|
||||
|
||||
public DailyDrillAnswerResponse answerDailyDrill(Long id, DailyDrillAnswerRequest request) {
|
||||
return answerDailyDrill(id, request, "");
|
||||
}
|
||||
@@ -900,7 +937,7 @@ public class AihrPracticeSeedService {
|
||||
}
|
||||
String party = isBlank(extPartyId) ? "" : extPartyId.trim();
|
||||
List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
|
||||
SELECT id, reason, source, status
|
||||
SELECT id, reason, source, status, scenario_id
|
||||
FROM aihr_practice_assignment
|
||||
WHERE tenant_id = ? AND id = ? AND source = 'daily'
|
||||
AND (? = '' OR ext_party_id = ?)
|
||||
@@ -909,7 +946,7 @@ public class AihrPracticeSeedService {
|
||||
if (rows.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
DailyDrillParts parts = dailyDrillParts((String) rows.get(0).get("reason"));
|
||||
DailyDrillParts parts = dailyDrillParts((String) rows.get(0).get("reason"), (String) rows.get(0).get("scenario_id"));
|
||||
String answer = maskSensitiveText(firstNonBlank(request == null ? null : request.answer(), ""));
|
||||
DailyDrillScore score = scoreDailyDrill(answer, parts.referenceAnswer());
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
@@ -1233,7 +1270,8 @@ public class AihrPracticeSeedService {
|
||||
rs.getString("scenario_name"),
|
||||
rs.getInt("total_score"),
|
||||
rs.getString("status"),
|
||||
rs.getString("summary")
|
||||
rs.getString("summary"),
|
||||
rs.getString("ext_party_id")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1241,7 +1279,8 @@ public class AihrPracticeSeedService {
|
||||
Timestamp createTime = rs.getTimestamp("create_time");
|
||||
String source = rs.getString("source");
|
||||
String reason = rs.getString("reason");
|
||||
DailyDrillParts daily = dailyDrillParts(reason);
|
||||
String status = rs.getString("status");
|
||||
DailyDrillParts daily = dailyDrillParts(reason, rs.getString("scenario_id"));
|
||||
Object correctValue = rs.getObject("correct");
|
||||
return new PracticeAssignmentResponse(
|
||||
rs.getLong("id"),
|
||||
@@ -1250,23 +1289,36 @@ public class AihrPracticeSeedService {
|
||||
rs.getString("scenario_id"),
|
||||
rs.getString("scenario_name"),
|
||||
source,
|
||||
reason,
|
||||
rs.getString("status"),
|
||||
"daily".equals(source) ? daily.question() : reason,
|
||||
status,
|
||||
createTime == null ? "" : createTime.toLocalDateTime().format(TIME_FORMATTER),
|
||||
"daily".equals(source) ? daily.question() : null,
|
||||
"daily".equals(source) ? daily.referenceAnswer() : null,
|
||||
"daily".equals(source) && "已完成".equals(status) ? daily.referenceAnswer() : null,
|
||||
rs.getString("answer_text"),
|
||||
rs.getString("feedback"),
|
||||
"daily".equals(source) ? stripReferenceAnswer(rs.getString("feedback")) : rs.getString("feedback"),
|
||||
correctValue == null ? null : rs.getBoolean("correct")
|
||||
);
|
||||
}
|
||||
|
||||
private DailyDrillParts dailyDrillParts(String reason) {
|
||||
private List<DailyDrillSeed> dailyDrills() {
|
||||
return List.of(
|
||||
new DailyDrillSeed("complaint-water", "每日三题 1:业主投诉车库积水,第一句话先说什么?", "先承接情绪,再确认点位、责任人和首次反馈时间。"),
|
||||
new DailyDrillSeed("fee-collection", "每日三题 2:业主拒缴物业费并质疑服务,如何拆分问题?", "把服务整改和费用沟通分开,说明复核、整改跟进和缴费节点。"),
|
||||
new DailyDrillSeed("repair-leak", "每日三题 3:报修跟进超时,如何给下一步承诺?", "说明已核实的信息、责任人、到场或反馈时间,并主动同步变化。")
|
||||
);
|
||||
}
|
||||
|
||||
private DailyDrillParts dailyDrillParts(String reason, String scenarioId) {
|
||||
String safeReason = firstNonBlank(reason, "");
|
||||
String marker = "参考答案:";
|
||||
int markerIndex = safeReason.indexOf(marker);
|
||||
if (markerIndex < 0) {
|
||||
return new DailyDrillParts(safeReason, "");
|
||||
String reference = dailyDrills().stream()
|
||||
.filter(drill -> drill.question().equals(safeReason) || drill.scenarioId().equals(scenarioId))
|
||||
.map(DailyDrillSeed::referenceAnswer)
|
||||
.findFirst()
|
||||
.orElse("");
|
||||
return new DailyDrillParts(safeReason, reference);
|
||||
}
|
||||
String question = safeReason.substring(0, markerIndex).trim();
|
||||
String reference = safeReason.substring(markerIndex + marker.length()).trim();
|
||||
@@ -1295,11 +1347,16 @@ public class AihrPracticeSeedService {
|
||||
boolean correct = score >= 70;
|
||||
String feedback = (correct ? "回答方向正确。" : "还缺关键动作。")
|
||||
+ (hit.isEmpty() ? "" : "已命中:" + String.join("、", hit) + "。")
|
||||
+ (missing.isEmpty() ? "" : "建议补充:" + String.join("、", missing) + "。")
|
||||
+ (isBlank(reference) ? "" : "参考答案:" + reference);
|
||||
+ (missing.isEmpty() ? "" : "建议补充:" + String.join("、", missing) + "。");
|
||||
return new DailyDrillScore(correct, score, feedback);
|
||||
}
|
||||
|
||||
private String stripReferenceAnswer(String feedback) {
|
||||
String text = firstNonBlank(feedback, "");
|
||||
int index = text.indexOf("参考答案:");
|
||||
return index < 0 ? text : text.substring(0, index).trim();
|
||||
}
|
||||
|
||||
private List<String> dailyDrillKeywords(String reference) {
|
||||
List<String> keywords = List.of("承接情绪", "确认", "点位", "责任人", "反馈时间", "服务整改", "费用沟通", "节点", "核实信息", "到场", "同步变化");
|
||||
List<String> result = new ArrayList<>();
|
||||
@@ -2507,7 +2564,7 @@ public class AihrPracticeSeedService {
|
||||
private record TurnEvidence(Integer emotion, Integer trust, Boolean redFlag, String coachHint) {
|
||||
}
|
||||
|
||||
private record ActiveSession(String scenarioId, String trainee, String extPartyId, boolean mobile, LocalDateTime startedAt,
|
||||
private record ActiveSession(String scenarioId, String trainee, String extPartyId, Long assignmentId, boolean mobile, LocalDateTime startedAt,
|
||||
List<String> traineeReplies, List<String> traineeAudioUrls, List<Long> traineeAudioOssIds,
|
||||
List<String> customerLines, List<TurnEvidence> turnEvidence) {
|
||||
}
|
||||
|
||||
+27
-1
@@ -2216,7 +2216,7 @@ public class AihrSopSeedService {
|
||||
}
|
||||
SearchDigest digest = searchDigest(queryText, hits);
|
||||
if (digest == null) {
|
||||
return noEvidenceResponse(queryText, responseCategory, extraCautions);
|
||||
return rawHitResponse(responseCategory, queryText, List.copyOf(docs.values()), snippets, extraCautions);
|
||||
}
|
||||
if (isNoEvidenceAnswer(digest.answer())) {
|
||||
return noEvidenceResponse(queryText, responseCategory, appendCautions(digest.cautions(), extraCautions));
|
||||
@@ -2236,6 +2236,25 @@ public class AihrSopSeedService {
|
||||
);
|
||||
}
|
||||
|
||||
static SearchResponse rawHitResponse(String category, String queryText, List<DocResponse> docs, List<SnippetResponse> snippets, List<String> extraCautions) {
|
||||
if (snippets == null || snippets.isEmpty()) {
|
||||
return noEvidenceResponse(queryText, category, extraCautions);
|
||||
}
|
||||
return new SearchResponse(
|
||||
queryText,
|
||||
firstNonBlank(category, "sop"),
|
||||
"已找到相关资料:" + snippetPreview(snippets.get(0).text()),
|
||||
"引用 " + (docs == null ? 0 : docs.size()) + " 份文档 / " + snippets.size() + " 个片段",
|
||||
docs == null ? List.of() : docs,
|
||||
snippets,
|
||||
List.of("题 1:请按命中 SOP 复述处理步骤。", "题 2:哪些信息必须留痕并可追溯?"),
|
||||
records(),
|
||||
List.of(),
|
||||
appendCautions(List.of("未生成结构化概要,已返回命中原文片段。"), extraCautions),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* RAG 生成环节:把命中片段交给 chat 模型压成「直接回答+要点+注意事项」结构化概要。
|
||||
* 模型未配置、超时或解析失败返回 null,由调用方回退片段原文拼接——检索永不因概要失败而报错。
|
||||
@@ -2509,6 +2528,13 @@ public class AihrSopSeedService {
|
||||
return score <= 0 ? "片段" : String.format("%.2f", score);
|
||||
}
|
||||
|
||||
private static String snippetPreview(String text) {
|
||||
String cleaned = Optional.ofNullable(text).orElse("")
|
||||
.replaceAll("\\s+", " ")
|
||||
.trim();
|
||||
return truncate(firstNonBlank(cleaned, "请参考下方命中片段。"), 180);
|
||||
}
|
||||
|
||||
private static boolean isNoEvidenceAnswer(String answer) {
|
||||
return NO_CLEAR_SOP_EVIDENCE.equals(Optional.ofNullable(answer).orElse("").trim());
|
||||
}
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package org.dromara.aihr.service;
|
||||
|
||||
import org.dromara.aihr.domain.AihrSopDto;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class AihrSopSeedServiceTest {
|
||||
|
||||
@Test
|
||||
@Tag("dev")
|
||||
public void rawHitResponseKeepsSnippetsWhenDigestUnavailable() {
|
||||
List<AihrSopDto.DocResponse> docs = List.of(new AihrSopDto.DocResponse("印章管理办法.pdf", "制度文件", "命中 3.00", "pill-success"));
|
||||
List<AihrSopDto.SnippetResponse> snippets = List.of(new AihrSopDto.SnippetResponse("第 1 段:印章管理办法.pdf", "印章使用前必须取得审批,并保留用印记录。", 100310L));
|
||||
|
||||
AihrSopDto.SearchResponse response = AihrSopSeedService.rawHitResponse("sop", "印章怎么使用?", docs, snippets, List.of());
|
||||
|
||||
assertFalse(response.docs().isEmpty());
|
||||
assertEquals(100310L, response.snippets().get(0).fragmentId());
|
||||
assertTrue(response.answer().contains("印章使用前必须取得审批"));
|
||||
assertTrue(response.reference().contains("1 份文档 / 1 个片段"));
|
||||
assertTrue(response.cautions().contains("未生成结构化概要,已返回命中原文片段。"));
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -75,9 +75,9 @@ public class SysLogininforServiceImpl implements ISysLogininforService {
|
||||
// 打印信息到日志
|
||||
log.info(s.toString(), logininforEvent.getArgs());
|
||||
// 获取客户端操作系统
|
||||
String os = userAgent.getOs().getName();
|
||||
String os = userAgent == null ? "Unknown" : userAgent.getOs().getName();
|
||||
// 获取客户端浏览器
|
||||
String browser = userAgent.getBrowser().getName();
|
||||
String browser = userAgent == null ? "Unknown" : userAgent.getBrowser().getName();
|
||||
// 封装对象
|
||||
SysLogininforBo logininfor = new SysLogininforBo();
|
||||
logininfor.setTenantId(logininforEvent.getTenantId());
|
||||
|
||||
Reference in New Issue
Block a user