|
@@ -0,0 +1,416 @@
|
|
|
|
+package com.ruoyi.file.service.impl;
|
|
|
|
+
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
|
|
+import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
|
+import com.ruoyi.file.domain.FileUpdateInfo;
|
|
|
|
+import com.ruoyi.file.domain.FileWithId;
|
|
|
|
+import com.ruoyi.file.mapper.FileUpdateInfoMapper;
|
|
|
|
+import com.ruoyi.file.service.FileUpdateInfoService;
|
|
|
|
+import com.usky.common.core.bean.CommonPage;
|
|
|
|
+import org.apache.commons.codec.digest.DigestUtils;
|
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
+import org.slf4j.Logger;
|
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
|
+
|
|
|
|
+import java.io.*;
|
|
|
|
+import java.net.HttpURLConnection;
|
|
|
|
+import java.net.URL;
|
|
|
|
+import java.nio.file.Files;
|
|
|
|
+import java.nio.file.Path;
|
|
|
|
+import java.nio.file.Paths;
|
|
|
|
+import java.nio.file.StandardCopyOption;
|
|
|
|
+import java.text.SimpleDateFormat;
|
|
|
|
+import java.time.LocalDateTime;
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
+import java.util.Date;
|
|
|
|
+import java.util.List;
|
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
+
|
|
|
|
+@Service
|
|
|
|
+public class FileUpdateInfoServiceImpl implements FileUpdateInfoService {
|
|
|
|
+ private static final Logger logger = LoggerFactory.getLogger(FileUpdateInfoServiceImpl.class);
|
|
|
|
+
|
|
|
|
+ @Autowired
|
|
|
|
+ private FileUpdateInfoMapper fileUpdateInfoMapper;
|
|
|
|
+
|
|
|
|
+ @Value("${file.remote-url}")
|
|
|
|
+ private String remoteUrlBase;
|
|
|
|
+
|
|
|
|
+ // 从配置文件中读取脚本名称
|
|
|
|
+ @Value("${file.linuxrestart}")
|
|
|
|
+ private String linuxContralScript;
|
|
|
|
+
|
|
|
|
+ @Value("${file.windowsstop}")
|
|
|
|
+ private String windowsStopScript;
|
|
|
|
+
|
|
|
|
+ @Value("${file.windowsstart}")
|
|
|
|
+ private String windowsStartScript;
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ public void addFileToDatabase(FileUpdateInfo fileUpdateInfo) throws Exception {
|
|
|
|
+ // 假设前端只传入文件名
|
|
|
|
+ String fileName = fileUpdateInfo.getFileName();
|
|
|
|
+ if (fileName == null || fileName.trim().isEmpty()) {
|
|
|
|
+ throw new IllegalArgumentException("文件名不能为空");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 设置文件路径
|
|
|
|
+ String filePath = FileUpdateInfo.getServiceDir() + File.separator + fileUpdateInfo.getFileName();
|
|
|
|
+
|
|
|
|
+ File file = new File(filePath);
|
|
|
|
+
|
|
|
|
+ if (file.exists()) {
|
|
|
|
+ // 如果文件存在,计算本地文件的 MD5 值
|
|
|
|
+ String fileMd5 = DigestUtils.md5Hex(new FileInputStream(file));
|
|
|
|
+ fileUpdateInfo.setFileMd5(fileMd5);
|
|
|
|
+ fileUpdateInfo.setUpdateStatus(0);
|
|
|
|
+ } else {
|
|
|
|
+ // 如果文件不存在,设置状态为“待更新”
|
|
|
|
+ fileUpdateInfo.setFileMd5(null);
|
|
|
|
+ fileUpdateInfo.setUpdateStatus(1);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ fileUpdateInfo.setVersion("v1.0.0"); // 初始化版本号
|
|
|
|
+ fileUpdateInfo.setCreateTime(LocalDateTime.now());
|
|
|
|
+ fileUpdateInfo.setUpdateTime(null);
|
|
|
|
+ fileUpdateInfoMapper.insert(fileUpdateInfo);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //检查是否需要更新
|
|
|
|
+ @Override
|
|
|
|
+ public boolean checkFileUpdate(Long id) throws Exception {
|
|
|
|
+ FileUpdateInfo fileUpdateInfo = fileUpdateInfoMapper.selectById(id);
|
|
|
|
+ if (fileUpdateInfo == null) {
|
|
|
|
+ throw new FileNotFoundException("文件信息未找到:id=" + id);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ String localFilePath = fileUpdateInfo.getFilePath(); // 使用新的 getFilePath 方法
|
|
|
|
+ File localFile = new File(localFilePath);
|
|
|
|
+
|
|
|
|
+ String remoteFileMd5 = getRemoteFileMd5(fileUpdateInfo.getFileName());
|
|
|
|
+
|
|
|
|
+ if (!localFile.exists()) {
|
|
|
|
+ // 如果本地文件不存在,设置状态为“待更新”
|
|
|
|
+ fileUpdateInfo.setRemoteMd5(remoteFileMd5);
|
|
|
|
+ fileUpdateInfo.setUpdateStatus(1); // 设置为待更新
|
|
|
|
+ fileUpdateInfoMapper.updateById(fileUpdateInfo);
|
|
|
|
+ return true; // 需要更新
|
|
|
|
+ } else {
|
|
|
|
+ // 如果本地文件存在,比较 MD5 值
|
|
|
|
+ String localFileMd5 = DigestUtils.md5Hex(new FileInputStream(localFile));
|
|
|
|
+ boolean isUpdateRequired = !localFileMd5.equals(remoteFileMd5);
|
|
|
|
+ fileUpdateInfo.setRemoteMd5(remoteFileMd5);
|
|
|
|
+ fileUpdateInfo.setUpdateStatus(isUpdateRequired ? 1 : 0); // 0表示无需更新,1表示待更新
|
|
|
|
+ fileUpdateInfoMapper.updateById(fileUpdateInfo);
|
|
|
|
+ return isUpdateRequired;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //下载更新
|
|
|
|
+ @Override
|
|
|
|
+ public void performFileUpdate(Long id) throws Exception {
|
|
|
|
+ FileUpdateInfo fileUpdateInfo = fileUpdateInfoMapper.selectById(id);
|
|
|
|
+ if (fileUpdateInfo == null) {
|
|
|
|
+ throw new FileNotFoundException("文件信息未找到:id=" + id);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ String localFilePath = fileUpdateInfo.getFilePath(); // 使用新的 getFilePath 方法
|
|
|
|
+ String fileName = fileUpdateInfo.getFileName();
|
|
|
|
+ String remoteUrl = remoteUrlBase + fileName;
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ // 获取 JAR 文件所在的目录
|
|
|
|
+ String serviceDir = FileUpdateInfo.getServiceDir();
|
|
|
|
+ String backupDirPath = serviceDir + File.separator + "backup";
|
|
|
|
+ File backupDir = new File(backupDirPath);
|
|
|
|
+ if (!backupDir.exists()) {
|
|
|
|
+ boolean created = backupDir.mkdirs();
|
|
|
|
+ if (!created) {
|
|
|
|
+ throw new IOException("无法创建备份目录: " + backupDirPath);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ File originalFile = new File(localFilePath);
|
|
|
|
+ if (originalFile.exists()) {
|
|
|
|
+ String timestamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
|
|
|
|
+ String backupFilePath = backupDirPath + File.separator + fileName + "." + timestamp;
|
|
|
|
+ Files.move(originalFile.toPath(), Paths.get(backupFilePath), StandardCopyOption.REPLACE_EXISTING);
|
|
|
|
+ System.out.println("旧文件已备份到: " + backupFilePath);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ downloadFileFromRemote(remoteUrl, localFilePath);
|
|
|
|
+
|
|
|
|
+ String newFileMd5 = DigestUtils.md5Hex(new FileInputStream(localFilePath));
|
|
|
|
+ fileUpdateInfo.setFileMd5(newFileMd5);
|
|
|
|
+ fileUpdateInfo.setCreateTime(fileUpdateInfo.getCreateTime());
|
|
|
|
+ fileUpdateInfo.setUpdateTime(LocalDateTime.now());
|
|
|
|
+ fileUpdateInfo.setUpdateStatus(0);
|
|
|
|
+
|
|
|
|
+ // 递增版本号
|
|
|
|
+ String currentVersion = fileUpdateInfo.getVersion();
|
|
|
|
+ String[] versionParts = currentVersion.substring(1).split("\\."); // 去掉 'v' 并拆分版本号
|
|
|
|
+ int major = Integer.parseInt(versionParts[0]);
|
|
|
|
+ int minor = Integer.parseInt(versionParts[1]);
|
|
|
|
+ int patch = Integer.parseInt(versionParts[2]) + 1; // 小版本号加 1
|
|
|
|
+ fileUpdateInfo.setVersion("v" + major + "." + minor + "." + patch);
|
|
|
|
+
|
|
|
|
+ fileUpdateInfoMapper.updateById(fileUpdateInfo);
|
|
|
|
+
|
|
|
|
+ // 下载完成后重启服务
|
|
|
|
+ controlApplication(fileName, "restart");
|
|
|
|
+
|
|
|
|
+ } catch (FileNotFoundException e) {
|
|
|
|
+ throw new Exception("文件未找到: " + localFilePath + ",原因: " + e.getMessage(), e);
|
|
|
|
+ } catch (IOException e) {
|
|
|
|
+ throw new Exception("文件写入失败: " + localFilePath + ",原因: " + e.getMessage(), e);
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
+ throw new Exception("文件更新失败: " + localFilePath + ",原因: " + e.getMessage(), e);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private void downloadFileFromRemote(String remoteUrl, String localFilePath) throws Exception {
|
|
|
|
+ URL url = new URL(remoteUrl);
|
|
|
|
+ Path path = Paths.get(localFilePath);
|
|
|
|
+ try {
|
|
|
|
+ Files.copy(url.openStream(), path, StandardCopyOption.REPLACE_EXISTING);
|
|
|
|
+ System.out.println("文件下载成功: " + localFilePath);
|
|
|
|
+ } catch (IOException e) {
|
|
|
|
+ System.err.println("文件下载失败: " + localFilePath + ",原因: " + e.getMessage());
|
|
|
|
+ throw new Exception("文件下载失败: " + localFilePath, e);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private String getRemoteFileMd5(String fileName) throws Exception {
|
|
|
|
+ String remoteUrl = remoteUrlBase + fileName;
|
|
|
|
+ System.out.println("远程文件 URL: " + remoteUrl);
|
|
|
|
+ URL url = new URL(remoteUrl);
|
|
|
|
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
|
|
|
+ connection.setRequestProperty("Cache-Control", "no-cache");
|
|
|
|
+ try (InputStream in = connection.getInputStream()) {
|
|
|
|
+ return DigestUtils.md5Hex(in);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ public CommonPage<FileUpdateInfo> getAllFiles(int current, int size) throws Exception {
|
|
|
|
+ // 使用 MyBatis-Plus 的分页功能
|
|
|
|
+ Page<FileUpdateInfo> page = new Page<>(current, size);
|
|
|
|
+ IPage<FileUpdateInfo> result = fileUpdateInfoMapper.selectPage(page, null);
|
|
|
|
+
|
|
|
|
+ // 更新每个文件的运行状态
|
|
|
|
+ for (FileUpdateInfo fileUpdateInfo : result.getRecords()) {
|
|
|
|
+ checkServiceStatus(fileUpdateInfo);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 将 MyBatis-Plus 的分页结果转换为 CommonPage
|
|
|
|
+ return new CommonPage<>(
|
|
|
|
+ result.getRecords(),
|
|
|
|
+ (int) result.getTotal(),
|
|
|
|
+ size,
|
|
|
|
+ current
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ public CommonPage<FileUpdateInfo> getFilesByFileNameContaining(String fileName, int current, int size) throws Exception {
|
|
|
|
+ // 使用 MyBatis-Plus 的分页功能
|
|
|
|
+ Page<FileUpdateInfo> page = new Page<>(current, size);
|
|
|
|
+ QueryWrapper<FileUpdateInfo> queryWrapper = new QueryWrapper<>();
|
|
|
|
+ queryWrapper.like("file_name", fileName);
|
|
|
|
+ IPage<FileUpdateInfo> result = fileUpdateInfoMapper.selectPage(page, queryWrapper);
|
|
|
|
+
|
|
|
|
+ // 更新每个文件的运行状态
|
|
|
|
+ for (FileUpdateInfo fileUpdateInfo : result.getRecords()) {
|
|
|
|
+ checkServiceStatus(fileUpdateInfo);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 将 MyBatis-Plus 的分页结果转换为 CommonPage
|
|
|
|
+ return new CommonPage<>(
|
|
|
|
+ result.getRecords(),
|
|
|
|
+ (int) result.getTotal(),
|
|
|
|
+ size,
|
|
|
|
+ current
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ public FileUpdateInfo getFileById(Long id) {
|
|
|
|
+ return fileUpdateInfoMapper.selectById(id);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ public void deleteFileById(Long id) {
|
|
|
|
+ fileUpdateInfoMapper.deleteById(id);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ public void controlApplication(String fileName, String action) throws Exception {
|
|
|
|
+ String osName = System.getProperty("os.name").toLowerCase();
|
|
|
|
+
|
|
|
|
+ if (osName.contains("linux") || osName.contains("unix")) {
|
|
|
|
+ // Linux 系统
|
|
|
|
+ String command = "sh " + linuxContralScript + " " + action + " " + fileName;
|
|
|
|
+ Runtime.getRuntime().exec(command);
|
|
|
|
+ } else if (osName.contains("windows")) {
|
|
|
|
+ // Windows 系统
|
|
|
|
+ String command;
|
|
|
|
+ switch (action.toLowerCase()) {
|
|
|
|
+ case "start":
|
|
|
|
+ command = windowsStartScript + " " + fileName;
|
|
|
|
+ break;
|
|
|
|
+ case "stop":
|
|
|
|
+ command = windowsStopScript + " " + fileName;
|
|
|
|
+ break;
|
|
|
|
+ case "restart":
|
|
|
|
+ // Windows 系统没有单独的重启脚本,需要先停止再启动
|
|
|
|
+ Runtime.getRuntime().exec(windowsStopScript + " " + fileName);
|
|
|
|
+ Thread.sleep(3000); // 等待 3 秒,确保服务停止
|
|
|
|
+ Runtime.getRuntime().exec(windowsStartScript + " " + fileName);
|
|
|
|
+ return;
|
|
|
|
+ default:
|
|
|
|
+ throw new IllegalArgumentException("不支持的操作类型: " + action);
|
|
|
|
+ }
|
|
|
|
+ Runtime.getRuntime().exec(command);
|
|
|
|
+ } else {
|
|
|
|
+ throw new Exception("不支持的操作系统: " + osName);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 等待操作完成
|
|
|
|
+ long startTime = System.currentTimeMillis();
|
|
|
|
+ long timeout = 8000; // 最长等待时间 8 秒
|
|
|
|
+ boolean isActionCompleted = false;
|
|
|
|
+
|
|
|
|
+ while (System.currentTimeMillis() - startTime < timeout) {
|
|
|
|
+ int currentStatus = getFileStatus(fileName); // 获取当前服务状态
|
|
|
|
+ if (action.equals("start") && currentStatus == 1) {
|
|
|
|
+ isActionCompleted = true;
|
|
|
|
+ break; // 服务已启动
|
|
|
|
+ } else if (action.equals("stop") && currentStatus == 2) {
|
|
|
|
+ isActionCompleted = true;
|
|
|
|
+ break; // 服务已停止
|
|
|
|
+ } else if (action.equals("restart") && currentStatus == 1) {
|
|
|
|
+ isActionCompleted = true;
|
|
|
|
+ break; // 服务已重启
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ Thread.sleep(500); // 每隔 500 毫秒检测一次状态
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (!isActionCompleted) {
|
|
|
|
+ throw new Exception("操作超时,服务状态未达到预期");
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //扫描服务所在目录下的文件
|
|
|
|
+ @Override
|
|
|
|
+ public CommonPage<FileWithId> scanFilesInServiceDir(int current, int size) throws Exception {
|
|
|
|
+ // 获取 JAR 文件所在的目录
|
|
|
|
+ String serviceDir = FileUpdateInfo.getServiceDir();
|
|
|
|
+ File dir = new File(serviceDir);
|
|
|
|
+
|
|
|
|
+ if (!dir.exists() || !dir.isDirectory()) {
|
|
|
|
+ throw new Exception("无法找到服务目录: " + serviceDir);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 获取目录中的所有文件名
|
|
|
|
+ File[] files = dir.listFiles();
|
|
|
|
+ if (files == null) {
|
|
|
|
+ throw new Exception("无法读取目录内容: " + serviceDir);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 提取文件名
|
|
|
|
+ List<String> fileNames = new ArrayList<>();
|
|
|
|
+ for (File file : files) {
|
|
|
|
+ if (file.isFile()) {
|
|
|
|
+ fileNames.add(file.getName());
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 查询数据库中已存在的文件名
|
|
|
|
+ List<String> existingFileNames = fileUpdateInfoMapper.selectList(null).stream()
|
|
|
|
+ .map(FileUpdateInfo::getFileName)
|
|
|
|
+ .collect(Collectors.toList());
|
|
|
|
+
|
|
|
|
+ // 从扫描结果中排除已存在的文件名
|
|
|
|
+ List<String> newFileNames = fileNames.stream()
|
|
|
|
+ .filter(fileName -> !existingFileNames.contains(fileName))
|
|
|
|
+ .collect(Collectors.toList());
|
|
|
|
+
|
|
|
|
+ // 对文件名列表进行分页处理
|
|
|
|
+ int total = newFileNames.size();
|
|
|
|
+ int start = (current - 1) * size;
|
|
|
|
+ int end = Math.min(start + size, total);
|
|
|
|
+
|
|
|
|
+ List<String> pageFileNames = newFileNames.subList(start, end);
|
|
|
|
+
|
|
|
|
+ // 创建包含 id 的文件信息列表
|
|
|
|
+ List<FileWithId> fileWithIds = new ArrayList<>();
|
|
|
|
+ for (int i = 0; i < pageFileNames.size(); i++) {
|
|
|
|
+ fileWithIds.add(new FileWithId((long) (start + i + 1), pageFileNames.get(i)));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 返回分页结果
|
|
|
|
+ return new CommonPage<>(fileWithIds, total, size, current);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ // 检测服务运行状态并更新到数据库
|
|
|
|
+ private void checkServiceStatus(FileUpdateInfo fileUpdateInfo) {
|
|
|
|
+ String fileName = fileUpdateInfo.getFileName();
|
|
|
|
+ int fileStatus = getFileStatus(fileName); // 调用方法获取文件运行状态
|
|
|
|
+ fileUpdateInfo.setFileStatus(fileStatus); // 更新运行状态
|
|
|
|
+ fileUpdateInfoMapper.updateById(fileUpdateInfo); // 更新数据库
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 获取文件运行状态
|
|
|
|
+ @Override
|
|
|
|
+ public int getFileStatus(String fileName) {
|
|
|
|
+ String osName = System.getProperty("os.name").toLowerCase();
|
|
|
|
+
|
|
|
|
+ if (osName.contains("linux") || osName.contains("unix")) {
|
|
|
|
+ // Linux 系统
|
|
|
|
+ String command = "pgrep -f " + fileName;
|
|
|
|
+ try {
|
|
|
|
+ Process process = Runtime.getRuntime().exec(command);
|
|
|
|
+ BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
|
|
|
+ String line;
|
|
|
|
+ boolean isRunning = false;
|
|
|
|
+ while ((line = reader.readLine()) != null) {
|
|
|
|
+ if (line != null && !line.isEmpty()) {
|
|
|
|
+ isRunning = true;
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ return isRunning ? 1 : 2; // 1: 运行中,2: 已停止
|
|
|
|
+ } catch (IOException e) {
|
|
|
|
+ logger.error("Failed to execute command: {}", command, e);
|
|
|
|
+ }
|
|
|
|
+ } else if (osName.contains("windows")) {
|
|
|
|
+ // Windows 系统
|
|
|
|
+ String command = "sc query " + fileName;
|
|
|
|
+ try {
|
|
|
|
+ Process process = Runtime.getRuntime().exec(command);
|
|
|
|
+ BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
|
|
|
+ String line;
|
|
|
|
+ boolean isRunning = false;
|
|
|
|
+ while ((line = reader.readLine()) != null) {
|
|
|
|
+ if (line.contains("RUNNING")) {
|
|
|
|
+ isRunning = true;
|
|
|
|
+ break;
|
|
|
|
+ } else if (line.contains("STOPPED")) {
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ return isRunning ? 1 : 2; // 1: 运行中,2: 已停止
|
|
|
|
+ } catch (IOException e) {
|
|
|
|
+ logger.error("Failed to execute command: {}", command, e);
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ logger.warn("Unsupported operating system: {}", osName);
|
|
|
|
+ }
|
|
|
|
+ return 0; // 未知
|
|
|
|
+ }
|
|
|
|
+}
|