From b30f4c40c77a940a2a749a676e901787f55040b0 Mon Sep 17 00:00:00 2001
From: zhangwencui <1064582902@qq.com>
Date: 星期五, 06 十一月 2026 11:48:25 +0800
Subject: [PATCH] 流程优化
---
src/main/java/com/ruoyi/projectManagement/service/ProjectWorkflowService.java | 763 ++++++++++++++++++++++++++++++++++++++++++++++++++++------
1 files changed, 679 insertions(+), 84 deletions(-)
diff --git a/src/main/java/com/ruoyi/projectManagement/service/ProjectWorkflowService.java b/src/main/java/com/ruoyi/projectManagement/service/ProjectWorkflowService.java
index 3241827..85b60ad 100644
--- a/src/main/java/com/ruoyi/projectManagement/service/ProjectWorkflowService.java
+++ b/src/main/java/com/ruoyi/projectManagement/service/ProjectWorkflowService.java
@@ -5,6 +5,7 @@
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.projectManagement.dto.ProjectWorkflowDtos;
+import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
@@ -16,10 +17,13 @@
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
@Service
@RequiredArgsConstructor
@@ -30,6 +34,28 @@
private final NamedParameterJdbcTemplate jdbc;
private final ObjectMapper objectMapper;
+
+ @PostConstruct
+ public void ensurePhaseHistoryTable() {
+ jdbc.getJdbcTemplate().execute("""
+ create table if not exists project_management_phase_history (
+ id bigint not null auto_increment,
+ project_id bigint not null,
+ phase_code varchar(30) not null,
+ phase_name varchar(50) not null,
+ action_type varchar(30) not null,
+ action_label varchar(100) not null,
+ form_data json default null,
+ remark varchar(2000) default null,
+ attachments json default null,
+ operator_name varchar(100) default null,
+ action_time datetime not null default current_timestamp,
+ primary key (id),
+ key idx_phase_history_project (project_id,action_time),
+ key idx_phase_history_phase (project_id,phase_code)
+ ) engine=InnoDB default charset=utf8mb4 comment='椤圭洰闃舵鍔炵悊鍘嗗彶'
+ """);
+ }
public Map<String, Object> getWorkbench(Long projectId) {
requireProject(projectId);
@@ -68,14 +94,23 @@
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
+ where r.project_id=:projectId and r.is_deleted=0 order by r.module_id,r.report_date,r.id
""", Map.of("projectId", projectId));
parseAttachments(reports);
+ Map<Long, BigDecimal> previousProgress = new LinkedHashMap<>();
for (Map<String, Object> row : reports) {
row.put("reporterIdList", parseLongList(row.get("reporterIds")));
row.put("reporterNameList", parseStringList(row.get("reporterNames")));
row.put("reporterShareList", parseFileList(row.get("reporterShares")));
+ Long moduleId = asLong(row.get("moduleId"));
+ BigDecimal previous = previousProgress.getOrDefault(moduleId, BigDecimal.ZERO);
+ BigDecimal cumulative = nvl((BigDecimal) row.get("progress"));
+ row.put("previousProgress", previous);
+ row.put("incrementProgress", cumulative.subtract(previous).max(BigDecimal.ZERO));
+ previousProgress.put(moduleId, cumulative.max(previous));
}
+ reports.sort(Comparator.comparing((Map<String, Object> row) -> Objects.toString(row.get("reportDate"), ""))
+ .thenComparing(row -> asLong(row.get("id")), Comparator.nullsLast(Long::compareTo)).reversed());
List<Map<String, Object>> materials = query("""
select id, project_id as projectId, material_name as materialName,
@@ -86,11 +121,31 @@
""", Map.of("projectId", projectId));
parseAttachments(materials);
+ List<Map<String, Object>> phases = query("""
+ select id,phase_code as phaseCode,phase_name as phaseName,status,form_data as formData,
+ remark,attachments,completed_time as completedTime,update_user_name as operatorName,
+ update_time as actionTime
+ from project_management_phase where project_id=:projectId and is_deleted=0 order by phase_order
+ """, Map.of("projectId", projectId));
+ parseAttachments(phases);
+ for (Map<String, Object> phase : phases) phase.put("formData", parseObjectMap(phase.get("formData")));
+
+ List<Map<String, Object>> phaseHistories = query("""
+ select id,phase_code as phaseCode,phase_name as phaseName,action_type as actionType,
+ action_label as actionLabel,form_data as formData,remark,attachments,
+ operator_name as operatorName,action_time as actionTime
+ from project_management_phase_history where project_id=:projectId order by action_time desc,id desc
+ """, Map.of("projectId", projectId));
+ parseAttachments(phaseHistories);
+ for (Map<String, Object> history : phaseHistories) history.put("formData", parseObjectMap(history.get("formData")));
+
Map<String, Object> result = new LinkedHashMap<>();
result.put("modules", modules);
result.put("followUps", followUps);
result.put("reports", reports);
result.put("materials", materials);
+ result.put("phases", phases);
+ result.put("phaseHistories", phaseHistories);
result.put("sourceBusiness", getSourceBusiness(projectId));
result.put("memberStatistics", memberStatistics(projectId));
result.put("followWindow", getFollowWindow(projectId));
@@ -100,13 +155,20 @@
@Transactional(rollbackFor = Exception.class)
public void startProject(Long projectId) {
requireProject(projectId);
+ String currentPhase = jdbc.queryForObject("""
+ select current_phase from project_management_info
+ where id=:projectId and is_delete=0 for update
+ """, Map.of("projectId", projectId), String.class);
+ if (!"鏈紑濮�".equals(currentPhase)) {
+ throw new ServiceException("鍙湁鏈紑濮嬬殑椤圭洰鍙互寮�濮嬶紝褰撳墠闃舵涓衡��" + currentPhase + "鈥�");
+ }
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
+ where id=:projectId and is_delete=0 and project_category_id is not null
+ and plan_start_time is not null and plan_end_time is not null
+ and plan_end_time>=plan_start_time
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<Map<String, Object>> modules = query("""
@@ -115,6 +177,15 @@
from project_management_module where project_id=:projectId and is_deleted=0
""", Map.of("projectId", projectId));
if (modules.isEmpty()) throw new ServiceException("椤圭洰寮�濮嬪墠璇疯嚦灏戞坊鍔犱竴涓」鐩ā鍧�");
+ Integer invalidModuleDates = jdbc.queryForObject("""
+ select count(*) from project_management_module m
+ join project_management_info p on p.id=m.project_id and p.is_delete=0
+ where m.project_id=:projectId and m.is_deleted=0
+ and (m.planned_start_date<p.plan_start_time or m.planned_start_date>p.plan_end_time)
+ """, Map.of("projectId", projectId), Integer.class);
+ if (invalidModuleDates != null && invalidModuleDates > 0) {
+ throw new ServiceException("妯″潡棰勮寮�濮嬫椂闂村繀椤诲湪椤圭洰璁″垝寮�濮嬪拰缁撴潫鏃ユ湡涔嬮棿");
+ }
BigDecimal total = BigDecimal.ZERO;
for (Map<String, Object> module : modules) {
if (isBlank(Objects.toString(module.get("moduleName"), "")) || module.get("plannedStartDate") == null
@@ -132,15 +203,30 @@
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));
+ jdbc.update("""
+ update project_management_info set current_phase='寮�鍙�',
+ actual_start_time=coalesce(actual_start_time,current_date),update_time=now()
+ where id=:projectId
+ """, Map.of("projectId", projectId));
+ ensurePhases(projectId);
+ jdbc.update("""
+ update project_management_phase set status='COMPLETED',completed_time=coalesce(completed_time,now()),
+ update_user_name=:userName,update_time=now()
+ where project_id=:projectId and phase_code='INITIATION' and is_deleted=0
+ """, new MapSqlParameterSource().addValue("projectId", projectId).addValue("userName", currentUserName()));
+ jdbc.update("""
+ update project_management_phase set status='IN_PROGRESS',completed_time=null,
+ update_user_name=:userName,update_time=now()
+ where project_id=:projectId and phase_code='DEVELOPMENT' and is_deleted=0
+ """, new MapSqlParameterSource().addValue("projectId", projectId).addValue("userName", currentUserName()));
}
@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",
+ ensurePhases(request.getProjectId());
+ String current = jdbc.queryForObject("select current_phase from project_management_info where id=:id and is_delete=0 for update",
Map.of("id", request.getProjectId()), String.class);
- if ("绔嬮」".equals(current)) current = "寮�鍙�";
Map<String, String> nextStages = Map.of(
"寮�鍙�", "鐢虫姤",
"鐢虫姤", "鐢虫姤缁撴灉",
@@ -151,6 +237,7 @@
if (expected == null || !expected.equals(request.getTargetStage())) {
throw new ServiceException("椤圭洰闃舵鍙兘鎸夊紑鍙戙�佺敵鎶ャ�佺敵鎶ョ粨鏋溿�侀獙鏀躲�佸凡瀹屾垚鐨勯『搴忔帹杩�");
}
+ validateTransitionForm(current, request.getFormData());
if ("寮�鍙�".equals(current)) {
Integer unfinished = jdbc.queryForObject("""
select count(*) from project_management_module
@@ -160,31 +247,50 @@
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<Map<String, Object>> 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<Long> oldIds = rows.isEmpty() ? List.of() : parseLongList(rows.getFirst().get("executorIds"));
- List<Long> 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("璇ラ」鐩墽琛屼汉宸叉湁椤圭洰姹囨姤宸ヤ綔鍗犳瘮锛屼笉鑳藉垹闄�");
- }
- }
+ Map<String, String> phaseCodes = Map.of(
+ "寮�鍙�", "DEVELOPMENT",
+ "鐢虫姤", "APPLICATION",
+ "鐢虫姤缁撴灉", "RESULT",
+ "楠屾敹", "ACCEPTANCE"
+ );
+ MapSqlParameterSource phaseParams = new MapSqlParameterSource()
+ .addValue("projectId", request.getProjectId())
+ .addValue("userName", currentUserName())
+ .addValue("formData", jsonObject(request.getFormData()))
+ .addValue("remark", request.getRemark())
+ .addValue("attachments", json(request.getAttachments()));
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())));
+ update project_management_phase set status='COMPLETED',completed_time=coalesce(completed_time,now()),
+ form_data=case when :saveCurrent=1 then :formData else form_data end,
+ remark=case when :saveCurrent=1 then :remark else remark end,
+ attachments=case when :saveCurrent=1 then :attachments else attachments end,
+ update_user_name=:userName,update_time=now()
+ where project_id=:projectId and phase_code=:phaseCode and is_deleted=0
+ """, phaseParams.addValue("phaseCode", phaseCodes.get(current)).addValue("saveCurrent", "宸插畬鎴�".equals(expected) ? 1 : 0));
+ if (!"宸插畬鎴�".equals(expected)) {
+ jdbc.update("""
+ update project_management_phase set status='IN_PROGRESS',completed_time=null,
+ form_data=:formData,remark=:remark,attachments=:attachments,
+ update_user_name=:userName,update_time=now()
+ where project_id=:projectId and phase_code=:phaseCode and is_deleted=0
+ """, new MapSqlParameterSource()
+ .addValue("projectId", request.getProjectId())
+ .addValue("userName", currentUserName())
+ .addValue("formData", jsonObject(request.getFormData()))
+ .addValue("remark", request.getRemark())
+ .addValue("attachments", json(request.getAttachments()))
+ .addValue("phaseCode", phaseCodes.get(expected)));
+ }
+ String historyPhase = "宸插畬鎴�".equals(expected) ? current : expected;
+ appendPhaseHistory(request.getProjectId(), phaseCodes.get(historyPhase), historyPhase,
+ stageHistoryAction(historyPhase, request.getFormData(), "宸插畬鎴�".equals(expected)),
+ stageHistoryLabel(historyPhase, request.getFormData(), "宸插畬鎴�".equals(expected)),
+ request.getFormData(), request.getRemark(), request.getAttachments());
+ jdbc.update("""
+ update project_management_info set current_phase=:stage,
+ actual_end_time=case when :stage='宸插畬鎴�' then coalesce(actual_end_time,current_date) else actual_end_time end,
+ update_time=now() where id=:projectId
+ """, new MapSqlParameterSource().addValue("stage", expected).addValue("projectId", request.getProjectId()));
}
@Transactional(rollbackFor = Exception.class)
@@ -194,19 +300,16 @@
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("闃舵鐘舵�佷笉姝g‘");
+ if (!"IN_PROGRESS".equals(status)) throw new ServiceException("闃舵琛ュ厖鍔炵悊鍙兘淇濇寔杩涜涓姸鎬�");
+ String currentPhase = jdbc.queryForObject("""
+ select current_phase from project_management_info
+ where id=:id and is_delete=0 for update
+ """, Map.of("id", request.getProjectId()), String.class);
+ String expectedCode = phaseCodeForName(currentPhase);
+ if (!Objects.equals(expectedCode, request.getPhaseCode())) {
+ throw new ServiceException("鍙兘缁存姢褰撳墠闃舵鈥�" + currentPhase + "鈥濓紝涓嶈兘璺宠浆鎴栧�掗��闃舵");
}
- 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) + "鈥�");
- }
- }
+ validateSupplementalPhase(currentPhase, request.getFormData());
MapSqlParameterSource p = new MapSqlParameterSource()
.addValue("projectId", request.getProjectId()).addValue("phaseCode", request.getPhaseCode())
.addValue("status", status).addValue("formData", jsonObject(request.getFormData()))
@@ -218,17 +321,67 @@
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()));
+ appendPhaseHistory(request.getProjectId(), request.getPhaseCode(), PHASE_NAMES.get(index),
+ stageHistoryAction(PHASE_NAMES.get(index), request.getFormData(), false),
+ stageHistoryLabel(PHASE_NAMES.get(index), request.getFormData(), false),
+ request.getFormData(), request.getRemark(), request.getAttachments());
return jdbc.queryForObject("select id from project_management_phase where project_id=:projectId and phase_code=:phaseCode and is_deleted=0",
p, Long.class);
+ }
+
+ public Map<String, Object> getLifecycle(Long projectId) {
+ Map<String, Object> project = query("""
+ select id,title,source,current_phase as currentPhase,establish_time as establishTime,
+ plan_start_time as planStartTime,plan_end_time as planEndTime,
+ actual_start_time as actualStartTime,actual_end_time as actualEndTime,
+ create_time as createTime,update_time as updateTime
+ from project_management_info where id=:projectId and is_delete=0
+ """, Map.of("projectId", projectId)).stream()
+ .findFirst().orElseThrow(() -> new ServiceException("椤圭洰涓嶅瓨鍦�"));
+ List<Map<String, Object>> phases = query("""
+ select phase_code as phaseCode,phase_name as phaseName,status,completed_time as completedTime,
+ update_time as updateTime,update_user_name as updateUserName,remark
+ from project_management_phase
+ where project_id=:projectId and is_deleted=0 order by phase_order
+ """, Map.of("projectId", projectId));
+ Map<String, Map<String, Object>> phaseMap = new LinkedHashMap<>();
+ for (Map<String, Object> phase : phases) phaseMap.put(Objects.toString(phase.get("phaseCode"), ""), phase);
+ List<Map<String, Object>> phaseHistories = query("""
+ select id,phase_code as phaseCode,phase_name as phaseName,action_type as actionType,
+ action_label as actionLabel,remark,operator_name as operatorName,action_time as actionTime
+ from project_management_phase_history where project_id=:projectId order by action_time,id
+ """, Map.of("projectId", projectId));
+
+ List<Map<String, Object>> nodes = new ArrayList<>();
+ Object establishTime = project.get("establishTime") == null ? project.get("createTime") : project.get("establishTime");
+ nodes.add(lifecycleNode("INITIATION", "椤圭洰绔嬮」", establishTime, "COMPLETED",
+ "椤圭洰宸插垱寤猴紝鏉ユ簮锛�" + Objects.toString(project.get("source"), "鎵嬪姩鍒涘缓")));
+ nodes.add(lifecycleNode("PROJECT_START", "椤圭洰寮�濮�", project.get("actualStartTime"),
+ project.get("actualStartTime") == null ? currentNodeStatus(project.get("currentPhase"), "鏈紑濮�") : "COMPLETED",
+ "璁″垝寮�濮嬶細" + Objects.toString(project.get("planStartTime"), "鏈~鍐�")));
+
+ addPhaseLifecycleNode(nodes, phaseMap.get("DEVELOPMENT"), "DEVELOPMENT", "椤圭洰寮�鍙�", project.get("currentPhase"), project.get("updateTime"));
+ addPhaseHistoryNodes(nodes, phaseHistories, "DEVELOPMENT");
+ addPhaseLifecycleNode(nodes, phaseMap.get("APPLICATION"), "APPLICATION", "椤圭洰鐢虫姤", project.get("currentPhase"), project.get("updateTime"));
+ addPhaseHistoryNodes(nodes, phaseHistories, "APPLICATION");
+ addPhaseLifecycleNode(nodes, phaseMap.get("RESULT"), "RESULT", "鐢虫姤缁撴灉", project.get("currentPhase"), project.get("updateTime"));
+ addPhaseHistoryNodes(nodes, phaseHistories, "RESULT");
+ addPhaseLifecycleNode(nodes, phaseMap.get("ACCEPTANCE"), "ACCEPTANCE", "椤圭洰楠屾敹", project.get("currentPhase"), project.get("updateTime"));
+ addPhaseHistoryNodes(nodes, phaseHistories, "ACCEPTANCE");
+ nodes.add(lifecycleNode("PROJECT_END", "椤圭洰缁撴潫", project.get("actualEndTime"),
+ project.get("actualEndTime") == null ? ("宸插畬鎴�".equals(project.get("currentPhase")) ? "CURRENT" : "PENDING") : "COMPLETED",
+ "璁″垝缁撴潫锛�" + Objects.toString(project.get("planEndTime"), "鏈~鍐�")));
+
+ Map<String, Object> result = new LinkedHashMap<>();
+ result.put("project", project);
+ result.put("nodes", nodes);
+ return result;
}
@Transactional(rollbackFor = Exception.class)
public Long saveModule(ProjectWorkflowDtos.ModuleSaveRequest request) {
requireProject(request.getProjectId());
+ ensureProjectMaintenanceEditable(request.getProjectId());
if (isBlank(request.getModuleName())) {
throw new ServiceException("璇疯緭鍏ユā鍧楀悕绉�");
}
@@ -245,15 +398,26 @@
+ "%锛屽綋鍓嶆ā鍧楁渶澶氬彲璁剧疆" + available.stripTrailingZeros().toPlainString() + "%");
}
validateMembers(request.getMembers());
- List<Long> projectExecutorIds = projectExecutorIds(request.getProjectId());
- if (request.getMembers().stream().anyMatch(member -> !projectExecutorIds.contains(member.getUserId()))) {
- throw new ServiceException("妯″潡鎵ц浜哄繀椤诲厛鍔犲叆椤圭洰鎵ц浜�");
- }
+ boolean started = isProjectStarted(request.getProjectId());
+ if (!started) {
+ Integer dateInProjectRange = jdbc.queryForObject("""
+ select count(*) from project_management_info
+ where id=:projectId and is_delete=0 and plan_start_time is not null and plan_end_time is not null
+ and :plannedStartDate between plan_start_time and plan_end_time
+ """, new MapSqlParameterSource().addValue("projectId", request.getProjectId())
+ .addValue("plannedStartDate", request.getPlannedStartDate()), Integer.class);
+ if (dateInProjectRange == null || dateInProjectRange == 0) {
+ throw new ServiceException("妯″潡棰勮寮�濮嬫椂闂村繀椤诲湪椤圭洰璁″垝寮�濮嬪拰缁撴潫鏃ユ湡涔嬮棿");
+ }
+ }
+ if (started && request.getId() == null) {
+ 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) {
+ if (started || (reportCount != null && reportCount > 0)) {
Map<String, Object> old = query("""
select module_name as moduleName,planned_start_date as plannedStartDate,
summary,project_weight as projectWeight from project_management_module
@@ -264,7 +428,9 @@
|| !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("璇ユā鍧楀凡鏈夎繘搴︽眹鎶ワ紝妯″潡鍩虹淇℃伅鍜屽崰姣斾笉鑳戒慨鏀癸紝浠呭彲璋冩暣鎵ц浜�");
+ if (changed) throw new ServiceException(started
+ ? "椤圭洰寮�濮嬪悗妯″潡淇℃伅涓嶈兘淇敼锛屼粎鍙鐞嗘ā鍧楁墽琛屼汉"
+ : "璇ユā鍧楀凡鏈夎繘搴︽眹鎶ワ紝妯″潡鍩虹淇℃伅鍜屽崰姣斾笉鑳戒慨鏀癸紝浠呭彲璋冩暣妯″潡鎵ц浜�");
}
List<Long> oldMemberIds = query("""
select user_id as userId from project_management_module_member
@@ -318,7 +484,55 @@
}
@Transactional(rollbackFor = Exception.class)
+ public void saveModulesBatch(ProjectWorkflowDtos.ModulesBatchRequest request) {
+ requireProject(request.getProjectId());
+ ensureProjectMaintenanceEditable(request.getProjectId());
+ List<ProjectWorkflowDtos.ModuleSaveRequest> modules = request.getModules() == null
+ ? new ArrayList<>() : new ArrayList<>(request.getModules());
+ if (modules.isEmpty()) throw new ServiceException("璇疯嚦灏戞坊鍔犱竴涓」鐩ā鍧�");
+
+ BigDecimal total = BigDecimal.ZERO;
+ Set<Long> requestedIds = new HashSet<>();
+ for (ProjectWorkflowDtos.ModuleSaveRequest module : modules) {
+ module.setProjectId(request.getProjectId());
+ total = total.add(percent(module.getProjectWeight(), "妯″潡鍦ㄩ」鐩腑鐨勫伐浣滈噺鍗犳瘮"));
+ if (module.getId() != null && !requestedIds.add(module.getId())) {
+ throw new ServiceException("妯″潡鏁版嵁閲嶅锛岃鍒锋柊鍚庨噸璇�");
+ }
+ }
+ if (total.compareTo(BigDecimal.valueOf(100)) != 0) {
+ throw new ServiceException("鎵�鏈夋ā鍧楃殑椤圭洰宸ヤ綔閲忓崰姣斿悎璁″繀椤讳负100%");
+ }
+
+ List<Map<String, Object>> existing = query("""
+ select id,project_weight as projectWeight from project_management_module
+ where project_id=:projectId and is_deleted=0
+ """, Map.of("projectId", request.getProjectId()));
+ Map<Long, BigDecimal> oldWeights = new LinkedHashMap<>();
+ for (Map<String, Object> row : existing) oldWeights.put(asLong(row.get("id")), nvl((BigDecimal) row.get("projectWeight")));
+ for (Long id : requestedIds) {
+ if (!oldWeights.containsKey(id)) throw new ServiceException("椤圭洰妯″潡涓嶅瓨鍦ㄦ垨涓嶅睘浜庡綋鍓嶉」鐩�");
+ }
+ if (isProjectStarted(request.getProjectId()) && (!requestedIds.equals(oldWeights.keySet()) || modules.size() != existing.size())) {
+ throw new ServiceException("椤圭洰寮�濮嬪悗涓嶈兘鏂板鎴栧垹闄ゆā鍧楋紝浠呭彲绠$悊妯″潡鎵ц浜�");
+ }
+ for (Long oldId : oldWeights.keySet()) {
+ if (!requestedIds.contains(oldId)) deleteModule(oldId);
+ }
+
+ // 鍏堜繚瀛樺崰姣斿噺灏戠殑妯″潡锛岀粰鍚庣画鏂板鎴栧鍔犲崰姣旂殑妯″潡閲婃斁绌洪棿銆�
+ modules.sort(Comparator.comparing(module -> nvl(module.getProjectWeight())
+ .subtract(oldWeights.getOrDefault(module.getId(), BigDecimal.ZERO))));
+ for (ProjectWorkflowDtos.ModuleSaveRequest module : modules) saveModule(module);
+ }
+
+ @Transactional(rollbackFor = Exception.class)
public void deleteModule(Long id) {
+ Long projectId = jdbc.query("select project_id from project_management_module where id=:id and is_deleted=0",
+ Map.of("id", id), rs -> rs.next() ? rs.getLong(1) : null);
+ if (projectId == null) throw new ServiceException("椤圭洰妯″潡涓嶅瓨鍦�");
+ ensureProjectMaintenanceEditable(projectId);
+ if (isProjectStarted(projectId)) throw new ServiceException("椤圭洰寮�濮嬪悗涓嶈兘鍒犻櫎妯″潡");
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));
@@ -328,15 +542,24 @@
@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("鍙湁椤圭洰璐熻矗浜哄彲浠ュ~鍐欓」鐩窡杩�");
- }
+ ensureProjectMaintenanceEditable(request.getProjectId());
+ ensureProjectLeader(request.getProjectId());
if (request.getFollowDate() == null || isBlank(request.getDetail())) {
throw new ServiceException("璇峰~鍐欒窡杩涙棩鏈熷拰璺熻繘璇︽儏");
}
- validateFollowWindow(request.getProjectId(), request.getFollowDate(), request.getId());
+ if (request.getFollowDate().isAfter(LocalDate.now())) throw new ServiceException("璺熻繘鏃ユ湡涓嶈兘鏅氫簬浠婂ぉ");
+ if (request.getId() == null) {
+ validateFollowWindow(request.getProjectId(), request.getFollowDate(), null);
+ } else {
+ LocalDate oldDate = jdbc.query("""
+ select follow_date from project_management_follow_up
+ where id=:id and project_id=:projectId and is_deleted=0
+ """, new MapSqlParameterSource().addValue("id", request.getId())
+ .addValue("projectId", request.getProjectId()),
+ rs -> rs.next() ? rs.getDate(1).toLocalDate() : null);
+ if (oldDate == null) throw new ServiceException("璺熻繘璁板綍涓嶅瓨鍦�");
+ if (!oldDate.equals(request.getFollowDate())) throw new ServiceException("宸叉湁璺熻繘璁板綍涓嶈兘淇敼璺熻繘鏃ユ湡");
+ }
MapSqlParameterSource p = new MapSqlParameterSource()
.addValue("id", request.getId()).addValue("projectId", request.getProjectId())
.addValue("followDate", request.getFollowDate()).addValue("detail", request.getDetail())
@@ -351,26 +574,43 @@
""", p, keys, new String[]{"id"});
return Objects.requireNonNull(keys.getKey()).longValue();
}
- jdbc.update("""
+ int updated = 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);
+ if (updated == 0) throw new ServiceException("璺熻繘璁板綍涓嶅瓨鍦�");
return request.getId();
+ }
+
+ @Transactional(rollbackFor = Exception.class)
+ public void deleteFollowUp(Long id) {
+ Long projectId = jdbc.query("""
+ select project_id from project_management_follow_up where id=:id and is_deleted=0
+ """, Map.of("id", id), rs -> rs.next() ? rs.getLong(1) : null);
+ if (projectId == null) throw new ServiceException("璺熻繘璁板綍涓嶅瓨鍦�");
+ ensureProjectMaintenanceEditable(projectId);
+ ensureProjectLeader(projectId);
+ jdbc.update("update project_management_follow_up set is_deleted=1,update_time=now() where id=:id", Map.of("id", id));
}
@Transactional(rollbackFor = Exception.class)
public Long saveReport(ProjectWorkflowDtos.ProgressReportRequest request) {
requireProject(request.getProjectId());
+ ensureProjectMaintenanceEditable(request.getProjectId());
if (request.getModuleId() == null || request.getReportDate() == null) {
throw new ServiceException("璇烽�夋嫨椤圭洰妯″潡鍜屾眹鎶ユ椂闂�");
}
- BigDecimal progress = percent(request.getProgress(), "妯″潡杩涘害");
+ if (request.getReportDate().isAfter(LocalDate.now())) {
+ throw new ServiceException("姹囨姤鏃堕棿涓嶈兘鏅氫簬浠婂ぉ");
+ }
+ BigDecimal increment = percent(request.getIncrementProgress(), "鏈杩涘害");
+ if (increment.compareTo(BigDecimal.ZERO) <= 0) throw new ServiceException("鏈杩涘害蹇呴』澶т簬0");
BigDecimal current = jdbc.queryForObject("""
select current_progress from project_management_module
where id=:moduleId and project_id=:projectId and is_deleted=0
+ for update
""", 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<ProjectWorkflowDtos.MemberRequest> reporterShares = normalizeReporterShares(request);
validateReporterShares(reporterShares);
List<Long> moduleMemberIds = query("""
@@ -381,8 +621,45 @@
if (reporterShares.stream().anyMatch(member -> !moduleMemberIds.contains(member.getUserId()))) {
throw new ServiceException("姹囨姤浜哄憳鍙兘閫夋嫨璇ユā鍧楁墽琛屼汉");
}
+ if (reporterShares.stream().noneMatch(member -> Objects.equals(member.getUserId(), SecurityUtils.getUserId()))) {
+ throw new ServiceException("褰撳墠鍔炵悊浜哄繀椤诲寘鍚湪鏈姹囨姤浜哄憳涓�");
+ }
request.setReporterIds(reporterShares.stream().map(ProjectWorkflowDtos.MemberRequest::getUserId).toList());
request.setReporterNames(reporterShares.stream().map(ProjectWorkflowDtos.MemberRequest::getUserName).toList());
+ Long reportId = request.getId();
+ BigDecimal progress;
+ BigDecimal progressAdjustment = BigDecimal.ZERO;
+ if (reportId == null) {
+ BigDecimal finalProgress = current.add(increment);
+ if (finalProgress.compareTo(BigDecimal.valueOf(100)) > 0) {
+ throw new ServiceException("妯″潡褰撳墠杩涘害涓�" + current.stripTrailingZeros().toPlainString()
+ + "% 锛屾湰娆¤繘搴︽渶澶氬彲濉啓" + BigDecimal.valueOf(100).subtract(current).stripTrailingZeros().toPlainString() + "%");
+ }
+ progress = previousReportProgress(request.getModuleId(), request.getReportDate(), Long.MAX_VALUE).add(increment);
+ } else {
+ Map<String, Object> oldReport = query("""
+ select id,module_id as moduleId,report_date as reportDate,progress,reporter_ids as reporterIds
+ from project_management_progress_report
+ where id=:id and project_id=:projectId and is_deleted=0
+ """, new MapSqlParameterSource().addValue("id", reportId).addValue("projectId", request.getProjectId()))
+ .stream().findFirst().orElseThrow(() -> new ServiceException("杩涘害姹囨姤涓嶅瓨鍦�"));
+ if (!Objects.equals(asLong(oldReport.get("moduleId")), request.getModuleId())) {
+ throw new ServiceException("宸叉湁杩涘害姹囨姤涓嶈兘鏇存崲椤圭洰妯″潡");
+ }
+ if (!parseLongList(oldReport.get("reporterIds")).contains(SecurityUtils.getUserId())) {
+ throw new ServiceException("鍙湁鍘熸眹鎶ュ姙鐞嗕汉鍙互缂栬緫璇ヨ繘搴︽眹鎶�");
+ }
+ LocalDate oldDate = oldReport.get("reportDate") instanceof java.sql.Date date ? date.toLocalDate() : request.getReportDate();
+ if (!Objects.equals(oldDate, request.getReportDate())) throw new ServiceException("宸叉湁杩涘害姹囨姤涓嶈兘淇敼姹囨姤鏃堕棿");
+ BigDecimal previous = previousReportProgress(request.getModuleId(), request.getReportDate(), reportId);
+ BigDecimal oldProgress = nvl((BigDecimal) oldReport.get("progress"));
+ progress = previous.add(increment);
+ progressAdjustment = progress.subtract(oldProgress);
+ BigDecimal adjustedCurrent = current.add(progressAdjustment);
+ if (adjustedCurrent.compareTo(BigDecimal.ZERO) < 0 || adjustedCurrent.compareTo(BigDecimal.valueOf(100)) > 0) {
+ throw new ServiceException("淇敼鍚庢ā鍧楄繘搴﹀繀椤诲湪0%鑷�100%涔嬮棿");
+ }
+ }
MapSqlParameterSource p = new MapSqlParameterSource()
.addValue("id", request.getId()).addValue("projectId", request.getProjectId())
.addValue("moduleId", request.getModuleId())
@@ -391,7 +668,6 @@
.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("""
@@ -400,6 +676,11 @@
values (:projectId,:moduleId,:reporterIds,:reporterNames,:reporterShares,:progress,:reportDate,:remark,:attachments,0,now(),now())
""", p, keys, new String[]{"id"});
reportId = Objects.requireNonNull(keys.getKey()).longValue();
+ jdbc.update("""
+ update project_management_progress_report set progress=progress+:increment,update_time=now()
+ where module_id=:moduleId and is_deleted=0 and report_date>:reportDate
+ """, new MapSqlParameterSource().addValue("increment", increment)
+ .addValue("moduleId", request.getModuleId()).addValue("reportDate", request.getReportDate()));
} else {
jdbc.update("""
update project_management_progress_report set module_id=:moduleId,reporter_ids=:reporterIds,
@@ -407,16 +688,58 @@
attachments=:attachments,update_time=now()
where id=:id and project_id=:projectId and is_deleted=0
""", p);
+ if (progressAdjustment.compareTo(BigDecimal.ZERO) != 0) {
+ jdbc.update("""
+ update project_management_progress_report set progress=progress+:adjustment,update_time=now()
+ where module_id=:moduleId and is_deleted=0 and
+ (report_date>:reportDate or (report_date=:reportDate and id>:id))
+ """, new MapSqlParameterSource().addValue("adjustment", progressAdjustment)
+ .addValue("moduleId", request.getModuleId()).addValue("reportDate", request.getReportDate()).addValue("id", reportId));
+ }
}
- 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()));
+ refreshModuleProgress(request.getModuleId());
return reportId;
+ }
+
+ @Transactional(rollbackFor = Exception.class)
+ public void deleteReport(Long id) {
+ Map<String, Object> report = query("""
+ select id,project_id as projectId,module_id as moduleId,report_date as reportDate,progress,
+ reporter_ids as reporterIds
+ from project_management_progress_report where id=:id and is_deleted=0
+ """, Map.of("id", id)).stream().findFirst().orElseThrow(() -> new ServiceException("杩涘害姹囨姤涓嶅瓨鍦�"));
+ ensureProjectMaintenanceEditable(asLong(report.get("projectId")));
+ Long moduleId = asLong(report.get("moduleId"));
+ if (!parseLongList(report.get("reporterIds")).contains(SecurityUtils.getUserId())) {
+ throw new ServiceException("鍙湁鍘熸眹鎶ュ姙鐞嗕汉鍙互鍒犻櫎璇ヨ繘搴︽眹鎶�");
+ }
+ BigDecimal lockedProgress = jdbc.queryForObject("""
+ select current_progress from project_management_module where id=:moduleId and is_deleted=0 for update
+ """, Map.of("moduleId", moduleId), BigDecimal.class);
+ if (lockedProgress == null) throw new ServiceException("椤圭洰妯″潡涓嶅瓨鍦�");
+ LocalDate reportDate = report.get("reportDate") instanceof java.sql.Date date ? date.toLocalDate() : null;
+ BigDecimal previous = previousReportProgress(moduleId, reportDate, id);
+ BigDecimal increment = nvl((BigDecimal) report.get("progress")).subtract(previous).max(BigDecimal.ZERO);
+ jdbc.update("update project_management_progress_report set is_deleted=1,update_time=now() where id=:id", Map.of("id", id));
+ if (increment.compareTo(BigDecimal.ZERO) > 0) {
+ jdbc.update("""
+ update project_management_progress_report set progress=greatest(0,progress-:increment),update_time=now()
+ where module_id=:moduleId and is_deleted=0 and
+ (report_date>:reportDate or (report_date=:reportDate and id>:id))
+ """, new MapSqlParameterSource().addValue("increment", increment).addValue("moduleId", moduleId)
+ .addValue("reportDate", reportDate).addValue("id", id));
+ }
+ refreshModuleProgress(moduleId);
}
@Transactional(rollbackFor = Exception.class)
public Long saveMaterial(ProjectWorkflowDtos.MaterialRequest request) {
requireProject(request.getProjectId());
+ ensureProjectMaintenanceEditable(request.getProjectId());
if (isBlank(request.getMaterialName())) throw new ServiceException("璇疯緭鍏ヨ祫鏂欏悕绉�");
+ if (request.getAttachments() == null || request.getAttachments().isEmpty()) {
+ throw new ServiceException("璇蜂笂浼犺嚦灏戜竴涓」鐩祫鏂欐枃浠�");
+ }
MapSqlParameterSource p = new MapSqlParameterSource()
.addValue("id", request.getId()).addValue("projectId", request.getProjectId())
.addValue("materialName", request.getMaterialName()).addValue("materialType", request.getMaterialType())
@@ -431,11 +754,12 @@
""", p, keys, new String[]{"id"});
return Objects.requireNonNull(keys.getKey()).longValue();
}
- jdbc.update("""
+ int updated = 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);
+ if (updated == 0) throw new ServiceException("椤圭洰璧勬枡涓嶅瓨鍦�");
return request.getId();
}
@@ -443,7 +767,14 @@
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);
+ if (!"government_policy_file".equals(table)) {
+ Long projectId = jdbc.query("select project_id from " + table + " where id=:id and is_deleted=0",
+ Map.of("id", id), rs -> rs.next() ? rs.getLong(1) : null);
+ if (projectId == null) throw new ServiceException("璁板綍涓嶅瓨鍦�");
+ ensureProjectMaintenanceEditable(projectId);
+ }
+ int updated = jdbc.getJdbcTemplate().update("update " + table + " set is_deleted=1,update_time=now() where id=? and is_deleted=0", id);
+ if (updated == 0) throw new ServiceException("璁板綍涓嶅瓨鍦�");
}
public List<Map<String, Object>> listCategories() {
@@ -458,6 +789,14 @@
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("涓婄骇鍒嗙被涓嶈兘閫夋嫨鑷繁");
+ validateCategoryParent(request.getId(), parentId);
+ Integer duplicate = jdbc.queryForObject("""
+ select count(*) from project_management_category
+ where coalesce(parent_id,0)=:parentId and category_name=:categoryName and is_deleted=0
+ and (:id is null or id<>:id)
+ """, new MapSqlParameterSource().addValue("parentId", parentId)
+ .addValue("categoryName", request.getCategoryName().trim()).addValue("id", request.getId()), Integer.class);
+ if (duplicate != null && duplicate > 0) 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());
@@ -469,17 +808,41 @@
""", p, keys, new String[]{"id"});
return Objects.requireNonNull(keys.getKey()).longValue();
}
- jdbc.update("""
+ int updated = 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);
+ if (updated == 0) throw new ServiceException("椤圭洰鍒嗙被涓嶅瓨鍦ㄦ垨宸插垹闄�");
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("璇峰厛鍒犻櫎涓嬬骇鍒嗙被");
+ String categoryName = jdbc.query("select category_name from project_management_category where id=:id and is_deleted=0",
+ Map.of("id", id), rs -> rs.next() ? rs.getString(1) : null);
+ if (categoryName == null) throw new ServiceException("椤圭洰鍒嗙被涓嶅瓨鍦�");
+ Integer used = jdbc.queryForObject("""
+ select count(*) from project_management_info
+ where is_delete=0 and (project_category_id=:id or
+ (project_category_id is null and project_classification=:categoryName))
+ """, new MapSqlParameterSource().addValue("id", id).addValue("categoryName", categoryName), Integer.class);
+ if (used != null && used > 0) throw new ServiceException("璇ュ垎绫诲凡琚」鐩娇鐢紝涓嶈兘鍒犻櫎");
jdbc.update("update project_management_category set is_deleted=1,update_time=now() where id=:id", Map.of("id", id));
+ }
+
+ private void validateCategoryParent(Long categoryId, Long parentId) {
+ if (parentId == null || parentId == 0L) return;
+ Set<Long> visited = new HashSet<>();
+ Long current = parentId;
+ while (current != null && current != 0L) {
+ if (Objects.equals(current, categoryId)) throw new ServiceException("涓婄骇鍒嗙被涓嶈兘閫夋嫨褰撳墠鍒嗙被鎴栧叾涓嬬骇鍒嗙被");
+ if (!visited.add(current)) throw new ServiceException("鍒嗙被鏁版嵁宸插瓨鍦ㄥ惊鐜紝璇峰厛淇鍒嗙被鍏崇郴");
+ current = jdbc.query("""
+ select parent_id from project_management_category where id=:id and is_deleted=0
+ """, Map.of("id", current), rs -> rs.next() ? rs.getLong(1) : null);
+ if (current == null) throw new ServiceException("涓婄骇鍒嗙被涓嶅瓨鍦ㄦ垨宸插垹闄�");
+ }
}
public List<Map<String, Object>> listPolicies(String keyword) {
@@ -512,11 +875,12 @@
""", p, keys, new String[]{"id"});
return Objects.requireNonNull(keys.getKey()).longValue();
}
- jdbc.update("""
+ int updated = 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);
+ if (updated == 0) throw new ServiceException("鏀跨瓥鏂囦欢涓嶅瓨鍦ㄦ垨宸插垹闄�");
return request.getId();
}
@@ -655,26 +1019,163 @@
}
}
- private List<Long> projectExecutorIds(Long projectId) {
- List<Map<String, Object>> 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 isProjectStarted(Long projectId) {
+ String phase = jdbc.query("select current_phase from project_management_info where id=:id and is_delete=0",
+ Map.of("id", projectId), rs -> rs.next() ? rs.getString(1) : null);
+ return phase != null && !phase.isBlank() && !"鏈紑濮�".equals(phase);
+ }
+
+ private Map<String, Object> lifecycleNode(String code, String name, Object time, String status, String description) {
+ Map<String, Object> node = new LinkedHashMap<>();
+ node.put("code", code);
+ node.put("name", name);
+ node.put("time", time);
+ node.put("status", status);
+ node.put("description", description);
+ return node;
+ }
+
+ private void appendPhaseHistory(Long projectId, String phaseCode, String phaseName,
+ String actionType, String actionLabel, Map<String, Object> formData,
+ String remark, List<Map<String, Object>> attachments) {
+ jdbc.update("""
+ insert into project_management_phase_history
+ (project_id,phase_code,phase_name,action_type,action_label,form_data,remark,attachments,operator_name,action_time)
+ values (:projectId,:phaseCode,:phaseName,:actionType,:actionLabel,:formData,:remark,:attachments,:operatorName,now())
+ """, new MapSqlParameterSource()
+ .addValue("projectId", projectId).addValue("phaseCode", phaseCode).addValue("phaseName", phaseName)
+ .addValue("actionType", actionType).addValue("actionLabel", actionLabel)
+ .addValue("formData", jsonObject(formData)).addValue("remark", remark)
+ .addValue("attachments", json(attachments)).addValue("operatorName", currentUserName()));
+ }
+
+ private String stageHistoryAction(String phaseName, Map<String, Object> formData, boolean completed) {
+ if ("楠屾敹".equals(phaseName)) {
+ return "RECTIFY".equals(Objects.toString(formData == null ? null : formData.get("acceptanceResult"), ""))
+ ? "RECTIFY" : completed ? "PASS" : "ENTER";
+ }
+ if ("鐢虫姤".equals(phaseName)) {
+ String result = Objects.toString(formData == null ? null : formData.get("resultStatus"), "");
+ if ("SUPPLEMENT".equals(result)) return "SUPPLEMENT";
+ if ("REJECTED".equals(result)) return "REJECTED";
+ return "SUBMIT";
+ }
+ if ("鐢虫姤缁撴灉".equals(phaseName)) return "PASS";
+ return completed ? "COMPLETE" : "SAVE";
+ }
+
+ private String stageHistoryLabel(String phaseName, Map<String, Object> formData, boolean completed) {
+ if ("楠屾敹".equals(phaseName)) {
+ String result = Objects.toString(formData == null ? null : formData.get("acceptanceResult"), "");
+ if ("RECTIFY".equals(result)) return "闇�瑕佹暣鏀�";
+ return completed || "PASS".equals(result) ? "楠屾敹閫氳繃" : "杩涘叆楠屾敹";
+ }
+ if ("鐢虫姤".equals(phaseName)) {
+ String result = Objects.toString(formData == null ? null : formData.get("resultStatus"), "");
+ return switch (result) {
+ case "SUPPLEMENT" -> "闇�瑕佽ˉ鍏�";
+ case "REJECTED" -> "鐢虫姤鏈�氳繃";
+ default -> "鎻愪氦鐢虫姤";
+ };
+ }
+ if ("鐢虫姤缁撴灉".equals(phaseName)) return "鐢虫姤閫氳繃";
+ return completed ? "闃舵瀹屾垚" : "闃舵鍔炵悊";
+ }
+
+ private void addPhaseHistoryNodes(List<Map<String, Object>> nodes, List<Map<String, Object>> histories, String phaseCode) {
+ histories.stream().filter(row -> phaseCode.equals(Objects.toString(row.get("phaseCode"), ""))).forEach(row -> {
+ String actionLabel = Objects.toString(row.get("actionLabel"), "鍔炵悊璁板綍");
+ String description = Objects.toString(row.get("remark"), actionLabel);
+ if (!isBlank(Objects.toString(row.get("operatorName"), ""))) description += "锛涙搷浣滀汉锛�" + row.get("operatorName");
+ Map<String, Object> node = lifecycleNode("HISTORY_" + row.get("id"),
+ row.get("phaseName") + " 路 " + actionLabel, row.get("actionTime"), "COMPLETED", description);
+ node.put("eventType", row.get("actionType"));
+ node.put("statusLabel", "鍔炵悊璁板綍");
+ nodes.add(node);
+ });
+ }
+
+ private void addPhaseLifecycleNode(List<Map<String, Object>> nodes, Map<String, Object> phase,
+ String code, String name, Object currentPhase, Object currentPhaseTime) {
+ String status;
+ Object time = null;
+ String description = "灏氭湭杩涘叆璇ユ祦绋�";
+ if (phase != null) {
+ String phaseStatus = Objects.toString(phase.get("status"), "NOT_STARTED");
+ status = switch (phaseStatus) {
+ case "COMPLETED" -> "COMPLETED";
+ case "IN_PROGRESS" -> "CURRENT";
+ default -> "PENDING";
+ };
+ int currentRank = lifecyclePhaseRank(Objects.toString(currentPhase, "鏈紑濮�"));
+ int nodeRank = lifecyclePhaseRank(name.replace("椤圭洰", ""));
+ if ("PENDING".equals(status) && currentRank == nodeRank) {
+ status = "CURRENT";
+ time = currentPhaseTime;
+ description = "褰撳墠姝e湪杩涜锛堟牴鎹」鐩樁娈垫洿鏂版椂闂磋ˉ褰曪級";
+ } else if ("PENDING".equals(status) && currentRank > nodeRank) {
+ status = "COMPLETED";
+ description = "璇ユ祦绋嬪凡瀹屾垚锛屽巻鍙叉椂闂存湭鍗曠嫭璁板綍";
+ }
+ if (!"PENDING".equals(status)) {
+ if (time == null) time = phase.get("completedTime") == null ? phase.get("updateTime") : phase.get("completedTime");
+ if (!description.contains("琛ュ綍")) description = "COMPLETED".equals(status) ? "璇ユ祦绋嬪凡瀹屾垚" : "璇ユ祦绋嬫鍦ㄨ繘琛�";
+ if (!isBlank(Objects.toString(phase.get("updateUserName"), ""))) {
+ description += "锛岀淮鎶や汉锛�" + phase.get("updateUserName");
+ }
+ if (!isBlank(Objects.toString(phase.get("remark"), ""))) description += "锛�" + phase.get("remark");
+ }
+ } else {
+ int currentRank = lifecyclePhaseRank(Objects.toString(currentPhase, "鏈紑濮�"));
+ int nodeRank = lifecyclePhaseRank(name.replace("椤圭洰", ""));
+ status = currentRank > nodeRank ? "COMPLETED" : currentRank == nodeRank ? "CURRENT" : "PENDING";
+ if ("CURRENT".equals(status)) description = "褰撳墠姝e湪杩涜";
+ else if ("COMPLETED".equals(status)) description = "璇ユ祦绋嬪凡瀹屾垚锛屾殏鏃犵嫭绔嬫椂闂磋褰�";
+ }
+ nodes.add(lifecycleNode(code, name, time, status, description));
+ }
+
+ private int lifecyclePhaseRank(String phase) {
+ return switch (phase) {
+ case "寮�鍙�" -> 1;
+ case "鐢虫姤" -> 2;
+ case "鐢虫姤缁撴灉" -> 3;
+ case "楠屾敹" -> 4;
+ case "宸插畬鎴�" -> 5;
+ default -> 0;
+ };
+ }
+
+ private String currentNodeStatus(Object currentPhase, String pendingPhase) {
+ return pendingPhase.equals(Objects.toString(currentPhase, "鏈紑濮�")) ? "PENDING" : "CURRENT";
+ }
+
+ private BigDecimal previousReportProgress(Long moduleId, LocalDate reportDate, Long reportId) {
+ BigDecimal previous = jdbc.query("""
+ select progress from project_management_progress_report
+ where module_id=:moduleId and is_deleted=0 and
+ (report_date<:reportDate or (report_date=:reportDate and id<:id))
+ order by report_date desc,id desc limit 1
+ """, new MapSqlParameterSource().addValue("moduleId", moduleId)
+ .addValue("reportDate", reportDate).addValue("id", reportId),
+ rs -> rs.next() ? rs.getBigDecimal(1) : BigDecimal.ZERO);
+ return nvl(previous);
+ }
+
+ private void refreshModuleProgress(Long moduleId) {
+ BigDecimal latest = jdbc.query("""
+ select progress from project_management_progress_report
+ where module_id=:moduleId and is_deleted=0 order by report_date desc,id desc limit 1
+ """, Map.of("moduleId", moduleId), rs -> rs.next() ? rs.getBigDecimal(1) : BigDecimal.ZERO);
+ jdbc.update("update project_management_module set current_progress=:progress,update_time=now() where id=:moduleId",
+ new MapSqlParameterSource().addValue("progress", nvl(latest)).addValue("moduleId", moduleId));
}
private boolean hasPositiveShare(List<Map<String, Object>> reports, Long userId) {
@@ -745,12 +1246,106 @@
return val.setScale(2, RoundingMode.HALF_UP);
}
+ private void validateTransitionForm(String currentPhase, Map<String, Object> formData) {
+ switch (currentPhase) {
+ case "寮�鍙�" -> {
+ requireDate(formData, "applicationDate", "鐢虫姤鏃ユ湡");
+ requireText(formData, "applicationType", "鐢虫姤绫诲瀷");
+ requireText(formData, "applicationName", "鐢虫姤鍚嶇О");
+ requireText(formData, "description", "鐢虫姤璇存槑");
+ }
+ case "鐢虫姤" -> {
+ requireDate(formData, "resultDate", "缁撴灉鏃ユ湡");
+ requireValue(formData, "resultStatus", "PASS", "鍙湁鐢虫姤閫氳繃鍚庢墠鑳借繘鍏ョ敵鎶ョ粨鏋滈樁娈�");
+ requireText(formData, "resultDescription", "缁撴灉璇存槑");
+ }
+ case "鐢虫姤缁撴灉" -> {
+ requireDate(formData, "acceptancePlanDate", "璁″垝楠屾敹鏃ユ湡");
+ requireText(formData, "acceptor", "楠屾敹浜�/瀹㈡埛");
+ requireText(formData, "acceptanceStandard", "楠屾敹鏍囧噯");
+ }
+ case "楠屾敹" -> {
+ requireDate(formData, "acceptanceDate", "楠屾敹鏃ユ湡");
+ requireText(formData, "acceptor", "楠屾敹浜�/瀹㈡埛");
+ requireValue(formData, "acceptanceResult", "PASS", "鍙湁楠屾敹閫氳繃鍚庢墠鑳藉畬鎴愰」鐩�");
+ requireText(formData, "acceptanceDescription", "楠屾敹璇存槑");
+ }
+ default -> throw new ServiceException("褰撳墠闃舵涓嶈兘鍔炵悊娴佽浆");
+ }
+ }
+
+ private void validateSupplementalPhase(String currentPhase, Map<String, Object> formData) {
+ if ("鐢虫姤".equals(currentPhase)) {
+ requireDate(formData, "resultDate", "缁撴灉鏃ユ湡");
+ String result = requireText(formData, "resultStatus", "鐢虫姤缁撴灉");
+ if (!List.of("SUPPLEMENT", "REJECTED").contains(result)) {
+ throw new ServiceException("鐢虫姤閫氳繃蹇呴』浣跨敤闃舵鎺ㄨ繘鎿嶄綔");
+ }
+ requireText(formData, "resultDescription", "缁撴灉璇存槑");
+ return;
+ }
+ if ("楠屾敹".equals(currentPhase)) {
+ requireDate(formData, "acceptanceDate", "楠屾敹鏃ユ湡");
+ requireText(formData, "acceptor", "楠屾敹浜�/瀹㈡埛");
+ requireValue(formData, "acceptanceResult", "RECTIFY", "楠屾敹閫氳繃蹇呴』浣跨敤瀹屾垚椤圭洰鎿嶄綔");
+ requireText(formData, "acceptanceDescription", "楠屾敹璇存槑");
+ return;
+ }
+ throw new ServiceException("褰撳墠闃舵涓嶆敮鎸佽ˉ鍏呭姙鐞嗭紝鍙兘鎸夋祦绋嬫帹杩�");
+ }
+
+ private String phaseCodeForName(String phaseName) {
+ return switch (Objects.toString(phaseName, "")) {
+ case "绔嬮」" -> "INITIATION";
+ case "寮�鍙�" -> "DEVELOPMENT";
+ case "鐢虫姤" -> "APPLICATION";
+ case "鐢虫姤缁撴灉" -> "RESULT";
+ case "楠屾敹" -> "ACCEPTANCE";
+ default -> null;
+ };
+ }
+
+ private String requireText(Map<String, Object> formData, String key, String label) {
+ String value = Objects.toString(formData == null ? null : formData.get(key), "").trim();
+ if (value.isEmpty()) throw new ServiceException("璇峰~鍐�" + label);
+ return value;
+ }
+
+ private void requireDate(Map<String, Object> formData, String key, String label) {
+ String value = requireText(formData, key, label);
+ try {
+ LocalDate.parse(value);
+ } catch (Exception e) {
+ throw new ServiceException(label + "鏍煎紡涓嶆纭�");
+ }
+ }
+
+ private void requireValue(Map<String, Object> formData, String key, String expected, String message) {
+ if (!expected.equals(requireText(formData, key, key))) throw new ServiceException(message);
+ }
+
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 void ensureProjectMaintenanceEditable(Long projectId) {
+ String phase = jdbc.queryForObject("select current_phase from project_management_info where id=:id and is_delete=0",
+ Map.of("id", projectId), String.class);
+ if (List.of("鐢虫姤", "鐢虫姤缁撴灉", "楠屾敹", "宸插畬鎴�").contains(phase)) {
+ throw new ServiceException("椤圭洰杩涘叆鐢虫姤闃舵鍚庯紝妯″潡銆佽窡杩涖�佹眹鎶ュ拰椤圭洰璧勬枡鍙兘鏌ョ湅锛屼笉鑳芥柊澧炪�佺紪杈戞垨鍒犻櫎");
+ }
+ }
+
+ private void ensureProjectLeader(Long projectId) {
+ Long leaderId = jdbc.query("select project_leader_id from project_management_info where id=:id and is_delete=0",
+ Map.of("id", projectId), rs -> rs.next() ? rs.getLong(1) : null);
+ if (leaderId == null || !leaderId.equals(SecurityUtils.getUserId())) {
+ throw new ServiceException("鍙湁椤圭洰璐熻矗浜哄彲浠ョ淮鎶ら」鐩窡杩�");
+ }
+ }
+
private List<Map<String, Object>> query(String sql, Map<String, ?> params) {
return jdbc.queryForList(sql, params);
}
--
Gitblit v1.9.3