From 4bda30f9c2f6b12b8aec8b821502669ab53603f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E9=B9=8F?= Date: Thu, 30 Jul 2026 14:25:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81S/K=E7=B2=BE=E5=93=81?= =?UTF-8?q?=E6=BA=90=E7=A0=81=E4=BB=98=E8=B4=B9=E8=A7=84=E6=A0=BC=E8=BD=AC?= =?UTF-8?q?=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/TtCodeServiceImpl.java | 134 +++++++++++++++--- .../service/impl/TtCodeServiceImplTest.java | 128 ++++++++++++++--- 2 files changed, 223 insertions(+), 39 deletions(-) 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 f5f926e..74f7b19 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 @@ -3,12 +3,16 @@ package com.ruoyi.office.service.impl; import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; 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.app.service.IAppResourceService; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.office.domain.TtCopyTemplate; @@ -33,6 +37,18 @@ import com.ruoyi.office.service.ITtCodeService; */ @Service public class TtCodeServiceImpl implements ITtCodeService { + private static final Pattern PREMIUM_PROJECT_CODE_PATTERN = + Pattern.compile("(?i)(?:^|【)\\s*([SK])\\s*\\d+"); + private static final long SOURCE_SHARE_RESOURCE_TYPE = 5L; + private static final long PREMIUM_PROJECT_RESOURCE_TYPE = 6L; + private static final long SOURCE_SHARE_ARTICLE_TYPE = 4L; + private static final long PREMIUM_SOURCE_ARTICLE_TYPE = 7L; + private static final long POINTS_ACCESS_TYPE = 2L; + private static final long PAID_ACCESS_TYPE = 3L; + private static final String BASIC_SPEC_NAME = "源码 + 数据库 + 论文 + 答辩PPT"; + private static final String DEPLOYMENT_SPEC_NAME = "源码 + 数据库 + 论文 + 答辩PPT + 项目部署"; + private static final String DEPLOYMENT_CONTACT = "调试部署加微信:forfeastcoding"; + @Autowired private TtCodeMapper ttCodeMapper; @Autowired @@ -42,6 +58,8 @@ public class TtCodeServiceImpl implements ITtCodeService { @Autowired private AppResourceMapper appResourceMapper; @Autowired + private IAppResourceService appResourceService; + @Autowired private TtProjectInfoMapper ttProjectInfoMapper; @Autowired private TtCopyTemplateMapper ttCopyTemplateMapper; @@ -150,7 +168,13 @@ public class TtCodeServiceImpl implements ITtCodeService { if (ttProjectInfo == null) { throw new ServiceException("未找到对应的项目清单信息"); } - ttCode.setDiskLink(ttProjectInfo.getProjectBaiduUrl()); + String projectSeries = resolveProjectSeries(ttCode, ttProjectInfo); + boolean premiumProject = isPremiumProject(projectSeries); + if (premiumProject && StringUtils.isEmpty(ttProjectInfo.getProjectUrl())) { + throw new ServiceException("该项目未配置夸克网盘链接,无法生成付费规格"); + } + ttCode.setDiskLink(premiumProject + ? ttProjectInfo.getProjectUrl() : ttProjectInfo.getProjectBaiduUrl()); TtFile file = new TtFile(); file.setCodeName(ttCode.getCodeName()); List fileList = fileMapper.selectTtFileList(file); @@ -160,7 +184,8 @@ public class TtCodeServiceImpl implements ITtCodeService { AppBlogArticle existingArticle = appBlogArticleMapper.selectAppBlogArticleByTitle(ttCode.getCodeName()); if (existingArticle != null) { - if (repairIncompleteConversion(existingArticle, ttProjectInfo, content, firstImageUrl)) { + if (repairIncompleteConversion(existingArticle, ttProjectInfo, content, + firstImageUrl, projectSeries)) { markCodePublished(ttCode); return "源码转文章成功,已补全资源链接!"; } @@ -174,17 +199,16 @@ public class TtCodeServiceImpl implements ITtCodeService { if (!StringUtils.isEmpty(firstImageUrl)) { resource.setShowImg(firstImageUrl); } - resource.setResourceType(5L); + configureResource(resource, projectSeries); resource.setIsShow(0L); - resource.setIsAd(2L); - resource.setAdNumber(100L); - resource.setPriceFen(0); - resource.setCreateTime(new Date()); - appResourceMapper.insertAppResource(resource); + List resourceList = buildResourceList(ttProjectInfo, null, projectSeries); + resource.setAppResourceListList(resourceList); + appResourceService.insertAppResource(resource); //新增文章 AppBlogArticle article = new AppBlogArticle(); article.setTitle(ttCode.getCodeName()); - article.setArticleType(4L); + article.setArticleType(premiumProject + ? PREMIUM_SOURCE_ARTICLE_TYPE : SOURCE_SHARE_ARTICLE_TYPE); article.setIsRecommendation(1L); article.setIsShow(1L); article.setIsAd(1L); @@ -195,12 +219,6 @@ public class TtCodeServiceImpl implements ITtCodeService { article.setAppResourceId(resource.getId()); article.setCreateTime(new Date()); appBlogArticleMapper.insertAppBlogArticle(article); - //新增资源网盘链接 - List resourceList = buildResourceList(ttProjectInfo, resource.getId()); - if (resourceList.size() > 0) - { - appResourceMapper.batchAppResourceList(resourceList); - } markCodePublished(ttCode); return "源码转文章成功!"; } @@ -209,7 +227,8 @@ public class TtCodeServiceImpl implements ITtCodeService { * 修复旧转换流程异常后已经生成文章、但尚未写入下载链接的半成品数据。 */ private boolean repairIncompleteConversion(AppBlogArticle article, TtProjectInfo projectInfo, - String content, String firstImageUrl) + String content, String firstImageUrl, + String projectSeries) { if (article.getAppResourceId() == null) { @@ -222,7 +241,8 @@ public class TtCodeServiceImpl implements ITtCodeService { return false; } - List resourceList = buildResourceList(projectInfo, resource.getId()); + List resourceList = buildResourceList( + projectInfo, resource.getId(), projectSeries); if (resourceList.isEmpty()) { return false; @@ -233,16 +253,20 @@ public class TtCodeServiceImpl implements ITtCodeService { { resource.setShowImg(firstImageUrl); } - resource.setPriceFen(0); - appResourceMapper.updateAppResource(resource); + configureResource(resource, projectSeries); + resource.setAppResourceListList(resourceList); + appResourceService.updateAppResource(resource); article.setContentInfo(content); if (!StringUtils.isEmpty(firstImageUrl)) { article.setShowImg(firstImageUrl); } + if (isPremiumProject(projectSeries)) + { + article.setArticleType(PREMIUM_SOURCE_ARTICLE_TYPE); + } appBlogArticleMapper.updateAppBlogArticle(article); - appResourceMapper.batchAppResourceList(resourceList); return true; } @@ -255,9 +279,26 @@ public class TtCodeServiceImpl implements ITtCodeService { return fileList.get(0).getFileUrl(); } - private List buildResourceList(TtProjectInfo projectInfo, Long resourceId) + private List buildResourceList(TtProjectInfo projectInfo, Long resourceId, + String projectSeries) { List resourceList = new ArrayList(); + if ("K".equals(projectSeries)) + { + addResourceList(resourceList, resourceId, BASIC_SPEC_NAME, + projectInfo.getProjectUrl(), 1900, 1); + addResourceList(resourceList, resourceId, DEPLOYMENT_SPEC_NAME, + DEPLOYMENT_CONTACT, 6600, 2); + return resourceList; + } + if ("S".equals(projectSeries)) + { + addResourceList(resourceList, resourceId, BASIC_SPEC_NAME, + projectInfo.getProjectUrl(), 5900, 1); + addResourceList(resourceList, resourceId, DEPLOYMENT_SPEC_NAME, + DEPLOYMENT_CONTACT, 9900, 2); + return resourceList; + } addResourceList(resourceList, resourceId, "百度网盘", projectInfo.getProjectBaiduUrl()); addResourceList(resourceList, resourceId, "夸克网盘", projectInfo.getProjectUrl()); return resourceList; @@ -281,6 +322,57 @@ public class TtCodeServiceImpl implements ITtCodeService { resourceList.add(item); } + private void addResourceList(List resourceList, Long resourceId, + String name, String url, int priceFen, int sortOrder) + { + AppResourceList item = new AppResourceList(); + item.setAppResourceId(resourceId); + item.setListName(name); + item.setListUrl(url); + item.setPassword(""); + item.setPriceFen(priceFen); + item.setStatus(1); + item.setSortOrder(sortOrder); + resourceList.add(item); + } + + private void configureResource(AppResource resource, String projectSeries) + { + if (isPremiumProject(projectSeries)) + { + resource.setResourceType(PREMIUM_PROJECT_RESOURCE_TYPE); + resource.setIsAd(PAID_ACCESS_TYPE); + resource.setAdNumber(0L); + resource.setPriceFen("K".equals(projectSeries) ? 1900 : 5900); + return; + } + resource.setResourceType(SOURCE_SHARE_RESOURCE_TYPE); + resource.setIsAd(POINTS_ACCESS_TYPE); + resource.setAdNumber(100L); + resource.setPriceFen(0); + } + + private boolean isPremiumProject(String projectSeries) + { + return "S".equals(projectSeries) || "K".equals(projectSeries); + } + + private String resolveProjectSeries(TtCode code, TtProjectInfo projectInfo) + { + String projectSeries = extractProjectSeries(projectInfo.getProjectNum()); + return projectSeries == null ? extractProjectSeries(code.getCodeName()) : projectSeries; + } + + private String extractProjectSeries(String value) + { + if (StringUtils.isEmpty(value)) + { + return null; + } + Matcher matcher = PREMIUM_PROJECT_CODE_PATTERN.matcher(value); + return matcher.find() ? matcher.group(1).toUpperCase(Locale.ROOT) : null; + } + private void markCodePublished(TtCode ttCode) { ttCode.setPublishFlag("Y"); 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 index 0058782..9f5378e 100644 --- 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 @@ -9,6 +9,8 @@ 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.app.service.IAppResourceService; +import com.ruoyi.common.exception.ServiceException; import com.ruoyi.office.domain.TtCode; import com.ruoyi.office.domain.TtCopyTemplate; import com.ruoyi.office.domain.TtFile; @@ -47,6 +49,8 @@ public class TtCodeServiceImplTest @Mock private AppResourceMapper resourceMapper; @Mock + private IAppResourceService resourceService; + @Mock private TtProjectInfoMapper projectInfoMapper; @Mock private TtCopyTemplateMapper copyTemplateMapper; @@ -68,6 +72,7 @@ public class TtCodeServiceImplTest code.setCodeName("【S009】自习室预约选座系统"); projectInfo = new TtProjectInfo(); + projectInfo.setProjectNum("S009"); projectInfo.setProjectBaiduUrl("https://pan.baidu.com/s/example"); projectInfo.setProjectUrl("https://pan.quark.cn/s/example"); @@ -80,16 +85,17 @@ public class TtCodeServiceImplTest when(copyTemplateMapper.selectTtCopyTemplateByTemplateId(1L)).thenReturn(template); when(codeMapper.selectTtCodeByCodeId(9L)).thenReturn(code); - when(projectInfoMapper.selectTtProjectInfoByName(code.getCodeName())).thenReturn(projectInfo); + when(projectInfoMapper.selectTtProjectInfoByName(any())).thenReturn(projectInfo); when(fileMapper.selectTtFileList(any())).thenReturn(Arrays.asList(firstImage, secondImage)); when(templateRenderer.render(template, code)).thenReturn("模板正文"); } @Test - public void fillsRequiredResourceListFields() + public void createsSPremiumProjectWithPaidSpecs() { + projectInfo.setProjectNum(null); when(articleMapper.selectAppBlogArticleByTitle(code.getCodeName())).thenReturn(null); - when(resourceMapper.insertAppResource(any())).thenAnswer(invocation -> { + when(resourceService.insertAppResource(any())).thenAnswer(invocation -> { AppResource resource = invocation.getArgument(0); resource.setId(100L); return 1; @@ -98,20 +104,45 @@ public class TtCodeServiceImplTest 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()); + verify(resourceService).insertAppResource(resourceCaptor.capture()); + AppResource resource = resourceCaptor.getValue(); + Assert.assertEquals(Long.valueOf(6L), resource.getResourceType()); + Assert.assertEquals(Long.valueOf(3L), resource.getIsAd()); + Assert.assertEquals(Integer.valueOf(5900), resource.getPriceFen()); + Assert.assertEquals("https://img.example.com/first.png", resource.getShowImg()); + Assert.assertEquals("https://pan.quark.cn/s/example", code.getDiskLink()); + assertPremiumSpecs(resource.getAppResourceListList(), 5900, 9900); ArgumentCaptor articleCaptor = ArgumentCaptor.forClass(AppBlogArticle.class); verify(articleMapper).insertAppBlogArticle(articleCaptor.capture()); Assert.assertEquals("https://img.example.com/first.png", articleCaptor.getValue().getShowImg()); + Assert.assertEquals(Long.valueOf(7L), articleCaptor.getValue().getArticleType()); + } - 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 createsKPremiumProjectWithPaidSpecs() + { + code.setCodeName("【K001】药店进销存系统"); + projectInfo.setProjectNum(" k001 "); + when(resourceService.insertAppResource(any())).thenAnswer(invocation -> { + AppResource resource = invocation.getArgument(0); + resource.setId(101L); + return 1; + }); + + service.transToArticle1(9L, 1L); + + ArgumentCaptor resourceCaptor = ArgumentCaptor.forClass(AppResource.class); + verify(resourceService).insertAppResource(resourceCaptor.capture()); + AppResource resource = resourceCaptor.getValue(); + Assert.assertEquals(Long.valueOf(6L), resource.getResourceType()); + Assert.assertEquals(Long.valueOf(3L), resource.getIsAd()); + Assert.assertEquals(Integer.valueOf(1900), resource.getPriceFen()); + assertPremiumSpecs(resource.getAppResourceListList(), 1900, 6600); + + ArgumentCaptor articleCaptor = ArgumentCaptor.forClass(AppBlogArticle.class); + verify(articleMapper).insertAppBlogArticle(articleCaptor.capture()); + Assert.assertEquals(Long.valueOf(7L), articleCaptor.getValue().getArticleType()); } @Test @@ -134,19 +165,80 @@ public class TtCodeServiceImplTest 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); + Assert.assertEquals(Long.valueOf(6L), existingResource.getResourceType()); + Assert.assertEquals(Long.valueOf(3L), existingResource.getIsAd()); + Assert.assertEquals(Long.valueOf(7L), existingArticle.getArticleType()); + assertPremiumSpecs(existingResource.getAppResourceListList(), 5900, 9900); + verify(resourceService).updateAppResource(existingResource); verify(articleMapper).updateAppBlogArticle(existingArticle); - verify(resourceMapper).batchAppResourceList(any()); - verify(resourceMapper, never()).insertAppResource(any()); + verify(resourceService, never()).insertAppResource(any()); verify(articleMapper, never()).insertAppBlogArticle(any()); } - private void assertResourceListDefaults(AppResourceList item, int sortOrder) + @Test + public void keepsLegacyRulesForOtherProjectSeries() { - Assert.assertEquals(Integer.valueOf(0), item.getPriceFen()); + code.setCodeName("【A001】普通源码项目"); + projectInfo.setProjectNum("A001"); + when(resourceService.insertAppResource(any())).thenAnswer(invocation -> { + AppResource resource = invocation.getArgument(0); + resource.setId(102L); + return 1; + }); + + service.transToArticle1(9L, 1L); + + ArgumentCaptor resourceCaptor = ArgumentCaptor.forClass(AppResource.class); + verify(resourceService).insertAppResource(resourceCaptor.capture()); + AppResource resource = resourceCaptor.getValue(); + Assert.assertEquals(Long.valueOf(5L), resource.getResourceType()); + Assert.assertEquals(Long.valueOf(2L), resource.getIsAd()); + Assert.assertEquals(Integer.valueOf(0), resource.getPriceFen()); + Assert.assertEquals("https://pan.baidu.com/s/example", code.getDiskLink()); + Assert.assertEquals("百度网盘", resource.getAppResourceListList().get(0).getListName()); + Assert.assertEquals("夸克网盘", resource.getAppResourceListList().get(1).getListName()); + + ArgumentCaptor articleCaptor = ArgumentCaptor.forClass(AppBlogArticle.class); + verify(articleMapper).insertAppBlogArticle(articleCaptor.capture()); + Assert.assertEquals(Long.valueOf(4L), articleCaptor.getValue().getArticleType()); + } + + @Test + public void rejectsPremiumProjectWithoutQuarkLink() + { + projectInfo.setProjectUrl(null); + + try + { + service.transToArticle1(9L, 1L); + Assert.fail("应拒绝未配置夸克链接的精品项目"); + } + catch (ServiceException exception) + { + Assert.assertEquals("该项目未配置夸克网盘链接,无法生成付费规格", exception.getMessage()); + } + + verify(resourceService, never()).insertAppResource(any()); + verify(articleMapper, never()).insertAppBlogArticle(any()); + } + + private void assertPremiumSpecs(List items, int firstPrice, int secondPrice) + { + Assert.assertEquals(2, items.size()); + assertPremiumSpec(items.get(0), "源码 + 数据库 + 论文 + 答辩PPT", + "https://pan.quark.cn/s/example", firstPrice, 1); + assertPremiumSpec(items.get(1), "源码 + 数据库 + 论文 + 答辩PPT + 项目部署", + "调试部署加微信:forfeastcoding", secondPrice, 2); + } + + private void assertPremiumSpec(AppResourceList item, String name, String url, + int priceFen, int sortOrder) + { + Assert.assertEquals(name, item.getListName()); + Assert.assertEquals(url, item.getListUrl()); + Assert.assertEquals(Integer.valueOf(priceFen), 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()); } }