From 719997218c3a20d6047503e1037443850c81caa9 Mon Sep 17 00:00:00 2001
From: lzm <2316711944@qq.com>
Date: Fri, 17 Apr 2026 18:15:34 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BB=A3=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
lyzsys-module-tjt/pom.xml | 64 +++
.../ProjectOutputSplitController.java | 43 ++
.../vo/ProjectOutputSplitRespVO.java | 99 ++++
.../vo/ProjectOutputSplitSaveReqVO.java | 57 +++
.../planning/ProjectPlanningController.java | 123 +++++
.../planning/vo/ProjectPlanningPageReqVO.java | 37 ++
.../planning/vo/ProjectPlanningRespVO.java | 127 +++++
.../planning/vo/ProjectPlanningSaveReqVO.java | 115 +++++
.../ProjectPlanningQuarterController.java | 83 ++++
.../vo/ProjectPlanningQuarterRespVO.java | 37 ++
.../vo/ProjectPlanningQuarterSaveReqVO.java | 40 ++
.../admin/profit/ProjectProfitController.java | 47 ++
.../profit/vo/ProjectProfitPageReqVO.java | 31 ++
.../admin/profit/vo/ProjectProfitRespVO.java | 55 +++
.../admin/project/ProjectController.java | 82 ++++
.../admin/project/vo/ProjectPageReqVO.java | 31 ++
.../admin/project/vo/ProjectRespVO.java | 85 ++++
.../admin/project/vo/ProjectSaveReqVO.java | 79 ++++
.../SpecialtyRoleSplitController.java | 45 ++
.../vo/SpecialtyRolePersonRespVO.java | 21 +
.../vo/SpecialtyRolePersonSaveReqVO.java | 18 +
.../vo/SpecialtyRoleSplitBatchSaveReqVO.java | 24 +
.../vo/SpecialtyRoleSplitRespVO.java | 55 +++
.../vo/SpecialtyRoleSplitSaveItemReqVO.java | 32 ++
.../outputsplit/ProjectOutputSplitDO.java | 52 +++
.../planning/ProjectPlanningDO.java | 94 ++++
.../ProjectPlanningQuarterDO.java | 36 ++
.../tjt/dal/dataobject/project/ProjectDO.java | 86 ++++
.../SpecialtyRoleSplitDO.java | 42 ++
.../outputsplit/ProjectOutputSplitMapper.java | 29 ++
.../mysql/planning/ProjectPlanningMapper.java | 37 ++
.../ProjectPlanningQuarterMapper.java | 47 ++
.../tjt/dal/mysql/project/ProjectMapper.java | 27 ++
.../SpecialtyRoleSplitMapper.java | 25 +
.../module/tjt/enums/ErrorCodeConstants.java | 43 ++
.../tjt/enums/OutputSplitBizConstants.java | 125 +++++
.../ProjectPlanningBizTypeConstants.java | 83 ++++
.../ProjectOutputSplitService.java | 32 ++
.../ProjectOutputSplitServiceImpl.java | 269 +++++++++++
.../planning/ProjectPlanningService.java | 36 ++
.../planning/ProjectPlanningServiceImpl.java | 438 ++++++++++++++++++
.../ProjectPlanningQuarterService.java | 27 ++
.../ProjectPlanningQuarterServiceImpl.java | 146 ++++++
.../service/profit/ProjectProfitService.java | 18 +
.../profit/ProjectProfitServiceImpl.java | 135 ++++++
.../tjt/service/project/ProjectService.java | 29 ++
.../service/project/ProjectServiceImpl.java | 72 +++
.../SpecialtyRoleSplitService.java | 23 +
.../SpecialtyRoleSplitServiceImpl.java | 402 ++++++++++++++++
lyzsys-server/pom.xml | 2 +-
.../src/main/resources/application-local.yaml | 2 +-
pom.xml | 3 +-
52 files changed, 3787 insertions(+), 3 deletions(-)
create mode 100644 lyzsys-module-tjt/pom.xml
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/outputsplit/ProjectOutputSplitController.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/outputsplit/vo/ProjectOutputSplitRespVO.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/outputsplit/vo/ProjectOutputSplitSaveReqVO.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planning/ProjectPlanningController.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planning/vo/ProjectPlanningPageReqVO.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planning/vo/ProjectPlanningRespVO.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planning/vo/ProjectPlanningSaveReqVO.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planningquarter/ProjectPlanningQuarterController.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planningquarter/vo/ProjectPlanningQuarterRespVO.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planningquarter/vo/ProjectPlanningQuarterSaveReqVO.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/profit/ProjectProfitController.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/profit/vo/ProjectProfitPageReqVO.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/profit/vo/ProjectProfitRespVO.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/project/ProjectController.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/project/vo/ProjectPageReqVO.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/project/vo/ProjectRespVO.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/project/vo/ProjectSaveReqVO.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/specialtyrolesplit/SpecialtyRoleSplitController.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/specialtyrolesplit/vo/SpecialtyRolePersonRespVO.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/specialtyrolesplit/vo/SpecialtyRolePersonSaveReqVO.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/specialtyrolesplit/vo/SpecialtyRoleSplitBatchSaveReqVO.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/specialtyrolesplit/vo/SpecialtyRoleSplitRespVO.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/specialtyrolesplit/vo/SpecialtyRoleSplitSaveItemReqVO.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/dataobject/outputsplit/ProjectOutputSplitDO.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/dataobject/planning/ProjectPlanningDO.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/dataobject/planningquarter/ProjectPlanningQuarterDO.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/dataobject/project/ProjectDO.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/dataobject/specialtyrolesplit/SpecialtyRoleSplitDO.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/mysql/outputsplit/ProjectOutputSplitMapper.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/mysql/planning/ProjectPlanningMapper.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/mysql/planningquarter/ProjectPlanningQuarterMapper.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/mysql/project/ProjectMapper.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/mysql/specialtyrolesplit/SpecialtyRoleSplitMapper.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/enums/ErrorCodeConstants.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/enums/OutputSplitBizConstants.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/enums/ProjectPlanningBizTypeConstants.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/outputsplit/ProjectOutputSplitService.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/outputsplit/ProjectOutputSplitServiceImpl.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/planning/ProjectPlanningService.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/planning/ProjectPlanningServiceImpl.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/planningquarter/ProjectPlanningQuarterService.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/planningquarter/ProjectPlanningQuarterServiceImpl.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/profit/ProjectProfitService.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/profit/ProjectProfitServiceImpl.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/project/ProjectService.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/project/ProjectServiceImpl.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/specialtyrolesplit/SpecialtyRoleSplitService.java
create mode 100644 lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/specialtyrolesplit/SpecialtyRoleSplitServiceImpl.java
diff --git a/lyzsys-module-tjt/pom.xml b/lyzsys-module-tjt/pom.xml
new file mode 100644
index 0000000..6443bd3
--- /dev/null
+++ b/lyzsys-module-tjt/pom.xml
@@ -0,0 +1,64 @@
+
+
+
+ cn.iocoder.boot
+ lyzsys
+ ${revision}
+
+ 4.0.0
+ lyzsys-module-tjt
+ jar
+
+ ${project.artifactId}
+
+ tjt 模块,承载特建投设计产值统计业务。
+
+
+
+
+ cn.iocoder.boot
+ lyzsys-module-infra
+ ${revision}
+
+
+
+ cn.iocoder.boot
+ lyzsys-spring-boot-starter-biz-tenant
+
+
+
+ cn.iocoder.boot
+ lyzsys-spring-boot-starter-security
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+ cn.iocoder.boot
+ lyzsys-spring-boot-starter-mybatis
+
+
+
+ cn.iocoder.boot
+ lyzsys-spring-boot-starter-redis
+
+
+
+ cn.iocoder.boot
+ lyzsys-spring-boot-starter-test
+ test
+
+
+
+ cn.iocoder.boot
+ lyzsys-spring-boot-starter-excel
+
+
+
+
+
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/outputsplit/ProjectOutputSplitController.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/outputsplit/ProjectOutputSplitController.java
new file mode 100644
index 0000000..85d6251
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/outputsplit/ProjectOutputSplitController.java
@@ -0,0 +1,43 @@
+package cn.iocoder.lyzsys.module.tjt.controller.admin.outputsplit;
+
+import cn.iocoder.lyzsys.framework.common.pojo.CommonResult;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.outputsplit.vo.ProjectOutputSplitRespVO;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.outputsplit.vo.ProjectOutputSplitSaveReqVO;
+import cn.iocoder.lyzsys.module.tjt.service.outputsplit.ProjectOutputSplitService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+
+import static cn.iocoder.lyzsys.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 特建投页面4拆分比例")
+@RestController
+@RequestMapping("/tjt/output-split")
+@Validated
+public class ProjectOutputSplitController {
+
+ @Resource
+ private ProjectOutputSplitService projectOutputSplitService;
+
+ @GetMapping("/get-by-planning")
+ @Operation(summary = "根据合约规划获得页面4拆分比例")
+ @Parameter(name = "planningId", description = "合约规划 ID", required = true, example = "1")
+ @PreAuthorize("@ss.hasPermission('tjt:output-split:query')")
+ public CommonResult getProjectOutputSplitByPlanningId(@RequestParam("planningId") Long planningId) {
+ return success(projectOutputSplitService.getProjectOutputSplit(planningId));
+ }
+
+ @PutMapping("/save")
+ @Operation(summary = "保存页面4拆分比例")
+ @PreAuthorize("@ss.hasPermission('tjt:output-split:update')")
+ public CommonResult saveProjectOutputSplit(@Valid @RequestBody ProjectOutputSplitSaveReqVO reqVO) {
+ return success(projectOutputSplitService.saveProjectOutputSplit(reqVO));
+ }
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/outputsplit/vo/ProjectOutputSplitRespVO.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/outputsplit/vo/ProjectOutputSplitRespVO.java
new file mode 100644
index 0000000..ab97a35
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/outputsplit/vo/ProjectOutputSplitRespVO.java
@@ -0,0 +1,99 @@
+package cn.iocoder.lyzsys.module.tjt.controller.admin.outputsplit.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Schema(description = "管理后台 - 页面4拆分比例 Response VO")
+@Data
+public class ProjectOutputSplitRespVO {
+
+ @Schema(description = "页面4拆分 ID", example = "1")
+ private Long id;
+
+ @Schema(description = "项目 ID", example = "1")
+ private Long projectId;
+
+ @Schema(description = "合约规划 ID", example = "1")
+ private Long planningId;
+
+ @Schema(description = "项目名称")
+ private String projectName;
+
+ @Schema(description = "规划内容")
+ private String planningContent;
+
+ @Schema(description = "年度")
+ private Integer year;
+
+ @Schema(description = "考核产值")
+ private BigDecimal assessmentOutputValue;
+
+ @Schema(description = "项目经理")
+ private String projectManagerName;
+
+ @Schema(description = "项目负责人")
+ private String engineeringLeaderName;
+
+ @Schema(description = "项目经理比例")
+ private BigDecimal projectManagerRatio;
+
+ @Schema(description = "项目经理金额")
+ private BigDecimal projectManagerAmount;
+
+ @Schema(description = "项目负责人比例")
+ private BigDecimal engineeringLeaderRatio;
+
+ @Schema(description = "项目负责人金额")
+ private BigDecimal engineeringLeaderAmount;
+
+ @Schema(description = "专业所比例")
+ private BigDecimal officeRatio;
+
+ @Schema(description = "专业所金额")
+ private BigDecimal officeAmount;
+
+ @Schema(description = "建筑专业比例")
+ private BigDecimal archRatio;
+
+ @Schema(description = "建筑专业金额")
+ private BigDecimal archAmount;
+
+ @Schema(description = "装修专业比例")
+ private BigDecimal decorRatio;
+
+ @Schema(description = "装修专业金额")
+ private BigDecimal decorAmount;
+
+ @Schema(description = "结构专业比例")
+ private BigDecimal structRatio;
+
+ @Schema(description = "结构专业金额")
+ private BigDecimal structAmount;
+
+ @Schema(description = "水专业比例")
+ private BigDecimal waterRatio;
+
+ @Schema(description = "水专业金额")
+ private BigDecimal waterAmount;
+
+ @Schema(description = "电气专业比例")
+ private BigDecimal elecRatio;
+
+ @Schema(description = "电气专业金额")
+ private BigDecimal elecAmount;
+
+ @Schema(description = "暖通专业比例")
+ private BigDecimal hvacRatio;
+
+ @Schema(description = "暖通专业金额")
+ private BigDecimal hvacAmount;
+
+ @Schema(description = "数字化设计专业比例")
+ private BigDecimal digitalRatio;
+
+ @Schema(description = "数字化设计专业金额")
+ private BigDecimal digitalAmount;
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/outputsplit/vo/ProjectOutputSplitSaveReqVO.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/outputsplit/vo/ProjectOutputSplitSaveReqVO.java
new file mode 100644
index 0000000..33f4ab3
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/outputsplit/vo/ProjectOutputSplitSaveReqVO.java
@@ -0,0 +1,57 @@
+package cn.iocoder.lyzsys.module.tjt.controller.admin.outputsplit.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+
+@Schema(description = "管理后台 - 页面4拆分比例新增/修改 Request VO")
+@Data
+public class ProjectOutputSplitSaveReqVO {
+
+ @Schema(description = "合同规划 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "合同规划 ID 不能为空")
+ private Long planningId;
+
+ @Schema(description = "项目经理比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.0200")
+ @NotNull(message = "项目经理比例不能为空")
+ private BigDecimal projectManagerRatio;
+
+ @Schema(description = "项目负责人比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.0300")
+ @NotNull(message = "项目负责人比例不能为空")
+ private BigDecimal engineeringLeaderRatio;
+
+ @Schema(description = "专业所比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.9500")
+ @NotNull(message = "专业所比例不能为空")
+ private BigDecimal officeRatio;
+
+ @Schema(description = "建筑专业比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.5000")
+ @NotNull(message = "建筑专业比例不能为空")
+ private BigDecimal archRatio;
+
+ @Schema(description = "装修专业比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.0000")
+ @NotNull(message = "装修专业比例不能为空")
+ private BigDecimal decorRatio;
+
+ @Schema(description = "结构专业比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.2000")
+ @NotNull(message = "结构专业比例不能为空")
+ private BigDecimal structRatio;
+
+ @Schema(description = "水专业比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.1000")
+ @NotNull(message = "水专业比例不能为空")
+ private BigDecimal waterRatio;
+
+ @Schema(description = "电气专业比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.1000")
+ @NotNull(message = "电气专业比例不能为空")
+ private BigDecimal elecRatio;
+
+ @Schema(description = "暖通专业比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.0500")
+ @NotNull(message = "暖通专业比例不能为空")
+ private BigDecimal hvacRatio;
+
+ @Schema(description = "数字化设计专业比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.0500")
+ @NotNull(message = "数字化设计专业比例不能为空")
+ private BigDecimal digitalRatio;
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planning/ProjectPlanningController.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planning/ProjectPlanningController.java
new file mode 100644
index 0000000..f6be548
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planning/ProjectPlanningController.java
@@ -0,0 +1,123 @@
+package cn.iocoder.lyzsys.module.tjt.controller.admin.planning;
+
+import cn.iocoder.lyzsys.framework.common.pojo.CommonResult;
+import cn.iocoder.lyzsys.framework.common.pojo.PageResult;
+import cn.iocoder.lyzsys.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.lyzsys.framework.common.util.object.BeanUtils;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.planning.vo.ProjectPlanningPageReqVO;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.planning.vo.ProjectPlanningRespVO;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.planning.vo.ProjectPlanningSaveReqVO;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.planning.ProjectPlanningDO;
+import cn.iocoder.lyzsys.module.tjt.service.planning.ProjectPlanningService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.lyzsys.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 特建投合约规划")
+@RestController
+@RequestMapping("/tjt/planning")
+@Validated
+public class ProjectPlanningController {
+
+ private static final int RATIO_SCALE = 4;
+ private static final BigDecimal ZERO_RATIO = BigDecimal.ZERO.setScale(RATIO_SCALE, RoundingMode.HALF_UP);
+
+ @Resource
+ private ProjectPlanningService projectPlanningService;
+
+ @PostMapping("/create")
+ @Operation(summary = "创建合约规划")
+ @PreAuthorize("@ss.hasPermission('tjt:planning:create')")
+ public CommonResult createProjectPlanning(@Valid @RequestBody ProjectPlanningSaveReqVO createReqVO) {
+ return success(projectPlanningService.createProjectPlanning(createReqVO));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "修改合约规划")
+ @PreAuthorize("@ss.hasPermission('tjt:planning:update')")
+ public CommonResult updateProjectPlanning(@Valid @RequestBody ProjectPlanningSaveReqVO updateReqVO) {
+ projectPlanningService.updateProjectPlanning(updateReqVO);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "删除合约规划")
+ @Parameter(name = "id", description = "编号", required = true, example = "1")
+ @PreAuthorize("@ss.hasPermission('tjt:planning:delete')")
+ public CommonResult deleteProjectPlanning(@RequestParam("id") Long id) {
+ projectPlanningService.deleteProjectPlanning(id);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete-list")
+ @Operation(summary = "批量删除合约规划")
+ @Parameter(name = "ids", description = "编号列表", required = true)
+ @PreAuthorize("@ss.hasPermission('tjt:planning:delete')")
+ public CommonResult deleteProjectPlanningList(@RequestParam("ids") List ids) {
+ projectPlanningService.deleteProjectPlanningList(ids);
+ return success(true);
+ }
+
+ @GetMapping("/page")
+ @Operation(summary = "获得合约规划分页")
+ @PreAuthorize("@ss.hasPermission('tjt:planning:query')")
+ public CommonResult> getProjectPlanningPage(@Valid ProjectPlanningPageReqVO pageReqVO) {
+ PageResult pageResult = projectPlanningService.getProjectPlanningPage(pageReqVO);
+ Map allocatedAmountMap = projectPlanningService.getAllocatedAmountMap(
+ CollectionUtils.convertSet(pageResult.getList(), ProjectPlanningDO::getId));
+ return success(BeanUtils.toBean(pageResult, ProjectPlanningRespVO.class,
+ respVO -> fillDistributionSummary(respVO, allocatedAmountMap)));
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "获得合约规划详情")
+ @Parameter(name = "id", description = "编号", required = true, example = "1")
+ @PreAuthorize("@ss.hasPermission('tjt:planning:query')")
+ public CommonResult getProjectPlanning(@RequestParam("id") Long id) {
+ ProjectPlanningDO planning = projectPlanningService.getProjectPlanning(id);
+ if (planning == null) {
+ return success(null);
+ }
+ Map allocatedAmountMap = projectPlanningService.getAllocatedAmountMap(
+ Collections.singleton(planning.getId()));
+ return success(BeanUtils.toBean(planning, ProjectPlanningRespVO.class,
+ respVO -> fillDistributionSummary(respVO, allocatedAmountMap)));
+ }
+
+ @GetMapping("/list-by-project")
+ @Operation(summary = "根据项目获得合约规划列表")
+ @Parameter(name = "projectId", description = "项目 ID", required = true, example = "1")
+ @PreAuthorize("@ss.hasPermission('tjt:planning:query')")
+ public CommonResult> getProjectPlanningListByProjectId(@RequestParam("projectId") Long projectId) {
+ List list = projectPlanningService.getProjectPlanningListByProjectId(projectId);
+ Map allocatedAmountMap = projectPlanningService.getAllocatedAmountMap(
+ CollectionUtils.convertSet(list, ProjectPlanningDO::getId));
+ return success(BeanUtils.toBean(list, ProjectPlanningRespVO.class,
+ respVO -> fillDistributionSummary(respVO, allocatedAmountMap)));
+ }
+
+ private void fillDistributionSummary(ProjectPlanningRespVO respVO, Map allocatedAmountMap) {
+ BigDecimal allocatedRatio = allocatedAmountMap.getOrDefault(respVO.getId(), ZERO_RATIO);
+ BigDecimal totalDistributionAmount = respVO.getTotalDistributionAmount() == null
+ ? ZERO_RATIO : respVO.getTotalDistributionAmount().setScale(RATIO_SCALE, RoundingMode.HALF_UP);
+ BigDecimal allocatedAmount = totalDistributionAmount.multiply(allocatedRatio)
+ .setScale(RATIO_SCALE, RoundingMode.HALF_UP);
+ respVO.setAllocatedAmount(allocatedAmount);
+ respVO.setPendingAmount(totalDistributionAmount.subtract(allocatedAmount)
+ .setScale(RATIO_SCALE, RoundingMode.HALF_UP));
+ }
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planning/vo/ProjectPlanningPageReqVO.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planning/vo/ProjectPlanningPageReqVO.java
new file mode 100644
index 0000000..c11340e
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planning/vo/ProjectPlanningPageReqVO.java
@@ -0,0 +1,37 @@
+package cn.iocoder.lyzsys.module.tjt.controller.admin.planning.vo;
+
+import cn.iocoder.lyzsys.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.lyzsys.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 合约规划分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ProjectPlanningPageReqVO extends PageParam {
+
+ @Schema(description = "项目 ID", example = "1")
+ private Long projectId;
+
+ @Schema(description = "归属类型", example = "专业所")
+ private String ownershipType;
+
+ @Schema(description = "产值计算方式", example = "指导价法")
+ private String calculationMethod;
+
+ @Schema(description = "规划内容,模糊匹配", example = "建筑")
+ private String planningContent;
+
+ @Schema(description = "开始年度", example = "2026")
+ private Integer planningStartYear;
+
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ @Schema(description = "创建时间")
+ private LocalDateTime[] createTime;
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planning/vo/ProjectPlanningRespVO.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planning/vo/ProjectPlanningRespVO.java
new file mode 100644
index 0000000..5402698
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planning/vo/ProjectPlanningRespVO.java
@@ -0,0 +1,127 @@
+package cn.iocoder.lyzsys.module.tjt.controller.admin.planning.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 合约规划 Response VO")
+@Data
+public class ProjectPlanningRespVO {
+
+ @Schema(description = "合约规划 ID", example = "1")
+ private Long id;
+
+ @Schema(description = "项目 ID", example = "1")
+ private Long projectId;
+
+ @Schema(description = "归属类型")
+ private String ownershipType;
+
+ @Schema(description = "产值计算方式")
+ private String calculationMethod;
+
+ @Schema(description = "规划内容")
+ private String planningContent;
+
+ @Schema(description = "规划金额")
+ private BigDecimal planningAmount;
+
+ @Schema(description = "管理费费率")
+ private BigDecimal managementFeeRate;
+
+ @Schema(description = "管理费")
+ private BigDecimal managementFee;
+
+ @Schema(description = "实施团队")
+ private String implementationTeam;
+
+ @Schema(description = "开始年度")
+ private Integer planningStartYear;
+
+ @Schema(description = "面积")
+ private BigDecimal planningArea;
+
+ @Schema(description = "设计阶段")
+ private String designStage;
+
+ @Schema(description = "本次设计阶段比例")
+ private BigDecimal currentDesignStageRatio;
+
+ @Schema(description = "审核审定是否外包")
+ private Boolean reviewOutsourceFlag;
+
+ @Schema(description = "审核审定占比")
+ private BigDecimal reviewOutsourceRatio;
+
+ @Schema(description = "总分配比例")
+ private BigDecimal totalDistributionAmount;
+
+ @Schema(description = "已分配比例")
+ private BigDecimal allocatedAmount;
+
+ @Schema(description = "待分配比例")
+ private BigDecimal pendingAmount;
+
+ @Schema(description = "Planning progress remark")
+ private String progressRemark;
+
+ @Schema(description = "楼栋数或户型数")
+ private Integer buildingOrUnitCount;
+
+ @Schema(description = "套图系数,保留两位小数")
+ private BigDecimal drawingSetFactor;
+
+ @Schema(description = "规模系数,保留两位小数")
+ private BigDecimal scaleFactor;
+
+ @Schema(description = "修改系数,保留两位小数")
+ private BigDecimal modificationFactor;
+
+ @Schema(description = "复杂系数/复杂等级,按比例值返回(100%=1.0000)")
+ private BigDecimal complexityFactor;
+
+ @Schema(description = "内部指导单价(元/㎡)")
+ private BigDecimal internalGuidanceUnitPrice;
+
+ @Schema(description = "虚拟产值计算方式")
+ private String virtualCalculationMethod;
+
+ @Schema(description = "工日")
+ private BigDecimal workingDayCount;
+
+ @Schema(description = "工日单价")
+ private BigDecimal workingDayUnitPrice;
+
+ @Schema(description = "指导单价")
+ private BigDecimal guidanceUnitPrice;
+
+ @Schema(description = "指导总价")
+ private BigDecimal guidanceTotalPrice;
+
+ @Schema(description = "虚拟总价")
+ private BigDecimal virtualTotalPrice;
+
+ @Schema(description = "产值计算比例")
+ private BigDecimal calculationRatio;
+
+ @Schema(description = "合同单价(元/㎡)")
+ private BigDecimal contractUnitPrice;
+
+ @Schema(description = "合计调整系数")
+ private BigDecimal totalAdjustmentFactor;
+
+ @Schema(description = "考核面积")
+ private BigDecimal assessmentArea;
+
+ @Schema(description = "虚拟产值")
+ private BigDecimal virtualOutputValue;
+
+ @Schema(description = "考核产值")
+ private BigDecimal assessmentOutputValue;
+
+ @Schema(description = "创建时间")
+ private LocalDateTime createTime;
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planning/vo/ProjectPlanningSaveReqVO.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planning/vo/ProjectPlanningSaveReqVO.java
new file mode 100644
index 0000000..481c442
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planning/vo/ProjectPlanningSaveReqVO.java
@@ -0,0 +1,115 @@
+package cn.iocoder.lyzsys.module.tjt.controller.admin.planning.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+import java.math.BigDecimal;
+
+@Schema(description = "管理后台 - 合约规划新增/修改 Request VO")
+@Data
+public class ProjectPlanningSaveReqVO {
+
+ @Schema(description = "合约规划 ID", example = "1")
+ private Long id;
+
+ @Schema(description = "项目 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "项目 ID 不能为空")
+ private Long projectId;
+
+ @Schema(description = "归属类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "专业所")
+ @NotBlank(message = "归属类型不能为空")
+ @Size(max = 20, message = "归属类型长度不能超过 20 个字符")
+ private String ownershipType;
+
+ @Schema(description = "产值计算方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "指导价法")
+ @NotBlank(message = "产值计算方式不能为空")
+ @Size(max = 30, message = "产值计算方式长度不能超过 30 个字符")
+ private String calculationMethod;
+
+ @Schema(description = "规划内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "建筑设计")
+ @NotBlank(message = "规划内容不能为空")
+ @Size(max = 255, message = "规划内容长度不能超过 255 个字符")
+ private String planningContent;
+
+ @Schema(description = "规划金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "500000")
+ @NotNull(message = "规划金额不能为空")
+ private BigDecimal planningAmount;
+
+ @Schema(description = "管理费费率", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.0500")
+ @NotNull(message = "管理费费率不能为空")
+ private BigDecimal managementFeeRate;
+
+ @Schema(description = "实施团队", example = "建筑一所")
+ @Size(max = 100, message = "实施团队长度不能超过 100 个字符")
+ private String implementationTeam;
+
+ @Schema(description = "开始年度", example = "2026")
+ private Integer planningStartYear;
+
+ @Schema(description = "面积", example = "30000")
+ private BigDecimal planningArea;
+
+ @Schema(description = "设计阶段", example = "方案设计")
+ @Size(max = 50, message = "设计阶段长度不能超过 50 个字符")
+ private String designStage;
+
+ @Schema(description = "本次设计阶段比例", example = "0.3000")
+ private BigDecimal currentDesignStageRatio;
+
+ @Schema(description = "审核审定是否外包", example = "false")
+ private Boolean reviewOutsourceFlag;
+
+ @Schema(description = "审核审定占比", example = "0.0600")
+ private BigDecimal reviewOutsourceRatio;
+
+ @Schema(description = "总分配比例", example = "1.0000")
+ private BigDecimal totalDistributionAmount;
+
+ @Schema(description = "Planning progress remark", example = "Q1 extraction arrangement")
+ @Size(max = 500, message = "Progress remark length must be within 500 characters")
+ private String progressRemark;
+
+ @Schema(description = "楼栋数或户型数", example = "10")
+ private Integer buildingOrUnitCount;
+
+ @Schema(description = "套图系数,保留两位小数", example = "1.00")
+ private BigDecimal drawingSetFactor;
+
+ @Schema(description = "规模系数,保留两位小数", example = "1.00")
+ private BigDecimal scaleFactor;
+
+ @Schema(description = "修改系数,保留两位小数", example = "1.00")
+ private BigDecimal modificationFactor;
+
+ @Schema(description = "复杂系数/复杂等级,按比例值存储(100%=1.0000)", example = "1.0000")
+ private BigDecimal complexityFactor;
+
+ @Schema(description = "内部指导单价(元/㎡)", example = "80.00")
+ private BigDecimal internalGuidanceUnitPrice;
+
+ @Schema(description = "虚拟产值计算方式", example = "工日法")
+ @Size(max = 30, message = "虚拟产值计算方式长度不能超过 30 个字符")
+ private String virtualCalculationMethod;
+
+ @Schema(description = "工日", example = "100")
+ private BigDecimal workingDayCount;
+
+ @Schema(description = "工日单价", example = "1000.00")
+ private BigDecimal workingDayUnitPrice;
+
+ @Schema(description = "指导单价", example = "88.00")
+ private BigDecimal guidanceUnitPrice;
+
+ @Schema(description = "指导总价", example = "880000.00")
+ private BigDecimal guidanceTotalPrice;
+
+ @Schema(description = "虚拟总价", example = "600000.00")
+ private BigDecimal virtualTotalPrice;
+
+ @Schema(description = "产值计算比例", example = "0.0800")
+ private BigDecimal calculationRatio;
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planningquarter/ProjectPlanningQuarterController.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planningquarter/ProjectPlanningQuarterController.java
new file mode 100644
index 0000000..94f2410
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planningquarter/ProjectPlanningQuarterController.java
@@ -0,0 +1,83 @@
+package cn.iocoder.lyzsys.module.tjt.controller.admin.planningquarter;
+
+import cn.iocoder.lyzsys.framework.common.pojo.CommonResult;
+import cn.iocoder.lyzsys.framework.common.util.object.BeanUtils;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.planningquarter.vo.ProjectPlanningQuarterRespVO;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.planningquarter.vo.ProjectPlanningQuarterSaveReqVO;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.planningquarter.ProjectPlanningQuarterDO;
+import cn.iocoder.lyzsys.module.tjt.service.planningquarter.ProjectPlanningQuarterService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.List;
+
+import static cn.iocoder.lyzsys.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 特建投季度分配")
+@RestController
+@RequestMapping("/tjt/planning-quarter")
+@Validated
+public class ProjectPlanningQuarterController {
+
+ @Resource
+ private ProjectPlanningQuarterService projectPlanningQuarterService;
+
+ @PostMapping("/create")
+ @Operation(summary = "创建季度分配")
+ @PreAuthorize("@ss.hasPermission('tjt:planning-quarter:create')")
+ public CommonResult createProjectPlanningQuarter(@Valid @RequestBody ProjectPlanningQuarterSaveReqVO createReqVO) {
+ return success(projectPlanningQuarterService.createProjectPlanningQuarter(createReqVO));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "修改季度分配")
+ @PreAuthorize("@ss.hasPermission('tjt:planning-quarter:update')")
+ public CommonResult updateProjectPlanningQuarter(@Valid @RequestBody ProjectPlanningQuarterSaveReqVO updateReqVO) {
+ projectPlanningQuarterService.updateProjectPlanningQuarter(updateReqVO);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "删除季度分配")
+ @Parameter(name = "id", description = "编号", required = true, example = "1")
+ @PreAuthorize("@ss.hasPermission('tjt:planning-quarter:delete')")
+ public CommonResult deleteProjectPlanningQuarter(@RequestParam("id") Long id) {
+ projectPlanningQuarterService.deleteProjectPlanningQuarter(id);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete-list")
+ @Operation(summary = "批量删除季度分配")
+ @Parameter(name = "ids", description = "编号列表", required = true)
+ @PreAuthorize("@ss.hasPermission('tjt:planning-quarter:delete')")
+ public CommonResult deleteProjectPlanningQuarterList(@RequestParam("ids") List ids) {
+ projectPlanningQuarterService.deleteProjectPlanningQuarterList(ids);
+ return success(true);
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "获得季度分配详情")
+ @Parameter(name = "id", description = "编号", required = true, example = "1")
+ @PreAuthorize("@ss.hasPermission('tjt:planning-quarter:query')")
+ public CommonResult getProjectPlanningQuarter(@RequestParam("id") Long id) {
+ ProjectPlanningQuarterDO quarter = projectPlanningQuarterService.getProjectPlanningQuarter(id);
+ return success(BeanUtils.toBean(quarter, ProjectPlanningQuarterRespVO.class));
+ }
+
+ @GetMapping("/list-by-planning")
+ @Operation(summary = "根据合约规划获得季度分配列表")
+ @Parameter(name = "planningId", description = "合约规划 ID", required = true, example = "1")
+ @PreAuthorize("@ss.hasPermission('tjt:planning-quarter:query')")
+ public CommonResult> getProjectPlanningQuarterListByPlanningId(
+ @RequestParam("planningId") Long planningId) {
+ List list = projectPlanningQuarterService.getProjectPlanningQuarterListByPlanningId(planningId);
+ return success(BeanUtils.toBean(list, ProjectPlanningQuarterRespVO.class));
+ }
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planningquarter/vo/ProjectPlanningQuarterRespVO.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planningquarter/vo/ProjectPlanningQuarterRespVO.java
new file mode 100644
index 0000000..2243d68
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planningquarter/vo/ProjectPlanningQuarterRespVO.java
@@ -0,0 +1,37 @@
+package cn.iocoder.lyzsys.module.tjt.controller.admin.planningquarter.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 季度分配 Response VO")
+@Data
+public class ProjectPlanningQuarterRespVO {
+
+ @Schema(description = "季度分配 ID", example = "1")
+ private Long id;
+
+ @Schema(description = "合约规划 ID", example = "1")
+ private Long planningId;
+
+ @Schema(description = "分配年度")
+ private Integer distributionYear;
+
+ @Schema(description = "季度")
+ private Integer quarterNo;
+
+ @Schema(description = "分配比例")
+ private BigDecimal distributionRatio;
+
+ @Schema(description = "分配金额")
+ private BigDecimal distributionAmount;
+
+ @Schema(description = "提取进度备注")
+ private String progressRemark;
+
+ @Schema(description = "创建时间")
+ private LocalDateTime createTime;
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planningquarter/vo/ProjectPlanningQuarterSaveReqVO.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planningquarter/vo/ProjectPlanningQuarterSaveReqVO.java
new file mode 100644
index 0000000..8cfc307
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/planningquarter/vo/ProjectPlanningQuarterSaveReqVO.java
@@ -0,0 +1,40 @@
+package cn.iocoder.lyzsys.module.tjt.controller.admin.planningquarter.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+import java.math.BigDecimal;
+
+@Schema(description = "管理后台 - 季度分配新增/修改 Request VO")
+@Data
+public class ProjectPlanningQuarterSaveReqVO {
+
+ @Schema(description = "季度分配 ID", example = "1")
+ private Long id;
+
+ @Schema(description = "合约规划 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "合约规划 ID 不能为空")
+ private Long planningId;
+
+ @Schema(description = "分配年度", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026")
+ @NotNull(message = "分配年度不能为空")
+ private Integer distributionYear;
+
+ @Schema(description = "季度", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "季度不能为空")
+ @Min(value = 1, message = "季度必须在 1 到 4 之间")
+ @Max(value = 4, message = "季度必须在 1 到 4 之间")
+ private Integer quarterNo;
+
+ @Schema(description = "分配比例", example = "0.2500")
+ private BigDecimal distributionRatio;
+
+ @Schema(description = "提取进度备注", example = "Q1 提取")
+ @Size(max = 500, message = "提取进度备注长度不能超过 500 个字符")
+ private String progressRemark;
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/profit/ProjectProfitController.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/profit/ProjectProfitController.java
new file mode 100644
index 0000000..1404bb6
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/profit/ProjectProfitController.java
@@ -0,0 +1,47 @@
+package cn.iocoder.lyzsys.module.tjt.controller.admin.profit;
+
+import cn.iocoder.lyzsys.framework.common.pojo.CommonResult;
+import cn.iocoder.lyzsys.framework.common.pojo.PageResult;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.profit.vo.ProjectProfitPageReqVO;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.profit.vo.ProjectProfitRespVO;
+import cn.iocoder.lyzsys.module.tjt.service.profit.ProjectProfitService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+
+import static cn.iocoder.lyzsys.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 特建投项目盈亏")
+@RestController
+@RequestMapping("/tjt/profit")
+@Validated
+public class ProjectProfitController {
+
+ @Resource
+ private ProjectProfitService projectProfitService;
+
+ @GetMapping("/get")
+ @Operation(summary = "获得项目盈亏详情")
+ @Parameter(name = "projectId", description = "项目 ID", required = true, example = "1")
+ @PreAuthorize("@ss.hasPermission('tjt:profit:query')")
+ public CommonResult getProjectProfit(@RequestParam("projectId") Long projectId) {
+ return success(projectProfitService.getProjectProfit(projectId));
+ }
+
+ @GetMapping("/page")
+ @Operation(summary = "获得项目盈亏分页")
+ @PreAuthorize("@ss.hasPermission('tjt:profit:query')")
+ public CommonResult> getProjectProfitPage(@Valid ProjectProfitPageReqVO pageReqVO) {
+ return success(projectProfitService.getProjectProfitPage(pageReqVO));
+ }
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/profit/vo/ProjectProfitPageReqVO.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/profit/vo/ProjectProfitPageReqVO.java
new file mode 100644
index 0000000..52d619b
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/profit/vo/ProjectProfitPageReqVO.java
@@ -0,0 +1,31 @@
+package cn.iocoder.lyzsys.module.tjt.controller.admin.profit.vo;
+
+import cn.iocoder.lyzsys.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.lyzsys.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 项目盈亏分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ProjectProfitPageReqVO extends PageParam {
+
+ @Schema(description = "工程名称,模糊匹配", example = "设计")
+ private String projectName;
+
+ @Schema(description = "是否签订合同", example = "true")
+ private Boolean contractSignedFlag;
+
+ @Schema(description = "项目开始年度", example = "2026")
+ private Integer projectStartYear;
+
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ @Schema(description = "创建时间")
+ private LocalDateTime[] createTime;
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/profit/vo/ProjectProfitRespVO.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/profit/vo/ProjectProfitRespVO.java
new file mode 100644
index 0000000..f0a8ff2
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/profit/vo/ProjectProfitRespVO.java
@@ -0,0 +1,55 @@
+package cn.iocoder.lyzsys.module.tjt.controller.admin.profit.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 项目盈亏 Response VO")
+@Data
+public class ProjectProfitRespVO {
+
+ @Schema(description = "项目 ID", example = "1")
+ private Long projectId;
+
+ @Schema(description = "工程名称")
+ private String projectName;
+
+ @Schema(description = "是否签订合同")
+ private Boolean contractSignedFlag;
+
+ @Schema(description = "合同金额")
+ private BigDecimal contractAmount;
+
+ @Schema(description = "最终结算金额")
+ private BigDecimal finalSettlementAmount;
+
+ @Schema(description = "综合所协作金额")
+ private BigDecimal comprehensivePlanningAmount;
+
+ @Schema(description = "专业分包金额")
+ private BigDecimal subcontractPlanningAmount;
+
+ @Schema(description = "专业所产值")
+ private BigDecimal majorOutputValue;
+
+ @Schema(description = "预计 K 值")
+ private BigDecimal expectedKValue;
+
+ @Schema(description = "专业所预计绩效")
+ private BigDecimal majorExpectedPerformance;
+
+ @Schema(description = "盈亏值")
+ private BigDecimal profitLossValue;
+
+ @Schema(description = "盈亏百分比")
+ private BigDecimal profitLossRate;
+
+ @Schema(description = "项目开始年度")
+ private Integer projectStartYear;
+
+ @Schema(description = "创建时间")
+ private LocalDateTime createTime;
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/project/ProjectController.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/project/ProjectController.java
new file mode 100644
index 0000000..f751d12
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/project/ProjectController.java
@@ -0,0 +1,82 @@
+package cn.iocoder.lyzsys.module.tjt.controller.admin.project;
+
+import cn.iocoder.lyzsys.framework.common.pojo.CommonResult;
+import cn.iocoder.lyzsys.framework.common.pojo.PageResult;
+import cn.iocoder.lyzsys.framework.common.util.object.BeanUtils;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.project.vo.ProjectPageReqVO;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.project.vo.ProjectRespVO;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.project.vo.ProjectSaveReqVO;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.project.ProjectDO;
+import cn.iocoder.lyzsys.module.tjt.service.project.ProjectService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.List;
+
+import static cn.iocoder.lyzsys.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 特建投项目")
+@RestController
+@RequestMapping("/tjt/project")
+@Validated
+public class ProjectController {
+
+ @Resource
+ private ProjectService projectService;
+
+ @PostMapping("/create")
+ @Operation(summary = "创建项目")
+ @PreAuthorize("@ss.hasPermission('tjt:project:create')")
+ public CommonResult createProject(@Valid @RequestBody ProjectSaveReqVO createReqVO) {
+ return success(projectService.createProject(createReqVO));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "修改项目")
+ @PreAuthorize("@ss.hasPermission('tjt:project:update')")
+ public CommonResult updateProject(@Valid @RequestBody ProjectSaveReqVO updateReqVO) {
+ projectService.updateProject(updateReqVO);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "删除项目")
+ @Parameter(name = "id", description = "编号", required = true, example = "1")
+ @PreAuthorize("@ss.hasPermission('tjt:project:delete')")
+ public CommonResult deleteProject(@RequestParam("id") Long id) {
+ projectService.deleteProject(id);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete-list")
+ @Operation(summary = "批量删除项目")
+ @Parameter(name = "ids", description = "编号列表", required = true)
+ @PreAuthorize("@ss.hasPermission('tjt:project:delete')")
+ public CommonResult deleteProjectList(@RequestParam("ids") List ids) {
+ projectService.deleteProjectList(ids);
+ return success(true);
+ }
+
+ @GetMapping("/page")
+ @Operation(summary = "获得项目分页")
+ @PreAuthorize("@ss.hasPermission('tjt:project:query')")
+ public CommonResult> getProjectPage(@Valid ProjectPageReqVO pageReqVO) {
+ PageResult pageResult = projectService.getProjectPage(pageReqVO);
+ return success(BeanUtils.toBean(pageResult, ProjectRespVO.class));
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "获得项目详情")
+ @Parameter(name = "id", description = "编号", required = true, example = "1")
+ @PreAuthorize("@ss.hasPermission('tjt:project:query')")
+ public CommonResult getProject(@RequestParam("id") Long id) {
+ return success(BeanUtils.toBean(projectService.getProject(id), ProjectRespVO.class));
+ }
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/project/vo/ProjectPageReqVO.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/project/vo/ProjectPageReqVO.java
new file mode 100644
index 0000000..94a1ed0
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/project/vo/ProjectPageReqVO.java
@@ -0,0 +1,31 @@
+package cn.iocoder.lyzsys.module.tjt.controller.admin.project.vo;
+
+import cn.iocoder.lyzsys.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.lyzsys.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 项目分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ProjectPageReqVO extends PageParam {
+
+ @Schema(description = "工程名称,模糊匹配", example = "设计")
+ private String projectName;
+
+ @Schema(description = "是否签订合同", example = "true")
+ private Boolean contractSignedFlag;
+
+ @Schema(description = "项目开始年度", example = "2026")
+ private Integer projectStartYear;
+
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ @Schema(description = "创建时间")
+ private LocalDateTime[] createTime;
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/project/vo/ProjectRespVO.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/project/vo/ProjectRespVO.java
new file mode 100644
index 0000000..074262e
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/project/vo/ProjectRespVO.java
@@ -0,0 +1,85 @@
+package cn.iocoder.lyzsys.module.tjt.controller.admin.project.vo;
+
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
+import cn.idev.excel.annotation.ExcelProperty;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 项目 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class ProjectRespVO {
+
+ private static final String FORMAT_YEAR_MONTH_DAY = "yyyy-MM-dd";
+
+ @Schema(description = "项目 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @ExcelProperty("项目ID")
+ private Long id;
+
+ @Schema(description = "工程名称", requiredMode = Schema.RequiredMode.REQUIRED)
+ @ExcelProperty("工程名称")
+ private String projectName;
+
+ @Schema(description = "是否签订合同", requiredMode = Schema.RequiredMode.REQUIRED)
+ @ExcelProperty("是否签订合同")
+ private Boolean contractSignedFlag;
+
+ @Schema(description = "合同金额", requiredMode = Schema.RequiredMode.REQUIRED)
+ @ExcelProperty("合同金额")
+ private BigDecimal contractAmount;
+
+ @Schema(description = "工程总面积", requiredMode = Schema.RequiredMode.REQUIRED)
+ @ExcelProperty("工程总面积")
+ private BigDecimal totalConstructionArea;
+
+ @Schema(description = "建设单位")
+ @ExcelProperty("建设单位")
+ private String constructionUnitName;
+
+ @Schema(description = "联系人")
+ @ExcelProperty("联系人")
+ private String contactName;
+
+ @Schema(description = "联系方式")
+ @ExcelProperty("联系方式")
+ private String contactPhone;
+
+ @Schema(description = "合同签订日期")
+ @ExcelProperty("合同签订日期")
+ @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY)
+ private LocalDate contractSigningDate;
+
+ @Schema(description = "项目经理")
+ @ExcelProperty("项目经理")
+ private String projectManagerName;
+
+ @Schema(description = "工程负责人")
+ @ExcelProperty("工程负责人")
+ private String engineeringPrincipalName;
+
+ @Schema(description = "工程类型")
+ @ExcelProperty("工程类型")
+ private String projectType;
+
+ @Schema(description = "项目开始年度")
+ @ExcelProperty("项目开始年度")
+ private Integer projectStartYear;
+
+ @Schema(description = "最终结算金额")
+ @ExcelProperty("最终结算金额")
+ private BigDecimal finalSettlementAmount;
+
+ @Schema(description = "预计 K 值")
+ @ExcelProperty("预计 K 值")
+ private BigDecimal expectedKValue;
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ @ExcelProperty("创建时间")
+ private LocalDateTime createTime;
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/project/vo/ProjectSaveReqVO.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/project/vo/ProjectSaveReqVO.java
new file mode 100644
index 0000000..0ae08ad
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/project/vo/ProjectSaveReqVO.java
@@ -0,0 +1,79 @@
+package cn.iocoder.lyzsys.module.tjt.controller.admin.project.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+import static cn.iocoder.lyzsys.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
+
+@Schema(description = "管理后台 - 项目新增/修改 Request VO")
+@Data
+public class ProjectSaveReqVO {
+
+ @Schema(description = "项目 ID", example = "1")
+ private Long id;
+
+ @Schema(description = "工程名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "XX 设计项目")
+ @NotBlank(message = "工程名称不能为空")
+ @Size(max = 200, message = "工程名称长度不能超过 200 个字符")
+ private String projectName;
+
+ @Schema(description = "是否签订合同", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+ @NotNull(message = "是否签订合同不能为空")
+ private Boolean contractSignedFlag;
+
+ @Schema(description = "合同金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000000")
+ @NotNull(message = "合同金额不能为空")
+ private BigDecimal contractAmount;
+
+ @Schema(description = "工程总面积", requiredMode = Schema.RequiredMode.REQUIRED, example = "30000")
+ @NotNull(message = "工程总面积不能为空")
+ private BigDecimal totalConstructionArea;
+
+ @Schema(description = "建设单位", example = "XX 建设单位")
+ @Size(max = 200, message = "建设单位长度不能超过 200 个字符")
+ private String constructionUnitName;
+
+ @Schema(description = "联系人", example = "张三")
+ @Size(max = 64, message = "联系人长度不能超过 64 个字符")
+ private String contactName;
+
+ @Schema(description = "联系方式", example = "13800000000")
+ @Size(max = 32, message = "联系方式长度不能超过 32 个字符")
+ private String contactPhone;
+
+ @Schema(description = "合同签订日期", example = "2026-04-14")
+ @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY)
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
+ private LocalDate contractSigningDate;
+
+ @Schema(description = "项目经理", example = "李四")
+ @Size(max = 64, message = "项目经理长度不能超过 64 个字符")
+ private String projectManagerName;
+
+ @Schema(description = "工程负责人", example = "王五")
+ @Size(max = 64, message = "工程负责人长度不能超过 64 个字符")
+ private String engineeringPrincipalName;
+
+ @Schema(description = "工程类型", example = "住宅")
+ @Size(max = 50, message = "工程类型长度不能超过 50 个字符")
+ private String projectType;
+
+ @Schema(description = "项目开始年度", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026")
+ @NotNull(message = "项目开始年度不能为空")
+ private Integer projectStartYear;
+
+ @Schema(description = "最终结算金额", example = "1200000")
+ private BigDecimal finalSettlementAmount;
+
+ @Schema(description = "预计 K 值", example = "0.8500")
+ private BigDecimal expectedKValue;
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/specialtyrolesplit/SpecialtyRoleSplitController.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/specialtyrolesplit/SpecialtyRoleSplitController.java
new file mode 100644
index 0000000..a657e81
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/specialtyrolesplit/SpecialtyRoleSplitController.java
@@ -0,0 +1,45 @@
+package cn.iocoder.lyzsys.module.tjt.controller.admin.specialtyrolesplit;
+
+import cn.iocoder.lyzsys.framework.common.pojo.CommonResult;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.specialtyrolesplit.vo.SpecialtyRoleSplitBatchSaveReqVO;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.specialtyrolesplit.vo.SpecialtyRoleSplitRespVO;
+import cn.iocoder.lyzsys.module.tjt.service.specialtyrolesplit.SpecialtyRoleSplitService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.List;
+
+import static cn.iocoder.lyzsys.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 特建投页面5角色比例")
+@RestController
+@RequestMapping("/tjt/specialty-role-split")
+@Validated
+public class SpecialtyRoleSplitController {
+
+ @Resource
+ private SpecialtyRoleSplitService specialtyRoleSplitService;
+
+ @GetMapping("/list-by-planning")
+ @Operation(summary = "根据合约规划获得页面5角色比例列表")
+ @Parameter(name = "planningId", description = "合约规划 ID", required = true, example = "1")
+ @PreAuthorize("@ss.hasPermission('tjt:specialty-role-split:query')")
+ public CommonResult> getSpecialtyRoleSplitListByPlanningId(@RequestParam("planningId") Long planningId) {
+ return success(specialtyRoleSplitService.getSpecialtyRoleSplitListByPlanningId(planningId));
+ }
+
+ @PutMapping("/save-batch")
+ @Operation(summary = "批量保存页面5角色比例")
+ @PreAuthorize("@ss.hasPermission('tjt:specialty-role-split:update')")
+ public CommonResult saveSpecialtyRoleSplitBatch(@Valid @RequestBody SpecialtyRoleSplitBatchSaveReqVO reqVO) {
+ specialtyRoleSplitService.saveSpecialtyRoleSplitBatch(reqVO);
+ return success(true);
+ }
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/specialtyrolesplit/vo/SpecialtyRolePersonRespVO.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/specialtyrolesplit/vo/SpecialtyRolePersonRespVO.java
new file mode 100644
index 0000000..1e02e7f
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/specialtyrolesplit/vo/SpecialtyRolePersonRespVO.java
@@ -0,0 +1,21 @@
+package cn.iocoder.lyzsys.module.tjt.controller.admin.specialtyrolesplit.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Schema(description = "管理后台 - 页面5角色人员 Response VO")
+@Data
+public class SpecialtyRolePersonRespVO {
+
+ @Schema(description = "人员名称")
+ private String personName;
+
+ @Schema(description = "人员比例")
+ private BigDecimal personRatio;
+
+ @Schema(description = "人员金额")
+ private BigDecimal personAmount;
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/specialtyrolesplit/vo/SpecialtyRolePersonSaveReqVO.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/specialtyrolesplit/vo/SpecialtyRolePersonSaveReqVO.java
new file mode 100644
index 0000000..ca6104a
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/specialtyrolesplit/vo/SpecialtyRolePersonSaveReqVO.java
@@ -0,0 +1,18 @@
+package cn.iocoder.lyzsys.module.tjt.controller.admin.specialtyrolesplit.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Schema(description = "管理后台 - 页面5角色人员保存 Item Request VO")
+@Data
+public class SpecialtyRolePersonSaveReqVO {
+
+ @Schema(description = "人员名称", example = "张三")
+ private String personName;
+
+ @Schema(description = "人员比例", example = "0.1000")
+ private BigDecimal personRatio;
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/specialtyrolesplit/vo/SpecialtyRoleSplitBatchSaveReqVO.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/specialtyrolesplit/vo/SpecialtyRoleSplitBatchSaveReqVO.java
new file mode 100644
index 0000000..bb3f8c5
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/specialtyrolesplit/vo/SpecialtyRoleSplitBatchSaveReqVO.java
@@ -0,0 +1,24 @@
+package cn.iocoder.lyzsys.module.tjt.controller.admin.specialtyrolesplit.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+@Schema(description = "管理后台 - 页面5角色比例批量保存 Request VO")
+@Data
+public class SpecialtyRoleSplitBatchSaveReqVO {
+
+ @Schema(description = "合约规划 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "合约规划 ID 不能为空")
+ private Long planningId;
+
+ @Schema(description = "角色配置列表", requiredMode = Schema.RequiredMode.REQUIRED)
+ @NotEmpty(message = "角色配置列表不能为空")
+ @Valid
+ private List items;
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/specialtyrolesplit/vo/SpecialtyRoleSplitRespVO.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/specialtyrolesplit/vo/SpecialtyRoleSplitRespVO.java
new file mode 100644
index 0000000..dee9f2a
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/specialtyrolesplit/vo/SpecialtyRoleSplitRespVO.java
@@ -0,0 +1,55 @@
+package cn.iocoder.lyzsys.module.tjt.controller.admin.specialtyrolesplit.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+@Schema(description = "管理后台 - 页面5角色比例 Response VO")
+@Data
+public class SpecialtyRoleSplitRespVO {
+
+ @Schema(description = "角色比例 ID", example = "1")
+ private Long id;
+
+ @Schema(description = "页面4拆分 ID", example = "1")
+ private Long outputSplitId;
+
+ @Schema(description = "合同规划 ID", example = "1")
+ private Long planningId;
+
+ @Schema(description = "项目名称")
+ private String projectName;
+
+ @Schema(description = "规划内容")
+ private String planningContent;
+
+ @Schema(description = "专业编码")
+ private String specialtyCode;
+
+ @Schema(description = "专业名称")
+ private String specialtyName;
+
+ @Schema(description = "专业金额")
+ private BigDecimal specialtyAmount;
+
+ @Schema(description = "角色编码")
+ private String roleCode;
+
+ @Schema(description = "角色名称")
+ private String roleName;
+
+ @Schema(description = "角色比例")
+ private BigDecimal roleRatio;
+
+ @Schema(description = "角色金额")
+ private BigDecimal roleAmount;
+
+ @Schema(description = "人员配置列表")
+ private List persons;
+
+ @Schema(description = "排序号")
+ private Integer sortNo;
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/specialtyrolesplit/vo/SpecialtyRoleSplitSaveItemReqVO.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/specialtyrolesplit/vo/SpecialtyRoleSplitSaveItemReqVO.java
new file mode 100644
index 0000000..e9930f4
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/controller/admin/specialtyrolesplit/vo/SpecialtyRoleSplitSaveItemReqVO.java
@@ -0,0 +1,32 @@
+package cn.iocoder.lyzsys.module.tjt.controller.admin.specialtyrolesplit.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+import java.util.List;
+
+@Schema(description = "管理后台 - 页面5角色比例保存 Item Request VO")
+@Data
+public class SpecialtyRoleSplitSaveItemReqVO {
+
+ @Schema(description = "专业编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "arch")
+ @NotBlank(message = "专业编码不能为空")
+ private String specialtyCode;
+
+ @Schema(description = "角色编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "director")
+ @NotBlank(message = "角色编码不能为空")
+ private String roleCode;
+
+ @Schema(description = "角色比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.1000")
+ @NotNull(message = "角色比例不能为空")
+ private BigDecimal roleRatio;
+
+ @Schema(description = "人员配置列表")
+ @Valid
+ private List persons;
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/dataobject/outputsplit/ProjectOutputSplitDO.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/dataobject/outputsplit/ProjectOutputSplitDO.java
new file mode 100644
index 0000000..f139657
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/dataobject/outputsplit/ProjectOutputSplitDO.java
@@ -0,0 +1,52 @@
+package cn.iocoder.lyzsys.module.tjt.dal.dataobject.outputsplit;
+
+import cn.iocoder.lyzsys.framework.tenant.core.db.TenantBaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+/**
+ * 页面 4 拆分比例 DO
+ *
+ * @author Codex
+ */
+@TableName("tjt_project_output_split")
+@KeySequence("tjt_project_output_split_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ProjectOutputSplitDO extends TenantBaseDO {
+
+ @TableId
+ private Long id;
+
+ private Long projectId;
+
+ private Long planningId;
+
+ private Integer year;
+
+ private BigDecimal projectManagerRatio;
+
+ private BigDecimal engineeringLeaderRatio;
+
+ private BigDecimal officeRatio;
+
+ private BigDecimal archRatio;
+
+ private BigDecimal decorRatio;
+
+ private BigDecimal structRatio;
+
+ private BigDecimal waterRatio;
+
+ private BigDecimal elecRatio;
+
+ private BigDecimal hvacRatio;
+
+ private BigDecimal digitalRatio;
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/dataobject/planning/ProjectPlanningDO.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/dataobject/planning/ProjectPlanningDO.java
new file mode 100644
index 0000000..1c8771c
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/dataobject/planning/ProjectPlanningDO.java
@@ -0,0 +1,94 @@
+package cn.iocoder.lyzsys.module.tjt.dal.dataobject.planning;
+
+import cn.iocoder.lyzsys.framework.tenant.core.db.TenantBaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+/**
+ * 合约规划 DO
+ *
+ * @author Codex
+ */
+@TableName("tjt_project_planning")
+@KeySequence("tjt_project_planning_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ProjectPlanningDO extends TenantBaseDO {
+
+ @TableId
+ private Long id;
+
+ private Long projectId;
+
+ private String ownershipType;
+
+ private String calculationMethod;
+
+ private String planningContent;
+
+ private BigDecimal planningAmount;
+
+ private BigDecimal managementFeeRate;
+
+ private BigDecimal managementFee;
+
+ private String implementationTeam;
+
+ private Integer planningStartYear;
+
+ private BigDecimal planningArea;
+
+ private String designStage;
+
+ private BigDecimal currentDesignStageRatio;
+
+ private Boolean reviewOutsourceFlag;
+
+ private BigDecimal reviewOutsourceRatio;
+
+ private BigDecimal totalDistributionAmount;
+
+ private String progressRemark;
+
+ private Integer buildingOrUnitCount;
+
+ private BigDecimal drawingSetFactor;
+
+ private BigDecimal scaleFactor;
+
+ private BigDecimal modificationFactor;
+
+ private BigDecimal complexityFactor;
+
+ private BigDecimal internalGuidanceUnitPrice;
+
+ private String virtualCalculationMethod;
+
+ private BigDecimal workingDayCount;
+
+ private BigDecimal workingDayUnitPrice;
+
+ private BigDecimal guidanceUnitPrice;
+
+ private BigDecimal guidanceTotalPrice;
+
+ private BigDecimal virtualTotalPrice;
+
+ private BigDecimal calculationRatio;
+
+ private BigDecimal contractUnitPrice;
+
+ private BigDecimal totalAdjustmentFactor;
+
+ private BigDecimal assessmentArea;
+
+ private BigDecimal virtualOutputValue;
+
+ private BigDecimal assessmentOutputValue;
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/dataobject/planningquarter/ProjectPlanningQuarterDO.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/dataobject/planningquarter/ProjectPlanningQuarterDO.java
new file mode 100644
index 0000000..0698c14
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/dataobject/planningquarter/ProjectPlanningQuarterDO.java
@@ -0,0 +1,36 @@
+package cn.iocoder.lyzsys.module.tjt.dal.dataobject.planningquarter;
+
+import cn.iocoder.lyzsys.framework.tenant.core.db.TenantBaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+/**
+ * 合约规划季度分配 DO
+ *
+ * @author Codex
+ */
+@TableName("tjt_project_planning_quarter")
+@KeySequence("tjt_project_planning_quarter_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ProjectPlanningQuarterDO extends TenantBaseDO {
+
+ @TableId
+ private Long id;
+
+ private Long planningId;
+
+ private Integer distributionYear;
+
+ private Integer quarterNo;
+
+ private BigDecimal distributionRatio;
+
+ private BigDecimal distributionAmount;
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/dataobject/project/ProjectDO.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/dataobject/project/ProjectDO.java
new file mode 100644
index 0000000..b0cf649
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/dataobject/project/ProjectDO.java
@@ -0,0 +1,86 @@
+package cn.iocoder.lyzsys.module.tjt.dal.dataobject.project;
+
+import cn.iocoder.lyzsys.framework.tenant.core.db.TenantBaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+/**
+ * 项目 DO
+ *
+ * @author Codex
+ */
+@TableName("tjt_project")
+@KeySequence("tjt_project_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ProjectDO extends TenantBaseDO {
+
+ /**
+ * 主键 ID
+ */
+ @TableId
+ private Long id;
+ /**
+ * 工程名称
+ */
+ private String projectName;
+ /**
+ * 是否签订合同
+ */
+ private Boolean contractSignedFlag;
+ /**
+ * 合同金额
+ */
+ private BigDecimal contractAmount;
+ /**
+ * 工程总面积
+ */
+ private BigDecimal totalConstructionArea;
+ /**
+ * 建设单位
+ */
+ private String constructionUnitName;
+ /**
+ * 联系人
+ */
+ private String contactName;
+ /**
+ * 联系方式
+ */
+ private String contactPhone;
+ /**
+ * 合同签订日期
+ */
+ private LocalDate contractSigningDate;
+ /**
+ * 项目经理
+ */
+ private String projectManagerName;
+ /**
+ * 工程负责人
+ */
+ private String engineeringPrincipalName;
+ /**
+ * 工程类型
+ */
+ private String projectType;
+ /**
+ * 项目开始年度
+ */
+ private Integer projectStartYear;
+ /**
+ * 最终结算金额
+ */
+ private BigDecimal finalSettlementAmount;
+ /**
+ * 预计 K 值
+ */
+ private BigDecimal expectedKValue;
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/dataobject/specialtyrolesplit/SpecialtyRoleSplitDO.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/dataobject/specialtyrolesplit/SpecialtyRoleSplitDO.java
new file mode 100644
index 0000000..d1c1f5e
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/dataobject/specialtyrolesplit/SpecialtyRoleSplitDO.java
@@ -0,0 +1,42 @@
+package cn.iocoder.lyzsys.module.tjt.dal.dataobject.specialtyrolesplit;
+
+import cn.iocoder.lyzsys.framework.tenant.core.db.TenantBaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+/**
+ * 页面 5 角色比例 DO
+ *
+ * @author Codex
+ */
+@TableName("tjt_specialty_role_split")
+@KeySequence("tjt_specialty_role_split_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class SpecialtyRoleSplitDO extends TenantBaseDO {
+
+ @TableId
+ private Long id;
+
+ private Long outputSplitId;
+
+ private String specialtyCode;
+
+ private String specialtyName;
+
+ private String roleCode;
+
+ private String roleName;
+
+ private BigDecimal roleRatio;
+
+ private String personNames;
+
+ private Integer sortNo;
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/mysql/outputsplit/ProjectOutputSplitMapper.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/mysql/outputsplit/ProjectOutputSplitMapper.java
new file mode 100644
index 0000000..419f8ad
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/mysql/outputsplit/ProjectOutputSplitMapper.java
@@ -0,0 +1,29 @@
+package cn.iocoder.lyzsys.module.tjt.dal.mysql.outputsplit;
+
+import cn.iocoder.lyzsys.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.lyzsys.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.outputsplit.ProjectOutputSplitDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 页面 4 拆分比例 Mapper
+ *
+ * @author Codex
+ */
+@Mapper
+public interface ProjectOutputSplitMapper extends BaseMapperX {
+
+ default ProjectOutputSplitDO selectByPlanningId(Long planningId) {
+ return selectOne(new LambdaQueryWrapperX()
+ .eq(ProjectOutputSplitDO::getPlanningId, planningId));
+ }
+
+ default List selectListByPlanningIds(Collection planningIds) {
+ return selectList(new LambdaQueryWrapperX()
+ .inIfPresent(ProjectOutputSplitDO::getPlanningId, planningIds));
+ }
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/mysql/planning/ProjectPlanningMapper.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/mysql/planning/ProjectPlanningMapper.java
new file mode 100644
index 0000000..7718c89
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/mysql/planning/ProjectPlanningMapper.java
@@ -0,0 +1,37 @@
+package cn.iocoder.lyzsys.module.tjt.dal.mysql.planning;
+
+import cn.iocoder.lyzsys.framework.common.pojo.PageResult;
+import cn.iocoder.lyzsys.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.lyzsys.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.planning.vo.ProjectPlanningPageReqVO;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.planning.ProjectPlanningDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * 合约规划 Mapper
+ *
+ * @author Codex
+ */
+@Mapper
+public interface ProjectPlanningMapper extends BaseMapperX {
+
+ default PageResult selectPage(ProjectPlanningPageReqVO reqVO) {
+ return selectPage(reqVO, new LambdaQueryWrapperX()
+ .eqIfPresent(ProjectPlanningDO::getProjectId, reqVO.getProjectId())
+ .eqIfPresent(ProjectPlanningDO::getOwnershipType, reqVO.getOwnershipType())
+ .eqIfPresent(ProjectPlanningDO::getCalculationMethod, reqVO.getCalculationMethod())
+ .likeIfPresent(ProjectPlanningDO::getPlanningContent, reqVO.getPlanningContent())
+ .eqIfPresent(ProjectPlanningDO::getPlanningStartYear, reqVO.getPlanningStartYear())
+ .betweenIfPresent(ProjectPlanningDO::getCreateTime, reqVO.getCreateTime())
+ .orderByDesc(ProjectPlanningDO::getId));
+ }
+
+ default List selectListByProjectId(Long projectId) {
+ return selectList(new LambdaQueryWrapperX()
+ .eq(ProjectPlanningDO::getProjectId, projectId)
+ .orderByDesc(ProjectPlanningDO::getId));
+ }
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/mysql/planningquarter/ProjectPlanningQuarterMapper.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/mysql/planningquarter/ProjectPlanningQuarterMapper.java
new file mode 100644
index 0000000..526b8b8
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/mysql/planningquarter/ProjectPlanningQuarterMapper.java
@@ -0,0 +1,47 @@
+package cn.iocoder.lyzsys.module.tjt.dal.mysql.planningquarter;
+
+import cn.iocoder.lyzsys.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.lyzsys.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.planningquarter.ProjectPlanningQuarterDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 合约规划季度分配 Mapper
+ *
+ * @author Codex
+ */
+@Mapper
+public interface ProjectPlanningQuarterMapper extends BaseMapperX {
+
+ default ProjectPlanningQuarterDO selectByPlanningIdAndDistributionYearAndQuarter(Long planningId,
+ Integer distributionYear,
+ Integer quarterNo) {
+ return selectOne(ProjectPlanningQuarterDO::getPlanningId, planningId,
+ ProjectPlanningQuarterDO::getDistributionYear, distributionYear,
+ ProjectPlanningQuarterDO::getQuarterNo, quarterNo);
+ }
+
+ default List selectListByPlanningId(Long planningId) {
+ return selectList(new LambdaQueryWrapperX()
+ .eq(ProjectPlanningQuarterDO::getPlanningId, planningId)
+ .orderByAsc(ProjectPlanningQuarterDO::getDistributionYear)
+ .orderByAsc(ProjectPlanningQuarterDO::getQuarterNo)
+ .orderByAsc(ProjectPlanningQuarterDO::getId));
+ }
+
+ default List selectListByPlanningIds(Collection planningIds) {
+ if (planningIds == null || planningIds.isEmpty()) {
+ return Collections.emptyList();
+ }
+ return selectList(new LambdaQueryWrapperX()
+ .in(ProjectPlanningQuarterDO::getPlanningId, planningIds)
+ .orderByAsc(ProjectPlanningQuarterDO::getDistributionYear)
+ .orderByAsc(ProjectPlanningQuarterDO::getQuarterNo)
+ .orderByAsc(ProjectPlanningQuarterDO::getId));
+ }
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/mysql/project/ProjectMapper.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/mysql/project/ProjectMapper.java
new file mode 100644
index 0000000..5eac064
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/mysql/project/ProjectMapper.java
@@ -0,0 +1,27 @@
+package cn.iocoder.lyzsys.module.tjt.dal.mysql.project;
+
+import cn.iocoder.lyzsys.framework.common.pojo.PageResult;
+import cn.iocoder.lyzsys.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.lyzsys.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.project.vo.ProjectPageReqVO;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.project.ProjectDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 项目 Mapper
+ *
+ * @author Codex
+ */
+@Mapper
+public interface ProjectMapper extends BaseMapperX {
+
+ default PageResult selectPage(ProjectPageReqVO reqVO) {
+ return selectPage(reqVO, new LambdaQueryWrapperX()
+ .likeIfPresent(ProjectDO::getProjectName, reqVO.getProjectName())
+ .eqIfPresent(ProjectDO::getContractSignedFlag, reqVO.getContractSignedFlag())
+ .eqIfPresent(ProjectDO::getProjectStartYear, reqVO.getProjectStartYear())
+ .betweenIfPresent(ProjectDO::getCreateTime, reqVO.getCreateTime())
+ .orderByDesc(ProjectDO::getId));
+ }
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/mysql/specialtyrolesplit/SpecialtyRoleSplitMapper.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/mysql/specialtyrolesplit/SpecialtyRoleSplitMapper.java
new file mode 100644
index 0000000..1a12a9f
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/dal/mysql/specialtyrolesplit/SpecialtyRoleSplitMapper.java
@@ -0,0 +1,25 @@
+package cn.iocoder.lyzsys.module.tjt.dal.mysql.specialtyrolesplit;
+
+import cn.iocoder.lyzsys.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.lyzsys.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.specialtyrolesplit.SpecialtyRoleSplitDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * 页面 5 角色比例 Mapper
+ *
+ * @author Codex
+ */
+@Mapper
+public interface SpecialtyRoleSplitMapper extends BaseMapperX {
+
+ default List selectListByOutputSplitId(Long outputSplitId) {
+ return selectList(new LambdaQueryWrapperX()
+ .eq(SpecialtyRoleSplitDO::getOutputSplitId, outputSplitId)
+ .orderByAsc(SpecialtyRoleSplitDO::getSortNo)
+ .orderByAsc(SpecialtyRoleSplitDO::getId));
+ }
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/enums/ErrorCodeConstants.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/enums/ErrorCodeConstants.java
new file mode 100644
index 0000000..0d085fe
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/enums/ErrorCodeConstants.java
@@ -0,0 +1,43 @@
+package cn.iocoder.lyzsys.module.tjt.enums;
+
+import cn.iocoder.lyzsys.framework.common.exception.ErrorCode;
+
+/**
+ * 特建投模块错误码
+ *
+ * @author Codex
+ */
+public interface ErrorCodeConstants {
+
+ // ========== 项目管理 1-020-001-000 ==========
+ ErrorCode PROJECT_NOT_EXISTS = new ErrorCode(1_020_001_000, "项目不存在");
+
+ // ========== 合约规划管理 1-020-002-000 ==========
+ ErrorCode PROJECT_PLANNING_NOT_EXISTS = new ErrorCode(1_020_002_000, "合约规划不存在");
+ ErrorCode PROJECT_PLANNING_PROJECT_NOT_EXISTS = new ErrorCode(1_020_002_001, "关联项目不存在");
+ ErrorCode PROJECT_PLANNING_OWNERSHIP_TYPE_INVALID = new ErrorCode(1_020_002_002, "归属类型不正确");
+ ErrorCode PROJECT_PLANNING_CALCULATION_METHOD_INVALID = new ErrorCode(1_020_002_003, "产值计算方式不正确");
+ ErrorCode PROJECT_PLANNING_OWNERSHIP_TYPE_IMMUTABLE = new ErrorCode(1_020_002_004, "归属类型保存后不允许修改");
+ ErrorCode PROJECT_PLANNING_CALCULATION_METHOD_IMMUTABLE = new ErrorCode(1_020_002_005, "产值计算方式保存后不允许修改");
+ ErrorCode PROJECT_PLANNING_VIRTUAL_CALCULATION_METHOD_INVALID = new ErrorCode(1_020_002_006, "虚拟产值计算方式不正确");
+
+ // ========== 季度分配管理 1-020-003-000 ==========
+ ErrorCode PROJECT_PLANNING_QUARTER_NOT_EXISTS = new ErrorCode(1_020_003_000, "季度分配明细不存在");
+ ErrorCode PROJECT_PLANNING_QUARTER_DUPLICATE = new ErrorCode(1_020_003_001, "同一合约规划下该年度季度分配已存在");
+ ErrorCode PROJECT_PLANNING_QUARTER_PLANNING_NOT_EXISTS = new ErrorCode(1_020_003_002, "关联合约规划不存在");
+
+ // ========== 页面 4 拆分管理 1-020-004-000 ==========
+ ErrorCode PROJECT_OUTPUT_SPLIT_PLANNING_NOT_EXISTS = new ErrorCode(1_020_004_000, "关联合约规划不存在");
+ ErrorCode PROJECT_OUTPUT_SPLIT_NOT_MAJOR = new ErrorCode(1_020_004_001, "仅专业所记录允许进行页面4拆分");
+ ErrorCode PROJECT_OUTPUT_SPLIT_RATIO_INVALID = new ErrorCode(1_020_004_002, "页面4比例合计必须等于 100%");
+
+ // ========== 页面 5 角色拆分管理 1-020-005-000 ==========
+ ErrorCode SPECIALTY_ROLE_SPLIT_OUTPUT_SPLIT_NOT_EXISTS = new ErrorCode(1_020_005_000, "页面4拆分记录不存在");
+ ErrorCode SPECIALTY_ROLE_SPLIT_ROLE_RATIO_INVALID = new ErrorCode(1_020_005_001, "页面5五类角色比例合计必须等于 100%");
+ ErrorCode SPECIALTY_ROLE_SPLIT_SPECIALTY_INVALID = new ErrorCode(1_020_005_002, "专业编码不正确");
+ ErrorCode SPECIALTY_ROLE_SPLIT_ROLE_INVALID = new ErrorCode(1_020_005_003, "角色编码不正确");
+ ErrorCode SPECIALTY_ROLE_SPLIT_PERSON_INVALID = new ErrorCode(1_020_005_004, "页面5人员名称和比例必须同时填写");
+ ErrorCode SPECIALTY_ROLE_SPLIT_DESIGN_PERSON_REQUIRED = new ErrorCode(1_020_005_005, "页面5设计角色金额大于 0 时必须配置人员");
+ ErrorCode SPECIALTY_ROLE_SPLIT_PERSON_RATIO_INVALID = new ErrorCode(1_020_005_007, "页面5同一角色下人员比例之和不能大于 100%");
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/enums/OutputSplitBizConstants.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/enums/OutputSplitBizConstants.java
new file mode 100644
index 0000000..e95c97b
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/enums/OutputSplitBizConstants.java
@@ -0,0 +1,125 @@
+package cn.iocoder.lyzsys.module.tjt.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 页面 4 / 页面 5 业务常量
+ *
+ * @author Codex
+ */
+public final class OutputSplitBizConstants {
+
+ public static final String OWNERSHIP_TYPE_MAJOR = "专业所";
+
+ public static final String SPECIALTY_ARCH = "arch";
+ public static final String SPECIALTY_DECOR = "decor";
+ public static final String SPECIALTY_STRUCT = "struct";
+ public static final String SPECIALTY_WATER = "water";
+ public static final String SPECIALTY_ELEC = "elec";
+ public static final String SPECIALTY_HVAC = "hvac";
+ public static final String SPECIALTY_DIGITAL = "digital";
+
+ public static final String ROLE_DIRECTOR = "director";
+ public static final String ROLE_CHECK = "check";
+ public static final String ROLE_REVIEW = "review";
+ public static final String ROLE_APPROVE = "approve";
+ public static final String ROLE_DESIGN = "design";
+
+ public static final List SPECIALTY_ITEMS = Arrays.asList(
+ new SpecialtyItem(SPECIALTY_ARCH, "建筑", 1),
+ new SpecialtyItem(SPECIALTY_DECOR, "装修", 2),
+ new SpecialtyItem(SPECIALTY_STRUCT, "结构", 3),
+ new SpecialtyItem(SPECIALTY_WATER, "水", 4),
+ new SpecialtyItem(SPECIALTY_ELEC, "电气", 5),
+ new SpecialtyItem(SPECIALTY_HVAC, "暖通", 6),
+ new SpecialtyItem(SPECIALTY_DIGITAL, "数字化设计", 7)
+ );
+
+ public static final List ROLE_ITEMS = Arrays.asList(
+ new RoleItem(ROLE_DIRECTOR, "专业负责人", 1),
+ new RoleItem(ROLE_CHECK, "校对", 2),
+ new RoleItem(ROLE_REVIEW, "审核", 3),
+ new RoleItem(ROLE_APPROVE, "审定", 4),
+ new RoleItem(ROLE_DESIGN, "设计", 5)
+ );
+
+ private OutputSplitBizConstants() {
+ }
+
+ public static boolean isMajorOwnershipType(String value) {
+ return OWNERSHIP_TYPE_MAJOR.equals(value);
+ }
+
+ public static boolean isValidSpecialtyCode(String code) {
+ return SPECIALTY_ITEMS.stream().anyMatch(item -> item.getCode().equals(code));
+ }
+
+ public static boolean isValidRoleCode(String code) {
+ return ROLE_ITEMS.stream().anyMatch(item -> item.getCode().equals(code));
+ }
+
+ public static String getSpecialtyName(String code) {
+ return SPECIALTY_ITEMS.stream()
+ .filter(item -> item.getCode().equals(code))
+ .findFirst()
+ .map(SpecialtyItem::getName)
+ .orElse(code);
+ }
+
+ public static String getRoleName(String code) {
+ return ROLE_ITEMS.stream()
+ .filter(item -> item.getCode().equals(code))
+ .findFirst()
+ .map(RoleItem::getName)
+ .orElse(code);
+ }
+
+ public static int getRoleSortNo(String code) {
+ return ROLE_ITEMS.stream()
+ .filter(item -> item.getCode().equals(code))
+ .findFirst()
+ .map(RoleItem::getSortNo)
+ .orElse(0);
+ }
+
+ public static int getSpecialtySortNo(String code) {
+ return SPECIALTY_ITEMS.stream()
+ .filter(item -> item.getCode().equals(code))
+ .findFirst()
+ .map(SpecialtyItem::getSortNo)
+ .orElse(0);
+ }
+
+ public static Map buildSpecialtyMap() {
+ Map map = new LinkedHashMap<>();
+ for (SpecialtyItem item : SPECIALTY_ITEMS) {
+ map.put(item.getCode(), item.getName());
+ }
+ return map;
+ }
+
+ @Getter
+ @AllArgsConstructor
+ public static class SpecialtyItem {
+
+ private final String code;
+ private final String name;
+ private final Integer sortNo;
+ }
+
+ @Getter
+ @AllArgsConstructor
+ public static class RoleItem {
+
+ private final String code;
+ private final String name;
+ private final Integer sortNo;
+ }
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/enums/ProjectPlanningBizTypeConstants.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/enums/ProjectPlanningBizTypeConstants.java
new file mode 100644
index 0000000..f77e8cd
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/enums/ProjectPlanningBizTypeConstants.java
@@ -0,0 +1,83 @@
+package cn.iocoder.lyzsys.module.tjt.enums;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * 合约规划业务类型常量
+ *
+ * @author Codex
+ */
+public final class ProjectPlanningBizTypeConstants {
+
+ public static final String OWNERSHIP_TYPE_MAJOR = "专业所";
+ public static final String OWNERSHIP_TYPE_COMPREHENSIVE = "综合所";
+ public static final String OWNERSHIP_TYPE_SUBCONTRACT = "专业分包";
+
+ public static final String CALCULATION_METHOD_GUIDANCE_PRICE = "指导价法";
+ public static final String CALCULATION_METHOD_CONTRACT_PRICE = "合同价法";
+ public static final String CALCULATION_METHOD_VIRTUAL_OUTPUT = "虚拟产值法";
+
+ public static final String VIRTUAL_CALCULATION_METHOD_GUIDANCE_PRICE = "指导单价法";
+ public static final String VIRTUAL_CALCULATION_METHOD_VIRTUAL_TOTAL_PRICE = "虚拟总价法";
+ public static final String VIRTUAL_CALCULATION_METHOD_WORKING_DAY = "工日法";
+
+ private static final Set OWNERSHIP_TYPES = new HashSet<>(Arrays.asList(
+ OWNERSHIP_TYPE_MAJOR,
+ OWNERSHIP_TYPE_COMPREHENSIVE,
+ OWNERSHIP_TYPE_SUBCONTRACT
+ ));
+
+ private static final Set CALCULATION_METHODS = new HashSet<>(Arrays.asList(
+ CALCULATION_METHOD_GUIDANCE_PRICE,
+ CALCULATION_METHOD_CONTRACT_PRICE,
+ CALCULATION_METHOD_VIRTUAL_OUTPUT
+ ));
+
+ private static final Set VIRTUAL_CALCULATION_METHODS = new HashSet<>(Arrays.asList(
+ VIRTUAL_CALCULATION_METHOD_GUIDANCE_PRICE,
+ VIRTUAL_CALCULATION_METHOD_VIRTUAL_TOTAL_PRICE,
+ VIRTUAL_CALCULATION_METHOD_WORKING_DAY
+ ));
+
+ private ProjectPlanningBizTypeConstants() {
+ }
+
+ public static boolean isValidOwnershipType(String value) {
+ return OWNERSHIP_TYPES.contains(value);
+ }
+
+ public static boolean isValidCalculationMethod(String value) {
+ return CALCULATION_METHODS.contains(value);
+ }
+
+ public static boolean isValidVirtualCalculationMethod(String value) {
+ return VIRTUAL_CALCULATION_METHODS.contains(value);
+ }
+
+ public static boolean isMajor(String value) {
+ return OWNERSHIP_TYPE_MAJOR.equals(value);
+ }
+
+ public static boolean isComprehensive(String value) {
+ return OWNERSHIP_TYPE_COMPREHENSIVE.equals(value);
+ }
+
+ public static boolean isSubcontract(String value) {
+ return OWNERSHIP_TYPE_SUBCONTRACT.equals(value);
+ }
+
+ public static boolean isGuidancePrice(String value) {
+ return CALCULATION_METHOD_GUIDANCE_PRICE.equals(value);
+ }
+
+ public static boolean isContractPrice(String value) {
+ return CALCULATION_METHOD_CONTRACT_PRICE.equals(value);
+ }
+
+ public static boolean isVirtualOutput(String value) {
+ return CALCULATION_METHOD_VIRTUAL_OUTPUT.equals(value);
+ }
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/outputsplit/ProjectOutputSplitService.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/outputsplit/ProjectOutputSplitService.java
new file mode 100644
index 0000000..cd30027
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/outputsplit/ProjectOutputSplitService.java
@@ -0,0 +1,32 @@
+package cn.iocoder.lyzsys.module.tjt.service.outputsplit;
+
+import cn.iocoder.lyzsys.module.tjt.controller.admin.outputsplit.vo.ProjectOutputSplitRespVO;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.outputsplit.vo.ProjectOutputSplitSaveReqVO;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.outputsplit.ProjectOutputSplitDO;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * 页面4拆分比例 Service 接口
+ *
+ * @author Codex
+ */
+public interface ProjectOutputSplitService {
+
+ ProjectOutputSplitRespVO getProjectOutputSplit(Long planningId);
+
+ Long saveProjectOutputSplit(ProjectOutputSplitSaveReqVO reqVO);
+
+ ProjectOutputSplitDO getOrCreateProjectOutputSplit(Long planningId);
+
+ Map getProjectOutputSplitMap(Collection planningIds);
+
+ BigDecimal getSpecialtyAmount(ProjectOutputSplitDO outputSplit, BigDecimal assessmentOutputValue, String specialtyCode);
+
+ void deleteByPlanningId(Long planningId);
+
+ void deleteByPlanningIds(Collection planningIds);
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/outputsplit/ProjectOutputSplitServiceImpl.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/outputsplit/ProjectOutputSplitServiceImpl.java
new file mode 100644
index 0000000..0cc1654
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/outputsplit/ProjectOutputSplitServiceImpl.java
@@ -0,0 +1,269 @@
+package cn.iocoder.lyzsys.module.tjt.service.outputsplit;
+
+import cn.iocoder.lyzsys.framework.common.util.object.BeanUtils;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.outputsplit.vo.ProjectOutputSplitRespVO;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.outputsplit.vo.ProjectOutputSplitSaveReqVO;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.outputsplit.ProjectOutputSplitDO;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.planning.ProjectPlanningDO;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.project.ProjectDO;
+import cn.iocoder.lyzsys.module.tjt.dal.mysql.outputsplit.ProjectOutputSplitMapper;
+import cn.iocoder.lyzsys.module.tjt.dal.mysql.planning.ProjectPlanningMapper;
+import cn.iocoder.lyzsys.module.tjt.dal.mysql.project.ProjectMapper;
+import cn.iocoder.lyzsys.module.tjt.enums.OutputSplitBizConstants;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import static cn.iocoder.lyzsys.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_NOT_EXISTS;
+import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_OUTPUT_SPLIT_NOT_MAJOR;
+import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_OUTPUT_SPLIT_PLANNING_NOT_EXISTS;
+import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_OUTPUT_SPLIT_RATIO_INVALID;
+
+/**
+ * 页面4拆分比例 Service 实现类
+ *
+ * @author Codex
+ */
+@Service
+@Validated
+public class ProjectOutputSplitServiceImpl implements ProjectOutputSplitService {
+
+ private static final int RATIO_SCALE = 4;
+ private static final int AMOUNT_SCALE = 2;
+ private static final BigDecimal ZERO_RATIO = BigDecimal.ZERO.setScale(RATIO_SCALE, RoundingMode.HALF_UP);
+ private static final BigDecimal ONE_RATIO = BigDecimal.ONE.setScale(RATIO_SCALE, RoundingMode.HALF_UP);
+ private static final BigDecimal ZERO_AMOUNT = BigDecimal.ZERO.setScale(AMOUNT_SCALE, RoundingMode.HALF_UP);
+
+ @Resource
+ private ProjectOutputSplitMapper projectOutputSplitMapper;
+ @Resource
+ private ProjectPlanningMapper projectPlanningMapper;
+ @Resource
+ private ProjectMapper projectMapper;
+
+ @Override
+ public ProjectOutputSplitRespVO getProjectOutputSplit(Long planningId) {
+ ProjectPlanningDO planning = validateMajorPlanning(planningId);
+ ProjectDO project = validateProjectExists(planning.getProjectId());
+ ProjectOutputSplitDO outputSplit = projectOutputSplitMapper.selectByPlanningId(planningId);
+ if (outputSplit == null) {
+ outputSplit = buildDefaultOutputSplit(planning);
+ }
+ return buildRespVO(outputSplit, planning, project);
+ }
+
+ @Override
+ public Long saveProjectOutputSplit(ProjectOutputSplitSaveReqVO reqVO) {
+ ProjectPlanningDO planning = validateMajorPlanning(reqVO.getPlanningId());
+ validateOutputSplitRatios(reqVO);
+ ProjectOutputSplitDO outputSplit = projectOutputSplitMapper.selectByPlanningId(reqVO.getPlanningId());
+ if (outputSplit == null) {
+ outputSplit = BeanUtils.toBean(reqVO, ProjectOutputSplitDO.class);
+ outputSplit.setProjectId(planning.getProjectId());
+ outputSplit.setYear(getPlanningYear(planning));
+ normalizeRatios(outputSplit);
+ projectOutputSplitMapper.insert(outputSplit);
+ } else {
+ ProjectOutputSplitDO updateObj = BeanUtils.toBean(reqVO, ProjectOutputSplitDO.class);
+ updateObj.setId(outputSplit.getId());
+ updateObj.setProjectId(planning.getProjectId());
+ updateObj.setYear(getPlanningYear(planning));
+ normalizeRatios(updateObj);
+ projectOutputSplitMapper.updateById(updateObj);
+ outputSplit = updateObj;
+ }
+ return outputSplit.getId();
+ }
+
+ @Override
+ public ProjectOutputSplitDO getOrCreateProjectOutputSplit(Long planningId) {
+ ProjectPlanningDO planning = validateMajorPlanning(planningId);
+ ProjectOutputSplitDO outputSplit = projectOutputSplitMapper.selectByPlanningId(planningId);
+ if (outputSplit != null) {
+ return outputSplit;
+ }
+ ProjectOutputSplitDO createObj = buildDefaultOutputSplit(planning);
+ projectOutputSplitMapper.insert(createObj);
+ return createObj;
+ }
+
+ @Override
+ public Map getProjectOutputSplitMap(Collection planningIds) {
+ if (planningIds == null || planningIds.isEmpty()) {
+ return Collections.emptyMap();
+ }
+ return projectOutputSplitMapper.selectListByPlanningIds(planningIds).stream()
+ .collect(Collectors.toMap(ProjectOutputSplitDO::getPlanningId, item -> item, (a, b) -> b));
+ }
+
+ @Override
+ public BigDecimal getSpecialtyAmount(ProjectOutputSplitDO outputSplit, BigDecimal assessmentOutputValue, String specialtyCode) {
+ if (outputSplit == null) {
+ return ZERO_AMOUNT;
+ }
+ BigDecimal officeAmount = multiplyAmount(amount(assessmentOutputValue), ratio(outputSplit.getOfficeRatio()));
+ return multiplyAmount(officeAmount, getSpecialtyRatio(outputSplit, specialtyCode));
+ }
+
+ @Override
+ public void deleteByPlanningId(Long planningId) {
+ ProjectOutputSplitDO outputSplit = projectOutputSplitMapper.selectByPlanningId(planningId);
+ if (outputSplit != null) {
+ projectOutputSplitMapper.deleteById(outputSplit.getId());
+ }
+ }
+
+ @Override
+ public void deleteByPlanningIds(Collection planningIds) {
+ if (planningIds == null || planningIds.isEmpty()) {
+ return;
+ }
+ projectOutputSplitMapper.delete(new cn.iocoder.lyzsys.framework.mybatis.core.query.LambdaQueryWrapperX()
+ .in(ProjectOutputSplitDO::getPlanningId, planningIds));
+ }
+
+ private ProjectOutputSplitRespVO buildRespVO(ProjectOutputSplitDO outputSplit, ProjectPlanningDO planning, ProjectDO project) {
+ ProjectOutputSplitRespVO respVO = BeanUtils.toBean(outputSplit, ProjectOutputSplitRespVO.class);
+ BigDecimal assessmentOutputValue = amount(planning.getAssessmentOutputValue());
+ BigDecimal projectManagerAmount = multiplyAmount(assessmentOutputValue, outputSplit.getProjectManagerRatio());
+ BigDecimal engineeringLeaderAmount = multiplyAmount(assessmentOutputValue, outputSplit.getEngineeringLeaderRatio());
+ BigDecimal officeAmount = multiplyAmount(assessmentOutputValue, outputSplit.getOfficeRatio());
+
+ respVO.setProjectName(project.getProjectName());
+ respVO.setPlanningContent(planning.getPlanningContent());
+ respVO.setYear(getPlanningYear(planning));
+ respVO.setAssessmentOutputValue(assessmentOutputValue);
+ respVO.setProjectManagerName(project.getProjectManagerName());
+ respVO.setEngineeringLeaderName(project.getEngineeringPrincipalName());
+ respVO.setProjectManagerAmount(projectManagerAmount);
+ respVO.setEngineeringLeaderAmount(engineeringLeaderAmount);
+ respVO.setOfficeAmount(officeAmount);
+ respVO.setArchAmount(multiplyAmount(officeAmount, outputSplit.getArchRatio()));
+ respVO.setDecorAmount(multiplyAmount(officeAmount, outputSplit.getDecorRatio()));
+ respVO.setStructAmount(multiplyAmount(officeAmount, outputSplit.getStructRatio()));
+ respVO.setWaterAmount(multiplyAmount(officeAmount, outputSplit.getWaterRatio()));
+ respVO.setElecAmount(multiplyAmount(officeAmount, outputSplit.getElecRatio()));
+ respVO.setHvacAmount(multiplyAmount(officeAmount, outputSplit.getHvacRatio()));
+ respVO.setDigitalAmount(multiplyAmount(officeAmount, outputSplit.getDigitalRatio()));
+ return respVO;
+ }
+
+ private ProjectPlanningDO validateMajorPlanning(Long planningId) {
+ ProjectPlanningDO planning = projectPlanningMapper.selectById(planningId);
+ if (planning == null) {
+ throw exception(PROJECT_OUTPUT_SPLIT_PLANNING_NOT_EXISTS);
+ }
+ if (!OutputSplitBizConstants.isMajorOwnershipType(planning.getOwnershipType())) {
+ throw exception(PROJECT_OUTPUT_SPLIT_NOT_MAJOR);
+ }
+ return planning;
+ }
+
+ private ProjectDO validateProjectExists(Long projectId) {
+ ProjectDO project = projectMapper.selectById(projectId);
+ if (project == null) {
+ throw exception(PROJECT_NOT_EXISTS);
+ }
+ return project;
+ }
+
+ private ProjectOutputSplitDO buildDefaultOutputSplit(ProjectPlanningDO planning) {
+ ProjectOutputSplitDO outputSplit = new ProjectOutputSplitDO();
+ outputSplit.setProjectId(planning.getProjectId());
+ outputSplit.setPlanningId(planning.getId());
+ outputSplit.setYear(getPlanningYear(planning));
+ outputSplit.setProjectManagerRatio(ZERO_RATIO);
+ outputSplit.setEngineeringLeaderRatio(ZERO_RATIO);
+ outputSplit.setOfficeRatio(ONE_RATIO);
+ outputSplit.setArchRatio(ONE_RATIO);
+ outputSplit.setDecorRatio(ZERO_RATIO);
+ outputSplit.setStructRatio(ZERO_RATIO);
+ outputSplit.setWaterRatio(ZERO_RATIO);
+ outputSplit.setElecRatio(ZERO_RATIO);
+ outputSplit.setHvacRatio(ZERO_RATIO);
+ outputSplit.setDigitalRatio(ZERO_RATIO);
+ return outputSplit;
+ }
+
+ private Integer getPlanningYear(ProjectPlanningDO planning) {
+ return planning.getPlanningStartYear() == null ? 0 : planning.getPlanningStartYear();
+ }
+
+ private void validateOutputSplitRatios(ProjectOutputSplitSaveReqVO reqVO) {
+ BigDecimal projectTotal = ratio(reqVO.getProjectManagerRatio())
+ .add(ratio(reqVO.getEngineeringLeaderRatio()))
+ .add(ratio(reqVO.getOfficeRatio()))
+ .setScale(RATIO_SCALE, RoundingMode.HALF_UP);
+ BigDecimal specialtyTotal = ratio(reqVO.getArchRatio())
+ .add(ratio(reqVO.getDecorRatio()))
+ .add(ratio(reqVO.getStructRatio()))
+ .add(ratio(reqVO.getWaterRatio()))
+ .add(ratio(reqVO.getElecRatio()))
+ .add(ratio(reqVO.getHvacRatio()))
+ .add(ratio(reqVO.getDigitalRatio()))
+ .setScale(RATIO_SCALE, RoundingMode.HALF_UP);
+ if (projectTotal.compareTo(ONE_RATIO) > 0 || specialtyTotal.compareTo(ONE_RATIO) != 0) {
+ throw exception(PROJECT_OUTPUT_SPLIT_RATIO_INVALID);
+ }
+ }
+
+ private void normalizeRatios(ProjectOutputSplitDO outputSplit) {
+ normalize(outputSplit::setProjectManagerRatio, outputSplit.getProjectManagerRatio());
+ normalize(outputSplit::setEngineeringLeaderRatio, outputSplit.getEngineeringLeaderRatio());
+ normalize(outputSplit::setOfficeRatio, outputSplit.getOfficeRatio());
+ normalize(outputSplit::setArchRatio, outputSplit.getArchRatio());
+ normalize(outputSplit::setDecorRatio, outputSplit.getDecorRatio());
+ normalize(outputSplit::setStructRatio, outputSplit.getStructRatio());
+ normalize(outputSplit::setWaterRatio, outputSplit.getWaterRatio());
+ normalize(outputSplit::setElecRatio, outputSplit.getElecRatio());
+ normalize(outputSplit::setHvacRatio, outputSplit.getHvacRatio());
+ normalize(outputSplit::setDigitalRatio, outputSplit.getDigitalRatio());
+ }
+
+ private void normalize(Consumer consumer, BigDecimal value) {
+ consumer.accept(ratio(value));
+ }
+
+ private BigDecimal getSpecialtyRatio(ProjectOutputSplitDO outputSplit, String specialtyCode) {
+ switch (specialtyCode) {
+ case OutputSplitBizConstants.SPECIALTY_ARCH:
+ return ratio(outputSplit.getArchRatio());
+ case OutputSplitBizConstants.SPECIALTY_DECOR:
+ return ratio(outputSplit.getDecorRatio());
+ case OutputSplitBizConstants.SPECIALTY_STRUCT:
+ return ratio(outputSplit.getStructRatio());
+ case OutputSplitBizConstants.SPECIALTY_WATER:
+ return ratio(outputSplit.getWaterRatio());
+ case OutputSplitBizConstants.SPECIALTY_ELEC:
+ return ratio(outputSplit.getElecRatio());
+ case OutputSplitBizConstants.SPECIALTY_HVAC:
+ return ratio(outputSplit.getHvacRatio());
+ case OutputSplitBizConstants.SPECIALTY_DIGITAL:
+ return ratio(outputSplit.getDigitalRatio());
+ default:
+ return ZERO_RATIO;
+ }
+ }
+
+ private BigDecimal multiplyAmount(BigDecimal left, BigDecimal right) {
+ return amount(left).multiply(ratio(right)).setScale(AMOUNT_SCALE, RoundingMode.HALF_UP);
+ }
+
+ private BigDecimal amount(BigDecimal value) {
+ return value == null ? ZERO_AMOUNT : value.setScale(AMOUNT_SCALE, RoundingMode.HALF_UP);
+ }
+
+ private BigDecimal ratio(BigDecimal value) {
+ return value == null ? ZERO_RATIO : value.setScale(RATIO_SCALE, RoundingMode.HALF_UP);
+ }
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/planning/ProjectPlanningService.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/planning/ProjectPlanningService.java
new file mode 100644
index 0000000..77a6f6c
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/planning/ProjectPlanningService.java
@@ -0,0 +1,36 @@
+package cn.iocoder.lyzsys.module.tjt.service.planning;
+
+import cn.iocoder.lyzsys.framework.common.pojo.PageResult;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.planning.vo.ProjectPlanningPageReqVO;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.planning.vo.ProjectPlanningSaveReqVO;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.planning.ProjectPlanningDO;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 合约规划 Service 接口
+ *
+ * @author Codex
+ */
+public interface ProjectPlanningService {
+
+ Long createProjectPlanning(ProjectPlanningSaveReqVO createReqVO);
+
+ void updateProjectPlanning(ProjectPlanningSaveReqVO updateReqVO);
+
+ void deleteProjectPlanning(Long id);
+
+ void deleteProjectPlanningList(List ids);
+
+ PageResult getProjectPlanningPage(ProjectPlanningPageReqVO pageReqVO);
+
+ List getProjectPlanningListByProjectId(Long projectId);
+
+ ProjectPlanningDO getProjectPlanning(Long id);
+
+ Map getAllocatedAmountMap(Collection planningIds);
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/planning/ProjectPlanningServiceImpl.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/planning/ProjectPlanningServiceImpl.java
new file mode 100644
index 0000000..fb3a85d
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/planning/ProjectPlanningServiceImpl.java
@@ -0,0 +1,438 @@
+package cn.iocoder.lyzsys.module.tjt.service.planning;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.lyzsys.framework.common.pojo.PageResult;
+import cn.iocoder.lyzsys.framework.common.util.object.BeanUtils;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.planning.vo.ProjectPlanningPageReqVO;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.planning.vo.ProjectPlanningSaveReqVO;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.outputsplit.ProjectOutputSplitDO;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.planning.ProjectPlanningDO;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.planningquarter.ProjectPlanningQuarterDO;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.project.ProjectDO;
+import cn.iocoder.lyzsys.module.tjt.dal.mysql.planning.ProjectPlanningMapper;
+import cn.iocoder.lyzsys.module.tjt.dal.mysql.planningquarter.ProjectPlanningQuarterMapper;
+import cn.iocoder.lyzsys.module.tjt.dal.mysql.project.ProjectMapper;
+import cn.iocoder.lyzsys.module.tjt.service.outputsplit.ProjectOutputSplitService;
+import cn.iocoder.lyzsys.module.tjt.service.specialtyrolesplit.SpecialtyRoleSplitService;
+import cn.iocoder.lyzsys.module.tjt.enums.ProjectPlanningBizTypeConstants;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import static cn.iocoder.lyzsys.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_PLANNING_CALCULATION_METHOD_IMMUTABLE;
+import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_PLANNING_CALCULATION_METHOD_INVALID;
+import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_PLANNING_NOT_EXISTS;
+import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_PLANNING_OWNERSHIP_TYPE_IMMUTABLE;
+import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_PLANNING_OWNERSHIP_TYPE_INVALID;
+import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_PLANNING_PROJECT_NOT_EXISTS;
+import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_PLANNING_VIRTUAL_CALCULATION_METHOD_INVALID;
+
+/**
+ * 合约规划 Service 实现类
+ *
+ * @author Codex
+ */
+@Service
+@Validated
+public class ProjectPlanningServiceImpl implements ProjectPlanningService {
+
+ private static final int AMOUNT_SCALE = 2;
+ private static final int RATIO_SCALE = 4;
+ private static final int FACTOR_SCALE = 2;
+
+ private static final BigDecimal ZERO_AMOUNT = BigDecimal.ZERO.setScale(AMOUNT_SCALE, RoundingMode.HALF_UP);
+ private static final BigDecimal ZERO_RATIO = BigDecimal.ZERO.setScale(RATIO_SCALE, RoundingMode.HALF_UP);
+ private static final BigDecimal ONE_RATIO = BigDecimal.ONE.setScale(RATIO_SCALE, RoundingMode.HALF_UP);
+ private static final BigDecimal DEFAULT_COMPREHENSIVE_RATIO = new BigDecimal("0.0800");
+ private static final BigDecimal DEFAULT_SUBCONTRACT_RATIO = new BigDecimal("0.0400");
+ private static final BigDecimal DEFAULT_MAJOR_OUTSOURCE_RATIO = new BigDecimal("0.0600");
+ private static final BigDecimal DEFAULT_COMMON_OUTSOURCE_RATIO = new BigDecimal("0.2500");
+ private static final BigDecimal DEFAULT_WORKING_DAY_UNIT_PRICE = new BigDecimal("1000.00");
+ private static final BigDecimal MIN_GUIDANCE_PRICE_RATIO = new BigDecimal("0.6000");
+
+ @Resource
+ private ProjectPlanningMapper projectPlanningMapper;
+ @Resource
+ private ProjectMapper projectMapper;
+ @Resource
+ private ProjectPlanningQuarterMapper projectPlanningQuarterMapper;
+ @Resource
+ private ProjectOutputSplitService projectOutputSplitService;
+ @Resource
+ private SpecialtyRoleSplitService specialtyRoleSplitService;
+
+ @Override
+ public Long createProjectPlanning(ProjectPlanningSaveReqVO createReqVO) {
+ ProjectDO project = validateProjectExists(createReqVO.getProjectId());
+ validateProjectPlanningForSave(createReqVO, null);
+ ProjectPlanningDO planning = BeanUtils.toBean(createReqVO, ProjectPlanningDO.class);
+ prepareProjectPlanning(planning, project);
+ calculateProjectPlanning(planning);
+ projectPlanningMapper.insert(planning);
+ return planning.getId();
+ }
+
+ @Override
+ public void updateProjectPlanning(ProjectPlanningSaveReqVO updateReqVO) {
+ ProjectPlanningDO dbPlanning = validateProjectPlanningExists(updateReqVO.getId());
+ ProjectDO project = validateProjectExists(updateReqVO.getProjectId());
+ validateProjectPlanningForSave(updateReqVO, dbPlanning);
+ ProjectPlanningDO updateObj = BeanUtils.toBean(updateReqVO, ProjectPlanningDO.class);
+ prepareProjectPlanning(updateObj, project);
+ calculateProjectPlanning(updateObj);
+ projectPlanningMapper.updateById(updateObj);
+ refreshQuarterDistributionAmounts(updateObj);
+ }
+
+ @Override
+ public void deleteProjectPlanning(Long id) {
+ validateProjectPlanningExists(id);
+ ProjectOutputSplitDO outputSplit = projectOutputSplitService.getProjectOutputSplitMap(Collections.singleton(id)).get(id);
+ if (outputSplit != null) {
+ specialtyRoleSplitService.deleteByOutputSplitId(outputSplit.getId());
+ projectOutputSplitService.deleteByPlanningId(id);
+ }
+ projectPlanningQuarterMapper.delete(ProjectPlanningQuarterDO::getPlanningId, id);
+ projectPlanningMapper.deleteById(id);
+ }
+
+ @Override
+ public void deleteProjectPlanningList(List ids) {
+ ids.forEach(this::validateProjectPlanningExists);
+ Map outputSplitMap = projectOutputSplitService.getProjectOutputSplitMap(ids);
+ if (!outputSplitMap.isEmpty()) {
+ specialtyRoleSplitService.deleteByOutputSplitIds(outputSplitMap.values().stream()
+ .map(ProjectOutputSplitDO::getId)
+ .collect(java.util.stream.Collectors.toList()));
+ projectOutputSplitService.deleteByPlanningIds(ids);
+ }
+ projectPlanningQuarterMapper.deleteBatch(ProjectPlanningQuarterDO::getPlanningId, ids);
+ projectPlanningMapper.deleteBatchIds(ids);
+ }
+
+ @Override
+ public PageResult getProjectPlanningPage(ProjectPlanningPageReqVO pageReqVO) {
+ if (pageReqVO.getProjectId() != null) {
+ validateProjectExists(pageReqVO.getProjectId());
+ }
+ return projectPlanningMapper.selectPage(pageReqVO);
+ }
+
+ @Override
+ public List getProjectPlanningListByProjectId(Long projectId) {
+ validateProjectExists(projectId);
+ return projectPlanningMapper.selectListByProjectId(projectId);
+ }
+
+ @Override
+ public ProjectPlanningDO getProjectPlanning(Long id) {
+ return projectPlanningMapper.selectById(id);
+ }
+
+ @Override
+ public Map getAllocatedAmountMap(Collection planningIds) {
+ if (planningIds == null || planningIds.isEmpty()) {
+ return Collections.emptyMap();
+ }
+ List quarterList = projectPlanningQuarterMapper.selectListByPlanningIds(planningIds);
+ Map allocatedAmountMap = new HashMap<>();
+ for (Long planningId : planningIds) {
+ allocatedAmountMap.put(planningId, ZERO_RATIO);
+ }
+ for (ProjectPlanningQuarterDO quarter : quarterList) {
+ allocatedAmountMap.merge(quarter.getPlanningId(), ratio(quarter.getDistributionRatio()), BigDecimal::add);
+ }
+ allocatedAmountMap.replaceAll((key, value) -> ratio(value));
+ return allocatedAmountMap;
+ }
+
+ private void validateProjectPlanningForSave(ProjectPlanningSaveReqVO reqVO, ProjectPlanningDO dbPlanning) {
+ if (!ProjectPlanningBizTypeConstants.isValidOwnershipType(reqVO.getOwnershipType())) {
+ throw exception(PROJECT_PLANNING_OWNERSHIP_TYPE_INVALID);
+ }
+ if (!ProjectPlanningBizTypeConstants.isValidCalculationMethod(reqVO.getCalculationMethod())) {
+ throw exception(PROJECT_PLANNING_CALCULATION_METHOD_INVALID);
+ }
+ if (ProjectPlanningBizTypeConstants.isVirtualOutput(reqVO.getCalculationMethod())
+ && StrUtil.isNotBlank(reqVO.getVirtualCalculationMethod())
+ && !ProjectPlanningBizTypeConstants.isValidVirtualCalculationMethod(reqVO.getVirtualCalculationMethod())) {
+ throw exception(PROJECT_PLANNING_VIRTUAL_CALCULATION_METHOD_INVALID);
+ }
+ if (dbPlanning == null) {
+ return;
+ }
+ if (!Objects.equals(dbPlanning.getOwnershipType(), reqVO.getOwnershipType())) {
+ throw exception(PROJECT_PLANNING_OWNERSHIP_TYPE_IMMUTABLE);
+ }
+ if (!Objects.equals(dbPlanning.getCalculationMethod(), reqVO.getCalculationMethod())) {
+ throw exception(PROJECT_PLANNING_CALCULATION_METHOD_IMMUTABLE);
+ }
+ }
+
+ private void prepareProjectPlanning(ProjectPlanningDO planning, ProjectDO project) {
+ if (planning.getPlanningStartYear() == null) {
+ planning.setPlanningStartYear(project.getProjectStartYear());
+ }
+ if (planning.getReviewOutsourceFlag() == null) {
+ planning.setReviewOutsourceFlag(Boolean.FALSE);
+ }
+ planning.setPlanningAmount(amount(planning.getPlanningAmount()));
+ planning.setManagementFeeRate(ratio(planning.getManagementFeeRate()));
+ if (planning.getPlanningArea() == null) {
+ planning.setPlanningArea(project.getTotalConstructionArea());
+ }
+ planning.setPlanningArea(amount(planning.getPlanningArea()));
+ planning.setCurrentDesignStageRatio(ratio(planning.getCurrentDesignStageRatio()));
+ planning.setTotalDistributionAmount(planning.getTotalDistributionAmount() == null
+ ? ONE_RATIO : ratio(planning.getTotalDistributionAmount()));
+
+ if (planning.getReviewOutsourceRatio() == null) {
+ planning.setReviewOutsourceRatio(defaultReviewOutsourceRatio(planning.getOwnershipType(),
+ Boolean.TRUE.equals(planning.getReviewOutsourceFlag())));
+ } else {
+ planning.setReviewOutsourceRatio(ratio(planning.getReviewOutsourceRatio()));
+ }
+ if ((ProjectPlanningBizTypeConstants.isComprehensive(planning.getOwnershipType())
+ || ProjectPlanningBizTypeConstants.isSubcontract(planning.getOwnershipType()))
+ && planning.getCalculationRatio() == null) {
+ planning.setCalculationRatio(defaultCalculationRatio(planning.getOwnershipType()));
+ } else if (planning.getCalculationRatio() != null) {
+ planning.setCalculationRatio(ratio(planning.getCalculationRatio()));
+ }
+
+ planning.setDrawingSetFactor(factorNullable(planning.getDrawingSetFactor()));
+ planning.setScaleFactor(factorNullable(planning.getScaleFactor()));
+ planning.setModificationFactor(factorNullable(planning.getModificationFactor()));
+ planning.setComplexityFactor(ratioNullable(planning.getComplexityFactor()));
+ planning.setInternalGuidanceUnitPrice(amountNullable(planning.getInternalGuidanceUnitPrice()));
+ planning.setWorkingDayCount(amountNullable(planning.getWorkingDayCount()));
+ planning.setWorkingDayUnitPrice(amountNullable(planning.getWorkingDayUnitPrice()));
+ planning.setGuidanceUnitPrice(amountNullable(planning.getGuidanceUnitPrice()));
+ planning.setGuidanceTotalPrice(amountNullable(planning.getGuidanceTotalPrice()));
+ planning.setVirtualTotalPrice(amountNullable(planning.getVirtualTotalPrice()));
+
+ if (ProjectPlanningBizTypeConstants.VIRTUAL_CALCULATION_METHOD_WORKING_DAY.equals(planning.getVirtualCalculationMethod())
+ && planning.getWorkingDayUnitPrice() == null) {
+ planning.setWorkingDayUnitPrice(DEFAULT_WORKING_DAY_UNIT_PRICE);
+ }
+ }
+
+ private void calculateProjectPlanning(ProjectPlanningDO planning) {
+ planning.setManagementFee(multiplyAmount(planning.getPlanningAmount(), planning.getManagementFeeRate()));
+ planning.setContractUnitPrice(divideAmount(planning.getPlanningAmount(), planning.getPlanningArea()));
+ planning.setTotalAdjustmentFactor(ZERO_RATIO);
+ planning.setAssessmentArea(ZERO_AMOUNT);
+ planning.setVirtualOutputValue(ZERO_AMOUNT);
+ planning.setAssessmentOutputValue(ZERO_AMOUNT);
+
+ if (ProjectPlanningBizTypeConstants.isMajor(planning.getOwnershipType())) {
+ calculateMajorPlanning(planning);
+ return;
+ }
+ if (ProjectPlanningBizTypeConstants.isComprehensive(planning.getOwnershipType())
+ || ProjectPlanningBizTypeConstants.isSubcontract(planning.getOwnershipType())) {
+ planning.setAssessmentOutputValue(multiplyAmount(
+ planning.getPlanningAmount(),
+ ratio(planning.getCalculationRatio()),
+ planning.getCurrentDesignStageRatio(),
+ oneMinus(planning.getReviewOutsourceRatio())));
+ }
+ }
+
+ private void calculateMajorPlanning(ProjectPlanningDO planning) {
+ if (ProjectPlanningBizTypeConstants.isGuidancePrice(planning.getCalculationMethod())) {
+ BigDecimal totalAdjustmentFactor = calculateTotalAdjustmentFactor(planning);
+ planning.setTotalAdjustmentFactor(totalAdjustmentFactor);
+ planning.setAssessmentArea(multiplyAmount(planning.getPlanningArea(), totalAdjustmentFactor));
+ BigDecimal applicableUnitPrice = calculateApplicableUnitPrice(
+ planning.getContractUnitPrice(), planning.getInternalGuidanceUnitPrice());
+ planning.setAssessmentOutputValue(multiplyAmount(
+ applicableUnitPrice,
+ planning.getAssessmentArea(),
+ planning.getCurrentDesignStageRatio(),
+ oneMinus(planning.getReviewOutsourceRatio())));
+ return;
+ }
+ if (ProjectPlanningBizTypeConstants.isContractPrice(planning.getCalculationMethod())) {
+ BigDecimal totalAdjustmentFactor = calculateTotalAdjustmentFactor(planning);
+ planning.setTotalAdjustmentFactor(totalAdjustmentFactor);
+ planning.setAssessmentArea(multiplyAmount(planning.getPlanningArea(), totalAdjustmentFactor));
+ planning.setAssessmentOutputValue(multiplyAmount(
+ planning.getPlanningAmount(),
+ totalAdjustmentFactor,
+ planning.getCurrentDesignStageRatio(),
+ oneMinus(planning.getReviewOutsourceRatio())));
+ return;
+ }
+ if (ProjectPlanningBizTypeConstants.isVirtualOutput(planning.getCalculationMethod())) {
+ planning.setTotalAdjustmentFactor(ONE_RATIO);
+ planning.setVirtualOutputValue(calculateVirtualOutputValue(planning));
+ planning.setAssessmentOutputValue(multiplyAmount(
+ planning.getVirtualOutputValue(),
+ planning.getCurrentDesignStageRatio(),
+ oneMinus(planning.getReviewOutsourceRatio())));
+ }
+ }
+
+ private BigDecimal calculateTotalAdjustmentFactor(ProjectPlanningDO planning) {
+ return productRatio(
+ defaultFactor(planning.getDrawingSetFactor()),
+ defaultFactor(planning.getScaleFactor()),
+ defaultFactor(planning.getModificationFactor()),
+ defaultFactor(planning.getComplexityFactor()));
+ }
+
+ private BigDecimal calculateApplicableUnitPrice(BigDecimal contractUnitPrice, BigDecimal internalGuidanceUnitPrice) {
+ BigDecimal guidanceUnitPrice = amount(internalGuidanceUnitPrice);
+ if (guidanceUnitPrice.compareTo(BigDecimal.ZERO) <= 0) {
+ return ZERO_AMOUNT;
+ }
+ BigDecimal unitPrice = amount(contractUnitPrice);
+ BigDecimal minimumGuidancePrice = multiplyAmount(guidanceUnitPrice, MIN_GUIDANCE_PRICE_RATIO);
+ if (unitPrice.compareTo(guidanceUnitPrice) > 0) {
+ return guidanceUnitPrice;
+ }
+ if (unitPrice.compareTo(minimumGuidancePrice) >= 0) {
+ return unitPrice;
+ }
+ return minimumGuidancePrice;
+ }
+
+ private BigDecimal calculateVirtualOutputValue(ProjectPlanningDO planning) {
+ if (ProjectPlanningBizTypeConstants.VIRTUAL_CALCULATION_METHOD_WORKING_DAY.equals(planning.getVirtualCalculationMethod())) {
+ return multiplyAmount(planning.getWorkingDayCount(), planning.getWorkingDayUnitPrice());
+ }
+ if (ProjectPlanningBizTypeConstants.VIRTUAL_CALCULATION_METHOD_GUIDANCE_PRICE.equals(planning.getVirtualCalculationMethod())) {
+ if (isPositive(planning.getGuidanceTotalPrice())) {
+ return amount(planning.getGuidanceTotalPrice());
+ }
+ return multiplyAmount(planning.getGuidanceUnitPrice(), planning.getPlanningArea());
+ }
+ if (ProjectPlanningBizTypeConstants.VIRTUAL_CALCULATION_METHOD_VIRTUAL_TOTAL_PRICE.equals(planning.getVirtualCalculationMethod())) {
+ return amount(planning.getVirtualTotalPrice());
+ }
+ return ZERO_AMOUNT;
+ }
+
+ private BigDecimal defaultReviewOutsourceRatio(String ownershipType, boolean outsourceFlag) {
+ if (!outsourceFlag) {
+ return ZERO_RATIO;
+ }
+ if (ProjectPlanningBizTypeConstants.isMajor(ownershipType)) {
+ return DEFAULT_MAJOR_OUTSOURCE_RATIO;
+ }
+ return DEFAULT_COMMON_OUTSOURCE_RATIO;
+ }
+
+ private BigDecimal defaultCalculationRatio(String ownershipType) {
+ if (ProjectPlanningBizTypeConstants.isComprehensive(ownershipType)) {
+ return DEFAULT_COMPREHENSIVE_RATIO;
+ }
+ if (ProjectPlanningBizTypeConstants.isSubcontract(ownershipType)) {
+ return DEFAULT_SUBCONTRACT_RATIO;
+ }
+ return null;
+ }
+
+ private ProjectDO validateProjectExists(Long projectId) {
+ ProjectDO project = projectMapper.selectById(projectId);
+ if (project == null) {
+ throw exception(PROJECT_PLANNING_PROJECT_NOT_EXISTS);
+ }
+ return project;
+ }
+
+ private ProjectPlanningDO validateProjectPlanningExists(Long id) {
+ ProjectPlanningDO planning = projectPlanningMapper.selectById(id);
+ if (planning == null) {
+ throw exception(PROJECT_PLANNING_NOT_EXISTS);
+ }
+ return planning;
+ }
+
+ private BigDecimal multiplyAmount(BigDecimal... values) {
+ BigDecimal result = BigDecimal.ONE;
+ for (BigDecimal value : values) {
+ result = result.multiply(value == null ? BigDecimal.ZERO : value);
+ }
+ return result.setScale(AMOUNT_SCALE, RoundingMode.HALF_UP);
+ }
+
+ private BigDecimal productRatio(BigDecimal... values) {
+ BigDecimal result = BigDecimal.ONE;
+ for (BigDecimal value : values) {
+ result = result.multiply(value == null ? BigDecimal.ONE : value);
+ }
+ return result.setScale(RATIO_SCALE, RoundingMode.HALF_UP);
+ }
+
+ private BigDecimal divideAmount(BigDecimal dividend, BigDecimal divisor) {
+ if (dividend == null || divisor == null || divisor.compareTo(BigDecimal.ZERO) == 0) {
+ return ZERO_AMOUNT;
+ }
+ return dividend.divide(divisor, AMOUNT_SCALE, RoundingMode.HALF_UP);
+ }
+
+ private BigDecimal oneMinus(BigDecimal value) {
+ return BigDecimal.ONE.subtract(ratio(value)).setScale(RATIO_SCALE, RoundingMode.HALF_UP);
+ }
+
+ private BigDecimal defaultFactor(BigDecimal value) {
+ return value == null ? ONE_RATIO : ratio(value);
+ }
+
+ private BigDecimal amount(BigDecimal value) {
+ return value == null ? ZERO_AMOUNT : value.setScale(AMOUNT_SCALE, RoundingMode.HALF_UP);
+ }
+
+ private BigDecimal amountNullable(BigDecimal value) {
+ return value == null ? null : amount(value);
+ }
+
+ private BigDecimal factor(BigDecimal value) {
+ return value == null ? BigDecimal.ZERO.setScale(FACTOR_SCALE, RoundingMode.HALF_UP)
+ : value.setScale(FACTOR_SCALE, RoundingMode.HALF_UP);
+ }
+
+ private BigDecimal factorNullable(BigDecimal value) {
+ return value == null ? null : factor(value);
+ }
+
+ private BigDecimal ratio(BigDecimal value) {
+ return value == null ? ZERO_RATIO : value.setScale(RATIO_SCALE, RoundingMode.HALF_UP);
+ }
+
+ private BigDecimal ratioNullable(BigDecimal value) {
+ return value == null ? null : ratio(value);
+ }
+
+ private boolean isPositive(BigDecimal value) {
+ return value != null && value.compareTo(BigDecimal.ZERO) > 0;
+ }
+
+ private void refreshQuarterDistributionAmounts(ProjectPlanningDO planning) {
+ List quarterList = projectPlanningQuarterMapper.selectListByPlanningId(planning.getId());
+ for (ProjectPlanningQuarterDO quarter : quarterList) {
+ quarter.setDistributionAmount(calculateQuarterDistributionAmount(planning, quarter.getDistributionRatio()));
+ projectPlanningQuarterMapper.updateById(quarter);
+ }
+ }
+
+ private BigDecimal calculateQuarterDistributionAmount(ProjectPlanningDO planning, BigDecimal distributionRatio) {
+ return multiplyAmount(
+ planning.getAssessmentOutputValue(),
+ planning.getTotalDistributionAmount(),
+ ratio(distributionRatio));
+ }
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/planningquarter/ProjectPlanningQuarterService.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/planningquarter/ProjectPlanningQuarterService.java
new file mode 100644
index 0000000..e3090b3
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/planningquarter/ProjectPlanningQuarterService.java
@@ -0,0 +1,27 @@
+package cn.iocoder.lyzsys.module.tjt.service.planningquarter;
+
+import cn.iocoder.lyzsys.module.tjt.controller.admin.planningquarter.vo.ProjectPlanningQuarterSaveReqVO;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.planningquarter.ProjectPlanningQuarterDO;
+
+import java.util.List;
+
+/**
+ * 季度分配 Service 接口
+ *
+ * @author Codex
+ */
+public interface ProjectPlanningQuarterService {
+
+ Long createProjectPlanningQuarter(ProjectPlanningQuarterSaveReqVO createReqVO);
+
+ void updateProjectPlanningQuarter(ProjectPlanningQuarterSaveReqVO updateReqVO);
+
+ void deleteProjectPlanningQuarter(Long id);
+
+ void deleteProjectPlanningQuarterList(List ids);
+
+ ProjectPlanningQuarterDO getProjectPlanningQuarter(Long id);
+
+ List getProjectPlanningQuarterListByPlanningId(Long planningId);
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/planningquarter/ProjectPlanningQuarterServiceImpl.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/planningquarter/ProjectPlanningQuarterServiceImpl.java
new file mode 100644
index 0000000..0d6189a
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/planningquarter/ProjectPlanningQuarterServiceImpl.java
@@ -0,0 +1,146 @@
+package cn.iocoder.lyzsys.module.tjt.service.planningquarter;
+
+import cn.iocoder.lyzsys.framework.common.util.object.BeanUtils;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.planningquarter.vo.ProjectPlanningQuarterSaveReqVO;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.planning.ProjectPlanningDO;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.planningquarter.ProjectPlanningQuarterDO;
+import cn.iocoder.lyzsys.module.tjt.dal.mysql.planning.ProjectPlanningMapper;
+import cn.iocoder.lyzsys.module.tjt.dal.mysql.planningquarter.ProjectPlanningQuarterMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.List;
+
+import static cn.iocoder.lyzsys.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_PLANNING_QUARTER_DUPLICATE;
+import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_PLANNING_QUARTER_NOT_EXISTS;
+import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_PLANNING_QUARTER_PLANNING_NOT_EXISTS;
+
+/**
+ * 季度分配 Service 实现类
+ *
+ * @author Codex
+ */
+@Service
+@Validated
+public class ProjectPlanningQuarterServiceImpl implements ProjectPlanningQuarterService {
+
+ private static final BigDecimal ZERO_AMOUNT = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+ private static final BigDecimal ZERO_RATIO = BigDecimal.ZERO.setScale(4, RoundingMode.HALF_UP);
+
+ @Resource
+ private ProjectPlanningQuarterMapper projectPlanningQuarterMapper;
+ @Resource
+ private ProjectPlanningMapper projectPlanningMapper;
+
+ @Override
+ public Long createProjectPlanningQuarter(ProjectPlanningQuarterSaveReqVO createReqVO) {
+ ProjectPlanningDO planning = validatePlanningExists(createReqVO.getPlanningId());
+ if (isZeroQuarter(createReqVO)) {
+ return null;
+ }
+ validateQuarterUnique(null, createReqVO.getPlanningId(),
+ createReqVO.getDistributionYear(), createReqVO.getQuarterNo());
+ ProjectPlanningQuarterDO quarter = BeanUtils.toBean(createReqVO, ProjectPlanningQuarterDO.class);
+ prepareQuarter(quarter, planning);
+ projectPlanningQuarterMapper.insert(quarter);
+ return quarter.getId();
+ }
+
+ @Override
+ public void updateProjectPlanningQuarter(ProjectPlanningQuarterSaveReqVO updateReqVO) {
+ ProjectPlanningQuarterDO dbQuarter = validateQuarterExists(updateReqVO.getId());
+ ProjectPlanningDO planning = validatePlanningExists(updateReqVO.getPlanningId());
+ if (isZeroQuarter(updateReqVO)) {
+ projectPlanningQuarterMapper.deleteById(dbQuarter.getId());
+ return;
+ }
+ validateQuarterUnique(updateReqVO.getId(), updateReqVO.getPlanningId(),
+ updateReqVO.getDistributionYear(), updateReqVO.getQuarterNo());
+ ProjectPlanningQuarterDO updateObj = BeanUtils.toBean(updateReqVO, ProjectPlanningQuarterDO.class);
+ prepareQuarter(updateObj, planning);
+ projectPlanningQuarterMapper.updateById(updateObj);
+ }
+
+ @Override
+ public void deleteProjectPlanningQuarter(Long id) {
+ validateQuarterExists(id);
+ projectPlanningQuarterMapper.deleteById(id);
+ }
+
+ @Override
+ public void deleteProjectPlanningQuarterList(List ids) {
+ ids.forEach(this::validateQuarterExists);
+ projectPlanningQuarterMapper.deleteBatchIds(ids);
+ }
+
+ @Override
+ public ProjectPlanningQuarterDO getProjectPlanningQuarter(Long id) {
+ return projectPlanningQuarterMapper.selectById(id);
+ }
+
+ @Override
+ public List getProjectPlanningQuarterListByPlanningId(Long planningId) {
+ validatePlanningExists(planningId);
+ return projectPlanningQuarterMapper.selectListByPlanningId(planningId);
+ }
+
+ private void validateQuarterUnique(Long id, Long planningId, Integer distributionYear, Integer quarterNo) {
+ ProjectPlanningQuarterDO quarter = projectPlanningQuarterMapper.selectByPlanningIdAndDistributionYearAndQuarter(
+ planningId, distributionYear, quarterNo);
+ if (quarter == null) {
+ return;
+ }
+ if (id == null || !quarter.getId().equals(id)) {
+ throw exception(PROJECT_PLANNING_QUARTER_DUPLICATE);
+ }
+ }
+
+ private ProjectPlanningDO validatePlanningExists(Long planningId) {
+ ProjectPlanningDO planning = projectPlanningMapper.selectById(planningId);
+ if (planning == null) {
+ throw exception(PROJECT_PLANNING_QUARTER_PLANNING_NOT_EXISTS);
+ }
+ return planning;
+ }
+
+ private ProjectPlanningQuarterDO validateQuarterExists(Long id) {
+ ProjectPlanningQuarterDO quarter = projectPlanningQuarterMapper.selectById(id);
+ if (quarter == null) {
+ throw exception(PROJECT_PLANNING_QUARTER_NOT_EXISTS);
+ }
+ return quarter;
+ }
+
+ private boolean isZeroQuarter(ProjectPlanningQuarterSaveReqVO reqVO) {
+ return ratio(reqVO.getDistributionRatio()).compareTo(BigDecimal.ZERO) == 0;
+ }
+
+ private void prepareQuarter(ProjectPlanningQuarterDO quarter, ProjectPlanningDO planning) {
+ quarter.setDistributionRatio(ratio(quarter.getDistributionRatio()));
+ quarter.setDistributionAmount(multiplyAmount(
+ planning.getAssessmentOutputValue(),
+ planning.getTotalDistributionAmount(),
+ quarter.getDistributionRatio()));
+ }
+
+ private BigDecimal amount(BigDecimal value) {
+ return value == null ? ZERO_AMOUNT : value.setScale(2, RoundingMode.HALF_UP);
+ }
+
+ private BigDecimal ratio(BigDecimal value) {
+ return value == null ? ZERO_RATIO : value.setScale(4, RoundingMode.HALF_UP);
+ }
+
+ private BigDecimal multiplyAmount(BigDecimal... values) {
+ BigDecimal result = BigDecimal.ONE;
+ for (BigDecimal value : values) {
+ result = result.multiply(value == null ? BigDecimal.ZERO : value);
+ }
+ return result.setScale(2, RoundingMode.HALF_UP);
+ }
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/profit/ProjectProfitService.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/profit/ProjectProfitService.java
new file mode 100644
index 0000000..df9737c
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/profit/ProjectProfitService.java
@@ -0,0 +1,18 @@
+package cn.iocoder.lyzsys.module.tjt.service.profit;
+
+import cn.iocoder.lyzsys.framework.common.pojo.PageResult;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.profit.vo.ProjectProfitPageReqVO;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.profit.vo.ProjectProfitRespVO;
+
+/**
+ * 项目盈亏 Service 接口
+ *
+ * @author Codex
+ */
+public interface ProjectProfitService {
+
+ ProjectProfitRespVO getProjectProfit(Long projectId);
+
+ PageResult getProjectProfitPage(ProjectProfitPageReqVO pageReqVO);
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/profit/ProjectProfitServiceImpl.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/profit/ProjectProfitServiceImpl.java
new file mode 100644
index 0000000..202e01b
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/profit/ProjectProfitServiceImpl.java
@@ -0,0 +1,135 @@
+package cn.iocoder.lyzsys.module.tjt.service.profit;
+
+import cn.iocoder.lyzsys.framework.common.pojo.PageResult;
+import cn.iocoder.lyzsys.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.lyzsys.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.profit.vo.ProjectProfitPageReqVO;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.profit.vo.ProjectProfitRespVO;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.planning.ProjectPlanningDO;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.project.ProjectDO;
+import cn.iocoder.lyzsys.module.tjt.dal.mysql.planning.ProjectPlanningMapper;
+import cn.iocoder.lyzsys.module.tjt.dal.mysql.project.ProjectMapper;
+import cn.iocoder.lyzsys.module.tjt.enums.ProjectPlanningBizTypeConstants;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.lyzsys.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_NOT_EXISTS;
+
+/**
+ * 项目盈亏 Service 实现类
+ *
+ * @author Codex
+ */
+@Service
+@Validated
+public class ProjectProfitServiceImpl implements ProjectProfitService {
+
+ private static final BigDecimal ZERO_AMOUNT = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+ private static final BigDecimal ZERO_RATIO = BigDecimal.ZERO.setScale(4, RoundingMode.HALF_UP);
+
+ @Resource
+ private ProjectMapper projectMapper;
+ @Resource
+ private ProjectPlanningMapper projectPlanningMapper;
+
+ @Override
+ public ProjectProfitRespVO getProjectProfit(Long projectId) {
+ ProjectDO project = validateProjectExists(projectId);
+ List planningList = projectPlanningMapper.selectList(ProjectPlanningDO::getProjectId,
+ Collections.singleton(projectId));
+ return buildProjectProfit(project, planningList);
+ }
+
+ @Override
+ public PageResult getProjectProfitPage(ProjectProfitPageReqVO pageReqVO) {
+ PageResult pageResult = projectMapper.selectPage(pageReqVO, new LambdaQueryWrapperX()
+ .likeIfPresent(ProjectDO::getProjectName, pageReqVO.getProjectName())
+ .eqIfPresent(ProjectDO::getContractSignedFlag, pageReqVO.getContractSignedFlag())
+ .eqIfPresent(ProjectDO::getProjectStartYear, pageReqVO.getProjectStartYear())
+ .betweenIfPresent(ProjectDO::getCreateTime, pageReqVO.getCreateTime())
+ .orderByDesc(ProjectDO::getId));
+ if (pageResult.getList().isEmpty()) {
+ return new PageResult<>(Collections.emptyList(), pageResult.getTotal());
+ }
+ Map> planningMap = CollectionUtils.convertMultiMap(
+ projectPlanningMapper.selectList(ProjectPlanningDO::getProjectId,
+ CollectionUtils.convertSet(pageResult.getList(), ProjectDO::getId)),
+ ProjectPlanningDO::getProjectId);
+ List list = CollectionUtils.convertList(pageResult.getList(),
+ project -> buildProjectProfit(project, planningMap.get(project.getId())));
+ return new PageResult<>(list, pageResult.getTotal());
+ }
+
+ private ProjectProfitRespVO buildProjectProfit(ProjectDO project, List planningList) {
+ List safePlanningList = planningList == null ? Collections.emptyList() : planningList;
+ BigDecimal comprehensivePlanningAmount = ZERO_AMOUNT;
+ BigDecimal subcontractPlanningAmount = ZERO_AMOUNT;
+ BigDecimal majorOutputValue = ZERO_AMOUNT;
+ for (ProjectPlanningDO planning : safePlanningList) {
+ if (ProjectPlanningBizTypeConstants.isComprehensive(planning.getOwnershipType())) {
+ comprehensivePlanningAmount = comprehensivePlanningAmount.add(amount(planning.getPlanningAmount()));
+ continue;
+ }
+ if (ProjectPlanningBizTypeConstants.isSubcontract(planning.getOwnershipType())) {
+ subcontractPlanningAmount = subcontractPlanningAmount.add(amount(planning.getPlanningAmount()));
+ continue;
+ }
+ if (ProjectPlanningBizTypeConstants.isMajor(planning.getOwnershipType())) {
+ majorOutputValue = majorOutputValue.add(amount(planning.getAssessmentOutputValue()));
+ }
+ }
+ BigDecimal expectedKValue = ratio(project.getExpectedKValue());
+ BigDecimal majorExpectedPerformance = majorOutputValue.multiply(expectedKValue).setScale(2, RoundingMode.HALF_UP);
+ BigDecimal finalSettlementAmount = amount(project.getFinalSettlementAmount());
+ BigDecimal profitLossValue = finalSettlementAmount
+ .subtract(comprehensivePlanningAmount)
+ .subtract(subcontractPlanningAmount)
+ .subtract(majorExpectedPerformance)
+ .setScale(2, RoundingMode.HALF_UP);
+ BigDecimal profitLossRate = finalSettlementAmount.compareTo(BigDecimal.ZERO) == 0
+ ? ZERO_RATIO
+ : profitLossValue.divide(finalSettlementAmount, 4, RoundingMode.HALF_UP);
+
+ ProjectProfitRespVO respVO = new ProjectProfitRespVO();
+ respVO.setProjectId(project.getId());
+ respVO.setProjectName(project.getProjectName());
+ respVO.setContractSignedFlag(project.getContractSignedFlag());
+ respVO.setContractAmount(amount(project.getContractAmount()));
+ respVO.setFinalSettlementAmount(finalSettlementAmount);
+ respVO.setComprehensivePlanningAmount(comprehensivePlanningAmount.setScale(2, RoundingMode.HALF_UP));
+ respVO.setSubcontractPlanningAmount(subcontractPlanningAmount.setScale(2, RoundingMode.HALF_UP));
+ respVO.setMajorOutputValue(majorOutputValue.setScale(2, RoundingMode.HALF_UP));
+ respVO.setExpectedKValue(expectedKValue);
+ respVO.setMajorExpectedPerformance(majorExpectedPerformance);
+ respVO.setProfitLossValue(profitLossValue);
+ respVO.setProfitLossRate(profitLossRate);
+ respVO.setProjectStartYear(project.getProjectStartYear());
+ respVO.setCreateTime(project.getCreateTime());
+ return respVO;
+ }
+
+ private ProjectDO validateProjectExists(Long projectId) {
+ ProjectDO project = projectMapper.selectById(projectId);
+ if (project == null) {
+ throw exception(PROJECT_NOT_EXISTS);
+ }
+ return project;
+ }
+
+ private BigDecimal amount(BigDecimal value) {
+ return value == null ? ZERO_AMOUNT : value.setScale(2, RoundingMode.HALF_UP);
+ }
+
+ private BigDecimal ratio(BigDecimal value) {
+ return value == null ? ZERO_RATIO : value.setScale(4, RoundingMode.HALF_UP);
+ }
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/project/ProjectService.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/project/ProjectService.java
new file mode 100644
index 0000000..009e057
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/project/ProjectService.java
@@ -0,0 +1,29 @@
+package cn.iocoder.lyzsys.module.tjt.service.project;
+
+import cn.iocoder.lyzsys.framework.common.pojo.PageResult;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.project.vo.ProjectPageReqVO;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.project.vo.ProjectSaveReqVO;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.project.ProjectDO;
+
+import java.util.List;
+
+/**
+ * 项目 Service 接口
+ *
+ * @author Codex
+ */
+public interface ProjectService {
+
+ Long createProject(ProjectSaveReqVO createReqVO);
+
+ void updateProject(ProjectSaveReqVO updateReqVO);
+
+ void deleteProject(Long id);
+
+ void deleteProjectList(List ids);
+
+ PageResult getProjectPage(ProjectPageReqVO pageReqVO);
+
+ ProjectDO getProject(Long id);
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/project/ProjectServiceImpl.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/project/ProjectServiceImpl.java
new file mode 100644
index 0000000..804c01f
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/project/ProjectServiceImpl.java
@@ -0,0 +1,72 @@
+package cn.iocoder.lyzsys.module.tjt.service.project;
+
+import cn.iocoder.lyzsys.framework.common.pojo.PageResult;
+import cn.iocoder.lyzsys.framework.common.util.object.BeanUtils;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.project.vo.ProjectPageReqVO;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.project.vo.ProjectSaveReqVO;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.project.ProjectDO;
+import cn.iocoder.lyzsys.module.tjt.dal.mysql.project.ProjectMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+import static cn.iocoder.lyzsys.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_NOT_EXISTS;
+
+/**
+ * 项目 Service 实现类
+ *
+ * @author Codex
+ */
+@Service
+@Validated
+public class ProjectServiceImpl implements ProjectService {
+
+ @Resource
+ private ProjectMapper projectMapper;
+
+ @Override
+ public Long createProject(ProjectSaveReqVO createReqVO) {
+ ProjectDO project = BeanUtils.toBean(createReqVO, ProjectDO.class);
+ projectMapper.insert(project);
+ return project.getId();
+ }
+
+ @Override
+ public void updateProject(ProjectSaveReqVO updateReqVO) {
+ validateProjectExists(updateReqVO.getId());
+ ProjectDO updateObj = BeanUtils.toBean(updateReqVO, ProjectDO.class);
+ projectMapper.updateById(updateObj);
+ }
+
+ @Override
+ public void deleteProject(Long id) {
+ validateProjectExists(id);
+ projectMapper.deleteById(id);
+ }
+
+ @Override
+ public void deleteProjectList(List ids) {
+ ids.forEach(this::validateProjectExists);
+ projectMapper.deleteBatchIds(ids);
+ }
+
+ @Override
+ public PageResult getProjectPage(ProjectPageReqVO pageReqVO) {
+ return projectMapper.selectPage(pageReqVO);
+ }
+
+ @Override
+ public ProjectDO getProject(Long id) {
+ return projectMapper.selectById(id);
+ }
+
+ private void validateProjectExists(Long id) {
+ if (projectMapper.selectById(id) == null) {
+ throw exception(PROJECT_NOT_EXISTS);
+ }
+ }
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/specialtyrolesplit/SpecialtyRoleSplitService.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/specialtyrolesplit/SpecialtyRoleSplitService.java
new file mode 100644
index 0000000..8db4a8d
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/specialtyrolesplit/SpecialtyRoleSplitService.java
@@ -0,0 +1,23 @@
+package cn.iocoder.lyzsys.module.tjt.service.specialtyrolesplit;
+
+import cn.iocoder.lyzsys.module.tjt.controller.admin.specialtyrolesplit.vo.SpecialtyRoleSplitBatchSaveReqVO;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.specialtyrolesplit.vo.SpecialtyRoleSplitRespVO;
+
+import java.util.List;
+
+/**
+ * 页面5角色比例 Service 接口
+ *
+ * @author Codex
+ */
+public interface SpecialtyRoleSplitService {
+
+ List getSpecialtyRoleSplitListByPlanningId(Long planningId);
+
+ void saveSpecialtyRoleSplitBatch(SpecialtyRoleSplitBatchSaveReqVO reqVO);
+
+ void deleteByOutputSplitId(Long outputSplitId);
+
+ void deleteByOutputSplitIds(List outputSplitIds);
+
+}
diff --git a/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/specialtyrolesplit/SpecialtyRoleSplitServiceImpl.java b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/specialtyrolesplit/SpecialtyRoleSplitServiceImpl.java
new file mode 100644
index 0000000..d2d5e20
--- /dev/null
+++ b/lyzsys-module-tjt/src/main/java/cn/iocoder/lyzsys/module/tjt/service/specialtyrolesplit/SpecialtyRoleSplitServiceImpl.java
@@ -0,0 +1,402 @@
+package cn.iocoder.lyzsys.module.tjt.service.specialtyrolesplit;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.lyzsys.framework.common.util.json.JsonUtils;
+import cn.iocoder.lyzsys.framework.common.util.object.BeanUtils;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.specialtyrolesplit.vo.SpecialtyRolePersonRespVO;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.specialtyrolesplit.vo.SpecialtyRolePersonSaveReqVO;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.specialtyrolesplit.vo.SpecialtyRoleSplitBatchSaveReqVO;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.specialtyrolesplit.vo.SpecialtyRoleSplitRespVO;
+import cn.iocoder.lyzsys.module.tjt.controller.admin.specialtyrolesplit.vo.SpecialtyRoleSplitSaveItemReqVO;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.outputsplit.ProjectOutputSplitDO;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.planning.ProjectPlanningDO;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.project.ProjectDO;
+import cn.iocoder.lyzsys.module.tjt.dal.dataobject.specialtyrolesplit.SpecialtyRoleSplitDO;
+import cn.iocoder.lyzsys.module.tjt.dal.mysql.planning.ProjectPlanningMapper;
+import cn.iocoder.lyzsys.module.tjt.dal.mysql.project.ProjectMapper;
+import cn.iocoder.lyzsys.module.tjt.dal.mysql.specialtyrolesplit.SpecialtyRoleSplitMapper;
+import cn.iocoder.lyzsys.module.tjt.enums.OutputSplitBizConstants;
+import cn.iocoder.lyzsys.module.tjt.service.outputsplit.ProjectOutputSplitService;
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static cn.iocoder.lyzsys.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_NOT_EXISTS;
+import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_OUTPUT_SPLIT_PLANNING_NOT_EXISTS;
+import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.SPECIALTY_ROLE_SPLIT_DESIGN_PERSON_REQUIRED;
+import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.SPECIALTY_ROLE_SPLIT_PERSON_INVALID;
+import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.SPECIALTY_ROLE_SPLIT_PERSON_RATIO_INVALID;
+import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.SPECIALTY_ROLE_SPLIT_ROLE_INVALID;
+import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.SPECIALTY_ROLE_SPLIT_ROLE_RATIO_INVALID;
+import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.SPECIALTY_ROLE_SPLIT_SPECIALTY_INVALID;
+
+/**
+ * 页面5角色比例 Service 实现类
+ *
+ * @author Codex
+ */
+@Service
+@Validated
+public class SpecialtyRoleSplitServiceImpl implements SpecialtyRoleSplitService {
+
+ private static final int RATIO_SCALE = 4;
+ private static final int AMOUNT_SCALE = 2;
+ private static final TypeReference> PERSON_LIST_TYPE =
+ new TypeReference>() {};
+ private static final BigDecimal ZERO_RATIO = BigDecimal.ZERO.setScale(RATIO_SCALE, RoundingMode.HALF_UP);
+ private static final BigDecimal ONE_RATIO = BigDecimal.ONE.setScale(RATIO_SCALE, RoundingMode.HALF_UP);
+ private static final BigDecimal ZERO_AMOUNT = BigDecimal.ZERO.setScale(AMOUNT_SCALE, RoundingMode.HALF_UP);
+
+ @Resource
+ private SpecialtyRoleSplitMapper specialtyRoleSplitMapper;
+ @Resource
+ private ProjectOutputSplitService projectOutputSplitService;
+ @Resource
+ private ProjectPlanningMapper projectPlanningMapper;
+ @Resource
+ private ProjectMapper projectMapper;
+
+ @Override
+ public List getSpecialtyRoleSplitListByPlanningId(Long planningId) {
+ ProjectPlanningDO planning = validatePlanningExists(planningId);
+ ProjectDO project = validateProjectExists(planning.getProjectId());
+ ProjectOutputSplitDO outputSplit = projectOutputSplitService.getOrCreateProjectOutputSplit(planningId);
+ List dbList = specialtyRoleSplitMapper.selectListByOutputSplitId(outputSplit.getId());
+ Map dbMap = dbList.stream().collect(Collectors.toMap(
+ item -> item.getSpecialtyCode() + ":" + item.getRoleCode(), item -> item, (a, b) -> b));
+ List result = new ArrayList<>();
+ BigDecimal assessmentOutputValue = planning.getAssessmentOutputValue();
+ for (OutputSplitBizConstants.SpecialtyItem specialtyItem : OutputSplitBizConstants.SPECIALTY_ITEMS) {
+ BigDecimal specialtyAmount = projectOutputSplitService.getSpecialtyAmount(
+ outputSplit, assessmentOutputValue, specialtyItem.getCode());
+ for (OutputSplitBizConstants.RoleItem roleItem : OutputSplitBizConstants.ROLE_ITEMS) {
+ SpecialtyRoleSplitDO dbItem = dbMap.get(specialtyItem.getCode() + ":" + roleItem.getCode());
+ BigDecimal roleRatio = getStoredRoleRatio(dbItem, roleItem.getCode());
+ BigDecimal roleAmount = multiplyAmount(specialtyAmount, roleRatio);
+ List persons = buildRespPersons(dbItem, roleAmount, roleRatio);
+ SpecialtyRoleSplitRespVO respVO = dbItem == null
+ ? new SpecialtyRoleSplitRespVO()
+ : BeanUtils.toBean(dbItem, SpecialtyRoleSplitRespVO.class);
+ if (respVO.getId() == null) {
+ respVO.setOutputSplitId(outputSplit.getId());
+ respVO.setSortNo(roleItem.getSortNo());
+ }
+ respVO.setPlanningId(planning.getId());
+ respVO.setProjectName(project.getProjectName());
+ respVO.setPlanningContent(planning.getPlanningContent());
+ respVO.setSpecialtyCode(specialtyItem.getCode());
+ respVO.setSpecialtyName(specialtyItem.getName());
+ respVO.setSpecialtyAmount(specialtyAmount);
+ respVO.setRoleCode(roleItem.getCode());
+ respVO.setRoleName(roleItem.getName());
+ respVO.setRoleRatio(roleRatio);
+ respVO.setRoleAmount(roleAmount);
+ respVO.setPersons(persons);
+ result.add(respVO);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public void saveSpecialtyRoleSplitBatch(SpecialtyRoleSplitBatchSaveReqVO reqVO) {
+ ProjectPlanningDO planning = validatePlanningExists(reqVO.getPlanningId());
+ ProjectOutputSplitDO outputSplit = projectOutputSplitService.getOrCreateProjectOutputSplit(reqVO.getPlanningId());
+ List dbList = specialtyRoleSplitMapper.selectListByOutputSplitId(outputSplit.getId());
+ Map dbMap = dbList.stream().collect(Collectors.toMap(
+ item -> item.getSpecialtyCode() + ":" + item.getRoleCode(), item -> item, (a, b) -> b));
+ Map> groupedMap = buildGroupedInput(reqVO.getItems());
+ BigDecimal assessmentOutputValue = planning.getAssessmentOutputValue();
+ for (OutputSplitBizConstants.SpecialtyItem specialtyItem : OutputSplitBizConstants.SPECIALTY_ITEMS) {
+ Map roleMap = groupedMap.getOrDefault(
+ specialtyItem.getCode(), new LinkedHashMap<>());
+ Map ratioMap = new LinkedHashMap<>();
+ Map> personMap = new LinkedHashMap<>();
+ BigDecimal roleTotal = ZERO_RATIO;
+ BigDecimal specialtyAmount = projectOutputSplitService.getSpecialtyAmount(
+ outputSplit, assessmentOutputValue, specialtyItem.getCode());
+ for (OutputSplitBizConstants.RoleItem roleItem : OutputSplitBizConstants.ROLE_ITEMS) {
+ BigDecimal roleRatio = getSaveRoleRatio(roleMap, roleItem.getCode());
+ BigDecimal roleAmount = multiplyAmount(specialtyAmount, roleRatio);
+ List persons = normalizePersons(
+ getSavePersons(roleMap, roleItem.getCode()));
+ validateRolePersons(roleItem.getCode(), roleAmount, persons);
+ ratioMap.put(roleItem.getCode(), roleRatio);
+ personMap.put(roleItem.getCode(), persons);
+ roleTotal = roleTotal.add(roleRatio).setScale(RATIO_SCALE, RoundingMode.HALF_UP);
+ }
+ validateRoleTotal(roleTotal);
+ for (OutputSplitBizConstants.RoleItem roleItem : OutputSplitBizConstants.ROLE_ITEMS) {
+ upsertRole(dbMap, outputSplit.getId(), specialtyItem.getCode(), specialtyItem.getName(),
+ roleItem.getCode(), ratioMap.get(roleItem.getCode()),
+ toPersonJson(personMap.get(roleItem.getCode())));
+ }
+ }
+ }
+
+ @Override
+ public void deleteByOutputSplitId(Long outputSplitId) {
+ specialtyRoleSplitMapper.delete(SpecialtyRoleSplitDO::getOutputSplitId, outputSplitId);
+ }
+
+ @Override
+ public void deleteByOutputSplitIds(List outputSplitIds) {
+ if (outputSplitIds == null || outputSplitIds.isEmpty()) {
+ return;
+ }
+ specialtyRoleSplitMapper.delete(new cn.iocoder.lyzsys.framework.mybatis.core.query.LambdaQueryWrapperX()
+ .in(SpecialtyRoleSplitDO::getOutputSplitId, outputSplitIds));
+ }
+
+ private Map> buildGroupedInput(List items) {
+ Map> result = new LinkedHashMap<>();
+ for (SpecialtyRoleSplitSaveItemReqVO item : items) {
+ if (!OutputSplitBizConstants.isValidSpecialtyCode(item.getSpecialtyCode())) {
+ throw exception(SPECIALTY_ROLE_SPLIT_SPECIALTY_INVALID);
+ }
+ if (!OutputSplitBizConstants.isValidRoleCode(item.getRoleCode())) {
+ throw exception(SPECIALTY_ROLE_SPLIT_ROLE_INVALID);
+ }
+ result.computeIfAbsent(item.getSpecialtyCode(), key -> new LinkedHashMap<>())
+ .put(item.getRoleCode(), item);
+ }
+ return result;
+ }
+
+ private void upsertRole(Map dbMap, Long outputSplitId, String specialtyCode,
+ String specialtyName, String roleCode, BigDecimal roleRatio, String personNames) {
+ String key = specialtyCode + ":" + roleCode;
+ SpecialtyRoleSplitDO dbItem = dbMap.get(key);
+ Integer sortNo = dbItem != null && dbItem.getSortNo() != null
+ ? dbItem.getSortNo()
+ : OutputSplitBizConstants.getSpecialtySortNo(specialtyCode) * 10
+ + OutputSplitBizConstants.getRoleSortNo(roleCode);
+ if (dbItem == null) {
+ SpecialtyRoleSplitDO createObj = new SpecialtyRoleSplitDO();
+ createObj.setOutputSplitId(outputSplitId);
+ createObj.setSpecialtyCode(specialtyCode);
+ createObj.setSpecialtyName(specialtyName);
+ createObj.setRoleCode(roleCode);
+ createObj.setRoleName(OutputSplitBizConstants.getRoleName(roleCode));
+ createObj.setRoleRatio(ratio(roleRatio));
+ createObj.setPersonNames(personNames);
+ createObj.setSortNo(sortNo);
+ specialtyRoleSplitMapper.insert(createObj);
+ return;
+ }
+ SpecialtyRoleSplitDO updateObj = new SpecialtyRoleSplitDO();
+ updateObj.setId(dbItem.getId());
+ updateObj.setOutputSplitId(outputSplitId);
+ updateObj.setSpecialtyCode(specialtyCode);
+ updateObj.setSpecialtyName(specialtyName);
+ updateObj.setRoleCode(roleCode);
+ updateObj.setRoleName(OutputSplitBizConstants.getRoleName(roleCode));
+ updateObj.setRoleRatio(ratio(roleRatio));
+ updateObj.setPersonNames(personNames);
+ updateObj.setSortNo(sortNo);
+ specialtyRoleSplitMapper.updateById(updateObj);
+ }
+
+ private List getSavePersons(Map roleMap,
+ String roleCode) {
+ SpecialtyRoleSplitSaveItemReqVO item = roleMap.get(roleCode);
+ return item == null ? new ArrayList<>() : item.getPersons();
+ }
+
+ private BigDecimal getSaveRoleRatio(Map roleMap, String roleCode) {
+ SpecialtyRoleSplitSaveItemReqVO item = roleMap.get(roleCode);
+ BigDecimal roleRatio = item == null ? ZERO_RATIO : ratio(item.getRoleRatio());
+ if (roleRatio.compareTo(ZERO_RATIO) < 0 || roleRatio.compareTo(ONE_RATIO) > 0) {
+ throw exception(SPECIALTY_ROLE_SPLIT_ROLE_RATIO_INVALID);
+ }
+ return roleRatio;
+ }
+
+ private List normalizePersons(List persons) {
+ List result = new ArrayList<>();
+ if (persons == null || persons.isEmpty()) {
+ return result;
+ }
+ for (SpecialtyRolePersonSaveReqVO person : persons) {
+ if (person == null) {
+ continue;
+ }
+ String personName = StrUtil.trim(person.getPersonName());
+ BigDecimal personRatio = person.getPersonRatio();
+ boolean hasName = StrUtil.isNotBlank(personName);
+ boolean hasRatio = personRatio != null;
+ if (!hasName && !hasRatio) {
+ continue;
+ }
+ if (!hasName || !hasRatio) {
+ throw exception(SPECIALTY_ROLE_SPLIT_PERSON_INVALID);
+ }
+ SpecialtyRolePersonSaveReqVO normalized = new SpecialtyRolePersonSaveReqVO();
+ normalized.setPersonName(personName);
+ BigDecimal normalizedRatio = ratio(personRatio);
+ if (normalizedRatio.compareTo(ZERO_RATIO) < 0 || normalizedRatio.compareTo(ONE_RATIO) > 0) {
+ throw exception(SPECIALTY_ROLE_SPLIT_PERSON_RATIO_INVALID);
+ }
+ normalized.setPersonRatio(normalizedRatio);
+ result.add(normalized);
+ }
+ return result;
+ }
+
+ private void validateRolePersons(String roleCode, BigDecimal roleAmount, List persons) {
+ BigDecimal personTotal = sumPersonRatios(persons);
+ if (personTotal.compareTo(ONE_RATIO) > 0) {
+ throw exception(SPECIALTY_ROLE_SPLIT_PERSON_RATIO_INVALID);
+ }
+ if (OutputSplitBizConstants.ROLE_DESIGN.equals(roleCode)
+ && roleAmount.compareTo(ZERO_AMOUNT) > 0
+ && persons.isEmpty()) {
+ throw exception(SPECIALTY_ROLE_SPLIT_DESIGN_PERSON_REQUIRED);
+ }
+ }
+
+ private void validateRoleTotal(BigDecimal roleTotal) {
+ if (ratio(roleTotal).compareTo(ONE_RATIO) != 0) {
+ throw exception(SPECIALTY_ROLE_SPLIT_ROLE_RATIO_INVALID);
+ }
+ }
+
+ private BigDecimal getStoredRoleRatio(SpecialtyRoleSplitDO dbItem, String roleCode) {
+ if (dbItem == null) {
+ return OutputSplitBizConstants.ROLE_DESIGN.equals(roleCode) ? ONE_RATIO : ZERO_RATIO;
+ }
+ return ratio(dbItem.getRoleRatio());
+ }
+
+ private List buildRespPersons(SpecialtyRoleSplitDO dbItem, BigDecimal roleAmount,
+ BigDecimal roleRatio) {
+ List result = new ArrayList<>();
+ for (SpecialtyRolePersonSaveReqVO person : parseStoredPersons(dbItem, roleRatio)) {
+ SpecialtyRolePersonRespVO respVO = new SpecialtyRolePersonRespVO();
+ respVO.setPersonName(person.getPersonName());
+ respVO.setPersonRatio(ratio(person.getPersonRatio()));
+ respVO.setPersonAmount(multiplyAmount(roleAmount, respVO.getPersonRatio()));
+ result.add(respVO);
+ }
+ return result;
+ }
+
+ private List parseStoredPersons(SpecialtyRoleSplitDO dbItem, BigDecimal roleRatio) {
+ List result = new ArrayList<>();
+ if (dbItem == null) {
+ return result;
+ }
+ String rawPersons = StrUtil.trimToEmpty(dbItem.getPersonNames());
+ BigDecimal storedRoleRatio = ratio(roleRatio);
+ if (StrUtil.isNotBlank(rawPersons) && JsonUtils.isJson(rawPersons)) {
+ List jsonPersons = JsonUtils.parseObjectQuietly(rawPersons, PERSON_LIST_TYPE);
+ if (jsonPersons != null) {
+ for (SpecialtyRolePersonSaveReqVO jsonPerson : jsonPersons) {
+ if (jsonPerson == null) {
+ continue;
+ }
+ String personName = StrUtil.trimToEmpty(jsonPerson.getPersonName());
+ BigDecimal personRatio = jsonPerson.getPersonRatio();
+ if (StrUtil.isBlank(personName) && personRatio == null) {
+ continue;
+ }
+ SpecialtyRolePersonSaveReqVO person = new SpecialtyRolePersonSaveReqVO();
+ person.setPersonName(personName);
+ person.setPersonRatio(ratio(personRatio));
+ result.add(person);
+ }
+ if (!result.isEmpty()) {
+ if (storedRoleRatio.compareTo(ZERO_RATIO) > 0
+ && sumPersonRatios(result).compareTo(storedRoleRatio) == 0) {
+ return convertLegacyPersons(result, storedRoleRatio);
+ }
+ return result;
+ }
+ }
+ }
+ if (StrUtil.isNotBlank(rawPersons)) {
+ result.add(buildStoredPerson(rawPersons, storedRoleRatio.compareTo(ZERO_RATIO) > 0 ? ONE_RATIO : ZERO_RATIO));
+ return result;
+ }
+ return result;
+ }
+
+ private List convertLegacyPersons(List persons,
+ BigDecimal roleRatio) {
+ List result = new ArrayList<>();
+ if (roleRatio.compareTo(ZERO_RATIO) <= 0) {
+ return persons;
+ }
+ for (SpecialtyRolePersonSaveReqVO person : persons) {
+ SpecialtyRolePersonSaveReqVO converted = new SpecialtyRolePersonSaveReqVO();
+ converted.setPersonName(person.getPersonName());
+ converted.setPersonRatio(ratio(person.getPersonRatio().divide(roleRatio, RATIO_SCALE, RoundingMode.HALF_UP)));
+ result.add(converted);
+ }
+ return result;
+ }
+
+ private SpecialtyRolePersonSaveReqVO buildStoredPerson(String personName, BigDecimal personRatio) {
+ SpecialtyRolePersonSaveReqVO person = new SpecialtyRolePersonSaveReqVO();
+ person.setPersonName(personName);
+ person.setPersonRatio(ratio(personRatio));
+ return person;
+ }
+
+ private String toPersonJson(List persons) {
+ return persons == null || persons.isEmpty() ? null : JsonUtils.toJsonString(persons);
+ }
+
+ private BigDecimal sumPersonRatios(List persons) {
+ BigDecimal sum = ZERO_RATIO;
+ if (persons == null || persons.isEmpty()) {
+ return sum;
+ }
+ for (SpecialtyRolePersonSaveReqVO person : persons) {
+ if (person == null) {
+ continue;
+ }
+ sum = sum.add(ratio(person.getPersonRatio())).setScale(RATIO_SCALE, RoundingMode.HALF_UP);
+ }
+ return sum;
+ }
+
+ private ProjectPlanningDO validatePlanningExists(Long planningId) {
+ ProjectPlanningDO planning = projectPlanningMapper.selectById(planningId);
+ if (planning == null) {
+ throw exception(PROJECT_OUTPUT_SPLIT_PLANNING_NOT_EXISTS);
+ }
+ return planning;
+ }
+
+ private ProjectDO validateProjectExists(Long projectId) {
+ ProjectDO project = projectMapper.selectById(projectId);
+ if (project == null) {
+ throw exception(PROJECT_NOT_EXISTS);
+ }
+ return project;
+ }
+
+ private BigDecimal multiplyAmount(BigDecimal left, BigDecimal right) {
+ return amount(left).multiply(ratio(right)).setScale(AMOUNT_SCALE, RoundingMode.HALF_UP);
+ }
+
+ private BigDecimal amount(BigDecimal value) {
+ return value == null ? ZERO_AMOUNT : value.setScale(AMOUNT_SCALE, RoundingMode.HALF_UP);
+ }
+
+ private BigDecimal ratio(BigDecimal value) {
+ return value == null ? ZERO_RATIO : value.setScale(RATIO_SCALE, RoundingMode.HALF_UP);
+ }
+
+}
diff --git a/lyzsys-server/pom.xml b/lyzsys-server/pom.xml
index 92ce35f..241f783 100644
--- a/lyzsys-server/pom.xml
+++ b/lyzsys-server/pom.xml
@@ -33,7 +33,7 @@
cn.iocoder.boot
- lyzsys-module-demo
+ lyzsys-module-tjt
${revision}
diff --git a/lyzsys-server/src/main/resources/application-local.yaml b/lyzsys-server/src/main/resources/application-local.yaml
index e476b49..9b1eba0 100644
--- a/lyzsys-server/src/main/resources/application-local.yaml
+++ b/lyzsys-server/src/main/resources/application-local.yaml
@@ -39,7 +39,7 @@ spring:
time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒(1 分钟)
min-evictable-idle-time-millis: 600000 # 配置一个连接在池中最小生存的时间,单位:毫秒(10 分钟)
max-evictable-idle-time-millis: 1800000 # 配置一个连接在池中最大生存的时间,单位:毫秒(30 分钟)
- validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效
+ validation-query: SELECT 1 # 配置检测连接是否有效
test-while-idle: true
test-on-borrow: false
test-on-return: false
diff --git a/pom.xml b/pom.xml
index 50791ef..47e3bfb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,7 +19,8 @@
lyzsys-module-infra
- lyzsys-module-demo
+
+ lyzsys-module-tjt