fix(aihr): separate onboarding and candidate review states
This commit is contained in:
+12
@@ -14,7 +14,9 @@ import org.dromara.aihr.domain.AihrPracticeDto.PracticeAlertResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PracticeAssignmentRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PracticeAssignmentResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PracticeTeamResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.OnboardTaskResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PromotionEvidenceResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.QualificationResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.RecordResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.MistakeResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.ReviewAdviceRequest;
|
||||
@@ -198,6 +200,16 @@ public class AihrMobileController {
|
||||
return R.ok(mobileSeedService.practiceAssignments(scopedAssignmentExtPartyId(extPartyId), currentAppUsername()));
|
||||
}
|
||||
|
||||
@GetMapping("/onboard/tasks")
|
||||
public R<List<OnboardTaskResponse>> onboardTasks() {
|
||||
return R.ok(mobileSeedService.onboardTasks(ownMobileExtPartyId(null)));
|
||||
}
|
||||
|
||||
@GetMapping("/qualification")
|
||||
public R<QualificationResponse> qualification() {
|
||||
return R.ok(mobileSeedService.qualification(ownMobileExtPartyId(null)));
|
||||
}
|
||||
|
||||
@PostMapping("/practice/assignments")
|
||||
public R<PracticeAssignmentResponse> createPracticeAssignment(@RequestBody PracticeAssignmentRequest request) {
|
||||
return R.ok(mobileSeedService.createPracticeAssignment(request, supervisorScopeExtPartyId()));
|
||||
|
||||
+36
@@ -277,6 +277,42 @@ public final class AihrPracticeDto {
|
||||
) {
|
||||
}
|
||||
|
||||
public record OnboardTaskResponse(
|
||||
Long id,
|
||||
String projectCode,
|
||||
String positionCode,
|
||||
Long sopId,
|
||||
Long courseId,
|
||||
String taskType,
|
||||
String status,
|
||||
String assignBy,
|
||||
String assignedTime,
|
||||
String dueTime,
|
||||
String completedTime,
|
||||
String evidenceRef
|
||||
) {
|
||||
}
|
||||
|
||||
public record QualificationGateResponse(
|
||||
Long id,
|
||||
String projectCode,
|
||||
String positionCode,
|
||||
String certId,
|
||||
Boolean passed,
|
||||
String validThru,
|
||||
String evidenceRef,
|
||||
String reviewer,
|
||||
String reviewedTime
|
||||
) {
|
||||
}
|
||||
|
||||
public record QualificationResponse(
|
||||
String status,
|
||||
String positionCode,
|
||||
List<QualificationGateResponse> gates
|
||||
) {
|
||||
}
|
||||
|
||||
public record TeamMemberResponse(String extPartyId, String name) {
|
||||
}
|
||||
|
||||
|
||||
+10
@@ -8,6 +8,8 @@ import org.dromara.aihr.domain.AihrPracticeDto.PracticeAssignmentRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PracticeAssignmentResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PracticeTeamResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PromotionEvidenceResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.OnboardTaskResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.QualificationResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.RecordResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.MistakeResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.ReviewAdviceRequest;
|
||||
@@ -166,6 +168,14 @@ public class AihrMobileSeedService {
|
||||
return practiceSeedService.assignments(extPartyId, supervisorExtPartyId, 10);
|
||||
}
|
||||
|
||||
public List<OnboardTaskResponse> onboardTasks(String extPartyId) {
|
||||
return practiceSeedService.onboardTasks(extPartyId, 20);
|
||||
}
|
||||
|
||||
public QualificationResponse qualification(String extPartyId) {
|
||||
return practiceSeedService.qualification(extPartyId);
|
||||
}
|
||||
|
||||
public PracticeAssignmentResponse createPracticeAssignment(PracticeAssignmentRequest request) {
|
||||
return practiceSeedService.createAssignment(request);
|
||||
}
|
||||
|
||||
+130
@@ -26,6 +26,9 @@ import org.dromara.aihr.domain.AihrPracticeDto.PracticeTeamResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PromptTemplateRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PromptTemplateResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PromotionEvidenceResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.OnboardTaskResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.QualificationGateResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.QualificationResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.RecordResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.MistakeResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.MistakeAggregateResponse;
|
||||
@@ -2036,6 +2039,96 @@ public class AihrPracticeSeedService {
|
||||
""".formatted(nameExpression), this::mapAssignment, tenantId(), party, party, normalizeLimit(limit));
|
||||
}
|
||||
|
||||
/**
|
||||
* Formal onboarding tasks are read-only in phase one. The employee identity is
|
||||
* resolved from the authenticated APP identity to the external organization ID;
|
||||
* no task is created from this endpoint.
|
||||
*/
|
||||
public List<OnboardTaskResponse> onboardTasks(String extPartyId, int limit) {
|
||||
String identity = isBlank(extPartyId) ? "" : extPartyId.trim();
|
||||
if (identity.isEmpty() || !formalPositionTablesExist("aihr_onboard_task")) {
|
||||
return List.of();
|
||||
}
|
||||
boolean hasOrgSnapshot = orgSnapshotTableExists() && orgSnapshotColumnExists("person_phone");
|
||||
String identityPredicate = hasOrgSnapshot
|
||||
? "(t.ext_party_id = ? OR t.ext_party_id IN (SELECT o.ext_party_id FROM aihr_org_snapshot o WHERE o.tenant_id = ? AND o.employment_status = 'active' AND (o.ext_party_id = ? OR o.person_phone = ?)))"
|
||||
: "t.ext_party_id = ?";
|
||||
List<Object> args = new ArrayList<>();
|
||||
args.add(tenantId());
|
||||
args.add(identity);
|
||||
if (hasOrgSnapshot) {
|
||||
args.add(tenantId());
|
||||
args.add(identity);
|
||||
args.add(identity);
|
||||
}
|
||||
args.add(normalizeLimit(limit));
|
||||
return jdbcTemplate.query("""
|
||||
SELECT t.id, t.project_code, t.position_code, t.sop_id, t.course_id, t.task_type,
|
||||
t.status, t.assign_by, t.assigned_time, t.due_time, t.completed_time, t.evidence_ref
|
||||
FROM aihr_onboard_task t
|
||||
WHERE t.tenant_id = ? AND %s
|
||||
ORDER BY CASE t.status WHEN '待完成' THEN 0 WHEN '进行中' THEN 1 WHEN '已完成' THEN 2 ELSE 3 END,
|
||||
COALESCE(t.due_time, t.create_time) ASC, t.id DESC
|
||||
LIMIT ?
|
||||
""".formatted(identityPredicate), (rs, rowNum) -> new OnboardTaskResponse(
|
||||
rs.getLong("id"),
|
||||
rs.getString("project_code"),
|
||||
rs.getString("position_code"),
|
||||
rs.getObject("sop_id") == null ? null : rs.getLong("sop_id"),
|
||||
rs.getObject("course_id") == null ? null : rs.getLong("course_id"),
|
||||
rs.getString("task_type"),
|
||||
rs.getString("status"),
|
||||
rs.getString("assign_by"),
|
||||
formatTimestamp(rs.getTimestamp("assigned_time")),
|
||||
formatTimestamp(rs.getTimestamp("due_time")),
|
||||
formatTimestamp(rs.getTimestamp("completed_time")),
|
||||
rs.getString("evidence_ref")
|
||||
), args.toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return qualification evidence for the current APP user without treating an
|
||||
* empty rules table as a pass. Formal qualification remains NOT_CONFIGURED until
|
||||
* HR supplies a gate row and its evidence.
|
||||
*/
|
||||
public QualificationResponse qualification(String extPartyId) {
|
||||
String identity = isBlank(extPartyId) ? "" : extPartyId.trim();
|
||||
if (identity.isEmpty() || !formalPositionTablesExist("aihr_qualification_gate")) {
|
||||
return new QualificationResponse("NOT_CONFIGURED", "", List.of());
|
||||
}
|
||||
boolean hasOrgSnapshot = orgSnapshotTableExists() && orgSnapshotColumnExists("person_phone");
|
||||
String identityPredicate = hasOrgSnapshot
|
||||
? "(q.ext_party_id = ? OR q.ext_party_id IN (SELECT o.ext_party_id FROM aihr_org_snapshot o WHERE o.tenant_id = ? AND o.employment_status = 'active' AND (o.ext_party_id = ? OR o.person_phone = ?)))"
|
||||
: "q.ext_party_id = ?";
|
||||
List<Object> args = new ArrayList<>();
|
||||
args.add(tenantId());
|
||||
args.add(identity);
|
||||
if (hasOrgSnapshot) {
|
||||
args.add(tenantId());
|
||||
args.add(identity);
|
||||
args.add(identity);
|
||||
}
|
||||
List<QualificationGateResponse> gates = jdbcTemplate.query("""
|
||||
SELECT q.id, q.project_code, q.position_code, q.cert_id, q.passed, q.valid_thru,
|
||||
q.evidence_ref, q.reviewer, q.reviewed_time
|
||||
FROM aihr_qualification_gate q
|
||||
WHERE q.tenant_id = ? AND %s
|
||||
ORDER BY q.position_code, q.cert_id, q.id DESC
|
||||
""".formatted(identityPredicate), (rs, rowNum) -> new QualificationGateResponse(
|
||||
rs.getLong("id"),
|
||||
rs.getString("project_code"),
|
||||
rs.getString("position_code"),
|
||||
rs.getString("cert_id"),
|
||||
rs.getBoolean("passed"),
|
||||
rs.getDate("valid_thru") == null ? null : rs.getDate("valid_thru").toLocalDate().toString(),
|
||||
rs.getString("evidence_ref"),
|
||||
rs.getString("reviewer"),
|
||||
formatTimestamp(rs.getTimestamp("reviewed_time"))
|
||||
), args.toArray());
|
||||
return new QualificationResponse(qualificationStatus(gates),
|
||||
gates.isEmpty() ? "" : firstNonBlank(gates.get(0).positionCode(), ""), gates);
|
||||
}
|
||||
|
||||
public List<PracticeAssignmentResponse> assignments(String extPartyId, String supervisorExtPartyId, int limit) {
|
||||
String party = isBlank(extPartyId) ? "" : extPartyId.trim();
|
||||
String supervisor = isBlank(supervisorExtPartyId) ? "" : supervisorExtPartyId.trim();
|
||||
@@ -3779,6 +3872,43 @@ public class AihrPracticeSeedService {
|
||||
return Math.min(limit, 200);
|
||||
}
|
||||
|
||||
private String formatTimestamp(Timestamp value) {
|
||||
return value == null ? "" : value.toLocalDateTime().format(TIME_FORMATTER);
|
||||
}
|
||||
|
||||
private String qualificationStatus(List<QualificationGateResponse> gates) {
|
||||
if (gates.isEmpty()) {
|
||||
return "NOT_CONFIGURED";
|
||||
}
|
||||
if (gates.stream().anyMatch(gate -> !Boolean.TRUE.equals(gate.passed()))) {
|
||||
return "BLOCKED";
|
||||
}
|
||||
LocalDate today = LocalDate.now();
|
||||
if (gates.stream().anyMatch(gate -> gate.validThru() != null
|
||||
&& !gate.validThru().isBlank()
|
||||
&& LocalDate.parse(gate.validThru()).isBefore(today))) {
|
||||
return "EXPIRED";
|
||||
}
|
||||
return "PASSED";
|
||||
}
|
||||
|
||||
private boolean formalPositionTablesExist(String tableName) {
|
||||
if (!Set.of("aihr_onboard_task", "aihr_qualification_gate").contains(tableName)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Integer count = jdbcTemplate.queryForObject("""
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?
|
||||
""", Integer.class, tableName);
|
||||
return count != null && count > 0;
|
||||
} catch (DataAccessException ex) {
|
||||
log.warn("formal position table check failed for {}, returning empty result", tableName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ensurePracticeTable() {
|
||||
if (practiceTableReady) {
|
||||
return;
|
||||
|
||||
+17
@@ -659,6 +659,23 @@ public class AihrPracticeSeedServiceTest {
|
||||
assertFalse(controllerSource.contains("supervisor H5 keeps team-scope"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void formalOnboardingReadsAreLoginScopedAndReadOnly() throws Exception {
|
||||
Path source = Path.of("src/main/java/org/dromara/aihr/controller/AihrMobileController.java");
|
||||
if (!Files.exists(source)) {
|
||||
source = Path.of("ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java");
|
||||
}
|
||||
String controllerSource = Files.readString(source);
|
||||
|
||||
assertTrue(controllerSource.contains("@SaCheckLogin\npublic class AihrMobileController"));
|
||||
assertTrue(controllerSource.contains("@GetMapping(\"/onboard/tasks\")"));
|
||||
assertTrue(controllerSource.contains("@GetMapping(\"/qualification\")"));
|
||||
assertTrue(controllerSource.contains("mobileSeedService.onboardTasks(ownMobileExtPartyId(null))"));
|
||||
assertTrue(controllerSource.contains("mobileSeedService.qualification(ownMobileExtPartyId(null))"));
|
||||
assertFalse(controllerSource.contains("@PostMapping(\"/onboard/tasks\")"));
|
||||
assertFalse(controllerSource.contains("@PostMapping(\"/qualification\")"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dashboardControllerRequiresHrRole() throws Exception {
|
||||
Path source = Path.of("src/main/java/org/dromara/aihr/controller/AihrDashboardController.java");
|
||||
|
||||
Reference in New Issue
Block a user