feat(我的绩效): 开发我的绩效功能。

This commit is contained in:
dk
2026-06-21 18:20:58 +08:00
parent 117dd91afd
commit d2224e0cfc
49 changed files with 2510 additions and 2 deletions

View File

@@ -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, "被考核员工部门信息不完整");
}

View File

@@ -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";
}

View File

@@ -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<PageResult<PerformanceSheetRespVO>> page(@Valid PerformanceSheetPageReqVO reqVO) {
return success(performanceSheetService.getSheetPage(reqVO));
}
@GetMapping("/{id}")
@Operation(summary = "获取绩效表详情")
@PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_QUERY + "')")
public CommonResult<PerformanceSheetRespVO> get(@PathVariable("id") Long id) {
return success(performanceSheetService.getSheet(id));
}
@PostMapping
@Operation(summary = "创建绩效表")
@PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_CREATE + "')")
public CommonResult<Long> 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<Boolean> 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<Boolean> 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<Boolean> 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<Boolean> 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<Boolean> 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<Boolean> 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<List<PerformanceSheetStatusLogRespVO>> 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<List<PerformanceSheetResponseRecordRespVO>> responseRecords(@PathVariable("id") Long id) {
return success(performanceSheetService.getResponseRecords(id));
}
@GetMapping("/monthly-result")
@Operation(summary = "获取月报审批绩效结果")
@PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_QUERY + "')")
public CommonResult<PerformanceMonthlyResultRespVO> 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<List<PerformanceSheetStatusDictRespVO>> statusDict() {
return success(performanceSheetService.getStatusDict());
}
@GetMapping("/status-transitions")
@Operation(summary = "获取绩效表状态动作字典")
@PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_QUERY + "')")
public CommonResult<List<PerformanceSheetStatusTransitionRespVO>> 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());
}
}

View File

@@ -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<PerformanceTemplateRespVO> current() {
return success(performanceTemplateService.getCurrentTemplate());
}
@GetMapping("/page")
@Operation(summary = "获取绩效模板分页")
@PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_TEMPLATE_QUERY + "')")
public CommonResult<PageResult<PerformanceTemplateRespVO>> page(@Valid PerformanceTemplatePageReqVO reqVO) {
return success(performanceTemplateService.getTemplatePage(reqVO));
}
@PostMapping("/upload")
@Operation(summary = "保存绩效模板上传记录")
@PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_TEMPLATE_UPDATE + "')")
public CommonResult<Long> upload(@Valid @RequestBody PerformanceTemplateUploadReqVO reqVO) {
return success(performanceTemplateService.uploadTemplate(reqVO));
}
@PostMapping("/{id}/activate")
@Operation(summary = "启用绩效模板")
@PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_TEMPLATE_UPDATE + "')")
public CommonResult<Boolean> activate(@PathVariable("id") Long id) {
performanceTemplateService.activateTemplate(id);
return success(true);
}
}

View File

@@ -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<TeamPerformanceSummaryRespVO> getSummary(@Valid TeamPerformanceSummaryReqVO reqVO) {
return success(teamPerformanceService.getSummary(reqVO));
}
@PostMapping("/remind")
@Operation(summary = "催办团队绩效")
@PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_TEAM_DASHBOARD + "')")
public CommonResult<TeamPerformanceRemindRespVO> remind(@Valid @RequestBody TeamPerformanceRemindReqVO reqVO) {
return success(teamPerformanceService.remind(reqVO));
}
}

View File

