diff --git a/base-server/src/main/resources/application.yml b/base-server/src/main/resources/application.yml
index 7eb362ae..4e9bb026 100644
--- a/base-server/src/main/resources/application.yml
+++ b/base-server/src/main/resources/application.yml
@@ -72,6 +72,8 @@ knife4j:
# MyBatis Plus 的配置项
mybatis-plus:
+ mapper-locations:
+ - classpath*:mapper/**/*.xml
configuration:
map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。
global-config:
diff --git a/base-server/src/main/resources/logback-spring.xml b/base-server/src/main/resources/logback-spring.xml
new file mode 100644
index 00000000..2bde9644
--- /dev/null
+++ b/base-server/src/main/resources/logback-spring.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ ${PATTERN_DEFAULT}
+
+
+
+
+
+
+
+
+
+ ${PATTERN_DEFAULT}
+
+
+
+ ${LOG_FILE}
+
+
+ ${LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN:-${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz}
+
+ ${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false}
+
+ ${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB}
+
+ ${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0}
+
+ ${LOGBACK_ROLLINGPOLICY_MAX_HISTORY:-30}
+
+
+
+
+
+ 0
+
+ 256
+
+
+
+
+
+
+
+ ${PATTERN_DEFAULT}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/deployment.yaml b/deployment.yaml
index 1da2c085..dad4a1de 100644
--- a/deployment.yaml
+++ b/deployment.yaml
@@ -23,18 +23,21 @@ spec:
containers:
- name: base-server
image: 172.16.46.66:10043/yudao/base-server:VERSION_PLACEHOLDER
+ env:
+ - name: TZ
+ value: Asia/Shanghai
readinessProbe:
httpGet:
path: /actuator/health
port: 48100
- initialDelaySeconds: 10
+ initialDelaySeconds: 50
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /actuator/health
port: 48100
- initialDelaySeconds: 30
+ initialDelaySeconds: 50
periodSeconds: 10
failureThreshold: 5
resources:
diff --git a/pom.xml b/pom.xml
index e65b2515..a2757fab 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,7 +19,7 @@
https://github.com/YunaiV/ruoyi-vue-pro
- 3.0.44
+ 3.0.45
17
${java.version}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/businessdictionarytype/BusinessDictionaryTypeApi.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/businessdictionarytype/BusinessDictionaryTypeApi.java
new file mode 100644
index 00000000..197b2817
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/businessdictionarytype/BusinessDictionaryTypeApi.java
@@ -0,0 +1,60 @@
+package com.zt.plat.module.base.api.businessdictionarytype;
+
+import com.zt.plat.framework.common.pojo.CommonResult;
+import com.zt.plat.framework.common.pojo.PageResult;
+import com.zt.plat.module.base.api.businessdictionarytype.dto.BusinessDictionaryDataDTO;
+import com.zt.plat.module.base.api.businessdictionarytype.dto.BusinessDictionaryTypePageReqDTO;
+import com.zt.plat.module.base.api.businessdictionarytype.dto.BusinessDictionaryTypeRespDTO;
+import com.zt.plat.module.base.api.businessdictionarytype.dto.BusinessDictionaryTypeSaveReqDTO;
+import com.zt.plat.module.base.enums.ApiConstants;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import org.springframework.cloud.openfeign.FeignClient;
+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.RequestParam;
+
+import java.util.List;
+
+@FeignClient(name = ApiConstants.NAME)
+@Tag(name = "RPC 服务 - 业务字典类型")
+public interface BusinessDictionaryTypeApi {
+
+ String PREFIX = ApiConstants.PREFIX + "/business-dictionary-type";
+
+ @PostMapping(PREFIX + "/create")
+ @Operation(summary = "创建业务字典类型")
+ CommonResult createBusinessDictionaryType(@Valid @RequestBody BusinessDictionaryTypeSaveReqDTO createReqDTO);
+
+ @PutMapping(PREFIX + "/update")
+ @Operation(summary = "更新业务字典类型")
+ CommonResult updateBusinessDictionaryType(@Valid @RequestBody BusinessDictionaryTypeSaveReqDTO updateReqDTO);
+
+ @DeleteMapping(PREFIX + "/delete")
+ @Operation(summary = "删除业务字典类型")
+ CommonResult deleteBusinessDictionaryType(@RequestParam("id") Long id);
+
+ @DeleteMapping(PREFIX + "/delete-list")
+ @Operation(summary = "批量删除业务字典类型")
+ CommonResult deleteBusinessDictionaryTypeList(@RequestBody List ids);
+
+ @GetMapping(PREFIX + "/get")
+ @Operation(summary = "获得业务字典类型")
+ CommonResult getBusinessDictionaryType(@RequestParam("id") Long id);
+
+ @GetMapping(PREFIX + "/page")
+ @Operation(summary = "获得业务字典类型分页")
+ CommonResult> getBusinessDictionaryTypePage(@Valid BusinessDictionaryTypePageReqDTO pageReqDTO);
+
+ @GetMapping(PREFIX + "/business-dictionary-data/list-by-dictionary-type-id")
+ @Operation(summary = "获得业务字典数据列表")
+ CommonResult> getBusinessDictionaryDataListByDictionaryTypeId(@RequestParam("dictionaryTypeId") Long dictionaryTypeId);
+
+ @GetMapping(PREFIX + "/business-dictionary-data/list-by-type")
+ @Operation(summary = "根据字典类型编码获取业务字典数据列表")
+ CommonResult> getBusinessDictionaryDataListByType(@RequestParam("type") String type);
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/businessdictionarytype/dto/BusinessDictionaryDataDTO.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/businessdictionarytype/dto/BusinessDictionaryDataDTO.java
new file mode 100644
index 00000000..1dcf4556
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/businessdictionarytype/dto/BusinessDictionaryDataDTO.java
@@ -0,0 +1,35 @@
+package com.zt.plat.module.base.api.businessdictionarytype.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * 业务字典数据 DTO
+ */
+@Data
+public class BusinessDictionaryDataDTO {
+
+ @Schema(description = "主键ID", example = "1001")
+ private Long id;
+
+ @Schema(description = "上级字典", example = "2001")
+ private Long parentId;
+
+ @Schema(description = "字典类型", example = "3001")
+ private Long dictionaryTypeId;
+
+ @Schema(description = "排序号", example = "10")
+ private Long sort;
+
+ @Schema(description = "字典标签", example = "状态")
+ private String label;
+
+ @Schema(description = "字典值", example = "ENABLE")
+ private String value;
+
+ @Schema(description = "状态(0正常 1停用)", example = "0")
+ private Long status;
+
+ @Schema(description = "备注", example = "同义词")
+ private String remark;
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/businessdictionarytype/dto/BusinessDictionaryTypePageReqDTO.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/businessdictionarytype/dto/BusinessDictionaryTypePageReqDTO.java
new file mode 100644
index 00000000..03ae629a
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/businessdictionarytype/dto/BusinessDictionaryTypePageReqDTO.java
@@ -0,0 +1,21 @@
+package com.zt.plat.module.base.api.businessdictionarytype.dto;
+
+import com.zt.plat.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * 业务字典类型分页 Request DTO
+ */
+@Data
+public class BusinessDictionaryTypePageReqDTO extends PageParam {
+
+ @Schema(description = "字典名称", example = "物料状态")
+ private String name;
+
+ @Schema(description = "字典类型", example = "base_material_status")
+ private String type;
+
+ @Schema(description = "状态(0正常 1停用)", example = "0")
+ private Long status;
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/businessdictionarytype/dto/BusinessDictionaryTypeRespDTO.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/businessdictionarytype/dto/BusinessDictionaryTypeRespDTO.java
new file mode 100644
index 00000000..87f8016f
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/businessdictionarytype/dto/BusinessDictionaryTypeRespDTO.java
@@ -0,0 +1,31 @@
+package com.zt.plat.module.base.api.businessdictionarytype.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 业务字典类型 Response DTO
+ */
+@Data
+public class BusinessDictionaryTypeRespDTO {
+
+ @Schema(description = "主键ID", example = "11771")
+ private Long id;
+
+ @Schema(description = "字典名称", example = "物料状态")
+ private String name;
+
+ @Schema(description = "字典类型", example = "base_material_status")
+ private String type;
+
+ @Schema(description = "状态(0正常 1停用)", example = "0")
+ private Long status;
+
+ @Schema(description = "备注", example = "基础物料状态")
+ private String remark;
+
+ @Schema(description = "创建时间")
+ private LocalDateTime createTime;
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/businessdictionarytype/dto/BusinessDictionaryTypeSaveReqDTO.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/businessdictionarytype/dto/BusinessDictionaryTypeSaveReqDTO.java
new file mode 100644
index 00000000..562dd2d7
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/businessdictionarytype/dto/BusinessDictionaryTypeSaveReqDTO.java
@@ -0,0 +1,38 @@
+package com.zt.plat.module.base.api.businessdictionarytype.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 业务字典类型新增/修改 Request DTO
+ */
+@Data
+public class BusinessDictionaryTypeSaveReqDTO {
+
+ @Schema(description = "主键ID", example = "11771")
+ private Long id;
+
+ @Schema(description = "字典名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "物料状态")
+ @NotEmpty(message = "字典名称不能为空")
+ private String name;
+
+ @Schema(description = "字典类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "base_material_status")
+ @NotEmpty(message = "字典类型不能为空")
+ private String type;
+
+ @Schema(description = "状态(0正常 1停用)", example = "0")
+ private Long status;
+
+ @Schema(description = "备注", example = "基础物料状态")
+ private String remark;
+
+ @Schema(description = "删除时间")
+ private LocalDateTime delTime;
+
+ @Schema(description = "业务字典数据列表")
+ private List businessDictionaryDatas;
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/departmentmaterial/DepartmentMaterialApi.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/departmentmaterial/DepartmentMaterialApi.java
new file mode 100644
index 00000000..a737b0da
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/departmentmaterial/DepartmentMaterialApi.java
@@ -0,0 +1,51 @@
+package com.zt.plat.module.base.api.departmentmaterial;
+
+import com.zt.plat.framework.common.pojo.CommonResult;
+import com.zt.plat.framework.common.pojo.PageResult;
+import com.zt.plat.module.base.api.departmentmaterial.dto.DepartmentMaterialPageReqDTO;
+import com.zt.plat.module.base.api.departmentmaterial.dto.DepartmentMaterialRespDTO;
+import com.zt.plat.module.base.api.departmentmaterial.dto.DepartmentMaterialSaveReqDTO;
+import com.zt.plat.module.base.enums.ApiConstants;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import org.springframework.cloud.openfeign.FeignClient;
+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.RequestParam;
+
+import java.util.List;
+
+@FeignClient(name = ApiConstants.NAME)
+@Tag(name = "RPC 服务 - 组织架构物料")
+public interface DepartmentMaterialApi {
+
+ String PREFIX = ApiConstants.PREFIX + "/department-material";
+
+ @PostMapping(PREFIX + "/create")
+ @Operation(summary = "创建组织架构物料")
+ CommonResult createDepartmentMaterial(@Valid @RequestBody DepartmentMaterialSaveReqDTO createReqDTO);
+
+ @PutMapping(PREFIX + "/update")
+ @Operation(summary = "更新组织架构物料")
+ CommonResult updateDepartmentMaterial(@Valid @RequestBody DepartmentMaterialSaveReqDTO updateReqDTO);
+
+ @DeleteMapping(PREFIX + "/delete")
+ @Operation(summary = "删除组织架构物料")
+ CommonResult deleteDepartmentMaterial(@RequestParam("id") Long id);
+
+ @DeleteMapping(PREFIX + "/delete-list")
+ @Operation(summary = "批量删除组织架构物料")
+ CommonResult deleteDepartmentMaterialList(@RequestBody List ids);
+
+ @GetMapping(PREFIX + "/get")
+ @Operation(summary = "获得组织架构物料")
+ CommonResult getDepartmentMaterial(@RequestParam("id") Long id);
+
+ @GetMapping(PREFIX + "/page")
+ @Operation(summary = "获得组织架构物料分页")
+ CommonResult> getDepartmentMaterialPage(@Valid DepartmentMaterialPageReqDTO pageReqDTO);
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/departmentmaterial/dto/DepartmentMaterialPageReqDTO.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/departmentmaterial/dto/DepartmentMaterialPageReqDTO.java
new file mode 100644
index 00000000..4573034f
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/departmentmaterial/dto/DepartmentMaterialPageReqDTO.java
@@ -0,0 +1,49 @@
+package com.zt.plat.module.base.api.departmentmaterial.dto;
+
+import com.zt.plat.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static com.zt.plat.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+/**
+ * 组织架构物料分页 Request DTO
+ */
+@Data
+public class DepartmentMaterialPageReqDTO extends PageParam {
+
+ @Schema(description = "物料信息ID", example = "3923")
+ private Long infomationId;
+
+ @Schema(description = "物料信息ID集合(内部使用)")
+ private List infomationIds;
+
+ @Schema(description = "物料分类ID", example = "30114")
+ private Long classesId;
+
+ @Schema(description = "部门ID", example = "1001")
+ private Long deptId;
+
+ @Schema(description = "字典数据值-物料类型")
+ private String dictionaryDataValue;
+
+ @Schema(description = "状态编码", example = "1")
+ private String status;
+
+ @Schema(description = "物料编码")
+ private String materialNumber;
+
+ @Schema(description = "物料名称")
+ private String materialName;
+
+ @Schema(description = "备注")
+ private String remark;
+
+ @Schema(description = "创建时间")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] createTime;
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/departmentmaterial/dto/DepartmentMaterialRespDTO.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/departmentmaterial/dto/DepartmentMaterialRespDTO.java
new file mode 100644
index 00000000..a7255024
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/departmentmaterial/dto/DepartmentMaterialRespDTO.java
@@ -0,0 +1,61 @@
+package com.zt.plat.module.base.api.departmentmaterial.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 组织架构物料 Response DTO
+ */
+@Data
+public class DepartmentMaterialRespDTO {
+
+ @Schema(description = "主键ID", example = "5674")
+ private Long id;
+
+ @Schema(description = "物料信息ID", example = "3923")
+ private Long infomationId;
+
+ @Schema(description = "物料分类ID", example = "30114")
+ private Long classesId;
+
+ @Schema(description = "字典数据值-物料类型")
+ private String dictionaryDataValue;
+
+ @Schema(description = "备注")
+ private String remark;
+
+ @Schema(description = "创建时间")
+ private LocalDateTime createTime;
+
+ @Schema(description = "部门ID")
+ private Long deptId;
+
+ @Schema(description = "部门名称")
+ private String deptName;
+
+ @Schema(description = "物料编码")
+ private String materialNumber;
+
+ @Schema(description = "物料名称")
+ private String materialName;
+
+ @Schema(description = "物料大类名称")
+ private String categoryLargeName;
+
+ @Schema(description = "物料中类名称")
+ private String categoryMediumName;
+
+ @Schema(description = "物料小类名称")
+ private String categorySmallName;
+
+ @Schema(description = "物料分类路径")
+ private String categoryPath;
+
+ @Schema(description = "组织物料类型名称")
+ private String dictionaryDataLabel;
+
+ @Schema(description = "状态编码")
+ private String status;
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/departmentmaterial/dto/DepartmentMaterialSaveReqDTO.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/departmentmaterial/dto/DepartmentMaterialSaveReqDTO.java
new file mode 100644
index 00000000..a5b0432f
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/departmentmaterial/dto/DepartmentMaterialSaveReqDTO.java
@@ -0,0 +1,39 @@
+package com.zt.plat.module.base.api.departmentmaterial.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * 组织架构物料新增/修改 Request DTO
+ */
+@Data
+public class DepartmentMaterialSaveReqDTO {
+
+ @Schema(description = "主键ID", example = "5674")
+ private Long id;
+
+ @Schema(description = "部门ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
+ @NotNull(message = "部门ID不能为空")
+ private Long deptId;
+
+ @Schema(description = "物料信息ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "3923")
+ @NotNull(message = "物料信息ID不能为空")
+ private Long infomationId;
+
+ @Schema(description = "物料分类ID", example = "30114")
+ private Long classesId;
+
+ @Schema(description = "字典数据值-物料类型", requiredMode = Schema.RequiredMode.REQUIRED)
+ @NotEmpty(message = "字典数据值-物料类型不能为空")
+ private String dictionaryDataValue;
+
+ @Schema(description = "状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotEmpty(message = "状态不能为空")
+ private String status;
+
+ @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED)
+ @NotEmpty(message = "备注不能为空")
+ private String remark;
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialclasses/MaterialClassesApi.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialclasses/MaterialClassesApi.java
new file mode 100644
index 00000000..216a37ed
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialclasses/MaterialClassesApi.java
@@ -0,0 +1,56 @@
+package com.zt.plat.module.base.api.materialclasses;
+
+import com.zt.plat.framework.common.pojo.CommonResult;
+import com.zt.plat.framework.common.pojo.PageResult;
+import com.zt.plat.module.base.api.materialclasses.dto.MaterialClassesPageReqDTO;
+import com.zt.plat.module.base.api.materialclasses.dto.MaterialClassesRespDTO;
+import com.zt.plat.module.base.api.materialclasses.dto.MaterialClassesSaveReqDTO;
+import com.zt.plat.module.base.api.materialclasses.dto.MaterialClassesTreeRespDTO;
+import com.zt.plat.module.base.enums.ApiConstants;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import org.springframework.cloud.openfeign.FeignClient;
+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.RequestParam;
+
+import java.util.List;
+
+@FeignClient(name = ApiConstants.NAME)
+@Tag(name = "RPC 服务 - 物料分类")
+public interface MaterialClassesApi {
+
+ String PREFIX = ApiConstants.PREFIX + "/material-classes";
+
+ @PostMapping(PREFIX + "/create")
+ @Operation(summary = "创建物料分类")
+ CommonResult createMaterialClasses(@Valid @RequestBody MaterialClassesSaveReqDTO createReqDTO);
+
+ @PutMapping(PREFIX + "/update")
+ @Operation(summary = "更新物料分类")
+ CommonResult updateMaterialClasses(@Valid @RequestBody MaterialClassesSaveReqDTO updateReqDTO);
+
+ @DeleteMapping(PREFIX + "/delete")
+ @Operation(summary = "删除物料分类")
+ CommonResult deleteMaterialClasses(@RequestParam("id") Long id);
+
+ @DeleteMapping(PREFIX + "/delete-list")
+ @Operation(summary = "批量删除物料分类")
+ CommonResult deleteMaterialClassesList(@RequestBody List ids);
+
+ @GetMapping(PREFIX + "/get")
+ @Operation(summary = "获得物料分类")
+ CommonResult getMaterialClasses(@RequestParam("id") Long id);
+
+ @GetMapping(PREFIX + "/page")
+ @Operation(summary = "获得物料分类分页")
+ CommonResult> getMaterialClassesPage(@Valid MaterialClassesPageReqDTO pageReqDTO);
+
+ @GetMapping(PREFIX + "/tree")
+ @Operation(summary = "获得物料分类树")
+ CommonResult> getMaterialClassesTree();
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialclasses/dto/MaterialClassesPageReqDTO.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialclasses/dto/MaterialClassesPageReqDTO.java
new file mode 100644
index 00000000..a8390e3e
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialclasses/dto/MaterialClassesPageReqDTO.java
@@ -0,0 +1,36 @@
+package com.zt.plat.module.base.api.materialclasses.dto;
+
+import com.zt.plat.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static com.zt.plat.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+/**
+ * 物料分类分页 Request DTO
+ */
+@Data
+public class MaterialClassesPageReqDTO extends PageParam {
+
+ @Schema(description = "父级ID", example = "20706")
+ private Long parentId;
+
+ @Schema(description = "分类编码")
+ private String code;
+
+ @Schema(description = "分类名称", example = "原材料")
+ private String name;
+
+ @Schema(description = "分类级别-用于类别层级(大/中/小类)")
+ private Long level;
+
+ @Schema(description = "备注")
+ private String remark;
+
+ @Schema(description = "创建时间")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] createTime;
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialclasses/dto/MaterialClassesRespDTO.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialclasses/dto/MaterialClassesRespDTO.java
new file mode 100644
index 00000000..6a8429ab
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialclasses/dto/MaterialClassesRespDTO.java
@@ -0,0 +1,34 @@
+package com.zt.plat.module.base.api.materialclasses.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 物料分类 Response DTO
+ */
+@Data
+public class MaterialClassesRespDTO {
+
+ @Schema(description = "主键ID", example = "4051")
+ private Long id;
+
+ @Schema(description = "父级ID", example = "20706")
+ private Long parentId;
+
+ @Schema(description = "分类编码")
+ private String code;
+
+ @Schema(description = "分类名称", example = "原材料")
+ private String name;
+
+ @Schema(description = "分类级别-用于类别层级(大/中/小类)")
+ private Long level;
+
+ @Schema(description = "备注")
+ private String remark;
+
+ @Schema(description = "创建时间")
+ private LocalDateTime createTime;
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialclasses/dto/MaterialClassesSaveReqDTO.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialclasses/dto/MaterialClassesSaveReqDTO.java
new file mode 100644
index 00000000..6b5551a3
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialclasses/dto/MaterialClassesSaveReqDTO.java
@@ -0,0 +1,33 @@
+package com.zt.plat.module.base.api.materialclasses.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+/**
+ * 物料分类新增/修改 Request DTO
+ */
+@Data
+public class MaterialClassesSaveReqDTO {
+
+ @Schema(description = "主键ID", example = "4051")
+ private Long id;
+
+ @Schema(description = "父级ID", example = "20706")
+ private Long parentId;
+
+ @Schema(description = "分类编码", requiredMode = Schema.RequiredMode.REQUIRED)
+ @NotEmpty(message = "分类编码不能为空")
+ private String code;
+
+ @Schema(description = "分类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "原材料")
+ @NotEmpty(message = "分类名称不能为空")
+ private String name;
+
+ @Schema(description = "分类级别-用于类别层级(大/中/小类)")
+ private Long level;
+
+ @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED)
+ @NotEmpty(message = "备注不能为空")
+ private String remark;
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialclasses/dto/MaterialClassesTreeRespDTO.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialclasses/dto/MaterialClassesTreeRespDTO.java
new file mode 100644
index 00000000..6016e51b
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialclasses/dto/MaterialClassesTreeRespDTO.java
@@ -0,0 +1,35 @@
+package com.zt.plat.module.base.api.materialclasses.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 物料分类树 Response DTO
+ */
+@Data
+public class MaterialClassesTreeRespDTO {
+
+ @Schema(description = "主键ID", example = "1001")
+ private Long id;
+
+ @Schema(description = "父级ID", example = "0")
+ private Long parentId;
+
+ @Schema(description = "分类编码", example = "CL-001")
+ private String code;
+
+ @Schema(description = "分类名称", example = "原材料")
+ private String name;
+
+ @Schema(description = "分类级别")
+ private Long level;
+
+ @Schema(description = "备注")
+ private String remark;
+
+ @Schema(description = "子节点")
+ private List children = new ArrayList<>();
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialhasclasses/MaterialHasClassesApi.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialhasclasses/MaterialHasClassesApi.java
new file mode 100644
index 00000000..b413ed29
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialhasclasses/MaterialHasClassesApi.java
@@ -0,0 +1,51 @@
+package com.zt.plat.module.base.api.materialhasclasses;
+
+import com.zt.plat.framework.common.pojo.CommonResult;
+import com.zt.plat.framework.common.pojo.PageResult;
+import com.zt.plat.module.base.api.materialhasclasses.dto.MaterialHasClassesPageReqDTO;
+import com.zt.plat.module.base.api.materialhasclasses.dto.MaterialHasClassesRespDTO;
+import com.zt.plat.module.base.api.materialhasclasses.dto.MaterialHasClassesSaveReqDTO;
+import com.zt.plat.module.base.enums.ApiConstants;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import org.springframework.cloud.openfeign.FeignClient;
+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.RequestParam;
+
+import java.util.List;
+
+@FeignClient(name = ApiConstants.NAME)
+@Tag(name = "RPC 服务 - 物料持有分类")
+public interface MaterialHasClassesApi {
+
+ String PREFIX = ApiConstants.PREFIX + "/material-has-classes";
+
+ @PostMapping(PREFIX + "/create")
+ @Operation(summary = "创建物料持有分类")
+ CommonResult createMaterialHasClasses(@Valid @RequestBody MaterialHasClassesSaveReqDTO createReqDTO);
+
+ @PutMapping(PREFIX + "/update")
+ @Operation(summary = "更新物料持有分类")
+ CommonResult updateMaterialHasClasses(@Valid @RequestBody MaterialHasClassesSaveReqDTO updateReqDTO);
+
+ @DeleteMapping(PREFIX + "/delete")
+ @Operation(summary = "删除物料持有分类")
+ CommonResult deleteMaterialHasClasses(@RequestParam("id") Long id);
+
+ @DeleteMapping(PREFIX + "/delete-list")
+ @Operation(summary = "批量删除物料持有分类")
+ CommonResult deleteMaterialHasClassesList(@RequestBody List ids);
+
+ @GetMapping(PREFIX + "/get")
+ @Operation(summary = "获得物料持有分类")
+ CommonResult getMaterialHasClasses(@RequestParam("id") Long id);
+
+ @GetMapping(PREFIX + "/page")
+ @Operation(summary = "获得物料持有分类分页")
+ CommonResult> getMaterialHasClassesPage(@Valid MaterialHasClassesPageReqDTO pageReqDTO);
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialhasclasses/dto/MaterialHasClassesPageReqDTO.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialhasclasses/dto/MaterialHasClassesPageReqDTO.java
new file mode 100644
index 00000000..d553e209
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialhasclasses/dto/MaterialHasClassesPageReqDTO.java
@@ -0,0 +1,27 @@
+package com.zt.plat.module.base.api.materialhasclasses.dto;
+
+import com.zt.plat.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static com.zt.plat.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+/**
+ * 物料持有分类分页 Request DTO
+ */
+@Data
+public class MaterialHasClassesPageReqDTO extends PageParam {
+
+ @Schema(description = "物料信息ID", example = "31031")
+ private Long infomationId;
+
+ @Schema(description = "分类ID", example = "5914")
+ private Long classesId;
+
+ @Schema(description = "创建时间")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] createTime;
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialhasclasses/dto/MaterialHasClassesRespDTO.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialhasclasses/dto/MaterialHasClassesRespDTO.java
new file mode 100644
index 00000000..40ff0e7e
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialhasclasses/dto/MaterialHasClassesRespDTO.java
@@ -0,0 +1,25 @@
+package com.zt.plat.module.base.api.materialhasclasses.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 物料持有分类 Response DTO
+ */
+@Data
+public class MaterialHasClassesRespDTO {
+
+ @Schema(description = "主键ID", example = "16228")
+ private Long id;
+
+ @Schema(description = "物料信息ID", example = "31031")
+ private Long infomationId;
+
+ @Schema(description = "分类ID", example = "5914")
+ private Long classesId;
+
+ @Schema(description = "创建时间")
+ private LocalDateTime createTime;
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialhasclasses/dto/MaterialHasClassesSaveReqDTO.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialhasclasses/dto/MaterialHasClassesSaveReqDTO.java
new file mode 100644
index 00000000..3e0c2a61
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialhasclasses/dto/MaterialHasClassesSaveReqDTO.java
@@ -0,0 +1,23 @@
+package com.zt.plat.module.base.api.materialhasclasses.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * 物料持有分类新增/修改 Request DTO
+ */
+@Data
+public class MaterialHasClassesSaveReqDTO {
+
+ @Schema(description = "主键ID", example = "16228")
+ private Long id;
+
+ @Schema(description = "物料信息ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "31031")
+ @NotNull(message = "物料信息ID不能为空")
+ private Long infomationId;
+
+ @Schema(description = "分类ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "5914")
+ @NotNull(message = "分类ID不能为空")
+ private Long classesId;
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialhasproperties/MaterialHasPropertiesApi.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialhasproperties/MaterialHasPropertiesApi.java
new file mode 100644
index 00000000..bc93490c
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialhasproperties/MaterialHasPropertiesApi.java
@@ -0,0 +1,51 @@
+package com.zt.plat.module.base.api.materialhasproperties;
+
+import com.zt.plat.framework.common.pojo.CommonResult;
+import com.zt.plat.framework.common.pojo.PageResult;
+import com.zt.plat.module.base.api.materialhasproperties.dto.MaterialHasPropertiesPageReqDTO;
+import com.zt.plat.module.base.api.materialhasproperties.dto.MaterialHasPropertiesRespDTO;
+import com.zt.plat.module.base.api.materialhasproperties.dto.MaterialHasPropertiesSaveReqDTO;
+import com.zt.plat.module.base.enums.ApiConstants;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import org.springframework.cloud.openfeign.FeignClient;
+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.RequestParam;
+
+import java.util.List;
+
+@FeignClient(name = ApiConstants.NAME)
+@Tag(name = "RPC 服务 - 物料持有属性")
+public interface MaterialHasPropertiesApi {
+
+ String PREFIX = ApiConstants.PREFIX + "/material-has-properties";
+
+ @PostMapping(PREFIX + "/create")
+ @Operation(summary = "创建物料持有属性")
+ CommonResult createMaterialHasProperties(@Valid @RequestBody MaterialHasPropertiesSaveReqDTO createReqDTO);
+
+ @PutMapping(PREFIX + "/update")
+ @Operation(summary = "更新物料持有属性")
+ CommonResult updateMaterialHasProperties(@Valid @RequestBody MaterialHasPropertiesSaveReqDTO updateReqDTO);
+
+ @DeleteMapping(PREFIX + "/delete")
+ @Operation(summary = "删除物料持有属性")
+ CommonResult deleteMaterialHasProperties(@RequestParam("id") Long id);
+
+ @DeleteMapping(PREFIX + "/delete-list")
+ @Operation(summary = "批量删除物料持有属性")
+ CommonResult deleteMaterialHasPropertiesList(@RequestBody List ids);
+
+ @GetMapping(PREFIX + "/get")
+ @Operation(summary = "获得物料持有属性")
+ CommonResult getMaterialHasProperties(@RequestParam("id") Long id);
+
+ @GetMapping(PREFIX + "/page")
+ @Operation(summary = "获得物料持有属性分页")
+ CommonResult> getMaterialHasPropertiesPage(@Valid MaterialHasPropertiesPageReqDTO pageReqDTO);
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialhasproperties/dto/MaterialHasPropertiesPageReqDTO.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialhasproperties/dto/MaterialHasPropertiesPageReqDTO.java
new file mode 100644
index 00000000..a9999a3c
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialhasproperties/dto/MaterialHasPropertiesPageReqDTO.java
@@ -0,0 +1,42 @@
+package com.zt.plat.module.base.api.materialhasproperties.dto;
+
+import com.zt.plat.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static com.zt.plat.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+/**
+ * 物料持有属性分页 Request DTO
+ */
+@Data
+public class MaterialHasPropertiesPageReqDTO extends PageParam {
+
+ @Schema(description = "物料信息ID", example = "2614")
+ private Long infomationId;
+
+ @Schema(description = "属性ID", example = "8607")
+ private Long propertiesId;
+
+ @Schema(description = "计量单位ID-默认计量单位", example = "23731")
+ private Long unitId;
+
+ @Schema(description = "属性值")
+ private String value;
+
+ @Schema(description = "是否关键属性-关键属性表示物料唯一性")
+ private Integer isKey;
+
+ @Schema(description = "是否计量定价")
+ private Integer isMetering;
+
+ @Schema(description = "排序号")
+ private Long sort;
+
+ @Schema(description = "创建时间")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] createTime;
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialhasproperties/dto/MaterialHasPropertiesRespDTO.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialhasproperties/dto/MaterialHasPropertiesRespDTO.java
new file mode 100644
index 00000000..5569ea8b
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialhasproperties/dto/MaterialHasPropertiesRespDTO.java
@@ -0,0 +1,40 @@
+package com.zt.plat.module.base.api.materialhasproperties.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 物料持有属性 Response DTO
+ */
+@Data
+public class MaterialHasPropertiesRespDTO {
+
+ @Schema(description = "主键ID", example = "6800")
+ private Long id;
+
+ @Schema(description = "物料信息ID", example = "2614")
+ private Long infomationId;
+
+ @Schema(description = "属性ID", example = "8607")
+ private Long propertiesId;
+
+ @Schema(description = "计量单位ID-默认计量单位", example = "23731")
+ private Long unitId;
+
+ @Schema(description = "属性值")
+ private String value;
+
+ @Schema(description = "是否关键属性-关键属性表示物料唯一性")
+ private Integer isKey;
+
+ @Schema(description = "是否计量定价")
+ private Integer isMetering;
+
+ @Schema(description = "排序号")
+ private Long sort;
+
+ @Schema(description = "创建时间")
+ private LocalDateTime createTime;
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialhasproperties/dto/MaterialHasPropertiesSaveReqDTO.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialhasproperties/dto/MaterialHasPropertiesSaveReqDTO.java
new file mode 100644
index 00000000..30f4372f
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialhasproperties/dto/MaterialHasPropertiesSaveReqDTO.java
@@ -0,0 +1,40 @@
+package com.zt.plat.module.base.api.materialhasproperties.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * 物料持有属性新增/修改 Request DTO
+ */
+@Data
+public class MaterialHasPropertiesSaveReqDTO {
+
+ @Schema(description = "主键ID", example = "6800")
+ private Long id;
+
+ @Schema(description = "物料信息ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "2614")
+ @NotNull(message = "物料信息ID不能为空")
+ private Long infomationId;
+
+ @Schema(description = "属性ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "8607")
+ @NotNull(message = "属性ID不能为空")
+ private Long propertiesId;
+
+ @Schema(description = "计量单位ID-默认计量单位", example = "23731")
+ private Long unitId;
+
+ @Schema(description = "属性值", requiredMode = Schema.RequiredMode.REQUIRED)
+ @NotEmpty(message = "属性值不能为空")
+ private String value;
+
+ @Schema(description = "是否关键属性-关键属性表示物料唯一性")
+ private Integer isKey;
+
+ @Schema(description = "是否计量定价")
+ private Integer isMetering;
+
+ @Schema(description = "排序号")
+ private Long sort;
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialproperties/MaterialPropertiesApi.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialproperties/MaterialPropertiesApi.java
new file mode 100644
index 00000000..a6d97604
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialproperties/MaterialPropertiesApi.java
@@ -0,0 +1,57 @@
+package com.zt.plat.module.base.api.materialproperties;
+
+import com.zt.plat.framework.common.pojo.CommonResult;
+import com.zt.plat.framework.common.pojo.PageResult;
+import com.zt.plat.module.base.api.materialproperties.dto.MaterialPropertiesPageReqDTO;
+import com.zt.plat.module.base.api.materialproperties.dto.MaterialPropertiesRespDTO;
+import com.zt.plat.module.base.api.materialproperties.dto.MaterialPropertiesSaveReqDTO;
+import com.zt.plat.module.base.api.materialproperties.dto.MaterialPropertiesSimplePageReqDTO;
+import com.zt.plat.module.base.api.materialproperties.dto.MaterialPropertiesSimpleRespDTO;
+import com.zt.plat.module.base.enums.ApiConstants;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import org.springframework.cloud.openfeign.FeignClient;
+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.RequestParam;
+
+import java.util.List;
+
+@FeignClient(name = ApiConstants.NAME)
+@Tag(name = "RPC 服务 - 物料属性")
+public interface MaterialPropertiesApi {
+
+ String PREFIX = ApiConstants.PREFIX + "/material-properties";
+
+ @PostMapping(PREFIX + "/create")
+ @Operation(summary = "创建物料属性")
+ CommonResult createMaterialProperties(@Valid @RequestBody MaterialPropertiesSaveReqDTO createReqDTO);
+
+ @PutMapping(PREFIX + "/update")
+ @Operation(summary = "更新物料属性")
+ CommonResult updateMaterialProperties(@Valid @RequestBody MaterialPropertiesSaveReqDTO updateReqDTO);
+
+ @DeleteMapping(PREFIX + "/delete")
+ @Operation(summary = "删除物料属性")
+ CommonResult deleteMaterialProperties(@RequestParam("id") Long id);
+
+ @DeleteMapping(PREFIX + "/delete-list")
+ @Operation(summary = "批量删除物料属性")
+ CommonResult deleteMaterialPropertiesList(@RequestBody List ids);
+
+ @GetMapping(PREFIX + "/get")
+ @Operation(summary = "获得物料属性")
+ CommonResult getMaterialProperties(@RequestParam("id") Long id);
+
+ @GetMapping(PREFIX + "/page")
+ @Operation(summary = "获得物料属性分页")
+ CommonResult> getMaterialPropertiesPage(@Valid MaterialPropertiesPageReqDTO pageReqDTO);
+
+ @GetMapping(PREFIX + "/simple-page")
+ @Operation(summary = "获得物料属性精简分页")
+ CommonResult> getMaterialPropertiesSimplePage(@Valid MaterialPropertiesSimplePageReqDTO pageReqDTO);
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialproperties/dto/MaterialPropertiesPageReqDTO.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialproperties/dto/MaterialPropertiesPageReqDTO.java
new file mode 100644
index 00000000..f670d50e
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialproperties/dto/MaterialPropertiesPageReqDTO.java
@@ -0,0 +1,42 @@
+package com.zt.plat.module.base.api.materialproperties.dto;
+
+import com.zt.plat.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static com.zt.plat.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+/**
+ * 物料属性分页 Request DTO
+ */
+@Data
+public class MaterialPropertiesPageReqDTO extends PageParam {
+
+ @Schema(description = "属性编码")
+ private String code;
+
+ @Schema(description = "属性名称", example = "含量")
+ private String name;
+
+ @Schema(description = "关键字(编码/名称模糊匹配)")
+ private String keyword;
+
+ @Schema(description = "计量单位量ID", example = "30468")
+ private Long unitQuantityId;
+
+ @Schema(description = "属性类型")
+ private String dictionaryDataValue;
+
+ @Schema(description = "数据类型", example = "1")
+ private String dataType;
+
+ @Schema(description = "备注")
+ private String remark;
+
+ @Schema(description = "创建时间")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] createTime;
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialproperties/dto/MaterialPropertiesRespDTO.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialproperties/dto/MaterialPropertiesRespDTO.java
new file mode 100644
index 00000000..4494d55b
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialproperties/dto/MaterialPropertiesRespDTO.java
@@ -0,0 +1,49 @@
+package com.zt.plat.module.base.api.materialproperties.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 物料属性 Response DTO
+ */
+@Data
+public class MaterialPropertiesRespDTO {
+
+ @Schema(description = "主键ID", example = "10591")
+ private Long id;
+
+ @Schema(description = "属性编码")
+ private String code;
+
+ @Schema(description = "属性名称", example = "含量")
+ private String name;
+
+ @Schema(description = "计量单位量ID", example = "30468")
+ private Long unitQuantityId;
+
+ @Schema(description = "属性类型")
+ private String dictionaryDataValue;
+
+ @Schema(description = "属性类型名称")
+ private String dictionaryDataLabel;
+
+ @Schema(description = "数据类型", example = "1")
+ private String dataType;
+
+ @Schema(description = "备注")
+ private String remark;
+
+ @Schema(description = "量纲名称")
+ private String unitQuantityName;
+
+ @Schema(description = "计量单位名称")
+ private String unitName;
+
+ @Schema(description = "计量单位符号")
+ private String unitSymbol;
+
+ @Schema(description = "创建时间")
+ private LocalDateTime createTime;
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialproperties/dto/MaterialPropertiesSaveReqDTO.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialproperties/dto/MaterialPropertiesSaveReqDTO.java
new file mode 100644
index 00000000..f7432f11
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialproperties/dto/MaterialPropertiesSaveReqDTO.java
@@ -0,0 +1,38 @@
+package com.zt.plat.module.base.api.materialproperties.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+/**
+ * 物料属性新增/修改 Request DTO
+ */
+@Data
+public class MaterialPropertiesSaveReqDTO {
+
+ @Schema(description = "主键ID", example = "10591")
+ private Long id;
+
+ @Schema(description = "属性编码", requiredMode = Schema.RequiredMode.REQUIRED)
+ @NotEmpty(message = "属性编码不能为空")
+ private String code;
+
+ @Schema(description = "属性名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "含量")
+ @NotEmpty(message = "属性名称不能为空")
+ private String name;
+
+ @Schema(description = "计量单位量ID", example = "30468")
+ private Long unitQuantityId;
+
+ @Schema(description = "属性类型(业务字典数据值)", requiredMode = Schema.RequiredMode.REQUIRED)
+ @NotEmpty(message = "属性类型不能为空")
+ private String dictionaryDataValue;
+
+ @Schema(description = "数据类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotEmpty(message = "数据类型不能为空")
+ private String dataType;
+
+ @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED)
+ @NotEmpty(message = "备注不能为空")
+ private String remark;
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialproperties/dto/MaterialPropertiesSimplePageReqDTO.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialproperties/dto/MaterialPropertiesSimplePageReqDTO.java
new file mode 100644
index 00000000..cd6df70d
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialproperties/dto/MaterialPropertiesSimplePageReqDTO.java
@@ -0,0 +1,18 @@
+package com.zt.plat.module.base.api.materialproperties.dto;
+
+import com.zt.plat.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * 物料属性精简分页 Request DTO
+ */
+@Data
+public class MaterialPropertiesSimplePageReqDTO extends PageParam {
+
+ @Schema(description = "关键字(编码/名称模糊匹配)")
+ private String keyword;
+
+ @Schema(description = "属性类型")
+ private String dictionaryDataValue;
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialproperties/dto/MaterialPropertiesSimpleRespDTO.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialproperties/dto/MaterialPropertiesSimpleRespDTO.java
new file mode 100644
index 00000000..4a9db2e1
--- /dev/null
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/api/materialproperties/dto/MaterialPropertiesSimpleRespDTO.java
@@ -0,0 +1,35 @@
+package com.zt.plat.module.base.api.materialproperties.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * 物料属性精简 Response DTO
+ */
+@Data
+public class MaterialPropertiesSimpleRespDTO {
+
+ @Schema(description = "物料属性ID", example = "1001")
+ private Long id;
+
+ @Schema(description = "属性编码")
+ private String code;
+
+ @Schema(description = "属性名称")
+ private String name;
+
+ @Schema(description = "数据类型")
+ private String dataType;
+
+ @Schema(description = "单位名称")
+ private String unitName;
+
+ @Schema(description = "单位符号")
+ private String unitSymbol;
+
+ @Schema(description = "属性类型")
+ private String dictionaryDataValue;
+
+ @Schema(description = "属性类型名称")
+ private String dictionaryDataLabel;
+}
diff --git a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/enums/ErrorCodeConstants.java b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/enums/ErrorCodeConstants.java
index 98cc53a5..8bda5275 100644
--- a/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/enums/ErrorCodeConstants.java
+++ b/zt-module-base/zt-module-base-api/src/main/java/com/zt/plat/module/base/enums/ErrorCodeConstants.java
@@ -69,4 +69,7 @@ public interface ErrorCodeConstants {
ErrorCode PROCESSING_INFOMATION_OPERATION_NOT_EXISTS = new ErrorCode(1_027_101_006, "工艺工序不存在");
ErrorCode PROCESSING_OPERATION_NOT_EXISTS = new ErrorCode(1_027_101_007, "工序不存在");
ErrorCode PROCESSING_OPERATION_MATERIAL_NOT_EXISTS = new ErrorCode(1_027_101_008, "工艺工序物料不存在");
+
+ // ========== 主数据同步 ==========
+ ErrorCode MASTER_DATA_SYNC_DISABLED = new ErrorCode(1_027_900_001, "主数据同步功能已禁用");
}
diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/api/businessdictionarytype/BusinessDictionaryTypeApiImpl.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/api/businessdictionarytype/BusinessDictionaryTypeApiImpl.java
new file mode 100644
index 00000000..21db9cd0
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/api/businessdictionarytype/BusinessDictionaryTypeApiImpl.java
@@ -0,0 +1,87 @@
+package com.zt.plat.module.base.api.businessdictionarytype;
+
+import com.zt.plat.framework.common.pojo.CommonResult;
+import com.zt.plat.framework.common.pojo.PageResult;
+import com.zt.plat.framework.common.util.object.BeanUtils;
+import com.zt.plat.module.base.api.businessdictionarytype.dto.BusinessDictionaryDataDTO;
+import com.zt.plat.module.base.api.businessdictionarytype.dto.BusinessDictionaryTypePageReqDTO;
+import com.zt.plat.module.base.api.businessdictionarytype.dto.BusinessDictionaryTypeRespDTO;
+import com.zt.plat.module.base.api.businessdictionarytype.dto.BusinessDictionaryTypeSaveReqDTO;
+import com.zt.plat.module.base.controller.admin.businessdictionarytype.vo.BusinessDictionaryTypePageReqVO;
+import com.zt.plat.module.base.controller.admin.businessdictionarytype.vo.BusinessDictionaryTypeRespVO;
+import com.zt.plat.module.base.controller.admin.businessdictionarytype.vo.BusinessDictionaryTypeSaveReqVO;
+import com.zt.plat.module.base.dal.dataobject.businessdictionarytype.BusinessDictionaryDataDO;
+import com.zt.plat.module.base.dal.dataobject.businessdictionarytype.BusinessDictionaryTypeDO;
+import com.zt.plat.module.base.service.businessdictionarytype.BusinessDictionaryTypeService;
+import jakarta.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+import static com.zt.plat.framework.common.pojo.CommonResult.success;
+
+@RestController
+@Validated
+public class BusinessDictionaryTypeApiImpl implements BusinessDictionaryTypeApi {
+
+ @Resource
+ private BusinessDictionaryTypeService businessDictionaryTypeService;
+
+ @Override
+ public CommonResult createBusinessDictionaryType(BusinessDictionaryTypeSaveReqDTO createReqDTO) {
+ BusinessDictionaryTypeRespVO respVO = businessDictionaryTypeService.createBusinessDictionaryType(convertSaveReq(createReqDTO));
+ return success(BeanUtils.toBean(respVO, BusinessDictionaryTypeRespDTO.class));
+ }
+
+ @Override
+ public CommonResult updateBusinessDictionaryType(BusinessDictionaryTypeSaveReqDTO updateReqDTO) {
+ businessDictionaryTypeService.updateBusinessDictionaryType(convertSaveReq(updateReqDTO));
+ return success(true);
+ }
+
+ @Override
+ public CommonResult deleteBusinessDictionaryType(Long id) {
+ businessDictionaryTypeService.deleteBusinessDictionaryType(id);
+ return success(true);
+ }
+
+ @Override
+ public CommonResult deleteBusinessDictionaryTypeList(List ids) {
+ businessDictionaryTypeService.deleteBusinessDictionaryTypeListByIds(ids);
+ return success(true);
+ }
+
+ @Override
+ public CommonResult getBusinessDictionaryType(Long id) {
+ BusinessDictionaryTypeDO typeDO = businessDictionaryTypeService.getBusinessDictionaryType(id);
+ return success(BeanUtils.toBean(typeDO, BusinessDictionaryTypeRespDTO.class));
+ }
+
+ @Override
+ public CommonResult> getBusinessDictionaryTypePage(BusinessDictionaryTypePageReqDTO pageReqDTO) {
+ BusinessDictionaryTypePageReqVO pageReqVO = BeanUtils.toBean(pageReqDTO, BusinessDictionaryTypePageReqVO.class);
+ PageResult pageResult = businessDictionaryTypeService.getBusinessDictionaryTypePage(pageReqVO);
+ return success(BeanUtils.toBean(pageResult, BusinessDictionaryTypeRespDTO.class));
+ }
+
+ @Override
+ public CommonResult> getBusinessDictionaryDataListByDictionaryTypeId(Long dictionaryTypeId) {
+ List list = businessDictionaryTypeService.getBusinessDictionaryDataListByDictionaryTypeId(dictionaryTypeId);
+ return success(BeanUtils.toBean(list, BusinessDictionaryDataDTO.class));
+ }
+
+ @Override
+ public CommonResult> getBusinessDictionaryDataListByType(String type) {
+ List list = businessDictionaryTypeService.getBusinessDictionaryDataListByType(type);
+ return success(BeanUtils.toBean(list, BusinessDictionaryDataDTO.class));
+ }
+
+ private BusinessDictionaryTypeSaveReqVO convertSaveReq(BusinessDictionaryTypeSaveReqDTO dto) {
+ BusinessDictionaryTypeSaveReqVO reqVO = BeanUtils.toBean(dto, BusinessDictionaryTypeSaveReqVO.class);
+ if (dto.getBusinessDictionaryDatas() != null && !dto.getBusinessDictionaryDatas().isEmpty()) {
+ reqVO.setBusinessDictionaryDatas(BeanUtils.toBean(dto.getBusinessDictionaryDatas(), BusinessDictionaryDataDO.class));
+ }
+ return reqVO;
+ }
+}
diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/api/departmentmaterial/DepartmentMaterialApiImpl.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/api/departmentmaterial/DepartmentMaterialApiImpl.java
new file mode 100644
index 00000000..2c6fdad8
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/api/departmentmaterial/DepartmentMaterialApiImpl.java
@@ -0,0 +1,64 @@
+package com.zt.plat.module.base.api.departmentmaterial;
+
+import com.zt.plat.framework.common.pojo.CommonResult;
+import com.zt.plat.framework.common.pojo.PageResult;
+import com.zt.plat.framework.common.util.object.BeanUtils;
+import com.zt.plat.module.base.api.departmentmaterial.dto.DepartmentMaterialPageReqDTO;
+import com.zt.plat.module.base.api.departmentmaterial.dto.DepartmentMaterialRespDTO;
+import com.zt.plat.module.base.api.departmentmaterial.dto.DepartmentMaterialSaveReqDTO;
+import com.zt.plat.module.base.controller.admin.departmentmaterial.vo.DepartmentMaterialPageReqVO;
+import com.zt.plat.module.base.controller.admin.departmentmaterial.vo.DepartmentMaterialRespVO;
+import com.zt.plat.module.base.controller.admin.departmentmaterial.vo.DepartmentMaterialSaveReqVO;
+import com.zt.plat.module.base.service.departmentmaterial.DepartmentMaterialService;
+import jakarta.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+import static com.zt.plat.framework.common.pojo.CommonResult.success;
+
+@RestController
+@Validated
+public class DepartmentMaterialApiImpl implements DepartmentMaterialApi {
+
+ @Resource
+ private DepartmentMaterialService departmentMaterialService;
+
+ @Override
+ public CommonResult createDepartmentMaterial(DepartmentMaterialSaveReqDTO createReqDTO) {
+ DepartmentMaterialRespVO respVO = departmentMaterialService.createDepartmentMaterial(BeanUtils.toBean(createReqDTO, DepartmentMaterialSaveReqVO.class));
+ return success(BeanUtils.toBean(respVO, DepartmentMaterialRespDTO.class));
+ }
+
+ @Override
+ public CommonResult updateDepartmentMaterial(DepartmentMaterialSaveReqDTO updateReqDTO) {
+ departmentMaterialService.updateDepartmentMaterial(BeanUtils.toBean(updateReqDTO, DepartmentMaterialSaveReqVO.class));
+ return success(true);
+ }
+
+ @Override
+ public CommonResult deleteDepartmentMaterial(Long id) {
+ departmentMaterialService.deleteDepartmentMaterial(id);
+ return success(true);
+ }
+
+ @Override
+ public CommonResult deleteDepartmentMaterialList(List ids) {
+ departmentMaterialService.deleteDepartmentMaterialListByIds(ids);
+ return success(true);
+ }
+
+ @Override
+ public CommonResult getDepartmentMaterial(Long id) {
+ DepartmentMaterialRespVO respVO = departmentMaterialService.getDepartmentMaterial(id);
+ return success(BeanUtils.toBean(respVO, DepartmentMaterialRespDTO.class));
+ }
+
+ @Override
+ public CommonResult> getDepartmentMaterialPage(DepartmentMaterialPageReqDTO pageReqDTO) {
+ DepartmentMaterialPageReqVO pageReqVO = BeanUtils.toBean(pageReqDTO, DepartmentMaterialPageReqVO.class);
+ PageResult pageResult = departmentMaterialService.getDepartmentMaterialPage(pageReqVO);
+ return success(BeanUtils.toBean(pageResult, DepartmentMaterialRespDTO.class));
+ }
+}
diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/api/materialclasses/MaterialClassesApiImpl.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/api/materialclasses/MaterialClassesApiImpl.java
new file mode 100644
index 00000000..3a1ba044
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/api/materialclasses/MaterialClassesApiImpl.java
@@ -0,0 +1,97 @@
+package com.zt.plat.module.base.api.materialclasses;
+
+import com.zt.plat.framework.common.pojo.CommonResult;
+import com.zt.plat.framework.common.pojo.PageResult;
+import com.zt.plat.framework.common.util.object.BeanUtils;
+import com.zt.plat.module.base.api.materialclasses.dto.MaterialClassesPageReqDTO;
+import com.zt.plat.module.base.api.materialclasses.dto.MaterialClassesRespDTO;
+import com.zt.plat.module.base.api.materialclasses.dto.MaterialClassesSaveReqDTO;
+import com.zt.plat.module.base.api.materialclasses.dto.MaterialClassesTreeRespDTO;
+import com.zt.plat.module.base.controller.admin.materialclasses.vo.MaterialClassesPageReqVO;
+import com.zt.plat.module.base.controller.admin.materialclasses.vo.MaterialClassesRespVO;
+import com.zt.plat.module.base.controller.admin.materialclasses.vo.MaterialClassesSaveReqVO;
+import com.zt.plat.module.base.dal.dataobject.materialclasses.MaterialClassesDO;
+import com.zt.plat.module.base.service.materialclasses.MaterialClassesService;
+import jakarta.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static com.zt.plat.framework.common.pojo.CommonResult.success;
+
+@RestController
+@Validated
+public class MaterialClassesApiImpl implements MaterialClassesApi {
+
+ @Resource
+ private MaterialClassesService materialClassesService;
+
+ @Override
+ public CommonResult createMaterialClasses(MaterialClassesSaveReqDTO createReqDTO) {
+ MaterialClassesRespVO respVO = materialClassesService.createMaterialClasses(BeanUtils.toBean(createReqDTO, MaterialClassesSaveReqVO.class));
+ return success(BeanUtils.toBean(respVO, MaterialClassesRespDTO.class));
+ }
+
+ @Override
+ public CommonResult updateMaterialClasses(MaterialClassesSaveReqDTO updateReqDTO) {
+ materialClassesService.updateMaterialClasses(BeanUtils.toBean(updateReqDTO, MaterialClassesSaveReqVO.class));
+ return success(true);
+ }
+
+ @Override
+ public CommonResult deleteMaterialClasses(Long id) {
+ materialClassesService.deleteMaterialClasses(id);
+ return success(true);
+ }
+
+ @Override
+ public CommonResult deleteMaterialClassesList(List ids) {
+ materialClassesService.deleteMaterialClassesListByIds(ids);
+ return success(true);
+ }
+
+ @Override
+ public CommonResult getMaterialClasses(Long id) {
+ MaterialClassesDO classesDO = materialClassesService.getMaterialClasses(id);
+ return success(BeanUtils.toBean(classesDO, MaterialClassesRespDTO.class));
+ }
+
+ @Override
+ public CommonResult> getMaterialClassesPage(MaterialClassesPageReqDTO pageReqDTO) {
+ MaterialClassesPageReqVO pageReqVO = BeanUtils.toBean(pageReqDTO, MaterialClassesPageReqVO.class);
+ PageResult pageResult = materialClassesService.getMaterialClassesPage(pageReqVO);
+ return success(BeanUtils.toBean(pageResult, MaterialClassesRespDTO.class));
+ }
+
+ @Override
+ public CommonResult> getMaterialClassesTree() {
+ List list = materialClassesService.getMaterialClassesList();
+ return success(buildTree(list));
+ }
+
+ private List buildTree(List list) {
+ if (list == null || list.isEmpty()) {
+ return Collections.emptyList();
+ }
+ Map nodeMap = new LinkedHashMap<>();
+ list.stream()
+ .sorted(Comparator.comparing(MaterialClassesDO::getId))
+ .forEach(item -> nodeMap.put(item.getId(), BeanUtils.toBean(item, MaterialClassesTreeRespDTO.class)));
+ List roots = new ArrayList<>();
+ nodeMap.values().forEach(node -> {
+ Long parentId = node.getParentId();
+ if (parentId == null || parentId == 0 || !nodeMap.containsKey(parentId)) {
+ roots.add(node);
+ } else {
+ nodeMap.get(parentId).getChildren().add(node);
+ }
+ });
+ return roots;
+ }
+}
diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/controller/admin/base/MasterDataSyncController.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/controller/admin/base/MasterDataSyncController.java
new file mode 100644
index 00000000..be85284f
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/controller/admin/base/MasterDataSyncController.java
@@ -0,0 +1,51 @@
+package com.zt.plat.module.base.controller.admin.base;
+
+import com.zt.plat.framework.common.pojo.CommonResult;
+import com.zt.plat.framework.common.util.object.BeanUtils;
+import com.zt.plat.module.base.controller.admin.base.vo.MasterDataSyncReqVO;
+import com.zt.plat.module.base.service.masterdatasync.MasterDataCategorySyncService;
+import com.zt.plat.module.base.service.masterdatasync.MasterDataSyncService;
+import com.zt.plat.module.base.service.masterdatasync.dto.MasterDataSyncCommand;
+import com.zt.plat.module.base.service.masterdatasync.dto.MasterDataSyncReport;
+import com.zt.plat.module.base.service.masterdatasync.dto.MasterDataCategorySyncReport;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+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.RestController;
+
+import static com.zt.plat.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 主数据同步")
+@RestController
+@RequestMapping("/base/master-data-sync")
+@Validated
+public class MasterDataSyncController {
+
+ @Resource
+ private MasterDataSyncService masterDataSyncService;
+ @Resource
+ private MasterDataCategorySyncService masterDataCategorySyncService;
+
+ @PostMapping("/execute")
+ @Operation(summary = "执行主数据同步")
+ @PreAuthorize("@ss.hasPermission('base:master-data-sync:execute')")
+ public CommonResult execute(@Valid @RequestBody MasterDataSyncReqVO reqVO) {
+ MasterDataSyncCommand command = BeanUtils.toBean(reqVO, MasterDataSyncCommand.class);
+ MasterDataSyncReport report = masterDataSyncService.sync(command);
+ return success(report);
+ }
+
+ @PostMapping("/categories")
+ @Operation(summary = "同步物料分类")
+ @PreAuthorize("@ss.hasPermission('base:master-data-sync:categories')")
+ public CommonResult syncCategories() {
+ MasterDataCategorySyncReport report = masterDataCategorySyncService.syncAll();
+ return success(report);
+ }
+}
diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/controller/admin/base/vo/MasterDataSyncReqVO.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/controller/admin/base/vo/MasterDataSyncReqVO.java
new file mode 100644
index 00000000..a1559f9a
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/controller/admin/base/vo/MasterDataSyncReqVO.java
@@ -0,0 +1,31 @@
+package com.zt.plat.module.base.controller.admin.base.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Positive;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+@Schema(description = "主数据同步请求参数")
+public class MasterDataSyncReqVO {
+
+ @Schema(description = "增量同步的起始记录时间,留空执行全量")
+ @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime since;
+
+ @Schema(description = "拉取批大小,不填写则使用配置默认值")
+ @Positive(message = "batchSize 必须为正数")
+ private Integer batchSize;
+
+ @Schema(description = "指定要刷新同步的物料编码列表")
+ private List materialCodes;
+
+ @Schema(description = "可选的本次同步记录数上限,未填写表示不限制")
+ @Positive(message = "recordLimit 必须为正数")
+ private Long recordLimit;
+}
diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/dao/materialclasses/MaterialClassesMapper.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/dao/materialclasses/MaterialClassesMapper.java
index c4861958..aa40fce2 100644
--- a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/dao/materialclasses/MaterialClassesMapper.java
+++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/dao/materialclasses/MaterialClassesMapper.java
@@ -28,4 +28,12 @@ public interface MaterialClassesMapper extends BaseMapperX {
.orderByDesc(MaterialClassesDO::getId));
}
+ default List selectByCodes(Collection codes) {
+ if (codes == null || codes.isEmpty()) {
+ return Collections.emptyList();
+ }
+ return selectList(new LambdaQueryWrapperX()
+ .in(MaterialClassesDO::getCode, codes));
+ }
+
}
\ No newline at end of file
diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/dao/materialhasclasses/MaterialHasClassesMapper.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/dao/materialhasclasses/MaterialHasClassesMapper.java
index 8bbde0f2..ba887ced 100644
--- a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/dao/materialhasclasses/MaterialHasClassesMapper.java
+++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/dao/materialhasclasses/MaterialHasClassesMapper.java
@@ -25,4 +25,12 @@ public interface MaterialHasClassesMapper extends BaseMapperX selectByInfoIds(Collection infoIds) {
+ if (infoIds == null || infoIds.isEmpty()) {
+ return Collections.emptyList();
+ }
+ return selectList(new LambdaQueryWrapperX()
+ .in(MaterialHasClassesDO::getInfomationId, infoIds));
+ }
+
}
\ No newline at end of file
diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/dao/materialhasproperties/MaterialHasPropertiesMapper.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/dao/materialhasproperties/MaterialHasPropertiesMapper.java
index 824da813..0b91f690 100644
--- a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/dao/materialhasproperties/MaterialHasPropertiesMapper.java
+++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/dao/materialhasproperties/MaterialHasPropertiesMapper.java
@@ -30,4 +30,13 @@ public interface MaterialHasPropertiesMapper extends BaseMapperX selectByInfoIdsAndPropertyIds(Collection infoIds, Collection propertyIds) {
+ if (infoIds == null || infoIds.isEmpty() || propertyIds == null || propertyIds.isEmpty()) {
+ return Collections.emptyList();
+ }
+ return selectList(new LambdaQueryWrapperX()
+ .in(MaterialHasPropertiesDO::getInfomationId, infoIds)
+ .in(MaterialHasPropertiesDO::getPropertiesId, propertyIds));
+ }
+
}
\ No newline at end of file
diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/dao/materialproperties/MaterialPropertiesMapper.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/dao/materialproperties/MaterialPropertiesMapper.java
index 4165ba01..2a8061a6 100644
--- a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/dao/materialproperties/MaterialPropertiesMapper.java
+++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/dao/materialproperties/MaterialPropertiesMapper.java
@@ -9,6 +9,10 @@ import com.zt.plat.module.base.controller.admin.materialproperties.vo.MaterialPr
import com.zt.plat.module.base.dal.dataobject.materialproperties.MaterialPropertiesDO;
import org.apache.ibatis.annotations.Mapper;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
/**
* 物料属性 Mapper
*
@@ -47,4 +51,12 @@ public interface MaterialPropertiesMapper extends BaseMapperX selectByCodes(Collection codes) {
+ if (codes == null || codes.isEmpty()) {
+ return Collections.emptyList();
+ }
+ return selectList(new LambdaQueryWrapperX()
+ .in(MaterialPropertiesDO::getCode, codes));
+ }
+
}
\ No newline at end of file
diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/dataobject/masterdata/MdmMaterialCategoryDO.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/dataobject/masterdata/MdmMaterialCategoryDO.java
new file mode 100644
index 00000000..78d8954f
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/dataobject/masterdata/MdmMaterialCategoryDO.java
@@ -0,0 +1,35 @@
+package com.zt.plat.module.base.dal.dataobject.masterdata;
+
+import lombok.Data;
+
+/**
+ * 外部 MDM 分类视图的数据对象。
+ */
+@Data
+public class MdmMaterialCategoryDO {
+
+ /**
+ * 分类主键 ID(CODEID)。
+ */
+ private Long codeId;
+
+ /**
+ * 分类编码,例如 01 / 0101 / 010101。
+ */
+ private String code;
+
+ /**
+ * 分类名称(DESC1)。
+ */
+ private String name;
+
+ /**
+ * 备注信息(DESC2)。
+ */
+ private String remark;
+
+ /**
+ * 父级分类主键 ID(PARENTID)。
+ */
+ private Long parentCodeId;
+}
diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/dataobject/masterdata/MdmMaterialViewDO.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/dataobject/masterdata/MdmMaterialViewDO.java
new file mode 100644
index 00000000..ce5b59ac
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/dataobject/masterdata/MdmMaterialViewDO.java
@@ -0,0 +1,35 @@
+package com.zt.plat.module.base.dal.dataobject.masterdata;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 外部 MDM MySQL 物料视图的投影,用于承接同步字段。
+ */
+@Data
+public class MdmMaterialViewDO {
+
+ private Long codeId;
+ private String materialCode;
+ private String categoryCode;
+ private String categoryName;
+ private String materialName;
+ private String baseUnitCode;
+ private String baseUnitName;
+ private String shortDescription;
+ private String longDescription;
+ private String chalcoCode;
+ private String majorClassCode;
+ private String majorClassName;
+ private String specification;
+ private String model;
+ private String texture;
+ private String drawingNumber;
+ private String orderNumber;
+ private String otherParameters;
+ private String equipmentCategory;
+ private String manufacturer;
+ private String source;
+ private LocalDateTime recordTime;
+}
diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/mysql/masterdata/MdmMaterialCategoryMapper.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/mysql/masterdata/MdmMaterialCategoryMapper.java
new file mode 100644
index 00000000..ec8770ae
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/mysql/masterdata/MdmMaterialCategoryMapper.java
@@ -0,0 +1,21 @@
+package com.zt.plat.module.base.dal.mysql.masterdata;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.zt.plat.module.base.dal.dataobject.masterdata.MdmMaterialCategoryDO;
+import com.zt.plat.module.base.framework.sync.constant.MasterDataSyncConstants;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * 读取外部 MDM 分类视图的 Mapper。
+ */
+@Mapper
+@DS(MasterDataSyncConstants.DEFAULT_SOURCE_DATASOURCE)
+public interface MdmMaterialCategoryMapper {
+
+ /**
+ * 拉取全部分类档案。
+ */
+ List selectAll();
+}
diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/mysql/masterdata/MdmMaterialViewMapper.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/mysql/masterdata/MdmMaterialViewMapper.java
new file mode 100644
index 00000000..42d88f1c
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/dal/mysql/masterdata/MdmMaterialViewMapper.java
@@ -0,0 +1,27 @@
+package com.zt.plat.module.base.dal.mysql.masterdata;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.zt.plat.module.base.dal.dataobject.masterdata.MdmMaterialViewDO;
+import com.zt.plat.module.base.framework.sync.constant.MasterDataSyncConstants;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.time.LocalDateTime;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Mapper that exposes read-only access to the external MDM material view.
+ */
+@Mapper
+@DS(MasterDataSyncConstants.DEFAULT_SOURCE_DATASOURCE)
+public interface MdmMaterialViewMapper {
+
+ List selectSlice(@Param("offset") long offset,
+ @Param("limit") int limit,
+ @Param("since") LocalDateTime since,
+ @Param("codes") Collection codes);
+
+ Long countEligible(@Param("since") LocalDateTime since,
+ @Param("codes") Collection codes);
+}
diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/framework/sync/config/MasterDataSyncConfiguration.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/framework/sync/config/MasterDataSyncConfiguration.java
new file mode 100644
index 00000000..41fa787a
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/framework/sync/config/MasterDataSyncConfiguration.java
@@ -0,0 +1,24 @@
+package com.zt.plat.module.base.framework.sync.config;
+
+import okhttp3.OkHttpClient;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 主数据同步用到的基础配置。
+ */
+@Configuration
+@EnableConfigurationProperties(MasterDataSyncProperties.class)
+public class MasterDataSyncConfiguration {
+
+ @Bean
+ public OkHttpClient masterDataSyncOkHttpClient(MasterDataSyncProperties properties) {
+ MasterDataSyncProperties.HttpProperties http = properties.getHttp();
+ return new OkHttpClient.Builder()
+ .connectTimeout(http.getConnectTimeout())
+ .readTimeout(http.getReadTimeout())
+ .writeTimeout(http.getWriteTimeout())
+ .build();
+ }
+}
diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/framework/sync/config/MasterDataSyncProperties.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/framework/sync/config/MasterDataSyncProperties.java
new file mode 100644
index 00000000..2ef02810
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/framework/sync/config/MasterDataSyncProperties.java
@@ -0,0 +1,68 @@
+package com.zt.plat.module.base.framework.sync.config;
+
+import com.zt.plat.module.base.framework.sync.constant.MasterDataSyncConstants;
+import jakarta.validation.constraints.Min;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.validation.annotation.Validated;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 主数据同步管道的配置项。
+ */
+@Data
+@Validated
+@ConfigurationProperties(prefix = "base.master-data-sync")
+public class MasterDataSyncProperties {
+
+ /**
+ * 是否允许运行主数据同步。
+ */
+ private boolean enabled = true;
+
+ /**
+ * 从外部数据源读取时的默认批量大小。
+ */
+ @Min(1)
+ private int batchSize = 500;
+
+ /**
+ * 指向外部 MDM MySQL 的动态数据源名称。
+ */
+ private String sourceDatasourceName = MasterDataSyncConstants.DEFAULT_SOURCE_DATASOURCE;
+
+ /**
+ * 成功是否进行回调通知。
+ */
+ private boolean notifyOnSuccess = true;
+
+ /**
+ * 失败是否进行回调通知。
+ */
+ private boolean notifyOnFailure = true;
+
+ /**
+ * 可选的回调地址,用于推送同步结果。
+ */
+ private String callbackUrl;
+
+ /**
+ * 回调请求需要附加的静态请求头。
+ */
+ private Map callbackHeaders = new HashMap<>();
+
+ /**
+ * HTTP 客户端的超时时间等参数。
+ */
+ private final HttpProperties http = new HttpProperties();
+
+ @Data
+ public static class HttpProperties {
+ private Duration connectTimeout = Duration.ofSeconds(5);
+ private Duration readTimeout = Duration.ofSeconds(30);
+ private Duration writeTimeout = Duration.ofSeconds(30);
+ }
+}
diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/framework/sync/constant/MasterDataSyncConstants.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/framework/sync/constant/MasterDataSyncConstants.java
new file mode 100644
index 00000000..4032604d
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/framework/sync/constant/MasterDataSyncConstants.java
@@ -0,0 +1,15 @@
+package com.zt.plat.module.base.framework.sync.constant;
+
+/**
+ * 主数据同步相关的通用常量。
+ */
+public final class MasterDataSyncConstants {
+
+ private MasterDataSyncConstants() {
+ }
+
+ /**
+ * 指向外部 MDM MySQL 的默认数据源名称。
+ */
+ public static final String DEFAULT_SOURCE_DATASOURCE = "mdm";
+}
diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/MasterDataCategorySyncService.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/MasterDataCategorySyncService.java
new file mode 100644
index 00000000..3e33148d
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/MasterDataCategorySyncService.java
@@ -0,0 +1,11 @@
+package com.zt.plat.module.base.service.masterdatasync;
+
+import com.zt.plat.module.base.service.masterdatasync.dto.MasterDataCategorySyncReport;
+
+/**
+ * 物料分类同步服务接口。
+ */
+public interface MasterDataCategorySyncService {
+
+ MasterDataCategorySyncReport syncAll();
+}
diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/MasterDataCategorySyncServiceImpl.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/MasterDataCategorySyncServiceImpl.java
new file mode 100644
index 00000000..0c0a7e07
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/MasterDataCategorySyncServiceImpl.java
@@ -0,0 +1,214 @@
+package com.zt.plat.module.base.service.masterdatasync;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
+import com.zt.plat.module.base.dal.dao.materialclasses.MaterialClassesMapper;
+import com.zt.plat.module.base.dal.dataobject.materialclasses.MaterialClassesDO;
+import com.zt.plat.module.base.dal.dataobject.masterdata.MdmMaterialCategoryDO;
+import com.zt.plat.module.base.dal.mysql.masterdata.MdmMaterialCategoryMapper;
+import com.zt.plat.module.base.enums.ErrorCodeConstants;
+import com.zt.plat.module.base.framework.sync.config.MasterDataSyncProperties;
+import com.zt.plat.module.base.service.masterdatasync.dto.MasterDataCategorySyncReport;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * 物料分类同步实现。
+ */
+@Slf4j
+@Service
+public class MasterDataCategorySyncServiceImpl implements MasterDataCategorySyncService {
+
+ private static final long ROOT_CLASS_PARENT_ID = 0L;
+
+ @Resource
+ private MdmMaterialCategoryMapper mdmMaterialCategoryMapper;
+ @Resource
+ private MaterialClassesMapper materialClassesMapper;
+ @Resource
+ private MasterDataSyncProperties properties;
+
+ @Override
+ public MasterDataCategorySyncReport syncAll() {
+ if (!properties.isEnabled()) {
+ throw ServiceExceptionUtil.exception(ErrorCodeConstants.MASTER_DATA_SYNC_DISABLED);
+ }
+ MasterDataCategorySyncReport report = MasterDataCategorySyncReport.start();
+ try {
+ List categories = mdmMaterialCategoryMapper.selectAll();
+ if (CollUtil.isEmpty(categories)) {
+ report.markSuccess();
+ return report;
+ }
+ categories.sort(Comparator.comparing(MdmMaterialCategoryDO::getCode));
+ Map classesByCode = loadExistingClasses(categories);
+ Map classesById = classesByCode.values().stream()
+ .filter(item -> item.getId() != null)
+ .collect(Collectors.toMap(MaterialClassesDO::getId, item -> item, (a, b) -> a, LinkedHashMap::new));
+ // 统一收集新增分类,批量写入提升性能
+ List pendingClassInserts = new ArrayList<>();
+ for (MdmMaterialCategoryDO category : categories) {
+ Long sourceId = category.getCodeId();
+ if (sourceId == null) {
+ log.warn("分类 {} 缺少主键 CODEID,已跳过", category.getCode());
+ continue;
+ }
+ String code = normalizeCode(category.getCode());
+ if (code == null) {
+ continue;
+ }
+ report.incrementProcessed(1);
+ long level = determineLevel(code);
+ Long parentId = resolveParentId(category, classesByCode, classesById, report);
+ if (parentId == null) {
+ parentId = ROOT_CLASS_PARENT_ID;
+ }
+ String name = StrUtil.emptyIfNull(StrUtil.trim(category.getName()));
+ String remark = StrUtil.emptyIfNull(StrUtil.trim(category.getRemark()));
+ MaterialClassesDO current = classesByCode.get(code);
+ if (current == null) {
+ MaterialClassesDO created = MaterialClassesDO.builder()
+ .id(sourceId)
+ .code(code)
+ .name(name)
+ .level(level)
+ .parentId(parentId)
+ .remark(remark)
+ .build();
+ pendingClassInserts.add(created);
+ classesByCode.put(code, created);
+ if (created.getId() != null) {
+ classesById.put(created.getId(), created);
+ }
+ report.incrementInserted();
+ } else {
+ if (current.getId() == null && sourceId != null) {
+ current.setId(sourceId);
+ classesById.put(sourceId, current);
+ }
+ boolean needUpdate = false;
+ MaterialClassesDO update = new MaterialClassesDO();
+ update.setId(current.getId());
+ if (!Objects.equals(current.getParentId(), parentId)) {
+ update.setParentId(parentId);
+ current.setParentId(parentId);
+ needUpdate = true;
+ }
+ if (StrUtil.isNotBlank(name) && !StrUtil.equals(current.getName(), name)) {
+ update.setName(name);
+ current.setName(name);
+ needUpdate = true;
+ }
+ if (!Objects.equals(current.getLevel(), level)) {
+ update.setLevel(level);
+ current.setLevel(level);
+ needUpdate = true;
+ }
+ if (!Objects.equals(StrUtil.emptyIfNull(current.getRemark()), remark)) {
+ update.setRemark(remark);
+ current.setRemark(remark);
+ needUpdate = true;
+ }
+ if (needUpdate) {
+ materialClassesMapper.updateById(update);
+ report.incrementUpdated();
+ }
+ }
+ }
+ if (CollUtil.isNotEmpty(pendingClassInserts)) {
+ materialClassesMapper.insertBatch(pendingClassInserts);
+ }
+ report.markSuccess();
+ return report;
+ } catch (Throwable ex) {
+ report.markFailure(ex.getMessage());
+ throw ex;
+ } finally {
+ report.finish();
+ }
+ }
+
+ private Map loadExistingClasses(List categories) {
+ Set codes = categories.stream()
+ .map(MdmMaterialCategoryDO::getCode)
+ .map(this::normalizeCode)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+ List existing = materialClassesMapper.selectByCodes(codes);
+ Map result = new LinkedHashMap<>();
+ if (existing != null) {
+ existing.forEach(item -> {
+ String key = normalizeCode(item.getCode());
+ if (key != null) {
+ result.put(key, item);
+ }
+ });
+ }
+ return result;
+ }
+
+ private Long resolveParentId(MdmMaterialCategoryDO category,
+ Map classesByCode,
+ Map classesById,
+ MasterDataCategorySyncReport report) {
+ if (category.getParentCodeId() != null && category.getParentCodeId() > 0) {
+ MaterialClassesDO parent = classesById.get(category.getParentCodeId());
+ if (parent != null) {
+ return parent.getId();
+ }
+ log.warn("分类 {} 指定父级 ID {} 不存在,将尝试按编码推断", category.getCode(), category.getParentCodeId());
+ }
+ String parentCode = resolveParentCode(normalizeCode(category.getCode()));
+ if (parentCode == null) {
+ return ROOT_CLASS_PARENT_ID;
+ }
+ MaterialClassesDO parent = classesByCode.get(parentCode);
+ if (parent == null) {
+ log.warn("分类 {} 缺失父级 {},将挂载到根节点", category.getCode(), parentCode);
+ if (report != null) {
+ report.incrementMissingParent();
+ }
+ return null;
+ }
+ return parent.getId();
+ }
+
+ private String resolveParentCode(String code) {
+ if (code == null) {
+ return null;
+ }
+ if (code.length() <= 2) {
+ return null;
+ }
+ if (code.length() <= 4) {
+ return code.substring(0, 2);
+ }
+ return code.substring(0, 4);
+ }
+
+ private long determineLevel(String code) {
+ if (code.length() <= 2) {
+ return 1L;
+ }
+ if (code.length() <= 4) {
+ return 2L;
+ }
+ return 3L;
+ }
+
+ private String normalizeCode(String code) {
+ return StrUtil.isBlank(code) ? null : StrUtil.trim(code);
+ }
+}
diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/MasterDataSyncService.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/MasterDataSyncService.java
new file mode 100644
index 00000000..3ffadd3f
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/MasterDataSyncService.java
@@ -0,0 +1,12 @@
+package com.zt.plat.module.base.service.masterdatasync;
+
+import com.zt.plat.module.base.service.masterdatasync.dto.MasterDataSyncCommand;
+import com.zt.plat.module.base.service.masterdatasync.dto.MasterDataSyncReport;
+
+/**
+ * 物料主数据同步的服务入口。
+ */
+public interface MasterDataSyncService {
+
+ MasterDataSyncReport sync(MasterDataSyncCommand command);
+}
diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/MasterDataSyncServiceImpl.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/MasterDataSyncServiceImpl.java
new file mode 100644
index 00000000..3fd3f984
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/MasterDataSyncServiceImpl.java
@@ -0,0 +1,488 @@
+package com.zt.plat.module.base.service.masterdatasync;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.toolkit.IdWorker;
+import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
+import com.zt.plat.module.base.dal.dao.materialclasses.MaterialClassesMapper;
+import com.zt.plat.module.base.dal.dao.materialhasclasses.MaterialHasClassesMapper;
+import com.zt.plat.module.base.dal.dao.materialhasproperties.MaterialHasPropertiesMapper;
+import com.zt.plat.module.base.dal.dao.materialproperties.MaterialPropertiesMapper;
+import com.zt.plat.module.base.dal.dataobject.base.MaterialInfomationDO;
+import com.zt.plat.module.base.dal.dataobject.materialclasses.MaterialClassesDO;
+import com.zt.plat.module.base.dal.dataobject.materialhasclasses.MaterialHasClassesDO;
+import com.zt.plat.module.base.dal.dataobject.materialhasproperties.MaterialHasPropertiesDO;
+import com.zt.plat.module.base.dal.dataobject.materialproperties.MaterialPropertiesDO;
+import com.zt.plat.module.base.dal.dataobject.masterdata.MdmMaterialViewDO;
+import com.zt.plat.module.base.dal.mysql.base.MaterialInfomationMapper;
+import com.zt.plat.module.base.dal.mysql.masterdata.MdmMaterialViewMapper;
+import com.zt.plat.module.base.enums.ErrorCodeConstants;
+import com.zt.plat.module.base.framework.sync.config.MasterDataSyncProperties;
+import com.zt.plat.module.base.service.masterdatasync.dto.MasterDataSyncCommand;
+import com.zt.plat.module.base.service.masterdatasync.dto.MasterDataSyncReport;
+import com.zt.plat.module.base.service.masterdatasync.support.MasterDataPropertyDefinition;
+import com.zt.plat.module.base.service.masterdatasync.support.MasterDataSyncNotifier;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.support.TransactionTemplate;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 主数据同步核心实现,负责从外部 MDM 视图读取数据并落库到本地 BSE 表。
+ */
+@Slf4j
+@Service
+public class MasterDataSyncServiceImpl implements MasterDataSyncService {
+
+ private static final long ROOT_CLASS_PARENT_ID = 0L;
+
+ @Resource
+ private MdmMaterialViewMapper mdmMaterialViewMapper;
+ @Resource
+ private MaterialInfomationMapper materialInfomationMapper;
+ @Resource
+ private MaterialClassesMapper materialClassesMapper;
+ @Resource
+ private MaterialHasClassesMapper materialHasClassesMapper;
+ @Resource
+ private MaterialPropertiesMapper materialPropertiesMapper;
+ @Resource
+ private MaterialHasPropertiesMapper materialHasPropertiesMapper;
+ @Resource
+ private TransactionTemplate transactionTemplate;
+ @Resource
+ private MasterDataSyncProperties properties;
+ @Resource
+ private MasterDataSyncNotifier notifier;
+
+ @Override
+ public MasterDataSyncReport sync(MasterDataSyncCommand command) {
+ if (!properties.isEnabled()) {
+ throw ServiceExceptionUtil.exception(ErrorCodeConstants.MASTER_DATA_SYNC_DISABLED);
+ }
+ MasterDataSyncReport report = MasterDataSyncReport.start(command);
+ List materialCodes = sanitizeCodes(command.getMaterialCodes());
+ report.setMaterialCodes(materialCodes);
+ int batchSize = resolveBatchSize(command.getBatchSize());
+ report.setBatchSize(batchSize);
+ Long recordLimit = resolveRecordLimit(command.getRecordLimit());
+ report.setRecordLimit(recordLimit);
+ LocalDateTime since = command.getSince();
+ Map propertyDefinitions = ensurePropertyDefinitions(report);
+ // 缓存已读取的物料与分类,避免批次间重复查询数据库
+ Map materialCacheById = new LinkedHashMap<>();
+ Map materialCacheByCode = new LinkedHashMap<>();
+ Map categoryCacheByCode = new LinkedHashMap<>();
+
+ long offset = 0L;
+ Throwable failure = null;
+ try {
+ while (true) {
+ int fetchSize = batchSize;
+ if (recordLimit != null) {
+ long remaining = recordLimit - report.getProcessedRecords();
+ if (remaining <= 0) {
+ break;
+ }
+ if (remaining < fetchSize) {
+ fetchSize = (int) remaining;
+ }
+ }
+ List slice = mdmMaterialViewMapper.selectSlice(offset, fetchSize, since, materialCodes);
+ if (CollUtil.isEmpty(slice)) {
+ break;
+ }
+ offset += slice.size();
+ report.incrementProcessedRecords(slice.size());
+ final List batch = slice;
+ transactionTemplate.executeWithoutResult(status -> processBatch(batch, propertyDefinitions, report,
+ materialCacheById, materialCacheByCode, categoryCacheByCode));
+ }
+ report.markSuccess();
+ return report;
+ } catch (Throwable ex) {
+ failure = ex;
+ report.markFailure(ex.getMessage());
+ throw ex;
+ } finally {
+ report.finish();
+ notifier.dispatch(report, failure);
+ }
+ }
+
+ private List sanitizeCodes(List codes) {
+ if (CollUtil.isEmpty(codes)) {
+ return Collections.emptyList();
+ }
+ return codes.stream()
+ .filter(StrUtil::isNotBlank)
+ .map(code -> StrUtil.trim(code).toUpperCase())
+ .distinct()
+ .collect(Collectors.toList());
+ }
+
+ private int resolveBatchSize(Integer requested) {
+ if (requested != null && requested > 0) {
+ return requested;
+ }
+ return Math.max(1, properties.getBatchSize());
+ }
+
+ private Long resolveRecordLimit(Long requested) {
+ if (requested == null || requested <= 0) {
+ return null;
+ }
+ return requested;
+ }
+
+ /**
+ * 确保所有需要的属性定义已经存在,若缺失则自动创建。
+ */
+ private Map ensurePropertyDefinitions(MasterDataSyncReport report) {
+ List codes = Arrays.stream(MasterDataPropertyDefinition.values())
+ .map(MasterDataPropertyDefinition::getCode)
+ .toList();
+ List existing = materialPropertiesMapper.selectByCodes(codes);
+ Map existingMap = Optional.ofNullable(existing)
+ .orElse(Collections.emptyList())
+ .stream()
+ .collect(Collectors.toMap(MaterialPropertiesDO::getCode, Function.identity()));
+
+ Map mapping = new EnumMap<>(MasterDataPropertyDefinition.class);
+ List pendingInsertDefinitions = new ArrayList<>();
+ for (MasterDataPropertyDefinition definition : MasterDataPropertyDefinition.values()) {
+ MaterialPropertiesDO current = existingMap.get(definition.getCode());
+ if (current == null) {
+ current = MaterialPropertiesDO.builder()
+ .id(IdWorker.getId())
+ .code(definition.getCode())
+ .name(definition.getDisplayName())
+ .dataType(definition.getDataType())
+ .remark("MDM同步自动创建")
+ .build();
+ pendingInsertDefinitions.add(current);
+ report.incrementInsertedPropertyDefinitions();
+ } else {
+ boolean needUpdate = false;
+ if (!StrUtil.equals(current.getName(), definition.getDisplayName())) {
+ current.setName(definition.getDisplayName());
+ needUpdate = true;
+ }
+ if (!StrUtil.equals(current.getDataType(), definition.getDataType())) {
+ current.setDataType(definition.getDataType());
+ needUpdate = true;
+ }
+ if (needUpdate) {
+ MaterialPropertiesDO update = new MaterialPropertiesDO();
+ update.setId(current.getId());
+ update.setName(current.getName());
+ update.setDataType(current.getDataType());
+ materialPropertiesMapper.updateById(update);
+ report.incrementUpdatedPropertyDefinitions();
+ }
+ }
+ mapping.put(definition, current);
+ }
+ if (CollUtil.isNotEmpty(pendingInsertDefinitions)) {
+ materialPropertiesMapper.insertBatch(pendingInsertDefinitions);
+ }
+ return mapping;
+ }
+
+ private void processBatch(List batch,
+ Map propertyDefinitions,
+ MasterDataSyncReport report,
+ Map materialCacheById,
+ Map materialCacheByCode,
+ Map categoryCacheByCode) {
+ if (CollUtil.isEmpty(batch)) {
+ return;
+ }
+ // 1. 落库物料主数据
+ Map materials = upsertMaterials(batch, report, materialCacheById, materialCacheByCode);
+ // 2. 读取已存在的分类信息
+ Map categoryClasses = loadCategoryClasses(batch, categoryCacheByCode);
+ // 3. 建立物料与分类的关系
+ bindMaterialClasses(batch, materials, categoryClasses, report);
+ // 4. 写入物料属性及属性值
+ upsertMaterialProperties(batch, materials, propertyDefinitions, report);
+ }
+
+ private Map loadCategoryClasses(List batch,
+ Map categoryCacheByCode) {
+ Set requestedCodes = batch.stream()
+ .map(MdmMaterialViewDO::getCategoryCode)
+ .map(this::normalizeValue)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+ if (requestedCodes.isEmpty()) {
+ return Collections.emptyMap();
+ }
+ Map mapping = requestedCodes.stream()
+ .map(code -> new Object[]{code, categoryCacheByCode.get(code)})
+ .filter(pair -> pair[1] != null)
+ .collect(Collectors.toMap(pair -> (String) pair[0], pair -> (MaterialClassesDO) pair[1], (a, b) -> a, LinkedHashMap::new));
+ Set missing = new LinkedHashSet<>(requestedCodes);
+ missing.removeIf(mapping::containsKey);
+ if (!missing.isEmpty()) {
+ List newlyLoaded = materialClassesMapper.selectByCodes(missing);
+ Optional.ofNullable(newlyLoaded)
+ .orElse(Collections.emptyList())
+ .forEach(item -> {
+ String code = normalizeValue(item.getCode());
+ if (code != null) {
+ categoryCacheByCode.put(code, item);
+ mapping.put(code, item);
+ }
+ });
+ missing.removeIf(mapping::containsKey);
+ if (!missing.isEmpty()) {
+ List preview = missing.stream().limit(10).toList();
+ log.warn("当前批次存在 {} 个分类未同步,示例: {}", missing.size(), preview);
+ }
+ }
+ return mapping;
+ }
+
+ private Map upsertMaterials(List batch,
+ MasterDataSyncReport report,
+ Map materialCacheById,
+ Map materialCacheByCode) {
+ Set sourceIds = batch.stream()
+ .map(MdmMaterialViewDO::getCodeId)
+ .filter(Objects::nonNull)
+ .filter(id -> id > 0)
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+ if (sourceIds.isEmpty()) {
+ log.warn("本次批次缺少有效 CODEID,无法同步物料主数据");
+ return Collections.emptyMap();
+ }
+ Map existingById = sourceIds.stream()
+ .map(materialCacheById::get)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toMap(MaterialInfomationDO::getId, Function.identity(), (a, b) -> a, LinkedHashMap::new));
+ Set missingIds = new LinkedHashSet<>(sourceIds);
+ missingIds.removeIf(existingById::containsKey);
+ if (!missingIds.isEmpty()) {
+ List fetched = materialInfomationMapper.selectBatchIds(missingIds);
+ Optional.ofNullable(fetched)
+ .orElse(Collections.emptyList())
+ .forEach(item -> {
+ if (item.getId() != null) {
+ existingById.put(item.getId(), item);
+ materialCacheById.put(item.getId(), item);
+ String normalizedCode = normalizeValue(item.getCode());
+ if (normalizedCode != null) {
+ materialCacheByCode.put(normalizedCode, item);
+ }
+ }
+ });
+ }
+ Map mapping = new LinkedHashMap<>();
+ List pendingMaterialInserts = new ArrayList<>();
+ for (MdmMaterialViewDO item : batch) {
+ Long codeId = item.getCodeId();
+ if (codeId == null || codeId <= 0) {
+ log.warn("物料 {} 缺少主键 CODEID,已跳过", item.getMaterialCode());
+ continue;
+ }
+ String code = normalizeValue(item.getMaterialCode());
+ if (code == null) {
+ continue;
+ }
+ MaterialInfomationDO current = existingById.get(codeId);
+ String name = normalizeValue(item.getMaterialName());
+ if (current == null) {
+ MaterialInfomationDO created = MaterialInfomationDO.builder()
+ .id(codeId)
+ .code(code)
+ .name(StrUtil.emptyIfNull(name))
+ .remark(null)
+ .build();
+ pendingMaterialInserts.add(created);
+ existingById.put(codeId, created);
+ materialCacheById.put(codeId, created);
+ materialCacheByCode.put(code, created);
+ mapping.put(code, created);
+ report.incrementInsertedMaterials();
+ continue;
+ }
+
+ boolean needUpdate = false;
+ MaterialInfomationDO update = null;
+ if (!StrUtil.equals(current.getCode(), code)) {
+ current.setCode(code);
+ needUpdate = true;
+ }
+ if (name != null && !StrUtil.equals(current.getName(), name)) {
+ current.setName(name);
+ needUpdate = true;
+ }
+ if (needUpdate) {
+ update = new MaterialInfomationDO();
+ update.setId(current.getId());
+ update.setCode(current.getCode());
+ update.setName(current.getName());
+ materialInfomationMapper.updateById(update);
+ report.incrementUpdatedMaterials();
+ }
+ mapping.put(code, current);
+ materialCacheByCode.put(code, current);
+ }
+ if (CollUtil.isNotEmpty(pendingMaterialInserts)) {
+ materialInfomationMapper.insertBatch(pendingMaterialInserts);
+ }
+ return mapping;
+ }
+
+ private void bindMaterialClasses(List batch,
+ Map materials,
+ Map categoryClasses,
+ MasterDataSyncReport report) {
+ if (materials.isEmpty() || categoryClasses.isEmpty()) {
+ return;
+ }
+ List infoIds = batch.stream()
+ .map(MdmMaterialViewDO::getMaterialCode)
+ .map(this::normalizeValue)
+ .map(materials::get)
+ .filter(Objects::nonNull)
+ .map(MaterialInfomationDO::getId)
+ .filter(Objects::nonNull)
+ .distinct()
+ .collect(Collectors.toList());
+ Map existingRelations = materialHasClassesMapper.selectByInfoIds(infoIds)
+ .stream()
+ .collect(Collectors.toMap(MaterialHasClassesDO::getInfomationId, Function.identity(), (a, b) -> a));
+ List relationsToInsert = new ArrayList<>();
+ for (MdmMaterialViewDO item : batch) {
+ String materialCode = normalizeValue(item.getMaterialCode());
+ MaterialInfomationDO info = materials.get(materialCode);
+ if (info == null) {
+ continue;
+ }
+ String categoryCode = normalizeValue(item.getCategoryCode());
+ MaterialClassesDO cls = categoryClasses.get(categoryCode);
+ if (cls == null) {
+ continue;
+ }
+ MaterialHasClassesDO relation = existingRelations.get(info.getId());
+ if (relation == null) {
+ MaterialHasClassesDO created = MaterialHasClassesDO.builder()
+ .infomationId(info.getId())
+ .classesId(cls.getId())
+ .build();
+ relationsToInsert.add(created);
+ existingRelations.put(info.getId(), created);
+ report.incrementCreatedClassRelations();
+ } else if (!Objects.equals(relation.getClassesId(), cls.getId())) {
+ MaterialHasClassesDO update = new MaterialHasClassesDO();
+ update.setId(relation.getId());
+ update.setClassesId(cls.getId());
+ materialHasClassesMapper.updateById(update);
+ relation.setClassesId(cls.getId());
+ report.incrementUpdatedClassRelations();
+ }
+ }
+ if (CollUtil.isNotEmpty(relationsToInsert)) {
+ materialHasClassesMapper.insertBatch(relationsToInsert);
+ }
+ }
+
+ private void upsertMaterialProperties(List batch,
+ Map materials,
+ Map propertyDefinitions,
+ MasterDataSyncReport report) {
+ if (materials.isEmpty() || propertyDefinitions.isEmpty()) {
+ return;
+ }
+ List infoIds = batch.stream()
+ .map(MdmMaterialViewDO::getMaterialCode)
+ .map(this::normalizeValue)
+ .map(materials::get)
+ .filter(Objects::nonNull)
+ .map(MaterialInfomationDO::getId)
+ .filter(Objects::nonNull)
+ .distinct()
+ .collect(Collectors.toList());
+ List propertyIds = propertyDefinitions.values().stream()
+ .map(MaterialPropertiesDO::getId)
+ .filter(Objects::nonNull)
+ .distinct()
+ .collect(Collectors.toList());
+ Map existing = materialHasPropertiesMapper
+ .selectByInfoIdsAndPropertyIds(infoIds, propertyIds)
+ .stream()
+ .collect(Collectors.toMap(item -> buildRelationKey(item.getInfomationId(), item.getPropertiesId()),
+ Function.identity(), (a, b) -> a));
+ List propertyRelationsToInsert = new ArrayList<>();
+ for (MdmMaterialViewDO item : batch) {
+ String materialCode = normalizeValue(item.getMaterialCode());
+ MaterialInfomationDO info = materials.get(materialCode);
+ if (info == null || info.getId() == null) {
+ continue;
+ }
+ for (Map.Entry entry : propertyDefinitions.entrySet()) {
+ MaterialPropertiesDO property = entry.getValue();
+ if (property == null || property.getId() == null) {
+ continue;
+ }
+ String value = normalizeValue(entry.getKey().extractValue(item));
+ if (value == null) {
+ continue;
+ }
+ String key = buildRelationKey(info.getId(), property.getId());
+ MaterialHasPropertiesDO relation = existing.get(key);
+ if (relation == null) {
+ MaterialHasPropertiesDO created = MaterialHasPropertiesDO.builder()
+ .infomationId(info.getId())
+ .propertiesId(property.getId())
+ .value(value)
+ .sort(entry.getKey().getSort())
+ .isKey(0)
+ .isMetering(0)
+ .build();
+ propertyRelationsToInsert.add(created);
+ existing.put(key, created);
+ report.incrementInsertedPropertyValues();
+ } else if (!StrUtil.equals(relation.getValue(), value)
+ || !Objects.equals(relation.getSort(), entry.getKey().getSort())) {
+ MaterialHasPropertiesDO update = new MaterialHasPropertiesDO();
+ update.setId(relation.getId());
+ update.setValue(value);
+ update.setSort(entry.getKey().getSort());
+ materialHasPropertiesMapper.updateById(update);
+ relation.setValue(value);
+ relation.setSort(entry.getKey().getSort());
+ report.incrementUpdatedPropertyValues();
+ }
+ }
+ }
+ if (CollUtil.isNotEmpty(propertyRelationsToInsert)) {
+ materialHasPropertiesMapper.insertBatch(propertyRelationsToInsert);
+ }
+ }
+
+ private String buildRelationKey(Long infoId, Long propertyId) {
+ return infoId + ":" + propertyId;
+ }
+
+ private String normalizeValue(String value) {
+ return StrUtil.isBlank(value) ? null : StrUtil.trim(value);
+ }
+}
diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/dto/MasterDataCategorySyncReport.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/dto/MasterDataCategorySyncReport.java
new file mode 100644
index 00000000..50985d12
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/dto/MasterDataCategorySyncReport.java
@@ -0,0 +1,59 @@
+package com.zt.plat.module.base.service.masterdatasync.dto;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 物料分类同步执行结果。
+ */
+@Data
+public class MasterDataCategorySyncReport {
+
+ private long processedCategories;
+ private long insertedCategories;
+ private long updatedCategories;
+ private long missingParentReferences;
+ private boolean success;
+ private String message;
+ private LocalDateTime startedAt;
+ private LocalDateTime finishedAt;
+
+ public static MasterDataCategorySyncReport start() {
+ MasterDataCategorySyncReport report = new MasterDataCategorySyncReport();
+ report.setStartedAt(LocalDateTime.now());
+ report.setSuccess(false);
+ report.setMessage("执行中");
+ return report;
+ }
+
+ public void markSuccess() {
+ this.success = true;
+ this.message = "执行成功";
+ }
+
+ public void markFailure(String message) {
+ this.success = false;
+ this.message = message;
+ }
+
+ public void finish() {
+ this.finishedAt = LocalDateTime.now();
+ }
+
+ public void incrementProcessed(long delta) {
+ this.processedCategories += delta;
+ }
+
+ public void incrementInserted() {
+ this.insertedCategories++;
+ }
+
+ public void incrementUpdated() {
+ this.updatedCategories++;
+ }
+
+ public void incrementMissingParent() {
+ this.missingParentReferences++;
+ }
+}
diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/dto/MasterDataSyncCommand.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/dto/MasterDataSyncCommand.java
new file mode 100644
index 00000000..cbc32777
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/dto/MasterDataSyncCommand.java
@@ -0,0 +1,33 @@
+package com.zt.plat.module.base.service.masterdatasync.dto;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 主数据同步的入参模型。
+ */
+@Data
+public class MasterDataSyncCommand {
+
+ /**
+ * 增量同步的起始时间,空值表示全量。
+ */
+ private LocalDateTime since;
+
+ /**
+ * 单批拉取的记录数,空值取配置默认值。
+ */
+ private Integer batchSize;
+
+ /**
+ * 需要限定同步范围的物料编码集合。
+ */
+ private List materialCodes;
+
+ /**
+ * 本次同步允许处理的最大记录数,空值表示不限制。
+ */
+ private Long recordLimit;
+}
diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/dto/MasterDataSyncReport.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/dto/MasterDataSyncReport.java
new file mode 100644
index 00000000..aa5300ca
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/dto/MasterDataSyncReport.java
@@ -0,0 +1,104 @@
+package com.zt.plat.module.base.service.masterdatasync.dto;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 主数据同步的执行结果统计。
+ */
+@Data
+public class MasterDataSyncReport {
+
+ private long processedRecords;
+ private long insertedMaterials;
+ private long updatedMaterials;
+ private long insertedClasses;
+ private long updatedClasses;
+ private long createdClassRelations;
+ private long updatedClassRelations;
+ private long insertedPropertyDefinitions;
+ private long updatedPropertyDefinitions;
+ private long insertedPropertyValues;
+ private long updatedPropertyValues;
+ private LocalDateTime startedAt;
+ private LocalDateTime finishedAt;
+ private boolean success;
+ private String message;
+ private Integer batchSize;
+ private LocalDateTime since;
+ private List materialCodes = Collections.emptyList();
+ private Long recordLimit;
+
+ public static MasterDataSyncReport start(MasterDataSyncCommand command) {
+ MasterDataSyncReport report = new MasterDataSyncReport();
+ report.setStartedAt(LocalDateTime.now());
+ report.setBatchSize(command.getBatchSize());
+ report.setSince(command.getSince());
+ report.setMaterialCodes(command.getMaterialCodes());
+ report.setRecordLimit(command.getRecordLimit());
+ report.setSuccess(false);
+ report.setMessage("执行中");
+ return report;
+ }
+
+ public void finish() {
+ this.finishedAt = LocalDateTime.now();
+ }
+
+ public void markSuccess() {
+ this.success = true;
+ this.message = "执行成功";
+ }
+
+ public void markFailure(String message) {
+ this.success = false;
+ this.message = message;
+ }
+
+ public void incrementProcessedRecords(long delta) {
+ this.processedRecords += delta;
+ }
+
+ public void incrementInsertedMaterials() {
+ this.insertedMaterials++;
+ }
+
+ public void incrementUpdatedMaterials() {
+ this.updatedMaterials++;
+ }
+
+ public void incrementInsertedClasses() {
+ this.insertedClasses++;
+ }
+
+ public void incrementUpdatedClasses() {
+ this.updatedClasses++;
+ }
+
+ public void incrementCreatedClassRelations() {
+ this.createdClassRelations++;
+ }
+
+ public void incrementUpdatedClassRelations() {
+ this.updatedClassRelations++;
+ }
+
+ public void incrementInsertedPropertyDefinitions() {
+ this.insertedPropertyDefinitions++;
+ }
+
+ public void incrementUpdatedPropertyDefinitions() {
+ this.updatedPropertyDefinitions++;
+ }
+
+ public void incrementInsertedPropertyValues() {
+ this.insertedPropertyValues++;
+ }
+
+ public void incrementUpdatedPropertyValues() {
+ this.updatedPropertyValues++;
+ }
+}
diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/support/MasterDataPropertyDefinition.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/support/MasterDataPropertyDefinition.java
new file mode 100644
index 00000000..33545eef
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/support/MasterDataPropertyDefinition.java
@@ -0,0 +1,52 @@
+package com.zt.plat.module.base.service.masterdatasync.support;
+
+import com.zt.plat.module.base.dal.dataobject.masterdata.MdmMaterialViewDO;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.time.format.DateTimeFormatter;
+import java.util.function.Function;
+
+/**
+ * 源视图字段与物料属性定义之间的标准映射关系。
+ */
+@Getter
+@RequiredArgsConstructor
+public enum MasterDataPropertyDefinition {
+
+ BASE_UNIT_CODE("MTRL_BASE_UNIT_CODE", "基本计量单位编码", 50L, MdmMaterialViewDO::getBaseUnitCode, "STRING"),
+ BASE_UNIT_NAME("MTRL_BASE_UNIT_NAME", "基本计量单位描述", 60L, MdmMaterialViewDO::getBaseUnitName, "STRING"),
+ SHORT_DESCRIPTION("MTRL_SHORT_DESC", "短描述", 70L, MdmMaterialViewDO::getShortDescription, "STRING"),
+ LONG_DESCRIPTION("MTRL_LONG_DESC", "长描述", 80L, MdmMaterialViewDO::getLongDescription, "STRING"),
+ CHALCO_CODE("MTRL_CHALCO_CODE", "中铝编码", 90L, MdmMaterialViewDO::getChalcoCode, "STRING"),
+ SPECIFICATION("MTRL_SPECIFICATION", "规格", 100L, MdmMaterialViewDO::getSpecification, "STRING"),
+ MODEL("MTRL_MODEL", "型号", 110L, MdmMaterialViewDO::getModel, "STRING"),
+ TEXTURE("MTRL_TEXTURE", "材质", 120L, MdmMaterialViewDO::getTexture, "STRING"),
+ DRAWING_NUMBER("MTRL_DRAWING_NUMBER", "图号", 130L, MdmMaterialViewDO::getDrawingNumber, "STRING"),
+ ORDER_NUMBER("MTRL_ORDER_NUMBER", "订货号", 140L, MdmMaterialViewDO::getOrderNumber, "STRING"),
+ OTHER_PARAMETERS("MTRL_OTHER_PARAMETERS", "其它参数", 150L, MdmMaterialViewDO::getOtherParameters, "STRING"),
+ EQUIPMENT_CATEGORY("MTRL_EQUIPMENT_CATEGORY", "设备类别", 160L, MdmMaterialViewDO::getEquipmentCategory, "STRING"),
+ MANUFACTURER("MTRL_MANUFACTURER", "主机生产商", 170L, MdmMaterialViewDO::getManufacturer, "STRING"),
+ SOURCE("MTRL_SOURCE", "来源", 180L, MdmMaterialViewDO::getSource, "STRING"),
+ RECORD_TIME("MTRL_RECORD_TIME", "记录时间", 190L,
+ source -> source.getRecordTime() == null ? null : source.getRecordTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
+ "DATETIME");
+
+ private final String code;
+ private final String displayName;
+ private final long sort;
+ private final Function extractor;
+ private final String dataType;
+
+ public String extractValue(MdmMaterialViewDO source) {
+ if (source == null) {
+ return null;
+ }
+ String value = extractor.apply(source);
+ if (value == null) {
+ return null;
+ }
+ value = value.trim();
+ return value.isEmpty() ? null : value;
+ }
+}
diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/support/MasterDataSyncNotificationPayload.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/support/MasterDataSyncNotificationPayload.java
new file mode 100644
index 00000000..995ef43b
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/support/MasterDataSyncNotificationPayload.java
@@ -0,0 +1,16 @@
+package com.zt.plat.module.base.service.masterdatasync.support;
+
+import com.zt.plat.module.base.service.masterdatasync.dto.MasterDataSyncReport;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+/**
+ * 同步结束后推送给下游监听方的 JSON 载荷。
+ */
+@Data
+@AllArgsConstructor
+public class MasterDataSyncNotificationPayload {
+
+ private MasterDataSyncReport report;
+ private String errorMessage;
+}
diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/support/MasterDataSyncNotifier.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/support/MasterDataSyncNotifier.java
new file mode 100644
index 00000000..32037ed1
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/masterdatasync/support/MasterDataSyncNotifier.java
@@ -0,0 +1,61 @@
+package com.zt.plat.module.base.service.masterdatasync.support;
+
+import cn.hutool.core.util.StrUtil;
+import com.zt.plat.framework.common.util.json.JsonUtils;
+import com.zt.plat.module.base.framework.sync.config.MasterDataSyncProperties;
+import com.zt.plat.module.base.service.masterdatasync.dto.MasterDataSyncReport;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+/**
+ * 在同步结束后发送回调通知。
+ */
+@Slf4j
+@Component
+public class MasterDataSyncNotifier {
+
+ private static final MediaType JSON = MediaType.get("application/json; charset=UTF-8");
+
+ private final OkHttpClient httpClient;
+ private final MasterDataSyncProperties properties;
+
+ public MasterDataSyncNotifier(@Qualifier("masterDataSyncOkHttpClient") OkHttpClient httpClient,
+ MasterDataSyncProperties properties) {
+ this.httpClient = httpClient;
+ this.properties = properties;
+ }
+
+ public void dispatch(MasterDataSyncReport report, Throwable failure) {
+ if (report == null || StrUtil.isBlank(properties.getCallbackUrl())) {
+ return;
+ }
+ if (report.isSuccess() && !properties.isNotifyOnSuccess()) {
+ return;
+ }
+ if (!report.isSuccess() && !properties.isNotifyOnFailure()) {
+ return;
+ }
+ MasterDataSyncNotificationPayload payload = new MasterDataSyncNotificationPayload(report,
+ failure == null ? null : failure.getMessage());
+ RequestBody body = RequestBody.create(JSON, JsonUtils.toJsonString(payload));
+ Request.Builder builder = new Request.Builder()
+ .url(properties.getCallbackUrl())
+ .post(body);
+ properties.getCallbackHeaders().forEach(builder::addHeader);
+ try (Response response = httpClient.newCall(builder.build()).execute()) {
+ if (!response.isSuccessful()) {
+ log.warn("主数据同步回调响应非成功状态: {}", response.code());
+ }
+ } catch (IOException ex) {
+ log.warn("调用主数据同步回调接口失败", ex);
+ }
+ }
+}
diff --git a/zt-module-base/zt-module-base-server/src/main/resources/logback-spring.xml b/zt-module-base/zt-module-base-server/src/main/resources/logback-spring.xml
index b1b9f3fa..0e551414 100644
--- a/zt-module-base/zt-module-base-server/src/main/resources/logback-spring.xml
+++ b/zt-module-base/zt-module-base-server/src/main/resources/logback-spring.xml
@@ -1,8 +1,8 @@
-
-
+
+
diff --git a/zt-module-base/zt-module-base-server/src/main/resources/mapper/masterdata/MdmMaterialCategoryMapper.xml b/zt-module-base/zt-module-base-server/src/main/resources/mapper/masterdata/MdmMaterialCategoryMapper.xml
new file mode 100644
index 00000000..426986bb
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/main/resources/mapper/masterdata/MdmMaterialCategoryMapper.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/zt-module-base/zt-module-base-server/src/main/resources/mapper/masterdata/MdmMaterialViewMapper.xml b/zt-module-base/zt-module-base-server/src/main/resources/mapper/masterdata/MdmMaterialViewMapper.xml
new file mode 100644
index 00000000..0e3c31e2
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/main/resources/mapper/masterdata/MdmMaterialViewMapper.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+ a.codeid AS code_id,
+ a.code AS material_code,
+ a.categorycode AS category_code,
+ a.categoryname AS category_name,
+ a.desc1 AS material_name,
+ a.desc6 AS base_unit_code,
+ a.desc19 AS base_unit_name,
+ a.descshort AS short_description,
+ a.desclong AS long_description,
+ a.desc86 AS chalco_code,
+ b.pur_class AS major_class_code,
+ c.desc1 AS major_class_name,
+ a.desc9 AS specification,
+ a.desc10 AS model,
+ a.desc11 AS texture,
+ a.desc12 AS drawing_number,
+ a.desc13 AS order_number,
+ a.desc14 AS other_parameters,
+ a.desc15 AS equipment_category,
+ a.desc16 AS manufacturer,
+ a.desc17 AS source,
+ a.recordtime AS record_time
+
+
+
+ mdm_wlzsj_code a
+ INNER JOIN z_pur_class b ON a.codeid = b.codeid
+ INNER JOIN mdm_wlfl_code c ON b.pur_class = c.code
+
+
+
+
+
+
diff --git a/zt-module-base/zt-module-base-server/src/test/java/com/zt/plat/module/base/api/businessdictionarytype/BusinessDictionaryTypeApiImplTest.java b/zt-module-base/zt-module-base-server/src/test/java/com/zt/plat/module/base/api/businessdictionarytype/BusinessDictionaryTypeApiImplTest.java
new file mode 100644
index 00000000..ab0c7f06
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/test/java/com/zt/plat/module/base/api/businessdictionarytype/BusinessDictionaryTypeApiImplTest.java
@@ -0,0 +1,201 @@
+package com.zt.plat.module.base.api.businessdictionarytype;
+
+import com.zt.plat.framework.common.pojo.CommonResult;
+import com.zt.plat.framework.common.pojo.PageResult;
+import com.zt.plat.module.base.api.businessdictionarytype.dto.BusinessDictionaryDataDTO;
+import com.zt.plat.module.base.api.businessdictionarytype.dto.BusinessDictionaryTypePageReqDTO;
+import com.zt.plat.module.base.api.businessdictionarytype.dto.BusinessDictionaryTypeRespDTO;
+import com.zt.plat.module.base.api.businessdictionarytype.dto.BusinessDictionaryTypeSaveReqDTO;
+import com.zt.plat.module.base.controller.admin.businessdictionarytype.vo.BusinessDictionaryTypePageReqVO;
+import com.zt.plat.module.base.controller.admin.businessdictionarytype.vo.BusinessDictionaryTypeRespVO;
+import com.zt.plat.module.base.controller.admin.businessdictionarytype.vo.BusinessDictionaryTypeSaveReqVO;
+import com.zt.plat.module.base.dal.dataobject.businessdictionarytype.BusinessDictionaryDataDO;
+import com.zt.plat.module.base.dal.dataobject.businessdictionarytype.BusinessDictionaryTypeDO;
+import com.zt.plat.module.base.service.businessdictionarytype.BusinessDictionaryTypeService;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class BusinessDictionaryTypeApiImplTest {
+
+ @InjectMocks
+ private BusinessDictionaryTypeApiImpl api;
+
+ @Mock
+ private BusinessDictionaryTypeService businessDictionaryTypeService;
+
+ private BusinessDictionaryTypeSaveReqDTO buildSaveReqDTO() {
+ BusinessDictionaryDataDTO dataDTO = new BusinessDictionaryDataDTO();
+ dataDTO.setId(10L);
+ dataDTO.setLabel("启用");
+ dataDTO.setValue("ENABLE");
+ dataDTO.setRemark("默认状态");
+ BusinessDictionaryTypeSaveReqDTO dto = new BusinessDictionaryTypeSaveReqDTO();
+ dto.setId(1L);
+ dto.setName("状态字典");
+ dto.setType("base_material_status");
+ dto.setStatus(0L);
+ dto.setRemark("备注");
+ dto.setBusinessDictionaryDatas(List.of(dataDTO));
+ return dto;
+ }
+
+ private BusinessDictionaryTypeRespVO buildRespVO() {
+ BusinessDictionaryTypeRespVO respVO = new BusinessDictionaryTypeRespVO();
+ respVO.setId(1L);
+ respVO.setName("状态字典");
+ respVO.setType("base_material_status");
+ respVO.setStatus(0L);
+ respVO.setRemark("备注");
+ respVO.setCreateTime(LocalDateTime.now());
+ return respVO;
+ }
+
+ @Test
+ void createBusinessDictionaryType_shouldForwardToServiceAndReturnConvertedDto() {
+ BusinessDictionaryTypeSaveReqDTO reqDTO = buildSaveReqDTO();
+ BusinessDictionaryTypeRespVO serviceResp = buildRespVO();
+ when(businessDictionaryTypeService.createBusinessDictionaryType(any(BusinessDictionaryTypeSaveReqVO.class)))
+ .thenReturn(serviceResp);
+
+ CommonResult result = api.createBusinessDictionaryType(reqDTO);
+
+ assertThat(result.isSuccess()).isTrue();
+ assertThat(result.getData()).usingRecursiveComparison()
+ .ignoringFields("createTime")
+ .isEqualTo(BeanAssertHelper.toRespDTO(serviceResp));
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(BusinessDictionaryTypeSaveReqVO.class);
+ verify(businessDictionaryTypeService).createBusinessDictionaryType(captor.capture());
+ BusinessDictionaryTypeSaveReqVO passed = captor.getValue();
+ assertThat(passed.getName()).isEqualTo(reqDTO.getName());
+ assertThat(passed.getBusinessDictionaryDatas()).hasSize(1);
+ BusinessDictionaryDataDO dataDO = passed.getBusinessDictionaryDatas().get(0);
+ assertThat(dataDO.getLabel()).isEqualTo("启用");
+ assertThat(dataDO.getValue()).isEqualTo("ENABLE");
+ }
+
+ @Test
+ void updateBusinessDictionaryType_shouldInvokeServiceWithConvertedPayload() {
+ BusinessDictionaryTypeSaveReqDTO reqDTO = buildSaveReqDTO();
+
+ CommonResult result = api.updateBusinessDictionaryType(reqDTO);
+
+ assertThat(result.isSuccess()).isTrue();
+ assertThat(result.getData()).isTrue();
+ ArgumentCaptor captor = ArgumentCaptor.forClass(BusinessDictionaryTypeSaveReqVO.class);
+ verify(businessDictionaryTypeService).updateBusinessDictionaryType(captor.capture());
+ assertThat(captor.getValue().getName()).isEqualTo(reqDTO.getName());
+ }
+
+ @Test
+ void deleteBusinessDictionaryType_shouldInvokeService() {
+ CommonResult result = api.deleteBusinessDictionaryType(123L);
+
+ assertThat(result.isSuccess()).isTrue();
+ assertThat(result.getData()).isTrue();
+ verify(businessDictionaryTypeService).deleteBusinessDictionaryType(123L);
+ }
+
+ @Test
+ void deleteBusinessDictionaryTypeList_shouldInvokeService() {
+ List ids = List.of(1L, 2L);
+
+ CommonResult result = api.deleteBusinessDictionaryTypeList(ids);
+
+ assertThat(result.isSuccess()).isTrue();
+ assertThat(result.getData()).isTrue();
+ verify(businessDictionaryTypeService).deleteBusinessDictionaryTypeListByIds(ids);
+ }
+
+ @Test
+ void getBusinessDictionaryType_shouldConvertDoToDto() {
+ BusinessDictionaryTypeDO typeDO = new BusinessDictionaryTypeDO();
+ typeDO.setId(55L);
+ typeDO.setName("状态字典");
+ when(businessDictionaryTypeService.getBusinessDictionaryType(55L)).thenReturn(typeDO);
+
+ CommonResult result = api.getBusinessDictionaryType(55L);
+
+ assertThat(result.isSuccess()).isTrue();
+ assertThat(result.getData().getId()).isEqualTo(55L);
+ assertThat(result.getData().getName()).isEqualTo("状态字典");
+ }
+
+ @Test
+ void getBusinessDictionaryTypePage_shouldConvertPageResult() {
+ BusinessDictionaryTypeDO typeDO = new BusinessDictionaryTypeDO();
+ typeDO.setId(11L);
+ typeDO.setName("状态");
+ PageResult page = new PageResult<>(List.of(typeDO), 1L);
+ when(businessDictionaryTypeService.getBusinessDictionaryTypePage(any(BusinessDictionaryTypePageReqVO.class)))
+ .thenReturn(page);
+
+ BusinessDictionaryTypePageReqDTO reqDTO = new BusinessDictionaryTypePageReqDTO();
+ reqDTO.setName("状态");
+ CommonResult> result = api.getBusinessDictionaryTypePage(reqDTO);
+
+ assertThat(result.isSuccess()).isTrue();
+ assertThat(result.getData().getList()).hasSize(1);
+ assertThat(result.getData().getList().get(0).getName()).isEqualTo("状态");
+ assertThat(result.getData().getTotal()).isEqualTo(1L);
+ }
+
+ @Test
+ void getBusinessDictionaryDataListByDictionaryTypeId_shouldConvertList() {
+ BusinessDictionaryDataDO dataDO = new BusinessDictionaryDataDO();
+ dataDO.setId(9L);
+ dataDO.setLabel("启用");
+ when(businessDictionaryTypeService.getBusinessDictionaryDataListByDictionaryTypeId(8L))
+ .thenReturn(List.of(dataDO));
+
+ CommonResult> result = api.getBusinessDictionaryDataListByDictionaryTypeId(8L);
+
+ assertThat(result.isSuccess()).isTrue();
+ assertThat(result.getData()).hasSize(1);
+ assertThat(result.getData().get(0).getLabel()).isEqualTo("启用");
+ }
+
+ @Test
+ void getBusinessDictionaryDataListByType_shouldConvertList() {
+ BusinessDictionaryDataDO dataDO = new BusinessDictionaryDataDO();
+ dataDO.setId(9L);
+ dataDO.setValue("ENABLE");
+ when(businessDictionaryTypeService.getBusinessDictionaryDataListByType("base_material_status"))
+ .thenReturn(List.of(dataDO));
+
+ CommonResult> result = api.getBusinessDictionaryDataListByType("base_material_status");
+
+ assertThat(result.isSuccess()).isTrue();
+ assertThat(result.getData()).hasSize(1);
+ assertThat(result.getData().get(0).getValue()).isEqualTo("ENABLE");
+ }
+
+ private static final class BeanAssertHelper {
+ private BeanAssertHelper() {
+ }
+
+ static BusinessDictionaryTypeRespDTO toRespDTO(BusinessDictionaryTypeRespVO respVO) {
+ BusinessDictionaryTypeRespDTO dto = new BusinessDictionaryTypeRespDTO();
+ dto.setId(respVO.getId());
+ dto.setName(respVO.getName());
+ dto.setType(respVO.getType());
+ dto.setStatus(respVO.getStatus());
+ dto.setRemark(respVO.getRemark());
+ dto.setCreateTime(respVO.getCreateTime());
+ return dto;
+ }
+ }
+}
diff --git a/zt-module-base/zt-module-base-server/src/test/java/com/zt/plat/module/base/api/departmentmaterial/DepartmentMaterialApiImplTest.java b/zt-module-base/zt-module-base-server/src/test/java/com/zt/plat/module/base/api/departmentmaterial/DepartmentMaterialApiImplTest.java
new file mode 100644
index 00000000..f068bc77
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/test/java/com/zt/plat/module/base/api/departmentmaterial/DepartmentMaterialApiImplTest.java
@@ -0,0 +1,146 @@
+package com.zt.plat.module.base.api.departmentmaterial;
+
+import com.zt.plat.framework.common.pojo.CommonResult;
+import com.zt.plat.framework.common.pojo.PageResult;
+import com.zt.plat.module.base.api.departmentmaterial.dto.DepartmentMaterialPageReqDTO;
+import com.zt.plat.module.base.api.departmentmaterial.dto.DepartmentMaterialRespDTO;
+import com.zt.plat.module.base.api.departmentmaterial.dto.DepartmentMaterialSaveReqDTO;
+import com.zt.plat.module.base.controller.admin.departmentmaterial.vo.DepartmentMaterialPageReqVO;
+import com.zt.plat.module.base.controller.admin.departmentmaterial.vo.DepartmentMaterialRespVO;
+import com.zt.plat.module.base.controller.admin.departmentmaterial.vo.DepartmentMaterialSaveReqVO;
+import com.zt.plat.module.base.service.departmentmaterial.DepartmentMaterialService;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class DepartmentMaterialApiImplTest {
+
+ @InjectMocks
+ private DepartmentMaterialApiImpl api;
+
+ @Mock
+ private DepartmentMaterialService departmentMaterialService;
+
+ private DepartmentMaterialSaveReqDTO buildSaveReq() {
+ DepartmentMaterialSaveReqDTO dto = new DepartmentMaterialSaveReqDTO();
+ dto.setId(100L);
+ dto.setDeptId(200L);
+ dto.setInfomationId(300L);
+ dto.setClassesId(400L);
+ dto.setDictionaryDataValue("TYPE");
+ dto.setStatus("ENABLE");
+ dto.setRemark("备注");
+ return dto;
+ }
+
+ private DepartmentMaterialRespVO buildRespVO() {
+ DepartmentMaterialRespVO respVO = new DepartmentMaterialRespVO();
+ respVO.setId(100L);
+ respVO.setDeptId(200L);
+ respVO.setInfomationId(300L);
+ respVO.setClassesId(400L);
+ respVO.setDictionaryDataValue("TYPE");
+ respVO.setRemark("备注");
+ respVO.setStatus("ENABLE");
+ respVO.setMaterialName("原材料");
+ respVO.setCreateTime(LocalDateTime.now());
+ return respVO;
+ }
+
+ @Test
+ void createDepartmentMaterial_shouldForwardToServiceAndReturnConvertedDto() {
+ DepartmentMaterialSaveReqDTO reqDTO = buildSaveReq();
+ DepartmentMaterialRespVO serviceResp = buildRespVO();
+ when(departmentMaterialService.createDepartmentMaterial(any(DepartmentMaterialSaveReqVO.class)))
+ .thenReturn(serviceResp);
+
+ CommonResult result = api.createDepartmentMaterial(reqDTO);
+
+ assertThat(result.isSuccess()).isTrue();
+ DepartmentMaterialRespDTO data = result.getData();
+ assertThat(data.getId()).isEqualTo(serviceResp.getId());
+ assertThat(data.getMaterialName()).isEqualTo("原材料");
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(DepartmentMaterialSaveReqVO.class);
+ verify(departmentMaterialService).createDepartmentMaterial(captor.capture());
+ DepartmentMaterialSaveReqVO passed = captor.getValue();
+ assertThat(passed.getDeptId()).isEqualTo(reqDTO.getDeptId());
+ assertThat(passed.getInfomationId()).isEqualTo(reqDTO.getInfomationId());
+ assertThat(passed.getDictionaryDataValue()).isEqualTo(reqDTO.getDictionaryDataValue());
+ }
+
+ @Test
+ void updateDepartmentMaterial_shouldInvokeService() {
+ DepartmentMaterialSaveReqDTO reqDTO = buildSaveReq();
+
+ CommonResult result = api.updateDepartmentMaterial(reqDTO);
+
+ assertThat(result.isSuccess()).isTrue();
+ assertThat(result.getData()).isTrue();
+ ArgumentCaptor captor = ArgumentCaptor.forClass(DepartmentMaterialSaveReqVO.class);
+ verify(departmentMaterialService).updateDepartmentMaterial(captor.capture());
+ assertThat(captor.getValue().getDeptId()).isEqualTo(reqDTO.getDeptId());
+ }
+
+ @Test
+ void deleteDepartmentMaterial_shouldInvokeService() {
+ CommonResult result = api.deleteDepartmentMaterial(99L);
+
+ assertThat(result.isSuccess()).isTrue();
+ assertThat(result.getData()).isTrue();
+ verify(departmentMaterialService).deleteDepartmentMaterial(99L);
+ }
+
+ @Test
+ void deleteDepartmentMaterialList_shouldInvokeService() {
+ List ids = List.of(1L, 2L, 3L);
+
+ CommonResult result = api.deleteDepartmentMaterialList(ids);
+
+ assertThat(result.isSuccess()).isTrue();
+ assertThat(result.getData()).isTrue();
+ verify(departmentMaterialService).deleteDepartmentMaterialListByIds(ids);
+ }
+
+ @Test
+ void getDepartmentMaterial_shouldConvertVoToDto() {
+ DepartmentMaterialRespVO respVO = buildRespVO();
+ when(departmentMaterialService.getDepartmentMaterial(11L)).thenReturn(respVO);
+
+ CommonResult result = api.getDepartmentMaterial(11L);
+
+ assertThat(result.isSuccess()).isTrue();
+ assertThat(result.getData().getId()).isEqualTo(respVO.getId());
+ assertThat(result.getData().getMaterialName()).isEqualTo(respVO.getMaterialName());
+ }
+
+ @Test
+ void getDepartmentMaterialPage_shouldConvertPageResult() {
+ DepartmentMaterialRespVO respVO = buildRespVO();
+ PageResult page = new PageResult<>(List.of(respVO), 1L);
+ when(departmentMaterialService.getDepartmentMaterialPage(any(DepartmentMaterialPageReqVO.class)))
+ .thenReturn(page);
+
+ DepartmentMaterialPageReqDTO reqDTO = new DepartmentMaterialPageReqDTO();
+ reqDTO.setDeptId(200L);
+
+ CommonResult> result = api.getDepartmentMaterialPage(reqDTO);
+
+ assertThat(result.isSuccess()).isTrue();
+ assertThat(result.getData().getList()).hasSize(1);
+ assertThat(result.getData().getList().get(0).getMaterialName()).isEqualTo("原材料");
+ assertThat(result.getData().getTotal()).isEqualTo(1L);
+ }
+}
diff --git a/zt-module-base/zt-module-base-server/src/test/java/com/zt/plat/module/base/api/materialclasses/MaterialClassesApiImplTest.java b/zt-module-base/zt-module-base-server/src/test/java/com/zt/plat/module/base/api/materialclasses/MaterialClassesApiImplTest.java
new file mode 100644
index 00000000..b5a9cd1d
--- /dev/null
+++ b/zt-module-base/zt-module-base-server/src/test/java/com/zt/plat/module/base/api/materialclasses/MaterialClassesApiImplTest.java
@@ -0,0 +1,180 @@
+package com.zt.plat.module.base.api.materialclasses;
+
+import com.zt.plat.framework.common.pojo.CommonResult;
+import com.zt.plat.framework.common.pojo.PageResult;
+import com.zt.plat.module.base.api.materialclasses.dto.MaterialClassesPageReqDTO;
+import com.zt.plat.module.base.api.materialclasses.dto.MaterialClassesRespDTO;
+import com.zt.plat.module.base.api.materialclasses.dto.MaterialClassesSaveReqDTO;
+import com.zt.plat.module.base.api.materialclasses.dto.MaterialClassesTreeRespDTO;
+import com.zt.plat.module.base.controller.admin.materialclasses.vo.MaterialClassesPageReqVO;
+import com.zt.plat.module.base.controller.admin.materialclasses.vo.MaterialClassesRespVO;
+import com.zt.plat.module.base.controller.admin.materialclasses.vo.MaterialClassesSaveReqVO;
+import com.zt.plat.module.base.dal.dataobject.materialclasses.MaterialClassesDO;
+import com.zt.plat.module.base.service.materialclasses.MaterialClassesService;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class MaterialClassesApiImplTest {
+
+ @InjectMocks
+ private MaterialClassesApiImpl api;
+
+ @Mock
+ private MaterialClassesService materialClassesService;
+
+ private MaterialClassesSaveReqDTO buildSaveReq() {
+ MaterialClassesSaveReqDTO dto = new MaterialClassesSaveReqDTO();
+ dto.setId(1L);
+ dto.setParentId(0L);
+ dto.setCode("CLS001");
+ dto.setName("原材料");
+ dto.setLevel(1L);
+ dto.setRemark("顶级分类");
+ return dto;
+ }
+
+ private MaterialClassesRespVO buildRespVO() {
+ MaterialClassesRespVO respVO = new MaterialClassesRespVO();
+ respVO.setId(1L);
+ respVO.setParentId(0L);
+ respVO.setCode("CLS001");
+ respVO.setName("原材料");
+ respVO.setLevel(1L);
+ respVO.setRemark("顶级分类");
+ respVO.setCreateTime(LocalDateTime.now());
+ return respVO;
+ }
+
+ @Test
+ void createMaterialClasses_shouldForwardToServiceAndReturnConvertedDto() {
+ MaterialClassesSaveReqDTO reqDTO = buildSaveReq();
+ MaterialClassesRespVO serviceResp = buildRespVO();
+ when(materialClassesService.createMaterialClasses(any(MaterialClassesSaveReqVO.class)))
+ .thenReturn(serviceResp);
+
+ CommonResult result = api.createMaterialClasses(reqDTO);
+
+ assertThat(result.isSuccess()).isTrue();
+ assertThat(result.getData().getName()).isEqualTo("原材料");
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(MaterialClassesSaveReqVO.class);
+ verify(materialClassesService).createMaterialClasses(captor.capture());
+ MaterialClassesSaveReqVO passed = captor.getValue();
+ assertThat(passed.getCode()).isEqualTo(reqDTO.getCode());
+ assertThat(passed.getName()).isEqualTo(reqDTO.getName());
+ }
+
+ @Test
+ void updateMaterialClasses_shouldInvokeService() {
+ MaterialClassesSaveReqDTO reqDTO = buildSaveReq();
+
+ CommonResult result = api.updateMaterialClasses(reqDTO);
+
+ assertThat(result.isSuccess()).isTrue();
+ assertThat(result.getData()).isTrue();
+ ArgumentCaptor captor = ArgumentCaptor.forClass(MaterialClassesSaveReqVO.class);
+ verify(materialClassesService).updateMaterialClasses(captor.capture());
+ assertThat(captor.getValue().getCode()).isEqualTo(reqDTO.getCode());
+ }
+
+ @Test
+ void deleteMaterialClasses_shouldInvokeService() {
+ CommonResult result = api.deleteMaterialClasses(5L);
+
+ assertThat(result.isSuccess()).isTrue();
+ assertThat(result.getData()).isTrue();
+ verify(materialClassesService).deleteMaterialClasses(5L);
+ }
+
+ @Test
+ void deleteMaterialClassesList_shouldInvokeService() {
+ List ids = List.of(3L, 4L);
+
+ CommonResult result = api.deleteMaterialClassesList(ids);
+
+ assertThat(result.isSuccess()).isTrue();
+ assertThat(result.getData()).isTrue();
+ verify(materialClassesService).deleteMaterialClassesListByIds(ids);
+ }
+
+ @Test
+ void getMaterialClasses_shouldConvertDoToDto() {
+ MaterialClassesDO classesDO = new MaterialClassesDO();
+ classesDO.setId(7L);
+ classesDO.setName("辅料");
+ when(materialClassesService.getMaterialClasses(7L)).thenReturn(classesDO);
+
+ CommonResult result = api.getMaterialClasses(7L);
+
+ assertThat(result.isSuccess()).isTrue();
+ assertThat(result.getData().getId()).isEqualTo(7L);
+ assertThat(result.getData().getName()).isEqualTo("辅料");
+ }
+
+ @Test
+ void getMaterialClassesPage_shouldConvertPageResult() {
+ MaterialClassesDO classesDO = new MaterialClassesDO();
+ classesDO.setId(8L);
+ classesDO.setName("辅料");
+ PageResult page = new PageResult<>(List.of(classesDO), 1L);
+ when(materialClassesService.getMaterialClassesPage(any(MaterialClassesPageReqVO.class)))
+ .thenReturn(page);
+
+ MaterialClassesPageReqDTO reqDTO = new MaterialClassesPageReqDTO();
+ reqDTO.setName("辅料");
+
+ CommonResult> result = api.getMaterialClassesPage(reqDTO);
+
+ assertThat(result.isSuccess()).isTrue();
+ assertThat(result.getData().getList()).hasSize(1);
+ assertThat(result.getData().getList().get(0).getName()).isEqualTo("辅料");
+ assertThat(result.getData().getTotal()).isEqualTo(1L);
+ }
+
+ @Test
+ void getMaterialClassesTree_shouldAssembleHierarchy() {
+ MaterialClassesDO root = new MaterialClassesDO();
+ root.setId(1L);
+ root.setParentId(0L);
+ root.setName("根节点");
+
+ MaterialClassesDO child = new MaterialClassesDO();
+ child.setId(2L);
+ child.setParentId(1L);
+ child.setName("子节点");
+
+ MaterialClassesDO orphan = new MaterialClassesDO();
+ orphan.setId(3L);
+ orphan.setParentId(99L);
+ orphan.setName("孤儿节点");
+
+ when(materialClassesService.getMaterialClassesList()).thenReturn(List.of(child, root, orphan));
+
+ CommonResult> result = api.getMaterialClassesTree();
+
+ assertThat(result.isSuccess()).isTrue();
+ List tree = result.getData();
+ assertThat(tree).hasSize(2);
+ MaterialClassesTreeRespDTO rootNode = tree.get(0);
+ assertThat(rootNode.getId()).isEqualTo(1L);
+ assertThat(rootNode.getChildren()).hasSize(1);
+ assertThat(rootNode.getChildren().get(0).getId()).isEqualTo(2L);
+
+ MaterialClassesTreeRespDTO orphanNode = tree.get(1);
+ assertThat(orphanNode.getId()).isEqualTo(3L);
+ assertThat(orphanNode.getChildren()).isEmpty();
+ }
+}
diff --git a/zt-module-base/主数据同步说明.md b/zt-module-base/主数据同步说明.md
new file mode 100644
index 00000000..33881d50
--- /dev/null
+++ b/zt-module-base/主数据同步说明.md
@@ -0,0 +1,140 @@
+# 主数据同步功能说明
+
+
+## 功能概述
+
+主数据同步由“分类同步 + 物料同步”两条独立链路组成:
+
+1. 分类同步:从 `mdm_wlfl_code` 全量拉取 `01 / 0101 / 010101` 结构的编码,生成 BSE 三层分类。
+2. 物料同步:从 `mdm_material_view` 拉取物料主数据,依赖已存在的分类信息完成入库。
+3. 物料同步会维护物料与分类绑定关系,并将非主表字段落入属性表。
+4. 同步结束后可选地向外部回调推送执行报告。
+
+## 同步入口
+
+### 分类同步接口
+
+- **接口地址**:`POST /base/master-data-sync/categories`
+- **权限点**:`base:master-data-sync:categories`
+- **请求体**:无(全量同步)
+- **返回体**:`MasterDataCategorySyncReport`
+
+字段说明:
+
+- `processedCategories`:本次遍历的分类数量。
+- `insertedCategories` / `updatedCategories`:新增或更新的分类记录数。
+- `missingParentReferences`:外部数据缺失父级时的降级次数(会挂到根节点)。
+- `success`、`message`、`startedAt`、`finishedAt`:同物料同步。
+
+> ⚠️ 分类同步需先于物料同步执行,确保 BSE 中已存在三层分类结构。
+
+### 物料同步接口
+
+- **接口地址**:`POST /base/master-data-sync/execute`
+- **权限点**:`base:master-data-sync:execute`
+- **请求体**:
+
+ | 字段 | 类型 | 说明 |
+ | --- | --- | --- |
+ | `since` | `yyyy-MM-dd HH:mm:ss` | (可选)增量同步起始时间,空值表示全量。 |
+ | `batchSize` | `Integer` | (可选)单批拉取条数,未填写则使用配置默认值。 |
+ | `materialCodes` | `List` | (可选)限定仅同步指定物料编码,留空同步全部。 |
+ | `recordLimit` | `Long` | (可选)本次同步允许处理的最大记录数,留空则不做总量限制。 |
+
+**返回体**为 `MasterDataSyncReport`,涵盖以下关键指标:
+
+- `processedRecords`:拉取并处理的记录数。
+- `insertedMaterials` / `updatedMaterials`:新增或更新的物料数量。
+- `createdClassRelations` / `updatedClassRelations`:物料与分类的关系处理次数。
+- `insertedPropertyDefinitions` / `updatedPropertyDefinitions`:属性定义的补齐情况。
+- `insertedPropertyValues` / `updatedPropertyValues`:属性值的写入情况。
+- `success` + `message`:执行状态及提示语。
+- `startedAt` / `finishedAt`:执行起止时间。
+- `recordLimit`:若请求指定了总量限制,会在报告中回显该值。
+
+## 数据处理策略
+
+1. **属性字典**:
+ - 程序会检查 `MasterDataPropertyDefinition` 枚举列出的全部属性(例如类别编码、规格、材质等)。
+ - 若属性定义不存在即创建,存在但名称/排序有差异会自动更新。
+2. **分类管理**:
+ - 通过分类同步接口预先写入 `01/0101/010101` 三层数据,物料同步阶段仅检查并使用现有分类,如缺失则记录日志并跳过绑定。
+3. **物料主体**:
+ - 按 `material_code` 进行 upsert,保留基础字段(物料编码、通用信息等)。
+4. **分类绑定**:
+ - 若物料已有绑定但分类变化,则更新关系;否则创建新绑定。
+5. **属性值写入**:
+ - 对比新旧值,仅在有变化或缺失时写入,避免无效更新。
+
+## 配置项
+
+在 `application-*.yml` 中通过 `base.master-data-sync` 节点进行控制:
+
+```yaml
+base:
+ master-data-sync:
+ enabled: true # 是否开放同步入口
+ batch-size: 500 # 默认批量大小
+ source-datasource-name: mdm # 指向外部 MDM 的动态数据源
+ notify-on-success: true # 成功时是否回调
+ notify-on-failure: true # 失败时是否回调
+ callback-url: http://example/callback
+ callback-headers:
+ X-TOKEN: xxx
+ http:
+ connect-timeout: 5s
+ read-timeout: 30s
+ write-timeout: 30s
+```
+
+> ⚠️ 请确保在 `datasource.dynamic` 中配置名为 `mdm`(或 `source-datasource-name` 指定值)的数据源,并指向外部 MDM 数据库。
+
+## 数据源说明
+
+- `master`:默认业务库(BSE 表所在库),所有增删改依旧走 `master` 数据源,这部分无需额外配置。
+- `mdm`:主数据来源库(外部 MySQL)。`MdmMaterialViewMapper`、`MdmMaterialCategoryMapper` 均通过 `@DS("mdm")` 读取该库。
+- `base.master-data-sync.source-datasource-name`:若外部库名称不是 `mdm`,可在此覆盖,示例:
+
+ ```yaml
+ base:
+ master-data-sync:
+ source-datasource-name: mdm
+ datasource:
+ dynamic:
+ datasource:
+ master: { ...本地库配置... }
+ mdm: { url: jdbc:mysql://mdm-host/mdm, username: xxx, password: yyy }
+ ```
+
+> ✅ 强调:主数据读取始终走 `mdm`(或你在 `source-datasource-name` 中声明的别名),不要与默认 `master` 数据源混淆。
+
+## 回调机制
+
+- 当配置了 `callbackUrl` 时,系统会在同步结束后推送 JSON 结果。
+- 推送内容为:
+
+```json
+{
+ "report": { ...MasterDataSyncReport... },
+ "errorMessage": "可选的错误提示"
+}
+```
+
+- 仅成功/失败分别受 `notifyOnSuccess`、`notifyOnFailure` 控制。
+- `callbackHeaders` 支持附加认证信息,如 Token、租户标识等。
+
+## 常见问题排查
+
+| 问题 | 检查点 |
+| --- | --- |
+| 数据源连接失败 | 确认 `mdm` 数据源配置、账号权限、网络可达性。 |
+| 同步后未触发回调 | 检查 `callbackUrl`、`notify-on-*` 配置,以及目标地址是否可达。 |
+| 属性值未生效 | 查看 `MasterDataPropertyDefinition` 是否包含该字段,或字段值是否为空/仅有空格。 |
+| 只需同步单个物料 | 调用接口时在 `materialCodes` 中传入目标编码即可。 |
+
+## 建议的使用流程
+
+1. 配置并验证外部数据源与 `base.master-data-sync` 参数。
+2. 先执行“分类同步接口”,确认三层分类齐全。
+3. 再执行“物料同步接口”,并确认报表数据、回调通知。
+4. 线上场景可定期以 `since` 参数驱动增量物料同步,也可按需指定 `materialCodes` 触发刷新;分类若有改动,可重新全量执行一次分类同步。
diff --git a/zt-module-contract-order/zt-module-contract-order-api/src/main/java/com/zt/plat/module/contractorder/enums/contract/AuditResultEnum.java b/zt-module-contract-order/zt-module-contract-order-api/src/main/java/com/zt/plat/module/contractorder/enums/contract/AuditResultEnum.java
new file mode 100644
index 00000000..14da5a35
--- /dev/null
+++ b/zt-module-contract-order/zt-module-contract-order-api/src/main/java/com/zt/plat/module/contractorder/enums/contract/AuditResultEnum.java
@@ -0,0 +1,46 @@
+package com.zt.plat.module.contractorder.enums.contract;
+
+/**
+ * 合同审核结果
+ */
+public enum AuditResultEnum {
+ /**
+ * 合同状态-草稿
+ */
+ PASS("通过","PASS", null),
+ /**
+ * 合同状态-正在审核
+ */
+ REJECT("驳回","REJECT",null);
+
+ AuditResultEnum(String label, String code, String remark) {
+ this.label = label;
+ this.code = code;
+ this.remark = remark;
+ }
+
+ /**
+ * 标签
+ */
+ private final String label;
+ /**
+ * 编码
+ */
+ private final String code;
+ /**
+ * 备注
+ */
+ private final String remark;
+
+ public String getLabel() {
+ return label;
+ }
+
+ public String getCode() {
+ return code;
+ }
+
+ public String getRemark() {
+ return remark;
+ }
+}
diff --git a/zt-module-contract-order/zt-module-contract-order-server/src/main/resources/application.yml b/zt-module-contract-order/zt-module-contract-order-server/src/main/resources/application.yml
index ba3ad1bf..5baca3de 100644
--- a/zt-module-contract-order/zt-module-contract-order-server/src/main/resources/application.yml
+++ b/zt-module-contract-order/zt-module-contract-order-server/src/main/resources/application.yml
@@ -72,6 +72,8 @@ knife4j:
# MyBatis Plus 的配置项
mybatis-plus:
+ mapper-locations:
+ - classpath*:mapper/**/*.xml
configuration:
map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。
global-config:
diff --git a/zt-module-contract-order/zt-module-contract-order-server/src/main/resources/logback-spring.xml b/zt-module-contract-order/zt-module-contract-order-server/src/main/resources/logback-spring.xml
index 66410855..2bde9644 100644
--- a/zt-module-contract-order/zt-module-contract-order-server/src/main/resources/logback-spring.xml
+++ b/zt-module-contract-order/zt-module-contract-order-server/src/main/resources/logback-spring.xml
@@ -1,8 +1,8 @@
-
-
+
+
@@ -57,7 +57,7 @@
-
+
@@ -65,8 +65,8 @@
-
-
+
+