diff --git a/yunxi-module-demo/yunxi-module-demo-api/pom.xml b/yunxi-module-demo/yunxi-module-demo-api/pom.xml
new file mode 100644
index 0000000..f1cff9e
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-api/pom.xml
@@ -0,0 +1,28 @@
+
+
+
+ yunxi-module-demo
+ com.yunxi.scm
+ ${revision}
+
+ 4.0.0
+
+ yunxi-module-demo-api
+ jar
+
+ ${project.artifactId}
+
+ demo 模块 API,暴露给其它模块调用
+
+
+
+
+
+ com.yunxi.scm
+ yunxi-common
+
+
+
+
\ No newline at end of file
diff --git a/yunxi-module-demo/yunxi-module-demo-api/src/main/java/com/yunxi/scm/module/demo/enums/DictTypeConstants.java b/yunxi-module-demo/yunxi-module-demo-api/src/main/java/com/yunxi/scm/module/demo/enums/DictTypeConstants.java
new file mode 100644
index 0000000..ef2087b
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-api/src/main/java/com/yunxi/scm/module/demo/enums/DictTypeConstants.java
@@ -0,0 +1,18 @@
+package com.yunxi.scm.module.demo.enums;
+
+/**
+ * System 字典类型的枚举类
+ *
+ * @author 芋道源码
+ */
+public interface DictTypeConstants {
+
+ String USER_TYPE = "user_type"; // 用户类型
+ String COMMON_STATUS = "common_status"; // 系统状态
+
+ String MATERIAL_STATUS = "material_status"; // 物料状态
+
+ // ========== SYSTEM 模块 ==========
+ String TRADE_ORDER_TYPE = "trade_order_type"; // 订单交易类型
+
+}
diff --git a/yunxi-module-demo/yunxi-module-demo-api/src/main/java/com/yunxi/scm/module/demo/enums/ErrorCodeConstants.java b/yunxi-module-demo/yunxi-module-demo-api/src/main/java/com/yunxi/scm/module/demo/enums/ErrorCodeConstants.java
new file mode 100644
index 0000000..c915f68
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-api/src/main/java/com/yunxi/scm/module/demo/enums/ErrorCodeConstants.java
@@ -0,0 +1,9 @@
+// TODO 待办:请将下面的错误码复制到 yunxi-module-demo-api 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
+// ========== 物料管理 TODO 补充编号 ==========
+package com.yunxi.scm.module.demo.enums;
+
+import com.yunxi.scm.framework.common.exception.ErrorCode;
+
+public interface ErrorCodeConstants {
+ ErrorCode MATERIAL_NOT_EXISTS = new ErrorCode(2023091001, "物料管理不存在");
+}
\ No newline at end of file
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/pom.xml b/yunxi-module-demo/yunxi-module-demo-biz/pom.xml
new file mode 100644
index 0000000..a7d7480
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/pom.xml
@@ -0,0 +1,72 @@
+
+
+
+ yunxi-module-demo
+ com.yunxi.scm
+ ${revision}
+
+ 4.0.0
+ jar
+
+ yunxi-module-demo-biz
+
+ ${project.artifactId}
+
+ demo 模块,主要实现 XXX、YYY、ZZZ 等功能。
+
+
+
+
+
+ com.yunxi.scm
+ yunxi-module-demo-api
+ ${revision}
+
+
+
+
+ com.yunxi.scm
+ yunxi-spring-boot-starter-biz-operatelog
+
+
+
+
+ com.yunxi.scm
+ yunxi-spring-boot-starter-web
+
+
+
+
+ com.yunxi.scm
+ yunxi-spring-boot-starter-security
+
+
+
+
+ com.yunxi.scm
+ yunxi-spring-boot-starter-biz-data-permission
+
+
+
+
+ com.yunxi.scm
+ yunxi-spring-boot-starter-mybatis
+
+
+
+
+ com.yunxi.scm
+ yunxi-spring-boot-starter-test
+
+
+
+
+ com.yunxi.scm
+ yunxi-spring-boot-starter-excel
+
+
+
+
+
\ No newline at end of file
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/DemoTestController.java b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/DemoTestController.java
new file mode 100644
index 0000000..e0ea321
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/DemoTestController.java
@@ -0,0 +1,119 @@
+package com.yunxi.scm.module.demo.controller.admin;
+
+import com.yunxi.scm.framework.common.pojo.CommonResult;
+import com.yunxi.scm.framework.excel.core.util.ExcelUtils;
+import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX;
+import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog;
+import com.yunxi.scm.module.demo.controller.admin.material.vo.MaterialExportTestVO;
+import com.yunxi.scm.module.demo.controller.admin.material.vo.MaterialImportTestVO;
+import com.yunxi.scm.module.demo.convert.material.MaterialXConvert;
+import com.yunxi.scm.module.demo.dal.dataobject.material.MaterialXDO;
+import com.yunxi.scm.module.demo.dal.mysql.material.MaterialXMapper;
+import com.yunxi.scm.module.demo.enums.material.CategoryEnum;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Parameters;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import javax.annotation.security.PermitAll;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static com.yunxi.scm.framework.common.pojo.CommonResult.success;
+import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.IMPORT;
+
+@Tag(name = "管理后台 - Test")
+@RestController
+@RequestMapping("/demo/test")
+@Validated
+public class DemoTestController {
+
+ @GetMapping("/get")
+ @Operation(summary = "获取 test 信息")
+ @PermitAll // 无需认证即可访问
+ public CommonResult get() {
+ return success("无需认证即可访问的后台接口");
+ }
+
+
+ @GetMapping("/test_export")
+ @Operation(summary = "测试导出和字典转化")
+ @PermitAll
+ @OperateLog(type = EXPORT)
+ public void testExport(HttpServletResponse response) throws IOException {
+ MaterialXDO xdo1 = new MaterialXDO(1L,"80inu5yxl3",Arrays.asList("1","2"));
+ MaterialXDO xdo2 = new MaterialXDO(2L,"Q2ZY1I9cCBf0YKvDUlzCbw==",Arrays.asList("1","3"));
+ List xdos = new ArrayList<>();
+ xdos.add(xdo1);
+ xdos.add(xdo2);
+ List datas = MaterialXConvert.INSTANCE.convertList03(xdos);
+ ExcelUtils.write(response, "测试导出.xls", "数据", MaterialExportTestVO.class, datas);
+ }
+
+
+ @PostMapping("/test_import")
+ @Operation(summary = "测试导入和转化为字典")
+ @PermitAll
+ @OperateLog(type = IMPORT)
+ @Parameters({
+ @Parameter(name = "file", description = "Excel 文件", required = true),
+ @Parameter(name = "updateSupport", description = "是否支持更新,默认为 false", example = "true")
+ })
+ public CommonResult> testImport(@RequestParam("file") MultipartFile file,
+ @RequestParam(value = "updateSupport", required = false, defaultValue = "false") Boolean updateSupport) throws IOException {
+ List list = ExcelUtils.read(file, MaterialImportTestVO.class);
+ return success(list);
+ }
+
+
+ @GetMapping("/get-import-template")
+ @PermitAll
+ @Operation(summary = "获得导入模板")
+ public void importTemplate(HttpServletResponse response) throws IOException {
+ List values = Stream.of(CategoryEnum.COMMON.getCode(),CategoryEnum.KANJIA.getCode())
+ .map(String::valueOf).collect(Collectors.toList());
+ // 手动创建导出 demo
+ List list = Arrays.asList(
+ MaterialImportTestVO.builder().category(values).build()
+ );
+ // 输出
+ ExcelUtils.write(response, "导入模板.xls", "物料标准模版", MaterialImportTestVO.class, list);
+ }
+
+ @Resource
+ MaterialXMapper materialXMapper;
+
+ @GetMapping("/test_encrypt")
+ @Operation(summary = "测试字段加密")
+ @PermitAll
+ public CommonResult testEncrypt() {
+ MaterialXDO xdo1 = new MaterialXDO(1L,"80inu5yxl3",Arrays.asList("1","2"));
+ MaterialXDO xdo2 = new MaterialXDO(2L,"80inu5yxl3",Arrays.asList("1","3"));
+ List xdos = new ArrayList<>();
+ xdos.add(xdo1);
+ xdos.add(xdo2);
+ materialXMapper.insertBatch(xdos);
+ return success("ok");
+ }
+
+ @GetMapping("/test_decrypt")
+ @Operation(summary = "测试字段解密")
+ @PermitAll
+ public CommonResult testDecrypt() {
+ LambdaQueryWrapperX wrapperX = new LambdaQueryWrapperX<>();
+ wrapperX.eq(MaterialXDO::getId, 1);
+ MaterialXDO xdo = materialXMapper.selectOne(wrapperX);
+ return success(MaterialXConvert.INSTANCE.convert(xdo));
+ }
+
+}
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/PMaterialController.java b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/PMaterialController.java
new file mode 100644
index 0000000..aa3121b
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/PMaterialController.java
@@ -0,0 +1,101 @@
+package com.yunxi.scm.module.demo.controller.admin.material;
+
+import com.yunxi.scm.framework.common.pojo.CommonResult;
+import com.yunxi.scm.framework.common.pojo.PageResult;
+import com.yunxi.scm.framework.datapermission.core.annotation.DataPermission;
+import com.yunxi.scm.framework.excel.core.util.ExcelUtils;
+import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog;
+import com.yunxi.scm.module.demo.controller.admin.material.vo.*;
+import com.yunxi.scm.module.demo.convert.material.MaterialConvert;
+import com.yunxi.scm.module.demo.dal.dataobject.material.MaterialDO;
+import com.yunxi.scm.module.demo.service.material.PMaterialService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+
+import static com.yunxi.scm.framework.common.pojo.CommonResult.success;
+import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+@Tag(name = "管理后台 - 物料管理")
+@RestController
+@RequestMapping("/demo/material")
+@Validated
+public class PMaterialController {
+
+ @Resource
+ private PMaterialService materialService;
+
+ @PostMapping("/create")
+ @Operation(summary = "创建物料管理")
+ @PreAuthorize("@ss.hasPermission('demo:material:create')")
+ public CommonResult createMaterial(@Valid @RequestBody MaterialCreateReqVO createReqVO) {
+ return success(materialService.createMaterial(createReqVO));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "更新物料管理")
+ @PreAuthorize("@ss.hasPermission('demo:material:update')")
+ public CommonResult updateMaterial(@Valid @RequestBody MaterialUpdateReqVO updateReqVO) {
+ materialService.updateMaterial(updateReqVO);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "删除物料管理")
+ @Parameter(name = "id", description = "编号", required = true)
+ @PreAuthorize("@ss.hasPermission('demo:material:delete')")
+ public CommonResult deleteMaterial(@RequestParam("id") Long id) {
+ materialService.deleteMaterial(id);
+ return success(true);
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "获得物料管理")
+ @Parameter(name = "id", description = "编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('demo:material:query')")
+ public CommonResult getMaterial(@RequestParam("id") Long id) {
+ MaterialDO material = materialService.getMaterial(id);
+ return success(MaterialConvert.INSTANCE.convert(material));
+ }
+
+ @GetMapping("/list")
+ @Operation(summary = "获得物料管理列表")
+ @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
+ @PreAuthorize("@ss.hasPermission('demo:material:query')")
+ public CommonResult> getMaterialList(@RequestParam("ids") Collection ids) {
+ List list = materialService.getMaterialList(ids);
+ return success(MaterialConvert.INSTANCE.convertList(list));
+ }
+
+ @GetMapping("/page")
+ @Operation(summary = "获得物料管理分页")
+ @PreAuthorize("@ss.hasPermission('demo:material:query')")
+ @DataPermission // 数据权限功能,默认是开启的
+ public CommonResult> getMaterialPage(@Valid MaterialPageReqVO pageVO) {
+ PageResult pageResult = materialService.getMaterialPage(pageVO);
+ return success(MaterialConvert.INSTANCE.convertPage(pageResult));
+ }
+
+ @GetMapping("/export-excel")
+ @Operation(summary = "导出物料管理 Excel")
+ @PreAuthorize("@ss.hasPermission('demo:material:export')")
+ @OperateLog(type = EXPORT)
+ public void exportMaterialExcel(@Valid MaterialExportReqVO exportReqVO,
+ HttpServletResponse response) throws IOException {
+ List list = materialService.getMaterialList(exportReqVO);
+ // 导出 Excel
+ List datas = MaterialConvert.INSTANCE.convertList02(list);
+ ExcelUtils.write(response, "物料管理.xls", "数据", MaterialExcelVO.class, datas);
+ }
+
+}
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialBaseVO.java b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialBaseVO.java
new file mode 100644
index 0000000..24bd461
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialBaseVO.java
@@ -0,0 +1,48 @@
+package com.yunxi.scm.module.demo.controller.admin.material.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 物料管理 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class MaterialBaseVO {
+
+ @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
+ @NotNull(message = "名称不能为空")
+ private String name;
+
+ @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "状态不能为空")
+ private Byte status;
+
+ @Schema(description = "类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+ @NotNull(message = "类型不能为空")
+ private Byte type;
+
+ @Schema(description = "分类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2")
+ @NotNull(message = "分类不能为空")
+ private List categories;
+
+ @Schema(description = "图片", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn")
+ @NotNull(message = "图片不能为空")
+ private String imgurl;
+
+ @Schema(description = "库存数量", requiredMode = Schema.RequiredMode.REQUIRED)
+ @NotNull(message = "库存数量不能为空")
+ private Integer stock;
+
+ @Schema(description = "单价", requiredMode = Schema.RequiredMode.REQUIRED, example = "18.00")
+ @NotNull(message = "单价不能为空")
+ private BigDecimal price;
+
+ @Schema(description = "备注", example = "物料备注")
+ private String remark;
+
+}
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialCreateReqVO.java b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialCreateReqVO.java
new file mode 100644
index 0000000..5c33d46
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialCreateReqVO.java
@@ -0,0 +1,14 @@
+package com.yunxi.scm.module.demo.controller.admin.material.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - 物料管理创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MaterialCreateReqVO extends MaterialBaseVO {
+
+}
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialExcelVO.java b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialExcelVO.java
new file mode 100644
index 0000000..d289090
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialExcelVO.java
@@ -0,0 +1,56 @@
+package com.yunxi.scm.module.demo.controller.admin.material.vo;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.yunxi.scm.framework.excel.core.annotations.DictFormat;
+import com.yunxi.scm.framework.excel.core.convert.DictConvert;
+import com.yunxi.scm.framework.excel.core.convert.JsonConvert;
+import com.yunxi.scm.module.demo.enums.DictTypeConstants;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+
+/**
+ * 物料管理 Excel VO
+ *
+ * @author ccongli
+ */
+@Data
+public class MaterialExcelVO {
+
+ @ExcelProperty("编号")
+ private Long id;
+
+ @ExcelProperty("名称")
+ private String name;
+
+ @ExcelProperty(value = "状态", converter = DictConvert.class)
+ @DictFormat(DictTypeConstants.MATERIAL_STATUS)
+ private Byte status;
+
+ @ExcelProperty(value = "类型", converter = DictConvert.class)
+ @DictFormat(DictTypeConstants.TRADE_ORDER_TYPE)
+ private Byte type;
+
+ @ExcelProperty(value = "分类", converter = JsonConvert.class)
+ private List categories;
+
+
+ @ExcelProperty("图片")
+ private String imgurl;
+
+ @ExcelProperty("库存数量")
+ private Integer stock;
+
+ @ExcelProperty("单价")
+ private BigDecimal price;
+
+ @ExcelProperty("备注")
+ private String remark;
+
+ @ExcelProperty("创建时间")
+ private LocalDateTime createTime;
+
+}
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialExportReqVO.java b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialExportReqVO.java
new file mode 100644
index 0000000..52b645b
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialExportReqVO.java
@@ -0,0 +1,31 @@
+package com.yunxi.scm.module.demo.controller.admin.material.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 物料管理 Excel 导出 Request VO,参数和 MaterialPageReqVO 是一致的")
+@Data
+public class MaterialExportReqVO {
+
+ @Schema(description = "名称", example = "李四")
+ private String name;
+
+ @Schema(description = "状态", example = "1")
+ private Byte status;
+
+ @Schema(description = "类型", example = "2")
+ private Byte type;
+
+ @Schema(description = "分类", example = "1")
+ private String category;
+
+ @Schema(description = "创建时间")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] createTime;
+
+}
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialExportTestVO.java b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialExportTestVO.java
new file mode 100644
index 0000000..55330c8
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialExportTestVO.java
@@ -0,0 +1,21 @@
+package com.yunxi.scm.module.demo.controller.admin.material.vo;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.yunxi.scm.framework.excel.core.annotations.DictFormat;
+import com.yunxi.scm.framework.excel.core.convert.MutilDictConvert;
+import com.yunxi.scm.module.demo.enums.DictTypeConstants;
+import lombok.Data;
+
+import java.util.List;
+
+// 测试转换
+@Data
+public class MaterialExportTestVO {
+
+ @ExcelProperty(value = "密码")
+ private String password;
+
+ @ExcelProperty(value = "分类测试", converter = MutilDictConvert.class)
+ @DictFormat(DictTypeConstants.TRADE_ORDER_TYPE)
+ private List category;
+}
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialImportTestVO.java b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialImportTestVO.java
new file mode 100644
index 0000000..63ddb9c
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialImportTestVO.java
@@ -0,0 +1,27 @@
+package com.yunxi.scm.module.demo.controller.admin.material.vo;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.yunxi.scm.framework.excel.core.annotations.DictFormat;
+import com.yunxi.scm.framework.excel.core.convert.MutilDictConvert;
+import com.yunxi.scm.module.demo.enums.DictTypeConstants;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import java.util.List;
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Accessors(chain = false) // 设置 chain = false,避免用户导入有问题
+public class MaterialImportTestVO {
+
+
+
+ @ExcelProperty(value = "分类测试", converter = MutilDictConvert.class)
+ @DictFormat(DictTypeConstants.TRADE_ORDER_TYPE)
+ private List category;
+}
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialPageReqVO.java b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialPageReqVO.java
new file mode 100644
index 0000000..1f97dfa
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialPageReqVO.java
@@ -0,0 +1,36 @@
+package com.yunxi.scm.module.demo.controller.admin.material.vo;
+
+import com.yunxi.scm.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 物料管理分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MaterialPageReqVO extends PageParam {
+
+ @Schema(description = "名称", example = "李四")
+ private String name;
+
+ @Schema(description = "状态", example = "1")
+ private Byte status;
+
+ @Schema(description = "类型", example = "2")
+ private Byte type;
+
+ @Schema(description = "分类", example = "2")
+ private String category;
+
+ @Schema(description = "创建时间")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] createTime;
+
+}
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialRespVO.java b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialRespVO.java
new file mode 100644
index 0000000..cb1ff3d
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialRespVO.java
@@ -0,0 +1,19 @@
+package com.yunxi.scm.module.demo.controller.admin.material.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 物料管理 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MaterialRespVO extends MaterialBaseVO {
+
+ @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "14541")
+ private Long id;
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime createTime;
+
+}
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialUpdateReqVO.java b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialUpdateReqVO.java
new file mode 100644
index 0000000..33089ba
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/admin/material/vo/MaterialUpdateReqVO.java
@@ -0,0 +1,18 @@
+package com.yunxi.scm.module.demo.controller.admin.material.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - 物料管理更新 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MaterialUpdateReqVO extends MaterialBaseVO {
+
+ @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "14541")
+ @NotNull(message = "ID不能为空")
+ private Long id;
+
+}
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/app/AppDemoTestController.java b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/app/AppDemoTestController.java
new file mode 100644
index 0000000..96c9b86
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/controller/app/AppDemoTestController.java
@@ -0,0 +1,27 @@
+package com.yunxi.scm.module.demo.controller.app;
+
+import com.yunxi.scm.framework.common.pojo.CommonResult;
+import com.yunxi.scm.framework.security.core.annotations.PreAuthenticated;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import static com.yunxi.scm.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "用户 App - Test")
+@RestController
+@RequestMapping("/demo/test")
+@Validated
+public class AppDemoTestController {
+
+ @GetMapping("/get")
+ @Operation(summary = "获取 test 信息")
+ @PreAuthenticated // 认证后可访问
+ public CommonResult get() {
+ return success("需认证后才能访问的app接口");
+ }
+
+}
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/convert/material/MaterialConvert.java b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/convert/material/MaterialConvert.java
new file mode 100644
index 0000000..0c6b6cb
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/convert/material/MaterialConvert.java
@@ -0,0 +1,37 @@
+package com.yunxi.scm.module.demo.convert.material;
+
+import com.yunxi.scm.framework.common.pojo.PageResult;
+import com.yunxi.scm.module.demo.controller.admin.material.vo.*;
+import com.yunxi.scm.module.demo.dal.dataobject.material.MaterialDO;
+import com.yunxi.scm.module.demo.dal.dataobject.material.MaterialXDO;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
+
+/**
+ * 物料管理 Convert
+ *
+ * @author ccongli
+ */
+@Mapper
+public interface MaterialConvert {
+
+ MaterialConvert INSTANCE = Mappers.getMapper(MaterialConvert.class);
+
+ MaterialDO convert(MaterialCreateReqVO bean);
+
+ MaterialDO convert(MaterialUpdateReqVO bean);
+
+ MaterialRespVO convert(MaterialDO bean);
+
+ List convertList(List list);
+
+ PageResult convertPage(PageResult page);
+
+ List convertList02(List list);
+
+
+ List convertList03(List list);
+
+}
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/convert/material/MaterialXConvert.java b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/convert/material/MaterialXConvert.java
new file mode 100644
index 0000000..b59f25f
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/convert/material/MaterialXConvert.java
@@ -0,0 +1,24 @@
+package com.yunxi.scm.module.demo.convert.material;
+
+import com.yunxi.scm.module.demo.controller.admin.material.vo.MaterialExportTestVO;
+import com.yunxi.scm.module.demo.dal.dataobject.material.MaterialXDO;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
+
+/**
+ * 物料管理 Convert
+ *
+ * @author ccongli
+ */
+@Mapper
+public interface MaterialXConvert {
+
+ MaterialXConvert INSTANCE = Mappers.getMapper(MaterialXConvert.class);
+
+ MaterialExportTestVO convert(MaterialXDO xdo);
+
+ List convertList03(List list);
+
+}
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/dal/dataobject/material/MaterialDO.java b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/dal/dataobject/material/MaterialDO.java
new file mode 100644
index 0000000..3eb3e1f
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/dal/dataobject/material/MaterialDO.java
@@ -0,0 +1,71 @@
+package com.yunxi.scm.module.demo.dal.dataobject.material;
+
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO;
+import com.yunxi.scm.framework.mybatis.core.type.StringListTypeHandler;
+import lombok.*;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 物料管理 DO
+ *
+ * @author ccongli
+ */
+@TableName(value = "demo_material",autoResultMap = true)
+@KeySequence("demo_material_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MaterialDO extends BaseDO {
+
+ /**
+ * ID
+ */
+ @TableId
+ private Long id;
+ /**
+ * 名称
+ */
+ private String name;
+ /**
+ * 状态
+ *
+ * 枚举 {@link TODO material_status 对应的类}
+ */
+ private Byte status;
+ /**
+ * 类型
+ */
+ private Byte type;
+ /**
+ * 分类
+ */
+// @TableField(typeHandler = JacksonTypeHandler.class)
+ @TableField(typeHandler = StringListTypeHandler.class)
+ private List categories;
+ /**
+ * 图片
+ */
+ private String imgurl;
+ /**
+ * 库存数量
+ */
+ private Integer stock;
+ /**
+ * 单价
+ */
+ private BigDecimal price;
+ /**
+ * 备注
+ */
+ private String remark;
+
+}
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/dal/dataobject/material/MaterialXDO.java b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/dal/dataobject/material/MaterialXDO.java
new file mode 100644
index 0000000..f0c246e
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/dal/dataobject/material/MaterialXDO.java
@@ -0,0 +1,30 @@
+package com.yunxi.scm.module.demo.dal.dataobject.material;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.yunxi.scm.framework.mybatis.core.type.EncryptTypeHandler;
+import com.yunxi.scm.framework.mybatis.core.type.StringListTypeHandler;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@TableName(value = "demo_xxx", autoResultMap = true)
+public class MaterialXDO implements Serializable {
+
+ @TableId
+ private Long id;
+
+ // 字段加密注解
+ @TableField(typeHandler = EncryptTypeHandler.class)
+ private String password;
+
+ @TableField(typeHandler = StringListTypeHandler.class)
+ private List category;
+}
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/dal/mysql/material/MaterialXMapper.java b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/dal/mysql/material/MaterialXMapper.java
new file mode 100644
index 0000000..ea31e5b
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/dal/mysql/material/MaterialXMapper.java
@@ -0,0 +1,15 @@
+package com.yunxi.scm.module.demo.dal.mysql.material;
+
+import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX;
+import com.yunxi.scm.module.demo.dal.dataobject.material.MaterialXDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 测试专用 Mapper
+ *
+ * @author ccongli
+ */
+@Mapper
+public interface MaterialXMapper extends BaseMapperX {
+
+}
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/dal/mysql/material/PMaterialMapper.java b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/dal/mysql/material/PMaterialMapper.java
new file mode 100644
index 0000000..297bf07
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/dal/mysql/material/PMaterialMapper.java
@@ -0,0 +1,41 @@
+package com.yunxi.scm.module.demo.dal.mysql.material;
+
+import com.yunxi.scm.framework.common.pojo.PageResult;
+import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX;
+import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX;
+import com.yunxi.scm.module.demo.controller.admin.material.vo.MaterialExportReqVO;
+import com.yunxi.scm.module.demo.controller.admin.material.vo.MaterialPageReqVO;
+import com.yunxi.scm.module.demo.dal.dataobject.material.MaterialDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * 物料管理 Mapper
+ *
+ * @author ccongli
+ */
+@Mapper
+public interface PMaterialMapper extends BaseMapperX {
+
+ default PageResult selectPage(MaterialPageReqVO reqVO) {
+ return selectPage(reqVO, new LambdaQueryWrapperX()
+ .likeIfPresent(MaterialDO::getName, reqVO.getName())
+ .eqIfPresent(MaterialDO::getStatus, reqVO.getStatus())
+ .eqIfPresent(MaterialDO::getType, reqVO.getType())
+ .likeIfPresent(MaterialDO::getCategories, reqVO.getCategory())
+ .betweenIfPresent(MaterialDO::getCreateTime, reqVO.getCreateTime())
+ .orderByDesc(MaterialDO::getId));
+ }
+
+ default List selectList(MaterialExportReqVO reqVO) {
+ return selectList(new LambdaQueryWrapperX()
+ .likeIfPresent(MaterialDO::getName, reqVO.getName())
+ .eqIfPresent(MaterialDO::getStatus, reqVO.getStatus())
+ .eqIfPresent(MaterialDO::getType, reqVO.getType())
+ .likeIfPresent(MaterialDO::getCategories, reqVO.getCategory())
+ .betweenIfPresent(MaterialDO::getCreateTime, reqVO.getCreateTime())
+ .orderByDesc(MaterialDO::getId));
+ }
+
+}
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/enums/material/CategoryEnum.java b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/enums/material/CategoryEnum.java
new file mode 100644
index 0000000..881e37f
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/enums/material/CategoryEnum.java
@@ -0,0 +1,34 @@
+package com.yunxi.scm.module.demo.enums.material;
+
+import com.yunxi.scm.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+@Getter
+@AllArgsConstructor
+public enum CategoryEnum implements IntArrayValuable {
+ COMMON(0, "普通"),
+ MIAOSHA(1, "秒杀"),
+
+ PINTUAN(2,"拼团"),
+
+ KANJIA(3, "砍价");
+
+ public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CategoryEnum::getCode).toArray();
+
+ /**
+ * 状态值
+ */
+ private final Integer code;
+ /**
+ * 状态名
+ */
+ private final String name;
+
+ @Override
+ public int[] array() {
+ return ARRAYS;
+ }
+}
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/framework/web/config/DemoWebConfiguration.java b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/framework/web/config/DemoWebConfiguration.java
new file mode 100644
index 0000000..44f2b08
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/framework/web/config/DemoWebConfiguration.java
@@ -0,0 +1,24 @@
+package com.yunxi.scm.module.demo.framework.web.config;
+
+import com.yunxi.scm.framework.swagger.config.YunxiSwaggerAutoConfiguration;
+import org.springdoc.core.GroupedOpenApi;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * system 模块的 web 组件的 Configuration
+ *
+ * @author 芋道源码
+ */
+@Configuration(proxyBeanMethods = false)
+public class DemoWebConfiguration {
+
+ /**
+ * demo 模块的 API 分组
+ */
+ @Bean
+ public GroupedOpenApi dempGroupedOpenApi() {
+ return YunxiSwaggerAutoConfiguration.buildGroupedOpenApi("demo");
+ }
+
+}
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/framework/web/package-info.java b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/framework/web/package-info.java
new file mode 100644
index 0000000..f71416f
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/framework/web/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * system 模块的 web 配置
+ */
+package com.yunxi.scm.module.demo.framework.web;
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/service/material/PMaterialService.java b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/service/material/PMaterialService.java
new file mode 100644
index 0000000..f318e18
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/service/material/PMaterialService.java
@@ -0,0 +1,70 @@
+package com.yunxi.scm.module.demo.service.material;
+
+import java.util.*;
+import javax.validation.*;
+import com.yunxi.scm.module.demo.controller.admin.material.vo.*;
+import com.yunxi.scm.module.demo.dal.dataobject.material.MaterialDO;
+import com.yunxi.scm.framework.common.pojo.PageResult;
+
+/**
+ * 物料管理 Service 接口
+ *
+ * @author ccongli
+ */
+public interface PMaterialService {
+
+ /**
+ * 创建物料管理
+ *
+ * @param createReqVO 创建信息
+ * @return 编号
+ */
+ Long createMaterial(@Valid MaterialCreateReqVO createReqVO);
+
+ /**
+ * 更新物料管理
+ *
+ * @param updateReqVO 更新信息
+ */
+ void updateMaterial(@Valid MaterialUpdateReqVO updateReqVO);
+
+ /**
+ * 删除物料管理
+ *
+ * @param id 编号
+ */
+ void deleteMaterial(Long id);
+
+ /**
+ * 获得物料管理
+ *
+ * @param id 编号
+ * @return 物料管理
+ */
+ MaterialDO getMaterial(Long id);
+
+ /**
+ * 获得物料管理列表
+ *
+ * @param ids 编号
+ * @return 物料管理列表
+ */
+ List getMaterialList(Collection ids);
+
+ /**
+ * 获得物料管理分页
+ *
+ * @param pageReqVO 分页查询
+ * @return 物料管理分页
+ */
+ PageResult getMaterialPage(MaterialPageReqVO pageReqVO);
+
+ /**
+ * 获得物料管理列表, 用于 Excel 导出
+ *
+ * @param exportReqVO 查询条件
+ * @return 物料管理列表
+ */
+ List getMaterialList(MaterialExportReqVO exportReqVO);
+
+}
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/service/material/PMaterialServiceImpl.java b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/service/material/PMaterialServiceImpl.java
new file mode 100644
index 0000000..5b51fb2
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/main/java/com/yunxi/scm/module/demo/service/material/PMaterialServiceImpl.java
@@ -0,0 +1,85 @@
+package com.yunxi.scm.module.demo.service.material;
+
+import com.yunxi.scm.framework.common.pojo.PageResult;
+import com.yunxi.scm.module.demo.controller.admin.material.vo.MaterialCreateReqVO;
+import com.yunxi.scm.module.demo.controller.admin.material.vo.MaterialExportReqVO;
+import com.yunxi.scm.module.demo.controller.admin.material.vo.MaterialPageReqVO;
+import com.yunxi.scm.module.demo.controller.admin.material.vo.MaterialUpdateReqVO;
+import com.yunxi.scm.module.demo.convert.material.MaterialConvert;
+import com.yunxi.scm.module.demo.dal.dataobject.material.MaterialDO;
+import com.yunxi.scm.module.demo.dal.mysql.material.PMaterialMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.Collection;
+import java.util.List;
+
+import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static com.yunxi.scm.module.demo.enums.ErrorCodeConstants.MATERIAL_NOT_EXISTS;
+
+/**
+ * 物料管理 Service 实现类
+ *
+ * @author ccongli
+ */
+@Service
+@Validated
+public class PMaterialServiceImpl implements PMaterialService {
+
+ @Resource
+ private PMaterialMapper materialMapper;
+
+ @Override
+ public Long createMaterial(MaterialCreateReqVO createReqVO) {
+ // 插入
+ MaterialDO material = MaterialConvert.INSTANCE.convert(createReqVO);
+ materialMapper.insert(material);
+ // 返回
+ return material.getId();
+ }
+
+ @Override
+ public void updateMaterial(MaterialUpdateReqVO updateReqVO) {
+ // 校验存在
+ validateMaterialExists(updateReqVO.getId());
+ // 更新
+ MaterialDO updateObj = MaterialConvert.INSTANCE.convert(updateReqVO);
+ materialMapper.updateById(updateObj);
+ }
+
+ @Override
+ public void deleteMaterial(Long id) {
+ // 校验存在
+ validateMaterialExists(id);
+ // 删除
+ materialMapper.deleteById(id);
+ }
+
+ private void validateMaterialExists(Long id) {
+ if (materialMapper.selectById(id) == null) {
+ throw exception(MATERIAL_NOT_EXISTS);
+ }
+ }
+
+ @Override
+ public MaterialDO getMaterial(Long id) {
+ return materialMapper.selectById(id);
+ }
+
+ @Override
+ public List getMaterialList(Collection ids) {
+ return materialMapper.selectBatchIds(ids);
+ }
+
+ @Override
+ public PageResult getMaterialPage(MaterialPageReqVO pageReqVO) {
+ return materialMapper.selectPage(pageReqVO);
+ }
+
+ @Override
+ public List getMaterialList(MaterialExportReqVO exportReqVO) {
+ return materialMapper.selectList(exportReqVO);
+ }
+
+}
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/main/resources/mapper/material/MaterialMapper.xml b/yunxi-module-demo/yunxi-module-demo-biz/src/main/resources/mapper/material/MaterialMapper.xml
new file mode 100644
index 0000000..f2091d2
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/main/resources/mapper/material/MaterialMapper.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/test/java/com/yunxi/scm/module/demo/service/material/MaterialServiceImplTest.java b/yunxi-module-demo/yunxi-module-demo-biz/src/test/java/com/yunxi/scm/module/demo/service/material/MaterialServiceImplTest.java
new file mode 100644
index 0000000..26d3bf6
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/test/java/com/yunxi/scm/module/demo/service/material/MaterialServiceImplTest.java
@@ -0,0 +1,179 @@
+package com.yunxi.scm.module.demo.service.material;
+
+import com.yunxi.scm.framework.common.pojo.PageResult;
+import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest;
+import com.yunxi.scm.module.demo.controller.admin.material.vo.MaterialCreateReqVO;
+import com.yunxi.scm.module.demo.controller.admin.material.vo.MaterialExportReqVO;
+import com.yunxi.scm.module.demo.controller.admin.material.vo.MaterialPageReqVO;
+import com.yunxi.scm.module.demo.controller.admin.material.vo.MaterialUpdateReqVO;
+import com.yunxi.scm.module.demo.dal.dataobject.material.MaterialDO;
+import com.yunxi.scm.module.demo.dal.mysql.material.PMaterialMapper;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Import;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
+import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId;
+import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals;
+import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException;
+import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomLongId;
+import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo;
+import static com.yunxi.scm.module.demo.enums.ErrorCodeConstants.MATERIAL_NOT_EXISTS;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * {@link PMaterialServiceImpl} 的单元测试类
+ *
+ * @author ccongli
+ */
+@Import(PMaterialServiceImpl.class)
+public class MaterialServiceImplTest extends BaseDbUnitTest {
+
+ @Resource
+ private PMaterialServiceImpl materialService;
+
+ @Resource
+ private PMaterialMapper materialMapper;
+
+ @Test
+ public void testCreateMaterial_success() {
+ // 准备参数
+ MaterialCreateReqVO reqVO = randomPojo(MaterialCreateReqVO.class);
+
+ // 调用
+ Long materialId = materialService.createMaterial(reqVO);
+ // 断言
+ assertNotNull(materialId);
+ // 校验记录的属性是否正确
+ MaterialDO material = materialMapper.selectById(materialId);
+ assertPojoEquals(reqVO, material);
+ }
+
+ @Test
+ public void testUpdateMaterial_success() {
+ // mock 数据
+ MaterialDO dbMaterial = randomPojo(MaterialDO.class);
+ materialMapper.insert(dbMaterial);// @Sql: 先插入出一条存在的数据
+ // 准备参数
+ MaterialUpdateReqVO reqVO = randomPojo(MaterialUpdateReqVO.class, o -> {
+ o.setId(dbMaterial.getId()); // 设置更新的 ID
+ });
+
+ // 调用
+ materialService.updateMaterial(reqVO);
+ // 校验是否更新正确
+ MaterialDO material = materialMapper.selectById(reqVO.getId()); // 获取最新的
+ assertPojoEquals(reqVO, material);
+ }
+
+ @Test
+ public void testUpdateMaterial_notExists() {
+ // 准备参数
+ MaterialUpdateReqVO reqVO = randomPojo(MaterialUpdateReqVO.class);
+
+ // 调用, 并断言异常
+ assertServiceException(() -> materialService.updateMaterial(reqVO), MATERIAL_NOT_EXISTS);
+ }
+
+ @Test
+ public void testDeleteMaterial_success() {
+ // mock 数据
+ MaterialDO dbMaterial = randomPojo(MaterialDO.class);
+ materialMapper.insert(dbMaterial);// @Sql: 先插入出一条存在的数据
+ // 准备参数
+ Long id = dbMaterial.getId();
+
+ // 调用
+ materialService.deleteMaterial(id);
+ // 校验数据不存在了
+ assertNull(materialMapper.selectById(id));
+ }
+
+ @Test
+ public void testDeleteMaterial_notExists() {
+ // 准备参数
+ Long id = randomLongId();
+
+ // 调用, 并断言异常
+ assertServiceException(() -> materialService.deleteMaterial(id), MATERIAL_NOT_EXISTS);
+ }
+
+ @Test
+ @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+ public void testGetMaterialPage() {
+ // mock 数据
+ MaterialDO dbMaterial = randomPojo(MaterialDO.class, o -> { // 等会查询到
+ o.setName(null);
+ o.setStatus(null);
+ o.setType(null);
+ o.setCategories(null);
+ o.setCreateTime(null);
+ });
+ materialMapper.insert(dbMaterial);
+ // 测试 name 不匹配
+ materialMapper.insert(cloneIgnoreId(dbMaterial, o -> o.setName(null)));
+ // 测试 status 不匹配
+ materialMapper.insert(cloneIgnoreId(dbMaterial, o -> o.setStatus(null)));
+ // 测试 type 不匹配
+ materialMapper.insert(cloneIgnoreId(dbMaterial, o -> o.setType(null)));
+ // 测试 category 不匹配
+ materialMapper.insert(cloneIgnoreId(dbMaterial, o -> o.setCategories(null)));
+ // 测试 createTime 不匹配
+ materialMapper.insert(cloneIgnoreId(dbMaterial, o -> o.setCreateTime(null)));
+ // 准备参数
+ MaterialPageReqVO reqVO = new MaterialPageReqVO();
+ reqVO.setName(null);
+ reqVO.setStatus(null);
+ reqVO.setType(null);
+ reqVO.setCategory(null);
+ reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+ // 调用
+ PageResult pageResult = materialService.getMaterialPage(reqVO);
+ // 断言
+ assertEquals(1, pageResult.getTotal());
+ assertEquals(1, pageResult.getList().size());
+ assertPojoEquals(dbMaterial, pageResult.getList().get(0));
+ }
+
+ @Test
+ @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+ public void testGetMaterialList() {
+ // mock 数据
+ MaterialDO dbMaterial = randomPojo(MaterialDO.class, o -> { // 等会查询到
+ o.setName(null);
+ o.setStatus(null);
+ o.setType(null);
+ o.setCategories(null);
+ o.setCreateTime(null);
+ });
+ materialMapper.insert(dbMaterial);
+ // 测试 name 不匹配
+ materialMapper.insert(cloneIgnoreId(dbMaterial, o -> o.setName(null)));
+ // 测试 status 不匹配
+ materialMapper.insert(cloneIgnoreId(dbMaterial, o -> o.setStatus(null)));
+ // 测试 type 不匹配
+ materialMapper.insert(cloneIgnoreId(dbMaterial, o -> o.setType(null)));
+ // 测试 category 不匹配
+ materialMapper.insert(cloneIgnoreId(dbMaterial, o -> o.setCategories(null)));
+ // 测试 createTime 不匹配
+ materialMapper.insert(cloneIgnoreId(dbMaterial, o -> o.setCreateTime(null)));
+ // 准备参数
+ MaterialExportReqVO reqVO = new MaterialExportReqVO();
+ reqVO.setName(null);
+ reqVO.setStatus(null);
+ reqVO.setType(null);
+ reqVO.setCategory(null);
+ reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+ // 调用
+ List list = materialService.getMaterialList(reqVO);
+ // 断言
+ assertEquals(1, list.size());
+ assertPojoEquals(dbMaterial, list.get(0));
+ }
+
+}
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/test/resources/sql/clean.sql b/yunxi-module-demo/yunxi-module-demo-biz/src/test/resources/sql/clean.sql
new file mode 100644
index 0000000..bdc8a99
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/test/resources/sql/clean.sql
@@ -0,0 +1,2 @@
+-- 将该删表 SQL 语句,添加到 yunxi-module-demo-biz 模块的 test/resources/sql/clean.sql 文件里
+DELETE FROM "demo_material";
\ No newline at end of file
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/test/resources/sql/create_tables.sql b/yunxi-module-demo/yunxi-module-demo-biz/src/test/resources/sql/create_tables.sql
new file mode 100644
index 0000000..1b8d090
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/test/resources/sql/create_tables.sql
@@ -0,0 +1,19 @@
+-- 将该建表 SQL 语句,添加到 yunxi-module-demo-biz 模块的 test/resources/sql/create_tables.sql 文件里
+CREATE TABLE IF NOT EXISTS "demo_material" (
+ "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+ "name" varchar NOT NULL,
+ "status" varchar NOT NULL,
+ "type" varchar NOT NULL,
+ "category" varchar NOT NULL,
+ "imgurl" varchar NOT NULL,
+ "stock" int NOT NULL,
+ "price" varchar NOT NULL,
+ "remark" varchar,
+ "creator" varchar DEFAULT '',
+ "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updater" varchar DEFAULT '',
+ "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ "deleted" bit NOT NULL DEFAULT FALSE,
+ "tenant_id" bigint NOT NULL,
+ PRIMARY KEY ("id")
+ ) COMMENT '物料表';
\ No newline at end of file
diff --git a/yunxi-module-demo/yunxi-module-demo-biz/src/test/resources/sql/menu.sql b/yunxi-module-demo/yunxi-module-demo-biz/src/test/resources/sql/menu.sql
new file mode 100644
index 0000000..6634071
--- /dev/null
+++ b/yunxi-module-demo/yunxi-module-demo-biz/src/test/resources/sql/menu.sql
@@ -0,0 +1,55 @@
+-- 菜单 SQL
+INSERT INTO system_menu(
+ name, permission, type, sort, parent_id,
+ path, icon, component, status, component_name
+)
+VALUES (
+ '物料管理管理', '', 2, 0, 1,
+ 'material', '', 'demo/material/index', 0, 'Material'
+ );
+
+-- 按钮父菜单ID
+-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
+SELECT @parentId := LAST_INSERT_ID();
+
+-- 按钮 SQL
+INSERT INTO system_menu(
+ name, permission, type, sort, parent_id,
+ path, icon, component, status
+)
+VALUES (
+ '物料管理查询', 'demo:material:query', 3, 1, @parentId,
+ '', '', '', 0
+ );
+INSERT INTO system_menu(
+ name, permission, type, sort, parent_id,
+ path, icon, component, status
+)
+VALUES (
+ '物料管理创建', 'demo:material:create', 3, 2, @parentId,
+ '', '', '', 0
+ );
+INSERT INTO system_menu(
+ name, permission, type, sort, parent_id,
+ path, icon, component, status
+)
+VALUES (
+ '物料管理更新', 'demo:material:update', 3, 3, @parentId,
+ '', '', '', 0
+ );
+INSERT INTO system_menu(
+ name, permission, type, sort, parent_id,
+ path, icon, component, status
+)
+VALUES (
+ '物料管理删除', 'demo:material:delete', 3, 4, @parentId,
+ '', '', '', 0
+ );
+INSERT INTO system_menu(
+ name, permission, type, sort, parent_id,
+ path, icon, component, status
+)
+VALUES (
+ '物料管理导出', 'demo:material:export', 3, 5, @parentId,
+ '', '', '', 0
+ );