@@ -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<Long> userIds;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<PendingSendUser> pendingSendUsers;
private List<PendingConfirmUser> pendingConfirmUsers;
private List<DeptOrgAverage> 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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<Long> ids;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<Long> 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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<PerformanceSheetDO> {
default PerformanceSheetDO selectByEmployeeIdAndPeriodMonth(Long employeeId, String periodMonth) {
return selectOne(new LambdaQueryWrapperX<PerformanceSheetDO>()
.eq(PerformanceSheetDO::getEmployeeId, employeeId)
.eq(PerformanceSheetDO::getPeriodMonth, periodMonth));
}
default PageResult<PerformanceSheetDO> selectEmployeePage(Long employeeId, PerformanceSheetPageReqVO reqVO) {
LambdaQueryWrapperX<PerformanceSheetDO> wrapper = buildPageQuery(reqVO)
.eq(PerformanceSheetDO::getEmployeeId, employeeId)
.orderByDesc(PerformanceSheetDO::getPeriodMonth)
.orderByDesc(PerformanceSheetDO::getId);
return selectPage(reqVO, wrapper);
}
default PageResult<PerformanceSheetDO> selectEmployeePage(Collection<Long> employeeIds,
PerformanceSheetPageReqVO reqVO) {
if (employeeIds == null || employeeIds.isEmpty()) {
return new PageResult<>(List.of(), 0L);
}
LambdaQueryWrapperX<PerformanceSheetDO> wrapper = buildPageQuery(reqVO)
.in(PerformanceSheetDO::getEmployeeId, employeeIds)
.orderByDesc(PerformanceSheetDO::getPeriodMonth)
.orderByDesc(PerformanceSheetDO::getId);
return selectPage(reqVO, wrapper);
}
default List<PerformanceSheetDO> selectListByEmployeeIdsAndPeriodMonth(Collection<Long> employeeIds,
String periodMonth) {
if (employeeIds == null || employeeIds.isEmpty()) {
return List.of();
}
return selectList(new LambdaQueryWrapperX<PerformanceSheetDO>()
.in(PerformanceSheetDO::getEmployeeId, employeeIds)
.eq(PerformanceSheetDO::getPeriodMonth, periodMonth)
.orderByAsc(PerformanceSheetDO::getEmployeeName));
}
/**
* 按员工 ID 列表与月份区间查询绩效表。
*/
default List<PerformanceSheetDO> selectListByEmployeeIdsAndPeriodMonthRange(Collection<Long> employeeIds,
String periodMonthStart,
String periodMonthEnd) {
if (employeeIds == null || employeeIds.isEmpty()) {
return List.of();
}
return selectList(new LambdaQueryWrapperX<PerformanceSheetDO>()
.in(PerformanceSheetDO::getEmployeeId, employeeIds)
.geIfPresent(PerformanceSheetDO::getPeriodMonth, periodMonthStart)
.leIfPresent(PerformanceSheetDO::getPeriodMonth, periodMonthEnd)
.orderByAsc(PerformanceSheetDO::getEmployeeName));
}
default List<PerformanceSheetDO> selectExportList(PerformanceSheetPageReqVO reqVO, Collection<Long> 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<PerformanceSheetDO>()
.eq(PerformanceSheetDO::getId, id)
.eq(PerformanceSheetDO::getFileVersion, fileVersion));
}
default int updateStatusByIdAndStatus(PerformanceSheetDO update, Long id, String fromStatus) {
return update(update, new LambdaQueryWrapperX<PerformanceSheetDO>()
.eq(PerformanceSheetDO::getId, id)
.eq(PerformanceSheetDO::getStatusCode, fromStatus));
}
private LambdaQueryWrapperX<PerformanceSheetDO> buildPageQuery(PerformanceSheetPageReqVO reqVO) {
return new LambdaQueryWrapperX<PerformanceSheetDO>()
.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());
}
}

View File

@@ -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<PerformanceSheetResponseRecordDO> {
default List<PerformanceSheetResponseRecordDO> selectListBySheetId(Long sheetId) {
return selectList(new LambdaQueryWrapperX<PerformanceSheetResponseRecordDO>()
.eq(PerformanceSheetResponseRecordDO::getSheetId, sheetId)
.orderByDesc(PerformanceSheetResponseRecordDO::getCreateTime)
.orderByDesc(PerformanceSheetResponseRecordDO::getId));
}
default int countBySheetId(Long sheetId) {
return Math.toIntExact(selectCount(new LambdaQueryWrapperX<PerformanceSheetResponseRecordDO>()
.eq(PerformanceSheetResponseRecordDO::getSheetId, sheetId)));
}
}

View File

@@ -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<PerformanceSheetStatusLogDO> {
default List<PerformanceSheetStatusLogDO> selectListBySheetId(Long sheetId) {
return selectList(new LambdaQueryWrapperX<PerformanceSheetStatusLogDO>()
.eq(PerformanceSheetStatusLogDO::getSheetId, sheetId)
.orderByDesc(PerformanceSheetStatusLogDO::getCreateTime)
.orderByDesc(PerformanceSheetStatusLogDO::getId));
}
}

View File

@@ -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<PerformanceTemplateDO> {
default PerformanceTemplateDO selectCurrent() {
return selectOne(new LambdaQueryWrapperX<PerformanceTemplateDO>()
.eq(PerformanceTemplateDO::getActiveFlag, true)
.orderByDesc(PerformanceTemplateDO::getVersionNo)
.last("LIMIT 1"));
}
default PageResult<PerformanceTemplateDO> selectPage(PerformanceTemplatePageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<PerformanceTemplateDO>()
.likeIfPresent(PerformanceTemplateDO::getTemplateName, reqVO.getTemplateName())
.eqIfPresent(PerformanceTemplateDO::getActiveFlag, reqVO.getActiveFlag())
.orderByDesc(PerformanceTemplateDO::getVersionNo)
.orderByDesc(PerformanceTemplateDO::getId));
}
default Integer selectMaxVersionNo() {
PerformanceTemplateDO template = selectOne(new LambdaQueryWrapperX<PerformanceTemplateDO>()
.select(PerformanceTemplateDO::getVersionNo)
.orderByDesc(PerformanceTemplateDO::getVersionNo)
.last("LIMIT 1"));
return template == null || template.getVersionNo() == null ? 0 : template.getVersionNo();
}
}

