diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java index db3910cc..56493b4c 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java @@ -128,7 +128,15 @@ public class AihrMobileController { return ownMobileExtPartyId(requested); } + /** + * APP supervisor tokens are org-scoped; management console sys_user keeps unscoped access + * so admin review/growth pages continue to work against the same mobile APIs. + */ private String supervisorScopeExtPartyId() { + LoginUser loginUser = LoginHelper.getLoginUser(); + if (loginUser != null && UserType.SYS_USER.getUserType().equals(loginUser.getUserType())) { + return ""; + } return mobileSeedService.requireSupervisorIdentity(currentAppUsername()); } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrOrgSyncController.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrOrgSyncController.java index b72772f6..2d9d5689 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrOrgSyncController.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrOrgSyncController.java @@ -1,10 +1,13 @@ package org.dromara.aihr.controller; +import cn.dev33.satoken.annotation.SaCheckRole; +import cn.dev33.satoken.annotation.SaMode; 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.constant.TenantConstants; import org.dromara.common.core.domain.R; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -18,13 +21,17 @@ import org.springframework.web.bind.annotation.RestController; @RequestMapping("/api/aihr/org") public class AihrOrgSyncController { + private static final String HR_OPERATOR_ROLE = "hr_operator"; + private final AihrOrgSyncService orgSyncService; + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @PostMapping("/sync") public R sync(@RequestBody(required = false) SyncRequest request) { return R.ok(orgSyncService.sync(request)); } + @SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR) @GetMapping("/snapshot") public R snapshot(@RequestParam(required = false) String keyword, @RequestParam(required = false) String projectCode, diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java index 3ed66988..d4f9a33c 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java @@ -952,9 +952,11 @@ public class AihrPracticeSeedService { return count(""" SELECT COUNT(*) FROM ( - SELECT DISTINCT ext_party_id + SELECT ext_party_id FROM aihr_practice_session WHERE tenant_id = ? AND mode = 'mobile' + GROUP BY ext_party_id + HAVING COUNT(*) >= 10 ) completed_people """, TENANT_ID); } @@ -1338,7 +1340,12 @@ public class AihrPracticeSeedService { public List assignments(String extPartyId, String supervisorExtPartyId, int limit) { String party = isBlank(extPartyId) ? "" : extPartyId.trim(); + String supervisor = isBlank(supervisorExtPartyId) ? "" : supervisorExtPartyId.trim(); if (!isBlank(party)) { + // Self-read must not depend on org team membership (SMS users may lack org snapshot rows). + if (!supervisor.isEmpty() && supervisor.equals(party)) { + return assignments(party, limit); + } return inTeamScope(supervisorExtPartyId, party) ? assignments(party, limit) : List.of(); } TeamScope scope = teamScope(supervisorExtPartyId); diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java index 498188bb..d980e3f1 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java @@ -354,11 +354,33 @@ public class AihrPracticeSeedServiceTest { assertTrue(controllerSource.contains("mobileSeedService.createPracticeAssignment(request, supervisorScopeExtPartyId())")); assertTrue(controllerSource.contains("private String supervisorScopeExtPartyId()")); assertTrue(controllerSource.contains("mobileSeedService.requireSupervisorIdentity(currentAppUsername())")); + assertTrue(controllerSource.contains("UserType.SYS_USER.getUserType().equals(loginUser.getUserType())")); assertFalse(controllerSource.contains("private static String supervisorScopeExtPartyId()")); - assertFalse(controllerSource.contains("supervisorScopeExtPartyId() {\n return \"\";")); assertFalse(controllerSource.contains("supervisor H5 keeps team-scope")); } + @Test + public void assignmentsSelfReadBypassesTeamScope() { + AssignmentJdbcTemplate jdbcTemplate = new AssignmentJdbcTemplate(null, List.of()); + AihrPracticeSeedService service = new AihrPracticeSeedService(new ObjectMapper(), jdbcTemplate, null, null); + + service.assignments("13900009999", "13900009999", 10); + + assertTrue(jdbcTemplate.assignmentSql.contains("SELECT id, ext_party_id")); + assertTrue(jdbcTemplate.assignmentSql.contains("ext_party_id = ?")); + assertFalse(jdbcTemplate.assignmentSql.contains("ext_party_id IN")); + } + + @Test + public void completedPilotPeopleCountRequiresTenSessions() { + CountingJdbcTemplate jdbcTemplate = new CountingJdbcTemplate(); + AihrPracticeSeedService service = new AihrPracticeSeedService(new ObjectMapper(), jdbcTemplate, null, null); + + assertEquals(2, service.mobileCompletedPeopleCount()); + assertTrue(jdbcTemplate.lastCountSql.contains("HAVING COUNT(*) >= 10")); + assertTrue(jdbcTemplate.lastCountSql.contains("GROUP BY ext_party_id")); + } + @Test public void practiceTeamSnapshotUsesActivityMembersWhenUnscopedWithOrgSnapshot() throws Exception { TeamSnapshotJdbcTemplate jdbcTemplate = new TeamSnapshotJdbcTemplate(true); @@ -971,4 +993,27 @@ public class AihrPracticeSeedServiceTest { } } + private static final class CountingJdbcTemplate extends JdbcTemplate { + private String lastCountSql = ""; + + @Override + public void execute(String sql) { + // Schema setup is irrelevant for count query tests. + } + + @Override + public T queryForObject(String sql, Class requiredType) { + return requiredType.cast(1); + } + + @Override + public T queryForObject(String sql, Class requiredType, Object... args) { + if (requiredType == Integer.class) { + lastCountSql = sql; + return requiredType.cast(2); + } + return null; + } + } + }