diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java index 458acb5..9b55bd8 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java @@ -72,6 +72,8 @@ public class CodegenEngine { .put(javaTemplatePath("controller/vo/listReqVO"), javaModuleImplVOFilePath("ListReqVO")) .put(javaTemplatePath("controller/vo/respVO"), javaModuleImplVOFilePath("RespVO")) .put(javaTemplatePath("controller/vo/saveReqVO"), javaModuleImplVOFilePath("SaveReqVO")) + .put(javaTemplatePath("controller/vo/importExcelVO"), javaModuleImplVOFilePath("ImportExcelVO")) + .put(javaTemplatePath("controller/vo/importRespVO"), javaModuleImplVOFilePath("ImportRespVO")) .put(javaTemplatePath("controller/controller"), javaModuleImplControllerFilePath()) .put(javaTemplatePath("dal/do"), javaModuleImplMainFilePath("dal/dataobject/${table.businessName}/${table.className}DO")) diff --git a/yudao-module-infra/src/main/resources/codegen/java/controller/controller.vm b/yudao-module-infra/src/main/resources/codegen/java/controller/controller.vm index 9f63e24..efe073d 100644 --- a/yudao-module-infra/src/main/resources/codegen/java/controller/controller.vm +++ b/yudao-module-infra/src/main/resources/codegen/java/controller/controller.vm @@ -1,6 +1,7 @@ package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import ${jakartaPackage}.annotation.Resource; import org.springframework.validation.annotation.Validated; #if ($sceneEnum.scene == 1)import org.springframework.security.access.prepost.PreAuthorize;#end @@ -160,6 +161,28 @@ public class ${sceneEnum.prefixClass}${table.className}Controller { } #end + @GetMapping("/get-import-template") + @Operation(summary = "获得导入${table.classComment}模板") +#if ($sceneEnum.scene == 1) + @PreAuthorize("@ss.hasPermission('${permissionPrefix}:import')") +#end + public void import${simpleClassName}Template(HttpServletResponse response) throws IOException { + ExcelUtils.write(response, "${table.classComment}导入模板.xls", "数据", + ${sceneEnum.prefixClass}${table.className}ImportExcelVO.class, Collections.emptyList()); + } + + @PostMapping("/import") + @Operation(summary = "导入${table.classComment}") + @Parameter(name = "file", description = "Excel 文件", required = true) +#if ($sceneEnum.scene == 1) + @PreAuthorize("@ss.hasPermission('${permissionPrefix}:import')") +#end + @ApiAccessLog(operateType = IMPORT) + public CommonResult<${sceneEnum.prefixClass}${table.className}ImportRespVO> import${simpleClassName}(@RequestParam("file") MultipartFile file) throws Exception { + List<${sceneEnum.prefixClass}${table.className}ImportExcelVO> list = ExcelUtils.read(file, ${sceneEnum.prefixClass}${table.className}ImportExcelVO.class); + return success(${classNameVar}Service.import${simpleClassName}List(list)); + } + ## 特殊:主子表专属逻辑 #foreach ($subTable in $subTables) #set ($index = $foreach.count - 1) @@ -268,4 +291,4 @@ public class ${sceneEnum.prefixClass}${table.className}Controller { #end #end -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/main/resources/codegen/java/controller/vo/importExcelVO.vm b/yudao-module-infra/src/main/resources/codegen/java/controller/vo/importExcelVO.vm new file mode 100644 index 0000000..51b0751 --- /dev/null +++ b/yudao-module-infra/src/main/resources/codegen/java/controller/vo/importExcelVO.vm @@ -0,0 +1,38 @@ +package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; + +import cn.idev.excel.annotation.ExcelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +#foreach ($column in $columns) +#if (${column.createOperation} && "$!column.dictType" != "") +import ${DictFormatClassName}; +import ${DictConvertClassName}; +#break +#end +#end + +/** + * ${table.classComment} Excel 导入 VO + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ${sceneEnum.prefixClass}${table.className}ImportExcelVO { + +## 逐个处理字段 +#foreach ($column in $columns) +#if (${column.createOperation}) +#if ("$!column.dictType" != "") + @ExcelProperty(value = "${column.columnComment}", converter = DictConvert.class) + @DictFormat("${column.dictType}") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中 +#else + @ExcelProperty("${column.columnComment}") +#end + private ${column.javaType} ${column.javaField}; + +#end +#end +} diff --git a/yudao-module-infra/src/main/resources/codegen/java/controller/vo/importRespVO.vm b/yudao-module-infra/src/main/resources/codegen/java/controller/vo/importRespVO.vm new file mode 100644 index 0000000..c3d1520 --- /dev/null +++ b/yudao-module-infra/src/main/resources/codegen/java/controller/vo/importRespVO.vm @@ -0,0 +1,23 @@ +package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +import java.util.Map; + +@Schema(description = "${sceneEnum.name} - ${table.classComment}导入 Response VO") +@Data +@Builder +public class ${sceneEnum.prefixClass}${table.className}ImportRespVO { + + @Schema(description = "创建成功的数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer successCount; + + @Schema(description = "导入失败的数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer failureCount; + + @Schema(description = "导入失败的数据集合,key 为行号,value 为失败原因", requiredMode = Schema.RequiredMode.REQUIRED) + private Map failureRows; + +} diff --git a/yudao-module-infra/src/main/resources/codegen/java/service/service.vm b/yudao-module-infra/src/main/resources/codegen/java/service/service.vm index 1cc68bf..fda518c 100644 --- a/yudao-module-infra/src/main/resources/codegen/java/service/service.vm +++ b/yudao-module-infra/src/main/resources/codegen/java/service/service.vm @@ -27,6 +27,14 @@ public interface ${table.className}Service { ${primaryColumn.javaType} create${simpleClassName}(@Valid ${saveReqVOClass} ${saveReqVOVar}); /** + * 导入${table.classComment} + * + * @param importList 导入信息 + * @return 导入结果 + */ + ${sceneEnum.prefixClass}${table.className}ImportRespVO import${simpleClassName}List(List<${sceneEnum.prefixClass}${table.className}ImportExcelVO> importList); + + /** * 更新${table.classComment} * * @param ${updateReqVOVar} 更新信息 @@ -162,4 +170,4 @@ public interface ${table.className}Service { #end #end -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/main/resources/codegen/java/service/serviceImpl.vm b/yudao-module-infra/src/main/resources/codegen/java/service/serviceImpl.vm index 42afdf9..215fecb 100644 --- a/yudao-module-infra/src/main/resources/codegen/java/service/serviceImpl.vm +++ b/yudao-module-infra/src/main/resources/codegen/java/service/serviceImpl.vm @@ -7,6 +7,7 @@ import org.springframework.validation.annotation.Validated; import org.springframework.transaction.annotation.Transactional; import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; ## 特殊:主子表专属逻辑 @@ -93,6 +94,46 @@ public class ${table.className}ServiceImpl implements ${table.className}Service } @Override + @Transactional(rollbackFor = Exception.class) + public ${sceneEnum.prefixClass}${table.className}ImportRespVO import${simpleClassName}List(List<${sceneEnum.prefixClass}${table.className}ImportExcelVO> importList) { + if (CollUtil.isEmpty(importList)) { + return ${sceneEnum.prefixClass}${table.className}ImportRespVO.builder() + .successCount(0) + .failureCount(0) + .failureRows(new LinkedHashMap<>()) + .build(); + } + AtomicInteger successCount = new AtomicInteger(); + Map failureRows = new LinkedHashMap<>(); + AtomicInteger index = new AtomicInteger(1); + importList.forEach(importItem -> { + int currentIndex = index.getAndIncrement(); + try { + create${simpleClassName}(BeanUtils.toBean(importItem, ${saveReqVOClass}.class)); + successCount.incrementAndGet(); + } catch (Exception ex) { + failureRows.put(currentIndex, resolveImportFailureMessage(ex)); + } + }); + return ${sceneEnum.prefixClass}${table.className}ImportRespVO.builder() + .successCount(successCount.get()) + .failureCount(failureRows.size()) + .failureRows(failureRows) + .build(); + } + + /** + * 仅返回根因异常信息,避免把 MyBatis 长堆栈直接暴露给前端。 + */ + private String resolveImportFailureMessage(Exception ex) { + Throwable root = ex; + while (root.getCause() != null && root.getCause() != root) { + root = root.getCause(); + } + return root.getMessage() != null ? root.getMessage() : ex.getMessage(); + } + + @Override ## 特殊:主子表专属逻辑(非 ERP 模式) #if ( $subTables && $subTables.size() > 0 && $table.templateType != 11 ) @Transactional(rollbackFor = Exception.class) @@ -359,6 +400,9 @@ public class ${table.className}ServiceImpl implements ${table.className}Service #else #if ( $subTable.subJoinMany) private void create${subSimpleClassName}List(${primaryColumn.javaType} ${subJoinColumn.javaField}, List<${subTable.className}DO> list) { + if (CollUtil.isEmpty(list)) { + return; + } list.forEach(o -> o.set${SubJoinColumnName}(${subJoinColumn.javaField}).clean()); ${subClassNameVars.get($index)}Mapper.insertBatch(list); } @@ -416,4 +460,4 @@ public class ${table.className}ServiceImpl implements ${table.className}Service #end #end -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/main/resources/codegen/sql/sql.vm b/yudao-module-infra/src/main/resources/codegen/sql/sql.vm index 6e50837..82a5cb0 100644 --- a/yudao-module-infra/src/main/resources/codegen/sql/sql.vm +++ b/yudao-module-infra/src/main/resources/codegen/sql/sql.vm @@ -1,6 +1,6 @@ ## 通用变量定义 -#set ($functionNames = ['查询', '创建', '更新', '删除', '导出']) -#set ($functionOps = ['query', 'create', 'update', 'delete', 'export']) +#set ($functionNames = ['查询', '创建', '更新', '删除', '导出', '导入']) +#set ($functionOps = ['query', 'create', 'update', 'delete', 'export', 'import']) ## ## 宏定义:生成按钮 SQL(通用部分) #macro(insertButtonSql $parentIdVar) diff --git a/yudao-module-infra/src/main/resources/codegen/vue3/api/api.ts.vm b/yudao-module-infra/src/main/resources/codegen/vue3/api/api.ts.vm index c9f7361..aeade85 100644 --- a/yudao-module-infra/src/main/resources/codegen/vue3/api/api.ts.vm +++ b/yudao-module-infra/src/main/resources/codegen/vue3/api/api.ts.vm @@ -98,6 +98,16 @@ export const ${simpleClassName}Api = { export${simpleClassName}: async (params) => { return await request.download({ url: `${baseURL}/export-excel`, params }) }, + + // 下载${table.classComment}导入模板 + import${simpleClassName}Template: async () => { + return await request.download({ url: `${baseURL}/get-import-template` }) + }, + + // 导入${table.classComment} + import${simpleClassName}: async (data: FormData) => { + return await request.upload({ url: `${baseURL}/import`, data }) + }, ## 特殊:主子表专属逻辑 #foreach ($subTable in $subTables) #set ($index = $foreach.count - 1) diff --git a/yudao-module-infra/src/main/resources/codegen/vue3/views/index.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3/views/index.vue.vm index 857972a..c722f4c 100644 --- a/yudao-module-infra/src/main/resources/codegen/vue3/views/index.vue.vm +++ b/yudao-module-infra/src/main/resources/codegen/vue3/views/index.vue.vm @@ -93,6 +93,14 @@ 新增 + 导入 + + + + + + +
将文件拖到此处,或点击上传
+ +
+ +
+ <${simpleClassName}Form ref="formRef" @success="getList" /> ## 特殊:主子表专属逻辑 @@ -263,6 +308,7 @@ import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict' import { isEmpty } from '@/utils/is' import { dateFormatter } from '@/utils/formatTime' +import type { UploadUserFile } from 'element-plus' ## 特殊:树表专属逻辑 #if ( $table.templateType == 2 ) import { handleTree } from '@/utils/tree' @@ -308,6 +354,10 @@ const queryParams = reactive({ }) const queryFormRef = ref() // 搜索的表单 const exportLoading = ref(false) // 导出的加载中 +const importDialogVisible = ref(false) // 导入弹窗 +const importFormLoading = ref(false) // 导入的加载中 +const uploadRef = ref() +const fileList = ref([]) /** 查询列表 */ const getList = async () => { @@ -345,6 +395,58 @@ const openForm = (type: string, id?: number) => { formRef.value.open(type, id) } +/** 导入按钮操作 */ +const handleImport = async () => { + importDialogVisible.value = true + await resetImportForm() +} + +/** 提交导入 */ +const submitImportForm = async () => { + if (fileList.value.length === 0) { + message.error('请上传文件') + return + } + importFormLoading.value = true + try { + const formData = new FormData() + formData.append('file', fileList.value[0].raw as Blob) + const res = await ${simpleClassName}Api.import${simpleClassName}(formData) + const data = res.data + let text = '导入成功数量:' + data.successCount + ';导入失败数量:' + data.failureCount + ';' + if (data.failureCount > 0) { + for (const rowNo in data.failureRows) { + text += '< 第' + rowNo + '行: ' + data.failureRows[rowNo] + ' >' + } + } + message.alert(text) + importDialogVisible.value = false + await getList() + } catch { + } finally { + importFormLoading.value = false + await resetImportForm() + } +} + +/** 下载导入模板 */ +const handleImportTemplate = async () => { + const data = await ${simpleClassName}Api.import${simpleClassName}Template() + download.excel(data, '${table.classComment}导入模板.xls') +} + +/** 导入文件超限 */ +const handleImportExceed = (): void => { + message.error('最多只能上传一个文件!') +} + +/** 重置导入表单 */ +const resetImportForm = async () => { + fileList.value = [] + await nextTick() + uploadRef.value?.clearFiles() +} + /** 删除按钮操作 */ const handleDelete = async (id: number) => { try { @@ -421,4 +523,4 @@ const toggleExpandAll = async () => { onMounted(() => { getList() }) - \ No newline at end of file +