feat: 优化代码生成及文案模板渲染

This commit is contained in:
王鹏
2026-07-29 16:17:49 +08:00
parent c631d058c1
commit e9d627df9c
15 changed files with 560 additions and 199 deletions

View File

@@ -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);
}
}

View File

@@ -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("</p>", "</p>\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));
}
}

View File

@@ -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);
}

View File

@@ -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)</p>", "</p>\n").trim());
}
private String buildScreenshots(List<TtFile> 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;
}
}

View File

@@ -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);
}

View File

@@ -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<TtArticles> 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("<p><strong>### 项目描述</strong></p>");
content.append(ttCode.getCodeDesc());
content.append("<p><strong>### 运行环境</strong></p>");
content.append(ttCode.getCodeEnvironment());
content.append("<p><strong>### 其他技术</strong></p>");
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("<p><strong>### 项目描述</strong></p>");
content.append(ttCode.getCodeDesc());
content.append("<p><strong>### 运行环境</strong></p>");
content.append(ttCode.getCodeEnvironment());
content.append("<p><strong>### 其他技术</strong></p>");
content.append(ttCode.getCodeTechnology());
content.append("<p><strong>### 演示视频</strong></p>");
content.append("请移步首页-<strong>视频资源</strong>,搜索<strong>项目编号</strong>查看");
TtFile file = new TtFile();
file.setCodeName(ttCode.getCodeName());
List<TtFile> fileList = fileMapper.selectTtFileList(file);
AppBlogArticle article = new AppBlogArticle();
article.setTitle(ttCode.getCodeName());
List<AppBlogArticle> 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<AppResourceList> resourceList = new ArrayList<AppResourceList>();
//百度网盘
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<AppResourceList> 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<AppResourceList> 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<TtFile> fileList)
{
if (fileList == null || fileList.isEmpty())
{
return null;
}
return fileList.get(0).getFileUrl();
}
private List<AppResourceList> buildResourceList(TtProjectInfo projectInfo, Long resourceId)
{
List<AppResourceList> resourceList = new ArrayList<AppResourceList>();
addResourceList(resourceList, resourceId, "百度网盘", projectInfo.getProjectBaiduUrl());
addResourceList(resourceList, resourceId, "夸克网盘", projectInfo.getProjectUrl());
return resourceList;
}
private void addResourceList(List<AppResourceList> 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);
}
}

View File

@@ -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);

View File

@@ -125,6 +125,13 @@
where id = #{id}
</update>
<update id="updateProjectNumIfBlank">
update tt_project_info
set project_num = #{projectNum}
where id = #{id}
and (project_num is null or trim(project_num) = '')
</update>
<delete id="deleteTtProjectInfoById" parameterType="Integer">
delete from tt_project_info where id = #{id}
</delete>

View File

@@ -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("<p>第一段</p><p>第二段</p>");
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);
}
}

View File

@@ -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<AppResource> 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<AppBlogArticle> articleCaptor = ArgumentCaptor.forClass(AppBlogArticle.class);
verify(articleMapper).insertAppBlogArticle(articleCaptor.capture());
Assert.assertEquals("https://img.example.com/first.png", articleCaptor.getValue().getShowImg());
ArgumentCaptor<List> listCaptor = ArgumentCaptor.forClass(List.class);
verify(resourceMapper).batchAppResourceList(listCaptor.capture());
List<AppResourceList> 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<AppResourceList>());
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());
}
}

View File

@@ -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<TtProjectInfo> 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");

View File

