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![](" + item.getFileUrl() + ")\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![](https://example.com/home.png)\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 @@