diff --git a/zt-module-base/zt-module-base-server/pom.xml b/zt-module-base/zt-module-base-server/pom.xml index c921c70f..3547b084 100644 --- a/zt-module-base/zt-module-base-server/pom.xml +++ b/zt-module-base/zt-module-base-server/pom.xml @@ -73,6 +73,20 @@ com.zt.plat zt-spring-boot-starter-mybatis + + + + com.mysql + mysql-connector-j + + + + + + + mysql + mysql-connector-java + 5.1.49 diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/controller/admin/materialhasproperties/MaterialHasPropertiesController.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/controller/admin/materialhasproperties/MaterialHasPropertiesController.java index 74cd3366..94308579 100644 --- a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/controller/admin/materialhasproperties/MaterialHasPropertiesController.java +++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/controller/admin/materialhasproperties/MaterialHasPropertiesController.java @@ -91,6 +91,14 @@ public class MaterialHasPropertiesController { return success(BeanUtils.toBean(pageResult, MaterialHasPropertiesRespVO.class)); } + @PostMapping("/batch-save") + @Operation(summary = "批量保存物料持有属性(全量替换)") + @PreAuthorize("@ss.hasPermission('base:material-has-properties:update')") + public CommonResult batchSave(@Valid @RequestBody MaterialHasPropertiesBatchSaveReqVO reqVO) { + MaterialHasPropertiesBatchSaveRespVO resp = materialHasPropertiesService.batchSave(reqVO); + return success(resp); + } + @GetMapping("/export-excel") @Operation(summary = "导出物料持有属性 Excel") @PreAuthorize("@ss.hasPermission('base:material-has-properties:export')") diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/controller/admin/materialhasproperties/vo/MaterialHasPropertiesBatchItemReqVO.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/controller/admin/materialhasproperties/vo/MaterialHasPropertiesBatchItemReqVO.java new file mode 100644 index 00000000..7f9cb6de --- /dev/null +++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/controller/admin/materialhasproperties/vo/MaterialHasPropertiesBatchItemReqVO.java @@ -0,0 +1,32 @@ +package com.zt.plat.module.base.controller.admin.materialhasproperties.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "物料持有属性批量保存单项 Request VO") +@Data +public class MaterialHasPropertiesBatchItemReqVO { + + @Schema(description = "主键ID", example = "6800") + private Long id; + + @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) + 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-server/src/main/java/com/zt/plat/module/base/controller/admin/materialhasproperties/vo/MaterialHasPropertiesBatchSaveReqVO.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/controller/admin/materialhasproperties/vo/MaterialHasPropertiesBatchSaveReqVO.java new file mode 100644 index 00000000..dac8badb --- /dev/null +++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/controller/admin/materialhasproperties/vo/MaterialHasPropertiesBatchSaveReqVO.java @@ -0,0 +1,22 @@ +package com.zt.plat.module.base.controller.admin.materialhasproperties.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Schema(description = "管理后台 - 物料持有属性批量保存 Request VO") +@Data +public class MaterialHasPropertiesBatchSaveReqVO { + + @Schema(description = "物料信息 ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "物料信息 ID 不能为空") + private Long infomationId; + + @Schema(description = "属性列表", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + private List properties = new ArrayList<>(); +} diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/controller/admin/materialhasproperties/vo/MaterialHasPropertiesBatchSaveRespVO.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/controller/admin/materialhasproperties/vo/MaterialHasPropertiesBatchSaveRespVO.java new file mode 100644 index 00000000..be50488e --- /dev/null +++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/controller/admin/materialhasproperties/vo/MaterialHasPropertiesBatchSaveRespVO.java @@ -0,0 +1,15 @@ +package com.zt.plat.module.base.controller.admin.materialhasproperties.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Schema(description = "管理后台 - 物料持有属性批量保存 Response VO") +@Data +public class MaterialHasPropertiesBatchSaveRespVO { + + @Schema(description = "行级错误列表(为空表示全部成功)") + private List rowErrors = new ArrayList<>(); +} diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/controller/admin/materialhasproperties/vo/RowValidationErrorVO.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/controller/admin/materialhasproperties/vo/RowValidationErrorVO.java new file mode 100644 index 00000000..884818d7 --- /dev/null +++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/controller/admin/materialhasproperties/vo/RowValidationErrorVO.java @@ -0,0 +1,19 @@ +package com.zt.plat.module.base.controller.admin.materialhasproperties.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema(description = "行级校验错误信息") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class RowValidationErrorVO { + + @Schema(description = "错误所在行(1 开始)") + private Integer rowIndex; + + @Schema(description = "错误提示") + private String message; +} diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/base/MaterialInfomationServiceImpl.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/base/MaterialInfomationServiceImpl.java index e1e87804..fcc2f08f 100644 --- a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/base/MaterialInfomationServiceImpl.java +++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/base/MaterialInfomationServiceImpl.java @@ -65,14 +65,7 @@ public class MaterialInfomationServiceImpl implements MaterialInfomationService validateMaterialClassForBinding(createReqVO.getClassesId()); MaterialInfomationDO materialInfomation = BeanUtils.toBean(createReqVO, MaterialInfomationDO.class); materialInfomationMapper.insert(materialInfomation); - if (createReqVO.getClassesId() != null) { - MaterialHasClassesDO relation = MaterialHasClassesDO.builder() - .classesId(createReqVO.getClassesId()) - .infomationId(materialInfomation.getId()) - .build(); - materialHasClassesMapper.insert(relation); - materialInfomation.setClassesId(createReqVO.getClassesId()); - } + bindMaterialClassIfAbsent(materialInfomation.getId(), createReqVO.getClassesId()); return CollUtil.getFirst(buildRespList(Collections.singletonList(materialInfomation))); } @@ -85,13 +78,27 @@ public class MaterialInfomationServiceImpl implements MaterialInfomationService materialInfomationMapper.updateById(updateObj); materialHasClassesMapper.delete(new LambdaUpdateWrapper() .eq(MaterialHasClassesDO::getInfomationId, updateReqVO.getId())); - if (updateReqVO.getClassesId() != null) { - MaterialHasClassesDO relation = MaterialHasClassesDO.builder() - .classesId(updateReqVO.getClassesId()) - .infomationId(updateReqVO.getId()) - .build(); - materialHasClassesMapper.insert(relation); + bindMaterialClassIfAbsent(updateReqVO.getId(), updateReqVO.getClassesId()); + } + + /** + * 绑定物料与分类关系(若已存在则跳过),避免唯一索引冲突 + */ + private void bindMaterialClassIfAbsent(Long materialId, Long classesId) { + if (materialId == null || classesId == null) { + return; } + Long exists = materialHasClassesMapper.selectCount(new LambdaQueryWrapperX() + .eq(MaterialHasClassesDO::getInfomationId, materialId) + .eq(MaterialHasClassesDO::getClassesId, classesId)); + if (exists != null && exists > 0) { + return; + } + MaterialHasClassesDO relation = MaterialHasClassesDO.builder() + .classesId(classesId) + .infomationId(materialId) + .build(); + materialHasClassesMapper.insert(relation); } @Override diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/materialhasproperties/MaterialHasPropertiesService.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/materialhasproperties/MaterialHasPropertiesService.java index 3e3d8485..aafbba27 100644 --- a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/materialhasproperties/MaterialHasPropertiesService.java +++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/materialhasproperties/MaterialHasPropertiesService.java @@ -59,4 +59,11 @@ public interface MaterialHasPropertiesService { */ PageResult getMaterialHasPropertiesPage(MaterialHasPropertiesPageReqVO pageReqVO); + /** + * 批量保存物料属性(全量替换指定物料的属性关系) + * @param batchReqVO 请求参数 + * @return 行级校验错误(为空表示成功) + */ + MaterialHasPropertiesBatchSaveRespVO batchSave(MaterialHasPropertiesBatchSaveReqVO batchReqVO); + } \ No newline at end of file diff --git a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/materialhasproperties/MaterialHasPropertiesServiceImpl.java b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/materialhasproperties/MaterialHasPropertiesServiceImpl.java index 2559c263..1618e5e2 100644 --- a/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/materialhasproperties/MaterialHasPropertiesServiceImpl.java +++ b/zt-module-base/zt-module-base-server/src/main/java/com/zt/plat/module/base/service/materialhasproperties/MaterialHasPropertiesServiceImpl.java @@ -1,6 +1,7 @@ package com.zt.plat.module.base.service.materialhasproperties; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; @@ -14,6 +15,7 @@ import com.zt.plat.framework.common.pojo.PageParam; import com.zt.plat.framework.common.util.object.BeanUtils; import com.zt.plat.module.base.dal.dao.materialhasproperties.MaterialHasPropertiesMapper; +import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX; import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception; import static com.zt.plat.framework.common.util.collection.CollectionUtils.convertList; @@ -89,4 +91,55 @@ public class MaterialHasPropertiesServiceImpl implements MaterialHasPropertiesSe return materialHasPropertiesMapper.selectPage(pageReqVO); } + @Override + @Transactional(rollbackFor = Exception.class) + public MaterialHasPropertiesBatchSaveRespVO batchSave(MaterialHasPropertiesBatchSaveReqVO batchReqVO) { + MaterialHasPropertiesBatchSaveRespVO resp = new MaterialHasPropertiesBatchSaveRespVO(); + Long infoId = batchReqVO.getInfomationId(); + if (infoId == null) { + resp.getRowErrors().add(new RowValidationErrorVO(0, "物料信息 ID 不能为空")); + return resp; + } + // 全量替换:先删除该物料的已有属性 + materialHasPropertiesMapper.delete(new LambdaQueryWrapperX() + .eq(MaterialHasPropertiesDO::getInfomationId, infoId)); + + List properties = batchReqVO.getProperties(); + if (CollUtil.isEmpty(properties)) { + return resp; + } + + // 去重并按提交顺序插入 + Set dedupKeys = new LinkedHashSet<>(); + for (int i = 0; i < properties.size(); i++) { + MaterialHasPropertiesBatchItemReqVO item = properties.get(i); + String propIdStr = item.getPropertiesId() == null ? null : String.valueOf(item.getPropertiesId()); + if (StrUtil.isBlank(propIdStr)) { + resp.getRowErrors().add(new RowValidationErrorVO(i + 1, "属性 ID 不能为空")); + continue; + } + if (StrUtil.isBlank(item.getValue())) { + resp.getRowErrors().add(new RowValidationErrorVO(i + 1, "属性值不能为空")); + continue; + } + String key = propIdStr; + if (!dedupKeys.add(key)) { + // 重复的属性直接跳过后续插入,避免唯一冲突 + continue; + } + MaterialHasPropertiesDO entity = MaterialHasPropertiesDO.builder() + .infomationId(infoId) + .propertiesId(item.getPropertiesId()) + .unitId(item.getUnitId()) + .value(item.getValue()) + .isKey(item.getIsKey()) + .isMetering(item.getIsMetering()) + .sort(item.getSort() == null ? (long) (i + 1) : item.getSort()) + .build(); + materialHasPropertiesMapper.insert(entity); + } + + return resp; + } + } \ No newline at end of file diff --git a/zt-module-base/zt-module-base-server/src/test/java/com/zt/plat/module/base/service/materialhasproperties/MaterialHasPropertiesServiceImplTest.java b/zt-module-base/zt-module-base-server/src/test/java/com/zt/plat/module/base/service/materialhasproperties/MaterialHasPropertiesServiceImplTest.java new file mode 100644 index 00000000..e1fe4b74 --- /dev/null +++ b/zt-module-base/zt-module-base-server/src/test/java/com/zt/plat/module/base/service/materialhasproperties/MaterialHasPropertiesServiceImplTest.java @@ -0,0 +1,146 @@ +package com.zt.plat.module.base.service.materialhasproperties; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.zt.plat.framework.test.core.ut.BaseDbUnitTest; +import com.zt.plat.module.base.controller.admin.materialhasproperties.vo.MaterialHasPropertiesBatchItemReqVO; +import com.zt.plat.module.base.controller.admin.materialhasproperties.vo.MaterialHasPropertiesBatchSaveReqVO; +import com.zt.plat.module.base.controller.admin.materialhasproperties.vo.MaterialHasPropertiesBatchSaveRespVO; +import com.zt.plat.module.base.controller.admin.materialhasproperties.vo.RowValidationErrorVO; +import com.zt.plat.module.base.dal.dao.materialhasproperties.MaterialHasPropertiesMapper; +import com.zt.plat.module.base.dal.dataobject.materialhasproperties.MaterialHasPropertiesDO; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.zt.plat.framework.test.core.util.RandomUtils.randomLongId; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * {@link MaterialHasPropertiesServiceImpl} 单元测试 + */ +@Import(MaterialHasPropertiesServiceImpl.class) +class MaterialHasPropertiesServiceImplTest extends BaseDbUnitTest { + + @Resource + private MaterialHasPropertiesServiceImpl materialHasPropertiesService; + + @Resource + private MaterialHasPropertiesMapper materialHasPropertiesMapper; + + private MaterialHasPropertiesDO insertRelation(Long infoId, Long propId, String value, Long sort) { + MaterialHasPropertiesDO entity = MaterialHasPropertiesDO.builder() + .id(randomLongId()) + .infomationId(infoId) + .propertiesId(propId) + .value(value) + .sort(sort) + .build(); + materialHasPropertiesMapper.insert(entity); + return entity; + } + + @Test + void batchSave_shouldReplaceAndDedup() { + Long infoId = 1001L; + // 旧数据:同一个物料下的旧记录 + 其他物料记录 + insertRelation(infoId, 9L, "old", 1L); + insertRelation(2002L, 99L, "other", 1L); + + MaterialHasPropertiesBatchItemReqVO item1 = new MaterialHasPropertiesBatchItemReqVO(); + item1.setPropertiesId(9L); + item1.setValue("new-val"); + item1.setIsKey(1); + + MaterialHasPropertiesBatchItemReqVO item2 = new MaterialHasPropertiesBatchItemReqVO(); + item2.setPropertiesId(10L); + item2.setValue("v10"); + item2.setSort(5L); + + // 重复属性,应该被去重跳过 + MaterialHasPropertiesBatchItemReqVO itemDuplicate = new MaterialHasPropertiesBatchItemReqVO(); + itemDuplicate.setPropertiesId(9L); + itemDuplicate.setValue("dup"); + + MaterialHasPropertiesBatchSaveReqVO req = new MaterialHasPropertiesBatchSaveReqVO(); + req.setInfomationId(infoId); + req.setProperties(CollUtil.newArrayList(item1, item2, itemDuplicate)); + + MaterialHasPropertiesBatchSaveRespVO resp = materialHasPropertiesService.batchSave(req); + + assertThat(resp.getRowErrors()).isEmpty(); + + List result = materialHasPropertiesMapper.selectList(); + // 只应保留当前物料的新两条 + 另一物料的 1 条 + assertThat(result).hasSize(3); + + List current = result.stream() + .filter(item -> infoId.equals(item.getInfomationId())) + .toList(); + assertThat(current).hasSize(2); + + Set propIds = current.stream().map(MaterialHasPropertiesDO::getPropertiesId).collect(Collectors.toSet()); + assertThat(propIds).containsExactlyInAnyOrder(9L, 10L); + + MaterialHasPropertiesDO updated = current.stream() + .filter(item -> item.getPropertiesId().equals(9L)).findFirst().orElseThrow(); + assertThat(updated.getValue()).isEqualTo("new-val"); + } + + @Test + void batchSave_missingInfoId_shouldReturnErrorAndSkipInsert() { + MaterialHasPropertiesBatchSaveReqVO req = new MaterialHasPropertiesBatchSaveReqVO(); + MaterialHasPropertiesBatchSaveRespVO resp = materialHasPropertiesService.batchSave(req); + + assertThat(resp.getRowErrors()).hasSize(1); + RowValidationErrorVO error = resp.getRowErrors().get(0); + assertThat(error.getMessage()).contains("物料信息 ID 不能为空"); + + assertThat(materialHasPropertiesMapper.selectList()).isEmpty(); + } + + @Test + void batchSave_emptyList_shouldDeleteExisting() { + Long infoId = 3003L; + insertRelation(infoId, 77L, "keep", 1L); + + MaterialHasPropertiesBatchSaveReqVO req = new MaterialHasPropertiesBatchSaveReqVO(); + req.setInfomationId(infoId); + req.setProperties(List.of()); + + MaterialHasPropertiesBatchSaveRespVO resp = materialHasPropertiesService.batchSave(req); + assertThat(resp.getRowErrors()).isEmpty(); + + List current = materialHasPropertiesMapper.selectList(new QueryWrapper() + .eq("INF_ID", infoId)); + assertThat(current).isEmpty(); + } + + @Test + void batchSave_blankValue_shouldCollectRowError() { + Long infoId = 4004L; + + MaterialHasPropertiesBatchItemReqVO item = new MaterialHasPropertiesBatchItemReqVO(); + item.setPropertiesId(55L); + item.setValue(" "); + + MaterialHasPropertiesBatchSaveReqVO req = new MaterialHasPropertiesBatchSaveReqVO(); + req.setInfomationId(infoId); + req.setProperties(List.of(item)); + + MaterialHasPropertiesBatchSaveRespVO resp = materialHasPropertiesService.batchSave(req); + + assertThat(resp.getRowErrors()).hasSize(1); + RowValidationErrorVO error = resp.getRowErrors().get(0); + assertThat(error.getRowIndex()).isEqualTo(1); + assertThat(error.getMessage()).contains("属性值不能为空"); + + List saved = materialHasPropertiesMapper.selectList(new QueryWrapper() + .eq("INF_ID", infoId)); + assertThat(saved).isEmpty(); + } +} diff --git a/zt-module-base/zt-module-base-server/src/test/resources/application-unit-test.yaml b/zt-module-base/zt-module-base-server/src/test/resources/application-unit-test.yaml new file mode 100644 index 00000000..3b2416ee --- /dev/null +++ b/zt-module-base/zt-module-base-server/src/test/resources/application-unit-test.yaml @@ -0,0 +1,39 @@ +spring: + main: + lazy-initialization: true + banner-mode: off + +--- #################### 数据库相关配置 #################### + +spring: + datasource: + name: zt-base + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value + driver-class-name: org.h2.Driver + username: sa + password: + druid: + async-init: true + initial-size: 1 + sql: + init: + schema-locations: classpath:/sql/create_tables.sql + + data: + redis: + host: 127.0.0.1 + port: 16379 + database: 0 + +mybatis-plus: + lazy-initialization: true + type-aliases-package: ${zt.info.base-package}.dal.dataobject + global-config: + db-config: + id-type: AUTO + +zt: + info: + base-package: com.zt.plat.module.base + AES: + key: XDV71a+xqStEA3WH diff --git a/zt-module-base/zt-module-base-server/src/test/resources/sql/clean.sql b/zt-module-base/zt-module-base-server/src/test/resources/sql/clean.sql new file mode 100644 index 00000000..5bcffbe4 --- /dev/null +++ b/zt-module-base/zt-module-base-server/src/test/resources/sql/clean.sql @@ -0,0 +1 @@ +TRUNCATE TABLE bse_mtrl_hs_prps; diff --git a/zt-module-base/zt-module-base-server/src/test/resources/sql/create_tables.sql b/zt-module-base/zt-module-base-server/src/test/resources/sql/create_tables.sql new file mode 100644 index 00000000..144d366a --- /dev/null +++ b/zt-module-base/zt-module-base-server/src/test/resources/sql/create_tables.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS bse_mtrl_hs_prps ( + id BIGINT PRIMARY KEY, + INF_ID BIGINT NOT NULL, + PRPS_ID BIGINT NOT NULL, + UNT_ID BIGINT, + VAL VARCHAR(255), + IS_KY INT, + IS_MTNG INT, + SRT BIGINT, + creator VARCHAR(64) DEFAULT '', + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updater VARCHAR(64) DEFAULT '', + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + deleted BIT DEFAULT FALSE NOT NULL, + tenant_id BIGINT DEFAULT 1 NOT NULL +);