Commit 48eb3e33ee9204b611d71eab98b7854ed753373e

Authored by YunaiV
2 parents d93730d1 60179ee6

Merge branch 'master-jdk17' of https://github.com/YunaiV/ruoyi-vue-pro into master-jdk17

yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java
... ... @@ -72,6 +72,8 @@ public class CodegenEngine {
72 72 .put(javaTemplatePath("controller/vo/listReqVO"), javaModuleImplVOFilePath("ListReqVO"))
73 73 .put(javaTemplatePath("controller/vo/respVO"), javaModuleImplVOFilePath("RespVO"))
74 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 77 .put(javaTemplatePath("controller/controller"), javaModuleImplControllerFilePath())
76 78 .put(javaTemplatePath("dal/do"),
77 79 javaModuleImplMainFilePath("dal/dataobject/${table.businessName}/${table.className}DO"))
... ...
yudao-module-infra/src/main/resources/codegen/java/controller/controller.vm
1 1 package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName};
2 2  
3 3 import org.springframework.web.bind.annotation.*;
  4 +import org.springframework.web.multipart.MultipartFile;
4 5 import ${jakartaPackage}.annotation.Resource;
5 6 import org.springframework.validation.annotation.Validated;
6 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 161 }
161 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 187 #foreach ($subTable in $subTables)
165 188 #set ($index = $foreach.count - 1)
... ... @@ -268,4 +291,4 @@ public class ${sceneEnum.prefixClass}${table.className}Controller {
268 291  
269 292 #end
270 293 #end
271   -}
272 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 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 38 * 更新${table.classComment}
31 39 *
32 40 * @param ${updateReqVOVar} 更新信息
... ... @@ -162,4 +170,4 @@ public interface ${table.className}Service {
162 170  
163 171 #end
164 172 #end
165   -}
166 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 7 import org.springframework.transaction.annotation.Transactional;
8 8  
9 9 import java.util.*;
  10 +import java.util.concurrent.atomic.AtomicInteger;
10 11 import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*;
11 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 94 }
94 95  
95 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 137 ## 特殊:主子表专属逻辑(非 ERP 模式)
97 138 #if ( $subTables && $subTables.size() > 0 && $table.templateType != 11 )
98 139 @Transactional(rollbackFor = Exception.class)
... ... @@ -359,6 +400,9 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
359 400 #else
360 401 #if ( $subTable.subJoinMany)
361 402 private void create${subSimpleClassName}List(${primaryColumn.javaType} ${subJoinColumn.javaField}, List<${subTable.className}DO> list) {
  403 + if (CollUtil.isEmpty(list)) {
  404 + return;
  405 + }
362 406 list.forEach(o -> o.set${SubJoinColumnName}(${subJoinColumn.javaField}).clean());
363 407 ${subClassNameVars.get($index)}Mapper.insertBatch(list);
364 408 }
... ... @@ -416,4 +460,4 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
416 460 #end
417 461  
418 462 #end
419   -}
420 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 5 ## 宏定义:生成按钮 SQL(通用部分)
6 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 98 export${simpleClassName}: async (params) => {
99 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 112 #foreach ($subTable in $subTables)
103 113 #set ($index = $foreach.count - 1)
... ...
yudao-module-infra/src/main/resources/codegen/vue3/views/index.vue.vm
... ... @@ -93,6 +93,14 @@
93 93 <Icon icon="ep:plus" class="mr-5px" /> 新增
94 94 </el-button>
95 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 104 type="success"
97 105 plain
98 106 @click="handleExport"
... ... @@ -238,6 +246,43 @@
238 246 />
239 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 287 <${simpleClassName}Form ref="formRef" @success="getList" />
243 288 ## 特殊:主子表专属逻辑
... ... @@ -263,6 +308,7 @@
263 308 import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
264 309 import { isEmpty } from '@/utils/is'
265 310 import { dateFormatter } from '@/utils/formatTime'
  311 +import type { UploadUserFile } from 'element-plus'
266 312 ## 特殊:树表专属逻辑
267 313 #if ( $table.templateType == 2 )
268 314 import { handleTree } from '@/utils/tree'
... ... @@ -308,6 +354,10 @@ const queryParams = reactive({
308 354 })
309 355 const queryFormRef = ref() // 搜索的表单
310 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 363 const getList = async () => {
... ... @@ -345,6 +395,58 @@ const openForm = (type: string, id?: number) =&gt; {
345 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 451 const handleDelete = async (id: number) => {
350 452 try {
... ... @@ -421,4 +523,4 @@ const toggleExpandAll = async () =&gt; {
421 523 onMounted(() => {
422 524 getList()
423 525 })
424   -</script>
425 526 \ No newline at end of file
  527 +</script>
... ...