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 |  276 ++++++++++++++++++++++++++++++++++++++++++++----------
 1 files changed, 224 insertions(+), 52 deletions(-)

diff --git a/src/main/java/com/ruoyi/projectManagement/service/ProjectWorkflowService.java b/src/main/java/com/ruoyi/projectManagement/service/ProjectWorkflowService.java
index f2e9b48..85b60ad 100644
--- a/src/main/java/com/ruoyi/projectManagement/service/ProjectWorkflowService.java
+++ b/src/main/java/com/ruoyi/projectManagement/service/ProjectWorkflowService.java
@@ -155,10 +155,18 @@
     @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<>''
                 """, Map.of("projectId", projectId), Integer.class);
@@ -169,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
@@ -208,9 +225,8 @@
     public void transitionStage(ProjectWorkflowDtos.StageTransitionRequest request) {
         requireProject(request.getProjectId());
         ensurePhases(request.getProjectId());
-        String current = jdbc.queryForObject("select current_phase from project_management_info where id=:id and is_delete=0",
+        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(
                 "寮�鍙�", "鐢虫姤",
                 "鐢虫姤", "鐢虫姤缁撴灉",
@@ -221,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
@@ -283,28 +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) {
-                String projectPhase = jdbc.queryForObject("select current_phase from project_management_info where id=:id and is_delete=0",
-                        Map.of("id", request.getProjectId()), String.class);
-                if (lifecyclePhaseRank(projectPhase) < index) {
-                    throw new ServiceException("璇峰厛瀹屾垚涓婁竴闃舵鈥�" + PHASE_NAMES.get(index - 1) + "鈥�");
-                }
-                jdbc.update("""
-                        update project_management_phase set status='COMPLETED',completed_time=coalesce(completed_time,now()),
-                        update_time=now() where project_id=:projectId and phase_code=:phaseCode and is_deleted=0
-                        """, new MapSqlParameterSource().addValue("projectId", request.getProjectId())
-                        .addValue("phaseCode", PHASE_CODES.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()))
@@ -320,13 +325,6 @@
                 stageHistoryAction(PHASE_NAMES.get(index), request.getFormData(), false),
                 stageHistoryLabel(PHASE_NAMES.get(index), request.getFormData(), false),
                 request.getFormData(), request.getRemark(), request.getAttachments());
-        String currentPhase = "COMPLETED".equals(status) && index == PHASE_CODES.size() - 1
-                ? "宸插畬鎴�" : PHASE_NAMES.get(index);
-        jdbc.update("""
-                update project_management_info set current_phase=:phase,
-                actual_end_time=case when :phase='宸插畬鎴�' then coalesce(actual_end_time,current_date) else actual_end_time end,
-                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);
     }
@@ -402,6 +400,17 @@
         validateMembers(request.getMembers());
 
         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("椤圭洰寮�濮嬪悗涓嶈兘鏂板妯″潡锛屼粎鍙鐞嗘ā鍧楁墽琛屼汉");
         }
@@ -534,15 +543,23 @@
     public Long saveFollowUp(ProjectWorkflowDtos.FollowUpRequest request) {
         requireProject(request.getProjectId());
         ensureProjectMaintenanceEditable(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("鍙湁椤圭洰璐熻矗浜哄彲浠ュ~鍐欓」鐩窡杩�");
-        }
+        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())
@@ -557,11 +574,23 @@
                     """, 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)
@@ -571,11 +600,15 @@
         if (request.getModuleId() == null || request.getReportDate() == null) {
             throw new ServiceException("璇烽�夋嫨椤圭洰妯″潡鍜屾眹鎶ユ椂闂�");
         }
+        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("椤圭洰妯″潡涓嶅瓨鍦�");
         List<ProjectWorkflowDtos.MemberRequest> reporterShares = normalizeReporterShares(request);
@@ -588,26 +621,33 @@
         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) {
-            progress = current.add(increment);
-            if (progress.compareTo(BigDecimal.valueOf(100)) > 0) {
+            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
+                    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("宸叉湁杩涘害姹囨姤涓嶈兘淇敼姹囨姤鏃堕棿");
@@ -636,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,
@@ -659,16 +704,19 @@
     @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
+                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"));
-        List<Long> moduleMemberIds = query("""
-                select user_id as userId from project_management_module_member
-                where module_id=:moduleId and is_deleted=0
-                """, Map.of("moduleId", moduleId)).stream().map(row -> asLong(row.get("userId"))).toList();
-        if (!moduleMemberIds.contains(SecurityUtils.getUserId())) throw new ServiceException("鍙湁璇ユā鍧楁墽琛屼汉鍙互鍒犻櫎杩涘害姹囨姤");
+        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);
@@ -689,6 +737,9 @@
         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())
@@ -703,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();
     }
 
@@ -721,7 +773,8 @@
             if (projectId == null) throw new ServiceException("璁板綍涓嶅瓨鍦�");
             ensureProjectMaintenanceEditable(projectId);
         }
-        jdbc.getJdbcTemplate().update("update " + table + " set is_deleted=1,update_time=now() where id=?", id);
+        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() {
@@ -736,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());
@@ -747,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) {
@@ -790,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();
     }
 
@@ -1160,6 +1246,84 @@
         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);
@@ -1174,6 +1338,14 @@
         }
     }
 
+    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