View File

@@ -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";
}

View File

@@ -0,0 +1,4 @@
package com.njcn.rdms.module.project.service.performance;
public record PerformanceDownloadFile(String filename, String contentType, byte[] content) {
}

View File

@@ -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";
}
}

View File

@@ -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<PerformanceSheetRespVO> 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<Long> ids);
PerformanceDownloadFile exportByCondition(PerformanceSheetPageReqVO reqVO);
List<PerformanceSheetStatusLogRespVO> getStatusLogs(Long id);
List<PerformanceSheetResponseRecordRespVO> getResponseRecords(Long id);
PerformanceMonthlyResultRespVO getMonthlyResult(Long employeeId, String periodMonth);
List<PerformanceSheetStatusDictRespVO> getStatusDict();
List<PerformanceSheetStatusTransitionRespVO> getStatusTransitions();
}

View File

@@ -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<PerformanceSheetRespVO> getSheetPage(PerformanceSheetPageReqVO reqVO) {
PageResult<PerformanceSheetDO> pageResult;
if (reqVO.getEmployeeIds() != null) {
List<Long> 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<Long> ids) {
if (ids == null || ids.isEmpty()) {
throw invalidParamException("绩效表 ID 不能为空");
}
List<PerformanceSheetDO> sheets = new ArrayList<>();
for (Long id : ids) {
sheets.add(validateReadableSheet(id));
}
return zipSheets("绩效表_" + LocalDate.now() + ".zip", sheets);
}
@Override
public PerformanceDownloadFile exportByCondition(PerformanceSheetPageReqVO reqVO) {
List<Long> employeeIds;
if (reqVO.getEmployeeIds() != null) {
employeeIds = teamDashboardAccessService.resolveRequestedSubordinateUserIds(
reqVO.getEmployeeIds(), PerformanceConstants.PERMISSION_TEAM_DASHBOARD);
} else {
employeeIds = List.of(SecurityFrameworkUtils.getLoginUserId());
}
List<PerformanceSheetDO> sheets = performanceSheetMapper.selectExportList(reqVO, employeeIds);
return zipSheets("绩效表_" + LocalDate.now() + ".zip", sheets);
}
@Override
public List<PerformanceSheetStatusLogRespVO> getStatusLogs(Long id) {
validateReadableSheet(id);
return BeanUtils.toBean(performanceSheetStatusLogMapper.selectListBySheetId(id),
PerformanceSheetStatusLogRespVO.class);
}
@Override
public List<PerformanceSheetResponseRecordRespVO> 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<PerformanceSheetStatusDictRespVO> getStatusDict() {
return objectStatusModelMapper.selectListByObjectTypeEnabled(PerformanceConstants.STATUS_OBJECT_TYPE)
.stream()
.map(this::buildStatusDictRespVO)
.toList();
}
@Override
public List<PerformanceSheetStatusTransitionRespVO> getStatusTransitions() {
List<String> 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<AdminUserRespDTO> 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<AdminUserRespDTO> 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<DeptRespDTO> 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<PerformanceSheetDO> sheets) {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
Map<String, Integer> 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<byte[]> 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<String, Integer> 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<Long> recipients, String templateCode, PerformanceSheetDO sheet) {
if (recipients == null || recipients.isEmpty()) {
return;
}
Map<String, Object> 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() : "";
}
}

View File

@@ -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<PerformanceTemplateRespVO> getTemplatePage(PerformanceTemplatePageReqVO reqVO);
Long uploadTemplate(PerformanceTemplateUploadReqVO reqVO);
void activateTemplate(Long id);
}

View File

@@ -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<PerformanceTemplateRespVO> getTemplatePage(PerformanceTemplatePageReqVO reqVO) {
PageResult<PerformanceTemplateDO> 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<PerformanceTemplateDO>()
.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() : "";
}
}

View File

@@ -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);
}

View File

