package com.ruoyi.projectManagement.service; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.projectManagement.dto.ProjectWorkflowDtos; import lombok.RequiredArgsConstructor; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @Service @RequiredArgsConstructor public class ProjectWorkflowService { private static final TypeReference>> FILE_LIST_TYPE = new TypeReference<>() { }; private static final List PHASE_CODES = List.of("INITIATION", "DEVELOPMENT", "APPLICATION", "RESULT", "ACCEPTANCE"); private static final List PHASE_NAMES = List.of("立项", "开发", "申报", "申报结果", "验收"); private final NamedParameterJdbcTemplate jdbc; private final ObjectMapper objectMapper; public Map getWorkbench(Long projectId) { requireProject(projectId); List> modules = query(""" select m.id, m.project_id as projectId, m.module_name as moduleName, m.planned_start_date as plannedStartDate, m.summary, m.project_weight as projectWeight, m.current_progress as currentProgress, (select count(*) from project_management_progress_report r where r.module_id=m.id and r.is_deleted=0) as reportCount, m.create_time as createTime, m.update_time as updateTime from project_management_module m where m.project_id=:projectId and m.is_deleted=0 order by m.id """, Map.of("projectId", projectId)); for (Map module : modules) { Long moduleId = asLong(module.get("id")); module.put("members", query(""" select id, module_id as moduleId, user_id as userId, user_name as userName, module_share as moduleShare from project_management_module_member where module_id=:moduleId and is_deleted=0 order by id """, Map.of("moduleId", moduleId))); } List> followUps = query(""" select id, project_id as projectId, follow_date as followDate, detail, attachments, create_user_id as createUserId, create_user_name as createUserName, create_time as createTime from project_management_follow_up where project_id=:projectId and is_deleted=0 order by follow_date desc,id desc """, Map.of("projectId", projectId)); parseAttachments(followUps); List> reports = query(""" select r.id, r.project_id as projectId, r.module_id as moduleId, m.module_name as moduleName, r.reporter_ids as reporterIds, r.reporter_names as reporterNames, r.progress, r.report_date as reportDate, r.reporter_shares as reporterShares, r.remark, r.attachments, r.create_time as createTime from project_management_progress_report r left join project_management_module m on m.id=r.module_id where r.project_id=:projectId and r.is_deleted=0 order by r.report_date desc,r.id desc """, Map.of("projectId", projectId)); parseAttachments(reports); for (Map row : reports) { row.put("reporterIdList", parseLongList(row.get("reporterIds"))); row.put("reporterNameList", parseStringList(row.get("reporterNames"))); row.put("reporterShareList", parseFileList(row.get("reporterShares"))); } List> materials = query(""" select id, project_id as projectId, material_name as materialName, material_type as materialType, remark, attachments, create_user_name as createUserName, create_time as createTime from project_management_material where project_id=:projectId and is_deleted=0 order by id desc """, Map.of("projectId", projectId)); parseAttachments(materials); Map result = new LinkedHashMap<>(); result.put("modules", modules); result.put("followUps", followUps); result.put("reports", reports); result.put("materials", materials); result.put("sourceBusiness", getSourceBusiness(projectId)); result.put("memberStatistics", memberStatistics(projectId)); result.put("followWindow", getFollowWindow(projectId)); return result; } @Transactional(rollbackFor = Exception.class) public void startProject(Long projectId) { requireProject(projectId); Integer basicReady = jdbc.queryForObject(""" select count(*) from project_management_info where id=:projectId and is_delete=0 and project_classification is not null and project_classification<>'' and plan_start_time is not null and plan_end_time is not null and project_leader_id is not null and project_manager_ids is not null and json_length(project_manager_ids)>0 and follow_cycle is not null and follow_cycle<>'' and project_executor_ids is not null and json_length(project_executor_ids)>0 """, Map.of("projectId", projectId), Integer.class); if (basicReady == null || basicReady == 0) throw new ServiceException("请先完整填写项目基本情况"); List> modules = query(""" select id,module_name as moduleName,planned_start_date as plannedStartDate, summary,project_weight as projectWeight from project_management_module where project_id=:projectId and is_deleted=0 """, Map.of("projectId", projectId)); if (modules.isEmpty()) throw new ServiceException("项目开始前请至少添加一个项目模块"); BigDecimal total = BigDecimal.ZERO; for (Map module : modules) { if (isBlank(Objects.toString(module.get("moduleName"), "")) || module.get("plannedStartDate") == null || isBlank(Objects.toString(module.get("summary"), ""))) { throw new ServiceException("请完整填写每个模块的名称、预计开始时间和模块概要"); } total = total.add(nvl((BigDecimal) module.get("projectWeight"))); Long moduleId = asLong(module.get("id")); Integer memberCount = jdbc.queryForObject(""" select count(*) from project_management_module_member where module_id=:moduleId and is_deleted=0 """, Map.of("moduleId", moduleId), Integer.class); if (memberCount == null || memberCount == 0) throw new ServiceException("每个模块至少选择一名执行人"); } if (total.setScale(2, RoundingMode.HALF_UP).compareTo(BigDecimal.valueOf(100).setScale(2)) != 0) { throw new ServiceException("项目开始前,所有模块的项目工作量占比合计必须为100%"); } jdbc.update("update project_management_info set current_phase='开发',update_time=now() where id=:projectId", Map.of("projectId", projectId)); } @Transactional(rollbackFor = Exception.class) public void transitionStage(ProjectWorkflowDtos.StageTransitionRequest request) { requireProject(request.getProjectId()); String current = jdbc.queryForObject("select current_phase from project_management_info where id=:id and is_delete=0", Map.of("id", request.getProjectId()), String.class); if ("立项".equals(current)) current = "开发"; Map nextStages = Map.of( "开发", "申报", "申报", "申报结果", "申报结果", "验收", "验收", "已完成" ); String expected = nextStages.get(current); if (expected == null || !expected.equals(request.getTargetStage())) { throw new ServiceException("项目阶段只能按开发、申报、申报结果、验收、已完成的顺序推进"); } if ("开发".equals(current)) { Integer unfinished = jdbc.queryForObject(""" select count(*) from project_management_module where project_id=:projectId and is_deleted=0 and current_progress<100 """, Map.of("projectId", request.getProjectId()), Integer.class); if (unfinished != null && unfinished > 0) { throw new ServiceException("所有项目模块进度达到100%后才能进入申报阶段"); } } jdbc.update("update project_management_info set current_phase=:stage,update_time=now() where id=:projectId", new MapSqlParameterSource().addValue("stage", expected).addValue("projectId", request.getProjectId())); } @Transactional(rollbackFor = Exception.class) public void updateProjectExecutors(ProjectWorkflowDtos.ProjectExecutorsRequest request) { requireProject(request.getProjectId()); validateMembers(request.getExecutors()); List> rows = query(""" select project_executor_ids as executorIds from project_management_info where id=:projectId and is_delete=0 """, Map.of("projectId", request.getProjectId())); List oldIds = rows.isEmpty() ? List.of() : parseLongList(rows.getFirst().get("executorIds")); List newIds = request.getExecutors().stream().map(ProjectWorkflowDtos.MemberRequest::getUserId).toList(); for (Long removedId : oldIds.stream().filter(id -> !newIds.contains(id)).toList()) { if (hasPositiveProjectShare(request.getProjectId(), removedId)) { throw new ServiceException("该项目执行人已有项目汇报工作占比,不能删除"); } } jdbc.update(""" update project_management_info set project_executor_ids=:ids,project_executor_names=:names,update_time=now() where id=:projectId and is_delete=0 """, new MapSqlParameterSource().addValue("projectId", request.getProjectId()) .addValue("ids", json(newIds)) .addValue("names", json(request.getExecutors().stream().map(ProjectWorkflowDtos.MemberRequest::getUserName).toList()))); } @Transactional(rollbackFor = Exception.class) public Long savePhase(ProjectWorkflowDtos.PhaseRequest request) { requireProject(request.getProjectId()); ensurePhases(request.getProjectId()); int index = PHASE_CODES.indexOf(Objects.toString(request.getPhaseCode(), "")); if (index < 0) throw new ServiceException("项目阶段不正确"); String status = Objects.toString(request.getStatus(), "IN_PROGRESS").toUpperCase(); if (!List.of("NOT_STARTED", "IN_PROGRESS", "COMPLETED").contains(status)) { throw new ServiceException("阶段状态不正确"); } if (index > 0 && !"NOT_STARTED".equals(status)) { Integer previousDone = jdbc.queryForObject(""" select count(*) from project_management_phase where project_id=:projectId and phase_code=:phaseCode and status='COMPLETED' and is_deleted=0 """, new MapSqlParameterSource().addValue("projectId", request.getProjectId()) .addValue("phaseCode", PHASE_CODES.get(index - 1)), Integer.class); if (previousDone == null || previousDone == 0) { throw new ServiceException("请先完成上一阶段“" + PHASE_NAMES.get(index - 1) + "”"); } } MapSqlParameterSource p = new MapSqlParameterSource() .addValue("projectId", request.getProjectId()).addValue("phaseCode", request.getPhaseCode()) .addValue("status", status).addValue("formData", jsonObject(request.getFormData())) .addValue("remark", request.getRemark()).addValue("attachments", json(request.getAttachments())) .addValue("userName", currentUserName()); jdbc.update(""" update project_management_phase set status=:status,form_data=:formData,remark=:remark, attachments=:attachments,completed_time=case when :status='COMPLETED' then now() else null end, update_user_name=:userName,update_time=now() where project_id=:projectId and phase_code=:phaseCode and is_deleted=0 """, p); String currentPhase = "COMPLETED".equals(status) && index == PHASE_CODES.size() - 1 ? "已完成" : PHASE_NAMES.get(index); jdbc.update("update project_management_info set current_phase=:phase,update_time=now() where id=:projectId", new MapSqlParameterSource().addValue("phase", currentPhase).addValue("projectId", request.getProjectId())); return jdbc.queryForObject("select id from project_management_phase where project_id=:projectId and phase_code=:phaseCode and is_deleted=0", p, Long.class); } @Transactional(rollbackFor = Exception.class) public Long saveModule(ProjectWorkflowDtos.ModuleSaveRequest request) { requireProject(request.getProjectId()); if (isBlank(request.getModuleName())) { throw new ServiceException("请输入模块名称"); } if (request.getPlannedStartDate() == null) throw new ServiceException("请选择模块预计开始时间"); if (isBlank(request.getSummary())) throw new ServiceException("请输入模块概要"); BigDecimal weight = percent(request.getProjectWeight(), "模块在项目中的工作量占比"); BigDecimal otherWeight = jdbc.queryForObject(""" select coalesce(sum(project_weight),0) from project_management_module where project_id=:projectId and is_deleted=0 and (:id is null or id<>:id) """, new MapSqlParameterSource().addValue("projectId", request.getProjectId()).addValue("id", request.getId()), BigDecimal.class); BigDecimal available = BigDecimal.valueOf(100).subtract(nvl(otherWeight)); if (weight.compareTo(available) > 0) { throw new ServiceException("其他模块已占" + nvl(otherWeight).stripTrailingZeros().toPlainString() + "%,当前模块最多可设置" + available.stripTrailingZeros().toPlainString() + "%"); } validateMembers(request.getMembers()); List projectExecutorIds = projectExecutorIds(request.getProjectId()); if (request.getMembers().stream().anyMatch(member -> !projectExecutorIds.contains(member.getUserId()))) { throw new ServiceException("模块执行人必须先加入项目执行人"); } if (request.getId() != null) { Integer reportCount = jdbc.queryForObject("select count(*) from project_management_progress_report where module_id=:id and is_deleted=0", Map.of("id", request.getId()), Integer.class); if (reportCount != null && reportCount > 0) { Map old = query(""" select module_name as moduleName,planned_start_date as plannedStartDate, summary,project_weight as projectWeight from project_management_module where id=:id and project_id=:projectId and is_deleted=0 """, new MapSqlParameterSource().addValue("id", request.getId()).addValue("projectId", request.getProjectId())).stream() .findFirst().orElseThrow(() -> new ServiceException("项目模块不存在")); boolean changed = !Objects.equals(Objects.toString(old.get("moduleName"), ""), request.getModuleName().trim()) || !Objects.equals(Objects.toString(old.get("plannedStartDate"), ""), Objects.toString(request.getPlannedStartDate(), "")) || !Objects.equals(Objects.toString(old.get("summary"), ""), Objects.toString(request.getSummary(), "")) || nvl((BigDecimal) old.get("projectWeight")).compareTo(weight) != 0; if (changed) throw new ServiceException("该模块已有进度汇报,模块基础信息和占比不能修改,仅可调整执行人"); } List oldMemberIds = query(""" select user_id as userId from project_management_module_member where module_id=:moduleId and is_deleted=0 """, Map.of("moduleId", request.getId())).stream().map(row -> asLong(row.get("userId"))).toList(); List newMemberIds = request.getMembers().stream().map(ProjectWorkflowDtos.MemberRequest::getUserId).toList(); for (Long removedId : oldMemberIds.stream().filter(id -> !newMemberIds.contains(id)).toList()) { if (hasPositiveModuleShare(request.getId(), removedId)) { throw new ServiceException("该模块执行人已有本模块工作占比,不能删除"); } } } Long moduleId = request.getId(); MapSqlParameterSource params = new MapSqlParameterSource() .addValue("projectId", request.getProjectId()) .addValue("moduleName", request.getModuleName().trim()) .addValue("plannedStartDate", request.getPlannedStartDate()) .addValue("summary", request.getSummary()) .addValue("projectWeight", weight); if (moduleId == null) { GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); jdbc.update(""" insert into project_management_module (project_id,module_name,planned_start_date,summary,project_weight,current_progress,is_deleted,create_time,update_time) values (:projectId,:moduleName,:plannedStartDate,:summary,:projectWeight,0,0,now(),now()) """, params, keyHolder, new String[]{"id"}); moduleId = Objects.requireNonNull(keyHolder.getKey()).longValue(); } else { params.addValue("id", moduleId); int rows = jdbc.update(""" update project_management_module set module_name=:moduleName, planned_start_date=:plannedStartDate,summary=:summary,project_weight=:projectWeight,update_time=now() where id=:id and project_id=:projectId and is_deleted=0 """, params); if (rows == 0) throw new ServiceException("项目模块不存在"); jdbc.update("update project_management_module_member set is_deleted=1 where module_id=:id", Map.of("id", moduleId)); } for (ProjectWorkflowDtos.MemberRequest member : request.getMembers()) { jdbc.update(""" insert into project_management_module_member (module_id,user_id,user_name,module_share,is_deleted,create_time,update_time) values (:moduleId,:userId,:userName,:moduleShare,0,now(),now()) """, new MapSqlParameterSource() .addValue("moduleId", moduleId) .addValue("userId", member.getUserId()) .addValue("userName", member.getUserName()) .addValue("moduleShare", BigDecimal.ZERO)); } return moduleId; } @Transactional(rollbackFor = Exception.class) public void deleteModule(Long id) { int count = jdbc.queryForObject("select count(*) from project_management_progress_report where module_id=:id and is_deleted=0", Map.of("id", id), Integer.class); if (count > 0) throw new ServiceException("该模块已有进度汇报,不能删除"); jdbc.update("update project_management_module set is_deleted=1,update_time=now() where id=:id", Map.of("id", id)); jdbc.update("update project_management_module_member set is_deleted=1,update_time=now() where module_id=:id", Map.of("id", id)); } @Transactional(rollbackFor = Exception.class) public Long saveFollowUp(ProjectWorkflowDtos.FollowUpRequest request) { requireProject(request.getProjectId()); Long leaderId = jdbc.queryForObject("select project_leader_id from project_management_info where id=:id", Map.of("id", request.getProjectId()), Long.class); if (leaderId == null || !leaderId.equals(SecurityUtils.getUserId())) { throw new ServiceException("只有项目负责人可以填写项目跟进"); } if (request.getFollowDate() == null || isBlank(request.getDetail())) { throw new ServiceException("请填写跟进日期和跟进详情"); } validateFollowWindow(request.getProjectId(), request.getFollowDate(), request.getId()); MapSqlParameterSource p = new MapSqlParameterSource() .addValue("id", request.getId()).addValue("projectId", request.getProjectId()) .addValue("followDate", request.getFollowDate()).addValue("detail", request.getDetail()) .addValue("attachments", json(request.getAttachments())) .addValue("userId", SecurityUtils.getUserId()).addValue("userName", currentUserName()); if (request.getId() == null) { GeneratedKeyHolder keys = new GeneratedKeyHolder(); jdbc.update(""" insert into project_management_follow_up (project_id,follow_date,detail,attachments,create_user_id,create_user_name,is_deleted,create_time,update_time) values (:projectId,:followDate,:detail,:attachments,:userId,:userName,0,now(),now()) """, p, keys, new String[]{"id"}); return Objects.requireNonNull(keys.getKey()).longValue(); } jdbc.update(""" update project_management_follow_up set follow_date=:followDate,detail=:detail, attachments=:attachments,update_time=now() where id=:id and project_id=:projectId and is_deleted=0 """, p); return request.getId(); } @Transactional(rollbackFor = Exception.class) public Long saveReport(ProjectWorkflowDtos.ProgressReportRequest request) { requireProject(request.getProjectId()); if (request.getModuleId() == null || request.getReportDate() == null) { throw new ServiceException("请选择项目模块和汇报时间"); } BigDecimal progress = percent(request.getProgress(), "模块进度"); BigDecimal current = jdbc.queryForObject(""" select current_progress from project_management_module where id=:moduleId and project_id=:projectId and is_deleted=0 """, new MapSqlParameterSource().addValue("moduleId", request.getModuleId()).addValue("projectId", request.getProjectId()), BigDecimal.class); if (current == null) throw new ServiceException("项目模块不存在"); if (progress.compareTo(current) < 0) throw new ServiceException("本次进度不能低于模块当前进度"); List reporterShares = normalizeReporterShares(request); validateReporterShares(reporterShares); List moduleMemberIds = query(""" select user_id as userId from project_management_module_member where module_id=:moduleId and is_deleted=0 """, Map.of("moduleId", request.getModuleId())).stream().map(row -> asLong(row.get("userId"))).toList(); if (!moduleMemberIds.contains(SecurityUtils.getUserId())) throw new ServiceException("只有该模块执行人可以提交进度汇报"); if (reporterShares.stream().anyMatch(member -> !moduleMemberIds.contains(member.getUserId()))) { throw new ServiceException("汇报人员只能选择该模块执行人"); } request.setReporterIds(reporterShares.stream().map(ProjectWorkflowDtos.MemberRequest::getUserId).toList()); request.setReporterNames(reporterShares.stream().map(ProjectWorkflowDtos.MemberRequest::getUserName).toList()); MapSqlParameterSource p = new MapSqlParameterSource() .addValue("id", request.getId()).addValue("projectId", request.getProjectId()) .addValue("moduleId", request.getModuleId()) .addValue("reporterIds", json(request.getReporterIds())) .addValue("reporterNames", json(request.getReporterNames())) .addValue("reporterShares", json(reporterShares)) .addValue("progress", progress).addValue("reportDate", request.getReportDate()) .addValue("remark", request.getRemark()).addValue("attachments", json(request.getAttachments())); Long reportId = request.getId(); if (reportId == null) { GeneratedKeyHolder keys = new GeneratedKeyHolder(); jdbc.update(""" insert into project_management_progress_report (project_id,module_id,reporter_ids,reporter_names,reporter_shares,progress,report_date,remark,attachments,is_deleted,create_time,update_time) values (:projectId,:moduleId,:reporterIds,:reporterNames,:reporterShares,:progress,:reportDate,:remark,:attachments,0,now(),now()) """, p, keys, new String[]{"id"}); reportId = Objects.requireNonNull(keys.getKey()).longValue(); } else { jdbc.update(""" update project_management_progress_report set module_id=:moduleId,reporter_ids=:reporterIds, reporter_names=:reporterNames,reporter_shares=:reporterShares,progress=:progress,report_date=:reportDate,remark=:remark, attachments=:attachments,update_time=now() where id=:id and project_id=:projectId and is_deleted=0 """, p); } jdbc.update("update project_management_module set current_progress=:progress,update_time=now() where id=:moduleId", new MapSqlParameterSource().addValue("progress", progress).addValue("moduleId", request.getModuleId())); return reportId; } @Transactional(rollbackFor = Exception.class) public Long saveMaterial(ProjectWorkflowDtos.MaterialRequest request) { requireProject(request.getProjectId()); if (isBlank(request.getMaterialName())) throw new ServiceException("请输入资料名称"); MapSqlParameterSource p = new MapSqlParameterSource() .addValue("id", request.getId()).addValue("projectId", request.getProjectId()) .addValue("materialName", request.getMaterialName()).addValue("materialType", request.getMaterialType()) .addValue("remark", request.getRemark()).addValue("attachments", json(request.getAttachments())) .addValue("userId", SecurityUtils.getUserId()).addValue("userName", currentUserName()); if (request.getId() == null) { GeneratedKeyHolder keys = new GeneratedKeyHolder(); jdbc.update(""" insert into project_management_material (project_id,material_name,material_type,remark,attachments,create_user_id,create_user_name,is_deleted,create_time,update_time) values (:projectId,:materialName,:materialType,:remark,:attachments,:userId,:userName,0,now(),now()) """, p, keys, new String[]{"id"}); return Objects.requireNonNull(keys.getKey()).longValue(); } jdbc.update(""" update project_management_material set material_name=:materialName,material_type=:materialType, remark=:remark,attachments=:attachments,update_time=now() where id=:id and project_id=:projectId and is_deleted=0 """, p); return request.getId(); } public void softDelete(String table, Long id) { if (!List.of("project_management_follow_up", "project_management_progress_report", "project_management_material", "government_policy_file").contains(table)) { throw new ServiceException("不支持的删除类型"); } jdbc.getJdbcTemplate().update("update " + table + " set is_deleted=1,update_time=now() where id=?", id); } public List> listCategories() { return query(""" select id,parent_id as parentId,category_name as categoryName,sort,remark from project_management_category where is_deleted=0 order by sort,id """, Map.of()); } @Transactional(rollbackFor = Exception.class) public Long saveCategory(ProjectWorkflowDtos.CategoryRequest request) { if (isBlank(request.getCategoryName())) throw new ServiceException("请输入分类名称"); Long parentId = request.getParentId() == null ? 0L : request.getParentId(); if (request.getId() != null && request.getId().equals(parentId)) throw new ServiceException("上级分类不能选择自己"); MapSqlParameterSource p = new MapSqlParameterSource().addValue("id", request.getId()) .addValue("parentId", parentId).addValue("categoryName", request.getCategoryName().trim()) .addValue("sort", request.getSort() == null ? 0 : request.getSort()).addValue("remark", request.getRemark()); if (request.getId() == null) { GeneratedKeyHolder keys = new GeneratedKeyHolder(); jdbc.update(""" insert into project_management_category(parent_id,category_name,sort,remark,is_deleted,create_time,update_time) values (:parentId,:categoryName,:sort,:remark,0,now(),now()) """, p, keys, new String[]{"id"}); return Objects.requireNonNull(keys.getKey()).longValue(); } jdbc.update(""" update project_management_category set parent_id=:parentId,category_name=:categoryName, sort=:sort,remark=:remark,update_time=now() where id=:id and is_deleted=0 """, p); return request.getId(); } public void deleteCategory(Long id) { int children = jdbc.queryForObject("select count(*) from project_management_category where parent_id=:id and is_deleted=0", Map.of("id", id), Integer.class); if (children > 0) throw new ServiceException("请先删除下级分类"); jdbc.update("update project_management_category set is_deleted=1,update_time=now() where id=:id", Map.of("id", id)); } public List> listPolicies(String keyword) { MapSqlParameterSource p = new MapSqlParameterSource().addValue("keyword", keyword == null ? "" : keyword.trim()); List> rows = query(""" select id,policy_name as policyName,publish_date as publishDate,policy_url as policyUrl, attachments,maintainer_name as maintainerName,remark,create_time as createTime from government_policy_file where is_deleted=0 and (:keyword='' or policy_name like concat('%',:keyword,'%')) order by publish_date desc,id desc """, p); parseAttachments(rows); return rows; } @Transactional(rollbackFor = Exception.class) public Long savePolicy(ProjectWorkflowDtos.PolicyRequest request) { if (isBlank(request.getPolicyName())) throw new ServiceException("请输入政策名称"); String maintainer = isBlank(request.getMaintainerName()) ? currentUserName() : request.getMaintainerName(); MapSqlParameterSource p = new MapSqlParameterSource().addValue("id", request.getId()) .addValue("policyName", request.getPolicyName()).addValue("publishDate", request.getPublishDate()) .addValue("policyUrl", request.getPolicyUrl()).addValue("attachments", json(request.getAttachments())) .addValue("maintainer", maintainer).addValue("remark", request.getRemark()); if (request.getId() == null) { GeneratedKeyHolder keys = new GeneratedKeyHolder(); jdbc.update(""" insert into government_policy_file (policy_name,publish_date,policy_url,attachments,maintainer_name,remark,is_deleted,create_time,update_time) values (:policyName,:publishDate,:policyUrl,:attachments,:maintainer,:remark,0,now(),now()) """, p, keys, new String[]{"id"}); return Objects.requireNonNull(keys.getKey()).longValue(); } jdbc.update(""" update government_policy_file set policy_name=:policyName,publish_date=:publishDate, policy_url=:policyUrl,attachments=:attachments,maintainer_name=:maintainer, remark=:remark,update_time=now() where id=:id and is_deleted=0 """, p); return request.getId(); } private List> memberStatistics(Long projectId) { List> reports = query(""" select r.module_id as moduleId,r.progress,r.reporter_ids as reporterIds, r.reporter_names as reporterNames,r.reporter_shares as reporterShares, m.project_weight as projectWeight from project_management_progress_report r join project_management_module m on m.id=r.module_id and m.is_deleted=0 where r.project_id=:projectId and r.is_deleted=0 order by r.module_id,r.report_date,r.id """, Map.of("projectId", projectId)); Map previousProgress = new LinkedHashMap<>(); Map> totals = new LinkedHashMap<>(); for (Map report : reports) { Long moduleId = asLong(report.get("moduleId")); BigDecimal progress = nvl((BigDecimal) report.get("progress")); BigDecimal previous = previousProgress.getOrDefault(moduleId, BigDecimal.ZERO); BigDecimal delta = progress.subtract(previous).max(BigDecimal.ZERO); previousProgress.put(moduleId, progress.max(previous)); List> shares = parseFileList(report.get("reporterShares")); if (shares.isEmpty()) shares = equalReporterShares(report.get("reporterIds"), report.get("reporterNames")); BigDecimal moduleWeight = nvl((BigDecimal) report.get("projectWeight")); for (Map share : shares) { Long userId = asLong(share.get("userId")); if (userId == null) continue; String userName = Objects.toString(share.get("userName"), ""); BigDecimal memberShare = new BigDecimal(Objects.toString(share.get("moduleShare"), "0")); BigDecimal contribution = moduleWeight.multiply(delta).multiply(memberShare) .divide(BigDecimal.valueOf(10000), 2, RoundingMode.HALF_UP); Map total = totals.computeIfAbsent(userId, id -> { Map item = new LinkedHashMap<>(); item.put("userId", id); item.put("userName", userName); item.put("value", BigDecimal.ZERO); return item; }); total.put("value", ((BigDecimal) total.get("value")).add(contribution)); } } return totals.values().stream() .sorted((a, b) -> ((BigDecimal) b.get("value")).compareTo((BigDecimal) a.get("value"))) .toList(); } private Map getFollowWindow(Long projectId) { List> projectRows = query(""" select follow_cycle as followCycle,plan_start_time as planStartTime from project_management_info where id=:projectId and is_delete=0 """, Map.of("projectId", projectId)); if (projectRows.isEmpty()) return Map.of(); String cycle = Objects.toString(projectRows.getFirst().get("followCycle"), ""); LocalDate last = jdbc.query(""" select follow_date from project_management_follow_up where project_id=:projectId and is_deleted=0 order by follow_date desc,id desc limit 1 """, Map.of("projectId", projectId), rs -> rs.next() ? rs.getDate(1).toLocalDate() : null); Map result = new LinkedHashMap<>(); result.put("cycle", cycle); result.put("lastFollowDate", last); LocalDate planStart = projectRows.getFirst().get("planStartTime") instanceof java.sql.Date date ? date.toLocalDate() : null; LocalDate base = last == null ? planStart : last; if (base != null) { LocalDate due = nextDue(base, cycle); result.put("dueDate", due); result.put("windowStart", due.minusDays(3)); result.put("windowEnd", due.plusDays(3)); } return result; } private void ensurePhases(Long projectId) { for (int i = 0; i < PHASE_CODES.size(); i++) { jdbc.update(""" insert into project_management_phase (project_id,phase_code,phase_name,phase_order,status,is_deleted,create_time,update_time) select :projectId,:phaseCode,:phaseName,:phaseOrder,'NOT_STARTED',0,now(),now() where not exists(select 1 from project_management_phase where project_id=:projectId and phase_code=:phaseCode and is_deleted=0) """, new MapSqlParameterSource().addValue("projectId", projectId) .addValue("phaseCode", PHASE_CODES.get(i)).addValue("phaseName", PHASE_NAMES.get(i)) .addValue("phaseOrder", i + 1)); } } private Map getSourceBusiness(Long projectId) { List> rows = query(""" select sl.id,sl.project_name as projectName,sl.customer_id as customerId, sl.customer_name as customerName,sl.salesman,sl.entry_date as entryDate, sl.remarks,sl.attachment_materials as attachmentMaterials, sl.business_approval_status as approvalStatus, sl.business_approval_remark as approvalRemark from project_management_info p join sales_ledger sl on sl.id=p.source_business_id where p.id=:projectId and p.source_business_id is not null """, Map.of("projectId", projectId)); if (rows.isEmpty()) return Map.of(); Map row = rows.getFirst(); row.put("attachmentList", parseFileList(row.get("attachmentMaterials"))); return row; } private void validateFollowWindow(Long projectId, LocalDate followDate, Long excludeId) { MapSqlParameterSource p = new MapSqlParameterSource().addValue("projectId", projectId).addValue("excludeId", excludeId); LocalDate last = jdbc.query(""" select follow_date from project_management_follow_up where project_id=:projectId and is_deleted=0 and (:excludeId is null or id<>:excludeId) order by follow_date desc,id desc limit 1 """, p, rs -> rs.next() ? rs.getDate(1).toLocalDate() : null); String cycle = jdbc.queryForObject("select follow_cycle from project_management_info where id=:projectId", Map.of("projectId", projectId), String.class); LocalDate base = last; if (base == null) { base = jdbc.query("select plan_start_time from project_management_info where id=:projectId", Map.of("projectId", projectId), rs -> rs.next() && rs.getDate(1) != null ? rs.getDate(1).toLocalDate() : null); } if (base == null) throw new ServiceException("请先填写项目预计开始时间"); LocalDate due = nextDue(base, cycle); if (followDate.isBefore(due.minusDays(3)) || followDate.isAfter(due.plusDays(3))) { throw new ServiceException("本次跟进应在" + due.minusDays(3) + "至" + due.plusDays(3) + "之间填写"); } } private LocalDate nextDue(LocalDate base, String cycle) { return switch (Objects.toString(cycle, "MONTH").toUpperCase()) { case "QUARTER", "季度", "每季度" -> base.plusMonths(3); case "YEAR", "年度", "每年" -> base.plusYears(1); default -> base.plusMonths(1); }; } private void validateMembers(List members) { if (members == null || members.isEmpty()) throw new ServiceException("请至少选择一名模块执行人"); List ids = new ArrayList<>(); for (ProjectWorkflowDtos.MemberRequest member : members) { if (member.getUserId() == null || isBlank(member.getUserName())) throw new ServiceException("模块执行人不能为空"); if (ids.contains(member.getUserId())) throw new ServiceException("模块执行人不能重复"); ids.add(member.getUserId()); } } private List projectExecutorIds(Long projectId) { List> rows = query(""" select project_executor_ids as executorIds from project_management_info where id=:projectId and is_delete=0 """, Map.of("projectId", projectId)); return rows.isEmpty() ? List.of() : parseLongList(rows.getFirst().get("executorIds")); } private boolean hasPositiveProjectShare(Long projectId, Long userId) { return hasPositiveShare(query(""" select reporter_ids as reporterIds,reporter_names as reporterNames,reporter_shares as reporterShares from project_management_progress_report where project_id=:projectId and is_deleted=0 """, Map.of("projectId", projectId)), userId); } private boolean hasPositiveModuleShare(Long moduleId, Long userId) { return hasPositiveShare(query(""" select reporter_ids as reporterIds,reporter_names as reporterNames,reporter_shares as reporterShares from project_management_progress_report where module_id=:moduleId and is_deleted=0 """, Map.of("moduleId", moduleId)), userId); } private boolean hasPositiveShare(List> reports, Long userId) { for (Map report : reports) { List> shares = parseFileList(report.get("reporterShares")); if (shares.isEmpty()) shares = equalReporterShares(report.get("reporterIds"), report.get("reporterNames")); for (Map share : shares) { if (Objects.equals(asLong(share.get("userId")), userId) && new BigDecimal(Objects.toString(share.get("moduleShare"), "0")).compareTo(BigDecimal.ZERO) > 0) { return true; } } } return false; } private void validateReporterShares(List members) { if (members == null || members.isEmpty()) throw new ServiceException("请至少选择一名汇报人"); BigDecimal sum = BigDecimal.ZERO; List ids = new ArrayList<>(); for (ProjectWorkflowDtos.MemberRequest member : members) { if (member.getUserId() == null || isBlank(member.getUserName())) throw new ServiceException("汇报人不能为空"); if (ids.contains(member.getUserId())) throw new ServiceException("汇报人不能重复"); ids.add(member.getUserId()); sum = sum.add(percent(member.getModuleShare(), "本次汇报人员比重")); } if (sum.setScale(2, RoundingMode.HALF_UP).compareTo(BigDecimal.valueOf(100).setScale(2)) != 0) { throw new ServiceException("本次汇报人员比重合计必须为100%"); } } private List normalizeReporterShares(ProjectWorkflowDtos.ProgressReportRequest request) { if (request.getReporterShares() != null && !request.getReporterShares().isEmpty()) return request.getReporterShares(); List ids = request.getReporterIds() == null ? List.of() : request.getReporterIds(); List names = request.getReporterNames() == null ? List.of() : request.getReporterNames(); if (ids.isEmpty()) return new ArrayList<>(); BigDecimal share = BigDecimal.valueOf(100).divide(BigDecimal.valueOf(ids.size()), 2, RoundingMode.DOWN); List result = new ArrayList<>(); for (int i = 0; i < ids.size(); i++) { ProjectWorkflowDtos.MemberRequest member = new ProjectWorkflowDtos.MemberRequest(); member.setUserId(ids.get(i)); member.setUserName(i < names.size() ? names.get(i) : ""); member.setModuleShare(i == ids.size() - 1 ? BigDecimal.valueOf(100).subtract(share.multiply(BigDecimal.valueOf(i))) : share); result.add(member); } return result; } private List> equalReporterShares(Object rawIds, Object rawNames) { List ids = parseLongList(rawIds); List names = parseStringList(rawNames); if (ids.isEmpty()) return new ArrayList<>(); BigDecimal share = BigDecimal.valueOf(100).divide(BigDecimal.valueOf(ids.size()), 2, RoundingMode.DOWN); List> result = new ArrayList<>(); for (int i = 0; i < ids.size(); i++) { Map item = new LinkedHashMap<>(); item.put("userId", ids.get(i)); item.put("userName", i < names.size() ? names.get(i) : ""); item.put("moduleShare", i == ids.size() - 1 ? BigDecimal.valueOf(100).subtract(share.multiply(BigDecimal.valueOf(i))) : share); result.add(item); } return result; } private BigDecimal percent(BigDecimal value, String label) { BigDecimal val = nvl(value); if (val.compareTo(BigDecimal.ZERO) < 0 || val.compareTo(BigDecimal.valueOf(100)) > 0) { throw new ServiceException(label + "必须在0到100之间"); } return val.setScale(2, RoundingMode.HALF_UP); } private void requireProject(Long projectId) { if (projectId == null) throw new ServiceException("项目不能为空"); Integer count = jdbc.queryForObject("select count(*) from project_management_info where id=:id and is_delete=0", Map.of("id", projectId), Integer.class); if (count == null || count == 0) throw new ServiceException("项目不存在"); } private List> query(String sql, Map params) { return jdbc.queryForList(sql, params); } private List> query(String sql, MapSqlParameterSource params) { return jdbc.queryForList(sql, params); } private void parseAttachments(List> rows) { for (Map row : rows) row.put("attachmentList", parseFileList(row.get("attachments"))); } private List> parseFileList(Object raw) { if (raw == null || raw.toString().isBlank()) return new ArrayList<>(); try { return objectMapper.readValue(raw.toString(), FILE_LIST_TYPE); } catch (Exception ignored) { return new ArrayList<>(); } } private List parseLongList(Object raw) { if (raw == null || raw.toString().isBlank()) return new ArrayList<>(); try { return objectMapper.readValue(raw.toString(), new TypeReference<>() { }); } catch (Exception ignored) { return new ArrayList<>(); } } private List parseStringList(Object raw) { if (raw == null || raw.toString().isBlank()) return new ArrayList<>(); try { return objectMapper.readValue(raw.toString(), new TypeReference<>() { }); } catch (Exception ignored) { return new ArrayList<>(); } } private Map parseObjectMap(Object raw) { if (raw == null || raw.toString().isBlank()) return new LinkedHashMap<>(); try { return objectMapper.readValue(raw.toString(), new TypeReference<>() { }); } catch (Exception ignored) { return new LinkedHashMap<>(); } } private String json(Object value) { try { return objectMapper.writeValueAsString(value == null ? List.of() : value); } catch (Exception e) { throw new ServiceException("附件或人员数据格式不正确"); } } private String jsonObject(Object value) { try { return objectMapper.writeValueAsString(value == null ? Map.of() : value); } catch (Exception e) { throw new ServiceException("阶段信息格式不正确"); } } private String currentUserName() { return SecurityUtils.getLoginUser() == null ? "" : SecurityUtils.getLoginUser().getNickName(); } private boolean isBlank(String value) { return value == null || value.trim().isEmpty(); } private BigDecimal nvl(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } private Long asLong(Object value) { return value == null ? null : Long.valueOf(value.toString()); } }