feat(aihr): scope supervisor practice data
This commit is contained in:
+6
-6
@@ -54,18 +54,18 @@ public class AihrMobileController {
|
||||
|
||||
@GetMapping("/practice/reviews")
|
||||
public R<List<RecordResponse>> practiceReviews() {
|
||||
return R.ok(mobileSeedService.practiceReviews());
|
||||
return R.ok(mobileSeedService.practiceReviews(currentAppUsername()));
|
||||
}
|
||||
|
||||
@GetMapping("/practice/reviews/{id}")
|
||||
public R<Map<String, Object>> practiceReview(@PathVariable Long id) {
|
||||
ReviewDetailResponse detail = mobileSeedService.practiceReview(id);
|
||||
ReviewDetailResponse detail = mobileSeedService.practiceReview(id, currentAppUsername());
|
||||
return detail == null ? R.fail("复盘记录不存在") : R.ok(reviewDetailPayload(detail));
|
||||
}
|
||||
|
||||
@PostMapping("/practice/reviews/{id}/reviewed")
|
||||
public R<Void> markReviewed(@PathVariable Long id, @RequestBody(required = false) ReviewAdviceRequest request) {
|
||||
return mobileSeedService.markReviewed(id, request) ? R.ok() : R.fail("复盘记录不存在");
|
||||
return mobileSeedService.markReviewed(id, request, currentAppUsername()) ? R.ok() : R.fail("复盘记录不存在");
|
||||
}
|
||||
|
||||
@GetMapping("/profile")
|
||||
@@ -80,17 +80,17 @@ public class AihrMobileController {
|
||||
|
||||
@GetMapping("/practice/alerts")
|
||||
public R<List<PracticeAlertResponse>> practiceAlerts() {
|
||||
return R.ok(mobileSeedService.practiceAlerts());
|
||||
return R.ok(mobileSeedService.practiceAlerts(currentAppUsername()));
|
||||
}
|
||||
|
||||
@GetMapping("/practice/assignments")
|
||||
public R<List<PracticeAssignmentResponse>> practiceAssignments(@RequestParam(required = false) String extPartyId) {
|
||||
return R.ok(mobileSeedService.practiceAssignments(scopedAssignmentExtPartyId(extPartyId)));
|
||||
return R.ok(mobileSeedService.practiceAssignments(scopedAssignmentExtPartyId(extPartyId), currentAppUsername()));
|
||||
}
|
||||
|
||||
@PostMapping("/practice/assignments")
|
||||
public R<PracticeAssignmentResponse> createPracticeAssignment(@RequestBody PracticeAssignmentRequest request) {
|
||||
return R.ok(mobileSeedService.createPracticeAssignment(request));
|
||||
return R.ok(mobileSeedService.createPracticeAssignment(request, currentAppUsername()));
|
||||
}
|
||||
|
||||
@PostMapping("/practice/assignments/{id}/answer")
|
||||
|
||||
+24
@@ -53,10 +53,18 @@ public class AihrMobileSeedService {
|
||||
return practiceSeedService.pendingReviewRecords(5);
|
||||
}
|
||||
|
||||
public List<RecordResponse> practiceReviews(String supervisorExtPartyId) {
|
||||
return practiceSeedService.pendingReviewRecords(supervisorExtPartyId, 5);
|
||||
}
|
||||
|
||||
public ReviewDetailResponse practiceReview(Long id) {
|
||||
return practiceSeedService.reviewDetail(id);
|
||||
}
|
||||
|
||||
public ReviewDetailResponse practiceReview(Long id, String supervisorExtPartyId) {
|
||||
return practiceSeedService.reviewDetail(id, supervisorExtPartyId);
|
||||
}
|
||||
|
||||
public boolean markReviewed(Long id) {
|
||||
return markReviewed(id, null);
|
||||
}
|
||||
@@ -65,6 +73,10 @@ public class AihrMobileSeedService {
|
||||
return practiceSeedService.markReviewed(id, request);
|
||||
}
|
||||
|
||||
public boolean markReviewed(Long id, ReviewAdviceRequest request, String supervisorExtPartyId) {
|
||||
return practiceSeedService.markReviewed(id, request, supervisorExtPartyId);
|
||||
}
|
||||
|
||||
public CompetencyResponse profile(String extPartyId) {
|
||||
return practiceSeedService.competency(extPartyId);
|
||||
}
|
||||
@@ -77,14 +89,26 @@ public class AihrMobileSeedService {
|
||||
return practiceSeedService.alerts();
|
||||
}
|
||||
|
||||
public List<PracticeAlertResponse> practiceAlerts(String supervisorExtPartyId) {
|
||||
return practiceSeedService.alerts(supervisorExtPartyId);
|
||||
}
|
||||
|
||||
public List<PracticeAssignmentResponse> practiceAssignments(String extPartyId) {
|
||||
return practiceSeedService.assignments(extPartyId, 10);
|
||||
}
|
||||
|
||||
public List<PracticeAssignmentResponse> practiceAssignments(String extPartyId, String supervisorExtPartyId) {
|
||||
return practiceSeedService.assignments(extPartyId, supervisorExtPartyId, 10);
|
||||
}
|
||||
|
||||
public PracticeAssignmentResponse createPracticeAssignment(PracticeAssignmentRequest request) {
|
||||
return practiceSeedService.createAssignment(request);
|
||||
}
|
||||
|
||||
public PracticeAssignmentResponse createPracticeAssignment(PracticeAssignmentRequest request, String supervisorExtPartyId) {
|
||||
return practiceSeedService.createAssignment(request, supervisorExtPartyId);
|
||||
}
|
||||
|
||||
public DailyDrillAnswerResponse answerDailyDrill(Long id, DailyDrillAnswerRequest request) {
|
||||
return answerDailyDrill(id, request, "");
|
||||
}
|
||||
|
||||
+192
@@ -488,6 +488,29 @@ public class AihrPracticeSeedService {
|
||||
""", this::mapRecord, TENANT_ID, normalizeLimit(limit));
|
||||
}
|
||||
|
||||
public List<RecordResponse> pendingReviewRecords(String supervisorExtPartyId, int limit) {
|
||||
TeamScope scope = teamScope(supervisorExtPartyId);
|
||||
if (!scope.scoped()) {
|
||||
return pendingReviewRecords(limit);
|
||||
}
|
||||
if (scope.extPartyIds().isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
ensurePracticeTable();
|
||||
List<Object> args = new ArrayList<>();
|
||||
args.add(TENANT_ID);
|
||||
args.addAll(scope.extPartyIds());
|
||||
args.add(normalizeLimit(limit));
|
||||
return jdbcTemplate.query("""
|
||||
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 = '待复盘'
|
||||
AND ext_party_id IN (%s)
|
||||
ORDER BY CASE WHEN total_score < 80 THEN 0 ELSE 1 END, total_score ASC, finished_time DESC, id DESC
|
||||
LIMIT ?
|
||||
""".formatted(inClause(scope.extPartyIds().size())), this::mapRecord, args.toArray());
|
||||
}
|
||||
|
||||
public String exportPilotCsv() {
|
||||
ensurePracticeTable();
|
||||
ensureAssignmentTable();
|
||||
@@ -564,6 +587,11 @@ public class AihrPracticeSeedService {
|
||||
return rows.isEmpty() ? null : rows.get(0);
|
||||
}
|
||||
|
||||
public ReviewDetailResponse reviewDetail(Long id, String supervisorExtPartyId) {
|
||||
ReviewDetailResponse detail = reviewDetail(id);
|
||||
return detail == null || !inTeamScope(supervisorExtPartyId, reviewExtPartyId(id)) ? null : detail;
|
||||
}
|
||||
|
||||
public boolean markReviewed(Long id) {
|
||||
return markReviewed(id, null);
|
||||
}
|
||||
@@ -604,6 +632,13 @@ public class AihrPracticeSeedService {
|
||||
return updated > 0;
|
||||
}
|
||||
|
||||
public boolean markReviewed(Long id, ReviewAdviceRequest request, String supervisorExtPartyId) {
|
||||
if (reviewDetail(id, supervisorExtPartyId) == null) {
|
||||
return false;
|
||||
}
|
||||
return markReviewed(id, request);
|
||||
}
|
||||
|
||||
public List<AnnotationResponse> saveAnnotations(String sessionId, AnnotationSaveRequest request) {
|
||||
ensurePracticeTable();
|
||||
if (isBlank(sessionId)) {
|
||||
@@ -820,6 +855,27 @@ public class AihrPracticeSeedService {
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<PracticeAlertResponse> alerts(String supervisorExtPartyId) {
|
||||
TeamScope scope = teamScope(supervisorExtPartyId);
|
||||
if (!scope.scoped()) {
|
||||
return alerts();
|
||||
}
|
||||
ensurePracticeTable();
|
||||
int pending = countPendingReview(scope);
|
||||
int lowScore = countLowScore(scope);
|
||||
List<PracticeAlertResponse> result = new ArrayList<>();
|
||||
if (pending > 0) {
|
||||
result.add(new PracticeAlertResponse("pending_review", "待复盘积压", pending + " 条对练等待主管复盘", "medium", ""));
|
||||
}
|
||||
if (lowScore > 0) {
|
||||
result.add(new PracticeAlertResponse("low_score", "低分训练预警", lowScore + " 条训练低于 80 分,建议派发定向再练", "high", ""));
|
||||
}
|
||||
if (result.isEmpty()) {
|
||||
result.add(new PracticeAlertResponse("stable", "暂无高风险预警", "团队训练记录稳定,可继续按今日安排推进", "low", ""));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<PracticeAssignmentResponse> assignments(String extPartyId, int limit) {
|
||||
ensureAssignmentTable();
|
||||
String party = isBlank(extPartyId) ? "" : extPartyId.trim();
|
||||
@@ -840,6 +896,39 @@ public class AihrPracticeSeedService {
|
||||
""", this::mapAssignment, TENANT_ID, party, party, normalizeLimit(limit));
|
||||
}
|
||||
|
||||
public List<PracticeAssignmentResponse> assignments(String extPartyId, String supervisorExtPartyId, int limit) {
|
||||
String party = isBlank(extPartyId) ? "" : extPartyId.trim();
|
||||
if (!isBlank(party)) {
|
||||
return inTeamScope(supervisorExtPartyId, party) ? assignments(party, limit) : List.of();
|
||||
}
|
||||
TeamScope scope = teamScope(supervisorExtPartyId);
|
||||
if (!scope.scoped()) {
|
||||
return assignments("", limit);
|
||||
}
|
||||
if (scope.extPartyIds().isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
ensureAssignmentTable();
|
||||
List<Object> args = new ArrayList<>();
|
||||
args.add(TENANT_ID);
|
||||
args.addAll(scope.extPartyIds());
|
||||
args.add(normalizeLimit(limit));
|
||||
return 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 IN (%s)
|
||||
AND (status = '待训练' OR (source = 'daily' AND DATE(create_time) = CURRENT_DATE()))
|
||||
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 ?
|
||||
""".formatted(inClause(scope.extPartyIds().size())), this::mapAssignment, args.toArray());
|
||||
}
|
||||
|
||||
private void ensureDailyDrills(String extPartyId) {
|
||||
if (isBlank(extPartyId) || countMobileRecords(extPartyId) >= 3) {
|
||||
return;
|
||||
@@ -904,6 +993,14 @@ public class AihrPracticeSeedService {
|
||||
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);
|
||||
}
|
||||
|
||||
public PracticeAssignmentResponse createAssignment(PracticeAssignmentRequest request, String supervisorExtPartyId) {
|
||||
String extPartyId = request == null ? "" : firstNonBlank(request.extPartyId(), "");
|
||||
if (!inTeamScope(supervisorExtPartyId, extPartyId)) {
|
||||
throw new ServiceException("只能派发给当前项目范围内员工");
|
||||
}
|
||||
return createAssignment(request);
|
||||
}
|
||||
|
||||
private void markAssignmentCompleted(Long requestAssignmentId, ActiveSession activeSession) {
|
||||
Long assignmentId = requestAssignmentId == null && activeSession != null ? activeSession.assignmentId() : requestAssignmentId;
|
||||
if (assignmentId == null || activeSession == null || isBlank(activeSession.extPartyId())) {
|
||||
@@ -1208,6 +1305,88 @@ public class AihrPracticeSeedService {
|
||||
""", TENANT_ID, extPartyId.trim());
|
||||
}
|
||||
|
||||
private int countPendingReview(TeamScope scope) {
|
||||
if (scope.extPartyIds().isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
List<Object> args = new ArrayList<>();
|
||||
args.add(TENANT_ID);
|
||||
args.addAll(scope.extPartyIds());
|
||||
return count("""
|
||||
SELECT COUNT(*)
|
||||
FROM aihr_practice_session
|
||||
WHERE tenant_id = ? AND mode = 'mobile' AND status = '待复盘'
|
||||
AND ext_party_id IN (%s)
|
||||
""".formatted(inClause(scope.extPartyIds().size())), args.toArray());
|
||||
}
|
||||
|
||||
private int countLowScore(TeamScope scope) {
|
||||
if (scope.extPartyIds().isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
List<Object> args = new ArrayList<>();
|
||||
args.add(TENANT_ID);
|
||||
args.addAll(scope.extPartyIds());
|
||||
return count("""
|
||||
SELECT COUNT(*)
|
||||
FROM aihr_practice_session
|
||||
WHERE tenant_id = ? AND mode = 'mobile' AND total_score < 80
|
||||
AND ext_party_id IN (%s)
|
||||
""".formatted(inClause(scope.extPartyIds().size())), args.toArray());
|
||||
}
|
||||
|
||||
private boolean inTeamScope(String supervisorExtPartyId, String extPartyId) {
|
||||
if (isBlank(extPartyId)) {
|
||||
return false;
|
||||
}
|
||||
TeamScope scope = teamScope(supervisorExtPartyId);
|
||||
return !scope.scoped() || scope.extPartyIds().contains(extPartyId.trim());
|
||||
}
|
||||
|
||||
private TeamScope teamScope(String supervisorExtPartyId) {
|
||||
String owner = isBlank(supervisorExtPartyId) ? "" : supervisorExtPartyId.trim();
|
||||
if (owner.isEmpty() || !orgSnapshotTableExists()) {
|
||||
return TeamScope.unscoped();
|
||||
}
|
||||
List<OrgScopeRow> owners = jdbcTemplate.query("""
|
||||
SELECT project_code, position_level
|
||||
FROM aihr_org_snapshot
|
||||
WHERE tenant_id = ? AND ext_party_id = ? AND employment_status = 'active'
|
||||
LIMIT 1
|
||||
""", (rs, rowNum) -> new OrgScopeRow(rs.getString("project_code"), rs.getString("position_level")), TENANT_ID, owner);
|
||||
if (owners.isEmpty()) {
|
||||
return TeamScope.unscoped();
|
||||
}
|
||||
OrgScopeRow row = owners.get(0);
|
||||
if (!canSeeProject(row.positionLevel())) {
|
||||
return TeamScope.scoped(List.of(owner));
|
||||
}
|
||||
List<String> ids = jdbcTemplate.query("""
|
||||
SELECT ext_party_id
|
||||
FROM aihr_org_snapshot
|
||||
WHERE tenant_id = ? AND project_code = ? AND employment_status = 'active'
|
||||
ORDER BY position_level, person_name, ext_party_id
|
||||
""", (rs, rowNum) -> rs.getString("ext_party_id"), TENANT_ID, row.projectCode());
|
||||
return TeamScope.scoped(ids);
|
||||
}
|
||||
|
||||
private boolean orgSnapshotTableExists() {
|
||||
Integer count = jdbcTemplate.queryForObject("""
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'aihr_org_snapshot'
|
||||
""", Integer.class);
|
||||
return count != null && count > 0;
|
||||
}
|
||||
|
||||
private static boolean canSeeProject(String positionLevel) {
|
||||
return "主管".equals(positionLevel) || "项目经理".equals(positionLevel);
|
||||
}
|
||||
|
||||
private static String inClause(int size) {
|
||||
return String.join(",", java.util.Collections.nCopies(size, "?"));
|
||||
}
|
||||
|
||||
private int count(String sql, Object... args) {
|
||||
Integer value = jdbcTemplate.queryForObject(sql, Integer.class, args);
|
||||
return value == null ? 0 : value;
|
||||
@@ -2567,6 +2746,19 @@ public class AihrPracticeSeedService {
|
||||
private record TurnEvidence(Integer emotion, Integer trust, Boolean redFlag, String coachHint) {
|
||||
}
|
||||
|
||||
private record OrgScopeRow(String projectCode, String positionLevel) {
|
||||
}
|
||||
|
||||
private record TeamScope(boolean scoped, List<String> extPartyIds) {
|
||||
static TeamScope unscoped() {
|
||||
return new TeamScope(false, List.of());
|
||||
}
|
||||
|
||||
static TeamScope scoped(List<String> extPartyIds) {
|
||||
return new TeamScope(true, extPartyIds);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
+49
@@ -4,9 +4,23 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PracticeAssignmentRequest;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.contains;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class AihrPracticeSeedServiceTest {
|
||||
|
||||
@@ -24,4 +38,39 @@ public class AihrPracticeSeedServiceTest {
|
||||
)));
|
||||
assertEquals("缺少员工ID,无法派发专项训练", error.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createAssignmentRejectsOutOfProjectAssignee() {
|
||||
JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class);
|
||||
when(jdbcTemplate.queryForObject(ArgumentMatchers.anyString(), eq(Integer.class))).thenReturn(1);
|
||||
when(jdbcTemplate.query(contains("SELECT project_code"), any(RowMapper.class), eq("000000"), eq("SUP-1")))
|
||||
.thenAnswer(invocation -> mapOne(invocation, Map.of(
|
||||
"project_code", "P1",
|
||||
"position_level", "主管"
|
||||
)));
|
||||
when(jdbcTemplate.query(contains("SELECT ext_party_id"), any(RowMapper.class), eq("000000"), eq("P1")))
|
||||
.thenAnswer(invocation -> mapOne(invocation, Map.of("ext_party_id", "EMP-1")));
|
||||
|
||||
AihrPracticeSeedService service = new AihrPracticeSeedService(new ObjectMapper(), jdbcTemplate, null, null);
|
||||
|
||||
ServiceException error = assertThrows(ServiceException.class, () -> service.createAssignment(new PracticeAssignmentRequest(
|
||||
"EMP-2",
|
||||
"越权员工",
|
||||
"fee-parking",
|
||||
null,
|
||||
"manual",
|
||||
"主管派发专项训练"
|
||||
), "SUP-1"));
|
||||
assertEquals("只能派发给当前项目范围内员工", error.getMessage());
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
private static <T> List<T> mapOne(InvocationOnMock invocation, Map<String, String> values) throws SQLException {
|
||||
RowMapper mapper = invocation.getArgument(1);
|
||||
ResultSet resultSet = mock(ResultSet.class);
|
||||
for (Map.Entry<String, String> entry : values.entrySet()) {
|
||||
when(resultSet.getString(entry.getKey())).thenReturn(entry.getValue());
|
||||
}
|
||||
return List.of((T) mapper.mapRow(resultSet, 0));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user