@@ -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<Long> subordinateIds = teamDashboardAccessService.getAllSubordinateUserIds();
List<PerformanceSheetDO> sheets = performanceSheetMapper
.selectListByEmployeeIdsAndPeriodMonthRange(subordinateIds, range[0], range[1]);
Map<Long, List<PerformanceSheetDO>> sheetsByEmployee = sheets.stream()
.collect(Collectors.groupingBy(PerformanceSheetDO::getEmployeeId));
Map<Long, AdminUserRespDTO> 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<Long> subordinateIds = teamDashboardAccessService.getAllSubordinateUserIds();
List<Long> targetEmployeeIds = reqVO.getUserIds() == null
? subordinateIds
: teamDashboardAccessService.resolveRequestedSubordinateUserIds(
reqVO.getUserIds(), PerformanceConstants.PERMISSION_TEAM_DASHBOARD);
List<PerformanceSheetDO> sheets = performanceSheetMapper
.selectListByEmployeeIdsAndPeriodMonthRange(targetEmployeeIds, range[0], range[1]);
Map<Long, List<PerformanceSheetDO>> sheetsByEmployee = sheets.stream()
.collect(Collectors.groupingBy(PerformanceSheetDO::getEmployeeId));
Map<Long, AdminUserRespDTO> 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<TeamPerformanceSummaryRespVO.PendingSendUser> buildPendingSendUsers(
List<Long> subordinateIds, Map<Long, List<PerformanceSheetDO>> sheetsByEmployee,
Map<Long, AdminUserRespDTO> userMap) {
List<TeamPerformanceSummaryRespVO.PendingSendUser> result = new ArrayList<>();
for (Long subordinateId : subordinateIds) {
List<PerformanceSheetDO> 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<TeamPerformanceSummaryRespVO.PendingConfirmUser> buildPendingConfirmUsers(
List<PerformanceSheetDO> sheets, Map<Long, AdminUserRespDTO> 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<TeamPerformanceSummaryRespVO.DeptOrgAverage> buildDeptOrgAverages(List<PerformanceSheetDO> sheets) {
Map<String, DeptAverageAccumulator> 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<PerformanceSheetDO> 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<PerformanceSheetDO> 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<Long> targetEmployeeIds, Map<Long, List<PerformanceSheetDO>> sheetsByEmployee,
Map<Long, AdminUserRespDTO> userMap, String defaultPeriodMonth) {
int count = 0;
for (Long employeeId : targetEmployeeIds) {
List<PerformanceSheetDO> 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<Long> recipients, String templateCode, PerformanceSheetDO sheet,
String employeeName) {
if (recipients == null || recipients.isEmpty()) {
return;
}
Map<String, Object> 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<Long, AdminUserRespDTO> loadUserMap(Collection<Long> userIds) {
if (userIds == null || userIds.isEmpty()) {
return Map.of();
}
CommonResult<List<AdminUserRespDTO>> result = adminUserApi.getUserList(new LinkedHashSet<>(userIds));
List<AdminUserRespDTO> 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<AdminUserRespDTO> 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<PerformanceSheetDO> 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;
}
}
}

View File

@@ -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

View File

@@ -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;

View File

@@ -21,4 +21,14 @@ public interface FileApi {
@Parameter(name = "url", description = "文件 URL", required = true)
CommonResult<FileRespDTO> getFileByUrl(@RequestParam("url") String url);
@GetMapping(PREFIX + "/get")
@Operation(summary = "通过文件 ID 查询文件")
@Parameter(name = "id", description = "文件 ID", required = true)
CommonResult<FileRespDTO> getFile(@RequestParam("id") Long id);
@GetMapping(PREFIX + "/content")
@Operation(summary = "通过文件 ID 读取文件内容")
@Parameter(name = "id", description = "文件 ID", required = true)
CommonResult<byte[]> getFileContent(@RequestParam("id") Long id);
}

View File

@@ -10,6 +10,11 @@ public class FileRespDTO {
*/
private Long id;
/**
* 文件配置编号。
*/
private Long configId;
/**
* 文件名称。
*/

View File

@@ -10,6 +10,7 @@ public enum DeptOrgTypeEnum {
COMPANY("company"),
DEPT("dept"),
DIRECTION("direction"),
FUNCTION("function"),
TEAM("team");
private final String type;

View File

@@ -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;

View File

@@ -26,4 +26,23 @@ public class FileApiImpl implements FileApi {
return success(BeanUtils.toBean(file, FileRespDTO.class));
}
@Override
public CommonResult<FileRespDTO> getFile(Long id) {
FileDO file = fileService.getFile(id);
return success(BeanUtils.toBean(file, FileRespDTO.class));
}
@Override
public CommonResult<byte[]> 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);
}
}
}

View File

@@ -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());