From 23e187554324e1bdff896d7d18634711a7372755 Mon Sep 17 00:00:00 2001
From: fuliqi <fuliqi@qq.com>
Date: 星期四, 12 十二月 2024 17:56:58 +0800
Subject: [PATCH] 项目库上传后端逻辑
---
common/src/main/java/com/ycl/common/utils/file/FileUploadUtils.java | 41 ++++++
common/src/main/java/com/ycl/common/utils/poi/ExcelUtil.java | 18 +++
business/src/main/java/com/ycl/listener/excel/ProjectImportListener.java | 142 +++++++++++++++++++++++
business/src/main/java/com/ycl/controller/ProjectInfoController.java | 16 ++
business/src/main/java/com/ycl/domain/entity/File.java | 2
business/src/main/java/com/ycl/service/impl/ProjectInfoServiceImpl.java | 130 +++++++++++++++++++++
business/src/main/java/com/ycl/service/ProjectInfoService.java | 3
7 files changed, 351 insertions(+), 1 deletions(-)
diff --git a/business/src/main/java/com/ycl/controller/ProjectInfoController.java b/business/src/main/java/com/ycl/controller/ProjectInfoController.java
index 963dfee..375f5ad 100644
--- a/business/src/main/java/com/ycl/controller/ProjectInfoController.java
+++ b/business/src/main/java/com/ycl/controller/ProjectInfoController.java
@@ -1,6 +1,7 @@
package com.ycl.controller;
import com.ycl.common.base.Result;
+import com.ycl.common.exception.base.BaseException;
import com.ycl.common.group.Add;
import com.ycl.common.group.Update;
import com.ycl.common.utils.ProjectCodeGenerator;
@@ -16,6 +17,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotEmpty;
@@ -147,4 +149,18 @@
public Result updateUsedStatus(@PathVariable Integer id, @PathVariable Integer usedStatus) {
return projectInfoService.updateUsedStatus(id, usedStatus);
}
+
+ /**
+ * 椤圭洰瀵煎叆
+ * @param file
+ * @return
+ */
+ @PostMapping("/import")
+ public Result importProject(@RequestPart("file") MultipartFile file) {
+ if (file.getSize() > 100 * 1024 * 1024) {
+ throw new BaseException("鏂囦欢杩囧ぇ锛屾枃浠朵笉寰楄秴杩�100MB");
+ }
+ projectInfoService.importProject(file);
+ return Result.ok();
+ }
}
diff --git a/business/src/main/java/com/ycl/domain/entity/File.java b/business/src/main/java/com/ycl/domain/entity/File.java
index 806883c..718b9af 100644
--- a/business/src/main/java/com/ycl/domain/entity/File.java
+++ b/business/src/main/java/com/ycl/domain/entity/File.java
@@ -4,6 +4,7 @@
import com.ycl.common.enums.business.FileTypeEnum;
import com.ycl.system.domain.base.AbsEntity;
import lombok.Data;
+import lombok.experimental.Accessors;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@@ -16,6 +17,7 @@
*/
@Data
@TableName("t_file")
+@Accessors(chain = true)
public class File extends AbsEntity {
private static final long serialVersionUID = 1L;
diff --git a/business/src/main/java/com/ycl/listener/excel/ProjectImportListener.java b/business/src/main/java/com/ycl/listener/excel/ProjectImportListener.java
new file mode 100644
index 0000000..b535353
--- /dev/null
+++ b/business/src/main/java/com/ycl/listener/excel/ProjectImportListener.java
@@ -0,0 +1,142 @@
+package com.ycl.listener.excel;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.excel.context.AnalysisContext;
+import com.alibaba.excel.event.AnalysisEventListener;
+import com.ycl.common.exception.ServiceException;
+import com.ycl.common.exception.base.BaseException;
+import com.ycl.common.utils.excel.core.ExcelListener;
+import com.ycl.common.utils.excel.core.ExcelResult;
+import com.ycl.common.utils.spring.SpringUtils;
+import com.ycl.domain.entity.*;
+import com.ycl.domain.excel.ProjectExcelTemplate;
+import com.ycl.service.ProjectInvestmentFundingService;
+import com.ycl.service.ProjectInvestmentInfoService;
+import com.ycl.service.ProjectInvestmentPolicyComplianceService;
+import com.ycl.service.ProjectUnitRegistrationInfoService;
+import com.ycl.service.impl.ProjectInfoServiceImpl;
+import lombok.Data;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Data
+//@NoArgsConstructor
+public class ProjectImportListener extends AnalysisEventListener<ProjectExcelTemplate> implements ExcelListener<ProjectExcelTemplate> {
+
+
+ private final ProjectInfoServiceImpl projectInfoService;
+ private final ProjectInvestmentInfoService projectInvestmentInfoService;
+ private final ProjectInvestmentFundingService projectInvestmentFundingService;
+ private final ProjectUnitRegistrationInfoService projectUnitRegistrationInfoService;
+ private final ProjectInvestmentPolicyComplianceService projectPolicyComplianceService;
+
+ private int successNum = 0;
+ private int failureNum = 0;
+ private final StringBuilder successMsg = new StringBuilder();
+ private final StringBuilder failureMsg = new StringBuilder();
+ private boolean hasData = false;
+
+ private Map<String, Long> projectInfoMap = new HashMap<>();
+ private Map<String, Long> investmentProjectPolicyComplianceMap = new HashMap<>();
+ private Map<String, Long> documentsMap = new HashMap<>();
+
+
+ public ProjectImportListener() {
+ this.projectInfoService = SpringUtils.getBean(ProjectInfoServiceImpl.class);
+ this.projectInvestmentInfoService = SpringUtils.getBean(ProjectInvestmentInfoService.class);
+ this.projectInvestmentFundingService = SpringUtils.getBean(ProjectInvestmentFundingService.class);
+ this.projectUnitRegistrationInfoService = SpringUtils.getBean(ProjectUnitRegistrationInfoService.class);
+ this.projectPolicyComplianceService = SpringUtils.getBean(ProjectInvestmentPolicyComplianceService.class);
+
+ }
+
+ @Override
+ public ExcelResult<ProjectExcelTemplate> getExcelResult() {
+ return new ExcelResult<>() {
+
+ @Override
+ public String getAnalysis() {
+ if (failureNum > 0) {
+ failureMsg.insert(0, "寰堟姳姝夛紝瀵煎叆澶辫触锛佸叡 " + failureNum + " 鏉℃暟鎹牸寮忎笉姝g‘锛岄敊璇涓嬶細");
+ throw new ServiceException(failureMsg.toString());
+ } else {
+ successMsg.insert(0, "鎭枩鎮紝鏁版嵁宸插叏閮ㄥ鍏ユ垚鍔燂紒鍏� " + successNum + " 鏉★紝鏁版嵁濡備笅锛�");
+ }
+ return successMsg.toString();
+ }
+
+ @Override
+ public List<ProjectExcelTemplate> getList() {
+ return null;
+ }
+
+ @Override
+ public List<String> getErrorList() {
+ return null;
+ }
+ };
+ }
+
+ @Override
+ public void invoke(ProjectExcelTemplate projectExcelTemplate, AnalysisContext analysisContext) {
+ hasData = true;
+ //澶勭悊椤圭洰鍩虹淇℃伅
+ ProjectInfo projectInfo = BeanUtil.toBean(projectExcelTemplate, ProjectInfo.class);
+ projectInfoService.checkProjectNameUnique(projectInfo);
+ projectInfoService.save(projectInfo);
+ //瀹℃壒璁″垝涔�
+ if (StrUtil.isNotBlank(projectExcelTemplate.getApprovalPlan())) {
+ String[] names = projectExcelTemplate.getApprovalPlan().split(",");
+ for (String name : names) {
+ projectInfoMap.put(name, projectInfo.getId());
+ }
+ }
+
+ //澶勭悊鎶曡祫绠$悊淇℃伅
+ ProjectInvestmentInfo projectInvestmentInfo = BeanUtil.toBean(projectExcelTemplate, ProjectInvestmentInfo.class);
+ projectInvestmentInfo.setProjectId(projectInfo.getId());
+ projectInvestmentInfoService.save(projectInvestmentInfo);
+
+ //澶勭悊椤圭洰鎶曡祫鍙婅祫閲戞潵婧�
+ ProjectInvestmentFunding projectInvestmentFunding = BeanUtil.toBean(projectExcelTemplate, ProjectInvestmentFunding.class);
+ projectInvestmentFunding.setProjectId(projectInfo.getId());
+ projectInvestmentFundingService.save(projectInvestmentFunding);
+
+ // 澶勭悊椤圭洰娉曚汉鍗曚綅淇℃伅
+ ProjectUnitRegistrationInfo projectUnitRegistrationInfo = BeanUtil.toBean(projectExcelTemplate, ProjectUnitRegistrationInfo.class);
+ projectUnitRegistrationInfo.setProjectId(projectInfo.getId());
+ projectUnitRegistrationInfoService.save(projectUnitRegistrationInfo);
+
+ //澶勭悊鎶曡祫浜т笟鏀跨瓥绗﹀悎鎯呭喌
+ ProjectInvestmentPolicyCompliance investmentProjectPolicyCompliance = BeanUtil.toBean(projectExcelTemplate, ProjectInvestmentPolicyCompliance.class);
+ investmentProjectPolicyCompliance.setProjectId(projectInfo.getId());
+ projectPolicyComplianceService.save(investmentProjectPolicyCompliance);
+ //绗﹀悎鏀跨瓥闄勪欢
+ if (StrUtil.isNotBlank(projectExcelTemplate.getPolicyComplianceAttachment())) {
+ String[] names2 = projectExcelTemplate.getPolicyComplianceAttachment().split(",");
+ for (String name : names2) {
+ investmentProjectPolicyComplianceMap.put(name, investmentProjectPolicyCompliance.getId());
+ }
+ }
+
+ // 鐩稿叧鏂囦功
+ if (StrUtil.isNotBlank(projectExcelTemplate.getDocuments())) {
+ String[] documentNames = projectExcelTemplate.getDocuments().split(",");
+ for (String name : documentNames) {
+ documentsMap.put(name, projectInfo.getId());
+ }
+ }
+ }
+
+ @Override
+ public void doAfterAllAnalysed(AnalysisContext analysisContext) {
+ if (!hasData) {
+ throw new BaseException("excel涓虹┖锛岃濉叆鏁版嵁鍚庡鍏�");
+ }
+ }
+
+
+}
diff --git a/business/src/main/java/com/ycl/service/ProjectInfoService.java b/business/src/main/java/com/ycl/service/ProjectInfoService.java
index 41fac95..5eee464 100644
--- a/business/src/main/java/com/ycl/service/ProjectInfoService.java
+++ b/business/src/main/java/com/ycl/service/ProjectInfoService.java
@@ -9,6 +9,7 @@
import com.ycl.domain.query.ProjectInfoQuery;
import com.ycl.domain.vo.IndexCountVO;
import com.ycl.domain.vo.IndexDTO;
+import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@@ -84,4 +85,6 @@
void export(HttpServletResponse response, ProjectExportQuery query) throws IOException;
Result updateUsedStatus(Integer id, Integer usedStatus);
+
+ void importProject(MultipartFile file);
}
diff --git a/business/src/main/java/com/ycl/service/impl/ProjectInfoServiceImpl.java b/business/src/main/java/com/ycl/service/impl/ProjectInfoServiceImpl.java
index afe0041..0dd853d 100644
--- a/business/src/main/java/com/ycl/service/impl/ProjectInfoServiceImpl.java
+++ b/business/src/main/java/com/ycl/service/impl/ProjectInfoServiceImpl.java
@@ -2,11 +2,13 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.ZipUtil;
import com.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ycl.common.base.Result;
import com.ycl.common.config.SysConfig;
@@ -21,6 +23,9 @@
import com.ycl.common.utils.StringUtils;
import com.ycl.common.utils.bean.BeanUtils;
import com.ycl.common.utils.excel.OutputExcelUtils;
+import com.ycl.common.utils.file.FileUploadUtils;
+import com.ycl.common.utils.file.FileUtils;
+import com.ycl.common.utils.poi.ExcelUtil;
import com.ycl.domain.entity.*;
import com.ycl.domain.excel.ProjectExcelTemplate;
import com.ycl.domain.form.DocumentInfoForm;
@@ -30,6 +35,7 @@
import com.ycl.domain.query.ProjectInfoQuery;
import com.ycl.domain.vo.*;
import com.ycl.framework.utils.PageUtil;
+import com.ycl.listener.excel.ProjectImportListener;
import com.ycl.mapper.*;
import com.ycl.service.FileService;
import com.ycl.service.ProjectInfoService;
@@ -38,20 +44,25 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
+import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
+import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
+import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import java.util.zip.ZipOutputStream;
/**
@@ -546,4 +557,123 @@
baseMapper.updateById(entity);
return Result.ok("鍒犻櫎鎴愬姛");
}
+
+ @Transactional
+ @Override
+ public void importProject(MultipartFile file) {
+ java.io.File tempZip = null;
+ java.io.File tempDir = null;
+ try {
+ tempZip = java.io.File.createTempFile("temp", ".zip");
+ try(InputStream inputStream = file.getInputStream()) {
+ Files.copy(inputStream, tempZip.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ }
+
+ tempDir = Files.createTempDirectory("temp-dir").toFile();
+ ZipUtil.unzip(tempZip, tempDir, StandardCharsets.ISO_8859_1);
+
+ Path path = tempDir.toPath();
+ ProjectImportListener projectImportListener = new ProjectImportListener();
+ //闇�瑕佸瓨鍏ユ暟鎹簱
+ ArrayList<File> attachments = new ArrayList<>();
+ //瀹為檯鐨勬枃浠�
+ List<java.io.File> files = new ArrayList<>();
+ try (Stream<Path> pathStream = Files.walk(path)) {
+ pathStream.forEach(filePath -> {
+ if (Files.isDirectory(filePath) && filePath.getFileName().toString().equals("attachment")) {
+ //闄勪欢澶勭悊
+ try (Stream<Path> stream = Files.walk(filePath)) {
+ stream.forEach(attachmentPath -> {
+ if (Files.isRegularFile(attachmentPath)) {
+ files.add(attachmentPath.toFile());
+ }
+ });
+ } catch (IOException e) {
+ log.error(e.getMessage());
+ throw new RuntimeException(e);
+ }
+ } else if (Files.isRegularFile(filePath) && filePath.getFileName().toString().equals("excel.xlsx")) {
+ //瀵煎叆鏁版嵁澶勭悊
+ try (FileInputStream fileInputStream = new FileInputStream(filePath.toFile())) {
+ ExcelUtil.importExcel(fileInputStream, ProjectExcelTemplate.class, projectImportListener);
+ } catch (IOException e) {
+ log.error(e.getMessage());
+ throw new RuntimeException(e);
+ }
+ }
+ });
+ }
+ /* 鎶婇檮浠跺瓨鍏ヤ笂浼犺矾寰� */
+ //涓婁紶鏂囦欢璺緞
+ String filePath = SysConfig.getUploadPath();
+ files.forEach(attachmentFile -> {
+ try {
+ //涓婁紶
+ String url = FileUploadUtils.upload(filePath, file);
+ //瀛樻斁鐨勬枃浠跺悕浼氬姞涓奯鏃堕棿鎴崇殑鍚庣紑
+ String newName = FileUtils.getName(url);
+ attachments.add(new File()
+ .setUrl(url)
+ .setName(newName)
+ .setOriginalName(attachmentFile.getName()));
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ });
+
+ Map<String, Long> projectInfoMap = projectImportListener.getProjectInfoMap();
+ Map<String, Long> investmentProjectPolicyComplianceMap = projectImportListener.getInvestmentProjectPolicyComplianceMap();
+ Map<String, Long> documentsMap = projectImportListener.getDocumentsMap();
+ //琛ュ厖type鍜宐usId瀛楁
+ attachments.forEach(attachment -> {
+ String fileName = attachment.getOriginalName();
+ if (ObjectUtil.isNotNull(projectInfoMap.get(fileName))) {
+ attachment.setBusId(projectInfoMap.get(fileName));
+ attachment.setType(FileTypeEnum.PROJECT_INFO);
+ } else if (ObjectUtil.isNotNull(investmentProjectPolicyComplianceMap.get(fileName))) {
+ attachment.setBusId(investmentProjectPolicyComplianceMap.get(fileName));
+ attachment.setType(FileTypeEnum.INVEST_POLICY);
+ } else if (ObjectUtil.isNotNull(documentsMap.get(fileName))) {
+ attachment.setBusId(documentsMap.get(fileName));
+ attachment.setType(FileTypeEnum.DOCUMENT_INFO);
+ }
+ });
+
+ // 娣诲姞椤圭洰闄勪欢鍏宠仈琛�
+ fileService.saveBatch(attachments);
+ } catch (IOException e) {
+ log.error(e.getMessage());
+ throw new RuntimeException(e);
+ } finally {
+ deleteDirectoryOrFile(tempZip);
+ deleteDirectoryOrFile(tempDir);
+ }
+ }
+
+ private static void deleteDirectoryOrFile(java.io.File file) {
+ if (ObjectUtil.isNull(file)) {
+ return;
+ }
+
+ if (file.isDirectory()) {
+ java.io.File[] files = file.listFiles();
+ if (files != null) {
+ for (java.io.File f : files) {
+ deleteDirectoryOrFile(f);
+ }
+ }
+ }
+ file.delete();
+ }
+ /**
+ * 椤圭洰鍚嶇О鏍¢獙閲嶅
+ * @return
+ */
+ public void checkProjectNameUnique(ProjectInfo projectInfo) {
+ if (StrUtil.isBlank(projectInfo.getProjectName())) {
+ throw new BaseException("椤圭洰鍚嶇О涓嶈兘涓虹┖");
+ }
+ Assert.isTrue(baseMapper.selectCount(Wrappers.<ProjectInfo>lambdaQuery().eq(ProjectInfo::getProjectName, projectInfo.getProjectName())) == 0, "椤圭洰鍚嶇О閲嶅");
+ }
+
}
diff --git a/common/src/main/java/com/ycl/common/utils/file/FileUploadUtils.java b/common/src/main/java/com/ycl/common/utils/file/FileUploadUtils.java
index bab638f..586f5b5 100644
--- a/common/src/main/java/com/ycl/common/utils/file/FileUploadUtils.java
+++ b/common/src/main/java/com/ycl/common/utils/file/FileUploadUtils.java
@@ -2,7 +2,9 @@
import java.io.File;
import java.io.IOException;
+import java.nio.file.Files;
import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
import java.util.Objects;
import org.apache.commons.io.FilenameUtils;
import org.springframework.web.multipart.MultipartFile;
@@ -85,7 +87,44 @@
throw new IOException(e.getMessage(), e);
}
}
-
+ /**
+ * 鏂囦欢涓婁紶
+ *
+ * @param baseDir 鐩稿搴旂敤鐨勫熀鐩綍
+ * @param file 涓婁紶鐨勬枃浠�
+ * @return 杩斿洖涓婁紶鎴愬姛鐨勬枃浠跺悕
+ * @throws FileSizeLimitExceededException 濡傛灉瓒呭嚭鏈�澶уぇ灏�
+ * @throws FileNameLengthLimitExceededException 鏂囦欢鍚嶅お闀�
+ * @throws IOException 姣斿璇诲啓鏂囦欢鍑洪敊鏃�
+ * @throws InvalidExtensionException 鏂囦欢鏍¢獙寮傚父
+ */
+ public static final String uploadIOFile(String baseDir, java.io.File file)
+ throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException
+ {
+ int fileNamelength = Objects.requireNonNull(file.getName()).length();
+ if (fileNamelength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH)
+ {
+ throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH);
+ }
+ //涓婁紶IOFile鏂囦欢閫昏緫
+ String fileName = file.getName();
+ //甯︽棩鏈熷墠缂�鐨勬枃浠跺悕
+ String formatName = StringUtils.format("{}/{}_{}.{}", DateUtils.datePath(),
+ FilenameUtils.getBaseName(fileName), Seq.getId(Seq.uploadSeqType), fileName.substring(fileName.lastIndexOf(".") + 1));
+ // /home/projectManagement/uploadPath/upload/2024/11/11/xxxx
+ String url = baseDir + File.separator + formatName;
+ File newFile = new File(url);
+ if (!newFile.exists())
+ {
+ if (!newFile.getParentFile().exists())
+ {
+ newFile.getParentFile().mkdirs();
+ }
+ }
+ //澶嶅埗鏂囦欢鍒颁笂浼犺矾寰�
+ Files.copy(file.toPath(), newFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ return getPathFileName(baseDir, formatName);
+ }
/**
* 鏂囦欢涓婁紶
*
diff --git a/common/src/main/java/com/ycl/common/utils/poi/ExcelUtil.java b/common/src/main/java/com/ycl/common/utils/poi/ExcelUtil.java
index 4a13764..6347658 100644
--- a/common/src/main/java/com/ycl/common/utils/poi/ExcelUtil.java
+++ b/common/src/main/java/com/ycl/common/utils/poi/ExcelUtil.java
@@ -24,6 +24,10 @@
import java.util.UUID;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse;
+
+import com.alibaba.excel.EasyExcel;
+import com.ycl.common.utils.excel.core.ExcelListener;
+import com.ycl.common.utils.excel.core.ExcelResult;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.RegExUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
@@ -500,6 +504,20 @@
}
/**
+ * 浣跨敤鑷畾涔夌洃鍚櫒 寮傛瀵煎叆 鑷畾涔夎繑鍥�
+ *
+ * @param is 杈撳叆娴�
+ * @param clazz 瀵硅薄绫诲瀷
+ * @param listener 鑷畾涔夌洃鍚櫒
+ * @return 杞崲鍚庨泦鍚�
+ */
+ public static <T> ExcelResult<T> importExcel(InputStream is, Class<T> clazz, ExcelListener<T> listener) {
+ EasyExcel.read(is, clazz, listener).sheet().doRead();
+ return listener.getExcelResult();
+ }
+
+
+ /**
* 瀵筶ist鏁版嵁婧愬皢鍏堕噷闈㈢殑鏁版嵁瀵煎叆鍒癳xcel琛ㄥ崟
*
* @param list 瀵煎嚭鏁版嵁闆嗗悎
--
Gitblit v1.8.0