diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/ProjectServerApplication.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/ProjectServerApplication.java index f8c939e..3366b4a 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/ProjectServerApplication.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/ProjectServerApplication.java @@ -2,11 +2,13 @@ package com.njcn.rdms.module.project; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; /** * 项目交付域服务启动类 */ @SpringBootApplication +@EnableScheduling public class ProjectServerApplication { public static void main(String[] args) { 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 4acf74a..13e4313 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 @@ -14,6 +14,7 @@ import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.Month import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportDefaultDraftReqVO; import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportExportVO; import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportPageReqVO; +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.service.workreport.export.WorkReportContentExportService; @@ -66,6 +67,13 @@ public class MonthlyReportController { return success(monthlyReportService.previewMonthlyDefaultDraft(reqVO)); } + @PostMapping("/refresh-draft") + @Operation(summary = "手动刷新月报默认稿") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_CREATE + "')") + public CommonResult refreshMonthlyDraft(@Valid @RequestBody MonthlyReportRefreshDraftReqVO reqVO) { + return success(monthlyReportService.refreshMonthlyDraft(reqVO)); + } + @PostMapping @Operation(summary = "新建月报草稿") @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_CREATE + "')") diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportRefreshDraftReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportRefreshDraftReqVO.java new file mode 100644 index 0000000..798625f --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/monthly/vo/MonthlyReportRefreshDraftReqVO.java @@ -0,0 +1,40 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo; + +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportPlanItemReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportReviewItemReqVO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; +import java.util.List; + +import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY; + +@Schema(description = "管理后台 - 月报手动刷新默认稿 Request VO") +@Data +public class MonthlyReportRefreshDraftReqVO { + + @NotBlank(message = "周期编码不能为空") + private String periodKey; + + @NotBlank(message = "周期名称不能为空") + private String periodLabel; + + @NotNull(message = "周期开始日期不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private LocalDate periodStartDate; + + @NotNull(message = "周期结束日期不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private LocalDate periodEndDate; + + @Valid + private List reviewItems; + + @Valid + private List planItems; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/ProjectReportController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/ProjectReportController.java index f4ddc65..92e6585 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/ProjectReportController.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/ProjectReportController.java @@ -14,6 +14,7 @@ import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.Proje import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportExportVO; import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportOwnerProjectOptionRespVO; import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportPageReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportRefreshDraftReqVO; import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportRespVO; import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportSaveReqVO; import com.njcn.rdms.module.project.service.workreport.export.WorkReportContentExportService; @@ -75,6 +76,14 @@ public class ProjectReportController { return success(projectReportService.previewProjectDefaultDraft(projectId, reqVO)); } + @PostMapping("/{projectId}/refresh-draft") + @Operation(summary = "手动刷新项目半月报默认稿") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_PROJECT_OWNER + "')") + public CommonResult refreshProjectDraft(@PathVariable("projectId") Long projectId, + @Valid @RequestBody ProjectReportRefreshDraftReqVO reqVO) { + return success(projectReportService.refreshProjectDraft(projectId, reqVO)); + } + @PostMapping @Operation(summary = "新建项目半月报草稿") @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_PROJECT_OWNER + "')") diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportRefreshDraftReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportRefreshDraftReqVO.java new file mode 100644 index 0000000..5d39682 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/project/vo/ProjectReportRefreshDraftReqVO.java @@ -0,0 +1,49 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.project.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; +import java.util.List; + +import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY; + +@Schema(description = "管理后台 - 项目半月报手动刷新默认稿 Request VO") +@Data +public class ProjectReportRefreshDraftReqVO { + + @NotBlank(message = "周期编码不能为空") + private String periodKey; + + @NotBlank(message = "周期名称不能为空") + private String periodLabel; + + @NotNull(message = "周期开始日期不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private LocalDate periodStartDate; + + @NotNull(message = "周期结束日期不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private LocalDate periodEndDate; + + @NotNull(message = "上半月/下半月标记不能为空") + private Integer flag; + + private String projectStatusDesc; + + private String projectProgressPlan; + + private String projectKeyPoints; + + private String projectProblems; + + @Valid + private List currentItems; + + @Valid + private List nextItems; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/WeeklyReportController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/WeeklyReportController.java index 78d4442..a4078d9 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/WeeklyReportController.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/WeeklyReportController.java @@ -13,6 +13,7 @@ import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.Weekly import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportDefaultDraftReqVO; import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportExportVO; import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportPageReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportRefreshDraftReqVO; import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportRespVO; import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportSaveReqVO; import com.njcn.rdms.module.project.service.workreport.export.WorkReportContentExportService; @@ -65,6 +66,13 @@ public class WeeklyReportController { return success(weeklyReportService.previewWeeklyDefaultDraft(reqVO)); } + @PostMapping("/refresh-draft") + @Operation(summary = "手动刷新周报默认稿") + @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_CREATE + "')") + public CommonResult refreshWeeklyDraft(@Valid @RequestBody WeeklyReportRefreshDraftReqVO reqVO) { + return success(weeklyReportService.refreshWeeklyDraft(reqVO)); + } + @PostMapping @Operation(summary = "新建周报草稿") @PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_CREATE + "')") diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportRefreshDraftReqVO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportRefreshDraftReqVO.java new file mode 100644 index 0000000..050f057 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/workreport/weekly/vo/WeeklyReportRefreshDraftReqVO.java @@ -0,0 +1,46 @@ +package com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo; + +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportPlanItemReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportReviewItemReqVO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; +import java.util.List; + +import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY; + +@Schema(description = "管理后台 - 周报手动刷新默认稿 Request VO") +@Data +public class WeeklyReportRefreshDraftReqVO { + + @NotBlank(message = "周期编码不能为空") + private String periodKey; + + @NotBlank(message = "周期名称不能为空") + private String periodLabel; + + @NotNull(message = "周期开始日期不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private LocalDate periodStartDate; + + @NotNull(message = "周期结束日期不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + private LocalDate periodEndDate; + + @NotNull(message = "是否出差不能为空") + private Boolean isBusinessTrip; + + @Valid + private List reviewItems; + + @Valid + private List planItems; + + @Valid + private List travelSegments; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/job/JobRunLogDO.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/job/JobRunLogDO.java new file mode 100644 index 0000000..cf05eb5 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/dataobject/job/JobRunLogDO.java @@ -0,0 +1,50 @@ +package com.njcn.rdms.module.project.dal.dataobject.job; + +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.LocalDate; +import java.time.LocalDateTime; + +/** + * RDMS 任务运行日志表 + */ +@TableName("rdms_work_report_job_run_log") +@Data +@EqualsAndHashCode(callSuper = true) +public class JobRunLogDO extends BaseDO { + + @TableId + private Long id; + + private String runId; + + private String jobName; + + private String jobStatus; + + private LocalDate runDate; + + private String periodKey; + + private String triggerSource; + + private String nodeName; + + private LocalDateTime startedAt; + + private LocalDateTime finishedAt; + + private Integer successCount; + + private Integer skipCount; + + private Integer failCount; + + private Long costMs; + + private String message; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/job/JobRunLogMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/job/JobRunLogMapper.java new file mode 100644 index 0000000..54ab9bd --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/job/JobRunLogMapper.java @@ -0,0 +1,9 @@ +package com.njcn.rdms.module.project.dal.mysql.job; + +import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX; +import com.njcn.rdms.module.project.dal.dataobject.job.JobRunLogDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface JobRunLogMapper extends BaseMapperX { +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectMapper.java index 4c5bba6..6f8e15d 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectMapper.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectMapper.java @@ -65,6 +65,16 @@ public interface ProjectMapper extends BaseMapperX { .orderByDesc(BaseDO::getCreateTime)); } + default List selectListByStatusCodesNotIn(Collection statusCodes) { + LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX<>(); + queryWrapper.isNotNull(ProjectDO::getManagerUserId); + queryWrapper.orderByDesc(BaseDO::getCreateTime); + if (statusCodes != null && !statusCodes.isEmpty()) { + queryWrapper.notIn(ProjectDO::getStatusCode, statusCodes); + } + return selectList(queryWrapper); + } + default int updateStatusByIdAndStatus(Long id, String fromStatus, String toStatus, String lastStatusReason) { ProjectDO update = new ProjectDO(); update.setStatusCode(toStatus); diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/autogen/AutoGenPeriod.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/autogen/AutoGenPeriod.java new file mode 100644 index 0000000..5511a10 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/autogen/AutoGenPeriod.java @@ -0,0 +1,21 @@ +package com.njcn.rdms.module.project.service.workreport.autogen; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.time.LocalDate; + +@Data +@Accessors(chain = true) +public class AutoGenPeriod { + + private String periodKey; + + private String periodLabel; + + private LocalDate periodStartDate; + + private LocalDate periodEndDate; + + private Integer flag; +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/autogen/AutoGenResult.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/autogen/AutoGenResult.java new file mode 100644 index 0000000..9c46cee --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/autogen/AutoGenResult.java @@ -0,0 +1,23 @@ +package com.njcn.rdms.module.project.service.workreport.autogen; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Data +public class AutoGenResult { + + private LocalDateTime startedAt; + + private LocalDateTime finishedAt; + + private int successCount; + + private int skipCount; + + private int failCount; + + private final List failMessages = new ArrayList<>(); +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/autogen/WorkReportAutoGenProperties.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/autogen/WorkReportAutoGenProperties.java new file mode 100644 index 0000000..91577ac --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/autogen/WorkReportAutoGenProperties.java @@ -0,0 +1,38 @@ +package com.njcn.rdms.module.project.service.workreport.autogen; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Component +@ConfigurationProperties(prefix = "rdms.work-report.auto-gen") +@Data +public class WorkReportAutoGenProperties { + + private boolean enabled = false; + + private TriggerProperties weekly = new TriggerProperties(); + + private TriggerProperties monthly = new TriggerProperties(); + + private TriggerProperties project = new TriggerProperties(); + + private ScopeProperties scope = new ScopeProperties(); + + @Data + public static class TriggerProperties { + + private boolean enabled = false; + + private String cron; + } + + @Data + public static class ScopeProperties { + + private List deptIds = new ArrayList<>(); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/autogen/WorkReportAutoGenerateScheduler.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/autogen/WorkReportAutoGenerateScheduler.java new file mode 100644 index 0000000..c079866 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/autogen/WorkReportAutoGenerateScheduler.java @@ -0,0 +1,27 @@ +package com.njcn.rdms.module.project.service.workreport.autogen; + +import jakarta.annotation.Resource; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +public class WorkReportAutoGenerateScheduler { + + @Resource + private WorkReportAutoGenerateService workReportAutoGenerateService; + + @Scheduled(cron = "${rdms.work-report.auto-gen.weekly.cron:0 0 12 ? * FRI}", zone = "Asia/Shanghai") + public void weekly() { + workReportAutoGenerateService.autoGenerateWeeklyBySchedule(); + } + + @Scheduled(cron = "${rdms.work-report.auto-gen.monthly.cron:0 0 12 1-31 * ?}", zone = "Asia/Shanghai") + public void monthlyDaily() { + workReportAutoGenerateService.autoGenerateMonthlyBySchedule(); + } + + @Scheduled(cron = "${rdms.work-report.auto-gen.project.cron:0 0 12 1-31 * ?}", zone = "Asia/Shanghai") + public void projectDaily() { + workReportAutoGenerateService.autoGenerateProjectBySchedule(); + } +} diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/autogen/WorkReportAutoGenerateService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/autogen/WorkReportAutoGenerateService.java new file mode 100644 index 0000000..b1489f0 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/autogen/WorkReportAutoGenerateService.java @@ -0,0 +1,486 @@ +package com.njcn.rdms.module.project.service.workreport.autogen; + +import com.njcn.rdms.framework.common.enums.UserTypeEnum; +import com.njcn.rdms.framework.common.exception.ServiceException; +import com.njcn.rdms.framework.common.pojo.CommonResult; +import com.njcn.rdms.framework.common.util.json.JsonUtils; +import com.njcn.rdms.framework.security.core.LoginUser; +import com.njcn.rdms.module.project.constant.ProjectObjectConstants; +import com.njcn.rdms.module.project.constant.WorkReportConstants; +import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportSaveReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportSaveReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportSaveReqVO; +import com.njcn.rdms.module.project.dal.dataobject.job.JobRunLogDO; +import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO; +import com.njcn.rdms.module.project.dal.mysql.job.JobRunLogMapper; +import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper; +import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper; +import com.njcn.rdms.module.project.dal.mysql.workreport.monthly.MonthlyReportMapper; +import com.njcn.rdms.module.project.dal.mysql.workreport.project.ProjectReportMapper; +import com.njcn.rdms.module.project.dal.mysql.workreport.weekly.WeeklyReportMapper; +import com.njcn.rdms.module.project.enums.ErrorCodeConstants; +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.user.AdminUserApi; +import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.net.InetAddress; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.TemporalAdjusters; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class WorkReportAutoGenerateService { + + private static final ZoneId ASIA_SHANGHAI = ZoneId.of("Asia/Shanghai"); + private static final String TRIGGER_SOURCE_SCHEDULE = "SCHEDULE"; + private static final String STATUS_RUNNING = "RUNNING"; + private static final String STATUS_SUCCESS = "SUCCESS"; + private static final String STATUS_PARTIAL_SUCCESS = "PARTIAL_SUCCESS"; + private static final String STATUS_FAILED = "FAILED"; + private static final String JOB_NAME_WEEKLY = "WORK_REPORT_AUTO_GEN_WEEKLY"; + private static final String JOB_NAME_MONTHLY = "WORK_REPORT_AUTO_GEN_MONTHLY"; + private static final String JOB_NAME_PROJECT = "WORK_REPORT_AUTO_GEN_PROJECT"; + private static final String LOCK_KEY_TEMPLATE = "auto-gen:work-report:%s:%s:%s"; + private static final int MAX_FAIL_MESSAGES = 20; + + @Resource + private WorkReportAutoGenProperties properties; + @Resource + private WorkReportCommonService workReportCommonService; + @Resource + private WeeklyReportMapper weeklyReportMapper; + @Resource + private MonthlyReportMapper monthlyReportMapper; + @Resource + private ProjectReportMapper projectReportMapper; + @Resource + private ProjectMapper projectMapper; + @Resource + private ObjectStatusModelMapper objectStatusModelMapper; + @Resource + private JobRunLogMapper jobRunLogMapper; + @Resource + private DeptApi deptApi; + @Resource + private AdminUserApi adminUserApi; + @Resource + private RedissonClient redissonClient; + + public void autoGenerateWeeklyBySchedule() { + if (!properties.isEnabled() || !properties.getWeekly().isEnabled()) { + return; + } + LocalDate today = LocalDate.now(ASIA_SHANGHAI); + AutoGenPeriod period = buildWeeklyPeriod(today); + runAs(buildSystemLoginUser(), () -> executeWeekly(period, today)); + } + + public void autoGenerateMonthlyBySchedule() { + if (!properties.isEnabled() || !properties.getMonthly().isEnabled()) { + return; + } + LocalDate today = LocalDate.now(ASIA_SHANGHAI); + if (!today.equals(today.with(TemporalAdjusters.lastDayOfMonth()))) { + return; + } + AutoGenPeriod period = buildMonthlyPeriod(today); + runAs(buildSystemLoginUser(), () -> executeMonthly(period, today)); + } + + public void autoGenerateProjectBySchedule() { + if (!properties.isEnabled() || !properties.getProject().isEnabled()) { + return; + } + LocalDate today = LocalDate.now(ASIA_SHANGHAI); + if (today.getDayOfMonth() != 15) { + return; + } + AutoGenPeriod period = buildProjectFirstHalfPeriod(today); + runAs(buildSystemLoginUser(), () -> executeProject(period, today)); + } + + private void executeWeekly(AutoGenPeriod period, LocalDate runDate) { + JobRunLogDO jobLog = createRunningJobLog(JOB_NAME_WEEKLY, runDate, period.getPeriodKey()); + AutoGenResult result = new AutoGenResult(); + result.setStartedAt(jobLog.getStartedAt()); + try { + for (AdminUserRespDTO user : loadRdUsers()) { + processWeeklyUser(period, result, user); + } + } catch (Exception ex) { + appendFailMessage(result, "job=" + JOB_NAME_WEEKLY + ", message=" + ex.getMessage()); + log.error("[executeWeekly][periodKey={}]", period.getPeriodKey(), ex); + } finally { + finishJobLog(jobLog, result); + } + } + + private void executeMonthly(AutoGenPeriod period, LocalDate runDate) { + JobRunLogDO jobLog = createRunningJobLog(JOB_NAME_MONTHLY, runDate, period.getPeriodKey()); + AutoGenResult result = new AutoGenResult(); + result.setStartedAt(jobLog.getStartedAt()); + try { + for (AdminUserRespDTO user : loadRdUsers()) { + processMonthlyUser(period, result, user); + } + } catch (Exception ex) { + appendFailMessage(result, "job=" + JOB_NAME_MONTHLY + ", message=" + ex.getMessage()); + log.error("[executeMonthly][periodKey={}]", period.getPeriodKey(), ex); + } finally { + finishJobLog(jobLog, result); + } + } + + private void executeProject(AutoGenPeriod period, LocalDate runDate) { + JobRunLogDO jobLog = createRunningJobLog(JOB_NAME_PROJECT, runDate, period.getPeriodKey()); + AutoGenResult result = new AutoGenResult(); + result.setStartedAt(jobLog.getStartedAt()); + try { + for (ProjectAutoGenCandidate candidate : loadProjectCandidates()) { + processProjectCandidate(period, result, candidate); + } + } catch (Exception ex) { + appendFailMessage(result, "job=" + JOB_NAME_PROJECT + ", message=" + ex.getMessage()); + log.error("[executeProject][periodKey={}]", period.getPeriodKey(), ex); + } finally { + finishJobLog(jobLog, result); + } + } + + private void processWeeklyUser(AutoGenPeriod period, AutoGenResult result, AdminUserRespDTO user) { + String subjectKey = String.valueOf(user.getId()); + executeWithLock(WorkReportConstants.REPORT_TYPE_WEEKLY, period.getPeriodKey(), subjectKey, result, () -> { + if (weeklyReportMapper.selectByReporterIdAndPeriodKey(user.getId(), period.getPeriodKey()) != null) { + result.setSkipCount(result.getSkipCount() + 1); + return; + } + WeeklyReportSaveReqVO reqVO = new WeeklyReportSaveReqVO(); + reqVO.setPeriodKey(period.getPeriodKey()); + reqVO.setPeriodLabel(period.getPeriodLabel()); + reqVO.setPeriodStartDate(period.getPeriodStartDate()); + reqVO.setPeriodEndDate(period.getPeriodEndDate()); + reqVO.setIsBusinessTrip(Boolean.FALSE); + reqVO.setReviewItems(Collections.emptyList()); + reqVO.setPlanItems(Collections.emptyList()); + reqVO.setTravelSegments(Collections.emptyList()); + runAs(buildLoginUser(user), () -> workReportCommonService.createWeeklyReport(reqVO, user.getId())); + result.setSuccessCount(result.getSuccessCount() + 1); + }, () -> "userId=" + user.getId()); + } + + private void processMonthlyUser(AutoGenPeriod period, AutoGenResult result, AdminUserRespDTO user) { + String subjectKey = String.valueOf(user.getId()); + executeWithLock(WorkReportConstants.REPORT_TYPE_MONTHLY, period.getPeriodKey(), subjectKey, result, () -> { + if (monthlyReportMapper.selectByReporterIdAndPeriodKey(user.getId(), period.getPeriodKey()) != null) { + result.setSkipCount(result.getSkipCount() + 1); + return; + } + MonthlyReportSaveReqVO reqVO = new MonthlyReportSaveReqVO(); + reqVO.setPeriodKey(period.getPeriodKey()); + reqVO.setPeriodLabel(period.getPeriodLabel()); + reqVO.setPeriodStartDate(period.getPeriodStartDate()); + reqVO.setPeriodEndDate(period.getPeriodEndDate()); + reqVO.setReviewItems(Collections.emptyList()); + reqVO.setPlanItems(Collections.emptyList()); + runAs(buildLoginUser(user), () -> workReportCommonService.createMonthlyReport(reqVO, user.getId())); + result.setSuccessCount(result.getSuccessCount() + 1); + }, () -> "userId=" + user.getId()); + } + + private void processProjectCandidate(AutoGenPeriod period, AutoGenResult result, ProjectAutoGenCandidate candidate) { + String subjectKey = candidate.projectId() + ":" + candidate.user().getId(); + executeWithLock(WorkReportConstants.REPORT_TYPE_PROJECT, period.getPeriodKey(), subjectKey, result, () -> { + if (projectReportMapper.selectByProjectIdAndPeriodKeyAndProjectOwnerId( + candidate.projectId(), period.getPeriodKey(), candidate.user().getId()) != null) { + result.setSkipCount(result.getSkipCount() + 1); + return; + } + ProjectReportSaveReqVO reqVO = new ProjectReportSaveReqVO(); + reqVO.setProjectId(candidate.projectId()); + reqVO.setPeriodKey(period.getPeriodKey()); + reqVO.setPeriodLabel(period.getPeriodLabel()); + reqVO.setPeriodStartDate(period.getPeriodStartDate()); + reqVO.setPeriodEndDate(period.getPeriodEndDate()); + reqVO.setFlag(period.getFlag()); + reqVO.setCurrentItems(Collections.emptyList()); + reqVO.setNextItems(Collections.emptyList()); + runAs(buildLoginUser(candidate.user()), () -> workReportCommonService.createProjectReport(reqVO, candidate.user().getId())); + result.setSuccessCount(result.getSuccessCount() + 1); + }, () -> "projectId=" + candidate.projectId() + ", userId=" + candidate.user().getId()); + } + + private void executeWithLock(String reportType, String periodKey, String subjectKey, AutoGenResult result, + Runnable action, Supplier failContextSupplier) { + RLock lock = redissonClient.getLock(String.format(LOCK_KEY_TEMPLATE, reportType, periodKey, subjectKey)); + try { + if (!lock.tryLock(0, 30, TimeUnit.SECONDS)) { + result.setSkipCount(result.getSkipCount() + 1); + return; + } + try { + action.run(); + } catch (ServiceException ex) { + if (Objects.equals(ex.getCode(), ErrorCodeConstants.WORK_REPORT_PERIOD_DUPLICATE.getCode())) { + result.setSkipCount(result.getSkipCount() + 1); + return; + } + result.setFailCount(result.getFailCount() + 1); + appendFailMessage(result, failContextSupplier.get() + ", code=" + ex.getCode() + ", message=" + ex.getMessage()); + log.error("[executeWithLock][reportType={}][periodKey={}][subjectKey={}]", reportType, periodKey, subjectKey, ex); + } catch (Exception ex) { + result.setFailCount(result.getFailCount() + 1); + appendFailMessage(result, failContextSupplier.get() + ", message=" + ex.getMessage()); + log.error("[executeWithLock][reportType={}][periodKey={}][subjectKey={}]", reportType, periodKey, subjectKey, ex); + } + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + result.setFailCount(result.getFailCount() + 1); + appendFailMessage(result, failContextSupplier.get() + ", message=lock interrupted"); + } finally { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } + + private List loadRdUsers() { + List rootDeptIds = properties.getScope().getDeptIds(); + if (rootDeptIds == null || rootDeptIds.isEmpty()) { + log.warn("[loadRdUsers][未配置 rdms.work-report.auto-gen.scope.dept-ids,自动生成功能跳过]"); + return Collections.emptyList(); + } + Set deptIds = new LinkedHashSet<>(); + for (Long rootDeptId : rootDeptIds) { + if (rootDeptId == null) { + continue; + } + deptIds.add(rootDeptId); + CommonResult> childResult = deptApi.getChildDeptList(rootDeptId); + List childDepts = childResult == null || childResult.getCheckedData() == null + ? Collections.emptyList() + : childResult.getCheckedData(); + for (DeptRespDTO dept : childDepts) { + if (dept != null && dept.getId() != null) { + deptIds.add(dept.getId()); + } + } + } + if (deptIds.isEmpty()) { + return Collections.emptyList(); + } + CommonResult> result = adminUserApi.getUserListByDeptIds(deptIds); + List users = result == null || result.getCheckedData() == null + ? Collections.emptyList() + : result.getCheckedData(); + Map deduplicated = new LinkedHashMap<>(); + for (AdminUserRespDTO user : users) { + if (user == null || user.getId() == null || !Objects.equals(user.getStatus(), 0)) { + continue; + } + deduplicated.put(user.getId(), user); + } + return new ArrayList<>(deduplicated.values()); + } + + private List loadProjectCandidates() { + List terminalStatusCodes = objectStatusModelMapper + .selectTerminalStatusCodesByObjectTypeEnabled(ProjectObjectConstants.OBJECT_TYPE); + List projects = projectMapper.selectListByStatusCodesNotIn(terminalStatusCodes); + if (projects == null || projects.isEmpty()) { + return Collections.emptyList(); + } + Set managerUserIds = projects.stream() + .map(ProjectDO::getManagerUserId) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(LinkedHashSet::new)); + if (managerUserIds.isEmpty()) { + return Collections.emptyList(); + } + CommonResult> result = adminUserApi.getUserList(managerUserIds); + List users = result == null || result.getCheckedData() == null + ? Collections.emptyList() + : result.getCheckedData(); + Map userMap = users.stream() + .filter(Objects::nonNull) + .filter(user -> user.getId() != null) + .collect(Collectors.toMap(AdminUserRespDTO::getId, user -> user, (left, right) -> left, LinkedHashMap::new)); + List candidates = new ArrayList<>(); + for (ProjectDO project : projects) { + if (project.getManagerUserId() == null) { + continue; + } + AdminUserRespDTO user = userMap.get(project.getManagerUserId()); + if (user == null || !Objects.equals(user.getStatus(), 0)) { + continue; + } + candidates.add(new ProjectAutoGenCandidate(project.getId(), user)); + } + return candidates; + } + + private AutoGenPeriod buildWeeklyPeriod(LocalDate today) { + LocalDate periodStartDate = today.minusDays(today.getDayOfWeek().getValue() - 1L); + LocalDate periodEndDate = periodStartDate.plusDays(6); + int week = periodStartDate.get(java.time.temporal.IsoFields.WEEK_OF_WEEK_BASED_YEAR); + int year = periodStartDate.get(java.time.temporal.IsoFields.WEEK_BASED_YEAR); + return new AutoGenPeriod() + .setPeriodKey(String.format("%d-W%02d", year, week)) + .setPeriodLabel(String.format("%d 年第 %02d 周", year, week)) + .setPeriodStartDate(periodStartDate) + .setPeriodEndDate(periodEndDate); + } + + private AutoGenPeriod buildMonthlyPeriod(LocalDate today) { + return new AutoGenPeriod() + .setPeriodKey(String.format("%d-%02d", today.getYear(), today.getMonthValue())) + .setPeriodLabel(String.format("%d 年 %02d 月", today.getYear(), today.getMonthValue())) + .setPeriodStartDate(today.withDayOfMonth(1)) + .setPeriodEndDate(today.withDayOfMonth(today.lengthOfMonth())); + } + + private AutoGenPeriod buildProjectFirstHalfPeriod(LocalDate today) { + return new AutoGenPeriod() + .setPeriodKey(String.format("%d-%02d-01", today.getYear(), today.getMonthValue())) + .setPeriodLabel(String.format("%d 年 %02d 月 上半月", today.getYear(), today.getMonthValue())) + .setPeriodStartDate(today.withDayOfMonth(1)) + .setPeriodEndDate(today.withDayOfMonth(15)) + .setFlag(1); + } + + private JobRunLogDO createRunningJobLog(String jobName, LocalDate runDate, String periodKey) { + JobRunLogDO jobLog = new JobRunLogDO(); + jobLog.setRunId(UUID.randomUUID().toString().replace("-", "")); + jobLog.setJobName(jobName); + jobLog.setJobStatus(STATUS_RUNNING); + jobLog.setRunDate(runDate); + jobLog.setPeriodKey(periodKey); + jobLog.setTriggerSource(TRIGGER_SOURCE_SCHEDULE); + jobLog.setNodeName(resolveNodeName()); + jobLog.setStartedAt(LocalDateTime.now(ASIA_SHANGHAI)); + jobLog.setSuccessCount(0); + jobLog.setSkipCount(0); + jobLog.setFailCount(0); + jobRunLogMapper.insert(jobLog); + return jobLog; + } + + private void finishJobLog(JobRunLogDO jobLog, AutoGenResult result) { + LocalDateTime finishedAt = LocalDateTime.now(ASIA_SHANGHAI); + result.setFinishedAt(finishedAt); + jobLog.setFinishedAt(finishedAt); + jobLog.setSuccessCount(result.getSuccessCount()); + jobLog.setSkipCount(result.getSkipCount()); + jobLog.setFailCount(result.getFailCount()); + jobLog.setCostMs(java.time.Duration.between(result.getStartedAt(), finishedAt).toMillis()); + jobLog.setJobStatus(resolveJobStatus(result)); + jobLog.setMessage(buildMessage(result.getFailMessages())); + jobRunLogMapper.updateById(jobLog); + } + + private String resolveJobStatus(AutoGenResult result) { + if (result.getFailCount() == 0) { + return STATUS_SUCCESS; + } + if (result.getSuccessCount() > 0 || result.getSkipCount() > 0) { + return STATUS_PARTIAL_SUCCESS; + } + return STATUS_FAILED; + } + + private String buildMessage(List failMessages) { + if (failMessages == null || failMessages.isEmpty()) { + return null; + } + String message = JsonUtils.toJsonString(failMessages); + return message.length() > 2000 ? message.substring(0, 2000) : message; + } + + private void appendFailMessage(AutoGenResult result, String message) { + if (result.getFailMessages().size() >= MAX_FAIL_MESSAGES) { + return; + } + result.getFailMessages().add(StringUtils.hasText(message) ? message : "unknown error"); + } + + private LoginUser buildLoginUser(AdminUserRespDTO user) { + LoginUser loginUser = new LoginUser(); + loginUser.setId(user.getId()); + loginUser.setUserType(UserTypeEnum.ADMIN.getValue()); + Map info = new LinkedHashMap<>(); + info.put(LoginUser.INFO_KEY_NICKNAME, safeText(user.getNickname())); + if (user.getDeptId() != null) { + info.put(LoginUser.INFO_KEY_DEPT_ID, String.valueOf(user.getDeptId())); + } + loginUser.setInfo(info); + return loginUser; + } + + private LoginUser buildSystemLoginUser() { + LoginUser loginUser = new LoginUser(); + loginUser.setId(0L); + loginUser.setUserType(UserTypeEnum.ADMIN.getValue()); + Map info = new LinkedHashMap<>(); + info.put(LoginUser.INFO_KEY_NICKNAME, "system"); + loginUser.setInfo(info); + return loginUser; + } + + private void runAs(LoginUser loginUser, Runnable action) { + runAs(loginUser, () -> { + action.run(); + return null; + }); + } + + private T runAs(LoginUser loginUser, Supplier supplier) { + SecurityContext previousContext = SecurityContextHolder.getContext(); + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(new UsernamePasswordAuthenticationToken(loginUser, null, Collections.emptyList())); + SecurityContextHolder.setContext(context); + try { + return supplier.get(); + } finally { + SecurityContextHolder.setContext(previousContext); + } + } + + private String resolveNodeName() { + try { + return InetAddress.getLocalHost().getHostName(); + } catch (Exception ex) { + return "unknown"; + } + } + + private String safeText(String value) { + return value == null ? "" : value.trim(); + } + + private record ProjectAutoGenCandidate(Long projectId, AdminUserRespDTO user) { + } +} 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 9189a36..ff8c467 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 @@ -166,8 +166,30 @@ public class WorkReportCommonService { @Transactional(rollbackFor = Exception.class) public Long createWeeklyReport(WeeklyReportSaveReqVO reqVO) { + return createWeeklyReportInternal(reqVO, null); + } + + @Transactional(rollbackFor = Exception.class) + public Long createWeeklyReport(WeeklyReportSaveReqVO reqVO, Long overrideReporterId) { + return createWeeklyReportInternal(reqVO, overrideReporterId); + } + + public WeeklyReportRespVO mergeWeeklyDraft(WeeklyReportRespVO latestDraft, WeeklyReportRefreshDraftReqVO reqVO) { + WeeklyReportRespVO respVO = latestDraft; + respVO.setIsBusinessTrip(Boolean.TRUE.equals(reqVO.getIsBusinessTrip())); + respVO.setTravelSegments(BeanUtils.toBean(defaultList(reqVO.getTravelSegments()), WeeklyReportTravelSegmentRespVO.class)); + respVO.setReviewItems(mergeReviewItems(reqVO.getReviewItems(), latestDraft.getReviewItems())); + respVO.setPlanItems(mergePlanItems(reqVO.getPlanItems(), latestDraft.getPlanItems())); + respVO.setTotalTravelDays(Boolean.TRUE.equals(reqVO.getIsBusinessTrip()) + ? defaultIfNull(sumTravelDays(reqVO.getTravelSegments())) + : BigDecimal.ZERO); + respVO.setTotalWorkHours(sumReviewWorkHoursResp(respVO.getReviewItems())); + return respVO; + } + + private Long createWeeklyReportInternal(WeeklyReportSaveReqVO reqVO, Long overrideReporterId) { validatePeriod(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate()); - CurrentUserProfile profile = loadCurrentUserProfile(true); + CurrentUserProfile profile = resolveProfile(overrideReporterId, true); validateWeeklyDuplicate(profile.userId(), reqVO.getPeriodKey(), null); WeeklyReportDO report = new WeeklyReportDO(); @@ -308,8 +330,25 @@ public class WorkReportCommonService { @Transactional(rollbackFor = Exception.class) public Long createMonthlyReport(MonthlyReportSaveReqVO reqVO) { + return createMonthlyReportInternal(reqVO, null); + } + + @Transactional(rollbackFor = Exception.class) + public Long createMonthlyReport(MonthlyReportSaveReqVO reqVO, Long overrideReporterId) { + return createMonthlyReportInternal(reqVO, overrideReporterId); + } + + public MonthlyReportRespVO mergeMonthlyDraft(MonthlyReportRespVO latestDraft, MonthlyReportRefreshDraftReqVO reqVO) { + MonthlyReportRespVO respVO = latestDraft; + respVO.setReviewItems(mergeReviewItems(reqVO.getReviewItems(), latestDraft.getReviewItems())); + respVO.setPlanItems(mergePlanItems(reqVO.getPlanItems(), latestDraft.getPlanItems())); + respVO.setTotalWorkHours(sumReviewWorkHoursResp(respVO.getReviewItems())); + return respVO; + } + + private Long createMonthlyReportInternal(MonthlyReportSaveReqVO reqVO, Long overrideReporterId) { validatePeriod(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate()); - CurrentUserProfile profile = loadCurrentUserProfile(true); + CurrentUserProfile profile = resolveProfile(overrideReporterId, true); validateMonthlyDuplicate(profile.userId(), reqVO.getPeriodKey(), null); MonthlyReportDO report = new MonthlyReportDO(); @@ -460,9 +499,30 @@ public class WorkReportCommonService { @Transactional(rollbackFor = Exception.class) public Long createProjectReport(ProjectReportSaveReqVO reqVO) { + return createProjectReportInternal(reqVO, null); + } + + @Transactional(rollbackFor = Exception.class) + public Long createProjectReport(ProjectReportSaveReqVO reqVO, Long overrideReporterId) { + return createProjectReportInternal(reqVO, overrideReporterId); + } + + public ProjectReportRespVO mergeProjectDraft(ProjectReportRespVO latestDraft, ProjectReportRefreshDraftReqVO reqVO) { + ProjectReportRespVO respVO = latestDraft; + respVO.setProjectStatusDesc(normalizeNullableText(reqVO.getProjectStatusDesc())); + respVO.setProjectProgressPlan(normalizeNullableText(reqVO.getProjectProgressPlan())); + respVO.setProjectKeyPoints(normalizeNullableText(reqVO.getProjectKeyPoints())); + respVO.setProjectProblems(normalizeNullableText(reqVO.getProjectProblems())); + respVO.setCurrentItems(mergeProjectItems(reqVO.getCurrentItems(), latestDraft.getCurrentItems())); + respVO.setNextItems(mergeProjectItems(reqVO.getNextItems(), latestDraft.getNextItems())); + respVO.setTotalWorkHours(sumProjectCurrentWorkHoursResp(respVO.getCurrentItems())); + return respVO; + } + + private Long createProjectReportInternal(ProjectReportSaveReqVO reqVO, Long overrideReporterId) { validatePeriod(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate()); validateProjectFlag(reqVO.getFlag()); - CurrentUserProfile profile = loadCurrentUserProfile(true); + CurrentUserProfile profile = resolveProfile(overrideReporterId, true); validateProjectReportDuplicate(reqVO.getProjectId(), reqVO.getPeriodKey(), profile.userId(), null); ProjectDO project = validateProjectExists(reqVO.getProjectId()); @@ -827,12 +887,22 @@ public class WorkReportCommonService { } } + private CurrentUserProfile resolveProfile(Long overrideReporterId, boolean requireManager) { + return overrideReporterId != null + ? loadUserProfile(overrideReporterId, null, requireManager) + : loadCurrentUserProfile(requireManager); + } + private CurrentUserProfile loadCurrentUserProfile(boolean requireManager) { Long userId = SecurityFrameworkUtils.getLoginUserId(); + return loadUserProfile(userId, SecurityFrameworkUtils.getLoginUserNickname(), requireManager); + } + + private CurrentUserProfile loadUserProfile(Long userId, String fallbackUserName, boolean requireManager) { AdminUserRespDTO user = loadUser(userId); String userName = StringUtils.hasText(user.getNickname()) ? user.getNickname().trim() - : defaultText(SecurityFrameworkUtils.getLoginUserNickname()); + : defaultText(fallbackUserName); String deptName = null; if (user.getDeptId() != null) { @@ -1010,6 +1080,74 @@ public class WorkReportCommonService { insertProjectNextItems(reportId, reqVO.getNextItems()); } + private List mergeReviewItems(List currentItems, + List latestItems) { + List merged = BeanUtils.toBean(defaultList(currentItems), + PersonalReportReviewItemRespVO.class); + Set existingKeys = merged.stream() + .map(item -> normalizeMergeText(item.getItemTitle())) + .filter(StringUtils::hasText) + .collect(Collectors.toCollection(LinkedHashSet::new)); + for (PersonalReportReviewItemRespVO item : defaultList(latestItems)) { + String key = normalizeMergeText(item.getItemTitle()); + if (StringUtils.hasText(key) && existingKeys.contains(key)) { + continue; + } + merged.add(BeanUtils.toBean(item, PersonalReportReviewItemRespVO.class)); + if (StringUtils.hasText(key)) { + existingKeys.add(key); + } + } + for (int i = 0; i < merged.size(); i++) { + merged.get(i).setItemNumber(i + 1); + } + return merged; + } + + private List mergePlanItems(List currentItems, + List latestItems) { + List merged = BeanUtils.toBean(defaultList(currentItems), + PersonalReportPlanItemRespVO.class); + Set existingKeys = merged.stream() + .map(item -> normalizeMergeText(item.getItemTitle())) + .filter(StringUtils::hasText) + .collect(Collectors.toCollection(LinkedHashSet::new)); + for (PersonalReportPlanItemRespVO item : defaultList(latestItems)) { + String key = normalizeMergeText(item.getItemTitle()); + if (StringUtils.hasText(key) && existingKeys.contains(key)) { + continue; + } + merged.add(BeanUtils.toBean(item, PersonalReportPlanItemRespVO.class)); + if (StringUtils.hasText(key)) { + existingKeys.add(key); + } + } + for (int i = 0; i < merged.size(); i++) { + merged.get(i).setItemNumber(i + 1); + } + return merged; + } + + private List mergeProjectItems(List currentItems, + List latestItems) { + List merged = BeanUtils.toBean(defaultList(currentItems), ProjectReportItemRespVO.class); + Set existingKeys = merged.stream() + .map(item -> buildProjectItemMergeKey(item.getItemTitle(), item.getPriorityCode())) + .filter(StringUtils::hasText) + .collect(Collectors.toCollection(LinkedHashSet::new)); + for (ProjectReportItemRespVO item : defaultList(latestItems)) { + String key = buildProjectItemMergeKey(item.getItemTitle(), item.getPriorityCode()); + if (StringUtils.hasText(key) && existingKeys.contains(key)) { + continue; + } + merged.add(BeanUtils.toBean(item, ProjectReportItemRespVO.class)); + if (StringUtils.hasText(key)) { + existingKeys.add(key); + } + } + return merged; + } + private void insertReviewItems(String reportType, Long reportId, List items) { List source = defaultList(items); for (int i = 0; i < source.size(); i++) { @@ -1326,6 +1464,16 @@ public class WorkReportCommonService { return total.compareTo(BigDecimal.ZERO) == 0 ? null : total; } + private BigDecimal sumReviewWorkHoursResp(List items) { + BigDecimal total = BigDecimal.ZERO; + for (PersonalReportReviewItemRespVO item : defaultList(items)) { + if (item.getWorkHours() != null) { + total = total.add(item.getWorkHours()); + } + } + return total.compareTo(BigDecimal.ZERO) == 0 ? null : total; + } + private BigDecimal sumProjectCurrentWorkHours(List items) { BigDecimal total = BigDecimal.ZERO; for (ProjectReportItemReqVO item : defaultList(items)) { @@ -1336,6 +1484,16 @@ public class WorkReportCommonService { return total.compareTo(BigDecimal.ZERO) == 0 ? null : total; } + private BigDecimal sumProjectCurrentWorkHoursResp(List items) { + BigDecimal total = BigDecimal.ZERO; + for (ProjectReportItemRespVO item : defaultList(items)) { + if (item.getWorkHours() != null) { + total = total.add(item.getWorkHours()); + } + } + return total.compareTo(BigDecimal.ZERO) == 0 ? null : total; + } + private BigDecimal sumTravelDays(List items) { BigDecimal total = BigDecimal.ZERO; for (WeeklyReportTravelSegmentReqVO item : defaultList(items)) { @@ -1376,6 +1534,23 @@ public class WorkReportCommonService { return value.trim(); } + private String normalizeMergeText(String value) { + return StringUtils.hasText(value) ? value.trim() : null; + } + + private String buildProjectItemMergeKey(String itemTitle, String priorityCode) { + String normalizedTitle = normalizeMergeText(itemTitle); + String normalizedPriorityCode = normalizeMergeText(priorityCode); + if (!StringUtils.hasText(normalizedTitle)) { + return null; + } + return normalizedTitle + "||" + defaultText(normalizedPriorityCode); + } + + private BigDecimal defaultIfNull(BigDecimal value) { + return value == null ? BigDecimal.ZERO : value; + } + private String defaultText(String value) { return StringUtils.hasText(value) ? value.trim() : ""; } @@ -1387,4 +1562,4 @@ public class WorkReportCommonService { private record CurrentUserProfile(Long userId, String userName, String deptName, String postName, Long directManagerId, String directManagerName) { } -} \ No newline at end of file +} 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 66c5550..cde4fe3 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 @@ -6,6 +6,7 @@ import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.Month import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportDefaultDraftReqVO; import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportExportVO; import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportPageReqVO; +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.common.vo.WorkReportStatusActionReqVO; @@ -19,6 +20,8 @@ public interface MonthlyReportService { MonthlyReportRespVO previewMonthlyDefaultDraft(MonthlyReportDefaultDraftReqVO reqVO); + MonthlyReportRespVO refreshMonthlyDraft(MonthlyReportRefreshDraftReqVO reqVO); + Long createMonthlyReport(MonthlyReportSaveReqVO reqVO); void updateMonthlyReport(Long id, MonthlyReportSaveReqVO reqVO); 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 d64168f..1a610f0 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 @@ -6,6 +6,7 @@ import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.Month import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportDefaultDraftReqVO; import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportExportVO; import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportPageReqVO; +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.common.vo.WorkReportStatusActionReqVO; @@ -35,9 +36,20 @@ public class MonthlyReportServiceImpl implements MonthlyReportService { return workReportDefaultDraftService.previewMonthlyDefaultDraft(reqVO); } + @Override + public MonthlyReportRespVO refreshMonthlyDraft(MonthlyReportRefreshDraftReqVO reqVO) { + MonthlyReportDefaultDraftReqVO draftReqVO = new MonthlyReportDefaultDraftReqVO(); + draftReqVO.setPeriodKey(reqVO.getPeriodKey()); + draftReqVO.setPeriodLabel(reqVO.getPeriodLabel()); + draftReqVO.setPeriodStartDate(reqVO.getPeriodStartDate()); + draftReqVO.setPeriodEndDate(reqVO.getPeriodEndDate()); + MonthlyReportRespVO latestDraft = workReportDefaultDraftService.previewMonthlyDefaultDraft(draftReqVO); + return workReportCommonService.mergeMonthlyDraft(latestDraft, reqVO); + } + @Override public Long createMonthlyReport(MonthlyReportSaveReqVO reqVO) { - return workReportCommonService.createMonthlyReport(reqVO); + return workReportCommonService.createMonthlyReport(reqVO, null); } @Override diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/project/ProjectReportService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/project/ProjectReportService.java index 8a2809c..bfaf4d9 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/project/ProjectReportService.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/project/ProjectReportService.java @@ -5,6 +5,7 @@ import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.Proje import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportDefaultDraftReqVO; import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportOwnerProjectOptionRespVO; import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportPageReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportRefreshDraftReqVO; import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportRespVO; import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportSaveReqVO; import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportApprovalRecordRespVO; @@ -21,6 +22,8 @@ public interface ProjectReportService { ProjectReportRespVO previewProjectDefaultDraft(Long projectId, ProjectReportDefaultDraftReqVO reqVO); + ProjectReportRespVO refreshProjectDraft(Long projectId, ProjectReportRefreshDraftReqVO reqVO); + Long createProjectReport(ProjectReportSaveReqVO reqVO); void updateProjectReport(Long id, ProjectReportSaveReqVO reqVO); diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/project/ProjectReportServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/project/ProjectReportServiceImpl.java index 0e87368..3248434 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/project/ProjectReportServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/project/ProjectReportServiceImpl.java @@ -5,6 +5,7 @@ import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.Proje import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportDefaultDraftReqVO; import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportOwnerProjectOptionRespVO; import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportPageReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportRefreshDraftReqVO; import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportRespVO; import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportSaveReqVO; import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportApprovalRecordRespVO; @@ -42,10 +43,23 @@ public class ProjectReportServiceImpl implements ProjectReportService { return workReportDefaultDraftService.previewProjectDefaultDraft(projectId, reqVO); } + @Override + public ProjectReportRespVO refreshProjectDraft(Long projectId, ProjectReportRefreshDraftReqVO reqVO) { + workReportCommonService.validateCurrentUserIsProjectReportProjectOwner(projectId); + ProjectReportDefaultDraftReqVO draftReqVO = new ProjectReportDefaultDraftReqVO(); + draftReqVO.setPeriodKey(reqVO.getPeriodKey()); + draftReqVO.setPeriodLabel(reqVO.getPeriodLabel()); + draftReqVO.setPeriodStartDate(reqVO.getPeriodStartDate()); + draftReqVO.setPeriodEndDate(reqVO.getPeriodEndDate()); + draftReqVO.setFlag(reqVO.getFlag()); + ProjectReportRespVO latestDraft = workReportDefaultDraftService.previewProjectDefaultDraft(projectId, draftReqVO); + return workReportCommonService.mergeProjectDraft(latestDraft, reqVO); + } + @Override public Long createProjectReport(ProjectReportSaveReqVO reqVO) { workReportCommonService.validateCurrentUserIsProjectReportProjectOwner(reqVO.getProjectId()); - return workReportCommonService.createProjectReport(reqVO); + return workReportCommonService.createProjectReport(reqVO, null); } @Override diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/weekly/WeeklyReportService.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/weekly/WeeklyReportService.java index da28fe6..f230d72 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/weekly/WeeklyReportService.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/weekly/WeeklyReportService.java @@ -4,6 +4,7 @@ import com.njcn.rdms.framework.common.pojo.PageResult; import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportExportVO; import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportDefaultDraftReqVO; import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportPageReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportRefreshDraftReqVO; import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportRespVO; import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportSaveReqVO; import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportApprovalRecordRespVO; @@ -18,6 +19,8 @@ public interface WeeklyReportService { WeeklyReportRespVO previewWeeklyDefaultDraft(WeeklyReportDefaultDraftReqVO reqVO); + WeeklyReportRespVO refreshWeeklyDraft(WeeklyReportRefreshDraftReqVO reqVO); + Long createWeeklyReport(WeeklyReportSaveReqVO reqVO); void updateWeeklyReport(Long id, WeeklyReportSaveReqVO reqVO); diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/weekly/WeeklyReportServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/weekly/WeeklyReportServiceImpl.java index 4e7ce87..9ca1a9d 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/weekly/WeeklyReportServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/workreport/weekly/WeeklyReportServiceImpl.java @@ -4,6 +4,7 @@ import com.njcn.rdms.framework.common.pojo.PageResult; import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportExportVO; import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportDefaultDraftReqVO; import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportPageReqVO; +import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportRefreshDraftReqVO; import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportRespVO; import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportSaveReqVO; import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportApprovalRecordRespVO; @@ -34,9 +35,20 @@ public class WeeklyReportServiceImpl implements WeeklyReportService { return workReportDefaultDraftService.previewWeeklyDefaultDraft(reqVO); } + @Override + public WeeklyReportRespVO refreshWeeklyDraft(WeeklyReportRefreshDraftReqVO reqVO) { + WeeklyReportDefaultDraftReqVO draftReqVO = new WeeklyReportDefaultDraftReqVO(); + draftReqVO.setPeriodKey(reqVO.getPeriodKey()); + draftReqVO.setPeriodLabel(reqVO.getPeriodLabel()); + draftReqVO.setPeriodStartDate(reqVO.getPeriodStartDate()); + draftReqVO.setPeriodEndDate(reqVO.getPeriodEndDate()); + WeeklyReportRespVO latestDraft = workReportDefaultDraftService.previewWeeklyDefaultDraft(draftReqVO); + return workReportCommonService.mergeWeeklyDraft(latestDraft, reqVO); + } + @Override public Long createWeeklyReport(WeeklyReportSaveReqVO reqVO) { - return workReportCommonService.createWeeklyReport(reqVO); + return workReportCommonService.createWeeklyReport(reqVO, null); } @Override diff --git a/rdms-project/rdms-project-boot/src/main/resources/application.yaml b/rdms-project/rdms-project-boot/src/main/resources/application.yaml index f37b4ea..411a064 100644 --- a/rdms-project/rdms-project-boot/src/main/resources/application.yaml +++ b/rdms-project/rdms-project-boot/src/main/resources/application.yaml @@ -106,5 +106,19 @@ rdms: email: dev@example.com license: Apache 2.0 license-url: https://www.apache.org/licenses/LICENSE-2.0.html + work-report: + auto-gen: + enabled: true + weekly: + enabled: true + cron: "0 0 12 ? * FRI" + monthly: + enabled: true + cron: "0 0 12 1-31 * ?" + project: + enabled: true + cron: "0 0 12 1-31 * ?" + scope: + dept-ids: [101] debug: false