@@ -21,6 +21,14 @@ public interface AppBlogArticleMapper
*/
public AppBlogArticle selectAppBlogArticleById(Long id);
/**
* 根据完整标题精确查询文章。
*
* @param title 文章标题
* @return 文章
*/
public AppBlogArticle selectAppBlogArticleByTitle(String title);
/**
* 查询文章列表
*

View File

@@ -91,6 +91,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<include refid="selectAppBlogArticleVo"/>
where id = #{id}
</select>
<select id="selectAppBlogArticleByTitle" parameterType="String" resultMap="AppBlogArticleResult">
<include refid="selectAppBlogArticleVo"/>
where title = #{title}
limit 1
</select>
<insert id="insertAppBlogArticle" parameterType="AppBlogArticle" useGeneratedKeys="true" keyProperty="id">
insert into app_blog_article
@@ -168,4 +174,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{id}
</foreach>
</delete>
</mapper>
</mapper>

View File

@@ -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
}
})
}

View File

@@ -115,14 +115,6 @@
<!-- <el-table-column label="视频文件" align="center" prop="videoFile" />-->
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-document-copy"
@click="transToArticle(scope.row)"
v-hasPermi="['office:code:edit']"
>转文章
</el-button>
<el-button
size="mini"
type="text"
@@ -262,6 +254,22 @@
<!-- 封面预览对话框 -->
<el-dialog :title="'封面预览'" :visible.sync="coverDialogVisible" width="500px" append-to-body>
<el-form label-width="80px">
<el-form-item label="文案模板">
<el-select
v-model="articleTemplateId"
placeholder="请选择文案模板"
style="width: 100%"
>
<el-option
v-for="tpl in copyTemplates"
:key="tpl.templateId"
:label="tpl.templateName"
:value="tpl.templateId"
/>
</el-select>
</el-form-item>
</el-form>
<div class="cover-preview" style="text-align: center;">
<img :src="coverImageUrl" style="max-width: 100%; height: auto;" v-if="coverImageUrl"/>
<div v-else>加载中...</div>
@@ -276,11 +284,10 @@
</template>
<script>
import {listCode, getCode, delCode, addCode, updateCode, transToArticle, transToArticle1} from "@/api/office/code";
import {listCode, getCode, delCode, addCode, updateCode, transToArticle1} from "@/api/office/code";
import {listEnabledTemplates} from "@/api/office/copyTemplate";
import {html2Text} from "../../../utils";
import {getToken} from "@/utils/auth";
import request from '@/utils/request';
export default {
name: "Code",
@@ -337,7 +344,9 @@
currentFileName: '',
currentRow: '',
// 文案模板列表(动态按钮)
copyTemplates: []
copyTemplates: [],
// 转文章时使用的文案模板
articleTemplateId: null
};
},
created() {
@@ -448,8 +457,21 @@
loadCopyTemplates() {
listEnabledTemplates().then(response => {
this.copyTemplates = response.data || [];
this.selectDefaultArticleTemplate();
});
},
selectDefaultArticleTemplate() {
if (!this.copyTemplates.length) {
this.articleTemplateId = null;
return;
}
const selectedExists = this.copyTemplates.some(tpl => tpl.templateId === this.articleTemplateId);
if (selectedExists) {
return;
}
const defaultTemplate = this.copyTemplates.find(tpl => tpl.templateName === '南音');
this.articleTemplateId = (defaultTemplate || this.copyTemplates[0]).templateId;
},
cancelDetail() {
this.openDetail = false;
this.reset();
@@ -507,14 +529,9 @@
this.title = "查看源码信息";
});
},
transToArticle(row) {
transToArticle(row.codeId).then(response => {
console.log(response)
this.$modal.msgSuccess(response.msg);
});
},
showCoverDialog(row) {
this.currentRow = row
this.selectDefaultArticleTemplate()
const id = row.codeId || this.ids
const baseUrl = process.env.VUE_APP_BASE_API
fetch(`${baseUrl}/office/project/cover/${id}`, {
@@ -564,36 +581,17 @@
this.$modal.msgSuccess('封面图片已下载')
},
handleTransToArticle1() {
// 将 Blob 转换为 File 对象
const file = new File([this.previewBlob], this.currentFileName, { type: 'image/png' });
// 创建 FormData
const formData = new FormData();
formData.append('file', file);
// 上传到七牛云
request({
url: '/common/qiniuUpload',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
}).then(response => {
if (response.code === 200) {
// 获取上传后的URL
const imageUrl = response.url;
// 调用转文章接口
transToArticle1(this.currentRow.codeId, imageUrl).then(response => {
this.$modal.msgSuccess(response.msg);
this.getList();
});
} else {
this.$modal.msgError('封面图片上传失败');
}
if (!this.articleTemplateId) {
this.$modal.msgError('请选择文案模板');
return;
}
transToArticle1(this.currentRow.codeId, this.articleTemplateId).then(response => {
this.$modal.msgSuccess(response.msg);
this.getList();
}).catch(error => {
console.error('转文章失败:', error);
this.$modal.msgError('转文章失败');
});
this.coverDialogVisible = false;
},
/** 提交按钮 */