diff --git a/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ErrorCodeConstants.java b/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ErrorCodeConstants.java index 2c1f1e7..6fa4fd8 100644 --- a/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ErrorCodeConstants.java +++ b/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ErrorCodeConstants.java @@ -259,4 +259,25 @@ public interface ErrorCodeConstants { ErrorCode WORK_REPORT_PERIOD_DUPLICATE = new ErrorCode(1_008_010_013, "当前周期的工作报告已存在,请勿重复创建"); ErrorCode WORK_REPORT_APPROVAL_RECORD_NOT_EXISTS = new ErrorCode(1_008_010_014, "工作报告审核记录不存在"); ErrorCode WORK_REPORT_PROJECT_OWNER_ONLY = new ErrorCode(1_008_010_015, "仅项目负责人可创建或维护该项目的项目半月报"); + + // ========== 绩效管理 1_008_011_xxx ========== + ErrorCode PERFORMANCE_TEMPLATE_NOT_EXISTS = new ErrorCode(1_008_011_001, "绩效模板不存在"); + ErrorCode PERFORMANCE_TEMPLATE_CURRENT_NOT_EXISTS = new ErrorCode(1_008_011_002, "当前没有已启用的绩效模板"); + ErrorCode PERFORMANCE_SHEET_NOT_EXISTS = new ErrorCode(1_008_011_003, "绩效表不存在"); + ErrorCode PERFORMANCE_SHEET_DUPLICATE = new ErrorCode(1_008_011_004, "该员工当前月份已存在绩效表"); + ErrorCode PERFORMANCE_SHEET_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_011_005, "当前绩效表状态不允许编辑"); + ErrorCode PERFORMANCE_SHEET_STATUS_NOT_ALLOW_DELETE = new ErrorCode(1_008_011_006, "仅待发送状态绩效表允许删除"); + ErrorCode PERFORMANCE_SHEET_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_011_007, "当前绩效表为「{}」状态,不支持「{}」操作"); + ErrorCode PERFORMANCE_SHEET_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_011_008, "绩效表已被其他人更新,请刷新后重试"); + ErrorCode PERFORMANCE_SHEET_MANAGER_ONLY = new ErrorCode(1_008_011_009, "仅直属上级可维护该员工绩效表"); + ErrorCode PERFORMANCE_SHEET_EMPLOYEE_ONLY = new ErrorCode(1_008_011_010, "仅被考核员工可执行该操作"); + ErrorCode PERFORMANCE_SHEET_READ_FORBIDDEN = new ErrorCode(1_008_011_011, "无权查看该绩效表"); + ErrorCode PERFORMANCE_SHEET_FILE_REQUIRED = new ErrorCode(1_008_011_012, "请先保存绩效 Excel 后再发送"); + ErrorCode PERFORMANCE_SHEET_REJECT_REASON_REQUIRED = new ErrorCode(1_008_011_013, "退回绩效表必须填写原因"); + ErrorCode PERFORMANCE_EMPLOYEE_INVALID = new ErrorCode(1_008_011_014, "被考核员工不是有效系统用户"); + ErrorCode PERFORMANCE_DIRECT_MANAGER_NOT_EXISTS = new ErrorCode(1_008_011_015, "被考核员工不存在生效中的直属上级"); + ErrorCode PERFORMANCE_FILE_NOT_EXISTS = new ErrorCode(1_008_011_016, "绩效 Excel 文件不存在"); + ErrorCode PERFORMANCE_SHEET_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_011_017, "绩效表状态定义不存在或已停用"); + ErrorCode PERFORMANCE_SHEET_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_011_018, "「{}」操作必须填写原因"); + ErrorCode PERFORMANCE_EMPLOYEE_DEPT_INVALID = new ErrorCode(1_008_011_019, "被考核员工部门信息不完整"); } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/PerformanceConstants.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/PerformanceConstants.java new file mode 100644 index 0000000..a61992c --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/PerformanceConstants.java @@ -0,0 +1,39 @@ +package com.njcn.rdms.module.project.constant; + +/** + * 绩效管理常量。 + */ +public final class PerformanceConstants { + + private PerformanceConstants() { + } + + public static final String STATUS_OBJECT_TYPE = "performance_sheet"; + + public static final String STATUS_DRAFT = "draft"; + public static final String STATUS_SENT = "sent"; + public static final String STATUS_CONFIRMED = "confirmed"; + public static final String STATUS_REJECTED = "rejected"; + + public static final String ACTION_SEND = "send"; + public static final String ACTION_RESEND = "resend"; + public static final String ACTION_CONFIRM = "confirm"; + public static final String ACTION_REJECT = "reject"; + + public static final String REMIND_TYPE_PENDING_CONFIRM = "pending_confirm"; + public static final String REMIND_TYPE_PENDING_SEND = "pending_send"; + + public static final String ORG_TYPE_DIRECTION = "direction"; + public static final String ORG_TYPE_FUNCTION = "function"; + + public static final String PERMISSION_TEMPLATE_QUERY = "project:performance-template:query"; + public static final String PERMISSION_TEMPLATE_UPDATE = "project:performance-template:update"; + public static final String PERMISSION_SHEET_QUERY = "project:performance-sheet:query"; + public static final String PERMISSION_SHEET_CREATE = "project:performance-sheet:create"; + public static final String PERMISSION_SHEET_UPDATE = "project:performance-sheet:update"; + public static final String PERMISSION_SHEET_DELETE = "project:performance-sheet:delete"; + public static final String PERMISSION_SHEET_CONFIRM = "project:performance-sheet:confirm"; + public static final String PERMISSION_SHEET_REJECT = "project:performance-sheet:reject"; + public static final String PERMISSION_SHEET_EXPORT = "project:performance-sheet:export"; + public static final String PERMISSION_TEAM_DASHBOARD = "project:performance-sheet:team-dashboard"; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/PerformanceSheetController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/PerformanceSheetController.java new file mode 100644 index 0000000..9d89cbe --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/PerformanceSheetController.java @@ -0,0 +1,194 @@ +package com.njcn.rdms.module.project.controller.admin.performance; + +import com.njcn.rdms.framework.apilog.core.annotation.ApiAccessLog; +import com.njcn.rdms.framework.common.pojo.CommonResult; +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.module.project.constant.PerformanceConstants; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceMonthlyResultRespVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetBatchDownloadReqVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetCreateReqVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetExcelUpdateReqVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetPageReqVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetRespVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetResponseRecordRespVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetStatusActionReqVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetStatusDictRespVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetStatusLogRespVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetStatusTransitionRespVO; +import com.njcn.rdms.module.project.service.performance.PerformanceDownloadFile; +import com.njcn.rdms.module.project.service.performance.PerformanceSheetService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static com.njcn.rdms.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static com.njcn.rdms.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 绩效表") +@RestController +@RequestMapping("/project/performance-sheets") +@Validated +public class PerformanceSheetController { + + @Resource + private PerformanceSheetService performanceSheetService; + + @GetMapping("/page") + @Operation(summary = "获取绩效表分页") + @PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_QUERY + "')") + public CommonResult> page(@Valid PerformanceSheetPageReqVO reqVO) { + return success(performanceSheetService.getSheetPage(reqVO)); + } + + @GetMapping("/{id}") + @Operation(summary = "获取绩效表详情") + @PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_QUERY + "')") + public CommonResult get(@PathVariable("id") Long id) { + return success(performanceSheetService.getSheet(id)); + } + + @PostMapping + @Operation(summary = "创建绩效表") + @PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_CREATE + "')") + public CommonResult create(@Valid @RequestBody PerformanceSheetCreateReqVO reqVO) { + return success(performanceSheetService.createSheet(reqVO)); + } + + @PutMapping("/{id}/excel") + @Operation(summary = "保存绩效 Excel") + @PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_UPDATE + "')") + public CommonResult updateExcel(@PathVariable("id") Long id, + @Valid @RequestBody PerformanceSheetExcelUpdateReqVO reqVO) { + performanceSheetService.updateExcel(id, reqVO); + return success(true); + } + + @DeleteMapping("/{id}") + @Operation(summary = "删除绩效表") + @PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_DELETE + "')") + public CommonResult delete(@PathVariable("id") Long id) { + performanceSheetService.deleteSheet(id); + return success(true); + } + + @PostMapping("/{id}/send") + @Operation(summary = "发送绩效表") + @PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_UPDATE + "')") + public CommonResult send(@PathVariable("id") Long id) { + performanceSheetService.send(id); + return success(true); + } + + @PostMapping("/{id}/resend") + @Operation(summary = "重新发送绩效表") + @PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_UPDATE + "')") + public CommonResult resend(@PathVariable("id") Long id) { + performanceSheetService.resend(id); + return success(true); + } + + @PostMapping("/{id}/confirm") + @Operation(summary = "员工确认绩效表") + @PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_CONFIRM + "')") + public CommonResult confirm(@PathVariable("id") Long id, + @Valid @RequestBody(required = false) PerformanceSheetStatusActionReqVO reqVO) { + performanceSheetService.confirm(id, reqVO); + return success(true); + } + + @PostMapping("/{id}/reject") + @Operation(summary = "员工退回绩效表") + @PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_REJECT + "')") + public CommonResult reject(@PathVariable("id") Long id, + @Valid @RequestBody PerformanceSheetStatusActionReqVO reqVO) { + performanceSheetService.reject(id, reqVO); + return success(true); + } + + @GetMapping("/{id}/download") + @Operation(summary = "下载单条绩效表") + @PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_EXPORT + "')") + @ApiAccessLog(operateType = EXPORT) + public void download(@PathVariable("id") Long id, HttpServletResponse response) throws IOException { + writeDownload(response, performanceSheetService.download(id)); + } + + @PostMapping("/batch-download") + @Operation(summary = "批量下载绩效表") + @PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_EXPORT + "')") + @ApiAccessLog(operateType = EXPORT) + public void batchDownload(@Valid @RequestBody PerformanceSheetBatchDownloadReqVO reqVO, + HttpServletResponse response) throws IOException { + writeDownload(response, performanceSheetService.batchDownload(reqVO.getIds())); + } + + @GetMapping("/export") + @Operation(summary = "按筛选条件导出绩效表") + @PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_EXPORT + "')") + @ApiAccessLog(operateType = EXPORT) + public void export(@Valid PerformanceSheetPageReqVO reqVO, HttpServletResponse response) throws IOException { + writeDownload(response, performanceSheetService.exportByCondition(reqVO)); + } + + @GetMapping("/{id}/status-logs") + @Operation(summary = "获取绩效表状态日志") + @PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_QUERY + "')") + public CommonResult> statusLogs(@PathVariable("id") Long id) { + return success(performanceSheetService.getStatusLogs(id)); + } + + @GetMapping("/{id}/response-records") + @Operation(summary = "获取绩效表员工反馈历史") + @PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_QUERY + "')") + public CommonResult> responseRecords(@PathVariable("id") Long id) { + return success(performanceSheetService.getResponseRecords(id)); + } + + @GetMapping("/monthly-result") + @Operation(summary = "获取月报审批绩效结果") + @PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_QUERY + "')") + public CommonResult monthlyResult(@RequestParam("employeeId") Long employeeId, + @RequestParam("periodMonth") String periodMonth) { + return success(performanceSheetService.getMonthlyResult(employeeId, periodMonth)); + } + + @GetMapping("/status-dict") + @Operation(summary = "获取绩效表状态字典") + @PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_QUERY + "')") + public CommonResult> statusDict() { + return success(performanceSheetService.getStatusDict()); + } + + @GetMapping("/status-transitions") + @Operation(summary = "获取绩效表状态动作字典") + @PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_QUERY + "')") + public CommonResult> statusTransitions() { + return success(performanceSheetService.getStatusTransitions()); + } + + private void writeDownload(HttpServletResponse response, PerformanceDownloadFile file) throws IOException { + response.setContentType(file.contentType()); + response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + + URLEncoder.encode(file.filename(), StandardCharsets.UTF_8).replace("+", "%20")); + response.setContentLength(file.content().length); + response.getOutputStream().write(file.content()); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/PerformanceTemplateController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/PerformanceTemplateController.java new file mode 100644 index 0000000..1bec4b7 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/PerformanceTemplateController.java @@ -0,0 +1,62 @@ +package com.njcn.rdms.module.project.controller.admin.performance; + +import com.njcn.rdms.framework.common.pojo.CommonResult; +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.module.project.constant.PerformanceConstants; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceTemplatePageReqVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceTemplateRespVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceTemplateUploadReqVO; +import com.njcn.rdms.module.project.service.performance.PerformanceTemplateService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static com.njcn.rdms.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 绩效模板") +@RestController +@RequestMapping("/project/performance-templates") +@Validated +public class PerformanceTemplateController { + + @Resource + private PerformanceTemplateService performanceTemplateService; + + @GetMapping("/current") + @Operation(summary = "获取当前生效绩效模板") + @PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_TEMPLATE_QUERY + "')") + public CommonResult current() { + return success(performanceTemplateService.getCurrentTemplate()); + } + + @GetMapping("/page") + @Operation(summary = "获取绩效模板分页") + @PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_TEMPLATE_QUERY + "')") + public CommonResult> page(@Valid PerformanceTemplatePageReqVO reqVO) { + return success(performanceTemplateService.getTemplatePage(reqVO)); + } + + @PostMapping("/upload") + @Operation(summary = "保存绩效模板上传记录") + @PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_TEMPLATE_UPDATE + "')") + public CommonResult upload(@Valid @RequestBody PerformanceTemplateUploadReqVO reqVO) { + return success(performanceTemplateService.uploadTemplate(reqVO)); + } + + @PostMapping("/{id}/activate") + @Operation(summary = "启用绩效模板") + @PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_TEMPLATE_UPDATE + "')") + public CommonResult activate(@PathVariable("id") Long id) { + performanceTemplateService.activateTemplate(id); + return success(true); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/team/TeamPerformanceController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/team/TeamPerformanceController.java new file mode 100644 index 0000000..d3a0f4a --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/team/TeamPerformanceController.java @@ -0,0 +1,46 @@ +package com.njcn.rdms.module.project.controller.admin.performance.team; + +import com.njcn.rdms.framework.common.pojo.CommonResult; +import com.njcn.rdms.module.project.constant.PerformanceConstants; +import com.njcn.rdms.module.project.controller.admin.performance.team.vo.TeamPerformanceRemindReqVO; +import com.njcn.rdms.module.project.controller.admin.performance.team.vo.TeamPerformanceRemindRespVO; +import com.njcn.rdms.module.project.controller.admin.performance.team.vo.TeamPerformanceSummaryReqVO; +import com.njcn.rdms.module.project.controller.admin.performance.team.vo.TeamPerformanceSummaryRespVO; +import com.njcn.rdms.module.project.service.performance.team.TeamPerformanceService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static com.njcn.rdms.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 团队绩效") +@RestController +@RequestMapping("/project/performance-sheets/team") +@Validated +public class TeamPerformanceController { + + @Resource + private TeamPerformanceService teamPerformanceService; + + @GetMapping("/summary") + @Operation(summary = "获取团队绩效统计") + @PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_TEAM_DASHBOARD + "')") + public CommonResult getSummary(@Valid TeamPerformanceSummaryReqVO reqVO) { + return success(teamPerformanceService.getSummary(reqVO)); + } + + @PostMapping("/remind") + @Operation(summary = "催办团队绩效") + @PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_TEAM_DASHBOARD + "')") + public CommonResult remind(@Valid @RequestBody TeamPerformanceRemindReqVO reqVO) { + return success(teamPerformanceService.remind(reqVO)); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/team/vo/TeamPerformanceRemindReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/team/vo/TeamPerformanceRemindReqVO.java new file mode 100644 index 0000000..06c42be --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/team/vo/TeamPerformanceRemindReqVO.java @@ -0,0 +1,25 @@ +package com.njcn.rdms.module.project.controller.admin.performance.team.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - 团队绩效催办 Request VO") +@Data +public class TeamPerformanceRemindReqVO { + + @Schema(description = "绩效月份起始,格式 yyyy-MM;与 periodMonthEnd 构成区间筛选,两者均为空时默认当前月", example = "2026-03") + private String periodMonthStart; + + @Schema(description = "绩效月份结束,格式 yyyy-MM;与 periodMonthStart 构成区间筛选,两者均为空时默认当前月", example = "2026-05") + private String periodMonthEnd; + + @Schema(description = "催办类型:pending_confirm / pending_send", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "催办类型不能为空") + private String remindType; + + @Schema(description = "目标员工 ID 列表;为空表示全部待办") + private List userIds; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/team/vo/TeamPerformanceRemindRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/team/vo/TeamPerformanceRemindRespVO.java new file mode 100644 index 0000000..07f90b2 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/team/vo/TeamPerformanceRemindRespVO.java @@ -0,0 +1,12 @@ +package com.njcn.rdms.module.project.controller.admin.performance.team.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 团队绩效催办 Response VO") +@Data +public class TeamPerformanceRemindRespVO { + + @Schema(description = "实际催办人数", example = "3") + private Integer remindedCount; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/team/vo/TeamPerformanceSummaryReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/team/vo/TeamPerformanceSummaryReqVO.java new file mode 100644 index 0000000..a8b1183 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/team/vo/TeamPerformanceSummaryReqVO.java @@ -0,0 +1,15 @@ +package com.njcn.rdms.module.project.controller.admin.performance.team.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 团队绩效统计 Request VO") +@Data +public class TeamPerformanceSummaryReqVO { + + @Schema(description = "绩效月份起始,格式 yyyy-MM;与 periodMonthEnd 构成区间筛选,两者均为空时默认当前月", example = "2026-03") + private String periodMonthStart; + + @Schema(description = "绩效月份结束,格式 yyyy-MM;与 periodMonthStart 构成区间筛选,两者均为空时默认当前月", example = "2026-05") + private String periodMonthEnd; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/team/vo/TeamPerformanceSummaryRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/team/vo/TeamPerformanceSummaryRespVO.java new file mode 100644 index 0000000..863be2a --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/team/vo/TeamPerformanceSummaryRespVO.java @@ -0,0 +1,50 @@ +package com.njcn.rdms.module.project.controller.admin.performance.team.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 团队绩效统计 Response VO") +@Data +public class TeamPerformanceSummaryRespVO { + + private String periodMonthStart; + private String periodMonthEnd; + private Integer totalSheetCount; + private Integer pendingSendCount; + private Integer pendingConfirmCount; + private BigDecimal confirmedRate; + private List pendingSendUsers; + private List pendingConfirmUsers; + private List deptOrgAverages; + + @Data + public static class PendingSendUser { + private Long userId; + private String userNickname; + private Long managerUserId; + private String managerName; + private Long sheetId; + private String statusCode; + } + + @Data + public static class PendingConfirmUser { + private Long userId; + private String userNickname; + private Long sheetId; + private LocalDateTime sentTime; + } + + @Data + public static class DeptOrgAverage { + private Long deptId; + private String deptName; + private String deptOrgType; + private BigDecimal averageScore; + private Integer confirmedCount; + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceMonthlyResultRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceMonthlyResultRespVO.java new file mode 100644 index 0000000..1b47df0 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceMonthlyResultRespVO.java @@ -0,0 +1,19 @@ +package com.njcn.rdms.module.project.controller.admin.performance.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - 月报审批绩效结果 Response VO") +@Data +public class PerformanceMonthlyResultRespVO { + + private Long sheetId; + private String periodMonth; + private Long employeeId; + private BigDecimal actualScoreTotal; + private BigDecimal baseScoreTotal; + private BigDecimal extraScoreTotal; + private String statusCode; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceScoreCellMappingRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceScoreCellMappingRespVO.java new file mode 100644 index 0000000..7215e59 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceScoreCellMappingRespVO.java @@ -0,0 +1,18 @@ +package com.njcn.rdms.module.project.controller.admin.performance.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 绩效分数单元格映射 Response VO") +@Data +public class PerformanceScoreCellMappingRespVO { + + @Schema(description = "实际得分总计单元格", example = "J10") + private String actualScoreTotalCell; + + @Schema(description = "基础得分总计单元格", example = "K10") + private String baseScoreTotalCell; + + @Schema(description = "附加得分总计单元格", example = "L10") + private String extraScoreTotalCell; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetBatchDownloadReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetBatchDownloadReqVO.java new file mode 100644 index 0000000..7e5fa88 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetBatchDownloadReqVO.java @@ -0,0 +1,16 @@ +package com.njcn.rdms.module.project.controller.admin.performance.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - 批量下载绩效表 Request VO") +@Data +public class PerformanceSheetBatchDownloadReqVO { + + @Schema(description = "绩效表 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "绩效表 ID 不能为空") + private List ids; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetCreateReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetCreateReqVO.java new file mode 100644 index 0000000..c88e3bf --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetCreateReqVO.java @@ -0,0 +1,21 @@ +package com.njcn.rdms.module.project.controller.admin.performance.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +@Schema(description = "管理后台 - 创建绩效表 Request VO") +@Data +public class PerformanceSheetCreateReqVO { + + @Schema(description = "绩效月份,格式 yyyy-MM", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-06") + @NotBlank(message = "绩效月份不能为空") + @Pattern(regexp = "^\\d{4}-\\d{2}$", message = "绩效月份格式必须为 yyyy-MM") + private String periodMonth; + + @Schema(description = "被考核员工 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1002") + @NotNull(message = "被考核员工不能为空") + private Long employeeId; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetExcelUpdateReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetExcelUpdateReqVO.java new file mode 100644 index 0000000..28b3ccf --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetExcelUpdateReqVO.java @@ -0,0 +1,39 @@ +package com.njcn.rdms.module.project.controller.admin.performance.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - 保存绩效 Excel Request VO") +@Data +public class PerformanceSheetExcelUpdateReqVO { + + @Schema(description = "当前文件 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "2042074259501088770") + @NotNull(message = "文件 ID 不能为空") + private Long fileId; + + @Schema(description = "当前文件名", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-06月-绩效表_张三.xlsx") + @NotBlank(message = "文件名不能为空") + @Size(max = 255, message = "文件名长度不能超过255个字符") + private String fileName; + + @Schema(description = "当前文件版本", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "文件版本不能为空") + private Integer fileVersion; + + @Schema(description = "实际得分总计", requiredMode = Schema.RequiredMode.REQUIRED, example = "13.00") + @NotNull(message = "实际得分总计不能为空") + private BigDecimal actualScoreTotal; + + @Schema(description = "基础得分总计", requiredMode = Schema.RequiredMode.REQUIRED, example = "6.00") + @NotNull(message = "基础得分总计不能为空") + private BigDecimal baseScoreTotal; + + @Schema(description = "附加得分总计", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.00") + @NotNull(message = "附加得分总计不能为空") + private BigDecimal extraScoreTotal; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetPageReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetPageReqVO.java new file mode 100644 index 0000000..5ac539a --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetPageReqVO.java @@ -0,0 +1,41 @@ +package com.njcn.rdms.module.project.controller.admin.performance.vo; + +import com.njcn.rdms.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +@Schema(description = "管理后台 - 绩效表分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class PerformanceSheetPageReqVO extends PageParam { + + @Schema(description = "团队视角下的员工 ID 列表") + private List employeeIds; + + @Schema(description = "绩效月份起始,格式 yyyy-MM;与 periodMonthEnd 构成区间筛选", example = "2026-03") + private String periodMonthStart; + + @Schema(description = "绩效月份结束,格式 yyyy-MM;与 periodMonthStart 构成区间筛选", example = "2026-05") + private String periodMonthEnd; + + @Schema(description = "员工姓名", example = "张三") + private String employeeName; + + @Schema(description = "员工部门 ID", example = "100") + private Long employeeDeptId; + + @Schema(description = "员工部门名称", example = "产品方向") + private String employeeDeptName; + + @Schema(description = "直属上级 ID", example = "9001") + private Long managerId; + + @Schema(description = "直属上级姓名", example = "王经理") + private String managerName; + + @Schema(description = "状态编码", example = "sent") + private String statusCode; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetRespVO.java new file mode 100644 index 0000000..2cf80ab --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetRespVO.java @@ -0,0 +1,37 @@ +package com.njcn.rdms.module.project.controller.admin.performance.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 绩效表 Response VO") +@Data +public class PerformanceSheetRespVO { + + private Long id; + private String periodMonth; + private Long employeeId; + private String employeeName; + private Long employeeDeptId; + private String employeeDeptName; + private String deptOrgType; + private Long managerId; + private String managerName; + private Long templateId; + private Long fileId; + private String fileName; + private Integer fileVersion; + private String statusCode; + private String statusName; + private BigDecimal actualScoreTotal; + private BigDecimal baseScoreTotal; + private BigDecimal extraScoreTotal; + private LocalDateTime sentTime; + private LocalDateTime confirmedTime; + private LocalDateTime rejectedTime; + private String lastStatusReason; + private LocalDateTime createTime; + private LocalDateTime updateTime; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetResponseRecordRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetResponseRecordRespVO.java new file mode 100644 index 0000000..83400b9 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetResponseRecordRespVO.java @@ -0,0 +1,23 @@ +package com.njcn.rdms.module.project.controller.admin.performance.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 绩效表员工反馈历史 Response VO") +@Data +public class PerformanceSheetResponseRecordRespVO { + + private Long id; + private Long sheetId; + private Long statusLogId; + private Integer roundNo; + private String actionType; + private String fromStatus; + private String toStatus; + private String opinion; + private Long responderUserId; + private String responderName; + private LocalDateTime createTime; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetStatusActionReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetStatusActionReqVO.java new file mode 100644 index 0000000..8cae8fb --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetStatusActionReqVO.java @@ -0,0 +1,14 @@ +package com.njcn.rdms.module.project.controller.admin.performance.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - 绩效表状态动作 Request VO") +@Data +public class PerformanceSheetStatusActionReqVO { + + @Schema(description = "原因/意见", example = "请重新确认分数") + @Size(max = 1000, message = "原因长度不能超过1000个字符") + private String reason; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetStatusDictRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetStatusDictRespVO.java new file mode 100644 index 0000000..699bec9 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetStatusDictRespVO.java @@ -0,0 +1,30 @@ +package com.njcn.rdms.module.project.controller.admin.performance.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 管理后台 - 绩效表状态字典 Response VO。 + */ +@Schema(description = "管理后台 - 绩效表状态字典 Response VO") +@Data +public class PerformanceSheetStatusDictRespVO { + + @Schema(description = "状态编码", example = "draft") + private String statusCode; + + @Schema(description = "状态名称", example = "待发送") + private String statusName; + + @Schema(description = "排序值", example = "10") + private Integer sort; + + @Schema(description = "是否初始状态", example = "true") + private Boolean initialFlag; + + @Schema(description = "是否终态", example = "false") + private Boolean terminalFlag; + + @Schema(description = "是否允许编辑", example = "true") + private Boolean allowEdit; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetStatusLogRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetStatusLogRespVO.java new file mode 100644 index 0000000..44dc682 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetStatusLogRespVO.java @@ -0,0 +1,24 @@ +package com.njcn.rdms.module.project.controller.admin.performance.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 绩效表状态日志 Response VO") +@Data +public class PerformanceSheetStatusLogRespVO { + + private Long id; + private Long sheetId; + private String actionType; + private String fromStatus; + private String toStatus; + private String reason; + private Long operatorUserId; + private String operatorName; + private String periodMonthSnapshot; + private String employeeNameSnapshot; + private String remark; + private LocalDateTime createTime; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetStatusTransitionRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetStatusTransitionRespVO.java new file mode 100644 index 0000000..03a145b --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetStatusTransitionRespVO.java @@ -0,0 +1,27 @@ +package com.njcn.rdms.module.project.controller.admin.performance.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 管理后台 - 绩效表状态动作字典 Response VO。 + */ +@Schema(description = "管理后台 - 绩效表状态动作字典 Response VO") +@Data +public class PerformanceSheetStatusTransitionRespVO { + + @Schema(description = "动作编码", example = "send") + private String actionCode; + + @Schema(description = "动作名称", example = "发送") + private String actionName; + + @Schema(description = "起始状态编码", example = "draft") + private String fromStatusCode; + + @Schema(description = "目标状态编码", example = "sent") + private String toStatusCode; + + @Schema(description = "是否必须填写原因", example = "false") + private Boolean needReason; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceTemplatePageReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceTemplatePageReqVO.java new file mode 100644 index 0000000..375583e --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceTemplatePageReqVO.java @@ -0,0 +1,18 @@ +package com.njcn.rdms.module.project.controller.admin.performance.vo; + +import com.njcn.rdms.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - 绩效模板分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class PerformanceTemplatePageReqVO extends PageParam { + + @Schema(description = "模板名称", example = "2026绩效模板") + private String templateName; + + @Schema(description = "是否当前生效", example = "true") + private Boolean activeFlag; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceTemplateRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceTemplateRespVO.java new file mode 100644 index 0000000..91140a5 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceTemplateRespVO.java @@ -0,0 +1,44 @@ +package com.njcn.rdms.module.project.controller.admin.performance.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 绩效模板 Response VO") +@Data +public class PerformanceTemplateRespVO { + + @Schema(description = "模板 ID", example = "1024") + private Long id; + + @Schema(description = "模板名称", example = "2026绩效模板") + private String templateName; + + @Schema(description = "文件 ID", example = "1024") + private Long fileId; + + @Schema(description = "原文件名", example = "绩效模板.xlsx") + private String fileName; + + @Schema(description = "版本号", example = "1") + private Integer versionNo; + + @Schema(description = "是否当前生效", example = "true") + private Boolean activeFlag; + + @Schema(description = "上传人 ID", example = "1001") + private Long uploadUserId; + + @Schema(description = "上传人名称", example = "张三") + private String uploadUserName; + + @Schema(description = "上传时间") + private LocalDateTime uploadTime; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "分数单元格映射") + private PerformanceScoreCellMappingRespVO scoreCellMapping; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceTemplateUploadReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceTemplateUploadReqVO.java new file mode 100644 index 0000000..6374fe0 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceTemplateUploadReqVO.java @@ -0,0 +1,33 @@ +package com.njcn.rdms.module.project.controller.admin.performance.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - 绩效模板上传 Request VO") +@Data +public class PerformanceTemplateUploadReqVO { + + @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026绩效模板") + @NotBlank(message = "模板名称不能为空") + @Size(max = 100, message = "模板名称长度不能超过100个字符") + private String templateName; + + @Schema(description = "文件 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "2042074259501088770") + @NotNull(message = "文件 ID 不能为空") + private Long fileId; + + @Schema(description = "原文件名", requiredMode = Schema.RequiredMode.REQUIRED, example = "绩效模板.xlsx") + @NotBlank(message = "文件名不能为空") + @Size(max = 255, message = "文件名长度不能超过255个字符") + private String fileName; + + @Schema(description = "是否上传后立即启用", example = "true") + private Boolean activeFlag; + + @Schema(description = "备注", example = "2026年度模板") + @Size(max = 500, message = "备注长度不能超过500个字符") + private String remark; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/performance/PerformanceSheetDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/performance/PerformanceSheetDO.java new file mode 100644 index 0000000..582e0b6 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/performance/PerformanceSheetDO.java @@ -0,0 +1,62 @@ +package com.njcn.rdms.module.project.dal.dataobject.performance; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 员工绩效表。 + */ +@TableName("rdms_performance_sheet") +@Data +@EqualsAndHashCode(callSuper = true) +public class PerformanceSheetDO extends BaseDO { + + @TableId + private Long id; + + private String periodMonth; + + private Long employeeId; + + private String employeeName; + + private Long employeeDeptId; + + private String employeeDeptName; + + private String deptOrgType; + + private Long managerId; + + private String managerName; + + private Long templateId; + + private Long fileId; + + private String fileName; + + private Integer fileVersion; + + private String statusCode; + + private BigDecimal actualScoreTotal; + + private BigDecimal baseScoreTotal; + + private BigDecimal extraScoreTotal; + + private LocalDateTime sentTime; + + private LocalDateTime confirmedTime; + + private LocalDateTime rejectedTime; + + private String lastStatusReason; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/performance/PerformanceSheetResponseRecordDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/performance/PerformanceSheetResponseRecordDO.java new file mode 100644 index 0000000..344adfe --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/performance/PerformanceSheetResponseRecordDO.java @@ -0,0 +1,37 @@ +package com.njcn.rdms.module.project.dal.dataobject.performance; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 绩效表员工反馈历史。 + */ +@TableName("rdms_performance_sheet_response_record") +@Data +@EqualsAndHashCode(callSuper = true) +public class PerformanceSheetResponseRecordDO extends BaseDO { + + @TableId + private Long id; + + private Long sheetId; + + private Long statusLogId; + + private Integer roundNo; + + private String actionType; + + private String fromStatus; + + private String toStatus; + + private String opinion; + + private Long responderUserId; + + private String responderName; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/performance/PerformanceSheetStatusLogDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/performance/PerformanceSheetStatusLogDO.java new file mode 100644 index 0000000..3d3377b --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/performance/PerformanceSheetStatusLogDO.java @@ -0,0 +1,39 @@ +package com.njcn.rdms.module.project.dal.dataobject.performance; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 绩效表状态日志。 + */ +@TableName("rdms_performance_sheet_status_log") +@Data +@EqualsAndHashCode(callSuper = true) +public class PerformanceSheetStatusLogDO extends BaseDO { + + @TableId + private Long id; + + private Long sheetId; + + private String actionType; + + private String fromStatus; + + private String toStatus; + + private String reason; + + private Long operatorUserId; + + private String operatorName; + + private String periodMonthSnapshot; + + private String employeeNameSnapshot; + + private String remark; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/performance/PerformanceTemplateDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/performance/PerformanceTemplateDO.java new file mode 100644 index 0000000..a74421e --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/performance/PerformanceTemplateDO.java @@ -0,0 +1,39 @@ +package com.njcn.rdms.module.project.dal.dataobject.performance; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * 绩效模板。 + */ +@TableName("rdms_performance_template") +@Data +@EqualsAndHashCode(callSuper = true) +public class PerformanceTemplateDO extends BaseDO { + + @TableId + private Long id; + + private String templateName; + + private Long fileId; + + private String fileName; + + private Integer versionNo; + + private Boolean activeFlag; + + private Long uploadUserId; + + private String uploadUserName; + + private LocalDateTime uploadTime; + + private String remark; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/performance/PerformanceSheetMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/performance/PerformanceSheetMapper.java new file mode 100644 index 0000000..ae9a2ba --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/performance/PerformanceSheetMapper.java @@ -0,0 +1,102 @@ +package com.njcn.rdms.module.project.dal.mysql.performance; + +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetPageReqVO; +import com.njcn.rdms.module.project.dal.dataobject.performance.PerformanceSheetDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface PerformanceSheetMapper extends BaseMapperX { + + default PerformanceSheetDO selectByEmployeeIdAndPeriodMonth(Long employeeId, String periodMonth) { + return selectOne(new LambdaQueryWrapperX() + .eq(PerformanceSheetDO::getEmployeeId, employeeId) + .eq(PerformanceSheetDO::getPeriodMonth, periodMonth)); + } + + default PageResult selectEmployeePage(Long employeeId, PerformanceSheetPageReqVO reqVO) { + LambdaQueryWrapperX wrapper = buildPageQuery(reqVO) + .eq(PerformanceSheetDO::getEmployeeId, employeeId) + .orderByDesc(PerformanceSheetDO::getPeriodMonth) + .orderByDesc(PerformanceSheetDO::getId); + return selectPage(reqVO, wrapper); + } + + default PageResult selectEmployeePage(Collection employeeIds, + PerformanceSheetPageReqVO reqVO) { + if (employeeIds == null || employeeIds.isEmpty()) { + return new PageResult<>(List.of(), 0L); + } + LambdaQueryWrapperX wrapper = buildPageQuery(reqVO) + .in(PerformanceSheetDO::getEmployeeId, employeeIds) + .orderByDesc(PerformanceSheetDO::getPeriodMonth) + .orderByDesc(PerformanceSheetDO::getId); + return selectPage(reqVO, wrapper); + } + + default List selectListByEmployeeIdsAndPeriodMonth(Collection employeeIds, + String periodMonth) { + if (employeeIds == null || employeeIds.isEmpty()) { + return List.of(); + } + return selectList(new LambdaQueryWrapperX() + .in(PerformanceSheetDO::getEmployeeId, employeeIds) + .eq(PerformanceSheetDO::getPeriodMonth, periodMonth) + .orderByAsc(PerformanceSheetDO::getEmployeeName)); + } + + /** + * 按员工 ID 列表与月份区间查询绩效表。 + */ + default List selectListByEmployeeIdsAndPeriodMonthRange(Collection employeeIds, + String periodMonthStart, + String periodMonthEnd) { + if (employeeIds == null || employeeIds.isEmpty()) { + return List.of(); + } + return selectList(new LambdaQueryWrapperX() + .in(PerformanceSheetDO::getEmployeeId, employeeIds) + .geIfPresent(PerformanceSheetDO::getPeriodMonth, periodMonthStart) + .leIfPresent(PerformanceSheetDO::getPeriodMonth, periodMonthEnd) + .orderByAsc(PerformanceSheetDO::getEmployeeName)); + } + + default List selectExportList(PerformanceSheetPageReqVO reqVO, Collection employeeIds) { + if (employeeIds == null || employeeIds.isEmpty()) { + return List.of(); + } + return selectList(buildPageQuery(reqVO) + .in(PerformanceSheetDO::getEmployeeId, employeeIds) + .orderByDesc(PerformanceSheetDO::getPeriodMonth) + .orderByDesc(PerformanceSheetDO::getId)); + } + + default int updateExcelByIdAndVersion(PerformanceSheetDO update, Long id, Integer fileVersion) { + return update(update, new LambdaQueryWrapperX() + .eq(PerformanceSheetDO::getId, id) + .eq(PerformanceSheetDO::getFileVersion, fileVersion)); + } + + default int updateStatusByIdAndStatus(PerformanceSheetDO update, Long id, String fromStatus) { + return update(update, new LambdaQueryWrapperX() + .eq(PerformanceSheetDO::getId, id) + .eq(PerformanceSheetDO::getStatusCode, fromStatus)); + } + + private LambdaQueryWrapperX buildPageQuery(PerformanceSheetPageReqVO reqVO) { + return new LambdaQueryWrapperX() + .geIfPresent(PerformanceSheetDO::getPeriodMonth, reqVO.getPeriodMonthStart()) + .leIfPresent(PerformanceSheetDO::getPeriodMonth, reqVO.getPeriodMonthEnd()) + .likeIfPresent(PerformanceSheetDO::getEmployeeName, reqVO.getEmployeeName()) + .eqIfPresent(PerformanceSheetDO::getEmployeeDeptId, reqVO.getEmployeeDeptId()) + .likeIfPresent(PerformanceSheetDO::getEmployeeDeptName, reqVO.getEmployeeDeptName()) + .eqIfPresent(PerformanceSheetDO::getManagerId, reqVO.getManagerId()) + .likeIfPresent(PerformanceSheetDO::getManagerName, reqVO.getManagerName()) + .eqIfPresent(PerformanceSheetDO::getStatusCode, reqVO.getStatusCode()); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/performance/PerformanceSheetResponseRecordMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/performance/PerformanceSheetResponseRecordMapper.java new file mode 100644 index 0000000..c31dcc8 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/performance/PerformanceSheetResponseRecordMapper.java @@ -0,0 +1,24 @@ +package com.njcn.rdms.module.project.dal.mysql.performance; + +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.njcn.rdms.module.project.dal.dataobject.performance.PerformanceSheetResponseRecordDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface PerformanceSheetResponseRecordMapper extends BaseMapperX { + + default List selectListBySheetId(Long sheetId) { + return selectList(new LambdaQueryWrapperX() + .eq(PerformanceSheetResponseRecordDO::getSheetId, sheetId) + .orderByDesc(PerformanceSheetResponseRecordDO::getCreateTime) + .orderByDesc(PerformanceSheetResponseRecordDO::getId)); + } + + default int countBySheetId(Long sheetId) { + return Math.toIntExact(selectCount(new LambdaQueryWrapperX() + .eq(PerformanceSheetResponseRecordDO::getSheetId, sheetId))); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/performance/PerformanceSheetStatusLogMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/performance/PerformanceSheetStatusLogMapper.java new file mode 100644 index 0000000..a37a520 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/performance/PerformanceSheetStatusLogMapper.java @@ -0,0 +1,19 @@ +package com.njcn.rdms.module.project.dal.mysql.performance; + +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.njcn.rdms.module.project.dal.dataobject.performance.PerformanceSheetStatusLogDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface PerformanceSheetStatusLogMapper extends BaseMapperX { + + default List selectListBySheetId(Long sheetId) { + return selectList(new LambdaQueryWrapperX() + .eq(PerformanceSheetStatusLogDO::getSheetId, sheetId) + .orderByDesc(PerformanceSheetStatusLogDO::getCreateTime) + .orderByDesc(PerformanceSheetStatusLogDO::getId)); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/performance/PerformanceTemplateMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/performance/PerformanceTemplateMapper.java new file mode 100644 index 0000000..911c3aa --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/performance/PerformanceTemplateMapper.java @@ -0,0 +1,35 @@ +package com.njcn.rdms.module.project.dal.mysql.performance; + +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceTemplatePageReqVO; +import com.njcn.rdms.module.project.dal.dataobject.performance.PerformanceTemplateDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PerformanceTemplateMapper extends BaseMapperX { + + default PerformanceTemplateDO selectCurrent() { + return selectOne(new LambdaQueryWrapperX() + .eq(PerformanceTemplateDO::getActiveFlag, true) + .orderByDesc(PerformanceTemplateDO::getVersionNo) + .last("LIMIT 1")); + } + + default PageResult selectPage(PerformanceTemplatePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(PerformanceTemplateDO::getTemplateName, reqVO.getTemplateName()) + .eqIfPresent(PerformanceTemplateDO::getActiveFlag, reqVO.getActiveFlag()) + .orderByDesc(PerformanceTemplateDO::getVersionNo) + .orderByDesc(PerformanceTemplateDO::getId)); + } + + default Integer selectMaxVersionNo() { + PerformanceTemplateDO template = selectOne(new LambdaQueryWrapperX() + .select(PerformanceTemplateDO::getVersionNo) + .orderByDesc(PerformanceTemplateDO::getVersionNo) + .last("LIMIT 1")); + return template == null || template.getVersionNo() == null ? 0 : template.getVersionNo(); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/NotifyTemplateCodeConstants.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/NotifyTemplateCodeConstants.java index a54db20..4974a0d 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/NotifyTemplateCodeConstants.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/notify/NotifyTemplateCodeConstants.java @@ -43,4 +43,16 @@ public class NotifyTemplateCodeConstants { /** 产品需求创建:通知处理人 */ public static final String PRODUCT_REQUIREMENT_ASSIGNED = "product_requirement_assigned"; + /** 绩效表发送:通知员工确认绩效表 */ + public static final String PERFORMANCE_SENT = "performance_sent"; + + /** 绩效待确认催办:主管催办员工确认绩效表 */ + public static final String PERFORMANCE_PENDING_CONFIRM_REMIND = "performance_pending_confirm_remind"; + + /** 绩效待发送催办:提醒直属上级发送或重新发送绩效表 */ + public static final String PERFORMANCE_PENDING_SEND_REMIND = "performance_pending_send_remind"; + + /** 绩效表退回:通知直属上级处理员工退回的绩效表 */ + public static final String PERFORMANCE_REJECTED = "performance_rejected"; + } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceDownloadFile.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceDownloadFile.java new file mode 100644 index 0000000..b91d8dd --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceDownloadFile.java @@ -0,0 +1,4 @@ +package com.njcn.rdms.module.project.service.performance; + +public record PerformanceDownloadFile(String filename, String contentType, byte[] content) { +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceProperties.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceProperties.java new file mode 100644 index 0000000..16141af --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceProperties.java @@ -0,0 +1,26 @@ +package com.njcn.rdms.module.project.service.performance; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 绩效配置。 + */ +@Component +@ConfigurationProperties(prefix = "rdms.performance") +@Data +public class PerformanceProperties { + + private ScoreCellMapping scoreCellMapping = new ScoreCellMapping(); + + @Data + public static class ScoreCellMapping { + + private String actualScoreTotal = "J10"; + + private String baseScoreTotal = "K10"; + + private String extraScoreTotal = "L10"; + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceSheetService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceSheetService.java new file mode 100644 index 0000000..e808822 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceSheetService.java @@ -0,0 +1,53 @@ +package com.njcn.rdms.module.project.service.performance; + +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceMonthlyResultRespVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetCreateReqVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetExcelUpdateReqVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetPageReqVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetRespVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetResponseRecordRespVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetStatusActionReqVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetStatusDictRespVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetStatusLogRespVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetStatusTransitionRespVO; + +import java.util.Collection; +import java.util.List; + +public interface PerformanceSheetService { + + PageResult getSheetPage(PerformanceSheetPageReqVO reqVO); + + PerformanceSheetRespVO getSheet(Long id); + + Long createSheet(PerformanceSheetCreateReqVO reqVO); + + void updateExcel(Long id, PerformanceSheetExcelUpdateReqVO reqVO); + + void deleteSheet(Long id); + + void send(Long id); + + void resend(Long id); + + void confirm(Long id, PerformanceSheetStatusActionReqVO reqVO); + + void reject(Long id, PerformanceSheetStatusActionReqVO reqVO); + + PerformanceDownloadFile download(Long id); + + PerformanceDownloadFile batchDownload(Collection ids); + + PerformanceDownloadFile exportByCondition(PerformanceSheetPageReqVO reqVO); + + List getStatusLogs(Long id); + + List getResponseRecords(Long id); + + PerformanceMonthlyResultRespVO getMonthlyResult(Long employeeId, String periodMonth); + + List getStatusDict(); + + List getStatusTransitions(); +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceSheetServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceSheetServiceImpl.java new file mode 100644 index 0000000..a42e4ff --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceSheetServiceImpl.java @@ -0,0 +1,634 @@ +package com.njcn.rdms.module.project.service.performance; + +import com.njcn.rdms.framework.common.pojo.CommonResult; +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.framework.common.util.object.BeanUtils; +import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils; +import com.njcn.rdms.module.project.constant.PerformanceConstants; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceMonthlyResultRespVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetCreateReqVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetExcelUpdateReqVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetPageReqVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetRespVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetResponseRecordRespVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetStatusActionReqVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetStatusDictRespVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetStatusLogRespVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetStatusTransitionRespVO; +import com.njcn.rdms.module.project.dal.dataobject.performance.PerformanceSheetDO; +import com.njcn.rdms.module.project.dal.dataobject.performance.PerformanceSheetResponseRecordDO; +import com.njcn.rdms.module.project.dal.dataobject.performance.PerformanceSheetStatusLogDO; +import com.njcn.rdms.module.project.dal.dataobject.performance.PerformanceTemplateDO; +import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO; +import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO; +import com.njcn.rdms.module.project.dal.mysql.performance.PerformanceSheetMapper; +import com.njcn.rdms.module.project.dal.mysql.performance.PerformanceSheetResponseRecordMapper; +import com.njcn.rdms.module.project.dal.mysql.performance.PerformanceSheetStatusLogMapper; +import com.njcn.rdms.module.project.dal.mysql.performance.PerformanceTemplateMapper; +import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper; +import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper; +import com.njcn.rdms.module.project.enums.ErrorCodeConstants; +import com.njcn.rdms.module.project.framework.notify.NotifySendEvent; +import com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants; +import com.njcn.rdms.module.project.service.status.StatusActionTextResolver; +import com.njcn.rdms.module.project.service.team.TeamDashboardAccessService; +import com.njcn.rdms.module.system.api.dept.DeptApi; +import com.njcn.rdms.module.system.api.dept.dto.DeptRespDTO; +import com.njcn.rdms.module.system.api.file.FileApi; +import com.njcn.rdms.module.system.api.user.AdminUserApi; +import com.njcn.rdms.module.system.api.user.UserManagementRelationApi; +import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO; +import com.njcn.rdms.module.system.enums.notify.NotifyMessageLevelConstants; +import jakarta.annotation.Resource; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.io.ByteArrayOutputStream; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.YearMonth; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; + +@Service +public class PerformanceSheetServiceImpl implements PerformanceSheetService { + + private static final String EXCEL_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + private static final String ZIP_CONTENT_TYPE = "application/zip"; + + @Resource + private PerformanceSheetMapper performanceSheetMapper; + @Resource + private PerformanceTemplateMapper performanceTemplateMapper; + @Resource + private PerformanceSheetStatusLogMapper performanceSheetStatusLogMapper; + @Resource + private PerformanceSheetResponseRecordMapper performanceSheetResponseRecordMapper; + @Resource + private ObjectStatusModelMapper objectStatusModelMapper; + @Resource + private ObjectStatusTransitionMapper objectStatusTransitionMapper; + @Resource + private StatusActionTextResolver statusActionTextResolver; + @Resource + private AdminUserApi adminUserApi; + @Resource + private DeptApi deptApi; + @Resource + private UserManagementRelationApi userManagementRelationApi; + @Resource + private FileApi fileApi; + @Resource + private TeamDashboardAccessService teamDashboardAccessService; + @Resource + private ApplicationEventPublisher applicationEventPublisher; + + @Override + public PageResult getSheetPage(PerformanceSheetPageReqVO reqVO) { + PageResult pageResult; + if (reqVO.getEmployeeIds() != null) { + List employeeIds = teamDashboardAccessService.resolveRequestedSubordinateUserIds( + reqVO.getEmployeeIds(), PerformanceConstants.PERMISSION_TEAM_DASHBOARD); + pageResult = performanceSheetMapper.selectEmployeePage(employeeIds, reqVO); + } else { + pageResult = performanceSheetMapper.selectEmployeePage(SecurityFrameworkUtils.getLoginUserId(), reqVO); + } + return new PageResult<>(pageResult.getList().stream().map(this::toRespVO).toList(), pageResult.getTotal()); + } + + @Override + public PerformanceSheetRespVO getSheet(Long id) { + PerformanceSheetDO sheet = validateReadableSheet(id); + return toRespVO(sheet); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createSheet(PerformanceSheetCreateReqVO reqVO) { + String periodMonth = normalizePeriodMonth(reqVO.getPeriodMonth()); + PerformanceTemplateDO template = performanceTemplateMapper.selectCurrent(); + if (template == null) { + throw exception(ErrorCodeConstants.PERFORMANCE_TEMPLATE_CURRENT_NOT_EXISTS); + } + if (performanceSheetMapper.selectByEmployeeIdAndPeriodMonth(reqVO.getEmployeeId(), periodMonth) != null) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_DUPLICATE); + } + + AdminUserRespDTO employee = validateActiveUser(reqVO.getEmployeeId()); + AdminUserRespDTO manager = loadDirectManager(employee.getId()); + validateManagerOnly(manager.getId()); + DeptRespDTO dept = validateEmployeeDept(employee.getDeptId()); + + PerformanceSheetDO sheet = new PerformanceSheetDO(); + sheet.setPeriodMonth(periodMonth); + sheet.setEmployeeId(employee.getId()); + sheet.setEmployeeName(defaultText(employee.getNickname())); + sheet.setEmployeeDeptId(employee.getDeptId()); + sheet.setEmployeeDeptName(dept.getName().trim()); + sheet.setDeptOrgType(dept.getOrgType().trim()); + sheet.setManagerId(manager.getId()); + sheet.setManagerName(defaultText(manager.getNickname())); + sheet.setTemplateId(template.getId()); + sheet.setFileName(buildPerformanceFileName(periodMonth, employee.getNickname())); + sheet.setFileVersion(0); + sheet.setStatusCode(getInitialStatusCode()); + performanceSheetMapper.insert(sheet); + return sheet.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateExcel(Long id, PerformanceSheetExcelUpdateReqVO reqVO) { + PerformanceSheetDO current = validateSheetExists(id); + validateManagerOnly(current.getManagerId()); + validateEditable(current); + + PerformanceSheetDO update = new PerformanceSheetDO(); + update.setFileId(reqVO.getFileId()); + update.setFileName(reqVO.getFileName().trim()); + update.setFileVersion(reqVO.getFileVersion() + 1); + update.setActualScoreTotal(normalizeScore(reqVO.getActualScoreTotal())); + update.setBaseScoreTotal(normalizeScore(reqVO.getBaseScoreTotal())); + update.setExtraScoreTotal(normalizeScore(reqVO.getExtraScoreTotal())); + int updateCount = performanceSheetMapper.updateExcelByIdAndVersion(update, id, reqVO.getFileVersion()); + if (updateCount != 1) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_STATUS_CONCURRENT_MODIFIED); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteSheet(Long id) { + PerformanceSheetDO sheet = validateSheetExists(id); + validateManagerOnly(sheet.getManagerId()); + if (!PerformanceConstants.STATUS_DRAFT.equals(sheet.getStatusCode())) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_STATUS_NOT_ALLOW_DELETE); + } + performanceSheetMapper.deleteById(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void send(Long id) { + processManagerStatusAction(id, PerformanceConstants.ACTION_SEND); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void resend(Long id) { + processManagerStatusAction(id, PerformanceConstants.ACTION_RESEND); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void confirm(Long id, PerformanceSheetStatusActionReqVO reqVO) { + processEmployeeResponse(id, PerformanceConstants.ACTION_CONFIRM, + normalizeNullableText(reqVO == null ? null : reqVO.getReason())); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void reject(Long id, PerformanceSheetStatusActionReqVO reqVO) { + String reason = normalizeNullableText(reqVO == null ? null : reqVO.getReason()); + if (!StringUtils.hasText(reason)) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_REJECT_REASON_REQUIRED); + } + PerformanceSheetDO sheet = processEmployeeResponse(id, PerformanceConstants.ACTION_REJECT, reason); + publishPerformanceNotice(List.of(sheet.getManagerId()), NotifyTemplateCodeConstants.PERFORMANCE_REJECTED, sheet); + } + + @Override + public PerformanceDownloadFile download(Long id) { + PerformanceSheetDO sheet = validateReadableSheet(id); + return new PerformanceDownloadFile(defaultDownloadName(sheet), EXCEL_CONTENT_TYPE, readFileContent(sheet)); + } + + @Override + public PerformanceDownloadFile batchDownload(Collection ids) { + if (ids == null || ids.isEmpty()) { + throw invalidParamException("绩效表 ID 不能为空"); + } + List sheets = new ArrayList<>(); + for (Long id : ids) { + sheets.add(validateReadableSheet(id)); + } + return zipSheets("绩效表_" + LocalDate.now() + ".zip", sheets); + } + + @Override + public PerformanceDownloadFile exportByCondition(PerformanceSheetPageReqVO reqVO) { + List employeeIds; + if (reqVO.getEmployeeIds() != null) { + employeeIds = teamDashboardAccessService.resolveRequestedSubordinateUserIds( + reqVO.getEmployeeIds(), PerformanceConstants.PERMISSION_TEAM_DASHBOARD); + } else { + employeeIds = List.of(SecurityFrameworkUtils.getLoginUserId()); + } + List sheets = performanceSheetMapper.selectExportList(reqVO, employeeIds); + return zipSheets("绩效表_" + LocalDate.now() + ".zip", sheets); + } + + @Override + public List getStatusLogs(Long id) { + validateReadableSheet(id); + return BeanUtils.toBean(performanceSheetStatusLogMapper.selectListBySheetId(id), + PerformanceSheetStatusLogRespVO.class); + } + + @Override + public List getResponseRecords(Long id) { + validateReadableSheet(id); + return BeanUtils.toBean(performanceSheetResponseRecordMapper.selectListBySheetId(id), + PerformanceSheetResponseRecordRespVO.class); + } + + @Override + public PerformanceMonthlyResultRespVO getMonthlyResult(Long employeeId, String periodMonth) { + String normalizedMonth = normalizePeriodMonth(periodMonth); + PerformanceSheetDO sheet = performanceSheetMapper.selectByEmployeeIdAndPeriodMonth(employeeId, normalizedMonth); + PerformanceMonthlyResultRespVO respVO = new PerformanceMonthlyResultRespVO(); + respVO.setEmployeeId(employeeId); + respVO.setPeriodMonth(normalizedMonth); + if (sheet == null) { + return respVO; + } + validateReadable(sheet); + respVO.setSheetId(sheet.getId()); + respVO.setActualScoreTotal(sheet.getActualScoreTotal()); + respVO.setBaseScoreTotal(sheet.getBaseScoreTotal()); + respVO.setExtraScoreTotal(sheet.getExtraScoreTotal()); + respVO.setStatusCode(sheet.getStatusCode()); + return respVO; + } + + @Override + public List getStatusDict() { + return objectStatusModelMapper.selectListByObjectTypeEnabled(PerformanceConstants.STATUS_OBJECT_TYPE) + .stream() + .map(this::buildStatusDictRespVO) + .toList(); + } + + @Override + public List getStatusTransitions() { + List statusCodes = objectStatusModelMapper + .selectListByObjectTypeEnabled(PerformanceConstants.STATUS_OBJECT_TYPE) + .stream() + .map(ObjectStatusModelDO::getStatusCode) + .toList(); + if (statusCodes.isEmpty()) { + return List.of(); + } + return objectStatusTransitionMapper + .selectListByObjectTypeAndFromStatuses(PerformanceConstants.STATUS_OBJECT_TYPE, statusCodes) + .stream() + .map(this::buildStatusTransitionRespVO) + .toList(); + } + + private void processManagerStatusAction(Long id, String actionType) { + PerformanceSheetDO current = validateSheetExists(id); + validateManagerOnly(current.getManagerId()); + ObjectStatusTransitionDO transition = validateTransition(current.getStatusCode(), actionType, null); + if (current.getFileId() == null) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_FILE_REQUIRED); + } + PerformanceSheetDO update = new PerformanceSheetDO(); + update.setStatusCode(transition.getToStatusCode()); + update.setSentTime(LocalDateTime.now()); + int updateCount = performanceSheetMapper.updateStatusByIdAndStatus(update, id, current.getStatusCode()); + if (updateCount != 1) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_STATUS_CONCURRENT_MODIFIED); + } + PerformanceSheetDO after = merge(current, update); + writeStatusLog(after, actionType, current.getStatusCode(), transition.getToStatusCode(), null); + publishPerformanceNotice(List.of(after.getEmployeeId()), NotifyTemplateCodeConstants.PERFORMANCE_SENT, after); + } + + private PerformanceSheetDO processEmployeeResponse(Long id, String actionType, String reason) { + PerformanceSheetDO current = validateSheetExists(id); + validateEmployeeOnly(current.getEmployeeId()); + ObjectStatusTransitionDO transition = validateTransition(current.getStatusCode(), actionType, reason); + PerformanceSheetDO update = new PerformanceSheetDO(); + update.setStatusCode(transition.getToStatusCode()); + if (PerformanceConstants.STATUS_CONFIRMED.equals(transition.getToStatusCode())) { + update.setConfirmedTime(LocalDateTime.now()); + } else if (PerformanceConstants.STATUS_REJECTED.equals(transition.getToStatusCode())) { + update.setRejectedTime(LocalDateTime.now()); + update.setLastStatusReason(reason); + } + int updateCount = performanceSheetMapper.updateStatusByIdAndStatus(update, id, current.getStatusCode()); + if (updateCount != 1) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_STATUS_CONCURRENT_MODIFIED); + } + PerformanceSheetDO after = merge(current, update); + PerformanceSheetStatusLogDO statusLog = writeStatusLog(after, actionType, current.getStatusCode(), + transition.getToStatusCode(), reason); + writeResponseRecord(after, statusLog, actionType, current.getStatusCode(), transition.getToStatusCode(), reason); + return after; + } + + private PerformanceSheetStatusLogDO writeStatusLog(PerformanceSheetDO sheet, String actionType, + String fromStatus, String toStatus, String reason) { + PerformanceSheetStatusLogDO statusLog = new PerformanceSheetStatusLogDO(); + statusLog.setSheetId(sheet.getId()); + statusLog.setActionType(actionType); + statusLog.setFromStatus(fromStatus); + statusLog.setToStatus(toStatus); + statusLog.setReason(defaultText(reason)); + statusLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId()); + statusLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname())); + statusLog.setPeriodMonthSnapshot(sheet.getPeriodMonth()); + statusLog.setEmployeeNameSnapshot(sheet.getEmployeeName()); + statusLog.setRemark(buildRemark(sheet)); + performanceSheetStatusLogMapper.insert(statusLog); + return statusLog; + } + + private void writeResponseRecord(PerformanceSheetDO sheet, PerformanceSheetStatusLogDO statusLog, + String actionType, String fromStatus, String toStatus, String opinion) { + PerformanceSheetResponseRecordDO record = new PerformanceSheetResponseRecordDO(); + record.setSheetId(sheet.getId()); + record.setStatusLogId(statusLog.getId()); + record.setRoundNo(performanceSheetResponseRecordMapper.countBySheetId(sheet.getId()) + 1); + record.setActionType(actionType); + record.setFromStatus(fromStatus); + record.setToStatus(toStatus); + record.setOpinion(defaultText(opinion)); + record.setResponderUserId(SecurityFrameworkUtils.getLoginUserId()); + record.setResponderName(defaultText(SecurityFrameworkUtils.getLoginUserNickname())); + performanceSheetResponseRecordMapper.insert(record); + } + + private PerformanceSheetDO validateSheetExists(Long id) { + PerformanceSheetDO sheet = id == null ? null : performanceSheetMapper.selectById(id); + if (sheet == null) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_NOT_EXISTS); + } + return sheet; + } + + private PerformanceSheetDO validateReadableSheet(Long id) { + PerformanceSheetDO sheet = validateSheetExists(id); + validateReadable(sheet); + return sheet; + } + + private void validateReadable(PerformanceSheetDO sheet) { + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + if (!Objects.equals(loginUserId, sheet.getEmployeeId()) + && !Objects.equals(loginUserId, sheet.getManagerId()) + && !teamDashboardAccessService.canReadSubordinateUser( + sheet.getEmployeeId(), PerformanceConstants.PERMISSION_TEAM_DASHBOARD)) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_READ_FORBIDDEN); + } + } + + private void validateEditable(PerformanceSheetDO sheet) { + ObjectStatusModelDO statusModel = objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled( + PerformanceConstants.STATUS_OBJECT_TYPE, sheet.getStatusCode()); + if (statusModel == null) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_STATUS_MODEL_NOT_EXISTS_OR_DISABLED); + } + if (!Boolean.TRUE.equals(statusModel.getAllowEdit())) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_STATUS_NOT_ALLOW_EDIT); + } + } + + private void validateManagerOnly(Long managerId) { + if (!Objects.equals(SecurityFrameworkUtils.getLoginUserId(), managerId)) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_MANAGER_ONLY); + } + } + + private void validateEmployeeOnly(Long employeeId) { + if (!Objects.equals(SecurityFrameworkUtils.getLoginUserId(), employeeId)) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_EMPLOYEE_ONLY); + } + } + + private AdminUserRespDTO validateActiveUser(Long userId) { + CommonResult result = adminUserApi.getUser(userId); + AdminUserRespDTO user = result == null ? null : result.getCheckedData(); + if (user == null || user.getId() == null || !Objects.equals(user.getStatus(), 0)) { + throw exception(ErrorCodeConstants.PERFORMANCE_EMPLOYEE_INVALID); + } + return user; + } + + private AdminUserRespDTO loadDirectManager(Long employeeId) { + CommonResult result = userManagementRelationApi.getDirectManager(employeeId); + AdminUserRespDTO manager = result == null ? null : result.getCheckedData(); + if (manager == null || manager.getId() == null || !Objects.equals(manager.getStatus(), 0)) { + throw exception(ErrorCodeConstants.PERFORMANCE_DIRECT_MANAGER_NOT_EXISTS); + } + return manager; + } + + private DeptRespDTO validateEmployeeDept(Long deptId) { + if (deptId == null) { + throw exception(ErrorCodeConstants.PERFORMANCE_EMPLOYEE_DEPT_INVALID); + } + CommonResult result = deptApi.getDept(deptId); + DeptRespDTO dept = result == null ? null : result.getCheckedData(); + if (dept == null || dept.getId() == null + || !StringUtils.hasText(dept.getName()) + || !StringUtils.hasText(dept.getOrgType())) { + throw exception(ErrorCodeConstants.PERFORMANCE_EMPLOYEE_DEPT_INVALID); + } + return dept; + } + + private PerformanceSheetRespVO toRespVO(PerformanceSheetDO sheet) { + PerformanceSheetRespVO respVO = BeanUtils.toBean(sheet, PerformanceSheetRespVO.class); + respVO.setStatusName(statusName(sheet.getStatusCode())); + return respVO; + } + + private PerformanceDownloadFile zipSheets(String filename, List sheets) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) { + Map filenameCounts = new HashMap<>(); + for (PerformanceSheetDO sheet : sheets) { + byte[] content = readFileContent(sheet); + String entryName = uniqueFilename(defaultDownloadName(sheet), filenameCounts); + zipOutputStream.putNextEntry(new ZipEntry(entryName)); + zipOutputStream.write(content); + zipOutputStream.closeEntry(); + } + zipOutputStream.finish(); + return new PerformanceDownloadFile(filename, ZIP_CONTENT_TYPE, outputStream.toByteArray()); + } catch (Exception ex) { + throw new IllegalStateException("打包绩效表失败", ex); + } + } + + private byte[] readFileContent(PerformanceSheetDO sheet) { + if (sheet.getFileId() == null) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_FILE_REQUIRED); + } + CommonResult result = fileApi.getFileContent(sheet.getFileId()); + byte[] content = result == null ? null : result.getCheckedData(); + if (content == null || content.length == 0) { + throw exception(ErrorCodeConstants.PERFORMANCE_FILE_NOT_EXISTS); + } + return content; + } + + private String uniqueFilename(String filename, Map filenameCounts) { + Integer count = filenameCounts.merge(filename, 1, Integer::sum); + if (count == 1) { + return filename; + } + int dotIndex = filename.lastIndexOf('.'); + if (dotIndex <= 0) { + return filename + "_" + count; + } + return filename.substring(0, dotIndex) + "_" + count + filename.substring(dotIndex); + } + + private PerformanceSheetDO merge(PerformanceSheetDO source, PerformanceSheetDO update) { + PerformanceSheetDO merged = BeanUtils.toBean(source, PerformanceSheetDO.class); + if (update.getStatusCode() != null) { + merged.setStatusCode(update.getStatusCode()); + } + if (update.getSentTime() != null) { + merged.setSentTime(update.getSentTime()); + } + if (update.getConfirmedTime() != null) { + merged.setConfirmedTime(update.getConfirmedTime()); + } + if (update.getRejectedTime() != null) { + merged.setRejectedTime(update.getRejectedTime()); + } + if (update.getLastStatusReason() != null) { + merged.setLastStatusReason(update.getLastStatusReason()); + } + return merged; + } + + private void publishPerformanceNotice(Collection recipients, String templateCode, PerformanceSheetDO sheet) { + if (recipients == null || recipients.isEmpty()) { + return; + } + Map params = new LinkedHashMap<>(); + params.put("periodMonth", sheet.getPeriodMonth()); + params.put("employeeName", sheet.getEmployeeName()); + params.put("managerName", sheet.getManagerName()); + params.put("sheetId", sheet.getId()); + params.put("statusName", statusName(sheet.getStatusCode())); + params.put("reason", sheet.getLastStatusReason()); + applicationEventPublisher.publishEvent(NotifySendEvent.of(new ArrayList<>(new LinkedHashSet<>(recipients)), + templateCode, params, NotifyMessageLevelConstants.REMIND)); + } + + private ObjectStatusTransitionDO validateTransition(String fromStatus, String actionCode, String reason) { + ObjectStatusTransitionDO transition = objectStatusTransitionMapper.selectByObjectTypeAndFromStatusAndAction( + PerformanceConstants.STATUS_OBJECT_TYPE, fromStatus, actionCode); + if (transition == null) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_STATUS_ACTION_NOT_ALLOWED, + statusActionTextResolver.statusName(PerformanceConstants.STATUS_OBJECT_TYPE, fromStatus), + statusActionTextResolver.actionName(PerformanceConstants.STATUS_OBJECT_TYPE, actionCode)); + } + if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_STATUS_ACTION_REASON_REQUIRED, + statusActionTextResolver.actionName(PerformanceConstants.STATUS_OBJECT_TYPE, actionCode)); + } + ObjectStatusModelDO toModel = objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled( + PerformanceConstants.STATUS_OBJECT_TYPE, transition.getToStatusCode()); + if (toModel == null) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_STATUS_MODEL_NOT_EXISTS_OR_DISABLED); + } + return transition; + } + + private String getInitialStatusCode() { + ObjectStatusModelDO statusModel = objectStatusModelMapper.selectInitialByObjectTypeEnabled( + PerformanceConstants.STATUS_OBJECT_TYPE); + if (statusModel == null || !StringUtils.hasText(statusModel.getStatusCode())) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_STATUS_MODEL_NOT_EXISTS_OR_DISABLED); + } + return statusModel.getStatusCode(); + } + + private String normalizePeriodMonth(String periodMonth) { + if (!StringUtils.hasText(periodMonth)) { + throw invalidParamException("绩效月份不能为空"); + } + try { + return YearMonth.parse(periodMonth.trim()).toString(); + } catch (DateTimeParseException ex) { + throw invalidParamException("绩效月份格式必须为 yyyy-MM"); + } + } + + private BigDecimal normalizeScore(BigDecimal value) { + return value.setScale(2, RoundingMode.HALF_UP); + } + + private String buildPerformanceFileName(String periodMonth, String employeeName) { + return periodMonth + "月-绩效表_" + defaultText(employeeName) + ".xlsx"; + } + + private String defaultDownloadName(PerformanceSheetDO sheet) { + return StringUtils.hasText(sheet.getFileName()) + ? sheet.getFileName() + : buildPerformanceFileName(sheet.getPeriodMonth(), sheet.getEmployeeName()); + } + + private String buildRemark(PerformanceSheetDO sheet) { + return sheet.getPeriodMonth() + " " + defaultText(sheet.getEmployeeName()) + " 绩效表"; + } + + private String statusName(String statusCode) { + return statusActionTextResolver.statusName(PerformanceConstants.STATUS_OBJECT_TYPE, statusCode); + } + + private PerformanceSheetStatusDictRespVO buildStatusDictRespVO(ObjectStatusModelDO statusModel) { + PerformanceSheetStatusDictRespVO respVO = new PerformanceSheetStatusDictRespVO(); + respVO.setStatusCode(statusModel.getStatusCode()); + respVO.setStatusName(statusModel.getStatusName()); + respVO.setSort(statusModel.getSort()); + respVO.setInitialFlag(statusModel.getInitialFlag()); + respVO.setTerminalFlag(statusModel.getTerminalFlag()); + respVO.setAllowEdit(statusModel.getAllowEdit()); + return respVO; + } + + private PerformanceSheetStatusTransitionRespVO buildStatusTransitionRespVO(ObjectStatusTransitionDO transition) { + PerformanceSheetStatusTransitionRespVO respVO = new PerformanceSheetStatusTransitionRespVO(); + respVO.setActionCode(transition.getActionCode()); + respVO.setActionName(transition.getActionName()); + respVO.setFromStatusCode(transition.getFromStatusCode()); + respVO.setToStatusCode(transition.getToStatusCode()); + respVO.setNeedReason(transition.getNeedReason()); + return respVO; + } + + private String normalizeNullableText(String value) { + if (!StringUtils.hasText(value)) { + return null; + } + return value.trim(); + } + + private String defaultText(String value) { + return StringUtils.hasText(value) ? value.trim() : ""; + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceTemplateService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceTemplateService.java new file mode 100644 index 0000000..6ff65d7 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceTemplateService.java @@ -0,0 +1,17 @@ +package com.njcn.rdms.module.project.service.performance; + +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceTemplatePageReqVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceTemplateRespVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceTemplateUploadReqVO; + +public interface PerformanceTemplateService { + + PerformanceTemplateRespVO getCurrentTemplate(); + + PageResult getTemplatePage(PerformanceTemplatePageReqVO reqVO); + + Long uploadTemplate(PerformanceTemplateUploadReqVO reqVO); + + void activateTemplate(Long id); +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceTemplateServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceTemplateServiceImpl.java new file mode 100644 index 0000000..9a8aa8f --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceTemplateServiceImpl.java @@ -0,0 +1,119 @@ +package com.njcn.rdms.module.project.service.performance; + +import com.njcn.rdms.framework.common.pojo.PageResult; +import com.njcn.rdms.framework.common.util.object.BeanUtils; +import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceScoreCellMappingRespVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceTemplatePageReqVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceTemplateRespVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceTemplateUploadReqVO; +import com.njcn.rdms.module.project.dal.dataobject.performance.PerformanceTemplateDO; +import com.njcn.rdms.module.project.dal.mysql.performance.PerformanceTemplateMapper; +import com.njcn.rdms.module.project.enums.ErrorCodeConstants; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; + +import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception; + +@Service +public class PerformanceTemplateServiceImpl implements PerformanceTemplateService { + + @Resource + private PerformanceTemplateMapper performanceTemplateMapper; + @Resource + private PerformanceProperties performanceProperties; + + @Override + public PerformanceTemplateRespVO getCurrentTemplate() { + PerformanceTemplateDO template = performanceTemplateMapper.selectCurrent(); + if (template == null) { + throw exception(ErrorCodeConstants.PERFORMANCE_TEMPLATE_CURRENT_NOT_EXISTS); + } + return toRespVO(template); + } + + @Override + public PageResult getTemplatePage(PerformanceTemplatePageReqVO reqVO) { + PageResult pageResult = performanceTemplateMapper.selectPage(reqVO); + return new PageResult<>(pageResult.getList().stream() + .map(this::toRespVO) + .toList(), pageResult.getTotal()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long uploadTemplate(PerformanceTemplateUploadReqVO reqVO) { + if (Boolean.TRUE.equals(reqVO.getActiveFlag())) { + deactivateAllTemplates(); + } + PerformanceTemplateDO template = new PerformanceTemplateDO(); + template.setTemplateName(reqVO.getTemplateName().trim()); + template.setFileId(reqVO.getFileId()); + template.setFileName(reqVO.getFileName().trim()); + template.setVersionNo(performanceTemplateMapper.selectMaxVersionNo() + 1); + template.setActiveFlag(Boolean.TRUE.equals(reqVO.getActiveFlag())); + template.setUploadUserId(SecurityFrameworkUtils.getLoginUserId()); + template.setUploadUserName(defaultText(SecurityFrameworkUtils.getLoginUserNickname())); + template.setUploadTime(LocalDateTime.now()); + template.setRemark(normalizeNullableText(reqVO.getRemark())); + performanceTemplateMapper.insert(template); + return template.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void activateTemplate(Long id) { + PerformanceTemplateDO template = validateTemplateExists(id); + deactivateAllTemplates(); + PerformanceTemplateDO update = new PerformanceTemplateDO(); + update.setId(template.getId()); + update.setActiveFlag(true); + performanceTemplateMapper.updateById(update); + } + + private PerformanceTemplateDO validateTemplateExists(Long id) { + PerformanceTemplateDO template = id == null ? null : performanceTemplateMapper.selectById(id); + if (template == null) { + throw exception(ErrorCodeConstants.PERFORMANCE_TEMPLATE_NOT_EXISTS); + } + return template; + } + + private void deactivateAllTemplates() { + PerformanceTemplateDO update = new PerformanceTemplateDO(); + update.setActiveFlag(false); + performanceTemplateMapper.update(update, new LambdaQueryWrapperX() + .eq(PerformanceTemplateDO::getActiveFlag, true)); + } + + private PerformanceTemplateRespVO toRespVO(PerformanceTemplateDO template) { + PerformanceTemplateRespVO respVO = BeanUtils.toBean(template, PerformanceTemplateRespVO.class); + respVO.setScoreCellMapping(scoreCellMapping()); + return respVO; + } + + private PerformanceScoreCellMappingRespVO scoreCellMapping() { + PerformanceProperties.ScoreCellMapping mapping = performanceProperties.getScoreCellMapping(); + PerformanceScoreCellMappingRespVO respVO = new PerformanceScoreCellMappingRespVO(); + respVO.setActualScoreTotalCell(mapping.getActualScoreTotal()); + respVO.setBaseScoreTotalCell(mapping.getBaseScoreTotal()); + respVO.setExtraScoreTotalCell(mapping.getExtraScoreTotal()); + return respVO; + } + + private String normalizeNullableText(String value) { + if (!StringUtils.hasText(value)) { + return null; + } + return value.trim(); + } + + private String defaultText(String value) { + return StringUtils.hasText(value) ? value.trim() : ""; + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/team/TeamPerformanceService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/team/TeamPerformanceService.java new file mode 100644 index 0000000..b0ff318 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/team/TeamPerformanceService.java @@ -0,0 +1,13 @@ +package com.njcn.rdms.module.project.service.performance.team; + +import com.njcn.rdms.module.project.controller.admin.performance.team.vo.TeamPerformanceRemindReqVO; +import com.njcn.rdms.module.project.controller.admin.performance.team.vo.TeamPerformanceRemindRespVO; +import com.njcn.rdms.module.project.controller.admin.performance.team.vo.TeamPerformanceSummaryReqVO; +import com.njcn.rdms.module.project.controller.admin.performance.team.vo.TeamPerformanceSummaryRespVO; + +public interface TeamPerformanceService { + + TeamPerformanceSummaryRespVO getSummary(TeamPerformanceSummaryReqVO reqVO); + + TeamPerformanceRemindRespVO remind(TeamPerformanceRemindReqVO reqVO); +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/team/TeamPerformanceServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/team/TeamPerformanceServiceImpl.java new file mode 100644 index 0000000..8444966 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/team/TeamPerformanceServiceImpl.java @@ -0,0 +1,357 @@ +package com.njcn.rdms.module.project.service.performance.team; + +import com.njcn.rdms.framework.common.pojo.CommonResult; +import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils; +import com.njcn.rdms.module.project.constant.PerformanceConstants; +import com.njcn.rdms.module.project.controller.admin.performance.team.vo.TeamPerformanceRemindReqVO; +import com.njcn.rdms.module.project.controller.admin.performance.team.vo.TeamPerformanceRemindRespVO; +import com.njcn.rdms.module.project.controller.admin.performance.team.vo.TeamPerformanceSummaryReqVO; +import com.njcn.rdms.module.project.controller.admin.performance.team.vo.TeamPerformanceSummaryRespVO; +import com.njcn.rdms.module.project.dal.dataobject.performance.PerformanceSheetDO; +import com.njcn.rdms.module.project.dal.mysql.performance.PerformanceSheetMapper; +import com.njcn.rdms.module.project.framework.notify.NotifySendEvent; +import com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants; +import com.njcn.rdms.module.project.service.team.TeamDashboardAccessService; +import com.njcn.rdms.module.system.api.user.AdminUserApi; +import com.njcn.rdms.module.system.api.user.UserManagementRelationApi; +import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO; +import com.njcn.rdms.module.system.enums.notify.NotifyMessageLevelConstants; +import jakarta.annotation.Resource; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.YearMonth; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; + +@Service +public class TeamPerformanceServiceImpl implements TeamPerformanceService { + + @Resource + private TeamDashboardAccessService teamDashboardAccessService; + @Resource + private PerformanceSheetMapper performanceSheetMapper; + @Resource + private AdminUserApi adminUserApi; + @Resource + private UserManagementRelationApi userManagementRelationApi; + @Resource + private ApplicationEventPublisher applicationEventPublisher; + + @Override + public TeamPerformanceSummaryRespVO getSummary(TeamPerformanceSummaryReqVO reqVO) { + teamDashboardAccessService.validateTeamDashboardPermission(PerformanceConstants.PERMISSION_TEAM_DASHBOARD); + String[] range = normalizePeriodMonthRange( + reqVO == null ? null : reqVO.getPeriodMonthStart(), + reqVO == null ? null : reqVO.getPeriodMonthEnd()); + List subordinateIds = teamDashboardAccessService.getAllSubordinateUserIds(); + List sheets = performanceSheetMapper + .selectListByEmployeeIdsAndPeriodMonthRange(subordinateIds, range[0], range[1]); + Map> sheetsByEmployee = sheets.stream() + .collect(Collectors.groupingBy(PerformanceSheetDO::getEmployeeId)); + Map userMap = loadUserMap(subordinateIds); + + TeamPerformanceSummaryRespVO respVO = new TeamPerformanceSummaryRespVO(); + respVO.setPeriodMonthStart(range[0]); + respVO.setPeriodMonthEnd(range[1]); + respVO.setTotalSheetCount(sheets.size()); + respVO.setPendingSendUsers(buildPendingSendUsers(subordinateIds, sheetsByEmployee, userMap)); + respVO.setPendingConfirmUsers(buildPendingConfirmUsers(sheets, userMap)); + respVO.setPendingSendCount(respVO.getPendingSendUsers().size()); + respVO.setPendingConfirmCount(respVO.getPendingConfirmUsers().size()); + respVO.setConfirmedRate(calculateConfirmedRate(sheets)); + respVO.setDeptOrgAverages(buildDeptOrgAverages(sheets)); + return respVO; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public TeamPerformanceRemindRespVO remind(TeamPerformanceRemindReqVO reqVO) { + teamDashboardAccessService.validateTeamDashboardPermission(PerformanceConstants.PERMISSION_TEAM_DASHBOARD); + String[] range = normalizePeriodMonthRange(reqVO.getPeriodMonthStart(), reqVO.getPeriodMonthEnd()); + List subordinateIds = teamDashboardAccessService.getAllSubordinateUserIds(); + List targetEmployeeIds = reqVO.getUserIds() == null + ? subordinateIds + : teamDashboardAccessService.resolveRequestedSubordinateUserIds( + reqVO.getUserIds(), PerformanceConstants.PERMISSION_TEAM_DASHBOARD); + List sheets = performanceSheetMapper + .selectListByEmployeeIdsAndPeriodMonthRange(targetEmployeeIds, range[0], range[1]); + Map> sheetsByEmployee = sheets.stream() + .collect(Collectors.groupingBy(PerformanceSheetDO::getEmployeeId)); + Map userMap = loadUserMap(targetEmployeeIds); + + int remindedCount; + if (PerformanceConstants.REMIND_TYPE_PENDING_CONFIRM.equals(reqVO.getRemindType())) { + remindedCount = remindPendingConfirm(sheets); + } else if (PerformanceConstants.REMIND_TYPE_PENDING_SEND.equals(reqVO.getRemindType())) { + remindedCount = remindPendingSend(targetEmployeeIds, sheetsByEmployee, userMap, range[0]); + } else { + throw invalidParamException("催办类型不合法"); + } + TeamPerformanceRemindRespVO respVO = new TeamPerformanceRemindRespVO(); + respVO.setRemindedCount(remindedCount); + return respVO; + } + + private List buildPendingSendUsers( + List subordinateIds, Map> sheetsByEmployee, + Map userMap) { + List result = new ArrayList<>(); + for (Long subordinateId : subordinateIds) { + List employeeSheets = sheetsByEmployee.getOrDefault(subordinateId, List.of()); + if (!hasPendingSend(employeeSheets)) { + continue; + } + AdminUserRespDTO user = userMap.get(subordinateId); + PerformanceSheetDO firstPending = employeeSheets.isEmpty() ? null : employeeSheets.get(0); + AdminUserRespDTO manager = firstPending == null ? loadDirectManagerQuietly(subordinateId) : null; + TeamPerformanceSummaryRespVO.PendingSendUser item = new TeamPerformanceSummaryRespVO.PendingSendUser(); + item.setUserId(subordinateId); + item.setUserNickname(user == null ? "" : defaultText(user.getNickname())); + item.setManagerUserId( + firstPending == null ? (manager == null ? null : manager.getId()) : firstPending.getManagerId()); + item.setManagerName(firstPending == null ? (manager == null ? "" : defaultText(manager.getNickname())) + : firstPending.getManagerName()); + item.setSheetId(firstPending == null ? null : firstPending.getId()); + item.setStatusCode(firstPending == null ? null : firstPending.getStatusCode()); + result.add(item); + } + return result; + } + + private List buildPendingConfirmUsers( + List sheets, Map userMap) { + return sheets.stream() + .filter(sheet -> PerformanceConstants.STATUS_SENT.equals(sheet.getStatusCode())) + .map(sheet -> { + AdminUserRespDTO user = userMap.get(sheet.getEmployeeId()); + TeamPerformanceSummaryRespVO.PendingConfirmUser item = new TeamPerformanceSummaryRespVO.PendingConfirmUser(); + item.setUserId(sheet.getEmployeeId()); + item.setUserNickname( + user == null ? defaultText(sheet.getEmployeeName()) : defaultText(user.getNickname())); + item.setSheetId(sheet.getId()); + item.setSentTime(sheet.getSentTime()); + return item; + }) + .toList(); + } + + private List buildDeptOrgAverages(List sheets) { + Map averages = new LinkedHashMap<>(); + for (PerformanceSheetDO sheet : sheets) { + if (!PerformanceConstants.STATUS_CONFIRMED.equals(sheet.getStatusCode()) + || sheet.getActualScoreTotal() == null + || sheet.getEmployeeDeptId() == null + || (!PerformanceConstants.ORG_TYPE_DIRECTION.equals(sheet.getDeptOrgType()) + && !PerformanceConstants.ORG_TYPE_FUNCTION.equals(sheet.getDeptOrgType()))) { + continue; + } + String key = sheet.getEmployeeDeptId() + ":" + sheet.getDeptOrgType(); + averages.computeIfAbsent(key, ignored -> new DeptAverageAccumulator( + sheet.getEmployeeDeptId(), sheet.getEmployeeDeptName(), sheet.getDeptOrgType())) + .add(sheet.getActualScoreTotal()); + } + return averages.values().stream() + .map(DeptAverageAccumulator::toRespVO) + .toList(); + } + + private BigDecimal calculateConfirmedRate(List sheets) { + if (sheets.isEmpty()) { + return BigDecimal.ZERO.setScale(2); + } + long confirmed = sheets.stream() + .filter(sheet -> PerformanceConstants.STATUS_CONFIRMED.equals(sheet.getStatusCode())) + .count(); + return BigDecimal.valueOf(confirmed * 100) + .divide(BigDecimal.valueOf(sheets.size()), 2, RoundingMode.HALF_UP); + } + + private int remindPendingConfirm(List sheets) { + int count = 0; + for (PerformanceSheetDO sheet : sheets) { + if (!PerformanceConstants.STATUS_SENT.equals(sheet.getStatusCode())) { + continue; + } + publishNotice(List.of(sheet.getEmployeeId()), + NotifyTemplateCodeConstants.PERFORMANCE_PENDING_CONFIRM_REMIND, sheet, sheet.getEmployeeName()); + count++; + } + return count; + } + + private int remindPendingSend(List targetEmployeeIds, Map> sheetsByEmployee, + Map userMap, String defaultPeriodMonth) { + int count = 0; + for (Long employeeId : targetEmployeeIds) { + List employeeSheets = sheetsByEmployee.getOrDefault(employeeId, List.of()); + if (!hasPendingSend(employeeSheets)) { + continue; + } + PerformanceSheetDO firstPending = employeeSheets.isEmpty() ? null : employeeSheets.get(0); + AdminUserRespDTO manager = firstPending == null ? loadDirectManagerQuietly(employeeId) : null; + Long managerId = firstPending == null ? (manager == null ? null : manager.getId()) + : firstPending.getManagerId(); + if (managerId == null) { + continue; + } + String employeeName = firstPending == null + ? defaultText(userMap.get(employeeId) == null ? null : userMap.get(employeeId).getNickname()) + : firstPending.getEmployeeName(); + PerformanceSheetDO noticeSheet = firstPending == null + ? buildVirtualSheet(defaultPeriodMonth, employeeId, employeeName, manager) + : firstPending; + publishNotice(List.of(managerId), + NotifyTemplateCodeConstants.PERFORMANCE_PENDING_SEND_REMIND, noticeSheet, employeeName); + count++; + } + return count; + } + + private PerformanceSheetDO buildVirtualSheet(String periodMonth, Long employeeId, String employeeName, + AdminUserRespDTO manager) { + PerformanceSheetDO sheet = new PerformanceSheetDO(); + sheet.setPeriodMonth(periodMonth); + sheet.setEmployeeId(employeeId); + sheet.setEmployeeName(employeeName); + sheet.setManagerId(manager == null ? null : manager.getId()); + sheet.setManagerName(manager == null ? "" : defaultText(manager.getNickname())); + return sheet; + } + + private void publishNotice(Collection recipients, String templateCode, PerformanceSheetDO sheet, + String employeeName) { + if (recipients == null || recipients.isEmpty()) { + return; + } + Map params = new HashMap<>(); + params.put("periodMonth", sheet.getPeriodMonth()); + params.put("employeeName", employeeName); + params.put("managerName", sheet.getManagerName()); + params.put("sheetId", sheet.getId()); + params.put("statusName", statusName(sheet.getStatusCode())); + applicationEventPublisher.publishEvent(NotifySendEvent.of(new ArrayList<>(new LinkedHashSet<>(recipients)), + templateCode, params, NotifyMessageLevelConstants.REMIND)); + } + + private Map loadUserMap(Collection userIds) { + if (userIds == null || userIds.isEmpty()) { + return Map.of(); + } + CommonResult> result = adminUserApi.getUserList(new LinkedHashSet<>(userIds)); + List users = result == null || result.getCheckedData() == null + ? List.of() + : result.getCheckedData(); + return users.stream() + .filter(Objects::nonNull) + .filter(user -> user.getId() != null) + .collect(Collectors.toMap(AdminUserRespDTO::getId, user -> user, (left, right) -> left)); + } + + private AdminUserRespDTO loadDirectManagerQuietly(Long employeeId) { + try { + CommonResult result = userManagementRelationApi.getDirectManager(employeeId); + return result == null ? null : result.getCheckedData(); + } catch (Exception ignored) { + return null; + } + } + + /** + * 校验并归一化月份区间,两者均为空时默认当前月。 + */ + private String[] normalizePeriodMonthRange(String start, String end) { + String normalizedStart = StringUtils.hasText(start) ? normalizeSinglePeriodMonth(start) : null; + String normalizedEnd = StringUtils.hasText(end) ? normalizeSinglePeriodMonth(end) : null; + if (normalizedStart == null && normalizedEnd == null) { + String currentMonth = YearMonth.now().toString(); + return new String[] { currentMonth, currentMonth }; + } + if (normalizedStart != null && normalizedEnd != null && normalizedStart.compareTo(normalizedEnd) > 0) { + throw invalidParamException("绩效月份起始不能晚于结束月份"); + } + return new String[] { normalizedStart, normalizedEnd }; + } + + private String normalizeSinglePeriodMonth(String periodMonth) { + try { + return YearMonth.parse(periodMonth.trim()).toString(); + } catch (DateTimeParseException ex) { + throw invalidParamException("绩效月份格式必须为 yyyy-MM"); + } + } + + /** + * 判断员工在区间内的绩效表是否存在“待发送”状态(无记录 / 含 draft / 含 rejected)。 + */ + private boolean hasPendingSend(List sheets) { + if (sheets == null || sheets.isEmpty()) { + return true; + } + return sheets.stream().anyMatch(sheet -> PerformanceConstants.STATUS_DRAFT.equals(sheet.getStatusCode()) + || PerformanceConstants.STATUS_REJECTED.equals(sheet.getStatusCode())); + } + + private String statusName(String statusCode) { + if (statusCode == null) { + return "未创建"; + } + return switch (statusCode) { + case PerformanceConstants.STATUS_DRAFT -> "待发送"; + case PerformanceConstants.STATUS_SENT -> "待确认"; + case PerformanceConstants.STATUS_CONFIRMED -> "已确认"; + case PerformanceConstants.STATUS_REJECTED -> "已退回"; + default -> statusCode; + }; + } + + private String defaultText(String text) { + return StringUtils.hasText(text) ? text.trim() : ""; + } + + private static class DeptAverageAccumulator { + + private final Long deptId; + private final String deptName; + private final String deptOrgType; + private BigDecimal total = BigDecimal.ZERO; + private int count = 0; + + DeptAverageAccumulator(Long deptId, String deptName, String deptOrgType) { + this.deptId = deptId; + this.deptName = deptName; + this.deptOrgType = deptOrgType; + } + + void add(BigDecimal score) { + total = total.add(score); + count++; + } + + TeamPerformanceSummaryRespVO.DeptOrgAverage toRespVO() { + TeamPerformanceSummaryRespVO.DeptOrgAverage respVO = new TeamPerformanceSummaryRespVO.DeptOrgAverage(); + respVO.setDeptId(deptId); + respVO.setDeptName(deptName); + respVO.setDeptOrgType(deptOrgType); + respVO.setConfirmedCount(count); + respVO.setAverageScore(count == 0 + ? BigDecimal.ZERO.setScale(2) + : total.divide(BigDecimal.valueOf(count), 2, RoundingMode.HALF_UP)); + return respVO; + } + } +} diff --git a/rdms-project/rdms-project-boot/src/main/resources/application.yaml b/rdms-project/rdms-project-boot/src/main/resources/application.yaml index d1fa3e1..2788404 100644 --- a/rdms-project/rdms-project-boot/src/main/resources/application.yaml +++ b/rdms-project/rdms-project-boot/src/main/resources/application.yaml @@ -133,5 +133,10 @@ rdms: cron: "0 0 12 1-31 * ?" scope: dept-ids: [100] + performance: + score-cell-mapping: + actual-score-total: J10 + base-score-total: K10 + extra-score-total: L10 debug: false diff --git a/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/dept/dto/DeptRespDTO.java b/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/dept/dto/DeptRespDTO.java index b1a274a..a3e2f13 100644 --- a/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/dept/dto/DeptRespDTO.java +++ b/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/dept/dto/DeptRespDTO.java @@ -16,6 +16,15 @@ public class DeptRespDTO { @Schema(description = "父部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Long parentId; + @Schema(description = "组织节点类型", example = "direction") + private String orgType; + + @Schema(description = "组织编码", example = "rd") + private String code; + + @Schema(description = "组织物化路径", example = "/100/101") + private String path; + @Schema(description = "部门状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer status; diff --git a/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/file/FileApi.java b/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/file/FileApi.java index 7c07915..b7230fc 100644 --- a/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/file/FileApi.java +++ b/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/file/FileApi.java @@ -21,4 +21,14 @@ public interface FileApi { @Parameter(name = "url", description = "文件 URL", required = true) CommonResult getFileByUrl(@RequestParam("url") String url); + @GetMapping(PREFIX + "/get") + @Operation(summary = "通过文件 ID 查询文件") + @Parameter(name = "id", description = "文件 ID", required = true) + CommonResult getFile(@RequestParam("id") Long id); + + @GetMapping(PREFIX + "/content") + @Operation(summary = "通过文件 ID 读取文件内容") + @Parameter(name = "id", description = "文件 ID", required = true) + CommonResult getFileContent(@RequestParam("id") Long id); + } diff --git a/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/file/dto/FileRespDTO.java b/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/file/dto/FileRespDTO.java index aefda41..a5e1555 100644 --- a/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/file/dto/FileRespDTO.java +++ b/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/file/dto/FileRespDTO.java @@ -10,6 +10,11 @@ public class FileRespDTO { */ private Long id; + /** + * 文件配置编号。 + */ + private Long configId; + /** * 文件名称。 */ diff --git a/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/enums/dept/DeptOrgTypeEnum.java b/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/enums/dept/DeptOrgTypeEnum.java index 6ce5648..1891d87 100644 --- a/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/enums/dept/DeptOrgTypeEnum.java +++ b/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/enums/dept/DeptOrgTypeEnum.java @@ -10,6 +10,7 @@ public enum DeptOrgTypeEnum { COMPANY("company"), DEPT("dept"), DIRECTION("direction"), + FUNCTION("function"), TEAM("team"); private final String type; diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/api/dept/DeptApiImpl.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/api/dept/DeptApiImpl.java index 9e91f8b..ddc0e79 100644 --- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/api/dept/DeptApiImpl.java +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/api/dept/DeptApiImpl.java @@ -6,7 +6,6 @@ import com.njcn.rdms.module.system.api.dept.dto.DeptRespDTO; import com.njcn.rdms.module.system.dal.dataobject.dept.DeptDO; import com.njcn.rdms.module.system.service.dept.DeptService; import io.swagger.v3.oas.annotations.Hidden; -import org.springframework.context.annotation.Bean; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.RestController; diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/api/file/FileApiImpl.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/api/file/FileApiImpl.java index 17cefb0..d75200b 100644 --- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/api/file/FileApiImpl.java +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/api/file/FileApiImpl.java @@ -26,4 +26,23 @@ public class FileApiImpl implements FileApi { return success(BeanUtils.toBean(file, FileRespDTO.class)); } + @Override + public CommonResult getFile(Long id) { + FileDO file = fileService.getFile(id); + return success(BeanUtils.toBean(file, FileRespDTO.class)); + } + + @Override + public CommonResult getFileContent(Long id) { + try { + FileDO file = fileService.getFile(id); + if (file == null) { + return success(null); + } + return success(fileService.getFileContent(file.getConfigId(), file.getPath())); + } catch (Exception ex) { + throw new IllegalStateException("读取文件内容失败", ex); + } + } + } diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/file/FileController.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/file/FileController.java index 16fb56f..4da0e93 100644 --- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/file/FileController.java +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/file/FileController.java @@ -102,7 +102,7 @@ public class FileController { @GetMapping("/download") @Operation(summary = "下载文件") @Parameter(name = "id", description = "编号", required = true) - @PreAuthorize("@ss.hasPermission('system:file:query')") + //@PreAuthorize("@ss.hasPermission('system:file:query')") public void downloadFile(@RequestParam("id") Long id, HttpServletResponse response) throws Exception { FileDO file = fileService.getFile(id); byte[] content = fileService.getFileContent(file.getConfigId(), file.getPath());