diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/BoqCatalogItemController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/BoqCatalogItemController.java index b8b6f7b..df9f654 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/BoqCatalogItemController.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/BoqCatalogItemController.java @@ -1,20 +1,29 @@ package com.yhy.module.core.controller.admin.boq; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import com.yhy.module.core.controller.admin.boq.vo.*; +import com.yhy.module.core.controller.admin.boq.vo.BoqCatalogItemBindQuotaReqVO; +import com.yhy.module.core.controller.admin.boq.vo.BoqCatalogItemRespVO; +import com.yhy.module.core.controller.admin.boq.vo.BoqCatalogItemSaveReqVO; +import com.yhy.module.core.controller.admin.boq.vo.BoqCatalogItemSwapSortReqVO; import com.yhy.module.core.service.boq.BoqCatalogItemService; 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 java.util.List; import javax.annotation.Resource; import javax.validation.Valid; -import java.util.List; - -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; /** * 清单配置目录树 Controller @@ -33,14 +42,16 @@ public class BoqCatalogItemController { @PostMapping("/create") @Operation(summary = "创建清单配置目录树节点") @PreAuthorize("@ss.hasPermission('core:boq-catalog-item:create')") - public CommonResult createBoqCatalogItem(@Valid @RequestBody BoqCatalogItemSaveReqVO createReqVO) { + public CommonResult createBoqCatalogItem( + @Validated(BoqCatalogItemSaveReqVO.CreateGroup.class) @RequestBody BoqCatalogItemSaveReqVO createReqVO) { return success(boqCatalogItemService.createBoqCatalogItem(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新清单配置目录树节点") @PreAuthorize("@ss.hasPermission('core:boq-catalog-item:update')") - public CommonResult updateBoqCatalogItem(@Valid @RequestBody BoqCatalogItemSaveReqVO updateReqVO) { + public CommonResult updateBoqCatalogItem( + @Validated(BoqCatalogItemSaveReqVO.UpdateGroup.class) @RequestBody BoqCatalogItemSaveReqVO updateReqVO) { boqCatalogItemService.updateBoqCatalogItem(updateReqVO); return success(true); } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/BoqDetailTreeController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/BoqDetailTreeController.java deleted file mode 100644 index 2936233..0000000 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/BoqDetailTreeController.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.yhy.module.core.controller.admin.boq; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import com.yhy.module.core.controller.admin.boq.vo.BoqDetailTreeRespVO; -import com.yhy.module.core.controller.admin.boq.vo.BoqDetailTreeSaveReqVO; -import com.yhy.module.core.controller.admin.boq.vo.BoqDetailTreeSwapSortReqVO; -import com.yhy.module.core.service.boq.BoqDetailTreeService; -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.yudao.framework.common.pojo.CommonResult.success; - -/** - * 清单明细树 Controller - * - * @author yhy - */ -@Tag(name = "管理后台 - 清单明细树") -@RestController -@RequestMapping("/core/boq/detail-tree") -@Validated -public class BoqDetailTreeController { - - @Resource - private BoqDetailTreeService boqDetailTreeService; - - @PostMapping("/create") - @Operation(summary = "创建清单明细树节点") - @PreAuthorize("@ss.hasPermission('core:boq:detail-tree:create')") - public CommonResult createNode(@Valid @RequestBody BoqDetailTreeSaveReqVO createReqVO) { - return success(boqDetailTreeService.createNode(createReqVO)); - } - - @PutMapping("/update") - @Operation(summary = "更新清单明细树节点") - @PreAuthorize("@ss.hasPermission('core:boq:detail-tree:update')") - public CommonResult updateNode(@Valid @RequestBody BoqDetailTreeSaveReqVO updateReqVO) { - boqDetailTreeService.updateNode(updateReqVO); - return success(true); - } - - @DeleteMapping("/delete") - @Operation(summary = "删除清单明细树节点") - @Parameter(name = "id", description = "节点ID", required = true) - @PreAuthorize("@ss.hasPermission('core:boq:detail-tree:delete')") - public CommonResult deleteNode(@RequestParam("id") Long id) { - boqDetailTreeService.deleteNode(id); - return success(true); - } - - @GetMapping("/get") - @Operation(summary = "获取清单明细树节点详情") - @Parameter(name = "id", description = "节点ID", required = true) - @PreAuthorize("@ss.hasPermission('core:boq:detail-tree:query')") - public CommonResult getNode(@RequestParam("id") Long id) { - return success(boqDetailTreeService.getNode(id)); - } - - @GetMapping("/tree") - @Operation(summary = "获取清单明细树形结构") - @Parameter(name = "boqSubItemId", description = "清单子项ID", required = true) - @PreAuthorize("@ss.hasPermission('core:boq:detail-tree:query')") - public CommonResult> getTree(@RequestParam("boqSubItemId") Long boqSubItemId) { - return success(boqDetailTreeService.getTree(boqSubItemId)); - } - - @GetMapping("/list") - @Operation(summary = "获取清单明细树列表") - @Parameter(name = "boqSubItemId", description = "清单子项ID", required = true) - @PreAuthorize("@ss.hasPermission('core:boq:detail-tree:query')") - public CommonResult> getList(@RequestParam("boqSubItemId") Long boqSubItemId) { - return success(boqDetailTreeService.getList(boqSubItemId)); - } - - @PostMapping("/swap-sort") - @Operation(summary = "交换清单明细树节点排序") - @PreAuthorize("@ss.hasPermission('core:boq:detail-tree:update')") - public CommonResult swapSort(@Valid @RequestBody BoqDetailTreeSwapSortReqVO reqVO) { - boqDetailTreeService.swapSort(reqVO.getNodeId1(), reqVO.getNodeId2()); - return success(true); - } -} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/BoqGuideTreeController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/BoqGuideTreeController.java new file mode 100644 index 0000000..76d2845 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/BoqGuideTreeController.java @@ -0,0 +1,117 @@ +package com.yhy.module.core.controller.admin.boq; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.*; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.boq.vo.BoqGuideTreeRespVO; +import com.yhy.module.core.controller.admin.boq.vo.BoqGuideTreeSaveReqVO; +import com.yhy.module.core.controller.admin.boq.vo.BoqGuideTreeSwapSortReqVO; +import com.yhy.module.core.controller.admin.boq.vo.QuotaCodeValidateRespVO; +import com.yhy.module.core.service.boq.BoqGuideTreeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import javax.annotation.Resource; +import javax.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 清单指引树 Controller + * + * @author yhy + */ +@Tag(name = "管理后台 - 清单指引树") +@RestController +@RequestMapping("/core/boq/guide-tree") +@Validated +public class BoqGuideTreeController { + + @Resource + private BoqGuideTreeService boqGuideTreeService; + + @PostMapping("/create") + @Operation(summary = "创建清单指引树节点") + @PreAuthorize("@ss.hasPermission('core:boq:guide-tree:create')") + public CommonResult createNode(@Valid @RequestBody BoqGuideTreeSaveReqVO createReqVO) { + return success(boqGuideTreeService.createNode(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新清单指引树节点") + @PreAuthorize("@ss.hasPermission('core:boq:guide-tree:update')") + public CommonResult updateNode(@Valid @RequestBody BoqGuideTreeSaveReqVO updateReqVO) { + boqGuideTreeService.updateNode(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除清单指引树节点") + @Parameter(name = "id", description = "节点ID", required = true) + @PreAuthorize("@ss.hasPermission('core:boq:guide-tree:delete')") + public CommonResult deleteNode(@RequestParam("id") Long id) { + boqGuideTreeService.deleteNode(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获取清单指引树节点详情") + @Parameter(name = "id", description = "节点ID", required = true) + @PreAuthorize("@ss.hasPermission('core:boq:guide-tree:query')") + public CommonResult getNode(@RequestParam("id") Long id) { + return success(boqGuideTreeService.getNode(id)); + } + + @GetMapping("/tree") + @Operation(summary = "获取清单指引树形结构") + @Parameter(name = "boqSubItemId", description = "清单子项ID", required = true) + @PreAuthorize("@ss.hasPermission('core:boq:guide-tree:query')") + public CommonResult> getTree(@RequestParam("boqSubItemId") Long boqSubItemId) { + return success(boqGuideTreeService.getTree(boqSubItemId)); + } + + @GetMapping("/list") + @Operation(summary = "获取清单指引树列表") + @Parameter(name = "boqSubItemId", description = "清单子项ID", required = true) + @PreAuthorize("@ss.hasPermission('core:boq:guide-tree:query')") + public CommonResult> getList(@RequestParam("boqSubItemId") Long boqSubItemId) { + return success(boqGuideTreeService.getList(boqSubItemId)); + } + + @PostMapping("/swap-sort") + @Operation(summary = "交换清单指引树节点排序") + @PreAuthorize("@ss.hasPermission('core:boq:guide-tree:update')") + public CommonResult swapSort(@Valid @RequestBody BoqGuideTreeSwapSortReqVO reqVO) { + boqGuideTreeService.swapSort(reqVO.getNodeId1(), reqVO.getNodeId2()); + return success(true); + } + + @GetMapping("/validate-quota-code") + @Operation(summary = "验证定额编码是否存在于绑定范围内") + @Parameter(name = "boqSubItemId", description = "清单子项ID", required = true) + @Parameter(name = "code", description = "定额编码", required = true) + @PreAuthorize("@ss.hasPermission('core:boq:guide-tree:query')") + public CommonResult validateQuotaCode(@RequestParam("boqSubItemId") Long boqSubItemId, + @RequestParam("code") String code) { + return success(boqGuideTreeService.validateQuotaCodeInRange(boqSubItemId, code)); + } + + @GetMapping("/validate-quota-code-with-info") + @Operation(summary = "验证定额编码并返回定额信息(包含名称)") + @Parameter(name = "boqSubItemId", description = "清单子项ID", required = true) + @Parameter(name = "code", description = "定额编码", required = true) + @PreAuthorize("@ss.hasPermission('core:boq:guide-tree:query')") + public CommonResult validateQuotaCodeWithInfo(@RequestParam("boqSubItemId") Long boqSubItemId, + @RequestParam("code") String code) { + return success(boqGuideTreeService.validateQuotaCodeWithInfo(boqSubItemId, code)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/BoqItemTreeController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/BoqItemTreeController.java index 90a0e63..fd80a1c 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/BoqItemTreeController.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/BoqItemTreeController.java @@ -1,6 +1,9 @@ package com.yhy.module.core.controller.admin.boq; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.boq.vo.BoqItemTreeDeleteCheckRespVO; import com.yhy.module.core.controller.admin.boq.vo.BoqItemTreeRespVO; import com.yhy.module.core.controller.admin.boq.vo.BoqItemTreeSaveReqVO; import com.yhy.module.core.controller.admin.boq.vo.BoqItemTreeSwapSortReqVO; @@ -8,15 +11,19 @@ import com.yhy.module.core.service.boq.BoqItemTreeService; 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 java.util.List; import javax.annotation.Resource; import javax.validation.Valid; -import java.util.List; - -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; /** * 清单项树 Controller @@ -35,14 +42,16 @@ public class BoqItemTreeController { @PostMapping("/create") @Operation(summary = "创建清单项树节点") @PreAuthorize("@ss.hasPermission('core:boq:item-tree:create')") - public CommonResult createBoqItemTree(@Valid @RequestBody BoqItemTreeSaveReqVO createReqVO) { + public CommonResult createBoqItemTree( + @Validated(BoqItemTreeSaveReqVO.CreateGroup.class) @RequestBody BoqItemTreeSaveReqVO createReqVO) { return success(boqItemTreeService.createBoqItemTree(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新清单项树节点") @PreAuthorize("@ss.hasPermission('core:boq:item-tree:update')") - public CommonResult updateBoqItemTree(@Valid @RequestBody BoqItemTreeSaveReqVO updateReqVO) { + public CommonResult updateBoqItemTree( + @Validated(BoqItemTreeSaveReqVO.UpdateGroup.class) @RequestBody BoqItemTreeSaveReqVO updateReqVO) { boqItemTreeService.updateBoqItemTree(updateReqVO); return success(true); } @@ -87,4 +96,21 @@ public class BoqItemTreeController { boqItemTreeService.swapSort(swapReqVO); return success(true); } + + @GetMapping("/check-delete") + @Operation(summary = "检查删除清单项树节点前的关联数据") + @Parameter(name = "id", description = "节点ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:boq:item-tree:delete')") + public CommonResult checkDeleteBoqItemTree(@RequestParam("id") Long id) { + return success(boqItemTreeService.checkDeleteBoqItemTree(id)); + } + + @DeleteMapping("/force-delete") + @Operation(summary = "强制删除清单项树节点(级联删除所有关联数据)") + @Parameter(name = "id", description = "节点ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:boq:item-tree:delete')") + public CommonResult forceDeleteBoqItemTree(@RequestParam("id") Long id) { + boqItemTreeService.forceDeleteBoqItemTree(id); + return success(true); + } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/BoqSubItemController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/BoqSubItemController.java index acf6c3c..00a3158 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/BoqSubItemController.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/BoqSubItemController.java @@ -1,5 +1,7 @@ package com.yhy.module.core.controller.admin.boq; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + import cn.iocoder.yudao.framework.common.pojo.CommonResult; import com.yhy.module.core.controller.admin.boq.vo.BoqSubItemRespVO; import com.yhy.module.core.controller.admin.boq.vo.BoqSubItemSaveReqVO; @@ -8,15 +10,19 @@ import com.yhy.module.core.service.boq.BoqSubItemService; 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 java.util.List; import javax.annotation.Resource; import javax.validation.Valid; -import java.util.List; - -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; /** * 清单子项 Controller @@ -35,14 +41,16 @@ public class BoqSubItemController { @PostMapping("/create") @Operation(summary = "创建清单子项") @PreAuthorize("@ss.hasPermission('core:boq:sub-item:create')") - public CommonResult createBoqSubItem(@Valid @RequestBody BoqSubItemSaveReqVO createReqVO) { + public CommonResult createBoqSubItem( + @Validated(BoqSubItemSaveReqVO.CreateGroup.class) @RequestBody BoqSubItemSaveReqVO createReqVO) { return success(boqSubItemService.createBoqSubItem(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新清单子项") @PreAuthorize("@ss.hasPermission('core:boq:sub-item:update')") - public CommonResult updateBoqSubItem(@Valid @RequestBody BoqSubItemSaveReqVO updateReqVO) { + public CommonResult updateBoqSubItem( + @Validated(BoqSubItemSaveReqVO.UpdateGroup.class) @RequestBody BoqSubItemSaveReqVO updateReqVO) { boqSubItemService.updateBoqSubItem(updateReqVO); return success(true); } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqCatalogItemSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqCatalogItemSaveReqVO.java index f7c5789..ff2cca4 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqCatalogItemSaveReqVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqCatalogItemSaveReqVO.java @@ -3,6 +3,7 @@ package com.yhy.module.core.controller.admin.boq.vo; import io.swagger.v3.oas.annotations.media.Schema; import java.util.Map; import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; import lombok.Data; /** @@ -14,22 +15,33 @@ import lombok.Data; @Data public class BoqCatalogItemSaveReqVO { + /** + * 创建时的校验分组 + */ + public interface CreateGroup {} + + /** + * 更新时的校验分组 + */ + public interface UpdateGroup {} + @Schema(description = "主键ID", example = "1") + @NotNull(message = "ID不能为空", groups = UpdateGroup.class) private Long id; @Schema(description = "父节点ID", example = "1") private Long parentId; @Schema(description = "编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "BOQ_GD") - @NotBlank(message = "编码不能为空") + @NotBlank(message = "编码不能为空", groups = CreateGroup.class) private String code; @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "广东") - @NotBlank(message = "名称不能为空") + @NotBlank(message = "名称不能为空", groups = CreateGroup.class) private String name; @Schema(description = "节点类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "province") - @NotBlank(message = "节点类型不能为空") + @NotBlank(message = "节点类型不能为空", groups = CreateGroup.class) private String nodeType; @Schema(description = "排序", example = "1") diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqDetailTreeRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqGuideTreeRespVO.java similarity index 76% rename from yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqDetailTreeRespVO.java rename to yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqGuideTreeRespVO.java index 3c65639..8eefb0e 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqDetailTreeRespVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqGuideTreeRespVO.java @@ -1,19 +1,19 @@ package com.yhy.module.core.controller.admin.boq.vo; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - +import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; +import lombok.Data; /** - * 清单明细树 - Response VO + * 清单指引树 - Response VO * * @author yhy */ -@Schema(description = "管理后台 - 清单明细树 Response VO") +@Schema(description = "管理后台 - 清单指引树 Response VO") @Data -public class BoqDetailTreeRespVO { +public class BoqGuideTreeRespVO { @Schema(description = "节点ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Long id; @@ -33,7 +33,7 @@ public class BoqDetailTreeRespVO { @Schema(description = "单位", example = "m³") private String unit; - @Schema(description = "节点类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "directory") + @Schema(description = "节点类型:directory-目录,quota-定额", requiredMode = Schema.RequiredMode.REQUIRED, example = "directory") private String nodeType; @Schema(description = "定额基价【第三层】节点ID", example = "1003") @@ -51,6 +51,12 @@ public class BoqDetailTreeRespVO { @Schema(description = "排序", example = "1") private Integer sortOrder; + @Schema(description = "除税基价", example = "100.00") + private BigDecimal basePriceExTax; + + @Schema(description = "含税基价", example = "113.00") + private BigDecimal basePriceInTax; + @Schema(description = "层级路径", example = "[\"1\", \"2\"]") private String[] path; @@ -58,7 +64,7 @@ public class BoqDetailTreeRespVO { private Integer level; @Schema(description = "子节点列表") - private List children; + private List children; @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqDetailTreeSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqGuideTreeSaveReqVO.java similarity index 79% rename from yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqDetailTreeSaveReqVO.java rename to yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqGuideTreeSaveReqVO.java index 63bac9a..6dcbe90 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqDetailTreeSaveReqVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqGuideTreeSaveReqVO.java @@ -1,19 +1,18 @@ package com.yhy.module.core.controller.admin.boq.vo; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; +import lombok.Data; /** - * 清单明细树 - 创建/更新 Request VO + * 清单指引树 - 创建/更新 Request VO * * @author yhy */ -@Schema(description = "管理后台 - 清单明细树创建/更新 Request VO") +@Schema(description = "管理后台 - 清单指引树创建/更新 Request VO") @Data -public class BoqDetailTreeSaveReqVO { +public class BoqGuideTreeSaveReqVO { @Schema(description = "节点ID(更新时必填)", example = "1") private Long id; @@ -36,11 +35,11 @@ public class BoqDetailTreeSaveReqVO { @Schema(description = "单位", example = "m³") private String unit; - @Schema(description = "节点类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "directory") + @Schema(description = "节点类型:directory-目录,quota-定额", requiredMode = Schema.RequiredMode.REQUIRED, example = "directory") @NotBlank(message = "节点类型不能为空") private String nodeType; - @Schema(description = "定额基价【第三层】节点ID(仅content类型需要)", example = "1003") + @Schema(description = "定额基价【第三层】节点ID(仅quota类型需要)", example = "1003") private Long quotaCatalogItemId; @Schema(description = "排序", example = "1") diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqDetailTreeSwapSortReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqGuideTreeSwapSortReqVO.java similarity index 78% rename from yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqDetailTreeSwapSortReqVO.java rename to yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqGuideTreeSwapSortReqVO.java index afe5195..d9f5637 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqDetailTreeSwapSortReqVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqGuideTreeSwapSortReqVO.java @@ -1,18 +1,17 @@ package com.yhy.module.core.controller.admin.boq.vo; import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotNull; import lombok.Data; -import javax.validation.constraints.NotNull; - /** - * 清单明细树 - 交换排序 Request VO + * 清单指引树 - 交换排序 Request VO * * @author yhy */ -@Schema(description = "管理后台 - 清单明细树交换排序 Request VO") +@Schema(description = "管理后台 - 清单指引树交换排序 Request VO") @Data -public class BoqDetailTreeSwapSortReqVO { +public class BoqGuideTreeSwapSortReqVO { @Schema(description = "节点ID1", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "节点ID1不能为空") diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqItemTreeDeleteCheckRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqItemTreeDeleteCheckRespVO.java new file mode 100644 index 0000000..1b495f1 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqItemTreeDeleteCheckRespVO.java @@ -0,0 +1,36 @@ +package com.yhy.module.core.controller.admin.boq.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 清单项树删除检查响应VO + */ +@Schema(description = "管理后台 - 清单项树删除检查 Response VO") +@Data +public class BoqItemTreeDeleteCheckRespVO { + + @Schema(description = "是否有子节点") + private Boolean hasChildren; + + @Schema(description = "子节点数量") + private Integer childrenCount; + + @Schema(description = "是否有清单子目") + private Boolean hasSubItems; + + @Schema(description = "清单子目数量") + private Integer subItemsCount; + + @Schema(description = "是否有清单指引") + private Boolean hasGuides; + + @Schema(description = "清单指引数量") + private Integer guidesCount; + + @Schema(description = "是否可以直接删除(无任何关联数据)") + private Boolean canDirectDelete; + + @Schema(description = "确认删除提示信息") + private String confirmMessage; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqItemTreeRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqItemTreeRespVO.java index 3fef6c4..b27b771 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqItemTreeRespVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqItemTreeRespVO.java @@ -34,6 +34,9 @@ public class BoqItemTreeRespVO { @Schema(description = "单位", example = "m³") private String unit; + @Schema(description = "说明(富文本)", example = "

适用于土石方工程的计量与描述

") + private String description; + @Schema(description = "排序", example = "1") private Integer sortOrder; diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqItemTreeSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqItemTreeSaveReqVO.java index 64f1243..168bbe9 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqItemTreeSaveReqVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqItemTreeSaveReqVO.java @@ -1,11 +1,10 @@ package com.yhy.module.core.controller.admin.boq.vo; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - +import java.util.Map; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; -import java.util.Map; +import lombok.Data; /** * 清单项树保存 Request VO @@ -16,27 +15,40 @@ import java.util.Map; @Data public class BoqItemTreeSaveReqVO { + /** + * 创建时的校验分组 + */ + public interface CreateGroup {} + + /** + * 更新时的校验分组 + */ + public interface UpdateGroup {} + @Schema(description = "节点ID", example = "1") + @NotNull(message = "ID不能为空", groups = UpdateGroup.class) private Long id; @Schema(description = "清单专业ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @NotNull(message = "清单专业ID不能为空") + @NotNull(message = "清单专业ID不能为空", groups = CreateGroup.class) private Long boqCatalogItemId; @Schema(description = "父节点ID", example = "1") private Long parentId; - @Schema(description = "编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "01") - @NotBlank(message = "编码不能为空") + @Schema(description = "编码", example = "01") private String code; @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "土石方工程") - @NotBlank(message = "名称不能为空") + @NotBlank(message = "名称不能为空", groups = CreateGroup.class) private String name; @Schema(description = "单位", example = "m³") private String unit; + @Schema(description = "说明(富文本)", example = "

适用于土石方工程的计量与描述

") + private String description; + @Schema(description = "排序", example = "1") private Integer sortOrder; diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqSubItemRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqSubItemRespVO.java index 81be05c..bf1a20d 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqSubItemRespVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqSubItemRespVO.java @@ -1,10 +1,9 @@ package com.yhy.module.core.controller.admin.boq.vo; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - import java.time.LocalDateTime; import java.util.Map; +import lombok.Data; /** * 清单子项 Response VO @@ -33,6 +32,9 @@ public class BoqSubItemRespVO { @Schema(description = "清单说明(富文本)", example = "

适用于建筑场地的平整工程

") private String description; + @Schema(description = "项目特征", example = "人工挖土;弃土外运") + private String features; + @Schema(description = "排序", example = "1") private Integer sortOrder; @@ -44,4 +46,7 @@ public class BoqSubItemRespVO { @Schema(description = "更新时间") private LocalDateTime updateTime; + + @Schema(description = "关联的定额专业ID(从清单专业获取)", example = "1") + private Long quotaCatalogItemId; } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqSubItemSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqSubItemSaveReqVO.java index a6b60bf..f9f1415 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqSubItemSaveReqVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/BoqSubItemSaveReqVO.java @@ -1,11 +1,10 @@ package com.yhy.module.core.controller.admin.boq.vo; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - +import java.util.Map; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; -import java.util.Map; +import lombok.Data; /** * 清单子项保存 Request VO @@ -16,19 +15,29 @@ import java.util.Map; @Data public class BoqSubItemSaveReqVO { + /** + * 创建时的校验分组 + */ + public interface CreateGroup {} + + /** + * 更新时的校验分组 + */ + public interface UpdateGroup {} + @Schema(description = "子项ID", example = "1") + @NotNull(message = "ID不能为空", groups = UpdateGroup.class) private Long id; @Schema(description = "清单项树ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @NotNull(message = "清单项树ID不能为空") + @NotNull(message = "清单项树ID不能为空", groups = CreateGroup.class) private Long boqItemTreeId; - @Schema(description = "编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "010101001") - @NotBlank(message = "编码不能为空") + @Schema(description = "编码", example = "010101001") private String code; @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "平整场地(人工)") - @NotBlank(message = "名称不能为空") + @NotBlank(message = "名称不能为空", groups = CreateGroup.class) private String name; @Schema(description = "单位", example = "m²") @@ -37,6 +46,9 @@ public class BoqSubItemSaveReqVO { @Schema(description = "清单说明(富文本)", example = "

适用于建筑场地的平整工程

") private String description; + @Schema(description = "项目特征", example = "人工挖土;弃土外运") + private String features; + @Schema(description = "排序", example = "1") private Integer sortOrder; diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/QuotaCodeValidateRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/QuotaCodeValidateRespVO.java new file mode 100644 index 0000000..eafa0e7 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/boq/vo/QuotaCodeValidateRespVO.java @@ -0,0 +1,32 @@ +package com.yhy.module.core.controller.admin.boq.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 定额编码验证结果 Response VO + * + * @author yhy + */ +@Schema(description = "管理后台 - 定额编码验证结果 Response VO") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class QuotaCodeValidateRespVO { + + @Schema(description = "定额基价ID", example = "10") + private Long id; + + @Schema(description = "定额编码", example = "de6") + private String code; + + @Schema(description = "定额名称", example = "土石方工程") + private String name; + + @Schema(description = "单位", example = "m³") + private String unit; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/CalcBaseRateCatalogController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/CalcBaseRateCatalogController.java new file mode 100644 index 0000000..8924b35 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/CalcBaseRateCatalogController.java @@ -0,0 +1,82 @@ +package com.yhy.module.core.controller.admin.calcbaserate; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.calcbaserate.vo.CalcBaseRateCatalogRespVO; +import com.yhy.module.core.controller.admin.calcbaserate.vo.CalcBaseRateCatalogSaveReqVO; +import com.yhy.module.core.service.calcbaserate.CalcBaseRateCatalogService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import javax.annotation.Resource; +import javax.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +/** + * 基数费率目录树 Controller + * + * @author yhy + */ +@Tag(name = "管理后台 - 基数费率目录树") +@RestController +@RequestMapping("/core/calc-base-rate/catalog") +@Validated +public class CalcBaseRateCatalogController { + + @Resource + private CalcBaseRateCatalogService calcBaseRateCatalogService; + + @PostMapping("/create") + @Operation(summary = "创建基数费率目录树节点") + @PreAuthorize("@ss.hasPermission('core:calc-base-rate-catalog:create')") + public CommonResult createCalcBaseRateCatalog( + @Valid @Validated(CalcBaseRateCatalogSaveReqVO.CreateGroup.class) + @RequestBody CalcBaseRateCatalogSaveReqVO createReqVO) { + return success(calcBaseRateCatalogService.createCalcBaseRateCatalog(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新基数费率目录树节点") + @PreAuthorize("@ss.hasPermission('core:calc-base-rate-catalog:update')") + public CommonResult updateCalcBaseRateCatalog( + @Valid @Validated(CalcBaseRateCatalogSaveReqVO.UpdateGroup.class) + @RequestBody CalcBaseRateCatalogSaveReqVO updateReqVO) { + calcBaseRateCatalogService.updateCalcBaseRateCatalog(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除基数费率目录树节点") + @Parameter(name = "id", description = "节点ID", required = true) + @PreAuthorize("@ss.hasPermission('core:calc-base-rate-catalog:delete')") + public CommonResult deleteCalcBaseRateCatalog(@RequestParam("id") Long id) { + calcBaseRateCatalogService.deleteCalcBaseRateCatalog(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获取基数费率目录树节点") + @Parameter(name = "id", description = "节点ID", required = true) + @PreAuthorize("@ss.hasPermission('core:calc-base-rate-catalog:query')") + public CommonResult getCalcBaseRateCatalog(@RequestParam("id") Long id) { + return success(calcBaseRateCatalogService.getCalcBaseRateCatalog(id)); + } + + @GetMapping("/tree") + @Operation(summary = "获取基数费率目录树") + @PreAuthorize("@ss.hasPermission('core:calc-base-rate-catalog:query')") + public CommonResult> getCalcBaseRateCatalogTree() { + return success(calcBaseRateCatalogService.getCalcBaseRateCatalogTree()); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/CalcBaseRateDirectoryController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/CalcBaseRateDirectoryController.java new file mode 100644 index 0000000..57b889c --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/CalcBaseRateDirectoryController.java @@ -0,0 +1,102 @@ +package com.yhy.module.core.controller.admin.calcbaserate; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.calcbaserate.vo.CalcBaseRateDirectoryRespVO; +import com.yhy.module.core.controller.admin.calcbaserate.vo.CalcBaseRateDirectorySaveReqVO; +import com.yhy.module.core.controller.admin.calcbaserate.vo.CalcBaseRateDirectorySwapSortReqVO; +import com.yhy.module.core.service.calcbaserate.CalcBaseRateDirectoryService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import javax.annotation.Resource; +import javax.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +/** + * 基数费率目录 Controller + * + * @author yhy + */ +@Tag(name = "管理后台 - 基数费率目录") +@RestController +@RequestMapping("/core/calc-base-rate/directory") +@Validated +public class CalcBaseRateDirectoryController { + + @Resource + private CalcBaseRateDirectoryService calcBaseRateDirectoryService; + + @PostMapping("/create") + @Operation(summary = "创建基数费率目录节点") + @PreAuthorize("@ss.hasPermission('core:calc-base-rate-directory:create')") + public CommonResult createCalcBaseRateDirectory( + @Valid @Validated(CalcBaseRateDirectorySaveReqVO.CreateGroup.class) + @RequestBody CalcBaseRateDirectorySaveReqVO createReqVO) { + return success(calcBaseRateDirectoryService.createCalcBaseRateDirectory(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新基数费率目录节点") + @PreAuthorize("@ss.hasPermission('core:calc-base-rate-directory:update')") + public CommonResult updateCalcBaseRateDirectory( + @Valid @Validated(CalcBaseRateDirectorySaveReqVO.UpdateGroup.class) + @RequestBody CalcBaseRateDirectorySaveReqVO updateReqVO) { + calcBaseRateDirectoryService.updateCalcBaseRateDirectory(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除基数费率目录节点") + @Parameter(name = "id", description = "节点ID", required = true) + @PreAuthorize("@ss.hasPermission('core:calc-base-rate-directory:delete')") + public CommonResult deleteCalcBaseRateDirectory(@RequestParam("id") Long id) { + calcBaseRateDirectoryService.deleteCalcBaseRateDirectory(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获取基数费率目录节点") + @Parameter(name = "id", description = "节点ID", required = true) + @PreAuthorize("@ss.hasPermission('core:calc-base-rate-directory:query')") + public CommonResult getCalcBaseRateDirectory(@RequestParam("id") Long id) { + return success(calcBaseRateDirectoryService.getCalcBaseRateDirectory(id)); + } + + @GetMapping("/tree") + @Operation(summary = "获取基数费率目录树") + @Parameter(name = "calcBaseRateCatalogId", description = "基数费率目录树节点ID", required = true) + @PreAuthorize("@ss.hasPermission('core:calc-base-rate-directory:query')") + public CommonResult> getCalcBaseRateDirectoryTree( + @RequestParam("calcBaseRateCatalogId") Long calcBaseRateCatalogId) { + return success(calcBaseRateDirectoryService.getCalcBaseRateDirectoryTree(calcBaseRateCatalogId)); + } + + @PostMapping("/swap-sort") + @Operation(summary = "交换排序") + @PreAuthorize("@ss.hasPermission('core:calc-base-rate-directory:update')") + public CommonResult swapSort(@Valid @RequestBody CalcBaseRateDirectorySwapSortReqVO swapReqVO) { + calcBaseRateDirectoryService.swapSort(swapReqVO); + return success(true); + } + + @DeleteMapping("/force-delete") + @Operation(summary = "强制删除目录节点(级联删除所有关联数据)") + @Parameter(name = "id", description = "节点ID", required = true) + @PreAuthorize("@ss.hasPermission('core:calc-base-rate-directory:delete')") + public CommonResult forceDeleteCalcBaseRateDirectory(@RequestParam("id") Long id) { + calcBaseRateDirectoryService.forceDeleteCalcBaseRateDirectory(id); + return success(true); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/CalcBaseRateItemController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/CalcBaseRateItemController.java new file mode 100644 index 0000000..8607418 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/CalcBaseRateItemController.java @@ -0,0 +1,84 @@ +package com.yhy.module.core.controller.admin.calcbaserate; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.calcbaserate.vo.CalcBaseRateItemRespVO; +import com.yhy.module.core.controller.admin.calcbaserate.vo.CalcBaseRateItemSaveReqVO; +import com.yhy.module.core.service.calcbaserate.CalcBaseRateItemService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import javax.annotation.Resource; +import javax.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +/** + * 基数费率项 Controller + * + * @author yhy + */ +@Tag(name = "管理后台 - 基数费率项") +@RestController +@RequestMapping("/core/calc-base-rate/item") +@Validated +public class CalcBaseRateItemController { + + @Resource + private CalcBaseRateItemService calcBaseRateItemService; + + @PostMapping("/create") + @Operation(summary = "创建基数费率项") + @PreAuthorize("@ss.hasPermission('core:calc-base-rate-item:create')") + public CommonResult createCalcBaseRateItem( + @Valid @Validated(CalcBaseRateItemSaveReqVO.CreateGroup.class) + @RequestBody CalcBaseRateItemSaveReqVO createReqVO) { + return success(calcBaseRateItemService.createCalcBaseRateItem(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新基数费率项") + @PreAuthorize("@ss.hasPermission('core:calc-base-rate-item:update')") + public CommonResult updateCalcBaseRateItem( + @Valid @Validated(CalcBaseRateItemSaveReqVO.UpdateGroup.class) + @RequestBody CalcBaseRateItemSaveReqVO updateReqVO) { + calcBaseRateItemService.updateCalcBaseRateItem(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除基数费率项") + @Parameter(name = "id", description = "费率项ID", required = true) + @PreAuthorize("@ss.hasPermission('core:calc-base-rate-item:delete')") + public CommonResult deleteCalcBaseRateItem(@RequestParam("id") Long id) { + calcBaseRateItemService.deleteCalcBaseRateItem(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获取基数费率项") + @Parameter(name = "id", description = "费率项ID", required = true) + @PreAuthorize("@ss.hasPermission('core:calc-base-rate-item:query')") + public CommonResult getCalcBaseRateItem(@RequestParam("id") Long id) { + return success(calcBaseRateItemService.getCalcBaseRateItem(id)); + } + + @GetMapping("/list") + @Operation(summary = "获取费率项列表") + @Parameter(name = "calcBaseRateDirectoryId", description = "目录ID", required = true) + @PreAuthorize("@ss.hasPermission('core:calc-base-rate-item:query')") + public CommonResult> getCalcBaseRateItemList( + @RequestParam("calcBaseRateDirectoryId") Long calcBaseRateDirectoryId) { + return success(calcBaseRateItemService.getCalcBaseRateItemList(calcBaseRateDirectoryId)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/vo/CalcBaseRateCatalogRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/vo/CalcBaseRateCatalogRespVO.java new file mode 100644 index 0000000..8ed2989 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/vo/CalcBaseRateCatalogRespVO.java @@ -0,0 +1,53 @@ +package com.yhy.module.core.controller.admin.calcbaserate.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import lombok.Data; + +/** + * 基数费率目录树 Response VO + * + * @author yhy + */ +@Schema(description = "管理后台 - 基数费率目录树 Response VO") +@Data +public class CalcBaseRateCatalogRespVO { + + @Schema(description = "主键ID", example = "1") + private Long id; + + @Schema(description = "父节点ID", example = "1") + private Long parentId; + + @Schema(description = "编码", example = "CBR_GD") + private String code; + + @Schema(description = "名称", example = "广东") + private String name; + + @Schema(description = "节点类型", example = "province") + private String nodeType; + + @Schema(description = "排序", example = "1") + private Integer sortOrder; + + @Schema(description = "树路径") + private String[] path; + + @Schema(description = "层级", example = "1") + private Integer level; + + @Schema(description = "扩展属性") + private Map attributes; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; + + @Schema(description = "子节点列表") + private List children; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/vo/CalcBaseRateCatalogSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/vo/CalcBaseRateCatalogSaveReqVO.java new file mode 100644 index 0000000..2d99f1c --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/vo/CalcBaseRateCatalogSaveReqVO.java @@ -0,0 +1,52 @@ +package com.yhy.module.core.controller.admin.calcbaserate.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Map; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 基数费率目录树保存 Request VO + * + * @author yhy + */ +@Schema(description = "管理后台 - 基数费率目录树保存 Request VO") +@Data +public class CalcBaseRateCatalogSaveReqVO { + + /** + * 创建时的校验分组 + */ + public interface CreateGroup {} + + /** + * 更新时的校验分组 + */ + public interface UpdateGroup {} + + @Schema(description = "主键ID", example = "1") + @NotNull(message = "ID不能为空", groups = UpdateGroup.class) + private Long id; + + @Schema(description = "父节点ID", example = "1") + private Long parentId; + + @Schema(description = "编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "CBR_GD") + @NotBlank(message = "编码不能为空", groups = CreateGroup.class) + private String code; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "广东") + @NotBlank(message = "名称不能为空", groups = CreateGroup.class) + private String name; + + @Schema(description = "节点类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "province") + @NotBlank(message = "节点类型不能为空", groups = CreateGroup.class) + private String nodeType; + + @Schema(description = "排序", example = "1") + private Integer sortOrder; + + @Schema(description = "扩展属性") + private Map attributes; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/vo/CalcBaseRateDirectoryRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/vo/CalcBaseRateDirectoryRespVO.java new file mode 100644 index 0000000..f2f2e61 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/vo/CalcBaseRateDirectoryRespVO.java @@ -0,0 +1,50 @@ +package com.yhy.module.core.controller.admin.calcbaserate.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import lombok.Data; + +/** + * 基数费率目录 Response VO + * + * @author yhy + */ +@Schema(description = "管理后台 - 基数费率目录 Response VO") +@Data +public class CalcBaseRateDirectoryRespVO { + + @Schema(description = "主键ID", example = "1") + private Long id; + + @Schema(description = "关联基数费率目录树节点ID", example = "1") + private Long calcBaseRateCatalogId; + + @Schema(description = "父节点ID", example = "1") + private Long parentId; + + @Schema(description = "目录名称", example = "目录1") + private String name; + + @Schema(description = "排序", example = "1") + private Integer sortOrder; + + @Schema(description = "树路径") + private String[] path; + + @Schema(description = "层级", example = "1") + private Integer level; + + @Schema(description = "扩展属性") + private Map attributes; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; + + @Schema(description = "子节点列表") + private List children; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/vo/CalcBaseRateDirectorySaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/vo/CalcBaseRateDirectorySaveReqVO.java new file mode 100644 index 0000000..2a5979b --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/vo/CalcBaseRateDirectorySaveReqVO.java @@ -0,0 +1,46 @@ +package com.yhy.module.core.controller.admin.calcbaserate.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Map; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 基数费率目录保存 Request VO + * + * @author yhy + */ +@Schema(description = "管理后台 - 基数费率目录保存 Request VO") +@Data +public class CalcBaseRateDirectorySaveReqVO { + + /** + * 创建时的校验分组 + */ + public interface CreateGroup {} + + /** + * 更新时的校验分组 + */ + public interface UpdateGroup {} + + @Schema(description = "主键ID", example = "1") + @NotNull(message = "ID不能为空", groups = UpdateGroup.class) + private Long id; + + @Schema(description = "关联基数费率目录树节点ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "目录树节点ID不能为空", groups = CreateGroup.class) + private Long calcBaseRateCatalogId; + + @Schema(description = "父节点ID", example = "1") + private Long parentId; + + @Schema(description = "目录名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "目录1") + private String name; + + @Schema(description = "排序", example = "1") + private Integer sortOrder; + + @Schema(description = "扩展属性") + private Map attributes; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/vo/CalcBaseRateDirectorySwapSortReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/vo/CalcBaseRateDirectorySwapSortReqVO.java new file mode 100644 index 0000000..9853986 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/vo/CalcBaseRateDirectorySwapSortReqVO.java @@ -0,0 +1,23 @@ +package com.yhy.module.core.controller.admin.calcbaserate.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 基数费率目录交换排序 Request VO + * + * @author yhy + */ +@Schema(description = "管理后台 - 基数费率目录交换排序 Request VO") +@Data +public class CalcBaseRateDirectorySwapSortReqVO { + + @Schema(description = "节点1 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "节点1 ID不能为空") + private Long nodeId1; + + @Schema(description = "节点2 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "节点2 ID不能为空") + private Long nodeId2; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/vo/CalcBaseRateItemRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/vo/CalcBaseRateItemRespVO.java new file mode 100644 index 0000000..c118778 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/vo/CalcBaseRateItemRespVO.java @@ -0,0 +1,43 @@ +package com.yhy.module.core.controller.admin.calcbaserate.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import java.util.Map; +import lombok.Data; + +/** + * 基数费率项 Response VO + * + * @author yhy + */ +@Schema(description = "管理后台 - 基数费率项 Response VO") +@Data +public class CalcBaseRateItemRespVO { + + @Schema(description = "主键ID", example = "1") + private Long id; + + @Schema(description = "关联基数费率目录ID", example = "1") + private Long calcBaseRateDirectoryId; + + @Schema(description = "名称", example = "人工费") + private String name; + + @Schema(description = "费率", example = "3.5%") + private String rate; + + @Schema(description = "备注", example = "...") + private String remark; + + @Schema(description = "排序", example = "1") + private Integer sortOrder; + + @Schema(description = "扩展属性") + private Map attributes; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/vo/CalcBaseRateItemSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/vo/CalcBaseRateItemSaveReqVO.java new file mode 100644 index 0000000..08e5182 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/calcbaserate/vo/CalcBaseRateItemSaveReqVO.java @@ -0,0 +1,49 @@ +package com.yhy.module.core.controller.admin.calcbaserate.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Map; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 基数费率项保存 Request VO + * + * @author yhy + */ +@Schema(description = "管理后台 - 基数费率项保存 Request VO") +@Data +public class CalcBaseRateItemSaveReqVO { + + /** + * 创建时的校验分组 + */ + public interface CreateGroup {} + + /** + * 更新时的校验分组 + */ + public interface UpdateGroup {} + + @Schema(description = "主键ID", example = "1") + @NotNull(message = "ID不能为空", groups = UpdateGroup.class) + private Long id; + + @Schema(description = "关联基数费率目录ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "目录ID不能为空", groups = CreateGroup.class) + private Long calcBaseRateDirectoryId; + + @Schema(description = "名称", example = "人工费") + private String name; + + @Schema(description = "费率", example = "3.5%") + private String rate; + + @Schema(description = "备注", example = "...") + private String remark; + + @Schema(description = "排序", example = "1") + private Integer sortOrder; + + @Schema(description = "扩展属性") + private Map attributes; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/ConfigProjectInfoController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/ConfigProjectInfoController.java new file mode 100644 index 0000000..5dbfb3a --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/ConfigProjectInfoController.java @@ -0,0 +1,89 @@ +package com.yhy.module.core.controller.admin.config; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.config.vo.ConfigProjectInfoRespVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigProjectInfoSaveReqVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigProjectInfoSwapSortReqVO; +import com.yhy.module.core.service.config.ConfigProjectInfoService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import javax.annotation.Resource; +import javax.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 工程信息配置 Controller + * + * @author yhy + */ +@Tag(name = "管理后台 - 工程信息配置") +@RestController +@RequestMapping("/core/config/project-info") +@Validated +public class ConfigProjectInfoController { + + @Resource + private ConfigProjectInfoService configProjectInfoService; + + @PostMapping("/create") + @Operation(summary = "创建工程信息配置") + @PreAuthorize("@ss.hasPermission('core:config-project-info:create')") + public CommonResult createConfigProjectInfo(@Valid @RequestBody ConfigProjectInfoSaveReqVO createReqVO) { + return success(configProjectInfoService.createConfigProjectInfo(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新工程信息配置") + @PreAuthorize("@ss.hasPermission('core:config-project-info:update')") + public CommonResult updateConfigProjectInfo(@Valid @RequestBody ConfigProjectInfoSaveReqVO updateReqVO) { + configProjectInfoService.updateConfigProjectInfo(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除工程信息配置") + @Parameter(name = "id", description = "ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:config-project-info:delete')") + public CommonResult deleteConfigProjectInfo(@RequestParam("id") Long id) { + configProjectInfoService.deleteConfigProjectInfo(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获取工程信息配置详情") + @Parameter(name = "id", description = "ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:config-project-info:query')") + public CommonResult getConfigProjectInfo(@RequestParam("id") Long id) { + return success(configProjectInfoService.getConfigProjectInfo(id)); + } + + @GetMapping("/tree") + @Operation(summary = "获取工程信息配置树(按行业节点)") + @Parameter(name = "configTreeId", description = "行业节点ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:config-project-info:query')") + public CommonResult> getConfigProjectInfoTree( + @RequestParam("configTreeId") Long configTreeId) { + return success(configProjectInfoService.getConfigProjectInfoTree(configTreeId)); + } + + @PostMapping("/swap-sort") + @Operation(summary = "交换排序") + @PreAuthorize("@ss.hasPermission('core:config-project-info:update')") + public CommonResult swapSort(@Valid @RequestBody ConfigProjectInfoSwapSortReqVO swapReqVO) { + configProjectInfoService.swapSort(swapReqVO); + return success(true); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/ConfigProjectTreeController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/ConfigProjectTreeController.java new file mode 100644 index 0000000..52af484 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/ConfigProjectTreeController.java @@ -0,0 +1,94 @@ +package com.yhy.module.core.controller.admin.config; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.*; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.config.vo.ConfigProjectTreeRespVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigProjectTreeSaveReqVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigProjectTreeSwapSortReqVO; +import com.yhy.module.core.service.config.ConfigProjectTreeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import javax.annotation.Resource; +import javax.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 项目界面配置树 Controller + * + * @author yhy + */ +@Tag(name = "管理后台 - 项目界面配置树") +@RestController +@RequestMapping("/core/config/project-tree") +@Validated +public class ConfigProjectTreeController { + + @Resource + private ConfigProjectTreeService configProjectTreeService; + + @PostMapping("/create") + @Operation(summary = "创建项目界面配置树节点") + @PreAuthorize("@ss.hasPermission('core:config-project-tree:create')") + public CommonResult createConfigProjectTree(@Valid @RequestBody ConfigProjectTreeSaveReqVO createReqVO) { + return success(configProjectTreeService.createConfigProjectTree(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新项目界面配置树节点") + @PreAuthorize("@ss.hasPermission('core:config-project-tree:update')") + public CommonResult updateConfigProjectTree(@Valid @RequestBody ConfigProjectTreeSaveReqVO updateReqVO) { + configProjectTreeService.updateConfigProjectTree(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除项目界面配置树节点") + @Parameter(name = "id", description = "节点ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:config-project-tree:delete')") + public CommonResult deleteConfigProjectTree(@RequestParam("id") Long id) { + configProjectTreeService.deleteConfigProjectTree(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获取项目界面配置树节点") + @Parameter(name = "id", description = "节点ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:config-project-tree:query')") + public CommonResult getConfigProjectTree(@RequestParam("id") Long id) { + return success(configProjectTreeService.getConfigProjectTree(id)); + } + + @GetMapping("/tree") + @Operation(summary = "获取项目界面配置树(树形结构)") + @PreAuthorize("@ss.hasPermission('core:config-project-tree:query')") + public CommonResult> getConfigProjectTreeList() { + return success(configProjectTreeService.getConfigProjectTreeList()); + } + + @PostMapping("/swap-sort") + @Operation(summary = "交换排序") + @PreAuthorize("@ss.hasPermission('core:config-project-tree:update')") + public CommonResult swapSort(@Valid @RequestBody ConfigProjectTreeSwapSortReqVO swapReqVO) { + configProjectTreeService.swapSort(swapReqVO); + return success(true); + } + + @GetMapping("/industry-options") + @Operation(summary = "获取行业下拉选项(省市-行业两级结构)") + @PreAuthorize("@ss.hasPermission('core:config-project-tree:query')") + public CommonResult> getIndustryOptions() { + return success(configProjectTreeService.getIndustryOptions()); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/ConfigUnitDivisionTemplateController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/ConfigUnitDivisionTemplateController.java new file mode 100644 index 0000000..381e072 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/ConfigUnitDivisionTemplateController.java @@ -0,0 +1,80 @@ +package com.yhy.module.core.controller.admin.config; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitDivisionTemplateRespVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitDivisionTemplateSaveReqVO; +import com.yhy.module.core.service.config.ConfigUnitDivisionTemplateService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import javax.annotation.Resource; +import javax.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "管理后台 - 单位工程界面配置 - 分部分项模板") +@RestController +@RequestMapping("/core/config/unit-division-template") +@Validated +public class ConfigUnitDivisionTemplateController { + + @Resource + private ConfigUnitDivisionTemplateService service; + + @PostMapping("/create") + @Operation(summary = "创建分部分项模板节点") + @PreAuthorize("@ss.hasPermission('core:config:unit-division-template:create')") + public CommonResult create(@Valid @RequestBody ConfigUnitDivisionTemplateSaveReqVO reqVO) { + return success(service.create(reqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新分部分项模板节点") + @PreAuthorize("@ss.hasPermission('core:config:unit-division-template:update')") + public CommonResult update(@Valid @RequestBody ConfigUnitDivisionTemplateSaveReqVO reqVO) { + service.update(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除分部分项模板节点(级联删除子节点和引用)") + @Parameter(name = "id", description = "ID", required = true) + @PreAuthorize("@ss.hasPermission('core:config:unit-division-template:delete')") + public CommonResult delete(@RequestParam("id") Long id) { + service.delete(id); + return success(true); + } + + @GetMapping("/tree") + @Operation(summary = "获取模板树(按标签页类型)") + @Parameters({ + @Parameter(name = "catalogItemId", description = "fields_majors 节点ID", required = true), + @Parameter(name = "tabType", description = "标签页类型:division/measure/other/unit_summary", required = true) + }) + @PreAuthorize("@ss.hasPermission('core:config:unit-division-template:query')") + public CommonResult> getTree(@RequestParam("catalogItemId") Long catalogItemId, + @RequestParam("tabType") String tabType) { + return success(service.getTree(catalogItemId, tabType)); + } + + @PostMapping("/swap-sort") + @Operation(summary = "交换排序") + @PreAuthorize("@ss.hasPermission('core:config:unit-division-template:update')") + public CommonResult swapSort(@RequestParam("nodeId1") Long nodeId1, + @RequestParam("nodeId2") Long nodeId2) { + service.swapSort(nodeId1, nodeId2); + return success(true); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/ConfigUnitFieldController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/ConfigUnitFieldController.java new file mode 100644 index 0000000..38c22c2 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/ConfigUnitFieldController.java @@ -0,0 +1,91 @@ +package com.yhy.module.core.controller.admin.config; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitFieldHiddenFieldsRespVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitFieldRespVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitFieldSaveReqVO; +import com.yhy.module.core.service.config.ConfigUnitFieldService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import javax.annotation.Resource; +import javax.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "管理后台 - 单位工程界面配置 - 工作台字段设置") +@RestController +@RequestMapping("/core/config/unit-field") +@Validated +public class ConfigUnitFieldController { + + @Resource + private ConfigUnitFieldService configUnitFieldService; + + @PostMapping("/create") + @Operation(summary = "创建工作台字段设置") + @PreAuthorize("@ss.hasPermission('core:config:unit-field:create')") + public CommonResult create(@Valid @RequestBody ConfigUnitFieldSaveReqVO createReqVO) { + return success(configUnitFieldService.create(createReqVO)); + } + + @PostMapping("/batch-create") + @Operation(summary = "批量创建工作台字段设置") + @PreAuthorize("@ss.hasPermission('core:config:unit-field:create')") + public CommonResult batchCreate(@Valid @RequestBody List createReqVOs) { + configUnitFieldService.batchCreate(createReqVOs); + return success(true); + } + + @PutMapping("/update") + @Operation(summary = "更新工作台字段设置") + @PreAuthorize("@ss.hasPermission('core:config:unit-field:update')") + public CommonResult update(@Valid @RequestBody ConfigUnitFieldSaveReqVO updateReqVO) { + configUnitFieldService.update(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除工作台字段设置") + @Parameter(name = "id", description = "ID", required = true) + @PreAuthorize("@ss.hasPermission('core:config:unit-field:delete')") + public CommonResult delete(@RequestParam("id") Long id) { + configUnitFieldService.delete(id); + return success(true); + } + + @GetMapping("/list") + @Operation(summary = "获取工作台字段设置列表") + @Parameter(name = "catalogItemId", description = "fields_majors 节点ID", required = true) + @PreAuthorize("@ss.hasPermission('core:config:unit-field:query')") + public CommonResult> getList(@RequestParam("catalogItemId") Long catalogItemId) { + return success(configUnitFieldService.getList(catalogItemId)); + } + + @PostMapping("/swap-sort") + @Operation(summary = "交换排序") + @PreAuthorize("@ss.hasPermission('core:config:unit-field:update')") + public CommonResult swapSort(@RequestParam("nodeId1") Long nodeId1, + @RequestParam("nodeId2") Long nodeId2) { + configUnitFieldService.swapSort(nodeId1, nodeId2); + return success(true); + } + + @GetMapping("/hidden-fields") + @Operation(summary = "获取隐藏字段列表(供工作台调用)") + @Parameter(name = "catalogItemId", description = "fields_majors 节点ID", required = true) + public CommonResult getHiddenFields(@RequestParam("catalogItemId") Long catalogItemId) { + return success(configUnitFieldService.getHiddenFields(catalogItemId)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/ConfigUnitResourceFieldController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/ConfigUnitResourceFieldController.java new file mode 100644 index 0000000..2430fe6 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/ConfigUnitResourceFieldController.java @@ -0,0 +1,90 @@ +package com.yhy.module.core.controller.admin.config; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitResourceFieldRespVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitResourceFieldSaveReqVO; +import com.yhy.module.core.service.config.ConfigUnitResourceFieldService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import javax.annotation.Resource; +import javax.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "管理后台 - 单位工程界面配置 - 工料机字段") +@RestController +@RequestMapping("/core/config/unit-resource-field") +@Validated +public class ConfigUnitResourceFieldController { + + @Resource + private ConfigUnitResourceFieldService service; + + @PostMapping("/create") + @Operation(summary = "创建工料机字段设置") + @PreAuthorize("@ss.hasPermission('core:config:unit-resource-field:create')") + public CommonResult create(@Valid @RequestBody ConfigUnitResourceFieldSaveReqVO reqVO) { + return success(service.create(reqVO)); + } + + @PostMapping("/batch-create") + @Operation(summary = "批量创建工料机字段设置") + @PreAuthorize("@ss.hasPermission('core:config:unit-resource-field:create')") + public CommonResult batchCreate(@Valid @RequestBody List createReqVOs) { + service.batchCreate(createReqVOs); + return success(true); + } + + @PutMapping("/update") + @Operation(summary = "更新工料机字段设置") + @PreAuthorize("@ss.hasPermission('core:config:unit-resource-field:update')") + public CommonResult update(@Valid @RequestBody ConfigUnitResourceFieldSaveReqVO reqVO) { + service.update(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除工料机字段设置") + @Parameter(name = "id", description = "ID", required = true) + @PreAuthorize("@ss.hasPermission('core:config:unit-resource-field:delete')") + public CommonResult delete(@RequestParam("id") Long id) { + service.delete(id); + return success(true); + } + + @GetMapping("/list") + @Operation(summary = "获取工料机字段设置列表") + @Parameter(name = "catalogItemId", description = "fields_majors 节点ID", required = true) + @PreAuthorize("@ss.hasPermission('core:config:unit-resource-field:query')") + public CommonResult> getList(@RequestParam("catalogItemId") Long catalogItemId) { + return success(service.getList(catalogItemId)); + } + + @PostMapping("/swap-sort") + @Operation(summary = "交换排序") + @PreAuthorize("@ss.hasPermission('core:config:unit-resource-field:update')") + public CommonResult swapSort(@RequestParam("nodeId1") Long nodeId1, + @RequestParam("nodeId2") Long nodeId2) { + service.swapSort(nodeId1, nodeId2); + return success(true); + } + + @GetMapping("/hidden-fields") + @Operation(summary = "获取隐藏字段列表(供工作台调用)") + @Parameter(name = "catalogItemId", description = "fields_majors 节点ID", required = true) + public CommonResult> getHiddenFields(@RequestParam("catalogItemId") Long catalogItemId) { + return success(service.getHiddenFields(catalogItemId)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/ConfigUnitTabRefController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/ConfigUnitTabRefController.java new file mode 100644 index 0000000..b22a202 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/ConfigUnitTabRefController.java @@ -0,0 +1,72 @@ +package com.yhy.module.core.controller.admin.config; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitTabRefRespVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitTabRefSaveReqVO; +import com.yhy.module.core.service.config.ConfigUnitTabRefService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import javax.annotation.Resource; +import javax.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "管理后台 - 单位工程界面配置 - 标签页引用") +@RestController +@RequestMapping("/core/config/unit-tab-ref") +@Validated +public class ConfigUnitTabRefController { + + @Resource + private ConfigUnitTabRefService service; + + @PostMapping("/save-refs") + @Operation(summary = "批量保存引用(覆盖式)") + @PreAuthorize("@ss.hasPermission('core:config:unit-tab-ref:create')") + public CommonResult saveRefs(@Valid @RequestBody ConfigUnitTabRefSaveReqVO reqVO) { + service.saveRefs(reqVO); + return success(true); + } + + @GetMapping("/list") + @Operation(summary = "获取引用列表(含模板节点详情)") + @Parameters({ + @Parameter(name = "catalogItemId", description = "fields_majors 节点ID", required = true), + @Parameter(name = "tabType", description = "标签页类型:measure/other/unit_summary", required = true) + }) + @PreAuthorize("@ss.hasPermission('core:config:unit-tab-ref:query')") + public CommonResult> getList(@RequestParam("catalogItemId") Long catalogItemId, + @RequestParam("tabType") String tabType) { + return success(service.getList(catalogItemId, tabType)); + } + + @DeleteMapping("/delete") + @Operation(summary = "取消单个引用") + @Parameter(name = "id", description = "引用ID", required = true) + @PreAuthorize("@ss.hasPermission('core:config:unit-tab-ref:delete')") + public CommonResult delete(@RequestParam("id") Long id) { + service.delete(id); + return success(true); + } + + @PostMapping("/swap-sort") + @Operation(summary = "交换排序") + @PreAuthorize("@ss.hasPermission('core:config:unit-tab-ref:update')") + public CommonResult swapSort(@RequestParam("nodeId1") Long nodeId1, + @RequestParam("nodeId2") Long nodeId2) { + service.swapSort(nodeId1, nodeId2); + return success(true); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigProjectInfoRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigProjectInfoRespVO.java new file mode 100644 index 0000000..87ab966 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigProjectInfoRespVO.java @@ -0,0 +1,45 @@ +package com.yhy.module.core.controller.admin.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import lombok.Data; + +/** + * 工程信息配置 Response VO + */ +@Schema(description = "管理后台 - 工程信息配置 Response VO") +@Data +public class ConfigProjectInfoRespVO { + + @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "关联的行业节点ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long configTreeId; + + @Schema(description = "父节点ID", example = "0") + private Long parentId; + + @Schema(description = "代号", example = "TAX_RATE") + private String code; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "税率设置") + private String name; + + @Schema(description = "内容配置") + private Map content; + + @Schema(description = "排序号", example = "1") + private Integer sortOrder; + + @Schema(description = "层级路径") + private String[] path; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "子节点列表") + private List children; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigProjectInfoSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigProjectInfoSaveReqVO.java new file mode 100644 index 0000000..b755927 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigProjectInfoSaveReqVO.java @@ -0,0 +1,38 @@ +package com.yhy.module.core.controller.admin.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Map; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import lombok.Data; + +/** + * 工程信息配置 创建/更新 Request VO + */ +@Schema(description = "管理后台 - 工程信息配置创建/更新 Request VO") +@Data +public class ConfigProjectInfoSaveReqVO { + + @Schema(description = "ID(更新时必填)", example = "1") + private Long id; + + @Schema(description = "关联的行业节点ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "关联的行业节点ID不能为空") + private Long configTreeId; + + @Schema(description = "父节点ID", example = "0") + private Long parentId; + + @Schema(description = "代号", example = "TAX_RATE") + @Size(max = 50, message = "代号长度不能超过50个字符") + private String code; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "税率设置") + @NotBlank(message = "名称不能为空") + @Size(max = 200, message = "名称长度不能超过200个字符") + private String name; + + @Schema(description = "内容配置(JSONB,存储复杂控制逻辑)") + private Map content; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigProjectInfoSwapSortReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigProjectInfoSwapSortReqVO.java new file mode 100644 index 0000000..962422c --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigProjectInfoSwapSortReqVO.java @@ -0,0 +1,21 @@ +package com.yhy.module.core.controller.admin.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 工程信息配置 交换排序 Request VO + */ +@Schema(description = "管理后台 - 工程信息配置交换排序 Request VO") +@Data +public class ConfigProjectInfoSwapSortReqVO { + + @Schema(description = "节点1 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "节点1 ID不能为空") + private Long nodeId1; + + @Schema(description = "节点2 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "节点2 ID不能为空") + private Long nodeId2; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigProjectTreeRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigProjectTreeRespVO.java new file mode 100644 index 0000000..854f9f1 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigProjectTreeRespVO.java @@ -0,0 +1,45 @@ +package com.yhy.module.core.controller.admin.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import lombok.Data; + +/** + * 项目界面配置树 Response VO + */ +@Schema(description = "管理后台 - 项目界面配置树 Response VO") +@Data +public class ConfigProjectTreeRespVO { + + @Schema(description = "节点ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "父节点ID", example = "0") + private Long parentId; + + @Schema(description = "编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "GD") + private String code; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "广东") + private String name; + + @Schema(description = "节点类型:root-根节点, province-省市, industry-行业", requiredMode = Schema.RequiredMode.REQUIRED, example = "province") + private String nodeType; + + @Schema(description = "排序号", example = "1") + private Integer sortOrder; + + @Schema(description = "层级路径") + private String[] path; + + @Schema(description = "扩展属性") + private Map attributes; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "子节点列表") + private List children; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigProjectTreeSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigProjectTreeSaveReqVO.java new file mode 100644 index 0000000..0f4792b --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigProjectTreeSaveReqVO.java @@ -0,0 +1,42 @@ +package com.yhy.module.core.controller.admin.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Map; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import lombok.Data; + +/** + * 项目界面配置树 创建/更新 Request VO + */ +@Schema(description = "管理后台 - 项目界面配置树创建/更新 Request VO") +@Data +public class ConfigProjectTreeSaveReqVO { + + @Schema(description = "节点ID(更新时必填)", example = "1") + private Long id; + + @Schema(description = "父节点ID", example = "0") + private Long parentId; + + @Schema(description = "编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "GD") + @NotBlank(message = "编码不能为空") + @Size(max = 50, message = "编码长度不能超过50个字符") + private String code; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "广东") + @NotBlank(message = "名称不能为空") + @Size(max = 200, message = "名称长度不能超过200个字符") + private String name; + + @Schema(description = "节点类型:root-根节点, province-省市, industry-行业", requiredMode = Schema.RequiredMode.REQUIRED, example = "province") + @NotBlank(message = "节点类型不能为空") + private String nodeType; + + @Schema(description = "扩展属性") + private Map attributes; + + // 验证分组 + public interface CreateGroup {} + public interface UpdateGroup {} +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigProjectTreeSwapSortReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigProjectTreeSwapSortReqVO.java new file mode 100644 index 0000000..6ad0d40 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigProjectTreeSwapSortReqVO.java @@ -0,0 +1,21 @@ +package com.yhy.module.core.controller.admin.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 项目界面配置树 交换排序 Request VO + */ +@Schema(description = "管理后台 - 项目界面配置树交换排序 Request VO") +@Data +public class ConfigProjectTreeSwapSortReqVO { + + @Schema(description = "节点1 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "节点1 ID不能为空") + private Long nodeId1; + + @Schema(description = "节点2 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "节点2 ID不能为空") + private Long nodeId2; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitDivisionTemplateRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitDivisionTemplateRespVO.java new file mode 100644 index 0000000..d193c06 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitDivisionTemplateRespVO.java @@ -0,0 +1,56 @@ +package com.yhy.module.core.controller.admin.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import java.util.List; +import lombok.Data; + +@Schema(description = "管理后台 - 分部分项模板 Response VO") +@Data +public class ConfigUnitDivisionTemplateRespVO { + + @Schema(description = "ID") + private Long id; + + @Schema(description = "关联 fields_majors 节点ID") + private Long catalogItemId; + + @Schema(description = "父节点ID") + private Long parentId; + + @Schema(description = "节点类型:division/boq") + private String nodeType; + + @Schema(description = "编码") + private String code; + + @Schema(description = "名称") + private String name; + + @Schema(description = "单位") + private String unit; + + @Schema(description = "排序") + private Integer sortOrder; + + @Schema(description = "标签页类型") + private String tabType; + + @Schema(description = "扩展属性") + private java.util.Map attributes; + + @Schema(description = "费率") + private java.math.BigDecimal rate; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "费用代号") + private String costCode; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "子节点列表") + private List children; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitDivisionTemplateSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitDivisionTemplateSaveReqVO.java new file mode 100644 index 0000000..2fd1079 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitDivisionTemplateSaveReqVO.java @@ -0,0 +1,61 @@ +package com.yhy.module.core.controller.admin.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - 分部分项模板 创建/更新 Request VO") +@Data +public class ConfigUnitDivisionTemplateSaveReqVO { + + @Schema(description = "ID(更新时必填)") + private Long id; + + @Schema(description = "关联 fields_majors 节点ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "catalogItemId 不能为空") + private Long catalogItemId; + + @Schema(description = "父节点ID") + private Long parentId; + + @Schema(description = "节点类型:division/boq", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "节点类型不能为空") + private String nodeType; + + @Schema(description = "编码") + private String code; + + @Schema(description = "名称") + private String name; + + @Schema(description = "单位") + private String unit; + + @Schema(description = "扩展属性(含基数calcBase、基数范围baseNumberRange等)") + private java.util.Map attributes; + + @Schema(description = "费率") + private java.math.BigDecimal rate; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "费用代号(只允许英文字母)") + @javax.validation.constraints.Pattern(regexp = "^[a-zA-Z]*$", message = "费用代号只允许英文字母") + private String costCode; + + @Schema(description = "标签页类型:division/measure/other/unit_summary", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "标签页类型不能为空") + private String tabType; + + @Schema(description = "插入位置:above/below/end") + private String insertPosition; + + @Schema(description = "参考节点ID") + private Long referenceNodeId; + + public static final String INSERT_POSITION_ABOVE = "above"; + public static final String INSERT_POSITION_BELOW = "below"; + public static final String INSERT_POSITION_END = "end"; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitFieldHiddenFieldsRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitFieldHiddenFieldsRespVO.java new file mode 100644 index 0000000..d91588b --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitFieldHiddenFieldsRespVO.java @@ -0,0 +1,24 @@ +package com.yhy.module.core.controller.admin.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.Builder; +import lombok.Data; + +@Schema(description = "管理后台 - 工作台字段隐藏列表 Response VO(供工作台调用)") +@Data +@Builder +public class ConfigUnitFieldHiddenFieldsRespVO { + + @Schema(description = "分部分项隐藏的字段编码列表") + private List divisionHiddenFields; + + @Schema(description = "措施项目隐藏的字段编码列表") + private List measureHiddenFields; + + @Schema(description = "其他项目隐藏的字段编码列表") + private List otherHiddenFields; + + @Schema(description = "汇总分析隐藏的字段编码列表") + private List summaryHiddenFields; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitFieldRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitFieldRespVO.java new file mode 100644 index 0000000..a6df183 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitFieldRespVO.java @@ -0,0 +1,46 @@ +package com.yhy.module.core.controller.admin.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import lombok.Data; + +@Schema(description = "管理后台 - 工作台字段设置 Response VO") +@Data +public class ConfigUnitFieldRespVO { + + @Schema(description = "ID") + private Long id; + + @Schema(description = "关联 fields_majors 节点ID") + private Long catalogItemId; + + @Schema(description = "序号") + private Integer seqNo; + + @Schema(description = "字段名称") + private String fieldName; + + @Schema(description = "字段编码") + private String fieldCode; + + @Schema(description = "分部分项隐藏") + private Boolean divisionHidden; + + @Schema(description = "措施项目隐藏") + private Boolean measureHidden; + + @Schema(description = "其他项目隐藏") + private Boolean otherHidden; + + @Schema(description = "汇总分析隐藏") + private Boolean summaryHidden; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "排序") + private Integer sortOrder; + + @Schema(description = "创建时间") + private LocalDateTime createTime; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitFieldSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitFieldSaveReqVO.java new file mode 100644 index 0000000..7e75b26 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitFieldSaveReqVO.java @@ -0,0 +1,55 @@ +package com.yhy.module.core.controller.admin.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - 工作台字段设置 创建/更新 Request VO") +@Data +public class ConfigUnitFieldSaveReqVO { + + @Schema(description = "ID(更新时必填)") + private Long id; + + @Schema(description = "关联 fields_majors 节点ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "catalogItemId 不能为空") + private Long catalogItemId; + + @Schema(description = "序号(用户录入)") + private Integer seqNo; + + @Schema(description = "字段名称", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "字段名称不能为空") + private String fieldName; + + @Schema(description = "字段编码(英文)", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "字段编码不能为空") + private String fieldCode; + + @Schema(description = "分部分项隐藏") + private Boolean divisionHidden; + + @Schema(description = "措施项目隐藏") + private Boolean measureHidden; + + @Schema(description = "其他项目隐藏") + private Boolean otherHidden; + + @Schema(description = "汇总分析隐藏") + private Boolean summaryHidden; + + @Schema(description = "备注") + private String remark; + + // 排序相关 + @Schema(description = "插入位置:above/below/end") + private String insertPosition; + + @Schema(description = "参考节点ID") + private Long referenceNodeId; + + public static final String INSERT_POSITION_ABOVE = "above"; + public static final String INSERT_POSITION_BELOW = "below"; + public static final String INSERT_POSITION_END = "end"; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitResourceFieldRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitResourceFieldRespVO.java new file mode 100644 index 0000000..b436cfd --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitResourceFieldRespVO.java @@ -0,0 +1,37 @@ +package com.yhy.module.core.controller.admin.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import lombok.Data; + +@Schema(description = "管理后台 - 工料机字段设置 Response VO") +@Data +public class ConfigUnitResourceFieldRespVO { + + @Schema(description = "ID") + private Long id; + + @Schema(description = "关联 fields_majors 节点ID") + private Long catalogItemId; + + @Schema(description = "序号") + private Integer seqNo; + + @Schema(description = "字段名称") + private String fieldName; + + @Schema(description = "字段编码") + private String fieldCode; + + @Schema(description = "是否显示") + private Boolean visible; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "排序") + private Integer sortOrder; + + @Schema(description = "创建时间") + private LocalDateTime createTime; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitResourceFieldSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitResourceFieldSaveReqVO.java new file mode 100644 index 0000000..324aa2d --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitResourceFieldSaveReqVO.java @@ -0,0 +1,41 @@ +package com.yhy.module.core.controller.admin.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - 工料机字段设置 创建/更新 Request VO") +@Data +public class ConfigUnitResourceFieldSaveReqVO { + + @Schema(description = "ID(更新时必填)") + private Long id; + + @Schema(description = "关联 fields_majors 节点ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "catalogItemId 不能为空") + private Long catalogItemId; + + @Schema(description = "序号") + private Integer seqNo; + + @Schema(description = "字段名称", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "字段名称不能为空") + private String fieldName; + + @Schema(description = "字段编码", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "字段编码不能为空") + private String fieldCode; + + @Schema(description = "是否显示") + private Boolean visible; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "插入位置:above/below/end") + private String insertPosition; + + @Schema(description = "参考节点ID") + private Long referenceNodeId; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitTabRefRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitTabRefRespVO.java new file mode 100644 index 0000000..28c37c6 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitTabRefRespVO.java @@ -0,0 +1,45 @@ +package com.yhy.module.core.controller.admin.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import java.util.List; +import lombok.Data; + +@Schema(description = "管理后台 - 标签页引用 Response VO") +@Data +public class ConfigUnitTabRefRespVO { + + @Schema(description = "引用ID") + private Long id; + + @Schema(description = "关联 fields_majors 节点ID") + private Long catalogItemId; + + @Schema(description = "标签页类型") + private String tabType; + + @Schema(description = "引用的模板节点ID") + private Long templateNodeId; + + @Schema(description = "排序") + private Integer sortOrder; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + // 关联的模板节点信息 + @Schema(description = "模板节点类型") + private String nodeType; + + @Schema(description = "模板节点编码") + private String code; + + @Schema(description = "模板节点名称") + private String name; + + @Schema(description = "模板节点单位") + private String unit; + + @Schema(description = "子节点列表(引用分部时包含其下的清单)") + private List children; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitTabRefSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitTabRefSaveReqVO.java new file mode 100644 index 0000000..4351c85 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/config/vo/ConfigUnitTabRefSaveReqVO.java @@ -0,0 +1,24 @@ +package com.yhy.module.core.controller.admin.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - 标签页引用 批量保存 Request VO") +@Data +public class ConfigUnitTabRefSaveReqVO { + + @Schema(description = "关联 fields_majors 节点ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "catalogItemId 不能为空") + private Long catalogItemId; + + @Schema(description = "标签页类型:measure/other/unit_summary", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "标签页类型不能为空") + private String tabType; + + @Schema(description = "引用的模板节点ID列表", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "模板节点ID列表不能为空") + private List templateNodeIds; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/InfoPriceBookController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/InfoPriceBookController.java index 52d7705..8a0d36b 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/InfoPriceBookController.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/InfoPriceBookController.java @@ -2,9 +2,11 @@ package com.yhy.module.core.controller.admin.infoprice; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceBookPageReqVO; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceBookRespVO; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceBookSaveReqVO; +import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceResourceRespVO; import com.yhy.module.core.service.infoprice.InfoPriceBookService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -65,4 +67,44 @@ public class InfoPriceBookController { public CommonResult> getInfoPriceBookPage(@Valid InfoPriceBookPageReqVO pageReqVO) { return success(infoPriceBookService.getInfoPriceBookPage(pageReqVO)); } + + @GetMapping("/copyPage") + @Operation(summary = "分页查询信息价册-复制按钮(排除租户1)") + @PreAuthorize("@ss.hasPermission('core:info-price:query')") + public CommonResult> getInfoPriceBookCopyPage(@Valid InfoPriceBookPageReqVO pageReqVO) { + // 忽略租户过滤,查询所有租户数据,然后在 Service 层排除租户1 + PageResult result = TenantUtils.executeIgnore(() -> + infoPriceBookService.getInfoPriceBookPageExcludeTenant(pageReqVO, 1L) + ); + return success(result); + } + @PostMapping("/create/copy") + @Operation(summary = "创建信息价册-复制") + @PreAuthorize("@ss.hasPermission('core:info-price:create')") + public CommonResult createInfoPriceBookCopy(@RequestParam("id") Long id) { + return success(infoPriceBookService.copyInfoPriceBook(id)); + } + + @GetMapping("/list") + @Operation(summary = "根据树节点ID查询全部信息价册") + @Parameter(name = "treeNodeId", description = "树节点ID", required = true) + @Parameter(name = "excludeBookId", description = "排除的信息价册ID", required = false) + @PreAuthorize("@ss.hasPermission('core:info-price:query')") + public CommonResult> getInfoPriceBookAll(InfoPriceBookPageReqVO pageReqVO) { + java.util.List result = TenantUtils.execute(1L,() -> + infoPriceBookService.getInfoPriceBookAll(pageReqVO) + ); + return success(result); + } + + @GetMapping("/wb-list") + @Operation(summary = "根据树节点ID查询信息价册列表(工作台专用,不过滤发布状态)") + @Parameter(name = "treeNodeId", description = "树节点ID", required = true) + @PreAuthorize("@ss.hasPermission('core:info-price:query')") + public CommonResult> getInfoPriceBookListForWorkbench(@RequestParam("treeNodeId") Long treeNodeId) { + java.util.List result = TenantUtils.execute(1L, () -> + infoPriceBookService.getInfoPriceBookListForWorkbench(treeNodeId) + ); + return success(result); + } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/InfoPriceResourceController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/InfoPriceResourceController.java index 2a293a2..80f19f3 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/InfoPriceResourceController.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/InfoPriceResourceController.java @@ -2,10 +2,13 @@ package com.yhy.module.core.controller.admin.infoprice; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceResourcePageReqVO; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceResourceRespVO; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceResourceSaveReqVO; +import com.yhy.module.core.dal.dataobject.resource.ResourceCatalogItemDO; import com.yhy.module.core.service.infoprice.InfoPriceResourceService; +import com.yhy.module.core.service.infoprice.InfoPriceResourcePriceService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -28,6 +31,9 @@ public class InfoPriceResourceController { @Resource private InfoPriceResourceService resourceService; + @Resource + private InfoPriceResourcePriceService resourcePriceService; + @PostMapping("/create") @Operation(summary = "创建信息价工料机信息") @PreAuthorize("@ss.hasPermission('core:info-price:create')") @@ -78,4 +84,26 @@ public class InfoPriceResourceController { return success(list); } + @PostMapping("/create-batch") + @Operation(summary = "批量创建信息价工料机信息") + @PreAuthorize("@ss.hasPermission('core:info-price:create')") + public CommonResult> createResourceBatch(@Valid @RequestBody List createReqVOList) { + List resourceBatch = resourceService.createResourceBatch(createReqVOList); + if(resourceBatch.size() > 0){ + // 根据sourceResourceItemId查询对应InfoPriceResourcePriceService信息价工料机价格历史,并创建复制内容 + resourcePriceService.copyResourcePriceHistories(createReqVOList, resourceBatch); + } + return success(resourceBatch); + } + + @GetMapping("/history") + @Operation(summary = "调用历史信息-获得信息价工料机信息分页") + @PreAuthorize("@ss.hasPermission('core:info-price:query')") + public CommonResult> getResourcePageHistory(@Valid InfoPriceResourcePageReqVO pageReqVO) { + pageReqVO.setHistory(true); + PageResult pageResult = TenantUtils.executeIgnore(() -> + resourceService.getResourcePage(pageReqVO) + ); + return success(pageResult); + } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/InfoPriceResourcePriceController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/InfoPriceResourcePriceController.java index db39c03..f46f1cd 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/InfoPriceResourcePriceController.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/InfoPriceResourcePriceController.java @@ -2,9 +2,11 @@ package com.yhy.module.core.controller.admin.infoprice; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceResourcePricePageReqVO; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceResourcePriceRespVO; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceResourcePriceSaveReqVO; +import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceResourceRespVO; import com.yhy.module.core.service.infoprice.InfoPriceResourcePriceService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -78,4 +80,13 @@ public class InfoPriceResourcePriceController { return success(list); } + @GetMapping("/history") + @Operation(summary = "调用历史信息-获得信息价工料机价格历史分页") + @PreAuthorize("@ss.hasPermission('core:info-price:query')") + public CommonResult> getResourcePricePageHistory(@Valid InfoPriceResourcePricePageReqVO pageReqVO) { + PageResult pageResult = TenantUtils.executeIgnore(() -> + resourcePriceService.getResourcePricePage(pageReqVO) + ); + return success(pageResult); + } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/InfoPriceTreeController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/InfoPriceTreeController.java index f4ac916..785297b 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/InfoPriceTreeController.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/InfoPriceTreeController.java @@ -1,6 +1,9 @@ package com.yhy.module.core.controller.admin.infoprice; import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceBookRespVO; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceTreeRespVO; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceTreeSaveReqVO; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceTreeSwapSortReqVO; @@ -64,7 +67,11 @@ public class InfoPriceTreeController { @Parameter(name = "enumType", description = "枚举类型", required = true, example = "region_a") @PreAuthorize("@ss.hasPermission('core:info-price:query')") public CommonResult> getInfoPriceTreeList(@RequestParam("enumType") String enumType) { - return success(infoPriceTreeService.getInfoPriceTreeList(enumType)); + // 忽略租户过滤,查询所有租户数据,然后在 Service 层排除租户1 + List result = TenantUtils.executeIgnore(() -> + infoPriceTreeService.getInfoPriceTreeList(enumType) + ); + return success(result); } @PostMapping("/swap-sort") diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceBookPageReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceBookPageReqVO.java index 3aa419a..4842c89 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceBookPageReqVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceBookPageReqVO.java @@ -1,6 +1,7 @@ package com.yhy.module.core.controller.admin.infoprice.vo; import cn.iocoder.yudao.framework.common.pojo.PageParam; +import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; @@ -28,17 +29,28 @@ public class InfoPriceBookPageReqVO extends PageParam { @Schema(description = "开始时间-开始", example = "2024-01-01") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY) private LocalDate startTimeBegin; @Schema(description = "开始时间-结束", example = "2024-01-31") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY) private LocalDate startTimeEnd; @Schema(description = "结束时间-开始", example = "2024-01-01") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY) private LocalDate endTimeBegin; @Schema(description = "结束时间-结束", example = "2024-01-31") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY) private LocalDate endTimeEnd; + + @Schema(description = "是否有附件", example = "true") + private Boolean attachment; + + @Schema(description = "除非树节点ID", example = "1") + private Long excludeBookId; + } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceBookRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceBookRespVO.java index 76954bb..78981fa 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceBookRespVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceBookRespVO.java @@ -1,11 +1,15 @@ package com.yhy.module.core.controller.admin.infoprice.vo; +import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.List; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 信息价册 Response VO") @Data public class InfoPriceBookRespVO { @@ -26,17 +30,22 @@ public class InfoPriceBookRespVO { private String name; @Schema(description = "开始时间", example = "2024-01-01") + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY) private LocalDate startTime; @Schema(description = "结束时间", example = "2024-01-31") + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY) private LocalDate endTime; @Schema(description = "发布状态", example = "published") private String publishStatus; @Schema(description = "发布时间", example = "2024-01-01 10:00:00") + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime publishTime; - + @Schema(description = "完成时间", example = "2024-01-01 10:00:00") + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime completedTime; @Schema(description = "附件", example = "http://example.com/file.pdf") private String attachment; @@ -54,4 +63,9 @@ public class InfoPriceBookRespVO { @Schema(description = "更新时间") private LocalDateTime updateTime; + @Schema(description = "分类树子节点列表") + private List children; + + @Schema(description = "工料机总数(虚拟字段)", example = "100") + private Long resourceCount; } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceBookSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceBookSaveReqVO.java index 73cdb99..f2744c5 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceBookSaveReqVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceBookSaveReqVO.java @@ -1,5 +1,6 @@ package com.yhy.module.core.controller.admin.infoprice.vo; +import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.springframework.format.annotation.DateTimeFormat; @@ -32,11 +33,13 @@ public class InfoPriceBookSaveReqVO { @Schema(description = "开始时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-01-01") @NotNull(message = "开始时间不能为空") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY) private LocalDate startTime; @Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-01-31") @NotNull(message = "结束时间不能为空") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY) private LocalDate endTime; @Schema(description = "发布状态", example = "published") diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceResourcePageReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceResourcePageReqVO.java index e3242fb..23a1353 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceResourcePageReqVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceResourcePageReqVO.java @@ -6,16 +6,13 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; -import javax.validation.constraints.NotNull; - @Schema(description = "管理后台 - 信息价工料机信息分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class InfoPriceResourcePageReqVO extends PageParam { - @Schema(description = "分类树节点ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @NotNull(message = "分类树节点ID不能为空") + @Schema(description = "分类树节点ID", example = "1") private Long categoryTreeId; @Schema(description = "编码(模糊查询)", example = "RES001") @@ -24,4 +21,22 @@ public class InfoPriceResourcePageReqVO extends PageParam { @Schema(description = "名称(模糊查询)", example = "水泥") private String name; + @Schema(description = "工料机ID(精确匹配)", example = "100") + private Long sourceResourceItemId; + + @Schema(description = "规格型号(模糊查询)", example = "P.O 42.5") + private String spec; + + @Schema(description = "信息价专业类型", example = "profession_1") + private String professionType; + + @Schema(description = "信息价树节点ID(地区)", example = "1") + private String treeNodeId; + + @Schema(description = "排除已关联的资源ID列表(用于未关联表格)") + private java.util.List excludeIds; + + @Schema(description = "是否查询历史") + private Boolean history; + } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceResourcePricePageReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceResourcePricePageReqVO.java index 9827806..5ba45dd 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceResourcePricePageReqVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceResourcePricePageReqVO.java @@ -5,8 +5,10 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; import javax.validation.constraints.NotNull; +import java.time.LocalDate; @Schema(description = "管理后台 - 信息价工料机价格历史分页 Request VO") @Data @@ -18,4 +20,12 @@ public class InfoPriceResourcePricePageReqVO extends PageParam { @NotNull(message = "工料机信息ID不能为空") private Long resourceId; + @Schema(description = "开始时间(查询范围)", example = "2024-01-01") + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate startTime; + + @Schema(description = "结束时间(查询范围)", example = "2024-12-31") + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate endTime; + } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceResourcePriceRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceResourcePriceRespVO.java index fbdd89b..31e4cab 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceResourcePriceRespVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceResourcePriceRespVO.java @@ -1,5 +1,6 @@ package com.yhy.module.core.controller.admin.infoprice.vo; +import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -8,6 +9,8 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.util.Map; +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY; + @Schema(description = "管理后台 - 信息价工料机价格历史 Response VO") @Data public class InfoPriceResourcePriceRespVO { @@ -22,9 +25,11 @@ public class InfoPriceResourcePriceRespVO { private Long resourceId; @Schema(description = "开始时间", example = "2024-01-01") + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY) private LocalDate startTime; @Schema(description = "结束时间", example = "2024-01-31") + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY) private LocalDate endTime; @Schema(description = "除税单价", example = "450.00") diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceResourceRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceResourceRespVO.java index 263e2af..4c142b7 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceResourceRespVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceResourceRespVO.java @@ -23,13 +23,16 @@ public class InfoPriceResourceRespVO { @Schema(description = "编码", example = "RES001") private String code; - @Schema(description = "名称", example = "水泥") + @Schema(description = "引用工料机ID", example = "1") + private Long sourceResourceItemId; + + @Schema(description = "名称(虚拟字段,从工料机表获取)", example = "水泥") private String name; - @Schema(description = "型号规格", example = "P.O 42.5") + @Schema(description = "型号规格(虚拟字段,从工料机表获取)", example = "P.O 42.5") private String spec; - @Schema(description = "单位", example = "t") + @Schema(description = "单位(虚拟字段,从工料机表获取)", example = "t") private String unit; @Schema(description = "除税编制价", example = "450.00") @@ -53,13 +56,22 @@ public class InfoPriceResourceRespVO { @Schema(description = "排序", example = "1") private Integer sortOrder; - @Schema(description = "资源项ID(可选,关联标准库)", example = "1") - private Long resourceItemId; - @Schema(description = "扩展属性", example = "{}") private Map attributes; @Schema(description = "创建时间") private LocalDateTime createTime; + @Schema(description = "信息价册名称(虚拟字段,从信息价册表获取)", example = "2024年1月深圳建筑工程信息价") + private String bookName; + + @Schema(description = "信息价册附件(虚拟字段,从信息价册表获取)", example = "http://example.com/attachment.pdf") + private String bookAttachment; + + @Schema(description = "信息价专业类型(虚拟字段,从信息价树表获取)", example = "profession_1") + private String professionType; + + @Schema(description = "完整地区路径(虚拟字段,从信息价树表获取)", example = "广东 / 深圳 / 南山区") + private String fullRegion; + } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceResourceSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceResourceSaveReqVO.java index 56d5293..65d3578 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceResourceSaveReqVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/infoprice/vo/InfoPriceResourceSaveReqVO.java @@ -4,7 +4,6 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; import java.math.BigDecimal; import java.util.Map; @@ -15,22 +14,21 @@ public class InfoPriceResourceSaveReqVO { @Schema(description = "主键", example = "1") private Long id; - @Schema(description = "分类树节点ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @NotNull(message = "分类树节点ID不能为空") + @Schema(description = "分类树节点ID(创建时必填,更新时可选)", example = "1") private Long categoryTreeId; @Schema(description = "编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "RES001") @NotBlank(message = "编码不能为空") private String code; - @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "水泥") + @Schema(description = "名称(用于查找或创建工料机)", requiredMode = Schema.RequiredMode.REQUIRED, example = "水泥") @NotBlank(message = "名称不能为空") private String name; - @Schema(description = "型号规格", example = "P.O 42.5") + @Schema(description = "型号规格(用于查找或创建工料机)", example = "P.O 42.5") private String spec; - @Schema(description = "单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "t") + @Schema(description = "单位(用于查找或创建工料机)", requiredMode = Schema.RequiredMode.REQUIRED, example = "t") @NotBlank(message = "单位不能为空") private String unit; @@ -49,12 +47,12 @@ public class InfoPriceResourceSaveReqVO { @Schema(description = "分类ID", example = "1") private Long categoryId; + @Schema(description = "源工料机ID(用于复制价格历史)", example = "1") + private Long sourceResourceItemId; + @Schema(description = "备注", example = "备注信息") private String remark; - @Schema(description = "资源项ID(可选,关联标准库)", example = "1") - private Long resourceItemId; - @Schema(description = "扩展属性", example = "{}") private Map attributes; diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaAdjustmentDetailController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaAdjustmentDetailController.java index fd777e9..facb82b 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaAdjustmentDetailController.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaAdjustmentDetailController.java @@ -73,7 +73,7 @@ public class QuotaAdjustmentDetailController { @GetMapping("/list") @Operation(summary = "获得定额调整明细列表") - @Parameter(name = "quotaItemId", description = "定额子目ID", required = true, example = "1") + @Parameter(name = "quotaItemId", description = "定额基价ID", required = true, example = "1") @PreAuthorize("@ss.hasPermission('core:quota:adjustment-detail:query')") public CommonResult> getQuotaAdjustmentDetailList(@RequestParam("quotaItemId") Long quotaItemId) { List list = quotaAdjustmentDetailService.getQuotaAdjustmentDetailListByQuotaItem(quotaItemId); @@ -99,7 +99,7 @@ public class QuotaAdjustmentDetailController { @GetMapping("/combined-list") @Operation(summary = "获取调整设置与明细的组合列表") - @Parameter(name = "quotaItemId", description = "定额子目ID", required = true, example = "1") + @Parameter(name = "quotaItemId", description = "定额基价ID", required = true, example = "1") @PreAuthorize("@ss.hasPermission('core:quota:adjustment-detail:query')") public CommonResult> getCombinedList(@RequestParam("quotaItemId") Long quotaItemId) { return success(quotaAdjustmentDetailService.getCombinedList(quotaItemId)); diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaAdjustmentSettingController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaAdjustmentSettingController.java index d129e42..c4b95c6 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaAdjustmentSettingController.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaAdjustmentSettingController.java @@ -70,7 +70,7 @@ public class QuotaAdjustmentSettingController { @GetMapping("/list") @Operation(summary = "获得定额调整设置列表") - @Parameter(name = "quotaItemId", description = "定额子目ID", required = true, example = "1") + @Parameter(name = "quotaItemId", description = "定额基价ID", required = true, example = "1") @PreAuthorize("@ss.hasPermission('core:quota:adjustment-setting:query')") public CommonResult> getQuotaAdjustmentSettingList(@RequestParam("quotaItemId") Long quotaItemId) { List list = quotaAdjustmentSettingService.getQuotaAdjustmentSettingList(quotaItemId); diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaCatalogItemController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaCatalogItemController.java index 35ba486..8cc6a25 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaCatalogItemController.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaCatalogItemController.java @@ -1,6 +1,6 @@ package com.yhy.module.core.controller.admin.quota; -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.*; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import com.yhy.module.core.controller.admin.quota.vo.QuotaCatalogItemBindSpecialtyReqVO; @@ -82,6 +82,14 @@ public class QuotaCatalogItemController { return success(quotaCatalogItemService.getQuotaCatalogItemList()); } + @GetMapping("/children") + @Operation(summary = "获得指定节点的子节点列表") + @Parameter(name = "parentId", description = "父节点ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:quota:query')") + public CommonResult> getChildrenByParentId(@RequestParam("parentId") Long parentId) { + return success(quotaCatalogItemService.getChildrenByParentId(parentId)); + } + @GetMapping("/tree") @Operation(summary = "获得定额专业树结构(第一层)") @PreAuthorize("@ss.hasPermission('core:quota:query')") diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaCatalogTreeController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaCatalogTreeController.java index 2e84954..6b97d79 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaCatalogTreeController.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaCatalogTreeController.java @@ -24,7 +24,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -@Tag(name = "管理后台 - 定额子目树") +@Tag(name = "管理后台 - 定额基价树") @RestController @RequestMapping("/core/quota/catalog-tree") @Validated @@ -34,14 +34,14 @@ public class QuotaCatalogTreeController { private QuotaCatalogTreeService quotaCatalogTreeService; @PostMapping("/create") - @Operation(summary = "创建定额子目树节点") + @Operation(summary = "创建定额基价树节点") @PreAuthorize("@ss.hasPermission('core:quota:catalog-tree:create')") public CommonResult createCatalogTree(@Valid @RequestBody QuotaCatalogTreeSaveReqVO createReqVO) { return success(quotaCatalogTreeService.createCatalogTree(createReqVO)); } @PutMapping("/update") - @Operation(summary = "更新定额子目树节点") + @Operation(summary = "更新定额基价树节点") @PreAuthorize("@ss.hasPermission('core:quota:catalog-tree:update')") public CommonResult updateCatalogTree(@Valid @RequestBody QuotaCatalogTreeSaveReqVO updateReqVO) { quotaCatalogTreeService.updateCatalogTree(updateReqVO); @@ -49,7 +49,7 @@ public class QuotaCatalogTreeController { } @DeleteMapping("/delete") - @Operation(summary = "删除定额子目树节点") + @Operation(summary = "删除定额基价树节点") @Parameter(name = "id", description = "节点ID", required = true, example = "1") @PreAuthorize("@ss.hasPermission('core:quota:catalog-tree:delete')") public CommonResult deleteCatalogTree(@RequestParam("id") Long id) { @@ -58,7 +58,7 @@ public class QuotaCatalogTreeController { } @GetMapping("/get") - @Operation(summary = "获取定额子目树节点详情") + @Operation(summary = "获取定额基价树节点详情") @Parameter(name = "id", description = "节点ID", required = true, example = "1") @PreAuthorize("@ss.hasPermission('core:quota:catalog-tree:query')") public CommonResult getCatalogTree(@RequestParam("id") Long id) { @@ -66,7 +66,7 @@ public class QuotaCatalogTreeController { } @GetMapping("/list") - @Operation(summary = "获取定额子目树节点列表") + @Operation(summary = "获取定额基价树节点列表") @Parameter(name = "catalogItemId", description = "定额专业节点ID", required = true, example = "1") @Parameter(name = "parentId", description = "父节点ID", example = "1") @PreAuthorize("@ss.hasPermission('core:quota:catalog-tree:query')") @@ -77,7 +77,7 @@ public class QuotaCatalogTreeController { } @GetMapping("/tree") - @Operation(summary = "获取定额子目树结构") + @Operation(summary = "获取定额基价树结构") @Parameter(name = "catalogItemId", description = "定额专业节点ID", required = true, example = "1") @PreAuthorize("@ss.hasPermission('core:quota:catalog-tree:query')") public CommonResult> getCatalogTreeTree( @@ -94,7 +94,7 @@ public class QuotaCatalogTreeController { } @GetMapping("/list-by-rate-item") - @Operation(summary = "根据费率项ID查询绑定的定额子目树") + @Operation(summary = "根据费率项ID查询绑定的定额基价树") @Parameter(name = "rateItemId", description = "费率项ID", required = true, example = "3011") @PreAuthorize("@ss.hasPermission('core:quota:catalog-tree:query')") public CommonResult> getCatalogTreeByRateItem( diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaItemController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaItemController.java index 0c92f05..ce3811c 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaItemController.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaItemController.java @@ -1,6 +1,6 @@ package com.yhy.module.core.controller.admin.quota; -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.*; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import com.yhy.module.core.controller.admin.quota.vo.QuotaItemRespVO; @@ -25,11 +25,11 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** - * 定额子目 Controller + * 定额基价 Controller * * @author yhy */ -@Tag(name = "管理后台 - 定额子目") +@Tag(name = "管理后台 - 定额基价") @RestController @RequestMapping("/core/quota/item") @Validated @@ -39,14 +39,14 @@ public class QuotaItemController { private QuotaItemService quotaItemService; @PostMapping("/create") - @Operation(summary = "创建定额子目") + @Operation(summary = "创建定额基价") @PreAuthorize("@ss.hasPermission('core:quota:create')") public CommonResult createQuotaItem(@Valid @RequestBody QuotaItemSaveReqVO createReqVO) { return success(quotaItemService.createQuotaItem(createReqVO)); } @PutMapping("/update") - @Operation(summary = "更新定额子目") + @Operation(summary = "更新定额基价") @PreAuthorize("@ss.hasPermission('core:quota:update')") public CommonResult updateQuotaItem(@Valid @RequestBody QuotaItemSaveReqVO updateReqVO) { quotaItemService.updateQuotaItem(updateReqVO); @@ -54,7 +54,7 @@ public class QuotaItemController { } @DeleteMapping("/delete") - @Operation(summary = "删除定额子目") + @Operation(summary = "删除定额基价") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('core:quota:delete')") public CommonResult deleteQuotaItem(@RequestParam("id") Long id) { @@ -63,7 +63,7 @@ public class QuotaItemController { } @GetMapping("/get") - @Operation(summary = "获得定额子目详情") + @Operation(summary = "获得定额基价详情") @Parameter(name = "id", description = "编号", required = true, example = "1") @PreAuthorize("@ss.hasPermission('core:quota:query')") public CommonResult getQuotaItem(@RequestParam("id") Long id) { @@ -71,7 +71,7 @@ public class QuotaItemController { } @GetMapping("/list") - @Operation(summary = "获得定额子目列表") + @Operation(summary = "获得定额基价列表") @Parameter(name = "catalogItemId", description = "定额条目ID", required = false, example = "1") @PreAuthorize("@ss.hasPermission('core:quota:query')") public CommonResult> getQuotaItemList(@RequestParam(value = "catalogItemId", required = false) Long catalogItemId) { @@ -83,10 +83,26 @@ public class QuotaItemController { @PostMapping("/calculate-price") @Operation(summary = "计算定额基价") - @Parameter(name = "id", description = "定额子目ID", required = true) + @Parameter(name = "id", description = "定额基价ID", required = true) @PreAuthorize("@ss.hasPermission('core:quota:update')") public CommonResult calculateBasePrice(@RequestParam("id") Long id) { quotaItemService.calculateBasePrice(id); return success(true); } + + @GetMapping("/get-by-code") + @Operation(summary = "根据编码查询定额基价") + @Parameter(name = "code", description = "编码", required = true, example = "A001") + @PreAuthorize("@ss.hasPermission('core:quota:query')") + public CommonResult> getQuotaItemByCode(@RequestParam("code") String code) { + return success(quotaItemService.getQuotaItemByCode(code)); + } + + @GetMapping("/get-rate-mode-id") + @Operation(summary = "获取定额基价对应的费率模式节点ID") + @Parameter(name = "quotaItemId", description = "定额基价ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:quota:query')") + public CommonResult getRateModeIdByQuotaItem(@RequestParam("quotaItemId") Long quotaItemId) { + return success(quotaItemService.getRateModeIdByQuotaItem(quotaItemId)); + } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaMarketMaterialController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaMarketMaterialController.java new file mode 100644 index 0000000..2f26693 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaMarketMaterialController.java @@ -0,0 +1,109 @@ +package com.yhy.module.core.controller.admin.quota; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.quota.vo.QuotaMarketMaterialRespVO; +import com.yhy.module.core.controller.admin.quota.vo.QuotaMarketMaterialSaveReqVO; +import com.yhy.module.core.controller.admin.resource.vo.ResourceItemRespVO; +import com.yhy.module.core.service.quota.QuotaMarketMaterialService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import javax.annotation.Resource; +import javax.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 定额市场主材设备 Controller + * + * @author yhy + */ +@Tag(name = "管理后台 - 定额市场主材设备") +@RestController +@RequestMapping("/core/quota/market-material") +@Validated +public class QuotaMarketMaterialController { + + @Resource + private QuotaMarketMaterialService quotaMarketMaterialService; + + @PostMapping("/create") + @Operation(summary = "添加定额市场主材设备") + @PreAuthorize("@ss.hasPermission('core:quota:create')") + public CommonResult createMarketMaterial(@Valid @RequestBody QuotaMarketMaterialSaveReqVO createReqVO) { + return success(quotaMarketMaterialService.createMarketMaterial(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新定额市场主材设备") + @PreAuthorize("@ss.hasPermission('core:quota:update')") + public CommonResult updateMarketMaterial(@Valid @RequestBody QuotaMarketMaterialSaveReqVO updateReqVO) { + quotaMarketMaterialService.updateMarketMaterial(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除定额市场主材设备") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('core:quota:delete')") + public CommonResult deleteMarketMaterial(@RequestParam("id") Long id) { + quotaMarketMaterialService.deleteMarketMaterial(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得定额市场主材设备") + @Parameter(name = "id", description = "编号", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:quota:query')") + public CommonResult getMarketMaterial(@RequestParam("id") Long id) { + return success(quotaMarketMaterialService.getMarketMaterialList(id).stream().findFirst().orElse(null)); + } + + @GetMapping("/list") + @Operation(summary = "获得定额市场主材设备列表") + @Parameter(name = "quotaItemId", description = "定额基价ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:quota:query')") + public CommonResult> getMarketMaterialList(@RequestParam("quotaItemId") Long quotaItemId) { + return success(quotaMarketMaterialService.getMarketMaterialList(quotaItemId)); + } + + @GetMapping("/available-list") + @Operation(summary = "获取可选的工料机列表(已过滤范围,支持模糊查询)") + @Parameter(name = "quotaItemId", description = "定额基价ID", required = true, example = "1") + @Parameter(name = "code", description = "编码(模糊查询)", required = false, example = "C001") + @Parameter(name = "name", description = "名称(模糊查询)", required = false, example = "水泥") + @Parameter(name = "spec", description = "型号规格(模糊查询)", required = false, example = "P.O 42.5") + @PreAuthorize("@ss.hasPermission('core:quota:query')") + public CommonResult> getAvailableResourceItems( + @RequestParam("quotaItemId") Long quotaItemId, + @RequestParam(value = "code", required = false) String code, + @RequestParam(value = "name", required = false) String name, + @RequestParam(value = "spec", required = false) String spec) { + if (code == null && name == null && spec == null) { + return success(quotaMarketMaterialService.getAvailableResourceItems(quotaItemId)); + } + return success(quotaMarketMaterialService.getAvailableResourceItemsWithFilter(quotaItemId, code, name, spec)); + } + + @GetMapping("/get-by-code") + @Operation(summary = "根据编码查询可用工料机(精确匹配)") + @Parameter(name = "quotaItemId", description = "定额基价ID", required = true, example = "1") + @Parameter(name = "code", description = "工料机编码", required = true, example = "C001") + @PreAuthorize("@ss.hasPermission('core:quota:query')") + public CommonResult getResourceItemByCode( + @RequestParam("quotaItemId") Long quotaItemId, + @RequestParam("code") String code) { + return success(quotaMarketMaterialService.getResourceItemByCode(quotaItemId, code)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaResourceController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaResourceController.java index 41bf0f8..4989683 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaResourceController.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaResourceController.java @@ -72,7 +72,7 @@ public class QuotaResourceController { @GetMapping("/list") @Operation(summary = "获得定额工料机组成列表") - @Parameter(name = "quotaItemId", description = "定额子目ID", required = true, example = "1") + @Parameter(name = "quotaItemId", description = "定额基价ID", required = true, example = "1") @PreAuthorize("@ss.hasPermission('core:quota:query')") public CommonResult> getQuotaResourceList(@RequestParam("quotaItemId") Long quotaItemId) { return success(quotaResourceService.getQuotaResourceList(quotaItemId)); @@ -80,7 +80,7 @@ public class QuotaResourceController { @GetMapping("/available-list") @Operation(summary = "获取可选的工料机列表(已过滤范围,支持模糊查询)") - @Parameter(name = "quotaItemId", description = "定额子目ID", required = true, example = "1") + @Parameter(name = "quotaItemId", description = "定额基价ID", required = true, example = "1") @Parameter(name = "code", description = "编码(模糊查询)", required = false, example = "C001") @Parameter(name = "name", description = "名称(模糊查询)", required = false, example = "水泥") @Parameter(name = "spec", description = "型号规格(模糊查询)", required = false, example = "P.O 42.5") @@ -97,4 +97,20 @@ public class QuotaResourceController { // 有查询条件时使用带过滤的方法 return success(quotaResourceService.getAvailableResourceItemsWithFilter(quotaItemId, code, name, spec)); } + + @GetMapping("/get-by-code") + @Operation(summary = "根据编码查询可用工料机(精确匹配)") + @Parameter(name = "quotaItemId", description = "定额基价ID", required = true, example = "1") + @Parameter(name = "code", description = "工料机编码", required = true, example = "C001") + @PreAuthorize("@ss.hasPermission('core:quota:query')") + public CommonResult getResourceItemByCode( + @RequestParam("quotaItemId") Long quotaItemId, + @RequestParam("code") String code) { + return success(quotaResourceService.getResourceItemByCode(quotaItemId, code)); + } + + // 【已删除】后台定额调整功能改为纯展示效果,以下接口已删除: + // - /apply-adjustment + // - /apply-dynamic-adjustment + // - /apply-dynamic-merge } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaUnifiedFeeController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaUnifiedFeeController.java new file mode 100644 index 0000000..fa9fb63 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaUnifiedFeeController.java @@ -0,0 +1,93 @@ +package com.yhy.module.core.controller.admin.quota; + +import cn.hutool.core.bean.BeanUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.quota.vo.QuotaFeeItemWithRateRespVO; +import com.yhy.module.core.controller.admin.quota.vo.QuotaUnifiedFeeRespVO; +import com.yhy.module.core.controller.admin.quota.vo.QuotaUnifiedFeeSaveReqVO; +import com.yhy.module.core.dal.dataobject.quota.QuotaUnifiedFeeDO; +import com.yhy.module.core.service.quota.QuotaUnifiedFeeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Resource; +import javax.validation.Valid; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 统一取费单价") +@RestController +@RequestMapping("/core/quota/unified-fee") +@Validated +public class QuotaUnifiedFeeController { + + @Resource + private QuotaUnifiedFeeService unifiedFeeService; + + @PostMapping("/create") + @Operation(summary = "创建统一取费单价") + public CommonResult createUnifiedFee(@Valid @RequestBody QuotaUnifiedFeeSaveReqVO createReqVO) { + return success(unifiedFeeService.createUnifiedFee(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新统一取费单价") + public CommonResult updateUnifiedFee(@Valid @RequestBody QuotaUnifiedFeeSaveReqVO updateReqVO) { + unifiedFeeService.updateUnifiedFee(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除统一取费单价") + @Parameter(name = "id", description = "编号", required = true) + public CommonResult deleteUnifiedFee(@RequestParam("id") Long id) { + unifiedFeeService.deleteUnifiedFee(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获取统一取费单价详情") + @Parameter(name = "id", description = "编号", required = true) + public CommonResult getUnifiedFee(@RequestParam("id") Long id) { + QuotaUnifiedFeeDO unifiedFee = unifiedFeeService.getUnifiedFee(id); + return success(BeanUtil.copyProperties(unifiedFee, QuotaUnifiedFeeRespVO.class)); + } + + @GetMapping("/list") + @Operation(summary = "获取统一取费单价列表") + @Parameter(name = "catalogItemId", description = "模式节点ID", required = true) + public CommonResult> getUnifiedFeeList( + @RequestParam("catalogItemId") Long catalogItemId) { + List list = unifiedFeeService.getUnifiedFeeList(catalogItemId); + return success(list.stream() + .map(item -> BeanUtil.copyProperties(item, QuotaUnifiedFeeRespVO.class)) + .collect(Collectors.toList())); + } + + @GetMapping("/tree") + @Operation(summary = "获取统一取费单价树(与定额取费显示一致的费率项+取费项合并视图)") + @Parameter(name = "catalogItemId", description = "模式节点ID", required = true) + public CommonResult> getUnifiedFeeTree( + @RequestParam("catalogItemId") Long catalogItemId) { + return success(unifiedFeeService.getUnifiedFeeTree(catalogItemId)); + } + + @PostMapping("/swap-sort") + @Operation(summary = "交换排序") + public CommonResult swapSort(@RequestParam("nodeId1") Long nodeId1, + @RequestParam("nodeId2") Long nodeId2) { + unifiedFeeService.swapSort(nodeId1, nodeId2); + return success(true); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaUnifiedFeeSettingController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaUnifiedFeeSettingController.java new file mode 100644 index 0000000..3dbe97e --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaUnifiedFeeSettingController.java @@ -0,0 +1,183 @@ +package com.yhy.module.core.controller.admin.quota; + +import cn.hutool.core.bean.BeanUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.quota.vo.QuotaUnifiedFeeResourceRespVO; +import com.yhy.module.core.controller.admin.quota.vo.QuotaUnifiedFeeResourceSaveReqVO; +import com.yhy.module.core.controller.admin.quota.vo.QuotaUnifiedFeeSettingRespVO; +import com.yhy.module.core.controller.admin.quota.vo.QuotaUnifiedFeeSettingSaveReqVO; +import com.yhy.module.core.dal.dataobject.quota.QuotaUnifiedFeeResourceDO; +import com.yhy.module.core.dal.dataobject.quota.QuotaUnifiedFeeSettingDO; +import com.yhy.module.core.service.quota.QuotaUnifiedFeeResourceService; +import com.yhy.module.core.service.quota.QuotaUnifiedFeeSettingService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Resource; +import javax.validation.Valid; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 统一取费设置") +@RestController +@RequestMapping("/core/quota/unified-fee-setting") +@Validated +public class QuotaUnifiedFeeSettingController { + + @Resource + private QuotaUnifiedFeeSettingService unifiedFeeSettingService; + + @Resource + private QuotaUnifiedFeeResourceService unifiedFeeResourceService; + + @PostMapping("/create") + @Operation(summary = "创建统一取费设置") + public CommonResult createUnifiedFeeSetting(@Valid @RequestBody QuotaUnifiedFeeSettingSaveReqVO createReqVO) { + return success(unifiedFeeSettingService.createUnifiedFeeSetting(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新统一取费设置") + public CommonResult updateUnifiedFeeSetting(@Valid @RequestBody QuotaUnifiedFeeSettingSaveReqVO updateReqVO) { + unifiedFeeSettingService.updateUnifiedFeeSetting(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除统一取费设置") + @Parameter(name = "id", description = "编号", required = true) + public CommonResult deleteUnifiedFeeSetting(@RequestParam("id") Long id) { + unifiedFeeSettingService.deleteUnifiedFeeSetting(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获取统一取费设置详情") + @Parameter(name = "id", description = "编号", required = true) + public CommonResult getUnifiedFeeSetting(@RequestParam("id") Long id) { + QuotaUnifiedFeeSettingDO unifiedFeeSetting = unifiedFeeSettingService.getUnifiedFeeSetting(id); + return success(BeanUtil.copyProperties(unifiedFeeSetting, QuotaUnifiedFeeSettingRespVO.class)); + } + + @GetMapping("/list") + @Operation(summary = "获取统一取费设置列表") + @Parameter(name = "catalogItemId", description = "模式节点ID", required = true) + public CommonResult> getUnifiedFeeSettingList( + @RequestParam("catalogItemId") Long catalogItemId) { + List list = unifiedFeeSettingService.getUnifiedFeeSettingList(catalogItemId); + return success(list.stream() + .map(item -> BeanUtil.copyProperties(item, QuotaUnifiedFeeSettingRespVO.class)) + .collect(Collectors.toList())); + } + + @GetMapping("/tree") + @Operation(summary = "获取统一取费设置树") + @Parameter(name = "catalogItemId", description = "模式节点ID", required = true) + public CommonResult> getUnifiedFeeSettingTree( + @RequestParam("catalogItemId") Long catalogItemId) { + return success(unifiedFeeSettingService.getUnifiedFeeSettingTree(catalogItemId)); + } + + @GetMapping("/parent-list") + @Operation(summary = "获取父定额列表(用于工作台,包含子定额取费章节组合用于范围过滤)") + @Parameter(name = "catalogItemId", description = "模式节点ID", required = true) + public CommonResult> getParentList( + @RequestParam("catalogItemId") Long catalogItemId) { + // 返回父定额列表,但每个父定额的feeChapter字段包含其所有子定额的取费章节组合 + return success(unifiedFeeSettingService.getParentListWithChildFeeChapters(catalogItemId)); + } + + @GetMapping("/child-list") + @Operation(summary = "获取子定额列表") + @Parameter(name = "parentId", description = "父定额ID", required = true) + public CommonResult> getChildList( + @RequestParam("parentId") Long parentId) { + List list = unifiedFeeSettingService.getChildList(parentId); + return success(list.stream() + .map(item -> BeanUtil.copyProperties(item, QuotaUnifiedFeeSettingRespVO.class)) + .collect(Collectors.toList())); + } + + @PostMapping("/swap-sort") + @Operation(summary = "交换排序") + public CommonResult swapSort(@RequestParam("nodeId1") Long nodeId1, + @RequestParam("nodeId2") Long nodeId2) { + unifiedFeeSettingService.swapSort(nodeId1, nodeId2); + return success(true); + } + + // ==================== 子目工料机相关接口 ==================== + + @PostMapping("/resource/create") + @Operation(summary = "创建子目工料机") + public CommonResult createUnifiedFeeResource(@Valid @RequestBody QuotaUnifiedFeeResourceSaveReqVO createReqVO) { + return success(unifiedFeeResourceService.createUnifiedFeeResource(createReqVO)); + } + + @PutMapping("/resource/update") + @Operation(summary = "更新子目工料机") + public CommonResult updateUnifiedFeeResource(@Valid @RequestBody QuotaUnifiedFeeResourceSaveReqVO updateReqVO) { + unifiedFeeResourceService.updateUnifiedFeeResource(updateReqVO); + return success(true); + } + + @DeleteMapping("/resource/delete") + @Operation(summary = "删除子目工料机") + @Parameter(name = "id", description = "编号", required = true) + public CommonResult deleteUnifiedFeeResource(@RequestParam("id") Long id) { + unifiedFeeResourceService.deleteUnifiedFeeResource(id); + return success(true); + } + + @GetMapping("/resource/list") + @Operation(summary = "获取子目工料机列表") + @Parameter(name = "unifiedFeeSettingId", description = "子定额ID", required = true) + public CommonResult> getUnifiedFeeResourceList( + @RequestParam("unifiedFeeSettingId") Long unifiedFeeSettingId) { + return success(unifiedFeeResourceService.getUnifiedFeeResourceList(unifiedFeeSettingId)); + } + + @PostMapping("/resource/swap-sort") + @Operation(summary = "交换子目工料机排序") + public CommonResult swapResourceSort(@RequestParam("nodeId1") Long nodeId1, + @RequestParam("nodeId2") Long nodeId2) { + unifiedFeeResourceService.swapSort(nodeId1, nodeId2); + return success(true); + } + + @GetMapping("/resource/get-by-code") + @Operation(summary = "根据编码查询工料机") + @Parameter(name = "unifiedFeeSettingId", description = "统一取费设置ID", required = true) + @Parameter(name = "code", description = "工料机编码", required = true) + public CommonResult getResourceItemByCode( + @RequestParam("unifiedFeeSettingId") Long unifiedFeeSettingId, + @RequestParam("code") String code) { + return success(unifiedFeeResourceService.getResourceItemByCode(unifiedFeeSettingId, code)); + } + + @GetMapping("/resource/available-list") + @Operation(summary = "获取可选工料机列表(支持模糊查询)") + @Parameter(name = "unifiedFeeSettingId", description = "统一取费设置ID", required = true) + @Parameter(name = "code", description = "编码(模糊查询)", required = false) + @Parameter(name = "name", description = "名称(模糊查询)", required = false) + @Parameter(name = "spec", description = "型号规格(模糊查询)", required = false) + public CommonResult> getAvailableResourceItems( + @RequestParam("unifiedFeeSettingId") Long unifiedFeeSettingId, + @RequestParam(value = "code", required = false) String code, + @RequestParam(value = "name", required = false) String name, + @RequestParam(value = "spec", required = false) String spec) { + return success(unifiedFeeResourceService.getAvailableResourceItemsWithFilter( + unifiedFeeSettingId, code, name, spec)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaVariableSettingController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaVariableSettingController.java new file mode 100644 index 0000000..9c70f53 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/QuotaVariableSettingController.java @@ -0,0 +1,140 @@ +package com.yhy.module.core.controller.admin.quota; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import com.yhy.module.core.controller.admin.quota.vo.QuotaVariableSettingRespVO; +import com.yhy.module.core.controller.admin.quota.vo.QuotaVariableSettingSaveReqVO; +import com.yhy.module.core.dal.dataobject.quota.QuotaVariableSettingDO; +import com.yhy.module.core.service.quota.QuotaVariableSettingService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import javax.annotation.Resource; +import javax.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "管理后台 - 单位工程变量设置") +@RestController +@RequestMapping("/core/quota/variable-setting") +@Validated +public class QuotaVariableSettingController { + + @Resource + private QuotaVariableSettingService quotaVariableSettingService; + + @PostMapping("/create") + @Operation(summary = "创建变量设置") + @PreAuthorize("@ss.hasPermission('core:quota:variable-setting:create')") + public CommonResult createVariableSetting(@Valid @RequestBody QuotaVariableSettingSaveReqVO createReqVO) { + return success(quotaVariableSettingService.createVariableSetting(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新变量设置") + @PreAuthorize("@ss.hasPermission('core:quota:variable-setting:update')") + public CommonResult updateVariableSetting(@Valid @RequestBody QuotaVariableSettingSaveReqVO updateReqVO) { + quotaVariableSettingService.updateVariableSetting(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除变量设置") + @Parameter(name = "id", description = "变量设置ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:quota:variable-setting:delete')") + public CommonResult deleteVariableSetting(@RequestParam("id") Long id) { + quotaVariableSettingService.deleteVariableSetting(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获取变量设置详情") + @Parameter(name = "id", description = "变量设置ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:quota:variable-setting:query')") + public CommonResult getVariableSetting(@RequestParam("id") Long id) { + QuotaVariableSettingDO variableSetting = quotaVariableSettingService.getVariableSetting(id); + return success(BeanUtils.toBean(variableSetting, QuotaVariableSettingRespVO.class)); + } + + @GetMapping("/list") + @Operation(summary = "获取变量设置列表") + @Parameters({ + @Parameter(name = "catalogItemId", description = "费率模式节点ID", required = true, example = "1002"), + @Parameter(name = "category", description = "类别:division/measure/other/unit_summary", required = true, example = "division") + }) + @PreAuthorize("@ss.hasPermission('core:quota:variable-setting:query')") + public CommonResult> getVariableSettingList( + @RequestParam("catalogItemId") Long catalogItemId, + @RequestParam("category") String category) { + List list = quotaVariableSettingService.getVariableSettingListWithFeeItems(catalogItemId, category); + return success(list); + } + + @GetMapping("/list-all") + @Operation(summary = "获取所有类别的变量设置列表") + @Parameter(name = "catalogItemId", description = "费率模式节点ID", required = true, example = "1002") + @PreAuthorize("@ss.hasPermission('core:quota:variable-setting:query')") + public CommonResult> getVariableSettingListAll( + @RequestParam("catalogItemId") Long catalogItemId) { + List list = quotaVariableSettingService.getVariableSettingListAllWithFeeItems(catalogItemId); + return success(list); + } + + @GetMapping("/list-all-with-summary") + @Operation(summary = "获取所有类别的变量设置列表(包含汇总值计算)") + @Parameters({ + @Parameter(name = "catalogItemId", description = "定额专业节点ID", required = true, example = "1002"), + @Parameter(name = "compileTreeId", description = "编制模式树的单位工程节点ID", required = true, example = "1001"), + @Parameter(name = "baseNumberRangeIds", description = "基数范围选中的节点ID列表(逗号分隔)", required = false, example = "1,2,3") + }) + @PreAuthorize("@ss.hasPermission('core:quota:variable-setting:query')") + public CommonResult> getVariableSettingListAllWithSummary( + @RequestParam("catalogItemId") Long catalogItemId, + @RequestParam("compileTreeId") Long compileTreeId, + @RequestParam(value = "baseNumberRangeIds", required = false) List baseNumberRangeIds) { + List list = quotaVariableSettingService.getVariableSettingListAllWithSummary( + catalogItemId, compileTreeId, baseNumberRangeIds); + return success(list); + } + + @GetMapping("/list-by-compile-tree") + @Operation(summary = "根据编制树ID获取所有变量设置(自动合并所有定额专业)") + @Parameters({ + @Parameter(name = "compileTreeId", description = "编制模式树的单位工程节点ID", required = true, example = "1001"), + @Parameter(name = "baseNumberRangeIds", description = "基数范围选中的节点ID列表(逗号分隔)", required = false, example = "1,2,3") + }) + @PreAuthorize("@ss.hasPermission('core:quota:variable-setting:query')") + public CommonResult> getVariableSettingListByCompileTree( + @RequestParam("compileTreeId") Long compileTreeId, + @RequestParam(value = "baseNumberRangeIds", required = false) List baseNumberRangeIds) { + List list = quotaVariableSettingService.getVariableSettingListByCompileTree( + compileTreeId, baseNumberRangeIds); + return success(list); + } + + @PostMapping("/swap-sort") + @Operation(summary = "交换排序") + @Parameters({ + @Parameter(name = "nodeId1", description = "节点1 ID", required = true, example = "1"), + @Parameter(name = "nodeId2", description = "节点2 ID", required = true, example = "2") + }) + @PreAuthorize("@ss.hasPermission('core:quota:variable-setting:update')") + public CommonResult swapSort( + @RequestParam("nodeId1") Long nodeId1, + @RequestParam("nodeId2") Long nodeId2) { + quotaVariableSettingService.swapSort(nodeId1, nodeId2); + return success(true); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaAdjustmentCombinedRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaAdjustmentCombinedRespVO.java index 81a8168..92051db 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaAdjustmentCombinedRespVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaAdjustmentCombinedRespVO.java @@ -14,7 +14,7 @@ public class QuotaAdjustmentCombinedRespVO { @Schema(description = "调整设置ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Long id; - @Schema(description = "定额子目ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @Schema(description = "定额基价ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Long quotaItemId; @Schema(description = "调整名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "人工费调整") diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaAdjustmentSettingRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaAdjustmentSettingRespVO.java index 72ab029..93606f7 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaAdjustmentSettingRespVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaAdjustmentSettingRespVO.java @@ -13,7 +13,7 @@ public class QuotaAdjustmentSettingRespVO { @Schema(description = "主键ID", example = "1") private Long id; - @Schema(description = "定额子目ID", example = "1") + @Schema(description = "定额基价ID", example = "1") private Long quotaItemId; @Schema(description = "调整名称", example = "人工费调整") diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaAdjustmentSettingSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaAdjustmentSettingSaveReqVO.java index 68ef130..ad1f42f 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaAdjustmentSettingSaveReqVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaAdjustmentSettingSaveReqVO.java @@ -14,8 +14,8 @@ public class QuotaAdjustmentSettingSaveReqVO { @Schema(description = "主键ID", example = "1") private Long id; - @Schema(description = "定额子目ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @NotNull(message = "定额子目ID不能为空") + @Schema(description = "定额基价ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "定额基价ID不能为空") private Long quotaItemId; @Schema(description = "调整名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "人工费调整") diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaCatalogTreeRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaCatalogTreeRespVO.java index 0fd48ee..bc1164d 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaCatalogTreeRespVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaCatalogTreeRespVO.java @@ -6,7 +6,7 @@ import java.util.List; import java.util.Map; import lombok.Data; -@Schema(description = "管理后台 - 定额子目树 Response VO") +@Schema(description = "管理后台 - 定额基价树 Response VO") @Data public class QuotaCatalogTreeRespVO { @@ -49,6 +49,9 @@ public class QuotaCatalogTreeRespVO { @Schema(description = "扩展属性") private Map attributes; + @Schema(description = "注解/备注", example = "这是一个注解") + private String remark; + @Schema(description = "创建时间") private LocalDateTime createTime; diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaCatalogTreeSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaCatalogTreeSaveReqVO.java index b4d28c3..fd2c38a 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaCatalogTreeSaveReqVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaCatalogTreeSaveReqVO.java @@ -6,7 +6,7 @@ import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import lombok.Data; -@Schema(description = "管理后台 - 定额子目树保存 Request VO") +@Schema(description = "管理后台 - 定额基价树保存 Request VO") @Data public class QuotaCatalogTreeSaveReqVO { @@ -61,4 +61,7 @@ public class QuotaCatalogTreeSaveReqVO { @Schema(description = "扩展属性") private Map attributes; + + @Schema(description = "注解/备注", example = "这是一个注解") + private String remark; } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaCatalogTreeSwapSortReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaCatalogTreeSwapSortReqVO.java index 9d0d27d..2d592c8 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaCatalogTreeSwapSortReqVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaCatalogTreeSwapSortReqVO.java @@ -4,7 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import javax.validation.constraints.NotNull; import lombok.Data; -@Schema(description = "管理后台 - 定额子目树交换排序 Request VO") +@Schema(description = "管理后台 - 定额基价树交换排序 Request VO") @Data public class QuotaCatalogTreeSwapSortReqVO { diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaDynamicAdjustReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaDynamicAdjustReqVO.java new file mode 100644 index 0000000..c69b420 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaDynamicAdjustReqVO.java @@ -0,0 +1,25 @@ +package com.yhy.module.core.controller.admin.quota.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.util.Map; +import javax.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - 定额动态调整请求 VO") +@Data +public class QuotaDynamicAdjustReqVO { + + @Schema(description = "定额基价ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "定额基价ID不能为空") + private Long quotaItemId; + + @Schema(description = "调整设置ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "调整设置ID不能为空") + private Long adjustmentSettingId; + + @Schema(description = "输入值映射(key=类别名称,value=输入值)", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "输入值不能为空") + private Map inputValues; + +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaFeeItemRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaFeeItemRespVO.java index 149aa98..e85059b 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaFeeItemRespVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaFeeItemRespVO.java @@ -30,9 +30,6 @@ public class QuotaFeeItemRespVO { @Schema(description = "计算基数") private Map calcBase; - @Schema(description = "费率代号(%)", example = "15") - private String rateCode; - @Schema(description = "代号", example = "QYGLF") private String code; @@ -51,6 +48,9 @@ public class QuotaFeeItemRespVO { @Schema(description = "是否为变量", example = "false") private Boolean variable; + @Schema(description = "系统行标识(ZHDJ=综合单价)", example = "ZHDJ") + private String systemCode; + @Schema(description = "创建时间") private LocalDateTime createTime; diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaFeeItemSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaFeeItemSaveReqVO.java index 5f577b9..1b18d51 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaFeeItemSaveReqVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaFeeItemSaveReqVO.java @@ -30,9 +30,6 @@ public class QuotaFeeItemSaveReqVO { @Schema(description = "计算基数", example = "{\"formula\":\"除税基价 + 含税基价\",\"variables\":{\"除税基价\":\"tax_excl_base_price\",\"含税基价\":\"tax_incl_base_price\"}}") private Map calcBase; - @Schema(description = "费率代号(%)", example = "15") - private String rateCode; - @Schema(description = "代号", example = "QYGLF") private String code; diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaFeeItemWithRateRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaFeeItemWithRateRespVO.java index 0aa615a..41934e1 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaFeeItemWithRateRespVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaFeeItemWithRateRespVO.java @@ -1,6 +1,7 @@ package com.yhy.module.core.controller.admin.quota.vo; import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; import java.util.Map; @@ -81,6 +82,20 @@ public class QuotaFeeItemWithRateRespVO { @Schema(description = "是否已配置取费项", example = "true") private Boolean hasFeeItem; + @Schema(description = "系统行标识(ZHDJ=综合单价)", example = "ZHDJ") + private String systemCode; + + // ==================== 计算结果(虚拟字段) ==================== + + @Schema(description = "计算基数值(虚拟字段)", example = "100.00") + private BigDecimal calcBaseValue; + + @Schema(description = "子单价(虚拟字段)", example = "15.00") + private BigDecimal subPrice; + + @Schema(description = "费率实际值(虚拟字段,费率代号转换后的数值)", example = "15.00") + private BigDecimal rateValue; + // ==================== 时间信息 ==================== @Schema(description = "创建时间") diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaItemRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaItemRespVO.java index a982b3f..5c44ec4 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaItemRespVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaItemRespVO.java @@ -1,19 +1,18 @@ package com.yhy.module.core.controller.admin.quota.vo; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; import java.util.Map; +import lombok.Data; /** - * 定额子目 Response VO + * 定额基价 Response VO * * @author yhy */ -@Schema(description = "管理后台 - 定额子目 Response VO") +@Schema(description = "管理后台 - 定额基价 Response VO") @Data public class QuotaItemRespVO { @@ -23,6 +22,12 @@ public class QuotaItemRespVO { @Schema(description = "定额条目ID", example = "1") private Long catalogItemId; + @Schema(description = "编码", example = "A001") + private String code; + + @Schema(description = "名称", example = "平整场地") + private String name; + @Schema(description = "计量单位", example = "m³") private String unit; @@ -58,6 +63,18 @@ public class QuotaItemRespVO { @Schema(description = "备注", example = "备注信息") private String remark; + @Schema(description = "除税定额单价", example = "100.00") + private BigDecimal taxExclBasePrice; + + @Schema(description = "含税定额单价", example = "113.00") + private BigDecimal taxInclBasePrice; + + @Schema(description = "除税编制单价", example = "120.00") + private BigDecimal taxExclCompilePrice; + + @Schema(description = "含税编制单价", example = "135.60") + private BigDecimal taxInclCompilePrice; + @Schema(description = "工料机组成列表") private List resources; diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaItemSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaItemSaveReqVO.java index ca3da3c..c3ecfd8 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaItemSaveReqVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaItemSaveReqVO.java @@ -1,19 +1,18 @@ package com.yhy.module.core.controller.admin.quota.vo; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -import javax.validation.constraints.NotNull; import java.math.BigDecimal; import java.util.List; import java.util.Map; +import javax.validation.constraints.NotNull; +import lombok.Data; /** - * 定额子目保存 Request VO + * 定额基价保存 Request VO * * @author yhy */ -@Schema(description = "管理后台 - 定额子目保存 Request VO") +@Schema(description = "管理后台 - 定额基价保存 Request VO") @Data public class QuotaItemSaveReqVO { @@ -24,6 +23,12 @@ public class QuotaItemSaveReqVO { @NotNull(message = "定额条目ID不能为空") private Long catalogItemId; + @Schema(description = "编码", example = "A001") + private String code; + + @Schema(description = "名称", example = "平整场地") + private String name; + @Schema(description = "计量单位", example = "m³") private String unit; diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaMarketMaterialRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaMarketMaterialRespVO.java new file mode 100644 index 0000000..9e3ae45 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaMarketMaterialRespVO.java @@ -0,0 +1,185 @@ +package com.yhy.module.core.controller.admin.quota.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import lombok.Data; + +/** + * 定额市场主材设备 Response VO + * + * @author yhy + */ +@Schema(description = "管理后台 - 定额市场主材设备 Response VO") +@Data +public class QuotaMarketMaterialRespVO { + + @Schema(description = "主键ID", example = "1") + private Long id; + + @Schema(description = "定额基价ID", example = "1") + private Long quotaItemId; + + @Schema(description = "资源项ID", example = "1") + private Long resourceItemId; + + @Schema(description = "定额消耗量", example = "350.00") + private BigDecimal dosage; + + @Schema(description = "调整消耗量", example = "700.00") + private BigDecimal adjustedDosage; + + @Schema(description = "扩展属性", example = "{\"loss_rate\":0.02}") + private Map attributes; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; + + // ========== 扩展字段(从工料机库关联) ========== + + @Schema(description = "损耗率", example = "0.02") + private BigDecimal lossRate; + + @Schema(description = "价格快照", example = "50.00") + private BigDecimal price; + + @Schema(description = "资源编码", example = "R001") + private String resourceCode; + + @Schema(description = "资源名称", example = "水泥") + private String resourceName; + + @Schema(description = "资源单位", example = "kg") + private String resourceUnit; + + @Schema(description = "资源型号规格", example = "P.O 42.5") + private String resourceSpec; + + @Schema(description = "资源类别ID", example = "1") + private Long resourceCategoryId; + + @Schema(description = "资源类型", example = "material") + private String resourceType; + + @Schema(description = "税率", example = "0.13") + private BigDecimal resourceTaxRate; + + @Schema(description = "除税基价", example = "450.00") + private BigDecimal resourceTaxExclBasePrice; + + @Schema(description = "含税基价", example = "508.50") + private BigDecimal resourceTaxInclBasePrice; + + @Schema(description = "除税编制价", example = "460.00") + private BigDecimal resourceTaxExclCompilePrice; + + @Schema(description = "含税编制价", example = "519.80") + private BigDecimal resourceTaxInclCompilePrice; + + @Schema(description = "计算基数", example = "{\"formula\":\"人机 + 材\",\"variables\":{\"人机\":2,\"材\":3}}") + private Map calcBase; + + @Schema(description = "实际消耗量(含损耗)", example = "357.00") + private BigDecimal actualDosage; + + @Schema(description = "金额", example = "17850.00") + private BigDecimal amount; + + @Schema(description = "是否复合工料机", example = "false") + private Boolean isMerged; + + @Schema(description = "复合工料机子数据列表") + private List mergedItems; + + // ========== 虚拟字段(复合工料机合价) ========== + + @Schema(description = "除税基价合价", example = "50.00") + private BigDecimal taxExclBaseTotalSum; + + @Schema(description = "含税基价合价", example = "54.50") + private BigDecimal taxInclBaseTotalSum; + + @Schema(description = "除税编制价合价", example = "55.00") + private BigDecimal taxExclCompileTotalSum; + + @Schema(description = "含税编制价合价", example = "59.95") + private BigDecimal taxInclCompileTotalSum; + + /** + * 复合工料机子数据 VO + */ + @Schema(description = "复合工料机子数据") + @Data + public static class MergedResourceItemVO { + @Schema(description = "子数据ID", example = "1") + private Long id; + + @Schema(description = "源工料机ID", example = "100") + private Long resourceItemId; + + @Schema(description = "源工料机编码", example = "C001") + private String resourceCode; + + @Schema(description = "源工料机名称", example = "水泥") + private String resourceName; + + @Schema(description = "源工料机单位", example = "t") + private String resourceUnit; + + @Schema(description = "源工料机型号规格", example = "P.O 42.5") + private String resourceSpec; + + @Schema(description = "源工料机类别ID", example = "1") + private Long resourceCategoryId; + + @Schema(description = "源工料机类型", example = "material") + private String resourceType; + + @Schema(description = "税率", example = "0.13") + private BigDecimal resourceTaxRate; + + @Schema(description = "除税基价", example = "100.00") + private BigDecimal resourceTaxExclBasePrice; + + @Schema(description = "含税基价", example = "113.00") + private BigDecimal resourceTaxInclBasePrice; + + @Schema(description = "除税编制价", example = "120.00") + private BigDecimal resourceTaxExclCompilePrice; + + @Schema(description = "含税编制价", example = "135.60") + private BigDecimal resourceTaxInclCompilePrice; + + @Schema(description = "定额消耗量", example = "1.5") + private BigDecimal dosage; + + @Schema(description = "除税市场价", example = "450.00") + private BigDecimal price; + + @Schema(description = "实际消耗量(含损耗)", example = "1.53") + private BigDecimal actualDosage; + + @Schema(description = "金额", example = "688.50") + private BigDecimal amount; + + @Schema(description = "计算基数") + private Map calcBase; + + @Schema(description = "除税基价合价", example = "50.00") + private BigDecimal taxExclBaseTotalSum; + + @Schema(description = "含税基价合价", example = "54.50") + private BigDecimal taxInclBaseTotalSum; + + @Schema(description = "除税编制价合价", example = "55.00") + private BigDecimal taxExclCompileTotalSum; + + @Schema(description = "含税编制价合价", example = "59.95") + private BigDecimal taxInclCompileTotalSum; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaMarketMaterialSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaMarketMaterialSaveReqVO.java new file mode 100644 index 0000000..7f89c49 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaMarketMaterialSaveReqVO.java @@ -0,0 +1,39 @@ +package com.yhy.module.core.controller.admin.quota.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.util.Map; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 定额市场主材设备保存 Request VO + * + * @author yhy + */ +@Schema(description = "管理后台 - 定额市场主材设备保存 Request VO") +@Data +public class QuotaMarketMaterialSaveReqVO { + + @Schema(description = "主键ID(更新时必填)", example = "1") + private Long id; + + @Schema(description = "定额基价ID", example = "1") + private Long quotaItemId; + + @Schema(description = "资源项ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "资源项ID不能为空") + private Long resourceItemId; + + @Schema(description = "定额消耗量", example = "350.00") + private BigDecimal dosage; + + @Schema(description = "调整消耗量", example = "700.00") + private BigDecimal adjustedDosage; + + @Schema(description = "扩展属性", example = "{\"loss_rate\":0.02}") + private Map attributes; + + @Schema(description = "排序字段", example = "1") + private Integer sortOrder; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaRateItemSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaRateItemSaveReqVO.java index d822070..8267f78 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaRateItemSaveReqVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaRateItemSaveReqVO.java @@ -52,6 +52,12 @@ public class QuotaRateItemSaveReqVO { @Schema(description = "排序值") private Integer sortOrder; + + @Schema(description = "参考节点ID(用于指定插入位置)") + private Long referenceNodeId; + + @Schema(description = "插入位置:above-在参考节点上方,below-在参考节点下方") + private String insertPosition; /** * 创建分组 diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaResourceRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaResourceRespVO.java index 4d7afaf..e51af1e 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaResourceRespVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaResourceRespVO.java @@ -19,7 +19,7 @@ public class QuotaResourceRespVO { @Schema(description = "主键ID", example = "1") private Long id; - @Schema(description = "定额子目ID", example = "1") + @Schema(description = "定额基价ID", example = "1") private Long quotaItemId; @Schema(description = "资源项ID", example = "1") @@ -28,6 +28,9 @@ public class QuotaResourceRespVO { @Schema(description = "定额消耗量", example = "350.00") private BigDecimal dosage; + @Schema(description = "调整消耗量(应用调整设置后的消耗量)", example = "700.00") + private BigDecimal adjustedDosage; + @Schema(description = "扩展属性", example = "{\"loss_rate\":0.02}") private Map attributes; @@ -93,6 +96,23 @@ public class QuotaResourceRespVO { @Schema(description = "复合工料机子数据列表") private List mergedItems; + // ========== 虚拟字段(复合工料机合价) ========== + + @Schema(description = "除税基价合价(虚拟字段,仅复合工料机)", example = "50.00") + private BigDecimal taxExclBaseTotalSum; + + @Schema(description = "含税基价合价(虚拟字段,仅复合工料机)", example = "54.50") + private BigDecimal taxInclBaseTotalSum; + + @Schema(description = "除税编制价合价(虚拟字段,仅复合工料机)", example = "55.00") + private BigDecimal taxExclCompileTotalSum; + + @Schema(description = "含税编制价合价(虚拟字段,仅复合工料机)", example = "59.95") + private BigDecimal taxInclCompileTotalSum; + + @Schema(description = "调整公式(虚拟字段,显示当前应用的定额调整设置计算公式)", example = "M×1+5=2") + private String adjustmentFormula; + /** * 复合工料机子数据 VO */ @@ -126,6 +146,18 @@ public class QuotaResourceRespVO { @Schema(description = "税率", example = "0.13") private BigDecimal resourceTaxRate; + @Schema(description = "除税基价", example = "100.00") + private BigDecimal resourceTaxExclBasePrice; + + @Schema(description = "含税基价", example = "113.00") + private BigDecimal resourceTaxInclBasePrice; + + @Schema(description = "除税编制价", example = "120.00") + private BigDecimal resourceTaxExclCompilePrice; + + @Schema(description = "含税编制价", example = "135.60") + private BigDecimal resourceTaxInclCompilePrice; + @Schema(description = "地区代码", example = "GD") private String regionCode; @@ -140,5 +172,22 @@ public class QuotaResourceRespVO { @Schema(description = "金额", example = "688.50") private BigDecimal amount; + + @Schema(description = "计算基数", example = "{\"formula\":\"人机 + 材\",\"variables\":{\"人机\":2,\"材\":3}}") + private Map calcBase; + + // ========== 虚拟字段(子工料机合价) ========== + + @Schema(description = "除税基价合价(虚拟字段)", example = "50.00") + private BigDecimal taxExclBaseTotalSum; + + @Schema(description = "含税基价合价(虚拟字段)", example = "54.50") + private BigDecimal taxInclBaseTotalSum; + + @Schema(description = "除税编制价合价(虚拟字段)", example = "55.00") + private BigDecimal taxExclCompileTotalSum; + + @Schema(description = "含税编制价合价(虚拟字段)", example = "59.95") + private BigDecimal taxInclCompileTotalSum; } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaResourceSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaResourceSaveReqVO.java index e097ffe..518ef11 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaResourceSaveReqVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaResourceSaveReqVO.java @@ -1,11 +1,10 @@ package com.yhy.module.core.controller.admin.quota.vo; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -import javax.validation.constraints.NotNull; import java.math.BigDecimal; import java.util.Map; +import javax.validation.constraints.NotNull; +import lombok.Data; /** * 定额工料机组成保存 Request VO @@ -19,7 +18,7 @@ public class QuotaResourceSaveReqVO { @Schema(description = "主键ID(更新时必填)", example = "1") private Long id; - @Schema(description = "定额子目ID", example = "1") + @Schema(description = "定额基价ID", example = "1") private Long quotaItemId; @Schema(description = "资源项ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @@ -30,6 +29,12 @@ public class QuotaResourceSaveReqVO { @NotNull(message = "定额消耗量不能为空") private BigDecimal dosage; + @Schema(description = "调整消耗量(应用调整设置后的消耗量)", example = "700.00") + private BigDecimal adjustedDosage; + @Schema(description = "扩展属性", example = "{\"loss_rate\":0.02}") private Map attributes; + + @Schema(description = "排序字段", example = "1") + private Integer sortOrder; } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaUnifiedFeeResourceRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaUnifiedFeeResourceRespVO.java new file mode 100644 index 0000000..783c83f --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaUnifiedFeeResourceRespVO.java @@ -0,0 +1,156 @@ +package com.yhy.module.core.controller.admin.quota.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import lombok.Data; + +@Schema(description = "管理后台 - 统一取费子目工料机响应 VO") +@Data +public class QuotaUnifiedFeeResourceRespVO { + + @Schema(description = "主键ID", example = "1") + private Long id; + + @Schema(description = "关联子定额ID", example = "1001") + private Long unifiedFeeSettingId; + + @Schema(description = "关联工料机ID(yhy_resource_item)", example = "2001") + private Long resourceItemId; + + @Schema(description = "定额消耗量", example = "1.5") + private BigDecimal dosage; + + @Schema(description = "调整消耗量", example = "1.8") + private BigDecimal adjustedDosage; + + // ==================== 关联工料机信息(从yhy_resource_item动态获取,命名与QuotaResourceRespVO保持一致)==================== + + @Schema(description = "资源编码", example = "RG001") + private String resourceCode; + + @Schema(description = "资源名称", example = "普通工") + private String resourceName; + + @Schema(description = "资源型号规格", example = "标准规格") + private String resourceSpec; + + @Schema(description = "资源类型", example = "labor") + private String resourceType; + + @Schema(description = "资源单位", example = "工日") + private String resourceUnit; + + @Schema(description = "税率", example = "0.09") + private BigDecimal resourceTaxRate; + + @Schema(description = "除税基价", example = "100.00") + private BigDecimal resourceTaxExclBasePrice; + + @Schema(description = "含税基价", example = "109.00") + private BigDecimal resourceTaxInclBasePrice; + + @Schema(description = "除税编制价", example = "120.00") + private BigDecimal resourceTaxExclCompilePrice; + + @Schema(description = "含税编制价", example = "130.80") + private BigDecimal resourceTaxInclCompilePrice; + + // ==================== 计算字段(合价 = 单价 × 消耗量)==================== + + @Schema(description = "除税基价合价", example = "150.00") + private BigDecimal taxExclBaseTotalSum; + + @Schema(description = "含税基价合价", example = "163.50") + private BigDecimal taxInclBaseTotalSum; + + @Schema(description = "除税编制价合价", example = "180.00") + private BigDecimal taxExclCompileTotalSum; + + @Schema(description = "含税编制价合价", example = "196.20") + private BigDecimal taxInclCompileTotalSum; + + @Schema(description = "扩展属性") + private Map attributes; + + @Schema(description = "计算基数", example = "{\"formula\":\"人机 + 材\",\"variables\":{\"人机\":2,\"材\":3}}") + private Map calcBase; + + @Schema(description = "排序字段", example = "1") + private Integer sortOrder; + + @Schema(description = "是否复合工料机", example = "false") + private Boolean isMerged; + + @Schema(description = "复合工料机子数据列表") + private List mergedItems; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; + + /** + * 复合工料机子数据 VO + */ + @Schema(description = "复合工料机子数据") + @Data + public static class MergedResourceItemVO { + @Schema(description = "子数据ID", example = "1") + private Long id; + + @Schema(description = "源工料机ID", example = "100") + private Long resourceItemId; + + @Schema(description = "源工料机编码", example = "C001") + private String resourceCode; + + @Schema(description = "源工料机名称", example = "水泥") + private String resourceName; + + @Schema(description = "源工料机单位", example = "t") + private String resourceUnit; + + @Schema(description = "源工料机型号规格", example = "P.O 42.5") + private String resourceSpec; + + @Schema(description = "源工料机类型", example = "material") + private String resourceType; + + @Schema(description = "税率", example = "0.13") + private BigDecimal resourceTaxRate; + + @Schema(description = "除税基价", example = "100.00") + private BigDecimal resourceTaxExclBasePrice; + + @Schema(description = "含税基价", example = "113.00") + private BigDecimal resourceTaxInclBasePrice; + + @Schema(description = "除税编制价", example = "120.00") + private BigDecimal resourceTaxExclCompilePrice; + + @Schema(description = "含税编制价", example = "135.60") + private BigDecimal resourceTaxInclCompilePrice; + + @Schema(description = "定额消耗量", example = "1.5") + private BigDecimal dosage; + + @Schema(description = "计算基数") + private Map calcBase; + + @Schema(description = "除税基价合价", example = "50.00") + private BigDecimal taxExclBaseTotalSum; + + @Schema(description = "含税基价合价", example = "54.50") + private BigDecimal taxInclBaseTotalSum; + + @Schema(description = "除税编制价合价", example = "55.00") + private BigDecimal taxExclCompileTotalSum; + + @Schema(description = "含税编制价合价", example = "59.95") + private BigDecimal taxInclCompileTotalSum; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaUnifiedFeeResourceSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaUnifiedFeeResourceSaveReqVO.java new file mode 100644 index 0000000..6957c06 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaUnifiedFeeResourceSaveReqVO.java @@ -0,0 +1,35 @@ +package com.yhy.module.core.controller.admin.quota.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.util.Map; +import javax.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - 统一取费子目工料机创建/更新 Request VO") +@Data +public class QuotaUnifiedFeeResourceSaveReqVO { + + @Schema(description = "主键ID(更新时必填)", example = "1") + private Long id; + + @Schema(description = "关联子定额ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001") + @NotNull(message = "关联子定额ID不能为空") + private Long unifiedFeeSettingId; + + @Schema(description = "关联工料机ID(yhy_resource_item)", requiredMode = Schema.RequiredMode.REQUIRED, example = "2001") + @NotNull(message = "关联工料机ID不能为空") + private Long resourceItemId; + + @Schema(description = "定额消耗量", example = "1.5") + private BigDecimal dosage; + + @Schema(description = "调整消耗量", example = "1.8") + private BigDecimal adjustedDosage; + + @Schema(description = "扩展属性") + private Map attributes; + + @Schema(description = "排序字段", example = "1") + private Integer sortOrder; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaUnifiedFeeRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaUnifiedFeeRespVO.java new file mode 100644 index 0000000..bf9c5b0 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaUnifiedFeeRespVO.java @@ -0,0 +1,72 @@ +package com.yhy.module.core.controller.admin.quota.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import lombok.Data; + +@Schema(description = "管理后台 - 统一取费单价响应 VO") +@Data +public class QuotaUnifiedFeeRespVO { + + @Schema(description = "主键ID", example = "1") + private Long id; + + @Schema(description = "模式节点ID", example = "1002") + private Long catalogItemId; + + @Schema(description = "关联定额取费项ID", example = "2001") + private Long feeItemId; + + @Schema(description = "父节点ID", example = "1001") + private Long parentId; + + @Schema(description = "自定义序号", example = "一") + private String customCode; + + @Schema(description = "名称", example = "统一取费单价1") + private String name; + + @Schema(description = "计算基数") + private Map calcBase; + + @Schema(description = "费率代号(%)", example = "A") + private String rateCode; + + @Schema(description = "代号", example = "TYTQDJ001") + private String code; + + @Schema(description = "默认费用归属", example = "间接费") + private String feeCategory; + + @Schema(description = "基数说明", example = "按除税基价计算") + private String baseDescription; + + @Schema(description = "是否隐藏", example = "false") + private Boolean hidden; + + @Schema(description = "是否费用变量", example = "false") + private Boolean variable; + + @Schema(description = "节点类型", example = "parent") + private String nodeType; + + @Schema(description = "扩展属性") + private Map attributes; + + @Schema(description = "排序字段", example = "1") + private Integer sortOrder; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; + + @Schema(description = "子节点列表") + private List children; + + @Schema(description = "是否有子节点") + private Boolean hasChildren; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaUnifiedFeeSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaUnifiedFeeSaveReqVO.java new file mode 100644 index 0000000..7554c4f --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaUnifiedFeeSaveReqVO.java @@ -0,0 +1,60 @@ +package com.yhy.module.core.controller.admin.quota.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Map; +import javax.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - 统一取费单价创建/更新 Request VO") +@Data +public class QuotaUnifiedFeeSaveReqVO { + + @Schema(description = "主键ID(更新时必填)", example = "1") + private Long id; + + @Schema(description = "关联的定额取费项ID(用于统一取费单价更新)", example = "1001") + private Long feeItemId; + + @Schema(description = "模式节点ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1002") + @NotNull(message = "模式节点ID不能为空") + private Long catalogItemId; + + @Schema(description = "父节点ID", example = "1001") + private Long parentId; + + @Schema(description = "自定义序号", example = "一") + private String customCode; + + @Schema(description = "名称", example = "统一取费单价1") + private String name; + + @Schema(description = "计算基数") + private Map calcBase; + + @Schema(description = "费率代号(%)", example = "A") + private String rateCode; + + @Schema(description = "代号", example = "TYTQDJ001") + private String code; + + @Schema(description = "默认费用归属", example = "间接费") + private String feeCategory; + + @Schema(description = "基数说明", example = "按除税基价计算") + private String baseDescription; + + @Schema(description = "是否隐藏", example = "false") + private Boolean hidden; + + @Schema(description = "是否费用变量", example = "false") + private Boolean variable; + + @Schema(description = "节点类型", example = "parent") + private String nodeType; + + @Schema(description = "扩展属性") + private Map attributes; + + @Schema(description = "排序字段", example = "1") + private Integer sortOrder; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaUnifiedFeeSettingRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaUnifiedFeeSettingRespVO.java new file mode 100644 index 0000000..0da61ce --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaUnifiedFeeSettingRespVO.java @@ -0,0 +1,70 @@ +package com.yhy.module.core.controller.admin.quota.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import lombok.Data; + +@Schema(description = "管理后台 - 统一取费设置响应 VO") +@Data +public class QuotaUnifiedFeeSettingRespVO { + + @Schema(description = "主键ID", example = "1") + private Long id; + + @Schema(description = "模式节点ID", example = "1002") + private Long catalogItemId; + + @Schema(description = "父节点ID", example = "1001") + private Long parentId; + + @Schema(description = "自定义序号", example = "一") + private String customCode; + + @Schema(description = "编号", example = "TYTQ001") + private String code; + + @Schema(description = "名称", example = "统一取费项目1") + private String name; + + @Schema(description = "取费章节", example = "第一章") + private String feeChapter; + + @Schema(description = "默认费用归属", example = "间接费") + private String feeCategory; + + @Schema(description = "本清单比例%", example = "100.00") + private BigDecimal thisListPercentage; + + @Schema(description = "指定清单比例%", example = "50.00") + private BigDecimal specifiedListPercentage; + + @Schema(description = "指定清单编码", example = "QD001") + private String specifiedListCode; + + @Schema(description = "节点类型", example = "parent") + private String nodeType; + + @Schema(description = "单位", example = "m³") + private String unit; + + @Schema(description = "扩展属性") + private Map attributes; + + @Schema(description = "排序字段", example = "1") + private Integer sortOrder; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; + + @Schema(description = "子节点列表") + private List children; + + @Schema(description = "是否有子节点") + private Boolean hasChildren; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaUnifiedFeeSettingSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaUnifiedFeeSettingSaveReqVO.java new file mode 100644 index 0000000..403c24a --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaUnifiedFeeSettingSaveReqVO.java @@ -0,0 +1,60 @@ +package com.yhy.module.core.controller.admin.quota.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.util.Map; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - 统一取费设置创建/更新 Request VO") +@Data +public class QuotaUnifiedFeeSettingSaveReqVO { + + @Schema(description = "主键ID(更新时必填)", example = "1") + private Long id; + + @Schema(description = "模式节点ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1002") + @NotNull(message = "模式节点ID不能为空") + private Long catalogItemId; + + @Schema(description = "父节点ID", example = "1001") + private Long parentId; + + @Schema(description = "自定义序号", example = "一") + private String customCode; + + @Schema(description = "编号", example = "TYTQ001") + private String code; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "统一取费项目1") + @NotBlank(message = "名称不能为空") + private String name; + + @Schema(description = "取费章节", example = "第一章") + private String feeChapter; + + @Schema(description = "默认费用归属", example = "间接费") + private String feeCategory; + + @Schema(description = "本清单比例%", example = "100.00") + private BigDecimal thisListPercentage; + + @Schema(description = "指定清单比例%", example = "50.00") + private BigDecimal specifiedListPercentage; + + @Schema(description = "指定清单编码", example = "QD001") + private String specifiedListCode; + + @Schema(description = "节点类型", example = "parent") + private String nodeType; + + @Schema(description = "单位", example = "m³") + private String unit; + + @Schema(description = "扩展属性") + private Map attributes; + + @Schema(description = "排序字段", example = "1") + private Integer sortOrder; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaVariableSettingRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaVariableSettingRespVO.java new file mode 100644 index 0000000..f6cf5e7 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaVariableSettingRespVO.java @@ -0,0 +1,47 @@ +package com.yhy.module.core.controller.admin.quota.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import java.util.Map; +import lombok.Data; + +@Schema(description = "管理后台 - 单位工程变量设置 Response VO") +@Data +public class QuotaVariableSettingRespVO { + + @Schema(description = "主键ID", example = "1") + private Long id; + + @Schema(description = "租户ID", example = "1") + private Long tenantId; + + @Schema(description = "费率模式节点ID", example = "1002") + private Long catalogItemId; + + @Schema(description = "类别:division-分部分项/measure-措施项目/other-其他项目/unit_summary-单位汇总", example = "division") + private String category; + + @Schema(description = "费用名称", example = "人工费") + private String name; + + @Schema(description = "费用代号", example = "RGF") + private String code; + + @Schema(description = "计算基数") + private Map calcBase; + + @Schema(description = "排序字段", example = "1") + private Integer sortOrder; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; + + @Schema(description = "数据来源:manual-手动配置,fee_item-定额取费引用", example = "manual") + private String source; + + @Schema(description = "汇总值(后端计算)", example = "12345.67") + private java.math.BigDecimal summaryValue; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaVariableSettingSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaVariableSettingSaveReqVO.java new file mode 100644 index 0000000..85bed8f --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/quota/vo/QuotaVariableSettingSaveReqVO.java @@ -0,0 +1,41 @@ +package com.yhy.module.core.controller.admin.quota.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Map; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - 单位工程变量设置创建/更新 Request VO") +@Data +public class QuotaVariableSettingSaveReqVO { + + @Schema(description = "主键ID(更新时必填)", example = "1") + private Long id; + + @Schema(description = "费率模式节点ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1002") + @NotNull(message = "费率模式节点ID不能为空") + private Long catalogItemId; + + @Schema(description = "类别:division-分部分项/measure-措施项目/other-其他项目/unit_summary-单位汇总", requiredMode = Schema.RequiredMode.REQUIRED, example = "division") + @NotBlank(message = "类别不能为空") + private String category; + + @Schema(description = "费用名称", example = "人工费") + private String name; + + @Schema(description = "费用代号", example = "RGF") + private String code; + + @Schema(description = "计算基数", example = "{\"formula\":\"DRGF+CLF\",\"variables\":{\"DRGF\":{\"categoryId\":1,\"priceField\":\"tax_excl_base_price\"}}}") + private Map calcBase; + + @Schema(description = "排序字段", example = "1") + private Integer sortOrder; + + @Schema(description = "参考节点ID(用于指定插入位置)") + private Long referenceNodeId; + + @Schema(description = "插入位置:above-在参考节点上方,below-在参考节点下方") + private String insertPosition; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/ResourceCatalogItemController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/ResourceCatalogItemController.java index b13cffa..34d7782 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/ResourceCatalogItemController.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/ResourceCatalogItemController.java @@ -3,10 +3,10 @@ package com.yhy.module.core.controller.admin.resource; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import com.yhy.module.core.controller.admin.resource.vo.CatalogItemSaveReqVO; -import com.yhy.module.core.controller.admin.resource.vo.ResourceCatalogItemTreeNodeVO; -import com.yhy.module.core.controller.admin.resource.vo.ResourceCategorySimpleRespVO; -import com.yhy.module.core.controller.admin.resource.vo.SwapSortOrderReqVO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceBookRespVO; +import com.yhy.module.core.controller.admin.resource.vo.*; import com.yhy.module.core.dal.dataobject.resource.ResourceCatalogItemDO; import com.yhy.module.core.service.resource.ResourceCatalogItemService; import com.yhy.module.core.service.resource.ResourceCategoryTreeService; @@ -49,6 +49,18 @@ public class ResourceCatalogItemController { return success(itemService.listByCatalog(catalogId)); } + @GetMapping("/list-level") + @Operation(summary = "获取指定层级的分类/条目列表", description = "查询指定path长度的所有节点,默认为2(第二层),如path={1,10}") + public CommonResult> listByLevel( + @io.swagger.v3.oas.annotations.Parameter(description = "层级深度,默认为2") + @org.springframework.web.bind.annotation.RequestParam(value = "pathLength", required = false, defaultValue = "2") Integer pathLength) { + // 忽略租户过滤,查询所有租户数据,然后在 Service 层排除租户1 + List result = TenantUtils.executeIgnore(() -> + itemService.listByPathLevel(pathLength) + ); + return success(result); + } + @GetMapping("/{id}/allowed-categories") @Operation(summary = "获取目录树节点允许的类别列表", description = "用于创建工料机项时,限制类别选择范围") public CommonResult> getAllowedCategories(@PathVariable("id") Long id) { @@ -81,4 +93,12 @@ public class ResourceCatalogItemController { itemService.swapSortOrder(reqVO.getNodeId1(), reqVO.getNodeId2()); return success(true); } + + @PutMapping("/drag") + @Operation(summary = "拖动节点到指定位置") + public CommonResult dragNode(@Valid @RequestBody DragNodeReqVO reqVO) { + itemService.dragNode(reqVO.getDragNodeId(), reqVO.getTargetNodeId(), reqVO.getPosition()); + return success(true); + } + } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/ResourceCategoryTreeController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/ResourceCategoryTreeController.java index 535cf65..a19a836 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/ResourceCategoryTreeController.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/ResourceCategoryTreeController.java @@ -1,6 +1,7 @@ package com.yhy.module.core.controller.admin.resource; import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.resource.vo.DragNodeReqVO; import com.yhy.module.core.controller.admin.resource.vo.ResourceCategorySimpleRespVO; import com.yhy.module.core.controller.admin.resource.vo.ResourceCategoryTreeMappingSaveReqVO; import com.yhy.module.core.controller.admin.resource.vo.ResourceCategoryTreeNodeVO; @@ -86,4 +87,11 @@ public class ResourceCategoryTreeController { categoryTreeService.swapSortOrder(reqVO.getNodeId1(), reqVO.getNodeId2()); return success(true); } + + @PutMapping("/drag") + @Operation(summary = "拖动节点到指定位置") + public CommonResult dragNode(@Valid @RequestBody DragNodeReqVO reqVO) { + categoryTreeService.dragNode(reqVO.getDragNodeId(), reqVO.getTargetNodeId(), reqVO.getPosition()); + return success(true); + } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/ResourceInfoPriceMappingController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/ResourceInfoPriceMappingController.java new file mode 100644 index 0000000..84abfef --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/ResourceInfoPriceMappingController.java @@ -0,0 +1,353 @@ +package com.yhy.module.core.controller.admin.resource; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.dal.dataobject.resource.ResourceInfoPriceMappingDO; +import com.yhy.module.core.dal.mysql.resource.ResourceInfoPriceMappingMapper; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +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 java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 工料机-信息价关联") +@RestController +@RequestMapping("/core/resource/info-price-mapping") +@Validated +public class ResourceInfoPriceMappingController { + + @Resource + private ResourceInfoPriceMappingMapper mappingMapper; + + @Resource + private com.yhy.module.core.dal.mysql.infoprice.InfoPriceResourcePriceMapper infoPriceResourcePriceMapper; + + @PostMapping("/create") + @Operation(summary = "创建工料机-信息价关联") + @Parameters({ + @Parameter(name = "resourceItemId", description = "工料机ID", required = true), + @Parameter(name = "infoPriceResourceId", description = "信息价工料机ID", required = true), + @Parameter(name = "projectId", description = "项目ID(价格来源影响项目级别)"), + @Parameter(name = "formula", description = "价格计算公式"), + @Parameter(name = "selectedPriceId", description = "选中的价格历史ID"), + @Parameter(name = "isCurrent", description = "是否为当前价格来源") + }) + @PreAuthorize("@ss.hasPermission('core:resource:update')") + public CommonResult createMapping(@RequestParam("resourceItemId") Long resourceItemId, + @RequestParam("infoPriceResourceId") Long infoPriceResourceId, + @RequestParam(value = "projectId", required = false) Long projectId, + @RequestParam(value = "formula", required = false) String formula, + @RequestParam(value = "selectedPriceId", required = false) Long selectedPriceId, + @RequestParam(value = "isCurrent", required = false) Integer isCurrent) { + // 检查是否已存在(按 projectId + resourceItemId 检查) + if (projectId != null) { + List existing = mappingMapper.selectListByProjectAndResourceItem(projectId, resourceItemId); + boolean exists = existing.stream().anyMatch(m -> m.getInfoPriceResourceId().equals(infoPriceResourceId)); + if (exists) { + return success(0L); // 已存在,返回0表示未新增 + } + } else if (mappingMapper.existsByResourceAndInfoPrice(resourceItemId, infoPriceResourceId)) { + return success(0L); // 已存在,返回0表示未新增 + } + + // 先物理删除已软删除的记录(避免唯一约束冲突) + mappingMapper.physicalDeleteByResourceAndInfoPrice(resourceItemId, infoPriceResourceId); + + // 公式逻辑(项目级):如果公式为空,查询同租户下该工料机+信息价组合最后修改的公式 + String finalFormula; + if (formula == null || formula.trim().isEmpty()) { + String lastFormula = mappingMapper.selectLastFormulaByResourceAndInfoPrice(resourceItemId, infoPriceResourceId); + finalFormula = (lastFormula != null && !lastFormula.trim().isEmpty()) ? lastFormula : "xxj"; + } else { + finalFormula = formula; + } + + ResourceInfoPriceMappingDO mapping = ResourceInfoPriceMappingDO.builder() + .resourceItemId(resourceItemId) + .infoPriceResourceId(infoPriceResourceId) + .projectId(projectId) + .formula(finalFormula) + .selectedPriceId(selectedPriceId) + .isCurrent(isCurrent != null ? isCurrent : 0) + .build(); + mappingMapper.insert(mapping); + return success(mapping.getId()); + } + + @PutMapping("/set-current") + @Operation(summary = "设置当前价格来源") + @Parameters({ + @Parameter(name = "resourceItemId", description = "工料机ID", required = true), + @Parameter(name = "projectId", description = "项目ID(价格来源影响项目级别)"), + @Parameter(name = "infoPriceResourceId", description = "信息价工料机ID", required = true), + @Parameter(name = "selectedPriceId", description = "选中的价格历史ID", required = true) + }) + @PreAuthorize("@ss.hasPermission('core:resource:update')") + public CommonResult setCurrentPrice(@RequestParam("resourceItemId") Long resourceItemId, + @RequestParam(value = "projectId", required = false) Long projectId, + @RequestParam("infoPriceResourceId") Long infoPriceResourceId, + @RequestParam("selectedPriceId") Long selectedPriceId) { + // 优先使用 projectId + if (projectId != null) { + // 1. 先将该项目下该工料机的所有关联的 isCurrent 设为 0 + mappingMapper.clearCurrentByProjectAndResourceItem(projectId, resourceItemId); + // 2. 设置指定关联为当前价 + mappingMapper.setCurrentPriceByProject(projectId, resourceItemId, infoPriceResourceId, selectedPriceId); + } else { + // 兼容旧逻辑(租户级别) + mappingMapper.clearCurrentByResourceItemId(resourceItemId); + mappingMapper.setCurrentPrice(resourceItemId, infoPriceResourceId, selectedPriceId); + } + + return success(true); + } + + @PutMapping("/update-formula") + @Operation(summary = "更新公式") + @Parameters({ + @Parameter(name = "resourceItemId", description = "工料机ID", required = true), + @Parameter(name = "infoPriceResourceId", description = "信息价工料机ID", required = true), + @Parameter(name = "projectId", description = "项目ID(价格来源影响项目级别)"), + @Parameter(name = "formula", description = "价格计算公式", required = true) + }) + @PreAuthorize("@ss.hasPermission('core:resource:update')") + public CommonResult updateFormula(@RequestParam("resourceItemId") Long resourceItemId, + @RequestParam("infoPriceResourceId") Long infoPriceResourceId, + @RequestParam(value = "projectId", required = false) Long projectId, + @RequestParam("formula") String formula) { + // 公式不允许为空,默认为 xxj + String finalFormula = (formula == null || formula.trim().isEmpty()) ? "xxj" : formula; + + if (projectId != null) { + mappingMapper.updateFormulaByProject(resourceItemId, infoPriceResourceId, projectId, finalFormula); + } else { + mappingMapper.updateFormula(resourceItemId, infoPriceResourceId, finalFormula); + } + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除工料机-信息价关联") + @Parameters({ + @Parameter(name = "resourceItemId", description = "工料机ID", required = true), + @Parameter(name = "infoPriceResourceId", description = "信息价工料机ID", required = true) + }) + @PreAuthorize("@ss.hasPermission('core:resource:update')") + public CommonResult deleteMapping(@RequestParam("resourceItemId") Long resourceItemId, + @RequestParam("infoPriceResourceId") Long infoPriceResourceId) { + mappingMapper.delete(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(ResourceInfoPriceMappingDO::getResourceItemId, resourceItemId) + .eq(ResourceInfoPriceMappingDO::getInfoPriceResourceId, infoPriceResourceId)); + return success(true); + } + + @GetMapping("/list") + @Operation(summary = "获取工料机关联的信息价列表") + @Parameters({ + @Parameter(name = "resourceItemId", description = "工料机ID", required = true), + @Parameter(name = "projectId", description = "项目ID(价格来源影响项目级别)") + }) + @PreAuthorize("@ss.hasPermission('core:resource:query')") + public CommonResult> getListByResourceItemId( + @RequestParam("resourceItemId") Long resourceItemId, + @RequestParam(value = "projectId", required = false) Long projectId) { + // 优先使用 projectId 查询 + List list; + if (projectId != null) { + list = mappingMapper.selectListByProjectAndResourceItem(projectId, resourceItemId); + } else { + list = mappingMapper.selectListByResourceItemId(resourceItemId); + } + return success(list); + } + + @GetMapping("/list-with-detail") + @Operation(summary = "获取工料机关联的信息价列表(含详情)") + @Parameters({ + @Parameter(name = "resourceItemId", description = "工料机ID", required = true), + @Parameter(name = "projectId", description = "项目ID(价格来源影响项目级别)") + }) + @PreAuthorize("@ss.hasPermission('core:resource:query')") + public CommonResult>> getListWithDetailByResourceItemId( + @RequestParam("resourceItemId") Long resourceItemId, + @RequestParam(value = "projectId", required = false) Long projectId) { + // 优先使用 projectId 查询 + List mappings; + if (projectId != null) { + mappings = mappingMapper.selectListByProjectAndResourceItem(projectId, resourceItemId); + } else { + mappings = mappingMapper.selectListByResourceItemId(resourceItemId); + } + if (mappings.isEmpty()) { + return success(java.util.Collections.emptyList()); + } + + // 获取信息价工料机详情 + List infoPriceResourceIds = mappings.stream() + .map(ResourceInfoPriceMappingDO::getInfoPriceResourceId) + .collect(java.util.stream.Collectors.toList()); + + List resources = + infoPriceResourceMapper.selectBatchIds(infoPriceResourceIds); + + java.util.Map resourceMap = + resources.stream().collect(java.util.stream.Collectors.toMap( + com.yhy.module.core.dal.dataobject.infoprice.InfoPriceResourceDO::getId, + r -> r)); + + // 获取工料机基础信息(名称、规格、单位) + List sourceResourceItemIds = resources.stream() + .map(com.yhy.module.core.dal.dataobject.infoprice.InfoPriceResourceDO::getSourceResourceItemId) + .filter(java.util.Objects::nonNull) + .distinct() + .collect(java.util.stream.Collectors.toList()); + + java.util.Map resourceItemMap = new java.util.HashMap<>(); + if (!sourceResourceItemIds.isEmpty()) { + List resourceItems = + resourceItemMapper.selectBatchIds(sourceResourceItemIds); + resourceItemMap = resourceItems.stream().collect(java.util.stream.Collectors.toMap( + com.yhy.module.core.dal.dataobject.resource.ResourceItemDO::getId, + r -> r)); + } + + // 组装结果 + List> result = new java.util.ArrayList<>(); + for (ResourceInfoPriceMappingDO mapping : mappings) { + java.util.Map item = new java.util.HashMap<>(); + item.put("id", mapping.getId()); + item.put("resourceItemId", mapping.getResourceItemId()); + item.put("infoPriceResourceId", mapping.getInfoPriceResourceId()); + item.put("formula", mapping.getFormula()); + item.put("selectedPriceId", mapping.getSelectedPriceId()); + item.put("isCurrent", mapping.getIsCurrent()); + + com.yhy.module.core.dal.dataobject.infoprice.InfoPriceResourceDO resource = resourceMap.get(mapping.getInfoPriceResourceId()); + if (resource != null) { + item.put("code", resource.getCode()); + + // 优先使用selectedPriceId对应的价格历史记录的价格信息 + if (mapping.getSelectedPriceId() != null) { + com.yhy.module.core.dal.dataobject.infoprice.InfoPriceResourcePriceDO selectedPrice = + infoPriceResourcePriceMapper.selectById(mapping.getSelectedPriceId()); + if (selectedPrice != null) { + item.put("priceTaxExcl", selectedPrice.getPriceTaxExcl()); + item.put("taxRate", selectedPrice.getTaxRate()); + item.put("priceTaxIncl", selectedPrice.getPriceTaxIncl()); + item.put("priceStartTime", selectedPrice.getStartTime()); + item.put("priceEndTime", selectedPrice.getEndTime()); + } else { + // selectedPriceId对应的记录不存在,使用资源默认价格 + item.put("priceTaxExcl", resource.getPriceTaxExcl()); + item.put("taxRate", resource.getTaxRate()); + item.put("priceTaxIncl", resource.getPriceTaxIncl()); + } + } else { + // 没有selectedPriceId,使用资源默认价格 + item.put("priceTaxExcl", resource.getPriceTaxExcl()); + item.put("taxRate", resource.getTaxRate()); + item.put("priceTaxIncl", resource.getPriceTaxIncl()); + } + + // 从工料机基础表获取名称、规格、单位 + if (resource.getSourceResourceItemId() != null) { + com.yhy.module.core.dal.dataobject.resource.ResourceItemDO resourceItem = + resourceItemMap.get(resource.getSourceResourceItemId()); + if (resourceItem != null) { + item.put("name", resourceItem.getName()); + item.put("spec", resourceItem.getSpec()); + item.put("unit", resourceItem.getUnit()); + } + } + + // 获取信息价专业和地区 + if (resource.getCategoryTreeId() != null) { + com.yhy.module.core.dal.dataobject.infoprice.InfoPriceCategoryTreeDO categoryTree = + infoPriceCategoryTreeMapper.selectById(resource.getCategoryTreeId()); + if (categoryTree != null && categoryTree.getBookId() != null) { + com.yhy.module.core.dal.dataobject.infoprice.InfoPriceBookDO book = + infoPriceBookMapper.selectById(categoryTree.getBookId()); + if (book != null && book.getTreeNodeId() != null) { + com.yhy.module.core.dal.dataobject.infoprice.InfoPriceTreeDO treeNode = + infoPriceTreeMapper.selectById(book.getTreeNodeId()); + if (treeNode != null) { + // 信息价专业 + item.put("professionType", treeNode.getEnumType()); + // 完整地区路径 + item.put("fullRegion", buildFullRegionPath(treeNode)); + } + } + } + } + } + result.add(item); + } + return success(result); + } + + /** + * 构建完整地区路径 + */ + private String buildFullRegionPath(com.yhy.module.core.dal.dataobject.infoprice.InfoPriceTreeDO treeNode) { + if (treeNode == null) { + return ""; + } + String[] pathIds = treeNode.getPath(); + if (pathIds == null || pathIds.length == 0) { + return treeNode.getName(); + } + StringBuilder pathBuilder = new StringBuilder(); + for (String pathId : pathIds) { + try { + Long id = Long.parseLong(pathId); + com.yhy.module.core.dal.dataobject.infoprice.InfoPriceTreeDO pathNode = infoPriceTreeMapper.selectById(id); + if (pathNode != null && pathNode.getName() != null && !pathNode.getName().isEmpty()) { + if (pathBuilder.length() > 0) { + pathBuilder.append(" / "); + } + pathBuilder.append(pathNode.getName()); + } + } catch (NumberFormatException e) { + // 忽略无效的ID + } + } + if (pathBuilder.length() > 0) { + pathBuilder.append(" / "); + } + pathBuilder.append(treeNode.getName()); + return pathBuilder.toString(); + } + + @GetMapping("/tenant-associated-ids") + @Operation(summary = "获取租户级已关联的信息价ID列表(用于弹窗置顶)") + @Parameters({ + @Parameter(name = "resourceItemId", description = "工料机ID", required = true) + }) + @PreAuthorize("@ss.hasPermission('core:resource:query')") + public CommonResult> getTenantAssociatedIds( + @RequestParam("resourceItemId") Long resourceItemId) { + java.util.Set ids = mappingMapper.selectAssociatedInfoPriceIdsByResourceItem(resourceItemId); + return success(ids); + } + + @Resource + private com.yhy.module.core.dal.mysql.infoprice.InfoPriceResourceMapper infoPriceResourceMapper; + + @Resource + private com.yhy.module.core.dal.mysql.resource.ResourceItemMapper resourceItemMapper; + + @Resource + private com.yhy.module.core.dal.mysql.infoprice.InfoPriceCategoryTreeMapper infoPriceCategoryTreeMapper; + + @Resource + private com.yhy.module.core.dal.mysql.infoprice.InfoPriceBookMapper infoPriceBookMapper; + + @Resource + private com.yhy.module.core.dal.mysql.infoprice.InfoPriceTreeMapper infoPriceTreeMapper; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/ResourceItemController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/ResourceItemController.java index 072623b..711ecbc 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/ResourceItemController.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/ResourceItemController.java @@ -9,6 +9,7 @@ import com.yhy.module.core.controller.admin.resource.vo.ResourceItemRespVO; import com.yhy.module.core.controller.admin.resource.vo.ResourceItemSaveReqVO; import com.yhy.module.core.controller.admin.resource.vo.ResourceItemWithPricesRespVO; import com.yhy.module.core.controller.admin.resource.vo.ResourcePriceSaveReqVO; +import com.yhy.module.core.controller.admin.resource.vo.SwapSortOrderReqVO; import com.yhy.module.core.dal.dataobject.resource.ResourcePriceDO; import com.yhy.module.core.service.resource.ResourceItemService; import io.swagger.v3.oas.annotations.Operation; @@ -94,4 +95,17 @@ public class ResourceItemController { resourceItemService.deletePrice(priceId); return success(true); } + + @GetMapping("/by-code/{code}") + @Operation(summary = "根据编码查询工料机项") + public CommonResult getByCode(@PathVariable("code") String code) { + return success(resourceItemService.getItemByCode(code)); + } + + @PutMapping("/swap-sort") + @Operation(summary = "交换两个工料机项的排序") + public CommonResult swapSortOrder(@Valid @RequestBody SwapSortOrderReqVO reqVO) { + resourceItemService.swapSortOrder(reqVO.getNodeId1(), reqVO.getNodeId2()); + return success(true); + } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/vo/CatalogItemSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/vo/CatalogItemSaveReqVO.java index 8da1b50..8c70009 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/vo/CatalogItemSaveReqVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/vo/CatalogItemSaveReqVO.java @@ -37,6 +37,12 @@ public class CatalogItemSaveReqVO { @Schema(description = "排序号") private Integer sortOrder; + @Schema(description = "参考节点ID,用于指定插入位置") + private Long referenceNodeId; + + @Schema(description = "插入位置:above-在参考节点上方,below-在参考节点下方") + private String insertPosition; + @Schema(description = "扩展属性") private Map attributes; } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/vo/DragNodeReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/vo/DragNodeReqVO.java new file mode 100644 index 0000000..b0029ba --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/vo/DragNodeReqVO.java @@ -0,0 +1,24 @@ +package com.yhy.module.core.controller.admin.resource.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@Schema(description = "拖动节点请求") +@Data +public class DragNodeReqVO { + + @Schema(description = "被拖动的节点ID", required = true, example = "1") + @NotNull(message = "被拖动的节点ID不能为空") + private Long dragNodeId; + + @Schema(description = "目标节点ID", required = true, example = "2") + @NotNull(message = "目标节点ID不能为空") + private Long targetNodeId; + + @Schema(description = "位置:before(目标节点前), after(目标节点后)", required = true, example = "before") + @NotBlank(message = "位置不能为空") + private String position; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/vo/ResourceCategoryTreeSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/vo/ResourceCategoryTreeSaveReqVO.java index 9fd423b..97cf77f 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/vo/ResourceCategoryTreeSaveReqVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/vo/ResourceCategoryTreeSaveReqVO.java @@ -31,6 +31,12 @@ public class ResourceCategoryTreeSaveReqVO { @Schema(description = "排序", example = "1") private Integer sortOrder; + @Schema(description = "参考节点ID(用于上方/下方插入)", example = "1") + private Long referenceNodeId; + + @Schema(description = "插入位置:above(上方插入), below(下方插入)", example = "above") + private String insertPosition; + @Schema(description = "扩展属性") private Map attributes; } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/vo/ResourceItemRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/vo/ResourceItemRespVO.java index 44a5066..870e184 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/vo/ResourceItemRespVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/vo/ResourceItemRespVO.java @@ -71,4 +71,21 @@ public class ResourceItemRespVO { @Schema(description = "工料机机类树节点名称") private String categoryTreeName; + + @Schema(description = "排序字段") + private Integer sortOrder; + + // ========== 虚拟字段(复合工料机合价) ========== + + @Schema(description = "除税基价合价(虚拟字段,仅复合工料机)", example = "500.00") + private java.math.BigDecimal taxExclBaseTotalSum; + + @Schema(description = "含税基价合价(虚拟字段,仅复合工料机)", example = "545.00") + private java.math.BigDecimal taxInclBaseTotalSum; + + @Schema(description = "除税编制价合价(虚拟字段,仅复合工料机)", example = "550.00") + private java.math.BigDecimal taxExclCompileTotalSum; + + @Schema(description = "含税编制价合价(虚拟字段,仅复合工料机)", example = "599.50") + private java.math.BigDecimal taxInclCompileTotalSum; } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/vo/ResourceItemSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/vo/ResourceItemSaveReqVO.java index 2e81b00..bf89956 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/vo/ResourceItemSaveReqVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/vo/ResourceItemSaveReqVO.java @@ -3,7 +3,6 @@ package com.yhy.module.core.controller.admin.resource.vo; import io.swagger.v3.oas.annotations.media.Schema; import java.util.Map; import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; import lombok.Data; @Data @@ -12,8 +11,7 @@ public class ResourceItemSaveReqVO { @Schema(description = "资源项ID,创建为空,更新必填") private Long id; - @Schema(description = "库条目ID", requiredMode = Schema.RequiredMode.REQUIRED) - @NotNull + @Schema(description = "库条目ID(创建时必填,更新时可选)") private Long catalogItemId; @Schema(description = "工料机编码", requiredMode = Schema.RequiredMode.REQUIRED) @@ -27,8 +25,7 @@ public class ResourceItemSaveReqVO { @Schema(description = "型号规格") private String spec; - @Schema(description = "类别ID", requiredMode = Schema.RequiredMode.REQUIRED) - @NotNull + @Schema(description = "类别ID") private Long categoryId; @Schema(description = "类型 labor/material/machine", requiredMode = Schema.RequiredMode.REQUIRED) @@ -64,4 +61,13 @@ public class ResourceItemSaveReqVO { @Schema(description = "工料机机类树节点ID") private Long categoryTreeId; + + @Schema(description = "排序字段", example = "1") + private Integer sortOrder; + + @Schema(description = "参考节点ID(用于指定插入位置)") + private Long referenceNodeId; + + @Schema(description = "插入位置:above-在参考节点上方,below-在参考节点下方") + private String insertPosition; } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/vo/ResourceMergedRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/vo/ResourceMergedRespVO.java index 861af21..c649ca8 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/vo/ResourceMergedRespVO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/resource/vo/ResourceMergedRespVO.java @@ -1,5 +1,7 @@ package com.yhy.module.core.controller.admin.resource.vo; +import com.baomidou.mybatisplus.annotation.TableField; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; import io.swagger.v3.oas.annotations.media.Schema; import java.math.BigDecimal; import java.time.LocalDateTime; @@ -70,6 +72,7 @@ public class ResourceMergedRespVO { private BigDecimal taxInclCompilePrice; @Schema(description = "计算基数(来自resource_item)") + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) private Map calcBase; // ========== 虚拟字段(计算合价) ========== diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/unit/UnitRateSettingController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/unit/UnitRateSettingController.java new file mode 100644 index 0000000..2d3ec11 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/unit/UnitRateSettingController.java @@ -0,0 +1,77 @@ +package com.yhy.module.core.controller.admin.unit; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.*; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.unit.vo.UnitRateSettingRespVO; +import com.yhy.module.core.controller.admin.unit.vo.UnitRateSettingSaveReqVO; +import com.yhy.module.core.service.unit.UnitRateSettingService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import javax.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 工程费率设定 Controller + */ +@Tag(name = "管理后台 - 工程费率设定") +@RestController +@RequestMapping("/core/unit/rate-setting") +@Validated +@RequiredArgsConstructor +public class UnitRateSettingController { + + private final UnitRateSettingService unitRateSettingService; + + @PostMapping("/save") + @Operation(summary = "保存或更新费率设定", description = "保存设置值变更,后端计算字段值并返回") + public CommonResult saveOrUpdate(@Valid @RequestBody UnitRateSettingSaveReqVO saveReqVO) { + return success(unitRateSettingService.saveOrUpdate(saveReqVO)); + } + + @GetMapping("/list") + @Operation(summary = "获取单位工程的费率设定列表") + @Parameter(name = "unitId", description = "单位工程ID", required = true) + public CommonResult> getListByUnitId(@RequestParam("unitId") Long unitId) { + return success(unitRateSettingService.getListByUnitId(unitId)); + } + + @GetMapping("/get") + @Operation(summary = "获取单个费率设定") + @Parameter(name = "unitId", description = "单位工程ID", required = true) + @Parameter(name = "rateItemId", description = "费率项ID", required = true) + public CommonResult getByUnitIdAndRateItemId( + @RequestParam("unitId") Long unitId, + @RequestParam("rateItemId") Long rateItemId) { + return success(unitRateSettingService.getByUnitIdAndRateItemId(unitId, rateItemId)); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除费率设定") + @Parameter(name = "id", description = "费率设定ID", required = true) + public CommonResult delete(@RequestParam("id") Long id) { + unitRateSettingService.delete(id); + return success(true); + } + + @PostMapping("/initialize") + @Operation(summary = "初始化单位工程的费率设定", description = "创建快照,从定额费率模板复制初始值") + @Parameter(name = "unitId", description = "单位工程ID", required = true) + @Parameter(name = "catalogItemId", description = "费率模式节点ID", required = true) + public CommonResult initializeSettings( + @RequestParam("unitId") Long unitId, + @RequestParam("catalogItemId") Long catalogItemId) { + unitRateSettingService.initializeSettings(unitId, catalogItemId); + return success(true); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/unit/vo/UnitRateSettingRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/unit/vo/UnitRateSettingRespVO.java new file mode 100644 index 0000000..4f0dd36 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/unit/vo/UnitRateSettingRespVO.java @@ -0,0 +1,48 @@ +package com.yhy.module.core.controller.admin.unit.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Map; +import lombok.Data; + +/** + * 工程费率设定 响应 VO + */ +@Schema(description = "管理后台 - 工程费率设定响应") +@Data +public class UnitRateSettingRespVO { + + @Schema(description = "费率设定ID") + private Long id; + + @Schema(description = "单位工程ID") + private Long unitId; + + @Schema(description = "费率项ID(目录节点)") + private Long rateItemId; + + @Schema(description = "选中的值节点ID(下拉模式)") + private Long selectedValueId; + + @Schema(description = "输入值(填写模式)") + private BigDecimal inputValue; + + @Schema(description = "基线值(快照用)") + private BigDecimal baseValue; + + @Schema(description = "计算后的字段值") + private Map fieldValues; + + @Schema(description = "基线字段值(快照用)") + private Map baseFieldValues; + + @Schema(description = "设置名称") + private String settingName; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/unit/vo/UnitRateSettingSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/unit/vo/UnitRateSettingSaveReqVO.java new file mode 100644 index 0000000..6b9896c --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/unit/vo/UnitRateSettingSaveReqVO.java @@ -0,0 +1,38 @@ +package com.yhy.module.core.controller.admin.unit.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.util.Map; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 工程费率设定 保存请求 VO + */ +@Schema(description = "管理后台 - 工程费率设定保存请求") +@Data +public class UnitRateSettingSaveReqVO { + + @Schema(description = "费率设定ID(更新时必填)") + private Long id; + + @Schema(description = "单位工程ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "单位工程ID不能为空") + private Long unitId; + + @Schema(description = "费率项ID(目录节点)", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "费率项ID不能为空") + private Long rateItemId; + + @Schema(description = "选中的值节点ID(下拉模式)") + private Long selectedValueId; + + @Schema(description = "输入值(填写模式)") + private BigDecimal inputValue; + + @Schema(description = "设置名称(用于显示)") + private String settingName; + + @Schema(description = "字段值(虚拟字段用户修改值,null表示恢复计算值)") + private Map fieldValues; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/AuditModeController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/AuditModeController.java new file mode 100644 index 0000000..076078d --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/AuditModeController.java @@ -0,0 +1,95 @@ +package com.yhy.module.core.controller.admin.workbench; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.workbench.vo.audit.AuditApproveDivisionUpdateReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.audit.AuditDivisionTreeRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.audit.AuditModeCreateReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.audit.AuditModeRespVO; +import com.yhy.module.core.service.workbench.AuditModeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import javax.annotation.Resource; +import javax.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 审核模式 Controller + * + * @author yhy + */ +@Tag(name = "工作台 - 审核模式") +@RestController +@RequestMapping("/core/wb/audit-mode") +@Validated +public class AuditModeController { + + @Resource + private AuditModeService auditModeService; + + @PostMapping("/create") + @Operation(summary = "创建审核模式(复制审定快照)") + @PreAuthorize("@ss.hasPermission('core:wb-audit:create')") + public CommonResult createAuditMode(@Valid @RequestBody AuditModeCreateReqVO createReqVO) { + return success(auditModeService.createAuditMode(createReqVO)); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除审核模式") + @Parameter(name = "id", description = "审核模式ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-audit:delete')") + public CommonResult deleteAuditMode(@RequestParam("id") Long id) { + auditModeService.deleteAuditMode(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获取审核模式详情") + @Parameter(name = "id", description = "审核模式ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-audit:query')") + public CommonResult getAuditMode(@RequestParam("id") Long id) { + return success(auditModeService.getAuditMode(id)); + } + + @GetMapping("/list") + @Operation(summary = "获取项目下的审核模式列表") + @Parameter(name = "projectId", description = "项目ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-audit:query')") + public CommonResult> getAuditModeList(@RequestParam("projectId") Long projectId) { + return success(auditModeService.getAuditModeList(projectId)); + } + + @PutMapping("/update-approve") + @Operation(summary = "更新审定数据") + @PreAuthorize("@ss.hasPermission('core:wb-audit:update')") + public CommonResult updateApproveData(@Valid @RequestBody AuditApproveDivisionUpdateReqVO updateReqVO) { + auditModeService.updateApproveData(updateReqVO); + return success(true); + } + + @GetMapping("/division-tree") + @Operation(summary = "获取分部分项树(包含送审、审定、差异)") + @Parameters({ + @Parameter(name = "auditModeId", description = "审核模式ID", required = true, example = "1"), + @Parameter(name = "compileTreeId", description = "编制模式树的单位工程节点ID", required = true, example = "1") + }) + @PreAuthorize("@ss.hasPermission('core:wb-audit:query')") + public CommonResult> getDivisionTree( + @RequestParam("auditModeId") Long auditModeId, + @RequestParam("compileTreeId") Long compileTreeId) { + return success(auditModeService.getDivisionTree(auditModeId, compileTreeId)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/ProgressDivisionController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/ProgressDivisionController.java new file mode 100644 index 0000000..de76ac0 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/ProgressDivisionController.java @@ -0,0 +1,105 @@ +package com.yhy.module.core.controller.admin.workbench; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.workbench.vo.progresspayment.ProgressDivisionBatchSaveReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.progresspayment.ProgressDivisionSaveReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.progresspayment.ProgressDivisionTreeRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.progresspayment.ProgressPaymentModeWithDivisionsRespVO; +import com.yhy.module.core.dal.dataobject.workbench.ProgressDivisionDO; +import com.yhy.module.core.service.workbench.ProgressDivisionService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import javax.annotation.Resource; +import javax.validation.Valid; +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.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 进度款-清单关联 Controller + * + * @author yhy + */ +@Tag(name = "工作台 - 进度款-清单关联") +@RestController +@RequestMapping("/core/wb/progress-division") +@Validated +public class ProgressDivisionController { + + @Resource + private ProgressDivisionService progressDivisionService; + + @PostMapping("/save") + @Operation(summary = "保存或更新期数值") + @PreAuthorize("@ss.hasPermission('core:wb-progress-payment:update')") + public CommonResult save(@Valid @RequestBody ProgressDivisionSaveReqVO saveReqVO) { + return success(progressDivisionService.saveOrUpdate( + saveReqVO.getProgressPaymentModeId(), + saveReqVO.getBoqDivisionId(), + saveReqVO.getPeriodValue())); + } + + @PostMapping("/batch-save") + @Operation(summary = "批量保存或更新期数值") + @PreAuthorize("@ss.hasPermission('core:wb-progress-payment:update')") + public CommonResult batchSave(@Valid @RequestBody ProgressDivisionBatchSaveReqVO batchSaveReqVO) { + progressDivisionService.batchSaveOrUpdate( + batchSaveReqVO.getProgressPaymentModeId(), + batchSaveReqVO.getPeriodValueMap()); + return success(true); + } + + @GetMapping("/list") + @Operation(summary = "根据进度款模式ID获取关联列表") + @Parameter(name = "progressPaymentModeId", description = "进度款模式ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-progress-payment:query')") + public CommonResult> getList(@RequestParam("progressPaymentModeId") Long progressPaymentModeId) { + return success(progressDivisionService.getListByProgressPaymentModeId(progressPaymentModeId)); + } + + @GetMapping("/map") + @Operation(summary = "根据进度款模式ID获取清单ID到期数值的映射") + @Parameter(name = "progressPaymentModeId", description = "进度款模式ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-progress-payment:query')") + public CommonResult> getMap(@RequestParam("progressPaymentModeId") Long progressPaymentModeId) { + return success(progressDivisionService.getPeriodValueMap(progressPaymentModeId)); + } + + @PostMapping("/delete-by-payment-mode") + @Operation(summary = "根据进度款模式ID删除所有关联") + @Parameter(name = "progressPaymentModeId", description = "进度款模式ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-progress-payment:delete')") + public CommonResult deleteByPaymentMode(@RequestParam("progressPaymentModeId") Long progressPaymentModeId) { + progressDivisionService.deleteByProgressPaymentModeId(progressPaymentModeId); + return success(true); + } + + @GetMapping("/tree-with-period-info") + @Operation(summary = "获取分部分项树(含定额单价计算,并绑定进度款期数信息)") + @Parameter(name = "compileTreeId", description = "编制模式树的单位工程节点ID", required = true, example = "1") + @Parameter(name = "progressPaymentModeId", description = "进度款模式ID", example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-progress-payment:query')") + public CommonResult> getTreeWithPeriodInfo( + @RequestParam("compileTreeId") Long compileTreeId, + @RequestParam(value = "progressPaymentModeId", required = false) Long progressPaymentModeId) { + return success(progressDivisionService.getTreeWithPeriodInfo(compileTreeId, progressPaymentModeId)); + } + + @GetMapping("/mode-with-divisions-list") + @Operation(summary = "获取项目下的进度款模式列表(含关联清单)") + @Parameter(name = "projectId", description = "项目ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-progress-payment:query')") + public CommonResult> getModeWithDivisionsList( + @RequestParam("projectId") Long projectId) { + return success(progressDivisionService.getProgressPaymentModeWithDivisionsList(projectId)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/ProgressPaymentModeController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/ProgressPaymentModeController.java new file mode 100644 index 0000000..5ad3506 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/ProgressPaymentModeController.java @@ -0,0 +1,80 @@ +package com.yhy.module.core.controller.admin.workbench; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.workbench.vo.progresspayment.ProgressPaymentModeCreateReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.progresspayment.ProgressPaymentModeRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.progresspayment.ProgressPaymentModeUpdateReqVO; +import com.yhy.module.core.service.workbench.ProgressPaymentModeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import javax.annotation.Resource; +import javax.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 进度款模式 Controller + * + * @author yhy + */ +@Tag(name = "工作台 - 进度款模式") +@RestController +@RequestMapping("/core/wb/progress-payment-mode") +@Validated +public class ProgressPaymentModeController { + + @Resource + private ProgressPaymentModeService progressPaymentModeService; + + @PostMapping("/create") + @Operation(summary = "创建进度款模式") + @PreAuthorize("@ss.hasPermission('core:wb-progress-payment:create')") + public CommonResult createProgressPaymentMode(@Valid @RequestBody ProgressPaymentModeCreateReqVO createReqVO) { + return success(progressPaymentModeService.createProgressPaymentMode(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新进度款模式") + @PreAuthorize("@ss.hasPermission('core:wb-progress-payment:update')") + public CommonResult updateProgressPaymentMode(@Valid @RequestBody ProgressPaymentModeUpdateReqVO updateReqVO) { + progressPaymentModeService.updateProgressPaymentMode(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除进度款模式") + @Parameter(name = "id", description = "进度款模式ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-progress-payment:delete')") + public CommonResult deleteProgressPaymentMode(@RequestParam("id") Long id) { + progressPaymentModeService.deleteProgressPaymentMode(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获取进度款模式详情") + @Parameter(name = "id", description = "进度款模式ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-progress-payment:query')") + public CommonResult getProgressPaymentMode(@RequestParam("id") Long id) { + return success(progressPaymentModeService.getProgressPaymentMode(id)); + } + + @GetMapping("/list") + @Operation(summary = "获取项目下的进度款模式列表") + @Parameter(name = "projectId", description = "项目ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-progress-payment:query')") + public CommonResult> getProgressPaymentModeList(@RequestParam("projectId") Long projectId) { + return success(progressPaymentModeService.getProgressPaymentModeList(projectId)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/SyncLibraryController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/SyncLibraryController.java new file mode 100644 index 0000000..eb80ed5 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/SyncLibraryController.java @@ -0,0 +1,102 @@ +package com.yhy.module.core.controller.admin.workbench; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.workbench.vo.sync.ApplySyncReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.sync.SetSyncReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.sync.SyncLibraryDivisionRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.sync.SyncLibraryDivisionUpdateReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.sync.SyncPendingRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.sync.SyncSourceUnitRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.sync.UnsetSyncReqVO; +import com.yhy.module.core.service.workbench.SyncLibraryService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import javax.annotation.Resource; +import javax.validation.Valid; +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.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 同步库 Controller + * + * @author yhy + */ +@Tag(name = "工作台 - 同步库") +@RestController +@RequestMapping("/core/wb/sync-library") +@Validated +public class SyncLibraryController { + + @Resource + private SyncLibraryService syncLibraryService; + + @GetMapping("/tree") + @Operation(summary = "获取项目同步库的分部分项树") + @Parameter(name = "projectId", description = "项目ID", required = true) + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:query')") + public CommonResult> getTree( + @RequestParam("projectId") Long projectId) { + return success(syncLibraryService.getTree(projectId)); + } + + @GetMapping("/source-units") + @Operation(summary = "获取同步库的来源单位工程列表") + @Parameter(name = "projectId", description = "项目ID", required = true) + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:query')") + public CommonResult> getSourceUnits( + @RequestParam("projectId") Long projectId) { + return success(syncLibraryService.getSourceUnits(projectId)); + } + + @PutMapping("/update-division") + @Operation(summary = "更新同步库分部分项节点") + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:update')") + public CommonResult updateDivision(@Valid @RequestBody SyncLibraryDivisionUpdateReqVO reqVO) { + syncLibraryService.updateDivision(reqVO); + return success(true); + } + + @PostMapping("/set-sync") + @Operation(summary = "设为同步") + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:update')") + public CommonResult setSync(@Valid @RequestBody SetSyncReqVO reqVO) { + syncLibraryService.setSync(reqVO); + return success(true); + } + + @PostMapping("/unset-sync") + @Operation(summary = "解除同步") + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:update')") + public CommonResult unsetSync(@Valid @RequestBody UnsetSyncReqVO reqVO) { + syncLibraryService.unsetSync(reqVO); + return success(true); + } + + @PostMapping("/apply-sync") + @Operation(summary = "套用同步") + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:update')") + public CommonResult applySync(@Valid @RequestBody ApplySyncReqVO reqVO) { + syncLibraryService.applySync(reqVO); + return success(true); + } + + @GetMapping("/check-pending") + @Operation(summary = "检查是否有待同步的变更") + @Parameter(name = "compileTreeId", description = "单位工程ID", required = true) + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:query')") + public CommonResult> checkPending( + @RequestParam("compileTreeId") Long compileTreeId) { + return success(syncLibraryService.checkPending(compileTreeId)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbAdjustmentSettingController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbAdjustmentSettingController.java new file mode 100644 index 0000000..c0612fc --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbAdjustmentSettingController.java @@ -0,0 +1,104 @@ +package com.yhy.module.core.controller.admin.workbench; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.*; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.service.workbench.WbAdjustmentSettingService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import javax.annotation.Resource; +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.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 工作台定额调整设置 Controller + * + * @author yhy + */ +@Tag(name = "工作台 - 定额调整设置") +@RestController +@RequestMapping("/core/wb/adjustment-setting") +@Validated +public class WbAdjustmentSettingController { + + @Resource + private WbAdjustmentSettingService wbAdjustmentSettingService; + + @GetMapping("/combined-list") + @Operation(summary = "获取调整设置与明细的组合列表") + @Parameter(name = "divisionId", description = "定额节点ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-adjustment-setting:query')") + public CommonResult>> getCombinedList(@RequestParam("divisionId") Long divisionId) { + return success(wbAdjustmentSettingService.getAdjustmentCombinedList(divisionId)); + } + + @PostMapping("/apply-adjustment") + @Operation(summary = "应用调整设置") + @PreAuthorize("@ss.hasPermission('core:wb-adjustment-setting:update')") + public CommonResult applyAdjustment( + @RequestParam("divisionId") Long divisionId, + @RequestParam("adjustmentSettingId") Long adjustmentSettingId, + @RequestParam("enabled") boolean enabled) { + wbAdjustmentSettingService.applyAdjustmentSetting(divisionId, adjustmentSettingId, enabled); + return success(true); + } + + @PostMapping("/apply-dynamic-adjustment") + @Operation(summary = "应用动态调整") + @PreAuthorize("@ss.hasPermission('core:wb-adjustment-setting:update')") + public CommonResult applyDynamicAdjustment(@RequestBody WbDynamicAdjustReqVO reqVO) { + wbAdjustmentSettingService.applyDynamicAdjustment( + reqVO.getDivisionId(), + reqVO.getAdjustmentSettingId(), + reqVO.getInputValues()); + return success(true); + } + + @PostMapping("/apply-dynamic-merge") + @Operation(summary = "应用动态合并定额") + @PreAuthorize("@ss.hasPermission('core:wb-adjustment-setting:update')") + public CommonResult applyDynamicMerge(@RequestBody WbDynamicAdjustReqVO reqVO) { + wbAdjustmentSettingService.applyDynamicMerge( + reqVO.getDivisionId(), + reqVO.getAdjustmentSettingId(), + reqVO.getInputValues()); + return success(true); + } + + @PostMapping("/update") + @Operation(summary = "更新调整设置") + @PreAuthorize("@ss.hasPermission('core:wb-adjustment-setting:update')") + public CommonResult updateAdjustmentSetting(@RequestBody WbUpdateAdjustmentSettingReqVO reqVO) { + wbAdjustmentSettingService.updateAdjustmentSetting(reqVO.getId(), reqVO.getAdjustmentContent()); + return success(true); + } + + /** + * 动态调整请求VO + */ + @lombok.Data + public static class WbDynamicAdjustReqVO { + private Long divisionId; + private Long adjustmentSettingId; + private Map inputValues; + } + + /** + * 更新调整设置请求VO + */ + @lombok.Data + public static class WbUpdateAdjustmentSettingReqVO { + private Long id; + private String adjustmentContent; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbBoqDivisionController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbBoqDivisionController.java new file mode 100644 index 0000000..92a2f06 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbBoqDivisionController.java @@ -0,0 +1,208 @@ +package com.yhy.module.core.controller.admin.workbench; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.yhy.module.core.controller.admin.workbench.vo.HistoryBoqListReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqDivisionRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqDivisionSaveReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqDivisionSwapSortReqVO; +import com.yhy.module.core.service.workbench.WbBoqDivisionService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import javax.annotation.Resource; +import javax.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 工作台分部分项树 Controller + * + * @author yhy + */ +@Tag(name = "工作台 - 分部分项树") +@RestController +@RequestMapping("/core/wb/boq-division") +@Validated +public class WbBoqDivisionController { + + @Resource + private WbBoqDivisionService wbBoqDivisionService; + + @Resource + private com.yhy.module.core.service.workbench.QuotaQtyFormulaService quotaQtyFormulaService; + + @PostMapping("/create") + @Operation(summary = "创建节点(分部、清单或定额)") + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:create')") + public CommonResult createNode(@Valid @RequestBody WbBoqDivisionSaveReqVO createReqVO) { + return success(wbBoqDivisionService.createNode(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新节点") + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:update')") + public CommonResult updateNode(@Valid @RequestBody WbBoqDivisionSaveReqVO updateReqVO) { + wbBoqDivisionService.updateNode(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除节点") + @Parameter(name = "id", description = "节点ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:delete')") + public CommonResult deleteNode(@RequestParam("id") Long id) { + wbBoqDivisionService.deleteNode(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获取节点详情") + @Parameter(name = "id", description = "节点ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:query')") + public CommonResult getNode(@RequestParam("id") Long id) { + return success(wbBoqDivisionService.getNode(id)); + } + + @GetMapping("/tree") + @Operation(summary = "获取分部分项树(可按标签页过滤)") + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:query')") + public CommonResult> getTree( + @RequestParam("compileTreeId") @Parameter(description = "编制模式树的单位工程节点ID", required = true) Long compileTreeId, + @RequestParam(value = "tabType", required = false) @Parameter(description = "标签页类型:division/measure/other/unit_summary,不传返回全量") String tabType, + @RequestParam(value = "excludeZeroQtyBoq", required = false, defaultValue = "false") @Parameter(description = "是否过滤工程量为0或空的清单节点") Boolean excludeZeroQtyBoq) { + if (tabType != null && !tabType.isEmpty()) { + return success(wbBoqDivisionService.getTreeByTab(compileTreeId, tabType, excludeZeroQtyBoq)); + } + return success(wbBoqDivisionService.getTree(compileTreeId, excludeZeroQtyBoq)); + } + + @GetMapping("/tree-for-unified-fee") + @Operation(summary = "获取统一取费范围选择树(专供统一取费弹窗使用)") + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:query')") + public CommonResult> getTreeForUnifiedFee( + @RequestParam("compileTreeId") @Parameter(description = "编制模式树的单位工程节点ID", required = true) Long compileTreeId, + @RequestParam(value = "tabType", required = false) @Parameter(description = "标签页类型:division/measure") String tabType, + @RequestParam(value = "feeChapter", required = false) @Parameter(description = "取费章节ID列表,逗号分隔") List feeChapter) { + return success(wbBoqDivisionService.getTreeForUnifiedFee(compileTreeId, tabType, feeChapter)); + } + + @GetMapping("/tree-with-price") + @Operation(summary = "获取分部分项树(含定额单价计算)") + @Parameter(name = "compileTreeId", description = "编制模式树的单位工程节点ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:query')") + public CommonResult> getTreeWithPrice(@RequestParam("compileTreeId") Long compileTreeId) { + return success(wbBoqDivisionService.getTreeWithPrice(compileTreeId)); + } + + @PutMapping("/swap-sort") + @Operation(summary = "交换排序") + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:update')") + public CommonResult swapSort(@Valid @RequestBody WbBoqDivisionSwapSortReqVO swapReqVO) { + wbBoqDivisionService.swapSort(swapReqVO); + return success(true); + } + + @GetMapping("/history-list") + @Operation(summary = "获取历史清单/分部列表(跨项目,分页)") + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:query')") + public CommonResult> getHistoryList(@Valid HistoryBoqListReqVO reqVO) { + return success(wbBoqDivisionService.getHistoryListPage(reqVO)); + } + + @PostMapping("/copy") + @Operation(summary = "复制清单到目标位置(含定额和工料机)") + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:create')") + public CommonResult copyBoq( + @RequestParam("sourceBoqId") @Parameter(description = "源清单节点ID") Long sourceBoqId, + @RequestParam("targetCompileTreeId") @Parameter(description = "目标单位工程节点ID") Long targetCompileTreeId, + @RequestParam("targetParentId") @Parameter(description = "目标父节点ID") Long targetParentId) { + return success(wbBoqDivisionService.copyBoqWithChildren(sourceBoqId, targetCompileTreeId, targetParentId)); + } + + @PostMapping("/copy-division") + @Operation(summary = "复制分部到目标位置(含子清单、定额和工料机)") + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:create')") + public CommonResult copyDivision( + @RequestParam("sourceDivisionId") @Parameter(description = "源分部节点ID") Long sourceDivisionId, + @RequestParam("targetCompileTreeId") @Parameter(description = "目标单位工程节点ID") Long targetCompileTreeId, + @RequestParam("targetParentId") @Parameter(description = "目标父节点ID") Long targetParentId) { + return success(wbBoqDivisionService.copyDivisionWithChildren(sourceDivisionId, targetCompileTreeId, targetParentId)); + } + + @PostMapping("/copy-from-guide") + @Operation(summary = "从清单指引复制清单子目到分部分项(含定额和工料机)") + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:create')") + public CommonResult copyFromGuide( + @RequestParam("compileTreeId") @Parameter(description = "目标单位工程节点ID") Long compileTreeId, + @RequestParam("parentId") @Parameter(description = "目标父节点ID(分部节点)") Long parentId, + @RequestParam("boqSubItemId") @Parameter(description = "清单子目ID") Long boqSubItemId, + @RequestParam("sourceBoqItemTreeId") @Parameter(description = "清单项树节点ID") Long sourceBoqItemTreeId) { + return success(wbBoqDivisionService.copyFromGuide(compileTreeId, parentId, boqSubItemId, sourceBoqItemTreeId)); + } + + @GetMapping("/validate-formula") + @Operation(summary = "验证定额工程量公式") + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:query')") + public CommonResult validateFormula( + @RequestParam("formula") @Parameter(description = "公式字符串,如 QDL*2") String formula, + @RequestParam(value = "qdlValue", required = false) @Parameter(description = "清单工程量测试值") java.math.BigDecimal qdlValue) { + com.yhy.module.core.service.workbench.QuotaQtyFormulaService.FormulaValidationResult result = + quotaQtyFormulaService.validateFormula(formula); + + FormulaValidationRespVO respVO = new FormulaValidationRespVO(); + respVO.setValid(result.isValid()); + respVO.setError(result.getError()); + + // 如果验证通过且提供了测试值,计算结果 + if (result.isValid() && qdlValue != null) { + respVO.setResult(quotaQtyFormulaService.calculateQuotaQty(formula, qdlValue)); + } else { + respVO.setResult(result.getTestResult()); + } + + return success(respVO); + } + + @GetMapping("/calculate-quota-qty") + @Operation(summary = "计算定额工程量") + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:query')") + public CommonResult calculateQuotaQty( + @RequestParam("formula") @Parameter(description = "公式字符串,如 QDL*2") String formula, + @RequestParam("qdlValue") @Parameter(description = "清单工程量值") java.math.BigDecimal qdlValue) { + return success(quotaQtyFormulaService.calculateQuotaQty(formula, qdlValue)); + } + + @GetMapping("/tree-by-tab") + @Operation(summary = "按标签页获取分部分项树(四个标签页独立)") + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:query')") + public CommonResult> getTreeByTab( + @RequestParam("compileTreeId") @Parameter(description = "编制模式树的单位工程节点ID") Long compileTreeId, + @RequestParam(value = "tabType", required = false) @Parameter(description = "标签页类型:division/measure/other/unit_summary") String tabType) { + return success(wbBoqDivisionService.getTreeByTab(compileTreeId, tabType)); + } + + /** + * 公式验证响应VO + */ + @lombok.Data + public static class FormulaValidationRespVO { + @io.swagger.v3.oas.annotations.media.Schema(description = "是否有效") + private Boolean valid; + @io.swagger.v3.oas.annotations.media.Schema(description = "错误信息") + private String error; + @io.swagger.v3.oas.annotations.media.Schema(description = "计算结果") + private java.math.BigDecimal result; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbBoqMarketMaterialController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbBoqMarketMaterialController.java new file mode 100644 index 0000000..0716b82 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbBoqMarketMaterialController.java @@ -0,0 +1,71 @@ +package com.yhy.module.core.controller.admin.workbench; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqMarketMaterialRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqMarketMaterialSaveReqVO; +import com.yhy.module.core.service.workbench.WbBoqMarketMaterialService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import javax.annotation.Resource; +import javax.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 工作台市场主材设备 Controller + * + * @author yhy + */ +@Tag(name = "工作台 - 市场主材设备") +@RestController +@RequestMapping("/core/wb/boq-market-material") +@Validated +public class WbBoqMarketMaterialController { + + @Resource + private WbBoqMarketMaterialService wbBoqMarketMaterialService; + + @PostMapping("/create") + @Operation(summary = "创建市场主材设备") + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:create')") + public CommonResult createMarketMaterial(@Valid @RequestBody WbBoqMarketMaterialSaveReqVO createReqVO) { + return success(wbBoqMarketMaterialService.createMarketMaterial(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新市场主材设备") + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:update')") + public CommonResult updateMarketMaterial(@Valid @RequestBody WbBoqMarketMaterialSaveReqVO updateReqVO) { + wbBoqMarketMaterialService.updateMarketMaterial(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除市场主材设备") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:delete')") + public CommonResult deleteMarketMaterial(@RequestParam("id") Long id) { + wbBoqMarketMaterialService.deleteMarketMaterial(id); + return success(true); + } + + @GetMapping("/list") + @Operation(summary = "获得市场主材设备列表") + @Parameter(name = "divisionId", description = "分部分项ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:query')") + public CommonResult> getMarketMaterialList(@RequestParam("divisionId") Long divisionId) { + return success(wbBoqMarketMaterialService.getMarketMaterialList(divisionId)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbBoqResourceController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbBoqResourceController.java new file mode 100644 index 0000000..6cf4fd8 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbBoqResourceController.java @@ -0,0 +1,124 @@ +package com.yhy.module.core.controller.admin.workbench; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceBatchUpdateResultVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceCategorySummaryVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceCreateBlankReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceFillReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceSaveReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbResourceSearchRespVO; +import com.yhy.module.core.service.workbench.WbBoqResourceService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import javax.annotation.Resource; +import javax.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 工作台工料机消耗 Controller + * + * @author yhy + */ +@Tag(name = "工作台 - 工料机消耗") +@RestController +@RequestMapping("/core/wb/boq-resource") +@Validated +public class WbBoqResourceController { + + @Resource + private WbBoqResourceService wbBoqResourceService; + + @PostMapping("/create") + @Operation(summary = "创建工料机消耗") + @PreAuthorize("@ss.hasPermission('core:wb-boq-resource:create')") + public CommonResult create(@Valid @RequestBody WbBoqResourceSaveReqVO createReqVO) { + return success(wbBoqResourceService.create(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新工料机消耗") + @PreAuthorize("@ss.hasPermission('core:wb-boq-resource:update')") + public CommonResult update(@Valid @RequestBody WbBoqResourceSaveReqVO updateReqVO) { + WbBoqResourceBatchUpdateResultVO result = wbBoqResourceService.update(updateReqVO); + return success(result); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除工料机消耗") + @Parameter(name = "id", description = "ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-boq-resource:delete')") + public CommonResult delete(@RequestParam("id") Long id) { + wbBoqResourceService.delete(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获取工料机消耗详情") + @Parameter(name = "id", description = "ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-boq-resource:query')") + public CommonResult get(@RequestParam("id") Long id) { + return success(wbBoqResourceService.get(id)); + } + + @GetMapping("/list") + @Operation(summary = "获取定额节点下的工料机列表") + @Parameter(name = "divisionId", description = "定额节点ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-boq-resource:query')") + public CommonResult> getList(@RequestParam("divisionId") Long divisionId) { + return success(wbBoqResourceService.getListByDivisionId(divisionId)); + } + + @GetMapping("/list-by-boq") + @Operation(summary = "获取清单节点下所有定额的工料机汇总") + @Parameter(name = "boqDivisionId", description = "清单节点ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-boq-resource:query')") + public CommonResult> getListByBoq(@RequestParam("boqDivisionId") Long boqDivisionId) { + return success(wbBoqResourceService.getListByBoqDivisionId(boqDivisionId)); + } + + @GetMapping("/category-summary") + @Operation(summary = "获取定额节点下工料机的分类汇总") + @Parameter(name = "divisionId", description = "定额节点ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-boq-resource:query')") + public CommonResult> getCategorySummary(@RequestParam("divisionId") Long divisionId) { + return success(wbBoqResourceService.getCategorySummary(divisionId)); + } + + @GetMapping("/search-by-code") + @Operation(summary = "编码弹窗查询工料机(本项目+非项目)") + @PreAuthorize("@ss.hasPermission('core:wb-boq-resource:query')") + public CommonResult> searchByCode( + @RequestParam("divisionId") Long divisionId, + @RequestParam("code") String code) { + return success(wbBoqResourceService.searchByCode(divisionId, code)); + } + + @PostMapping("/create-blank") + @Operation(summary = "创建空白工料机行") + @PreAuthorize("@ss.hasPermission('core:wb-boq-resource:create')") + public CommonResult createBlank(@Valid @RequestBody WbBoqResourceCreateBlankReqVO createBlankReqVO) { + return success(wbBoqResourceService.createBlank(createBlankReqVO)); + } + + @PutMapping("/fill-from-search") + @Operation(summary = "双击填充工料机数据到空白行") + @PreAuthorize("@ss.hasPermission('core:wb-boq-resource:update')") + public CommonResult fillFromSearch(@Valid @RequestBody WbBoqResourceFillReqVO fillReqVO) { + wbBoqResourceService.fillFromSearch(fillReqVO); + return success(true); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbCalcBaseRateController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbCalcBaseRateController.java new file mode 100644 index 0000000..dc50ae9 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbCalcBaseRateController.java @@ -0,0 +1,86 @@ +package com.yhy.module.core.controller.admin.workbench; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.workbench.vo.calcbaserate.CalcBaseRateCatalogTreeRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.calcbaserate.CalcBaseRateDirectoryRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.calcbaserate.CalcBaseRateItemRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.calcbaserate.UpdateBoqDivisionBaseRateReqVO; +import com.yhy.module.core.service.calcbaserate.CalcBaseRateCatalogService; +import com.yhy.module.core.service.calcbaserate.CalcBaseRateDirectoryService; +import com.yhy.module.core.service.calcbaserate.CalcBaseRateItemService; +import com.yhy.module.core.service.workbench.WbBoqDivisionService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import javax.annotation.Resource; +import javax.validation.Valid; +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.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +/** + * 工作台 - 基数费率 Controller + * 用于项目指引弹窗中的基数费率标签页 + * + * @author yhy + */ +@Tag(name = "工作台 - 基数费率") +@RestController +@RequestMapping("/core/wb/calc-base-rate") +@Validated +public class WbCalcBaseRateController { + + @Resource + private CalcBaseRateCatalogService calcBaseRateCatalogService; + + @Resource + private CalcBaseRateDirectoryService calcBaseRateDirectoryService; + + @Resource + private CalcBaseRateItemService calcBaseRateItemService; + + @Resource + private WbBoqDivisionService wbBoqDivisionService; + + @GetMapping("/catalog/tree") + @Operation(summary = "获取基数费率目录树(全部)") + @PreAuthorize("@ss.hasPermission('core:wb:query')") + public CommonResult> getCalcBaseRateCatalogTree() { + return success(calcBaseRateCatalogService.getCalcBaseRateCatalogTreeForWorkbench()); + } + + @GetMapping("/directory/tree") + @Operation(summary = "获取基数费率目录树(根据目录树节点)") + @Parameter(name = "catalogId", description = "基数费率目录树节点ID", required = true) + @PreAuthorize("@ss.hasPermission('core:wb:query')") + public CommonResult> getCalcBaseRateDirectoryTree( + @RequestParam("catalogId") Long catalogId) { + return success(calcBaseRateDirectoryService.getCalcBaseRateDirectoryTreeForWorkbench(catalogId)); + } + + @GetMapping("/item/list") + @Operation(summary = "获取基数费率项列表") + @Parameter(name = "directoryId", description = "基数费率目录ID", required = true) + @PreAuthorize("@ss.hasPermission('core:wb:query')") + public CommonResult> getCalcBaseRateItemList( + @RequestParam("directoryId") Long directoryId) { + return success(calcBaseRateItemService.getCalcBaseRateItemListForWorkbench(directoryId)); + } + + @PutMapping("/update-division") + @Operation(summary = "更新清单的基数费率") + @PreAuthorize("@ss.hasPermission('core:wb:update')") + public CommonResult updateBoqDivisionBaseRate( + @Valid @RequestBody UpdateBoqDivisionBaseRateReqVO reqVO) { + wbBoqDivisionService.updateBoqDivisionBaseRate(reqVO); + return success(true); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbCompileTreeController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbCompileTreeController.java new file mode 100644 index 0000000..91891a4 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbCompileTreeController.java @@ -0,0 +1,97 @@ +package com.yhy.module.core.controller.admin.workbench; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.workbench.vo.WbCompileTreeRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbCompileTreeSaveReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbCompileTreeSwapSortReqVO; +import com.yhy.module.core.service.workbench.WbCompileTreeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import javax.annotation.Resource; +import javax.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 编制模式树 Controller + * + * @author yhy + */ +@Tag(name = "工作台 - 编制模式树") +@RestController +@RequestMapping("/core/wb/compile") +@Validated +public class WbCompileTreeController { + + @Resource + private WbCompileTreeService wbCompileTreeService; + + @PostMapping("/init") + @Operation(summary = "初始化编制模式树(创建根节点)") + @PreAuthorize("@ss.hasPermission('core:wb-compile:create')") + public CommonResult initCompileTree( + @RequestParam("projectId") Long projectId, + @RequestParam("projectName") String projectName) { + return success(wbCompileTreeService.initCompileTree(projectId, projectName)); + } + + @PostMapping("/create") + @Operation(summary = "创建节点(单项或单位工程)") + @PreAuthorize("@ss.hasPermission('core:wb-compile:create')") + public CommonResult createNode(@Valid @RequestBody WbCompileTreeSaveReqVO createReqVO) { + return success(wbCompileTreeService.createNode(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新节点") + @PreAuthorize("@ss.hasPermission('core:wb-compile:update')") + public CommonResult updateNode(@Valid @RequestBody WbCompileTreeSaveReqVO updateReqVO) { + wbCompileTreeService.updateNode(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除节点") + @Parameter(name = "id", description = "节点ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-compile:delete')") + public CommonResult deleteNode(@RequestParam("id") Long id) { + wbCompileTreeService.deleteNode(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获取节点详情") + @Parameter(name = "id", description = "节点ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-compile:query')") + public CommonResult getNode(@RequestParam("id") Long id) { + return success(wbCompileTreeService.getNode(id)); + } + + @GetMapping("/tree") + @Operation(summary = "获取编制模式树") + @Parameter(name = "projectId", description = "项目节点ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-compile:query')") + public CommonResult> getTree(@RequestParam("projectId") Long projectId) { + return success(wbCompileTreeService.getTree(projectId)); + } + + @PostMapping("/swap-sort") + @Operation(summary = "交换排序") + @PreAuthorize("@ss.hasPermission('core:wb-compile:update')") + public CommonResult swapSort(@Valid @RequestBody WbCompileTreeSwapSortReqVO swapReqVO) { + wbCompileTreeService.swapSort(swapReqVO); + return success(true); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbItemInfoController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbItemInfoController.java new file mode 100644 index 0000000..32a074b --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbItemInfoController.java @@ -0,0 +1,60 @@ +package com.yhy.module.core.controller.admin.workbench; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.workbench.vo.WbItemInfoRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbItemInfoSaveReqVO; +import com.yhy.module.core.service.workbench.WbItemInfoService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import javax.annotation.Resource; +import javax.validation.Valid; +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.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 单项基本信息 Controller + * + * @author yhy + */ +@Tag(name = "工作台 - 单项基本信息") +@RestController +@RequestMapping("/core/wb/item-info") +@Validated +public class WbItemInfoController { + + @Resource + private WbItemInfoService wbItemInfoService; + + @GetMapping("/get") + @Operation(summary = "获取单项基本信息") + @Parameter(name = "compileTreeId", description = "编制树节点ID(单项节点)", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-item-info:query')") + public CommonResult get(@RequestParam("compileTreeId") Long compileTreeId) { + return success(wbItemInfoService.getByCompileTreeId(compileTreeId)); + } + + @PostMapping("/save") + @Operation(summary = "保存单项基本信息") + @PreAuthorize("@ss.hasPermission('core:wb-item-info:update')") + public CommonResult save(@Valid @RequestBody WbItemInfoSaveReqVO saveReqVO) { + return success(wbItemInfoService.save(saveReqVO)); + } + + @PostMapping("/refresh-config") + @Operation(summary = "刷新配置快照") + @Parameter(name = "compileTreeId", description = "编制树节点ID(单项节点)", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-item-info:update')") + public CommonResult refreshConfigSnapshot(@RequestParam("compileTreeId") Long compileTreeId) { + wbItemInfoService.refreshConfigSnapshot(compileTreeId); + return success(true); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbProjectController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbProjectController.java new file mode 100644 index 0000000..a6264b2 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbProjectController.java @@ -0,0 +1,244 @@ +package com.yhy.module.core.controller.admin.workbench; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.*; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.workbench.vo.WbProjectTreeRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbProjectTreeSaveReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbProjectTreeSwapSortReqVO; +import com.yhy.module.core.service.workbench.WbProjectService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import java.util.Map; +import javax.annotation.Resource; +import javax.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 工作台项目管理树 Controller + * + * @author yhy + */ +@Tag(name = "工作台 - 项目管理") +@RestController +@RequestMapping("/core/wb/project") +@Validated +public class WbProjectController { + + @Resource + private WbProjectService wbProjectService; + + @Resource + private com.yhy.module.core.service.workbench.WbSnapshotReadService wbSnapshotReadService; + + @Resource + private com.yhy.module.core.service.quota.QuotaRateItemService quotaRateItemService; + + @Resource + private com.yhy.module.core.service.quota.QuotaRateFieldLabelService quotaRateFieldLabelService; + + @Resource + private com.yhy.module.core.service.quota.QuotaFeeItemService quotaFeeItemService; + + @Resource + private com.yhy.module.core.service.workbench.AuditModeService auditModeService; + + @Resource + private com.yhy.module.core.service.workbench.ProgressPaymentModeService progressPaymentModeService; + + @PostMapping("/create") + @Operation(summary = "创建节点(目录或项目)") + @PreAuthorize("@ss.hasPermission('core:wb-project:create')") + public CommonResult createNode(@Valid @RequestBody WbProjectTreeSaveReqVO createReqVO) { + return success(wbProjectService.createNode(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新节点") + @PreAuthorize("@ss.hasPermission('core:wb-project:update')") + public CommonResult updateNode(@Valid @RequestBody WbProjectTreeSaveReqVO updateReqVO) { + wbProjectService.updateNode(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除节点") + @Parameter(name = "id", description = "节点ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-project:delete')") + public CommonResult deleteNode(@RequestParam("id") Long id) { + wbProjectService.deleteNode(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获取节点详情") + @Parameter(name = "id", description = "节点ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-project:query')") + public CommonResult getNode(@RequestParam("id") Long id) { + return success(wbProjectService.getNode(id)); + } + + @GetMapping("/tree") + @Operation(summary = "获取项目管理树") + @PreAuthorize("@ss.hasPermission('core:wb-project:query')") + public CommonResult> getTree() { + return success(wbProjectService.getTree()); + } + + @PostMapping("/swap-sort") + @Operation(summary = "交换排序") + @PreAuthorize("@ss.hasPermission('core:wb-project:update')") + public CommonResult swapSort(@Valid @RequestBody WbProjectTreeSwapSortReqVO swapReqVO) { + wbProjectService.swapSort(swapReqVO); + return success(true); + } + + @PostMapping("/archive") + @Operation(summary = "归档项目") + @Parameter(name = "id", description = "项目节点ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-project:update')") + public CommonResult archiveProject(@RequestParam("id") Long id) { + wbProjectService.archiveProject(id); + return success(true); + } + + @PutMapping("/save-to-history") + @Operation(summary = "将项目保存至历史库") + @Parameter(name = "id", description = "项目节点ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-project:update')") + public CommonResult saveToHistoryLibrary(@RequestParam("id") Long id) { + wbProjectService.saveToHistoryLibrary(id); + return success(true); + } + + @PutMapping("/remove-from-history") + @Operation(summary = "从历史库撤销项目") + @Parameter(name = "id", description = "项目节点ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-project:update')") + public CommonResult removeFromHistoryLibrary(@RequestParam("id") Long id) { + wbProjectService.removeFromHistoryLibrary(id); + return success(true); + } + + @GetMapping("/rate-item-tree") + @Operation(summary = "获取费率项树(优先使用快照数据)") + @Parameters({ + @Parameter(name = "compileTreeId", description = "单位工程ID", required = true, example = "1"), + @Parameter(name = "rateModeId", description = "费率模式ID", required = true, example = "100") + }) + @PreAuthorize("@ss.hasPermission('core:wb-project:query')") + public CommonResult> getRateItemTree( + @RequestParam("compileTreeId") Long compileTreeId, + @RequestParam("rateModeId") Long rateModeId) { + // 1. 优先从快照表读取 + List snapshotItems = + wbSnapshotReadService.getRateItems(compileTreeId, rateModeId); + + // 2. 转换为VO(复用后台标准库的逻辑) + List result; + if (snapshotItems != null && !snapshotItems.isEmpty()) { + // 从快照数据构建树(转换为QuotaRateItemDO后调用公共方法) + List rateItems = snapshotItems.stream() + .map(wb -> { + com.yhy.module.core.dal.dataobject.quota.QuotaRateItemDO quota = new com.yhy.module.core.dal.dataobject.quota.QuotaRateItemDO(); + quota.setId(wb.getId()); + quota.setCatalogItemId(wb.getCatalogItemId()); + quota.setParentId(wb.getParentId()); + quota.setCustomCode(wb.getCustomCode()); + quota.setName(wb.getName()); + quota.setRateCode(wb.getRateCode()); + quota.setIsEditable(wb.getIsEditable()); + quota.setDefaultValue(wb.getDefaultValue()); + quota.setValueMode(wb.getValueMode()); + quota.setSettings(wb.getSettings()); + quota.setNodeType(wb.getNodeType()); + quota.setLevel(wb.getLevel()); + quota.setSortOrder(wb.getSortOrder()); + return quota; + }) + .collect(java.util.stream.Collectors.toList()); + // 快照没有字段配置,传空列表 + result = quotaRateItemService.buildRateItemTree(rateItems, java.util.Collections.emptyList()); + } else { + // 快照不存在,返回空列表(不回退到后台标准库) + result = java.util.Collections.emptyList(); + } + return success(result); + } + + @GetMapping("/rate-field-label-list") + @Operation(summary = "获取费率字段标签列表(优先使用快照数据)") + @Parameters({ + @Parameter(name = "compileTreeId", description = "单位工程ID", required = true, example = "1"), + @Parameter(name = "rateModeId", description = "费率模式ID", required = true, example = "100") + }) + @PreAuthorize("@ss.hasPermission('core:wb-project:query')") + public CommonResult> getRateFieldLabelList( + @RequestParam("compileTreeId") Long compileTreeId, + @RequestParam("rateModeId") Long rateModeId) { + // 1. 优先从快照表读取 + List snapshotItems = + wbSnapshotReadService.getRateFieldLabels(compileTreeId, rateModeId); + + // 2. 转换为VO + List result = new java.util.ArrayList<>(); + if (snapshotItems != null && !snapshotItems.isEmpty()) { + for (com.yhy.module.core.dal.dataobject.workbench.WbRateFieldLabelDO item : snapshotItems) { + com.yhy.module.core.controller.admin.quota.vo.QuotaRateFieldLabelRespVO vo = + new com.yhy.module.core.controller.admin.quota.vo.QuotaRateFieldLabelRespVO(); + vo.setId(item.getId()); + vo.setCatalogItemId(item.getCatalogItemId()); + vo.setLabelName(item.getLabelName()); + vo.setSortOrder(item.getSortOrder()); + result.add(vo); + } + } else { + // 快照不存在,返回空列表(不回退到后台标准库) + } + return success(result); + } + + @GetMapping("/fee-item-tree") + @Operation(summary = "获取取费项树(只使用快照数据)") + @Parameters({ + @Parameter(name = "compileTreeId", description = "单位工程ID", required = true, example = "1"), + @Parameter(name = "rateModeId", description = "费率模式ID", required = true, example = "100") + }) + @PreAuthorize("@ss.hasPermission('core:wb-project:query')") + public CommonResult> getFeeItemTree( + @RequestParam("compileTreeId") Long compileTreeId, + @RequestParam("rateModeId") Long rateModeId) { + // 只从快照表读取,不回退到后台标准库 + List result = + wbSnapshotReadService.getFeeItemWithRateTree(compileTreeId, rateModeId); + + if (result == null) { + // 快照不存在,返回空列表(不回退到后台标准库) + result = java.util.Collections.emptyList(); + } + return success(result); + } + + @GetMapping("/check-mode-exists") + @Operation(summary = "检查项目是否存在审核模式和进度款模式数据") + @Parameter(name = "projectId", description = "项目ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-project:query')") + public CommonResult> checkModeExists(@RequestParam("projectId") Long projectId) { + Map result = new java.util.HashMap<>(); + result.put("hasAuditMode", !auditModeService.getAuditModeList(projectId).isEmpty()); + result.put("hasProgressPaymentMode", !progressPaymentModeService.getProgressPaymentModeList(projectId).isEmpty()); + return success(result); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbResourceSummaryController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbResourceSummaryController.java new file mode 100644 index 0000000..21c9131 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbResourceSummaryController.java @@ -0,0 +1,103 @@ +package com.yhy.module.core.controller.admin.workbench; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.workbench.vo.WbResourceSourceBoqRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbResourceSourceUnitRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbResourceSummaryRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbResourceSummaryTreeRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbResourceSummaryUpdateReqVO; +import com.yhy.module.core.service.workbench.WbResourceSummaryService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import javax.annotation.Resource; +import javax.validation.Valid; +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.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 工料机汇总 Controller + * + * @author yhy + */ +@Tag(name = "工作台 - 工料机汇总") +@RestController +@RequestMapping("/core/wb/resource-summary") +@Validated +public class WbResourceSummaryController { + + @Resource + private WbResourceSummaryService wbResourceSummaryService; + + @GetMapping("/tree") + @Operation(summary = "获取工料机汇总树") + @Parameters({ + @Parameter(name = "projectId", description = "项目ID", required = true, example = "1"), + @Parameter(name = "compileTreeId", description = "编制模式树的单位工程节点ID(可选,传了只汇总该单位工程)", example = "1") + }) + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:query')") + public CommonResult> getSummaryTree( + @RequestParam("projectId") Long projectId, + @RequestParam(value = "compileTreeId", required = false) Long compileTreeId) { + return success(wbResourceSummaryService.getSummaryTree(projectId, compileTreeId)); + } + + @GetMapping("/list") + @Operation(summary = "获取工料机汇总列表(按类别)") + @Parameters({ + @Parameter(name = "projectId", description = "项目ID", required = true, example = "1"), + @Parameter(name = "category", description = "类别:labor/material/machine/bid_material", required = true, example = "material"), + @Parameter(name = "compileTreeId", description = "编制模式树的单位工程节点ID(可选,传了只汇总该单位工程)", example = "1") + }) + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:query')") + public CommonResult> getSummaryList( + @RequestParam("projectId") Long projectId, + @RequestParam("category") String category, + @RequestParam(value = "compileTreeId", required = false) Long compileTreeId) { + return success(wbResourceSummaryService.getSummaryList(projectId, category, compileTreeId)); + } + + @PutMapping("/update") + @Operation(summary = "更新工料机汇总(打印、评标指定材料、编码等)") + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:update')") + public CommonResult updateSummary(@Valid @RequestBody WbResourceSummaryUpdateReqVO updateReqVO) { + wbResourceSummaryService.updateSummary(updateReqVO); + return success(true); + } + + @GetMapping("/source-units") + @Operation(summary = "获取工料机来源-单位工程列表") + @Parameters({ + @Parameter(name = "projectId", description = "项目ID", required = true, example = "1"), + @Parameter(name = "resourceKey", description = "资源唯一键", required = true, example = "abc123") + }) + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:query')") + public CommonResult> getSourceUnits( + @RequestParam("projectId") Long projectId, + @RequestParam("resourceKey") String resourceKey) { + return success(wbResourceSummaryService.getSourceUnits(projectId, resourceKey)); + } + + @GetMapping("/source-boqs") + @Operation(summary = "获取工料机来源-清单列表") + @Parameters({ + @Parameter(name = "compileTreeId", description = "编制模式树ID(单位工程节点)", required = true, example = "1"), + @Parameter(name = "resourceKey", description = "资源唯一键", required = true, example = "abc123") + }) + @PreAuthorize("@ss.hasPermission('core:wb-boq-division:query')") + public CommonResult> getSourceBoqs( + @RequestParam("compileTreeId") Long compileTreeId, + @RequestParam("resourceKey") String resourceKey) { + return success(wbResourceSummaryService.getSourceBoqs(compileTreeId, resourceKey)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbUnifiedFeeConfigController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbUnifiedFeeConfigController.java new file mode 100644 index 0000000..2feb0cd --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbUnifiedFeeConfigController.java @@ -0,0 +1,223 @@ +package com.yhy.module.core.controller.admin.workbench; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.workbench.vo.WbUnifiedFeeConfigRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbUnifiedFeeConfigSaveReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbUnifiedFeeSummaryItemVO; +import com.yhy.module.core.controller.admin.quota.vo.QuotaUnifiedFeeSettingRespVO; +import com.yhy.module.core.dal.dataobject.workbench.WbUnifiedFeeConfigDO; +import com.yhy.module.core.dal.dataobject.workbench.WbUnifiedFeeSettingDO; +import com.yhy.module.core.service.workbench.WbUnifiedFeeConfigService; +import com.yhy.module.core.service.workbench.WbSnapshotReadService; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.json.JSONUtil; +import java.util.ArrayList; +import java.util.Map; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +/** + * 工作台统一取费配置 Controller + */ +@Tag(name = "工作台 - 统一取费配置") +@RestController +@RequestMapping("/core/wb/unified-fee-config") +@Validated +public class WbUnifiedFeeConfigController { + + @Resource + private WbUnifiedFeeConfigService wbUnifiedFeeConfigService; + + @Resource + private WbSnapshotReadService wbSnapshotReadService; + + + @GetMapping("/list") + @Operation(summary = "获取统一取费配置列表") + @Parameter(name = "compileTreeId", description = "单位工程节点ID", required = true) + public CommonResult> getList(@RequestParam("compileTreeId") Long compileTreeId) { + List list = wbUnifiedFeeConfigService.getListByCompileTreeId(compileTreeId); + return success(convertToRespVOList(list)); + } + + @GetMapping("/list-by-rate-mode") + @Operation(summary = "根据费率模式获取统一取费配置列表") + @Parameter(name = "compileTreeId", description = "单位工程节点ID", required = true) + @Parameter(name = "rateModeId", description = "费率模式ID", required = true) + public CommonResult> getListByRateMode( + @RequestParam("compileTreeId") Long compileTreeId, + @RequestParam("rateModeId") Long rateModeId) { + List list = wbUnifiedFeeConfigService.getListByCompileTreeIdAndRateModeId(compileTreeId, rateModeId); + return success(convertToRespVOList(list)); + } + + @GetMapping("/get-by-setting") + @Operation(summary = "根据统一取费设置ID获取配置") + @Parameter(name = "compileTreeId", description = "单位工程节点ID", required = true) + @Parameter(name = "sourceUnifiedFeeSettingId", description = "来源统一取费设置ID", required = true) + public CommonResult getBySourceUnifiedFeeSettingId( + @RequestParam("compileTreeId") Long compileTreeId, + @RequestParam("sourceUnifiedFeeSettingId") Long sourceUnifiedFeeSettingId) { + WbUnifiedFeeConfigDO config = wbUnifiedFeeConfigService.getBySourceUnifiedFeeSettingId(compileTreeId, sourceUnifiedFeeSettingId); + return success(config != null ? convertToRespVO(config) : null); + } + + @PostMapping("/save") + @Operation(summary = "保存统一取费配置") + public CommonResult save(@Valid @RequestBody WbUnifiedFeeConfigSaveReqVO reqVO) { + WbUnifiedFeeConfigDO config = convertToDO(reqVO); + Long id = wbUnifiedFeeConfigService.saveOrUpdate(config); + return success(id); + } + + @PostMapping("/batch-save") + @Operation(summary = "批量保存统一取费配置") + @Parameter(name = "compileTreeId", description = "单位工程节点ID", required = true) + @Parameter(name = "rateModeId", description = "费率模式ID", required = true) + public CommonResult batchSave( + @RequestParam("compileTreeId") Long compileTreeId, + @RequestParam("rateModeId") Long rateModeId, + @Valid @RequestBody List reqVOList) { + List configs = reqVOList.stream() + .map(this::convertToDO) + .collect(Collectors.toList()); + wbUnifiedFeeConfigService.batchSave(compileTreeId, rateModeId, configs); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除统一取费配置") + @Parameter(name = "id", description = "配置ID", required = true) + public CommonResult delete(@RequestParam("id") Long id) { + wbUnifiedFeeConfigService.delete(id); + return success(true); + } + + @PostMapping("/apply") + @Operation(summary = "应用统一取费 - 将统一取费目录按范围添加到对应清单") + @Parameter(name = "compileTreeId", description = "单位工程节点ID", required = true) + public CommonResult apply(@RequestParam("compileTreeId") Long compileTreeId) { + wbUnifiedFeeConfigService.applyUnifiedFee(compileTreeId); + return success(true); + } + + @GetMapping("/setting-parent-list") + @Operation(summary = "获取统一取费设置父列表(从快照表读取)") + @Parameter(name = "compileTreeId", description = "单位工程节点ID", required = true) + @Parameter(name = "rateModeId", description = "费率模式ID", required = true) + public CommonResult> getSettingParentList( + @RequestParam("compileTreeId") Long compileTreeId, + @RequestParam("rateModeId") Long rateModeId) { + // 从快照表读取统一取费设置 + List snapshotItems = wbSnapshotReadService.getUnifiedFeeSettings(compileTreeId, rateModeId); + + // 按parentId分组 + Map> childrenMap = snapshotItems.stream() + .filter(item -> item.getParentId() != null) + .collect(Collectors.groupingBy(WbUnifiedFeeSettingDO::getParentId)); + + // 构建树形结构:directory → fee(parent) → sub_fee(child) + List result = snapshotItems.stream() + .filter(item -> item.getParentId() == null) // 一级节点(目录或费用) + .map(rootItem -> buildSnapshotTreeNode(rootItem, childrenMap)) + .collect(Collectors.toList()); + + return success(result); + } + + @GetMapping("/summary-source") + @Operation(summary = "获取统一取费汇总来源 - 返回范围内的定额列表") + @Parameter(name = "divisionId", description = "统一取费节点ID", required = true) + public CommonResult> getSummarySource( + @RequestParam("divisionId") Long divisionId) { + List list = wbUnifiedFeeConfigService.getSummarySourceByDivisionId(divisionId); + return success(list); + } + + private WbUnifiedFeeConfigRespVO convertToRespVO(WbUnifiedFeeConfigDO config) { + WbUnifiedFeeConfigRespVO respVO = new WbUnifiedFeeConfigRespVO(); + respVO.setId(config.getId()); + respVO.setCompileTreeId(config.getCompileTreeId()); + respVO.setDivisionId(config.getDivisionId()); + respVO.setSourceUnifiedFeeSettingId(config.getSourceUnifiedFeeSettingId()); + respVO.setRateModeId(config.getRateModeId()); + respVO.setConfigData(config.getConfigData()); + respVO.setAttributes(config.getAttributes()); + respVO.setSortOrder(config.getSortOrder()); + return respVO; + } + + private List convertToRespVOList(List list) { + return list.stream().map(this::convertToRespVO).collect(Collectors.toList()); + } + + private WbUnifiedFeeConfigDO convertToDO(WbUnifiedFeeConfigSaveReqVO reqVO) { + WbUnifiedFeeConfigDO config = new WbUnifiedFeeConfigDO(); + config.setId(reqVO.getId()); + config.setCompileTreeId(reqVO.getCompileTreeId()); + config.setDivisionId(reqVO.getDivisionId()); + config.setSourceUnifiedFeeSettingId(reqVO.getSourceUnifiedFeeSettingId()); + config.setRateModeId(reqVO.getRateModeId()); + config.setConfigData(reqVO.getConfigData()); + config.setAttributes(reqVO.getAttributes()); + config.setSortOrder(reqVO.getSortOrder()); + return config; + } + + /** + * 递归构建快照树节点,并合并子节点的feeChapter + */ + private QuotaUnifiedFeeSettingRespVO buildSnapshotTreeNode(WbUnifiedFeeSettingDO item, Map> childrenMap) { + QuotaUnifiedFeeSettingRespVO vo = BeanUtil.copyProperties(item, QuotaUnifiedFeeSettingRespVO.class); + // 使用快照表的sourceId作为id,保持与后台标准库的兼容性 + vo.setId(item.getSourceId()); + + List children = childrenMap.get(item.getId()); + if (children != null && !children.isEmpty()) { + // 递归构建子节点 + List childVOs = children.stream() + .map(child -> buildSnapshotTreeNode(child, childrenMap)) + .collect(Collectors.toList()); + vo.setChildren(childVOs); + + // 合并所有子孙节点的feeChapter到当前节点 + List allChapters = new ArrayList<>(); + collectSnapshotFeeChapters(childVOs, allChapters); + if (!allChapters.isEmpty()) { + vo.setFeeChapter(JSONUtil.toJsonStr(allChapters)); + } + } + return vo; + } + + /** + * 递归收集所有子孙节点的feeChapter + */ + private void collectSnapshotFeeChapters(List nodes, List result) { + for (QuotaUnifiedFeeSettingRespVO node : nodes) { + // 收集当前节点的feeChapter + String fc = node.getFeeChapter(); + if (fc != null && !fc.isEmpty() && !"[]".equals(fc)) { + try { + List ids = JSONUtil.toList(fc, String.class); + result.addAll(ids); + } catch (Exception e) { + // 解析失败时忽略 + } + } + // 递归收集子节点 + if (node.getChildren() != null && !node.getChildren().isEmpty()) { + collectSnapshotFeeChapters(node.getChildren(), result); + } + } + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbUnitFeeSettingController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbUnitFeeSettingController.java new file mode 100644 index 0000000..ddf89be --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbUnitFeeSettingController.java @@ -0,0 +1,80 @@ +package com.yhy.module.core.controller.admin.workbench; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.*; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.quota.vo.QuotaFeeItemWithRateRespVO; +import com.yhy.module.core.service.workbench.WbUnitFeeSettingService; +import com.yhy.module.core.service.workbench.WbUnitFeeSettingService.FeeOverrideDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import lombok.RequiredArgsConstructor; +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.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 工作台 - 单位工程取费项设定 Controller + */ +@Tag(name = "工作台 - 单位工程取费项设定") +@RestController +@RequestMapping("/core/wb/unit-fee-setting") +@Validated +@RequiredArgsConstructor +public class WbUnitFeeSettingController { + + private final WbUnitFeeSettingService feeSettingService; + + @GetMapping("/list-by-unit") + @Operation(summary = "获取单位工程的取费项列表(合并覆写值)") + @Parameter(name = "unitId", description = "单位工程ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb:unit-fee-setting:query')") + public CommonResult> getListByUnitId(@RequestParam("unitId") Long unitId) { + return success(feeSettingService.getMergedFeeList(unitId)); + } + + @GetMapping("/list-by-division") + @Operation(summary = "根据分部分项节点ID获取取费项列表(合并覆写值)") + @Parameter(name = "divisionId", description = "分部分项节点ID(定额节点)", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb:unit-fee-setting:query')") + public CommonResult> getListByDivisionId(@RequestParam("divisionId") Long divisionId) { + return success(feeSettingService.getMergedFeeListByDivisionId(divisionId)); + } + + @PostMapping("/batch-save") + @Operation(summary = "批量保存取费项覆写值") + @Parameter(name = "divisionId", description = "分部分项节点ID(定额节点)", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb:unit-fee-setting:update')") + public CommonResult batchSave( + @RequestParam("divisionId") Long divisionId, + @RequestBody List overrides) { + feeSettingService.batchSaveOverrides(divisionId, overrides); + return success(true); + } + + @PostMapping("/mark-deleted") + @Operation(summary = "标记取费项为已删除") + @PreAuthorize("@ss.hasPermission('core:wb:unit-fee-setting:delete')") + public CommonResult markAsDeleted( + @RequestParam("divisionId") Long divisionId, + @RequestParam("feeItemId") Long feeItemId) { + feeSettingService.markAsDeleted(divisionId, feeItemId); + return success(true); + } + + @PostMapping("/initialize") + @Operation(summary = "初始化定额节点的取费项设定(创建快照)") + @Parameter(name = "divisionId", description = "分部分项节点ID(定额节点)", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb:unit-fee-setting:create')") + public CommonResult initialize(@RequestParam("divisionId") Long divisionId) { + feeSettingService.initializeSettings(divisionId); + return success(true); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbUnitInfoController.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbUnitInfoController.java new file mode 100644 index 0000000..04ace86 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/WbUnitInfoController.java @@ -0,0 +1,120 @@ +package com.yhy.module.core.controller.admin.workbench; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.workbench.vo.WbUnitInfoRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbUnitInfoSaveReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbUnitRateConfigSaveReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbUsedQuotaSpecialtyRespVO; +import com.yhy.module.core.service.workbench.WbUnitInfoService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import java.util.Map; +import javax.annotation.Resource; +import javax.validation.Valid; +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.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 单位工程信息 Controller + * + * @author yhy + */ +@Tag(name = "工作台 - 单位工程信息") +@RestController +@RequestMapping("/core/wb/unit-info") +@Validated +public class WbUnitInfoController { + + @Resource + private WbUnitInfoService wbUnitInfoService; + + @GetMapping("/get") + @Operation(summary = "获取单位工程信息") + @Parameter(name = "compileTreeId", description = "编制树节点ID(单位工程节点)", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-unit-info:query')") + public CommonResult get(@RequestParam("compileTreeId") Long compileTreeId) { + return success(wbUnitInfoService.getByCompileTreeId(compileTreeId)); + } + + @PostMapping("/save") + @Operation(summary = "保存单位工程信息") + @PreAuthorize("@ss.hasPermission('core:wb-unit-info:update')") + public CommonResult save(@Valid @RequestBody WbUnitInfoSaveReqVO saveReqVO) { + return success(wbUnitInfoService.save(saveReqVO)); + } + + // ==================== 单位工程级费率及取费相关接口 ==================== + + @GetMapping("/rate-mode-bindings") + @Operation(summary = "获取单位工程的费率模式绑定列表") + @Parameter(name = "compileTreeId", description = "编制树节点ID(单位工程节点)", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-unit-info:query')") + public CommonResult>> getUnitRateModeBindings(@RequestParam("compileTreeId") Long compileTreeId) { + return success(wbUnitInfoService.getUnitRateModeBindings(compileTreeId)); + } + + @PostMapping("/switch-rate-mode") + @Operation(summary = "切换单位工程的费率模式") + @Parameters({ + @Parameter(name = "compileTreeId", description = "编制树节点ID(单位工程节点)", required = true, example = "1"), + @Parameter(name = "quotaCatalogItemId", description = "定额专业ID", required = true, example = "100"), + @Parameter(name = "rateModeId", description = "费率模式ID", required = true, example = "200") + }) + @PreAuthorize("@ss.hasPermission('core:wb-unit-info:update')") + public CommonResult switchUnitRateMode( + @RequestParam("compileTreeId") Long compileTreeId, + @RequestParam("quotaCatalogItemId") Long quotaCatalogItemId, + @RequestParam("rateModeId") Long rateModeId) { + wbUnitInfoService.switchUnitRateMode(compileTreeId, quotaCatalogItemId, rateModeId); + return success(true); + } + + @GetMapping("/rate-config") + @Operation(summary = "获取单位工程的费率配置") + @Parameters({ + @Parameter(name = "compileTreeId", description = "编制树节点ID(单位工程节点)", required = true, example = "1"), + @Parameter(name = "quotaCatalogItemId", description = "定额专业ID", required = true, example = "100") + }) + @PreAuthorize("@ss.hasPermission('core:wb-unit-info:query')") + public CommonResult> getUnitRateConfig( + @RequestParam("compileTreeId") Long compileTreeId, + @RequestParam("quotaCatalogItemId") Long quotaCatalogItemId) { + return success(wbUnitInfoService.getUnitRateConfig(compileTreeId, quotaCatalogItemId)); + } + + @PostMapping("/save-rate-config") + @Operation(summary = "保存单位工程的费率配置") + @PreAuthorize("@ss.hasPermission('core:wb-unit-info:update')") + public CommonResult saveUnitRateConfig(@Valid @RequestBody WbUnitRateConfigSaveReqVO reqVO) { + wbUnitInfoService.saveUnitRateConfig(reqVO.getCompileTreeId(), reqVO.getQuotaCatalogItemId(), + reqVO.getRateModeId(), reqVO.getRateSettings(), reqVO.getFeeSettings()); + return success(true); + } + + @GetMapping("/used-quota-specialties-by-unit") + @Operation(summary = "获取单位工程使用的定额专业列表") + @Parameter(name = "compileTreeId", description = "编制树节点ID(单位工程节点)", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-unit-info:query')") + public CommonResult> getUsedQuotaSpecialtiesByUnit(@RequestParam("compileTreeId") Long compileTreeId) { + return success(wbUnitInfoService.getUsedQuotaSpecialtiesByUnit(compileTreeId)); + } + + @GetMapping("/used-quota-specialties") + @Operation(summary = "获取项目下所有使用的定额专业列表(合并所有单位工程)") + @Parameter(name = "projectId", description = "项目ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('core:wb-unit-info:query')") + public CommonResult> getUsedQuotaSpecialties(@RequestParam("projectId") Long projectId) { + return success(wbUnitInfoService.getUsedQuotaSpecialtiesByProject(projectId)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/HistoryBoqListReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/HistoryBoqListReqVO.java new file mode 100644 index 0000000..a8c88b6 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/HistoryBoqListReqVO.java @@ -0,0 +1,45 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 历史套用查询参数 Request VO + */ +@Schema(description = "工作台 - 历史套用查询参数 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class HistoryBoqListReqVO extends PageParam { + + @Schema(description = "类别(清单/分部)", example = "boq") + private String category; + + @Schema(description = "库类别", example = "material") + private String libraryCategory; + + @Schema(description = "清单章节ID", example = "1") + private Long listChapter; + + @Schema(description = "定额专业ID", example = "1") + private Long quotaProfession; + + @Schema(description = "定额章节ID", example = "1") + private Long quotaChapter; + + @Schema(description = "开始时间(时间戳)", example = "1704067200000") + private Long startTime; + + @Schema(description = "结束时间(时间戳)", example = "1735689600000") + private Long endTime; + + @Schema(description = "套价方式", example = "quota") + private String pricingMethod; + + @Schema(description = "名称(模糊查询)", example = "土石方") + private String name; + + @Schema(description = "项目特征(模糊查询)", example = "一类土") + private String feature; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqDivisionRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqDivisionRespVO.java new file mode 100644 index 0000000..e92555a --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqDivisionRespVO.java @@ -0,0 +1,115 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import lombok.Data; + +/** + * 工作台分部分项树 Response VO + */ +@Schema(description = "工作台 - 分部分项树 Response VO") +@Data +public class WbBoqDivisionRespVO { + + @Schema(description = "节点ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "编制模式树的单位工程节点ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long compileTreeId; + + @Schema(description = "父节点ID", example = "1") + private Long parentId; + + @Schema(description = "节点类型:division-分部, boq-清单, quota-定额", requiredMode = Schema.RequiredMode.REQUIRED, example = "division") + private String nodeType; + + @Schema(description = "来源类型:catalog-从标准库引用, manual-手工录入", example = "manual") + private String sourceType; + + @Schema(description = "引用清单目录ID", example = "1") + private Long sourceBoqCatalogId; + + @Schema(description = "引用清单项树ID", example = "1") + private Long sourceBoqItemTreeId; + + @Schema(description = "清单说明(关联查询,来自后台清单子目)", example = "

适用于建筑场地的平整工程

") + private String boqDescription; + + @Schema(description = "清单章节名称(关联查询)", example = "土石方工程") + private String boqChapterName; + + @Schema(description = "清单专业名称(关联查询)", example = "广东建筑工程清单") + private String boqProfessionName; + + @Schema(description = "引用定额基价ID", example = "1") + private Long sourceQuotaItemId; + + @Schema(description = "定额所属子目录ID(用于取费章节过滤)", example = "1") + private Long sourceCatalogItemId; + + @Schema(description = "定额所属子目录路径(用于取费章节过滤,包含所有祖先节点ID)") + private String[] sourceCatalogPath; + + @Schema(description = "编码", example = "010101") + private String code; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "土石方工程") + private String name; + + @Schema(description = "项目特征", example = "1.土壤类别:一类土") + private String feature; + + @Schema(description = "单位(字典)", example = "m3") + private String unit; + + @Schema(description = "工程量", example = "100.00") + private BigDecimal qty; + + @Schema(description = "费用代号(只允许英文字符串,代表合价的值)", example = "CLF") + private String costCode; + + @Schema(description = "费率", example = "3.5") + private BigDecimal rate; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "定额工程量公式(如 QDL*2,QDL代表清单工程量)", example = "QDL*2") + private String quotaQtyFormula; + + @Schema(description = "单价(虚拟字段,内存计算)", example = "25.50") + private BigDecimal unitPrice; + + @Schema(description = "合价(虚拟字段,内存计算)", example = "2550.00") + private BigDecimal amount; + + @Schema(description = "行号", example = "1") + private String lineNo; + + @Schema(description = "排序号", example = "1") + private Integer sortOrder; + + @Schema(description = "层级路径") + private String[] path; + + @Schema(description = "扩展属性") + private Map attributes; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; + + @Schema(description = "关联同步库ID(非空表示已同步)") + private Long syncLibraryId; + + @Schema(description = "是否为同步来源节点") + private Boolean isSyncSource; + + @Schema(description = "子节点列表") + private List children; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqDivisionSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqDivisionSaveReqVO.java new file mode 100644 index 0000000..b8fd252 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqDivisionSaveReqVO.java @@ -0,0 +1,83 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.util.Map; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import lombok.Data; + +/** + * 工作台分部分项树 创建/更新 Request VO + */ +@Schema(description = "工作台 - 分部分项树创建/更新 Request VO") +@Data +public class WbBoqDivisionSaveReqVO { + + @Schema(description = "节点ID(更新时必填)", example = "1") + private Long id; + + @Schema(description = "编制模式树的单位工程节点ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "单位工程节点ID不能为空") + private Long compileTreeId; + + @Schema(description = "父节点ID(分部节点为空)", example = "1") + private Long parentId; + + @Schema(description = "节点类型:division-分部, boq-清单, quota-定额", requiredMode = Schema.RequiredMode.REQUIRED, example = "division") + @NotBlank(message = "节点类型不能为空") + private String nodeType; + + @Schema(description = "来源类型:catalog-从标准库引用, manual-手工录入", example = "manual") + private String sourceType; + + @Schema(description = "引用清单目录ID(分部/清单用)", example = "1") + private Long sourceBoqCatalogId; + + @Schema(description = "引用清单项树ID", example = "1") + private Long sourceBoqItemTreeId; + + @Schema(description = "引用定额基价ID(定额用)", example = "1") + private Long sourceQuotaItemId; + + @Schema(description = "编码", example = "010101") + @Size(max = 50, message = "编码长度不能超过50个字符") + private String code; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "土石方工程") + @NotBlank(message = "名称不能为空") + @Size(max = 500, message = "名称长度不能超过500个字符") + private String name; + + @Schema(description = "项目特征", example = "1.土壤类别:一类土") + private String feature; + + @Schema(description = "单位(字典)", example = "m3") + @Size(max = 20, message = "单位长度不能超过20个字符") + private String unit; + + @Schema(description = "工程量", example = "100.00") + private BigDecimal qty; + + @Schema(description = "费用代号(只允许英文字符串,代表合价的值)", example = "CLF") + @Size(max = 50, message = "费用代号长度不能超过50个字符") + @javax.validation.constraints.Pattern(regexp = "^[a-zA-Z]*$", message = "费用代号只允许英文字母") + private String costCode; + + @Schema(description = "费率", example = "3.5") + private java.math.BigDecimal rate; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "定额工程量公式(如 QDL*2,QDL代表清单工程量)", example = "QDL*2") + @Size(max = 200, message = "公式长度不能超过200个字符") + private String quotaQtyFormula; + + @Schema(description = "扩展属性(包含基数范围等)") + private Map attributes; + + @Schema(description = "标签页类型:division/measure/other/unit_summary(创建时指定,子节点自动继承父节点)", example = "measure") + private String tabType; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqDivisionSwapSortReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqDivisionSwapSortReqVO.java new file mode 100644 index 0000000..8491ad7 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqDivisionSwapSortReqVO.java @@ -0,0 +1,21 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 工作台分部分项树 交换排序 Request VO + */ +@Schema(description = "工作台 - 分部分项树交换排序 Request VO") +@Data +public class WbBoqDivisionSwapSortReqVO { + + @Schema(description = "节点ID1", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "节点ID1不能为空") + private Long id1; + + @Schema(description = "节点ID2", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "节点ID2不能为空") + private Long id2; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqMarketMaterialRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqMarketMaterialRespVO.java new file mode 100644 index 0000000..9c41db7 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqMarketMaterialRespVO.java @@ -0,0 +1,177 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import lombok.Data; + +/** + * 工作台市场主材设备 Response VO + * + * @author yhy + */ +@Schema(description = "工作台 - 市场主材设备 Response VO") +@Data +public class WbBoqMarketMaterialRespVO { + + @Schema(description = "主键ID", example = "1") + private Long id; + + @Schema(description = "分部分项ID", example = "1") + private Long divisionId; + + @Schema(description = "引用工料机ID", example = "1") + private Long sourceResourceItemId; + + @Schema(description = "引用定额市场主材设备ID", example = "1") + private Long sourceMarketMaterialId; + + @Schema(description = "资源类型", example = "material") + private String resourceType; + + @Schema(description = "编码", example = "C001") + private String code; + + @Schema(description = "名称", example = "水泥") + private String name; + + @Schema(description = "规格型号", example = "P.O 42.5") + private String spec; + + @Schema(description = "单位", example = "t") + private String unit; + + @Schema(description = "消耗量", example = "1.5") + private BigDecimal consumeQty; + + @Schema(description = "调整消耗量", example = "1.6") + private BigDecimal adjustConsumeQty; + + @Schema(description = "税率", example = "0.13") + private BigDecimal taxRate; + + @Schema(description = "除税基价", example = "450.00") + private BigDecimal taxExclBasePrice; + + @Schema(description = "含税基价", example = "508.50") + private BigDecimal taxInclBasePrice; + + @Schema(description = "除税编制价", example = "460.00") + private BigDecimal taxExclCompilePrice; + + @Schema(description = "含税编制价", example = "519.80") + private BigDecimal taxInclCompilePrice; + + @Schema(description = "调整系数", example = "1.0") + private BigDecimal adjustRate; + + @Schema(description = "机类ID", example = "1") + private Long categoryId; + + @Schema(description = "父工料机ID", example = "1") + private Long parentId; + + @Schema(description = "来源类型", example = "system") + private String sourceType; + + @Schema(description = "用量", example = "100") + private BigDecimal usageQty; + + @Schema(description = "排序号", example = "1") + private Integer sortOrder; + + @Schema(description = "扩展属性") + private Map attributes; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; + + // ========== 虚拟字段 ========== + + @Schema(description = "除税基价合价", example = "675.00") + private BigDecimal taxExclBaseTotalSum; + + @Schema(description = "含税基价合价", example = "762.75") + private BigDecimal taxInclBaseTotalSum; + + @Schema(description = "除税编制价合价", example = "690.00") + private BigDecimal taxExclCompileTotalSum; + + @Schema(description = "含税编制价合价", example = "779.70") + private BigDecimal taxInclCompileTotalSum; + + @Schema(description = "计算基数(单位为%的工料机使用)") + private Map calcBase; + + @Schema(description = "调整公式(虚拟字段)", example = "((x*1.2)+0.5)") + private String adjustmentFormula; + + @Schema(description = "是否复合工料机", example = "false") + private Boolean isMerged; + + @Schema(description = "复合工料机子数据列表") + private List mergedItems; + + /** + * 复合工料机子数据 VO + */ + @Schema(description = "复合工料机子数据") + @Data + public static class MergedItemVO { + @Schema(description = "子数据ID", example = "1") + private Long id; + + @Schema(description = "源工料机ID", example = "100") + private Long resourceItemId; + + @Schema(description = "编码", example = "C001") + private String code; + + @Schema(description = "名称", example = "水泥") + private String name; + + @Schema(description = "单位", example = "t") + private String unit; + + @Schema(description = "规格型号", example = "P.O 42.5") + private String spec; + + @Schema(description = "资源类型", example = "material") + private String resourceType; + + @Schema(description = "税率", example = "0.13") + private BigDecimal taxRate; + + @Schema(description = "除税基价", example = "100.00") + private BigDecimal taxExclBasePrice; + + @Schema(description = "含税基价", example = "113.00") + private BigDecimal taxInclBasePrice; + + @Schema(description = "除税编制价", example = "120.00") + private BigDecimal taxExclCompilePrice; + + @Schema(description = "含税编制价", example = "135.60") + private BigDecimal taxInclCompilePrice; + + @Schema(description = "消耗量", example = "1.5") + private BigDecimal consumeQty; + + @Schema(description = "除税基价合价", example = "150.00") + private BigDecimal taxExclBaseTotalSum; + + @Schema(description = "含税基价合价", example = "169.50") + private BigDecimal taxInclBaseTotalSum; + + @Schema(description = "除税编制价合价", example = "180.00") + private BigDecimal taxExclCompileTotalSum; + + @Schema(description = "含税编制价合价", example = "203.40") + private BigDecimal taxInclCompileTotalSum; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqMarketMaterialSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqMarketMaterialSaveReqVO.java new file mode 100644 index 0000000..88de23e --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqMarketMaterialSaveReqVO.java @@ -0,0 +1,87 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.util.Map; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 工作台市场主材设备保存 Request VO + * + * @author yhy + */ +@Schema(description = "工作台 - 市场主材设备保存 Request VO") +@Data +public class WbBoqMarketMaterialSaveReqVO { + + @Schema(description = "主键ID(更新时必填)", example = "1") + private Long id; + + @Schema(description = "分部分项ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "分部分项ID不能为空") + private Long divisionId; + + @Schema(description = "引用工料机ID", example = "1") + private Long sourceResourceItemId; + + @Schema(description = "引用定额市场主材设备ID", example = "1") + private Long sourceMarketMaterialId; + + @Schema(description = "资源类型", example = "material") + private String resourceType; + + @Schema(description = "编码", example = "C001") + private String code; + + @Schema(description = "名称", example = "水泥") + private String name; + + @Schema(description = "规格型号", example = "P.O 42.5") + private String spec; + + @Schema(description = "单位", example = "t") + private String unit; + + @Schema(description = "消耗量", example = "1.5") + private BigDecimal consumeQty; + + @Schema(description = "调整消耗量", example = "1.6") + private BigDecimal adjustConsumeQty; + + @Schema(description = "税率", example = "0.13") + private BigDecimal taxRate; + + @Schema(description = "除税基价", example = "450.00") + private BigDecimal taxExclBasePrice; + + @Schema(description = "含税基价", example = "508.50") + private BigDecimal taxInclBasePrice; + + @Schema(description = "除税编制价", example = "460.00") + private BigDecimal taxExclCompilePrice; + + @Schema(description = "含税编制价", example = "519.80") + private BigDecimal taxInclCompilePrice; + + @Schema(description = "调整系数", example = "1.0") + private BigDecimal adjustRate; + + @Schema(description = "机类ID", example = "1") + private Long categoryId; + + @Schema(description = "父工料机ID", example = "1") + private Long parentId; + + @Schema(description = "来源类型", example = "system") + private String sourceType; + + @Schema(description = "用量", example = "100") + private BigDecimal usageQty; + + @Schema(description = "排序号", example = "1") + private Integer sortOrder; + + @Schema(description = "扩展属性") + private Map attributes; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqResourceBatchUpdateResultVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqResourceBatchUpdateResultVO.java new file mode 100644 index 0000000..97f1806 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqResourceBatchUpdateResultVO.java @@ -0,0 +1,34 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.Data; + +/** + * 工作台 - 工料机批量更新结果 Response VO + * 用于返回编制价/价格来源批量同步的影响信息 + */ +@Schema(description = "工作台 - 工料机批量更新结果 Response VO") +@Data +public class WbBoqResourceBatchUpdateResultVO { + + @Schema(description = "总影响行数", example = "5") + private int totalAffectedRows; + + @Schema(description = "受影响的单位工程列表") + private List affectedUnits; + + @Schema(description = "受影响的单位工程信息") + @Data + public static class AffectedUnitInfo { + + @Schema(description = "单位工程ID(compile_tree_id)", example = "1") + private Long compileTreeId; + + @Schema(description = "单位工程名称", example = "1号楼") + private String unitName; + + @Schema(description = "该单位工程下影响的行数", example = "3") + private int affectedRows; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqResourceCategorySummaryVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqResourceCategorySummaryVO.java new file mode 100644 index 0000000..da0b161 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqResourceCategorySummaryVO.java @@ -0,0 +1,32 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 工料机分类汇总 VO + */ +@Schema(description = "工料机分类汇总") +@Data +public class WbBoqResourceCategorySummaryVO { + + @Schema(description = "序号") + private Integer index; + + @Schema(description = "类别(人、材、机)") + private String category; + + @Schema(description = "除税基价合价") + private BigDecimal totalBasePriceExTax; + + @Schema(description = "含税基价合价") + private BigDecimal totalBasePriceInTax; + + @Schema(description = "除税编制价合价") + private BigDecimal totalCompilePriceExTax; + + @Schema(description = "含税编制价合价") + private BigDecimal totalCompilePriceInTax; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqResourceCreateBlankReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqResourceCreateBlankReqVO.java new file mode 100644 index 0000000..90795b0 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqResourceCreateBlankReqVO.java @@ -0,0 +1,20 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 工作台 - 创建空白工料机行 Request VO + */ +@Schema(description = "工作台 - 创建空白工料机行 Request VO") +@Data +public class WbBoqResourceCreateBlankReqVO { + + @Schema(description = "关联定额节点ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "定额节点ID不能为空") + private Long divisionId; + + @Schema(description = "在此工料机行下方插入(可选,不传则追加到末尾)", example = "1") + private Long afterResourceId; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqResourceFillReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqResourceFillReqVO.java new file mode 100644 index 0000000..6b92bac --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqResourceFillReqVO.java @@ -0,0 +1,55 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 工作台 - 双击填充工料机数据 Request VO + * 从弹窗查询结果复制数据到空白行 + */ +@Schema(description = "工作台 - 双击填充工料机数据 Request VO") +@Data +public class WbBoqResourceFillReqVO { + + @Schema(description = "空白行ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "空白行ID不能为空") + private Long id; + + @Schema(description = "后台工料机ID", example = "1") + private Long sourceResourceItemId; + + @Schema(description = "编码", example = "C001") + private String code; + + @Schema(description = "名称", example = "水泥") + private String name; + + @Schema(description = "型号规格", example = "P.O 42.5") + private String spec; + + @Schema(description = "单位", example = "t") + private String unit; + + @Schema(description = "类型:labor-人工, material-材料, machine-机械", example = "material") + private String resourceType; + + @Schema(description = "税率", example = "0.09") + private BigDecimal taxRate; + + @Schema(description = "除税基价", example = "450.00") + private BigDecimal taxExclBasePrice; + + @Schema(description = "含税基价", example = "490.50") + private BigDecimal taxInclBasePrice; + + @Schema(description = "除税编制价", example = "460.00") + private BigDecimal taxExclCompilePrice; + + @Schema(description = "含税编制价", example = "501.40") + private BigDecimal taxInclCompilePrice; + + @Schema(description = "机类ID(跨专业超出范围时可为null)", example = "1") + private Long categoryId; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqResourceRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqResourceRespVO.java new file mode 100644 index 0000000..cb3a232 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqResourceRespVO.java @@ -0,0 +1,130 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import lombok.Data; + +/** + * 工作台工料机消耗 Response VO + */ +@Schema(description = "工作台 - 工料机消耗 Response VO") +@Data +public class WbBoqResourceRespVO { + + @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "关联定额节点ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long divisionId; + + @Schema(description = "引用工料机ID", example = "1") + private Long sourceResourceItemId; + + @Schema(description = "引用定额工料机组成ID", example = "1") + private Long sourceQuotaResourceId; + + @Schema(description = "类型:labor-人工, material-材料, machine-机械", example = "material") + private String resourceType; + + @Schema(description = "编码", example = "C001") + private String code; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "水泥") + private String name; + + @Schema(description = "规格型号", example = "P.O 42.5") + private String spec; + + @Schema(description = "单位", example = "t") + private String unit; + + @Schema(description = "消耗量", example = "0.5") + private BigDecimal consumeQty; + + @Schema(description = "调整消耗量(用户手动覆写,null表示使用原始逻辑值)", example = "0.6") + private BigDecimal adjustConsumeQty; + + @Schema(description = "除税基价", example = "450.00") + private BigDecimal taxExclBasePrice; + + @Schema(description = "含税基价", example = "490.50") + private BigDecimal taxInclBasePrice; + + @Schema(description = "除税编制价", example = "460.00") + private BigDecimal taxExclCompilePrice; + + @Schema(description = "含税编制价", example = "501.40") + private BigDecimal taxInclCompilePrice; + + @Schema(description = "税率", example = "0.09") + private BigDecimal taxRate; + + @Schema(description = "调整系数", example = "1.0") + private BigDecimal adjustRate; + + @Schema(description = "实际单价(虚拟字段,内存计算:basePrice * adjustRate)", example = "450.00") + private BigDecimal price; + + @Schema(description = "合价(虚拟字段,内存计算:consumeQty * price)", example = "225.00") + private BigDecimal amount; + + @Schema(description = "除税基价合价(虚拟字段)", example = "50.00") + private BigDecimal taxExclBaseTotalSum; + + @Schema(description = "含税基价合价(虚拟字段)", example = "54.50") + private BigDecimal taxInclBaseTotalSum; + + @Schema(description = "除税编制价合价(虚拟字段)", example = "55.00") + private BigDecimal taxExclCompileTotalSum; + + @Schema(description = "含税编制价合价(虚拟字段)", example = "59.95") + private BigDecimal taxInclCompileTotalSum; + + @Schema(description = "除税合价(虚拟字段,用量×除税编制价)", example = "165.00") + private BigDecimal taxExclTotalSum; + + @Schema(description = "含税合价(虚拟字段,用量×含税编制价)", example = "179.85") + private BigDecimal taxInclTotalSum; + + @Schema(description = "计算基数(单位为%的工料机使用)") + private Map calcBase; + + @Schema(description = "调整公式(虚拟字段)", example = "((x*1.2)+0.5)") + private String adjustmentFormula; + + @Schema(description = "用量(工作台独有字段)", example = "10.00") + private BigDecimal usageQty; + + @Schema(description = "机类ID", example = "1") + private Long categoryId; + + @Schema(description = "机类代码", example = "人") + private String categoryCode; + + @Schema(description = "来源类型:system-从定额复制, tenant-租户手动添加", example = "system") + private String sourceType; + + @Schema(description = "价格来源显示文本(虚拟字段)", example = "信*01*广东*珠海*金湾") + private String priceSourceText; + + @Schema(description = "是否有当前价格来源(虚拟字段,用于前端显示蓝色字体)", example = "true") + private Boolean hasPriceSource; + + @Schema(description = "排序号", example = "1") + private Integer sortOrder; + + @Schema(description = "扩展属性") + private Map attributes; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; + + @Schema(description = "子工料机列表(复合工料机展开)") + private List children; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqResourceSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqResourceSaveReqVO.java new file mode 100644 index 0000000..0678239 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbBoqResourceSaveReqVO.java @@ -0,0 +1,77 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import lombok.Data; + +/** + * 工作台工料机消耗 创建/更新 Request VO + */ +@Schema(description = "工作台 - 工料机消耗创建/更新 Request VO") +@Data +public class WbBoqResourceSaveReqVO { + + @Schema(description = "ID(更新时必填)", example = "1") + private Long id; + + @Schema(description = "关联定额节点ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "定额节点ID不能为空") + private Long divisionId; + + @Schema(description = "引用工料机ID", example = "1") + private Long sourceResourceItemId; + + @Schema(description = "引用定额工料机组成ID", example = "1") + private Long sourceQuotaResourceId; + + @Schema(description = "类型:labor-人工, material-材料, machine-机械", example = "material") + @Size(max = 20, message = "类型长度不能超过20个字符") + private String resourceType; + + @Schema(description = "编码", example = "C001") + @Size(max = 50, message = "编码长度不能超过50个字符") + private String code; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "水泥") + @NotBlank(message = "名称不能为空") + @Size(max = 200, message = "名称长度不能超过200个字符") + private String name; + + @Schema(description = "规格型号", example = "P.O 42.5") + @Size(max = 500, message = "规格型号长度不能超过500个字符") + private String spec; + + @Schema(description = "单位", example = "t") + @Size(max = 20, message = "单位长度不能超过20个字符") + private String unit; + + @Schema(description = "消耗量", example = "0.5") + private BigDecimal consumeQty; + + @Schema(description = "调整消耗量(用户手动覆写,null表示使用原始逻辑值)", example = "0.6") + private BigDecimal adjustConsumeQty; + + @Schema(description = "调整系数", example = "1.0") + private BigDecimal adjustRate; + + @Schema(description = "税率", example = "0.09") + private BigDecimal taxRate; + + @Schema(description = "除税基价", example = "450.00") + private BigDecimal taxExclBasePrice; + + @Schema(description = "含税基价", example = "490.50") + private BigDecimal taxInclBasePrice; + + @Schema(description = "除税编制价", example = "460.00") + private BigDecimal taxExclCompilePrice; + + @Schema(description = "含税编制价", example = "501.40") + private BigDecimal taxInclCompilePrice; + + @Schema(description = "用量", example = "10.00") + private BigDecimal usageQty; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbCompileTreeRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbCompileTreeRespVO.java new file mode 100644 index 0000000..433fb4e --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbCompileTreeRespVO.java @@ -0,0 +1,48 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import lombok.Data; + +/** + * 编制模式树 Response VO + */ +@Schema(description = "工作台 - 编制模式树 Response VO") +@Data +public class WbCompileTreeRespVO { + + @Schema(description = "节点ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "项目节点ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long projectId; + + @Schema(description = "父节点ID", example = "1") + private Long parentId; + + @Schema(description = "节点类型:root-根节点, item-单项, unit-单位工程", requiredMode = Schema.RequiredMode.REQUIRED, example = "item") + private String nodeType; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "单项1") + private String name; + + @Schema(description = "排序号", example = "1") + private Integer sortOrder; + + @Schema(description = "层级路径") + private String[] path; + + @Schema(description = "扩展属性") + private Map attributes; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; + + @Schema(description = "子节点列表") + private List children; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbCompileTreeSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbCompileTreeSaveReqVO.java new file mode 100644 index 0000000..15916b5 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbCompileTreeSaveReqVO.java @@ -0,0 +1,34 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import lombok.Data; + +/** + * 编制模式树 创建/更新 Request VO + */ +@Schema(description = "工作台 - 编制模式树创建/更新 Request VO") +@Data +public class WbCompileTreeSaveReqVO { + + @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 = "父节点ID", example = "1") + private Long parentId; + + @Schema(description = "节点类型:item-单项, unit-单位工程", requiredMode = Schema.RequiredMode.REQUIRED, example = "item") + @NotBlank(message = "节点类型不能为空") + private String nodeType; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "单项1") + @NotBlank(message = "名称不能为空") + @Size(max = 200, message = "名称长度不能超过200个字符") + private String name; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbCompileTreeSwapSortReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbCompileTreeSwapSortReqVO.java new file mode 100644 index 0000000..969142d --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbCompileTreeSwapSortReqVO.java @@ -0,0 +1,21 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 编制模式树 交换排序 Request VO + */ +@Schema(description = "工作台 - 编制模式树交换排序 Request VO") +@Data +public class WbCompileTreeSwapSortReqVO { + + @Schema(description = "节点1 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "节点1 ID不能为空") + private Long nodeId1; + + @Schema(description = "节点2 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "节点2 ID不能为空") + private Long nodeId2; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbItemInfoRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbItemInfoRespVO.java new file mode 100644 index 0000000..ae3b9a1 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbItemInfoRespVO.java @@ -0,0 +1,32 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import java.util.Map; +import lombok.Data; + +/** + * 单项基本信息 Response VO + */ +@Schema(description = "工作台 - 单项基本信息 Response VO") +@Data +public class WbItemInfoRespVO { + + @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "编制树节点ID(单项节点)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long compileTreeId; + + @Schema(description = "项目界面配置快照") + private Map configSnapshot; + + @Schema(description = "基本信息数据(用户填写的值)") + private Map infoData; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbItemInfoSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbItemInfoSaveReqVO.java new file mode 100644 index 0000000..32cec72 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbItemInfoSaveReqVO.java @@ -0,0 +1,21 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Map; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 单项基本信息 保存 Request VO + */ +@Schema(description = "工作台 - 单项基本信息保存 Request VO") +@Data +public class WbItemInfoSaveReqVO { + + @Schema(description = "编制树节点ID(单项节点)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "编制树节点ID不能为空") + private Long compileTreeId; + + @Schema(description = "基本信息数据(用户填写的值)") + private Map infoData; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbProjectTreeRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbProjectTreeRespVO.java new file mode 100644 index 0000000..c510d7b --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbProjectTreeRespVO.java @@ -0,0 +1,107 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import lombok.Data; + +/** + * 工作台项目管理树 Response VO + */ +@Schema(description = "工作台 - 项目管理树 Response VO") +@Data +public class WbProjectTreeRespVO { + + @Schema(description = "节点ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "父节点ID", example = "0") + private Long parentId; + + @Schema(description = "节点类型:directory-目录, project-项目", requiredMode = Schema.RequiredMode.REQUIRED, example = "directory") + private String nodeType; + + @Schema(description = "名称(目录名称或项目名称)", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026年项目") + private String name; + + // ========== 以下字段仅项目节点使用 ========== + + @Schema(description = "项目编号(仅项目节点)", example = "PRJ-2026-001") + private String projectCode; + + @Schema(description = "行业-省市ID(仅项目节点)", example = "1") + private Long industryProvinceId; + + @Schema(description = "行业-省市名称(仅项目节点)", example = "广东") + private String industryProvinceName; + + @Schema(description = "行业ID(仅项目节点)", example = "2") + private Long industryId; + + @Schema(description = "行业名称(仅项目节点)", example = "建筑工程") + private String industryName; + + @Schema(description = "信息价专业ID(仅项目节点)", example = "100") + private Long infoPriceProfessionId; + + @Schema(description = "信息价专业名称(仅项目节点)", example = "建筑工程") + private String infoPriceProfessionName; + + @Schema(description = "信息价专业完整路径(仅项目节点)") + private Long[] infoPriceProfessionPath; + + @Schema(description = "信息价地区ID(仅项目节点)", example = "200") + private Long infoPriceRegionId; + + @Schema(description = "信息价地区名称(仅项目节点)", example = "深圳市") + private String infoPriceRegionName; + + @Schema(description = "信息价地区完整路径(仅项目节点)") + private Long[] infoPriceRegionPath; + + @Schema(description = "信息价册ID(仅项目节点)", example = "300") + private Long infoPriceBookId; + + @Schema(description = "信息价册名称(仅项目节点)", example = "2024年1月深圳建筑工程信息价") + private String infoPriceBookName; + + @Schema(description = "工作内容(仅项目节点)", example = "bid_control_price") + private String workContent; + + @Schema(description = "文件类型(仅项目节点)", example = "bid_document") + private String fileType; + + @Schema(description = "参与人员ID数组(仅项目节点)") + private Long[] memberIds; + + @Schema(description = "项目状态(仅项目节点)", example = "draft") + private String status; + + @Schema(description = "备注", example = "这是一个测试项目") + private String remark; + + @Schema(description = "是否已保存至历史库", example = "false") + private Boolean inHistoryLibrary; + + @Schema(description = "排序号", example = "1") + private Integer sortOrder; + + @Schema(description = "层级路径") + private String[] path; + + @Schema(description = "快照数据(仅项目节点)") + private Map snapshotJson; + + @Schema(description = "扩展属性") + private Map attributes; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; + + @Schema(description = "子节点列表") + private List children; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbProjectTreeSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbProjectTreeSaveReqVO.java new file mode 100644 index 0000000..c463fa8 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbProjectTreeSaveReqVO.java @@ -0,0 +1,83 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import lombok.Data; + +/** + * 工作台项目管理树 创建/更新 Request VO + */ +@Schema(description = "工作台 - 项目管理树创建/更新 Request VO") +@Data +public class WbProjectTreeSaveReqVO { + + @Schema(description = "节点ID(更新时必填)", example = "1") + private Long id; + + @Schema(description = "父节点ID", example = "0") + private Long parentId; + + @Schema(description = "节点类型:directory-目录, project-项目", requiredMode = Schema.RequiredMode.REQUIRED, example = "directory") + @NotBlank(message = "节点类型不能为空") + private String nodeType; + + @Schema(description = "名称(目录名称或项目名称)", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026年项目") + @NotBlank(message = "名称不能为空") + @Size(max = 200, message = "名称长度不能超过200个字符") + private String name; + + // ========== 以下字段仅项目节点使用 ========== + + @Schema(description = "项目编号(仅项目节点)", example = "PRJ-2026-001") + @Size(max = 50, message = "项目编号长度不能超过50个字符") + private String projectCode; + + @Schema(description = "行业-省市ID(仅项目节点)", example = "1") + private Long industryProvinceId; + + @Schema(description = "行业ID(仅项目节点)", example = "2") + private Long industryId; + + @Schema(description = "信息价专业ID(仅项目节点)", example = "100") + private Long infoPriceProfessionId; + + @Schema(description = "信息价专业字典值(仅项目节点,字典:info_profession)", example = "profession_1") + private String infoPriceProfessionType; + + @Schema(description = "信息价地区ID(仅项目节点)", example = "200") + private Long infoPriceRegionId; + + @Schema(description = "信息价册ID(仅项目节点)", example = "300") + private Long infoPriceBookId; + + @Schema(description = "工作内容(仅项目节点,字典:wb_work_content)", example = "bid_control_price") + private String workContent; + + @Schema(description = "文件类型(仅项目节点,字典:wb_file_type)", example = "bid_document") + private String fileType; + + @Schema(description = "参与人员ID数组(仅项目节点)", example = "[1, 2, 3]") + private Long[] memberIds; + + @Schema(description = "项目状态(仅项目节点)", example = "active") + private String status; + + @Schema(description = "备注", example = "这是一个测试项目") + private String remark; + + // ========== 插入位置控制字段 ========== + + @Schema(description = "参考节点ID(用于上方/下方插入时指定位置)", example = "100") + private Long referenceNodeId; + + @Schema(description = "插入位置:above-上方插入, below-下方插入, end-尾部插入(默认)", example = "below") + private String insertPosition; + + /** 插入位置常量:上方插入 */ + public static final String INSERT_POSITION_ABOVE = "above"; + /** 插入位置常量:下方插入 */ + public static final String INSERT_POSITION_BELOW = "below"; + /** 插入位置常量:尾部插入(默认) */ + public static final String INSERT_POSITION_END = "end"; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbProjectTreeSwapSortReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbProjectTreeSwapSortReqVO.java new file mode 100644 index 0000000..0895029 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbProjectTreeSwapSortReqVO.java @@ -0,0 +1,21 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 工作台项目管理树 交换排序 Request VO + */ +@Schema(description = "工作台 - 项目管理树交换排序 Request VO") +@Data +public class WbProjectTreeSwapSortReqVO { + + @Schema(description = "节点1 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "节点1 ID不能为空") + private Long nodeId1; + + @Schema(description = "节点2 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "节点2 ID不能为空") + private Long nodeId2; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbResourceSearchRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbResourceSearchRespVO.java new file mode 100644 index 0000000..d3b0fd9 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbResourceSearchRespVO.java @@ -0,0 +1,56 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import lombok.Data; + +/** + * 工作台 - 工料机编码查询 Response VO + * 用于新增工料机时弹窗查询 + */ +@Schema(description = "工作台 - 工料机编码查询 Response VO") +@Data +public class WbResourceSearchRespVO { + + @Schema(description = "工料机ID(本项目=wb_boq_resource.sourceResourceItemId,非项目=resource_item.id)") + private Long id; + + @Schema(description = "编码") + private String code; + + @Schema(description = "名称") + private String name; + + @Schema(description = "型号规格") + private String spec; + + @Schema(description = "单位") + private String unit; + + @Schema(description = "数据来源:project=本项目, standard=非项目") + private String source; + + @Schema(description = "类型:labor-人工, material-材料, machine-机械") + private String resourceType; + + @Schema(description = "税率") + private BigDecimal taxRate; + + @Schema(description = "除税基价") + private BigDecimal taxExclBasePrice; + + @Schema(description = "含税基价") + private BigDecimal taxInclBasePrice; + + @Schema(description = "除税编制价") + private BigDecimal taxExclCompilePrice; + + @Schema(description = "含税编制价") + private BigDecimal taxInclCompilePrice; + + @Schema(description = "机类ID") + private Long categoryId; + + @Schema(description = "后台工料机ID(用于创建时引用)") + private Long sourceResourceItemId; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbResourceSourceBoqRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbResourceSourceBoqRespVO.java new file mode 100644 index 0000000..90f1154 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbResourceSourceBoqRespVO.java @@ -0,0 +1,33 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import lombok.Data; + +/** + * 工料机来源-清单 Response VO + * + * @author yhy + */ +@Schema(description = "工作台 - 工料机来源清单 Response VO") +@Data +public class WbResourceSourceBoqRespVO { + + @Schema(description = "清单ID", example = "1") + private Long boqId; + + @Schema(description = "清单编码", example = "010101001001") + private String boqCode; + + @Schema(description = "清单名称", example = "挖一般土方") + private String boqName; + + @Schema(description = "单位", example = "m3") + private String unit; + + @Schema(description = "工程量", example = "100.00") + private BigDecimal qty; + + @Schema(description = "标签页类型:division/measure/other/unit_summary", example = "division") + private String tabType; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbResourceSourceUnitRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbResourceSourceUnitRespVO.java new file mode 100644 index 0000000..2855ced --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbResourceSourceUnitRespVO.java @@ -0,0 +1,23 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 工料机来源-单位工程 Response VO + * + * @author yhy + */ +@Schema(description = "工作台 - 工料机来源单位工程 Response VO") +@Data +public class WbResourceSourceUnitRespVO { + + @Schema(description = "编制模式树ID(单位工程节点)", example = "1") + private Long compileTreeId; + + @Schema(description = "单位工程名称", example = "1#楼") + private String unitName; + + @Schema(description = "该单位工程下的工料机数量", example = "5") + private Integer resourceCount; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbResourceSummaryRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbResourceSummaryRespVO.java new file mode 100644 index 0000000..fc45581 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbResourceSummaryRespVO.java @@ -0,0 +1,72 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import lombok.Data; + +/** + * 工料机汇总 Response VO + * + * @author yhy + */ +@Schema(description = "工作台 - 工料机汇总 Response VO") +@Data +public class WbResourceSummaryRespVO { + + @Schema(description = "汇总记录ID(可能为空,表示尚未创建用户标记)", example = "1") + private Long id; + + @Schema(description = "资源唯一键", requiredMode = Schema.RequiredMode.REQUIRED, example = "abc123") + private String resourceKey; + + @Schema(description = "是否打印", example = "false") + private Boolean isPrint; + + @Schema(description = "编码", example = "C001") + private String code; + + @Schema(description = "名称", example = "水泥") + private String name; + + @Schema(description = "型号规格", example = "P.O 42.5") + private String spec; + + @Schema(description = "单位", example = "t") + private String unit; + + @Schema(description = "资源类型", example = "material") + private String resourceType; + + @Schema(description = "税率", example = "0.13") + private BigDecimal taxRate; + + @Schema(description = "总数量(汇总)", example = "100.5") + private BigDecimal totalQty; + + @Schema(description = "除税基价", example = "400.00") + private BigDecimal taxExclBasePrice; + + @Schema(description = "含税基价", example = "452.00") + private BigDecimal taxInclBasePrice; + + @Schema(description = "除税编制价", example = "450.00") + private BigDecimal taxExclCompilePrice; + + @Schema(description = "含税编制价", example = "508.50") + private BigDecimal taxInclCompilePrice; + + @Schema(description = "除税合价", example = "45225.00") + private BigDecimal taxExclTotalPrice; + + @Schema(description = "含税合价", example = "51104.25") + private BigDecimal taxInclTotalPrice; + + @Schema(description = "是否评标指定材料", example = "false") + private Boolean isBidMaterial; + + @Schema(description = "价格来源", example = "信息价") + private String priceSource; + + @Schema(description = "备注", example = "") + private String remark; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbResourceSummaryTreeRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbResourceSummaryTreeRespVO.java new file mode 100644 index 0000000..15ffe7b --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbResourceSummaryTreeRespVO.java @@ -0,0 +1,39 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.Data; + +/** + * 工料机汇总树 Response VO + * + * @author yhy + */ +@Schema(description = "工作台 - 工料机汇总树 Response VO") +@Data +public class WbResourceSummaryTreeRespVO { + + @Schema(description = "节点ID", example = "root") + private String id; + + @Schema(description = "节点名称", example = "工料机汇总") + private String name; + + @Schema(description = "节点类型:root/category/bid_material", example = "root") + private String nodeType; + + @Schema(description = "资源类型(类别节点时有值)", example = "material") + private String resourceType; + + @Schema(description = "数量", example = "10") + private Integer count; + + @Schema(description = "子节点") + private List children; + + // 节点类型常量 + public static final String NODE_TYPE_ROOT = "root"; + public static final String NODE_TYPE_CATEGORY = "category"; + public static final String NODE_TYPE_BID_MATERIAL = "bid_material"; + +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbResourceSummaryUpdateReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbResourceSummaryUpdateReqVO.java new file mode 100644 index 0000000..3224bbc --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbResourceSummaryUpdateReqVO.java @@ -0,0 +1,39 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 工料机汇总更新 Request VO + * + * @author yhy + */ +@Schema(description = "工作台 - 工料机汇总更新 Request VO") +@Data +public class WbResourceSummaryUpdateReqVO { + + @Schema(description = "项目ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "项目ID不能为空") + private Long projectId; + + @Schema(description = "资源唯一键", requiredMode = Schema.RequiredMode.REQUIRED, example = "abc123") + @NotBlank(message = "资源唯一键不能为空") + private String resourceKey; + + @Schema(description = "是否打印", example = "true") + private Boolean isPrint; + + @Schema(description = "是否评标指定材料", example = "true") + private Boolean isBidMaterial; + + @Schema(description = "价格来源", example = "信息价") + private String priceSource; + + @Schema(description = "备注", example = "") + private String remark; + + @Schema(description = "新编码(修改编码时使用,会同步到工作台工料机)", example = "C002") + private String newCode; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbUnifiedFeeConfigRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbUnifiedFeeConfigRespVO.java new file mode 100644 index 0000000..2296070 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbUnifiedFeeConfigRespVO.java @@ -0,0 +1,37 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Map; +import lombok.Data; + +/** + * 工作台统一取费配置 Response VO + */ +@Schema(description = "工作台 - 统一取费配置 Response VO") +@Data +public class WbUnifiedFeeConfigRespVO { + + @Schema(description = "主键ID") + private Long id; + + @Schema(description = "单位工程节点ID") + private Long compileTreeId; + + @Schema(description = "分部分项节点ID(范围)") + private Long divisionId; + + @Schema(description = "来源统一取费设置ID") + private Long sourceUnifiedFeeSettingId; + + @Schema(description = "费率模式ID") + private Long rateModeId; + + @Schema(description = "配置数据") + private Map configData; + + @Schema(description = "扩展属性") + private Map attributes; + + @Schema(description = "排序字段") + private Integer sortOrder; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbUnifiedFeeConfigSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbUnifiedFeeConfigSaveReqVO.java new file mode 100644 index 0000000..0b31225 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbUnifiedFeeConfigSaveReqVO.java @@ -0,0 +1,41 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotNull; +import java.util.Map; +import lombok.Data; + +/** + * 工作台统一取费配置 Save Request VO + */ +@Schema(description = "工作台 - 统一取费配置 Save Request VO") +@Data +public class WbUnifiedFeeConfigSaveReqVO { + + @Schema(description = "主键ID(更新时必填)") + private Long id; + + @Schema(description = "单位工程节点ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "单位工程节点ID不能为空") + private Long compileTreeId; + + @Schema(description = "分部分项节点ID(范围)") + private Long divisionId; + + @Schema(description = "来源统一取费设置ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "来源统一取费设置ID不能为空") + private Long sourceUnifiedFeeSettingId; + + @Schema(description = "费率模式ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "费率模式ID不能为空") + private Long rateModeId; + + @Schema(description = "配置数据") + private Map configData; + + @Schema(description = "扩展属性") + private Map attributes; + + @Schema(description = "排序字段") + private Integer sortOrder; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbUnifiedFeeSummaryItemVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbUnifiedFeeSummaryItemVO.java new file mode 100644 index 0000000..ba98216 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbUnifiedFeeSummaryItemVO.java @@ -0,0 +1,32 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "工作台 - 统一取费汇总来源项") +@Data +public class WbUnifiedFeeSummaryItemVO { + + @Schema(description = "节点ID") + private Long id; + + @Schema(description = "编码") + private String code; + + @Schema(description = "类别") + private String category; + + @Schema(description = "名称") + private String name; + + @Schema(description = "单位") + private String unit; + + @Schema(description = "工程量") + private BigDecimal quantity; + + @Schema(description = "来源定额目录节点ID(用于取费章节过滤)") + private Long sourceCatalogItemId; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbUnitInfoRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbUnitInfoRespVO.java new file mode 100644 index 0000000..9461ef3 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbUnitInfoRespVO.java @@ -0,0 +1,65 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import java.util.Map; +import lombok.Data; + +/** + * 单位工程信息 Response VO + */ +@Schema(description = "工作台 - 单位工程信息 Response VO") +@Data +public class WbUnitInfoRespVO { + + @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "编制树节点ID(单位工程节点)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long compileTreeId; + + @Schema(description = "工程编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "UNIT-001") + private String unitCode; + + @Schema(description = "工程名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "1号楼") + private String unitName; + + @Schema(description = "专业类别(字典:wb_specialty_type)", example = "architecture") + private String specialtyType; + + @Schema(description = "库类别(字典:wb_library_type)", example = "boq_pricing") + private String libraryType; + + @Schema(description = "清单数据库(清单目录树ID)", example = "100") + private Long boqCatalogItemId; + + @Schema(description = "清单数据库名称", example = "广东建筑工程清单") + private String boqCatalogItemName; + + @Schema(description = "定额数据库(定额专业树ID)", example = "200") + private Long quotaCatalogItemId; + + @Schema(description = "定额数据库名称", example = "广东建筑工程定额") + private String quotaCatalogItemName; + + @Schema(description = "执行费率文件(费率模式节点ID)", example = "300") + private Long rateModeId; + + @Schema(description = "执行费率文件名称", example = "一般计税模式") + private String rateModeName; + + @Schema(description = "建设规模(字典:wb_construction_scale)", example = "large") + private String constructionScale; + + @Schema(description = "配置快照(预留)") + private Map configSnapshot; + + @Schema(description = "扩展属性") + private Map attributes; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbUnitInfoSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbUnitInfoSaveReqVO.java new file mode 100644 index 0000000..936f807 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbUnitInfoSaveReqVO.java @@ -0,0 +1,50 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import lombok.Data; + +/** + * 单位工程信息 保存 Request VO + */ +@Schema(description = "工作台 - 单位工程信息保存 Request VO") +@Data +public class WbUnitInfoSaveReqVO { + + @Schema(description = "ID(更新时必填)", example = "1") + private Long id; + + @Schema(description = "编制树节点ID(单位工程节点)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "编制树节点ID不能为空") + private Long compileTreeId; + + @Schema(description = "工程编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "UNIT-001") + @NotBlank(message = "工程编号不能为空") + @Size(max = 50, message = "工程编号长度不能超过50个字符") + private String unitCode; + + @Schema(description = "工程名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "1号楼") + @NotBlank(message = "工程名称不能为空") + @Size(max = 200, message = "工程名称长度不能超过200个字符") + private String unitName; + + @Schema(description = "专业类别(fields_majors 节点ID)", example = "2043533844464746497") + private String specialtyType; + + @Schema(description = "库类别(字典:wb_library_type)", example = "boq_pricing") + private String libraryType; + + @Schema(description = "清单数据库(清单目录树ID)", example = "100") + private Long boqCatalogItemId; + + @Schema(description = "定额数据库(定额专业树ID)", example = "200") + private Long quotaCatalogItemId; + + @Schema(description = "执行费率文件(费率模式节点ID)", example = "300") + private Long rateModeId; + + @Schema(description = "建设规模(字典:wb_construction_scale)", example = "large") + private String constructionScale; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbUnitRateConfigSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbUnitRateConfigSaveReqVO.java new file mode 100644 index 0000000..a5c08a7 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbUnitRateConfigSaveReqVO.java @@ -0,0 +1,33 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Map; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 工作台 - 单位工程费率配置保存 Request VO + * + * @author yhy + */ +@Schema(description = "工作台 - 单位工程费率配置保存 Request VO") +@Data +public class WbUnitRateConfigSaveReqVO { + + @Schema(description = "编制树节点ID(单位工程节点)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "编制树节点ID不能为空") + private Long compileTreeId; + + @Schema(description = "定额专业ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + @NotNull(message = "定额专业ID不能为空") + private Long quotaCatalogItemId; + + @Schema(description = "费率模式ID", example = "200") + private Long rateModeId; + + @Schema(description = "费率覆写值") + private Map rateSettings; + + @Schema(description = "取费覆写值") + private Map feeSettings; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbUsedQuotaSpecialtyRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbUsedQuotaSpecialtyRespVO.java new file mode 100644 index 0000000..dc7853e --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/WbUsedQuotaSpecialtyRespVO.java @@ -0,0 +1,38 @@ +package com.yhy.module.core.controller.admin.workbench.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 项目使用的定额专业 Response VO + * + * @author yhy + */ +@Schema(description = "管理后台 - 项目使用的定额专业 Response VO") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WbUsedQuotaSpecialtyRespVO { + + @Schema(description = "定额专业ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long quotaCatalogItemId; + + @Schema(description = "定额专业名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "建筑工程") + private String quotaCatalogItemName; + + @Schema(description = "费率模式ID(用于加载费率数据)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long rateModeId; + + @Schema(description = "费率模式名称", example = "标准费率") + private String rateModeName; + + @Schema(description = "使用该定额专业的单位工程数量", example = "3") + private Integer unitCount; + + @Schema(description = "单位工程ID(用于快照数据读取)", example = "1") + private Long compileTreeId; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/audit/AuditApproveDivisionRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/audit/AuditApproveDivisionRespVO.java new file mode 100644 index 0000000..b776849 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/audit/AuditApproveDivisionRespVO.java @@ -0,0 +1,76 @@ +package com.yhy.module.core.controller.admin.workbench.vo.audit; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import lombok.Data; + +/** + * 审定分部分项 Response VO(快照数据,可编辑) + */ +@Schema(description = "工作台 - 审定分部分项 Response VO") +@Data +public class AuditApproveDivisionRespVO { + + @Schema(description = "审定分部分项ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "审核编制树的单位工程节点ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long auditCompileTreeId; + + @Schema(description = "来源分部分项ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long sourceDivisionId; + + @Schema(description = "父节点ID", example = "1") + private Long parentId; + + @Schema(description = "节点类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "quota") + private String nodeType; + + @Schema(description = "来源类型", example = "manual") + private String sourceType; + + @Schema(description = "编码", example = "010101") + private String code; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "土石方工程") + private String name; + + @Schema(description = "单位", example = "m3") + private String unit; + + @Schema(description = "项目特征", example = "1.土壤类别:一类土") + private String feature; + + @Schema(description = "排序号", example = "1") + private Integer sortOrder; + + @Schema(description = "层级路径") + private String[] path; + + @Schema(description = "审定工程量", example = "100.00") + private BigDecimal approveQty; + + @Schema(description = "审定单价", example = "25.50") + private BigDecimal approveUnitPrice; + + @Schema(description = "审定费率", example = "3.5") + private BigDecimal approveRate; + + @Schema(description = "审定合价", example = "2550.00") + private BigDecimal approveTotalPrice; + + @Schema(description = "扩展属性") + private Map attributes; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; + + @Schema(description = "子节点列表") + private List children; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/audit/AuditApproveDivisionUpdateReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/audit/AuditApproveDivisionUpdateReqVO.java new file mode 100644 index 0000000..49b4950 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/audit/AuditApproveDivisionUpdateReqVO.java @@ -0,0 +1,42 @@ +package com.yhy.module.core.controller.admin.workbench.vo.audit; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 审定分部分项更新 Request VO + */ +@Schema(description = "工作台 - 审定分部分项更新 Request VO") +@Data +public class AuditApproveDivisionUpdateReqVO { + + @Schema(description = "审定分部分项ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "审定分部分项ID不能为空") + private Long id; + + @Schema(description = "编码(可覆盖原编制模式数据)", example = "010101001001") + private String code; + + @Schema(description = "名称(可覆盖原编制模式数据)", example = "挖土方") + private String name; + + @Schema(description = "单位(可覆盖原编制模式数据)", example = "m³") + private String unit; + + @Schema(description = "项目特征(可覆盖原编制模式数据)", example = "土壤类别:一类土") + private String feature; + + @Schema(description = "审定工程量", example = "100.00") + private BigDecimal approveQty; + + @Schema(description = "审定单价", example = "25.50") + private BigDecimal approveUnitPrice; + + @Schema(description = "审定费率", example = "3.5") + private BigDecimal approveRate; + + @Schema(description = "审定合价", example = "2550.00") + private BigDecimal approveTotalPrice; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/audit/AuditDiffDivisionRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/audit/AuditDiffDivisionRespVO.java new file mode 100644 index 0000000..1b6c3b0 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/audit/AuditDiffDivisionRespVO.java @@ -0,0 +1,69 @@ +package com.yhy.module.core.controller.admin.workbench.vo.audit; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import lombok.Data; + +/** + * 差异分部分项 Response VO(送审 - 审定) + */ +@Schema(description = "工作台 - 差异分部分项 Response VO") +@Data +public class AuditDiffDivisionRespVO { + + @Schema(description = "来源分部分项ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long sourceDivisionId; + + @Schema(description = "父节点ID", example = "1") + private Long parentId; + + @Schema(description = "节点类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "quota") + private String nodeType; + + @Schema(description = "来源类型", example = "manual") + private String sourceType; + + @Schema(description = "编码", example = "010101") + private String code; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "土石方工程") + private String name; + + @Schema(description = "单位", example = "m3") + private String unit; + + @Schema(description = "排序号", example = "1") + private Integer sortOrder; + + @Schema(description = "层级路径") + private String[] path; + + @Schema(description = "量差(送审-审定)", example = "10.00") + private BigDecimal diffQty; + + @Schema(description = "价差(送审-审定)", example = "5.50") + private BigDecimal diffUnitPrice; + + @Schema(description = "费率差(送审-审定)", example = "0.5") + private BigDecimal diffRate; + + @Schema(description = "合价差(送审-审定)", example = "550.00") + private BigDecimal diffTotalPrice; + + @Schema(description = "变化类型:none/modify/add/delete", example = "modify") + private String changeType; + + @Schema(description = "扩展属性") + private Map attributes; + + @Schema(description = "子节点列表") + private List children; + + // 变化类型常量 + public static final String CHANGE_TYPE_NONE = "none"; + public static final String CHANGE_TYPE_MODIFY = "modify"; + public static final String CHANGE_TYPE_ADD = "add"; + public static final String CHANGE_TYPE_DELETE = "delete"; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/audit/AuditDivisionTreeRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/audit/AuditDivisionTreeRespVO.java new file mode 100644 index 0000000..5805116 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/audit/AuditDivisionTreeRespVO.java @@ -0,0 +1,140 @@ +package com.yhy.module.core.controller.admin.workbench.vo.audit; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import lombok.Data; + +/** + * 审核模式分部分项树 Response VO(包含送审、审定、差异三组数据) + */ +@Schema(description = "工作台 - 审核模式分部分项树 Response VO") +@Data +public class AuditDivisionTreeRespVO { + + @Schema(description = "审定分部分项ID(快照表ID,用于编辑)", example = "1") + private Long id; + + @Schema(description = "来源分部分项ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long sourceDivisionId; + + @Schema(description = "父节点ID", example = "1") + private Long parentId; + + @Schema(description = "节点类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "quota") + private String nodeType; + + @Schema(description = "来源类型", example = "manual") + private String sourceType; + + @Schema(description = "编码", example = "010101") + private String code; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "土石方工程") + private String name; + + @Schema(description = "单位(审定值,可编辑覆盖)", example = "m3") + private String unit; + + @Schema(description = "项目特征(审定值,可编辑覆盖)", example = "1.土壤类别:一类土") + private String feature; + + @Schema(description = "编码是否被编辑过", example = "false") + private Boolean codeEdited; + + @Schema(description = "名称是否被编辑过", example = "false") + private Boolean nameEdited; + + @Schema(description = "单位是否被编辑过", example = "false") + private Boolean unitEdited; + + @Schema(description = "项目特征是否被编辑过", example = "false") + private Boolean featureEdited; + + @Schema(description = "排序号", example = "1") + private Integer sortOrder; + + @Schema(description = "层级路径") + private String[] path; + + // ========== 送审数据(编制模式实时) ========== + @Schema(description = "送审编码(原编制模式数据)", example = "010101") + private String submitCode; + + @Schema(description = "送审名称(原编制模式数据)", example = "土石方工程") + private String submitName; + + @Schema(description = "送审单位(原编制模式数据)", example = "m3") + private String submitUnit; + + @Schema(description = "送审项目特征(原编制模式数据)", example = "1.土壤类别:一类土") + private String submitFeature; + + @Schema(description = "送审工程量", example = "100.00") + private BigDecimal submitQty; + + @Schema(description = "送审单价", example = "25.50") + private BigDecimal submitUnitPrice; + + @Schema(description = "送审费率", example = "3.5") + private BigDecimal submitRate; + + @Schema(description = "送审合价", example = "2550.00") + private BigDecimal submitTotalPrice; + + // ========== 审定数据(快照,可编辑) ========== + @Schema(description = "审定工程量", example = "90.00") + private BigDecimal approveQty; + + @Schema(description = "审定单价", example = "25.50") + private BigDecimal approveUnitPrice; + + @Schema(description = "审定费率", example = "3.5") + private BigDecimal approveRate; + + @Schema(description = "审定合价", example = "2295.00") + private BigDecimal approveTotalPrice; + + // ========== 差异数据(送审 - 审定) ========== + @Schema(description = "量差(送审-审定)", example = "10.00") + private BigDecimal diffQty; + + @Schema(description = "价差(送审-审定)", example = "0.00") + private BigDecimal diffUnitPrice; + + @Schema(description = "费率差(送审-审定)", example = "0.0") + private BigDecimal diffRate; + + @Schema(description = "合价差(送审-审定)", example = "255.00") + private BigDecimal diffTotalPrice; + + @Schema(description = "变化类型:none/modify/add/delete", example = "modify") + private String changeType; + + @Schema(description = "数据来源:submit=送审行, approve=审核行(仅定额节点有值)", example = "submit") + private String dataSource; + + @Schema(description = "是否可编辑(送审定额行false,审核定额行true)", example = "true") + private Boolean editable; + + @Schema(description = "扩展属性") + private Map attributes; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; + + @Schema(description = "子节点列表") + private List children; + + // 变化类型常量 + public static final String CHANGE_TYPE_NONE = "none"; + public static final String CHANGE_TYPE_MODIFY = "modify"; + public static final String CHANGE_TYPE_ADD = "add"; + public static final String CHANGE_TYPE_DELETE = "delete"; + + // 数据来源常量(仅定额节点使用) + public static final String DATA_SOURCE_SUBMIT = "submit"; + public static final String DATA_SOURCE_APPROVE = "approve"; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/audit/AuditModeCreateReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/audit/AuditModeCreateReqVO.java new file mode 100644 index 0000000..ee2fecd --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/audit/AuditModeCreateReqVO.java @@ -0,0 +1,22 @@ +package com.yhy.module.core.controller.admin.workbench.vo.audit; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 审核模式创建 Request VO + */ +@Schema(description = "工作台 - 审核模式创建 Request VO") +@Data +public class AuditModeCreateReqVO { + + @Schema(description = "项目ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "项目ID不能为空") + private Long projectId; + + @Schema(description = "审核模式名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "第一次送审") + @NotBlank(message = "审核模式名称不能为空") + private String name; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/audit/AuditModeRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/audit/AuditModeRespVO.java new file mode 100644 index 0000000..b383533 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/audit/AuditModeRespVO.java @@ -0,0 +1,41 @@ +package com.yhy.module.core.controller.admin.workbench.vo.audit; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import java.util.Map; +import lombok.Data; + +/** + * 审核模式 Response VO + */ +@Schema(description = "工作台 - 审核模式 Response VO") +@Data +public class AuditModeRespVO { + + @Schema(description = "审核模式ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "项目ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long projectId; + + @Schema(description = "审核模式名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "第一次送审") + private String name; + + @Schema(description = "状态:draft/submitted/approved", example = "draft") + private String status; + + @Schema(description = "送审时间") + private LocalDateTime submitTime; + + @Schema(description = "审定时间") + private LocalDateTime approveTime; + + @Schema(description = "扩展属性") + private Map attributes; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/audit/AuditSubmitDivisionRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/audit/AuditSubmitDivisionRespVO.java new file mode 100644 index 0000000..90272d1 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/audit/AuditSubmitDivisionRespVO.java @@ -0,0 +1,63 @@ +package com.yhy.module.core.controller.admin.workbench.vo.audit; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import lombok.Data; + +/** + * 送审分部分项 Response VO(联表查询编制模式实时数据) + */ +@Schema(description = "工作台 - 送审分部分项 Response VO") +@Data +public class AuditSubmitDivisionRespVO { + + @Schema(description = "来源分部分项ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long sourceDivisionId; + + @Schema(description = "父节点ID", example = "1") + private Long parentId; + + @Schema(description = "节点类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "quota") + private String nodeType; + + @Schema(description = "来源类型", example = "manual") + private String sourceType; + + @Schema(description = "编码", example = "010101") + private String code; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "土石方工程") + private String name; + + @Schema(description = "单位", example = "m3") + private String unit; + + @Schema(description = "项目特征", example = "1.土壤类别:一类土") + private String feature; + + @Schema(description = "排序号", example = "1") + private Integer sortOrder; + + @Schema(description = "层级路径") + private String[] path; + + @Schema(description = "送审工程量", example = "100.00") + private BigDecimal submitQty; + + @Schema(description = "送审单价", example = "25.50") + private BigDecimal submitUnitPrice; + + @Schema(description = "送审费率", example = "3.5") + private BigDecimal submitRate; + + @Schema(description = "送审合价", example = "2550.00") + private BigDecimal submitTotalPrice; + + @Schema(description = "扩展属性") + private Map attributes; + + @Schema(description = "子节点列表") + private List children; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/calcbaserate/CalcBaseRateCatalogTreeRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/calcbaserate/CalcBaseRateCatalogTreeRespVO.java new file mode 100644 index 0000000..c2e2e40 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/calcbaserate/CalcBaseRateCatalogTreeRespVO.java @@ -0,0 +1,34 @@ +package com.yhy.module.core.controller.admin.workbench.vo.calcbaserate; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * 基数费率目录树节点 VO(工作台项目指引使用) + * + * @author yhy + */ +@Data +@Schema(description = "基数费率目录树节点 VO") +public class CalcBaseRateCatalogTreeRespVO { + + @Schema(description = "节点ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "123") + private Long id; + + @Schema(description = "父节点ID", example = "456") + private Long parentId; + + @Schema(description = "编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ROOT") + private String code; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "广东省") + private String name; + + @Schema(description = "节点类型:root/province/content", requiredMode = Schema.RequiredMode.REQUIRED, example = "province") + private String nodeType; + + @Schema(description = "子节点列表") + private List children; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/calcbaserate/CalcBaseRateDirectoryRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/calcbaserate/CalcBaseRateDirectoryRespVO.java new file mode 100644 index 0000000..c1c6c4e --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/calcbaserate/CalcBaseRateDirectoryRespVO.java @@ -0,0 +1,31 @@ +package com.yhy.module.core.controller.admin.workbench.vo.calcbaserate; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * 基数费率目录节点 VO(工作台项目指引使用) + * + * @author yhy + */ +@Data +@Schema(description = "基数费率目录节点 VO") +public class CalcBaseRateDirectoryRespVO { + + @Schema(description = "节点ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "123") + private Long id; + + @Schema(description = "父节点ID", example = "456") + private Long parentId; + + @Schema(description = "目录名称", example = "土石方工程") + private String name; + + @Schema(description = "排序", example = "1") + private Integer sortOrder; + + @Schema(description = "子节点列表") + private List children; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/calcbaserate/CalcBaseRateItemRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/calcbaserate/CalcBaseRateItemRespVO.java new file mode 100644 index 0000000..4130c62 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/calcbaserate/CalcBaseRateItemRespVO.java @@ -0,0 +1,29 @@ +package com.yhy.module.core.controller.admin.workbench.vo.calcbaserate; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 基数费率项 VO(工作台项目指引使用) + * + * @author yhy + */ +@Data +@Schema(description = "基数费率项 VO") +public class CalcBaseRateItemRespVO { + + @Schema(description = "项ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "123") + private Long id; + + @Schema(description = "名称", example = "夜间施工增加费") + private String name; + + @Schema(description = "费率", example = "20") + private String rate; + + @Schema(description = "备注", example = "按定额人工费+定额机械费计算") + private String remark; + + @Schema(description = "排序", example = "1") + private Integer sortOrder; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/calcbaserate/UpdateBoqDivisionBaseRateReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/calcbaserate/UpdateBoqDivisionBaseRateReqVO.java new file mode 100644 index 0000000..59dc2dc --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/calcbaserate/UpdateBoqDivisionBaseRateReqVO.java @@ -0,0 +1,24 @@ +package com.yhy.module.core.controller.admin.workbench.vo.calcbaserate; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 基数费率项 DO + * 字段:名称、费率、备注 + * + * @author yhy + */ +@Data +@Schema(description = "更新清单基数费率请求 VO") +public class UpdateBoqDivisionBaseRateReqVO { + + @Schema(description = "分部分项节点ID(清单节点)", requiredMode = Schema.RequiredMode.REQUIRED, example = "123") + @NotNull(message = "分部分项节点ID不能为空") + private Long divisionId; + + @Schema(description = "基数费率项ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "456") + @NotNull(message = "基数费率项ID不能为空") + private Long calcBaseRateItemId; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/price/AdjustmentDetail.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/price/AdjustmentDetail.java new file mode 100644 index 0000000..0cf99eb --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/price/AdjustmentDetail.java @@ -0,0 +1,41 @@ +package com.yhy.module.core.controller.admin.workbench.vo.price; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 调整明细(调试用) + * + * 用于定额单价计算中的第2层:定额调整计算明细 + * + * @author yhy + */ +@Schema(description = "工作台 - 调整明细") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AdjustmentDetail { + + @Schema(description = "调整设置ID", example = "1") + private Long settingId; + + @Schema(description = "调整名称", example = "人工系数调整") + private String settingName; + + @Schema(description = "调整类型", example = "adjust_coefficient") + private String adjustmentType; + + @Schema(description = "是否启用", example = "true") + private Boolean enabled; + + @Schema(description = "应用的公式", example = "x*1.1") + private String appliedFormula; + + @Schema(description = "受影响的工料机编码列表") + private List affectedResources; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/price/CategoryPriceSum.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/price/CategoryPriceSum.java new file mode 100644 index 0000000..c9ee3fd --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/price/CategoryPriceSum.java @@ -0,0 +1,115 @@ +package com.yhy.module.core.controller.admin.workbench.vo.price; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 分类价格汇总(按机类统计) + * + * 用于定额单价计算中的第3层:按机类分类汇总子目工料机合价 + * + * @author yhy + */ +@Schema(description = "工作台 - 分类价格汇总") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CategoryPriceSum { + + @Schema(description = "机类ID", example = "1") + private Long categoryId; + + @Schema(description = "机类代码", example = "人") + private String categoryCode; + + @Schema(description = "机类名称", example = "人工") + private String categoryName; + + @Schema(description = "除税基价合价", example = "1000.00") + private BigDecimal taxExclBaseSum; + + @Schema(description = "含税基价合价", example = "1130.00") + private BigDecimal taxInclBaseSum; + + @Schema(description = "除税编制价合价", example = "1050.00") + private BigDecimal taxExclCompileSum; + + @Schema(description = "含税编制价合价", example = "1186.50") + private BigDecimal taxInclCompileSum; + + /** + * 构造函数(仅categoryId) + */ + public CategoryPriceSum(Long categoryId) { + this.categoryId = categoryId; + this.taxExclBaseSum = BigDecimal.ZERO; + this.taxInclBaseSum = BigDecimal.ZERO; + this.taxExclCompileSum = BigDecimal.ZERO; + this.taxInclCompileSum = BigDecimal.ZERO; + } + + /** + * 累加除税基价合价 + */ + public void addTaxExclBaseSum(BigDecimal value) { + if (value != null) { + this.taxExclBaseSum = this.taxExclBaseSum.add(value); + } + } + + /** + * 累加含税基价合价 + */ + public void addTaxInclBaseSum(BigDecimal value) { + if (value != null) { + this.taxInclBaseSum = this.taxInclBaseSum.add(value); + } + } + + /** + * 累加除税编制价合价 + */ + public void addTaxExclCompileSum(BigDecimal value) { + if (value != null) { + this.taxExclCompileSum = this.taxExclCompileSum.add(value); + } + } + + /** + * 累加含税编制价合价 + */ + public void addTaxInclCompileSum(BigDecimal value) { + if (value != null) { + this.taxInclCompileSum = this.taxInclCompileSum.add(value); + } + } + + /** + * 根据价格字段类型获取对应的合价 + * + * @param priceField 价格字段类型 + * @return 对应的合价值 + */ + public BigDecimal getPriceByField(String priceField) { + if (priceField == null) { + return BigDecimal.ZERO; + } + switch (priceField) { + case "tax_excl_base_price": + return taxExclBaseSum != null ? taxExclBaseSum : BigDecimal.ZERO; + case "tax_incl_base_price": + return taxInclBaseSum != null ? taxInclBaseSum : BigDecimal.ZERO; + case "tax_excl_compile_price": + return taxExclCompileSum != null ? taxExclCompileSum : BigDecimal.ZERO; + case "tax_incl_compile_price": + return taxInclCompileSum != null ? taxInclCompileSum : BigDecimal.ZERO; + default: + return BigDecimal.ZERO; + } + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/price/FeeItemPriceResult.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/price/FeeItemPriceResult.java new file mode 100644 index 0000000..85157d7 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/price/FeeItemPriceResult.java @@ -0,0 +1,57 @@ +package com.yhy.module.core.controller.admin.workbench.vo.price; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 单价构成计算结果 + * + * 用于定额单价计算中的第4-5层:取费项子单价计算结果 + * + * @author yhy + */ +@Schema(description = "工作台 - 单价构成计算结果") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FeeItemPriceResult { + + @Schema(description = "取费项ID", example = "1") + private Long feeItemId; + + @Schema(description = "取费项代号", example = "RGF") + private String code; + + @Schema(description = "取费项名称", example = "人工费") + private String name; + + @Schema(description = "计算基数配置(原始配置)") + private Map calcBase; + + @Schema(description = "计算基数值(公式计算结果)", example = "100.00") + private BigDecimal calcBaseValue; + + @Schema(description = "费率代号", example = "10") + private String rateCode; + + @Schema(description = "费率百分比", example = "10.00") + private BigDecimal ratePercent; + + @Schema(description = "子单价(= 计算基数值 × 费率% / 100)", example = "10.00") + private BigDecimal subPrice; + + @Schema(description = "是否为综合单价行", example = "false") + private Boolean isTotal; + + @Schema(description = "计算公式(调试用,显示变量替换后的公式)", example = "100.00 + 50.00") + private String evaluatedFormula; + + @Schema(description = "错误信息(如果计算失败)") + private String errorMessage; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/price/QuotaUnitPriceResult.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/price/QuotaUnitPriceResult.java new file mode 100644 index 0000000..2b5adf1 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/price/QuotaUnitPriceResult.java @@ -0,0 +1,84 @@ +package com.yhy.module.core.controller.admin.workbench.vo.price; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 定额单价计算结果(含完整调试信息) + * + * 用于定额单价计算的最终结果,包含各层计算的中间结果用于调试 + * + * @author yhy + */ +@Schema(description = "工作台 - 定额单价计算结果") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class QuotaUnitPriceResult { + + // ========== 最终结果 ========== + + @Schema(description = "定额节点ID", example = "1") + private Long divisionId; + + @Schema(description = "定额基价ID", example = "1") + private Long quotaItemId; + + @Schema(description = "定额单价(综合单价)", example = "12.50") + private BigDecimal unitPrice; + + @Schema(description = "计算是否成功", example = "true") + private Boolean success; + + @Schema(description = "错误信息(如果计算失败)") + private String errorMessage; + + // ========== 调试信息(可选返回) ========== + + @Schema(description = "第1层:工料机合价明细") + private List resourceDetails; + + @Schema(description = "第2层:调整消耗量明细") + private List adjustmentDetails; + + @Schema(description = "第3层:分类汇总(key=categoryId)") + private Map categorySums; + + @Schema(description = "第4-5层:单价构成明细") + private List feeItemDetails; + + @Schema(description = "计算耗时(毫秒)", example = "45") + private Long calculateTimeMs; + + /** + * 创建成功结果 + */ + public static QuotaUnitPriceResult success(Long divisionId, Long quotaItemId, BigDecimal unitPrice) { + return QuotaUnitPriceResult.builder() + .divisionId(divisionId) + .quotaItemId(quotaItemId) + .unitPrice(unitPrice) + .success(true) + .build(); + } + + /** + * 创建失败结果 + */ + public static QuotaUnitPriceResult fail(Long divisionId, Long quotaItemId, String errorMessage) { + return QuotaUnitPriceResult.builder() + .divisionId(divisionId) + .quotaItemId(quotaItemId) + .unitPrice(BigDecimal.ZERO) + .success(false) + .errorMessage(errorMessage) + .build(); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/price/ResourcePriceDetail.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/price/ResourcePriceDetail.java new file mode 100644 index 0000000..07030f3 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/price/ResourcePriceDetail.java @@ -0,0 +1,86 @@ +package com.yhy.module.core.controller.admin.workbench.vo.price; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 工料机价格明细(调试用) + * + * 用于定额单价计算中的第1层:工料机合价计算明细 + * + * @author yhy + */ +@Schema(description = "工作台 - 工料机价格明细") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ResourcePriceDetail { + + @Schema(description = "工料机ID", example = "1") + private Long resourceId; + + @Schema(description = "工料机编码", example = "R001") + private String resourceCode; + + @Schema(description = "工料机名称", example = "人工") + private String resourceName; + + @Schema(description = "工料机类型", example = "labor") + private String resourceType; + + @Schema(description = "机类ID", example = "1") + private Long categoryId; + + @Schema(description = "机类代码", example = "人") + private String categoryCode; + + @Schema(description = "原始消耗量", example = "100.00") + private BigDecimal dosage; + + @Schema(description = "调整消耗量", example = "110.00") + private BigDecimal adjustedDosage; + + @Schema(description = "有效消耗量(adjustedDosage ?? dosage)", example = "110.00") + private BigDecimal effectiveDosage; + + @Schema(description = "除税基价", example = "10.00") + private BigDecimal taxExclBasePrice; + + @Schema(description = "含税基价", example = "11.30") + private BigDecimal taxInclBasePrice; + + @Schema(description = "除税编制价", example = "10.50") + private BigDecimal taxExclCompilePrice; + + @Schema(description = "含税编制价", example = "11.87") + private BigDecimal taxInclCompilePrice; + + @Schema(description = "除税基价合价", example = "11.00") + private BigDecimal taxExclBaseTotalSum; + + @Schema(description = "含税基价合价", example = "12.43") + private BigDecimal taxInclBaseTotalSum; + + @Schema(description = "除税编制价合价", example = "11.55") + private BigDecimal taxExclCompileTotalSum; + + @Schema(description = "含税编制价合价", example = "13.06") + private BigDecimal taxInclCompileTotalSum; + + @Schema(description = "调整公式(如 '(x)*1.1')", example = "(x)*1.1") + private String adjustmentFormula; + + @Schema(description = "是否为复合工料机", example = "false") + private Boolean isMerged; + + @Schema(description = "是否为单位%工料机", example = "false") + private Boolean isPercentUnit; + + @Schema(description = "计算基数(单位为%的工料机使用)") + private java.util.Map calcBase; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/PeriodInfoVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/PeriodInfoVO.java new file mode 100644 index 0000000..1419ccc --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/PeriodInfoVO.java @@ -0,0 +1,26 @@ +package com.yhy.module.core.controller.admin.workbench.vo.progresspayment; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import lombok.Data; + +/** + * 进度款期数信息 VO + * 用于存储进度款模式下清单的绑定数据 + */ +@Schema(description = "进度款期数信息 VO") +@Data +public class PeriodInfoVO { + + @Schema(description = "进度款模式ID", example = "1") + private Long progressPaymentModeId; + + @Schema(description = "期数值", example = "100.00") + private BigDecimal periodValue; + + @Schema(description = "期数序号", example = "1") + private Integer periodNumber; + + @Schema(description = "进度款名称", example = "第一期") + private String progressPaymentModeName; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/ProgressDivisionBatchSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/ProgressDivisionBatchSaveReqVO.java new file mode 100644 index 0000000..21cbec3 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/ProgressDivisionBatchSaveReqVO.java @@ -0,0 +1,22 @@ +package com.yhy.module.core.controller.admin.workbench.vo.progresspayment; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.util.Map; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 进度款-清单关联批量保存 Request VO + */ +@Schema(description = "工作台 - 进度款-清单关联批量保存 Request VO") +@Data +public class ProgressDivisionBatchSaveReqVO { + + @Schema(description = "进度款模式ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "进度款模式ID不能为空") + private Long progressPaymentModeId; + + @Schema(description = "期数值映射(key=清单节点ID, value=期数值)", example = "{\"1\": 100.00, \"2\": 200.00}") + private Map periodValueMap; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/ProgressDivisionSaveReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/ProgressDivisionSaveReqVO.java new file mode 100644 index 0000000..835bd1a --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/ProgressDivisionSaveReqVO.java @@ -0,0 +1,25 @@ +package com.yhy.module.core.controller.admin.workbench.vo.progresspayment; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 进度款-清单关联保存 Request VO + */ +@Schema(description = "工作台 - 进度款-清单关联保存 Request VO") +@Data +public class ProgressDivisionSaveReqVO { + + @Schema(description = "进度款模式ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "进度款模式ID不能为空") + private Long progressPaymentModeId; + + @Schema(description = "清单节点ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "清单节点ID不能为空") + private Long boqDivisionId; + + @Schema(description = "期数值", example = "100.00") + private BigDecimal periodValue; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/ProgressDivisionSimpleVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/ProgressDivisionSimpleVO.java new file mode 100644 index 0000000..4e31d21 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/ProgressDivisionSimpleVO.java @@ -0,0 +1,23 @@ +package com.yhy.module.core.controller.admin.workbench.vo.progresspayment; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import lombok.Data; + +/** + * 进度款-清单关联简要信息 VO + */ +@Schema(description = "工作台 - 进度款-清单关联简要信息 VO") +@Data +public class ProgressDivisionSimpleVO { + +// @Schema(description = "关联ID", example = "1") +// private Long id; + + @Schema(description = "关联清单节点ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long boqDivisionId; + + @Schema(description = "期数值", example = "0.5") + private BigDecimal periodValue; + +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/ProgressDivisionTreeRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/ProgressDivisionTreeRespVO.java new file mode 100644 index 0000000..fdc28c8 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/ProgressDivisionTreeRespVO.java @@ -0,0 +1,22 @@ +package com.yhy.module.core.controller.admin.workbench.vo.progresspayment; + +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqDivisionRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 进度款分部分项树 Response VO + * 继承 WbBoqDivisionRespVO,添加进度款绑定数据 + * + * 注意:children 字段继承自父类 WbBoqDivisionRespVO, + * 运行时可存储 ProgressDivisionTreeRespVO 对象(多态) + */ +@Schema(description = "工作台 - 进度款分部分项树 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class ProgressDivisionTreeRespVO extends WbBoqDivisionRespVO { + + @Schema(description = "进度款期数信息(进度款模式下使用)") + private PeriodInfoVO period; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/ProgressPaymentModeCreateReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/ProgressPaymentModeCreateReqVO.java new file mode 100644 index 0000000..ac27f84 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/ProgressPaymentModeCreateReqVO.java @@ -0,0 +1,62 @@ +package com.yhy.module.core.controller.admin.workbench.vo.progresspayment; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.time.LocalDate; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 进度款模式创建 Request VO + */ +@Schema(description = "工作台 - 进度款模式创建 Request VO") +@Data +public class ProgressPaymentModeCreateReqVO { + + @Schema(description = "项目ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "项目ID不能为空") + private Long projectId; + + @Schema(description = "名称(为空时自动生成:第X期)", example = "第一期进度款") + private String name; + + @Schema(description = "期数序号", example = "1") + private Integer periodNumber; + + @Schema(description = "目标行ID(用于插入位置计算)", example = "1") + private Long targetId; + + @Schema(description = "插入位置:above-在目标行上方,below-在目标行下方", example = "below") + private String insertPosition; + + @Schema(description = "开始日期", example = "2024-01-01") + private LocalDate startDate; + + @Schema(description = "结束日期", example = "2024-03-31") + private LocalDate endDate; + + @Schema(description = "总造价", example = "1000000.00") + private BigDecimal totalCost = BigDecimal.ZERO; + + @Schema(description = "本期产值", example = "200000.00") + private BigDecimal currentPeriodValue = BigDecimal.ZERO; + + @Schema(description = "已完产值", example = "500000.00") + private BigDecimal completedValue = BigDecimal.ZERO; + + @Schema(description = "已完比例", example = "0.5") + private BigDecimal completedRatio = BigDecimal.ZERO; + + @Schema(description = "支付比例(%)", example = "80") + private BigDecimal paymentRatio = BigDecimal.ZERO; + + @Schema(description = "支付金额", example = "400000.00") + private BigDecimal paymentAmount = BigDecimal.ZERO; + + @Schema(description = "材料调差", example = "10000.00") + private BigDecimal materialAdjustment = BigDecimal.ZERO; + + @Schema(description = "备注", example = "第一期进度款") + private String remark; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/ProgressPaymentModeRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/ProgressPaymentModeRespVO.java new file mode 100644 index 0000000..9c13ef3 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/ProgressPaymentModeRespVO.java @@ -0,0 +1,63 @@ +package com.yhy.module.core.controller.admin.workbench.vo.progresspayment; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import lombok.Data; + +/** + * 进度款模式 Response VO + */ +@Schema(description = "工作台 - 进度款模式 Response VO") +@Data +public class ProgressPaymentModeRespVO { + + @Schema(description = "进度款模式ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "项目ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long projectId; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "第一期进度款") + private String name; + + @Schema(description = "期数序号", example = "1") + private Integer periodNumber; + + @Schema(description = "开始日期", example = "2024-01-01") + private LocalDate startDate; + + @Schema(description = "结束日期", example = "2024-03-31") + private LocalDate endDate; + + @Schema(description = "总造价", example = "1000000.00") + private BigDecimal totalCost; + + @Schema(description = "本期产值", example = "200000.00") + private BigDecimal currentPeriodValue; + + @Schema(description = "已完产值", example = "500000.00") + private BigDecimal completedValue; + + @Schema(description = "已完比例", example = "0.5") + private BigDecimal completedRatio; + + @Schema(description = "支付比例(%)", example = "80") + private BigDecimal paymentRatio; + + @Schema(description = "支付金额", example = "400000.00") + private BigDecimal paymentAmount; + + @Schema(description = "材料调差", example = "10000.00") + private BigDecimal materialAdjustment; + + @Schema(description = "备注", example = "第一期进度款") + private String remark; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/ProgressPaymentModeUpdateReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/ProgressPaymentModeUpdateReqVO.java new file mode 100644 index 0000000..1633fc8 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/ProgressPaymentModeUpdateReqVO.java @@ -0,0 +1,55 @@ +package com.yhy.module.core.controller.admin.workbench.vo.progresspayment; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.time.LocalDate; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 进度款模式更新 Request VO + */ +@Schema(description = "工作台 - 进度款模式更新 Request VO") +@Data +public class ProgressPaymentModeUpdateReqVO { + + @Schema(description = "进度款模式ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "进度款模式ID不能为空") + private Long id; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "第一期进度款") + private String name; + + @Schema(description = "期数序号", example = "1") + private Integer periodNumber; + + @Schema(description = "开始日期", example = "2024-01-01") + private LocalDate startDate; + + @Schema(description = "结束日期", example = "2024-03-31") + private LocalDate endDate; + + @Schema(description = "总造价", example = "1000000.00") + private BigDecimal totalCost; + + @Schema(description = "本期产值", example = "200000.00") + private BigDecimal currentPeriodValue; + + @Schema(description = "已完产值", example = "500000.00") + private BigDecimal completedValue; + + @Schema(description = "已完比例", example = "0.5") + private BigDecimal completedRatio; + + @Schema(description = "支付比例(%)", example = "80") + private BigDecimal paymentRatio; + + @Schema(description = "支付金额", example = "400000.00") + private BigDecimal paymentAmount; + + @Schema(description = "材料调差", example = "10000.00") + private BigDecimal materialAdjustment; + + @Schema(description = "备注", example = "第一期进度款") + private String remark; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/ProgressPaymentModeWithDivisionsRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/ProgressPaymentModeWithDivisionsRespVO.java new file mode 100644 index 0000000..d8b1daf --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/progresspayment/ProgressPaymentModeWithDivisionsRespVO.java @@ -0,0 +1,18 @@ +package com.yhy.module.core.controller.admin.workbench.vo.progresspayment; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 进度款模式(含关联清单) Response VO + */ +@Schema(description = "工作台 - 进度款模式(含关联清单) Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class ProgressPaymentModeWithDivisionsRespVO extends ProgressPaymentModeRespVO { + + @Schema(description = "关联的清单列表") + private List children; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/sync/ApplySyncReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/sync/ApplySyncReqVO.java new file mode 100644 index 0000000..04c47dc --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/sync/ApplySyncReqVO.java @@ -0,0 +1,19 @@ +package com.yhy.module.core.controller.admin.workbench.vo.sync; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 套用同步请求 VO + * + * @author yhy + */ +@Schema(description = "套用同步请求") +@Data +public class ApplySyncReqVO { + + @Schema(description = "单位工程ID", required = true) + @NotNull(message = "单位工程ID不能为空") + private Long compileTreeId; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/sync/SetSyncReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/sync/SetSyncReqVO.java new file mode 100644 index 0000000..c4fb3aa --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/sync/SetSyncReqVO.java @@ -0,0 +1,19 @@ +package com.yhy.module.core.controller.admin.workbench.vo.sync; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 设为同步请求 VO + * + * @author yhy + */ +@Schema(description = "设为同步请求") +@Data +public class SetSyncReqVO { + + @Schema(description = "分部分项ID(清单/分部节点)", required = true) + @NotNull(message = "分部分项ID不能为空") + private Long divisionId; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/sync/SyncLibraryDivisionRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/sync/SyncLibraryDivisionRespVO.java new file mode 100644 index 0000000..eb290b9 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/sync/SyncLibraryDivisionRespVO.java @@ -0,0 +1,65 @@ +package com.yhy.module.core.controller.admin.workbench.vo.sync; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import lombok.Data; + +/** + * 同步库分部分项树响应 VO + * + * @author yhy + */ +@Schema(description = "同步库分部分项树响应") +@Data +public class SyncLibraryDivisionRespVO { + + @Schema(description = "节点ID") + private String id; + + @Schema(description = "同步库ID") + private String syncLibraryId; + + @Schema(description = "父节点ID") + private String parentId; + + @Schema(description = "节点类型") + private String nodeType; + + @Schema(description = "编码") + private String code; + + @Schema(description = "名称") + private String name; + + @Schema(description = "项目特征") + private String feature; + + @Schema(description = "单位") + private String unit; + + @Schema(description = "工程量") + private BigDecimal qty; + + @Schema(description = "费率") + private BigDecimal rate; + + @Schema(description = "费用代号") + private String costCode; + + @Schema(description = "定额工程量公式") + private String quotaQtyFormula; + + @Schema(description = "排序号") + private Integer sortOrder; + + @Schema(description = "扩展属性") + private Map attributes; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "子节点") + private List children; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/sync/SyncLibraryDivisionUpdateReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/sync/SyncLibraryDivisionUpdateReqVO.java new file mode 100644 index 0000000..323344b --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/sync/SyncLibraryDivisionUpdateReqVO.java @@ -0,0 +1,51 @@ +package com.yhy.module.core.controller.admin.workbench.vo.sync; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.util.Map; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 同步库分部分项更新请求 VO + * + * @author yhy + */ +@Schema(description = "同步库分部分项更新请求") +@Data +public class SyncLibraryDivisionUpdateReqVO { + + @Schema(description = "节点ID", required = true) + @NotNull(message = "节点ID不能为空") + private Long id; + + @Schema(description = "编码") + private String code; + + @Schema(description = "名称") + private String name; + + @Schema(description = "项目特征") + private String feature; + + @Schema(description = "单位") + private String unit; + + @Schema(description = "工程量") + private BigDecimal qty; + + @Schema(description = "费率") + private BigDecimal rate; + + @Schema(description = "费用代号") + private String costCode; + + @Schema(description = "定额工程量公式") + private String quotaQtyFormula; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "扩展属性") + private Map attributes; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/sync/SyncPendingRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/sync/SyncPendingRespVO.java new file mode 100644 index 0000000..26074ee --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/sync/SyncPendingRespVO.java @@ -0,0 +1,29 @@ +package com.yhy.module.core.controller.admin.workbench.vo.sync; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 待同步变更响应 VO + * + * @author yhy + */ +@Schema(description = "待同步变更响应") +@Data +public class SyncPendingRespVO { + + @Schema(description = "同步库ID") + private String syncLibraryId; + + @Schema(description = "同步库名称") + private String syncLibraryName; + + @Schema(description = "同步库编码") + private String syncLibraryCode; + + @Schema(description = "当前版本") + private Integer currentVersion; + + @Schema(description = "已同步版本") + private Integer syncedVersion; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/sync/SyncSourceUnitRespVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/sync/SyncSourceUnitRespVO.java new file mode 100644 index 0000000..275579c --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/sync/SyncSourceUnitRespVO.java @@ -0,0 +1,29 @@ +package com.yhy.module.core.controller.admin.workbench.vo.sync; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 同步库来源单位工程 VO + * + * @author yhy + */ +@Schema(description = "同步库来源单位工程") +@Data +public class SyncSourceUnitRespVO { + + @Schema(description = "绑定ID") + private String bindingId; + + @Schema(description = "分部分项ID") + private String divisionId; + + @Schema(description = "单位工程ID") + private String compileTreeId; + + @Schema(description = "单位工程名称") + private String compileTreeName; + + @Schema(description = "已同步版本号") + private Integer syncedVersion; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/sync/UnsetSyncReqVO.java b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/sync/UnsetSyncReqVO.java new file mode 100644 index 0000000..38ec729 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/controller/admin/workbench/vo/sync/UnsetSyncReqVO.java @@ -0,0 +1,19 @@ +package com.yhy.module.core.controller.admin.workbench.vo.sync; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotNull; +import lombok.Data; + +/** + * 解除同步请求 VO + * + * @author yhy + */ +@Schema(description = "解除同步请求") +@Data +public class UnsetSyncReqVO { + + @Schema(description = "分部分项ID(已同步的清单/分部节点)", required = true) + @NotNull(message = "分部分项ID不能为空") + private Long divisionId; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/convert/config/ConfigProjectInfoConvert.java b/yhy-module-core/src/main/java/com/yhy/module/core/convert/config/ConfigProjectInfoConvert.java new file mode 100644 index 0000000..ed801c2 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/convert/config/ConfigProjectInfoConvert.java @@ -0,0 +1,25 @@ +package com.yhy.module.core.convert.config; + +import com.yhy.module.core.controller.admin.config.vo.ConfigProjectInfoRespVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigProjectInfoSaveReqVO; +import com.yhy.module.core.dal.dataobject.config.ConfigProjectInfoDO; +import java.util.List; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 工程信息配置 Convert + * + * @author yhy + */ +@Mapper +public interface ConfigProjectInfoConvert { + + ConfigProjectInfoConvert INSTANCE = Mappers.getMapper(ConfigProjectInfoConvert.class); + + ConfigProjectInfoDO convert(ConfigProjectInfoSaveReqVO bean); + + ConfigProjectInfoRespVO convert(ConfigProjectInfoDO bean); + + List convertList(List list); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/convert/config/ConfigProjectTreeConvert.java b/yhy-module-core/src/main/java/com/yhy/module/core/convert/config/ConfigProjectTreeConvert.java new file mode 100644 index 0000000..6abbeda --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/convert/config/ConfigProjectTreeConvert.java @@ -0,0 +1,25 @@ +package com.yhy.module.core.convert.config; + +import com.yhy.module.core.controller.admin.config.vo.ConfigProjectTreeRespVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigProjectTreeSaveReqVO; +import com.yhy.module.core.dal.dataobject.config.ConfigProjectTreeDO; +import java.util.List; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 项目界面配置树 Convert + * + * @author yhy + */ +@Mapper +public interface ConfigProjectTreeConvert { + + ConfigProjectTreeConvert INSTANCE = Mappers.getMapper(ConfigProjectTreeConvert.class); + + ConfigProjectTreeDO convert(ConfigProjectTreeSaveReqVO bean); + + ConfigProjectTreeRespVO convert(ConfigProjectTreeDO bean); + + List convertList(List list); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/convert/workbench/WbBoqDivisionConvert.java b/yhy-module-core/src/main/java/com/yhy/module/core/convert/workbench/WbBoqDivisionConvert.java new file mode 100644 index 0000000..a11a6b4 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/convert/workbench/WbBoqDivisionConvert.java @@ -0,0 +1,26 @@ +package com.yhy.module.core.convert.workbench; + +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqDivisionRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqDivisionSaveReqVO; +import com.yhy.module.core.dal.dataobject.workbench.WbBoqDivisionDO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * 工作台分部分项树 Convert + * + * @author yhy + */ +@Mapper +public interface WbBoqDivisionConvert { + + WbBoqDivisionConvert INSTANCE = Mappers.getMapper(WbBoqDivisionConvert.class); + + WbBoqDivisionDO convert(WbBoqDivisionSaveReqVO bean); + + @Mapping(source = "isSyncSource", target = "isSyncSource") + WbBoqDivisionRespVO convert(WbBoqDivisionDO bean); + + java.util.List convertList(java.util.List list); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/convert/workbench/WbBoqResourceConvert.java b/yhy-module-core/src/main/java/com/yhy/module/core/convert/workbench/WbBoqResourceConvert.java new file mode 100644 index 0000000..7a31ba6 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/convert/workbench/WbBoqResourceConvert.java @@ -0,0 +1,22 @@ +package com.yhy.module.core.convert.workbench; + +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceSaveReqVO; +import com.yhy.module.core.dal.dataobject.workbench.WbBoqResourceDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 工作台工料机消耗 Convert + * + * @author yhy + */ +@Mapper +public interface WbBoqResourceConvert { + + WbBoqResourceConvert INSTANCE = Mappers.getMapper(WbBoqResourceConvert.class); + + WbBoqResourceDO convert(WbBoqResourceSaveReqVO bean); + + WbBoqResourceRespVO convert(WbBoqResourceDO bean); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/convert/workbench/WbCompileTreeConvert.java b/yhy-module-core/src/main/java/com/yhy/module/core/convert/workbench/WbCompileTreeConvert.java new file mode 100644 index 0000000..804e0b9 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/convert/workbench/WbCompileTreeConvert.java @@ -0,0 +1,25 @@ +package com.yhy.module.core.convert.workbench; + +import com.yhy.module.core.controller.admin.workbench.vo.WbCompileTreeRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbCompileTreeSaveReqVO; +import com.yhy.module.core.dal.dataobject.workbench.WbCompileTreeDO; +import java.util.List; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 编制模式树 Convert + * + * @author yhy + */ +@Mapper +public interface WbCompileTreeConvert { + + WbCompileTreeConvert INSTANCE = Mappers.getMapper(WbCompileTreeConvert.class); + + WbCompileTreeDO convert(WbCompileTreeSaveReqVO bean); + + WbCompileTreeRespVO convert(WbCompileTreeDO bean); + + List convertList(List list); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/convert/workbench/WbItemInfoConvert.java b/yhy-module-core/src/main/java/com/yhy/module/core/convert/workbench/WbItemInfoConvert.java new file mode 100644 index 0000000..5292958 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/convert/workbench/WbItemInfoConvert.java @@ -0,0 +1,22 @@ +package com.yhy.module.core.convert.workbench; + +import com.yhy.module.core.controller.admin.workbench.vo.WbItemInfoRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbItemInfoSaveReqVO; +import com.yhy.module.core.dal.dataobject.workbench.WbItemInfoDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 单项基本信息 Convert + * + * @author yhy + */ +@Mapper +public interface WbItemInfoConvert { + + WbItemInfoConvert INSTANCE = Mappers.getMapper(WbItemInfoConvert.class); + + WbItemInfoDO convert(WbItemInfoSaveReqVO bean); + + WbItemInfoRespVO convert(WbItemInfoDO bean); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/convert/workbench/WbProjectConvert.java b/yhy-module-core/src/main/java/com/yhy/module/core/convert/workbench/WbProjectConvert.java new file mode 100644 index 0000000..7de57a0 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/convert/workbench/WbProjectConvert.java @@ -0,0 +1,25 @@ +package com.yhy.module.core.convert.workbench; + +import com.yhy.module.core.controller.admin.workbench.vo.WbProjectTreeRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbProjectTreeSaveReqVO; +import com.yhy.module.core.dal.dataobject.workbench.WbProjectTreeDO; +import java.util.List; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 工作台项目管理树 Convert + * + * @author yhy + */ +@Mapper +public interface WbProjectConvert { + + WbProjectConvert INSTANCE = Mappers.getMapper(WbProjectConvert.class); + + WbProjectTreeDO convert(WbProjectTreeSaveReqVO bean); + + WbProjectTreeRespVO convert(WbProjectTreeDO bean); + + List convertList(List list); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/convert/workbench/WbUnitInfoConvert.java b/yhy-module-core/src/main/java/com/yhy/module/core/convert/workbench/WbUnitInfoConvert.java new file mode 100644 index 0000000..1888c97 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/convert/workbench/WbUnitInfoConvert.java @@ -0,0 +1,22 @@ +package com.yhy.module.core.convert.workbench; + +import com.yhy.module.core.controller.admin.workbench.vo.WbUnitInfoRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbUnitInfoSaveReqVO; +import com.yhy.module.core.dal.dataobject.workbench.WbUnitInfoDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 单位工程信息 Convert + * + * @author yhy + */ +@Mapper +public interface WbUnitInfoConvert { + + WbUnitInfoConvert INSTANCE = Mappers.getMapper(WbUnitInfoConvert.class); + + WbUnitInfoDO convert(WbUnitInfoSaveReqVO bean); + + WbUnitInfoRespVO convert(WbUnitInfoDO bean); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/boq/BoqCatalogItemDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/boq/BoqCatalogItemDO.java index d8f6116..0c11c23 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/boq/BoqCatalogItemDO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/boq/BoqCatalogItemDO.java @@ -1,6 +1,7 @@ package com.yhy.module.core.dal.dataobject.boq; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; @@ -16,7 +17,9 @@ import lombok.ToString; /** * 清单配置目录树 DO - * 第一层:省市/清单专业 + * 第一层:root(顶级分类) + * 第二层:province(省市) + * 第三层:specialty(清单专业) * * @author yhy */ @@ -30,9 +33,9 @@ import lombok.ToString; public class BoqCatalogItemDO extends BaseDO { /** - * 主键(标准库,使用自增ID) + * 主键(标准库,使用雪花ID) */ - @TableId + @TableId(type = IdType.ASSIGN_ID) private Long id; /** @@ -56,7 +59,7 @@ public class BoqCatalogItemDO extends BaseDO { private String name; /** - * 节点类型:province(省市)/specialty(清单专业) + * 节点类型:root(顶级分类)/province(省市)/specialty(清单专业) */ private String nodeType; @@ -93,6 +96,9 @@ public class BoqCatalogItemDO extends BaseDO { private Map attributes; // 节点类型常量 + public static final String NODE_TYPE_ROOT = "root"; public static final String NODE_TYPE_PROVINCE = "province"; public static final String NODE_TYPE_SPECIALTY = "specialty"; + public static final String NODE_TYPE_CONTENT = "content"; + } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/boq/BoqDetailTreeDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/boq/BoqGuideTreeDO.java similarity index 73% rename from yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/boq/BoqDetailTreeDO.java rename to yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/boq/BoqGuideTreeDO.java index 16157b0..8416539 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/boq/BoqDetailTreeDO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/boq/BoqGuideTreeDO.java @@ -1,32 +1,36 @@ package com.yhy.module.core.dal.dataobject.boq; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; -import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; import com.yhy.module.core.framework.mybatis.typehandler.StringArrayTypeHandler; -import lombok.*; - import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; /** - * 清单明细树 DO - * 第四层:树结构,分为目录和内容 - * - 目录节点:可以无限创建子目录 - * - 内容节点:关联定额基价【第三层】的ID,不能添加子节点 + * 清单指引树 DO + * 第四层:树结构,分为目录和定额 + * - 目录节点:可以无限创建子目录和定额节点 + * - 定额节点:关联定额基价【第三层】的ID,不能添加子节点 * * @author yhy */ -@TableName(value = "yhy_boq_detail_tree", autoResultMap = true) +@TableName(value = "yhy_boq_guide_tree", autoResultMap = true) @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor -public class BoqDetailTreeDO extends BaseDO { +public class BoqGuideTreeDO extends BaseDO { /** * 主键 @@ -65,12 +69,12 @@ public class BoqDetailTreeDO extends BaseDO { private String unit; /** - * 节点类型:directory-目录,content-内容 + * 节点类型:directory-目录,quota-定额 */ private String nodeType; /** - * 定额基价【第三层】节点ID(仅content类型使用) + * 定额基价【第三层】节点ID(仅quota类型使用) */ private Long quotaCatalogItemId; diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/boq/BoqItemTreeDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/boq/BoqItemTreeDO.java index 932c290..c11cf0f 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/boq/BoqItemTreeDO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/boq/BoqItemTreeDO.java @@ -77,7 +77,10 @@ public class BoqItemTreeDO extends BaseDO { * 层级 */ private Integer level; - + /** + * 说明(富文本) + */ + private String description; /** * 扩展属性 */ diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/boq/BoqSubItemDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/boq/BoqSubItemDO.java index 0750c87..52585b9 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/boq/BoqSubItemDO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/boq/BoqSubItemDO.java @@ -60,7 +60,10 @@ public class BoqSubItemDO extends BaseDO { * 清单说明(富文本) */ private String description; - + /** + * 项目特征 + */ + private String features; /** * 排序 */ diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/calcbaserate/CalcBaseRateCatalogDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/calcbaserate/CalcBaseRateCatalogDO.java new file mode 100644 index 0000000..cb57ccb --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/calcbaserate/CalcBaseRateCatalogDO.java @@ -0,0 +1,92 @@ +package com.yhy.module.core.dal.dataobject.calcbaserate; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import com.yhy.module.core.dal.typehandler.PostgreSQLTextArrayTypeHandler; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 基数费率目录树 DO + * 第一层:root(顶级分类) + * 第二层:province(省市) + * 第三层:content(清单) + * + * @author yhy + */ +@TableName(value = "yhy_calc_base_rate_catalog", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CalcBaseRateCatalogDO extends BaseDO { + + /** + * 主键(标准库,使用雪花ID) + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 父节点ID + */ + private Long parentId; + + /** + * 编码 + */ + private String code; + + /** + * 名称 + */ + private String name; + + /** + * 节点类型:root(顶级分类)/province(省市)/content(清单) + */ + private String nodeType; + + /** + * 排序 + */ + private Integer sortOrder; + + /** + * 树路径 + */ + @TableField(typeHandler = PostgreSQLTextArrayTypeHandler.class) + private String[] path; + + /** + * 层级 + */ + private Integer level; + + /** + * 扩展属性 + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map attributes; + + // 节点类型常量 + public static final String NODE_TYPE_ROOT = "root"; + public static final String NODE_TYPE_PROVINCE = "province"; + public static final String NODE_TYPE_CONTENT = "content"; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/calcbaserate/CalcBaseRateDirectoryDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/calcbaserate/CalcBaseRateDirectoryDO.java new file mode 100644 index 0000000..27ec3f4 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/calcbaserate/CalcBaseRateDirectoryDO.java @@ -0,0 +1,80 @@ +package com.yhy.module.core.dal.dataobject.calcbaserate; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import com.yhy.module.core.dal.typehandler.PostgreSQLTextArrayTypeHandler; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 基数费率目录 DO + * 树结构:只有序号和目录名称 + * + * @author yhy + */ +@TableName(value = "yhy_calc_base_rate_directory", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CalcBaseRateDirectoryDO extends BaseDO { + + /** + * 主键 + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 关联基数费率目录树节点ID(左侧树的content节点) + */ + private Long calcBaseRateCatalogId; + + /** + * 父节点ID + */ + private Long parentId; + + /** + * 目录名称 + */ + private String name; + + /** + * 排序 + */ + private Integer sortOrder; + + /** + * 树路径 + */ + @TableField(typeHandler = PostgreSQLTextArrayTypeHandler.class) + private String[] path; + + /** + * 层级 + */ + private Integer level; + + /** + * 扩展属性 + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map attributes; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/calcbaserate/CalcBaseRateItemDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/calcbaserate/CalcBaseRateItemDO.java new file mode 100644 index 0000000..59f117b --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/calcbaserate/CalcBaseRateItemDO.java @@ -0,0 +1,73 @@ +package com.yhy.module.core.dal.dataobject.calcbaserate; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 基数费率项 DO + * 字段:名称、费率、备注 + * + * @author yhy + */ +@TableName(value = "yhy_calc_base_rate_item", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CalcBaseRateItemDO extends BaseDO { + + /** + * 主键 + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 关联基数费率目录ID(右侧第一个表格的节点ID) + */ + private Long calcBaseRateDirectoryId; + + /** + * 名称 + */ + private String name; + + /** + * 费率 + */ + private String rate; + + /** + * 备注 + */ + private String remark; + + /** + * 排序 + */ + private Integer sortOrder; + + /** + * 扩展属性 + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map attributes; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/config/ConfigProjectInfoDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/config/ConfigProjectInfoDO.java new file mode 100644 index 0000000..f2edf49 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/config/ConfigProjectInfoDO.java @@ -0,0 +1,80 @@ +package com.yhy.module.core.dal.dataobject.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import com.yhy.module.core.dal.typehandler.PostgreSQLTextArrayTypeHandler; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 工程信息配置 DO + * 行业节点的一对多树结构数据 + * + * @author yhy + */ +@TableName(value = "yhy_config_project_info", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ConfigProjectInfoDO extends BaseDO { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 关联的行业节点ID(yhy_config_project_tree.id) + */ + private Long configTreeId; + + /** + * 父节点ID(树结构) + */ + private Long parentId; + + /** + * 代号 + */ + private String code; + + /** + * 名称 + */ + private String name; + + /** + * 内容配置(JSONB,存储复杂控制逻辑) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map content; + + /** + * 排序号 + */ + private Integer sortOrder; + + /** + * 层级路径(ID数组) + */ + @TableField(typeHandler = PostgreSQLTextArrayTypeHandler.class) + private String[] path; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/config/ConfigProjectTreeDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/config/ConfigProjectTreeDO.java new file mode 100644 index 0000000..8c4f4cb --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/config/ConfigProjectTreeDO.java @@ -0,0 +1,85 @@ +package com.yhy.module.core.dal.dataobject.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import com.yhy.module.core.dal.typehandler.PostgreSQLTextArrayTypeHandler; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 项目界面配置树 DO + * 节点类型:root-根节点, province-省市, industry-行业 + * + * @author yhy + */ +@TableName(value = "yhy_config_project_tree", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ConfigProjectTreeDO extends BaseDO { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 父节点ID + */ + private Long parentId; + + /** + * 编码 + */ + private String code; + + /** + * 名称 + */ + private String name; + + /** + * 节点类型:root-根节点, province-省市, industry-行业 + */ + private String nodeType; + + /** + * 排序号 + */ + private Integer sortOrder; + + /** + * 层级路径(ID数组) + */ + @TableField(typeHandler = PostgreSQLTextArrayTypeHandler.class) + private String[] path; + + /** + * 扩展属性(JSONB) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map attributes; + + // 节点类型常量 + public static final String NODE_TYPE_ROOT = "root"; + public static final String NODE_TYPE_PROVINCE = "province"; + public static final String NODE_TYPE_INDUSTRY = "industry"; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/config/ConfigUnitDivisionTemplateDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/config/ConfigUnitDivisionTemplateDO.java new file mode 100644 index 0000000..691a420 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/config/ConfigUnitDivisionTemplateDO.java @@ -0,0 +1,97 @@ +package com.yhy.module.core.dal.dataobject.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import com.yhy.module.core.dal.typehandler.PostgreSQLTextArrayTypeHandler; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 单位工程界面配置 - 分部分项模板(源数据) DO + * 只支持 division(分部)和 boq(清单)两种节点类型 + * + * @author yhy + */ +@TableName(value = "yhy_config_unit_division_template", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ConfigUnitDivisionTemplateDO extends BaseDO { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** 租户ID */ + private Long tenantId; + + /** 关联 fields_majors 节点ID */ + private Long catalogItemId; + + /** 父节点ID */ + private Long parentId; + + /** 节点类型:division-分部, boq-清单 */ + private String nodeType; + + /** 编码 */ + private String code; + + /** 名称 */ + private String name; + + /** 单位 */ + private String unit; + + /** 排序 */ + private Integer sortOrder; + + /** 标签页类型:division-分部分项/measure-措施项目/other-其他项目/unit_summary-单位汇总 */ + private String tabType; + + /** 费率 */ + private java.math.BigDecimal rate; + + /** 备注 */ + private String remark; + + /** 费用代号 */ + private String costCode; + + /** 层级路径 */ + @TableField(typeHandler = PostgreSQLTextArrayTypeHandler.class) + private String[] path; + + /** 扩展属性 */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map attributes; + + // 节点类型常量 + public static final String NODE_TYPE_DIVISION = "division"; + public static final String NODE_TYPE_BOQ = "boq"; + + // 标签页类型常量 + public static final String TAB_TYPE_DIVISION = "division"; + public static final String TAB_TYPE_MEASURE = "measure"; + public static final String TAB_TYPE_OTHER = "other"; + public static final String TAB_TYPE_UNIT_SUMMARY = "unit_summary"; + + /** 有效的标签页类型集合 */ + public static final Set VALID_TAB_TYPES = new HashSet<>(Arrays.asList( + TAB_TYPE_DIVISION, TAB_TYPE_MEASURE, TAB_TYPE_OTHER, TAB_TYPE_UNIT_SUMMARY + )); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/config/ConfigUnitFieldDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/config/ConfigUnitFieldDO.java new file mode 100644 index 0000000..f4e1ce3 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/config/ConfigUnitFieldDO.java @@ -0,0 +1,64 @@ +package com.yhy.module.core.dal.dataobject.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 单位工程界面配置 - 工作台字段设置 DO + * 控制工作台的分部分项、措施项目、其他项目、单位汇总的字段显示/隐藏 + * + * @author yhy + */ +@TableName("yhy_config_unit_field") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ConfigUnitFieldDO extends BaseDO { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** 租户ID */ + private Long tenantId; + + /** 关联 fields_majors 节点ID */ + private Long catalogItemId; + + /** 序号(用户录入,不作为排序依据) */ + private Integer seqNo; + + /** 字段名称 */ + private String fieldName; + + /** 字段编码(英文字符串,供工作台逻辑判断) */ + private String fieldCode; + + /** 分部分项隐藏 */ + private Boolean divisionHidden; + + /** 措施项目隐藏 */ + private Boolean measureHidden; + + /** 其他项目隐藏 */ + private Boolean otherHidden; + + /** 汇总分析隐藏 */ + private Boolean summaryHidden; + + /** 备注 */ + private String remark; + + /** 排序字段 */ + private Integer sortOrder; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/config/ConfigUnitResourceFieldDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/config/ConfigUnitResourceFieldDO.java new file mode 100644 index 0000000..4c46531 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/config/ConfigUnitResourceFieldDO.java @@ -0,0 +1,55 @@ +package com.yhy.module.core.dal.dataobject.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 单位工程界面配置 - 工料机字段 DO + * 控制工作台的【子目工料机】【市场主材设备】的字段显示/隐藏 + * + * @author yhy + */ +@TableName("yhy_config_unit_resource_field") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ConfigUnitResourceFieldDO extends BaseDO { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** 租户ID */ + private Long tenantId; + + /** 关联 fields_majors 节点ID */ + private Long catalogItemId; + + /** 序号(用户录入) */ + private Integer seqNo; + + /** 字段名称 */ + private String fieldName; + + /** 字段编码 */ + private String fieldCode; + + /** 是否显示 */ + private Boolean visible; + + /** 备注 */ + private String remark; + + /** 排序字段 */ + private Integer sortOrder; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/config/ConfigUnitTabRefDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/config/ConfigUnitTabRefDO.java new file mode 100644 index 0000000..c313856 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/config/ConfigUnitTabRefDO.java @@ -0,0 +1,59 @@ +package com.yhy.module.core.dal.dataobject.config; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 单位工程界面配置 - 标签页引用 DO + * 措施项目/其他项目/单位汇总 引用分部分项模板中的节点 + * + * @author yhy + */ +@TableName("yhy_config_unit_tab_ref") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ConfigUnitTabRefDO extends BaseDO { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** 租户ID */ + private Long tenantId; + + /** 关联 fields_majors 节点ID */ + private Long catalogItemId; + + /** 标签页类型:measure/other/unit_summary */ + private String tabType; + + /** 引用的分部分项模板节点ID */ + private Long templateNodeId; + + /** 排序 */ + private Integer sortOrder; + + // 标签页类型常量 + public static final String TAB_TYPE_MEASURE = "measure"; + public static final String TAB_TYPE_OTHER = "other"; + public static final String TAB_TYPE_UNIT_SUMMARY = "unit_summary"; + + /** 有效的标签页类型集合 */ + public static final Set VALID_TAB_TYPES = new HashSet<>(Arrays.asList( + TAB_TYPE_MEASURE, TAB_TYPE_OTHER, TAB_TYPE_UNIT_SUMMARY + )); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/infoprice/InfoPriceBookDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/infoprice/InfoPriceBookDO.java index 7335ef1..77969d3 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/infoprice/InfoPriceBookDO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/infoprice/InfoPriceBookDO.java @@ -26,7 +26,7 @@ public class InfoPriceBookDO extends BaseDO { /** * 主键 */ - @TableId(type = IdType.AUTO) + @TableId(type = IdType.ASSIGN_ID) private Long id; /** @@ -69,6 +69,11 @@ public class InfoPriceBookDO extends BaseDO { */ private LocalDateTime publishTime; + /** + * 完成时间 + */ + private LocalDateTime completedTime; + /** * 附件 */ diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/infoprice/InfoPriceCategoryTreeDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/infoprice/InfoPriceCategoryTreeDO.java index 3358ff9..7c2a5c2 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/infoprice/InfoPriceCategoryTreeDO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/infoprice/InfoPriceCategoryTreeDO.java @@ -28,7 +28,7 @@ public class InfoPriceCategoryTreeDO extends BaseDO { /** * 主键 */ - @TableId(type = IdType.AUTO) + @TableId(type = IdType.ASSIGN_ID) private Long id; /** diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/infoprice/InfoPriceResourceDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/infoprice/InfoPriceResourceDO.java index d437ee4..be790ba 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/infoprice/InfoPriceResourceDO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/infoprice/InfoPriceResourceDO.java @@ -28,7 +28,7 @@ public class InfoPriceResourceDO extends BaseDO { /** * 主键 */ - @TableId(type = IdType.AUTO) + @TableId(type = IdType.ASSIGN_ID) private Long id; /** @@ -47,19 +47,9 @@ public class InfoPriceResourceDO extends BaseDO { private String code; /** - * 名称 + * 引用工料机ID(关联 yhy_resource_item) */ - private String name; - - /** - * 型号规格 - */ - private String spec; - - /** - * 单位 - */ - private String unit; + private Long sourceResourceItemId; /** * 除税编制价 @@ -96,11 +86,6 @@ public class InfoPriceResourceDO extends BaseDO { */ private Integer sortOrder; - /** - * 资源项ID(可选,关联标准库) - */ - private Long resourceItemId; - /** * 扩展属性 */ diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/infoprice/InfoPriceResourcePriceDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/infoprice/InfoPriceResourcePriceDO.java index 0e3c948..522c5eb 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/infoprice/InfoPriceResourcePriceDO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/infoprice/InfoPriceResourcePriceDO.java @@ -29,7 +29,7 @@ public class InfoPriceResourcePriceDO extends BaseDO { /** * 主键 */ - @TableId(type = IdType.AUTO) + @TableId(type = IdType.ASSIGN_ID) private Long id; /** diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaAdjustmentSettingDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaAdjustmentSettingDO.java index 7a75d8a..7b488ea 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaAdjustmentSettingDO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaAdjustmentSettingDO.java @@ -36,7 +36,7 @@ public class QuotaAdjustmentSettingDO extends TenantBaseDO { private Long id; /** - * 定额子目ID + * 定额基价ID */ private Long quotaItemId; diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaCatalogTreeDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaCatalogTreeDO.java index 61acf72..4fe0300 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaCatalogTreeDO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaCatalogTreeDO.java @@ -16,8 +16,8 @@ import lombok.NoArgsConstructor; import lombok.ToString; /** - * 定额子目树 DO - * 第二层:定额子目树(目录/内容) + * 定额基价树 DO + * 第二层:定额基价树(目录/内容) * * @author yhy */ @@ -97,4 +97,9 @@ public class QuotaCatalogTreeDO extends BaseDO { */ @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) private Map attributes; + + /** + * 注解/备注 + */ + private String remark; } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaFeeItemDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaFeeItemDO.java index 3413d4e..3a75a6b 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaFeeItemDO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaFeeItemDO.java @@ -90,12 +90,6 @@ public class QuotaFeeItemDO extends BaseDO { @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) private Map calcBase; - /** - * 费率代号(%) - * 如:"15"、"18.5" - */ - private String rateCode; - /** * 代号 */ @@ -127,8 +121,21 @@ public class QuotaFeeItemDO extends BaseDO { */ private Boolean variable; + /** + * 系统行标识 + * ZHDJ=综合单价(系统必须行,不可删除) + */ + private String systemCode; + // ==================== 便捷方法 ==================== + /** + * 是否为系统行(不可删除) + */ + public boolean isSystemRow() { + return systemCode != null && !systemCode.isEmpty(); + } + /** * 是否有计算基数 */ @@ -167,4 +174,14 @@ public class QuotaFeeItemDO extends BaseDO { public static final String PRICE_CODE_TAX_INCL_BASE = "tax_incl_base_price"; public static final String PRICE_CODE_TAX_EXCL_COMPILE = "tax_excl_compile_price"; public static final String PRICE_CODE_TAX_INCL_COMPILE = "tax_incl_compile_price"; + + /** + * 系统行代码:综合单价 + */ + public static final String SYSTEM_CODE_ZHDJ = "ZHDJ"; + + /** + * 综合单价行的排序值(确保始终排在最后) + */ + public static final Integer SYSTEM_ROW_SORT_ORDER = Integer.MAX_VALUE; } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaItemDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaItemDO.java index 21e69d9..88db26d 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaItemDO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaItemDO.java @@ -12,7 +12,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; /** - * 定额子目/基价 DO + * 定额基价/基价 DO * * 对应表 yhy_quota_item * @@ -33,12 +33,22 @@ public class QuotaItemDO extends TenantBaseDO { private Long id; /** - * 定额子目树节点ID(关联 yhy_quota_catalog_tree) + * 定额基价树节点ID(关联 yhy_quota_catalog_tree) * * 只能关联 content_type='content' 的节点 */ private Long catalogItemId; + /** + * 编码 + */ + private String code; + + /** + * 名称 + */ + private String name; + /** * 计量单位 */ @@ -70,6 +80,34 @@ public class QuotaItemDO extends TenantBaseDO { @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) private Map attributes; + /** + * 除税定额单价 + * + * 由子目工料机的除税基价合价自动计算:tax_excl_base_price = Σ(除税基价合价) + */ + private BigDecimal taxExclBasePrice; + + /** + * 含税定额单价 + * + * 由子目工料机的含税基价合价自动计算:tax_incl_base_price = Σ(含税基价合价) + */ + private BigDecimal taxInclBasePrice; + + /** + * 除税编制单价 + * + * 由子目工料机的除税编制价合价自动计算:tax_excl_compile_price = Σ(除税编制价合价) + */ + private BigDecimal taxExclCompilePrice; + + /** + * 含税编制单价 + * + * 由子目工料机的含税编制价合价自动计算:tax_incl_compile_price = Σ(含税编制价合价) + */ + private BigDecimal taxInclCompilePrice; + // ========== 便捷方法 ========== /** diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaMarketMaterialDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaMarketMaterialDO.java new file mode 100644 index 0000000..adee1e8 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaMarketMaterialDO.java @@ -0,0 +1,217 @@ +package com.yhy.module.core.dal.dataobject.quota; + +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import java.math.BigDecimal; +import java.util.Map; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 定额基价市场主材设备 DO + * + * 对应表 yhy_quota_market_material + * + * 定义定额基价包含哪些市场主材设备及其用量 + * 功能与子目工料机(QuotaResourceDO)类似,但独立存储 + * + * @author yhy + */ +@TableName(value = "yhy_quota_market_material", autoResultMap = true) +@KeySequence("yhy_quota_market_material_id_seq") +@Data +@EqualsAndHashCode(callSuper = true) +public class QuotaMarketMaterialDO extends TenantBaseDO { + + /** + * 主键 + */ + @TableId + private Long id; + + /** + * 定额基价ID(关联 yhy_quota_item) + */ + private Long quotaItemId; + + /** + * 资源项ID(关联 yhy_resource_item) + */ + private Long resourceItemId; + + /** + * 定额消耗量/用量 + */ + private BigDecimal dosage; + + /** + * 调整消耗量 + * + * 应用调整设置后的消耗量 + */ + private BigDecimal adjustedDosage; + + /** + * 排序字段 + */ + private Integer sortOrder; + + /** + * 扩展属性(JSONB) + * + * 可存储: + * - loss_rate: 损耗率 + * - price: 价格快照 + * - resource_code: 资源编码快照 + * - resource_name: 资源名称快照 + * - resource_unit: 资源单位快照 + * - 其他自定义字段 + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map attributes; + + // ========== 便捷方法 ========== + + /** + * 获取损耗率 + */ + public BigDecimal getLossRate() { + if (attributes == null) return BigDecimal.ZERO; + Object value = attributes.get("loss_rate"); + if (value instanceof Number) { + return new BigDecimal(value.toString()); + } + return BigDecimal.ZERO; + } + + /** + * 设置损耗率 + */ + public void setLossRate(BigDecimal lossRate) { + if (attributes == null) { + attributes = new java.util.HashMap<>(); + } + attributes.put("loss_rate", lossRate); + } + + /** + * 获取价格快照 + */ + public BigDecimal getPrice() { + if (attributes == null) return null; + Object value = attributes.get("price"); + if (value instanceof Number) { + return new BigDecimal(value.toString()); + } + return null; + } + + /** + * 设置价格快照 + */ + public void setPrice(BigDecimal price) { + if (attributes == null) { + attributes = new java.util.HashMap<>(); + } + attributes.put("price", price); + } + + /** + * 获取资源编码快照 + */ + public String getResourceCode() { + return attributes != null ? (String) attributes.get("resource_code") : null; + } + + /** + * 设置资源编码快照 + */ + public void setResourceCode(String resourceCode) { + if (attributes == null) { + attributes = new java.util.HashMap<>(); + } + attributes.put("resource_code", resourceCode); + } + + /** + * 获取资源名称快照 + */ + public String getResourceName() { + return attributes != null ? (String) attributes.get("resource_name") : null; + } + + /** + * 设置资源名称快照 + */ + public void setResourceName(String resourceName) { + if (attributes == null) { + attributes = new java.util.HashMap<>(); + } + attributes.put("resource_name", resourceName); + } + + /** + * 获取资源单位快照 + */ + public String getResourceUnit() { + return attributes != null ? (String) attributes.get("resource_unit") : null; + } + + /** + * 设置资源单位快照 + */ + public void setResourceUnit(String resourceUnit) { + if (attributes == null) { + attributes = new java.util.HashMap<>(); + } + attributes.put("resource_unit", resourceUnit); + } + + /** + * 获取资源型号规格快照 + */ + public String getResourceSpec() { + return attributes != null ? (String) attributes.get("resource_spec") : null; + } + + /** + * 设置资源型号规格快照 + */ + public void setResourceSpec(String resourceSpec) { + if (attributes == null) { + attributes = new java.util.HashMap<>(); + } + attributes.put("resource_spec", resourceSpec); + } + + /** + * 计算实际消耗量(含损耗) + */ + public BigDecimal getActualDosage() { + BigDecimal lossRate = getLossRate(); + return dosage.multiply(BigDecimal.ONE.add(lossRate)); + } + + /** + * 获取有效消耗量(用于计算合价) + */ + public BigDecimal getEffectiveDosage() { + if (adjustedDosage != null && adjustedDosage.compareTo(BigDecimal.ZERO) != 0) { + return adjustedDosage; + } + return dosage; + } + + /** + * 计算金额 + */ + public BigDecimal getAmount() { + BigDecimal price = getPrice(); + if (price == null) return null; + return getActualDosage().multiply(price); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaResourceDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaResourceDO.java index aad8e5b..67462d6 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaResourceDO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaResourceDO.java @@ -12,11 +12,11 @@ import lombok.Data; import lombok.EqualsAndHashCode; /** - * 定额子目工料机组成 DO + * 定额基价工料机组成 DO * * 对应表 yhy_quota_resource * - * 定义定额子目包含哪些工料机资源及其用量 + * 定义定额基价包含哪些工料机资源及其用量 * * @author yhy */ @@ -33,7 +33,7 @@ public class QuotaResourceDO extends TenantBaseDO { private Long id; /** - * 定额子目ID(关联 yhy_quota_item) + * 定额基价ID(关联 yhy_quota_item) */ private Long quotaItemId; @@ -49,6 +49,21 @@ public class QuotaResourceDO extends TenantBaseDO { */ private BigDecimal dosage; + /** + * 调整消耗量 + * + * 应用调整设置后的消耗量,计算公式:adjustedDosage = dosage * 系数 + * 当此字段非空且非0时,用此字段代替 dosage 计算合价 + */ + private BigDecimal adjustedDosage; + + /** + * 排序字段 + * + * 用于控制工料机在列表中的显示顺序 + */ + private Integer sortOrder; + /** * 扩展属性(JSONB) * @@ -187,6 +202,19 @@ public class QuotaResourceDO extends TenantBaseDO { return dosage.multiply(BigDecimal.ONE.add(lossRate)); } + /** + * 获取有效消耗量(用于计算合价) + * + * 如果 adjustedDosage 非空且非0,返回 adjustedDosage + * 否则返回 dosage + */ + public BigDecimal getEffectiveDosage() { + if (adjustedDosage != null && adjustedDosage.compareTo(BigDecimal.ZERO) != 0) { + return adjustedDosage; + } + return dosage; + } + /** * 计算金额 * diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaUnifiedFeeDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaUnifiedFeeDO.java new file mode 100644 index 0000000..94a5c96 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaUnifiedFeeDO.java @@ -0,0 +1,154 @@ +package com.yhy.module.core.dal.dataobject.quota; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import java.util.Map; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 统一取费单价 DO + * + * 树结构,用于配置统一取费的单价计算规则 + */ +@TableName(value = "yhy_quota_unified_fee", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +public class QuotaUnifiedFeeDO extends BaseDO { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 关联到"模式"节点(yhy_catalog_item.id,node_type='rate_mode') + */ + private Long catalogItemId; + + /** + * 关联定额取费项ID(yhy_quota_fee_item.id) + * 其他字段从定额取费动态获取,只有calcBase可编辑覆盖 + */ + private Long feeItemId; + + /** + * 父节点ID + */ + private Long parentId; + + /** + * 自定义序号(字符串,显示用,不参与排序) + */ + private String customCode; + + /** + * 名称 + */ + private String name; + + /** + * 计算基数(JSONB) + * + * 格式与 QuotaFeeItemDO.calcBase 一致 + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map calcBase; + + /** + * 费率代号(%) + */ + private String rateCode; + + /** + * 代号 + */ + private String code; + + /** + * 默认费用归属 + */ + private String feeCategory; + + /** + * 基数说明 + */ + private String baseDescription; + + /** + * 是否隐藏 + */ + private Boolean hidden; + + /** + * 是否费用变量 + */ + private Boolean variable; + + /** + * 节点类型 + * - parent:父节点 + * - child:子节点 + */ + private String nodeType; + + /** + * 扩展属性(JSONB) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map attributes; + + /** + * 排序字段 + */ + private Integer sortOrder; + + // ==================== 便捷方法 ==================== + + /** + * 是否有计算基数 + */ + public boolean hasCalcBase() { + return calcBase != null && !calcBase.isEmpty(); + } + + /** + * 获取公式 + */ + @SuppressWarnings("unchecked") + public String getFormula() { + if (calcBase == null) { + return null; + } + return (String) calcBase.get("formula"); + } + + /** + * 是否为父节点 + */ + public boolean isParent() { + return NODE_TYPE_PARENT.equals(nodeType); + } + + /** + * 是否为子节点 + */ + public boolean isChild() { + return NODE_TYPE_CHILD.equals(nodeType); + } + + // ==================== 常量 ==================== + + public static final String NODE_TYPE_PARENT = "parent"; + public static final String NODE_TYPE_CHILD = "child"; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaUnifiedFeeResourceDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaUnifiedFeeResourceDO.java new file mode 100644 index 0000000..da5cbcb --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaUnifiedFeeResourceDO.java @@ -0,0 +1,84 @@ +package com.yhy.module.core.dal.dataobject.quota; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import java.math.BigDecimal; +import java.util.Map; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 统一取费子目工料机 DO + * + * 关联到子定额(node_type='child')下的工料机组成 + * 结构与 QuotaResourceDO 一致,通过 resourceItemId 关联工料机基础数据 + */ +@TableName(value = "yhy_quota_unified_fee_resource", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +public class QuotaUnifiedFeeResourceDO extends BaseDO { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 关联子定额ID(yhy_quota_unified_fee_setting) + */ + private Long unifiedFeeSettingId; + + /** + * 关联工料机ID(yhy_resource_item) + */ + private Long resourceItemId; + + /** + * 定额消耗量 + */ + private BigDecimal dosage; + + /** + * 调整消耗量 + * + * 应用调整设置后的消耗量 + * 当此字段非空且非0时,用此字段代替 dosage 计算合价 + */ + private BigDecimal adjustedDosage; + + /** + * 扩展属性(JSONB) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map attributes; + + /** + * 排序字段 + */ + private Integer sortOrder; + + // ==================== 便捷方法 ==================== + + /** + * 获取有效消耗量(用于计算合价) + * + * 如果 adjustedDosage 非空且非0,返回 adjustedDosage + * 否则返回 dosage + */ + public BigDecimal getEffectiveDosage() { + if (adjustedDosage != null && adjustedDosage.compareTo(BigDecimal.ZERO) != 0) { + return adjustedDosage; + } + return dosage; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaUnifiedFeeSettingDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaUnifiedFeeSettingDO.java new file mode 100644 index 0000000..d16d892 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaUnifiedFeeSettingDO.java @@ -0,0 +1,137 @@ +package com.yhy.module.core.dal.dataobject.quota; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import java.math.BigDecimal; +import java.util.Map; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 统一取费设置 DO + * + * 树结构,支持父子定额关系: + * - 父定额(node_type='parent'):在工作台显示,配置总参数 + * - 子定额(node_type='child'):不在工作台显示,下方有子目工料机 + */ +@TableName(value = "yhy_quota_unified_fee_setting", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +public class QuotaUnifiedFeeSettingDO extends BaseDO { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 关联到"模式"节点(yhy_catalog_item.id,node_type='rate_mode') + */ + private Long catalogItemId; + + /** + * 父节点ID(树结构) + */ + private Long parentId; + + /** + * 自定义序号(字符串,显示用,不参与排序) + */ + private String customCode; + + /** + * 编号 + */ + private String code; + + /** + * 名称 + */ + private String name; + + /** + * 取费章节 + */ + private String feeChapter; + + /** + * 默认费用归属 + */ + private String feeCategory; + + /** + * 本清单比例% + */ + private BigDecimal thisListPercentage; + + /** + * 指定清单比例% + */ + private BigDecimal specifiedListPercentage; + + /** + * 指定清单编码 + */ + private String specifiedListCode; + + /** + * 节点类型 + * - parent:父定额(在工作台显示) + * - child:子定额(不在工作台显示) + */ + private String nodeType; + + /** + * 单位 + */ + private String unit; + + /** + * 扩展属性(JSONB) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map attributes; + + /** + * 排序字段 + */ + private Integer sortOrder; + + // ==================== 便捷方法 ==================== + + /** + * 是否为父定额 + */ + public boolean isParent() { + return NODE_TYPE_PARENT.equals(nodeType); + } + + /** + * 是否为子定额 + */ + public boolean isChild() { + return NODE_TYPE_CHILD.equals(nodeType); + } + + /** + * 是否为根节点 + */ + public boolean isRoot() { + return parentId == null; + } + + // ==================== 常量 ==================== + + public static final String NODE_TYPE_PARENT = "parent"; + public static final String NODE_TYPE_CHILD = "child"; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaVariableSettingDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaVariableSettingDO.java new file mode 100644 index 0000000..de59bfc --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/quota/QuotaVariableSettingDO.java @@ -0,0 +1,123 @@ +package com.yhy.module.core.dal.dataobject.quota; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import java.util.Map; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 单位工程变量设置 DO + * + * 按费率模式节点(node_type='rate_mode')配置变量设置 + * 分为四个类别:分部分项、措施项目、其他项目、单位汇总 + */ +@TableName(value = "yhy_quota_variable_setting", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +public class QuotaVariableSettingDO extends BaseDO { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 关联费率模式节点(yhy_quota_catalog_item.id,node_type='rate_mode') + */ + private Long catalogItemId; + + /** + * 类别 + * @see CategoryEnum + */ + private String category; + + /** + * 排序字段 + */ + private Integer sortOrder; + + /** + * 费用名称 + */ + private String name; + + /** + * 费用代号 + */ + private String code; + + /** + * 计算基数(JSONB) + * + * 格式与定额取费一致: + * { + * "formula": "DRGF+HDRGF/DCLF-(DRGF*DCLF+12)", + * "variables": { + * "DRGF": { + * "categoryId": 1, + * "priceField": "tax_excl_base_price" + * } + * } + * } + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map calcBase; + + // ==================== 便捷方法 ==================== + + /** + * 是否有计算基数 + */ + public boolean hasCalcBase() { + return calcBase != null && !calcBase.isEmpty(); + } + + /** + * 获取公式 + */ + public String getFormula() { + if (calcBase == null) { + return null; + } + return (String) calcBase.get("formula"); + } + + /** + * 获取 calcBase 中的变量映射 + */ + @SuppressWarnings("unchecked") + public Map getCalcBaseVariables() { + if (calcBase == null) { + return null; + } + return (Map) calcBase.get("variables"); + } + + // ==================== 枚举常量 ==================== + + /** + * 类别枚举 + */ + public static class CategoryEnum { + /** 分部分项 */ + public static final String DIVISION = "division"; + /** 措施项目 */ + public static final String MEASURE = "measure"; + /** 其他项目 */ + public static final String OTHER = "other"; + /** 单位汇总 */ + public static final String UNIT_SUMMARY = "unit_summary"; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/resource/ResourceInfoPriceMappingDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/resource/ResourceInfoPriceMappingDO.java new file mode 100644 index 0000000..a33190c --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/resource/ResourceInfoPriceMappingDO.java @@ -0,0 +1,63 @@ +package com.yhy.module.core.dal.dataobject.resource; + +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * 工料机-信息价关联 DO + * 用于记录工料机与信息价的多对多关联关系 + * + * @author yhy + */ +@TableName("yhy_resource_info_price_mapping") +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ResourceInfoPriceMappingDO extends TenantBaseDO { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 工料机ID(引用 yhy_resource_item.id) + */ + private Long resourceItemId; + + /** + * 信息价工料机ID(引用 yhy_info_price_resource.id) + */ + private Long infoPriceResourceId; + + /** + * 价格计算公式,如 xxj+20 + */ + private String formula; + + /** + * 选中的价格历史ID + */ + private Long selectedPriceId; + + /** + * 是否为当前价格来源 0-否 1-是 + */ + private Integer isCurrent; + + /** + * 项目ID(引用 yhy_proj_project.id) + * 价格来源影响项目级别,同一项目下所有使用该工料机的定额共享价格来源 + */ + private Long projectId; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/resource/ResourceItemDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/resource/ResourceItemDO.java index cbd70d2..78a77fb 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/resource/ResourceItemDO.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/resource/ResourceItemDO.java @@ -109,4 +109,19 @@ public class ResourceItemDO extends TenantBaseDO { * 工料机机类树节点ID(引用yhy_resource_category_tree) */ private Long categoryTreeId; + + /** + * 来源类型:system-后台创建, tenant-租户创建 + */ + private String sourceType; + + /** + * 排序字段 + */ + private Integer sortOrder; + + // 来源类型常量 + public static final String SOURCE_TYPE_SYSTEM = "system"; + public static final String SOURCE_TYPE_TENANT = "tenant"; + public static final String SOURCE_TYPE_INFO_PRICE = "info_price"; } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/AuditApproveDivisionDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/AuditApproveDivisionDO.java new file mode 100644 index 0000000..0437859 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/AuditApproveDivisionDO.java @@ -0,0 +1,137 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import com.yhy.module.core.dal.typehandler.PostgreSQLTextArrayTypeHandler; +import java.math.BigDecimal; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 审定分部分项快照 DO(可编辑) + * + * @author yhy + */ +@TableName(value = "yhy_wb_audit_approve_division", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AuditApproveDivisionDO extends BaseDO { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 关联审核模式ID + */ + private Long auditModeId; + + /** + * 关联编制模式树的单位工程节点ID(直接引用,不复制) + */ + private Long compileTreeId; + + /** + * 来源分部分项ID(关联用) + */ + private Long sourceDivisionId; + + /** + * 父节点ID + */ + private Long parentId; + + /** + * 节点类型 + */ + private String nodeType; + + /** + * 来源类型 + */ + private String sourceType; + + /** + * 编码 + */ + private String code; + + /** + * 名称 + */ + private String name; + + /** + * 单位 + */ + private String unit; + + /** + * 项目特征 + */ + private String feature; + + /** + * 排序号 + */ + private Integer sortOrder; + + /** + * 树路径 + */ + @TableField(typeHandler = PostgreSQLTextArrayTypeHandler.class) + private String[] path; + + /** + * 审定工程量 + */ + private BigDecimal approveQty; + + /** + * 审定单价 + */ + private BigDecimal approveUnitPrice; + + /** + * 审定费率 + */ + private BigDecimal approveRate; + + /** + * 审定合价 + */ + private BigDecimal approveTotalPrice; + + /** + * 扩展属性(JSONB) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map attributes; + + // 节点类型常量 + public static final String NODE_TYPE_ROOT = "root"; + public static final String NODE_TYPE_DIVISION = "division"; + public static final String NODE_TYPE_BOQ = "boq"; + public static final String NODE_TYPE_QUOTA = "quota"; + public static final String NODE_TYPE_UNIFIED_FEE = "unified_fee"; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/AuditApproveResourceDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/AuditApproveResourceDO.java new file mode 100644 index 0000000..8c71423 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/AuditApproveResourceDO.java @@ -0,0 +1,113 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import java.math.BigDecimal; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 审定工料机快照 DO(可编辑) + * + * @author yhy + */ +@TableName(value = "yhy_wb_audit_approve_resource", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AuditApproveResourceDO extends BaseDO { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 关联审定分部分项 + */ + private Long auditApproveDivisionId; + + /** + * 来源工料机ID + */ + private Long sourceResourceId; + + /** + * 父工料机ID(复合工料机) + */ + private Long parentId; + + /** + * 类型:labor/material/machine + */ + private String resourceType; + + /** + * 编码 + */ + private String code; + + /** + * 名称 + */ + private String name; + + /** + * 规格型号 + */ + private String spec; + + /** + * 单位 + */ + private String unit; + + /** + * 类别ID + */ + private Long categoryId; + + /** + * 排序号 + */ + private Integer sortOrder; + + /** + * 审定消耗量 + */ + private BigDecimal approveConsumeQty; + + /** + * 审定单价 + */ + private BigDecimal approvePrice; + + /** + * 扩展属性(JSONB) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map attributes; + + // 资源类型常量 + public static final String RESOURCE_TYPE_LABOR = "labor"; + public static final String RESOURCE_TYPE_MATERIAL = "material"; + public static final String RESOURCE_TYPE_MACHINE = "machine"; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/AuditModeDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/AuditModeDO.java new file mode 100644 index 0000000..f8243a8 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/AuditModeDO.java @@ -0,0 +1,78 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import java.time.LocalDateTime; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 审核模式 DO + * + * @author yhy + */ +@TableName(value = "yhy_wb_audit_mode", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AuditModeDO extends BaseDO { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 关联项目节点ID(yhy_wb_project_tree.id) + */ + private Long projectId; + + /** + * 审核模式名称 + */ + private String name; + + /** + * 状态:draft/submitted/approved + */ + private String status; + + /** + * 送审时间 + */ + private LocalDateTime submitTime; + + /** + * 审定时间 + */ + private LocalDateTime approveTime; + + /** + * 扩展属性(JSONB) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map attributes; + + // 状态常量 + public static final String STATUS_DRAFT = "draft"; + public static final String STATUS_SUBMITTED = "submitted"; + public static final String STATUS_APPROVED = "approved"; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/ProgressDivisionDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/ProgressDivisionDO.java new file mode 100644 index 0000000..47f2c9e --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/ProgressDivisionDO.java @@ -0,0 +1,55 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import java.math.BigDecimal; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 进度款-清单关联 DO + * 存储进度款模式下清单的期数值 + * + * @author yhy + */ +@TableName(value = "yhy_wb_progress_division") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ProgressDivisionDO extends BaseDO { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 关联进度款模式ID + */ + private Long progressPaymentModeId; + + /** + * 关联清单节点ID(yhy_wb_boq_division.id,node_type='boq') + */ + private Long boqDivisionId; + + /** + * 期数值 + */ + private BigDecimal periodValue; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/ProgressPaymentModeDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/ProgressPaymentModeDO.java new file mode 100644 index 0000000..db3ee4d --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/ProgressPaymentModeDO.java @@ -0,0 +1,105 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 进度款模式 DO + * + * @author yhy + */ +@TableName(value = "yhy_wb_progress_payment_mode") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ProgressPaymentModeDO extends BaseDO { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 项目ID + */ + private Long projectId; + + /** + * 名称 + */ + private String name; + + /** + * 期数序号 + */ + private Integer periodNumber; + + /** + * 开始日期 + */ + private LocalDate startDate; + + /** + * 结束日期 + */ + private LocalDate endDate; + + /** + * 总造价 + */ + private BigDecimal totalCost; + + /** + * 本期产值 + */ + private BigDecimal currentPeriodValue; + + /** + * 已完产值 + */ + private BigDecimal completedValue; + + /** + * 已完比例 + */ + private BigDecimal completedRatio; + + /** + * 支付比例(%) + */ + private BigDecimal paymentRatio; + + /** + * 支付金额 + */ + private BigDecimal paymentAmount; + + /** + * 材料调差 + */ + private BigDecimal materialAdjustment; + + /** + * 备注 + */ + private String remark; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/SyncBindingDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/SyncBindingDO.java new file mode 100644 index 0000000..250690a --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/SyncBindingDO.java @@ -0,0 +1,47 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 同步绑定关系 DO + * 记录同步库与工作台分部分项的绑定关系 + * + * @author yhy + */ +@TableName(value = "yhy_wb_sync_binding", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SyncBindingDO extends BaseDO { + + /** 主键ID */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** 租户ID */ + private Long tenantId; + + /** 同步库ID */ + private Long syncLibraryId; + + /** 工作台分部分项ID */ + private Long divisionId; + + /** 单位工程ID */ + private Long compileTreeId; + + /** 已同步的版本号 */ + private Integer syncedVersion; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/SyncLibraryDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/SyncLibraryDO.java new file mode 100644 index 0000000..7171729 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/SyncLibraryDO.java @@ -0,0 +1,76 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import java.math.BigDecimal; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 同步库主表 DO + * 存储跨单位工程共享的清单/分部数据 + * + * @author yhy + */ +@TableName(value = "yhy_wb_sync_library", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SyncLibraryDO extends BaseDO { + + /** 主键ID */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** 租户ID */ + private Long tenantId; + + /** 项目ID */ + private Long projectId; + + /** 来源分部分项ID(首次设为同步时的节点) */ + private Long sourceDivisionId; + + /** 来源单位工程ID */ + private Long sourceCompileTreeId; + + /** 节点类型: boq-清单, division-分部 */ + private String nodeType; + + /** 原始编码 */ + private String code; + + /** 名称 */ + private String name; + + /** 项目特征 */ + private String feature; + + /** 单位 */ + private String unit; + + /** 工程量 */ + private BigDecimal qty; + + /** 费率 */ + private BigDecimal rate; + + /** 扩展属性(JSONB) */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map attributes; + + /** 版本号(每次编辑+1,用于检测变更) */ + private Integer version; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/SyncLibraryDivisionDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/SyncLibraryDivisionDO.java new file mode 100644 index 0000000..92db2e6 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/SyncLibraryDivisionDO.java @@ -0,0 +1,99 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import com.yhy.module.core.dal.typehandler.PostgreSQLTextArrayTypeHandler; +import java.math.BigDecimal; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 同步库分部分项树 DO + * 存储同步库内的分部→清单→定额树结构 + * + * @author yhy + */ +@TableName(value = "yhy_wb_sync_library_division", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SyncLibraryDivisionDO extends BaseDO { + + /** 主键ID */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** 租户ID */ + private Long tenantId; + + /** 关联同步库主表ID */ + private Long syncLibraryId; + + /** 父节点ID */ + private Long parentId; + + /** 节点类型: division/boq/quota/work_content */ + private String nodeType; + + /** 来源类型 */ + private String sourceType; + + /** 引用清单目录ID */ + private Long sourceBoqCatalogId; + + /** 引用清单项树ID */ + private Long sourceBoqItemTreeId; + + /** 来源定额基价ID */ + private Long sourceQuotaItemId; + + /** 编码 */ + private String code; + + /** 名称 */ + private String name; + + /** 项目特征 */ + private String feature; + + /** 单位 */ + private String unit; + + /** 工程量 */ + private BigDecimal qty; + + /** 费率 */ + private BigDecimal rate; + + /** 费用代号 */ + private String costCode; + + /** 定额工程量公式 */ + private String quotaQtyFormula; + + /** 排序号 */ + private Integer sortOrder; + + /** 层级路径 */ + @TableField(typeHandler = PostgreSQLTextArrayTypeHandler.class) + private String[] path; + + /** 扩展属性(JSONB) */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map attributes; + + /** 备注 */ + private String remark; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/SyncLibraryMarketMaterialDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/SyncLibraryMarketMaterialDO.java new file mode 100644 index 0000000..86ba694 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/SyncLibraryMarketMaterialDO.java @@ -0,0 +1,71 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import java.math.BigDecimal; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 同步库市场主材设备 DO + * + * @author yhy + */ +@TableName(value = "yhy_wb_sync_library_market_material", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SyncLibraryMarketMaterialDO extends BaseDO { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + private Long tenantId; + /** 关联同步库分部分项ID(定额节点) */ + private Long syncDivisionId; + private Long parentId; + private Long sourceResourceItemId; + private Long sourceMarketMaterialId; + private String resourceType; + private String code; + private String name; + private String spec; + private String unit; + private BigDecimal consumeQty; + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal adjustConsumeQty; + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal taxRate; + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal taxExclBasePrice; + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal taxInclBasePrice; + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal taxExclCompilePrice; + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal taxInclCompilePrice; + private BigDecimal adjustRate; + private Long categoryId; + private Long parentResourceId; + private String sourceType; + private BigDecimal baseConsumeQty; + private BigDecimal baseBasePrice; + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map snapshotJson; + private BigDecimal usageQty; + private Integer sortOrder; + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map attributes; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/SyncLibraryResourceDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/SyncLibraryResourceDO.java new file mode 100644 index 0000000..0953b03 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/SyncLibraryResourceDO.java @@ -0,0 +1,73 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import java.math.BigDecimal; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 同步库工料机 DO + * 存储同步库定额节点下的工料机数据 + * + * @author yhy + */ +@TableName(value = "yhy_wb_sync_library_resource", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SyncLibraryResourceDO extends BaseDO { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + private Long tenantId; + /** 关联同步库分部分项ID(定额节点) */ + private Long syncDivisionId; + /** 父工料机ID(复合工料机) */ + private Long parentId; + private Long sourceResourceItemId; + private Long sourceQuotaResourceId; + /** 类型: labor/material/machine */ + private String resourceType; + private String code; + private String name; + private String spec; + private String unit; + private BigDecimal consumeQty; + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal adjustConsumeQty; + private BigDecimal adjustRate; + private BigDecimal baseConsumeQty; + private BigDecimal baseBasePrice; + private BigDecimal usageQty; + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal taxRate; + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal taxExclBasePrice; + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal taxInclBasePrice; + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal taxExclCompilePrice; + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal taxInclCompilePrice; + private Long categoryId; + private String sourceType; + private Integer sortOrder; + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map attributes; + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map snapshotJson; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbAdjustmentSettingDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbAdjustmentSettingDO.java new file mode 100644 index 0000000..86b1f4d --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbAdjustmentSettingDO.java @@ -0,0 +1,91 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import com.yhy.module.core.util.AdjustmentFormulaCalculator; +import java.math.BigDecimal; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 工作台定额调整设置 DO + * 复制模式:每条定额有自己独立的调整设置 + * + * @author yhy + */ +@TableName(value = "yhy_wb_adjustment_setting", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WbAdjustmentSettingDO extends BaseDO implements AdjustmentFormulaCalculator.AdjustmentSetting { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 关联分部分项树的定额节点ID(yhy_wb_boq_division.id,node_type=quota) + */ + private Long divisionId; + + /** + * 来源定额调整设置ID(用于追溯) + */ + private Long sourceAdjustmentSettingId; + + /** + * 调整名称 + */ + private String name; + + /** + * 定额值 + */ + private BigDecimal quotaValue; + + /** + * 调整内容 + */ + private String adjustmentContent; + + /** + * 调整类型 + */ + private String adjustmentType; + + /** + * 调整规则(JSONB) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map adjustmentRules; + + /** + * 排序号 + */ + private Integer sortOrder; + + /** + * 快照信息(JSONB) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map snapshotJson; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbBoqDivisionDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbBoqDivisionDO.java new file mode 100644 index 0000000..4475b23 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbBoqDivisionDO.java @@ -0,0 +1,192 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import com.yhy.module.core.dal.typehandler.PostgreSQLTextArrayTypeHandler; +import java.math.BigDecimal; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 工作台分部分项树 DO + * 节点类型:division-分部, boq-清单, quota-定额 + * 层级关系:分部 → 清单 → 定额 + * + * @author yhy + */ +@TableName(value = "yhy_wb_boq_division", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WbBoqDivisionDO extends BaseDO { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 关联编制模式树的单位工程节点ID + */ + private Long compileTreeId; + + /** + * 父节点ID + */ + private Long parentId; + + /** + * 节点类型:division-分部, boq-清单, quota-定额 + */ + private String nodeType; + + /** + * 来源类型:catalog-从标准库引用, manual-手工录入 + */ + private String sourceType; + + /** + * 引用清单目录ID(分部/清单用) + */ + private Long sourceBoqCatalogId; + + /** + * 引用清单项树ID + */ + private Long sourceBoqItemTreeId; + + /** + * 引用定额基价ID(定额用) + */ + private Long sourceQuotaItemId; + + /** + * 编码 + */ + private String code; + + /** + * 名称 + */ + private String name; + + /** + * 项目特征 + */ + private String feature; + + /** + * 单位(字典) + */ + private String unit; + + /** + * 工程量 + */ + private BigDecimal qty; + + /** + * 费用代号(只允许英文字符串,代表合价的值,用于公式计算) + */ + private String costCode; + + /** + * 费率 + */ + private BigDecimal rate; + + /** + * 行号 + */ + private String lineNo; + + /** + * 排序号 + */ + private Integer sortOrder; + + /** + * 层级路径(ID数组) + */ + @TableField(typeHandler = PostgreSQLTextArrayTypeHandler.class) + private String[] path; + + /** + * 预留:基线编码 + */ + private String baseCode; + + /** + * 预留:基线名称 + */ + private String baseName; + + /** + * 预留:基线单位 + */ + private String baseUnit; + + /** + * 预留:完整快照(JSONB) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map snapshotJson; + + /** + * 扩展属性(JSONB) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map attributes; + + /** + * 备注 + */ + private String remark; + + /** + * 定额工程量公式(如 QDL*2,QDL代表清单工程量) + * 仅定额节点使用 + */ + private String quotaQtyFormula; + + /** + * 关联同步库ID(非空表示已同步) + */ + private Long syncLibraryId; + + /** + * 是否为同步来源节点 + */ + private Boolean isSyncSource; + + // 节点类型常量 + public static final String NODE_TYPE_ROOT = "root"; // 根目录(单位工程) + public static final String NODE_TYPE_DIVISION = "division"; + public static final String NODE_TYPE_BOQ = "boq"; + public static final String NODE_TYPE_QUOTA = "quota"; + public static final String NODE_TYPE_UNIFIED_FEE = "unified_fee"; // 统一取费节点 + + // 来源类型常量 + public static final String SOURCE_TYPE_CATALOG = "catalog"; + public static final String SOURCE_TYPE_MANUAL = "manual"; + public static final String SOURCE_TYPE_SYSTEM = "system"; // 系统自动创建 + public static final String SOURCE_TYPE_COPY = "copy"; // 历史套用复制 +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbBoqMarketMaterialDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbBoqMarketMaterialDO.java new file mode 100644 index 0000000..b139e4a --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbBoqMarketMaterialDO.java @@ -0,0 +1,186 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import java.math.BigDecimal; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 工作台市场主材设备消耗 DO + * 关联定额节点下的市场主材设备数据 + * + * @author yhy + */ +@TableName(value = "yhy_wb_boq_market_material", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WbBoqMarketMaterialDO extends BaseDO { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 关联定额节点ID(yhy_wb_boq_division.id,node_type=quota) + */ + private Long divisionId; + + /** + * 引用工料机ID + */ + private Long sourceResourceItemId; + + /** + * 引用定额市场主材设备ID + */ + private Long sourceMarketMaterialId; + + /** + * 类型:labor-人工, material-材料, machine-机械 + */ + private String resourceType; + + /** + * 编码 + */ + private String code; + + /** + * 名称 + */ + private String name; + + /** + * 规格型号 + */ + private String spec; + + /** + * 单位 + */ + private String unit; + + /** + * 消耗量(原始值) + */ + private BigDecimal consumeQty; + + /** + * 调整消耗量(用户手动覆写,null表示使用原始逻辑值) + */ + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal adjustConsumeQty; + + /** + * 税率 + */ + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal taxRate; + + /** + * 除税基价 + */ + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal taxExclBasePrice; + + /** + * 含税基价 + */ + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal taxInclBasePrice; + + /** + * 除税编制价 + */ + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal taxExclCompilePrice; + + /** + * 含税编制价 + */ + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal taxInclCompilePrice; + + /** + * 调整系数 + */ + private BigDecimal adjustRate; + + /** + * 机类ID + */ + private Long categoryId; + + /** + * 父工料机ID(复合工料机的子工料机使用) + */ + private Long parentId; + + /** + * 来源类型:system-从定额复制, tenant-租户手动添加 + */ + private String sourceType; + + /** + * 预留:基线消耗量 + */ + private BigDecimal baseConsumeQty; + + /** + * 预留:基线基价 + */ + private BigDecimal baseBasePrice; + + /** + * 预留:完整快照(JSONB) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map snapshotJson; + + /** + * 用量(工作台独有字段,用户手动录入) + */ + private BigDecimal usageQty; + + /** + * 排序号 + */ + private Integer sortOrder; + + /** + * 扩展属性(JSONB) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map attributes; + + // 资源类型常量 + public static final String RESOURCE_TYPE_LABOR = "labor"; + public static final String RESOURCE_TYPE_MATERIAL = "material"; + public static final String RESOURCE_TYPE_MACHINE = "machine"; + + // 来源类型常量 + public static final String SOURCE_TYPE_SYSTEM = "system"; + public static final String SOURCE_TYPE_TENANT = "tenant"; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbBoqResourceDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbBoqResourceDO.java new file mode 100644 index 0000000..cb81ee7 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbBoqResourceDO.java @@ -0,0 +1,191 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import java.math.BigDecimal; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 工作台工料机消耗 DO + * 关联定额节点下的工料机数据 + * + * @author yhy + */ +@TableName(value = "yhy_wb_boq_resource", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WbBoqResourceDO extends BaseDO { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 关联定额节点ID(yhy_wb_boq_division.id,node_type=quota) + */ + private Long divisionId; + + /** + * 引用工料机ID + */ + private Long sourceResourceItemId; + + /** + * 引用定额工料机组成ID + */ + private Long sourceQuotaResourceId; + + /** + * 类型:labor-人工, material-材料, machine-机械 + */ + private String resourceType; + + /** + * 编码 + */ + private String code; + + /** + * 名称 + */ + private String name; + + /** + * 规格型号 + */ + private String spec; + + /** + * 单位 + */ + private String unit; + + /** + * 消耗量(原始值) + */ + private BigDecimal consumeQty; + + /** + * 调整消耗量(用户手动覆写,null表示使用原始逻辑值) + */ + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal adjustConsumeQty; + + /** + * 税率 + * 单位为%的工料机此字段必须为空,使用 ALWAYS 策略确保可以更新为 null + */ + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal taxRate; + + /** + * 除税基价 + * 单位为%的工料机此字段必须为空,使用 ALWAYS 策略确保可以更新为 null + */ + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal taxExclBasePrice; + + /** + * 含税基价 + * 单位为%的工料机此字段必须为空,使用 ALWAYS 策略确保可以更新为 null + */ + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal taxInclBasePrice; + + /** + * 除税编制价 + * 单位为%的工料机此字段必须为空,使用 ALWAYS 策略确保可以更新为 null + */ + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal taxExclCompilePrice; + + /** + * 含税编制价 + * 单位为%的工料机此字段必须为空,使用 ALWAYS 策略确保可以更新为 null + */ + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal taxInclCompilePrice; + + /** + * 调整系数 + */ + private BigDecimal adjustRate; + + /** + * 机类ID + */ + private Long categoryId; + + /** + * 父工料机ID(复合工料机的子工料机使用) + */ + private Long parentId; + + /** + * 来源类型:system-从定额复制, tenant-租户手动添加 + */ + private String sourceType; + + /** + * 预留:基线消耗量 + */ + private BigDecimal baseConsumeQty; + + /** + * 预留:基线基价 + */ + private BigDecimal baseBasePrice; + + /** + * 预留:完整快照(JSONB) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map snapshotJson; + + /** + * 用量(工作台独有字段,用户手动录入) + */ + private BigDecimal usageQty; + + /** + * 排序号 + */ + private Integer sortOrder; + + /** + * 扩展属性(JSONB) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map attributes; + + // 资源类型常量 + public static final String RESOURCE_TYPE_LABOR = "labor"; + public static final String RESOURCE_TYPE_MATERIAL = "material"; + public static final String RESOURCE_TYPE_MACHINE = "machine"; + + // 来源类型常量 + public static final String SOURCE_TYPE_SYSTEM = "system"; + public static final String SOURCE_TYPE_TENANT = "tenant"; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbCategoryTreeDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbCategoryTreeDO.java new file mode 100644 index 0000000..1385839 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbCategoryTreeDO.java @@ -0,0 +1,85 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import com.yhy.module.core.dal.typehandler.PostgreSQLTextArrayTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Map; + +/** + * 工作台-工料机机类树快照 DO + * + * 创建单位工程时从后台 yhy_resource_category_tree 复制 + * + * @author yihuiyong + */ +@TableName(value = "yhy_wb_category_tree", autoResultMap = true) +@KeySequence("yhy_wb_category_tree_id_seq") +@Data +@EqualsAndHashCode(callSuper = true) +public class WbCategoryTreeDO extends TenantBaseDO { + + /** + * 主键 + */ + @TableId + private Long id; + + /** + * 关联单位工程ID(yhy_wb_compile_tree.id,node_type='unit') + */ + private Long compileTreeId; + + /** + * 来源后台ID(yhy_resource_category_tree.id) + */ + private Long sourceId; + + /** + * 资源库版本ID + */ + private Long catalogId; + + /** + * 父节点ID + */ + private Long parentId; + + /** + * 节点编码 + */ + private String code; + + /** + * 节点名称 + */ + private String name; + + /** + * 节点类型:region(地区)/specialty(工料机专业) + */ + private String nodeType; + + /** + * 树路径(text[]) + */ + @TableField(typeHandler = PostgreSQLTextArrayTypeHandler.class) + private String[] path; + + /** + * 排序 + */ + private Integer sortOrder; + + /** + * 扩展属性(jsonb) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map attributes; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbCategoryTreeMappingDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbCategoryTreeMappingDO.java new file mode 100644 index 0000000..e6b5edb --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbCategoryTreeMappingDO.java @@ -0,0 +1,53 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.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; + +/** + * 工作台-机类与类别字典关联快照 DO + * + * 创建单位工程时从后台 yhy_resource_category_tree_mapping 复制 + * + * @author yihuiyong + */ +@TableName("yhy_wb_category_tree_mapping") +@KeySequence("yhy_wb_category_tree_mapping_id_seq") +@Data +@EqualsAndHashCode(callSuper = true) +public class WbCategoryTreeMappingDO extends TenantBaseDO { + + /** + * 主键 + */ + @TableId + private Long id; + + /** + * 关联单位工程ID + */ + private Long compileTreeId; + + /** + * 来源后台ID(yhy_resource_category_tree_mapping.id) + */ + private Long sourceId; + + /** + * 工料机机类树节点ID(关联 yhy_wb_category_tree.id) + */ + private Long categoryTreeId; + + /** + * 类别字典ID(关联 yhy_resource_category.id) + */ + private Long categoryId; + + /** + * 排序 + */ + private Integer sortOrder; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbCompileTreeDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbCompileTreeDO.java new file mode 100644 index 0000000..a001ac7 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbCompileTreeDO.java @@ -0,0 +1,85 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import com.yhy.module.core.dal.typehandler.PostgreSQLTextArrayTypeHandler; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 编制模式树 DO + * 节点类型:root-根节点, item-单项, unit-单位工程 + * + * @author yhy + */ +@TableName(value = "yhy_wb_compile_tree", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WbCompileTreeDO extends BaseDO { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 关联项目节点ID + */ + private Long projectId; + + /** + * 父节点ID + */ + private Long parentId; + + /** + * 节点类型:root-根节点, item-单项, unit-单位工程 + */ + private String nodeType; + + /** + * 名称 + */ + private String name; + + /** + * 排序号 + */ + private Integer sortOrder; + + /** + * 层级路径(ID数组) + */ + @TableField(typeHandler = PostgreSQLTextArrayTypeHandler.class) + private String[] path; + + /** + * 扩展属性(JSONB) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map attributes; + + // 节点类型常量 + public static final String NODE_TYPE_ROOT = "root"; + public static final String NODE_TYPE_ITEM = "item"; + public static final String NODE_TYPE_UNIT = "unit"; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbFeeItemDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbFeeItemDO.java new file mode 100644 index 0000000..6a8baff --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbFeeItemDO.java @@ -0,0 +1,107 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Map; + +/** + * 工作台-取费模板快照 DO + * + * 创建单位工程时从后台 yhy_quota_fee_item 复制 + * + * @author yihuiyong + */ +@TableName(value = "yhy_wb_fee_item", autoResultMap = true) +@KeySequence("yhy_wb_fee_item_id_seq") +@Data +@EqualsAndHashCode(callSuper = true) +public class WbFeeItemDO extends TenantBaseDO { + + /** + * 主键ID + */ + @TableId + private Long id; + + /** + * 关联单位工程ID + */ + private Long compileTreeId; + + /** + * 来源后台ID(yhy_quota_fee_item.id) + */ + private Long sourceId; + + /** + * 关联到"模式"节点 + */ + private Long catalogItemId; + + /** + * 关联的费率项ID + */ + private Long rateItemId; + + /** + * 自定义序号 + */ + private String customCode; + + /** + * 名称 + */ + private String name; + + /** + * 计算基数(JSONB) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map calcBase; + + /** + * 代号 + */ + private String code; + + /** + * 费用归属 + */ + private String feeCategory; + + /** + * 基数说明 + */ + private String baseDescription; + + /** + * 排序字段 + */ + private Integer sortOrder; + + /** + * 是否隐藏 + */ + private Boolean hidden; + + /** + * 是否为变量 + */ + private Boolean variable; + + /** + * 系统行标识:ZHDJ=综合单价 + */ + private String systemCode; + + // ==================== 常量 ==================== + + public static final String SYSTEM_CODE_ZHDJ = "ZHDJ"; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbItemInfoDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbItemInfoDO.java new file mode 100644 index 0000000..747f418 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbItemInfoDO.java @@ -0,0 +1,58 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 单项基本信息 DO + * + * @author yhy + */ +@TableName(value = "yhy_wb_item_info", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WbItemInfoDO extends BaseDO { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 关联编制树节点ID(单项节点) + */ + private Long compileTreeId; + + /** + * 项目界面配置快照(创建时复制) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map configSnapshot; + + /** + * 基本信息数据(用户填写的值) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map infoData; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbProjectTreeDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbProjectTreeDO.java new file mode 100644 index 0000000..da1ec3f --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbProjectTreeDO.java @@ -0,0 +1,372 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLBigintArrayTypeHandler; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import com.yhy.module.core.dal.typehandler.PostgreSQLTextArrayTypeHandler; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 工作台项目管理树 DO + * 节点类型:directory-目录, project-项目 + * 目录节点只需要名称,项目节点包含完整业务字段 + * + * @author yhy + */ +@TableName(value = "yhy_wb_project_tree", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WbProjectTreeDO extends BaseDO { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 父节点ID + */ + private Long parentId; + + /** + * 节点类型:directory-目录, project-项目 + */ + private String nodeType; + + /** + * 名称(目录名称或项目名称) + */ + private String name; + + /** + * 项目编号(仅项目节点,手动录入) + */ + private String projectCode; + + /** + * 行业-省市(仅项目节点) + */ + private Long industryProvinceId; + + /** + * 行业(仅项目节点) + */ + private Long industryId; + + /** + * 信息价专业叶子节点ID(仅项目节点) + */ + private Long infoPriceProfessionId; + + /** + * 信息价专业完整路径(仅项目节点) + */ + @TableField(typeHandler = PostgreSQLBigintArrayTypeHandler.class) + private Long[] infoPriceProfessionPath; + + /** + * 信息价地区叶子节点ID(仅项目节点) + */ + private Long infoPriceRegionId; + + /** + * 信息价地区完整路径(仅项目节点) + */ + @TableField(typeHandler = PostgreSQLBigintArrayTypeHandler.class) + private Long[] infoPriceRegionPath; + + /** + * 信息价册ID(仅项目节点) + */ + private Long infoPriceBookId; + + /** + * 工作内容(仅项目节点,字典:wb_work_content) + */ + private String workContent; + + /** + * 文件类型(仅项目节点,字典:wb_file_type) + */ + private String fileType; + + /** + * 参与人员ID数组(仅项目节点) + */ + @TableField(typeHandler = PostgreSQLBigintArrayTypeHandler.class) + private Long[] memberIds; + + /** + * 项目状态(仅项目节点):draft-草稿, active-进行中, archived-已归档 + */ + private String status; + + /** + * 备注 + */ + private String remark; + + /** + * 是否已保存至历史库 + */ + private Boolean inHistoryLibrary; + + /** + * 排序号 + */ + private Integer sortOrder; + + /** + * 层级路径(ID数组) + */ + @TableField(typeHandler = PostgreSQLTextArrayTypeHandler.class) + private String[] path; + + /** + * 快照数据(仅项目节点,创建时的引用数据快照) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map snapshotJson; + + /** + * 扩展属性(JSONB) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map attributes; + + // 节点类型常量 + public static final String NODE_TYPE_DIRECTORY = "directory"; + public static final String NODE_TYPE_PROJECT = "project"; + + // 状态常量 + public static final String STATUS_DRAFT = "draft"; + public static final String STATUS_ACTIVE = "active"; + public static final String STATUS_ARCHIVED = "archived"; + + // attributes 中的 key 常量 + public static final String ATTR_RATE_MODE_BINDINGS = "rateModeBindings"; + public static final String ATTR_LAST_SELECTED_QUOTA_SPECIALTY = "lastSelectedQuotaSpecialty"; + + /** + * 获取费率模式绑定Map + * 格式: { "定额专业ID": { "rateModeId": 123, "rateModeName": "xxx", "quotaSpecialtyName": "xxx" } } + */ + @SuppressWarnings("unchecked") + public Map> getRateModeBindings() { + if (attributes == null) { + return new java.util.HashMap<>(); + } + Object bindings = attributes.get(ATTR_RATE_MODE_BINDINGS); + if (bindings instanceof Map) { + return (Map>) bindings; + } + return new java.util.HashMap<>(); + } + + /** + * 设置费率模式绑定Map + */ + public void setRateModeBindings(Map> rateModeBindings) { + if (attributes == null) { + attributes = new java.util.HashMap<>(); + } + attributes.put(ATTR_RATE_MODE_BINDINGS, rateModeBindings); + } + + /** + * 获取指定定额专业的费率模式ID + * @param quotaCatalogItemId 定额专业ID + * @return 费率模式ID,如果未绑定返回null + */ + public Long getRateModeIdByQuotaSpecialty(Long quotaCatalogItemId) { + Map> bindings = getRateModeBindings(); + Map binding = bindings.get(String.valueOf(quotaCatalogItemId)); + if (binding != null && binding.get("rateModeId") != null) { + Object rateModeId = binding.get("rateModeId"); + if (rateModeId instanceof Number) { + return ((Number) rateModeId).longValue(); + } + if (rateModeId instanceof String) { + return Long.parseLong((String) rateModeId); + } + } + return null; + } + + /** + * 绑定定额专业的费率模式 + * @param quotaCatalogItemId 定额专业ID + * @param rateModeId 费率模式ID + * @param rateModeName 费率模式名称(快照用) + * @param quotaSpecialtyName 定额专业名称(快照用) + */ + public void bindRateMode(Long quotaCatalogItemId, Long rateModeId, String rateModeName, String quotaSpecialtyName) { + Map> bindings = getRateModeBindings(); + Map binding = bindings.getOrDefault(String.valueOf(quotaCatalogItemId), new java.util.HashMap<>()); + // 使用字符串存储大整数ID,避免JavaScript精度丢失 + binding.put("rateModeId", String.valueOf(rateModeId)); + binding.put("rateModeName", rateModeName); + binding.put("quotaSpecialtyName", quotaSpecialtyName); + binding.put("bindTime", java.time.LocalDateTime.now().toString()); + bindings.put(String.valueOf(quotaCatalogItemId), binding); + setRateModeBindings(bindings); + } + + /** + * 绑定定额专业的费率模式并锁定 + * @param quotaCatalogItemId 定额专业ID + * @param rateModeId 费率模式ID + * @param rateModeName 费率模式名称(快照用) + * @param quotaSpecialtyName 定额专业名称(快照用) + * @param lockedByUnitId 锁定时的单位工程ID + */ + public void bindAndLockRateMode(Long quotaCatalogItemId, Long rateModeId, String rateModeName, + String quotaSpecialtyName, Long lockedByUnitId) { + Map> bindings = getRateModeBindings(); + Map binding = bindings.getOrDefault(String.valueOf(quotaCatalogItemId), new java.util.HashMap<>()); + // 使用字符串存储大整数ID,避免JavaScript精度丢失 + binding.put("rateModeId", String.valueOf(rateModeId)); + binding.put("rateModeName", rateModeName); + binding.put("quotaSpecialtyName", quotaSpecialtyName); + binding.put("bindTime", java.time.LocalDateTime.now().toString()); + binding.put("isLocked", true); + binding.put("lockedByUnitId", lockedByUnitId != null ? String.valueOf(lockedByUnitId) : null); + bindings.put(String.valueOf(quotaCatalogItemId), binding); + setRateModeBindings(bindings); + } + + /** + * 检查指定定额专业是否已锁定 + * @param quotaCatalogItemId 定额专业ID + * @return 是否已锁定 + */ + public boolean isRateModeLocked(Long quotaCatalogItemId) { + Map> bindings = getRateModeBindings(); + Map binding = bindings.get(String.valueOf(quotaCatalogItemId)); + if (binding != null) { + Object isLocked = binding.get("isLocked"); + return Boolean.TRUE.equals(isLocked); + } + return false; + } + + /** + * 获取指定定额专业的完整绑定信息 + * @param quotaCatalogItemId 定额专业ID + * @return 绑定信息Map,如果未绑定返回null + */ + public Map getRateModeBinding(Long quotaCatalogItemId) { + Map> bindings = getRateModeBindings(); + return bindings.get(String.valueOf(quotaCatalogItemId)); + } + + /** + * 保存费率覆写值 + * @param quotaCatalogItemId 定额专业ID + * @param rateSettings 费率覆写值 { "rateItemId": { "fieldIndex": "value" } } + */ + @SuppressWarnings("unchecked") + public void saveRateSettings(Long quotaCatalogItemId, Map rateSettings) { + Map> bindings = getRateModeBindings(); + Map binding = bindings.getOrDefault(String.valueOf(quotaCatalogItemId), new java.util.HashMap<>()); + binding.put("rateSettings", rateSettings); + bindings.put(String.valueOf(quotaCatalogItemId), binding); + setRateModeBindings(bindings); + } + + /** + * 保存取费覆写值 + * @param quotaCatalogItemId 定额专业ID + * @param feeSettings 取费覆写值 { "feeItemId": { "name": "xxx", ... } } + */ + @SuppressWarnings("unchecked") + public void saveFeeSettings(Long quotaCatalogItemId, Map feeSettings) { + Map> bindings = getRateModeBindings(); + Map binding = bindings.getOrDefault(String.valueOf(quotaCatalogItemId), new java.util.HashMap<>()); + binding.put("feeSettings", feeSettings); + bindings.put(String.valueOf(quotaCatalogItemId), binding); + setRateModeBindings(bindings); + } + + /** + * 获取费率覆写值 + * @param quotaCatalogItemId 定额专业ID + * @return 费率覆写值Map + */ + @SuppressWarnings("unchecked") + public Map getRateSettings(Long quotaCatalogItemId) { + Map binding = getRateModeBinding(quotaCatalogItemId); + if (binding != null && binding.get("rateSettings") instanceof Map) { + return (Map) binding.get("rateSettings"); + } + return new java.util.HashMap<>(); + } + + /** + * 获取取费覆写值 + * @param quotaCatalogItemId 定额专业ID + * @return 取费覆写值Map + */ + @SuppressWarnings("unchecked") + public Map getFeeSettings(Long quotaCatalogItemId) { + Map binding = getRateModeBinding(quotaCatalogItemId); + if (binding != null && binding.get("feeSettings") instanceof Map) { + return (Map) binding.get("feeSettings"); + } + return new java.util.HashMap<>(); + } + + /** + * 获取用户上次选择的定额专业ID + * @return 定额专业ID,如果未设置返回null + */ + public Long getLastSelectedQuotaSpecialty() { + if (attributes == null) { + return null; + } + Object value = attributes.get(ATTR_LAST_SELECTED_QUOTA_SPECIALTY); + if (value instanceof Number) { + return ((Number) value).longValue(); + } + if (value instanceof String) { + try { + return Long.parseLong((String) value); + } catch (NumberFormatException e) { + return null; + } + } + return null; + } + + /** + * 设置用户上次选择的定额专业ID + * @param quotaCatalogItemId 定额专业ID + */ + public void setLastSelectedQuotaSpecialty(Long quotaCatalogItemId) { + if (attributes == null) { + attributes = new java.util.HashMap<>(); + } + // 使用字符串存储,避免大整数精度丢失 + attributes.put(ATTR_LAST_SELECTED_QUOTA_SPECIALTY, quotaCatalogItemId != null ? String.valueOf(quotaCatalogItemId) : null); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbRateFieldLabelDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbRateFieldLabelDO.java new file mode 100644 index 0000000..0498392 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbRateFieldLabelDO.java @@ -0,0 +1,51 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 工作台费率字段标签快照 DO + * + * @author yhy + */ +@TableName("yhy_wb_rate_field_label") +@KeySequence("yhy_wb_rate_field_label_id_seq") +@Data +@EqualsAndHashCode(callSuper = true) +public class WbRateFieldLabelDO extends BaseDO { + + /** + * 主键ID + */ + @TableId + private Long id; + + /** + * 单位工程ID + */ + private Long compileTreeId; + + /** + * 后台标准库字段标签ID + */ + private Long sourceId; + + /** + * 费率模式ID + */ + private Long catalogItemId; + + /** + * 标签名称 + */ + private String labelName; + + /** + * 排序 + */ + private Integer sortOrder; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbRateItemDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbRateItemDO.java new file mode 100644 index 0000000..f2c2fa3 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbRateItemDO.java @@ -0,0 +1,109 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.util.Map; + +/** + * 工作台-费率模板快照 DO + * + * 创建单位工程时从后台 yhy_quota_rate_item 复制 + * + * @author yihuiyong + */ +@TableName(value = "yhy_wb_rate_item", autoResultMap = true) +@KeySequence("yhy_wb_rate_item_id_seq") +@Data +@EqualsAndHashCode(callSuper = true) +public class WbRateItemDO extends TenantBaseDO { + + /** + * 主键ID + */ + @TableId + private Long id; + + /** + * 关联单位工程ID + */ + private Long compileTreeId; + + /** + * 来源后台ID(yhy_quota_rate_item.id) + */ + private Long sourceId; + + /** + * 关联到"模式"节点(yhy_catalog_item.id,node_type='rate_mode') + */ + private Long catalogItemId; + + /** + * 父节点ID(支持三级树) + */ + private Long parentId; + + /** + * 自定义序号 + */ + private String customCode; + + /** + * 费率项名称 + */ + private String name; + + /** + * 费率代号(%) + */ + private String rateCode; + + /** + * 是否填写 + */ + private Boolean isEditable; + + /** + * 默认值 + */ + private BigDecimal defaultValue; + + /** + * 取值模式:static/dynamic + */ + private String valueMode; + + /** + * 扩展设置(JSONB) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map settings; + + /** + * 节点类型:directory/value + */ + private String nodeType; + + /** + * 层级(1-3) + */ + private Integer level; + + /** + * 排序字段 + */ + private Integer sortOrder; + + // ==================== 常量 ==================== + + public static final String NODE_TYPE_DIRECTORY = "directory"; + public static final String NODE_TYPE_VALUE = "value"; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbResourceSummaryDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbResourceSummaryDO.java new file mode 100644 index 0000000..1e46836 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbResourceSummaryDO.java @@ -0,0 +1,69 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * 工料机汇总 DO + * + * 存储用户标记字段(打印、评标指定材料、价格来源、备注) + * 其他字段通过关联 yhy_wb_boq_resource 实时查询 + * + * @author yhy + */ +@TableName("yhy_wb_resource_summary") +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WbResourceSummaryDO extends BaseDO { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 项目ID + */ + private Long projectId; + + /** + * 资源唯一键(MD5(code+name+spec+unit)) + */ + private String resourceKey; + + /** + * 是否打印(0-否,1-是) + */ + private Integer isPrint; + + /** + * 是否评标指定材料(0-否,1-是) + */ + private Integer isBidMaterial; + + /** + * 价格来源 + */ + private String priceSource; + + /** + * 备注 + */ + private String remark; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbUnifiedFeeConfigDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbUnifiedFeeConfigDO.java new file mode 100644 index 0000000..a6f181c --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbUnifiedFeeConfigDO.java @@ -0,0 +1,72 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import java.util.Map; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 工作台统一取费配置 DO + * + * 记录工作台中统一取费的配置信息 + * 当费率文件切换时,需要清空该配置 + */ +@TableName(value = "yhy_wb_unified_fee_config", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +public class WbUnifiedFeeConfigDO extends BaseDO { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 单位工程节点ID(关联 yhy_wb_compile_tree) + */ + private Long compileTreeId; + + /** + * 分部分项节点ID(范围,关联 yhy_wb_boq_division) + */ + private Long divisionId; + + /** + * 来源统一取费设置ID(关联 yhy_quota_unified_fee_setting) + */ + private Long sourceUnifiedFeeSettingId; + + /** + * 费率文件ID(关联费率模式节点) + * 用于切换费率文件时清空配置 + */ + private Long rateModeId; + + /** + * 配置数据(JSONB) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map configData; + + /** + * 扩展属性(JSONB) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map attributes; + + /** + * 排序字段 + */ + private Integer sortOrder; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbUnifiedFeeResourceDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbUnifiedFeeResourceDO.java new file mode 100644 index 0000000..3e625d0 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbUnifiedFeeResourceDO.java @@ -0,0 +1,86 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.util.Map; + +/** + * 工作台-统一取费子目工料机快照 DO + * + * 创建单位工程时从后台 yhy_quota_unified_fee_resource 复制 + * + * @author yihuiyong + */ +@TableName(value = "yhy_wb_unified_fee_resource", autoResultMap = true) +@KeySequence("yhy_wb_unified_fee_resource_id_seq") +@Data +@EqualsAndHashCode(callSuper = true) +public class WbUnifiedFeeResourceDO extends TenantBaseDO { + + /** + * 主键ID + */ + @TableId + private Long id; + + /** + * 关联单位工程ID + */ + private Long compileTreeId; + + /** + * 来源后台ID(yhy_quota_unified_fee_resource.id) + */ + private Long sourceId; + + /** + * 关联统一取费设置ID(yhy_wb_unified_fee_setting.id) + */ + private Long unifiedFeeSettingId; + + /** + * 关联工料机ID(yhy_resource_item.id) + */ + private Long resourceItemId; + + /** + * 定额消耗量 + */ + private BigDecimal dosage; + + /** + * 调整消耗量 + */ + private BigDecimal adjustedDosage; + + /** + * 扩展属性(JSONB) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map attributes; + + /** + * 排序字段 + */ + private Integer sortOrder; + + // ==================== 便捷方法 ==================== + + /** + * 获取有效消耗量 + */ + public BigDecimal getEffectiveDosage() { + if (adjustedDosage != null && adjustedDosage.compareTo(BigDecimal.ZERO) != 0) { + return adjustedDosage; + } + return dosage; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbUnifiedFeeSettingDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbUnifiedFeeSettingDO.java new file mode 100644 index 0000000..ba17e8d --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbUnifiedFeeSettingDO.java @@ -0,0 +1,120 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.util.Map; + +/** + * 工作台-统一取费设置快照 DO + * + * 创建单位工程时从后台 yhy_quota_unified_fee_setting 复制 + * + * @author yihuiyong + */ +@TableName(value = "yhy_wb_unified_fee_setting", autoResultMap = true) +@KeySequence("yhy_wb_unified_fee_setting_id_seq") +@Data +@EqualsAndHashCode(callSuper = true) +public class WbUnifiedFeeSettingDO extends TenantBaseDO { + + /** + * 主键ID + */ + @TableId + private Long id; + + /** + * 关联单位工程ID + */ + private Long compileTreeId; + + /** + * 来源后台ID(yhy_quota_unified_fee_setting.id) + */ + private Long sourceId; + + /** + * 关联到"模式"节点 + */ + private Long catalogItemId; + + /** + * 父节点ID(树结构) + */ + private Long parentId; + + /** + * 自定义序号 + */ + private String customCode; + + /** + * 编号 + */ + private String code; + + /** + * 名称 + */ + private String name; + + /** + * 取费章节 + */ + private String feeChapter; + + /** + * 默认费用归属 + */ + private String feeCategory; + + /** + * 本清单比例% + */ + private BigDecimal thisListPercentage; + + /** + * 指定清单比例% + */ + private BigDecimal specifiedListPercentage; + + /** + * 指定清单编码 + */ + private String specifiedListCode; + + /** + * 节点类型:directory/parent/child + */ + private String nodeType; + + /** + * 单位 + */ + private String unit; + + /** + * 扩展属性(JSONB) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map attributes; + + /** + * 排序字段 + */ + private Integer sortOrder; + + // ==================== 常量 ==================== + + public static final String NODE_TYPE_DIRECTORY = "directory"; + public static final String NODE_TYPE_PARENT = "parent"; + public static final String NODE_TYPE_CHILD = "child"; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbUnitFeeSettingDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbUnitFeeSettingDO.java new file mode 100644 index 0000000..73312e0 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbUnitFeeSettingDO.java @@ -0,0 +1,142 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import java.util.Map; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 单位工程取费项设定 DO + * + * 存储单位工程级别的取费项覆写值,支持快照功能 + * - 当前值用于业务计算 + * - 基线值用于版本对比和快照 + */ +@TableName(value = "yhy_wb_unit_fee_setting", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +public class WbUnitFeeSettingDO extends BaseDO { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 分部分项节点ID(关联 yhy_wb_boq_division.id,定额节点) + */ + private Long divisionId; + + /** + * 单位工程ID(关联 yhy_wb_unit_info.id,可选,用于快照) + */ + private Long unitId; + + /** + * 来源取费项ID(关联 yhy_quota_fee_item.id) + */ + private Long sourceFeeItemId; + + /** + * 来源费率项ID(关联 yhy_quota_rate_item.id) + */ + private Long sourceRateItemId; + + /** + * 排序值 + */ + private Integer sortOrder; + + /** + * 名称(覆写值) + */ + private String name; + + /** + * 名称基线值(快照) + */ + private String baseName; + + /** + * 费率代号(覆写值) + */ + private String ratePercentage; + + /** + * 费率代号基线值(快照) + */ + private String baseRatePercentage; + + /** + * 代号(覆写值) + */ + private String code; + + /** + * 代号基线值(快照) + */ + private String baseCode; + + /** + * 费用归属(覆写值) + */ + private String feeCategory; + + /** + * 费用归属基线值(快照) + */ + private String baseFeeCategory; + + /** + * 基数说明(覆写值) + */ + private String baseDescription; + + /** + * 基数说明基线值(快照) + */ + private String baseBaseDescription; + + /** + * 计算基数(覆写值,JSONB) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map calcBase; + + /** + * 计算基数基线值(快照,JSONB) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map baseCalcBase; + + /** + * 是否隐藏 + */ + private Boolean hidden; + + /** + * 是否可变 + */ + private Boolean variable; + + /** + * 是否为系统行(如ZHDJ不可删除) + */ + private Boolean systemRow; + + /** + * 是否已删除(软删除标记,用于标记用户删除的行) + */ + private Boolean userDeleted; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbUnitInfoDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbUnitInfoDO.java new file mode 100644 index 0000000..b302fd2 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbUnitInfoDO.java @@ -0,0 +1,265 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * 单位工程信息 DO + * + * @author yhy + */ +@TableName(value = "yhy_wb_unit_info", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WbUnitInfoDO extends BaseDO { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 关联编制树节点ID(单位工程节点) + */ + private Long compileTreeId; + + /** + * 工程编号(项目范围内唯一) + */ + private String unitCode; + + /** + * 工程名称 + */ + private String unitName; + + /** + * 专业类别(字典:wb_specialty_type) + */ + private String specialtyType; + + /** + * 库类别(字典:wb_library_type) + */ + private String libraryType; + + /** + * 清单数据库(清单目录树ID) + */ + private Long boqCatalogItemId; + + /** + * 定额数据库(定额专业树ID) + */ + private Long quotaCatalogItemId; + + /** + * 执行费率文件(费率模式节点ID) + */ + private Long rateModeId; + + /** + * 建设规模(字典:wb_construction_scale) + */ + private String constructionScale; + + /** + * 配置快照(预留) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map configSnapshot; + + /** + * 扩展属性(JSONB) + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map attributes; + + // attributes 中的 key 常量 + public static final String ATTR_RATE_MODE_BINDINGS = "rateModeBindings"; + public static final String ATTR_LAST_SELECTED_QUOTA_SPECIALTY = "lastSelectedQuotaSpecialty"; + + /** + * 获取费率模式绑定Map + * 格式: { "定额专业ID": { "rateModeId": 123, "rateModeName": "xxx", "quotaSpecialtyName": "xxx" } } + */ + @SuppressWarnings("unchecked") + public Map> getRateModeBindings() { + if (attributes == null) { + return new java.util.HashMap<>(); + } + Object bindings = attributes.get(ATTR_RATE_MODE_BINDINGS); + if (bindings instanceof Map) { + return (Map>) bindings; + } + return new java.util.HashMap<>(); + } + + /** + * 设置费率模式绑定Map + */ + public void setRateModeBindings(Map> rateModeBindings) { + if (attributes == null) { + attributes = new java.util.HashMap<>(); + } + attributes.put(ATTR_RATE_MODE_BINDINGS, rateModeBindings); + } + + /** + * 获取指定定额专业的费率模式ID + * @param quotaCatalogItemId 定额专业ID + * @return 费率模式ID,如果未绑定返回null + */ + public Long getRateModeIdByQuotaSpecialty(Long quotaCatalogItemId) { + Map> bindings = getRateModeBindings(); + Map binding = bindings.get(String.valueOf(quotaCatalogItemId)); + if (binding != null && binding.get("rateModeId") != null) { + Object rateModeIdObj = binding.get("rateModeId"); + if (rateModeIdObj instanceof Number) { + return ((Number) rateModeIdObj).longValue(); + } + if (rateModeIdObj instanceof String) { + return Long.parseLong((String) rateModeIdObj); + } + } + return null; + } + + /** + * 绑定定额专业的费率模式 + * @param quotaCatalogItemId 定额专业ID + * @param rateModeId 费率模式ID + * @param rateModeName 费率模式名称(快照用) + * @param quotaSpecialtyName 定额专业名称(快照用) + */ + public void bindRateMode(Long quotaCatalogItemId, Long rateModeId, String rateModeName, String quotaSpecialtyName) { + Map> bindings = getRateModeBindings(); + Map binding = bindings.getOrDefault(String.valueOf(quotaCatalogItemId), new java.util.HashMap<>()); + // 使用字符串存储大整数ID,避免JavaScript精度丢失 + binding.put("rateModeId", String.valueOf(rateModeId)); + binding.put("rateModeName", rateModeName); + binding.put("quotaSpecialtyName", quotaSpecialtyName); + binding.put("bindTime", java.time.LocalDateTime.now().toString()); + bindings.put(String.valueOf(quotaCatalogItemId), binding); + setRateModeBindings(bindings); + } + + /** + * 获取指定定额专业的完整绑定信息 + * @param quotaCatalogItemId 定额专业ID + * @return 绑定信息Map,如果未绑定返回null + */ + public Map getRateModeBinding(Long quotaCatalogItemId) { + Map> bindings = getRateModeBindings(); + return bindings.get(String.valueOf(quotaCatalogItemId)); + } + + /** + * 保存费率覆写值 + * @param quotaCatalogItemId 定额专业ID + * @param rateSettings 费率覆写值 { "rateItemId": { "fieldIndex": "value" } } + */ + public void saveRateSettings(Long quotaCatalogItemId, Map rateSettings) { + Map> bindings = getRateModeBindings(); + Map binding = bindings.getOrDefault(String.valueOf(quotaCatalogItemId), new java.util.HashMap<>()); + binding.put("rateSettings", rateSettings); + bindings.put(String.valueOf(quotaCatalogItemId), binding); + setRateModeBindings(bindings); + } + + /** + * 保存取费覆写值 + * @param quotaCatalogItemId 定额专业ID + * @param feeSettings 取费覆写值 { "feeItemId": { "name": "xxx", ... } } + */ + public void saveFeeSettings(Long quotaCatalogItemId, Map feeSettings) { + Map> bindings = getRateModeBindings(); + Map binding = bindings.getOrDefault(String.valueOf(quotaCatalogItemId), new java.util.HashMap<>()); + binding.put("feeSettings", feeSettings); + bindings.put(String.valueOf(quotaCatalogItemId), binding); + setRateModeBindings(bindings); + } + + /** + * 获取费率覆写值 + * @param quotaCatalogItemId 定额专业ID + * @return 费率覆写值Map + */ + @SuppressWarnings("unchecked") + public Map getRateSettings(Long quotaCatalogItemId) { + Map binding = getRateModeBinding(quotaCatalogItemId); + if (binding != null && binding.get("rateSettings") instanceof Map) { + return (Map) binding.get("rateSettings"); + } + return new java.util.HashMap<>(); + } + + /** + * 获取取费覆写值 + * @param quotaCatalogItemId 定额专业ID + * @return 取费覆写值Map + */ + @SuppressWarnings("unchecked") + public Map getFeeSettings(Long quotaCatalogItemId) { + Map binding = getRateModeBinding(quotaCatalogItemId); + if (binding != null && binding.get("feeSettings") instanceof Map) { + return (Map) binding.get("feeSettings"); + } + return new java.util.HashMap<>(); + } + + /** + * 获取用户上次选择的定额专业ID + * @return 定额专业ID,如果未设置返回null + */ + public Long getLastSelectedQuotaSpecialty() { + if (attributes == null) { + return null; + } + Object value = attributes.get(ATTR_LAST_SELECTED_QUOTA_SPECIALTY); + if (value instanceof Number) { + return ((Number) value).longValue(); + } + if (value instanceof String) { + try { + return Long.parseLong((String) value); + } catch (NumberFormatException e) { + return null; + } + } + return null; + } + + /** + * 设置用户上次选择的定额专业ID + * @param quotaCatalogItemId 定额专业ID + */ + public void setLastSelectedQuotaSpecialty(Long quotaCatalogItemId) { + if (attributes == null) { + attributes = new java.util.HashMap<>(); + } + // 使用字符串存储,避免大整数精度丢失 + attributes.put(ATTR_LAST_SELECTED_QUOTA_SPECIALTY, quotaCatalogItemId != null ? String.valueOf(quotaCatalogItemId) : null); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbUnitRateSettingDO.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbUnitRateSettingDO.java new file mode 100644 index 0000000..df7743a --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/dataobject/workbench/WbUnitRateSettingDO.java @@ -0,0 +1,82 @@ +package com.yhy.module.core.dal.dataobject.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.yhy.module.core.dal.typehandler.PostgreSQLJsonbTypeHandler; +import java.math.BigDecimal; +import java.util.Map; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 工程费率设定 DO + * + * 存储单位工程级别的费率设定,支持快照功能 + * - 当前值用于业务计算 + * - 基线值用于版本对比和快照 + */ +@TableName(value = "yhy_wb_unit_rate_setting", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +public class WbUnitRateSettingDO extends BaseDO { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + private Long tenantId; + + /** + * 单位工程ID(关联 yhy_proj_unit.id) + */ + private Long unitId; + + /** + * 费率项ID(目录节点,关联 yhy_quota_rate_item.id) + */ + private Long rateItemId; + + /** + * 选中的值节点ID(下拉模式,关联 yhy_quota_rate_item.id) + */ + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private Long selectedValueId; + + /** + * 输入值(填写模式) + */ + private BigDecimal inputValue; + + /** + * 基线值(快照用,创建工程时冻结) + */ + private BigDecimal baseValue; + + /** + * 计算后的字段值(JSONB) + * 格式:{"1": 1.00, "2": 2.00, ...} + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map fieldValues; + + /** + * 基线字段值(快照用,JSONB) + * 格式:{"1": 1.00, "2": 2.00, ...} + */ + @TableField(typeHandler = PostgreSQLJsonbTypeHandler.class) + private Map baseFieldValues; + + /** + * 设置名称(用于显示,如选中的值节点名称) + */ + private String settingName; +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/boq/BoqCatalogItemMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/boq/BoqCatalogItemMapper.java index 82abe5f..729a349 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/boq/BoqCatalogItemMapper.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/boq/BoqCatalogItemMapper.java @@ -2,10 +2,8 @@ package com.yhy.module.core.dal.mysql.boq; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import com.yhy.module.core.dal.dataobject.boq.BoqCatalogItemDO; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; - import java.util.List; +import org.apache.ibatis.annotations.Mapper; /** * 清单配置目录树 Mapper @@ -56,4 +54,11 @@ public interface BoqCatalogItemMapper extends BaseMapperX { .isNull(parentId == null, BoqCatalogItemDO::getParentId) .orderByAsc(BoqCatalogItemDO::getSortOrder)); } + + /** + * 根据节点类型查询 + */ + default BoqCatalogItemDO selectByNodeType(String nodeType) { + return selectOne(BoqCatalogItemDO::getNodeType, nodeType); + } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/boq/BoqDetailTreeMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/boq/BoqDetailTreeMapper.java deleted file mode 100644 index 3bda1a9..0000000 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/boq/BoqDetailTreeMapper.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.yhy.module.core.dal.mysql.boq; - -import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; -import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; -import com.yhy.module.core.dal.dataobject.boq.BoqDetailTreeDO; -import org.apache.ibatis.annotations.Mapper; - -import java.util.List; - -/** - * 清单明细树 Mapper - * - * @author yhy - */ -@Mapper -public interface BoqDetailTreeMapper extends BaseMapperX { - - /** - * 根据清单子项ID查询树列表 - */ - default List selectListByBoqSubItemId(Long boqSubItemId) { - return selectList(new LambdaQueryWrapperX() - .eq(BoqDetailTreeDO::getBoqSubItemId, boqSubItemId) - .orderByAsc(BoqDetailTreeDO::getSortOrder)); - } - - /** - * 根据父节点ID查询子节点列表 - */ - default List selectListByParentId(Long parentId) { - return selectList(new LambdaQueryWrapperX() - .eq(BoqDetailTreeDO::getParentId, parentId) - .orderByAsc(BoqDetailTreeDO::getSortOrder)); - } - - /** - * 检查编码是否存在(同一清单子项下) - */ - default BoqDetailTreeDO selectByBoqSubItemIdAndCode(Long boqSubItemId, String code) { - return selectOne(new LambdaQueryWrapperX() - .eq(BoqDetailTreeDO::getBoqSubItemId, boqSubItemId) - .eq(BoqDetailTreeDO::getCode, code)); - } - - /** - * 检查是否有子节点 - */ - default boolean hasChildren(Long id) { - return selectCount(new LambdaQueryWrapperX() - .eq(BoqDetailTreeDO::getParentId, id)) > 0; - } - - /** - * 根据清单子项ID删除所有节点 - */ - default int deleteByBoqSubItemId(Long boqSubItemId) { - return delete(new LambdaQueryWrapperX() - .eq(BoqDetailTreeDO::getBoqSubItemId, boqSubItemId)); - } - - /** - * 根据ID查询节点(加锁) - */ - default BoqDetailTreeDO selectByIdForUpdate(Long id) { - return selectOne(new LambdaQueryWrapperX() - .eq(BoqDetailTreeDO::getId, id) - .last("FOR UPDATE")); - } -} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/boq/BoqGuideTreeMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/boq/BoqGuideTreeMapper.java new file mode 100644 index 0000000..fde9253 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/boq/BoqGuideTreeMapper.java @@ -0,0 +1,68 @@ +package com.yhy.module.core.dal.mysql.boq; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.boq.BoqGuideTreeDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 清单指引树 Mapper + * + * @author yhy + */ +@Mapper +public interface BoqGuideTreeMapper extends BaseMapperX { + + /** + * 根据清单子项ID查询树列表 + */ + default List selectListByBoqSubItemId(Long boqSubItemId) { + return selectList(new LambdaQueryWrapperX() + .eq(BoqGuideTreeDO::getBoqSubItemId, boqSubItemId) + .orderByAsc(BoqGuideTreeDO::getSortOrder)); + } + + /** + * 根据父节点ID查询子节点列表 + */ + default List selectListByParentId(Long parentId) { + return selectList(new LambdaQueryWrapperX() + .eq(BoqGuideTreeDO::getParentId, parentId) + .orderByAsc(BoqGuideTreeDO::getSortOrder)); + } + + /** + * 检查编码是否存在(同一清单子项下) + */ + default BoqGuideTreeDO selectByBoqSubItemIdAndCode(Long boqSubItemId, String code) { + return selectOne(new LambdaQueryWrapperX() + .eq(BoqGuideTreeDO::getBoqSubItemId, boqSubItemId) + .eq(BoqGuideTreeDO::getCode, code)); + } + + /** + * 检查是否有子节点 + */ + default boolean hasChildren(Long id) { + return selectCount(new LambdaQueryWrapperX() + .eq(BoqGuideTreeDO::getParentId, id)) > 0; + } + + /** + * 根据清单子项ID删除所有节点 + */ + default int deleteByBoqSubItemId(Long boqSubItemId) { + return delete(new LambdaQueryWrapperX() + .eq(BoqGuideTreeDO::getBoqSubItemId, boqSubItemId)); + } + + /** + * 根据ID查询节点(加锁) + */ + default BoqGuideTreeDO selectByIdForUpdate(Long id) { + return selectOne(new LambdaQueryWrapperX() + .eq(BoqGuideTreeDO::getId, id) + .last("FOR UPDATE")); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/boq/BoqItemTreeMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/boq/BoqItemTreeMapper.java index 46e3ff3..17417b7 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/boq/BoqItemTreeMapper.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/boq/BoqItemTreeMapper.java @@ -1,12 +1,10 @@ package com.yhy.module.core.dal.mysql.boq; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.yhy.module.core.dal.dataobject.boq.BoqItemTreeDO; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; -import org.apache.ibatis.annotations.Select; - import java.util.List; +import org.apache.ibatis.annotations.Mapper; /** * 清单项树 Mapper @@ -40,10 +38,11 @@ public interface BoqItemTreeMapper extends BaseMapperX { /** * 查询同级节点 */ - @Select("SELECT * FROM yhy_boq_item_tree WHERE " + - "(parent_id = #{parentId} OR (parent_id IS NULL AND #{parentId} IS NULL)) " + - "AND boq_catalog_item_id = #{boqCatalogItemId} " + - "ORDER BY sort_order") - List selectSiblings(@Param("parentId") Long parentId, - @Param("boqCatalogItemId") Long boqCatalogItemId); + default List selectSiblings(Long parentId, Long boqCatalogItemId) { + return selectList(new LambdaQueryWrapper() + .eq(parentId != null, BoqItemTreeDO::getParentId, parentId) + .isNull(parentId == null, BoqItemTreeDO::getParentId) + .eq(BoqItemTreeDO::getBoqCatalogItemId, boqCatalogItemId) + .orderByAsc(BoqItemTreeDO::getSortOrder)); + } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/boq/BoqSubItemMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/boq/BoqSubItemMapper.java index cbb242a..6e5b0bc 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/boq/BoqSubItemMapper.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/boq/BoqSubItemMapper.java @@ -27,4 +27,11 @@ public interface BoqSubItemMapper extends BaseMapperX { default List selectListByBoqItemTreeId(Long boqItemTreeId) { return selectList("boq_item_tree_id", boqItemTreeId); } + + /** + * 根据清单项树ID列表批量查询子项 + */ + default List selectListByBoqItemTreeIds(java.util.Collection boqItemTreeIds) { + return selectList(BoqSubItemDO::getBoqItemTreeId, boqItemTreeIds); + } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/calcbaserate/CalcBaseRateCatalogMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/calcbaserate/CalcBaseRateCatalogMapper.java new file mode 100644 index 0000000..ca9dc2e --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/calcbaserate/CalcBaseRateCatalogMapper.java @@ -0,0 +1,57 @@ +package com.yhy.module.core.dal.mysql.calcbaserate; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.yhy.module.core.dal.dataobject.calcbaserate.CalcBaseRateCatalogDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 基数费率目录树 Mapper + * + * @author yhy + */ +@Mapper +public interface CalcBaseRateCatalogMapper extends BaseMapperX { + + /** + * 查询所有节点(树形结构) + */ + default List selectList() { + return selectList(new LambdaQueryWrapper() + .orderByAsc(CalcBaseRateCatalogDO::getSortOrder)); + } + + /** + * 根据父节点ID查询子节点 + */ + default List selectListByParentId(Long parentId) { + return selectList(new LambdaQueryWrapper() + .eq(CalcBaseRateCatalogDO::getParentId, parentId) + .orderByAsc(CalcBaseRateCatalogDO::getSortOrder)); + } + + /** + * 根据编码查询 + */ + default CalcBaseRateCatalogDO selectByCode(String code) { + return selectOne(CalcBaseRateCatalogDO::getCode, code); + } + + /** + * 查询同级节点(用于排序) + */ + default List selectSiblings(Long parentId) { + return selectList(new LambdaQueryWrapper() + .eq(parentId != null, CalcBaseRateCatalogDO::getParentId, parentId) + .isNull(parentId == null, CalcBaseRateCatalogDO::getParentId) + .orderByAsc(CalcBaseRateCatalogDO::getSortOrder)); + } + + /** + * 根据节点类型查询 + */ + default CalcBaseRateCatalogDO selectByNodeType(String nodeType) { + return selectOne(CalcBaseRateCatalogDO::getNodeType, nodeType); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/calcbaserate/CalcBaseRateDirectoryMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/calcbaserate/CalcBaseRateDirectoryMapper.java new file mode 100644 index 0000000..6439c00 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/calcbaserate/CalcBaseRateDirectoryMapper.java @@ -0,0 +1,45 @@ +package com.yhy.module.core.dal.mysql.calcbaserate; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.yhy.module.core.dal.dataobject.calcbaserate.CalcBaseRateDirectoryDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 基数费率目录 Mapper + * + * @author yhy + */ +@Mapper +public interface CalcBaseRateDirectoryMapper extends BaseMapperX { + + /** + * 根据基数费率目录树节点ID查询所有目录 + */ + default List selectListByCalcBaseRateCatalogId(Long calcBaseRateCatalogId) { + return selectList(new LambdaQueryWrapper() + .eq(CalcBaseRateDirectoryDO::getCalcBaseRateCatalogId, calcBaseRateCatalogId) + .orderByAsc(CalcBaseRateDirectoryDO::getSortOrder)); + } + + /** + * 根据父节点ID查询子节点 + */ + default List selectListByParentId(Long parentId) { + return selectList(new LambdaQueryWrapper() + .eq(CalcBaseRateDirectoryDO::getParentId, parentId) + .orderByAsc(CalcBaseRateDirectoryDO::getSortOrder)); + } + + /** + * 查询同级节点(用于排序) + */ + default List selectSiblings(Long parentId, Long calcBaseRateCatalogId) { + return selectList(new LambdaQueryWrapper() + .eq(CalcBaseRateDirectoryDO::getCalcBaseRateCatalogId, calcBaseRateCatalogId) + .eq(parentId != null, CalcBaseRateDirectoryDO::getParentId, parentId) + .isNull(parentId == null, CalcBaseRateDirectoryDO::getParentId) + .orderByAsc(CalcBaseRateDirectoryDO::getSortOrder)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/calcbaserate/CalcBaseRateItemMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/calcbaserate/CalcBaseRateItemMapper.java new file mode 100644 index 0000000..8eec9c4 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/calcbaserate/CalcBaseRateItemMapper.java @@ -0,0 +1,36 @@ +package com.yhy.module.core.dal.mysql.calcbaserate; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.yhy.module.core.dal.dataobject.calcbaserate.CalcBaseRateItemDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 基数费率项 Mapper + * + * @author yhy + */ +@Mapper +public interface CalcBaseRateItemMapper extends BaseMapperX { + + /** + * 根据目录ID查询所有费率项 + */ + default List selectListByCalcBaseRateDirectoryId(Long calcBaseRateDirectoryId) { + return selectList(new LambdaQueryWrapper() + .eq(CalcBaseRateItemDO::getCalcBaseRateDirectoryId, calcBaseRateDirectoryId) + .orderByAsc(CalcBaseRateItemDO::getSortOrder)); + } + + /** + * 批量更新排序值:将 sortOrder >= threshold 的记录全部 +1 + */ + default void shiftSortOrder(Long calcBaseRateDirectoryId, Integer threshold) { + update(CalcBaseRateItemDO.builder().build(), + new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() + .eq(CalcBaseRateItemDO::getCalcBaseRateDirectoryId, calcBaseRateDirectoryId) + .ge(CalcBaseRateItemDO::getSortOrder, threshold) + .setSql("sort_order = sort_order + 1")); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/config/ConfigProjectInfoMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/config/ConfigProjectInfoMapper.java new file mode 100644 index 0000000..caf692c --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/config/ConfigProjectInfoMapper.java @@ -0,0 +1,56 @@ +package com.yhy.module.core.dal.mysql.config; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.config.ConfigProjectInfoDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 工程信息配置 Mapper + * + * @author yhy + */ +@Mapper +public interface ConfigProjectInfoMapper extends BaseMapperX { + + /** + * 根据配置树节点ID查询列表 + */ + default List selectListByConfigTreeId(Long configTreeId) { + return selectList(new LambdaQueryWrapperX() + .eq(ConfigProjectInfoDO::getConfigTreeId, configTreeId) + .orderByAsc(ConfigProjectInfoDO::getSortOrder)); + } + + /** + * 根据父节点ID查询子节点列表 + */ + default List selectListByParentId(Long parentId) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(ConfigProjectInfoDO::getParentId, parentId) + .orderByAsc(ConfigProjectInfoDO::getSortOrder)); + } + + /** + * 根据配置树节点ID和父节点ID查询 + */ + default List selectListByConfigTreeIdAndParentId(Long configTreeId, Long parentId) { + return selectList(new LambdaQueryWrapperX() + .eq(ConfigProjectInfoDO::getConfigTreeId, configTreeId) + .eqIfPresent(ConfigProjectInfoDO::getParentId, parentId) + .orderByAsc(ConfigProjectInfoDO::getSortOrder)); + } + + /** + * 获取同级最大排序号 + */ + default Integer selectMaxSortOrderByParentId(Long configTreeId, Long parentId) { + ConfigProjectInfoDO maxSortNode = selectOne(new LambdaQueryWrapperX() + .eq(ConfigProjectInfoDO::getConfigTreeId, configTreeId) + .eqIfPresent(ConfigProjectInfoDO::getParentId, parentId) + .orderByDesc(ConfigProjectInfoDO::getSortOrder) + .last("LIMIT 1")); + return maxSortNode != null ? maxSortNode.getSortOrder() : 0; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/config/ConfigProjectTreeMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/config/ConfigProjectTreeMapper.java new file mode 100644 index 0000000..96c6949 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/config/ConfigProjectTreeMapper.java @@ -0,0 +1,61 @@ +package com.yhy.module.core.dal.mysql.config; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.config.ConfigProjectTreeDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 项目界面配置树 Mapper + * + * @author yhy + */ +@Mapper +public interface ConfigProjectTreeMapper extends BaseMapperX { + + /** + * 根据父节点ID查询子节点列表 + */ + default List selectListByParentId(Long parentId) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(ConfigProjectTreeDO::getParentId, parentId) + .orderByAsc(ConfigProjectTreeDO::getSortOrder)); + } + + /** + * 查询所有节点(按排序) + */ + default List selectAllOrderBySortOrder() { + return selectList(new LambdaQueryWrapperX() + .orderByAsc(ConfigProjectTreeDO::getSortOrder)); + } + + /** + * 根据编码查询 + */ + default ConfigProjectTreeDO selectByCode(String code) { + return selectOne(new LambdaQueryWrapperX() + .eq(ConfigProjectTreeDO::getCode, code)); + } + + /** + * 根据节点类型查询 + */ + default List selectListByNodeType(String nodeType) { + return selectList(new LambdaQueryWrapperX() + .eq(ConfigProjectTreeDO::getNodeType, nodeType) + .orderByAsc(ConfigProjectTreeDO::getSortOrder)); + } + + /** + * 获取同级最大排序号 + */ + default Integer selectMaxSortOrderByParentId(Long parentId) { + ConfigProjectTreeDO maxSortNode = selectOne(new LambdaQueryWrapperX() + .eqIfPresent(ConfigProjectTreeDO::getParentId, parentId) + .orderByDesc(ConfigProjectTreeDO::getSortOrder) + .last("LIMIT 1")); + return maxSortNode != null ? maxSortNode.getSortOrder() : 0; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/config/ConfigUnitDivisionTemplateMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/config/ConfigUnitDivisionTemplateMapper.java new file mode 100644 index 0000000..baf6279 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/config/ConfigUnitDivisionTemplateMapper.java @@ -0,0 +1,51 @@ +package com.yhy.module.core.dal.mysql.config; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import com.yhy.module.core.dal.dataobject.config.ConfigUnitDivisionTemplateDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +@Mapper +public interface ConfigUnitDivisionTemplateMapper extends BaseMapperX { + + default List selectListByCatalogItemId(Long catalogItemId) { + return selectList(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(ConfigUnitDivisionTemplateDO::getCatalogItemId, catalogItemId) + .orderByAsc(ConfigUnitDivisionTemplateDO::getSortOrder)); + } + + /** 按 catalogItemId + tabType 查询模板列表 */ + default List selectListByCatalogItemIdAndTabType(Long catalogItemId, String tabType) { + return selectList(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(ConfigUnitDivisionTemplateDO::getCatalogItemId, catalogItemId) + .eq(ConfigUnitDivisionTemplateDO::getTabType, tabType) + .orderByAsc(ConfigUnitDivisionTemplateDO::getSortOrder)); + } + + default List selectListByParentId(Long parentId) { + return selectList(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(ConfigUnitDivisionTemplateDO::getParentId, parentId) + .orderByAsc(ConfigUnitDivisionTemplateDO::getSortOrder)); + } + + @Select("") + Integer selectMaxSortOrderByParentId(@Param("catalogItemId") Long catalogItemId, + @Param("parentId") Long parentId, + @Param("tabType") String tabType); + + default List selectListByParentIdAndSortOrderGe(Long parentId, Integer sortOrder) { + return selectList(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(ConfigUnitDivisionTemplateDO::getParentId, parentId) + .ge(ConfigUnitDivisionTemplateDO::getSortOrder, sortOrder) + .orderByAsc(ConfigUnitDivisionTemplateDO::getSortOrder)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/config/ConfigUnitFieldMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/config/ConfigUnitFieldMapper.java new file mode 100644 index 0000000..6d17444 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/config/ConfigUnitFieldMapper.java @@ -0,0 +1,26 @@ +package com.yhy.module.core.dal.mysql.config; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import com.yhy.module.core.dal.dataobject.config.ConfigUnitFieldDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +@Mapper +public interface ConfigUnitFieldMapper extends BaseMapperX { + + default List selectListByCatalogItemId(Long catalogItemId) { + return selectList(ConfigUnitFieldDO::getCatalogItemId, catalogItemId); + } + + @Select("SELECT COALESCE(MAX(sort_order), 0) FROM yhy_config_unit_field WHERE catalog_item_id = #{catalogItemId} AND deleted = 0") + Integer selectMaxSortOrder(@Param("catalogItemId") Long catalogItemId); + + default List selectListByCatalogItemIdAndSortOrderGe(Long catalogItemId, Integer sortOrder) { + return selectList(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(ConfigUnitFieldDO::getCatalogItemId, catalogItemId) + .ge(ConfigUnitFieldDO::getSortOrder, sortOrder) + .orderByAsc(ConfigUnitFieldDO::getSortOrder)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/config/ConfigUnitResourceFieldMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/config/ConfigUnitResourceFieldMapper.java new file mode 100644 index 0000000..4529bda --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/config/ConfigUnitResourceFieldMapper.java @@ -0,0 +1,26 @@ +package com.yhy.module.core.dal.mysql.config; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import com.yhy.module.core.dal.dataobject.config.ConfigUnitResourceFieldDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +@Mapper +public interface ConfigUnitResourceFieldMapper extends BaseMapperX { + + default List selectListByCatalogItemId(Long catalogItemId) { + return selectList(ConfigUnitResourceFieldDO::getCatalogItemId, catalogItemId); + } + + @Select("SELECT COALESCE(MAX(sort_order), 0) FROM yhy_config_unit_resource_field WHERE catalog_item_id = #{catalogItemId} AND deleted = 0") + Integer selectMaxSortOrder(@Param("catalogItemId") Long catalogItemId); + + default List selectListByCatalogItemIdAndSortOrderGe(Long catalogItemId, Integer sortOrder) { + return selectList(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(ConfigUnitResourceFieldDO::getCatalogItemId, catalogItemId) + .ge(ConfigUnitResourceFieldDO::getSortOrder, sortOrder) + .orderByAsc(ConfigUnitResourceFieldDO::getSortOrder)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/config/ConfigUnitTabRefMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/config/ConfigUnitTabRefMapper.java new file mode 100644 index 0000000..063bac9 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/config/ConfigUnitTabRefMapper.java @@ -0,0 +1,34 @@ +package com.yhy.module.core.dal.mysql.config; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import com.yhy.module.core.dal.dataobject.config.ConfigUnitTabRefDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +@Mapper +public interface ConfigUnitTabRefMapper extends BaseMapperX { + + default List selectListByCatalogAndTab(Long catalogItemId, String tabType) { + return selectList(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(ConfigUnitTabRefDO::getCatalogItemId, catalogItemId) + .eq(ConfigUnitTabRefDO::getTabType, tabType) + .orderByAsc(ConfigUnitTabRefDO::getSortOrder)); + } + + default List selectListByCatalogItemId(Long catalogItemId) { + return selectList(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(ConfigUnitTabRefDO::getCatalogItemId, catalogItemId) + .orderByAsc(ConfigUnitTabRefDO::getSortOrder)); + } + + /** 删除指定模板节点的所有引用(级联删除用) */ + default void deleteByTemplateNodeId(Long templateNodeId) { + delete(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(ConfigUnitTabRefDO::getTemplateNodeId, templateNodeId)); + } + + @Select("SELECT COALESCE(MAX(sort_order), 0) FROM yhy_config_unit_tab_ref WHERE catalog_item_id = #{catalogItemId} AND tab_type = #{tabType} AND deleted = 0") + Integer selectMaxSortOrder(@Param("catalogItemId") Long catalogItemId, @Param("tabType") String tabType); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/infoprice/InfoPriceBookMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/infoprice/InfoPriceBookMapper.java index 832745b..d964e97 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/infoprice/InfoPriceBookMapper.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/infoprice/InfoPriceBookMapper.java @@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceBookPageReqVO; import com.yhy.module.core.dal.dataobject.infoprice.InfoPriceBookDO; +import com.yhy.module.core.enums.infoprice.InfoPricePublishStatusEnum; import org.apache.ibatis.annotations.Mapper; /** @@ -22,13 +23,19 @@ public interface InfoPriceBookMapper extends BaseMapperX { * @return 分页结果 */ default PageResult selectPage(InfoPriceBookPageReqVO reqVO) { - return selectPage(reqVO, new LambdaQueryWrapperX() + LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX() .eqIfPresent(InfoPriceBookDO::getTreeNodeId, reqVO.getTreeNodeId()) .likeIfPresent(InfoPriceBookDO::getName, reqVO.getName()) .eqIfPresent(InfoPriceBookDO::getPublishStatus, reqVO.getPublishStatus()) .betweenIfPresent(InfoPriceBookDO::getStartTime, reqVO.getStartTimeBegin(), reqVO.getStartTimeEnd()) .betweenIfPresent(InfoPriceBookDO::getEndTime, reqVO.getEndTimeBegin(), reqVO.getEndTimeEnd()) - .orderByDesc(InfoPriceBookDO::getStartTime)); + .orderByDesc(InfoPriceBookDO::getStartTime); + // 筛选有附件的记录 + if (Boolean.TRUE.equals(reqVO.getAttachment())) { + wrapper.isNotNull(InfoPriceBookDO::getAttachment) + .ne(InfoPriceBookDO::getAttachment, ""); + } + return selectPage(reqVO, wrapper); } /** @@ -41,4 +48,50 @@ public interface InfoPriceBookMapper extends BaseMapperX { return selectCount(new LambdaQueryWrapperX() .eq(InfoPriceBookDO::getTreeNodeId, treeNodeId)); } + + /** + * 分页查询信息价册(排除指定租户) + * + * @param reqVO 查询条件 + * @param excludeTenantId 要排除的租户ID + * @return 分页结果 + */ + default PageResult selectPageExcludeTenant(InfoPriceBookPageReqVO reqVO, Long excludeTenantId) { + if (reqVO.getTreeNodeId() == null) { + return PageResult.empty(0L); + } + LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX() + .eq(InfoPriceBookDO::getTreeNodeId, reqVO.getTreeNodeId()) + .eq(InfoPriceBookDO::getPublishStatus, InfoPricePublishStatusEnum.COMPLETED.getCode()) + .likeIfPresent(InfoPriceBookDO::getName, reqVO.getName()) + .eqIfPresent(InfoPriceBookDO::getPublishStatus, reqVO.getPublishStatus()) + .betweenIfPresent(InfoPriceBookDO::getStartTime, reqVO.getStartTimeBegin(), reqVO.getStartTimeEnd()) + .betweenIfPresent(InfoPriceBookDO::getEndTime, reqVO.getEndTimeBegin(), reqVO.getEndTimeEnd()) + .orderByDesc(InfoPriceBookDO::getStartTime); + // 筛选有附件的记录 + if (Boolean.TRUE.equals(reqVO.getAttachment())) { + wrapper.isNotNull(InfoPriceBookDO::getAttachment) + .ne(InfoPriceBookDO::getAttachment, ""); + } + if (excludeTenantId != null) { + wrapper.ne(InfoPriceBookDO::getTenantId, excludeTenantId); + + wrapper.isNotNull(InfoPriceBookDO::getStartTime) + .isNotNull(InfoPriceBookDO::getEndTime); + + wrapper.apply( + "NOT EXISTS (" + + " SELECT 1" + + " FROM yhy_info_price_book b" + + " WHERE b.tenant_id = {0}" + + " AND b.tree_node_id = yhy_info_price_book.tree_node_id" + + " AND b.start_time IS NOT NULL" + + " AND b.end_time IS NOT NULL" + + " AND b.deleted = 0" + + " AND NOT (yhy_info_price_book.end_time < b.start_time OR yhy_info_price_book.start_time > b.end_time)" + + ")", + excludeTenantId); + } + return selectPage(reqVO, wrapper); + } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/infoprice/InfoPriceCategoryTreeMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/infoprice/InfoPriceCategoryTreeMapper.java index eb0aa65..9a19e57 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/infoprice/InfoPriceCategoryTreeMapper.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/infoprice/InfoPriceCategoryTreeMapper.java @@ -35,4 +35,20 @@ public interface InfoPriceCategoryTreeMapper extends BaseMapperX selectListByBookIds(List bookIds) { + if (bookIds == null || bookIds.isEmpty()) { + return java.util.Collections.emptyList(); + } + return selectList(new LambdaQueryWrapperX() + .in(InfoPriceCategoryTreeDO::getBookId, bookIds) + .orderByAsc(InfoPriceCategoryTreeDO::getBookId) + .orderByAsc(InfoPriceCategoryTreeDO::getSortOrder)); + } + } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/infoprice/InfoPriceResourceMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/infoprice/InfoPriceResourceMapper.java index 3df9cfb..d24730f 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/infoprice/InfoPriceResourceMapper.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/infoprice/InfoPriceResourceMapper.java @@ -5,9 +5,9 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceResourcePageReqVO; import com.yhy.module.core.dal.dataobject.infoprice.InfoPriceResourceDO; -import org.apache.ibatis.annotations.Mapper; - +import java.util.Collections; import java.util.List; +import org.apache.ibatis.annotations.Mapper; /** * 信息价工料机信息 Mapper @@ -19,11 +19,32 @@ public interface InfoPriceResourceMapper extends BaseMapperX selectPage(InfoPriceResourcePageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() - .eq(InfoPriceResourceDO::getCategoryTreeId, reqVO.getCategoryTreeId()) + .eqIfPresent(InfoPriceResourceDO::getCategoryTreeId, reqVO.getCategoryTreeId()) + .eqIfPresent(InfoPriceResourceDO::getSourceResourceItemId, reqVO.getSourceResourceItemId()) .likeIfPresent(InfoPriceResourceDO::getCode, reqVO.getCode()) - .likeIfPresent(InfoPriceResourceDO::getName, reqVO.getName()) .orderByAsc(InfoPriceResourceDO::getSortOrder)); } + + /** + * 分页查询(支持按categoryTreeIds过滤) + * @param reqVO 查询条件 + * @param categoryTreeIds 允许的categoryTreeId列表(地区过滤),null表示不过滤 + * @return 分页结果 + */ + default PageResult selectPageWithCategoryTreeIds(InfoPriceResourcePageReqVO reqVO, List categoryTreeIds) { + LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX() + .eqIfPresent(InfoPriceResourceDO::getCategoryTreeId, reqVO.getCategoryTreeId()) + .eqIfPresent(InfoPriceResourceDO::getSourceResourceItemId, reqVO.getSourceResourceItemId()) + .likeIfPresent(InfoPriceResourceDO::getCode, reqVO.getCode()); + + // 如果有categoryTreeIds过滤条件,添加IN查询 + if (categoryTreeIds != null && !categoryTreeIds.isEmpty()) { + wrapper.in(InfoPriceResourceDO::getCategoryTreeId, categoryTreeIds); + } + + wrapper.orderByAsc(InfoPriceResourceDO::getSortOrder); + return selectPage(reqVO, wrapper); + } default List selectListByCategoryTreeId(Long categoryTreeId) { return selectList(new LambdaQueryWrapperX() @@ -31,10 +52,36 @@ public interface InfoPriceResourceMapper extends BaseMapperX selectListByIds(List ids) { + if (ids == null || ids.isEmpty()) { + return Collections.emptyList(); + } + return selectList("id", ids); + } + default InfoPriceResourceDO selectByCategoryTreeIdAndCode(Long categoryTreeId, String code) { return selectOne(new LambdaQueryWrapperX() .eq(InfoPriceResourceDO::getCategoryTreeId, categoryTreeId) .eq(InfoPriceResourceDO::getCode, code)); } + /** + * 批量统计多个分类树节点下的工料机数量 + * + * @param categoryTreeIds 分类树节点ID列表 + * @return Map,key为categoryTreeId,value为工料机数量 + */ + default java.util.Map countByCategoryTreeIds(List categoryTreeIds) { + if (categoryTreeIds == null || categoryTreeIds.isEmpty()) { + return java.util.Collections.emptyMap(); + } + List list = selectList(new LambdaQueryWrapperX() + .in(InfoPriceResourceDO::getCategoryTreeId, categoryTreeIds) + .select(InfoPriceResourceDO::getCategoryTreeId)); + return list.stream() + .collect(java.util.stream.Collectors.groupingBy( + InfoPriceResourceDO::getCategoryTreeId, + java.util.stream.Collectors.counting())); + } + } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/infoprice/InfoPriceResourcePriceMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/infoprice/InfoPriceResourcePriceMapper.java index 6604e5b..67fc0cb 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/infoprice/InfoPriceResourcePriceMapper.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/infoprice/InfoPriceResourcePriceMapper.java @@ -5,9 +5,9 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceResourcePricePageReqVO; import com.yhy.module.core.dal.dataobject.infoprice.InfoPriceResourcePriceDO; -import org.apache.ibatis.annotations.Mapper; - +import java.util.Collections; import java.util.List; +import org.apache.ibatis.annotations.Mapper; /** * 信息价工料机价格历史 Mapper @@ -30,10 +30,27 @@ public interface InfoPriceResourcePriceMapper extends BaseMapperX selectListByIds(List ids) { + if (ids == null || ids.isEmpty()) { + return Collections.emptyList(); + } + return selectList("id", ids); + } + default PageResult selectPage(InfoPriceResourcePricePageReqVO reqVO) { - return selectPage(reqVO, new LambdaQueryWrapperX() - .eq(InfoPriceResourcePriceDO::getResourceId, reqVO.getResourceId()) - .orderByDesc(InfoPriceResourcePriceDO::getStartTime)); + LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX() + .eq(InfoPriceResourcePriceDO::getResourceId, reqVO.getResourceId()); + + // 日期范围过滤:查询时间段与指定范围有交集的记录 + if (reqVO.getStartTime() != null) { + wrapper.ge(InfoPriceResourcePriceDO::getEndTime, reqVO.getStartTime()); + } + if (reqVO.getEndTime() != null) { + wrapper.le(InfoPriceResourcePriceDO::getStartTime, reqVO.getEndTime()); + } + + wrapper.orderByDesc(InfoPriceResourcePriceDO::getStartTime); + return selectPage(reqVO, wrapper); } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaAdjustmentDetailMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaAdjustmentDetailMapper.java index b082867..b460bed 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaAdjustmentDetailMapper.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaAdjustmentDetailMapper.java @@ -17,14 +17,16 @@ public interface QuotaAdjustmentDetailMapper extends BaseMapperX selectListBySettingId(Long adjustmentSettingId) { return selectList(new LambdaQueryWrapperX() .eq(QuotaAdjustmentDetailDO::getAdjustmentSettingId, adjustmentSettingId) - .orderByAsc(QuotaAdjustmentDetailDO::getSortOrder)); + .orderByAsc(QuotaAdjustmentDetailDO::getSortOrder) + .orderByAsc(QuotaAdjustmentDetailDO::getId)); // 次要排序:按ID(创建顺序) } default List selectListBySettingIds(List adjustmentSettingIds) { return selectList(new LambdaQueryWrapperX() .in(QuotaAdjustmentDetailDO::getAdjustmentSettingId, adjustmentSettingIds) .orderByAsc(QuotaAdjustmentDetailDO::getAdjustmentSettingId) - .orderByAsc(QuotaAdjustmentDetailDO::getSortOrder)); + .orderByAsc(QuotaAdjustmentDetailDO::getSortOrder) + .orderByAsc(QuotaAdjustmentDetailDO::getId)); // 次要排序:按ID(创建顺序) } default QuotaAdjustmentDetailDO selectByIdForUpdate(Long id) { diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaAdjustmentSettingMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaAdjustmentSettingMapper.java index bcdda00..a8fe876 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaAdjustmentSettingMapper.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaAdjustmentSettingMapper.java @@ -17,7 +17,8 @@ public interface QuotaAdjustmentSettingMapper extends BaseMapperX selectListByQuotaItemId(Long quotaItemId) { return selectList(new LambdaQueryWrapperX() .eq(QuotaAdjustmentSettingDO::getQuotaItemId, quotaItemId) - .orderByAsc(QuotaAdjustmentSettingDO::getSortOrder)); + .orderByAsc(QuotaAdjustmentSettingDO::getSortOrder) + .orderByAsc(QuotaAdjustmentSettingDO::getId)); // 次要排序:按ID(创建顺序) } default QuotaAdjustmentSettingDO selectByIdForUpdate(Long id) { diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaCatalogItemMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaCatalogItemMapper.java index b3896b5..fd73c7b 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaCatalogItemMapper.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaCatalogItemMapper.java @@ -70,4 +70,26 @@ public interface QuotaCatalogItemMapper extends BaseMapperX .eq(QuotaCatalogItemDO::getId, id) .last("FOR UPDATE")); } + + /** + * 根据父节点ID和节点类型查询子节点列表 + */ + default List selectByParentIdAndNodeType(Long parentId, String nodeType) { + return selectList(new LambdaQueryWrapperX() + .eq(QuotaCatalogItemDO::getParentId, parentId) + .eq(QuotaCatalogItemDO::getNodeType, nodeType)); + } + + /** + * 查询指定节点的所有子节点ID(直接子节点,不递归) + * 用于获取定额专业下的所有费率模式ID + */ + default List selectChildIds(Long parentId) { + List children = selectList(new LambdaQueryWrapperX() + .eq(QuotaCatalogItemDO::getParentId, parentId) + .select(QuotaCatalogItemDO::getId)); + return children.stream() + .map(QuotaCatalogItemDO::getId) + .collect(java.util.stream.Collectors.toList()); + } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaCatalogTreeMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaCatalogTreeMapper.java index 58bdc2f..4a3a0fe 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaCatalogTreeMapper.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaCatalogTreeMapper.java @@ -7,7 +7,7 @@ import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; /** - * 定额子目树 Mapper + * 定额基价树 Mapper * * @author yhy */ diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaFeeItemMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaFeeItemMapper.java index 6ec6986..f0b5918 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaFeeItemMapper.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaFeeItemMapper.java @@ -3,9 +3,8 @@ package com.yhy.module.core.dal.mysql.quota; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import com.yhy.module.core.dal.dataobject.quota.QuotaFeeItemDO; -import org.apache.ibatis.annotations.Mapper; - import java.util.List; +import org.apache.ibatis.annotations.Mapper; /** * 定额取费项 Mapper @@ -32,13 +31,57 @@ public interface QuotaFeeItemMapper extends BaseMapperX { } /** - * 查询同级最大排序值 + * 查询同级最大排序值(排除系统行) */ default Integer selectMaxSortOrder(Long catalogItemId) { QuotaFeeItemDO maxItem = selectOne(new LambdaQueryWrapperX() .eq(QuotaFeeItemDO::getCatalogItemId, catalogItemId) + .isNull(QuotaFeeItemDO::getSystemCode) .orderByDesc(QuotaFeeItemDO::getSortOrder) .last("LIMIT 1")); return maxItem != null ? maxItem.getSortOrder() : 0; } + + /** + * 根据模式节点ID和系统行代码查询 + */ + default QuotaFeeItemDO selectBySystemCode(Long catalogItemId, String systemCode) { + return selectOne(new LambdaQueryWrapperX() + .eq(QuotaFeeItemDO::getCatalogItemId, catalogItemId) + .eq(QuotaFeeItemDO::getSystemCode, systemCode)); + } + + /** + * 根据模式节点ID和代号查询 + */ + default QuotaFeeItemDO selectByCode(Long catalogItemId, String code) { + return selectOne(new LambdaQueryWrapperX() + .eq(QuotaFeeItemDO::getCatalogItemId, catalogItemId) + .eq(QuotaFeeItemDO::getCode, code)); + } + + /** + * 根据模式节点ID查询 variable=true 的取费项列表 + */ + default List selectVariableListByCatalogItemId(Long catalogItemId) { + return selectList(new LambdaQueryWrapperX() + .eq(QuotaFeeItemDO::getCatalogItemId, catalogItemId) + .eq(QuotaFeeItemDO::getVariable, true) + .orderByAsc(QuotaFeeItemDO::getSortOrder)); + } + + /** + * 根据多个模式节点ID查询 variable=true 的取费项列表 + * 用于定额专业节点下所有费率模式的变量汇总 + */ + default List selectVariableListByCatalogItemIds(List catalogItemIds) { + if (catalogItemIds == null || catalogItemIds.isEmpty()) { + return java.util.Collections.emptyList(); + } + return selectList(new LambdaQueryWrapperX() + .in(QuotaFeeItemDO::getCatalogItemId, catalogItemIds) + .eq(QuotaFeeItemDO::getVariable, true) + .orderByAsc(QuotaFeeItemDO::getCatalogItemId) + .orderByAsc(QuotaFeeItemDO::getSortOrder)); + } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaItemMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaItemMapper.java index 392f78c..281140d 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaItemMapper.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaItemMapper.java @@ -3,12 +3,11 @@ package com.yhy.module.core.dal.mysql.quota; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import com.yhy.module.core.dal.dataobject.quota.QuotaItemDO; +import java.util.List; import org.apache.ibatis.annotations.Mapper; -import java.util.List; - /** - * 定额子目/基价 Mapper + * 定额基价/基价 Mapper * * @author yhy */ @@ -16,7 +15,7 @@ import java.util.List; public interface QuotaItemMapper extends BaseMapperX { /** - * 根据定额条目ID查询定额子目列表 + * 根据定额条目ID查询定额基价列表 */ default List selectListByCatalogItemId(Long catalogItemId) { return selectList(QuotaItemDO::getCatalogItemId, catalogItemId); @@ -30,7 +29,7 @@ public interface QuotaItemMapper extends BaseMapperX { } /** - * 分页查询定额子目 + * 分页查询定额基价 */ default List selectPage(Long catalogItemId, Integer offset, Integer limit) { return selectList(new LambdaQueryWrapperX() @@ -40,9 +39,24 @@ public interface QuotaItemMapper extends BaseMapperX { } /** - * 统计定额子目数量 + * 统计定额基价数量 */ default Long countByCatalogItemId(Long catalogItemId) { return selectCount(QuotaItemDO::getCatalogItemId, catalogItemId); } + + + /** + * 更新定额基价的四个价格字段 + */ + void updatePrices(@org.apache.ibatis.annotations.Param("quotaItemId") Long quotaItemId, + @org.apache.ibatis.annotations.Param("taxExclBasePrice") java.math.BigDecimal taxExclBasePrice, + @org.apache.ibatis.annotations.Param("taxInclBasePrice") java.math.BigDecimal taxInclBasePrice, + @org.apache.ibatis.annotations.Param("taxExclCompilePrice") java.math.BigDecimal taxExclCompilePrice, + @org.apache.ibatis.annotations.Param("taxInclCompilePrice") java.math.BigDecimal taxInclCompilePrice); + + /** + * 根据编码查询定额基价(编码存储在 attributes->code 中) + */ + List selectByCode(@org.apache.ibatis.annotations.Param("code") String code); } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaMarketMaterialMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaMarketMaterialMapper.java new file mode 100644 index 0000000..c37b00c --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaMarketMaterialMapper.java @@ -0,0 +1,54 @@ +package com.yhy.module.core.dal.mysql.quota; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.quota.QuotaMarketMaterialDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 定额基价市场主材设备 Mapper + * + * @author yhy + */ +@Mapper +public interface QuotaMarketMaterialMapper extends BaseMapperX { + + /** + * 根据定额基价ID查询市场主材设备列表 + */ + default List selectListByQuotaItemId(Long quotaItemId) { + return selectList(new LambdaQueryWrapperX() + .eq(QuotaMarketMaterialDO::getQuotaItemId, quotaItemId) + .orderByAsc(QuotaMarketMaterialDO::getSortOrder) + .orderByAsc(QuotaMarketMaterialDO::getCreateTime)); + } + + /** + * 根据定额基价ID列表批量查询 + */ + default List selectListByQuotaItemIds(List quotaItemIds) { + return selectList(QuotaMarketMaterialDO::getQuotaItemId, quotaItemIds); + } + + /** + * 根据资源项ID查询使用该资源的列表 + */ + default List selectListByResourceItemId(Long resourceItemId) { + return selectList(QuotaMarketMaterialDO::getResourceItemId, resourceItemId); + } + + /** + * 删除定额基价的所有市场主材设备 + */ + default int deleteByQuotaItemId(Long quotaItemId) { + return delete(QuotaMarketMaterialDO::getQuotaItemId, quotaItemId); + } + + /** + * 批量插入市场主材设备 + */ + default void insertBatch(List list) { + list.forEach(this::insert); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaRateFieldMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaRateFieldMapper.java index 0fdb61f..c013a95 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaRateFieldMapper.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaRateFieldMapper.java @@ -3,9 +3,12 @@ package com.yhy.module.core.dal.mysql.quota; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import com.yhy.module.core.dal.dataobject.quota.QuotaRateFieldDO; +import com.yhy.module.core.dal.typehandler.PostgreSQLBigintArrayTypeHandler; import java.util.List; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Result; +import org.apache.ibatis.annotations.Results; import org.apache.ibatis.annotations.Select; /** @@ -16,12 +19,13 @@ public interface QuotaRateFieldMapper extends BaseMapperX { /** * 根据费率模式节点ID查询字段列表 + * 使用 @Select 注解以确保 binding_ids 数组字段正确映射 */ - default List selectListByCatalogItemId(Long catalogItemId) { - return selectList(new LambdaQueryWrapperX() - .eq(QuotaRateFieldDO::getCatalogItemId, catalogItemId) - .orderByAsc(QuotaRateFieldDO::getFieldIndex)); - } + @Select("SELECT * FROM yhy_quota_rate_field WHERE catalog_item_id = #{catalogItemId} AND deleted = 0 ORDER BY field_index ASC") + @Results({ + @Result(column = "binding_ids", property = "bindingIds", typeHandler = PostgreSQLBigintArrayTypeHandler.class) + }) + List selectListByCatalogItemId(@Param("catalogItemId") Long catalogItemId); /** * 根据费率模式节点ID和字段索引查询 diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaRateItemMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaRateItemMapper.java index 00fa1dd..f58aeb7 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaRateItemMapper.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaRateItemMapper.java @@ -73,4 +73,16 @@ public interface QuotaRateItemMapper extends BaseMapperX { return selectCount(new LambdaQueryWrapperX() .eq(QuotaRateItemDO::getCatalogItemId, catalogItemId)); } + + /** + * 将指定排序值及之后的节点排序值+1(用于插入操作) + */ + @Update("UPDATE yhy_quota_rate_item SET sort_order = sort_order + 1, update_time = NOW() " + + "WHERE catalog_item_id = #{catalogItemId} " + + "AND (parent_id = #{parentId} OR (#{parentId}::bigint IS NULL AND parent_id IS NULL)) " + + "AND sort_order >= #{fromSortOrder} " + + "AND deleted = 0") + int incrementSortOrderFrom(@Param("catalogItemId") Long catalogItemId, + @Param("parentId") Long parentId, + @Param("fromSortOrder") Integer fromSortOrder); } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaResourceMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaResourceMapper.java index e1ab069..35d632e 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaResourceMapper.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaResourceMapper.java @@ -1,13 +1,13 @@ package com.yhy.module.core.dal.mysql.quota; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import com.yhy.module.core.dal.dataobject.quota.QuotaResourceDO; +import java.util.List; import org.apache.ibatis.annotations.Mapper; -import java.util.List; - /** - * 定额子目工料机组成 Mapper + * 定额基价工料机组成 Mapper * * @author yhy */ @@ -15,14 +15,17 @@ import java.util.List; public interface QuotaResourceMapper extends BaseMapperX { /** - * 根据定额子目ID查询工料机组成列表 + * 根据定额基价ID查询工料机组成列表(按排序字段排序,相同排序值按创建时间排序) */ default List selectListByQuotaItemId(Long quotaItemId) { - return selectList(QuotaResourceDO::getQuotaItemId, quotaItemId); + return selectList(new LambdaQueryWrapperX() + .eq(QuotaResourceDO::getQuotaItemId, quotaItemId) + .orderByAsc(QuotaResourceDO::getSortOrder) + .orderByAsc(QuotaResourceDO::getCreateTime)); } /** - * 根据定额子目ID列表批量查询 + * 根据定额基价ID列表批量查询 */ default List selectListByQuotaItemIds(List quotaItemIds) { return selectList(QuotaResourceDO::getQuotaItemId, quotaItemIds); @@ -36,7 +39,7 @@ public interface QuotaResourceMapper extends BaseMapperX { } /** - * 删除定额子目的所有工料机组成 + * 删除定额基价的所有工料机组成 */ default int deleteByQuotaItemId(Long quotaItemId) { return delete(QuotaResourceDO::getQuotaItemId, quotaItemId); diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaUnifiedFeeMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaUnifiedFeeMapper.java new file mode 100644 index 0000000..13a8be7 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaUnifiedFeeMapper.java @@ -0,0 +1,85 @@ +package com.yhy.module.core.dal.mysql.quota; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.quota.QuotaUnifiedFeeDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 统一取费单价 Mapper + */ +@Mapper +public interface QuotaUnifiedFeeMapper extends BaseMapperX { + + /** + * 根据模式节点ID查询列表 + */ + default List selectListByCatalogItemId(Long catalogItemId) { + return selectList(new LambdaQueryWrapperX() + .eq(QuotaUnifiedFeeDO::getCatalogItemId, catalogItemId) + .orderByAsc(QuotaUnifiedFeeDO::getSortOrder)); + } + + /** + * 根据模式节点ID查询父节点列表 + */ + default List selectParentListByCatalogItemId(Long catalogItemId) { + return selectList(new LambdaQueryWrapperX() + .eq(QuotaUnifiedFeeDO::getCatalogItemId, catalogItemId) + .isNull(QuotaUnifiedFeeDO::getParentId) + .orderByAsc(QuotaUnifiedFeeDO::getSortOrder)); + } + + /** + * 根据父节点ID查询子节点列表 + */ + default List selectChildListByParentId(Long parentId) { + return selectList(new LambdaQueryWrapperX() + .eq(QuotaUnifiedFeeDO::getParentId, parentId) + .orderByAsc(QuotaUnifiedFeeDO::getSortOrder)); + } + + /** + * 查询同级最大排序值 + */ + default Integer selectMaxSortOrder(Long catalogItemId, Long parentId) { + LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX() + .eq(QuotaUnifiedFeeDO::getCatalogItemId, catalogItemId) + .orderByDesc(QuotaUnifiedFeeDO::getSortOrder) + .last("LIMIT 1"); + if (parentId != null) { + wrapper.eq(QuotaUnifiedFeeDO::getParentId, parentId); + } else { + wrapper.isNull(QuotaUnifiedFeeDO::getParentId); + } + QuotaUnifiedFeeDO maxItem = selectOne(wrapper); + return maxItem != null ? maxItem.getSortOrder() : 0; + } + + /** + * 根据模式节点ID和代号查询 + */ + default QuotaUnifiedFeeDO selectByCode(Long catalogItemId, String code) { + return selectOne(new LambdaQueryWrapperX() + .eq(QuotaUnifiedFeeDO::getCatalogItemId, catalogItemId) + .eq(QuotaUnifiedFeeDO::getCode, code)); + } + + /** + * 检查是否有子节点 + */ + default boolean hasChildren(Long parentId) { + return selectCount(new LambdaQueryWrapperX() + .eq(QuotaUnifiedFeeDO::getParentId, parentId)) > 0; + } + + /** + * 根据feeItemId和catalogItemId查询 + */ + default QuotaUnifiedFeeDO selectByFeeItemIdAndCatalogItemId(Long feeItemId, Long catalogItemId) { + return selectOne(new LambdaQueryWrapperX() + .eq(QuotaUnifiedFeeDO::getFeeItemId, feeItemId) + .eq(QuotaUnifiedFeeDO::getCatalogItemId, catalogItemId)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaUnifiedFeeResourceMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaUnifiedFeeResourceMapper.java new file mode 100644 index 0000000..9b95b56 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaUnifiedFeeResourceMapper.java @@ -0,0 +1,51 @@ +package com.yhy.module.core.dal.mysql.quota; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.quota.QuotaUnifiedFeeResourceDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 统一取费子目工料机 Mapper + */ +@Mapper +public interface QuotaUnifiedFeeResourceMapper extends BaseMapperX { + + /** + * 根据子定额ID查询工料机列表 + */ + default List selectListByUnifiedFeeSettingId(Long unifiedFeeSettingId) { + return selectList(new LambdaQueryWrapperX() + .eq(QuotaUnifiedFeeResourceDO::getUnifiedFeeSettingId, unifiedFeeSettingId) + .orderByAsc(QuotaUnifiedFeeResourceDO::getSortOrder)); + } + + /** + * 根据子定额ID删除工料机 + */ + default int deleteByUnifiedFeeSettingId(Long unifiedFeeSettingId) { + return delete(new LambdaQueryWrapperX() + .eq(QuotaUnifiedFeeResourceDO::getUnifiedFeeSettingId, unifiedFeeSettingId)); + } + + /** + * 查询同级最大排序值 + */ + default Integer selectMaxSortOrder(Long unifiedFeeSettingId) { + QuotaUnifiedFeeResourceDO maxItem = selectOne(new LambdaQueryWrapperX() + .eq(QuotaUnifiedFeeResourceDO::getUnifiedFeeSettingId, unifiedFeeSettingId) + .orderByDesc(QuotaUnifiedFeeResourceDO::getSortOrder) + .last("LIMIT 1")); + return maxItem != null ? maxItem.getSortOrder() : 0; + } + + /** + * 根据子定额ID和工料机ID查询 + */ + default QuotaUnifiedFeeResourceDO selectByResourceItemId(Long unifiedFeeSettingId, Long resourceItemId) { + return selectOne(new LambdaQueryWrapperX() + .eq(QuotaUnifiedFeeResourceDO::getUnifiedFeeSettingId, unifiedFeeSettingId) + .eq(QuotaUnifiedFeeResourceDO::getResourceItemId, resourceItemId)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaUnifiedFeeSettingMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaUnifiedFeeSettingMapper.java new file mode 100644 index 0000000..6d43469 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaUnifiedFeeSettingMapper.java @@ -0,0 +1,98 @@ +package com.yhy.module.core.dal.mysql.quota; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.quota.QuotaUnifiedFeeSettingDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 统一取费设置 Mapper + */ +@Mapper +public interface QuotaUnifiedFeeSettingMapper extends BaseMapperX { + + /** + * 根据模式节点ID查询列表 + */ + default List selectListByCatalogItemId(Long catalogItemId) { + return selectList(new LambdaQueryWrapperX() + .eq(QuotaUnifiedFeeSettingDO::getCatalogItemId, catalogItemId) + .orderByAsc(QuotaUnifiedFeeSettingDO::getSortOrder)); + } + + /** + * 根据模式节点ID查询父定额列表 + */ + default List selectParentListByCatalogItemId(Long catalogItemId) { + return selectList(new LambdaQueryWrapperX() + .eq(QuotaUnifiedFeeSettingDO::getCatalogItemId, catalogItemId) + .eq(QuotaUnifiedFeeSettingDO::getNodeType, QuotaUnifiedFeeSettingDO.NODE_TYPE_PARENT) + .isNull(QuotaUnifiedFeeSettingDO::getParentId) + .orderByAsc(QuotaUnifiedFeeSettingDO::getSortOrder)); + } + + /** + * 根据父节点ID查询子定额列表 + */ + default List selectChildListByParentId(Long parentId) { + return selectList(new LambdaQueryWrapperX() + .eq(QuotaUnifiedFeeSettingDO::getParentId, parentId) + .eq(QuotaUnifiedFeeSettingDO::getNodeType, QuotaUnifiedFeeSettingDO.NODE_TYPE_CHILD) + .orderByAsc(QuotaUnifiedFeeSettingDO::getSortOrder)); + } + + /** + * 查询同级最大排序值 + */ + default Integer selectMaxSortOrder(Long catalogItemId, Long parentId) { + LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX() + .eq(QuotaUnifiedFeeSettingDO::getCatalogItemId, catalogItemId) + .orderByDesc(QuotaUnifiedFeeSettingDO::getSortOrder) + .last("LIMIT 1"); + if (parentId != null) { + wrapper.eq(QuotaUnifiedFeeSettingDO::getParentId, parentId); + } else { + wrapper.isNull(QuotaUnifiedFeeSettingDO::getParentId); + } + QuotaUnifiedFeeSettingDO maxItem = selectOne(wrapper); + return maxItem != null ? maxItem.getSortOrder() : 0; + } + + /** + * 根据模式节点ID和编号查询 + */ + default QuotaUnifiedFeeSettingDO selectByCode(Long catalogItemId, String code) { + return selectOne(new LambdaQueryWrapperX() + .eq(QuotaUnifiedFeeSettingDO::getCatalogItemId, catalogItemId) + .eq(QuotaUnifiedFeeSettingDO::getCode, code)); + } + + /** + * 将指定位置及之后的记录的 sortOrder 都 +1 + * 用于在指定位置插入新记录 + */ + default void incrementSortOrderFrom(Long catalogItemId, Long parentId, Integer fromSortOrder) { + LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX<>(); + wrapper.eq(QuotaUnifiedFeeSettingDO::getCatalogItemId, catalogItemId); + wrapper.ge(QuotaUnifiedFeeSettingDO::getSortOrder, fromSortOrder); + if (parentId != null) { + wrapper.eq(QuotaUnifiedFeeSettingDO::getParentId, parentId); + } else { + wrapper.isNull(QuotaUnifiedFeeSettingDO::getParentId); + } + List records = selectList(wrapper); + for (QuotaUnifiedFeeSettingDO record : records) { + record.setSortOrder(record.getSortOrder() + 1); + updateById(record); + } + } + + /** + * 检查是否有子节点 + */ + default boolean hasChildren(Long parentId) { + return selectCount(new LambdaQueryWrapperX() + .eq(QuotaUnifiedFeeSettingDO::getParentId, parentId)) > 0; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaVariableSettingMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaVariableSettingMapper.java new file mode 100644 index 0000000..73e43d9 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/quota/QuotaVariableSettingMapper.java @@ -0,0 +1,75 @@ +package com.yhy.module.core.dal.mysql.quota; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.quota.QuotaVariableSettingDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +/** + * 单位工程变量设置 Mapper + */ +@Mapper +public interface QuotaVariableSettingMapper extends BaseMapperX { + + /** + * 根据费率模式节点ID和类别查询列表 + */ + default List selectListByCatalogItemIdAndCategory(Long catalogItemId, String category) { + return selectList(new LambdaQueryWrapperX() + .eq(QuotaVariableSettingDO::getCatalogItemId, catalogItemId) + .eq(QuotaVariableSettingDO::getCategory, category) + .orderByAsc(QuotaVariableSettingDO::getSortOrder)); + } + + /** + * 根据费率模式节点ID查询所有类别的列表 + */ + default List selectListByCatalogItemId(Long catalogItemId) { + return selectList(new LambdaQueryWrapperX() + .eq(QuotaVariableSettingDO::getCatalogItemId, catalogItemId) + .orderByAsc(QuotaVariableSettingDO::getCategory) + .orderByAsc(QuotaVariableSettingDO::getSortOrder)); + } + + /** + * 根据费率模式节点ID、类别和代号查询 + */ + default QuotaVariableSettingDO selectByCode(Long catalogItemId, String category, String code) { + return selectOne(new LambdaQueryWrapperX() + .eq(QuotaVariableSettingDO::getCatalogItemId, catalogItemId) + .eq(QuotaVariableSettingDO::getCategory, category) + .eq(QuotaVariableSettingDO::getCode, code)); + } + + /** + * 根据费率模式节点ID和代号查询(跨类别,不限定 category) + */ + default QuotaVariableSettingDO selectByCodeAcrossCategories(Long catalogItemId, String code) { + return selectOne(new LambdaQueryWrapperX() + .eq(QuotaVariableSettingDO::getCatalogItemId, catalogItemId) + .eq(QuotaVariableSettingDO::getCode, code) + .last("LIMIT 1")); + } + + /** + * 查询同类别最大排序值 + */ + default Integer selectMaxSortOrder(Long catalogItemId, String category) { + QuotaVariableSettingDO maxItem = selectOne(new LambdaQueryWrapperX() + .eq(QuotaVariableSettingDO::getCatalogItemId, catalogItemId) + .eq(QuotaVariableSettingDO::getCategory, category) + .orderByDesc(QuotaVariableSettingDO::getSortOrder) + .last("LIMIT 1")); + return maxItem != null ? maxItem.getSortOrder() : 0; + } + + /** + * 将指定位置及之后的行的排序值都加1 + * 实现在 QuotaVariableSettingMapper.xml 中 + */ + void incrementSortOrderFrom(@Param("catalogItemId") Long catalogItemId, + @Param("category") String category, + @Param("sortOrder") Integer sortOrder); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/resource/ResourceCatalogItemMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/resource/ResourceCatalogItemMapper.java index fb7ac7d..8a5a2a7 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/resource/ResourceCatalogItemMapper.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/resource/ResourceCatalogItemMapper.java @@ -27,4 +27,20 @@ public interface ResourceCatalogItemMapper extends BaseMapperX selectByPathLevel(@Param("pathLength") Integer pathLength, + @Param("tenantId") Long tenantId); } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/resource/ResourceCategoryTreeMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/resource/ResourceCategoryTreeMapper.java index e3f2670..03b2df3 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/resource/ResourceCategoryTreeMapper.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/resource/ResourceCategoryTreeMapper.java @@ -1,7 +1,9 @@ package com.yhy.module.core.dal.mysql.resource; -import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import com.yhy.module.core.dal.dataobject.resource.ResourceCategoryTreeDO; +import java.util.List; import org.apache.ibatis.annotations.Mapper; /** @@ -10,5 +12,14 @@ import org.apache.ibatis.annotations.Mapper; * @author yihuiyong */ @Mapper -public interface ResourceCategoryTreeMapper extends BaseMapper { +public interface ResourceCategoryTreeMapper extends BaseMapperX { + + /** + * 根据catalogId查询机类树 + */ + default List selectByCatalogId(Long catalogId) { + return selectList(new LambdaQueryWrapperX() + .eq(ResourceCategoryTreeDO::getCatalogId, catalogId) + .orderByAsc(ResourceCategoryTreeDO::getSortOrder)); + } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/resource/ResourceCategoryTreeMappingMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/resource/ResourceCategoryTreeMappingMapper.java index 189f1cd..690c195 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/resource/ResourceCategoryTreeMappingMapper.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/resource/ResourceCategoryTreeMappingMapper.java @@ -1,9 +1,12 @@ package com.yhy.module.core.dal.mysql.resource; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import com.yhy.module.core.dal.dataobject.resource.ResourceCategoryTreeMappingDO; import org.apache.ibatis.annotations.Mapper; +import java.util.List; + /** * 工料机机类树与类别字典关联 Mapper * @@ -11,4 +14,13 @@ import org.apache.ibatis.annotations.Mapper; */ @Mapper public interface ResourceCategoryTreeMappingMapper extends BaseMapperX { + + /** + * 根据机类树节点ID查询映射 + */ + default List selectByCategoryTreeId(Long categoryTreeId) { + return selectList(new LambdaQueryWrapperX() + .eq(ResourceCategoryTreeMappingDO::getCategoryTreeId, categoryTreeId) + .orderByAsc(ResourceCategoryTreeMappingDO::getSortOrder)); + } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/resource/ResourceInfoPriceMappingMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/resource/ResourceInfoPriceMappingMapper.java new file mode 100644 index 0000000..6ac46ed --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/resource/ResourceInfoPriceMappingMapper.java @@ -0,0 +1,215 @@ +package com.yhy.module.core.dal.mysql.resource; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.resource.ResourceInfoPriceMappingDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 工料机-信息价关联 Mapper + * + * @author yhy + */ +@Mapper +public interface ResourceInfoPriceMappingMapper extends BaseMapperX { + + /** + * 根据工料机ID查询关联的信息价列表 + */ + default List selectListByResourceItemId(Long resourceItemId) { + return selectList(new LambdaQueryWrapperX() + .eq(ResourceInfoPriceMappingDO::getResourceItemId, resourceItemId)); + } + + /** + * 根据项目ID和工料机ID查询关联的信息价列表 + */ + default List selectListByProjectAndResourceItem(Long projectId, Long resourceItemId) { + return selectList(new LambdaQueryWrapperX() + .eq(ResourceInfoPriceMappingDO::getProjectId, projectId) + .eq(ResourceInfoPriceMappingDO::getResourceItemId, resourceItemId)); + } + + /** + * 根据信息价工料机ID查询关联的工料机列表 + */ + default List selectListByInfoPriceResourceId(Long infoPriceResourceId) { + return selectList(new LambdaQueryWrapperX() + .eq(ResourceInfoPriceMappingDO::getInfoPriceResourceId, infoPriceResourceId)); + } + + /** + * 统计工料机关联的信息价数量 + */ + default Long selectCountByResourceItemId(Long resourceItemId) { + return selectCount(new LambdaQueryWrapperX() + .eq(ResourceInfoPriceMappingDO::getResourceItemId, resourceItemId)); + } + + /** + * 检查关联是否已存在 + */ + default boolean existsByResourceAndInfoPrice(Long resourceItemId, Long infoPriceResourceId) { + return selectCount(new LambdaQueryWrapperX() + .eq(ResourceInfoPriceMappingDO::getResourceItemId, resourceItemId) + .eq(ResourceInfoPriceMappingDO::getInfoPriceResourceId, infoPriceResourceId)) > 0; + } + + /** + * 删除指定工料机的所有关联 + */ + default int deleteByResourceItemId(Long resourceItemId) { + return delete(new LambdaQueryWrapperX() + .eq(ResourceInfoPriceMappingDO::getResourceItemId, resourceItemId)); + } + + /** + * 物理删除指定的关联记录(包括已软删除的) + * 用于避免唯一约束冲突 + */ + @org.apache.ibatis.annotations.Delete("DELETE FROM yhy_resource_info_price_mapping WHERE resource_item_id = #{resourceItemId} AND info_price_resource_id = #{infoPriceResourceId}") + int physicalDeleteByResourceAndInfoPrice(@org.apache.ibatis.annotations.Param("resourceItemId") Long resourceItemId, + @org.apache.ibatis.annotations.Param("infoPriceResourceId") Long infoPriceResourceId); + + /** + * 清除工料机的所有当前价标记 + */ + @org.apache.ibatis.annotations.Update("UPDATE yhy_resource_info_price_mapping SET is_current = 0 WHERE resource_item_id = #{resourceItemId} AND deleted = 0") + int clearCurrentByResourceItemId(@org.apache.ibatis.annotations.Param("resourceItemId") Long resourceItemId); + + /** + * 清除项目下工料机的所有当前价标记 + */ + @org.apache.ibatis.annotations.Update("UPDATE yhy_resource_info_price_mapping SET is_current = 0 WHERE project_id = #{projectId} AND resource_item_id = #{resourceItemId} AND deleted = 0") + int clearCurrentByProjectAndResourceItem(@org.apache.ibatis.annotations.Param("projectId") Long projectId, + @org.apache.ibatis.annotations.Param("resourceItemId") Long resourceItemId); + + /** + * 设置当前价格来源 + */ + @org.apache.ibatis.annotations.Update("UPDATE yhy_resource_info_price_mapping SET is_current = 1, selected_price_id = #{selectedPriceId} WHERE resource_item_id = #{resourceItemId} AND info_price_resource_id = #{infoPriceResourceId} AND deleted = 0") + int setCurrentPrice(@org.apache.ibatis.annotations.Param("resourceItemId") Long resourceItemId, + @org.apache.ibatis.annotations.Param("infoPriceResourceId") Long infoPriceResourceId, + @org.apache.ibatis.annotations.Param("selectedPriceId") Long selectedPriceId); + + /** + * 设置当前价格来源(按项目ID和工料机ID) + */ + @org.apache.ibatis.annotations.Update("UPDATE yhy_resource_info_price_mapping SET is_current = 1, selected_price_id = #{selectedPriceId} WHERE project_id = #{projectId} AND resource_item_id = #{resourceItemId} AND info_price_resource_id = #{infoPriceResourceId} AND deleted = 0") + int setCurrentPriceByProject(@org.apache.ibatis.annotations.Param("projectId") Long projectId, + @org.apache.ibatis.annotations.Param("resourceItemId") Long resourceItemId, + @org.apache.ibatis.annotations.Param("infoPriceResourceId") Long infoPriceResourceId, + @org.apache.ibatis.annotations.Param("selectedPriceId") Long selectedPriceId); + + /** + * 更新公式(项目级别) + */ + @org.apache.ibatis.annotations.Update("UPDATE yhy_resource_info_price_mapping SET formula = #{formula} WHERE resource_item_id = #{resourceItemId} AND info_price_resource_id = #{infoPriceResourceId} AND project_id = #{projectId} AND deleted = 0") + int updateFormulaByProject(@org.apache.ibatis.annotations.Param("resourceItemId") Long resourceItemId, + @org.apache.ibatis.annotations.Param("infoPriceResourceId") Long infoPriceResourceId, + @org.apache.ibatis.annotations.Param("projectId") Long projectId, + @org.apache.ibatis.annotations.Param("formula") String formula); + + /** + * 更新公式(兼容旧逻辑) + */ + @org.apache.ibatis.annotations.Update("UPDATE yhy_resource_info_price_mapping SET formula = #{formula} WHERE resource_item_id = #{resourceItemId} AND info_price_resource_id = #{infoPriceResourceId} AND deleted = 0") + int updateFormula(@org.apache.ibatis.annotations.Param("resourceItemId") Long resourceItemId, + @org.apache.ibatis.annotations.Param("infoPriceResourceId") Long infoPriceResourceId, + @org.apache.ibatis.annotations.Param("formula") String formula); + + /** + * 批量查询有当前价格来源的工料机ID + */ + default java.util.Set selectCurrentPriceResourceIds(java.util.List resourceItemIds) { + if (resourceItemIds == null || resourceItemIds.isEmpty()) { + return java.util.Collections.emptySet(); + } + java.util.List list = selectList(new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX() + .in(ResourceInfoPriceMappingDO::getResourceItemId, resourceItemIds) + .eq(ResourceInfoPriceMappingDO::getIsCurrent, 1)); + return list.stream() + .map(ResourceInfoPriceMappingDO::getResourceItemId) + .collect(java.util.stream.Collectors.toSet()); + } + + /** + * 批量查询当前价格来源的详细信息(包含 selectedPriceId) + * @return Map + * @deprecated 使用 selectCurrentPriceMappingsByWbBoqResourceIds 替代 + */ + default java.util.Map selectCurrentPriceMappings(java.util.List resourceItemIds) { + if (resourceItemIds == null || resourceItemIds.isEmpty()) { + return java.util.Collections.emptyMap(); + } + java.util.List list = selectList(new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX() + .in(ResourceInfoPriceMappingDO::getResourceItemId, resourceItemIds) + .eq(ResourceInfoPriceMappingDO::getIsCurrent, 1)); + return list.stream() + .collect(java.util.stream.Collectors.toMap( + ResourceInfoPriceMappingDO::getResourceItemId, + m -> m, + (existing, replacement) -> existing)); + } + + /** + * 批量查询项目下当前价格来源的详细信息 + * @return Map + */ + default java.util.Map selectCurrentPriceMappingsByProject(Long projectId, java.util.List resourceItemIds) { + if (projectId == null || resourceItemIds == null || resourceItemIds.isEmpty()) { + return java.util.Collections.emptyMap(); + } + java.util.List list = selectList(new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX() + .eq(ResourceInfoPriceMappingDO::getProjectId, projectId) + .in(ResourceInfoPriceMappingDO::getResourceItemId, resourceItemIds) + .eq(ResourceInfoPriceMappingDO::getIsCurrent, 1)); + return list.stream() + .collect(java.util.stream.Collectors.toMap( + ResourceInfoPriceMappingDO::getResourceItemId, + m -> m, + (existing, replacement) -> existing)); + } + + default java.util.Map selectCurrentPriceMappingsByResourceItemIds(java.util.List resourceItemIds) { + if (resourceItemIds == null || resourceItemIds.isEmpty()) { + return java.util.Collections.emptyMap(); + } + java.util.List list = selectList(new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX() + .in(ResourceInfoPriceMappingDO::getResourceItemId, resourceItemIds) + .eq(ResourceInfoPriceMappingDO::getIsCurrent, 1)); + return list.stream() + .collect(java.util.stream.Collectors.toMap( + ResourceInfoPriceMappingDO::getResourceItemId, + m -> m, + (existing, replacement) -> existing)); + } + + /** + * 查询租户下该工料机+信息价组合最后修改的公式(按更新时间倒序取第一条非空公式) + * 用于:公式是项目级,相同工料机且公式为空时,按最后一次修改的值出现 + */ + default String selectLastFormulaByResourceAndInfoPrice(Long resourceItemId, Long infoPriceResourceId) { + java.util.List list = selectList(new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX() + .eq(ResourceInfoPriceMappingDO::getResourceItemId, resourceItemId) + .eq(ResourceInfoPriceMappingDO::getInfoPriceResourceId, infoPriceResourceId) + .isNotNull(ResourceInfoPriceMappingDO::getFormula) + .ne(ResourceInfoPriceMappingDO::getFormula, "") + .orderByDesc(ResourceInfoPriceMappingDO::getUpdateTime) + .last("LIMIT 1")); + return list.isEmpty() ? null : list.get(0).getFormula(); + } + + /** + * 查询租户下该工料机已关联的所有信息价ID集合(用于弹窗置顶,租户级) + */ + default java.util.Set selectAssociatedInfoPriceIdsByResourceItem(Long resourceItemId) { + java.util.List list = selectList(new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX() + .eq(ResourceInfoPriceMappingDO::getResourceItemId, resourceItemId) + .select(ResourceInfoPriceMappingDO::getInfoPriceResourceId)); + return list.stream() + .map(ResourceInfoPriceMappingDO::getInfoPriceResourceId) + .collect(java.util.stream.Collectors.toSet()); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/resource/ResourceItemMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/resource/ResourceItemMapper.java index f0cf623..f8babdc 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/resource/ResourceItemMapper.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/resource/ResourceItemMapper.java @@ -15,7 +15,7 @@ public interface ResourceItemMapper extends BaseMapperX { /** * 根据工料机机类树节点ID查询工料机列表 * - * 用于定额子目工料机的范围过滤 + * 用于定额基价工料机的范围过滤 * 通过 catalog_item_id 关联 yhy_catalog_item 表,再过滤 category_tree_id */ @Select("SELECT ri.* FROM yhy_resource_item ri " + @@ -89,4 +89,120 @@ public interface ResourceItemMapper extends BaseMapperX { @Param("taxInclBasePrice") BigDecimal taxInclBasePrice, @Param("taxExclCompilePrice") BigDecimal taxExclCompilePrice, @Param("taxInclCompilePrice") BigDecimal taxInclCompilePrice); + + /** + * 清空工料机的税率字段(用于复合工料机) + * + * @param id 工料机ID + */ + @Update("UPDATE yhy_resource_item SET " + + "tax_rate = NULL, " + + "update_time = now() " + + "WHERE id = #{id} AND deleted = 0") + void clearTaxRate(@Param("id") Long id); + + /** + * 清空工料机的四个价格字段(用于复合工料机) + * + * @param id 工料机ID + */ + @Update("UPDATE yhy_resource_item SET " + + "tax_excl_base_price = NULL, " + + "tax_incl_base_price = NULL, " + + "tax_excl_compile_price = NULL, " + + "tax_incl_compile_price = NULL, " + + "update_time = now() " + + "WHERE id = #{id} AND deleted = 0") + void clearPriceFields(@Param("id") Long id); + + /** + * 查询复合工料机的所有子工料机数据(用于计算合价总和) + * 注意:单位为 % 的子工料机需要在 Service 层根据 calc_base 计算 + * + * @param mergedId 复合工料机ID + * @return 子工料机列表(包含价格、定额消耗量、单位、计算基数、类别ID等信息) + */ + @Select("SELECT " + + " rm.id, rm.source_id, rm.quota_consumption, " + + " ri.unit, ri.calc_base, ri.category_id, " + + " ri.tax_excl_base_price, ri.tax_incl_base_price, " + + " ri.tax_excl_compile_price, ri.tax_incl_compile_price " + + "FROM yhy_resource_merged rm " + + "LEFT JOIN yhy_resource_item ri ON rm.source_id = ri.id AND ri.deleted = 0 " + + "WHERE rm.merged_id = #{mergedId} AND rm.deleted = 0") + List> selectMergedChildrenForSum(@Param("mergedId") Long mergedId); + + /** + * 更新工料机的 is_merged 标记 + * + * @param id 工料机ID + * @param isMerged 是否为复合工料机(0=否,1=是) + */ + @Update("UPDATE yhy_resource_item SET " + + "is_merged = #{isMerged}, " + + "update_time = now() " + + "WHERE id = #{id} AND deleted = 0") + void updateIsMerged(@Param("id") Long id, @Param("isMerged") Integer isMerged); + + /** + * 根据编码查询工料机 + * + * @param code 工料机编码 + * @return 工料机项,如果不存在返回null + */ + @Select("SELECT * FROM yhy_resource_item WHERE code = #{code} AND deleted = 0 LIMIT 1") + ResourceItemDO selectByCode(@Param("code") String code); + + /** + * 根据工料机机类树节点ID和编码精确查询工料机 + * + * @param categoryTreeId 工料机机类树节点ID + * @param code 工料机编码 + * @return 工料机项,如果不存在返回null + */ + @Select("SELECT ri.* FROM yhy_resource_item ri " + + "INNER JOIN yhy_catalog_item rci ON ri.catalog_item_id = rci.id " + + "WHERE rci.category_tree_id = #{categoryTreeId} " + + "AND ri.code = #{code} " + + "AND ri.deleted = 0 " + + "AND rci.deleted = 0 " + + "LIMIT 1") + ResourceItemDO selectByCategoryTreeIdAndCode(@Param("categoryTreeId") Long categoryTreeId, @Param("code") String code); + + /** + * 根据名称、型号规格、单位查询工料机(全局唯一约束) + * + * @param name 名称 + * @param spec 型号规格 + * @param unit 单位 + * @return 工料机项,如果不存在返回null + */ + @Select("") + ResourceItemDO selectByNameSpecUnit(@Param("name") String name, @Param("spec") String spec, @Param("unit") String unit); + + /** + * 查询同目录节点下的最大排序值 + * + * @param catalogItemId 目录节点ID + * @return 最大排序值,如果没有数据返回0 + */ + @Select("SELECT COALESCE(MAX(sort_order), 0) FROM yhy_resource_item " + + "WHERE catalog_item_id = #{catalogItemId} AND deleted = 0") + Integer selectMaxSortOrder(@Param("catalogItemId") Long catalogItemId); + + /** + * 将指定目录节点下,排序值 >= sortOrder 的行的排序值都加1(通过XML实现) + * + * @param catalogItemId 目录节点ID + * @param sortOrder 起始排序值 + */ + void incrementSortOrderFrom(@Param("catalogItemId") Long catalogItemId, + @Param("sortOrder") Integer sortOrder); } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/resource/ResourceMergedMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/resource/ResourceMergedMapper.java index dffb122..2e68572 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/resource/ResourceMergedMapper.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/resource/ResourceMergedMapper.java @@ -34,10 +34,10 @@ public interface ResourceMergedMapper extends BaseMapperX { " ri.tax_excl_compile_price, ri.tax_incl_compile_price, " + " ri.calc_base, " + " rc.name AS category_name, " + - " ROUND(COALESCE(ri.tax_excl_base_price, 0) * COALESCE(rm.quota_consumption, 0) / 100, 4) AS tax_excl_base_total, " + - " ROUND(COALESCE(ri.tax_incl_base_price, 0) * COALESCE(rm.quota_consumption, 0) / 100, 4) AS tax_incl_base_total, " + - " ROUND(COALESCE(ri.tax_excl_compile_price, 0) * COALESCE(rm.quota_consumption, 0) / 100, 4) AS tax_excl_compile_total, " + - " ROUND(COALESCE(ri.tax_incl_compile_price, 0) * COALESCE(rm.quota_consumption, 0) / 100, 4) AS tax_incl_compile_total " + + " ROUND(COALESCE(ri.tax_excl_base_price, 0) * COALESCE(rm.quota_consumption, 0), 4) AS tax_excl_base_total, " + + " ROUND(COALESCE(ri.tax_incl_base_price, 0) * COALESCE(rm.quota_consumption, 0), 4) AS tax_incl_base_total, " + + " ROUND(COALESCE(ri.tax_excl_compile_price, 0) * COALESCE(rm.quota_consumption, 0), 4) AS tax_excl_compile_total, " + + " ROUND(COALESCE(ri.tax_incl_compile_price, 0) * COALESCE(rm.quota_consumption, 0), 4) AS tax_incl_compile_total " + "FROM yhy_resource_merged rm " + "LEFT JOIN yhy_resource_item ri ON rm.source_id = ri.id AND ri.deleted = 0 " + "LEFT JOIN yhy_resource_category rc ON ri.category_id = rc.id AND rc.deleted = 0 " + @@ -50,37 +50,6 @@ public interface ResourceMergedMapper extends BaseMapperX { * @param pageReqVO 分页查询条件 * @return 复合工料机列表(包含第四层信息) */ - @Select("") List selectPageWithDetails(@Param("pageReqVO") ResourceMergedPageReqVO pageReqVO, @Param("offset") int offset); @@ -105,6 +74,8 @@ public interface ResourceMergedMapper extends BaseMapperX { */ default List selectByMergedId(Long mergedId) { return selectList(new LambdaQueryWrapperX() - .eq(ResourceMergedDO::getMergedId, mergedId)); + .eq(ResourceMergedDO::getMergedId, mergedId) + .orderByAsc(ResourceMergedDO::getSourceId) + .orderByAsc(ResourceMergedDO::getRegionCode)); } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/AuditApproveDivisionMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/AuditApproveDivisionMapper.java new file mode 100644 index 0000000..07ba6e0 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/AuditApproveDivisionMapper.java @@ -0,0 +1,62 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.workbench.AuditApproveDivisionDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 审定分部分项快照 Mapper + * + * @author yhy + */ +@Mapper +public interface AuditApproveDivisionMapper extends BaseMapperX { + + /** + * 根据审核模式ID和编制树ID查询所有节点 + */ + default List selectListByAuditModeIdAndCompileTreeId(Long auditModeId, Long compileTreeId) { + return selectList(new LambdaQueryWrapperX() + .eq(AuditApproveDivisionDO::getAuditModeId, auditModeId) + .eq(AuditApproveDivisionDO::getCompileTreeId, compileTreeId) + .orderByAsc(AuditApproveDivisionDO::getSortOrder)); + } + + /** + * 根据审核模式ID查询所有节点 + */ + default List selectListByAuditModeId(Long auditModeId) { + return selectList(new LambdaQueryWrapperX() + .eq(AuditApproveDivisionDO::getAuditModeId, auditModeId) + .orderByAsc(AuditApproveDivisionDO::getSortOrder)); + } + + /** + * 根据父节点ID查询子节点列表 + */ + default List selectListByParentId(Long parentId) { + return selectList(new LambdaQueryWrapperX() + .eq(AuditApproveDivisionDO::getParentId, parentId) + .orderByAsc(AuditApproveDivisionDO::getSortOrder)); + } + + /** + * 根据来源分部分项ID查询 + */ + default AuditApproveDivisionDO selectBySourceDivisionId(Long auditModeId, Long compileTreeId, Long sourceDivisionId) { + return selectOne(new LambdaQueryWrapperX() + .eq(AuditApproveDivisionDO::getAuditModeId, auditModeId) + .eq(AuditApproveDivisionDO::getCompileTreeId, compileTreeId) + .eq(AuditApproveDivisionDO::getSourceDivisionId, sourceDivisionId)); + } + + /** + * 根据审核模式ID删除所有节点 + */ + default int deleteByAuditModeId(Long auditModeId) { + return delete(new LambdaQueryWrapperX() + .eq(AuditApproveDivisionDO::getAuditModeId, auditModeId)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/AuditApproveResourceMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/AuditApproveResourceMapper.java new file mode 100644 index 0000000..db1c436 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/AuditApproveResourceMapper.java @@ -0,0 +1,53 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.workbench.AuditApproveResourceDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 审定工料机快照 Mapper + * + * @author yhy + */ +@Mapper +public interface AuditApproveResourceMapper extends BaseMapperX { + + /** + * 根据审定分部分项ID查询工料机列表 + */ + default List selectListByAuditApproveDivisionId(Long auditApproveDivisionId) { + return selectList(new LambdaQueryWrapperX() + .eq(AuditApproveResourceDO::getAuditApproveDivisionId, auditApproveDivisionId) + .orderByAsc(AuditApproveResourceDO::getSortOrder)); + } + + /** + * 根据来源工料机ID查询 + */ + default AuditApproveResourceDO selectBySourceResourceId(Long auditApproveDivisionId, Long sourceResourceId) { + return selectOne(new LambdaQueryWrapperX() + .eq(AuditApproveResourceDO::getAuditApproveDivisionId, auditApproveDivisionId) + .eq(AuditApproveResourceDO::getSourceResourceId, sourceResourceId)); + } + + /** + * 根据审定分部分项ID删除所有工料机 + */ + default int deleteByAuditApproveDivisionId(Long auditApproveDivisionId) { + return delete(new LambdaQueryWrapperX() + .eq(AuditApproveResourceDO::getAuditApproveDivisionId, auditApproveDivisionId)); + } + + /** + * 根据审定分部分项ID列表批量删除 + */ + default int deleteByAuditApproveDivisionIds(List auditApproveDivisionIds) { + if (auditApproveDivisionIds == null || auditApproveDivisionIds.isEmpty()) { + return 0; + } + return delete(new LambdaQueryWrapperX() + .in(AuditApproveResourceDO::getAuditApproveDivisionId, auditApproveDivisionIds)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/AuditModeMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/AuditModeMapper.java new file mode 100644 index 0000000..23c2ce5 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/AuditModeMapper.java @@ -0,0 +1,35 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.workbench.AuditModeDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 审核模式 Mapper + * + * @author yhy + */ +@Mapper +public interface AuditModeMapper extends BaseMapperX { + + /** + * 根据项目ID查询审核模式列表 + */ + default List selectListByProjectId(Long projectId) { + return selectList(new LambdaQueryWrapperX() + .eq(AuditModeDO::getProjectId, projectId) + .orderByDesc(AuditModeDO::getCreateTime)); + } + + /** + * 根据项目ID和状态查询 + */ + default List selectListByProjectIdAndStatus(Long projectId, String status) { + return selectList(new LambdaQueryWrapperX() + .eq(AuditModeDO::getProjectId, projectId) + .eqIfPresent(AuditModeDO::getStatus, status) + .orderByDesc(AuditModeDO::getCreateTime)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/ProgressDivisionMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/ProgressDivisionMapper.java new file mode 100644 index 0000000..2255d92 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/ProgressDivisionMapper.java @@ -0,0 +1,58 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.workbench.ProgressDivisionDO; +import java.util.Collection; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 进度款-清单关联 Mapper + * + * @author yhy + */ +@Mapper +public interface ProgressDivisionMapper extends BaseMapperX { + + /** + * 根据进度款模式ID查询关联列表 + */ + default List selectListByProgressPaymentModeId(Long progressPaymentModeId) { + return selectList(new LambdaQueryWrapperX() + .eq(ProgressDivisionDO::getProgressPaymentModeId, progressPaymentModeId)); + } + + /** + * 根据清单节点ID查询关联列表 + */ + default List selectListByBoqDivisionId(Long boqDivisionId) { + return selectList(new LambdaQueryWrapperX() + .eq(ProgressDivisionDO::getBoqDivisionId, boqDivisionId)); + } + + /** + * 根据进度款模式ID和清单节点ID查询 + */ + default ProgressDivisionDO selectByPaymentModeIdAndBoqDivisionId(Long progressPaymentModeId, Long boqDivisionId) { + return selectOne(new LambdaQueryWrapperX() + .eq(ProgressDivisionDO::getProgressPaymentModeId, progressPaymentModeId) + .eq(ProgressDivisionDO::getBoqDivisionId, boqDivisionId)); + } + + /** + * 根据进度款模式ID删除所有关联 + */ + default int deleteByProgressPaymentModeId(Long progressPaymentModeId) { + return delete(new LambdaQueryWrapperX() + .eq(ProgressDivisionDO::getProgressPaymentModeId, progressPaymentModeId)); + } + + /** + * 根据进度款模式ID列表批量查询关联列表 + */ + default List selectListByProgressPaymentModeIds(Collection progressPaymentModeIds) { + return selectList(new LambdaQueryWrapperX() + .in(ProgressDivisionDO::getProgressPaymentModeId, progressPaymentModeIds)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/ProgressPaymentModeMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/ProgressPaymentModeMapper.java new file mode 100644 index 0000000..0ac4c70 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/ProgressPaymentModeMapper.java @@ -0,0 +1,25 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.workbench.ProgressPaymentModeDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 进度款模式 Mapper + * + * @author yhy + */ +@Mapper +public interface ProgressPaymentModeMapper extends BaseMapperX { + + /** + * 根据项目ID查询进度款模式列表(按期数升序) + */ + default List selectListByProjectId(Long projectId) { + return selectList(new LambdaQueryWrapperX() + .eq(ProgressPaymentModeDO::getProjectId, projectId) + .orderByAsc(ProgressPaymentModeDO::getPeriodNumber)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/SyncBindingMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/SyncBindingMapper.java new file mode 100644 index 0000000..ef7a1f2 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/SyncBindingMapper.java @@ -0,0 +1,72 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.workbench.SyncBindingDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 同步绑定关系 Mapper + * + * @author yhy + */ +@Mapper +public interface SyncBindingMapper extends BaseMapperX { + + /** + * 根据同步库ID查询绑定列表 + */ + default List selectListBySyncLibraryId(Long syncLibraryId) { + return selectList(new LambdaQueryWrapperX() + .eq(SyncBindingDO::getSyncLibraryId, syncLibraryId)); + } + + /** + * 根据多个同步库ID批量查询绑定列表 + */ + default List selectListBySyncLibraryIds(List syncLibraryIds) { + return selectList(new LambdaQueryWrapperX() + .in(SyncBindingDO::getSyncLibraryId, syncLibraryIds)); + } + + /** + * 根据分部分项ID查询绑定 + */ + default SyncBindingDO selectByDivisionId(Long divisionId) { + return selectOne(new LambdaQueryWrapperX() + .eq(SyncBindingDO::getDivisionId, divisionId)); + } + + /** + * 根据单位工程ID查询绑定列表 + */ + default List selectListByCompileTreeId(Long compileTreeId) { + return selectList(new LambdaQueryWrapperX() + .eq(SyncBindingDO::getCompileTreeId, compileTreeId)); + } + + /** + * 根据同步库ID统计绑定数量 + */ + default Long selectCountBySyncLibraryId(Long syncLibraryId) { + return selectCount(new LambdaQueryWrapperX() + .eq(SyncBindingDO::getSyncLibraryId, syncLibraryId)); + } + + /** + * 根据分部分项ID删除绑定 + */ + default int deleteByDivisionId(Long divisionId) { + return delete(new LambdaQueryWrapperX() + .eq(SyncBindingDO::getDivisionId, divisionId)); + } + + /** + * 根据同步库ID删除所有绑定 + */ + default int deleteBySyncLibraryId(Long syncLibraryId) { + return delete(new LambdaQueryWrapperX() + .eq(SyncBindingDO::getSyncLibraryId, syncLibraryId)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/SyncLibraryDivisionMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/SyncLibraryDivisionMapper.java new file mode 100644 index 0000000..804e0a2 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/SyncLibraryDivisionMapper.java @@ -0,0 +1,51 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.workbench.SyncLibraryDivisionDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 同步库分部分项树 Mapper + * + * @author yhy + */ +@Mapper +public interface SyncLibraryDivisionMapper extends BaseMapperX { + + /** + * 根据同步库ID查询所有节点 + */ + default List selectListBySyncLibraryId(Long syncLibraryId) { + return selectList(new LambdaQueryWrapperX() + .eq(SyncLibraryDivisionDO::getSyncLibraryId, syncLibraryId) + .orderByAsc(SyncLibraryDivisionDO::getSortOrder)); + } + + /** + * 根据多个同步库ID批量查询所有节点 + */ + default List selectListBySyncLibraryIds(List syncLibraryIds) { + return selectList(new LambdaQueryWrapperX() + .in(SyncLibraryDivisionDO::getSyncLibraryId, syncLibraryIds) + .orderByAsc(SyncLibraryDivisionDO::getSortOrder)); + } + + /** + * 根据父节点ID查询子节点 + */ + default List selectListByParentId(Long parentId) { + return selectList(new LambdaQueryWrapperX() + .eq(SyncLibraryDivisionDO::getParentId, parentId) + .orderByAsc(SyncLibraryDivisionDO::getSortOrder)); + } + + /** + * 根据同步库ID删除所有节点 + */ + default int deleteBySyncLibraryId(Long syncLibraryId) { + return delete(new LambdaQueryWrapperX() + .eq(SyncLibraryDivisionDO::getSyncLibraryId, syncLibraryId)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/SyncLibraryMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/SyncLibraryMapper.java new file mode 100644 index 0000000..4b7467c --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/SyncLibraryMapper.java @@ -0,0 +1,33 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.workbench.SyncLibraryDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 同步库主表 Mapper + * + * @author yhy + */ +@Mapper +public interface SyncLibraryMapper extends BaseMapperX { + + /** + * 根据项目ID查询同步库列表 + */ + default List selectListByProjectId(Long projectId) { + return selectList(new LambdaQueryWrapperX() + .eq(SyncLibraryDO::getProjectId, projectId) + .orderByDesc(SyncLibraryDO::getCreateTime)); + } + + /** + * 根据来源分部分项ID查询 + */ + default SyncLibraryDO selectBySourceDivisionId(Long sourceDivisionId) { + return selectOne(new LambdaQueryWrapperX() + .eq(SyncLibraryDO::getSourceDivisionId, sourceDivisionId)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/SyncLibraryMarketMaterialMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/SyncLibraryMarketMaterialMapper.java new file mode 100644 index 0000000..c0ed38c --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/SyncLibraryMarketMaterialMapper.java @@ -0,0 +1,50 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.workbench.SyncLibraryMarketMaterialDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 同步库市场主材设备 Mapper + * + * @author yhy + */ +@Mapper +public interface SyncLibraryMarketMaterialMapper extends BaseMapperX { + + /** + * 根据同步库分部分项ID查询 + */ + default List selectListBySyncDivisionId(Long syncDivisionId) { + return selectList(new LambdaQueryWrapperX() + .eq(SyncLibraryMarketMaterialDO::getSyncDivisionId, syncDivisionId) + .orderByAsc(SyncLibraryMarketMaterialDO::getSortOrder)); + } + + /** + * 根据同步库分部分项ID列表批量查询 + */ + default List selectListBySyncDivisionIds(List syncDivisionIds) { + return selectList(new LambdaQueryWrapperX() + .in(SyncLibraryMarketMaterialDO::getSyncDivisionId, syncDivisionIds) + .orderByAsc(SyncLibraryMarketMaterialDO::getSortOrder)); + } + + /** + * 根据同步库分部分项ID删除 + */ + default int deleteBySyncDivisionId(Long syncDivisionId) { + return delete(new LambdaQueryWrapperX() + .eq(SyncLibraryMarketMaterialDO::getSyncDivisionId, syncDivisionId)); + } + + /** + * 根据同步库分部分项ID列表批量删除 + */ + default int deleteBySyncDivisionIds(List syncDivisionIds) { + return delete(new LambdaQueryWrapperX() + .in(SyncLibraryMarketMaterialDO::getSyncDivisionId, syncDivisionIds)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/SyncLibraryResourceMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/SyncLibraryResourceMapper.java new file mode 100644 index 0000000..de0de12 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/SyncLibraryResourceMapper.java @@ -0,0 +1,50 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.workbench.SyncLibraryResourceDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 同步库工料机 Mapper + * + * @author yhy + */ +@Mapper +public interface SyncLibraryResourceMapper extends BaseMapperX { + + /** + * 根据同步库分部分项ID查询工料机列表 + */ + default List selectListBySyncDivisionId(Long syncDivisionId) { + return selectList(new LambdaQueryWrapperX() + .eq(SyncLibraryResourceDO::getSyncDivisionId, syncDivisionId) + .orderByAsc(SyncLibraryResourceDO::getSortOrder)); + } + + /** + * 根据同步库分部分项ID列表批量查询 + */ + default List selectListBySyncDivisionIds(List syncDivisionIds) { + return selectList(new LambdaQueryWrapperX() + .in(SyncLibraryResourceDO::getSyncDivisionId, syncDivisionIds) + .orderByAsc(SyncLibraryResourceDO::getSortOrder)); + } + + /** + * 根据同步库分部分项ID删除 + */ + default int deleteBySyncDivisionId(Long syncDivisionId) { + return delete(new LambdaQueryWrapperX() + .eq(SyncLibraryResourceDO::getSyncDivisionId, syncDivisionId)); + } + + /** + * 根据同步库分部分项ID列表批量删除 + */ + default int deleteBySyncDivisionIds(List syncDivisionIds) { + return delete(new LambdaQueryWrapperX() + .in(SyncLibraryResourceDO::getSyncDivisionId, syncDivisionIds)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbAdjustmentSettingMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbAdjustmentSettingMapper.java new file mode 100644 index 0000000..7020ab6 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbAdjustmentSettingMapper.java @@ -0,0 +1,69 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.workbench.WbAdjustmentSettingDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 工作台定额调整设置 Mapper + * + * @author yhy + */ +@Mapper +public interface WbAdjustmentSettingMapper extends BaseMapperX { + + /** + * 根据分部分项定额节点ID查询调整设置列表 + */ + default List selectListByDivisionId(Long divisionId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbAdjustmentSettingDO::getDivisionId, divisionId) + .orderByAsc(WbAdjustmentSettingDO::getSortOrder)); + } + + /** + * 根据分部分项定额节点ID列表批量查询 + */ + default List selectListByDivisionIds(List divisionIds) { + return selectList(new LambdaQueryWrapperX() + .in(WbAdjustmentSettingDO::getDivisionId, divisionIds) + .orderByAsc(WbAdjustmentSettingDO::getSortOrder)); + } + + /** + * 根据来源调整设置ID查询 + */ + default List selectListBySourceSettingId(Long sourceAdjustmentSettingId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbAdjustmentSettingDO::getSourceAdjustmentSettingId, sourceAdjustmentSettingId)); + } + + /** + * 根据分部分项定额节点ID删除所有调整设置 + */ + default int deleteByDivisionId(Long divisionId) { + return delete(new LambdaQueryWrapperX() + .eq(WbAdjustmentSettingDO::getDivisionId, divisionId)); + } + + /** + * 获取同级最大排序号 + */ + default Integer selectMaxSortOrderByDivisionId(Long divisionId) { + WbAdjustmentSettingDO maxSortNode = selectOne(new LambdaQueryWrapperX() + .eq(WbAdjustmentSettingDO::getDivisionId, divisionId) + .orderByDesc(WbAdjustmentSettingDO::getSortOrder) + .last("LIMIT 1")); + return maxSortNode != null ? maxSortNode.getSortOrder() : 0; + } + + /** + * 统计分部分项定额节点下的调整设置数量 + */ + default Long selectCountByDivisionId(Long divisionId) { + return selectCount(new LambdaQueryWrapperX() + .eq(WbAdjustmentSettingDO::getDivisionId, divisionId)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbBoqDivisionMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbBoqDivisionMapper.java new file mode 100644 index 0000000..fc252a8 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbBoqDivisionMapper.java @@ -0,0 +1,237 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.controller.admin.workbench.vo.HistoryBoqListReqVO; +import com.yhy.module.core.dal.dataobject.workbench.WbBoqDivisionDO; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 工作台分部分项树 Mapper + * + * @author yhy + */ +@Mapper +public interface WbBoqDivisionMapper extends BaseMapperX { + + /** + * 根据单位工程节点ID查询所有分部分项节点 + */ + default List selectListByCompileTreeId(Long compileTreeId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbBoqDivisionDO::getCompileTreeId, compileTreeId) + .orderByAsc(WbBoqDivisionDO::getSortOrder)); + } + + /** + * 根据父节点ID查询子节点列表 + */ + default List selectListByParentId(Long parentId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbBoqDivisionDO::getParentId, parentId) + .orderByAsc(WbBoqDivisionDO::getSortOrder)); + } + + /** + * 根据单位工程节点ID查询分部节点(根节点) + */ + default List selectDivisionsByCompileTreeId(Long compileTreeId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbBoqDivisionDO::getCompileTreeId, compileTreeId) + .eq(WbBoqDivisionDO::getNodeType, WbBoqDivisionDO.NODE_TYPE_DIVISION) + .isNull(WbBoqDivisionDO::getParentId) + .orderByAsc(WbBoqDivisionDO::getSortOrder)); + } + + /** + * 获取同级最大排序号 + */ + default Integer selectMaxSortOrderByParentId(Long compileTreeId, Long parentId) { + LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX() + .eq(WbBoqDivisionDO::getCompileTreeId, compileTreeId) + .orderByDesc(WbBoqDivisionDO::getSortOrder) + .last("LIMIT 1"); + if (parentId != null) { + wrapper.eq(WbBoqDivisionDO::getParentId, parentId); + } else { + wrapper.isNull(WbBoqDivisionDO::getParentId); + } + WbBoqDivisionDO maxSortNode = selectOne(wrapper); + return maxSortNode != null ? maxSortNode.getSortOrder() : 0; + } + + /** + * 根据节点类型查询 + */ + default List selectListByNodeType(Long compileTreeId, String nodeType) { + return selectList(new LambdaQueryWrapperX() + .eq(WbBoqDivisionDO::getCompileTreeId, compileTreeId) + .eq(WbBoqDivisionDO::getNodeType, nodeType) + .orderByAsc(WbBoqDivisionDO::getSortOrder)); + } + + /** + * 查询子节点数量 + */ + default Long selectCountByParentId(Long parentId) { + return selectCount(new LambdaQueryWrapperX() + .eq(WbBoqDivisionDO::getParentId, parentId)); + } + + /** + * 根据引用定额ID查询 + */ + default List selectListBySourceQuotaItemId(Long sourceQuotaItemId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbBoqDivisionDO::getSourceQuotaItemId, sourceQuotaItemId)); + } + + /** + * 查询单位工程下所有定额节点(有sourceQuotaItemId的) + */ + default List selectQuotaNodesByCompileTreeId(Long compileTreeId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbBoqDivisionDO::getCompileTreeId, compileTreeId) + .eq(WbBoqDivisionDO::getNodeType, WbBoqDivisionDO.NODE_TYPE_QUOTA) + .isNotNull(WbBoqDivisionDO::getSourceQuotaItemId)); + } + + /** + * 查询租户所有清单节点(跨项目) + */ + default List selectAllBoqNodes() { + return selectList(new LambdaQueryWrapperX() + .eq(WbBoqDivisionDO::getNodeType, WbBoqDivisionDO.NODE_TYPE_BOQ) + .orderByDesc(WbBoqDivisionDO::getCreateTime)); + } + + /** + * 查询清单节点及其子节点(定额) + */ + default List selectBoqWithChildren(Long boqId) { + // 先获取清单节点 + WbBoqDivisionDO boq = selectById(boqId); + if (boq == null) { + return java.util.Collections.emptyList(); + } + // 获取清单及其子节点(定额) + List result = new java.util.ArrayList<>(); + result.add(boq); + result.addAll(selectListByParentId(boqId)); + return result; + } + + /** + * 查询历史清单/分部列表(支持查询条件) + */ + default List selectHistoryList(HistoryBoqListReqVO reqVO) { + LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX<>(); + + // 根据类别筛选节点类型 + if (StrUtil.isNotBlank(reqVO.getCategory())) { + if ("division".equals(reqVO.getCategory())) { + wrapper.eq(WbBoqDivisionDO::getNodeType, WbBoqDivisionDO.NODE_TYPE_DIVISION); + } else if ("boq".equals(reqVO.getCategory())) { + wrapper.eq(WbBoqDivisionDO::getNodeType, WbBoqDivisionDO.NODE_TYPE_BOQ); + } + } else { + // 默认查询清单和分部 + wrapper.in(WbBoqDivisionDO::getNodeType, + WbBoqDivisionDO.NODE_TYPE_BOQ, + WbBoqDivisionDO.NODE_TYPE_DIVISION); + } + + // 注意:清单章节筛选已移至 Service 层通过 sourceBoqItemTreeId 关联 BoqItemTree.boqCatalogItemId 实现 + + // 名称模糊查询 + if (StrUtil.isNotBlank(reqVO.getName())) { + wrapper.like(WbBoqDivisionDO::getName, reqVO.getName()); + } + + // 项目特征模糊查询 + if (StrUtil.isNotBlank(reqVO.getFeature())) { + wrapper.like(WbBoqDivisionDO::getFeature, reqVO.getFeature()); + } + + // 时间范围筛选 + if (reqVO.getStartTime() != null) { + LocalDateTime startDateTime = LocalDateTime.ofInstant( + Instant.ofEpochMilli(reqVO.getStartTime()), ZoneId.systemDefault()); + wrapper.ge(WbBoqDivisionDO::getCreateTime, startDateTime); + } + if (reqVO.getEndTime() != null) { + LocalDateTime endDateTime = LocalDateTime.ofInstant( + Instant.ofEpochMilli(reqVO.getEndTime()), ZoneId.systemDefault()); + wrapper.le(WbBoqDivisionDO::getCreateTime, endDateTime); + } + + // 按创建时间倒序 + wrapper.orderByDesc(WbBoqDivisionDO::getCreateTime); + + return selectList(wrapper); + } + + /** + * 统计同一单位工程下相同编码的清单数量(用于唯一性校验) + * @param compileTreeId 单位工程ID + * @param code 清单编码 + * @param excludeId 排除的节点ID(更新时排除自身) + */ + default Long countBoqByCodeAndCompileTreeId(Long compileTreeId, String code, Long excludeId) { + LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX() + .eq(WbBoqDivisionDO::getCompileTreeId, compileTreeId) + .eq(WbBoqDivisionDO::getNodeType, WbBoqDivisionDO.NODE_TYPE_BOQ) + .eq(WbBoqDivisionDO::getCode, code); + if (excludeId != null) { + wrapper.ne(WbBoqDivisionDO::getId, excludeId); + } + return selectCount(wrapper); + } + + /** + * 查询单位工程下指定费率模式的统一取费节点 + * 统一取费节点的 rateModeId 存储在 attributes JSONB 字段中 + */ + default List selectUnifiedFeeNodesByRateModeId(Long compileTreeId, Long rateModeId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbBoqDivisionDO::getCompileTreeId, compileTreeId) + .eq(WbBoqDivisionDO::getNodeType, "unified_fee") + .apply("attributes->>'rateModeId' = {0}", String.valueOf(rateModeId))); + } + + /** + * 删除单位工程下指定费率模式的统一取费节点 + */ + default int deleteUnifiedFeeNodesByRateModeId(Long compileTreeId, Long rateModeId) { + return delete(new LambdaQueryWrapperX() + .eq(WbBoqDivisionDO::getCompileTreeId, compileTreeId) + .eq(WbBoqDivisionDO::getNodeType, "unified_fee") + .apply("attributes->>'rateModeId' = {0}", String.valueOf(rateModeId))); + } + + /** + * 删除单位工程下不属于指定费率模式的统一取费节点(费率切换时清空其他费率模式的节点) + */ + default int deleteUnifiedFeeNodesExcludeRateModeId(Long compileTreeId, Long rateModeId) { + return delete(new LambdaQueryWrapperX() + .eq(WbBoqDivisionDO::getCompileTreeId, compileTreeId) + .eq(WbBoqDivisionDO::getNodeType, "unified_fee") + .apply("attributes->>'rateModeId' <> {0}", String.valueOf(rateModeId))); + } + + /** + * 清理指定同步库关联的同步状态(sync_library_id 和 is_sync_source) + * 用于同步库删除时防止脏数据 + */ + default int clearSyncStatus(Long syncLibraryId) { + return update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() + .eq(WbBoqDivisionDO::getSyncLibraryId, syncLibraryId) + .set(WbBoqDivisionDO::getSyncLibraryId, null) + .set(WbBoqDivisionDO::getIsSyncSource, false)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbBoqMarketMaterialMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbBoqMarketMaterialMapper.java new file mode 100644 index 0000000..8525a94 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbBoqMarketMaterialMapper.java @@ -0,0 +1,67 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.workbench.WbBoqMarketMaterialDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 工作台市场主材设备消耗 Mapper + * + * @author yhy + */ +@Mapper +public interface WbBoqMarketMaterialMapper extends BaseMapperX { + + /** + * 根据分部分项ID查询市场主材设备列表 + */ + default List selectListByDivisionId(Long divisionId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbBoqMarketMaterialDO::getDivisionId, divisionId) + .orderByAsc(WbBoqMarketMaterialDO::getSortOrder) + .orderByAsc(WbBoqMarketMaterialDO::getCreateTime)); + } + + /** + * 根据分部分项ID列表批量查询 + */ + default List selectListByDivisionIds(List divisionIds) { + return selectList(WbBoqMarketMaterialDO::getDivisionId, divisionIds); + } + + /** + * 根据父工料机ID查询子工料机列表 + */ + default List selectListByParentId(Long parentId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbBoqMarketMaterialDO::getParentId, parentId) + .orderByAsc(WbBoqMarketMaterialDO::getSortOrder)); + } + + /** + * 删除分部分项下的所有市场主材设备 + */ + default int deleteByDivisionId(Long divisionId) { + return delete(WbBoqMarketMaterialDO::getDivisionId, divisionId); + } + + /** + * 批量删除分部分项下的所有市场主材设备 + */ + default int deleteByDivisionIds(List divisionIds) { + if (divisionIds == null || divisionIds.isEmpty()) { + return 0; + } + return delete(new LambdaQueryWrapperX() + .in(WbBoqMarketMaterialDO::getDivisionId, divisionIds)); + } + + /** + * 批量插入市场主材设备 + */ + default void insertBatch(List list) { + list.forEach(this::insert); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbBoqResourceMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbBoqResourceMapper.java new file mode 100644 index 0000000..6cc5a7d --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbBoqResourceMapper.java @@ -0,0 +1,141 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.workbench.WbBoqResourceDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +/** + * 工作台工料机消耗 Mapper + * + * @author yhy + */ +@Mapper +public interface WbBoqResourceMapper extends BaseMapperX { + + /** + * 根据定额节点ID查询工料机列表 + */ + default List selectListByDivisionId(Long divisionId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbBoqResourceDO::getDivisionId, divisionId) + .orderByAsc(WbBoqResourceDO::getSortOrder)); + } + + /** + * 根据定额节点ID列表批量查询工料机 + */ + default List selectListByDivisionIds(List divisionIds) { + return selectList(new LambdaQueryWrapperX() + .in(WbBoqResourceDO::getDivisionId, divisionIds) + .orderByAsc(WbBoqResourceDO::getSortOrder)); + } + + /** + * 根据资源类型查询 + */ + default List selectListByResourceType(Long divisionId, String resourceType) { + return selectList(new LambdaQueryWrapperX() + .eq(WbBoqResourceDO::getDivisionId, divisionId) + .eq(WbBoqResourceDO::getResourceType, resourceType) + .orderByAsc(WbBoqResourceDO::getSortOrder)); + } + + /** + * 获取同级最大排序号 + */ + default Integer selectMaxSortOrderByDivisionId(Long divisionId) { + WbBoqResourceDO maxSortNode = selectOne(new LambdaQueryWrapperX() + .eq(WbBoqResourceDO::getDivisionId, divisionId) + .orderByDesc(WbBoqResourceDO::getSortOrder) + .last("LIMIT 1")); + return maxSortNode != null ? maxSortNode.getSortOrder() : 0; + } + + /** + * 根据定额节点ID删除所有工料机 + */ + default int deleteByDivisionId(Long divisionId) { + return delete(new LambdaQueryWrapperX() + .eq(WbBoqResourceDO::getDivisionId, divisionId)); + } + + /** + * 根据引用定额工料机组成ID查询(用于统计引用次数) + */ + default List selectListBySourceQuotaResourceId(Long sourceQuotaResourceId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbBoqResourceDO::getSourceQuotaResourceId, sourceQuotaResourceId)); + } + + /** + * 统计引用定额工料机组成的数量 + */ + default Long selectCountBySourceQuotaResourceId(Long sourceQuotaResourceId) { + return selectCount(new LambdaQueryWrapperX() + .eq(WbBoqResourceDO::getSourceQuotaResourceId, sourceQuotaResourceId)); + } + + /** + * 根据编制模式树ID(单位工程)和编码模糊查询工料机 + * 用于新增工料机弹窗的"本项目"范围查询 + */ + default List selectByCompileTreeIdAndCodeLike(Long compileTreeId, String code) { + return selectList(new LambdaQueryWrapperX() + .inSql(WbBoqResourceDO::getDivisionId, + "SELECT id FROM yhy_wb_boq_division WHERE compile_tree_id = " + compileTreeId + + " AND node_type = 'quota' AND deleted = 0") + .like(WbBoqResourceDO::getCode, code) + .isNull(WbBoqResourceDO::getParentId) + .orderByAsc(WbBoqResourceDO::getCode)); + } + + /** + * 将指定定额节点下排序号大于指定值的工料机排序号+1 + * 用于在某行下方插入空白行 + */ + default void incrementSortOrderAfter(Long divisionId, Integer afterSortOrder) { + update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() + .eq(WbBoqResourceDO::getDivisionId, divisionId) + .gt(WbBoqResourceDO::getSortOrder, afterSortOrder) + .setSql("sort_order = sort_order + 1")); + } + + /** + * 查询同项目下"编码+名称+型号规格+单位"一致的工料机(排除自身) + * 用于编制价/价格来源的项目级批量同步 + * + * @param projectId 项目ID + * @param code 编码 + * @param name 名称 + * @param spec 型号规格 + * @param unit 单位 + * @param excludeId 排除的工料机ID + * @return 匹配的工料机列表 + */ + @Select("") + List selectSameResourceInProject( + @Param("projectId") Long projectId, + @Param("code") String code, + @Param("name") String name, + @Param("spec") String spec, + @Param("unit") String unit, + @Param("excludeId") Long excludeId); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbCategoryTreeMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbCategoryTreeMapper.java new file mode 100644 index 0000000..f4f8020 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbCategoryTreeMapper.java @@ -0,0 +1,53 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.workbench.WbCategoryTreeDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 工作台-工料机机类树快照 Mapper + * + * @author yihuiyong + */ +@Mapper +public interface WbCategoryTreeMapper extends BaseMapperX { + + /** + * 根据单位工程ID查询机类树 + */ + default List selectByCompileTreeId(Long compileTreeId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbCategoryTreeDO::getCompileTreeId, compileTreeId) + .orderByAsc(WbCategoryTreeDO::getSortOrder)); + } + + /** + * 根据单位工程ID和节点类型查询 + */ + default List selectByCompileTreeIdAndNodeType(Long compileTreeId, String nodeType) { + return selectList(new LambdaQueryWrapperX() + .eq(WbCategoryTreeDO::getCompileTreeId, compileTreeId) + .eq(WbCategoryTreeDO::getNodeType, nodeType) + .orderByAsc(WbCategoryTreeDO::getSortOrder)); + } + + /** + * 根据单位工程ID删除所有机类树数据 + */ + default int deleteByCompileTreeId(Long compileTreeId) { + return delete(new LambdaQueryWrapperX() + .eq(WbCategoryTreeDO::getCompileTreeId, compileTreeId)); + } + + /** + * 根据来源ID查询(用于ID映射) + */ + default WbCategoryTreeDO selectBySourceId(Long compileTreeId, Long sourceId) { + return selectOne(new LambdaQueryWrapperX() + .eq(WbCategoryTreeDO::getCompileTreeId, compileTreeId) + .eq(WbCategoryTreeDO::getSourceId, sourceId)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbCategoryTreeMappingMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbCategoryTreeMappingMapper.java new file mode 100644 index 0000000..89d661c --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbCategoryTreeMappingMapper.java @@ -0,0 +1,43 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.workbench.WbCategoryTreeMappingDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 工作台-机类与类别字典关联快照 Mapper + * + * @author yihuiyong + */ +@Mapper +public interface WbCategoryTreeMappingMapper extends BaseMapperX { + + /** + * 根据单位工程ID查询所有映射 + */ + default List selectByCompileTreeId(Long compileTreeId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbCategoryTreeMappingDO::getCompileTreeId, compileTreeId) + .orderByAsc(WbCategoryTreeMappingDO::getSortOrder)); + } + + /** + * 根据机类树节点ID查询映射的类别ID列表 + */ + default List selectByCategoryTreeId(Long categoryTreeId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbCategoryTreeMappingDO::getCategoryTreeId, categoryTreeId) + .orderByAsc(WbCategoryTreeMappingDO::getSortOrder)); + } + + /** + * 根据单位工程ID删除所有映射数据 + */ + default int deleteByCompileTreeId(Long compileTreeId) { + return delete(new LambdaQueryWrapperX() + .eq(WbCategoryTreeMappingDO::getCompileTreeId, compileTreeId)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbCompileTreeMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbCompileTreeMapper.java new file mode 100644 index 0000000..4755bd0 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbCompileTreeMapper.java @@ -0,0 +1,73 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.workbench.WbCompileTreeDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 编制模式树 Mapper + * + * @author yhy + */ +@Mapper +public interface WbCompileTreeMapper extends BaseMapperX { + + /** + * 根据项目ID查询所有节点 + */ + default List selectListByProjectId(Long projectId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbCompileTreeDO::getProjectId, projectId) + .orderByAsc(WbCompileTreeDO::getSortOrder)); + } + + /** + * 根据父节点ID查询子节点列表 + */ + default List selectListByParentId(Long parentId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbCompileTreeDO::getParentId, parentId) + .orderByAsc(WbCompileTreeDO::getSortOrder)); + } + + /** + * 根据项目ID和节点类型查询 + */ + default WbCompileTreeDO selectByProjectIdAndNodeType(Long projectId, String nodeType) { + return selectOne(new LambdaQueryWrapperX() + .eq(WbCompileTreeDO::getProjectId, projectId) + .eq(WbCompileTreeDO::getNodeType, nodeType)); + } + + /** + * 获取同级最大排序号 + */ + default Integer selectMaxSortOrderByParentId(Long parentId) { + WbCompileTreeDO maxSortNode = selectOne(new LambdaQueryWrapperX() + .eq(WbCompileTreeDO::getParentId, parentId) + .orderByDesc(WbCompileTreeDO::getSortOrder) + .last("LIMIT 1")); + return maxSortNode != null ? maxSortNode.getSortOrder() : 0; + } + + /** + * 根据项目ID查询根节点 + */ + default WbCompileTreeDO selectRootByProjectId(Long projectId) { + return selectOne(new LambdaQueryWrapperX() + .eq(WbCompileTreeDO::getProjectId, projectId) + .eq(WbCompileTreeDO::getNodeType, WbCompileTreeDO.NODE_TYPE_ROOT)); + } + + /** + * 根据项目ID查询所有单位工程节点 + */ + default List selectUnitNodesByProjectId(Long projectId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbCompileTreeDO::getProjectId, projectId) + .eq(WbCompileTreeDO::getNodeType, WbCompileTreeDO.NODE_TYPE_UNIT) + .orderByAsc(WbCompileTreeDO::getSortOrder)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbFeeItemMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbFeeItemMapper.java new file mode 100644 index 0000000..d8b9879 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbFeeItemMapper.java @@ -0,0 +1,72 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.workbench.WbFeeItemDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 工作台-取费模板快照 Mapper + * + * @author yihuiyong + */ +@Mapper +public interface WbFeeItemMapper extends BaseMapperX { + + /** + * 根据单位工程ID查询取费模板 + */ + default List selectByCompileTreeId(Long compileTreeId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbFeeItemDO::getCompileTreeId, compileTreeId) + .orderByAsc(WbFeeItemDO::getSortOrder)); + } + + /** + * 根据单位工程ID和费率模式ID查询取费模板 + */ + default List selectByCompileTreeIdAndCatalogItemId(Long compileTreeId, Long catalogItemId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbFeeItemDO::getCompileTreeId, compileTreeId) + .eq(WbFeeItemDO::getCatalogItemId, catalogItemId) + .orderByAsc(WbFeeItemDO::getSortOrder)); + } + + /** + * 根据单位工程ID和费率项ID查询 + */ + default List selectByRateItemId(Long compileTreeId, Long rateItemId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbFeeItemDO::getCompileTreeId, compileTreeId) + .eq(WbFeeItemDO::getRateItemId, rateItemId) + .orderByAsc(WbFeeItemDO::getSortOrder)); + } + + /** + * 根据单位工程ID删除所有取费模板数据 + */ + default int deleteByCompileTreeId(Long compileTreeId) { + return delete(new LambdaQueryWrapperX() + .eq(WbFeeItemDO::getCompileTreeId, compileTreeId)); + } + + /** + * 根据单位工程ID和费率模式ID删除取费模板数据 + */ + default int deleteByCompileTreeIdAndCatalogItemId(Long compileTreeId, Long catalogItemId) { + return delete(new LambdaQueryWrapperX() + .eq(WbFeeItemDO::getCompileTreeId, compileTreeId) + .eq(WbFeeItemDO::getCatalogItemId, catalogItemId)); + } + + /** + * 根据来源ID查询(用于ID映射) + */ + default WbFeeItemDO selectBySourceId(Long compileTreeId, Long sourceId) { + return selectOne(new LambdaQueryWrapperX() + .eq(WbFeeItemDO::getCompileTreeId, compileTreeId) + .eq(WbFeeItemDO::getSourceId, sourceId)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbItemInfoMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbItemInfoMapper.java new file mode 100644 index 0000000..0e88bb4 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbItemInfoMapper.java @@ -0,0 +1,31 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.workbench.WbItemInfoDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 单项基本信息 Mapper + * + * @author yhy + */ +@Mapper +public interface WbItemInfoMapper extends BaseMapperX { + + /** + * 根据编制树节点ID查询 + */ + default WbItemInfoDO selectByCompileTreeId(Long compileTreeId) { + return selectOne(new LambdaQueryWrapperX() + .eq(WbItemInfoDO::getCompileTreeId, compileTreeId)); + } + + /** + * 根据编制树节点ID删除 + */ + default int deleteByCompileTreeId(Long compileTreeId) { + return delete(new LambdaQueryWrapperX() + .eq(WbItemInfoDO::getCompileTreeId, compileTreeId)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbProjectTreeMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbProjectTreeMapper.java new file mode 100644 index 0000000..935be23 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbProjectTreeMapper.java @@ -0,0 +1,82 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.workbench.WbProjectTreeDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 工作台项目管理树 Mapper + * + * @author yhy + */ +@Mapper +public interface WbProjectTreeMapper extends BaseMapperX { + + /** + * 根据父节点ID查询子节点列表 + */ + default List selectListByParentId(Long parentId) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(WbProjectTreeDO::getParentId, parentId) + .orderByAsc(WbProjectTreeDO::getSortOrder)); + } + + /** + * 查询所有节点(按排序) + */ + default List selectAllOrderBySortOrder() { + return selectList(new LambdaQueryWrapperX() + .orderByAsc(WbProjectTreeDO::getSortOrder)); + } + + /** + * 根据节点类型查询 + */ + default List selectListByNodeType(String nodeType) { + return selectList(new LambdaQueryWrapperX() + .eq(WbProjectTreeDO::getNodeType, nodeType) + .orderByAsc(WbProjectTreeDO::getSortOrder)); + } + + /** + * 根据项目编号查询 + */ + default WbProjectTreeDO selectByProjectCode(String projectCode) { + return selectOne(new LambdaQueryWrapperX() + .eq(WbProjectTreeDO::getProjectCode, projectCode) + .eq(WbProjectTreeDO::getNodeType, WbProjectTreeDO.NODE_TYPE_PROJECT)); + } + + /** + * 获取同级最大排序号 + */ + default Integer selectMaxSortOrderByParentId(Long parentId) { + WbProjectTreeDO maxSortNode = selectOne(new LambdaQueryWrapperX() + .eqIfPresent(WbProjectTreeDO::getParentId, parentId) + .orderByDesc(WbProjectTreeDO::getSortOrder) + .last("LIMIT 1")); + return maxSortNode != null ? maxSortNode.getSortOrder() : 0; + } + + /** + * 根据状态查询项目列表 + */ + default List selectProjectListByStatus(String status) { + return selectList(new LambdaQueryWrapperX() + .eq(WbProjectTreeDO::getNodeType, WbProjectTreeDO.NODE_TYPE_PROJECT) + .eqIfPresent(WbProjectTreeDO::getStatus, status) + .orderByDesc(WbProjectTreeDO::getCreateTime)); + } + + /** + * 查询同级节点中排序号大于等于指定值的节点 + */ + default List selectListByParentIdAndSortOrderGe(Long parentId, Integer sortOrder) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(WbProjectTreeDO::getParentId, parentId) + .ge(WbProjectTreeDO::getSortOrder, sortOrder) + .orderByAsc(WbProjectTreeDO::getSortOrder)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbRateFieldLabelMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbRateFieldLabelMapper.java new file mode 100644 index 0000000..3656d81 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbRateFieldLabelMapper.java @@ -0,0 +1,53 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.workbench.WbRateFieldLabelDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 工作台费率字段标签快照 Mapper + * + * @author yhy + */ +@Mapper +public interface WbRateFieldLabelMapper extends BaseMapperX { + + /** + * 根据单位工程ID查询字段标签 + */ + default List selectByCompileTreeId(Long compileTreeId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbRateFieldLabelDO::getCompileTreeId, compileTreeId) + .orderByAsc(WbRateFieldLabelDO::getSortOrder)); + } + + /** + * 根据单位工程ID和费率模式ID查询字段标签 + */ + default List selectByCompileTreeIdAndCatalogItemId(Long compileTreeId, Long catalogItemId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbRateFieldLabelDO::getCompileTreeId, compileTreeId) + .eq(WbRateFieldLabelDO::getCatalogItemId, catalogItemId) + .orderByAsc(WbRateFieldLabelDO::getSortOrder)); + } + + /** + * 根据单位工程ID删除 + */ + default int deleteByCompileTreeId(Long compileTreeId) { + return delete(new LambdaQueryWrapperX() + .eq(WbRateFieldLabelDO::getCompileTreeId, compileTreeId)); + } + + /** + * 根据单位工程ID和费率模式ID删除 + */ + default int deleteByCompileTreeIdAndCatalogItemId(Long compileTreeId, Long catalogItemId) { + return delete(new LambdaQueryWrapperX() + .eq(WbRateFieldLabelDO::getCompileTreeId, compileTreeId) + .eq(WbRateFieldLabelDO::getCatalogItemId, catalogItemId)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbRateItemMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbRateItemMapper.java new file mode 100644 index 0000000..82997df --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbRateItemMapper.java @@ -0,0 +1,82 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.workbench.WbRateItemDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 工作台-费率模板快照 Mapper + * + * @author yihuiyong + */ +@Mapper +public interface WbRateItemMapper extends BaseMapperX { + + /** + * 根据单位工程ID查询费率模板 + */ + default List selectByCompileTreeId(Long compileTreeId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbRateItemDO::getCompileTreeId, compileTreeId) + .orderByAsc(WbRateItemDO::getSortOrder)); + } + + /** + * 根据单位工程ID和费率模式ID查询费率模板 + */ + default List selectByCompileTreeIdAndCatalogItemId(Long compileTreeId, Long catalogItemId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbRateItemDO::getCompileTreeId, compileTreeId) + .eq(WbRateItemDO::getCatalogItemId, catalogItemId) + .orderByAsc(WbRateItemDO::getSortOrder)); + } + + /** + * 根据单位工程ID和父节点ID查询子节点 + */ + default List selectByParentId(Long compileTreeId, Long parentId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbRateItemDO::getCompileTreeId, compileTreeId) + .eq(WbRateItemDO::getParentId, parentId) + .orderByAsc(WbRateItemDO::getSortOrder)); + } + + /** + * 根据单位工程ID查询根节点 + */ + default List selectRootNodes(Long compileTreeId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbRateItemDO::getCompileTreeId, compileTreeId) + .isNull(WbRateItemDO::getParentId) + .orderByAsc(WbRateItemDO::getSortOrder)); + } + + /** + * 根据单位工程ID删除所有费率模板数据 + */ + default int deleteByCompileTreeId(Long compileTreeId) { + return delete(new LambdaQueryWrapperX() + .eq(WbRateItemDO::getCompileTreeId, compileTreeId)); + } + + /** + * 根据单位工程ID和费率模式ID删除费率模板数据 + */ + default int deleteByCompileTreeIdAndCatalogItemId(Long compileTreeId, Long catalogItemId) { + return delete(new LambdaQueryWrapperX() + .eq(WbRateItemDO::getCompileTreeId, compileTreeId) + .eq(WbRateItemDO::getCatalogItemId, catalogItemId)); + } + + /** + * 根据来源ID查询(用于ID映射) + */ + default WbRateItemDO selectBySourceId(Long compileTreeId, Long sourceId) { + return selectOne(new LambdaQueryWrapperX() + .eq(WbRateItemDO::getCompileTreeId, compileTreeId) + .eq(WbRateItemDO::getSourceId, sourceId)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbResourceSummaryMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbResourceSummaryMapper.java new file mode 100644 index 0000000..75493bf --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbResourceSummaryMapper.java @@ -0,0 +1,39 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import com.yhy.module.core.dal.dataobject.workbench.WbResourceSummaryDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +/** + * 工料机汇总 Mapper + * + * @author yhy + */ +@Mapper +public interface WbResourceSummaryMapper extends BaseMapperX { + + /** + * 根据项目ID和资源唯一键查询 + */ + default WbResourceSummaryDO selectByProjectIdAndResourceKey(Long projectId, String resourceKey) { + return selectOne(WbResourceSummaryDO::getProjectId, projectId, + WbResourceSummaryDO::getResourceKey, resourceKey); + } + + /** + * 根据项目ID查询所有汇总记录 + */ + default List selectListByProjectId(Long projectId) { + return selectList(WbResourceSummaryDO::getProjectId, projectId); + } + + /** + * 根据项目ID查询评标指定材料 + */ + default List selectBidMaterialsByProjectId(Long projectId) { + return selectList(WbResourceSummaryDO::getProjectId, projectId, + WbResourceSummaryDO::getIsBidMaterial, 1); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbUnifiedFeeConfigMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbUnifiedFeeConfigMapper.java new file mode 100644 index 0000000..57e280b --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbUnifiedFeeConfigMapper.java @@ -0,0 +1,67 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.workbench.WbUnifiedFeeConfigDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 工作台统一取费配置 Mapper + */ +@Mapper +public interface WbUnifiedFeeConfigMapper extends BaseMapperX { + + /** + * 根据单位工程节点ID查询配置列表 + */ + default List selectListByCompileTreeId(Long compileTreeId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbUnifiedFeeConfigDO::getCompileTreeId, compileTreeId) + .orderByAsc(WbUnifiedFeeConfigDO::getSortOrder)); + } + + /** + * 根据分部分项节点ID查询配置 + */ + default WbUnifiedFeeConfigDO selectByDivisionId(Long divisionId) { + return selectOne(new LambdaQueryWrapperX() + .eq(WbUnifiedFeeConfigDO::getDivisionId, divisionId)); + } + + /** + * 根据费率模式ID删除配置(费率切换时清空) + */ + default int deleteByRateModeId(Long compileTreeId, Long rateModeId) { + return delete(new LambdaQueryWrapperX() + .eq(WbUnifiedFeeConfigDO::getCompileTreeId, compileTreeId) + .eq(WbUnifiedFeeConfigDO::getRateModeId, rateModeId)); + } + + /** + * 根据单位工程节点ID删除所有配置 + */ + default int deleteByCompileTreeId(Long compileTreeId) { + return delete(new LambdaQueryWrapperX() + .eq(WbUnifiedFeeConfigDO::getCompileTreeId, compileTreeId)); + } + + /** + * 根据单位工程节点ID和费率模式ID查询配置列表 + */ + default List selectListByCompileTreeIdAndRateModeId(Long compileTreeId, Long rateModeId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbUnifiedFeeConfigDO::getCompileTreeId, compileTreeId) + .eq(WbUnifiedFeeConfigDO::getRateModeId, rateModeId) + .orderByAsc(WbUnifiedFeeConfigDO::getSortOrder)); + } + + /** + * 删除单位工程下不属于指定费率模式的配置(费率切换时清空其他费率模式的配置) + */ + default int deleteByCompileTreeIdExcludeRateModeId(Long compileTreeId, Long rateModeId) { + return delete(new LambdaQueryWrapperX() + .eq(WbUnifiedFeeConfigDO::getCompileTreeId, compileTreeId) + .ne(WbUnifiedFeeConfigDO::getRateModeId, rateModeId)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbUnifiedFeeResourceMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbUnifiedFeeResourceMapper.java new file mode 100644 index 0000000..e8f65f7 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbUnifiedFeeResourceMapper.java @@ -0,0 +1,52 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.workbench.WbUnifiedFeeResourceDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 工作台-统一取费子目工料机快照 Mapper + * + * @author yihuiyong + */ +@Mapper +public interface WbUnifiedFeeResourceMapper extends BaseMapperX { + + /** + * 根据单位工程ID查询所有子目工料机 + */ + default List selectByCompileTreeId(Long compileTreeId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbUnifiedFeeResourceDO::getCompileTreeId, compileTreeId) + .orderByAsc(WbUnifiedFeeResourceDO::getSortOrder)); + } + + /** + * 根据统一取费设置ID查询子目工料机 + */ + default List selectByUnifiedFeeSettingId(Long unifiedFeeSettingId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbUnifiedFeeResourceDO::getUnifiedFeeSettingId, unifiedFeeSettingId) + .orderByAsc(WbUnifiedFeeResourceDO::getSortOrder)); + } + + /** + * 根据单位工程ID删除所有子目工料机数据 + */ + default int deleteByCompileTreeId(Long compileTreeId) { + return delete(new LambdaQueryWrapperX() + .eq(WbUnifiedFeeResourceDO::getCompileTreeId, compileTreeId)); + } + + /** + * 根据来源ID查询(用于ID映射) + */ + default WbUnifiedFeeResourceDO selectBySourceId(Long compileTreeId, Long sourceId) { + return selectOne(new LambdaQueryWrapperX() + .eq(WbUnifiedFeeResourceDO::getCompileTreeId, compileTreeId) + .eq(WbUnifiedFeeResourceDO::getSourceId, sourceId)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbUnifiedFeeSettingMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbUnifiedFeeSettingMapper.java new file mode 100644 index 0000000..dd62715 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbUnifiedFeeSettingMapper.java @@ -0,0 +1,91 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.workbench.WbUnifiedFeeSettingDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 工作台-统一取费设置快照 Mapper + * + * @author yihuiyong + */ +@Mapper +public interface WbUnifiedFeeSettingMapper extends BaseMapperX { + + /** + * 根据单位工程ID查询统一取费设置 + */ + default List selectByCompileTreeId(Long compileTreeId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbUnifiedFeeSettingDO::getCompileTreeId, compileTreeId) + .orderByAsc(WbUnifiedFeeSettingDO::getSortOrder)); + } + + /** + * 根据单位工程ID和父节点ID查询子节点 + */ + default List selectByParentId(Long compileTreeId, Long parentId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbUnifiedFeeSettingDO::getCompileTreeId, compileTreeId) + .eq(WbUnifiedFeeSettingDO::getParentId, parentId) + .orderByAsc(WbUnifiedFeeSettingDO::getSortOrder)); + } + + /** + * 根据单位工程ID查询根节点(目录节点) + */ + default List selectRootNodes(Long compileTreeId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbUnifiedFeeSettingDO::getCompileTreeId, compileTreeId) + .isNull(WbUnifiedFeeSettingDO::getParentId) + .orderByAsc(WbUnifiedFeeSettingDO::getSortOrder)); + } + + /** + * 根据单位工程ID和节点类型查询 + */ + default List selectByNodeType(Long compileTreeId, String nodeType) { + return selectList(new LambdaQueryWrapperX() + .eq(WbUnifiedFeeSettingDO::getCompileTreeId, compileTreeId) + .eq(WbUnifiedFeeSettingDO::getNodeType, nodeType) + .orderByAsc(WbUnifiedFeeSettingDO::getSortOrder)); + } + + /** + * 根据单位工程ID删除所有统一取费设置数据 + */ + default int deleteByCompileTreeId(Long compileTreeId) { + return delete(new LambdaQueryWrapperX() + .eq(WbUnifiedFeeSettingDO::getCompileTreeId, compileTreeId)); + } + + /** + * 根据单位工程ID和费率模式ID删除统一取费设置数据 + */ + default int deleteByCompileTreeIdAndCatalogItemId(Long compileTreeId, Long catalogItemId) { + return delete(new LambdaQueryWrapperX() + .eq(WbUnifiedFeeSettingDO::getCompileTreeId, compileTreeId) + .eq(WbUnifiedFeeSettingDO::getCatalogItemId, catalogItemId)); + } + + /** + * 根据单位工程ID和catalogItemId(费率模式ID)查询统一取费设置 + */ + default List selectByCompileTreeIdAndCatalogItemId(Long compileTreeId, Long catalogItemId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbUnifiedFeeSettingDO::getCompileTreeId, compileTreeId) + .eq(WbUnifiedFeeSettingDO::getCatalogItemId, catalogItemId) + .orderByAsc(WbUnifiedFeeSettingDO::getSortOrder)); + } + + /** + * 根据来源ID查询(用于ID映射) + */ + default WbUnifiedFeeSettingDO selectBySourceId(Long compileTreeId, Long sourceId) { + return selectOne(new LambdaQueryWrapperX() + .eq(WbUnifiedFeeSettingDO::getCompileTreeId, compileTreeId) + .eq(WbUnifiedFeeSettingDO::getSourceId, sourceId)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbUnitFeeSettingMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbUnitFeeSettingMapper.java new file mode 100644 index 0000000..48dc2de --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbUnitFeeSettingMapper.java @@ -0,0 +1,42 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import com.yhy.module.core.dal.dataobject.workbench.WbUnitFeeSettingDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 单位工程取费项设定 Mapper + */ +@Mapper +public interface WbUnitFeeSettingMapper extends BaseMapperX { + + /** + * 根据分部分项节点ID查询取费项设定列表 + */ + default List selectByDivisionId(Long divisionId) { + return selectList(WbUnitFeeSettingDO::getDivisionId, divisionId); + } + + /** + * 根据分部分项节点ID和取费项ID查询 + */ + default WbUnitFeeSettingDO selectByDivisionIdAndFeeItemId(Long divisionId, Long feeItemId) { + return selectOne(WbUnitFeeSettingDO::getDivisionId, divisionId, + WbUnitFeeSettingDO::getSourceFeeItemId, feeItemId); + } + + /** + * 根据分部分项节点ID删除所有取费项设定 + */ + default int deleteByDivisionId(Long divisionId) { + return delete(WbUnitFeeSettingDO::getDivisionId, divisionId); + } + + /** + * 根据单位工程ID查询取费项设定列表 + */ + default List selectByUnitId(Long unitId) { + return selectList(WbUnitFeeSettingDO::getUnitId, unitId); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbUnitInfoMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbUnitInfoMapper.java new file mode 100644 index 0000000..3bc2106 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbUnitInfoMapper.java @@ -0,0 +1,61 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.workbench.WbUnitInfoDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 单位工程信息 Mapper + * + * @author yhy + */ +@Mapper +public interface WbUnitInfoMapper extends BaseMapperX { + + /** + * 根据编制树节点ID查询 + */ + default WbUnitInfoDO selectByCompileTreeId(Long compileTreeId) { + return selectOne(new LambdaQueryWrapperX() + .eq(WbUnitInfoDO::getCompileTreeId, compileTreeId)); + } + + /** + * 根据编制树节点ID删除 + */ + default int deleteByCompileTreeId(Long compileTreeId) { + return delete(new LambdaQueryWrapperX() + .eq(WbUnitInfoDO::getCompileTreeId, compileTreeId)); + } + + /** + * 根据项目ID和工程编号查询(用于唯一性校验) + */ + default WbUnitInfoDO selectByProjectIdAndUnitCode(Long projectId, String unitCode) { + // 需要通过编制树关联查询,这里简化处理 + return null; + } + + /** + * 根据编制树节点ID列表查询 + */ + default java.util.List selectListByCompileTreeIds(java.util.List compileTreeIds) { + if (compileTreeIds == null || compileTreeIds.isEmpty()) { + return java.util.Collections.emptyList(); + } + return selectList(new LambdaQueryWrapperX() + .in(WbUnitInfoDO::getCompileTreeId, compileTreeIds)); + } + + /** + * 根据库类别查询编制树节点ID列表 + */ + default java.util.List selectCompileTreeIdsByLibraryType(String libraryType) { + java.util.List list = selectList(new LambdaQueryWrapperX() + .eq(WbUnitInfoDO::getLibraryType, libraryType)); + return list.stream() + .map(WbUnitInfoDO::getCompileTreeId) + .collect(java.util.stream.Collectors.toList()); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbUnitRateSettingMapper.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbUnitRateSettingMapper.java new file mode 100644 index 0000000..625fdc8 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/mysql/workbench/WbUnitRateSettingMapper.java @@ -0,0 +1,39 @@ +package com.yhy.module.core.dal.mysql.workbench; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.dal.dataobject.workbench.WbUnitRateSettingDO; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +/** + * 工程费率设定 Mapper + */ +@Mapper +public interface WbUnitRateSettingMapper extends BaseMapperX { + + /** + * 根据单位工程ID查询费率设定列表 + */ + default List selectByUnitId(Long unitId) { + return selectList(new LambdaQueryWrapperX() + .eq(WbUnitRateSettingDO::getUnitId, unitId)); + } + + /** + * 根据单位工程ID和费率项ID查询费率设定 + */ + default WbUnitRateSettingDO selectByUnitIdAndRateItemId(Long unitId, Long rateItemId) { + return selectOne(new LambdaQueryWrapperX() + .eq(WbUnitRateSettingDO::getUnitId, unitId) + .eq(WbUnitRateSettingDO::getRateItemId, rateItemId)); + } + + /** + * 删除单位工程的所有费率设定 + */ + default int deleteByUnitId(Long unitId) { + return delete(new LambdaQueryWrapperX() + .eq(WbUnitRateSettingDO::getUnitId, unitId)); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/dal/typehandler/PostgreSQLJsonbTypeHandler.java b/yhy-module-core/src/main/java/com/yhy/module/core/dal/typehandler/PostgreSQLJsonbTypeHandler.java index f989cb9..f01a21a 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/dal/typehandler/PostgreSQLJsonbTypeHandler.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/dal/typehandler/PostgreSQLJsonbTypeHandler.java @@ -1,17 +1,17 @@ package com.yhy.module.core.dal.typehandler; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.ibatis.type.BaseTypeHandler; -import org.apache.ibatis.type.JdbcType; -import org.apache.ibatis.type.MappedTypes; -import org.postgresql.util.PGobject; - import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Map; +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedTypes; +import org.postgresql.util.PGobject; /** * PostgreSQL jsonb 类型处理器 @@ -19,7 +19,9 @@ import java.util.Map; @MappedTypes({Map.class}) public class PostgreSQLJsonbTypeHandler extends BaseTypeHandler> { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() + // 启用长整数作为Long类型读取(而不是Integer),避免溢出 + .enable(DeserializationFeature.USE_LONG_FOR_INTS); @Override public void setNonNullParameter(PreparedStatement ps, int i, Map parameter, JdbcType jdbcType) throws SQLException { diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/enums/ErrorCodeConstants.java b/yhy-module-core/src/main/java/com/yhy/module/core/enums/ErrorCodeConstants.java index 3ac1614..2ca6355 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/enums/ErrorCodeConstants.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/enums/ErrorCodeConstants.java @@ -27,7 +27,7 @@ public interface ErrorCodeConstants { // ========== 定额目录模块 1-010-005-000 ========== ErrorCode QUOTA_CATALOG_ITEM_NOT_EXISTS = new ErrorCode(1_010_005_000, "定额目录条目不存在"); - ErrorCode QUOTA_CATALOG_ITEM_HAS_CHILDREN = new ErrorCode(1_010_005_001, "该节点存在子节点,无法删除"); + ErrorCode QUOTA_CATALOG_ITEM_HAS_CHILDREN = new ErrorCode(1_010_005_001, "{}"); ErrorCode QUOTA_CATALOG_ITEM_NOT_SPECIALTY = new ErrorCode(1_010_005_002, "该节点不是定额专业节点"); ErrorCode QUOTA_CATALOG_ITEM_NOT_SAME_LEVEL = new ErrorCode(1_010_005_003, "两个节点不在同一层级,无法交换排序"); ErrorCode QUOTA_CATALOG_ITEM_ALREADY_BOUND = new ErrorCode(1_010_005_004, "该定额专业已绑定工料机专业,无法重复绑定"); @@ -38,17 +38,18 @@ public interface ErrorCodeConstants { ErrorCode QUOTA_CATALOG_ITEM_PARENT_NOT_EXISTS = new ErrorCode(1_010_005_009, "父节点不存在"); ErrorCode QUOTA_CATALOG_ITEM_SPECIALTY_NOT_FOUND = new ErrorCode(1_010_005_010, "未找到定额专业节点"); - // ========== 定额子目树模块 1-010-006-000 ========== - ErrorCode QUOTA_CATALOG_TREE_NOT_EXISTS = new ErrorCode(1_010_006_000, "定额子目树节点不存在"); + // ========== 定额基价树模块 1-010-006-000 ========== + ErrorCode QUOTA_CATALOG_TREE_NOT_EXISTS = new ErrorCode(1_010_006_000, "定额基价树节点不存在"); ErrorCode QUOTA_CATALOG_TREE_HAS_CHILDREN = new ErrorCode(1_010_006_001, "该节点存在子节点,无法删除"); ErrorCode QUOTA_CATALOG_TREE_PARENT_NOT_EXISTS = new ErrorCode(1_010_006_002, "父节点不存在"); ErrorCode QUOTA_CATALOG_TREE_PARENT_NOT_ALLOW_CHILDREN = new ErrorCode(1_010_006_003, "父节点不允许添加子节点"); ErrorCode QUOTA_CATALOG_TREE_NOT_SAME_LEVEL = new ErrorCode(1_010_006_004, "两个节点不在同一层级"); - // ========== 定额子目模块 1-010-007-000 ========== - ErrorCode QUOTA_ITEM_NOT_EXISTS = new ErrorCode(1_010_007_000, "定额子目不存在"); - ErrorCode QUOTA_ITEM_HAS_RESOURCES = new ErrorCode(1_010_007_001, "该定额子目存在工料机组成,无法删除"); - ErrorCode QUOTA_CATALOG_ITEM_NOT_CONTENT = new ErrorCode(1_010_007_002, "只有内容节点才能添加定额子目"); + // ========== 定额基价模块 1-010-007-000 ========== + ErrorCode QUOTA_ITEM_NOT_EXISTS = new ErrorCode(1_010_007_000, "定额基价不存在"); + ErrorCode QUOTA_ITEM_HAS_RESOURCES = new ErrorCode(1_010_007_001, "该定额基价存在工料机组成,请先删除工料机后再删除定额"); + ErrorCode QUOTA_ITEM_HAS_MARKET_MATERIALS = new ErrorCode(1_010_007_004, "该定额基价存在市场主材设备,请先删除市场主材后再删除定额"); + ErrorCode QUOTA_CATALOG_ITEM_NOT_CONTENT = new ErrorCode(1_010_007_002, "只有内容节点才能添加定额基价"); ErrorCode QUOTA_SPECIALTY_NOT_BOUND = new ErrorCode(1_010_007_003, "定额专业尚未绑定工料机专业"); ErrorCode QUOTA_SPECIALTY_NOT_FOUND = new ErrorCode(1_010_007_004, "未找到定额专业节点"); @@ -56,6 +57,10 @@ public interface ErrorCodeConstants { ErrorCode QUOTA_RESOURCE_NOT_EXISTS = new ErrorCode(1_010_008_000, "定额工料机组成不存在"); ErrorCode QUOTA_RESOURCE_OUT_OF_SCOPE = new ErrorCode(1_010_008_001, "该工料机不在定额专业的数据范围内"); ErrorCode RESOURCE_CATALOG_ITEM_NOT_EXISTS = new ErrorCode(1_010_007_002, "工料机目录条目不存在"); + ErrorCode QUOTA_RESOURCE_PERCENT_UNIT_DOSAGE_READONLY = new ErrorCode(1_010_008_002, "单位为%的工料机不允许修改定额消耗量,因为比例需要保持不变"); + + // ========== 定额市场主材设备模块 1-010-009-000 ========== + ErrorCode QUOTA_MARKET_MATERIAL_NOT_EXISTS = new ErrorCode(1_010_009_000, "定额市场主材设备不存在"); // ========== 定额费率项模块 1-010-009-000 ========== ErrorCode QUOTA_RATE_ITEM_NOT_EXISTS = new ErrorCode(1_010_009_000, "定额费率项不存在"); @@ -78,7 +83,8 @@ public interface ErrorCodeConstants { ErrorCode QUOTA_RATE_TIER_COMPARE_TYPE_INVALID = new ErrorCode(1_010_009_022, "阶梯规则 [{}] 的比较类型无效"); ErrorCode QUOTA_RATE_TIER_FIELD_VALUES_EMPTY = new ErrorCode(1_010_009_023, "阶梯规则 [{}] 的字段值不能为空"); ErrorCode QUOTA_RATE_TIER_THRESHOLD_NOT_ASCENDING = new ErrorCode(1_010_009_024, "阶梯规则 [{}] 和 [{}] 的阈值必须递增"); - ErrorCode QUOTA_RATE_TIER_COMPARE_TYPE_MIXED = new ErrorCode(1_010_009_025, "阶梯规则 [{}] 和 [{}] 不能混用不同的比较类型(小于等于/小于 与 大于等于/大于)"); + ErrorCode QUOTA_RATE_TIER_COMPARE_TYPE_MIXED = new ErrorCode(1_010_009_025, "阶梯规则 [{}] 和 [{}] 不能混用不同的比较类型(小于等于/小于 与 每增加)"); + ErrorCode QUOTA_RATE_TIER_PER_INCREMENT_ONLY_ONE = new ErrorCode(1_010_009_026, "每增加类型只允许配置一条规则"); // 增量规则验证错误码 ErrorCode QUOTA_RATE_INCREMENT_SEQ_DUPLICATE = new ErrorCode(1_010_009_030, "增量规则序号 [{}] 重复"); @@ -108,16 +114,24 @@ public interface ErrorCodeConstants { ErrorCode QUOTA_FEE_CALC_BASE_INVALID_PRICE_CODE = new ErrorCode(1_010_010_006, "无效的价格代码: {}"); ErrorCode QUOTA_FEE_CALC_BASE_FORMULA_BRACKET_MISMATCH = new ErrorCode(1_010_010_007, "公式括号不匹配"); ErrorCode QUOTA_FEE_CALC_BASE_FORMULA_INVALID_CHAR = new ErrorCode(1_010_010_008, "公式包含非法字符: {}"); + ErrorCode QUOTA_FEE_ITEM_RATE_ITEM_NOT_DIRECTORY = new ErrorCode(1_010_011_009, "取费项只能关联目录类型的费率项"); + ErrorCode QUOTA_FEE_ITEM_SYSTEM_ROW_CANNOT_DELETE = new ErrorCode(1_010_011_010, "系统行不可删除"); + ErrorCode QUOTA_FEE_ITEM_SYSTEM_ROW_CANNOT_MODIFY_CODE = new ErrorCode(1_010_011_011, "系统行的代号不可修改"); + ErrorCode QUOTA_FEE_ITEM_SYSTEM_ROW_CANNOT_SWAP = new ErrorCode(1_010_011_012, "系统行不可参与排序交换"); + ErrorCode QUOTA_FEE_ITEM_CODE_DUPLICATE = new ErrorCode(1_010_011_013, "代号 [{}] 已存在"); + // ========== 定额调整设置模块(第五层)1-010-011-000 ========== ErrorCode QUOTA_ADJUSTMENT_SETTING_NOT_EXISTS = new ErrorCode(1_010_011_000, "定额调整设置不存在"); - ErrorCode QUOTA_ADJUSTMENT_SETTING_QUOTA_ITEM_NOT_EXISTS = new ErrorCode(1_010_011_001, "定额子目不存在"); + ErrorCode QUOTA_ADJUSTMENT_SETTING_QUOTA_ITEM_NOT_EXISTS = new ErrorCode(1_010_011_001, "定额基价不存在"); ErrorCode QUOTA_ADJUSTMENT_SETTING_NAME_DUPLICATE = new ErrorCode(1_010_011_002, "调整设置名称重复"); ErrorCode QUOTA_ADJUSTMENT_SETTING_TYPE_INVALID = new ErrorCode(1_010_011_003, "调整类型无效"); ErrorCode QUOTA_ADJUSTMENT_SETTING_RULES_INVALID = new ErrorCode(1_010_011_004, "调整规则格式无效"); ErrorCode QUOTA_ADJUSTMENT_SETTING_HAS_DETAILS = new ErrorCode(1_010_011_005, "调整设置下存在明细,无法删除"); ErrorCode QUOTA_ADJUSTMENT_SETTING_REFERENCED = new ErrorCode(1_010_011_006, "调整设置被其他明细引用,无法删除"); - ErrorCode QUOTA_ADJUSTMENT_SETTING_NOT_SAME_QUOTA_ITEM = new ErrorCode(1_010_011_007, "只能在同一定额子目下交换排序"); + ErrorCode QUOTA_ADJUSTMENT_SETTING_NOT_SAME_QUOTA_ITEM = new ErrorCode(1_010_011_007, "只能在同一定额基价下交换排序"); + ErrorCode QUOTA_ADJUSTMENT_SETTING_NOT_MATCH = new ErrorCode(1_010_011_008, "调整设置不属于该定额基价"); + ErrorCode QUOTA_ADJUSTMENT_INPUT_OUT_OF_RANGE = new ErrorCode(1_010_011_009, "类别 {0} 的输入值必须在 {1} 到 {2} 之间,当前值:{3}"); // ========== 定额调整明细模块(第六层)1-010-012-000 ========== ErrorCode QUOTA_ADJUSTMENT_DETAIL_NOT_EXISTS = new ErrorCode(1_010_012_000, "定额调整明细不存在"); @@ -139,6 +153,8 @@ public interface ErrorCodeConstants { ErrorCode BOQ_CATALOG_ITEM_PARENT_NOT_EXISTS = new ErrorCode(1_010_013_008, "父节点不存在"); ErrorCode BOQ_CATALOG_ITEM_NOT_SAME_LEVEL = new ErrorCode(1_010_013_009, "只能在同级节点间交换排序"); ErrorCode BOQ_CATALOG_ITEM_NOT_BOUND_QUOTA = new ErrorCode(1_010_013_010, "该清单专业尚未绑定定额基价"); + ErrorCode BOQ_CATALOG_ITEM_ROOT_EXISTS = new ErrorCode(1_010_013_011, "根节点已存在,只允许创建一个根节点"); + ErrorCode BOQ_CATALOG_ITEM_QUOTA_NOT_SPECIALTY = new ErrorCode(1_010_013_012, "只能绑定定额专业节点(specialty类型),不能绑定地区节点(region类型)"); // ========== 清单项树模块 1-010-014-000 ========== ErrorCode BOQ_ITEM_TREE_NOT_EXISTS = new ErrorCode(1_010_014_000, "清单项树节点不存在"); @@ -152,17 +168,18 @@ public interface ErrorCodeConstants { ErrorCode BOQ_SUB_ITEM_CODE_DUPLICATE = new ErrorCode(1_010_015_001, "编码 [{}] 已存在"); ErrorCode BOQ_SUB_ITEM_NOT_SAME_TREE = new ErrorCode(1_010_015_002, "只能在同一清单项树下交换排序"); - // ========== 清单明细树模块 1-010-016-000 ========== - ErrorCode BOQ_DETAIL_TREE_NOT_EXISTS = new ErrorCode(1_010_016_000, "清单明细树节点不存在"); - ErrorCode BOQ_DETAIL_TREE_CODE_DUPLICATE = new ErrorCode(1_010_016_001, "编码 [{}] 已存在"); - ErrorCode BOQ_DETAIL_TREE_HAS_CHILDREN = new ErrorCode(1_010_016_002, "该节点存在子节点,无法删除"); - ErrorCode BOQ_DETAIL_TREE_PARENT_NOT_DIRECTORY = new ErrorCode(1_010_016_003, "父节点必须是目录类型"); - ErrorCode BOQ_DETAIL_TREE_INVALID_NODE_TYPE = new ErrorCode(1_010_016_004, "无效的节点类型"); - ErrorCode BOQ_DETAIL_TREE_CONTENT_NEED_QUOTA = new ErrorCode(1_010_016_005, "内容节点必须关联定额"); - ErrorCode BOQ_DETAIL_TREE_DIRECTORY_NO_QUOTA = new ErrorCode(1_010_016_006, "目录节点不能关联定额"); - ErrorCode BOQ_DETAIL_TREE_QUOTA_NOT_IN_RANGE = new ErrorCode(1_010_016_007, "该定额不在绑定的定额专业范围内"); - ErrorCode BOQ_DETAIL_TREE_QUOTA_NOT_CONTENT_TYPE = new ErrorCode(1_010_016_008, "只能关联定额基价第三层的内容节点"); - ErrorCode BOQ_DETAIL_TREE_NOT_SAME_LEVEL = new ErrorCode(1_010_016_009, "只能在同级节点间交换排序"); + // ========== 清单指引树模块 1-010-016-000 ========== + ErrorCode BOQ_GUIDE_TREE_NOT_EXISTS = new ErrorCode(1_010_016_000, "清单指引树节点不存在"); + ErrorCode BOQ_GUIDE_TREE_CODE_DUPLICATE = new ErrorCode(1_010_016_001, "编码 [{}] 已存在"); + ErrorCode BOQ_GUIDE_TREE_HAS_CHILDREN = new ErrorCode(1_010_016_002, "该节点存在子节点,无法删除"); + ErrorCode BOQ_GUIDE_TREE_PARENT_NOT_DIRECTORY = new ErrorCode(1_010_016_003, "父节点必须是目录类型"); + ErrorCode BOQ_GUIDE_TREE_INVALID_NODE_TYPE = new ErrorCode(1_010_016_004, "无效的节点类型,只能是directory或quota"); + ErrorCode BOQ_GUIDE_TREE_QUOTA_NEED_QUOTA = new ErrorCode(1_010_016_005, "定额节点必须关联定额"); + ErrorCode BOQ_GUIDE_TREE_DIRECTORY_NO_QUOTA = new ErrorCode(1_010_016_006, "目录节点不能关联定额"); + ErrorCode BOQ_GUIDE_TREE_QUOTA_NOT_IN_RANGE = new ErrorCode(1_010_016_007, "该定额不在绑定的定额专业范围内"); + ErrorCode BOQ_GUIDE_TREE_QUOTA_NOT_CONTENT_TYPE = new ErrorCode(1_010_016_008, "只能关联定额基价第三层的内容节点"); + ErrorCode BOQ_GUIDE_TREE_NOT_SAME_LEVEL = new ErrorCode(1_010_016_009, "只能在同级节点间交换排序"); + ErrorCode BOQ_GUIDE_TREE_QUOTA_NEED_PARENT = new ErrorCode(1_010_016_010, "定额节点必须在目录下创建,不能作为根节点"); // ========== 信息价树结构模块 1-010-017-000 ========== ErrorCode INFO_PRICE_TREE_NOT_EXISTS = new ErrorCode(1_010_017_000, "信息价树节点不存在"); @@ -211,5 +228,211 @@ public interface ErrorCodeConstants { ErrorCode RESOURCE_MERGED_CALC_BASE_CATEGORY_NOT_FOUND = new ErrorCode(1_010_021_003, "计算基数引用的类别在同组中未找到"); ErrorCode RESOURCE_MERGED_FORMULA_PARSE_ERROR = new ErrorCode(1_010_021_004, "公式解析失败"); ErrorCode RESOURCE_MERGED_SELF_REFERENCE = new ErrorCode(1_010_021_005, "不允许自己关联自己"); + ErrorCode RESOURCE_MERGED_PERCENT_UNIT_NOT_ALLOWED = new ErrorCode(1_010_021_006, "单位为 % 的工料机不能成为复合工料机"); + ErrorCode RESOURCE_MERGED_CHILD_NOT_EDITABLE = new ErrorCode(1_010_021_007, "复合工料机的子工料机不允许编辑"); + + // ========== 项目界面配置树模块 1-010-022-000 ========== + ErrorCode CONFIG_PROJECT_TREE_NOT_EXISTS = new ErrorCode(1_010_022_000, "项目界面配置树节点不存在"); + ErrorCode CONFIG_PROJECT_TREE_CODE_EXISTS = new ErrorCode(1_010_022_001, "编码已存在"); + ErrorCode CONFIG_PROJECT_TREE_HAS_CHILDREN = new ErrorCode(1_010_022_002, "该节点存在子节点,无法删除"); + ErrorCode CONFIG_PROJECT_TREE_NOT_SAME_LEVEL = new ErrorCode(1_010_022_003, "只能在同级节点间交换排序"); + ErrorCode CONFIG_PROJECT_TREE_NODE_TYPE_CANNOT_CHANGE = new ErrorCode(1_010_022_004, "不允许修改节点类型"); + ErrorCode CONFIG_PROJECT_TREE_ROOT_PARENT_MUST_NULL = new ErrorCode(1_010_022_005, "根节点的父节点必须为空"); + ErrorCode CONFIG_PROJECT_TREE_ROOT_ONLY_ONE = new ErrorCode(1_010_022_006, "只能有一个根节点"); + ErrorCode CONFIG_PROJECT_TREE_PROVINCE_PARENT_REQUIRED = new ErrorCode(1_010_022_007, "省市节点必须有父节点"); + ErrorCode CONFIG_PROJECT_TREE_PARENT_NOT_EXISTS = new ErrorCode(1_010_022_008, "父节点不存在"); + ErrorCode CONFIG_PROJECT_TREE_PROVINCE_PARENT_INVALID = new ErrorCode(1_010_022_009, "省市节点的父节点必须是根节点或省市节点"); + ErrorCode CONFIG_PROJECT_TREE_INDUSTRY_PARENT_REQUIRED = new ErrorCode(1_010_022_010, "行业节点必须有父节点"); + ErrorCode CONFIG_PROJECT_TREE_INDUSTRY_PARENT_INVALID = new ErrorCode(1_010_022_011, "行业节点的父节点必须是省市节点"); + ErrorCode CONFIG_PROJECT_TREE_NODE_TYPE_INVALID = new ErrorCode(1_010_022_012, "无效的节点类型"); + ErrorCode CONFIG_PROJECT_TREE_HAS_INFO = new ErrorCode(1_010_022_013, "该节点存在工程信息配置,无法删除"); + + // ========== 工程信息配置模块 1-010-023-000 ========== + ErrorCode CONFIG_PROJECT_INFO_NOT_EXISTS = new ErrorCode(1_010_023_000, "工程信息配置不存在"); + ErrorCode CONFIG_PROJECT_INFO_HAS_CHILDREN = new ErrorCode(1_010_023_001, "该节点存在子节点,无法删除"); + ErrorCode CONFIG_PROJECT_INFO_NOT_SAME_LEVEL = new ErrorCode(1_010_023_002, "只能在同级节点间交换排序"); + ErrorCode CONFIG_PROJECT_INFO_PARENT_NOT_EXISTS = new ErrorCode(1_010_023_003, "父节点不存在"); + ErrorCode CONFIG_PROJECT_INFO_TREE_NOT_INDUSTRY = new ErrorCode(1_010_023_004, "只能在行业节点下添加工程信息"); + + // ========== 工作台项目管理树模块 1-010-024-000 ========== + ErrorCode WB_PROJECT_NOT_EXISTS = new ErrorCode(1_010_024_000, "工作台项目节点不存在"); + ErrorCode WB_PROJECT_CODE_EXISTS = new ErrorCode(1_010_024_001, "项目编号已存在"); + ErrorCode WB_PROJECT_INDUSTRY_NOT_EXISTS = new ErrorCode(1_010_024_002, "行业配置不存在"); + ErrorCode WB_PROJECT_INFO_PRICE_BOOK_NOT_EXISTS = new ErrorCode(1_010_024_003, "信息价册不存在"); + ErrorCode WB_PROJECT_HAS_CHILDREN = new ErrorCode(1_010_024_004, "该节点存在子节点,无法删除"); + ErrorCode WB_PROJECT_NOT_SAME_LEVEL = new ErrorCode(1_010_024_005, "只能在同级节点间交换排序"); + ErrorCode WB_PROJECT_NODE_TYPE_CANNOT_CHANGE = new ErrorCode(1_010_024_006, "不允许修改节点类型"); + ErrorCode WB_PROJECT_NODE_TYPE_INVALID = new ErrorCode(1_010_024_007, "无效的节点类型"); + ErrorCode WB_PROJECT_PARENT_NOT_EXISTS = new ErrorCode(1_010_024_008, "父节点不存在"); + ErrorCode WB_PROJECT_PARENT_NOT_DIRECTORY = new ErrorCode(1_010_024_009, "只有目录节点才能添加子节点"); + ErrorCode WB_PROJECT_NOT_PROJECT_NODE = new ErrorCode(1_010_024_010, "该节点不是项目节点"); + + // ========== 编制模式树模块 1-010-025-000 ========== + ErrorCode WB_COMPILE_TREE_NOT_EXISTS = new ErrorCode(1_010_025_000, "编制模式树节点不存在"); + ErrorCode WB_COMPILE_TREE_HAS_CHILDREN = new ErrorCode(1_010_025_001, "该节点存在子节点,无法删除"); + ErrorCode WB_COMPILE_TREE_NOT_SAME_LEVEL = new ErrorCode(1_010_025_002, "只能在同级节点间交换排序"); + ErrorCode WB_COMPILE_TREE_NODE_TYPE_INVALID = new ErrorCode(1_010_025_003, "无效的节点类型"); + ErrorCode WB_COMPILE_TREE_PARENT_NOT_EXISTS = new ErrorCode(1_010_025_004, "父节点不存在"); + ErrorCode WB_COMPILE_TREE_ITEM_PARENT_MUST_ROOT = new ErrorCode(1_010_025_005, "单项只能在根节点下创建"); + ErrorCode WB_COMPILE_TREE_UNIT_PARENT_MUST_ITEM = new ErrorCode(1_010_025_006, "单位工程只能在单项下创建"); + ErrorCode WB_COMPILE_TREE_ROOT_EXISTS = new ErrorCode(1_010_025_007, "该项目已存在根节点"); + ErrorCode WB_COMPILE_TREE_NODE_TYPE_CANNOT_CHANGE = new ErrorCode(1_010_025_008, "不允许修改节点类型"); + + // ========== 单项基本信息模块 1-010-026-000 ========== + ErrorCode WB_ITEM_INFO_NOT_EXISTS = new ErrorCode(1_010_026_000, "单项基本信息不存在"); + ErrorCode WB_ITEM_INFO_NODE_NOT_ITEM = new ErrorCode(1_010_026_001, "该节点不是单项节点"); + + // ========== 单位工程信息模块 1-010-027-000 ========== + ErrorCode WB_UNIT_INFO_NOT_EXISTS = new ErrorCode(1_010_027_000, "单位工程信息不存在"); + ErrorCode WB_UNIT_INFO_NODE_NOT_UNIT = new ErrorCode(1_010_027_001, "该节点不是单位工程节点"); + ErrorCode WB_UNIT_INFO_CODE_EXISTS = new ErrorCode(1_010_027_002, "工程编号在该项目中已存在"); + + // ========== 分部分项树模块 1-010-028-000 ========== + ErrorCode WB_BOQ_DIVISION_NOT_EXISTS = new ErrorCode(1_010_028_000, "分部分项节点不存在"); + ErrorCode WB_BOQ_DIVISION_HAS_CHILDREN = new ErrorCode(1_010_028_001, "该节点存在子节点,无法删除"); + ErrorCode WB_BOQ_DIVISION_NOT_SAME_LEVEL = new ErrorCode(1_010_028_002, "只能在同级节点间交换排序"); + ErrorCode WB_BOQ_DIVISION_NODE_TYPE_INVALID = new ErrorCode(1_010_028_003, "无效的节点类型"); + ErrorCode WB_BOQ_DIVISION_PARENT_NOT_EXISTS = new ErrorCode(1_010_028_004, "父节点不存在"); + ErrorCode WB_BOQ_DIVISION_BOQ_PARENT_MUST_DIVISION = new ErrorCode(1_010_028_005, "清单只能在分部节点下创建"); + ErrorCode WB_BOQ_DIVISION_QUOTA_PARENT_MUST_BOQ = new ErrorCode(1_010_028_006, "定额只能在清单节点下创建"); + ErrorCode WB_BOQ_DIVISION_NODE_TYPE_CANNOT_CHANGE = new ErrorCode(1_010_028_007, "不允许修改节点类型"); + ErrorCode WB_BOQ_DIVISION_COMPILE_TREE_NOT_UNIT = new ErrorCode(1_010_028_008, "只能在单位工程节点下创建分部分项"); + ErrorCode WB_BOQ_DIVISION_DIVISION_MUST_ROOT = new ErrorCode(1_010_028_009, "分部节点只能作为根节点"); + ErrorCode WB_BOQ_DIVISION_ROOT_CANNOT_DELETE = new ErrorCode(1_010_028_010, "根目录节点不能删除"); + ErrorCode WB_BOQ_DIVISION_ROOT_CANNOT_UPDATE = new ErrorCode(1_010_028_011, "根目录节点不能编辑"); + ErrorCode WB_BOQ_DIVISION_NODE_TYPE_ERROR = new ErrorCode(1_010_028_012, "节点类型错误"); + ErrorCode WB_BOQ_DIVISION_FORMULA_INVALID = new ErrorCode(1_010_028_013, "定额工程量公式无效: {}"); + ErrorCode WB_BOQ_DIVISION_BOQ_CODE_EXISTS = new ErrorCode(1_010_028_014, "清单编码在该单位工程下已存在"); + ErrorCode WB_BOQ_DIVISION_BASE_NUMBER_FORMULA_INVALID = new ErrorCode(1_010_028_015, "基数公式无效: {}"); + + // ========== 工料机消耗模块 1-010-029-000 ========== + ErrorCode WB_BOQ_RESOURCE_NOT_EXISTS = new ErrorCode(1_010_029_000, "工料机消耗不存在"); + ErrorCode WB_BOQ_RESOURCE_DIVISION_NOT_QUOTA = new ErrorCode(1_010_029_001, "只能在定额节点下添加工料机"); + ErrorCode WB_BOQ_RESOURCE_CHILD_CONSUME_QTY_READONLY = new ErrorCode(1_010_029_002, "复合工料机的子工料机不允许修改定额消耗量"); + ErrorCode WB_BOQ_MARKET_MATERIAL_NOT_EXISTS = new ErrorCode(1_010_029_003, "市场主材设备不存在"); + ErrorCode WB_BOQ_RESOURCE_MANUAL_CONSUME_QTY_READONLY = new ErrorCode(1_010_029_004, "手动新增的工料机不允许修改定额消耗量"); + + // ========== 工作台定额取费模块 1-010-030-000 ========== + ErrorCode WB_BOQ_FEE_NOT_EXISTS = new ErrorCode(1_010_030_000, "定额取费项不存在"); + ErrorCode WB_BOQ_FEE_ZHDJ_CANNOT_DELETE = new ErrorCode(1_010_030_001, "综合单价行(ZHDJ)不可删除"); + ErrorCode WB_BOQ_FEE_ZHDJ_CODE_CANNOT_MODIFY = new ErrorCode(1_010_030_002, "综合单价行(ZHDJ)的代号不可修改"); + + // ========== 公式校验模块 1-010-031-000 ========== + ErrorCode FORMULA_INVALID = new ErrorCode(1_010_031_000, "取费项 [{}] 的计算基数公式不合法: {}"); + + // ========== 统一取费设置模块 1-010-032-000 ========== + ErrorCode UNIFIED_FEE_SETTING_NOT_EXISTS = new ErrorCode(1_010_032_000, "统一取费设置不存在"); + ErrorCode UNIFIED_FEE_SETTING_HAS_CHILDREN = new ErrorCode(1_010_032_001, "该节点存在子节点,无法删除"); + ErrorCode UNIFIED_FEE_SETTING_CODE_DUPLICATE = new ErrorCode(1_010_032_002, "编号 [{}] 已存在"); + ErrorCode UNIFIED_FEE_SETTING_NOT_SAME_CATALOG = new ErrorCode(1_010_032_003, "只能在同一模式下交换排序"); + ErrorCode UNIFIED_FEE_CHAPTER_INTERSECTION = new ErrorCode(1_010_032_004, "取费章节与 [{}] 存在交集,请重新选择"); + + // ========== 统一取费子目工料机模块 1-010-033-000 ========== + ErrorCode UNIFIED_FEE_RESOURCE_NOT_EXISTS = new ErrorCode(1_010_033_000, "统一取费子目工料机不存在"); + ErrorCode UNIFIED_FEE_RESOURCE_CODE_DUPLICATE = new ErrorCode(1_010_033_001, "编码 [{}] 已存在"); + ErrorCode UNIFIED_FEE_RESOURCE_NOT_SAME_SETTING = new ErrorCode(1_010_033_002, "只能在同一子定额下交换排序"); + + // ========== 统一取费单价模块 1-010-034-000 ========== + ErrorCode UNIFIED_FEE_NOT_EXISTS = new ErrorCode(1_010_034_000, "统一取费单价不存在"); + ErrorCode UNIFIED_FEE_HAS_CHILDREN = new ErrorCode(1_010_034_001, "该节点存在子节点,无法删除"); + ErrorCode UNIFIED_FEE_CODE_DUPLICATE = new ErrorCode(1_010_034_002, "代号 [{}] 已存在"); + ErrorCode UNIFIED_FEE_NOT_SAME_CATALOG = new ErrorCode(1_010_034_003, "只能在同一模式下交换排序"); + + // ========== 工作台统一取费配置模块 1-010-035-000 ========== + ErrorCode WB_UNIFIED_FEE_CONFIG_NOT_EXISTS = new ErrorCode(1_010_035_000, "工作台统一取费配置不存在"); + + // ========== 单位工程变量设置模块 1-010-036-000 ========== + ErrorCode QUOTA_VARIABLE_SETTING_NOT_EXISTS = new ErrorCode(1_010_036_000, "变量设置不存在"); + ErrorCode QUOTA_VARIABLE_SETTING_INVALID_NODE_TYPE = new ErrorCode(1_010_036_001, "catalogItemId 必须指向 specialty 类型的节点(定额专业)"); + ErrorCode QUOTA_VARIABLE_SETTING_INVALID_CATEGORY = new ErrorCode(1_010_036_002, "无效的类别,必须为 division/measure/other/unit_summary"); + ErrorCode QUOTA_VARIABLE_SETTING_CODE_DUPLICATE = new ErrorCode(1_010_036_003, "费用代号 [{}] 已存在"); + ErrorCode QUOTA_VARIABLE_SETTING_NOT_SAME_CATALOG = new ErrorCode(1_010_036_004, "只能在同一模式下交换排序"); + ErrorCode QUOTA_VARIABLE_SETTING_CALC_BASE_INVALID = new ErrorCode(1_010_036_005, "计算基数公式格式无效,只允许字母、数字、运算符和括号"); + ErrorCode QUOTA_VARIABLE_SETTING_CALC_BASE_VARIABLE_NOT_EXISTS = new ErrorCode(1_010_036_006, "计算基数公式中包含无效的变量: {}"); + ErrorCode QUOTA_VARIABLE_SETTING_CODE_FORMAT_INVALID = new ErrorCode(1_010_036_007, "费用代号格式无效,必须以字母开头,只能包含字母、数字、下划线"); + ErrorCode QUOTA_VARIABLE_SETTING_CODE_DUPLICATE_ACROSS_TABS = new ErrorCode(1_010_036_008, "费用代号 [{}] 已在 [{}] 标签页中使用,四个标签页不允许出现相同的费用代号"); + + // ========== 审核模式模块 1-010-037-000 ========== + ErrorCode AUDIT_MODE_NOT_EXISTS = new ErrorCode(1_010_037_000, "审核模式不存在"); + ErrorCode AUDIT_APPROVE_DIVISION_NOT_EXISTS = new ErrorCode(1_010_037_002, "审定分部分项不存在"); + ErrorCode AUDIT_MODE_ALREADY_EXISTS = new ErrorCode(1_010_037_003, "该项目已存在审核模式,每个项目只允许创建一次"); + + // ========== 进度款模式模块 1-010-038-000 ========== + ErrorCode PROGRESS_PAYMENT_MODE_NOT_EXISTS = new ErrorCode(1_010_038_000, "进度款模式不存在"); + + // ========== 基数设置目录树模块 1-010-039-000 ========== + ErrorCode CALC_BASE_CATALOG_NOT_EXISTS = new ErrorCode(1_010_039_000, "基数设置目录树节点不存在"); + ErrorCode CALC_BASE_CATALOG_CODE_DUPLICATE = new ErrorCode(1_010_039_001, "编码已存在"); + ErrorCode CALC_BASE_CATALOG_INVALID_NODE_TYPE = new ErrorCode(1_010_039_002, "无效的节点类型"); + ErrorCode CALC_BASE_CATALOG_HAS_CHILDREN = new ErrorCode(1_010_039_003, "该节点存在子节点,无法删除"); + ErrorCode CALC_BASE_CATALOG_PARENT_NOT_EXISTS = new ErrorCode(1_010_039_004, "父节点不存在"); + ErrorCode CALC_BASE_CATALOG_ROOT_EXISTS = new ErrorCode(1_010_039_005, "根节点已存在,只允许创建一个根节点"); + + // ========== 基数设置目录模块 1-010-040-000 ========== + ErrorCode CALC_BASE_DIRECTORY_NOT_EXISTS = new ErrorCode(1_010_040_000, "基数设置目录节点不存在"); + ErrorCode CALC_BASE_DIRECTORY_HAS_CHILDREN = new ErrorCode(1_010_040_001, "该节点存在子节点,无法删除"); + ErrorCode CALC_BASE_DIRECTORY_PARENT_NOT_EXISTS = new ErrorCode(1_010_040_002, "父节点不存在"); + ErrorCode CALC_BASE_DIRECTORY_NOT_SAME_LEVEL = new ErrorCode(1_010_040_003, "只能在同级节点间交换排序"); + + // ========== 基数设置费率模块 1-010-041-000 ========== + ErrorCode CALC_BASE_RATE_NOT_EXISTS = new ErrorCode(1_010_041_000, "基数设置费率不存在"); + + // ========== 基数费率目录树模块 1-010-042-000 ========== + ErrorCode CALC_BASE_RATE_CATALOG_NOT_EXISTS = new ErrorCode(1_010_042_000, "基数费率目录树节点不存在"); + ErrorCode CALC_BASE_RATE_CATALOG_CODE_DUPLICATE = new ErrorCode(1_010_042_001, "编码已存在"); + ErrorCode CALC_BASE_RATE_CATALOG_INVALID_NODE_TYPE = new ErrorCode(1_010_042_002, "无效的节点类型"); + ErrorCode CALC_BASE_RATE_CATALOG_HAS_CHILDREN = new ErrorCode(1_010_042_003, "该节点存在子节点,无法删除"); + ErrorCode CALC_BASE_RATE_CATALOG_PARENT_NOT_EXISTS = new ErrorCode(1_010_042_004, "父节点不存在"); + ErrorCode CALC_BASE_RATE_CATALOG_ROOT_EXISTS = new ErrorCode(1_010_042_005, "根节点已存在,只允许创建一个根节点"); + + // ========== 基数费率目录模块 1-010-043-000 ========== + ErrorCode CALC_BASE_RATE_DIRECTORY_NOT_EXISTS = new ErrorCode(1_010_043_000, "基数费率目录节点不存在"); + ErrorCode CALC_BASE_RATE_DIRECTORY_HAS_CHILDREN = new ErrorCode(1_010_043_001, "该节点存在子节点,无法删除"); + ErrorCode CALC_BASE_RATE_DIRECTORY_PARENT_NOT_EXISTS = new ErrorCode(1_010_043_002, "父节点不存在"); + ErrorCode CALC_BASE_RATE_DIRECTORY_NOT_SAME_LEVEL = new ErrorCode(1_010_043_003, "只能在同级节点间交换排序"); + + // ========== 基数费率项模块 1-010-044-000 ========== + ErrorCode CALC_BASE_RATE_ITEM_NOT_EXISTS = new ErrorCode(1_010_044_000, "基数费率项不存在"); + + // ========== 单位工程界面配置-工作台字段设置模块 1-010-046-000 ========== + ErrorCode CONFIG_UNIT_FIELD_NOT_EXISTS = new ErrorCode(1_010_046_000, "工作台字段设置不存在"); + ErrorCode CONFIG_UNIT_FIELD_CODE_DUPLICATE = new ErrorCode(1_010_046_001, "字段编码 [{}] 已存在"); + ErrorCode CONFIG_UNIT_FIELD_NOT_FIELDS_MAJORS = new ErrorCode(1_010_046_002, "catalogItemId 必须指向 fields_majors 类型的节点"); + ErrorCode CONFIG_UNIT_FIELD_NOT_SAME_CATALOG = new ErrorCode(1_010_046_003, "只能在同一专业类别下交换排序"); + + // ========== 单位工程界面配置-工料机字段模块 1-010-047-000 ========== + ErrorCode CONFIG_UNIT_RESOURCE_FIELD_NOT_EXISTS = new ErrorCode(1_010_047_000, "工料机字段设置不存在"); + ErrorCode CONFIG_UNIT_RESOURCE_FIELD_CODE_DUPLICATE = new ErrorCode(1_010_047_001, "字段编码 [{}] 已存在"); + ErrorCode CONFIG_UNIT_RESOURCE_FIELD_NOT_FIELDS_MAJORS = new ErrorCode(1_010_047_002, "catalogItemId 必须指向 fields_majors 类型的节点"); + ErrorCode CONFIG_UNIT_RESOURCE_FIELD_NOT_SAME_CATALOG = new ErrorCode(1_010_047_003, "只能在同一专业类别下交换排序"); + + // ========== 单位工程界面配置-分部分项模板模块 1-010-048-000 ========== + ErrorCode CONFIG_UNIT_DIV_TPL_NOT_EXISTS = new ErrorCode(1_010_048_000, "分部分项模板节点不存在"); + ErrorCode CONFIG_UNIT_DIV_TPL_HAS_CHILDREN = new ErrorCode(1_010_048_001, "该节点存在子节点,无法删除"); + ErrorCode CONFIG_UNIT_DIV_TPL_NOT_FIELDS_MAJORS = new ErrorCode(1_010_048_002, "catalogItemId 必须指向 fields_majors 类型的节点"); + ErrorCode CONFIG_UNIT_DIV_TPL_NOT_SAME_LEVEL = new ErrorCode(1_010_048_003, "只能在同级节点间交换排序"); + ErrorCode CONFIG_UNIT_DIV_TPL_INVALID_NODE_TYPE = new ErrorCode(1_010_048_004, "无效的节点类型,只能是 division 或 boq"); + ErrorCode CONFIG_UNIT_DIV_TPL_PARENT_NOT_EXISTS = new ErrorCode(1_010_048_005, "父节点不存在"); + ErrorCode CONFIG_UNIT_DIV_TPL_BOQ_PARENT_MUST_DIVISION = new ErrorCode(1_010_048_006, "清单只能在分部节点下创建"); + ErrorCode CONFIG_UNIT_DIV_TPL_INVALID_TAB_TYPE = new ErrorCode(1_010_048_007, "无效的标签页类型,只能是 division/measure/other/unit_summary"); + + // ========== 单位工程界面配置-标签页引用模块 1-010-049-000 ========== + ErrorCode CONFIG_UNIT_TAB_REF_NOT_EXISTS = new ErrorCode(1_010_049_000, "标签页引用不存在"); + ErrorCode CONFIG_UNIT_TAB_REF_NOT_FIELDS_MAJORS = new ErrorCode(1_010_049_001, "catalogItemId 必须指向 fields_majors 类型的节点"); + ErrorCode CONFIG_UNIT_TAB_REF_INVALID_TAB_TYPE = new ErrorCode(1_010_049_002, "无效的标签页类型,必须为 measure/other/unit_summary"); + ErrorCode CONFIG_UNIT_TAB_REF_TEMPLATE_NOT_EXISTS = new ErrorCode(1_010_049_003, "引用的模板节点不存在"); + ErrorCode CONFIG_UNIT_TAB_REF_DUPLICATE = new ErrorCode(1_010_049_004, "该标签页已引用此模板节点"); + ErrorCode CONFIG_UNIT_TAB_REF_NOT_SAME_CATALOG = new ErrorCode(1_010_049_005, "只能在同一专业类别下交换排序"); + + // ========== 同步库模块 1-010-045-000 ========== + ErrorCode SYNC_LIBRARY_DIVISION_NOT_EXISTS = new ErrorCode(1_010_045_000, "同步库节点不存在"); + ErrorCode SYNC_DIVISION_NOT_EXISTS = new ErrorCode(1_010_045_001, "分部分项节点不存在"); + ErrorCode SYNC_ONLY_BOQ_OR_DIVISION = new ErrorCode(1_010_045_002, "只有清单或分部节点才能设为同步"); + ErrorCode SYNC_ALREADY_SYNCED = new ErrorCode(1_010_045_003, "该节点已设为同步"); + ErrorCode SYNC_BOQ_NO_QUOTA = new ErrorCode(1_010_045_004, "清单节点必须有子定额才能设为同步"); + ErrorCode SYNC_UNIFIED_FEE_NOT_ALLOWED = new ErrorCode(1_010_045_005, "统一取费节点不能设为同步"); + ErrorCode SYNC_COMPILE_TREE_NOT_EXISTS = new ErrorCode(1_010_045_006, "单位工程不存在"); + ErrorCode SYNC_NOT_SYNCED = new ErrorCode(1_010_045_007, "该节点未设为同步"); + ErrorCode SYNC_LIBRARY_HAS_BINDINGS = new ErrorCode(1_010_045_008, "同步库还有绑定关系,请先解除所有同步"); } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/enums/infoprice/InfoPriceCategoryNodeTypeEnum.java b/yhy-module-core/src/main/java/com/yhy/module/core/enums/infoprice/InfoPriceCategoryNodeTypeEnum.java index c77df76..66ac56a 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/enums/infoprice/InfoPriceCategoryNodeTypeEnum.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/enums/infoprice/InfoPriceCategoryNodeTypeEnum.java @@ -15,7 +15,7 @@ import java.util.Arrays; @Getter @AllArgsConstructor public enum InfoPriceCategoryNodeTypeEnum implements ArrayValuable { - + ROOT(0, "根目录", "根目录"), DIRECTORY(1, "目录", "可以添加子目录和内容节点"), CONTENT(2, "内容", "只能添加工料机信息,不能添加子节点"); diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/enums/infoprice/InfoPriceTypeEnum.java b/yhy-module-core/src/main/java/com/yhy/module/core/enums/infoprice/InfoPriceTypeEnum.java index 0d7cb17..b2630e3 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/enums/infoprice/InfoPriceTypeEnum.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/enums/infoprice/InfoPriceTypeEnum.java @@ -2,11 +2,10 @@ package com.yhy.module.core.enums.infoprice; import cn.hutool.core.util.ArrayUtil; import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import java.util.Arrays; import lombok.AllArgsConstructor; import lombok.Getter; -import java.util.Arrays; - /** * 信息价类型枚举 * diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/BoqDetailTreeService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/BoqDetailTreeService.java deleted file mode 100644 index 59b44ba..0000000 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/BoqDetailTreeService.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.yhy.module.core.service.boq; - -import com.yhy.module.core.controller.admin.boq.vo.BoqDetailTreeRespVO; -import com.yhy.module.core.controller.admin.boq.vo.BoqDetailTreeSaveReqVO; - -import javax.validation.Valid; -import java.util.List; - -/** - * 清单明细树 Service 接口 - * - * @author yhy - */ -public interface BoqDetailTreeService { - - /** - * 创建节点 - * - * @param createReqVO 创建信息 - * @return 节点ID - */ - Long createNode(@Valid BoqDetailTreeSaveReqVO createReqVO); - - /** - * 更新节点 - * - * @param updateReqVO 更新信息 - */ - void updateNode(@Valid BoqDetailTreeSaveReqVO updateReqVO); - - /** - * 删除节点 - * - * @param id 节点ID - */ - void deleteNode(Long id); - - /** - * 获取节点详情 - * - * @param id 节点ID - * @return 节点信息 - */ - BoqDetailTreeRespVO getNode(Long id); - - /** - * 获取树形结构 - * - * @param boqSubItemId 清单子项ID - * @return 树形结构 - */ - List getTree(Long boqSubItemId); - - /** - * 获取列表 - * - * @param boqSubItemId 清单子项ID - * @return 列表 - */ - List getList(Long boqSubItemId); - - /** - * 交换排序 - * - * @param nodeId1 节点ID1 - * @param nodeId2 节点ID2 - */ - void swapSort(Long nodeId1, Long nodeId2); -} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/BoqGuideTreeService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/BoqGuideTreeService.java new file mode 100644 index 0000000..feeaa0a --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/BoqGuideTreeService.java @@ -0,0 +1,86 @@ +package com.yhy.module.core.service.boq; + +import com.yhy.module.core.controller.admin.boq.vo.BoqGuideTreeRespVO; +import com.yhy.module.core.controller.admin.boq.vo.BoqGuideTreeSaveReqVO; +import java.util.List; +import javax.validation.Valid; + +/** + * 清单指引树 Service 接口 + * + * @author yhy + */ +public interface BoqGuideTreeService { + + /** + * 创建节点 + * + * @param createReqVO 创建信息 + * @return 节点ID + */ + Long createNode(@Valid BoqGuideTreeSaveReqVO createReqVO); + + /** + * 更新节点 + * + * @param updateReqVO 更新信息 + */ + void updateNode(@Valid BoqGuideTreeSaveReqVO updateReqVO); + + /** + * 删除节点 + * + * @param id 节点ID + */ + void deleteNode(Long id); + + /** + * 获取节点详情 + * + * @param id 节点ID + * @return 节点信息 + */ + BoqGuideTreeRespVO getNode(Long id); + + /** + * 获取树形结构 + * + * @param boqSubItemId 清单子项ID + * @return 树形结构 + */ + List getTree(Long boqSubItemId); + + /** + * 获取列表 + * + * @param boqSubItemId 清单子项ID + * @return 列表 + */ + List getList(Long boqSubItemId); + + /** + * 交换排序 + * + * @param nodeId1 节点ID1 + * @param nodeId2 节点ID2 + */ + void swapSort(Long nodeId1, Long nodeId2); + + /** + * 根据编码验证定额是否存在于绑定范围内 + * + * @param boqSubItemId 清单子项ID + * @param code 定额编码 + * @return 定额基价ID,如果不存在返回null + */ + Long validateQuotaCodeInRange(Long boqSubItemId, String code); + + /** + * 根据编码验证定额是否存在于绑定范围内,并返回定额信息 + * + * @param boqSubItemId 清单子项ID + * @param code 定额编码 + * @return 定额信息(包含id、code、name、unit),如果不存在返回null + */ + com.yhy.module.core.controller.admin.boq.vo.QuotaCodeValidateRespVO validateQuotaCodeWithInfo(Long boqSubItemId, String code); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/BoqItemTreeService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/BoqItemTreeService.java index a365492..8235462 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/BoqItemTreeService.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/BoqItemTreeService.java @@ -1,5 +1,6 @@ package com.yhy.module.core.service.boq; +import com.yhy.module.core.controller.admin.boq.vo.BoqItemTreeDeleteCheckRespVO; import com.yhy.module.core.controller.admin.boq.vo.BoqItemTreeRespVO; import com.yhy.module.core.controller.admin.boq.vo.BoqItemTreeSaveReqVO; import com.yhy.module.core.controller.admin.boq.vo.BoqItemTreeSwapSortReqVO; @@ -66,4 +67,19 @@ public interface BoqItemTreeService { * @param swapReqVO 交换信息 */ void swapSort(@Valid BoqItemTreeSwapSortReqVO swapReqVO); + + /** + * 检查删除清单项树节点前的关联数据 + * + * @param id 节点ID + * @return 检查结果 + */ + BoqItemTreeDeleteCheckRespVO checkDeleteBoqItemTree(Long id); + + /** + * 强制删除清单项树节点(级联删除所有关联数据) + * + * @param id 节点ID + */ + void forceDeleteBoqItemTree(Long id); } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/BoqSubItemService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/BoqSubItemService.java index 1306012..d99984f 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/BoqSubItemService.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/BoqSubItemService.java @@ -3,9 +3,8 @@ package com.yhy.module.core.service.boq; import com.yhy.module.core.controller.admin.boq.vo.BoqSubItemRespVO; import com.yhy.module.core.controller.admin.boq.vo.BoqSubItemSaveReqVO; import com.yhy.module.core.controller.admin.boq.vo.BoqSubItemSwapSortReqVO; - -import javax.validation.Valid; import java.util.List; +import javax.validation.Valid; /** * 清单子项 Service 接口 @@ -58,4 +57,12 @@ public interface BoqSubItemService { * @param swapReqVO 交换信息 */ void swapSort(@Valid BoqSubItemSwapSortReqVO swapReqVO); + + /** + * 获取清单子项关联的目录信息(包含绑定的定额专业ID) + * + * @param id 清单子项ID + * @return 包含定额专业ID的子项信息 + */ + BoqSubItemRespVO getBoqSubItemCatalogInfo(Long id); } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/impl/BoqCatalogItemServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/impl/BoqCatalogItemServiceImpl.java index b46578e..3e867ad 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/impl/BoqCatalogItemServiceImpl.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/impl/BoqCatalogItemServiceImpl.java @@ -1,8 +1,21 @@ package com.yhy.module.core.service.boq.impl; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yhy.module.core.enums.ErrorCodeConstants.BOQ_CATALOG_ITEM_ALREADY_BOUND; +import static com.yhy.module.core.enums.ErrorCodeConstants.BOQ_CATALOG_ITEM_CODE_DUPLICATE; +import static com.yhy.module.core.enums.ErrorCodeConstants.BOQ_CATALOG_ITEM_HAS_CHILDREN; +import static com.yhy.module.core.enums.ErrorCodeConstants.BOQ_CATALOG_ITEM_INVALID_NODE_TYPE; +import static com.yhy.module.core.enums.ErrorCodeConstants.BOQ_CATALOG_ITEM_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.BOQ_CATALOG_ITEM_NOT_SAME_LEVEL; +import static com.yhy.module.core.enums.ErrorCodeConstants.BOQ_CATALOG_ITEM_NOT_SPECIALTY; +import static com.yhy.module.core.enums.ErrorCodeConstants.BOQ_CATALOG_ITEM_PARENT_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.BOQ_CATALOG_ITEM_QUOTA_NOT_SPECIALTY; +import static com.yhy.module.core.enums.ErrorCodeConstants.BOQ_CATALOG_ITEM_ROOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.BOQ_CATALOG_ITEM_SPECIALTY_LOCKED; +import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_CATALOG_ITEM_NOT_EXISTS; + import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ArrayUtil; -import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; import com.yhy.module.core.controller.admin.boq.vo.BoqCatalogItemBindQuotaReqVO; import com.yhy.module.core.controller.admin.boq.vo.BoqCatalogItemRespVO; import com.yhy.module.core.controller.admin.boq.vo.BoqCatalogItemSaveReqVO; @@ -11,21 +24,16 @@ import com.yhy.module.core.dal.dataobject.boq.BoqCatalogItemDO; import com.yhy.module.core.dal.dataobject.quota.QuotaCatalogItemDO; import com.yhy.module.core.dal.mysql.boq.BoqCatalogItemMapper; import com.yhy.module.core.dal.mysql.quota.QuotaCatalogItemMapper; -import com.yhy.module.core.enums.ErrorCodeConstants; import com.yhy.module.core.service.boq.BoqCatalogItemService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.validation.annotation.Validated; - -import javax.annotation.Resource; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; - -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static com.yhy.module.core.enums.ErrorCodeConstants.*; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; /** * 清单配置目录树 Service 实现类 @@ -52,20 +60,43 @@ public class BoqCatalogItemServiceImpl implements BoqCatalogItemService { // 2. 校验节点类型 validateNodeType(createReqVO.getNodeType()); - // 3. 构建DO对象 - BoqCatalogItemDO boqCatalogItem = buildBoqCatalogItemDO(createReqVO); + // 3. 校验根节点唯一性 + if (BoqCatalogItemDO.NODE_TYPE_ROOT.equals(createReqVO.getNodeType())) { + validateRootUnique(); + } - // 4. 计算路径和层级 - calculatePathAndLevel(boqCatalogItem, createReqVO.getParentId()); + // 4. 构建DO对象 + BoqCatalogItemDO boqCatalogItem = buildBoqCatalogItemDO(createReqVO); // 5. 设置排序 if (boqCatalogItem.getSortOrder() == null) { boqCatalogItem.setSortOrder(getNextSortOrder(createReqVO.getParentId())); } - // 6. 插入数据库 + // 6. 预设level和临时path(为了满足NOT NULL约束) + BoqCatalogItemDO parent = null; + if (createReqVO.getParentId() != null) { + parent = boqCatalogItemMapper.selectById(createReqVO.getParentId()); + if (parent == null) { + throw exception(BOQ_CATALOG_ITEM_PARENT_NOT_EXISTS); + } + boqCatalogItem.setLevel(parent.getLevel() + 1); + boqCatalogItem.setPath(parent.getPath()); // 临时用父路径 + } else { + boqCatalogItem.setLevel(0); // 根节点level=0 + boqCatalogItem.setPath(new String[]{}); // 临时空数组 + } + + // 7. 插入数据库获取ID boqCatalogItemMapper.insert(boqCatalogItem); + // 8. 更新真实路径 + String[] realPath = parent == null + ? new String[]{boqCatalogItem.getId().toString()} + : ArrayUtil.append(parent.getPath(), boqCatalogItem.getId().toString()); + boqCatalogItem.setPath(realPath); + boqCatalogItemMapper.updateById(boqCatalogItem); + return boqCatalogItem.getId(); } @@ -75,18 +106,28 @@ public class BoqCatalogItemServiceImpl implements BoqCatalogItemService { // 1. 校验节点存在 BoqCatalogItemDO existingItem = validateBoqCatalogItemExists(updateReqVO.getId()); - // 2. 校验编码唯一性 - validateCodeUnique(updateReqVO.getId(), updateReqVO.getCode()); - - // 3. 校验节点类型 - validateNodeType(updateReqVO.getNodeType()); - - // 4. 校验是否已绑定(已绑定的专业节点不允许修改) - if (Boolean.TRUE.equals(existingItem.getSpecialtyLocked())) { - throw exception(BOQ_CATALOG_ITEM_SPECIALTY_LOCKED); + // 2. 校验编码唯一性(如果传了编码) + if (updateReqVO.getCode() != null) { + validateCodeUnique(updateReqVO.getId(), updateReqVO.getCode()); } - // 5. 构建更新对象 + // 3. 校验节点类型(如果传了节点类型) + if (updateReqVO.getNodeType() != null) { + validateNodeType(updateReqVO.getNodeType()); + } + + // 4. 校验是否已绑定(已绑定的专业节点不允许修改节点类型和编码) + if (Boolean.TRUE.equals(existingItem.getSpecialtyLocked())) { + // 已绑定时,只允许修改名称和排序 + if (updateReqVO.getNodeType() != null && !updateReqVO.getNodeType().equals(existingItem.getNodeType())) { + throw exception(BOQ_CATALOG_ITEM_SPECIALTY_LOCKED); + } + if (updateReqVO.getCode() != null && !updateReqVO.getCode().equals(existingItem.getCode())) { + throw exception(BOQ_CATALOG_ITEM_SPECIALTY_LOCKED); + } + } + + // 5. 构建更新对象(只更新非空字段) BoqCatalogItemDO updateObj = BoqCatalogItemDO.builder() .id(updateReqVO.getId()) .code(updateReqVO.getCode()) @@ -160,17 +201,18 @@ public class BoqCatalogItemServiceImpl implements BoqCatalogItemService { throw exception(BOQ_CATALOG_ITEM_ALREADY_BOUND); } - // 4. 校验定额基价节点存在且为第一层 + // 4. 校验定额基价节点存在 QuotaCatalogItemDO quotaItem = quotaCatalogItemMapper.selectById(bindReqVO.getQuotaCatalogItemId()); if (quotaItem == null) { throw exception(QUOTA_CATALOG_ITEM_NOT_EXISTS); } - // 通过 path 数组长度判断层级(第一层的 path 长度为 1) - if (quotaItem.getPath() == null || quotaItem.getPath().length != 1) { - throw exception(BOQ_CATALOG_ITEM_QUOTA_NOT_FIRST_LEVEL); + + // 5. 校验定额基价节点类型必须是 specialty(定额专业),不能是 region(地区)或 rate_mode(费率模式) + if (!"specialty".equals(quotaItem.getNodeType())) { + throw exception(BOQ_CATALOG_ITEM_QUOTA_NOT_SPECIALTY); } - // 5. 更新绑定信息 + // 6. 更新绑定信息 BoqCatalogItemDO updateObj = BoqCatalogItemDO.builder() .id(bindReqVO.getBoqCatalogItemId()) .quotaCatalogItemId(bindReqVO.getQuotaCatalogItemId()) @@ -226,12 +268,27 @@ public class BoqCatalogItemServiceImpl implements BoqCatalogItemService { * 校验节点类型 */ private void validateNodeType(String nodeType) { - if (!BoqCatalogItemDO.NODE_TYPE_PROVINCE.equals(nodeType) - && !BoqCatalogItemDO.NODE_TYPE_SPECIALTY.equals(nodeType)) { + List validTypes = Arrays.asList( + BoqCatalogItemDO.NODE_TYPE_ROOT, + BoqCatalogItemDO.NODE_TYPE_PROVINCE, + BoqCatalogItemDO.NODE_TYPE_SPECIALTY, + BoqCatalogItemDO.NODE_TYPE_CONTENT + ); + if (!validTypes.contains(nodeType)) { throw exception(BOQ_CATALOG_ITEM_INVALID_NODE_TYPE); } } + /** + * 校验根节点唯一性 + */ + private void validateRootUnique() { + BoqCatalogItemDO existingRoot = boqCatalogItemMapper.selectByNodeType(BoqCatalogItemDO.NODE_TYPE_ROOT); + if (existingRoot != null) { + throw exception(BOQ_CATALOG_ITEM_ROOT_EXISTS); + } + } + /** * 校验节点存在 */ @@ -259,29 +316,6 @@ public class BoqCatalogItemServiceImpl implements BoqCatalogItemService { .build(); } - /** - * 计算路径和层级 - */ - private void calculatePathAndLevel(BoqCatalogItemDO item, Long parentId) { - if (parentId == null) { - // 根节点 - item.setPath(new String[]{item.getId().toString()}); - item.setLevel(1); - } else { - // 子节点 - BoqCatalogItemDO parent = boqCatalogItemMapper.selectById(parentId); - if (parent == null) { - throw exception(BOQ_CATALOG_ITEM_PARENT_NOT_EXISTS); - } - - // 拼接路径 - String[] parentPath = parent.getPath(); - String[] newPath = ArrayUtil.append(parentPath, item.getId().toString()); - item.setPath(newPath); - item.setLevel(parent.getLevel() + 1); - } - } - /** * 获取下一个排序值 */ diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/impl/BoqDetailTreeServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/impl/BoqDetailTreeServiceImpl.java deleted file mode 100644 index 3c0b043..0000000 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/impl/BoqDetailTreeServiceImpl.java +++ /dev/null @@ -1,391 +0,0 @@ -package com.yhy.module.core.service.boq.impl; - -import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import com.yhy.module.core.controller.admin.boq.vo.BoqDetailTreeRespVO; -import com.yhy.module.core.controller.admin.boq.vo.BoqDetailTreeSaveReqVO; -import com.yhy.module.core.dal.dataobject.boq.BoqCatalogItemDO; -import com.yhy.module.core.dal.dataobject.boq.BoqDetailTreeDO; -import com.yhy.module.core.dal.dataobject.boq.BoqItemTreeDO; -import com.yhy.module.core.dal.dataobject.boq.BoqSubItemDO; -import com.yhy.module.core.dal.dataobject.quota.QuotaCatalogItemDO; -import com.yhy.module.core.dal.mysql.boq.BoqCatalogItemMapper; -import com.yhy.module.core.dal.mysql.boq.BoqDetailTreeMapper; -import com.yhy.module.core.dal.mysql.boq.BoqItemTreeMapper; -import com.yhy.module.core.dal.mysql.boq.BoqSubItemMapper; -import com.yhy.module.core.dal.mysql.quota.QuotaCatalogItemMapper; -import com.yhy.module.core.service.boq.BoqDetailTreeService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.validation.annotation.Validated; - -import javax.annotation.Resource; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static com.yhy.module.core.enums.ErrorCodeConstants.*; - -/** - * 清单明细树 Service 实现类 - * - * @author yhy - */ -@Service -@Validated -@Slf4j -public class BoqDetailTreeServiceImpl implements BoqDetailTreeService { - - @Resource - private BoqDetailTreeMapper boqDetailTreeMapper; - - @Resource - private BoqSubItemMapper boqSubItemMapper; - - @Resource - private BoqItemTreeMapper boqItemTreeMapper; - - @Resource - private BoqCatalogItemMapper boqCatalogItemMapper; - - @Resource - private QuotaCatalogItemMapper quotaCatalogItemMapper; - - @Override - @Transactional(rollbackFor = Exception.class) - public Long createNode(BoqDetailTreeSaveReqVO createReqVO) { - // 1. 校验清单子项存在 - validateBoqSubItemExists(createReqVO.getBoqSubItemId()); - - // 2. 校验编码唯一性 - validateCodeUnique(null, createReqVO.getBoqSubItemId(), createReqVO.getCode()); - - // 3. 校验节点类型 - validateNodeType(createReqVO); - - // 4. 如果是content类型,验证定额是否在范围内 - if ("content".equals(createReqVO.getNodeType())) { - validateQuotaInRange(createReqVO.getBoqSubItemId(), createReqVO.getQuotaCatalogItemId()); - } - - // 5. 构建节点 - BoqDetailTreeDO node = BeanUtils.toBean(createReqVO, BoqDetailTreeDO.class); - - // 6. 计算路径和层级 - if (createReqVO.getParentId() != null) { - BoqDetailTreeDO parent = validateNodeExists(createReqVO.getParentId()); - // 校验父节点是否为目录类型 - if (!"directory".equals(parent.getNodeType())) { - throw exception(BOQ_DETAIL_TREE_PARENT_NOT_DIRECTORY); - } - // 设置路径和层级 - List pathList = new ArrayList<>(Arrays.asList(parent.getPath())); - pathList.add(String.valueOf(node.getId())); - node.setPath(pathList.toArray(new String[0])); - node.setLevel(parent.getLevel() + 1); - } else { - node.setPath(new String[]{String.valueOf(node.getId())}); - node.setLevel(1); - } - - // 7. 设置排序 - if (node.getSortOrder() == null) { - List siblings = boqDetailTreeMapper.selectListByParentId(createReqVO.getParentId()); - node.setSortOrder(siblings.isEmpty() ? 1 : siblings.get(siblings.size() - 1).getSortOrder() + 1); - } - - // 8. 插入节点 - boqDetailTreeMapper.insert(node); - - // 9. 更新路径(因为ID是插入后才生成的) - List pathList = new ArrayList<>(); - if (createReqVO.getParentId() != null) { - BoqDetailTreeDO parent = boqDetailTreeMapper.selectById(createReqVO.getParentId()); - pathList.addAll(Arrays.asList(parent.getPath())); - } - pathList.add(String.valueOf(node.getId())); - node.setPath(pathList.toArray(new String[0])); - boqDetailTreeMapper.updateById(node); - - return node.getId(); - } - - @Override - @Transactional(rollbackFor = Exception.class) - public void updateNode(BoqDetailTreeSaveReqVO updateReqVO) { - // 1. 校验节点存在 - BoqDetailTreeDO node = validateNodeExists(updateReqVO.getId()); - - // 2. 校验编码唯一性 - validateCodeUnique(updateReqVO.getId(), node.getBoqSubItemId(), updateReqVO.getCode()); - - // 3. 校验节点类型 - validateNodeType(updateReqVO); - - // 4. 如果是content类型,验证定额是否在范围内 - if ("content".equals(updateReqVO.getNodeType())) { - validateQuotaInRange(node.getBoqSubItemId(), updateReqVO.getQuotaCatalogItemId()); - } - - // 5. 更新节点 - BoqDetailTreeDO updateObj = BeanUtils.toBean(updateReqVO, BoqDetailTreeDO.class); - boqDetailTreeMapper.updateById(updateObj); - } - - @Override - @Transactional(rollbackFor = Exception.class) - public void deleteNode(Long id) { - // 1. 校验节点存在 - validateNodeExists(id); - - // 2. 校验是否有子节点 - if (boqDetailTreeMapper.hasChildren(id)) { - throw exception(BOQ_DETAIL_TREE_HAS_CHILDREN); - } - - // 3. 删除节点 - boqDetailTreeMapper.deleteById(id); - } - - @Override - public BoqDetailTreeRespVO getNode(Long id) { - BoqDetailTreeDO node = validateNodeExists(id); - BoqDetailTreeRespVO vo = BeanUtils.toBean(node, BoqDetailTreeRespVO.class); - - // 如果是content类型,填充定额信息 - if ("content".equals(node.getNodeType()) && node.getQuotaCatalogItemId() != null) { - QuotaCatalogItemDO quota = quotaCatalogItemMapper.selectById(node.getQuotaCatalogItemId()); - if (quota != null) { - vo.setQuotaCode(quota.getCode()); - vo.setQuotaName(quota.getName()); - vo.setQuotaUnit(quota.getUnit()); - } - } - - return vo; - } - - @Override - public List getTree(Long boqSubItemId) { - // 查询所有节点 - List allNodes = boqDetailTreeMapper.selectListByBoqSubItemId(boqSubItemId); - - // 转换为VO并填充定额信息 - List allVOs = allNodes.stream() - .map(node -> { - BoqDetailTreeRespVO vo = BeanUtils.toBean(node, BoqDetailTreeRespVO.class); - // 如果是content类型,填充定额信息 - if ("content".equals(node.getNodeType()) && node.getQuotaCatalogItemId() != null) { - QuotaCatalogItemDO quota = quotaCatalogItemMapper.selectById(node.getQuotaCatalogItemId()); - if (quota != null) { - vo.setQuotaCode(quota.getCode()); - vo.setQuotaName(quota.getName()); - vo.setQuotaUnit(quota.getUnit()); - } - } - return vo; - }) - .collect(Collectors.toList()); - - // 构建树形结构 - return buildTree(allVOs, null); - } - - @Override - public List getList(Long boqSubItemId) { - List nodes = boqDetailTreeMapper.selectListByBoqSubItemId(boqSubItemId); - return nodes.stream() - .map(node -> { - BoqDetailTreeRespVO vo = BeanUtils.toBean(node, BoqDetailTreeRespVO.class); - // 如果是content类型,填充定额信息 - if ("content".equals(node.getNodeType()) && node.getQuotaCatalogItemId() != null) { - QuotaCatalogItemDO quota = quotaCatalogItemMapper.selectById(node.getQuotaCatalogItemId()); - if (quota != null) { - vo.setQuotaCode(quota.getCode()); - vo.setQuotaName(quota.getName()); - vo.setQuotaUnit(quota.getUnit()); - } - } - return vo; - }) - .collect(Collectors.toList()); - } - - @Override - @Transactional(rollbackFor = Exception.class) - public void swapSort(Long nodeId1, Long nodeId2) { - // 1. 查询两个节点(加锁) - BoqDetailTreeDO node1 = boqDetailTreeMapper.selectByIdForUpdate(nodeId1); - BoqDetailTreeDO node2 = boqDetailTreeMapper.selectByIdForUpdate(nodeId2); - - // 2. 校验存在 - if (node1 == null || node2 == null) { - throw exception(BOQ_DETAIL_TREE_NOT_EXISTS); - } - - // 3. 校验是否同级 - if (!isSameLevel(node1, node2)) { - throw exception(BOQ_DETAIL_TREE_NOT_SAME_LEVEL); - } - - // 4. 交换排序值 - Integer tempSort = node1.getSortOrder(); - node1.setSortOrder(node2.getSortOrder()); - node2.setSortOrder(tempSort); - - // 5. 更新数据库 - boqDetailTreeMapper.updateById(node1); - boqDetailTreeMapper.updateById(node2); - } - - // ==================== 私有方法 ==================== - - /** - * 校验节点存在 - */ - private BoqDetailTreeDO validateNodeExists(Long id) { - BoqDetailTreeDO node = boqDetailTreeMapper.selectById(id); - if (node == null) { - throw exception(BOQ_DETAIL_TREE_NOT_EXISTS); - } - return node; - } - - /** - * 校验清单子项存在 - */ - private void validateBoqSubItemExists(Long boqSubItemId) { - if (boqSubItemMapper.selectById(boqSubItemId) == null) { - throw exception(BOQ_SUB_ITEM_NOT_EXISTS); - } - } - - /** - * 校验编码唯一性 - */ - private void validateCodeUnique(Long id, Long boqSubItemId, String code) { - BoqDetailTreeDO existing = boqDetailTreeMapper.selectByBoqSubItemIdAndCode(boqSubItemId, code); - if (existing != null && !existing.getId().equals(id)) { - throw exception(BOQ_DETAIL_TREE_CODE_DUPLICATE, code); - } - } - - /** - * 校验节点类型 - */ - private void validateNodeType(BoqDetailTreeSaveReqVO reqVO) { - if (!"directory".equals(reqVO.getNodeType()) && !"content".equals(reqVO.getNodeType())) { - throw exception(BOQ_DETAIL_TREE_INVALID_NODE_TYPE); - } - - // content类型必须有定额ID - if ("content".equals(reqVO.getNodeType()) && reqVO.getQuotaCatalogItemId() == null) { - throw exception(BOQ_DETAIL_TREE_CONTENT_NEED_QUOTA); - } - - // directory类型不能有定额ID - if ("directory".equals(reqVO.getNodeType()) && reqVO.getQuotaCatalogItemId() != null) { - throw exception(BOQ_DETAIL_TREE_DIRECTORY_NO_QUOTA); - } - } - - /** - * 验证定额是否在绑定的定额专业范围内 - */ - private void validateQuotaInRange(Long boqSubItemId, Long quotaCatalogItemId) { - // 1. 获取清单专业绑定的定额基价第一层节点ID - Long boundQuotaId = getQuotaCatalogItemIdByBoqSubItem(boqSubItemId); - - // 2. 查询定额节点 - QuotaCatalogItemDO quotaNode = quotaCatalogItemMapper.selectById(quotaCatalogItemId); - if (quotaNode == null) { - throw exception(BOQ_DETAIL_TREE_QUOTA_NOT_IN_RANGE); - } - - // 3. 验证是否为内容节点 - if (!"content".equals(quotaNode.getContentType())) { - throw exception(BOQ_DETAIL_TREE_QUOTA_NOT_CONTENT_TYPE); - } - - // 4. 从定额节点向上追溯到第一层 - QuotaCatalogItemDO current = quotaNode; - while (current != null && current.getParentId() != null) { - current = quotaCatalogItemMapper.selectById(current.getParentId()); - } - - // 5. 验证是否匹配 - if (current == null || !current.getId().equals(boundQuotaId)) { - throw exception(BOQ_DETAIL_TREE_QUOTA_NOT_IN_RANGE); - } - } - - /** - * 从清单子项追溯到清单专业的绑定定额 - */ - private Long getQuotaCatalogItemIdByBoqSubItem(Long boqSubItemId) { - // 1. 查询清单子项 - BoqSubItemDO subItem = boqSubItemMapper.selectById(boqSubItemId); - if (subItem == null) { - throw exception(BOQ_SUB_ITEM_NOT_EXISTS); - } - - // 2. 查询清单项树 - BoqItemTreeDO itemTree = boqItemTreeMapper.selectById(subItem.getBoqItemTreeId()); - if (itemTree == null) { - throw exception(BOQ_ITEM_TREE_NOT_EXISTS); - } - - // 3. 查询清单专业 - BoqCatalogItemDO catalogItem = boqCatalogItemMapper.selectById(itemTree.getBoqCatalogItemId()); - if (catalogItem == null) { - throw exception(BOQ_CATALOG_ITEM_NOT_EXISTS); - } - - // 4. 返回绑定的定额基价第一层节点ID - if (catalogItem.getQuotaCatalogItemId() == null) { - throw exception(BOQ_CATALOG_ITEM_NOT_BOUND_QUOTA); - } - - return catalogItem.getQuotaCatalogItemId(); - } - - /** - * 判断两个节点是否同级 - */ - private boolean isSameLevel(BoqDetailTreeDO node1, BoqDetailTreeDO node2) { - // 同一清单子项下 - if (!node1.getBoqSubItemId().equals(node2.getBoqSubItemId())) { - return false; - } - // 同一父节点 - if (node1.getParentId() == null && node2.getParentId() == null) { - return true; - } - if (node1.getParentId() == null || node2.getParentId() == null) { - return false; - } - return node1.getParentId().equals(node2.getParentId()); - } - - /** - * 构建树形结构 - */ - private List buildTree(List allNodes, Long parentId) { - return allNodes.stream() - .filter(node -> { - if (parentId == null) { - return node.getParentId() == null; - } - return parentId.equals(node.getParentId()); - }) - .peek(node -> { - List children = buildTree(allNodes, node.getId()); - if (!children.isEmpty()) { - node.setChildren(children); - } - }) - .collect(Collectors.toList()); - } -} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/impl/BoqGuideTreeServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/impl/BoqGuideTreeServiceImpl.java new file mode 100644 index 0000000..376a3b9 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/impl/BoqGuideTreeServiceImpl.java @@ -0,0 +1,519 @@ +package com.yhy.module.core.service.boq.impl; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.*; +import static com.yhy.module.core.enums.ErrorCodeConstants.*; + +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import com.yhy.module.core.controller.admin.boq.vo.BoqGuideTreeRespVO; +import com.yhy.module.core.controller.admin.boq.vo.BoqGuideTreeSaveReqVO; +import com.yhy.module.core.dal.dataobject.boq.BoqCatalogItemDO; +import com.yhy.module.core.dal.dataobject.boq.BoqGuideTreeDO; +import com.yhy.module.core.dal.dataobject.boq.BoqItemTreeDO; +import com.yhy.module.core.dal.dataobject.boq.BoqSubItemDO; +import com.yhy.module.core.dal.dataobject.quota.QuotaCatalogItemDO; +import com.yhy.module.core.dal.dataobject.quota.QuotaItemDO; +import com.yhy.module.core.dal.mysql.boq.BoqCatalogItemMapper; +import com.yhy.module.core.dal.mysql.boq.BoqGuideTreeMapper; +import com.yhy.module.core.dal.mysql.boq.BoqItemTreeMapper; +import com.yhy.module.core.dal.mysql.boq.BoqSubItemMapper; +import com.yhy.module.core.dal.mysql.quota.QuotaCatalogItemMapper; +import com.yhy.module.core.dal.mysql.quota.QuotaCatalogTreeMapper; +import com.yhy.module.core.dal.mysql.quota.QuotaItemMapper; +import com.yhy.module.core.service.boq.BoqGuideTreeService; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * 清单指引树 Service 实现类 + * + * @author yhy + */ +@Service +@Validated +@Slf4j +public class BoqGuideTreeServiceImpl implements BoqGuideTreeService { + + @Resource + private BoqGuideTreeMapper boqGuideTreeMapper; + + @Resource + private BoqSubItemMapper boqSubItemMapper; + + @Resource + private BoqItemTreeMapper boqItemTreeMapper; + + @Resource + private BoqCatalogItemMapper boqCatalogItemMapper; + + @Resource + private QuotaCatalogItemMapper quotaCatalogItemMapper; + + @Resource + private QuotaCatalogTreeMapper quotaCatalogTreeMapper; + + @Resource + private QuotaItemMapper quotaItemMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createNode(BoqGuideTreeSaveReqVO createReqVO) { + // 1. 校验清单子项存在 + validateBoqSubItemExists(createReqVO.getBoqSubItemId()); + + // 2. 校验编码唯一性 + validateCodeUnique(null, createReqVO.getBoqSubItemId(), createReqVO.getCode()); + + // 3. 校验节点类型 + validateNodeType(createReqVO); + + // 4. 如果是quota类型且有定额ID,验证定额是否在范围内 + if ("quota".equals(createReqVO.getNodeType()) && createReqVO.getQuotaCatalogItemId() != null) { + validateQuotaInRange(createReqVO.getBoqSubItemId(), createReqVO.getQuotaCatalogItemId()); + } + + // 5. 构建节点 + BoqGuideTreeDO node = BeanUtils.toBean(createReqVO, BoqGuideTreeDO.class); + + // 6. 计算路径和层级 + if (createReqVO.getParentId() != null) { + BoqGuideTreeDO parent = validateNodeExists(createReqVO.getParentId()); + // 校验父节点是否为目录类型 + if (!"directory".equals(parent.getNodeType())) { + throw exception(BOQ_GUIDE_TREE_PARENT_NOT_DIRECTORY); + } + // 设置路径和层级 + List pathList = new ArrayList<>(Arrays.asList(parent.getPath())); + pathList.add(String.valueOf(node.getId())); + node.setPath(pathList.toArray(new String[0])); + node.setLevel(parent.getLevel() + 1); + } else { + node.setPath(new String[]{String.valueOf(node.getId())}); + node.setLevel(1); + } + + // 7. 设置排序 + if (node.getSortOrder() == null) { + List siblings = boqGuideTreeMapper.selectListByParentId(createReqVO.getParentId()); + node.setSortOrder(siblings.isEmpty() ? 1 : siblings.get(siblings.size() - 1).getSortOrder() + 1); + } + + // 8. 插入节点 + boqGuideTreeMapper.insert(node); + + // 9. 更新路径(因为ID是插入后才生成的) + List pathList = new ArrayList<>(); + if (createReqVO.getParentId() != null) { + BoqGuideTreeDO parent = boqGuideTreeMapper.selectById(createReqVO.getParentId()); + pathList.addAll(Arrays.asList(parent.getPath())); + } + pathList.add(String.valueOf(node.getId())); + node.setPath(pathList.toArray(new String[0])); + boqGuideTreeMapper.updateById(node); + + return node.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateNode(BoqGuideTreeSaveReqVO updateReqVO) { + // 1. 校验节点存在 + BoqGuideTreeDO node = validateNodeExists(updateReqVO.getId()); + + // 2. 校验编码唯一性 + validateCodeUnique(updateReqVO.getId(), node.getBoqSubItemId(), updateReqVO.getCode()); + + // 3. 校验节点类型 + validateNodeType(updateReqVO); + + // 4. 如果是quota类型且有定额ID,验证定额是否在范围内 + if ("quota".equals(updateReqVO.getNodeType()) && updateReqVO.getQuotaCatalogItemId() != null) { + validateQuotaInRange(node.getBoqSubItemId(), updateReqVO.getQuotaCatalogItemId()); + } + + // 5. 更新节点 + BoqGuideTreeDO updateObj = BeanUtils.toBean(updateReqVO, BoqGuideTreeDO.class); + boqGuideTreeMapper.updateById(updateObj); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteNode(Long id) { + // 1. 校验节点存在 + validateNodeExists(id); + + // 2. 校验是否有子节点 + if (boqGuideTreeMapper.hasChildren(id)) { + throw exception(BOQ_GUIDE_TREE_HAS_CHILDREN); + } + + // 3. 删除节点 + boqGuideTreeMapper.deleteById(id); + } + + @Override + public BoqGuideTreeRespVO getNode(Long id) { + BoqGuideTreeDO node = validateNodeExists(id); + BoqGuideTreeRespVO vo = BeanUtils.toBean(node, BoqGuideTreeRespVO.class); + + // 如果是quota类型,填充定额信息 + if ("quota".equals(node.getNodeType()) && node.getQuotaCatalogItemId() != null) { + QuotaCatalogItemDO quota = quotaCatalogItemMapper.selectById(node.getQuotaCatalogItemId()); + if (quota != null) { + vo.setQuotaCode(quota.getCode()); + vo.setQuotaName(quota.getName()); + vo.setQuotaUnit(quota.getUnit()); + } + } + + return vo; + } + + @Override + public List getTree(Long boqSubItemId) { + // 查询所有节点 + List allNodes = boqGuideTreeMapper.selectListByBoqSubItemId(boqSubItemId); + + // 转换为VO并填充定额信息 + List allVOs = allNodes.stream() + .map(node -> { + BoqGuideTreeRespVO vo = BeanUtils.toBean(node, BoqGuideTreeRespVO.class); + // 如果是quota类型,填充定额信息和价格 + if ("quota".equals(node.getNodeType()) && node.getQuotaCatalogItemId() != null) { + QuotaCatalogItemDO quota = quotaCatalogItemMapper.selectById(node.getQuotaCatalogItemId()); + if (quota != null) { + vo.setQuotaCode(quota.getCode()); + vo.setQuotaName(quota.getName()); + vo.setQuotaUnit(quota.getUnit()); + } + // 查询定额基价获取价格信息 + List quotaItems = quotaItemMapper.selectListByCatalogItemId(node.getQuotaCatalogItemId()); + if (quotaItems != null && !quotaItems.isEmpty()) { + // 取第一条定额基价的价格(通常一个定额目录节点只对应一条定额基价) + QuotaItemDO quotaItem = quotaItems.get(0); + vo.setBasePriceExTax(quotaItem.getTaxExclBasePrice()); + vo.setBasePriceInTax(quotaItem.getTaxInclBasePrice()); + } + } + return vo; + }) + .collect(Collectors.toList()); + + // 构建树形结构 + return buildTree(allVOs, null); + } + + @Override + public List getList(Long boqSubItemId) { + List nodes = boqGuideTreeMapper.selectListByBoqSubItemId(boqSubItemId); + return nodes.stream() + .map(node -> { + BoqGuideTreeRespVO vo = BeanUtils.toBean(node, BoqGuideTreeRespVO.class); + // 如果是quota类型,填充定额信息 + if ("quota".equals(node.getNodeType()) && node.getQuotaCatalogItemId() != null) { + QuotaCatalogItemDO quota = quotaCatalogItemMapper.selectById(node.getQuotaCatalogItemId()); + if (quota != null) { + vo.setQuotaCode(quota.getCode()); + vo.setQuotaName(quota.getName()); + vo.setQuotaUnit(quota.getUnit()); + } + } + return vo; + }) + .collect(Collectors.toList()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void swapSort(Long nodeId1, Long nodeId2) { + // 1. 查询两个节点(加锁) + BoqGuideTreeDO node1 = boqGuideTreeMapper.selectByIdForUpdate(nodeId1); + BoqGuideTreeDO node2 = boqGuideTreeMapper.selectByIdForUpdate(nodeId2); + + // 2. 校验存在 + if (node1 == null || node2 == null) { + throw exception(BOQ_GUIDE_TREE_NOT_EXISTS); + } + + // 3. 校验是否同级 + if (!isSameLevel(node1, node2)) { + throw exception(BOQ_GUIDE_TREE_NOT_SAME_LEVEL); + } + + // 4. 交换排序值 + Integer tempSort = node1.getSortOrder(); + node1.setSortOrder(node2.getSortOrder()); + node2.setSortOrder(tempSort); + + // 5. 更新数据库 + boqGuideTreeMapper.updateById(node1); + boqGuideTreeMapper.updateById(node2); + } + + // ==================== 私有方法 ==================== + + /** + * 校验节点存在 + */ + private BoqGuideTreeDO validateNodeExists(Long id) { + BoqGuideTreeDO node = boqGuideTreeMapper.selectById(id); + if (node == null) { + throw exception(BOQ_GUIDE_TREE_NOT_EXISTS); + } + return node; + } + + /** + * 校验清单子项存在 + */ + private void validateBoqSubItemExists(Long boqSubItemId) { + if (boqSubItemMapper.selectById(boqSubItemId) == null) { + throw exception(BOQ_SUB_ITEM_NOT_EXISTS); + } + } + + /** + * 校验编码唯一性 + */ + private void validateCodeUnique(Long id, Long boqSubItemId, String code) { + BoqGuideTreeDO existing = boqGuideTreeMapper.selectByBoqSubItemIdAndCode(boqSubItemId, code); + if (existing != null && !existing.getId().equals(id)) { + throw exception(BOQ_GUIDE_TREE_CODE_DUPLICATE, code); + } + } + + /** + * 校验节点类型 + */ + private void validateNodeType(BoqGuideTreeSaveReqVO reqVO) { + if (!"directory".equals(reqVO.getNodeType()) && !"quota".equals(reqVO.getNodeType())) { + throw exception(BOQ_GUIDE_TREE_INVALID_NODE_TYPE); + } + + // quota类型必须有父节点(定额必须在目录下) + if ("quota".equals(reqVO.getNodeType()) && reqVO.getParentId() == null) { + throw exception(BOQ_GUIDE_TREE_QUOTA_NEED_PARENT); + } + + // quota类型允许quotaCatalogItemId为空(用户可以只填写文字数据) + // 不再强制要求关联定额 + + // directory类型不能有定额ID + if ("directory".equals(reqVO.getNodeType()) && reqVO.getQuotaCatalogItemId() != null) { + throw exception(BOQ_GUIDE_TREE_DIRECTORY_NO_QUOTA); + } + } + + /** + * 验证定额是否在绑定的定额专业范围内 + * @param boqSubItemId 清单子项ID + * @param quotaItemId 定额基价ID(yhy_quota_item表的ID) + */ + private void validateQuotaInRange(Long boqSubItemId, Long quotaItemId) { + // 1. 获取清单专业绑定的定额专业节点ID + Long boundQuotaId = getQuotaCatalogItemIdByBoqSubItem(boqSubItemId); + + // 2. 查询定额基价 + QuotaItemDO quotaItem = quotaItemMapper.selectById(quotaItemId); + if (quotaItem == null) { + throw exception(BOQ_GUIDE_TREE_QUOTA_NOT_IN_RANGE); + } + + // 3. 获取定额基价关联的定额子目树节点ID + Long catalogTreeId = quotaItem.getCatalogItemId(); + if (catalogTreeId == null) { + throw exception(BOQ_GUIDE_TREE_QUOTA_NOT_IN_RANGE); + } + + // 4. 查询定额子目树节点,获取其关联的定额专业节点ID + com.yhy.module.core.dal.dataobject.quota.QuotaCatalogTreeDO catalogTree = + quotaCatalogTreeMapper.selectById(catalogTreeId); + if (catalogTree == null) { + throw exception(BOQ_GUIDE_TREE_QUOTA_NOT_IN_RANGE); + } + + // 5. 获取定额专业节点ID + Long quotaCatalogItemId = catalogTree.getCatalogItemId(); + if (quotaCatalogItemId == null) { + throw exception(BOQ_GUIDE_TREE_QUOTA_NOT_IN_RANGE); + } + + // 6. 直接比较定额专业节点ID是否匹配绑定的定额专业 + if (!quotaCatalogItemId.equals(boundQuotaId)) { + throw exception(BOQ_GUIDE_TREE_QUOTA_NOT_IN_RANGE); + } + } + + /** + * 从清单子项追溯到清单专业的绑定定额 + */ + private Long getQuotaCatalogItemIdByBoqSubItem(Long boqSubItemId) { + // 1. 查询清单子项 + BoqSubItemDO subItem = boqSubItemMapper.selectById(boqSubItemId); + if (subItem == null) { + throw exception(BOQ_SUB_ITEM_NOT_EXISTS); + } + + // 2. 查询清单项树 + BoqItemTreeDO itemTree = boqItemTreeMapper.selectById(subItem.getBoqItemTreeId()); + if (itemTree == null) { + throw exception(BOQ_ITEM_TREE_NOT_EXISTS); + } + + // 3. 查询清单专业 + BoqCatalogItemDO catalogItem = boqCatalogItemMapper.selectById(itemTree.getBoqCatalogItemId()); + if (catalogItem == null) { + throw exception(BOQ_CATALOG_ITEM_NOT_EXISTS); + } + + // 4. 返回绑定的定额基价第一层节点ID + if (catalogItem.getQuotaCatalogItemId() == null) { + throw exception(BOQ_CATALOG_ITEM_NOT_BOUND_QUOTA); + } + + return catalogItem.getQuotaCatalogItemId(); + } + + /** + * 判断两个节点是否同级 + */ + private boolean isSameLevel(BoqGuideTreeDO node1, BoqGuideTreeDO node2) { + // 同一清单子项下 + if (!node1.getBoqSubItemId().equals(node2.getBoqSubItemId())) { + return false; + } + // 同一父节点 + if (node1.getParentId() == null && node2.getParentId() == null) { + return true; + } + if (node1.getParentId() == null || node2.getParentId() == null) { + return false; + } + return node1.getParentId().equals(node2.getParentId()); + } + + /** + * 构建树形结构 + */ + private List buildTree(List allNodes, Long parentId) { + return allNodes.stream() + .filter(node -> { + if (parentId == null) { + return node.getParentId() == null; + } + return parentId.equals(node.getParentId()); + }) + .peek(node -> { + List children = buildTree(allNodes, node.getId()); + if (!children.isEmpty()) { + node.setChildren(children); + } + }) + .collect(Collectors.toList()); + } + + @Override + public Long validateQuotaCodeInRange(Long boqSubItemId, String code) { + if (boqSubItemId == null || code == null || code.trim().isEmpty()) { + return null; + } + + // 1. 获取清单专业绑定的定额基价第一层节点ID + Long boundQuotaId = getQuotaCatalogItemIdByBoqSubItem(boqSubItemId); + + // 2. 根据编码查询定额基价 + List quotaItems = quotaItemMapper.selectByCode(code.trim()); + if (quotaItems == null || quotaItems.isEmpty()) { + return null; + } + + // 3. 遍历查询结果,找到在绑定范围内的定额 + for (QuotaItemDO quotaItem : quotaItems) { + // 获取定额基价关联的定额子目树节点 + Long catalogItemId = quotaItem.getCatalogItemId(); + if (catalogItemId == null) { + continue; + } + + // 从定额子目树节点向上追溯到第一层(定额专业节点) + // 定额子目树(yhy_quota_catalog_tree)通过 catalog_item_id 关联到定额专业树(yhy_quota_catalog_item) + // 需要先查询定额子目树节点,获取其关联的定额专业节点ID + com.yhy.module.core.dal.dataobject.quota.QuotaCatalogTreeDO catalogTree = + quotaCatalogTreeMapper.selectById(catalogItemId); + if (catalogTree == null) { + continue; + } + + // 获取定额专业节点ID(定额子目树直接关联到定额专业节点) + Long quotaCatalogItemId = catalogTree.getCatalogItemId(); + if (quotaCatalogItemId == null) { + continue; + } + + // 直接比较定额专业节点ID是否匹配绑定的定额专业 + // boundQuotaId 是清单专业绑定的定额专业节点ID + // quotaCatalogItemId 是定额基价所属的定额专业节点ID + if (quotaCatalogItemId.equals(boundQuotaId)) { + return quotaItem.getId(); + } + } + + return null; + } + + @Override + public com.yhy.module.core.controller.admin.boq.vo.QuotaCodeValidateRespVO validateQuotaCodeWithInfo(Long boqSubItemId, String code) { + if (boqSubItemId == null || code == null || code.trim().isEmpty()) { + return null; + } + + // 1. 获取清单专业绑定的定额专业节点ID + Long boundQuotaId = getQuotaCatalogItemIdByBoqSubItem(boqSubItemId); + + // 2. 根据编码查询定额基价 + List quotaItems = quotaItemMapper.selectByCode(code.trim()); + if (quotaItems == null || quotaItems.isEmpty()) { + return null; + } + + // 3. 遍历查询结果,找到在绑定范围内的定额 + for (QuotaItemDO quotaItem : quotaItems) { + Long catalogItemId = quotaItem.getCatalogItemId(); + if (catalogItemId == null) { + continue; + } + + // 查询定额子目树节点 + com.yhy.module.core.dal.dataobject.quota.QuotaCatalogTreeDO catalogTree = + quotaCatalogTreeMapper.selectById(catalogItemId); + if (catalogTree == null) { + continue; + } + + // 获取定额专业节点ID + Long quotaCatalogItemId = catalogTree.getCatalogItemId(); + if (quotaCatalogItemId == null) { + continue; + } + + // 比较定额专业节点ID + if (quotaCatalogItemId.equals(boundQuotaId)) { + // 返回定额信息 + return com.yhy.module.core.controller.admin.boq.vo.QuotaCodeValidateRespVO.builder() + .id(quotaItem.getId()) + .code(quotaItem.getCode()) + .name(quotaItem.getName()) + .unit(quotaItem.getUnit()) + .build(); + } + } + + return null; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/impl/BoqItemTreeServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/impl/BoqItemTreeServiceImpl.java index 66bc2b6..0360a26 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/impl/BoqItemTreeServiceImpl.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/impl/BoqItemTreeServiceImpl.java @@ -1,26 +1,34 @@ package com.yhy.module.core.service.boq.impl; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yhy.module.core.enums.ErrorCodeConstants.BOQ_ITEM_TREE_CODE_DUPLICATE; +import static com.yhy.module.core.enums.ErrorCodeConstants.BOQ_ITEM_TREE_HAS_CHILDREN; +import static com.yhy.module.core.enums.ErrorCodeConstants.BOQ_ITEM_TREE_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.BOQ_ITEM_TREE_NOT_SAME_LEVEL; +import static com.yhy.module.core.enums.ErrorCodeConstants.BOQ_ITEM_TREE_PARENT_NOT_EXISTS; + import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ArrayUtil; +import com.yhy.module.core.controller.admin.boq.vo.BoqItemTreeDeleteCheckRespVO; import com.yhy.module.core.controller.admin.boq.vo.BoqItemTreeRespVO; import com.yhy.module.core.controller.admin.boq.vo.BoqItemTreeSaveReqVO; import com.yhy.module.core.controller.admin.boq.vo.BoqItemTreeSwapSortReqVO; +import com.yhy.module.core.dal.dataobject.boq.BoqGuideTreeDO; import com.yhy.module.core.dal.dataobject.boq.BoqItemTreeDO; +import com.yhy.module.core.dal.dataobject.boq.BoqSubItemDO; +import com.yhy.module.core.dal.mysql.boq.BoqGuideTreeMapper; import com.yhy.module.core.dal.mysql.boq.BoqItemTreeMapper; +import com.yhy.module.core.dal.mysql.boq.BoqSubItemMapper; import com.yhy.module.core.service.boq.BoqItemTreeService; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; -import javax.annotation.Resource; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static com.yhy.module.core.enums.ErrorCodeConstants.*; - /** * 清单项树 Service 实现类 * @@ -34,26 +42,52 @@ public class BoqItemTreeServiceImpl implements BoqItemTreeService { @Resource private BoqItemTreeMapper boqItemTreeMapper; + @Resource + private BoqSubItemMapper boqSubItemMapper; + + @Resource + private BoqGuideTreeMapper boqGuideTreeMapper; + @Override @Transactional(rollbackFor = Exception.class) public Long createBoqItemTree(BoqItemTreeSaveReqVO createReqVO) { - // 1. 校验编码唯一性 - validateCodeUnique(null, createReqVO.getCode()); + // 1. 校验编码唯一性(如果编码不为空) + if (createReqVO.getCode() != null && !createReqVO.getCode().isEmpty()) { + validateCodeUnique(null, createReqVO.getCode()); + } // 2. 构建DO对象 BoqItemTreeDO boqItemTree = buildBoqItemTreeDO(createReqVO); - // 3. 计算路径和层级 - calculatePathAndLevel(boqItemTree, createReqVO.getParentId(), createReqVO.getBoqCatalogItemId()); - - // 4. 设置排序 + // 3. 设置排序 if (boqItemTree.getSortOrder() == null) { boqItemTree.setSortOrder(getNextSortOrder(createReqVO.getParentId(), createReqVO.getBoqCatalogItemId())); } - // 5. 插入数据库 + // 4. 预设level和临时path(为了满足NOT NULL约束) + BoqItemTreeDO parent = null; + if (createReqVO.getParentId() != null) { + parent = boqItemTreeMapper.selectById(createReqVO.getParentId()); + if (parent == null) { + throw exception(BOQ_ITEM_TREE_PARENT_NOT_EXISTS); + } + boqItemTree.setLevel(parent.getLevel() + 1); + boqItemTree.setPath(parent.getPath()); // 临时用父路径 + } else { + boqItemTree.setLevel(1); + boqItemTree.setPath(new String[]{}); // 临时空数组 + } + + // 5. 插入数据库获取ID boqItemTreeMapper.insert(boqItemTree); + // 6. 更新真实路径 + String[] realPath = parent == null + ? new String[]{boqItemTree.getId().toString()} + : ArrayUtil.append(parent.getPath(), boqItemTree.getId().toString()); + boqItemTree.setPath(realPath); + boqItemTreeMapper.updateById(boqItemTree); + return boqItemTree.getId(); } @@ -63,15 +97,18 @@ public class BoqItemTreeServiceImpl implements BoqItemTreeService { // 1. 校验节点存在 validateBoqItemTreeExists(updateReqVO.getId()); - // 2. 校验编码唯一性 - validateCodeUnique(updateReqVO.getId(), updateReqVO.getCode()); + // 2. 校验编码唯一性(如果编码不为空) + if (updateReqVO.getCode() != null && !updateReqVO.getCode().isEmpty()) { + validateCodeUnique(updateReqVO.getId(), updateReqVO.getCode()); + } - // 3. 构建更新对象 + // 3. 构建更新对象(只更新非空字段) BoqItemTreeDO updateObj = BoqItemTreeDO.builder() .id(updateReqVO.getId()) .code(updateReqVO.getCode()) .name(updateReqVO.getName()) .unit(updateReqVO.getUnit()) + .description(updateReqVO.getDescription()) .sortOrder(updateReqVO.getSortOrder()) .attributes(updateReqVO.getAttributes()) .build(); @@ -106,16 +143,76 @@ public class BoqItemTreeServiceImpl implements BoqItemTreeService { } @Override + @Transactional(rollbackFor = Exception.class) public List getBoqItemTreeTree(Long boqCatalogItemId) { // 1. 查询所有节点 List allItems = boqItemTreeMapper.selectListByBoqCatalogItemId(boqCatalogItemId); - // 2. 转换为VO + // 2. 查找所有parentId为null的节点 + List rootNodes = allItems.stream() + .filter(item -> item.getParentId() == null) + .sorted((a, b) -> { + Integer sortA = a.getSortOrder() != null ? a.getSortOrder() : Integer.MAX_VALUE; + Integer sortB = b.getSortOrder() != null ? b.getSortOrder() : Integer.MAX_VALUE; + return sortA.compareTo(sortB); + }) + .collect(Collectors.toList()); + + BoqItemTreeDO rootNode; + + // 3. 如果没有根节点,自动创建一个 + if (rootNodes.isEmpty()) { + rootNode = BoqItemTreeDO.builder() + .boqCatalogItemId(boqCatalogItemId) + .parentId(null) + .code("") + .name("根目录") + .level(1) + .sortOrder(1) + .path(new String[]{}) // 临时空数组 + .build(); + boqItemTreeMapper.insert(rootNode); + + // 更新真实路径 + rootNode.setPath(new String[]{rootNode.getId().toString()}); + boqItemTreeMapper.updateById(rootNode); + + // 添加到列表 + allItems.add(rootNode); + } else { + // 取sortOrder最小的作为根节点 + rootNode = rootNodes.get(0); + + // 4. 如果存在多个parentId为null的节点,将其他节点挂到根节点下 + if (rootNodes.size() > 1) { + for (int i = 1; i < rootNodes.size(); i++) { + BoqItemTreeDO orphanNode = rootNodes.get(i); + orphanNode.setParentId(rootNode.getId()); + orphanNode.setLevel(rootNode.getLevel() + 1); + // 更新路径 + String[] newPath = ArrayUtil.append(rootNode.getPath(), orphanNode.getId().toString()); + orphanNode.setPath(newPath); + boqItemTreeMapper.updateById(orphanNode); + + // 更新内存中的数据 + allItems.stream() + .filter(item -> item.getId().equals(orphanNode.getId())) + .findFirst() + .ifPresent(item -> { + item.setParentId(rootNode.getId()); + item.setLevel(orphanNode.getLevel()); + item.setPath(newPath); + }); + } + } + } + + // 5. 转换为VO List allVOs = allItems.stream() .map(this::convertToRespVO) .collect(Collectors.toList()); - // 3. 构建树形结构 + // 6. 构建树形结构 return buildTree(allVOs, null); } @@ -187,37 +284,15 @@ public class BoqItemTreeServiceImpl implements BoqItemTreeService { .id(reqVO.getId()) .boqCatalogItemId(reqVO.getBoqCatalogItemId()) .parentId(reqVO.getParentId()) - .code(reqVO.getCode()) + .code(reqVO.getCode() != null ? reqVO.getCode() : "") // code不能为null .name(reqVO.getName()) - .unit(reqVO.getUnit()) + .unit(reqVO.getUnit() != null ? reqVO.getUnit() : "") // unit不能为null + .description(reqVO.getDescription()) .sortOrder(reqVO.getSortOrder()) .attributes(reqVO.getAttributes()) .build(); } - /** - * 计算路径和层级 - */ - private void calculatePathAndLevel(BoqItemTreeDO item, Long parentId, Long boqCatalogItemId) { - if (parentId == null) { - // 根节点 - item.setPath(new String[]{item.getId().toString()}); - item.setLevel(1); - } else { - // 子节点 - BoqItemTreeDO parent = boqItemTreeMapper.selectById(parentId); - if (parent == null) { - throw exception(BOQ_ITEM_TREE_PARENT_NOT_EXISTS); - } - - // 拼接路径 - String[] parentPath = parent.getPath(); - String[] newPath = ArrayUtil.append(parentPath, item.getId().toString()); - item.setPath(newPath); - item.setLevel(parent.getLevel() + 1); - } - } - /** * 获取下一个排序值 */ @@ -243,6 +318,7 @@ public class BoqItemTreeServiceImpl implements BoqItemTreeService { vo.setCode(item.getCode()); vo.setName(item.getName()); vo.setUnit(item.getUnit()); + vo.setDescription(item.getDescription()); vo.setSortOrder(item.getSortOrder()); vo.setPath(item.getPath()); vo.setLevel(item.getLevel()); @@ -283,4 +359,113 @@ public class BoqItemTreeServiceImpl implements BoqItemTreeService { } return node1.getParentId().equals(node2.getParentId()); } + + @Override + public BoqItemTreeDeleteCheckRespVO checkDeleteBoqItemTree(Long id) { + // 1. 校验节点存在 + validateBoqItemTreeExists(id); + + BoqItemTreeDeleteCheckRespVO result = new BoqItemTreeDeleteCheckRespVO(); + + // 2. 检查子节点 + List children = boqItemTreeMapper.selectListByParentId(id); + int childrenCount = children.size(); + result.setHasChildren(childrenCount > 0); + result.setChildrenCount(childrenCount); + + // 3. 递归获取所有子孙节点ID(用于检查清单子目和指引) + List allDescendantIds = new ArrayList<>(); + allDescendantIds.add(id); + collectDescendantIds(id, allDescendantIds); + + // 4. 检查清单子目 + int subItemsCount = 0; + for (Long nodeId : allDescendantIds) { + List subItems = boqSubItemMapper.selectListByBoqItemTreeId(nodeId); + subItemsCount += subItems.size(); + } + result.setHasSubItems(subItemsCount > 0); + result.setSubItemsCount(subItemsCount); + + // 5. 检查清单指引(通过清单子目关联) + int guidesCount = 0; + for (Long nodeId : allDescendantIds) { + List subItems = boqSubItemMapper.selectListByBoqItemTreeId(nodeId); + for (BoqSubItemDO subItem : subItems) { + List guides = boqGuideTreeMapper.selectListByBoqSubItemId(subItem.getId()); + guidesCount += guides.size(); + } + } + result.setHasGuides(guidesCount > 0); + result.setGuidesCount(guidesCount); + + // 6. 判断是否可以直接删除 + boolean canDirectDelete = childrenCount == 0 && subItemsCount == 0 && guidesCount == 0; + result.setCanDirectDelete(canDirectDelete); + + // 7. 生成确认提示信息 + if (canDirectDelete) { + result.setConfirmMessage("确定要删除该节点吗?"); + } else { + StringBuilder msg = new StringBuilder("该节点包含以下关联数据:\n"); + if (childrenCount > 0) { + msg.append("- 子节点:").append(childrenCount).append(" 个\n"); + } + if (subItemsCount > 0) { + msg.append("- 清单子目:").append(subItemsCount).append(" 个\n"); + } + if (guidesCount > 0) { + msg.append("- 清单指引:").append(guidesCount).append(" 个\n"); + } + msg.append("\n删除后将同时删除所有关联数据,确定要继续吗?"); + result.setConfirmMessage(msg.toString()); + } + + return result; + } + + /** + * 递归收集所有子孙节点ID + */ + private void collectDescendantIds(Long parentId, List ids) { + List children = boqItemTreeMapper.selectListByParentId(parentId); + for (BoqItemTreeDO child : children) { + ids.add(child.getId()); + collectDescendantIds(child.getId(), ids); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void forceDeleteBoqItemTree(Long id) { + // 1. 校验节点存在 + validateBoqItemTreeExists(id); + + // 2. 递归获取所有子孙节点ID + List allDescendantIds = new ArrayList<>(); + allDescendantIds.add(id); + collectDescendantIds(id, allDescendantIds); + + // 3. 从叶子节点开始删除(逆序) + for (int i = allDescendantIds.size() - 1; i >= 0; i--) { + Long nodeId = allDescendantIds.get(i); + + // 3.1 删除该节点关联的清单指引 + List subItems = boqSubItemMapper.selectListByBoqItemTreeId(nodeId); + for (BoqSubItemDO subItem : subItems) { + List guides = boqGuideTreeMapper.selectListByBoqSubItemId(subItem.getId()); + for (BoqGuideTreeDO guide : guides) { + boqGuideTreeMapper.deleteById(guide.getId()); + } + } + + // 3.2 删除该节点关联的清单子目 + for (BoqSubItemDO subItem : subItems) { + boqSubItemMapper.deleteById(subItem.getId()); + } + + // 3.3 删除该节点 + boqItemTreeMapper.deleteById(nodeId); + } + } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/impl/BoqSubItemServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/impl/BoqSubItemServiceImpl.java index 58d1b61..1725546 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/impl/BoqSubItemServiceImpl.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/boq/impl/BoqSubItemServiceImpl.java @@ -1,24 +1,31 @@ package com.yhy.module.core.service.boq.impl; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yhy.module.core.enums.ErrorCodeConstants.BOQ_CATALOG_ITEM_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.BOQ_ITEM_TREE_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.BOQ_SUB_ITEM_CODE_DUPLICATE; +import static com.yhy.module.core.enums.ErrorCodeConstants.BOQ_SUB_ITEM_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.BOQ_SUB_ITEM_NOT_SAME_TREE; + import cn.hutool.core.collection.CollUtil; import com.yhy.module.core.controller.admin.boq.vo.BoqSubItemRespVO; import com.yhy.module.core.controller.admin.boq.vo.BoqSubItemSaveReqVO; import com.yhy.module.core.controller.admin.boq.vo.BoqSubItemSwapSortReqVO; +import com.yhy.module.core.dal.dataobject.boq.BoqCatalogItemDO; +import com.yhy.module.core.dal.dataobject.boq.BoqItemTreeDO; import com.yhy.module.core.dal.dataobject.boq.BoqSubItemDO; +import com.yhy.module.core.dal.mysql.boq.BoqCatalogItemMapper; +import com.yhy.module.core.dal.mysql.boq.BoqItemTreeMapper; import com.yhy.module.core.dal.mysql.boq.BoqSubItemMapper; import com.yhy.module.core.service.boq.BoqSubItemService; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; -import javax.annotation.Resource; -import java.util.List; -import java.util.stream.Collectors; - -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static com.yhy.module.core.enums.ErrorCodeConstants.*; - /** * 清单子项 Service 实现类 * @@ -32,6 +39,12 @@ public class BoqSubItemServiceImpl implements BoqSubItemService { @Resource private BoqSubItemMapper boqSubItemMapper; + @Resource + private BoqItemTreeMapper boqItemTreeMapper; + + @Resource + private BoqCatalogItemMapper boqCatalogItemMapper; + @Override @Transactional(rollbackFor = Exception.class) public Long createBoqSubItem(BoqSubItemSaveReqVO createReqVO) { @@ -68,6 +81,7 @@ public class BoqSubItemServiceImpl implements BoqSubItemService { .name(updateReqVO.getName()) .unit(updateReqVO.getUnit()) .description(updateReqVO.getDescription()) + .features(updateReqVO.getFeatures()) .sortOrder(updateReqVO.getSortOrder()) .attributes(updateReqVO.getAttributes()) .build(); @@ -165,6 +179,7 @@ public class BoqSubItemServiceImpl implements BoqSubItemService { .name(reqVO.getName()) .unit(reqVO.getUnit()) .description(reqVO.getDescription()) + .features(reqVO.getFeatures()) .sortOrder(reqVO.getSortOrder()) .attributes(reqVO.getAttributes()) .build(); @@ -195,10 +210,37 @@ public class BoqSubItemServiceImpl implements BoqSubItemService { vo.setName(item.getName()); vo.setUnit(item.getUnit()); vo.setDescription(item.getDescription()); + vo.setFeatures(item.getFeatures()); vo.setSortOrder(item.getSortOrder()); vo.setAttributes(item.getAttributes()); vo.setCreateTime(item.getCreateTime()); vo.setUpdateTime(item.getUpdateTime()); return vo; } + + @Override + public BoqSubItemRespVO getBoqSubItemCatalogInfo(Long id) { + // 1. 查询清单子项 + BoqSubItemDO subItem = boqSubItemMapper.selectById(id); + if (subItem == null) { + throw exception(BOQ_SUB_ITEM_NOT_EXISTS); + } + + // 2. 查询清单项树 + BoqItemTreeDO itemTree = boqItemTreeMapper.selectById(subItem.getBoqItemTreeId()); + if (itemTree == null) { + throw exception(BOQ_ITEM_TREE_NOT_EXISTS); + } + + // 3. 查询清单专业 + BoqCatalogItemDO catalogItem = boqCatalogItemMapper.selectById(itemTree.getBoqCatalogItemId()); + if (catalogItem == null) { + throw exception(BOQ_CATALOG_ITEM_NOT_EXISTS); + } + + // 4. 构建返回对象,包含定额专业ID + BoqSubItemRespVO vo = convertToRespVO(subItem); + vo.setQuotaCatalogItemId(catalogItem.getQuotaCatalogItemId()); + return vo; + } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/calcbaserate/CalcBaseRateCatalogService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/calcbaserate/CalcBaseRateCatalogService.java new file mode 100644 index 0000000..de6a417 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/calcbaserate/CalcBaseRateCatalogService.java @@ -0,0 +1,58 @@ +package com.yhy.module.core.service.calcbaserate; + +import com.yhy.module.core.controller.admin.calcbaserate.vo.CalcBaseRateCatalogRespVO; +import com.yhy.module.core.controller.admin.calcbaserate.vo.CalcBaseRateCatalogSaveReqVO; +import java.util.List; +import javax.validation.Valid; + +/** + * 基数费率目录树 Service 接口 + * + * @author yhy + */ +public interface CalcBaseRateCatalogService { + + /** + * 创建基数费率目录树节点 + * + * @param createReqVO 创建信息 + * @return 节点ID + */ + Long createCalcBaseRateCatalog(@Valid CalcBaseRateCatalogSaveReqVO createReqVO); + + /** + * 更新基数费率目录树节点 + * + * @param updateReqVO 更新信息 + */ + void updateCalcBaseRateCatalog(@Valid CalcBaseRateCatalogSaveReqVO updateReqVO); + + /** + * 删除基数费率目录树节点 + * + * @param id 节点ID + */ + void deleteCalcBaseRateCatalog(Long id); + + /** + * 获取基数费率目录树节点 + * + * @param id 节点ID + * @return 节点信息 + */ + CalcBaseRateCatalogRespVO getCalcBaseRateCatalog(Long id); + + /** + * 获取基数费率目录树(树形结构) + * + * @return 树形结构 + */ + List getCalcBaseRateCatalogTree(); + + /** + * 获取基数费率目录树(工作台项目指引使用) + * + * @return 树形结构 + */ + List getCalcBaseRateCatalogTreeForWorkbench(); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/calcbaserate/CalcBaseRateDirectoryService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/calcbaserate/CalcBaseRateDirectoryService.java new file mode 100644 index 0000000..953c90c --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/calcbaserate/CalcBaseRateDirectoryService.java @@ -0,0 +1,75 @@ +package com.yhy.module.core.service.calcbaserate; + +import com.yhy.module.core.controller.admin.calcbaserate.vo.CalcBaseRateDirectoryRespVO; +import com.yhy.module.core.controller.admin.calcbaserate.vo.CalcBaseRateDirectorySaveReqVO; +import com.yhy.module.core.controller.admin.calcbaserate.vo.CalcBaseRateDirectorySwapSortReqVO; +import java.util.List; +import javax.validation.Valid; + +/** + * 基数费率目录 Service 接口 + * + * @author yhy + */ +public interface CalcBaseRateDirectoryService { + + /** + * 创建基数费率目录节点 + * + * @param createReqVO 创建信息 + * @return 节点ID + */ + Long createCalcBaseRateDirectory(@Valid CalcBaseRateDirectorySaveReqVO createReqVO); + + /** + * 更新基数费率目录节点 + * + * @param updateReqVO 更新信息 + */ + void updateCalcBaseRateDirectory(@Valid CalcBaseRateDirectorySaveReqVO updateReqVO); + + /** + * 删除基数费率目录节点 + * + * @param id 节点ID + */ + void deleteCalcBaseRateDirectory(Long id); + + /** + * 获取基数费率目录节点 + * + * @param id 节点ID + * @return 节点信息 + */ + CalcBaseRateDirectoryRespVO getCalcBaseRateDirectory(Long id); + + /** + * 获取基数费率目录树(树形结构) + * + * @param calcBaseRateCatalogId 基数费率目录树节点ID + * @return 树形结构 + */ + List getCalcBaseRateDirectoryTree(Long calcBaseRateCatalogId); + + /** + * 交换排序 + * + * @param swapReqVO 交换信息 + */ + void swapSort(@Valid CalcBaseRateDirectorySwapSortReqVO swapReqVO); + + /** + * 强制删除目录节点(级联删除所有子节点和费率项) + * + * @param id 节点ID + */ + void forceDeleteCalcBaseRateDirectory(Long id); + + /** + * 获取基数费率目录树(工作台项目指引使用) + * + * @param calcBaseRateCatalogId 基数费率目录树节点ID + * @return 树形结构 + */ + List getCalcBaseRateDirectoryTreeForWorkbench(Long calcBaseRateCatalogId); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/calcbaserate/CalcBaseRateItemService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/calcbaserate/CalcBaseRateItemService.java new file mode 100644 index 0000000..59bbb3e --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/calcbaserate/CalcBaseRateItemService.java @@ -0,0 +1,60 @@ +package com.yhy.module.core.service.calcbaserate; + +import com.yhy.module.core.controller.admin.calcbaserate.vo.CalcBaseRateItemRespVO; +import com.yhy.module.core.controller.admin.calcbaserate.vo.CalcBaseRateItemSaveReqVO; +import java.util.List; +import javax.validation.Valid; + +/** + * 基数费率项 Service 接口 + * + * @author yhy + */ +public interface CalcBaseRateItemService { + + /** + * 创建基数费率项 + * + * @param createReqVO 创建信息 + * @return 费率项ID + */ + Long createCalcBaseRateItem(@Valid CalcBaseRateItemSaveReqVO createReqVO); + + /** + * 更新基数费率项 + * + * @param updateReqVO 更新信息 + */ + void updateCalcBaseRateItem(@Valid CalcBaseRateItemSaveReqVO updateReqVO); + + /** + * 删除基数费率项 + * + * @param id 费率项ID + */ + void deleteCalcBaseRateItem(Long id); + + /** + * 获取基数费率项 + * + * @param id 费率项ID + * @return 费率项信息 + */ + CalcBaseRateItemRespVO getCalcBaseRateItem(Long id); + + /** + * 获取费率项列表 + * + * @param calcBaseRateDirectoryId 目录ID + * @return 列表 + */ + List getCalcBaseRateItemList(Long calcBaseRateDirectoryId); + + /** + * 获取费率项列表(工作台项目指引使用) + * + * @param calcBaseRateDirectoryId 目录ID + * @return 列表 + */ + List getCalcBaseRateItemListForWorkbench(Long calcBaseRateDirectoryId); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/calcbaserate/impl/CalcBaseRateCatalogServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/calcbaserate/impl/CalcBaseRateCatalogServiceImpl.java new file mode 100644 index 0000000..9e4fd17 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/calcbaserate/impl/CalcBaseRateCatalogServiceImpl.java @@ -0,0 +1,281 @@ +package com.yhy.module.core.service.calcbaserate.impl; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yhy.module.core.enums.ErrorCodeConstants.CALC_BASE_RATE_CATALOG_CODE_DUPLICATE; +import static com.yhy.module.core.enums.ErrorCodeConstants.CALC_BASE_RATE_CATALOG_HAS_CHILDREN; +import static com.yhy.module.core.enums.ErrorCodeConstants.CALC_BASE_RATE_CATALOG_INVALID_NODE_TYPE; +import static com.yhy.module.core.enums.ErrorCodeConstants.CALC_BASE_RATE_CATALOG_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.CALC_BASE_RATE_CATALOG_PARENT_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.CALC_BASE_RATE_CATALOG_ROOT_EXISTS; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ArrayUtil; +import com.yhy.module.core.controller.admin.calcbaserate.vo.CalcBaseRateCatalogRespVO; +import com.yhy.module.core.controller.admin.calcbaserate.vo.CalcBaseRateCatalogSaveReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.calcbaserate.CalcBaseRateCatalogTreeRespVO; +import com.yhy.module.core.dal.dataobject.calcbaserate.CalcBaseRateCatalogDO; +import com.yhy.module.core.dal.mysql.calcbaserate.CalcBaseRateCatalogMapper; +import com.yhy.module.core.service.calcbaserate.CalcBaseRateCatalogService; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * 基数费率目录树 Service 实现类 + * + * @author yhy + */ +@Service +@Validated +@Slf4j +public class CalcBaseRateCatalogServiceImpl implements CalcBaseRateCatalogService { + + @Resource + private CalcBaseRateCatalogMapper calcBaseRateCatalogMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createCalcBaseRateCatalog(CalcBaseRateCatalogSaveReqVO createReqVO) { + // 1. 校验编码唯一性 + validateCodeUnique(null, createReqVO.getCode()); + + // 2. 校验节点类型 + validateNodeType(createReqVO.getNodeType()); + + // 3. 校验根节点唯一性 + if (CalcBaseRateCatalogDO.NODE_TYPE_ROOT.equals(createReqVO.getNodeType())) { + validateRootUnique(); + } + + // 4. 构建DO对象 + CalcBaseRateCatalogDO calcBaseRateCatalog = buildCalcBaseRateCatalogDO(createReqVO); + + // 5. 设置排序 + if (calcBaseRateCatalog.getSortOrder() == null) { + calcBaseRateCatalog.setSortOrder(getNextSortOrder(createReqVO.getParentId())); + } + + // 6. 预设level和临时path + CalcBaseRateCatalogDO parent = null; + if (createReqVO.getParentId() != null) { + parent = calcBaseRateCatalogMapper.selectById(createReqVO.getParentId()); + if (parent == null) { + throw exception(CALC_BASE_RATE_CATALOG_PARENT_NOT_EXISTS); + } + calcBaseRateCatalog.setLevel(parent.getLevel() + 1); + calcBaseRateCatalog.setPath(parent.getPath()); + } else { + calcBaseRateCatalog.setLevel(0); + calcBaseRateCatalog.setPath(new String[]{}); + } + + // 7. 插入数据库获取ID + calcBaseRateCatalogMapper.insert(calcBaseRateCatalog); + + // 8. 更新真实路径 + String[] realPath = parent == null + ? new String[]{calcBaseRateCatalog.getId().toString()} + : ArrayUtil.append(parent.getPath(), calcBaseRateCatalog.getId().toString()); + calcBaseRateCatalog.setPath(realPath); + calcBaseRateCatalogMapper.updateById(calcBaseRateCatalog); + + return calcBaseRateCatalog.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateCalcBaseRateCatalog(CalcBaseRateCatalogSaveReqVO updateReqVO) { + // 1. 校验节点存在 + validateCalcBaseRateCatalogExists(updateReqVO.getId()); + + // 2. 校验编码唯一性 + if (updateReqVO.getCode() != null) { + validateCodeUnique(updateReqVO.getId(), updateReqVO.getCode()); + } + + // 3. 校验节点类型 + if (updateReqVO.getNodeType() != null) { + validateNodeType(updateReqVO.getNodeType()); + } + + // 4. 构建更新对象 + CalcBaseRateCatalogDO updateObj = CalcBaseRateCatalogDO.builder() + .id(updateReqVO.getId()) + .code(updateReqVO.getCode()) + .name(updateReqVO.getName()) + .nodeType(updateReqVO.getNodeType()) + .sortOrder(updateReqVO.getSortOrder()) + .attributes(updateReqVO.getAttributes()) + .build(); + + // 5. 更新数据库 + calcBaseRateCatalogMapper.updateById(updateObj); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteCalcBaseRateCatalog(Long id) { + // 1. 校验节点存在 + validateCalcBaseRateCatalogExists(id); + + // 2. 校验是否有子节点 + List children = calcBaseRateCatalogMapper.selectListByParentId(id); + if (CollUtil.isNotEmpty(children)) { + throw exception(CALC_BASE_RATE_CATALOG_HAS_CHILDREN); + } + + // 3. 删除节点 + calcBaseRateCatalogMapper.deleteById(id); + } + + @Override + public CalcBaseRateCatalogRespVO getCalcBaseRateCatalog(Long id) { + CalcBaseRateCatalogDO item = calcBaseRateCatalogMapper.selectById(id); + if (item == null) { + return null; + } + return convertToRespVO(item); + } + + @Override + public List getCalcBaseRateCatalogTree() { + // 1. 查询所有节点 + List allItems = calcBaseRateCatalogMapper.selectList(); + + // 2. 转换为VO + List allVOs = allItems.stream() + .map(this::convertToRespVO) + .collect(Collectors.toList()); + + // 3. 构建树形结构 + return buildTree(allVOs, null); + } + + // ==================== 私有方法 ==================== + + private void validateCodeUnique(Long id, String code) { + CalcBaseRateCatalogDO existing = calcBaseRateCatalogMapper.selectByCode(code); + if (existing != null && !existing.getId().equals(id)) { + throw exception(CALC_BASE_RATE_CATALOG_CODE_DUPLICATE); + } + } + + private void validateNodeType(String nodeType) { + List validTypes = Arrays.asList( + CalcBaseRateCatalogDO.NODE_TYPE_ROOT, + CalcBaseRateCatalogDO.NODE_TYPE_PROVINCE, + CalcBaseRateCatalogDO.NODE_TYPE_CONTENT + ); + if (!validTypes.contains(nodeType)) { + throw exception(CALC_BASE_RATE_CATALOG_INVALID_NODE_TYPE); + } + } + + private void validateRootUnique() { + CalcBaseRateCatalogDO existingRoot = calcBaseRateCatalogMapper.selectByNodeType(CalcBaseRateCatalogDO.NODE_TYPE_ROOT); + if (existingRoot != null) { + throw exception(CALC_BASE_RATE_CATALOG_ROOT_EXISTS); + } + } + + private CalcBaseRateCatalogDO validateCalcBaseRateCatalogExists(Long id) { + CalcBaseRateCatalogDO item = calcBaseRateCatalogMapper.selectById(id); + if (item == null) { + throw exception(CALC_BASE_RATE_CATALOG_NOT_EXISTS); + } + return item; + } + + private CalcBaseRateCatalogDO buildCalcBaseRateCatalogDO(CalcBaseRateCatalogSaveReqVO reqVO) { + return CalcBaseRateCatalogDO.builder() + .id(reqVO.getId()) + .parentId(reqVO.getParentId()) + .code(reqVO.getCode()) + .name(reqVO.getName()) + .nodeType(reqVO.getNodeType()) + .sortOrder(reqVO.getSortOrder()) + .attributes(reqVO.getAttributes()) + .build(); + } + + private Integer getNextSortOrder(Long parentId) { + List siblings = calcBaseRateCatalogMapper.selectSiblings(parentId); + if (CollUtil.isEmpty(siblings)) { + return 1; + } + return siblings.stream() + .mapToInt(CalcBaseRateCatalogDO::getSortOrder) + .max() + .orElse(0) + 1; + } + + private CalcBaseRateCatalogRespVO convertToRespVO(CalcBaseRateCatalogDO item) { + CalcBaseRateCatalogRespVO vo = new CalcBaseRateCatalogRespVO(); + vo.setId(item.getId()); + vo.setParentId(item.getParentId()); + vo.setCode(item.getCode()); + vo.setName(item.getName()); + vo.setNodeType(item.getNodeType()); + vo.setSortOrder(item.getSortOrder()); + vo.setPath(item.getPath()); + vo.setLevel(item.getLevel()); + vo.setAttributes(item.getAttributes()); + vo.setCreateTime(item.getCreateTime()); + vo.setUpdateTime(item.getUpdateTime()); + return vo; + } + + @Override + public List getCalcBaseRateCatalogTreeForWorkbench() { + // 1. 查询所有节点 + List allItems = calcBaseRateCatalogMapper.selectList(); + + // 2. 转换为工作台VO + List allVOs = allItems.stream() + .map(this::convertToWorkbenchRespVO) + .collect(Collectors.toList()); + + // 3. 构建树形结构 + return buildWorkbenchTree(allVOs, null); + } + + private List buildTree(List allNodes, Long parentId) { + List result = new ArrayList<>(); + for (CalcBaseRateCatalogRespVO node : allNodes) { + if ((parentId == null && node.getParentId() == null) + || (parentId != null && parentId.equals(node.getParentId()))) { + node.setChildren(buildTree(allNodes, node.getId())); + result.add(node); + } + } + return result; + } + + private CalcBaseRateCatalogTreeRespVO convertToWorkbenchRespVO(CalcBaseRateCatalogDO item) { + CalcBaseRateCatalogTreeRespVO vo = new CalcBaseRateCatalogTreeRespVO(); + vo.setId(item.getId()); + vo.setParentId(item.getParentId()); + vo.setCode(item.getCode()); + vo.setName(item.getName()); + vo.setNodeType(item.getNodeType()); + return vo; + } + + private List buildWorkbenchTree(List allNodes, Long parentId) { + List result = new ArrayList<>(); + for (CalcBaseRateCatalogTreeRespVO node : allNodes) { + if ((parentId == null && node.getParentId() == null) + || (parentId != null && parentId.equals(node.getParentId()))) { + node.setChildren(buildWorkbenchTree(allNodes, node.getId())); + result.add(node); + } + } + return result; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/calcbaserate/impl/CalcBaseRateDirectoryServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/calcbaserate/impl/CalcBaseRateDirectoryServiceImpl.java new file mode 100644 index 0000000..7b9795d --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/calcbaserate/impl/CalcBaseRateDirectoryServiceImpl.java @@ -0,0 +1,362 @@ +package com.yhy.module.core.service.calcbaserate.impl; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yhy.module.core.enums.ErrorCodeConstants.CALC_BASE_RATE_DIRECTORY_HAS_CHILDREN; +import static com.yhy.module.core.enums.ErrorCodeConstants.CALC_BASE_RATE_DIRECTORY_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.CALC_BASE_RATE_DIRECTORY_NOT_SAME_LEVEL; +import static com.yhy.module.core.enums.ErrorCodeConstants.CALC_BASE_RATE_DIRECTORY_PARENT_NOT_EXISTS; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ArrayUtil; +import com.yhy.module.core.controller.admin.calcbaserate.vo.CalcBaseRateDirectoryRespVO; +import com.yhy.module.core.controller.admin.calcbaserate.vo.CalcBaseRateDirectorySaveReqVO; +import com.yhy.module.core.controller.admin.calcbaserate.vo.CalcBaseRateDirectorySwapSortReqVO; +import com.yhy.module.core.dal.dataobject.calcbaserate.CalcBaseRateDirectoryDO; +import com.yhy.module.core.dal.dataobject.calcbaserate.CalcBaseRateItemDO; +import com.yhy.module.core.dal.mysql.calcbaserate.CalcBaseRateDirectoryMapper; +import com.yhy.module.core.dal.mysql.calcbaserate.CalcBaseRateItemMapper; +import com.yhy.module.core.service.calcbaserate.CalcBaseRateDirectoryService; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * 基数费率目录 Service 实现类 + * + * @author yhy + */ +@Service +@Validated +@Slf4j +public class CalcBaseRateDirectoryServiceImpl implements CalcBaseRateDirectoryService { + + @Resource + private CalcBaseRateDirectoryMapper calcBaseRateDirectoryMapper; + + @Resource + private CalcBaseRateItemMapper calcBaseRateItemMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createCalcBaseRateDirectory(CalcBaseRateDirectorySaveReqVO createReqVO) { + // 1. 构建DO对象 + CalcBaseRateDirectoryDO directory = buildCalcBaseRateDirectoryDO(createReqVO); + + // 2. 设置排序 + if (directory.getSortOrder() == null) { + directory.setSortOrder(getNextSortOrder(createReqVO.getParentId(), createReqVO.getCalcBaseRateCatalogId())); + } + + // 3. 预设level和临时path + CalcBaseRateDirectoryDO parent = null; + if (createReqVO.getParentId() != null) { + parent = calcBaseRateDirectoryMapper.selectById(createReqVO.getParentId()); + if (parent == null) { + throw exception(CALC_BASE_RATE_DIRECTORY_PARENT_NOT_EXISTS); + } + directory.setLevel(parent.getLevel() + 1); + directory.setPath(parent.getPath()); + } else { + directory.setLevel(1); + directory.setPath(new String[]{}); + } + + // 4. 插入数据库获取ID + calcBaseRateDirectoryMapper.insert(directory); + + // 5. 更新真实路径 + String[] realPath = parent == null + ? new String[]{directory.getId().toString()} + : ArrayUtil.append(parent.getPath(), directory.getId().toString()); + directory.setPath(realPath); + calcBaseRateDirectoryMapper.updateById(directory); + + return directory.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateCalcBaseRateDirectory(CalcBaseRateDirectorySaveReqVO updateReqVO) { + // 1. 校验节点存在 + validateCalcBaseRateDirectoryExists(updateReqVO.getId()); + + // 2. 构建更新对象 + CalcBaseRateDirectoryDO updateObj = CalcBaseRateDirectoryDO.builder() + .id(updateReqVO.getId()) + .name(updateReqVO.getName()) + .sortOrder(updateReqVO.getSortOrder()) + .attributes(updateReqVO.getAttributes()) + .build(); + + // 3. 更新数据库 + calcBaseRateDirectoryMapper.updateById(updateObj); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteCalcBaseRateDirectory(Long id) { + // 1. 校验节点存在 + validateCalcBaseRateDirectoryExists(id); + + // 2. 校验是否有子节点 + List children = calcBaseRateDirectoryMapper.selectListByParentId(id); + if (CollUtil.isNotEmpty(children)) { + throw exception(CALC_BASE_RATE_DIRECTORY_HAS_CHILDREN); + } + + // 3. 删除节点 + calcBaseRateDirectoryMapper.deleteById(id); + } + + @Override + public CalcBaseRateDirectoryRespVO getCalcBaseRateDirectory(Long id) { + CalcBaseRateDirectoryDO item = calcBaseRateDirectoryMapper.selectById(id); + if (item == null) { + return null; + } + return convertToRespVO(item); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public List getCalcBaseRateDirectoryTree(Long calcBaseRateCatalogId) { + // 1. 查询所有节点 + List allItems = calcBaseRateDirectoryMapper.selectListByCalcBaseRateCatalogId(calcBaseRateCatalogId); + + // 2. 查找所有parentId为null的节点 + List rootNodes = allItems.stream() + .filter(item -> item.getParentId() == null) + .sorted((a, b) -> { + Integer sortA = a.getSortOrder() != null ? a.getSortOrder() : Integer.MAX_VALUE; + Integer sortB = b.getSortOrder() != null ? b.getSortOrder() : Integer.MAX_VALUE; + return sortA.compareTo(sortB); + }) + .collect(Collectors.toList()); + + CalcBaseRateDirectoryDO rootNode; + + // 3. 如果没有根节点,自动创建一个 + if (rootNodes.isEmpty()) { + rootNode = CalcBaseRateDirectoryDO.builder() + .calcBaseRateCatalogId(calcBaseRateCatalogId) + .parentId(null) + .name("根目录") + .level(1) + .sortOrder(1) + .path(new String[]{}) + .build(); + calcBaseRateDirectoryMapper.insert(rootNode); + + // 更新真实路径 + rootNode.setPath(new String[]{rootNode.getId().toString()}); + calcBaseRateDirectoryMapper.updateById(rootNode); + + allItems.add(rootNode); + } else { + rootNode = rootNodes.get(0); + + // 4. 如果存在多个parentId为null的节点,将其他节点挂到根节点下 + if (rootNodes.size() > 1) { + for (int i = 1; i < rootNodes.size(); i++) { + CalcBaseRateDirectoryDO orphanNode = rootNodes.get(i); + orphanNode.setParentId(rootNode.getId()); + orphanNode.setLevel(rootNode.getLevel() + 1); + String[] newPath = ArrayUtil.append(rootNode.getPath(), orphanNode.getId().toString()); + orphanNode.setPath(newPath); + calcBaseRateDirectoryMapper.updateById(orphanNode); + + allItems.stream() + .filter(item -> item.getId().equals(orphanNode.getId())) + .findFirst() + .ifPresent(item -> { + item.setParentId(rootNode.getId()); + item.setLevel(orphanNode.getLevel()); + item.setPath(newPath); + }); + } + } + } + + // 5. 转换为VO + List allVOs = allItems.stream() + .map(this::convertToRespVO) + .collect(Collectors.toList()); + + // 6. 构建树形结构 + return buildTree(allVOs, null); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void swapSort(CalcBaseRateDirectorySwapSortReqVO swapReqVO) { + // 1. 查询两个节点 + CalcBaseRateDirectoryDO node1 = calcBaseRateDirectoryMapper.selectById(swapReqVO.getNodeId1()); + CalcBaseRateDirectoryDO node2 = calcBaseRateDirectoryMapper.selectById(swapReqVO.getNodeId2()); + + if (node1 == null || node2 == null) { + throw exception(CALC_BASE_RATE_DIRECTORY_NOT_EXISTS); + } + + // 2. 校验是否同级 + if (!isSameLevel(node1, node2)) { + throw exception(CALC_BASE_RATE_DIRECTORY_NOT_SAME_LEVEL); + } + + // 3. 交换排序值 + Integer tempSort = node1.getSortOrder(); + node1.setSortOrder(node2.getSortOrder()); + node2.setSortOrder(tempSort); + + // 4. 更新数据库 + calcBaseRateDirectoryMapper.updateById(node1); + calcBaseRateDirectoryMapper.updateById(node2); + + log.info("[swapSort] 交换目录节点[{}]和节点[{}]的排序成功", swapReqVO.getNodeId1(), swapReqVO.getNodeId2()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void forceDeleteCalcBaseRateDirectory(Long id) { + // 1. 校验节点存在 + validateCalcBaseRateDirectoryExists(id); + + // 2. 递归获取所有子孙节点ID + List allDescendantIds = new ArrayList<>(); + allDescendantIds.add(id); + collectDescendantIds(id, allDescendantIds); + + // 3. 从叶子节点开始删除(逆序) + for (int i = allDescendantIds.size() - 1; i >= 0; i--) { + Long nodeId = allDescendantIds.get(i); + + // 3.1 删除该节点关联的费率项 + List items = calcBaseRateItemMapper.selectListByCalcBaseRateDirectoryId(nodeId); + for (CalcBaseRateItemDO item : items) { + calcBaseRateItemMapper.deleteById(item.getId()); + } + + // 3.2 删除该节点 + calcBaseRateDirectoryMapper.deleteById(nodeId); + } + } + + // ==================== 私有方法 ==================== + + private CalcBaseRateDirectoryDO validateCalcBaseRateDirectoryExists(Long id) { + CalcBaseRateDirectoryDO item = calcBaseRateDirectoryMapper.selectById(id); + if (item == null) { + throw exception(CALC_BASE_RATE_DIRECTORY_NOT_EXISTS); + } + return item; + } + + private CalcBaseRateDirectoryDO buildCalcBaseRateDirectoryDO(CalcBaseRateDirectorySaveReqVO reqVO) { + return CalcBaseRateDirectoryDO.builder() + .id(reqVO.getId()) + .calcBaseRateCatalogId(reqVO.getCalcBaseRateCatalogId()) + .parentId(reqVO.getParentId()) + .name(reqVO.getName()) + .sortOrder(reqVO.getSortOrder()) + .attributes(reqVO.getAttributes()) + .build(); + } + + private Integer getNextSortOrder(Long parentId, Long calcBaseRateCatalogId) { + List siblings = calcBaseRateDirectoryMapper.selectSiblings(parentId, calcBaseRateCatalogId); + if (CollUtil.isEmpty(siblings)) { + return 1; + } + return siblings.stream() + .mapToInt(CalcBaseRateDirectoryDO::getSortOrder) + .max() + .orElse(0) + 1; + } + + private CalcBaseRateDirectoryRespVO convertToRespVO(CalcBaseRateDirectoryDO item) { + CalcBaseRateDirectoryRespVO vo = new CalcBaseRateDirectoryRespVO(); + vo.setId(item.getId()); + vo.setCalcBaseRateCatalogId(item.getCalcBaseRateCatalogId()); + vo.setParentId(item.getParentId()); + vo.setName(item.getName()); + vo.setSortOrder(item.getSortOrder()); + vo.setPath(item.getPath()); + vo.setLevel(item.getLevel()); + vo.setAttributes(item.getAttributes()); + vo.setCreateTime(item.getCreateTime()); + vo.setUpdateTime(item.getUpdateTime()); + return vo; + } + + private List buildTree(List allNodes, Long parentId) { + List result = new ArrayList<>(); + for (CalcBaseRateDirectoryRespVO node : allNodes) { + if ((parentId == null && node.getParentId() == null) + || (parentId != null && parentId.equals(node.getParentId()))) { + node.setChildren(buildTree(allNodes, node.getId())); + result.add(node); + } + } + return result; + } + + private boolean isSameLevel(CalcBaseRateDirectoryDO node1, CalcBaseRateDirectoryDO node2) { + if (!node1.getCalcBaseRateCatalogId().equals(node2.getCalcBaseRateCatalogId())) { + return false; + } + if (node1.getParentId() == null && node2.getParentId() == null) { + return true; + } + if (node1.getParentId() == null || node2.getParentId() == null) { + return false; + } + return node1.getParentId().equals(node2.getParentId()); + } + + private void collectDescendantIds(Long parentId, List ids) { + List children = calcBaseRateDirectoryMapper.selectListByParentId(parentId); + for (CalcBaseRateDirectoryDO child : children) { + ids.add(child.getId()); + collectDescendantIds(child.getId(), ids); + } + } + + @Override + public List getCalcBaseRateDirectoryTreeForWorkbench(Long calcBaseRateCatalogId) { + // 1. 查询所有节点 + List allItems = calcBaseRateDirectoryMapper.selectListByCalcBaseRateCatalogId(calcBaseRateCatalogId); + + // 2. 转换为工作台VO + List allVOs = allItems.stream() + .map(this::convertToWorkbenchRespVO) + .collect(Collectors.toList()); + + // 3. 构建树形结构 + return buildWorkbenchTree(allVOs, null); + } + + private com.yhy.module.core.controller.admin.workbench.vo.calcbaserate.CalcBaseRateDirectoryRespVO convertToWorkbenchRespVO(CalcBaseRateDirectoryDO item) { + com.yhy.module.core.controller.admin.workbench.vo.calcbaserate.CalcBaseRateDirectoryRespVO vo = new com.yhy.module.core.controller.admin.workbench.vo.calcbaserate.CalcBaseRateDirectoryRespVO(); + vo.setId(item.getId()); + vo.setParentId(item.getParentId()); + vo.setName(item.getName()); + vo.setSortOrder(item.getSortOrder()); + return vo; + } + + private List buildWorkbenchTree(List allNodes, Long parentId) { + List result = new ArrayList<>(); + for (com.yhy.module.core.controller.admin.workbench.vo.calcbaserate.CalcBaseRateDirectoryRespVO node : allNodes) { + if ((parentId == null && node.getParentId() == null) + || (parentId != null && parentId.equals(node.getParentId()))) { + node.setChildren(buildWorkbenchTree(allNodes, node.getId())); + result.add(node); + } + } + return result; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/calcbaserate/impl/CalcBaseRateItemServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/calcbaserate/impl/CalcBaseRateItemServiceImpl.java new file mode 100644 index 0000000..fb7ddc8 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/calcbaserate/impl/CalcBaseRateItemServiceImpl.java @@ -0,0 +1,163 @@ +package com.yhy.module.core.service.calcbaserate.impl; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yhy.module.core.enums.ErrorCodeConstants.CALC_BASE_RATE_ITEM_NOT_EXISTS; + +import cn.hutool.core.collection.CollUtil; +import com.yhy.module.core.controller.admin.calcbaserate.vo.CalcBaseRateItemRespVO; +import com.yhy.module.core.controller.admin.calcbaserate.vo.CalcBaseRateItemSaveReqVO; +import com.yhy.module.core.dal.dataobject.calcbaserate.CalcBaseRateItemDO; +import com.yhy.module.core.dal.mysql.calcbaserate.CalcBaseRateItemMapper; +import com.yhy.module.core.service.calcbaserate.CalcBaseRateItemService; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * 基数费率项 Service 实现类 + * + * @author yhy + */ +@Service +@Validated +@Slf4j +public class CalcBaseRateItemServiceImpl implements CalcBaseRateItemService { + + @Resource + private CalcBaseRateItemMapper calcBaseRateItemMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createCalcBaseRateItem(CalcBaseRateItemSaveReqVO createReqVO) { + // 1. 构建DO对象 + CalcBaseRateItemDO item = buildCalcBaseRateItemDO(createReqVO); + + // 2. 设置排序:如果指定了 sortOrder,先将 >= 该值的现有记录 +1(实现"上方/下方插入") + if (item.getSortOrder() != null) { + calcBaseRateItemMapper.shiftSortOrder(createReqVO.getCalcBaseRateDirectoryId(), item.getSortOrder()); + } else { + item.setSortOrder(getNextSortOrder(createReqVO.getCalcBaseRateDirectoryId())); + } + + // 3. 插入数据库 + calcBaseRateItemMapper.insert(item); + + return item.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateCalcBaseRateItem(CalcBaseRateItemSaveReqVO updateReqVO) { + // 1. 校验存在 + validateCalcBaseRateItemExists(updateReqVO.getId()); + + // 2. 构建更新对象 + CalcBaseRateItemDO updateObj = CalcBaseRateItemDO.builder() + .id(updateReqVO.getId()) + .name(updateReqVO.getName()) + .rate(updateReqVO.getRate()) + .remark(updateReqVO.getRemark()) + .sortOrder(updateReqVO.getSortOrder()) + .attributes(updateReqVO.getAttributes()) + .build(); + + // 3. 更新数据库 + calcBaseRateItemMapper.updateById(updateObj); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteCalcBaseRateItem(Long id) { + // 1. 校验存在 + validateCalcBaseRateItemExists(id); + + // 2. 删除 + calcBaseRateItemMapper.deleteById(id); + } + + @Override + public CalcBaseRateItemRespVO getCalcBaseRateItem(Long id) { + CalcBaseRateItemDO item = calcBaseRateItemMapper.selectById(id); + if (item == null) { + return null; + } + return convertToRespVO(item); + } + + @Override + public List getCalcBaseRateItemList(Long calcBaseRateDirectoryId) { + List items = calcBaseRateItemMapper.selectListByCalcBaseRateDirectoryId(calcBaseRateDirectoryId); + return items.stream() + .map(this::convertToRespVO) + .collect(Collectors.toList()); + } + + @Override + public List getCalcBaseRateItemListForWorkbench(Long calcBaseRateDirectoryId) { + List items = calcBaseRateItemMapper.selectListByCalcBaseRateDirectoryId(calcBaseRateDirectoryId); + return items.stream() + .map(this::convertToWorkbenchRespVO) + .collect(Collectors.toList()); + } + + private com.yhy.module.core.controller.admin.workbench.vo.calcbaserate.CalcBaseRateItemRespVO convertToWorkbenchRespVO(CalcBaseRateItemDO item) { + com.yhy.module.core.controller.admin.workbench.vo.calcbaserate.CalcBaseRateItemRespVO vo = new com.yhy.module.core.controller.admin.workbench.vo.calcbaserate.CalcBaseRateItemRespVO(); + vo.setId(item.getId()); + vo.setName(item.getName()); + vo.setRate(item.getRate()); + vo.setRemark(item.getRemark()); + vo.setSortOrder(item.getSortOrder()); + return vo; + } + + // ==================== 私有方法 ==================== + + private CalcBaseRateItemDO validateCalcBaseRateItemExists(Long id) { + CalcBaseRateItemDO item = calcBaseRateItemMapper.selectById(id); + if (item == null) { + throw exception(CALC_BASE_RATE_ITEM_NOT_EXISTS); + } + return item; + } + + private CalcBaseRateItemDO buildCalcBaseRateItemDO(CalcBaseRateItemSaveReqVO reqVO) { + return CalcBaseRateItemDO.builder() + .id(reqVO.getId()) + .calcBaseRateDirectoryId(reqVO.getCalcBaseRateDirectoryId()) + .name(reqVO.getName()) + .rate(reqVO.getRate()) + .remark(reqVO.getRemark()) + .sortOrder(reqVO.getSortOrder()) + .attributes(reqVO.getAttributes()) + .build(); + } + + private Integer getNextSortOrder(Long calcBaseRateDirectoryId) { + List items = calcBaseRateItemMapper.selectListByCalcBaseRateDirectoryId(calcBaseRateDirectoryId); + if (CollUtil.isEmpty(items)) { + return 1; + } + return items.stream() + .mapToInt(CalcBaseRateItemDO::getSortOrder) + .max() + .orElse(0) + 1; + } + + private CalcBaseRateItemRespVO convertToRespVO(CalcBaseRateItemDO item) { + CalcBaseRateItemRespVO vo = new CalcBaseRateItemRespVO(); + vo.setId(item.getId()); + vo.setCalcBaseRateDirectoryId(item.getCalcBaseRateDirectoryId()); + vo.setName(item.getName()); + vo.setRate(item.getRate()); + vo.setRemark(item.getRemark()); + vo.setSortOrder(item.getSortOrder()); + vo.setAttributes(item.getAttributes()); + vo.setCreateTime(item.getCreateTime()); + vo.setUpdateTime(item.getUpdateTime()); + return vo; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/config/ConfigProjectInfoService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/config/ConfigProjectInfoService.java new file mode 100644 index 0000000..c996a78 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/config/ConfigProjectInfoService.java @@ -0,0 +1,60 @@ +package com.yhy.module.core.service.config; + +import com.yhy.module.core.controller.admin.config.vo.ConfigProjectInfoRespVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigProjectInfoSaveReqVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigProjectInfoSwapSortReqVO; +import java.util.List; +import javax.validation.Valid; + +/** + * 工程信息配置 Service 接口 + * + * @author yhy + */ +public interface ConfigProjectInfoService { + + /** + * 创建工程信息 + * + * @param createReqVO 创建信息 + * @return ID + */ + Long createConfigProjectInfo(@Valid ConfigProjectInfoSaveReqVO createReqVO); + + /** + * 更新工程信息 + * + * @param updateReqVO 更新信息 + */ + void updateConfigProjectInfo(@Valid ConfigProjectInfoSaveReqVO updateReqVO); + + /** + * 删除工程信息 + * + * @param id ID + */ + void deleteConfigProjectInfo(Long id); + + /** + * 获取工程信息详情 + * + * @param id ID + * @return 工程信息 + */ + ConfigProjectInfoRespVO getConfigProjectInfo(Long id); + + /** + * 获取工程信息树(按行业节点) + * + * @param configTreeId 行业节点ID + * @return 树形结构 + */ + List getConfigProjectInfoTree(Long configTreeId); + + /** + * 交换排序 + * + * @param swapReqVO 交换信息 + */ + void swapSort(@Valid ConfigProjectInfoSwapSortReqVO swapReqVO); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/config/ConfigProjectTreeService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/config/ConfigProjectTreeService.java new file mode 100644 index 0000000..dc37dee --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/config/ConfigProjectTreeService.java @@ -0,0 +1,66 @@ +package com.yhy.module.core.service.config; + +import com.yhy.module.core.controller.admin.config.vo.ConfigProjectTreeRespVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigProjectTreeSaveReqVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigProjectTreeSwapSortReqVO; +import java.util.List; +import javax.validation.Valid; + +/** + * 项目界面配置树 Service 接口 + * + * @author yhy + */ +public interface ConfigProjectTreeService { + + /** + * 创建节点 + * + * @param createReqVO 创建信息 + * @return 节点ID + */ + Long createConfigProjectTree(@Valid ConfigProjectTreeSaveReqVO createReqVO); + + /** + * 更新节点 + * + * @param updateReqVO 更新信息 + */ + void updateConfigProjectTree(@Valid ConfigProjectTreeSaveReqVO updateReqVO); + + /** + * 删除节点 + * + * @param id 节点ID + */ + void deleteConfigProjectTree(Long id); + + /** + * 获取节点详情 + * + * @param id 节点ID + * @return 节点信息 + */ + ConfigProjectTreeRespVO getConfigProjectTree(Long id); + + /** + * 获取树形结构 + * + * @return 树形结构 + */ + List getConfigProjectTreeList(); + + /** + * 交换排序 + * + * @param swapReqVO 交换信息 + */ + void swapSort(@Valid ConfigProjectTreeSwapSortReqVO swapReqVO); + + /** + * 获取行业下拉选项(两级联动:省市 -> 行业) + * + * @return 省市列表(包含行业子节点) + */ + List getIndustryOptions(); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/config/ConfigUnitDivisionTemplateService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/config/ConfigUnitDivisionTemplateService.java new file mode 100644 index 0000000..3fe0e06 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/config/ConfigUnitDivisionTemplateService.java @@ -0,0 +1,24 @@ +package com.yhy.module.core.service.config; + +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitDivisionTemplateRespVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitDivisionTemplateSaveReqVO; +import java.util.List; +import javax.validation.Valid; + +/** + * 单位工程界面配置 - 分部分项模板 Service + * 四个标签页(分部分项/措施项目/其他项目/单位汇总)各自独立维护模板数据 + */ +public interface ConfigUnitDivisionTemplateService { + + Long create(@Valid ConfigUnitDivisionTemplateSaveReqVO createReqVO); + + void update(@Valid ConfigUnitDivisionTemplateSaveReqVO updateReqVO); + + void delete(Long id); + + /** 按标签页类型获取树结构 */ + List getTree(Long catalogItemId, String tabType); + + void swapSort(Long nodeId1, Long nodeId2); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/config/ConfigUnitFieldService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/config/ConfigUnitFieldService.java new file mode 100644 index 0000000..b606a44 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/config/ConfigUnitFieldService.java @@ -0,0 +1,28 @@ +package com.yhy.module.core.service.config; + +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitFieldHiddenFieldsRespVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitFieldRespVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitFieldSaveReqVO; +import java.util.List; +import javax.validation.Valid; + +/** + * 单位工程界面配置 - 工作台字段设置 Service + */ +public interface ConfigUnitFieldService { + + Long create(@Valid ConfigUnitFieldSaveReqVO createReqVO); + + void batchCreate(@Valid List createReqVOs); + + void update(@Valid ConfigUnitFieldSaveReqVO updateReqVO); + + void delete(Long id); + + List getList(Long catalogItemId); + + void swapSort(Long nodeId1, Long nodeId2); + + /** 供工作台调用:获取各标签页的隐藏字段编码列表 */ + ConfigUnitFieldHiddenFieldsRespVO getHiddenFields(Long catalogItemId); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/config/ConfigUnitResourceFieldService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/config/ConfigUnitResourceFieldService.java new file mode 100644 index 0000000..8a20b67 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/config/ConfigUnitResourceFieldService.java @@ -0,0 +1,27 @@ +package com.yhy.module.core.service.config; + +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitResourceFieldRespVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitResourceFieldSaveReqVO; +import java.util.List; +import javax.validation.Valid; + +/** + * 单位工程界面配置 - 工料机字段 Service + */ +public interface ConfigUnitResourceFieldService { + + Long create(@Valid ConfigUnitResourceFieldSaveReqVO createReqVO); + + void batchCreate(@Valid List createReqVOs); + + void update(@Valid ConfigUnitResourceFieldSaveReqVO updateReqVO); + + void delete(Long id); + + List getList(Long catalogItemId); + + void swapSort(Long nodeId1, Long nodeId2); + + /** 供工作台调用:获取隐藏的字段编码列表 */ + List getHiddenFields(Long catalogItemId); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/config/ConfigUnitTabRefService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/config/ConfigUnitTabRefService.java new file mode 100644 index 0000000..c4f9785 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/config/ConfigUnitTabRefService.java @@ -0,0 +1,22 @@ +package com.yhy.module.core.service.config; + +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitTabRefRespVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitTabRefSaveReqVO; +import java.util.List; +import javax.validation.Valid; + +/** + * 单位工程界面配置 - 标签页引用 Service + */ +public interface ConfigUnitTabRefService { + + /** 批量保存引用(覆盖式) */ + void saveRefs(@Valid ConfigUnitTabRefSaveReqVO saveReqVO); + + /** 查询引用列表(含模板节点详情) */ + List getList(Long catalogItemId, String tabType); + + void delete(Long id); + + void swapSort(Long nodeId1, Long nodeId2); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/config/impl/ConfigProjectInfoServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/config/impl/ConfigProjectInfoServiceImpl.java new file mode 100644 index 0000000..a70b482 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/config/impl/ConfigProjectInfoServiceImpl.java @@ -0,0 +1,226 @@ +package com.yhy.module.core.service.config.impl; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_PROJECT_INFO_HAS_CHILDREN; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_PROJECT_INFO_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_PROJECT_INFO_NOT_SAME_LEVEL; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_PROJECT_INFO_PARENT_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_PROJECT_INFO_TREE_NOT_INDUSTRY; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_PROJECT_TREE_NOT_EXISTS; + +import cn.hutool.core.collection.CollUtil; +import com.yhy.module.core.controller.admin.config.vo.ConfigProjectInfoRespVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigProjectInfoSaveReqVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigProjectInfoSwapSortReqVO; +import com.yhy.module.core.convert.config.ConfigProjectInfoConvert; +import com.yhy.module.core.dal.dataobject.config.ConfigProjectInfoDO; +import com.yhy.module.core.dal.dataobject.config.ConfigProjectTreeDO; +import com.yhy.module.core.dal.mysql.config.ConfigProjectInfoMapper; +import com.yhy.module.core.dal.mysql.config.ConfigProjectTreeMapper; +import com.yhy.module.core.service.config.ConfigProjectInfoService; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * 工程信息配置 Service 实现类 + * + * @author yhy + */ +@Service +@Validated +@Slf4j +public class ConfigProjectInfoServiceImpl implements ConfigProjectInfoService { + + @Resource + private ConfigProjectInfoMapper configProjectInfoMapper; + + @Resource + private ConfigProjectTreeMapper configProjectTreeMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createConfigProjectInfo(ConfigProjectInfoSaveReqVO createReqVO) { + // 1. 校验行业节点存在且类型正确 + validateConfigTreeIsIndustry(createReqVO.getConfigTreeId()); + + // 2. 校验父节点存在 + if (createReqVO.getParentId() != null) { + validateParentExists(createReqVO.getParentId()); + } + + // 3. 构建DO + ConfigProjectInfoDO configProjectInfo = ConfigProjectInfoConvert.INSTANCE.convert(createReqVO); + + // 4. 设置排序号 + Integer maxSortOrder = configProjectInfoMapper.selectMaxSortOrderByParentId( + createReqVO.getConfigTreeId(), createReqVO.getParentId()); + configProjectInfo.setSortOrder(maxSortOrder + 1); + + // 5. 设置路径 + configProjectInfo.setPath(buildPath(createReqVO.getParentId())); + + // 6. 插入数据库 + configProjectInfoMapper.insert(configProjectInfo); + + // 7. 更新路径(包含自身ID) + updatePathWithSelfId(configProjectInfo); + + return configProjectInfo.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateConfigProjectInfo(ConfigProjectInfoSaveReqVO updateReqVO) { + // 1. 校验存在 + validateConfigProjectInfoExists(updateReqVO.getId()); + + // 2. 更新 + ConfigProjectInfoDO updateObj = ConfigProjectInfoConvert.INSTANCE.convert(updateReqVO); + configProjectInfoMapper.updateById(updateObj); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteConfigProjectInfo(Long id) { + // 1. 校验存在 + validateConfigProjectInfoExists(id); + + // 2. 校验是否有子节点 + List children = configProjectInfoMapper.selectListByParentId(id); + if (CollUtil.isNotEmpty(children)) { + throw exception(CONFIG_PROJECT_INFO_HAS_CHILDREN); + } + + // 3. 删除 + configProjectInfoMapper.deleteById(id); + } + + @Override + public ConfigProjectInfoRespVO getConfigProjectInfo(Long id) { + ConfigProjectInfoDO configProjectInfo = configProjectInfoMapper.selectById(id); + return ConfigProjectInfoConvert.INSTANCE.convert(configProjectInfo); + } + + @Override + public List getConfigProjectInfoTree(Long configTreeId) { + List list = configProjectInfoMapper.selectListByConfigTreeId(configTreeId); + return buildTree(list); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void swapSort(ConfigProjectInfoSwapSortReqVO swapReqVO) { + // 1. 校验两个节点存在 + ConfigProjectInfoDO node1 = validateConfigProjectInfoExists(swapReqVO.getNodeId1()); + ConfigProjectInfoDO node2 = validateConfigProjectInfoExists(swapReqVO.getNodeId2()); + + // 2. 校验是否同级 + if (!Objects.equals(node1.getParentId(), node2.getParentId())) { + throw exception(CONFIG_PROJECT_INFO_NOT_SAME_LEVEL); + } + + // 3. 交换排序号 + Integer tempSortOrder = node1.getSortOrder(); + node1.setSortOrder(node2.getSortOrder()); + node2.setSortOrder(tempSortOrder); + + configProjectInfoMapper.updateById(node1); + configProjectInfoMapper.updateById(node2); + } + + /** + * 校验行业节点存在且类型正确 + */ + private void validateConfigTreeIsIndustry(Long configTreeId) { + ConfigProjectTreeDO treeNode = configProjectTreeMapper.selectById(configTreeId); + if (treeNode == null) { + throw exception(CONFIG_PROJECT_TREE_NOT_EXISTS); + } + if (!ConfigProjectTreeDO.NODE_TYPE_INDUSTRY.equals(treeNode.getNodeType())) { + throw exception(CONFIG_PROJECT_INFO_TREE_NOT_INDUSTRY); + } + } + + /** + * 校验父节点存在 + */ + private void validateParentExists(Long parentId) { + ConfigProjectInfoDO parent = configProjectInfoMapper.selectById(parentId); + if (parent == null) { + throw exception(CONFIG_PROJECT_INFO_PARENT_NOT_EXISTS); + } + } + + /** + * 校验节点存在 + */ + private ConfigProjectInfoDO validateConfigProjectInfoExists(Long id) { + ConfigProjectInfoDO configProjectInfo = configProjectInfoMapper.selectById(id); + if (configProjectInfo == null) { + throw exception(CONFIG_PROJECT_INFO_NOT_EXISTS); + } + return configProjectInfo; + } + + /** + * 构建路径 + */ + private String[] buildPath(Long parentId) { + if (parentId == null) { + return new String[]{}; + } + ConfigProjectInfoDO parent = configProjectInfoMapper.selectById(parentId); + if (parent == null || parent.getPath() == null) { + return new String[]{String.valueOf(parentId)}; + } + List pathList = new ArrayList<>(Arrays.asList(parent.getPath())); + pathList.add(String.valueOf(parentId)); + return pathList.toArray(new String[0]); + } + + /** + * 更新路径(包含自身ID) + */ + private void updatePathWithSelfId(ConfigProjectInfoDO node) { + List pathList = node.getPath() != null ? new ArrayList<>(Arrays.asList(node.getPath())) : new ArrayList<>(); + pathList.add(String.valueOf(node.getId())); + node.setPath(pathList.toArray(new String[0])); + configProjectInfoMapper.updateById(node); + } + + /** + * 构建树形结构 + */ + private List buildTree(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + + // 转换为VO + List voList = list.stream() + .map(ConfigProjectInfoConvert.INSTANCE::convert) + .collect(Collectors.toList()); + + // 构建父子关系 + Map> parentIdMap = voList.stream() + .filter(vo -> vo.getParentId() != null) + .collect(Collectors.groupingBy(ConfigProjectInfoRespVO::getParentId)); + + voList.forEach(vo -> vo.setChildren(parentIdMap.get(vo.getId()))); + + // 返回根节点 + return voList.stream() + .filter(vo -> vo.getParentId() == null) + .collect(Collectors.toList()); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/config/impl/ConfigProjectTreeServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/config/impl/ConfigProjectTreeServiceImpl.java new file mode 100644 index 0000000..fd6ee59 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/config/impl/ConfigProjectTreeServiceImpl.java @@ -0,0 +1,292 @@ +package com.yhy.module.core.service.config.impl; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_PROJECT_TREE_CODE_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_PROJECT_TREE_HAS_CHILDREN; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_PROJECT_TREE_INDUSTRY_PARENT_INVALID; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_PROJECT_TREE_INDUSTRY_PARENT_REQUIRED; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_PROJECT_TREE_NODE_TYPE_CANNOT_CHANGE; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_PROJECT_TREE_NODE_TYPE_INVALID; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_PROJECT_TREE_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_PROJECT_TREE_NOT_SAME_LEVEL; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_PROJECT_TREE_PARENT_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_PROJECT_TREE_PROVINCE_PARENT_INVALID; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_PROJECT_TREE_PROVINCE_PARENT_REQUIRED; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_PROJECT_TREE_ROOT_ONLY_ONE; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_PROJECT_TREE_ROOT_PARENT_MUST_NULL; + +import cn.hutool.core.collection.CollUtil; +import com.yhy.module.core.controller.admin.config.vo.ConfigProjectTreeRespVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigProjectTreeSaveReqVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigProjectTreeSwapSortReqVO; +import com.yhy.module.core.convert.config.ConfigProjectTreeConvert; +import com.yhy.module.core.dal.dataobject.config.ConfigProjectTreeDO; +import com.yhy.module.core.dal.mysql.config.ConfigProjectTreeMapper; +import com.yhy.module.core.service.config.ConfigProjectTreeService; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * 项目界面配置树 Service 实现类 + * + * @author yhy + */ +@Service +@Validated +@Slf4j +public class ConfigProjectTreeServiceImpl implements ConfigProjectTreeService { + + @Resource + private ConfigProjectTreeMapper configProjectTreeMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createConfigProjectTree(ConfigProjectTreeSaveReqVO createReqVO) { + // 1. 校验节点类型和父节点 + validateNodeTypeAndParent(createReqVO.getNodeType(), createReqVO.getParentId(), null); + + // 2. 校验编码唯一性 + validateCodeUnique(createReqVO.getCode(), null); + + // 3. 构建DO + ConfigProjectTreeDO configProjectTree = ConfigProjectTreeConvert.INSTANCE.convert(createReqVO); + + // 4. 设置排序号 + Integer maxSortOrder = configProjectTreeMapper.selectMaxSortOrderByParentId(createReqVO.getParentId()); + configProjectTree.setSortOrder(maxSortOrder + 1); + + // 5. 设置路径 + configProjectTree.setPath(buildPath(createReqVO.getParentId())); + + // 6. 插入数据库 + configProjectTreeMapper.insert(configProjectTree); + + // 7. 更新路径(包含自身ID) + updatePathWithSelfId(configProjectTree); + + return configProjectTree.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateConfigProjectTree(ConfigProjectTreeSaveReqVO updateReqVO) { + // 1. 校验存在 + ConfigProjectTreeDO existNode = validateConfigProjectTreeExists(updateReqVO.getId()); + + // 2. 校验编码唯一性 + validateCodeUnique(updateReqVO.getCode(), updateReqVO.getId()); + + // 3. 不允许修改节点类型 + if (!existNode.getNodeType().equals(updateReqVO.getNodeType())) { + throw exception(CONFIG_PROJECT_TREE_NODE_TYPE_CANNOT_CHANGE); + } + + // 4. 更新 + ConfigProjectTreeDO updateObj = ConfigProjectTreeConvert.INSTANCE.convert(updateReqVO); + configProjectTreeMapper.updateById(updateObj); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteConfigProjectTree(Long id) { + // 1. 校验存在 + validateConfigProjectTreeExists(id); + + // 2. 校验是否有子节点 + List children = configProjectTreeMapper.selectListByParentId(id); + if (CollUtil.isNotEmpty(children)) { + throw exception(CONFIG_PROJECT_TREE_HAS_CHILDREN); + } + + // 3. 删除 + configProjectTreeMapper.deleteById(id); + } + + @Override + public ConfigProjectTreeRespVO getConfigProjectTree(Long id) { + ConfigProjectTreeDO configProjectTree = configProjectTreeMapper.selectById(id); + return ConfigProjectTreeConvert.INSTANCE.convert(configProjectTree); + } + + @Override + public List getConfigProjectTreeList() { + List list = configProjectTreeMapper.selectAllOrderBySortOrder(); + return buildTree(list); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void swapSort(ConfigProjectTreeSwapSortReqVO swapReqVO) { + // 1. 校验两个节点存在 + ConfigProjectTreeDO node1 = validateConfigProjectTreeExists(swapReqVO.getNodeId1()); + ConfigProjectTreeDO node2 = validateConfigProjectTreeExists(swapReqVO.getNodeId2()); + + // 2. 校验是否同级 + if (!Objects.equals(node1.getParentId(), node2.getParentId())) { + throw exception(CONFIG_PROJECT_TREE_NOT_SAME_LEVEL); + } + + // 3. 交换排序号 + Integer tempSortOrder = node1.getSortOrder(); + node1.setSortOrder(node2.getSortOrder()); + node2.setSortOrder(tempSortOrder); + + configProjectTreeMapper.updateById(node1); + configProjectTreeMapper.updateById(node2); + } + + @Override + public List getIndustryOptions() { + // 获取所有省市节点和行业节点 + List allNodes = configProjectTreeMapper.selectAllOrderBySortOrder(); + + // 过滤出省市节点 + List provinceNodes = allNodes.stream() + .filter(node -> ConfigProjectTreeDO.NODE_TYPE_PROVINCE.equals(node.getNodeType())) + .collect(Collectors.toList()); + + // 构建省市 -> 行业的两级结构 + List result = new ArrayList<>(); + for (ConfigProjectTreeDO province : provinceNodes) { + ConfigProjectTreeRespVO provinceVO = ConfigProjectTreeConvert.INSTANCE.convert(province); + + // 获取该省市下的行业节点 + List industries = allNodes.stream() + .filter(node -> ConfigProjectTreeDO.NODE_TYPE_INDUSTRY.equals(node.getNodeType()) + && Objects.equals(node.getParentId(), province.getId())) + .map(ConfigProjectTreeConvert.INSTANCE::convert) + .collect(Collectors.toList()); + + provinceVO.setChildren(industries); + result.add(provinceVO); + } + + return result; + } + + /** + * 校验节点类型和父节点 + */ + private void validateNodeTypeAndParent(String nodeType, Long parentId, Long selfId) { + if (ConfigProjectTreeDO.NODE_TYPE_ROOT.equals(nodeType)) { + // 根节点:父节点必须为空,且只能有一个根节点 + if (parentId != null) { + throw exception(CONFIG_PROJECT_TREE_ROOT_PARENT_MUST_NULL); + } + List rootNodes = configProjectTreeMapper.selectListByNodeType(ConfigProjectTreeDO.NODE_TYPE_ROOT); + if (CollUtil.isNotEmpty(rootNodes) && (selfId == null || !rootNodes.get(0).getId().equals(selfId))) { + throw exception(CONFIG_PROJECT_TREE_ROOT_ONLY_ONE); + } + } else if (ConfigProjectTreeDO.NODE_TYPE_PROVINCE.equals(nodeType)) { + // 省市节点:父节点必须是根节点或省市节点 + if (parentId == null) { + throw exception(CONFIG_PROJECT_TREE_PROVINCE_PARENT_REQUIRED); + } + ConfigProjectTreeDO parent = configProjectTreeMapper.selectById(parentId); + if (parent == null) { + throw exception(CONFIG_PROJECT_TREE_PARENT_NOT_EXISTS); + } + if (!ConfigProjectTreeDO.NODE_TYPE_ROOT.equals(parent.getNodeType()) + && !ConfigProjectTreeDO.NODE_TYPE_PROVINCE.equals(parent.getNodeType())) { + throw exception(CONFIG_PROJECT_TREE_PROVINCE_PARENT_INVALID); + } + } else if (ConfigProjectTreeDO.NODE_TYPE_INDUSTRY.equals(nodeType)) { + // 行业节点:父节点必须是省市节点 + if (parentId == null) { + throw exception(CONFIG_PROJECT_TREE_INDUSTRY_PARENT_REQUIRED); + } + ConfigProjectTreeDO parent = configProjectTreeMapper.selectById(parentId); + if (parent == null) { + throw exception(CONFIG_PROJECT_TREE_PARENT_NOT_EXISTS); + } + if (!ConfigProjectTreeDO.NODE_TYPE_PROVINCE.equals(parent.getNodeType())) { + throw exception(CONFIG_PROJECT_TREE_INDUSTRY_PARENT_INVALID); + } + } else { + throw exception(CONFIG_PROJECT_TREE_NODE_TYPE_INVALID); + } + } + + /** + * 校验编码唯一性 + */ + private void validateCodeUnique(String code, Long selfId) { + ConfigProjectTreeDO existNode = configProjectTreeMapper.selectByCode(code); + if (existNode != null && !existNode.getId().equals(selfId)) { + throw exception(CONFIG_PROJECT_TREE_CODE_EXISTS); + } + } + + /** + * 校验节点存在 + */ + private ConfigProjectTreeDO validateConfigProjectTreeExists(Long id) { + ConfigProjectTreeDO configProjectTree = configProjectTreeMapper.selectById(id); + if (configProjectTree == null) { + throw exception(CONFIG_PROJECT_TREE_NOT_EXISTS); + } + return configProjectTree; + } + + /** + * 构建路径 + */ + private String[] buildPath(Long parentId) { + if (parentId == null) { + return new String[]{}; + } + ConfigProjectTreeDO parent = configProjectTreeMapper.selectById(parentId); + if (parent == null || parent.getPath() == null) { + return new String[]{String.valueOf(parentId)}; + } + List pathList = new ArrayList<>(Arrays.asList(parent.getPath())); + pathList.add(String.valueOf(parentId)); + return pathList.toArray(new String[0]); + } + + /** + * 更新路径(包含自身ID) + */ + private void updatePathWithSelfId(ConfigProjectTreeDO node) { + List pathList = node.getPath() != null ? new ArrayList<>(Arrays.asList(node.getPath())) : new ArrayList<>(); + pathList.add(String.valueOf(node.getId())); + node.setPath(pathList.toArray(new String[0])); + configProjectTreeMapper.updateById(node); + } + + /** + * 构建树形结构 + */ + private List buildTree(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + + // 转换为VO + List voList = list.stream() + .map(ConfigProjectTreeConvert.INSTANCE::convert) + .collect(Collectors.toList()); + + // 构建父子关系 + Map> parentIdMap = voList.stream() + .filter(vo -> vo.getParentId() != null) + .collect(Collectors.groupingBy(ConfigProjectTreeRespVO::getParentId)); + + voList.forEach(vo -> vo.setChildren(parentIdMap.get(vo.getId()))); + + // 返回根节点 + return voList.stream() + .filter(vo -> vo.getParentId() == null) + .collect(Collectors.toList()); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/config/impl/ConfigUnitDivisionTemplateServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/config/impl/ConfigUnitDivisionTemplateServiceImpl.java new file mode 100644 index 0000000..10cf4e2 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/config/impl/ConfigUnitDivisionTemplateServiceImpl.java @@ -0,0 +1,249 @@ +package com.yhy.module.core.service.config.impl; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_UNIT_DIV_TPL_BOQ_PARENT_MUST_DIVISION; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_UNIT_DIV_TPL_INVALID_NODE_TYPE; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_UNIT_DIV_TPL_INVALID_TAB_TYPE; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_UNIT_DIV_TPL_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_UNIT_DIV_TPL_NOT_FIELDS_MAJORS; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_UNIT_DIV_TPL_NOT_SAME_LEVEL; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_UNIT_DIV_TPL_PARENT_NOT_EXISTS; + +import cn.hutool.core.collection.CollUtil; +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitDivisionTemplateRespVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitDivisionTemplateSaveReqVO; +import com.yhy.module.core.dal.dataobject.config.ConfigUnitDivisionTemplateDO; +import com.yhy.module.core.dal.dataobject.quota.QuotaCatalogItemDO; +import com.yhy.module.core.dal.mysql.config.ConfigUnitDivisionTemplateMapper; +import com.yhy.module.core.dal.mysql.quota.QuotaCatalogItemMapper; +import com.yhy.module.core.service.config.ConfigUnitDivisionTemplateService; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * 单位工程界面配置 - 分部分项模板 Service 实现 + * 四个标签页(分部分项/措施项目/其他项目/单位汇总)各自独立维护模板数据 + */ +@Service +@Validated +@Slf4j +public class ConfigUnitDivisionTemplateServiceImpl implements ConfigUnitDivisionTemplateService { + + @Resource + private ConfigUnitDivisionTemplateMapper templateMapper; + @Resource + private QuotaCatalogItemMapper quotaCatalogItemMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long create(ConfigUnitDivisionTemplateSaveReqVO reqVO) { + validateFieldsMajorsNode(reqVO.getCatalogItemId()); + validateNodeType(reqVO.getNodeType()); + validateTabType(reqVO.getTabType()); + validateParentNode(reqVO); + + ConfigUnitDivisionTemplateDO node = ConfigUnitDivisionTemplateDO.builder() + .catalogItemId(reqVO.getCatalogItemId()) + .parentId(reqVO.getParentId()) + .nodeType(reqVO.getNodeType()) + .tabType(reqVO.getTabType()) + .code(reqVO.getCode()) + .name(reqVO.getName()) + .unit(reqVO.getUnit()) + .attributes(reqVO.getAttributes()) + .rate(reqVO.getRate()) + .remark(reqVO.getRemark()) + .costCode(reqVO.getCostCode()) + .build(); + + node.setSortOrder(calculateSortOrder(reqVO)); + node.setPath(buildPath(reqVO.getParentId())); + templateMapper.insert(node); + + // 更新路径(包含自身ID) + String[] currentPath = node.getPath() != null ? node.getPath() : new String[]{}; + String[] newPath = Arrays.copyOf(currentPath, currentPath.length + 1); + newPath[currentPath.length] = String.valueOf(node.getId()); + node.setPath(newPath); + templateMapper.updateById(node); + + return node.getId(); + } + + @Override + public void update(ConfigUnitDivisionTemplateSaveReqVO reqVO) { + ConfigUnitDivisionTemplateDO exist = templateMapper.selectById(reqVO.getId()); + if (exist == null) throw exception(CONFIG_UNIT_DIV_TPL_NOT_EXISTS); + + ConfigUnitDivisionTemplateDO updateObj = new ConfigUnitDivisionTemplateDO(); + updateObj.setId(reqVO.getId()); + updateObj.setCode(reqVO.getCode()); + updateObj.setName(reqVO.getName()); + updateObj.setUnit(reqVO.getUnit()); + updateObj.setAttributes(reqVO.getAttributes()); + updateObj.setRate(reqVO.getRate()); + updateObj.setRemark(reqVO.getRemark()); + updateObj.setCostCode(reqVO.getCostCode()); + // 不允许修改 nodeType、parentId、tabType + templateMapper.updateById(updateObj); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Long id) { + ConfigUnitDivisionTemplateDO exist = templateMapper.selectById(id); + if (exist == null) throw exception(CONFIG_UNIT_DIV_TPL_NOT_EXISTS); + + // 级联删除子节点 + List children = templateMapper.selectListByParentId(id); + if (CollUtil.isNotEmpty(children)) { + for (ConfigUnitDivisionTemplateDO child : children) { + delete(child.getId()); + } + } + + templateMapper.deleteById(id); + log.info("[delete] 删除模板节点 id={}, tabType={}", id, exist.getTabType()); + } + + @Override + public List getTree(Long catalogItemId, String tabType) { + validateTabType(tabType); + List list = templateMapper.selectListByCatalogItemIdAndTabType(catalogItemId, tabType); + return buildTree(list); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void swapSort(Long nodeId1, Long nodeId2) { + ConfigUnitDivisionTemplateDO n1 = templateMapper.selectById(nodeId1); + ConfigUnitDivisionTemplateDO n2 = templateMapper.selectById(nodeId2); + if (n1 == null || n2 == null) throw exception(CONFIG_UNIT_DIV_TPL_NOT_EXISTS); + if (!Objects.equals(n1.getParentId(), n2.getParentId())) throw exception(CONFIG_UNIT_DIV_TPL_NOT_SAME_LEVEL); + + Integer temp = n1.getSortOrder(); + n1.setSortOrder(n2.getSortOrder()); + n2.setSortOrder(temp); + templateMapper.updateById(n1); + templateMapper.updateById(n2); + } + + // ========== 私有方法 ========== + + private void validateFieldsMajorsNode(Long catalogItemId) { + QuotaCatalogItemDO node = quotaCatalogItemMapper.selectById(catalogItemId); + if (node == null || !"fields_majors".equals(node.getNodeType())) { + throw exception(CONFIG_UNIT_DIV_TPL_NOT_FIELDS_MAJORS); + } + } + + private void validateNodeType(String nodeType) { + if (!ConfigUnitDivisionTemplateDO.NODE_TYPE_DIVISION.equals(nodeType) + && !ConfigUnitDivisionTemplateDO.NODE_TYPE_BOQ.equals(nodeType)) { + throw exception(CONFIG_UNIT_DIV_TPL_INVALID_NODE_TYPE); + } + } + + /** 校验标签页类型 */ + private void validateTabType(String tabType) { + if (!ConfigUnitDivisionTemplateDO.VALID_TAB_TYPES.contains(tabType)) { + throw exception(CONFIG_UNIT_DIV_TPL_INVALID_TAB_TYPE); + } + } + + private void validateParentNode(ConfigUnitDivisionTemplateSaveReqVO reqVO) { + if (reqVO.getParentId() != null) { + ConfigUnitDivisionTemplateDO parent = templateMapper.selectById(reqVO.getParentId()); + if (parent == null) throw exception(CONFIG_UNIT_DIV_TPL_PARENT_NOT_EXISTS); + // 清单只能在分部下创建 + if (ConfigUnitDivisionTemplateDO.NODE_TYPE_BOQ.equals(reqVO.getNodeType()) + && !ConfigUnitDivisionTemplateDO.NODE_TYPE_DIVISION.equals(parent.getNodeType())) { + throw exception(CONFIG_UNIT_DIV_TPL_BOQ_PARENT_MUST_DIVISION); + } + // 父节点必须属于同一标签页 + if (!Objects.equals(parent.getTabType(), reqVO.getTabType())) { + throw exception(CONFIG_UNIT_DIV_TPL_PARENT_NOT_EXISTS); + } + } else { + // 无父节点时,只能创建分部 + if (ConfigUnitDivisionTemplateDO.NODE_TYPE_BOQ.equals(reqVO.getNodeType())) { + throw exception(CONFIG_UNIT_DIV_TPL_BOQ_PARENT_MUST_DIVISION); + } + } + } + + private String[] buildPath(Long parentId) { + if (parentId == null) return new String[]{}; + ConfigUnitDivisionTemplateDO parent = templateMapper.selectById(parentId); + return parent != null && parent.getPath() != null ? parent.getPath() : new String[]{}; + } + + private Integer calculateSortOrder(ConfigUnitDivisionTemplateSaveReqVO reqVO) { + String pos = reqVO.getInsertPosition(); + Long refId = reqVO.getReferenceNodeId(); + if (pos == null || ConfigUnitDivisionTemplateSaveReqVO.INSERT_POSITION_END.equals(pos) || refId == null) { + return templateMapper.selectMaxSortOrderByParentId(reqVO.getCatalogItemId(), reqVO.getParentId(), reqVO.getTabType()) + 1; + } + ConfigUnitDivisionTemplateDO ref = templateMapper.selectById(refId); + if (ref == null) { + return templateMapper.selectMaxSortOrderByParentId(reqVO.getCatalogItemId(), reqVO.getParentId(), reqVO.getTabType()) + 1; + } + Integer target = ConfigUnitDivisionTemplateSaveReqVO.INSERT_POSITION_ABOVE.equals(pos) + ? ref.getSortOrder() : ref.getSortOrder() + 1; + for (ConfigUnitDivisionTemplateDO n : templateMapper.selectListByParentIdAndSortOrderGe(ref.getParentId(), target)) { + n.setSortOrder(n.getSortOrder() + 1); + templateMapper.updateById(n); + } + return target; + } + + private List buildTree(List list) { + Map voMap = new LinkedHashMap<>(); + for (ConfigUnitDivisionTemplateDO node : list) { + voMap.put(node.getId(), convertToRespVO(node)); + } + List roots = new ArrayList<>(); + for (ConfigUnitDivisionTemplateRespVO vo : voMap.values()) { + if (vo.getParentId() == null) { + roots.add(vo); + } else { + ConfigUnitDivisionTemplateRespVO parent = voMap.get(vo.getParentId()); + if (parent != null) { + if (parent.getChildren() == null) parent.setChildren(new ArrayList<>()); + parent.getChildren().add(vo); + } else { + roots.add(vo); + } + } + } + return roots; + } + + private ConfigUnitDivisionTemplateRespVO convertToRespVO(ConfigUnitDivisionTemplateDO node) { + ConfigUnitDivisionTemplateRespVO vo = new ConfigUnitDivisionTemplateRespVO(); + vo.setId(node.getId()); + vo.setCatalogItemId(node.getCatalogItemId()); + vo.setParentId(node.getParentId()); + vo.setNodeType(node.getNodeType()); + vo.setCode(node.getCode()); + vo.setName(node.getName()); + vo.setUnit(node.getUnit()); + vo.setSortOrder(node.getSortOrder()); + vo.setTabType(node.getTabType()); + vo.setAttributes(node.getAttributes()); + vo.setRate(node.getRate()); + vo.setRemark(node.getRemark()); + vo.setCostCode(node.getCostCode()); + vo.setCreateTime(node.getCreateTime()); + return vo; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/config/impl/ConfigUnitFieldServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/config/impl/ConfigUnitFieldServiceImpl.java new file mode 100644 index 0000000..cbad017 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/config/impl/ConfigUnitFieldServiceImpl.java @@ -0,0 +1,236 @@ +package com.yhy.module.core.service.config.impl; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_UNIT_FIELD_CODE_DUPLICATE; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_UNIT_FIELD_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_UNIT_FIELD_NOT_FIELDS_MAJORS; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_UNIT_FIELD_NOT_SAME_CATALOG; + +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitFieldHiddenFieldsRespVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitFieldRespVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitFieldSaveReqVO; +import com.yhy.module.core.dal.dataobject.config.ConfigUnitFieldDO; +import com.yhy.module.core.dal.dataobject.quota.QuotaCatalogItemDO; +import com.yhy.module.core.dal.mysql.config.ConfigUnitFieldMapper; +import com.yhy.module.core.dal.mysql.quota.QuotaCatalogItemMapper; +import com.yhy.module.core.service.config.ConfigUnitFieldService; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +@Service +@Validated +@Slf4j +public class ConfigUnitFieldServiceImpl implements ConfigUnitFieldService { + + @Resource + private ConfigUnitFieldMapper configUnitFieldMapper; + @Resource + private QuotaCatalogItemMapper quotaCatalogItemMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long create(ConfigUnitFieldSaveReqVO createReqVO) { + // 1. 校验 catalogItemId 是 fields_majors 类型 + validateFieldsMajorsNode(createReqVO.getCatalogItemId()); + // 2. 校验字段编码唯一 + validateFieldCodeUnique(createReqVO.getCatalogItemId(), createReqVO.getFieldCode(), null); + + // 3. 构建 DO + ConfigUnitFieldDO field = ConfigUnitFieldDO.builder() + .catalogItemId(createReqVO.getCatalogItemId()) + .seqNo(createReqVO.getSeqNo()) + .fieldName(createReqVO.getFieldName()) + .fieldCode(createReqVO.getFieldCode()) + .divisionHidden(createReqVO.getDivisionHidden() != null ? createReqVO.getDivisionHidden() : false) + .measureHidden(createReqVO.getMeasureHidden() != null ? createReqVO.getMeasureHidden() : false) + .otherHidden(createReqVO.getOtherHidden() != null ? createReqVO.getOtherHidden() : false) + .summaryHidden(createReqVO.getSummaryHidden() != null ? createReqVO.getSummaryHidden() : false) + .remark(createReqVO.getRemark()) + .build(); + + // 4. 计算排序号 + field.setSortOrder(calculateSortOrder(createReqVO)); + + configUnitFieldMapper.insert(field); + return field.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void batchCreate(List createReqVOs) { + // 1. 校验并构建 DO 列表 + List fields = new ArrayList<>(); + for (ConfigUnitFieldSaveReqVO reqVO : createReqVOs) { +// validateFieldsMajorsNode(reqVO.getCatalogItemId()); +// validateFieldCodeUnique(reqVO.getCatalogItemId(), reqVO.getFieldCode(), null); + ConfigUnitFieldDO field = ConfigUnitFieldDO.builder() + .catalogItemId(reqVO.getCatalogItemId()) + .seqNo(reqVO.getSeqNo()) + .fieldName(reqVO.getFieldName()) + .fieldCode(reqVO.getFieldCode()) + .divisionHidden(reqVO.getDivisionHidden() != null ? reqVO.getDivisionHidden() : false) + .measureHidden(reqVO.getMeasureHidden() != null ? reqVO.getMeasureHidden() : false) + .otherHidden(reqVO.getOtherHidden() != null ? reqVO.getOtherHidden() : false) + .summaryHidden(reqVO.getSummaryHidden() != null ? reqVO.getSummaryHidden() : false) + .remark(reqVO.getRemark()) + .build(); + field.setSortOrder(calculateSortOrder(reqVO)); + fields.add(field); + } + // 2. 批量插入 + configUnitFieldMapper.insertBatch(fields); + } + + @Override + public void update(ConfigUnitFieldSaveReqVO updateReqVO) { + // 1. 校验存在 + ConfigUnitFieldDO exist = configUnitFieldMapper.selectById(updateReqVO.getId()); + if (exist == null) { + throw exception(CONFIG_UNIT_FIELD_NOT_EXISTS); + } + // 2. 校验字段编码唯一 + validateFieldCodeUnique(exist.getCatalogItemId(), updateReqVO.getFieldCode(), updateReqVO.getId()); + + // 3. 更新 + ConfigUnitFieldDO updateObj = ConfigUnitFieldDO.builder() + .id(updateReqVO.getId()) + .seqNo(updateReqVO.getSeqNo()) + .fieldName(updateReqVO.getFieldName()) + .fieldCode(updateReqVO.getFieldCode()) + .divisionHidden(updateReqVO.getDivisionHidden()) + .measureHidden(updateReqVO.getMeasureHidden()) + .otherHidden(updateReqVO.getOtherHidden()) + .summaryHidden(updateReqVO.getSummaryHidden()) + .remark(updateReqVO.getRemark()) + .build(); + configUnitFieldMapper.updateById(updateObj); + } + + @Override + public void delete(Long id) { + ConfigUnitFieldDO exist = configUnitFieldMapper.selectById(id); + if (exist == null) { + throw exception(CONFIG_UNIT_FIELD_NOT_EXISTS); + } + configUnitFieldMapper.deleteById(id); + } + + @Override + public List getList(Long catalogItemId) { + List list = configUnitFieldMapper.selectListByCatalogItemId(catalogItemId); + return list.stream().sorted(Comparator.comparingInt(a -> a.getSeqNo() != null ? a.getSeqNo() : 0)) + .map(this::convertToRespVO).collect(Collectors.toList()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void swapSort(Long nodeId1, Long nodeId2) { + ConfigUnitFieldDO node1 = configUnitFieldMapper.selectById(nodeId1); + ConfigUnitFieldDO node2 = configUnitFieldMapper.selectById(nodeId2); + if (node1 == null || node2 == null) { + throw exception(CONFIG_UNIT_FIELD_NOT_EXISTS); + } + if (!node1.getCatalogItemId().equals(node2.getCatalogItemId())) { + throw exception(CONFIG_UNIT_FIELD_NOT_SAME_CATALOG); + } + Integer temp = node1.getSortOrder(); + node1.setSortOrder(node2.getSortOrder()); + node2.setSortOrder(temp); + configUnitFieldMapper.updateById(node1); + configUnitFieldMapper.updateById(node2); + } + + @Override + public ConfigUnitFieldHiddenFieldsRespVO getHiddenFields(Long catalogItemId) { + List list = configUnitFieldMapper.selectListByCatalogItemId(catalogItemId); + List divisionHidden = new ArrayList<>(); + List measureHidden = new ArrayList<>(); + List otherHidden = new ArrayList<>(); + List summaryHidden = new ArrayList<>(); + for (ConfigUnitFieldDO field : list) { + if (Boolean.TRUE.equals(field.getDivisionHidden())) divisionHidden.add(field.getFieldCode()); + if (Boolean.TRUE.equals(field.getMeasureHidden())) measureHidden.add(field.getFieldCode()); + if (Boolean.TRUE.equals(field.getOtherHidden())) otherHidden.add(field.getFieldCode()); + if (Boolean.TRUE.equals(field.getSummaryHidden())) summaryHidden.add(field.getFieldCode()); + } + return ConfigUnitFieldHiddenFieldsRespVO.builder() + .divisionHiddenFields(divisionHidden) + .measureHiddenFields(measureHidden) + .otherHiddenFields(otherHidden) + .summaryHiddenFields(summaryHidden) + .build(); + } + + // ========== 私有方法 ========== + + private void validateFieldsMajorsNode(Long catalogItemId) { + QuotaCatalogItemDO node = quotaCatalogItemMapper.selectById(catalogItemId); + if (node == null || !"fields_majors".equals(node.getNodeType())) { + throw exception(CONFIG_UNIT_FIELD_NOT_FIELDS_MAJORS); + } + } + + private void validateFieldCodeUnique(Long catalogItemId, String fieldCode, Long excludeId) { + List list = configUnitFieldMapper.selectListByCatalogItemId(catalogItemId); + for (ConfigUnitFieldDO field : list) { + if (field.getFieldCode().equals(fieldCode) && !field.getId().equals(excludeId)) { + throw exception(CONFIG_UNIT_FIELD_CODE_DUPLICATE, fieldCode); + } + } + } + + private Integer calculateSortOrder(ConfigUnitFieldSaveReqVO reqVO) { + String insertPosition = reqVO.getInsertPosition(); + Long referenceNodeId = reqVO.getReferenceNodeId(); + + if (insertPosition == null || ConfigUnitFieldSaveReqVO.INSERT_POSITION_END.equals(insertPosition) + || referenceNodeId == null) { + return configUnitFieldMapper.selectMaxSortOrder(reqVO.getCatalogItemId()) + 1; + } + + ConfigUnitFieldDO refNode = configUnitFieldMapper.selectById(referenceNodeId); + if (refNode == null) { + return configUnitFieldMapper.selectMaxSortOrder(reqVO.getCatalogItemId()) + 1; + } + + Integer targetSortOrder; + if (ConfigUnitFieldSaveReqVO.INSERT_POSITION_ABOVE.equals(insertPosition)) { + targetSortOrder = refNode.getSortOrder(); + } else { + targetSortOrder = refNode.getSortOrder() + 1; + } + + // 后移 + List toShift = configUnitFieldMapper + .selectListByCatalogItemIdAndSortOrderGe(reqVO.getCatalogItemId(), targetSortOrder); + for (ConfigUnitFieldDO node : toShift) { + node.setSortOrder(node.getSortOrder() + 1); + configUnitFieldMapper.updateById(node); + } + return targetSortOrder; + } + + private ConfigUnitFieldRespVO convertToRespVO(ConfigUnitFieldDO field) { + ConfigUnitFieldRespVO vo = new ConfigUnitFieldRespVO(); + vo.setId(field.getId()); + vo.setCatalogItemId(field.getCatalogItemId()); + vo.setSeqNo(field.getSeqNo()); + vo.setFieldName(field.getFieldName()); + vo.setFieldCode(field.getFieldCode()); + vo.setDivisionHidden(field.getDivisionHidden()); + vo.setMeasureHidden(field.getMeasureHidden()); + vo.setOtherHidden(field.getOtherHidden()); + vo.setSummaryHidden(field.getSummaryHidden()); + vo.setRemark(field.getRemark()); + vo.setSortOrder(field.getSortOrder()); + vo.setCreateTime(field.getCreateTime()); + return vo; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/config/impl/ConfigUnitResourceFieldServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/config/impl/ConfigUnitResourceFieldServiceImpl.java new file mode 100644 index 0000000..2e2b209 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/config/impl/ConfigUnitResourceFieldServiceImpl.java @@ -0,0 +1,177 @@ +package com.yhy.module.core.service.config.impl; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_UNIT_RESOURCE_FIELD_CODE_DUPLICATE; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_UNIT_RESOURCE_FIELD_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_UNIT_RESOURCE_FIELD_NOT_FIELDS_MAJORS; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_UNIT_RESOURCE_FIELD_NOT_SAME_CATALOG; + +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitResourceFieldRespVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitResourceFieldSaveReqVO; +import com.yhy.module.core.dal.dataobject.config.ConfigUnitResourceFieldDO; +import com.yhy.module.core.dal.dataobject.quota.QuotaCatalogItemDO; +import com.yhy.module.core.dal.mysql.config.ConfigUnitResourceFieldMapper; +import com.yhy.module.core.dal.mysql.quota.QuotaCatalogItemMapper; +import com.yhy.module.core.service.config.ConfigUnitResourceFieldService; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +@Service +@Validated +@Slf4j +public class ConfigUnitResourceFieldServiceImpl implements ConfigUnitResourceFieldService { + + @Resource + private ConfigUnitResourceFieldMapper mapper; + @Resource + private QuotaCatalogItemMapper quotaCatalogItemMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long create(ConfigUnitResourceFieldSaveReqVO reqVO) { + validateFieldsMajorsNode(reqVO.getCatalogItemId()); + validateFieldCodeUnique(reqVO.getCatalogItemId(), reqVO.getFieldCode(), null); + + ConfigUnitResourceFieldDO field = ConfigUnitResourceFieldDO.builder() + .catalogItemId(reqVO.getCatalogItemId()) + .seqNo(reqVO.getSeqNo()) + .fieldName(reqVO.getFieldName()) + .fieldCode(reqVO.getFieldCode()) + .visible(reqVO.getVisible() != null ? reqVO.getVisible() : true) + .remark(reqVO.getRemark()) + .build(); + field.setSortOrder(calculateSortOrder(reqVO)); + mapper.insert(field); + return field.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void batchCreate(List createReqVOs) { + // 1. 校验并构建 DO 列表 + List fields = new ArrayList<>(); + for (ConfigUnitResourceFieldSaveReqVO reqVO : createReqVOs) { +// validateFieldsMajorsNode(reqVO.getCatalogItemId()); +// validateFieldCodeUnique(reqVO.getCatalogItemId(), reqVO.getFieldCode(), null); + ConfigUnitResourceFieldDO field = ConfigUnitResourceFieldDO.builder() + .catalogItemId(reqVO.getCatalogItemId()) + .seqNo(reqVO.getSeqNo()) + .fieldName(reqVO.getFieldName()) + .fieldCode(reqVO.getFieldCode()) + .visible(reqVO.getVisible() != null ? reqVO.getVisible() : true) + .remark(reqVO.getRemark()) + .build(); + field.setSortOrder(calculateSortOrder(reqVO)); + fields.add(field); + } + // 2. 批量插入 + mapper.insertBatch(fields); + } + + @Override + public void update(ConfigUnitResourceFieldSaveReqVO reqVO) { + ConfigUnitResourceFieldDO exist = mapper.selectById(reqVO.getId()); + if (exist == null) throw exception(CONFIG_UNIT_RESOURCE_FIELD_NOT_EXISTS); + validateFieldCodeUnique(exist.getCatalogItemId(), reqVO.getFieldCode(), reqVO.getId()); + + ConfigUnitResourceFieldDO updateObj = ConfigUnitResourceFieldDO.builder() + .id(reqVO.getId()) + .seqNo(reqVO.getSeqNo()) + .fieldName(reqVO.getFieldName()) + .fieldCode(reqVO.getFieldCode()) + .visible(reqVO.getVisible()) + .remark(reqVO.getRemark()) + .build(); + mapper.updateById(updateObj); + } + + @Override + public void delete(Long id) { + if (mapper.selectById(id) == null) throw exception(CONFIG_UNIT_RESOURCE_FIELD_NOT_EXISTS); + mapper.deleteById(id); + } + + @Override + public List getList(Long catalogItemId) { + return mapper.selectListByCatalogItemId(catalogItemId).stream() + .sorted(Comparator.comparingInt(a -> a.getSeqNo() != null ? a.getSeqNo() : 0)) + .map(this::convertToRespVO).collect(Collectors.toList()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void swapSort(Long nodeId1, Long nodeId2) { + ConfigUnitResourceFieldDO n1 = mapper.selectById(nodeId1); + ConfigUnitResourceFieldDO n2 = mapper.selectById(nodeId2); + if (n1 == null || n2 == null) throw exception(CONFIG_UNIT_RESOURCE_FIELD_NOT_EXISTS); + if (!n1.getCatalogItemId().equals(n2.getCatalogItemId())) throw exception(CONFIG_UNIT_RESOURCE_FIELD_NOT_SAME_CATALOG); + Integer temp = n1.getSortOrder(); + n1.setSortOrder(n2.getSortOrder()); + n2.setSortOrder(temp); + mapper.updateById(n1); + mapper.updateById(n2); + } + + @Override + public List getHiddenFields(Long catalogItemId) { + List hidden = new ArrayList<>(); + for (ConfigUnitResourceFieldDO f : mapper.selectListByCatalogItemId(catalogItemId)) { + // 返回用户勾选的字段(visible=true 表示用户选中了该字段需要隐藏) + if (Boolean.TRUE.equals(f.getVisible())) hidden.add(f.getFieldCode()); + } + return hidden; + } + + private void validateFieldsMajorsNode(Long catalogItemId) { + QuotaCatalogItemDO node = quotaCatalogItemMapper.selectById(catalogItemId); + if (node == null || !"fields_majors".equals(node.getNodeType())) { + throw exception(CONFIG_UNIT_RESOURCE_FIELD_NOT_FIELDS_MAJORS); + } + } + + private void validateFieldCodeUnique(Long catalogItemId, String fieldCode, Long excludeId) { + for (ConfigUnitResourceFieldDO f : mapper.selectListByCatalogItemId(catalogItemId)) { + if (f.getFieldCode().equals(fieldCode) && !f.getId().equals(excludeId)) { + throw exception(CONFIG_UNIT_RESOURCE_FIELD_CODE_DUPLICATE, fieldCode); + } + } + } + + private Integer calculateSortOrder(ConfigUnitResourceFieldSaveReqVO reqVO) { + String pos = reqVO.getInsertPosition(); + Long refId = reqVO.getReferenceNodeId(); + if (pos == null || "end".equals(pos) || refId == null) { + return mapper.selectMaxSortOrder(reqVO.getCatalogItemId()) + 1; + } + ConfigUnitResourceFieldDO ref = mapper.selectById(refId); + if (ref == null) return mapper.selectMaxSortOrder(reqVO.getCatalogItemId()) + 1; + + Integer target = "above".equals(pos) ? ref.getSortOrder() : ref.getSortOrder() + 1; + for (ConfigUnitResourceFieldDO n : mapper.selectListByCatalogItemIdAndSortOrderGe(reqVO.getCatalogItemId(), target)) { + n.setSortOrder(n.getSortOrder() + 1); + mapper.updateById(n); + } + return target; + } + + private ConfigUnitResourceFieldRespVO convertToRespVO(ConfigUnitResourceFieldDO f) { + ConfigUnitResourceFieldRespVO vo = new ConfigUnitResourceFieldRespVO(); + vo.setId(f.getId()); + vo.setCatalogItemId(f.getCatalogItemId()); + vo.setSeqNo(f.getSeqNo()); + vo.setFieldName(f.getFieldName()); + vo.setFieldCode(f.getFieldCode()); + vo.setVisible(f.getVisible()); + vo.setRemark(f.getRemark()); + vo.setSortOrder(f.getSortOrder()); + vo.setCreateTime(f.getCreateTime()); + return vo; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/config/impl/ConfigUnitTabRefServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/config/impl/ConfigUnitTabRefServiceImpl.java new file mode 100644 index 0000000..1ec009f --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/config/impl/ConfigUnitTabRefServiceImpl.java @@ -0,0 +1,179 @@ +package com.yhy.module.core.service.config.impl; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_UNIT_TAB_REF_INVALID_TAB_TYPE; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_UNIT_TAB_REF_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_UNIT_TAB_REF_NOT_FIELDS_MAJORS; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_UNIT_TAB_REF_NOT_SAME_CATALOG; +import static com.yhy.module.core.enums.ErrorCodeConstants.CONFIG_UNIT_TAB_REF_TEMPLATE_NOT_EXISTS; + +import cn.hutool.core.collection.CollUtil; +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitDivisionTemplateRespVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitTabRefRespVO; +import com.yhy.module.core.controller.admin.config.vo.ConfigUnitTabRefSaveReqVO; +import com.yhy.module.core.dal.dataobject.config.ConfigUnitDivisionTemplateDO; +import com.yhy.module.core.dal.dataobject.config.ConfigUnitTabRefDO; +import com.yhy.module.core.dal.dataobject.quota.QuotaCatalogItemDO; +import com.yhy.module.core.dal.mysql.config.ConfigUnitDivisionTemplateMapper; +import com.yhy.module.core.dal.mysql.config.ConfigUnitTabRefMapper; +import com.yhy.module.core.dal.mysql.quota.QuotaCatalogItemMapper; +import com.yhy.module.core.service.config.ConfigUnitTabRefService; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +@Service +@Validated +@Slf4j +public class ConfigUnitTabRefServiceImpl implements ConfigUnitTabRefService { + + @Resource + private ConfigUnitTabRefMapper tabRefMapper; + @Resource + private ConfigUnitDivisionTemplateMapper templateMapper; + @Resource + private QuotaCatalogItemMapper quotaCatalogItemMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveRefs(ConfigUnitTabRefSaveReqVO saveReqVO) { + // 1. 校验 + validateFieldsMajorsNode(saveReqVO.getCatalogItemId()); + validateTabType(saveReqVO.getTabType()); + + // 2. 校验所有模板节点存在 + for (Long nodeId : saveReqVO.getTemplateNodeIds()) { + ConfigUnitDivisionTemplateDO tpl = templateMapper.selectById(nodeId); + if (tpl == null) throw exception(CONFIG_UNIT_TAB_REF_TEMPLATE_NOT_EXISTS); + } + + // 3. 查询现有引用 + List existRefs = tabRefMapper.selectListByCatalogAndTab( + saveReqVO.getCatalogItemId(), saveReqVO.getTabType()); + Set existNodeIds = existRefs.stream() + .map(ConfigUnitTabRefDO::getTemplateNodeId).collect(Collectors.toSet()); + Set newNodeIds = new HashSet<>(saveReqVO.getTemplateNodeIds()); + + // 4. 删除不再引用的 + for (ConfigUnitTabRefDO ref : existRefs) { + if (!newNodeIds.contains(ref.getTemplateNodeId())) { + tabRefMapper.deleteById(ref.getId()); + } + } + + // 5. 新增新引用 + int maxSort = tabRefMapper.selectMaxSortOrder(saveReqVO.getCatalogItemId(), saveReqVO.getTabType()); + for (Long nodeId : saveReqVO.getTemplateNodeIds()) { + if (!existNodeIds.contains(nodeId)) { + ConfigUnitTabRefDO ref = ConfigUnitTabRefDO.builder() + .catalogItemId(saveReqVO.getCatalogItemId()) + .tabType(saveReqVO.getTabType()) + .templateNodeId(nodeId) + .sortOrder(++maxSort) + .build(); + tabRefMapper.insert(ref); + } + } + } + + @Override + public List getList(Long catalogItemId, String tabType) { + List refs = tabRefMapper.selectListByCatalogAndTab(catalogItemId, tabType); + if (CollUtil.isEmpty(refs)) return Collections.emptyList(); + + // 查询所有模板数据(用于填充详情和子节点) + List allTemplates = templateMapper.selectListByCatalogItemId(catalogItemId); + Map templateMap = allTemplates.stream() + .collect(Collectors.toMap(ConfigUnitDivisionTemplateDO::getId, t -> t)); + + List result = new ArrayList<>(); + for (ConfigUnitTabRefDO ref : refs) { + ConfigUnitTabRefRespVO vo = new ConfigUnitTabRefRespVO(); + vo.setId(ref.getId()); + vo.setCatalogItemId(ref.getCatalogItemId()); + vo.setTabType(ref.getTabType()); + vo.setTemplateNodeId(ref.getTemplateNodeId()); + vo.setSortOrder(ref.getSortOrder()); + vo.setCreateTime(ref.getCreateTime()); + + // 填充模板节点信息 + ConfigUnitDivisionTemplateDO tpl = templateMap.get(ref.getTemplateNodeId()); + if (tpl != null) { + vo.setNodeType(tpl.getNodeType()); + vo.setCode(tpl.getCode()); + vo.setName(tpl.getName()); + vo.setUnit(tpl.getUnit()); + + // 如果是分部节点,填充子节点 + if (ConfigUnitDivisionTemplateDO.NODE_TYPE_DIVISION.equals(tpl.getNodeType())) { + List children = allTemplates.stream() + .filter(t -> ref.getTemplateNodeId().equals(t.getParentId())) + .sorted(Comparator.comparingInt(t -> t.getSortOrder() != null ? t.getSortOrder() : 0)) + .map(this::convertTemplateToRespVO) + .collect(Collectors.toList()); + vo.setChildren(children); + } + } + result.add(vo); + } + return result; + } + + @Override + public void delete(Long id) { + if (tabRefMapper.selectById(id) == null) throw exception(CONFIG_UNIT_TAB_REF_NOT_EXISTS); + tabRefMapper.deleteById(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void swapSort(Long nodeId1, Long nodeId2) { + ConfigUnitTabRefDO n1 = tabRefMapper.selectById(nodeId1); + ConfigUnitTabRefDO n2 = tabRefMapper.selectById(nodeId2); + if (n1 == null || n2 == null) throw exception(CONFIG_UNIT_TAB_REF_NOT_EXISTS); + if (!n1.getCatalogItemId().equals(n2.getCatalogItemId())) throw exception(CONFIG_UNIT_TAB_REF_NOT_SAME_CATALOG); + + Integer temp = n1.getSortOrder(); + n1.setSortOrder(n2.getSortOrder()); + n2.setSortOrder(temp); + tabRefMapper.updateById(n1); + tabRefMapper.updateById(n2); + } + + private void validateFieldsMajorsNode(Long catalogItemId) { + QuotaCatalogItemDO node = quotaCatalogItemMapper.selectById(catalogItemId); + if (node == null || !"fields_majors".equals(node.getNodeType())) { + throw exception(CONFIG_UNIT_TAB_REF_NOT_FIELDS_MAJORS); + } + } + + private void validateTabType(String tabType) { + if (!ConfigUnitTabRefDO.VALID_TAB_TYPES.contains(tabType)) { + throw exception(CONFIG_UNIT_TAB_REF_INVALID_TAB_TYPE); + } + } + + private ConfigUnitDivisionTemplateRespVO convertTemplateToRespVO(ConfigUnitDivisionTemplateDO node) { + ConfigUnitDivisionTemplateRespVO vo = new ConfigUnitDivisionTemplateRespVO(); + vo.setId(node.getId()); + vo.setCatalogItemId(node.getCatalogItemId()); + vo.setParentId(node.getParentId()); + vo.setNodeType(node.getNodeType()); + vo.setCode(node.getCode()); + vo.setName(node.getName()); + vo.setUnit(node.getUnit()); + vo.setSortOrder(node.getSortOrder()); + vo.setCreateTime(node.getCreateTime()); + return vo; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/InfoPriceBookService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/InfoPriceBookService.java index d074279..b2a11de 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/InfoPriceBookService.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/InfoPriceBookService.java @@ -51,4 +51,38 @@ public interface InfoPriceBookService { * @return 分页结果 */ PageResult getInfoPriceBookPage(InfoPriceBookPageReqVO pageReqVO); + + /** + * 分页查询信息价册(排除指定租户) + * + * @param pageReqVO 分页查询条件 + * @param excludeTenantId 要排除的租户ID + * @return 分页结果 + */ + PageResult getInfoPriceBookPageExcludeTenant(InfoPriceBookPageReqVO pageReqVO, Long excludeTenantId); + + /** + * 复制信息价册 + * + * @param sourceId 源信息价册ID + * @return 新信息价册ID + */ + Long copyInfoPriceBook(Long sourceId); + + /** + * 根据树节点ID查询全部信息价册 + * + * @param treeNodeId 树节点ID + * @param excludeBookId 排除的信息价册ID(可选) + * @return 信息价册列表 + */ + java.util.List getInfoPriceBookAll(InfoPriceBookPageReqVO reqVO); + + /** + * 根据树节点ID查询信息价册简要列表(工作台专用,不过滤发布状态) + * + * @param treeNodeId 树节点ID + * @return 信息价册列表(仅包含id、name、startTime、endTime) + */ + java.util.List getInfoPriceBookListForWorkbench(Long treeNodeId); } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/InfoPriceCategoryTreeService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/InfoPriceCategoryTreeService.java index d59c2d2..de62e38 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/InfoPriceCategoryTreeService.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/InfoPriceCategoryTreeService.java @@ -67,4 +67,12 @@ public interface InfoPriceCategoryTreeService { */ void swapSort(@Valid InfoPriceCategoryTreeSwapSortReqVO reqVO); + /** + * 批量获取多个信息价册的分类树(树形结构) + * + * @param bookIds 信息价册ID列表 + * @return Map,key为bookId,value为对应的分类树 + */ + java.util.Map> getCategoryTreeTreeBatch(List bookIds); + } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/InfoPriceResourcePriceService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/InfoPriceResourcePriceService.java index c078f06..2525abc 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/InfoPriceResourcePriceService.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/InfoPriceResourcePriceService.java @@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceResourcePricePageReqVO; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceResourcePriceRespVO; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceResourcePriceSaveReqVO; +import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceResourceSaveReqVO; import javax.validation.Valid; import java.util.List; @@ -61,4 +62,28 @@ public interface InfoPriceResourcePriceService { */ PageResult getResourcePricePage(@Valid InfoPriceResourcePricePageReqVO pageReqVO); + /** + * 批量创建信息价工料机价格历史 + * + * @param createReqVOList 创建信息列表 + * @return 创建的ID列表 + */ + List createResourcePriceBatch(@Valid List createReqVOList); + + /** + * 根据源工料机ID查询价格历史列表 + * + * @param sourceResourceItemId 源工料机ID + * @return 价格历史列表 + */ + List getResourcePriceListBySourceResourceItemId(Long sourceResourceItemId); + + /** + * 复制工料机价格历史 + * + * @param createReqVOList 创建请求列表 + * @param newResourceIds 新创建的资源ID列表 + */ + void copyResourcePriceHistories(List createReqVOList, List newResourceIds); + } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/InfoPriceResourceService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/InfoPriceResourceService.java index a820443..cb7b158 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/InfoPriceResourceService.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/InfoPriceResourceService.java @@ -61,4 +61,20 @@ public interface InfoPriceResourceService { */ List getResourceList(Long categoryTreeId); + /** + * 批量创建信息价工料机信息 + * + * @param createReqVOList 创建信息列表 + * @return 创建的ID列表 + */ + List createResourceBatch(@Valid List createReqVOList); + + /** + * 根据源工料机ID查询信息价工料机信息列表 + * + * @param sourceResourceItemId 源工料机ID + * @return 信息价工料机信息列表 + */ + List getResourceListBySourceResourceItemId(Long sourceResourceItemId); + } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/impl/InfoPriceBookServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/impl/InfoPriceBookServiceImpl.java index f802483..36de8d6 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/impl/InfoPriceBookServiceImpl.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/impl/InfoPriceBookServiceImpl.java @@ -3,15 +3,32 @@ package com.yhy.module.core.service.infoprice.impl; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceBookPageReqVO; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceBookRespVO; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceBookSaveReqVO; +import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceCategoryTreeRespVO; +import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceCategoryTreeSaveReqVO; +import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceResourceRespVO; +import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceResourceSaveReqVO; +import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceResourcePriceRespVO; +import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceResourcePriceSaveReqVO; import com.yhy.module.core.dal.dataobject.infoprice.InfoPriceBookDO; +import com.yhy.module.core.dal.dataobject.infoprice.InfoPriceCategoryTreeDO; +import com.yhy.module.core.dal.dataobject.infoprice.InfoPriceResourceDO; import com.yhy.module.core.dal.dataobject.infoprice.InfoPriceTreeDO; +import com.yhy.module.core.dal.dataobject.resource.ResourceCatalogItemDO; import com.yhy.module.core.dal.mysql.infoprice.InfoPriceBookMapper; +import com.yhy.module.core.dal.mysql.infoprice.InfoPriceCategoryTreeMapper; +import com.yhy.module.core.dal.mysql.infoprice.InfoPriceResourceMapper; +import com.yhy.module.core.dal.mysql.infoprice.InfoPriceResourcePriceMapper; import com.yhy.module.core.dal.mysql.infoprice.InfoPriceTreeMapper; import com.yhy.module.core.enums.infoprice.InfoPricePublishStatusEnum; import com.yhy.module.core.service.infoprice.InfoPriceBookService; +import com.yhy.module.core.service.infoprice.InfoPriceCategoryTreeService; +import com.yhy.module.core.service.infoprice.InfoPriceResourceService; +import com.yhy.module.core.service.infoprice.InfoPriceResourcePriceService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -41,35 +58,49 @@ public class InfoPriceBookServiceImpl implements InfoPriceBookService { @Resource private InfoPriceTreeMapper infoPriceTreeMapper; + @Resource + private InfoPriceCategoryTreeMapper infoPriceCategoryTreeMapper; + + @Resource + private InfoPriceResourceMapper infoPriceResourceMapper; + + @Resource + private InfoPriceResourcePriceMapper infoPriceResourcePriceMapper; + + @Resource + private InfoPriceCategoryTreeService categoryTreeService; + + @Resource + private InfoPriceResourceService infoPriceResourceService; + + @Resource + private InfoPriceResourcePriceService infoPriceResourcePriceService; + @Override @Transactional(rollbackFor = Exception.class) public Long createInfoPriceBook(InfoPriceBookSaveReqVO createReqVO) { // 1. 验证树节点存在 validateTreeNodeExists(createReqVO.getTreeNodeId()); - - // 2. 验证时间范围 + + // 2. 验证时间范围(开始时间不能大于结束时间) validateTimeRange(createReqVO.getStartTime(), createReqVO.getEndTime()); + + // 3. 验证时间段不与同一树节点下的其他信息价册重叠 + validateTimeOverlap(createReqVO.getTreeNodeId(), createReqVO.getStartTime(), createReqVO.getEndTime(), null); - // 3. 验证发布状态 - if (createReqVO.getPublishStatus() != null) { - validatePublishStatus(createReqVO.getPublishStatus()); - } + // 4. 验证发布状态 +// if (createReqVO.getPublishStatus() != null) { +// validatePublishStatus(createReqVO.getPublishStatus()); +// } + createReqVO.setPublishStatus(InfoPricePublishStatusEnum.UNCOMPLETED.getCode()); - // 4. 构建DO对象 + // 5. 构建DO对象 InfoPriceBookDO book = BeanUtils.toBean(createReqVO, InfoPriceBookDO.class); Long tenantId = cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder.getTenantId(); book.setTenantId(tenantId); - // 5. 插入数据库(时间段重叠由数据库约束保证) - try { - infoPriceBookMapper.insert(book); - } catch (Exception e) { - // 捕获数据库约束异常 - if (e.getMessage().contains("exclude_time_overlap")) { - throw exception(INFO_PRICE_BOOK_TIME_OVERLAP); - } - throw e; - } + // 6. 插入数据库 + infoPriceBookMapper.insert(book); return book.getId(); } @@ -83,24 +114,36 @@ public class InfoPriceBookServiceImpl implements InfoPriceBookService { // 2. 验证树节点存在 validateTreeNodeExists(updateReqVO.getTreeNodeId()); - // 3. 验证时间范围 + // 3. 验证时间范围(开始时间不能大于结束时间) validateTimeRange(updateReqVO.getStartTime(), updateReqVO.getEndTime()); + + // 4. 验证时间段不与同一树节点下的其他信息价册重叠(排除自身) + validateTimeOverlap(updateReqVO.getTreeNodeId(), updateReqVO.getStartTime(), updateReqVO.getEndTime(), updateReqVO.getId()); - // 4. 验证发布状态 + // 5. 验证发布状态 if (updateReqVO.getPublishStatus() != null) { validatePublishStatus(updateReqVO.getPublishStatus()); } - // 5. 更新数据 + // 6. 更新数据 InfoPriceBookDO updateObj = BeanUtils.toBean(updateReqVO, InfoPriceBookDO.class); - try { + + // 如果发布状态为completed,设置完成时间 + if(updateReqVO.getPublishStatus() != null && updateReqVO.getPublishStatus().equals(InfoPricePublishStatusEnum.COMPLETED.getCode())){ + updateObj.setCompletedTime(java.time.LocalDateTime.now()); + } + + // 使用 LambdaUpdateWrapper 明确设置 publishTime 为 null + if(updateReqVO.getPublishStatus() != null && updateReqVO.getPublishStatus().equals(InfoPricePublishStatusEnum.UNPUBLISHED.getCode())){ + com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper updateWrapper = + new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<>(); + updateWrapper.eq(InfoPriceBookDO::getId, updateReqVO.getId()) + .set(InfoPriceBookDO::getPublishStatus, updateReqVO.getPublishStatus()) + .set(InfoPriceBookDO::getPublishTime, null); // 明确设置为 null + + infoPriceBookMapper.update(null, updateWrapper); + }else{ infoPriceBookMapper.updateById(updateObj); - } catch (Exception e) { - // 捕获数据库约束异常 - if (e.getMessage().contains("exclude_time_overlap")) { - throw exception(INFO_PRICE_BOOK_TIME_OVERLAP); - } - throw e; } } @@ -142,6 +185,32 @@ public class InfoPriceBookServiceImpl implements InfoPriceBookService { // 2. 转换为VO List voList = BeanUtils.toBean(pageResult.getList(), InfoPriceBookRespVO.class); + // 3. 批量计算虚拟字段"地区" + List treeNodeIds = voList.stream() + .map(InfoPriceBookRespVO::getTreeNodeId) + .distinct() + .collect(Collectors.toList()); + + Map regionMap = TenantUtils.executeIgnore(() -> + calculateRegionBatch(treeNodeIds) + ); + + voList.forEach(vo -> vo.setRegion(regionMap.get(vo.getTreeNodeId()))); + + return new PageResult<>(voList, pageResult.getTotal()); + } + + @Override + public PageResult getInfoPriceBookPageExcludeTenant(InfoPriceBookPageReqVO pageReqVO, Long excludeTenantId) { + // 1. 分页查询(排除指定租户) + PageResult pageResult = infoPriceBookMapper.selectPageExcludeTenant(pageReqVO, excludeTenantId); + if (CollUtil.isEmpty(pageResult.getList())) { + return PageResult.empty(pageResult.getTotal()); + } + + // 2. 转换为VO + List voList = BeanUtils.toBean(pageResult.getList(), InfoPriceBookRespVO.class); + // 3. 批量计算虚拟字段"地区" List treeNodeIds = voList.stream() .map(InfoPriceBookRespVO::getTreeNodeId) @@ -152,16 +221,131 @@ public class InfoPriceBookServiceImpl implements InfoPriceBookService { voList.forEach(vo -> vo.setRegion(regionMap.get(vo.getTreeNodeId()))); + // 4. 批量统计每个信息价册下的工料机总数 + List bookIds = voList.stream() + .map(InfoPriceBookRespVO::getId) + .collect(Collectors.toList()); + Map resourceCountMap = calculateResourceCountBatch(bookIds); + + voList.forEach(vo -> vo.setResourceCount(resourceCountMap.getOrDefault(vo.getId(), 0L))); + return new PageResult<>(voList, pageResult.getTotal()); } + @Override + @Transactional(rollbackFor = Exception.class) + public Long copyInfoPriceBook(Long sourceId) { + // 1. 查询源信息价册(忽略租户过滤,支持跨租户复制) + InfoPriceBookDO sourceBook = cn.iocoder.yudao.framework.tenant.core.util.TenantUtils.executeIgnore(() -> + infoPriceBookMapper.selectById(sourceId) + ); + + if (sourceBook == null) { + throw exception(INFO_PRICE_BOOK_NOT_EXISTS); + } + + // 2. 验证树节点存在(在当前租户下) + validateTreeNodeExists(sourceBook.getTreeNodeId()); + + // 3. 验证时间段不与当前租户下的其他信息价册重叠 + validateTimeOverlap(sourceBook.getTreeNodeId(), sourceBook.getStartTime(), sourceBook.getEndTime(), null); + + // 4. 构建新的信息价册对象 + InfoPriceBookDO newBook = InfoPriceBookDO.builder() + .treeNodeId(sourceBook.getTreeNodeId()) + .catalogVersion(sourceBook.getCatalogVersion()) + .name(sourceBook.getName()) + .startTime(sourceBook.getStartTime()) + .endTime(sourceBook.getEndTime()) + .publishStatus(InfoPricePublishStatusEnum.PUBLISHED.getCode()) + .publishTime(java.time.LocalDateTime.now()) // 发布时间设置为当前时间 + .attachment(sourceBook.getAttachment()) + .tenantId(cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder.getTenantId()) + .build(); + + // 5. 插入数据库 + infoPriceBookMapper.insert(newBook); + + // 6. 获取源分类树后再创建新的树结构根据新的newBook.getId() + List sourceCategoryTrees = categoryTreeService.getCategoryTreeTree(sourceBook.getId()); + if (CollUtil.isNotEmpty(sourceCategoryTrees)) { + copyCategoryTrees(sourceCategoryTrees, null, newBook.getId()); + } + + // 7. 获取源分类树后的所有id来查询全部信息价工料机信息InfoPriceResourceService把所有categoryTreeId修改为新的再创建复制内容 + List newCategoryTrees = infoPriceCategoryTreeMapper.selectListByBookId(newBook.getId()); + if (CollUtil.isNotEmpty(newCategoryTrees)) { + copyResources(sourceBook.getId(), newCategoryTrees); + } + // 8. 获取源对应信息价工料机信息关联id的InfoPriceResourcePriceService的信息价工料机价格历史,创建复制内容 + copyResourcePrices(sourceBook.getId(), newBook.getId()); + log.info("复制信息价册成功,源ID: {}, 新ID: {}", sourceId, newBook.getId()); + return newBook.getId(); + } + + @Override + public List getInfoPriceBookAll(InfoPriceBookPageReqVO reqVO) { + // 1. 查询该树节点下的所有信息价册 + cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX wrapper = + new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX() + .eq(InfoPriceBookDO::getPublishStatus,reqVO.getPublishStatus()) + .eq(InfoPriceBookDO::getTreeNodeId, reqVO.getTreeNodeId()) + .neIfPresent(InfoPriceBookDO::getId, reqVO.getExcludeBookId()) + .orderByDesc(InfoPriceBookDO::getStartTime); + + List bookList = infoPriceBookMapper.selectList(wrapper); + + if (CollUtil.isEmpty(bookList)) { + return java.util.Collections.emptyList(); + } + + // 2. 转换为VO + List voList = BeanUtils.toBean(bookList, InfoPriceBookRespVO.class); + + // 3. 计算虚拟字段"地区" + String region = calculateRegion(reqVO.getTreeNodeId()); + + // 4. 批量查询所有信息价册的分类树(一次SQL查询) + List bookIds = voList.stream() + .map(InfoPriceBookRespVO::getId) + .collect(Collectors.toList()); + Map> categoryTreeMap = categoryTreeService.getCategoryTreeTreeBatch(bookIds); + + // 5. 为每个信息价册填充分类树子节点 + for (InfoPriceBookRespVO vo : voList) { + vo.setRegion(region); + vo.setChildren(categoryTreeMap.getOrDefault(vo.getId(), java.util.Collections.emptyList())); + } + + return voList; + } + + @Override + public List getInfoPriceBookListForWorkbench(Long treeNodeId) { + // 查询该树节点下的所有信息价册(不过滤发布状态) + cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX wrapper = + new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX() + .eq(InfoPriceBookDO::getTreeNodeId, treeNodeId) + .orderByDesc(InfoPriceBookDO::getStartTime); + + List bookList = infoPriceBookMapper.selectList(wrapper); + + if (CollUtil.isEmpty(bookList)) { + return java.util.Collections.emptyList(); + } + + return BeanUtils.toBean(bookList, InfoPriceBookRespVO.class); + } + // ==================== 私有方法 ==================== /** * 验证信息价册存在 */ private InfoPriceBookDO validateBookExists(Long id) { - InfoPriceBookDO book = infoPriceBookMapper.selectById(id); + InfoPriceBookDO book = TenantUtils.executeIsAdmin(() -> + infoPriceBookMapper.selectById(id) + ); if (book == null) { throw exception(INFO_PRICE_BOOK_NOT_EXISTS); } @@ -172,14 +356,16 @@ public class InfoPriceBookServiceImpl implements InfoPriceBookService { * 验证树节点存在 */ private void validateTreeNodeExists(Long treeNodeId) { - InfoPriceTreeDO tree = infoPriceTreeMapper.selectById(treeNodeId); + InfoPriceTreeDO tree = TenantUtils.executeIgnore(() -> + infoPriceTreeMapper.selectById(treeNodeId) + ); if (tree == null) { throw exception(INFO_PRICE_TREE_NOT_EXISTS); } } /** - * 验证时间范围 + * 验证时间范围(开始时间不能大于结束时间) */ private void validateTimeRange(java.time.LocalDate startTime, java.time.LocalDate endTime) { if (startTime != null && endTime != null && endTime.isBefore(startTime)) { @@ -187,6 +373,39 @@ public class InfoPriceBookServiceImpl implements InfoPriceBookService { } } + /** + * 验证时间段不与同一树节点下的其他信息价册重叠 + * + * @param treeNodeId 树节点ID + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param excludeId 排除的ID(更新时排除自身) + */ + private void validateTimeOverlap(Long treeNodeId, java.time.LocalDate startTime, java.time.LocalDate endTime, Long excludeId) { + if (treeNodeId == null || startTime == null || endTime == null) { + return; + } + + // 查询同一树节点下的所有信息价册 + List existingBooks = infoPriceBookMapper.selectList( + new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX() + .eq(InfoPriceBookDO::getTreeNodeId, treeNodeId) + .neIfPresent(InfoPriceBookDO::getId, excludeId) + ); + + // 检查时间段是否有交集 + for (InfoPriceBookDO book : existingBooks) { + if (book.getStartTime() == null || book.getEndTime() == null) { + continue; + } + // 判断时间段是否有交集:!(新结束 < 旧开始 || 新开始 > 旧结束) + // 即:新结束 >= 旧开始 && 新开始 <= 旧结束 + if (!endTime.isBefore(book.getStartTime()) && !startTime.isAfter(book.getEndTime())) { + throw exception(INFO_PRICE_BOOK_TIME_OVERLAP); + } + } + } + /** * 验证发布状态 */ @@ -277,4 +496,234 @@ public class InfoPriceBookServiceImpl implements InfoPriceBookService { } )); } + + /** + * 批量统计每个信息价册下的工料机总数 + * + * SQL逻辑: + * 1. 根据 bookIds 查询所有分类树节点(yhy_info_price_category_tree) + * 2. 根据分类树节点ID统计工料机数量(yhy_info_price_resource) + * 3. 按 bookId 汇总工料机总数 + * + * @param bookIds 信息价册ID列表 + * @return Map,key为bookId,value为工料机总数 + */ + private Map calculateResourceCountBatch(List bookIds) { + if (CollUtil.isEmpty(bookIds)) { + return java.util.Collections.emptyMap(); + } + + // 1. 查询所有信息价册下的分类树节点 + List categoryTrees = infoPriceCategoryTreeMapper.selectListByBookIds(bookIds); + if (CollUtil.isEmpty(categoryTrees)) { + return java.util.Collections.emptyMap(); + } + + // 2. 构建 categoryTreeId -> bookId 的映射 + Map categoryTreeToBookMap = categoryTrees.stream() + .collect(Collectors.toMap(InfoPriceCategoryTreeDO::getId, InfoPriceCategoryTreeDO::getBookId)); + + // 3. 获取所有分类树节点ID + List categoryTreeIds = categoryTrees.stream() + .map(InfoPriceCategoryTreeDO::getId) + .collect(Collectors.toList()); + + // 4. 批量统计每个分类树节点下的工料机数量 + Map categoryTreeResourceCountMap = infoPriceResourceMapper.countByCategoryTreeIds(categoryTreeIds); + + // 5. 按 bookId 汇总工料机总数 + Map bookResourceCountMap = new java.util.HashMap<>(); + for (Map.Entry entry : categoryTreeResourceCountMap.entrySet()) { + Long categoryTreeId = entry.getKey(); + Long resourceCount = entry.getValue(); + Long bookId = categoryTreeToBookMap.get(categoryTreeId); + if (bookId != null) { + bookResourceCountMap.merge(bookId, resourceCount, Long::sum); + } + } + + return bookResourceCountMap; + } + + /** + * 递归复制分类树 + * + * @param sourceTrees 源分类树列表 + * @param newParentId 新的父节点ID + * @param newBookId 新的信息价册ID + * @return 源节点ID到新节点ID的映射 + */ + private Map copyCategoryTrees(List sourceTrees, Long newParentId, Long newBookId) { + Map idMapping = new java.util.HashMap<>(); + + for (InfoPriceCategoryTreeRespVO sourceTree : sourceTrees) { + // 只处理顶级节点或指定父节点的子节点 +// if (newParentId == null && sourceTree.getParentId() != null) { +// continue; // 跳过非顶级节点 +// } +// if (newParentId != null && !newParentId.equals(sourceTree.getParentId())) { +// continue; // 跳过不是指定父节点的子节点 +// } + + // 创建新的分类树节点 + InfoPriceCategoryTreeSaveReqVO createReqVO = new InfoPriceCategoryTreeSaveReqVO(); + createReqVO.setBookId(newBookId); + createReqVO.setParentId(newParentId); + createReqVO.setCode(sourceTree.getCode()); + createReqVO.setName(sourceTree.getName()); + createReqVO.setNodeType(sourceTree.getNodeType()); + createReqVO.setAttributes(sourceTree.getAttributes()); + + Long newCategoryId = categoryTreeService.createCategoryTree(createReqVO); + idMapping.put(sourceTree.getId(), newCategoryId); + + // 递归处理子节点 +// List children = sourceTrees.stream() +// .filter(tree -> sourceTree.getId().equals(tree.getParentId())) +// .collect(Collectors.toList()); + if (CollUtil.isNotEmpty(sourceTree.getChildren())) { + Map childMapping = copyCategoryTrees(sourceTree.getChildren(), newCategoryId, newBookId); + idMapping.putAll(childMapping); + } + } + + return idMapping; + } + + /** + * 复制工料机信息 + * + * @param sourceBookId 源信息价册ID + * @param newCategoryTrees 新的分类树节点列表 + */ + private void copyResources(Long sourceBookId, List newCategoryTrees) { + // 构建新分类树节点的ID映射(用于查找对应的源节点) +// Map codeToNewIdMap = newCategoryTrees.stream() +// .collect(Collectors.toMap( +// InfoPriceCategoryTreeDO::getCode, +// InfoPriceCategoryTreeDO::getId +// )); + + // 获取源分类树节点(用于查找对应的工料机) + List sourceCategoryTrees = cn.iocoder.yudao.framework.tenant.core.util.TenantUtils.executeIgnore(() -> + infoPriceCategoryTreeMapper.selectListByBookId(sourceBookId) + ); + + // 构建源分类树节点编码到ID的映射 + Map codeToSourceIdMap = sourceCategoryTrees.stream() + .collect(Collectors.toMap( + InfoPriceCategoryTreeDO::getCode, + InfoPriceCategoryTreeDO::getId + )); + + // 为每个新分类树节点复制工料机信息 + for (InfoPriceCategoryTreeDO newCategoryTree : newCategoryTrees) { + String code = newCategoryTree.getCode(); + Long sourceCategoryId = codeToSourceIdMap.get(code); + + if (sourceCategoryId != null) { + // 查询源分类树节点下的工料机信息 + List sourceResources = cn.iocoder.yudao.framework.tenant.core.util.TenantUtils.executeIgnore(() -> + infoPriceResourceService.getResourceList(sourceCategoryId) + ); + + if (CollUtil.isNotEmpty(sourceResources)) { + // 转换为创建请求VO + List createReqVOList = sourceResources.stream() + .map(sourceResource -> { + InfoPriceResourceSaveReqVO createReqVO = new InfoPriceResourceSaveReqVO(); + createReqVO.setCategoryTreeId(newCategoryTree.getId()); + createReqVO.setCode(sourceResource.getCode()+"-copy"); + createReqVO.setName(sourceResource.getName()); + createReqVO.setSpec(sourceResource.getSpec()); + createReqVO.setUnit(sourceResource.getUnit()); + createReqVO.setPriceTaxExcl(sourceResource.getPriceTaxExcl()); + createReqVO.setTaxRate(sourceResource.getTaxRate()); + createReqVO.setPriceTaxIncl(sourceResource.getPriceTaxIncl()); + createReqVO.setDrawingUrl(sourceResource.getDrawingUrl()); + createReqVO.setCategoryId(sourceResource.getCategoryId()); + createReqVO.setRemark(sourceResource.getRemark()); + createReqVO.setAttributes(sourceResource.getAttributes()); + return createReqVO; + }) + .collect(Collectors.toList()); + + // 批量创建工料机信息 + infoPriceResourceService.createResourceBatch(createReqVOList); + } + } + } + } + + /** + * 复制工料机价格历史 + * + * @param sourceBookId 源信息价册ID + * @param newBookId 新信息价册ID + */ + private void copyResourcePrices(Long sourceBookId, Long newBookId) { + // 获取源册的所有工料机信息 + List sourceResources = cn.iocoder.yudao.framework.tenant.core.util.TenantUtils.executeIgnore(() -> + infoPriceResourceMapper.selectList( + new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX() + .in(InfoPriceResourceDO::getCategoryTreeId, + infoPriceCategoryTreeMapper.selectListByBookId(sourceBookId).stream() + .map(InfoPriceCategoryTreeDO::getId) + .collect(Collectors.toList())) + ) + ); + + if (CollUtil.isEmpty(sourceResources)) { + return; + } + + // 获取新册的所有工料机信息,建立编码映射 + List newResources = infoPriceResourceMapper.selectList( + new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX() + .in(InfoPriceResourceDO::getCategoryTreeId, + infoPriceCategoryTreeMapper.selectListByBookId(newBookId).stream() + .map(InfoPriceCategoryTreeDO::getId) + .collect(Collectors.toList())) + ); + + // 建立编码到新资源ID的映射 + Map codeToNewResourceIdMap = newResources.stream() + .collect(Collectors.toMap( + InfoPriceResourceDO::getCode, + InfoPriceResourceDO::getId + )); + + // 为每个源工料机复制价格历史 + for (InfoPriceResourceDO sourceResource : sourceResources) { + String code = sourceResource.getCode(); + Long newResourceId = codeToNewResourceIdMap.get(code); + + if (newResourceId != null) { + // 查询源工料机的价格历史 + List sourcePrices = cn.iocoder.yudao.framework.tenant.core.util.TenantUtils.executeIgnore(() -> + infoPriceResourcePriceService.getResourcePriceList(sourceResource.getId()) + ); + + if (CollUtil.isNotEmpty(sourcePrices)) { + // 转换为创建请求VO列表 + List createReqVOList = sourcePrices.stream() + .map(sourcePrice -> { + InfoPriceResourcePriceSaveReqVO createReqVO = new InfoPriceResourcePriceSaveReqVO(); + createReqVO.setResourceId(newResourceId); + createReqVO.setStartTime(sourcePrice.getStartTime()); + createReqVO.setEndTime(sourcePrice.getEndTime()); + createReqVO.setPriceTaxExcl(sourcePrice.getPriceTaxExcl()); + createReqVO.setTaxRate(sourcePrice.getTaxRate()); + createReqVO.setPriceTaxIncl(sourcePrice.getPriceTaxIncl()); + createReqVO.setAttributes(sourcePrice.getAttributes()); + return createReqVO; + }) + .collect(Collectors.toList()); + + // 批量创建价格历史 + infoPriceResourcePriceService.createResourcePriceBatch(createReqVOList); + } + } + } + } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/impl/InfoPriceCategoryTreeServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/impl/InfoPriceCategoryTreeServiceImpl.java index 5b8e920..a366ee0 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/impl/InfoPriceCategoryTreeServiceImpl.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/impl/InfoPriceCategoryTreeServiceImpl.java @@ -1,6 +1,8 @@ package com.yhy.module.core.service.infoprice.impl; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceCategoryTreeRespVO; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceCategoryTreeSaveReqVO; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceCategoryTreeSwapSortReqVO; @@ -46,7 +48,18 @@ public class InfoPriceCategoryTreeServiceImpl implements InfoPriceCategoryTreeSe // 1. 校验节点类型 validateNodeType(createReqVO.getNodeType()); - // 2. 校验父节点 + // 2. 根节点唯一性校验:同一信息价册只允许一个根节点 + if (createReqVO.getParentId() == null) { + List roots = categoryTreeMapper.selectListByParentId( + createReqVO.getBookId(), null); + if (!roots.isEmpty()) { + throw new cn.iocoder.yudao.framework.common.exception.ServiceException( + cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST.getCode(), + "已存在根节点「" + roots.get(0).getName() + "」,不允许重复创建"); + } + } + + // 3. 校验父节点 InfoPriceCategoryTreeDO parent = null; if (createReqVO.getParentId() != null) { parent = validateParentExists(createReqVO.getParentId()); @@ -139,7 +152,9 @@ public class InfoPriceCategoryTreeServiceImpl implements InfoPriceCategoryTreeSe @Override public List getCategoryTreeTree(Long bookId) { - List list = categoryTreeMapper.selectListByBookId(bookId); + List list = TenantUtils.executeIsAdmin(() -> + categoryTreeMapper.selectListByBookId(bookId) + ); return buildTree(list); } @@ -249,4 +264,35 @@ public class InfoPriceCategoryTreeServiceImpl implements InfoPriceCategoryTreeSe return tree; } + @Override + public Map> getCategoryTreeTreeBatch(List bookIds) { + if (bookIds == null || bookIds.isEmpty()) { + return new java.util.HashMap<>(); + } + + // 1. 一次性查询所有分类树数据 + List allList = categoryTreeMapper.selectListByBookIds(bookIds); + if (allList.isEmpty()) { + // 返回空Map,每个bookId对应空列表 + Map> result = new java.util.HashMap<>(); + for (Long bookId : bookIds) { + result.put(bookId, new ArrayList<>()); + } + return result; + } + + // 2. 按bookId分组 + Map> groupedByBookId = allList.stream() + .collect(Collectors.groupingBy(InfoPriceCategoryTreeDO::getBookId)); + + // 3. 为每个bookId构建树 + Map> result = new java.util.HashMap<>(); + for (Long bookId : bookIds) { + List bookList = groupedByBookId.getOrDefault(bookId, new ArrayList<>()); + result.put(bookId, buildTree(bookList)); + } + + return result; + } + } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/impl/InfoPriceResourcePriceServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/impl/InfoPriceResourcePriceServiceImpl.java index c0ffb7c..d425cdc 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/impl/InfoPriceResourcePriceServiceImpl.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/impl/InfoPriceResourcePriceServiceImpl.java @@ -2,9 +2,11 @@ package com.yhy.module.core.service.infoprice.impl; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceResourcePricePageReqVO; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceResourcePriceRespVO; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceResourcePriceSaveReqVO; +import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceResourceSaveReqVO; import com.yhy.module.core.dal.dataobject.infoprice.InfoPriceResourceDO; import com.yhy.module.core.dal.dataobject.infoprice.InfoPriceResourcePriceDO; import com.yhy.module.core.dal.mysql.infoprice.InfoPriceResourceMapper; @@ -18,6 +20,8 @@ import org.springframework.validation.annotation.Validated; import javax.annotation.Resource; import java.time.LocalDate; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static com.yhy.module.core.enums.ErrorCodeConstants.*; @@ -137,4 +141,128 @@ public class InfoPriceResourcePriceServiceImpl implements InfoPriceResourcePrice } } + @Override + @Transactional(rollbackFor = Exception.class) + public List createResourcePriceBatch(List createReqVOList) { + if (createReqVOList == null || createReqVOList.isEmpty()) { + return new java.util.ArrayList<>(); + } + + List priceList = new java.util.ArrayList<>(); + + for (InfoPriceResourcePriceSaveReqVO createReqVO : createReqVOList) { + // 校验工料机信息存在 +// validateResourceExists(createReqVO.getResourceId()); + + // 校验时间段有效性 +// validateTimeRange(createReqVO.getStartTime(), createReqVO.getEndTime()); + + // 构建DO对象 + InfoPriceResourcePriceDO price = new InfoPriceResourcePriceDO(); + price.setResourceId(createReqVO.getResourceId()); + price.setStartTime(createReqVO.getStartTime()); + price.setEndTime(createReqVO.getEndTime()); + price.setPriceTaxExcl(createReqVO.getPriceTaxExcl()); + price.setTaxRate(createReqVO.getTaxRate()); + price.setPriceTaxIncl(createReqVO.getPriceTaxIncl()); + price.setAttributes(createReqVO.getAttributes()); + + priceList.add(price); + } + + // 批量插入 + infoPriceResourcePriceMapper.insertBatch(priceList); + + // 返回ID列表 + return priceList.stream() + .map(InfoPriceResourcePriceDO::getId) + .collect(Collectors.toList()); + } + + @Override + public List getResourcePriceListBySourceResourceItemId(Long sourceResourceItemId) { + // 1. 根据源工料机ID查询所有相关的信息价工料机 + List resourceList = infoPriceResourceMapper.selectList( + new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX() + .eq(InfoPriceResourceDO::getSourceResourceItemId, sourceResourceItemId)); + + if (resourceList.isEmpty()) { + return new java.util.ArrayList<>(); + } + + // 2. 提取所有resourceId + List resourceIds = resourceList.stream() + .map(InfoPriceResourceDO::getId) + .collect(Collectors.toList()); + + // 3. 批量查询所有价格历史 + List priceList = infoPriceResourcePriceMapper.selectList( + new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX() + .in(InfoPriceResourcePriceDO::getResourceId, resourceIds) + .orderByDesc(InfoPriceResourcePriceDO::getStartTime)); + + return BeanUtils.toBean(priceList, InfoPriceResourcePriceRespVO.class); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void copyResourcePriceHistories(List createReqVOList, List newResourceIds) { + // 收集所有sourceResourceItemId + List sourceResourceItemIds = createReqVOList.stream() + .map(InfoPriceResourceSaveReqVO::getSourceResourceItemId) + .filter(id -> id != null) + .distinct() + .collect(Collectors.toList()); + + if (sourceResourceItemIds.isEmpty()) { + return; + } + + // 批量查询源工料机的价格历史 + Map> sourcePriceHistories = new java.util.HashMap<>(); + for (Long sourceResourceItemId : sourceResourceItemIds) { + List priceHistories = TenantUtils.executeIgnore(() -> + getResourcePriceListBySourceResourceItemId(sourceResourceItemId) + ); + if (!priceHistories.isEmpty()) { + sourcePriceHistories.put(sourceResourceItemId, priceHistories); + } + } + + if (sourcePriceHistories.isEmpty()) { + return; + } + + // 构建新资源的价格历史创建请求 + List newPriceHistories = new java.util.ArrayList<>(); + + for (int i = 0; i < createReqVOList.size() && i < newResourceIds.size(); i++) { + InfoPriceResourceSaveReqVO createReq = createReqVOList.get(i); + Long newResourceId = newResourceIds.get(i); + Long sourceResourceItemId = createReq.getSourceResourceItemId(); + + if (sourceResourceItemId != null && sourcePriceHistories.containsKey(sourceResourceItemId)) { + List sourcePrices = sourcePriceHistories.get(sourceResourceItemId); + + for (InfoPriceResourcePriceRespVO sourcePrice : sourcePrices) { + InfoPriceResourcePriceSaveReqVO newPrice = new InfoPriceResourcePriceSaveReqVO(); + newPrice.setResourceId(newResourceId); + newPrice.setStartTime(sourcePrice.getStartTime()); + newPrice.setEndTime(sourcePrice.getEndTime()); + newPrice.setPriceTaxExcl(sourcePrice.getPriceTaxExcl()); + newPrice.setTaxRate(sourcePrice.getTaxRate()); + newPrice.setPriceTaxIncl(sourcePrice.getPriceTaxIncl()); + newPrice.setAttributes(sourcePrice.getAttributes()); + + newPriceHistories.add(newPrice); + } + } + } + + // 批量创建价格历史 + if (!newPriceHistories.isEmpty()) { + createResourcePriceBatch(newPriceHistories); + } + } + } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/impl/InfoPriceResourceServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/impl/InfoPriceResourceServiceImpl.java index 9fa0f57..44a0f64 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/impl/InfoPriceResourceServiceImpl.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/impl/InfoPriceResourceServiceImpl.java @@ -2,14 +2,22 @@ package com.yhy.module.core.service.infoprice.impl; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceResourcePageReqVO; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceResourceRespVO; import com.yhy.module.core.controller.admin.infoprice.vo.InfoPriceResourceSaveReqVO; +import com.yhy.module.core.dal.dataobject.infoprice.InfoPriceBookDO; import com.yhy.module.core.dal.dataobject.infoprice.InfoPriceCategoryTreeDO; import com.yhy.module.core.dal.dataobject.infoprice.InfoPriceResourceDO; +import com.yhy.module.core.dal.dataobject.infoprice.InfoPriceTreeDO; +import com.yhy.module.core.dal.dataobject.resource.ResourceItemDO; +import com.yhy.module.core.dal.mysql.infoprice.InfoPriceBookMapper; import com.yhy.module.core.dal.mysql.infoprice.InfoPriceCategoryTreeMapper; import com.yhy.module.core.dal.mysql.infoprice.InfoPriceResourceMapper; import com.yhy.module.core.dal.mysql.infoprice.InfoPriceResourcePriceMapper; +import com.yhy.module.core.dal.mysql.infoprice.InfoPriceTreeMapper; +import com.yhy.module.core.dal.mysql.resource.ResourceItemMapper; import com.yhy.module.core.enums.infoprice.InfoPriceCategoryNodeTypeEnum; import com.yhy.module.core.service.infoprice.InfoPriceResourceService; import lombok.extern.slf4j.Slf4j; @@ -19,6 +27,8 @@ import org.springframework.validation.annotation.Validated; import javax.annotation.Resource; import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static com.yhy.module.core.enums.ErrorCodeConstants.*; @@ -42,17 +52,43 @@ public class InfoPriceResourceServiceImpl implements InfoPriceResourceService { @Resource private InfoPriceResourcePriceMapper infoPriceResourcePriceMapper; + @Resource + private ResourceItemMapper resourceItemMapper; + + @Resource + private InfoPriceBookMapper infoPriceBookMapper; + + @Resource + private InfoPriceTreeMapper infoPriceTreeMapper; + @Override @Transactional(rollbackFor = Exception.class) public Long createResource(InfoPriceResourceSaveReqVO createReqVO) { + // 0. 创建时 categoryTreeId 必填 + if (createReqVO.getCategoryTreeId() == null) { + throw exception(INFO_PRICE_CATEGORY_TREE_NOT_EXISTS); + } // 1. 校验分类树节点存在且为内容节点 validateContentNode(createReqVO.getCategoryTreeId()); // 2. 校验编码唯一性 validateCodeUnique(createReqVO.getCategoryTreeId(), createReqVO.getCode(), null); - // 3. 插入 - InfoPriceResourceDO resource = BeanUtils.toBean(createReqVO, InfoPriceResourceDO.class); + // 3. 查找或创建工料机 + Long resourceItemId = findOrCreateResourceItem(createReqVO.getName(), createReqVO.getSpec(), createReqVO.getUnit()); + + // 4. 插入 + InfoPriceResourceDO resource = new InfoPriceResourceDO(); + resource.setCategoryTreeId(createReqVO.getCategoryTreeId()); + resource.setCode(createReqVO.getCode()); + resource.setSourceResourceItemId(resourceItemId); + resource.setPriceTaxExcl(createReqVO.getPriceTaxExcl()); + resource.setTaxRate(createReqVO.getTaxRate()); + resource.setPriceTaxIncl(createReqVO.getPriceTaxIncl()); + resource.setDrawingUrl(createReqVO.getDrawingUrl()); + resource.setCategoryId(createReqVO.getCategoryId()); + resource.setRemark(createReqVO.getRemark()); + resource.setAttributes(createReqVO.getAttributes()); // 计算排序 List siblings = resourceMapper.selectListByCategoryTreeId(createReqVO.getCategoryTreeId()); @@ -71,8 +107,21 @@ public class InfoPriceResourceServiceImpl implements InfoPriceResourceService { // 2. 校验编码唯一性 validateCodeUnique(resource.getCategoryTreeId(), updateReqVO.getCode(), updateReqVO.getId()); - // 3. 更新 - InfoPriceResourceDO updateObj = BeanUtils.toBean(updateReqVO, InfoPriceResourceDO.class); + // 3. 查找或创建工料机 + Long resourceItemId = findOrCreateResourceItem(updateReqVO.getName(), updateReqVO.getSpec(), updateReqVO.getUnit()); + + // 4. 更新 + InfoPriceResourceDO updateObj = new InfoPriceResourceDO(); + updateObj.setId(updateReqVO.getId()); + updateObj.setCode(updateReqVO.getCode()); + updateObj.setSourceResourceItemId(resourceItemId); + updateObj.setPriceTaxExcl(updateReqVO.getPriceTaxExcl()); + updateObj.setTaxRate(updateReqVO.getTaxRate()); + updateObj.setPriceTaxIncl(updateReqVO.getPriceTaxIncl()); + updateObj.setDrawingUrl(updateReqVO.getDrawingUrl()); + updateObj.setCategoryId(updateReqVO.getCategoryId()); + updateObj.setRemark(updateReqVO.getRemark()); + updateObj.setAttributes(updateReqVO.getAttributes()); resourceMapper.updateById(updateObj); } @@ -95,19 +144,171 @@ public class InfoPriceResourceServiceImpl implements InfoPriceResourceService { @Override public InfoPriceResourceRespVO getResource(Long id) { InfoPriceResourceDO resource = resourceMapper.selectById(id); - return BeanUtils.toBean(resource, InfoPriceResourceRespVO.class); + return convertToVO(resource); } @Override public PageResult getResourcePage(InfoPriceResourcePageReqVO pageReqVO) { - PageResult pageResult = resourceMapper.selectPage(pageReqVO); - return BeanUtils.toBean(pageResult, InfoPriceResourceRespVO.class); + // 如果有地区筛选,先找出匹配的categoryTreeIds + List matchingCategoryTreeIds = null; + if (pageReqVO.getTreeNodeId() != null && !pageReqVO.getTreeNodeId().isEmpty()) { + try { + Long treeNodeId = Long.parseLong(pageReqVO.getTreeNodeId()); + matchingCategoryTreeIds = findMatchingCategoryTreeIds(treeNodeId); + if (matchingCategoryTreeIds.isEmpty()) { + // 没有匹配的地区,直接返回空结果 + return new PageResult<>(java.util.Collections.emptyList(), 0L); + } + } catch (NumberFormatException e) { + // 忽略无效的ID + } + } + + // 执行数据库查询(带categoryTreeIds过滤) + List finalMatchingCategoryTreeIds = matchingCategoryTreeIds; + PageResult pageResult = + (pageReqVO.getHistory()!= null && pageReqVO.getHistory()) ? TenantUtils.executeIgnore(() -> resourceMapper.selectPageWithCategoryTreeIds(pageReqVO, finalMatchingCategoryTreeIds)): + TenantUtils.executeIsAdmin(()->resourceMapper.selectPageWithCategoryTreeIds(pageReqVO, finalMatchingCategoryTreeIds)); + + // 在 Service 层进行虚拟字段过滤(name、spec、professionType) + List filteredList = + pageResult.getList().stream() + .map(m-> TenantUtils.executeIgnore(() -> convertToVO(m))) + .collect(Collectors.toList()); + + + // 按名称过滤(模糊匹配) + if (pageReqVO.getName() != null && !pageReqVO.getName().isEmpty()) { + String nameLower = pageReqVO.getName().toLowerCase(); + filteredList = filteredList.stream() + .filter(vo -> vo.getName() != null && vo.getName().toLowerCase().contains(nameLower)) + .collect(Collectors.toList()); + } + + // 按规格型号过滤(模糊匹配) + if (pageReqVO.getSpec() != null && !pageReqVO.getSpec().isEmpty()) { + String specLower = pageReqVO.getSpec().toLowerCase(); + filteredList = filteredList.stream() + .filter(vo -> vo.getSpec() != null && vo.getSpec().toLowerCase().contains(specLower)) + .collect(Collectors.toList()); + } + + // 按专业类型过滤(精确匹配) + if (pageReqVO.getProfessionType() != null && !pageReqVO.getProfessionType().isEmpty()) { + filteredList = filteredList.stream() + .filter(vo -> pageReqVO.getProfessionType().equals(vo.getProfessionType())) + .collect(Collectors.toList()); + } + + // 排除指定的ID列表(用于未关联表格排除已关联数据) + if (pageReqVO.getExcludeIds() != null && !pageReqVO.getExcludeIds().isEmpty()) { + java.util.Set excludeIdSet = new java.util.HashSet<>(pageReqVO.getExcludeIds()); + filteredList = filteredList.stream() + .filter(vo -> !excludeIdSet.contains(vo.getId())) + .collect(Collectors.toList()); + } + + return new PageResult<>(filteredList, (long) filteredList.size()); + } + + /** + * 根据地区节点ID找出所有匹配的categoryTreeIds + * 匹配规则:信息价册的treeNodeId等于过滤节点,或过滤节点在信息价册treeNodeId的路径中 + */ + private List findMatchingCategoryTreeIds(Long treeNodeId) { + log.info("[findMatchingCategoryTreeIds] 开始查找匹配地区节点: treeNodeId={}", treeNodeId); + + // 1. 找出所有匹配地区的信息价册 + List allBooks = infoPriceBookMapper.selectList(); + log.info("[findMatchingCategoryTreeIds] 查询到所有信息价册数量: {}", allBooks.size()); + + List matchingBookIds = allBooks.stream() + .filter(book -> { + if (book.getTreeNodeId() == null) return false; + // 精确匹配 + if (book.getTreeNodeId().equals(treeNodeId)) { + log.debug("[findMatchingCategoryTreeIds] 精确匹配: bookId={}, bookTreeNodeId={}", book.getId(), book.getTreeNodeId()); + return true; + } + // 检查路径(过滤节点是信息价册地区的祖先) + InfoPriceTreeDO treeNode = infoPriceTreeMapper.selectById(book.getTreeNodeId()); + if (treeNode != null && treeNode.getPath() != null) { + for (String pathId : treeNode.getPath()) { + try { + // 使用equals比较Long,避免大数==比较失败 + if (treeNodeId.equals(Long.parseLong(pathId))) { + log.debug("[findMatchingCategoryTreeIds] 路径匹配: bookId={}, bookTreeNodeId={}, path={}", + book.getId(), book.getTreeNodeId(), treeNode.getPath()); + return true; + } + } catch (NumberFormatException e) { + // 忽略 + } + } + } + return false; + }) + .map(InfoPriceBookDO::getId) + .collect(Collectors.toList()); + + log.info("[findMatchingCategoryTreeIds] 匹配的信息价册IDs: {}", matchingBookIds); + + if (matchingBookIds.isEmpty()) { + log.warn("[findMatchingCategoryTreeIds] 没有找到匹配的信息价册,返回空列表"); + return java.util.Collections.emptyList(); + } + + // 2. 找出属于这些信息价册的所有categoryTreeIds + List categoryTrees = categoryTreeMapper.selectListByBookIds(matchingBookIds); + List categoryTreeIds = categoryTrees.stream() + .map(InfoPriceCategoryTreeDO::getId) + .collect(Collectors.toList()); + + log.info("[findMatchingCategoryTreeIds] 匹配的categoryTreeIds数量: {}", categoryTreeIds.size()); + return categoryTreeIds; + } + + /** + * 检查工料机是否匹配指定的树节点(包括子节点) + */ + private boolean matchTreeNode(InfoPriceResourceRespVO vo, Long treeNodeId) { + if (vo.getCategoryTreeId() == null) { + return false; + } + // 获取分类树节点 + InfoPriceCategoryTreeDO categoryTree = categoryTreeMapper.selectById(vo.getCategoryTreeId()); + if (categoryTree == null || categoryTree.getBookId() == null) { + return false; + } + // 获取信息价册 + InfoPriceBookDO book = infoPriceBookMapper.selectById(categoryTree.getBookId()); + if (book == null || book.getTreeNodeId() == null) { + return false; + } + // 检查是否匹配(精确匹配或在路径中) + if (book.getTreeNodeId().equals(treeNodeId)) { + return true; + } + // 检查路径 + InfoPriceTreeDO treeNode = infoPriceTreeMapper.selectById(book.getTreeNodeId()); + if (treeNode != null && treeNode.getPath() != null) { + for (String pathId : treeNode.getPath()) { + try { + if (Long.parseLong(pathId) == treeNodeId) { + return true; + } + } catch (NumberFormatException e) { + // 忽略 + } + } + } + return false; } @Override public List getResourceList(Long categoryTreeId) { List list = resourceMapper.selectListByCategoryTreeId(categoryTreeId); - return BeanUtils.toBean(list, InfoPriceResourceRespVO.class); + return list.stream().map(this::convertToVO).collect(Collectors.toList()); } // ==================== 私有方法 ==================== @@ -137,4 +338,174 @@ public class InfoPriceResourceServiceImpl implements InfoPriceResourceService { } } + /** + * 查找或创建工料机 + * 根据(名称、型号规格、单位)查找工料机,如果不存在则创建 + */ + private Long findOrCreateResourceItem(String name, String spec, String unit) { + // 1. 查找已存在的工料机 + ResourceItemDO existingItem = resourceItemMapper.selectByNameSpecUnit(name, spec, unit); + if (existingItem != null) { + return existingItem.getId(); + } + + // 2. 创建新的工料机 + ResourceItemDO newItem = new ResourceItemDO(); + newItem.setName(name); + newItem.setSpec(spec); + newItem.setUnit(unit); + newItem.setSourceType(ResourceItemDO.SOURCE_TYPE_INFO_PRICE); + // 默认类型为材料 + newItem.setType("material"); + // 设置默认 catalog_item_id(信息价创建的工料机使用默认值1) + newItem.setCatalogItemId(1L); + resourceItemMapper.insert(newItem); + log.info("创建信息价工料机:name={}, spec={}, unit={}, id={}", name, spec, unit, newItem.getId()); + return newItem.getId(); + } + + /** + * 将 DO 转换为 VO,填充工料机信息和信息价册信息 + */ + private InfoPriceResourceRespVO convertToVO(InfoPriceResourceDO resource) { + if (resource == null) { + return null; + } + InfoPriceResourceRespVO vo = new InfoPriceResourceRespVO(); + vo.setId(resource.getId()); + vo.setTenantId(resource.getTenantId()); + vo.setCategoryTreeId(resource.getCategoryTreeId()); + vo.setCode(resource.getCode()); + vo.setSourceResourceItemId(resource.getSourceResourceItemId()); + vo.setPriceTaxExcl(resource.getPriceTaxExcl()); + vo.setTaxRate(resource.getTaxRate()); + vo.setPriceTaxIncl(resource.getPriceTaxIncl()); + vo.setDrawingUrl(resource.getDrawingUrl()); + vo.setCategoryId(resource.getCategoryId()); + vo.setRemark(resource.getRemark()); + vo.setSortOrder(resource.getSortOrder()); + vo.setAttributes(resource.getAttributes()); + vo.setCreateTime(resource.getCreateTime()); + + // 从工料机表获取 name/spec/unit + if (resource.getSourceResourceItemId() != null) { + ResourceItemDO resourceItem = resourceItemMapper.selectById(resource.getSourceResourceItemId()); + if (resourceItem != null) { + vo.setName(resourceItem.getName()); + vo.setSpec(resourceItem.getSpec()); + vo.setUnit(resourceItem.getUnit()); + } + } + + // 从分类树获取信息价册信息(附件、专业、地区) + if (resource.getCategoryTreeId() != null) { + InfoPriceCategoryTreeDO categoryTree = categoryTreeMapper.selectById(resource.getCategoryTreeId()); + if (categoryTree != null && categoryTree.getBookId() != null) { + InfoPriceBookDO book = infoPriceBookMapper.selectById(categoryTree.getBookId()); + if (book != null) { + vo.setBookName(book.getName()); + vo.setBookAttachment(book.getAttachment()); + + // 获取信息价树节点(专业和地区) + if (book.getTreeNodeId() != null) { + InfoPriceTreeDO treeNode = infoPriceTreeMapper.selectById(book.getTreeNodeId()); + if (treeNode != null) { + vo.setProfessionType(treeNode.getEnumType()); + // 获取完整地区路径 + vo.setFullRegion(buildFullRegionPath(treeNode)); + } + } + } + } + } + return vo; + } + + /** + * 构建完整地区路径(从根节点到当前节点的名称拼接) + */ + private String buildFullRegionPath(InfoPriceTreeDO treeNode) { + if (treeNode == null) { + return ""; + } + + // 获取路径中的所有节点ID + String[] pathIds = treeNode.getPath(); + if (pathIds == null || pathIds.length == 0) { + return treeNode.getName(); + } + + // 查询路径中所有节点的名称 + StringBuilder pathBuilder = new StringBuilder(); + for (String pathId : pathIds) { + try { + Long id = Long.parseLong(pathId); + InfoPriceTreeDO pathNode = infoPriceTreeMapper.selectById(id); + if (pathNode != null && pathNode.getName() != null && !pathNode.getName().isEmpty()) { + if (pathBuilder.length() > 0) { + pathBuilder.append(" / "); + } + pathBuilder.append(pathNode.getName()); + } + } catch (NumberFormatException e) { + // 忽略无效的ID + } + } + + // 添加当前节点名称 + if (pathBuilder.length() > 0) { + pathBuilder.append(" / "); + } + pathBuilder.append(treeNode.getName()); + + return pathBuilder.toString(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public List createResourceBatch(List createReqVOList) { + if (createReqVOList == null || createReqVOList.isEmpty()) { + return new java.util.ArrayList<>(); + } + List resourceList = TenantUtils.execute(1L,() -> { + List list = new java.util.ArrayList<>(); + for (InfoPriceResourceSaveReqVO item : createReqVOList) { + + // 查找或创建工料机 + Long resourceItemId = findOrCreateResourceItem(item.getName(), item.getSpec(), item.getUnit()); + + InfoPriceResourceDO resource = new InfoPriceResourceDO(); + resource.setCategoryTreeId(item.getCategoryTreeId()); + resource.setCode(item.getCode()); + resource.setSourceResourceItemId(resourceItemId); + resource.setPriceTaxExcl(item.getPriceTaxExcl()); + resource.setTaxRate(item.getTaxRate()); + resource.setPriceTaxIncl(item.getPriceTaxIncl()); + resource.setDrawingUrl(item.getDrawingUrl()); + resource.setCategoryId(item.getCategoryId()); + resource.setRemark(item.getRemark()); + resource.setAttributes(item.getAttributes()); + list.add(resource); + } + return list; + }); + + + // 批量插入 + resourceMapper.insertBatch(resourceList); + + // 返回ID列表 + return resourceList.stream() + .map(InfoPriceResourceDO::getId) + .collect(Collectors.toList()); + } + + @Override + public List getResourceListBySourceResourceItemId(Long sourceResourceItemId) { + List resourceList = resourceMapper.selectList(new LambdaQueryWrapperX() + .eq(InfoPriceResourceDO::getSourceResourceItemId, sourceResourceItemId)); + + return BeanUtils.toBean(resourceList, InfoPriceResourceRespVO.class); + } + } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/impl/InfoPriceTreeServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/impl/InfoPriceTreeServiceImpl.java index a674b24..5b46263 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/impl/InfoPriceTreeServiceImpl.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/infoprice/impl/InfoPriceTreeServiceImpl.java @@ -45,7 +45,7 @@ public class InfoPriceTreeServiceImpl implements InfoPriceTreeService { @Transactional(rollbackFor = Exception.class) public Long createInfoPriceTree(InfoPriceTreeSaveReqVO createReqVO) { // 1. 验证枚举类型 - validateEnumType(createReqVO.getEnumType()); +// validateEnumType(createReqVO.getEnumType()); // 2. 获取租户ID(从上下文) Long tenantId = cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder.getTenantId(); @@ -53,7 +53,20 @@ public class InfoPriceTreeServiceImpl implements InfoPriceTreeService { // 3. 验证编码唯一性 validateCodeUnique(null, tenantId, createReqVO.getEnumType(), createReqVO.getCode()); - // 4. 验证父节点 + // 4. 根节点唯一性校验:同一枚举类型只允许一个根节点 + if (createReqVO.getParentId() == null) { + List roots = infoPriceTreeMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .isNull(InfoPriceTreeDO::getParentId) + .eq(InfoPriceTreeDO::getEnumType, createReqVO.getEnumType())); + if (!roots.isEmpty()) { + throw new cn.iocoder.yudao.framework.common.exception.ServiceException( + cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST.getCode(), + "已存在根节点「" + roots.get(0).getName() + "」,不允许重复创建"); + } + } + + // 5. 验证父节点 InfoPriceTreeDO parent = null; if (createReqVO.getParentId() != null) { parent = validateParentExists(createReqVO.getParentId()); @@ -93,7 +106,7 @@ public class InfoPriceTreeServiceImpl implements InfoPriceTreeService { InfoPriceTreeDO tree = validateTreeExists(updateReqVO.getId()); // 2. 验证枚举类型 - validateEnumType(updateReqVO.getEnumType()); +// validateEnumType(updateReqVO.getEnumType()); // 3. 验证编码唯一性 validateCodeUnique(updateReqVO.getId(), tree.getTenantId(), updateReqVO.getEnumType(), updateReqVO.getCode()); @@ -134,7 +147,7 @@ public class InfoPriceTreeServiceImpl implements InfoPriceTreeService { @Override public List getInfoPriceTreeList(String enumType) { // 1. 验证枚举类型 - validateEnumType(enumType); +// validateEnumType(enumType); // 2. 查询所有节点 List list = infoPriceTreeMapper.selectListByEnumType(enumType); diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaAdjustmentDetailService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaAdjustmentDetailService.java index 2c195fb..d5f8aec 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaAdjustmentDetailService.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaAdjustmentDetailService.java @@ -45,9 +45,9 @@ public interface QuotaAdjustmentDetailService { QuotaAdjustmentDetailDO getQuotaAdjustmentDetail(Long id); /** - * 获得定额调整明细列表(按定额子目ID) + * 获得定额调整明细列表(按定额基价ID) * - * @param quotaItemId 定额子目ID + * @param quotaItemId 定额基价ID * @return 定额调整明细列表 */ List getQuotaAdjustmentDetailListByQuotaItem(Long quotaItemId); @@ -77,9 +77,9 @@ public interface QuotaAdjustmentDetailService { List getAvailableSettings(Long adjustmentSettingId); /** - * 获取调整设置与明细的组合列表(按定额子目ID) + * 获取调整设置与明细的组合列表(按定额基价ID) * - * @param quotaItemId 定额子目ID + * @param quotaItemId 定额基价ID * @return 调整设置与明细的组合列表 */ List getCombinedList(Long quotaItemId); diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaAdjustmentSettingService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaAdjustmentSettingService.java index d159737..5b7b8d1 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaAdjustmentSettingService.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaAdjustmentSettingService.java @@ -45,7 +45,7 @@ public interface QuotaAdjustmentSettingService { /** * 获得定额调整设置列表 * - * @param quotaItemId 定额子目ID + * @param quotaItemId 定额基价ID * @return 定额调整设置列表 */ List getQuotaAdjustmentSettingList(Long quotaItemId); diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaCatalogItemService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaCatalogItemService.java index f5d5e1c..68f4599 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaCatalogItemService.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaCatalogItemService.java @@ -51,6 +51,14 @@ public interface QuotaCatalogItemService { */ List getQuotaCatalogItemList(); + /** + * 获得指定节点的子节点列表 + * + * @param parentId 父节点ID + * @return 子节点列表 + */ + List getChildrenByParentId(Long parentId); + /** * 绑定工料机专业 * diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaCatalogTreeService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaCatalogTreeService.java index 46bfd24..99c713d 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaCatalogTreeService.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaCatalogTreeService.java @@ -6,39 +6,39 @@ import java.util.List; import javax.validation.Valid; /** - * 定额子目树 Service 接口 + * 定额基价树 Service 接口 * * @author yhy */ public interface QuotaCatalogTreeService { /** - * 创建定额子目树节点 + * 创建定额基价树节点 */ Long createCatalogTree(@Valid QuotaCatalogTreeSaveReqVO createReqVO); /** - * 更新定额子目树节点 + * 更新定额基价树节点 */ void updateCatalogTree(@Valid QuotaCatalogTreeSaveReqVO updateReqVO); /** - * 删除定额子目树节点 + * 删除定额基价树节点 */ void deleteCatalogTree(Long id); /** - * 获取定额子目树节点详情 + * 获取定额基价树节点详情 */ QuotaCatalogTreeRespVO getCatalogTree(Long id); /** - * 获取定额子目树节点列表 + * 获取定额基价树节点列表 */ List getCatalogTreeList(Long catalogItemId, Long parentId); /** - * 获取定额子目树结构 + * 获取定额基价树结构 */ List getCatalogTreeTree(Long catalogItemId); @@ -48,10 +48,10 @@ public interface QuotaCatalogTreeService { void swapSort(Long nodeId1, Long nodeId2); /** - * 根据费率项ID查询绑定的定额子目树 + * 根据费率项ID查询绑定的定额基价树 * * @param rateItemId 费率项ID - * @return 绑定的定额子目树列表 + * @return 绑定的定额基价树列表 */ List getCatalogTreeByRateItem(Long rateItemId); diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaFeeItemService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaFeeItemService.java index f2f63d2..6a341d8 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaFeeItemService.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaFeeItemService.java @@ -46,6 +46,19 @@ public interface QuotaFeeItemService { */ List getFeeItemWithRateList(Long catalogItemId); + /** + * 根据费率项和取费项数据构建取费项与费率项合并列表(供快照API复用) + * + * @param rateItems 费率项列表(目录节点) + * @param feeItems 取费项列表 + * @param catalogItemId 模式节点ID + * @return 合并后的列表 + */ + List buildFeeItemWithRateList( + List rateItems, + List feeItems, + Long catalogItemId); + /** * 交换排序 */ @@ -55,4 +68,11 @@ public interface QuotaFeeItemService { * 验证取费项是否存在 */ void validateFeeItemExists(Long id); + + /** + * 验证计算基数格式和公式语法 + * + * @param calcBase 计算基数对象,包含formula和variables + */ + void validateCalcBase(java.util.Map calcBase); } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaItemService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaItemService.java index 9d047a3..d9793b2 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaItemService.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaItemService.java @@ -3,19 +3,18 @@ package com.yhy.module.core.service.quota; import com.yhy.module.core.controller.admin.quota.vo.QuotaItemRespVO; import com.yhy.module.core.controller.admin.quota.vo.QuotaItemSaveReqVO; import com.yhy.module.core.dal.dataobject.quota.QuotaItemDO; - -import javax.validation.Valid; import java.util.List; +import javax.validation.Valid; /** - * 定额子目 Service 接口 + * 定额基价 Service 接口 * * @author yhy */ public interface QuotaItemService { /** - * 创建定额子目 + * 创建定额基价 * * @param createReqVO 创建信息 * @return 编号 @@ -23,40 +22,40 @@ public interface QuotaItemService { Long createQuotaItem(@Valid QuotaItemSaveReqVO createReqVO); /** - * 更新定额子目 + * 更新定额基价 * * @param updateReqVO 更新信息 */ void updateQuotaItem(@Valid QuotaItemSaveReqVO updateReqVO); /** - * 删除定额子目 + * 删除定额基价 * * @param id 编号 */ void deleteQuotaItem(Long id); /** - * 获得定额子目 + * 获得定额基价 * * @param id 编号 - * @return 定额子目 + * @return 定额基价 */ QuotaItemDO getQuotaItem(Long id); /** - * 获得定额子目详情(包含工料机组成) + * 获得定额基价详情(包含工料机组成) * * @param id 编号 - * @return 定额子目详情 + * @return 定额基价详情 */ QuotaItemRespVO getQuotaItemDetail(Long id); /** - * 获得定额子目列表 + * 获得定额基价列表 * * @param catalogItemId 定额条目ID - * @return 定额子目列表 + * @return 定额基价列表 */ List getQuotaItemList(Long catalogItemId); @@ -65,18 +64,59 @@ public interface QuotaItemService { * * 基价 = Σ(resource_price * dosage * (1 + loss_rate)) * - * @param id 定额子目ID + * @param id 定额基价ID * @return 计算后的基价 */ void calculateBasePrice(Long id); /** - * 获取定额子目绑定的工料机专业ID + * 获取定额基价绑定的工料机专业ID * * 通过向上追溯到定额专业节点,获取其绑定的 category_tree_id * - * @param quotaItemId 定额子目ID + * @param quotaItemId 定额基价ID * @return 工料机专业ID */ Long getCategoryTreeIdByQuotaItem(Long quotaItemId); + + /** + * 更新定额基价的四个价格字段 + * + * @param quotaItemId 定额基价ID + * @param taxExclBasePrice 除税定额单价 + * @param taxInclBasePrice 含税定额单价 + * @param taxExclCompilePrice 除税编制单价 + * @param taxInclCompilePrice 含税编制单价 + */ + void updateQuotaItemPrices(Long quotaItemId, java.math.BigDecimal taxExclBasePrice, + java.math.BigDecimal taxInclBasePrice, java.math.BigDecimal taxExclCompilePrice, + java.math.BigDecimal taxInclCompilePrice); + + /** + * 根据编码查询定额基价 + * + * @param code 编码 + * @return 定额基价列表(可能有多个匹配) + */ + List getQuotaItemByCode(String code); + + /** + * 获取定额基价对应的费率模式节点ID + * + * 通过向上追溯到定额专业节点,然后查找其下的费率模式子节点 + * + * @param quotaItemId 定额基价ID + * @return 费率模式节点ID,如果不存在则返回null + */ + Long getRateModeIdByQuotaItem(Long quotaItemId); + + /** + * 获取定额基价对应的定额专业节点ID + * + * 通过向上追溯到定额专业节点(nodeType='specialty') + * + * @param quotaItemId 定额基价ID + * @return 定额专业节点ID,如果不存在则返回null + */ + Long getSpecialtyIdByQuotaItem(Long quotaItemId); } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaMarketMaterialService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaMarketMaterialService.java new file mode 100644 index 0000000..977bcff --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaMarketMaterialService.java @@ -0,0 +1,90 @@ +package com.yhy.module.core.service.quota; + +import com.yhy.module.core.controller.admin.quota.vo.QuotaMarketMaterialRespVO; +import com.yhy.module.core.controller.admin.quota.vo.QuotaMarketMaterialSaveReqVO; +import com.yhy.module.core.controller.admin.resource.vo.ResourceItemRespVO; +import com.yhy.module.core.dal.dataobject.quota.QuotaMarketMaterialDO; +import java.util.List; +import javax.validation.Valid; + +/** + * 定额市场主材设备 Service 接口 + * + * @author yhy + */ +public interface QuotaMarketMaterialService { + + /** + * 添加定额市场主材设备 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createMarketMaterial(@Valid QuotaMarketMaterialSaveReqVO createReqVO); + + /** + * 更新定额市场主材设备 + * + * @param updateReqVO 更新信息 + */ + void updateMarketMaterial(@Valid QuotaMarketMaterialSaveReqVO updateReqVO); + + /** + * 删除定额市场主材设备 + * + * @param id 编号 + */ + void deleteMarketMaterial(Long id); + + /** + * 获得定额市场主材设备 + * + * @param id 编号 + * @return 定额市场主材设备 + */ + QuotaMarketMaterialDO getMarketMaterial(Long id); + + /** + * 获得定额市场主材设备列表 + * + * @param quotaItemId 定额基价ID + * @return 定额市场主材设备列表 + */ + List getMarketMaterialList(Long quotaItemId); + + /** + * 获取可选的工料机列表(已过滤范围) + * + * @param quotaItemId 定额基价ID + * @return 可选的工料机列表 + */ + List getAvailableResourceItems(Long quotaItemId); + + /** + * 获取可选的工料机列表(已过滤范围,支持模糊查询) + * + * @param quotaItemId 定额基价ID + * @param code 编码(模糊查询,可选) + * @param name 名称(模糊查询,可选) + * @param spec 型号规格(模糊查询,可选) + * @return 可选的工料机列表 + */ + List getAvailableResourceItemsWithFilter(Long quotaItemId, String code, String name, String spec); + + /** + * 根据编码精确查询可用工料机 + * + * @param quotaItemId 定额基价ID + * @param code 工料机编码 + * @return 工料机信息,如果未找到返回null + */ + ResourceItemRespVO getResourceItemByCode(Long quotaItemId, String code); + + /** + * 批量添加定额市场主材设备 + * + * @param quotaItemId 定额基价ID + * @param materials 市场主材设备列表 + */ + void batchCreateMarketMaterials(Long quotaItemId, List materials); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaRateItemService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaRateItemService.java index 0dfcb77..4327a7b 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaRateItemService.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaRateItemService.java @@ -54,6 +54,17 @@ public interface QuotaRateItemService { */ List getRateItemTree(Long catalogItemId); + /** + * 根据费率项数据构建费率项树(供快照API复用) + * + * @param rateItems 费率项列表 + * @param fieldConfigs 字段绑定配置列表 + * @return 费率项树 + */ + List buildRateItemTree( + List rateItems, + List fieldConfigs); + /** * 同级交换排序 */ diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaResourceService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaResourceService.java index 7833c29..3092a96 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaResourceService.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaResourceService.java @@ -51,7 +51,7 @@ public interface QuotaResourceService { /** * 获得定额工料机组成列表 * - * @param quotaItemId 定额子目ID + * @param quotaItemId 定额基价ID * @return 定额工料机组成列表 */ List getQuotaResourceList(Long quotaItemId); @@ -61,7 +61,7 @@ public interface QuotaResourceService { * * 只返回定额专业绑定的工料机专业下的工料机数据 * - * @param quotaItemId 定额子目ID + * @param quotaItemId 定额基价ID * @return 可选的工料机列表 */ List getAvailableResourceItems(Long quotaItemId); @@ -71,7 +71,7 @@ public interface QuotaResourceService { * * 只返回定额专业绑定的工料机专业下的工料机数据 * - * @param quotaItemId 定额子目ID + * @param quotaItemId 定额基价ID * @param code 编码(模糊查询,可选) * @param name 名称(模糊查询,可选) * @param spec 型号规格(模糊查询,可选) @@ -79,10 +79,19 @@ public interface QuotaResourceService { */ List getAvailableResourceItemsWithFilter(Long quotaItemId, String code, String name, String spec); + /** + * 根据编码精确查询可用工料机 + * + * @param quotaItemId 定额基价ID + * @param code 工料机编码 + * @return 工料机信息,如果未找到返回null + */ + ResourceItemRespVO getResourceItemByCode(Long quotaItemId, String code); + /** * 验证工料机是否在定额专业的数据范围内 * - * @param quotaItemId 定额子目ID + * @param quotaItemId 定额基价ID * @param resourceItemId 工料机ID * @throws cn.iocoder.yudao.framework.common.exception.ServiceException 如果不在范围内 */ @@ -91,8 +100,13 @@ public interface QuotaResourceService { /** * 批量添加定额工料机组成 * - * @param quotaItemId 定额子目ID + * @param quotaItemId 定额基价ID * @param resources 工料机组成列表 */ void batchCreateQuotaResources(Long quotaItemId, List resources); + + // 【已删除】后台定额调整功能改为纯展示效果,以下接口已删除: + // - applyAdjustmentSetting + // - applyDynamicAdjustment + // - applyDynamicMerge } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaUnifiedFeeResourceService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaUnifiedFeeResourceService.java new file mode 100644 index 0000000..8d840c2 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaUnifiedFeeResourceService.java @@ -0,0 +1,89 @@ +package com.yhy.module.core.service.quota; + +import com.yhy.module.core.controller.admin.quota.vo.QuotaUnifiedFeeResourceSaveReqVO; +import com.yhy.module.core.dal.dataobject.quota.QuotaUnifiedFeeResourceDO; +import java.util.List; +import javax.validation.Valid; + +/** + * 统一取费子目工料机 Service 接口 + */ +public interface QuotaUnifiedFeeResourceService { + + /** + * 创建子目工料机 + */ + Long createUnifiedFeeResource(@Valid QuotaUnifiedFeeResourceSaveReqVO createReqVO); + + /** + * 更新子目工料机 + */ + void updateUnifiedFeeResource(@Valid QuotaUnifiedFeeResourceSaveReqVO updateReqVO); + + /** + * 删除子目工料机 + */ + void deleteUnifiedFeeResource(Long id); + + /** + * 获取子目工料机详情 + */ + QuotaUnifiedFeeResourceDO getUnifiedFeeResource(Long id); + + /** + * 获取子目工料机列表(包含工料机详细信息) + */ + List getUnifiedFeeResourceList(Long unifiedFeeSettingId); + + /** + * 批量创建子目工料机 + */ + void batchCreateUnifiedFeeResource(List createReqVOList); + + /** + * 根据子定额ID删除所有子目工料机 + */ + void deleteByUnifiedFeeSettingId(Long unifiedFeeSettingId); + + /** + * 交换排序 + */ + void swapSort(Long nodeId1, Long nodeId2); + + /** + * 验证子目工料机是否存在 + */ + void validateUnifiedFeeResourceExists(Long id); + + /** + * 根据编码精确查询可用工料机 + * + * @param unifiedFeeSettingId 统一取费设置ID + * @param code 工料机编码 + * @return 工料机信息,如果未找到返回null + */ + com.yhy.module.core.controller.admin.resource.vo.ResourceItemRespVO getResourceItemByCode(Long unifiedFeeSettingId, String code); + + /** + * 验证工料机是否在统一取费设置的数据范围内 + * + * @param unifiedFeeSettingId 统一取费设置ID + * @param resourceItemId 工料机ID + * @throws cn.iocoder.yudao.framework.common.exception.ServiceException 如果不在范围内 + */ + void validateResourceInScope(Long unifiedFeeSettingId, Long resourceItemId); + + /** + * 获取可选的工料机列表(已过滤范围,支持模糊查询) + * + * 只返回统一取费设置绑定的工料机专业下的工料机数据 + * + * @param unifiedFeeSettingId 统一取费设置ID + * @param code 编码(模糊查询,可选) + * @param name 名称(模糊查询,可选) + * @param spec 型号规格(模糊查询,可选) + * @return 可选的工料机列表 + */ + java.util.List getAvailableResourceItemsWithFilter( + Long unifiedFeeSettingId, String code, String name, String spec); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaUnifiedFeeService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaUnifiedFeeService.java new file mode 100644 index 0000000..5fdd2c0 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaUnifiedFeeService.java @@ -0,0 +1,60 @@ +package com.yhy.module.core.service.quota; + +import com.yhy.module.core.controller.admin.quota.vo.QuotaFeeItemWithRateRespVO; +import com.yhy.module.core.controller.admin.quota.vo.QuotaUnifiedFeeRespVO; +import com.yhy.module.core.controller.admin.quota.vo.QuotaUnifiedFeeSaveReqVO; +import com.yhy.module.core.dal.dataobject.quota.QuotaUnifiedFeeDO; +import java.util.List; +import javax.validation.Valid; + +/** + * 统一取费单价 Service 接口 + */ +public interface QuotaUnifiedFeeService { + + /** + * 创建统一取费单价 + */ + Long createUnifiedFee(@Valid QuotaUnifiedFeeSaveReqVO createReqVO); + + /** + * 更新统一取费单价 + */ + void updateUnifiedFee(@Valid QuotaUnifiedFeeSaveReqVO updateReqVO); + + /** + * 删除统一取费单价 + */ + void deleteUnifiedFee(Long id); + + /** + * 获取统一取费单价详情 + */ + QuotaUnifiedFeeDO getUnifiedFee(Long id); + + /** + * 获取统一取费单价列表 + */ + List getUnifiedFeeList(Long catalogItemId); + + /** + * 获取统一取费单价树(与定额取费显示一致的费率项+取费项合并视图) + */ + List getUnifiedFeeTree(Long catalogItemId); + + /** + * 交换排序 + */ + void swapSort(Long nodeId1, Long nodeId2); + + /** + * 验证统一取费单价是否存在 + */ + void validateUnifiedFeeExists(Long id); + + /** + * 从定额取费初始化统一取费单价 + * 首次访问时,自动从定额取费创建对应的记录 + */ + void initFromFeeItems(Long catalogItemId); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaUnifiedFeeSettingService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaUnifiedFeeSettingService.java new file mode 100644 index 0000000..afa9caf --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaUnifiedFeeSettingService.java @@ -0,0 +1,68 @@ +package com.yhy.module.core.service.quota; + +import com.yhy.module.core.controller.admin.quota.vo.QuotaUnifiedFeeSettingRespVO; +import com.yhy.module.core.controller.admin.quota.vo.QuotaUnifiedFeeSettingSaveReqVO; +import com.yhy.module.core.dal.dataobject.quota.QuotaUnifiedFeeSettingDO; +import java.util.List; +import javax.validation.Valid; + +/** + * 统一取费设置 Service 接口 + */ +public interface QuotaUnifiedFeeSettingService { + + /** + * 创建统一取费设置 + */ + Long createUnifiedFeeSetting(@Valid QuotaUnifiedFeeSettingSaveReqVO createReqVO); + + /** + * 更新统一取费设置 + */ + void updateUnifiedFeeSetting(@Valid QuotaUnifiedFeeSettingSaveReqVO updateReqVO); + + /** + * 删除统一取费设置 + */ + void deleteUnifiedFeeSetting(Long id); + + /** + * 获取统一取费设置详情 + */ + QuotaUnifiedFeeSettingDO getUnifiedFeeSetting(Long id); + + /** + * 获取统一取费设置列表 + */ + List getUnifiedFeeSettingList(Long catalogItemId); + + /** + * 获取统一取费设置树 + */ + List getUnifiedFeeSettingTree(Long catalogItemId); + + /** + * 获取父定额列表(用于工作台) + */ + List getParentList(Long catalogItemId); + + /** + * 获取父定额列表,包含子定额取费章节组合(用于工作台范围过滤) + */ + List getParentListWithChildFeeChapters(Long catalogItemId); + + /** + * 获取子定额列表 + */ + List getChildList(Long parentId); + + /** + * 交换排序 + */ + void swapSort(Long nodeId1, Long nodeId2); + + /** + * 验证统一取费设置是否存在 + */ + void validateUnifiedFeeSettingExists(Long id); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaVariableSettingService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaVariableSettingService.java new file mode 100644 index 0000000..4693917 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/QuotaVariableSettingService.java @@ -0,0 +1,104 @@ +package com.yhy.module.core.service.quota; + +import com.yhy.module.core.controller.admin.quota.vo.QuotaVariableSettingRespVO; +import com.yhy.module.core.controller.admin.quota.vo.QuotaVariableSettingSaveReqVO; +import com.yhy.module.core.dal.dataobject.quota.QuotaVariableSettingDO; +import java.util.List; +import javax.validation.Valid; + +/** + * 单位工程变量设置 Service 接口 + */ +public interface QuotaVariableSettingService { + + /** + * 创建变量设置 + */ + Long createVariableSetting(@Valid QuotaVariableSettingSaveReqVO createReqVO); + + /** + * 更新变量设置 + */ + void updateVariableSetting(@Valid QuotaVariableSettingSaveReqVO updateReqVO); + + /** + * 删除变量设置 + */ + void deleteVariableSetting(Long id); + + /** + * 获取变量设置详情 + */ + QuotaVariableSettingDO getVariableSetting(Long id); + + /** + * 根据费率模式节点ID和类别获取列表 + * + * @param catalogItemId 费率模式节点ID + * @param category 类别:division/measure/other/unit_summary + * @return 变量设置列表 + */ + List getVariableSettingList(Long catalogItemId, String category); + + /** + * 根据费率模式节点ID和类别获取列表(包含定额取费中 variable=true 的数据) + * + * @param catalogItemId 费率模式节点ID + * @param category 类别:division/measure/other/unit_summary + * @return 变量设置列表(包含 source 字段标识数据来源) + */ + List getVariableSettingListWithFeeItems(Long catalogItemId, String category); + + /** + * 根据费率模式节点ID获取所有类别的列表 + * + * @param catalogItemId 费率模式节点ID + * @return 变量设置列表 + */ + List getVariableSettingListAll(Long catalogItemId); + + /** + * 根据费率模式节点ID获取所有类别的列表(包含定额取费中 variable=true 的数据) + * + * @param catalogItemId 费率模式节点ID + * @return 变量设置列表(包含 source 字段标识数据来源) + */ + List getVariableSettingListAllWithFeeItems(Long catalogItemId); + + /** + * 交换排序 + */ + void swapSort(Long nodeId1, Long nodeId2); + + /** + * 验证变量设置是否存在 + */ + void validateVariableSettingExists(Long id); + + /** + * 根据定额专业节点ID获取所有类别的列表(包含汇总值计算) + * + * @param catalogItemId 定额专业节点ID + * @param compileTreeId 编制模式树的单位工程节点ID(用于计算汇总值) + * @param baseNumberRangeIds 基数范围选中的节点ID列表(可选,为空时使用整个单位工程) + * @return 变量设置列表(包含 summaryValue 字段) + */ + List getVariableSettingListAllWithSummary( + Long catalogItemId, + Long compileTreeId, + List baseNumberRangeIds + ); + + /** + * 根据编制树ID获取所有变量设置(自动合并所有定额专业) + * 一次性查询该单位工程使用的所有定额专业的变量设置,并计算汇总值 + * + * @param compileTreeId 编制模式树的单位工程节点ID + * @param baseNumberRangeIds 基数范围选中的节点ID列表(可选,为空时使用整个单位工程) + * @return 变量设置列表(按类别分组,包含 summaryValue 字段) + */ + List getVariableSettingListByCompileTree( + Long compileTreeId, + List baseNumberRangeIds + ); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaAdjustmentDetailServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaAdjustmentDetailServiceImpl.java index 730bb88..aa70050 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaAdjustmentDetailServiceImpl.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaAdjustmentDetailServiceImpl.java @@ -107,7 +107,7 @@ public class QuotaAdjustmentDetailServiceImpl implements QuotaAdjustmentDetailSe @Override public List getQuotaAdjustmentDetailListByQuotaItem(Long quotaItemId) { - // 1. 查询该定额子目下的所有调整设置 + // 1. 查询该定额基价下的所有调整设置 List settings = quotaAdjustmentSettingMapper.selectListByQuotaItemId(quotaItemId); if (settings.isEmpty()) { @@ -166,10 +166,10 @@ public class QuotaAdjustmentDetailServiceImpl implements QuotaAdjustmentDetailSe // 获取定额专业ID Long specialtyId = getSpecialtyIdByQuotaItem(currentSetting.getQuotaItemId()); - // 查询同一定额专业下的所有定额子目 + // 查询同一定额专业下的所有定额基价 List catalogTrees = quotaCatalogTreeMapper.selectListByCatalogItemId(specialtyId); - // 查询这些定额子目下的所有调整设置 + // 查询这些定额基价下的所有调整设置 // 这里简化处理,返回所有调整设置,前端可以根据需要过滤 // 实际应该根据 catalogTrees 中的 ID 查询对应的 quotaItem,再查询调整设置 return quotaAdjustmentSettingMapper.selectList(); @@ -177,7 +177,7 @@ public class QuotaAdjustmentDetailServiceImpl implements QuotaAdjustmentDetailSe @Override public List getCombinedList(Long quotaItemId) { - // 1. 查询该定额子目下的所有调整设置(第五层) + // 1. 查询该定额基价下的所有调整设置(第五层) List settings = quotaAdjustmentSettingMapper.selectListByQuotaItemId(quotaItemId); if (settings.isEmpty()) { @@ -269,16 +269,16 @@ public class QuotaAdjustmentDetailServiceImpl implements QuotaAdjustmentDetailSe } /** - * 通过定额子目追溯到定额专业 + * 通过定额基价追溯到定额专业 */ private Long getSpecialtyIdByQuotaItem(Long quotaItemId) { - // 1. 查询定额子目(第三层) + // 1. 查询定额基价(第三层) QuotaItemDO quotaItem = quotaItemMapper.selectById(quotaItemId); if (quotaItem == null) { throw exception(QUOTA_ADJUSTMENT_SETTING_QUOTA_ITEM_NOT_EXISTS); } - // 2. 查询定额子目树节点(第二层) + // 2. 查询定额基价树节点(第二层) QuotaCatalogTreeDO catalogTree = quotaCatalogTreeMapper.selectById(quotaItem.getCatalogItemId()); if (catalogTree == null) { throw exception(QUOTA_ADJUSTMENT_SETTING_QUOTA_ITEM_NOT_EXISTS); diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaAdjustmentSettingServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaAdjustmentSettingServiceImpl.java index 058519f..d1c6cf2 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaAdjustmentSettingServiceImpl.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaAdjustmentSettingServiceImpl.java @@ -1,20 +1,22 @@ package com.yhy.module.core.service.quota.impl; -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_ADJUSTMENT_SETTING_HAS_DETAILS; -import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_ADJUSTMENT_SETTING_NOT_EXISTS; -import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_ADJUSTMENT_SETTING_NOT_SAME_QUOTA_ITEM; -import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_ADJUSTMENT_SETTING_QUOTA_ITEM_NOT_EXISTS; -import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_ADJUSTMENT_SETTING_REFERENCED; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.*; +import static com.yhy.module.core.enums.ErrorCodeConstants.*; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import com.yhy.module.core.controller.admin.quota.vo.QuotaAdjustmentSettingSaveReqVO; import com.yhy.module.core.dal.dataobject.quota.QuotaAdjustmentSettingDO; +import com.yhy.module.core.dal.dataobject.quota.QuotaItemDO; import com.yhy.module.core.dal.mysql.quota.QuotaAdjustmentDetailMapper; import com.yhy.module.core.dal.mysql.quota.QuotaAdjustmentSettingMapper; +import com.yhy.module.core.dal.mysql.quota.QuotaCatalogItemMapper; +import com.yhy.module.core.dal.mysql.quota.QuotaCatalogTreeMapper; import com.yhy.module.core.dal.mysql.quota.QuotaItemMapper; +import com.yhy.module.core.dal.mysql.resource.ResourceItemMapper; import com.yhy.module.core.service.quota.QuotaAdjustmentSettingService; import java.util.List; +import java.util.Map; import javax.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -40,11 +42,23 @@ public class QuotaAdjustmentSettingServiceImpl implements QuotaAdjustmentSetting @Resource private QuotaItemMapper quotaItemMapper; + @Resource + private QuotaCatalogTreeMapper quotaCatalogTreeMapper; + + @Resource + private QuotaCatalogItemMapper quotaCatalogItemMapper; + + @Resource + private ResourceItemMapper resourceItemMapper; + @Override public Long createQuotaAdjustmentSetting(QuotaAdjustmentSettingSaveReqVO createReqVO) { - // 验证定额子目是否存在 + // 验证定额基价是否存在 validateQuotaItemExists(createReqVO.getQuotaItemId()); + // 验证调整规则中的编码(如果是动态合并定额类型) + validateAdjustmentRulesCodes(createReqVO.getQuotaItemId(), createReqVO.getAdjustmentRules()); + // 插入 QuotaAdjustmentSettingDO setting = BeanUtils.toBean(createReqVO, QuotaAdjustmentSettingDO.class); if (setting.getSortOrder() == null) { @@ -58,9 +72,12 @@ public class QuotaAdjustmentSettingServiceImpl implements QuotaAdjustmentSetting public void updateQuotaAdjustmentSetting(QuotaAdjustmentSettingSaveReqVO updateReqVO) { // 校验存在 validateQuotaAdjustmentSettingExists(updateReqVO.getId()); - // 验证定额子目是否存在 + // 验证定额基价是否存在 validateQuotaItemExists(updateReqVO.getQuotaItemId()); + // 验证调整规则中的编码(如果是动态合并定额类型) + validateAdjustmentRulesCodes(updateReqVO.getQuotaItemId(), updateReqVO.getAdjustmentRules()); + // 更新 QuotaAdjustmentSettingDO updateObj = BeanUtils.toBean(updateReqVO, QuotaAdjustmentSettingDO.class); quotaAdjustmentSettingMapper.updateById(updateObj); @@ -111,7 +128,7 @@ public class QuotaAdjustmentSettingServiceImpl implements QuotaAdjustmentSetting throw exception(QUOTA_ADJUSTMENT_SETTING_NOT_EXISTS); } - // 验证是否在同一定额子目下 + // 验证是否在同一定额基价下 if (!setting1.getQuotaItemId().equals(setting2.getQuotaItemId())) { throw exception(QUOTA_ADJUSTMENT_SETTING_NOT_SAME_QUOTA_ITEM); } @@ -137,4 +154,240 @@ public class QuotaAdjustmentSettingServiceImpl implements QuotaAdjustmentSetting } } + /** + * 验证调整规则中的数据 + * + * @param currentQuotaItemId 当前定额基价ID + * @param adjustmentRules 调整规则Map + */ + private void validateAdjustmentRulesCodes(Long currentQuotaItemId, Map adjustmentRules) { + if (adjustmentRules == null || adjustmentRules.isEmpty()) { + return; + } + + try { + // 获取类型 + String type = adjustmentRules.get("type") != null ? adjustmentRules.get("type").toString() : ""; + Object itemsObj = adjustmentRules.get("items"); + + if (!(itemsObj instanceof java.util.List)) { + return; + } + + @SuppressWarnings("unchecked") + java.util.List> items = (java.util.List>) itemsObj; + + // 验证 adjust_coefficient 类型的系数字段 + if ("adjust_coefficient".equals(type)) { + java.util.List invalidCoefficients = new java.util.ArrayList<>(); + for (int i = 0; i < items.size(); i++) { + Map item = items.get(i); + Object coefficientObj = item.get("coefficient"); + if (coefficientObj != null) { + String coefficientStr = coefficientObj.toString(); + try { + new java.math.BigDecimal(coefficientStr); + } catch (NumberFormatException e) { + String name = item.get("name") != null ? item.get("name").toString() : "第" + (i + 1) + "行"; + invalidCoefficients.add(name + ": " + coefficientStr); + } + } + } + if (!invalidCoefficients.isEmpty()) { + throw exception0(BAD_REQUEST.getCode(), "系数必须是数字,以下数据无效:" + String.join(", ", invalidCoefficients)); + } + } + + // 验证 dynamic_adjust 类型的必填字段和系数设置字段 + if ("dynamic_adjust".equals(type)) { + java.util.List missingFields = new java.util.ArrayList<>(); + java.util.List invalidCoefficients = new java.util.ArrayList<>(); + + for (int i = 0; i < items.size(); i++) { + Map item = items.get(i); + java.util.List rowMissing = new java.util.ArrayList<>(); + + // 验证必填字段(name 字段已从前端移除,不再校验) + Object nameObj = item.get("name"); + Object categoryObj = item.get("category"); + if (categoryObj == null || categoryObj.toString().trim().isEmpty()) { + rowMissing.add("类别"); + } + Object coefficientObj = item.get("coefficientSetting"); + if (coefficientObj == null || coefficientObj.toString().trim().isEmpty()) { + rowMissing.add("系数设置"); + } + Object valueRuleObj = item.get("valueRule"); + if (valueRuleObj == null || valueRuleObj.toString().trim().isEmpty()) { + rowMissing.add("取值规则"); + } + + if (!rowMissing.isEmpty()) { + String name = nameObj != null && !nameObj.toString().trim().isEmpty() + ? nameObj.toString() : "第" + (i + 1) + "行"; + missingFields.add(name + "缺少:" + String.join("、", rowMissing)); + } + + // 验证所有数值字段是否为有效数字 + String rowName = nameObj != null && !nameObj.toString().trim().isEmpty() + ? nameObj.toString() : "第" + (i + 1) + "行"; + validateNumericField(item.get("coefficientSetting"), "系数设置", rowName, invalidCoefficients); + validateNumericField(item.get("minValue"), "最小值", rowName, invalidCoefficients); + validateNumericField(item.get("maxValue"), "最大值", rowName, invalidCoefficients); + validateNumericField(item.get("denominatorValue"), "分母值", rowName, invalidCoefficients); + validateNumericField(item.get("baseValue"), "基础值", rowName, invalidCoefficients); + } + + if (!missingFields.isEmpty()) { + throw exception0(BAD_REQUEST.getCode(), "必填字段未填写:" + String.join(";", missingFields)); + } + if (!invalidCoefficients.isEmpty()) { + throw exception0(BAD_REQUEST.getCode(), "数值字段必须是数字,以下数据无效:" + String.join(", ", invalidCoefficients)); + } + } + + // 验证动态合并定额类型(编码是定额基价编码) + // consumption_adjustment 类型的编码是工料机编码,在应用调整时才匹配,不在此处验证 + if ("dynamic_merge".equals(type)) { + // 限制只允许一行数据 + if (items.size() > 1) { + throw exception0(BAD_REQUEST.getCode(), "动态合并定额只允许设置一行数据"); + } + java.util.List invalidCodes = new java.util.ArrayList<>(); + java.util.List selfBindingCodes = new java.util.ArrayList<>(); + java.util.List differentSpecialtyCodes = new java.util.ArrayList<>(); + + // 获取当前定额基价的专业节点ID + Long currentSpecialtyId = getSpecialtyIdByQuotaItemId(currentQuotaItemId); + QuotaItemDO currentQuotaItem = quotaItemMapper.selectById(currentQuotaItemId); + String currentCode = currentQuotaItem != null ? currentQuotaItem.getCode() : null; + + for (Map item : items) { + String code = item.get("code") != null ? item.get("code").toString() : ""; + if (!code.isEmpty()) { + // 1. 禁止自己绑定自己 + if (code.equals(currentCode)) { + selfBindingCodes.add(code); + continue; + } + + // 查询定额基价编码是否存在 + List quotaItems = quotaItemMapper.selectByCode(code); + if (quotaItems == null || quotaItems.isEmpty()) { + invalidCodes.add(code); + continue; + } + + // 2. 只允许添加同一定额专业节点下的定额 + if (currentSpecialtyId != null) { + for (QuotaItemDO targetQuotaItem : quotaItems) { + Long targetSpecialtyId = getSpecialtyIdByQuotaItemId(targetQuotaItem.getId()); + if (targetSpecialtyId == null || !targetSpecialtyId.equals(currentSpecialtyId)) { + differentSpecialtyCodes.add(code); + break; + } + } + } + } + } + + // 抛出错误 + if (!selfBindingCodes.isEmpty()) { + throw exception0(BAD_REQUEST.getCode(), "不允许绑定自己:" + String.join(", ", selfBindingCodes)); + } + if (!invalidCodes.isEmpty()) { + throw exception0(BAD_REQUEST.getCode(), "以下定额基价编码不存在:" + String.join(", ", invalidCodes)); + } + if (!differentSpecialtyCodes.isEmpty()) { + throw exception0(BAD_REQUEST.getCode(), "只允许添加本定额专业节点下的定额,以下编码不在同一专业下:" + String.join(", ", differentSpecialtyCodes)); + } + } + + // 验证增减材料消耗量类型(编码是工料机编码) + if ("consumption_adjustment".equals(type)) { + java.util.List invalidCodes = new java.util.ArrayList<>(); + java.util.List differentCategoryTreeCodes = new java.util.ArrayList<>(); + + // 获取当前定额专业绑定的工料机专业节点ID + Long currentSpecialtyId = getSpecialtyIdByQuotaItemId(currentQuotaItemId); + Long boundCategoryTreeId = null; + if (currentSpecialtyId != null) { + com.yhy.module.core.dal.dataobject.quota.QuotaCatalogItemDO catalogItem = quotaCatalogItemMapper.selectById(currentSpecialtyId); + if (catalogItem != null) { + boundCategoryTreeId = catalogItem.getCategoryTreeId(); + } + } + + for (Map item : items) { + String code = item.get("code") != null ? item.get("code").toString() : ""; + if (!code.isEmpty()) { + // 查询工料机是否存在 + com.yhy.module.core.dal.dataobject.resource.ResourceItemDO resourceItem = resourceItemMapper.selectByCode(code); + if (resourceItem == null) { + invalidCodes.add(code); + } else if (boundCategoryTreeId != null) { + // 验证工料机是否属于绑定的工料机专业节点 + Long resourceCategoryTreeId = resourceItem.getCategoryTreeId(); + if (resourceCategoryTreeId == null || !resourceCategoryTreeId.equals(boundCategoryTreeId)) { + differentCategoryTreeCodes.add(code); + } + } + } + } + + // 抛出错误 + if (!invalidCodes.isEmpty()) { + throw exception0(BAD_REQUEST.getCode(), "以下工料机编码不存在:" + String.join(", ", invalidCodes)); + } + if (!differentCategoryTreeCodes.isEmpty()) { + throw exception0(BAD_REQUEST.getCode(), "只允许添加本定额专业绑定的工料机专业下的工料机,以下编码不在绑定的工料机专业下:" + String.join(", ", differentCategoryTreeCodes)); + } + } + } catch (Exception e) { + if (e instanceof cn.iocoder.yudao.framework.common.exception.ServiceException) { + throw (cn.iocoder.yudao.framework.common.exception.ServiceException) e; + } + log.error("[validateAdjustmentRulesCodes] 验证调整规则失败", e); + } + } + + /** + * 根据定额基价ID获取所属的定额专业节点ID + * 定额基价 -> 定额子目树(catalog_item_id) -> 定额专业节点(catalog_item_id) + * + * @param quotaItemId 定额基价ID + * @return 定额专业节点ID,如果找不到返回null + */ + private Long getSpecialtyIdByQuotaItemId(Long quotaItemId) { + if (quotaItemId == null) { + return null; + } + QuotaItemDO quotaItem = quotaItemMapper.selectById(quotaItemId); + if (quotaItem == null || quotaItem.getCatalogItemId() == null) { + return null; + } + // 查询定额子目树节点 + com.yhy.module.core.dal.dataobject.quota.QuotaCatalogTreeDO catalogTree = quotaCatalogTreeMapper.selectById(quotaItem.getCatalogItemId()); + if (catalogTree == null) { + return null; + } + // 返回定额专业节点ID + return catalogTree.getCatalogItemId(); + } + + /** + * 验证数值字段是否为有效数字 + */ + private void validateNumericField(Object value, String fieldName, String rowName, java.util.List invalidList) { + if (value == null || value.toString().trim().isEmpty()) { + return; + } + String valueStr = value.toString().trim(); + try { + new java.math.BigDecimal(valueStr); + } catch (NumberFormatException e) { + invalidList.add(rowName + "的" + fieldName + ": " + valueStr); + } + } + } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaCatalogItemServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaCatalogItemServiceImpl.java index 400b340..dba7c62 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaCatalogItemServiceImpl.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaCatalogItemServiceImpl.java @@ -20,6 +20,7 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; @@ -55,7 +56,16 @@ public class QuotaCatalogItemServiceImpl implements QuotaCatalogItemService { @Override @Transactional(rollbackFor = Exception.class) public Long createQuotaCatalogItem(QuotaCatalogItemSaveReqVO createReqVO) { - // 转换�?DO + // 根节点唯一性校验:只允许一个根节点 + if (createReqVO.getParentId() == null) { + List roots = quotaCatalogItemMapper.selectRootList(); + if (!roots.isEmpty()) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.QUOTA_CATALOG_ITEM_HAS_CHILDREN, + "已存在根节点「" + roots.get(0).getName() + "」,不允许重复创建"); + } + } + + // 转换为DO QuotaCatalogItemDO catalogItem = convertToDO(createReqVO); // 设置路径 @@ -93,11 +103,16 @@ public class QuotaCatalogItemServiceImpl implements QuotaCatalogItemService { @Transactional(rollbackFor = Exception.class) public void deleteQuotaCatalogItem(Long id) { // 校验存在 - validateExists(id); + QuotaCatalogItemDO item = validateExists(id); - // 检查是否有子节�? - if (quotaCatalogItemMapper.hasChildren(id)) { - throw ServiceExceptionUtil.exception(ErrorCodeConstants.QUOTA_CATALOG_ITEM_HAS_CHILDREN); + // 检查是否有子节点,如果有则提示具体的子节点名称 + List children = quotaCatalogItemMapper.selectListByParentId(id); + if (!children.isEmpty()) { + String childNames = children.stream() + .map(c -> c.getName() + "(" + c.getNodeType() + ")") + .collect(java.util.stream.Collectors.joining("、")); + throw ServiceExceptionUtil.exception(ErrorCodeConstants.QUOTA_CATALOG_ITEM_HAS_CHILDREN, + "节点「" + item.getName() + "」下存在子节点:" + childNames + ",请先删除子节点"); } // 删除 @@ -116,6 +131,15 @@ public class QuotaCatalogItemServiceImpl implements QuotaCatalogItemService { return list.stream().map(this::convertToVO).collect(Collectors.toList()); } + @Override + public List getChildrenByParentId(Long parentId) { + List list = quotaCatalogItemMapper.selectListByParentId(parentId); + return list.stream() + .sorted(Comparator.comparing(QuotaCatalogItemDO::getSortOrder, Comparator.nullsLast(Comparator.naturalOrder()))) + .map(this::convertToVO) + .collect(Collectors.toList()); + } + @Override @Transactional(rollbackFor = Exception.class) public void bindResourceSpecialty(Long catalogItemId, Long categoryTreeId) { @@ -245,14 +269,39 @@ public class QuotaCatalogItemServiceImpl implements QuotaCatalogItemService { // 查询所有节点 List allNodes = quotaCatalogItemMapper.selectList(); - // 如果有排除类型,先过滤 + // 如果有排除类型,进行智能过滤 if (excludeNodeTypes != null && !excludeNodeTypes.isEmpty()) { Set excludeSet = excludeNodeTypes.stream() .filter(StrUtil::isNotBlank) .collect(Collectors.toSet()); if (!excludeSet.isEmpty()) { - allNodes = allNodes.stream() + // 找出所有需要保留的节点(不在排除列表中的节点) + Set keepNodeIds = allNodes.stream() .filter(node -> !excludeSet.contains(node.getNodeType())) + .map(QuotaCatalogItemDO::getId) + .collect(Collectors.toSet()); + + // 找出这些节点的所有祖先节点ID + Set ancestorIds = new HashSet<>(); + for (QuotaCatalogItemDO node : allNodes) { + if (keepNodeIds.contains(node.getId()) && node.getPath() != null) { + for (String pathId : node.getPath()) { + try { + ancestorIds.add(Long.parseLong(pathId)); + } catch (NumberFormatException ignored) { + } + } + } + } + + // 合并需要保留的节点ID和祖先节点ID + Set finalKeepIds = new HashSet<>(); + finalKeepIds.addAll(keepNodeIds); + finalKeepIds.addAll(ancestorIds); + + // 过滤节点 + allNodes = allNodes.stream() + .filter(node -> finalKeepIds.contains(node.getId())) .collect(Collectors.toList()); } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaCatalogTreeServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaCatalogTreeServiceImpl.java index dfceec5..c61e372 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaCatalogTreeServiceImpl.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaCatalogTreeServiceImpl.java @@ -1,15 +1,7 @@ package com.yhy.module.core.service.quota.impl; -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_CATALOG_ITEM_NOT_EXISTS; -import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_CATALOG_ITEM_PARENT_NOT_EXISTS; -import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_CATALOG_ITEM_SPECIALTY_NOT_FOUND; -import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_CATALOG_TREE_HAS_CHILDREN; -import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_CATALOG_TREE_NOT_EXISTS; -import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_CATALOG_TREE_NOT_SAME_LEVEL; -import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_CATALOG_TREE_PARENT_NOT_ALLOW_CHILDREN; -import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_CATALOG_TREE_PARENT_NOT_EXISTS; -import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_RATE_ITEM_NOT_EXISTS; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.*; +import static com.yhy.module.core.enums.ErrorCodeConstants.*; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ArrayUtil; @@ -36,7 +28,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; /** - * 定额子目树 Service 实现类 + * 定额基价树 Service 实现类 * * @author yhy */ @@ -113,6 +105,7 @@ public class QuotaCatalogTreeServiceImpl implements QuotaCatalogTreeService { .unit(updateReqVO.getUnit()) .sortOrder(updateReqVO.getSortOrder()) .attributes(updateReqVO.getAttributes()) + .remark(updateReqVO.getRemark()) .build(); // 3. 更新数据库 @@ -283,6 +276,7 @@ public class QuotaCatalogTreeServiceImpl implements QuotaCatalogTreeService { vo.setPath(item.getPath()); vo.setLevel(item.getLevel()); vo.setAttributes(item.getAttributes()); + vo.setRemark(item.getRemark()); vo.setCreateTime(item.getCreateTime()); return vo; } @@ -343,7 +337,7 @@ public class QuotaCatalogTreeServiceImpl implements QuotaCatalogTreeService { throw exception(QUOTA_CATALOG_ITEM_SPECIALTY_NOT_FOUND); } - // 4. 查询定额专业节点对应的定额子目树(第二层) + // 4. 查询定额专业节点对应的定额基价树(第二层) List catalogTrees = quotaCatalogTreeMapper.selectListByCatalogItemId(specialtyNode.getId()); // 5. 转换为VO @@ -375,7 +369,7 @@ public class QuotaCatalogTreeServiceImpl implements QuotaCatalogTreeService { throw exception(QUOTA_CATALOG_ITEM_SPECIALTY_NOT_FOUND); } - // 4. 查询定额专业节点对应的定额子目树(第二层) + // 4. 查询定额专业节点对应的定额基价树(第二层) List catalogTrees = quotaCatalogTreeMapper.selectListByCatalogItemId(specialtyNode.getId()); // 5. 查询该费率模式节点的所有字段绑定 diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaFeeItemServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaFeeItemServiceImpl.java index 713719f..15acff4 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaFeeItemServiceImpl.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaFeeItemServiceImpl.java @@ -1,16 +1,7 @@ package com.yhy.module.core.service.quota.impl; -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_CATALOG_ITEM_NOT_EXISTS; -import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_FEE_CALC_BASE_FORMULA_BRACKET_MISMATCH; -import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_FEE_CALC_BASE_FORMULA_EMPTY; -import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_FEE_CALC_BASE_FORMULA_INVALID_CHAR; -import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_FEE_CALC_BASE_INVALID_PRICE_CODE; -import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_FEE_CALC_BASE_VARIABLES_EMPTY; -import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_FEE_ITEM_CATALOG_NOT_RATE_MODE; -import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_FEE_ITEM_CUSTOM_CODE_DUPLICATE; -import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_FEE_ITEM_NOT_EXISTS; -import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_FEE_ITEM_NOT_SAME_CATALOG; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.*; +import static com.yhy.module.core.enums.ErrorCodeConstants.*; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.map.MapUtil; @@ -26,10 +17,14 @@ import com.yhy.module.core.dal.mysql.quota.QuotaFeeItemMapper; import com.yhy.module.core.dal.mysql.quota.QuotaRateItemMapper; import com.yhy.module.core.service.quota.QuotaFeeItemService; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import com.yhy.module.core.controller.admin.resource.vo.ResourceCategoryFullRespVO; +import com.yhy.module.core.service.quota.QuotaCatalogItemService; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import javax.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -53,6 +48,9 @@ public class QuotaFeeItemServiceImpl implements QuotaFeeItemService { @Resource private QuotaRateItemMapper quotaRateItemMapper; + @Resource + private QuotaCatalogItemService quotaCatalogItemService; + @Override @Transactional(rollbackFor = Exception.class) public Long createFeeItem(QuotaFeeItemSaveReqVO createReqVO) { @@ -64,9 +62,9 @@ public class QuotaFeeItemServiceImpl implements QuotaFeeItemService { validateRateItem(createReqVO.getCatalogItemId(), createReqVO.getRateItemId()); } - // 3. 验证计算基数 + // 3. 验证计算基数(创建时不是系统行) if (createReqVO.getCalcBase() != null) { - validateCalcBase(createReqVO.getCalcBase()); + validateCalcBaseInternal(createReqVO.getCalcBase(), false, createReqVO.getCatalogItemId()); } // 4. 验证自定义序号唯一性 @@ -74,13 +72,18 @@ public class QuotaFeeItemServiceImpl implements QuotaFeeItemService { validateCustomCodeUnique(null, createReqVO.getCatalogItemId(), createReqVO.getCustomCode()); } - // 5. 设置排序值 + // 5. 验证代号唯一性 + if (StrUtil.isNotBlank(createReqVO.getCode())) { + validateCodeUnique(null, createReqVO.getCatalogItemId(), createReqVO.getCode()); + } + + // 6. 设置排序值 if (createReqVO.getSortOrder() == null) { Integer maxSortOrder = quotaFeeItemMapper.selectMaxSortOrder(createReqVO.getCatalogItemId()); createReqVO.setSortOrder(maxSortOrder + 1); } - // 6. 插入数据 + // 7. 插入数据 QuotaFeeItemDO feeItem = BeanUtils.toBean(createReqVO, QuotaFeeItemDO.class); quotaFeeItemMapper.insert(feeItem); @@ -91,28 +94,50 @@ public class QuotaFeeItemServiceImpl implements QuotaFeeItemService { @Transactional(rollbackFor = Exception.class) public void updateFeeItem(QuotaFeeItemSaveReqVO updateReqVO) { // 1. 验证存在 - validateFeeItemExists(updateReqVO.getId()); + QuotaFeeItemDO existingItem = quotaFeeItemMapper.selectById(updateReqVO.getId()); + if (existingItem == null) { + throw exception(QUOTA_FEE_ITEM_NOT_EXISTS); + } - // 2. 验证模式节点 + // 2. 系统行保护:禁止修改代号 + if (existingItem.isSystemRow() && updateReqVO.getCode() != null + && !existingItem.getCode().equals(updateReqVO.getCode())) { + throw exception(QUOTA_FEE_ITEM_SYSTEM_ROW_CANNOT_MODIFY_CODE); + } + + // 3. 验证模式节点 validateRateModeNode(updateReqVO.getCatalogItemId()); - // 3. 验证费率项ID(如果提供) + // 4. 验证费率项ID(如果提供) if (updateReqVO.getRateItemId() != null) { validateRateItem(updateReqVO.getCatalogItemId(), updateReqVO.getRateItemId()); } - // 4. 验证计算基数 + // 5. 验证计算基数 + // - 综合单价(ZHDJ):跳过变量验证,公式使用其他取费项代号作为变量 + // - 普通取费项:验证公式中的变量是否有效(取费项代号或类别价格代码) if (updateReqVO.getCalcBase() != null) { - validateCalcBase(updateReqVO.getCalcBase()); + String systemCode = existingItem.getSystemCode(); + boolean isZhdj = QuotaFeeItemDO.SYSTEM_CODE_ZHDJ.equals(systemCode); + validateCalcBaseInternal(updateReqVO.getCalcBase(), isZhdj, updateReqVO.getCatalogItemId()); } - // 5. 验证自定义序号唯一性 + // 6. 验证自定义序号唯一性 if (StrUtil.isNotBlank(updateReqVO.getCustomCode())) { validateCustomCodeUnique(updateReqVO.getId(), updateReqVO.getCatalogItemId(), updateReqVO.getCustomCode()); } - // 6. 更新数据 + // 7. 验证代号唯一性(非系统行才验证) + if (!existingItem.isSystemRow() && StrUtil.isNotBlank(updateReqVO.getCode())) { + validateCodeUnique(updateReqVO.getId(), updateReqVO.getCatalogItemId(), updateReqVO.getCode()); + } + + // 8. 更新数据(系统行不更新 systemCode 和 sortOrder) QuotaFeeItemDO updateObj = BeanUtils.toBean(updateReqVO, QuotaFeeItemDO.class); + if (existingItem.isSystemRow()) { + updateObj.setSystemCode(null); // 不更新 systemCode + updateObj.setSortOrder(null); // 不更新 sortOrder + } quotaFeeItemMapper.updateById(updateObj); } @@ -120,9 +145,17 @@ public class QuotaFeeItemServiceImpl implements QuotaFeeItemService { @Transactional(rollbackFor = Exception.class) public void deleteFeeItem(Long id) { // 1. 验证存在 - validateFeeItemExists(id); + QuotaFeeItemDO feeItem = quotaFeeItemMapper.selectById(id); + if (feeItem == null) { + throw exception(QUOTA_FEE_ITEM_NOT_EXISTS); + } - // 2. 删除 + // 2. 系统行保护:禁止删除 + if (feeItem.isSystemRow()) { + throw exception(QUOTA_FEE_ITEM_SYSTEM_ROW_CANNOT_DELETE); + } + + // 3. 删除 quotaFeeItemMapper.deleteById(id); } @@ -132,23 +165,43 @@ public class QuotaFeeItemServiceImpl implements QuotaFeeItemService { } @Override + @Transactional(rollbackFor = Exception.class) public List getFeeItemList(Long catalogItemId) { - return quotaFeeItemMapper.selectListByCatalogItemId(catalogItemId); + List list = quotaFeeItemMapper.selectListByCatalogItemId(catalogItemId); + + // 懒加载:确保综合单价行存在 + ensureSystemRowExists(catalogItemId, list); + + return list; } @Override + @Transactional(rollbackFor = Exception.class) public List getFeeItemWithRateList(Long catalogItemId) { // 1. 验证模式节点 validateRateModeNode(catalogItemId); - // 2. 查询所有费率项节点(不限制层级) - List rateItems = quotaRateItemMapper.selectListByCatalogItemId(catalogItemId); + // 2. 查询所有费率项节点(只获取目录节点,不获取子项) + List allRateItems = quotaRateItemMapper.selectListByCatalogItemId(catalogItemId); + // 过滤只保留目录节点 + List rateItems = allRateItems.stream() + .filter(QuotaRateItemDO::isDirectory) + .collect(Collectors.toList()); // 3. 查询取费项列表 List feeItems = quotaFeeItemMapper.selectListByCatalogItemId(catalogItemId); - // 4. 构建取费项Map(优先使用 rate_item_id,其次使用 rateCode) - Map feeItemByRateItemIdMap = feeItems.stream() + // 4. 使用公共方法构建结果 + return buildFeeItemWithRateList(rateItems, feeItems, catalogItemId); + } + + @Override + public List buildFeeItemWithRateList( + List rateItems, + List feeItems, + Long catalogItemId) { + // 1. 构建取费项Map(使用 rate_item_id) + Map feeItemByRateItemIdMap = feeItems.stream() .filter(item -> item.getRateItemId() != null) .collect(Collectors.toMap( QuotaFeeItemDO::getRateItemId, @@ -156,15 +209,7 @@ public class QuotaFeeItemServiceImpl implements QuotaFeeItemService { (existing, replacement) -> existing )); - Map feeItemByRateCodeMap = feeItems.stream() - .filter(item -> StrUtil.isNotBlank(item.getRateCode()) && item.getRateItemId() == null) - .collect(Collectors.toMap( - QuotaFeeItemDO::getRateCode, - item -> item, - (existing, replacement) -> existing - )); - - // 5. 转换为 VO 并合并取费项数据 + // 2. 转换为 VO 并合并取费项数据 List allVos = new ArrayList<>(); for (QuotaRateItemDO rateItem : rateItems) { QuotaFeeItemWithRateRespVO vo = new QuotaFeeItemWithRateRespVO(); @@ -177,12 +222,13 @@ public class QuotaFeeItemServiceImpl implements QuotaFeeItemService { vo.setParentId(rateItem.getParentId()); vo.setNodeType(rateItem.getNodeType()); vo.setCatalogItemId(catalogItemId); + // 费率代号始终从费率项获取(无论是否有取费项) + vo.setRatePercentage(rateItem.getRateCode()); + // 费率实际值从费率项的默认值获取 + vo.setRateValue(rateItem.getDefaultValue()); - // 查找对应的取费项(优先使用 rate_item_id,其次使用 rateCode) + // 查找对应的取费项(使用 rate_item_id) QuotaFeeItemDO feeItem = feeItemByRateItemIdMap.get(rateItem.getId()); - if (feeItem == null && StrUtil.isNotBlank(rateItem.getRateCode())) { - feeItem = feeItemByRateCodeMap.get(rateItem.getRateCode()); - } if (feeItem != null) { // 设置取费项信息 @@ -190,7 +236,6 @@ public class QuotaFeeItemServiceImpl implements QuotaFeeItemService { vo.setFeeItemName(feeItem.getName()); vo.setFeeItemCustomCode(feeItem.getCustomCode()); vo.setCalcBase(feeItem.getCalcBase()); - vo.setRatePercentage(feeItem.getRateCode()); vo.setCode(feeItem.getCode()); vo.setFeeCategory(feeItem.getFeeCategory()); vo.setBaseDescription(feeItem.getBaseDescription()); @@ -207,8 +252,95 @@ public class QuotaFeeItemServiceImpl implements QuotaFeeItemService { allVos.add(vo); } - // 6. 构建树形结构 - return buildTree(allVos); + // 3. 构建树形结构 + List result = buildTree(allVos); + + // 4. 追加综合单价系统行(始终排在最后) + appendSystemRowFromList(catalogItemId, feeItems, result); + + return result; + } + + /** + * 追加系统行到结果列表末尾(只有综合单价) + */ + private void appendSystemRow(Long catalogItemId, List feeItems, List result) { + // 处理综合单价系统行 + QuotaFeeItemDO zhdjItem = feeItems.stream() + .filter(item -> QuotaFeeItemDO.SYSTEM_CODE_ZHDJ.equals(item.getSystemCode())) + .findFirst() + .orElse(null); + + if (zhdjItem == null) { + zhdjItem = new QuotaFeeItemDO(); + zhdjItem.setCatalogItemId(catalogItemId); + zhdjItem.setName("综合单价"); + zhdjItem.setCode(QuotaFeeItemDO.SYSTEM_CODE_ZHDJ); + zhdjItem.setSystemCode(QuotaFeeItemDO.SYSTEM_CODE_ZHDJ); + zhdjItem.setSortOrder(QuotaFeeItemDO.SYSTEM_ROW_SORT_ORDER); + zhdjItem.setHidden(false); + zhdjItem.setVariable(false); + quotaFeeItemMapper.insert(zhdjItem); + log.info("[appendSystemRow] 自动创建综合单价系统行, catalogItemId={}, id={}", catalogItemId, zhdjItem.getId()); + } + + // 转换综合单价为 VO + QuotaFeeItemWithRateRespVO zhdjVo = convertSystemRowToVO(zhdjItem, catalogItemId); + result.add(zhdjVo); + } + + /** + * 追加系统行到结果列表末尾(供buildFeeItemWithRateList使用,自动创建缺失的系统行) + */ + private void appendSystemRowFromList(Long catalogItemId, List feeItems, List result) { + // 处理综合单价系统行 + QuotaFeeItemDO zhdjItem = feeItems.stream() + .filter(item -> QuotaFeeItemDO.SYSTEM_CODE_ZHDJ.equals(item.getSystemCode())) + .findFirst() + .orElse(null); + + if (zhdjItem == null) { + // 自动创建综合单价系统行 + zhdjItem = new QuotaFeeItemDO(); + zhdjItem.setCatalogItemId(catalogItemId); + zhdjItem.setName("综合单价"); + zhdjItem.setCode(QuotaFeeItemDO.SYSTEM_CODE_ZHDJ); + zhdjItem.setSystemCode(QuotaFeeItemDO.SYSTEM_CODE_ZHDJ); + zhdjItem.setSortOrder(QuotaFeeItemDO.SYSTEM_ROW_SORT_ORDER); + zhdjItem.setHidden(false); + zhdjItem.setVariable(false); + quotaFeeItemMapper.insert(zhdjItem); + log.info("[appendSystemRowFromList] 自动创建综合单价系统行, catalogItemId={}, id={}", catalogItemId, zhdjItem.getId()); + } + QuotaFeeItemWithRateRespVO zhdjVo = convertSystemRowToVO(zhdjItem, catalogItemId); + result.add(zhdjVo); + } + + /** + * 将系统行 DO 转换为 VO + */ + private QuotaFeeItemWithRateRespVO convertSystemRowToVO(QuotaFeeItemDO item, Long catalogItemId) { + QuotaFeeItemWithRateRespVO vo = new QuotaFeeItemWithRateRespVO(); + vo.setFeeItemId(item.getId()); + vo.setFeeItemName(item.getName()); + vo.setFeeItemCustomCode(item.getCustomCode()); + vo.setCalcBase(item.getCalcBase()); + vo.setCode(item.getCode()); + vo.setFeeCategory(item.getFeeCategory()); + vo.setBaseDescription(item.getBaseDescription()); + vo.setSortOrder(item.getSortOrder()); + vo.setHidden(item.getHidden()); + vo.setVariable(item.getVariable()); + vo.setSystemCode(item.getSystemCode()); + vo.setCatalogItemId(catalogItemId); + vo.setHasFeeItem(true); + vo.setCreateTime(item.getCreateTime()); + vo.setUpdateTime(item.getUpdateTime()); + // 系统行没有关联费率项 + vo.setRateItemId(null); + vo.setRateItemName(null); + vo.setNodeType("system"); // 标识为系统行 + return vo; } /** @@ -273,17 +405,22 @@ public class QuotaFeeItemServiceImpl implements QuotaFeeItemService { throw exception(QUOTA_FEE_ITEM_NOT_EXISTS); } - // 2. 验证是否在同一模式下 + // 2. 系统行保护:禁止参与排序交换 + if (node1.isSystemRow() || node2.isSystemRow()) { + throw exception(QUOTA_FEE_ITEM_SYSTEM_ROW_CANNOT_SWAP); + } + + // 3. 验证是否在同一模式下 if (!node1.getCatalogItemId().equals(node2.getCatalogItemId())) { throw exception(QUOTA_FEE_ITEM_NOT_SAME_CATALOG); } - // 3. 交换排序值 + // 4. 交换排序值 Integer tempSort = node1.getSortOrder(); node1.setSortOrder(node2.getSortOrder()); node2.setSortOrder(tempSort); - // 4. 更新数据库 + // 5. 更新数据库 quotaFeeItemMapper.updateById(node1); quotaFeeItemMapper.updateById(node2); } @@ -327,19 +464,47 @@ public class QuotaFeeItemServiceImpl implements QuotaFeeItemService { throw exception(QUOTA_FEE_ITEM_NOT_SAME_CATALOG); } - // 注意:不再限制必须是一级节点,所有层级的费率项都可以关联取费项 + // 验证费率项必须是目录节点(只有一级和二级目录可以关联取费项) + if (!rateItem.isDirectory()) { + throw exception(QUOTA_FEE_ITEM_RATE_ITEM_NOT_DIRECTORY); + } + } + + /** + * 验证计算基数(公开方法,供其他服务调用) + * @param calcBase 计算基数对象 + */ + @Override + public void validateCalcBase(Map calcBase) { + validateCalcBaseInternal(calcBase, false, null); } /** * 验证计算基数 + * @param calcBase 计算基数对象 + * @param isZhdj 是否为综合单价行,跳过变量验证 + * @param catalogItemId 模式节点ID,用于获取有效的取费项代号和类别价格代码 */ - private void validateCalcBase(Map calcBase) { + private void validateCalcBaseInternal(Map calcBase, boolean isZhdj, Long catalogItemId) { // 1. 验证必填字段 String formula = (String) calcBase.get("formula"); if (StrUtil.isBlank(formula)) { throw exception(QUOTA_FEE_CALC_BASE_FORMULA_EMPTY); } + // 综合单价(ZHDJ):只验证公式不为空,跳过变量验证 + // 因为综合单价的公式使用其他取费项的代号作为变量,不需要 categoryId/priceField 定义 + if (isZhdj) { + return; + } + + // 普通取费项:验证公式中的变量是否有效(取费项代号或类别价格代码) + if (catalogItemId != null) { + validateFeeItemFormulaVariables(formula, catalogItemId); + return; + } + + // 兼容旧逻辑:验证 variables 中的 categoryId 和 priceField @SuppressWarnings("unchecked") Map variables = (Map) calcBase.get("variables"); if (MapUtil.isEmpty(variables)) { @@ -384,6 +549,67 @@ public class QuotaFeeItemServiceImpl implements QuotaFeeItemService { validateFormulaSyntax(formula, variables.keySet()); } + /** + * 验证普通取费项公式中的变量是否有效(可引用取费项代号和类别价格代码) + * @param formula 公式字符串 + * @param catalogItemId 模式节点ID + */ + private void validateFeeItemFormulaVariables(String formula, Long catalogItemId) { + // 1. 提取公式中的所有变量(去除运算符和数字) + String cleanFormula = formula.replaceAll("[+\\-*/()\\s\\d.]+", " "); + String[] potentialCodes = cleanFormula.trim().split("\\s+"); + + if (potentialCodes.length == 0 || (potentialCodes.length == 1 && potentialCodes[0].isEmpty())) { + return; // 没有变量,只有数字和运算符 + } + + // 2. 获取所有有效的变量代码 + Set validCodes = new HashSet<>(); + + // 2.1 获取取费项代号(排除当前行和综合单价) + List feeItems = quotaFeeItemMapper.selectList( + new LambdaQueryWrapperX() + .eq(QuotaFeeItemDO::getCatalogItemId, catalogItemId) + ); + for (QuotaFeeItemDO item : feeItems) { + if (StrUtil.isNotBlank(item.getCode()) + && !QuotaFeeItemDO.SYSTEM_CODE_ZHDJ.equals(item.getSystemCode())) { + validCodes.add(item.getCode()); + } + } + + // 2.2 获取类别价格代码(从机类字典获取) + List categories = quotaCatalogItemService.getCategoriesByCatalogItem(catalogItemId); + for (ResourceCategoryFullRespVO category : categories) { + // 添加4个价格代码 + if (StrUtil.isNotBlank(category.getTaxExclBaseCode())) { + validCodes.add(category.getTaxExclBaseCode()); + } + if (StrUtil.isNotBlank(category.getTaxInclBaseCode())) { + validCodes.add(category.getTaxInclBaseCode()); + } + if (StrUtil.isNotBlank(category.getTaxExclCompileCode())) { + validCodes.add(category.getTaxExclCompileCode()); + } + if (StrUtil.isNotBlank(category.getTaxInclCompileCode())) { + validCodes.add(category.getTaxInclCompileCode()); + } + } + + // 3. 检查公式中的变量是否都有效 + List invalidCodes = new ArrayList<>(); + for (String code : potentialCodes) { + if (StrUtil.isNotBlank(code) && !validCodes.contains(code)) { + invalidCodes.add(code); + } + } + + if (!invalidCodes.isEmpty()) { + throw exception(QUOTA_FEE_CALC_BASE_INVALID_PRICE_CODE, + "公式中包含无效的变量: " + String.join(", ", invalidCodes)); + } + } + /** * 验证公式语法 */ @@ -534,5 +760,43 @@ public class QuotaFeeItemServiceImpl implements QuotaFeeItemService { throw exception(QUOTA_FEE_ITEM_CUSTOM_CODE_DUPLICATE, customCode); } } + + /** + * 验证代号唯一性 + */ + private void validateCodeUnique(Long id, Long catalogItemId, String code) { + QuotaFeeItemDO existingItem = quotaFeeItemMapper.selectByCode(catalogItemId, code); + if (existingItem != null && !existingItem.getId().equals(id)) { + throw exception(QUOTA_FEE_ITEM_CODE_DUPLICATE, code); + } + } + + /** + * 确保系统行存在(懒加载) + * 如果综合单价行不存在,则自动创建 + */ + private void ensureSystemRowExists(Long catalogItemId, List list) { + // 检查是否已存在 ZHDJ 系统行 + boolean hasZhdj = list.stream() + .anyMatch(item -> QuotaFeeItemDO.SYSTEM_CODE_ZHDJ.equals(item.getSystemCode())); + + if (!hasZhdj) { + // 创建综合单价系统行 + QuotaFeeItemDO zhdjRow = new QuotaFeeItemDO(); + zhdjRow.setCatalogItemId(catalogItemId); + zhdjRow.setName("综合单价"); + zhdjRow.setCode(QuotaFeeItemDO.SYSTEM_CODE_ZHDJ); + zhdjRow.setSystemCode(QuotaFeeItemDO.SYSTEM_CODE_ZHDJ); + zhdjRow.setSortOrder(QuotaFeeItemDO.SYSTEM_ROW_SORT_ORDER); + zhdjRow.setHidden(false); + zhdjRow.setVariable(false); + + quotaFeeItemMapper.insert(zhdjRow); + list.add(zhdjRow); + + log.info("[ensureSystemRowExists] 自动创建综合单价系统行, catalogItemId={}, id={}", + catalogItemId, zhdjRow.getId()); + } + } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaItemServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaItemServiceImpl.java index c377a07..4fbd033 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaItemServiceImpl.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaItemServiceImpl.java @@ -24,7 +24,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; /** - * 定额子目 Service 实现类 + * 定额基价 Service 实现类 * * @author yhy */ @@ -48,26 +48,29 @@ public class QuotaItemServiceImpl implements QuotaItemService { @Resource private QuotaResourceService quotaResourceService; + @Resource + private com.yhy.module.core.dal.mysql.quota.QuotaMarketMaterialMapper quotaMarketMaterialMapper; + @Override @Transactional(rollbackFor = Exception.class) public Long createQuotaItem(QuotaItemSaveReqVO createReqVO) { // 验证定额条目存在且是内容节点 QuotaCatalogTreeDO catalogItem = validateCatalogItem(createReqVO.getCatalogItemId()); - + // 转换为 DO QuotaItemDO quotaItem = convertToDO(createReqVO); - + // 插入 quotaItemMapper.insert(quotaItem); - + // 如果有工料机组成,批量添加 if (createReqVO.getResources() != null && !createReqVO.getResources().isEmpty()) { quotaResourceService.batchCreateQuotaResources(quotaItem.getId(), createReqVO.getResources()); - + // 计算基价 calculateBasePrice(quotaItem.getId()); } - + return quotaItem.getId(); } @@ -76,21 +79,21 @@ public class QuotaItemServiceImpl implements QuotaItemService { public void updateQuotaItem(QuotaItemSaveReqVO updateReqVO) { // 校验存在 validateExists(updateReqVO.getId()); - + // 转换为 DO QuotaItemDO updateObj = convertToDO(updateReqVO); updateObj.setId(updateReqVO.getId()); - + // 更新 quotaItemMapper.updateById(updateObj); - + // 如果有工料机组成,先删除旧的,再添加新的 if (updateReqVO.getResources() != null) { quotaResourceMapper.deleteByQuotaItemId(updateReqVO.getId()); if (!updateReqVO.getResources().isEmpty()) { quotaResourceService.batchCreateQuotaResources(updateReqVO.getId(), updateReqVO.getResources()); } - + // 重新计算基价 calculateBasePrice(updateReqVO.getId()); } @@ -101,11 +104,24 @@ public class QuotaItemServiceImpl implements QuotaItemService { public void deleteQuotaItem(Long id) { // 校验存在 validateExists(id); - - // 删除工料机组成 - quotaResourceMapper.deleteByQuotaItemId(id); - - // 删除定额子目 + + // 校验是否存在工料机组成 + Long resourceCount = quotaResourceMapper.selectCount( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(com.yhy.module.core.dal.dataobject.quota.QuotaResourceDO::getQuotaItemId, id)); + if (resourceCount > 0) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.QUOTA_ITEM_HAS_RESOURCES); + } + + // 校验是否存在市场主材设备 + Long marketMaterialCount = quotaMarketMaterialMapper.selectCount( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(com.yhy.module.core.dal.dataobject.quota.QuotaMarketMaterialDO::getQuotaItemId, id)); + if (marketMaterialCount > 0) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.QUOTA_ITEM_HAS_MARKET_MATERIALS); + } + + // 删除定额基价 quotaItemMapper.deleteById(id); } @@ -117,59 +133,106 @@ public class QuotaItemServiceImpl implements QuotaItemService { @Override public QuotaItemRespVO getQuotaItemDetail(Long id) { QuotaItemDO quotaItem = validateExists(id); - + // 转换为 VO QuotaItemRespVO vo = convertToVO(quotaItem); - + // 查询工料机组成 List resources = quotaResourceService.getQuotaResourceList(id); vo.setResources(resources); + // 计算四个价格虚拟字段(从工料机合价总和计算) + calculateQuotaItemPricesFromResources(vo, resources); + return vo; } @Override public List getQuotaItemList(Long catalogItemId) { List list = quotaItemMapper.selectListByCatalogItemId(catalogItemId); - return list.stream().map(this::convertToVO).collect(Collectors.toList()); + List result = list.stream().map(this::convertToVO).collect(Collectors.toList()); + + // 为每个定额基价计算四个价格虚拟字段 + for (QuotaItemRespVO vo : result) { + calculateQuotaItemPrices(vo); + } + + return result; + } + + /** + * 从已查询的工料机列表计算四个价格虚拟字段 + * 避免重复查询工料机数据 + */ + private void calculateQuotaItemPricesFromResources(QuotaItemRespVO vo, List resources) { + if (vo == null || resources == null) { + return; + } + + // 计算四个价格字段的总和 + BigDecimal taxExclBaseTotal = BigDecimal.ZERO; + BigDecimal taxInclBaseTotal = BigDecimal.ZERO; + BigDecimal taxExclCompileTotal = BigDecimal.ZERO; + BigDecimal taxInclCompileTotal = BigDecimal.ZERO; + + for (QuotaResourceRespVO resource : resources) { + if (resource.getTaxExclBaseTotalSum() != null) { + taxExclBaseTotal = taxExclBaseTotal.add(resource.getTaxExclBaseTotalSum()); + } + if (resource.getTaxInclBaseTotalSum() != null) { + taxInclBaseTotal = taxInclBaseTotal.add(resource.getTaxInclBaseTotalSum()); + } + if (resource.getTaxExclCompileTotalSum() != null) { + taxExclCompileTotal = taxExclCompileTotal.add(resource.getTaxExclCompileTotalSum()); + } + if (resource.getTaxInclCompileTotalSum() != null) { + taxInclCompileTotal = taxInclCompileTotal.add(resource.getTaxInclCompileTotalSum()); + } + } + + // 设置虚拟字段 + vo.setTaxExclBasePrice(taxExclBaseTotal); + vo.setTaxInclBasePrice(taxInclBaseTotal); + vo.setTaxExclCompilePrice(taxExclCompileTotal); + vo.setTaxInclCompilePrice(taxInclCompileTotal); } @Override @Transactional(rollbackFor = Exception.class) public void calculateBasePrice(Long id) { QuotaItemDO quotaItem = validateExists(id); - + // 查询工料机组成 List resources = quotaResourceMapper.selectListByQuotaItemId(id); - + if (resources.isEmpty()) { - log.warn("[calculateBasePrice] 定额子目没有工料机组成,无法计算基价,quotaItemId={}", id); + log.warn("[calculateBasePrice] 定额基价没有工料机组成,无法计算基价,quotaItemId={}", id); return; } - + // 计算基价:Σ(price * dosage * (1 + loss_rate)) BigDecimal basePrice = BigDecimal.ZERO; BigDecimal laborCost = BigDecimal.ZERO; BigDecimal materialCost = BigDecimal.ZERO; BigDecimal machineCost = BigDecimal.ZERO; - + for (QuotaResourceDO resource : resources) { BigDecimal price = resource.getPrice(); if (price == null) { log.warn("[calculateBasePrice] 工料机价格为空,跳过,resourceId={}", resource.getResourceItemId()); continue; } - + // 计算实际消耗量(含损耗) BigDecimal actualDosage = resource.getActualDosage(); - + // 计算金额 BigDecimal amount = price.multiply(actualDosage); basePrice = basePrice.add(amount); - + // 根据资源类型累加费用 - String resourceType = resource.getAttributes() != null ? - (String) resource.getAttributes().get("resource_type") : null; + String resourceType = resource.getAttributes() != null ? + (String) resource.getAttributes().get("resource_type") : null; if ("labor".equals(resourceType)) { laborCost = laborCost.add(amount); } else if ("material".equals(resourceType)) { @@ -178,40 +241,40 @@ public class QuotaItemServiceImpl implements QuotaItemService { machineCost = machineCost.add(amount); } } - - // 更新定额子目 + + // 更新定额基价 quotaItem.setBasePrice(basePrice); quotaItem.setLaborCost(laborCost); quotaItem.setMaterialCost(materialCost); quotaItem.setMachineCost(machineCost); - + quotaItemMapper.updateById(quotaItem); - + log.info("[calculateBasePrice] 计算基价成功,quotaItemId={}, basePrice={}", id, basePrice); } @Override public Long getCategoryTreeIdByQuotaItem(Long quotaItemId) { - // 1. 获取定额子目 + // 1. 获取定额基价 QuotaItemDO quotaItem = validateExists(quotaItemId); - - // 2. 获取定额子目树节点(第二层) + + // 2. 获取定额基价树节点(第二层) com.yhy.module.core.dal.dataobject.quota.QuotaCatalogTreeDO catalogTreeNode = quotaCatalogTreeMapper.selectById(quotaItem.getCatalogItemId()); if (catalogTreeNode == null) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.QUOTA_CATALOG_TREE_NOT_EXISTS); } - + // 3. 获取定额专业节点(第一层) QuotaCatalogItemDO specialtyNode = quotaCatalogItemMapper.selectById(catalogTreeNode.getCatalogItemId()); if (specialtyNode == null) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.QUOTA_CATALOG_ITEM_NOT_EXISTS); } - + // 4. 返回绑定的工料机专业ID if (specialtyNode.getCategoryTreeId() == null) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.QUOTA_SPECIALTY_NOT_BOUND); } - + return specialtyNode.getCategoryTreeId(); } @@ -230,12 +293,12 @@ public class QuotaItemServiceImpl implements QuotaItemService { if (catalogTree == null) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.QUOTA_CATALOG_TREE_NOT_EXISTS); } - + // 验证是内容节点 if (!"content".equals(catalogTree.getContentType())) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.QUOTA_CATALOG_ITEM_NOT_CONTENT); } - + return catalogTree; } @@ -247,7 +310,7 @@ public class QuotaItemServiceImpl implements QuotaItemService { if ("specialty".equals(node.getNodeType())) { return node; } - + // 如果有父节点,继续向上查找 if (node.getParentId() != null) { QuotaCatalogItemDO parent = quotaCatalogItemMapper.selectById(node.getParentId()); @@ -255,7 +318,7 @@ public class QuotaItemServiceImpl implements QuotaItemService { return findSpecialtyNode(parent); } } - + // 尝试从 path 数组中查找 if (node.getPath() != null && node.getPath().length > 0) { for (String pathId : node.getPath()) { @@ -265,13 +328,15 @@ public class QuotaItemServiceImpl implements QuotaItemService { } } } - + throw ServiceExceptionUtil.exception(ErrorCodeConstants.QUOTA_SPECIALTY_NOT_FOUND); } private QuotaItemDO convertToDO(QuotaItemSaveReqVO vo) { QuotaItemDO quotaItem = new QuotaItemDO(); quotaItem.setCatalogItemId(vo.getCatalogItemId()); + quotaItem.setCode(vo.getCode()); + quotaItem.setName(vo.getName()); quotaItem.setUnit(vo.getUnit()); quotaItem.setBasePrice(vo.getBasePrice()); quotaItem.setAmount(vo.getAmount()); @@ -283,20 +348,196 @@ public class QuotaItemServiceImpl implements QuotaItemService { QuotaItemRespVO vo = new QuotaItemRespVO(); vo.setId(quotaItem.getId()); vo.setCatalogItemId(quotaItem.getCatalogItemId()); + vo.setCode(quotaItem.getCode()); + vo.setName(quotaItem.getName()); vo.setUnit(quotaItem.getUnit()); vo.setBasePrice(quotaItem.getBasePrice()); vo.setAmount(quotaItem.getAmount()); vo.setAttributes(quotaItem.getAttributes()); vo.setCreateTime(quotaItem.getCreateTime()); vo.setUpdateTime(quotaItem.getUpdateTime()); - + // 扩展字段 vo.setLaborCost(quotaItem.getLaborCost()); vo.setMaterialCost(quotaItem.getMaterialCost()); vo.setMachineCost(quotaItem.getMachineCost()); vo.setVersionNo(quotaItem.getVersionNo()); vo.setRemark(quotaItem.getRemark()); - + + // 四个价格字段改为虚拟字段,由 calculateQuotaItemPrices 方法动态计算 + // 这里先设置为 null,后续在 getQuotaItemList 和 getQuotaItemDetail 中计算 + vo.setTaxExclBasePrice(null); + vo.setTaxInclBasePrice(null); + vo.setTaxExclCompilePrice(null); + vo.setTaxInclCompilePrice(null); + return vo; } + + /** + * 计算定额基价的四个价格虚拟字段 + * 数据来源于子目工料机的合价总和 + */ + private void calculateQuotaItemPrices(QuotaItemRespVO vo) { + if (vo == null || vo.getId() == null) { + return; + } + + // 查询该定额基价的所有工料机组成 + List resources = quotaResourceService.getQuotaResourceList(vo.getId()); + + // 计算四个价格字段的总和 + BigDecimal taxExclBaseTotal = BigDecimal.ZERO; + BigDecimal taxInclBaseTotal = BigDecimal.ZERO; + BigDecimal taxExclCompileTotal = BigDecimal.ZERO; + BigDecimal taxInclCompileTotal = BigDecimal.ZERO; + + for (QuotaResourceRespVO resource : resources) { + if (resource.getTaxExclBaseTotalSum() != null) { + taxExclBaseTotal = taxExclBaseTotal.add(resource.getTaxExclBaseTotalSum()); + } + if (resource.getTaxInclBaseTotalSum() != null) { + taxInclBaseTotal = taxInclBaseTotal.add(resource.getTaxInclBaseTotalSum()); + } + if (resource.getTaxExclCompileTotalSum() != null) { + taxExclCompileTotal = taxExclCompileTotal.add(resource.getTaxExclCompileTotalSum()); + } + if (resource.getTaxInclCompileTotalSum() != null) { + taxInclCompileTotal = taxInclCompileTotal.add(resource.getTaxInclCompileTotalSum()); + } + } + + // 设置虚拟字段 + vo.setTaxExclBasePrice(taxExclBaseTotal); + vo.setTaxInclBasePrice(taxInclBaseTotal); + vo.setTaxExclCompilePrice(taxExclCompileTotal); + vo.setTaxInclCompilePrice(taxInclCompileTotal); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @Deprecated + public void updateQuotaItemPrices(Long quotaItemId, BigDecimal taxExclBasePrice, + BigDecimal taxInclBasePrice, BigDecimal taxExclCompilePrice, BigDecimal taxInclCompilePrice) { + // 注意:四个价格字段现在是虚拟字段,由 calculateQuotaItemPrices 方法在查询时动态计算 + // 此方法已废弃,不再更新数据库 + log.warn("[updateQuotaItemPrices] 此方法已废弃,四个价格字段现在是虚拟字段,quotaItemId={}", quotaItemId); + } + + @Override + public List getQuotaItemByCode(String code) { + // 根据编码查询定额基价(编码存储在 attributes->code 中) + List items = quotaItemMapper.selectByCode(code); + + return items.stream() + .map(this::convertToVO) + .collect(Collectors.toList()); + } + + @Override + public Long getRateModeIdByQuotaItem(Long quotaItemId) { + // 1. 获取定额基价 + QuotaItemDO quotaItem = validateExists(quotaItemId); + + // 2. 获取定额基价树节点(第二层) + QuotaCatalogTreeDO catalogTreeNode = quotaCatalogTreeMapper.selectById(quotaItem.getCatalogItemId()); + if (catalogTreeNode == null) { + log.warn("[getRateModeIdByQuotaItem] 定额基价树节点不存在, quotaItemId={}, catalogItemId={}", + quotaItemId, quotaItem.getCatalogItemId()); + return null; + } + + // 3. 获取定额专业节点(第一层) + QuotaCatalogItemDO specialtyNode = quotaCatalogItemMapper.selectById(catalogTreeNode.getCatalogItemId()); + if (specialtyNode == null) { + log.warn("[getRateModeIdByQuotaItem] 定额专业节点不存在, quotaItemId={}, catalogItemId={}", + quotaItemId, catalogTreeNode.getCatalogItemId()); + return null; + } + + // 4. 查找该专业节点下的费率模式子节点 + List children = quotaCatalogItemMapper.selectListByParentId(specialtyNode.getId()); + for (QuotaCatalogItemDO child : children) { + if ("rate_mode".equals(child.getNodeType())) { + return child.getId(); + } + } + + // 5. 如果没有找到费率模式节点,返回null + log.warn("[getRateModeIdByQuotaItem] 未找到费率模式节点, quotaItemId={}, specialtyNodeId={}", + quotaItemId, specialtyNode.getId()); + return null; + } + + @Override + public Long getSpecialtyIdByQuotaItem(Long quotaItemId) { + if (quotaItemId == null) { + return null; + } + + // 1. 获取定额基价 + QuotaItemDO quotaItem = quotaItemMapper.selectById(quotaItemId); + if (quotaItem == null) { + log.warn("[getSpecialtyIdByQuotaItem] 定额基价不存在, quotaItemId={}", quotaItemId); + return null; + } + + // 2. 获取定额基价树节点(第二层) + QuotaCatalogTreeDO catalogTreeNode = quotaCatalogTreeMapper.selectById(quotaItem.getCatalogItemId()); + if (catalogTreeNode == null) { + log.warn("[getSpecialtyIdByQuotaItem] 定额基价树节点不存在, quotaItemId={}, catalogItemId={}", + quotaItemId, quotaItem.getCatalogItemId()); + return null; + } + + // 3. 获取定额专业节点(第一层) + QuotaCatalogItemDO specialtyNode = quotaCatalogItemMapper.selectById(catalogTreeNode.getCatalogItemId()); + if (specialtyNode == null) { + log.warn("[getSpecialtyIdByQuotaItem] 定额专业节点不存在, quotaItemId={}, catalogItemId={}", + quotaItemId, catalogTreeNode.getCatalogItemId()); + return null; + } + + // 4. 如果当前节点不是专业节点,向上追溯 + if (!"specialty".equals(specialtyNode.getNodeType())) { + return findSpecialtyNodeId(specialtyNode); + } + + return specialtyNode.getId(); + } + + /** + * 向上追溯到定额专业节点 + */ + private Long findSpecialtyNodeId(QuotaCatalogItemDO node) { + if (node == null) { + return null; + } + + // 如果当前节点就是定额专业节点 + if ("specialty".equals(node.getNodeType())) { + return node.getId(); + } + + // 如果有父节点,继续向上查找 + if (node.getParentId() != null) { + QuotaCatalogItemDO parent = quotaCatalogItemMapper.selectById(node.getParentId()); + if (parent != null) { + return findSpecialtyNodeId(parent); + } + } + + // 尝试从 path 数组中查找 + if (node.getPath() != null && node.getPath().length > 0) { + for (String pathId : node.getPath()) { + QuotaCatalogItemDO pathNode = quotaCatalogItemMapper.selectById(Long.parseLong(pathId)); + if (pathNode != null && "specialty".equals(pathNode.getNodeType())) { + return pathNode.getId(); + } + } + } + + log.warn("[findSpecialtyNodeId] 未找到定额专业节点, nodeId={}", node.getId()); + return null; + } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaMarketMaterialServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaMarketMaterialServiceImpl.java new file mode 100644 index 0000000..7a487e6 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaMarketMaterialServiceImpl.java @@ -0,0 +1,502 @@ +package com.yhy.module.core.service.quota.impl; + +import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; +import com.yhy.module.core.controller.admin.quota.vo.QuotaMarketMaterialRespVO; +import com.yhy.module.core.controller.admin.quota.vo.QuotaMarketMaterialSaveReqVO; +import com.yhy.module.core.controller.admin.resource.vo.ResourceItemRespVO; +import com.yhy.module.core.dal.dataobject.quota.QuotaMarketMaterialDO; +import com.yhy.module.core.dal.dataobject.resource.ResourceItemDO; +import com.yhy.module.core.dal.dataobject.resource.ResourceMergedDO; +import com.yhy.module.core.dal.mysql.quota.QuotaMarketMaterialMapper; +import com.yhy.module.core.dal.mysql.resource.ResourceCatalogItemMapper; +import com.yhy.module.core.dal.mysql.resource.ResourceItemMapper; +import com.yhy.module.core.dal.mysql.resource.ResourceMergedMapper; +import com.yhy.module.core.enums.ErrorCodeConstants; +import com.yhy.module.core.service.quota.QuotaItemService; +import com.yhy.module.core.service.quota.QuotaMarketMaterialService; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * 定额市场主材设备 Service 实现类 + * + * @author yhy + */ +@Service +@Validated +@Slf4j +public class QuotaMarketMaterialServiceImpl implements QuotaMarketMaterialService { + + @Resource + private QuotaMarketMaterialMapper quotaMarketMaterialMapper; + + @Resource + private ResourceItemMapper resourceItemMapper; + + @Resource + private ResourceCatalogItemMapper resourceCatalogItemMapper; + + @Resource + private ResourceMergedMapper resourceMergedMapper; + + @Resource + @Lazy + private QuotaItemService quotaItemService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createMarketMaterial(QuotaMarketMaterialSaveReqVO createReqVO) { + // 1. 验证工料机是否在范围内 + validateResourceInScope(createReqVO.getQuotaItemId(), createReqVO.getResourceItemId()); + + // 2. 验证工料机是否存在 + ResourceItemDO resourceItem = resourceItemMapper.selectById(createReqVO.getResourceItemId()); + if (resourceItem == null) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.RESOURCE_ITEM_NOT_EXISTS); + } + + // 3. 定额消耗量默认为0 + if (createReqVO.getDosage() == null) { + createReqVO.setDosage(java.math.BigDecimal.ZERO); + } + + // 4. 转换为 DO + QuotaMarketMaterialDO marketMaterial = convertToDO(createReqVO); + + // 4. 处理 sortOrder + if (marketMaterial.getSortOrder() == null) { + Integer maxSortOrder = getMaxSortOrder(createReqVO.getQuotaItemId()); + marketMaterial.setSortOrder(maxSortOrder + 1); + } else { + shiftSortOrdersFrom(createReqVO.getQuotaItemId(), marketMaterial.getSortOrder()); + } + + // 5. 插入 + quotaMarketMaterialMapper.insert(marketMaterial); + + log.info("[createMarketMaterial] 添加定额市场主材设备成功,quotaItemId={}, resourceItemId={}", + createReqVO.getQuotaItemId(), createReqVO.getResourceItemId()); + + return marketMaterial.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateMarketMaterial(QuotaMarketMaterialSaveReqVO updateReqVO) { + // 校验存在 + validateExists(updateReqVO.getId()); + + // 验证工料机是否在范围内(如果修改了 resourceItemId) + QuotaMarketMaterialDO existing = quotaMarketMaterialMapper.selectById(updateReqVO.getId()); + if (!existing.getResourceItemId().equals(updateReqVO.getResourceItemId())) { + validateResourceInScope(updateReqVO.getQuotaItemId(), updateReqVO.getResourceItemId()); + + ResourceItemDO resourceItem = resourceItemMapper.selectById(updateReqVO.getResourceItemId()); + if (resourceItem == null) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.RESOURCE_ITEM_NOT_EXISTS); + } + } + + // 校验:单位为%的工料机不允许修改定额消耗量 + ResourceItemDO resourceItem = resourceItemMapper.selectById(existing.getResourceItemId()); + if (resourceItem != null && "%".equals(resourceItem.getUnit())) { + if (updateReqVO.getDosage() != null && existing.getDosage() != null + && updateReqVO.getDosage().compareTo(existing.getDosage()) != 0) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.QUOTA_RESOURCE_PERCENT_UNIT_DOSAGE_READONLY); + } + } + + // 转换为 DO + QuotaMarketMaterialDO updateObj = convertToDO(updateReqVO); + updateObj.setId(updateReqVO.getId()); + + // 合并 attributes + if (existing.getAttributes() != null) { + java.util.Map mergedAttributes = new java.util.HashMap<>(existing.getAttributes()); + if (updateReqVO.getAttributes() != null) { + mergedAttributes.putAll(updateReqVO.getAttributes()); + } + updateObj.setAttributes(mergedAttributes); + } + + quotaMarketMaterialMapper.updateById(updateObj); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteMarketMaterial(Long id) { + validateExists(id); + quotaMarketMaterialMapper.deleteById(id); + } + + @Override + public QuotaMarketMaterialDO getMarketMaterial(Long id) { + return quotaMarketMaterialMapper.selectById(id); + } + + @Override + public List getMarketMaterialList(Long quotaItemId) { + List list = quotaMarketMaterialMapper.selectListByQuotaItemId(quotaItemId); + List result = list.stream().map(this::convertToVO).collect(Collectors.toList()); + + // 格式化数值 + result.forEach(this::formatVO); + + return result; + } + + @Override + public List getAvailableResourceItems(Long quotaItemId) { + Long categoryTreeId = quotaItemService.getCategoryTreeIdByQuotaItem(quotaItemId); + List resourceItems = resourceItemMapper.selectByCategoryTreeId(categoryTreeId); + return resourceItems.stream() + .map(this::convertResourceItemToVO) + .collect(Collectors.toList()); + } + + @Override + public List getAvailableResourceItemsWithFilter(Long quotaItemId, String code, String name, String spec) { + Long categoryTreeId = quotaItemService.getCategoryTreeIdByQuotaItem(quotaItemId); + List resourceItems = resourceItemMapper.selectByCategoryTreeIdWithFilter( + categoryTreeId, code, name, spec); + return resourceItems.stream() + .map(this::convertResourceItemToVO) + .collect(Collectors.toList()); + } + + @Override + public ResourceItemRespVO getResourceItemByCode(Long quotaItemId, String code) { + Long categoryTreeId = quotaItemService.getCategoryTreeIdByQuotaItem(quotaItemId); + ResourceItemDO resourceItem = resourceItemMapper.selectByCategoryTreeIdAndCode(categoryTreeId, code); + if (resourceItem == null) { + return null; + } + return convertResourceItemToVO(resourceItem); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void batchCreateMarketMaterials(Long quotaItemId, List materials) { + for (QuotaMarketMaterialSaveReqVO material : materials) { + material.setQuotaItemId(quotaItemId); + createMarketMaterial(material); + } + } + + // ========== 私有方法 ========== + + private void validateResourceInScope(Long quotaItemId, Long resourceItemId) { + Long allowedCategoryTreeId = quotaItemService.getCategoryTreeIdByQuotaItem(quotaItemId); + + ResourceItemDO resourceItem = resourceItemMapper.selectById(resourceItemId); + if (resourceItem == null) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.RESOURCE_ITEM_NOT_EXISTS); + } + + com.yhy.module.core.dal.dataobject.resource.ResourceCatalogItemDO catalogItem = + resourceCatalogItemMapper.selectById(resourceItem.getCatalogItemId()); + if (catalogItem == null) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.RESOURCE_CATALOG_ITEM_NOT_EXISTS); + } + + if (!allowedCategoryTreeId.equals(catalogItem.getCategoryTreeId())) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.QUOTA_RESOURCE_OUT_OF_SCOPE); + } + } + + private QuotaMarketMaterialDO validateExists(Long id) { + QuotaMarketMaterialDO marketMaterial = quotaMarketMaterialMapper.selectById(id); + if (marketMaterial == null) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.QUOTA_MARKET_MATERIAL_NOT_EXISTS); + } + return marketMaterial; + } + + private QuotaMarketMaterialDO convertToDO(QuotaMarketMaterialSaveReqVO vo) { + QuotaMarketMaterialDO marketMaterial = new QuotaMarketMaterialDO(); + marketMaterial.setQuotaItemId(vo.getQuotaItemId()); + marketMaterial.setResourceItemId(vo.getResourceItemId()); + marketMaterial.setDosage(vo.getDosage()); + marketMaterial.setAdjustedDosage(vo.getAdjustedDosage()); + marketMaterial.setAttributes(vo.getAttributes()); + marketMaterial.setSortOrder(vo.getSortOrder()); + return marketMaterial; + } + + private Integer getMaxSortOrder(Long quotaItemId) { + List materials = quotaMarketMaterialMapper.selectListByQuotaItemId(quotaItemId); + if (materials == null || materials.isEmpty()) { + return 0; + } + return materials.stream() + .map(QuotaMarketMaterialDO::getSortOrder) + .filter(java.util.Objects::nonNull) + .max(Integer::compareTo) + .orElse(0); + } + + private void shiftSortOrdersFrom(Long quotaItemId, Integer fromSortOrder) { + List materials = quotaMarketMaterialMapper.selectListByQuotaItemId(quotaItemId); + for (QuotaMarketMaterialDO material : materials) { + if (material.getSortOrder() != null && material.getSortOrder() >= fromSortOrder) { + material.setSortOrder(material.getSortOrder() + 1); + quotaMarketMaterialMapper.updateById(material); + } + } + } + + private QuotaMarketMaterialRespVO convertToVO(QuotaMarketMaterialDO marketMaterial) { + QuotaMarketMaterialRespVO vo = new QuotaMarketMaterialRespVO(); + vo.setId(marketMaterial.getId()); + vo.setQuotaItemId(marketMaterial.getQuotaItemId()); + vo.setResourceItemId(marketMaterial.getResourceItemId()); + vo.setDosage(marketMaterial.getDosage()); + vo.setAdjustedDosage(marketMaterial.getAdjustedDosage()); + vo.setAttributes(marketMaterial.getAttributes()); + vo.setCreateTime(marketMaterial.getCreateTime()); + vo.setUpdateTime(marketMaterial.getUpdateTime()); + + // 扩展字段 + vo.setLossRate(marketMaterial.getLossRate()); + + // 实时查询工料机信息 + ResourceItemDO resourceItem = resourceItemMapper.selectById(marketMaterial.getResourceItemId()); + if (resourceItem != null) { + vo.setResourceCode(resourceItem.getCode()); + vo.setResourceName(resourceItem.getName()); + vo.setResourceUnit(resourceItem.getUnit()); + vo.setResourceSpec(resourceItem.getSpec()); + vo.setResourceCategoryId(resourceItem.getCategoryId()); + vo.setResourceType(convertTypeToDisplay(resourceItem.getType())); + vo.setCalcBase(resourceItem.getCalcBase()); + + boolean isMerged = resourceItem.getIsMerged() != null && resourceItem.getIsMerged() == 1; + vo.setIsMerged(isMerged); + + if (isMerged) { + vo.setResourceTaxRate(null); + vo.setResourceTaxExclBasePrice(null); + vo.setResourceTaxInclBasePrice(null); + vo.setResourceTaxExclCompilePrice(null); + vo.setResourceTaxInclCompilePrice(null); + vo.setPrice(null); + + // 查询复合工料机子数据 + List mergedList = resourceMergedMapper.selectByMergedId(resourceItem.getId()); + if (mergedList != null && !mergedList.isEmpty()) { + List mergedItems = new ArrayList<>(); + for (ResourceMergedDO merged : mergedList) { + QuotaMarketMaterialRespVO.MergedResourceItemVO item = convertMergedToVO(merged, marketMaterial.getDosage()); + if (item != null) { + mergedItems.add(item); + } + } + vo.setMergedItems(mergedItems); + + // 计算复合工料机合价 + calculateMergedTotalSums(vo, mergedItems); + } + } else { + vo.setResourceTaxRate(resourceItem.getTaxRate()); + vo.setResourceTaxExclBasePrice(resourceItem.getTaxExclBasePrice()); + vo.setResourceTaxInclBasePrice(resourceItem.getTaxInclBasePrice()); + vo.setResourceTaxExclCompilePrice(resourceItem.getTaxExclCompilePrice()); + vo.setResourceTaxInclCompilePrice(resourceItem.getTaxInclCompilePrice()); + + if (resourceItem.getTaxExclBasePrice() != null) { + vo.setPrice(resourceItem.getTaxExclBasePrice()); + } else if (resourceItem.getTaxInclBasePrice() != null) { + vo.setPrice(resourceItem.getTaxInclBasePrice()); + } + + // 计算合价 + BigDecimal effectiveDosage = marketMaterial.getDosage(); + if (effectiveDosage != null) { + vo.setTaxExclBaseTotalSum(multiplyOrZero(resourceItem.getTaxExclBasePrice(), effectiveDosage)); + vo.setTaxInclBaseTotalSum(multiplyOrZero(resourceItem.getTaxInclBasePrice(), effectiveDosage)); + vo.setTaxExclCompileTotalSum(multiplyOrZero(resourceItem.getTaxExclCompilePrice(), effectiveDosage)); + vo.setTaxInclCompileTotalSum(multiplyOrZero(resourceItem.getTaxInclCompilePrice(), effectiveDosage)); + } + } + + // 计算实际消耗量和金额 + vo.setActualDosage(marketMaterial.getActualDosage()); + if (vo.getPrice() != null && vo.getActualDosage() != null) { + vo.setAmount(vo.getActualDosage().multiply(vo.getPrice())); + } + } + + return vo; + } + + private QuotaMarketMaterialRespVO.MergedResourceItemVO convertMergedToVO(ResourceMergedDO merged, BigDecimal parentDosage) { + if (merged.getSourceId() == null) { + return null; + } + + ResourceItemDO sourceItem = resourceItemMapper.selectById(merged.getSourceId()); + if (sourceItem == null) { + return null; + } + + QuotaMarketMaterialRespVO.MergedResourceItemVO item = new QuotaMarketMaterialRespVO.MergedResourceItemVO(); + item.setId(merged.getId()); + item.setResourceItemId(merged.getSourceId()); + item.setResourceCode(sourceItem.getCode()); + item.setResourceName(sourceItem.getName()); + item.setResourceUnit(sourceItem.getUnit()); + item.setResourceSpec(sourceItem.getSpec()); + item.setResourceCategoryId(sourceItem.getCategoryId()); + item.setResourceType(convertTypeToDisplay(sourceItem.getType())); + item.setResourceTaxRate(sourceItem.getTaxRate()); + item.setResourceTaxExclBasePrice(sourceItem.getTaxExclBasePrice()); + item.setResourceTaxInclBasePrice(sourceItem.getTaxInclBasePrice()); + item.setResourceTaxExclCompilePrice(sourceItem.getTaxExclCompilePrice()); + item.setResourceTaxInclCompilePrice(sourceItem.getTaxInclCompilePrice()); + item.setCalcBase(sourceItem.getCalcBase()); + + // 子项定额消耗量 = 原定额消耗量 × 父定额消耗量 + BigDecimal childDosage = merged.getQuotaConsumption(); + if (childDosage != null && parentDosage != null) { + item.setDosage(childDosage.multiply(parentDosage).setScale(6, RoundingMode.HALF_UP)); + } else { + item.setDosage(childDosage); + } + item.setPrice(merged.getTaxExclMarketPrice()); + + // 计算子项合价 + BigDecimal factor = item.getDosage(); + if (factor != null) { + item.setTaxExclBaseTotalSum(multiplyOrZero(sourceItem.getTaxExclBasePrice(), factor)); + item.setTaxInclBaseTotalSum(multiplyOrZero(sourceItem.getTaxInclBasePrice(), factor)); + item.setTaxExclCompileTotalSum(multiplyOrZero(sourceItem.getTaxExclCompilePrice(), factor)); + item.setTaxInclCompileTotalSum(multiplyOrZero(sourceItem.getTaxInclCompilePrice(), factor)); + } + + return item; + } + + private void calculateMergedTotalSums(QuotaMarketMaterialRespVO vo, List mergedItems) { + BigDecimal taxExclBaseSum = BigDecimal.ZERO; + BigDecimal taxInclBaseSum = BigDecimal.ZERO; + BigDecimal taxExclCompileSum = BigDecimal.ZERO; + BigDecimal taxInclCompileSum = BigDecimal.ZERO; + + for (QuotaMarketMaterialRespVO.MergedResourceItemVO item : mergedItems) { + if (item.getTaxExclBaseTotalSum() != null) { + taxExclBaseSum = taxExclBaseSum.add(item.getTaxExclBaseTotalSum()); + } + if (item.getTaxInclBaseTotalSum() != null) { + taxInclBaseSum = taxInclBaseSum.add(item.getTaxInclBaseTotalSum()); + } + if (item.getTaxExclCompileTotalSum() != null) { + taxExclCompileSum = taxExclCompileSum.add(item.getTaxExclCompileTotalSum()); + } + if (item.getTaxInclCompileTotalSum() != null) { + taxInclCompileSum = taxInclCompileSum.add(item.getTaxInclCompileTotalSum()); + } + } + + vo.setTaxExclBaseTotalSum(taxExclBaseSum); + vo.setTaxInclBaseTotalSum(taxInclBaseSum); + vo.setTaxExclCompileTotalSum(taxExclCompileSum); + vo.setTaxInclCompileTotalSum(taxInclCompileSum); + } + + private void formatVO(QuotaMarketMaterialRespVO vo) { + // 价格/税率/合价:2位小数 + vo.setResourceTaxRate(roundToScale(vo.getResourceTaxRate(), 2)); + vo.setResourceTaxExclBasePrice(roundToScale(vo.getResourceTaxExclBasePrice(), 2)); + vo.setResourceTaxInclBasePrice(roundToScale(vo.getResourceTaxInclBasePrice(), 2)); + vo.setResourceTaxExclCompilePrice(roundToScale(vo.getResourceTaxExclCompilePrice(), 2)); + vo.setResourceTaxInclCompilePrice(roundToScale(vo.getResourceTaxInclCompilePrice(), 2)); + vo.setTaxExclBaseTotalSum(roundToScale(vo.getTaxExclBaseTotalSum(), 2)); + vo.setTaxInclBaseTotalSum(roundToScale(vo.getTaxInclBaseTotalSum(), 2)); + vo.setTaxExclCompileTotalSum(roundToScale(vo.getTaxExclCompileTotalSum(), 2)); + vo.setTaxInclCompileTotalSum(roundToScale(vo.getTaxInclCompileTotalSum(), 2)); + vo.setPrice(roundToScale(vo.getPrice(), 2)); + vo.setAmount(roundToScale(vo.getAmount(), 2)); + + // 消耗量:4位小数 + vo.setDosage(roundToScale(vo.getDosage(), 4)); + vo.setAdjustedDosage(roundToScale(vo.getAdjustedDosage(), 4)); + vo.setActualDosage(roundToScale(vo.getActualDosage(), 4)); + + // 格式化子工料机 + if (vo.getMergedItems() != null) { + for (QuotaMarketMaterialRespVO.MergedResourceItemVO item : vo.getMergedItems()) { + item.setResourceTaxRate(roundToScale(item.getResourceTaxRate(), 2)); + item.setResourceTaxExclBasePrice(roundToScale(item.getResourceTaxExclBasePrice(), 2)); + item.setResourceTaxInclBasePrice(roundToScale(item.getResourceTaxInclBasePrice(), 2)); + item.setResourceTaxExclCompilePrice(roundToScale(item.getResourceTaxExclCompilePrice(), 2)); + item.setResourceTaxInclCompilePrice(roundToScale(item.getResourceTaxInclCompilePrice(), 2)); + item.setTaxExclBaseTotalSum(roundToScale(item.getTaxExclBaseTotalSum(), 2)); + item.setTaxInclBaseTotalSum(roundToScale(item.getTaxInclBaseTotalSum(), 2)); + item.setTaxExclCompileTotalSum(roundToScale(item.getTaxExclCompileTotalSum(), 2)); + item.setTaxInclCompileTotalSum(roundToScale(item.getTaxInclCompileTotalSum(), 2)); + item.setPrice(roundToScale(item.getPrice(), 2)); + item.setAmount(roundToScale(item.getAmount(), 2)); + item.setDosage(roundToScale(item.getDosage(), 4)); + item.setActualDosage(roundToScale(item.getActualDosage(), 4)); + } + } + } + + private BigDecimal roundToScale(BigDecimal value, int scale) { + if (value == null) { + return null; + } + return value.setScale(scale, RoundingMode.HALF_UP); + } + + private BigDecimal multiplyOrZero(BigDecimal a, BigDecimal b) { + if (a == null || b == null) { + return BigDecimal.ZERO; + } + return a.multiply(b); + } + + private String convertTypeToDisplay(String type) { + if (type == null) { + return null; + } + switch (type) { + case "1": + return "labor"; + case "2": + return "material"; + case "3": + return "machine"; + default: + return type; + } + } + + private ResourceItemRespVO convertResourceItemToVO(ResourceItemDO resourceItem) { + ResourceItemRespVO vo = new ResourceItemRespVO(); + vo.setId(resourceItem.getId()); + vo.setCode(resourceItem.getCode()); + vo.setName(resourceItem.getName()); + vo.setUnit(resourceItem.getUnit()); + vo.setSpec(resourceItem.getSpec()); + vo.setType(resourceItem.getType()); + vo.setCategoryId(resourceItem.getCategoryId()); + vo.setTaxRate(resourceItem.getTaxRate()); + vo.setTaxExclBasePrice(resourceItem.getTaxExclBasePrice()); + vo.setTaxInclBasePrice(resourceItem.getTaxInclBasePrice()); + vo.setTaxExclCompilePrice(resourceItem.getTaxExclCompilePrice()); + vo.setTaxInclCompilePrice(resourceItem.getTaxInclCompilePrice()); + vo.setIsMerged(resourceItem.getIsMerged()); + vo.setCalcBase(resourceItem.getCalcBase()); + return vo; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaRateFieldServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaRateFieldServiceImpl.java index 29f9860..dfcd5be 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaRateFieldServiceImpl.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaRateFieldServiceImpl.java @@ -248,9 +248,9 @@ public class QuotaRateFieldServiceImpl implements QuotaRateFieldService { } // 2. 验证每个绑定节点是否属于同一定额专业 - // 注意:bindingIds 是定额子目树(yhy_quota_catalog_tree)的ID,不是定额专业树的ID + // 注意:bindingIds 是定额基价树(yhy_quota_catalog_tree)的ID,不是定额专业树的ID for (Long bindingId : bindingIds) { - // 查询定额子目树节点 + // 查询定额基价树节点 QuotaCatalogTreeDO treeNode = catalogTreeMapper.selectById(bindingId); if (treeNode == null) { throw exception(QUOTA_CATALOG_ITEM_NOT_EXISTS); diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaRateItemServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaRateItemServiceImpl.java index 6accfd8..b8fcd11 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaRateItemServiceImpl.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaRateItemServiceImpl.java @@ -23,6 +23,7 @@ import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_RATE_TIER_COMPA import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_RATE_TIER_FIELD_VALUES_EMPTY; import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_RATE_TIER_SEQ_DUPLICATE; import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_RATE_TIER_THRESHOLD_INVALID; +import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_RATE_TIER_PER_INCREMENT_ONLY_ONE; import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_RATE_TIER_THRESHOLD_NOT_ASCENDING; import cn.hutool.core.collection.CollUtil; @@ -122,10 +123,35 @@ public class QuotaRateItemServiceImpl implements QuotaRateItemService { throw exception(QUOTA_RATE_ITEM_LEVEL_EXCEED); } - // 3. 设置排序值 + // 3. 设置排序值(支持基于参考节点的插入位置) Integer sortOrder = createReqVO.getSortOrder(); if (sortOrder == null) { - sortOrder = rateItemMapper.selectMaxSortOrder(createReqVO.getCatalogItemId(), createReqVO.getParentId()) + 1; + Long referenceNodeId = createReqVO.getReferenceNodeId(); + String insertPosition = createReqVO.getInsertPosition(); + + if (referenceNodeId != null && insertPosition != null) { + // 基于参考节点计算排序值 + QuotaRateItemDO referenceNode = rateItemMapper.selectById(referenceNodeId); + if (referenceNode != null) { + int refSortOrder = referenceNode.getSortOrder(); + if ("above".equals(insertPosition)) { + // 在参考节点上方插入:使用参考节点的排序值,并将参考节点及其后的节点排序值+1 + sortOrder = refSortOrder; + rateItemMapper.incrementSortOrderFrom(createReqVO.getCatalogItemId(), createReqVO.getParentId(), refSortOrder); + } else if ("below".equals(insertPosition)) { + // 在参考节点下方插入:使用参考节点排序值+1,并将后续节点排序值+1 + sortOrder = refSortOrder + 1; + rateItemMapper.incrementSortOrderFrom(createReqVO.getCatalogItemId(), createReqVO.getParentId(), refSortOrder + 1); + } else { + // 默认追加到末尾 + sortOrder = rateItemMapper.selectMaxSortOrder(createReqVO.getCatalogItemId(), createReqVO.getParentId()) + 1; + } + } else { + sortOrder = rateItemMapper.selectMaxSortOrder(createReqVO.getCatalogItemId(), createReqVO.getParentId()) + 1; + } + } else { + sortOrder = rateItemMapper.selectMaxSortOrder(createReqVO.getCatalogItemId(), createReqVO.getParentId()) + 1; + } } // 4. 创建费率项 @@ -236,8 +262,20 @@ public class QuotaRateItemServiceImpl implements QuotaRateItemService { // 2. 查询字段绑定配置(基于catalog_item_id,所有费率项共享) List allFieldConfigs = rateFieldMapper.selectListByCatalogItemId(catalogItemId); - // 3. 转换为 VO - List allVOs = allItems.stream().map(item -> { + // 3. 使用公共方法构建结果 + return buildRateItemTree(allItems, allFieldConfigs); + } + + @Override + public List buildRateItemTree( + List rateItems, + List fieldConfigs) { + if (CollUtil.isEmpty(rateItems)) { + return Collections.emptyList(); + } + + // 1. 转换为 VO + List allVOs = rateItems.stream().map(item -> { QuotaRateItemRespVO vo = BeanUtils.toBean(item, QuotaRateItemRespVO.class); // 构建字段列表 @@ -279,8 +317,8 @@ public class QuotaRateItemServiceImpl implements QuotaRateItemService { } // 如果有字段配置,使用配置信息 - if (!allFieldConfigs.isEmpty()) { - fields = allFieldConfigs.stream().map(config -> { + if (fieldConfigs != null && !fieldConfigs.isEmpty()) { + fields = fieldConfigs.stream().map(config -> { QuotaRateFieldRespVO fieldVO = new QuotaRateFieldRespVO(); fieldVO.setId(config.getId()); fieldVO.setFieldIndex(config.getFieldIndex()); @@ -312,7 +350,7 @@ public class QuotaRateItemServiceImpl implements QuotaRateItemService { return vo; }).collect(Collectors.toList()); - // 4. 构建树结构 + // 2. 构建树结构 return buildTree(allVOs); } @@ -456,61 +494,134 @@ public class QuotaRateItemServiceImpl implements QuotaRateItemService { Map result = new HashMap<>(); - // 1. 获取阶梯规则(默认计算方式) + // 1. 获取阶梯规则 List> tiers = (List>) valueRules.get("tiers"); if (tiers == null || tiers.isEmpty()) { return result; } - // 2. 按阈值排序(从小到大) - tiers.sort(Comparator.comparing(t -> new BigDecimal(t.get("threshold").toString()))); - - // 3. 默认使用阶梯规则:找到第一个满足条件的阶梯 - Map matchedTier = null; + // 2. 分离阶梯规则(lte/lt)和每增加规则(per_increment) + List> stepTiers = new ArrayList<>(); + Map perIncrementTier = null; for (Map tier : tiers) { - BigDecimal threshold = new BigDecimal(tier.get("threshold").toString()); String compareType = (String) tier.getOrDefault("compareType", "lte"); - - boolean matched = false; - switch (compareType) { - case "lte": - matched = baseValue.compareTo(threshold) <= 0; - break; - case "lt": - matched = baseValue.compareTo(threshold) < 0; - break; - case "gte": - matched = baseValue.compareTo(threshold) >= 0; - break; - case "gt": - matched = baseValue.compareTo(threshold) > 0; - break; - } - - if (matched) { - matchedTier = tier; - break; + if ("per_increment".equals(compareType)) { + perIncrementTier = tier; + } else { + stepTiers.add(tier); } } - // 4. 如果没有匹配到任何阶梯,使用最后一个阶梯(兜底逻辑) - if (matchedTier == null && !tiers.isEmpty()) { - matchedTier = tiers.get(tiers.size() - 1); - } + // 3. 对阶梯规则按阈值从小到大排序 + stepTiers.sort(Comparator.comparing(t -> new BigDecimal(t.get("threshold").toString()))); - // 5. 获取阶梯对应的字段值(这是默认的计算结果) - if (matchedTier != null) { - Map fieldValues = (Map) matchedTier.get("fieldValues"); - if (fieldValues != null) { - for (Map.Entry entry : fieldValues.entrySet()) { - Integer fieldIndex = Integer.parseInt(entry.getKey()); - BigDecimal value = new BigDecimal(entry.getValue().toString()); - result.put(fieldIndex, value); + // 4. 找到匹配的阶梯区间(低阈值和高阈值) + Map lowerTier = null; + Map upperTier = null; + BigDecimal lowerThreshold = null; + BigDecimal upperThreshold = null; + + if (!stepTiers.isEmpty()) { + BigDecimal minThreshold = new BigDecimal(stepTiers.get(0).get("threshold").toString()); + + // 如果输入值小于最小阈值,使用最小阈值对应的值(如<50按50取) + if (baseValue.compareTo(minThreshold) < 0) { + lowerTier = stepTiers.get(0); + lowerThreshold = minThreshold; + } else { + // 找到输入值所在的阶梯区间 + for (int i = 0; i < stepTiers.size(); i++) { + BigDecimal threshold = new BigDecimal(stepTiers.get(i).get("threshold").toString()); + if (baseValue.compareTo(threshold) >= 0) { + lowerTier = stepTiers.get(i); + lowerThreshold = threshold; + // 检查是否有下一个阶梯(高阈值) + if (i + 1 < stepTiers.size()) { + upperTier = stepTiers.get(i + 1); + upperThreshold = new BigDecimal(stepTiers.get(i + 1).get("threshold").toString()); + } + } } } } - // 6. 增量规则(可选,如果配置了则在阶梯基础上叠加) + // 5. 计算字段值(支持内插计算) + if (lowerTier != null) { + Map lowerFieldValues = (Map) lowerTier.get("fieldValues"); + + // 如果有高阈值且输入值在两个阶梯之间,使用内插计算 + if (upperTier != null && lowerFieldValues != null && baseValue.compareTo(lowerThreshold) > 0) { + Map upperFieldValues = (Map) upperTier.get("fieldValues"); + BigDecimal range = upperThreshold.subtract(lowerThreshold); + + // 防止除零:如果 range 为零,直接使用低阈值的值 + if (upperFieldValues != null && range.compareTo(BigDecimal.ZERO) > 0) { + // 计算内插比例:(输入值 - 低阈值) / (高阈值 - 低阈值) + BigDecimal offset = baseValue.subtract(lowerThreshold); + BigDecimal ratio = offset.divide(range, 10, RoundingMode.HALF_UP); + + for (Map.Entry entry : lowerFieldValues.entrySet()) { + Integer fieldIndex = Integer.parseInt(entry.getKey()); + BigDecimal lowerValue = new BigDecimal(entry.getValue().toString()); + // 获取高阈值对应的字段值 + Object upperValueObj = upperFieldValues.get(entry.getKey()); + if (upperValueObj != null) { + BigDecimal upperValue = new BigDecimal(upperValueObj.toString()); + // 内插计算:低值 + 比例 × (高值 - 低值) + BigDecimal diff = upperValue.subtract(lowerValue); + BigDecimal interpolated = lowerValue.add(ratio.multiply(diff)); + result.put(fieldIndex, interpolated); + } else { + result.put(fieldIndex, lowerValue); + } + } + } else { + // range为零或无高阈值字段值,使用低阈值的值 + for (Map.Entry entry : lowerFieldValues.entrySet()) { + Integer fieldIndex = Integer.parseInt(entry.getKey()); + BigDecimal value = new BigDecimal(entry.getValue().toString()); + result.put(fieldIndex, value); + } + } + } else { + // 精确匹配或小于最小阈值,直接使用低阈值的值 + if (lowerFieldValues != null) { + for (Map.Entry entry : lowerFieldValues.entrySet()) { + Integer fieldIndex = Integer.parseInt(entry.getKey()); + BigDecimal value = new BigDecimal(entry.getValue().toString()); + result.put(fieldIndex, value); + } + } + } + } + + // 6. 如果有每增加规则,且输入值超过最后一个阶梯阈值,计算增量并叠加 + // 公式:最终值 = 基准值 + (输入值 - 基准阈值) / 步长 × 增量值 + BigDecimal lastThreshold = !stepTiers.isEmpty() ? + new BigDecimal(stepTiers.get(stepTiers.size() - 1).get("threshold").toString()) : null; + + if (perIncrementTier != null && lastThreshold != null && baseValue.compareTo(lastThreshold) > 0) { + BigDecimal step = new BigDecimal(perIncrementTier.get("threshold").toString()); + + if (step.compareTo(BigDecimal.ZERO) > 0) { + BigDecimal excess = baseValue.subtract(lastThreshold); + BigDecimal times = excess.divide(step, 0, RoundingMode.DOWN); + + Map incrementValues = (Map) perIncrementTier.get("fieldValues"); + if (incrementValues != null) { + for (Map.Entry entry : incrementValues.entrySet()) { + Integer fieldIndex = Integer.parseInt(entry.getKey()); + BigDecimal incrementValue = new BigDecimal(entry.getValue().toString()); + BigDecimal increment = incrementValue.multiply(times); + // 叠加到基准值上 + BigDecimal baseFieldValue = result.getOrDefault(fieldIndex, BigDecimal.ZERO); + result.put(fieldIndex, baseFieldValue.add(increment)); + } + } + } + } + + // 8. 增量规则(可选,如果配置了则在阶梯基础上叠加) // 注意:增量规则是可选的,不配置也不影响阶梯规则的使用 List> increments = (List>) valueRules.get("increments"); if (increments != null && !increments.isEmpty()) { @@ -581,6 +692,7 @@ public class QuotaRateItemServiceImpl implements QuotaRateItemService { if (!Objects.equals(parent.getCatalogItemId(), catalogItemId)) { throw exception(QUOTA_RATE_ITEM_PARENT_NOT_SAME_MODE); } + // 只有目录节点才能添加子节点 if (!parent.isDirectory()) { throw exception(QUOTA_RATE_ITEM_PARENT_NOT_DIRECTORY); } @@ -654,7 +766,7 @@ public class QuotaRateItemServiceImpl implements QuotaRateItemService { // 验证比较类型 String compareType = current.getCompareType(); if (compareType == null || (!compareType.equals("lte") && !compareType.equals("lt") - && !compareType.equals("gte") && !compareType.equals("gt"))) { + && !compareType.equals("per_increment"))) { throw exception(QUOTA_RATE_TIER_COMPARE_TYPE_INVALID, current.getSeq()); } @@ -662,40 +774,26 @@ public class QuotaRateItemServiceImpl implements QuotaRateItemService { if (current.getFieldValues() == null || current.getFieldValues().isEmpty()) { throw exception(QUOTA_RATE_TIER_FIELD_VALUES_EMPTY, current.getSeq()); } - - // 验证相邻阶梯的阈值关系 - if (i > 0) { - QuotaRateValueRulesReqVO.TierRule previous = sortedTiers.get(i - 1); - - // 如果前一个是 lte/lt,当前应该是 lte/lt,且阈值递增 - if ((previous.getCompareType().equals("lte") || previous.getCompareType().equals("lt")) - && (compareType.equals("lte") || compareType.equals("lt"))) { - if (current.getThreshold().compareTo(previous.getThreshold()) <= 0) { - throw exception(QUOTA_RATE_TIER_THRESHOLD_NOT_ASCENDING, - previous.getSeq(), current.getSeq()); - } - } - - // 如果前一个是 gte/gt,当前应该是 gte/gt,且阈值递增 - if ((previous.getCompareType().equals("gte") || previous.getCompareType().equals("gt")) - && (compareType.equals("gte") || compareType.equals("gt"))) { - if (current.getThreshold().compareTo(previous.getThreshold()) <= 0) { - throw exception(QUOTA_RATE_TIER_THRESHOLD_NOT_ASCENDING, - previous.getSeq(), current.getSeq()); - } - } - - // 不允许混用 lte/lt 和 gte/gt - if ((previous.getCompareType().equals("lte") || previous.getCompareType().equals("lt")) - && (compareType.equals("gte") || compareType.equals("gt"))) { - throw exception(QUOTA_RATE_TIER_COMPARE_TYPE_MIXED, - previous.getSeq(), current.getSeq()); - } - if ((previous.getCompareType().equals("gte") || previous.getCompareType().equals("gt")) - && (compareType.equals("lte") || compareType.equals("lt"))) { - throw exception(QUOTA_RATE_TIER_COMPARE_TYPE_MIXED, - previous.getSeq(), current.getSeq()); - } + } + + // 验证每增加类型只允许一条规则 + long perIncrementCount = tiers.stream() + .filter(t -> "per_increment".equals(t.getCompareType())) + .count(); + if (perIncrementCount > 1) { + throw exception(QUOTA_RATE_TIER_PER_INCREMENT_ONLY_ONE); + } + + // 验证阶梯规则(lte/lt)的阈值递增 + List stepTiers = sortedTiers.stream() + .filter(t -> !"per_increment".equals(t.getCompareType())) + .collect(Collectors.toList()); + for (int i = 1; i < stepTiers.size(); i++) { + QuotaRateValueRulesReqVO.TierRule previous = stepTiers.get(i - 1); + QuotaRateValueRulesReqVO.TierRule current = stepTiers.get(i); + if (current.getThreshold().compareTo(previous.getThreshold()) <= 0) { + throw exception(QUOTA_RATE_TIER_THRESHOLD_NOT_ASCENDING, + previous.getSeq(), current.getSeq()); } } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaResourceServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaResourceServiceImpl.java index 0ee4e02..e945d02 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaResourceServiceImpl.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaResourceServiceImpl.java @@ -13,6 +13,7 @@ import com.yhy.module.core.dal.mysql.resource.ResourceItemMapper; import com.yhy.module.core.enums.ErrorCodeConstants; import com.yhy.module.core.service.quota.QuotaItemService; import com.yhy.module.core.service.quota.QuotaResourceService; +import com.yhy.module.core.util.ResourcePriceCalculator; import java.math.BigDecimal; import java.util.List; import java.util.stream.Collectors; @@ -49,6 +50,19 @@ public class QuotaResourceServiceImpl implements QuotaResourceService { @Resource private com.yhy.module.core.dal.mysql.resource.ResourceMergedMapper resourceMergedMapper; + @Resource + @Lazy + private com.yhy.module.core.service.resource.ResourceMergedService resourceMergedService; + + @Resource + private com.yhy.module.core.dal.mysql.resource.ResourceCategoryMapper resourceCategoryMapper; + + @Resource + private com.yhy.module.core.dal.mysql.quota.QuotaAdjustmentSettingMapper quotaAdjustmentSettingMapper; + + @Resource + private com.yhy.module.core.dal.mysql.quota.QuotaItemMapper quotaItemMapper; + @Override @Transactional(rollbackFor = Exception.class) public Long createQuotaResource(QuotaResourceSaveReqVO createReqVO) { @@ -64,12 +78,24 @@ public class QuotaResourceServiceImpl implements QuotaResourceService { // 3. 转换为 DO QuotaResourceDO quotaResource = convertToDO(createReqVO); - // 4. 插入(标准库不需要快照,查询时实时读取) + // 4. 处理 sortOrder + if (quotaResource.getSortOrder() == null) { + // 没有指定 sortOrder,追加到末尾 + Integer maxSortOrder = getMaxSortOrder(createReqVO.getQuotaItemId()); + quotaResource.setSortOrder(maxSortOrder + 1); + } else { + // 指定了 sortOrder,需要将该位置及之后的记录的 sortOrder 都 +1 + shiftSortOrdersFrom(createReqVO.getQuotaItemId(), quotaResource.getSortOrder()); + } + + // 5. 插入(标准库不需要快照,查询时实时读取) quotaResourceMapper.insert(quotaResource); log.info("[createQuotaResource] 添加定额工料机组成成功,quotaItemId={}, resourceItemId={}", createReqVO.getQuotaItemId(), createReqVO.getResourceItemId()); + // 注意:四个价格字段现在是虚拟字段,不需要更新到数据库 + return quotaResource.getId(); } @@ -91,6 +117,16 @@ public class QuotaResourceServiceImpl implements QuotaResourceService { } } + // 校验:单位为%的工料机不允许修改定额消耗量 + ResourceItemDO resourceItem = resourceItemMapper.selectById(existing.getResourceItemId()); + if (resourceItem != null && "%".equals(resourceItem.getUnit())) { + // 检查是否尝试修改定额消耗量 + if (updateReqVO.getDosage() != null && existing.getDosage() != null + && updateReqVO.getDosage().compareTo(existing.getDosage()) != 0) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.QUOTA_RESOURCE_PERCENT_UNIT_DOSAGE_READONLY); + } + } + // 转换为 DO QuotaResourceDO updateObj = convertToDO(updateReqVO); updateObj.setId(updateReqVO.getId()); @@ -106,16 +142,20 @@ public class QuotaResourceServiceImpl implements QuotaResourceService { // 更新(标准库不需要快照,查询时实时读取) quotaResourceMapper.updateById(updateObj); + + // 注意:四个价格字段现在是虚拟字段,不需要更新到数据库 } @Override @Transactional(rollbackFor = Exception.class) public void deleteQuotaResource(Long id) { // 校验存在 - validateExists(id); + QuotaResourceDO quotaResource = validateExists(id); // 删除 quotaResourceMapper.deleteById(id); + + // 注意:四个价格字段现在是虚拟字段,不需要更新到数据库 } @Override @@ -126,7 +166,150 @@ public class QuotaResourceServiceImpl implements QuotaResourceService { @Override public List getQuotaResourceList(Long quotaItemId) { List list = quotaResourceMapper.selectListByQuotaItemId(quotaItemId); - return list.stream().map(this::convertToVO).collect(Collectors.toList()); + List result = list.stream().map(this::convertToVO).collect(Collectors.toList()); + + // 计算单位为 % 的工料机的合价(需要先有其他工料机的合价数据) + calculatePercentUnitTotalSums(result); + + // 【已禁用】后台定额调整功能改为纯展示效果,不再计算调整消耗量和调整公式 + // 计算逻辑已移至工作台,后台仅保留排版展示 + // calculateAdjustmentValues(quotaItemId, result); + + // 格式化数值:价格/税率保留2位小数,消耗量保留4位小数 + result.forEach(this::formatResourceVO); + + return result; + } + + /** + * 格式化工料机VO的数值字段 + * 价格、税率、合价:保留2位小数,四舍五入 + * 消耗量:保留4位小数,四舍五入 + */ + private void formatResourceVO(QuotaResourceRespVO vo) { + // 价格/税率/合价:2位小数 + vo.setResourceTaxRate(roundToScale(vo.getResourceTaxRate(), 2)); + vo.setResourceTaxExclBasePrice(roundToScale(vo.getResourceTaxExclBasePrice(), 2)); + vo.setResourceTaxInclBasePrice(roundToScale(vo.getResourceTaxInclBasePrice(), 2)); + vo.setResourceTaxExclCompilePrice(roundToScale(vo.getResourceTaxExclCompilePrice(), 2)); + vo.setResourceTaxInclCompilePrice(roundToScale(vo.getResourceTaxInclCompilePrice(), 2)); + vo.setTaxExclBaseTotalSum(roundToScale(vo.getTaxExclBaseTotalSum(), 2)); + vo.setTaxInclBaseTotalSum(roundToScale(vo.getTaxInclBaseTotalSum(), 2)); + vo.setTaxExclCompileTotalSum(roundToScale(vo.getTaxExclCompileTotalSum(), 2)); + vo.setTaxInclCompileTotalSum(roundToScale(vo.getTaxInclCompileTotalSum(), 2)); + vo.setPrice(roundToScale(vo.getPrice(), 2)); + vo.setAmount(roundToScale(vo.getAmount(), 2)); + + // 消耗量:4位小数 + vo.setDosage(roundToScale(vo.getDosage(), 4)); + vo.setAdjustedDosage(roundToScale(vo.getAdjustedDosage(), 4)); + vo.setActualDosage(roundToScale(vo.getActualDosage(), 4)); + + // 格式化子工料机 + if (vo.getMergedItems() != null) { + vo.getMergedItems().forEach(this::formatMergedItemVO); + } + } + + /** + * 格式化复合工料机子项VO的数值字段 + */ + private void formatMergedItemVO(QuotaResourceRespVO.MergedResourceItemVO item) { + // 价格/税率/合价:2位小数 + item.setResourceTaxRate(roundToScale(item.getResourceTaxRate(), 2)); + item.setResourceTaxExclBasePrice(roundToScale(item.getResourceTaxExclBasePrice(), 2)); + item.setResourceTaxInclBasePrice(roundToScale(item.getResourceTaxInclBasePrice(), 2)); + item.setResourceTaxExclCompilePrice(roundToScale(item.getResourceTaxExclCompilePrice(), 2)); + item.setResourceTaxInclCompilePrice(roundToScale(item.getResourceTaxInclCompilePrice(), 2)); + item.setTaxExclBaseTotalSum(roundToScale(item.getTaxExclBaseTotalSum(), 2)); + item.setTaxInclBaseTotalSum(roundToScale(item.getTaxInclBaseTotalSum(), 2)); + item.setTaxExclCompileTotalSum(roundToScale(item.getTaxExclCompileTotalSum(), 2)); + item.setTaxInclCompileTotalSum(roundToScale(item.getTaxInclCompileTotalSum(), 2)); + item.setPrice(roundToScale(item.getPrice(), 2)); + item.setAmount(roundToScale(item.getAmount(), 2)); + + // 消耗量:4位小数 + item.setDosage(roundToScale(item.getDosage(), 4)); + item.setActualDosage(roundToScale(item.getActualDosage(), 4)); + } + + /** + * 四舍五入到指定小数位数 + */ + private BigDecimal roundToScale(BigDecimal value, int scale) { + if (value == null) { + return null; + } + return value.setScale(scale, java.math.RoundingMode.HALF_UP); + } + + /** + * 计算定额基价中单位为 % 的工料机的合价 + * 委托给 ResourcePriceCalculator 统一处理 + */ + private void calculatePercentUnitTotalSums(List resources) { + ResourcePriceCalculator.calculatePercentUnitTotalSums( + resources, resources, quotaResourceFieldAccessor()); + } + + // 【已删除】calculateAdjustmentValues 方法 - 后台定额调整功能已禁用,计算逻辑收归工作台 + + /** + * 重新计算合价(使用调整消耗量) + * 委托给 ResourcePriceCalculator 统一处理 + */ + @SuppressWarnings("unchecked") + private void recalculateTotalSums(List resources) { + ResourcePriceCalculator.recalculateTotalSums( + resources, + quotaResourceFieldAccessor(), + QuotaResourceRespVO::getIsMerged, + QuotaResourceRespVO::getResourceTaxExclBasePrice, + QuotaResourceRespVO::getResourceTaxInclBasePrice, + QuotaResourceRespVO::getResourceTaxExclCompilePrice, + QuotaResourceRespVO::getResourceTaxInclCompilePrice); + } + + /** + * 构建后台定额工料机的 FieldAccessor + * 映射 QuotaResourceRespVO 的字段名到 ResourcePriceCalculator 的通用接口 + */ + private ResourcePriceCalculator.FieldAccessor quotaResourceFieldAccessor() { + return ResourcePriceCalculator.FieldAccessor.builder() + .getUnit(QuotaResourceRespVO::getResourceUnit) + .getCategoryId(QuotaResourceRespVO::getResourceCategoryId) + .getEffectiveDosage(vo -> vo.getAdjustedDosage() != null ? vo.getAdjustedDosage() : vo.getDosage()) + .getCalcBase(QuotaResourceRespVO::getCalcBase) + .getTaxExclBaseTotalSum(QuotaResourceRespVO::getTaxExclBaseTotalSum) + .getTaxInclBaseTotalSum(QuotaResourceRespVO::getTaxInclBaseTotalSum) + .getTaxExclCompileTotalSum(QuotaResourceRespVO::getTaxExclCompileTotalSum) + .getTaxInclCompileTotalSum(QuotaResourceRespVO::getTaxInclCompileTotalSum) + .setTaxExclBaseTotalSum(QuotaResourceRespVO::setTaxExclBaseTotalSum) + .setTaxInclBaseTotalSum(QuotaResourceRespVO::setTaxInclBaseTotalSum) + .setTaxExclCompileTotalSum(QuotaResourceRespVO::setTaxExclCompileTotalSum) + .setTaxInclCompileTotalSum(QuotaResourceRespVO::setTaxInclCompileTotalSum) + .build(); + } + + /** + * 格式化数字用于公式显示 + * 整数不显示小数点,小数最多保留3位 + */ + private String formatNumber(BigDecimal value) { + if (value == null) { + return "0"; + } + // 去除尾部多余的0 + value = value.stripTrailingZeros(); + // 如果是整数(scale <= 0),直接转为整数字符串 + if (value.scale() <= 0) { + return value.toBigInteger().toString(); + } + // 如果小数位数超过3位,保留3位 + if (value.scale() > 3) { + value = value.setScale(3, java.math.RoundingMode.HALF_UP).stripTrailingZeros(); + } + return value.toPlainString(); } @Override @@ -158,6 +341,23 @@ public class QuotaResourceServiceImpl implements QuotaResourceService { .collect(Collectors.toList()); } + @Override + public ResourceItemRespVO getResourceItemByCode(Long quotaItemId, String code) { + // 1. 获取定额专业绑定的工料机专业ID + Long categoryTreeId = quotaItemService.getCategoryTreeIdByQuotaItem(quotaItemId); + + // 2. 根据编码精确查询工料机 + ResourceItemDO resourceItem = resourceItemMapper.selectByCategoryTreeIdAndCode(categoryTreeId, code); + + // 3. 如果未找到返回null + if (resourceItem == null) { + return null; + } + + // 4. 转换为 VO + return convertResourceItemToVO(resourceItem); + } + @Override public void validateResourceInScope(Long quotaItemId, Long resourceItemId) { // 1. 获取定额专业绑定的工料机专业ID @@ -213,16 +413,51 @@ public class QuotaResourceServiceImpl implements QuotaResourceService { quotaResource.setQuotaItemId(vo.getQuotaItemId()); quotaResource.setResourceItemId(vo.getResourceItemId()); quotaResource.setDosage(vo.getDosage()); + // 注意:adjustedDosage 是虚拟字段,由后端动态计算,不从请求中设置 + // quotaResource.setAdjustedDosage(vo.getAdjustedDosage()); quotaResource.setAttributes(vo.getAttributes()); + quotaResource.setSortOrder(vo.getSortOrder()); return quotaResource; } + /** + * 获取指定定额基价下工料机的最大排序值 + */ + private Integer getMaxSortOrder(Long quotaItemId) { + List resources = quotaResourceMapper.selectListByQuotaItemId(quotaItemId); + if (resources == null || resources.isEmpty()) { + return 0; + } + return resources.stream() + .map(QuotaResourceDO::getSortOrder) + .filter(java.util.Objects::nonNull) + .max(Integer::compareTo) + .orElse(0); + } + + /** + * 将指定位置及之后的记录的 sortOrder 都 +1 + * 用于在指定位置插入新记录 + */ + private void shiftSortOrdersFrom(Long quotaItemId, Integer fromSortOrder) { + List resources = quotaResourceMapper.selectListByQuotaItemId(quotaItemId); + for (QuotaResourceDO resource : resources) { + if (resource.getSortOrder() != null && resource.getSortOrder() >= fromSortOrder) { + resource.setSortOrder(resource.getSortOrder() + 1); + quotaResourceMapper.updateById(resource); + } + } + } + private QuotaResourceRespVO convertToVO(QuotaResourceDO quotaResource) { QuotaResourceRespVO vo = new QuotaResourceRespVO(); vo.setId(quotaResource.getId()); vo.setQuotaItemId(quotaResource.getQuotaItemId()); vo.setResourceItemId(quotaResource.getResourceItemId()); vo.setDosage(quotaResource.getDosage()); + // 注意:adjustedDosage 是虚拟字段,由 calculateAdjustmentValues 方法动态计算 + // 这里不再从数据库读取,初始化为 null + vo.setAdjustedDosage(null); vo.setAttributes(quotaResource.getAttributes()); vo.setCreateTime(quotaResource.getCreateTime()); vo.setUpdateTime(quotaResource.getUpdateTime()); @@ -238,19 +473,46 @@ public class QuotaResourceServiceImpl implements QuotaResourceService { vo.setResourceUnit(resourceItem.getUnit()); vo.setResourceSpec(resourceItem.getSpec()); vo.setResourceCategoryId(resourceItem.getCategoryId()); - vo.setResourceType(resourceItem.getType()); - vo.setResourceTaxRate(resourceItem.getTaxRate()); - vo.setResourceTaxExclBasePrice(resourceItem.getTaxExclBasePrice()); - vo.setResourceTaxInclBasePrice(resourceItem.getTaxInclBasePrice()); - vo.setResourceTaxExclCompilePrice(resourceItem.getTaxExclCompilePrice()); - vo.setResourceTaxInclCompilePrice(resourceItem.getTaxInclCompilePrice()); + vo.setResourceType(convertTypeToDisplay(resourceItem.getType())); vo.setCalcBase(resourceItem.getCalcBase()); - // 价格优先使用除税基价 - if (resourceItem.getTaxExclBasePrice() != null) { - vo.setPrice(resourceItem.getTaxExclBasePrice()); - } else if (resourceItem.getTaxInclBasePrice() != null) { - vo.setPrice(resourceItem.getTaxInclBasePrice()); + // 判断是否为复合工料机 + boolean isMerged = resourceItem.getIsMerged() != null && resourceItem.getIsMerged() == 1; + vo.setIsMerged(isMerged); + + if (isMerged) { + // 复合工料机:税率和四个价格字段应该为空 + vo.setResourceTaxRate(null); + vo.setResourceTaxExclBasePrice(null); + vo.setResourceTaxInclBasePrice(null); + vo.setResourceTaxExclCompilePrice(null); + vo.setResourceTaxInclCompilePrice(null); + vo.setPrice(null); + } else { + // 普通工料机:设置税率和价格字段 + vo.setResourceTaxRate(resourceItem.getTaxRate()); + vo.setResourceTaxExclBasePrice(resourceItem.getTaxExclBasePrice()); + vo.setResourceTaxInclBasePrice(resourceItem.getTaxInclBasePrice()); + vo.setResourceTaxExclCompilePrice(resourceItem.getTaxExclCompilePrice()); + vo.setResourceTaxInclCompilePrice(resourceItem.getTaxInclCompilePrice()); + + // 价格优先使用除税基价 + if (resourceItem.getTaxExclBasePrice() != null) { + vo.setPrice(resourceItem.getTaxExclBasePrice()); + } else if (resourceItem.getTaxInclBasePrice() != null) { + vo.setPrice(resourceItem.getTaxInclBasePrice()); + } + + // 普通工料机:计算四个合价虚拟字段 + // 合价 = 价格 × 定额消耗量 + // 注意:这里先使用原始消耗量计算,后续在 calculateAdjustmentValues 中会根据调整消耗量重新计算 + BigDecimal effectiveDosage = quotaResource.getDosage(); + if (effectiveDosage != null) { + vo.setTaxExclBaseTotalSum(multiplyOrZero(resourceItem.getTaxExclBasePrice(), effectiveDosage)); + vo.setTaxInclBaseTotalSum(multiplyOrZero(resourceItem.getTaxInclBasePrice(), effectiveDosage)); + vo.setTaxExclCompileTotalSum(multiplyOrZero(resourceItem.getTaxExclCompilePrice(), effectiveDosage)); + vo.setTaxInclCompileTotalSum(multiplyOrZero(resourceItem.getTaxInclCompilePrice(), effectiveDosage)); + } } // 计算实际消耗量和金额 @@ -259,21 +521,67 @@ public class QuotaResourceServiceImpl implements QuotaResourceService { vo.setAmount(vo.getActualDosage().multiply(vo.getPrice())); } - // 判断是否为复合工料机 - if (resourceItem.getIsMerged() != null && resourceItem.getIsMerged() == 1) { - vo.setIsMerged(true); + // 如果是复合工料机,查询子数据 + if (isMerged) { // 查询复合工料机的子数据 List mergedList = resourceMergedMapper.selectByMergedId(resourceItem.getId()); if (mergedList != null && !mergedList.isEmpty()) { + // 先构建类别价格映射表(用于计算单位为 % 的工料机) + java.util.Map categoryPriceMap = new java.util.HashMap<>(); + + // 第一次遍历:构建类别价格映射表(只处理普通工料机) + // 类别价格映射表中的合价需要乘以父定额消耗量,用于单位%工料机的计算基数 + BigDecimal parentDosageForMap = quotaResource.getDosage(); + for (ResourceMergedDO merged : mergedList) { + if (merged.getSourceId() == null || merged.getQuotaConsumption() == null) { + continue; + } + + ResourceItemDO source = resourceItemMapper.selectById(merged.getSourceId()); + if (source == null || "%".equals(source.getUnit())) { + continue; + } + + // 子工料机合价:factor = 子定额消耗量 × 父定额消耗量 + // 这样计算出的合价与显示的子项合价一致 + BigDecimal factor = merged.getQuotaConsumption(); + if (parentDosageForMap != null) { + factor = factor.multiply(parentDosageForMap); + } + + // 计算合价并累加到类别映射表 + if (source.getCategoryId() != null) { + CategoryPriceSum categoryPrice = categoryPriceMap.computeIfAbsent( + source.getCategoryId(), k -> new CategoryPriceSum()); + + categoryPrice.taxExclBasePrice = categoryPrice.taxExclBasePrice.add( + multiplyOrZero(source.getTaxExclBasePrice(), factor)); + categoryPrice.taxInclBasePrice = categoryPrice.taxInclBasePrice.add( + multiplyOrZero(source.getTaxInclBasePrice(), factor)); + categoryPrice.taxExclCompilePrice = categoryPrice.taxExclCompilePrice.add( + multiplyOrZero(source.getTaxExclCompilePrice(), factor)); + categoryPrice.taxInclCompilePrice = categoryPrice.taxInclCompilePrice.add( + multiplyOrZero(source.getTaxInclCompilePrice(), factor)); + } + } + + // 第二次遍历:构建 mergedItems 并计算虚拟字段 List mergedItems = mergedList.stream() .map(merged -> { QuotaResourceRespVO.MergedResourceItemVO item = new QuotaResourceRespVO.MergedResourceItemVO(); item.setId(merged.getId()); item.setResourceItemId(merged.getSourceId()); item.setRegionCode(merged.getRegionCode()); - item.setDosage(merged.getQuotaConsumption()); + // 子项定额消耗量 = 原定额消耗量 × 父定额消耗量 + BigDecimal childDosage = merged.getQuotaConsumption(); + BigDecimal parentDosage = quotaResource.getDosage(); + if (childDosage != null && parentDosage != null) { + item.setDosage(childDosage.multiply(parentDosage).setScale(6, java.math.RoundingMode.HALF_UP)); + } else { + item.setDosage(childDosage); + } item.setPrice(merged.getTaxExclMarketPrice()); // 查询源工料机信息 @@ -285,8 +593,107 @@ public class QuotaResourceServiceImpl implements QuotaResourceService { item.setResourceUnit(sourceItem.getUnit()); item.setResourceSpec(sourceItem.getSpec()); item.setResourceCategoryId(sourceItem.getCategoryId()); - item.setResourceType(sourceItem.getType()); + item.setResourceType(convertTypeToDisplay(sourceItem.getType())); item.setResourceTaxRate(sourceItem.getTaxRate()); + item.setCalcBase(sourceItem.getCalcBase()); // 设置计算基数 + + // 设置原工料机的四个价格字段(子工料机保留原工料机的价格) + item.setResourceTaxExclBasePrice(sourceItem.getTaxExclBasePrice()); + item.setResourceTaxInclBasePrice(sourceItem.getTaxInclBasePrice()); + item.setResourceTaxExclCompilePrice(sourceItem.getTaxExclCompilePrice()); + item.setResourceTaxInclCompilePrice(sourceItem.getTaxInclCompilePrice()); + + // 计算虚拟字段(四个合价) + BigDecimal quotaConsumption = merged.getQuotaConsumption(); + BigDecimal parentDosageForCalc = quotaResource.getDosage(); + + if (quotaConsumption == null || parentDosageForCalc == null) { + // 消耗量为空时,设置合价为0 + item.setTaxExclBaseTotalSum(BigDecimal.ZERO); + item.setTaxInclBaseTotalSum(BigDecimal.ZERO); + item.setTaxExclCompileTotalSum(BigDecimal.ZERO); + item.setTaxInclCompileTotalSum(BigDecimal.ZERO); + } else { + // 子工料机合价计算:factor = 子定额消耗量 × 父定额消耗量 + BigDecimal factor = quotaConsumption.multiply(parentDosageForCalc); + + if ("%".equals(sourceItem.getUnit()) && sourceItem.getCalcBase() != null) { + // 单位为 % 的工料机:使用计算基数公式 + // 合价 = 公式结果 × (显示的定额消耗量 / 100) + // 显示的定额消耗量 = 子定额消耗量 × 父定额消耗量 + BigDecimal displayDosage = quotaConsumption.multiply(parentDosageForCalc); + // percentFactor = 显示的定额消耗量 / 100(作为百分比) + BigDecimal percentFactor = displayDosage.divide(new BigDecimal("100"), 6, java.math.RoundingMode.HALF_UP); + boolean calculated = false; + try { + java.util.Map calcBase = sourceItem.getCalcBase(); + if (calcBase.containsKey("formula") && calcBase.containsKey("variables")) { + String formula = (String) calcBase.get("formula"); + @SuppressWarnings("unchecked") + java.util.Map variablesConfig = (java.util.Map) calcBase.get("variables"); + + // 构建变量值映射表 + java.util.Map taxExclBaseVars = new java.util.HashMap<>(); + java.util.Map taxInclBaseVars = new java.util.HashMap<>(); + java.util.Map taxExclCompileVars = new java.util.HashMap<>(); + java.util.Map taxInclCompileVars = new java.util.HashMap<>(); + + for (java.util.Map.Entry varEntry : variablesConfig.entrySet()) { + String varName = varEntry.getKey(); + Long categoryId = Long.valueOf(varEntry.getValue().toString()); + + CategoryPriceSum categoryPrice = categoryPriceMap.get(categoryId); + if (categoryPrice == null) { + // 如果类别不存在,使用 0 作为默认值 + log.debug("[convertToVO] 单位为%的工料机类别数据为空,使用0作为默认值,sourceId={}, categoryId={}", + sourceItem.getId(), categoryId); + taxExclBaseVars.put(varName, BigDecimal.ZERO); + taxInclBaseVars.put(varName, BigDecimal.ZERO); + taxExclCompileVars.put(varName, BigDecimal.ZERO); + taxInclCompileVars.put(varName, BigDecimal.ZERO); + } else { + taxExclBaseVars.put(varName, categoryPrice.taxExclBasePrice); + taxInclBaseVars.put(varName, categoryPrice.taxInclBasePrice); + taxExclCompileVars.put(varName, categoryPrice.taxExclCompilePrice); + taxInclCompileVars.put(varName, categoryPrice.taxInclCompilePrice); + } + } + + { + // 计算公式结果 + BigDecimal taxExclBaseSum = com.yhy.module.core.util.FormulaEvaluator.evaluate(formula, taxExclBaseVars); + BigDecimal taxInclBaseSum = com.yhy.module.core.util.FormulaEvaluator.evaluate(formula, taxInclBaseVars); + BigDecimal taxExclCompileSum = com.yhy.module.core.util.FormulaEvaluator.evaluate(formula, taxExclCompileVars); + BigDecimal taxInclCompileSum = com.yhy.module.core.util.FormulaEvaluator.evaluate(formula, taxInclCompileVars); + + // 设置虚拟字段:合价 = 公式结果 × 显示的定额消耗量% + item.setTaxExclBaseTotalSum(taxExclBaseSum.multiply(percentFactor).setScale(4, java.math.RoundingMode.HALF_UP)); + item.setTaxInclBaseTotalSum(taxInclBaseSum.multiply(percentFactor).setScale(4, java.math.RoundingMode.HALF_UP)); + item.setTaxExclCompileTotalSum(taxExclCompileSum.multiply(percentFactor).setScale(4, java.math.RoundingMode.HALF_UP)); + item.setTaxInclCompileTotalSum(taxInclCompileSum.multiply(percentFactor).setScale(4, java.math.RoundingMode.HALF_UP)); + calculated = true; + } + } + } catch (Exception e) { + log.error("[convertToVO] 计算单位为%的工料机合价失败,sourceId={}, error={}", + sourceItem.getId(), e.getMessage(), e); + } + + // 如果缺少类别数据或计算失败,设置合价为0 + if (!calculated) { + item.setTaxExclBaseTotalSum(BigDecimal.ZERO); + item.setTaxInclBaseTotalSum(BigDecimal.ZERO); + item.setTaxExclCompileTotalSum(BigDecimal.ZERO); + item.setTaxInclCompileTotalSum(BigDecimal.ZERO); + } + } else { + // 普通工料机:合价 = 价格 × 子定额消耗量 × 父定额消耗量 + item.setTaxExclBaseTotalSum(multiplyOrZero(sourceItem.getTaxExclBasePrice(), factor)); + item.setTaxInclBaseTotalSum(multiplyOrZero(sourceItem.getTaxInclBasePrice(), factor)); + item.setTaxExclCompileTotalSum(multiplyOrZero(sourceItem.getTaxExclCompilePrice(), factor)); + item.setTaxInclCompileTotalSum(multiplyOrZero(sourceItem.getTaxInclCompilePrice(), factor)); + } + } } } @@ -315,15 +722,215 @@ public class QuotaResourceServiceImpl implements QuotaResourceService { .collect(Collectors.toList()); vo.setMergedItems(mergedItems); + + // 计算复合工料机的虚拟字段(四个合价总和 = 子项合价之和) + calculateMergedTotalSumsFromItems(vo, mergedItems); + + // 反算复合工料机的四个单价字段:单价 = 合价 / 定额消耗量 + BigDecimal parentDosageForPrice = quotaResource.getDosage(); + if (parentDosageForPrice != null && parentDosageForPrice.compareTo(BigDecimal.ZERO) != 0) { + vo.setResourceTaxExclBasePrice(vo.getTaxExclBaseTotalSum() != null ? + vo.getTaxExclBaseTotalSum().divide(parentDosageForPrice, 6, java.math.RoundingMode.HALF_UP) : null); + vo.setResourceTaxInclBasePrice(vo.getTaxInclBaseTotalSum() != null ? + vo.getTaxInclBaseTotalSum().divide(parentDosageForPrice, 6, java.math.RoundingMode.HALF_UP) : null); + vo.setResourceTaxExclCompilePrice(vo.getTaxExclCompileTotalSum() != null ? + vo.getTaxExclCompileTotalSum().divide(parentDosageForPrice, 6, java.math.RoundingMode.HALF_UP) : null); + vo.setResourceTaxInclCompilePrice(vo.getTaxInclCompileTotalSum() != null ? + vo.getTaxInclCompileTotalSum().divide(parentDosageForPrice, 6, java.math.RoundingMode.HALF_UP) : null); + } } - } else { - vo.setIsMerged(false); } } return vo; } + /** + * 计算复合工料机的虚拟字段(四个合价总和) + * + * @param vo 定额工料机组成 VO + * @param mergedList 复合工料机子数据列表 + * @param quotaDosage 定额基价中的定额消耗量 + */ + private void calculateMergedTotalSums(QuotaResourceRespVO vo, List mergedList, BigDecimal quotaDosage) { + if (mergedList == null || mergedList.isEmpty() || quotaDosage == null) { + return; + } + + // 按类别分组,用于计算基数公式 + java.util.Map categoryPriceMap = new java.util.HashMap<>(); + + // 计算父定额消耗量因子 + // 第一次遍历:构建类别价格映射表(只处理普通工料机) + for (ResourceMergedDO merged : mergedList) { + if (merged.getSourceId() == null || merged.getQuotaConsumption() == null) { + continue; + } + + ResourceItemDO source = resourceItemMapper.selectById(merged.getSourceId()); + if (source == null || "%".equals(source.getUnit())) { + continue; + } + + // 子工料机合价:factor = 子定额消耗量 × 父定额消耗量 + BigDecimal factor = merged.getQuotaConsumption().multiply(quotaDosage); + + // 计算合价并累加到类别映射表 + if (source.getCategoryId() != null) { + CategoryPriceSum categoryPrice = categoryPriceMap.computeIfAbsent( + source.getCategoryId(), k -> new CategoryPriceSum()); + + categoryPrice.taxExclBasePrice = categoryPrice.taxExclBasePrice.add( + multiplyOrZero(source.getTaxExclBasePrice(), factor)); + categoryPrice.taxInclBasePrice = categoryPrice.taxInclBasePrice.add( + multiplyOrZero(source.getTaxInclBasePrice(), factor)); + categoryPrice.taxExclCompilePrice = categoryPrice.taxExclCompilePrice.add( + multiplyOrZero(source.getTaxExclCompilePrice(), factor)); + categoryPrice.taxInclCompilePrice = categoryPrice.taxInclCompilePrice.add( + multiplyOrZero(source.getTaxInclCompilePrice(), factor)); + } + } + + // 第二次遍历:处理单位为 % 的工料机 + for (ResourceMergedDO merged : mergedList) { + if (merged.getSourceId() == null || merged.getQuotaConsumption() == null) { + continue; + } + + ResourceItemDO source = resourceItemMapper.selectById(merged.getSourceId()); + if (source == null || !"%".equals(source.getUnit()) || source.getCalcBase() == null) { + continue; + } + + try { + // 解析 calc_base + java.util.Map calcBase = source.getCalcBase(); + if (!calcBase.containsKey("formula") || !calcBase.containsKey("variables")) { + continue; + } + + String formula = (String) calcBase.get("formula"); + @SuppressWarnings("unchecked") + java.util.Map variablesConfig = (java.util.Map) calcBase.get("variables"); + + // 构建变量值映射表 + java.util.Map taxExclBaseVars = new java.util.HashMap<>(); + java.util.Map taxInclBaseVars = new java.util.HashMap<>(); + java.util.Map taxExclCompileVars = new java.util.HashMap<>(); + java.util.Map taxInclCompileVars = new java.util.HashMap<>(); + + for (java.util.Map.Entry varEntry : variablesConfig.entrySet()) { + String varName = varEntry.getKey(); + Long categoryId = Long.valueOf(varEntry.getValue().toString()); + + CategoryPriceSum categoryPrice = categoryPriceMap.get(categoryId); + if (categoryPrice == null) { + // 如果类别不存在,使用 0 作为默认值 + taxExclBaseVars.put(varName, BigDecimal.ZERO); + taxInclBaseVars.put(varName, BigDecimal.ZERO); + taxExclCompileVars.put(varName, BigDecimal.ZERO); + taxInclCompileVars.put(varName, BigDecimal.ZERO); + } else { + taxExclBaseVars.put(varName, categoryPrice.taxExclBasePrice); + taxInclBaseVars.put(varName, categoryPrice.taxInclBasePrice); + taxExclCompileVars.put(varName, categoryPrice.taxExclCompilePrice); + taxInclCompileVars.put(varName, categoryPrice.taxInclCompilePrice); + } + } + + // 计算公式结果 + BigDecimal taxExclBaseSum = com.yhy.module.core.util.FormulaEvaluator.evaluate(formula, taxExclBaseVars); + BigDecimal taxInclBaseSum = com.yhy.module.core.util.FormulaEvaluator.evaluate(formula, taxInclBaseVars); + BigDecimal taxExclCompileSum = com.yhy.module.core.util.FormulaEvaluator.evaluate(formula, taxExclCompileVars); + BigDecimal taxInclCompileSum = com.yhy.module.core.util.FormulaEvaluator.evaluate(formula, taxInclCompileVars); + + // 单位为 % 的工料机:合价 = 公式结果 × (显示的定额消耗量 / 100) + // 显示的定额消耗量 = 子定额消耗量 × 父定额消耗量 + BigDecimal displayDosage = merged.getQuotaConsumption().multiply(quotaDosage); + BigDecimal percentFactor = displayDosage.divide(new BigDecimal("100"), 6, java.math.RoundingMode.HALF_UP); + + // 累加到类别价格映射表 + if (source.getCategoryId() != null) { + CategoryPriceSum categoryPrice = categoryPriceMap.computeIfAbsent( + source.getCategoryId(), k -> new CategoryPriceSum()); + + categoryPrice.taxExclBasePrice = categoryPrice.taxExclBasePrice.add( + taxExclBaseSum.multiply(percentFactor).setScale(4, java.math.RoundingMode.HALF_UP)); + categoryPrice.taxInclBasePrice = categoryPrice.taxInclBasePrice.add( + taxInclBaseSum.multiply(percentFactor).setScale(4, java.math.RoundingMode.HALF_UP)); + categoryPrice.taxExclCompilePrice = categoryPrice.taxExclCompilePrice.add( + taxExclCompileSum.multiply(percentFactor).setScale(4, java.math.RoundingMode.HALF_UP)); + categoryPrice.taxInclCompilePrice = categoryPrice.taxInclCompilePrice.add( + taxInclCompileSum.multiply(percentFactor).setScale(4, java.math.RoundingMode.HALF_UP)); + } + } catch (Exception e) { + log.error("[calculateMergedTotalSums] 计算失败,mergedId={}, sourceId={}, error={}", + merged.getMergedId(), merged.getSourceId(), e.getMessage(), e); + } + } + + // 汇总所有类别的价格 + BigDecimal totalTaxExclBase = BigDecimal.ZERO; + BigDecimal totalTaxInclBase = BigDecimal.ZERO; + BigDecimal totalTaxExclCompile = BigDecimal.ZERO; + BigDecimal totalTaxInclCompile = BigDecimal.ZERO; + + for (CategoryPriceSum categoryPrice : categoryPriceMap.values()) { + totalTaxExclBase = totalTaxExclBase.add(categoryPrice.taxExclBasePrice); + totalTaxInclBase = totalTaxInclBase.add(categoryPrice.taxInclBasePrice); + totalTaxExclCompile = totalTaxExclCompile.add(categoryPrice.taxExclCompilePrice); + totalTaxInclCompile = totalTaxInclCompile.add(categoryPrice.taxInclCompilePrice); + } + + // 设置虚拟字段 + vo.setTaxExclBaseTotalSum(totalTaxExclBase); + vo.setTaxInclBaseTotalSum(totalTaxInclBase); + vo.setTaxExclCompileTotalSum(totalTaxExclCompile); + vo.setTaxInclCompileTotalSum(totalTaxInclCompile); + } + + /** + * 从子项列表直接汇总复合工料机的四个合价 + * 委托给 ResourcePriceCalculator.sumChildrenToParent(支持父子不同类型) + */ + private void calculateMergedTotalSumsFromItems(QuotaResourceRespVO vo, List mergedItems) { + ResourcePriceCalculator.sumChildrenToParent( + vo, mergedItems, mergedItemFieldAccessor(), quotaResourceFieldAccessor()); + } + + /** + * 构建复合工料机子项的 FieldAccessor(只需 getter) + */ + private ResourcePriceCalculator.FieldAccessor mergedItemFieldAccessor() { + return ResourcePriceCalculator.FieldAccessor.builder() + .getTaxExclBaseTotalSum(QuotaResourceRespVO.MergedResourceItemVO::getTaxExclBaseTotalSum) + .getTaxInclBaseTotalSum(QuotaResourceRespVO.MergedResourceItemVO::getTaxInclBaseTotalSum) + .getTaxExclCompileTotalSum(QuotaResourceRespVO.MergedResourceItemVO::getTaxExclCompileTotalSum) + .getTaxInclCompileTotalSum(QuotaResourceRespVO.MergedResourceItemVO::getTaxInclCompileTotalSum) + .build(); + } + + /** + * 安全乘法(处理 null 值) + * 委托给 ResourcePriceCalculator.multiplyOrZero + */ + private BigDecimal multiplyOrZero(BigDecimal value, BigDecimal factor) { + return ResourcePriceCalculator.multiplyOrZero(value, factor); + } + + // 注意:updateQuotaItemPrices 方法已删除,因为四个价格字段现在是虚拟字段, + // 由 QuotaItemServiceImpl.calculateQuotaItemPrices 方法在查询时动态计算 + + /** + * 类别价格汇总 + */ + private static class CategoryPriceSum { + BigDecimal taxExclBasePrice = BigDecimal.ZERO; + BigDecimal taxInclBasePrice = BigDecimal.ZERO; + BigDecimal taxExclCompilePrice = BigDecimal.ZERO; + BigDecimal taxInclCompilePrice = BigDecimal.ZERO; + } + private ResourceItemRespVO convertResourceItemToVO(ResourceItemDO resourceItem) { ResourceItemRespVO vo = new ResourceItemRespVO(); vo.setId(resourceItem.getId()); @@ -331,7 +938,7 @@ public class QuotaResourceServiceImpl implements QuotaResourceService { vo.setCode(resourceItem.getCode()); vo.setName(resourceItem.getName()); vo.setSpec(resourceItem.getSpec()); - vo.setType(resourceItem.getType()); + vo.setType(convertTypeToDisplay(resourceItem.getType())); vo.setUnit(resourceItem.getUnit()); vo.setCategoryId(resourceItem.getCategoryId()); vo.setCategoryTreeId(resourceItem.getCategoryTreeId()); @@ -340,6 +947,31 @@ public class QuotaResourceServiceImpl implements QuotaResourceService { vo.setTaxInclBasePrice(resourceItem.getTaxInclBasePrice()); vo.setTaxExclCompilePrice(resourceItem.getTaxExclCompilePrice()); vo.setTaxInclCompilePrice(resourceItem.getTaxInclCompilePrice()); + vo.setCalcBase(resourceItem.getCalcBase()); + vo.setIsMerged(resourceItem.getIsMerged()); return vo; } + + // 【已删除】后台定额调整功能改为纯展示效果,以下方法已删除: + // - applyAdjustmentSetting + // - applyAdjustCoefficient + // - applyConsumptionAdjustment + // - applyDynamicAdjustment + // - applyDynamicMerge + // - applyValueRule + // - DynamicAdjustParams + // - DynamicMergeParams + + /** + * 将英文类型转换为中文显示 + */ + private String convertTypeToDisplay(String type) { + if (type == null) return null; + switch (type) { + case "labor": return "人"; + case "material": return "材"; + case "machine": return "机"; + default: return type; + } + } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaUnifiedFeeResourceServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaUnifiedFeeResourceServiceImpl.java new file mode 100644 index 0000000..b166c5e --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaUnifiedFeeResourceServiceImpl.java @@ -0,0 +1,403 @@ +package com.yhy.module.core.service.quota.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; +import com.yhy.module.core.controller.admin.quota.vo.QuotaUnifiedFeeResourceSaveReqVO; +import com.yhy.module.core.dal.dataobject.quota.QuotaUnifiedFeeResourceDO; +import com.yhy.module.core.dal.mysql.quota.QuotaUnifiedFeeResourceMapper; +import com.yhy.module.core.enums.ErrorCodeConstants; +import com.yhy.module.core.service.quota.QuotaUnifiedFeeResourceService; +import java.util.List; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * 统一取费子目工料机 Service 实现类 + */ +@Service +@Validated +@Slf4j +public class QuotaUnifiedFeeResourceServiceImpl implements QuotaUnifiedFeeResourceService { + + @Resource + private QuotaUnifiedFeeResourceMapper unifiedFeeResourceMapper; + + @Resource + private com.yhy.module.core.dal.mysql.quota.QuotaCatalogItemMapper quotaCatalogItemMapper; + + @Resource + private com.yhy.module.core.dal.mysql.resource.ResourceItemMapper resourceItemMapper; + + @Resource + private com.yhy.module.core.dal.mysql.resource.ResourceCatalogItemMapper resourceCatalogItemMapper; + + @Resource + private com.yhy.module.core.dal.mysql.quota.QuotaUnifiedFeeSettingMapper unifiedFeeSettingMapper; + + @Resource + private com.yhy.module.core.dal.mysql.resource.ResourceMergedMapper resourceMergedMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createUnifiedFeeResource(QuotaUnifiedFeeResourceSaveReqVO createReqVO) { + // 1. 验证工料机是否在范围内 + validateResourceInScope(createReqVO.getUnifiedFeeSettingId(), createReqVO.getResourceItemId()); + + // 2. 验证工料机是否存在 + com.yhy.module.core.dal.dataobject.resource.ResourceItemDO resourceItem = + resourceItemMapper.selectById(createReqVO.getResourceItemId()); + if (resourceItem == null) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.RESOURCE_ITEM_NOT_EXISTS); + } + + // 3. 设置排序值 + if (createReqVO.getSortOrder() == null) { + Integer maxSortOrder = unifiedFeeResourceMapper.selectMaxSortOrder(createReqVO.getUnifiedFeeSettingId()); + createReqVO.setSortOrder(maxSortOrder + 1); + } + + // 4. 转换并插入 + QuotaUnifiedFeeResourceDO unifiedFeeResource = BeanUtil.copyProperties(createReqVO, QuotaUnifiedFeeResourceDO.class); + unifiedFeeResourceMapper.insert(unifiedFeeResource); + + log.info("[createUnifiedFeeResource] 添加统一取费子目工料机成功,unifiedFeeSettingId={}, resourceItemId={}", + createReqVO.getUnifiedFeeSettingId(), createReqVO.getResourceItemId()); + + return unifiedFeeResource.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateUnifiedFeeResource(QuotaUnifiedFeeResourceSaveReqVO updateReqVO) { + // 校验存在 + validateUnifiedFeeResourceExists(updateReqVO.getId()); + + // 更新 + QuotaUnifiedFeeResourceDO updateObj = BeanUtil.copyProperties(updateReqVO, QuotaUnifiedFeeResourceDO.class); + unifiedFeeResourceMapper.updateById(updateObj); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteUnifiedFeeResource(Long id) { + // 校验存在 + validateUnifiedFeeResourceExists(id); + + // 删除 + unifiedFeeResourceMapper.deleteById(id); + } + + @Override + public QuotaUnifiedFeeResourceDO getUnifiedFeeResource(Long id) { + return unifiedFeeResourceMapper.selectById(id); + } + + @Override + public List getUnifiedFeeResourceList(Long unifiedFeeSettingId) { + List list = unifiedFeeResourceMapper.selectListByUnifiedFeeSettingId(unifiedFeeSettingId); + return list.stream().map(this::convertToVO).collect(java.util.stream.Collectors.toList()); + } + + /** + * 转换DO为VO,并关联查询工料机详细信息 + */ + private com.yhy.module.core.controller.admin.quota.vo.QuotaUnifiedFeeResourceRespVO convertToVO(QuotaUnifiedFeeResourceDO resource) { + com.yhy.module.core.controller.admin.quota.vo.QuotaUnifiedFeeResourceRespVO vo = + new com.yhy.module.core.controller.admin.quota.vo.QuotaUnifiedFeeResourceRespVO(); + BeanUtil.copyProperties(resource, vo); + + // 实时查询工料机信息(参考QuotaResourceServiceImpl.convertToVO,字段命名保持一致) + com.yhy.module.core.dal.dataobject.resource.ResourceItemDO resourceItem = + resourceItemMapper.selectById(resource.getResourceItemId()); + if (resourceItem != null) { + vo.setResourceCode(resourceItem.getCode()); + vo.setResourceName(resourceItem.getName()); + vo.setResourceSpec(resourceItem.getSpec()); + vo.setResourceUnit(resourceItem.getUnit()); + vo.setResourceType(resourceItem.getType()); + vo.setCalcBase(resourceItem.getCalcBase()); + + // 判断是否为复合工料机 + boolean isMerged = resourceItem.getIsMerged() != null && resourceItem.getIsMerged() == 1; + vo.setIsMerged(isMerged); + + if (isMerged) { + // 复合工料机:税率和四个价格字段应该为空 + vo.setResourceTaxRate(null); + vo.setResourceTaxExclBasePrice(null); + vo.setResourceTaxInclBasePrice(null); + vo.setResourceTaxExclCompilePrice(null); + vo.setResourceTaxInclCompilePrice(null); + + // 查询复合工料机的子数据 + java.util.List mergedList = + resourceMergedMapper.selectByMergedId(resourceItem.getId()); + if (mergedList != null && !mergedList.isEmpty()) { + java.util.List mergedItems = + new java.util.ArrayList<>(); + + java.math.BigDecimal totalTaxExclBase = java.math.BigDecimal.ZERO; + java.math.BigDecimal totalTaxInclBase = java.math.BigDecimal.ZERO; + java.math.BigDecimal totalTaxExclCompile = java.math.BigDecimal.ZERO; + java.math.BigDecimal totalTaxInclCompile = java.math.BigDecimal.ZERO; + + for (com.yhy.module.core.dal.dataobject.resource.ResourceMergedDO merged : mergedList) { + com.yhy.module.core.controller.admin.quota.vo.QuotaUnifiedFeeResourceRespVO.MergedResourceItemVO item = + new com.yhy.module.core.controller.admin.quota.vo.QuotaUnifiedFeeResourceRespVO.MergedResourceItemVO(); + item.setId(merged.getId()); + item.setResourceItemId(merged.getSourceId()); + item.setDosage(merged.getQuotaConsumption()); + + // 查询子工料机详情 + com.yhy.module.core.dal.dataobject.resource.ResourceItemDO childResource = + resourceItemMapper.selectById(merged.getSourceId()); + if (childResource != null) { + item.setResourceCode(childResource.getCode()); + item.setResourceName(childResource.getName()); + item.setResourceUnit(childResource.getUnit()); + item.setResourceSpec(childResource.getSpec()); + item.setResourceType(childResource.getType()); + item.setResourceTaxRate(childResource.getTaxRate()); + item.setResourceTaxExclBasePrice(childResource.getTaxExclBasePrice()); + item.setResourceTaxInclBasePrice(childResource.getTaxInclBasePrice()); + item.setResourceTaxExclCompilePrice(childResource.getTaxExclCompilePrice()); + item.setResourceTaxInclCompilePrice(childResource.getTaxInclCompilePrice()); + item.setCalcBase(childResource.getCalcBase()); + + // 计算子项合价 + java.math.BigDecimal childDosage = merged.getQuotaConsumption() != null ? merged.getQuotaConsumption() : java.math.BigDecimal.ZERO; + if (childResource.getTaxExclBasePrice() != null) { + java.math.BigDecimal sum = childResource.getTaxExclBasePrice().multiply(childDosage); + item.setTaxExclBaseTotalSum(sum); + totalTaxExclBase = totalTaxExclBase.add(sum); + } + if (childResource.getTaxInclBasePrice() != null) { + java.math.BigDecimal sum = childResource.getTaxInclBasePrice().multiply(childDosage); + item.setTaxInclBaseTotalSum(sum); + totalTaxInclBase = totalTaxInclBase.add(sum); + } + if (childResource.getTaxExclCompilePrice() != null) { + java.math.BigDecimal sum = childResource.getTaxExclCompilePrice().multiply(childDosage); + item.setTaxExclCompileTotalSum(sum); + totalTaxExclCompile = totalTaxExclCompile.add(sum); + } + if (childResource.getTaxInclCompilePrice() != null) { + java.math.BigDecimal sum = childResource.getTaxInclCompilePrice().multiply(childDosage); + item.setTaxInclCompileTotalSum(sum); + totalTaxInclCompile = totalTaxInclCompile.add(sum); + } + } + mergedItems.add(item); + } + vo.setMergedItems(mergedItems); + + // 复合工料机的合价 = 子项合价之和 × 父消耗量 + java.math.BigDecimal parentQuantity = resource.getAdjustedDosage() != null ? + resource.getAdjustedDosage() : (resource.getDosage() != null ? resource.getDosage() : java.math.BigDecimal.ONE); + vo.setTaxExclBaseTotalSum(totalTaxExclBase.multiply(parentQuantity)); + vo.setTaxInclBaseTotalSum(totalTaxInclBase.multiply(parentQuantity)); + vo.setTaxExclCompileTotalSum(totalTaxExclCompile.multiply(parentQuantity)); + vo.setTaxInclCompileTotalSum(totalTaxInclCompile.multiply(parentQuantity)); + } + } else { + // 普通工料机 + vo.setResourceTaxRate(resourceItem.getTaxRate()); + vo.setResourceTaxExclBasePrice(resourceItem.getTaxExclBasePrice()); + vo.setResourceTaxInclBasePrice(resourceItem.getTaxInclBasePrice()); + vo.setResourceTaxExclCompilePrice(resourceItem.getTaxExclCompilePrice()); + vo.setResourceTaxInclCompilePrice(resourceItem.getTaxInclCompilePrice()); + + // 计算合价 = 单价 × 消耗量(优先使用调整消耗量,否则使用定额消耗量) + java.math.BigDecimal quantity = resource.getAdjustedDosage() != null ? + resource.getAdjustedDosage() : resource.getDosage(); + if (quantity != null) { + if (resourceItem.getTaxExclBasePrice() != null) { + vo.setTaxExclBaseTotalSum(resourceItem.getTaxExclBasePrice().multiply(quantity)); + } + if (resourceItem.getTaxInclBasePrice() != null) { + vo.setTaxInclBaseTotalSum(resourceItem.getTaxInclBasePrice().multiply(quantity)); + } + if (resourceItem.getTaxExclCompilePrice() != null) { + vo.setTaxExclCompileTotalSum(resourceItem.getTaxExclCompilePrice().multiply(quantity)); + } + if (resourceItem.getTaxInclCompilePrice() != null) { + vo.setTaxInclCompileTotalSum(resourceItem.getTaxInclCompilePrice().multiply(quantity)); + } + } + } + } + + return vo; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void batchCreateUnifiedFeeResource(List createReqVOList) { + for (QuotaUnifiedFeeResourceSaveReqVO createReqVO : createReqVOList) { + createUnifiedFeeResource(createReqVO); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteByUnifiedFeeSettingId(Long unifiedFeeSettingId) { + unifiedFeeResourceMapper.deleteByUnifiedFeeSettingId(unifiedFeeSettingId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void swapSort(Long nodeId1, Long nodeId2) { + QuotaUnifiedFeeResourceDO node1 = validateUnifiedFeeResourceExistsAndGet(nodeId1); + QuotaUnifiedFeeResourceDO node2 = validateUnifiedFeeResourceExistsAndGet(nodeId2); + + // 交换排序值 + Integer tempSort = node1.getSortOrder(); + node1.setSortOrder(node2.getSortOrder()); + node2.setSortOrder(tempSort); + + unifiedFeeResourceMapper.updateById(node1); + unifiedFeeResourceMapper.updateById(node2); + } + + @Override + public void validateUnifiedFeeResourceExists(Long id) { + if (unifiedFeeResourceMapper.selectById(id) == null) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.UNIFIED_FEE_RESOURCE_NOT_EXISTS); + } + } + + private QuotaUnifiedFeeResourceDO validateUnifiedFeeResourceExistsAndGet(Long id) { + QuotaUnifiedFeeResourceDO unifiedFeeResource = unifiedFeeResourceMapper.selectById(id); + if (unifiedFeeResource == null) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.UNIFIED_FEE_RESOURCE_NOT_EXISTS); + } + return unifiedFeeResource; + } + + @Override + public com.yhy.module.core.controller.admin.resource.vo.ResourceItemRespVO getResourceItemByCode(Long unifiedFeeSettingId, String code) { + // 1. 获取统一取费设置绑定的工料机专业ID + Long categoryTreeId = getCategoryTreeIdByUnifiedFeeSetting(unifiedFeeSettingId); + + // 2. 根据编码精确查询工料机 + com.yhy.module.core.dal.dataobject.resource.ResourceItemDO resourceItem = + resourceItemMapper.selectByCategoryTreeIdAndCode(categoryTreeId, code); + + // 3. 如果未找到返回null + if (resourceItem == null) { + return null; + } + + // 4. 转换为 VO + return convertResourceItemToVO(resourceItem); + } + + @Override + public void validateResourceInScope(Long unifiedFeeSettingId, Long resourceItemId) { + // 1. 获取统一取费设置绑定的工料机专业ID + Long allowedCategoryTreeId = getCategoryTreeIdByUnifiedFeeSetting(unifiedFeeSettingId); + + // 2. 获取工料机数据 + com.yhy.module.core.dal.dataobject.resource.ResourceItemDO resourceItem = resourceItemMapper.selectById(resourceItemId); + if (resourceItem == null) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.RESOURCE_ITEM_NOT_EXISTS); + } + + // 3. 获取工料机所属的目录节点(yhy_resource_catalog_item 表) + com.yhy.module.core.dal.dataobject.resource.ResourceCatalogItemDO catalogItem = + resourceCatalogItemMapper.selectById(resourceItem.getCatalogItemId()); + if (catalogItem == null) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.RESOURCE_CATALOG_ITEM_NOT_EXISTS); + } + + // 4. 验证工料机是否在范围内 + if (!allowedCategoryTreeId.equals(catalogItem.getCategoryTreeId())) { + log.warn("[validateResourceInScope] 工料机不在统一取费设置的数据范围内," + + "unifiedFeeSettingId={}, resourceItemId={}, allowedCategoryTreeId={}, actualCategoryTreeId={}", + unifiedFeeSettingId, resourceItemId, allowedCategoryTreeId, catalogItem.getCategoryTreeId()); + + throw ServiceExceptionUtil.exception(ErrorCodeConstants.QUOTA_RESOURCE_OUT_OF_SCOPE); + } + + log.debug("[validateResourceInScope] 工料机在范围内,unifiedFeeSettingId={}, resourceItemId={}", + unifiedFeeSettingId, resourceItemId); + } + + @Override + public java.util.List getAvailableResourceItemsWithFilter( + Long unifiedFeeSettingId, String code, String name, String spec) { + // 1. 获取统一取费设置绑定的工料机专业ID + Long categoryTreeId = getCategoryTreeIdByUnifiedFeeSetting(unifiedFeeSettingId); + + // 2. 查询该专业下的所有工料机(支持模糊查询) + java.util.List resourceItems = + resourceItemMapper.selectByCategoryTreeIdWithFilter(categoryTreeId, code, name, spec); + + // 3. 转换为 VO + return resourceItems.stream() + .map(this::convertResourceItemToVO) + .collect(java.util.stream.Collectors.toList()); + } + + /** + * 获取统一取费设置绑定的工料机专业ID + * + * 逻辑:统一取费设置 → catalog_item_id(费率模式节点) → 向上查找定额专业节点 → category_tree_id + * + * 注意:费率模式节点(rate_mode)本身可能没有绑定工料机专业,需要向上查找父节点(specialty)的绑定 + */ + private Long getCategoryTreeIdByUnifiedFeeSetting(Long unifiedFeeSettingId) { + // 1. 获取统一取费设置 + com.yhy.module.core.dal.dataobject.quota.QuotaUnifiedFeeSettingDO unifiedFeeSetting = + unifiedFeeSettingMapper.selectById(unifiedFeeSettingId); + if (unifiedFeeSetting == null) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.UNIFIED_FEE_SETTING_NOT_EXISTS); + } + + // 2. 获取费率模式节点(catalog_item_id 指向 yhy_quota_catalog_item) + com.yhy.module.core.dal.dataobject.quota.QuotaCatalogItemDO catalogItem = + quotaCatalogItemMapper.selectById(unifiedFeeSetting.getCatalogItemId()); + if (catalogItem == null) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.QUOTA_CATALOG_ITEM_NOT_EXISTS); + } + + // 3. 如果当前节点有绑定,直接返回 + if (catalogItem.getCategoryTreeId() != null) { + return catalogItem.getCategoryTreeId(); + } + + // 4. 如果当前节点没有绑定,向上查找父节点的绑定(最多查找10层防止死循环) + com.yhy.module.core.dal.dataobject.quota.QuotaCatalogItemDO currentNode = catalogItem; + for (int i = 0; i < 10 && currentNode.getParentId() != null; i++) { + com.yhy.module.core.dal.dataobject.quota.QuotaCatalogItemDO parentNode = + quotaCatalogItemMapper.selectById(currentNode.getParentId()); + if (parentNode == null) { + break; + } + if (parentNode.getCategoryTreeId() != null) { + log.debug("[getCategoryTreeIdByUnifiedFeeSetting] 从父节点获取categoryTreeId, " + + "unifiedFeeSettingId={}, parentNodeId={}, categoryTreeId={}", + unifiedFeeSettingId, parentNode.getId(), parentNode.getCategoryTreeId()); + return parentNode.getCategoryTreeId(); + } + currentNode = parentNode; + } + + // 5. 如果向上查找也没有找到,抛出异常 + throw ServiceExceptionUtil.exception(ErrorCodeConstants.QUOTA_SPECIALTY_NOT_BOUND); + } + + /** + * 转换 ResourceItemDO 为 ResourceItemRespVO + */ + private com.yhy.module.core.controller.admin.resource.vo.ResourceItemRespVO convertResourceItemToVO( + com.yhy.module.core.dal.dataobject.resource.ResourceItemDO resourceItem) { + com.yhy.module.core.controller.admin.resource.vo.ResourceItemRespVO vo = + new com.yhy.module.core.controller.admin.resource.vo.ResourceItemRespVO(); + BeanUtil.copyProperties(resourceItem, vo); + return vo; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaUnifiedFeeServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaUnifiedFeeServiceImpl.java new file mode 100644 index 0000000..6a7c9c2 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaUnifiedFeeServiceImpl.java @@ -0,0 +1,226 @@ +package com.yhy.module.core.service.quota.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; +import com.yhy.module.core.controller.admin.quota.vo.QuotaFeeItemWithRateRespVO; +import com.yhy.module.core.controller.admin.quota.vo.QuotaUnifiedFeeSaveReqVO; +import com.yhy.module.core.dal.dataobject.quota.QuotaFeeItemDO; +import com.yhy.module.core.dal.dataobject.quota.QuotaUnifiedFeeDO; +import com.yhy.module.core.dal.mysql.quota.QuotaFeeItemMapper; +import com.yhy.module.core.dal.mysql.quota.QuotaUnifiedFeeMapper; +import com.yhy.module.core.enums.ErrorCodeConstants; +import com.yhy.module.core.service.quota.QuotaFeeItemService; +import com.yhy.module.core.service.quota.QuotaUnifiedFeeService; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * 统一取费单价 Service 实现类 + */ +@Service +@Validated +@Slf4j +public class QuotaUnifiedFeeServiceImpl implements QuotaUnifiedFeeService { + + @Resource + private QuotaUnifiedFeeMapper unifiedFeeMapper; + + @Resource + private QuotaFeeItemMapper feeItemMapper; + + @Resource + private QuotaFeeItemService quotaFeeItemService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createUnifiedFee(QuotaUnifiedFeeSaveReqVO createReqVO) { + // 设置默认节点类型 + if (createReqVO.getNodeType() == null) { + createReqVO.setNodeType(createReqVO.getParentId() == null ? + QuotaUnifiedFeeDO.NODE_TYPE_PARENT : QuotaUnifiedFeeDO.NODE_TYPE_CHILD); + } + + // 设置排序值 + if (createReqVO.getSortOrder() == null) { + Integer maxSortOrder = unifiedFeeMapper.selectMaxSortOrder( + createReqVO.getCatalogItemId(), createReqVO.getParentId()); + createReqVO.setSortOrder(maxSortOrder + 1); + } + + // 转换并插入 + QuotaUnifiedFeeDO unifiedFee = BeanUtil.copyProperties(createReqVO, QuotaUnifiedFeeDO.class); + unifiedFeeMapper.insert(unifiedFee); + return unifiedFee.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateUnifiedFee(QuotaUnifiedFeeSaveReqVO updateReqVO) { + // 验证计算基数格式和公式语法 + if (updateReqVO.getCalcBase() != null) { + quotaFeeItemService.validateCalcBase(updateReqVO.getCalcBase()); + } + + QuotaUnifiedFeeDO updateObj; + + // 如果id为空但feeItemId不为空,通过feeItemId查找 + if (updateReqVO.getId() == null && updateReqVO.getFeeItemId() != null && updateReqVO.getCatalogItemId() != null) { + // 通过feeItemId和catalogItemId查找 + QuotaUnifiedFeeDO existing = unifiedFeeMapper.selectByFeeItemIdAndCatalogItemId( + updateReqVO.getFeeItemId(), updateReqVO.getCatalogItemId()); + + if (existing != null) { + // 存在则更新 + updateObj = BeanUtil.copyProperties(updateReqVO, QuotaUnifiedFeeDO.class); + updateObj.setId(existing.getId()); + } else { + // 不存在则创建 + updateObj = new QuotaUnifiedFeeDO(); + updateObj.setCatalogItemId(updateReqVO.getCatalogItemId()); + updateObj.setFeeItemId(updateReqVO.getFeeItemId()); + updateObj.setCalcBase(updateReqVO.getCalcBase()); + updateObj.setSortOrder(0); + unifiedFeeMapper.insert(updateObj); + return; + } + } else { + // 校验存在 + validateUnifiedFeeExists(updateReqVO.getId()); + updateObj = BeanUtil.copyProperties(updateReqVO, QuotaUnifiedFeeDO.class); + } + + // 更新 + unifiedFeeMapper.updateById(updateObj); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteUnifiedFee(Long id) { + // 校验存在 + QuotaUnifiedFeeDO unifiedFee = validateUnifiedFeeExistsAndGet(id); + + // 如果有子节点,需要先删除子节点 + if (unifiedFeeMapper.hasChildren(id)) { + List children = unifiedFeeMapper.selectChildListByParentId(id); + for (QuotaUnifiedFeeDO child : children) { + unifiedFeeMapper.deleteById(child.getId()); + } + } + + // 删除本节点 + unifiedFeeMapper.deleteById(id); + } + + @Override + public QuotaUnifiedFeeDO getUnifiedFee(Long id) { + return unifiedFeeMapper.selectById(id); + } + + @Override + public List getUnifiedFeeList(Long catalogItemId) { + return unifiedFeeMapper.selectListByCatalogItemId(catalogItemId); + } + + @Override + public List getUnifiedFeeTree(Long catalogItemId) { + // 1. 获取定额取费的费率项+取费项合并视图(与定额取费页面显示一致) + List feeItemWithRateList = quotaFeeItemService.getFeeItemWithRateList(catalogItemId); + + // 2. 获取统一取费单价的calcBase覆盖值 + List unifiedFees = unifiedFeeMapper.selectListByCatalogItemId(catalogItemId); + Map unifiedFeeByFeeItemIdMap = unifiedFees.stream() + .filter(item -> item.getFeeItemId() != null) + .collect(Collectors.toMap(QuotaUnifiedFeeDO::getFeeItemId, item -> item, (a, b) -> a)); + + // 3. 合并calcBase覆盖值(如果统一取费单价有覆盖,则使用覆盖值) + mergeCalcBaseOverrides(feeItemWithRateList, unifiedFeeByFeeItemIdMap); + + return feeItemWithRateList; + } + + /** + * 递归合并calcBase覆盖值 + */ + private void mergeCalcBaseOverrides(List items, Map unifiedFeeMap) { + if (items == null) return; + for (QuotaFeeItemWithRateRespVO item : items) { + if (item.getFeeItemId() != null) { + QuotaUnifiedFeeDO unifiedFee = unifiedFeeMap.get(item.getFeeItemId()); + if (unifiedFee != null && unifiedFee.getCalcBase() != null) { + // 使用统一取费单价的覆盖值 + item.setCalcBase(unifiedFee.getCalcBase()); + } + } + // 递归处理子节点 + mergeCalcBaseOverrides(item.getChildren(), unifiedFeeMap); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void initFromFeeItems(Long catalogItemId) { + // 获取定额取费项 + List feeItems = feeItemMapper.selectListByCatalogItemId(catalogItemId); + if (feeItems.isEmpty()) { + return; + } + + // 获取已存在的统一取费单价(按fee_item_id) + List existingItems = unifiedFeeMapper.selectListByCatalogItemId(catalogItemId); + java.util.Set existingFeeItemIds = existingItems.stream() + .filter(item -> item.getFeeItemId() != null) + .map(QuotaUnifiedFeeDO::getFeeItemId) + .collect(Collectors.toSet()); + + // 为每个定额取费项创建对应的统一取费单价(如果不存在) + int sortOrder = existingItems.size(); + for (QuotaFeeItemDO feeItem : feeItems) { + if (!existingFeeItemIds.contains(feeItem.getId())) { + QuotaUnifiedFeeDO unifiedFee = new QuotaUnifiedFeeDO(); + unifiedFee.setCatalogItemId(catalogItemId); + unifiedFee.setFeeItemId(feeItem.getId()); + // 不复制其他字段,从定额取费动态获取 + unifiedFee.setSortOrder(++sortOrder); + unifiedFeeMapper.insert(unifiedFee); + log.debug("[initFromFeeItems] 创建统一取费单价, catalogItemId={}, feeItemId={}", + catalogItemId, feeItem.getId()); + } + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void swapSort(Long nodeId1, Long nodeId2) { + QuotaUnifiedFeeDO node1 = validateUnifiedFeeExistsAndGet(nodeId1); + QuotaUnifiedFeeDO node2 = validateUnifiedFeeExistsAndGet(nodeId2); + + // 交换排序值 + Integer tempSort = node1.getSortOrder(); + node1.setSortOrder(node2.getSortOrder()); + node2.setSortOrder(tempSort); + + unifiedFeeMapper.updateById(node1); + unifiedFeeMapper.updateById(node2); + } + + @Override + public void validateUnifiedFeeExists(Long id) { + if (unifiedFeeMapper.selectById(id) == null) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.UNIFIED_FEE_NOT_EXISTS); + } + } + + private QuotaUnifiedFeeDO validateUnifiedFeeExistsAndGet(Long id) { + QuotaUnifiedFeeDO unifiedFee = unifiedFeeMapper.selectById(id); + if (unifiedFee == null) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.UNIFIED_FEE_NOT_EXISTS); + } + return unifiedFee; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaUnifiedFeeSettingServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaUnifiedFeeSettingServiceImpl.java new file mode 100644 index 0000000..ebffd1d --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaUnifiedFeeSettingServiceImpl.java @@ -0,0 +1,308 @@ +package com.yhy.module.core.service.quota.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; +import com.yhy.module.core.controller.admin.quota.vo.QuotaUnifiedFeeSettingRespVO; +import com.yhy.module.core.controller.admin.quota.vo.QuotaUnifiedFeeSettingSaveReqVO; +import com.yhy.module.core.dal.dataobject.quota.QuotaUnifiedFeeSettingDO; +import com.yhy.module.core.dal.mysql.quota.QuotaUnifiedFeeSettingMapper; +import com.yhy.module.core.enums.ErrorCodeConstants; +import com.yhy.module.core.service.quota.QuotaUnifiedFeeResourceService; +import com.yhy.module.core.service.quota.QuotaUnifiedFeeSettingService; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * 统一取费设置 Service 实现类 + */ +@Service +@Validated +@Slf4j +public class QuotaUnifiedFeeSettingServiceImpl implements QuotaUnifiedFeeSettingService { + + @Resource + private QuotaUnifiedFeeSettingMapper unifiedFeeSettingMapper; + + @Resource + @Lazy + private QuotaUnifiedFeeResourceService unifiedFeeResourceService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createUnifiedFeeSetting(QuotaUnifiedFeeSettingSaveReqVO createReqVO) { + // 设置默认节点类型 + if (createReqVO.getNodeType() == null) { + createReqVO.setNodeType(createReqVO.getParentId() == null ? + QuotaUnifiedFeeSettingDO.NODE_TYPE_PARENT : QuotaUnifiedFeeSettingDO.NODE_TYPE_CHILD); + } + + // 验证取费章节不能与其他行有交集 + validateFeeChapterNoIntersection(createReqVO.getCatalogItemId(), null, createReqVO.getFeeChapter()); + + // 设置排序值 + if (createReqVO.getSortOrder() == null) { + // 没有指定 sortOrder,追加到末尾 + Integer maxSortOrder = unifiedFeeSettingMapper.selectMaxSortOrder( + createReqVO.getCatalogItemId(), createReqVO.getParentId()); + createReqVO.setSortOrder(maxSortOrder + 1); + } else { + // 指定了 sortOrder,需要将该位置及之后的记录的 sortOrder 都 +1 + unifiedFeeSettingMapper.incrementSortOrderFrom( + createReqVO.getCatalogItemId(), createReqVO.getParentId(), createReqVO.getSortOrder()); + } + + // 转换并插入 + QuotaUnifiedFeeSettingDO unifiedFeeSetting = BeanUtil.copyProperties(createReqVO, QuotaUnifiedFeeSettingDO.class); + unifiedFeeSettingMapper.insert(unifiedFeeSetting); + return unifiedFeeSetting.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateUnifiedFeeSetting(QuotaUnifiedFeeSettingSaveReqVO updateReqVO) { + // 校验存在 + QuotaUnifiedFeeSettingDO existing = validateUnifiedFeeSettingExistsAndGet(updateReqVO.getId()); + + // 验证取费章节不能与其他行有交集 + validateFeeChapterNoIntersection(existing.getCatalogItemId(), updateReqVO.getId(), updateReqVO.getFeeChapter()); + + // 更新 + QuotaUnifiedFeeSettingDO updateObj = BeanUtil.copyProperties(updateReqVO, QuotaUnifiedFeeSettingDO.class); + unifiedFeeSettingMapper.updateById(updateObj); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteUnifiedFeeSetting(Long id) { + // 校验存在 + QuotaUnifiedFeeSettingDO unifiedFeeSetting = validateUnifiedFeeSettingExistsAndGet(id); + + // 如果是父定额,需要删除所有子定额及其工料机 + if (unifiedFeeSetting.isParent()) { + List children = unifiedFeeSettingMapper.selectChildListByParentId(id); + for (QuotaUnifiedFeeSettingDO child : children) { + // 删除子定额的工料机 + unifiedFeeResourceService.deleteByUnifiedFeeSettingId(child.getId()); + // 删除子定额 + unifiedFeeSettingMapper.deleteById(child.getId()); + } + } else { + // 如果是子定额,删除其工料机 + unifiedFeeResourceService.deleteByUnifiedFeeSettingId(id); + } + + // 删除本节点 + unifiedFeeSettingMapper.deleteById(id); + } + + @Override + public QuotaUnifiedFeeSettingDO getUnifiedFeeSetting(Long id) { + return unifiedFeeSettingMapper.selectById(id); + } + + @Override + public List getUnifiedFeeSettingList(Long catalogItemId) { + return unifiedFeeSettingMapper.selectListByCatalogItemId(catalogItemId); + } + + @Override + public List getUnifiedFeeSettingTree(Long catalogItemId) { + // 获取所有节点 + List allItems = unifiedFeeSettingMapper.selectListByCatalogItemId(catalogItemId); + + // 转换为VO + List allVOs = allItems.stream() + .map(item -> BeanUtil.copyProperties(item, QuotaUnifiedFeeSettingRespVO.class)) + .collect(Collectors.toList()); + + // 构建树 + return buildTree(allVOs); + } + + @Override + public List getParentList(Long catalogItemId) { + return unifiedFeeSettingMapper.selectParentListByCatalogItemId(catalogItemId); + } + + @Override + public List getParentListWithChildFeeChapters(Long catalogItemId) { + List allItems = unifiedFeeSettingMapper.selectListByCatalogItemId(catalogItemId); + + // 按parentId分组 + Map> childrenMap = allItems.stream() + .filter(item -> item.getParentId() != null) + .collect(Collectors.groupingBy(QuotaUnifiedFeeSettingDO::getParentId)); + + // 构建树形结构:directory → fee(parent) → sub_fee(child) + return allItems.stream() + .filter(item -> item.getParentId() == null) // 一级节点(目录或费用) + .map(rootItem -> buildTreeNode(rootItem, childrenMap)) + .collect(Collectors.toList()); + } + + /** + * 递归构建树节点,并合并子节点的feeChapter + */ + private QuotaUnifiedFeeSettingRespVO buildTreeNode(QuotaUnifiedFeeSettingDO item, Map> childrenMap) { + QuotaUnifiedFeeSettingRespVO vo = BeanUtil.copyProperties(item, QuotaUnifiedFeeSettingRespVO.class); + + List children = childrenMap.get(item.getId()); + if (children != null && !children.isEmpty()) { + // 递归构建子节点 + List childVOs = children.stream() + .map(child -> buildTreeNode(child, childrenMap)) + .collect(Collectors.toList()); + vo.setChildren(childVOs); + + // 合并所有子孙节点的feeChapter到当前节点 + List allChapters = new ArrayList<>(); + collectFeeChapters(childVOs, allChapters); + if (!allChapters.isEmpty()) { + vo.setFeeChapter(cn.hutool.json.JSONUtil.toJsonStr(allChapters)); + } + } + return vo; + } + + /** + * 递归收集所有子孙节点的feeChapter + */ + private void collectFeeChapters(List nodes, List result) { + for (QuotaUnifiedFeeSettingRespVO node : nodes) { + // 收集当前节点的feeChapter + String fc = node.getFeeChapter(); + if (fc != null && !fc.isEmpty() && !"[]".equals(fc)) { + try { + List ids = cn.hutool.json.JSONUtil.toList(fc, String.class); + result.addAll(ids); + } catch (Exception e) { + log.warn("解析feeChapter失败: {}", fc); + } + } + // 递归收集子节点 + if (node.getChildren() != null && !node.getChildren().isEmpty()) { + collectFeeChapters(node.getChildren(), result); + } + } + } + + @Override + public List getChildList(Long parentId) { + return unifiedFeeSettingMapper.selectChildListByParentId(parentId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void swapSort(Long nodeId1, Long nodeId2) { + QuotaUnifiedFeeSettingDO node1 = validateUnifiedFeeSettingExistsAndGet(nodeId1); + QuotaUnifiedFeeSettingDO node2 = validateUnifiedFeeSettingExistsAndGet(nodeId2); + + // 交换排序值 + Integer tempSort = node1.getSortOrder(); + node1.setSortOrder(node2.getSortOrder()); + node2.setSortOrder(tempSort); + + unifiedFeeSettingMapper.updateById(node1); + unifiedFeeSettingMapper.updateById(node2); + } + + @Override + public void validateUnifiedFeeSettingExists(Long id) { + if (unifiedFeeSettingMapper.selectById(id) == null) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.UNIFIED_FEE_SETTING_NOT_EXISTS); + } + } + + private QuotaUnifiedFeeSettingDO validateUnifiedFeeSettingExistsAndGet(Long id) { + QuotaUnifiedFeeSettingDO unifiedFeeSetting = unifiedFeeSettingMapper.selectById(id); + if (unifiedFeeSetting == null) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.UNIFIED_FEE_SETTING_NOT_EXISTS); + } + return unifiedFeeSetting; + } + + /** + * 验证取费章节不能与其他行有交集 + * @param catalogItemId 模式节点ID + * @param excludeId 排除的ID(更新时排除自身) + * @param feeChapter 取费章节JSON数组字符串 + */ + private void validateFeeChapterNoIntersection(Long catalogItemId, Long excludeId, String feeChapter) { + if (feeChapter == null || feeChapter.isEmpty() || "[]".equals(feeChapter)) { + return; + } + + // 解析当前的取费章节ID列表 + List currentIds; + try { + currentIds = cn.hutool.json.JSONUtil.toList(feeChapter, String.class); + } catch (Exception e) { + return; // 解析失败则跳过验证 + } + + if (currentIds == null || currentIds.isEmpty()) { + return; + } + + // 获取同一模式节点下的所有设置 + List allSettings = unifiedFeeSettingMapper.selectListByCatalogItemId(catalogItemId); + + for (QuotaUnifiedFeeSettingDO setting : allSettings) { + // 排除自身 + if (excludeId != null && excludeId.equals(setting.getId())) { + continue; + } + + String existingFeeChapter = setting.getFeeChapter(); + if (existingFeeChapter == null || existingFeeChapter.isEmpty() || "[]".equals(existingFeeChapter)) { + continue; + } + + try { + List existingIds = cn.hutool.json.JSONUtil.toList(existingFeeChapter, String.class); + if (existingIds == null || existingIds.isEmpty()) { + continue; + } + + // 检查交集 + for (String id : currentIds) { + if (existingIds.contains(id)) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.UNIFIED_FEE_CHAPTER_INTERSECTION, + setting.getName() != null ? setting.getName() : setting.getCode()); + } + } + } catch (cn.iocoder.yudao.framework.common.exception.ServiceException e) { + throw e; // 重新抛出业务异常 + } catch (Exception e) { + // 解析失败则跳过 + } + } + } + + private List buildTree(List allVOs) { + // 按parentId分组 + Map> childrenMap = allVOs.stream() + .filter(vo -> vo.getParentId() != null) + .collect(Collectors.groupingBy(QuotaUnifiedFeeSettingRespVO::getParentId)); + + // 设置子节点 + for (QuotaUnifiedFeeSettingRespVO vo : allVOs) { + List children = childrenMap.get(vo.getId()); + vo.setChildren(children != null ? children : new ArrayList<>()); + vo.setHasChildren(children != null && !children.isEmpty()); + } + + // 返回根节点列表 + return allVOs.stream() + .filter(vo -> vo.getParentId() == null) + .collect(Collectors.toList()); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaVariableSettingServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaVariableSettingServiceImpl.java new file mode 100644 index 0000000..ac4360d --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/quota/impl/QuotaVariableSettingServiceImpl.java @@ -0,0 +1,981 @@ +package com.yhy.module.core.service.quota.impl; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_CATALOG_ITEM_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_VARIABLE_SETTING_CALC_BASE_INVALID; +import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_VARIABLE_SETTING_CALC_BASE_VARIABLE_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_VARIABLE_SETTING_CODE_DUPLICATE; +import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_VARIABLE_SETTING_CODE_DUPLICATE_ACROSS_TABS; +import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_VARIABLE_SETTING_CODE_FORMAT_INVALID; +import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_VARIABLE_SETTING_INVALID_CATEGORY; +import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_VARIABLE_SETTING_INVALID_NODE_TYPE; +import static com.yhy.module.core.enums.ErrorCodeConstants.QUOTA_VARIABLE_SETTING_NOT_EXISTS; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import com.yhy.module.core.controller.admin.quota.vo.QuotaVariableSettingRespVO; +import com.yhy.module.core.controller.admin.quota.vo.QuotaVariableSettingSaveReqVO; +import com.yhy.module.core.dal.dataobject.quota.QuotaCatalogItemDO; +import com.yhy.module.core.dal.dataobject.quota.QuotaFeeItemDO; +import com.yhy.module.core.dal.dataobject.quota.QuotaVariableSettingDO; +import com.yhy.module.core.dal.dataobject.resource.ResourceCategoryDO; +import com.yhy.module.core.dal.mysql.quota.QuotaCatalogItemMapper; +import com.yhy.module.core.dal.mysql.quota.QuotaFeeItemMapper; +import com.yhy.module.core.dal.mysql.quota.QuotaVariableSettingMapper; +import com.yhy.module.core.dal.mysql.resource.ResourceCategoryMapper; +import com.yhy.module.core.service.quota.QuotaVariableSettingService; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * 单位工程变量设置 Service 实现类 + */ +@Service +@Validated +@Slf4j +public class QuotaVariableSettingServiceImpl implements QuotaVariableSettingService { + + private static final Set VALID_CATEGORIES = new HashSet<>(Arrays.asList( + QuotaVariableSettingDO.CategoryEnum.DIVISION, + QuotaVariableSettingDO.CategoryEnum.MEASURE, + QuotaVariableSettingDO.CategoryEnum.OTHER, + QuotaVariableSettingDO.CategoryEnum.UNIT_SUMMARY + )); + + @Resource + private QuotaVariableSettingMapper quotaVariableSettingMapper; + + @Resource + private QuotaCatalogItemMapper quotaCatalogItemMapper; + + @Resource + private ResourceCategoryMapper resourceCategoryMapper; + + @Resource + private QuotaFeeItemMapper quotaFeeItemMapper; + + @Resource + private com.yhy.module.core.service.workbench.WbBoqDivisionService wbBoqDivisionService; + + @Resource + private com.yhy.module.core.service.workbench.WbBoqResourceService wbBoqResourceService; + + @Resource + private com.yhy.module.core.dal.mysql.workbench.WbBoqResourceMapper wbBoqResourceMapper; + + @Resource + private com.yhy.module.core.service.workbench.WbSnapshotReadService wbSnapshotReadService; + + @Resource + private com.yhy.module.core.dal.mysql.resource.ResourceItemMapper resourceItemMapper; + + @Resource + private com.yhy.module.core.service.workbench.QuotaPriceCalculatorService quotaPriceCalculatorService; + + @Resource + private com.yhy.module.core.dal.mysql.workbench.WbUnitInfoMapper wbUnitInfoMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createVariableSetting(QuotaVariableSettingSaveReqVO createReqVO) { + // 1. 验证定额专业节点 + validateSpecialtyNode(createReqVO.getCatalogItemId()); + + // 2. 验证类别 + validateCategory(createReqVO.getCategory()); + + // 3. 验证代号格式和唯一性 + if (StrUtil.isNotBlank(createReqVO.getCode())) { + validateCodeFormat(createReqVO.getCode()); + validateCodeUnique(null, createReqVO.getCatalogItemId(), createReqVO.getCategory(), createReqVO.getCode()); + // 跨标签页唯一性校验:四个标签页不允许出现相同的费用代号 + validateCodeUniqueAcrossCategories(null, createReqVO.getCatalogItemId(), createReqVO.getCategory(), createReqVO.getCode()); + } + + // 4. 设置排序值(支持基于参考节点的插入位置) + Integer sortOrder = createReqVO.getSortOrder(); + if (sortOrder == null) { + Long referenceNodeId = createReqVO.getReferenceNodeId(); + String insertPosition = createReqVO.getInsertPosition(); + + if (referenceNodeId != null && insertPosition != null) { + // 基于参考节点计算排序值 + QuotaVariableSettingDO referenceNode = quotaVariableSettingMapper.selectById(referenceNodeId); + if (referenceNode != null) { + int refSortOrder = referenceNode.getSortOrder(); + if ("above".equals(insertPosition)) { + // 在参考节点上方插入:使用参考节点的排序值,并将参考节点及其后的节点排序值+1 + sortOrder = refSortOrder; + quotaVariableSettingMapper.incrementSortOrderFrom( + createReqVO.getCatalogItemId(), createReqVO.getCategory(), refSortOrder); + } else if ("below".equals(insertPosition)) { + // 在参考节点下方插入:使用参考节点排序值+1,并将后续节点排序值+1 + sortOrder = refSortOrder + 1; + quotaVariableSettingMapper.incrementSortOrderFrom( + createReqVO.getCatalogItemId(), createReqVO.getCategory(), refSortOrder + 1); + } else { + // 默认追加到末尾 + sortOrder = quotaVariableSettingMapper.selectMaxSortOrder( + createReqVO.getCatalogItemId(), createReqVO.getCategory()) + 1; + } + } else { + sortOrder = quotaVariableSettingMapper.selectMaxSortOrder( + createReqVO.getCatalogItemId(), createReqVO.getCategory()) + 1; + } + } else { + sortOrder = quotaVariableSettingMapper.selectMaxSortOrder( + createReqVO.getCatalogItemId(), createReqVO.getCategory()) + 1; + } + } + createReqVO.setSortOrder(sortOrder); + + // 5. 验证计算基数 + if (createReqVO.getCalcBase() != null) { + validateCalcBase(createReqVO.getCalcBase(), createReqVO.getCatalogItemId(), createReqVO.getCategory()); + } + + // 6. 插入数据 + QuotaVariableSettingDO variableSetting = BeanUtils.toBean(createReqVO, QuotaVariableSettingDO.class); + quotaVariableSettingMapper.insert(variableSetting); + + return variableSetting.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateVariableSetting(QuotaVariableSettingSaveReqVO updateReqVO) { + // 1. 验证存在 + QuotaVariableSettingDO existingItem = quotaVariableSettingMapper.selectById(updateReqVO.getId()); + if (existingItem == null) { + throw exception(QUOTA_VARIABLE_SETTING_NOT_EXISTS); + } + + // 2. 验证定额专业节点 + validateSpecialtyNode(updateReqVO.getCatalogItemId()); + + // 3. 验证类别 + validateCategory(updateReqVO.getCategory()); + + // 4. 验证代号格式和唯一性 + if (StrUtil.isNotBlank(updateReqVO.getCode())) { + validateCodeFormat(updateReqVO.getCode()); + validateCodeUnique(updateReqVO.getId(), updateReqVO.getCatalogItemId(), + updateReqVO.getCategory(), updateReqVO.getCode()); + // 跨标签页唯一性校验:四个标签页不允许出现相同的费用代号 + validateCodeUniqueAcrossCategories(updateReqVO.getId(), updateReqVO.getCatalogItemId(), + updateReqVO.getCategory(), updateReqVO.getCode()); + } + + // 5. 验证计算基数 + if (updateReqVO.getCalcBase() != null) { + validateCalcBase(updateReqVO.getCalcBase(), updateReqVO.getCatalogItemId(), updateReqVO.getCategory()); + } + + // 6. 更新数据 + QuotaVariableSettingDO updateObj = BeanUtils.toBean(updateReqVO, QuotaVariableSettingDO.class); + quotaVariableSettingMapper.updateById(updateObj); + } + + /** + * 验证计算基数 + * @param calcBase 计算基数 + * @param catalogItemId 模式节点ID + * @param category 类别 + */ + private void validateCalcBase(Map calcBase, Long catalogItemId, String category) { + if (calcBase == null) { + return; + } + + String formula = (String) calcBase.get("formula"); + // 公式可以为空,表示清空计算基数 + if (StrUtil.isBlank(formula)) { + return; + } + + // 验证公式语法:只允许字母、数字、运算符和括号 + String validPattern = "^[A-Za-z0-9_+\\-*/().\\s]+$"; + if (!formula.matches(validPattern)) { + throw exception(QUOTA_VARIABLE_SETTING_CALC_BASE_INVALID); + } + + // 验证公式中的变量是否有效 + validateFormulaVariables(formula, catalogItemId, category); + } + + /** + * 验证公式中的变量是否有效 + * 参考定额费率的验证逻辑,验证类别价格代码(如 DCLF、DRGF 等) + * @param formula 公式 + * @param catalogItemId 模式节点ID + * @param category 类别 + */ + private void validateFormulaVariables(String formula, Long catalogItemId, String category) { + // 1. 提取公式中的所有变量(变量由字母、数字、下划线组成,但必须以字母开头) + java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("[A-Za-z][A-Za-z0-9_]*"); + java.util.regex.Matcher matcher = pattern.matcher(formula); + List potentialCodes = new java.util.ArrayList<>(); + while (matcher.find()) { + potentialCodes.add(matcher.group()); + } + + if (potentialCodes.isEmpty()) { + return; // 没有变量,只有数字和运算符 + } + + // 2. 获取所有有效的变量代码 + Set validCodes = new HashSet<>(); + + // 2.1 获取同一定额专业节点下同一类别的所有有效费用代号(手动配置) + List settings = quotaVariableSettingMapper.selectListByCatalogItemIdAndCategory(catalogItemId, category); + for (QuotaVariableSettingDO setting : settings) { + if (StrUtil.isNotBlank(setting.getCode())) { + validCodes.add(setting.getCode()); + } + } + + // 2.2 获取定额专业节点下所有费率模式子节点的定额取费变量代号 + List rateModeIds = quotaCatalogItemMapper.selectChildIds(catalogItemId); + List feeItems = quotaFeeItemMapper.selectVariableListByCatalogItemIds(rateModeIds); + for (QuotaFeeItemDO feeItem : feeItems) { + if (StrUtil.isNotBlank(feeItem.getCode())) { + validCodes.add(feeItem.getCode()); + } + } + + // 2.3 获取所有类别价格代码(从机类字典获取,如 DCLF、DRGF 等) + List categories = resourceCategoryMapper.selectList(); + for (ResourceCategoryDO cat : categories) { + // 添加4个价格代码 + if (StrUtil.isNotBlank(cat.getTaxExclBaseCode())) { + validCodes.add(cat.getTaxExclBaseCode()); + } + if (StrUtil.isNotBlank(cat.getTaxInclBaseCode())) { + validCodes.add(cat.getTaxInclBaseCode()); + } + if (StrUtil.isNotBlank(cat.getTaxExclCompileCode())) { + validCodes.add(cat.getTaxExclCompileCode()); + } + if (StrUtil.isNotBlank(cat.getTaxInclCompileCode())) { + validCodes.add(cat.getTaxInclCompileCode()); + } + } + + // 3. 检查公式中的变量是否都有效 + List invalidCodes = new java.util.ArrayList<>(); + for (String code : potentialCodes) { + if (StrUtil.isNotBlank(code) && !validCodes.contains(code)) { + invalidCodes.add(code); + } + } + + if (!invalidCodes.isEmpty()) { + throw exception(QUOTA_VARIABLE_SETTING_CALC_BASE_VARIABLE_NOT_EXISTS, String.join(", ", invalidCodes)); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteVariableSetting(Long id) { + // 1. 验证存在 + QuotaVariableSettingDO variableSetting = quotaVariableSettingMapper.selectById(id); + if (variableSetting == null) { + throw exception(QUOTA_VARIABLE_SETTING_NOT_EXISTS); + } + + // 2. 删除 + quotaVariableSettingMapper.deleteById(id); + } + + @Override + public QuotaVariableSettingDO getVariableSetting(Long id) { + return quotaVariableSettingMapper.selectById(id); + } + + @Override + public List getVariableSettingList(Long catalogItemId, String category) { + return quotaVariableSettingMapper.selectListByCatalogItemIdAndCategory(catalogItemId, category); + } + + @Override + public List getVariableSettingListWithFeeItems(Long catalogItemId, String category) { + List result = new ArrayList<>(); + + // 1. 获取手动配置的变量设置(现在绑定到定额专业节点) + List manualSettings = quotaVariableSettingMapper.selectListByCatalogItemIdAndCategory(catalogItemId, category); + for (QuotaVariableSettingDO setting : manualSettings) { + QuotaVariableSettingRespVO vo = BeanUtils.toBean(setting, QuotaVariableSettingRespVO.class); + vo.setSource("manual"); + result.add(vo); + } + + // 2. 获取定额专业节点下所有费率模式子节点的ID + List rateModeIds = quotaCatalogItemMapper.selectChildIds(catalogItemId); + + // 3. 获取所有费率模式下 variable=true 的定额取费数据 + List feeItems = quotaFeeItemMapper.selectVariableListByCatalogItemIds(rateModeIds); + + // 收集已有的代号,避免重复 + Set existingCodes = new HashSet<>(); + for (QuotaVariableSettingDO setting : manualSettings) { + if (StrUtil.isNotBlank(setting.getCode())) { + existingCodes.add(setting.getCode()); + } + } + + // 将定额取费数据转换为变量设置格式 + for (QuotaFeeItemDO feeItem : feeItems) { + // 跳过已存在相同代号的数据(手动配置优先) + if (StrUtil.isNotBlank(feeItem.getCode()) && existingCodes.contains(feeItem.getCode())) { + continue; + } + + QuotaVariableSettingRespVO vo = new QuotaVariableSettingRespVO(); + vo.setId(feeItem.getId()); + vo.setCatalogItemId(catalogItemId); // 使用定额专业节点ID + vo.setCategory(category); + vo.setName(feeItem.getName()); + vo.setCode(feeItem.getCode()); + vo.setCalcBase(feeItem.getCalcBase()); + vo.setSortOrder(feeItem.getSortOrder() != null ? feeItem.getSortOrder() + 10000 : 10000); // 排在手动配置之后 + vo.setSource("fee_item"); + result.add(vo); + } + + // 4. 按 sortOrder 排序 + result.sort((a, b) -> { + int sortA = a.getSortOrder() != null ? a.getSortOrder() : 0; + int sortB = b.getSortOrder() != null ? b.getSortOrder() : 0; + return Integer.compare(sortA, sortB); + }); + + return result; + } + + @Override + public List getVariableSettingListAll(Long catalogItemId) { + return quotaVariableSettingMapper.selectListByCatalogItemId(catalogItemId); + } + + @Override + public List getVariableSettingListAllWithFeeItems(Long catalogItemId) { + List result = new ArrayList<>(); + + // 遍历所有类别,分别调用 getVariableSettingListWithFeeItems 获取数据 + // 这样可以确保每个类别的定额取费数据都能正确获取 + for (String category : VALID_CATEGORIES) { + List categorySettings = getVariableSettingListWithFeeItems(catalogItemId, category); + log.info("[getVariableSettingListAllWithFeeItems] 定额专业 {} 类别 {} 的变量设置数量: {}", + catalogItemId, category, categorySettings.size()); + result.addAll(categorySettings); + } + + log.info("[getVariableSettingListAllWithFeeItems] 定额专业 {} 的变量设置总数: {}, 类别分布: {}", + catalogItemId, result.size(), + result.stream().collect(java.util.stream.Collectors.groupingBy( + s -> s.getCategory() != null ? s.getCategory() : "null", + java.util.stream.Collectors.counting()))); + + // 按 category 和 sortOrder 排序 + result.sort((a, b) -> { + // 先按 category 排序 + int categoryCompare = (a.getCategory() != null ? a.getCategory() : "").compareTo(b.getCategory() != null ? b.getCategory() : ""); + if (categoryCompare != 0) { + return categoryCompare; + } + // 再按 sortOrder 排序 + int sortA = a.getSortOrder() != null ? a.getSortOrder() : 0; + int sortB = b.getSortOrder() != null ? b.getSortOrder() : 0; + return Integer.compare(sortA, sortB); + }); + + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void swapSort(Long nodeId1, Long nodeId2) { + QuotaVariableSettingDO item1 = quotaVariableSettingMapper.selectById(nodeId1); + QuotaVariableSettingDO item2 = quotaVariableSettingMapper.selectById(nodeId2); + + if (item1 == null || item2 == null) { + throw exception(QUOTA_VARIABLE_SETTING_NOT_EXISTS); + } + + // 交换排序值 + Integer tempSort = item1.getSortOrder(); + item1.setSortOrder(item2.getSortOrder()); + item2.setSortOrder(tempSort); + + quotaVariableSettingMapper.updateById(item1); + quotaVariableSettingMapper.updateById(item2); + } + + @Override + public void validateVariableSettingExists(Long id) { + if (quotaVariableSettingMapper.selectById(id) == null) { + throw exception(QUOTA_VARIABLE_SETTING_NOT_EXISTS); + } + } + + @Override + public List getVariableSettingListAllWithSummary( + Long catalogItemId, + Long compileTreeId, + List baseNumberRangeIds) { + // 1. 获取变量设置列表(包含取费项) + List result = getVariableSettingListAllWithFeeItems(catalogItemId); + + // 2. 如果没有编制树ID,直接返回(不计算汇总值) + if (compileTreeId == null) { + return result; + } + + // 3. 获取分部分项树数据(含价格计算) + List divisionTree = + wbBoqDivisionService.getTreeWithPrice(compileTreeId); + + // 4. 获取工料机消耗数据,按分类和价格字段汇总 + // 变量代号如 DRGF = 除税人工费,需要通过 calcBase.variables 中的 categoryId 和 priceField 来计算 + Map variableValueMap = buildVariableValueMap( + compileTreeId, + divisionTree, + baseNumberRangeIds != null ? new HashSet<>(baseNumberRangeIds) : null + ); + + log.info("汇总值计算 - 变量值映射: {}", variableValueMap); + + // 5. 为每个变量计算汇总值 + for (QuotaVariableSettingRespVO vo : result) { + java.math.BigDecimal summaryValue = calculateSummaryValue(vo.getCalcBase(), variableValueMap); + vo.setSummaryValue(summaryValue); + } + + return result; + } + + /** + * 构建变量值映射(基数设置弹窗汇总值计算) + * 根据 calcBase.variables 中的配置,从工料机消耗表汇总各变量的值 + * 变量代号格式:D/H + RGF/CLF/JXF/SBF/QT 等 + * D = 除税(tax_excl), H = 含税(tax_incl) + * RGF = 人工费(categoryId=1), CLF = 材料费(categoryId=3), JXF = 机械费(categoryId=4) + * + * 合价计算规则: + * - 用量 = 定额工程量(quotaQty) × 消耗量(consumeQty) + * - 合价 = 用量 × 单价 + * - 子工料机(parentId != null)不参与汇总 + * - 单位为%的工料机不参与汇总(其单价字段为null) + */ + private Map buildVariableValueMap( + Long compileTreeId, + List divisionTree, + Set baseNumberRangeIds) { + Map result = new HashMap<>(); + boolean hasRange = baseNumberRangeIds != null && !baseNumberRangeIds.isEmpty(); + + // 如果有基数范围,先收集范围内的所有节点ID(包括子节点) + Set nodesInRange = new HashSet<>(); + if (hasRange) { + collectNodesInRange(divisionTree, baseNumberRangeIds, nodesInRange, false); + } + + // 收集范围内的定额节点及其工程量 + Map quotaNodeQtyMap = new HashMap<>(); + collectQuotaNodeQtyMap(divisionTree, quotaNodeQtyMap, hasRange, nodesInRange, false); + + // 收集范围内的统一取费节点 + List unifiedFeeNodes = new ArrayList<>(); + collectUnifiedFeeNodes(divisionTree, unifiedFeeNodes, hasRange, nodesInRange, false); + + log.info("汇总值计算 - 定额节点数量: {}, 统一取费节点数量: {}", quotaNodeQtyMap.size(), unifiedFeeNodes.size()); + + // 按分类汇总各价格字段 + // categoryId: 1=人工, 2=主材, 3=材料, 4=机械, 5=设备 + Map> categoryPriceMap = new HashMap<>(); + + // 处理普通定额节点的工料机 + if (!quotaNodeQtyMap.isEmpty()) { + // 从工料机消耗表批量查询原始数据(DO层面,不走简陋的convert) + List allResources = + wbBoqResourceMapper.selectListByDivisionIds(new ArrayList<>(quotaNodeQtyMap.keySet())); + + log.info("汇总值计算 - 工料机数量(含子): {}", allResources != null ? allResources.size() : 0); + + if (allResources != null) { + for (com.yhy.module.core.dal.dataobject.workbench.WbBoqResourceDO resource : allResources) { + // 跳过子工料机(复合工料机只取父值) + if (resource.getParentId() != null) continue; + + Long categoryId = resource.getCategoryId(); + if (categoryId == null) continue; + + // 获取消耗量(优先调整消耗量) + java.math.BigDecimal consumeQty = resource.getAdjustConsumeQty() != null + ? resource.getAdjustConsumeQty() : resource.getConsumeQty(); + if (consumeQty == null) consumeQty = java.math.BigDecimal.ZERO; + + // 获取定额工程量 + java.math.BigDecimal quotaQty = quotaNodeQtyMap.get(resource.getDivisionId()); + if (quotaQty == null) quotaQty = java.math.BigDecimal.ONE; + + // 用量 = 定额工程量 × 消耗量(单位为%的工料机:用量=消耗量) + java.math.BigDecimal usageQty; + if ("%".equals(resource.getUnit())) { + usageQty = consumeQty; + } else { + usageQty = quotaQty.multiply(consumeQty); + } + + Map priceMap = categoryPriceMap.computeIfAbsent(categoryId, k -> new HashMap<>()); + + // 合价 = 用量 × 单价 + addPriceToMap(priceMap, "tax_excl_base_price", resource.getTaxExclBasePrice(), usageQty); + addPriceToMap(priceMap, "tax_incl_base_price", resource.getTaxInclBasePrice(), usageQty); + addPriceToMap(priceMap, "tax_excl_compile_price", resource.getTaxExclCompilePrice(), usageQty); + addPriceToMap(priceMap, "tax_incl_compile_price", resource.getTaxInclCompilePrice(), usageQty); + } + } + } + + // 将统一取费节点的子目工料机也汇总到分类价格映射中 + if (!unifiedFeeNodes.isEmpty()) { + aggregateUnifiedFeeResources(unifiedFeeNodes, compileTreeId, categoryPriceMap); + } + + if (categoryPriceMap.isEmpty()) { + return result; + } + + log.info("汇总值计算 - 分类价格映射: {}", categoryPriceMap); + + // 从工料机总类表查询所有分类,构建变量代号到值的映射 + // 变量代号来源:yhy_resource_category 表的四个代码字段 + // - tax_excl_base_code: 除税基价代码(如 DRGF) + // - tax_incl_base_code: 含税基价代码(如 HDRGF) + // - tax_excl_compile_code: 除税编制价代码(如 RGF) + // - tax_incl_compile_code: 含税编制价代码(如 HRGF) + List categories = resourceCategoryMapper.selectList(); + + for (ResourceCategoryDO category : categories) { + Map priceMap = categoryPriceMap.get(category.getId()); + if (priceMap == null) continue; + + // 除税基价代码 -> tax_excl_base_price + if (cn.hutool.core.util.StrUtil.isNotBlank(category.getTaxExclBaseCode())) { + java.math.BigDecimal value = priceMap.get("tax_excl_base_price"); + if (value != null) { + result.put(category.getTaxExclBaseCode(), value); + } + } + // 含税基价代码 -> tax_incl_base_price + if (cn.hutool.core.util.StrUtil.isNotBlank(category.getTaxInclBaseCode())) { + java.math.BigDecimal value = priceMap.get("tax_incl_base_price"); + if (value != null) { + result.put(category.getTaxInclBaseCode(), value); + } + } + // 除税编制价代码 -> tax_excl_compile_price + if (cn.hutool.core.util.StrUtil.isNotBlank(category.getTaxExclCompileCode())) { + java.math.BigDecimal value = priceMap.get("tax_excl_compile_price"); + if (value != null) { + result.put(category.getTaxExclCompileCode(), value); + } + } + // 含税编制价代码 -> tax_incl_compile_price + if (cn.hutool.core.util.StrUtil.isNotBlank(category.getTaxInclCompileCode())) { + java.math.BigDecimal value = priceMap.get("tax_incl_compile_price"); + if (value != null) { + result.put(category.getTaxInclCompileCode(), value); + } + } + } + + // 收集分部分项的费用代号(costCode)及其对应的合价(amount) + collectCostCodeValues(divisionTree, result); + + log.info("汇总值计算 - 变量值映射: {}", result); + + return result; + } + + /** + * 收集分部分项的费用代号及其对应的合价值 + * 遍历分部分项树,将每个节点的 costCode -> amount 添加到变量映射中 + */ + private void collectCostCodeValues( + List nodes, + Map result) { + if (nodes == null) return; + + for (com.yhy.module.core.controller.admin.workbench.vo.WbBoqDivisionRespVO node : nodes) { + // 如果节点有费用代号且有合价,则添加到映射 + String costCode = node.getCostCode(); + java.math.BigDecimal amount = node.getAmount(); + if (cn.hutool.core.util.StrUtil.isNotBlank(costCode) && amount != null) { + // 费用代号可能重复,使用累加方式 + result.merge(costCode, amount, java.math.BigDecimal::add); + } + + // 递归处理子节点 + if (node.getChildren() != null && !node.getChildren().isEmpty()) { + collectCostCodeValues(node.getChildren(), result); + } + } + } + + /** + * 累加价格到映射 + */ + private void addPriceToMap(Map priceMap, String priceField, + java.math.BigDecimal unitPrice, java.math.BigDecimal qty) { + if (unitPrice == null) return; + java.math.BigDecimal amount = unitPrice.multiply(qty); + priceMap.merge(priceField, amount, java.math.BigDecimal::add); + } + + /** + * 收集范围内的定额节点ID及其工程量(quotaQty) + * 替代原 collectQuotaNodeIds,同时收集 id → qty 映射 + */ + private void collectQuotaNodeQtyMap( + List nodes, + Map quotaNodeQtyMap, + boolean hasRange, + Set nodesInRange, + boolean parentInRange) { + if (nodes == null) return; + + for (com.yhy.module.core.controller.admin.workbench.vo.WbBoqDivisionRespVO node : nodes) { + boolean inRange = !hasRange || parentInRange || nodesInRange.contains(node.getId()); + + // 统一取费节点:递归处理其子节点(不收集自身,由 collectUnifiedFeeNodes 单独处理) + if ("unified_fee".equals(node.getNodeType())) { + if (node.getChildren() != null && !node.getChildren().isEmpty()) { + collectQuotaNodeQtyMap(node.getChildren(), quotaNodeQtyMap, hasRange, nodesInRange, inRange); + } + continue; + } + + // 收集定额节点及其工程量 + if ("quota".equals(node.getNodeType()) && inRange) { + java.math.BigDecimal qty = node.getQty() != null ? node.getQty() : java.math.BigDecimal.ONE; + quotaNodeQtyMap.put(node.getId(), qty); + } + + // 递归处理子节点 + if (node.getChildren() != null && !node.getChildren().isEmpty()) { + collectQuotaNodeQtyMap(node.getChildren(), quotaNodeQtyMap, hasRange, nodesInRange, inRange); + } + } + } + + /** + * 收集范围内的统一取费节点(unified_fee) + * 用于基数设置汇总值计算时,将统一取费涉及的子目工料机也纳入分类汇总 + */ + private void collectUnifiedFeeNodes( + List nodes, + List unifiedFeeNodes, + boolean hasRange, + Set nodesInRange, + boolean parentInRange) { + if (nodes == null) return; + + for (com.yhy.module.core.controller.admin.workbench.vo.WbBoqDivisionRespVO node : nodes) { + boolean inRange = !hasRange || parentInRange || nodesInRange.contains(node.getId()); + + if ("unified_fee".equals(node.getNodeType()) && inRange) { + unifiedFeeNodes.add(node); + } + + // 递归处理子节点 + if (node.getChildren() != null && !node.getChildren().isEmpty()) { + collectUnifiedFeeNodes(node.getChildren(), unifiedFeeNodes, hasRange, nodesInRange, inRange); + } + } + } + + /** + * 将统一取费节点的子目工料机汇总到分类价格映射中 + * + * 统一取费节点没有自己的 yhy_wb_boq_resource 数据,需要溯源到后台统一取费设置的子定额及其子目工料机: + * unified_fee 节点 → attributes.sourceUnifiedFeeSettingId → 快照 WbUnifiedFeeSettingDO + * → 子费用(child) → WbUnifiedFeeResourceDO → ResourceItemDO(获取价格和分类) + */ + private void aggregateUnifiedFeeResources( + List unifiedFeeNodes, + Long compileTreeId, + Map> categoryPriceMap) { + if (unifiedFeeNodes == null || unifiedFeeNodes.isEmpty()) return; + + for (com.yhy.module.core.controller.admin.workbench.vo.WbBoqDivisionRespVO ufNode : unifiedFeeNodes) { + // 通过 QuotaPriceCalculatorService 获取统一取费节点的分类合价 + // 该方法内部会:获取母定额列表 → 获取母定额分类合价 → 用calcBase公式计算%工料机合价 → 按categoryId汇总 + Map ufCategorySums = + quotaPriceCalculatorService.getUnifiedFeeCategorySums(ufNode.getId()); + + if (ufCategorySums == null || ufCategorySums.isEmpty()) { + log.info("[基数汇总-统一取费] 统一取费节点无分类合价, nodeId={}", ufNode.getId()); + continue; + } + + log.info("[基数汇总-统一取费] nodeId={}, 分类合价={}", ufNode.getId(), ufCategorySums); + + // 将统一取费的分类合价合并到总的 categoryPriceMap + for (Map.Entry entry : ufCategorySums.entrySet()) { + Long categoryId = entry.getKey(); + com.yhy.module.core.controller.admin.workbench.vo.price.CategoryPriceSum sum = entry.getValue(); + + Map priceMap = categoryPriceMap.computeIfAbsent(categoryId, k -> new HashMap<>()); + priceMap.merge("tax_excl_base_price", sum.getTaxExclBaseSum() != null ? sum.getTaxExclBaseSum() : java.math.BigDecimal.ZERO, java.math.BigDecimal::add); + priceMap.merge("tax_incl_base_price", sum.getTaxInclBaseSum() != null ? sum.getTaxInclBaseSum() : java.math.BigDecimal.ZERO, java.math.BigDecimal::add); + priceMap.merge("tax_excl_compile_price", sum.getTaxExclCompileSum() != null ? sum.getTaxExclCompileSum() : java.math.BigDecimal.ZERO, java.math.BigDecimal::add); + priceMap.merge("tax_incl_compile_price", sum.getTaxInclCompileSum() != null ? sum.getTaxInclCompileSum() : java.math.BigDecimal.ZERO, java.math.BigDecimal::add); + } + } + } + + /** + * 收集基数范围内的所有节点ID(包括选中节点的所有子节点) + */ + private void collectNodesInRange( + List nodes, + Set selectedIds, + Set result, + boolean parentSelected) { + if (nodes == null) return; + + for (com.yhy.module.core.controller.admin.workbench.vo.WbBoqDivisionRespVO node : nodes) { + boolean isSelected = selectedIds.contains(node.getId()) || parentSelected; + if (isSelected) { + result.add(node.getId()); + } + // 递归处理子节点 + if (node.getChildren() != null && !node.getChildren().isEmpty()) { + collectNodesInRange(node.getChildren(), selectedIds, result, isSelected); + } + } + } + + /** + * 根据计算基数公式计算汇总值 + */ + private java.math.BigDecimal calculateSummaryValue( + Map calcBase, + Map variableValueMap) { + if (calcBase == null) { + return null; + } + + // 获取公式字符串 + Object formulaObj = calcBase.get("formula"); + if (formulaObj == null) { + return null; + } + String formula = formulaObj.toString().trim(); + if (StrUtil.isBlank(formula)) { + return null; + } + + try { + // 替换公式中的变量为对应的值 + String expression = formula; + for (Map.Entry entry : variableValueMap.entrySet()) { + String code = entry.getKey(); + java.math.BigDecimal value = entry.getValue(); + expression = expression.replaceAll("\\b" + code + "\\b", value.toPlainString()); + } + + // 将未知变量替换为0 + expression = expression.replaceAll("[A-Za-z][A-Za-z0-9_]*", "0"); + + log.debug("汇总值计算 - 公式: {}, 表达式: {}", formula, expression); + + // 使用 JavaScript 引擎计算表达式 + javax.script.ScriptEngineManager manager = new javax.script.ScriptEngineManager(); + javax.script.ScriptEngine engine = manager.getEngineByName("JavaScript"); + if (engine == null) { + engine = manager.getEngineByName("nashorn"); + } + if (engine == null) { + log.warn("无法获取脚本引擎,无法计算公式: {}", formula); + return null; + } + + Object result = engine.eval(expression); + if (result instanceof Number) { + return new java.math.BigDecimal(result.toString()).setScale(2, java.math.RoundingMode.HALF_UP); + } + return null; + } catch (Exception e) { + log.warn("计算汇总值失败,公式: {}, 错误: {}", formula, e.getMessage()); + return null; + } + } + + @Override + public List getVariableSettingListByCompileTree( + Long compileTreeId, + List baseNumberRangeIds) { + + // 1. 按单位工程信息中的定额数据库(创建单位工程时选定的 quotaCatalogItemId = 定额专业)查询变量设置列表 + Set specialtyIds = getQuotaSpecialtyIdsFromUnitInfo(compileTreeId); + + if (specialtyIds.isEmpty()) { + log.info("[getVariableSettingListByCompileTree] 单位工程 {} 未配置定额数据库,无法加载变量设置", compileTreeId); + return new ArrayList<>(); + } + + log.info("[getVariableSettingListByCompileTree] 单位工程 {} 定额数据库(定额专业): {}", compileTreeId, specialtyIds); + + // 2. 获取分部分项树数据(只查询一次,供所有专业共用) + List divisionTree = + wbBoqDivisionService.getTreeWithPrice(compileTreeId); + + // 3. 构建变量值映射(只计算一次) + Map variableValueMap = buildVariableValueMap( + compileTreeId, + divisionTree, + baseNumberRangeIds != null ? new HashSet<>(baseNumberRangeIds) : null + ); + + log.info("[getVariableSettingListByCompileTree] 变量值映射: {}", variableValueMap); + + // 4. 加载该定额专业下的变量设置(按 id+category 去重,因为同一个定额取费可能出现在多个类别中) + List result = new ArrayList<>(); + Set addedKeys = new HashSet<>(); + + for (Long specialtyId : specialtyIds) { + List settings = getVariableSettingListAllWithFeeItems(specialtyId); + for (QuotaVariableSettingRespVO vo : settings) { + // 使用 id + category 作为去重键 + String key = vo.getId() + "_" + (vo.getCategory() != null ? vo.getCategory() : ""); + if (vo.getId() != null && !addedKeys.contains(key)) { + addedKeys.add(key); + // 计算汇总值 + java.math.BigDecimal summaryValue = calculateSummaryValue(vo.getCalcBase(), variableValueMap); + vo.setSummaryValue(summaryValue); + result.add(vo); + } + } + } + + // 5. 按 category 和 sortOrder 排序 + result.sort((a, b) -> { + int categoryCompare = (a.getCategory() != null ? a.getCategory() : "").compareTo(b.getCategory() != null ? b.getCategory() : ""); + if (categoryCompare != 0) { + return categoryCompare; + } + int sortA = a.getSortOrder() != null ? a.getSortOrder() : 0; + int sortB = b.getSortOrder() != null ? b.getSortOrder() : 0; + return Integer.compare(sortA, sortB); + }); + + return result; + } + + /** + * 基数设置弹窗表格:变量设置列表的查询锚点。 + * 使用单位工程 {@link com.yhy.module.core.dal.dataobject.workbench.WbUnitInfoDO#getQuotaCatalogItemId()}(创建单位工程时选定的定额数据库/定额专业), + * 不再根据分部分项树中已套定额反推专业。 + */ + private Set getQuotaSpecialtyIdsFromUnitInfo(Long compileTreeId) { + Set specialtyIds = new HashSet<>(); + com.yhy.module.core.dal.dataobject.workbench.WbUnitInfoDO unitInfo = + wbUnitInfoMapper.selectByCompileTreeId(compileTreeId); + if (unitInfo == null || unitInfo.getQuotaCatalogItemId() == null) { + log.info("[getQuotaSpecialtyIdsFromUnitInfo] 单位工程 {} 无单位信息或未配置定额数据库(quotaCatalogItemId)", compileTreeId); + return specialtyIds; + } + specialtyIds.add(unitInfo.getQuotaCatalogItemId()); + log.info("[getQuotaSpecialtyIdsFromUnitInfo] 单位工程 {} 定额数据库(定额专业)ID={}", compileTreeId, unitInfo.getQuotaCatalogItemId()); + return specialtyIds; + } + + // ==================== 私有方法 ==================== + + /** + * 验证定额专业节点(变量设置现在绑定到 specialty 节点) + */ + private void validateSpecialtyNode(Long catalogItemId) { + QuotaCatalogItemDO catalogItem = quotaCatalogItemMapper.selectById(catalogItemId); + if (catalogItem == null) { + throw exception(QUOTA_CATALOG_ITEM_NOT_EXISTS); + } + if (!"specialty".equals(catalogItem.getNodeType())) { + throw exception(QUOTA_VARIABLE_SETTING_INVALID_NODE_TYPE); + } + } + + /** + * 验证类别 + */ + private void validateCategory(String category) { + if (!VALID_CATEGORIES.contains(category)) { + throw exception(QUOTA_VARIABLE_SETTING_INVALID_CATEGORY); + } + } + + /** + * 验证代号格式:不允许纯数字,必须包含字母 + */ + private void validateCodeFormat(String code) { + // 纯数字检查:如果全是数字则不允许 + if (code.matches("^\\d+$")) { + throw exception(QUOTA_VARIABLE_SETTING_CODE_FORMAT_INVALID); + } + // 格式检查:必须以字母开头,只能包含字母、数字、下划线 + if (!code.matches("^[A-Za-z][A-Za-z0-9_]*$")) { + throw exception(QUOTA_VARIABLE_SETTING_CODE_FORMAT_INVALID); + } + } + + /** + * 验证代号唯一性 + */ + private void validateCodeUnique(Long id, Long catalogItemId, String category, String code) { + QuotaVariableSettingDO existing = quotaVariableSettingMapper.selectByCode(catalogItemId, category, code); + if (existing != null && !existing.getId().equals(id)) { + throw exception(QUOTA_VARIABLE_SETTING_CODE_DUPLICATE); + } + } + + /** + * 验证费用代号在同一定额专业节点下跨四个标签页的唯一性 + * 四个标签页(分部分项/措施项目/其他项目/单位汇总)不允许出现相同的费用代号 + */ + private void validateCodeUniqueAcrossCategories(Long id, Long catalogItemId, String currentCategory, String code) { + QuotaVariableSettingDO existing = quotaVariableSettingMapper.selectByCodeAcrossCategories(catalogItemId, code); + if (existing != null && !existing.getId().equals(id)) { + // 同类别内的重复由 validateCodeUnique 处理,这里只处理跨类别的情况 + if (!existing.getCategory().equals(currentCategory)) { + String categoryLabel = getCategoryLabel(existing.getCategory()); + throw exception(QUOTA_VARIABLE_SETTING_CODE_DUPLICATE_ACROSS_TABS, code, categoryLabel); + } + } + } + + /** + * 获取类别的中文标签 + */ + private String getCategoryLabel(String category) { + switch (category) { + case "division": return "分部分项"; + case "measure": return "措施项目"; + case "other": return "其他项目"; + case "unit_summary": return "单位汇总"; + default: return category; + } + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/ResourceCatalogItemService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/ResourceCatalogItemService.java index b7063d4..ff19102 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/ResourceCatalogItemService.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/ResourceCatalogItemService.java @@ -39,4 +39,21 @@ public interface ResourceCatalogItemService { * @param nodeId2 节点2的ID */ void swapSortOrder(Long nodeId1, Long nodeId2); + + /** + * 查询指定层级的所有节点 + * + * @param pathLength path数组长度(层级深度,默认为2) + * @return 指定层级的节点列表 + */ + List listByPathLevel(Integer pathLength); + + /** + * 拖动节点到指定位置 + * + * @param dragNodeId 被拖动的节点ID + * @param targetNodeId 目标节点ID + * @param position 位置:before(目标节点前), after(目标节点后) + */ + void dragNode(Long dragNodeId, Long targetNodeId, String position); } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/ResourceCategoryTreeService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/ResourceCategoryTreeService.java index cdd6d75..87fc2f6 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/ResourceCategoryTreeService.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/ResourceCategoryTreeService.java @@ -56,4 +56,13 @@ public interface ResourceCategoryTreeService { * @param nodeId2 节点2的ID */ void swapSortOrder(Long nodeId1, Long nodeId2); + + /** + * 拖动节点到指定位置 + * + * @param dragNodeId 被拖动的节点ID + * @param targetNodeId 目标节点ID + * @param position 位置:before(目标节点前), after(目标节点后) + */ + void dragNode(Long dragNodeId, Long targetNodeId, String position); } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/ResourceItemService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/ResourceItemService.java index 5ab393d..847d081 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/ResourceItemService.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/ResourceItemService.java @@ -34,4 +34,20 @@ public interface ResourceItemService { void updatePrice(ResourcePriceSaveReqVO reqVO); void deletePrice(Long id); + + /** + * 根据编码查询工料机项 + * + * @param code 工料机编码 + * @return 工料机项,如果不存在返回null + */ + ResourceItemRespVO getItemByCode(String code); + + /** + * 交换两个工料机项的排序 + * + * @param nodeId1 节点1的ID + * @param nodeId2 节点2的ID + */ + void swapSortOrder(Long nodeId1, Long nodeId2); } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/impl/ResourceCatalogItemServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/impl/ResourceCatalogItemServiceImpl.java index d88fd90..5b65c1d 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/impl/ResourceCatalogItemServiceImpl.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/impl/ResourceCatalogItemServiceImpl.java @@ -62,6 +62,7 @@ public class ResourceCatalogItemServiceImpl implements ResourceCatalogItemServic ResourceCatalogItemDO item = BeanUtils.toBean(reqVO, ResourceCatalogItemDO.class); item.setTenantId(tenantId); + item.setSortOrder(null); // 初始不设置排序号 // 先设置一个临时的 path(空数组),插入后再更新 item.setPath(new String[0]); catalogItemMapper.insert(item); @@ -70,8 +71,17 @@ public class ResourceCatalogItemServiceImpl implements ResourceCatalogItemServic List parentPathIds = parentPath; String[] finalPath = buildPath(parentPathIds, String.valueOf(item.getId())); item.setPath(finalPath); + // 设置一个较大的 sortOrder,确保 reorderSiblings 时新节点排在最后(数据库默认值为0会导致排到第一位) + item.setSortOrder(Integer.MAX_VALUE); catalogItemMapper.updateById(item); + // 处理排序 + if (reqVO.getReferenceNodeId() != null && reqVO.getInsertPosition() != null) { + insertNodeAtPosition(item, reqVO.getReferenceNodeId(), reqVO.getInsertPosition()); + } else { + reorderSiblings(item.getCategoryTreeId(), item.getPath()); + } + return item.getId(); } @@ -275,10 +285,16 @@ public class ResourceCatalogItemServiceImpl implements ResourceCatalogItemServic // 1. 查询目录树节点 ResourceCatalogItemDO catalogItem = catalogItemMapper.selectById(catalogItemId); if (catalogItem == null) { - throw exception0(BAD_REQUEST.getCode(), "目录树节点不存在"); + // 节点不存在时返回空列表,避免前端报错 + return new ArrayList<>(); } - // 2. 查询该目录树节点所属的机类树节点允许的类别列表 + // 2. 检查 categoryTreeId 是否存在 + if (catalogItem.getCategoryTreeId() == null) { + return new ArrayList<>(); + } + + // 3. 查询该目录树节点所属的机类树节点允许的类别列表 return categoryTreeService.getCategoriesByTreeNode(catalogItem.getCategoryTreeId()); } @@ -318,6 +334,70 @@ public class ResourceCatalogItemServiceImpl implements ResourceCatalogItemServic catalogItemMapper.updateById(node2); } + @Override + @Transactional(rollbackFor = Exception.class) + public void dragNode(Long dragNodeId, Long targetNodeId, String position) { + Long tenantId = TenantContextHolder.getTenantId(); + + // 1. 查询拖动节点和目标节点(使用行锁) + ResourceCatalogItemDO dragNode = catalogItemMapper.selectOne( + new LambdaQueryWrapper() + .eq(ResourceCatalogItemDO::getId, dragNodeId) + .eq(ResourceCatalogItemDO::getTenantId, tenantId) + .last("FOR UPDATE") + ); + + ResourceCatalogItemDO targetNode = catalogItemMapper.selectOne( + new LambdaQueryWrapper() + .eq(ResourceCatalogItemDO::getId, targetNodeId) + .eq(ResourceCatalogItemDO::getTenantId, tenantId) + .last("FOR UPDATE") + ); + + if (dragNode == null || targetNode == null) { + throw exception0(BAD_REQUEST.getCode(), "节点不存在"); + } + + // 2. 验证是否同级(path 长度相同且父路径相同) + if (!isSameLevel(dragNode.getPath(), targetNode.getPath())) { + throw exception0(BAD_REQUEST.getCode(), "只能在同级节点之间拖动"); + } + + // 3. 查询同级节点列表 + List siblings = getSiblingNodes(dragNode.getCategoryTreeId(), dragNode.getPath(), tenantId); + + // 4. 移除拖动节点 + siblings.removeIf(node -> node.getId().equals(dragNodeId)); + + // 5. 找到目标节点位置 + int targetIndex = -1; + for (int i = 0; i < siblings.size(); i++) { + if (siblings.get(i).getId().equals(targetNodeId)) { + targetIndex = i; + break; + } + } + + if (targetIndex == -1) { + throw exception0(BAD_REQUEST.getCode(), "目标节点不存在"); + } + + // 6. 根据位置插入拖动节点 + if ("before".equals(position)) { + siblings.add(targetIndex, dragNode); + } else if ("after".equals(position)) { + siblings.add(targetIndex + 1, dragNode); + } else { + throw exception0(BAD_REQUEST.getCode(), "无效的插入位置"); + } + + // 7. 重新分配 sortOrder + for (int i = 0; i < siblings.size(); i++) { + siblings.get(i).setSortOrder(i + 1); + } + catalogItemMapper.updateBatch(siblings); + } + /** * 判断两个节点是否同级(通过 path 数组判断) */ @@ -345,4 +425,128 @@ public class ResourceCatalogItemServiceImpl implements ResourceCatalogItemServic return true; } + + @Override + public List listByPathLevel(Integer pathLength) { + if (pathLength == null || pathLength < 1) { + pathLength = 2; + } + //TenantContextHolder.getTenantId() + return catalogItemMapper.selectByPathLevel(pathLength, 1L); + } + + /** + * 在指定位置插入新节点 + * @param newNode 新节点 + * @param referenceNodeId 参考节点ID + * @param position 插入位置:above/below + */ + private void insertNodeAtPosition(ResourceCatalogItemDO newNode, Long referenceNodeId, String position) { + Long tenantId = TenantContextHolder.getTenantId(); + + ResourceCatalogItemDO referenceNode = catalogItemMapper.selectById(referenceNodeId); + if (referenceNode == null) { + throw exception0(BAD_REQUEST.getCode(), "参考节点不存在"); + } + + // 验证是否同级(path 长度相同且父路径相同) + if (!isSameLevel(newNode.getPath(), referenceNode.getPath())) { + throw exception0(BAD_REQUEST.getCode(), "参考节点与新节点不在同一层级"); + } + + // 查询同级节点(按 sortOrder 排序) + List siblings = getSiblingNodes(newNode.getCategoryTreeId(), newNode.getPath(), tenantId); + + // 先移除新节点(如果已存在于列表中),避免影响 referenceIndex 计算 + siblings.removeIf(node -> node.getId().equals(newNode.getId())); + + // 找到参考节点的位置 + int referenceIndex = -1; + for (int i = 0; i < siblings.size(); i++) { + if (siblings.get(i).getId().equals(referenceNodeId)) { + referenceIndex = i; + break; + } + } + + if (referenceIndex == -1) { + // 参考节点不在同级节点中,回退到普通排序 + reorderSiblings(newNode.getCategoryTreeId(), newNode.getPath()); + return; + } + + // 在指定位置插入新节点 + if ("above".equals(position)) { + siblings.add(referenceIndex, newNode); + } else if ("below".equals(position)) { + siblings.add(referenceIndex + 1, newNode); + } else { + siblings.add(newNode); + } + + // 重新分配 sortOrder + for (int i = 0; i < siblings.size(); i++) { + siblings.get(i).setSortOrder(i + 1); + } + catalogItemMapper.updateBatch(siblings); + } + + /** + * 重新排序同级节点 + * @param categoryTreeId 机类树ID + * @param path 当前节点路径(用于确定层级) + */ + private void reorderSiblings(Long categoryTreeId, String[] path) { + Long tenantId = TenantContextHolder.getTenantId(); + + List siblings = getSiblingNodes(categoryTreeId, path, tenantId); + if (siblings.isEmpty()) { + return; + } + + // 重新分配 sortOrder + for (int i = 0; i < siblings.size(); i++) { + siblings.get(i).setSortOrder(i + 1); + } + catalogItemMapper.updateBatch(siblings); + } + + /** + * 获取同级节点列表 + * @param categoryTreeId 机类树ID + * @param path 当前节点路径 + * @param tenantId 租户ID + * @return 同级节点列表 + */ + private List getSiblingNodes(Long categoryTreeId, String[] path, Long tenantId) { + // 构建父路径条件 + if (path == null || path.length == 0) { + return new ArrayList<>(); + } + + // 获取父路径(去掉最后一个元素) + String[] parentPath = null; + if (path.length > 1) { + parentPath = new String[path.length - 1]; + System.arraycopy(path, 0, parentPath, 0, parentPath.length); + } + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(ResourceCatalogItemDO::getTenantId, tenantId) + .eq(ResourceCatalogItemDO::getCategoryTreeId, categoryTreeId); + + if (parentPath == null) { + // 根节点:path 长度为 1 + queryWrapper.apply("array_length(path, 1) = 1"); + } else { + // 非根节点:父路径相同 + // 构建数组字符串(parentPath 来自数据库 path 字段,是 ID 值,安全) + String arrayStr = "ARRAY['" + String.join("','", parentPath) + "']::varchar[]"; + queryWrapper.apply("path[1:array_length(path,1)-1]::varchar[] = " + arrayStr); + } + + queryWrapper.orderByAsc(ResourceCatalogItemDO::getSortOrder); + + return catalogItemMapper.selectList(queryWrapper); + } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/impl/ResourceCategoryTreeServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/impl/ResourceCategoryTreeServiceImpl.java index 269b263..284d22f 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/impl/ResourceCategoryTreeServiceImpl.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/impl/ResourceCategoryTreeServiceImpl.java @@ -62,11 +62,19 @@ public class ResourceCategoryTreeServiceImpl implements ResourceCategoryTreeServ public Long createNode(ResourceCategoryTreeSaveReqVO reqVO) { Long tenantId = TenantContextHolder.getTenantId(); - // 不检查编码唯一性,允许同一父节点下有相同编码 - // 编码由前端自动生成(如:父编码-随机数) + // 根节点唯一性校验:只允许一个根节点 + if (reqVO.getParentId() == null) { + List roots = categoryTreeMapper.selectList( + new LambdaQueryWrapper() + .isNull(ResourceCategoryTreeDO::getParentId)); + if (!roots.isEmpty()) { + throw exception0(BAD_REQUEST.getCode(), "已存在根节点,不允许重复创建"); + } + } ResourceCategoryTreeDO node = BeanUtils.toBean(reqVO, ResourceCategoryTreeDO.class); node.setTenantId(tenantId); + node.setSortOrder(null); // 构建路径 if (reqVO.getParentId() != null) { @@ -94,6 +102,12 @@ public class ResourceCategoryTreeServiceImpl implements ResourceCategoryTreeServ } categoryTreeMapper.updateById(node); + if (reqVO.getReferenceNodeId() != null && reqVO.getInsertPosition() != null) { + insertNodeAtPosition(node, reqVO.getReferenceNodeId(), reqVO.getInsertPosition()); + } else { + reorderSiblings(node.getParentId()); + } + return node.getId(); } @@ -122,6 +136,8 @@ public class ResourceCategoryTreeServiceImpl implements ResourceCategoryTreeServ } // 2. 检查第三层:工料机目录树(所有节点都需要检查) + Long parentId = node.getParentId(); + Long catalogItemCount = catalogItemMapper.selectCount( new LambdaQueryWrapper() .eq(ResourceCatalogItemDO::getTenantId, tenantId) @@ -173,6 +189,8 @@ public class ResourceCategoryTreeServiceImpl implements ResourceCategoryTreeServ // 5. 删除节点 categoryTreeMapper.deleteById(id); + + reorderSiblings(parentId); } @Override @@ -360,8 +378,153 @@ public class ResourceCategoryTreeServiceImpl implements ResourceCategoryTreeServ return parentId1.equals(parentId2); } + @Override + @Transactional(rollbackFor = Exception.class) + public void dragNode(Long dragNodeId, Long targetNodeId, String position) { + Long tenantId = TenantContextHolder.getTenantId(); + + ResourceCategoryTreeDO dragNode = categoryTreeMapper.selectOne( + new LambdaQueryWrapper() + .eq(ResourceCategoryTreeDO::getId, dragNodeId) + .eq(ResourceCategoryTreeDO::getTenantId, tenantId) + .last("FOR UPDATE") + ); + + ResourceCategoryTreeDO targetNode = categoryTreeMapper.selectOne( + new LambdaQueryWrapper() + .eq(ResourceCategoryTreeDO::getId, targetNodeId) + .eq(ResourceCategoryTreeDO::getTenantId, tenantId) + .last("FOR UPDATE") + ); + + if (dragNode == null || targetNode == null) { + throw exception0(BAD_REQUEST.getCode(), "节点不存在"); + } + + if (!isSameParent(dragNode.getParentId(), targetNode.getParentId())) { + throw exception0(BAD_REQUEST.getCode(), "只能在同级节点之间拖动"); + } + + List siblings = categoryTreeMapper.selectList( + new LambdaQueryWrapper() + .eq(ResourceCategoryTreeDO::getTenantId, tenantId) + .eq(ResourceCategoryTreeDO::getParentId, dragNode.getParentId()) + .orderByAsc(ResourceCategoryTreeDO::getSortOrder) + // .orderByAsc(ResourceCategoryTreeDO::getCreateTime) + ); + + siblings.removeIf(node -> node.getId().equals(dragNodeId)); + + int targetIndex = -1; + for (int i = 0; i < siblings.size(); i++) { + if (siblings.get(i).getId().equals(targetNodeId)) { + targetIndex = i; + break; + } + } + + if (targetIndex == -1) { + throw exception0(BAD_REQUEST.getCode(), "目标节点不存在"); + } + + if ("before".equals(position)) { + siblings.add(targetIndex, dragNode); + } else if ("after".equals(position)) { + siblings.add(targetIndex + 1, dragNode); + } else { + throw exception0(BAD_REQUEST.getCode(), "无效的插入位置"); + } + + for (int i = 0; i < siblings.size(); i++) { + siblings.get(i).setSortOrder(i + 1); + } + categoryTreeMapper.updateBatch(siblings); + } + /** + * 在指定位置插入新节点 + * @param newNode + * @param referenceNodeId + * @param position + */ + private void insertNodeAtPosition(ResourceCategoryTreeDO newNode, Long referenceNodeId, String position) { + Long tenantId = TenantContextHolder.getTenantId(); + + ResourceCategoryTreeDO referenceNode = categoryTreeMapper.selectById(referenceNodeId); + if (referenceNode == null) { + throw exception0(BAD_REQUEST.getCode(), "参考节点不存在"); + } + + if (!isSameParent(newNode.getParentId(), referenceNode.getParentId())) { + throw exception0(BAD_REQUEST.getCode(), "参考节点与新节点不在同一层级"); + } + + List siblings = categoryTreeMapper.selectList( + new LambdaQueryWrapper() + .eq(ResourceCategoryTreeDO::getTenantId, tenantId) + .eq(ResourceCategoryTreeDO::getParentId, newNode.getParentId()) + .orderByAsc(ResourceCategoryTreeDO::getSortOrder) +// .orderByAsc(ResourceCategoryTreeDO::getCreateTime) + ); + + int referenceIndex = -1; + for (int i = 0; i < siblings.size(); i++) { + if (siblings.get(i).getId().equals(referenceNodeId)) { + referenceIndex = i; + break; + } + } + + if (referenceIndex == -1) { + reorderSiblings(newNode.getParentId()); + return; + } + + if ("above".equals(position)) { + siblings.add(referenceIndex, newNode); + } else if ("below".equals(position)) { + siblings.add(referenceIndex + 1, newNode); + } else { + siblings.add(newNode); + } + + for (int i = 0; i < siblings.size(); i++) { + siblings.get(i).setSortOrder(i + 1); + } + categoryTreeMapper.updateBatch(siblings); + } + /** + * 重新排序同级节点 + * @param parentId + */ + private void reorderSiblings(Long parentId) { + Long tenantId = TenantContextHolder.getTenantId(); + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + if (parentId == null) { + queryWrapper.isNull(ResourceCategoryTreeDO::getParentId); + } else { + queryWrapper.eq(ResourceCategoryTreeDO::getParentId, parentId); + } + queryWrapper.eq(ResourceCategoryTreeDO::getTenantId, tenantId) + .orderByAsc(ResourceCategoryTreeDO::getSortOrder); +// .orderByAsc(ResourceCategoryTreeDO::getCreateTime); + + List siblings = categoryTreeMapper.selectList(queryWrapper); + + // 空列表时直接返回,避免 updateBatch 抛出 MybatisPlusException + if (siblings.isEmpty()) { + return; + } + + for (int i = 0; i < siblings.size(); i++) { + siblings.get(i).setSortOrder(i + 1); + } + categoryTreeMapper.updateBatch(siblings); + } + /** * 构建树形结构 + * 排序规则:优先按 sortOrder 升序,sortOrder 为 null 时按 createTime 升序(新创建的排最后) */ private List buildTree(List allNodes, Long parentId) { return allNodes.stream() @@ -371,6 +534,24 @@ public class ResourceCategoryTreeServiceImpl implements ResourceCategoryTreeServ } return parentId.equals(node.getParentId()); }) + .sorted((a, b) -> { + Integer sortA = a.getSortOrder(); + Integer sortB = b.getSortOrder(); + // 都有 sortOrder 时按 sortOrder 排序 + if (sortA != null && sortB != null) { + return sortA.compareTo(sortB); + } + // 都没有 sortOrder 时按 createTime 排序 + if (sortA == null && sortB == null) { + if (a.getCreateTime() == null && b.getCreateTime() == null) return 0; + if (a.getCreateTime() == null) return 1; + if (b.getCreateTime() == null) return -1; + return a.getCreateTime().compareTo(b.getCreateTime()); + } + // 有 sortOrder 的排前面 + if (sortA == null) return 1; + return -1; + }) .map(node -> { ResourceCategoryTreeNodeVO vo = BeanUtils.toBean(node, ResourceCategoryTreeNodeVO.class); vo.setChildren(buildTree(allNodes, node.getId())); diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/impl/ResourceItemServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/impl/ResourceItemServiceImpl.java index ad7d19b..e0490fa 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/impl/ResourceItemServiceImpl.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/impl/ResourceItemServiceImpl.java @@ -23,13 +23,16 @@ import com.yhy.module.core.dal.mysql.resource.ResourceCategoryTreeMappingMapper; import com.yhy.module.core.dal.mysql.resource.ResourceItemMapper; import com.yhy.module.core.dal.mysql.resource.ResourcePriceMapper; import com.yhy.module.core.service.resource.ResourceItemService; +import java.math.BigDecimal; import java.time.format.DateTimeFormatter; import java.util.List; import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; +@Slf4j @Service public class ResourceItemServiceImpl implements ResourceItemService { @@ -51,17 +54,18 @@ public class ResourceItemServiceImpl implements ResourceItemService { public Long createItem(ResourceItemSaveReqVO reqVO) { Long tenantId = TenantContextHolder.getTenantId(); - // 1. 验证编码是否重复 - if (reqVO.getCode() != null && !reqVO.getCode().trim().isEmpty()) { - Long count = resourceItemMapper.selectCount( - new LambdaQueryWrapper() - .eq(ResourceItemDO::getTenantId, tenantId) - .eq(ResourceItemDO::getCode, reqVO.getCode()) - ); - if (count > 0) { - throw exception0(BAD_REQUEST.getCode(), - String.format("工料机编码 [%s] 已存在,请使用其他编码", reqVO.getCode())); - } + // 0. 创建时catalogItemId必填 + if (reqVO.getCatalogItemId() == null) { + throw exception0(BAD_REQUEST.getCode(), "创建工料机项时,目录树节点ID不能为空"); + } + + // 1. 如果单位为 %,自动清空税率和价格字段 + if ("%".equals(reqVO.getUnit())) { + reqVO.setTaxRate(null); + reqVO.setTaxExclBasePrice(null); + reqVO.setTaxInclBasePrice(null); + reqVO.setTaxExclCompilePrice(null); + reqVO.setTaxInclCompilePrice(null); } // 2. 验证目录树节点是否存在 @@ -70,17 +74,51 @@ public class ResourceItemServiceImpl implements ResourceItemService { throw exception0(BAD_REQUEST.getCode(), "目录树节点不存在"); } - // 3. 验证 category_id 是否在机类树节点的允许范围内 + // 4. 验证 category_id 是否在机类树节点的允许范围内 validateCategoryInAllowedRange(catalogItem.getCategoryTreeId(), reqVO.getCategoryId()); - // 4. 验证计算基数 + // 5. 验证计算基数 validateCalcBase(reqVO, catalogItem.getCategoryTreeId()); - // 5. 创建工料机项 + // 6. 创建工料机项 ResourceItemDO item = BeanUtils.toBean(reqVO, ResourceItemDO.class); item.setTenantId(tenantId); // 自动设置 category_tree_id(从目录树节点获取) item.setCategoryTreeId(catalogItem.getCategoryTreeId()); + + // 7. 处理排序(支持基于参考节点的插入位置) + Integer sortOrder = reqVO.getSortOrder(); + if (sortOrder == null) { + Long referenceNodeId = reqVO.getReferenceNodeId(); + String insertPosition = reqVO.getInsertPosition(); + + if (referenceNodeId != null && insertPosition != null) { + // 基于参考节点计算排序值 + ResourceItemDO referenceNode = resourceItemMapper.selectById(referenceNodeId); + if (referenceNode != null) { + int refSortOrder = referenceNode.getSortOrder() != null ? referenceNode.getSortOrder() : 0; + if ("above".equals(insertPosition)) { + // 在参考节点上方插入:使用参考节点的排序值,并将参考节点及其后的节点排序值+1 + sortOrder = refSortOrder; + resourceItemMapper.incrementSortOrderFrom(reqVO.getCatalogItemId(), refSortOrder); + } else if ("below".equals(insertPosition)) { + // 在参考节点下方插入:使用参考节点排序值+1,并将后续节点排序值+1 + sortOrder = refSortOrder + 1; + resourceItemMapper.incrementSortOrderFrom(reqVO.getCatalogItemId(), refSortOrder + 1); + } else { + // 默认追加到末尾 + sortOrder = resourceItemMapper.selectMaxSortOrder(reqVO.getCatalogItemId()) + 1; + } + } else { + sortOrder = resourceItemMapper.selectMaxSortOrder(reqVO.getCatalogItemId()) + 1; + } + } else { + // 无参考节点,追加到末尾 + sortOrder = resourceItemMapper.selectMaxSortOrder(reqVO.getCatalogItemId()) + 1; + } + } + item.setSortOrder(sortOrder); + resourceItemMapper.insert(item); return item.getId(); } @@ -96,38 +134,119 @@ public class ResourceItemServiceImpl implements ResourceItemService { throw exception0(BAD_REQUEST.getCode(), "工料机项不存在"); } - // 2. 验证编码是否重复(排除自己) - if (reqVO.getCode() != null && !reqVO.getCode().trim().isEmpty()) { - Long count = resourceItemMapper.selectCount( - new LambdaQueryWrapper() - .eq(ResourceItemDO::getTenantId, tenantId) - .eq(ResourceItemDO::getCode, reqVO.getCode()) - .ne(ResourceItemDO::getId, reqVO.getId()) - ); - if (count > 0) { - throw exception0(BAD_REQUEST.getCode(), - String.format("工料机编码 [%s] 已存在,请使用其他编码", reqVO.getCode())); - } + // 2. 如果是复合工料机,不允许将单位改为 % + String unit = reqVO.getUnit() != null ? reqVO.getUnit() : db.getUnit(); + if (db.getIsMerged() != null && db.getIsMerged() == 1 && "%".equals(unit)) { + throw exception0(BAD_REQUEST.getCode(), "复合工料机不允许将单位设置为 %,请先删除所有子工料机"); } - // 3. 验证目录树节点是否存在 - ResourceCatalogItemDO catalogItem = catalogItemMapper.selectById(reqVO.getCatalogItemId()); + // 3. 如果单位改为 % 或者已经是 %,自动清空税率和价格字段 + boolean needClearFields = "%".equals(unit); + + if (needClearFields) { + reqVO.setTaxRate(null); + reqVO.setTaxExclBasePrice(null); + reqVO.setTaxInclBasePrice(null); + reqVO.setTaxExclCompilePrice(null); + reqVO.setTaxInclCompilePrice(null); + } + + // 4. 如果是复合工料机(is_merged=1),自动清空税率 + boolean isMergedItem = db.getIsMerged() != null && db.getIsMerged() == 1; + if (isMergedItem) { + reqVO.setTaxRate(null); + } + + // 5. 如果没有传递catalogItemId,使用数据库中的值 + Long catalogItemId = reqVO.getCatalogItemId() != null ? reqVO.getCatalogItemId() : db.getCatalogItemId(); + + // 6. 验证目录树节点是否存在 + ResourceCatalogItemDO catalogItem = catalogItemMapper.selectById(catalogItemId); if (catalogItem == null) { throw exception0(BAD_REQUEST.getCode(), "目录树节点不存在"); } - // 4. 验证 category_id 是否在机类树节点的允许范围内 - validateCategoryInAllowedRange(catalogItem.getCategoryTreeId(), reqVO.getCategoryId()); + // 7. 如果没有传递categoryId,使用数据库中的值 + Long categoryId = reqVO.getCategoryId() != null ? reqVO.getCategoryId() : db.getCategoryId(); - // 5. 验证计算基数 + // 8. 验证 category_id 是否在机类树节点的允许范围内 + validateCategoryInAllowedRange(catalogItem.getCategoryTreeId(), categoryId); + + // 8. 验证计算基数 validateCalcBase(reqVO, catalogItem.getCategoryTreeId()); - // 6. 更新工料机项 + // 9. 更新工料机项 ResourceItemDO item = BeanUtils.toBean(reqVO, ResourceItemDO.class); item.setTenantId(tenantId); // 自动设置 category_tree_id(从目录树节点获取) item.setCategoryTreeId(catalogItem.getCategoryTreeId()); - resourceItemMapper.updateById(item); + + // 10. 如果单位为 %,使用 UpdateWrapper 强制更新字段为 null + if (needClearFields) { + com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper updateWrapper = + new com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper<>(); + updateWrapper.eq("id", reqVO.getId()) + .eq("tenant_id", tenantId) + .set("tax_rate", null) + .set("tax_excl_base_price", null) + .set("tax_incl_base_price", null) + .set("tax_excl_compile_price", null) + .set("tax_incl_compile_price", null); + + // 设置其他需要更新的字段(不包括 jsonb 类型) + if (item.getCode() != null) updateWrapper.set("code", item.getCode()); + if (item.getName() != null) updateWrapper.set("name", item.getName()); + if (item.getSpec() != null) updateWrapper.set("spec", item.getSpec()); + if (item.getCategoryId() != null) updateWrapper.set("category_id", item.getCategoryId()); + if (item.getUnit() != null) updateWrapper.set("unit", item.getUnit()); + if (item.getCatalogItemId() != null) updateWrapper.set("catalog_item_id", item.getCatalogItemId()); + if (item.getCategoryTreeId() != null) updateWrapper.set("category_tree_id", item.getCategoryTreeId()); + // 注意:不在这里设置 calc_base 和 attributes,因为 jsonb 类型会导致 hstore 错误 + + resourceItemMapper.update(null, updateWrapper); + + // 单独更新 jsonb 字段(如果有值) + if (item.getCalcBase() != null || item.getAttributes() != null) { + ResourceItemDO jsonbUpdate = new ResourceItemDO(); + jsonbUpdate.setId(reqVO.getId()); + if (item.getCalcBase() != null) jsonbUpdate.setCalcBase(item.getCalcBase()); + if (item.getAttributes() != null) jsonbUpdate.setAttributes(item.getAttributes()); + resourceItemMapper.updateById(jsonbUpdate); + } + } else { + // 使用 UpdateWrapper 来支持更新 null 值(如税率清空) + com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper updateWrapper = + new com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper<>(); + updateWrapper.eq("id", reqVO.getId()) + .eq("tenant_id", tenantId); + + // 设置所有字段(包括可能为 null 的字段) + updateWrapper.set("code", item.getCode()); + updateWrapper.set("name", item.getName()); + updateWrapper.set("spec", item.getSpec()); + updateWrapper.set("category_id", item.getCategoryId()); + updateWrapper.set("unit", item.getUnit()); + updateWrapper.set("catalog_item_id", item.getCatalogItemId()); + updateWrapper.set("category_tree_id", item.getCategoryTreeId()); + updateWrapper.set("is_merged", item.getIsMerged()); + // 价格和税率字段(允许为 null) + updateWrapper.set("tax_rate", item.getTaxRate()); + updateWrapper.set("tax_excl_base_price", item.getTaxExclBasePrice()); + updateWrapper.set("tax_incl_base_price", item.getTaxInclBasePrice()); + updateWrapper.set("tax_excl_compile_price", item.getTaxExclCompilePrice()); + updateWrapper.set("tax_incl_compile_price", item.getTaxInclCompilePrice()); + + resourceItemMapper.update(null, updateWrapper); + + // 单独更新 jsonb 字段(如果有值) + if (item.getCalcBase() != null || item.getAttributes() != null) { + ResourceItemDO jsonbUpdate = new ResourceItemDO(); + jsonbUpdate.setId(reqVO.getId()); + if (item.getCalcBase() != null) jsonbUpdate.setCalcBase(item.getCalcBase()); + if (item.getAttributes() != null) jsonbUpdate.setAttributes(item.getAttributes()); + resourceItemMapper.updateById(jsonbUpdate); + } + } } @Override @@ -180,7 +299,7 @@ public class ResourceItemServiceImpl implements ResourceItemService { .or().like(ResourceItemDO::getUnit, like)); } } - wrapper.orderByDesc(ResourceItemDO::getId); + wrapper.orderByAsc(ResourceItemDO::getSortOrder); Page page = resourceItemMapper.selectPage(new Page<>(reqVO.getPageNo(), reqVO.getPageSize()), wrapper); // 转换为 VO 并填充类别名称 @@ -258,6 +377,11 @@ public class ResourceItemServiceImpl implements ResourceItemService { private ResourceItemRespVO convertToRespVO(ResourceItemDO item) { ResourceItemRespVO vo = BeanUtils.toBean(item, ResourceItemRespVO.class); + // 将英文类型转换为中文显示 + if (item.getType() != null) { + vo.setType(convertTypeToDisplay(item.getType())); + } + // 填充类别名称 if (item.getCategoryId() != null) { ResourceCategoryDO category = categoryMapper.selectById(item.getCategoryId()); @@ -275,8 +399,225 @@ public class ResourceItemServiceImpl implements ResourceItemService { // } } + // 如果是复合工料机(is_merged = 1),计算并填充虚拟字段 + if (item.getIsMerged() != null && item.getIsMerged() == 1) { + calculateMergedTotalSums(item.getId(), vo); + } + return vo; } + + /** + * 将英文类型转换为中文显示 + */ + private String convertTypeToDisplay(String type) { + if (type == null) return null; + switch (type) { + case "labor": return "人"; + case "material": return "材"; + case "machine": return "机"; + default: return type; // 已经是中文或其他格式,直接返回 + } + } + + /** + * 计算复合工料机的合价总和 + * + * @param mergedId 复合工料机ID + * @param vo 响应VO + */ + private void calculateMergedTotalSums(Long mergedId, ResourceItemRespVO vo) { + // 1. 查询所有子工料机数据 + List> children = resourceItemMapper.selectMergedChildrenForSum(mergedId); + if (children == null || children.isEmpty()) { + vo.setTaxExclBaseTotalSum(BigDecimal.ZERO); + vo.setTaxInclBaseTotalSum(BigDecimal.ZERO); + vo.setTaxExclCompileTotalSum(BigDecimal.ZERO); + vo.setTaxInclCompileTotalSum(BigDecimal.ZERO); + return; + } + + // 2. 初始化总和 + BigDecimal taxExclBaseTotalSum = BigDecimal.ZERO; + BigDecimal taxInclBaseTotalSum = BigDecimal.ZERO; + BigDecimal taxExclCompileTotalSum = BigDecimal.ZERO; + BigDecimal taxInclCompileTotalSum = BigDecimal.ZERO; + + // 3. 按类别分组,用于计算基数公式 + java.util.Map categoryPriceMap = new java.util.HashMap<>(); + + // 4. 第一次遍历:处理所有普通工料机,构建 categoryPriceMap + for (java.util.Map child : children) { + String unit = (String) child.get("unit"); + boolean isPercentUnit = "%".equals(unit); + + if (!isPercentUnit) { + // 普通工料机,直接计算 + BigDecimal quotaConsumption = (BigDecimal) child.get("quota_consumption"); + if (quotaConsumption == null) { + quotaConsumption = BigDecimal.ZERO; + } + + // 直接使用消耗量作为因子(与工作台保持一致) + BigDecimal factor = quotaConsumption; + + BigDecimal taxExclBasePrice = (BigDecimal) child.get("tax_excl_base_price"); + BigDecimal taxInclBasePrice = (BigDecimal) child.get("tax_incl_base_price"); + BigDecimal taxExclCompilePrice = (BigDecimal) child.get("tax_excl_compile_price"); + BigDecimal taxInclCompilePrice = (BigDecimal) child.get("tax_incl_compile_price"); + + BigDecimal taxExclBaseTotal = multiplyOrZero(taxExclBasePrice, factor); + BigDecimal taxInclBaseTotal = multiplyOrZero(taxInclBasePrice, factor); + BigDecimal taxExclCompileTotal = multiplyOrZero(taxExclCompilePrice, factor); + BigDecimal taxInclCompileTotal = multiplyOrZero(taxInclCompilePrice, factor); + + taxExclBaseTotalSum = taxExclBaseTotalSum.add(taxExclBaseTotal); + taxInclBaseTotalSum = taxInclBaseTotalSum.add(taxInclBaseTotal); + taxExclCompileTotalSum = taxExclCompileTotalSum.add(taxExclCompileTotal); + taxInclCompileTotalSum = taxInclCompileTotalSum.add(taxInclCompileTotal); + + // 记录类别价格(用于计算基数) + Long categoryId = (Long) child.get("category_id"); + if (categoryId != null) { + CategoryPriceSum categorySum = categoryPriceMap.computeIfAbsent(categoryId, k -> new CategoryPriceSum()); + categorySum.taxExclBasePrice = categorySum.taxExclBasePrice.add(taxExclBaseTotal); + categorySum.taxInclBasePrice = categorySum.taxInclBasePrice.add(taxInclBaseTotal); + categorySum.taxExclCompilePrice = categorySum.taxExclCompilePrice.add(taxExclCompileTotal); + categorySum.taxInclCompilePrice = categorySum.taxInclCompilePrice.add(taxInclCompileTotal); + } + } + } + + // 5. 第二次遍历:处理所有单位为 % 的工料机 + for (java.util.Map child : children) { + String unit = (String) child.get("unit"); + boolean isPercentUnit = "%".equals(unit); + + if (isPercentUnit) { + // 单位为 %,需要根据计算基数计算 + BigDecimal quotaConsumption = (BigDecimal) child.get("quota_consumption"); + if (quotaConsumption == null) { + quotaConsumption = BigDecimal.ZERO; + } + + // 直接使用消耗量作为因子(与工作台保持一致) + BigDecimal factor = quotaConsumption; + + Object calcBaseObj = child.get("calc_base"); + java.util.Map calcBase = null; + + // 处理 calc_base 可能是 String 或 Map 的情况 + if (calcBaseObj instanceof java.util.Map) { + @SuppressWarnings("unchecked") + java.util.Map tempMap = (java.util.Map) calcBaseObj; + calcBase = tempMap; + } else if (calcBaseObj instanceof String) { + // 如果是 String,尝试解析为 JSON + try { + @SuppressWarnings("unchecked") + java.util.Map tempMap = new com.fasterxml.jackson.databind.ObjectMapper() + .readValue((String) calcBaseObj, java.util.Map.class); + calcBase = tempMap; + } catch (Exception e) { + log.error("[calculateMergedTotalSums] 解析 calc_base 失败: {}", e.getMessage(), e); + throw new RuntimeException("计算基数格式错误,无法解析: " + e.getMessage(), e); + } + } + + if (calcBase != null && calcBase.containsKey("formula") && calcBase.containsKey("variables")) { + // 使用计算基数公式计算 + CategoryPriceSum priceSum = calculateWithCalcBase(calcBase, categoryPriceMap, factor); + + taxExclBaseTotalSum = taxExclBaseTotalSum.add(priceSum.taxExclBasePrice); + taxInclBaseTotalSum = taxInclBaseTotalSum.add(priceSum.taxInclBasePrice); + taxExclCompileTotalSum = taxExclCompileTotalSum.add(priceSum.taxExclCompilePrice); + taxInclCompileTotalSum = taxInclCompileTotalSum.add(priceSum.taxInclCompilePrice); + } + } + } + + // 6. 设置虚拟字段 + vo.setTaxExclBaseTotalSum(taxExclBaseTotalSum); + vo.setTaxInclBaseTotalSum(taxInclBaseTotalSum); + vo.setTaxExclCompileTotalSum(taxExclCompileTotalSum); + vo.setTaxInclCompileTotalSum(taxInclCompileTotalSum); + } + + /** + * 使用计算基数公式计算价格 + */ + @SuppressWarnings("unchecked") + private CategoryPriceSum calculateWithCalcBase(java.util.Map calcBase, + java.util.Map categoryPriceMap, + BigDecimal factor) { + CategoryPriceSum result = new CategoryPriceSum(); + + String formula = (String) calcBase.get("formula"); + java.util.Map variablesConfig = (java.util.Map) calcBase.get("variables"); + + // 构建变量值映射表(4个价格字段分别计算) + java.util.Map taxExclBaseVars = new java.util.HashMap<>(); + java.util.Map taxInclBaseVars = new java.util.HashMap<>(); + java.util.Map taxExclCompileVars = new java.util.HashMap<>(); + java.util.Map taxInclCompileVars = new java.util.HashMap<>(); + + for (java.util.Map.Entry entry : variablesConfig.entrySet()) { + String varName = entry.getKey(); + Long categoryId = Long.valueOf(entry.getValue().toString()); + + // 从类别价格映射表中获取价格 + CategoryPriceSum categoryPrice = categoryPriceMap.get(categoryId); + if (categoryPrice == null) { + // 如果类别价格不存在,使用0 + categoryPrice = new CategoryPriceSum(); + } + + taxExclBaseVars.put(varName, categoryPrice.taxExclBasePrice); + taxInclBaseVars.put(varName, categoryPrice.taxInclBasePrice); + taxExclCompileVars.put(varName, categoryPrice.taxExclCompilePrice); + taxInclCompileVars.put(varName, categoryPrice.taxInclCompilePrice); + } + + // 计算公式结果 + try { + BigDecimal taxExclBaseSum = com.yhy.module.core.util.FormulaEvaluator.evaluate(formula, taxExclBaseVars); + BigDecimal taxInclBaseSum = com.yhy.module.core.util.FormulaEvaluator.evaluate(formula, taxInclBaseVars); + BigDecimal taxExclCompileSum = com.yhy.module.core.util.FormulaEvaluator.evaluate(formula, taxExclCompileVars); + BigDecimal taxInclCompileSum = com.yhy.module.core.util.FormulaEvaluator.evaluate(formula, taxInclCompileVars); + + // 乘以因子 + result.taxExclBasePrice = taxExclBaseSum.multiply(factor).setScale(4, java.math.RoundingMode.HALF_UP); + result.taxInclBasePrice = taxInclBaseSum.multiply(factor).setScale(4, java.math.RoundingMode.HALF_UP); + result.taxExclCompilePrice = taxExclCompileSum.multiply(factor).setScale(4, java.math.RoundingMode.HALF_UP); + result.taxInclCompilePrice = taxInclCompileSum.multiply(factor).setScale(4, java.math.RoundingMode.HALF_UP); + } catch (Exception e) { + log.error("[calculateWithCalcBase] 公式计算失败,formula={}, error={}", formula, e.getMessage(), e); + // 计算失败时返回0 + result = new CategoryPriceSum(); + } + + return result; + } + + /** + * 安全乘法(处理 null 值) + */ + private BigDecimal multiplyOrZero(BigDecimal value, BigDecimal factor) { + if (value == null) { + return BigDecimal.ZERO; + } + return value.multiply(factor).setScale(4, java.math.RoundingMode.HALF_UP); + } + + /** + * 类别价格总和对象 + */ + private static class CategoryPriceSum { + BigDecimal taxExclBasePrice = BigDecimal.ZERO; + BigDecimal taxInclBasePrice = BigDecimal.ZERO; + BigDecimal taxExclCompilePrice = BigDecimal.ZERO; + BigDecimal taxInclCompilePrice = BigDecimal.ZERO; + } @Override public List listPrices(Long resourceId) { @@ -372,6 +713,7 @@ public class ResourceItemServiceImpl implements ResourceItemService { @SuppressWarnings("unchecked") java.util.Map variables = (java.util.Map) reqVO.getCalcBase().get("variables"); + // 4. 验证必填字段 if (formula == null || formula.trim().isEmpty()) { throw exception0(BAD_REQUEST.getCode(), "计算基数的公式不能为空"); } @@ -380,32 +722,43 @@ public class ResourceItemServiceImpl implements ResourceItemService { throw exception0(BAD_REQUEST.getCode(), "计算基数的变量映射不能为空"); } - // 4. 获取允许的类别列表 + // 5. 获取允许的类别列表 List allowedCategoryIds = getAllowedCategoryIds(categoryTreeId); + if (allowedCategoryIds.isEmpty()) { + throw exception0(BAD_REQUEST.getCode(), "当前机类树节点未配置任何类别,无法设置计算基数"); + } - // 5. 验证公式中的变量是否都在允许的类别列表中 + // 6. 验证公式中的变量是否都在允许的类别列表中 for (java.util.Map.Entry entry : variables.entrySet()) { + String varName = entry.getKey(); Long categoryId = null; + if (entry.getValue() instanceof Integer) { categoryId = ((Integer) entry.getValue()).longValue(); } else if (entry.getValue() instanceof Long) { categoryId = (Long) entry.getValue(); } else { throw exception0(BAD_REQUEST.getCode(), - String.format("变量 %s 的类别ID格式不正确", entry.getKey())); + String.format("变量 %s 的类别ID格式不正确,必须是整数", varName)); } if (!allowedCategoryIds.contains(categoryId)) { throw exception0(BAD_REQUEST.getCode(), - String.format("变量 %s 的类别ID %d 不在允许的类别范围内", entry.getKey(), categoryId)); + String.format("变量 %s 的类别ID %d 不在允许的类别范围内", varName, categoryId)); + } + + // 验证变量名在公式中是否使用 + if (!formula.contains(varName)) { + throw exception0(BAD_REQUEST.getCode(), + String.format("变量 %s 在公式中未使用", varName)); } } - // 6. 验证公式语法(简单验证) + // 7. 验证公式语法 validateFormulaSyntax(formula, variables.keySet()); } catch (ClassCastException e) { - throw exception0(BAD_REQUEST.getCode(), "计算基数格式不正确"); + throw exception0(BAD_REQUEST.getCode(), "计算基数格式不正确:" + e.getMessage()); } } @@ -448,14 +801,53 @@ public class ResourceItemServiceImpl implements ResourceItemService { if (c == '(') count++; if (c == ')') count--; if (count < 0) { - throw exception0(BAD_REQUEST.getCode(), "公式中的括号不匹配"); + throw exception0(BAD_REQUEST.getCode(), "公式中的括号不匹配:右括号多于左括号"); } } if (count != 0) { - throw exception0(BAD_REQUEST.getCode(), "公式中的括号不匹配"); + throw exception0(BAD_REQUEST.getCode(), "公式中的括号不匹配:左括号多于右括号"); } - // 3. 检查公式中的变量是否都在 variables 中定义 + // 3. 检查公式不能以运算符结尾(除了括号) + String trimmed = formula.trim(); + if (trimmed.isEmpty()) { + throw exception0(BAD_REQUEST.getCode(), "公式不能为空"); + } + + char lastChar = trimmed.charAt(trimmed.length() - 1); + if (lastChar == '+' || lastChar == '-' || lastChar == '*' || lastChar == '/') { + throw exception0(BAD_REQUEST.getCode(), "公式不能以运算符结尾"); + } + + // 4. 检查公式不能以运算符开头(除了负号和括号) + char firstChar = trimmed.charAt(0); + if (firstChar == '+' || firstChar == '*' || firstChar == '/') { + throw exception0(BAD_REQUEST.getCode(), "公式不能以运算符 " + firstChar + " 开头"); + } + + // 5. 检查是否有连续的运算符(除了负号) + for (int i = 0; i < trimmed.length() - 1; i++) { + char c1 = trimmed.charAt(i); + char c2 = trimmed.charAt(i + 1); + if (isOperator(c1) && isOperator(c2)) { + // 允许 +- 或 -- 或 *- 或 /- (负号) + if (c2 != '-') { + throw exception0(BAD_REQUEST.getCode(), + String.format("公式中存在连续的运算符:%c%c", c1, c2)); + } + } + } + + // 6. 提取公式中的所有中文变量,检查是否都已定义 + java.util.Set formulaVariables = extractChineseVariables(formula); + for (String formulaVar : formulaVariables) { + if (!variableNames.contains(formulaVar)) { + throw exception0(BAD_REQUEST.getCode(), + String.format("公式中的变量 '%s' 未定义,请先在变量映射中添加", formulaVar)); + } + } + + // 7. 检查所有定义的变量是否都在公式中使用 for (String varName : variableNames) { if (!formula.contains(varName)) { throw exception0(BAD_REQUEST.getCode(), @@ -463,4 +855,72 @@ public class ResourceItemServiceImpl implements ResourceItemService { } } } + + /** + * 提取公式中的所有中文变量 + * + * @param formula 公式 + * @return 中文变量集合 + */ + private java.util.Set extractChineseVariables(String formula) { + java.util.Set variables = new java.util.HashSet<>(); + StringBuilder currentVar = new StringBuilder(); + + for (int i = 0; i < formula.length(); i++) { + char c = formula.charAt(i); + + // 如果是中文字符,累积到当前变量 + if (c >= '\u4e00' && c <= '\u9fa5') { + currentVar.append(c); + } else { + // 遇到非中文字符,如果当前变量不为空,则保存 + if (currentVar.length() > 0) { + variables.add(currentVar.toString()); + currentVar = new StringBuilder(); + } + } + } + + // 处理最后一个变量 + if (currentVar.length() > 0) { + variables.add(currentVar.toString()); + } + + return variables; + } + + /** + * 判断字符是否为运算符 + */ + private boolean isOperator(char c) { + return c == '+' || c == '-' || c == '*' || c == '/'; + } + + @Override + public ResourceItemRespVO getItemByCode(String code) { + if (code == null || code.trim().isEmpty()) { + return null; + } + ResourceItemDO resourceItem = resourceItemMapper.selectByCode(code); + if (resourceItem == null) { + return null; + } + return BeanUtils.toBean(resourceItem, ResourceItemRespVO.class); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void swapSortOrder(Long nodeId1, Long nodeId2) { + ResourceItemDO item1 = resourceItemMapper.selectById(nodeId1); + ResourceItemDO item2 = resourceItemMapper.selectById(nodeId2); + if (item1 == null || item2 == null) { + throw exception0(BAD_REQUEST.getCode(), "工料机项不存在"); + } + // 交换排序值 + Integer tempSort = item1.getSortOrder(); + item1.setSortOrder(item2.getSortOrder()); + item2.setSortOrder(tempSort); + resourceItemMapper.updateById(item1); + resourceItemMapper.updateById(item2); + } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/impl/ResourceMergedServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/impl/ResourceMergedServiceImpl.java index d83cf37..e03e7f5 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/impl/ResourceMergedServiceImpl.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/resource/impl/ResourceMergedServiceImpl.java @@ -5,6 +5,7 @@ import static com.yhy.module.core.enums.ErrorCodeConstants.RESOURCE_MERGED_CALC_ import static com.yhy.module.core.enums.ErrorCodeConstants.RESOURCE_MERGED_CALC_BASE_INVALID; import static com.yhy.module.core.enums.ErrorCodeConstants.RESOURCE_MERGED_FORMULA_PARSE_ERROR; import static com.yhy.module.core.enums.ErrorCodeConstants.RESOURCE_MERGED_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.RESOURCE_MERGED_PERCENT_UNIT_NOT_ALLOWED; import static com.yhy.module.core.enums.ErrorCodeConstants.RESOURCE_MERGED_SELF_REFERENCE; import static com.yhy.module.core.enums.ErrorCodeConstants.RESOURCE_MERGED_SOURCE_NOT_EXISTS; @@ -56,12 +57,24 @@ public class ResourceMergedServiceImpl implements ResourceMergedService { throw exception(RESOURCE_MERGED_SELF_REFERENCE); } + // 校验父工料机单位不能为 % + validateParentUnitNotPercent(createReqVO.getMergedId()); + + // 校验子工料机如果单位为 %,必须配置计算基数 + validateSourceCalcBase(createReqVO.getSourceId()); + + // 确保父工料机(merged_id)的税率为空 + ensureParentTaxRateIsNull(createReqVO.getMergedId()); + // 插入 ResourceMergedDO resourceMerged = BeanUtils.toBean(createReqVO, ResourceMergedDO.class); resourceMergedMapper.insert(resourceMerged); - // 触发计算 - calculateMergedPrices(resourceMerged.getMergedId()); + // 将父工料机标记为复合工料机 + updateParentIsMerged(createReqVO.getMergedId(), true); + + // 不再自动触发价格计算,由虚拟字段动态计算 + // calculateMergedPrices(resourceMerged.getMergedId()); // 返回 return resourceMerged.getId(); @@ -73,27 +86,25 @@ public class ResourceMergedServiceImpl implements ResourceMergedService { // 校验存在 ResourceMergedDO existing = validateResourceMergedExists(updateReqVO.getId()); - // 校验不允许自己关联自己 - Long mergedId = updateReqVO.getMergedId() != null ? updateReqVO.getMergedId() : existing.getMergedId(); - Long sourceId = updateReqVO.getSourceId() != null ? updateReqVO.getSourceId() : existing.getSourceId(); - if (mergedId != null && sourceId != null && mergedId.equals(sourceId)) { - throw exception(RESOURCE_MERGED_SELF_REFERENCE); - } - - // 更新 - ResourceMergedDO updateObj = BeanUtils.toBean(updateReqVO, ResourceMergedDO.class); + // 只允许更新定额消耗量 + ResourceMergedDO updateObj = new ResourceMergedDO(); + updateObj.setId(existing.getId()); + updateObj.setQuotaConsumption(updateReqVO.getQuotaConsumption()); resourceMergedMapper.updateById(updateObj); - - // 触发计算(使用更新后的 mergedId) - calculateMergedPrices(mergedId); } @Override + @Transactional(rollbackFor = Exception.class) public void deleteResourceMerged(Long id) { // 校验存在 - validateResourceMergedExists(id); + ResourceMergedDO merged = validateResourceMergedExists(id); + Long mergedId = merged.getMergedId(); + // 删除 resourceMergedMapper.deleteById(id); + + // 检查父工料机是否还有子项,如果没有则取消复合工料机标记 + checkAndUpdateParentIsMerged(mergedId); } private ResourceMergedDO validateResourceMergedExists(Long id) { @@ -111,6 +122,8 @@ public class ResourceMergedServiceImpl implements ResourceMergedService { if (respVO == null) { throw exception(RESOURCE_MERGED_NOT_EXISTS); } + // 格式化数值:价格/税率保留2位小数,消耗量/合价保留4位小数 + formatSingleItem(respVO); return respVO; } @@ -122,12 +135,271 @@ public class ResourceMergedServiceImpl implements ResourceMergedService { // 查询数据(JOIN 第四层) List list = resourceMergedMapper.selectPageWithDetails(pageReqVO, offset); + // 为单位为 % 的子工料机计算合价 + calculatePercentUnitTotals(list); + + // 格式化数值:价格/税率保留2位小数,消耗量/合价保留4位小数 + formatDecimalValues(list); + // 查询总数 Long total = resourceMergedMapper.selectCountWithDetails(pageReqVO); return new PageResult<>(list, total); } + /** + * 为单位为 % 的子工料机计算合价 + * + * @param list 子工料机列表 + */ + @SuppressWarnings("unchecked") + private void calculatePercentUnitTotals(List list) { + if (list == null || list.isEmpty()) { + return; + } + + // 按 mergedId 分组 + Map> groupedByMergedId = list.stream() + .collect(Collectors.groupingBy(ResourceMergedRespVO::getMergedId)); + + // 遍历每个组 + for (Map.Entry> entry : groupedByMergedId.entrySet()) { + List group = entry.getValue(); + + // 第一次遍历:构建类别价格映射表(只处理普通工料机) + Map categoryPriceMap = new HashMap<>(); + + for (ResourceMergedRespVO item : group) { + if (!"%".equals(item.getUnit())) { + // 普通工料机:计算合价并记录到类别映射表 + if (item.getCategoryId() != null) { + CategoryPriceSum categoryPrice = categoryPriceMap.computeIfAbsent( + item.getCategoryId(), k -> new CategoryPriceSum()); + + // 累加到类别价格 + categoryPrice.taxExclBasePrice = categoryPrice.taxExclBasePrice.add( + item.getTaxExclBaseTotal() != null ? item.getTaxExclBaseTotal() : BigDecimal.ZERO); + categoryPrice.taxInclBasePrice = categoryPrice.taxInclBasePrice.add( + item.getTaxInclBaseTotal() != null ? item.getTaxInclBaseTotal() : BigDecimal.ZERO); + categoryPrice.taxExclCompilePrice = categoryPrice.taxExclCompilePrice.add( + item.getTaxExclCompileTotal() != null ? item.getTaxExclCompileTotal() : BigDecimal.ZERO); + categoryPrice.taxInclCompilePrice = categoryPrice.taxInclCompilePrice.add( + item.getTaxInclCompileTotal() != null ? item.getTaxInclCompileTotal() : BigDecimal.ZERO); + } + } + } + + // 第二次遍历:处理单位为 % 的工料机 + for (ResourceMergedRespVO item : group) { + if ("%".equals(item.getUnit()) && item.getCalcBase() != null) { + try { + // 解析 calc_base + Map calcBase = item.getCalcBase(); + if (!calcBase.containsKey("formula") || !calcBase.containsKey("variables")) { + continue; + } + + String formula = (String) calcBase.get("formula"); + Map variablesConfig = (Map) calcBase.get("variables"); + + // 构建变量值映射表 + Map taxExclBaseVars = new HashMap<>(); + Map taxInclBaseVars = new HashMap<>(); + Map taxExclCompileVars = new HashMap<>(); + Map taxInclCompileVars = new HashMap<>(); + + boolean allCategoriesFound = true; + for (Map.Entry varEntry : variablesConfig.entrySet()) { + String varName = varEntry.getKey(); + Long categoryId = Long.valueOf(varEntry.getValue().toString()); + + CategoryPriceSum categoryPrice = categoryPriceMap.get(categoryId); + if (categoryPrice == null) { + allCategoriesFound = false; + break; + } + + taxExclBaseVars.put(varName, categoryPrice.taxExclBasePrice); + taxInclBaseVars.put(varName, categoryPrice.taxInclBasePrice); + taxExclCompileVars.put(varName, categoryPrice.taxExclCompilePrice); + taxInclCompileVars.put(varName, categoryPrice.taxInclCompilePrice); + } + + if (!allCategoriesFound) { + continue; + } + + // 计算公式结果 + BigDecimal taxExclBaseSum = FormulaEvaluator.evaluate(formula, taxExclBaseVars); + BigDecimal taxInclBaseSum = FormulaEvaluator.evaluate(formula, taxInclBaseVars); + BigDecimal taxExclCompileSum = FormulaEvaluator.evaluate(formula, taxExclCompileVars); + BigDecimal taxInclCompileSum = FormulaEvaluator.evaluate(formula, taxInclCompileVars); + + // 乘以 (定额消耗量 / 100) + BigDecimal quotaConsumption = item.getQuotaConsumption() != null + ? item.getQuotaConsumption() : BigDecimal.ZERO; + BigDecimal factor = quotaConsumption.divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP); + + // 设置虚拟字段 + item.setTaxExclBaseTotal(taxExclBaseSum.multiply(factor).setScale(4, RoundingMode.HALF_UP)); + item.setTaxInclBaseTotal(taxInclBaseSum.multiply(factor).setScale(4, RoundingMode.HALF_UP)); + item.setTaxExclCompileTotal(taxExclCompileSum.multiply(factor).setScale(4, RoundingMode.HALF_UP)); + item.setTaxInclCompileTotal(taxInclCompileSum.multiply(factor).setScale(4, RoundingMode.HALF_UP)); + } catch (Exception e) { + log.error("[calculatePercentUnitTotals] 计算失败,id={}, error={}", item.getId(), e.getMessage(), e); + } + } + } + } + } + + /** + * 类别价格汇总 + */ + private static class CategoryPriceSum { + BigDecimal taxExclBasePrice = BigDecimal.ZERO; + BigDecimal taxInclBasePrice = BigDecimal.ZERO; + BigDecimal taxExclCompilePrice = BigDecimal.ZERO; + BigDecimal taxInclCompilePrice = BigDecimal.ZERO; + } + + /** + * 格式化数值:价格/税率保留2位小数,消耗量/合价保留4位小数 + * + * @param list 子工料机列表 + */ + private void formatDecimalValues(List list) { + if (list == null || list.isEmpty()) { + return; + } + + for (ResourceMergedRespVO item : list) { + // 价格/税率保留2位小数 + item.setTaxRate(formatScale(item.getTaxRate(), 2)); + item.setTaxExclBasePrice(formatScale(item.getTaxExclBasePrice(), 2)); + item.setTaxInclBasePrice(formatScale(item.getTaxInclBasePrice(), 2)); + item.setTaxExclCompilePrice(formatScale(item.getTaxExclCompilePrice(), 2)); + item.setTaxInclCompilePrice(formatScale(item.getTaxInclCompilePrice(), 2)); + item.setTaxExclMarketPrice(formatScale(item.getTaxExclMarketPrice(), 2)); + + // 消耗量保留4位小数 + item.setQuotaConsumption(formatScale(item.getQuotaConsumption(), 4)); + + // 合价保留4位小数 + item.setTaxExclBaseTotal(formatScale(item.getTaxExclBaseTotal(), 4)); + item.setTaxInclBaseTotal(formatScale(item.getTaxInclBaseTotal(), 4)); + item.setTaxExclCompileTotal(formatScale(item.getTaxExclCompileTotal(), 4)); + item.setTaxInclCompileTotal(formatScale(item.getTaxInclCompileTotal(), 4)); + } + } + + /** + * 格式化单个工料机项的数值 + */ + private void formatSingleItem(ResourceMergedRespVO item) { + if (item == null) { + return; + } + // 价格/税率保留2位小数 + item.setTaxRate(formatScale(item.getTaxRate(), 2)); + item.setTaxExclBasePrice(formatScale(item.getTaxExclBasePrice(), 2)); + item.setTaxInclBasePrice(formatScale(item.getTaxInclBasePrice(), 2)); + item.setTaxExclCompilePrice(formatScale(item.getTaxExclCompilePrice(), 2)); + item.setTaxInclCompilePrice(formatScale(item.getTaxInclCompilePrice(), 2)); + item.setTaxExclMarketPrice(formatScale(item.getTaxExclMarketPrice(), 2)); + + // 消耗量保留4位小数 + item.setQuotaConsumption(formatScale(item.getQuotaConsumption(), 4)); + + // 合价保留4位小数 + item.setTaxExclBaseTotal(formatScale(item.getTaxExclBaseTotal(), 4)); + item.setTaxInclBaseTotal(formatScale(item.getTaxInclBaseTotal(), 4)); + item.setTaxExclCompileTotal(formatScale(item.getTaxExclCompileTotal(), 4)); + item.setTaxInclCompileTotal(formatScale(item.getTaxInclCompileTotal(), 4)); + } + + /** + * 格式化BigDecimal精度 + */ + private BigDecimal formatScale(BigDecimal value, int scale) { + if (value == null) { + return null; + } + return value.setScale(scale, RoundingMode.HALF_UP); + } + + /** + * 验证父工料机单位不能为 % + * + * @param mergedId 复合工料机ID + */ + private void validateParentUnitNotPercent(Long mergedId) { + if (mergedId == null) { + return; + } + + ResourceItemDO parent = resourceItemMapper.selectById(mergedId); + if (parent == null) { + throw exception(RESOURCE_MERGED_NOT_EXISTS); + } + + // 如果单位为 %,不允许添加子工料机 + if ("%".equals(parent.getUnit())) { + throw exception(RESOURCE_MERGED_PERCENT_UNIT_NOT_ALLOWED); + } + } + + /** + * 验证子工料机如果单位为 %,必须配置计算基数 + * + * @param sourceId 子工料机ID + */ + private void validateSourceCalcBase(Long sourceId) { + if (sourceId == null) { + return; + } + + ResourceItemDO source = resourceItemMapper.selectById(sourceId); + if (source == null) { + throw exception(RESOURCE_MERGED_SOURCE_NOT_EXISTS); + } + + // 如果单位为 %,必须配置计算基数 + if ("%".equals(source.getUnit())) { + if (source.getCalcBase() == null || source.getCalcBase().isEmpty()) { + throw exception(RESOURCE_MERGED_CALC_BASE_INVALID, "单位为 % 的子工料机必须配置计算基数"); + } + + // 验证计算基数格式 + if (!source.getCalcBase().containsKey("formula") || !source.getCalcBase().containsKey("variables")) { + throw exception(RESOURCE_MERGED_CALC_BASE_INVALID, "计算基数格式不正确,必须包含 formula 和 variables"); + } + } + } + + /** + * 确保父工料机(复合工料机)的税率为空 + * + * @param mergedId 复合工料机ID + */ + private void ensureParentTaxRateIsNull(Long mergedId) { + if (mergedId == null) { + return; + } + + ResourceItemDO parent = resourceItemMapper.selectById(mergedId); + if (parent == null) { + throw exception(RESOURCE_MERGED_NOT_EXISTS); + } + + // 如果税率不为空,则清空 + if (parent.getTaxRate() != null) { + log.info("[ensureParentTaxRateIsNull] 清空复合工料机的税率,mergedId={}, 原税率={}", + mergedId, parent.getTaxRate()); + resourceItemMapper.clearTaxRate(mergedId); + } + } + /** * 计算指定 merged_id 的价格 * @@ -235,12 +507,11 @@ public class ResourceMergedServiceImpl implements ResourceMergedService { result = calculateWithCalcBase(merged, source, allMergedList, sourceMap, categoryPriceMap); } else { // 场景1:普通工料机 - BigDecimal factor = quotaConsumption.divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP); - - result.taxExclBasePrice = multiplyOrZero(source.getTaxExclBasePrice(), factor); - result.taxInclBasePrice = multiplyOrZero(source.getTaxInclBasePrice(), factor); - result.taxExclCompilePrice = multiplyOrZero(source.getTaxExclCompilePrice(), factor); - result.taxInclCompilePrice = multiplyOrZero(source.getTaxInclCompilePrice(), factor); + // 合价 = 价格 × 消耗量(与定额基价模块保持一致) + result.taxExclBasePrice = multiplyOrZero(source.getTaxExclBasePrice(), quotaConsumption); + result.taxInclBasePrice = multiplyOrZero(source.getTaxInclBasePrice(), quotaConsumption); + result.taxExclCompilePrice = multiplyOrZero(source.getTaxExclCompilePrice(), quotaConsumption); + result.taxInclCompilePrice = multiplyOrZero(source.getTaxInclCompilePrice(), quotaConsumption); } return result; @@ -294,13 +565,14 @@ public class ResourceMergedServiceImpl implements ResourceMergedService { BigDecimal taxExclCompileSum = FormulaEvaluator.evaluate(formula, taxExclCompileVars); BigDecimal taxInclCompileSum = FormulaEvaluator.evaluate(formula, taxInclCompileVars); - // 乘以 (定额消耗量 / 100) - BigDecimal factor = merged.getQuotaConsumption().divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP); + // 单位为%的工料机:合价 = 公式结果 × (消耗量 / 100) + // 与定额基价模块保持一致 + BigDecimal percentFactor = merged.getQuotaConsumption().divide(new BigDecimal("100"), 6, RoundingMode.HALF_UP); - result.taxExclBasePrice = taxExclBaseSum.multiply(factor).setScale(4, RoundingMode.HALF_UP); - result.taxInclBasePrice = taxInclBaseSum.multiply(factor).setScale(4, RoundingMode.HALF_UP); - result.taxExclCompilePrice = taxExclCompileSum.multiply(factor).setScale(4, RoundingMode.HALF_UP); - result.taxInclCompilePrice = taxInclCompileSum.multiply(factor).setScale(4, RoundingMode.HALF_UP); + result.taxExclBasePrice = taxExclBaseSum.multiply(percentFactor).setScale(4, RoundingMode.HALF_UP); + result.taxInclBasePrice = taxInclBaseSum.multiply(percentFactor).setScale(4, RoundingMode.HALF_UP); + result.taxExclCompilePrice = taxExclCompileSum.multiply(percentFactor).setScale(4, RoundingMode.HALF_UP); + result.taxInclCompilePrice = taxInclCompileSum.multiply(percentFactor).setScale(4, RoundingMode.HALF_UP); } catch (Exception e) { log.error("[calculateWithCalcBase] 公式计算失败,formula={}, error={}", formula, e.getMessage()); throw exception(RESOURCE_MERGED_FORMULA_PARSE_ERROR); @@ -335,4 +607,46 @@ public class ResourceMergedServiceImpl implements ResourceMergedService { this.taxInclCompilePrice = this.taxInclCompilePrice.add(other.taxInclCompilePrice); } } + + /** + * 更新父工料机的 is_merged 标记 + * + * @param mergedId 父工料机ID + * @param isMerged 是否为复合工料机 + */ + private void updateParentIsMerged(Long mergedId, boolean isMerged) { + if (mergedId == null) { + return; + } + + ResourceItemDO parent = resourceItemMapper.selectById(mergedId); + if (parent == null) { + return; + } + + int newValue = isMerged ? 1 : 0; + if (parent.getIsMerged() == null || parent.getIsMerged() != newValue) { + log.info("[updateParentIsMerged] 更新复合工料机标记,mergedId={}, isMerged={}", mergedId, isMerged); + resourceItemMapper.updateIsMerged(mergedId, newValue); + } + } + + /** + * 检查并更新父工料机的 is_merged 标记 + * 如果父工料机没有子项了,则将 is_merged 设为 0 + * + * @param mergedId 父工料机ID + */ + private void checkAndUpdateParentIsMerged(Long mergedId) { + if (mergedId == null) { + return; + } + + // 查询该父工料机是否还有子项 + List children = resourceMergedMapper.selectByMergedId(mergedId); + boolean hasChildren = children != null && !children.isEmpty(); + + // 更新 is_merged 标记 + updateParentIsMerged(mergedId, hasChildren); + } } diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/unit/UnitRateSettingService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/unit/UnitRateSettingService.java new file mode 100644 index 0000000..fa2625c --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/unit/UnitRateSettingService.java @@ -0,0 +1,51 @@ +package com.yhy.module.core.service.unit; + +import com.yhy.module.core.controller.admin.unit.vo.UnitRateSettingRespVO; +import com.yhy.module.core.controller.admin.unit.vo.UnitRateSettingSaveReqVO; +import java.util.List; + +/** + * 工程费率设定 Service 接口 + */ +public interface UnitRateSettingService { + + /** + * 保存或更新费率设定 + * + * @param saveReqVO 保存请求 + * @return 保存后的费率设定(包含计算后的字段值) + */ + UnitRateSettingRespVO saveOrUpdate(UnitRateSettingSaveReqVO saveReqVO); + + /** + * 获取单位工程的费率设定列表 + * + * @param unitId 单位工程ID + * @return 费率设定列表 + */ + List getListByUnitId(Long unitId); + + /** + * 获取单个费率设定 + * + * @param unitId 单位工程ID + * @param rateItemId 费率项ID + * @return 费率设定 + */ + UnitRateSettingRespVO getByUnitIdAndRateItemId(Long unitId, Long rateItemId); + + /** + * 删除费率设定 + * + * @param id 费率设定ID + */ + void delete(Long id); + + /** + * 初始化单位工程的费率设定(创建快照) + * + * @param unitId 单位工程ID + * @param catalogItemId 费率模式节点ID + */ + void initializeSettings(Long unitId, Long catalogItemId); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/unit/UnitRateSettingServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/unit/UnitRateSettingServiceImpl.java new file mode 100644 index 0000000..f71093e --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/unit/UnitRateSettingServiceImpl.java @@ -0,0 +1,249 @@ +package com.yhy.module.core.service.unit; + +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import com.yhy.module.core.controller.admin.unit.vo.UnitRateSettingRespVO; +import com.yhy.module.core.controller.admin.unit.vo.UnitRateSettingSaveReqVO; +import com.yhy.module.core.dal.dataobject.quota.QuotaRateItemDO; +import com.yhy.module.core.dal.dataobject.workbench.WbRateItemDO; +import com.yhy.module.core.dal.dataobject.workbench.WbUnitRateSettingDO; +import com.yhy.module.core.dal.mysql.quota.QuotaRateItemMapper; +import com.yhy.module.core.dal.mysql.workbench.WbRateItemMapper; +import com.yhy.module.core.dal.mysql.workbench.WbUnitRateSettingMapper; +import com.yhy.module.core.service.quota.QuotaRateItemService; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * 工程费率设定 Service 实现 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class UnitRateSettingServiceImpl implements UnitRateSettingService { + + private final WbUnitRateSettingMapper unitRateSettingMapper; + private final QuotaRateItemMapper quotaRateItemMapper; + private final WbRateItemMapper wbRateItemMapper; + private final QuotaRateItemService quotaRateItemService; + + @Override + @Transactional(rollbackFor = Exception.class) + public UnitRateSettingRespVO saveOrUpdate(UnitRateSettingSaveReqVO saveReqVO) { + // 查询是否已存在 + WbUnitRateSettingDO existingSetting = unitRateSettingMapper.selectByUnitIdAndRateItemId( + saveReqVO.getUnitId(), saveReqVO.getRateItemId()); + + WbUnitRateSettingDO settingDO; + if (existingSetting != null) { + // 更新 + settingDO = existingSetting; + settingDO.setSelectedValueId(saveReqVO.getSelectedValueId()); + settingDO.setInputValue(saveReqVO.getInputValue()); + settingDO.setSettingName(saveReqVO.getSettingName()); + } else { + // 新建 + settingDO = BeanUtils.toBean(saveReqVO, WbUnitRateSettingDO.class); + } + + // 计算或使用用户提供的字段值 + Map fieldValues; + if (saveReqVO.getFieldValues() != null && !saveReqVO.getFieldValues().isEmpty()) { + // 用户直接提供了字段值(虚拟字段修改) + fieldValues = existingSetting != null && existingSetting.getFieldValues() != null + ? new HashMap<>(existingSetting.getFieldValues()) + : new HashMap<>(); + // 合并用户修改的字段值 + // 注意:null 值也要保存,表示用户清空了该字段(显示为空) + for (Map.Entry entry : saveReqVO.getFieldValues().entrySet()) { + fieldValues.put(entry.getKey(), entry.getValue()); + } + } else { + // 通过选择或输入值计算字段值 + fieldValues = calculateFieldValues(saveReqVO); + } + settingDO.setFieldValues(fieldValues); + + // 保存 + if (existingSetting != null) { + unitRateSettingMapper.updateById(settingDO); + } else { + // 首次保存时,设置基线值 + settingDO.setBaseValue(saveReqVO.getInputValue()); + settingDO.setBaseFieldValues(fieldValues); + unitRateSettingMapper.insert(settingDO); + } + + return BeanUtils.toBean(settingDO, UnitRateSettingRespVO.class); + } + + @Override + public List getListByUnitId(Long unitId) { + List list = unitRateSettingMapper.selectByUnitId(unitId); + return list.stream() + .map(item -> BeanUtils.toBean(item, UnitRateSettingRespVO.class)) + .collect(Collectors.toList()); + } + + @Override + public UnitRateSettingRespVO getByUnitIdAndRateItemId(Long unitId, Long rateItemId) { + WbUnitRateSettingDO setting = unitRateSettingMapper.selectByUnitIdAndRateItemId(unitId, rateItemId); + return setting != null ? BeanUtils.toBean(setting, UnitRateSettingRespVO.class) : null; + } + + @Override + public void delete(Long id) { + unitRateSettingMapper.deleteById(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void initializeSettings(Long unitId, Long catalogItemId) { + // 获取费率项树 + List rateItems = quotaRateItemMapper.selectListByCatalogItemId(catalogItemId); + + // 为每个目录节点创建初始设定 + for (QuotaRateItemDO rateItem : rateItems) { + if ("directory".equals(rateItem.getNodeType())) { + WbUnitRateSettingDO setting = new WbUnitRateSettingDO(); + setting.setUnitId(unitId); + setting.setRateItemId(rateItem.getId()); + setting.setSettingName(rateItem.getName()); + + // 设置默认值和基线值 + if (rateItem.getDefaultValue() != null) { + setting.setInputValue(rateItem.getDefaultValue()); + setting.setBaseValue(rateItem.getDefaultValue()); + } + + unitRateSettingMapper.insert(setting); + } + } + } + + /** + * 计算字段值 + */ + private Map calculateFieldValues(UnitRateSettingSaveReqVO saveReqVO) { + Map fieldValues = new HashMap<>(); + + if (saveReqVO.getSelectedValueId() != null) { + // 下拉模式:从选中的值节点获取字段值 + // 优先从快照表查询,如果不存在则从后台标准库查询 + Map settings = getSettingsFromRateItem(saveReqVO.getSelectedValueId()); + if (settings != null) { + Object fieldValuesObj = settings.get("fieldValues"); + if (fieldValuesObj instanceof Map) { + @SuppressWarnings("unchecked") + Map fv = (Map) fieldValuesObj; + for (Map.Entry entry : fv.entrySet()) { + try { + int fieldIndex = Integer.parseInt(entry.getKey()); + BigDecimal value = new BigDecimal(entry.getValue().toString()); + fieldValues.put(fieldIndex, value); + } catch (NumberFormatException e) { + log.warn("无法解析字段值: key={}, value={}", entry.getKey(), entry.getValue()); + } + } + } + } + } else if (saveReqVO.getInputValue() != null) { + // 填写模式:从快照表获取费率项的settings进行计算 + Map settings = getSettingsFromRateItem(saveReqVO.getRateItemId()); + if (settings != null && settings.containsKey("valueRules")) { + // 使用快照数据的valueRules进行计算 + Map calculated = calculateDynamicFieldsFromSettings( + settings, saveReqVO.getInputValue()); + if (calculated != null) { + fieldValues.putAll(calculated); + } + } + // 不再回退到后台标准库,只使用快照数据 + } + + return fieldValues; + } + + /** + * 从费率项获取settings(只查询快照表) + */ + private Map getSettingsFromRateItem(Long rateItemId) { + // 只从快照表查询,不回退到后台标准库 + WbRateItemDO wbRateItem = wbRateItemMapper.selectById(rateItemId); + if (wbRateItem != null) { + log.debug("[费率设置] 从快照表读取费率项, rateItemId={}", rateItemId); + return wbRateItem.getSettings(); + } + + log.warn("[费率设置] 快照表中费率项不存在, rateItemId={}", rateItemId); + return null; + } + + /** + * 根据settings中的valueRules计算动态字段值 + */ + @SuppressWarnings("unchecked") + private Map calculateDynamicFieldsFromSettings(Map settings, BigDecimal inputValue) { + Map result = new HashMap<>(); + + Object valueRulesObj = settings.get("valueRules"); + if (!(valueRulesObj instanceof Map)) { + return result; + } + + Map valueRules = (Map) valueRulesObj; + Object tiersObj = valueRules.get("tiers"); + if (!(tiersObj instanceof List)) { + return result; + } + + List> tiers = (List>) tiersObj; + + // 查找匹配的tier + for (Map tier : tiers) { + Object thresholdObj = tier.get("threshold"); + Object compareTypeObj = tier.get("compareType"); + + if (thresholdObj == null || compareTypeObj == null) continue; + + BigDecimal threshold = new BigDecimal(thresholdObj.toString()); + String compareType = compareTypeObj.toString(); + + boolean matched = false; + if ("lte".equals(compareType)) { + matched = inputValue.compareTo(threshold) <= 0; + } else if ("lt".equals(compareType)) { + matched = inputValue.compareTo(threshold) < 0; + } else if ("gte".equals(compareType)) { + matched = inputValue.compareTo(threshold) >= 0; + } else if ("gt".equals(compareType)) { + matched = inputValue.compareTo(threshold) > 0; + } + + if (matched) { + Object fieldValuesObj = tier.get("fieldValues"); + if (fieldValuesObj instanceof Map) { + Map fv = (Map) fieldValuesObj; + for (Map.Entry entry : fv.entrySet()) { + try { + int fieldIndex = Integer.parseInt(entry.getKey()); + BigDecimal value = new BigDecimal(entry.getValue().toString()); + result.put(fieldIndex, value); + } catch (NumberFormatException e) { + log.warn("无法解析字段值: key={}, value={}", entry.getKey(), entry.getValue()); + } + } + } + break; + } + } + + return result; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/AuditModeService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/AuditModeService.java new file mode 100644 index 0000000..056bb9d --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/AuditModeService.java @@ -0,0 +1,63 @@ +package com.yhy.module.core.service.workbench; + +import com.yhy.module.core.controller.admin.workbench.vo.audit.AuditApproveDivisionUpdateReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.audit.AuditDivisionTreeRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.audit.AuditModeCreateReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.audit.AuditModeRespVO; +import java.util.List; +import javax.validation.Valid; + +/** + * 审核模式 Service 接口 + * + * @author yhy + */ +public interface AuditModeService { + + /** + * 创建审核模式(复制审定快照) + * + * @param createReqVO 创建信息 + * @return 审核模式ID + */ + Long createAuditMode(@Valid AuditModeCreateReqVO createReqVO); + + /** + * 删除审核模式 + * + * @param id 审核模式ID + */ + void deleteAuditMode(Long id); + + /** + * 获取审核模式详情 + * + * @param id 审核模式ID + * @return 审核模式详情 + */ + AuditModeRespVO getAuditMode(Long id); + + /** + * 获取项目下的审核模式列表 + * + * @param projectId 项目ID + * @return 审核模式列表 + */ + List getAuditModeList(Long projectId); + + /** + * 更新审定数据 + * + * @param updateReqVO 更新信息 + */ + void updateApproveData(@Valid AuditApproveDivisionUpdateReqVO updateReqVO); + + /** + * 获取分部分项树(包含送审、审定、差异三组数据) + * + * @param auditModeId 审核模式ID + * @param compileTreeId 编制模式树的单位工程节点ID + * @return 分部分项树 + */ + List getDivisionTree(Long auditModeId, Long compileTreeId); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/ProgressDivisionService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/ProgressDivisionService.java new file mode 100644 index 0000000..b96be85 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/ProgressDivisionService.java @@ -0,0 +1,82 @@ +package com.yhy.module.core.service.workbench; + +import com.yhy.module.core.controller.admin.workbench.vo.progresspayment.ProgressDivisionTreeRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.progresspayment.ProgressPaymentModeWithDivisionsRespVO; +import com.yhy.module.core.dal.dataobject.workbench.ProgressDivisionDO; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +/** + * 进度款-清单关联 Service 接口 + * + * @author yhy + */ +public interface ProgressDivisionService { + + /** + * 保存或更新期数值 + * 如果已存在则更新,不存在则创建 + * + * @param progressPaymentModeId 进度款模式ID + * @param boqDivisionId 清单节点ID + * @param periodValue 期数值 + * @return 关联ID + */ + Long saveOrUpdate(Long progressPaymentModeId, Long boqDivisionId, BigDecimal periodValue); + + /** + * 批量保存或更新期数值 + * + * @param progressPaymentModeId 进度款模式ID + * @param periodValueMap key=清单节点ID, value=期数值 + */ + void batchSaveOrUpdate(Long progressPaymentModeId, Map periodValueMap); + + /** + * 根据进度款模式ID获取所有关联数据 + * + * @param progressPaymentModeId 进度款模式ID + * @return 关联列表 + */ + List getListByProgressPaymentModeId(Long progressPaymentModeId); + + /** + * 根据进度款模式ID获取清单ID到期数值的映射 + * + * @param progressPaymentModeId 进度款模式ID + * @return Map<清单节点ID, 期数值> + */ + Map getPeriodValueMap(Long progressPaymentModeId); + + /** + * 根据进度款模式ID删除所有关联 + * + * @param progressPaymentModeId 进度款模式ID + */ + void deleteByProgressPaymentModeId(Long progressPaymentModeId); + + /** + * 根据清单节点ID删除所有关联 + * + * @param boqDivisionId 清单节点ID + */ + void deleteByBoqDivisionId(Long boqDivisionId); + + /** + * 获取分部分项树(含定额单价计算,并绑定进度款期数信息) + * + * @param compileTreeId 编制模式树的单位工程节点ID + * @param progressPaymentModeId 进度款模式ID + * @return 树形结构(清单节点包含期数信息) + */ + List getTreeWithPeriodInfo(Long compileTreeId, Long progressPaymentModeId); + + /** + * 获取项目下的进度款模式列表(含关联清单) + * + * @param projectId 项目ID + * @return 进度款模式列表(每个模式包含关联的清单列表) + */ + List getProgressPaymentModeWithDivisionsList(Long projectId); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/ProgressPaymentModeService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/ProgressPaymentModeService.java new file mode 100644 index 0000000..e7976ec --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/ProgressPaymentModeService.java @@ -0,0 +1,53 @@ +package com.yhy.module.core.service.workbench; + +import com.yhy.module.core.controller.admin.workbench.vo.progresspayment.ProgressPaymentModeCreateReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.progresspayment.ProgressPaymentModeRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.progresspayment.ProgressPaymentModeUpdateReqVO; +import java.util.List; +import javax.validation.Valid; + +/** + * 进度款模式 Service 接口 + * + * @author yhy + */ +public interface ProgressPaymentModeService { + + /** + * 创建进度款模式 + * + * @param createReqVO 创建信息 + * @return 进度款模式ID + */ + Long createProgressPaymentMode(@Valid ProgressPaymentModeCreateReqVO createReqVO); + + /** + * 更新进度款模式 + * + * @param updateReqVO 更新信息 + */ + void updateProgressPaymentMode(@Valid ProgressPaymentModeUpdateReqVO updateReqVO); + + /** + * 删除进度款模式 + * + * @param id 进度款模式ID + */ + void deleteProgressPaymentMode(Long id); + + /** + * 获取进度款模式详情 + * + * @param id 进度款模式ID + * @return 进度款模式详情 + */ + ProgressPaymentModeRespVO getProgressPaymentMode(Long id); + + /** + * 获取项目下的进度款模式列表 + * + * @param projectId 项目ID + * @return 进度款模式列表 + */ + List getProgressPaymentModeList(Long projectId); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/QuotaPriceCalculatorService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/QuotaPriceCalculatorService.java new file mode 100644 index 0000000..629cdd4 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/QuotaPriceCalculatorService.java @@ -0,0 +1,166 @@ +package com.yhy.module.core.service.workbench; + +import com.yhy.module.core.controller.admin.quota.vo.QuotaFeeItemWithRateRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.price.CategoryPriceSum; +import com.yhy.module.core.controller.admin.workbench.vo.price.FeeItemPriceResult; +import com.yhy.module.core.controller.admin.workbench.vo.price.QuotaUnitPriceResult; +import com.yhy.module.core.controller.admin.workbench.vo.price.ResourcePriceDetail; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +/** + * 定额单价计算服务 + * + * 设计原则: + * 1. 复用后台已有的工料机合价计算逻辑 + * 2. 新增分类汇总和取费计算逻辑 + * 3. 工作台和后台共用此服务 + * 4. 支持分层验证和调试 + * + * @author yhy + */ +public interface QuotaPriceCalculatorService { + + // ========== 主计算接口 ========== + + /** + * 计算定额单价(简洁版,仅返回最终结果) + * + * @param quotaItemId 定额基价ID(后台用) + * @param divisionId 分部分项定额节点ID(工作台用,可选) + * @return 定额单价计算结果 + */ + QuotaUnitPriceResult calculateQuotaUnitPrice(Long quotaItemId, Long divisionId); + + /** + * 计算定额单价(调试版,返回完整计算过程) + * + * @param quotaItemId 定额基价ID + * @param divisionId 分部分项定额节点ID + * @param debug 是否返回调试信息 + * @return 定额单价计算结果(含调试信息) + */ + QuotaUnitPriceResult calculateQuotaUnitPrice(Long quotaItemId, Long divisionId, boolean debug); + + /** + * 批量计算定额单价 + * + * @param divisionIds 分部分项定额节点ID列表 + * @return 定额单价计算结果Map,key=divisionId + */ + Map batchCalculateQuotaUnitPrices(List divisionIds); + + // ========== 分层验证接口(单独测试每一层) ========== + + /** + * 【第3层】按机类分类汇总 + * + * @param resourceDetails 工料机价格明细列表 + * @return 分类汇总Map,key=categoryId + */ + Map calculateCategorySums(List resourceDetails); + + /** + * 【第4层】解析计算基数公式 + * + * @param calcBase 计算基数配置 + * @param categorySums 分类汇总 + * @return 公式计算结果 + */ + BigDecimal evaluateCalcBaseFormula(Map calcBase, Map categorySums); + + /** + * 【第5层】计算单个取费项的子单价 + * + * @param feeItem 取费项(含计算基数) + * @param categorySums 分类汇总 + * @return 子单价计算结果 + */ + FeeItemPriceResult calculateFeeItemPrice(QuotaFeeItemWithRateRespVO feeItem, Map categorySums); + + /** + * 【第5层】批量计算取费项的子单价 + * + * @param feeItems 取费项列表 + * @param categorySums 分类汇总 + * @return 子单价计算结果列表 + */ + List calculateFeeItemPrices(List feeItems, Map categorySums); + + /** + * 【第6层】计算综合单价(定额单价) + * + * @param feeItemResults 取费项计算结果列表 + * @return 综合单价 + */ + BigDecimal calculateTotalPrice(List feeItemResults); + + // ========== 统一取费父定额单价计算 ========== + + /** + * 计算统一取费父定额的单价 + * + * 计算流程: + * 1. 获取母定额列表(汇总来源的quota节点) + * 2. 对每个母定额: + * a. 获取母定额的工料机分类合价(CategoryPriceSum) + * b. 获取统一取费单价的取费项配置 + * c. 用母定额的分类合价替换取费项的calcBase变量 + * d. 计算子定额的综合单价B + * e. B × 母定额的工程量 = 该母定额贡献的单价 + * 3. 父定额单价C = SUM(各母定额贡献的单价) + * + * @param divisionId 统一取费节点ID(nodeType='unified_fee') + * @return 父定额单价 + */ + BigDecimal calculateUnifiedFeeParentPrice(Long divisionId); + + /** + * 计算统一取费父定额的单价(调试版) + * + * @param divisionId 统一取费节点ID + * @param debug 是否返回调试信息 + * @return 父定额单价计算结果(含调试信息) + */ + QuotaUnitPriceResult calculateUnifiedFeeParentPrice(Long divisionId, boolean debug); + + /** + * 获取统一取费节点的分类合价(用于基数范围汇总) + * + * 计算逻辑: + * 1. 获取母定额列表(通过 motherQuotaIds) + * 2. 对每个母定额获取工料机分类合价(CategoryPriceSum) + * 3. 遍历子费用的子目工料机: + * - %工料机:用 calcBase 公式引用母定额分类合价计算实际合价 + * - 普通工料机:用单价 × 消耗量 + * 4. 按工料机的 categoryId 汇总四个合价字段 + * + * @param divisionId 统一取费节点ID(nodeType='unified_fee') + * @return 分类合价Map,key=categoryId;如果节点无效或无数据返回空Map + */ + Map getUnifiedFeeCategorySums(Long divisionId); + + // ========== 未来快照功能预留 ========== + + // /** + // * 创建定额单价快照 + // * @param divisionId 定额节点ID + // * @return 快照数据(JSON格式) + // */ + // Map createPriceSnapshot(Long divisionId); + + // /** + // * 基于快照计算定额单价(用于历史版本查看) + // * @param snapshotJson 快照数据 + // * @return 定额单价计算结果 + // */ + // QuotaUnitPriceResult calculateFromSnapshot(Map snapshotJson); + + // /** + // * 对比当前值与快照值 + // * @param divisionId 定额节点ID + // * @return 差异列表 + // */ + // List compareWithSnapshot(Long divisionId); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/QuotaQtyFormulaService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/QuotaQtyFormulaService.java new file mode 100644 index 0000000..81b6389 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/QuotaQtyFormulaService.java @@ -0,0 +1,100 @@ +package com.yhy.module.core.service.workbench; + +import java.math.BigDecimal; + +/** + * 定额工程量公式计算服务 + * + * @author yhy + */ +public interface QuotaQtyFormulaService { + + /** + * 验证公式合法性 + * + * @param formula 公式字符串(如 QDL*2) + * @return 验证结果 + */ + FormulaValidationResult validateFormula(String formula); + + /** + * 计算定额工程量 + * + * @param formula 公式字符串(如 QDL*2) + * @param qdlValue 清单工程量值 + * @return 计算结果(四舍五入保留3位小数) + */ + BigDecimal calculateQuotaQty(String formula, BigDecimal qdlValue); + + /** + * 计算定额工程量(带单位换算) + * + * @param formula 公式字符串(如 QDL*2) + * @param qdlValue 清单工程量值 + * @param quotaUnit 定额单位(如"100m"、"m"、"件") + * @return 计算结果(四舍五入保留3位小数) + * + * 单位换算规则: + * - 如果单位以数字开头(如"100m"),则提取数字作为换算系数,QDL = 清单工程量 / 系数 + * - 如果单位是纯字符串(如"m"、"件"),则不换算,QDL = 清单工程量 + */ + BigDecimal calculateQuotaQty(String formula, BigDecimal qdlValue, String quotaUnit); + + /** + * 从单位字符串中解析换算系数 + * + * @param unit 单位字符串(如"100m"、"m"、"件") + * @return 换算系数(如100、1) + */ + BigDecimal parseUnitFactor(String unit); + + /** + * 根据定额ID重新计算工程量 + * + * @param quotaDivisionId 定额节点ID + * @return 计算后的工程量 + */ + BigDecimal recalculateQuotaQty(Long quotaDivisionId); + + /** + * 清单工程量变化时,重新计算所有子定额的工程量 + * + * @param boqDivisionId 清单节点ID + */ + void recalculateChildQuotaQty(Long boqDivisionId); + + /** + * 公式验证结果 + */ + class FormulaValidationResult { + private boolean valid; + private String error; + private BigDecimal testResult; + + public static FormulaValidationResult success(BigDecimal testResult) { + FormulaValidationResult result = new FormulaValidationResult(); + result.valid = true; + result.testResult = testResult; + return result; + } + + public static FormulaValidationResult failure(String error) { + FormulaValidationResult result = new FormulaValidationResult(); + result.valid = false; + result.error = error; + return result; + } + + public boolean isValid() { + return valid; + } + + public String getError() { + return error; + } + + public BigDecimal getTestResult() { + return testResult; + } + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/SyncLibraryService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/SyncLibraryService.java new file mode 100644 index 0000000..fee51af --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/SyncLibraryService.java @@ -0,0 +1,71 @@ +package com.yhy.module.core.service.workbench; + +import com.yhy.module.core.controller.admin.workbench.vo.sync.ApplySyncReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.sync.SetSyncReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.sync.SyncLibraryDivisionRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.sync.SyncLibraryDivisionUpdateReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.sync.SyncPendingRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.sync.SyncSourceUnitRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.sync.UnsetSyncReqVO; +import java.util.List; +import javax.validation.Valid; + +/** + * 同步库 Service 接口 + * + * @author yhy + */ +public interface SyncLibraryService { + + /** + * 获取项目同步库的分部分项树(合并所有同步库记录) + * + * @param projectId 项目ID + * @return 树结构 + */ + List getTree(Long projectId); + + /** + * 获取项目同步库的来源单位工程列表 + * + * @param projectId 项目ID + * @return 来源单位工程列表 + */ + List getSourceUnits(Long projectId); + + /** + * 更新同步库分部分项节点 + * + * @param reqVO 更新请求 + */ + void updateDivision(@Valid SyncLibraryDivisionUpdateReqVO reqVO); + + /** + * 设为同步 + * + * @param reqVO 请求 + */ + void setSync(@Valid SetSyncReqVO reqVO); + + /** + * 解除同步 + * + * @param reqVO 请求 + */ + void unsetSync(@Valid UnsetSyncReqVO reqVO); + + /** + * 套用同步 + * + * @param reqVO 请求 + */ + void applySync(@Valid ApplySyncReqVO reqVO); + + /** + * 检查是否有待同步的变更 + * + * @param compileTreeId 单位工程ID + * @return 待同步变更列表 + */ + List checkPending(Long compileTreeId); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbAdjustmentSettingService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbAdjustmentSettingService.java new file mode 100644 index 0000000..1d5da6a --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbAdjustmentSettingService.java @@ -0,0 +1,90 @@ +package com.yhy.module.core.service.workbench; + +import com.yhy.module.core.dal.dataobject.workbench.WbAdjustmentSettingDO; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +/** + * 工作台定额调整设置 Service 接口 + * + * @author yhy + */ +public interface WbAdjustmentSettingService { + + /** + * 根据分部分项定额节点ID获取调整设置列表 + * + * @param divisionId 定额节点ID + * @return 调整设置列表 + */ + List getListByDivisionId(Long divisionId); + + /** + * 从后台定额调整设置复制到工作台 + * 当定额节点首次加载调整设置时,从后台复制数据 + * + * @param divisionId 定额节点ID + * @param sourceQuotaItemId 来源定额基价ID + * @return 复制后的调整设置列表 + */ + List copyFromQuotaAdjustmentSetting(Long divisionId, Long sourceQuotaItemId); + + /** + * 更新调整设置 + * + * @param id 调整设置ID + * @param adjustmentRules 调整规则 + */ + void updateAdjustmentRules(Long id, Map adjustmentRules); + + /** + * 应用调整设置(复选框类型) + * + * @param divisionId 定额节点ID + * @param adjustmentSettingId 调整设置ID + * @param enabled 是否启用 + */ + void applyAdjustmentSetting(Long divisionId, Long adjustmentSettingId, boolean enabled); + + /** + * 应用动态调整 + * + * @param divisionId 定额节点ID + * @param adjustmentSettingId 调整设置ID + * @param inputValues 输入值(类别ID -> 输入值) + */ + void applyDynamicAdjustment(Long divisionId, Long adjustmentSettingId, Map inputValues); + + /** + * 应用动态合并定额 + * + * @param divisionId 定额节点ID + * @param adjustmentSettingId 调整设置ID + * @param inputValues 输入值(定额编码 -> 输入值) + */ + void applyDynamicMerge(Long divisionId, Long adjustmentSettingId, Map inputValues); + + /** + * 删除定额节点下的所有调整设置 + * + * @param divisionId 定额节点ID + */ + void deleteByDivisionId(Long divisionId); + + /** + * 获取调整设置与明细的组合列表(用于前端展示) + * + * @param divisionId 定额节点ID + * @return 组合列表 + */ + List> getAdjustmentCombinedList(Long divisionId); + + /** + * 更新调整设置的调整内容 + * + * @param id 调整设置ID + * @param adjustmentContent 调整内容 + */ + void updateAdjustmentSetting(Long id, String adjustmentContent); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbBoqDivisionService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbBoqDivisionService.java new file mode 100644 index 0000000..d7f8b65 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbBoqDivisionService.java @@ -0,0 +1,200 @@ +package com.yhy.module.core.service.workbench; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.yhy.module.core.controller.admin.workbench.vo.HistoryBoqListReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqDivisionRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqDivisionSaveReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqDivisionSwapSortReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.calcbaserate.UpdateBoqDivisionBaseRateReqVO; +import java.util.List; +import javax.validation.Valid; + +/** + * 工作台分部分项树 Service 接口 + * + * @author yhy + */ +public interface WbBoqDivisionService { + + /** + * 创建节点(分部、清单或定额) + * + * @param createReqVO 创建信息 + * @return 节点ID + */ + Long createNode(@Valid WbBoqDivisionSaveReqVO createReqVO); + + /** + * 更新节点 + * + * @param updateReqVO 更新信息 + */ + void updateNode(@Valid WbBoqDivisionSaveReqVO updateReqVO); + + /** + * 删除节点 + * + * @param id 节点ID + */ + void deleteNode(Long id); + + /** + * 获取节点详情 + * + * @param id 节点ID + * @return 节点详情 + */ + WbBoqDivisionRespVO getNode(Long id); + + /** + * 获取单位工程的分部分项树 + * + * @param compileTreeId 编制模式树的单位工程节点ID + * @return 树形结构 + */ + List getTree(Long compileTreeId); + + /** + * 获取单位工程的分部分项树(支持过滤工程量为0的清单) + * + * @param compileTreeId 编制模式树的单位工程节点ID + * @param excludeZeroQtyBoq 是否过滤工程量为0或空的清单节点 + * @return 树形结构 + */ + List getTree(Long compileTreeId, Boolean excludeZeroQtyBoq); + + /** + * 交换排序 + * + * @param swapReqVO 交换信息 + */ + void swapSort(@Valid WbBoqDivisionSwapSortReqVO swapReqVO); + + /** + * 根据单位工程节点ID删除所有分部分项 + * + * @param compileTreeId 编制模式树的单位工程节点ID + */ + void deleteByCompileTreeId(Long compileTreeId); + + /** + * 获取单位工程的分部分项树(含定额单价计算) + * + * @param compileTreeId 编制模式树的单位工程节点ID + * @return 树形结构(定额节点包含计算后的单价和合价) + */ + List getTreeWithPrice(Long compileTreeId); + + /** + * 获取单位工程的分部分项树(含定额单价计算,调试模式) + * + * @param compileTreeId 编制模式树的单位工程节点ID + * @param debug 是否返回调试信息 + * @return 树形结构(定额节点包含计算后的单价和合价) + */ + List getTreeWithPrice(Long compileTreeId, boolean debug); + + /** + * 创建分部分项根目录节点(系统自动创建,用于单位工程创建时) + * + * @param compileTreeId 编制模式树的单位工程节点ID + * @param unitName 单位工程名称 + * @return 根目录节点ID + */ + Long createRootNode(Long compileTreeId, String unitName); + + /** + * 获取租户所有清单节点(历史套用) + * + * @return 清单节点列表 + */ + List getAllBoqNodes(); + + /** + * 复制清单到目标位置(含定额和工料机) + * + * @param sourceBoqId 源清单节点ID + * @param targetCompileTreeId 目标单位工程节点ID + * @param targetParentId 目标父节点ID(分部或清单的父节点) + * @return 新创建的清单节点ID + */ + Long copyBoqWithChildren(Long sourceBoqId, Long targetCompileTreeId, Long targetParentId); + + /** + * 获取历史清单/分部列表(支持查询条件,分页) + * + * @param reqVO 查询参数(含分页) + * @return 清单/分部节点分页结果 + */ + PageResult getHistoryListPage(HistoryBoqListReqVO reqVO); + + /** + * 复制分部到目标位置(含子清单、定额和工料机) + * + * @param sourceDivisionId 源分部节点ID + * @param targetCompileTreeId 目标单位工程节点ID + * @param targetParentId 目标父节点ID + * @return 新创建的分部节点ID + */ + Long copyDivisionWithChildren(Long sourceDivisionId, Long targetCompileTreeId, Long targetParentId); + + /** + * 从清单指引复制清单子目到分部分项(含定额和工料机) + * + * @param compileTreeId 目标单位工程节点ID + * @param parentId 目标父节点ID(必须是分部节点) + * @param boqSubItemId 清单子目ID(标准库) + * @param sourceBoqItemTreeId 清单项树节点ID(标准库) + * @return 新创建的清单节点ID + */ + Long copyFromGuide(Long compileTreeId, Long parentId, Long boqSubItemId, Long sourceBoqItemTreeId); + + /** + * 更新清单的基数费率 + * + * @param reqVO 更新信息 + */ + void updateBoqDivisionBaseRate(@Valid UpdateBoqDivisionBaseRateReqVO reqVO); + + /** + * 导入模板到工作台(从后台单位工程界面配置复制分部分项模板和标签页引用) + * + * @param compileTreeId 单位工程节点ID + * @param catalogItemId fields_majors 节点ID(用于查询模板) + */ + void importTemplate(Long compileTreeId, Long catalogItemId); + + /** + * 按标签页获取分部分项树 + * - tabType=division 或 null:返回全量树(数据总线完整视图) + * - tabType=measure/other/unit_summary:只返回对应 tabType 的节点及其子节点 + * + * @param compileTreeId 编制模式树的单位工程节点ID + * @param tabType 标签页类型(division/measure/other/unit_summary) + * @return 树形结构 + */ + List getTreeByTab(Long compileTreeId, String tabType); + + /** + * 按标签页获取分部分项树(支持过滤工程量为0的清单) + * + * @param compileTreeId 编制模式树的单位工程节点ID + * @param tabType 标签页类型(division/measure/other/unit_summary) + * @param excludeZeroQtyBoq 是否过滤工程量为0或空的清单节点 + * @return 树形结构 + */ + List getTreeByTab(Long compileTreeId, String tabType, Boolean excludeZeroQtyBoq); + + /** + * 获取统一取费范围选择树 + * 专供统一取费弹窗使用,按标签页过滤后: + * 1. 过滤掉 unified_fee 类型节点 + * 2. 如果传了 feeChapter,根据取费章节过滤定额,并裁剪空清单 + * + * @param compileTreeId 编制模式树的单位工程节点ID + * @param tabType 标签页类型(division/measure) + * @param feeChapter 取费章节ID列表(可选,为空则不过滤定额) + * @return 树形结构 + */ + List getTreeForUnifiedFee(Long compileTreeId, String tabType, List feeChapter); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbBoqMarketMaterialService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbBoqMarketMaterialService.java new file mode 100644 index 0000000..fea147c --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbBoqMarketMaterialService.java @@ -0,0 +1,68 @@ +package com.yhy.module.core.service.workbench; + +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqMarketMaterialRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqMarketMaterialSaveReqVO; +import com.yhy.module.core.dal.dataobject.workbench.WbBoqMarketMaterialDO; +import java.util.List; +import javax.validation.Valid; + +/** + * 工作台市场主材设备 Service 接口 + * + * @author yhy + */ +public interface WbBoqMarketMaterialService { + + /** + * 创建市场主材设备 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createMarketMaterial(@Valid WbBoqMarketMaterialSaveReqVO createReqVO); + + /** + * 更新市场主材设备 + * + * @param updateReqVO 更新信息 + */ + void updateMarketMaterial(@Valid WbBoqMarketMaterialSaveReqVO updateReqVO); + + /** + * 删除市场主材设备 + * + * @param id 编号 + */ + void deleteMarketMaterial(Long id); + + /** + * 获得市场主材设备 + * + * @param id 编号 + * @return 市场主材设备 + */ + WbBoqMarketMaterialDO getMarketMaterial(Long id); + + /** + * 获得市场主材设备列表 + * + * @param divisionId 分部分项ID + * @return 市场主材设备列表 + */ + List getMarketMaterialList(Long divisionId); + + /** + * 从定额复制市场主材设备到工作台 + * + * @param divisionId 分部分项ID(定额节点) + * @param quotaItemId 定额基价ID + */ + void copyFromQuota(Long divisionId, Long quotaItemId); + + /** + * 批量删除分部分项下的所有市场主材设备 + * + * @param divisionIds 分部分项ID列表 + */ + void deleteByDivisionIds(List divisionIds); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbBoqResourceService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbBoqResourceService.java new file mode 100644 index 0000000..bcc8eae --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbBoqResourceService.java @@ -0,0 +1,118 @@ +package com.yhy.module.core.service.workbench; + +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceCategorySummaryVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceSaveReqVO; +import java.util.List; +import javax.validation.Valid; + +/** + * 工作台工料机消耗 Service 接口 + * + * @author yhy + */ +public interface WbBoqResourceService { + + /** + * 创建工料机消耗 + * + * @param createReqVO 创建信息 + * @return ID + */ + Long create(@Valid WbBoqResourceSaveReqVO createReqVO); + + /** + * 更新工料机消耗 + * 当编制价/价格来源变更时,会批量同步项目内同编码+名称+规格+单位的工料机 + * + * @param updateReqVO 更新信息 + * @return 批量更新结果(受影响的单位工程和行数),无批量同步时返回null + */ + com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceBatchUpdateResultVO update(@Valid WbBoqResourceSaveReqVO updateReqVO); + + /** + * 删除工料机消耗 + * + * @param id ID + */ + void delete(Long id); + + /** + * 获取工料机消耗详情 + * + * @param id ID + * @return 详情 + */ + WbBoqResourceRespVO get(Long id); + + /** + * 获取定额节点下的工料机列表 + * + * @param divisionId 定额节点ID + * @return 工料机列表 + */ + List getListByDivisionId(Long divisionId); + + /** + * 根据定额节点ID删除所有工料机 + * + * @param divisionId 定额节点ID + */ + void deleteByDivisionId(Long divisionId); + + /** + * 从后台定额工料机复制数据到工作台(快照模式) + * + * @param divisionId 定额节点ID + * @param quotaItemId 后台定额基价ID + */ + void copyFromQuotaResource(Long divisionId, Long quotaItemId); + + /** + * 获取清单节点下所有定额的工料机汇总 + * + * @param boqDivisionId 清单节点ID + * @return 工料机汇总列表 + */ + List getListByBoqDivisionId(Long boqDivisionId); + + /** + * 获取定额节点下工料机的分类汇总(按类别汇总四个合价) + * + * @param divisionId 定额节点ID + * @return 分类汇总列表 + */ + List getCategorySummary(Long divisionId); + + /** + * 根据多个定额节点ID获取工料机列表 + * + * @param divisionIds 定额节点ID列表 + * @return 工料机列表 + */ + List getResourcesByDivisionIds(List divisionIds); + + /** + * 编码弹窗查询工料机(本项目+非项目) + * + * @param divisionId 定额节点ID + * @param code 编码(模糊查询) + * @return 查询结果列表 + */ + List searchByCode(Long divisionId, String code); + + /** + * 创建空白工料机行 + * + * @param createBlankReqVO 创建请求 + * @return 新行ID + */ + Long createBlank(com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceCreateBlankReqVO createBlankReqVO); + + /** + * 双击填充工料机数据到空白行 + * + * @param fillReqVO 填充请求 + */ + void fillFromSearch(com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceFillReqVO fillReqVO); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbCompileTreeService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbCompileTreeService.java new file mode 100644 index 0000000..ded1540 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbCompileTreeService.java @@ -0,0 +1,69 @@ +package com.yhy.module.core.service.workbench; + +import com.yhy.module.core.controller.admin.workbench.vo.WbCompileTreeRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbCompileTreeSaveReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbCompileTreeSwapSortReqVO; +import java.util.List; +import javax.validation.Valid; + +/** + * 编制模式树 Service 接口 + * + * @author yhy + */ +public interface WbCompileTreeService { + + /** + * 初始化项目的编制模式树(创建根节点) + * + * @param projectId 项目节点ID + * @param projectName 项目名称 + * @return 根节点ID + */ + Long initCompileTree(Long projectId, String projectName); + + /** + * 创建节点(单项或单位工程) + * + * @param createReqVO 创建信息 + * @return 节点ID + */ + Long createNode(@Valid WbCompileTreeSaveReqVO createReqVO); + + /** + * 更新节点 + * + * @param updateReqVO 更新信息 + */ + void updateNode(@Valid WbCompileTreeSaveReqVO updateReqVO); + + /** + * 删除节点 + * + * @param id 节点ID + */ + void deleteNode(Long id); + + /** + * 获取节点详情 + * + * @param id 节点ID + * @return 节点详情 + */ + WbCompileTreeRespVO getNode(Long id); + + /** + * 获取项目的编制模式树 + * + * @param projectId 项目节点ID + * @return 树形结构 + */ + List getTree(Long projectId); + + /** + * 交换排序 + * + * @param swapReqVO 交换信息 + */ + void swapSort(@Valid WbCompileTreeSwapSortReqVO swapReqVO); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbItemInfoService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbItemInfoService.java new file mode 100644 index 0000000..b7053f2 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbItemInfoService.java @@ -0,0 +1,43 @@ +package com.yhy.module.core.service.workbench; + +import com.yhy.module.core.controller.admin.workbench.vo.WbItemInfoRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbItemInfoSaveReqVO; +import javax.validation.Valid; + +/** + * 单项基本信息 Service 接口 + * + * @author yhy + */ +public interface WbItemInfoService { + + /** + * 获取单项基本信息 + * + * @param compileTreeId 编制树节点ID(单项节点) + * @return 单项基本信息 + */ + WbItemInfoRespVO getByCompileTreeId(Long compileTreeId); + + /** + * 保存单项基本信息 + * + * @param saveReqVO 保存信息 + * @return ID + */ + Long save(@Valid WbItemInfoSaveReqVO saveReqVO); + + /** + * 刷新配置快照 + * + * @param compileTreeId 编制树节点ID(单项节点) + */ + void refreshConfigSnapshot(Long compileTreeId); + + /** + * 删除单项基本信息(根据编制树节点ID) + * + * @param compileTreeId 编制树节点ID + */ + void deleteByCompileTreeId(Long compileTreeId); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbProjectService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbProjectService.java new file mode 100644 index 0000000..11a8972 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbProjectService.java @@ -0,0 +1,80 @@ +package com.yhy.module.core.service.workbench; + +import com.yhy.module.core.controller.admin.workbench.vo.WbProjectTreeRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbProjectTreeSaveReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbProjectTreeSwapSortReqVO; +import java.util.List; +import javax.validation.Valid; + +/** + * 工作台项目管理树 Service 接口 + * + * @author yhy + */ +public interface WbProjectService { + + /** + * 创建节点(目录或项目) + * + * @param createReqVO 创建信息 + * @return 节点ID + */ + Long createNode(@Valid WbProjectTreeSaveReqVO createReqVO); + + /** + * 更新节点 + * + * @param updateReqVO 更新信息 + */ + void updateNode(@Valid WbProjectTreeSaveReqVO updateReqVO); + + /** + * 删除节点 + * + * @param id 节点ID + */ + void deleteNode(Long id); + + /** + * 获取节点详情 + * + * @param id 节点ID + * @return 节点详情 + */ + WbProjectTreeRespVO getNode(Long id); + + /** + * 获取项目管理树(树形结构) + * + * @return 树形结构 + */ + List getTree(); + + /** + * 交换排序 + * + * @param swapReqVO 交换信息 + */ + void swapSort(@Valid WbProjectTreeSwapSortReqVO swapReqVO); + + /** + * 归档项目 + * + * @param id 项目节点ID + */ + void archiveProject(Long id); + + /** + * 将项目保存至历史库 + * + * @param projectId 项目节点ID + */ + void saveToHistoryLibrary(Long projectId); + + /** + * 从历史库撤销项目 + * + * @param projectId 项目节点ID + */ + void removeFromHistoryLibrary(Long projectId); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbResourceSummaryService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbResourceSummaryService.java new file mode 100644 index 0000000..40fe792 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbResourceSummaryService.java @@ -0,0 +1,61 @@ +package com.yhy.module.core.service.workbench; + +import com.yhy.module.core.controller.admin.workbench.vo.WbResourceSourceBoqRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbResourceSourceUnitRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbResourceSummaryRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbResourceSummaryTreeRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbResourceSummaryUpdateReqVO; +import java.util.List; +import javax.validation.Valid; + +/** + * 工料机汇总 Service 接口 + * + * @author yhy + */ +public interface WbResourceSummaryService { + + /** + * 获取工料机汇总树 + * + * @param projectId 项目ID + * @param compileTreeId 编制模式树的单位工程节点ID(可选,传了只汇总该单位工程) + * @return 树结构 + */ + List getSummaryTree(Long projectId, Long compileTreeId); + + /** + * 获取工料机汇总列表(按类别) + * + * @param projectId 项目ID + * @param category 类别:labor/material/machine/bid_material + * @param compileTreeId 编制模式树的单位工程节点ID(可选,传了只汇总该单位工程) + * @return 汇总列表 + */ + List getSummaryList(Long projectId, String category, Long compileTreeId); + + /** + * 更新工料机汇总(打印、评标指定材料、编码等) + * + * @param updateReqVO 更新信息 + */ + void updateSummary(@Valid WbResourceSummaryUpdateReqVO updateReqVO); + + /** + * 获取工料机来源-单位工程列表 + * + * @param projectId 项目ID + * @param resourceKey 资源唯一键 + * @return 单位工程列表 + */ + List getSourceUnits(Long projectId, String resourceKey); + + /** + * 获取工料机来源-清单列表 + * + * @param compileTreeId 编制模式树ID(单位工程节点) + * @param resourceKey 资源唯一键 + * @return 清单列表 + */ + List getSourceBoqs(Long compileTreeId, String resourceKey); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbSnapshotReadService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbSnapshotReadService.java new file mode 100644 index 0000000..a1ee260 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbSnapshotReadService.java @@ -0,0 +1,109 @@ +package com.yhy.module.core.service.workbench; + +import com.yhy.module.core.dal.dataobject.workbench.*; + +import java.util.List; + +/** + * 工作台快照读取服务 + * + * 提供从快照表读取数据的方法,只从快照表读取,不回退到后台标准库 + * + * @author yihuiyong + */ +public interface WbSnapshotReadService { + + /** + * 获取费率模板列表(只从快照表读取) + * + * @param compileTreeId 单位工程ID + * @param rateModeId 费率模式ID(保留参数,不再用于回退) + * @return 费率模板列表,快照不存在返回空列表 + */ + List getRateItems(Long compileTreeId, Long rateModeId); + + /** + * 获取取费模板列表(只从快照表读取) + * + * @param compileTreeId 单位工程ID + * @param rateModeId 费率模式ID(保留参数,不再用于回退) + * @return 取费模板列表,快照不存在返回空列表 + */ + List getFeeItems(Long compileTreeId, Long rateModeId); + + /** + * 获取统一取费设置列表(只从快照表读取) + * + * @param compileTreeId 单位工程ID + * @param rateModeId 费率模式ID(保留参数,不再用于回退) + * @return 统一取费设置列表,快照不存在返回空列表 + */ + List getUnifiedFeeSettings(Long compileTreeId, Long rateModeId); + + /** + * 获取统一取费子目工料机列表 + * + * @param compileTreeId 单位工程ID + * @param unifiedFeeSettingId 统一取费设置ID(快照表中的ID) + * @return 子目工料机列表 + */ + List getUnifiedFeeResources(Long compileTreeId, Long unifiedFeeSettingId); + + /** + * 获取机类树列表(只从快照表读取) + * + * @param compileTreeId 单位工程ID + * @param quotaCatalogItemId 定额专业ID(保留参数,不再用于回退) + * @return 机类树列表,快照不存在返回空列表 + */ + List getCategoryTree(Long compileTreeId, Long quotaCatalogItemId); + + /** + * 获取机类映射列表 + * + * @param compileTreeId 单位工程ID + * @param categoryTreeId 机类树节点ID(快照表中的ID) + * @return 机类映射列表 + */ + List getCategoryTreeMappings(Long compileTreeId, Long categoryTreeId); + + /** + * 检查单位工程是否有快照数据 + * + * @param compileTreeId 单位工程ID + * @return 是否有快照 + */ + boolean hasSnapshot(Long compileTreeId); + + /** + * 根据快照费率项ID获取费率项 + * + * @param compileTreeId 单位工程ID + * @param snapshotRateItemId 快照表中的费率项ID + * @return 费率项 + */ + WbRateItemDO getRateItemById(Long compileTreeId, Long snapshotRateItemId); + + /** + * 获取取费项树形结构(含费率项信息) + * + * 只从快照表读取,快照不存在返回null + * 返回与 QuotaFeeItemWithRateRespVO 兼容的树形结构 + * + * @param compileTreeId 单位工程ID + * @param rateModeId 费率模式ID + * @return 取费项树形列表,快照不存在返回null + */ + java.util.List getFeeItemWithRateTree(Long compileTreeId, Long rateModeId); + + /** + * 获取费率字段标签列表 + * + * 从快照表读取,如果快照不存在则返回null + * + * @param compileTreeId 单位工程ID + * @param rateModeId 费率模式ID + * @return 字段标签列表,如果快照不存在则返回null + */ + java.util.List getRateFieldLabels(Long compileTreeId, Long rateModeId); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbSnapshotService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbSnapshotService.java new file mode 100644 index 0000000..187329e --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbSnapshotService.java @@ -0,0 +1,47 @@ +package com.yhy.module.core.service.workbench; + +/** + * 工作台快照服务 + * + * 负责在创建单位工程时,从后台标准库复制数据到工作台快照表 + * + * @author yihuiyong + */ +public interface WbSnapshotService { + + /** + * 创建单位工程快照 + * + * 复制以下数据: + * 1. 机类树(yhy_resource_category_tree → yhy_wb_category_tree) + * 2. 机类映射(yhy_resource_category_tree_mapping → yhy_wb_category_tree_mapping) + * 3. 费率模板(yhy_quota_rate_item → yhy_wb_rate_item) + * 4. 取费模板(yhy_quota_fee_item → yhy_wb_fee_item) + * 5. 统一取费设置(yhy_quota_unified_fee_setting → yhy_wb_unified_fee_setting) + * 6. 统一取费子目工料机(yhy_quota_unified_fee_resource → yhy_wb_unified_fee_resource) + * + * @param compileTreeId 单位工程ID(yhy_wb_compile_tree.id) + * @param quotaCatalogItemId 定额专业ID(用于获取机类树) + * @param rateModeId 费率模式ID(用于获取费率/取费/统一取费模板) + */ + void createSnapshot(Long compileTreeId, Long quotaCatalogItemId, Long rateModeId); + + /** + * 删除单位工程快照 + * + * 删除所有关联的快照数据 + * + * @param compileTreeId 单位工程ID + */ + void deleteSnapshot(Long compileTreeId); + + /** + * 切换费率模式时更新快照 + * + * 删除旧的费率相关快照,复制新费率模式的数据 + * + * @param compileTreeId 单位工程ID + * @param newRateModeId 新费率模式ID + */ + void switchRateModeSnapshot(Long compileTreeId, Long newRateModeId); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbUnifiedFeeConfigService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbUnifiedFeeConfigService.java new file mode 100644 index 0000000..f3558e1 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbUnifiedFeeConfigService.java @@ -0,0 +1,67 @@ +package com.yhy.module.core.service.workbench; + +import com.yhy.module.core.controller.admin.workbench.vo.WbUnifiedFeeSummaryItemVO; +import com.yhy.module.core.dal.dataobject.workbench.WbUnifiedFeeConfigDO; +import java.util.List; + +/** + * 工作台统一取费配置 Service 接口 + */ +public interface WbUnifiedFeeConfigService { + + /** + * 根据单位工程节点ID获取配置列表 + */ + List getListByCompileTreeId(Long compileTreeId); + + /** + * 根据单位工程节点ID和费率模式ID获取配置列表 + */ + List getListByCompileTreeIdAndRateModeId(Long compileTreeId, Long rateModeId); + + /** + * 根据来源统一取费设置ID获取配置 + */ + WbUnifiedFeeConfigDO getBySourceUnifiedFeeSettingId(Long compileTreeId, Long sourceUnifiedFeeSettingId); + + /** + * 保存或更新配置 + */ + Long saveOrUpdate(WbUnifiedFeeConfigDO config); + + /** + * 批量保存配置 + */ + void batchSave(Long compileTreeId, Long rateModeId, List configs); + + /** + * 删除配置 + */ + void delete(Long id); + + /** + * 根据费率模式ID删除配置(费率切换时清空) + */ + void deleteByRateModeId(Long compileTreeId, Long rateModeId); + + /** + * 应用统一取费 - 将统一取费目录按范围添加到对应清单 + * @param compileTreeId 单位工程节点ID + */ + void applyUnifiedFee(Long compileTreeId); + + /** + * 获取统一取费汇总来源 - 返回范围内的定额列表 + * @param compileTreeId 单位工程节点ID + * @param sourceUnifiedFeeSettingId 来源统一取费设置ID + * @return 范围内的定额列表 + */ + List getSummarySource(Long compileTreeId, Long sourceUnifiedFeeSettingId); + + /** + * 根据统一取费节点ID获取汇总来源 - 返回范围内的定额列表 + * @param divisionId 统一取费节点ID + * @return 范围内的定额列表 + */ + List getSummarySourceByDivisionId(Long divisionId); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbUnitFeeSettingService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbUnitFeeSettingService.java new file mode 100644 index 0000000..d8c4fd4 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbUnitFeeSettingService.java @@ -0,0 +1,117 @@ +package com.yhy.module.core.service.workbench; + +import com.yhy.module.core.controller.admin.quota.vo.QuotaFeeItemWithRateRespVO; +import java.util.List; + +/** + * 单位工程取费项设定 Service 接口 + */ +public interface WbUnitFeeSettingService { + + /** + * 获取单位工程的取费项列表(合并标准库数据与覆写值) + * + * @param unitId 单位工程ID + * @return 取费项列表(已合并覆写值) + */ + List getMergedFeeList(Long unitId); + + /** + * 根据分部分项节点ID获取取费项列表 + * + * @param divisionId 分部分项节点ID(定额节点) + * @return 取费项列表(已合并覆写值) + */ + List getMergedFeeListByDivisionId(Long divisionId); + + /** + * 根据分部分项节点ID获取取费项列表(使用预计算的分类汇总,避免重复查询工料机) + * + * @param divisionId 分部分项节点ID(定额节点) + * @param categorySums 预计算的分类汇总(来自调用方已加载的工料机数据) + * @return 取费项列表(已合并覆写值,含子单价计算) + */ + List getMergedFeeListByDivisionId(Long divisionId, + java.util.Map categorySums); + + /** + * 保存取费项覆写值 + * + * @param divisionId 分部分项节点ID(定额节点) + * @param feeItemId 取费项ID + * @param name 名称覆写值 + * @param ratePercentage 费率代号覆写值 + * @param code 代号覆写值 + * @param feeCategory 费用归属覆写值 + * @param baseDescription 基数说明覆写值 + */ + void saveOverride(Long divisionId, Long feeItemId, String name, String ratePercentage, + String code, String feeCategory, String baseDescription); + + /** + * 批量保存取费项覆写值 + * + * @param divisionId 分部分项节点ID(定额节点) + * @param overrides 覆写值列表 + */ + void batchSaveOverrides(Long divisionId, List overrides); + + /** + * 删除取费项(标记为用户删除) + * + * @param divisionId 分部分项节点ID(定额节点) + * @param feeItemId 取费项ID + */ + void markAsDeleted(Long divisionId, Long feeItemId); + + /** + * 初始化定额节点的取费项设定(创建快照) + * + * @param divisionId 分部分项节点ID(定额节点) + */ + void initializeSettings(Long divisionId); + + /** + * 取费项覆写 DTO + */ + class FeeOverrideDTO { + private Long feeItemId; + private Long rateItemId; + private Integer sortOrder; + private String name; + private String ratePercentage; + private String code; + private String feeCategory; + private String baseDescription; + private Boolean hidden; + private Boolean variable; + private Boolean userDeleted; + private java.util.Map calcBase; + + // Getters and Setters + public Long getFeeItemId() { return feeItemId; } + public void setFeeItemId(Long feeItemId) { this.feeItemId = feeItemId; } + public Long getRateItemId() { return rateItemId; } + public void setRateItemId(Long rateItemId) { this.rateItemId = rateItemId; } + public Integer getSortOrder() { return sortOrder; } + public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getRatePercentage() { return ratePercentage; } + public void setRatePercentage(String ratePercentage) { this.ratePercentage = ratePercentage; } + public String getCode() { return code; } + public void setCode(String code) { this.code = code; } + public String getFeeCategory() { return feeCategory; } + public void setFeeCategory(String feeCategory) { this.feeCategory = feeCategory; } + public String getBaseDescription() { return baseDescription; } + public void setBaseDescription(String baseDescription) { this.baseDescription = baseDescription; } + public Boolean getHidden() { return hidden; } + public void setHidden(Boolean hidden) { this.hidden = hidden; } + public Boolean getVariable() { return variable; } + public void setVariable(Boolean variable) { this.variable = variable; } + public Boolean getUserDeleted() { return userDeleted; } + public void setUserDeleted(Boolean userDeleted) { this.userDeleted = userDeleted; } + public java.util.Map getCalcBase() { return calcBase; } + public void setCalcBase(java.util.Map calcBase) { this.calcBase = calcBase; } + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbUnitInfoService.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbUnitInfoService.java new file mode 100644 index 0000000..3950d04 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/WbUnitInfoService.java @@ -0,0 +1,103 @@ +package com.yhy.module.core.service.workbench; + +import com.yhy.module.core.controller.admin.workbench.vo.WbUnitInfoRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbUnitInfoSaveReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbUsedQuotaSpecialtyRespVO; +import java.util.List; +import javax.validation.Valid; + +/** + * 单位工程信息 Service 接口 + * + * @author yhy + */ +public interface WbUnitInfoService { + + /** + * 获取单位工程信息 + * + * @param compileTreeId 编制树节点ID(单位工程节点) + * @return 单位工程信息 + */ + WbUnitInfoRespVO getByCompileTreeId(Long compileTreeId); + + /** + * 保存单位工程信息 + * + * @param saveReqVO 保存信息 + * @return ID + */ + Long save(@Valid WbUnitInfoSaveReqVO saveReqVO); + + /** + * 删除单位工程信息(根据编制树节点ID) + * + * @param compileTreeId 编制树节点ID + */ + void deleteByCompileTreeId(Long compileTreeId); + + /** + * 校验工程编号在项目范围内唯一 + * + * @param projectId 项目ID + * @param unitCode 工程编号 + * @param selfId 自身ID(更新时排除) + */ + void validateUnitCodeUnique(Long projectId, String unitCode, Long selfId); + + // ==================== 单位工程级费率及取费相关方法 ==================== + + /** + * 获取单位工程的费率模式绑定列表 + * + * @param compileTreeId 编制树节点ID(单位工程节点) + * @return 费率模式绑定列表 + */ + List> getUnitRateModeBindings(Long compileTreeId); + + /** + * 切换单位工程的费率模式 + * + * @param compileTreeId 编制树节点ID(单位工程节点) + * @param quotaCatalogItemId 定额专业ID + * @param rateModeId 费率模式ID + */ + void switchUnitRateMode(Long compileTreeId, Long quotaCatalogItemId, Long rateModeId); + + /** + * 获取单位工程的费率配置 + * + * @param compileTreeId 编制树节点ID(单位工程节点) + * @param quotaCatalogItemId 定额专业ID + * @return 费率配置 + */ + java.util.Map getUnitRateConfig(Long compileTreeId, Long quotaCatalogItemId); + + /** + * 保存单位工程的费率配置 + * + * @param compileTreeId 编制树节点ID(单位工程节点) + * @param quotaCatalogItemId 定额专业ID + * @param rateModeId 费率模式ID + * @param rateSettings 费率覆写值 + * @param feeSettings 取费覆写值 + */ + void saveUnitRateConfig(Long compileTreeId, Long quotaCatalogItemId, Long rateModeId, + java.util.Map rateSettings, java.util.Map feeSettings); + + /** + * 获取单位工程使用的定额专业列表 + * + * @param compileTreeId 编制树节点ID(单位工程节点) + * @return 定额专业列表 + */ + List getUsedQuotaSpecialtiesByUnit(Long compileTreeId); + + /** + * 获取项目下所有使用的定额专业列表(合并所有单位工程) + * + * @param projectId 项目ID + * @return 定额专业列表(去重) + */ + List getUsedQuotaSpecialtiesByProject(Long projectId); +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/AuditModeServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/AuditModeServiceImpl.java new file mode 100644 index 0000000..31d908b --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/AuditModeServiceImpl.java @@ -0,0 +1,962 @@ +package com.yhy.module.core.service.workbench.impl; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yhy.module.core.enums.ErrorCodeConstants.AUDIT_APPROVE_DIVISION_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.AUDIT_MODE_ALREADY_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.AUDIT_MODE_NOT_EXISTS; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqDivisionRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.audit.AuditApproveDivisionRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.audit.AuditApproveDivisionUpdateReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.audit.AuditDiffDivisionRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.audit.AuditDivisionTreeRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.audit.AuditModeCreateReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.audit.AuditModeRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.audit.AuditSubmitDivisionRespVO; +import com.yhy.module.core.dal.dataobject.workbench.AuditApproveDivisionDO; +import com.yhy.module.core.dal.dataobject.workbench.AuditApproveResourceDO; +import com.yhy.module.core.dal.dataobject.workbench.AuditModeDO; +import com.yhy.module.core.dal.dataobject.workbench.WbBoqDivisionDO; +import com.yhy.module.core.dal.dataobject.workbench.WbBoqResourceDO; +import com.yhy.module.core.dal.dataobject.workbench.WbCompileTreeDO; +import com.yhy.module.core.dal.mysql.workbench.AuditApproveDivisionMapper; +import com.yhy.module.core.dal.mysql.workbench.AuditApproveResourceMapper; +import com.yhy.module.core.dal.mysql.workbench.AuditModeMapper; +import com.yhy.module.core.dal.mysql.workbench.WbBoqDivisionMapper; +import com.yhy.module.core.dal.mysql.workbench.WbBoqResourceMapper; +import com.yhy.module.core.dal.mysql.workbench.WbCompileTreeMapper; +import com.yhy.module.core.service.workbench.AuditModeService; +import com.yhy.module.core.service.workbench.WbBoqDivisionService; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * 审核模式 Service 实现类 + * + * @author yhy + */ +@Service +@Validated +@Slf4j +public class AuditModeServiceImpl implements AuditModeService { + + @Resource + private AuditModeMapper auditModeMapper; + + + @Resource + private AuditApproveDivisionMapper auditApproveDivisionMapper; + + @Resource + private AuditApproveResourceMapper auditApproveResourceMapper; + + @Resource + private WbCompileTreeMapper wbCompileTreeMapper; + + @Resource + private WbBoqResourceMapper wbBoqResourceMapper; + + @Resource + private WbBoqDivisionMapper wbBoqDivisionMapper; + + @Resource + @Lazy + private WbBoqDivisionService wbBoqDivisionService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createAuditMode(AuditModeCreateReqVO createReqVO) { + // 0. 校验:每个项目只允许创建一次审核模式 + List existingModes = auditModeMapper.selectListByProjectId(createReqVO.getProjectId()); + if (CollUtil.isNotEmpty(existingModes)) { + throw exception(AUDIT_MODE_ALREADY_EXISTS); + } + + // 1. 创建审核模式主记录 + AuditModeDO auditMode = new AuditModeDO(); + auditMode.setProjectId(createReqVO.getProjectId()); + auditMode.setName(createReqVO.getName()); + auditMode.setStatus(AuditModeDO.STATUS_DRAFT); + auditMode.setSubmitTime(LocalDateTime.now()); + auditModeMapper.insert(auditMode); + + // 2. 获取项目下所有单位工程节点 + List unitNodes = wbCompileTreeMapper.selectUnitNodesByProjectId(createReqVO.getProjectId()); + + // 3. 为每个单位工程复制分部分项 → 审定快照表 + Map divisionIdMapping = new HashMap<>(); + for (WbCompileTreeDO unitNode : unitNodes) { + copyApproveDivisionsForUnit(auditMode.getId(), unitNode.getId(), divisionIdMapping); + } + + // 4. 复制工料机 → 审定工料机快照表 + copyApproveResources(divisionIdMapping); + + return auditMode.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteAuditMode(Long id) { + // 1. 校验存在 + AuditModeDO auditMode = auditModeMapper.selectById(id); + if (auditMode == null) { + throw exception(AUDIT_MODE_NOT_EXISTS); + } + + // 2. 获取所有审定分部分项 + List divisions = auditApproveDivisionMapper.selectListByAuditModeId(id); + List divisionIds = divisions.stream() + .map(AuditApproveDivisionDO::getId) + .collect(Collectors.toList()); + + // 3. 删除审定工料机 + if (CollUtil.isNotEmpty(divisionIds)) { + auditApproveResourceMapper.deleteByAuditApproveDivisionIds(divisionIds); + } + + // 4. 删除审定分部分项 + auditApproveDivisionMapper.deleteByAuditModeId(id); + + // 5. 删除审核模式主记录 + auditModeMapper.deleteById(id); + } + + @Override + public AuditModeRespVO getAuditMode(Long id) { + AuditModeDO auditMode = auditModeMapper.selectById(id); + if (auditMode == null) { + return null; + } + return BeanUtil.copyProperties(auditMode, AuditModeRespVO.class); + } + + @Override + public List getAuditModeList(Long projectId) { + List list = auditModeMapper.selectListByProjectId(projectId); + return BeanUtil.copyToList(list, AuditModeRespVO.class); + } + + /** + * 获取送审数据(联表查询编制模式实时数据) + */ + private List getSubmitTree(Long auditModeId, Long compileTreeId) { + // 直接联表查询编制模式实时数据 + List compileDivisions = wbBoqDivisionService.getTreeWithPrice(compileTreeId); + + // 转换为送审VO + return convertToSubmitTree(compileDivisions); + } + + /** + * 获取审定数据(快照) + */ + private List getApproveTree(Long auditModeId, Long compileTreeId) { + // 直接查询审定快照表 + List list = auditApproveDivisionMapper + .selectListByAuditModeIdAndCompileTreeId(auditModeId, compileTreeId); + if (CollUtil.isEmpty(list)) { + return new ArrayList<>(); + } + + // 转换为VO + List voList = BeanUtil.copyToList(list, AuditApproveDivisionRespVO.class); + + // 构建树形结构 + return buildApproveTree(voList); + } + + /** + * 获取差异数据(送审 - 审定) + */ + private List getDiffTree(Long auditModeId, Long compileTreeId) { + // 1. 获取送审数据(编制模式实时数据) + List submitList = getSubmitTree(auditModeId, compileTreeId); + + // 2. 获取审定数据(快照) + List approveList = getApproveTree(auditModeId, compileTreeId); + + // 3. 扁平化审定数据,按 sourceDivisionId 索引 + List flatApproveList = flattenApproveTree(approveList); + Map approveMap = flatApproveList.stream() + .collect(Collectors.toMap(AuditApproveDivisionRespVO::getSourceDivisionId, a -> a, (a, b) -> a)); + + // 4. 计算差异 + Set matchedIds = new HashSet<>(); + List diffList = calculateDiff(submitList, approveMap, matchedIds); + + // 5. 处理删除的节点(审定有,编制模式删除了) + for (AuditApproveDivisionRespVO approve : flatApproveList) { + if (!matchedIds.contains(approve.getSourceDivisionId())) { + AuditDiffDivisionRespVO diff = new AuditDiffDivisionRespVO(); + diff.setSourceDivisionId(approve.getSourceDivisionId()); + diff.setParentId(approve.getParentId()); + diff.setNodeType(approve.getNodeType()); + diff.setCode(approve.getCode()); + diff.setName(approve.getName()); + diff.setUnit(approve.getUnit()); + diff.setDiffQty(negate(approve.getApproveQty())); + diff.setDiffUnitPrice(negate(approve.getApproveUnitPrice())); + diff.setDiffRate(negate(approve.getApproveRate())); + diff.setDiffTotalPrice(negate(approve.getApproveTotalPrice())); + diff.setChangeType(AuditDiffDivisionRespVO.CHANGE_TYPE_DELETE); + diffList.add(diff); + } + } + + return diffList; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateApproveData(AuditApproveDivisionUpdateReqVO updateReqVO) { + // 1. 校验存在 + AuditApproveDivisionDO approve = auditApproveDivisionMapper.selectById(updateReqVO.getId()); + if (approve == null) { + throw exception(AUDIT_APPROVE_DIVISION_NOT_EXISTS); + } + + // 2. 获取原编制模式数据,用于比较 + WbBoqDivisionDO sourceDivision = null; + if (approve.getSourceDivisionId() != null) { + sourceDivision = wbBoqDivisionMapper.selectById(approve.getSourceDivisionId()); + } + + // 3. 更新基础信息(编码、名称、单位、项目特征) + // 只有与原值不同时才存储,相同则设为null(引用原值) + if (updateReqVO.getCode() != null) { + String sourceCode = sourceDivision != null ? sourceDivision.getCode() : null; + approve.setCode(StrUtil.equals(updateReqVO.getCode(), sourceCode) ? null : updateReqVO.getCode()); + } + if (updateReqVO.getName() != null) { + String sourceName = sourceDivision != null ? sourceDivision.getName() : null; + approve.setName(StrUtil.equals(updateReqVO.getName(), sourceName) ? null : updateReqVO.getName()); + } + if (updateReqVO.getUnit() != null) { + String sourceUnit = sourceDivision != null ? sourceDivision.getUnit() : null; + approve.setUnit(StrUtil.equals(updateReqVO.getUnit(), sourceUnit) ? null : updateReqVO.getUnit()); + } + if (updateReqVO.getFeature() != null) { + String sourceFeature = sourceDivision != null ? sourceDivision.getFeature() : null; + approve.setFeature(StrUtil.equals(updateReqVO.getFeature(), sourceFeature) ? null : updateReqVO.getFeature()); + } + + // 4. 更新审定值 + if (updateReqVO.getApproveQty() != null) { + approve.setApproveQty(updateReqVO.getApproveQty()); + } + if (updateReqVO.getApproveUnitPrice() != null) { + approve.setApproveUnitPrice(updateReqVO.getApproveUnitPrice()); + } + if (updateReqVO.getApproveRate() != null) { + approve.setApproveRate(updateReqVO.getApproveRate()); + } + if (updateReqVO.getApproveTotalPrice() != null) { + approve.setApproveTotalPrice(updateReqVO.getApproveTotalPrice()); + } + + auditApproveDivisionMapper.updateById(approve); + } + + // ==================== 私有方法 ==================== + + /** + * 为单位工程复制分部分项到审定快照表 + */ + private void copyApproveDivisionsForUnit(Long auditModeId, Long compileTreeId, Map divisionIdMapping) { + // 获取编制模式的分部分项数据(含计算后的单价/合价) + List divisions = wbBoqDivisionService.getTreeWithPrice(compileTreeId); + + // 扁平化树结构 + List flatDivisions = flattenDivisionTree(divisions); + + // 第一遍:创建所有节点,记录ID映射 + for (WbBoqDivisionRespVO div : flatDivisions) { + AuditApproveDivisionDO auditDiv = new AuditApproveDivisionDO(); + auditDiv.setAuditModeId(auditModeId); + auditDiv.setCompileTreeId(compileTreeId); + auditDiv.setSourceDivisionId(div.getId()); + auditDiv.setNodeType(div.getNodeType()); + auditDiv.setSourceType(div.getSourceType()); + // code/name/unit/feature 默认为null,只有编辑且与原值不同时才存储 + // auditDiv.setCode(null); + // auditDiv.setName(null); + // auditDiv.setUnit(null); + // auditDiv.setFeature(null); + auditDiv.setSortOrder(div.getSortOrder()); + auditDiv.setPath(div.getPath()); + + // 审定值 = 当前编制值 + auditDiv.setApproveQty(div.getQty()); + auditDiv.setApproveUnitPrice(div.getUnitPrice()); + auditDiv.setApproveRate(div.getRate()); + auditDiv.setApproveTotalPrice(div.getAmount()); + + auditDiv.setAttributes(div.getAttributes()); + + auditApproveDivisionMapper.insert(auditDiv); + divisionIdMapping.put(div.getId(), auditDiv.getId()); + } + + // 第二遍:更新父节点ID + for (WbBoqDivisionRespVO div : flatDivisions) { + if (div.getParentId() != null) { + Long newId = divisionIdMapping.get(div.getId()); + Long newParentId = divisionIdMapping.get(div.getParentId()); + if (newId != null && newParentId != null) { + AuditApproveDivisionDO auditDiv = auditApproveDivisionMapper.selectById(newId); + auditDiv.setParentId(newParentId); + auditApproveDivisionMapper.updateById(auditDiv); + } + } + } + } + + /** + * 复制工料机到审定工料机快照表 + */ + private void copyApproveResources(Map divisionIdMapping) { + Map resourceIdMapping = new HashMap<>(); + + for (Map.Entry entry : divisionIdMapping.entrySet()) { + Long sourceDivisionId = entry.getKey(); + Long auditDivisionId = entry.getValue(); + + // 获取工料机列表 + List resources = wbBoqResourceMapper.selectListByDivisionId(sourceDivisionId); + if (CollUtil.isEmpty(resources)) { + continue; + } + + // 第一遍:创建所有工料机,记录ID映射 + for (WbBoqResourceDO res : resources) { + AuditApproveResourceDO auditRes = new AuditApproveResourceDO(); + auditRes.setAuditApproveDivisionId(auditDivisionId); + auditRes.setSourceResourceId(res.getId()); + auditRes.setResourceType(res.getResourceType()); + auditRes.setCode(res.getCode()); + auditRes.setName(res.getName()); + auditRes.setSpec(res.getSpec()); + auditRes.setUnit(res.getUnit()); + auditRes.setCategoryId(res.getCategoryId()); + auditRes.setSortOrder(res.getSortOrder()); + + // 审定值 = 当前编制值 + auditRes.setApproveConsumeQty(res.getConsumeQty()); + auditRes.setApprovePrice(res.getTaxExclCompilePrice()); + + auditRes.setAttributes(res.getAttributes()); + + auditApproveResourceMapper.insert(auditRes); + resourceIdMapping.put(res.getId(), auditRes.getId()); + } + + // 第二遍:更新父工料机ID + for (WbBoqResourceDO res : resources) { + if (res.getParentId() != null) { + Long newId = resourceIdMapping.get(res.getId()); + Long newParentId = resourceIdMapping.get(res.getParentId()); + if (newId != null && newParentId != null) { + AuditApproveResourceDO auditRes = auditApproveResourceMapper.selectById(newId); + auditRes.setParentId(newParentId); + auditApproveResourceMapper.updateById(auditRes); + } + } + } + } + } + + /** + * 扁平化分部分项树 + */ + private List flattenDivisionTree(List tree) { + List result = new ArrayList<>(); + for (WbBoqDivisionRespVO node : tree) { + result.add(node); + if (CollUtil.isNotEmpty(node.getChildren())) { + result.addAll(flattenDivisionTree(node.getChildren())); + } + } + return result; + } + + /** + * 扁平化审定树 + */ + private List flattenApproveTree(List tree) { + List result = new ArrayList<>(); + for (AuditApproveDivisionRespVO node : tree) { + result.add(node); + if (CollUtil.isNotEmpty(node.getChildren())) { + result.addAll(flattenApproveTree(node.getChildren())); + } + } + return result; + } + + /** + * 构建审定树形结构 + */ + private List buildApproveTree(List list) { + Map map = list.stream() + .collect(Collectors.toMap(AuditApproveDivisionRespVO::getId, v -> v)); + + List roots = new ArrayList<>(); + for (AuditApproveDivisionRespVO vo : list) { + if (vo.getParentId() == null) { + roots.add(vo); + } else { + AuditApproveDivisionRespVO parent = map.get(vo.getParentId()); + if (parent != null) { + if (parent.getChildren() == null) { + parent.setChildren(new ArrayList<>()); + } + parent.getChildren().add(vo); + } + } + } + return roots; + } + + /** + * 转换为送审树 + */ + private List convertToSubmitTree(List compileDivisions) { + List result = new ArrayList<>(); + for (WbBoqDivisionRespVO compile : compileDivisions) { + AuditSubmitDivisionRespVO submit = new AuditSubmitDivisionRespVO(); + submit.setSourceDivisionId(compile.getId()); + submit.setParentId(compile.getParentId()); + submit.setNodeType(compile.getNodeType()); + submit.setSourceType(compile.getSourceType()); + submit.setCode(compile.getCode()); + submit.setName(compile.getName()); + submit.setUnit(compile.getUnit()); + submit.setFeature(compile.getFeature()); + submit.setSortOrder(compile.getSortOrder()); + submit.setPath(compile.getPath()); + submit.setSubmitQty(compile.getQty()); + submit.setSubmitUnitPrice(compile.getUnitPrice()); + submit.setSubmitRate(compile.getRate()); + submit.setSubmitTotalPrice(compile.getAmount()); + submit.setAttributes(compile.getAttributes()); + + if (CollUtil.isNotEmpty(compile.getChildren())) { + submit.setChildren(convertToSubmitTree(compile.getChildren())); + } + + result.add(submit); + } + return result; + } + + /** + * 计算差异 + */ + private List calculateDiff( + List submitList, + Map approveMap, + Set matchedIds) { + + List result = new ArrayList<>(); + + for (AuditSubmitDivisionRespVO submit : submitList) { + AuditDiffDivisionRespVO diff = new AuditDiffDivisionRespVO(); + diff.setSourceDivisionId(submit.getSourceDivisionId()); + diff.setParentId(submit.getParentId()); + diff.setNodeType(submit.getNodeType()); + diff.setSourceType(submit.getSourceType()); + diff.setCode(submit.getCode()); + diff.setName(submit.getName()); + diff.setUnit(submit.getUnit()); + diff.setSortOrder(submit.getSortOrder()); + diff.setPath(submit.getPath()); + diff.setAttributes(submit.getAttributes()); + + AuditApproveDivisionRespVO approve = approveMap.get(submit.getSourceDivisionId()); + if (approve != null) { + matchedIds.add(submit.getSourceDivisionId()); + // 差异 = 送审 - 审定 + diff.setDiffQty(subtract(submit.getSubmitQty(), approve.getApproveQty())); + diff.setDiffUnitPrice(subtract(submit.getSubmitUnitPrice(), approve.getApproveUnitPrice())); + diff.setDiffRate(subtract(submit.getSubmitRate(), approve.getApproveRate())); + diff.setDiffTotalPrice(subtract(submit.getSubmitTotalPrice(), approve.getApproveTotalPrice())); + diff.setChangeType(isAllZero(diff) ? AuditDiffDivisionRespVO.CHANGE_TYPE_NONE + : AuditDiffDivisionRespVO.CHANGE_TYPE_MODIFY); + } else { + // 新增节点(编制模式新增,审定快照没有) + diff.setDiffQty(submit.getSubmitQty()); + diff.setDiffUnitPrice(submit.getSubmitUnitPrice()); + diff.setDiffRate(submit.getSubmitRate()); + diff.setDiffTotalPrice(submit.getSubmitTotalPrice()); + diff.setChangeType(AuditDiffDivisionRespVO.CHANGE_TYPE_ADD); + } + + // 递归处理子节点 + if (CollUtil.isNotEmpty(submit.getChildren())) { + diff.setChildren(calculateDiff(submit.getChildren(), approveMap, matchedIds)); + } + + result.add(diff); + } + + return result; + } + + /** + * 减法(处理null) + */ + private BigDecimal subtract(BigDecimal a, BigDecimal b) { + if (a == null && b == null) { + return BigDecimal.ZERO; + } + if (a == null) { + return b.negate(); + } + if (b == null) { + return a; + } + return a.subtract(b); + } + + /** + * 取反(处理null) + */ + private BigDecimal negate(BigDecimal value) { + return value == null ? null : value.negate(); + } + + /** + * 判断差异是否全为0 + */ + private boolean isAllZero(AuditDiffDivisionRespVO diff) { + return isZero(diff.getDiffQty()) + && isZero(diff.getDiffUnitPrice()) + && isZero(diff.getDiffRate()) + && isZero(diff.getDiffTotalPrice()); + } + + /** + * 判断是否为0 + */ + private boolean isZero(BigDecimal value) { + return value == null || value.compareTo(BigDecimal.ZERO) == 0; + } + + @Override + public List getDivisionTree(Long auditModeId, Long compileTreeId) { + // 1. 获取送审数据(编制模式实时) + List submitList = getSubmitTree(auditModeId, compileTreeId); + + // 2. 获取审定数据(快照) + List approveList = getApproveTree(auditModeId, compileTreeId); + + // 3. 扁平化数据,建立索引 + Map submitMap = flattenSubmitTree(submitList).stream() + .collect(Collectors.toMap(AuditSubmitDivisionRespVO::getSourceDivisionId, v -> v, (a, b) -> a)); + Map approveMap = flattenApproveTree(approveList).stream() + .collect(Collectors.toMap(AuditApproveDivisionRespVO::getSourceDivisionId, v -> v, (a, b) -> a)); + + // 4. 以审定数据为基准合并(因为审定有ID,需要用于编辑) + Set processedSourceIds = new HashSet<>(); + List result = mergeDivisionTree(approveList, submitMap, processedSourceIds); + + // 5. 处理新增节点(送审有,审定无) + addNewNodes(result, submitList, approveMap); + + return result; + } + + /** + * 扁平化送审树 + */ + private List flattenSubmitTree(List tree) { + List result = new ArrayList<>(); + if (tree == null) { + return result; + } + for (AuditSubmitDivisionRespVO node : tree) { + result.add(node); + if (CollUtil.isNotEmpty(node.getChildren())) { + result.addAll(flattenSubmitTree(node.getChildren())); + } + } + return result; + } + + /** + * 合并分部分项树(以审定为基准) + * 定额节点拆分为送审行+审核行,差异只保留清单级 + */ + private List mergeDivisionTree( + List approveList, + Map submitMap, + Set processedSourceIds) { + + List result = new ArrayList<>(); + if (approveList == null) { + return result; + } + + for (AuditApproveDivisionRespVO approve : approveList) { + boolean isQuota = AuditApproveDivisionDO.NODE_TYPE_QUOTA.equals(approve.getNodeType()); + AuditSubmitDivisionRespVO submit = submitMap.get(approve.getSourceDivisionId()); + + if (isQuota) { + // ========== 定额节点:拆分为送审行+审核行 ========== + if (submit != null) { + processedSourceIds.add(approve.getSourceDivisionId()); + } + // 送审行和审核行分别收集,后续统一添加到父节点children + // 这里先添加到result,由调用方处理排序 + result.addAll(buildQuotaSplitRows(approve, submit)); + } else { + // ========== 非定额节点:保持合并逻辑 ========== + AuditDivisionTreeRespVO vo = buildMergedNonQuotaRow(approve, submit, processedSourceIds); + + // 递归处理子节点,并对定额子节点做分组排序 + if (CollUtil.isNotEmpty(approve.getChildren())) { + List childResult = mergeDivisionTree(approve.getChildren(), submitMap, processedSourceIds); + // 对清单节点的子节点做分组排序:先送审定额,后审核定额 + if (AuditApproveDivisionDO.NODE_TYPE_BOQ.equals(approve.getNodeType())) { + childResult = sortQuotaChildren(childResult); + } + vo.setChildren(childResult); + } + + result.add(vo); + } + } + + return result; + } + + /** + * 构建定额节点的送审行+审核行(一拆二) + */ + private List buildQuotaSplitRows( + AuditApproveDivisionRespVO approve, + AuditSubmitDivisionRespVO submit) { + + List rows = new ArrayList<>(); + + // 1. 送审行(不可编辑,只填送审列组) + if (submit != null) { + AuditDivisionTreeRespVO submitRow = new AuditDivisionTreeRespVO(); + // 不设id,送审行不可编辑 + submitRow.setSourceDivisionId(approve.getSourceDivisionId()); + submitRow.setParentId(approve.getParentId()); + submitRow.setNodeType(approve.getNodeType()); + submitRow.setSourceType(approve.getSourceType()); + submitRow.setSortOrder(approve.getSortOrder()); + submitRow.setPath(approve.getPath()); + submitRow.setAttributes(approve.getAttributes()); + submitRow.setDataSource(AuditDivisionTreeRespVO.DATA_SOURCE_SUBMIT); + submitRow.setEditable(false); + + // 基础信息取编制模式实时值 + submitRow.setCode(submit.getCode()); + submitRow.setName(submit.getName()); + submitRow.setUnit(submit.getUnit()); + submitRow.setFeature(submit.getFeature()); + submitRow.setSubmitCode(submit.getCode()); + submitRow.setSubmitName(submit.getName()); + submitRow.setSubmitUnit(submit.getUnit()); + submitRow.setSubmitFeature(submit.getFeature()); + submitRow.setCodeEdited(false); + submitRow.setNameEdited(false); + submitRow.setUnitEdited(false); + submitRow.setFeatureEdited(false); + + // 送审数值 + submitRow.setSubmitQty(submit.getSubmitQty()); + submitRow.setSubmitUnitPrice(submit.getSubmitUnitPrice()); + submitRow.setSubmitRate(submit.getSubmitRate()); + submitRow.setSubmitTotalPrice(submit.getSubmitTotalPrice()); + + // 审核列组和差异列组留null(定额不计算差异) + submitRow.setChangeType(AuditDivisionTreeRespVO.CHANGE_TYPE_NONE); + + rows.add(submitRow); + } + + // 2. 审核行(可编辑,只填审核列组) + AuditDivisionTreeRespVO approveRow = new AuditDivisionTreeRespVO(); + approveRow.setId(approve.getId()); // 保留审定表ID,用于编辑保存 + approveRow.setSourceDivisionId(approve.getSourceDivisionId()); + approveRow.setParentId(approve.getParentId()); + approveRow.setNodeType(approve.getNodeType()); + approveRow.setSourceType(approve.getSourceType()); + approveRow.setSortOrder(approve.getSortOrder()); + approveRow.setPath(approve.getPath()); + approveRow.setAttributes(approve.getAttributes()); + approveRow.setUpdateTime(approve.getUpdateTime()); + approveRow.setDataSource(AuditDivisionTreeRespVO.DATA_SOURCE_APPROVE); + approveRow.setEditable(true); + + // 基础信息:编辑过则显示审定值,否则显示编制模式值 + boolean codeEdited = approve.getCode() != null; + boolean nameEdited = approve.getName() != null; + boolean unitEdited = approve.getUnit() != null; + boolean featureEdited = approve.getFeature() != null; + approveRow.setCodeEdited(codeEdited); + approveRow.setNameEdited(nameEdited); + approveRow.setUnitEdited(unitEdited); + approveRow.setFeatureEdited(featureEdited); + + if (submit != null) { + approveRow.setCode(codeEdited ? approve.getCode() : submit.getCode()); + approveRow.setName(nameEdited ? approve.getName() : submit.getName()); + approveRow.setUnit(unitEdited ? approve.getUnit() : submit.getUnit()); + approveRow.setFeature(featureEdited ? approve.getFeature() : submit.getFeature()); + approveRow.setSubmitCode(submit.getCode()); + approveRow.setSubmitName(submit.getName()); + approveRow.setSubmitUnit(submit.getUnit()); + approveRow.setSubmitFeature(submit.getFeature()); + } else { + // 编制模式已删除该定额,显示审定表中存储的值 + approveRow.setCode(approve.getCode()); + approveRow.setName(approve.getName()); + approveRow.setUnit(approve.getUnit()); + approveRow.setFeature(approve.getFeature()); + } + + // 审核数值 + approveRow.setApproveQty(approve.getApproveQty()); + approveRow.setApproveUnitPrice(approve.getApproveUnitPrice()); + approveRow.setApproveRate(approve.getApproveRate()); + approveRow.setApproveTotalPrice(approve.getApproveTotalPrice()); + + // 差异列组留null(定额不计算差异) + if (submit == null) { + approveRow.setChangeType(AuditDivisionTreeRespVO.CHANGE_TYPE_DELETE); + } else { + approveRow.setChangeType(AuditDivisionTreeRespVO.CHANGE_TYPE_NONE); + } + + rows.add(approveRow); + + return rows; + } + + /** + * 构建非定额节点的合并行(保持原逻辑,差异只保留清单级) + */ + private AuditDivisionTreeRespVO buildMergedNonQuotaRow( + AuditApproveDivisionRespVO approve, + AuditSubmitDivisionRespVO submit, + Set processedSourceIds) { + + AuditDivisionTreeRespVO vo = new AuditDivisionTreeRespVO(); + + // 基础字段 + vo.setId(approve.getId()); + vo.setSourceDivisionId(approve.getSourceDivisionId()); + vo.setParentId(approve.getParentId()); + vo.setNodeType(approve.getNodeType()); + vo.setSourceType(approve.getSourceType()); + vo.setSortOrder(approve.getSortOrder()); + vo.setPath(approve.getPath()); + vo.setAttributes(approve.getAttributes()); + vo.setUpdateTime(approve.getUpdateTime()); + + // 审定数据 + vo.setApproveQty(approve.getApproveQty()); + vo.setApproveUnitPrice(approve.getApproveUnitPrice()); + vo.setApproveRate(approve.getApproveRate()); + vo.setApproveTotalPrice(approve.getApproveTotalPrice()); + + if (submit != null) { + processedSourceIds.add(approve.getSourceDivisionId()); + // 送审基础信息 + vo.setSubmitCode(submit.getCode()); + vo.setSubmitName(submit.getName()); + vo.setSubmitUnit(submit.getUnit()); + vo.setSubmitFeature(submit.getFeature()); + + boolean codeEdited = approve.getCode() != null; + boolean nameEdited = approve.getName() != null; + boolean unitEdited = approve.getUnit() != null; + boolean featureEdited = approve.getFeature() != null; + vo.setCodeEdited(codeEdited); + vo.setNameEdited(nameEdited); + vo.setUnitEdited(unitEdited); + vo.setFeatureEdited(featureEdited); + + vo.setCode(codeEdited ? approve.getCode() : submit.getCode()); + vo.setName(nameEdited ? approve.getName() : submit.getName()); + vo.setUnit(unitEdited ? approve.getUnit() : submit.getUnit()); + vo.setFeature(featureEdited ? approve.getFeature() : submit.getFeature()); + + // 送审数值 + vo.setSubmitQty(submit.getSubmitQty()); + vo.setSubmitUnitPrice(submit.getSubmitUnitPrice()); + vo.setSubmitRate(submit.getSubmitRate()); + vo.setSubmitTotalPrice(submit.getSubmitTotalPrice()); + + // 差异只保留清单(boq)、分部(division)、根(root)级别 + String nodeType = approve.getNodeType(); + if (AuditApproveDivisionDO.NODE_TYPE_BOQ.equals(nodeType) + || AuditApproveDivisionDO.NODE_TYPE_DIVISION.equals(nodeType) + || AuditApproveDivisionDO.NODE_TYPE_ROOT.equals(nodeType)) { + vo.setDiffQty(subtract(submit.getSubmitQty(), approve.getApproveQty())); + vo.setDiffUnitPrice(subtract(submit.getSubmitUnitPrice(), approve.getApproveUnitPrice())); + vo.setDiffRate(subtract(submit.getSubmitRate(), approve.getApproveRate())); + vo.setDiffTotalPrice(subtract(submit.getSubmitTotalPrice(), approve.getApproveTotalPrice())); + } + // 其他非定额节点(如unified_fee)不计算差异 + + if (isAllZeroForMerged(vo)) { + vo.setChangeType(AuditDivisionTreeRespVO.CHANGE_TYPE_NONE); + } else { + vo.setChangeType(AuditDivisionTreeRespVO.CHANGE_TYPE_MODIFY); + } + } else { + // 送审无,审定有 → delete + vo.setCode(approve.getCode()); + vo.setName(approve.getName()); + vo.setUnit(approve.getUnit()); + vo.setFeature(approve.getFeature()); + vo.setCodeEdited(false); + vo.setNameEdited(false); + vo.setUnitEdited(false); + vo.setFeatureEdited(false); + + vo.setDiffQty(negate(approve.getApproveQty())); + vo.setDiffUnitPrice(negate(approve.getApproveUnitPrice())); + vo.setDiffRate(negate(approve.getApproveRate())); + vo.setDiffTotalPrice(negate(approve.getApproveTotalPrice())); + vo.setChangeType(AuditDivisionTreeRespVO.CHANGE_TYPE_DELETE); + } + + return vo; + } + + /** + * 对清单子节点做分组排序:先送审定额,后审核定额,非定额节点保持原位 + */ + private List sortQuotaChildren(List children) { + if (children == null || children.isEmpty()) { + return children; + } + // 分三组:非定额节点、送审定额、审核定额 + List nonQuota = new ArrayList<>(); + List submitQuotas = new ArrayList<>(); + List approveQuotas = new ArrayList<>(); + + for (AuditDivisionTreeRespVO child : children) { + if (!AuditApproveDivisionDO.NODE_TYPE_QUOTA.equals(child.getNodeType())) { + nonQuota.add(child); + } else if (AuditDivisionTreeRespVO.DATA_SOURCE_SUBMIT.equals(child.getDataSource())) { + submitQuotas.add(child); + } else { + approveQuotas.add(child); + } + } + + // 合并:非定额节点 + 送审定额 + 审核定额 + List sorted = new ArrayList<>(); + sorted.addAll(nonQuota); + sorted.addAll(submitQuotas); + sorted.addAll(approveQuotas); + return sorted; + } + + /** + * 添加新增节点(送审有,审定无) + * 定额节点只生成送审行,非定额节点保持原逻辑 + */ + private void addNewNodes( + List result, + List submitList, + Map approveMap) { + + if (submitList == null) { + return; + } + + for (AuditSubmitDivisionRespVO submit : submitList) { + if (!approveMap.containsKey(submit.getSourceDivisionId())) { + boolean isQuota = AuditApproveDivisionDO.NODE_TYPE_QUOTA.equals(submit.getNodeType()); + + // 新增节点 + AuditDivisionTreeRespVO vo = new AuditDivisionTreeRespVO(); + vo.setSourceDivisionId(submit.getSourceDivisionId()); + vo.setParentId(submit.getParentId()); + vo.setNodeType(submit.getNodeType()); + vo.setSourceType(submit.getSourceType()); + vo.setCode(submit.getCode()); + vo.setName(submit.getName()); + vo.setUnit(submit.getUnit()); + vo.setFeature(submit.getFeature()); + vo.setSortOrder(submit.getSortOrder()); + vo.setPath(submit.getPath()); + vo.setAttributes(submit.getAttributes()); + + // 送审基础信息 + vo.setSubmitCode(submit.getCode()); + vo.setSubmitName(submit.getName()); + vo.setSubmitUnit(submit.getUnit()); + vo.setSubmitFeature(submit.getFeature()); + + vo.setCodeEdited(false); + vo.setNameEdited(false); + vo.setUnitEdited(false); + vo.setFeatureEdited(false); + + // 送审数值 + vo.setSubmitQty(submit.getSubmitQty()); + vo.setSubmitUnitPrice(submit.getSubmitUnitPrice()); + vo.setSubmitRate(submit.getSubmitRate()); + vo.setSubmitTotalPrice(submit.getSubmitTotalPrice()); + + if (isQuota) { + // 新增定额:只生成送审行,标记为submit + vo.setDataSource(AuditDivisionTreeRespVO.DATA_SOURCE_SUBMIT); + vo.setEditable(false); + // 定额不计算差异 + } else { + // 非定额新增节点:差异 = 送审值 + vo.setDiffQty(submit.getSubmitQty()); + vo.setDiffUnitPrice(submit.getSubmitUnitPrice()); + vo.setDiffRate(submit.getSubmitRate()); + vo.setDiffTotalPrice(submit.getSubmitTotalPrice()); + } + vo.setChangeType(AuditDivisionTreeRespVO.CHANGE_TYPE_ADD); + + result.add(vo); + } + + // 递归处理子节点 + if (CollUtil.isNotEmpty(submit.getChildren())) { + addNewNodes(result, submit.getChildren(), approveMap); + } + } + } + + /** + * 判断合并后的差异是否全为0 + */ + private boolean isAllZeroForMerged(AuditDivisionTreeRespVO vo) { + return isZero(vo.getDiffQty()) + && isZero(vo.getDiffUnitPrice()) + && isZero(vo.getDiffRate()) + && isZero(vo.getDiffTotalPrice()); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/ProgressDivisionServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/ProgressDivisionServiceImpl.java new file mode 100644 index 0000000..afa834b --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/ProgressDivisionServiceImpl.java @@ -0,0 +1,265 @@ +package com.yhy.module.core.service.workbench.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqDivisionRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.progresspayment.PeriodInfoVO; +import com.yhy.module.core.controller.admin.workbench.vo.progresspayment.ProgressDivisionSimpleVO; +import com.yhy.module.core.controller.admin.workbench.vo.progresspayment.ProgressDivisionTreeRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.progresspayment.ProgressPaymentModeRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.progresspayment.ProgressPaymentModeWithDivisionsRespVO; +import com.yhy.module.core.dal.dataobject.workbench.ProgressDivisionDO; +import com.yhy.module.core.dal.dataobject.workbench.ProgressPaymentModeDO; +import com.yhy.module.core.dal.mysql.workbench.ProgressDivisionMapper; +import com.yhy.module.core.service.workbench.ProgressDivisionService; +import com.yhy.module.core.service.workbench.ProgressPaymentModeService; +import com.yhy.module.core.service.workbench.WbBoqDivisionService; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Resource; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * 进度款-清单关联 Service 实现类 + * + * @author yhy + */ +@Service +public class ProgressDivisionServiceImpl implements ProgressDivisionService { + + @Resource + private ProgressDivisionMapper progressDivisionMapper; + + @Resource + private WbBoqDivisionService wbBoqDivisionService; + + @Resource + private ProgressPaymentModeService progressPaymentModeService; + + @Resource + private com.yhy.module.core.dal.mysql.workbench.ProgressPaymentModeMapper progressPaymentModeMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long saveOrUpdate(Long progressPaymentModeId, Long boqDivisionId, BigDecimal periodValue) { + // 查询是否已存在 + ProgressDivisionDO existDO = progressDivisionMapper.selectByPaymentModeIdAndBoqDivisionId( + progressPaymentModeId, boqDivisionId); + + if (existDO != null) { + // 更新 + ProgressDivisionDO updateDO = new ProgressDivisionDO(); + updateDO.setId(existDO.getId()); + updateDO.setPeriodValue(periodValue); + progressDivisionMapper.updateById(updateDO); + return existDO.getId(); + } else { + // 创建 + ProgressDivisionDO newDO = ProgressDivisionDO.builder() + .progressPaymentModeId(progressPaymentModeId) + .boqDivisionId(boqDivisionId) + .periodValue(periodValue) + .build(); + progressDivisionMapper.insert(newDO); + return newDO.getId(); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void batchSaveOrUpdate(Long progressPaymentModeId, Map periodValueMap) { + if (periodValueMap == null || periodValueMap.isEmpty()) { + return; + } + + // 查询已存在的记录 + List existList = progressDivisionMapper.selectListByProgressPaymentModeId(progressPaymentModeId); + Map existMap = new HashMap<>(); + for (ProgressDivisionDO item : existList) { + existMap.put(item.getBoqDivisionId(), item); + } + + // 遍历处理 + for (Map.Entry entry : periodValueMap.entrySet()) { + Long boqDivisionId = entry.getKey(); + BigDecimal periodValue = entry.getValue(); + + ProgressDivisionDO existDO = existMap.get(boqDivisionId); + if (existDO != null) { + // 更新(只更新有变化的) + if (!periodValue.equals(existDO.getPeriodValue())) { + ProgressDivisionDO updateDO = new ProgressDivisionDO(); + updateDO.setId(existDO.getId()); + updateDO.setPeriodValue(periodValue); + progressDivisionMapper.updateById(updateDO); + } + } else { + // 创建 + ProgressDivisionDO newDO = ProgressDivisionDO.builder() + .progressPaymentModeId(progressPaymentModeId) + .boqDivisionId(boqDivisionId) + .periodValue(periodValue) + .build(); + progressDivisionMapper.insert(newDO); + } + } + } + + @Override + public List getListByProgressPaymentModeId(Long progressPaymentModeId) { + return progressDivisionMapper.selectListByProgressPaymentModeId(progressPaymentModeId); + } + + @Override + public Map getPeriodValueMap(Long progressPaymentModeId) { + List list = getListByProgressPaymentModeId(progressPaymentModeId); + Map map = new HashMap<>(); + for (ProgressDivisionDO item : list) { + map.put(item.getBoqDivisionId(), item.getPeriodValue()); + } + return map; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteByProgressPaymentModeId(Long progressPaymentModeId) { + progressDivisionMapper.deleteByProgressPaymentModeId(progressPaymentModeId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteByBoqDivisionId(Long boqDivisionId) { + progressDivisionMapper.delete(new LambdaQueryWrapperX() + .eq(ProgressDivisionDO::getBoqDivisionId, boqDivisionId)); + } + + @Override + public List getTreeWithPeriodInfo(Long compileTreeId, Long progressPaymentModeId) { + // 1. 获取分部分项树(含定额单价计算) + List tree = wbBoqDivisionService.getTreeWithPrice(compileTreeId); + + // 2. 获取进度款模式信息 + ProgressPaymentModeDO paymentMode = null; + if (progressPaymentModeId != null) { + paymentMode = progressPaymentModeMapper.selectById(progressPaymentModeId); + } + + // 3. 查询进度款-清单关联数据,构建 Map + Map periodValueMap = new HashMap<>(); + if (progressPaymentModeId != null) { + periodValueMap = getPeriodValueMap(progressPaymentModeId); + } + + // 4. 转换树并绑定期数信息 + return convertTreeWithPeriodInfo(tree, paymentMode, periodValueMap); + } + + /** + * 转换树结构并绑定期数信息 + * 只对 nodeType='boq' 的清单节点进行绑定 + */ + private List convertTreeWithPeriodInfo( + List sourceTree, + ProgressPaymentModeDO paymentMode, + Map periodValueMap) { + + if (CollUtil.isEmpty(sourceTree)) { + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + + for (WbBoqDivisionRespVO source : sourceTree) { + ProgressDivisionTreeRespVO target = new ProgressDivisionTreeRespVO(); + BeanUtils.copyProperties(source, target); + + // 只对清单节点绑定期数信息 + if ("boq".equals(source.getNodeType()) && paymentMode != null) { + BigDecimal periodValue = periodValueMap.get(source.getId()); +// if (periodValue != null) { + PeriodInfoVO periodInfo = new PeriodInfoVO(); + periodInfo.setProgressPaymentModeId(paymentMode.getId()); + periodInfo.setPeriodValue(periodValue); + periodInfo.setPeriodNumber(paymentMode.getPeriodNumber()); + periodInfo.setProgressPaymentModeName(paymentMode.getName()); + target.setPeriod(periodInfo); +// } + } + + // 递归处理子节点 + if (CollUtil.isNotEmpty(source.getChildren())) { + // 由于泛型类型限制,需要强制转换(运行时安全,因为 ProgressDivisionTreeRespVO 继承 WbBoqDivisionRespVO) + @SuppressWarnings("unchecked") + List children = (List) (List) + convertTreeWithPeriodInfo(source.getChildren(), paymentMode, periodValueMap); + target.setChildren(children); + } + + result.add(target); + } + + return result; + } + + @Override + public List getProgressPaymentModeWithDivisionsList(Long projectId) { + // 1. 查询项目下的所有进度款模式 + List modeList = progressPaymentModeService.getProgressPaymentModeList(projectId); + if (CollUtil.isEmpty(modeList)) { + return new ArrayList<>(); + } + + // 2. 收集所有进度款模式ID + List modeIds = modeList.stream() + .map(ProgressPaymentModeRespVO::getId) + .collect(java.util.stream.Collectors.toList()); + + // 3. 批量查询所有关联的清单数据 + List divisionList = progressDivisionMapper.selectListByProgressPaymentModeIds(modeIds); + + // 4. 按 progressPaymentModeId 分组 + Map> divisionMap = new HashMap<>(); + for (ProgressDivisionDO division : divisionList) { + divisionMap.computeIfAbsent(division.getProgressPaymentModeId(), k -> new ArrayList<>()) + .add(division); + } + + // 5. 构建返回结果 + List result = new ArrayList<>(); + for (ProgressPaymentModeRespVO mode : modeList) { + ProgressPaymentModeWithDivisionsRespVO respVO = new ProgressPaymentModeWithDivisionsRespVO(); + BeanUtils.copyProperties(mode, respVO); + + // 设置关联的清单列表 + List divisions = divisionMap.get(mode.getId()); + if (CollUtil.isNotEmpty(divisions)) { + List children = divisions.stream() + .map(this::convertToSimpleVO) + .collect(java.util.stream.Collectors.toList()); + respVO.setChildren(children); + } else { + respVO.setChildren(new ArrayList<>()); + } + + result.add(respVO); + } + + return result; + } + + /** + * 转换为简要信息 VO + */ + private ProgressDivisionSimpleVO convertToSimpleVO(ProgressDivisionDO division) { + ProgressDivisionSimpleVO simpleVO = new ProgressDivisionSimpleVO(); +// simpleVO.setId(division.getId()); + simpleVO.setBoqDivisionId(division.getBoqDivisionId()); + simpleVO.setPeriodValue(division.getPeriodValue()); + return simpleVO; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/ProgressPaymentModeServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/ProgressPaymentModeServiceImpl.java new file mode 100644 index 0000000..06b407e --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/ProgressPaymentModeServiceImpl.java @@ -0,0 +1,199 @@ +package com.yhy.module.core.service.workbench.impl; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yhy.module.core.enums.ErrorCodeConstants.PROGRESS_PAYMENT_MODE_NOT_EXISTS; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import com.yhy.module.core.controller.admin.workbench.vo.progresspayment.ProgressPaymentModeCreateReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.progresspayment.ProgressPaymentModeRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.progresspayment.ProgressPaymentModeUpdateReqVO; +import com.yhy.module.core.dal.dataobject.workbench.ProgressPaymentModeDO; +import com.yhy.module.core.dal.mysql.workbench.ProgressPaymentModeMapper; +import com.yhy.module.core.service.workbench.ProgressPaymentModeService; + +import java.util.LinkedList; +import java.util.List; +import javax.annotation.Resource; +import javax.validation.Valid; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * 进度款模式 Service 实现类 + * + * @author yhy + */ +@Service +@Validated +public class ProgressPaymentModeServiceImpl implements ProgressPaymentModeService { + + @Resource + private ProgressPaymentModeMapper progressPaymentModeMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createProgressPaymentMode(@Valid ProgressPaymentModeCreateReqVO createReqVO) { + Long projectId = createReqVO.getProjectId(); + Long targetId = createReqVO.getTargetId(); + String insertPosition = createReqVO.getInsertPosition(); + + // 查询项目下所有数据(按periodNumber排序) + List list = progressPaymentModeMapper.selectListByProjectId(projectId); + + // 创建新记录 + ProgressPaymentModeDO newMode = new ProgressPaymentModeDO(); + newMode.setProjectId(projectId); + newMode.setName(createReqVO.getName()); + newMode.setStartDate(createReqVO.getStartDate()); + newMode.setEndDate(createReqVO.getEndDate()); + newMode.setTotalCost(createReqVO.getTotalCost()); + newMode.setCurrentPeriodValue(createReqVO.getCurrentPeriodValue()); + newMode.setCompletedValue(createReqVO.getCompletedValue()); + newMode.setCompletedRatio(createReqVO.getCompletedRatio()); + newMode.setPaymentRatio(createReqVO.getPaymentRatio()); + newMode.setPaymentAmount(createReqVO.getPaymentAmount()); + newMode.setMaterialAdjustment(createReqVO.getMaterialAdjustment()); + newMode.setRemark(createReqVO.getRemark()); + + // 计算插入位置 + int insertIndex; + if (targetId != null && insertPosition != null) { + // 指定插入位置:找到目标行的索引 + int targetIndex = -1; + for (int i = 0; i < list.size(); i++) { + if (list.get(i).getId().equals(targetId)) { + targetIndex = i; + break; + } + } + if (targetIndex == -1) { + throw exception(PROGRESS_PAYMENT_MODE_NOT_EXISTS); + } + + if ("above".equalsIgnoreCase(insertPosition)) { + insertIndex = targetIndex; + } else { + insertIndex = targetIndex + 1; + } + } else { + // 默认:追加到末尾 + insertIndex = list.size(); + } + + // 插入新记录到列表 + list.add(insertIndex, newMode); + + // 先插入新记录到数据库(获取ID) + progressPaymentModeMapper.insert(newMode); + + // 重新编号整个列表:1, 2, 3...N + List updateList = new LinkedList<>(); + for (int i = 0; i < list.size(); i++) { + ProgressPaymentModeDO item = list.get(i); + int newPeriodNumber = i + 1; + // 只收集periodNumber变化的记录 + if (!Integer.valueOf(newPeriodNumber).equals(item.getPeriodNumber())) { + item.setPeriodNumber(newPeriodNumber); + updateList.add(item); + } + } +// // 设置name(如果为空则自动生成) +// String name = createReqVO.getName(); +// if (name == null || name.trim().isEmpty()) { +// name = "第" + newMode.getPeriodNumber() + "期"; +// } +// newMode.setName("第" + newMode.getPeriodNumber() + "期"); + + // 批量更新(newMode 已在 updateList 中,name 会一起更新) + if (!updateList.isEmpty()) { + progressPaymentModeMapper.updateBatch(updateList); + } + + return newMode.getId(); + } + + @Override + public void updateProgressPaymentMode(@Valid ProgressPaymentModeUpdateReqVO updateReqVO) { + // 校验存在 + ProgressPaymentModeDO existProgressPaymentMode = progressPaymentModeMapper.selectById(updateReqVO.getId()); + if (existProgressPaymentMode == null) { + throw exception(PROGRESS_PAYMENT_MODE_NOT_EXISTS); + } + + // 更新进度款模式 + ProgressPaymentModeDO updateObj = new ProgressPaymentModeDO(); + updateObj.setId(updateReqVO.getId()); + updateObj.setName(updateReqVO.getName()); + updateObj.setPeriodNumber(updateReqVO.getPeriodNumber()); + updateObj.setStartDate(updateReqVO.getStartDate()); + updateObj.setEndDate(updateReqVO.getEndDate()); + updateObj.setTotalCost(updateReqVO.getTotalCost()); + updateObj.setCurrentPeriodValue(updateReqVO.getCurrentPeriodValue()); + updateObj.setCompletedValue(updateReqVO.getCompletedValue()); + updateObj.setCompletedRatio(updateReqVO.getCompletedRatio()); + updateObj.setPaymentRatio(updateReqVO.getPaymentRatio()); + updateObj.setPaymentAmount(updateReqVO.getPaymentAmount()); + updateObj.setMaterialAdjustment(updateReqVO.getMaterialAdjustment()); + updateObj.setRemark(updateReqVO.getRemark()); + progressPaymentModeMapper.updateById(updateObj); + } + + @Override + public void deleteProgressPaymentMode(Long id) { + // 校验存在 + ProgressPaymentModeDO existProgressPaymentMode = progressPaymentModeMapper.selectById(id); + if (existProgressPaymentMode == null) { + throw exception(PROGRESS_PAYMENT_MODE_NOT_EXISTS); + } + + // 删除进度款模式 + progressPaymentModeMapper.deleteById(id); + } + + @Override + public ProgressPaymentModeRespVO getProgressPaymentMode(Long id) { + ProgressPaymentModeDO progressPaymentMode = progressPaymentModeMapper.selectById(id); + if (progressPaymentMode == null) { + return null; + } + return convertToRespVO(progressPaymentMode); + } + + @Override + public List getProgressPaymentModeList(Long projectId) { + List list = progressPaymentModeMapper.selectListByProjectId(projectId); + return convertToRespVOList(list); + } + + /** + * 转换为 Response VO + */ + private ProgressPaymentModeRespVO convertToRespVO(ProgressPaymentModeDO progressPaymentMode) { + ProgressPaymentModeRespVO respVO = new ProgressPaymentModeRespVO(); + respVO.setId(progressPaymentMode.getId()); + respVO.setProjectId(progressPaymentMode.getProjectId()); + respVO.setName(progressPaymentMode.getName()); + respVO.setPeriodNumber(progressPaymentMode.getPeriodNumber()); + respVO.setStartDate(progressPaymentMode.getStartDate()); + respVO.setEndDate(progressPaymentMode.getEndDate()); + respVO.setTotalCost(progressPaymentMode.getTotalCost()); + respVO.setCurrentPeriodValue(progressPaymentMode.getCurrentPeriodValue()); + respVO.setCompletedValue(progressPaymentMode.getCompletedValue()); + respVO.setCompletedRatio(progressPaymentMode.getCompletedRatio()); + respVO.setPaymentRatio(progressPaymentMode.getPaymentRatio()); + respVO.setPaymentAmount(progressPaymentMode.getPaymentAmount()); + respVO.setMaterialAdjustment(progressPaymentMode.getMaterialAdjustment()); + respVO.setRemark(progressPaymentMode.getRemark()); + respVO.setCreateTime(progressPaymentMode.getCreateTime()); + respVO.setUpdateTime(progressPaymentMode.getUpdateTime()); + return respVO; + } + + /** + * 转换为 Response VO 列表 + */ + private List convertToRespVOList(List list) { + return list.stream().map(this::convertToRespVO).collect(java.util.stream.Collectors.toList()); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/QuotaPriceCalculatorServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/QuotaPriceCalculatorServiceImpl.java new file mode 100644 index 0000000..d24d7f7 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/QuotaPriceCalculatorServiceImpl.java @@ -0,0 +1,1382 @@ +package com.yhy.module.core.service.workbench.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import com.yhy.module.core.controller.admin.quota.vo.QuotaFeeItemWithRateRespVO; +import com.yhy.module.core.controller.admin.quota.vo.QuotaResourceRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.price.AdjustmentDetail; +import com.yhy.module.core.controller.admin.workbench.vo.price.CategoryPriceSum; +import com.yhy.module.core.controller.admin.workbench.vo.price.FeeItemPriceResult; +import com.yhy.module.core.controller.admin.workbench.vo.price.QuotaUnitPriceResult; +import com.yhy.module.core.controller.admin.workbench.vo.price.ResourcePriceDetail; +import com.yhy.module.core.dal.dataobject.resource.ResourceCategoryDO; +import com.yhy.module.core.dal.dataobject.resource.ResourceItemDO; +import com.yhy.module.core.dal.dataobject.workbench.WbBoqDivisionDO; +import com.yhy.module.core.dal.mysql.resource.ResourceCategoryMapper; +import com.yhy.module.core.dal.mysql.workbench.WbBoqDivisionMapper; +import com.yhy.module.core.service.quota.QuotaFeeItemService; +import com.yhy.module.core.service.quota.QuotaResourceService; +import com.yhy.module.core.service.workbench.QuotaPriceCalculatorService; +import com.yhy.module.core.service.workbench.WbBoqResourceService; +import com.yhy.module.core.service.workbench.WbUnifiedFeeConfigService; +import com.yhy.module.core.service.workbench.WbUnitFeeSettingService; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.stream.Collectors; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +/** + * 定额单价计算服务实现类 + * + * 设计原则: + * 1. 复用后台已有的工料机合价计算逻辑 + * 2. 新增分类汇总和取费计算逻辑 + * 3. 支持分层验证和调试 + * 4. 现阶段保证实时性,不缓存计算结果 + * + * @author yhy + */ +@Service +@Slf4j +public class QuotaPriceCalculatorServiceImpl implements QuotaPriceCalculatorService { + + private static final int SCALE = 6; + private static final RoundingMode ROUNDING_MODE = RoundingMode.HALF_UP; + private static final BigDecimal HUNDRED = new BigDecimal("100"); + + @Resource + private WbBoqDivisionMapper wbBoqDivisionMapper; + + @Resource + private ResourceCategoryMapper resourceCategoryMapper; + + @Resource + private QuotaFeeItemService quotaFeeItemService; + + @Resource + private WbUnitFeeSettingService wbUnitFeeSettingService; + + @Resource + private QuotaResourceService quotaResourceService; + + @Resource + private WbBoqResourceService wbBoqResourceService; + + @Resource + private WbUnifiedFeeConfigService wbUnifiedFeeConfigService; + + @Resource + private com.yhy.module.core.service.quota.QuotaUnifiedFeeService quotaUnifiedFeeService; + + @Resource + private com.yhy.module.core.service.quota.QuotaUnifiedFeeResourceService quotaUnifiedFeeResourceService; + + @Resource + private com.yhy.module.core.service.workbench.WbSnapshotReadService wbSnapshotReadService; + + @Resource + private com.yhy.module.core.dal.mysql.workbench.WbUnifiedFeeSettingMapper wbUnifiedFeeSettingMapper; + + @Resource + private com.yhy.module.core.dal.mysql.resource.ResourceItemMapper resourceItemMapper; + + // ========== 主计算接口 ========== + + @Override + public QuotaUnitPriceResult calculateQuotaUnitPrice(Long quotaItemId, Long divisionId) { + return calculateQuotaUnitPrice(quotaItemId, divisionId, false); + } + + @Override + public QuotaUnitPriceResult calculateQuotaUnitPrice(Long quotaItemId, Long divisionId, boolean debug) { + long startTime = System.currentTimeMillis(); + + try { + // log.debug("[定额单价计算] 开始 - quotaItemId={}, divisionId={}, debug={}", quotaItemId, divisionId, debug); + + // 1. 获取定额基价ID(优先从divisionId获取) + Long effectiveQuotaItemId = quotaItemId; + if (divisionId != null && effectiveQuotaItemId == null) { + WbBoqDivisionDO division = wbBoqDivisionMapper.selectById(divisionId); + if (division != null) { + effectiveQuotaItemId = division.getSourceQuotaItemId(); + } + } + + if (effectiveQuotaItemId == null) { + log.warn("[定额单价计算] 无法获取定额基价ID"); + return QuotaUnitPriceResult.fail(divisionId, quotaItemId, "无法获取定额基价ID"); + } + + // 2. 优先使用工作台工料机数据(包含价格来源虚拟替换),如果没有则使用后台数据 + List resourceDetails; + if (divisionId != null) { + // 使用工作台工料机数据(包含价格来源虚拟替换) + List wbResources = + wbBoqResourceService.getListByDivisionId(divisionId); + if (CollUtil.isNotEmpty(wbResources)) { + // log.debug("[定额单价计算] 使用工作台工料机数据(含价格来源替换)- 数量={}", wbResources.size()); + resourceDetails = convertWbResourcesToResourcePriceDetails(wbResources); + } else { + // 【已禁用】工作台没有数据时,不再 fallback 到后台数据,直接返回零单价 + // 计算逻辑已收归工作台,后台不再参与计算 + return QuotaUnitPriceResult.success(divisionId, effectiveQuotaItemId, BigDecimal.ZERO); + } + } else { + // 【已禁用】没有divisionId时,不再使用后台数据,直接返回零单价 + // 计算逻辑已收归工作台,后台不再参与计算 + return QuotaUnitPriceResult.success(divisionId, effectiveQuotaItemId, BigDecimal.ZERO); + } + + // 3. 第1层:工料机合价数据已准备好 + // log.debug("[定额单价计算] 第1层完成 - 工料机数量={}", resourceDetails.size()); + + // 4. 第2层:调整消耗量信息(后台已计算,此处仅用于调试) + List adjustmentDetails = null; + if (debug) { + // log.debug("[定额单价计算] 第2层 - 调整信息已由后台计算"); + adjustmentDetails = new ArrayList<>(); + } + + // 4. 第3层:按机类分类汇总 + // log.debug("[定额单价计算] 第3层开始 - 按机类分类汇总"); + Map categorySums = calculateCategorySums(resourceDetails); + // log.debug("[定额单价计算] 第3层完成 - 分类数量={}", categorySums.size()); + // if (log.isDebugEnabled()) { + // categorySums.forEach((k, v) -> + // log.trace(" 分类[{}]: 除税基价合价={}", k, v.getTaxExclBaseSum())); + // } + + // 5. 第4-5层:获取取费项并计算子单价(传入categorySums避免重复加载工料机) + // log.debug("[定额单价计算] 第4-5层开始 - 计算单价构成"); + List feeItems = getFeeItems(divisionId, effectiveQuotaItemId, categorySums); + List feeItemDetails = calculateFeeItemPrices(feeItems, categorySums); + // log.debug("[定额单价计算] 第4-5层完成 - 取费项数量={}", feeItemDetails.size()); + + // 6. 第6层:计算综合单价 + BigDecimal unitPrice = calculateTotalPrice(feeItemDetails); + // log.debug("[定额单价计算] 完成 - divisionId={}, unitPrice={}, 总耗时={}ms", + // divisionId, unitPrice, System.currentTimeMillis() - startTime); + + // 7. 构建结果 + QuotaUnitPriceResult result = QuotaUnitPriceResult.success(divisionId, effectiveQuotaItemId, unitPrice); + result.setCalculateTimeMs(System.currentTimeMillis() - startTime); + + if (debug) { + result.setResourceDetails(resourceDetails); + result.setAdjustmentDetails(adjustmentDetails); + result.setCategorySums(categorySums); + result.setFeeItemDetails(feeItemDetails); + } + + return result; + + } catch (Exception e) { + log.error("[定额单价计算] 异常 - divisionId={}, quotaItemId={}", divisionId, quotaItemId, e); + return QuotaUnitPriceResult.fail(divisionId, quotaItemId, "计算异常: " + e.getMessage()); + } + } + + @Override + public Map batchCalculateQuotaUnitPrices(List divisionIds) { + if (CollUtil.isEmpty(divisionIds)) { + return new HashMap<>(); + } + + log.debug("[定额单价批量计算] 开始 - 数量={}", divisionIds.size()); + long startTime = System.currentTimeMillis(); + + // 1. 批量查询定额节点 + List divisions = wbBoqDivisionMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .in(WbBoqDivisionDO::getId, divisionIds)); + Map divisionMap = divisions.stream() + .collect(Collectors.toMap(WbBoqDivisionDO::getId, Function.identity())); + + // 2. 并行计算(每个divisionId调用calculateQuotaUnitPrice,内部会调用QuotaResourceService获取数据) + Map results = new ConcurrentHashMap<>(); + divisionIds.parallelStream().forEach(divisionId -> { + WbBoqDivisionDO division = divisionMap.get(divisionId); + if (division == null) { + results.put(divisionId, QuotaUnitPriceResult.fail(divisionId, null, "定额节点不存在")); + return; + } + + Long quotaItemId = division.getSourceQuotaItemId(); + QuotaUnitPriceResult result = calculateQuotaUnitPrice(quotaItemId, divisionId, false); + results.put(divisionId, result); + }); + + log.debug("[定额单价批量计算] 完成 - 数量={}, 耗时={}ms", + divisionIds.size(), System.currentTimeMillis() - startTime); + + return results; + } + + // ========== 分层验证接口 ========== + + @Override + public Map calculateCategorySums(List resourceDetails) { + Map result = new HashMap<>(); + + if (CollUtil.isEmpty(resourceDetails)) { + return result; + } + + // 获取机类字典 + Map categoryMap = getCategoryMap(); + + for (ResourcePriceDetail detail : resourceDetails) { + Long categoryId = detail.getCategoryId(); + if (categoryId == null) { + continue; + } + + // 所有工料机(包括%单位工料机)按categoryId汇总,与分类值标签页逻辑一致 + // 复合工料机:分类统计时只取父值(子工料机不在列表中) + // 注意:工作台工料机列表只包含顶层数据,子数据不在列表中 + + CategoryPriceSum sum = result.computeIfAbsent(categoryId, k -> { + ResourceCategoryDO category = categoryMap.get(k); + CategoryPriceSum newSum = new CategoryPriceSum(k); + if (category != null) { + newSum.setCategoryCode(category.getCode()); + newSum.setCategoryName(category.getName()); + } + return newSum; + }); + + // 累加四个合价 + sum.addTaxExclBaseSum(detail.getTaxExclBaseTotalSum()); + sum.addTaxInclBaseSum(detail.getTaxInclBaseTotalSum()); + sum.addTaxExclCompileSum(detail.getTaxExclCompileTotalSum()); + sum.addTaxInclCompileSum(detail.getTaxInclCompileTotalSum()); + } + + return result; + } + + @Override + public BigDecimal evaluateCalcBaseFormula(Map calcBase, Map categorySums) { + if (MapUtil.isEmpty(calcBase)) { + return BigDecimal.ZERO; + } + + String formula = (String) calcBase.get("formula"); + if (StrUtil.isBlank(formula)) { + return BigDecimal.ZERO; + } + + @SuppressWarnings("unchecked") + Map variables = (Map) calcBase.get("variables"); + if (MapUtil.isEmpty(variables)) { + // 如果没有变量,尝试直接解析为数字 + try { + return new BigDecimal(formula.trim()); + } catch (NumberFormatException e) { + log.warn("[公式解析] 无变量且非数字公式: {}", formula); + return BigDecimal.ZERO; + } + } + + // 构建变量值映射 + Map varValues = new HashMap<>(); + for (Map.Entry entry : variables.entrySet()) { + String varName = entry.getKey(); + Object varValue = entry.getValue(); + + if (!(varValue instanceof Map)) { + continue; + } + + @SuppressWarnings("unchecked") + Map varInfo = (Map) varValue; + + Object categoryIdObj = varInfo.get("categoryId"); + String priceField = (String) varInfo.get("priceField"); + + if (categoryIdObj == null || priceField == null) { + varValues.put(varName, BigDecimal.ZERO); + continue; + } + + Long categoryId = ((Number) categoryIdObj).longValue(); + CategoryPriceSum sum = categorySums.get(categoryId); + + BigDecimal value = BigDecimal.ZERO; + if (sum != null) { + value = sum.getPriceByField(priceField); + } + + varValues.put(varName, value != null ? value : BigDecimal.ZERO); + } + + // 使用简单的表达式计算(替换变量后计算) + return evaluateFormula(formula, varValues); + } + + @Override + public FeeItemPriceResult calculateFeeItemPrice(QuotaFeeItemWithRateRespVO feeItem, + Map categorySums) { + + FeeItemPriceResult result = FeeItemPriceResult.builder() + .feeItemId(feeItem.getFeeItemId()) + .code(feeItem.getCode()) + .name(feeItem.getFeeItemName()) + .calcBase(feeItem.getCalcBase()) + .build(); + + try { + // 优先使用已计算的子单价(来自 WbUnitFeeSettingService.getMergedFeeListByDivisionId) + if (feeItem.getSubPrice() != null && feeItem.getSubPrice().compareTo(BigDecimal.ZERO) != 0) { + result.setCalcBaseValue(feeItem.getCalcBaseValue()); + result.setRateCode(feeItem.getRatePercentage()); + result.setSubPrice(feeItem.getSubPrice()); + log.debug("[取费项计算] 使用已计算的子单价 - code={}, subPrice={}", + feeItem.getCode(), feeItem.getSubPrice()); + return result; + } + + // 1. 计算基数值 + BigDecimal calcBaseValue = BigDecimal.ZERO; + if (feeItem.getCalcBase() != null) { + calcBaseValue = evaluateCalcBaseFormula(feeItem.getCalcBase(), categorySums); + } + result.setCalcBaseValue(calcBaseValue); + + // 2. 获取费率百分比 + BigDecimal ratePercent = BigDecimal.ZERO; + String rateCode = feeItem.getRatePercentage(); + result.setRateCode(rateCode); + + // 优先使用 rateValue(费率实际值) + if (feeItem.getRateValue() != null && feeItem.getRateValue().compareTo(BigDecimal.ZERO) != 0) { + ratePercent = feeItem.getRateValue(); + } else if (StrUtil.isNotBlank(rateCode)) { + try { + ratePercent = new BigDecimal(rateCode); + } catch (NumberFormatException e) { + // 费率代号可能是非数字(如引用其他取费项),跳过 + log.debug("[取费项计算] 费率代号非数字: {}", rateCode); + } + } + result.setRatePercent(ratePercent); + + // 3. 计算子单价 + BigDecimal subPrice; + if (ratePercent.compareTo(BigDecimal.ZERO) == 0) { + // 费率为0或未配置时,直接使用计算基数值作为子单价 + // 这适用于统一取费单价场景,calcBase公式直接计算出最终值 + subPrice = calcBaseValue.setScale(SCALE, ROUNDING_MODE); + } else { + // 子单价 = 计算基数值 × (费率% / 100) + subPrice = calcBaseValue + .multiply(ratePercent) + .divide(HUNDRED, SCALE, ROUNDING_MODE); + } + result.setSubPrice(subPrice); + + } catch (Exception e) { + log.error("[取费项计算] 异常 - feeItemId={}, code={}", feeItem.getFeeItemId(), feeItem.getCode(), e); + result.setSubPrice(BigDecimal.ZERO); + result.setErrorMessage("计算异常: " + e.getMessage()); + } + + return result; + } + + @Override + public List calculateFeeItemPrices(List feeItems, + Map categorySums) { + + if (CollUtil.isEmpty(feeItems)) { + return new ArrayList<>(); + } + + List results = new ArrayList<>(); + for (QuotaFeeItemWithRateRespVO feeItem : feeItems) { + FeeItemPriceResult result = calculateFeeItemPrice(feeItem, categorySums); + results.add(result); + } + + return results; + } + + @Override + public BigDecimal calculateTotalPrice(List feeItemResults) { + if (CollUtil.isEmpty(feeItemResults)) { + return BigDecimal.ZERO; + } + + // 综合单价代号 + final String ZHDJ_CODE = "ZHDJ"; + + // 1. 构建取费项代号到子单价的映射 + Map feeItemSubPriceMap = new java.util.HashMap<>(); + FeeItemPriceResult zhdjItem = null; + + for (FeeItemPriceResult item : feeItemResults) { + if (ZHDJ_CODE.equals(item.getCode())) { + zhdjItem = item; + } else if (item.getCode() != null && item.getSubPrice() != null) { + feeItemSubPriceMap.put(item.getCode(), item.getSubPrice()); + } + } + + // 2. 如果有综合单价行 + if (zhdjItem != null) { + BigDecimal calcBaseValue = BigDecimal.ZERO; + boolean hasFormula = false; + + // 检查综合单价是否有calcBase公式 + if (zhdjItem.getCalcBase() != null) { + @SuppressWarnings("unchecked") + Map calcBase = zhdjItem.getCalcBase(); + String formula = (String) calcBase.get("formula"); + + if (formula != null && !formula.isEmpty()) { + hasFormula = true; + // 使用公式计算(引用其他取费项的子单价) + calcBaseValue = evaluateZhdjFormula(formula, feeItemSubPriceMap); + } + } + + // 只有在没有配置公式时,才使用子单价之和作为回退 + // 如果配置了公式但计算结果为0,则直接使用0(不回退) + if (!hasFormula && calcBaseValue.compareTo(BigDecimal.ZERO) == 0) { + for (BigDecimal subPrice : feeItemSubPriceMap.values()) { + calcBaseValue = calcBaseValue.add(subPrice); + } + } + + // 应用费率 + BigDecimal zhdjRate = parseZhdjRate(zhdjItem.getRateCode()); + return calcBaseValue + .multiply(zhdjRate) + .divide(HUNDRED, SCALE, ROUNDING_MODE); + } + + // 3. 没有综合单价行,直接返回子单价之和 + BigDecimal totalSubPrice = BigDecimal.ZERO; + for (BigDecimal subPrice : feeItemSubPriceMap.values()) { + totalSubPrice = totalSubPrice.add(subPrice); + } + return totalSubPrice.setScale(SCALE, ROUNDING_MODE); + } + + /** + * 解析综合单价公式(变量为其他取费项的代号) + */ + private BigDecimal evaluateZhdjFormula(String formula, Map feeItemSubPriceMap) { + if (formula == null || formula.isEmpty()) { + return BigDecimal.ZERO; + } + + try { + // 替换变量为对应的子单价值 + String expression = formula; + for (Map.Entry entry : feeItemSubPriceMap.entrySet()) { + String varName = entry.getKey(); + BigDecimal value = entry.getValue(); + expression = expression.replaceAll("\\b" + java.util.regex.Pattern.quote(varName) + "\\b", + value.toPlainString()); + } + + // 使用JavaScript引擎计算表达式 + javax.script.ScriptEngineManager manager = new javax.script.ScriptEngineManager(); + javax.script.ScriptEngine engine = manager.getEngineByName("JavaScript"); + if (engine == null) { + engine = manager.getEngineByName("nashorn"); + } + if (engine == null) { + log.warn("[综合单价计算] 无法获取JavaScript引擎"); + return BigDecimal.ZERO; + } + + Object result = engine.eval(expression); + if (result instanceof Number) { + return new BigDecimal(result.toString()).setScale(SCALE, ROUNDING_MODE); + } + return BigDecimal.ZERO; + } catch (Exception e) { + log.error("[综合单价计算] 公式计算异常: formula={}, error={}", formula, e.getMessage()); + return BigDecimal.ZERO; + } + } + + /** + * 解析综合单价的费率代号 + * + * @param rateCode 费率代号(只允许数字或空值) + * @return 费率百分比,空值返回100 + */ + private BigDecimal parseZhdjRate(String rateCode) { + if (StrUtil.isBlank(rateCode)) { + return HUNDRED; // 空值默认100% + } + + try { + return new BigDecimal(rateCode.trim()); + } catch (NumberFormatException e) { + log.warn("[综合单价费率] 费率代号非数字,使用默认值100%: {}", rateCode); + return HUNDRED; + } + } + + // ========== 私有方法 ========== + + /** + * 获取机类字典(缓存) + */ + @Cacheable(value = "resourceCategory", key = "'all'") + public Map getCategoryMap() { + List categories = resourceCategoryMapper.selectList(); + return categories.stream() + .collect(Collectors.toMap(ResourceCategoryDO::getId, Function.identity())); + } + + /** + * 获取取费项列表 + * + * 优先使用工作台覆写值,否则使用标准库取费项 + */ + private List getFeeItems(Long divisionId, Long quotaItemId) { + return getFeeItems(divisionId, quotaItemId, null); + } + + /** + * 获取取费项列表(支持传入预计算的分类汇总,避免重复查询工料机) + */ + private List getFeeItems(Long divisionId, Long quotaItemId, + Map categorySums) { + // 尝试获取工作台覆写的取费项 + if (divisionId != null) { + try { + List mergedList = categorySums != null + ? wbUnitFeeSettingService.getMergedFeeListByDivisionId(divisionId, categorySums) + : wbUnitFeeSettingService.getMergedFeeListByDivisionId(divisionId); + if (CollUtil.isNotEmpty(mergedList)) { + return mergedList; + } + } catch (Exception e) { + log.warn("[获取取费项] 获取工作台覆写失败,使用标准库取费项 - divisionId={}", divisionId, e); + } + } + + // 使用标准库取费项 + if (quotaItemId != null) { + // 需要通过定额基价ID获取对应的目录ID + // 这里简化处理,直接返回空列表,实际需要根据业务逻辑获取 + log.debug("[获取取费项] 使用标准库取费项 - quotaItemId={}", quotaItemId); + } + + return new ArrayList<>(); + } + + /** + * 安全乘法(处理null值) + */ + private BigDecimal safeMultiply(BigDecimal a, BigDecimal b) { + if (a == null || b == null) { + return BigDecimal.ZERO; + } + return a.multiply(b).setScale(SCALE, ROUNDING_MODE); + } + + /** + * 简单表达式计算 + * + * 支持:加减乘除、括号、变量替换 + */ + private BigDecimal evaluateFormula(String formula, Map varValues) { + if (StrUtil.isBlank(formula)) { + return BigDecimal.ZERO; + } + + // 替换变量 + String expression = formula; + + // 按变量名长度降序排序,避免短变量名替换长变量名的一部分 + List sortedVarNames = new ArrayList<>(varValues.keySet()); + sortedVarNames.sort((a, b) -> Integer.compare(b.length(), a.length())); + + for (String varName : sortedVarNames) { + BigDecimal value = varValues.get(varName); + expression = expression.replace(varName, value.toPlainString()); + } + + // 计算表达式 + try { + return evaluateExpression(expression); + } catch (Exception e) { + log.warn("[公式计算] 计算失败 - formula={}, expression={}", formula, expression, e); + return BigDecimal.ZERO; + } + } + + /** + * 计算数学表达式(支持加减乘除和括号) + */ + private BigDecimal evaluateExpression(String expression) { + expression = expression.replaceAll("\\s+", ""); + return parseExpression(expression, new int[]{0}); + } + + private BigDecimal parseExpression(String expr, int[] pos) { + BigDecimal result = parseTerm(expr, pos); + + while (pos[0] < expr.length()) { + char op = expr.charAt(pos[0]); + if (op == '+') { + pos[0]++; + result = result.add(parseTerm(expr, pos)); + } else if (op == '-') { + pos[0]++; + result = result.subtract(parseTerm(expr, pos)); + } else { + break; + } + } + + return result; + } + + private BigDecimal parseTerm(String expr, int[] pos) { + BigDecimal result = parseFactor(expr, pos); + + while (pos[0] < expr.length()) { + char op = expr.charAt(pos[0]); + if (op == '*') { + pos[0]++; + result = result.multiply(parseFactor(expr, pos)); + } else if (op == '/') { + pos[0]++; + BigDecimal divisor = parseFactor(expr, pos); + if (divisor.compareTo(BigDecimal.ZERO) == 0) { + return BigDecimal.ZERO; + } + result = result.divide(divisor, SCALE, ROUNDING_MODE); + } else { + break; + } + } + + return result; + } + + private BigDecimal parseFactor(String expr, int[] pos) { + if (pos[0] >= expr.length()) { + return BigDecimal.ZERO; + } + + char c = expr.charAt(pos[0]); + + // 处理负号 + boolean negative = false; + if (c == '-') { + negative = true; + pos[0]++; + if (pos[0] >= expr.length()) { + return BigDecimal.ZERO; + } + c = expr.charAt(pos[0]); + } + + BigDecimal result; + + if (c == '(') { + pos[0]++; + result = parseExpression(expr, pos); + if (pos[0] < expr.length() && expr.charAt(pos[0]) == ')') { + pos[0]++; + } + } else { + // 解析数字 + int start = pos[0]; + while (pos[0] < expr.length() && (Character.isDigit(expr.charAt(pos[0])) || expr.charAt(pos[0]) == '.')) { + pos[0]++; + } + if (start == pos[0]) { + return BigDecimal.ZERO; + } + result = new BigDecimal(expr.substring(start, pos[0])); + } + + return negative ? result.negate() : result; + } + + /** + * 将工作台工料机VO转换为ResourcePriceDetail(包含价格来源虚拟替换后的数据) + * 直接复用 VO 中已计算好的合价(含%工料机公式计算值和复合工料机汇总值) + */ + private List convertWbResourcesToResourcePriceDetails( + List wbResources) { + List result = new ArrayList<>(); + + for (com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceRespVO resource : wbResources) { + BigDecimal originalQty = resource.getConsumeQty() != null ? resource.getConsumeQty() : BigDecimal.ZERO; + BigDecimal effectiveQty = resource.getAdjustConsumeQty() != null ? resource.getAdjustConsumeQty() : originalQty; + BigDecimal taxExclBasePrice = resource.getTaxExclBasePrice() != null ? resource.getTaxExclBasePrice() : BigDecimal.ZERO; + BigDecimal taxInclBasePrice = resource.getTaxInclBasePrice() != null ? resource.getTaxInclBasePrice() : BigDecimal.ZERO; + BigDecimal taxExclCompilePrice = resource.getTaxExclCompilePrice() != null ? resource.getTaxExclCompilePrice() : BigDecimal.ZERO; + BigDecimal taxInclCompilePrice = resource.getTaxInclCompilePrice() != null ? resource.getTaxInclCompilePrice() : BigDecimal.ZERO; + + // 直接使用 VO 中已计算好的合价(%工料机和复合工料机的合价已由 getListByDivisionId 计算) + BigDecimal taxExclBaseTotalSum = resource.getTaxExclBaseTotalSum(); + BigDecimal taxInclBaseTotalSum = resource.getTaxInclBaseTotalSum(); + BigDecimal taxExclCompileTotalSum = resource.getTaxExclCompileTotalSum(); + BigDecimal taxInclCompileTotalSum = resource.getTaxInclCompileTotalSum(); + + boolean isMerged = resource.getChildren() != null && !resource.getChildren().isEmpty(); + boolean isPercentUnit = "%".equals(resource.getUnit()); + + ResourcePriceDetail detail = ResourcePriceDetail.builder() + .resourceId(resource.getId()) + .resourceCode(resource.getCode()) + .resourceName(resource.getName()) + .resourceType(resource.getResourceType()) + .categoryId(resource.getCategoryId()) + .dosage(originalQty) + .adjustedDosage(effectiveQty) + .effectiveDosage(effectiveQty) + .taxExclBasePrice(taxExclBasePrice) + .taxInclBasePrice(taxInclBasePrice) + .taxExclCompilePrice(taxExclCompilePrice) + .taxInclCompilePrice(taxInclCompilePrice) + .taxExclBaseTotalSum(taxExclBaseTotalSum) + .taxInclBaseTotalSum(taxInclBaseTotalSum) + .taxExclCompileTotalSum(taxExclCompileTotalSum) + .taxInclCompileTotalSum(taxInclCompileTotalSum) + .isMerged(isMerged) + .isPercentUnit(isPercentUnit) + .calcBase(resource.getCalcBase()) + .build(); + + result.add(detail); + } + + return result; + } + + /** + * 将后台定额工料机VO转换为ResourcePriceDetail(复用后台已计算好的数据) + */ + private List convertToResourcePriceDetails(List quotaResources) { + List result = new ArrayList<>(); + + for (QuotaResourceRespVO resource : quotaResources) { + boolean isPercentUnit = "%".equals(resource.getResourceUnit()); + + ResourcePriceDetail detail = ResourcePriceDetail.builder() + .resourceId(resource.getId()) + .resourceCode(resource.getResourceCode()) + .resourceName(resource.getResourceName()) + .resourceType(resource.getResourceType()) + .categoryId(resource.getResourceCategoryId()) + .dosage(resource.getDosage()) + .adjustedDosage(resource.getAdjustedDosage()) + .effectiveDosage(resource.getActualDosage()) + .taxExclBasePrice(resource.getResourceTaxExclBasePrice()) + .taxInclBasePrice(resource.getResourceTaxInclBasePrice()) + .taxExclCompilePrice(resource.getResourceTaxExclCompilePrice()) + .taxInclCompilePrice(resource.getResourceTaxInclCompilePrice()) + .taxExclBaseTotalSum(resource.getTaxExclBaseTotalSum()) + .taxInclBaseTotalSum(resource.getTaxInclBaseTotalSum()) + .taxExclCompileTotalSum(resource.getTaxExclCompileTotalSum()) + .taxInclCompileTotalSum(resource.getTaxInclCompileTotalSum()) + .isMerged(resource.getIsMerged() != null && resource.getIsMerged()) + .isPercentUnit(isPercentUnit) + .build(); + + result.add(detail); + } + + return result; + } + + // ========== 统一取费父定额单价计算 ========== + + @Override + public BigDecimal calculateUnifiedFeeParentPrice(Long divisionId) { + QuotaUnitPriceResult result = calculateUnifiedFeeParentPrice(divisionId, false); + return Boolean.TRUE.equals(result.getSuccess()) ? result.getUnitPrice() : BigDecimal.ZERO; + } + + @Override + public QuotaUnitPriceResult calculateUnifiedFeeParentPrice(Long divisionId, boolean debug) { + long startTime = System.currentTimeMillis(); + + try { + // 1. 获取统一取费节点 + WbBoqDivisionDO unifiedFeeNode = wbBoqDivisionMapper.selectById(divisionId); + if (unifiedFeeNode == null || !"unified_fee".equals(unifiedFeeNode.getNodeType())) { + return QuotaUnitPriceResult.fail(divisionId, null, "非统一取费节点"); + } + + // 2. 获取母定额列表(汇总来源的quota节点) + List summaryItems = + wbUnifiedFeeConfigService.getSummarySourceByDivisionId(divisionId); + + if (CollUtil.isEmpty(summaryItems)) { + log.debug("[统一取费单价] 汇总来源为空 - divisionId={}", divisionId); + return QuotaUnitPriceResult.success(divisionId, null, BigDecimal.ZERO); + } + + // 3. 获取统一取费单价的取费项配置 + // 从节点attributes获取sourceUnifiedFeeSettingId,然后获取catalogItemId + Map attributes = unifiedFeeNode.getAttributes(); + if (attributes == null || attributes.get("sourceUnifiedFeeSettingId") == null) { + return QuotaUnitPriceResult.fail(divisionId, null, "缺少sourceUnifiedFeeSettingId"); + } + + Long sourceUnifiedFeeSettingId = Long.parseLong(attributes.get("sourceUnifiedFeeSettingId").toString()); + Long compileTreeId = unifiedFeeNode.getCompileTreeId(); + + // 通过sourceUnifiedFeeSettingId从快照表获取统一取费设置 + com.yhy.module.core.dal.dataobject.workbench.WbUnifiedFeeSettingDO unifiedFeeSetting = + getUnifiedFeeSettingFromSnapshot(compileTreeId, sourceUnifiedFeeSettingId); + if (unifiedFeeSetting == null) { + log.warn("[统一取费单价] 快照中统一取费设置不存在 - compileTreeId={}, sourceId={}", compileTreeId, sourceUnifiedFeeSettingId); + return QuotaUnitPriceResult.fail(divisionId, null, "统一取费设置不存在"); + } + + // 4. 获取比例配置 + BigDecimal thisListPercentage = BigDecimal.valueOf(100); // 默认100% + BigDecimal specifiedListPercentage = BigDecimal.valueOf(100); // 默认100% + Boolean isSpecifiedList = false; + + // 注意:JSONB 反序列化时 key 存在但值为 JSON null 或空字符串时需跳过,否则 BigDecimal 构造会抛异常 + Object rawThisListPercentage = attributes.get("thisListPercentage"); + if (rawThisListPercentage != null) { + String strVal = rawThisListPercentage.toString().trim(); + if (!strVal.isEmpty() && !"null".equals(strVal)) { + thisListPercentage = new BigDecimal(strVal); + } + } + Object rawSpecifiedListPercentage = attributes.get("specifiedListPercentage"); + if (rawSpecifiedListPercentage != null) { + String strVal = rawSpecifiedListPercentage.toString().trim(); + if (!strVal.isEmpty() && !"null".equals(strVal)) { + specifiedListPercentage = new BigDecimal(strVal); + } + } + Object rawIsSpecifiedList = attributes.get("isSpecifiedList"); + if (rawIsSpecifiedList != null) { + String strVal = rawIsSpecifiedList.toString().trim(); + if (!strVal.isEmpty() && !"null".equals(strVal)) { + isSpecifiedList = Boolean.valueOf(strVal); + } + } + + log.info("[统一取费单价] 比例配置 - thisListPercentage={}, specifiedListPercentage={}, isSpecifiedList={}", + thisListPercentage, specifiedListPercentage, isSpecifiedList); + + // 5. 获取统一取费设置的子费用列表(从快照表读取) + List childSettings = + getChildSettingsFromSnapshot(compileTreeId, unifiedFeeSetting.getId()); + log.info("[统一取费单价] 子费用数量={}", childSettings.size()); + + // 6. 对每个母定额计算贡献的单价 + BigDecimal totalPrice = BigDecimal.ZERO; + List> debugDetails = debug ? new ArrayList<>() : null; + + log.info("[统一取费单价] 开始计算 - divisionId={}, 母定额数量={}", divisionId, summaryItems.size()); + + for (com.yhy.module.core.controller.admin.workbench.vo.WbUnifiedFeeSummaryItemVO summaryItem : summaryItems) { + Long motherDivisionId = summaryItem.getId(); + BigDecimal motherQuantity = summaryItem.getQuantity() != null ? summaryItem.getQuantity() : BigDecimal.ONE; + + log.info("[统一取费单价] 处理母定额 - id={}, code={}, qty={}", + motherDivisionId, summaryItem.getCode(), motherQuantity); + + // 6a. 获取母定额的工料机分类合价 + Map motherCategorySums = getMotherCategorySums(motherDivisionId); + log.info("[统一取费单价] 母定额分类合价 - categorySums={}", motherCategorySums); + + // 6b. 获取母定额的sourceCatalogItemId用于取费章节过滤 + Long motherCatalogItemId = summaryItem.getSourceCatalogItemId(); + log.info("[统一取费单价] 母定额sourceCatalogItemId={}", motherCatalogItemId); + + // 6c. 遍历子费用,只计算匹配取费章节的子费用下的子目工料机合价 + BigDecimal childUnitPrice = BigDecimal.ZERO; + for (com.yhy.module.core.dal.dataobject.workbench.WbUnifiedFeeSettingDO childSetting : childSettings) { + // 检查母定额是否在该子费用的取费章节范围内 + if (!isMotherQuotaInFeeChapter(motherCatalogItemId, childSetting.getFeeChapter())) { + log.debug("[统一取费单价] 子费用 {} 的取费章节不匹配,跳过", childSetting.getCode()); + continue; + } + + // 获取子费用下的子目工料机列表(从快照表读取) + List snapshotResources = + wbSnapshotReadService.getUnifiedFeeResources(compileTreeId, childSetting.getId()); + log.info("[统一取费单价] 子费用 {} 匹配,子目工料机数量={}", childSetting.getCode(), snapshotResources.size()); + + for (com.yhy.module.core.dal.dataobject.workbench.WbUnifiedFeeResourceDO snapshotRes : snapshotResources) { + Long resourceItemId = snapshotRes.getResourceItemId(); + if (resourceItemId == null) continue; + ResourceItemDO resourceItem = resourceItemMapper.selectById(resourceItemId); + if (resourceItem == null) continue; + + BigDecimal dosage = snapshotRes.getEffectiveDosage() != null ? snapshotRes.getEffectiveDosage() : BigDecimal.ZERO; + + // 检查是否为%工料机(从ResourceItemDO获取单位) + if ("%".equals(resourceItem.getUnit())) { + // calcBase存储在yhy_resource_item.calc_base字段 + Map calcBase = resourceItem.getCalcBase(); + BigDecimal calcBaseValue = evaluateCalcBaseForUnifiedFee(calcBase, motherCategorySums); + + // dosage 表示百分比,如 100 表示 100% + BigDecimal resourcePrice = calcBaseValue.multiply(dosage).divide(HUNDRED, SCALE, ROUNDING_MODE); + + log.info("[统一取费单价] %工料机 - code={}, calcBase={}, calcBaseValue={}, dosage={}%, resourcePrice={}", + resourceItem.getCode(), calcBase, calcBaseValue, dosage, resourcePrice); + + childUnitPrice = childUnitPrice.add(resourcePrice); + } else { + // 普通工料机:使用单价 × 消耗量 + BigDecimal price = resourceItem.getTaxExclCompilePrice() != null ? + resourceItem.getTaxExclCompilePrice() : BigDecimal.ZERO; + BigDecimal resourcePrice = price.multiply(dosage).setScale(SCALE, ROUNDING_MODE); + + log.info("[统一取费单价] 普通工料机 - code={}, price={}, dosage={}, resourcePrice={}", + resourceItem.getCode(), price, dosage, resourcePrice); + + childUnitPrice = childUnitPrice.add(resourcePrice); + } + } + } + + log.info("[统一取费单价] 子定额综合单价B={}", childUnitPrice); + + // 5e. B × 母定额的工程量 = 该母定额贡献的单价 + BigDecimal contribution = childUnitPrice.multiply(motherQuantity).setScale(SCALE, ROUNDING_MODE); + log.info("[统一取费单价] 母定额贡献单价={}", contribution); + totalPrice = totalPrice.add(contribution); + + if (debug) { + Map detail = new HashMap<>(); + detail.put("motherDivisionId", motherDivisionId); + detail.put("motherCode", summaryItem.getCode()); + detail.put("motherName", summaryItem.getName()); + detail.put("motherQuantity", motherQuantity); + detail.put("categorySums", motherCategorySums); + detail.put("childUnitPrice", childUnitPrice); + detail.put("contribution", contribution); + debugDetails.add(detail); + } + } + + // 6. 应用比例计算 + BigDecimal finalPrice; + if (isSpecifiedList) { + // 指定清单节点:应用指定清单比例% + finalPrice = totalPrice.multiply(specifiedListPercentage) + .divide(BigDecimal.valueOf(100), SCALE, ROUNDING_MODE); + log.info("[统一取费单价] 指定清单节点 - totalPrice={}, specifiedListPercentage={}%, finalPrice={}", + totalPrice, specifiedListPercentage, finalPrice); + } else { + // 普通节点:应用本清单比例% + finalPrice = totalPrice.multiply(thisListPercentage) + .divide(BigDecimal.valueOf(100), SCALE, ROUNDING_MODE); + log.info("[统一取费单价] 普通节点 - totalPrice={}, thisListPercentage={}%, finalPrice={}", + totalPrice, thisListPercentage, finalPrice); + } + + // 7. 构建结果 + QuotaUnitPriceResult result = QuotaUnitPriceResult.success(divisionId, null, finalPrice); + result.setCalculateTimeMs(System.currentTimeMillis() - startTime); + + if (debug && debugDetails != null) { + // 将调试信息存入result(可以扩展QuotaUnitPriceResult添加unifiedFeeDetails字段) + log.debug("[统一取费单价] 计算完成 - divisionId={}, totalPrice={}, details={}", + divisionId, totalPrice, debugDetails); + } + + return result; + + } catch (Exception e) { + log.error("[统一取费单价] 计算异常 - divisionId={}", divisionId, e); + return QuotaUnitPriceResult.fail(divisionId, null, "计算异常: " + e.getMessage()); + } + } + + @Override + public Map getUnifiedFeeCategorySums(Long divisionId) { + Map result = new HashMap<>(); + try { + // 1. 获取统一取费节点 + WbBoqDivisionDO unifiedFeeNode = wbBoqDivisionMapper.selectById(divisionId); + if (unifiedFeeNode == null || !"unified_fee".equals(unifiedFeeNode.getNodeType())) { + return result; + } + + Map attributes = unifiedFeeNode.getAttributes(); + if (attributes == null || attributes.get("sourceUnifiedFeeSettingId") == null) { + return result; + } + + Long sourceUnifiedFeeSettingId = Long.valueOf(attributes.get("sourceUnifiedFeeSettingId").toString()); + Long compileTreeId = unifiedFeeNode.getCompileTreeId(); + + // 2. 获取母定额列表 + List summaryItems = + wbUnifiedFeeConfigService.getSummarySourceByDivisionId(divisionId); + if (CollUtil.isEmpty(summaryItems)) { + return result; + } + + // 3. 从快照表获取统一取费设置 + com.yhy.module.core.dal.dataobject.workbench.WbUnifiedFeeSettingDO unifiedFeeSetting = + getUnifiedFeeSettingFromSnapshot(compileTreeId, sourceUnifiedFeeSettingId); + if (unifiedFeeSetting == null) { + return result; + } + + // 4. 获取子费用列表 + List childSettings = + getChildSettingsFromSnapshot(compileTreeId, unifiedFeeSetting.getId()); + + // 5. 对每个母定额计算分类合价贡献 + for (com.yhy.module.core.controller.admin.workbench.vo.WbUnifiedFeeSummaryItemVO summaryItem : summaryItems) { + Long motherDivisionId = summaryItem.getId(); + BigDecimal motherQuantity = summaryItem.getQuantity() != null ? summaryItem.getQuantity() : BigDecimal.ONE; + Long motherCatalogItemId = summaryItem.getSourceCatalogItemId(); + + // 获取母定额的工料机分类合价 + Map motherCategorySums = getMotherCategorySums(motherDivisionId); + + // 遍历子费用 + for (com.yhy.module.core.dal.dataobject.workbench.WbUnifiedFeeSettingDO childSetting : childSettings) { + // 取费章节过滤 + if (!isMotherQuotaInFeeChapter(motherCatalogItemId, childSetting.getFeeChapter())) { + continue; + } + + // 获取子费用的子目工料机(直接使用快照DO,calcBase从ResourceItemDO获取) + List snapshotResources = + wbSnapshotReadService.getUnifiedFeeResources(compileTreeId, childSetting.getId()); + + for (com.yhy.module.core.dal.dataobject.workbench.WbUnifiedFeeResourceDO snapshotRes : snapshotResources) { + Long resourceItemId = snapshotRes.getResourceItemId(); + if (resourceItemId == null) continue; + + ResourceItemDO resourceItem = resourceItemMapper.selectById(resourceItemId); + if (resourceItem == null) continue; + Long categoryId = resourceItem.getCategoryId(); + if (categoryId == null) continue; + + CategoryPriceSum sum = result.computeIfAbsent(categoryId, k -> { + CategoryPriceSum newSum = new CategoryPriceSum(k); + ResourceCategoryDO category = getCategoryMap().get(k); + if (category != null) { + newSum.setCategoryCode(category.getCode()); + newSum.setCategoryName(category.getName()); + } + return newSum; + }); + + BigDecimal dosage = snapshotRes.getEffectiveDosage() != null ? snapshotRes.getEffectiveDosage() : BigDecimal.ZERO; + + if ("%".equals(resourceItem.getUnit())) { + // %工料机:用calcBase公式计算基数值,再乘以dosage% + // calcBase存储在yhy_resource_item.calc_base字段,而非快照attributes中 + Map calcBase = resourceItem.getCalcBase(); + // 分别对四个价格字段计算 + BigDecimal calcBaseExclBase = evaluateCalcBaseForUnifiedFeeByField(calcBase, motherCategorySums, "taxExclBaseSum"); + BigDecimal calcBaseInclBase = evaluateCalcBaseForUnifiedFeeByField(calcBase, motherCategorySums, "taxInclBaseSum"); + BigDecimal calcBaseExclCompile = evaluateCalcBaseForUnifiedFeeByField(calcBase, motherCategorySums, "taxExclCompileSum"); + BigDecimal calcBaseInclCompile = evaluateCalcBaseForUnifiedFeeByField(calcBase, motherCategorySums, "taxInclCompileSum"); + + BigDecimal dosagePercent = dosage.divide(HUNDRED, SCALE, ROUNDING_MODE); + // 合价 = calcBase × dosage% × 母定额工程量 + sum.addTaxExclBaseSum(calcBaseExclBase.multiply(dosagePercent).multiply(motherQuantity).setScale(SCALE, ROUNDING_MODE)); + sum.addTaxInclBaseSum(calcBaseInclBase.multiply(dosagePercent).multiply(motherQuantity).setScale(SCALE, ROUNDING_MODE)); + sum.addTaxExclCompileSum(calcBaseExclCompile.multiply(dosagePercent).multiply(motherQuantity).setScale(SCALE, ROUNDING_MODE)); + sum.addTaxInclCompileSum(calcBaseInclCompile.multiply(dosagePercent).multiply(motherQuantity).setScale(SCALE, ROUNDING_MODE)); + } else { + // 普通工料机:单价 × 消耗量 × 母定额工程量 + BigDecimal exclBase = resourceItem.getTaxExclBasePrice() != null ? resourceItem.getTaxExclBasePrice() : BigDecimal.ZERO; + BigDecimal inclBase = resourceItem.getTaxInclBasePrice() != null ? resourceItem.getTaxInclBasePrice() : BigDecimal.ZERO; + BigDecimal exclCompile = resourceItem.getTaxExclCompilePrice() != null ? resourceItem.getTaxExclCompilePrice() : BigDecimal.ZERO; + BigDecimal inclCompile = resourceItem.getTaxInclCompilePrice() != null ? resourceItem.getTaxInclCompilePrice() : BigDecimal.ZERO; + + BigDecimal qty = dosage.multiply(motherQuantity); + sum.addTaxExclBaseSum(exclBase.multiply(qty).setScale(SCALE, ROUNDING_MODE)); + sum.addTaxInclBaseSum(inclBase.multiply(qty).setScale(SCALE, ROUNDING_MODE)); + sum.addTaxExclCompileSum(exclCompile.multiply(qty).setScale(SCALE, ROUNDING_MODE)); + sum.addTaxInclCompileSum(inclCompile.multiply(qty).setScale(SCALE, ROUNDING_MODE)); + } + } + } + } + + log.info("[统一取费分类合价] divisionId={}, categorySums={}", divisionId, result); + return result; + } catch (Exception e) { + log.error("[统一取费分类合价] 计算异常 - divisionId={}", divisionId, e); + return result; + } + } + + /** + * 根据指定的价格字段类型计算统一取费的calcBase值 + */ + private BigDecimal evaluateCalcBaseForUnifiedFeeByField(Map calcBase, + Map categorySums, String priceField) { + if (MapUtil.isEmpty(calcBase)) { + return BigDecimal.ZERO; + } + String formula = (String) calcBase.get("formula"); + if (StrUtil.isBlank(formula)) { + return BigDecimal.ZERO; + } + @SuppressWarnings("unchecked") + Map variables = (Map) calcBase.get("variables"); + if (MapUtil.isEmpty(variables)) { + try { + return new BigDecimal(formula.trim()); + } catch (NumberFormatException e) { + return BigDecimal.ZERO; + } + } + + Map varValues = new HashMap<>(); + for (Map.Entry entry : variables.entrySet()) { + String varName = entry.getKey(); + Object varValue = entry.getValue(); + Long categoryId = null; + if (varValue instanceof Number) { + categoryId = ((Number) varValue).longValue(); + } else if (varValue instanceof Map) { + @SuppressWarnings("unchecked") + Map varInfo = (Map) varValue; + Object categoryIdObj = varInfo.get("categoryId"); + if (categoryIdObj != null) { + categoryId = ((Number) categoryIdObj).longValue(); + } + } + if (categoryId == null) { + varValues.put(varName, BigDecimal.ZERO); + continue; + } + CategoryPriceSum sum = categorySums.get(categoryId); + BigDecimal value = BigDecimal.ZERO; + if (sum != null) { + switch (priceField) { + case "taxExclBaseSum": value = sum.getTaxExclBaseSum(); break; + case "taxInclBaseSum": value = sum.getTaxInclBaseSum(); break; + case "taxExclCompileSum": value = sum.getTaxExclCompileSum(); break; + case "taxInclCompileSum": value = sum.getTaxInclCompileSum(); break; + default: value = sum.getTaxExclCompileSum(); break; + } + } + varValues.put(varName, value != null ? value : BigDecimal.ZERO); + } + return evaluateFormula(formula, varValues); + } + + /** + * 获取母定额的工料机分类合价 + */ + private Map getMotherCategorySums(Long motherDivisionId) { + // 获取母定额的工料机列表 + List wbResources = + wbBoqResourceService.getListByDivisionId(motherDivisionId); + + if (CollUtil.isEmpty(wbResources)) { + // 【已禁用】不再 fallback 到后台数据,直接返回空 + // 计算逻辑已收归工作台,后台不再参与计算 + return new HashMap<>(); + } + + List resourceDetails = convertWbResourcesToResourcePriceDetails(wbResources); + return calculateCategorySums(resourceDetails); + } + + /** + * 从快照表获取统一取费设置(通过sourceId查找) + */ + private com.yhy.module.core.dal.dataobject.workbench.WbUnifiedFeeSettingDO getUnifiedFeeSettingFromSnapshot(Long compileTreeId, Long sourceId) { + try { + List settings = + wbUnifiedFeeSettingMapper.selectByCompileTreeId(compileTreeId); + return settings.stream() + .filter(s -> sourceId.equals(s.getSourceId())) + .findFirst() + .orElse(null); + } catch (Exception e) { + log.warn("[统一取费单价] 从快照获取统一取费设置失败 - compileTreeId={}, sourceId={}", compileTreeId, sourceId, e); + return null; + } + } + + /** + * 从快照表获取统一取费设置的子费用列表 + */ + private List getChildSettingsFromSnapshot(Long compileTreeId, Long parentSnapshotId) { + try { + List settings = + wbUnifiedFeeSettingMapper.selectByCompileTreeId(compileTreeId); + return settings.stream() + .filter(s -> parentSnapshotId.equals(s.getParentId())) + .collect(Collectors.toList()); + } catch (Exception e) { + log.warn("[统一取费单价] 从快照获取子费用列表失败 - compileTreeId={}, parentSnapshotId={}", compileTreeId, parentSnapshotId, e); + return new ArrayList<>(); + } + } + + /** + * 将快照统一取费子目工料机转换为VO格式 + */ + private List convertWbUnifiedFeeResourcesToVO( + List snapshotResources) { + if (CollUtil.isEmpty(snapshotResources)) { + return new ArrayList<>(); + } + return snapshotResources.stream().map(wb -> { + com.yhy.module.core.controller.admin.quota.vo.QuotaUnifiedFeeResourceRespVO vo = + new com.yhy.module.core.controller.admin.quota.vo.QuotaUnifiedFeeResourceRespVO(); + vo.setId(wb.getId()); + vo.setUnifiedFeeSettingId(wb.getUnifiedFeeSettingId()); + vo.setDosage(wb.getEffectiveDosage()); + vo.setSortOrder(wb.getSortOrder()); + // 从attributes中获取额外字段 + Map attrs = wb.getAttributes(); + if (attrs != null) { + if (attrs.get("resourceCode") != null) { + vo.setResourceCode(attrs.get("resourceCode").toString()); + } + if (attrs.get("resourceName") != null) { + vo.setResourceName(attrs.get("resourceName").toString()); + } + if (attrs.get("resourceUnit") != null) { + vo.setResourceUnit(attrs.get("resourceUnit").toString()); + } + if (attrs.get("calcBase") != null) { + @SuppressWarnings("unchecked") + Map calcBase = (Map) attrs.get("calcBase"); + vo.setCalcBase(calcBase); + } + if (attrs.get("resourceTaxExclCompilePrice") != null) { + vo.setResourceTaxExclCompilePrice(new BigDecimal(attrs.get("resourceTaxExclCompilePrice").toString())); + } + } + return vo; + }).collect(Collectors.toList()); + } + + /** + * 检查母定额是否在子费用的取费章节范围内 + * @param motherCatalogItemId 母定额的目录节点ID + * @param feeChapter 子费用的取费章节JSON数组字符串,如 ["2027009068267687937"] + * @return 如果匹配或feeChapter为空则返回true + */ + private boolean isMotherQuotaInFeeChapter(Long motherCatalogItemId, String feeChapter) { + // 如果feeChapter为空,表示不限制,所有母定额都匹配 + if (StrUtil.isBlank(feeChapter) || "[]".equals(feeChapter)) { + return true; + } + + // 如果母定额没有catalogItemId,无法匹配 + if (motherCatalogItemId == null) { + log.warn("[统一取费单价] 母定额缺少sourceCatalogItemId,无法进行取费章节过滤"); + return true; // 默认匹配,避免漏算 + } + + try { + // 解析feeChapter JSON数组 + List chapterIds = cn.hutool.json.JSONUtil.toList(feeChapter, String.class); + if (chapterIds == null || chapterIds.isEmpty()) { + return true; + } + + // 检查母定额的catalogItemId是否在取费章节列表中 + String motherCatalogIdStr = motherCatalogItemId.toString(); + return chapterIds.contains(motherCatalogIdStr); + } catch (Exception e) { + log.warn("[统一取费单价] 解析feeChapter失败: {}", feeChapter, e); + return true; // 解析失败时默认匹配 + } + } + + /** + * 计算统一取费子目工料机的计算基数值 + * 子目工料机的calcBase格式:{"formula": "材", "variables": {"材": 3}} + * 其中 variables 的值是 categoryId + */ + private BigDecimal evaluateCalcBaseForUnifiedFee(Map calcBase, Map categorySums) { + if (MapUtil.isEmpty(calcBase)) { + return BigDecimal.ZERO; + } + + String formula = (String) calcBase.get("formula"); + if (StrUtil.isBlank(formula)) { + return BigDecimal.ZERO; + } + + @SuppressWarnings("unchecked") + Map variables = (Map) calcBase.get("variables"); + if (MapUtil.isEmpty(variables)) { + // 如果没有变量,尝试直接解析为数字 + try { + return new BigDecimal(formula.trim()); + } catch (NumberFormatException e) { + log.warn("[统一取费calcBase] 无变量且非数字公式: {}", formula); + return BigDecimal.ZERO; + } + } + + // 构建变量值映射 + // 统一取费子目工料机的calcBase格式与定额取费不同 + // 格式:{"formula": "材", "variables": {"材": 3}} + // 其中 variables 的值直接是 categoryId(数字) + Map varValues = new HashMap<>(); + for (Map.Entry entry : variables.entrySet()) { + String varName = entry.getKey(); + Object varValue = entry.getValue(); + + Long categoryId = null; + if (varValue instanceof Number) { + // 直接是 categoryId + categoryId = ((Number) varValue).longValue(); + } else if (varValue instanceof Map) { + // 兼容定额取费的格式:{"categoryId": 3, "priceField": "tax_excl_base_price"} + @SuppressWarnings("unchecked") + Map varInfo = (Map) varValue; + Object categoryIdObj = varInfo.get("categoryId"); + if (categoryIdObj != null) { + categoryId = ((Number) categoryIdObj).longValue(); + } + } + + if (categoryId == null) { + varValues.put(varName, BigDecimal.ZERO); + continue; + } + + CategoryPriceSum sum = categorySums.get(categoryId); + BigDecimal value = BigDecimal.ZERO; + if (sum != null) { + // 默认使用除税编制价合价 + value = sum.getTaxExclCompileSum(); + } + + varValues.put(varName, value != null ? value : BigDecimal.ZERO); + log.debug("[统一取费calcBase] 变量 {} -> categoryId={}, value={}", varName, categoryId, value); + } + + // 使用简单的表达式计算(替换变量后计算) + return evaluateFormula(formula, varValues); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/QuotaQtyFormulaServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/QuotaQtyFormulaServiceImpl.java new file mode 100644 index 0000000..f85388a --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/QuotaQtyFormulaServiceImpl.java @@ -0,0 +1,240 @@ +package com.yhy.module.core.service.workbench.impl; + +import com.yhy.module.core.dal.dataobject.workbench.WbBoqDivisionDO; +import com.yhy.module.core.dal.mysql.workbench.WbBoqDivisionMapper; +import com.yhy.module.core.service.workbench.QuotaQtyFormulaService; +import com.yhy.module.core.util.FormulaEvaluator; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +/** + * 定额工程量公式计算服务实现 + * + * @author yhy + */ +@Service +@Slf4j +public class QuotaQtyFormulaServiceImpl implements QuotaQtyFormulaService { + + /** + * 公式合法性正则:只允许 QDL、数字、+-* /()、空格、小数点 + */ + private static final Pattern FORMULA_PATTERN = Pattern.compile("^[QDL0-9+\\-*/().\\s]+$"); + + /** + * 工程量小数位数 + */ + private static final int QTY_SCALE = 3; + + @Resource + private WbBoqDivisionMapper wbBoqDivisionMapper; + + @Override + public FormulaValidationResult validateFormula(String formula) { + if (!StringUtils.hasText(formula)) { + return FormulaValidationResult.failure("公式不能为空"); + } + + String trimmedFormula = formula.trim(); + + // 检查公式格式 + if (!FORMULA_PATTERN.matcher(trimmedFormula).matches()) { + return FormulaValidationResult.failure("公式格式不正确,只允许使用 QDL、数字和运算符(+-*/)"); + } + + // 尝试用测试值计算 + try { + BigDecimal testResult = calculateQuotaQty(trimmedFormula, BigDecimal.valueOf(100)); + return FormulaValidationResult.success(testResult); + } catch (Exception e) { + return FormulaValidationResult.failure("公式计算失败: " + e.getMessage()); + } + } + + @Override + public BigDecimal calculateQuotaQty(String formula, BigDecimal qdlValue) { + return calculateQuotaQty(formula, qdlValue, null); + } + + @Override + public BigDecimal calculateQuotaQty(String formula, BigDecimal qdlValue, String quotaUnit) { + if (!StringUtils.hasText(formula)) { + return BigDecimal.ZERO; + } + + String trimmedFormula = formula.trim(); + + // 解析单位换算系数(提前解析,纯数字也需要换算) + BigDecimal unitFactor = parseUnitFactor(quotaUnit); + + // 如果公式是纯数字,进行单位换算后返回 + // 例如:输入9,单位100m,结果为 9/100 = 0.090 + try { + BigDecimal directValue = new BigDecimal(trimmedFormula); + // 单位带数值时,需要除以换算系数 + if (unitFactor.compareTo(BigDecimal.ONE) != 0 && unitFactor.compareTo(BigDecimal.ZERO) != 0) { + directValue = directValue.divide(unitFactor, QTY_SCALE, RoundingMode.HALF_UP); + } + return directValue.setScale(QTY_SCALE, RoundingMode.HALF_UP); + } catch (NumberFormatException ignored) { + // 不是纯数字,继续解析公式 + } + + // QDL 使用原始清单工程量(不预先换算) + BigDecimal originalQdlValue = qdlValue != null ? qdlValue : BigDecimal.ZERO; + + // 如果公式只是 QDL,直接返回换算后的工程量 + if ("QDL".equalsIgnoreCase(trimmedFormula)) { + // 单位带数值时,结果 = 清单工程量 / 单位系数 + if (unitFactor.compareTo(BigDecimal.ONE) != 0 && unitFactor.compareTo(BigDecimal.ZERO) != 0) { + return originalQdlValue.divide(unitFactor, QTY_SCALE, RoundingMode.HALF_UP); + } + return originalQdlValue.setScale(QTY_SCALE, RoundingMode.HALF_UP); + } + + // 替换 QDL 变量并计算(使用原始清单工程量) + Map variables = new HashMap<>(); + variables.put("QDL", originalQdlValue); + + BigDecimal result = FormulaEvaluator.evaluate(trimmedFormula, variables); + + // 公式计算完成后,再进行单位换算 + // 例如:QDL*(20+95)+6 = 9*115+6 = 1041,然后 1041/100 = 10.41 + if (unitFactor.compareTo(BigDecimal.ONE) != 0 && unitFactor.compareTo(BigDecimal.ZERO) != 0) { + result = result.divide(unitFactor, QTY_SCALE, RoundingMode.HALF_UP); + } + return result.setScale(QTY_SCALE, RoundingMode.HALF_UP); + } + + @Override + public BigDecimal parseUnitFactor(String unit) { + if (!StringUtils.hasText(unit)) { + return BigDecimal.ONE; + } + + String trimmedUnit = unit.trim(); + + // 使用正则匹配单位开头的数字(支持整数和小数) + java.util.regex.Matcher matcher = Pattern.compile("^(\\d+\\.?\\d*)").matcher(trimmedUnit); + if (matcher.find()) { + try { + return new BigDecimal(matcher.group(1)); + } catch (NumberFormatException e) { + log.warn("解析单位系数失败: {}", unit); + return BigDecimal.ONE; + } + } + + // 没有数字前缀,返回1(不换算) + return BigDecimal.ONE; + } + + @Override + public BigDecimal recalculateQuotaQty(Long quotaDivisionId) { + WbBoqDivisionDO quota = wbBoqDivisionMapper.selectById(quotaDivisionId); + if (quota == null) { + log.warn("定额节点不存在: {}", quotaDivisionId); + return BigDecimal.ZERO; + } + + if (!WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(quota.getNodeType())) { + log.warn("节点不是定额类型: {}", quotaDivisionId); + return quota.getQty(); + } + + // 获取父清单的工程量 + BigDecimal qdlValue = getParentBoqQty(quota.getParentId()); + + // 计算定额工程量 + String formula = quota.getQuotaQtyFormula(); + if (!StringUtils.hasText(formula)) { + // 没有公式,保持原值 + return quota.getQty(); + } + + // 带单位换算计算工程量 + BigDecimal newQty = calculateQuotaQty(formula, qdlValue, quota.getUnit()); + + // 更新数据库 + WbBoqDivisionDO updateDO = new WbBoqDivisionDO(); + updateDO.setId(quotaDivisionId); + updateDO.setQty(newQty); + wbBoqDivisionMapper.updateById(updateDO); + + return newQty; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void recalculateChildQuotaQty(Long boqDivisionId) { + WbBoqDivisionDO boq = wbBoqDivisionMapper.selectById(boqDivisionId); + if (boq == null) { + log.warn("清单节点不存在: {}", boqDivisionId); + return; + } + + if (!WbBoqDivisionDO.NODE_TYPE_BOQ.equals(boq.getNodeType())) { + log.warn("节点不是清单类型: {}", boqDivisionId); + return; + } + + BigDecimal qdlValue = boq.getQty(); + + // 查询所有子定额 + List quotas = wbBoqDivisionMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(WbBoqDivisionDO::getParentId, boqDivisionId) + .eq(WbBoqDivisionDO::getNodeType, WbBoqDivisionDO.NODE_TYPE_QUOTA) + ); + + for (WbBoqDivisionDO quota : quotas) { + String formula = quota.getQuotaQtyFormula(); + if (!StringUtils.hasText(formula)) { + continue; + } + + // 带单位换算计算工程量 + BigDecimal newQty = calculateQuotaQty(formula, qdlValue, quota.getUnit()); + + // 只有值变化时才更新 + if (quota.getQty() == null || quota.getQty().compareTo(newQty) != 0) { + WbBoqDivisionDO updateDO = new WbBoqDivisionDO(); + updateDO.setId(quota.getId()); + updateDO.setQty(newQty); + wbBoqDivisionMapper.updateById(updateDO); + log.debug("更新定额工程量: {} -> {}", quota.getId(), newQty); + } + } + } + + /** + * 获取父清单的工程量 + */ + private BigDecimal getParentBoqQty(Long parentId) { + if (parentId == null) { + return BigDecimal.ZERO; + } + + WbBoqDivisionDO parent = wbBoqDivisionMapper.selectById(parentId); + if (parent == null) { + return BigDecimal.ZERO; + } + + // 如果父节点是清单,返回其工程量 + if (WbBoqDivisionDO.NODE_TYPE_BOQ.equals(parent.getNodeType())) { + return parent.getQty() != null ? parent.getQty() : BigDecimal.ZERO; + } + + // 如果父节点不是清单,继续向上查找 + return getParentBoqQty(parent.getParentId()); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/SyncLibraryServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/SyncLibraryServiceImpl.java new file mode 100644 index 0000000..3386834 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/SyncLibraryServiceImpl.java @@ -0,0 +1,742 @@ +package com.yhy.module.core.service.workbench.impl; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yhy.module.core.enums.ErrorCodeConstants.SYNC_ALREADY_SYNCED; +import static com.yhy.module.core.enums.ErrorCodeConstants.SYNC_BOQ_NO_QUOTA; +import static com.yhy.module.core.enums.ErrorCodeConstants.SYNC_COMPILE_TREE_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.SYNC_DIVISION_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.SYNC_LIBRARY_DIVISION_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.SYNC_NOT_SYNCED; +import static com.yhy.module.core.enums.ErrorCodeConstants.SYNC_ONLY_BOQ_OR_DIVISION; +import static com.yhy.module.core.enums.ErrorCodeConstants.SYNC_UNIFIED_FEE_NOT_ALLOWED; + +import cn.hutool.core.collection.CollUtil; +import com.yhy.module.core.controller.admin.workbench.vo.sync.ApplySyncReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.sync.SetSyncReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.sync.SyncLibraryDivisionRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.sync.SyncLibraryDivisionUpdateReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.sync.SyncPendingRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.sync.SyncSourceUnitRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.sync.UnsetSyncReqVO; +import com.yhy.module.core.dal.dataobject.workbench.SyncBindingDO; +import com.yhy.module.core.dal.dataobject.workbench.SyncLibraryDO; +import com.yhy.module.core.dal.dataobject.workbench.SyncLibraryDivisionDO; +import com.yhy.module.core.dal.dataobject.workbench.SyncLibraryMarketMaterialDO; +import com.yhy.module.core.dal.dataobject.workbench.SyncLibraryResourceDO; +import com.yhy.module.core.dal.dataobject.workbench.WbBoqDivisionDO; +import com.yhy.module.core.dal.dataobject.workbench.WbBoqMarketMaterialDO; +import com.yhy.module.core.dal.dataobject.workbench.WbBoqResourceDO; +import com.yhy.module.core.dal.dataobject.workbench.WbCompileTreeDO; +import com.yhy.module.core.dal.mysql.workbench.SyncBindingMapper; +import com.yhy.module.core.dal.mysql.workbench.SyncLibraryDivisionMapper; +import com.yhy.module.core.dal.mysql.workbench.SyncLibraryMapper; +import com.yhy.module.core.dal.mysql.workbench.SyncLibraryMarketMaterialMapper; +import com.yhy.module.core.dal.mysql.workbench.SyncLibraryResourceMapper; +import com.yhy.module.core.dal.mysql.workbench.WbBoqDivisionMapper; +import com.yhy.module.core.dal.mysql.workbench.WbBoqMarketMaterialMapper; +import com.yhy.module.core.dal.mysql.workbench.WbBoqResourceMapper; +import com.yhy.module.core.dal.mysql.workbench.WbCompileTreeMapper; +import com.yhy.module.core.service.workbench.SyncLibraryService; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * 同步库 Service 实现 + * + * @author yhy + */ +@Service +@Validated +@Slf4j +public class SyncLibraryServiceImpl implements SyncLibraryService { + + @Resource + private SyncLibraryMapper syncLibraryMapper; + @Resource + private SyncLibraryDivisionMapper syncLibraryDivisionMapper; + @Resource + private SyncLibraryResourceMapper syncLibraryResourceMapper; + @Resource + private SyncLibraryMarketMaterialMapper syncLibraryMarketMaterialMapper; + @Resource + private SyncBindingMapper syncBindingMapper; + @Resource + private WbBoqDivisionMapper wbBoqDivisionMapper; + @Resource + private WbBoqResourceMapper wbBoqResourceMapper; + @Resource + private WbBoqMarketMaterialMapper wbBoqMarketMaterialMapper; + @Resource + private WbCompileTreeMapper wbCompileTreeMapper; + + @Override + public List getTree(Long projectId) { + // 查询项目下所有同步库 + List libraries = syncLibraryMapper.selectListByProjectId(projectId); + if (CollUtil.isEmpty(libraries)) { + return Collections.emptyList(); + } + // 收集所有同步库ID,批量查询所有分部分项节点 + List syncLibraryIds = libraries.stream() + .map(SyncLibraryDO::getId) + .collect(Collectors.toList()); + List allNodes = syncLibraryDivisionMapper.selectListBySyncLibraryIds(syncLibraryIds); + if (CollUtil.isEmpty(allNodes)) { + return Collections.emptyList(); + } + // 构建树结构(合并所有同步库的节点) + Map> childrenMap = allNodes.stream() + .filter(n -> n.getParentId() != null) + .collect(Collectors.groupingBy(SyncLibraryDivisionDO::getParentId)); + // 找到根节点(parentId为null的) + List roots = allNodes.stream() + .filter(n -> n.getParentId() == null) + .collect(Collectors.toList()); + return roots.stream() + .map(root -> buildTreeNode(root, childrenMap)) + .collect(Collectors.toList()); + } + + @Override + public List getSourceUnits(Long projectId) { + // 查询项目下所有同步库 + List libraries = syncLibraryMapper.selectListByProjectId(projectId); + if (CollUtil.isEmpty(libraries)) { + return Collections.emptyList(); + } + // 收集所有同步库ID,批量查询绑定关系 + List syncLibraryIds = libraries.stream() + .map(SyncLibraryDO::getId) + .collect(Collectors.toList()); + List bindings = syncBindingMapper.selectListBySyncLibraryIds(syncLibraryIds); + return bindings.stream().map(binding -> { + SyncSourceUnitRespVO vo = new SyncSourceUnitRespVO(); + vo.setBindingId(String.valueOf(binding.getId())); + vo.setDivisionId(String.valueOf(binding.getDivisionId())); + vo.setCompileTreeId(String.valueOf(binding.getCompileTreeId())); + vo.setSyncedVersion(binding.getSyncedVersion()); + // 获取单位工程名称 + WbCompileTreeDO compileTree = wbCompileTreeMapper.selectById(binding.getCompileTreeId()); + if (compileTree != null) { + vo.setCompileTreeName(compileTree.getName()); + } + return vo; + }).collect(Collectors.toList()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateDivision(SyncLibraryDivisionUpdateReqVO reqVO) { + SyncLibraryDivisionDO node = syncLibraryDivisionMapper.selectById(reqVO.getId()); + if (node == null) { + throw exception(SYNC_LIBRARY_DIVISION_NOT_EXISTS); + } + // 更新节点字段 + SyncLibraryDivisionDO updateDO = new SyncLibraryDivisionDO(); + updateDO.setId(node.getId()); + if (reqVO.getCode() != null) updateDO.setCode(reqVO.getCode()); + if (reqVO.getName() != null) updateDO.setName(reqVO.getName()); + if (reqVO.getFeature() != null) updateDO.setFeature(reqVO.getFeature()); + if (reqVO.getUnit() != null) updateDO.setUnit(reqVO.getUnit()); + if (reqVO.getQty() != null) updateDO.setQty(reqVO.getQty()); + if (reqVO.getRate() != null) updateDO.setRate(reqVO.getRate()); + if (reqVO.getCostCode() != null) updateDO.setCostCode(reqVO.getCostCode()); + if (reqVO.getQuotaQtyFormula() != null) updateDO.setQuotaQtyFormula(reqVO.getQuotaQtyFormula()); + if (reqVO.getRemark() != null) updateDO.setRemark(reqVO.getRemark()); + if (reqVO.getAttributes() != null) updateDO.setAttributes(reqVO.getAttributes()); + syncLibraryDivisionMapper.updateById(updateDO); + + // 同步库版本号+1 + SyncLibraryDO library = syncLibraryMapper.selectById(node.getSyncLibraryId()); + if (library != null) { + SyncLibraryDO updateLib = new SyncLibraryDO(); + updateLib.setId(library.getId()); + updateLib.setVersion(library.getVersion() + 1); + syncLibraryMapper.updateById(updateLib); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void setSync(SetSyncReqVO reqVO) { + WbBoqDivisionDO division = wbBoqDivisionMapper.selectById(reqVO.getDivisionId()); + if (division == null) { + throw exception(SYNC_DIVISION_NOT_EXISTS); + } + // 校验节点类型 + if (!WbBoqDivisionDO.NODE_TYPE_BOQ.equals(division.getNodeType()) + && !WbBoqDivisionDO.NODE_TYPE_DIVISION.equals(division.getNodeType())) { + throw exception(SYNC_ONLY_BOQ_OR_DIVISION); + } + // 校验是否已同步 + if (division.getSyncLibraryId() != null) { + throw exception(SYNC_ALREADY_SYNCED); + } + // 清单节点必须有子定额 + if (WbBoqDivisionDO.NODE_TYPE_BOQ.equals(division.getNodeType())) { + Long childCount = wbBoqDivisionMapper.selectCountByParentId(division.getId()); + if (childCount == 0) { + throw exception(SYNC_BOQ_NO_QUOTA); + } + } + // 排除统一取费节点 + if (WbBoqDivisionDO.NODE_TYPE_UNIFIED_FEE.equals(division.getNodeType())) { + throw exception(SYNC_UNIFIED_FEE_NOT_ALLOWED); + } + + // 获取项目ID + WbCompileTreeDO compileTree = wbCompileTreeMapper.selectById(division.getCompileTreeId()); + if (compileTree == null) { + throw exception(SYNC_COMPILE_TREE_NOT_EXISTS); + } + + // 1. 创建同步库主表 + SyncLibraryDO syncLibrary = SyncLibraryDO.builder() + .tenantId(division.getTenantId()) + .projectId(compileTree.getProjectId()) + .sourceDivisionId(division.getId()) + .sourceCompileTreeId(division.getCompileTreeId()) + .nodeType(division.getNodeType()) + .code(division.getCode()) + .name(division.getName()) + .feature(division.getFeature()) + .unit(division.getUnit()) + .qty(division.getQty()) + .rate(division.getRate()) + .attributes(division.getAttributes()) + .version(1) + .build(); + syncLibraryMapper.insert(syncLibrary); + + // 2. 复制分部分项树到同步库(含子节点) + List allNodes = new ArrayList<>(); + allNodes.add(division); + collectChildNodes(division.getId(), allNodes); + + // 过滤掉统一取费节点 + allNodes = allNodes.stream() + .filter(n -> !WbBoqDivisionDO.NODE_TYPE_UNIFIED_FEE.equals(n.getNodeType())) + .collect(Collectors.toList()); + + // ID映射:原ID → 新ID + Map idMapping = new HashMap<>(); + for (WbBoqDivisionDO node : allNodes) { + SyncLibraryDivisionDO syncDiv = SyncLibraryDivisionDO.builder() + .tenantId(node.getTenantId()) + .syncLibraryId(syncLibrary.getId()) + .nodeType(node.getNodeType()) + .sourceType(node.getSourceType()) + .sourceBoqCatalogId(node.getSourceBoqCatalogId()) + .sourceBoqItemTreeId(node.getSourceBoqItemTreeId()) + .sourceQuotaItemId(node.getSourceQuotaItemId()) + .code(node.getCode()) + .name(node.getName()) + .feature(node.getFeature()) + .unit(node.getUnit()) + .qty(node.getQty()) + .rate(node.getRate()) + .costCode(node.getCostCode()) + .quotaQtyFormula(node.getQuotaQtyFormula()) + .sortOrder(node.getSortOrder()) + .attributes(node.getAttributes()) + .remark(node.getRemark()) + .build(); + syncLibraryDivisionMapper.insert(syncDiv); + idMapping.put(node.getId(), syncDiv.getId()); + } + + // 更新父节点ID映射 + for (WbBoqDivisionDO node : allNodes) { + if (node.getParentId() != null && idMapping.containsKey(node.getParentId())) { + Long newId = idMapping.get(node.getId()); + Long newParentId = idMapping.get(node.getParentId()); + SyncLibraryDivisionDO updateParent = new SyncLibraryDivisionDO(); + updateParent.setId(newId); + updateParent.setParentId(newParentId); + syncLibraryDivisionMapper.updateById(updateParent); + } + // 根节点(即被同步的节点本身)parentId设为null + } + + // 3. 复制工料机到同步库 + List quotaNodeIds = allNodes.stream() + .filter(n -> WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(n.getNodeType())) + .map(WbBoqDivisionDO::getId) + .collect(Collectors.toList()); + if (CollUtil.isNotEmpty(quotaNodeIds)) { + List resources = wbBoqResourceMapper.selectListByDivisionIds(quotaNodeIds); + // 工料机ID映射(用于复合工料机的parentId映射) + Map resIdMapping = new HashMap<>(); + for (WbBoqResourceDO res : resources) { + Long newDivisionId = idMapping.get(res.getDivisionId()); + if (newDivisionId == null) continue; + SyncLibraryResourceDO syncRes = SyncLibraryResourceDO.builder() + .tenantId(res.getTenantId()) + .syncDivisionId(newDivisionId) + .sourceResourceItemId(res.getSourceResourceItemId()) + .sourceQuotaResourceId(res.getSourceQuotaResourceId()) + .resourceType(res.getResourceType()) + .code(res.getCode()) + .name(res.getName()) + .spec(res.getSpec()) + .unit(res.getUnit()) + .consumeQty(res.getConsumeQty()) + .adjustConsumeQty(res.getAdjustConsumeQty()) + .adjustRate(res.getAdjustRate()) + .baseConsumeQty(res.getBaseConsumeQty()) + .baseBasePrice(res.getBaseBasePrice()) + .usageQty(res.getUsageQty()) + .taxRate(res.getTaxRate()) + .taxExclBasePrice(res.getTaxExclBasePrice()) + .taxInclBasePrice(res.getTaxInclBasePrice()) + .taxExclCompilePrice(res.getTaxExclCompilePrice()) + .taxInclCompilePrice(res.getTaxInclCompilePrice()) + .categoryId(res.getCategoryId()) + .sourceType(res.getSourceType()) + .sortOrder(res.getSortOrder()) + .attributes(res.getAttributes()) + .snapshotJson(res.getSnapshotJson()) + .build(); + syncLibraryResourceMapper.insert(syncRes); + resIdMapping.put(res.getId(), syncRes.getId()); + } + // 更新复合工料机的parentId + for (WbBoqResourceDO res : resources) { + if (res.getParentId() != null && resIdMapping.containsKey(res.getParentId())) { + Long newResId = resIdMapping.get(res.getId()); + Long newParentResId = resIdMapping.get(res.getParentId()); + if (newResId != null && newParentResId != null) { + SyncLibraryResourceDO updateRes = new SyncLibraryResourceDO(); + updateRes.setId(newResId); + updateRes.setParentId(newParentResId); + syncLibraryResourceMapper.updateById(updateRes); + } + } + } + + // 4. 复制市场主材设备到同步库 + List marketMaterials = wbBoqMarketMaterialMapper.selectListByDivisionIds(quotaNodeIds); + for (WbBoqMarketMaterialDO mm : marketMaterials) { + Long newDivisionId = idMapping.get(mm.getDivisionId()); + if (newDivisionId == null) continue; + SyncLibraryMarketMaterialDO syncMm = SyncLibraryMarketMaterialDO.builder() + .tenantId(mm.getTenantId()) + .syncDivisionId(newDivisionId) + .sourceResourceItemId(mm.getSourceResourceItemId()) + .sourceMarketMaterialId(mm.getSourceMarketMaterialId()) + .resourceType(mm.getResourceType()) + .code(mm.getCode()) + .name(mm.getName()) + .spec(mm.getSpec()) + .unit(mm.getUnit()) + .consumeQty(mm.getConsumeQty()) + .adjustConsumeQty(mm.getAdjustConsumeQty()) + .taxRate(mm.getTaxRate()) + .taxExclBasePrice(mm.getTaxExclBasePrice()) + .taxInclBasePrice(mm.getTaxInclBasePrice()) + .taxExclCompilePrice(mm.getTaxExclCompilePrice()) + .taxInclCompilePrice(mm.getTaxInclCompilePrice()) + .adjustRate(mm.getAdjustRate()) + .categoryId(mm.getCategoryId()) + .sourceType(mm.getSourceType()) + .baseConsumeQty(mm.getBaseConsumeQty()) + .baseBasePrice(mm.getBaseBasePrice()) + .snapshotJson(mm.getSnapshotJson()) + .usageQty(mm.getUsageQty()) + .sortOrder(mm.getSortOrder()) + .attributes(mm.getAttributes()) + .build(); + syncLibraryMarketMaterialMapper.insert(syncMm); + } + } + + // 5. 更新原节点同步状态 + WbBoqDivisionDO updateDiv = new WbBoqDivisionDO(); + updateDiv.setId(division.getId()); + updateDiv.setSyncLibraryId(syncLibrary.getId()); + updateDiv.setIsSyncSource(true); + wbBoqDivisionMapper.updateById(updateDiv); + + // 6. 创建绑定关系 + SyncBindingDO binding = SyncBindingDO.builder() + .tenantId(division.getTenantId()) + .syncLibraryId(syncLibrary.getId()) + .divisionId(division.getId()) + .compileTreeId(division.getCompileTreeId()) + .syncedVersion(1) + .build(); + syncBindingMapper.insert(binding); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void unsetSync(UnsetSyncReqVO reqVO) { + WbBoqDivisionDO division = wbBoqDivisionMapper.selectById(reqVO.getDivisionId()); + if (division == null) { + throw exception(SYNC_DIVISION_NOT_EXISTS); + } + if (division.getSyncLibraryId() == null) { + throw exception(SYNC_NOT_SYNCED); + } + + Long syncLibraryId = division.getSyncLibraryId(); + + // 1. 删除绑定关系 + syncBindingMapper.deleteByDivisionId(division.getId()); + + // 2. 更新原节点:清除同步状态(使用 UpdateWrapper 显式设置 null) + wbBoqDivisionMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper() + .eq(WbBoqDivisionDO::getId, division.getId()) + .set(WbBoqDivisionDO::getSyncLibraryId, null) + .set(WbBoqDivisionDO::getIsSyncSource, false)); + + // 3. 如果该同步库没有其他绑定,删除同步库 + Long bindingCount = syncBindingMapper.selectCountBySyncLibraryId(syncLibraryId); + if (bindingCount == 0) { + deleteSyncLibraryInternal(syncLibraryId); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void applySync(ApplySyncReqVO reqVO) { + List bindings = syncBindingMapper.selectListByCompileTreeId(reqVO.getCompileTreeId()); + if (CollUtil.isEmpty(bindings)) { + return; + } + + for (SyncBindingDO binding : bindings) { + SyncLibraryDO library = syncLibraryMapper.selectById(binding.getSyncLibraryId()); + if (library == null) continue; + + // 版本一致则跳过 + if (Objects.equals(binding.getSyncedVersion(), library.getVersion())) { + continue; + } + + WbBoqDivisionDO targetDivision = wbBoqDivisionMapper.selectById(binding.getDivisionId()); + if (targetDivision == null) continue; + + // 获取同步库的分部分项树 + List syncNodes = syncLibraryDivisionMapper.selectListBySyncLibraryId(library.getId()); + + // 删除原节点下的子节点(定额及其工料机) + deleteChildNodesAndResources(targetDivision.getId()); + + // 从同步库复制最新数据 + copyFromSyncLibrary(syncNodes, targetDivision, library); + + // 更新绑定的已同步版本 + SyncBindingDO updateBinding = new SyncBindingDO(); + updateBinding.setId(binding.getId()); + updateBinding.setSyncedVersion(library.getVersion()); + syncBindingMapper.updateById(updateBinding); + } + } + + @Override + public List checkPending(Long compileTreeId) { + List bindings = syncBindingMapper.selectListByCompileTreeId(compileTreeId); + if (CollUtil.isEmpty(bindings)) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + for (SyncBindingDO binding : bindings) { + SyncLibraryDO library = syncLibraryMapper.selectById(binding.getSyncLibraryId()); + if (library == null) continue; + + if (!Objects.equals(binding.getSyncedVersion(), library.getVersion())) { + SyncPendingRespVO vo = new SyncPendingRespVO(); + vo.setSyncLibraryId(String.valueOf(library.getId())); + vo.setSyncLibraryName(library.getName()); + vo.setSyncLibraryCode(library.getCode()); + vo.setCurrentVersion(library.getVersion()); + vo.setSyncedVersion(binding.getSyncedVersion()); + result.add(vo); + } + } + return result; + } + + + // ==================== 私有方法 ==================== + + /** + * 递归收集子节点 + */ + private void collectChildNodes(Long parentId, List result) { + List children = wbBoqDivisionMapper.selectListByParentId(parentId); + for (WbBoqDivisionDO child : children) { + result.add(child); + collectChildNodes(child.getId(), result); + } + } + + /** + * 构建树节点 + */ + private SyncLibraryDivisionRespVO buildTreeNode(SyncLibraryDivisionDO node, + Map> childrenMap) { + SyncLibraryDivisionRespVO vo = new SyncLibraryDivisionRespVO(); + vo.setId(String.valueOf(node.getId())); + vo.setSyncLibraryId(String.valueOf(node.getSyncLibraryId())); + vo.setParentId(node.getParentId() != null ? String.valueOf(node.getParentId()) : null); + vo.setNodeType(node.getNodeType()); + vo.setCode(node.getCode()); + vo.setName(node.getName()); + vo.setFeature(node.getFeature()); + vo.setUnit(node.getUnit()); + vo.setQty(node.getQty()); + vo.setRate(node.getRate()); + vo.setCostCode(node.getCostCode()); + vo.setQuotaQtyFormula(node.getQuotaQtyFormula()); + vo.setSortOrder(node.getSortOrder()); + vo.setAttributes(node.getAttributes()); + vo.setRemark(node.getRemark()); + + List children = childrenMap.get(node.getId()); + if (CollUtil.isNotEmpty(children)) { + vo.setChildren(children.stream() + .map(child -> buildTreeNode(child, childrenMap)) + .collect(Collectors.toList())); + } + return vo; + } + + /** + * 删除子节点及其工料机 + */ + private void deleteChildNodesAndResources(Long parentDivisionId) { + List children = wbBoqDivisionMapper.selectListByParentId(parentDivisionId); + if (CollUtil.isEmpty(children)) return; + + for (WbBoqDivisionDO child : children) { + // 递归删除子节点 + deleteChildNodesAndResources(child.getId()); + // 删除工料机 + wbBoqResourceMapper.deleteByDivisionId(child.getId()); + // 删除市场主材 + wbBoqMarketMaterialMapper.deleteByDivisionId(child.getId()); + // 删除节点本身 + wbBoqDivisionMapper.deleteById(child.getId()); + } + } + + /** + * 从同步库复制数据到工作台 + */ + private void copyFromSyncLibrary(List syncNodes, + WbBoqDivisionDO targetDivision, + SyncLibraryDO library) { + if (CollUtil.isEmpty(syncNodes)) return; + + // 找到同步库中的根节点(parentId为null的) + List rootNodes = syncNodes.stream() + .filter(n -> n.getParentId() == null) + .collect(Collectors.toList()); + + // 同步库根节点的子节点才需要复制(根节点本身对应targetDivision) + Map> childrenMap = syncNodes.stream() + .filter(n -> n.getParentId() != null) + .collect(Collectors.groupingBy(SyncLibraryDivisionDO::getParentId)); + + // 更新目标节点的基本信息 + WbBoqDivisionDO updateTarget = new WbBoqDivisionDO(); + updateTarget.setId(targetDivision.getId()); + if (CollUtil.isNotEmpty(rootNodes)) { + SyncLibraryDivisionDO rootNode = rootNodes.get(0); + updateTarget.setCode(rootNode.getCode()); + updateTarget.setName(rootNode.getName()); + updateTarget.setFeature(rootNode.getFeature()); + updateTarget.setUnit(rootNode.getUnit()); + updateTarget.setQty(rootNode.getQty()); + updateTarget.setRate(rootNode.getRate()); + } + wbBoqDivisionMapper.updateById(updateTarget); + + // 复制子节点 + for (SyncLibraryDivisionDO rootNode : rootNodes) { + List children = childrenMap.get(rootNode.getId()); + if (CollUtil.isNotEmpty(children)) { + copySyncChildren(children, targetDivision.getId(), targetDivision.getCompileTreeId(), + targetDivision.getTenantId(), childrenMap, syncNodes); + } + } + } + + /** + * 递归复制同步库子节点到工作台 + */ + private void copySyncChildren(List children, Long parentId, + Long compileTreeId, Long tenantId, + Map> childrenMap, + List allSyncNodes) { + for (SyncLibraryDivisionDO syncChild : children) { + // 创建工作台节点 + WbBoqDivisionDO newNode = WbBoqDivisionDO.builder() + .tenantId(tenantId) + .compileTreeId(compileTreeId) + .parentId(parentId) + .nodeType(syncChild.getNodeType()) + .sourceType(syncChild.getSourceType()) + .sourceBoqCatalogId(syncChild.getSourceBoqCatalogId()) + .sourceBoqItemTreeId(syncChild.getSourceBoqItemTreeId()) + .sourceQuotaItemId(syncChild.getSourceQuotaItemId()) + .code(syncChild.getCode()) + .name(syncChild.getName()) + .feature(syncChild.getFeature()) + .unit(syncChild.getUnit()) + .qty(syncChild.getQty()) + .rate(syncChild.getRate()) + .costCode(syncChild.getCostCode()) + .quotaQtyFormula(syncChild.getQuotaQtyFormula()) + .sortOrder(syncChild.getSortOrder()) + .attributes(syncChild.getAttributes()) + .remark(syncChild.getRemark()) + .build(); + wbBoqDivisionMapper.insert(newNode); + + // 如果是定额节点,复制工料机和市场主材 + if (WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(syncChild.getNodeType())) { + copySyncResources(syncChild.getId(), newNode.getId(), tenantId); + copySyncMarketMaterials(syncChild.getId(), newNode.getId(), tenantId); + } + + // 递归复制子节点 + List grandChildren = childrenMap.get(syncChild.getId()); + if (CollUtil.isNotEmpty(grandChildren)) { + copySyncChildren(grandChildren, newNode.getId(), compileTreeId, tenantId, + childrenMap, allSyncNodes); + } + } + } + + /** + * 从同步库复制工料机到工作台 + */ + private void copySyncResources(Long syncDivisionId, Long targetDivisionId, Long tenantId) { + List syncResources = syncLibraryResourceMapper.selectListBySyncDivisionId(syncDivisionId); + if (CollUtil.isEmpty(syncResources)) return; + + Map resIdMapping = new HashMap<>(); + for (SyncLibraryResourceDO syncRes : syncResources) { + WbBoqResourceDO newRes = WbBoqResourceDO.builder() + .tenantId(tenantId) + .divisionId(targetDivisionId) + .sourceResourceItemId(syncRes.getSourceResourceItemId()) + .sourceQuotaResourceId(syncRes.getSourceQuotaResourceId()) + .resourceType(syncRes.getResourceType()) + .code(syncRes.getCode()) + .name(syncRes.getName()) + .spec(syncRes.getSpec()) + .unit(syncRes.getUnit()) + .consumeQty(syncRes.getConsumeQty()) + .adjustConsumeQty(syncRes.getAdjustConsumeQty()) + .adjustRate(syncRes.getAdjustRate()) + .baseConsumeQty(syncRes.getBaseConsumeQty()) + .baseBasePrice(syncRes.getBaseBasePrice()) + .usageQty(syncRes.getUsageQty()) + .taxRate(syncRes.getTaxRate()) + .taxExclBasePrice(syncRes.getTaxExclBasePrice()) + .taxInclBasePrice(syncRes.getTaxInclBasePrice()) + .taxExclCompilePrice(syncRes.getTaxExclCompilePrice()) + .taxInclCompilePrice(syncRes.getTaxInclCompilePrice()) + .categoryId(syncRes.getCategoryId()) + .sourceType(syncRes.getSourceType()) + .sortOrder(syncRes.getSortOrder()) + .attributes(syncRes.getAttributes()) + .snapshotJson(syncRes.getSnapshotJson()) + .build(); + wbBoqResourceMapper.insert(newRes); + resIdMapping.put(syncRes.getId(), newRes.getId()); + } + // 更新复合工料机parentId + for (SyncLibraryResourceDO syncRes : syncResources) { + if (syncRes.getParentId() != null && resIdMapping.containsKey(syncRes.getParentId())) { + Long newResId = resIdMapping.get(syncRes.getId()); + Long newParentId = resIdMapping.get(syncRes.getParentId()); + if (newResId != null && newParentId != null) { + WbBoqResourceDO updateRes = new WbBoqResourceDO(); + updateRes.setId(newResId); + updateRes.setParentId(newParentId); + wbBoqResourceMapper.updateById(updateRes); + } + } + } + } + + /** + * 从同步库复制市场主材到工作台 + */ + private void copySyncMarketMaterials(Long syncDivisionId, Long targetDivisionId, Long tenantId) { + List syncMaterials = + syncLibraryMarketMaterialMapper.selectListBySyncDivisionId(syncDivisionId); + if (CollUtil.isEmpty(syncMaterials)) return; + + for (SyncLibraryMarketMaterialDO syncMm : syncMaterials) { + WbBoqMarketMaterialDO newMm = WbBoqMarketMaterialDO.builder() + .tenantId(tenantId) + .divisionId(targetDivisionId) + .sourceResourceItemId(syncMm.getSourceResourceItemId()) + .sourceMarketMaterialId(syncMm.getSourceMarketMaterialId()) + .resourceType(syncMm.getResourceType()) + .code(syncMm.getCode()) + .name(syncMm.getName()) + .spec(syncMm.getSpec()) + .unit(syncMm.getUnit()) + .consumeQty(syncMm.getConsumeQty()) + .adjustConsumeQty(syncMm.getAdjustConsumeQty()) + .taxRate(syncMm.getTaxRate()) + .taxExclBasePrice(syncMm.getTaxExclBasePrice()) + .taxInclBasePrice(syncMm.getTaxInclBasePrice()) + .taxExclCompilePrice(syncMm.getTaxExclCompilePrice()) + .taxInclCompilePrice(syncMm.getTaxInclCompilePrice()) + .adjustRate(syncMm.getAdjustRate()) + .categoryId(syncMm.getCategoryId()) + .sourceType(syncMm.getSourceType()) + .baseConsumeQty(syncMm.getBaseConsumeQty()) + .baseBasePrice(syncMm.getBaseBasePrice()) + .snapshotJson(syncMm.getSnapshotJson()) + .usageQty(syncMm.getUsageQty()) + .sortOrder(syncMm.getSortOrder()) + .attributes(syncMm.getAttributes()) + .build(); + wbBoqMarketMaterialMapper.insert(newMm); + } + } + + /** + * 内部删除同步库(含所有关联数据) + */ + private void deleteSyncLibraryInternal(Long syncLibraryId) { + // 获取所有分部分项节点ID + List divisions = syncLibraryDivisionMapper.selectListBySyncLibraryId(syncLibraryId); + List divisionIds = divisions.stream().map(SyncLibraryDivisionDO::getId).collect(Collectors.toList()); + + if (CollUtil.isNotEmpty(divisionIds)) { + // 删除工料机 + syncLibraryResourceMapper.deleteBySyncDivisionIds(divisionIds); + // 删除市场主材 + syncLibraryMarketMaterialMapper.deleteBySyncDivisionIds(divisionIds); + } + // 删除分部分项 + syncLibraryDivisionMapper.deleteBySyncLibraryId(syncLibraryId); + // 删除绑定关系 + syncBindingMapper.deleteBySyncLibraryId(syncLibraryId); + // 清理工作台分部分项树上的同步状态(防止脏数据) + wbBoqDivisionMapper.clearSyncStatus(syncLibraryId); + // 删除主表 + syncLibraryMapper.deleteById(syncLibraryId); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbAdjustmentSettingServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbAdjustmentSettingServiceImpl.java new file mode 100644 index 0000000..d266efb --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbAdjustmentSettingServiceImpl.java @@ -0,0 +1,265 @@ +package com.yhy.module.core.service.workbench.impl; + +import cn.hutool.core.collection.CollUtil; +import com.yhy.module.core.dal.dataobject.quota.QuotaAdjustmentSettingDO; +import com.yhy.module.core.dal.dataobject.workbench.WbAdjustmentSettingDO; +import com.yhy.module.core.dal.dataobject.workbench.WbBoqDivisionDO; +import com.yhy.module.core.dal.mysql.quota.QuotaAdjustmentSettingMapper; +import com.yhy.module.core.dal.mysql.workbench.WbAdjustmentSettingMapper; +import com.yhy.module.core.dal.mysql.workbench.WbBoqDivisionMapper; +import com.yhy.module.core.service.workbench.WbAdjustmentSettingService; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * 工作台定额调整设置 Service 实现类 + * + * @author yhy + */ +@Service +@Validated +@Slf4j +public class WbAdjustmentSettingServiceImpl implements WbAdjustmentSettingService { + + @Resource + private WbAdjustmentSettingMapper wbAdjustmentSettingMapper; + + @Resource + private WbBoqDivisionMapper wbBoqDivisionMapper; + + @Resource + private QuotaAdjustmentSettingMapper quotaAdjustmentSettingMapper; + + @Override + public List getListByDivisionId(Long divisionId) { + return wbAdjustmentSettingMapper.selectListByDivisionId(divisionId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public List copyFromQuotaAdjustmentSetting(Long divisionId, Long sourceQuotaItemId) { + // 1. 检查是否已经复制过 + List existingSettings = wbAdjustmentSettingMapper.selectListByDivisionId(divisionId); + if (CollUtil.isNotEmpty(existingSettings)) { + log.debug("[copyFromQuotaAdjustmentSetting] 已存在调整设置,divisionId={}", divisionId); + return existingSettings; + } + + // 2. 从后台获取定额调整设置 + List sourceSettings = quotaAdjustmentSettingMapper.selectListByQuotaItemId(sourceQuotaItemId); + if (CollUtil.isEmpty(sourceSettings)) { + log.debug("[copyFromQuotaAdjustmentSetting] 后台无调整设置,sourceQuotaItemId={}", sourceQuotaItemId); + return new ArrayList<>(); + } + + // 3. 复制到工作台(不复制 adjustmentContent 和 inputValue,新复制的定额调整应该没有初始值) + List copiedSettings = new ArrayList<>(); + for (QuotaAdjustmentSettingDO source : sourceSettings) { + WbAdjustmentSettingDO target = new WbAdjustmentSettingDO(); + target.setDivisionId(divisionId); + target.setSourceAdjustmentSettingId(source.getId()); + target.setName(source.getName()); + target.setQuotaValue(source.getQuotaValue()); + // 不复制 adjustmentContent,新复制的定额调整应该没有初始值 + // target.setAdjustmentContent(source.getAdjustmentContent()); + target.setAdjustmentType(source.getAdjustmentType()); + // 复制 adjustmentRules,但移除 items 中的 inputValue(用户填写的值不应复制) + target.setAdjustmentRules(copyAdjustmentRulesWithoutInputValue(source.getAdjustmentRules())); + target.setSortOrder(source.getSortOrder()); + + // 保存快照 + Map snapshot = new HashMap<>(); + snapshot.put("sourceId", source.getId()); + snapshot.put("sourceName", source.getName()); + snapshot.put("sourceQuotaItemId", sourceQuotaItemId); + target.setSnapshotJson(snapshot); + + wbAdjustmentSettingMapper.insert(target); + copiedSettings.add(target); + } + + log.info("[copyFromQuotaAdjustmentSetting] 复制完成,divisionId={}, count={}", divisionId, copiedSettings.size()); + return copiedSettings; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateAdjustmentRules(Long id, Map adjustmentRules) { + WbAdjustmentSettingDO setting = wbAdjustmentSettingMapper.selectById(id); + if (setting == null) { + log.warn("[updateAdjustmentRules] 调整设置不存在,id={}", id); + return; + } + setting.setAdjustmentRules(adjustmentRules); + wbAdjustmentSettingMapper.updateById(setting); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void applyAdjustmentSetting(Long divisionId, Long adjustmentSettingId, boolean enabled) { + // TODO: 实现应用调整设置逻辑 + // 类似后台的 applyAdjustmentSetting,但操作的是工作台的工料机数据 + log.info("[applyAdjustmentSetting] divisionId={}, adjustmentSettingId={}, enabled={}", + divisionId, adjustmentSettingId, enabled); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void applyDynamicAdjustment(Long divisionId, Long adjustmentSettingId, Map inputValues) { + // 1. 获取调整设置 + WbAdjustmentSettingDO setting = wbAdjustmentSettingMapper.selectById(adjustmentSettingId); + if (setting == null) { + log.warn("[applyDynamicAdjustment] 调整设置不存在,adjustmentSettingId={}", adjustmentSettingId); + return; + } + + // 2. 更新 inputValue 到 adjustmentRules + Map adjustmentRules = setting.getAdjustmentRules(); + if (adjustmentRules != null && adjustmentRules.get("items") instanceof List) { + @SuppressWarnings("unchecked") + List> items = (List>) adjustmentRules.get("items"); + for (Map item : items) { + String categoryId = item.get("category") != null ? item.get("category").toString() : null; + if (categoryId != null) { + if (inputValues.containsKey(categoryId)) { + item.put("inputValue", inputValues.get(categoryId)); + } else { + // 没有输入值则清空(用户清除了值) + item.remove("inputValue"); + } + } + } + setting.setAdjustmentRules(adjustmentRules); + wbAdjustmentSettingMapper.updateById(setting); + } + + // TODO: 实现动态调整计算逻辑 + log.info("[applyDynamicAdjustment] divisionId={}, adjustmentSettingId={}, inputValues={}", + divisionId, adjustmentSettingId, inputValues); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void applyDynamicMerge(Long divisionId, Long adjustmentSettingId, Map inputValues) { + // 1. 获取调整设置 + WbAdjustmentSettingDO setting = wbAdjustmentSettingMapper.selectById(adjustmentSettingId); + if (setting == null) { + log.warn("[applyDynamicMerge] 调整设置不存在,adjustmentSettingId={}", adjustmentSettingId); + return; + } + + // 2. 更新 inputValue 到 adjustmentRules + Map adjustmentRules = setting.getAdjustmentRules(); + if (adjustmentRules != null && adjustmentRules.get("items") instanceof List) { + @SuppressWarnings("unchecked") + List> items = (List>) adjustmentRules.get("items"); + for (Map item : items) { + String code = item.get("code") != null ? item.get("code").toString() : null; + if (code != null) { + if (inputValues.containsKey(code)) { + // 有输入值则更新 + item.put("inputValue", inputValues.get(code)); + } else { + // 没有输入值则清空(用户清除了值) + item.remove("inputValue"); + } + } + } + setting.setAdjustmentRules(adjustmentRules); + wbAdjustmentSettingMapper.updateById(setting); + } + + // TODO: 实现动态合并定额计算逻辑 + log.info("[applyDynamicMerge] divisionId={}, adjustmentSettingId={}, inputValues={}", + divisionId, adjustmentSettingId, inputValues); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteByDivisionId(Long divisionId) { + wbAdjustmentSettingMapper.deleteByDivisionId(divisionId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateAdjustmentSetting(Long id, String adjustmentContent) { + WbAdjustmentSettingDO setting = wbAdjustmentSettingMapper.selectById(id); + if (setting == null) { + log.warn("[updateAdjustmentSetting] 调整设置不存在,id={}", id); + return; + } + setting.setAdjustmentContent(adjustmentContent); + wbAdjustmentSettingMapper.updateById(setting); + log.info("[updateAdjustmentSetting] 更新调整设置成功,id={}, adjustmentContent={}", id, adjustmentContent); + } + + @Override + public List> getAdjustmentCombinedList(Long divisionId) { + // 1. 获取分部分项节点,获取关联的定额基价ID + WbBoqDivisionDO division = wbBoqDivisionMapper.selectById(divisionId); + if (division == null || !"quota".equals(division.getNodeType())) { + return new ArrayList<>(); + } + + Long sourceQuotaItemId = division.getSourceQuotaItemId(); + if (sourceQuotaItemId == null) { + return new ArrayList<>(); + } + + // 2. 确保已复制调整设置 + List settings = copyFromQuotaAdjustmentSetting(divisionId, sourceQuotaItemId); + + // 3. 转换为前端需要的格式 + List> result = new ArrayList<>(); + for (WbAdjustmentSettingDO setting : settings) { + Map item = new HashMap<>(); + item.put("id", setting.getId()); + item.put("divisionId", setting.getDivisionId()); + item.put("name", setting.getName()); + item.put("quotaValue", setting.getQuotaValue()); + item.put("adjustmentType", setting.getAdjustmentType()); + item.put("adjustmentRules", setting.getAdjustmentRules()); + item.put("adjustmentContent", setting.getAdjustmentContent()); + item.put("sortOrder", setting.getSortOrder()); + result.add(item); + } + + return result; + } + + /** + * 复制 adjustmentRules,但移除 items 中的 inputValue + * 项目指引复制定额时,用户填写的调整值不应复制,应由用户重新填写 + */ + @SuppressWarnings("unchecked") + private Map copyAdjustmentRulesWithoutInputValue(Map sourceRules) { + if (sourceRules == null || sourceRules.isEmpty()) { + return sourceRules; + } + + // 深拷贝 + Map rules = new HashMap<>(sourceRules); + + // 移除 items 中的 inputValue + if (rules.get("items") instanceof List) { + List> items = (List>) rules.get("items"); + List> newItems = new ArrayList<>(); + for (Map item : items) { + Map newItem = new HashMap<>(item); + newItem.remove("inputValue"); + newItems.add(newItem); + } + rules.put("items", newItems); + } + + return rules; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbBoqDivisionServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbBoqDivisionServiceImpl.java new file mode 100644 index 0000000..c0ba457 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbBoqDivisionServiceImpl.java @@ -0,0 +1,3251 @@ +package com.yhy.module.core.service.workbench.impl; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0; +import static com.yhy.module.core.enums.ErrorCodeConstants.WB_BOQ_DIVISION_BASE_NUMBER_FORMULA_INVALID; +import static com.yhy.module.core.enums.ErrorCodeConstants.WB_BOQ_DIVISION_BOQ_PARENT_MUST_DIVISION; +import static com.yhy.module.core.enums.ErrorCodeConstants.WB_BOQ_DIVISION_COMPILE_TREE_NOT_UNIT; +import static com.yhy.module.core.enums.ErrorCodeConstants.WB_BOQ_DIVISION_DIVISION_MUST_ROOT; +import static com.yhy.module.core.enums.ErrorCodeConstants.WB_BOQ_DIVISION_FORMULA_INVALID; +import static com.yhy.module.core.enums.ErrorCodeConstants.WB_BOQ_DIVISION_HAS_CHILDREN; +import static com.yhy.module.core.enums.ErrorCodeConstants.WB_BOQ_DIVISION_NODE_TYPE_CANNOT_CHANGE; +import static com.yhy.module.core.enums.ErrorCodeConstants.WB_BOQ_DIVISION_NODE_TYPE_ERROR; +import static com.yhy.module.core.enums.ErrorCodeConstants.WB_BOQ_DIVISION_NODE_TYPE_INVALID; +import static com.yhy.module.core.enums.ErrorCodeConstants.WB_BOQ_DIVISION_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.WB_BOQ_DIVISION_NOT_SAME_LEVEL; +import static com.yhy.module.core.enums.ErrorCodeConstants.WB_BOQ_DIVISION_PARENT_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.WB_BOQ_DIVISION_QUOTA_PARENT_MUST_BOQ; +import static com.yhy.module.core.enums.ErrorCodeConstants.WB_BOQ_DIVISION_ROOT_CANNOT_DELETE; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import com.yhy.module.core.controller.admin.workbench.vo.HistoryBoqListReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqDivisionRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqDivisionSaveReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqDivisionSwapSortReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.price.QuotaUnitPriceResult; +import com.yhy.module.core.convert.workbench.WbBoqDivisionConvert; +import com.yhy.module.core.dal.dataobject.quota.QuotaMarketMaterialDO; +import com.yhy.module.core.dal.dataobject.quota.QuotaResourceDO; +import com.yhy.module.core.dal.dataobject.resource.ResourceItemDO; +import com.yhy.module.core.dal.dataobject.resource.ResourceMergedDO; +import com.yhy.module.core.dal.dataobject.workbench.WbBoqDivisionDO; +import com.yhy.module.core.dal.dataobject.workbench.WbBoqMarketMaterialDO; +import com.yhy.module.core.dal.dataobject.workbench.WbBoqResourceDO; +import com.yhy.module.core.dal.dataobject.workbench.WbCompileTreeDO; +import com.yhy.module.core.dal.mysql.calcbaserate.CalcBaseRateItemMapper; +import com.yhy.module.core.dal.mysql.quota.QuotaMarketMaterialMapper; +import com.yhy.module.core.dal.mysql.quota.QuotaResourceMapper; +import com.yhy.module.core.dal.mysql.resource.ResourceItemMapper; +import com.yhy.module.core.dal.mysql.resource.ResourceMergedMapper; +import com.yhy.module.core.dal.mysql.workbench.WbBoqDivisionMapper; +import com.yhy.module.core.dal.mysql.workbench.WbBoqMarketMaterialMapper; +import com.yhy.module.core.dal.mysql.workbench.WbBoqResourceMapper; +import com.yhy.module.core.dal.mysql.workbench.WbCompileTreeMapper; +import com.yhy.module.core.service.workbench.QuotaPriceCalculatorService; +import com.yhy.module.core.service.workbench.QuotaQtyFormulaService; +import com.yhy.module.core.service.workbench.WbBoqDivisionService; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * 工作台分部分项树 Service 实现类 + * + * @author yhy + */ +@Service +@Validated +@Slf4j +public class WbBoqDivisionServiceImpl implements WbBoqDivisionService { + + @Resource + private WbBoqDivisionMapper wbBoqDivisionMapper; + + @Resource + private WbBoqResourceMapper wbBoqResourceMapper; + + @Resource + private WbBoqMarketMaterialMapper wbBoqMarketMaterialMapper; + + @Resource + private WbCompileTreeMapper wbCompileTreeMapper; + + @Resource + private QuotaResourceMapper quotaResourceMapper; + + @Resource + private QuotaMarketMaterialMapper quotaMarketMaterialMapper; + + @Resource + private ResourceItemMapper resourceItemMapper; + + @Resource + private ResourceMergedMapper resourceMergedMapper; + + @Resource + private QuotaPriceCalculatorService quotaPriceCalculatorService; + + @Resource + private com.yhy.module.core.dal.mysql.boq.BoqItemTreeMapper boqItemTreeMapper; + + @Resource + private com.yhy.module.core.dal.mysql.boq.BoqCatalogItemMapper boqCatalogItemMapper; + + @Resource + private com.yhy.module.core.dal.mysql.quota.QuotaItemMapper quotaItemMapper; + + @Resource + private com.yhy.module.core.dal.mysql.workbench.WbUnitInfoMapper wbUnitInfoMapper; + + @Resource + private com.yhy.module.core.dal.mysql.boq.BoqSubItemMapper boqSubItemMapper; + + @Resource + private com.yhy.module.core.dal.mysql.boq.BoqGuideTreeMapper boqGuideTreeMapper; + + @Resource + private com.yhy.module.core.dal.mysql.workbench.WbProjectTreeMapper wbProjectTreeMapper; + + @Resource + private com.yhy.module.core.dal.mysql.quota.QuotaCatalogTreeMapper quotaCatalogTreeMapper; + + @Resource + private com.yhy.module.core.service.quota.QuotaItemService quotaItemService; + + @Resource + private QuotaQtyFormulaService quotaQtyFormulaService; + + @Resource + private CalcBaseRateItemMapper calcBaseRateItemMapper; + + @Resource + private com.yhy.module.core.service.workbench.WbBoqResourceService wbBoqResourceService; + + @Resource + private com.yhy.module.core.dal.mysql.quota.QuotaCatalogItemMapper quotaCatalogItemMapper; + + @Resource + private com.yhy.module.core.dal.mysql.quota.QuotaVariableSettingMapper quotaVariableSettingMapper; + + @Resource + private com.yhy.module.core.dal.mysql.resource.ResourceCategoryMapper resourceCategoryMapper; + + @Resource + private com.yhy.module.core.service.workbench.WbSnapshotReadService wbSnapshotReadService; + + @Resource + private com.yhy.module.core.dal.mysql.config.ConfigUnitDivisionTemplateMapper configUnitDivisionTemplateMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createNode(WbBoqDivisionSaveReqVO createReqVO) { + // 1. 校验单位工程节点 + validateCompileTreeIsUnit(createReqVO.getCompileTreeId()); + + // 2. 校验节点类型 + validateNodeType(createReqVO.getNodeType()); + + // 3. 校验父节点和层级关系 + WbBoqDivisionDO parent = null; + if (createReqVO.getParentId() != null) { + parent = validateParentExists(createReqVO.getParentId()); + } else if (WbBoqDivisionDO.NODE_TYPE_DIVISION.equals(createReqVO.getNodeType())) { + // 分部节点如果没有指定父节点,自动挂到 root 节点下 + List rootNodes = wbBoqDivisionMapper.selectListByNodeType( + createReqVO.getCompileTreeId(), WbBoqDivisionDO.NODE_TYPE_ROOT); + if (!rootNodes.isEmpty()) { + parent = rootNodes.get(0); + createReqVO.setParentId(parent.getId()); + log.info("[createNode] 分部节点自动挂到root节点下, compileTreeId={}, rootId={}", + createReqVO.getCompileTreeId(), parent.getId()); + } + } + validateHierarchy(createReqVO.getNodeType(), parent); + + // 3.3 如果是清单节点,校验编码在同一单位工程下唯一 + if (WbBoqDivisionDO.NODE_TYPE_BOQ.equals(createReqVO.getNodeType()) + && StrUtil.isNotBlank(createReqVO.getCode())) { + validateBoqCodeUnique(createReqVO.getCompileTreeId(), createReqVO.getCode(), null); + } + + // 3.5 如果是定额节点且有引用定额基价ID,检查费率模式是否存在,并自动建立绑定 + if (WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(createReqVO.getNodeType()) + && createReqVO.getSourceQuotaItemId() != null) { + Long rateModeId = quotaItemService.getRateModeIdByQuotaItem(createReqVO.getSourceQuotaItemId()); + if (rateModeId == null) { + log.warn("[createNode] 定额基价关联的定额专业下没有费率模式, sourceQuotaItemId={}", + createReqVO.getSourceQuotaItemId()); + throw exception0(400, "该定额关联的定额专业下没有配置费率模式,无法添加。"); + } + + // 自动建立项目级费率模式绑定(如果尚未绑定) + autoBindRateModeForProject(createReqVO.getCompileTreeId(), createReqVO.getSourceQuotaItemId(), rateModeId); + } + + // 4. 构建DO + WbBoqDivisionDO node = WbBoqDivisionConvert.INSTANCE.convert(createReqVO); + + // 4.1 处理 tabType:写入 attributes.tabTypes + String tabType = createReqVO.getTabType(); + if (StrUtil.isBlank(tabType) && parent != null) { + // 未指定时从父节点继承 + Map parentAttrs = parent.getAttributes(); + if (parentAttrs != null) { + // 优先从 tabTypes 数组继承 + Object parentTabTypes = parentAttrs.get("tabTypes"); + if (parentTabTypes instanceof java.util.List && !((java.util.List) parentTabTypes).isEmpty()) { + Map attrs = node.getAttributes(); + if (attrs == null) { + attrs = new java.util.HashMap<>(); + } + attrs.put("tabTypes", parentTabTypes); + node.setAttributes(attrs); + tabType = null; // 已处理,跳过下面的单值逻辑 + } else if (parentAttrs.get("tabType") != null) { + // 兼容旧的单值格式 + tabType = (String) parentAttrs.get("tabType"); + } + } + } + if (StrUtil.isNotBlank(tabType)) { + Map attrs = node.getAttributes(); + if (attrs == null) { + attrs = new java.util.HashMap<>(); + } + List tabTypes = new ArrayList<>(); + tabTypes.add(tabType); + attrs.put("tabTypes", tabTypes); + node.setAttributes(attrs); + } + + // 4.5 定额节点默认设置工程量公式为 QDL + if (WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(node.getNodeType())) { + if (StrUtil.isBlank(node.getQuotaQtyFormula())) { + node.setQuotaQtyFormula("QDL"); + } + // 如果有公式且有父清单,计算工程量(带单位换算) + if (StrUtil.isNotBlank(node.getQuotaQtyFormula()) && parent != null + && WbBoqDivisionDO.NODE_TYPE_BOQ.equals(parent.getNodeType())) { + BigDecimal qdlValue = parent.getQty() != null ? parent.getQty() : BigDecimal.ZERO; + BigDecimal calculatedQty = quotaQtyFormulaService.calculateQuotaQty( + node.getQuotaQtyFormula(), qdlValue, node.getUnit()); + node.setQty(calculatedQty); + } + } + + // 5. 设置默认来源类型 + if (node.getSourceType() == null) { + node.setSourceType(WbBoqDivisionDO.SOURCE_TYPE_MANUAL); + } + + // 6. 设置排序号(所有节点类型都插入到末尾) + Integer maxSortOrder = wbBoqDivisionMapper.selectMaxSortOrderByParentId( + createReqVO.getCompileTreeId(), createReqVO.getParentId()); + node.setSortOrder(maxSortOrder + 1); + + // 7. 设置路径 + node.setPath(buildPath(parent)); + + // 8. 插入数据库 + wbBoqDivisionMapper.insert(node); + + // 9. 更新路径(包含自身ID) + updatePathWithSelfId(node); + + // 10. 如果是定额节点且有引用定额基价ID,复制工料机数据 + if (WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(node.getNodeType()) + && createReqVO.getSourceQuotaItemId() != null) { + copyQuotaResources(node.getId(), node.getTenantId(), createReqVO.getSourceQuotaItemId()); + } + + return node.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateNode(WbBoqDivisionSaveReqVO updateReqVO) { + // 1. 校验存在 + WbBoqDivisionDO existNode = validateNodeExists(updateReqVO.getId()); + + // 2. 根目录节点只允许更新费用代号字段 + if (WbBoqDivisionDO.NODE_TYPE_ROOT.equals(existNode.getNodeType())) { + // 只更新 costCode 字段 + WbBoqDivisionDO updateObj = new WbBoqDivisionDO(); + updateObj.setId(existNode.getId()); + updateObj.setCostCode(updateReqVO.getCostCode()); + wbBoqDivisionMapper.updateById(updateObj); + return; + } + + // 3. 不允许修改节点类型 + if (!existNode.getNodeType().equals(updateReqVO.getNodeType())) { + throw exception(WB_BOQ_DIVISION_NODE_TYPE_CANNOT_CHANGE); + } + + // 3.5 如果是清单节点且编码有变化,校验编码在同一单位工程下唯一 + if (WbBoqDivisionDO.NODE_TYPE_BOQ.equals(existNode.getNodeType()) + && StrUtil.isNotBlank(updateReqVO.getCode()) + && !updateReqVO.getCode().equals(existNode.getCode())) { + validateBoqCodeUnique(existNode.getCompileTreeId(), updateReqVO.getCode(), existNode.getId()); + } + + // 4. 更新 + WbBoqDivisionDO updateObj = WbBoqDivisionConvert.INSTANCE.convert(updateReqVO); + updateObj.setId(existNode.getId()); + // 保留不可修改的字段 + updateObj.setCompileTreeId(existNode.getCompileTreeId()); + updateObj.setParentId(existNode.getParentId()); + updateObj.setSortOrder(existNode.getSortOrder()); + updateObj.setPath(existNode.getPath()); + // 合并扩展属性(如果请求中有attributes,则合并到现有属性中) + if (updateReqVO.getAttributes() != null) { + Map mergedAttrs = new java.util.HashMap<>(); + if (existNode.getAttributes() != null) { + mergedAttrs.putAll(existNode.getAttributes()); + } + mergedAttrs.putAll(updateReqVO.getAttributes()); + + // 验证基数公式(如果存在),传入当前节点的费用代号用于防止循环引用 + validateBaseNumberFormula(mergedAttrs, updateReqVO.getCostCode()); + + updateObj.setAttributes(mergedAttrs); + } + + // 5. 定额节点:处理工程量公式 + if (WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(existNode.getNodeType())) { + String formula = updateReqVO.getQuotaQtyFormula(); + // 空值恢复为默认公式 QDL + if (StrUtil.isBlank(formula)) { + formula = "QDL"; + updateObj.setQuotaQtyFormula(formula); + } + // 验证公式合法性 + QuotaQtyFormulaService.FormulaValidationResult validationResult = + quotaQtyFormulaService.validateFormula(formula); + if (!validationResult.isValid()) { + throw exception(WB_BOQ_DIVISION_FORMULA_INVALID, validationResult.getError()); + } + // 获取父清单工程量并计算(带单位换算) + BigDecimal qdlValue = getParentBoqQty(existNode.getParentId()); + // 优先使用更新请求中的单位,否则使用现有单位 + String quotaUnit = updateReqVO.getUnit() != null ? updateReqVO.getUnit() : existNode.getUnit(); + BigDecimal calculatedQty = quotaQtyFormulaService.calculateQuotaQty(formula, qdlValue, quotaUnit); + updateObj.setQty(calculatedQty); + } + + // 6. 单独清单(无子节点):根据基数公式计算工程量 + if (WbBoqDivisionDO.NODE_TYPE_BOQ.equals(existNode.getNodeType())) { + // 检查是否为单独清单(没有子节点) + List children = wbBoqDivisionMapper.selectListByParentId(existNode.getId()); + boolean isStandaloneBoq = CollUtil.isEmpty(children); + + if (isStandaloneBoq) { + // 单独清单:根据基数公式计算工程量 + // 获取树数据(含价格计算,用于费用代号变量求值) + List divisionTree = getTreeWithPrice(existNode.getCompileTreeId()); + BigDecimal calculatedQty = calculateStandaloneBoqQty(existNode.getCompileTreeId(), updateObj, divisionTree); + if (calculatedQty != null) { + updateObj.setQty(calculatedQty); + } + } + } + + wbBoqDivisionMapper.updateById(updateObj); + + // 7. 有子节点的清单:如果工程量变化,重新计算所有子定额的工程量 + if (WbBoqDivisionDO.NODE_TYPE_BOQ.equals(existNode.getNodeType())) { + List children = wbBoqDivisionMapper.selectListByParentId(existNode.getId()); + if (CollUtil.isNotEmpty(children)) { + BigDecimal oldQty = existNode.getQty(); + BigDecimal newQty = updateReqVO.getQty(); + if (oldQty == null && newQty != null + || oldQty != null && newQty == null + || oldQty != null && oldQty.compareTo(newQty) != 0) { + quotaQtyFormulaService.recalculateChildQuotaQty(existNode.getId()); + } + } + } + } + + /** + * 获取父清单的工程量 + */ + private BigDecimal getParentBoqQty(Long parentId) { + if (parentId == null) { + return BigDecimal.ZERO; + } + WbBoqDivisionDO parent = wbBoqDivisionMapper.selectById(parentId); + if (parent == null) { + return BigDecimal.ZERO; + } + if (WbBoqDivisionDO.NODE_TYPE_BOQ.equals(parent.getNodeType())) { + return parent.getQty() != null ? parent.getQty() : BigDecimal.ZERO; + } + return getParentBoqQty(parent.getParentId()); + } + + /** + * 验证基数公式 + * 参考 QuotaVariableSettingServiceImpl.validateCalcBase 的验证逻辑 + * @param attributes 扩展属性 + * @param currentCostCode 当前节点的费用代号(用于防止循环引用) + */ + @SuppressWarnings("unchecked") + private void validateBaseNumberFormula(Map attributes, String currentCostCode) { + if (attributes == null) { + return; + } + + Object baseNumberObj = attributes.get("baseNumber"); + if (baseNumberObj == null) { + return; + } + + if (!(baseNumberObj instanceof Map)) { + return; + } + + Map baseNumber = (Map) baseNumberObj; + String formula = (String) baseNumber.get("formula"); + + // 空公式是允许的 + if (StrUtil.isBlank(formula)) { + return; + } + + String trimmedFormula = formula.trim(); + + // 验证公式中不能包含本行的费用代号(防止循环引用) + if (StrUtil.isNotBlank(currentCostCode)) { + // 使用正则匹配完整的变量名(避免部分匹配,如 ABC 不应该匹配 ABCD) + String pattern = "\\b" + java.util.regex.Pattern.quote(currentCostCode) + "\\b"; + if (java.util.regex.Pattern.compile(pattern).matcher(trimmedFormula).find()) { + throw exception(WB_BOQ_DIVISION_BASE_NUMBER_FORMULA_INVALID, + "公式不能引用本行的费用代号[" + currentCostCode + "],以免造成无限循环"); + } + } + + // 验证公式语法:只允许字母、数字、运算符和括号 + String validPattern = "^[A-Za-z0-9_+\\-*/().\\s]+$"; + if (!trimmedFormula.matches(validPattern)) { + throw exception(WB_BOQ_DIVISION_BASE_NUMBER_FORMULA_INVALID, "公式只能包含字母、数字、运算符(+-*/)和括号"); + } + + // 验证括号匹配 + int parenCount = 0; + for (char c : trimmedFormula.toCharArray()) { + if (c == '(') parenCount++; + if (c == ')') parenCount--; + if (parenCount < 0) { + throw exception(WB_BOQ_DIVISION_BASE_NUMBER_FORMULA_INVALID, "括号不匹配"); + } + } + if (parenCount != 0) { + throw exception(WB_BOQ_DIVISION_BASE_NUMBER_FORMULA_INVALID, "括号不匹配"); + } + + // 验证公式不能以运算符或小数点结尾 + if (trimmedFormula.matches(".*[+\\-*/.]$")) { + throw exception(WB_BOQ_DIVISION_BASE_NUMBER_FORMULA_INVALID, "公式不能以运算符或小数点结尾"); + } + + // 验证公式不能以运算符开头(除了负号) + if (trimmedFormula.matches("^[+*/.].*")) { + throw exception(WB_BOQ_DIVISION_BASE_NUMBER_FORMULA_INVALID, "公式不能以运算符或小数点开头"); + } + + // 验证不能有连续的运算符 + if (trimmedFormula.matches(".*[+\\-*/]{2,}.*")) { + throw exception(WB_BOQ_DIVISION_BASE_NUMBER_FORMULA_INVALID, "公式不能有连续的运算符"); + } + + // 验证小数点格式(不能有多个连续小数点) + if (trimmedFormula.matches(".*\\.{2,}.*")) { + throw exception(WB_BOQ_DIVISION_BASE_NUMBER_FORMULA_INVALID, "小数点格式不正确"); + } + + // 验证小数点后面不能直接跟运算符或括号 + if (trimmedFormula.matches(".*\\.[+\\-*/()].*")) { + throw exception(WB_BOQ_DIVISION_BASE_NUMBER_FORMULA_INVALID, "小数点后面必须跟数字"); + } + } + + /** + * 计算单独清单的工程量 + * 单独清单的工程量 = 基数公式计算出来的汇总值 + * + * @param compileTreeId 编制树ID + * @param node 清单节点(包含 attributes.calcBase 和 attributes.baseNumberRangeIds) + * @param divisionTree 已有的分部分项树数据(避免循环调用) + * @return 计算后的工程量,如果无法计算则返回 null + */ + @SuppressWarnings("unchecked") + private BigDecimal calculateStandaloneBoqQty(Long compileTreeId, WbBoqDivisionDO node, List divisionTree) { + Map attributes = node.getAttributes(); + if (attributes == null) { + return null; + } + + // 获取基数公式(从 baseNumber.formula 获取) + Object baseNumberObj = attributes.get("baseNumber"); + if (baseNumberObj == null || !(baseNumberObj instanceof Map)) { + return null; + } + Map baseNumber = (Map) baseNumberObj; + String formula = (String) baseNumber.get("formula"); + if (StrUtil.isBlank(formula)) { + return null; + } + + log.info("[calculateStandaloneBoqQty] 原始公式: {}", formula); + + // 获取基数范围(从 baseNumberRange.selectedIds 获取) + List baseNumberRangeIds = null; + Object rangeObj = attributes.get("baseNumberRange"); + if (rangeObj instanceof Map) { + Map rangeMap = (Map) rangeObj; + Object selectedIdsObj = rangeMap.get("selectedIds"); + if (selectedIdsObj instanceof List) { + List rangeIdsList = (List) selectedIdsObj; + baseNumberRangeIds = new ArrayList<>(); + for (Object id : rangeIdsList) { + if (id instanceof Number) { + baseNumberRangeIds.add(((Number) id).longValue()); + } else if (id instanceof String) { + try { + baseNumberRangeIds.add(Long.parseLong((String) id)); + } catch (NumberFormatException e) { + // 忽略无效的ID + } + } + } + } + } + + // 构建变量值映射(从工料机消耗表按分类汇总价格) + // 排除当前节点自身,避免循环依赖 + Map variableValueMap = buildVariableValueMapForStandaloneBoq( + compileTreeId, divisionTree, + baseNumberRangeIds != null ? new java.util.HashSet<>(baseNumberRangeIds) : null, + node.getId() // 传入当前节点ID,用于排除 + ); + + log.info("[calculateStandaloneBoqQty] 标准变量值映射: {}", variableValueMap); + + // 解析公式中的自定义变量(如 aaa123 -> DRGF) + String resolvedFormula = resolveCustomVariables(formula, compileTreeId, variableValueMap); + log.info("[calculateStandaloneBoqQty] 解析后公式: {}", resolvedFormula); + + // 计算公式 + BigDecimal result = evaluateFormula(resolvedFormula, variableValueMap); + log.info("[calculateStandaloneBoqQty] 计算结果: {}", result); + return result; + } + + /** + * 解析公式中的自定义变量 + * 自定义变量是指在变量设置表中定义的变量(如 aaa123),其 calc_base.formula 指向标准变量(如 DRGF) + * + * @param formula 原始公式 + * @param compileTreeId 编制树ID + * @param variableValueMap 标准变量值映射 + * @return 解析后的公式(自定义变量被替换为其 calc_base.formula) + */ + @SuppressWarnings("unchecked") + private String resolveCustomVariables(String formula, Long compileTreeId, Map variableValueMap) { + if (StrUtil.isBlank(formula)) { + return formula; + } + + // 提取公式中的所有变量名 + java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("[A-Za-z][A-Za-z0-9_]*"); + java.util.regex.Matcher matcher = pattern.matcher(formula); + java.util.Set variableNames = new java.util.HashSet<>(); + while (matcher.find()) { + variableNames.add(matcher.group()); + } + + if (variableNames.isEmpty()) { + return formula; + } + + // 查找自定义变量(不在标准变量映射中的变量) + java.util.Set customVariables = new java.util.HashSet<>(); + for (String varName : variableNames) { + if (!variableValueMap.containsKey(varName)) { + customVariables.add(varName); + } + } + + if (customVariables.isEmpty()) { + return formula; + } + + log.info("[resolveCustomVariables] 自定义变量: {}", customVariables); + + // 从变量设置表查询自定义变量的 calc_base + // 由于单独清单可能使用多个定额专业的变量,需要查询所有匹配 code 的变量设置 + String resolvedFormula = formula; + for (String customVar : customVariables) { + // 查询所有匹配该 code 的变量设置(可能来自不同的定额专业) + List varSettings = + quotaVariableSettingMapper.selectList( + new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX() + .eq(com.yhy.module.core.dal.dataobject.quota.QuotaVariableSettingDO::getCode, customVar) + ); + + // 取第一个匹配的变量设置(假设同名变量的 calc_base 相同) + if (!varSettings.isEmpty()) { + com.yhy.module.core.dal.dataobject.quota.QuotaVariableSettingDO varSetting = varSettings.get(0); + if (varSetting.getCalcBase() != null) { + Map calcBase = varSetting.getCalcBase(); + String innerFormula = (String) calcBase.get("formula"); + if (StrUtil.isNotBlank(innerFormula)) { + log.info("[resolveCustomVariables] 变量 {} -> {}", customVar, innerFormula); + // 用括号包裹以保证运算优先级 + resolvedFormula = resolvedFormula.replaceAll("\\b" + customVar + "\\b", "(" + innerFormula + ")"); + } + } + } + } + + return resolvedFormula; + } + + /** + * 为单独清单构建变量值映射 + * + * 合价计算规则: + * - 用量 = 定额工程量(quotaQty) × 消耗量(consumeQty) + * - 合价 = 用量 × 单价 + * - 子工料机(parentId != null)不参与汇总 + * + * 变量代号来源:yhy_resource_category 表的四个代码字段 + * - tax_excl_base_code: 除税基价代码(如 DRGF) + * - tax_incl_base_code: 含税基价代码(如 HDRGF) + * - tax_excl_compile_code: 除税编制价代码(如 RGF) + * - tax_incl_compile_code: 含税编制价代码(如 HRGF) + */ + private Map buildVariableValueMapForStandaloneBoq( + Long compileTreeId, + List divisionTree, + java.util.Set baseNumberRangeIds, + Long excludeNodeId) { + Map result = new java.util.HashMap<>(); + boolean hasRange = baseNumberRangeIds != null && !baseNumberRangeIds.isEmpty(); + + // 收集范围内的定额节点及其工程量 + java.util.Map quotaNodeQtyMap = new java.util.HashMap<>(); + // 收集范围内的统一取费节点 + List unifiedFeeNodes = new ArrayList<>(); + + if (hasRange) { + // 如果有基数范围,先收集范围内的所有节点ID(包括子节点) + java.util.Set nodesInRange = new java.util.HashSet<>(); + collectNodesInRangeForBoq(divisionTree, baseNumberRangeIds, nodesInRange, false); + // 排除当前节点 + if (excludeNodeId != null) { + nodesInRange.remove(excludeNodeId); + } + // 从树中收集范围内的定额节点 + collectQuotaNodeQtyMapForBoq(divisionTree, quotaNodeQtyMap, true, nodesInRange, false); + // 从树中收集范围内的统一取费节点 + collectUnifiedFeeNodesForBoq(divisionTree, unifiedFeeNodes, true, nodesInRange, false); + } else { + // 如果没有基数范围,查询整个单位工程的所有定额节点 + log.info("[buildVariableValueMapForStandaloneBoq] 基数范围为空,查询整个单位工程的定额节点, compileTreeId={}", compileTreeId); + List allQuotaNodes = wbBoqDivisionMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(WbBoqDivisionDO::getCompileTreeId, compileTreeId) + .eq(WbBoqDivisionDO::getNodeType, WbBoqDivisionDO.NODE_TYPE_QUOTA) + .eq(WbBoqDivisionDO::getDeleted, 0) + ); + for (WbBoqDivisionDO quota : allQuotaNodes) { + // 排除当前节点 + if (excludeNodeId != null && excludeNodeId.equals(quota.getId())) { + continue; + } + BigDecimal qty = quota.getQty() != null ? quota.getQty() : BigDecimal.ONE; + quotaNodeQtyMap.put(quota.getId(), qty); + } + // 无基数范围时,也查询所有统一取费节点 + List allUnifiedFeeNodes = wbBoqDivisionMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(WbBoqDivisionDO::getCompileTreeId, compileTreeId) + .eq(WbBoqDivisionDO::getNodeType, WbBoqDivisionDO.NODE_TYPE_UNIFIED_FEE) + .eq(WbBoqDivisionDO::getDeleted, 0) + ); + for (WbBoqDivisionDO ufNode : allUnifiedFeeNodes) { + if (excludeNodeId != null && excludeNodeId.equals(ufNode.getId())) { + continue; + } + WbBoqDivisionRespVO vo = new WbBoqDivisionRespVO(); + vo.setId(ufNode.getId()); + vo.setNodeType(ufNode.getNodeType()); + vo.setQty(ufNode.getQty()); + vo.setAttributes(ufNode.getAttributes()); + unifiedFeeNodes.add(vo); + } + } + + log.info("[buildVariableValueMapForStandaloneBoq] 定额节点数量: {}, 统一取费节点数量: {}", quotaNodeQtyMap.size(), unifiedFeeNodes.size()); + + // 按分类汇总各价格字段 + Map> categoryPriceMap = new java.util.HashMap<>(); + + // 处理普通定额节点的工料机 + if (!quotaNodeQtyMap.isEmpty()) { + List allResources = + wbBoqResourceMapper.selectListByDivisionIds(new ArrayList<>(quotaNodeQtyMap.keySet())); + + log.info("[buildVariableValueMapForStandaloneBoq] 工料机数量(含子): {}", allResources != null ? allResources.size() : 0); + + if (allResources != null) { + for (WbBoqResourceDO resource : allResources) { + // 跳过子工料机(复合工料机只取父值) + if (resource.getParentId() != null) continue; + + Long categoryId = resource.getCategoryId(); + if (categoryId == null) continue; + + // 获取消耗量(优先调整消耗量) + BigDecimal consumeQty = resource.getAdjustConsumeQty() != null + ? resource.getAdjustConsumeQty() : resource.getConsumeQty(); + if (consumeQty == null) consumeQty = BigDecimal.ZERO; + + // 获取定额工程量 + BigDecimal quotaQty = quotaNodeQtyMap.get(resource.getDivisionId()); + if (quotaQty == null) quotaQty = BigDecimal.ONE; + + // 用量 = 定额工程量 × 消耗量(单位为%的工料机:用量=消耗量) + BigDecimal usageQty; + if ("%".equals(resource.getUnit())) { + usageQty = consumeQty; + } else { + usageQty = quotaQty.multiply(consumeQty); + } + + Map priceMap = categoryPriceMap.computeIfAbsent(categoryId, k -> new java.util.HashMap<>()); + + // 合价 = 用量 × 单价 + addPriceToMapForBoq(priceMap, "tax_excl_base_price", resource.getTaxExclBasePrice(), usageQty); + addPriceToMapForBoq(priceMap, "tax_incl_base_price", resource.getTaxInclBasePrice(), usageQty); + addPriceToMapForBoq(priceMap, "tax_excl_compile_price", resource.getTaxExclCompilePrice(), usageQty); + addPriceToMapForBoq(priceMap, "tax_incl_compile_price", resource.getTaxInclCompilePrice(), usageQty); + } + } + } + + // 将统一取费节点的子目工料机也汇总到分类价格映射中 + if (!unifiedFeeNodes.isEmpty()) { + aggregateUnifiedFeeResourcesForBoq(unifiedFeeNodes, compileTreeId, categoryPriceMap); + } + + if (categoryPriceMap.isEmpty()) { + return result; + } + + log.info("[buildVariableValueMapForStandaloneBoq] 分类价格映射: {}", categoryPriceMap); + + // 从工料机总类表查询所有分类,构建变量代号到值的映射 + List categories = + resourceCategoryMapper.selectList(); + + for (com.yhy.module.core.dal.dataobject.resource.ResourceCategoryDO category : categories) { + Map priceMap = categoryPriceMap.get(category.getId()); + if (priceMap == null) continue; + + // 除税基价代码 -> tax_excl_base_price + if (StrUtil.isNotBlank(category.getTaxExclBaseCode())) { + BigDecimal value = priceMap.get("tax_excl_base_price"); + if (value != null) { + result.put(category.getTaxExclBaseCode(), value); + } + } + // 含税基价代码 -> tax_incl_base_price + if (StrUtil.isNotBlank(category.getTaxInclBaseCode())) { + BigDecimal value = priceMap.get("tax_incl_base_price"); + if (value != null) { + result.put(category.getTaxInclBaseCode(), value); + } + } + // 除税编制价代码 -> tax_excl_compile_price + if (StrUtil.isNotBlank(category.getTaxExclCompileCode())) { + BigDecimal value = priceMap.get("tax_excl_compile_price"); + if (value != null) { + result.put(category.getTaxExclCompileCode(), value); + } + } + // 含税编制价代码 -> tax_incl_compile_price + if (StrUtil.isNotBlank(category.getTaxInclCompileCode())) { + BigDecimal value = priceMap.get("tax_incl_compile_price"); + if (value != null) { + result.put(category.getTaxInclCompileCode(), value); + } + } + } + + // 收集分部分项的费用代号(costCode)及其对应的合价(amount) + collectCostCodeValuesForBoq(divisionTree, result); + + log.info("[buildVariableValueMapForStandaloneBoq] 变量值映射: {}", result); + + return result; + } + + /** + * 收集分部分项的费用代号及其对应的合价值 + * 遍历分部分项树,将每个节点的 costCode -> amount 添加到变量映射中 + * + * 注意:如果节点的 amount 为 null(尚未计算),则尝试从子节点汇总计算 + */ + private void collectCostCodeValuesForBoq( + List nodes, + Map result) { + if (nodes == null) return; + + for (WbBoqDivisionRespVO node : nodes) { + // 递归处理子节点(先处理子节点,以便计算父节点的合价) + if (node.getChildren() != null && !node.getChildren().isEmpty()) { + collectCostCodeValuesForBoq(node.getChildren(), result); + } + + // 如果节点有费用代号,则添加到映射 + String costCode = node.getCostCode(); + if (StrUtil.isNotBlank(costCode)) { + BigDecimal amount = node.getAmount(); + + // 如果 amount 为 null,尝试从子节点汇总计算 + if (amount == null && node.getChildren() != null && !node.getChildren().isEmpty()) { + amount = calculateNodeAmountFromChildren(node); + } + + if (amount != null) { + // 费用代号可能重复,使用累加方式 + result.merge(costCode, amount, BigDecimal::add); + } + } + } + } + + /** + * 从子节点汇总计算节点的合价 + * 用于费用代号变量求值时,节点的 amount 尚未计算的情况 + */ + private BigDecimal calculateNodeAmountFromChildren(WbBoqDivisionRespVO node) { + List children = node.getChildren(); + if (children == null || children.isEmpty()) { + return null; + } + + BigDecimal total = BigDecimal.ZERO; + for (WbBoqDivisionRespVO child : children) { + BigDecimal childAmount = child.getAmount(); + // 如果子节点的 amount 也为 null,递归计算 + if (childAmount == null && child.getChildren() != null && !child.getChildren().isEmpty()) { + childAmount = calculateNodeAmountFromChildren(child); + } + if (childAmount != null) { + total = total.add(childAmount); + } + } + return total; + } + + private void addPriceToMapForBoq(Map priceMap, String priceField, + BigDecimal unitPrice, BigDecimal qty) { + if (unitPrice == null) return; + BigDecimal amount = unitPrice.multiply(qty); + priceMap.merge(priceField, amount, BigDecimal::add); + } + + private void collectNodesInRangeForBoq( + List nodes, + java.util.Set selectedIds, + java.util.Set result, + boolean parentSelected) { + if (nodes == null) return; + + for (WbBoqDivisionRespVO node : nodes) { + boolean isSelected = selectedIds.contains(node.getId()) || parentSelected; + if (isSelected) { + result.add(node.getId()); + } + if (node.getChildren() != null && !node.getChildren().isEmpty()) { + collectNodesInRangeForBoq(node.getChildren(), selectedIds, result, isSelected); + } + } + } + + /** + * 收集范围内的定额节点ID及其工程量 + */ + private void collectQuotaNodeQtyMapForBoq( + List nodes, + java.util.Map quotaNodeQtyMap, + boolean hasRange, + java.util.Set nodesInRange, + boolean parentInRange) { + if (nodes == null) return; + + for (WbBoqDivisionRespVO node : nodes) { + boolean inRange = !hasRange || parentInRange || nodesInRange.contains(node.getId()); + + // 跳过统一取费节点,但递归处理其子节点 + if (WbBoqDivisionDO.NODE_TYPE_UNIFIED_FEE.equals(node.getNodeType())) { + if (node.getChildren() != null && !node.getChildren().isEmpty()) { + collectQuotaNodeQtyMapForBoq(node.getChildren(), quotaNodeQtyMap, hasRange, nodesInRange, inRange); + } + continue; + } + + // 收集定额节点及其工程量 + if (WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(node.getNodeType()) && inRange) { + BigDecimal qty = node.getQty() != null ? node.getQty() : BigDecimal.ONE; + quotaNodeQtyMap.put(node.getId(), qty); + } + + // 递归处理子节点 + if (node.getChildren() != null && !node.getChildren().isEmpty()) { + collectQuotaNodeQtyMapForBoq(node.getChildren(), quotaNodeQtyMap, hasRange, nodesInRange, inRange); + } + } + } + + /** + * 收集范围内的统一取费节点(unified_fee) + * 用于基数设置汇总值计算时,将统一取费涉及的子目工料机也纳入分类汇总 + */ + private void collectUnifiedFeeNodesForBoq( + List nodes, + List unifiedFeeNodes, + boolean hasRange, + java.util.Set nodesInRange, + boolean parentInRange) { + if (nodes == null) return; + + for (WbBoqDivisionRespVO node : nodes) { + boolean inRange = !hasRange || parentInRange || nodesInRange.contains(node.getId()); + + if (WbBoqDivisionDO.NODE_TYPE_UNIFIED_FEE.equals(node.getNodeType()) && inRange) { + unifiedFeeNodes.add(node); + } + + // 递归处理子节点 + if (node.getChildren() != null && !node.getChildren().isEmpty()) { + collectUnifiedFeeNodesForBoq(node.getChildren(), unifiedFeeNodes, hasRange, nodesInRange, inRange); + } + } + } + + /** + * 将统一取费节点的子目工料机汇总到分类价格映射中 + * + * 统一取费节点没有自己的 yhy_wb_boq_resource 数据,需要溯源到后台统一取费设置的子定额及其子目工料机: + * unified_fee 节点 → attributes.sourceUnifiedFeeSettingId → 快照 WbUnifiedFeeSettingDO + * → 子费用(child) → WbUnifiedFeeResourceDO → ResourceItemDO(获取价格和分类) + */ + private void aggregateUnifiedFeeResourcesForBoq( + List unifiedFeeNodes, + Long compileTreeId, + Map> categoryPriceMap) { + if (unifiedFeeNodes == null || unifiedFeeNodes.isEmpty()) return; + + for (WbBoqDivisionRespVO ufNode : unifiedFeeNodes) { + // 通过 QuotaPriceCalculatorService 获取统一取费节点的分类合价 + // 该方法内部会:获取母定额列表 → 获取母定额分类合价 → 用calcBase公式计算%工料机合价 → 按categoryId汇总 + Map ufCategorySums = + quotaPriceCalculatorService.getUnifiedFeeCategorySums(ufNode.getId()); + + if (ufCategorySums == null || ufCategorySums.isEmpty()) { + log.info("[基数汇总-统一取费] 统一取费节点无分类合价, nodeId={}", ufNode.getId()); + continue; + } + + log.info("[基数汇总-统一取费] nodeId={}, 分类合价={}", ufNode.getId(), ufCategorySums); + + // 将统一取费的分类合价合并到总的 categoryPriceMap + for (Map.Entry entry : ufCategorySums.entrySet()) { + Long categoryId = entry.getKey(); + com.yhy.module.core.controller.admin.workbench.vo.price.CategoryPriceSum sum = entry.getValue(); + + Map priceMap = categoryPriceMap.computeIfAbsent(categoryId, k -> new java.util.HashMap<>()); + priceMap.merge("tax_excl_base_price", sum.getTaxExclBaseSum() != null ? sum.getTaxExclBaseSum() : BigDecimal.ZERO, BigDecimal::add); + priceMap.merge("tax_incl_base_price", sum.getTaxInclBaseSum() != null ? sum.getTaxInclBaseSum() : BigDecimal.ZERO, BigDecimal::add); + priceMap.merge("tax_excl_compile_price", sum.getTaxExclCompileSum() != null ? sum.getTaxExclCompileSum() : BigDecimal.ZERO, BigDecimal::add); + priceMap.merge("tax_incl_compile_price", sum.getTaxInclCompileSum() != null ? sum.getTaxInclCompileSum() : BigDecimal.ZERO, BigDecimal::add); + } + } + } + + /** + * 计算公式 + */ + private BigDecimal evaluateFormula(String formula, Map variableValueMap) { + if (StrUtil.isBlank(formula)) { + return null; + } + + try { + // 替换公式中的变量为对应的值 + String expression = formula; + for (Map.Entry entry : variableValueMap.entrySet()) { + String code = entry.getKey(); + BigDecimal value = entry.getValue(); + expression = expression.replaceAll("\\b" + code + "\\b", value.toPlainString()); + } + + // 将未知变量替换为0 + expression = expression.replaceAll("[A-Za-z][A-Za-z0-9_]*", "0"); + + log.debug("[evaluateFormula] 公式: {}, 表达式: {}", formula, expression); + + // 使用 JavaScript 引擎计算表达式 + javax.script.ScriptEngineManager manager = new javax.script.ScriptEngineManager(); + javax.script.ScriptEngine engine = manager.getEngineByName("JavaScript"); + if (engine == null) { + engine = manager.getEngineByName("nashorn"); + } + if (engine == null) { + log.warn("[evaluateFormula] 无法获取脚本引擎,无法计算公式: {}", formula); + return null; + } + + Object result = engine.eval(expression); + if (result instanceof Number) { + return new BigDecimal(result.toString()).setScale(3, java.math.RoundingMode.HALF_UP); + } + return null; + } catch (Exception e) { + log.warn("[evaluateFormula] 计算公式失败,公式: {}, 错误: {}", formula, e.getMessage()); + return null; + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteNode(Long id) { + // 1. 校验存在 + WbBoqDivisionDO node = validateNodeExists(id); + + // 2. 禁止删除根目录节点 + if (WbBoqDivisionDO.NODE_TYPE_ROOT.equals(node.getNodeType())) { + throw exception(WB_BOQ_DIVISION_ROOT_CANNOT_DELETE); + } + + // 3. 校验是否有子节点 + Long childCount = wbBoqDivisionMapper.selectCountByParentId(id); + if (childCount > 0) { + throw exception(WB_BOQ_DIVISION_HAS_CHILDREN); + } + + // 4. 如果是定额节点,删除关联的工料机 + if (WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(node.getNodeType())) { + wbBoqResourceMapper.deleteByDivisionId(id); + } + + // 5. 删除节点 + wbBoqDivisionMapper.deleteById(id); + } + + @Override + public WbBoqDivisionRespVO getNode(Long id) { + WbBoqDivisionDO node = wbBoqDivisionMapper.selectById(id); + if (node == null) { + return null; + } + return WbBoqDivisionConvert.INSTANCE.convert(node); + } + + @Override + public List getTree(Long compileTreeId) { + List list = wbBoqDivisionMapper.selectListByCompileTreeId(compileTreeId); + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + + // 填充定额节点的sourceCatalogItemId和sourceCatalogPath(用于取费章节过滤) + List voList = list.stream() + .map(WbBoqDivisionConvert.INSTANCE::convert) + .collect(Collectors.toList()); + + // 批量查询定额项的catalogItemId + List quotaItemIds = list.stream() + .filter(node -> WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(node.getNodeType())) + .filter(node -> node.getSourceQuotaItemId() != null) + .map(WbBoqDivisionDO::getSourceQuotaItemId) + .distinct() + .collect(Collectors.toList()); + Map quotaItemToCatalogMap = new java.util.HashMap<>(); + if (CollUtil.isNotEmpty(quotaItemIds)) { + List quotaItems = + quotaItemMapper.selectBatchIds(quotaItemIds); + for (com.yhy.module.core.dal.dataobject.quota.QuotaItemDO item : quotaItems) { + if (item.getCatalogItemId() != null) { + quotaItemToCatalogMap.put(item.getId(), item.getCatalogItemId()); + } + } + } + + // 批量查询定额子目录的path + List catalogItemIds = new java.util.ArrayList<>(quotaItemToCatalogMap.values()); + Map catalogItemToPathMap = new java.util.HashMap<>(); + if (CollUtil.isNotEmpty(catalogItemIds)) { + List catalogTrees = + quotaCatalogTreeMapper.selectBatchIds(catalogItemIds); + for (com.yhy.module.core.dal.dataobject.quota.QuotaCatalogTreeDO tree : catalogTrees) { + if (tree.getPath() != null) { + catalogItemToPathMap.put(tree.getId(), tree.getPath()); + } + } + } + + // 设置定额节点的sourceCatalogItemId和sourceCatalogPath + Map doMap = list.stream() + .collect(Collectors.toMap(WbBoqDivisionDO::getId, node -> node)); + for (WbBoqDivisionRespVO vo : voList) { + if (WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(vo.getNodeType())) { + WbBoqDivisionDO doDO = doMap.get(vo.getId()); + if (doDO != null && doDO.getSourceQuotaItemId() != null) { + Long catalogItemId = quotaItemToCatalogMap.get(doDO.getSourceQuotaItemId()); + vo.setSourceCatalogItemId(catalogItemId); + if (catalogItemId != null) { + String[] catalogPath = catalogItemToPathMap.get(catalogItemId); + vo.setSourceCatalogPath(catalogPath); + log.info("[getTree] 定额节点: {}, catalogItemId: {}, catalogPath: {}", + vo.getName(), catalogItemId, java.util.Arrays.toString(catalogPath)); + } + } + } + } + + return buildTreeFromVOList(voList); + } + + @Override + public List getTree(Long compileTreeId, Boolean excludeZeroQtyBoq) { + List tree = getTree(compileTreeId); + if (Boolean.TRUE.equals(excludeZeroQtyBoq)) { + filterZeroQtyBoqNodes(tree); + } + return tree; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void swapSort(WbBoqDivisionSwapSortReqVO swapReqVO) { + // 1. 校验两个节点存在 + WbBoqDivisionDO node1 = validateNodeExists(swapReqVO.getId1()); + WbBoqDivisionDO node2 = validateNodeExists(swapReqVO.getId2()); + + // 2. 校验是否同级 + if (!Objects.equals(node1.getParentId(), node2.getParentId())) { + throw exception(WB_BOQ_DIVISION_NOT_SAME_LEVEL); + } + + // 3. 交换排序号 + Integer tempSortOrder = node1.getSortOrder(); + node1.setSortOrder(node2.getSortOrder()); + node2.setSortOrder(tempSortOrder); + + wbBoqDivisionMapper.updateById(node1); + wbBoqDivisionMapper.updateById(node2); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteByCompileTreeId(Long compileTreeId) { + // 1. 查询所有定额节点 + List quotaNodes = wbBoqDivisionMapper.selectListByNodeType( + compileTreeId, WbBoqDivisionDO.NODE_TYPE_QUOTA); + + // 2. 删除所有定额节点关联的工料机 + for (WbBoqDivisionDO quotaNode : quotaNodes) { + wbBoqResourceMapper.deleteByDivisionId(quotaNode.getId()); + } + + // 3. 删除所有分部分项节点 + List allNodes = wbBoqDivisionMapper.selectListByCompileTreeId(compileTreeId); + for (WbBoqDivisionDO node : allNodes) { + wbBoqDivisionMapper.deleteById(node.getId()); + } + } + + @Override + public List getTreeWithPrice(Long compileTreeId) { + return getTreeWithPrice(compileTreeId, false); + } + + @Override + public List getTreeWithPrice(Long compileTreeId, boolean debug) { + // 1. 获取所有节点 + List list = wbBoqDivisionMapper.selectListByCompileTreeId(compileTreeId); + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + + // 2. 转换为VO + List voList = list.stream() + .map(WbBoqDivisionConvert.INSTANCE::convert) + .collect(Collectors.toList()); + + // 3. 获取所有定额节点ID和sourceQuotaItemId + List quotaDivisionIds = list.stream() + .filter(node -> WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(node.getNodeType())) + .map(WbBoqDivisionDO::getId) + .collect(Collectors.toList()); + + // 3.1 批量查询定额项的catalogItemId,用于取费章节过滤 + List quotaItemIds = list.stream() + .filter(node -> WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(node.getNodeType())) + .filter(node -> node.getSourceQuotaItemId() != null) + .map(WbBoqDivisionDO::getSourceQuotaItemId) + .distinct() + .collect(Collectors.toList()); + Map quotaItemToCatalogMap = new java.util.HashMap<>(); + if (CollUtil.isNotEmpty(quotaItemIds)) { + List quotaItems = + quotaItemMapper.selectBatchIds(quotaItemIds); + for (com.yhy.module.core.dal.dataobject.quota.QuotaItemDO item : quotaItems) { + if (item.getCatalogItemId() != null) { + quotaItemToCatalogMap.put(item.getId(), item.getCatalogItemId()); + } + } + } + + // 3.2 批量查询定额子目录的path,用于取费章节过滤(支持父节点匹配) + List catalogItemIds = new java.util.ArrayList<>(quotaItemToCatalogMap.values()); + Map catalogItemToPathMap = new java.util.HashMap<>(); + if (CollUtil.isNotEmpty(catalogItemIds)) { + List catalogTrees = + quotaCatalogTreeMapper.selectBatchIds(catalogItemIds); + for (com.yhy.module.core.dal.dataobject.quota.QuotaCatalogTreeDO tree : catalogTrees) { + if (tree.getPath() != null) { + catalogItemToPathMap.put(tree.getId(), tree.getPath()); + } + } + } + + // 4. 批量计算定额单价 + Map priceResultMap = new java.util.HashMap<>(); + if (CollUtil.isNotEmpty(quotaDivisionIds)) { + log.debug("[getTreeWithPrice] 开始计算定额单价 - 定额节点数量={}", quotaDivisionIds.size()); + + // 构建 divisionId -> quotaItemId 的映射 + Map divisionQuotaMap = list.stream() + .filter(node -> WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(node.getNodeType())) + .filter(node -> node.getSourceQuotaItemId() != null) + .collect(Collectors.toMap(WbBoqDivisionDO::getId, WbBoqDivisionDO::getSourceQuotaItemId)); + + // 逐个计算定额单价(后续可优化为批量计算) + for (Long divisionId : quotaDivisionIds) { + Long quotaItemId = divisionQuotaMap.get(divisionId); + if (quotaItemId != null) { + QuotaUnitPriceResult result = quotaPriceCalculatorService + .calculateQuotaUnitPrice(quotaItemId, divisionId, debug); + priceResultMap.put(divisionId, result); + } + } + + log.debug("[getTreeWithPrice] 定额单价计算完成 - 成功数量={}", priceResultMap.size()); + } + + // 5. 设置定额节点的单价和合价 + Map doMap = list.stream() + .collect(Collectors.toMap(WbBoqDivisionDO::getId, node -> node)); + + for (WbBoqDivisionRespVO vo : voList) { + if (WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(vo.getNodeType())) { + // 设置sourceCatalogItemId和sourceCatalogPath(用于取费章节过滤) + WbBoqDivisionDO doDO = doMap.get(vo.getId()); + if (doDO != null && doDO.getSourceQuotaItemId() != null) { + Long catalogItemId = quotaItemToCatalogMap.get(doDO.getSourceQuotaItemId()); + vo.setSourceCatalogItemId(catalogItemId); + // 设置子目录路径(包含所有祖先节点ID) + if (catalogItemId != null) { + String[] catalogPath = catalogItemToPathMap.get(catalogItemId); + vo.setSourceCatalogPath(catalogPath); + } + } + + QuotaUnitPriceResult priceResult = priceResultMap.get(vo.getId()); + if (priceResult != null && priceResult.getSuccess()) { + vo.setUnitPrice(priceResult.getUnitPrice()); + + // 计算合价 = 工程量 × 单价 + if (doDO != null && doDO.getQty() != null) { + BigDecimal amount = doDO.getQty().multiply(priceResult.getUnitPrice()); + vo.setAmount(amount); + } + + // 如果是调试模式,设置调试信息 + if (debug && priceResult.getFeeItemDetails() != null) { + // 可以将调试信息存储到VO的扩展字段中 + // vo.setPriceDebugInfo(priceResult); + } + } else { + // 计算失败,设置为0 + vo.setUnitPrice(BigDecimal.ZERO); + vo.setAmount(BigDecimal.ZERO); + if (priceResult != null) { + log.warn("[getTreeWithPrice] 定额单价计算失败 - divisionId={}, error={}", + vo.getId(), priceResult.getErrorMessage()); + } + } + } else if ("unified_fee".equals(vo.getNodeType())) { + // 统一取费节点单价计算 + QuotaUnitPriceResult priceResult = quotaPriceCalculatorService + .calculateUnifiedFeeParentPrice(vo.getId(), debug); + if (priceResult != null && Boolean.TRUE.equals(priceResult.getSuccess())) { + vo.setUnitPrice(priceResult.getUnitPrice()); + + // 统一取费节点的工程量默认为1 + WbBoqDivisionDO doDO = doMap.get(vo.getId()); + BigDecimal qty = (doDO != null && doDO.getQty() != null) ? doDO.getQty() : BigDecimal.ONE; + BigDecimal amount = qty.multiply(priceResult.getUnitPrice()); + vo.setAmount(amount); + } else { + vo.setUnitPrice(BigDecimal.ZERO); + vo.setAmount(BigDecimal.ZERO); + if (priceResult != null) { + log.warn("[getTreeWithPrice] 统一取费单价计算失败 - divisionId={}, error={}", + vo.getId(), priceResult.getErrorMessage()); + } + } + } + } + + // 6. 构建父子关系(提前到这里,以便后续计算能使用 children) + Map> parentIdMap = voList.stream() + .filter(vo -> vo.getParentId() != null) + .collect(Collectors.groupingBy(WbBoqDivisionRespVO::getParentId)); + + // 对子节点按sortOrder排序 + voList.forEach(vo -> { + List children = parentIdMap.get(vo.getId()); + if (children != null) { + children.sort((a, b) -> Integer.compare( + a.getSortOrder() != null ? a.getSortOrder() : 0, + b.getSortOrder() != null ? b.getSortOrder() : 0)); + } + vo.setChildren(children); + }); + + // 7. 汇总清单和分部的合价 + calculateParentAmounts(voList, parentIdMap); + + // 7.5 实时计算单独清单的工程量(无子节点的 boq 类型节点) + // 在构建父子关系和汇总合价之后,以便费用代号变量能正确求值 + List standaloneBoqList = voList.stream() + .filter(vo -> WbBoqDivisionDO.NODE_TYPE_BOQ.equals(vo.getNodeType())) + .filter(vo -> CollUtil.isEmpty(vo.getChildren())) // 无子节点 + .filter(vo -> { + // 检查是否有基数公式 + Map attributes = vo.getAttributes(); + if (attributes == null) return false; + Object baseNumberObj = attributes.get("baseNumber"); + if (!(baseNumberObj instanceof Map)) return false; + Map baseNumber = (Map) baseNumberObj; + String formula = (String) baseNumber.get("formula"); + return StrUtil.isNotBlank(formula); + }) + .collect(Collectors.toList()); + + if (CollUtil.isNotEmpty(standaloneBoqList)) { + log.info("[getTreeWithPrice] 发现 {} 个单独清单需要计算工程量", standaloneBoqList.size()); + // 获取根节点列表(已构建父子关系) + List treeWithChildren = voList.stream() + .filter(vo -> vo.getParentId() == null) + .collect(Collectors.toList()); + for (WbBoqDivisionRespVO vo : standaloneBoqList) { + WbBoqDivisionDO doDO = doMap.get(vo.getId()); + if (doDO != null) { + try { + // 传入已构建父子关系的树数据,以便费用代号变量能正确求值 + BigDecimal calculatedQty = calculateStandaloneBoqQty(compileTreeId, doDO, treeWithChildren); + if (calculatedQty != null) { + // 更新数据库 + doDO.setQty(calculatedQty); + wbBoqDivisionMapper.updateById(doDO); + // 更新VO(用于立即显示) + vo.setQty(calculatedQty); + log.info("[getTreeWithPrice] 单独清单 {} 工程量计算完成: {}", vo.getId(), calculatedQty); + } + } catch (Exception e) { + log.warn("[getTreeWithPrice] 单独清单 {} 工程量计算失败: {}", vo.getId(), e.getMessage()); + } + } + } + } + + // 8. 返回根节点(分部节点) + return voList.stream() + .filter(vo -> vo.getParentId() == null) + .collect(Collectors.toList()); + } + + /** + * 递归计算父节点的合价和单价 + * - 定额/统一取费:合价已在前面步骤计算完成 + * - 清单合价 = 子定额合价之和(含统一取费等) + * - 清单单价 = 清单合价 / 清单工程量(无工程量或为0时单价为空) + * - 分部合价 = 子节点(清单或子分部)合价之和 + * - 根节点合价 = 子分部合价之和 + * + * 使用递归方式从叶子节点向上汇总,支持多级分部结构 + */ + private void calculateParentAmounts(List voList, + Map> parentIdMap) { + // 找到根节点,从根节点开始递归计算 + List rootNodes = voList.stream() + .filter(vo -> vo.getParentId() == null) + .collect(Collectors.toList()); + + for (WbBoqDivisionRespVO root : rootNodes) { + calculateNodeAmount(root); + } + } + + /** + * 递归计算单个节点的合价(深度优先,先计算子节点再计算当前节点) + * 直接使用 node.getChildren() 获取子节点,因为父子关系已在之前设置 + * 合价保留2位小数,四舍五入 + */ + private BigDecimal calculateNodeAmount(WbBoqDivisionRespVO node) { + String nodeType = node.getNodeType(); + List children = node.getChildren(); + + // 定额和统一取费节点:合价已计算,保留2位小数后返回 + if (WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(nodeType) + || WbBoqDivisionDO.NODE_TYPE_UNIFIED_FEE.equals(nodeType)) { + BigDecimal amount = node.getAmount() != null ? node.getAmount() : BigDecimal.ZERO; + amount = amount.setScale(2, java.math.RoundingMode.HALF_UP); + node.setAmount(amount); + // 定额单价保留2位小数 + if (node.getUnitPrice() != null) { + node.setUnitPrice(node.getUnitPrice().setScale(2, java.math.RoundingMode.HALF_UP)); + } + // 定额工程量保留3位小数 + if (node.getQty() != null) { + node.setQty(node.getQty().setScale(3, java.math.RoundingMode.HALF_UP)); + } + return amount; + } + + // 清单节点 + if (WbBoqDivisionDO.NODE_TYPE_BOQ.equals(nodeType)) { + // 有子节点的清单:合价 = 子定额合价之和 + if (CollUtil.isNotEmpty(children)) { + BigDecimal totalAmount = BigDecimal.ZERO; + for (WbBoqDivisionRespVO child : children) { + BigDecimal childAmount = calculateNodeAmount(child); + totalAmount = totalAmount.add(childAmount); + } + // 合价保留2位小数 + totalAmount = totalAmount.setScale(2, java.math.RoundingMode.HALF_UP); + node.setAmount(totalAmount); + + // 清单工程量保留3位小数 + BigDecimal qty = node.getQty(); + if (qty != null) { + qty = qty.setScale(3, java.math.RoundingMode.HALF_UP); + node.setQty(qty); + } + // 清单单价 = 合价 / 工程量(无工程量或为0时单价为空),单价保留2位小数 + if (qty != null && qty.compareTo(BigDecimal.ZERO) != 0) { + BigDecimal unitPrice = totalAmount.divide(qty, 10, java.math.RoundingMode.HALF_UP); + unitPrice = unitPrice.setScale(2, java.math.RoundingMode.HALF_UP); + node.setUnitPrice(unitPrice); + } else { + node.setUnitPrice(null); + } + return totalAmount; + } + + // 单独清单(无子节点):合价 = 工程量 × 费率% + // 工程量已在保存时由基数公式计算并存储 + BigDecimal qty = node.getQty(); + BigDecimal rate = node.getRate(); + if (qty != null) { + qty = qty.setScale(3, java.math.RoundingMode.HALF_UP); + node.setQty(qty); + } + if (qty != null && rate != null) { + // 合价 = 工程量 × 费率%(费率是百分比,需要除以100) + BigDecimal amount = qty.multiply(rate).divide(new BigDecimal("100"), 2, java.math.RoundingMode.HALF_UP); + node.setAmount(amount); + return amount; + } + BigDecimal amount = node.getAmount() != null ? node.getAmount() : BigDecimal.ZERO; + amount = amount.setScale(2, java.math.RoundingMode.HALF_UP); + node.setAmount(amount); + return amount; + } + + // 分部节点或根节点:合价 = 子节点(清单或子分部)合价之和 + if (WbBoqDivisionDO.NODE_TYPE_DIVISION.equals(nodeType) + || WbBoqDivisionDO.NODE_TYPE_ROOT.equals(nodeType)) { + if (CollUtil.isNotEmpty(children)) { + BigDecimal totalAmount = BigDecimal.ZERO; + for (WbBoqDivisionRespVO child : children) { + BigDecimal childAmount = calculateNodeAmount(child); + totalAmount = totalAmount.add(childAmount); + } + // 合价保留2位小数 + totalAmount = totalAmount.setScale(2, java.math.RoundingMode.HALF_UP); + node.setAmount(totalAmount); + return totalAmount; + } + BigDecimal amount = node.getAmount() != null ? node.getAmount() : BigDecimal.ZERO; + amount = amount.setScale(2, java.math.RoundingMode.HALF_UP); + node.setAmount(amount); + return amount; + } + + // 其他类型节点,返回已有合价(保留2位小数) + BigDecimal amount = node.getAmount() != null ? node.getAmount() : BigDecimal.ZERO; + amount = amount.setScale(2, java.math.RoundingMode.HALF_UP); + node.setAmount(amount); + return amount; + } + + /** + * 校验编制模式树节点是否为单位工程 + */ + private void validateCompileTreeIsUnit(Long compileTreeId) { + WbCompileTreeDO compileTree = wbCompileTreeMapper.selectById(compileTreeId); + if (compileTree == null || !WbCompileTreeDO.NODE_TYPE_UNIT.equals(compileTree.getNodeType())) { + throw exception(WB_BOQ_DIVISION_COMPILE_TREE_NOT_UNIT); + } + } + + /** + * 校验节点类型 + */ + private void validateNodeType(String nodeType) { + if (!WbBoqDivisionDO.NODE_TYPE_ROOT.equals(nodeType) + && !WbBoqDivisionDO.NODE_TYPE_DIVISION.equals(nodeType) + && !WbBoqDivisionDO.NODE_TYPE_BOQ.equals(nodeType) + && !WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(nodeType)) { + throw exception(WB_BOQ_DIVISION_NODE_TYPE_INVALID); + } + } + + /** + * 校验父节点存在 + */ + private WbBoqDivisionDO validateParentExists(Long parentId) { + WbBoqDivisionDO parent = wbBoqDivisionMapper.selectById(parentId); + if (parent == null) { + throw exception(WB_BOQ_DIVISION_PARENT_NOT_EXISTS); + } + return parent; + } + + /** + * 校验层级关系 + */ + private void validateHierarchy(String nodeType, WbBoqDivisionDO parent) { + // 定额是最后一级,不能在定额下创建普通子节点(但允许unified_fee节点) + if (parent != null && WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(parent.getNodeType())) { + if (!WbBoqDivisionDO.NODE_TYPE_UNIFIED_FEE.equals(nodeType)) { + throw exception0(400, "定额是最后一级节点,不能在定额下创建子节点"); + } + } + + if (WbBoqDivisionDO.NODE_TYPE_ROOT.equals(nodeType)) { + // 根目录只能作为顶级节点(无父节点) + if (parent != null) { + throw exception(WB_BOQ_DIVISION_DIVISION_MUST_ROOT); + } + } else if (WbBoqDivisionDO.NODE_TYPE_DIVISION.equals(nodeType)) { + // 分部可以在根目录或其他分部下创建(支持子分部嵌套),或作为顶级节点(兼容旧数据) + if (parent != null + && !WbBoqDivisionDO.NODE_TYPE_ROOT.equals(parent.getNodeType()) + && !WbBoqDivisionDO.NODE_TYPE_DIVISION.equals(parent.getNodeType())) { + throw exception(WB_BOQ_DIVISION_DIVISION_MUST_ROOT); + } + } else if (WbBoqDivisionDO.NODE_TYPE_BOQ.equals(nodeType)) { + // 清单可以在根目录或分部下创建 + if (parent == null || + (!WbBoqDivisionDO.NODE_TYPE_ROOT.equals(parent.getNodeType()) + && !WbBoqDivisionDO.NODE_TYPE_DIVISION.equals(parent.getNodeType()))) { + throw exception(WB_BOQ_DIVISION_BOQ_PARENT_MUST_DIVISION); + } + } else if (WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(nodeType)) { + // 定额只能在清单下创建 + if (parent == null || !WbBoqDivisionDO.NODE_TYPE_BOQ.equals(parent.getNodeType())) { + throw exception(WB_BOQ_DIVISION_QUOTA_PARENT_MUST_BOQ); + } + } + } + + /** + * 校验节点存在 + */ + private WbBoqDivisionDO validateNodeExists(Long id) { + WbBoqDivisionDO node = wbBoqDivisionMapper.selectById(id); + if (node == null) { + throw exception(WB_BOQ_DIVISION_NOT_EXISTS); + } + return node; + } + + /** + * 校验清单编码在同一单位工程下唯一 + * @param compileTreeId 单位工程ID + * @param code 清单编码 + * @param excludeId 排除的节点ID(更新时排除自身) + */ + private void validateBoqCodeUnique(Long compileTreeId, String code, Long excludeId) { + Long count = wbBoqDivisionMapper.countBoqByCodeAndCompileTreeId(compileTreeId, code, excludeId); + if (count > 0) { + throw exception0(400, "清单编码[" + code + "]在当前单位工程下已存在,请使用其他编码"); + } + } + + /** + * 构建路径 + */ + private String[] buildPath(WbBoqDivisionDO parent) { + if (parent == null || parent.getPath() == null) { + return new String[]{}; + } + return parent.getPath(); + } + + /** + * 更新路径(包含自身ID) + */ + private void updatePathWithSelfId(WbBoqDivisionDO node) { + List pathList = node.getPath() != null + ? new ArrayList<>(Arrays.asList(node.getPath())) + : new ArrayList<>(); + pathList.add(String.valueOf(node.getId())); + node.setPath(pathList.toArray(new String[0])); + wbBoqDivisionMapper.updateById(node); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long copyFromGuide(Long compileTreeId, Long parentId, Long boqSubItemId, Long sourceBoqItemTreeId) { + // 1. 校验单位工程节点 + validateCompileTreeIsUnit(compileTreeId); + + // 2. 校验父节点必须是分部 + WbBoqDivisionDO parent = validateParentExists(parentId); + if (!WbBoqDivisionDO.NODE_TYPE_DIVISION.equals(parent.getNodeType())) { + throw exception(WB_BOQ_DIVISION_BOQ_PARENT_MUST_DIVISION); + } + + // 3. 获取清单子目信息(忽略租户,标准库数据) + com.yhy.module.core.dal.dataobject.boq.BoqSubItemDO subItem = + cn.iocoder.yudao.framework.tenant.core.util.TenantUtils.executeIgnore( + () -> boqSubItemMapper.selectById(boqSubItemId)); + if (subItem == null) { + throw exception0(400, "清单子目不存在"); + } + + // 4. 创建清单(boq)节点 + WbBoqDivisionSaveReqVO boqReqVO = new WbBoqDivisionSaveReqVO(); + boqReqVO.setCompileTreeId(compileTreeId); + boqReqVO.setParentId(parentId); + boqReqVO.setNodeType(WbBoqDivisionDO.NODE_TYPE_BOQ); + boqReqVO.setCode(subItem.getCode()); + boqReqVO.setName(subItem.getName()); + boqReqVO.setUnit(subItem.getUnit()); + boqReqVO.setFeature(subItem.getFeatures()); // 复制清单子目的项目特征 + boqReqVO.setSourceBoqItemTreeId(sourceBoqItemTreeId); + Long newBoqId = createNode(boqReqVO); + log.info("[copyFromGuide] 创建清单节点, boqSubItemId={}, newBoqId={}", boqSubItemId, newBoqId); + + // 5. 加载指引树中的定额节点并逐个创建(忽略租户,标准库数据) + List guideNodes = + cn.iocoder.yudao.framework.tenant.core.util.TenantUtils.executeIgnore( + () -> boqGuideTreeMapper.selectListByBoqSubItemId(boqSubItemId)); + int quotaCount = 0; + for (com.yhy.module.core.dal.dataobject.boq.BoqGuideTreeDO guideNode : guideNodes) { + if (!"quota".equals(guideNode.getNodeType()) || guideNode.getQuotaCatalogItemId() == null) { + continue; + } + try { + WbBoqDivisionSaveReqVO quotaReqVO = new WbBoqDivisionSaveReqVO(); + quotaReqVO.setCompileTreeId(compileTreeId); + quotaReqVO.setParentId(newBoqId); + quotaReqVO.setNodeType(WbBoqDivisionDO.NODE_TYPE_QUOTA); + quotaReqVO.setCode(guideNode.getCode()); + quotaReqVO.setName(guideNode.getName()); + quotaReqVO.setUnit(guideNode.getUnit()); + quotaReqVO.setSourceQuotaItemId(guideNode.getQuotaCatalogItemId()); + createNode(quotaReqVO); + quotaCount++; + } catch (Exception e) { + log.warn("[copyFromGuide] 创建定额节点失败, guideNodeId={}, error={}", guideNode.getId(), e.getMessage()); + } + } + log.info("[copyFromGuide] 完成, newBoqId={}, 定额数={}", newBoqId, quotaCount); + return newBoqId; + } + + /** + * 构建树形结构 + */ + private List buildTree(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + + // 转换为VO + List voList = list.stream() + .map(WbBoqDivisionConvert.INSTANCE::convert) + .collect(Collectors.toList()); + + return buildTreeFromVOList(voList); + } + + /** + * 从VO列表构建树形结构 + */ + private List buildTreeFromVOList(List voList) { + if (CollUtil.isEmpty(voList)) { + return Collections.emptyList(); + } + + // 构建父子关系 + Map> parentIdMap = voList.stream() + .filter(vo -> vo.getParentId() != null) + .collect(Collectors.groupingBy(WbBoqDivisionRespVO::getParentId)); + + // 对子节点按sortOrder排序 + voList.forEach(vo -> { + List children = parentIdMap.get(vo.getId()); + if (children != null) { + children.sort((a, b) -> Integer.compare( + a.getSortOrder() != null ? a.getSortOrder() : 0, + b.getSortOrder() != null ? b.getSortOrder() : 0)); + } + vo.setChildren(children); + }); + + // 返回根节点(分部节点) + return voList.stream() + .filter(vo -> vo.getParentId() == null) + .collect(Collectors.toList()); + } + + /** + * 获取节点类型的排序优先级 + */ + private int getNodeTypeOrder(String nodeType) { + if (WbBoqDivisionDO.NODE_TYPE_DIVISION.equals(nodeType)) { + return 1; + } else if (WbBoqDivisionDO.NODE_TYPE_BOQ.equals(nodeType)) { + return 2; + } else if (WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(nodeType)) { + return 3; + } + return 0; + } + + /** + * 从定额基价复制工料机数据到分部分项工料机表 + * + * @param divisionId 分部分项定额节点ID + * @param tenantId 租户ID + * @param quotaItemId 定额基价ID + */ + private void copyQuotaResources(Long divisionId, Long tenantId, Long quotaItemId) { + // 1. 查询定额基价的工料机组成 + List quotaResources = quotaResourceMapper.selectListByQuotaItemId(quotaItemId); + if (CollUtil.isEmpty(quotaResources)) { + log.info("定额基价 {} 没有工料机组成数据", quotaItemId); + return; + } + + // 2. 批量查询资源项信息 + List resourceItemIds = quotaResources.stream() + .map(QuotaResourceDO::getResourceItemId) + .collect(Collectors.toList()); + Map resourceItemMap = resourceItemMapper.selectBatchIds(resourceItemIds) + .stream() + .collect(Collectors.toMap(ResourceItemDO::getId, r -> r)); + + // 3. 转换并插入分部分项工料机表 + int sortOrder = 0; + int childCount = 0; + for (QuotaResourceDO quotaResource : quotaResources) { + ResourceItemDO resourceItem = resourceItemMap.get(quotaResource.getResourceItemId()); + + // 判断是否为复合工料机 + boolean isMerged = resourceItem != null && resourceItem.getIsMerged() != null && resourceItem.getIsMerged() == 1; + + WbBoqResourceDO wbResource = WbBoqResourceDO.builder() + .tenantId(tenantId) + .divisionId(divisionId) + .sourceResourceItemId(quotaResource.getResourceItemId()) + .sourceQuotaResourceId(quotaResource.getId()) + .resourceType(resourceItem != null ? resourceItem.getType() : null) + .code(resourceItem != null ? resourceItem.getCode() : null) + .name(resourceItem != null ? resourceItem.getName() : null) + .spec(resourceItem != null ? resourceItem.getSpec() : null) + .unit(resourceItem != null ? resourceItem.getUnit() : null) + .consumeQty(quotaResource.getAdjustedDosage() != null + ? quotaResource.getAdjustedDosage() + : quotaResource.getDosage()) + .baseConsumeQty(quotaResource.getDosage()) + // 普通工料机复制价格字段,复合工料机价格为空(由子项汇总) + .taxRate(!isMerged && resourceItem != null ? resourceItem.getTaxRate() : null) + .taxExclBasePrice(!isMerged && resourceItem != null ? resourceItem.getTaxExclBasePrice() : null) + .taxInclBasePrice(!isMerged && resourceItem != null ? resourceItem.getTaxInclBasePrice() : null) + .taxExclCompilePrice(!isMerged && resourceItem != null ? resourceItem.getTaxExclCompilePrice() : null) + .taxInclCompilePrice(!isMerged && resourceItem != null ? resourceItem.getTaxInclCompilePrice() : null) + .categoryId(resourceItem != null ? resourceItem.getCategoryId() : null) + .sortOrder(++sortOrder) + .sourceType(WbBoqResourceDO.SOURCE_TYPE_SYSTEM) + .build(); + + wbBoqResourceMapper.insert(wbResource); + + // 4. 如果是复合工料机,复制子数据 + if (resourceItem != null && resourceItem.getIsMerged() != null && resourceItem.getIsMerged() == 1) { + List mergedList = resourceMergedMapper.selectByMergedId(resourceItem.getId()); + if (CollUtil.isNotEmpty(mergedList)) { + int childSortOrder = 0; + for (ResourceMergedDO merged : mergedList) { + ResourceItemDO childResourceItem = resourceItemMapper.selectById(merged.getSourceId()); + if (childResourceItem == null) { + continue; + } + + // 子项定额消耗量 = 原定额消耗量 × 父定额消耗量 + BigDecimal childDosage = merged.getQuotaConsumption(); + BigDecimal parentDosage = quotaResource.getDosage(); + BigDecimal effectiveChildDosage = childDosage; + if (childDosage != null && parentDosage != null) { + effectiveChildDosage = childDosage.multiply(parentDosage); + } + + WbBoqResourceDO childResource = WbBoqResourceDO.builder() + .tenantId(tenantId) + .divisionId(divisionId) + .parentId(wbResource.getId()) + .sourceResourceItemId(merged.getSourceId()) + .sourceQuotaResourceId(merged.getId()) + .resourceType(childResourceItem.getType()) + .code(childResourceItem.getCode()) + .name(childResourceItem.getName()) + .spec(childResourceItem.getSpec()) + .unit(childResourceItem.getUnit()) + .consumeQty(effectiveChildDosage) + .baseConsumeQty(effectiveChildDosage) + .taxRate(childResourceItem.getTaxRate()) + .taxExclBasePrice(childResourceItem.getTaxExclBasePrice()) + .taxInclBasePrice(childResourceItem.getTaxInclBasePrice()) + .taxExclCompilePrice(childResourceItem.getTaxExclCompilePrice()) + .taxInclCompilePrice(childResourceItem.getTaxInclCompilePrice()) + .sortOrder(++childSortOrder) + .sourceType(WbBoqResourceDO.SOURCE_TYPE_SYSTEM) + .build(); + + wbBoqResourceMapper.insert(childResource); + childCount++; + } + } + } + } + + log.info("从定额基价 {} 复制了 {} 条工料机数据(含 {} 条子数据)到分部分项定额节点 {}", + quotaItemId, quotaResources.size(), childCount, divisionId); + + // 5. 复制市场主材设备 + copyQuotaMarketMaterials(divisionId, tenantId, quotaItemId); + } + + /** + * 从定额基价复制市场主材设备到分部分项 + */ + private void copyQuotaMarketMaterials(Long divisionId, Long tenantId, Long quotaItemId) { + // 1. 查询定额基价的市场主材设备 + List quotaMaterials = quotaMarketMaterialMapper.selectListByQuotaItemId(quotaItemId); + if (CollUtil.isEmpty(quotaMaterials)) { + return; + } + + // 2. 批量查询资源项信息 + List resourceItemIds = quotaMaterials.stream() + .map(QuotaMarketMaterialDO::getResourceItemId) + .collect(Collectors.toList()); + Map resourceItemMap = resourceItemMapper.selectBatchIds(resourceItemIds) + .stream() + .collect(Collectors.toMap(ResourceItemDO::getId, r -> r)); + + // 3. 转换并插入 + int sortOrder = 0; + for (QuotaMarketMaterialDO quotaMaterial : quotaMaterials) { + ResourceItemDO resourceItem = resourceItemMap.get(quotaMaterial.getResourceItemId()); + + WbBoqMarketMaterialDO wbMaterial = WbBoqMarketMaterialDO.builder() + .tenantId(tenantId) + .divisionId(divisionId) + .sourceResourceItemId(quotaMaterial.getResourceItemId()) + .sourceMarketMaterialId(quotaMaterial.getId()) + .resourceType(resourceItem != null ? resourceItem.getType() : null) + .code(resourceItem != null ? resourceItem.getCode() : null) + .name(resourceItem != null ? resourceItem.getName() : null) + .spec(resourceItem != null ? resourceItem.getSpec() : null) + .unit(resourceItem != null ? resourceItem.getUnit() : null) + .consumeQty(quotaMaterial.getAdjustedDosage() != null + ? quotaMaterial.getAdjustedDosage() + : quotaMaterial.getDosage()) + .baseConsumeQty(quotaMaterial.getDosage()) + .taxRate(resourceItem != null ? resourceItem.getTaxRate() : null) + .taxExclBasePrice(resourceItem != null ? resourceItem.getTaxExclBasePrice() : null) + .taxInclBasePrice(resourceItem != null ? resourceItem.getTaxInclBasePrice() : null) + .taxExclCompilePrice(resourceItem != null ? resourceItem.getTaxExclCompilePrice() : null) + .taxInclCompilePrice(resourceItem != null ? resourceItem.getTaxInclCompilePrice() : null) + .categoryId(resourceItem != null ? resourceItem.getCategoryId() : null) + .sortOrder(++sortOrder) + .sourceType(WbBoqMarketMaterialDO.SOURCE_TYPE_SYSTEM) + .build(); + + wbBoqMarketMaterialMapper.insert(wbMaterial); + } + + log.info("从定额基价 {} 复制了 {} 条市场主材设备到分部分项定额节点 {}", + quotaItemId, quotaMaterials.size(), divisionId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createRootNode(Long compileTreeId, String unitName) { + // 1. 校验单位工程节点 + validateCompileTreeIsUnit(compileTreeId); + + // 2. 检查是否已存在根目录节点 + List existingRoots = wbBoqDivisionMapper.selectListByNodeType( + compileTreeId, WbBoqDivisionDO.NODE_TYPE_ROOT); + if (!existingRoots.isEmpty()) { + log.warn("单位工程 {} 已存在根目录节点,跳过创建", compileTreeId); + return existingRoots.get(0).getId(); + } + + // 3. 创建根目录节点(名称固定为"单位工程") + WbBoqDivisionDO rootNode = WbBoqDivisionDO.builder() + .compileTreeId(compileTreeId) + .parentId(null) + .nodeType(WbBoqDivisionDO.NODE_TYPE_ROOT) + .sourceType(WbBoqDivisionDO.SOURCE_TYPE_SYSTEM) + .name("单位工程") + .sortOrder(0) + .path(new String[]{}) + .build(); + + wbBoqDivisionMapper.insert(rootNode); + + // 4. 更新路径(包含自身ID) + updatePathWithSelfId(rootNode); + + log.info("为单位工程 {} 创建了分部分项根目录节点 {}", compileTreeId, rootNode.getId()); + return rootNode.getId(); + } + + @Override + public List getAllBoqNodes() { + List boqNodes = wbBoqDivisionMapper.selectAllBoqNodes(); + return boqNodes.stream() + .map(WbBoqDivisionConvert.INSTANCE::convert) + .collect(Collectors.toList()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long copyBoqWithChildren(Long sourceBoqId, Long targetCompileTreeId, Long targetParentId) { + // 1. 校验源清单节点 + WbBoqDivisionDO sourceBoq = wbBoqDivisionMapper.selectById(sourceBoqId); + if (sourceBoq == null || !WbBoqDivisionDO.NODE_TYPE_BOQ.equals(sourceBoq.getNodeType())) { + throw exception(WB_BOQ_DIVISION_NOT_EXISTS); + } + + // 2. 校验目标单位工程 + validateCompileTreeIsUnit(targetCompileTreeId); + + // 3. 获取目标父节点的路径 + String[] parentPath = new String[]{}; + if (targetParentId != null) { + WbBoqDivisionDO parent = wbBoqDivisionMapper.selectById(targetParentId); + if (parent != null && parent.getPath() != null) { + parentPath = parent.getPath(); + } + } + + // 4. 获取排序号 + Integer maxSortOrder = wbBoqDivisionMapper.selectMaxSortOrderByParentId(targetCompileTreeId, targetParentId); + + // 5. 复制清单节点 + WbBoqDivisionDO newBoq = WbBoqDivisionDO.builder() + .compileTreeId(targetCompileTreeId) + .parentId(targetParentId) + .nodeType(WbBoqDivisionDO.NODE_TYPE_BOQ) + .sourceType(WbBoqDivisionDO.SOURCE_TYPE_COPY) + .sourceBoqCatalogId(sourceBoq.getSourceBoqCatalogId()) + .sourceBoqItemTreeId(sourceBoq.getSourceBoqItemTreeId()) + .code(sourceBoq.getCode()) + .name(sourceBoq.getName()) + .feature(sourceBoq.getFeature()) + .unit(sourceBoq.getUnit()) + .qty(sourceBoq.getQty()) + .lineNo(sourceBoq.getLineNo()) + .sortOrder(maxSortOrder + 1) + .baseCode(sourceBoq.getBaseCode()) + .baseName(sourceBoq.getBaseName()) + .baseUnit(sourceBoq.getBaseUnit()) + .snapshotJson(sourceBoq.getSnapshotJson()) + .attributes(sourceBoq.getAttributes()) + .build(); + + wbBoqDivisionMapper.insert(newBoq); + + // 6. 更新路径 + String[] newPath = Arrays.copyOf(parentPath, parentPath.length + 1); + newPath[parentPath.length] = String.valueOf(newBoq.getId()); + newBoq.setPath(newPath); + wbBoqDivisionMapper.updateById(newBoq); + + // 7. 复制子节点(定额) + List quotaNodes = wbBoqDivisionMapper.selectListByParentId(sourceBoqId); + for (WbBoqDivisionDO sourceQuota : quotaNodes) { + if (WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(sourceQuota.getNodeType())) { + copyQuotaNode(sourceQuota, newBoq.getId(), targetCompileTreeId, newPath); + } + } + + log.info("复制清单 {} 到目标位置 compileTreeId={}, parentId={}, 新清单ID={}", + sourceBoqId, targetCompileTreeId, targetParentId, newBoq.getId()); + return newBoq.getId(); + } + + /** + * 复制定额节点(含工料机) + */ + private void copyQuotaNode(WbBoqDivisionDO sourceQuota, Long newParentId, Long targetCompileTreeId, String[] parentPath) { + // 1. 获取排序号 + Integer maxSortOrder = wbBoqDivisionMapper.selectMaxSortOrderByParentId(targetCompileTreeId, newParentId); + + // 2. 复制定额节点 + WbBoqDivisionDO newQuota = WbBoqDivisionDO.builder() + .compileTreeId(targetCompileTreeId) + .parentId(newParentId) + .nodeType(WbBoqDivisionDO.NODE_TYPE_QUOTA) + .sourceType(WbBoqDivisionDO.SOURCE_TYPE_COPY) + .sourceQuotaItemId(sourceQuota.getSourceQuotaItemId()) + .code(sourceQuota.getCode()) + .name(sourceQuota.getName()) + .feature(sourceQuota.getFeature()) + .unit(sourceQuota.getUnit()) + .qty(sourceQuota.getQty()) + .lineNo(sourceQuota.getLineNo()) + .sortOrder(maxSortOrder + 1) + .baseCode(sourceQuota.getBaseCode()) + .baseName(sourceQuota.getBaseName()) + .baseUnit(sourceQuota.getBaseUnit()) + .snapshotJson(sourceQuota.getSnapshotJson()) + .attributes(sourceQuota.getAttributes()) + .build(); + + wbBoqDivisionMapper.insert(newQuota); + + // 3. 更新路径 + String[] newPath = Arrays.copyOf(parentPath, parentPath.length + 1); + newPath[parentPath.length] = String.valueOf(newQuota.getId()); + newQuota.setPath(newPath); + wbBoqDivisionMapper.updateById(newQuota); + + // 4. 复制工料机数据 + copyBoqResources(sourceQuota.getId(), newQuota.getId()); + + log.info("复制定额 {} 到新定额 {}", sourceQuota.getId(), newQuota.getId()); + } + + /** + * 复制工料机数据 + */ + private void copyBoqResources(Long sourceDivisionId, Long targetDivisionId) { + List sourceResources = wbBoqResourceMapper.selectListByDivisionId(sourceDivisionId); + if (CollUtil.isEmpty(sourceResources)) { + return; + } + + for (WbBoqResourceDO source : sourceResources) { + WbBoqResourceDO newResource = WbBoqResourceDO.builder() + .divisionId(targetDivisionId) + .parentId(null) // 父子关系后续处理 + .sourceResourceItemId(source.getSourceResourceItemId()) + .sourceQuotaResourceId(source.getSourceQuotaResourceId()) + .resourceType(source.getResourceType()) + .code(source.getCode()) + .name(source.getName()) + .spec(source.getSpec()) + .unit(source.getUnit()) + .categoryId(source.getCategoryId()) + .consumeQty(source.getConsumeQty()) + .taxExclBasePrice(source.getTaxExclBasePrice()) + .taxInclBasePrice(source.getTaxInclBasePrice()) + .taxExclCompilePrice(source.getTaxExclCompilePrice()) + .taxInclCompilePrice(source.getTaxInclCompilePrice()) + .taxRate(source.getTaxRate()) + .adjustRate(source.getAdjustRate()) + .sortOrder(source.getSortOrder()) + .sourceType(source.getSourceType()) + .baseConsumeQty(source.getBaseConsumeQty()) + .baseBasePrice(source.getBaseBasePrice()) + .usageQty(source.getUsageQty()) + .snapshotJson(source.getSnapshotJson()) + .attributes(source.getAttributes()) + .build(); + + wbBoqResourceMapper.insert(newResource); + } + + log.info("复制工料机 {} 条从分部分项 {} 到 {}", sourceResources.size(), sourceDivisionId, targetDivisionId); + } + + @Override + public PageResult getHistoryListPage(HistoryBoqListReqVO reqVO) { + // 0. 历史库过滤:只查询已保存至历史库的项目下的数据 + List historyProjectIds = wbProjectTreeMapper.selectList( + new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX() + .eq(com.yhy.module.core.dal.dataobject.workbench.WbProjectTreeDO::getInHistoryLibrary, true) + .eq(com.yhy.module.core.dal.dataobject.workbench.WbProjectTreeDO::getNodeType, "project") + ).stream().map(com.yhy.module.core.dal.dataobject.workbench.WbProjectTreeDO::getId).collect(Collectors.toList()); + + if (CollUtil.isEmpty(historyProjectIds)) { + return new PageResult<>(Collections.emptyList(), 0L); + } + + // 获取这些项目下的所有 compileTreeId + List historyCompileTreeIds = wbCompileTreeMapper.selectList( + new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX() + .in(WbCompileTreeDO::getProjectId, historyProjectIds) + ).stream().map(WbCompileTreeDO::getId).collect(Collectors.toList()); + + if (CollUtil.isEmpty(historyCompileTreeIds)) { + return new PageResult<>(Collections.emptyList(), 0L); + } + + // 1. 查询清单和分部节点(全量查询用于后续过滤和去重) + List nodes = wbBoqDivisionMapper.selectHistoryList(reqVO); + + // 1.1 过滤:只保留属于历史库项目的数据 + if (CollUtil.isNotEmpty(nodes)) { + nodes = nodes.stream() + .filter(node -> historyCompileTreeIds.contains(node.getCompileTreeId())) + .collect(Collectors.toList()); + } + + // 1.2 清单章节筛选(通过 sourceBoqItemTreeId 关联 BoqItemTree.boqCatalogItemId) + if (reqVO.getListChapter() != null && CollUtil.isNotEmpty(nodes)) { + // 收集所有有 sourceBoqItemTreeId 的节点 + List allBoqItemTreeIds = nodes.stream() + .map(WbBoqDivisionDO::getSourceBoqItemTreeId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + + if (CollUtil.isNotEmpty(allBoqItemTreeIds)) { + // 忽略租户查询清单项树,找出 boqCatalogItemId 匹配的 sourceBoqItemTreeId + final List finalAllBoqItemTreeIds = allBoqItemTreeIds; + final Long listChapterId = reqVO.getListChapter(); + java.util.Set matchedBoqItemTreeIds = cn.iocoder.yudao.framework.tenant.core.util.TenantUtils.executeIgnore(() -> { + List boqItemTrees = + boqItemTreeMapper.selectBatchIds(finalAllBoqItemTreeIds); + return boqItemTrees.stream() + .filter(item -> listChapterId.equals(item.getBoqCatalogItemId())) + .map(com.yhy.module.core.dal.dataobject.boq.BoqItemTreeDO::getId) + .collect(Collectors.toSet()); + }); + + nodes = nodes.stream() + .filter(node -> node.getSourceBoqItemTreeId() != null + && matchedBoqItemTreeIds.contains(node.getSourceBoqItemTreeId())) + .collect(Collectors.toList()); + } else { + nodes = Collections.emptyList(); + } + } + + // 2. 库类别过滤(通过 yhy_wb_unit_info 表关联) + if (StrUtil.isNotBlank(reqVO.getLibraryCategory()) && CollUtil.isNotEmpty(nodes)) { + List matchedCompileTreeIds = wbUnitInfoMapper.selectCompileTreeIdsByLibraryType(reqVO.getLibraryCategory()); + if (CollUtil.isNotEmpty(matchedCompileTreeIds)) { + nodes = nodes.stream() + .filter(node -> matchedCompileTreeIds.contains(node.getCompileTreeId())) + .collect(Collectors.toList()); + } else { + nodes = Collections.emptyList(); + } + } + + // 2.5 定额专业/定额章节筛选(通过子定额节点 → QuotaItem.catalog_item_id → QuotaCatalogTree) + if ((reqVO.getQuotaProfession() != null || reqVO.getQuotaChapter() != null) && CollUtil.isNotEmpty(nodes)) { + // 收集所有 boq/division 节点ID + List nodeIds = nodes.stream().map(WbBoqDivisionDO::getId).collect(Collectors.toList()); + + // 查询这些节点下的所有定额子节点 + List allQuotaNodes = wbBoqDivisionMapper.selectList( + new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX() + .in(WbBoqDivisionDO::getParentId, nodeIds) + .eq(WbBoqDivisionDO::getNodeType, WbBoqDivisionDO.NODE_TYPE_QUOTA) + .isNotNull(WbBoqDivisionDO::getSourceQuotaItemId)); + + if (CollUtil.isNotEmpty(allQuotaNodes)) { + // 收集所有 sourceQuotaItemId + List quotaItemIds = allQuotaNodes.stream() + .map(WbBoqDivisionDO::getSourceQuotaItemId) + .distinct() + .collect(Collectors.toList()); + + // 忽略租户查询 QuotaItem,获取 catalog_item_id(指向 QuotaCatalogTree 节点) + final List finalQuotaItemIds = quotaItemIds; + // Map: quotaItemId -> catalogTreeNodeId (QuotaCatalogTree.id) + Map quotaItemToCatalogTreeMap = cn.iocoder.yudao.framework.tenant.core.util.TenantUtils.executeIgnore(() -> { + List quotaItems = + quotaItemMapper.selectBatchIds(finalQuotaItemIds); + Map map = new java.util.HashMap<>(); + for (com.yhy.module.core.dal.dataobject.quota.QuotaItemDO qi : quotaItems) { + if (qi.getCatalogItemId() != null) { + map.put(qi.getId(), qi.getCatalogItemId()); + } + } + return map; + }); + + // 确定匹配的 sourceQuotaItemId 集合 + java.util.Set matchedQuotaItemIds = new java.util.HashSet<>(); + + if (reqVO.getQuotaChapter() != null) { + // 定额章节筛选:QuotaItem.catalog_item_id = quotaChapter(QuotaCatalogTree节点ID) + Long chapterId = reqVO.getQuotaChapter(); + for (Map.Entry entry : quotaItemToCatalogTreeMap.entrySet()) { + if (chapterId.equals(entry.getValue())) { + matchedQuotaItemIds.add(entry.getKey()); + } + } + } else if (reqVO.getQuotaProfession() != null) { + // 定额专业筛选:需要通过 QuotaCatalogTree.catalog_item_id 关联到 QuotaCatalogItem + java.util.Set catalogTreeNodeIds = new java.util.HashSet<>(quotaItemToCatalogTreeMap.values()); + if (CollUtil.isNotEmpty(catalogTreeNodeIds)) { + Long professionId = reqVO.getQuotaProfession(); + // 忽略租户查询 QuotaCatalogTree 节点 + java.util.Set matchedCatalogTreeIds = cn.iocoder.yudao.framework.tenant.core.util.TenantUtils.executeIgnore(() -> { + List catalogTrees = + quotaCatalogTreeMapper.selectBatchIds(new java.util.ArrayList<>(catalogTreeNodeIds)); + return catalogTrees.stream() + .filter(ct -> professionId.equals(ct.getCatalogItemId())) + .map(com.yhy.module.core.dal.dataobject.quota.QuotaCatalogTreeDO::getId) + .collect(Collectors.toSet()); + }); + + for (Map.Entry entry : quotaItemToCatalogTreeMap.entrySet()) { + if (matchedCatalogTreeIds.contains(entry.getValue())) { + matchedQuotaItemIds.add(entry.getKey()); + } + } + } + } + + // 找出包含匹配定额的父节点(boq/division) + java.util.Set matchedParentIds = allQuotaNodes.stream() + .filter(q -> matchedQuotaItemIds.contains(q.getSourceQuotaItemId())) + .map(WbBoqDivisionDO::getParentId) + .collect(Collectors.toSet()); + + nodes = nodes.stream() + .filter(node -> matchedParentIds.contains(node.getId())) + .collect(Collectors.toList()); + } else { + nodes = Collections.emptyList(); + } + } + + // 3. 去重:按项目特征+定额工料机内容判断唯一性 + if (CollUtil.isNotEmpty(nodes)) { + nodes = deduplicateByContentHash(nodes); + } + + // 4. 计算总数并进行内存分页 + long total = nodes.size(); + int pageNo = reqVO.getPageNo(); + int pageSize = reqVO.getPageSize(); + int fromIndex = (pageNo - 1) * pageSize; + int toIndex = Math.min(fromIndex + pageSize, nodes.size()); + + List pagedNodes = fromIndex < nodes.size() + ? nodes.subList(fromIndex, toIndex) + : Collections.emptyList(); + + // 5. 转换为 VO + List result = WbBoqDivisionConvert.INSTANCE.convertList(pagedNodes); + + // 3. 关联查询清单章节名称 + if (CollUtil.isNotEmpty(result)) { + // 收集所有 sourceBoqItemTreeId + List boqItemTreeIds = result.stream() + .map(WbBoqDivisionRespVO::getSourceBoqItemTreeId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + + log.debug("查询清单章节,sourceBoqItemTreeIds: {}", boqItemTreeIds); + + if (CollUtil.isNotEmpty(boqItemTreeIds)) { + // 忽略租户过滤查询公共清单项树数据 + final List finalBoqItemTreeIds = boqItemTreeIds; + Map boqItemTreeNameMap = cn.iocoder.yudao.framework.tenant.core.util.TenantUtils.executeIgnore(() -> { + // 批量查询清单项树 + List boqItemTrees = + boqItemTreeMapper.selectBatchIds(finalBoqItemTreeIds); + log.debug("查询到清单项树数量: {}", boqItemTrees.size()); + + // 收集所有需要查询的根节点ID + java.util.Set rootIds = new java.util.HashSet<>(); + for (com.yhy.module.core.dal.dataobject.boq.BoqItemTreeDO item : boqItemTrees) { + if (item.getPath() != null && item.getPath().length > 0) { + try { + rootIds.add(Long.parseLong(item.getPath()[0])); + } catch (NumberFormatException ignored) {} + } + } + + // 批量查询根节点 + Map rootNameMap = new java.util.HashMap<>(); + if (CollUtil.isNotEmpty(rootIds)) { + List roots = + boqItemTreeMapper.selectBatchIds(new java.util.ArrayList<>(rootIds)); + for (com.yhy.module.core.dal.dataobject.boq.BoqItemTreeDO root : roots) { + rootNameMap.put(root.getId(), root.getName()); + } + } + + // 构建ID到章节名称的映射 + Map nameMap = new java.util.HashMap<>(); + for (com.yhy.module.core.dal.dataobject.boq.BoqItemTreeDO item : boqItemTrees) { + String chapterName = item.getName(); + if (item.getPath() != null && item.getPath().length > 0) { + try { + Long rootId = Long.parseLong(item.getPath()[0]); + chapterName = rootNameMap.getOrDefault(rootId, item.getName()); + } catch (NumberFormatException ignored) {} + } + nameMap.put(item.getId(), chapterName); + } + return nameMap; + }); + + // 查询清单专业名称(通过boqCatalogItemId) + final List finalBoqItemTreeIds2 = boqItemTreeIds; + Map boqProfessionNameMap = cn.iocoder.yudao.framework.tenant.core.util.TenantUtils.executeIgnore(() -> { + // 批量查询清单项树获取boqCatalogItemId + List boqItemTrees = + boqItemTreeMapper.selectBatchIds(finalBoqItemTreeIds2); + + // 收集所有boqCatalogItemId + java.util.Set catalogItemIds = new java.util.HashSet<>(); + Map itemToCatalogMap = new java.util.HashMap<>(); + for (com.yhy.module.core.dal.dataobject.boq.BoqItemTreeDO item : boqItemTrees) { + if (item.getBoqCatalogItemId() != null) { + catalogItemIds.add(item.getBoqCatalogItemId()); + itemToCatalogMap.put(item.getId(), item.getBoqCatalogItemId()); + } + } + + // 批量查询清单专业 + Map catalogNameMap = new java.util.HashMap<>(); + if (CollUtil.isNotEmpty(catalogItemIds)) { + List catalogs = + boqCatalogItemMapper.selectBatchIds(new java.util.ArrayList<>(catalogItemIds)); + for (com.yhy.module.core.dal.dataobject.boq.BoqCatalogItemDO catalog : catalogs) { + catalogNameMap.put(catalog.getId(), catalog.getName()); + } + } + + // 构建ID到专业名称的映射 + Map professionMap = new java.util.HashMap<>(); + for (Map.Entry entry : itemToCatalogMap.entrySet()) { + professionMap.put(entry.getKey(), catalogNameMap.get(entry.getValue())); + } + return professionMap; + }); + + // 查询清单说明(通过boqSubItem) + final List finalBoqItemTreeIds3 = boqItemTreeIds; + Map boqDescriptionMap = cn.iocoder.yudao.framework.tenant.core.util.TenantUtils.executeIgnore(() -> { + // 批量查询清单子项 + List boqSubItems = + boqSubItemMapper.selectListByBoqItemTreeIds(finalBoqItemTreeIds3); + + // 构建ID到清单说明的映射(取第一个子项的说明) + Map descMap = new java.util.HashMap<>(); + for (com.yhy.module.core.dal.dataobject.boq.BoqSubItemDO subItem : boqSubItems) { + if (subItem.getBoqItemTreeId() != null && subItem.getDescription() != null) { + // 如果已有值则不覆盖(取第一个) + descMap.putIfAbsent(subItem.getBoqItemTreeId(), subItem.getDescription()); + } + } + return descMap; + }); + + // 设置章节名称、专业名称和清单说明 + for (WbBoqDivisionRespVO vo : result) { + if (vo.getSourceBoqItemTreeId() != null) { + vo.setBoqChapterName(boqItemTreeNameMap.get(vo.getSourceBoqItemTreeId())); + vo.setBoqProfessionName(boqProfessionNameMap.get(vo.getSourceBoqItemTreeId())); + vo.setBoqDescription(boqDescriptionMap.get(vo.getSourceBoqItemTreeId())); + } + } + } + } + + return new PageResult<>(result, total); + } + + /** + * 按内容哈希去重:清单按项目特征+定额工料机内容判断唯一性,分部按其下所有清单内容判断 + */ + private List deduplicateByContentHash(List nodes) { + if (CollUtil.isEmpty(nodes)) { + return nodes; + } + + // 收集所有节点ID + List nodeIds = nodes.stream().map(WbBoqDivisionDO::getId).collect(Collectors.toList()); + + // 查询所有清单节点的子定额 + Map> boqToQuotasMap = new java.util.HashMap<>(); + for (WbBoqDivisionDO node : nodes) { + if (WbBoqDivisionDO.NODE_TYPE_BOQ.equals(node.getNodeType())) { + List quotas = wbBoqDivisionMapper.selectListByParentId(node.getId()); + boqToQuotasMap.put(node.getId(), quotas); + } + } + + // 查询所有定额的工料机 + java.util.Set allQuotaIds = new java.util.HashSet<>(); + for (List quotas : boqToQuotasMap.values()) { + for (WbBoqDivisionDO quota : quotas) { + allQuotaIds.add(quota.getId()); + } + } + + // 批量查询工料机 + Map> quotaToResourcesMap = new java.util.HashMap<>(); + if (CollUtil.isNotEmpty(allQuotaIds)) { + for (Long quotaId : allQuotaIds) { + List resources = wbBoqResourceMapper.selectListByDivisionId(quotaId); + quotaToResourcesMap.put(quotaId, resources); + } + } + + // 对于分部节点,查询其下所有清单 + Map> divisionToBoqsMap = new java.util.HashMap<>(); + for (WbBoqDivisionDO node : nodes) { + if (WbBoqDivisionDO.NODE_TYPE_DIVISION.equals(node.getNodeType())) { + List boqs = wbBoqDivisionMapper.selectListByParentId(node.getId()); + divisionToBoqsMap.put(node.getId(), boqs.stream() + .filter(b -> WbBoqDivisionDO.NODE_TYPE_BOQ.equals(b.getNodeType())) + .collect(Collectors.toList())); + // 也需要查询这些清单的定额和工料机 + for (WbBoqDivisionDO boq : divisionToBoqsMap.get(node.getId())) { + if (!boqToQuotasMap.containsKey(boq.getId())) { + List quotas = wbBoqDivisionMapper.selectListByParentId(boq.getId()); + boqToQuotasMap.put(boq.getId(), quotas); + for (WbBoqDivisionDO quota : quotas) { + if (!quotaToResourcesMap.containsKey(quota.getId())) { + quotaToResourcesMap.put(quota.getId(), wbBoqResourceMapper.selectListByDivisionId(quota.getId())); + } + } + } + } + } + } + + // 计算每个节点的内容哈希并去重(分部类型不去重) + java.util.Set seenHashes = new java.util.HashSet<>(); + List deduplicated = new java.util.ArrayList<>(); + + for (WbBoqDivisionDO node : nodes) { + // 分部类型不参与去重,直接保留 + if (WbBoqDivisionDO.NODE_TYPE_DIVISION.equals(node.getNodeType())) { + deduplicated.add(node); + continue; + } + // 清单类型按内容哈希去重 + String contentHash = calculateContentHash(node, boqToQuotasMap, quotaToResourcesMap, divisionToBoqsMap); + if (!seenHashes.contains(contentHash)) { + seenHashes.add(contentHash); + deduplicated.add(node); + } + } + + log.debug("去重前 {} 条,去重后 {} 条", nodes.size(), deduplicated.size()); + return deduplicated; + } + + /** + * 计算节点内容哈希 + */ + private String calculateContentHash(WbBoqDivisionDO node, + Map> boqToQuotasMap, + Map> quotaToResourcesMap, + Map> divisionToBoqsMap) { + StringBuilder sb = new StringBuilder(); + + if (WbBoqDivisionDO.NODE_TYPE_BOQ.equals(node.getNodeType())) { + // 清单:项目特征 + 定额工料机内容 + sb.append("BOQ:"); + sb.append(node.getFeature() != null ? node.getFeature() : ""); + sb.append("|"); + + // 添加定额和工料机信息 + List quotas = boqToQuotasMap.getOrDefault(node.getId(), Collections.emptyList()); + quotas.sort((a, b) -> { + int cmp = (a.getCode() != null ? a.getCode() : "").compareTo(b.getCode() != null ? b.getCode() : ""); + return cmp != 0 ? cmp : Long.compare(a.getId(), b.getId()); + }); + + for (WbBoqDivisionDO quota : quotas) { + sb.append("Q:").append(quota.getCode()).append(",").append(quota.getName()).append(";"); + List resources = quotaToResourcesMap.getOrDefault(quota.getId(), Collections.emptyList()); + resources.sort((a, b) -> { + int cmp = (a.getCode() != null ? a.getCode() : "").compareTo(b.getCode() != null ? b.getCode() : ""); + return cmp != 0 ? cmp : Long.compare(a.getId(), b.getId()); + }); + for (WbBoqResourceDO res : resources) { + sb.append("R:").append(res.getCode()).append(",") + .append(res.getName()).append(",") + .append(res.getConsumeQty()).append(";"); + } + } + } else if (WbBoqDivisionDO.NODE_TYPE_DIVISION.equals(node.getNodeType())) { + // 分部:所有清单的内容哈希组合 + sb.append("DIV:"); + sb.append(node.getName() != null ? node.getName() : ""); + sb.append("|"); + + List boqs = divisionToBoqsMap.getOrDefault(node.getId(), Collections.emptyList()); + boqs.sort((a, b) -> { + int cmp = (a.getCode() != null ? a.getCode() : "").compareTo(b.getCode() != null ? b.getCode() : ""); + return cmp != 0 ? cmp : Long.compare(a.getId(), b.getId()); + }); + + for (WbBoqDivisionDO boq : boqs) { + String boqHash = calculateContentHash(boq, boqToQuotasMap, quotaToResourcesMap, divisionToBoqsMap); + sb.append(boqHash).append("|"); + } + } + + // 使用 MD5 生成哈希 + return cn.hutool.crypto.digest.DigestUtil.md5Hex(sb.toString()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long copyDivisionWithChildren(Long sourceDivisionId, Long targetCompileTreeId, Long targetParentId) { + // 1. 校验源分部节点 + WbBoqDivisionDO sourceDivision = wbBoqDivisionMapper.selectById(sourceDivisionId); + if (sourceDivision == null) { + throw exception(WB_BOQ_DIVISION_NOT_EXISTS); + } + if (!WbBoqDivisionDO.NODE_TYPE_DIVISION.equals(sourceDivision.getNodeType())) { + throw exception(WB_BOQ_DIVISION_NODE_TYPE_ERROR); + } + + // 2. 校验目标单位工程节点 + validateCompileTreeIsUnit(targetCompileTreeId); + + // 3. 获取目标父节点路径 + String[] parentPath; + if (targetParentId != null) { + WbBoqDivisionDO targetParent = validateNodeExists(targetParentId); + parentPath = targetParent.getPath() != null ? targetParent.getPath() : new String[]{String.valueOf(targetParentId)}; + } else { + // 如果没有父节点,查找根节点 + WbBoqDivisionDO root = wbBoqDivisionMapper.selectOne(new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX() + .eq(WbBoqDivisionDO::getCompileTreeId, targetCompileTreeId) + .eq(WbBoqDivisionDO::getNodeType, WbBoqDivisionDO.NODE_TYPE_ROOT)); + if (root != null) { + targetParentId = root.getId(); + parentPath = root.getPath() != null ? root.getPath() : new String[]{String.valueOf(root.getId())}; + } else { + parentPath = new String[0]; + } + } + + // 4. 获取同级最大排序号 + Integer maxSortOrder = wbBoqDivisionMapper.selectMaxSortOrderByParentId(targetCompileTreeId, targetParentId); + + // 5. 复制分部节点 + WbBoqDivisionDO newDivision = WbBoqDivisionDO.builder() + .compileTreeId(targetCompileTreeId) + .parentId(targetParentId) + .nodeType(WbBoqDivisionDO.NODE_TYPE_DIVISION) + .sourceType(WbBoqDivisionDO.SOURCE_TYPE_COPY) + .sourceBoqCatalogId(sourceDivision.getSourceBoqCatalogId()) + .sourceBoqItemTreeId(sourceDivision.getSourceBoqItemTreeId()) + .code(sourceDivision.getCode()) + .name(sourceDivision.getName()) + .feature(sourceDivision.getFeature()) + .unit(sourceDivision.getUnit()) + .qty(sourceDivision.getQty()) + .lineNo(sourceDivision.getLineNo()) + .sortOrder(maxSortOrder + 1) + .baseCode(sourceDivision.getBaseCode()) + .baseName(sourceDivision.getBaseName()) + .baseUnit(sourceDivision.getBaseUnit()) + .snapshotJson(sourceDivision.getSnapshotJson()) + .attributes(sourceDivision.getAttributes()) + .build(); + + wbBoqDivisionMapper.insert(newDivision); + + // 6. 更新路径 + String[] newPath = Arrays.copyOf(parentPath, parentPath.length + 1); + newPath[parentPath.length] = String.valueOf(newDivision.getId()); + newDivision.setPath(newPath); + wbBoqDivisionMapper.updateById(newDivision); + + // 7. 递归复制子节点(清单和定额) + copyDivisionChildren(sourceDivisionId, newDivision.getId(), targetCompileTreeId, newPath); + + log.info("复制分部 {} 到新分部 {}", sourceDivisionId, newDivision.getId()); + return newDivision.getId(); + } + + /** + * 复制清单节点(含定额和工料机)- 用于分部复制时 + */ + private void copyBoqNode(WbBoqDivisionDO sourceBoq, Long newParentId, Long targetCompileTreeId, String[] parentPath) { + // 1. 获取排序号 + Integer maxSortOrder = wbBoqDivisionMapper.selectMaxSortOrderByParentId(targetCompileTreeId, newParentId); + + // 2. 复制清单节点 + WbBoqDivisionDO newBoq = WbBoqDivisionDO.builder() + .compileTreeId(targetCompileTreeId) + .parentId(newParentId) + .nodeType(WbBoqDivisionDO.NODE_TYPE_BOQ) + .sourceType(WbBoqDivisionDO.SOURCE_TYPE_COPY) + .sourceBoqCatalogId(sourceBoq.getSourceBoqCatalogId()) + .sourceBoqItemTreeId(sourceBoq.getSourceBoqItemTreeId()) + .code(sourceBoq.getCode()) + .name(sourceBoq.getName()) + .feature(sourceBoq.getFeature()) + .unit(sourceBoq.getUnit()) + .qty(sourceBoq.getQty()) + .lineNo(sourceBoq.getLineNo()) + .sortOrder(maxSortOrder + 1) + .baseCode(sourceBoq.getBaseCode()) + .baseName(sourceBoq.getBaseName()) + .baseUnit(sourceBoq.getBaseUnit()) + .snapshotJson(sourceBoq.getSnapshotJson()) + .attributes(sourceBoq.getAttributes()) + .build(); + + wbBoqDivisionMapper.insert(newBoq); + + // 3. 更新路径 + String[] newPath = Arrays.copyOf(parentPath, parentPath.length + 1); + newPath[parentPath.length] = String.valueOf(newBoq.getId()); + newBoq.setPath(newPath); + wbBoqDivisionMapper.updateById(newBoq); + + // 4. 复制子节点(定额) + List quotaNodes = wbBoqDivisionMapper.selectListByParentId(sourceBoq.getId()); + for (WbBoqDivisionDO sourceQuota : quotaNodes) { + if (WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(sourceQuota.getNodeType())) { + copyQuotaNode(sourceQuota, newBoq.getId(), targetCompileTreeId, newPath); + } + } + + log.info("复制清单 {} 到新清单 {}", sourceBoq.getId(), newBoq.getId()); + } + + /** + * 递归复制分部的子节点 + */ + private void copyDivisionChildren(Long sourceParentId, Long newParentId, Long targetCompileTreeId, String[] parentPath) { + List children = wbBoqDivisionMapper.selectListByParentId(sourceParentId); + if (CollUtil.isEmpty(children)) { + return; + } + + for (WbBoqDivisionDO child : children) { + if (WbBoqDivisionDO.NODE_TYPE_BOQ.equals(child.getNodeType())) { + // 复制清单节点(含定额和工料机) + copyBoqNode(child, newParentId, targetCompileTreeId, parentPath); + } else if (WbBoqDivisionDO.NODE_TYPE_DIVISION.equals(child.getNodeType())) { + // 递归复制子分部 + Integer maxSortOrder = wbBoqDivisionMapper.selectMaxSortOrderByParentId(targetCompileTreeId, newParentId); + + WbBoqDivisionDO newChild = WbBoqDivisionDO.builder() + .compileTreeId(targetCompileTreeId) + .parentId(newParentId) + .nodeType(WbBoqDivisionDO.NODE_TYPE_DIVISION) + .sourceType(WbBoqDivisionDO.SOURCE_TYPE_COPY) + .sourceBoqCatalogId(child.getSourceBoqCatalogId()) + .sourceBoqItemTreeId(child.getSourceBoqItemTreeId()) + .code(child.getCode()) + .name(child.getName()) + .feature(child.getFeature()) + .unit(child.getUnit()) + .qty(child.getQty()) + .lineNo(child.getLineNo()) + .sortOrder(maxSortOrder + 1) + .baseCode(child.getBaseCode()) + .baseName(child.getBaseName()) + .baseUnit(child.getBaseUnit()) + .snapshotJson(child.getSnapshotJson()) + .attributes(child.getAttributes()) + .build(); + + wbBoqDivisionMapper.insert(newChild); + + String[] newPath = Arrays.copyOf(parentPath, parentPath.length + 1); + newPath[parentPath.length] = String.valueOf(newChild.getId()); + newChild.setPath(newPath); + wbBoqDivisionMapper.updateById(newChild); + + // 递归复制子节点 + copyDivisionChildren(child.getId(), newChild.getId(), targetCompileTreeId, newPath); + } + } + } + + /** + * 自动建立项目级费率模式绑定(如果尚未绑定) + * 当添加定额时,自动将定额所属的费率模式绑定到项目 + */ + private void autoBindRateModeForProject(Long compileTreeId, Long sourceQuotaItemId, Long rateModeId) { + // 1. 获取单位工程节点 + WbCompileTreeDO compileTree = wbCompileTreeMapper.selectById(compileTreeId); + if (compileTree == null || compileTree.getProjectId() == null) { + return; + } + + // 2. 获取项目 + com.yhy.module.core.dal.dataobject.workbench.WbProjectTreeDO project = + wbProjectTreeMapper.selectById(compileTree.getProjectId()); + if (project == null) { + return; + } + + // 3. 获取定额专业ID + Long quotaSpecialtyId = quotaItemService.getSpecialtyIdByQuotaItem(sourceQuotaItemId); + if (quotaSpecialtyId == null) { + return; + } + + // 4. 检查是否已经绑定 + Long existingRateModeId = project.getRateModeIdByQuotaSpecialty(quotaSpecialtyId); + if (existingRateModeId != null) { + // 已经绑定,不需要重复绑定 + return; + } + + // 5. 获取费率模式和定额专业名称 + com.yhy.module.core.dal.dataobject.quota.QuotaCatalogItemDO rateMode = + quotaCatalogItemMapper.selectById(rateModeId); + com.yhy.module.core.dal.dataobject.quota.QuotaCatalogItemDO specialty = + quotaCatalogItemMapper.selectById(quotaSpecialtyId); + + String rateModeName = rateMode != null ? rateMode.getName() : ""; + String quotaSpecialtyName = specialty != null ? specialty.getName() : ""; + + // 6. 建立绑定 + project.bindRateMode(quotaSpecialtyId, rateModeId, rateModeName, quotaSpecialtyName); + wbProjectTreeMapper.updateById(project); + + log.info("[autoBindRateModeForProject] 自动建立费率模式绑定, projectId={}, quotaSpecialtyId={}, rateModeId={}", + compileTree.getProjectId(), quotaSpecialtyId, rateModeId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateBoqDivisionBaseRate(com.yhy.module.core.controller.admin.workbench.vo.calcbaserate.UpdateBoqDivisionBaseRateReqVO reqVO) { + // 1. 校验节点存在且为清单类型 + WbBoqDivisionDO node = validateNodeExists(reqVO.getDivisionId()); + if (!WbBoqDivisionDO.NODE_TYPE_BOQ.equals(node.getNodeType())) { + throw exception0(400, "只能选择清单节点进行基数费率替换"); + } + + // 2. 获取基数费率项信息 + com.yhy.module.core.dal.dataobject.calcbaserate.CalcBaseRateItemDO item = + calcBaseRateItemMapper.selectById(reqVO.getCalcBaseRateItemId()); + if (item == null) { + throw exception0(400, "基数费率项不存在"); + } + + // 3. 更新清单的费率和备注字段(用基数费率的备注替换清单备注) + String rateStr = item.getRate(); + if (rateStr != null && rateStr.endsWith("%")) { + rateStr = rateStr.substring(0, rateStr.length() - 1); + } + node.setRate(new java.math.BigDecimal(rateStr)); + node.setRemark(item.getRemark()); // 将基数费率项的备注作为清单备注 + wbBoqDivisionMapper.updateById(node); + + log.info("[updateBoqDivisionBaseRate] 更新清单基数费率成功, divisionId={}, rate={}, remark={}", + reqVO.getDivisionId(), item.getRate(), item.getRemark()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void importTemplate(Long compileTreeId, Long catalogItemId) { + // 1. 校验单位工程节点 + validateCompileTreeIsUnit(compileTreeId); + + // 2. 确保根节点存在 + List existingRoots = wbBoqDivisionMapper.selectListByNodeType( + compileTreeId, WbBoqDivisionDO.NODE_TYPE_ROOT); + Long rootId; + if (existingRoots.isEmpty()) { + rootId = createRootNode(compileTreeId, "单位工程"); + } else { + rootId = existingRoots.get(0).getId(); + } + WbBoqDivisionDO rootNode = wbBoqDivisionMapper.selectById(rootId); + + // 3. 查询后台所有标签页的模板数据(四个标签页独立存储) + List templates = + configUnitDivisionTemplateMapper.selectListByCatalogItemId(catalogItemId); + if (CollUtil.isEmpty(templates)) { + log.info("[importTemplate] catalogItemId={} 没有模板数据,跳过", catalogItemId); + return; + } + + // 4. 按 path 长度排序(保证父节点先处理),同层按 sortOrder + List sortedTemplates = + templates.stream() + .sorted((a, b) -> { + int depthA = a.getPath() != null ? a.getPath().length : 0; + int depthB = b.getPath() != null ? b.getPath().length : 0; + if (depthA != depthB) return Integer.compare(depthA, depthB); + int sa = a.getSortOrder() != null ? a.getSortOrder() : 0; + int sb = b.getSortOrder() != null ? b.getSortOrder() : 0; + return Integer.compare(sa, sb); + }) + .collect(Collectors.toList()); + + // 5. 复制模板到工作台,维护 ID 映射(旧模板ID → 新工作台ID) + Map idMapping = new java.util.HashMap<>(); + int sortCounter = wbBoqDivisionMapper.selectMaxSortOrderByParentId(compileTreeId, rootId); + for (com.yhy.module.core.dal.dataobject.config.ConfigUnitDivisionTemplateDO tpl : sortedTemplates) { + // 确定父节点ID + Long parentId; + if (tpl.getParentId() == null) { + parentId = rootId; + } else { + parentId = idMapping.get(tpl.getParentId()); + if (parentId == null) { + log.warn("[importTemplate] 模板节点 {} 的父节点 {} 未找到映射,跳过", tpl.getId(), tpl.getParentId()); + continue; + } + } + + // 从模板的 tabType 字段直接获取标签页归属(非 division 的设置 tabTypes) + String tabType = tpl.getTabType(); + List tabTypes = null; + if (tabType != null && !"division".equals(tabType)) { + tabTypes = java.util.Collections.singletonList(tabType); + } + + // 构建工作台节点 + Map attrs = new java.util.HashMap<>(); + if (tpl.getAttributes() != null) { + attrs.putAll(tpl.getAttributes()); + } + if (tabTypes != null) { + attrs.put("tabTypes", tabTypes); + } + + WbBoqDivisionDO node = WbBoqDivisionDO.builder() + .compileTreeId(compileTreeId) + .parentId(parentId) + .nodeType(tpl.getNodeType()) + .sourceType(WbBoqDivisionDO.SOURCE_TYPE_SYSTEM) + .code(tpl.getCode()) + .name(tpl.getName()) + .unit(tpl.getUnit()) + .rate(tpl.getRate()) + .remark(tpl.getRemark()) + .costCode(tpl.getCostCode()) + .sortOrder(++sortCounter) + .attributes(attrs.isEmpty() ? null : attrs) + .build(); + + // 设置路径 + WbBoqDivisionDO parentNode = parentId.equals(rootId) ? rootNode : wbBoqDivisionMapper.selectById(parentId); + node.setPath(buildPath(parentNode)); + + wbBoqDivisionMapper.insert(node); + updatePathWithSelfId(node); + + // 记录 ID 映射 + idMapping.put(tpl.getId(), node.getId()); + + log.debug("[importTemplate] 复制模板节点: {} -> 工作台节点: {}, tabType={}", tpl.getId(), node.getId(), tabType); + } + + // 6. 修正基数范围中的节点ID引用(模板ID → 工作台ID) + for (Map.Entry entry : idMapping.entrySet()) { + Long newNodeId = entry.getValue(); + WbBoqDivisionDO node = wbBoqDivisionMapper.selectById(newNodeId); + if (node == null || node.getAttributes() == null) { + continue; + } + + Object rangeObj = node.getAttributes().get("baseNumberRange"); + if (!(rangeObj instanceof Map)) { + continue; + } + + @SuppressWarnings("unchecked") + Map rangeMap = (Map) rangeObj; + Object selectedIdsObj = rangeMap.get("selectedIds"); + if (!(selectedIdsObj instanceof List)) { + continue; + } + + List oldIds = (List) selectedIdsObj; + if (oldIds.isEmpty()) { + continue; + } + + // 将模板ID映射为工作台ID + List newIds = new java.util.ArrayList<>(); + for (Object oldId : oldIds) { + Long oldLong; + if (oldId instanceof Number) { + oldLong = ((Number) oldId).longValue(); + } else { + try { + oldLong = Long.parseLong(String.valueOf(oldId)); + } catch (NumberFormatException e) { + continue; // 忽略无效ID + } + } + Long mapped = idMapping.get(oldLong); + if (mapped != null) { + newIds.add(mapped); + } + // 映射不到的ID直接丢弃(模板中已删除的节点) + } + + // 更新 attributes 中的 baseNumberRange + Map updatedAttrs = new java.util.HashMap<>(node.getAttributes()); + Map updatedRange = new java.util.HashMap<>(rangeMap); + updatedRange.put("selectedIds", newIds); + updatedRange.put("count", newIds.size()); + updatedAttrs.put("baseNumberRange", updatedRange); + + node.setAttributes(updatedAttrs); + wbBoqDivisionMapper.updateById(node); + + log.debug("[importTemplate] 修正基数范围ID: 节点={}, 旧IDs={}, 新IDs={}", newNodeId, oldIds, newIds); + } + + log.info("[importTemplate] 导入模板完成, compileTreeId={}, catalogItemId={}, 共复制 {} 个节点", + compileTreeId, catalogItemId, idMapping.size()); + } + + @Override + public List getTreeByTab(Long compileTreeId, String tabType) { + // 1. 获取全量数据 + List allNodes = wbBoqDivisionMapper.selectListByCompileTreeId(compileTreeId); + if (CollUtil.isEmpty(allNodes)) { + return Collections.emptyList(); + } + + // 2. 四个标签页独立:每个标签页只显示属于自己的节点 + // division 标签页 = 无 tabTypes 或 tabTypes 为空的节点(默认归属) + // measure/other/unit_summary = tabTypes 包含对应值的节点 + boolean isDivision = StrUtil.isBlank(tabType) || "division".equals(tabType); + + java.util.Set matchedIds = new java.util.HashSet<>(); + for (WbBoqDivisionDO node : allNodes) { + // root 节点始终包含 + if (WbBoqDivisionDO.NODE_TYPE_ROOT.equals(node.getNodeType())) { + continue; + } + Map attrs = node.getAttributes(); + if (isDivision) { + // 分部分项:只显示没有 tabTypes 或 tabTypes 为空的节点 + if (attrs == null || !attrs.containsKey("tabTypes")) { + matchedIds.add(node.getId()); + } else { + Object tabTypesObj = attrs.get("tabTypes"); + if (tabTypesObj instanceof java.util.List && ((java.util.List) tabTypesObj).isEmpty()) { + matchedIds.add(node.getId()); + } + } + } else { + // 其他标签页:tabTypes 包含目标值的节点 + if (attrs != null) { + Object tabTypesObj = attrs.get("tabTypes"); + if (tabTypesObj instanceof java.util.List) { + java.util.List tabTypesList = (java.util.List) tabTypesObj; + if (tabTypesList.contains(tabType)) { + matchedIds.add(node.getId()); + } + } + // 兼容旧的 tabType 单值格式 + if (tabType.equals(attrs.get("tabType"))) { + matchedIds.add(node.getId()); + } + } + } + } + + // 3. 递归收集所有子节点ID + Map> parentIdMap = allNodes.stream() + .filter(n -> n.getParentId() != null) + .collect(Collectors.groupingBy(WbBoqDivisionDO::getParentId)); + java.util.Set allMatchedIds = new java.util.HashSet<>(matchedIds); + for (Long matchedId : matchedIds) { + collectChildIdsForTab(matchedId, parentIdMap, allMatchedIds, tabType); + } + + // 4. 加入 root 节点,构建树 + List filteredVoList = new ArrayList<>(); + for (WbBoqDivisionDO node : allNodes) { + if (WbBoqDivisionDO.NODE_TYPE_ROOT.equals(node.getNodeType()) || allMatchedIds.contains(node.getId())) { + filteredVoList.add(WbBoqDivisionConvert.INSTANCE.convert(node)); + } + } + + return buildTreeFromVOList(filteredVoList); + } + + @Override + public List getTreeByTab(Long compileTreeId, String tabType, Boolean excludeZeroQtyBoq) { + List tree = getTreeByTab(compileTreeId, tabType); + if (Boolean.TRUE.equals(excludeZeroQtyBoq)) { + filterZeroQtyBoqNodes(tree); + } + return tree; + } + + @Override + public List getTreeForUnifiedFee(Long compileTreeId, String tabType, List feeChapter) { + // 1. 获取按标签页过滤后的树(复用现有逻辑) + List tree = getTreeByTab(compileTreeId, tabType); + + // 2. 过滤掉 unified_fee 类型节点 + filterNodesByType(tree, WbBoqDivisionDO.NODE_TYPE_UNIFIED_FEE); + + // 3. 如果传了 feeChapter,根据取费章节过滤定额,并裁剪空清单 + if (CollUtil.isNotEmpty(feeChapter)) { + // 3.1 收集树中所有定额节点的 sourceQuotaItemId,批量查询 catalogPath + List allQuotaNodes = new java.util.ArrayList<>(); + collectNodesByType(tree, WbBoqDivisionDO.NODE_TYPE_QUOTA, allQuotaNodes); + + // 批量查询定额项的 catalogItemId + List quotaItemIds = allQuotaNodes.stream() + .filter(n -> n.getSourceQuotaItemId() != null) + .map(WbBoqDivisionRespVO::getSourceQuotaItemId) + .distinct() + .collect(Collectors.toList()); + + // 构建 sourceQuotaItemId -> catalogPath 的映射 + java.util.Set matchedQuotaItemIds = new java.util.HashSet<>(); + if (CollUtil.isNotEmpty(quotaItemIds)) { + Map quotaItemToCatalogMap = new java.util.HashMap<>(); + List quotaItems = + quotaItemMapper.selectBatchIds(quotaItemIds); + for (com.yhy.module.core.dal.dataobject.quota.QuotaItemDO item : quotaItems) { + if (item.getCatalogItemId() != null) { + quotaItemToCatalogMap.put(item.getId(), item.getCatalogItemId()); + } + } + + // 批量查询定额子目录的 path + List catalogItemIds = new java.util.ArrayList<>(new java.util.HashSet<>(quotaItemToCatalogMap.values())); + Map catalogItemToPathMap = new java.util.HashMap<>(); + if (CollUtil.isNotEmpty(catalogItemIds)) { + List catalogTrees = + quotaCatalogTreeMapper.selectBatchIds(catalogItemIds); + for (com.yhy.module.core.dal.dataobject.quota.QuotaCatalogTreeDO ct : catalogTrees) { + if (ct.getPath() != null) { + catalogItemToPathMap.put(ct.getId(), ct.getPath()); + } + } + } + + // 判断每个定额的 catalogPath 是否与 feeChapter 匹配 + java.util.Set feeChapterStrSet = feeChapter.stream() + .map(String::valueOf) + .collect(Collectors.toSet()); + for (WbBoqDivisionRespVO quotaNode : allQuotaNodes) { + if (quotaNode.getSourceQuotaItemId() == null) continue; + Long catalogItemId = quotaItemToCatalogMap.get(quotaNode.getSourceQuotaItemId()); + if (catalogItemId != null) { + // catalogItemId 本身匹配 + if (feeChapterStrSet.contains(String.valueOf(catalogItemId))) { + matchedQuotaItemIds.add(quotaNode.getId()); + continue; + } + // catalogPath 中任意节点匹配 + String[] catalogPath = catalogItemToPathMap.get(catalogItemId); + if (catalogPath != null) { + for (String pathId : catalogPath) { + if (feeChapterStrSet.contains(pathId)) { + matchedQuotaItemIds.add(quotaNode.getId()); + break; + } + } + } + } + } + } + + // 3.2 过滤不匹配的定额节点 + filterUnmatchedQuotaNodes(tree, matchedQuotaItemIds); + + // 3.3 裁剪没有定额子节点的空清单 + filterEmptyBoqNodes(tree); + } + + return tree; + } + + /** + * 递归过滤指定类型的节点 + */ + private void filterNodesByType(List nodes, String nodeType) { + if (nodes == null) return; + nodes.removeIf(node -> { + if (node.getChildren() != null) { + filterNodesByType(node.getChildren(), nodeType); + } + return nodeType.equals(node.getNodeType()); + }); + } + + /** + * 递归收集指定类型的节点 + */ + private void collectNodesByType(List nodes, String nodeType, List result) { + if (nodes == null) return; + for (WbBoqDivisionRespVO node : nodes) { + if (nodeType.equals(node.getNodeType())) { + result.add(node); + } + if (node.getChildren() != null) { + collectNodesByType(node.getChildren(), nodeType, result); + } + } + } + + /** + * 递归过滤不在匹配集合中的定额节点 + */ + private void filterUnmatchedQuotaNodes(List nodes, java.util.Set matchedIds) { + if (nodes == null) return; + nodes.removeIf(node -> { + if (node.getChildren() != null) { + filterUnmatchedQuotaNodes(node.getChildren(), matchedIds); + } + return WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(node.getNodeType()) && !matchedIds.contains(node.getId()); + }); + } + + /** + * 递归裁剪没有子节点的清单节点 + */ + private void filterEmptyBoqNodes(List nodes) { + if (nodes == null) return; + nodes.removeIf(node -> { + if (node.getChildren() != null) { + filterEmptyBoqNodes(node.getChildren()); + } + return WbBoqDivisionDO.NODE_TYPE_BOQ.equals(node.getNodeType()) + && (node.getChildren() == null || node.getChildren().isEmpty()); + }); + } + + /** + * 递归过滤工程量为0或空的清单节点(boq类型) + * 过滤规则:移除 nodeType='boq' 且 qty 为 null 或 0 的节点 + * 保留分部/根/定额等其他类型节点 + */ + private void filterZeroQtyBoqNodes(List nodes) { + if (nodes == null) return; + nodes.removeIf(node -> { + // 先递归处理子节点 + if (node.getChildren() != null) { + filterZeroQtyBoqNodes(node.getChildren()); + } + // 只过滤清单节点 + if (WbBoqDivisionDO.NODE_TYPE_BOQ.equals(node.getNodeType())) { + return node.getQty() == null || node.getQty().compareTo(java.math.BigDecimal.ZERO) == 0; + } + return false; + }); + } + + /** + * 递归收集子节点ID + */ + private void collectChildIds(Long parentId, Map> parentIdMap, java.util.Set result) { + List children = parentIdMap.get(parentId); + if (children == null) return; + for (WbBoqDivisionDO child : children) { + result.add(child.getId()); + collectChildIds(child.getId(), parentIdMap, result); + } + } + + /** + * 递归收集子节点ID(标签页感知版本) + * 如果子节点自身有 tabTypes 且不包含目标 tabType,则跳过该子节点及其后代 + * 如果子节点没有 tabTypes(如定额、工作内容等纯子节点),则收集 + */ + private void collectChildIdsForTab(Long parentId, Map> parentIdMap, + java.util.Set result, String tabType) { + List children = parentIdMap.get(parentId); + if (children == null) return; + for (WbBoqDivisionDO child : children) { + Map attrs = child.getAttributes(); + boolean hasTabTypes = false; + boolean matchesTab = false; + if (attrs != null) { + Object tabTypesObj = attrs.get("tabTypes"); + if (tabTypesObj instanceof java.util.List && !((java.util.List) tabTypesObj).isEmpty()) { + hasTabTypes = true; + matchesTab = ((java.util.List) tabTypesObj).contains(tabType); + } + // 兼容旧的 tabType 单值 + if (!hasTabTypes && attrs.get("tabType") != null) { + hasTabTypes = true; + matchesTab = tabType.equals(attrs.get("tabType")); + } + } + // 有 tabTypes 但不匹配 → 跳过 + if (hasTabTypes && !matchesTab) { + continue; + } + result.add(child.getId()); + collectChildIdsForTab(child.getId(), parentIdMap, result, tabType); + } + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbBoqMarketMaterialServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbBoqMarketMaterialServiceImpl.java new file mode 100644 index 0000000..a7fbdbf --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbBoqMarketMaterialServiceImpl.java @@ -0,0 +1,279 @@ +package com.yhy.module.core.service.workbench.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqMarketMaterialRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqMarketMaterialSaveReqVO; +import com.yhy.module.core.dal.dataobject.quota.QuotaMarketMaterialDO; +import com.yhy.module.core.dal.dataobject.resource.ResourceItemDO; +import com.yhy.module.core.dal.dataobject.workbench.WbAdjustmentSettingDO; +import com.yhy.module.core.dal.dataobject.workbench.WbBoqMarketMaterialDO; +import com.yhy.module.core.dal.mysql.quota.QuotaMarketMaterialMapper; +import com.yhy.module.core.dal.mysql.resource.ResourceItemMapper; +import com.yhy.module.core.dal.mysql.workbench.WbAdjustmentSettingMapper; +import com.yhy.module.core.dal.mysql.workbench.WbBoqMarketMaterialMapper; +import com.yhy.module.core.enums.ErrorCodeConstants; +import com.yhy.module.core.service.workbench.WbBoqMarketMaterialService; +import com.yhy.module.core.util.AdjustmentFormulaCalculator; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * 工作台市场主材设备 Service 实现类 + * + * @author yhy + */ +@Service +@Validated +@Slf4j +public class WbBoqMarketMaterialServiceImpl implements WbBoqMarketMaterialService { + + @Resource + private WbBoqMarketMaterialMapper wbBoqMarketMaterialMapper; + + @Resource + private QuotaMarketMaterialMapper quotaMarketMaterialMapper; + + @Resource + private ResourceItemMapper resourceItemMapper; + + @Resource + private WbAdjustmentSettingMapper wbAdjustmentSettingMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createMarketMaterial(WbBoqMarketMaterialSaveReqVO createReqVO) { + WbBoqMarketMaterialDO marketMaterial = BeanUtil.copyProperties(createReqVO, WbBoqMarketMaterialDO.class); + marketMaterial.setTenantId(TenantContextHolder.getTenantId()); + + // 处理 sortOrder + if (marketMaterial.getSortOrder() == null) { + Integer maxSortOrder = getMaxSortOrder(createReqVO.getDivisionId()); + marketMaterial.setSortOrder(maxSortOrder + 1); + } + + wbBoqMarketMaterialMapper.insert(marketMaterial); + return marketMaterial.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateMarketMaterial(WbBoqMarketMaterialSaveReqVO updateReqVO) { + validateExists(updateReqVO.getId()); + + WbBoqMarketMaterialDO updateObj = BeanUtil.copyProperties(updateReqVO, WbBoqMarketMaterialDO.class); + wbBoqMarketMaterialMapper.updateById(updateObj); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteMarketMaterial(Long id) { + validateExists(id); + wbBoqMarketMaterialMapper.deleteById(id); + } + + @Override + public WbBoqMarketMaterialDO getMarketMaterial(Long id) { + return wbBoqMarketMaterialMapper.selectById(id); + } + + @Override + public List getMarketMaterialList(Long divisionId) { + List list = wbBoqMarketMaterialMapper.selectListByDivisionId(divisionId); + List result = list.stream().map(this::convertToVO).collect(Collectors.toList()); + + // 计算调整公式(虚拟字段),与子目工料机逻辑一致 + calculateAdjustmentFormulas(divisionId, result); + + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void copyFromQuota(Long divisionId, Long quotaItemId) { + // 1. 查询定额基价下的市场主材设备 + List quotaMaterials = quotaMarketMaterialMapper.selectListByQuotaItemId(quotaItemId); + if (quotaMaterials == null || quotaMaterials.isEmpty()) { + return; + } + + // 2. 复制到工作台 + List wbMaterials = new ArrayList<>(); + for (QuotaMarketMaterialDO quotaMaterial : quotaMaterials) { + WbBoqMarketMaterialDO wbMaterial = convertFromQuota(quotaMaterial, divisionId); + wbMaterials.add(wbMaterial); + } + + // 3. 批量插入 + wbBoqMarketMaterialMapper.insertBatch(wbMaterials); + + log.info("[copyFromQuota] 复制市场主材设备成功,divisionId={}, quotaItemId={}, count={}", + divisionId, quotaItemId, wbMaterials.size()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteByDivisionIds(List divisionIds) { + if (divisionIds == null || divisionIds.isEmpty()) { + return; + } + wbBoqMarketMaterialMapper.deleteByDivisionIds(divisionIds); + } + + // ========== 私有方法 ========== + + private WbBoqMarketMaterialDO validateExists(Long id) { + WbBoqMarketMaterialDO marketMaterial = wbBoqMarketMaterialMapper.selectById(id); + if (marketMaterial == null) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.WB_BOQ_MARKET_MATERIAL_NOT_EXISTS); + } + return marketMaterial; + } + + private Integer getMaxSortOrder(Long divisionId) { + List materials = wbBoqMarketMaterialMapper.selectListByDivisionId(divisionId); + if (materials == null || materials.isEmpty()) { + return 0; + } + return materials.stream() + .map(WbBoqMarketMaterialDO::getSortOrder) + .filter(java.util.Objects::nonNull) + .max(Integer::compareTo) + .orElse(0); + } + + private WbBoqMarketMaterialDO convertFromQuota(QuotaMarketMaterialDO quotaMaterial, Long divisionId) { + WbBoqMarketMaterialDO wbMaterial = new WbBoqMarketMaterialDO(); + wbMaterial.setTenantId(TenantContextHolder.getTenantId()); + wbMaterial.setDivisionId(divisionId); + wbMaterial.setSourceResourceItemId(quotaMaterial.getResourceItemId()); + wbMaterial.setSourceMarketMaterialId(quotaMaterial.getId()); + wbMaterial.setConsumeQty(quotaMaterial.getDosage()); + wbMaterial.setAdjustConsumeQty(quotaMaterial.getAdjustedDosage()); + wbMaterial.setSortOrder(quotaMaterial.getSortOrder()); + wbMaterial.setSourceType(WbBoqMarketMaterialDO.SOURCE_TYPE_SYSTEM); + wbMaterial.setAttributes(quotaMaterial.getAttributes()); + + // 从工料机库获取信息 + ResourceItemDO resourceItem = resourceItemMapper.selectById(quotaMaterial.getResourceItemId()); + if (resourceItem != null) { + wbMaterial.setCode(resourceItem.getCode()); + wbMaterial.setName(resourceItem.getName()); + wbMaterial.setSpec(resourceItem.getSpec()); + wbMaterial.setUnit(resourceItem.getUnit()); + wbMaterial.setResourceType(convertTypeToDisplay(resourceItem.getType())); + wbMaterial.setCategoryId(resourceItem.getCategoryId()); + wbMaterial.setTaxRate(resourceItem.getTaxRate()); + wbMaterial.setTaxExclBasePrice(resourceItem.getTaxExclBasePrice()); + wbMaterial.setTaxInclBasePrice(resourceItem.getTaxInclBasePrice()); + wbMaterial.setTaxExclCompilePrice(resourceItem.getTaxExclCompilePrice()); + wbMaterial.setTaxInclCompilePrice(resourceItem.getTaxInclCompilePrice()); + } + + return wbMaterial; + } + + private WbBoqMarketMaterialRespVO convertToVO(WbBoqMarketMaterialDO marketMaterial) { + WbBoqMarketMaterialRespVO vo = BeanUtil.copyProperties(marketMaterial, WbBoqMarketMaterialRespVO.class); + + // 从源工料机获取 calcBase + if (marketMaterial.getSourceResourceItemId() != null) { + ResourceItemDO resourceItem = resourceItemMapper.selectById(marketMaterial.getSourceResourceItemId()); + if (resourceItem != null) { + vo.setCalcBase(resourceItem.getCalcBase()); + } + } + + // 计算合价 + BigDecimal effectiveQty = marketMaterial.getAdjustConsumeQty() != null + ? marketMaterial.getAdjustConsumeQty() + : marketMaterial.getConsumeQty(); + + if (effectiveQty != null) { + vo.setTaxExclBaseTotalSum(multiplyOrZero(marketMaterial.getTaxExclBasePrice(), effectiveQty)); + vo.setTaxInclBaseTotalSum(multiplyOrZero(marketMaterial.getTaxInclBasePrice(), effectiveQty)); + vo.setTaxExclCompileTotalSum(multiplyOrZero(marketMaterial.getTaxExclCompilePrice(), effectiveQty)); + vo.setTaxInclCompileTotalSum(multiplyOrZero(marketMaterial.getTaxInclCompilePrice(), effectiveQty)); + } + + // 格式化数值 + formatVO(vo); + + return vo; + } + + private void formatVO(WbBoqMarketMaterialRespVO vo) { + // 价格/税率/合价:2位小数 + vo.setTaxRate(roundToScale(vo.getTaxRate(), 2)); + vo.setTaxExclBasePrice(roundToScale(vo.getTaxExclBasePrice(), 2)); + vo.setTaxInclBasePrice(roundToScale(vo.getTaxInclBasePrice(), 2)); + vo.setTaxExclCompilePrice(roundToScale(vo.getTaxExclCompilePrice(), 2)); + vo.setTaxInclCompilePrice(roundToScale(vo.getTaxInclCompilePrice(), 2)); + vo.setTaxExclBaseTotalSum(roundToScale(vo.getTaxExclBaseTotalSum(), 2)); + vo.setTaxInclBaseTotalSum(roundToScale(vo.getTaxInclBaseTotalSum(), 2)); + vo.setTaxExclCompileTotalSum(roundToScale(vo.getTaxExclCompileTotalSum(), 2)); + vo.setTaxInclCompileTotalSum(roundToScale(vo.getTaxInclCompileTotalSum(), 2)); + + // 消耗量:4位小数 + vo.setConsumeQty(roundToScale(vo.getConsumeQty(), 4)); + vo.setAdjustConsumeQty(roundToScale(vo.getAdjustConsumeQty(), 4)); + } + + private BigDecimal roundToScale(BigDecimal value, int scale) { + if (value == null) { + return null; + } + return value.setScale(scale, RoundingMode.HALF_UP); + } + + private BigDecimal multiplyOrZero(BigDecimal a, BigDecimal b) { + if (a == null || b == null) { + return BigDecimal.ZERO; + } + return a.multiply(b); + } + + private String convertTypeToDisplay(String type) { + if (type == null) { + return null; + } + switch (type) { + case "1": + return "labor"; + case "2": + return "material"; + case "3": + return "machine"; + default: + return type; + } + } + + /** + * 计算调整公式(虚拟字段) + * 与子目工料机逻辑一致,使用公共工具类 AdjustmentFormulaCalculator + */ + private void calculateAdjustmentFormulas(Long divisionId, List materials) { + List settings = wbAdjustmentSettingMapper.selectListByDivisionId(divisionId); + if (settings == null || settings.isEmpty()) { + return; + } + + AdjustmentFormulaCalculator.calculateFormulas( + materials, + settings, + WbBoqMarketMaterialRespVO::getCode, + WbBoqMarketMaterialRespVO::getCategoryId, + WbBoqMarketMaterialRespVO::setAdjustmentFormula + ); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbBoqResourceServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbBoqResourceServiceImpl.java new file mode 100644 index 0000000..7187727 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbBoqResourceServiceImpl.java @@ -0,0 +1,2034 @@ +package com.yhy.module.core.service.workbench.impl; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yhy.module.core.enums.ErrorCodeConstants.WB_BOQ_RESOURCE_CHILD_CONSUME_QTY_READONLY; +import static com.yhy.module.core.enums.ErrorCodeConstants.WB_BOQ_RESOURCE_DIVISION_NOT_QUOTA; +import static com.yhy.module.core.enums.ErrorCodeConstants.WB_BOQ_RESOURCE_MANUAL_CONSUME_QTY_READONLY; +import static com.yhy.module.core.enums.ErrorCodeConstants.WB_BOQ_RESOURCE_NOT_EXISTS; + +import cn.hutool.core.collection.CollUtil; +import com.yhy.module.core.controller.admin.quota.vo.QuotaResourceRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceCategorySummaryVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceSaveReqVO; +import com.yhy.module.core.convert.workbench.WbBoqResourceConvert; +import com.yhy.module.core.dal.dataobject.resource.ResourceCategoryDO; +import com.yhy.module.core.dal.dataobject.resource.ResourceItemDO; +import com.yhy.module.core.dal.dataobject.workbench.WbAdjustmentSettingDO; +import com.yhy.module.core.dal.dataobject.workbench.WbBoqDivisionDO; +import com.yhy.module.core.dal.dataobject.workbench.WbBoqResourceDO; +import com.yhy.module.core.dal.mysql.resource.ResourceItemMapper; +import com.yhy.module.core.dal.mysql.workbench.WbAdjustmentSettingMapper; +import com.yhy.module.core.dal.mysql.workbench.WbBoqDivisionMapper; +import com.yhy.module.core.dal.mysql.workbench.WbBoqResourceMapper; +import com.yhy.module.core.service.quota.QuotaResourceService; +import com.yhy.module.core.service.workbench.WbBoqResourceService; +import com.yhy.module.core.util.AdjustmentFormulaCalculator; +import com.yhy.module.core.util.FormulaEvaluator; +import com.yhy.module.core.util.ResourcePriceCalculator; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * 工作台工料机消耗 Service 实现类 + * + * @author yhy + */ +@Service +@Validated +@Slf4j +public class WbBoqResourceServiceImpl implements WbBoqResourceService { + + @Resource + private WbBoqResourceMapper wbBoqResourceMapper; + + @Resource + private WbBoqDivisionMapper wbBoqDivisionMapper; + + @Resource + private QuotaResourceService quotaResourceService; + + @Resource + private ResourceItemMapper resourceItemMapper; + + @Resource + private WbAdjustmentSettingMapper wbAdjustmentSettingMapper; + + @Resource + private com.yhy.module.core.dal.mysql.resource.ResourceInfoPriceMappingMapper resourceInfoPriceMappingMapper; + + @Resource + private com.yhy.module.core.dal.mysql.infoprice.InfoPriceResourcePriceMapper infoPriceResourcePriceMapper; + + @Resource + private com.yhy.module.core.dal.mysql.infoprice.InfoPriceResourceMapper infoPriceResourceMapper; + + @Resource + private com.yhy.module.core.dal.mysql.infoprice.InfoPriceCategoryTreeMapper infoPriceCategoryTreeMapper; + + @Resource + private com.yhy.module.core.dal.mysql.infoprice.InfoPriceBookMapper infoPriceBookMapper; + + @Resource + private com.yhy.module.core.dal.mysql.infoprice.InfoPriceTreeMapper infoPriceTreeMapper; + + @Resource + private com.yhy.module.core.dal.mysql.workbench.WbCompileTreeMapper wbCompileTreeMapper; + + @Resource + private com.yhy.module.core.dal.mysql.resource.ResourceCategoryMapper resourceCategoryMapper; + + @Resource + private com.yhy.module.core.service.quota.QuotaItemService quotaItemService; + + @Resource + private com.yhy.module.core.service.workbench.WbSnapshotReadService wbSnapshotReadService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long create(WbBoqResourceSaveReqVO createReqVO) { + // 1. 校验定额节点 + validateDivisionIsQuota(createReqVO.getDivisionId()); + + // 2. 构建DO + WbBoqResourceDO resource = WbBoqResourceConvert.INSTANCE.convert(createReqVO); + + // 3. 租户添加工料机时,双表写入逻辑 + if (createReqVO.getSourceResourceItemId() == null && + createReqVO.getName() != null && createReqVO.getUnit() != null) { + // 查找或创建后台工料机 + Long resourceItemId = findOrCreateResourceItem( + createReqVO.getName(), + createReqVO.getSpec(), + createReqVO.getUnit(), + createReqVO.getResourceType() + ); + resource.setSourceResourceItemId(resourceItemId); + resource.setSourceType(WbBoqResourceDO.SOURCE_TYPE_TENANT); + } + + // 4. 设置默认值 + if (resource.getConsumeQty() == null) { + resource.setConsumeQty(BigDecimal.ZERO); + } + if (resource.getAdjustRate() == null) { + resource.setAdjustRate(BigDecimal.ONE); + } + if (resource.getSourceType() == null) { + resource.setSourceType(WbBoqResourceDO.SOURCE_TYPE_SYSTEM); + } + + // 5. 设置排序号 + Integer maxSortOrder = wbBoqResourceMapper.selectMaxSortOrderByDivisionId(createReqVO.getDivisionId()); + resource.setSortOrder(maxSortOrder + 1); + + // 6. 插入数据库 + wbBoqResourceMapper.insert(resource); + + return resource.getId(); + } + + /** + * 查找或创建后台工料机(根据名称、型号、单位全局唯一) + */ + private Long findOrCreateResourceItem(String name, String spec, String unit, String resourceType) { + return findOrCreateResourceItem(name, spec, unit, resourceType, null); + } + + /** + * 查找或创建后台工料机(根据名称、型号、单位全局唯一) + * @param name 名称 + * @param spec 型号规格 + * @param unit 单位 + * @param resourceType 资源类型 + * @param sourceResourceItemId 原工料机ID(用于获取 catalogItemId) + */ + private Long findOrCreateResourceItem(String name, String spec, String unit, String resourceType, Long sourceResourceItemId) { + // 1. 查找是否已存在 + ResourceItemDO existItem = resourceItemMapper.selectByNameSpecUnit(name, spec, unit); + if (existItem != null) { + return existItem.getId(); + } + + // 2. 不存在则创建新记录 + ResourceItemDO newItem = new ResourceItemDO(); + newItem.setName(name); + newItem.setSpec(spec); + newItem.setUnit(unit); + newItem.setType(resourceType); + newItem.setSourceType(ResourceItemDO.SOURCE_TYPE_TENANT); + + // 3. 设置 catalogItemId(从原工料机获取,或使用默认值1) + Long catalogItemId = 1L; // 默认值 + if (sourceResourceItemId != null) { + ResourceItemDO sourceItem = resourceItemMapper.selectById(sourceResourceItemId); + if (sourceItem != null && sourceItem.getCatalogItemId() != null) { + catalogItemId = sourceItem.getCatalogItemId(); + } + } + newItem.setCatalogItemId(catalogItemId); + + resourceItemMapper.insert(newItem); + + log.info("租户创建新工料机:name={}, spec={}, unit={}, catalogItemId={}, id={}", + name, spec, unit, catalogItemId, newItem.getId()); + return newItem.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceBatchUpdateResultVO update(WbBoqResourceSaveReqVO updateReqVO) { + // 1. 校验存在 + WbBoqResourceDO existResource = validateResourceExists(updateReqVO.getId()); + + // 2. 复合工料机的子工料机禁止修改定额消耗量 + if (existResource.getParentId() != null) { + // 检查是否尝试修改 consumeQty + if (updateReqVO.getConsumeQty() != null && + !updateReqVO.getConsumeQty().equals(existResource.getConsumeQty())) { + throw exception(WB_BOQ_RESOURCE_CHILD_CONSUME_QTY_READONLY); + } + } + + // 2.1 手动新增的工料机(consumeQty为null且sourceType=tenant),禁止修改定额消耗量 + if (existResource.getConsumeQty() == null + && WbBoqResourceDO.SOURCE_TYPE_TENANT.equals(existResource.getSourceType())) { + if (updateReqVO.getConsumeQty() != null) { + throw exception(WB_BOQ_RESOURCE_MANUAL_CONSUME_QTY_READONLY); + } + } + + // 3. 更新 + WbBoqResourceDO updateObj = WbBoqResourceConvert.INSTANCE.convert(updateReqVO); + updateObj.setId(existResource.getId()); + // 保留不可修改的字段 + updateObj.setDivisionId(existResource.getDivisionId()); + updateObj.setSortOrder(existResource.getSortOrder()); + updateObj.setParentId(existResource.getParentId()); // 保留父子关系,防止复合工料机结构被破坏 + updateObj.setCategoryId(existResource.getCategoryId()); // 保留机类ID,防止被覆盖为null + + // 4. 检查是否修改了唯一性字段(名称、型号规格、单位) + boolean nameChanged = updateReqVO.getName() != null && !updateReqVO.getName().equals(existResource.getName()); + boolean specChanged = (updateReqVO.getSpec() != null && !updateReqVO.getSpec().equals(existResource.getSpec())) + || (updateReqVO.getSpec() == null && existResource.getSpec() != null); + boolean unitChanged = updateReqVO.getUnit() != null && !updateReqVO.getUnit().equals(existResource.getUnit()); + + if (nameChanged || specChanged || unitChanged) { + // 唯一性字段发生变化,需要检查后台工料机表 + String newName = updateReqVO.getName() != null ? updateReqVO.getName() : existResource.getName(); + String newSpec = updateReqVO.getSpec() != null ? updateReqVO.getSpec() : existResource.getSpec(); + String newUnit = updateReqVO.getUnit() != null ? updateReqVO.getUnit() : existResource.getUnit(); + String resourceType = updateReqVO.getResourceType() != null ? updateReqVO.getResourceType() : existResource.getResourceType(); + + // 查找或创建后台工料机(传递原工料机ID用于获取catalogItemId) + Long newResourceItemId = findOrCreateResourceItem(newName, newSpec, newUnit, resourceType, existResource.getSourceResourceItemId()); + updateObj.setSourceResourceItemId(newResourceItemId); + updateObj.setSourceType(WbBoqResourceDO.SOURCE_TYPE_TENANT); + + log.info("工料机唯一性字段变更,更新引用:id={}, oldSourceId={}, newSourceId={}", + updateReqVO.getId(), existResource.getSourceResourceItemId(), newResourceItemId); + } + + // 5. 单位为 % 的工料机:自动清空税率和价格字段 + String finalUnit = updateReqVO.getUnit() != null ? updateReqVO.getUnit() : existResource.getUnit(); + if ("%".equals(finalUnit)) { + updateObj.setTaxRate(null); + updateObj.setTaxExclBasePrice(null); + updateObj.setTaxInclBasePrice(null); + updateObj.setTaxExclCompilePrice(null); + updateObj.setTaxInclCompilePrice(null); + log.info("单位为%的工料机,自动清空税率和价格字段:id={}", updateReqVO.getId()); + } + + // 6. 税率与价格联动(仅普通工料机:非%单位、非子工料机) + if (!"%".equals(finalUnit) && existResource.getParentId() == null) { + applyTaxRatePriceLinkage(updateObj, updateReqVO, existResource); + } + + // 7. 用量与调整消耗量联动 + applyUsageQtyLinkage(updateObj, updateReqVO, existResource); + + wbBoqResourceMapper.updateById(updateObj); + + // 8. 项目级编制价/价格来源批量同步 + return syncCompilePriceAcrossProject(updateObj, updateReqVO, existResource); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Long id) { + // 1. 校验存在 + validateResourceExists(id); + + // 2. 删除 + wbBoqResourceMapper.deleteById(id); + } + + @Override + public WbBoqResourceRespVO get(Long id) { + WbBoqResourceDO resource = wbBoqResourceMapper.selectById(id); + if (resource == null) { + return null; + } + return WbBoqResourceConvert.INSTANCE.convert(resource); + } + + @Override + public List getListByDivisionId(Long divisionId) { + // 1. 获取分部分项节点 + WbBoqDivisionDO division = wbBoqDivisionMapper.selectById(divisionId); + if (division == null || !"quota".equals(division.getNodeType())) { + return new ArrayList<>(); + } + + // 2. 从本地表查询工料机数据(复制模式) + List resources = wbBoqResourceMapper.selectListByDivisionId(divisionId); + + // 3. 如果本地没有数据,尝试从后台复制 + if (CollUtil.isEmpty(resources)) { + Long quotaItemId = division.getSourceQuotaItemId(); + if (quotaItemId != null) { + // 从后台复制工料机数据 + copyFromQuotaResource(divisionId, quotaItemId); + // 重新查询 + resources = wbBoqResourceMapper.selectListByDivisionId(divisionId); + } + } + + if (CollUtil.isEmpty(resources)) { + return new ArrayList<>(); + } + + // 4. 转换为VO格式,处理父子关系(传入定额工程量用于计算合价) + BigDecimal quotaQty = division.getQty() != null ? division.getQty() : BigDecimal.ONE; + List result = convertToVOList(resources, quotaQty); + + // 5. 填充价格来源状态并虚拟替换价格(必须在%工料机计算之前,否则%工料机使用的是未替换的价格) + fillPriceSourceStatus(result, division); + + // 6. 计算单位为%的工料机合价(需要先有价格来源替换后的合价数据) + calculatePercentUnitTotalSums(result); + + // 7. 计算调整公式(虚拟字段) + calculateAdjustmentFormulas(divisionId, result); + + // 8. 格式化数值:价格/税率保留2位小数,消耗量保留4位小数 + result.forEach(this::formatResourceVO); + + return result; + } + + /** + * 格式化工料机VO的数值字段 + * 价格、税率、合价:保留2位小数,四舍五入 + * 消耗量:保留4位小数,四舍五入 + */ + private void formatResourceVO(WbBoqResourceRespVO vo) { + // 价格/税率/合价:2位小数 + vo.setTaxRate(roundToScale(vo.getTaxRate(), 2)); + vo.setTaxExclBasePrice(roundToScale(vo.getTaxExclBasePrice(), 2)); + vo.setTaxInclBasePrice(roundToScale(vo.getTaxInclBasePrice(), 2)); + vo.setTaxExclCompilePrice(roundToScale(vo.getTaxExclCompilePrice(), 2)); + vo.setTaxInclCompilePrice(roundToScale(vo.getTaxInclCompilePrice(), 2)); + vo.setTaxExclBaseTotalSum(roundToScale(vo.getTaxExclBaseTotalSum(), 2)); + vo.setTaxInclBaseTotalSum(roundToScale(vo.getTaxInclBaseTotalSum(), 2)); + vo.setTaxExclCompileTotalSum(roundToScale(vo.getTaxExclCompileTotalSum(), 2)); + vo.setTaxInclCompileTotalSum(roundToScale(vo.getTaxInclCompileTotalSum(), 2)); + + // 消耗量:4位小数 + vo.setConsumeQty(roundToScale(vo.getConsumeQty(), 4)); + vo.setAdjustConsumeQty(roundToScale(vo.getAdjustConsumeQty(), 4)); + + // 格式化子工料机 + if (vo.getChildren() != null) { + vo.getChildren().forEach(this::formatResourceVO); + } + } + + /** + * 四舍五入到指定小数位数 + */ + private BigDecimal roundToScale(BigDecimal value, int scale) { + if (value == null) { + return null; + } + return value.setScale(scale, java.math.RoundingMode.HALF_UP); + } + + /** + * 从后台定额工料机复制数据到工作台(快照模式) + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void copyFromQuotaResource(Long divisionId, Long quotaItemId) { + // 1. 获取后台定额工料机数据 + List quotaResources = quotaResourceService.getQuotaResourceList(quotaItemId); + if (CollUtil.isEmpty(quotaResources)) { + return; + } + + // 2. 删除已有数据(如果有) + wbBoqResourceMapper.deleteByDivisionId(divisionId); + + // 3. 复制数据到本地表 + int sortOrder = 0; + for (QuotaResourceRespVO quotaResource : quotaResources) { + // 复制主工料机 + WbBoqResourceDO resource = convertToDO(quotaResource, divisionId, null, ++sortOrder); + wbBoqResourceMapper.insert(resource); + + // 复制子工料机(复合工料机) + Boolean isMerged = quotaResource.getIsMerged(); + List mergedItems = quotaResource.getMergedItems(); + log.debug("复制工料机: id={}, name={}, isMerged={}, mergedItems={}", + quotaResource.getId(), quotaResource.getResourceName(), isMerged, + mergedItems != null ? mergedItems.size() : "null"); + + if (mergedItems != null && !mergedItems.isEmpty()) { + int childSortOrder = 0; + for (QuotaResourceRespVO.MergedResourceItemVO mergedItem : mergedItems) { + WbBoqResourceDO childResource = convertMergedToDO(mergedItem, divisionId, resource.getId(), ++childSortOrder); + wbBoqResourceMapper.insert(childResource); + log.debug("复制子工料机: parentId={}, childName={}", resource.getId(), mergedItem.getResourceName()); + } + } + } + + log.info("从后台复制工料机数据完成,divisionId={}, quotaItemId={}, count={}", divisionId, quotaItemId, quotaResources.size()); + } + + /** + * 将后台定额工料机转换为本地DO + */ + private WbBoqResourceDO convertToDO(QuotaResourceRespVO quotaResource, Long divisionId, Long parentId, int sortOrder) { + WbBoqResourceDO resource = new WbBoqResourceDO(); + resource.setDivisionId(divisionId); + resource.setParentId(parentId); + resource.setSourceResourceItemId(quotaResource.getResourceItemId()); + resource.setSourceQuotaResourceId(quotaResource.getId()); + resource.setResourceType(quotaResource.getResourceType()); + resource.setCode(quotaResource.getResourceCode()); + resource.setName(quotaResource.getResourceName()); + resource.setSpec(quotaResource.getResourceSpec()); + resource.setUnit(quotaResource.getResourceUnit()); + resource.setConsumeQty(quotaResource.getActualDosage()); + resource.setTaxRate(quotaResource.getResourceTaxRate()); + resource.setTaxExclBasePrice(quotaResource.getResourceTaxExclBasePrice()); + resource.setTaxInclBasePrice(quotaResource.getResourceTaxInclBasePrice()); + resource.setTaxExclCompilePrice(quotaResource.getResourceTaxExclCompilePrice()); + resource.setTaxInclCompilePrice(quotaResource.getResourceTaxInclCompilePrice()); + resource.setAdjustRate(BigDecimal.ONE); + resource.setCategoryId(quotaResource.getResourceCategoryId()); + resource.setSortOrder(sortOrder); + // 设置来源类型为系统(从定额复制) + resource.setSourceType(WbBoqResourceDO.SOURCE_TYPE_SYSTEM); + // 保存基线值(快照) + resource.setBaseConsumeQty(quotaResource.getActualDosage()); + resource.setBaseBasePrice(quotaResource.getResourceTaxExclBasePrice()); + return resource; + } + + /** + * 将复合工料机的子项转换为本地DO + */ + private WbBoqResourceDO convertMergedToDO(QuotaResourceRespVO.MergedResourceItemVO mergedItem, Long divisionId, Long parentId, int sortOrder) { + WbBoqResourceDO resource = new WbBoqResourceDO(); + resource.setDivisionId(divisionId); + resource.setParentId(parentId); + resource.setSourceResourceItemId(mergedItem.getResourceItemId()); + resource.setSourceQuotaResourceId(mergedItem.getId()); + resource.setResourceType(mergedItem.getResourceType()); + resource.setCode(mergedItem.getResourceCode()); + resource.setName(mergedItem.getResourceName()); + resource.setSpec(mergedItem.getResourceSpec()); + resource.setUnit(mergedItem.getResourceUnit()); + resource.setConsumeQty(mergedItem.getDosage()); + resource.setTaxRate(mergedItem.getResourceTaxRate()); + resource.setTaxExclBasePrice(mergedItem.getResourceTaxExclBasePrice()); + resource.setTaxInclBasePrice(mergedItem.getResourceTaxInclBasePrice()); + resource.setTaxExclCompilePrice(mergedItem.getResourceTaxExclCompilePrice()); + resource.setTaxInclCompilePrice(mergedItem.getResourceTaxInclCompilePrice()); + resource.setAdjustRate(BigDecimal.ONE); + resource.setCategoryId(mergedItem.getResourceCategoryId()); + resource.setSortOrder(sortOrder); + // 设置来源类型为系统(从定额复制) + resource.setSourceType(WbBoqResourceDO.SOURCE_TYPE_SYSTEM); + // 保存基线值(快照) + resource.setBaseConsumeQty(mergedItem.getDosage()); + resource.setBaseBasePrice(mergedItem.getResourceTaxExclBasePrice()); + return resource; + } + + /** + * 将DO列表转换为VO列表(处理父子关系) + * 优化:批量预加载所有 ResourceItemDO,避免 N+1 查询 + * @param resources 工料机DO列表 + * @param quotaQty 定额工程量,用于计算合价(合价 = 用量 × 单价,用量 = 定额工程量 × 消耗量) + */ + private List convertToVOList(List resources, BigDecimal quotaQty) { + // 批量预加载所有需要的 ResourceItemDO(避免 convertToVO 中逐个查询) + List sourceResourceItemIds = resources.stream() + .map(WbBoqResourceDO::getSourceResourceItemId) + .filter(java.util.Objects::nonNull) + .distinct() + .collect(java.util.stream.Collectors.toList()); + Map resourceItemMap = new java.util.HashMap<>(); + if (!sourceResourceItemIds.isEmpty()) { + List resourceItems = resourceItemMapper.selectBatchIds(sourceResourceItemIds); + resourceItemMap = resourceItems.stream() + .collect(java.util.stream.Collectors.toMap(ResourceItemDO::getId, r -> r, (a, b) -> a)); + } + + // 分离父工料机和子工料机 + List parentResources = new ArrayList<>(); + Map> childrenMap = new java.util.HashMap<>(); + + for (WbBoqResourceDO resource : resources) { + if (resource.getParentId() == null) { + parentResources.add(resource); + } else { + childrenMap.computeIfAbsent(resource.getParentId(), k -> new ArrayList<>()).add(resource); + } + } + + // 转换为VO + List result = new ArrayList<>(); + for (WbBoqResourceDO parent : parentResources) { + WbBoqResourceRespVO vo = convertToVO(parent, resourceItemMap, quotaQty); + // 添加子工料机 + List children = childrenMap.get(parent.getId()); + if (children != null && !children.isEmpty()) { + // 子工料机用量计算: + // 1. 单位为%:用量 = 消耗量(直接引用) + // 2. 其他单位:用量 = 父工料机用量 × 子工料机消耗量 + BigDecimal parentUsageQty = vo.getUsageQty(); + List childVOs = new ArrayList<>(); + List percentChildVOs = new ArrayList<>(); // 单位为%的子工料机,需要延迟计算合价 + + // 第一轮:计算非%单位子工料机的用量和合价 + for (WbBoqResourceDO child : children) { + WbBoqResourceRespVO childVO = convertToVO(child, resourceItemMap, null); + BigDecimal childConsumeQty = child.getAdjustConsumeQty() != null + ? child.getAdjustConsumeQty() : child.getConsumeQty(); + + if ("%".equals(child.getUnit())) { + // 单位为%:用量直接等于消耗量,合价延迟计算 + childVO.setUsageQty(childConsumeQty); + percentChildVOs.add(childVO); + } else if (parentUsageQty != null && childConsumeQty != null) { + // 普通子工料机:用量 = 父用量 × 子消耗量 + BigDecimal childUsageQty = parentUsageQty.multiply(childConsumeQty).setScale(4, java.math.RoundingMode.HALF_UP); + childVO.setUsageQty(childUsageQty); + // 基于子工料机用量计算除税合价和含税合价(合价 = 用量 × 编制价) + if (childVO.getTaxExclCompilePrice() != null) { + childVO.setTaxExclTotalSum(childUsageQty.multiply(childVO.getTaxExclCompilePrice()).setScale(2, java.math.RoundingMode.HALF_UP)); + } + if (childVO.getTaxInclCompilePrice() != null) { + childVO.setTaxInclTotalSum(childUsageQty.multiply(childVO.getTaxInclCompilePrice()).setScale(2, java.math.RoundingMode.HALF_UP)); + } + // 四个合价字段 = 单价 × 调整消耗量(非%子工料机) + if (childVO.getTaxExclBasePrice() != null) { + childVO.setTaxExclBaseTotalSum(childVO.getTaxExclBasePrice().multiply(childConsumeQty).setScale(2, java.math.RoundingMode.HALF_UP)); + } + if (childVO.getTaxInclBasePrice() != null) { + childVO.setTaxInclBaseTotalSum(childVO.getTaxInclBasePrice().multiply(childConsumeQty).setScale(2, java.math.RoundingMode.HALF_UP)); + } + if (childVO.getTaxExclCompilePrice() != null) { + childVO.setTaxExclCompileTotalSum(childVO.getTaxExclCompilePrice().multiply(childConsumeQty).setScale(2, java.math.RoundingMode.HALF_UP)); + } + if (childVO.getTaxInclCompilePrice() != null) { + childVO.setTaxInclCompileTotalSum(childVO.getTaxInclCompilePrice().multiply(childConsumeQty).setScale(2, java.math.RoundingMode.HALF_UP)); + } + } + childVOs.add(childVO); + } + + // 构建子工料机的类别合价映射(用于%子工料机的calcBase公式计算) + // 六组映射:除税基价合价、含税基价合价、除税编制合价、含税编制合价、除税合价、含税合价 + Map childCategoryTaxExclBaseMap = new HashMap<>(); + Map childCategoryTaxInclBaseMap = new HashMap<>(); + Map childCategoryTaxExclCompileMap = new HashMap<>(); + Map childCategoryTaxInclCompileMap = new HashMap<>(); + Map childCategoryTaxExclMap = new HashMap<>(); + Map childCategoryTaxInclMap = new HashMap<>(); + for (WbBoqResourceRespVO childVO : childVOs) { + if (!"%".equals(childVO.getUnit()) && childVO.getCategoryId() != null) { + Long catId = childVO.getCategoryId(); + if (childVO.getTaxExclBaseTotalSum() != null) { + childCategoryTaxExclBaseMap.merge(catId, childVO.getTaxExclBaseTotalSum(), BigDecimal::add); + } + if (childVO.getTaxInclBaseTotalSum() != null) { + childCategoryTaxInclBaseMap.merge(catId, childVO.getTaxInclBaseTotalSum(), BigDecimal::add); + } + if (childVO.getTaxExclCompileTotalSum() != null) { + childCategoryTaxExclCompileMap.merge(catId, childVO.getTaxExclCompileTotalSum(), BigDecimal::add); + } + if (childVO.getTaxInclCompileTotalSum() != null) { + childCategoryTaxInclCompileMap.merge(catId, childVO.getTaxInclCompileTotalSum(), BigDecimal::add); + } + if (childVO.getTaxExclTotalSum() != null) { + childCategoryTaxExclMap.merge(catId, childVO.getTaxExclTotalSum(), BigDecimal::add); + } + if (childVO.getTaxInclTotalSum() != null) { + childCategoryTaxInclMap.merge(catId, childVO.getTaxInclTotalSum(), BigDecimal::add); + } + } + } + + // 第二轮:计算%单位子工料机的合价(使用calcBase公式计算基数) + for (WbBoqResourceRespVO percentChild : percentChildVOs) { + BigDecimal usageQty = percentChild.getUsageQty(); + Map calcBase = percentChild.getCalcBase(); + if (usageQty != null && calcBase != null && calcBase.containsKey("formula") && calcBase.containsKey("variables")) { + try { + String formula = (String) calcBase.get("formula"); + @SuppressWarnings("unchecked") + Map variablesConfig = (Map) calcBase.get("variables"); + + // 构建六组变量映射 + Map taxExclBaseVars = new HashMap<>(); + Map taxInclBaseVars = new HashMap<>(); + Map taxExclCompileVars = new HashMap<>(); + Map taxInclCompileVars = new HashMap<>(); + Map taxExclVars = new HashMap<>(); + Map taxInclVars = new HashMap<>(); + + for (Map.Entry varEntry : variablesConfig.entrySet()) { + String varName = varEntry.getKey(); + Long categoryId = Long.valueOf(varEntry.getValue().toString()); + // 当公式某些条件不满足时按0处理 + taxExclBaseVars.put(varName, childCategoryTaxExclBaseMap.getOrDefault(categoryId, BigDecimal.ZERO)); + taxInclBaseVars.put(varName, childCategoryTaxInclBaseMap.getOrDefault(categoryId, BigDecimal.ZERO)); + taxExclCompileVars.put(varName, childCategoryTaxExclCompileMap.getOrDefault(categoryId, BigDecimal.ZERO)); + taxInclCompileVars.put(varName, childCategoryTaxInclCompileMap.getOrDefault(categoryId, BigDecimal.ZERO)); + taxExclVars.put(varName, childCategoryTaxExclMap.getOrDefault(categoryId, BigDecimal.ZERO)); + taxInclVars.put(varName, childCategoryTaxInclMap.getOrDefault(categoryId, BigDecimal.ZERO)); + } + + // 计算六个基数值 + BigDecimal taxExclBaseResult = FormulaEvaluator.evaluate(formula, taxExclBaseVars); + BigDecimal taxInclBaseResult = FormulaEvaluator.evaluate(formula, taxInclBaseVars); + BigDecimal taxExclCompileResult = FormulaEvaluator.evaluate(formula, taxExclCompileVars); + BigDecimal taxInclCompileResult = FormulaEvaluator.evaluate(formula, taxInclCompileVars); + BigDecimal taxExclResult = FormulaEvaluator.evaluate(formula, taxExclVars); + BigDecimal taxInclResult = FormulaEvaluator.evaluate(formula, taxInclVars); + + BigDecimal factor = usageQty.divide(new BigDecimal("100"), 6, java.math.RoundingMode.HALF_UP); + // 六个合价 = calcBase公式计算值 × 用量% + percentChild.setTaxExclBaseTotalSum(taxExclBaseResult.multiply(factor).setScale(2, java.math.RoundingMode.HALF_UP)); + percentChild.setTaxInclBaseTotalSum(taxInclBaseResult.multiply(factor).setScale(2, java.math.RoundingMode.HALF_UP)); + percentChild.setTaxExclCompileTotalSum(taxExclCompileResult.multiply(factor).setScale(2, java.math.RoundingMode.HALF_UP)); + percentChild.setTaxInclCompileTotalSum(taxInclCompileResult.multiply(factor).setScale(2, java.math.RoundingMode.HALF_UP)); + percentChild.setTaxExclTotalSum(taxExclResult.multiply(factor).setScale(2, java.math.RoundingMode.HALF_UP)); + percentChild.setTaxInclTotalSum(taxInclResult.multiply(factor).setScale(2, java.math.RoundingMode.HALF_UP)); + } catch (Exception e) { + log.error("[convertToVOList] 计算%子工料机合价失败,error={}", e.getMessage(), e); + } + } + } + + vo.setChildren(childVOs); + // 复合工料机:父级合价 = 子项合价之和 + calculateMergedParentTotalSums(vo, childVOs); + // 复合工料机:计算单价(单价 = 合价 / 用量) + calculateMergedParentPrices(vo, quotaQty); + } + result.add(vo); + } + return result; + } + + /** + * 将DO转换为VO(不计算合价,用于汇总场景) + */ + private WbBoqResourceRespVO convertToVO(WbBoqResourceDO resource, Map resourceItemMap) { + return convertToVO(resource, resourceItemMap, null); + } + + /** + * 将DO转换为VO(使用预加载的 ResourceItemDO 映射) + * @param quotaQty 定额工程量,用于计算合价(合价 = 用量 × 单价),为null时不计算合价 + */ + private WbBoqResourceRespVO convertToVO(WbBoqResourceDO resource, Map resourceItemMap, BigDecimal quotaQty) { + WbBoqResourceRespVO vo = new WbBoqResourceRespVO(); + vo.setId(resource.getId()); + vo.setDivisionId(resource.getDivisionId()); + vo.setSourceResourceItemId(resource.getSourceResourceItemId()); + vo.setSourceQuotaResourceId(resource.getSourceQuotaResourceId()); + vo.setResourceType(resource.getResourceType()); + vo.setCode(resource.getCode()); + vo.setName(resource.getName()); + vo.setSpec(resource.getSpec()); + vo.setUnit(resource.getUnit()); + vo.setConsumeQty(resource.getConsumeQty()); + vo.setAdjustConsumeQty(resource.getAdjustConsumeQty()); + vo.setAdjustRate(resource.getAdjustRate()); + + // 单位为 % 的工料机:税率和价格字段强制为空 + if ("%".equals(resource.getUnit())) { + vo.setTaxRate(null); + vo.setTaxExclBasePrice(null); + vo.setTaxInclBasePrice(null); + vo.setTaxExclCompilePrice(null); + vo.setTaxInclCompilePrice(null); + } else { + vo.setTaxRate(resource.getTaxRate()); + vo.setTaxExclBasePrice(resource.getTaxExclBasePrice()); + vo.setTaxInclBasePrice(resource.getTaxInclBasePrice()); + vo.setTaxExclCompilePrice(resource.getTaxExclCompilePrice()); + vo.setTaxInclCompilePrice(resource.getTaxInclCompilePrice()); + } + vo.setCategoryId(resource.getCategoryId()); + vo.setSortOrder(resource.getSortOrder()); + // 用量计算逻辑: + // 1. 单位为 % 的工料机:用量 = 消耗量(直接引用) + // 2. 其他工料机:用量 = 定额工程量 × 消耗量 + BigDecimal consumeQtyForUsage = resource.getAdjustConsumeQty() != null ? resource.getAdjustConsumeQty() : resource.getConsumeQty(); + if ("%".equals(resource.getUnit())) { + // 单位为%:用量直接等于消耗量 + vo.setUsageQty(consumeQtyForUsage); + } else if (quotaQty != null && consumeQtyForUsage != null) { + // 普通工料机:用量 = 定额工程量 × 消耗量 + vo.setUsageQty(quotaQty.multiply(consumeQtyForUsage).setScale(4, java.math.RoundingMode.HALF_UP)); + } else { + vo.setUsageQty(resource.getUsageQty()); + } + vo.setSourceType(resource.getSourceType()); + + // 从预加载的映射中补充 calcBase 和 categoryId(避免逐个查询) + if (resource.getSourceResourceItemId() != null) { + ResourceItemDO resourceItem = resourceItemMap.get(resource.getSourceResourceItemId()); + if (resourceItem != null) { + vo.setCalcBase(resourceItem.getCalcBase()); + // 兼容旧数据:如果 categoryId 为空,从源工料机获取 + if (vo.getCategoryId() == null && resourceItem.getCategoryId() != null) { + vo.setCategoryId(resourceItem.getCategoryId()); + } + } + } + + // 计算虚拟字段(合价) + // 单位为 % 的工料机:合价需要根据 calcBase 计算,这里暂不计算(由前端或其他服务处理) + if (!"%".equals(resource.getUnit())) { + // 优先使用调整消耗量,否则使用原始消耗量 + BigDecimal consumeQty = resource.getAdjustConsumeQty() != null ? resource.getAdjustConsumeQty() : resource.getConsumeQty(); + if (consumeQty != null) { + // 基价合价/编制合价 = 消耗量 × 单价(原有逻辑,保持不变) + if (resource.getTaxExclBasePrice() != null) { + vo.setTaxExclBaseTotalSum(consumeQty.multiply(resource.getTaxExclBasePrice())); + } + if (resource.getTaxInclBasePrice() != null) { + vo.setTaxInclBaseTotalSum(consumeQty.multiply(resource.getTaxInclBasePrice())); + } + if (resource.getTaxExclCompilePrice() != null) { + vo.setTaxExclCompileTotalSum(consumeQty.multiply(resource.getTaxExclCompilePrice())); + } + if (resource.getTaxInclCompilePrice() != null) { + vo.setTaxInclCompileTotalSum(consumeQty.multiply(resource.getTaxInclCompilePrice())); + } + // 除税合价/含税合价 = 用量 × 单价(用量 = 定额工程量 × 消耗量) + if (quotaQty != null) { + BigDecimal usageQty = quotaQty.multiply(consumeQty); + if (resource.getTaxExclCompilePrice() != null) { + vo.setTaxExclTotalSum(usageQty.multiply(resource.getTaxExclCompilePrice())); + } + if (resource.getTaxInclCompilePrice() != null) { + vo.setTaxInclTotalSum(usageQty.multiply(resource.getTaxInclCompilePrice())); + } + } + } + } + + return vo; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteByDivisionId(Long divisionId) { + wbBoqResourceMapper.deleteByDivisionId(divisionId); + } + + /** + * 校验定额节点是否为定额类型 + */ + private void validateDivisionIsQuota(Long divisionId) { + WbBoqDivisionDO division = wbBoqDivisionMapper.selectById(divisionId); + if (division == null || !WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(division.getNodeType())) { + throw exception(WB_BOQ_RESOURCE_DIVISION_NOT_QUOTA); + } + } + + /** + * 校验工料机存在 + */ + private WbBoqResourceDO validateResourceExists(Long id) { + WbBoqResourceDO resource = wbBoqResourceMapper.selectById(id); + if (resource == null) { + throw exception(WB_BOQ_RESOURCE_NOT_EXISTS); + } + return resource; + } + + /** + * 计算复合工料机父级的六个合价(= 子项合价之和) + * 包括:除税基价合价、含税基价合价、除税编制合价、含税编制合价、除税合价、含税合价 + */ + private void calculateMergedParentTotalSums(WbBoqResourceRespVO parent, List children) { + if (children == null || children.isEmpty()) { + return; + } + BigDecimal totalTaxExclBase = BigDecimal.ZERO; + BigDecimal totalTaxInclBase = BigDecimal.ZERO; + BigDecimal totalTaxExclCompile = BigDecimal.ZERO; + BigDecimal totalTaxInclCompile = BigDecimal.ZERO; + BigDecimal totalTaxExcl = BigDecimal.ZERO; + BigDecimal totalTaxIncl = BigDecimal.ZERO; + + for (WbBoqResourceRespVO child : children) { + if (child.getTaxExclBaseTotalSum() != null) totalTaxExclBase = totalTaxExclBase.add(child.getTaxExclBaseTotalSum()); + if (child.getTaxInclBaseTotalSum() != null) totalTaxInclBase = totalTaxInclBase.add(child.getTaxInclBaseTotalSum()); + if (child.getTaxExclCompileTotalSum() != null) totalTaxExclCompile = totalTaxExclCompile.add(child.getTaxExclCompileTotalSum()); + if (child.getTaxInclCompileTotalSum() != null) totalTaxInclCompile = totalTaxInclCompile.add(child.getTaxInclCompileTotalSum()); + if (child.getTaxExclTotalSum() != null) totalTaxExcl = totalTaxExcl.add(child.getTaxExclTotalSum()); + if (child.getTaxInclTotalSum() != null) totalTaxIncl = totalTaxIncl.add(child.getTaxInclTotalSum()); + } + + parent.setTaxExclBaseTotalSum(totalTaxExclBase.setScale(2, java.math.RoundingMode.HALF_UP)); + parent.setTaxInclBaseTotalSum(totalTaxInclBase.setScale(2, java.math.RoundingMode.HALF_UP)); + parent.setTaxExclCompileTotalSum(totalTaxExclCompile.setScale(2, java.math.RoundingMode.HALF_UP)); + parent.setTaxInclCompileTotalSum(totalTaxInclCompile.setScale(2, java.math.RoundingMode.HALF_UP)); + parent.setTaxExclTotalSum(totalTaxExcl.setScale(2, java.math.RoundingMode.HALF_UP)); + parent.setTaxInclTotalSum(totalTaxIncl.setScale(2, java.math.RoundingMode.HALF_UP)); + } + + /** + * 计算复合工料机父级的单价和合价 + * - 单价 = 对应合价(除税基价=除税基价合价、含税基价=含税基价合价、除税编制价=除税编制合价、含税编制价=含税编制合价) + * - 除税合价/含税合价 = 用量 × 单价(用量 = 定额工程量 × 消耗量) + */ + private void calculateMergedParentPrices(WbBoqResourceRespVO parent, BigDecimal quotaQty) { + // 获取消耗量(优先使用调整消耗量) + BigDecimal consumeQty = parent.getAdjustConsumeQty() != null ? parent.getAdjustConsumeQty() : parent.getConsumeQty(); + if (consumeQty == null || consumeQty.compareTo(BigDecimal.ZERO) == 0) { + return; + } + // 单价 = 对应合价 + if (parent.getTaxExclBaseTotalSum() != null) { + parent.setTaxExclBasePrice(parent.getTaxExclBaseTotalSum().setScale(2, java.math.RoundingMode.HALF_UP)); + } + if (parent.getTaxInclBaseTotalSum() != null) { + parent.setTaxInclBasePrice(parent.getTaxInclBaseTotalSum().setScale(2, java.math.RoundingMode.HALF_UP)); + } + if (parent.getTaxExclCompileTotalSum() != null) { + parent.setTaxExclCompilePrice(parent.getTaxExclCompileTotalSum().setScale(2, java.math.RoundingMode.HALF_UP)); + } + if (parent.getTaxInclCompileTotalSum() != null) { + parent.setTaxInclCompilePrice(parent.getTaxInclCompileTotalSum().setScale(2, java.math.RoundingMode.HALF_UP)); + } + // 除税合价/含税合价 = 用量 × 单价(用量 = 定额工程量 × 消耗量) + if (quotaQty != null) { + BigDecimal usageQty = quotaQty.multiply(consumeQty); + if (parent.getTaxExclCompilePrice() != null) { + parent.setTaxExclTotalSum(usageQty.multiply(parent.getTaxExclCompilePrice())); + } + if (parent.getTaxInclCompilePrice() != null) { + parent.setTaxInclTotalSum(usageQty.multiply(parent.getTaxInclCompilePrice())); + } + } + } + + /** + * 计算顶层单位为%的工料机合价 + * 四个合价字段分别使用对应的分类合价来计算: + * - 除税基价合价 = 计算基数(除税基价合价)× 调整消耗量% + * - 含税基价合价 = 计算基数(含税基价合价)× 调整消耗量% + * - 除税编制合价 = 计算基数(除税编制合价)× 调整消耗量% + * - 含税编制合价 = 计算基数(含税编制合价)× 调整消耗量% + * 注意:复合工料机内部的%子工料机已在 calculateMergedParentTotalSums 中处理 + */ + private void calculatePercentUnitTotalSums(List resources) { + if (CollUtil.isEmpty(resources)) { + return; + } + // 四个合价字段使用原来的分类合价(消耗量×单价) + // 只处理顶层资源(复合工料机的子工料机已在convertToVOList中清空合价字段) + ResourcePriceCalculator.calculatePercentUnitTotalSums( + resources, resources, wbBoqResourceFieldAccessor()); + // 计算%工料机的除税合价和含税合价(使用用量×单价的分类合价) + calculatePercentUnitUsageTotalSums(resources); + } + + /** + * 计算%工料机的四个合价字段 + * 使用四个合价(除税基价合价、含税基价合价、除税编制价合价、含税编制价合价)作为分类合价来源 + */ + private void calculatePercentUnitUsageTotalSums(List resources) { + // 1. 构建类别价格映射表(存储四个合价:除税基价、含税基价、除税编制价、含税编制价) + Map categoryPriceMap = new HashMap<>(); + for (WbBoqResourceRespVO vo : resources) { + if ("%".equals(vo.getUnit())) { + continue; + } + Long categoryId = vo.getCategoryId(); + if (categoryId != null) { + BigDecimal[] sums = categoryPriceMap.computeIfAbsent(categoryId, + k -> new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO}); + // sums[0]=除税基价合价, sums[1]=含税基价合价, sums[2]=除税编制价合价, sums[3]=含税编制价合价 + if (vo.getTaxExclBaseTotalSum() != null) sums[0] = sums[0].add(vo.getTaxExclBaseTotalSum()); + if (vo.getTaxInclBaseTotalSum() != null) sums[1] = sums[1].add(vo.getTaxInclBaseTotalSum()); + if (vo.getTaxExclTotalSum() != null) sums[2] = sums[2].add(vo.getTaxExclTotalSum()); + if (vo.getTaxInclTotalSum() != null) sums[3] = sums[3].add(vo.getTaxInclTotalSum()); + } + } + + // 2. 计算%工料机的四个合价字段(除税定额合价、含税定额合价、除税合价、含税合价) + // 公式:合价 = 计算基数公式得出的值 × 用量% + // 当公式某些条件不满足时按0处理(例如"人+机",没有类别为人时就当"0+机") + for (WbBoqResourceRespVO vo : resources) { + if (!"%".equals(vo.getUnit())) { + continue; + } + Map calcBase = vo.getCalcBase(); + if (calcBase == null || !calcBase.containsKey("formula") || !calcBase.containsKey("variables")) { + continue; + } + try { + String formula = (String) calcBase.get("formula"); + @SuppressWarnings("unchecked") + Map variablesConfig = (Map) calcBase.get("variables"); + + // 构建四组变量映射(除税基价、含税基价、除税编制价、含税编制价) + Map taxExclBaseVars = new HashMap<>(); + Map taxInclBaseVars = new HashMap<>(); + Map taxExclCompileVars = new HashMap<>(); + Map taxInclCompileVars = new HashMap<>(); + + for (Map.Entry varEntry : variablesConfig.entrySet()) { + String varName = varEntry.getKey(); + Long categoryId = Long.valueOf(varEntry.getValue().toString()); + BigDecimal[] sums = categoryPriceMap.get(categoryId); + // 当公式某些条件不满足时按0处理 + if (sums == null) { + taxExclBaseVars.put(varName, BigDecimal.ZERO); + taxInclBaseVars.put(varName, BigDecimal.ZERO); + taxExclCompileVars.put(varName, BigDecimal.ZERO); + taxInclCompileVars.put(varName, BigDecimal.ZERO); + } else { + // sums[0]=除税基价合价, sums[1]=含税基价合价, sums[2]=除税编制价合价, sums[3]=含税编制价合价 + taxExclBaseVars.put(varName, sums[0] != null ? sums[0] : BigDecimal.ZERO); + taxInclBaseVars.put(varName, sums[1] != null ? sums[1] : BigDecimal.ZERO); + taxExclCompileVars.put(varName, sums.length > 2 && sums[2] != null ? sums[2] : BigDecimal.ZERO); + taxInclCompileVars.put(varName, sums.length > 3 && sums[3] != null ? sums[3] : BigDecimal.ZERO); + } + } + + // 计算四个基数值 + BigDecimal taxExclBaseResult = FormulaEvaluator.evaluate(formula, taxExclBaseVars); + BigDecimal taxInclBaseResult = FormulaEvaluator.evaluate(formula, taxInclBaseVars); + BigDecimal taxExclCompileResult = FormulaEvaluator.evaluate(formula, taxExclCompileVars); + BigDecimal taxInclCompileResult = FormulaEvaluator.evaluate(formula, taxInclCompileVars); + + // 用量% = 消耗量 / 100 + BigDecimal dosage = vo.getUsageQty(); // 用量已经等于消耗量 + if (dosage != null) { + BigDecimal factor = dosage.divide(new BigDecimal("100"), 6, java.math.RoundingMode.HALF_UP); + // 四个合价 = 计算基数 × 用量% + vo.setTaxExclBaseTotalSum(taxExclBaseResult.multiply(factor).setScale(2, java.math.RoundingMode.HALF_UP)); + vo.setTaxInclBaseTotalSum(taxInclBaseResult.multiply(factor).setScale(2, java.math.RoundingMode.HALF_UP)); + vo.setTaxExclTotalSum(taxExclCompileResult.multiply(factor).setScale(2, java.math.RoundingMode.HALF_UP)); + vo.setTaxInclTotalSum(taxInclCompileResult.multiply(factor).setScale(2, java.math.RoundingMode.HALF_UP)); + } + } catch (Exception e) { + log.error("[calculatePercentUnitUsageTotalSums] 计算%工料机合价失败,error={}", e.getMessage(), e); + } + } + } + + /** + * 构建工作台工料机的 FieldAccessor + * 映射 WbBoqResourceRespVO 的字段名到 ResourcePriceCalculator 的通用接口 + */ + private ResourcePriceCalculator.FieldAccessor wbBoqResourceFieldAccessor() { + return ResourcePriceCalculator.FieldAccessor.builder() + .getUnit(WbBoqResourceRespVO::getUnit) + .getCategoryId(WbBoqResourceRespVO::getCategoryId) + .getEffectiveDosage(vo -> vo.getAdjustConsumeQty() != null ? vo.getAdjustConsumeQty() : vo.getConsumeQty()) + .getCalcBase(WbBoqResourceRespVO::getCalcBase) + .getTaxExclBaseTotalSum(WbBoqResourceRespVO::getTaxExclBaseTotalSum) + .getTaxInclBaseTotalSum(WbBoqResourceRespVO::getTaxInclBaseTotalSum) + .getTaxExclCompileTotalSum(WbBoqResourceRespVO::getTaxExclCompileTotalSum) + .getTaxInclCompileTotalSum(WbBoqResourceRespVO::getTaxInclCompileTotalSum) + .setTaxExclBaseTotalSum(WbBoqResourceRespVO::setTaxExclBaseTotalSum) + .setTaxInclBaseTotalSum(WbBoqResourceRespVO::setTaxInclBaseTotalSum) + .setTaxExclCompileTotalSum(WbBoqResourceRespVO::setTaxExclCompileTotalSum) + .setTaxInclCompileTotalSum(WbBoqResourceRespVO::setTaxInclCompileTotalSum) + .build(); + } + + /** + * 计算调整公式(虚拟字段) + * 根据工作台的调整设置计算每个工料机的调整公式 + * 使用公共工具类 AdjustmentFormulaCalculator 保持与后台一致的计算逻辑 + */ + private void calculateAdjustmentFormulas(Long divisionId, List resources) { + // 1. 获取工作台的调整设置 + List settings = wbAdjustmentSettingMapper.selectListByDivisionId(divisionId); + if (CollUtil.isEmpty(settings)) { + return; + } + + // 2. 展开所有工料机(包括子工料机) + List allResources = new ArrayList<>(); + for (WbBoqResourceRespVO vo : resources) { + allResources.add(vo); + if (vo.getChildren() != null && !vo.getChildren().isEmpty()) { + allResources.addAll(vo.getChildren()); + } + } + + // 3. 使用公共工具类计算调整公式 + AdjustmentFormulaCalculator.calculateFormulas( + allResources, + settings, + WbBoqResourceRespVO::getCode, + WbBoqResourceRespVO::getCategoryId, + WbBoqResourceRespVO::setAdjustmentFormula + ); + } + + /** + * 填充价格来源状态(hasPriceSource)并虚拟替换价格 + * 查询工料机是否有当前价格来源(is_current=1),如果有则用信息价的价格虚拟替换 + * 注意:价格来源绑定到项目级别(projectId + resourceItemId),影响项目下所有使用该工料机的定额 + * @param resources 工料机列表 + * @param division 分部分项节点(已加载,避免重复查询) + */ + private void fillPriceSourceStatus(List resources, WbBoqDivisionDO division) { + if (CollUtil.isEmpty(resources)) { + return; + } + + // 从已加载的 division 获取 projectId(避免重复查询) + Long projectId = null; + if (division != null && division.getCompileTreeId() != null) { + com.yhy.module.core.dal.dataobject.workbench.WbCompileTreeDO compileTree = + wbCompileTreeMapper.selectById(division.getCompileTreeId()); + if (compileTree != null) { + projectId = compileTree.getProjectId(); + } + } + + // 收集所有工料机的sourceResourceItemId(包括子工料机) + List resourceItemIds = new ArrayList<>(); + for (WbBoqResourceRespVO vo : resources) { + if (vo.getSourceResourceItemId() != null) { + resourceItemIds.add(vo.getSourceResourceItemId()); + } + if (vo.getChildren() != null) { + for (WbBoqResourceRespVO child : vo.getChildren()) { + if (child.getSourceResourceItemId() != null) { + resourceItemIds.add(child.getSourceResourceItemId()); + } + } + } + } + + if (resourceItemIds.isEmpty()) { + return; + } + + // 批量查询当前价格来源的详细信息(按项目ID和工料机ID) + Map currentPriceMappings; + if (projectId != null) { + currentPriceMappings = resourceInfoPriceMappingMapper.selectCurrentPriceMappingsByProject(projectId, resourceItemIds); + } else { + // 兼容旧逻辑(租户级别) + currentPriceMappings = resourceInfoPriceMappingMapper.selectCurrentPriceMappingsByResourceItemIds(resourceItemIds); + } + + // 收集所有需要查询的价格历史ID + List priceIds = currentPriceMappings.values().stream() + .map(com.yhy.module.core.dal.dataobject.resource.ResourceInfoPriceMappingDO::getSelectedPriceId) + .filter(java.util.Objects::nonNull) + .distinct() + .collect(java.util.stream.Collectors.toList()); + + // 批量查询价格历史详情 + Map priceMap = new java.util.HashMap<>(); + if (!priceIds.isEmpty()) { + List prices = + infoPriceResourcePriceMapper.selectListByIds(priceIds); + priceMap = prices.stream() + .collect(java.util.stream.Collectors.toMap( + com.yhy.module.core.dal.dataobject.infoprice.InfoPriceResourcePriceDO::getId, + p -> p, + (existing, replacement) -> existing)); + } + + // 收集所有信息价工料机ID,用于生成价格来源显示文本 + List infoPriceResourceIds = currentPriceMappings.values().stream() + .map(com.yhy.module.core.dal.dataobject.resource.ResourceInfoPriceMappingDO::getInfoPriceResourceId) + .filter(java.util.Objects::nonNull) + .distinct() + .collect(java.util.stream.Collectors.toList()); + + // 批量查询信息价工料机详情,用于获取 categoryTreeId + Map infoPriceResourceMap = new java.util.HashMap<>(); + if (!infoPriceResourceIds.isEmpty()) { + List infoPriceResources = + infoPriceResourceMapper.selectListByIds(infoPriceResourceIds); + infoPriceResourceMap = infoPriceResources.stream() + .collect(java.util.stream.Collectors.toMap( + com.yhy.module.core.dal.dataobject.infoprice.InfoPriceResourceDO::getId, + r -> r, + (existing, replacement) -> existing)); + } + + // 获取定额数量(用于计算合价,从已加载的 division 获取,避免重复查询) + java.math.BigDecimal quotaQty = java.math.BigDecimal.ONE; + if (division != null && division.getQty() != null && division.getQty().compareTo(java.math.BigDecimal.ZERO) > 0) { + quotaQty = division.getQty(); + } + + // 填充 hasPriceSource 字段并虚拟替换价格 + for (WbBoqResourceRespVO vo : resources) { + applyPriceSourceToResource(vo, currentPriceMappings, priceMap, infoPriceResourceMap, quotaQty); + if (vo.getChildren() != null) { + for (WbBoqResourceRespVO child : vo.getChildren()) { + applyPriceSourceToResource(child, currentPriceMappings, priceMap, infoPriceResourceMap, quotaQty); + } + } + } + } + + /** + * 应用价格来源到工料机(虚拟替换价格并生成显示文本) + * 注意:使用 sourceResourceItemId 来查找映射(项目级别的价格来源) + * @param quotaQty 定额数量,用于计算合价(用量 = 定额数量 × 消耗量) + */ + private void applyPriceSourceToResource( + WbBoqResourceRespVO vo, + Map currentPriceMappings, + Map priceMap, + Map infoPriceResourceMap, + java.math.BigDecimal quotaQty) { + + if (vo.getSourceResourceItemId() == null) { + return; + } + + // 使用 sourceResourceItemId 来查找映射(项目级别) + com.yhy.module.core.dal.dataobject.resource.ResourceInfoPriceMappingDO mapping = + currentPriceMappings.get(vo.getSourceResourceItemId()); + + if (mapping != null) { + vo.setHasPriceSource(true); + + // 如果有选中的价格历史,则用信息价的价格虚拟替换 + if (mapping.getSelectedPriceId() != null) { + com.yhy.module.core.dal.dataobject.infoprice.InfoPriceResourcePriceDO price = + priceMap.get(mapping.getSelectedPriceId()); + if (price != null) { + // 获取公式 + String formula = mapping.getFormula(); + + // 虚拟替换税率 + if (price.getTaxRate() != null) { + vo.setTaxRate(price.getTaxRate()); + } + + // 计算除税编制价(应用公式) + java.math.BigDecimal taxExclPrice = price.getPriceTaxExcl(); + if (taxExclPrice != null) { + taxExclPrice = applyFormula(taxExclPrice, formula); + vo.setTaxExclCompilePrice(taxExclPrice); + } + + // 计算含税编制价(应用公式) + java.math.BigDecimal taxInclPrice = price.getPriceTaxIncl(); + if (taxInclPrice != null) { + taxInclPrice = applyFormula(taxInclPrice, formula); + vo.setTaxInclCompilePrice(taxInclPrice); + } + + // 计算用量 = 定额数量 × 消耗量 + java.math.BigDecimal consumeQty = vo.getConsumeQty() != null ? vo.getConsumeQty() : java.math.BigDecimal.ZERO; + java.math.BigDecimal usageQty = quotaQty.multiply(consumeQty); + + // 重新计算编制价合价(用量 × 编制价) + if (taxExclPrice != null) { + vo.setTaxExclCompileTotalSum(usageQty.multiply(taxExclPrice).setScale(2, java.math.RoundingMode.HALF_UP)); + } + if (taxInclPrice != null) { + vo.setTaxInclCompileTotalSum(usageQty.multiply(taxInclPrice).setScale(2, java.math.RoundingMode.HALF_UP)); + } + + // 重新计算基价合价(用量 × 基价,使用新税率) + java.math.BigDecimal taxExclBasePrice = vo.getTaxExclBasePrice(); + java.math.BigDecimal taxInclBasePrice = vo.getTaxInclBasePrice(); + if (taxExclBasePrice != null) { + vo.setTaxExclBaseTotalSum(usageQty.multiply(taxExclBasePrice).setScale(2, java.math.RoundingMode.HALF_UP)); + } + if (taxInclBasePrice != null) { + vo.setTaxInclBaseTotalSum(usageQty.multiply(taxInclBasePrice).setScale(2, java.math.RoundingMode.HALF_UP)); + } + } + } + + // 生成价格来源显示文本:信*01*广东*珠海*金湾 + String priceSourceText = buildPriceSourceText(mapping.getInfoPriceResourceId(), infoPriceResourceMap); + vo.setPriceSourceText(priceSourceText); + } else { + vo.setHasPriceSource(false); + } + } + + /** + * 构建价格来源显示文本 + * 格式:信*01*广东*珠海*金湾 + * - "信" 代表信息价 + * - "01" 代表信息价专业的序号 + * - "广东*珠海*金湾" 代表地区路径 + */ + private String buildPriceSourceText(Long infoPriceResourceId, + Map infoPriceResourceMap) { + if (infoPriceResourceId == null) { + return ""; + } + + com.yhy.module.core.dal.dataobject.infoprice.InfoPriceResourceDO infoPriceResource = + infoPriceResourceMap.get(infoPriceResourceId); + if (infoPriceResource == null || infoPriceResource.getCategoryTreeId() == null) { + return "信"; + } + + try { + // 获取分类树节点 + com.yhy.module.core.dal.dataobject.infoprice.InfoPriceCategoryTreeDO categoryTree = + infoPriceCategoryTreeMapper.selectById(infoPriceResource.getCategoryTreeId()); + if (categoryTree == null || categoryTree.getBookId() == null) { + return "信"; + } + + // 获取信息价册 + com.yhy.module.core.dal.dataobject.infoprice.InfoPriceBookDO book = + infoPriceBookMapper.selectById(categoryTree.getBookId()); + if (book == null || book.getTreeNodeId() == null) { + return "信"; + } + + // 获取树节点(包含专业类型和地区路径) + com.yhy.module.core.dal.dataobject.infoprice.InfoPriceTreeDO treeNode = + infoPriceTreeMapper.selectById(book.getTreeNodeId()); + if (treeNode == null) { + return "信"; + } + + StringBuilder sb = new StringBuilder("信"); + + // 添加专业序号 + String professionIndex = getProfessionIndex(treeNode.getEnumType()); + sb.append("*").append(professionIndex); + + // 添加地区路径 + String regionPath = buildRegionPath(treeNode); + if (!regionPath.isEmpty()) { + sb.append("*").append(regionPath); + } + + return sb.toString(); + } catch (Exception e) { + log.warn("构建价格来源显示文本失败: {}", e.getMessage()); + return "信"; + } + } + + /** + * 获取专业序号(01, 02, 03) + */ + private String getProfessionIndex(String enumType) { + if (enumType == null) { + return "01"; + } + switch (enumType) { + case "region_a": + return "01"; + case "region_b": + return "02"; + case "region_c": + return "03"; + default: + return "01"; + } + } + + /** + * 构建地区路径(广东*珠海*金湾) + */ + private String buildRegionPath(com.yhy.module.core.dal.dataobject.infoprice.InfoPriceTreeDO treeNode) { + if (treeNode == null) { + return ""; + } + + StringBuilder pathBuilder = new StringBuilder(); + String[] pathIds = treeNode.getPath(); + + // 遍历路径ID获取名称 + if (pathIds != null && pathIds.length > 0) { + for (String pathId : pathIds) { + try { + Long id = Long.parseLong(pathId); + com.yhy.module.core.dal.dataobject.infoprice.InfoPriceTreeDO pathNode = + infoPriceTreeMapper.selectById(id); + if (pathNode != null && pathNode.getName() != null && !pathNode.getName().isEmpty()) { + if (pathBuilder.length() > 0) { + pathBuilder.append("*"); + } + pathBuilder.append(pathNode.getName()); + } + } catch (NumberFormatException e) { + // 忽略无效的ID + } + } + } + + // 添加当前节点名称 + if (treeNode.getName() != null && !treeNode.getName().isEmpty()) { + if (pathBuilder.length() > 0) { + pathBuilder.append("*"); + } + pathBuilder.append(treeNode.getName()); + } + + return pathBuilder.toString(); + } + + /** + * 应用公式计算价格 + * 公式格式:xxj, xxj+20, xxj-10, xxj*1.1, (xxj+10)*0.9 等 + * xxj 代表信息价原价 + */ + private java.math.BigDecimal applyFormula(java.math.BigDecimal originalPrice, String formula) { + if (originalPrice == null) { + return null; + } + if (formula == null || formula.trim().isEmpty()) { + return originalPrice; + } + + try { + // 将 xxj 替换为实际价格值 + String expression = formula.trim().replace("xxj", originalPrice.toPlainString()); + + // 使用 JavaScript 引擎计算表达式 + javax.script.ScriptEngineManager manager = new javax.script.ScriptEngineManager(); + javax.script.ScriptEngine engine = manager.getEngineByName("JavaScript"); + if (engine == null) { + // 如果没有 JavaScript 引擎,尝试使用 Nashorn + engine = manager.getEngineByName("nashorn"); + } + if (engine == null) { + // 如果仍然没有引擎,返回原价 + return originalPrice; + } + + Object result = engine.eval(expression); + if (result instanceof Number) { + return new java.math.BigDecimal(result.toString()).setScale(2, java.math.RoundingMode.HALF_UP); + } + return originalPrice; + } catch (Exception e) { + // 公式计算失败,返回原价 + return originalPrice; + } + } + + @Override + public List getCategorySummary(Long divisionId) { + List resources = getListByDivisionId(divisionId); + if (CollUtil.isEmpty(resources)) { + return new ArrayList<>(); + } + + Map categoryMap = loadCategoryMap(resources); + Map summaryMap = new HashMap<>(); + + for (WbBoqResourceRespVO resource : resources) { + Long categoryId = resource.getCategoryId(); + WbBoqResourceCategorySummaryVO summary = summaryMap.computeIfAbsent(categoryId, key -> { + WbBoqResourceCategorySummaryVO vo = new WbBoqResourceCategorySummaryVO(); + ResourceCategoryDO category = categoryMap.get(key); + if (category != null) { + vo.setCategory(category.getName() != null ? category.getName() : category.getCode()); + } else if (resource.getCategoryCode() != null) { + vo.setCategory(resource.getCategoryCode()); + } else { + vo.setCategory("未分类"); + } + vo.setTotalBasePriceExTax(BigDecimal.ZERO); + vo.setTotalBasePriceInTax(BigDecimal.ZERO); + vo.setTotalCompilePriceExTax(BigDecimal.ZERO); + vo.setTotalCompilePriceInTax(BigDecimal.ZERO); + return vo; + }); + + summary.setTotalBasePriceExTax(add(summary.getTotalBasePriceExTax(), resource.getTaxExclBaseTotalSum())); + summary.setTotalBasePriceInTax(add(summary.getTotalBasePriceInTax(), resource.getTaxInclBaseTotalSum())); + summary.setTotalCompilePriceExTax(add(summary.getTotalCompilePriceExTax(), resource.getTaxExclCompileTotalSum())); + summary.setTotalCompilePriceInTax(add(summary.getTotalCompilePriceInTax(), resource.getTaxInclCompileTotalSum())); + } + + List> entries = new ArrayList<>(summaryMap.entrySet()); + entries.sort(Comparator.comparingInt(entry -> { + ResourceCategoryDO category = categoryMap.get(entry.getKey()); + return category != null && category.getSortOrder() != null ? category.getSortOrder() : Integer.MAX_VALUE; + })); + + List result = new ArrayList<>(entries.size()); + for (int i = 0; i < entries.size(); i++) { + WbBoqResourceCategorySummaryVO summary = entries.get(i).getValue(); + summary.setIndex(i + 1); + summary.setTotalBasePriceExTax(roundToScale(summary.getTotalBasePriceExTax(), 2)); + summary.setTotalBasePriceInTax(roundToScale(summary.getTotalBasePriceInTax(), 2)); + summary.setTotalCompilePriceExTax(roundToScale(summary.getTotalCompilePriceExTax(), 2)); + summary.setTotalCompilePriceInTax(roundToScale(summary.getTotalCompilePriceInTax(), 2)); + result.add(summary); + } + return result; + } + + private Map loadCategoryMap(List resources) { + List categoryIds = resources.stream() + .map(WbBoqResourceRespVO::getCategoryId) + .filter(java.util.Objects::nonNull) + .distinct() + .collect(java.util.stream.Collectors.toList()); + if (categoryIds.isEmpty()) { + return new HashMap<>(); + } + return resourceCategoryMapper.selectList("id", categoryIds).stream() + .collect(java.util.stream.Collectors.toMap(ResourceCategoryDO::getId, category -> category, (a, b) -> a)); + } + + private BigDecimal add(BigDecimal left, BigDecimal right) { + return (left != null ? left : BigDecimal.ZERO).add(right != null ? right : BigDecimal.ZERO); + } + + @Override + public List getListByBoqDivisionId(Long boqDivisionId) { + // 1. 验证是清单节点 + WbBoqDivisionDO boqDivision = wbBoqDivisionMapper.selectById(boqDivisionId); + if (boqDivision == null || !WbBoqDivisionDO.NODE_TYPE_BOQ.equals(boqDivision.getNodeType())) { + return java.util.Collections.emptyList(); + } + + // 2. 获取清单下所有定额节点 + List quotaDivisions = wbBoqDivisionMapper.selectListByParentId(boqDivisionId); + List quotaDivisionIds = quotaDivisions.stream() + .filter(d -> WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(d.getNodeType())) + .map(WbBoqDivisionDO::getId) + .collect(java.util.stream.Collectors.toList()); + + if (quotaDivisionIds.isEmpty()) { + return java.util.Collections.emptyList(); + } + + // 3. 批量查询所有定额的工料机(包含子工料机) + List allResources = new ArrayList<>(); + for (Long quotaDivisionId : quotaDivisionIds) { + List resources = wbBoqResourceMapper.selectListByDivisionId(quotaDivisionId); + allResources.addAll(resources); + } + + if (allResources.isEmpty()) { + return java.util.Collections.emptyList(); + } + + // 4. 分离父工料机和子工料机 + List parentResources = new ArrayList<>(); + Map> childrenMap = new java.util.HashMap<>(); + + for (WbBoqResourceDO resource : allResources) { + if (resource.getParentId() == null) { + parentResources.add(resource); + } else { + childrenMap.computeIfAbsent(resource.getParentId(), k -> new ArrayList<>()).add(resource); + } + } + + // 4.1 批量预加载所有需要的 ResourceItemDO(避免 convertToVO 中逐个查询) + List sourceResourceItemIds = allResources.stream() + .map(WbBoqResourceDO::getSourceResourceItemId) + .filter(java.util.Objects::nonNull) + .distinct() + .collect(java.util.stream.Collectors.toList()); + Map resourceItemMap = new java.util.HashMap<>(); + if (!sourceResourceItemIds.isEmpty()) { + List resourceItems = resourceItemMapper.selectBatchIds(sourceResourceItemIds); + resourceItemMap = resourceItems.stream() + .collect(java.util.stream.Collectors.toMap(ResourceItemDO::getId, r -> r, (a, b) -> a)); + } + + // 5. 按 sourceResourceItemId 汇总父工料机(相同工料机合并消耗量) + java.util.Map summaryMap = new java.util.LinkedHashMap<>(); + // 记录每个 sourceResourceItemId 对应的子工料机 + java.util.Map> childrenSummaryMap = new java.util.HashMap<>(); + int sortOrder = 1; + + for (WbBoqResourceDO resource : parentResources) { + Long sourceId = resource.getSourceResourceItemId(); + WbBoqResourceRespVO vo; + + if (sourceId == null) { + // 没有 sourceResourceItemId 的工料机单独显示 + vo = convertToVO(resource, resourceItemMap); + vo.setSortOrder(sortOrder++); + summaryMap.put(resource.getId(), vo); + } else if (summaryMap.containsKey(sourceId)) { + // 已存在,累加消耗量 + WbBoqResourceRespVO existing = summaryMap.get(sourceId); + if (resource.getConsumeQty() != null) { + java.math.BigDecimal newConsumeQty = existing.getConsumeQty() != null + ? existing.getConsumeQty().add(resource.getConsumeQty()) + : resource.getConsumeQty(); + existing.setConsumeQty(newConsumeQty); + } + vo = existing; + } else { + // 新工料机 + vo = convertToVO(resource, resourceItemMap); + vo.setSortOrder(sortOrder++); + summaryMap.put(sourceId, vo); + } + + // 处理子工料机 + List children = childrenMap.get(resource.getId()); + if (children != null && !children.isEmpty()) { + Long key = sourceId != null ? sourceId : resource.getId(); + List existingChildren = childrenSummaryMap.computeIfAbsent(key, k -> new ArrayList<>()); + + for (WbBoqResourceDO child : children) { + Long childSourceId = child.getSourceResourceItemId(); + // 查找是否已有相同 sourceResourceItemId 的子工料机 + WbBoqResourceRespVO existingChild = null; + if (childSourceId != null) { + for (WbBoqResourceRespVO c : existingChildren) { + if (childSourceId.equals(c.getSourceResourceItemId())) { + existingChild = c; + break; + } + } + } + + if (existingChild != null) { + // 累加消耗量 + if (child.getConsumeQty() != null) { + java.math.BigDecimal newConsumeQty = existingChild.getConsumeQty() != null + ? existingChild.getConsumeQty().add(child.getConsumeQty()) + : child.getConsumeQty(); + existingChild.setConsumeQty(newConsumeQty); + } + } else { + // 新子工料机 + WbBoqResourceRespVO childVo = convertToVO(child, resourceItemMap); + existingChildren.add(childVo); + } + } + } + } + + // 6. 将子工料机添加到父工料机,并计算复合工料机的合价 + for (Map.Entry entry : summaryMap.entrySet()) { + Long key = entry.getKey(); + WbBoqResourceRespVO parent = entry.getValue(); + List children = childrenSummaryMap.get(key); + if (children != null && !children.isEmpty()) { + parent.setChildren(children); + // 计算复合工料机的合价(子工料机合价之和) + calculateMergedParentTotalSums(parent, children); + } + } + + // 7. 计算普通工料机的合价(消耗量 × 单价) + List result = new ArrayList<>(summaryMap.values()); + for (WbBoqResourceRespVO vo : result) { + // 跳过已计算合价的复合工料机 + if (vo.getChildren() != null && !vo.getChildren().isEmpty()) { + continue; + } + // 计算普通工料机的合价 + BigDecimal consumeQty = vo.getConsumeQty(); + if (consumeQty != null) { + if (vo.getTaxExclBasePrice() != null) { + vo.setTaxExclBaseTotalSum(consumeQty.multiply(vo.getTaxExclBasePrice())); + } + if (vo.getTaxInclBasePrice() != null) { + vo.setTaxInclBaseTotalSum(consumeQty.multiply(vo.getTaxInclBasePrice())); + } + if (vo.getTaxExclCompilePrice() != null) { + vo.setTaxExclCompileTotalSum(consumeQty.multiply(vo.getTaxExclCompilePrice())); + } + if (vo.getTaxInclCompilePrice() != null) { + vo.setTaxInclCompileTotalSum(consumeQty.multiply(vo.getTaxInclCompilePrice())); + } + } + } + + return result; + } + + @Override + public List getResourcesByDivisionIds(List divisionIds) { + if (CollUtil.isEmpty(divisionIds)) { + return new ArrayList<>(); + } + + // 查询所有定额节点下的工料机 + List resources = wbBoqResourceMapper.selectListByDivisionIds(divisionIds); + if (CollUtil.isEmpty(resources)) { + return new ArrayList<>(); + } + + // 转换为VO + List result = new ArrayList<>(resources.size()); + for (WbBoqResourceDO resource : resources) { + result.add(WbBoqResourceConvert.INSTANCE.convert(resource)); + } + return result; + } + + // ========== 新增工料机功能 ========== + + @Override + public List searchByCode(Long divisionId, String code) { + // 1. 校验定额节点 + WbBoqDivisionDO division = wbBoqDivisionMapper.selectById(divisionId); + if (division == null || !WbBoqDivisionDO.NODE_TYPE_QUOTA.equals(division.getNodeType())) { + throw exception(WB_BOQ_RESOURCE_DIVISION_NOT_QUOTA); + } + + List result = new ArrayList<>(); + + // 2. 查询"本项目"范围:当前单位工程下所有定额的工料机 + List projectResources = wbBoqResourceMapper + .selectByCompileTreeIdAndCodeLike(division.getCompileTreeId(), code); + // 用于去重(按 name+spec+unit) + java.util.Set seenKeys = new java.util.HashSet<>(); + for (WbBoqResourceDO res : projectResources) { + String key = (res.getName() == null ? "" : res.getName()) + "|" + + (res.getSpec() == null ? "" : res.getSpec()) + "|" + + (res.getUnit() == null ? "" : res.getUnit()); + if (seenKeys.add(key)) { + com.yhy.module.core.controller.admin.workbench.vo.WbResourceSearchRespVO vo = + new com.yhy.module.core.controller.admin.workbench.vo.WbResourceSearchRespVO(); + vo.setId(res.getSourceResourceItemId()); + vo.setCode(res.getCode()); + vo.setName(res.getName()); + vo.setSpec(res.getSpec()); + vo.setUnit(res.getUnit()); + vo.setSource("project"); + vo.setResourceType(res.getResourceType()); + vo.setTaxRate(res.getTaxRate()); + vo.setTaxExclBasePrice(res.getTaxExclBasePrice()); + vo.setTaxInclBasePrice(res.getTaxInclBasePrice()); + vo.setTaxExclCompilePrice(res.getTaxExclCompilePrice()); + vo.setTaxInclCompilePrice(res.getTaxInclCompilePrice()); + vo.setCategoryId(res.getCategoryId()); + vo.setSourceResourceItemId(res.getSourceResourceItemId()); + result.add(vo); + } + } + + // 3. 查询"非项目"范围:后台标准库工料机 + Long quotaItemId = division.getSourceQuotaItemId(); + if (quotaItemId != null) { + Long categoryTreeId = quotaItemService.getCategoryTreeIdByQuotaItem(quotaItemId); + if (categoryTreeId != null) { + List standardResources = resourceItemMapper + .selectByCategoryTreeIdWithFilter(categoryTreeId, code, null, null); + for (ResourceItemDO item : standardResources) { + String key = (item.getName() == null ? "" : item.getName()) + "|" + + (item.getSpec() == null ? "" : item.getSpec()) + "|" + + (item.getUnit() == null ? "" : item.getUnit()); + if (seenKeys.add(key)) { + com.yhy.module.core.controller.admin.workbench.vo.WbResourceSearchRespVO vo = + new com.yhy.module.core.controller.admin.workbench.vo.WbResourceSearchRespVO(); + vo.setId(item.getId()); + vo.setCode(item.getCode()); + vo.setName(item.getName()); + vo.setSpec(item.getSpec()); + vo.setUnit(item.getUnit()); + vo.setSource("standard"); + vo.setResourceType(item.getType()); + vo.setTaxRate(item.getTaxRate()); + vo.setTaxExclBasePrice(item.getTaxExclBasePrice()); + vo.setTaxInclBasePrice(item.getTaxInclBasePrice()); + vo.setTaxExclCompilePrice(item.getTaxExclCompilePrice()); + vo.setTaxInclCompilePrice(item.getTaxInclCompilePrice()); + vo.setCategoryId(item.getCategoryId()); + vo.setSourceResourceItemId(item.getId()); + result.add(vo); + } + } + } + } + + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createBlank(com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceCreateBlankReqVO createBlankReqVO) { + // 1. 校验定额节点 + validateDivisionIsQuota(createBlankReqVO.getDivisionId()); + + // 2. 计算排序号 + int newSortOrder; + if (createBlankReqVO.getAfterResourceId() != null) { + // 在指定行下方插入 + WbBoqResourceDO afterResource = wbBoqResourceMapper.selectById(createBlankReqVO.getAfterResourceId()); + if (afterResource == null) { + throw exception(WB_BOQ_RESOURCE_NOT_EXISTS); + } + // 将后续行的排序号+1 + wbBoqResourceMapper.incrementSortOrderAfter(createBlankReqVO.getDivisionId(), afterResource.getSortOrder()); + newSortOrder = afterResource.getSortOrder() + 1; + } else { + // 追加到末尾 + Integer maxSortOrder = wbBoqResourceMapper.selectMaxSortOrderByDivisionId(createBlankReqVO.getDivisionId()); + newSortOrder = maxSortOrder + 1; + } + + // 3. 创建空白行(所有业务字段为空) + WbBoqResourceDO blankResource = WbBoqResourceDO.builder() + .divisionId(createBlankReqVO.getDivisionId()) + .sourceType(WbBoqResourceDO.SOURCE_TYPE_TENANT) + .sortOrder(newSortOrder) + .build(); + wbBoqResourceMapper.insert(blankResource); + + log.info("[createBlank] 创建空白工料机行:divisionId={}, id={}, sortOrder={}", + createBlankReqVO.getDivisionId(), blankResource.getId(), newSortOrder); + return blankResource.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void fillFromSearch(com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceFillReqVO fillReqVO) { + // 1. 校验空白行存在 + WbBoqResourceDO existResource = validateResourceExists(fillReqVO.getId()); + + // 2. 获取定额节点信息,用于类别校验 + WbBoqDivisionDO division = wbBoqDivisionMapper.selectById(existResource.getDivisionId()); + + // 3. 校验并解析类别ID(只能是本定额专业的类别范围) + Long resolvedCategoryId = validateAndResolveCategoryId(division, fillReqVO.getCategoryId()); + + // 4. 处理后台工料机引用(双表写入) + Long sourceResourceItemId = fillReqVO.getSourceResourceItemId(); + if (sourceResourceItemId == null && fillReqVO.getName() != null && fillReqVO.getUnit() != null) { + // 纯手动输入,查找或创建后台工料机 + sourceResourceItemId = findOrCreateResourceItem( + fillReqVO.getName(), fillReqVO.getSpec(), fillReqVO.getUnit(), + fillReqVO.getResourceType()); + } + + // 5. 更新空白行数据 + WbBoqResourceDO updateObj = new WbBoqResourceDO(); + updateObj.setId(fillReqVO.getId()); + updateObj.setSourceResourceItemId(sourceResourceItemId); + updateObj.setCode(fillReqVO.getCode()); + updateObj.setName(fillReqVO.getName()); + updateObj.setSpec(fillReqVO.getSpec()); + updateObj.setUnit(fillReqVO.getUnit()); + updateObj.setResourceType(fillReqVO.getResourceType()); + updateObj.setTaxRate(fillReqVO.getTaxRate()); + updateObj.setTaxExclBasePrice(fillReqVO.getTaxExclBasePrice()); + updateObj.setTaxInclBasePrice(fillReqVO.getTaxInclBasePrice()); + updateObj.setTaxExclCompilePrice(fillReqVO.getTaxExclCompilePrice()); + updateObj.setTaxInclCompilePrice(fillReqVO.getTaxInclCompilePrice()); + updateObj.setCategoryId(resolvedCategoryId); + updateObj.setSourceType(WbBoqResourceDO.SOURCE_TYPE_TENANT); + // consumeQty 保持 null(不保存定额消耗量) + // adjustConsumeQty 保持 null(等用户手动编辑) + + wbBoqResourceMapper.updateById(updateObj); + + log.info("[fillFromSearch] 填充工料机数据:id={}, code={}, name={}, categoryId={}", + fillReqVO.getId(), fillReqVO.getCode(), fillReqVO.getName(), resolvedCategoryId); + } + + /** + * 用量与调整消耗量联动计算 + * + * 规则(定额工程量为固定值,不受影响): + * - 修改用量 → 调整消耗量 = 用量 / 定额工程量 + * - 修改调整消耗量 → 用量 = 调整消耗量 × 定额工程量 + * + * @param updateObj 待更新的DO对象(会被修改) + * @param reqVO 请求参数 + * @param existResource 数据库中的现有记录 + */ + private void applyUsageQtyLinkage(WbBoqResourceDO updateObj, + WbBoqResourceSaveReqVO reqVO, + WbBoqResourceDO existResource) { + boolean usageQtyChanged = reqVO.getUsageQty() != null + && !reqVO.getUsageQty().equals(existResource.getUsageQty()); + boolean adjustConsumeQtyChanged = reqVO.getAdjustConsumeQty() != null + && !reqVO.getAdjustConsumeQty().equals(existResource.getAdjustConsumeQty()); + + if (!usageQtyChanged && !adjustConsumeQtyChanged) { + return; // 两个字段都没变,不做联动 + } + + // 获取定额工程量(从关联的定额节点) + WbBoqDivisionDO division = wbBoqDivisionMapper.selectById(existResource.getDivisionId()); + if (division == null || division.getQty() == null || division.getQty().compareTo(BigDecimal.ZERO) == 0) { + // 定额工程量为空或0,无法联动计算 + return; + } + BigDecimal quotaQty = division.getQty(); + + if (usageQtyChanged) { + // 修改用量 → 调整消耗量 = 用量 / 定额工程量 + BigDecimal newAdjustConsumeQty = reqVO.getUsageQty() + .divide(quotaQty, 6, java.math.RoundingMode.HALF_UP); + updateObj.setAdjustConsumeQty(newAdjustConsumeQty); + updateObj.setUsageQty(reqVO.getUsageQty()); + } else if (adjustConsumeQtyChanged) { + // 修改调整消耗量 → 用量 = 调整消耗量 × 定额工程量 + BigDecimal newUsageQty = reqVO.getAdjustConsumeQty() + .multiply(quotaQty).setScale(6, java.math.RoundingMode.HALF_UP); + updateObj.setUsageQty(newUsageQty); + updateObj.setAdjustConsumeQty(reqVO.getAdjustConsumeQty()); + } + } + + /** + * 项目级编制价/价格来源批量同步 + * 当"除税编制价"或"含税编制价"变更时,同步更新同项目下所有"编码+名称+型号规格+单位"一致的工料机 + * + * @param updateObj 已更新的DO对象 + * @param reqVO 请求参数 + * @param existResource 数据库中的原始记录 + * @return 批量更新结果,无需同步时返回null + */ + private com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceBatchUpdateResultVO syncCompilePriceAcrossProject( + WbBoqResourceDO updateObj, WbBoqResourceSaveReqVO reqVO, WbBoqResourceDO existResource) { + + // 判断编制价是否变更(使用updateObj中的最终值,因为可能经过税率联动计算) + boolean taxExclCompileChanged = updateObj.getTaxExclCompilePrice() != null + && !updateObj.getTaxExclCompilePrice().equals(existResource.getTaxExclCompilePrice()); + boolean taxInclCompileChanged = updateObj.getTaxInclCompilePrice() != null + && !updateObj.getTaxInclCompilePrice().equals(existResource.getTaxInclCompilePrice()); + + if (!taxExclCompileChanged && !taxInclCompileChanged) { + return null; // 编制价未变更,不需要批量同步 + } + + // 匹配条件:编码+名称+型号规格+单位 四个字段一致 + String code = existResource.getCode(); + String name = existResource.getName(); + String unit = existResource.getUnit(); + if (code == null || name == null || unit == null) { + return null; // 关键字段为空,无法匹配 + } + + // 获取项目ID + WbBoqDivisionDO division = wbBoqDivisionMapper.selectById(existResource.getDivisionId()); + if (division == null) { + return null; + } + com.yhy.module.core.dal.dataobject.workbench.WbCompileTreeDO compileTree = + wbCompileTreeMapper.selectById(division.getCompileTreeId()); + if (compileTree == null || compileTree.getProjectId() == null) { + return null; + } + Long projectId = compileTree.getProjectId(); + + // 查询同项目下匹配的工料机(排除自身) + List matchedResources = wbBoqResourceMapper.selectSameResourceInProject( + projectId, code, name, existResource.getSpec(), unit, existResource.getId()); + + if (CollUtil.isEmpty(matchedResources)) { + return null; + } + + // 批量更新编制价 + java.math.BigDecimal newTaxExclCompilePrice = updateObj.getTaxExclCompilePrice(); + java.math.BigDecimal newTaxInclCompilePrice = updateObj.getTaxInclCompilePrice(); + + // 按 compileTreeId 分组统计 + Map> groupByCompileTree = new java.util.LinkedHashMap<>(); + for (WbBoqResourceDO matched : matchedResources) { + WbBoqDivisionDO matchedDiv = wbBoqDivisionMapper.selectById(matched.getDivisionId()); + if (matchedDiv != null) { + groupByCompileTree.computeIfAbsent(matchedDiv.getCompileTreeId(), k -> new ArrayList<>()).add(matched); + } + } + + int totalAffected = 0; + List affectedUnits = new ArrayList<>(); + + for (Map.Entry> entry : groupByCompileTree.entrySet()) { + Long compileTreeId = entry.getKey(); + List resources = entry.getValue(); + + for (WbBoqResourceDO res : resources) { + WbBoqResourceDO batchUpdate = new WbBoqResourceDO(); + batchUpdate.setId(res.getId()); + if (newTaxExclCompilePrice != null) { + batchUpdate.setTaxExclCompilePrice(newTaxExclCompilePrice); + } + if (newTaxInclCompilePrice != null) { + batchUpdate.setTaxInclCompilePrice(newTaxInclCompilePrice); + } + wbBoqResourceMapper.updateById(batchUpdate); + } + + // 获取单位工程名称 + com.yhy.module.core.dal.dataobject.workbench.WbCompileTreeDO unitTree = + wbCompileTreeMapper.selectById(compileTreeId); + String unitName = unitTree != null ? unitTree.getName() : "未知"; + + com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceBatchUpdateResultVO.AffectedUnitInfo info = + new com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceBatchUpdateResultVO.AffectedUnitInfo(); + info.setCompileTreeId(compileTreeId); + info.setUnitName(unitName); + info.setAffectedRows(resources.size()); + affectedUnits.add(info); + totalAffected += resources.size(); + } + + com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceBatchUpdateResultVO result = + new com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceBatchUpdateResultVO(); + result.setTotalAffectedRows(totalAffected); + result.setAffectedUnits(affectedUnits); + + log.info("[syncCompilePriceAcrossProject] 项目级编制价同步:projectId={}, code={}, name={}, 影响{}个单位工程共{}行", + projectId, code, name, affectedUnits.size(), totalAffected); + + return result; + } + + /** + * 税率与价格联动计算(仅普通工料机) + * + * 规则: + * - 修改税率:含税基价 = 除税基价 × (1 + 税率/100),含税编制价 = 除税编制价 × (1 + 税率/100) + * - 修改除税基价:含税基价 = 除税基价 × (1 + 税率/100) + * - 修改含税基价:除税基价 = 含税基价 / (1 + 税率/100) + * - 修改除税编制价:含税编制价 = 除税编制价 × (1 + 税率/100) + * - 修改含税编制价:除税编制价 = 含税编制价 / (1 + 税率/100) + * + * @param updateObj 待更新的DO对象(会被修改) + * @param reqVO 请求参数 + * @param existResource 数据库中的现有记录 + */ + private void applyTaxRatePriceLinkage(WbBoqResourceDO updateObj, + WbBoqResourceSaveReqVO reqVO, + WbBoqResourceDO existResource) { + // 确定最终税率(优先用本次传入的,否则用数据库已有的) + BigDecimal taxRate = reqVO.getTaxRate() != null ? reqVO.getTaxRate() : existResource.getTaxRate(); + if (taxRate == null) { + return; // 没有税率,不做联动 + } + + // 税率因子 = 1 + 税率/100 + BigDecimal taxFactor = BigDecimal.ONE.add(taxRate.divide(new BigDecimal("100"), 10, java.math.RoundingMode.HALF_UP)); + + // 判断本次修改了哪些字段 + boolean taxRateChanged = reqVO.getTaxRate() != null && !reqVO.getTaxRate().equals(existResource.getTaxRate()); + boolean taxExclBasePriceChanged = reqVO.getTaxExclBasePrice() != null + && !reqVO.getTaxExclBasePrice().equals(existResource.getTaxExclBasePrice()); + boolean taxInclBasePriceChanged = reqVO.getTaxInclBasePrice() != null + && !reqVO.getTaxInclBasePrice().equals(existResource.getTaxInclBasePrice()); + boolean taxExclCompilePriceChanged = reqVO.getTaxExclCompilePrice() != null + && !reqVO.getTaxExclCompilePrice().equals(existResource.getTaxExclCompilePrice()); + boolean taxInclCompilePriceChanged = reqVO.getTaxInclCompilePrice() != null + && !reqVO.getTaxInclCompilePrice().equals(existResource.getTaxInclCompilePrice()); + + if (taxRateChanged) { + // 税率变更:以除税价为基础重算含税价 + BigDecimal baseExcl = reqVO.getTaxExclBasePrice() != null ? reqVO.getTaxExclBasePrice() : existResource.getTaxExclBasePrice(); + BigDecimal compileExcl = reqVO.getTaxExclCompilePrice() != null ? reqVO.getTaxExclCompilePrice() : existResource.getTaxExclCompilePrice(); + if (baseExcl != null) { + updateObj.setTaxInclBasePrice(baseExcl.multiply(taxFactor).setScale(2, java.math.RoundingMode.HALF_UP)); + } + if (compileExcl != null) { + updateObj.setTaxInclCompilePrice(compileExcl.multiply(taxFactor).setScale(2, java.math.RoundingMode.HALF_UP)); + } + } else { + // 税率未变,处理价格字段联动 + if (taxExclBasePriceChanged) { + // 除税基价变更 → 含税基价 = 除税基价 × (1 + 税率/100) + updateObj.setTaxInclBasePrice(reqVO.getTaxExclBasePrice().multiply(taxFactor) + .setScale(2, java.math.RoundingMode.HALF_UP)); + } else if (taxInclBasePriceChanged) { + // 含税基价变更 → 除税基价 = 含税基价 / (1 + 税率/100) + updateObj.setTaxExclBasePrice(reqVO.getTaxInclBasePrice().divide(taxFactor, 2, java.math.RoundingMode.HALF_UP)); + } + + if (taxExclCompilePriceChanged) { + // 除税编制价变更 → 含税编制价 = 除税编制价 × (1 + 税率/100) + updateObj.setTaxInclCompilePrice(reqVO.getTaxExclCompilePrice().multiply(taxFactor) + .setScale(2, java.math.RoundingMode.HALF_UP)); + } else if (taxInclCompilePriceChanged) { + // 含税编制价变更 → 除税编制价 = 含税编制价 / (1 + 税率/100) + updateObj.setTaxExclCompilePrice(reqVO.getTaxInclCompilePrice().divide(taxFactor, 2, java.math.RoundingMode.HALF_UP)); + } + } + } + + /** + * 校验并解析类别ID:只能是本定额专业的类别范围(快照数据) + * 跨定额专业的工料机类别超出范围时置空 + * + * @param division 定额节点 + * @param categoryId 待校验的类别ID + * @return 校验后的类别ID,超出范围返回null + */ + private Long validateAndResolveCategoryId(WbBoqDivisionDO division, Long categoryId) { + if (categoryId == null) { + return null; + } + + Long quotaItemId = division.getSourceQuotaItemId(); + if (quotaItemId == null) { + // 手动创建的定额节点,没有关联后台定额,类别置空 + return null; + } + + // 获取定额专业绑定的 categoryTreeId + Long categoryTreeId = quotaItemService.getCategoryTreeIdByQuotaItem(quotaItemId); + if (categoryTreeId == null) { + return null; + } + + // 从快照表获取允许的类别ID列表(工作台切割后的数据) + List mappings = + wbSnapshotReadService.getCategoryTreeMappings(division.getCompileTreeId(), categoryTreeId); + + java.util.Set allowedCategoryIds = mappings.stream() + .map(com.yhy.module.core.dal.dataobject.workbench.WbCategoryTreeMappingDO::getCategoryId) + .collect(java.util.stream.Collectors.toSet()); + + // 超出范围则置空 + if (!allowedCategoryIds.contains(categoryId)) { + log.info("[validateAndResolveCategoryId] 类别ID={}不在定额专业允许范围内,置空处理。divisionId={}", + categoryId, division.getId()); + return null; + } + + return categoryId; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbCompileTreeServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbCompileTreeServiceImpl.java new file mode 100644 index 0000000..541fd90 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbCompileTreeServiceImpl.java @@ -0,0 +1,345 @@ +package com.yhy.module.core.service.workbench.impl; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.*; +import static com.yhy.module.core.enums.ErrorCodeConstants.*; + +import cn.hutool.core.collection.CollUtil; +import com.yhy.module.core.controller.admin.workbench.vo.WbCompileTreeRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbCompileTreeSaveReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbCompileTreeSwapSortReqVO; +import com.yhy.module.core.convert.workbench.WbCompileTreeConvert; +import com.yhy.module.core.dal.dataobject.workbench.WbCompileTreeDO; +import com.yhy.module.core.dal.mysql.workbench.WbCompileTreeMapper; +import com.yhy.module.core.service.workbench.WbCompileTreeService; +import com.yhy.module.core.service.workbench.WbItemInfoService; +import com.yhy.module.core.service.workbench.WbUnitInfoService; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * 编制模式树 Service 实现类 + * + * @author yhy + */ +@Service +@Validated +@Slf4j +public class WbCompileTreeServiceImpl implements WbCompileTreeService { + + @Resource + private WbCompileTreeMapper wbCompileTreeMapper; + + @Resource + @Lazy + private WbItemInfoService wbItemInfoService; + + @Resource + @Lazy + private WbUnitInfoService wbUnitInfoService; + + @Resource + @Lazy + private com.yhy.module.core.service.workbench.WbBoqDivisionService wbBoqDivisionService; + + @Resource + private com.yhy.module.core.dal.mysql.workbench.WbProjectTreeMapper wbProjectTreeMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long initCompileTree(Long projectId, String projectName) { + // 检查是否已存在根节点 + WbCompileTreeDO existRoot = wbCompileTreeMapper.selectRootByProjectId(projectId); + if (existRoot != null) { + throw exception(WB_COMPILE_TREE_ROOT_EXISTS); + } + + // 创建根节点 + WbCompileTreeDO root = WbCompileTreeDO.builder() + .projectId(projectId) + .parentId(null) + .nodeType(WbCompileTreeDO.NODE_TYPE_ROOT) + .name(projectName) + .sortOrder(0) + .build(); + + wbCompileTreeMapper.insert(root); + + // 更新路径 + root.setPath(new String[]{String.valueOf(root.getId())}); + wbCompileTreeMapper.updateById(root); + + return root.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createNode(WbCompileTreeSaveReqVO createReqVO) { + // 1. 校验节点类型 + validateNodeType(createReqVO.getNodeType()); + + // 2. 校验父节点 + WbCompileTreeDO parent = validateParentExists(createReqVO.getParentId()); + + // 3. 校验层级关系 + validateHierarchy(createReqVO.getNodeType(), parent); + + // 4. 构建DO + WbCompileTreeDO node = WbCompileTreeConvert.INSTANCE.convert(createReqVO); + + // 5. 设置排序号 + Integer maxSortOrder = wbCompileTreeMapper.selectMaxSortOrderByParentId(createReqVO.getParentId()); + node.setSortOrder(maxSortOrder + 1); + + // 6. 设置路径 + node.setPath(buildPath(parent)); + + // 7. 插入数据库 + wbCompileTreeMapper.insert(node); + + // 8. 更新路径(包含自身ID) + updatePathWithSelfId(node); + + // 9. 如果是单项节点,创建单项基本信息并复制配置快照 + if (WbCompileTreeDO.NODE_TYPE_ITEM.equals(createReqVO.getNodeType())) { + wbItemInfoService.refreshConfigSnapshot(node.getId()); + } + + // 10. 如果是单位工程节点,自动创建分部分项根目录 + if (WbCompileTreeDO.NODE_TYPE_UNIT.equals(createReqVO.getNodeType())) { + wbBoqDivisionService.createRootNode(node.getId(), createReqVO.getName()); + } + + return node.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateNode(WbCompileTreeSaveReqVO updateReqVO) { + // 1. 校验存在 + WbCompileTreeDO existNode = validateNodeExists(updateReqVO.getId()); + + // 2. 不允许修改节点类型 + if (!existNode.getNodeType().equals(updateReqVO.getNodeType())) { + throw exception(WB_COMPILE_TREE_NODE_TYPE_CANNOT_CHANGE); + } + + // 3. 不允许修改根节点 + if (WbCompileTreeDO.NODE_TYPE_ROOT.equals(existNode.getNodeType())) { + // 根节点只能修改名称 + existNode.setName(updateReqVO.getName()); + wbCompileTreeMapper.updateById(existNode); + return; + } + + // 4. 更新 + WbCompileTreeDO updateObj = WbCompileTreeConvert.INSTANCE.convert(updateReqVO); + updateObj.setId(existNode.getId()); + wbCompileTreeMapper.updateById(updateObj); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteNode(Long id) { + // 1. 校验存在 + WbCompileTreeDO node = validateNodeExists(id); + + // 2. 不允许删除根节点 + if (WbCompileTreeDO.NODE_TYPE_ROOT.equals(node.getNodeType())) { + throw exception(WB_COMPILE_TREE_HAS_CHILDREN); + } + + // 3. 校验是否有子节点 + List children = wbCompileTreeMapper.selectListByParentId(id); + if (CollUtil.isNotEmpty(children)) { + throw exception(WB_COMPILE_TREE_HAS_CHILDREN); + } + + // 4. 删除关联数据 + if (WbCompileTreeDO.NODE_TYPE_ITEM.equals(node.getNodeType())) { + wbItemInfoService.deleteByCompileTreeId(id); + } else if (WbCompileTreeDO.NODE_TYPE_UNIT.equals(node.getNodeType())) { + wbUnitInfoService.deleteByCompileTreeId(id); + } + + // 5. 删除节点 + wbCompileTreeMapper.deleteById(id); + } + + @Override + public WbCompileTreeRespVO getNode(Long id) { + WbCompileTreeDO node = wbCompileTreeMapper.selectById(id); + if (node == null) { + return null; + } + return WbCompileTreeConvert.INSTANCE.convert(node); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public List getTree(Long projectId) { + List list = wbCompileTreeMapper.selectListByProjectId(projectId); + + // 如果没有根节点,自动创建 + if (CollUtil.isEmpty(list)) { + // 获取项目名称 + String projectName = getProjectName(projectId); + // 创建根节点 + WbCompileTreeDO root = WbCompileTreeDO.builder() + .projectId(projectId) + .parentId(null) + .nodeType(WbCompileTreeDO.NODE_TYPE_ROOT) + .name(projectName) + .sortOrder(0) + .build(); + wbCompileTreeMapper.insert(root); + // 更新路径 + root.setPath(new String[]{String.valueOf(root.getId())}); + wbCompileTreeMapper.updateById(root); + list = wbCompileTreeMapper.selectListByProjectId(projectId); + } + + return buildTree(list); + } + + /** + * 获取项目名称 + */ + private String getProjectName(Long projectId) { + // 从项目管理树获取项目名称 + com.yhy.module.core.dal.dataobject.workbench.WbProjectTreeDO project = + wbProjectTreeMapper.selectById(projectId); + return project != null ? project.getName() : "新建项目"; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void swapSort(WbCompileTreeSwapSortReqVO swapReqVO) { + // 1. 校验两个节点存在 + WbCompileTreeDO node1 = validateNodeExists(swapReqVO.getNodeId1()); + WbCompileTreeDO node2 = validateNodeExists(swapReqVO.getNodeId2()); + + // 2. 校验是否同级 + if (!Objects.equals(node1.getParentId(), node2.getParentId())) { + throw exception(WB_COMPILE_TREE_NOT_SAME_LEVEL); + } + + // 3. 交换排序号 + Integer tempSortOrder = node1.getSortOrder(); + node1.setSortOrder(node2.getSortOrder()); + node2.setSortOrder(tempSortOrder); + + wbCompileTreeMapper.updateById(node1); + wbCompileTreeMapper.updateById(node2); + } + + /** + * 校验节点类型 + */ + private void validateNodeType(String nodeType) { + if (!WbCompileTreeDO.NODE_TYPE_ITEM.equals(nodeType) + && !WbCompileTreeDO.NODE_TYPE_UNIT.equals(nodeType)) { + throw exception(WB_COMPILE_TREE_NODE_TYPE_INVALID); + } + } + + /** + * 校验父节点存在 + */ + private WbCompileTreeDO validateParentExists(Long parentId) { + if (parentId == null) { + throw exception(WB_COMPILE_TREE_PARENT_NOT_EXISTS); + } + WbCompileTreeDO parent = wbCompileTreeMapper.selectById(parentId); + if (parent == null) { + throw exception(WB_COMPILE_TREE_PARENT_NOT_EXISTS); + } + return parent; + } + + /** + * 校验层级关系 + */ + private void validateHierarchy(String nodeType, WbCompileTreeDO parent) { + if (WbCompileTreeDO.NODE_TYPE_ITEM.equals(nodeType)) { + // 单项只能在根节点下创建 + if (!WbCompileTreeDO.NODE_TYPE_ROOT.equals(parent.getNodeType())) { + throw exception(WB_COMPILE_TREE_ITEM_PARENT_MUST_ROOT); + } + } else if (WbCompileTreeDO.NODE_TYPE_UNIT.equals(nodeType)) { + // 单位工程只能在单项下创建 + if (!WbCompileTreeDO.NODE_TYPE_ITEM.equals(parent.getNodeType())) { + throw exception(WB_COMPILE_TREE_UNIT_PARENT_MUST_ITEM); + } + } + } + + /** + * 校验节点存在 + */ + private WbCompileTreeDO validateNodeExists(Long id) { + WbCompileTreeDO node = wbCompileTreeMapper.selectById(id); + if (node == null) { + throw exception(WB_COMPILE_TREE_NOT_EXISTS); + } + return node; + } + + /** + * 构建路径 + */ + private String[] buildPath(WbCompileTreeDO parent) { + if (parent == null || parent.getPath() == null) { + return new String[]{}; + } + return parent.getPath(); + } + + /** + * 更新路径(包含自身ID) + */ + private void updatePathWithSelfId(WbCompileTreeDO node) { + List pathList = node.getPath() != null + ? new ArrayList<>(Arrays.asList(node.getPath())) + : new ArrayList<>(); + pathList.add(String.valueOf(node.getId())); + node.setPath(pathList.toArray(new String[0])); + wbCompileTreeMapper.updateById(node); + } + + /** + * 构建树形结构 + */ + private List buildTree(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + + // 转换为VO + List voList = list.stream() + .map(WbCompileTreeConvert.INSTANCE::convert) + .collect(Collectors.toList()); + + // 构建父子关系 + Map> parentIdMap = voList.stream() + .filter(vo -> vo.getParentId() != null) + .collect(Collectors.groupingBy(WbCompileTreeRespVO::getParentId)); + + voList.forEach(vo -> vo.setChildren(parentIdMap.get(vo.getId()))); + + // 返回根节点 + return voList.stream() + .filter(vo -> vo.getParentId() == null) + .collect(Collectors.toList()); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbItemInfoServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbItemInfoServiceImpl.java new file mode 100644 index 0000000..3de3f7e --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbItemInfoServiceImpl.java @@ -0,0 +1,175 @@ +package com.yhy.module.core.service.workbench.impl; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yhy.module.core.enums.ErrorCodeConstants.WB_COMPILE_TREE_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.WB_ITEM_INFO_NODE_NOT_ITEM; + +import cn.hutool.core.collection.CollUtil; +import com.yhy.module.core.controller.admin.workbench.vo.WbItemInfoRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbItemInfoSaveReqVO; +import com.yhy.module.core.convert.workbench.WbItemInfoConvert; +import com.yhy.module.core.dal.dataobject.config.ConfigProjectInfoDO; +import com.yhy.module.core.dal.dataobject.workbench.WbCompileTreeDO; +import com.yhy.module.core.dal.dataobject.workbench.WbItemInfoDO; +import com.yhy.module.core.dal.dataobject.workbench.WbProjectTreeDO; +import com.yhy.module.core.dal.mysql.config.ConfigProjectInfoMapper; +import com.yhy.module.core.dal.mysql.workbench.WbCompileTreeMapper; +import com.yhy.module.core.dal.mysql.workbench.WbItemInfoMapper; +import com.yhy.module.core.dal.mysql.workbench.WbProjectTreeMapper; +import com.yhy.module.core.service.workbench.WbItemInfoService; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * 单项基本信息 Service 实现类 + * + * @author yhy + */ +@Service +@Validated +@Slf4j +public class WbItemInfoServiceImpl implements WbItemInfoService { + + @Resource + private WbItemInfoMapper wbItemInfoMapper; + + @Resource + private WbCompileTreeMapper wbCompileTreeMapper; + + @Resource + private WbProjectTreeMapper wbProjectTreeMapper; + + @Resource + private ConfigProjectInfoMapper configProjectInfoMapper; + + @Override + public WbItemInfoRespVO getByCompileTreeId(Long compileTreeId) { + // 1. 校验节点存在且是单项节点 + validateItemNode(compileTreeId); + + // 2. 查询单项基本信息 + WbItemInfoDO itemInfo = wbItemInfoMapper.selectByCompileTreeId(compileTreeId); + if (itemInfo == null) { + return null; + } + + return WbItemInfoConvert.INSTANCE.convert(itemInfo); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long save(WbItemInfoSaveReqVO saveReqVO) { + // 1. 校验节点存在且是单项节点 + validateItemNode(saveReqVO.getCompileTreeId()); + + // 2. 查询是否已存在 + WbItemInfoDO existInfo = wbItemInfoMapper.selectByCompileTreeId(saveReqVO.getCompileTreeId()); + + if (existInfo != null) { + // 更新 + existInfo.setInfoData(saveReqVO.getInfoData()); + wbItemInfoMapper.updateById(existInfo); + return existInfo.getId(); + } else { + // 新增 + WbItemInfoDO itemInfo = WbItemInfoConvert.INSTANCE.convert(saveReqVO); + wbItemInfoMapper.insert(itemInfo); + return itemInfo.getId(); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void refreshConfigSnapshot(Long compileTreeId) { + // 1. 校验节点存在且是单项节点 + WbCompileTreeDO node = validateItemNode(compileTreeId); + + // 2. 获取项目信息 + WbProjectTreeDO project = wbProjectTreeMapper.selectById(node.getProjectId()); + if (project == null) { + return; + } + + // 3. 获取项目的行业ID + Long industryId = project.getIndustryId(); + if (industryId == null) { + return; + } + + // 4. 查询行业下的配置信息 + List configList = configProjectInfoMapper.selectListByConfigTreeId(industryId); + + // 5. 构建配置快照 + Map configSnapshot = buildConfigSnapshot(configList); + + // 6. 查询或创建单项基本信息 + WbItemInfoDO itemInfo = wbItemInfoMapper.selectByCompileTreeId(compileTreeId); + if (itemInfo == null) { + itemInfo = WbItemInfoDO.builder() + .compileTreeId(compileTreeId) + .configSnapshot(configSnapshot) + .infoData(new HashMap<>()) + .build(); + wbItemInfoMapper.insert(itemInfo); + } else { + itemInfo.setConfigSnapshot(configSnapshot); + wbItemInfoMapper.updateById(itemInfo); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteByCompileTreeId(Long compileTreeId) { + wbItemInfoMapper.deleteByCompileTreeId(compileTreeId); + } + + /** + * 校验节点存在且是单项节点 + */ + private WbCompileTreeDO validateItemNode(Long compileTreeId) { + WbCompileTreeDO node = wbCompileTreeMapper.selectById(compileTreeId); + if (node == null) { + throw exception(WB_COMPILE_TREE_NOT_EXISTS); + } + if (!WbCompileTreeDO.NODE_TYPE_ITEM.equals(node.getNodeType())) { + throw exception(WB_ITEM_INFO_NODE_NOT_ITEM); + } + return node; + } + + /** + * 构建配置快照 + */ + private Map buildConfigSnapshot(List configList) { + Map snapshot = new HashMap<>(); + snapshot.put("snapshotTime", System.currentTimeMillis()); + + if (CollUtil.isEmpty(configList)) { + snapshot.put("configs", new ArrayList<>()); + return snapshot; + } + + // 将配置列表转换为快照格式 + List> configs = new ArrayList<>(); + for (ConfigProjectInfoDO config : configList) { + Map configItem = new HashMap<>(); + configItem.put("id", config.getId()); + configItem.put("code", config.getCode()); + configItem.put("name", config.getName()); + configItem.put("content", config.getContent()); + configItem.put("parentId", config.getParentId()); + configItem.put("sortOrder", config.getSortOrder()); + configs.add(configItem); + } + snapshot.put("configs", configs); + + return snapshot; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbProjectServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbProjectServiceImpl.java new file mode 100644 index 0000000..fcb5ed9 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbProjectServiceImpl.java @@ -0,0 +1,510 @@ +package com.yhy.module.core.service.workbench.impl; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.*; +import static com.yhy.module.core.enums.ErrorCodeConstants.*; + +import cn.hutool.core.collection.CollUtil; +import com.yhy.module.core.controller.admin.workbench.vo.WbProjectTreeRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbProjectTreeSaveReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbProjectTreeSwapSortReqVO; +import com.yhy.module.core.convert.workbench.WbProjectConvert; +import com.yhy.module.core.dal.dataobject.config.ConfigProjectTreeDO; +import com.yhy.module.core.dal.dataobject.infoprice.InfoPriceBookDO; +import com.yhy.module.core.dal.dataobject.infoprice.InfoPriceTreeDO; +import com.yhy.module.core.dal.dataobject.workbench.WbProjectTreeDO; +import com.yhy.module.core.dal.mysql.config.ConfigProjectTreeMapper; +import com.yhy.module.core.dal.mysql.infoprice.InfoPriceBookMapper; +import com.yhy.module.core.dal.mysql.infoprice.InfoPriceTreeMapper; +import com.yhy.module.core.dal.mysql.workbench.WbProjectTreeMapper; +import com.yhy.module.core.service.workbench.WbProjectService; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * 工作台项目管理树 Service 实现类 + * + * @author yhy + */ +@Service +@Validated +@Slf4j +public class WbProjectServiceImpl implements WbProjectService { + + @Resource + private WbProjectTreeMapper wbProjectTreeMapper; + + @Resource + private ConfigProjectTreeMapper configProjectTreeMapper; + + @Resource + private InfoPriceTreeMapper infoPriceTreeMapper; + + @Resource + private InfoPriceBookMapper infoPriceBookMapper; + + @Resource + private com.yhy.module.core.dal.mysql.workbench.WbCompileTreeMapper wbCompileTreeMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createNode(WbProjectTreeSaveReqVO createReqVO) { + // 1. 校验节点类型 + validateNodeType(createReqVO.getNodeType()); + + // 2. 校验父节点 + if (createReqVO.getParentId() != null) { + WbProjectTreeDO parent = validateParentExists(createReqVO.getParentId()); + // 只有目录节点才能添加子节点 + if (!WbProjectTreeDO.NODE_TYPE_DIRECTORY.equals(parent.getNodeType())) { + throw exception(WB_PROJECT_PARENT_NOT_DIRECTORY); + } + } + + // 3. 如果是项目节点,校验项目编号唯一性 + if (WbProjectTreeDO.NODE_TYPE_PROJECT.equals(createReqVO.getNodeType())) { + if (createReqVO.getProjectCode() != null) { + validateProjectCodeUnique(createReqVO.getProjectCode(), null); + } + } + + // 4. 构建DO + WbProjectTreeDO node = WbProjectConvert.INSTANCE.convert(createReqVO); + + // 5. 设置排序号(支持上方/下方插入) + Integer sortOrder = calculateSortOrder(createReqVO); + node.setSortOrder(sortOrder); + + // 6. 设置路径 + node.setPath(buildPath(createReqVO.getParentId())); + + // 7. 如果是项目节点,设置默认状态和快照 + if (WbProjectTreeDO.NODE_TYPE_PROJECT.equals(createReqVO.getNodeType())) { + node.setStatus(WbProjectTreeDO.STATUS_DRAFT); + + // 设置信息价路径 + setInfoPricePaths(node, createReqVO); + + // 构建快照 + node.setSnapshotJson(buildSnapshot(createReqVO)); + } + + // 8. 插入数据库 + wbProjectTreeMapper.insert(node); + + // 9. 更新路径(包含自身ID) + updatePathWithSelfId(node); + + return node.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateNode(WbProjectTreeSaveReqVO updateReqVO) { + // 1. 校验存在 + WbProjectTreeDO existNode = validateNodeExists(updateReqVO.getId()); + + // 2. 不允许修改节点类型 + if (!existNode.getNodeType().equals(updateReqVO.getNodeType())) { + throw exception(WB_PROJECT_NODE_TYPE_CANNOT_CHANGE); + } + + // 3. 如果是项目节点,校验项目编号唯一性 + if (WbProjectTreeDO.NODE_TYPE_PROJECT.equals(updateReqVO.getNodeType())) { + if (updateReqVO.getProjectCode() != null) { + validateProjectCodeUnique(updateReqVO.getProjectCode(), updateReqVO.getId()); + } + } + + // 4. 更新 + WbProjectTreeDO updateObj = WbProjectConvert.INSTANCE.convert(updateReqVO); + wbProjectTreeMapper.updateById(updateObj); + + // 5. 如果是项目节点且名称有变化,同步更新编制树的根节点名称 + if (WbProjectTreeDO.NODE_TYPE_PROJECT.equals(updateReqVO.getNodeType()) + && updateReqVO.getName() != null + && !updateReqVO.getName().equals(existNode.getName())) { + syncCompileTreeRootName(updateReqVO.getId(), updateReqVO.getName()); + } + } + + /** + * 同步编制树根节点名称 + */ + private void syncCompileTreeRootName(Long projectId, String newName) { + com.yhy.module.core.dal.dataobject.workbench.WbCompileTreeDO root = + wbCompileTreeMapper.selectRootByProjectId(projectId); + if (root != null) { + root.setName(newName); + wbCompileTreeMapper.updateById(root); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteNode(Long id) { + // 1. 校验存在 + validateNodeExists(id); + + // 2. 校验是否有子节点 + List children = wbProjectTreeMapper.selectListByParentId(id); + if (CollUtil.isNotEmpty(children)) { + throw exception(WB_PROJECT_HAS_CHILDREN); + } + + // 3. 删除 + wbProjectTreeMapper.deleteById(id); + } + + @Override + public WbProjectTreeRespVO getNode(Long id) { + WbProjectTreeDO node = wbProjectTreeMapper.selectById(id); + if (node == null) { + return null; + } + + WbProjectTreeRespVO respVO = WbProjectConvert.INSTANCE.convert(node); + + // 如果是项目节点,填充关联名称 + if (WbProjectTreeDO.NODE_TYPE_PROJECT.equals(node.getNodeType())) { + fillRelatedNames(respVO, node); + } + + return respVO; + } + + @Override + public List getTree() { + List list = wbProjectTreeMapper.selectAllOrderBySortOrder(); + return buildTree(list); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void swapSort(WbProjectTreeSwapSortReqVO swapReqVO) { + // 1. 校验两个节点存在 + WbProjectTreeDO node1 = validateNodeExists(swapReqVO.getNodeId1()); + WbProjectTreeDO node2 = validateNodeExists(swapReqVO.getNodeId2()); + + // 2. 校验是否同级 + if (!Objects.equals(node1.getParentId(), node2.getParentId())) { + throw exception(WB_PROJECT_NOT_SAME_LEVEL); + } + + // 3. 交换排序号 + Integer tempSortOrder = node1.getSortOrder(); + node1.setSortOrder(node2.getSortOrder()); + node2.setSortOrder(tempSortOrder); + + wbProjectTreeMapper.updateById(node1); + wbProjectTreeMapper.updateById(node2); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void archiveProject(Long id) { + // 1. 校验存在且是项目节点 + WbProjectTreeDO node = validateNodeExists(id); + if (!WbProjectTreeDO.NODE_TYPE_PROJECT.equals(node.getNodeType())) { + throw exception(WB_PROJECT_NOT_PROJECT_NODE); + } + + // 2. 更新状态为已归档 + node.setStatus(WbProjectTreeDO.STATUS_ARCHIVED); + wbProjectTreeMapper.updateById(node); + } + + /** + * 校验节点类型 + */ + private void validateNodeType(String nodeType) { + if (!WbProjectTreeDO.NODE_TYPE_DIRECTORY.equals(nodeType) + && !WbProjectTreeDO.NODE_TYPE_PROJECT.equals(nodeType)) { + throw exception(WB_PROJECT_NODE_TYPE_INVALID); + } + } + + /** + * 校验父节点存在 + */ + private WbProjectTreeDO validateParentExists(Long parentId) { + WbProjectTreeDO parent = wbProjectTreeMapper.selectById(parentId); + if (parent == null) { + throw exception(WB_PROJECT_PARENT_NOT_EXISTS); + } + return parent; + } + + /** + * 校验项目编号唯一性 + */ + private void validateProjectCodeUnique(String projectCode, Long selfId) { + WbProjectTreeDO existNode = wbProjectTreeMapper.selectByProjectCode(projectCode); + if (existNode != null && !existNode.getId().equals(selfId)) { + throw exception(WB_PROJECT_CODE_EXISTS); + } + } + + /** + * 校验节点存在 + */ + private WbProjectTreeDO validateNodeExists(Long id) { + WbProjectTreeDO node = wbProjectTreeMapper.selectById(id); + if (node == null) { + throw exception(WB_PROJECT_NOT_EXISTS); + } + return node; + } + + /** + * 构建路径 + */ + private String[] buildPath(Long parentId) { + if (parentId == null) { + return new String[]{}; + } + WbProjectTreeDO parent = wbProjectTreeMapper.selectById(parentId); + if (parent == null || parent.getPath() == null) { + return new String[]{String.valueOf(parentId)}; + } + List pathList = new ArrayList<>(Arrays.asList(parent.getPath())); + pathList.add(String.valueOf(parentId)); + return pathList.toArray(new String[0]); + } + + /** + * 更新路径(包含自身ID) + */ + private void updatePathWithSelfId(WbProjectTreeDO node) { + List pathList = node.getPath() != null ? new ArrayList<>(Arrays.asList(node.getPath())) : new ArrayList<>(); + pathList.add(String.valueOf(node.getId())); + node.setPath(pathList.toArray(new String[0])); + wbProjectTreeMapper.updateById(node); + } + + /** + * 设置信息价路径 + */ + private void setInfoPricePaths(WbProjectTreeDO node, WbProjectTreeSaveReqVO reqVO) { + if (reqVO.getInfoPriceProfessionId() != null) { + InfoPriceTreeDO professionNode = infoPriceTreeMapper.selectById(reqVO.getInfoPriceProfessionId()); + if (professionNode != null && professionNode.getPath() != null) { + node.setInfoPriceProfessionPath(convertPathToLongArray(professionNode.getPath())); + } + } + if (reqVO.getInfoPriceRegionId() != null) { + InfoPriceTreeDO regionNode = infoPriceTreeMapper.selectById(reqVO.getInfoPriceRegionId()); + if (regionNode != null && regionNode.getPath() != null) { + node.setInfoPriceRegionPath(convertPathToLongArray(regionNode.getPath())); + } + } + } + + /** + * 计算排序号(支持上方/下方插入) + * @param reqVO 创建请求 + * @return 排序号 + */ + private Integer calculateSortOrder(WbProjectTreeSaveReqVO reqVO) { + String insertPosition = reqVO.getInsertPosition(); + Long referenceNodeId = reqVO.getReferenceNodeId(); + + // 默认尾部插入 + if (insertPosition == null || WbProjectTreeSaveReqVO.INSERT_POSITION_END.equals(insertPosition) + || referenceNodeId == null) { + Integer maxSortOrder = wbProjectTreeMapper.selectMaxSortOrderByParentId(reqVO.getParentId()); + return maxSortOrder + 1; + } + + // 获取参考节点 + WbProjectTreeDO referenceNode = wbProjectTreeMapper.selectById(referenceNodeId); + if (referenceNode == null) { + Integer maxSortOrder = wbProjectTreeMapper.selectMaxSortOrderByParentId(reqVO.getParentId()); + return maxSortOrder + 1; + } + + Integer targetSortOrder; + if (WbProjectTreeSaveReqVO.INSERT_POSITION_ABOVE.equals(insertPosition)) { + // 上方插入:使用参考节点的排序号 + targetSortOrder = referenceNode.getSortOrder(); + } else { + // 下方插入:使用参考节点排序号 + 1 + targetSortOrder = referenceNode.getSortOrder() + 1; + } + + // 将目标位置及之后的节点排序号都加1 + List nodesToShift = wbProjectTreeMapper.selectListByParentIdAndSortOrderGe( + referenceNode.getParentId(), targetSortOrder); + for (WbProjectTreeDO nodeToShift : nodesToShift) { + nodeToShift.setSortOrder(nodeToShift.getSortOrder() + 1); + wbProjectTreeMapper.updateById(nodeToShift); + } + + return targetSortOrder; + } + + /** + * 构建快照数据 + */ + private Map buildSnapshot(WbProjectTreeSaveReqVO reqVO) { + Map snapshot = new HashMap<>(); + snapshot.put("snapshotTime", System.currentTimeMillis()); + + if (reqVO.getIndustryProvinceId() != null) { + ConfigProjectTreeDO provinceNode = configProjectTreeMapper.selectById(reqVO.getIndustryProvinceId()); + if (provinceNode != null) { + snapshot.put("industryProvinceName", provinceNode.getName()); + snapshot.put("industryProvinceCode", provinceNode.getCode()); + } + } + if (reqVO.getIndustryId() != null) { + ConfigProjectTreeDO industryNode = configProjectTreeMapper.selectById(reqVO.getIndustryId()); + if (industryNode != null) { + snapshot.put("industryName", industryNode.getName()); + snapshot.put("industryCode", industryNode.getCode()); + } + } + if (reqVO.getInfoPriceBookId() != null) { + InfoPriceBookDO book = infoPriceBookMapper.selectById(reqVO.getInfoPriceBookId()); + if (book != null) { + snapshot.put("infoPriceBookName", book.getName()); + snapshot.put("infoPriceBookVersion", book.getCatalogVersion()); + } + } + // 存储信息价专业字典值 + if (reqVO.getInfoPriceProfessionType() != null) { + snapshot.put("infoPriceProfessionType", reqVO.getInfoPriceProfessionType()); + } + + return snapshot; + } + + /** + * 填充关联名称 + */ + private void fillRelatedNames(WbProjectTreeRespVO respVO, WbProjectTreeDO node) { + if (node.getIndustryProvinceId() != null) { + ConfigProjectTreeDO provinceNode = configProjectTreeMapper.selectById(node.getIndustryProvinceId()); + if (provinceNode != null) { + respVO.setIndustryProvinceName(provinceNode.getName()); + } + } + if (node.getIndustryId() != null) { + ConfigProjectTreeDO industryNode = configProjectTreeMapper.selectById(node.getIndustryId()); + if (industryNode != null) { + respVO.setIndustryName(industryNode.getName()); + } + } + if (node.getInfoPriceProfessionId() != null) { + InfoPriceTreeDO professionNode = infoPriceTreeMapper.selectById(node.getInfoPriceProfessionId()); + if (professionNode != null) { + respVO.setInfoPriceProfessionName(professionNode.getName()); + } + } + // 如果没有infoPriceProfessionId,尝试从快照读取专业类型 + if (respVO.getInfoPriceProfessionName() == null && node.getSnapshotJson() != null) { + Object professionType = node.getSnapshotJson().get("infoPriceProfessionType"); + if (professionType != null) { + // 返回字典值,前端通过字典转换为中文名称 + respVO.setInfoPriceProfessionName(String.valueOf(professionType)); + } + } + if (node.getInfoPriceRegionId() != null) { + InfoPriceTreeDO regionNode = infoPriceTreeMapper.selectById(node.getInfoPriceRegionId()); + if (regionNode != null) { + respVO.setInfoPriceRegionName(regionNode.getName()); + } + } + if (node.getInfoPriceBookId() != null) { + InfoPriceBookDO book = infoPriceBookMapper.selectById(node.getInfoPriceBookId()); + if (book != null) { + respVO.setInfoPriceBookName(book.getName()); + } + } + } + + /** + * 将String[]路径转换为Long[] + */ + private Long[] convertPathToLongArray(String[] path) { + if (path == null) { + return null; + } + Long[] result = new Long[path.length]; + for (int i = 0; i < path.length; i++) { + result[i] = Long.parseLong(path[i]); + } + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveToHistoryLibrary(Long projectId) { + // 1. 校验项目存在且为项目节点 + WbProjectTreeDO project = validateNodeExists(projectId); + if (!WbProjectTreeDO.NODE_TYPE_PROJECT.equals(project.getNodeType())) { + throw exception(WB_PROJECT_NOT_PROJECT_NODE); + } + // 2. 更新为已保存至历史库 + project.setInHistoryLibrary(true); + wbProjectTreeMapper.updateById(project); + log.info("[saveToHistoryLibrary] 项目已保存至历史库, projectId={}", projectId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void removeFromHistoryLibrary(Long projectId) { + // 1. 校验项目存在且为项目节点 + WbProjectTreeDO project = validateNodeExists(projectId); + if (!WbProjectTreeDO.NODE_TYPE_PROJECT.equals(project.getNodeType())) { + throw exception(WB_PROJECT_NOT_PROJECT_NODE); + } + // 2. 更新为从历史库撤销 + project.setInHistoryLibrary(false); + wbProjectTreeMapper.updateById(project); + log.info("[removeFromHistoryLibrary] 项目已从历史库撤销, projectId={}", projectId); + } + + /** + * 构建树形结构 + */ + private List buildTree(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + + // 转换为VO并填充关联名称 + List voList = list.stream() + .map(node -> { + WbProjectTreeRespVO vo = WbProjectConvert.INSTANCE.convert(node); + // 如果是项目节点,填充关联名称 + if (WbProjectTreeDO.NODE_TYPE_PROJECT.equals(node.getNodeType())) { + fillRelatedNames(vo, node); + } + return vo; + }) + .collect(Collectors.toList()); + + // 构建父子关系 + Map> parentIdMap = voList.stream() + .filter(vo -> vo.getParentId() != null) + .collect(Collectors.groupingBy(WbProjectTreeRespVO::getParentId)); + + voList.forEach(vo -> vo.setChildren(parentIdMap.get(vo.getId()))); + + // 返回根节点 + return voList.stream() + .filter(vo -> vo.getParentId() == null) + .collect(Collectors.toList()); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbResourceSummaryServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbResourceSummaryServiceImpl.java new file mode 100644 index 0000000..eb1f6f3 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbResourceSummaryServiceImpl.java @@ -0,0 +1,606 @@ +package com.yhy.module.core.service.workbench.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import com.yhy.module.core.controller.admin.workbench.vo.WbResourceSourceBoqRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbResourceSourceUnitRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbResourceSummaryRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbResourceSummaryTreeRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbResourceSummaryUpdateReqVO; +import com.yhy.module.core.dal.dataobject.workbench.WbBoqDivisionDO; +import com.yhy.module.core.dal.dataobject.workbench.WbBoqResourceDO; +import com.yhy.module.core.dal.dataobject.workbench.WbCompileTreeDO; +import com.yhy.module.core.dal.dataobject.workbench.WbResourceSummaryDO; +import com.yhy.module.core.dal.mysql.workbench.WbBoqDivisionMapper; +import com.yhy.module.core.dal.mysql.workbench.WbBoqResourceMapper; +import com.yhy.module.core.dal.mysql.workbench.WbCompileTreeMapper; +import com.yhy.module.core.dal.mysql.workbench.WbResourceSummaryMapper; +import com.yhy.module.core.service.workbench.WbResourceSummaryService; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * 工料机汇总 Service 实现类 + * + * @author yhy + */ +@Service +@Validated +@Slf4j +public class WbResourceSummaryServiceImpl implements WbResourceSummaryService { + + @Resource + private WbResourceSummaryMapper wbResourceSummaryMapper; + + @Resource + private WbBoqResourceMapper wbBoqResourceMapper; + + @Resource + private WbBoqDivisionMapper wbBoqDivisionMapper; + + @Resource + private WbCompileTreeMapper wbCompileTreeMapper; + + @Override + public List getSummaryTree(Long projectId, Long compileTreeId) { + // 1. 查询工料机汇总数据(支持单位工程级过滤) + List aggregations = getResourceAggregations(projectId, null, compileTreeId); + + // 2. 查询评标指定材料数量 + List bidMaterials = wbResourceSummaryMapper.selectBidMaterialsByProjectId(projectId); + + // 3. 按工料机类别(resource_type)动态统计数量 + Map categoryCount = new LinkedHashMap<>(); + for (ResourceAggregation agg : aggregations) { + String type = agg.getResourceType(); + if (type != null) { + categoryCount.merge(type, 1, Integer::sum); + } + } + + // 4. 动态构建类别子节点 + List children = new ArrayList<>(); + for (Map.Entry entry : categoryCount.entrySet()) { + WbResourceSummaryTreeRespVO node = new WbResourceSummaryTreeRespVO(); + node.setId(entry.getKey()); + node.setName(entry.getKey()); + node.setNodeType(WbResourceSummaryTreeRespVO.NODE_TYPE_CATEGORY); + node.setResourceType(entry.getKey()); + node.setCount(entry.getValue()); + children.add(node); + } + + // 评标指定材料 + WbResourceSummaryTreeRespVO bidMaterialNode = new WbResourceSummaryTreeRespVO(); + bidMaterialNode.setId("bid_material"); + bidMaterialNode.setName("评标指定材料"); + bidMaterialNode.setNodeType(WbResourceSummaryTreeRespVO.NODE_TYPE_BID_MATERIAL); + bidMaterialNode.setCount(bidMaterials.size()); + children.add(bidMaterialNode); + + // 根节点 + WbResourceSummaryTreeRespVO root = new WbResourceSummaryTreeRespVO(); + root.setId("root"); + root.setName("工料机汇总"); + root.setNodeType(WbResourceSummaryTreeRespVO.NODE_TYPE_ROOT); + root.setCount(aggregations.size()); + root.setChildren(children); + + List result = new ArrayList<>(); + result.add(root); + return result; + } + + @Override + public List getSummaryList(Long projectId, String category, Long compileTreeId) { + // 1. 查询汇总数据(支持单位工程级过滤) + List aggregations; + if ("bid_material".equals(category)) { + // 评标指定材料:只返回标记为评标指定材料的 + aggregations = getBidMaterialAggregations(projectId); + } else { + // 按类别查询(category 直接就是 resource_type 值,如"人"/"材"/"机") + aggregations = getResourceAggregations(projectId, category, compileTreeId); + } + + // 2. 查询用户标记 + List summaries = wbResourceSummaryMapper.selectListByProjectId(projectId); + Map summaryMap = summaries.stream() + .collect(Collectors.toMap(WbResourceSummaryDO::getResourceKey, s -> s, (a, b) -> a)); + + // 3. 转换为VO + return aggregations.stream().map(agg -> { + WbResourceSummaryRespVO vo = new WbResourceSummaryRespVO(); + vo.setResourceKey(agg.getResourceKey()); + vo.setCode(agg.getCode()); + vo.setName(agg.getName()); + vo.setSpec(agg.getSpec()); + vo.setUnit(agg.getUnit()); + vo.setResourceType(agg.getResourceType()); + vo.setTaxRate(agg.getTaxRate()); + vo.setTotalQty(agg.getTotalQty()); + vo.setTaxExclBasePrice(agg.getTaxExclBasePrice()); + vo.setTaxInclBasePrice(agg.getTaxInclBasePrice()); + vo.setTaxExclCompilePrice(agg.getTaxExclCompilePrice()); + vo.setTaxInclCompilePrice(agg.getTaxInclCompilePrice()); + + // 计算合价 + if (agg.getTotalQty() != null) { + if (agg.getTaxExclCompilePrice() != null) { + vo.setTaxExclTotalPrice(agg.getTotalQty().multiply(agg.getTaxExclCompilePrice()) + .setScale(2, RoundingMode.HALF_UP)); + } + if (agg.getTaxInclCompilePrice() != null) { + vo.setTaxInclTotalPrice(agg.getTotalQty().multiply(agg.getTaxInclCompilePrice()) + .setScale(2, RoundingMode.HALF_UP)); + } + } + + // 用户标记 + WbResourceSummaryDO summary = summaryMap.get(agg.getResourceKey()); + if (summary != null) { + vo.setId(summary.getId()); + vo.setIsPrint(summary.getIsPrint() != null && summary.getIsPrint() == 1); + vo.setIsBidMaterial(summary.getIsBidMaterial() != null && summary.getIsBidMaterial() == 1); + vo.setPriceSource(summary.getPriceSource()); + vo.setRemark(summary.getRemark()); + } else { + vo.setIsPrint(false); + vo.setIsBidMaterial(false); + } + + return vo; + }).collect(Collectors.toList()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateSummary(WbResourceSummaryUpdateReqVO updateReqVO) { + Long projectId = updateReqVO.getProjectId(); + String resourceKey = updateReqVO.getResourceKey(); + + // 1. 查询或创建汇总记录 + WbResourceSummaryDO summary = wbResourceSummaryMapper.selectByProjectIdAndResourceKey(projectId, resourceKey); + if (summary == null) { + summary = new WbResourceSummaryDO(); + summary.setTenantId(TenantContextHolder.getTenantId()); + summary.setProjectId(projectId); + summary.setResourceKey(resourceKey); + summary.setIsPrint(0); + summary.setIsBidMaterial(0); + } + + // 2. 更新字段 + if (updateReqVO.getIsPrint() != null) { + summary.setIsPrint(updateReqVO.getIsPrint() ? 1 : 0); + } + if (updateReqVO.getIsBidMaterial() != null) { + summary.setIsBidMaterial(updateReqVO.getIsBidMaterial() ? 1 : 0); + } + if (updateReqVO.getPriceSource() != null) { + summary.setPriceSource(updateReqVO.getPriceSource()); + } + if (updateReqVO.getRemark() != null) { + summary.setRemark(updateReqVO.getRemark()); + } + + // 3. 保存 + if (summary.getId() == null) { + wbResourceSummaryMapper.insert(summary); + } else { + wbResourceSummaryMapper.updateById(summary); + } + + // 4. 如果修改了编码,同步到工作台工料机 + if (StrUtil.isNotBlank(updateReqVO.getNewCode())) { + updateResourceCode(projectId, resourceKey, updateReqVO.getNewCode()); + } + } + + @Override + public List getSourceUnits(Long projectId, String resourceKey) { + // 1. 获取项目下所有单位工程 + List unitNodes = wbCompileTreeMapper.selectUnitNodesByProjectId(projectId); + if (CollUtil.isEmpty(unitNodes)) { + return new ArrayList<>(); + } + + // 2. 解析resourceKey获取code/name/spec/unit + ResourceKeyInfo keyInfo = parseResourceKey(projectId, resourceKey); + if (keyInfo == null) { + return new ArrayList<>(); + } + + // 3. 统计每个单位工程下该工料机的数量 + List result = new ArrayList<>(); + for (WbCompileTreeDO unit : unitNodes) { + int count = countResourceInUnit(unit.getId(), keyInfo); + if (count > 0) { + WbResourceSourceUnitRespVO vo = new WbResourceSourceUnitRespVO(); + vo.setCompileTreeId(unit.getId()); + vo.setUnitName(unit.getName()); + vo.setResourceCount(count); + result.add(vo); + } + } + + return result; + } + + @Override + public List getSourceBoqs(Long compileTreeId, String resourceKey) { + // 1. 解析resourceKey + ResourceKeyInfo keyInfo = parseResourceKeyFromDb(compileTreeId, resourceKey); + if (keyInfo == null) { + return new ArrayList<>(); + } + + // 2. 查询该单位工程下所有定额节点 + List quotaNodes = wbBoqDivisionMapper.selectQuotaNodesByCompileTreeId(compileTreeId); + if (CollUtil.isEmpty(quotaNodes)) { + return new ArrayList<>(); + } + + // 3. 查询包含该工料机的定额节点,并获取其父清单节点信息 + List result = new ArrayList<>(); + for (WbBoqDivisionDO quotaNode : quotaNodes) { + // 检查该定额节点是否包含该工料机 + List resources = wbBoqResourceMapper.selectListByDivisionId(quotaNode.getId()); + boolean hasResource = resources.stream().anyMatch(r -> + matchResourceKey(r, keyInfo)); + + if (hasResource) { + // 获取父清单节点 + WbBoqDivisionDO boqNode = findParentBoqNode(quotaNode); + if (boqNode != null) { + WbResourceSourceBoqRespVO vo = new WbResourceSourceBoqRespVO(); + vo.setBoqId(boqNode.getId()); + vo.setBoqCode(boqNode.getCode()); + vo.setBoqName(boqNode.getName()); + vo.setUnit(boqNode.getUnit()); + vo.setQty(boqNode.getQty()); + // 从 attributes.tabTypes 获取标签页类型,无 tabTypes 或为空则默认 division + vo.setTabType(resolveTabType(boqNode)); + result.add(vo); + } + } + } + + return result; + } + + // ========== 私有方法 ========== + + /** + * 获取项目下工料机汇总数据 + */ + private List getResourceAggregations(Long projectId, String resourceType) { + return getResourceAggregations(projectId, resourceType, null); + } + + /** + * 获取项目下工料机汇总数据(支持单位工程级过滤) + * + * @param projectId 项目ID + * @param resourceType 资源类型过滤(可选) + * @param compileTreeId 编制模式树的单位工程节点ID(可选,传了只汇总该单位工程) + */ + private List getResourceAggregations(Long projectId, String resourceType, Long compileTreeId) { + // 1. 获取单位工程列表 + List unitNodes; + if (compileTreeId != null) { + // 单位工程级:只查指定的单位工程 + WbCompileTreeDO unitNode = wbCompileTreeMapper.selectById(compileTreeId); + unitNodes = unitNode != null ? Collections.singletonList(unitNode) : new ArrayList<>(); + } else { + // 项目级:查所有单位工程 + unitNodes = wbCompileTreeMapper.selectUnitNodesByProjectId(projectId); + } + if (CollUtil.isEmpty(unitNodes)) { + return new ArrayList<>(); + } + + // 2. 收集所有工料机数据 + Map aggregationMap = new HashMap<>(); + for (WbCompileTreeDO unit : unitNodes) { + // 获取该单位工程下所有定额节点 + List quotaNodes = wbBoqDivisionMapper.selectQuotaNodesByCompileTreeId(unit.getId()); + for (WbBoqDivisionDO quotaNode : quotaNodes) { + // 获取定额节点下的工料机 + List resources = wbBoqResourceMapper.selectListByDivisionId(quotaNode.getId()); + for (WbBoqResourceDO resource : resources) { + // 归一化类别(兼容旧数据英文格式) + String normalizedType = normalizeResourceType(resource.getResourceType()); + // 过滤类型 + if (resourceType != null && !resourceType.equals(normalizedType)) { + continue; + } + // 跳过子工料机(复合工料机的子项) + if (resource.getParentId() != null) { + continue; + } + + // 生成唯一键 + String key = generateResourceKey(resource.getCode(), resource.getName(), + resource.getSpec(), resource.getUnit()); + + // 汇总 + ResourceAggregation agg = aggregationMap.get(key); + if (agg == null) { + agg = new ResourceAggregation(); + agg.setResourceKey(key); + agg.setCode(resource.getCode()); + agg.setName(resource.getName()); + agg.setSpec(resource.getSpec()); + agg.setUnit(resource.getUnit()); + agg.setResourceType(normalizedType); + agg.setTaxRate(resource.getTaxRate()); + agg.setTaxExclBasePrice(resource.getTaxExclBasePrice()); + agg.setTaxInclBasePrice(resource.getTaxInclBasePrice()); + agg.setTaxExclCompilePrice(resource.getTaxExclCompilePrice()); + agg.setTaxInclCompilePrice(resource.getTaxInclCompilePrice()); + agg.setTotalQty(BigDecimal.ZERO); + aggregationMap.put(key, agg); + } + + // 累加数量(优先使用调整消耗量,其次使用定额消耗量) + BigDecimal qty = resource.getAdjustConsumeQty() != null + ? resource.getAdjustConsumeQty() : resource.getConsumeQty(); + if (qty != null) { + agg.setTotalQty(agg.getTotalQty().add(qty)); + } + } + } + } + + return new ArrayList<>(aggregationMap.values()); + } + + /** + * 获取评标指定材料汇总数据 + */ + private List getBidMaterialAggregations(Long projectId) { + // 1. 获取所有评标指定材料的resourceKey + List bidMaterials = wbResourceSummaryMapper.selectBidMaterialsByProjectId(projectId); + if (CollUtil.isEmpty(bidMaterials)) { + return new ArrayList<>(); + } + + // 2. 获取所有汇总数据 + List allAggregations = getResourceAggregations(projectId, null); + + // 3. 过滤出评标指定材料 + java.util.Set bidKeys = bidMaterials.stream() + .map(WbResourceSummaryDO::getResourceKey) + .collect(Collectors.toSet()); + + return allAggregations.stream() + .filter(agg -> bidKeys.contains(agg.getResourceKey())) + .collect(Collectors.toList()); + } + + /** + * 归一化工料机类别(兼容旧数据英文格式转为中文类别code) + */ + private String normalizeResourceType(String type) { + if (type == null) { + return null; + } + switch (type) { + case "labor": + return "人"; + case "material": + return "材"; + case "machine": + return "机"; + default: + return type; + } + } + + /** + * 生成资源唯一键 + */ + private String generateResourceKey(String code, String name, String spec, String unit) { + String raw = StrUtil.nullToEmpty(code) + "|" + + StrUtil.nullToEmpty(name) + "|" + + StrUtil.nullToEmpty(spec) + "|" + + StrUtil.nullToEmpty(unit); + return DigestUtil.md5Hex(raw); + } + + /** + * 更新工料机编码(同步到工作台工料机) + */ + private void updateResourceCode(Long projectId, String resourceKey, String newCode) { + // 1. 获取项目下所有单位工程 + List unitNodes = wbCompileTreeMapper.selectUnitNodesByProjectId(projectId); + if (CollUtil.isEmpty(unitNodes)) { + return; + } + + // 2. 解析原resourceKey获取原始信息 + ResourceKeyInfo keyInfo = parseResourceKey(projectId, resourceKey); + if (keyInfo == null) { + return; + } + + // 3. 遍历所有单位工程,更新匹配的工料机编码 + int updateCount = 0; + for (WbCompileTreeDO unit : unitNodes) { + List quotaNodes = wbBoqDivisionMapper.selectQuotaNodesByCompileTreeId(unit.getId()); + for (WbBoqDivisionDO quotaNode : quotaNodes) { + List resources = wbBoqResourceMapper.selectListByDivisionId(quotaNode.getId()); + for (WbBoqResourceDO resource : resources) { + if (matchResourceKey(resource, keyInfo)) { + resource.setCode(newCode); + wbBoqResourceMapper.updateById(resource); + updateCount++; + } + } + } + } + + // 4. 更新汇总表的resourceKey + if (updateCount > 0) { + String newResourceKey = generateResourceKey(newCode, keyInfo.getName(), + keyInfo.getSpec(), keyInfo.getUnit()); + WbResourceSummaryDO summary = wbResourceSummaryMapper.selectByProjectIdAndResourceKey(projectId, resourceKey); + if (summary != null) { + summary.setResourceKey(newResourceKey); + wbResourceSummaryMapper.updateById(summary); + } + } + + log.info("[updateResourceCode] 更新工料机编码, projectId={}, oldKey={}, newCode={}, updateCount={}", + projectId, resourceKey, newCode, updateCount); + } + + /** + * 解析resourceKey获取原始信息 + */ + private ResourceKeyInfo parseResourceKey(Long projectId, String resourceKey) { + // 通过遍历工料机找到匹配的记录 + List unitNodes = wbCompileTreeMapper.selectUnitNodesByProjectId(projectId); + for (WbCompileTreeDO unit : unitNodes) { + ResourceKeyInfo info = parseResourceKeyFromDb(unit.getId(), resourceKey); + if (info != null) { + return info; + } + } + return null; + } + + /** + * 从数据库解析resourceKey + */ + private ResourceKeyInfo parseResourceKeyFromDb(Long compileTreeId, String resourceKey) { + List quotaNodes = wbBoqDivisionMapper.selectQuotaNodesByCompileTreeId(compileTreeId); + for (WbBoqDivisionDO quotaNode : quotaNodes) { + List resources = wbBoqResourceMapper.selectListByDivisionId(quotaNode.getId()); + for (WbBoqResourceDO resource : resources) { + String key = generateResourceKey(resource.getCode(), resource.getName(), + resource.getSpec(), resource.getUnit()); + if (key.equals(resourceKey)) { + ResourceKeyInfo info = new ResourceKeyInfo(); + info.setCode(resource.getCode()); + info.setName(resource.getName()); + info.setSpec(resource.getSpec()); + info.setUnit(resource.getUnit()); + return info; + } + } + } + return null; + } + + /** + * 统计单位工程下该工料机的数量 + */ + private int countResourceInUnit(Long compileTreeId, ResourceKeyInfo keyInfo) { + int count = 0; + List quotaNodes = wbBoqDivisionMapper.selectQuotaNodesByCompileTreeId(compileTreeId); + for (WbBoqDivisionDO quotaNode : quotaNodes) { + List resources = wbBoqResourceMapper.selectListByDivisionId(quotaNode.getId()); + for (WbBoqResourceDO resource : resources) { + if (matchResourceKey(resource, keyInfo)) { + count++; + } + } + } + return count; + } + + /** + * 匹配工料机是否符合resourceKey + */ + private boolean matchResourceKey(WbBoqResourceDO resource, ResourceKeyInfo keyInfo) { + return StrUtil.equals(resource.getCode(), keyInfo.getCode()) && + StrUtil.equals(resource.getName(), keyInfo.getName()) && + StrUtil.equals(resource.getSpec(), keyInfo.getSpec()) && + StrUtil.equals(resource.getUnit(), keyInfo.getUnit()); + } + + /** + * 查找父清单节点 + */ + private WbBoqDivisionDO findParentBoqNode(WbBoqDivisionDO quotaNode) { + if (quotaNode.getParentId() == null) { + return null; + } + WbBoqDivisionDO parent = wbBoqDivisionMapper.selectById(quotaNode.getParentId()); + if (parent == null) { + return null; + } + if (WbBoqDivisionDO.NODE_TYPE_BOQ.equals(parent.getNodeType())) { + return parent; + } + // 递归查找 + return findParentBoqNode(parent); + } + + /** + * 从节点的 attributes.tabTypes 解析标签页类型 + * 无 tabTypes 或为空数组则默认返回 "division" + */ + @SuppressWarnings("unchecked") + private String resolveTabType(WbBoqDivisionDO node) { + java.util.Map attrs = node.getAttributes(); + if (attrs != null) { + Object tabTypesObj = attrs.get("tabTypes"); + if (tabTypesObj instanceof java.util.List) { + java.util.List tabTypes = (java.util.List) tabTypesObj; + if (!tabTypes.isEmpty()) { + return String.valueOf(tabTypes.get(0)); + } + } + } + return "division"; + } + + // ========== 内部类 ========== + + /** + * 资源汇总数据 + */ + @lombok.Data + private static class ResourceAggregation { + private String resourceKey; + private String code; + private String name; + private String spec; + private String unit; + private String resourceType; + private BigDecimal taxRate; + private BigDecimal totalQty; + private BigDecimal taxExclBasePrice; + private BigDecimal taxInclBasePrice; + private BigDecimal taxExclCompilePrice; + private BigDecimal taxInclCompilePrice; + } + + /** + * 资源唯一键信息 + */ + @lombok.Data + private static class ResourceKeyInfo { + private String code; + private String name; + private String spec; + private String unit; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbSnapshotReadServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbSnapshotReadServiceImpl.java new file mode 100644 index 0000000..15e4fe6 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbSnapshotReadServiceImpl.java @@ -0,0 +1,254 @@ +package com.yhy.module.core.service.workbench.impl; + +import cn.hutool.core.collection.CollUtil; +import com.yhy.module.core.dal.dataobject.quota.QuotaFeeItemDO; +import com.yhy.module.core.dal.dataobject.quota.QuotaRateItemDO; +import com.yhy.module.core.dal.dataobject.workbench.WbCategoryTreeDO; +import com.yhy.module.core.dal.dataobject.workbench.WbCategoryTreeMappingDO; +import com.yhy.module.core.dal.dataobject.workbench.WbFeeItemDO; +import com.yhy.module.core.dal.dataobject.workbench.WbRateFieldLabelDO; +import com.yhy.module.core.dal.dataobject.workbench.WbRateItemDO; +import com.yhy.module.core.dal.dataobject.workbench.WbUnifiedFeeResourceDO; +import com.yhy.module.core.dal.dataobject.workbench.WbUnifiedFeeSettingDO; +import com.yhy.module.core.dal.mysql.workbench.WbCategoryTreeMapper; +import com.yhy.module.core.dal.mysql.workbench.WbCategoryTreeMappingMapper; +import com.yhy.module.core.dal.mysql.workbench.WbFeeItemMapper; +import com.yhy.module.core.dal.mysql.workbench.WbRateFieldLabelMapper; +import com.yhy.module.core.dal.mysql.workbench.WbRateItemMapper; +import com.yhy.module.core.dal.mysql.workbench.WbUnifiedFeeResourceMapper; +import com.yhy.module.core.dal.mysql.workbench.WbUnifiedFeeSettingMapper; +import com.yhy.module.core.service.quota.QuotaFeeItemService; +import com.yhy.module.core.service.workbench.WbSnapshotReadService; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +/** + * 工作台快照读取服务实现 + * + * @author yihuiyong + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WbSnapshotReadServiceImpl implements WbSnapshotReadService { + + // 快照Mapper + private final WbRateItemMapper wbRateItemMapper; + private final WbFeeItemMapper wbFeeItemMapper; + private final WbUnifiedFeeSettingMapper wbUnifiedFeeSettingMapper; + private final WbUnifiedFeeResourceMapper wbUnifiedFeeResourceMapper; + private final WbCategoryTreeMapper wbCategoryTreeMapper; + private final WbCategoryTreeMappingMapper wbCategoryTreeMappingMapper; + private final WbRateFieldLabelMapper wbRateFieldLabelMapper; + + // 后台Service(用于复用树构建逻辑) + @Lazy + private final QuotaFeeItemService quotaFeeItemService; + + @Override + public List getRateItems(Long compileTreeId, Long rateModeId) { + // 只从快照表读取,不回退到后台标准库 + // 按 rateModeId(catalogItemId)过滤,避免多定额专业数据混合返回 + List snapshotItems; + if (rateModeId != null) { + snapshotItems = wbRateItemMapper.selectByCompileTreeIdAndCatalogItemId(compileTreeId, rateModeId); + } else { + snapshotItems = wbRateItemMapper.selectByCompileTreeId(compileTreeId); + } + if (CollUtil.isNotEmpty(snapshotItems)) { + log.debug("[getRateItems] 从快照表读取费率模板, compileTreeId={}, rateModeId={}, count={}", compileTreeId, rateModeId, snapshotItems.size()); + return snapshotItems; + } + + log.warn("[getRateItems] 快照不存在, compileTreeId={}, rateModeId={}", compileTreeId, rateModeId); + return new ArrayList<>(); + } + + @Override + public List getFeeItems(Long compileTreeId, Long rateModeId) { + // 只从快照表读取,不回退到后台标准库 + // 按 rateModeId(catalogItemId)过滤,避免多定额专业数据混合返回 + List snapshotItems; + if (rateModeId != null) { + snapshotItems = wbFeeItemMapper.selectByCompileTreeIdAndCatalogItemId(compileTreeId, rateModeId); + } else { + snapshotItems = wbFeeItemMapper.selectByCompileTreeId(compileTreeId); + } + if (CollUtil.isNotEmpty(snapshotItems)) { + log.debug("[getFeeItems] 从快照表读取取费模板, compileTreeId={}, rateModeId={}, count={}", compileTreeId, rateModeId, snapshotItems.size()); + return snapshotItems; + } + + log.warn("[getFeeItems] 快照不存在, compileTreeId={}, rateModeId={}", compileTreeId, rateModeId); + return new ArrayList<>(); + } + + @Override + public List getUnifiedFeeSettings(Long compileTreeId, Long rateModeId) { + // 只从快照表读取,不回退到后台标准库 + // 使用 catalogItemId(即 rateModeId)过滤,确保切换定额专业时返回对应费率模式的数据 + List snapshotItems; + if (rateModeId != null) { + snapshotItems = wbUnifiedFeeSettingMapper.selectByCompileTreeIdAndCatalogItemId(compileTreeId, rateModeId); + } else { + snapshotItems = wbUnifiedFeeSettingMapper.selectByCompileTreeId(compileTreeId); + } + if (CollUtil.isNotEmpty(snapshotItems)) { + log.debug("[getUnifiedFeeSettings] 从快照表读取统一取费设置, compileTreeId={}, rateModeId={}, count={}", compileTreeId, rateModeId, snapshotItems.size()); + return snapshotItems; + } + + log.warn("[getUnifiedFeeSettings] 快照不存在, compileTreeId={}, rateModeId={}", compileTreeId, rateModeId); + return new ArrayList<>(); + } + + @Override + public List getUnifiedFeeResources(Long compileTreeId, Long unifiedFeeSettingId) { + // 从快照表读取 + return wbUnifiedFeeResourceMapper.selectByUnifiedFeeSettingId(unifiedFeeSettingId); + } + + @Override + public List getCategoryTree(Long compileTreeId, Long quotaCatalogItemId) { + // 只从快照表读取,不回退到后台标准库 + List snapshotItems = wbCategoryTreeMapper.selectByCompileTreeId(compileTreeId); + if (CollUtil.isNotEmpty(snapshotItems)) { + log.debug("[getCategoryTree] 从快照表读取机类树, compileTreeId={}, count={}", compileTreeId, snapshotItems.size()); + return snapshotItems; + } + + log.warn("[getCategoryTree] 快照不存在, compileTreeId={}", compileTreeId); + return new ArrayList<>(); + } + + @Override + public List getCategoryTreeMappings(Long compileTreeId, Long categoryTreeId) { + // 从快照表读取 + return wbCategoryTreeMappingMapper.selectByCategoryTreeId(categoryTreeId); + } + + @Override + public boolean hasSnapshot(Long compileTreeId) { + // 检查是否有费率模板快照(作为快照存在的标志) + List items = wbRateItemMapper.selectByCompileTreeId(compileTreeId); + return CollUtil.isNotEmpty(items); + } + + @Override + public WbRateItemDO getRateItemById(Long compileTreeId, Long snapshotRateItemId) { + return wbRateItemMapper.selectById(snapshotRateItemId); + } + + // ==================== 转换方法 ==================== + + // ==================== 快照到Quota的转换方法(用于复用后台标准库逻辑) ==================== + + @Override + public List getFeeItemWithRateTree(Long compileTreeId, Long rateModeId) { + // 1. 检查是否有快照 + if (!hasSnapshot(compileTreeId)) { + log.debug("[getFeeItemWithRateTree] 快照不存在,返回null让调用方使用后台标准库, compileTreeId={}", compileTreeId); + return null; + } + + // 2. 从快照表读取费率项和取费项(使用 rateModeId 过滤) + List allRateItems; + List wbFeeItems; + + if (rateModeId != null) { + // 按费率模式ID过滤 + allRateItems = wbRateItemMapper.selectByCompileTreeIdAndCatalogItemId(compileTreeId, rateModeId); + wbFeeItems = wbFeeItemMapper.selectByCompileTreeIdAndCatalogItemId(compileTreeId, rateModeId); + log.debug("[getFeeItemWithRateTree] 使用rateModeId过滤, compileTreeId={}, rateModeId={}", compileTreeId, rateModeId); + } else { + // 不过滤,返回所有数据 + allRateItems = wbRateItemMapper.selectByCompileTreeId(compileTreeId); + wbFeeItems = wbFeeItemMapper.selectByCompileTreeId(compileTreeId); + log.debug("[getFeeItemWithRateTree] 未指定rateModeId,返回所有数据, compileTreeId={}", compileTreeId); + } + + if (CollUtil.isEmpty(allRateItems)) { + log.debug("[getFeeItemWithRateTree] 快照费率项为空, compileTreeId={}, rateModeId={}", compileTreeId, rateModeId); + return null; + } + + // 3. 过滤只保留目录节点(与后台标准库API保持一致) + List wbRateItems = allRateItems.stream() + .filter(r -> "directory".equals(r.getNodeType())) + .collect(Collectors.toList()); + + // 4. 转换为QuotaRateItemDO和QuotaFeeItemDO,复用后台标准库的逻辑 + List rateItems = wbRateItems.stream() + .map(this::convertToQuotaRateItemDO) + .collect(Collectors.toList()); + + List feeItems = wbFeeItems.stream() + .map(this::convertToQuotaFeeItemDO) + .collect(Collectors.toList()); + + // 5. 调用公共方法构建结果(复用后台标准库的逻辑) + log.debug("[getFeeItemWithRateTree] 从快照构建取费项树, compileTreeId={}, rateModeId={}, rateItemCount={}, feeItemCount={}", + compileTreeId, rateModeId, rateItems.size(), feeItems.size()); + return quotaFeeItemService.buildFeeItemWithRateList(rateItems, feeItems, rateModeId); + } + + /** + * 将WbRateItemDO转换为QuotaRateItemDO + */ + private QuotaRateItemDO convertToQuotaRateItemDO(WbRateItemDO wb) { + QuotaRateItemDO quota = new QuotaRateItemDO(); + quota.setId(wb.getId()); + quota.setCatalogItemId(wb.getCatalogItemId()); + quota.setParentId(wb.getParentId()); + quota.setCustomCode(wb.getCustomCode()); + quota.setName(wb.getName()); + quota.setRateCode(wb.getRateCode()); + quota.setIsEditable(wb.getIsEditable()); + quota.setDefaultValue(wb.getDefaultValue()); + quota.setValueMode(wb.getValueMode()); + quota.setSettings(wb.getSettings()); + quota.setNodeType(wb.getNodeType()); + quota.setLevel(wb.getLevel()); + quota.setSortOrder(wb.getSortOrder()); + return quota; + } + + /** + * 将WbFeeItemDO转换为QuotaFeeItemDO + */ + private QuotaFeeItemDO convertToQuotaFeeItemDO(WbFeeItemDO wb) { + QuotaFeeItemDO quota = new QuotaFeeItemDO(); + quota.setId(wb.getId()); + quota.setCatalogItemId(wb.getCatalogItemId()); + quota.setRateItemId(wb.getRateItemId()); + quota.setName(wb.getName()); + quota.setCustomCode(wb.getCustomCode()); + quota.setCalcBase(wb.getCalcBase()); + quota.setCode(wb.getCode()); + quota.setFeeCategory(wb.getFeeCategory()); + quota.setBaseDescription(wb.getBaseDescription()); + quota.setSortOrder(wb.getSortOrder()); + quota.setHidden(wb.getHidden()); + quota.setVariable(wb.getVariable()); + quota.setSystemCode(wb.getSystemCode()); + return quota; + } + + @Override + public List getRateFieldLabels(Long compileTreeId, Long rateModeId) { + if (compileTreeId == null || rateModeId == null) { + return null; + } + + List labels = wbRateFieldLabelMapper.selectByCompileTreeIdAndCatalogItemId(compileTreeId, rateModeId); + if (CollUtil.isEmpty(labels)) { + return null; + } + return labels; + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbSnapshotServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbSnapshotServiceImpl.java new file mode 100644 index 0000000..27f01a6 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbSnapshotServiceImpl.java @@ -0,0 +1,398 @@ +package com.yhy.module.core.service.workbench.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.IdUtil; +import com.yhy.module.core.dal.dataobject.quota.QuotaFeeItemDO; +import com.yhy.module.core.dal.dataobject.quota.QuotaRateFieldLabelDO; +import com.yhy.module.core.dal.dataobject.quota.QuotaRateItemDO; +import com.yhy.module.core.dal.dataobject.quota.QuotaUnifiedFeeResourceDO; +import com.yhy.module.core.dal.dataobject.quota.QuotaUnifiedFeeSettingDO; +import com.yhy.module.core.dal.dataobject.resource.ResourceCategoryTreeDO; +import com.yhy.module.core.dal.dataobject.resource.ResourceCategoryTreeMappingDO; +import com.yhy.module.core.dal.dataobject.workbench.*; +import com.yhy.module.core.dal.mysql.quota.QuotaFeeItemMapper; +import com.yhy.module.core.dal.mysql.quota.QuotaRateFieldLabelMapper; +import com.yhy.module.core.dal.mysql.quota.QuotaRateItemMapper; +import com.yhy.module.core.dal.mysql.quota.QuotaUnifiedFeeResourceMapper; +import com.yhy.module.core.dal.mysql.quota.QuotaUnifiedFeeSettingMapper; +import com.yhy.module.core.dal.mysql.resource.ResourceCategoryTreeMapper; +import com.yhy.module.core.dal.mysql.resource.ResourceCategoryTreeMappingMapper; +import com.yhy.module.core.dal.mysql.workbench.*; +import com.yhy.module.core.service.workbench.WbSnapshotService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 工作台快照服务实现 + * + * @author yihuiyong + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WbSnapshotServiceImpl implements WbSnapshotService { + + // 后台Mapper + private final ResourceCategoryTreeMapper resourceCategoryTreeMapper; + private final ResourceCategoryTreeMappingMapper resourceCategoryTreeMappingMapper; + private final QuotaRateItemMapper quotaRateItemMapper; + private final QuotaFeeItemMapper quotaFeeItemMapper; + private final QuotaUnifiedFeeSettingMapper quotaUnifiedFeeSettingMapper; + private final QuotaUnifiedFeeResourceMapper quotaUnifiedFeeResourceMapper; + private final QuotaRateFieldLabelMapper quotaRateFieldLabelMapper; + + // 工作台快照Mapper + private final WbCategoryTreeMapper wbCategoryTreeMapper; + private final WbCategoryTreeMappingMapper wbCategoryTreeMappingMapper; + private final WbRateItemMapper wbRateItemMapper; + private final WbFeeItemMapper wbFeeItemMapper; + private final WbUnifiedFeeSettingMapper wbUnifiedFeeSettingMapper; + private final WbUnifiedFeeResourceMapper wbUnifiedFeeResourceMapper; + private final WbRateFieldLabelMapper wbRateFieldLabelMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public void createSnapshot(Long compileTreeId, Long quotaCatalogItemId, Long rateModeId) { + log.info("[createSnapshot] 开始创建快照, compileTreeId={}, quotaCatalogItemId={}, rateModeId={}", + compileTreeId, quotaCatalogItemId, rateModeId); + + // 1. 复制机类树 + Map categoryTreeIdMapping = copyCategoryTree(compileTreeId, quotaCatalogItemId); + log.info("[createSnapshot] 机类树复制完成, 数量={}", categoryTreeIdMapping.size()); + + // 2. 复制机类映射 + copyCategoryTreeMapping(compileTreeId, categoryTreeIdMapping); + log.info("[createSnapshot] 机类映射复制完成"); + + // 3. 复制费率模板 + Map rateItemIdMapping = copyRateItems(compileTreeId, rateModeId); + log.info("[createSnapshot] 费率模板复制完成, 数量={}", rateItemIdMapping.size()); + + // 4. 复制取费模板 + copyFeeItems(compileTreeId, rateModeId, rateItemIdMapping); + log.info("[createSnapshot] 取费模板复制完成"); + + // 5. 复制统一取费设置 + Map unifiedFeeSettingIdMapping = copyUnifiedFeeSettings(compileTreeId, rateModeId); + log.info("[createSnapshot] 统一取费设置复制完成, 数量={}", unifiedFeeSettingIdMapping.size()); + + // 6. 复制统一取费子目工料机 + copyUnifiedFeeResources(compileTreeId, unifiedFeeSettingIdMapping); + log.info("[createSnapshot] 统一取费子目工料机复制完成"); + + // 7. 复制费率字段标签 + copyRateFieldLabels(compileTreeId, rateModeId); + log.info("[createSnapshot] 费率字段标签复制完成"); + + log.info("[createSnapshot] 快照创建完成, compileTreeId={}", compileTreeId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteSnapshot(Long compileTreeId) { + log.info("[deleteSnapshot] 开始删除快照, compileTreeId={}", compileTreeId); + + wbUnifiedFeeResourceMapper.deleteByCompileTreeId(compileTreeId); + wbUnifiedFeeSettingMapper.deleteByCompileTreeId(compileTreeId); + wbFeeItemMapper.deleteByCompileTreeId(compileTreeId); + wbRateItemMapper.deleteByCompileTreeId(compileTreeId); + wbRateFieldLabelMapper.deleteByCompileTreeId(compileTreeId); + wbCategoryTreeMappingMapper.deleteByCompileTreeId(compileTreeId); + wbCategoryTreeMapper.deleteByCompileTreeId(compileTreeId); + + log.info("[deleteSnapshot] 快照删除完成, compileTreeId={}", compileTreeId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void switchRateModeSnapshot(Long compileTreeId, Long newRateModeId) { + log.info("[switchRateModeSnapshot] 切换费率模式快照, compileTreeId={}, newRateModeId={}", + compileTreeId, newRateModeId); + + // 删除指定费率模式的快照(按 catalogItemId 过滤,保留其他费率模式的快照) + int deletedRateFieldLabels = wbRateFieldLabelMapper.deleteByCompileTreeIdAndCatalogItemId(compileTreeId, newRateModeId); + int deletedRateItems = wbRateItemMapper.deleteByCompileTreeIdAndCatalogItemId(compileTreeId, newRateModeId); + int deletedFeeItems = wbFeeItemMapper.deleteByCompileTreeIdAndCatalogItemId(compileTreeId, newRateModeId); + + // 删除统一取费设置及其关联的工料机(级联删除) + List oldSettings = wbUnifiedFeeSettingMapper.selectByCompileTreeIdAndCatalogItemId(compileTreeId, newRateModeId); + int deletedUnifiedFeeSettings = wbUnifiedFeeSettingMapper.deleteByCompileTreeIdAndCatalogItemId(compileTreeId, newRateModeId); + + // 删除关联的工料机(通过 unifiedFeeSettingId) + int deletedUnifiedFeeResources = 0; + for (WbUnifiedFeeSettingDO setting : oldSettings) { + List resources = wbUnifiedFeeResourceMapper.selectByUnifiedFeeSettingId(setting.getId()); + for (WbUnifiedFeeResourceDO resource : resources) { + wbUnifiedFeeResourceMapper.deleteById(resource.getId()); + deletedUnifiedFeeResources++; + } + } + + log.info("[switchRateModeSnapshot] 删除旧快照完成, rateModeId={}, deletedRateFieldLabels={}, deletedRateItems={}, deletedFeeItems={}, deletedUnifiedFeeSettings={}, deletedUnifiedFeeResources={}", + newRateModeId, deletedRateFieldLabels, deletedRateItems, deletedFeeItems, deletedUnifiedFeeSettings, deletedUnifiedFeeResources); + + // 复制新费率模式的数据 + Map rateItemIdMapping = copyRateItems(compileTreeId, newRateModeId); + copyFeeItems(compileTreeId, newRateModeId, rateItemIdMapping); + Map unifiedFeeSettingIdMapping = copyUnifiedFeeSettings(compileTreeId, newRateModeId); + copyUnifiedFeeResources(compileTreeId, unifiedFeeSettingIdMapping); + copyRateFieldLabels(compileTreeId, newRateModeId); + + log.info("[switchRateModeSnapshot] 费率模式快照切换完成"); + } + + /** + * 复制机类树 + * + * @return 旧ID → 新ID 映射 + */ + private Map copyCategoryTree(Long compileTreeId, Long quotaCatalogItemId) { + Map idMapping = new HashMap<>(); + + // 查询后台机类树(根据定额专业关联的catalogId) + List sourceList = resourceCategoryTreeMapper.selectByCatalogId(quotaCatalogItemId); + if (CollUtil.isEmpty(sourceList)) { + log.warn("[copyCategoryTree] 未找到机类树数据, quotaCatalogItemId={}", quotaCatalogItemId); + return idMapping; + } + + for (ResourceCategoryTreeDO source : sourceList) { + WbCategoryTreeDO target = new WbCategoryTreeDO(); + target.setId(IdUtil.getSnowflakeNextId()); + target.setCompileTreeId(compileTreeId); + target.setSourceId(source.getId()); + target.setCatalogId(source.getCatalogId()); + target.setParentId(source.getParentId() != null ? idMapping.get(source.getParentId()) : null); + target.setCode(source.getCode()); + target.setName(source.getName()); + target.setNodeType(source.getNodeType()); + target.setPath(source.getPath()); + target.setSortOrder(source.getSortOrder()); + target.setAttributes(source.getAttributes()); + + wbCategoryTreeMapper.insert(target); + idMapping.put(source.getId(), target.getId()); + } + + return idMapping; + } + + /** + * 复制机类映射 + */ + private void copyCategoryTreeMapping(Long compileTreeId, Map categoryTreeIdMapping) { + if (categoryTreeIdMapping.isEmpty()) { + return; + } + + for (Map.Entry entry : categoryTreeIdMapping.entrySet()) { + Long sourceCategoryTreeId = entry.getKey(); + Long targetCategoryTreeId = entry.getValue(); + + List mappings = + resourceCategoryTreeMappingMapper.selectByCategoryTreeId(sourceCategoryTreeId); + + for (ResourceCategoryTreeMappingDO source : mappings) { + WbCategoryTreeMappingDO target = new WbCategoryTreeMappingDO(); + target.setId(IdUtil.getSnowflakeNextId()); + target.setCompileTreeId(compileTreeId); + target.setSourceId(source.getId()); + target.setCategoryTreeId(targetCategoryTreeId); + target.setCategoryId(source.getCategoryId()); + target.setSortOrder(source.getSortOrder()); + + wbCategoryTreeMappingMapper.insert(target); + } + } + } + + /** + * 复制费率模板 + * + * @return 旧ID → 新ID 映射 + */ + private Map copyRateItems(Long compileTreeId, Long rateModeId) { + Map idMapping = new HashMap<>(); + + List sourceList = quotaRateItemMapper.selectListByCatalogItemId(rateModeId); + if (CollUtil.isEmpty(sourceList)) { + log.warn("[copyRateItems] 未找到费率模板数据, rateModeId={}", rateModeId); + return idMapping; + } + + // 按层级排序,确保父节点先插入 + sourceList.sort((a, b) -> { + int levelA = a.getLevel() != null ? a.getLevel() : 0; + int levelB = b.getLevel() != null ? b.getLevel() : 0; + return Integer.compare(levelA, levelB); + }); + + for (QuotaRateItemDO source : sourceList) { + WbRateItemDO target = new WbRateItemDO(); + target.setId(IdUtil.getSnowflakeNextId()); + target.setCompileTreeId(compileTreeId); + target.setSourceId(source.getId()); + target.setCatalogItemId(source.getCatalogItemId()); + target.setParentId(source.getParentId() != null ? idMapping.get(source.getParentId()) : null); + target.setCustomCode(source.getCustomCode()); + target.setName(source.getName()); + target.setRateCode(source.getRateCode()); + target.setIsEditable(source.getIsEditable()); + target.setDefaultValue(source.getDefaultValue()); + target.setValueMode(source.getValueMode()); + target.setSettings(source.getSettings()); + target.setNodeType(source.getNodeType()); + target.setLevel(source.getLevel()); + target.setSortOrder(source.getSortOrder()); + + wbRateItemMapper.insert(target); + idMapping.put(source.getId(), target.getId()); + } + + return idMapping; + } + + /** + * 复制取费模板 + */ + private void copyFeeItems(Long compileTreeId, Long rateModeId, Map rateItemIdMapping) { + List sourceList = quotaFeeItemMapper.selectListByCatalogItemId(rateModeId); + if (CollUtil.isEmpty(sourceList)) { + log.warn("[copyFeeItems] 未找到取费模板数据, rateModeId={}", rateModeId); + return; + } + + for (QuotaFeeItemDO source : sourceList) { + WbFeeItemDO target = new WbFeeItemDO(); + target.setId(IdUtil.getSnowflakeNextId()); + target.setCompileTreeId(compileTreeId); + target.setSourceId(source.getId()); + target.setCatalogItemId(source.getCatalogItemId()); + // 映射rateItemId到新ID + target.setRateItemId(source.getRateItemId() != null ? + rateItemIdMapping.getOrDefault(source.getRateItemId(), source.getRateItemId()) : null); + target.setCustomCode(source.getCustomCode()); + target.setName(source.getName()); + target.setCalcBase(source.getCalcBase()); + target.setCode(source.getCode()); + target.setFeeCategory(source.getFeeCategory()); + target.setBaseDescription(source.getBaseDescription()); + target.setSortOrder(source.getSortOrder()); + target.setHidden(source.getHidden()); + target.setVariable(source.getVariable()); + target.setSystemCode(source.getSystemCode()); + + wbFeeItemMapper.insert(target); + } + } + + /** + * 复制统一取费设置 + * + * @return 旧ID → 新ID 映射 + */ + private Map copyUnifiedFeeSettings(Long compileTreeId, Long rateModeId) { + Map idMapping = new HashMap<>(); + + List sourceList = quotaUnifiedFeeSettingMapper.selectListByCatalogItemId(rateModeId); + if (CollUtil.isEmpty(sourceList)) { + log.warn("[copyUnifiedFeeSettings] 未找到统一取费设置数据, rateModeId={}", rateModeId); + return idMapping; + } + + // 按parentId排序,确保父节点先插入(null在前) + sourceList.sort((a, b) -> { + if (a.getParentId() == null && b.getParentId() == null) return 0; + if (a.getParentId() == null) return -1; + if (b.getParentId() == null) return 1; + return Long.compare(a.getParentId(), b.getParentId()); + }); + + for (QuotaUnifiedFeeSettingDO source : sourceList) { + WbUnifiedFeeSettingDO target = new WbUnifiedFeeSettingDO(); + target.setId(IdUtil.getSnowflakeNextId()); + target.setCompileTreeId(compileTreeId); + target.setSourceId(source.getId()); + target.setCatalogItemId(source.getCatalogItemId()); + target.setParentId(source.getParentId() != null ? idMapping.get(source.getParentId()) : null); + target.setCustomCode(source.getCustomCode()); + target.setCode(source.getCode()); + target.setName(source.getName()); + target.setFeeChapter(source.getFeeChapter()); + target.setFeeCategory(source.getFeeCategory()); + target.setThisListPercentage(source.getThisListPercentage()); + target.setSpecifiedListPercentage(source.getSpecifiedListPercentage()); + target.setSpecifiedListCode(source.getSpecifiedListCode()); + target.setNodeType(source.getNodeType()); + target.setUnit(source.getUnit()); + target.setAttributes(source.getAttributes()); + target.setSortOrder(source.getSortOrder()); + + wbUnifiedFeeSettingMapper.insert(target); + idMapping.put(source.getId(), target.getId()); + } + + return idMapping; + } + + /** + * 复制统一取费子目工料机 + */ + private void copyUnifiedFeeResources(Long compileTreeId, Map unifiedFeeSettingIdMapping) { + if (unifiedFeeSettingIdMapping.isEmpty()) { + return; + } + + for (Map.Entry entry : unifiedFeeSettingIdMapping.entrySet()) { + Long sourceSettingId = entry.getKey(); + Long targetSettingId = entry.getValue(); + + List resources = + quotaUnifiedFeeResourceMapper.selectListByUnifiedFeeSettingId(sourceSettingId); + + for (QuotaUnifiedFeeResourceDO source : resources) { + WbUnifiedFeeResourceDO target = new WbUnifiedFeeResourceDO(); + target.setId(IdUtil.getSnowflakeNextId()); + target.setCompileTreeId(compileTreeId); + target.setSourceId(source.getId()); + target.setUnifiedFeeSettingId(targetSettingId); + target.setResourceItemId(source.getResourceItemId()); + target.setDosage(source.getDosage()); + target.setAdjustedDosage(source.getAdjustedDosage()); + target.setAttributes(source.getAttributes()); + target.setSortOrder(source.getSortOrder()); + + wbUnifiedFeeResourceMapper.insert(target); + } + } + } + + /** + * 复制费率字段标签 + */ + private void copyRateFieldLabels(Long compileTreeId, Long rateModeId) { + List sourceList = quotaRateFieldLabelMapper.selectListByCatalogItemId(rateModeId); + if (CollUtil.isEmpty(sourceList)) { + log.warn("[copyRateFieldLabels] 未找到费率字段标签数据, rateModeId={}", rateModeId); + return; + } + + for (QuotaRateFieldLabelDO source : sourceList) { + WbRateFieldLabelDO target = new WbRateFieldLabelDO(); + target.setId(IdUtil.getSnowflakeNextId()); + target.setCompileTreeId(compileTreeId); + target.setSourceId(source.getId()); + target.setCatalogItemId(source.getCatalogItemId()); + target.setLabelName(source.getLabelName()); + target.setSortOrder(source.getSortOrder()); + + wbRateFieldLabelMapper.insert(target); + } + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbUnifiedFeeConfigServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbUnifiedFeeConfigServiceImpl.java new file mode 100644 index 0000000..6a7a48b --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbUnifiedFeeConfigServiceImpl.java @@ -0,0 +1,653 @@ +package com.yhy.module.core.service.workbench.impl; + +import com.yhy.module.core.controller.admin.workbench.vo.WbUnifiedFeeSummaryItemVO; +import com.yhy.module.core.dal.dataobject.quota.QuotaUnifiedFeeSettingDO; +import com.yhy.module.core.dal.dataobject.workbench.WbBoqDivisionDO; +import com.yhy.module.core.dal.dataobject.workbench.WbUnifiedFeeConfigDO; +import com.yhy.module.core.dal.dataobject.workbench.WbUnitInfoDO; +import com.yhy.module.core.dal.mysql.quota.QuotaUnifiedFeeSettingMapper; +import com.yhy.module.core.dal.mysql.workbench.WbBoqDivisionMapper; +import com.yhy.module.core.dal.mysql.workbench.WbUnifiedFeeConfigMapper; +import com.yhy.module.core.dal.mysql.workbench.WbUnitInfoMapper; +import com.yhy.module.core.service.workbench.WbUnifiedFeeConfigService; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * 工作台统一取费配置 Service 实现类 + */ +@Slf4j +@Service +public class WbUnifiedFeeConfigServiceImpl implements WbUnifiedFeeConfigService { + + @Resource + private WbUnifiedFeeConfigMapper wbUnifiedFeeConfigMapper; + + @Resource + private WbBoqDivisionMapper wbBoqDivisionMapper; + + @Resource + private WbUnitInfoMapper wbUnitInfoMapper; + + @Resource + private QuotaUnifiedFeeSettingMapper quotaUnifiedFeeSettingMapper; + + @Resource + private com.yhy.module.core.dal.mysql.quota.QuotaItemMapper quotaItemMapper; + + @Resource + private com.yhy.module.core.service.workbench.WbSnapshotReadService wbSnapshotReadService; + + @Resource + private com.yhy.module.core.dal.mysql.workbench.WbUnifiedFeeSettingMapper wbUnifiedFeeSettingMapper; + + @Override + public List getListByCompileTreeId(Long compileTreeId) { + return wbUnifiedFeeConfigMapper.selectListByCompileTreeId(compileTreeId); + } + + @Override + public List getListByCompileTreeIdAndRateModeId(Long compileTreeId, Long rateModeId) { + return wbUnifiedFeeConfigMapper.selectListByCompileTreeIdAndRateModeId(compileTreeId, rateModeId); + } + + @Override + public WbUnifiedFeeConfigDO getBySourceUnifiedFeeSettingId(Long compileTreeId, Long sourceUnifiedFeeSettingId) { + return wbUnifiedFeeConfigMapper.selectOne(new LambdaQueryWrapperX() + .eq(WbUnifiedFeeConfigDO::getCompileTreeId, compileTreeId) + .eq(WbUnifiedFeeConfigDO::getSourceUnifiedFeeSettingId, sourceUnifiedFeeSettingId)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long saveOrUpdate(WbUnifiedFeeConfigDO config) { + if (config.getId() != null) { + // 合并configData而不是完全覆盖 + WbUnifiedFeeConfigDO existing = wbUnifiedFeeConfigMapper.selectById(config.getId()); + if (existing != null && existing.getConfigData() != null && config.getConfigData() != null) { + Map mergedConfigData = new HashMap<>(existing.getConfigData()); + mergedConfigData.putAll(config.getConfigData()); + config.setConfigData(mergedConfigData); + } + wbUnifiedFeeConfigMapper.updateById(config); + return config.getId(); + } else { + // 检查是否已存在相同的配置 + WbUnifiedFeeConfigDO existing = getBySourceUnifiedFeeSettingId( + config.getCompileTreeId(), config.getSourceUnifiedFeeSettingId()); + if (existing != null) { + // 合并configData而不是完全覆盖 + if (existing.getConfigData() != null && config.getConfigData() != null) { + Map mergedConfigData = new HashMap<>(existing.getConfigData()); + mergedConfigData.putAll(config.getConfigData()); + config.setConfigData(mergedConfigData); + } + config.setId(existing.getId()); + wbUnifiedFeeConfigMapper.updateById(config); + return existing.getId(); + } else { + wbUnifiedFeeConfigMapper.insert(config); + return config.getId(); + } + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void batchSave(Long compileTreeId, Long rateModeId, List configs) { + for (WbUnifiedFeeConfigDO config : configs) { + config.setCompileTreeId(compileTreeId); + config.setRateModeId(rateModeId); + saveOrUpdate(config); + } + } + + @Override + public void delete(Long id) { + wbUnifiedFeeConfigMapper.deleteById(id); + } + + @Override + public void deleteByRateModeId(Long compileTreeId, Long rateModeId) { + wbUnifiedFeeConfigMapper.deleteByRateModeId(compileTreeId, rateModeId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void applyUnifiedFee(Long compileTreeId) { + log.info("[应用统一取费] 开始 - compileTreeId={}", compileTreeId); + + // 1. 获取该单位工程的所有统一取费配置(支持多个定额专业/费率模式) + List configs = wbUnifiedFeeConfigMapper.selectList( + new LambdaQueryWrapperX() + .eq(WbUnifiedFeeConfigDO::getCompileTreeId, compileTreeId)); + if (configs.isEmpty()) { + log.info("[应用统一取费] 单位工程{}没有统一取费配置", compileTreeId); + return; + } + log.info("[应用统一取费] 找到{}条统一取费配置", configs.size()); + + // 3. 获取统一取费设置(父定额)信息 - 优先从快照表读取 + Set settingIds = configs.stream() + .map(WbUnifiedFeeConfigDO::getSourceUnifiedFeeSettingId) + .collect(Collectors.toSet()); + + // 3.1 尝试从快照表读取 + List snapshotSettings = + wbUnifiedFeeSettingMapper.selectByCompileTreeId(compileTreeId); + Map snapshotSettingMap = + snapshotSettings.stream() + .filter(s -> s.getSourceId() != null) + .collect(Collectors.toMap( + com.yhy.module.core.dal.dataobject.workbench.WbUnifiedFeeSettingDO::getSourceId, + s -> s, + (a, b) -> a)); + + // 3.2 只从快照表读取,不回退到后台标准库 + Map settingMap; + if (snapshotSettingMap.isEmpty()) { + log.warn("[应用统一取费] 快照不存在, compileTreeId={}", compileTreeId); + settingMap = new HashMap<>(); + } else { + log.info("[应用统一取费] 从快照表读取统一取费设置, 数量={}", snapshotSettingMap.size()); + // 将快照数据转换为后台数据格式(用于兼容后续逻辑) + settingMap = new HashMap<>(); + for (Long sourceId : settingIds) { + com.yhy.module.core.dal.dataobject.workbench.WbUnifiedFeeSettingDO snapshot = snapshotSettingMap.get(sourceId); + if (snapshot != null) { + QuotaUnifiedFeeSettingDO converted = new QuotaUnifiedFeeSettingDO(); + converted.setId(snapshot.getSourceId()); + converted.setCode(snapshot.getCode()); + converted.setName(snapshot.getName()); + converted.setFeeChapter(snapshot.getFeeChapter()); + converted.setFeeCategory(snapshot.getFeeCategory()); + converted.setThisListPercentage(snapshot.getThisListPercentage()); + converted.setSpecifiedListPercentage(snapshot.getSpecifiedListPercentage()); + converted.setSpecifiedListCode(snapshot.getSpecifiedListCode()); + converted.setNodeType(snapshot.getNodeType()); + converted.setUnit(snapshot.getUnit()); + settingMap.put(sourceId, converted); + } + } + } + + // 4. 获取已存在的统一取费目录节点(用于防止重复添加) + List existingUnifiedFeeNodes = wbBoqDivisionMapper.selectList(new LambdaQueryWrapperX() + .eq(WbBoqDivisionDO::getCompileTreeId, compileTreeId) + .eq(WbBoqDivisionDO::getNodeType, "unified_fee")); + // 构建已存在的映射:parentId + sourceUnifiedFeeSettingId -> 节点 + Map existingMap = new HashMap<>(); + for (WbBoqDivisionDO node : existingUnifiedFeeNodes) { + Map attrs = node.getAttributes(); + if (attrs != null && attrs.get("sourceUnifiedFeeSettingId") != null) { + String key = node.getParentId() + "_" + attrs.get("sourceUnifiedFeeSettingId"); + existingMap.put(key, node); + } + } + + // 5. 遍历配置,将统一取费目录添加到对应的清单下(与定额同级) + for (WbUnifiedFeeConfigDO config : configs) { + Map configData = config.getConfigData(); + if (configData == null) continue; + + // 优先使用quotaIds(定额节点ID),如果没有则使用divisionIds(兼容旧数据) + @SuppressWarnings("unchecked") + List quotaIdList = (List) configData.get("quotaIds"); + if (quotaIdList == null || quotaIdList.isEmpty()) { + // 兼容旧数据:使用divisionIds + @SuppressWarnings("unchecked") + List divisionIds = (List) configData.get("divisionIds"); + quotaIdList = divisionIds; + } + + QuotaUnifiedFeeSettingDO setting = settingMap.get(config.getSourceUnifiedFeeSettingId()); + if (setting == null) continue; + + // 如果quotaIds为空,说明用户清空了选择,需要删除对应的统一取费节点 + if (quotaIdList == null || quotaIdList.isEmpty()) { + log.info("[应用统一取费] 配置{}的quotaIds为空,删除对应的统一取费节点", config.getId()); + // 删除该配置对应的所有统一取费节点 + for (WbBoqDivisionDO existingNode : existingUnifiedFeeNodes) { + Map attrs = existingNode.getAttributes(); + if (attrs != null && config.getSourceUnifiedFeeSettingId().equals( + Long.valueOf(attrs.get("sourceUnifiedFeeSettingId").toString()))) { + wbBoqDivisionMapper.deleteById(existingNode.getId()); + log.info("[应用统一取费] 删除统一取费节点:{}", existingNode.getId()); + } + } + continue; + } + + // 找到所有选中定额的父节点(清单),按父节点分组 + // 统一取费节点应该插入到清单下,与定额同级 + Map> parentToQuotaIds = new HashMap<>(); + for (Object divisionIdObj : quotaIdList) { + Long quotaId = Long.valueOf(divisionIdObj.toString()); + WbBoqDivisionDO quotaNode = wbBoqDivisionMapper.selectById(quotaId); + if (quotaNode == null) { + log.warn("定额节点不存在:{}", quotaId); + continue; + } + // 只处理定额类型的节点 + if (!"quota".equals(quotaNode.getNodeType())) { + log.warn("节点不是定额类型,跳过:id={}, nodeType={}", quotaId, quotaNode.getNodeType()); + continue; + } + Long parentId = quotaNode.getParentId(); + if (parentId == null) { + log.warn("定额节点没有父节点:{}", quotaId); + continue; + } + parentToQuotaIds.computeIfAbsent(parentId, k -> new ArrayList<>()).add(quotaId); + } + + // 为每个清单(父节点)创建一个统一取费节点 + for (Map.Entry> entry : parentToQuotaIds.entrySet()) { + Long parentId = entry.getKey(); // 清单ID + List quotaIds = entry.getValue(); // 该清单下被选中的定额ID列表 + + // 获取父节点(清单) + WbBoqDivisionDO parentNode = wbBoqDivisionMapper.selectById(parentId); + if (parentNode == null) { + log.warn("清单节点不存在:{}", parentId); + continue; + } + + // 使用parentId构建key,与existingMap中的key保持一致 + String key = parentId + "_" + config.getSourceUnifiedFeeSettingId(); + + // 检查是否已存在 + WbBoqDivisionDO existingNode = existingMap.get(key); + if (existingNode != null) { + // 已存在,更新motherQuotaIds和其他配置 + Map existingAttrs = existingNode.getAttributes(); + if (existingAttrs == null) { + existingAttrs = new HashMap<>(); + } + existingAttrs.put("motherQuotaIds", quotaIds); + existingAttrs.put("feeCategory", configData.get("feeCategory") != null ? configData.get("feeCategory") : setting.getFeeCategory()); + existingAttrs.put("thisListPercentage", configData.get("thisListPercentage") != null ? configData.get("thisListPercentage") : setting.getThisListPercentage()); + existingAttrs.put("specifiedListPercentage", configData.get("specifiedListPercentage") != null ? configData.get("specifiedListPercentage") : setting.getSpecifiedListPercentage()); + existingAttrs.put("specifiedListCode", configData.get("specifiedListCode") != null ? configData.get("specifiedListCode") : setting.getSpecifiedListCode()); + existingNode.setAttributes(existingAttrs); + wbBoqDivisionMapper.updateById(existingNode); + log.info("更新统一取费目录:parentId(清单)={}, settingId={}, nodeId={}, 母定额数量={}", + parentId, config.getSourceUnifiedFeeSettingId(), existingNode.getId(), quotaIds.size()); + continue; + } + + // 标记为已处理,防止同一次请求中重复添加 + existingMap.put(key, null); + + // 获取清单下的最大排序号 + Integer maxSortOrder = wbBoqDivisionMapper.selectMaxSortOrderByParentId(compileTreeId, parentId); + int newSortOrder = (maxSortOrder != null ? maxSortOrder : 0) + 1; + + // 创建统一取费目录节点(作为清单的子节点,与定额同级) + WbBoqDivisionDO unifiedFeeNode = new WbBoqDivisionDO(); + unifiedFeeNode.setCompileTreeId(compileTreeId); + unifiedFeeNode.setParentId(parentId); + unifiedFeeNode.setNodeType("unified_fee"); + unifiedFeeNode.setSourceType("unified_fee"); + unifiedFeeNode.setCode(setting.getCode()); + unifiedFeeNode.setName(setting.getName()); + unifiedFeeNode.setSortOrder(newSortOrder); + // 统一取费节点的工程量默认为1 + unifiedFeeNode.setQty(java.math.BigDecimal.ONE); + + // 存储来源信息到attributes,包括选中的定额ID列表 + Map attributes = new HashMap<>(); + attributes.put("sourceUnifiedFeeSettingId", config.getSourceUnifiedFeeSettingId()); + attributes.put("rateModeId", config.getRateModeId()); + attributes.put("feeChapter", setting.getFeeChapter()); + attributes.put("feeCategory", configData.get("feeCategory") != null ? configData.get("feeCategory") : setting.getFeeCategory()); + attributes.put("thisListPercentage", configData.get("thisListPercentage") != null ? configData.get("thisListPercentage") : setting.getThisListPercentage()); + attributes.put("specifiedListPercentage", configData.get("specifiedListPercentage") != null ? configData.get("specifiedListPercentage") : setting.getSpecifiedListPercentage()); + attributes.put("specifiedListCode", configData.get("specifiedListCode") != null ? configData.get("specifiedListCode") : setting.getSpecifiedListCode()); + // 存储选中的母定额ID列表,用于计算汇总 + attributes.put("motherQuotaIds", quotaIds); + unifiedFeeNode.setAttributes(attributes); + + // 构建path(基于清单的path) + String[] parentPath = parentNode.getPath(); + String[] newPath = new String[(parentPath != null ? parentPath.length : 0) + 1]; + if (parentPath != null) { + System.arraycopy(parentPath, 0, newPath, 0, parentPath.length); + } + + wbBoqDivisionMapper.insert(unifiedFeeNode); + + // 更新path(包含自己的ID) + newPath[newPath.length - 1] = String.valueOf(unifiedFeeNode.getId()); + unifiedFeeNode.setPath(newPath); + wbBoqDivisionMapper.updateById(unifiedFeeNode); + + log.info("添加统一取费目录:parentId(清单)={}, settingId={}, nodeId={}, 母定额数量={}", + parentId, config.getSourceUnifiedFeeSettingId(), unifiedFeeNode.getId(), quotaIds.size()); + } + + // 6. 处理指定清单编码逻辑:在指定清单下也创建统一取费节点 + String specifiedListCode = configData.get("specifiedListCode") != null ? + configData.get("specifiedListCode").toString() : setting.getSpecifiedListCode(); + if (specifiedListCode != null && !specifiedListCode.isEmpty()) { + // 查找指定清单编码对应的清单节点 + List specifiedLists = wbBoqDivisionMapper.selectList( + new LambdaQueryWrapperX() + .eq(WbBoqDivisionDO::getCompileTreeId, compileTreeId) + .eq(WbBoqDivisionDO::getNodeType, "boq") + .eq(WbBoqDivisionDO::getCode, specifiedListCode)); + + for (WbBoqDivisionDO specifiedList : specifiedLists) { + Long specifiedParentId = specifiedList.getId(); + String specifiedKey = specifiedParentId + "_" + config.getSourceUnifiedFeeSettingId() + "_specified"; + + // 检查是否已存在 + if (existingMap.containsKey(specifiedKey)) { + continue; + } + + // 检查指定清单下是否已有该统一取费节点 + WbBoqDivisionDO existingSpecifiedNode = null; + for (WbBoqDivisionDO node : existingUnifiedFeeNodes) { + if (specifiedParentId.equals(node.getParentId())) { + Map attrs = node.getAttributes(); + if (attrs != null && config.getSourceUnifiedFeeSettingId().equals( + Long.valueOf(attrs.get("sourceUnifiedFeeSettingId").toString()))) { + existingSpecifiedNode = node; + break; + } + } + } + + if (existingSpecifiedNode != null) { + // 已存在,更新配置 + Map existingAttrs = existingSpecifiedNode.getAttributes(); + if (existingAttrs == null) { + existingAttrs = new HashMap<>(); + } + // 指定清单节点的motherQuotaIds应该包含所有相关定额 + List allQuotaIds = new ArrayList<>(); + for (List ids : parentToQuotaIds.values()) { + allQuotaIds.addAll(ids); + } + existingAttrs.put("motherQuotaIds", allQuotaIds); + existingAttrs.put("isSpecifiedList", true); + existingSpecifiedNode.setAttributes(existingAttrs); + wbBoqDivisionMapper.updateById(existingSpecifiedNode); + log.info("更新指定清单统一取费目录:specifiedListCode={}, nodeId={}", + specifiedListCode, existingSpecifiedNode.getId()); + existingMap.put(specifiedKey, existingSpecifiedNode); + continue; + } + + existingMap.put(specifiedKey, null); + + // 获取指定清单下的最大排序号 + Integer maxSortOrder = wbBoqDivisionMapper.selectMaxSortOrderByParentId(compileTreeId, specifiedParentId); + int newSortOrder = (maxSortOrder != null ? maxSortOrder : 0) + 1; + + // 创建指定清单下的统一取费节点 + WbBoqDivisionDO specifiedUnifiedFeeNode = new WbBoqDivisionDO(); + specifiedUnifiedFeeNode.setCompileTreeId(compileTreeId); + specifiedUnifiedFeeNode.setParentId(specifiedParentId); + specifiedUnifiedFeeNode.setNodeType("unified_fee"); + specifiedUnifiedFeeNode.setSourceType("unified_fee"); + specifiedUnifiedFeeNode.setCode(setting.getCode()); + specifiedUnifiedFeeNode.setName(setting.getName()); + specifiedUnifiedFeeNode.setSortOrder(newSortOrder); + // 统一取费节点的工程量默认为1 + specifiedUnifiedFeeNode.setQty(java.math.BigDecimal.ONE); + + // 存储来源信息,标记为指定清单节点 + Map specifiedAttrs = new HashMap<>(); + specifiedAttrs.put("sourceUnifiedFeeSettingId", config.getSourceUnifiedFeeSettingId()); + specifiedAttrs.put("rateModeId", config.getRateModeId()); + specifiedAttrs.put("feeChapter", setting.getFeeChapter()); + specifiedAttrs.put("feeCategory", configData.get("feeCategory") != null ? configData.get("feeCategory") : setting.getFeeCategory()); + specifiedAttrs.put("thisListPercentage", configData.get("thisListPercentage") != null ? configData.get("thisListPercentage") : setting.getThisListPercentage()); + specifiedAttrs.put("specifiedListPercentage", configData.get("specifiedListPercentage") != null ? configData.get("specifiedListPercentage") : setting.getSpecifiedListPercentage()); + specifiedAttrs.put("specifiedListCode", specifiedListCode); + specifiedAttrs.put("isSpecifiedList", true); + // 指定清单节点的motherQuotaIds应该包含所有相关定额 + List allQuotaIds = new ArrayList<>(); + for (List ids : parentToQuotaIds.values()) { + allQuotaIds.addAll(ids); + } + specifiedAttrs.put("motherQuotaIds", allQuotaIds); + specifiedUnifiedFeeNode.setAttributes(specifiedAttrs); + + // 构建path + String[] specifiedParentPath = specifiedList.getPath(); + String[] specifiedNewPath = new String[(specifiedParentPath != null ? specifiedParentPath.length : 0) + 1]; + if (specifiedParentPath != null) { + System.arraycopy(specifiedParentPath, 0, specifiedNewPath, 0, specifiedParentPath.length); + } + + wbBoqDivisionMapper.insert(specifiedUnifiedFeeNode); + + specifiedNewPath[specifiedNewPath.length - 1] = String.valueOf(specifiedUnifiedFeeNode.getId()); + specifiedUnifiedFeeNode.setPath(specifiedNewPath); + wbBoqDivisionMapper.updateById(specifiedUnifiedFeeNode); + + log.info("添加指定清单统一取费目录:specifiedListCode={}, parentId={}, nodeId={}, 母定额数量={}", + specifiedListCode, specifiedParentId, specifiedUnifiedFeeNode.getId(), allQuotaIds.size()); + } + } + } + } + + @Override + public List getSummarySource(Long compileTreeId, Long sourceUnifiedFeeSettingId) { + // 1. 获取统一取费配置 + WbUnifiedFeeConfigDO config = getBySourceUnifiedFeeSettingId(compileTreeId, sourceUnifiedFeeSettingId); + if (config == null || config.getConfigData() == null) { + return new ArrayList<>(); + } + + // 2. 从configData中获取divisionIds(范围) + Map configData = config.getConfigData(); + Object divisionIdsObj = configData.get("divisionIds"); + if (divisionIdsObj == null) { + return new ArrayList<>(); + } + + List divisionIds = new ArrayList<>(); + if (divisionIdsObj instanceof List) { + for (Object id : (List) divisionIdsObj) { + if (id instanceof Number) { + divisionIds.add(((Number) id).longValue()); + } else if (id instanceof String) { + try { + divisionIds.add(Long.parseLong((String) id)); + } catch (NumberFormatException e) { + log.warn("无法解析divisionId: {}", id); + } + } + } + } + + if (divisionIds.isEmpty()) { + return new ArrayList<>(); + } + + // 3. 查询这些分部/清单下的所有定额节点,或者divisionIds本身就是定额节点 + // 情况1:divisionIds是分部/清单ID,查询其下的quota节点 + List quotaNodes = wbBoqDivisionMapper.selectList( + new LambdaQueryWrapperX() + .eq(WbBoqDivisionDO::getCompileTreeId, compileTreeId) + .eq(WbBoqDivisionDO::getNodeType, "quota") + .in(WbBoqDivisionDO::getParentId, divisionIds) + ); + + // 情况2:divisionIds本身就是quota节点ID + if (quotaNodes.isEmpty()) { + quotaNodes = wbBoqDivisionMapper.selectList( + new LambdaQueryWrapperX() + .eq(WbBoqDivisionDO::getCompileTreeId, compileTreeId) + .eq(WbBoqDivisionDO::getNodeType, "quota") + .in(WbBoqDivisionDO::getId, divisionIds) + ); + } + + // 4. 获取所有父节点ID,用于查询父节点的sortOrder + Set parentIds = quotaNodes.stream() + .map(WbBoqDivisionDO::getParentId) + .filter(id -> id != null) + .collect(Collectors.toSet()); + Map parentSortOrderMap = new HashMap<>(); + if (!parentIds.isEmpty()) { + List parentNodes = wbBoqDivisionMapper.selectBatchIds(new ArrayList<>(parentIds)); + for (WbBoqDivisionDO parent : parentNodes) { + parentSortOrderMap.put(parent.getId(), parent.getSortOrder() != null ? parent.getSortOrder() : 0); + } + } + + // 按分部分项顺序排序(先按父节点sortOrder,再按自身sortOrder) + quotaNodes.sort((a, b) -> { + Integer parentSortA = parentSortOrderMap.getOrDefault(a.getParentId(), 0); + Integer parentSortB = parentSortOrderMap.getOrDefault(b.getParentId(), 0); + int parentCmp = parentSortA.compareTo(parentSortB); + if (parentCmp != 0) return parentCmp; + + Integer sortA = a.getSortOrder() != null ? a.getSortOrder() : 0; + Integer sortB = b.getSortOrder() != null ? b.getSortOrder() : 0; + return sortA.compareTo(sortB); + }); + + // 5. 转换为VO(category使用nodeType,前端会通过字典转换为显示文本) + return quotaNodes.stream().map(node -> { + WbUnifiedFeeSummaryItemVO vo = new WbUnifiedFeeSummaryItemVO(); + vo.setId(node.getId()); + vo.setCode(node.getCode()); + vo.setCategory(node.getNodeType()); // 前端通过division_category字典转换 + vo.setName(node.getName()); + vo.setUnit(node.getUnit()); // 单位字段 + vo.setQuantity(node.getQty()); + return vo; + }).collect(Collectors.toList()); + } + + @Override + public List getSummarySourceByDivisionId(Long divisionId) { + // 1. 获取统一取费节点 + WbBoqDivisionDO unifiedFeeNode = wbBoqDivisionMapper.selectById(divisionId); + if (unifiedFeeNode == null || !"unified_fee".equals(unifiedFeeNode.getNodeType())) { + return new ArrayList<>(); + } + + // 2. 从节点的attributes中获取motherQuotaIds(优先)或sourceUnifiedFeeSettingId + Map attributes = unifiedFeeNode.getAttributes(); + if (attributes == null) { + return new ArrayList<>(); + } + + // 优先使用motherQuotaIds(新逻辑:直接存储在节点属性中) + @SuppressWarnings("unchecked") + List motherQuotaIdsObj = (List) attributes.get("motherQuotaIds"); + if (motherQuotaIdsObj != null && !motherQuotaIdsObj.isEmpty()) { + // 直接根据motherQuotaIds获取定额节点 + List motherQuotaIds = motherQuotaIdsObj.stream() + .map(obj -> { + if (obj instanceof Number) { + return ((Number) obj).longValue(); + } else if (obj instanceof String) { + return Long.parseLong((String) obj); + } + return null; + }) + .filter(id -> id != null) + .collect(Collectors.toList()); + + if (!motherQuotaIds.isEmpty()) { + List quotaNodes = wbBoqDivisionMapper.selectBatchIds(motherQuotaIds); + + // 获取所有父节点ID,用于查询父节点的sortOrder + Set parentIds = quotaNodes.stream() + .map(WbBoqDivisionDO::getParentId) + .filter(id -> id != null) + .collect(Collectors.toSet()); + Map parentSortOrderMap = new HashMap<>(); + if (!parentIds.isEmpty()) { + List parentNodes = wbBoqDivisionMapper.selectBatchIds(new ArrayList<>(parentIds)); + for (WbBoqDivisionDO parent : parentNodes) { + parentSortOrderMap.put(parent.getId(), parent.getSortOrder() != null ? parent.getSortOrder() : 0); + } + } + + // 按分部分项顺序排序(先按父节点sortOrder,再按自身sortOrder) + quotaNodes.sort((a, b) -> { + // 先比较父节点的sortOrder(清单的顺序) + Integer parentSortA = parentSortOrderMap.getOrDefault(a.getParentId(), 0); + Integer parentSortB = parentSortOrderMap.getOrDefault(b.getParentId(), 0); + int parentCmp = parentSortA.compareTo(parentSortB); + if (parentCmp != 0) return parentCmp; + + // 父节点相同则按自身sortOrder排序 + Integer sortA = a.getSortOrder() != null ? a.getSortOrder() : 0; + Integer sortB = b.getSortOrder() != null ? b.getSortOrder() : 0; + return sortA.compareTo(sortB); + }); + // 批量查询定额基价的catalogItemId + Set quotaItemIds = quotaNodes.stream() + .map(WbBoqDivisionDO::getSourceQuotaItemId) + .filter(id -> id != null) + .collect(Collectors.toSet()); + Map quotaItemToCatalogMap = new HashMap<>(); + if (!quotaItemIds.isEmpty()) { + List quotaItems = + quotaItemMapper.selectBatchIds(new ArrayList<>(quotaItemIds)); + for (com.yhy.module.core.dal.dataobject.quota.QuotaItemDO item : quotaItems) { + quotaItemToCatalogMap.put(item.getId(), item.getCatalogItemId()); + } + } + + return quotaNodes.stream().map(node -> { + WbUnifiedFeeSummaryItemVO vo = new WbUnifiedFeeSummaryItemVO(); + vo.setId(node.getId()); + vo.setCode(node.getCode()); + vo.setCategory(node.getNodeType()); + vo.setName(node.getName()); + vo.setUnit(node.getUnit()); + vo.setQuantity(node.getQty()); + // 设置sourceCatalogItemId用于取费章节过滤 + if (node.getSourceQuotaItemId() != null) { + vo.setSourceCatalogItemId(quotaItemToCatalogMap.get(node.getSourceQuotaItemId())); + } + return vo; + }).collect(Collectors.toList()); + } + } + + // 兼容旧逻辑:使用sourceUnifiedFeeSettingId从配置中获取 + Object settingIdObj = attributes.get("sourceUnifiedFeeSettingId"); + if (settingIdObj == null) { + return new ArrayList<>(); + } + + Long sourceUnifiedFeeSettingId; + if (settingIdObj instanceof Number) { + sourceUnifiedFeeSettingId = ((Number) settingIdObj).longValue(); + } else if (settingIdObj instanceof String) { + try { + sourceUnifiedFeeSettingId = Long.parseLong((String) settingIdObj); + } catch (NumberFormatException e) { + log.warn("无法解析sourceUnifiedFeeSettingId: {}", settingIdObj); + return new ArrayList<>(); + } + } else { + return new ArrayList<>(); + } + + // 调用原有方法获取汇总来源 + return getSummarySource(unifiedFeeNode.getCompileTreeId(), sourceUnifiedFeeSettingId); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbUnitFeeSettingServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbUnitFeeSettingServiceImpl.java new file mode 100644 index 0000000..757334e --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbUnitFeeSettingServiceImpl.java @@ -0,0 +1,913 @@ +package com.yhy.module.core.service.workbench.impl; + +import cn.hutool.core.collection.CollUtil; +import com.yhy.module.core.controller.admin.quota.vo.QuotaFeeItemWithRateRespVO; +import com.yhy.module.core.controller.admin.quota.vo.QuotaResourceRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.price.CategoryPriceSum; +import com.yhy.module.core.controller.admin.workbench.vo.price.ResourcePriceDetail; +import com.yhy.module.core.dal.dataobject.workbench.WbBoqDivisionDO; +import com.yhy.module.core.dal.dataobject.workbench.WbCompileTreeDO; +import com.yhy.module.core.dal.dataobject.workbench.WbProjectTreeDO; +import com.yhy.module.core.dal.dataobject.workbench.WbUnitFeeSettingDO; +import com.yhy.module.core.dal.dataobject.workbench.WbUnitInfoDO; +import com.yhy.module.core.dal.dataobject.workbench.WbUnitRateSettingDO; +import com.yhy.module.core.dal.mysql.quota.QuotaRateItemMapper; +import com.yhy.module.core.dal.mysql.workbench.WbBoqDivisionMapper; +import com.yhy.module.core.dal.mysql.workbench.WbCompileTreeMapper; +import com.yhy.module.core.dal.mysql.workbench.WbProjectTreeMapper; +import com.yhy.module.core.dal.mysql.workbench.WbUnitFeeSettingMapper; +import com.yhy.module.core.dal.mysql.workbench.WbUnitInfoMapper; +import com.yhy.module.core.dal.mysql.workbench.WbUnitRateSettingMapper; +import com.yhy.module.core.service.quota.QuotaFeeItemService; +import com.yhy.module.core.service.quota.QuotaItemService; +import com.yhy.module.core.service.quota.QuotaResourceService; +import com.yhy.module.core.service.workbench.QuotaPriceCalculatorService; +import com.yhy.module.core.service.workbench.WbBoqResourceService; +import com.yhy.module.core.service.workbench.WbUnitFeeSettingService; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * 单位工程取费项设定 Service 实现 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class WbUnitFeeSettingServiceImpl implements WbUnitFeeSettingService { + + private final WbUnitFeeSettingMapper feeSettingMapper; + private final WbUnitInfoMapper unitInfoMapper; + private final WbBoqDivisionMapper divisionMapper; + private final QuotaFeeItemService quotaFeeItemService; + private final QuotaItemService quotaItemService; + private final QuotaResourceService quotaResourceService; + private final QuotaPriceCalculatorService quotaPriceCalculatorService; + private final QuotaRateItemMapper quotaRateItemMapper; + private final WbUnitRateSettingMapper unitRateSettingMapper; + private final WbCompileTreeMapper compileTreeMapper; + private final WbBoqResourceService wbBoqResourceService; + private final WbProjectTreeMapper projectTreeMapper; + private final com.yhy.module.core.service.workbench.WbSnapshotReadService wbSnapshotReadService; + private final com.yhy.module.core.dal.mysql.workbench.WbRateItemMapper wbRateItemMapper; + + @Override + public List getMergedFeeList(Long unitId) { + // 1. 获取单位工程信息 + WbUnitInfoDO unitInfo = unitInfoMapper.selectById(unitId); + if (unitInfo == null || unitInfo.getRateModeId() == null) { + log.warn("单位工程不存在或未设置费率模式, unitId={}", unitId); + return new ArrayList<>(); + } + + // 2. 获取取费项列表 - 只从快照表读取,不回退到后台标准库 + List standardList = wbSnapshotReadService.getFeeItemWithRateTree(unitInfo.getCompileTreeId(), unitInfo.getRateModeId()); + if (standardList == null) { + log.warn("[getMergedFeeList] 快照不存在, compileTreeId={}", unitInfo.getCompileTreeId()); + standardList = new ArrayList<>(); + } else { + log.debug("[getMergedFeeList] 从快照表读取取费项, compileTreeId={}, count={}", + unitInfo.getCompileTreeId(), standardList.size()); + } + if (CollUtil.isEmpty(standardList)) { + return new ArrayList<>(); + } + + // 3. 获取覆写值 + List overrides = feeSettingMapper.selectByUnitId(unitId); + Map overrideMap = overrides.stream() + .collect(Collectors.toMap(WbUnitFeeSettingDO::getSourceFeeItemId, Function.identity(), (a, b) -> a)); + + // 4. 扁平化并合并 + List result = new ArrayList<>(); + flattenAndMerge(standardList, overrideMap, null, result); + + return result; + } + + @Override + public List getMergedFeeListByDivisionId(Long divisionId) { + return getMergedFeeListByDivisionIdInternal(divisionId, null); + } + + @Override + public List getMergedFeeListByDivisionId(Long divisionId, + Map categorySums) { + return getMergedFeeListByDivisionIdInternal(divisionId, categorySums); + } + + /** + * 内部实现:获取取费项列表并计算子单价 + * @param divisionId 分部分项节点ID(定额节点) + * @param preComputedCategorySums 预计算的分类汇总(如果非null则跳过重新加载工料机) + */ + private List getMergedFeeListByDivisionIdInternal(Long divisionId, + Map preComputedCategorySums) { + // 1. 获取分部分项节点 + WbBoqDivisionDO division = divisionMapper.selectById(divisionId); + if (division == null) { + log.warn("分部分项节点不存在, divisionId={}", divisionId); + return new ArrayList<>(); + } + + // 2. 只有定额类型的节点才有取费数据 + if (!"quota".equals(division.getNodeType())) { + log.info("非定额节点,不返回取费数据, divisionId={}, nodeType={}", divisionId, division.getNodeType()); + return new ArrayList<>(); + } + + // 3. 获取费率模式ID:优先使用单位工程的用户选择,fallback到标准库树推导 + if (division.getSourceQuotaItemId() == null) { + log.warn("定额节点未关联定额基价, divisionId={}", divisionId); + return new ArrayList<>(); + } + + Long rateModeId = null; + // 3.1 获取定额所属的专业ID + Long quotaSpecialtyId = quotaItemService.getSpecialtyIdByQuotaItem(division.getSourceQuotaItemId()); + + // 3.2 提前查询单位工程信息(后续多个 fallback 步骤复用,避免重复查询) + WbUnitInfoDO unitInfo = division.getCompileTreeId() != null + ? unitInfoMapper.selectByCompileTreeId(division.getCompileTreeId()) : null; + + // 3.3 优先从单位工程的费率模式绑定中查找(单位工程级费率及取费) + if (quotaSpecialtyId != null && unitInfo != null) { + // 从单位工程的 rateModeBindings 中查找对应定额专业的费率模式 + rateModeId = unitInfo.getRateModeIdByQuotaSpecialty(quotaSpecialtyId); + if (rateModeId != null) { + log.info("从单位工程费率绑定获取定额专业对应的费率模式, divisionId={}, quotaSpecialtyId={}, rateModeId={}", + divisionId, quotaSpecialtyId, rateModeId); + } + } + + // 3.4 fallback:从项目的费率模式绑定中查找(兼容旧数据) + if (rateModeId == null && quotaSpecialtyId != null && division.getCompileTreeId() != null) { + WbCompileTreeDO compileTree = compileTreeMapper.selectById(division.getCompileTreeId()); + if (compileTree != null && compileTree.getProjectId() != null) { + WbProjectTreeDO project = projectTreeMapper.selectById(compileTree.getProjectId()); + if (project != null) { + // 从项目的 rateModeBindings 中查找对应定额专业的费率模式 + rateModeId = project.getRateModeIdByQuotaSpecialty(quotaSpecialtyId); + if (rateModeId != null) { + log.info("从项目费率绑定获取定额专业对应的费率模式(fallback), divisionId={}, quotaSpecialtyId={}, rateModeId={}", + divisionId, quotaSpecialtyId, rateModeId); + } + } + } + } + + // 3.5 fallback:从单位工程主费率模式获取(仅当定额专业与单位工程专业一致时) + if (rateModeId == null && unitInfo != null && unitInfo.getRateModeId() != null) { + // 检查单位工程的定额专业是否与定额所属专业一致 + if (quotaSpecialtyId == null || quotaSpecialtyId.equals(unitInfo.getQuotaCatalogItemId())) { + rateModeId = unitInfo.getRateModeId(); + log.info("从单位工程主费率模式获取, divisionId={}, compileTreeId={}, rateModeId={}", + divisionId, division.getCompileTreeId(), rateModeId); + } + } + + // 3.6 fallback:通过定额基价在标准库树中查找默认费率模式 + if (rateModeId == null) { + rateModeId = quotaItemService.getRateModeIdByQuotaItem(division.getSourceQuotaItemId()); + log.info("通过定额基价查找默认费率模式, divisionId={}, sourceQuotaItemId={}, rateModeId={}", + divisionId, division.getSourceQuotaItemId(), rateModeId); + } + + if (rateModeId == null) { + log.warn("未找到费率模式节点, divisionId={}, sourceQuotaItemId={}", + divisionId, division.getSourceQuotaItemId()); + return new ArrayList<>(); + } + + // 4. 获取取费项列表 - 只从快照表读取,不回退到后台标准库 + List standardList = wbSnapshotReadService.getFeeItemWithRateTree(division.getCompileTreeId(), rateModeId); + if (standardList == null) { + log.warn("[getMergedFeeListByDivisionId] 快照不存在, compileTreeId={}", division.getCompileTreeId()); + standardList = new ArrayList<>(); + } else { + log.debug("[getMergedFeeListByDivisionId] 从快照表读取取费项, compileTreeId={}, count={}", + division.getCompileTreeId(), standardList.size()); + } + if (CollUtil.isEmpty(standardList)) { + return new ArrayList<>(); + } + + // 6. 获取覆写值(基于分部分项节点) + List overrides = feeSettingMapper.selectByDivisionId(divisionId); + + // 6.1 有 sourceFeeItemId 的覆写值(用于合并标准库取费项) + Map overrideMap = overrides.stream() + .filter(o -> o.getSourceFeeItemId() != null) + .collect(java.util.stream.Collectors.toMap( + WbUnitFeeSettingDO::getSourceFeeItemId, + java.util.function.Function.identity(), + (a, b) -> a)); + + // 6.2 有 sourceRateItemId 的覆写值(用于合并标准库费率项) + Map rateOverrideMap = overrides.stream() + .filter(o -> o.getSourceFeeItemId() == null && o.getSourceRateItemId() != null) + .collect(java.util.stream.Collectors.toMap( + WbUnitFeeSettingDO::getSourceRateItemId, + java.util.function.Function.identity(), + (a, b) -> a)); + + // 6.3 没有 sourceFeeItemId 也没有 sourceRateItemId 的才是真正的自定义行 + List customRows = overrides.stream() + .filter(o -> o.getSourceFeeItemId() == null && o.getSourceRateItemId() == null && !Boolean.TRUE.equals(o.getUserDeleted())) + .collect(java.util.stream.Collectors.toList()); + + // 7. 扁平化并合并 + List result = new ArrayList<>(); + flattenAndMerge(standardList, overrideMap, rateOverrideMap, result); + + // 7.1 添加自定义行(自定义行不设置 feeItemId,让前端知道这是自定义行) + for (WbUnitFeeSettingDO customRow : customRows) { + QuotaFeeItemWithRateRespVO vo = new QuotaFeeItemWithRateRespVO(); + // 自定义行不设置 feeItemId,前端保存时也不传 feeItemId + vo.setFeeItemName(customRow.getName()); + vo.setCode(customRow.getCode()); + vo.setRatePercentage(customRow.getRatePercentage()); + vo.setFeeCategory(customRow.getFeeCategory()); + vo.setBaseDescription(customRow.getBaseDescription()); + vo.setSortOrder(customRow.getSortOrder()); + vo.setCalcBase(customRow.getCalcBase()); + vo.setHidden(customRow.getHidden()); + vo.setVariable(customRow.getVariable()); + result.add(vo); + } + + // 7.2 按 sortOrder 排序 + result.sort((a, b) -> { + Integer sortA = a.getSortOrder() != null ? a.getSortOrder() : Integer.MAX_VALUE; + Integer sortB = b.getSortOrder() != null ? b.getSortOrder() : Integer.MAX_VALUE; + return sortA.compareTo(sortB); + }); + + // 8. 获取项目ID(通过 compileTreeId 查询) + Long projectId = null; + if (division.getCompileTreeId() != null) { + WbCompileTreeDO compileTree = compileTreeMapper.selectById(division.getCompileTreeId()); + if (compileTree != null) { + projectId = compileTree.getProjectId(); + } + } + + // 9. 计算子单价 + if (preComputedCategorySums != null) { + // 使用调用方预计算的分类汇总(避免重复加载工料机数据) + calculateSubPricesWithCategorySums(result, preComputedCategorySums, projectId); + } else { + // 需要自行加载工料机数据计算分类汇总 + calculateSubPrices(result, divisionId, division.getSourceQuotaItemId(), projectId); + } + + return result; + } + + /** + * 计算取费项的子单价(需要自行加载工料机数据) + * @param feeItems 取费项列表 + * @param divisionId 分部分项节点ID + * @param quotaItemId 定额基价ID + * @param projectId 项目ID(用于查询项目级别的费率设置) + */ + private void calculateSubPrices(List feeItems, Long divisionId, Long quotaItemId, Long projectId) { + if (CollUtil.isEmpty(feeItems)) { + return; + } + + // 1. 优先从工作台获取工料机数据(包含用户的调整消耗量) + List wbResources = + wbBoqResourceService.getListByDivisionId(divisionId); + + List resourceDetails; + if (CollUtil.isNotEmpty(wbResources)) { + // 使用工作台数据(含adjustConsumeQty) + resourceDetails = wbResources.stream() + .map(this::convertWbResourceToDetail) + .collect(Collectors.toList()); + } else { + // 【已禁用fallback】费用切割复制后与后台无关,工作台没有数据则直接返回 + return; + } + + // 2. 计算分类汇总 + Map categorySums = quotaPriceCalculatorService.calculateCategorySums(resourceDetails); + + // 3. 使用分类汇总计算子单价 + calculateSubPricesWithCategorySums(feeItems, categorySums, projectId); + } + + /** + * 使用预计算的分类汇总计算取费项子单价(避免重复加载工料机数据) + * @param feeItems 取费项列表 + * @param categorySums 分类汇总 + * @param projectId 项目ID(用于查询项目级别的费率设置) + */ + private void calculateSubPricesWithCategorySums(List feeItems, + Map categorySums, Long projectId) { + if (CollUtil.isEmpty(feeItems)) { + return; + } + + // 1. 构建取费项代号到子单价的映射(用于综合单价计算) + Map feeItemSubPriceMap = new java.util.HashMap<>(); + + // 2. 先计算普通取费项的子单价(非综合单价) + for (QuotaFeeItemWithRateRespVO feeItem : feeItems) { + if (!"ZHDJ".equals(feeItem.getSystemCode())) { + calculateSingleSubPrice(feeItem, categorySums, projectId); + // 记录代号和子单价的映射 + if (feeItem.getCode() != null && feeItem.getSubPrice() != null) { + feeItemSubPriceMap.put(feeItem.getCode(), feeItem.getSubPrice()); + } + } + } + + // 3. 再计算综合单价(引用其他取费项的子单价) + for (QuotaFeeItemWithRateRespVO feeItem : feeItems) { + if ("ZHDJ".equals(feeItem.getSystemCode())) { + calculateZhdjSubPrice(feeItem, feeItemSubPriceMap, projectId); + } + } + } + + /** + * 转换工作台工料机VO为ResourcePriceDetail + * 直接复用 VO 中已计算好的合价(含%工料机公式计算值和复合工料机汇总值) + */ + private ResourcePriceDetail convertWbResourceToDetail(com.yhy.module.core.controller.admin.workbench.vo.WbBoqResourceRespVO resource) { + return ResourcePriceDetail.builder() + .resourceId(resource.getId()) + .resourceCode(resource.getCode()) + .resourceName(resource.getName()) + .resourceType(resource.getResourceType()) + .categoryId(resource.getCategoryId()) + .taxExclBaseTotalSum(resource.getTaxExclBaseTotalSum()) + .taxInclBaseTotalSum(resource.getTaxInclBaseTotalSum()) + .taxExclCompileTotalSum(resource.getTaxExclCompileTotalSum()) + .taxInclCompileTotalSum(resource.getTaxInclCompileTotalSum()) + .isPercentUnit("%".equals(resource.getUnit())) + .calcBase(resource.getCalcBase()) + .build(); + } + + /** + * 转换QuotaResourceRespVO为ResourcePriceDetail + */ + private ResourcePriceDetail convertToResourcePriceDetail(QuotaResourceRespVO resource) { + return ResourcePriceDetail.builder() + .resourceId(resource.getId()) + .resourceCode(resource.getResourceCode()) + .resourceName(resource.getResourceName()) + .resourceType(resource.getResourceType()) + .categoryId(resource.getResourceCategoryId()) + .taxExclBaseTotalSum(resource.getTaxExclBaseTotalSum()) + .taxInclBaseTotalSum(resource.getTaxInclBaseTotalSum()) + .taxExclCompileTotalSum(resource.getTaxExclCompileTotalSum()) + .taxInclCompileTotalSum(resource.getTaxInclCompileTotalSum()) + .isPercentUnit("%".equals(resource.getResourceUnit())) + .build(); + } + + /** + * 计算单个取费项的子单价 + * @param feeItem 取费项 + * @param categorySums 分类汇总 + * @param projectId 项目ID(用于查询项目级别的费率设置) + */ + private void calculateSingleSubPrice(QuotaFeeItemWithRateRespVO feeItem, Map categorySums, Long projectId) { + // 1. 计算基数值 + BigDecimal calcBaseValue = BigDecimal.ZERO; + if (feeItem.getCalcBase() != null) { + calcBaseValue = quotaPriceCalculatorService.evaluateCalcBaseFormula(feeItem.getCalcBase(), categorySums); + } + feeItem.setCalcBaseValue(calcBaseValue); + + // 2. 获取费率百分比(优先从项目级别的费率设置中获取) + BigDecimal ratePercent = getRatePercentValue(feeItem, projectId); + feeItem.setRateValue(ratePercent); + + // 3. 计算子单价:子单价 = 计算基数值 × (费率% / 100) + BigDecimal subPrice = calcBaseValue + .multiply(ratePercent) + .divide(new BigDecimal("100"), 6, RoundingMode.HALF_UP); + feeItem.setSubPrice(subPrice); + } + + /** + * 获取费率百分比的实际数值 + * 优先级: + * 1. 如果 ratePercentage 是数字,直接使用 + * 2. 优先从项目级别的费率设置(WbUnitRateSettingDO)中获取 + * 3. 否则从关联的费率项的 settings.fieldValues 中获取第一个字段值 + * + * @param feeItem 取费项 + * @param projectId 项目ID(用于查询项目级别的费率设置) + */ + private BigDecimal getRatePercentValue(QuotaFeeItemWithRateRespVO feeItem, Long projectId) { + String ratePercentage = feeItem.getRatePercentage(); + + // 1. 尝试直接解析为数字 + if (ratePercentage != null && !ratePercentage.isEmpty()) { + try { + return new BigDecimal(ratePercentage); + } catch (NumberFormatException e) { + // 不是数字,继续尝试从费率项获取 + log.debug("[取费项计算] 费率代号非数字: {},尝试从费率设置获取实际值", ratePercentage); + } + } + + Long rateItemId = feeItem.getRateItemId(); + if (rateItemId == null) { + log.debug("[取费项计算] 未关联费率项, feeItemId={}", feeItem.getFeeItemId()); + return BigDecimal.ZERO; + } + + // 2. 优先从项目级别的费率设置中获取(用户选择的子节点值) + if (projectId != null) { + WbUnitRateSettingDO rateSetting = unitRateSettingMapper.selectByUnitIdAndRateItemId(projectId, rateItemId); + if (rateSetting != null && rateSetting.getFieldValues() != null && !rateSetting.getFieldValues().isEmpty()) { + // 获取第一个字段的值 + Object firstValue = rateSetting.getFieldValues().get(1); + if (firstValue == null) { + firstValue = rateSetting.getFieldValues().values().iterator().next(); + } + if (firstValue != null) { + try { + BigDecimal value = new BigDecimal(firstValue.toString()); + log.debug("[取费项计算] 从项目费率设置获取费率值, projectId={}, rateItemId={}, rateCode={}, value={}", + projectId, rateItemId, ratePercentage, value); + return value; + } catch (NumberFormatException e) { + log.warn("[取费项计算] 项目费率设置值无法转换为数字, projectId={}, rateItemId={}, value={}", + projectId, rateItemId, firstValue); + } + } + } + } + + // 3. 从关联的费率项的 settings.fieldValues 中获取(只从快照表读取) + Map settings = null; + + // 只从快照表读取,不回退到后台标准库 + com.yhy.module.core.dal.dataobject.workbench.WbRateItemDO wbRateItem = wbRateItemMapper.selectById(rateItemId); + if (wbRateItem != null) { + settings = wbRateItem.getSettings(); + log.debug("[取费项计算] 从快照表读取费率项, rateItemId={}", rateItemId); + } else { + log.warn("[取费项计算] 快照表中费率项不存在, rateItemId={}", rateItemId); + return BigDecimal.ZERO; + } + if (settings == null) { + log.debug("[取费项计算] 费率项无settings, rateItemId={}", rateItemId); + return BigDecimal.ZERO; + } + + @SuppressWarnings("unchecked") + Map fieldValues = (Map) settings.get("fieldValues"); + if (fieldValues == null || fieldValues.isEmpty()) { + log.debug("[取费项计算] 费率项无fieldValues, rateItemId={}", rateItemId); + return BigDecimal.ZERO; + } + + // 获取第一个字段值(通常是字段1) + Object firstValue = fieldValues.get("1"); + if (firstValue == null) { + // 尝试获取任意第一个值 + firstValue = fieldValues.values().iterator().next(); + } + + if (firstValue != null) { + try { + BigDecimal value = new BigDecimal(firstValue.toString()); + log.debug("[取费项计算] 从费率项获取费率值, rateItemId={}, rateCode={}, value={}", + rateItemId, ratePercentage, value); + return value; + } catch (NumberFormatException e) { + log.warn("[取费项计算] 字段值无法转换为数字, rateItemId={}, value={}", rateItemId, firstValue); + } + } + + return BigDecimal.ZERO; + } + + /** + * 计算综合单价的子单价(引用其他取费项的子单价) + * @param feeItem 取费项 + * @param feeItemSubPriceMap 取费项代号到子单价的映射 + * @param projectId 项目ID(用于查询项目级别的费率设置) + */ + private void calculateZhdjSubPrice(QuotaFeeItemWithRateRespVO feeItem, Map feeItemSubPriceMap, Long projectId) { + BigDecimal calcBaseValue = BigDecimal.ZERO; + + if (feeItem.getCalcBase() != null) { + @SuppressWarnings("unchecked") + Map calcBase = feeItem.getCalcBase(); + String formula = (String) calcBase.get("formula"); + + if (formula != null && !formula.isEmpty()) { + // 解析公式,替换变量为对应取费项的子单价 + calcBaseValue = evaluateZhdjFormula(formula, feeItemSubPriceMap); + } + } + feeItem.setCalcBaseValue(calcBaseValue); + + // 获取费率百分比(综合单价默认100%) + BigDecimal ratePercent = new BigDecimal("100"); // 默认100% + String ratePercentage = feeItem.getRatePercentage(); + if (ratePercentage != null && !ratePercentage.isEmpty()) { + try { + ratePercent = new BigDecimal(ratePercentage); + } catch (NumberFormatException e) { + log.debug("[综合单价计算] 费率代号非数字,使用默认值100%: {}", ratePercentage); + ratePercent = new BigDecimal("100"); + } + } + feeItem.setRateValue(ratePercent); + + // 计算子单价:subPrice = calcBaseValue * ratePercent / 100 + BigDecimal subPrice = calcBaseValue + .multiply(ratePercent) + .divide(new BigDecimal("100"), 6, RoundingMode.HALF_UP); + feeItem.setSubPrice(subPrice); + } + + /** + * 解析综合单价公式(变量为其他取费项的代号) + */ + private BigDecimal evaluateZhdjFormula(String formula, Map feeItemSubPriceMap) { + if (formula == null || formula.isEmpty()) { + return BigDecimal.ZERO; + } + + try { + // 替换变量为对应的子单价值 + String expression = formula; + for (Map.Entry entry : feeItemSubPriceMap.entrySet()) { + String varName = entry.getKey(); + BigDecimal value = entry.getValue(); + // 使用正则替换,确保只替换完整的变量名 + expression = expression.replaceAll("\\b" + java.util.regex.Pattern.quote(varName) + "\\b", + value.toPlainString()); + } + + // 使用JavaScript引擎计算表达式 + javax.script.ScriptEngineManager manager = new javax.script.ScriptEngineManager(); + javax.script.ScriptEngine engine = manager.getEngineByName("JavaScript"); + if (engine == null) { + engine = manager.getEngineByName("nashorn"); + } + if (engine == null) { + log.warn("[综合单价计算] 无法获取JavaScript引擎"); + return BigDecimal.ZERO; + } + + Object result = engine.eval(expression); + if (result instanceof Number) { + return new BigDecimal(result.toString()).setScale(6, RoundingMode.HALF_UP); + } + return BigDecimal.ZERO; + } catch (Exception e) { + log.error("[综合单价计算] 公式计算异常: formula={}, error={}", formula, e.getMessage()); + return BigDecimal.ZERO; + } + } + + /** + * 扁平化树形数据并合并覆写值 + * @param items 标准库取费项/费率项列表 + * @param overrideMap 取费项覆写映射(key: feeItemId) + * @param rateOverrideMap 费率项覆写映射(key: rateItemId) + * @param result 结果列表 + */ + private void flattenAndMerge(List items, + Map overrideMap, + Map rateOverrideMap, + List result) { + for (QuotaFeeItemWithRateRespVO item : items) { + // 检查是否被用户删除 + WbUnitFeeSettingDO override = null; + if (item.getFeeItemId() != null) { + override = overrideMap.get(item.getFeeItemId()); + } else if (item.getRateItemId() != null && rateOverrideMap != null) { + // 只有 rateItemId 没有 feeItemId 的费率项,从 rateOverrideMap 获取覆写 + override = rateOverrideMap.get(item.getRateItemId()); + } + + if (override != null && Boolean.TRUE.equals(override.getUserDeleted())) { + // 用户已删除,跳过 + continue; + } + + // 创建新的 VO 并合并覆写值 + QuotaFeeItemWithRateRespVO merged = new QuotaFeeItemWithRateRespVO(); + // 复制标准库数据 + merged.setRateItemId(item.getRateItemId()); + merged.setRateItemName(item.getRateItemName()); + merged.setRateItemCustomCode(item.getRateItemCustomCode()); + merged.setRateCode(item.getRateCode()); + merged.setParentId(item.getParentId()); + merged.setNodeType(item.getNodeType()); + merged.setFeeItemId(item.getFeeItemId()); + merged.setFeeItemName(item.getFeeItemName()); + merged.setFeeItemCustomCode(item.getFeeItemCustomCode()); + merged.setCalcBase(item.getCalcBase()); + merged.setRatePercentage(item.getRatePercentage()); + merged.setCode(item.getCode()); + merged.setFeeCategory(item.getFeeCategory()); + merged.setBaseDescription(item.getBaseDescription()); + merged.setSortOrder(item.getSortOrder()); + merged.setHidden(item.getHidden()); + merged.setVariable(item.getVariable()); + merged.setSystemCode(item.getSystemCode()); + merged.setCatalogItemId(item.getCatalogItemId()); + merged.setHasFeeItem(item.getHasFeeItem()); + + // 应用覆写值 + if (override != null) { + if (override.getName() != null) { + merged.setFeeItemName(override.getName()); + } + if (override.getRatePercentage() != null) { + merged.setRatePercentage(override.getRatePercentage()); + } + if (override.getCode() != null) { + merged.setCode(override.getCode()); + } + if (override.getFeeCategory() != null) { + merged.setFeeCategory(override.getFeeCategory()); + } + if (override.getBaseDescription() != null) { + merged.setBaseDescription(override.getBaseDescription()); + } + if (override.getSortOrder() != null) { + merged.setSortOrder(override.getSortOrder()); + } + if (override.getHidden() != null) { + merged.setHidden(override.getHidden()); + } + if (override.getVariable() != null) { + merged.setVariable(override.getVariable()); + } + if (override.getCalcBase() != null) { + merged.setCalcBase(override.getCalcBase()); + } + } + + result.add(merged); + + // 递归处理子节点 + if (CollUtil.isNotEmpty(item.getChildren())) { + flattenAndMerge(item.getChildren(), overrideMap, rateOverrideMap, result); + } + } + } + + @Override + public void saveOverride(Long divisionId, Long feeItemId, String name, String ratePercentage, + String code, String feeCategory, String baseDescription) { + WbUnitFeeSettingDO existing = feeSettingMapper.selectByDivisionIdAndFeeItemId(divisionId, feeItemId); + + if (existing != null) { + existing.setName(name); + existing.setRatePercentage(ratePercentage); + existing.setCode(code); + existing.setFeeCategory(feeCategory); + existing.setBaseDescription(baseDescription); + feeSettingMapper.updateById(existing); + } else { + WbUnitFeeSettingDO setting = new WbUnitFeeSettingDO(); + setting.setDivisionId(divisionId); + setting.setSourceFeeItemId(feeItemId); + setting.setName(name); + setting.setRatePercentage(ratePercentage); + setting.setCode(code); + setting.setFeeCategory(feeCategory); + setting.setBaseDescription(baseDescription); + // 设置基线值 + setting.setBaseName(name); + setting.setBaseRatePercentage(ratePercentage); + setting.setBaseCode(code); + setting.setBaseFeeCategory(feeCategory); + setting.setBaseBaseDescription(baseDescription); + feeSettingMapper.insert(setting); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void batchSaveOverrides(Long divisionId, List overrides) { + if (CollUtil.isEmpty(overrides)) { + return; + } + + // 0. 校验所有 calcBase 中的公式合法性 + for (FeeOverrideDTO dto : overrides) { + if (dto.getCalcBase() != null) { + String formula = (String) dto.getCalcBase().get("formula"); + if (formula != null && !formula.isEmpty()) { + String error = validateFormula(formula); + if (error != null) { + throw cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception( + com.yhy.module.core.enums.ErrorCodeConstants.FORMULA_INVALID, + dto.getName(), error); + } + } + } + } + + // 1. 先删除该 divisionId 下所有的覆写记录(全量替换策略) + feeSettingMapper.deleteByDivisionId(divisionId); + + // 2. 重新插入所有记录 + for (FeeOverrideDTO dto : overrides) { + WbUnitFeeSettingDO setting = new WbUnitFeeSettingDO(); + setting.setDivisionId(divisionId); + setting.setSourceFeeItemId(dto.getFeeItemId()); + setting.setSourceRateItemId(dto.getRateItemId()); + setting.setSortOrder(dto.getSortOrder()); + setting.setName(dto.getName()); + setting.setRatePercentage(dto.getRatePercentage()); + setting.setCode(dto.getCode()); + setting.setFeeCategory(dto.getFeeCategory()); + setting.setBaseDescription(dto.getBaseDescription()); + setting.setHidden(dto.getHidden()); + setting.setVariable(dto.getVariable()); + setting.setUserDeleted(dto.getUserDeleted()); + setting.setCalcBase(dto.getCalcBase()); + // 设置基线值 + setting.setBaseName(dto.getName()); + setting.setBaseRatePercentage(dto.getRatePercentage()); + setting.setBaseCode(dto.getCode()); + setting.setBaseFeeCategory(dto.getFeeCategory()); + setting.setBaseBaseDescription(dto.getBaseDescription()); + setting.setBaseCalcBase(dto.getCalcBase()); + feeSettingMapper.insert(setting); + } + } + + @Override + public void markAsDeleted(Long divisionId, Long feeItemId) { + // 1. 先尝试通过 sourceFeeItemId 查找(标准库取费项的覆写) + WbUnitFeeSettingDO existing = feeSettingMapper.selectByDivisionIdAndFeeItemId(divisionId, feeItemId); + + if (existing != null) { + existing.setUserDeleted(true); + feeSettingMapper.updateById(existing); + return; + } + + // 2. 尝试通过 id 查找(自定义行,feeItemId 实际上是覆写记录的 id) + WbUnitFeeSettingDO customRow = feeSettingMapper.selectById(feeItemId); + if (customRow != null && divisionId.equals(customRow.getDivisionId()) && customRow.getSourceFeeItemId() == null) { + // 自定义行直接物理删除 + feeSettingMapper.deleteById(feeItemId); + return; + } + + // 3. 都没找到,创建新的删除标记记录 + WbUnitFeeSettingDO setting = new WbUnitFeeSettingDO(); + setting.setDivisionId(divisionId); + setting.setSourceFeeItemId(feeItemId); + setting.setUserDeleted(true); + feeSettingMapper.insert(setting); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void initializeSettings(Long divisionId) { + log.info("初始化定额节点取费项设定, divisionId={}", divisionId); + + // 获取分部分项节点 + WbBoqDivisionDO division = divisionMapper.selectById(divisionId); + if (division == null || !"quota".equals(division.getNodeType())) { + log.warn("分部分项节点不存在或非定额节点, divisionId={}", divisionId); + return; + } + + // 获取费率模式ID:优先使用单位工程的用户选择,fallback到标准库树推导 + Long rateModeId = null; + if (division.getCompileTreeId() != null) { + WbUnitInfoDO unitInfo = unitInfoMapper.selectByCompileTreeId(division.getCompileTreeId()); + if (unitInfo != null && unitInfo.getRateModeId() != null) { + rateModeId = unitInfo.getRateModeId(); + } + } + if (rateModeId == null) { + rateModeId = quotaItemService.getRateModeIdByQuotaItem(division.getSourceQuotaItemId()); + } + if (rateModeId == null) { + log.warn("未找到费率模式节点, divisionId={}, sourceQuotaItemId={}", + divisionId, division.getSourceQuotaItemId()); + return; + } + + // 获取取费项 - 只从快照表读取,不回退到后台标准库 + List feeItems = wbSnapshotReadService.getFeeItemWithRateTree(division.getCompileTreeId(), rateModeId); + if (CollUtil.isEmpty(feeItems)) { + log.warn("[initializeSettings] 快照取费项为空, compileTreeId={}, rateModeId={}", division.getCompileTreeId(), rateModeId); + return; + } + + // 扁平化并创建快照 + List settings = new ArrayList<>(); + flattenAndCreateSnapshot(feeItems, divisionId, settings, new int[]{1}); + + // 批量插入 + for (WbUnitFeeSettingDO setting : settings) { + feeSettingMapper.insert(setting); + } + } + + /** + * 扁平化树形数据并创建快照 + */ + private void flattenAndCreateSnapshot(List items, Long divisionId, + List result, int[] sortOrder) { + for (QuotaFeeItemWithRateRespVO item : items) { + WbUnitFeeSettingDO setting = new WbUnitFeeSettingDO(); + setting.setDivisionId(divisionId); + setting.setSourceFeeItemId(item.getFeeItemId()); + setting.setSourceRateItemId(item.getRateItemId()); + setting.setSortOrder(sortOrder[0]++); + + // 当前值 + String name = item.getRateItemName() != null ? item.getRateItemName() : item.getFeeItemName(); + setting.setName(name); + setting.setRatePercentage(item.getRatePercentage()); + setting.setCode(item.getCode()); + setting.setFeeCategory(item.getFeeCategory()); + setting.setBaseDescription(item.getBaseDescription()); + setting.setHidden(item.getHidden()); + setting.setVariable(item.getVariable()); + setting.setSystemRow("ZHDJ".equals(item.getCode()) || "SJ".equals(item.getCode()) || "system".equals(item.getNodeType())); + + // 基线值(快照) + setting.setBaseName(name); + setting.setBaseRatePercentage(item.getRatePercentage()); + setting.setBaseCode(item.getCode()); + setting.setBaseFeeCategory(item.getFeeCategory()); + setting.setBaseBaseDescription(item.getBaseDescription()); + + result.add(setting); + + // 递归处理子节点 + if (CollUtil.isNotEmpty(item.getChildren())) { + flattenAndCreateSnapshot(item.getChildren(), divisionId, result, sortOrder); + } + } + } + + /** + * 校验公式合法性 + * @param formula 公式字符串 + * @return 错误信息,null 表示校验通过 + */ + private String validateFormula(String formula) { + // 允许公式为空 + if (formula == null || formula.trim().isEmpty()) { + return null; // 空公式视为合法 + } + + // 检查括号是否匹配 + int bracketCount = 0; + for (char c : formula.toCharArray()) { + if (c == '(') bracketCount++; + if (c == ')') bracketCount--; + if (bracketCount < 0) { + return "括号不匹配"; + } + } + if (bracketCount != 0) { + return "括号不匹配"; + } + + // 检查是否有连续的运算符(去除空格后) + String noSpaces = formula.replaceAll("\\s+", ""); + if (noSpaces.matches(".*[+\\-*/]{2,}.*")) { + return "存在连续的运算符"; + } + + // 检查是否以 * 或 / 开头,或以运算符结尾 + String trimmed = formula.trim(); + if (trimmed.matches("^[*/].*") || trimmed.matches(".*[+\\-*/]$")) { + return "公式格式不正确"; + } + + // 检查是否包含非法字符(只允许字母、数字、下划线、运算符、括号、空格、小数点、中文) + if (!noSpaces.matches("^[a-zA-Z0-9_+\\-*/().\\u4e00-\\u9fa5]+$")) { + return "包含非法字符"; + } + + return null; // 校验通过 + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbUnitInfoServiceImpl.java b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbUnitInfoServiceImpl.java new file mode 100644 index 0000000..d815ef3 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/service/workbench/impl/WbUnitInfoServiceImpl.java @@ -0,0 +1,665 @@ +package com.yhy.module.core.service.workbench.impl; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0; +import static com.yhy.module.core.enums.ErrorCodeConstants.WB_COMPILE_TREE_NOT_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.WB_UNIT_INFO_CODE_EXISTS; +import static com.yhy.module.core.enums.ErrorCodeConstants.WB_UNIT_INFO_NODE_NOT_UNIT; +import static com.yhy.module.core.enums.ErrorCodeConstants.WB_UNIT_INFO_NOT_EXISTS; + +import com.yhy.module.core.controller.admin.workbench.vo.WbUnitInfoRespVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbUnitInfoSaveReqVO; +import com.yhy.module.core.controller.admin.workbench.vo.WbUsedQuotaSpecialtyRespVO; +import com.yhy.module.core.convert.workbench.WbUnitInfoConvert; +import com.yhy.module.core.dal.dataobject.boq.BoqCatalogItemDO; +import com.yhy.module.core.dal.dataobject.quota.QuotaCatalogItemDO; +import com.yhy.module.core.dal.dataobject.quota.QuotaCatalogTreeDO; +import com.yhy.module.core.dal.dataobject.quota.QuotaItemDO; +import com.yhy.module.core.dal.dataobject.workbench.WbBoqDivisionDO; +import com.yhy.module.core.dal.dataobject.workbench.WbCompileTreeDO; +import com.yhy.module.core.dal.dataobject.workbench.WbProjectTreeDO; +import com.yhy.module.core.dal.dataobject.workbench.WbUnitInfoDO; +import com.yhy.module.core.dal.mysql.boq.BoqCatalogItemMapper; +import com.yhy.module.core.dal.mysql.quota.QuotaCatalogItemMapper; +import com.yhy.module.core.dal.mysql.quota.QuotaCatalogTreeMapper; +import com.yhy.module.core.dal.mysql.quota.QuotaItemMapper; +import com.yhy.module.core.dal.mysql.workbench.WbBoqDivisionMapper; +import com.yhy.module.core.dal.mysql.workbench.WbCompileTreeMapper; +import com.yhy.module.core.dal.mysql.workbench.WbProjectTreeMapper; +import com.yhy.module.core.dal.mysql.workbench.WbUnitInfoMapper; +import com.yhy.module.core.service.workbench.WbUnitInfoService; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +/** + * 单位工程信息 Service 实现类 + * + * @author yhy + */ +@Service +@Validated +@Slf4j +public class WbUnitInfoServiceImpl implements WbUnitInfoService { + + @Resource + private WbUnitInfoMapper wbUnitInfoMapper; + + @Resource + private WbCompileTreeMapper wbCompileTreeMapper; + + @Resource + private BoqCatalogItemMapper boqCatalogItemMapper; + + @Resource + private QuotaCatalogItemMapper quotaCatalogItemMapper; + + @Resource + private WbBoqDivisionMapper wbBoqDivisionMapper; + + @Resource + private QuotaItemMapper quotaItemMapper; + + @Resource + private QuotaCatalogTreeMapper quotaCatalogTreeMapper; + + @Resource + private WbProjectTreeMapper wbProjectTreeMapper; + + @Resource + private com.yhy.module.core.service.workbench.WbSnapshotService wbSnapshotService; + + @Resource + private com.yhy.module.core.service.workbench.WbBoqDivisionService wbBoqDivisionService; + + @Override + public WbUnitInfoRespVO getByCompileTreeId(Long compileTreeId) { + // 1. 校验节点存在且是单位工程节点 + validateUnitNode(compileTreeId); + + // 2. 查询单位工程信息 + WbUnitInfoDO unitInfo = wbUnitInfoMapper.selectByCompileTreeId(compileTreeId); + if (unitInfo == null) { + return null; + } + + // 3. 转换并填充关联名称 + WbUnitInfoRespVO respVO = WbUnitInfoConvert.INSTANCE.convert(unitInfo); + fillRelatedNames(respVO, unitInfo); + + return respVO; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long save(WbUnitInfoSaveReqVO saveReqVO) { + // 1. 校验节点存在且是单位工程节点 + WbCompileTreeDO node = validateUnitNode(saveReqVO.getCompileTreeId()); + + // 2. 校验工程编号唯一性 + validateUnitCodeUnique(node.getProjectId(), saveReqVO.getUnitCode(), saveReqVO.getId()); + + // 3. 校验并处理费率模式绑定(项目级锁定规则) + handleRateModeBinding(node.getProjectId(), saveReqVO); + + // 4. 查询是否已存在 + WbUnitInfoDO existInfo = wbUnitInfoMapper.selectByCompileTreeId(saveReqVO.getCompileTreeId()); + + if (existInfo != null) { + // 更新 + WbUnitInfoDO updateObj = WbUnitInfoConvert.INSTANCE.convert(saveReqVO); + updateObj.setId(existInfo.getId()); + wbUnitInfoMapper.updateById(updateObj); + + // 如果定额专业或费率模式变化,更新快照 + boolean quotaChanged = !java.util.Objects.equals(existInfo.getQuotaCatalogItemId(), saveReqVO.getQuotaCatalogItemId()); + boolean rateModeChanged = !java.util.Objects.equals(existInfo.getRateModeId(), saveReqVO.getRateModeId()); + if ((quotaChanged || rateModeChanged) && saveReqVO.getQuotaCatalogItemId() != null && saveReqVO.getRateModeId() != null) { + log.info("[save] 定额专业或费率模式变化,更新快照, compileTreeId={}, quotaCatalogItemId={}, rateModeId={}", + saveReqVO.getCompileTreeId(), saveReqVO.getQuotaCatalogItemId(), saveReqVO.getRateModeId()); + wbSnapshotService.deleteSnapshot(saveReqVO.getCompileTreeId()); + wbSnapshotService.createSnapshot(saveReqVO.getCompileTreeId(), saveReqVO.getQuotaCatalogItemId(), saveReqVO.getRateModeId()); + } + + return existInfo.getId(); + } else { + // 新增 + WbUnitInfoDO unitInfo = WbUnitInfoConvert.INSTANCE.convert(saveReqVO); + wbUnitInfoMapper.insert(unitInfo); + + // 创建快照 + if (saveReqVO.getQuotaCatalogItemId() != null && saveReqVO.getRateModeId() != null) { + log.info("[save] 新建单位工程,创建快照, compileTreeId={}, quotaCatalogItemId={}, rateModeId={}", + saveReqVO.getCompileTreeId(), saveReqVO.getQuotaCatalogItemId(), saveReqVO.getRateModeId()); + wbSnapshotService.createSnapshot(saveReqVO.getCompileTreeId(), saveReqVO.getQuotaCatalogItemId(), saveReqVO.getRateModeId()); + } + + // 导入分部分项模板(如果指定了专业类别 specialtyType) + if (cn.hutool.core.util.StrUtil.isNotBlank(saveReqVO.getSpecialtyType())) { + try { + Long catalogItemId = Long.parseLong(saveReqVO.getSpecialtyType()); + log.info("[save] 新建单位工程,导入分部分项模板, compileTreeId={}, catalogItemId={}", + saveReqVO.getCompileTreeId(), catalogItemId); + wbBoqDivisionService.importTemplate(saveReqVO.getCompileTreeId(), catalogItemId); + } catch (NumberFormatException e) { + log.warn("[save] specialtyType 不是有效的节点ID,跳过模板导入: {}", saveReqVO.getSpecialtyType()); + } + } + + return unitInfo.getId(); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteByCompileTreeId(Long compileTreeId) { + // 删除快照数据 + wbSnapshotService.deleteSnapshot(compileTreeId); + // 删除单位工程信息 + wbUnitInfoMapper.deleteByCompileTreeId(compileTreeId); + } + + @Override + public void validateUnitCodeUnique(Long projectId, String unitCode, Long selfId) { + // 查询项目下所有单位工程节点 + List allNodes = wbCompileTreeMapper.selectListByProjectId(projectId); + + // 过滤出单位工程节点 + for (WbCompileTreeDO node : allNodes) { + if (!WbCompileTreeDO.NODE_TYPE_UNIT.equals(node.getNodeType())) { + continue; + } + + // 查询该节点的单位工程信息 + WbUnitInfoDO unitInfo = wbUnitInfoMapper.selectByCompileTreeId(node.getId()); + if (unitInfo == null) { + continue; + } + + // 排除自身 + if (selfId != null && selfId.equals(unitInfo.getId())) { + continue; + } + + // 检查编号是否重复 + if (unitCode.equals(unitInfo.getUnitCode())) { + throw exception(WB_UNIT_INFO_CODE_EXISTS); + } + } + } + + /** + * 校验节点存在且是单位工程节点 + */ + private WbCompileTreeDO validateUnitNode(Long compileTreeId) { + WbCompileTreeDO node = wbCompileTreeMapper.selectById(compileTreeId); + if (node == null) { + throw exception(WB_COMPILE_TREE_NOT_EXISTS); + } + if (!WbCompileTreeDO.NODE_TYPE_UNIT.equals(node.getNodeType())) { + throw exception(WB_UNIT_INFO_NODE_NOT_UNIT); + } + return node; + } + + /** + * 处理费率模式绑定(项目级锁定规则) + * 规则:同一定额专业下,项目级只允许选择一个费率模式 + * - 如果项目已锁定该定额专业的费率模式,则强制使用已锁定的值 + * - 如果项目已绑定但未锁定,则校验是否一致 + * - 如果项目未绑定,则创建绑定关系并锁定 + */ + private void handleRateModeBinding(Long projectId, WbUnitInfoSaveReqVO saveReqVO) { + Long quotaCatalogItemId = saveReqVO.getQuotaCatalogItemId(); + Long rateModeId = saveReqVO.getRateModeId(); + + // 如果没有选择定额专业或费率模式,跳过 + if (quotaCatalogItemId == null || rateModeId == null) { + return; + } + + // 获取项目节点 + WbProjectTreeDO project = wbProjectTreeMapper.selectById(projectId); + if (project == null) { + log.warn("[handleRateModeBinding] 项目不存在, projectId={}", projectId); + return; + } + + // 检查是否已锁定 + boolean isLocked = project.isRateModeLocked(quotaCatalogItemId); + Long boundRateModeId = project.getRateModeIdByQuotaSpecialty(quotaCatalogItemId); + + if (isLocked && boundRateModeId != null) { + // 已锁定:强制使用已锁定的值 + if (!boundRateModeId.equals(rateModeId)) { + log.info("[handleRateModeBinding] 定额专业已锁定,强制使用已锁定的费率模式, projectId={}, quotaCatalogItemId={}, lockedRateModeId={}, requestedRateModeId={}", + projectId, quotaCatalogItemId, boundRateModeId, rateModeId); + // 强制覆盖请求中的值 + saveReqVO.setRateModeId(boundRateModeId); + } + } else if (boundRateModeId != null) { + // 已绑定但未锁定:校验是否一致 + if (!boundRateModeId.equals(rateModeId)) { + QuotaCatalogItemDO boundRateMode = quotaCatalogItemMapper.selectById(boundRateModeId); + String boundRateModeName = boundRateMode != null ? boundRateMode.getName() : String.valueOf(boundRateModeId); + throw exception0(400, "该定额专业已绑定费率模式【" + boundRateModeName + "】,请在项目级【费率及取费】页面切换"); + } + } else { + // 未绑定:创建绑定关系并锁定(首个单位工程) + QuotaCatalogItemDO quotaSpecialty = quotaCatalogItemMapper.selectById(quotaCatalogItemId); + String quotaSpecialtyName = quotaSpecialty != null ? quotaSpecialty.getName() : ""; + + QuotaCatalogItemDO rateMode = quotaCatalogItemMapper.selectById(rateModeId); + String rateModeName = rateMode != null ? rateMode.getName() : ""; + + // 绑定并锁定费率模式 + project.bindAndLockRateMode(quotaCatalogItemId, rateModeId, rateModeName, quotaSpecialtyName, saveReqVO.getId()); + wbProjectTreeMapper.updateById(project); + + log.info("[handleRateModeBinding] 项目绑定并锁定费率模式, projectId={}, quotaCatalogItemId={}, rateModeId={}", + projectId, quotaCatalogItemId, rateModeId); + } + } + + /** + * 填充关联名称 + */ + private void fillRelatedNames(WbUnitInfoRespVO respVO, WbUnitInfoDO unitInfo) { + // 清单数据库名称 + if (unitInfo.getBoqCatalogItemId() != null) { + BoqCatalogItemDO boqCatalogItem = boqCatalogItemMapper.selectById(unitInfo.getBoqCatalogItemId()); + if (boqCatalogItem != null) { + respVO.setBoqCatalogItemName(boqCatalogItem.getName()); + } + } + + // 定额数据库名称 + if (unitInfo.getQuotaCatalogItemId() != null) { + QuotaCatalogItemDO quotaCatalogItem = quotaCatalogItemMapper.selectById(unitInfo.getQuotaCatalogItemId()); + if (quotaCatalogItem != null) { + respVO.setQuotaCatalogItemName(quotaCatalogItem.getName()); + } + } + + // 执行费率文件名称 + if (unitInfo.getRateModeId() != null) { + QuotaCatalogItemDO rateMode = quotaCatalogItemMapper.selectById(unitInfo.getRateModeId()); + if (rateMode != null) { + respVO.setRateModeName(rateMode.getName()); + } + } + } + + // ==================== 单位工程级费率及取费相关方法 ==================== + + @Resource + private com.yhy.module.core.dal.mysql.workbench.WbUnitFeeSettingMapper wbUnitFeeSettingMapper; + + @Resource + private com.yhy.module.core.dal.mysql.workbench.WbUnifiedFeeConfigMapper wbUnifiedFeeConfigMapper; + + @Resource + private com.yhy.module.core.service.quota.QuotaItemService quotaItemService; + + @Override + public List> getUnitRateModeBindings(Long compileTreeId) { + // 1. 校验单位工程存在 + WbCompileTreeDO node = validateUnitNode(compileTreeId); + + // 2. 获取单位工程信息 + WbUnitInfoDO unitInfo = wbUnitInfoMapper.selectByCompileTreeId(compileTreeId); + if (unitInfo == null) { + return new ArrayList<>(); + } + + // 3. 获取费率模式绑定 + Map> bindings = unitInfo.getRateModeBindings(); + + // 4. 获取用户上次选择的定额专业ID + Long lastSelectedQuotaSpecialty = unitInfo.getLastSelectedQuotaSpecialty(); + String lastSelectedStr = lastSelectedQuotaSpecialty != null ? String.valueOf(lastSelectedQuotaSpecialty) : null; + + // 5. 转换为列表格式,确保所有ID为字符串类型避免前端大整数精度丢失 + List> result = new ArrayList<>(); + for (Map.Entry> entry : bindings.entrySet()) { + Map item = new HashMap<>(entry.getValue()); + item.put("quotaCatalogItemId", entry.getKey()); // 保持字符串类型 + // 确保 rateModeId 为字符串类型 + if (item.get("rateModeId") != null) { + item.put("rateModeId", String.valueOf(item.get("rateModeId"))); + } + // 标记是否为上次选择的定额专业 + item.put("isLastSelected", entry.getKey().equals(lastSelectedStr)); + result.add(item); + } + + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void switchUnitRateMode(Long compileTreeId, Long quotaCatalogItemId, Long rateModeId) { + // 1. 校验单位工程存在 + WbCompileTreeDO node = validateUnitNode(compileTreeId); + + // 2. 获取单位工程信息 + WbUnitInfoDO unitInfo = wbUnitInfoMapper.selectByCompileTreeId(compileTreeId); + if (unitInfo == null) { + throw exception(WB_UNIT_INFO_NOT_EXISTS); + } + + // 3. 校验费率模式存在且属于该定额专业 + QuotaCatalogItemDO rateMode = quotaCatalogItemMapper.selectById(rateModeId); + if (rateMode == null) { + throw exception0(400, "费率模式不存在"); + } + if (!"rate_mode".equals(rateMode.getNodeType())) { + throw exception0(400, "所选节点不是费率模式节点"); + } + if (!quotaCatalogItemId.equals(rateMode.getParentId())) { + throw exception0(400, "费率模式不属于该定额专业"); + } + + // 4. 获取定额专业名称 + QuotaCatalogItemDO quotaSpecialty = quotaCatalogItemMapper.selectById(quotaCatalogItemId); + String quotaSpecialtyName = quotaSpecialty != null ? quotaSpecialty.getName() : ""; + + // 5. 更新单位工程的费率模式绑定 + unitInfo.bindRateMode(quotaCatalogItemId, rateModeId, rateMode.getName(), quotaSpecialtyName); + + // 6. 如果是主定额专业,同步更新 rate_mode_id 字段 + if (quotaCatalogItemId.equals(unitInfo.getQuotaCatalogItemId())) { + unitInfo.setRateModeId(rateModeId); + log.info("[switchUnitRateMode] 同步更新单位工程主费率模式, compileTreeId={}, rateModeId={}", + compileTreeId, rateModeId); + } + + wbUnitInfoMapper.updateById(unitInfo); + + // 7. 获取该定额专业下所有旧的费率模式ID(用于清除统一取费) + List oldRateModeIds = quotaCatalogItemMapper.selectChildIds(quotaCatalogItemId); + // 从旧费率模式列表中移除新的费率模式 + oldRateModeIds.remove(rateModeId); + + // 8. 清除该单位工程下相关定额节点的单价构成覆写数据 + // 批量获取所有定额节点的 sourceQuotaItemId,然后批量查询对应的定额专业ID + int clearedFeeSettingCount = 0; + List quotaNodes = wbBoqDivisionMapper.selectQuotaNodesByCompileTreeId(compileTreeId); + + // 收集所有有效的 sourceQuotaItemId + Set sourceQuotaItemIds = new HashSet<>(); + for (WbBoqDivisionDO quotaNode : quotaNodes) { + if (quotaNode.getSourceQuotaItemId() != null) { + sourceQuotaItemIds.add(quotaNode.getSourceQuotaItemId()); + } + } + + // 批量查询定额基价 → 子目树 → 定额专业,建立 sourceQuotaItemId → specialtyId 映射 + Map quotaItemToSpecialtyMap = new HashMap<>(); + if (!sourceQuotaItemIds.isEmpty()) { + List quotaItems = quotaItemMapper.selectList(QuotaItemDO::getId, sourceQuotaItemIds); + Set catalogTreeIds = new HashSet<>(); + Map quotaItemToCatalogTree = new HashMap<>(); + for (QuotaItemDO item : quotaItems) { + if (item.getCatalogItemId() != null) { + catalogTreeIds.add(item.getCatalogItemId()); + quotaItemToCatalogTree.put(item.getId(), item.getCatalogItemId()); + } + } + if (!catalogTreeIds.isEmpty()) { + List catalogTrees = quotaCatalogTreeMapper.selectList(QuotaCatalogTreeDO::getId, catalogTreeIds); + Map catalogTreeToSpecialty = new HashMap<>(); + for (QuotaCatalogTreeDO tree : catalogTrees) { + if (tree.getCatalogItemId() != null) { + catalogTreeToSpecialty.put(tree.getId(), tree.getCatalogItemId()); + } + } + // 组装最终映射:sourceQuotaItemId → specialtyId + for (Map.Entry entry : quotaItemToCatalogTree.entrySet()) { + Long specialtyId = catalogTreeToSpecialty.get(entry.getValue()); + if (specialtyId != null) { + quotaItemToSpecialtyMap.put(entry.getKey(), specialtyId); + } + } + } + } + + // 根据映射清除属于当前定额专业的定额节点的单价构成 + for (WbBoqDivisionDO quotaNode : quotaNodes) { + if (quotaNode.getSourceQuotaItemId() != null) { + Long quotaSpecialtyId = quotaItemToSpecialtyMap.get(quotaNode.getSourceQuotaItemId()); + if (quotaCatalogItemId.equals(quotaSpecialtyId)) { + int deleted = wbUnitFeeSettingMapper.deleteByDivisionId(quotaNode.getId()); + clearedFeeSettingCount += deleted; + } + } + } + + // 9. 更新快照(切换费率模式) + wbSnapshotService.switchRateModeSnapshot(compileTreeId, rateModeId); + log.info("[switchUnitRateMode] 更新单位工程快照, compileTreeId={}, rateModeId={}", + compileTreeId, rateModeId); + + // 10. 清除该定额专业下旧费率模式的统一取费配置和节点 + int totalDeletedUnifiedFeeConfig = 0; + int totalDeletedUnifiedFeeNodes = 0; + for (Long oldRateModeId : oldRateModeIds) { + // 删除统一取费配置记录 + int deletedConfig = wbUnifiedFeeConfigMapper.deleteByRateModeId(compileTreeId, oldRateModeId); + totalDeletedUnifiedFeeConfig += deletedConfig; + // 删除统一取费节点 + int deletedNodes = wbBoqDivisionMapper.deleteUnifiedFeeNodesByRateModeId(compileTreeId, oldRateModeId); + totalDeletedUnifiedFeeNodes += deletedNodes; + } + + if (totalDeletedUnifiedFeeConfig > 0 || totalDeletedUnifiedFeeNodes > 0) { + log.info("[switchUnitRateMode] 清除统一取费, compileTreeId={}, quotaCatalogItemId={}, newRateModeId={}, deletedConfig={}, deletedNodes={}", + compileTreeId, quotaCatalogItemId, rateModeId, totalDeletedUnifiedFeeConfig, totalDeletedUnifiedFeeNodes); + } + + log.info("[switchUnitRateMode] 单位工程切换费率模式, compileTreeId={}, quotaCatalogItemId={}, rateModeId={}, clearedFeeSettings={}", + compileTreeId, quotaCatalogItemId, rateModeId, clearedFeeSettingCount); + } + + @Override + public Map getUnitRateConfig(Long compileTreeId, Long quotaCatalogItemId) { + // 1. 校验单位工程存在 + validateUnitNode(compileTreeId); + + // 2. 获取单位工程信息 + WbUnitInfoDO unitInfo = wbUnitInfoMapper.selectByCompileTreeId(compileTreeId); + if (unitInfo == null) { + return new HashMap<>(); + } + + // 3. 获取绑定信息 + Map binding = unitInfo.getRateModeBinding(quotaCatalogItemId); + if (binding == null) { + return new HashMap<>(); + } + + // 4. 返回完整的费率配置 + Map result = new HashMap<>(binding); + result.put("quotaCatalogItemId", String.valueOf(quotaCatalogItemId)); + + // 确保 rateModeId 为字符串类型,避免前端大整数精度丢失 + if (result.get("rateModeId") != null) { + result.put("rateModeId", String.valueOf(result.get("rateModeId"))); + } + + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveUnitRateConfig(Long compileTreeId, Long quotaCatalogItemId, Long rateModeId, + Map rateSettings, Map feeSettings) { + // 1. 校验单位工程存在 + validateUnitNode(compileTreeId); + + // 2. 获取单位工程信息 + WbUnitInfoDO unitInfo = wbUnitInfoMapper.selectByCompileTreeId(compileTreeId); + if (unitInfo == null) { + throw exception(WB_UNIT_INFO_NOT_EXISTS); + } + + // 3. 如果传入了rateModeId,更新费率模式绑定 + if (rateModeId != null) { + Long currentRateModeId = unitInfo.getRateModeIdByQuotaSpecialty(quotaCatalogItemId); + if (currentRateModeId == null || !currentRateModeId.equals(rateModeId)) { + // 费率模式发生变化,需要切换 + switchUnitRateMode(compileTreeId, quotaCatalogItemId, rateModeId); + // 重新获取unitInfo对象 + unitInfo = wbUnitInfoMapper.selectByCompileTreeId(compileTreeId); + } else { + // 费率模式未变化,但仍需更新bindTime + QuotaCatalogItemDO rateMode = quotaCatalogItemMapper.selectById(rateModeId); + QuotaCatalogItemDO quotaSpecialty = quotaCatalogItemMapper.selectById(quotaCatalogItemId); + String rateModeName = rateMode != null ? rateMode.getName() : ""; + String quotaSpecialtyName = quotaSpecialty != null ? quotaSpecialty.getName() : ""; + unitInfo.bindRateMode(quotaCatalogItemId, rateModeId, rateModeName, quotaSpecialtyName); + } + } + + // 4. 保存费率覆写值 + if (rateSettings != null) { + unitInfo.saveRateSettings(quotaCatalogItemId, rateSettings); + } + + // 5. 保存取费覆写值 + if (feeSettings != null) { + unitInfo.saveFeeSettings(quotaCatalogItemId, feeSettings); + } + + // 6. 记录用户上次选择的定额专业(用于页面刷新时回显) + unitInfo.setLastSelectedQuotaSpecialty(quotaCatalogItemId); + + wbUnitInfoMapper.updateById(unitInfo); + + log.info("[saveUnitRateConfig] 保存单位工程费率配置, compileTreeId={}, quotaCatalogItemId={}, rateModeId={}", + compileTreeId, quotaCatalogItemId, rateModeId); + } + + @Override + public List getUsedQuotaSpecialtiesByUnit(Long compileTreeId) { + // 1. 校验单位工程存在 + validateUnitNode(compileTreeId); + + // 2. 获取单位工程信息(用于获取已绑定的定额专业) + WbUnitInfoDO unitInfo = wbUnitInfoMapper.selectByCompileTreeId(compileTreeId); + + // 3. 从绑定关系中获取已绑定的定额专业ID集合 + Set specialtyIds = new HashSet<>(); + if (unitInfo != null && unitInfo.getRateModeBindings() != null) { + for (String key : unitInfo.getRateModeBindings().keySet()) { + try { + specialtyIds.add(Long.parseLong(key)); + } catch (NumberFormatException e) { + log.warn("[getUsedQuotaSpecialtiesByUnit] 无效的定额专业ID格式: {}", key); + } + } + } + + // 4. 查询该单位工程下的分部分项定额节点(有sourceQuotaItemId的),补充定额专业 + Set quotaItemIds = new HashSet<>(); + List quotaNodes = wbBoqDivisionMapper.selectQuotaNodesByCompileTreeId(compileTreeId); + for (WbBoqDivisionDO node : quotaNodes) { + if (node.getSourceQuotaItemId() != null) { + quotaItemIds.add(node.getSourceQuotaItemId()); + } + } + + // 5. 批量查询定额基价,获取 catalogItemId(子目树节点ID) + if (!quotaItemIds.isEmpty()) { + List quotaItems = quotaItemMapper.selectList(QuotaItemDO::getId, quotaItemIds); + Set catalogTreeIds = new HashSet<>(); + Map quotaItemToCatalogTree = new HashMap<>(); // quotaItemId -> catalogTreeId + for (QuotaItemDO item : quotaItems) { + if (item.getCatalogItemId() != null) { + catalogTreeIds.add(item.getCatalogItemId()); + quotaItemToCatalogTree.put(item.getId(), item.getCatalogItemId()); + } + } + + // 6. 批量查询子目树节点,获取 catalogItemId(定额专业节点ID) + if (!catalogTreeIds.isEmpty()) { + List catalogTrees = quotaCatalogTreeMapper.selectList(QuotaCatalogTreeDO::getId, catalogTreeIds); + for (QuotaCatalogTreeDO tree : catalogTrees) { + if (tree.getCatalogItemId() != null) { + specialtyIds.add(tree.getCatalogItemId()); + } + } + } + } + + if (specialtyIds.isEmpty()) { + return new ArrayList<>(); + } + + // 7. 批量查询定额专业节点 + List specialties = quotaCatalogItemMapper.selectList(QuotaCatalogItemDO::getId, specialtyIds); + Map specialtyMap = new HashMap<>(); + for (QuotaCatalogItemDO specialty : specialties) { + specialtyMap.put(specialty.getId(), specialty.getName()); + } + + // 8. 获取每个定额专业下的费率模式节点 + List result = new ArrayList<>(); + for (Map.Entry entry : specialtyMap.entrySet()) { + Long specialtyId = entry.getKey(); + String specialtyName = entry.getValue(); + + // 查询该定额专业下的所有费率模式节点 + List rateModes = quotaCatalogItemMapper.selectByParentIdAndNodeType(specialtyId, "rate_mode"); + for (QuotaCatalogItemDO rateMode : rateModes) { + result.add(WbUsedQuotaSpecialtyRespVO.builder() + .quotaCatalogItemId(specialtyId) + .quotaCatalogItemName(specialtyName) + .rateModeId(rateMode.getId()) + .rateModeName(rateMode.getName()) + .unitCount(0) + .compileTreeId(compileTreeId) + .build()); + } + } + + return result; + } + + @Override + public List getUsedQuotaSpecialtiesByProject(Long projectId) { + // 查询项目下所有单位工程节点 + List allNodes = wbCompileTreeMapper.selectListByProjectId(projectId); + List unitNodes = allNodes.stream() + .filter(n -> "unit".equals(n.getNodeType())) + .collect(java.util.stream.Collectors.toList()); + + if (unitNodes.isEmpty()) { + return new ArrayList<>(); + } + + // 遍历每个单位工程,收集定额专业(按 quotaCatalogItemId + rateModeId 去重) + Map deduplicatedMap = new java.util.LinkedHashMap<>(); + for (WbCompileTreeDO unitNode : unitNodes) { + try { + List unitSpecialties = getUsedQuotaSpecialtiesByUnit(unitNode.getId()); + for (WbUsedQuotaSpecialtyRespVO vo : unitSpecialties) { + String key = vo.getQuotaCatalogItemId() + "_" + vo.getRateModeId(); + deduplicatedMap.putIfAbsent(key, vo); + } + } catch (Exception e) { + log.warn("[getUsedQuotaSpecialtiesByProject] 查询单位工程定额专业失败,compileTreeId={}", unitNode.getId(), e); + } + } + + return new ArrayList<>(deduplicatedMap.values()); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/util/AdjustmentFormulaCalculator.java b/yhy-module-core/src/main/java/com/yhy/module/core/util/AdjustmentFormulaCalculator.java new file mode 100644 index 0000000..2efe28b --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/util/AdjustmentFormulaCalculator.java @@ -0,0 +1,230 @@ +package com.yhy.module.core.util; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 调整公式计算工具类 + * 用于计算工料机的调整公式(虚拟字段) + * 后台和工作台共用此逻辑 + * + * @author yhy + */ +public class AdjustmentFormulaCalculator { + + /** + * 计算调整公式 + * + * @param resources 工料机列表(需要包含 code 和 categoryId) + * @param settings 调整设置列表 + * @param codeGetter 获取工料机编码的函数 + * @param categoryIdGetter 获取工料机类别ID的函数 + * @param formulaSetter 设置调整公式的函数 + * @param 工料机类型 + */ + public static void calculateFormulas( + List resources, + List settings, + java.util.function.Function codeGetter, + java.util.function.Function categoryIdGetter, + java.util.function.BiConsumer formulaSetter) { + + if (resources == null || resources.isEmpty() || settings == null || settings.isEmpty()) { + return; + } + + // 1. 初始化公式映射(工料机编码 -> 公式) + Map formulaMap = new HashMap<>(); + for (T vo : resources) { + String code = codeGetter.apply(vo); + if (code != null) { + formulaMap.put(code, "x"); + } + } + + // 2. 按类别分组工料机 + Map> resourceCategoryMap = new HashMap<>(); + for (T vo : resources) { + Long categoryId = categoryIdGetter.apply(vo); + if (categoryId != null) { + resourceCategoryMap.computeIfAbsent(categoryId, k -> new ArrayList<>()).add(vo); + } + } + + // 3. 遍历调整设置,计算公式 + for (AdjustmentSetting setting : settings) { + String adjustmentContent = setting.getAdjustmentContent(); + Map adjustmentRules = setting.getAdjustmentRules(); + + if (adjustmentRules == null || adjustmentRules.isEmpty()) { + continue; + } + + String type = (String) adjustmentRules.get("type"); + @SuppressWarnings("unchecked") + List> items = (List>) adjustmentRules.get("items"); + + if (items == null || items.isEmpty() || type == null) { + continue; + } + + switch (type) { + case "adjust_coefficient": + // 调整类别系数:x * 系数 + if (!"true".equalsIgnoreCase(adjustmentContent)) { + break; + } + for (Map item : items) { + Object categoryIdObj = item.get("categoryId"); + if (categoryIdObj == null) { + categoryIdObj = item.get("category"); + } + Object coefficientObj = item.get("coefficient"); + if (categoryIdObj == null || coefficientObj == null) continue; + + Long categoryId = Long.parseLong(categoryIdObj.toString()); + BigDecimal coefficient = new BigDecimal(coefficientObj.toString()); + String coefficientStr = formatNumber(coefficient); + + List categoryResources = resourceCategoryMap.get(categoryId); + if (categoryResources != null) { + for (T vo : categoryResources) { + String code = codeGetter.apply(vo); + if (code != null && formulaMap.containsKey(code)) { + String currentFormula = formulaMap.get(code); + formulaMap.put(code, "(" + currentFormula + ")*" + coefficientStr); + } + } + } + } + break; + + case "consumption_adjustment": + // 增减材料消耗量:x + 增减值 + if (!"true".equalsIgnoreCase(adjustmentContent)) { + break; + } + for (Map item : items) { + Object codeObj = item.get("code"); + Object consumptionValueObj = item.get("consumptionValue"); + if (consumptionValueObj == null) { + consumptionValueObj = item.get("consumption"); + } + if (codeObj == null || consumptionValueObj == null) continue; + + String code = codeObj.toString(); + BigDecimal consumptionValue = new BigDecimal(consumptionValueObj.toString()); + + if (formulaMap.containsKey(code)) { + String currentFormula = formulaMap.get(code); + String sign = consumptionValue.compareTo(BigDecimal.ZERO) >= 0 ? "+" : ""; + formulaMap.put(code, "(" + currentFormula + ")" + sign + formatNumber(consumptionValue)); + } + } + break; + + case "dynamic_adjust": + // 动态调整类别系数:x * 系数 * A + for (Map item : items) { + Object categoryObj = item.get("category"); + if (categoryObj == null) { + categoryObj = item.get("categoryId"); + } + Object inputValueObj = item.get("inputValue"); + Object baseValueObj = item.get("baseValue"); + Object denominatorValueObj = item.get("denominatorValue"); + Object coefficientSettingObj = item.get("coefficientSetting"); + Object valueRuleObj = item.get("valueRule"); + + if (categoryObj == null || inputValueObj == null) continue; + + Long categoryId = Long.parseLong(categoryObj.toString()); + BigDecimal inputValue = new BigDecimal(inputValueObj.toString()); + BigDecimal baseValue = baseValueObj != null ? new BigDecimal(baseValueObj.toString()) : BigDecimal.ZERO; + BigDecimal denominatorValue = denominatorValueObj != null ? new BigDecimal(denominatorValueObj.toString()) : BigDecimal.ONE; + BigDecimal coefficientSetting = coefficientSettingObj != null ? new BigDecimal(coefficientSettingObj.toString()) : BigDecimal.ONE; + String valueRule = valueRuleObj != null ? valueRuleObj.toString() : "original"; + + // 计算 A = (输入值 - 基础值) / 分母值 + BigDecimal A = inputValue.subtract(baseValue); + if (denominatorValue.compareTo(BigDecimal.ZERO) != 0) { + A = A.divide(denominatorValue, 6, RoundingMode.HALF_UP); + } + A = applyValueRule(A, valueRule); + + List categoryResources = resourceCategoryMap.get(categoryId); + if (categoryResources != null) { + for (T vo : categoryResources) { + String code = codeGetter.apply(vo); + if (code != null && formulaMap.containsKey(code)) { + String currentFormula = formulaMap.get(code); + formulaMap.put(code, "(" + currentFormula + ")*" + formatNumber(coefficientSetting) + "*" + formatNumber(A)); + } + } + } + } + break; + + case "dynamic_merge": + // 动态合并定额:暂不处理公式(需要查询其他定额的工料机数据) + break; + + default: + break; + } + } + + // 4. 将公式设置到对应的VO + for (T vo : resources) { + String code = codeGetter.apply(vo); + if (code != null && formulaMap.containsKey(code)) { + String formula = formulaMap.get(code); + if (!"x".equals(formula)) { + formulaSetter.accept(vo, formula); + } + } + } + } + + /** + * 格式化数字,去除尾部多余的0 + */ + public static String formatNumber(BigDecimal number) { + if (number == null) { + return "0"; + } + return number.stripTrailingZeros().toPlainString(); + } + + /** + * 应用值规则 + */ + public static BigDecimal applyValueRule(BigDecimal value, String valueRule) { + if (value == null || valueRule == null) { + return value; + } + switch (valueRule) { + case "round_up": + return value.setScale(0, RoundingMode.CEILING); + case "round_down": + return value.setScale(0, RoundingMode.FLOOR); + case "round": + return value.setScale(0, RoundingMode.HALF_UP); + default: + return value; + } + } + + /** + * 调整设置接口 + * 后台和工作台的调整设置DO都需要实现此接口 + */ + public interface AdjustmentSetting { + String getAdjustmentContent(); + Map getAdjustmentRules(); + } +} diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/util/FormulaEvaluator.java b/yhy-module-core/src/main/java/com/yhy/module/core/util/FormulaEvaluator.java index d1e1a61..1f5d83c 100644 --- a/yhy-module-core/src/main/java/com/yhy/module/core/util/FormulaEvaluator.java +++ b/yhy-module-core/src/main/java/com/yhy/module/core/util/FormulaEvaluator.java @@ -107,13 +107,27 @@ public class FormulaEvaluator { continue; } - // 处理数字(包括小数) + // 处理数字(包括小数和科学计数法,如 1.5E-5) if (Character.isDigit(c) || c == '.') { StringBuilder sb = new StringBuilder(); - while (i < expression.length() && - (Character.isDigit(expression.charAt(i)) || expression.charAt(i) == '.')) { - sb.append(expression.charAt(i)); - i++; + while (i < expression.length()) { + char ch = expression.charAt(i); + // 支持数字、小数点、科学计数法(E/e)及其后的正负号 + if (Character.isDigit(ch) || ch == '.') { + sb.append(ch); + i++; + } else if ((ch == 'E' || ch == 'e') && sb.length() > 0) { + // 科学计数法 + sb.append(ch); + i++; + // 处理指数部分的正负号 + if (i < expression.length() && (expression.charAt(i) == '+' || expression.charAt(i) == '-')) { + sb.append(expression.charAt(i)); + i++; + } + } else { + break; + } } numbers.push(new BigDecimal(sb.toString())); continue; diff --git a/yhy-module-core/src/main/java/com/yhy/module/core/util/ResourcePriceCalculator.java b/yhy-module-core/src/main/java/com/yhy/module/core/util/ResourcePriceCalculator.java new file mode 100644 index 0000000..4c568c4 --- /dev/null +++ b/yhy-module-core/src/main/java/com/yhy/module/core/util/ResourcePriceCalculator.java @@ -0,0 +1,367 @@ +package com.yhy.module.core.util; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Function; +import lombok.extern.slf4j.Slf4j; + +/** + * 工料机价格计算工具类 + * 后台(QuotaResourceServiceImpl)和工作台(WbBoqResourceServiceImpl)共用此逻辑 + * + * 设计模式:函数式工具类 + FieldAccessor Builder + * 与 AdjustmentFormulaCalculator 保持一致的设计风格 + * + * @author yhy + */ +@Slf4j +public class ResourcePriceCalculator { + + /** + * 字段访问器,用于泛型访问不同 VO 类型的相同语义字段 + * 后台 VO 字段名:resourceUnit / resourceCategoryId / dosage / adjustedDosage + * 工作台 VO 字段名:unit / categoryId / consumeQty / adjustConsumeQty + */ + public static class FieldAccessor { + private final Function getUnit; + private final Function getCategoryId; + private final Function getEffectiveDosage; + private final Function> getCalcBase; + private final Function getTaxExclBaseTotalSum; + private final Function getTaxInclBaseTotalSum; + private final Function getTaxExclCompileTotalSum; + private final Function getTaxInclCompileTotalSum; + private final BiConsumer setTaxExclBaseTotalSum; + private final BiConsumer setTaxInclBaseTotalSum; + private final BiConsumer setTaxExclCompileTotalSum; + private final BiConsumer setTaxInclCompileTotalSum; + + private FieldAccessor(Builder builder) { + this.getUnit = builder.getUnit; + this.getCategoryId = builder.getCategoryId; + this.getEffectiveDosage = builder.getEffectiveDosage; + this.getCalcBase = builder.getCalcBase; + this.getTaxExclBaseTotalSum = builder.getTaxExclBaseTotalSum; + this.getTaxInclBaseTotalSum = builder.getTaxInclBaseTotalSum; + this.getTaxExclCompileTotalSum = builder.getTaxExclCompileTotalSum; + this.getTaxInclCompileTotalSum = builder.getTaxInclCompileTotalSum; + this.setTaxExclBaseTotalSum = builder.setTaxExclBaseTotalSum; + this.setTaxInclBaseTotalSum = builder.setTaxInclBaseTotalSum; + this.setTaxExclCompileTotalSum = builder.setTaxExclCompileTotalSum; + this.setTaxInclCompileTotalSum = builder.setTaxInclCompileTotalSum; + } + + public static Builder builder() { + return new Builder<>(); + } + + public static class Builder { + private Function getUnit; + private Function getCategoryId; + private Function getEffectiveDosage; + private Function> getCalcBase; + private Function getTaxExclBaseTotalSum; + private Function getTaxInclBaseTotalSum; + private Function getTaxExclCompileTotalSum; + private Function getTaxInclCompileTotalSum; + private BiConsumer setTaxExclBaseTotalSum; + private BiConsumer setTaxInclBaseTotalSum; + private BiConsumer setTaxExclCompileTotalSum; + private BiConsumer setTaxInclCompileTotalSum; + + public Builder getUnit(Function getUnit) { + this.getUnit = getUnit; + return this; + } + + public Builder getCategoryId(Function getCategoryId) { + this.getCategoryId = getCategoryId; + return this; + } + + public Builder getEffectiveDosage(Function getEffectiveDosage) { + this.getEffectiveDosage = getEffectiveDosage; + return this; + } + + public Builder getCalcBase(Function> getCalcBase) { + this.getCalcBase = getCalcBase; + return this; + } + + public Builder getTaxExclBaseTotalSum(Function getTaxExclBaseTotalSum) { + this.getTaxExclBaseTotalSum = getTaxExclBaseTotalSum; + return this; + } + + public Builder getTaxInclBaseTotalSum(Function getTaxInclBaseTotalSum) { + this.getTaxInclBaseTotalSum = getTaxInclBaseTotalSum; + return this; + } + + public Builder getTaxExclCompileTotalSum(Function getTaxExclCompileTotalSum) { + this.getTaxExclCompileTotalSum = getTaxExclCompileTotalSum; + return this; + } + + public Builder getTaxInclCompileTotalSum(Function getTaxInclCompileTotalSum) { + this.getTaxInclCompileTotalSum = getTaxInclCompileTotalSum; + return this; + } + + public Builder setTaxExclBaseTotalSum(BiConsumer setTaxExclBaseTotalSum) { + this.setTaxExclBaseTotalSum = setTaxExclBaseTotalSum; + return this; + } + + public Builder setTaxInclBaseTotalSum(BiConsumer setTaxInclBaseTotalSum) { + this.setTaxInclBaseTotalSum = setTaxInclBaseTotalSum; + return this; + } + + public Builder setTaxExclCompileTotalSum(BiConsumer setTaxExclCompileTotalSum) { + this.setTaxExclCompileTotalSum = setTaxExclCompileTotalSum; + return this; + } + + public Builder setTaxInclCompileTotalSum(BiConsumer setTaxInclCompileTotalSum) { + this.setTaxInclCompileTotalSum = setTaxInclCompileTotalSum; + return this; + } + + public FieldAccessor build() { + return new FieldAccessor<>(this); + } + } + } + + /** + * 计算单位为%的工料机合价 + * + * 算法: + * 1. 按 categoryId 汇总非%工料机的四个合价(分类总和) + * 2. 遍历%工料机,用 calcBase.formula 和 calcBase.variables 计算公式结果 + * 3. 合价 = 公式结果 × (消耗量 / 100) + * + * @param topLevelResources 顶层工料机列表(用于构建分类总和,不含子工料机) + * @param allResources 所有工料机列表(含子工料机,用于计算%合价) + * @param accessor 字段访问器 + * @param 工料机 VO 类型 + */ + @SuppressWarnings("unchecked") + public static void calculatePercentUnitTotalSums( + List topLevelResources, + List allResources, + FieldAccessor accessor) { + + if (topLevelResources == null || topLevelResources.isEmpty()) { + return; + } + + // 1. 构建类别价格映射表(跳过单位为%的工料机) + // 复合工料机(有children)取父级合价,子工料机不单独累加 + Map categoryPriceMap = new HashMap<>(); + + for (T vo : topLevelResources) { + // 跳过单位为 % 的工料机 + if ("%".equals(accessor.getUnit.apply(vo))) { + continue; + } + + Long categoryId = accessor.getCategoryId.apply(vo); + if (categoryId != null) { + BigDecimal[] sums = categoryPriceMap.computeIfAbsent(categoryId, + k -> new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO}); + + BigDecimal taxExclBase = accessor.getTaxExclBaseTotalSum.apply(vo); + BigDecimal taxInclBase = accessor.getTaxInclBaseTotalSum.apply(vo); + BigDecimal taxExclCompile = accessor.getTaxExclCompileTotalSum.apply(vo); + BigDecimal taxInclCompile = accessor.getTaxInclCompileTotalSum.apply(vo); + + if (taxExclBase != null) sums[0] = sums[0].add(taxExclBase); + if (taxInclBase != null) sums[1] = sums[1].add(taxInclBase); + if (taxExclCompile != null) sums[2] = sums[2].add(taxExclCompile); + if (taxInclCompile != null) sums[3] = sums[3].add(taxInclCompile); + } + } + + // 2. 处理单位为 % 的工料机 + for (T vo : allResources) { + if (!"%".equals(accessor.getUnit.apply(vo))) { + continue; + } + + Map calcBase = accessor.getCalcBase.apply(vo); + if (calcBase == null || !calcBase.containsKey("formula") || !calcBase.containsKey("variables")) { + continue; + } + + try { + String formula = (String) calcBase.get("formula"); + Map variablesConfig = (Map) calcBase.get("variables"); + + // 构建四组变量值映射表 + Map taxExclBaseVars = new HashMap<>(); + Map taxInclBaseVars = new HashMap<>(); + Map taxExclCompileVars = new HashMap<>(); + Map taxInclCompileVars = new HashMap<>(); + + for (Map.Entry varEntry : variablesConfig.entrySet()) { + String varName = varEntry.getKey(); + Long categoryId = Long.valueOf(varEntry.getValue().toString()); + BigDecimal[] sums = categoryPriceMap.get(categoryId); + + if (sums == null) { + log.debug("[calculatePercentUnitTotalSums] 类别数据为空,使用0作为默认值,categoryId={}", categoryId); + taxExclBaseVars.put(varName, BigDecimal.ZERO); + taxInclBaseVars.put(varName, BigDecimal.ZERO); + taxExclCompileVars.put(varName, BigDecimal.ZERO); + taxInclCompileVars.put(varName, BigDecimal.ZERO); + } else { + taxExclBaseVars.put(varName, sums[0]); + taxInclBaseVars.put(varName, sums[1]); + taxExclCompileVars.put(varName, sums[2]); + taxInclCompileVars.put(varName, sums[3]); + } + } + + // 计算公式结果 + BigDecimal taxExclBaseResult = FormulaEvaluator.evaluate(formula, taxExclBaseVars); + BigDecimal taxInclBaseResult = FormulaEvaluator.evaluate(formula, taxInclBaseVars); + BigDecimal taxExclCompileResult = FormulaEvaluator.evaluate(formula, taxExclCompileVars); + BigDecimal taxInclCompileResult = FormulaEvaluator.evaluate(formula, taxInclCompileVars); + + // 合价 = 公式结果 × (消耗量 / 100) + BigDecimal dosage = accessor.getEffectiveDosage.apply(vo); + if (dosage != null) { + BigDecimal factor = dosage.divide(new BigDecimal("100"), 6, RoundingMode.HALF_UP); + accessor.setTaxExclBaseTotalSum.accept(vo, taxExclBaseResult.multiply(factor).setScale(4, RoundingMode.HALF_UP)); + accessor.setTaxInclBaseTotalSum.accept(vo, taxInclBaseResult.multiply(factor).setScale(4, RoundingMode.HALF_UP)); + accessor.setTaxExclCompileTotalSum.accept(vo, taxExclCompileResult.multiply(factor).setScale(4, RoundingMode.HALF_UP)); + accessor.setTaxInclCompileTotalSum.accept(vo, taxInclCompileResult.multiply(factor).setScale(4, RoundingMode.HALF_UP)); + } + } catch (Exception e) { + log.error("[calculatePercentUnitTotalSums] 计算单位为%的工料机合价失败,error={}", e.getMessage(), e); + } + } + } + + /** + * 重新计算合价(价格 × 有效消耗量) + * 跳过复合工料机(已汇总子项合价)和单位为%的工料机(由公式计算) + * + * @param resources 所有工料机列表 + * @param accessor 字段访问器 + * @param isMergedChecker 判断是否为复合工料机的函数 + * @param priceGetters 四个价格字段的 getter [taxExclBase, taxInclBase, taxExclCompile, taxInclCompile] + * @param 工料机 VO 类型 + */ + @SafeVarargs + public static void recalculateTotalSums( + List resources, + FieldAccessor accessor, + Function isMergedChecker, + Function... priceGetters) { + + if (resources == null || resources.isEmpty() || priceGetters.length != 4) { + return; + } + + for (T vo : resources) { + BigDecimal effectiveDosage = accessor.getEffectiveDosage.apply(vo); + if (effectiveDosage == null) { + continue; + } + + // 跳过复合工料机和单位为%的工料机 + Boolean isMerged = isMergedChecker.apply(vo); + if (isMerged != null && isMerged) { + continue; + } + if ("%".equals(accessor.getUnit.apply(vo))) { + continue; + } + + accessor.setTaxExclBaseTotalSum.accept(vo, multiplyOrZero(priceGetters[0].apply(vo), effectiveDosage)); + accessor.setTaxInclBaseTotalSum.accept(vo, multiplyOrZero(priceGetters[1].apply(vo), effectiveDosage)); + accessor.setTaxExclCompileTotalSum.accept(vo, multiplyOrZero(priceGetters[2].apply(vo), effectiveDosage)); + accessor.setTaxInclCompileTotalSum.accept(vo, multiplyOrZero(priceGetters[3].apply(vo), effectiveDosage)); + } + } + + /** + * 计算复合工料机父级的四个合价 + * 1. 先用兄弟级类别合价计算%子工料机的合价 + * 2. 再汇总所有子项合价到父级 + * + * 适用于父子类型相同的场景(如工作台 WbBoqResourceRespVO) + * + * @param parent 父级工料机 + * @param children 子工料机列表 + * @param accessor 字段访问器(父子共用) + * @param 工料机 VO 类型 + */ + public static void calculateMergedParentTotalSums( + T parent, List children, FieldAccessor accessor) { + if (children == null || children.isEmpty()) { + return; + } + // 1. 先计算%子工料机的合价(使用兄弟级类别合价) + calculatePercentUnitTotalSums(children, children, accessor); + // 2. 汇总所有子项合价到父级 + sumChildrenToParent(parent, children, accessor, accessor); + } + + /** + * 汇总子项合价到父级(支持父子不同类型) + * + * @param parent 父级工料机 + * @param children 子工料机列表 + * @param childAccessor 子级字段访问器(只用 getter) + * @param parentAccessor 父级字段访问器(只用 setter) + * @param

父级类型 + * @param 子级类型 + */ + public static void sumChildrenToParent( + P parent, List children, + FieldAccessor childAccessor, + FieldAccessor

parentAccessor) { + if (children == null || children.isEmpty()) { + return; + } + BigDecimal totalTaxExclBase = BigDecimal.ZERO; + BigDecimal totalTaxInclBase = BigDecimal.ZERO; + BigDecimal totalTaxExclCompile = BigDecimal.ZERO; + BigDecimal totalTaxInclCompile = BigDecimal.ZERO; + + for (C child : children) { + BigDecimal v0 = childAccessor.getTaxExclBaseTotalSum.apply(child); + BigDecimal v1 = childAccessor.getTaxInclBaseTotalSum.apply(child); + BigDecimal v2 = childAccessor.getTaxExclCompileTotalSum.apply(child); + BigDecimal v3 = childAccessor.getTaxInclCompileTotalSum.apply(child); + if (v0 != null) totalTaxExclBase = totalTaxExclBase.add(v0); + if (v1 != null) totalTaxInclBase = totalTaxInclBase.add(v1); + if (v2 != null) totalTaxExclCompile = totalTaxExclCompile.add(v2); + if (v3 != null) totalTaxInclCompile = totalTaxInclCompile.add(v3); + } + + parentAccessor.setTaxExclBaseTotalSum.accept(parent, totalTaxExclBase.setScale(4, RoundingMode.HALF_UP)); + parentAccessor.setTaxInclBaseTotalSum.accept(parent, totalTaxInclBase.setScale(4, RoundingMode.HALF_UP)); + parentAccessor.setTaxExclCompileTotalSum.accept(parent, totalTaxExclCompile.setScale(4, RoundingMode.HALF_UP)); + parentAccessor.setTaxInclCompileTotalSum.accept(parent, totalTaxInclCompile.setScale(4, RoundingMode.HALF_UP)); + } + + /** + * 安全乘法:如果 value 为 null 返回 ZERO,否则返回 value * factor + */ + public static BigDecimal multiplyOrZero(BigDecimal value, BigDecimal factor) { + if (value == null) { + return BigDecimal.ZERO; + } + return value.multiply(factor); + } +} diff --git a/yhy-module-core/src/main/resources/mapper/quota/QuotaItemMapper.xml b/yhy-module-core/src/main/resources/mapper/quota/QuotaItemMapper.xml new file mode 100644 index 0000000..47cb240 --- /dev/null +++ b/yhy-module-core/src/main/resources/mapper/quota/QuotaItemMapper.xml @@ -0,0 +1,26 @@ + + + + + + + UPDATE yhy_quota_item + SET tax_excl_base_price = #{taxExclBasePrice}, + tax_incl_base_price = #{taxInclBasePrice}, + tax_excl_compile_price = #{taxExclCompilePrice}, + tax_incl_compile_price = #{taxInclCompilePrice}, + update_time = NOW() + WHERE id = #{quotaItemId} + AND deleted = 0 + + + + + + diff --git a/yhy-module-core/src/main/resources/mapper/quota/QuotaVariableSettingMapper.xml b/yhy-module-core/src/main/resources/mapper/quota/QuotaVariableSettingMapper.xml new file mode 100644 index 0000000..4bd23c7 --- /dev/null +++ b/yhy-module-core/src/main/resources/mapper/quota/QuotaVariableSettingMapper.xml @@ -0,0 +1,16 @@ + + + + + + + UPDATE yhy_quota_variable_setting + SET sort_order = sort_order + 1, + update_time = NOW() + WHERE catalog_item_id = #{catalogItemId} + AND category = #{category} + AND sort_order >= #{sortOrder} + AND deleted = 0 + + + diff --git a/yhy-module-core/src/main/resources/mapper/resource/ResourceItemMapper.xml b/yhy-module-core/src/main/resources/mapper/resource/ResourceItemMapper.xml new file mode 100644 index 0000000..31ae2e1 --- /dev/null +++ b/yhy-module-core/src/main/resources/mapper/resource/ResourceItemMapper.xml @@ -0,0 +1,15 @@ + + + + + + + UPDATE yhy_resource_item + SET sort_order = sort_order + 1, + update_time = NOW() + WHERE catalog_item_id = #{catalogItemId} + AND sort_order >= #{sortOrder} + AND deleted = 0 + + + diff --git a/yhy-module-core/src/main/resources/mapper/resource/ResourceMergedMapper.xml b/yhy-module-core/src/main/resources/mapper/resource/ResourceMergedMapper.xml new file mode 100644 index 0000000..7b22ffa --- /dev/null +++ b/yhy-module-core/src/main/resources/mapper/resource/ResourceMergedMapper.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/yhy-module-core/src/test/java/com/yhy/module/core/service/quota/QuotaRateFieldLabelServiceTest.java b/yhy-module-core/src/test/java/com/yhy/module/core/service/quota/QuotaRateFieldLabelServiceTest.java deleted file mode 100644 index 8d7a218..0000000 --- a/yhy-module-core/src/test/java/com/yhy/module/core/service/quota/QuotaRateFieldLabelServiceTest.java +++ /dev/null @@ -1,157 +0,0 @@ -package com.yhy.module.core.service.quota; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.yhy.module.core.controller.admin.quota.vo.QuotaRateFieldLabelRespVO; -import com.yhy.module.core.controller.admin.quota.vo.QuotaRateFieldLabelSaveReqVO; -import com.yhy.module.core.dal.dataobject.quota.QuotaRateFieldLabelDO; -import java.util.List; -import javax.annotation.Resource; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -/** - * 定额费率字段标签字典 Service 测试 - * - * @author yhy - */ -@SpringBootTest -public class QuotaRateFieldLabelServiceTest { - - @Resource - private QuotaRateFieldLabelService fieldLabelService; - - /** - * 测试创建字段标签 - */ - @Test - public void testCreateFieldLabel() { - // 准备数据 - QuotaRateFieldLabelSaveReqVO createReqVO = new QuotaRateFieldLabelSaveReqVO(); - createReqVO.setCatalogItemId(1002L); // 模式节点ID - createReqVO.setLabelName("土石方工程"); - - // 执行创建 - Long labelId = fieldLabelService.createFieldLabel(createReqVO); - - // 验证结果 - assertNotNull(labelId); - System.out.println("创建标签成功,ID: " + labelId); - - // 查询验证 - QuotaRateFieldLabelDO label = fieldLabelService.getFieldLabel(labelId); - assertNotNull(label); - assertEquals("土石方工程", label.getLabelName()); - assertEquals(1002L, label.getCatalogItemId()); - } - - /** - * 测试更新字段标签 - */ - @Test - public void testUpdateFieldLabel() { - // 先创建 - QuotaRateFieldLabelSaveReqVO createReqVO = new QuotaRateFieldLabelSaveReqVO(); - createReqVO.setCatalogItemId(1002L); - createReqVO.setLabelName("土石方工程"); - Long labelId = fieldLabelService.createFieldLabel(createReqVO); - - // 更新 - QuotaRateFieldLabelSaveReqVO updateReqVO = new QuotaRateFieldLabelSaveReqVO(); - updateReqVO.setId(labelId); - updateReqVO.setCatalogItemId(1002L); - updateReqVO.setLabelName("土方工程"); - fieldLabelService.updateFieldLabel(updateReqVO); - - // 验证 - QuotaRateFieldLabelDO label = fieldLabelService.getFieldLabel(labelId); - assertEquals("土方工程", label.getLabelName()); - System.out.println("更新标签成功,新名称: " + label.getLabelName()); - } - - /** - * 测试查询标签列表 - */ - @Test - public void testGetFieldLabelList() { - // 创建多个标签 - Long catalogItemId = 1002L; - - QuotaRateFieldLabelSaveReqVO label1 = new QuotaRateFieldLabelSaveReqVO(); - label1.setCatalogItemId(catalogItemId); - label1.setLabelName("土石方工程"); - fieldLabelService.createFieldLabel(label1); - - QuotaRateFieldLabelSaveReqVO label2 = new QuotaRateFieldLabelSaveReqVO(); - label2.setCatalogItemId(catalogItemId); - label2.setLabelName("地基处理工程"); - fieldLabelService.createFieldLabel(label2); - - // 查询列表 - List labels = fieldLabelService.getFieldLabelList(catalogItemId); - - // 验证 - assertNotNull(labels); - assertTrue(labels.size() >= 2); - System.out.println("查询到标签数量: " + labels.size()); - labels.forEach(label -> System.out.println(" - " + label.getLabelName())); - } - - /** - * 测试交换排序 - */ - @Test - public void testSwapSort() { - // 创建两个标签 - Long catalogItemId = 1002L; - - QuotaRateFieldLabelSaveReqVO label1 = new QuotaRateFieldLabelSaveReqVO(); - label1.setCatalogItemId(catalogItemId); - label1.setLabelName("标签A"); - Long labelId1 = fieldLabelService.createFieldLabel(label1); - - QuotaRateFieldLabelSaveReqVO label2 = new QuotaRateFieldLabelSaveReqVO(); - label2.setCatalogItemId(catalogItemId); - label2.setLabelName("标签B"); - Long labelId2 = fieldLabelService.createFieldLabel(label2); - - // 获取初始排序 - QuotaRateFieldLabelDO before1 = fieldLabelService.getFieldLabel(labelId1); - QuotaRateFieldLabelDO before2 = fieldLabelService.getFieldLabel(labelId2); - System.out.println("交换前 - 标签A排序: " + before1.getSortOrder() + ", 标签B排序: " + before2.getSortOrder()); - - // 交换排序 - fieldLabelService.swapSort(labelId1, labelId2); - - // 验证排序已交换 - QuotaRateFieldLabelDO after1 = fieldLabelService.getFieldLabel(labelId1); - QuotaRateFieldLabelDO after2 = fieldLabelService.getFieldLabel(labelId2); - System.out.println("交换后 - 标签A排序: " + after1.getSortOrder() + ", 标签B排序: " + after2.getSortOrder()); - - assertEquals(before1.getSortOrder(), after2.getSortOrder()); - assertEquals(before2.getSortOrder(), after1.getSortOrder()); - } - - /** - * 测试删除标签(未被使用) - */ - @Test - public void testDeleteFieldLabel() { - // 创建标签 - QuotaRateFieldLabelSaveReqVO createReqVO = new QuotaRateFieldLabelSaveReqVO(); - createReqVO.setCatalogItemId(1002L); - createReqVO.setLabelName("临时标签"); - Long labelId = fieldLabelService.createFieldLabel(createReqVO); - - // 删除 - fieldLabelService.deleteFieldLabel(labelId); - - // 验证已删除 - QuotaRateFieldLabelDO label = fieldLabelService.getFieldLabel(labelId); - assertNull(label); - System.out.println("删除标签成功"); - } -} diff --git a/yhy-module-core/src/test/java/com/yhy/module/core/service/quota/QuotaRateItemCalculationTest.java b/yhy-module-core/src/test/java/com/yhy/module/core/service/quota/QuotaRateItemCalculationTest.java deleted file mode 100644 index 1f364bc..0000000 --- a/yhy-module-core/src/test/java/com/yhy/module/core/service/quota/QuotaRateItemCalculationTest.java +++ /dev/null @@ -1,208 +0,0 @@ -package com.yhy.module.core.service.quota; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import com.yhy.module.core.dal.dataobject.quota.QuotaRateItemDO; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -/** - * 定额费率项计算逻辑测试 - * - * 测试默认阶梯规则的计算逻辑 - */ -@DisplayName("定额费率项计算逻辑测试") -public class QuotaRateItemCalculationTest { - - @Test - @DisplayName("测试阶梯规则 - 基数50万以内") - public void testTierRule_Under50() { - // 模拟费率项配置 - QuotaRateItemDO rateItem = createRateItemWithTiers(); - - // 基数值:30万 - BigDecimal baseValue = new BigDecimal("30"); - - // 预期结果:字段1=0.224 - Map expected = new HashMap<>(); - expected.put(1, new BigDecimal("0.224")); - - // 验证匹配第一个阶梯 - Map valueRules = rateItem.getValueRules(); - List> tiers = (List>) valueRules.get("tiers"); - - Map matchedTier = findMatchedTier(tiers, baseValue); - assertNotNull(matchedTier); - assertEquals(new BigDecimal("50"), new BigDecimal(matchedTier.get("threshold").toString())); - } - - @Test - @DisplayName("测试阶梯规则 - 基数50-100万之间") - public void testTierRule_Between50And100() { - QuotaRateItemDO rateItem = createRateItemWithTiers(); - - // 基数值:80万 - BigDecimal baseValue = new BigDecimal("80"); - - // 预期结果:字段1=0.301 - Map valueRules = rateItem.getValueRules(); - List> tiers = (List>) valueRules.get("tiers"); - - Map matchedTier = findMatchedTier(tiers, baseValue); - assertNotNull(matchedTier); - assertEquals(new BigDecimal("100"), new BigDecimal(matchedTier.get("threshold").toString())); - } - - @Test - @DisplayName("测试阶梯规则 - 基数超过最大阈值") - public void testTierRule_ExceedMax() { - QuotaRateItemDO rateItem = createRateItemWithTiers(); - - // 基数值:5000万 - BigDecimal baseValue = new BigDecimal("5000"); - - // 预期结果:使用最后一个阶梯 - Map valueRules = rateItem.getValueRules(); - List> tiers = (List>) valueRules.get("tiers"); - - Map matchedTier = findMatchedTier(tiers, baseValue); - // 如果没有匹配,应该使用最后一个阶梯 - if (matchedTier == null) { - matchedTier = tiers.get(tiers.size() - 1); - } - assertNotNull(matchedTier); - } - - @Test - @DisplayName("测试阶梯规则 + 增量规则") - public void testTierWithIncrement() { - QuotaRateItemDO rateItem = createRateItemWithTiersAndIncrements(); - - // 基数值:1200万(超过1000万基础阈值200万) - BigDecimal baseValue = new BigDecimal("1200"); - - // 预期结果: - // 1. 阶梯基础值:字段1=0.301(匹配最后一个阶梯) - // 2. 增量计算:(1200-1000)/100 * 0.036 = 2 * 0.036 = 0.072 - // 3. 最终结果:0.301 + 0.072 = 0.373 - - Map valueRules = rateItem.getValueRules(); - List> tiers = (List>) valueRules.get("tiers"); - List> increments = (List>) valueRules.get("increments"); - - assertNotNull(tiers); - assertNotNull(increments); - assertFalse(increments.isEmpty()); - } - - // ==================== 辅助方法 ==================== - - /** - * 创建只有阶梯规则的费率项 - */ - private QuotaRateItemDO createRateItemWithTiers() { - QuotaRateItemDO rateItem = new QuotaRateItemDO(); - rateItem.setValueMode("dynamic"); - - Map settings = new HashMap<>(); - Map valueRules = new HashMap<>(); - - // 阶梯规则 - List> tiers = new ArrayList<>(); - - // 第1档:≤50万,费率0.224% - Map tier1 = new HashMap<>(); - tier1.put("seq", 1); - tier1.put("threshold", new BigDecimal("50")); - tier1.put("compareType", "lte"); - Map fieldValues1 = new HashMap<>(); - fieldValues1.put("1", new BigDecimal("0.224")); - tier1.put("fieldValues", fieldValues1); - tiers.add(tier1); - - // 第2档:≤100万,费率0.301% - Map tier2 = new HashMap<>(); - tier2.put("seq", 2); - tier2.put("threshold", new BigDecimal("100")); - tier2.put("compareType", "lte"); - Map fieldValues2 = new HashMap<>(); - fieldValues2.put("1", new BigDecimal("0.301")); - tier2.put("fieldValues", fieldValues2); - tiers.add(tier2); - - valueRules.put("tiers", tiers); - settings.put("valueRules", valueRules); - rateItem.setSettings(settings); - - return rateItem; - } - - /** - * 创建包含阶梯规则和增量规则的费率项 - */ - private QuotaRateItemDO createRateItemWithTiersAndIncrements() { - QuotaRateItemDO rateItem = createRateItemWithTiers(); - - Map settings = rateItem.getSettings(); - Map valueRules = (Map) settings.get("valueRules"); - - // 增量规则 - List> increments = new ArrayList<>(); - - Map increment1 = new HashMap<>(); - increment1.put("seq", 1); - increment1.put("step", new BigDecimal("100")); - increment1.put("baseThreshold", new BigDecimal("1000")); - Map fieldIncrements = new HashMap<>(); - fieldIncrements.put("1", new BigDecimal("0.036")); - increment1.put("fieldIncrements", fieldIncrements); - increments.add(increment1); - - valueRules.put("increments", increments); - - return rateItem; - } - - /** - * 查找匹配的阶梯 - */ - private Map findMatchedTier(List> tiers, BigDecimal baseValue) { - // 按阈值排序 - tiers.sort(Comparator.comparing(t -> new BigDecimal(t.get("threshold").toString()))); - - for (Map tier : tiers) { - BigDecimal threshold = new BigDecimal(tier.get("threshold").toString()); - String compareType = (String) tier.getOrDefault("compareType", "lte"); - - boolean matched = false; - switch (compareType) { - case "lte": - matched = baseValue.compareTo(threshold) <= 0; - break; - case "lt": - matched = baseValue.compareTo(threshold) < 0; - break; - case "gte": - matched = baseValue.compareTo(threshold) >= 0; - break; - case "gt": - matched = baseValue.compareTo(threshold) > 0; - break; - } - - if (matched) { - return tier; - } - } - - return null; - } -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/util/TenantUtils.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/util/TenantUtils.java index b05b3c0..54fb08f 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/util/TenantUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/util/TenantUtils.java @@ -110,4 +110,8 @@ public class TenantUtils { } } + public static V executeIsAdmin(Callable callable) { + Long id = TenantContextHolder.getTenantId(); + return id.equals(1L) ? executeIgnore(callable): execute(id,callable); + } } diff --git a/yudao-server/pom.xml b/yudao-server/pom.xml index 0244071..a471107 100644 --- a/yudao-server/pom.xml +++ b/yudao-server/pom.xml @@ -63,6 +63,13 @@ postgresql + + + org.springframework.boot + spring-boot-starter-test + test + + diff --git a/yudao-server/src/main/resources/application-dev.yaml b/yudao-server/src/main/resources/application-dev.yaml index 8d5ce79..b305dea 100644 --- a/yudao-server/src/main/resources/application-dev.yaml +++ b/yudao-server/src/main/resources/application-dev.yaml @@ -58,10 +58,10 @@ spring: # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 redis: - host: 192.168.5.118 # 地址 - port: 6379 # 端口 - database: 1 # 数据库索引 -# password: 123456 # 密码,建议生产环境开启 + host: 106.55.147.251 # 地址 + port: 6380 # 端口 + database: 10 # 数据库索引 + password: yudao123456 # 密码,建议生产环境开启 --- #################### 定时任务相关配置 #################### diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml index 6d60b5f..04bdda5 100644 --- a/yudao-server/src/main/resources/application-local.yaml +++ b/yudao-server/src/main/resources/application-local.yaml @@ -48,7 +48,7 @@ spring: primary: master datasource: master: - url: jdbc:postgresql://192.168.5.118:5432/ruoyi-vue-pro # PostgreSQL 连接配置 + url: jdbc:postgresql://192.168.5.118:5432/postgres # PostgreSQL 连接配置 username: postgres password: 123456 # username: sa # SQL Server 连接的示例 @@ -59,7 +59,7 @@ spring: # password: Yudao@2024 # OpenGauss 连接的示例 slave: # 模拟从库,可根据自己需要修改 lazy: true # 开启懒加载,保证启动速度 - url: jdbc:postgresql://192.168.5.118:5432/ruoyi-vue-pro # PostgreSQL 连接配置 + url: jdbc:postgresql://192.168.5.118:5432/postgres # PostgreSQL 连接配置 username: postgres password: 123456 # tdengine: # IoT 数据库(需要 IoT 物联网再开启噢!) diff --git a/yudao-server/src/main/resources/application-pro.yaml b/yudao-server/src/main/resources/application-pro.yaml index 9a71e45..fa16dcf 100644 --- a/yudao-server/src/main/resources/application-pro.yaml +++ b/yudao-server/src/main/resources/application-pro.yaml @@ -58,8 +58,8 @@ spring: # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 redis: - host: 10.1.12.13 # 地址 - port: 6379 # 端口 + host: 106.55.147.251 # 地址 + port: 6380 # 端口 database: 1 # 数据库索引 password: yudao123456 # 密码,建议生产环境开启 diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index f844a5e..a4e8cc8 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -3,7 +3,7 @@ spring: name: yudao-server profiles: - active: local + active: dev main: allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。