Pārlūkot izejas kodu

fix: 修复OKR绩效核心流程与权限控制

wangkangyjy 1 mēnesi atpakaļ
vecāks
revīzija
d5fadf0caa
83 mainītis faili ar 733 papildinājumiem un 106 dzēšanām
  1. 6 0
      backend/src/main/java/com/yimeng/okr/controller/AssessmentPeriodController.java
  2. 3 10
      backend/src/main/java/com/yimeng/okr/controller/AuthController.java
  3. 4 0
      backend/src/main/java/com/yimeng/okr/controller/DimensionController.java
  4. 4 1
      backend/src/main/java/com/yimeng/okr/controller/FeedbackController.java
  5. 10 4
      backend/src/main/java/com/yimeng/okr/controller/KpiController.java
  6. 15 19
      backend/src/main/java/com/yimeng/okr/controller/OkrController.java
  7. 11 0
      backend/src/main/java/com/yimeng/okr/controller/OrganizationController.java
  8. 19 1
      backend/src/main/java/com/yimeng/okr/controller/PerformanceController.java
  9. 44 0
      backend/src/main/java/com/yimeng/okr/service/AssessmentPeriodService.java
  10. 11 0
      backend/src/main/java/com/yimeng/okr/service/AuthService.java
  11. 90 0
      backend/src/main/java/com/yimeng/okr/service/AuthorizationService.java
  12. 5 2
      backend/src/main/java/com/yimeng/okr/service/FeedbackService.java
  13. 11 4
      backend/src/main/java/com/yimeng/okr/service/KpiService.java
  14. 83 9
      backend/src/main/java/com/yimeng/okr/service/OkrService.java
  15. 53 7
      backend/src/main/java/com/yimeng/okr/service/OrganizationService.java
  16. 72 3
      backend/src/main/java/com/yimeng/okr/service/PerformanceService.java
  17. 0 0
      backend/src/main/resources/static/assets/AlignmentView-BRySBaI5.js
  18. 0 0
      backend/src/main/resources/static/assets/AlignmentView-NAl29DoE.js
  19. 0 0
      backend/src/main/resources/static/assets/DimensionConfigView-CLFYyX-V.js
  20. 0 0
      backend/src/main/resources/static/assets/DimensionConfigView-Ck7C63qq.js
  21. 0 0
      backend/src/main/resources/static/assets/FeedbackView-D51Aoljt.js
  22. 0 0
      backend/src/main/resources/static/assets/HistoryView-Bf9VEKrJ.js
  23. 0 0
      backend/src/main/resources/static/assets/KpiTemplateView-Dbzxvtjx.js
  24. 0 0
      backend/src/main/resources/static/assets/LogView-C7C2nywu.js
  25. 0 0
      backend/src/main/resources/static/assets/LogView-olsuDgTD.js
  26. 0 0
      backend/src/main/resources/static/assets/LoginView-66itVib7.css
  27. 0 0
      backend/src/main/resources/static/assets/LoginView-BJPGcjE3.js
  28. 0 0
      backend/src/main/resources/static/assets/LoginView-BQP2ttAi.js
  29. 0 0
      backend/src/main/resources/static/assets/LoginView-sBh933FF.css
  30. 0 1
      backend/src/main/resources/static/assets/MainLayout-CE96Whde.js
  31. 0 0
      backend/src/main/resources/static/assets/MyOkrView-D9R_i1vF.js
  32. 0 0
      backend/src/main/resources/static/assets/MyResultView-DFeke8dT.js
  33. 0 0
      backend/src/main/resources/static/assets/NotificationView-lsYMS3tn.js
  34. 0 0
      backend/src/main/resources/static/assets/OrgView-Dq2k16Fq.js
  35. 0 0
      backend/src/main/resources/static/assets/PeriodBanner-CBesIvM9.js
  36. 0 0
      backend/src/main/resources/static/assets/PeriodListView-Dh3VFwV9.js
  37. 0 0
      backend/src/main/resources/static/assets/ScoringEditView-CM_Lq_SZ.js
  38. 0 0
      backend/src/main/resources/static/assets/ScoringListView-DEzdjH5N.js
  39. 0 0
      backend/src/main/resources/static/assets/ScoringListView-DwES_S9I.js
  40. 1 0
      backend/src/main/resources/static/assets/TeamOkrView-DRyKaGxV.js
  41. 0 1
      backend/src/main/resources/static/assets/TeamOkrView-nqCYUcoC.js
  42. 0 1
      backend/src/main/resources/static/assets/auth-BfQBitXm.js
  43. 1 0
      backend/src/main/resources/static/assets/auth-Cv0WaWtQ.js
  44. 0 0
      backend/src/main/resources/static/assets/index-BrgsZ4qv.js
  45. 1 1
      backend/src/main/resources/static/assets/index-DzdxblQI.js
  46. 1 1
      backend/src/main/resources/static/index.html
  47. 68 0
      backend/src/test/java/com/yimeng/okr/service/AuthorizationServiceTest.java
  48. 56 0
      backend/src/test/java/com/yimeng/okr/service/OkrServiceTest.java
  49. 64 0
      backend/src/test/java/com/yimeng/okr/service/OrganizationServiceTest.java
  50. 89 0
      backend/src/test/java/com/yimeng/okr/service/PerformanceServiceTest.java
  51. 0 0
      frontend/dist/assets/AlignmentView-BRySBaI5.js
  52. 0 0
      frontend/dist/assets/AlignmentView-NAl29DoE.js
  53. 0 0
      frontend/dist/assets/DimensionConfigView-CLFYyX-V.js
  54. 0 0
      frontend/dist/assets/DimensionConfigView-Ck7C63qq.js
  55. 0 0
      frontend/dist/assets/FeedbackView-D51Aoljt.js
  56. 0 0
      frontend/dist/assets/HistoryView-Bf9VEKrJ.js
  57. 0 0
      frontend/dist/assets/KpiTemplateView-Dbzxvtjx.js
  58. 0 0
      frontend/dist/assets/LogView-C7C2nywu.js
  59. 0 0
      frontend/dist/assets/LogView-olsuDgTD.js
  60. 0 0
      frontend/dist/assets/LoginView-66itVib7.css
  61. 0 0
      frontend/dist/assets/LoginView-BJPGcjE3.js
  62. 0 0
      frontend/dist/assets/LoginView-BQP2ttAi.js
  63. 0 0
      frontend/dist/assets/LoginView-sBh933FF.css
  64. 0 1
      frontend/dist/assets/MainLayout-CE96Whde.js
  65. 0 0
      frontend/dist/assets/MyOkrView-D9R_i1vF.js
  66. 0 0
      frontend/dist/assets/MyResultView-DFeke8dT.js
  67. 0 0
      frontend/dist/assets/NotificationView-lsYMS3tn.js
  68. 0 0
      frontend/dist/assets/OrgView-Dq2k16Fq.js
  69. 0 0
      frontend/dist/assets/PeriodBanner-CBesIvM9.js
  70. 0 0
      frontend/dist/assets/PeriodListView-Dh3VFwV9.js
  71. 0 0
      frontend/dist/assets/ScoringEditView-CM_Lq_SZ.js
  72. 0 0
      frontend/dist/assets/ScoringListView-DEzdjH5N.js
  73. 0 0
      frontend/dist/assets/ScoringListView-DwES_S9I.js
  74. 1 0
      frontend/dist/assets/TeamOkrView-DRyKaGxV.js
  75. 0 1
      frontend/dist/assets/TeamOkrView-nqCYUcoC.js
  76. 0 1
      frontend/dist/assets/auth-BfQBitXm.js
  77. 1 0
      frontend/dist/assets/auth-Cv0WaWtQ.js
  78. 0 0
      frontend/dist/assets/index-BrgsZ4qv.js
  79. 1 1
      frontend/dist/assets/index-DzdxblQI.js
  80. 1 1
      frontend/dist/index.html
  81. 0 1
      frontend/src/api/index.js
  82. 0 8
      frontend/src/stores/auth.js
  83. 7 27
      frontend/src/views/auth/LoginView.vue

+ 6 - 0
backend/src/main/java/com/yimeng/okr/controller/AssessmentPeriodController.java

@@ -1,6 +1,7 @@
 package com.yimeng.okr.controller;
 
 import com.yimeng.okr.dto.ApiResult;
+import com.yimeng.okr.annotation.OperationLogAnnotation;
 import com.yimeng.okr.entity.AssessmentPeriod;
 import com.yimeng.okr.enums.PeriodStatus;
 import com.yimeng.okr.enums.UserRole;
@@ -38,6 +39,7 @@ public class AssessmentPeriodController {
     }
 
     @PostMapping
