diff --git a/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ErrorCodeConstants.java b/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ErrorCodeConstants.java index 99ba0ed..21cc688 100644 --- a/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ErrorCodeConstants.java +++ b/rdms-project/rdms-project-api/src/main/java/com/njcn/rdms/module/project/enums/ErrorCodeConstants.java @@ -261,6 +261,15 @@ 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, "仅项目负责人可创建或维护该项目的项目半月报"); + ErrorCode WORK_REPORT_SIGN_OFF_NOT_ALLOWED = new ErrorCode(1_008_010_016, "当前月报未处于待员工确认状态"); + ErrorCode WORK_REPORT_SIGN_OFF_BATCH_LOCKED = new ErrorCode(1_008_010_017, "该月份已下签,禁止重复操作"); + ErrorCode WORK_REPORT_MONTHLY_SIGN_OFF_NOT_EXISTS = new ErrorCode(1_008_010_018, "月报下签实例不存在"); + ErrorCode WORK_REPORT_MONTHLY_SIGN_OFF_CONFIRM_ONLY_REPORTER = new ErrorCode(1_008_010_019, "仅月报填报人可确认下签"); + ErrorCode WORK_REPORT_MONTHLY_SIGN_OFF_APPROVAL_RECORD_NOT_EXISTS = new ErrorCode(1_008_010_020, "月报缺少审批通过记录,无法完成下签确认"); + ErrorCode WORK_REPORT_MONTHLY_SIGN_OFF_CANDIDATE_EMPTY = new ErrorCode(1_008_010_021, "没有可下签的月报数据"); + ErrorCode SIGN_OFF_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_010_022, "当前下签状态为「{}」,不支持「{}」操作"); + ErrorCode SIGN_OFF_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_010_023, "「{}」操作必须填写原因"); + ErrorCode SIGN_OFF_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_010_024, "下签状态定义不存在或已停用"); // ========== 绩效管理 1_008_011_xxx ========== ErrorCode PERFORMANCE_TEMPLATE_NOT_EXISTS = new ErrorCode(1_008_011_001, "绩效模板不存在"); @@ -282,4 +291,12 @@ public interface ErrorCodeConstants { 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, "被考核员工部门信息不完整"); + ErrorCode PERFORMANCE_SHEET_SIGN_OFF_NOT_ALLOWED = new ErrorCode(1_008_011_020, "当前绩效表未处于待员工确认状态"); + ErrorCode PERFORMANCE_SHEET_SIGN_OFF_BATCH_LOCKED = new ErrorCode(1_008_011_021, "该月份已下签,禁止重复操作"); + ErrorCode PERFORMANCE_SHEET_SIGN_OFF_NOT_EXISTS = new ErrorCode(1_008_011_022, "绩效下签实例不存在"); + ErrorCode PERFORMANCE_SHEET_IMPORT_ZIP_EMPTY = new ErrorCode(1_008_011_023, "导入压缩包不能为空"); + ErrorCode PERFORMANCE_SHEET_IMPORT_TARGET_EMPTY = new ErrorCode(1_008_011_024, "没有匹配到可导入的绩效表"); + ErrorCode PERFORMANCE_SHEET_SIGN_OFF_CANDIDATE_EMPTY = new ErrorCode(1_008_011_025, "没有可下签的绩效表数据"); + ErrorCode PERFORMANCE_SHEET_MONTHLY_REPORT_NOT_EXISTS = new ErrorCode(1_008_011_026, "未找到对应月报,无法同步绩效结果"); + ErrorCode PERFORMANCE_SHEET_MONTHLY_APPROVAL_RECORD_NOT_EXISTS = new ErrorCode(1_008_011_027, "未找到对应月报审批记录,无法同步绩效结果"); } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/PerformanceConstants.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/PerformanceConstants.java index a61992c..abd4f29 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/PerformanceConstants.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/PerformanceConstants.java @@ -36,4 +36,6 @@ public final class PerformanceConstants { 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"; + public static final String PERMISSION_SHEET_IMPORT = "project:performance-sheet:import"; + public static final String PERMISSION_SHEET_SIGN_OFF = "project:performance-sheet:sign-off"; } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/SignOffConstants.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/SignOffConstants.java new file mode 100644 index 0000000..213c254 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/SignOffConstants.java @@ -0,0 +1,21 @@ +package com.njcn.rdms.module.project.constant; + +/** + * 员工下签常量。 + */ +public final class SignOffConstants { + + private SignOffConstants() { + } + + public static final String STATUS_OBJECT_TYPE = "employee_sign_off"; + + public static final String STATUS_NOT_STARTED = "not_started"; + public static final String STATUS_PENDING_EMPLOYEE_CONFIRM = "pending_employee_confirm"; + public static final String STATUS_EMPLOYEE_CONFIRMED = "employee_confirmed"; + public static final String STATUS_EMPLOYEE_REJECTED = "employee_rejected"; + + public static final String ACTION_ISSUE = "issue"; + public static final String ACTION_CONFIRM = "confirm"; + public static final String ACTION_REJECT = "reject"; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/WorkReportConstants.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/WorkReportConstants.java index 07c8d2a..da1299d 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/WorkReportConstants.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/WorkReportConstants.java @@ -39,4 +39,6 @@ public final class WorkReportConstants { public static final String PERMISSION_EXPORT = "project:work-report:export"; public static final String PERMISSION_PROJECT_OWNER = "project:work-report:project-owner"; public static final String PERMISSION_TEAM_DASHBOARD = "project:work-report:team-dashboard"; + public static final String PERMISSION_SIGN_OFF_QUERY = "project:work-report:sign-off-query"; + public static final String PERMISSION_SIGN_OFF_CONFIRM = "project:work-report:sign-off-confirm"; } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/PerformanceSheetController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/PerformanceSheetController.java index 7842050..2310f4e 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/PerformanceSheetController.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/PerformanceSheetController.java @@ -8,10 +8,13 @@ import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceM 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.PerformanceSheetImportZipReqVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetImportZipRespVO; import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetPageReqVO; import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetPrefillRespVO; 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.PerformanceSheetSignOffReqVO; 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; @@ -124,6 +127,21 @@ public class PerformanceSheetController { return success(true); } + @PostMapping("/import-zip") + @Operation(summary = "导入绩效压缩包") + @PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_IMPORT + "')") + public CommonResult importZip(@Valid PerformanceSheetImportZipReqVO reqVO) + throws Exception { + return success(performanceSheetService.importZip(reqVO)); + } + + @PostMapping("/sign-off") + @Operation(summary = "一键下签") + @PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_SIGN_OFF + "')") + public CommonResult signOff(@Valid @RequestBody PerformanceSheetSignOffReqVO reqVO) { + return success(performanceSheetService.signOff(reqVO)); + } + @GetMapping("/{id}/download") @Operation(summary = "下载单条绩效表") @PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_EXPORT + "')") diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetImportZipReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetImportZipReqVO.java new file mode 100644 index 0000000..4d28854 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetImportZipReqVO.java @@ -0,0 +1,25 @@ +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 lombok.Data; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@Schema(description = "管理后台 - 绩效压缩包导入 Request VO") +@Data +public class PerformanceSheetImportZipReqVO { + + @Schema(description = "zip 文件", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "导入文件不能为空") + private MultipartFile file; + + @Schema(description = "绩效月份,格式 yyyy-MM", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-06") + @NotBlank(message = "绩效月份不能为空") + private String periodMonth; + + @Schema(description = "限定员工 ID 列表") + private List employeeIds; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetImportZipRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetImportZipRespVO.java new file mode 100644 index 0000000..8c3e0e8 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetImportZipRespVO.java @@ -0,0 +1,23 @@ +package com.njcn.rdms.module.project.controller.admin.performance.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - 绩效压缩包导入结果 Response VO") +@Data +public class PerformanceSheetImportZipRespVO { + + private Integer workbookCount; + + private Integer sheetCount; + + private Integer successCount; + + private Integer skippedCount; + + private Integer failedCount; + + private List messages; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetPageReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetPageReqVO.java index 5ac539a..0c77365 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetPageReqVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetPageReqVO.java @@ -38,4 +38,7 @@ public class PerformanceSheetPageReqVO extends PageParam { @Schema(description = "状态编码", example = "sent") private String statusCode; + + @Schema(description = "下签状态编码", example = "pending_employee_confirm") + private String signOffStatus; } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetRespVO.java index 2cf80ab..e4c0559 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetRespVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetRespVO.java @@ -25,6 +25,8 @@ public class PerformanceSheetRespVO { private Integer fileVersion; private String statusCode; private String statusName; + private String signOffStatus; + private String signOffStatusName; private BigDecimal actualScoreTotal; private BigDecimal baseScoreTotal; private BigDecimal extraScoreTotal; diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetSignOffReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetSignOffReqVO.java new file mode 100644 index 0000000..54f92f7 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/performance/vo/PerformanceSheetSignOffReqVO.java @@ -0,0 +1,24 @@ +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.Size; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - 绩效一键下签 Request VO") +@Data +public class PerformanceSheetSignOffReqVO { + + @Schema(description = "绩效月份,格式 yyyy-MM", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-06") + @NotBlank(message = "绩效月份不能为空") + private String periodMonth; + + @Schema(description = "员工 ID 列表;为空则默认全部下属") + private List employeeIds; + + @Schema(description = "备注") + @Size(max = 500, message = "备注长度不能超过 500 个字符") + private String remark; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/MonthlyReportController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/MonthlyReportController.java index 13e4313..d45bcd9 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/MonthlyReportController.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/MonthlyReportController.java @@ -17,6 +17,8 @@ import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.Month import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportRefreshDraftReqVO; import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportRespVO; import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportSaveReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportSignOffConfirmReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportSignOffDetailRespVO; import com.njcn.rdms.module.project.service.workreport.export.WorkReportContentExportService; import com.njcn.rdms.module.project.service.workreport.monthly.MonthlyReportService; import io.swagger.v3.oas.annotations.Operation; @@ -111,6 +113,20 @@ public class MonthlyReportController { return success(monthlyReportService.getMonthlyApprovalPage(reqVO)); } + @GetMapping("/sign-off-page") + @Operation(summary = "获取待我确认的月报分页") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_SIGN_OFF_QUERY + "')") + public CommonResult> getMonthlySignOffPage(@Valid MonthlyReportPageReqVO reqVO) { + return success(monthlyReportService.getMonthlySignOffPage(reqVO)); + } + + @GetMapping("/{id}/sign-off-detail") + @Operation(summary = "获取月报下签详情") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_SIGN_OFF_QUERY + "')") + public CommonResult getMonthlySignOffDetail(@PathVariable("id") Long id) { + return success(monthlyReportService.getMonthlySignOffDetail(id)); + } + @PostMapping("/{id}/submit") @Operation(summary = "提交月报") @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_UPDATE + "')") @@ -137,6 +153,15 @@ public class MonthlyReportController { return success(true); } + @PostMapping("/{id}/confirm-sign-off") + @Operation(summary = "确认月报下签") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_SIGN_OFF_CONFIRM + "')") + public CommonResult confirmMonthlySignOff(@PathVariable("id") Long id, + @Valid @RequestBody MonthlyReportSignOffConfirmReqVO reqVO) { + monthlyReportService.confirmMonthlySignOff(id, reqVO); + return success(true); + } + @DeleteMapping("/{id}") @Operation(summary = "删除月报") @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_DELETE + "')") diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportApprovalRecordRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportApprovalRecordRespVO.java index 2bc7682..eb159d4 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportApprovalRecordRespVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportApprovalRecordRespVO.java @@ -36,10 +36,14 @@ public class MonthlyReportApprovalRecordRespVO { private String employeeSignName; + private Long employeeSignatureFileId; + private LocalDate employeeSignedDate; private String supervisorSignName; + private Long supervisorSignatureFileId; + private LocalDate supervisorSignedDate; private Long auditorUserId; diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportPageReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportPageReqVO.java index bc4cec4..4da0ed0 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportPageReqVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportPageReqVO.java @@ -14,4 +14,7 @@ public class MonthlyReportPageReqVO extends WorkReportBasePageReqVO { @Schema(description = "团队视角下的填报人用户编号列表") private List reporterIds; + + @Schema(description = "下签状态编码", example = "pending_employee_confirm") + private String signOffStatus; } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportRespVO.java index a2afa01..8213a1f 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportRespVO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportRespVO.java @@ -27,6 +27,8 @@ public class MonthlyReportRespVO { private LocalDate periodEndDate; private String statusCode; private String statusName; + private String signOffStatus; + private String signOffStatusName; private Boolean allowEdit; private Boolean terminal; private BigDecimal totalWorkHours; diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportSignOffConfirmReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportSignOffConfirmReqVO.java new file mode 100644 index 0000000..b4e009e --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportSignOffConfirmReqVO.java @@ -0,0 +1,24 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; + +import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY; + +@Schema(description = "管理后台 - 月报下签确认 Request VO") +@Data +public class MonthlyReportSignOffConfirmReqVO { + + @Schema(description = "面谈日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "面谈日期不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private LocalDate meetingDate; + + @Schema(description = "员工签字日期") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private LocalDate employeeSignedDate; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportSignOffDetailRespVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportSignOffDetailRespVO.java new file mode 100644 index 0000000..4012bb1 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportSignOffDetailRespVO.java @@ -0,0 +1,13 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 月报下签详情 Response VO") +@Data +public class MonthlyReportSignOffDetailRespVO { + + private MonthlyReportRespVO report; + + private MonthlyReportApprovalRecordRespVO latestApprovalRecord; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/performance/PerformanceSheetDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/performance/PerformanceSheetDO.java index 582e0b6..deac0a0 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/performance/PerformanceSheetDO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/performance/PerformanceSheetDO.java @@ -46,6 +46,8 @@ public class PerformanceSheetDO extends BaseDO { private String statusCode; + private String signOffStatus; + private BigDecimal actualScoreTotal; private BigDecimal baseScoreTotal; diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/performance/PerformanceSheetSignOffDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/performance/PerformanceSheetSignOffDO.java new file mode 100644 index 0000000..8026b16 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/performance/PerformanceSheetSignOffDO.java @@ -0,0 +1,47 @@ +package com.njcn.rdms.module.project.dal.dataobject.performance; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +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_sheet_sign_off") +@Data +@EqualsAndHashCode(callSuper = true) +public class PerformanceSheetSignOffDO extends BaseDO { + + @TableId + private Long id; + + private Long performanceSheetId; + + private Long batchNo; + + private String statusCode; + + private Long issueUserId; + + private String issueUserName; + + private LocalDateTime issueTime; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private Long confirmUserId; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String confirmUserName; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private LocalDateTime confirmTime; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String lastRejectReason; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/performance/PerformanceSheetSignOffRecordDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/performance/PerformanceSheetSignOffRecordDO.java new file mode 100644 index 0000000..dcb87e3 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/performance/PerformanceSheetSignOffRecordDO.java @@ -0,0 +1,37 @@ +package com.njcn.rdms.module.project.dal.dataobject.performance; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 绩效下签操作记录。 + */ +@TableName("rdms_performance_sheet_sign_off_record") +@Data +@EqualsAndHashCode(callSuper = true) +public class PerformanceSheetSignOffRecordDO extends BaseDO { + + @TableId + private Long id; + + private Long signOffId; + + private Long performanceSheetId; + + private Long batchNo; + + private String actionType; + + private String fromSignOffStatus; + + private String toSignOffStatus; + + private String reason; + + private Long operatorUserId; + + private String operatorName; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/signoff/EmployeeSignOffBatchDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/signoff/EmployeeSignOffBatchDO.java new file mode 100644 index 0000000..36fe196 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/signoff/EmployeeSignOffBatchDO.java @@ -0,0 +1,33 @@ +package com.njcn.rdms.module.project.dal.dataobject.signoff; + +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_employee_sign_off_batch") +@Data +@EqualsAndHashCode(callSuper = true) +public class EmployeeSignOffBatchDO extends BaseDO { + + @TableId + private Long id; + + private String periodMonth; + + private Long deptId; + + private String deptName; + + private Long operatorUserId; + + private String operatorName; + + private Integer lockedFlag; + + private String remark; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/monthly/MonthlyReportApprovalRecordDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/monthly/MonthlyReportApprovalRecordDO.java index 6c953a8..54ccd9f 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/monthly/MonthlyReportApprovalRecordDO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/monthly/MonthlyReportApprovalRecordDO.java @@ -56,12 +56,18 @@ public class MonthlyReportApprovalRecordDO extends BaseDO { @TableField(updateStrategy = FieldStrategy.ALWAYS) private String employeeSignName; + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private Long employeeSignatureFileId; + @TableField(updateStrategy = FieldStrategy.ALWAYS) private LocalDate employeeSignedDate; @TableField(updateStrategy = FieldStrategy.ALWAYS) private String supervisorSignName; + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private Long supervisorSignatureFileId; + @TableField(updateStrategy = FieldStrategy.ALWAYS) private LocalDate supervisorSignedDate; diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/monthly/MonthlyReportDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/monthly/MonthlyReportDO.java index 6cf08c6..48d1e4a 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/monthly/MonthlyReportDO.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/monthly/MonthlyReportDO.java @@ -47,6 +47,8 @@ public class MonthlyReportDO extends BaseDO { private String statusCode; + private String signOffStatus; + @TableField(updateStrategy = FieldStrategy.ALWAYS) private BigDecimal totalWorkHours; diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/monthly/MonthlyReportSignOffDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/monthly/MonthlyReportSignOffDO.java new file mode 100644 index 0000000..12a3041 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/monthly/MonthlyReportSignOffDO.java @@ -0,0 +1,44 @@ +package com.njcn.rdms.module.project.dal.dataobject.workreport.monthly; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +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_work_report_monthly_sign_off") +@Data +@EqualsAndHashCode(callSuper = true) +public class MonthlyReportSignOffDO extends BaseDO { + + @TableId + private Long id; + + private Long monthlyReportId; + + private Long batchNo; + + private String statusCode; + + private Long issueUserId; + + private String issueUserName; + + private LocalDateTime issueTime; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private Long confirmUserId; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String confirmUserName; + + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private LocalDateTime confirmTime; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/monthly/MonthlyReportSignOffRecordDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/monthly/MonthlyReportSignOffRecordDO.java new file mode 100644 index 0000000..4188a38 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/workreport/monthly/MonthlyReportSignOffRecordDO.java @@ -0,0 +1,37 @@ +package com.njcn.rdms.module.project.dal.dataobject.workreport.monthly; + +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_work_report_monthly_sign_off_record") +@Data +@EqualsAndHashCode(callSuper = true) +public class MonthlyReportSignOffRecordDO extends BaseDO { + + @TableId + private Long id; + + private Long signOffId; + + private Long monthlyReportId; + + private Long approvalRecordId; + + private Long batchNo; + + private String actionType; + + private String fromSignOffStatus; + + private String toSignOffStatus; + + private Long operatorUserId; + + private String operatorName; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/performance/PerformanceSheetMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/performance/PerformanceSheetMapper.java index d018e31..71ae2ed 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/performance/PerformanceSheetMapper.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/performance/PerformanceSheetMapper.java @@ -1,8 +1,8 @@ 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.framework.mybatis.core.mapper.BaseMapperX; import com.njcn.rdms.module.project.constant.PerformanceConstants; import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetPageReqVO; import com.njcn.rdms.module.project.dal.dataobject.performance.PerformanceSheetDO; @@ -102,6 +102,14 @@ public interface PerformanceSheetMapper extends BaseMapperX .eq(PerformanceSheetDO::getStatusCode, fromStatus)); } + default int updateByIdAndStatusAndSignOffStatus(PerformanceSheetDO update, Long id, String fromStatus, + String fromSignOffStatus) { + return update(update, new LambdaQueryWrapperX() + .eq(PerformanceSheetDO::getId, id) + .eq(PerformanceSheetDO::getStatusCode, fromStatus) + .eq(PerformanceSheetDO::getSignOffStatus, fromSignOffStatus)); + } + private LambdaQueryWrapperX buildPageQuery(PerformanceSheetPageReqVO reqVO) { return new LambdaQueryWrapperX() .geIfPresent(PerformanceSheetDO::getPeriodMonth, reqVO.getPeriodMonthStart()) @@ -111,6 +119,7 @@ public interface PerformanceSheetMapper extends BaseMapperX .likeIfPresent(PerformanceSheetDO::getEmployeeDeptName, reqVO.getEmployeeDeptName()) .eqIfPresent(PerformanceSheetDO::getManagerId, reqVO.getManagerId()) .likeIfPresent(PerformanceSheetDO::getManagerName, reqVO.getManagerName()) - .eqIfPresent(PerformanceSheetDO::getStatusCode, reqVO.getStatusCode()); + .eqIfPresent(PerformanceSheetDO::getStatusCode, reqVO.getStatusCode()) + .eqIfPresent(PerformanceSheetDO::getSignOffStatus, reqVO.getSignOffStatus()); } } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/performance/PerformanceSheetSignOffMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/performance/PerformanceSheetSignOffMapper.java new file mode 100644 index 0000000..762a219 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/performance/PerformanceSheetSignOffMapper.java @@ -0,0 +1,22 @@ +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.PerformanceSheetSignOffDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PerformanceSheetSignOffMapper extends BaseMapperX { + + default PerformanceSheetSignOffDO selectByPerformanceSheetId(Long performanceSheetId) { + return selectOne(new LambdaQueryWrapperX() + .eq(PerformanceSheetSignOffDO::getPerformanceSheetId, performanceSheetId)); + } + + default int updateByPerformanceSheetIdAndStatus(PerformanceSheetSignOffDO update, Long performanceSheetId, + String fromStatus) { + return update(update, new LambdaQueryWrapperX() + .eq(PerformanceSheetSignOffDO::getPerformanceSheetId, performanceSheetId) + .eq(PerformanceSheetSignOffDO::getStatusCode, fromStatus)); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/performance/PerformanceSheetSignOffRecordMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/performance/PerformanceSheetSignOffRecordMapper.java new file mode 100644 index 0000000..21f0955 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/performance/PerformanceSheetSignOffRecordMapper.java @@ -0,0 +1,9 @@ +package com.njcn.rdms.module.project.dal.mysql.performance; + +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.module.project.dal.dataobject.performance.PerformanceSheetSignOffRecordDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PerformanceSheetSignOffRecordMapper extends BaseMapperX { +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/signoff/EmployeeSignOffBatchMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/signoff/EmployeeSignOffBatchMapper.java new file mode 100644 index 0000000..439bf5c --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/signoff/EmployeeSignOffBatchMapper.java @@ -0,0 +1,16 @@ +package com.njcn.rdms.module.project.dal.mysql.signoff; + +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.signoff.EmployeeSignOffBatchDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface EmployeeSignOffBatchMapper extends BaseMapperX { + + default EmployeeSignOffBatchDO selectByPeriodMonthAndDeptId(String periodMonth, Long deptId) { + return selectOne(new LambdaQueryWrapperX() + .eq(EmployeeSignOffBatchDO::getPeriodMonth, periodMonth) + .eq(EmployeeSignOffBatchDO::getDeptId, deptId)); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/monthly/MonthlyReportApprovalRecordMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/monthly/MonthlyReportApprovalRecordMapper.java index a1924de..1854803 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/monthly/MonthlyReportApprovalRecordMapper.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/monthly/MonthlyReportApprovalRecordMapper.java @@ -26,4 +26,13 @@ public interface MonthlyReportApprovalRecordMapper extends BaseMapperX() .eq(MonthlyReportApprovalRecordDO::getMonthlyReportId, monthlyReportId)); } + + default MonthlyReportApprovalRecordDO selectLatestApprovedByMonthlyReportId(Long monthlyReportId) { + return selectOne(new LambdaQueryWrapperX() + .eq(MonthlyReportApprovalRecordDO::getMonthlyReportId, monthlyReportId) + .eq(MonthlyReportApprovalRecordDO::getConclusion, "approved") + .orderByDesc(MonthlyReportApprovalRecordDO::getApprovalRound) + .orderByDesc(MonthlyReportApprovalRecordDO::getId) + .last("LIMIT 1")); + } } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/monthly/MonthlyReportMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/monthly/MonthlyReportMapper.java index 0a5d698..d655248 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/monthly/MonthlyReportMapper.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/monthly/MonthlyReportMapper.java @@ -147,6 +147,14 @@ public interface MonthlyReportMapper extends BaseMapperX { .eq(MonthlyReportDO::getStatusCode, fromStatus)); } + default int updateByIdAndStatusAndSignOffStatus(MonthlyReportDO update, Long id, String fromStatus, + String fromSignOffStatus) { + return update(update, new LambdaQueryWrapperX() + .eq(MonthlyReportDO::getId, id) + .eq(MonthlyReportDO::getStatusCode, fromStatus) + .eq(MonthlyReportDO::getSignOffStatus, fromSignOffStatus)); + } + default int updateByIdAndStatusesAndReporterId(MonthlyReportDO update, Long id, Collection statuses, Long reporterId) { return update(update, new LambdaQueryWrapperX() @@ -177,6 +185,7 @@ public interface MonthlyReportMapper extends BaseMapperX { private LambdaQueryWrapperX buildPageQuery(MonthlyReportPageReqVO reqVO) { LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX() .eqIfPresent(MonthlyReportDO::getStatusCode, reqVO.getStatusCode()) + .eqIfPresent(MonthlyReportDO::getSignOffStatus, reqVO.getSignOffStatus()) .betweenIfPresent(MonthlyReportDO::getPeriodStartDate, reqVO.getPeriodStartDate()) .betweenIfPresent(MonthlyReportDO::getSubmitTime, reqVO.getSubmitTime()); if (StringUtils.hasText(reqVO.getKeyword())) { diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/monthly/MonthlyReportSignOffMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/monthly/MonthlyReportSignOffMapper.java new file mode 100644 index 0000000..f62b8f6 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/monthly/MonthlyReportSignOffMapper.java @@ -0,0 +1,22 @@ +package com.njcn.rdms.module.project.dal.mysql.workreport.monthly; + +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.workreport.monthly.MonthlyReportSignOffDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface MonthlyReportSignOffMapper extends BaseMapperX { + + default MonthlyReportSignOffDO selectByMonthlyReportId(Long monthlyReportId) { + return selectOne(new LambdaQueryWrapperX() + .eq(MonthlyReportSignOffDO::getMonthlyReportId, monthlyReportId)); + } + + default int updateByMonthlyReportIdAndStatus(MonthlyReportSignOffDO update, Long monthlyReportId, + String fromStatus) { + return update(update, new LambdaQueryWrapperX() + .eq(MonthlyReportSignOffDO::getMonthlyReportId, monthlyReportId) + .eq(MonthlyReportSignOffDO::getStatusCode, fromStatus)); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/monthly/MonthlyReportSignOffRecordMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/monthly/MonthlyReportSignOffRecordMapper.java new file mode 100644 index 0000000..8f86c97 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/workreport/monthly/MonthlyReportSignOffRecordMapper.java @@ -0,0 +1,9 @@ +package com.njcn.rdms.module.project.dal.mysql.workreport.monthly; + +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.module.project.dal.dataobject.workreport.monthly.MonthlyReportSignOffRecordDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface MonthlyReportSignOffRecordMapper extends BaseMapperX { +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/rpc/config/RpcConfiguration.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/rpc/config/RpcConfiguration.java index ddc04a6..8306562 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/rpc/config/RpcConfiguration.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/framework/rpc/config/RpcConfiguration.java @@ -11,6 +11,7 @@ import com.njcn.rdms.module.system.api.permission.PermissionApi; import com.njcn.rdms.module.system.api.permission.UserVisibilityConfigApi; 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.UserSignatureApi; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Configuration; @@ -22,6 +23,6 @@ import org.springframework.context.annotation.Configuration; {AdminUserApi.class, ObjectPermissionApi.class, DictDataApi.class, FileApi.class, PermissionApi.class, OrgLeaderApi.class, UserVisibilityConfigApi.class, DeptApi.class, PostApi.class, UserManagementRelationApi.class, - NotifyMessageSendApi.class}) + NotifyMessageSendApi.class, UserSignatureApi.class}) public class RpcConfiguration { } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceSheetService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceSheetService.java index e0fcb66..529215b 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceSheetService.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceSheetService.java @@ -4,10 +4,13 @@ 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.PerformanceSheetImportZipReqVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetImportZipRespVO; import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetPageReqVO; import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetPrefillRespVO; 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.PerformanceSheetSignOffReqVO; 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; @@ -36,6 +39,10 @@ public interface PerformanceSheetService { void reject(Long id, PerformanceSheetStatusActionReqVO reqVO); + PerformanceSheetImportZipRespVO importZip(PerformanceSheetImportZipReqVO reqVO) throws Exception; + + Long signOff(PerformanceSheetSignOffReqVO reqVO); + PerformanceDownloadFile download(Long id); PerformanceDownloadFile batchDownload(Collection ids); diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceSheetServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceSheetServiceImpl.java index d096ede..d2abc9c 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceSheetServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceSheetServiceImpl.java @@ -6,37 +6,52 @@ 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.PerformanceExcelCellConstants; import com.njcn.rdms.module.project.constant.PerformanceConstants; +import com.njcn.rdms.module.project.constant.SignOffConstants; +import com.njcn.rdms.module.project.constant.WorkReportConstants; 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.PerformanceSheetImportZipReqVO; +import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetImportZipRespVO; import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetPageReqVO; import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetPrefillRespVO; 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.PerformanceSheetSignOffReqVO; 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.PerformanceSheetSignOffDO; +import com.njcn.rdms.module.project.dal.dataobject.performance.PerformanceSheetSignOffRecordDO; 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.signoff.EmployeeSignOffBatchDO; 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.PerformanceSheetSignOffMapper; +import com.njcn.rdms.module.project.dal.mysql.performance.PerformanceSheetSignOffRecordMapper; 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.signoff.EmployeeSignOffBatchMapper; 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.dal.mysql.workreport.monthly.MonthlyReportApprovalRecordMapper; 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.signoff.EmployeeSignOffStatusService; import com.njcn.rdms.module.project.service.status.StatusActionTextResolver; import com.njcn.rdms.module.project.service.team.TeamDashboardAccessService; +import com.njcn.rdms.module.project.service.workreport.common.WorkReportCommonService; 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.file.dto.FileContentCreateReqDTO; 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; @@ -71,13 +86,18 @@ import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; import lombok.extern.slf4j.Slf4j; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.YearMonth; @@ -95,6 +115,8 @@ import java.util.Objects; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception; import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; @@ -116,12 +138,22 @@ public class PerformanceSheetServiceImpl implements PerformanceSheetService { @Resource private PerformanceSheetResponseRecordMapper performanceSheetResponseRecordMapper; @Resource + private PerformanceSheetSignOffMapper performanceSheetSignOffMapper; + @Resource + private PerformanceSheetSignOffRecordMapper performanceSheetSignOffRecordMapper; + @Resource + private EmployeeSignOffBatchMapper employeeSignOffBatchMapper; + @Resource + private MonthlyReportApprovalRecordMapper monthlyReportApprovalRecordMapper; + @Resource private ObjectStatusModelMapper objectStatusModelMapper; @Resource private ObjectStatusTransitionMapper objectStatusTransitionMapper; @Resource private StatusActionTextResolver statusActionTextResolver; @Resource + private EmployeeSignOffStatusService employeeSignOffStatusService; + @Resource private AdminUserApi adminUserApi; @Resource private DeptApi deptApi; @@ -130,9 +162,13 @@ public class PerformanceSheetServiceImpl implements PerformanceSheetService { @Resource private FileApi fileApi; @Resource + private WorkReportCommonService workReportCommonService; + @Resource private TeamDashboardAccessService teamDashboardAccessService; @Resource private ApplicationEventPublisher applicationEventPublisher; + @Resource + private TransactionTemplate transactionTemplate; @Override public PageResult getSheetPage(PerformanceSheetPageReqVO reqVO) { @@ -183,6 +219,7 @@ public class PerformanceSheetServiceImpl implements PerformanceSheetService { sheet.setFileName(buildPerformanceFileName(periodMonth, employee.getNickname())); sheet.setFileVersion(0); sheet.setStatusCode(getInitialStatusCode()); + sheet.setSignOffStatus(SignOffConstants.STATUS_NOT_STARTED); performanceSheetMapper.insert(sheet); return sheet.getId(); } @@ -248,6 +285,158 @@ public class PerformanceSheetServiceImpl implements PerformanceSheetService { publishPerformanceNotice(List.of(sheet.getManagerId()), NotifyTemplateCodeConstants.PERFORMANCE_REJECTED, sheet); } + @Override + public PerformanceSheetImportZipRespVO importZip(PerformanceSheetImportZipReqVO reqVO) throws Exception { + MultipartFile file = reqVO.getFile(); + if (file == null || file.isEmpty()) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_IMPORT_ZIP_EMPTY); + } + String periodMonth = normalizePeriodMonth(reqVO.getPeriodMonth()); + CurrentOperatorProfile operatorProfile = loadCurrentOperatorProfile(); + validateBatchUnlocked(periodMonth, operatorProfile.deptId()); + List candidateEmployeeIds = resolveRequestedEmployeeIds(reqVO.getEmployeeIds(), + PerformanceConstants.PERMISSION_SHEET_IMPORT); + if (candidateEmployeeIds.isEmpty()) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_IMPORT_TARGET_EMPTY); + } + + Map> sheetsByEmployeeName = performanceSheetMapper + .selectListByEmployeeIdsAndPeriodMonth(candidateEmployeeIds, periodMonth) + .stream() + .collect(java.util.stream.Collectors.groupingBy(PerformanceSheetDO::getEmployeeName, + LinkedHashMap::new, java.util.stream.Collectors.toList())); + if (sheetsByEmployeeName.isEmpty()) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_IMPORT_TARGET_EMPTY); + } + + List messages = new ArrayList<>(); + int workbookCount = 0; + int sheetCount = 0; + int successCount = 0; + int skippedCount = 0; + int failedCount = 0; + List workbookEntries = readZipWorkbookEntries(file.getBytes()); + for (ImportedWorkbookEntry workbookEntry : workbookEntries) { + if (shouldIgnoreImportedWorkbook(workbookEntry.displayName())) { + skippedCount++; + messages.add("跳过工作簿:" + workbookEntry.displayName() + ",原因:兼容忽略 0_绩效表汇总"); + log.info("导入绩效压缩包时跳过工作簿,workbookName={}, reason={}", + workbookEntry.displayName(), "兼容忽略 0_绩效表汇总"); + continue; + } + workbookCount++; + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(workbookEntry.content()); + Workbook workbook = WorkbookFactory.create(inputStream)) { + FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator(); + DataFormatter formatter = new DataFormatter(); + for (int sheetIndex = 0; sheetIndex < workbook.getNumberOfSheets(); sheetIndex++) { + Sheet importedSheet = workbook.getSheetAt(sheetIndex); + sheetCount++; + try { + importOneSheet(periodMonth, workbookEntry.displayName(), importedSheet, evaluator, formatter, + sheetsByEmployeeName); + successCount++; + } catch (Exception ex) { + failedCount++; + String message = "导入失败:" + workbookEntry.displayName() + " / " + importedSheet.getSheetName() + + ",原因:" + ex.getMessage(); + messages.add(message); + log.warn("导入绩效 sheet 失败,workbookName={}, sheetName={}, reason={}", + workbookEntry.displayName(), importedSheet.getSheetName(), ex.getMessage(), ex); + } + } + } + } + PerformanceSheetImportZipRespVO respVO = new PerformanceSheetImportZipRespVO(); + respVO.setWorkbookCount(workbookCount); + respVO.setSheetCount(sheetCount); + respVO.setSuccessCount(successCount); + respVO.setSkippedCount(skippedCount); + respVO.setFailedCount(failedCount); + respVO.setMessages(messages); + return respVO; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long signOff(PerformanceSheetSignOffReqVO reqVO) { + String periodMonth = normalizePeriodMonth(reqVO.getPeriodMonth()); + CurrentOperatorProfile operatorProfile = loadCurrentOperatorProfile(); + validateBatchUnlocked(periodMonth, operatorProfile.deptId()); + + boolean explicitSelection = reqVO.getEmployeeIds() != null && !reqVO.getEmployeeIds().isEmpty(); + List requestedEmployeeIds = resolveRequestedEmployeeIds(reqVO.getEmployeeIds(), + PerformanceConstants.PERMISSION_SHEET_SIGN_OFF); + if (requestedEmployeeIds.isEmpty()) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_SIGN_OFF_CANDIDATE_EMPTY); + } + + List performanceSheets = performanceSheetMapper + .selectListByEmployeeIdsAndPeriodMonth(requestedEmployeeIds, periodMonth); + List eligiblePerformanceSheets = performanceSheets.stream() + .filter(this::isEligibleForIssue) + .toList(); + Map performanceByEmployeeId = eligiblePerformanceSheets.stream() + .collect(java.util.stream.Collectors.toMap(PerformanceSheetDO::getEmployeeId, item -> item, + (left, right) -> left, LinkedHashMap::new)); + + List monthlyReports = + workReportCommonService.listMonthlyReportsForSignOff(requestedEmployeeIds, periodMonth); + List eligibleMonthlyReports = + monthlyReports.stream() + .filter(this::isEligibleMonthlyReportForIssue) + .filter(report -> hasLatestApprovedRecord(report.getId())) + .toList(); + Map monthlyByEmployeeId = + eligibleMonthlyReports.stream() + .collect(java.util.stream.Collectors.toMap( + com.njcn.rdms.module.project.dal.dataobject.workreport.monthly.MonthlyReportDO::getReporterId, + item -> item, (left, right) -> left, LinkedHashMap::new)); + + List targetEmployeeIds = new ArrayList<>(); + if (explicitSelection) { + for (Long employeeId : requestedEmployeeIds) { + if (!performanceByEmployeeId.containsKey(employeeId) || !monthlyByEmployeeId.containsKey(employeeId)) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_SIGN_OFF_CANDIDATE_EMPTY); + } + targetEmployeeIds.add(employeeId); + } + } else { + for (Long employeeId : requestedEmployeeIds) { + if (performanceByEmployeeId.containsKey(employeeId) && monthlyByEmployeeId.containsKey(employeeId)) { + targetEmployeeIds.add(employeeId); + } + } + if (targetEmployeeIds.isEmpty()) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_SIGN_OFF_CANDIDATE_EMPTY); + } + } + + List targetSheets = targetEmployeeIds.stream() + .map(performanceByEmployeeId::get) + .filter(Objects::nonNull) + .toList(); + List targetReports = + targetEmployeeIds.stream() + .map(monthlyByEmployeeId::get) + .filter(Objects::nonNull) + .toList(); + + EmployeeSignOffBatchDO batch = new EmployeeSignOffBatchDO(); + batch.setPeriodMonth(periodMonth); + batch.setDeptId(operatorProfile.deptId()); + batch.setDeptName(operatorProfile.deptName()); + batch.setOperatorUserId(operatorProfile.userId()); + batch.setOperatorName(operatorProfile.userName()); + batch.setLockedFlag(1); + batch.setRemark(normalizeNullableText(reqVO.getRemark())); + employeeSignOffBatchMapper.insert(batch); + + issuePerformanceSignOff(targetSheets, batch.getId()); + workReportCommonService.issueMonthlySignOff(targetReports, batch.getId()); + return batch.getId(); + } + @Override public PerformanceDownloadFile download(Long id) { PerformanceSheetDO sheet = validateReadableSheet(id); @@ -393,23 +582,38 @@ public class PerformanceSheetServiceImpl implements PerformanceSheetService { private PerformanceSheetDO processEmployeeResponse(Long id, String actionType, String reason) { PerformanceSheetDO current = validateSheetExists(id); validateEmployeeOnly(current.getEmployeeId()); + PerformanceEmployeeResponseContext responseContext = resolvePerformanceEmployeeResponseContext(current); 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()); + if (responseContext.signOffFlow()) { + update.setSignOffStatus(SignOffConstants.STATUS_EMPLOYEE_CONFIRMED); + } } else if (PerformanceConstants.STATUS_REJECTED.equals(transition.getToStatusCode())) { update.setRejectedTime(LocalDateTime.now()); update.setLastStatusReason(reason); + if (responseContext.signOffFlow()) { + update.setSignOffStatus(SignOffConstants.STATUS_EMPLOYEE_REJECTED); + } } - int updateCount = performanceSheetMapper.updateStatusByIdAndStatus(update, id, current.getStatusCode()); + int updateCount = performanceSheetMapper.updateByIdAndStatusAndSignOffStatus(update, id, current.getStatusCode(), + current.getSignOffStatus()); if (updateCount != 1) { throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_STATUS_CONCURRENT_MODIFIED); } + if (responseContext.signOffFlow()) { + updatePerformanceSignOffAfterEmployeeAction(current, responseContext.signOff(), actionType, reason); + } PerformanceSheetDO after = merge(current, update); PerformanceSheetStatusLogDO statusLog = writeStatusLog(after, actionType, current.getStatusCode(), transition.getToStatusCode(), reason); writeResponseRecord(after, statusLog, actionType, current.getStatusCode(), transition.getToStatusCode(), reason); + if (PerformanceConstants.ACTION_CONFIRM.equals(actionType)) { + workReportCommonService.syncPerformanceResult(after.getEmployeeId(), after.getPeriodMonth(), + formatScoreForMonthlyResult(after.getActualScoreTotal())); + } return after; } @@ -527,6 +731,11 @@ public class PerformanceSheetServiceImpl implements PerformanceSheetService { private PerformanceSheetRespVO toRespVO(PerformanceSheetDO sheet) { PerformanceSheetRespVO respVO = BeanUtils.toBean(sheet, PerformanceSheetRespVO.class); respVO.setStatusName(statusName(sheet.getStatusCode())); + String signOffStatus = StringUtils.hasText(sheet.getSignOffStatus()) + ? sheet.getSignOffStatus() + : SignOffConstants.STATUS_NOT_STARTED; + respVO.setSignOffStatus(signOffStatus); + respVO.setSignOffStatusName(employeeSignOffStatusService.statusName(signOffStatus)); return respVO; } @@ -1095,17 +1304,22 @@ public class PerformanceSheetServiceImpl implements PerformanceSheetService { Sheet sourceSheet = workbook.getSheetAt(0); FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator(); DataFormatter formatter = new DataFormatter(); - Map cellValues = new LinkedHashMap<>(); - for (String cellAddress : cellAddresses) { - String value = readCellDisplayValue(sourceSheet, evaluator, formatter, cellAddress); - if (value != null) { - cellValues.put(cellAddress, value); - } - } - return cellValues; + return readSelectedCellValues(sourceSheet, evaluator, formatter, cellAddresses); } } + private Map readSelectedCellValues(Sheet sheet, FormulaEvaluator evaluator, + DataFormatter formatter, List cellAddresses) { + Map cellValues = new LinkedHashMap<>(); + for (String cellAddress : cellAddresses) { + String value = readCellDisplayValue(sheet, evaluator, formatter, cellAddress); + if (value != null) { + cellValues.put(cellAddress, value); + } + } + return cellValues; + } + private String readCellDisplayValue(Sheet sheet, FormulaEvaluator evaluator, DataFormatter formatter, @@ -1123,6 +1337,348 @@ public class PerformanceSheetServiceImpl implements PerformanceSheetService { return StringUtils.hasText(value) ? value.trim() : null; } + private void importOneSheet(String periodMonth, String workbookName, Sheet importedSheet, + FormulaEvaluator evaluator, DataFormatter formatter, + Map> sheetsByEmployeeName) { + transactionTemplate.executeWithoutResult(status -> { + String sheetName = importedSheet.getSheetName(); + String employeeName = parseEmployeeNameFromSheetName(periodMonth, sheetName); + List matchedSheets = sheetsByEmployeeName.getOrDefault(employeeName, List.of()); + if (matchedSheets.isEmpty()) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_IMPORT_TARGET_EMPTY); + } + if (matchedSheets.size() > 1) { + throw invalidParamException("存在重名员工,无法唯一匹配:" + employeeName); + } + PerformanceSheetDO currentSheet = matchedSheets.get(0); + Map importedValues = readSelectedCellValues(importedSheet, evaluator, formatter, + PerformanceExcelCellConstants.MERGED_EXPORT_CELL_ADDRESSES); + if (importedValues.isEmpty()) { + throw invalidParamException("未读取到可回写的绩效单元格"); + } + applyImportedSheetValues(currentSheet, workbookName, sheetName, importedValues); + }); + } + + private void applyImportedSheetValues(PerformanceSheetDO currentSheet, String workbookName, String sheetName, + Map importedValues) { + byte[] sourceContent = readFileContent(currentSheet); + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(sourceContent); + Workbook workbook = WorkbookFactory.create(inputStream); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + if (workbook.getNumberOfSheets() <= 0) { + throw invalidParamException("原始绩效文件缺少 sheet 页"); + } + Sheet targetSheet = workbook.getSheetAt(0); + writeMergedExportCellValues(targetSheet, importedValues); + workbook.setForceFormulaRecalculation(true); + FormulaEvaluator targetEvaluator = workbook.getCreationHelper().createFormulaEvaluator(); + targetEvaluator.evaluateAll(); + DataFormatter formatter = new DataFormatter(); + BigDecimal actualScoreTotal = parseScore(readCellDisplayValue(targetSheet, targetEvaluator, formatter, + PerformanceExcelCellConstants.ACTUAL_SCORE_TOTAL_CELL)); + BigDecimal baseScoreTotal = parseScore(readCellDisplayValue(targetSheet, targetEvaluator, formatter, + PerformanceExcelCellConstants.BASE_SCORE_TOTAL_CELL)); + BigDecimal extraScoreTotal = parseScore(readCellDisplayValue(targetSheet, targetEvaluator, formatter, + PerformanceExcelCellConstants.EXTRA_SCORE_TOTAL_CELL)); + + workbook.write(outputStream); + FileContentCreateReqDTO fileCreateReqDTO = new FileContentCreateReqDTO(); + fileCreateReqDTO.setContent(outputStream.toByteArray()); + fileCreateReqDTO.setName(defaultDownloadName(currentSheet)); + fileCreateReqDTO.setDirectory("rdms/performance"); + fileCreateReqDTO.setType(resolveExcelContentType(defaultDownloadName(currentSheet))); + CommonResult createFileResult = fileApi.createFileByContent(fileCreateReqDTO); + Long newFileId = createFileResult == null ? null : createFileResult.getCheckedData(); + if (newFileId == null) { + throw new IllegalStateException("上传更新后的绩效文件失败"); + } + + PerformanceSheetDO update = new PerformanceSheetDO(); + update.setFileId(newFileId); + update.setFileName(defaultDownloadName(currentSheet)); + update.setFileVersion(currentSheet.getFileVersion() == null ? 1 : currentSheet.getFileVersion() + 1); + update.setActualScoreTotal(actualScoreTotal); + update.setBaseScoreTotal(baseScoreTotal); + update.setExtraScoreTotal(extraScoreTotal); + int updateCount = performanceSheetMapper.updateExcelByIdAndVersion(update, currentSheet.getId(), + currentSheet.getFileVersion()); + if (updateCount != 1) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_STATUS_CONCURRENT_MODIFIED); + } + workReportCommonService.syncPerformanceResult(currentSheet.getEmployeeId(), currentSheet.getPeriodMonth(), + formatScoreForMonthlyResult(actualScoreTotal)); + log.info("导入绩效 sheet 成功,workbookName={}, sheetName={}, sheetId={}, employeeId={}", + workbookName, sheetName, currentSheet.getId(), currentSheet.getEmployeeId()); + } catch (RuntimeException ex) { + throw ex; + } catch (Exception ex) { + throw new IllegalStateException("回写绩效 Excel 失败", ex); + } + } + + private List readZipWorkbookEntries(byte[] zipBytes) throws IOException { + List charsets = List.of(StandardCharsets.UTF_8, Charset.forName("GBK")); + Exception lastException = null; + for (Charset charset : charsets) { + try { + return readZipWorkbookEntries(zipBytes, charset); + } catch (Exception ex) { + lastException = ex; + } + } + if (lastException instanceof IOException ioException) { + throw ioException; + } + throw new IOException("读取 zip 文件失败", lastException); + } + + private List readZipWorkbookEntries(byte[] zipBytes, Charset charset) throws IOException { + List workbookEntries = new ArrayList<>(); + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(zipBytes); + ZipInputStream zipInputStream = new ZipInputStream(inputStream, charset)) { + ZipEntry entry; + while ((entry = zipInputStream.getNextEntry()) != null) { + if (entry.isDirectory()) { + continue; + } + String displayName = extractEntryDisplayName(entry.getName()); + if (!isExcelEntry(displayName)) { + continue; + } + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + zipInputStream.transferTo(outputStream); + workbookEntries.add(new ImportedWorkbookEntry(displayName, outputStream.toByteArray())); + } + } + return workbookEntries; + } + + private String extractEntryDisplayName(String entryName) { + if (!StringUtils.hasText(entryName)) { + return ""; + } + String normalized = entryName.replace("\\", "/"); + int index = normalized.lastIndexOf('/'); + return index >= 0 ? normalized.substring(index + 1) : normalized; + } + + private boolean isExcelEntry(String fileName) { + if (!StringUtils.hasText(fileName)) { + return false; + } + String lowerCase = fileName.toLowerCase(); + return lowerCase.endsWith(".xlsx") || lowerCase.endsWith(".xlsm") || lowerCase.endsWith(".xls"); + } + + private boolean shouldIgnoreImportedWorkbook(String workbookName) { + return StringUtils.hasText(workbookName) && workbookName.startsWith("0_绩效表汇总"); + } + + private String parseEmployeeNameFromSheetName(String periodMonth, String sheetName) { + if (!StringUtils.hasText(sheetName)) { + throw invalidParamException("sheet 名不能为空"); + } + String normalizedName = sheetName.trim(); + int splitIndex = normalizedName.indexOf('_'); + if (splitIndex <= 0 || splitIndex >= normalizedName.length() - 1) { + throw invalidParamException("sheet 名不符合“月份_姓名”规则:" + normalizedName); + } + String monthPart = normalizedName.substring(0, splitIndex).trim(); + if (!periodMonth.equals(monthPart)) { + throw invalidParamException("sheet 月份与导入月份不一致:" + normalizedName); + } + String employeeName = normalizedName.substring(splitIndex + 1).trim(); + if (!StringUtils.hasText(employeeName)) { + throw invalidParamException("sheet 名未包含员工姓名:" + normalizedName); + } + return employeeName; + } + + private PerformanceEmployeeResponseContext resolvePerformanceEmployeeResponseContext(PerformanceSheetDO sheet) { + if (!PerformanceConstants.STATUS_SENT.equals(sheet.getStatusCode())) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_SIGN_OFF_NOT_ALLOWED); + } + if (SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM.equals(sheet.getSignOffStatus())) { + PerformanceSheetSignOffDO signOff = validatePerformanceSignOffExists(sheet.getId()); + if (!SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM.equals(signOff.getStatusCode())) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_SIGN_OFF_NOT_ALLOWED); + } + return new PerformanceEmployeeResponseContext(signOff, true); + } + if (SignOffConstants.STATUS_EMPLOYEE_REJECTED.equals(sheet.getSignOffStatus())) { + PerformanceSheetSignOffDO signOff = validatePerformanceSignOffExists(sheet.getId()); + if (!SignOffConstants.STATUS_EMPLOYEE_REJECTED.equals(signOff.getStatusCode())) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_SIGN_OFF_NOT_ALLOWED); + } + return new PerformanceEmployeeResponseContext(signOff, false); + } + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_SIGN_OFF_NOT_ALLOWED); + } + + private PerformanceSheetSignOffDO validatePerformanceSignOffExists(Long sheetId) { + PerformanceSheetSignOffDO signOff = performanceSheetSignOffMapper.selectByPerformanceSheetId(sheetId); + if (signOff == null) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_SIGN_OFF_NOT_EXISTS); + } + return signOff; + } + + private void updatePerformanceSignOffAfterEmployeeAction(PerformanceSheetDO current, PerformanceSheetSignOffDO signOff, + String actionType, String reason) { + PerformanceSheetSignOffDO signOffUpdate = new PerformanceSheetSignOffDO(); + signOffUpdate.setStatusCode(PerformanceConstants.ACTION_CONFIRM.equals(actionType) + ? SignOffConstants.STATUS_EMPLOYEE_CONFIRMED + : SignOffConstants.STATUS_EMPLOYEE_REJECTED); + signOffUpdate.setConfirmUserId(SecurityFrameworkUtils.getLoginUserId()); + signOffUpdate.setConfirmUserName(defaultText(SecurityFrameworkUtils.getLoginUserNickname())); + signOffUpdate.setConfirmTime(LocalDateTime.now()); + signOffUpdate.setLastRejectReason(PerformanceConstants.ACTION_REJECT.equals(actionType) ? reason : null); + int signOffUpdateCount = performanceSheetSignOffMapper.updateByPerformanceSheetIdAndStatus(signOffUpdate, + current.getId(), signOff.getStatusCode()); + if (signOffUpdateCount != 1) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_STATUS_CONCURRENT_MODIFIED); + } + writePerformanceSignOffRecord(signOff.getId(), current.getId(), signOff.getBatchNo(), actionType, + signOff.getStatusCode(), signOffUpdate.getStatusCode(), reason); + } + + private void issuePerformanceSignOff(List sheets, Long batchNo) { + if (sheets == null || sheets.isEmpty()) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_SIGN_OFF_CANDIDATE_EMPTY); + } + LocalDateTime issueTime = LocalDateTime.now(); + Long operatorUserId = SecurityFrameworkUtils.getLoginUserId(); + String operatorName = defaultText(SecurityFrameworkUtils.getLoginUserNickname()); + for (PerformanceSheetDO sheet : sheets) { + PerformanceSheetDO update = new PerformanceSheetDO(); + update.setSignOffStatus(SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM); + int updateCount = performanceSheetMapper.updateByIdAndStatusAndSignOffStatus(update, sheet.getId(), + sheet.getStatusCode(), sheet.getSignOffStatus()); + if (updateCount != 1) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_STATUS_CONCURRENT_MODIFIED); + } + PerformanceSheetSignOffDO currentSignOff = performanceSheetSignOffMapper.selectByPerformanceSheetId(sheet.getId()); + if (currentSignOff == null) { + PerformanceSheetSignOffDO signOff = new PerformanceSheetSignOffDO(); + signOff.setPerformanceSheetId(sheet.getId()); + signOff.setBatchNo(batchNo); + signOff.setStatusCode(SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM); + signOff.setIssueUserId(operatorUserId); + signOff.setIssueUserName(operatorName); + signOff.setIssueTime(issueTime); + performanceSheetSignOffMapper.insert(signOff); + writePerformanceSignOffRecord(signOff.getId(), sheet.getId(), batchNo, SignOffConstants.ACTION_ISSUE, + SignOffConstants.STATUS_NOT_STARTED, SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM, null); + } else { + employeeSignOffStatusService.validateTransition(currentSignOff.getStatusCode(), SignOffConstants.ACTION_ISSUE, null); + PerformanceSheetSignOffDO signOffUpdate = new PerformanceSheetSignOffDO(); + signOffUpdate.setStatusCode(SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM); + signOffUpdate.setBatchNo(batchNo); + signOffUpdate.setIssueUserId(operatorUserId); + signOffUpdate.setIssueUserName(operatorName); + signOffUpdate.setIssueTime(issueTime); + signOffUpdate.setConfirmUserId(null); + signOffUpdate.setConfirmUserName(null); + signOffUpdate.setConfirmTime(null); + signOffUpdate.setLastRejectReason(null); + int signOffUpdateCount = performanceSheetSignOffMapper.updateByPerformanceSheetIdAndStatus(signOffUpdate, + sheet.getId(), currentSignOff.getStatusCode()); + if (signOffUpdateCount != 1) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_STATUS_CONCURRENT_MODIFIED); + } + writePerformanceSignOffRecord(currentSignOff.getId(), sheet.getId(), batchNo, + SignOffConstants.ACTION_ISSUE, currentSignOff.getStatusCode(), + SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM, null); + } + } + } + + private void writePerformanceSignOffRecord(Long signOffId, Long sheetId, Long batchNo, String actionType, + String fromStatus, String toStatus, String reason) { + PerformanceSheetSignOffRecordDO record = new PerformanceSheetSignOffRecordDO(); + record.setSignOffId(signOffId); + record.setPerformanceSheetId(sheetId); + record.setBatchNo(batchNo); + record.setActionType(actionType); + record.setFromSignOffStatus(fromStatus); + record.setToSignOffStatus(toStatus); + record.setReason(reason); + record.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId()); + record.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname())); + performanceSheetSignOffRecordMapper.insert(record); + } + + private void validateBatchUnlocked(String periodMonth, Long deptId) { + if (employeeSignOffBatchMapper.selectByPeriodMonthAndDeptId(periodMonth, deptId) != null) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_SIGN_OFF_BATCH_LOCKED); + } + } + + private List resolveRequestedEmployeeIds(Collection employeeIds, String permission) { + return teamDashboardAccessService.resolveRequestedSubordinateUserIds(employeeIds, permission); + } + + private boolean isEligibleForIssue(PerformanceSheetDO sheet) { + return sheet != null + && PerformanceConstants.STATUS_SENT.equals(sheet.getStatusCode()) + && isIssueAllowed(sheet.getSignOffStatus()) + && sheet.getFileId() != null; + } + + private boolean isEligibleMonthlyReportForIssue( + com.njcn.rdms.module.project.dal.dataobject.workreport.monthly.MonthlyReportDO report) { + return report != null + && WorkReportConstants.STATUS_APPROVED.equals(report.getStatusCode()) + && isIssueAllowed(report.getSignOffStatus()); + } + + private boolean hasLatestApprovedRecord(Long monthlyReportId) { + return monthlyReportApprovalRecordMapper.selectLatestApprovedByMonthlyReportId(monthlyReportId) != null; + } + + private boolean isIssueAllowed(String signOffStatus) { + return !StringUtils.hasText(signOffStatus) + || SignOffConstants.STATUS_NOT_STARTED.equals(signOffStatus); + } + + private BigDecimal parseScore(String value) { + if (!StringUtils.hasText(value)) { + return null; + } + try { + return normalizeScore(new BigDecimal(value.trim().replace(",", ""))); + } catch (NumberFormatException ex) { + return null; + } + } + + private String formatScoreForMonthlyResult(BigDecimal score) { + return score == null ? null : score.stripTrailingZeros().toPlainString(); + } + + private String resolveExcelContentType(String fileName) { + return StringUtils.hasText(fileName) && fileName.toLowerCase().endsWith(".xls") + ? "application/vnd.ms-excel" + : EXCEL_CONTENT_TYPE; + } + + private CurrentOperatorProfile loadCurrentOperatorProfile() { + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + CommonResult userResult = adminUserApi.getUser(loginUserId); + AdminUserRespDTO user = userResult == null ? null : userResult.getCheckedData(); + if (user == null || user.getDeptId() == null) { + throw invalidParamException("当前登录用户部门信息不完整"); + } + CommonResult deptResult = deptApi.getDept(user.getDeptId()); + DeptRespDTO dept = deptResult == null ? null : deptResult.getCheckedData(); + if (dept == null || !StringUtils.hasText(dept.getName())) { + throw invalidParamException("当前登录用户部门信息不完整"); + } + return new CurrentOperatorProfile(loginUserId, defaultText(SecurityFrameworkUtils.getLoginUserNickname()), + dept.getId(), dept.getName().trim()); + } + private PerformanceSheetDO merge(PerformanceSheetDO source, PerformanceSheetDO update) { PerformanceSheetDO merged = BeanUtils.toBean(source, PerformanceSheetDO.class); if (update.getStatusCode() != null) { @@ -1140,6 +1696,9 @@ public class PerformanceSheetServiceImpl implements PerformanceSheetService { if (update.getLastStatusReason() != null) { merged.setLastStatusReason(update.getLastStatusReason()); } + if (update.getSignOffStatus() != null) { + merged.setSignOffStatus(update.getSignOffStatus()); + } return merged; } @@ -1258,4 +1817,13 @@ public class PerformanceSheetServiceImpl implements PerformanceSheetService { private final Map fontCache = new IdentityHashMap<>(); private final Map dataFormatCache = new HashMap<>(); } + + private record ImportedWorkbookEntry(String displayName, byte[] content) { + } + + private record PerformanceEmployeeResponseContext(PerformanceSheetSignOffDO signOff, boolean signOffFlow) { + } + + private record CurrentOperatorProfile(Long userId, String userName, Long deptId, String deptName) { + } } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/signoff/EmployeeSignOffStatusService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/signoff/EmployeeSignOffStatusService.java new file mode 100644 index 0000000..c74dece --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/signoff/EmployeeSignOffStatusService.java @@ -0,0 +1,15 @@ +package com.njcn.rdms.module.project.service.signoff; + +import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO; +import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO; + +public interface EmployeeSignOffStatusService { + + String getInitialStatusCode(); + + String statusName(String statusCode); + + ObjectStatusTransitionDO validateTransition(String fromStatus, String actionCode, String reason); + + ObjectStatusModelDO validateStatusModel(String statusCode); +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/signoff/EmployeeSignOffStatusServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/signoff/EmployeeSignOffStatusServiceImpl.java new file mode 100644 index 0000000..1384a2d --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/signoff/EmployeeSignOffStatusServiceImpl.java @@ -0,0 +1,66 @@ +package com.njcn.rdms.module.project.service.signoff; + +import com.njcn.rdms.module.project.constant.SignOffConstants; +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.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.service.status.StatusActionTextResolver; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception; + +@Service +public class EmployeeSignOffStatusServiceImpl implements EmployeeSignOffStatusService { + + @Resource + private ObjectStatusModelMapper objectStatusModelMapper; + @Resource + private ObjectStatusTransitionMapper objectStatusTransitionMapper; + @Resource + private StatusActionTextResolver statusActionTextResolver; + + @Override + public String getInitialStatusCode() { + ObjectStatusModelDO model = objectStatusModelMapper.selectInitialByObjectTypeEnabled(SignOffConstants.STATUS_OBJECT_TYPE); + if (model == null || !StringUtils.hasText(model.getStatusCode())) { + throw exception(ErrorCodeConstants.SIGN_OFF_STATUS_MODEL_NOT_EXISTS_OR_DISABLED); + } + return model.getStatusCode(); + } + + @Override + public String statusName(String statusCode) { + return statusActionTextResolver.statusName(SignOffConstants.STATUS_OBJECT_TYPE, statusCode); + } + + @Override + public ObjectStatusTransitionDO validateTransition(String fromStatus, String actionCode, String reason) { + ObjectStatusTransitionDO transition = objectStatusTransitionMapper.selectByObjectTypeAndFromStatusAndAction( + SignOffConstants.STATUS_OBJECT_TYPE, fromStatus, actionCode); + if (transition == null) { + throw exception(ErrorCodeConstants.SIGN_OFF_STATUS_ACTION_NOT_ALLOWED, + statusActionTextResolver.statusName(SignOffConstants.STATUS_OBJECT_TYPE, fromStatus), + statusActionTextResolver.actionName(SignOffConstants.STATUS_OBJECT_TYPE, actionCode)); + } + if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) { + throw exception(ErrorCodeConstants.SIGN_OFF_STATUS_ACTION_REASON_REQUIRED, + statusActionTextResolver.actionName(SignOffConstants.STATUS_OBJECT_TYPE, actionCode)); + } + validateStatusModel(transition.getToStatusCode()); + return transition; + } + + @Override + public ObjectStatusModelDO validateStatusModel(String statusCode) { + ObjectStatusModelDO model = objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled( + SignOffConstants.STATUS_OBJECT_TYPE, statusCode); + if (model == null) { + throw exception(ErrorCodeConstants.SIGN_OFF_STATUS_MODEL_NOT_EXISTS_OR_DISABLED); + } + return model; + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/common/WorkReportCommonService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/common/WorkReportCommonService.java index dc8a10d..8ddc5a9 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/common/WorkReportCommonService.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/common/WorkReportCommonService.java @@ -6,6 +6,7 @@ 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.ProjectObjectConstants; +import com.njcn.rdms.module.project.constant.SignOffConstants; import com.njcn.rdms.module.project.constant.WorkReportConstants; import com.njcn.rdms.module.project.framework.notify.NotifySendEvent; import com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants; @@ -24,6 +25,8 @@ import com.njcn.rdms.module.project.dal.dataobject.workreport.common.WorkReportM import com.njcn.rdms.module.project.dal.dataobject.workreport.common.WorkReportStatusLogDO; import com.njcn.rdms.module.project.dal.dataobject.workreport.monthly.MonthlyReportApprovalRecordDO; import com.njcn.rdms.module.project.dal.dataobject.workreport.monthly.MonthlyReportDO; +import com.njcn.rdms.module.project.dal.dataobject.workreport.monthly.MonthlyReportSignOffDO; +import com.njcn.rdms.module.project.dal.dataobject.workreport.monthly.MonthlyReportSignOffRecordDO; import com.njcn.rdms.module.project.dal.dataobject.workreport.project.ProjectReportApprovalRecordDO; import com.njcn.rdms.module.project.dal.dataobject.workreport.project.ProjectReportCurrentItemDO; import com.njcn.rdms.module.project.dal.dataobject.workreport.project.ProjectReportDO; @@ -41,6 +44,8 @@ import com.njcn.rdms.module.project.dal.mysql.workreport.common.PersonalReportRe import com.njcn.rdms.module.project.dal.mysql.workreport.common.WorkReportStatusLogMapper; import com.njcn.rdms.module.project.dal.mysql.workreport.monthly.MonthlyReportApprovalRecordMapper; import com.njcn.rdms.module.project.dal.mysql.workreport.monthly.MonthlyReportMapper; +import com.njcn.rdms.module.project.dal.mysql.workreport.monthly.MonthlyReportSignOffMapper; +import com.njcn.rdms.module.project.dal.mysql.workreport.monthly.MonthlyReportSignOffRecordMapper; import com.njcn.rdms.module.project.dal.mysql.workreport.project.ProjectReportApprovalRecordMapper; import com.njcn.rdms.module.project.dal.mysql.workreport.project.ProjectReportCurrentItemMapper; import com.njcn.rdms.module.project.dal.mysql.workreport.project.ProjectReportMapper; @@ -49,10 +54,12 @@ import com.njcn.rdms.module.project.dal.mysql.workreport.weekly.WeeklyReportAppr import com.njcn.rdms.module.project.dal.mysql.workreport.weekly.WeeklyReportMapper; import com.njcn.rdms.module.project.dal.mysql.workreport.weekly.WeeklyReportTravelSegmentMapper; import com.njcn.rdms.module.project.enums.ErrorCodeConstants; +import com.njcn.rdms.module.project.service.signoff.EmployeeSignOffStatusService; 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.PostApi; +import com.njcn.rdms.module.system.api.user.UserSignatureApi; import com.njcn.rdms.module.system.api.dept.dto.DeptRespDTO; import com.njcn.rdms.module.system.api.dept.dto.PostRespDTO; import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi; @@ -60,6 +67,7 @@ import com.njcn.rdms.module.system.api.permission.dto.ObjectRoleRespDTO; 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.api.user.dto.UserSignatureRespDTO; import com.njcn.rdms.module.system.enums.notify.NotifyMessageLevelConstants; import jakarta.annotation.Resource; import org.springframework.context.ApplicationEventPublisher; @@ -109,6 +117,10 @@ public class WorkReportCommonService { @Resource private MonthlyReportApprovalRecordMapper monthlyReportApprovalRecordMapper; @Resource + private MonthlyReportSignOffMapper monthlyReportSignOffMapper; + @Resource + private MonthlyReportSignOffRecordMapper monthlyReportSignOffRecordMapper; + @Resource private ProjectReportApprovalRecordMapper projectReportApprovalRecordMapper; @Resource private WorkReportStatusLogMapper workReportStatusLogMapper; @@ -121,6 +133,8 @@ public class WorkReportCommonService { @Resource private StatusActionTextResolver statusActionTextResolver; @Resource + private EmployeeSignOffStatusService employeeSignOffStatusService; + @Resource private AdminUserApi adminUserApi; @Resource private DeptApi deptApi; @@ -129,6 +143,8 @@ public class WorkReportCommonService { @Resource private UserManagementRelationApi userManagementRelationApi; @Resource + private UserSignatureApi userSignatureApi; + @Resource private ObjectPermissionApi objectPermissionApi; @Resource private ProjectMapper projectMapper; @@ -368,6 +384,7 @@ public class WorkReportCommonService { MonthlyReportDO report = new MonthlyReportDO(); applyMonthlySaveFields(report, reqVO, profile); report.setStatusCode(getInitialStatusCode()); + report.setSignOffStatus(SignOffConstants.STATUS_NOT_STARTED); monthlyReportMapper.insert(report); replaceMonthlyChildren(report.getId(), reqVO); MonthlyReportDO latest = monthlyReportMapper.selectById(report.getId()); @@ -439,6 +456,29 @@ public class WorkReportCommonService { .collect(Collectors.toList()), pageResult.getTotal()); } + public PageResult getMonthlySignOffPage(MonthlyReportPageReqVO reqVO) { + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + reqVO.setStatusCode(WorkReportConstants.STATUS_APPROVED); + reqVO.setSignOffStatus(SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM); + PageResult pageResult = monthlyReportMapper.selectReporterPage(loginUserId, reqVO, + List.of(WorkReportConstants.STATUS_APPROVED)); + return new PageResult<>(pageResult.getList().stream() + .map(report -> toMonthlyRespVO(report, false)) + .collect(Collectors.toList()), pageResult.getTotal()); + } + + public MonthlyReportSignOffDetailRespVO getMonthlySignOffDetail(Long id) { + MonthlyReportDO report = validateMonthlyReportExists(id); + validateReporter(SecurityFrameworkUtils.getLoginUserId(), report.getReporterId()); + MonthlyReportSignOffDetailRespVO respVO = new MonthlyReportSignOffDetailRespVO(); + respVO.setReport(toMonthlyRespVO(report, true)); + MonthlyReportApprovalRecordDO latestApprovalRecord = monthlyReportApprovalRecordMapper + .selectLatestApprovedByMonthlyReportId(id); + respVO.setLatestApprovalRecord(latestApprovalRecord == null ? null + : BeanUtils.toBean(latestApprovalRecord, MonthlyReportApprovalRecordRespVO.class)); + return respVO; + } + @Transactional(rollbackFor = Exception.class) public void submitMonthlyReport(Long id) { MonthlyReportDO current = validateMonthlyReportExists(id); @@ -478,6 +518,55 @@ public class WorkReportCommonService { processMonthlyApproval(id, WorkReportConstants.ACTION_REJECT, rejectReqVO); } + @Transactional(rollbackFor = Exception.class) + public void confirmMonthlySignOff(Long id, MonthlyReportSignOffConfirmReqVO reqVO) { + MonthlyReportDO current = validateMonthlyReportExists(id); + Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); + validateReporter(loginUserId, current.getReporterId()); + if (!WorkReportConstants.STATUS_APPROVED.equals(current.getStatusCode()) + || !SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM.equals(current.getSignOffStatus())) { + throw exception(ErrorCodeConstants.WORK_REPORT_SIGN_OFF_NOT_ALLOWED); + } + MonthlyReportSignOffDO signOff = validateMonthlySignOffExists(current.getId()); + employeeSignOffStatusService.validateTransition(signOff.getStatusCode(), SignOffConstants.ACTION_CONFIRM, null); + MonthlyReportApprovalRecordDO approvalRecord = monthlyReportApprovalRecordMapper + .selectLatestApprovedByMonthlyReportId(current.getId()); + if (approvalRecord == null) { + throw exception(ErrorCodeConstants.WORK_REPORT_MONTHLY_SIGN_OFF_APPROVAL_RECORD_NOT_EXISTS); + } + + UserSignatureRespDTO userSignature = getUserSignature(loginUserId); + LocalDate signedDate = reqVO.getEmployeeSignedDate() != null ? reqVO.getEmployeeSignedDate() : LocalDate.now(); + MonthlyReportApprovalRecordDO approvalUpdate = new MonthlyReportApprovalRecordDO(); + approvalUpdate.setId(approvalRecord.getId()); + approvalUpdate.setMeetingDate(reqVO.getMeetingDate()); + approvalUpdate.setEmployeeSignName(defaultText(SecurityFrameworkUtils.getLoginUserNickname())); + approvalUpdate.setEmployeeSignatureFileId(userSignature == null ? null : userSignature.getFileId()); + approvalUpdate.setEmployeeSignedDate(signedDate); + monthlyReportApprovalRecordMapper.updateById(approvalUpdate); + + MonthlyReportDO reportUpdate = new MonthlyReportDO(); + reportUpdate.setSignOffStatus(SignOffConstants.STATUS_EMPLOYEE_CONFIRMED); + int reportUpdateCount = monthlyReportMapper.updateByIdAndStatusAndSignOffStatus(reportUpdate, current.getId(), + current.getStatusCode(), current.getSignOffStatus()); + if (reportUpdateCount != 1) { + throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED); + } + + MonthlyReportSignOffDO signOffUpdate = new MonthlyReportSignOffDO(); + signOffUpdate.setStatusCode(SignOffConstants.STATUS_EMPLOYEE_CONFIRMED); + signOffUpdate.setConfirmUserId(loginUserId); + signOffUpdate.setConfirmUserName(defaultText(SecurityFrameworkUtils.getLoginUserNickname())); + signOffUpdate.setConfirmTime(LocalDateTime.now()); + int signOffUpdateCount = monthlyReportSignOffMapper.updateByMonthlyReportIdAndStatus(signOffUpdate, + current.getId(), signOff.getStatusCode()); + if (signOffUpdateCount != 1) { + throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED); + } + writeMonthlySignOffRecord(signOff.getId(), current.getId(), approvalRecord.getId(), signOff.getBatchNo(), + SignOffConstants.ACTION_CONFIRM, signOff.getStatusCode(), SignOffConstants.STATUS_EMPLOYEE_CONFIRMED); + } + @Transactional(rollbackFor = Exception.class) public void deleteMonthlyReport(Long id) { MonthlyReportDO current = validateMonthlyReportExists(id); @@ -513,6 +602,89 @@ public class WorkReportCommonService { return BeanUtils.toBean(getMonthlyReportPage(reqVO).getList(), MonthlyReportExportVO.class); } + public List listMonthlyReportsForSignOff(Collection reporterIds, String periodMonth) { + return monthlyReportMapper.selectListByReporterIdsAndPeriodKey(reporterIds, periodMonth, + List.of(WorkReportConstants.STATUS_APPROVED)); + } + + public void issueMonthlySignOff(List reports, Long batchNo) { + if (reports == null || reports.isEmpty()) { + throw exception(ErrorCodeConstants.WORK_REPORT_MONTHLY_SIGN_OFF_CANDIDATE_EMPTY); + } + Long operatorUserId = SecurityFrameworkUtils.getLoginUserId(); + String operatorName = defaultText(SecurityFrameworkUtils.getLoginUserNickname()); + LocalDateTime issueTime = LocalDateTime.now(); + for (MonthlyReportDO report : reports) { + if (!WorkReportConstants.STATUS_APPROVED.equals(report.getStatusCode()) + || !isIssueAllowed(report.getSignOffStatus())) { + throw exception(ErrorCodeConstants.WORK_REPORT_SIGN_OFF_NOT_ALLOWED); + } + MonthlyReportApprovalRecordDO approvalRecord = monthlyReportApprovalRecordMapper + .selectLatestApprovedByMonthlyReportId(report.getId()); + if (approvalRecord == null) { + throw exception(ErrorCodeConstants.WORK_REPORT_MONTHLY_SIGN_OFF_APPROVAL_RECORD_NOT_EXISTS); + } + + MonthlyReportDO reportUpdate = new MonthlyReportDO(); + reportUpdate.setSignOffStatus(SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM); + int reportUpdateCount = monthlyReportMapper.updateByIdAndStatusAndSignOffStatus(reportUpdate, report.getId(), + report.getStatusCode(), report.getSignOffStatus()); + if (reportUpdateCount != 1) { + throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED); + } + + MonthlyReportSignOffDO currentSignOff = monthlyReportSignOffMapper.selectByMonthlyReportId(report.getId()); + if (currentSignOff == null) { + MonthlyReportSignOffDO signOff = new MonthlyReportSignOffDO(); + signOff.setMonthlyReportId(report.getId()); + signOff.setBatchNo(batchNo); + signOff.setStatusCode(SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM); + signOff.setIssueUserId(operatorUserId); + signOff.setIssueUserName(operatorName); + signOff.setIssueTime(issueTime); + monthlyReportSignOffMapper.insert(signOff); + writeMonthlySignOffRecord(signOff.getId(), report.getId(), approvalRecord.getId(), batchNo, + SignOffConstants.ACTION_ISSUE, SignOffConstants.STATUS_NOT_STARTED, + SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM); + } else { + employeeSignOffStatusService.validateTransition(currentSignOff.getStatusCode(), SignOffConstants.ACTION_ISSUE, null); + MonthlyReportSignOffDO signOffUpdate = new MonthlyReportSignOffDO(); + signOffUpdate.setStatusCode(SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM); + signOffUpdate.setBatchNo(batchNo); + signOffUpdate.setIssueUserId(operatorUserId); + signOffUpdate.setIssueUserName(operatorName); + signOffUpdate.setIssueTime(issueTime); + signOffUpdate.setConfirmUserId(null); + signOffUpdate.setConfirmUserName(null); + signOffUpdate.setConfirmTime(null); + int signOffUpdateCount = monthlyReportSignOffMapper.updateByMonthlyReportIdAndStatus(signOffUpdate, + report.getId(), currentSignOff.getStatusCode()); + if (signOffUpdateCount != 1) { + throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED); + } + writeMonthlySignOffRecord(currentSignOff.getId(), report.getId(), approvalRecord.getId(), batchNo, + SignOffConstants.ACTION_ISSUE, currentSignOff.getStatusCode(), + SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM); + } + } + } + + public void syncPerformanceResult(Long reporterId, String periodMonth, String performanceResult) { + MonthlyReportDO report = monthlyReportMapper.selectByReporterIdAndPeriodKey(reporterId, periodMonth); + if (report == null) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_MONTHLY_REPORT_NOT_EXISTS); + } + MonthlyReportApprovalRecordDO approvalRecord = monthlyReportApprovalRecordMapper + .selectLatestApprovedByMonthlyReportId(report.getId()); + if (approvalRecord == null) { + throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_MONTHLY_APPROVAL_RECORD_NOT_EXISTS); + } + MonthlyReportApprovalRecordDO update = new MonthlyReportApprovalRecordDO(); + update.setId(approvalRecord.getId()); + update.setPerformanceResult(performanceResult); + monthlyReportApprovalRecordMapper.updateById(update); + } + public ProjectReportRespVO initProjectReport(Long projectId) { CurrentUserProfile profile = loadCurrentUserProfile(true); ProjectDO project = validateProjectExists(projectId); @@ -810,6 +982,14 @@ public class WorkReportCommonService { return report; } + private MonthlyReportSignOffDO validateMonthlySignOffExists(Long monthlyReportId) { + MonthlyReportSignOffDO signOff = monthlyReportSignOffMapper.selectByMonthlyReportId(monthlyReportId); + if (signOff == null) { + throw exception(ErrorCodeConstants.WORK_REPORT_MONTHLY_SIGN_OFF_NOT_EXISTS); + } + return signOff; + } + private ProjectReportDO validateReadableProjectReport(Long id) { ProjectReportDO report = validateProjectReportExists(id); validateReadable(report.getProjectOwnerId(), report.getSupervisorUserId()); @@ -1438,6 +1618,7 @@ public class WorkReportCommonService { record.setConclusion(statusLog.getToStatus()); record.setOpinion(normalizeReason(reqVO)); if (WorkReportConstants.ACTION_APPROVE.equals(actionCode)) { + UserSignatureRespDTO userSignature = getUserSignature(SecurityFrameworkUtils.getLoginUserId()); record.setMeetingDate(reqVO.getMeetingDate()); record.setStrengthDesc(normalizeNullableText(reqVO.getStrengthDesc())); record.setStrengthExample(normalizeNullableText(reqVO.getStrengthExample())); @@ -1446,15 +1627,32 @@ public class WorkReportCommonService { record.setImprovementSuggestion(normalizeNullableText(reqVO.getImprovementSuggestion())); record.setPerformanceResult(normalizeNullableText(reqVO.getPerformanceResult())); record.setEmployeeSignName(normalizeNullableText(reqVO.getEmployeeSignName())); + record.setEmployeeSignatureFileId(null); record.setEmployeeSignedDate(reqVO.getEmployeeSignedDate()); - record.setSupervisorSignName(normalizeNullableText(reqVO.getSupervisorSignName())); - record.setSupervisorSignedDate(reqVO.getSupervisorSignedDate()); + record.setSupervisorSignName(defaultText(SecurityFrameworkUtils.getLoginUserNickname())); + record.setSupervisorSignatureFileId(userSignature == null ? null : userSignature.getFileId()); + record.setSupervisorSignedDate(LocalDate.now()); } record.setAuditorUserId(SecurityFrameworkUtils.getLoginUserId()); record.setAuditorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname())); monthlyReportApprovalRecordMapper.insert(record); } + private void writeMonthlySignOffRecord(Long signOffId, Long monthlyReportId, Long approvalRecordId, Long batchNo, + String actionType, String fromStatus, String toStatus) { + MonthlyReportSignOffRecordDO record = new MonthlyReportSignOffRecordDO(); + record.setSignOffId(signOffId); + record.setMonthlyReportId(monthlyReportId); + record.setApprovalRecordId(approvalRecordId); + record.setBatchNo(batchNo); + record.setActionType(actionType); + record.setFromSignOffStatus(fromStatus); + record.setToSignOffStatus(toStatus); + record.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId()); + record.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname())); + monthlyReportSignOffRecordMapper.insert(record); + } + private void writeProjectApprovalRecord(ProjectReportDO report, WorkReportStatusLogDO statusLog, String reason) { ProjectReportApprovalRecordDO record = new ProjectReportApprovalRecordDO(); record.setProjectReportId(report.getId()); @@ -1513,6 +1711,11 @@ public class WorkReportCommonService { private MonthlyReportRespVO toMonthlyRespVO(MonthlyReportDO report, boolean withChildren) { MonthlyReportRespVO respVO = BeanUtils.toBean(report, MonthlyReportRespVO.class); applyStatusView(respVO, getStatusModel(report.getStatusCode())); + String signOffStatus = StringUtils.hasText(report.getSignOffStatus()) + ? report.getSignOffStatus() + : SignOffConstants.STATUS_NOT_STARTED; + respVO.setSignOffStatus(signOffStatus); + respVO.setSignOffStatusName(employeeSignOffStatusService.statusName(signOffStatus)); if (withChildren) { respVO.setReviewItems(BeanUtils.toBean( personalReportReviewItemMapper.selectListByReport(WorkReportConstants.REPORT_TYPE_MONTHLY, report.getId()), @@ -1704,6 +1907,19 @@ public class WorkReportCommonService { return StringUtils.hasText(value) ? value.trim() : ""; } + private boolean isIssueAllowed(String signOffStatus) { + return !StringUtils.hasText(signOffStatus) + || SignOffConstants.STATUS_NOT_STARTED.equals(signOffStatus); + } + + private UserSignatureRespDTO getUserSignature(Long userId) { + if (userId == null) { + return null; + } + CommonResult result = userSignatureApi.getByUserId(userId); + return result == null ? null : result.getCheckedData(); + } + private List defaultList(List list) { return list == null ? Collections.emptyList() : list; } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/monthly/MonthlyReportService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/monthly/MonthlyReportService.java index cde4fe3..3c5e110 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/monthly/MonthlyReportService.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/monthly/MonthlyReportService.java @@ -9,6 +9,8 @@ import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.Month import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportRefreshDraftReqVO; import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportRespVO; import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportSaveReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportSignOffConfirmReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportSignOffDetailRespVO; import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusActionReqVO; import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusLogRespVO; @@ -32,12 +34,18 @@ public interface MonthlyReportService { PageResult getMonthlyApprovalPage(MonthlyReportPageReqVO reqVO); + PageResult getMonthlySignOffPage(MonthlyReportPageReqVO reqVO); + + MonthlyReportSignOffDetailRespVO getMonthlySignOffDetail(Long id); + void submitMonthlyReport(Long id); void approveMonthlyReport(Long id, MonthlyReportApproveReqVO reqVO); void rejectMonthlyReport(Long id, WorkReportStatusActionReqVO reqVO); + void confirmMonthlySignOff(Long id, MonthlyReportSignOffConfirmReqVO reqVO); + void deleteMonthlyReport(Long id); List getMonthlyStatusLogs(Long id); diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/monthly/MonthlyReportServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/monthly/MonthlyReportServiceImpl.java index 118bc8a..04044a8 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/monthly/MonthlyReportServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/monthly/MonthlyReportServiceImpl.java @@ -9,6 +9,8 @@ import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.Month import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportRefreshDraftReqVO; import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportRespVO; import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportSaveReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportSignOffConfirmReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportSignOffDetailRespVO; import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusActionReqVO; import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusLogRespVO; import com.njcn.rdms.module.project.service.workreport.common.WorkReportCommonService; @@ -71,6 +73,16 @@ public class MonthlyReportServiceImpl implements MonthlyReportService { return workReportCommonService.getMonthlyApprovalPage(reqVO); } + @Override + public PageResult getMonthlySignOffPage(MonthlyReportPageReqVO reqVO) { + return workReportCommonService.getMonthlySignOffPage(reqVO); + } + + @Override + public MonthlyReportSignOffDetailRespVO getMonthlySignOffDetail(Long id) { + return workReportCommonService.getMonthlySignOffDetail(id); + } + @Override public void submitMonthlyReport(Long id) { workReportCommonService.submitMonthlyReport(id); @@ -86,6 +98,11 @@ public class MonthlyReportServiceImpl implements MonthlyReportService { workReportCommonService.rejectMonthlyReport(id, reqVO); } + @Override + public void confirmMonthlySignOff(Long id, MonthlyReportSignOffConfirmReqVO reqVO) { + workReportCommonService.confirmMonthlySignOff(id, reqVO); + } + @Override public void deleteMonthlyReport(Long id) { workReportCommonService.deleteMonthlyReport(id); diff --git a/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/file/FileApi.java b/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/file/FileApi.java index b7230fc..240bfba 100644 --- a/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/file/FileApi.java +++ b/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/file/FileApi.java @@ -1,6 +1,7 @@ package com.njcn.rdms.module.system.api.file; import com.njcn.rdms.framework.common.pojo.CommonResult; +import com.njcn.rdms.module.system.api.file.dto.FileContentCreateReqDTO; import com.njcn.rdms.module.system.api.file.dto.FileRespDTO; import com.njcn.rdms.module.system.enums.ApiConstants; import io.swagger.v3.oas.annotations.Operation; @@ -8,6 +9,8 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.cloud.openfeign.FeignClient; 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.RequestParam; @FeignClient(name = ApiConstants.NAME) @@ -31,4 +34,8 @@ public interface FileApi { @Parameter(name = "id", description = "文件 ID", required = true) CommonResult getFileContent(@RequestParam("id") Long id); + @PostMapping(PREFIX + "/create-by-content") + @Operation(summary = "通过文件字节创建文件") + CommonResult createFileByContent(@RequestBody FileContentCreateReqDTO reqDTO); + } diff --git a/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/file/dto/FileContentCreateReqDTO.java b/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/file/dto/FileContentCreateReqDTO.java new file mode 100644 index 0000000..94c199e --- /dev/null +++ b/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/file/dto/FileContentCreateReqDTO.java @@ -0,0 +1,21 @@ +package com.njcn.rdms.module.system.api.file.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "RPC 服务 - 文件字节创建 Request DTO") +@Data +public class FileContentCreateReqDTO { + + @Schema(description = "文件内容", requiredMode = Schema.RequiredMode.REQUIRED) + private byte[] content; + + @Schema(description = "文件名", example = "2026-06_张三.xlsx") + private String name; + + @Schema(description = "目录", example = "rdms/performance") + private String directory; + + @Schema(description = "MIME 类型", example = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + private String type; +} diff --git a/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/user/UserSignatureApi.java b/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/user/UserSignatureApi.java new file mode 100644 index 0000000..d8582a7 --- /dev/null +++ b/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/user/UserSignatureApi.java @@ -0,0 +1,23 @@ +package com.njcn.rdms.module.system.api.user; + +import com.njcn.rdms.framework.common.pojo.CommonResult; +import com.njcn.rdms.module.system.api.user.dto.UserSignatureRespDTO; +import com.njcn.rdms.module.system.enums.ApiConstants; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@FeignClient(name = ApiConstants.NAME) +@Tag(name = "RPC 服务 - 用户电子签名") +public interface UserSignatureApi { + + String PREFIX = ApiConstants.PREFIX + "/user-signature"; + + @GetMapping(PREFIX + "/get") + @Operation(summary = "根据用户 ID 查询电子签名") + @Parameter(name = "userId", description = "用户 ID", required = true) + CommonResult getByUserId(@RequestParam("userId") Long userId); +} diff --git a/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/user/dto/UserSignatureRespDTO.java b/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/user/dto/UserSignatureRespDTO.java new file mode 100644 index 0000000..f76aeda --- /dev/null +++ b/rdms-system/rdms-system-api/src/main/java/com/njcn/rdms/module/system/api/user/dto/UserSignatureRespDTO.java @@ -0,0 +1,27 @@ +package com.njcn.rdms.module.system.api.user.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "RPC 服务 - 用户电子签名 Response DTO") +@Data +public class UserSignatureRespDTO { + + @Schema(description = "主键", example = "1") + private Long id; + + @Schema(description = "用户 ID", example = "1024") + private Long userId; + + @Schema(description = "文件 ID", example = "2048") + private Long fileId; + + @Schema(description = "文件名", example = "zhangsan-sign.png") + private String fileName; + + @Schema(description = "状态", example = "0") + private Integer status; + + @Schema(description = "备注") + private String remark; +} diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/api/file/FileApiImpl.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/api/file/FileApiImpl.java index d75200b..aa3b3e9 100644 --- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/api/file/FileApiImpl.java +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/api/file/FileApiImpl.java @@ -2,6 +2,7 @@ package com.njcn.rdms.module.system.api.file; import com.njcn.rdms.framework.common.pojo.CommonResult; import com.njcn.rdms.framework.common.util.object.BeanUtils; +import com.njcn.rdms.module.system.api.file.dto.FileContentCreateReqDTO; import com.njcn.rdms.module.system.api.file.dto.FileRespDTO; import com.njcn.rdms.module.system.dal.dataobject.file.FileDO; import com.njcn.rdms.module.system.service.file.FileService; @@ -45,4 +46,10 @@ public class FileApiImpl implements FileApi { } } + @Override + public CommonResult createFileByContent(FileContentCreateReqDTO reqDTO) { + FileDO file = fileService.createFile(reqDTO.getContent(), reqDTO.getName(), reqDTO.getDirectory(), reqDTO.getType()); + return success(file.getId()); + } + } diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/api/user/UserSignatureApiImpl.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/api/user/UserSignatureApiImpl.java new file mode 100644 index 0000000..e4347a6 --- /dev/null +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/api/user/UserSignatureApiImpl.java @@ -0,0 +1,28 @@ +package com.njcn.rdms.module.system.api.user; + +import com.njcn.rdms.framework.common.pojo.CommonResult; +import com.njcn.rdms.framework.common.util.object.BeanUtils; +import com.njcn.rdms.module.system.api.user.dto.UserSignatureRespDTO; +import com.njcn.rdms.module.system.dal.dataobject.user.UserSignatureDO; +import com.njcn.rdms.module.system.service.user.UserSignatureService; +import io.swagger.v3.oas.annotations.Hidden; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RestController; + +import static com.njcn.rdms.framework.common.pojo.CommonResult.success; + +@RestController +@Validated +@Hidden +public class UserSignatureApiImpl implements UserSignatureApi { + + @Resource + private UserSignatureService userSignatureService; + + @Override + public CommonResult getByUserId(Long userId) { + UserSignatureDO signature = userSignatureService.getByUserId(userId); + return success(BeanUtils.toBean(signature, UserSignatureRespDTO.class)); + } +} diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/user/UserSignatureController.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/user/UserSignatureController.java new file mode 100644 index 0000000..5931f93 --- /dev/null +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/user/UserSignatureController.java @@ -0,0 +1,70 @@ +package com.njcn.rdms.module.system.controller.admin.user; + +import com.njcn.rdms.framework.common.pojo.CommonResult; +import com.njcn.rdms.framework.common.util.object.BeanUtils; +import com.njcn.rdms.module.system.controller.admin.user.vo.signature.UserSignatureRespVO; +import com.njcn.rdms.module.system.controller.admin.user.vo.signature.UserSignatureSaveReqVO; +import com.njcn.rdms.module.system.dal.dataobject.user.UserSignatureDO; +import com.njcn.rdms.module.system.service.user.UserSignatureService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +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.DeleteMapping; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import static com.njcn.rdms.framework.common.pojo.CommonResult.success; +import static com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 用户电子签名") +@RestController +@RequestMapping("/system/user-signatures") +@Validated +public class UserSignatureController { + + @Resource + private UserSignatureService userSignatureService; + + @GetMapping("/get") + @Operation(summary = "获取用户电子签名") + @Parameter(name = "userId", description = "用户 ID", required = true) + @PreAuthorize("@ss.hasPermission('system:user-signature:query')") + public CommonResult get(@RequestParam("userId") Long userId) { + return success(toRespVO(userSignatureService.getByUserId(userId))); + } + + @PostMapping("/save") + @Operation(summary = "保存用户电子签名") + @PreAuthorize("@ss.hasPermission('system:user-signature:update')") + public CommonResult save(@Valid @RequestBody UserSignatureSaveReqVO reqVO) { + userSignatureService.save(reqVO.getUserId(), reqVO.getFileId(), reqVO.getFileName(), reqVO.getStatus(), reqVO.getRemark()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除用户电子签名") + @Parameter(name = "userId", description = "用户 ID", required = true) + @PreAuthorize("@ss.hasPermission('system:user-signature:update')") + public CommonResult delete(@RequestParam("userId") Long userId) { + userSignatureService.deleteByUserId(userId); + return success(true); + } + + @GetMapping("/my") + @Operation(summary = "获取当前登录用户电子签名") + public CommonResult my() { + return success(toRespVO(userSignatureService.getByUserId(getLoginUserId()))); + } + + private UserSignatureRespVO toRespVO(UserSignatureDO signature) { + return BeanUtils.toBean(signature, UserSignatureRespVO.class); + } +} diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/user/vo/signature/UserSignatureRespVO.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/user/vo/signature/UserSignatureRespVO.java new file mode 100644 index 0000000..5d55c3d --- /dev/null +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/user/vo/signature/UserSignatureRespVO.java @@ -0,0 +1,27 @@ +package com.njcn.rdms.module.system.controller.admin.user.vo.signature; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 用户电子签名 Response VO") +@Data +public class UserSignatureRespVO { + + private Long id; + + private Long userId; + + private Long fileId; + + private String fileName; + + private Integer status; + + private String remark; + + private LocalDateTime createTime; + + private LocalDateTime updateTime; +} diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/user/vo/signature/UserSignatureSaveReqVO.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/user/vo/signature/UserSignatureSaveReqVO.java new file mode 100644 index 0000000..43be6be --- /dev/null +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/user/vo/signature/UserSignatureSaveReqVO.java @@ -0,0 +1,30 @@ +package com.njcn.rdms.module.system.controller.admin.user.vo.signature; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - 用户电子签名保存 Request VO") +@Data +public class UserSignatureSaveReqVO { + + @Schema(description = "用户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "用户 ID 不能为空") + private Long userId; + + @Schema(description = "电子签文件 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "电子签文件 ID 不能为空") + private Long fileId; + + @Schema(description = "文件名", example = "zhangsan-sign.png") + @Size(max = 255, message = "文件名长度不能超过 255 个字符") + private String fileName; + + @Schema(description = "状态", example = "0") + private Integer status; + + @Schema(description = "备注") + @Size(max = 255, message = "备注长度不能超过 255 个字符") + private String remark; +} diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/dataobject/user/UserSignatureDO.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/dataobject/user/UserSignatureDO.java new file mode 100644 index 0000000..fe7bb93 --- /dev/null +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/dataobject/user/UserSignatureDO.java @@ -0,0 +1,29 @@ +package com.njcn.rdms.module.system.dal.dataobject.user; + +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("system_user_signature") +@Data +@EqualsAndHashCode(callSuper = true) +public class UserSignatureDO extends BaseDO { + + @TableId + private Long id; + + private Long userId; + + private Long fileId; + + private String fileName; + + private Integer status; + + private String remark; +} diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/mysql/user/UserSignatureMapper.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/mysql/user/UserSignatureMapper.java new file mode 100644 index 0000000..38fa90c --- /dev/null +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/dal/mysql/user/UserSignatureMapper.java @@ -0,0 +1,20 @@ +package com.njcn.rdms.module.system.dal.mysql.user; + +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.njcn.rdms.module.system.dal.dataobject.user.UserSignatureDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface UserSignatureMapper extends BaseMapperX { + + default UserSignatureDO selectByUserId(Long userId) { + return selectOne(new LambdaQueryWrapperX() + .eq(UserSignatureDO::getUserId, userId)); + } + + default int deleteByUserId(Long userId) { + return delete(new LambdaQueryWrapperX() + .eq(UserSignatureDO::getUserId, userId)); + } +} diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/user/UserSignatureService.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/user/UserSignatureService.java new file mode 100644 index 0000000..8a5f542 --- /dev/null +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/user/UserSignatureService.java @@ -0,0 +1,12 @@ +package com.njcn.rdms.module.system.service.user; + +import com.njcn.rdms.module.system.dal.dataobject.user.UserSignatureDO; + +public interface UserSignatureService { + + UserSignatureDO getByUserId(Long userId); + + void save(Long userId, Long fileId, String fileName, Integer status, String remark); + + void deleteByUserId(Long userId); +} diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/user/UserSignatureServiceImpl.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/user/UserSignatureServiceImpl.java new file mode 100644 index 0000000..4c7d963 --- /dev/null +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/user/UserSignatureServiceImpl.java @@ -0,0 +1,63 @@ +package com.njcn.rdms.module.system.service.user; + +import com.njcn.rdms.framework.common.enums.CommonStatusEnum; +import com.njcn.rdms.module.system.dal.dataobject.file.FileDO; +import com.njcn.rdms.module.system.dal.dataobject.user.UserSignatureDO; +import com.njcn.rdms.module.system.dal.mysql.user.UserSignatureMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import static com.njcn.rdms.framework.common.util.collection.CollectionUtils.singleton; + +@Service +public class UserSignatureServiceImpl implements UserSignatureService { + + @Resource + private UserSignatureMapper userSignatureMapper; + @Resource + private AdminUserService adminUserService; + @Resource + private com.njcn.rdms.module.system.service.file.FileService fileService; + + @Override + public UserSignatureDO getByUserId(Long userId) { + if (userId == null) { + return null; + } + return userSignatureMapper.selectByUserId(userId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void save(Long userId, Long fileId, String fileName, Integer status, String remark) { + adminUserService.validateUserList(singleton(userId)); + FileDO file = fileService.getFile(fileId); + + UserSignatureDO existing = userSignatureMapper.selectByUserId(userId); + UserSignatureDO target = new UserSignatureDO(); + if (existing != null) { + target.setId(existing.getId()); + } + target.setUserId(userId); + target.setFileId(fileId); + target.setFileName(StringUtils.hasText(fileName) ? fileName.trim() : file.getName()); + target.setStatus(status == null ? CommonStatusEnum.ENABLE.getStatus() : status); + target.setRemark(StringUtils.hasText(remark) ? remark.trim() : null); + if (existing == null) { + userSignatureMapper.insert(target); + } else { + userSignatureMapper.updateById(target); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteByUserId(Long userId) { + if (userId == null) { + return; + } + userSignatureMapper.deleteByUserId(userId); + } +}