Commit 60179ee6b907eaac1219b06f62ddef30911671e5

Authored by 芋道源码
Committed by GitHub
2 parents 63c0fcdf 34292c25

Merge pull request #1082 from itkdm/feat/codegen-import-detail-jdk17

  feat(infra): 代码生成器为后端和 Vue3 模板添加 Excel 导入功能
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java
@@ -72,6 +72,8 @@ public class CodegenEngine { @@ -72,6 +72,8 @@ public class CodegenEngine {
72 .put(javaTemplatePath("controller/vo/listReqVO"), javaModuleImplVOFilePath("ListReqVO")) 72 .put(javaTemplatePath("controller/vo/listReqVO"), javaModuleImplVOFilePath("ListReqVO"))
73 .put(javaTemplatePath("controller/vo/respVO"), javaModuleImplVOFilePath("RespVO")) 73 .put(javaTemplatePath("controller/vo/respVO"), javaModuleImplVOFilePath("RespVO"))
74 .put(javaTemplatePath("controller/vo/saveReqVO"), javaModuleImplVOFilePath("SaveReqVO")) 74 .put(javaTemplatePath("controller/vo/saveReqVO"), javaModuleImplVOFilePath("SaveReqVO"))
  75 + .put(javaTemplatePath("controller/vo/importExcelVO"), javaModuleImplVOFilePath("ImportExcelVO"))
  76 + .put(javaTemplatePath("controller/vo/importRespVO"), javaModuleImplVOFilePath("ImportRespVO"))
75 .put(javaTemplatePath("controller/controller"), javaModuleImplControllerFilePath()) 77 .put(javaTemplatePath("controller/controller"), javaModuleImplControllerFilePath())
76 .put(javaTemplatePath("dal/do"), 78 .put(javaTemplatePath("dal/do"),
77 javaModuleImplMainFilePath("dal/dataobject/${table.businessName}/${table.className}DO")) 79 javaModuleImplMainFilePath("dal/dataobject/${table.businessName}/${table.className}DO"))
yudao-module-infra/src/main/resources/codegen/java/controller/controller.vm
1 package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}; 1 package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName};
2 2
3 import org.springframework.web.bind.annotation.*; 3 import org.springframework.web.bind.annotation.*;
  4 +import org.springframework.web.multipart.MultipartFile;
4 import ${jakartaPackage}.annotation.Resource; 5 import ${jakartaPackage}.annotation.Resource;
5 import org.springframework.validation.annotation.Validated; 6 import org.springframework.validation.annotation.Validated;
6 #if ($sceneEnum.scene == 1)import org.springframework.security.access.prepost.PreAuthorize;#end 7 #if ($sceneEnum.scene == 1)import org.springframework.security.access.prepost.PreAuthorize;#end
@@ -160,6 +161,28 @@ public class ${sceneEnum.prefixClass}${table.className}Controller { @@ -160,6 +161,28 @@ public class ${sceneEnum.prefixClass}${table.className}Controller {
160 } 161 }
161 #end 162 #end
162 163
  164 + @GetMapping("/get-import-template")
  165 + @Operation(summary = "获得导入${table.classComment}模板")
  166 +#if ($sceneEnum.scene == 1)
  167 + @PreAuthorize("@ss.hasPermission('${permissionPrefix}:import')")
  168 +#end
  169 + public void import${simpleClassName}Template(HttpServletResponse response) throws IOException {
  170 + ExcelUtils.write(response, "${table.classComment}导入模板.xls", "数据",
  171 + ${sceneEnum.prefixClass}${table.className}ImportExcelVO.class, Collections.emptyList());
  172 + }
  173 +
  174 + @PostMapping("/import")
  175 + @Operation(summary = "导入${table.classComment}")
  176 + @Parameter(name = "file", description = "Excel 文件", required = true)
  177 +#if ($sceneEnum.scene == 1)
  178 + @PreAuthorize("@ss.hasPermission('${permissionPrefix}:import')")
  179 +#end
  180 + @ApiAccessLog(operateType = IMPORT)
  181 + public CommonResult<${sceneEnum.prefixClass}${table.className}ImportRespVO> import${simpleClassName}(@RequestParam("file") MultipartFile file) throws Exception {
  182 + List<${sceneEnum.prefixClass}${table.className}ImportExcelVO> list = ExcelUtils.read(file, ${sceneEnum.prefixClass}${table.className}ImportExcelVO.class);
  183 + return success(${classNameVar}Service.import${simpleClassName}List(list));
  184 + }
  185 +