+    @OperationLogAnnotation(action = "CREATE_PERIOD", entityType = "assessment_period")
     public ApiResult<AssessmentPeriod> create(@RequestBody AssessmentPeriod period) {
         if (!SecurityUtils.hasRole(UserRole.HR_ADMIN)) {
             throw new BusinessException(403, "无权限,需要HR管理员或以上权限");
@@ -46,6 +48,7 @@ public class AssessmentPeriodController {
     }
 
     @PutMapping("/{id}")
+    @OperationLogAnnotation(action = "UPDATE_PERIOD", entityType = "assessment_period")
     public ApiResult<Void> update(@PathVariable Long id, @RequestBody AssessmentPeriod period) {
         if (!SecurityUtils.hasRole(UserRole.HR_ADMIN)) {
             throw new BusinessException(403, "无权限,需要HR管理员或以上权限");
@@ -56,6 +59,7 @@ public class AssessmentPeriodController {
     }
 
     @DeleteMapping("/{id}")
+    @OperationLogAnnotation(action = "DELETE_PERIOD", entityType = "assessment_period")
     public ApiResult<Void> delete(@PathVariable Long id) {
         if (!SecurityUtils.hasRole(UserRole.HR_ADMIN)) {
             throw new BusinessException(403, "无权限,需要HR管理员或以上权限");
@@ -65,6 +69,7 @@ public class AssessmentPeriodController {
     }
 
     @PutMapping("/{id}/status")
+    @OperationLogAnnotation(action = "TRANSITION_PERIOD", entityType = "assessment_period")
     public ApiResult<Void> transitionStatus(@PathVariable Long id, @RequestBody Map<String, String> body) {
         if (!SecurityUtils.hasRole(UserRole.HR_ADMIN)) {
             throw new BusinessException(403, "无权限,需要HR管理员或以上权限");
@@ -75,6 +80,7 @@ public class AssessmentPeriodController {
     }
 
     @PostMapping("/batch")
+    @OperationLogAnnotation(action = "BATCH_CREATE_PERIOD", entityType = "assessment_period")
     public ApiResult<List<AssessmentPeriod>> batchCreate(@RequestBody Map<String, Object> body) {
         if (!SecurityUtils.hasRole(UserRole.HR_ADMIN)) {
             throw new BusinessException(403, "无权限,需要HR管理员或以上权限");

+ 3 - 10
backend/src/main/java/com/yimeng/okr/controller/AuthController.java

@@ -1,7 +1,7 @@
 package com.yimeng.okr.controller;
 
 import com.yimeng.okr.dto.*;
-import com.yimeng.okr.security.JwtTokenProvider;
+import com.yimeng.okr.exception.BusinessException;
 import com.yimeng.okr.service.AuthService;
 import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
@@ -16,7 +16,6 @@ import java.util.Map;
 public class AuthController {
 
     private final AuthService authService;
-    private final JwtTokenProvider tokenProvider;
 
     @PostMapping("/login")
     public ApiResult<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
@@ -25,7 +24,7 @@ public class AuthController {
 
     @PostMapping("/register")
     public ApiResult<LoginResponse> register(@Valid @RequestBody RegisterRequest request) {
-        return ApiResult.success(authService.register(request));
+        throw new BusinessException(403, "内部系统已关闭公开注册,请联系HR管理员创建账号");
     }
 
     @GetMapping("/me")
@@ -44,12 +43,6 @@ public class AuthController {
     @PostMapping("/refresh")
     public ApiResult<Map<String, String>> refreshToken(@RequestBody Map<String, String> body) {
         String refreshToken = body.get("refreshToken");
-        if (!tokenProvider.validateToken(refreshToken)) {
-            return ApiResult.error(401, "Token已过期,请重新登录");
-        }
-        Long userId = tokenProvider.getUserIdFromToken(refreshToken);
-        String role = tokenProvider.getRoleFromToken(refreshToken);
-        String newAccessToken = tokenProvider.generateAccessToken(userId, "refresh", role);
-        return ApiResult.success(Map.of("accessToken", newAccessToken));
+        return ApiResult.success(Map.of("accessToken", authService.refreshAccessToken(refreshToken)));
     }
 }

+ 4 - 0
backend/src/main/java/com/yimeng/okr/controller/DimensionController.java

@@ -1,6 +1,7 @@
 package com.yimeng.okr.controller;
 
 import com.yimeng.okr.dto.ApiResult;
+import com.yimeng.okr.annotation.OperationLogAnnotation;
 import com.yimeng.okr.entity.AssessmentDimension;
 import com.yimeng.okr.enums.UserRole;
 import com.yimeng.okr.exception.BusinessException;
@@ -24,6 +25,7 @@ public class DimensionController {
     }
 
     @PostMapping
+    @OperationLogAnnotation(action = "CREATE_DIMENSION", entityType = "assessment_dimension")
     public ApiResult<AssessmentDimension> create(@RequestBody AssessmentDimension dim) {
         if (!SecurityUtils.hasRole(UserRole.HR_ADMIN)) {
             throw new BusinessException(403, "无权限,需要HR管理员或以上权限");
@@ -32,6 +34,7 @@ public class DimensionController {
     }
 
     @PutMapping("/{id}")
+    @OperationLogAnnotation(action = "UPDATE_DIMENSION", entityType = "assessment_dimension")
     public ApiResult<Void> update(@PathVariable Long id, @RequestBody AssessmentDimension dim) {
         if (!SecurityUtils.hasRole(UserRole.HR_ADMIN)) {
             throw new BusinessException(403, "无权限,需要HR管理员或以上权限");
@@ -42,6 +45,7 @@ public class DimensionController {
     }
 
     @DeleteMapping("/{id}")
+    @OperationLogAnnotation(action = "DELETE_DIMENSION", entityType = "assessment_dimension")
     public ApiResult<Void> delete(@PathVariable Long id) {
         if (!SecurityUtils.hasRole(UserRole.HR_ADMIN)) {
             throw new BusinessException(403, "无权限,需要HR管理员或以上权限");

+ 4 - 1
backend/src/main/java/com/yimeng/okr/controller/FeedbackController.java

@@ -1,6 +1,7 @@
 package com.yimeng.okr.controller;
 
 import com.yimeng.okr.dto.ApiResult;
+import com.yimeng.okr.annotation.OperationLogAnnotation;
 import com.yimeng.okr.dto.ScoringDto;
 import com.yimeng.okr.entity.PerformanceFeedback;
 import com.yimeng.okr.security.SecurityUtils;
@@ -18,6 +19,7 @@ public class FeedbackController {
     private final FeedbackService feedbackService;
 
     @PostMapping("/interview")
+    @OperationLogAnnotation(action = "SAVE_INTERVIEW", entityType = "performance_feedback")
     public ApiResult<PerformanceFeedback> saveInterview(@RequestBody Map<String, Object> body) {
         Long periodId = Long.valueOf(body.get("periodId").toString());
         Long userId = Long.valueOf(body.get("userId").toString());
@@ -31,6 +33,7 @@ public class FeedbackController {
     }
 
     @PostMapping("/reply")
+    @OperationLogAnnotation(action = "SUBMIT_FEEDBACK_REPLY", entityType = "performance_feedback")
     public ApiResult<PerformanceFeedback> submitReply(@RequestBody Map<String, String> body) {
         Long periodId = Long.valueOf(body.get("periodId"));
         Long userId = SecurityUtils.getCurrentUserId();
@@ -41,6 +44,6 @@ public class FeedbackController {
     @GetMapping("/page")
     public ApiResult<ScoringDto> getFeedbackPage(@RequestParam Long periodId,
                                                   @RequestParam Long userId) {
-        return ApiResult.success(feedbackService.getFeedbackPage(periodId, userId));
+        return ApiResult.success(feedbackService.getFeedbackPage(periodId, userId, SecurityUtils.getCurrentUserId()));
     }
 }

+ 10 - 4
backend/src/main/java/com/yimeng/okr/controller/KpiController.java

@@ -1,6 +1,7 @@
 package com.yimeng.okr.controller;
 
 import com.yimeng.okr.dto.ApiResult;
+import com.yimeng.okr.annotation.OperationLogAnnotation;
 import com.yimeng.okr.entity.KpiConfig;
 import com.yimeng.okr.entity.KpiTemplate;
 import com.yimeng.okr.enums.UserRole;
@@ -27,6 +28,7 @@ public class KpiController {
     }
 
     @PostMapping("/templates")
+    @OperationLogAnnotation(action = "CREATE_KPI_TEMPLATE", entityType = "kpi_template")
     public ApiResult<KpiTemplate> createTemplate(@RequestBody KpiTemplate template) {
         if (!SecurityUtils.hasRole(UserRole.HR_ADMIN)) {
             throw new BusinessException(403, "无权限,需要HR管理员或以上权限");
@@ -35,6 +37,7 @@ public class KpiController {
     }
 
     @PutMapping("/templates/{id}")
+    @OperationLogAnnotation(action = "UPDATE_KPI_TEMPLATE", entityType = "kpi_template")
     public ApiResult<Void> updateTemplate(@PathVariable Long id, @RequestBody KpiTemplate template) {
         if (!SecurityUtils.hasRole(UserRole.HR_ADMIN)) {
             throw new BusinessException(403, "无权限,需要HR管理员或以上权限");
@@ -45,6 +48,7 @@ public class KpiController {
     }
 
     @DeleteMapping("/templates/{id}")
+    @OperationLogAnnotation(action = "DELETE_KPI_TEMPLATE", entityType = "kpi_template")
     public ApiResult<Void> deleteTemplate(@PathVariable Long id) {
         if (!SecurityUtils.hasRole(UserRole.HR_ADMIN)) {
             throw new BusinessException(403, "无权限,需要HR管理员或以上权限");
@@ -57,23 +61,25 @@ public class KpiController {
 
     @GetMapping("/configs/{periodId}/{userId}")
     public ApiResult<KpiConfig> getConfig(@PathVariable Long periodId, @PathVariable Long userId) {
-        return ApiResult.success(kpiService.getConfig(periodId, userId));
+        return ApiResult.success(kpiService.getConfig(periodId, userId, SecurityUtils.getCurrentUserId()));
     }
 
     @PostMapping("/configs")
+    @OperationLogAnnotation(action = "SAVE_KPI_CONFIG", entityType = "kpi_config")
     public ApiResult<KpiConfig> saveConfig(@RequestBody KpiConfig config) {
-        return ApiResult.success(kpiService.saveConfig(config));
+        return ApiResult.success(kpiService.saveConfig(config, SecurityUtils.getCurrentUserId()));
     }
 
     @PutMapping("/configs/{id}/lock")
+    @OperationLogAnnotation(action = "LOCK_KPI_CONFIG", entityType = "kpi_config")
     public ApiResult<Void> lockConfig(@PathVariable Long id) {
-        kpiService.lockConfig(id);
+        kpiService.lockConfig(id, SecurityUtils.getCurrentUserId());
         return ApiResult.success();
     }
 
     @GetMapping("/configs/period/{periodId}")
     public ApiResult<List<KpiConfig>> getConfigsByPeriod(@PathVariable Long periodId) {
-        return ApiResult.success(kpiService.getConfigsByPeriod(periodId));
+        return ApiResult.success(kpiService.getConfigsByPeriod(periodId, SecurityUtils.getCurrentUserId()));
     }
 
     @GetMapping("/templates/by-position")

+ 15 - 19
backend/src/main/java/com/yimeng/okr/controller/OkrController.java

@@ -1,14 +1,11 @@
 package com.yimeng.okr.controller;
 
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.yimeng.okr.annotation.OperationLogAnnotation;
 import com.yimeng.okr.dto.ApiResult;
 import com.yimeng.okr.dto.OkrDetailDto;
 import com.yimeng.okr.entity.KrKeyResult;
 import com.yimeng.okr.entity.KrProgressLog;
-import com.yimeng.okr.entity.OkrObjective;
 import com.yimeng.okr.enums.KrStatus;
-import com.yimeng.okr.exception.BusinessException;
-import com.yimeng.okr.mapper.OkrObjectiveMapper;
 import com.yimeng.okr.security.SecurityUtils;
 import com.yimeng.okr.service.OkrService;
 import lombok.RequiredArgsConstructor;
@@ -24,9 +21,9 @@ import java.util.Map;
 public class OkrController {
 
     private final OkrService okrService;
-    private final OkrObjectiveMapper objectiveMapper;
 
     @PostMapping("/submit")
+    @OperationLogAnnotation(action = "SUBMIT_OKR", entityType = "okr_objective")
     public ApiResult<?> submitOkr(@RequestBody Map<String, Object> body) {
         Long periodId = Long.valueOf(body.get("periodId").toString());
         Long userId = SecurityUtils.getCurrentUserId();
@@ -53,6 +50,7 @@ public class OkrController {
     }
 
     @PutMapping("/{id}/update")
+    @OperationLogAnnotation(action = "UPDATE_OKR", entityType = "okr_objective")
     public ApiResult<?> updateMyOkr(@PathVariable Long id, @RequestBody Map<String, Object> body) {
         Long userId = SecurityUtils.getCurrentUserId();
         String title = body.get("title").toString();
@@ -78,6 +76,7 @@ public class OkrController {
     }
 
     @PutMapping("/{id}/review")
+    @OperationLogAnnotation(action = "REVIEW_OKR", entityType = "okr_objective")
     public ApiResult<Void> reviewOkr(@PathVariable Long id, @RequestBody Map<String, Object> body) {
         boolean approved = (boolean) body.get("approved");
         String remark = body.getOrDefault("remark", "").toString();
@@ -86,6 +85,7 @@ public class OkrController {
     }
 
     @PutMapping("/kr/{krId}/progress")
+    @OperationLogAnnotation(action = "UPDATE_KR_PROGRESS", entityType = "kr_key_result")
     public ApiResult<Void> updateKrProgress(@PathVariable Long krId, @RequestBody Map<String, Object> body) {
         Double newValue = Double.valueOf(body.get("newValue").toString());
         KrStatus newStatus = KrStatus.valueOf(body.get("status").toString());
@@ -96,7 +96,7 @@ public class OkrController {
 
     @GetMapping("/kr/{krId}/history")
     public ApiResult<List<KrProgressLog>> getKrHistory(@PathVariable Long krId) {
-        return ApiResult.success(okrService.getKrProgressHistory(krId));
+        return ApiResult.success(okrService.getKrProgressHistory(krId, SecurityUtils.getCurrentUserId()));
     }
 
     @GetMapping("/period/{periodId}/superior")
@@ -127,7 +127,7 @@ public class OkrController {
 
     @GetMapping("/period/{periodId}/dept-members/{deptId}")
     public ApiResult<List<OkrDetailDto>> getDeptMembers(@PathVariable Long periodId, @PathVariable Long deptId) {
-        return ApiResult.success(okrService.getDeptMemberOkrs(periodId, deptId));
+        return ApiResult.success(okrService.getDeptMemberOkrs(periodId, deptId, SecurityUtils.getCurrentUserId()));
     }
 
     @GetMapping("/period/{periodId}/pending-reviews")
@@ -137,15 +137,16 @@ public class OkrController {
 
     @GetMapping("/period/{periodId}/all")
     public ApiResult<List<OkrDetailDto>> getAllOkrs(@PathVariable Long periodId) {
-        return ApiResult.success(okrService.getAllOkrsForPeriod(periodId));
+        return ApiResult.success(okrService.getAllOkrsForPeriod(periodId, SecurityUtils.getCurrentUserId()));
     }
 
     @GetMapping("/{id}")
     public ApiResult<OkrDetailDto> getDetail(@PathVariable Long id) {
-        return ApiResult.success(okrService.getOkrDetail(id));
+        return ApiResult.success(okrService.getOkrDetail(id, SecurityUtils.getCurrentUserId()));
     }
 
     @PostMapping("/submit-hierarchy")
+    @OperationLogAnnotation(action = "SUBMIT_HIERARCHY_OKR", entityType = "okr_objective")
     public ApiResult<?> submitHierarchyOkr(@RequestBody Map<String, Object> body) {
         Long periodId = Long.valueOf(body.get("periodId").toString());
         Long userId = SecurityUtils.getCurrentUserId();
@@ -172,30 +173,25 @@ public class OkrController {
 
     @GetMapping("/period/{periodId}/tree")
     public ApiResult<List<OkrDetailDto>> getOkrTree(@PathVariable Long periodId) {
-        return ApiResult.success(okrService.getOkrTree(periodId));
+        return ApiResult.success(okrService.getOkrTree(periodId, SecurityUtils.getCurrentUserId()));
     }
 
     @GetMapping("/period/{periodId}/orphans")
     public ApiResult<List<OkrDetailDto>> getOrphanOkrs(@PathVariable Long periodId) {
-        return ApiResult.success(okrService.getOrphanOkrs(periodId));
+        return ApiResult.success(okrService.getOrphanOkrs(periodId, SecurityUtils.getCurrentUserId()));
     }
 
     @PostMapping("/{id}/align")
+    @OperationLogAnnotation(action = "ALIGN_OKR", entityType = "okr_objective")
     public ApiResult<Void> setAlignment(@PathVariable Long id, @RequestBody Map<String, Object> body) {
-        OkrObjective obj = objectiveMapper.selectById(id);
-        if (obj == null) throw new BusinessException(404, "OKR不存在");
         Long parentId = body.get("parentObjectiveId") != null
                 ? Long.valueOf(body.get("parentObjectiveId").toString()) : null;
-        obj.setParentObjectiveId(parentId);
-        objectiveMapper.updateById(obj);
+        okrService.setAlignment(id, parentId, SecurityUtils.getCurrentUserId());
         return ApiResult.success();
     }
 
     @GetMapping("/{id}/children")
     public ApiResult<List<OkrDetailDto>> getChildren(@PathVariable Long id) {
-        List<OkrObjective> children = objectiveMapper.selectList(
-                new LambdaQueryWrapper<OkrObjective>()
-                        .eq(OkrObjective::getParentObjectiveId, id));
-        return ApiResult.success(okrService.enrichObjectives(children));
+        return ApiResult.success(okrService.getChildren(id, SecurityUtils.getCurrentUserId()));
     }
 }

+ 11 - 0
backend/src/main/java/com/yimeng/okr/controller/OrganizationController.java

@@ -1,6 +1,7 @@
 package com.yimeng.okr.controller;
 
 import com.yimeng.okr.dto.ApiResult;
+import com.yimeng.okr.annotation.OperationLogAnnotation;
 import com.yimeng.okr.dto.DepartmentTreeNode;
 import com.yimeng.okr.dto.UserInfo;
 import com.yimeng.okr.entity.SysDepartment;
@@ -27,6 +28,7 @@ public class OrganizationController {
     }
 
     @PostMapping("/departments")
+    @OperationLogAnnotation(action = "CREATE_DEPARTMENT", entityType = "sys_department")
     public ApiResult<SysDepartment> createDepartment(@RequestBody SysDepartment dept) {
         if (!SecurityUtils.hasRole(UserRole.HR_ADMIN)) {
             throw new BusinessException(403, "无权限,需要HR管理员或以上权限");
@@ -35,6 +37,7 @@ public class OrganizationController {
     }
 
     @PutMapping("/departments/{id}")
+    @OperationLogAnnotation(action = "UPDATE_DEPARTMENT", entityType = "sys_department")
     public ApiResult<Void> updateDepartment(@PathVariable Long id, @RequestBody SysDepartment dept) {
         if (!SecurityUtils.hasRole(UserRole.HR_ADMIN)) {
             throw new BusinessException(403, "无权限,需要HR管理员或以上权限");
@@ -45,6 +48,7 @@ public class OrganizationController {
     }
 
     @DeleteMapping("/departments/{id}")
+    @OperationLogAnnotation(action = "DELETE_DEPARTMENT", entityType = "sys_department")
     public ApiResult<Void> deleteDepartment(@PathVariable Long id) {
         if (!SecurityUtils.hasRole(UserRole.HR_ADMIN)) {
             throw new BusinessException(403, "无权限,需要HR管理员或以上权限");
@@ -69,6 +73,7 @@ public class OrganizationController {
     }
 
     @PutMapping("/users/{id}/superior")
+    @OperationLogAnnotation(action = "UPDATE_USER_SUPERIOR", entityType = "sys_user")
     public ApiResult<Void> updateSuperior(@PathVariable Long id, @RequestBody Map<String, Long> body) {
         if (!SecurityUtils.hasRole(UserRole.HR_ADMIN)) {
             throw new BusinessException(403, "无权限,需要HR管理员或以上权限");
@@ -78,6 +83,7 @@ public class OrganizationController {
     }
 
     @PutMapping("/users/{id}/department")
+    @OperationLogAnnotation(action = "UPDATE_USER_DEPARTMENT", entityType = "sys_user")
     public ApiResult<Void> updateDepartment(@PathVariable Long id, @RequestBody Map<String, Long> body) {
         if (!SecurityUtils.hasRole(UserRole.HR_ADMIN)) {
             throw new BusinessException(403, "无权限,需要HR管理员或以上权限");
@@ -87,6 +93,7 @@ public class OrganizationController {
     }
 
     @PutMapping("/users/{id}/position")
+    @OperationLogAnnotation(action = "UPDATE_USER_POSITION", entityType = "sys_user")
     public ApiResult<Void> updatePosition(@PathVariable Long id, @RequestBody Map<String, String> body) {
         if (!SecurityUtils.hasRole(UserRole.HR_ADMIN)) {
             throw new BusinessException(403, "无权限,需要HR管理员或以上权限");
@@ -102,6 +109,7 @@ public class OrganizationController {
     }
 
     @PostMapping("/users")
+    @OperationLogAnnotation(action = "CREATE_USER", entityType = "sys_user")
     public ApiResult<UserInfo> createUser(@RequestBody Map<String, Object> body) {
         if (!SecurityUtils.hasRole(UserRole.HR_ADMIN)) {
             throw new BusinessException(403, "无权限,需要HR管理员或以上权限");
@@ -110,6 +118,7 @@ public class OrganizationController {
     }
 
     @PutMapping("/users/{id}")
+    @OperationLogAnnotation(action = "UPDATE_USER", entityType = "sys_user")
     public ApiResult<Void> updateUser(@PathVariable Long id, @RequestBody Map<String, Object> body) {
         if (!SecurityUtils.hasRole(UserRole.HR_ADMIN)) {
             throw new BusinessException(403, "无权限,需要HR管理员或以上权限");
@@ -119,6 +128,7 @@ public class OrganizationController {
     }
 
     @DeleteMapping("/users/{id}")
+    @OperationLogAnnotation(action = "DELETE_USER", entityType = "sys_user")
     public ApiResult<Void> deleteUser(@PathVariable Long id) {
         if (!SecurityUtils.hasRole(UserRole.HR_ADMIN)) {
             throw new BusinessException(403, "无权限,需要HR管理员或以上权限");
@@ -128,6 +138,7 @@ public class OrganizationController {
     }
 
     @PostMapping("/init-seed")
+    @OperationLogAnnotation(action = "INIT_SEED_DATA", entityType = "sys_department")
     public ApiResult<Void> initSeedData() {
         if (!SecurityUtils.hasRole(UserRole.HR_ADMIN)) {
             throw new BusinessException(403, "无权限,需要HR管理员或以上权限");

+ 19 - 1
backend/src/main/java/com/yimeng/okr/controller/PerformanceController.java

@@ -1,6 +1,7 @@
 package com.yimeng.okr.controller;
 
 import com.yimeng.okr.dto.ApiResult;
+import com.yimeng.okr.annotation.OperationLogAnnotation;
 import com.yimeng.okr.dto.ScoringDto;
 import com.yimeng.okr.entity.PerformanceScore;
 import com.yimeng.okr.security.SecurityUtils;
@@ -21,6 +22,7 @@ public class PerformanceController {
     // Self evaluation
 
     @PostMapping("/self")
+    @OperationLogAnnotation(action = "SUBMIT_SELF_SCORE", entityType = "performance_score")
     public ApiResult<PerformanceScore> submitSelf(@RequestBody Map<String, Object> body) {
         Long periodId = Long.valueOf(body.get("periodId").toString());
         Long userId = SecurityUtils.getCurrentUserId();
@@ -36,6 +38,7 @@ public class PerformanceController {
     // Superior scoring
 
     @PostMapping("/superior")
+    @OperationLogAnnotation(action = "SUBMIT_SUPERIOR_SCORE", entityType = "performance_score")
     public ApiResult<PerformanceScore> submitSuperior(@RequestBody Map<String, Object> body) {
         Long periodId = Long.valueOf(body.get("periodId").toString());
         Long targetUserId = Long.valueOf(body.get("userId").toString());
@@ -55,7 +58,7 @@ public class PerformanceController {
     @GetMapping("/page")
     public ApiResult<ScoringDto> getScoringPage(@RequestParam Long periodId,
                                                  @RequestParam Long userId) {
-        return ApiResult.success(performanceService.getScoringPage(periodId, userId));
+        return ApiResult.success(performanceService.getScoringPage(periodId, userId, SecurityUtils.getCurrentUserId()));
     }
 
     @GetMapping("/my")
@@ -64,6 +67,7 @@ public class PerformanceController {
     }
 
     @PostMapping("/confirm")
+    @OperationLogAnnotation(action = "CONFIRM_SCORE", entityType = "performance_score")
     public ApiResult<Void> confirmScore(@RequestBody Map<String, Object> body) {
         Long periodId = Long.valueOf(body.get("periodId").toString());
         performanceService.confirmScore(periodId, SecurityUtils.getCurrentUserId());
@@ -76,6 +80,20 @@ public class PerformanceController {
                 performanceService.getSubordinateScores(periodId, SecurityUtils.getCurrentUserId()));
     }
 
+    @PostMapping("/{id}/correction")
+    @OperationLogAnnotation(action = "CORRECT_FINAL_SCORE", entityType = "performance_score")
+    public ApiResult<PerformanceScore> correctFinalScore(@PathVariable Long id, @RequestBody Map<String, Object> body) {
+        return ApiResult.success(performanceService.correctFinalScore(
+                id,
+                SecurityUtils.getCurrentUserId(),
+                toDouble(body.get("okrScore")),
+                toDouble(body.get("kpiScore")),
+                toDouble(body.get("bonus")),
+                toDouble(body.get("penalty")),
+                body.getOrDefault("commentsJson", "{}").toString(),
+                body.getOrDefault("reason", "").toString()));
+    }
+
     private Double toDouble(Object val) {
         if (val == null) return 0.0;
         if (val instanceof Number n) return n.doubleValue();

+ 44 - 0
backend/src/main/java/com/yimeng/okr/service/AssessmentPeriodService.java

@@ -1,13 +1,21 @@
 package com.yimeng.okr.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.yimeng.okr.entity.AssessmentPeriod;
+import com.yimeng.okr.entity.KpiConfig;
+import com.yimeng.okr.entity.KrKeyResult;
+import com.yimeng.okr.entity.OkrObjective;
 import com.yimeng.okr.entity.SysUser;
 import com.yimeng.okr.enums.PeriodStatus;
 import com.yimeng.okr.enums.PeriodType;
 import com.yimeng.okr.event.NotificationEvent;
 import com.yimeng.okr.exception.BusinessException;
 import com.yimeng.okr.mapper.AssessmentPeriodMapper;
+import com.yimeng.okr.mapper.KpiConfigMapper;
+import com.yimeng.okr.mapper.KrKeyResultMapper;
+import com.yimeng.okr.mapper.OkrObjectiveMapper;
 import com.yimeng.okr.mapper.SysUserMapper;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -27,7 +35,11 @@ public class AssessmentPeriodService {
 
     private final AssessmentPeriodMapper periodMapper;
     private final SysUserMapper userMapper;
+    private final OkrObjectiveMapper objectiveMapper;
+    private final KrKeyResultMapper krMapper;
+    private final KpiConfigMapper kpiConfigMapper;
     private final ApplicationEventPublisher publisher;
+    private final ObjectMapper objectMapper = new ObjectMapper();
 
     public List<AssessmentPeriod> list(boolean isAdmin) {
         LambdaQueryWrapper<AssessmentPeriod> wrapper = new LambdaQueryWrapper<AssessmentPeriod>()
@@ -84,6 +96,9 @@ public class AssessmentPeriodService {
 
         period.setStatus(targetStatus);
         periodMapper.updateById(period);
+        if (targetStatus == PeriodStatus.ARCHIVED) {
+            snapshotPeriod(period.getId());
+        }
         log.info("Period {} status: {} -> {}", id, current.getLabel(), targetStatus.getLabel());
 
         notifyPeriodTransition(period, targetStatus);
@@ -184,4 +199,33 @@ public class AssessmentPeriodService {
             case ARCHIVED -> false;
         };
     }
+
+    private void snapshotPeriod(Long periodId) {
+        List<OkrObjective> objectives = objectiveMapper.selectList(
+                new LambdaQueryWrapper<OkrObjective>().eq(OkrObjective::getPeriodId, periodId));
+        for (OkrObjective objective : objectives) {
+            List<KrKeyResult> krs = krMapper.selectList(
+                    new LambdaQueryWrapper<KrKeyResult>().eq(KrKeyResult::getObjectiveId, objective.getId()));
+            objective.setSnapshotJson(toJson(Map.of(
+                    "objective", objective,
+                    "keyResults", krs
+            )));
+            objectiveMapper.updateById(objective);
+        }
+
+        List<KpiConfig> configs = kpiConfigMapper.selectList(
+                new LambdaQueryWrapper<KpiConfig>().eq(KpiConfig::getPeriodId, periodId));
+        for (KpiConfig config : configs) {
+            config.setSnapshotJson(toJson(config));
+            kpiConfigMapper.updateById(config);
+        }
+    }
+
+    private String toJson(Object value) {
+        try {
+            return objectMapper.writeValueAsString(value);
+        } catch (JsonProcessingException e) {
+            throw new BusinessException("归档快照生成失败");
+        }
+    }
 }

+ 11 - 0
backend/src/main/java/com/yimeng/okr/service/AuthService.java

@@ -80,6 +80,7 @@ public class AuthService {
 
     public void changePassword(Long userId, String oldPassword, String newPassword) {
         SysUser user = userMapper.selectById(userId);
+        if (user == null) throw new BusinessException(404, "用户不存在");
         if (!passwordEncoder.matches(oldPassword, user.getPasswordHash())) {
             throw new BusinessException("原密码错误");
         }
@@ -87,6 +88,16 @@ public class AuthService {
         userMapper.updateById(user);
     }
 
+    public String refreshAccessToken(String refreshToken) {
+        if (refreshToken == null || !tokenProvider.validateToken(refreshToken)) {
+            throw new BusinessException(401, "Token已过期,请重新登录");
+        }
+        Long userId = tokenProvider.getUserIdFromToken(refreshToken);
+        SysUser user = userMapper.selectById(userId);
+        if (user == null) throw new BusinessException(401, "用户不存在,请重新登录");
+        return tokenProvider.generateAccessToken(user.getId(), user.getUsername(), user.getRole().name());
+    }
+
     @Transactional
     public void initAdmin() {
         long count = userMapper.selectCount(

+ 90 - 0
backend/src/main/java/com/yimeng/okr/service/AuthorizationService.java

@@ -0,0 +1,90 @@
+package com.yimeng.okr.service;
+
+import com.yimeng.okr.entity.OkrObjective;
+import com.yimeng.okr.entity.SysDepartment;
+import com.yimeng.okr.entity.SysUser;
+import com.yimeng.okr.enums.UserRole;
+import com.yimeng.okr.exception.BusinessException;
+import com.yimeng.okr.mapper.SysDepartmentMapper;
+import com.yimeng.okr.mapper.SysUserMapper;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.Objects;
+
+@Service
+@RequiredArgsConstructor
+public class AuthorizationService {
+
+    private final SysUserMapper userMapper;
+    private final SysDepartmentMapper departmentMapper;
+
+    public void requireCanViewUserData(Long actorId, Long targetUserId) {
+        if (canViewUserData(actorId, targetUserId)) return;
+        throw new BusinessException(403, "无权查看该员工数据");
+    }
+
+    public void requireCanManageUserData(Long actorId, Long targetUserId) {
+        if (canManageUserData(actorId, targetUserId)) return;
+        throw new BusinessException(403, "无权管理该员工数据");
+    }
+
+    public void requireCanViewDepartment(Long actorId, Long deptId) {
+        SysUser actor = requireUser(actorId);
+        if (isHrOrAbove(actor) || isDepartmentLeader(actorId, deptId)) return;
+        throw new BusinessException(403, "无权查看该部门数据");
+    }
+
+    public void requireCanViewOkr(Long actorId, OkrObjective objective) {
+        if (objective == null) throw new BusinessException(404, "OKR不存在");
+        if (canViewOkr(actorId, objective)) return;
+        throw new BusinessException(403, "无权查看该OKR");
+    }
+
+    public boolean canViewOkr(Long actorId, OkrObjective objective) {
+        if (objective == null) return false;
+        if (objective.getUserId() == null) return isHrOrAbove(requireUser(actorId));
+        if (canViewUserData(actorId, objective.getUserId())) return true;
+        return "APPROVED".equals(objective.getStatus())
+                && ("COMPANY".equals(objective.getLevel()) || "DEPARTMENT".equals(objective.getLevel()));
+    }
+
+    public boolean canViewUserData(Long actorId, Long targetUserId) {
+        if (actorId == null || targetUserId == null) return false;
+        if (Objects.equals(actorId, targetUserId)) return true;
+        SysUser actor = requireUser(actorId);
+        SysUser target = requireUser(targetUserId);
+        return isHrOrAbove(actor)
+                || Objects.equals(actorId, target.getSuperiorId())
+                || isDepartmentLeader(actorId, target.getDepartmentId());
+    }
+
+    public boolean canManageUserData(Long actorId, Long targetUserId) {
+        if (actorId == null || targetUserId == null || Objects.equals(actorId, targetUserId)) return false;
+        SysUser actor = requireUser(actorId);
+        SysUser target = requireUser(targetUserId);
+        return isHrOrAbove(actor)
+                || Objects.equals(actorId, target.getSuperiorId())
+                || isDepartmentLeader(actorId, target.getDepartmentId());
+    }
+
+    public boolean isHrOrAbove(Long actorId) {
+        return isHrOrAbove(requireUser(actorId));
+    }
+
+    private boolean isHrOrAbove(SysUser user) {
+        return user.getRole() == UserRole.HR_ADMIN || user.getRole() == UserRole.SUPER_ADMIN;
+    }
+
+    private boolean isDepartmentLeader(Long actorId, Long deptId) {
+        if (actorId == null || deptId == null) return false;
+        SysDepartment dept = departmentMapper.selectById(deptId);
+        return dept != null && Objects.equals(actorId, dept.getLeaderId());
+    }
+
+    private SysUser requireUser(Long userId) {
+        SysUser user = userMapper.selectById(userId);
+        if (user == null) throw new BusinessException(404, "用户不存在");
+        return user;
+    }
+}

+ 5 - 2
backend/src/main/java/com/yimeng/okr/service/FeedbackService.java

@@ -25,12 +25,14 @@ public class FeedbackService {
     private final PerformanceScoreMapper scoreMapper;
     private final OkrService okrService;
     private final PerformanceService performanceService;
+    private final AuthorizationService authorizationService;
     private final ApplicationEventPublisher publisher;
 
     @Transactional
     public PerformanceFeedback saveInterviewNotes(Long periodId, Long userId, Long superiorId,
                                                    String interviewNotes, String highlights,
                                                    String issues, String improvements) {
+        authorizationService.requireCanManageUserData(superiorId, userId);
         PerformanceFeedback existing = feedbackMapper.selectOne(
                 new LambdaQueryWrapper<PerformanceFeedback>()
                         .eq(PerformanceFeedback::getPeriodId, periodId)
@@ -78,7 +80,8 @@ public class FeedbackService {
         return existing;
     }
 
-    public ScoringDto getFeedbackPage(Long periodId, Long userId) {
-        return performanceService.getScoringPage(periodId, userId);
+    public ScoringDto getFeedbackPage(Long periodId, Long userId, Long actorId) {
+        authorizationService.requireCanViewUserData(actorId, userId);
+        return performanceService.getScoringPage(periodId, userId, actorId);
     }
 }

+ 11 - 4
backend/src/main/java/com/yimeng/okr/service/KpiService.java

@@ -19,6 +19,7 @@ public class KpiService {
 
     private final KpiTemplateMapper templateMapper;
     private final KpiConfigMapper configMapper;
+    private final AuthorizationService authorizationService;
 
     // Templates
 
@@ -56,7 +57,8 @@ public class KpiService {
     }
 
     @Transactional
-    public KpiConfig saveConfig(KpiConfig config) {
+    public KpiConfig saveConfig(KpiConfig config, Long actorId) {
+        authorizationService.requireCanManageUserData(actorId, config.getUserId());
         KpiConfig existing = configMapper.selectOne(
                 new LambdaQueryWrapper<KpiConfig>()
                         .eq(KpiConfig::getPeriodId, config.getPeriodId())
@@ -75,21 +77,26 @@ public class KpiService {
         return config;
     }
 
-    public void lockConfig(Long configId) {
+    public void lockConfig(Long configId, Long actorId) {
         KpiConfig config = configMapper.selectById(configId);
         if (config == null) throw new BusinessException(404, "KPI配置不存在");
+        authorizationService.requireCanManageUserData(actorId, config.getUserId());
         config.setLockedAt(LocalDateTime.now());
         configMapper.updateById(config);
     }
 
-    public KpiConfig getConfig(Long periodId, Long userId) {
+    public KpiConfig getConfig(Long periodId, Long userId, Long actorId) {
+        authorizationService.requireCanViewUserData(actorId, userId);
         return configMapper.selectOne(
                 new LambdaQueryWrapper<KpiConfig>()
                         .eq(KpiConfig::getPeriodId, periodId)
                         .eq(KpiConfig::getUserId, userId));
     }
 
-    public List<KpiConfig> getConfigsByPeriod(Long periodId) {
+    public List<KpiConfig> getConfigsByPeriod(Long periodId, Long actorId) {
+        if (!authorizationService.isHrOrAbove(actorId)) {
+            throw new BusinessException(403, "无权限,需要HR管理员或以上权限");
+        }
         return configMapper.selectList(
                 new LambdaQueryWrapper<KpiConfig>().eq(KpiConfig::getPeriodId, periodId));
     }

+ 83 - 9
backend/src/main/java/com/yimeng/okr/service/OkrService.java

@@ -30,6 +30,7 @@ public class OkrService {
     private final AssessmentPeriodMapper periodMapper;
     private final SysUserMapper userMapper;
     private final SysDepartmentMapper departmentMapper;
+    private final AuthorizationService authorizationService;
     private final ApplicationEventPublisher publisher;
 
     @Transactional
@@ -152,6 +153,15 @@ public class OkrService {
             if (krs == null || krs.size() < 2 || krs.size() > 5) {
                 throw new BusinessException("个人OKR需配置2~5条KR");
             }
+            long existingCount = objectiveMapper.selectCount(
+                    new LambdaQueryWrapper<OkrObjective>()
+                            .eq(OkrObjective::getPeriodId, periodId)
+                            .eq(OkrObjective::getUserId, userId)
+                            .eq(OkrObjective::getLevel, "INDIVIDUAL")
+                            .ne(OkrObjective::getStatus, "REJECTED"));
+            if (existingCount > 0) {
+                throw new BusinessException("你已提交过本周期个人OKR,如需修改请先由上级驳回");
+            }
         }
 
         int weightSum = krs.stream().mapToInt(k -> k.getWeight() != null ? k.getWeight() : 0).sum();
@@ -160,6 +170,20 @@ public class OkrService {
         if (parentObjectiveId != null) {
             OkrObjective parent = objectiveMapper.selectById(parentObjectiveId);
             if (parent == null) throw new BusinessException(404, "对齐目标不存在");
+            if (!Objects.equals(parent.getPeriodId(), periodId)) {
+                throw new BusinessException("只能对齐同一考核周期内的OKR");
+            }
+            authorizationService.requireCanViewOkr(userId, parent);
+        }
+        if (alignedObjectiveIds != null && !alignedObjectiveIds.isEmpty()) {
+            for (Long alignedId : alignedObjectiveIds) {
+                OkrObjective aligned = objectiveMapper.selectById(alignedId);
+                if (aligned == null) throw new BusinessException(404, "对齐目标不存在");
+                if (!Objects.equals(aligned.getPeriodId(), periodId)) {
+                    throw new BusinessException("只能对齐同一考核周期内的OKR");
+                }
+                authorizationService.requireCanViewOkr(userId, aligned);
+            }
         }
 
         OkrObjective obj = new OkrObjective();
@@ -291,6 +315,11 @@ public class OkrService {
         if (period == null || period.getStatus() != PeriodStatus.EXECUTING) {
             throw new BusinessException("当前周期不在执行阶段");
         }
+        boolean isOwner = Objects.equals(userId, obj.getUserId());
+        boolean isAssignee = kr.getAssignedTo() != null && Objects.equals(userId, kr.getAssignedTo());
+        if (!isOwner && !isAssignee) {
+            throw new BusinessException(403, "无权更新该KR进度");
+        }
         KrProgressLog log = new KrProgressLog();
         log.setKrId(krId);
         log.setOldValue(kr.getCurrentValue());
@@ -387,7 +416,8 @@ public class OkrService {
         return enrichObjectives(objectives);
     }
 
-    public List<OkrDetailDto> getDeptMemberOkrs(Long periodId, Long deptId) {
+    public List<OkrDetailDto> getDeptMemberOkrs(Long periodId, Long deptId, Long actorId) {
+        authorizationService.requireCanViewDepartment(actorId, deptId);
         List<SysUser> members = userMapper.selectList(
                 new LambdaQueryWrapper<SysUser>().eq(SysUser::getDepartmentId, deptId));
         if (members.isEmpty()) return List.of();
@@ -420,33 +450,39 @@ public class OkrService {
         return enrichObjectives(objectives);
     }
 
-    public OkrDetailDto getOkrDetail(Long objectiveId) {
+    public OkrDetailDto getOkrDetail(Long objectiveId, Long actorId) {
         OkrObjective obj = objectiveMapper.selectById(objectiveId);
         if (obj == null) throw new BusinessException(404, "OKR不存在");
+        authorizationService.requireCanViewOkr(actorId, obj);
         return enrichObjectives(List.of(obj)).get(0);
     }
 
-    public List<KrProgressLog> getKrProgressHistory(Long krId) {
+    public List<KrProgressLog> getKrProgressHistory(Long krId, Long actorId) {
+        KrKeyResult kr = krMapper.selectById(krId);
+        if (kr == null) throw new BusinessException(404, "KR不存在");
+        OkrObjective obj = objectiveMapper.selectById(kr.getObjectiveId());
+        if (obj == null) throw new BusinessException(404, "OKR目标不存在");
+        authorizationService.requireCanViewOkr(actorId, obj);
         return progressLogMapper.selectList(
                 new LambdaQueryWrapper<KrProgressLog>()
                         .eq(KrProgressLog::getKrId, krId)
                         .orderByDesc(KrProgressLog::getCreatedAt));
     }
 
-    public List<OkrDetailDto> getAllOkrsForPeriod(Long periodId) {
+    public List<OkrDetailDto> getAllOkrsForPeriod(Long periodId, Long actorId) {
         List<OkrObjective> objectives = objectiveMapper.selectList(
                 new LambdaQueryWrapper<OkrObjective>().eq(OkrObjective::getPeriodId, periodId));
-        return enrichObjectives(objectives);
+        return enrichObjectives(filterVisibleObjectives(objectives, actorId));
     }
 
     public List<OkrDetailDto> getDeptOkrsForPeriod(Long periodId, Long deptId) {
         return List.of(); // deprecated
     }
 
-    public List<OkrDetailDto> getOkrTree(Long periodId) {
+    public List<OkrDetailDto> getOkrTree(Long periodId, Long actorId) {
         List<OkrObjective> all = objectiveMapper.selectList(
                 new LambdaQueryWrapper<OkrObjective>().eq(OkrObjective::getPeriodId, periodId));
-        List<OkrDetailDto> enriched = enrichObjectives(all);
+        List<OkrDetailDto> enriched = enrichObjectives(filterVisibleObjectives(all, actorId));
 
         Map<Long, OkrDetailDto> map = enriched.stream()
                 .collect(Collectors.toMap(OkrDetailDto::getId, o -> o, (a, b) -> a));
@@ -468,13 +504,51 @@ public class OkrService {
         return roots;
     }
 
-    public List<OkrDetailDto> getOrphanOkrs(Long periodId) {
+    public List<OkrDetailDto> getOrphanOkrs(Long periodId, Long actorId) {
         List<OkrObjective> individuals = objectiveMapper.selectList(
                 new LambdaQueryWrapper<OkrObjective>()
                         .eq(OkrObjective::getPeriodId, periodId)
                         .eq(OkrObjective::getLevel, "INDIVIDUAL")
                         .isNull(OkrObjective::getParentObjectiveId));
-        return enrichObjectives(individuals);
+        return enrichObjectives(filterVisibleObjectives(individuals, actorId));
+    }
+
+    public List<OkrDetailDto> getChildren(Long objectiveId, Long actorId) {
+        OkrObjective parent = objectiveMapper.selectById(objectiveId);
+        if (parent == null) throw new BusinessException(404, "OKR不存在");
+        authorizationService.requireCanViewOkr(actorId, parent);
+        List<OkrObjective> children = objectiveMapper.selectList(
+                new LambdaQueryWrapper<OkrObjective>().eq(OkrObjective::getParentObjectiveId, objectiveId));
+        return enrichObjectives(filterVisibleObjectives(children, actorId));
+    }
+
+    @Transactional
+    public void setAlignment(Long objectiveId, Long parentId, Long actorId) {
+        OkrObjective obj = objectiveMapper.selectById(objectiveId);
+        if (obj == null) throw new BusinessException(404, "OKR不存在");
+        if (!Objects.equals(obj.getUserId(), actorId) && !authorizationService.isHrOrAbove(actorId)) {
+            throw new BusinessException(403, "只能调整自己的OKR对齐关系");
+        }
+        AssessmentPeriod period = periodMapper.selectById(obj.getPeriodId());
+        if (period == null || period.getStatus() != PeriodStatus.OKR_ALIGN) {
+            throw new BusinessException("当前周期不在OKR对齐阶段");
+        }
+        if (parentId != null) {
+            OkrObjective parent = objectiveMapper.selectById(parentId);
+            if (parent == null) throw new BusinessException(404, "对齐目标不存在");
+            if (!Objects.equals(parent.getPeriodId(), obj.getPeriodId())) {
+                throw new BusinessException("只能对齐同一考核周期内的OKR");
+            }
+            authorizationService.requireCanViewOkr(actorId, parent);
+        }
+        obj.setParentObjectiveId(parentId);
+        objectiveMapper.updateById(obj);
+    }
+
+    private List<OkrObjective> filterVisibleObjectives(List<OkrObjective> objectives, Long actorId) {
+        return objectives.stream()
+                .filter(obj -> authorizationService.canViewOkr(actorId, obj))
+                .toList();
     }
 
     // --- Helper ---

+ 53 - 7
backend/src/main/java/com/yimeng/okr/service/OrganizationService.java

@@ -3,20 +3,26 @@ package com.yimeng.okr.service;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.yimeng.okr.dto.DepartmentTreeNode;
 import com.yimeng.okr.dto.UserInfo;
+import com.yimeng.okr.entity.KpiConfig;
+import com.yimeng.okr.entity.OkrObjective;
+import com.yimeng.okr.entity.PerformanceFeedback;
+import com.yimeng.okr.entity.PerformanceScore;
 import com.yimeng.okr.entity.SysDepartment;
 import com.yimeng.okr.entity.SysUser;
 import com.yimeng.okr.enums.UserRole;
 import com.yimeng.okr.exception.BusinessException;
-import com.yimeng.okr.mapper.SysDepartmentMapper;
-import com.yimeng.okr.mapper.SysUserMapper;
+import com.yimeng.okr.mapper.*;
 import lombok.RequiredArgsConstructor;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 @Service
@@ -25,6 +31,10 @@ public class OrganizationService {
 
     private final SysDepartmentMapper departmentMapper;
     private final SysUserMapper userMapper;
+    private final OkrObjectiveMapper objectiveMapper;
+    private final PerformanceScoreMapper scoreMapper;
+    private final PerformanceFeedbackMapper feedbackMapper;
+    private final KpiConfigMapper kpiConfigMapper;
     private final PasswordEncoder passwordEncoder;
 
     // Department CRUD
@@ -93,7 +103,7 @@ public class OrganizationService {
     public void updateUserSuperior(Long userId, Long superiorId) {
         SysUser user = userMapper.selectById(userId);
         if (user == null) throw new BusinessException(404, "用户不存在");
-        if (userId.equals(superiorId)) throw new BusinessException("不能将自己设为直属上级");
+        validateSuperiorAssignment(userId, superiorId);
         user.setSuperiorId(superiorId);
         userMapper.updateById(user);
     }
@@ -159,11 +169,17 @@ public class OrganizationService {
         if (body.containsKey("position")) user.setPosition(body.get("position").toString());
         if (body.containsKey("departmentId") && body.get("departmentId") != null)
             user.setDepartmentId(Long.valueOf(body.get("departmentId").toString()));
+        if (body.containsKey("superiorId") && body.get("superiorId") != null)
+            validateSuperiorAssignment(id, Long.valueOf(body.get("superiorId").toString()));
         if (body.containsKey("superiorId") && body.get("superiorId") != null)
             user.setSuperiorId(Long.valueOf(body.get("superiorId").toString()));
         if (body.containsKey("role")) {
             try {
-                user.setRole(UserRole.valueOf(body.get("role").toString()));
+                UserRole newRole = UserRole.valueOf(body.get("role").toString());
+                if (user.getRole() == UserRole.SUPER_ADMIN && newRole != UserRole.SUPER_ADMIN) {
+                    ensureMoreThanOneSuperAdmin();
+                }
+                user.setRole(newRole);
             } catch (IllegalArgumentException e) {
                 throw new BusinessException("无效角色: " + body.get("role"));
             }
@@ -177,13 +193,43 @@ public class OrganizationService {
         SysUser user = userMapper.selectById(id);
         if (user == null) throw new BusinessException(404, "用户不存在");
         if (user.getRole() == UserRole.SUPER_ADMIN) {
-            long adminCount = userMapper.selectCount(
-                    new LambdaQueryWrapper<SysUser>().eq(SysUser::getRole, UserRole.SUPER_ADMIN));
-            if (adminCount <= 1) throw new BusinessException("至少保留一个超级管理员");
+            ensureMoreThanOneSuperAdmin();
         }
+        long subordinateCount = userMapper.selectCount(
+                new LambdaQueryWrapper<SysUser>().eq(SysUser::getSuperiorId, id));
+        if (subordinateCount > 0) throw new BusinessException("该用户仍有直属下属,不能删除");
+        long leaderCount = departmentMapper.selectCount(
+                new LambdaQueryWrapper<SysDepartment>().eq(SysDepartment::getLeaderId, id));
+        if (leaderCount > 0) throw new BusinessException("该用户仍是部门负责人,不能删除");
+        long historyCount = objectiveMapper.selectCount(new LambdaQueryWrapper<OkrObjective>().eq(OkrObjective::getUserId, id))
+                + scoreMapper.selectCount(new LambdaQueryWrapper<PerformanceScore>().eq(PerformanceScore::getUserId, id))
+                + feedbackMapper.selectCount(new LambdaQueryWrapper<PerformanceFeedback>().eq(PerformanceFeedback::getUserId, id))
+                + kpiConfigMapper.selectCount(new LambdaQueryWrapper<KpiConfig>().eq(KpiConfig::getUserId, id));
+        if (historyCount > 0) throw new BusinessException("该用户存在历史考核数据,不能删除");
         userMapper.deleteById(id);
     }
 
+    private void validateSuperiorAssignment(Long userId, Long superiorId) {
+        if (superiorId == null || superiorId <= 0) return;
+        if (Objects.equals(userId, superiorId)) throw new BusinessException("不能将自己设为直属上级");
+        if (userMapper.selectById(superiorId) == null) throw new BusinessException(404, "直属上级不存在");
+        Set<Long> visited = new HashSet<>();
+        Long cursor = superiorId;
+        while (cursor != null && cursor > 0) {
+            if (!visited.add(cursor)) throw new BusinessException("直属上级关系存在循环");
+            if (Objects.equals(cursor, userId)) throw new BusinessException("直属上级关系存在循环");
+            SysUser superior = userMapper.selectById(cursor);
+            if (superior == null) return;
+            cursor = superior.getSuperiorId();
+        }
+    }
+
+    private void ensureMoreThanOneSuperAdmin() {
+        long adminCount = userMapper.selectCount(
+                new LambdaQueryWrapper<SysUser>().eq(SysUser::getRole, UserRole.SUPER_ADMIN));
+        if (adminCount <= 1) throw new BusinessException("至少保留一个超级管理员");
+    }
+
     private Map<Long, SysUser> getLeaderMap() {
         List<SysUser> allUsers = userMapper.selectList(null);
         return allUsers.stream().collect(Collectors.toMap(SysUser::getId, u -> u));

+ 72 - 3
backend/src/main/java/com/yimeng/okr/service/PerformanceService.java

@@ -11,6 +11,8 @@ import com.yimeng.okr.event.NotificationEvent;
 import com.yimeng.okr.exception.BusinessException;
 import com.yimeng.okr.mapper.*;
 import java.time.LocalDateTime;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.context.ApplicationEventPublisher;
@@ -28,8 +30,11 @@ public class PerformanceService {
     private final PerformanceFeedbackMapper feedbackMapper;
     private final AssessmentPeriodMapper periodMapper;
     private final SysUserMapper userMapper;
+    private final AuditCorrectionMapper auditCorrectionMapper;
     private final OkrService okrService;
+    private final AuthorizationService authorizationService;
     private final ApplicationEventPublisher publisher;
+    private final ObjectMapper objectMapper = new ObjectMapper();
 
     // Self evaluation
 
@@ -37,6 +42,7 @@ public class PerformanceService {
     public PerformanceScore submitSelfScore(Long periodId, Long userId, Double okrScore, Double kpiScore,
                                             Double bonus, Double penalty, String commentsJson) {
         AssessmentPeriod period = checkPeriodStatus(periodId);
+        validateScoreParts(okrScore, kpiScore, bonus, penalty);
 
         PerformanceScore existing = findExisting(periodId, userId, ScoreType.SELF);
         if (existing != null) {
@@ -66,6 +72,7 @@ public class PerformanceService {
                                                  Double okrScore, Double kpiScore,
                                                  Double bonus, Double penalty, String commentsJson) {
         AssessmentPeriod period = checkPeriodStatus(periodId);
+        validateScoreParts(okrScore, kpiScore, bonus, penalty);
 
         // Permission: evaluator must be the direct superior
         SysUser targetUser = userMapper.selectById(userId);
@@ -121,7 +128,8 @@ public class PerformanceService {
 
     // Queries with OKR records (same-screen display)
 
-    public ScoringDto getScoringPage(Long periodId, Long userId) {
+    public ScoringDto getScoringPage(Long periodId, Long userId, Long actorId) {
+        authorizationService.requireCanViewUserData(actorId, userId);
         AssessmentPeriod period = periodMapper.selectById(periodId);
         if (period == null) throw new BusinessException(404, "考核周期不存在");
         SysUser user = userMapper.selectById(userId);
@@ -158,11 +166,48 @@ public class PerformanceService {
     public List<ScoringDto> getSubordinateScores(Long periodId, Long superiorId) {
         List<SysUser> subs = userMapper.selectList(
                 new LambdaQueryWrapper<SysUser>().eq(SysUser::getSuperiorId, superiorId));
-        return subs.stream().map(sub -> getScoringPage(periodId, sub.getId())).toList();
+        return subs.stream().map(sub -> getScoringPage(periodId, sub.getId(), superiorId)).toList();
     }
 
     public ScoringDto getMyResult(Long periodId, Long userId) {
-        return getScoringPage(periodId, userId);
+        return getScoringPage(periodId, userId, userId);
+    }
+
+    @Transactional
+    public PerformanceScore correctFinalScore(Long scoreId, Long operatorId, Double okrScore, Double kpiScore,
+                                              Double bonus, Double penalty, String commentsJson, String reason) {
+        if (!authorizationService.isHrOrAbove(operatorId)) {
+            throw new BusinessException(403, "无权限,需要HR管理员或以上权限");
+        }
+        if (reason == null || reason.trim().isEmpty()) {
+            throw new BusinessException("修正原因不能为空");
+        }
+        validateScoreParts(okrScore, kpiScore, bonus, penalty);
+        PerformanceScore score = scoreMapper.selectById(scoreId);
+        if (score == null) throw new BusinessException(404, "绩效结果不存在");
+        if (score.getType() != ScoreType.FINAL) {
+            throw new BusinessException("仅最终绩效结果允许审计修正");
+        }
+
+        AuditCorrection audit = new AuditCorrection();
+        audit.setPerformanceId(scoreId);
+        audit.setCorrectionType("FINAL_SCORE");
+        audit.setOldValuesJson(toJson(score));
+
+        score.setOkrScore(okrScore);
+        score.setKpiScore(kpiScore);
+        score.setBonus(bonus != null ? bonus : 0);
+        score.setPenalty(penalty != null ? penalty : 0);
+        score.setTotal(calculateTotal(okrScore, kpiScore, bonus, penalty));
+        score.setGrade(Grade.fromScore(score.getTotal()));
+        score.setCommentsJson(commentsJson);
+
+        audit.setNewValuesJson(toJson(score));
+        audit.setReason(reason.trim());
+        audit.setOperatedBy(operatorId);
+        auditCorrectionMapper.insert(audit);
+        scoreMapper.updateById(score);
+        return score;
     }
 
     @Transactional
@@ -197,6 +242,30 @@ public class PerformanceService {
         return Math.round(raw * 10.0) / 10.0;
     }
 
+    private void validateScoreParts(Double okrScore, Double kpiScore, Double bonus, Double penalty) {
+        requireRange("OKR得分", okrScore, 0, 40);
+        requireRange("KPI得分", kpiScore, 0, 60);
+        requireRange("加分", bonus != null ? bonus : 0, 0, 20);
+        requireRange("扣分", penalty != null ? penalty : 0, 0, 20);
+        double total = calculateTotal(okrScore, kpiScore, bonus, penalty);
+        requireRange("总分", total, 0, 100);
+    }
+
+    private void requireRange(String label, Double value, double min, double max) {
+        double v = value != null ? value : 0;
+        if (v < min || v > max) {
+            throw new BusinessException(label + "必须在" + min + "~" + max + "之间");
+        }
+    }
+
+    private String toJson(Object value) {
+        try {
+            return objectMapper.writeValueAsString(value);
+        } catch (JsonProcessingException e) {
+            throw new BusinessException("审计数据序列化失败");
+        }
+    }
+
     private PerformanceScore findExisting(Long periodId, Long userId, ScoreType type) {
         return scoreMapper.selectOne(
                 new LambdaQueryWrapper<PerformanceScore>()

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
backend/src/main/resources/static/assets/AlignmentView-BRySBaI5.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
backend/src/main/resources/static/assets/AlignmentView-NAl29DoE.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
backend/src/main/resources/static/assets/DimensionConfigView-CLFYyX-V.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
backend/src/main/resources/static/assets/DimensionConfigView-Ck7C63qq.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
backend/src/main/resources/static/assets/FeedbackView-D51Aoljt.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
backend/src/main/resources/static/assets/HistoryView-Bf9VEKrJ.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
backend/src/main/resources/static/assets/KpiTemplateView-Dbzxvtjx.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
backend/src/main/resources/static/assets/LogView-C7C2nywu.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
backend/src/main/resources/static/assets/LogView-olsuDgTD.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
backend/src/main/resources/static/assets/LoginView-66itVib7.css


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
backend/src/main/resources/static/assets/LoginView-BJPGcjE3.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
backend/src/main/resources/static/assets/LoginView-BQP2ttAi.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
backend/src/main/resources/static/assets/LoginView-sBh933FF.css


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 1
backend/src/main/resources/static/assets/MainLayout-CE96Whde.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
backend/src/main/resources/static/assets/MyOkrView-D9R_i1vF.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
backend/src/main/resources/static/assets/MyResultView-DFeke8dT.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
backend/src/main/resources/static/assets/NotificationView-lsYMS3tn.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
backend/src/main/resources/static/assets/OrgView-Dq2k16Fq.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
backend/src/main/resources/static/assets/PeriodBanner-CBesIvM9.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
backend/src/main/resources/static/assets/PeriodListView-Dh3VFwV9.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
backend/src/main/resources/static/assets/ScoringEditView-CM_Lq_SZ.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
backend/src/main/resources/static/assets/ScoringListView-DEzdjH5N.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
backend/src/main/resources/static/assets/ScoringListView-DwES_S9I.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 0
backend/src/main/resources/static/assets/TeamOkrView-DRyKaGxV.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 1
backend/src/main/resources/static/assets/TeamOkrView-nqCYUcoC.js


+ 0 - 1
backend/src/main/resources/static/assets/auth-BfQBitXm.js

@@ -1 +0,0 @@
-import{A as t}from"./index-BwYZ2HSO.js";import{authApi as o}from"./index-Dhl0yoWK.js";const n=t("auth",{state:()=>({user:null,accessToken:localStorage.getItem("accessToken")||"",refreshToken:localStorage.getItem("refreshToken")||""}),getters:{isLoggedIn:s=>!!s.accessToken,isAdmin:s=>{var e;return((e=s.user)==null?void 0:e.role)==="SUPER_ADMIN"},isHrAdminOrAbove:s=>{var e;return["SUPER_ADMIN","HR_ADMIN"].includes((e=s.user)==null?void 0:e.role)},isDeptLeaderOrAbove:s=>{var e;return["SUPER_ADMIN","HR_ADMIN","DEPT_LEADER"].includes((e=s.user)==null?void 0:e.role)},roleLabel:s=>{var r;return{EMPLOYEE:"员工",TEAM_LEADER:"团队主管",DEPT_LEADER:"部门负责人",HR_ADMIN:"HR管理员",SUPER_ADMIN:"管理员"}[(r=s.user)==null?void 0:r.role]||"员工"},roleColor:s=>{var r;return{SUPER_ADMIN:"#f56c6c",HR_ADMIN:"#e6a23c",DEPT_LEADER:"#8b5cf6",TEAM_LEADER:"#3ad4d8",EMPLOYEE:"#409eff"}[(r=s.user)==null?void 0:r.role]||"#409eff"}},actions:{async login(s,e){const r=await o.login({username:s,password:e});this.accessToken=r.accessToken,this.refreshToken=r.refreshToken,this.user=r.user,localStorage.setItem("accessToken",r.accessToken),localStorage.setItem("refreshToken",r.refreshToken)},async register(s){const e=await o.register(s);this.accessToken=e.accessToken,this.refreshToken=e.refreshToken,this.user=e.user,localStorage.setItem("accessToken",e.accessToken),localStorage.setItem("refreshToken",e.refreshToken)},async fetchUser(){try{this.user=await o.me()}catch{this.logout()}},logout(){this.user=null,this.accessToken="",this.refreshToken="",localStorage.removeItem("accessToken"),localStorage.removeItem("refreshToken")}}});export{n as u};

+ 1 - 0
backend/src/main/resources/static/assets/auth-Cv0WaWtQ.js

@@ -0,0 +1 @@
+import{A as t}from"./index-DzdxblQI.js";import{authApi as r}from"./index-BrgsZ4qv.js";const n=t("auth",{state:()=>({user:null,accessToken:localStorage.getItem("accessToken")||"",refreshToken:localStorage.getItem("refreshToken")||""}),getters:{isLoggedIn:e=>!!e.accessToken,isAdmin:e=>{var s;return((s=e.user)==null?void 0:s.role)==="SUPER_ADMIN"},isHrAdminOrAbove:e=>{var s;return["SUPER_ADMIN","HR_ADMIN"].includes((s=e.user)==null?void 0:s.role)},isDeptLeaderOrAbove:e=>{var s;return["SUPER_ADMIN","HR_ADMIN","DEPT_LEADER"].includes((s=e.user)==null?void 0:s.role)},roleLabel:e=>{var o;return{EMPLOYEE:"员工",TEAM_LEADER:"团队主管",DEPT_LEADER:"部门负责人",HR_ADMIN:"HR管理员",SUPER_ADMIN:"管理员"}[(o=e.user)==null?void 0:o.role]||"员工"},roleColor:e=>{var o;return{SUPER_ADMIN:"#f56c6c",HR_ADMIN:"#e6a23c",DEPT_LEADER:"#8b5cf6",TEAM_LEADER:"#3ad4d8",EMPLOYEE:"#409eff"}[(o=e.user)==null?void 0:o.role]||"#409eff"}},actions:{async login(e,s){const o=await r.login({username:e,password:s});this.accessToken=o.accessToken,this.refreshToken=o.refreshToken,this.user=o.user,localStorage.setItem("accessToken",o.accessToken),localStorage.setItem("refreshToken",o.refreshToken)},async fetchUser(){try{this.user=await r.me()}catch{this.logout()}},logout(){this.user=null,this.accessToken="",this.refreshToken="",localStorage.removeItem("accessToken"),localStorage.removeItem("refreshToken")}}});export{n as u};

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
backend/src/main/resources/static/assets/index-BrgsZ4qv.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
backend/src/main/resources/static/assets/index-DzdxblQI.js


+ 1 - 1
backend/src/main/resources/static/index.html

@@ -5,7 +5,7 @@
   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
   <title>医梦AI · OKR绩效</title>
   <link rel="icon" type="image/png" href="/logo-small.png" />
-  <script type="module" crossorigin src="/assets/index-BwYZ2HSO.js"></script>
+  <script type="module" crossorigin src="/assets/index-DzdxblQI.js"></script>
   <link rel="stylesheet" crossorigin href="/assets/index-CT17CCWP.css">
 </head>
 <body>

+ 68 - 0
backend/src/test/java/com/yimeng/okr/service/AuthorizationServiceTest.java

@@ -0,0 +1,68 @@
+package com.yimeng.okr.service;
+
+import com.yimeng.okr.entity.SysDepartment;
+import com.yimeng.okr.entity.SysUser;
+import com.yimeng.okr.enums.UserRole;
+import com.yimeng.okr.exception.BusinessException;
+import com.yimeng.okr.mapper.SysDepartmentMapper;
+import com.yimeng.okr.mapper.SysUserMapper;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class AuthorizationServiceTest {
+
+    @Mock SysUserMapper userMapper;
+    @Mock SysDepartmentMapper departmentMapper;
+    @InjectMocks AuthorizationService authorizationService;
+
+    @Test
+    void requireCanViewUserDataRejectsPeerAccess() {
+        when(userMapper.selectById(1L)).thenReturn(user(1L, UserRole.EMPLOYEE, 10L, null));
+        when(userMapper.selectById(2L)).thenReturn(user(2L, UserRole.EMPLOYEE, 10L, 9L));
+
+        BusinessException ex = assertThrows(BusinessException.class,
+                () -> authorizationService.requireCanViewUserData(1L, 2L));
+
+        assertEquals(403, ex.getCode());
+    }
+
+    @Test
+    void requireCanViewUserDataAllowsDirectSuperiorAndHr() {
+        when(userMapper.selectById(7L)).thenReturn(user(7L, UserRole.TEAM_LEADER, 10L, null));
+        when(userMapper.selectById(8L)).thenReturn(user(8L, UserRole.EMPLOYEE, 10L, 7L));
+        when(userMapper.selectById(9L)).thenReturn(user(9L, UserRole.HR_ADMIN, null, null));
+
+        assertDoesNotThrow(() -> authorizationService.requireCanViewUserData(7L, 8L));
+        assertDoesNotThrow(() -> authorizationService.requireCanViewUserData(9L, 8L));
+    }
+
+    @Test
+    void requireCanManageUserDataAllowsDepartmentLeader() {
+        when(userMapper.selectById(3L)).thenReturn(user(3L, UserRole.DEPT_LEADER, 20L, null));
+        when(userMapper.selectById(4L)).thenReturn(user(4L, UserRole.EMPLOYEE, 20L, 5L));
+        SysDepartment dept = new SysDepartment();
+        dept.setId(20L);
+        dept.setLeaderId(3L);
+        when(departmentMapper.selectById(20L)).thenReturn(dept);
+
+        assertDoesNotThrow(() -> authorizationService.requireCanManageUserData(3L, 4L));
+    }
+
+    private SysUser user(Long id, UserRole role, Long deptId, Long superiorId) {
+        SysUser user = new SysUser();
+        user.setId(id);
+        user.setUsername("u" + id);
+        user.setRealName("用户" + id);
+        user.setRole(role);
+        user.setDepartmentId(deptId);
+        user.setSuperiorId(superiorId);
+        return user;
+    }
+}

+ 56 - 0
backend/src/test/java/com/yimeng/okr/service/OkrServiceTest.java

@@ -0,0 +1,56 @@
+package com.yimeng.okr.service;
+
+import com.yimeng.okr.entity.AssessmentPeriod;
+import com.yimeng.okr.entity.KrKeyResult;
+import com.yimeng.okr.entity.OkrObjective;
+import com.yimeng.okr.enums.KrStatus;
+import com.yimeng.okr.enums.PeriodStatus;
+import com.yimeng.okr.exception.BusinessException;
+import com.yimeng.okr.mapper.*;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.context.ApplicationEventPublisher;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class OkrServiceTest {
+
+    @Mock OkrObjectiveMapper objectiveMapper;
+    @Mock KrKeyResultMapper krMapper;
+    @Mock KrProgressLogMapper progressLogMapper;
+    @Mock AssessmentPeriodMapper periodMapper;
+    @Mock SysUserMapper userMapper;
+    @Mock SysDepartmentMapper departmentMapper;
+    @Mock AuthorizationService authorizationService;
+    @Mock ApplicationEventPublisher publisher;
+    @InjectMocks OkrService okrService;
+
+    @Test
+    void updateKrProgressRejectsUnrelatedUser() {
+        KrKeyResult kr = new KrKeyResult();
+        kr.setId(5L);
+        kr.setObjectiveId(6L);
+        kr.setAssignedTo(null);
+        when(krMapper.selectById(5L)).thenReturn(kr);
+        OkrObjective obj = new OkrObjective();
+        obj.setId(6L);
+        obj.setUserId(2L);
+        obj.setPeriodId(1L);
+        when(objectiveMapper.selectById(6L)).thenReturn(obj);
+        AssessmentPeriod period = new AssessmentPeriod();
+        period.setId(1L);
+        period.setStatus(PeriodStatus.EXECUTING);
+        when(periodMapper.selectById(1L)).thenReturn(period);
+
+        BusinessException ex = assertThrows(BusinessException.class,
+                () -> okrService.updateKrProgress(5L, 50.0, KrStatus.IN_PROGRESS, "", 9L));
+
+        assertEquals(403, ex.getCode());
+        verify(progressLogMapper, never()).insert(any());
+    }
+}

+ 64 - 0
backend/src/test/java/com/yimeng/okr/service/OrganizationServiceTest.java

@@ -0,0 +1,64 @@
+package com.yimeng.okr.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.yimeng.okr.entity.SysUser;
+import com.yimeng.okr.enums.UserRole;
+import com.yimeng.okr.exception.BusinessException;
+import com.yimeng.okr.mapper.*;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class OrganizationServiceTest {
+
+    @Mock SysDepartmentMapper departmentMapper;
+    @Mock SysUserMapper userMapper;
+    @Mock OkrObjectiveMapper objectiveMapper;
+    @Mock PerformanceScoreMapper scoreMapper;
+    @Mock PerformanceFeedbackMapper feedbackMapper;
+    @Mock KpiConfigMapper kpiConfigMapper;
+    @Mock PasswordEncoder passwordEncoder;
+    @InjectMocks OrganizationService organizationService;
+
+    @Test
+    void updateUserSuperiorRejectsCycle() {
+        when(userMapper.selectById(1L)).thenReturn(user(1L, 2L));
+        when(userMapper.selectById(3L)).thenReturn(user(3L, 4L));
+        when(userMapper.selectById(4L)).thenReturn(user(4L, 1L));
+
+        BusinessException ex = assertThrows(BusinessException.class,
+                () -> organizationService.updateUserSuperior(1L, 3L));
+
+        assertTrue(ex.getMessage().contains("循环"));
+    }
+
+    @Test
+    void deleteUserRejectsUsersWithHistory() {
+        when(userMapper.selectById(2L)).thenReturn(user(2L, null));
+        when(userMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(0L);
+        when(objectiveMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(1L);
+
+        BusinessException ex = assertThrows(BusinessException.class,
+                () -> organizationService.deleteUser(2L));
+
+        assertTrue(ex.getMessage().contains("历史"));
+    }
+
+    private SysUser user(Long id, Long superiorId) {
+        SysUser user = new SysUser();
+        user.setId(id);
+        user.setUsername("u" + id);
+        user.setRealName("用户" + id);
+        user.setRole(UserRole.EMPLOYEE);
+        user.setSuperiorId(superiorId);
+        return user;
+    }
+}

+ 89 - 0
backend/src/test/java/com/yimeng/okr/service/PerformanceServiceTest.java

@@ -0,0 +1,89 @@
+package com.yimeng.okr.service;
+
+import com.yimeng.okr.entity.AssessmentPeriod;
+import com.yimeng.okr.entity.AuditCorrection;
+import com.yimeng.okr.entity.PerformanceScore;
+import com.yimeng.okr.enums.PeriodStatus;
+import com.yimeng.okr.enums.ScoreType;
+import com.yimeng.okr.exception.BusinessException;
+import com.yimeng.okr.mapper.*;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.context.ApplicationEventPublisher;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class PerformanceServiceTest {
+
+    @Mock PerformanceScoreMapper scoreMapper;
+    @Mock PerformanceFeedbackMapper feedbackMapper;
+    @Mock AssessmentPeriodMapper periodMapper;
+    @Mock SysUserMapper userMapper;
+    @Mock AuditCorrectionMapper auditCorrectionMapper;
+    @Mock OkrService okrService;
+    @Mock AuthorizationService authorizationService;
+    @Mock ApplicationEventPublisher publisher;
+    @InjectMocks PerformanceService performanceService;
+
+    @Test
+    void submitSelfScoreRejectsOkrScoreAboveForty() {
+        when(periodMapper.selectById(1L)).thenReturn(period());
+
+        BusinessException ex = assertThrows(BusinessException.class,
+                () -> performanceService.submitSelfScore(1L, 2L, 41.0, 50.0, 0.0, 0.0, "{}"));
+
+        assertTrue(ex.getMessage().contains("OKR"));
+        verify(scoreMapper, never()).insert(any());
+    }
+
+    @Test
+    void getScoringPageRequiresViewPermission() {
+        doThrow(new BusinessException(403, "无权查看"))
+                .when(authorizationService).requireCanViewUserData(9L, 2L);
+
+        BusinessException ex = assertThrows(BusinessException.class,
+                () -> performanceService.getScoringPage(1L, 2L, 9L));
+
+        assertEquals(403, ex.getCode());
+    }
+
+    @Test
+    void correctFinalScoreWritesAuditBeforeUpdatingScore() {
+        PerformanceScore finalScore = new PerformanceScore();
+        finalScore.setId(11L);
+        finalScore.setPeriodId(1L);
+        finalScore.setUserId(2L);
+        finalScore.setEvaluatorId(3L);
+        finalScore.setType(ScoreType.FINAL);
+        finalScore.setOkrScore(30.0);
+        finalScore.setKpiScore(50.0);
+        finalScore.setBonus(0.0);
+        finalScore.setPenalty(0.0);
+        finalScore.setTotal(80.0);
+        when(scoreMapper.selectById(11L)).thenReturn(finalScore);
+        when(authorizationService.isHrOrAbove(99L)).thenReturn(true);
+
+        PerformanceScore corrected = performanceService.correctFinalScore(
+                11L, 99L, 35.0, 55.0, 0.0, 0.0, "{\"note\":\"修正\"}", "复核通过");
+
+        assertEquals(90.0, corrected.getTotal());
+        verify(auditCorrectionMapper).insert(argThat(audit ->
+                audit instanceof AuditCorrection
+                        && ((AuditCorrection) audit).getPerformanceId().equals(11L)
+                        && ((AuditCorrection) audit).getOperatedBy().equals(99L)));
+        verify(scoreMapper).updateById(finalScore);
+    }
+
+    private AssessmentPeriod period() {
+        AssessmentPeriod period = new AssessmentPeriod();
+        period.setId(1L);
+        period.setStatus(PeriodStatus.ASSESSING);
+        return period;
+    }
+}

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
frontend/dist/assets/AlignmentView-BRySBaI5.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
frontend/dist/assets/AlignmentView-NAl29DoE.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
frontend/dist/assets/DimensionConfigView-CLFYyX-V.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
frontend/dist/assets/DimensionConfigView-Ck7C63qq.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
frontend/dist/assets/FeedbackView-D51Aoljt.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
frontend/dist/assets/HistoryView-Bf9VEKrJ.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
frontend/dist/assets/KpiTemplateView-Dbzxvtjx.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
frontend/dist/assets/LogView-C7C2nywu.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
frontend/dist/assets/LogView-olsuDgTD.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
frontend/dist/assets/LoginView-66itVib7.css


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
frontend/dist/assets/LoginView-BJPGcjE3.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
frontend/dist/assets/LoginView-BQP2ttAi.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
frontend/dist/assets/LoginView-sBh933FF.css


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 1
frontend/dist/assets/MainLayout-CE96Whde.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
frontend/dist/assets/MyOkrView-D9R_i1vF.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
frontend/dist/assets/MyResultView-DFeke8dT.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
frontend/dist/assets/NotificationView-lsYMS3tn.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
frontend/dist/assets/OrgView-Dq2k16Fq.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
frontend/dist/assets/PeriodBanner-CBesIvM9.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
frontend/dist/assets/PeriodListView-Dh3VFwV9.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
frontend/dist/assets/ScoringEditView-CM_Lq_SZ.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
frontend/dist/assets/ScoringListView-DEzdjH5N.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
frontend/dist/assets/ScoringListView-DwES_S9I.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 0
frontend/dist/assets/TeamOkrView-DRyKaGxV.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 1
frontend/dist/assets/TeamOkrView-nqCYUcoC.js


+ 0 - 1
frontend/dist/assets/auth-BfQBitXm.js

@@ -1 +0,0 @@
-import{A as t}from"./index-BwYZ2HSO.js";import{authApi as o}from"./index-Dhl0yoWK.js";const n=t("auth",{state:()=>({user:null,accessToken:localStorage.getItem("accessToken")||"",refreshToken:localStorage.getItem("refreshToken")||""}),getters:{isLoggedIn:s=>!!s.accessToken,isAdmin:s=>{var e;return((e=s.user)==null?void 0:e.role)==="SUPER_ADMIN"},isHrAdminOrAbove:s=>{var e;return["SUPER_ADMIN","HR_ADMIN"].includes((e=s.user)==null?void 0:e.role)},isDeptLeaderOrAbove:s=>{var e;return["SUPER_ADMIN","HR_ADMIN","DEPT_LEADER"].includes((e=s.user)==null?void 0:e.role)},roleLabel:s=>{var r;return{EMPLOYEE:"员工",TEAM_LEADER:"团队主管",DEPT_LEADER:"部门负责人",HR_ADMIN:"HR管理员",SUPER_ADMIN:"管理员"}[(r=s.user)==null?void 0:r.role]||"员工"},roleColor:s=>{var r;return{SUPER_ADMIN:"#f56c6c",HR_ADMIN:"#e6a23c",DEPT_LEADER:"#8b5cf6",TEAM_LEADER:"#3ad4d8",EMPLOYEE:"#409eff"}[(r=s.user)==null?void 0:r.role]||"#409eff"}},actions:{async login(s,e){const r=await o.login({username:s,password:e});this.accessToken=r.accessToken,this.refreshToken=r.refreshToken,this.user=r.user,localStorage.setItem("accessToken",r.accessToken),localStorage.setItem("refreshToken",r.refreshToken)},async register(s){const e=await o.register(s);this.accessToken=e.accessToken,this.refreshToken=e.refreshToken,this.user=e.user,localStorage.setItem("accessToken",e.accessToken),localStorage.setItem("refreshToken",e.refreshToken)},async fetchUser(){try{this.user=await o.me()}catch{this.logout()}},logout(){this.user=null,this.accessToken="",this.refreshToken="",localStorage.removeItem("accessToken"),localStorage.removeItem("refreshToken")}}});export{n as u};

+ 1 - 0
frontend/dist/assets/auth-Cv0WaWtQ.js

@@ -0,0 +1 @@
+import{A as t}from"./index-DzdxblQI.js";import{authApi as r}from"./index-BrgsZ4qv.js";const n=t("auth",{state:()=>({user:null,accessToken:localStorage.getItem("accessToken")||"",refreshToken:localStorage.getItem("refreshToken")||""}),getters:{isLoggedIn:e=>!!e.accessToken,isAdmin:e=>{var s;return((s=e.user)==null?void 0:s.role)==="SUPER_ADMIN"},isHrAdminOrAbove:e=>{var s;return["SUPER_ADMIN","HR_ADMIN"].includes((s=e.user)==null?void 0:s.role)},isDeptLeaderOrAbove:e=>{var s;return["SUPER_ADMIN","HR_ADMIN","DEPT_LEADER"].includes((s=e.user)==null?void 0:s.role)},roleLabel:e=>{var o;return{EMPLOYEE:"员工",TEAM_LEADER:"团队主管",DEPT_LEADER:"部门负责人",HR_ADMIN:"HR管理员",SUPER_ADMIN:"管理员"}[(o=e.user)==null?void 0:o.role]||"员工"},roleColor:e=>{var o;return{SUPER_ADMIN:"#f56c6c",HR_ADMIN:"#e6a23c",DEPT_LEADER:"#8b5cf6",TEAM_LEADER:"#3ad4d8",EMPLOYEE:"#409eff"}[(o=e.user)==null?void 0:o.role]||"#409eff"}},actions:{async login(e,s){const o=await r.login({username:e,password:s});this.accessToken=o.accessToken,this.refreshToken=o.refreshToken,this.user=o.user,localStorage.setItem("accessToken",o.accessToken),localStorage.setItem("refreshToken",o.refreshToken)},async fetchUser(){try{this.user=await r.me()}catch{this.logout()}},logout(){this.user=null,this.accessToken="",this.refreshToken="",localStorage.removeItem("accessToken"),localStorage.removeItem("refreshToken")}}});export{n as u};

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
frontend/dist/assets/index-BrgsZ4qv.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
frontend/dist/assets/index-DzdxblQI.js


+ 1 - 1
frontend/dist/index.html

@@ -5,7 +5,7 @@
   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
   <title>医梦AI · OKR绩效</title>
   <link rel="icon" type="image/png" href="/logo-small.png" />
-  <script type="module" crossorigin src="/assets/index-BwYZ2HSO.js"></script>
+  <script type="module" crossorigin src="/assets/index-DzdxblQI.js"></script>
   <link rel="stylesheet" crossorigin href="/assets/index-CT17CCWP.css">
 </head>
 <body>

+ 0 - 1
frontend/src/api/index.js

@@ -39,7 +39,6 @@ export default api
 // Auth API
 export const authApi = {
   login: data => api.post('/auth/login', data).then(r => r.data),
-  register: data => api.post('/auth/register', data).then(r => r.data),
   me: () => api.get('/auth/me').then(r => r.data),
   changePassword: data => api.put('/auth/password', data)
 }

+ 0 - 8
frontend/src/stores/auth.js

@@ -30,14 +30,6 @@ export const useAuthStore = defineStore('auth', {
       localStorage.setItem('accessToken', result.accessToken)
       localStorage.setItem('refreshToken', result.refreshToken)
     },
-    async register(data) {
-      const result = await authApi.register(data)
-      this.accessToken = result.accessToken
-      this.refreshToken = result.refreshToken
-      this.user = result.user
-      localStorage.setItem('accessToken', result.accessToken)
-      localStorage.setItem('refreshToken', result.refreshToken)
-    },
     async fetchUser() {
       try {
         this.user = await authApi.me()

+ 7 - 27
frontend/src/views/auth/LoginView.vue

@@ -11,7 +11,7 @@
     <section class="internal-login__panel med-panel">
       <div class="login-panel__header">
         <span class="login-panel__eyebrow">Internal Access</span>
-        <h2>{{ isRegister ? '注册新账号' : '账号登录' }}</h2>
+        <h2>账号登录</h2>
       </div>
       <el-form :model="form" :rules="rules" ref="formRef" @submit.prevent="handleSubmit" class="login-form">
         <el-form-item prop="username">
@@ -32,27 +32,17 @@
             class="custom-input"
           />
         </el-form-item>
-        <el-form-item v-if="isRegister" prop="realName">
-          <el-input
-            v-model="form.realName"
-            placeholder="真实姓名"
-            size="large"
-            class="custom-input"
-          />
-        </el-form-item>
         <el-button
           class="login-btn"
           size="large"
           native-type="submit"
           :loading="loading"
         >
-          {{ isRegister ? '注册' : '登录' }}
+          登录
         </el-button>
       </el-form>
       <div class="login-panel__footer">
-        <button type="button" class="switch-link" @click="isRegister = !isRegister">
-          {{ isRegister ? '已有账号?立即登录' : '创建新账号' }}
-        </button>
+        <span class="login-help">账号由 HR 管理员创建,如需开通请联系组织管理员</span>
       </div>
     </section>
   </div>
@@ -67,13 +57,11 @@ const router = useRouter()
 const auth = useAuthStore()
 const formRef = ref(null)
 const loading = ref(false)
-const isRegister = ref(false)
 
-const form = reactive({ username: '', password: '', realName: '' })
+const form = reactive({ username: '', password: '' })
 const rules = {
   username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
-  password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
-  realName: [{ required: true, message: '请输入姓名', trigger: 'blur' }]
+  password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
 }
 
 async function handleSubmit() {
@@ -81,11 +69,7 @@ async function handleSubmit() {
   if (!valid) return
   loading.value = true
   try {
-    if (isRegister.value) {
-      await auth.register({ username: form.username, password: form.password, realName: form.realName })
-    } else {
-      await auth.login(form.username, form.password)
-    }
+    await auth.login(form.username, form.password)
     router.push('/')
   } finally {
     loading.value = false
@@ -199,15 +183,11 @@ async function handleSubmit() {
   text-align: center;
   padding-top: 14px;
 }
-.switch-link {
-  background: none;
-  border: none;
+.login-help {
   color: var(--color-text-muted);
   font-size: 13px;
-  cursor: pointer;
   letter-spacing: 0;
 }
-.switch-link:hover { color: var(--color-accent-strong); }
 
 @media (max-width: 900px) {
   .internal-login {

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels