From e9d627df9c0ff4f0b50cc1e18092bb85bdfe6db4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=8E=8B=E9=B9=8F?=
Date: Wed, 29 Jul 2026 16:17:49 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?=
=?UTF-8?q?=E7=94=9F=E6=88=90=E5=8F=8A=E6=96=87=E6=A1=88=E6=A8=A1=E6=9D=BF?=
=?UTF-8?q?=E6=B8=B2=E6=9F=93?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../office/controller/TtCodeController.java | 16 +-
.../controller/TtCopyTemplateController.java | 56 +----
.../office/mapper/TtProjectInfoMapper.java | 2 +
.../office/service/CopyTemplateRenderer.java | 99 +++++++++
.../ruoyi/office/service/ITtCodeService.java | 10 +-
.../service/impl/TtCodeServiceImpl.java | 194 +++++++++++-------
.../impl/TtProjectInfoServiceImpl.java | 17 +-
.../mapper/office/TtProjectInfoMapper.xml | 7 +
.../service/CopyTemplateRendererTest.java | 56 +++++
.../service/impl/TtCodeServiceImplTest.java | 152 ++++++++++++++
.../impl/TtProjectInfoServiceImplTest.java | 38 ++++
.../app/mapper/AppBlogArticleMapper.java | 8 +
.../mapper/app/AppBlogArticleMapper.xml | 8 +-
ruoyi-ui/src/api/office/code.js | 12 +-
ruoyi-ui/src/views/office/code/index.vue | 84 ++++----
15 files changed, 560 insertions(+), 199 deletions(-)
create mode 100644 ruoyi-office/src/main/java/com/ruoyi/office/service/CopyTemplateRenderer.java
create mode 100644 ruoyi-office/src/test/java/com/ruoyi/office/service/CopyTemplateRendererTest.java
create mode 100644 ruoyi-office/src/test/java/com/ruoyi/office/service/impl/TtCodeServiceImplTest.java
diff --git a/ruoyi-office/src/main/java/com/ruoyi/office/controller/TtCodeController.java b/ruoyi-office/src/main/java/com/ruoyi/office/controller/TtCodeController.java
index b5305f9..1480895 100644
--- a/ruoyi-office/src/main/java/com/ruoyi/office/controller/TtCodeController.java
+++ b/ruoyi-office/src/main/java/com/ruoyi/office/controller/TtCodeController.java
@@ -103,26 +103,14 @@ public class TtCodeController extends BaseController
return toAjax(ttCodeService.deleteTtCodeByCodeIds(codeIds));
}
- /**
- * 转文章
- */
- @PreAuthorize("@ss.hasPermi('office:code:edit')")
- @Log(title = "源码管理", businessType = BusinessType.UPDATE)
- @GetMapping("/transToArticle/{codeId}")
- public AjaxResult transToArticle(@PathVariable Long codeId)
- {
- String msg = ttCodeService.transToArticle(codeId);
- return success(msg);
- }
-
/**
* 转文章
*/
@PreAuthorize("@ss.hasPermi('office:code:edit')")
@Log(title = "转文章(南音)", businessType = BusinessType.UPDATE)
@GetMapping("/transToArticle1/{codeId}")
- public AjaxResult transToArticle1(@PathVariable Long codeId, @RequestParam String coverUrl) {
- String msg = ttCodeService.transToArticle1(codeId, coverUrl);
+ public AjaxResult transToArticle1(@PathVariable Long codeId, @RequestParam Long templateId) {
+ String msg = ttCodeService.transToArticle1(codeId, templateId);
return success(msg);
}
}
diff --git a/ruoyi-office/src/main/java/com/ruoyi/office/controller/TtCopyTemplateController.java b/ruoyi-office/src/main/java/com/ruoyi/office/controller/TtCopyTemplateController.java
index a618a68..e7fca4d 100644
--- a/ruoyi-office/src/main/java/com/ruoyi/office/controller/TtCopyTemplateController.java
+++ b/ruoyi-office/src/main/java/com/ruoyi/office/controller/TtCopyTemplateController.java
@@ -18,11 +18,9 @@ import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.BusinessType;
-import com.ruoyi.common.utils.HtmlUtils;
-import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.office.domain.TtCode;
-import com.ruoyi.office.domain.TtFile;
import com.ruoyi.office.domain.TtCopyTemplate;
+import com.ruoyi.office.service.CopyTemplateRenderer;
import com.ruoyi.office.service.ITtCodeService;
import com.ruoyi.office.service.ITtCopyTemplateService;
import com.ruoyi.common.utils.poi.ExcelUtil;
@@ -44,6 +42,9 @@ public class TtCopyTemplateController extends BaseController {
@Autowired
private ITtCodeService ttCodeService;
+ @Autowired
+ private CopyTemplateRenderer copyTemplateRenderer;
+
/**
* 查询文案模板列表
*/
@@ -134,53 +135,6 @@ public class TtCopyTemplateController extends BaseController {
if (code == null) {
return error("未找到名称为【" + codeName + "】的源码项目");
}
- // 3. 提取变量
- String fullCodeName = code.getCodeName() != null ? code.getCodeName() : "";
- String projectCode = "";
- String projectName = fullCodeName;
- if (fullCodeName.length() > 0) {
- java.util.regex.Matcher m = java.util.regex.Pattern.compile("【(.+?)】").matcher(fullCodeName);
- if (m.find()) {
- projectCode = m.group(1);
- }
- projectName = fullCodeName.replaceAll("【.+?】", "").trim();
- if (projectName.startsWith("基于")) {
- java.util.regex.Matcher nameMatcher = java.util.regex.Pattern.compile("实现的(.+?)$").matcher(projectName);
- if (nameMatcher.find()) {
- projectName = nameMatcher.group(1);
- }
- }
- }
- // 4. 生成纯文本描述
- String plainDesc = "";
- if (StringUtils.isNotEmpty(code.getCodeDesc())) {
- String codeDesc = code.getCodeDesc().replaceAll("
", "\n");
- plainDesc = HtmlUtils.htmlToText(codeDesc.trim());
- }
- // 5. 生成截图列表
- String screenshots = "";
- int idx = 0;
- if (code.getFileList() != null && !code.getFileList().isEmpty()) {
- for (TtFile item : code.getFileList()) {
- screenshots += (++idx) + "." + item.getFileName() + "\n + ")\n\n";
- }
- } else {
- screenshots = "请前往微信小程序:南音源码库。查看项目详情!\n\n";
- }
- // 6. 替换占位符
- String content = template.getTemplateBody();
- content = content.replace("{codeName}", fullCodeName);
- content = content.replace("{projectCode}", projectCode);
- 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);
+ return success(copyTemplateRenderer.render(template, code));
}
}
diff --git a/ruoyi-office/src/main/java/com/ruoyi/office/mapper/TtProjectInfoMapper.java b/ruoyi-office/src/main/java/com/ruoyi/office/mapper/TtProjectInfoMapper.java
index f016989..2254de5 100644
--- a/ruoyi-office/src/main/java/com/ruoyi/office/mapper/TtProjectInfoMapper.java
+++ b/ruoyi-office/src/main/java/com/ruoyi/office/mapper/TtProjectInfoMapper.java
@@ -67,4 +67,6 @@ public interface TtProjectInfoMapper
int updateProjectQuarkUrl(@Param("id") Integer id, @Param("projectUrl") String projectUrl);
int updateProjectBaiduUrl(@Param("id") Integer id, @Param("projectBaiduUrl") String projectBaiduUrl);
+
+ int updateProjectNumIfBlank(@Param("id") Integer id, @Param("projectNum") String projectNum);
}
diff --git a/ruoyi-office/src/main/java/com/ruoyi/office/service/CopyTemplateRenderer.java b/ruoyi-office/src/main/java/com/ruoyi/office/service/CopyTemplateRenderer.java
new file mode 100644
index 0000000..c565c7d
--- /dev/null
+++ b/ruoyi-office/src/main/java/com/ruoyi/office/service/CopyTemplateRenderer.java
@@ -0,0 +1,99 @@
+package com.ruoyi.office.service;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.springframework.stereotype.Component;
+
+import com.ruoyi.common.utils.HtmlUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.office.domain.TtCode;
+import com.ruoyi.office.domain.TtCopyTemplate;
+import com.ruoyi.office.domain.TtFile;
+
+/**
+ * 文案模板占位符渲染器。
+ */
+@Component
+public class CopyTemplateRenderer
+{
+ private static final Pattern PROJECT_CODE_PATTERN = Pattern.compile("【(.+?)】");
+ private static final Pattern PROJECT_NAME_PATTERN = Pattern.compile("实现的(.+?)$");
+
+ public String render(TtCopyTemplate template, TtCode code)
+ {
+ String codeName = value(code.getCodeName());
+ String projectCode = extractProjectCode(codeName);
+ String projectName = extractProjectName(codeName);
+ String content = value(template.getTemplateBody());
+
+ content = content.replace("{codeName}", codeName);
+ content = content.replace("{projectCode}", projectCode);
+ content = content.replace("{projectName}", projectName);
+ content = content.replace("{codeDesc}", toPlainDescription(code.getCodeDesc()));
+ content = content.replace("{codeEnvironment}", value(code.getCodeEnvironment()));
+ content = content.replace("{frontendTechnology}", value(code.getFrontendTechnology()));
+ content = content.replace("{backendTechnology}", value(code.getBackendTechnology()));
+ content = content.replace("{databaseTechnology}", value(code.getDatabaseTechnology()));
+ content = content.replace("{codeTechnology}", value(code.getCodeTechnology()));
+ content = content.replace("{diskLink}", value(code.getDiskLink()));
+ content = content.replace("{screenshots}", buildScreenshots(code.getFileList()));
+ return content;
+ }
+
+ private String extractProjectCode(String codeName)
+ {
+ Matcher matcher = PROJECT_CODE_PATTERN.matcher(codeName);
+ return matcher.find() ? matcher.group(1) : "";
+ }
+
+ private String extractProjectName(String codeName)
+ {
+ String projectName = PROJECT_CODE_PATTERN.matcher(codeName).replaceFirst("").trim();
+ if (projectName.startsWith("基于"))
+ {
+ Matcher matcher = PROJECT_NAME_PATTERN.matcher(projectName);
+ if (matcher.find())
+ {
+ return matcher.group(1);
+ }
+ }
+ return projectName;
+ }
+
+ private String toPlainDescription(String codeDesc)
+ {
+ if (StringUtils.isEmpty(codeDesc))
+ {
+ return "";
+ }
+ return HtmlUtils.htmlToText(codeDesc.replaceAll("(?i)", "\n").trim());
+ }
+
+ private String buildScreenshots(List fileList)
+ {
+ if (fileList == null || fileList.isEmpty())
+ {
+ return "请前往微信小程序:南音源码库。查看项目详情!\n\n";
+ }
+
+ StringBuilder screenshots = new StringBuilder();
+ int index = 0;
+ for (TtFile file : fileList)
+ {
+ screenshots.append(++index)
+ .append(".")
+ .append(value(file.getFileName()))
+ .append("\n
+ .append(value(file.getFileUrl()))
+ .append(")\n\n");
+ }
+ return screenshots.toString();
+ }
+
+ private String value(String value)
+ {
+ return value == null ? "" : value;
+ }
+}
diff --git a/ruoyi-office/src/main/java/com/ruoyi/office/service/ITtCodeService.java b/ruoyi-office/src/main/java/com/ruoyi/office/service/ITtCodeService.java
index 134f643..c62c413 100644
--- a/ruoyi-office/src/main/java/com/ruoyi/office/service/ITtCodeService.java
+++ b/ruoyi-office/src/main/java/com/ruoyi/office/service/ITtCodeService.java
@@ -67,16 +67,8 @@ public interface ITtCodeService {
*/
public int deleteTtCodeByCodeId(Long codeId);
- /**
- * 转文章
- *
- * @param codeId 源码管理主键
- * @return 结果
- */
- public String transToArticle(Long codeId);
-
/**
* 转文章(南音)
*/
- public String transToArticle1(Long codeId, String coverUrl);
+ public String transToArticle1(Long codeId, Long templateId);
}
diff --git a/ruoyi-office/src/main/java/com/ruoyi/office/service/impl/TtCodeServiceImpl.java b/ruoyi-office/src/main/java/com/ruoyi/office/service/impl/TtCodeServiceImpl.java
index 55ed399..f5f926e 100644
--- a/ruoyi-office/src/main/java/com/ruoyi/office/service/impl/TtCodeServiceImpl.java
+++ b/ruoyi-office/src/main/java/com/ruoyi/office/service/impl/TtCodeServiceImpl.java
@@ -9,15 +9,18 @@ import com.ruoyi.app.domain.AppResource;
import com.ruoyi.app.domain.AppResourceList;
import com.ruoyi.app.mapper.AppBlogArticleMapper;
import com.ruoyi.app.mapper.AppResourceMapper;
+import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
-import com.ruoyi.office.domain.TtArticles;
+import com.ruoyi.office.domain.TtCopyTemplate;
import com.ruoyi.office.domain.TtFile;
import com.ruoyi.office.domain.TtProjectInfo;
-import com.ruoyi.office.mapper.TtArticlesMapper;
+import com.ruoyi.office.mapper.TtCopyTemplateMapper;
import com.ruoyi.office.mapper.TtFileMapper;
import com.ruoyi.office.mapper.TtProjectInfoMapper;
+import com.ruoyi.office.service.CopyTemplateRenderer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.office.mapper.TtCodeMapper;
import com.ruoyi.office.domain.TtCode;
import com.ruoyi.office.service.ITtCodeService;
@@ -33,8 +36,6 @@ public class TtCodeServiceImpl implements ITtCodeService {
@Autowired
private TtCodeMapper ttCodeMapper;
@Autowired
- private TtArticlesMapper ttArticlesMapper;
- @Autowired
private TtFileMapper fileMapper;
@Autowired
private AppBlogArticleMapper appBlogArticleMapper;
@@ -42,6 +43,10 @@ public class TtCodeServiceImpl implements ITtCodeService {
private AppResourceMapper appResourceMapper;
@Autowired
private TtProjectInfoMapper ttProjectInfoMapper;
+ @Autowired
+ private TtCopyTemplateMapper ttCopyTemplateMapper;
+ @Autowired
+ private CopyTemplateRenderer copyTemplateRenderer;
/**
* 查询源码管理
@@ -131,105 +136,154 @@ public class TtCodeServiceImpl implements ITtCodeService {
}
@Override
- public String transToArticle(Long codeId) {
- TtCode ttCode = ttCodeMapper.selectTtCodeByCodeId(codeId);
- TtArticles ttArticles = new TtArticles();
- ttArticles.setTitle(ttCode.getCodeName());
- List list = ttArticlesMapper.selectTtArticlesList(ttArticles);
- if (!list.isEmpty()) {
- return "不能重复转文章!";
+ @Transactional(rollbackFor = Exception.class)
+ public String transToArticle1(Long codeId, Long templateId) {
+ TtCopyTemplate template = ttCopyTemplateMapper.selectTtCopyTemplateByTemplateId(templateId);
+ if (template == null || !"0".equals(template.getStatus())) {
+ throw new ServiceException("所选文案模板不存在或已停用");
}
- ttArticles.setAuthor("Feast");
- ttArticles.setCategory(ttCode.getPaymentType());
- if (ttCode.getPaymentType().equals("2")) {
- ttArticles.setAttachmentUrl(ttCode.getDiskLink());
- }
- StringBuffer content = new StringBuffer();
- content.append("### 项目描述
");
- content.append(ttCode.getCodeDesc());
- content.append("### 运行环境
");
- content.append(ttCode.getCodeEnvironment());
- content.append("### 其他技术
");
- content.append(ttCode.getCodeTechnology());
- ttArticles.setContent(content.toString());
- ttArticlesMapper.insertTtArticles(ttArticles);
- return "源码转文章成功!";
- }
-
- @Override
- public String transToArticle1(Long codeId, String coverUrl) {
TtCode ttCode = ttCodeMapper.selectTtCodeByCodeId(codeId);
+ if (ttCode == null) {
+ throw new ServiceException("源码项目不存在");
+ }
TtProjectInfo ttProjectInfo = ttProjectInfoMapper.selectTtProjectInfoByName(ttCode.getCodeName());
+ if (ttProjectInfo == null) {
+ throw new ServiceException("未找到对应的项目清单信息");
+ }
ttCode.setDiskLink(ttProjectInfo.getProjectBaiduUrl());
- ttCode.setPublishFlag("Y");
- ttCodeMapper.updateTtCode(ttCode);
- StringBuffer content = new StringBuffer();
- content.append("### 项目描述
");
- content.append(ttCode.getCodeDesc());
- content.append("### 运行环境
");
- content.append(ttCode.getCodeEnvironment());
- content.append("### 其他技术
");
- content.append(ttCode.getCodeTechnology());
- content.append("### 演示视频
");
- content.append("请移步首页-视频资源,搜索项目编号查看");
TtFile file = new TtFile();
file.setCodeName(ttCode.getCodeName());
List fileList = fileMapper.selectTtFileList(file);
- AppBlogArticle article = new AppBlogArticle();
- article.setTitle(ttCode.getCodeName());
- List list = appBlogArticleMapper.selectAppBlogArticleList(article);
- if (!list.isEmpty()) {
+ ttCode.setFileList(fileList);
+ String content = copyTemplateRenderer.render(template, ttCode);
+ String firstImageUrl = getFirstImageUrl(fileList);
+
+ AppBlogArticle existingArticle = appBlogArticleMapper.selectAppBlogArticleByTitle(ttCode.getCodeName());
+ if (existingArticle != null) {
+ if (repairIncompleteConversion(existingArticle, ttProjectInfo, content, firstImageUrl)) {
+ markCodePublished(ttCode);
+ return "源码转文章成功,已补全资源链接!";
+ }
return "不能重复转文章!";
}
+
//新增资源
AppResource resource = new AppResource();
- resource.setExplain(content.toString());
+ resource.setExplain(content);
resource.setResourceTitle(ttCode.getCodeName());
- if (!fileList.isEmpty()) {
- resource.setShowImg(fileList.get(0).getFileUrl());
+ if (!StringUtils.isEmpty(firstImageUrl)) {
+ resource.setShowImg(firstImageUrl);
}
resource.setResourceType(5L);
resource.setIsShow(0L);
resource.setIsAd(2L);
resource.setAdNumber(100L);
+ resource.setPriceFen(0);
resource.setCreateTime(new Date());
- resource.setShowImg(coverUrl);
appResourceMapper.insertAppResource(resource);
//新增文章
+ AppBlogArticle article = new AppBlogArticle();
+ article.setTitle(ttCode.getCodeName());
article.setArticleType(4L);
article.setIsRecommendation(1L);
article.setIsShow(1L);
article.setIsAd(1L);
- if (!fileList.isEmpty()) {
- article.setShowImg(fileList.get(0).getFileUrl());
+ if (!StringUtils.isEmpty(firstImageUrl)) {
+ article.setShowImg(firstImageUrl);
}
- article.setContentInfo(content.toString());
+ article.setContentInfo(content);
article.setAppResourceId(resource.getId());
article.setCreateTime(new Date());
- article.setShowImg(coverUrl);
appBlogArticleMapper.insertAppBlogArticle(article);
//新增资源网盘链接
- List resourceList = new ArrayList();
- //百度网盘
- if(!StringUtils.isEmpty(ttProjectInfo.getProjectBaiduUrl())){
- AppResourceList appResourceList = new AppResourceList();
- appResourceList.setAppResourceId(resource.getId());
- appResourceList.setListName("百度网盘");
- appResourceList.setListUrl(ttProjectInfo.getProjectBaiduUrl());
- resourceList.add(appResourceList);
- }
- //夸克网盘
- if(!StringUtils.isEmpty(ttProjectInfo.getProjectUrl())){
- AppResourceList appResourceList1 = new AppResourceList();
- appResourceList1.setAppResourceId(resource.getId());
- appResourceList1.setListName("夸克网盘");
- appResourceList1.setListUrl(ttProjectInfo.getProjectUrl());
- resourceList.add(appResourceList1);
- }
+ List resourceList = buildResourceList(ttProjectInfo, resource.getId());
if (resourceList.size() > 0)
{
appResourceMapper.batchAppResourceList(resourceList);
}
+ markCodePublished(ttCode);
return "源码转文章成功!";
}
+
+ /**
+ * 修复旧转换流程异常后已经生成文章、但尚未写入下载链接的半成品数据。
+ */
+ private boolean repairIncompleteConversion(AppBlogArticle article, TtProjectInfo projectInfo,
+ String content, String firstImageUrl)
+ {
+ if (article.getAppResourceId() == null)
+ {
+ return false;
+ }
+ AppResource resource = appResourceMapper.selectAppResourceById(article.getAppResourceId());
+ if (resource == null || (resource.getAppResourceListList() != null
+ && !resource.getAppResourceListList().isEmpty()))
+ {
+ return false;
+ }
+
+ List resourceList = buildResourceList(projectInfo, resource.getId());
+ if (resourceList.isEmpty())
+ {
+ return false;
+ }
+
+ resource.setExplain(content);
+ if (!StringUtils.isEmpty(firstImageUrl))
+ {
+ resource.setShowImg(firstImageUrl);
+ }
+ resource.setPriceFen(0);
+ appResourceMapper.updateAppResource(resource);
+
+ article.setContentInfo(content);
+ if (!StringUtils.isEmpty(firstImageUrl))
+ {
+ article.setShowImg(firstImageUrl);
+ }
+ appBlogArticleMapper.updateAppBlogArticle(article);
+ appResourceMapper.batchAppResourceList(resourceList);
+ return true;
+ }
+
+ private String getFirstImageUrl(List fileList)
+ {
+ if (fileList == null || fileList.isEmpty())
+ {
+ return null;
+ }
+ return fileList.get(0).getFileUrl();
+ }
+
+ private List buildResourceList(TtProjectInfo projectInfo, Long resourceId)
+ {
+ List resourceList = new ArrayList();
+ addResourceList(resourceList, resourceId, "百度网盘", projectInfo.getProjectBaiduUrl());
+ addResourceList(resourceList, resourceId, "夸克网盘", projectInfo.getProjectUrl());
+ return resourceList;
+ }
+
+ private void addResourceList(List resourceList, Long resourceId,
+ String name, String url)
+ {
+ if (StringUtils.isEmpty(url))
+ {
+ return;
+ }
+ AppResourceList item = new AppResourceList();
+ item.setAppResourceId(resourceId);
+ item.setListName(name);
+ item.setListUrl(url);
+ item.setPassword("");
+ item.setPriceFen(0);
+ item.setStatus(1);
+ item.setSortOrder(resourceList.size());
+ resourceList.add(item);
+ }
+
+ private void markCodePublished(TtCode ttCode)
+ {
+ ttCode.setPublishFlag("Y");
+ ttCodeMapper.updateTtCode(ttCode);
+ }
}
diff --git a/ruoyi-office/src/main/java/com/ruoyi/office/service/impl/TtProjectInfoServiceImpl.java b/ruoyi-office/src/main/java/com/ruoyi/office/service/impl/TtProjectInfoServiceImpl.java
index 048d86b..cd09694 100644
--- a/ruoyi-office/src/main/java/com/ruoyi/office/service/impl/TtProjectInfoServiceImpl.java
+++ b/ruoyi-office/src/main/java/com/ruoyi/office/service/impl/TtProjectInfoServiceImpl.java
@@ -39,7 +39,7 @@ import com.ruoyi.office.service.ITtProjectInfoService;
public class TtProjectInfoServiceImpl implements ITtProjectInfoService
{
private static final Pattern PROJECT_NUM_PATTERN =
- Pattern.compile("^[【\\[]\\s*(S\\d+)\\s*[】\\]]", Pattern.CASE_INSENSITIVE);
+ Pattern.compile("【\\s*([^】]+?)\\s*】");
private static final Pattern QUARK_URL_PATTERN =
Pattern.compile("https://pan\\.quark\\.cn/s/[A-Za-z0-9_-]+(?:\\?[^\\s\"'<>,。;]*)?");
@@ -203,6 +203,7 @@ public class TtProjectInfoServiceImpl implements ITtProjectInfoService
}
TtProjectInfo project = matches.get(0);
+ backfillProjectNumIfBlank(project, row.projectName);
String oldLink = NetDiskConstants.DISK_QUARK.equals(normalizedDiskType)
? project.getProjectUrl() : project.getProjectBaiduUrl();
if (StringUtils.equals(StringUtils.trimToEmpty(oldLink), row.linkUrl))
@@ -390,6 +391,20 @@ public class TtProjectInfoServiceImpl implements ITtProjectInfoService
return project;
}
+ private void backfillProjectNumIfBlank(TtProjectInfo project, String projectName)
+ {
+ if (StringUtils.isNotBlank(project.getProjectNum()))
+ {
+ return;
+ }
+ String projectNum = extractProjectNum(projectName);
+ if (StringUtils.isNotBlank(projectNum)
+ && ttProjectInfoMapper.updateProjectNumIfBlank(project.getId(), projectNum) == 1)
+ {
+ project.setProjectNum(projectNum);
+ }
+ }
+
private String extractProjectNum(String projectName)
{
Matcher matcher = PROJECT_NUM_PATTERN.matcher(projectName);
diff --git a/ruoyi-office/src/main/resources/mapper/office/TtProjectInfoMapper.xml b/ruoyi-office/src/main/resources/mapper/office/TtProjectInfoMapper.xml
index 6493380..6c79cf6 100644
--- a/ruoyi-office/src/main/resources/mapper/office/TtProjectInfoMapper.xml
+++ b/ruoyi-office/src/main/resources/mapper/office/TtProjectInfoMapper.xml
@@ -125,6 +125,13 @@
where id = #{id}
+
+ update tt_project_info
+ set project_num = #{projectNum}
+ where id = #{id}
+ and (project_num is null or trim(project_num) = '')
+
+
delete from tt_project_info where id = #{id}
diff --git a/ruoyi-office/src/test/java/com/ruoyi/office/service/CopyTemplateRendererTest.java b/ruoyi-office/src/test/java/com/ruoyi/office/service/CopyTemplateRendererTest.java
new file mode 100644
index 0000000..dd29675
--- /dev/null
+++ b/ruoyi-office/src/test/java/com/ruoyi/office/service/CopyTemplateRendererTest.java
@@ -0,0 +1,56 @@
+package com.ruoyi.office.service;
+
+import java.util.Arrays;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.ruoyi.office.domain.TtCode;
+import com.ruoyi.office.domain.TtCopyTemplate;
+import com.ruoyi.office.domain.TtFile;
+
+public class CopyTemplateRendererTest
+{
+ private final CopyTemplateRenderer renderer = new CopyTemplateRenderer();
+
+ @Test
+ public void rendersCodeFieldsAndScreenshots()
+ {
+ TtCopyTemplate template = new TtCopyTemplate();
+ template.setTemplateBody("{codeName}|{projectCode}|{projectName}|{codeDesc}|"
+ + "{codeEnvironment}|{frontendTechnology}|{backendTechnology}|"
+ + "{databaseTechnology}|{codeTechnology}|{diskLink}|{screenshots}");
+
+ TtCode code = new TtCode();
+ code.setCodeName("【S008】基于SpringBoot实现的爱心众筹系统");
+ code.setCodeDesc("第一段
第二段
");
+ code.setCodeEnvironment("JDK 17");
+ code.setFrontendTechnology("Vue3");
+ code.setBackendTechnology("SpringBoot");
+ code.setDatabaseTechnology("MySQL");
+ code.setCodeTechnology("ECharts");
+ code.setDiskLink("https://example.com/resource");
+
+ TtFile screenshot = new TtFile();
+ screenshot.setFileName("首页");
+ screenshot.setFileUrl("https://example.com/home.png");
+ code.setFileList(Arrays.asList(screenshot));
+
+ String content = renderer.render(template, code);
+
+ Assert.assertEquals("【S008】基于SpringBoot实现的爱心众筹系统|S008|爱心众筹系统|"
+ + "第一段\n第二段|JDK 17|Vue3|SpringBoot|MySQL|ECharts|"
+ + "https://example.com/resource|1.首页\n\n\n", content);
+ }
+
+ @Test
+ public void usesFallbackWhenThereAreNoScreenshots()
+ {
+ TtCopyTemplate template = new TtCopyTemplate();
+ template.setTemplateBody("{screenshots}");
+
+ String content = renderer.render(template, new TtCode());
+
+ Assert.assertEquals("请前往微信小程序:南音源码库。查看项目详情!\n\n", content);
+ }
+}
diff --git a/ruoyi-office/src/test/java/com/ruoyi/office/service/impl/TtCodeServiceImplTest.java b/ruoyi-office/src/test/java/com/ruoyi/office/service/impl/TtCodeServiceImplTest.java
new file mode 100644
index 0000000..0058782
--- /dev/null
+++ b/ruoyi-office/src/test/java/com/ruoyi/office/service/impl/TtCodeServiceImplTest.java
@@ -0,0 +1,152 @@
+package com.ruoyi.office.service.impl;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import com.ruoyi.app.domain.AppBlogArticle;
+import com.ruoyi.app.domain.AppResource;
+import com.ruoyi.app.domain.AppResourceList;
+import com.ruoyi.app.mapper.AppBlogArticleMapper;
+import com.ruoyi.app.mapper.AppResourceMapper;
+import com.ruoyi.office.domain.TtCode;
+import com.ruoyi.office.domain.TtCopyTemplate;
+import com.ruoyi.office.domain.TtFile;
+import com.ruoyi.office.domain.TtProjectInfo;
+import com.ruoyi.office.mapper.TtCodeMapper;
+import com.ruoyi.office.mapper.TtCopyTemplateMapper;
+import com.ruoyi.office.mapper.TtFileMapper;
+import com.ruoyi.office.mapper.TtProjectInfoMapper;
+import com.ruoyi.office.service.CopyTemplateRenderer;
+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 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 TtCodeServiceImplTest
+{
+ @InjectMocks
+ private TtCodeServiceImpl service;
+
+ @Mock
+ private TtCodeMapper codeMapper;
+ @Mock
+ private TtFileMapper fileMapper;
+ @Mock
+ private AppBlogArticleMapper articleMapper;
+ @Mock
+ private AppResourceMapper resourceMapper;
+ @Mock
+ private TtProjectInfoMapper projectInfoMapper;
+ @Mock
+ private TtCopyTemplateMapper copyTemplateMapper;
+ @Mock
+ private CopyTemplateRenderer templateRenderer;
+
+ private TtCode code;
+ private TtProjectInfo projectInfo;
+
+ @Before
+ public void setUp()
+ {
+ TtCopyTemplate template = new TtCopyTemplate();
+ template.setTemplateId(1L);
+ template.setStatus("0");
+
+ code = new TtCode();
+ code.setCodeId(9L);
+ code.setCodeName("【S009】自习室预约选座系统");
+
+ projectInfo = new TtProjectInfo();
+ projectInfo.setProjectBaiduUrl("https://pan.baidu.com/s/example");
+ projectInfo.setProjectUrl("https://pan.quark.cn/s/example");
+
+ TtFile firstImage = new TtFile();
+ firstImage.setFileName("第一张");
+ firstImage.setFileUrl("https://img.example.com/first.png");
+ TtFile secondImage = new TtFile();
+ secondImage.setFileName("第二张");
+ secondImage.setFileUrl("https://img.example.com/second.png");
+
+ when(copyTemplateMapper.selectTtCopyTemplateByTemplateId(1L)).thenReturn(template);
+ when(codeMapper.selectTtCodeByCodeId(9L)).thenReturn(code);
+ when(projectInfoMapper.selectTtProjectInfoByName(code.getCodeName())).thenReturn(projectInfo);
+ when(fileMapper.selectTtFileList(any())).thenReturn(Arrays.asList(firstImage, secondImage));
+ when(templateRenderer.render(template, code)).thenReturn("模板正文");
+ }
+
+ @Test
+ public void fillsRequiredResourceListFields()
+ {
+ when(articleMapper.selectAppBlogArticleByTitle(code.getCodeName())).thenReturn(null);
+ when(resourceMapper.insertAppResource(any())).thenAnswer(invocation -> {
+ AppResource resource = invocation.getArgument(0);
+ resource.setId(100L);
+ return 1;
+ });
+
+ service.transToArticle1(9L, 1L);
+
+ ArgumentCaptor resourceCaptor = ArgumentCaptor.forClass(AppResource.class);
+ verify(resourceMapper).insertAppResource(resourceCaptor.capture());
+ Assert.assertEquals(Integer.valueOf(0), resourceCaptor.getValue().getPriceFen());
+ Assert.assertEquals("https://img.example.com/first.png", resourceCaptor.getValue().getShowImg());
+
+ ArgumentCaptor articleCaptor = ArgumentCaptor.forClass(AppBlogArticle.class);
+ verify(articleMapper).insertAppBlogArticle(articleCaptor.capture());
+ Assert.assertEquals("https://img.example.com/first.png", articleCaptor.getValue().getShowImg());
+
+ ArgumentCaptor listCaptor = ArgumentCaptor.forClass(List.class);
+ verify(resourceMapper).batchAppResourceList(listCaptor.capture());
+ List items = listCaptor.getValue();
+ Assert.assertEquals(2, items.size());
+ assertResourceListDefaults(items.get(0), 0);
+ assertResourceListDefaults(items.get(1), 1);
+ }
+
+ @Test
+ public void repairsArticleLeftByPreviousFailedConversion()
+ {
+ AppBlogArticle existingArticle = new AppBlogArticle();
+ existingArticle.setId(200L);
+ existingArticle.setTitle(code.getCodeName());
+ existingArticle.setAppResourceId(100L);
+
+ AppResource existingResource = new AppResource();
+ existingResource.setId(100L);
+ existingResource.setAppResourceListList(new ArrayList());
+
+ when(articleMapper.selectAppBlogArticleByTitle(code.getCodeName())).thenReturn(existingArticle);
+ when(resourceMapper.selectAppResourceById(100L)).thenReturn(existingResource);
+
+ String message = service.transToArticle1(9L, 1L);
+
+ Assert.assertEquals("源码转文章成功,已补全资源链接!", message);
+ Assert.assertEquals("https://img.example.com/first.png", existingResource.getShowImg());
+ Assert.assertEquals("https://img.example.com/first.png", existingArticle.getShowImg());
+ verify(resourceMapper).updateAppResource(existingResource);
+ verify(articleMapper).updateAppBlogArticle(existingArticle);
+ verify(resourceMapper).batchAppResourceList(any());
+ verify(resourceMapper, never()).insertAppResource(any());
+ verify(articleMapper, never()).insertAppBlogArticle(any());
+ }
+
+ private void assertResourceListDefaults(AppResourceList item, int sortOrder)
+ {
+ Assert.assertEquals(Integer.valueOf(0), item.getPriceFen());
+ Assert.assertEquals(Integer.valueOf(1), item.getStatus());
+ Assert.assertEquals(Integer.valueOf(sortOrder), item.getSortOrder());
+ Assert.assertEquals(Long.valueOf(100L), item.getAppResourceId());
+ Assert.assertEquals("", item.getPassword());
+ }
+}
diff --git a/ruoyi-office/src/test/java/com/ruoyi/office/service/impl/TtProjectInfoServiceImplTest.java b/ruoyi-office/src/test/java/com/ruoyi/office/service/impl/TtProjectInfoServiceImplTest.java
index 8ae81b1..3fda126 100644
--- a/ruoyi-office/src/test/java/com/ruoyi/office/service/impl/TtProjectInfoServiceImplTest.java
+++ b/ruoyi-office/src/test/java/com/ruoyi/office/service/impl/TtProjectInfoServiceImplTest.java
@@ -77,6 +77,43 @@ public class TtProjectInfoServiceImplTest
Assert.assertEquals(1, result.getAddedCount());
}
+ @Test
+ public void extractsAnyTextInsideFullWidthBracketsAsProjectNumber()
+ {
+ quarkRow.setProjectName("课程项目【 K001 】药店进销存系统");
+ 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);
+
+ service.importProjectLinks(file, "QUARK");
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(TtProjectInfo.class);
+ verify(projectInfoMapper).insertTtProjectInfo(captor.capture());
+ Assert.assertEquals("K001", captor.getValue().getProjectNum());
+ }
+
+ @Test
+ public void backfillsProjectNumberForExistingProjectWhenBlank()
+ {
+ quarkRow.setProjectName("【K001】药店进销存系统");
+ TtProjectInfo existing = new TtProjectInfo();
+ existing.setId(1);
+ existing.setProjectName("【K001】药店进销存系统");
+ existing.setProjectUrl("https://pan.quark.cn/s/741bf63df1bb");
+
+ when(parser.parse(file, "QUARK")).thenReturn(Collections.singletonList(quarkRow));
+ when(projectInfoMapper.selectTtProjectInfoList(any(TtProjectInfo.class)))
+ .thenReturn(Collections.singletonList(existing));
+ when(projectInfoMapper.updateProjectNumIfBlank(1, "K001")).thenReturn(1);
+
+ ProjectLinkImportResult result = service.importProjectLinks(file, "QUARK");
+
+ verify(projectInfoMapper).updateProjectNumIfBlank(1, "K001");
+ Assert.assertEquals("K001", existing.getProjectNum());
+ Assert.assertEquals(1, result.getUnchangedCount());
+ }
+
@Test
public void updatesOnlyBaiduLinkForExistingProjectAndAddsPasscode()
{
@@ -89,6 +126,7 @@ public class TtProjectInfoServiceImplTest
TtProjectInfo existing = new TtProjectInfo();
existing.setId(31);
+ existing.setProjectNum("S031");
existing.setProjectName("【S031】婚纱摄影管理系统");
existing.setProjectName1("原源码名称");
existing.setProjectUrl("https://pan.quark.cn/s/old");
diff --git a/ruoyi-system/src/main/java/com/ruoyi/app/mapper/AppBlogArticleMapper.java b/ruoyi-system/src/main/java/com/ruoyi/app/mapper/AppBlogArticleMapper.java
index 6732180..140e9c8 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/app/mapper/AppBlogArticleMapper.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/app/mapper/AppBlogArticleMapper.java
@@ -21,6 +21,14 @@ public interface AppBlogArticleMapper
*/
public AppBlogArticle selectAppBlogArticleById(Long id);
+ /**
+ * 根据完整标题精确查询文章。
+ *
+ * @param title 文章标题
+ * @return 文章
+ */
+ public AppBlogArticle selectAppBlogArticleByTitle(String title);
+
/**
* 查询文章列表
*
diff --git a/ruoyi-system/src/main/resources/mapper/app/AppBlogArticleMapper.xml b/ruoyi-system/src/main/resources/mapper/app/AppBlogArticleMapper.xml
index 0e19e7c..bba8528 100644
--- a/ruoyi-system/src/main/resources/mapper/app/AppBlogArticleMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/app/AppBlogArticleMapper.xml
@@ -91,6 +91,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where id = #{id}
+
+
insert into app_blog_article
@@ -168,4 +174,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{id}
-
\ No newline at end of file
+
diff --git a/ruoyi-ui/src/api/office/code.js b/ruoyi-ui/src/api/office/code.js
index adce93d..016d597 100644
--- a/ruoyi-ui/src/api/office/code.js
+++ b/ruoyi-ui/src/api/office/code.js
@@ -43,20 +43,12 @@ export function delCode(codeId) {
})
}
-export function transToArticle(codeId) {
- return request({
- url: '/office/code/transToArticle/' + codeId,
- method: 'get'
- })
-}
-
-export function transToArticle1(codeId, coverUrl) {
+export function transToArticle1(codeId, templateId) {
return request({
url: '/office/code/transToArticle1/' + codeId,
method: 'get',
params: {
- coverUrl: coverUrl
+ templateId: templateId
}
})
}
-
diff --git a/ruoyi-ui/src/views/office/code/index.vue b/ruoyi-ui/src/views/office/code/index.vue
index 9e11956..3a328a9 100644
--- a/ruoyi-ui/src/views/office/code/index.vue
+++ b/ruoyi-ui/src/views/office/code/index.vue
@@ -115,14 +115,6 @@
- 转文章
-
+
+
+
+
+
+
+
加载中...
@@ -276,11 +284,10 @@