fix(aihr): scope mobile home metrics by identity

This commit is contained in:
2026-07-14 10:23:56 +08:00
parent b2d95dcff7
commit 2cae83ff1b
9 changed files with 254 additions and 19 deletions
@@ -63,7 +63,17 @@ public class AihrMobileController {
@SaIgnore
@GetMapping("/home/{role}")
public R<HomeResponse> home(@PathVariable String role) {
return R.ok(mobileSeedService.home(role));
if (!LoginHelper.isLogin()) {
return R.ok(mobileSeedService.publicHome(role));
}
LoginUser loginUser = LoginHelper.getLoginUser();
String identity = currentAppUsername();
if (loginUser != null
&& UserType.APP_USER.getUserType().equals(loginUser.getUserType())
&& supervisorRoleRequested(role)) {
mobileSeedService.requireSupervisorIdentity(identity);
}
return R.ok(mobileSeedService.home(role, identity));
}
/**
@@ -226,6 +236,16 @@ public class AihrMobileController {
return username == null ? "" : username.trim();
}
private static boolean supervisorRoleRequested(String role) {
if (role == null) {
return false;
}
return switch (role.toLowerCase()) {
case "supervisor", "manager", "admin" -> true;
default -> false;
};
}
private static Map<String, Object> reviewDetailPayload(ReviewDetailResponse detail) {
Map<String, Object> data = new LinkedHashMap<>();
data.put("id", detail.id());
@@ -41,13 +41,43 @@ public class AihrMobileSeedService {
}
public HomeResponse home(String role) {
return home(role, "");
}
public HomeResponse home(String role, String mobileIdentity) {
return switch (normalize(role)) {
case "candidate" -> candidateHome();
case "supervisor" -> supervisorHome();
default -> userHome();
case "supervisor" -> supervisorHomeForIdentity(mobileIdentity);
default -> userHome(mobileIdentity);
};
}
/**
* Return a public first-screen seed without reading tenant business metrics.
* The authenticated home path remains available through {@link #home(String)}.
*/
public HomeResponse publicHome(String role) {
return switch (normalize(role)) {
case "candidate" -> candidateHome();
case "supervisor" -> supervisorHome(false, "");
default -> workerHome("user", "员工端", "avatar-woman", "早上好,王敏", "星河湾一期 · 客服管家", false, "");
};
}
private HomeResponse supervisorHomeForIdentity(String supervisorIdentity) {
if (supervisorIdentity == null || supervisorIdentity.isBlank()) {
return supervisorHome(true, new AihrPracticeSeedService.MobileTeamMetrics(
practiceSeedService.mobileCompletedPeopleCount(),
practiceSeedService.mobilePilotPeopleCount(),
practiceSeedService.mobilePendingReviewCount(),
practiceSeedService.mobilePendingAssignmentCount(),
practiceSeedService.mobileLowScoreCount(),
practiceSeedService.mobileAverageScore()
));
}
return supervisorHome(true, practiceSeedService.mobileTeamMetrics(supervisorIdentity));
}
public List<RecordResponse> practiceHistory(String extPartyId) {
return practiceSeedService.mobileHistory(extPartyId, 5);
}
@@ -159,8 +189,8 @@ public class AihrMobileSeedService {
};
}
private HomeResponse userHome() {
return workerHome("user", "员工端", "avatar-woman", "早上好,王敏", "星河湾一期 · 客服管家");
private HomeResponse userHome(String mobileIdentity) {
return workerHome("user", "员工端", "avatar-woman", "早上好,王敏", "星河湾一期 · 客服管家", true, mobileIdentity);
}
private static HomeResponse candidateHome() {
@@ -208,10 +238,18 @@ public class AihrMobileSeedService {
);
}
private HomeResponse workerHome(String role, String roleLabel, String avatarTone, String greeting, String location) {
int completed = practiceSeedService.mobileCompletedCount();
int pendingReview = practiceSeedService.mobilePendingReviewCount();
int averageScore = practiceSeedService.mobileAverageScore();
private HomeResponse workerHome(String role, String roleLabel, String avatarTone, String greeting, String location,
boolean includeLiveMetrics, String mobileIdentity) {
boolean scoped = mobileIdentity != null && !mobileIdentity.isBlank();
int completed = includeLiveMetrics
? scoped ? practiceSeedService.mobileCompletedCount(mobileIdentity) : practiceSeedService.mobileCompletedCount()
: 0;
int pendingReview = includeLiveMetrics
? scoped ? practiceSeedService.mobilePendingReviewCount(mobileIdentity) : practiceSeedService.mobilePendingReviewCount()
: 0;
int averageScore = includeLiveMetrics
? scoped ? practiceSeedService.mobileAverageScore(mobileIdentity) : practiceSeedService.mobileAverageScore()
: 0;
return new HomeResponse(
role,
roleLabel,
@@ -256,14 +294,21 @@ public class AihrMobileSeedService {
);
}
private HomeResponse supervisorHome() {
int completed = practiceSeedService.mobileCompletedPeopleCount();
int total = practiceSeedService.mobilePilotPeopleCount();
private HomeResponse supervisorHome(boolean includeLiveMetrics, String supervisorIdentity) {
AihrPracticeSeedService.MobileTeamMetrics metrics = includeLiveMetrics
? practiceSeedService.mobileTeamMetrics(supervisorIdentity)
: AihrPracticeSeedService.MobileTeamMetrics.zero();
return supervisorHome(includeLiveMetrics, metrics);
}
private HomeResponse supervisorHome(boolean includeLiveMetrics, AihrPracticeSeedService.MobileTeamMetrics metrics) {
int completed = metrics.completedPeople();
int total = metrics.pilotPeople();
int pending = Math.max(0, total - completed);
int pendingReview = practiceSeedService.mobilePendingReviewCount();
int pendingAssignment = practiceSeedService.mobilePendingAssignmentCount();
int lowScore = practiceSeedService.mobileLowScoreCount();
int averageScore = practiceSeedService.mobileAverageScore();
int pendingReview = metrics.pendingReview();
int pendingAssignment = metrics.pendingAssignment();
int lowScore = metrics.lowScore();
int averageScore = metrics.averageScore();
int completionRate = total == 0 ? 0 : Math.round(completed * 100F / total);
return new HomeResponse(
"supervisor",
@@ -694,10 +694,18 @@ public class AihrPracticeSeedService {
return countMobileRecords("");
}
public int mobileCompletedCount(String extPartyId) {
return countMobileRecords(extPartyId);
}
public int mobilePendingReviewCount() {
return countPendingReview("");
}
public int mobilePendingReviewCount(String extPartyId) {
return countPendingReview(extPartyId);
}
public int mobileCompletedPeopleCount() {
return countCompletedPilotPeople();
}
@@ -735,6 +743,59 @@ public class AihrPracticeSeedService {
return value == null ? 0 : Math.round(value.floatValue());
}
public int mobileAverageScore(String extPartyId) {
if (isBlank(extPartyId)) {
return mobileAverageScore();
}
ensurePracticeTable();
Double value = jdbcTemplate.queryForObject("""
SELECT COALESCE(AVG(total_score), 0)
FROM aihr_practice_session
WHERE tenant_id = ? AND mode = 'mobile' AND ext_party_id = ?
""", Double.class, tenantId(), extPartyId.trim());
return value == null ? 0 : Math.round(value.floatValue());
}
public MobileTeamMetrics mobileTeamMetrics(String supervisorExtPartyId) {
TeamScope scope = teamScope(supervisorExtPartyId);
if (!scope.scoped()) {
return new MobileTeamMetrics(
mobileCompletedPeopleCount(),
mobilePilotPeopleCount(),
mobilePendingReviewCount(),
mobilePendingAssignmentCount(),
mobileLowScoreCount(),
mobileAverageScore()
);
}
if (scope.extPartyIds().isEmpty()) {
return MobileTeamMetrics.zero();
}
ensurePracticeTable();
ensureAssignmentTable();
return new MobileTeamMetrics(
countCompletedPilotPeople(scope),
countPilotPeople(scope),
countPendingReview(scope),
countPendingAssignment(scope),
countLowScore(scope),
averageScore(scope)
);
}
public record MobileTeamMetrics(
int completedPeople,
int pilotPeople,
int pendingReview,
int pendingAssignment,
int lowScore,
int averageScore
) {
public static MobileTeamMetrics zero() {
return new MobileTeamMetrics(0, 0, 0, 0, 0, 0);
}
}
public List<RecordResponse> mobileHistory(String extPartyId, int limit) {
ensurePracticeTable();
int safeLimit = normalizeLimit(limit);
@@ -1197,6 +1258,23 @@ public class AihrPracticeSeedService {
""", tenantId());
}
private int countCompletedPilotPeople(TeamScope scope) {
List<Object> args = new ArrayList<>();
args.add(tenantId());
args.addAll(scope.extPartyIds());
return count("""
SELECT COUNT(*)
FROM (
SELECT ext_party_id
FROM aihr_practice_session
WHERE tenant_id = ? AND mode = 'mobile'
AND ext_party_id IN (%s)
GROUP BY ext_party_id
HAVING COUNT(*) >= 10
) completed_people
""".formatted(inClause(scope.extPartyIds().size())), args.toArray());
}
private int countPilotPeople() {
ensurePracticeTable();
ensureAssignmentTable();
@@ -1216,6 +1294,29 @@ public class AihrPracticeSeedService {
""", tenantId(), tenantId());
}
private int countPilotPeople(TeamScope scope) {
List<Object> args = new ArrayList<>();
args.add(tenantId());
args.addAll(scope.extPartyIds());
args.add(tenantId());
args.addAll(scope.extPartyIds());
return count("""
SELECT COUNT(*)
FROM (
SELECT ext_party_id
FROM aihr_practice_session
WHERE tenant_id = ? AND mode = 'mobile'
AND ext_party_id IN (%s)
UNION
SELECT ext_party_id
FROM aihr_practice_assignment
WHERE tenant_id = ? AND ext_party_id IN (%s)
AND status = '待训练'
AND (source <> 'daily' OR DATE(create_time) = CURRENT_DATE())
) pilot_people
""".formatted(inClause(scope.extPartyIds().size()), inClause(scope.extPartyIds().size())), args.toArray());
}
public ReviewDetailResponse reviewDetail(Long id) {
ensurePracticeTable();
if (id == null) {
@@ -2072,6 +2173,22 @@ public class AihrPracticeSeedService {
""".formatted(inClause(scope.extPartyIds().size())), args.toArray());
}
private int countPendingAssignment(TeamScope scope) {
if (scope.extPartyIds().isEmpty()) {
return 0;
}
List<Object> args = new ArrayList<>();
args.add(tenantId());
args.addAll(scope.extPartyIds());
return count("""
SELECT COUNT(*)
FROM aihr_practice_assignment
WHERE tenant_id = ? AND status = '待训练'
AND (source <> 'daily' OR DATE(create_time) = CURRENT_DATE())
AND ext_party_id IN (%s)
""".formatted(inClause(scope.extPartyIds().size())), args.toArray());
}
private int countLowScore(TeamScope scope) {
if (scope.extPartyIds().isEmpty()) {
return 0;
@@ -2087,6 +2204,22 @@ public class AihrPracticeSeedService {
""".formatted(inClause(scope.extPartyIds().size())), args.toArray());
}
private int averageScore(TeamScope scope) {
if (scope.extPartyIds().isEmpty()) {
return 0;
}
List<Object> args = new ArrayList<>();
args.add(tenantId());
args.addAll(scope.extPartyIds());
Double value = jdbcTemplate.queryForObject("""
SELECT COALESCE(AVG(total_score), 0)
FROM aihr_practice_session
WHERE tenant_id = ? AND mode = 'mobile'
AND ext_party_id IN (%s)
""".formatted(inClause(scope.extPartyIds().size())), Double.class, args.toArray());
return value == null ? 0 : Math.round(value.floatValue());
}
private boolean inTeamScope(String supervisorExtPartyId, String extPartyId) {
if (isBlank(extPartyId)) {
return false;
@@ -8,6 +8,7 @@ import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@Tag("dev")
@@ -38,5 +39,28 @@ public class AihrMobileSeedServiceTest {
assertEquals(3, supervisor.todos().get(0).count());
assertEquals(2, supervisor.todos().get(1).count());
assertEquals("1", supervisor.team().stats().get(2).value());
AihrPracticeSeedService publicPractice = mock(AihrPracticeSeedService.class);
AihrMobileSeedService publicService = new AihrMobileSeedService(publicPractice);
HomeResponse publicUser = publicService.publicHome("user");
HomeResponse publicSupervisor = publicService.publicHome("supervisor");
assertEquals("0", publicUser.metrics().get(0).value());
assertEquals("0人已完成 · 0人待跟进", publicSupervisor.team().progressText());
assertEquals(0, publicSupervisor.todos().get(0).count());
verifyNoInteractions(publicPractice);
when(practice.mobileCompletedCount("employee-1")).thenReturn(2);
when(practice.mobilePendingReviewCount("employee-1")).thenReturn(1);
when(practice.mobileAverageScore("employee-1")).thenReturn(88);
when(practice.mobileTeamMetrics("supervisor-1"))
.thenReturn(new AihrPracticeSeedService.MobileTeamMetrics(3, 4, 2, 1, 1, 79));
HomeResponse scopedUser = service.home("user", "employee-1");
HomeResponse scopedSupervisor = service.home("supervisor", "supervisor-1");
assertEquals("2", scopedUser.metrics().get(0).value());
assertEquals("88", scopedUser.metrics().get(1).value());
assertEquals("1", scopedUser.metrics().get(2).value());
assertEquals("3人已完成 · 1人待跟进", scopedSupervisor.team().progressText());
assertEquals(2, scopedSupervisor.todos().get(0).count());
assertEquals(1, scopedSupervisor.todos().get(1).count());
}
}
@@ -401,6 +401,11 @@ public class AihrPracticeSeedServiceTest {
assertTrue(controllerSource.contains("mobileSeedService.requireSupervisorIdentity(currentAppUsername())"));
assertTrue(controllerSource.contains("@SaCheckLogin\npublic class AihrMobileController"));
assertTrue(controllerSource.contains("@SaIgnore\n @GetMapping(\"/home/{role}\")"));
assertTrue(controllerSource.contains("if (!LoginHelper.isLogin())"));
assertTrue(controllerSource.contains("return R.ok(mobileSeedService.publicHome(role))"));
assertTrue(controllerSource.contains("return R.ok(mobileSeedService.home(role, identity))"));
assertTrue(controllerSource.contains("mobileSeedService.requireSupervisorIdentity(identity)"));
assertTrue(controllerSource.contains("private static boolean supervisorRoleRequested(String role)"));
assertTrue(controllerSource.contains("UserType.SYS_USER.getUserType().equals(loginUser.getUserType())"));
assertTrue(controllerSource.contains("StpUtil.hasRoleOr(TenantConstants.SUPER_ADMIN_ROLE_KEY, \"hr_operator\")"));
assertFalse(controllerSource.contains("private static String supervisorScopeExtPartyId()"));