163 ## 特殊:主子表专属逻辑 186 ## 特殊:主子表专属逻辑
164 #foreach ($subTable in $subTables) 187 #foreach ($subTable in $subTables)
165 #set ($index = $foreach.count - 1) 188 #set ($index = $foreach.count - 1)
@@ -268,4 +291,4 @@ public class ${sceneEnum.prefixClass}${table.className}Controller { @@ -268,4 +291,4 @@ public class ${sceneEnum.prefixClass}${table.className}Controller {
268 291
269 #end 292 #end
270 #end 293 #end
271 -}  
272 \ No newline at end of file 294 \ No newline at end of file
  295 +}
yudao-module-infra/src/main/resources/codegen/java/controller/vo/importExcelVO.vm 0 → 100644
  1 +package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo;
  2 +
  3 +import cn.idev.excel.annotation.ExcelProperty;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Builder;
  6 +import lombok.Data;
  7 +import lombok.NoArgsConstructor;
  8 +#foreach ($column in $columns)
  9 +#if (${column.createOperation} && "$!column.dictType" != "")
  10 +import ${DictFormatClassName};
  11 +import ${DictConvertClassName};
  12 +#break
  13 +#end
  14 +#end
  15 +
  16 +/**
  17 + * ${table.classComment} Excel 导入 VO
  18 + */
  19 +@Data
  20 +@Builder
  21 +@AllArgsConstructor
  22 +@NoArgsConstructor
  23 +public class ${sceneEnum.prefixClass}${table.className}ImportExcelVO {
  24 +
  25 +## 逐个处理字段
  26 +#foreach ($column in $columns)
  27 +#if (${column.createOperation})
  28 +#if ("$!column.dictType" != "")
  29 + @ExcelProperty(value = "${column.columnComment}", converter = DictConvert.class)
  30 + @DictFormat("${column.dictType}") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
  31 +#else
  32 + @ExcelProperty("${column.columnComment}")
  33 +#end
  34 + private ${column.javaType} ${column.javaField};
  35 +
  36 +#end
  37 +#end
  38 +}
yudao-module-infra/src/main/resources/codegen/java/controller/vo/importRespVO.vm 0 → 100644
  1 +package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Builder;
  5 +import lombok.Data;
  6 +
  7 +import java.util.Map;
  8 +
  9 +@Schema(description = "${sceneEnum.name} - ${table.classComment}导入 Response VO")
  10 +@Data
  11 +@Builder
  12 +public class ${sceneEnum.prefixClass}${table.className}ImportRespVO {
  13 +
  14 + @Schema(description = "创建成功的数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
  15 + private Integer successCount;
  16 +
  17 + @Schema(description = "导入失败的数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  18 + private Integer failureCount;
  19 +
  20 + @Schema(description = "导入失败的数据集合,key 为行号,value 为失败原因", requiredMode = Schema.RequiredMode.REQUIRED)
  21 + private Map<Integer, String> failureRows;
  22 +
  23 +}
yudao-module-infra/src/main/resources/codegen/java/service/service.vm
@@ -27,6 +27,14 @@ public interface ${table.className}Service { @@ -27,6 +27,14 @@ public interface ${table.className}Service {
27 ${primaryColumn.javaType} create${simpleClassName}(@Valid ${saveReqVOClass} ${saveReqVOVar}); 27 ${primaryColumn.javaType} create${simpleClassName}(@Valid ${saveReqVOClass} ${saveReqVOVar});
28 28
29 /** 29 /**
  30 + * 导入${table.classComment}
  31 + *
  32 + * @param importList 导入信息
  33 + * @return 导入结果
  34 + */
  35 + ${sceneEnum.prefixClass}${table.className}ImportRespVO import${simpleClassName}List(List<${sceneEnum.prefixClass}${table.className}ImportExcelVO> importList);
  36 +
  37 + /**
30 * 更新${table.classComment} 38 * 更新${table.classComment}
31 * 39 *
32 * @param ${updateReqVOVar} 更新信息 40 * @param ${updateReqVOVar} 更新信息
@@ -162,4 +170,4 @@ public interface ${table.className}Service { @@ -162,4 +170,4 @@ public interface ${table.className}Service {
162 170
163 #end 171 #end
164 #end 172 #end
165 -}  
166 \ No newline at end of file 173 \ No newline at end of file
  174 +}
yudao-module-infra/src/main/resources/codegen/java/service/serviceImpl.vm
@@ -7,6 +7,7 @@ import org.springframework.validation.annotation.Validated; @@ -7,6 +7,7 @@ import org.springframework.validation.annotation.Validated;
7 import org.springframework.transaction.annotation.Transactional; 7 import org.springframework.transaction.annotation.Transactional;
8 8
9 import java.util.*; 9 import java.util.*;
  10 +import java.util.concurrent.atomic.AtomicInteger;
10 import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; 11 import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*;
11 import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; 12 import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO;
12 ## 特殊:主子表专属逻辑 13 ## 特殊:主子表专属逻辑
@@ -93,6 +94,46 @@ public class ${table.className}ServiceImpl implements ${table.className}Service @@ -93,6 +94,46 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
93 } 94 }
94 95
95 @Override 96 @Override
  97 + @Transactional(rollbackFor = Exception.class)
  98 + public ${sceneEnum.prefixClass}${table.className}ImportRespVO import${simpleClassName}List(List<${sceneEnum.prefixClass}${table.className}ImportExcelVO> importList) {
  99 + if (CollUtil.isEmpty(importList)) {
  100 + return ${sceneEnum.prefixClass}${table.className}ImportRespVO.builder()
  101 + .successCount(0)
  102 + .failureCount(0)
  103 + .failureRows(new LinkedHashMap<>())
  104 + .build();
  105 + }
  106 + AtomicInteger successCount = new AtomicInteger();
  107 + Map<Integer, String> failureRows = new LinkedHashMap<>();
  108 + AtomicInteger index = new AtomicInteger(1);
  109 + importList.forEach(importItem -> {
  110 + int currentIndex = index.getAndIncrement();
  111 + try {
  112 + create${simpleClassName}(BeanUtils.toBean(importItem, ${saveReqVOClass}.class));
  113 + successCount.incrementAndGet();
  114 + } catch (Exception ex) {
  115 + failureRows.put(currentIndex, resolveImportFailureMessage(ex));
  116 + }
  117 + });
  118 + return ${sceneEnum.prefixClass}${table.className}ImportRespVO.builder()
  119 + .successCount(successCount.get())
  120 + .failureCount(failureRows.size())
  121 + .failureRows(failureRows)
  122 + .build();
  123 + }
  124 +
  125 + /**
  126 + * 仅返回根因异常信息,避免把 MyBatis 长堆栈直接暴露给前端。
  127 + */
  128 + private String resolveImportFailureMessage(Exception ex) {
  129 + Throwable root = ex;
  130 + while (root.getCause() != null && root.getCause() != root) {
  131 + root = root.getCause();
  132 + }
  133 + return root.getMessage() != null ? root.getMessage() : ex.getMessage();
  134 + }
  135 +
  136 + @Override
96 ## 特殊:主子表专属逻辑(非 ERP 模式) 137 ## 特殊:主子表专属逻辑(非 ERP 模式)
97 #if ( $subTables && $subTables.size() > 0 && $table.templateType != 11 ) 138 #if ( $subTables && $subTables.size() > 0 && $table.templateType != 11 )
98 @Transactional(rollbackFor = Exception.class) 139 @Transactional(rollbackFor = Exception.class)
@@ -359,6 +400,9 @@ public class ${table.className}ServiceImpl implements ${table.className}Service @@ -359,6 +400,9 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
359 #else 400 #else
360 #if ( $subTable.subJoinMany) 401 #if ( $subTable.subJoinMany)
361 private void create${subSimpleClassName}List(${primaryColumn.javaType} ${subJoinColumn.javaField}, List<${subTable.className}DO> list) { 402 private void create${subSimpleClassName}List(${primaryColumn.javaType} ${subJoinColumn.javaField}, List<${subTable.className}DO> list) {
  403 + if (CollUtil.isEmpty(list)) {
  404 + return;
  405 + }
362 list.forEach(o -> o.set${SubJoinColumnName}(${subJoinColumn.javaField}).clean()); 406 list.forEach(o -> o.set${SubJoinColumnName}(${subJoinColumn.javaField}).clean());
363 ${subClassNameVars.get($index)}Mapper.insertBatch(list); 407 ${subClassNameVars.get($index)}Mapper.insertBatch(list);
364 } 408 }
@@ -416,4 +460,4 @@ public class ${table.className}ServiceImpl implements ${table.className}Service @@ -416,4 +460,4 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
416 #end 460 #end
417 461
418 #end 462 #end
419 -}  
420 \ No newline at end of file 463 \ No newline at end of file
  464 +}
yudao-module-infra/src/main/resources/codegen/sql/sql.vm
1 ## 通用变量定义 1 ## 通用变量定义
2 -#set ($functionNames = ['查询', '创建', '更新', '删除', '导出'])  
3 -#set ($functionOps = ['query', 'create', 'update', 'delete', 'export']) 2 +#set ($functionNames = ['查询', '创建', '更新', '删除', '导出', '导入'])
  3 +#set ($functionOps = ['query', 'create', 'update', 'delete', 'export', 'import'])
4 ## 4 ##
5 ## 宏定义:生成按钮 SQL(通用部分) 5 ## 宏定义:生成按钮 SQL(通用部分)
6 #macro(insertButtonSql $parentIdVar) 6 #macro(insertButtonSql $parentIdVar)
yudao-module-infra/src/main/resources/codegen/vue3/api/api.ts.vm
@@ -98,6 +98,16 @@ export const ${simpleClassName}Api = { @@ -98,6 +98,16 @@ export const ${simpleClassName}Api = {
98 export${simpleClassName}: async (params) => { 98 export${simpleClassName}: async (params) => {
99 return await request.download({ url: `${baseURL}/export-excel`, params }) 99 return await request.download({ url: `${baseURL}/export-excel`, params })
100 }, 100 },
  101 +
  102 + // 下载${table.classComment}导入模板
  103 + import${simpleClassName}Template: async () => {
  104 + return await request.download({ url: `${baseURL}/get-import-template` })
  105 + },
  106 +
  107 + // 导入${table.classComment}
  108 + import${simpleClassName}: async (data: FormData) => {
  109 + return await request.upload({ url: `${baseURL}/import`, data })
  110 + },
101 ## 特殊:主子表专属逻辑 111 ## 特殊:主子表专属逻辑
102 #foreach ($subTable in $subTables) 112 #foreach ($subTable in $subTables)
103 #set ($index = $foreach.count - 1) 113 #set ($index = $foreach.count - 1)
yudao-module-infra/src/main/resources/codegen/vue3/views/index.vue.vm
@@ -93,6 +93,14 @@ @@ -93,6 +93,14 @@
93 <Icon icon="ep:plus" class="mr-5px" /> 新增 93 <Icon icon="ep:plus" class="mr-5px" /> 新增
94 </el-button> 94 </el-button>
95 <el-button 95 <el-button
  96 + type="warning"
  97 + plain
  98 + @click="handleImport"
  99 + v-hasPermi="['${permissionPrefix}:import']"
  100 + >
  101 + <Icon icon="ep:upload" class="mr-5px" /> 导入
  102 + </el-button>
  103 + <el-button
96 type="success" 104 type="success"
97 plain 105 plain
98 @click="handleExport" 106 @click="handleExport"
@@ -238,6 +246,43 @@ @@ -238,6 +246,43 @@
238 /> 246 />
239 </ContentWrap> 247 </ContentWrap>
240 248
  249 + <!-- 导入弹窗 -->
  250 + <Dialog v-model="importDialogVisible" title="${table.classComment}导入" width="400">
  251 + <el-upload
  252 + ref="uploadRef"
  253 + v-model:file-list="fileList"
  254 + :auto-upload="false"
  255 + :disabled="importFormLoading"
  256 + :limit="1"
  257 + :on-exceed="handleImportExceed"
  258 + accept=".xlsx, .xls"
  259 + action="none"
  260 + drag
  261 + >
  262 + <Icon icon="ep:upload" />
  263 + <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
  264 + <template #tip>
  265 + <div class="el-upload__tip text-center">
  266 + <span>仅允许导入 xls、xlsx 格式文件。</span>
  267 + <el-link
  268 + :underline="false"
  269 + style="font-size: 12px; vertical-align: baseline"
  270 + type="primary"
  271 + @click="handleImportTemplate"
  272 + >
  273 + 下载模板
  274 + </el-link>
  275 + </div>
  276 + </template>
  277 + </el-upload>
  278 + <template #footer>
  279 + <el-button :disabled="importFormLoading" type="primary" @click="submitImportForm">
  280 + 确 定
  281 + </el-button>
  282 + <el-button @click="importDialogVisible = false">取 消</el-button>
  283 + </template>
  284 + </Dialog>
  285 +
241 <!-- 表单弹窗:添加/修改 --> 286 <!-- 表单弹窗:添加/修改 -->
242 <${simpleClassName}Form ref="formRef" @success="getList" /> 287 <${simpleClassName}Form ref="formRef" @success="getList" />
243 ## 特殊:主子表专属逻辑 288 ## 特殊:主子表专属逻辑
@@ -263,6 +308,7 @@ @@ -263,6 +308,7 @@
263 import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict' 308 import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
264 import { isEmpty } from '@/utils/is' 309 import { isEmpty } from '@/utils/is'
265 import { dateFormatter } from '@/utils/formatTime' 310 import { dateFormatter } from '@/utils/formatTime'
  311 +import type { UploadUserFile } from 'element-plus'
266 ## 特殊:树表专属逻辑 312 ## 特殊:树表专属逻辑
267 #if ( $table.templateType == 2 ) 313 #if ( $table.templateType == 2 )
268 import { handleTree } from '@/utils/tree' 314 import { handleTree } from '@/utils/tree'
@@ -308,6 +354,10 @@ const queryParams = reactive({ @@ -308,6 +354,10 @@ const queryParams = reactive({
308 }) 354 })
309 const queryFormRef = ref() // 搜索的表单 355 const queryFormRef = ref() // 搜索的表单
310 const exportLoading = ref(false) // 导出的加载中 356 const exportLoading = ref(false) // 导出的加载中
  357 +const importDialogVisible = ref(false) // 导入弹窗
  358 +const importFormLoading = ref(false) // 导入的加载中
  359 +const uploadRef = ref()
  360 +const fileList = ref<UploadUserFile[]>([])
311 361
312 /** 查询列表 */ 362 /** 查询列表 */
313 const getList = async () => { 363 const getList = async () => {
@@ -345,6 +395,58 @@ const openForm = (type: string, id?: number) =&gt; { @@ -345,6 +395,58 @@ const openForm = (type: string, id?: number) =&gt; {
345 formRef.value.open(type, id) 395 formRef.value.open(type, id)
346 } 396 }
347 397
  398 +/** 导入按钮操作 */
  399 +const handleImport = async () => {
  400 + importDialogVisible.value = true
  401 + await resetImportForm()
  402 +}
  403 +
  404 +/** 提交导入 */
  405 +const submitImportForm = async () => {
  406 + if (fileList.value.length === 0) {
  407 + message.error('请上传文件')
  408 + return
  409 + }
  410 + importFormLoading.value = true
  411 + try {
  412 + const formData = new FormData()
  413 + formData.append('file', fileList.value[0].raw as Blob)
  414 + const res = await ${simpleClassName}Api.import${simpleClassName}(formData)
  415 + const data = res.data
  416 + let text = '导入成功数量:' + data.successCount + ';导入失败数量:' + data.failureCount + ';'
  417 + if (data.failureCount > 0) {
  418 + for (const rowNo in data.failureRows) {
  419 + text += '< 第' + rowNo + '行: ' + data.failureRows[rowNo] + ' >'
  420 + }
  421 + }
  422 + message.alert(text)
  423 + importDialogVisible.value = false
  424 + await getList()
  425 + } catch {
  426 + } finally {
  427 + importFormLoading.value = false
  428 + await resetImportForm()
  429 + }
  430 +}
  431 +
  432 +/** 下载导入模板 */
  433 +const handleImportTemplate = async () => {
  434 + const data = await ${simpleClassName}Api.import${simpleClassName}Template()
  435 + download.excel(data, '${table.classComment}导入模板.xls')
  436 +}
  437 +
  438 +/** 导入文件超限 */
  439 +const handleImportExceed = (): void => {
  440 + message.error('最多只能上传一个文件!')
  441 +}
  442 +
  443 +/** 重置导入表单 */
  444 +const resetImportForm = async () => {
  445 + fileList.value = []
  446 + await nextTick()
  447 + uploadRef.value?.clearFiles()
  448 +}
  449 +
348 /** 删除按钮操作 */ 450 /** 删除按钮操作 */
349 const handleDelete = async (id: number) => { 451 const handleDelete = async (id: number) => {
350 try { 452 try {
@@ -421,4 +523,4 @@ const toggleExpandAll = async () =&gt; { @@ -421,4 +523,4 @@ const toggleExpandAll = async () =&gt; {
421 onMounted(() => { 523 onMounted(() => {
422 getList() 524 getList()
423 }) 525 })
424 -</script>  
425 \ No newline at end of file 526 \ No newline at end of file
  527 +</script>