docs: 完善项目文档并清理过时文件

新增文档:
- API_INTEGRATION_GUIDE.md: API集成指南(快速开始、SDK示例、常见场景)
- DEPLOYMENT_GUIDE.md: 部署指南(环境要求、生产部署、Docker部署)
- CONFIGURATION_GUIDE.md: 配置指南(环境配置、数据库、Redis、安全)
- DEVELOPMENT_GUIDE.md: 开发指南(环境搭建、项目结构、开发规范)

文档更新:
- api.md: 补充8个缺失的API端点(分享跟踪、回调、用户奖励)

文档清理:
- 归档18个过时文档到 docs/archive/2026-03-04-cleanup/
- 删除3个调试文档(ralph-loop-*)

代码清理:
- 删除4个.bak备份文件
- 删除1个.disabled测试文件

文档结构优化:
- 从~40个文档精简到12个核心文档
- 建立清晰的文档导航体系
- 完善文档间的交叉引用
This commit is contained in:
Your Name
2026-03-04 10:41:38 +08:00
parent e79d69f0af
commit 0eed01e9eb
31 changed files with 3229 additions and 1476 deletions

View File

@@ -1,487 +0,0 @@
package com.mosquito.project.service;
import com.mosquito.project.config.PosterConfig;
import com.mosquito.project.domain.Activity;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.awt.*;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
/**
* PosterRenderService 边界条件和异常处理测试
*/
@ExtendWith(MockitoExtension.class)
@DisplayName("PosterRenderService 边界测试")
class PosterRenderServiceBoundaryTest {
@Mock
private PosterConfig posterConfig;
@Mock
private ShortLinkService shortLinkService;
@Mock
private PosterConfig.PosterTemplate mockTemplate;
@InjectMocks
private PosterRenderService posterRenderService;
private Activity testActivity;
@BeforeEach
void setUp() throws Exception {
testActivity = new Activity();
testActivity.setId(123L);
testActivity.setName("测试活动");
// 清除图片缓存
Field imageCacheField = PosterRenderService.class.getDeclaredField("imageCache");
imageCacheField.setAccessible(true);
Map<String, Image> imageCache = (Map<String, Image>) imageCacheField.get(posterRenderService);
imageCache.clear();
}
@Nested
@DisplayName("renderPoster边界条件测试")
class RenderPosterBoundaryTests {
@Test
@DisplayName("null模板名应该使用默认模板")
void shouldUseDefaultTemplate_WhenTemplateNameIsNull() {
// Given
String defaultTemplateName = "default";
when(posterConfig.getTemplate(null)).thenReturn(null);
when(posterConfig.getTemplate(defaultTemplateName)).thenReturn(mockTemplate);
when(posterConfig.getDefaultTemplate()).thenReturn(defaultTemplateName);
when(mockTemplate.getWidth()).thenReturn(800);
when(mockTemplate.getHeight()).thenReturn(600);
when(mockTemplate.getBackgroundColor()).thenReturn("#FFFFFF");
// When
byte[] result = posterRenderService.renderPoster(1L, 1L, null);
// Then
assertNotNull(result);
assertTrue(result.length > 0);
verify(posterConfig).getTemplate(null);
verify(posterConfig).getTemplate(defaultTemplateName);
}
@Test
@DisplayName("不存在的模板名应该使用默认模板")
void shouldUseDefaultTemplate_WhenTemplateNotFound() {
// Given
String invalidTemplateName = "nonexistent";
String defaultTemplateName = "default";
when(posterConfig.getTemplate(invalidTemplateName)).thenReturn(null);
when(posterConfig.getTemplate(defaultTemplateName)).thenReturn(mockTemplate);
when(posterConfig.getDefaultTemplate()).thenReturn(defaultTemplateName);
when(mockTemplate.getWidth()).thenReturn(800);
when(mockTemplate.getHeight()).thenReturn(600);
when(mockTemplate.getBackgroundColor()).thenReturn("#FFFFFF");
// When
byte[] result = posterRenderService.renderPoster(1L, 1L, invalidTemplateName);
// Then
assertNotNull(result);
verify(posterConfig).getTemplate(invalidTemplateName);
verify(posterConfig).getTemplate(defaultTemplateName);
}
@Test
@DisplayName("模板背景为空字符串应该使用背景色")
void shouldUseBackgroundColor_WhenTemplateBackgroundIsEmpty() {
// Given
when(posterConfig.getTemplate(anyString())).thenReturn(mockTemplate);
when(mockTemplate.getWidth()).thenReturn(800);
when(mockTemplate.getHeight()).thenReturn(600);
when(mockTemplate.getBackground()).thenReturn("");
when(mockTemplate.getBackgroundColor()).thenReturn("#FF0000");
// When
byte[] result = posterRenderService.renderPoster(1L, 1L, "test");
// Then
assertNotNull(result);
assertTrue(result.length > 0);
}
@Test
@DisplayName("模板背景为null应该使用背景色")
void shouldUseBackgroundColor_WhenTemplateBackgroundIsNull() {
// Given
when(posterConfig.getTemplate(anyString())).thenReturn(mockTemplate);
when(mockTemplate.getWidth()).thenReturn(800);
when(mockTemplate.getHeight()).thenReturn(600);
when(mockTemplate.getBackground()).thenReturn(null);
when(mockTemplate.getBackgroundColor()).thenReturn("#00FF00");
// When
byte[] result = posterRenderService.renderPoster(1L, 1L, "test");
// Then
assertNotNull(result);
assertTrue(result.length > 0);
}
@Test
@DisplayName("模板没有背景设置应该使用背景色")
void shouldUseBackgroundColor_WhenTemplateHasNoBackground() {
// Given
when(posterConfig.getTemplate(anyString())).thenReturn(mockTemplate);
when(mockTemplate.getWidth()).thenReturn(800);
when(mockTemplate.getHeight()).thenReturn(600);
when(mockTemplate.getBackgroundColor()).thenReturn("#0000FF");
// When
byte[] result = posterRenderService.renderPoster(1L, 1L, "test");
// Then
assertNotNull(result);
assertTrue(result.length > 0);
}
@Test
@DisplayName("图片加载失败应该使用背景色")
void shouldUseBackgroundColor_WhenImageLoadFails() {
// Given
String invalidImageUrl = "nonexistent-image.jpg";
when(posterConfig.getTemplate(anyString())).thenReturn(mockTemplate);
when(mockTemplate.getWidth()).thenReturn(800);
when(mockTemplate.getHeight()).thenReturn(600);
when(mockTemplate.getBackground()).thenReturn(invalidImageUrl);
when(mockTemplate.getBackgroundColor()).thenReturn("#FFAA00");
when(posterConfig.getCdnBaseUrl()).thenReturn("https://cdn.example.com");
// When
byte[] result = posterRenderService.renderPoster(1L, 1L, "test");
// Then
assertNotNull(result);
assertTrue(result.length > 0);
}
@Test
@DisplayName("空元素列表应该正常渲染")
void shouldRenderSuccessfully_WhenElementsListIsEmpty() throws Exception {
// Given
when(posterConfig.getTemplate(anyString())).thenReturn(mockTemplate);
when(mockTemplate.getWidth()).thenReturn(800);
when(mockTemplate.getHeight()).thenReturn(600);
when(mockTemplate.getBackgroundColor()).thenReturn("#FFFFFF");
// 设置空元素map
Field elementsField = PosterConfig.PosterTemplate.class.getDeclaredField("elements");
elementsField.setAccessible(true);
elementsField.set(mockTemplate, new ConcurrentHashMap<>());
// When
byte[] result = posterRenderService.renderPoster(1L, 1L, "test");
// Then
assertNotNull(result);
assertTrue(result.length > 0);
}
@ParameterizedTest
@ValueSource(strings = {"#FF0000", "#00FF00", "#0000FF", "#FFFFFF", "#000000"})
@DisplayName("不同背景色应该正确渲染")
void shouldRenderCorrectly_WithDifferentBackgroundColors(String backgroundColor) {
// Given
when(posterConfig.getTemplate(anyString())).thenReturn(mockTemplate);
when(mockTemplate.getWidth()).thenReturn(800);
when(mockTemplate.getHeight()).thenReturn(600);
when(mockTemplate.getBackgroundColor()).thenReturn(backgroundColor);
// When & Then
assertDoesNotThrow(() -> {
byte[] result = posterRenderService.renderPoster(1L, 1L, "test");
assertNotNull(result);
assertTrue(result.length > 0);
});
}
}
@Nested
@DisplayName("renderPosterHtml边界条件测试")
class RenderPosterHtmlBoundaryTests {
@Test
@DisplayName("null活动名称应该使用默认值")
void shouldUseDefaultTitle_WhenActivityNameIsNull() {
// Given
Activity nullNameActivity = new Activity();
nullNameActivity.setId(123L);
nullNameActivity.setName(null);
setupMockTemplate();
when(shortLinkService.create(anyString())).thenReturn(new com.mosquito.project.domain.ShortLink());
when(shortLinkService.create(anyString())).thenReturn(new com.mosquito.project.domain.ShortLink());
// 使用反射设置模拟活动
// 注意这里需要更复杂的反射来模拟activityService的返回值
// When
String html = posterRenderService.renderPosterHtml(123L, 456L, "test");
// Then
assertNotNull(html);
assertTrue(html.contains("<title>分享</title>"));
}
@Test
@DisplayName("空活动名称应该使用默认值")
void shouldUseDefaultTitle_WhenActivityNameIsEmpty() {
// Given
Activity emptyNameActivity = new Activity();
emptyNameActivity.setId(123L);
emptyNameActivity.setName("");
setupMockTemplate();
// When
String html = posterRenderService.renderPosterHtml(123L, 456L, "test");
// Then
assertNotNull(html);
assertTrue(html.contains("<title>分享</title>"));
}
@Test
@DisplayName("活动名称包含特殊字符应该正确转义")
void shouldEscapeHtml_WhenActivityNameContainsSpecialChars() {
// Given
Activity specialCharActivity = new Activity();
specialCharActivity.setId(123L);
specialCharActivity.setName("活动名称 & <script> alert('xss') </script>\"");
setupMockTemplate();
// When
String html = posterRenderService.renderPosterHtml(123L, 456L, "test");
// Then
assertNotNull(html);
assertFalse(html.contains("<script>"));
assertTrue(html.contains("&lt;script&gt;"));
}
@Test
@DisplayName("URL编码失败应该使用原始URL")
void shouldUseOriginalUrl_WhenUrlEncodingFails() {
// Given
setupMockTemplate();
setupQrCodeElement();
// When
String html = posterRenderService.renderPosterHtml(123L, 456L, "test");
// Then
assertNotNull(html);
assertTrue(html.contains("data="));
// 确保即使编码失败也生成HTML
}
@Test
@DisplayName("长活动名称应该正确处理")
void shouldHandleLongActivityName() {
// Given
StringBuilder longName = new StringBuilder();
for (int i = 0; i < 1000; i++) {
longName.append("很长的活动名称");
}
Activity longNameActivity = new Activity();
longNameActivity.setId(123L);
longNameActivity.setName(longName.toString());
setupMockTemplate();
// When
String html = posterRenderService.renderPosterHtml(123L, 456L, "test");
// Then
assertNotNull(html);
assertTrue(html.length() > 0);
}
private void setupMockTemplate() {
when(posterConfig.getTemplate(anyString())).thenReturn(mockTemplate);
when(mockTemplate.getWidth()).thenReturn(800);
when(mockTemplate.getHeight()).thenReturn(600);
when(mockTemplate.getBackgroundColor()).thenReturn("#FFFFFF");
}
private void setupQrCodeElement() {
PosterConfig.PosterElement qrElement = new PosterConfig.PosterElement();
qrElement.setType("qrcode");
qrElement.setX(100);
qrElement.setY(100);
qrElement.setWidth(200);
qrElement.setHeight(200);
Map<String, PosterConfig.PosterElement> elements = Map.of("qrcode", qrElement);
when(mockTemplate.getElements()).thenReturn(elements);
}
}
@Nested
@DisplayName("异常处理测试")
class ExceptionHandlingTests {
@Test
@DisplayName("ImageIO写入失败应该抛出RuntimeException")
void shouldThrowRuntimeException_WhenImageIoWriteFails() {
// Given
when(posterConfig.getTemplate(anyString())).thenReturn(mockTemplate);
when(mockTemplate.getWidth()).thenReturn(Integer.MAX_VALUE); // 极大尺寸可能导致内存错误
when(mockTemplate.getHeight()).thenReturn(Integer.MAX_VALUE);
when(mockTemplate.getBackgroundColor()).thenReturn("#FFFFFF");
// When & Then
assertThrows(RuntimeException.class, () -> {
posterRenderService.renderPoster(1L, 1L, "test");
});
}
@Test
@DisplayName("无效颜色代码应该抛出异常")
void shouldThrowException_WhenColorCodeIsInvalid() {
// Given
when(posterConfig.getTemplate(anyString())).thenReturn(mockTemplate);
when(mockTemplate.getWidth()).thenReturn(800);
when(mockTemplate.getHeight()).thenReturn(600);
when(mockTemplate.getBackgroundColor()).thenReturn("invalid-color");
// When & Then
assertThrows(NumberFormatException.class, () -> {
posterRenderService.renderPoster(1L, 1L, "test");
});
}
@ParameterizedTest
@NullAndEmptySource
@DisplayName("null或空模板名应该不抛出异常")
void shouldNotThrowException_WhenTemplateNameIsNullOrEmpty(String templateName) {
// Given
when(posterConfig.getTemplate(templateName)).thenReturn(null);
when(posterConfig.getTemplate("default")).thenReturn(mockTemplate);
when(posterConfig.getDefaultTemplate()).thenReturn("default");
when(mockTemplate.getWidth()).thenReturn(800);
when(mockTemplate.getHeight()).thenReturn(600);
when(mockTemplate.getBackgroundColor()).thenReturn("#FFFFFF");
// When & Then
assertDoesNotThrow(() -> {
byte[] result = posterRenderService.renderPoster(1L, 1L, templateName);
assertNotNull(result);
});
}
}
@Nested
@DisplayName("辅助方法边界测试")
class HelperMethodBoundaryTests {
@Test
@DisplayName("解析字体大小失败应该使用默认值")
void shouldUseDefaultFontSize_WhenParsingFails() throws Exception {
// Given
when(posterConfig.getTemplate(anyString())).thenReturn(mockTemplate);
when(mockTemplate.getWidth()).thenReturn(800);
when(mockTemplate.getHeight()).thenReturn(600);
when(mockTemplate.getBackgroundColor()).thenReturn("#FFFFFF");
PosterConfig.PosterElement textElement = new PosterConfig.PosterElement();
textElement.setType("text");
textElement.setContent("测试文本");
textElement.setFontSize("invalid-size"); // 无效的字体大小
textElement.setColor("#000000");
textElement.setFontFamily("Arial");
textElement.setX(100);
textElement.setY(100);
Map<String, PosterConfig.PosterElement> elements = Map.of("text", textElement);
when(mockTemplate.getElements()).thenReturn(elements);
// When & Then
assertDoesNotThrow(() -> {
byte[] result = posterRenderService.renderPoster(1L, 1L, "test");
assertNotNull(result);
});
}
@Test
@DisplayName("空内容字符串应该正确处理")
void shouldHandleEmptyContentString() throws Exception {
// Given
when(posterConfig.getTemplate(anyString())).thenReturn(mockTemplate);
when(mockTemplate.getWidth()).thenReturn(800);
when(mockTemplate.getHeight()).thenReturn(600);
when(mockTemplate.getBackgroundColor()).thenReturn("#FFFFFF");
PosterConfig.PosterElement textElement = new PosterConfig.PosterElement();
textElement.setType("text");
textElement.setContent(""); // 空内容
textElement.setFontSize("16px");
textElement.setColor("#000000");
textElement.setFontFamily("Arial");
textElement.setX(100);
textElement.setY(100);
Map<String, PosterConfig.PosterElement> elements = Map.of("text", textElement);
when(mockTemplate.getElements()).thenReturn(elements);
// When & Then
assertDoesNotThrow(() -> {
byte[] result = posterRenderService.renderPoster(1L, 1L, "test");
assertNotNull(result);
});
}
@Test
@DisplayName("null内容应该返回空字符串")
void shouldReturnEmptyString_WhenContentIsNull() throws Exception {
// Given
when(posterConfig.getTemplate(anyString())).thenReturn(mockTemplate);
when(mockTemplate.getWidth()).thenReturn(800);
when(mockTemplate.getHeight()).thenReturn(600);
when(mockTemplate.getBackgroundColor()).thenReturn("#FFFFFF");
PosterConfig.PosterElement textElement = new PosterConfig.PosterElement();
textElement.setType("text");
textElement.setContent(null); // null内容
textElement.setFontSize("16px");
textElement.setColor("#000000");
textElement.setFontFamily("Arial");
textElement.setX(100);
textElement.setY(100);
Map<String, PosterConfig.PosterElement> elements = Map.of("text", textElement);
when(mockTemplate.getElements()).thenReturn(elements);
// When & Then
assertDoesNotThrow(() -> {
byte[] result = posterRenderService.renderPoster(1L, 1L, "test");
assertNotNull(result);
});
}
}
}