feat(aihr): filter supervisor reviews by org scope

This commit is contained in:
2026-07-09 12:43:16 +08:00
parent 45d06f56cd
commit 9bd567a72f
8 changed files with 281 additions and 40 deletions
@@ -53,8 +53,10 @@ public class AihrMobileController {
}
@GetMapping("/practice/reviews")
public R<List<RecordResponse>> practiceReviews() {
return R.ok(mobileSeedService.practiceReviews(currentAppUsername()));
public R<List<RecordResponse>> practiceReviews(@RequestParam(required = false) String projectCode,
@RequestParam(required = false) String extPartyId,
@RequestParam(required = false) Integer limit) {
return R.ok(mobileSeedService.practiceReviews(currentAppUsername(), projectCode, extPartyId, limit));
}
@GetMapping("/practice/reviews/{id}")
@@ -57,6 +57,10 @@ public class AihrMobileSeedService {
return practiceSeedService.pendingReviewRecords(supervisorExtPartyId, 5);
}
public List<RecordResponse> practiceReviews(String supervisorExtPartyId, String projectCode, String extPartyId, Integer limit) {
return practiceSeedService.pendingReviewRecords(supervisorExtPartyId, projectCode, extPartyId, limit == null ? 20 : limit);
}
public ReviewDetailResponse practiceReview(Long id) {
return practiceSeedService.reviewDetail(id);
}
@@ -478,37 +478,55 @@ 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, ext_party_id
FROM aihr_practice_session
WHERE tenant_id = ? AND mode = 'mobile' AND status = '待复盘'
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));
return pendingReviewRecords("", "", "", limit);
}
public List<RecordResponse> pendingReviewRecords(String supervisorExtPartyId, int limit) {
return pendingReviewRecords(supervisorExtPartyId, "", "", limit);
}
public List<RecordResponse> pendingReviewRecords(String supervisorExtPartyId, String projectCode, String extPartyId, int limit) {
TeamScope scope = teamScope(supervisorExtPartyId);
if (!scope.scoped()) {
return pendingReviewRecords(limit);
if (scope.scoped() && scope.extPartyIds().isEmpty()) {
return List.of();
}
if (scope.extPartyIds().isEmpty()) {
String project = isBlank(projectCode) ? "" : projectCode.trim();
if (!project.isEmpty() && !orgSnapshotTableExists()) {
return List.of();
}
ensurePracticeTable();
List<Object> args = new ArrayList<>();
args.add(TENANT_ID);
args.addAll(scope.extPartyIds());
StringBuilder where = new StringBuilder("WHERE tenant_id = ? AND mode = 'mobile' AND status = '待复盘'");
String party = isBlank(extPartyId) ? "" : extPartyId.trim();
if (!party.isEmpty()) {
where.append(" AND ext_party_id = ?");
args.add(party);
}
if (!project.isEmpty()) {
where.append("""
AND ext_party_id IN (
SELECT ext_party_id
FROM aihr_org_snapshot
WHERE tenant_id = ? AND project_code = ? AND employment_status = 'active'
)
""");
args.add(TENANT_ID);
args.add(project);
}
if (scope.scoped()) {
where.append(" AND ext_party_id IN (").append(inClause(scope.extPartyIds().size())).append(")");
args.addAll(scope.extPartyIds());
}
args.add(normalizeLimit(limit));
return jdbcTemplate.query("""
String sql = """
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)
""" + where + """
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());
""";
return jdbcTemplate.query(sql, this::mapRecord, args.toArray());
}
public String exportPilotCsv() {
@@ -3,6 +3,7 @@ package org.dromara.aihr.service;
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.Tag;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatchers;
import org.mockito.invocation.InvocationOnMock;
@@ -11,17 +12,20 @@ import org.springframework.jdbc.core.RowMapper;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
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.junit.jupiter.api.Assertions.assertTrue;
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;
@Tag("dev")
public class AihrPracticeSeedServiceTest {
@Test
@@ -64,13 +68,101 @@ public class AihrPracticeSeedServiceTest {
assertEquals("只能派发给当前项目范围内员工", error.getMessage());
}
@Test
public void pendingReviewRecordsAppliesProjectAndAssigneeFilters() {
PracticeReviewJdbcTemplate jdbcTemplate = new PracticeReviewJdbcTemplate("主管", List.of("SUP-1", "EMP-1"));
AihrPracticeSeedService service = new AihrPracticeSeedService(new ObjectMapper(), jdbcTemplate, null, null);
service.pendingReviewRecords("SUP-1", "P1", "EMP-1", 50);
assertTrue(jdbcTemplate.reviewSql.contains("status = '待复盘'"));
assertTrue(jdbcTemplate.reviewSql.contains("ext_party_id = ?"));
assertTrue(jdbcTemplate.reviewSql.contains("project_code = ?"));
assertTrue(jdbcTemplate.reviewSql.contains("ext_party_id IN (?,?)"));
assertEquals(List.of("000000", "EMP-1", "000000", "P1", "SUP-1", "EMP-1", 20), jdbcTemplate.reviewArgs);
}
@Test
public void pendingReviewRecordsLimitsNonSupervisorToSelf() {
PracticeReviewJdbcTemplate jdbcTemplate = new PracticeReviewJdbcTemplate("员工", List.of("SUP-1", "EMP-1"));
AihrPracticeSeedService service = new AihrPracticeSeedService(new ObjectMapper(), jdbcTemplate, null, null);
service.pendingReviewRecords("SUP-1", "", "", 0);
assertTrue(jdbcTemplate.reviewSql.contains("ext_party_id IN (?)"));
assertEquals(List.of("000000", "SUP-1", 5), jdbcTemplate.reviewArgs);
}
@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));
return mapRows(mapper, List.of(values));
}
@SuppressWarnings({"unchecked", "rawtypes"})
private static <T> List<T> mapRows(RowMapper mapper, List<Map<String, String>> rows) throws SQLException {
List<T> mapped = new ArrayList<>();
int index = 0;
for (Map<String, String> values : rows) {
ResultSet resultSet = mock(ResultSet.class);
for (Map.Entry<String, String> entry : values.entrySet()) {
when(resultSet.getString(entry.getKey())).thenReturn(entry.getValue());
}
mapped.add((T) mapper.mapRow(resultSet, index++));
}
return mapped;
}
private static final class PracticeReviewJdbcTemplate extends JdbcTemplate {
private final String ownerLevel;
private final List<String> scopeIds;
private String reviewSql = "";
private List<Object> reviewArgs = List.of();
private PracticeReviewJdbcTemplate(String ownerLevel, List<String> scopeIds) {
this.ownerLevel = ownerLevel;
this.scopeIds = scopeIds;
}
@Override
public <T> T queryForObject(String sql, Class<T> requiredType) {
return requiredType.cast(1);
}
@Override
public <T> T queryForObject(String sql, Class<T> requiredType, Object... args) {
return requiredType.cast(1);
}
@Override
public void execute(String sql) {
// Table creation is irrelevant for query assembly tests.
}
@Override
public <T> List<T> query(String sql, RowMapper<T> rowMapper, Object... args) {
try {
if (sql.contains("FROM aihr_practice_session")) {
reviewSql = sql;
reviewArgs = List.of(args);
return List.of();
}
if (sql.contains("SELECT project_code") && sql.contains("LIMIT 1")) {
return mapRows(rowMapper, List.of(Map.of(
"project_code", "P1",
"position_level", ownerLevel
)));
}
if (sql.contains("SELECT ext_party_id") && sql.contains("ORDER BY position_level")) {
return mapRows(rowMapper, scopeIds.stream()
.map(id -> Map.of("ext_party_id", id))
.toList());
}
return List.of();
} catch (SQLException e) {
throw new IllegalStateException(e);
}
}
}
}