diff --git a/backend/src/main/java/com/yfd/platform/data/controller/FishDraftDataController.java b/backend/src/main/java/com/yfd/platform/data/controller/FishDraftDataController.java index cb32886..ef26c1a 100644 --- a/backend/src/main/java/com/yfd/platform/data/controller/FishDraftDataController.java +++ b/backend/src/main/java/com/yfd/platform/data/controller/FishDraftDataController.java @@ -7,8 +7,10 @@ import com.yfd.platform.config.ResponseResult; import com.yfd.platform.data.domain.FishDraftData; import com.yfd.platform.data.domain.FishImportRequest; import com.yfd.platform.data.domain.FishImportResult; +import com.yfd.platform.data.domain.ImportTask; import com.yfd.platform.data.service.IFishDraftDataService; import com.yfd.platform.data.service.IFishImportService; +import com.yfd.platform.data.service.IImportTaskService; import com.yfd.platform.utils.KendoUtil; import com.yfd.platform.utils.QgcQueryWrapperUtil; import io.swagger.v3.oas.annotations.Operation; @@ -20,6 +22,7 @@ import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.Date; import java.util.List; +import java.util.UUID; /** *

@@ -37,18 +40,8 @@ public class FishDraftDataController { @Resource private IFishImportService fishImportService; -// @GetMapping("/page") -// @Operation(summary = "分页查询过鱼数据") -// public ResponseResult queryPageList( -// @RequestParam(defaultValue = "1") Integer current, -// @RequestParam(defaultValue = "10") Integer size, -// @RequestParam(required = false) String stcd, -// @RequestParam(required = false) String status, -// @RequestParam(required = false) String ftp) { -// Page page = new Page<>(current, size); -// Page result = fishDraftDataService.queryPageList(page, stcd, status, ftp); -// return ResponseResult.successData(result); -// } + @Resource + private IImportTaskService importTaskService; @PostMapping("/page") @Operation(summary = "分页查询过鱼数据") @@ -71,7 +64,7 @@ public class FishDraftDataController { @GetMapping("/getById") @Operation(summary = "根据ID查询") - public ResponseResult getById(@RequestParam Long id) { + public ResponseResult getById(@RequestParam String id) { FishDraftData fishDraftData = fishDraftDataService.getById(id); return ResponseResult.successData(fishDraftData); } @@ -177,38 +170,82 @@ public class FishDraftDataController { return result ? ResponseResult.success("删除成功") : ResponseResult.error("删除失败"); } - @PostMapping("/importExcel") - @Operation(summary = "导入Excel过鱼数据") - public ResponseResult importExcel(@RequestParam("file") MultipartFile file, - @RequestParam(required = false) String importNo, - @RequestParam(required = false) Long uploadUserId) { + @PostMapping("/importZip") + @Operation(summary = "导入ZIP过鱼数据(每个用户同时只能进行一次导入)") + public ResponseResult importZip(@RequestParam("file") MultipartFile file, + @RequestParam String uploadUserId) { if (file == null || file.isEmpty()) { return ResponseResult.error("请上传文件"); } String fileName = file.getOriginalFilename(); - if (fileName == null || (!fileName.endsWith(".xlsx") && !fileName.endsWith(".xls"))) { - return ResponseResult.error("请上传Excel文件(.xlsx或.xls)"); + if (fileName == null || (!fileName.endsWith(".zip"))) { + return ResponseResult.error("请上传ZIP文件(.zip)"); } + + if (importTaskService.hasImportingTask(uploadUserId)) { + return ResponseResult.error("您有正在进行的导入任务,请等待完成后重试"); + } + + String importNo = "IMP" + System.currentTimeMillis(); + String taskId = UUID.randomUUID().toString(); + try { + ImportTask task = new ImportTask(); + task.setId(taskId); + task.setImportNo(importNo); + task.setBizType("FISH"); + task.setFileName(fileName); + task.setFileSize(file.getSize()); + task.setStatus("UPLOADED"); + task.setUploadUserId(uploadUserId); + task.setUploadTime(new Date()); + importTaskService.save(task); + FishImportRequest request = new FishImportRequest(); - request.setFilePath(file.getOriginalFilename()); request.setImportNo(importNo); request.setUploadUserId(uploadUserId); request.setBizType("FISH"); - FishImportResult result = fishImportService.parseAndMapExcel(request, file.getInputStream()); + + FishImportResult result = fishImportService.parseAndMapZip(file, uploadUserId); + + importTaskService.updateStatus(taskId, "VALIDATED", null); + importTaskService.updateProgress(taskId, result.getTotalCount(), result.getSuccessCount(), result.getFailedCount()); + return ResponseResult.successData(result); - } catch (IOException e) { - return ResponseResult.error("文件读取失败: " + e.getMessage()); + + } catch (Exception e) { + importTaskService.markFailed(taskId, "导入失败: " + e.getMessage()); + return ResponseResult.error("导入失败: " + e.getMessage()); } } - @PostMapping("/importExcelFromPath") - @Operation(summary = "从文件路径导入Excel过鱼数据") - public ResponseResult importExcelFromPath(@RequestBody FishImportRequest request) { - if (request.getFilePath() == null || request.getFilePath().isEmpty()) { - return ResponseResult.error("请提供文件路径"); + @PostMapping("/cancelImport") + @Operation(summary = "取消导入任务") + public ResponseResult cancelImport(@RequestParam String taskId, + @RequestParam String operatorId) { + boolean result = importTaskService.cancelTask(taskId, operatorId); + return result ? ResponseResult.success("取消成功") : ResponseResult.error("取消失败"); + } + + @GetMapping("/checkImportStatus") + @Operation(summary = "检查用户导入状态") + public ResponseResult checkImportStatus(@RequestParam String uploadUserId) { + boolean hasTask = importTaskService.hasImportingTask(uploadUserId); + ImportTask currentTask = importTaskService.getCurrentTaskByUserId(uploadUserId); + return ResponseResult.successData(java.util.Map.of( + "hasImportingTask", hasTask, + "currentTask", currentTask + )); + } + + @PostMapping("/cleanupTemp") + @Operation(summary = "清理临时文件") + public ResponseResult cleanupTemp(@RequestParam String tempDir) { + try { + com.yfd.platform.data.utils.ZipFileUtil.cleanupTempDir(tempDir); + return ResponseResult.success("清理成功"); + } catch (Exception e) { + return ResponseResult.error("清理失败: " + e.getMessage()); } - FishImportResult result = fishImportService.parseAndMapExcelFromPath(request); - return ResponseResult.successData(result); } } \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/data/domain/FishImportRequest.java b/backend/src/main/java/com/yfd/platform/data/domain/FishImportRequest.java index 44c909d..3d063da 100644 --- a/backend/src/main/java/com/yfd/platform/data/domain/FishImportRequest.java +++ b/backend/src/main/java/com/yfd/platform/data/domain/FishImportRequest.java @@ -14,7 +14,7 @@ public class FishImportRequest implements Serializable { private String bizType = "FISH"; - private Long uploadUserId; + private String uploadUserId; private Map columnMapping; } \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/data/domain/FishImportResult.java b/backend/src/main/java/com/yfd/platform/data/domain/FishImportResult.java index ec27ddd..6a5ecaa 100644 --- a/backend/src/main/java/com/yfd/platform/data/domain/FishImportResult.java +++ b/backend/src/main/java/com/yfd/platform/data/domain/FishImportResult.java @@ -3,7 +3,9 @@ package com.yfd.platform.data.domain; import lombok.Data; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; @Data public class FishImportResult { @@ -11,6 +13,11 @@ public class FishImportResult { private List successRows; private List failedRows; private List unrecognizedFields; + private Map imageFiles; + private Map videoFiles; + private String tempDir; + private String excelFileName; + private String excelFilePath; private int totalCount; private int successCount; private int failedCount; @@ -20,6 +27,8 @@ public class FishImportResult { this.successRows = new ArrayList<>(); this.failedRows = new ArrayList<>(); this.unrecognizedFields = new ArrayList<>(); + this.imageFiles = new LinkedHashMap<>(); + this.videoFiles = new LinkedHashMap<>(); } @Data diff --git a/backend/src/main/java/com/yfd/platform/data/mapper/ImportTaskMapper.java b/backend/src/main/java/com/yfd/platform/data/mapper/ImportTaskMapper.java index dbc74bc..5a45a82 100644 --- a/backend/src/main/java/com/yfd/platform/data/mapper/ImportTaskMapper.java +++ b/backend/src/main/java/com/yfd/platform/data/mapper/ImportTaskMapper.java @@ -36,7 +36,17 @@ public interface ImportTaskMapper extends BaseMapper { * 根据上传人查询 */ @Select("SELECT * FROM IMPORT_TASK WHERE UPLOAD_USER_ID = #{uploadUserId} ORDER BY CREATED_AT DESC") - List selectByUploadUserId(@Param("uploadUserId") Long uploadUserId); + List selectByUploadUserId(@Param("uploadUserId") String uploadUserId); + + @Select("") + List selectByUserIdAndStatuses(@Param("uploadUserId") String uploadUserId, + @Param("statuses") List statuses); /** * 查询过期的任务 diff --git a/backend/src/main/java/com/yfd/platform/data/service/IFishImportService.java b/backend/src/main/java/com/yfd/platform/data/service/IFishImportService.java index f85b28d..7a597ba 100644 --- a/backend/src/main/java/com/yfd/platform/data/service/IFishImportService.java +++ b/backend/src/main/java/com/yfd/platform/data/service/IFishImportService.java @@ -2,6 +2,7 @@ package com.yfd.platform.data.service; import com.yfd.platform.data.domain.FishImportRequest; import com.yfd.platform.data.domain.FishImportResult; +import org.springframework.web.multipart.MultipartFile; import java.io.InputStream; @@ -12,20 +13,9 @@ import java.io.InputStream; */ public interface IFishImportService { - /** - * 解析过鱼数据Excel文件并映射字段 - * - * @param request 导入请求 - * @param inputStream Excel文件输入流 - * @return 解析结果 - */ FishImportResult parseAndMapExcel(FishImportRequest request, InputStream inputStream); - /** - * 解析过鱼数据Excel文件(从文件路径) - * - * @param request 导入请求 - * @return 解析结果 - */ FishImportResult parseAndMapExcelFromPath(FishImportRequest request); + + FishImportResult parseAndMapZip(MultipartFile file, String uploadUserId); } \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/data/service/IFishZipImportService.java b/backend/src/main/java/com/yfd/platform/data/service/IFishZipImportService.java new file mode 100644 index 0000000..a853439 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/data/service/IFishZipImportService.java @@ -0,0 +1,40 @@ +package com.yfd.platform.data.service; + +import com.yfd.platform.data.domain.FishImportResult; + +import java.io.InputStream; + +/** + *

+ * 过鱼数据ZIP导入服务接口 + *

+ */ +public interface IFishZipImportService { + + /** + * 解析过鱼数据ZIP文件并映射字段 + * + * @param importTaskId 导入任务ID + * @param inputStream ZIP文件输入流 + * @param uploadUserId 上传用户ID + * @return 解析结果 + */ + FishImportResult parseAndMapZip(String importTaskId, InputStream inputStream, String uploadUserId); + + /** + * 检查用户是否有正在进行的导入任务 + * + * @param uploadUserId 上传用户ID + * @return true表示有任务在进行,false表示可以开始新任务 + */ + boolean hasImportingTask(String uploadUserId); + + /** + * 取消导入任务 + * + * @param importTaskId 任务ID + * @param operatorId 操作人ID + * @return 是否成功 + */ + boolean cancelImportTask(String importTaskId, String operatorId); +} \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/data/service/IImportTaskService.java b/backend/src/main/java/com/yfd/platform/data/service/IImportTaskService.java index 7b305bd..5879bd5 100644 --- a/backend/src/main/java/com/yfd/platform/data/service/IImportTaskService.java +++ b/backend/src/main/java/com/yfd/platform/data/service/IImportTaskService.java @@ -62,4 +62,19 @@ public interface IImportTaskService extends IService { * 删除过期任务 */ boolean deleteExpiredTasks(); + + /** + * 检查用户是否有正在进行的导入任务 + */ + boolean hasImportingTask(String uploadUserId); + + /** + * 取消导入任务 + */ + boolean cancelTask(String taskId, String operatorId); + + /** + * 获取用户当前正在进行的导入任务 + */ + ImportTask getCurrentTaskByUserId(String uploadUserId); } \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/data/service/impl/FishImportServiceImpl.java b/backend/src/main/java/com/yfd/platform/data/service/impl/FishImportServiceImpl.java index 1030ec7..b37b1b1 100644 --- a/backend/src/main/java/com/yfd/platform/data/service/impl/FishImportServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/data/service/impl/FishImportServiceImpl.java @@ -4,6 +4,7 @@ import com.yfd.platform.data.domain.FishDraftData; import com.yfd.platform.data.domain.FishImportRequest; import com.yfd.platform.data.domain.FishImportResult; import com.yfd.platform.data.service.IFishImportService; +import com.yfd.platform.data.utils.ZipFileUtil; import com.yfd.platform.env.domain.SdEngInfoBH; import com.yfd.platform.env.domain.SdHydrobase; import com.yfd.platform.env.mapper.SdEngInfoBHMapper; @@ -13,7 +14,10 @@ import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.text.ParseException; @@ -400,4 +404,39 @@ public class FishImportServiceImpl implements IFishImportService { } return "IMPORT"; } + + @Override + public FishImportResult parseAndMapZip(MultipartFile file, String uploadUserId) { + FishImportResult result = new FishImportResult(); + + try { + ZipFileUtil.ZipContent zipContent = ZipFileUtil.extractZipToTemp(file); + + if (zipContent.excelFilePath == null) { + result.setSummary("ZIP文件中未找到Excel文件"); + ZipFileUtil.cleanupTempDir(zipContent.tempDir); + return result; + } + + try (Workbook workbook = new XSSFWorkbook(new FileInputStream(zipContent.excelFilePath))) { + Sheet sheet = workbook.getSheetAt(0); + result = parseSheet(sheet, result); + } + + result.setImageFiles(zipContent.images); + result.setVideoFiles(zipContent.videos); + result.setTempDir(zipContent.tempDir); + result.setExcelFileName(zipContent.excelFileName); + result.setExcelFilePath(zipContent.excelFilePath); + + result.setSummary(result.getSummary() + String.format("\nZIP内容: 发现%d张图片, %d个视频, 临时目录: %s", + zipContent.images.size(), zipContent.videos.size(), zipContent.tempDir)); + + return result; + + } catch (Exception e) { + result.setSummary("ZIP文件解析失败: " + e.getMessage()); + return result; + } + } } \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/data/service/impl/ImportTaskServiceImpl.java b/backend/src/main/java/com/yfd/platform/data/service/impl/ImportTaskServiceImpl.java index 0e31749..3fdee2d 100644 --- a/backend/src/main/java/com/yfd/platform/data/service/impl/ImportTaskServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/data/service/impl/ImportTaskServiceImpl.java @@ -137,4 +137,38 @@ public class ImportTaskServiceImpl extends ServiceImpl ids = expiredTasks.stream().map(ImportTask::getId).toList(); return this.removeByIds(ids); } + + @Override + public boolean hasImportingTask(String uploadUserId) { + List activeStatuses = List.of("UPLOADED", "PARSING", "VALIDATING"); + List tasks = importTaskMapper.selectByUserIdAndStatuses(uploadUserId, activeStatuses); + return tasks != null && !tasks.isEmpty(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean cancelTask(String taskId, String operatorId) { + ImportTask importTask = this.getById(taskId); + if (importTask == null) { + return false; + } + String currentStatus = importTask.getStatus(); + if ("CONFIRMED".equals(currentStatus) || "FAILED".equals(currentStatus) || "CANCELLED".equals(currentStatus)) { + return false; + } + importTask.setStatus("CANCELLED"); + importTask.setErrorMsg("用户取消: " + operatorId); + importTask.setUpdatedAt(new Date()); + return this.updateById(importTask); + } + + @Override + public ImportTask getCurrentTaskByUserId(String uploadUserId) { + List activeStatuses = List.of("UPLOADED", "PARSING", "VALIDATING"); + List tasks = importTaskMapper.selectByUserIdAndStatuses(uploadUserId, activeStatuses); + if (tasks == null || tasks.isEmpty()) { + return null; + } + return tasks.get(0); + } } \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/data/utils/ZipFileUtil.java b/backend/src/main/java/com/yfd/platform/data/utils/ZipFileUtil.java new file mode 100644 index 0000000..137e293 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/data/utils/ZipFileUtil.java @@ -0,0 +1,219 @@ +package com.yfd.platform.data.utils; + +import org.springframework.web.multipart.MultipartFile; + +import java.io.*; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; + +public class ZipFileUtil { + + public static final String DEFAULT_TEMP_BASE = "D:\\zip_import_temp"; + + public static class ZipContent { + public String excelFileName; + public String excelFilePath; + public Map images; + public Map videos; + public Map otherFiles; + public String tempDir; + + public ZipContent() { + this.images = new HashMap<>(); + this.videos = new HashMap<>(); + this.otherFiles = new HashMap<>(); + } + + public InputStream getExcelStream() throws IOException { + if (excelFilePath == null) { + return null; + } + return new FileInputStream(excelFilePath); + } + } + + public static String getDefaultTempDir() { + return DEFAULT_TEMP_BASE; + } + + public static ZipContent extractZipToTemp(MultipartFile file) throws IOException { + return extractZipToTemp(file, DEFAULT_TEMP_BASE); + } + + public static ZipContent extractZipToTemp(MultipartFile file, String baseTempDir) throws IOException { + ZipContent content = new ZipContent(); + + String taskId = UUID.randomUUID().toString().substring(0, 8); + Path tempDirPath = Paths.get(baseTempDir, "zip_" + taskId); + Files.createDirectories(tempDirPath); + content.tempDir = tempDirPath.toString(); + + File zipFile = new File(tempDirPath.toFile(), "upload.zip"); + file.transferTo(zipFile); + + try { + if (!isValidZipFile(zipFile)) { + throw new IOException("文件不是有效的ZIP格式或已损坏"); + } + + IOException lastException = null; + Charset[] charsets = new Charset[]{ + StandardCharsets.UTF_8, + Charset.forName("GBK"), + StandardCharsets.ISO_8859_1, + Charset.forName("GB18030") + }; + + for (Charset charset : charsets) { + try { + content = extractFromZipFile(zipFile, tempDirPath.toFile(), charset); + content.tempDir = tempDirPath.toString(); + return content; + } catch (IOException e) { + lastException = e; + content = new ZipContent(); + content.tempDir = tempDirPath.toString(); + } + } + + throw lastException != null ? lastException : new IOException("无法解析ZIP文件"); + + } finally { + if (zipFile.exists()) { + zipFile.delete(); + } + } + } + + private static boolean isValidZipFile(File file) { + if (file == null || file.length() < 4) { + return false; + } + try (FileInputStream fis = new FileInputStream(file)) { + byte[] signature = new byte[4]; + int read = fis.read(signature); + if (read != 4) { + return false; + } + return signature[0] == 0x50 && signature[1] == 0x4B && + signature[2] == 0x03 && signature[3] == 0x04; + } catch (IOException e) { + return false; + } + } + + private static ZipContent extractFromZipFile(File zipFile, File tempDir, Charset charset) throws IOException { + ZipContent content = new ZipContent(); + + try (ZipFile zip = new ZipFile(zipFile, charset)) { + Enumeration entries = zip.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + String entryName = entry.getName(); + + if (entry.isDirectory()) { + continue; + } + + try (InputStream is = zip.getInputStream(entry)) { + String lowerName = entryName.toLowerCase(); + + if (lowerName.endsWith(".xlsx") || lowerName.endsWith(".xls")) { + String excelPath = saveFileToDir(is, tempDir, "excel", getFileName(entryName)); + content.excelFileName = getFileName(entryName); + content.excelFilePath = excelPath; + } else if (isImageFile(lowerName)) { + String folder = "images"; + String path = saveFileToDir(is, tempDir, folder, getFileName(entryName)); + content.images.put(entryName, path); + } else if (isVideoFile(lowerName)) { + String folder = "videos"; + String path = saveFileToDir(is, tempDir, folder, getFileName(entryName)); + content.videos.put(entryName, path); + } else { + String folder = "others"; + String path = saveFileToDir(is, tempDir, folder, getFileName(entryName)); + content.otherFiles.put(entryName, path); + } + } + } + } + + return content; + } + + private static String getFileName(String entryName) { + if (entryName == null) return ""; + int lastSep = Math.max(entryName.lastIndexOf('/'), entryName.lastIndexOf('\\')); + return lastSep >= 0 ? entryName.substring(lastSep + 1) : entryName; + } + + private static String saveFileToDir(InputStream is, File tempDir, String subFolder, String fileName) throws IOException { + File folder = new File(tempDir, subFolder); + if (!folder.exists()) { + folder.mkdirs(); + } + + File file = new File(folder, fileName); + try (FileOutputStream fos = new FileOutputStream(file)) { + byte[] buffer = new byte[4096]; + int len; + while ((len = is.read(buffer)) > 0) { + fos.write(buffer, 0, len); + } + } + return file.getAbsolutePath(); + } + + private static boolean isImageFile(String fileName) { + return fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") || + fileName.endsWith(".png") || fileName.endsWith(".gif") || + fileName.endsWith(".bmp") || fileName.endsWith(".webp"); + } + + private static boolean isVideoFile(String fileName) { + return fileName.endsWith(".mp4") || fileName.endsWith(".avi") || + fileName.endsWith(".mov") || fileName.endsWith(".wmv") || + fileName.endsWith(".flv") || fileName.endsWith(".mkv"); + } + + public static void cleanupTempDir(String tempDir) { + if (tempDir == null || tempDir.isEmpty()) { + return; + } + File dir = new File(tempDir); + if (dir.exists()) { + deleteDirectory(dir); + } + } + + private static void deleteDirectory(File dir) { + File[] files = dir.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + deleteDirectory(file); + } else { + file.delete(); + } + } + } + dir.delete(); + } + + public static String getFileExtension(String fileName) { + if (fileName == null) return ""; + int lastDot = fileName.lastIndexOf('.'); + return lastDot >= 0 ? fileName.substring(lastDot + 1).toLowerCase() : ""; + } +} \ No newline at end of file diff --git a/backend/src/main/resources/application-devtw.yml b/backend/src/main/resources/application-devtw.yml index efdf42b..05839c9 100644 --- a/backend/src/main/resources/application-devtw.yml +++ b/backend/src/main/resources/application-devtw.yml @@ -58,8 +58,8 @@ mybatis-plus: configuration: map-underscore-to-camel-case: true cache-enabled: false -# log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl - log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl +# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 登录相关配置