feat: add dashboard and virtual pay enhancements
This commit is contained in:
@@ -28,6 +28,18 @@
|
||||
<artifactId>ruoyi-system</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.13.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
</project>
|
||||
|
||||
@@ -174,10 +174,13 @@ public class TtCopyTemplateController extends BaseController {
|
||||
content = content.replace("{projectName}", projectName);
|
||||
content = content.replace("{codeDesc}", plainDesc);
|
||||
content = content.replace("{codeEnvironment}", code.getCodeEnvironment() != null ? code.getCodeEnvironment() : "");
|
||||
content = content.replace("{frontendTechnology}", code.getFrontendTechnology() != null ? code.getFrontendTechnology() : "");
|
||||
content = content.replace("{backendTechnology}", code.getBackendTechnology() != null ? code.getBackendTechnology() : "");
|
||||
content = content.replace("{databaseTechnology}", code.getDatabaseTechnology() != null ? code.getDatabaseTechnology() : "");
|
||||
content = content.replace("{codeTechnology}", code.getCodeTechnology() != null ? code.getCodeTechnology() : "");
|
||||
content = content.replace("{diskLink}", code.getDiskLink() != null ? code.getDiskLink() : "");
|
||||
content = content.replace("{screenshots}", screenshots);
|
||||
|
||||
return success(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import javax.imageio.ImageIO;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import com.ruoyi.common.utils.CoverGenerator;
|
||||
import com.ruoyi.office.domain.ProjectLinkImportResult;
|
||||
import com.ruoyi.office.domain.TtCode;
|
||||
import com.ruoyi.office.service.ITtCodeService;
|
||||
import com.ruoyi.office.service.IProjectLinkCheckService;
|
||||
@@ -20,7 +21,9 @@ import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
@@ -70,6 +73,18 @@ public class TtProjectInfoController extends BaseController {
|
||||
util.exportExcel(response, list, "项目清单数据");
|
||||
}
|
||||
|
||||
/**
|
||||
* 按项目名称导入夸克或百度网盘链接。
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('office:project:edit')")
|
||||
@Log(title = "导入项目网盘链接", businessType = BusinessType.IMPORT)
|
||||
@PostMapping("/import-links")
|
||||
public AjaxResult importLinks(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("diskType") String diskType) {
|
||||
ProjectLinkImportResult result = ttProjectInfoService.importProjectLinks(file, diskType);
|
||||
return AjaxResult.success(result.buildMessage(), result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目清单详细信息
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.ruoyi.office.domain;
|
||||
|
||||
/**
|
||||
* 项目网盘链接导入明细。
|
||||
*/
|
||||
public class ProjectLinkImportDetail
|
||||
{
|
||||
/** 原文件中的行号。 */
|
||||
private int rowNumber;
|
||||
|
||||
/** 项目名称。 */
|
||||
private String projectName;
|
||||
|
||||
/** SKIPPED / FAILED。 */
|
||||
private String resultType;
|
||||
|
||||
/** 跳过或失败原因。 */
|
||||
private String reason;
|
||||
|
||||
public ProjectLinkImportDetail()
|
||||
{
|
||||
}
|
||||
|
||||
public ProjectLinkImportDetail(int rowNumber, String projectName, String resultType, String reason)
|
||||
{
|
||||
this.rowNumber = rowNumber;
|
||||
this.projectName = projectName;
|
||||
this.resultType = resultType;
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
public int getRowNumber()
|
||||
{
|
||||
return rowNumber;
|
||||
}
|
||||
|
||||
public void setRowNumber(int rowNumber)
|
||||
{
|
||||
this.rowNumber = rowNumber;
|
||||
}
|
||||
|
||||
public String getProjectName()
|
||||
{
|
||||
return projectName;
|
||||
}
|
||||
|
||||
public void setProjectName(String projectName)
|
||||
{
|
||||
this.projectName = projectName;
|
||||
}
|
||||
|
||||
public String getResultType()
|
||||
{
|
||||
return resultType;
|
||||
}
|
||||
|
||||
public void setResultType(String resultType)
|
||||
{
|
||||
this.resultType = resultType;
|
||||
}
|
||||
|
||||
public String getReason()
|
||||
{
|
||||
return reason;
|
||||
}
|
||||
|
||||
public void setReason(String reason)
|
||||
{
|
||||
this.reason = reason;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.ruoyi.office.domain;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 项目网盘链接导入结果。
|
||||
*/
|
||||
public class ProjectLinkImportResult
|
||||
{
|
||||
private int totalCount;
|
||||
|
||||
private int addedCount;
|
||||
|
||||
private int updatedCount;
|
||||
|
||||
private int unchangedCount;
|
||||
|
||||
private int skippedCount;
|
||||
|
||||
private int failedCount;
|
||||
|
||||
private final List<ProjectLinkImportDetail> details = new ArrayList<>();
|
||||
|
||||
public void addSkipped(int rowNumber, String projectName, String reason)
|
||||
{
|
||||
skippedCount++;
|
||||
details.add(new ProjectLinkImportDetail(rowNumber, projectName, "SKIPPED", reason));
|
||||
}
|
||||
|
||||
public void addFailed(int rowNumber, String projectName, String reason)
|
||||
{
|
||||
failedCount++;
|
||||
details.add(new ProjectLinkImportDetail(rowNumber, projectName, "FAILED", reason));
|
||||
}
|
||||
|
||||
public String buildMessage()
|
||||
{
|
||||
return "导入完成:共 " + totalCount + " 条,新增 " + addedCount + " 条,更新 "
|
||||
+ updatedCount + " 条,未变化 " + unchangedCount + " 条,跳过 "
|
||||
+ skippedCount + " 条,失败 " + failedCount + " 条";
|
||||
}
|
||||
|
||||
public int getTotalCount()
|
||||
{
|
||||
return totalCount;
|
||||
}
|
||||
|
||||
public void setTotalCount(int totalCount)
|
||||
{
|
||||
this.totalCount = totalCount;
|
||||
}
|
||||
|
||||
public int getAddedCount()
|
||||
{
|
||||
return addedCount;
|
||||
}
|
||||
|
||||
public void incrementAddedCount()
|
||||
{
|
||||
addedCount++;
|
||||
}
|
||||
|
||||
public int getUpdatedCount()
|
||||
{
|
||||
return updatedCount;
|
||||
}
|
||||
|
||||
public void incrementUpdatedCount()
|
||||
{
|
||||
updatedCount++;
|
||||
}
|
||||
|
||||
public int getUnchangedCount()
|
||||
{
|
||||
return unchangedCount;
|
||||
}
|
||||
|
||||
public void incrementUnchangedCount()
|
||||
{
|
||||
unchangedCount++;
|
||||
}
|
||||
|
||||
public int getSkippedCount()
|
||||
{
|
||||
return skippedCount;
|
||||
}
|
||||
|
||||
public int getFailedCount()
|
||||
{
|
||||
return failedCount;
|
||||
}
|
||||
|
||||
public List<ProjectLinkImportDetail> getDetails()
|
||||
{
|
||||
return details;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.ruoyi.office.domain;
|
||||
|
||||
/**
|
||||
* 项目网盘链接导入文件中的一行原始数据。
|
||||
*/
|
||||
public class ProjectLinkImportRow
|
||||
{
|
||||
private int rowNumber;
|
||||
|
||||
private String projectName;
|
||||
|
||||
private String shareAddress;
|
||||
|
||||
private String extractCode;
|
||||
|
||||
private String shareStatus;
|
||||
|
||||
public int getRowNumber()
|
||||
{
|
||||
return rowNumber;
|
||||
}
|
||||
|
||||
public void setRowNumber(int rowNumber)
|
||||
{
|
||||
this.rowNumber = rowNumber;
|
||||
}
|
||||
|
||||
public String getProjectName()
|
||||
{
|
||||
return projectName;
|
||||
}
|
||||
|
||||
public void setProjectName(String projectName)
|
||||
{
|
||||
this.projectName = projectName;
|
||||
}
|
||||
|
||||
public String getShareAddress()
|
||||
{
|
||||
return shareAddress;
|
||||
}
|
||||
|
||||
public void setShareAddress(String shareAddress)
|
||||
{
|
||||
this.shareAddress = shareAddress;
|
||||
}
|
||||
|
||||
public String getExtractCode()
|
||||
{
|
||||
return extractCode;
|
||||
}
|
||||
|
||||
public void setExtractCode(String extractCode)
|
||||
{
|
||||
this.extractCode = extractCode;
|
||||
}
|
||||
|
||||
public String getShareStatus()
|
||||
{
|
||||
return shareStatus;
|
||||
}
|
||||
|
||||
public void setShareStatus(String shareStatus)
|
||||
{
|
||||
this.shareStatus = shareStatus;
|
||||
}
|
||||
}
|
||||
@@ -40,11 +40,29 @@ public class TtCode extends BaseEntity {
|
||||
private String codeEnvironment;
|
||||
|
||||
/**
|
||||
* 项目技术
|
||||
* 其他技术
|
||||
*/
|
||||
@Excel(name = "项目技术")
|
||||
@Excel(name = "其他技术")
|
||||
private String codeTechnology;
|
||||
|
||||
/**
|
||||
* 前端
|
||||
*/
|
||||
@Excel(name = "前端")
|
||||
private String frontendTechnology;
|
||||
|
||||
/**
|
||||
* 后端
|
||||
*/
|
||||
@Excel(name = "后端")
|
||||
private String backendTechnology;
|
||||
|
||||
/**
|
||||
* 数据库
|
||||
*/
|
||||
@Excel(name = "数据库")
|
||||
private String databaseTechnology;
|
||||
|
||||
/**
|
||||
* 来源
|
||||
*/
|
||||
@@ -123,6 +141,30 @@ public class TtCode extends BaseEntity {
|
||||
return codeTechnology;
|
||||
}
|
||||
|
||||
public void setFrontendTechnology(String frontendTechnology) {
|
||||
this.frontendTechnology = frontendTechnology;
|
||||
}
|
||||
|
||||
public String getFrontendTechnology() {
|
||||
return frontendTechnology;
|
||||
}
|
||||
|
||||
public void setBackendTechnology(String backendTechnology) {
|
||||
this.backendTechnology = backendTechnology;
|
||||
}
|
||||
|
||||
public String getBackendTechnology() {
|
||||
return backendTechnology;
|
||||
}
|
||||
|
||||
public void setDatabaseTechnology(String databaseTechnology) {
|
||||
this.databaseTechnology = databaseTechnology;
|
||||
}
|
||||
|
||||
public String getDatabaseTechnology() {
|
||||
return databaseTechnology;
|
||||
}
|
||||
|
||||
public void setCodeSource(String codeSource) {
|
||||
this.codeSource = codeSource;
|
||||
}
|
||||
@@ -187,6 +229,9 @@ public class TtCode extends BaseEntity {
|
||||
.append("codeDesc", getCodeDesc())
|
||||
.append("codeEnvironment", getCodeEnvironment())
|
||||
.append("codeTechnology", getCodeTechnology())
|
||||
.append("frontendTechnology", getFrontendTechnology())
|
||||
.append("backendTechnology", getBackendTechnology())
|
||||
.append("databaseTechnology", getDatabaseTechnology())
|
||||
.append("codeSource", getCodeSource())
|
||||
.append("paymentType", getPaymentType())
|
||||
.append("diskLink", getDiskLink())
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.ruoyi.office.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.office.domain.TtProjectInfo;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 项目清单Mapper接口
|
||||
@@ -62,4 +63,8 @@ public interface TtProjectInfoMapper
|
||||
List<TtProjectInfo> lastUpdateList(String searchKey);
|
||||
|
||||
TtProjectInfo selectTtProjectInfoByName(String codeName);
|
||||
|
||||
int updateProjectQuarkUrl(@Param("id") Integer id, @Param("projectUrl") String projectUrl);
|
||||
|
||||
int updateProjectBaiduUrl(@Param("id") Integer id, @Param("projectBaiduUrl") String projectBaiduUrl);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.office.mapper;
|
||||
|
||||
import com.ruoyi.office.domain.TtProjectLinkCheck;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 项目网盘链接检测结果 Mapper。
|
||||
@@ -12,4 +13,7 @@ public interface TtProjectLinkCheckMapper
|
||||
int deleteByProjectId(Integer projectId);
|
||||
|
||||
int deleteByProjectIds(Integer[] projectIds);
|
||||
|
||||
int deleteByProjectIdAndDiskType(@Param("projectId") Integer projectId,
|
||||
@Param("diskType") String diskType);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.ruoyi.office.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.office.domain.TtProjectInfo;
|
||||
import com.ruoyi.office.domain.ProjectLinkImportResult;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* 项目清单Service接口
|
||||
@@ -64,4 +66,13 @@ public interface ITtProjectInfoService
|
||||
List<?> lastUpdateList(String searchKey);
|
||||
|
||||
TtProjectInfo selectTtProjectInfoByName(String codeName);
|
||||
|
||||
/**
|
||||
* 导入夸克或百度网盘分享链接。
|
||||
*
|
||||
* @param file 网盘客户端导出的 CSV/Excel 文件
|
||||
* @param diskType QUARK/BAIDU
|
||||
* @return 导入结果
|
||||
*/
|
||||
ProjectLinkImportResult importProjectLinks(MultipartFile file, String diskType);
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ public class TtCodeServiceImpl implements ITtCodeService {
|
||||
content.append(ttCode.getCodeDesc());
|
||||
content.append("<p><strong>### 运行环境</strong></p>");
|
||||
content.append(ttCode.getCodeEnvironment());
|
||||
content.append("<p><strong>### 项目技术</strong></p>");
|
||||
content.append("<p><strong>### 其他技术</strong></p>");
|
||||
content.append(ttCode.getCodeTechnology());
|
||||
ttArticles.setContent(content.toString());
|
||||
ttArticlesMapper.insertTtArticles(ttArticles);
|
||||
@@ -168,7 +168,7 @@ public class TtCodeServiceImpl implements ITtCodeService {
|
||||
content.append(ttCode.getCodeDesc());
|
||||
content.append("<p><strong>### 运行环境</strong></p>");
|
||||
content.append(ttCode.getCodeEnvironment());
|
||||
content.append("<p><strong>### 项目技术</strong></p>");
|
||||
content.append("<p><strong>### 其他技术</strong></p>");
|
||||
content.append(ttCode.getCodeTechnology());
|
||||
content.append("<p><strong>### 演示视频</strong></p>");
|
||||
content.append("请移步首页-<strong>视频资源</strong>,搜索<strong>项目编号</strong>查看");
|
||||
|
||||
@@ -1,15 +1,30 @@
|
||||
package com.ruoyi.office.service.impl;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.office.domain.ProjectLinkImportResult;
|
||||
import com.ruoyi.office.domain.ProjectLinkImportRow;
|
||||
import com.ruoyi.office.domain.TtCode;
|
||||
import com.ruoyi.office.mapper.TtCodeMapper;
|
||||
import com.ruoyi.office.mapper.TtProjectLinkCheckMapper;
|
||||
import com.ruoyi.office.service.importer.ProjectLinkImportParser;
|
||||
import com.ruoyi.office.service.netdisk.NetDiskConstants;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.ruoyi.office.mapper.TtProjectInfoMapper;
|
||||
import com.ruoyi.office.domain.TtProjectInfo;
|
||||
import com.ruoyi.office.service.ITtProjectInfoService;
|
||||
@@ -23,12 +38,23 @@ import com.ruoyi.office.service.ITtProjectInfoService;
|
||||
@Service
|
||||
public class TtProjectInfoServiceImpl implements ITtProjectInfoService
|
||||
{
|
||||
private static final Pattern PROJECT_NUM_PATTERN =
|
||||
Pattern.compile("^[【\\[]\\s*(S\\d+)\\s*[】\\]]", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private static final Pattern QUARK_URL_PATTERN =
|
||||
Pattern.compile("https://pan\\.quark\\.cn/s/[A-Za-z0-9_-]+(?:\\?[^\\s\"'<>,。;]*)?");
|
||||
|
||||
private static final Pattern BAIDU_PWD_PATTERN =
|
||||
Pattern.compile("(?:\\?|&)pwd=[^&]*", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
@Autowired
|
||||
private TtProjectInfoMapper ttProjectInfoMapper;
|
||||
@Autowired
|
||||
private TtCodeMapper ttCodeMapper;
|
||||
@Autowired
|
||||
private TtProjectLinkCheckMapper ttProjectLinkCheckMapper;
|
||||
@Autowired
|
||||
private ProjectLinkImportParser projectLinkImportParser;
|
||||
|
||||
/**
|
||||
* 查询项目清单
|
||||
@@ -129,4 +155,281 @@ public class TtProjectInfoServiceImpl implements ITtProjectInfoService
|
||||
public TtProjectInfo selectTtProjectInfoByName(String codeName) {
|
||||
return ttProjectInfoMapper.selectTtProjectInfoByName(codeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按项目名称导入网盘链接。新项目同时写入项目名称和源码名称;
|
||||
* 已存在项目只更新本次导入的网盘链接字段。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ProjectLinkImportResult importProjectLinks(MultipartFile file, String diskType)
|
||||
{
|
||||
String normalizedDiskType = normalizeDiskType(diskType);
|
||||
List<ProjectLinkImportRow> sourceRows = projectLinkImportParser.parse(file, normalizedDiskType);
|
||||
ProjectLinkImportResult result = new ProjectLinkImportResult();
|
||||
result.setTotalCount(sourceRows.size());
|
||||
|
||||
List<ValidatedImportRow> validRows = validateRows(sourceRows, normalizedDiskType, result);
|
||||
Set<String> duplicateNames = findDuplicateNames(validRows);
|
||||
Map<String, List<TtProjectInfo>> projectsByName = loadProjectsByNormalizedName();
|
||||
|
||||
for (ValidatedImportRow row : validRows)
|
||||
{
|
||||
if (duplicateNames.contains(row.normalizedName))
|
||||
{
|
||||
result.addFailed(row.rowNumber, row.projectName, "导入文件中存在重复项目名称");
|
||||
continue;
|
||||
}
|
||||
|
||||
List<TtProjectInfo> matches = projectsByName.get(row.normalizedName);
|
||||
if (matches != null && matches.size() > 1)
|
||||
{
|
||||
result.addFailed(row.rowNumber, row.projectName, "数据库中存在多个同名项目,无法确定更新目标");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (matches == null || matches.isEmpty())
|
||||
{
|
||||
TtProjectInfo project = createProject(row, normalizedDiskType);
|
||||
if (ttProjectInfoMapper.insertTtProjectInfo(project) != 1)
|
||||
{
|
||||
throw new ServiceException("新增项目失败:" + row.projectName);
|
||||
}
|
||||
List<TtProjectInfo> inserted = new ArrayList<>();
|
||||
inserted.add(project);
|
||||
projectsByName.put(row.normalizedName, inserted);
|
||||
result.incrementAddedCount();
|
||||
continue;
|
||||
}
|
||||
|
||||
TtProjectInfo project = matches.get(0);
|
||||
String oldLink = NetDiskConstants.DISK_QUARK.equals(normalizedDiskType)
|
||||
? project.getProjectUrl() : project.getProjectBaiduUrl();
|
||||
if (StringUtils.equals(StringUtils.trimToEmpty(oldLink), row.linkUrl))
|
||||
{
|
||||
result.incrementUnchangedCount();
|
||||
continue;
|
||||
}
|
||||
|
||||
int updated;
|
||||
if (NetDiskConstants.DISK_QUARK.equals(normalizedDiskType))
|
||||
{
|
||||
updated = ttProjectInfoMapper.updateProjectQuarkUrl(project.getId(), row.linkUrl);
|
||||
project.setProjectUrl(row.linkUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
updated = ttProjectInfoMapper.updateProjectBaiduUrl(project.getId(), row.linkUrl);
|
||||
project.setProjectBaiduUrl(row.linkUrl);
|
||||
}
|
||||
if (updated != 1)
|
||||
{
|
||||
throw new ServiceException("更新项目链接失败:" + row.projectName);
|
||||
}
|
||||
ttProjectLinkCheckMapper.deleteByProjectIdAndDiskType(project.getId(), normalizedDiskType);
|
||||
result.incrementUpdatedCount();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<ValidatedImportRow> validateRows(List<ProjectLinkImportRow> sourceRows,
|
||||
String diskType,
|
||||
ProjectLinkImportResult result)
|
||||
{
|
||||
List<ValidatedImportRow> validRows = new ArrayList<>();
|
||||
String successStatus = NetDiskConstants.DISK_QUARK.equals(diskType) ? "成功" : "生成成功";
|
||||
for (ProjectLinkImportRow sourceRow : sourceRows)
|
||||
{
|
||||
String projectName = normalizeProjectName(sourceRow.getProjectName());
|
||||
String status = StringUtils.trimToEmpty(sourceRow.getShareStatus());
|
||||
if (!successStatus.equals(status))
|
||||
{
|
||||
result.addSkipped(sourceRow.getRowNumber(), projectName,
|
||||
"分享状态不是" + successStatus + ":" + StringUtils.defaultIfBlank(status, "空"));
|
||||
continue;
|
||||
}
|
||||
if (StringUtils.isBlank(projectName))
|
||||
{
|
||||
result.addFailed(sourceRow.getRowNumber(), "", "项目名称不能为空");
|
||||
continue;
|
||||
}
|
||||
if (projectName.length() > 255)
|
||||
{
|
||||
result.addFailed(sourceRow.getRowNumber(), projectName, "项目名称超过 255 个字符");
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
String linkUrl = buildAndValidateLink(sourceRow, diskType);
|
||||
validRows.add(new ValidatedImportRow(sourceRow.getRowNumber(), projectName,
|
||||
normalizeProjectName(projectName), linkUrl));
|
||||
}
|
||||
catch (IllegalArgumentException e)
|
||||
{
|
||||
result.addFailed(sourceRow.getRowNumber(), projectName, e.getMessage());
|
||||
}
|
||||
}
|
||||
return validRows;
|
||||
}
|
||||
|
||||
private String buildAndValidateLink(ProjectLinkImportRow row, String diskType)
|
||||
{
|
||||
String linkUrl;
|
||||
if (NetDiskConstants.DISK_QUARK.equals(diskType))
|
||||
{
|
||||
Matcher matcher = QUARK_URL_PATTERN.matcher(StringUtils.defaultString(row.getShareAddress()));
|
||||
if (!matcher.find())
|
||||
{
|
||||
throw new IllegalArgumentException("未找到有效的夸克网盘链接");
|
||||
}
|
||||
linkUrl = matcher.group();
|
||||
}
|
||||
else
|
||||
{
|
||||
linkUrl = StringUtils.trimToEmpty(row.getShareAddress());
|
||||
String extractCode = StringUtils.trimToEmpty(row.getExtractCode());
|
||||
if (StringUtils.isNotBlank(extractCode) && !BAIDU_PWD_PATTERN.matcher(linkUrl).find())
|
||||
{
|
||||
try
|
||||
{
|
||||
linkUrl += linkUrl.contains("?") ? "&" : "?";
|
||||
linkUrl += "pwd=" + URLEncoder.encode(extractCode, "UTF-8");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new IllegalArgumentException("百度网盘提取码处理失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (linkUrl.length() > 1000)
|
||||
{
|
||||
throw new IllegalArgumentException("网盘链接超过 1000 个字符");
|
||||
}
|
||||
validateLinkHostAndPath(linkUrl, diskType);
|
||||
return linkUrl;
|
||||
}
|
||||
|
||||
private void validateLinkHostAndPath(String linkUrl, String diskType)
|
||||
{
|
||||
try
|
||||
{
|
||||
URI uri = new URI(linkUrl);
|
||||
String host = uri.getHost();
|
||||
String path = uri.getPath();
|
||||
boolean isHttps = "https".equalsIgnoreCase(uri.getScheme());
|
||||
boolean isQuark = NetDiskConstants.DISK_QUARK.equals(diskType)
|
||||
&& "pan.quark.cn".equalsIgnoreCase(host)
|
||||
&& path != null && path.startsWith("/s/");
|
||||
boolean isBaidu = NetDiskConstants.DISK_BAIDU.equals(diskType)
|
||||
&& host != null
|
||||
&& ("pan.baidu.com".equalsIgnoreCase(host) || "yun.baidu.com".equalsIgnoreCase(host))
|
||||
&& path != null
|
||||
&& (path.startsWith("/s/") || path.startsWith("/share/"));
|
||||
if (!isHttps || uri.getUserInfo() != null || (uri.getPort() != -1 && uri.getPort() != 443)
|
||||
|| (!isQuark && !isBaidu))
|
||||
{
|
||||
throw new IllegalArgumentException("网盘链接格式不正确");
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException e)
|
||||
{
|
||||
throw e;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new IllegalArgumentException("网盘链接格式不正确");
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, List<TtProjectInfo>> loadProjectsByNormalizedName()
|
||||
{
|
||||
Map<String, List<TtProjectInfo>> projectsByName = new HashMap<>();
|
||||
List<TtProjectInfo> projects = ttProjectInfoMapper.selectTtProjectInfoList(new TtProjectInfo());
|
||||
for (TtProjectInfo project : projects)
|
||||
{
|
||||
String normalizedName = normalizeProjectName(project.getProjectName());
|
||||
if (StringUtils.isBlank(normalizedName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
projectsByName.computeIfAbsent(normalizedName, key -> new ArrayList<>()).add(project);
|
||||
}
|
||||
return projectsByName;
|
||||
}
|
||||
|
||||
private Set<String> findDuplicateNames(List<ValidatedImportRow> rows)
|
||||
{
|
||||
Set<String> names = new HashSet<>();
|
||||
Set<String> duplicateNames = new HashSet<>();
|
||||
for (ValidatedImportRow row : rows)
|
||||
{
|
||||
if (!names.add(row.normalizedName))
|
||||
{
|
||||
duplicateNames.add(row.normalizedName);
|
||||
}
|
||||
}
|
||||
return duplicateNames;
|
||||
}
|
||||
|
||||
private TtProjectInfo createProject(ValidatedImportRow row, String diskType)
|
||||
{
|
||||
TtProjectInfo project = new TtProjectInfo();
|
||||
project.setProjectName(row.projectName);
|
||||
project.setProjectName1(row.projectName);
|
||||
project.setProjectNum(extractProjectNum(row.projectName));
|
||||
if (NetDiskConstants.DISK_QUARK.equals(diskType))
|
||||
{
|
||||
project.setProjectUrl(row.linkUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
project.setProjectBaiduUrl(row.linkUrl);
|
||||
}
|
||||
return project;
|
||||
}
|
||||
|
||||
private String extractProjectNum(String projectName)
|
||||
{
|
||||
Matcher matcher = PROJECT_NUM_PATTERN.matcher(projectName);
|
||||
return matcher.find() ? matcher.group(1).toUpperCase(Locale.ROOT) : null;
|
||||
}
|
||||
|
||||
private String normalizeProjectName(String projectName)
|
||||
{
|
||||
return StringUtils.trimToEmpty(projectName)
|
||||
.replace("\uFEFF", "")
|
||||
.replace('\u00A0', ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
private String normalizeDiskType(String diskType)
|
||||
{
|
||||
String value = StringUtils.trimToEmpty(diskType).toUpperCase(Locale.ROOT);
|
||||
if (!NetDiskConstants.DISK_QUARK.equals(value) && !NetDiskConstants.DISK_BAIDU.equals(value))
|
||||
{
|
||||
throw new ServiceException("不支持的网盘类型");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static class ValidatedImportRow
|
||||
{
|
||||
private final int rowNumber;
|
||||
|
||||
private final String projectName;
|
||||
|
||||
private final String normalizedName;
|
||||
|
||||
private final String linkUrl;
|
||||
|
||||
private ValidatedImportRow(int rowNumber, String projectName, String normalizedName, String linkUrl)
|
||||
{
|
||||
this.rowNumber = rowNumber;
|
||||
this.projectName = projectName;
|
||||
this.normalizedName = normalizedName;
|
||||
this.linkUrl = linkUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
package com.ruoyi.office.service.importer;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.StringReader;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.CharacterCodingException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.CharsetDecoder;
|
||||
import java.nio.charset.CodingErrorAction;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import cn.hutool.core.text.csv.CsvData;
|
||||
import cn.hutool.core.text.csv.CsvReadConfig;
|
||||
import cn.hutool.core.text.csv.CsvRow;
|
||||
import cn.hutool.core.text.csv.CsvUtil;
|
||||
import cn.hutool.poi.excel.ExcelReader;
|
||||
import cn.hutool.poi.excel.ExcelUtil;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.office.domain.ProjectLinkImportRow;
|
||||
import com.ruoyi.office.service.netdisk.NetDiskConstants;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* 解析夸克、百度网盘客户端导出的 CSV 或 Excel 文件。
|
||||
*/
|
||||
@Component
|
||||
public class ProjectLinkImportParser
|
||||
{
|
||||
private static final long MAX_FILE_SIZE = 5L * 1024L * 1024L;
|
||||
|
||||
private static final int MAX_ROW_COUNT = 2000;
|
||||
|
||||
private static final Charset GB18030 = Charset.forName("GB18030");
|
||||
|
||||
public List<ProjectLinkImportRow> parse(MultipartFile file, String diskType)
|
||||
{
|
||||
validateFile(file);
|
||||
String normalizedDiskType = normalizeDiskType(diskType);
|
||||
String extension = FilenameUtils.getExtension(file.getOriginalFilename()).toLowerCase(Locale.ROOT);
|
||||
try
|
||||
{
|
||||
byte[] bytes = file.getBytes();
|
||||
List<SourceRow> sourceRows;
|
||||
if ("csv".equals(extension))
|
||||
{
|
||||
sourceRows = readCsv(bytes);
|
||||
}
|
||||
else if ("xls".equals(extension) || "xlsx".equals(extension))
|
||||
{
|
||||
sourceRows = readExcel(bytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ServiceException("仅支持 csv、xls、xlsx 格式文件");
|
||||
}
|
||||
if (sourceRows.size() > MAX_ROW_COUNT)
|
||||
{
|
||||
throw new ServiceException("单次最多导入 " + MAX_ROW_COUNT + " 条数据");
|
||||
}
|
||||
validateHeaders(sourceRows, normalizedDiskType);
|
||||
return convertRows(sourceRows, normalizedDiskType);
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
throw e;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new ServiceException("文件解析失败,请确认文件格式与网盘类型是否正确");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateFile(MultipartFile file)
|
||||
{
|
||||
if (file == null || file.isEmpty())
|
||||
{
|
||||
throw new ServiceException("请选择需要导入的文件");
|
||||
}
|
||||
if (file.getSize() > MAX_FILE_SIZE)
|
||||
{
|
||||
throw new ServiceException("导入文件不能超过 5MB");
|
||||
}
|
||||
if (StringUtils.isBlank(file.getOriginalFilename()))
|
||||
{
|
||||
throw new ServiceException("无法识别导入文件名");
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeDiskType(String diskType)
|
||||
{
|
||||
String value = StringUtils.trimToEmpty(diskType).toUpperCase(Locale.ROOT);
|
||||
if (!NetDiskConstants.DISK_QUARK.equals(value) && !NetDiskConstants.DISK_BAIDU.equals(value))
|
||||
{
|
||||
throw new ServiceException("不支持的网盘类型");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private List<SourceRow> readCsv(byte[] bytes)
|
||||
{
|
||||
String csvText = decodeCsv(bytes);
|
||||
CsvReadConfig config = CsvReadConfig.defaultConfig()
|
||||
.setContainsHeader(true)
|
||||
.setSkipEmptyRows(true)
|
||||
.setErrorOnDifferentFieldCount(true);
|
||||
CsvData csvData = CsvUtil.getReader(new StringReader(csvText), config).read();
|
||||
List<SourceRow> rows = new ArrayList<>();
|
||||
for (CsvRow csvRow : csvData.getRows())
|
||||
{
|
||||
rows.add(new SourceRow((int) csvRow.getOriginalLineNumber() + 1,
|
||||
normalizeFieldMap(csvRow.getFieldMap())));
|
||||
}
|
||||
if (rows.isEmpty())
|
||||
{
|
||||
rows.add(new SourceRow(1, headerOnlyMap(csvData.getHeader())));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private List<SourceRow> readExcel(byte[] bytes)
|
||||
{
|
||||
List<SourceRow> rows = new ArrayList<>();
|
||||
try (ExcelReader reader = ExcelUtil.getReader(new ByteArrayInputStream(bytes)))
|
||||
{
|
||||
List<Map<String, Object>> excelRows = reader.readAll();
|
||||
int rowNumber = 2;
|
||||
for (Map<String, Object> excelRow : excelRows)
|
||||
{
|
||||
Map<String, String> fields = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, Object> entry : excelRow.entrySet())
|
||||
{
|
||||
fields.put(normalizeHeader(entry.getKey()), objectToString(entry.getValue()));
|
||||
}
|
||||
rows.add(new SourceRow(rowNumber++, fields));
|
||||
}
|
||||
if (rows.isEmpty())
|
||||
{
|
||||
List<Object> headers = reader.readRow(0);
|
||||
Map<String, String> headerMap = new LinkedHashMap<>();
|
||||
for (Object header : headers)
|
||||
{
|
||||
headerMap.put(normalizeHeader(objectToString(header)), "");
|
||||
}
|
||||
rows.add(new SourceRow(1, headerMap));
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private String decodeCsv(byte[] bytes)
|
||||
{
|
||||
try
|
||||
{
|
||||
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder()
|
||||
.onMalformedInput(CodingErrorAction.REPORT)
|
||||
.onUnmappableCharacter(CodingErrorAction.REPORT);
|
||||
CharBuffer decoded = decoder.decode(ByteBuffer.wrap(bytes));
|
||||
return decoded.toString();
|
||||
}
|
||||
catch (CharacterCodingException ignored)
|
||||
{
|
||||
return new String(bytes, GB18030);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String> normalizeFieldMap(Map<String, String> source)
|
||||
{
|
||||
Map<String, String> fields = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, String> entry : source.entrySet())
|
||||
{
|
||||
fields.put(normalizeHeader(entry.getKey()), entry.getValue());
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
private Map<String, String> headerOnlyMap(List<String> headers)
|
||||
{
|
||||
Map<String, String> fields = new LinkedHashMap<>();
|
||||
if (headers != null)
|
||||
{
|
||||
for (String header : headers)
|
||||
{
|
||||
fields.put(normalizeHeader(header), "");
|
||||
}
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
private void validateHeaders(List<SourceRow> rows, String diskType)
|
||||
{
|
||||
Map<String, String> fields = rows.get(0).fields;
|
||||
List<String> requiredHeaders = new ArrayList<>();
|
||||
if (NetDiskConstants.DISK_QUARK.equals(diskType))
|
||||
{
|
||||
requiredHeaders.add("创建分享状态");
|
||||
requiredHeaders.add("分享名");
|
||||
requiredHeaders.add("分享地址");
|
||||
}
|
||||
else
|
||||
{
|
||||
requiredHeaders.add("文件名");
|
||||
requiredHeaders.add("链接");
|
||||
requiredHeaders.add("分享状态");
|
||||
}
|
||||
for (String header : requiredHeaders)
|
||||
{
|
||||
if (!fields.containsKey(header))
|
||||
{
|
||||
throw new ServiceException("文件缺少必要列:" + header);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<ProjectLinkImportRow> convertRows(List<SourceRow> sourceRows, String diskType)
|
||||
{
|
||||
List<ProjectLinkImportRow> rows = new ArrayList<>();
|
||||
for (SourceRow sourceRow : sourceRows)
|
||||
{
|
||||
if (sourceRow.rowNumber == 1 && allValuesBlank(sourceRow.fields))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
ProjectLinkImportRow row = new ProjectLinkImportRow();
|
||||
row.setRowNumber(sourceRow.rowNumber);
|
||||
if (NetDiskConstants.DISK_QUARK.equals(diskType))
|
||||
{
|
||||
row.setProjectName(sourceRow.fields.get("分享名"));
|
||||
row.setShareAddress(sourceRow.fields.get("分享地址"));
|
||||
row.setExtractCode(sourceRow.fields.get("提取码"));
|
||||
row.setShareStatus(sourceRow.fields.get("创建分享状态"));
|
||||
}
|
||||
else
|
||||
{
|
||||
row.setProjectName(sourceRow.fields.get("文件名"));
|
||||
row.setShareAddress(sourceRow.fields.get("链接"));
|
||||
row.setExtractCode(sourceRow.fields.get("提取码"));
|
||||
row.setShareStatus(sourceRow.fields.get("分享状态"));
|
||||
}
|
||||
rows.add(row);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private boolean allValuesBlank(Map<String, String> fields)
|
||||
{
|
||||
for (String value : fields.values())
|
||||
{
|
||||
if (StringUtils.isNotBlank(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private String normalizeHeader(String header)
|
||||
{
|
||||
return StringUtils.trimToEmpty(header).replace("\uFEFF", "");
|
||||
}
|
||||
|
||||
private String objectToString(Object value)
|
||||
{
|
||||
return value == null ? "" : String.valueOf(value);
|
||||
}
|
||||
|
||||
private static class SourceRow
|
||||
{
|
||||
private final int rowNumber;
|
||||
|
||||
private final Map<String, String> fields;
|
||||
|
||||
private SourceRow(int rowNumber, Map<String, String> fields)
|
||||
{
|
||||
this.rowNumber = rowNumber;
|
||||
this.fields = fields;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="codeDesc" column="code_desc" />
|
||||
<result property="codeEnvironment" column="code_environment" />
|
||||
<result property="codeTechnology" column="code_technology" />
|
||||
<result property="frontendTechnology" column="frontend_technology" />
|
||||
<result property="backendTechnology" column="backend_technology" />
|
||||
<result property="databaseTechnology" column="database_technology" />
|
||||
<result property="codeSource" column="code_source" />
|
||||
<result property="paymentType" column="payment_type" />
|
||||
<result property="diskLink" column="disk_link" />
|
||||
@@ -19,7 +22,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectTtCodeVo">
|
||||
select code_id, code_name, code_desc, code_environment, code_technology, code_source, payment_type, disk_link, publish_flag, picture_file, video_file from tt_code
|
||||
select code_id, code_name, code_desc, code_environment, code_technology,
|
||||
frontend_technology, backend_technology, database_technology,
|
||||
code_source, payment_type, disk_link, publish_flag, picture_file, video_file
|
||||
from tt_code
|
||||
</sql>
|
||||
|
||||
<select id="selectTtCodeList" parameterType="TtCode" resultMap="TtCodeResult">
|
||||
@@ -29,6 +35,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="codeDesc != null and codeDesc != ''"> and code_desc = #{codeDesc}</if>
|
||||
<if test="codeEnvironment != null and codeEnvironment != ''"> and code_environment = #{codeEnvironment}</if>
|
||||
<if test="codeTechnology != null and codeTechnology != ''"> and code_technology = #{codeTechnology}</if>
|
||||
<if test="frontendTechnology != null and frontendTechnology != ''"> and frontend_technology = #{frontendTechnology}</if>
|
||||
<if test="backendTechnology != null and backendTechnology != ''"> and backend_technology = #{backendTechnology}</if>
|
||||
<if test="databaseTechnology != null and databaseTechnology != ''"> and database_technology = #{databaseTechnology}</if>
|
||||
<if test="codeSource != null and codeSource != ''"> and code_source = #{codeSource}</if>
|
||||
<if test="paymentType != null and paymentType != ''"> and payment_type = #{paymentType}</if>
|
||||
<if test="diskLink != null and diskLink != ''"> and disk_link = #{diskLink}</if>
|
||||
@@ -57,6 +66,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="codeDesc != null">code_desc,</if>
|
||||
<if test="codeEnvironment != null">code_environment,</if>
|
||||
<if test="codeTechnology != null">code_technology,</if>
|
||||
<if test="frontendTechnology != null">frontend_technology,</if>
|
||||
<if test="backendTechnology != null">backend_technology,</if>
|
||||
<if test="databaseTechnology != null">database_technology,</if>
|
||||
<if test="codeSource != null">code_source,</if>
|
||||
<if test="paymentType != null">payment_type,</if>
|
||||
<if test="diskLink != null">disk_link,</if>
|
||||
@@ -70,6 +82,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="codeDesc != null">#{codeDesc},</if>
|
||||
<if test="codeEnvironment != null">#{codeEnvironment},</if>
|
||||
<if test="codeTechnology != null">#{codeTechnology},</if>
|
||||
<if test="frontendTechnology != null">#{frontendTechnology},</if>
|
||||
<if test="backendTechnology != null">#{backendTechnology},</if>
|
||||
<if test="databaseTechnology != null">#{databaseTechnology},</if>
|
||||
<if test="codeSource != null">#{codeSource},</if>
|
||||
<if test="paymentType != null">#{paymentType},</if>
|
||||
<if test="diskLink != null">#{diskLink},</if>
|
||||
@@ -86,6 +101,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="codeDesc != null">code_desc = #{codeDesc},</if>
|
||||
<if test="codeEnvironment != null">code_environment = #{codeEnvironment},</if>
|
||||
<if test="codeTechnology != null">code_technology = #{codeTechnology},</if>
|
||||
<if test="frontendTechnology != null">frontend_technology = #{frontendTechnology},</if>
|
||||
<if test="backendTechnology != null">backend_technology = #{backendTechnology},</if>
|
||||
<if test="databaseTechnology != null">database_technology = #{databaseTechnology},</if>
|
||||
<if test="codeSource != null">code_source = #{codeSource},</if>
|
||||
<if test="paymentType != null">payment_type = #{paymentType},</if>
|
||||
<if test="diskLink != null">disk_link = #{diskLink},</if>
|
||||
|
||||
@@ -113,6 +113,18 @@
|
||||
where id = #{id}
|
||||
</update>
|
||||
|
||||
<update id="updateProjectQuarkUrl">
|
||||
update tt_project_info
|
||||
set project_url = #{projectUrl}
|
||||
where id = #{id}
|
||||
</update>
|
||||
|
||||
<update id="updateProjectBaiduUrl">
|
||||
update tt_project_info
|
||||
set project_baidu_url = #{projectBaiduUrl}
|
||||
where id = #{id}
|
||||
</update>
|
||||
|
||||
<delete id="deleteTtProjectInfoById" parameterType="Integer">
|
||||
delete from tt_project_info where id = #{id}
|
||||
</delete>
|
||||
|
||||
@@ -52,4 +52,10 @@
|
||||
#{projectId}
|
||||
</foreach>
|
||||
</delete>
|
||||
|
||||
<delete id="deleteByProjectIdAndDiskType">
|
||||
delete from tt_project_link_check
|
||||
where project_id = #{projectId}
|
||||
and disk_type = #{diskType}
|
||||
</delete>
|
||||
</mapper>
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.ruoyi.office.service.impl;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import com.ruoyi.office.domain.ProjectLinkImportResult;
|
||||
import com.ruoyi.office.domain.ProjectLinkImportRow;
|
||||
import com.ruoyi.office.domain.TtProjectInfo;
|
||||
import com.ruoyi.office.mapper.TtCodeMapper;
|
||||
import com.ruoyi.office.mapper.TtProjectInfoMapper;
|
||||
import com.ruoyi.office.mapper.TtProjectLinkCheckMapper;
|
||||
import com.ruoyi.office.service.importer.ProjectLinkImportParser;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class TtProjectInfoServiceImplTest
|
||||
{
|
||||
@InjectMocks
|
||||
private TtProjectInfoServiceImpl service;
|
||||
|
||||
@Mock
|
||||
private TtProjectInfoMapper projectInfoMapper;
|
||||
|
||||
@Mock
|
||||
private TtCodeMapper codeMapper;
|
||||
|
||||
@Mock
|
||||
private TtProjectLinkCheckMapper linkCheckMapper;
|
||||
|
||||
@Mock
|
||||
private ProjectLinkImportParser parser;
|
||||
|
||||
@Mock
|
||||
private MultipartFile file;
|
||||
|
||||
private ProjectLinkImportRow quarkRow;
|
||||
|
||||
@Before
|
||||
public void setUp()
|
||||
{
|
||||
quarkRow = new ProjectLinkImportRow();
|
||||
quarkRow.setRowNumber(2);
|
||||
quarkRow.setProjectName("【S031】婚纱摄影管理系统");
|
||||
quarkRow.setShareAddress("分享内容\r\n链接:https://pan.quark.cn/s/741bf63df1bb");
|
||||
quarkRow.setShareStatus("成功");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createsMissingProjectWithBothNameFields()
|
||||
{
|
||||
when(parser.parse(file, "QUARK")).thenReturn(Collections.singletonList(quarkRow));
|
||||
when(projectInfoMapper.selectTtProjectInfoList(any(TtProjectInfo.class)))
|
||||
.thenReturn(Collections.emptyList());
|
||||
when(projectInfoMapper.insertTtProjectInfo(any(TtProjectInfo.class))).thenReturn(1);
|
||||
|
||||
ProjectLinkImportResult result = service.importProjectLinks(file, "QUARK");
|
||||
|
||||
ArgumentCaptor<TtProjectInfo> captor = ArgumentCaptor.forClass(TtProjectInfo.class);
|
||||
verify(projectInfoMapper).insertTtProjectInfo(captor.capture());
|
||||
TtProjectInfo inserted = captor.getValue();
|
||||
Assert.assertEquals("【S031】婚纱摄影管理系统", inserted.getProjectName());
|
||||
Assert.assertEquals("【S031】婚纱摄影管理系统", inserted.getProjectName1());
|
||||
Assert.assertEquals("S031", inserted.getProjectNum());
|
||||
Assert.assertEquals("https://pan.quark.cn/s/741bf63df1bb", inserted.getProjectUrl());
|
||||
Assert.assertEquals(1, result.getAddedCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updatesOnlyBaiduLinkForExistingProjectAndAddsPasscode()
|
||||
{
|
||||
ProjectLinkImportRow baiduRow = new ProjectLinkImportRow();
|
||||
baiduRow.setRowNumber(2);
|
||||
baiduRow.setProjectName("【S031】婚纱摄影管理系统");
|
||||
baiduRow.setShareAddress("https://pan.baidu.com/s/1abc");
|
||||
baiduRow.setExtractCode("iuxg");
|
||||
baiduRow.setShareStatus("生成成功");
|
||||
|
||||
TtProjectInfo existing = new TtProjectInfo();
|
||||
existing.setId(31);
|
||||
existing.setProjectName("【S031】婚纱摄影管理系统");
|
||||
existing.setProjectName1("原源码名称");
|
||||
existing.setProjectUrl("https://pan.quark.cn/s/old");
|
||||
|
||||
when(parser.parse(file, "BAIDU")).thenReturn(Collections.singletonList(baiduRow));
|
||||
when(projectInfoMapper.selectTtProjectInfoList(any(TtProjectInfo.class)))
|
||||
.thenReturn(Collections.singletonList(existing));
|
||||
when(projectInfoMapper.updateProjectBaiduUrl(31,
|
||||
"https://pan.baidu.com/s/1abc?pwd=iuxg")).thenReturn(1);
|
||||
|
||||
ProjectLinkImportResult result = service.importProjectLinks(file, "BAIDU");
|
||||
|
||||
verify(projectInfoMapper).updateProjectBaiduUrl(31,
|
||||
"https://pan.baidu.com/s/1abc?pwd=iuxg");
|
||||
verify(linkCheckMapper).deleteByProjectIdAndDiskType(31, "BAIDU");
|
||||
verify(projectInfoMapper, never()).updateTtProjectInfo(any(TtProjectInfo.class));
|
||||
verify(projectInfoMapper, never()).insertTtProjectInfo(any(TtProjectInfo.class));
|
||||
Assert.assertEquals("原源码名称", existing.getProjectName1());
|
||||
Assert.assertEquals(1, result.getUpdatedCount());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.ruoyi.office.service.importer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
import com.ruoyi.office.domain.ProjectLinkImportRow;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
public class ProjectLinkImportParserTest
|
||||
{
|
||||
private final ProjectLinkImportParser parser = new ProjectLinkImportParser();
|
||||
|
||||
@Test
|
||||
public void parsesQuarkCsvWithMultilineShareAddress()
|
||||
{
|
||||
String csv = "创建分享状态,分享名,分享地址,提取码,分享时间\r\n"
|
||||
+ "成功,【S031】婚纱摄影管理系统,\"我用夸克网盘分享了文件。\r\n"
|
||||
+ "链接:https://pan.quark.cn/s/741bf63df1bb\",,2026-07-29 12:25\r\n";
|
||||
MockMultipartFile file = new MockMultipartFile("file", "quark.csv", "text/csv",
|
||||
csv.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
List<ProjectLinkImportRow> rows = parser.parse(file, "QUARK");
|
||||
|
||||
Assert.assertEquals(1, rows.size());
|
||||
Assert.assertEquals(2, rows.get(0).getRowNumber());
|
||||
Assert.assertEquals("【S031】婚纱摄影管理系统", rows.get(0).getProjectName());
|
||||
Assert.assertTrue(rows.get(0).getShareAddress().contains("https://pan.quark.cn/s/741bf63df1bb"));
|
||||
Assert.assertEquals("成功", rows.get(0).getShareStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parsesBaiduCsv()
|
||||
{
|
||||
String csv = "文件名,链接,提取码,分享时间,分享状态\r\n"
|
||||
+ "【S001】家政服务人员技能评级系统,"
|
||||
+ "https://pan.baidu.com/s/1EimsC2N8zzvrCP6mn3fNyQ,iuxg,"
|
||||
+ "2026-07-29 12:35,生成成功\r\n";
|
||||
MockMultipartFile file = new MockMultipartFile("file", "baidu.csv", "text/csv",
|
||||
csv.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
List<ProjectLinkImportRow> rows = parser.parse(file, "BAIDU");
|
||||
|
||||
Assert.assertEquals(1, rows.size());
|
||||
Assert.assertEquals(2, rows.get(0).getRowNumber());
|
||||
Assert.assertEquals("【S001】家政服务人员技能评级系统", rows.get(0).getProjectName());
|
||||
Assert.assertEquals("iuxg", rows.get(0).getExtractCode());
|
||||
Assert.assertEquals("生成成功", rows.get(0).getShareStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parsesBaiduExcel() throws Exception
|
||||
{
|
||||
byte[] excelBytes;
|
||||
try (XSSFWorkbook workbook = new XSSFWorkbook();
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream())
|
||||
{
|
||||
Sheet sheet = workbook.createSheet("分享记录");
|
||||
Row header = sheet.createRow(0);
|
||||
header.createCell(0).setCellValue("文件名");
|
||||
header.createCell(1).setCellValue("链接");
|
||||
header.createCell(2).setCellValue("提取码");
|
||||
header.createCell(3).setCellValue("分享时间");
|
||||
header.createCell(4).setCellValue("分享状态");
|
||||
Row data = sheet.createRow(1);
|
||||
data.createCell(0).setCellValue("【S001】家政服务人员技能评级系统");
|
||||
data.createCell(1).setCellValue("https://pan.baidu.com/s/1abc");
|
||||
data.createCell(2).setCellValue("iuxg");
|
||||
data.createCell(3).setCellValue("2026-07-29 12:35");
|
||||
data.createCell(4).setCellValue("生成成功");
|
||||
workbook.write(output);
|
||||
excelBytes = output.toByteArray();
|
||||
}
|
||||
|
||||
MockMultipartFile file = new MockMultipartFile("file", "baidu.xlsx",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", excelBytes);
|
||||
List<ProjectLinkImportRow> rows = parser.parse(file, "BAIDU");
|
||||
|
||||
Assert.assertEquals(1, rows.size());
|
||||
Assert.assertEquals("【S001】家政服务人员技能评级系统", rows.get(0).getProjectName());
|
||||
Assert.assertEquals("生成成功", rows.get(0).getShareStatus());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user