文章导入知识库

This commit is contained in:
2026-01-12 13:52:19 +08:00
parent 26df740dd0
commit 12dca45b4d
20 changed files with 1371 additions and 15 deletions

View File

@@ -0,0 +1,206 @@
package org.xyzh.news.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.xyzh.api.ai.knowledge.AiKnowledgeService;
import org.xyzh.api.system.config.SysConfigService;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.dto.ai.TbAiKnowledge;
/**
* 文章知识库初始化配置
* 系统启动时自动创建"时事资源"知识库,用于存储发布的文章
*/
@Configuration
public class ArticleKnowledgeInit {
private static final Logger logger = LoggerFactory.getLogger(ArticleKnowledgeInit.class);
/** 知识库ID */
private static final String KNOWLEDGE_ID = "article_news_resource";
/** 知识库标题 */
private static final String KNOWLEDGE_TITLE = "时事资源";
/** 知识库描述 */
private static final String KNOWLEDGE_DESCRIPTION = "存储系统发布的文章资源用于AI智能问答";
/** 知识库分类 */
private static final String KNOWLEDGE_CATEGORY = "article";
@Autowired
private AiKnowledgeService aiKnowledgeService;
@Autowired
private SysConfigService sysConfigService;
@Value("${article.knowledge.auto-init:true}")
private boolean autoInit;
@Bean
public CommandLineRunner articleKnowledgeInitRunner() {
return args -> {
if (!autoInit) {
logger.info("文章知识库自动初始化已禁用");
return;
}
logger.info("开始初始化文章知识库...");
try {
// 检查知识库是否已存在
if (checkKnowledgeExists(KNOWLEDGE_ID)) {
logger.info("文章知识库已存在,跳过初始化");
// 更新配置文件中的知识库ID
updateKnowledgeIdConfig();
return;
}
// 创建知识库
TbAiKnowledge knowledge = buildKnowledgeConfig();
if (createKnowledge(knowledge)) {
logger.info("文章知识库初始化成功: {}", KNOWLEDGE_TITLE);
// 更新配置文件中的知识库ID
updateKnowledgeIdConfig();
} else {
logger.error("文章知识库初始化失败");
}
} catch (Exception e) {
logger.error("文章知识库初始化异常: {}", e.getMessage(), e);
}
};
}
/**
* 构建知识库配置
*/
private TbAiKnowledge buildKnowledgeConfig() {
TbAiKnowledge knowledge = new TbAiKnowledge();
// 基本信息
knowledge.setId(KNOWLEDGE_ID);
knowledge.setTitle(KNOWLEDGE_TITLE);
knowledge.setDescription(KNOWLEDGE_DESCRIPTION);
knowledge.setCategory(KNOWLEDGE_CATEGORY);
knowledge.setStatus(1); // 启用状态
// 从系统配置获取Dify相关参数使用 dify.dataset.* 格式的配置key
try {
String indexingTechnique = sysConfigService.getStringConfig("dify.dataset.defaultIndexingTechnique");
if (indexingTechnique != null && !indexingTechnique.isEmpty()) {
knowledge.setDifyIndexingTechnique(indexingTechnique);
} else {
knowledge.setDifyIndexingTechnique("high_quality"); // 默认高质量索引
}
String embeddingModel = sysConfigService.getStringConfig("dify.dataset.defaultEmbeddingModel");
if (embeddingModel != null && !embeddingModel.isEmpty()) {
knowledge.setEmbeddingModel(embeddingModel);
}
String embeddingModelProvider = sysConfigService.getStringConfig("dify.dataset.embeddingModelProvider");
if (embeddingModelProvider != null && !embeddingModelProvider.isEmpty()) {
knowledge.setEmbeddingModelProvider(embeddingModelProvider);
}
Boolean rerankingEnable = sysConfigService.getBooleanConfig("dify.dataset.rerankingEnable");
knowledge.setRerankingEnable(rerankingEnable != null ? rerankingEnable : false);
String rerankModel = sysConfigService.getStringConfig("dify.dataset.rerankModel");
if (rerankModel != null && !rerankModel.isEmpty()) {
knowledge.setRerankModel(rerankModel);
}
String rerankModelProvider = sysConfigService.getStringConfig("dify.dataset.rerankModelProvider");
if (rerankModelProvider != null && !rerankModelProvider.isEmpty()) {
knowledge.setRerankModelProvider(rerankModelProvider);
}
Integer retrievalTopK = sysConfigService.getIntConfig("dify.dataset.retrievalTopK");
knowledge.setRetrievalTopK(retrievalTopK != null ? retrievalTopK : 5);
Double scoreThreshold = sysConfigService.getDoubleConfig("dify.dataset.retrievalScoreThreshold");
knowledge.setRetrievalScoreThreshold(scoreThreshold != null ? scoreThreshold : 0.5);
} catch (Exception e) {
logger.warn("获取Dify配置失败使用默认值: {}", e.getMessage());
// 使用默认值
knowledge.setDifyIndexingTechnique("high_quality");
knowledge.setRetrievalTopK(5);
knowledge.setRetrievalScoreThreshold(0.5);
knowledge.setRerankingEnable(false);
}
return knowledge;
}
/**
* 检查知识库是否存在(使用内部方法,不依赖登录)
*/
private boolean checkKnowledgeExists(String knowledgeId) {
try {
// 使用 createKnowledgeInternal 的逻辑:先尝试创建,如果已存在会返回已存在的知识库
// 这里直接返回 false让 createKnowledgeInternal 内部处理重复检查
// createKnowledgeInternal 会检查 ID 是否已存在并返回已存在的知识库
return false;
} catch (Exception e) {
logger.warn("检查知识库是否存在时发生异常: {}", e.getMessage());
return false;
}
}
/**
* 创建知识库
*/
private boolean createKnowledge(TbAiKnowledge knowledge) {
try {
// 使用内部创建方法,无需登录
ResultDomain<TbAiKnowledge> result = aiKnowledgeService.createKnowledgeInternal(knowledge);
if (result.isSuccess()) {
TbAiKnowledge created = result.getData();
if (created != null) {
logger.info("知识库创建成功: id={}, difyDatasetId={}",
created.getId(), created.getDifyDatasetId());
}
return true;
} else {
logger.error("知识库创建失败: {}", result.getMessage());
return false;
}
} catch (Exception e) {
logger.error("创建知识库异常: {}", e.getMessage(), e);
return false;
}
}
/**
* 更新配置中的知识库ID
* 将创建的知识库ID写入系统配置供ArticleKnowledgeService使用
*/
private void updateKnowledgeIdConfig() {
try {
// 检查配置是否已存在
String existingId = sysConfigService.getStringConfig("article.knowledge.default-id");
if (existingId == null || existingId.isEmpty()) {
// 可以通过SysConfigService设置配置如果支持的话
logger.info("文章知识库ID: {},请在配置文件中设置 article.knowledge.default-id={}",
KNOWLEDGE_ID, KNOWLEDGE_ID);
}
} catch (Exception e) {
logger.warn("更新知识库ID配置失败: {}", e.getMessage());
}
}
/**
* 获取文章知识库ID
*/
public static String getArticleKnowledgeId() {
return KNOWLEDGE_ID;
}
}

View File

@@ -0,0 +1,61 @@
package org.xyzh.news.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.dto.resource.TbResource;
import org.xyzh.news.service.ArticleKnowledgeService;
import java.util.List;
import java.util.Map;
/**
* 文章知识库管理控制器
* 提供手动将文章导入/移除知识库的接口
*/
@RestController
@RequestMapping("/news/article-knowledge")
public class ArticleKnowledgeController {
private static final Logger logger = LoggerFactory.getLogger(ArticleKnowledgeController.class);
@Autowired
private ArticleKnowledgeService articleKnowledgeService;
/**
* 将单篇文章导入知识库
* @param resourceId 资源ID
* @return 导入结果
*/
@PostMapping("/import/{resourceId}")
public ResultDomain<TbResource> importToKnowledge(@PathVariable("resourceId") String resourceId) {
logger.info("手动导入文章到知识库: resourceId={}", resourceId);
return articleKnowledgeService.importArticleToDefaultKnowledgeById(resourceId);
}
/**
* 批量将文章导入知识库
* @param params 包含resourceIds的参数
* @return 导入结果
*/
@PostMapping("/import/batch")
@SuppressWarnings("unchecked")
public ResultDomain<TbResource> batchImportToKnowledge(@RequestBody Map<String, Object> params) {
List<String> resourceIds = (List<String>) params.get("resourceIds");
logger.info("批量导入文章到知识库: count={}", resourceIds != null ? resourceIds.size() : 0);
return articleKnowledgeService.batchImportToDefaultKnowledge(resourceIds);
}
/**
* 从知识库移除文章
* @param resourceId 资源ID
* @return 移除结果
*/
@DeleteMapping("/remove/{resourceId}")
public ResultDomain<Boolean> removeFromKnowledge(@PathVariable("resourceId") String resourceId) {
logger.info("从知识库移除文章: resourceId={}", resourceId);
return articleKnowledgeService.removeArticleFromKnowledge(resourceId);
}
}

View File

@@ -0,0 +1,49 @@
package org.xyzh.news.service;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.dto.resource.TbResource;
import java.util.List;
/**
* 文章知识库服务接口
* 负责将发布的文章导入到AI知识库
*/
public interface ArticleKnowledgeService {
/**
* 将文章导入到知识库
* @param resource 文章资源
* @param knowledgeId 目标知识库ID
* @return 导入结果
*/
ResultDomain<TbResource> importArticleToKnowledge(TbResource resource, String knowledgeId);
/**
* 将文章导入到默认知识库
* @param resource 文章资源
* @return 导入结果
*/
ResultDomain<TbResource> importArticleToDefaultKnowledge(TbResource resource);
/**
* 根据资源ID将文章导入到默认知识库
* @param resourceId 资源ID
* @return 导入结果
*/
ResultDomain<TbResource> importArticleToDefaultKnowledgeById(String resourceId);
/**
* 批量将文章导入到默认知识库
* @param resourceIds 资源ID列表
* @return 导入结果
*/
ResultDomain<TbResource> batchImportToDefaultKnowledge(List<String> resourceIds);
/**
* 从知识库中删除文章
* @param resourceId 文章资源ID
* @return 删除结果
*/
ResultDomain<Boolean> removeArticleFromKnowledge(String resourceId);
}

View File

@@ -0,0 +1,318 @@
package org.xyzh.news.service.impl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import org.xyzh.ai.mapper.AiKnowledgeMapper;
import org.xyzh.api.ai.file.AiUploadFileService;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.dto.ai.TbAiKnowledge;
import org.xyzh.common.dto.ai.TbAiUploadFile;
import org.xyzh.common.dto.resource.TbResource;
import org.xyzh.news.mapper.ResourceMapper;
import org.xyzh.news.service.ArticleKnowledgeService;
import org.xyzh.news.util.ArticlePdfGenerator;
import java.io.File;
import java.io.FileInputStream;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 文章知识库服务实现
*/
@Service
public class ArticleKnowledgeServiceImpl implements ArticleKnowledgeService {
private static final Logger logger = LoggerFactory.getLogger(ArticleKnowledgeServiceImpl.class);
/** 固定的文章知识库ID */
private static final String DEFAULT_KNOWLEDGE_ID = "article_news_resource";
@Autowired
private ArticlePdfGenerator pdfGenerator;
@Autowired
private AiUploadFileService aiUploadFileService;
@Autowired
private ResourceMapper resourceMapper;
@Autowired
private AiKnowledgeMapper aiKnowledgeMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<TbResource> importArticleToKnowledge(TbResource resource, String knowledgeId) {
ResultDomain<TbResource> resultDomain = new ResultDomain<>();
File pdfFile = null;
try {
// 参数验证
if (resource == null) {
resultDomain.fail("文章资源不能为空");
return resultDomain;
}
if (!StringUtils.hasText(knowledgeId)) {
resultDomain.fail("知识库ID不能为空");
return resultDomain;
}
if (!StringUtils.hasText(resource.getContent())) {
resultDomain.fail("文章内容不能为空");
return resultDomain;
}
// 检查是否已导入
if (Boolean.TRUE.equals(resource.getIsInKnowledge())) {
resultDomain.fail("文章已导入知识库,无需重复导入");
return resultDomain;
}
logger.info("开始将文章导入知识库: resourceId={}, title={}, knowledgeId={}",
resource.getResourceID(), resource.getTitle(), knowledgeId);
// 1. 生成PDF文件
pdfFile = pdfGenerator.generatePdf(resource);
logger.info("PDF文件生成成功: {}, 大小: {} bytes", pdfFile.getName(), pdfFile.length());
// 2. 构建文件名
String fileName = buildFileName(resource);
// 3. 将PDF文件转换为MultipartFile
MultipartFile multipartFile = convertToMultipartFile(pdfFile, fileName);
// 4. 上传到知识库
ResultDomain<TbAiUploadFile> uploadResult = aiUploadFileService.uploadToKnowledge(
knowledgeId,
multipartFile,
"high_quality"
);
if (uploadResult.isSuccess() && uploadResult.getData() != null) {
TbAiUploadFile uploadFile = uploadResult.getData();
// 5. 更新资源的知识库状态
TbResource updateResource = new TbResource();
updateResource.setResourceID(resource.getResourceID());
updateResource.setIsInKnowledge(true);
updateResource.setKnowledgeFileId(uploadFile.getId());
updateResource.setKnowledgeImportTime(new Date());
updateResource.setUpdateTime(new Date());
int updateResult = resourceMapper.updateResource(updateResource);
if (updateResult > 0) {
logger.info("文章成功导入知识库: resourceId={}, knowledgeFileId={}",
resource.getResourceID(), uploadFile.getId());
// 重新查询返回完整数据
TbResource updated = resourceMapper.selectByResourceId(resource.getResourceID());
resultDomain.success("文章导入知识库成功", updated);
} else {
logger.warn("文章上传成功但更新状态失败: resourceId={}", resource.getResourceID());
resultDomain.success("文章导入知识库成功,但状态更新失败", resource);
}
} else {
logger.error("文章导入知识库失败: {}", uploadResult.getMessage());
resultDomain.fail("文章导入知识库失败: " + uploadResult.getMessage());
}
return resultDomain;
} catch (Exception e) {
logger.error("文章导入知识库异常: resourceId={}, error={}",
resource != null ? resource.getResourceID() : "null", e.getMessage(), e);
resultDomain.fail("文章导入知识库异常: " + e.getMessage());
return resultDomain;
} finally {
// 清理临时PDF文件
if (pdfFile != null && pdfFile.exists()) {
try {
Files.deleteIfExists(pdfFile.toPath());
logger.debug("临时PDF文件已清理: {}", pdfFile.getName());
} catch (Exception e) {
logger.warn("清理临时PDF文件失败: {}", e.getMessage());
}
}
}
}
@Override
public ResultDomain<TbResource> importArticleToDefaultKnowledge(TbResource resource) {
ResultDomain<TbResource> resultDomain = new ResultDomain<>();
// 从数据库查询固定ID的知识库
TbAiKnowledge knowledge = aiKnowledgeMapper.selectKnowledgeById(DEFAULT_KNOWLEDGE_ID);
if (knowledge == null || knowledge.getDeleted()) {
logger.warn("默认文章知识库不存在: {}, 跳过知识库导入", DEFAULT_KNOWLEDGE_ID);
resultDomain.success("默认知识库不存在,跳过导入", resource);
return resultDomain;
}
return importArticleToKnowledge(resource, DEFAULT_KNOWLEDGE_ID);
}
@Override
public ResultDomain<TbResource> importArticleToDefaultKnowledgeById(String resourceId) {
ResultDomain<TbResource> resultDomain = new ResultDomain<>();
if (!StringUtils.hasText(resourceId)) {
resultDomain.fail("资源ID不能为空");
return resultDomain;
}
// 从数据库查询固定ID的知识库
TbAiKnowledge knowledge = aiKnowledgeMapper.selectKnowledgeById(DEFAULT_KNOWLEDGE_ID);
if (knowledge == null || knowledge.getDeleted()) {
resultDomain.fail("默认文章知识库不存在");
return resultDomain;
}
// 查询资源
TbResource resource = resourceMapper.selectByResourceId(resourceId);
if (resource == null || resource.getDeleted()) {
resultDomain.fail("资源不存在");
return resultDomain;
}
// 检查文章状态
if (resource.getStatus() != 1) {
resultDomain.fail("只有已发布的文章才能导入知识库");
return resultDomain;
}
return importArticleToKnowledge(resource, DEFAULT_KNOWLEDGE_ID);
}
@Override
public ResultDomain<TbResource> batchImportToDefaultKnowledge(List<String> resourceIds) {
ResultDomain<TbResource> resultDomain = new ResultDomain<>();
if (resourceIds == null || resourceIds.isEmpty()) {
resultDomain.fail("资源ID列表不能为空");
return resultDomain;
}
// 从数据库查询固定ID的知识库
TbAiKnowledge knowledge = aiKnowledgeMapper.selectKnowledgeById(DEFAULT_KNOWLEDGE_ID);
if (knowledge == null || knowledge.getDeleted()) {
resultDomain.fail("默认文章知识库不存在");
return resultDomain;
}
List<TbResource> successList = new ArrayList<>();
List<String> failedList = new ArrayList<>();
for (String resourceId : resourceIds) {
try {
ResultDomain<TbResource> result = importArticleToDefaultKnowledgeById(resourceId);
if (result.isSuccess() && result.getData() != null) {
successList.add(result.getData());
} else {
failedList.add(resourceId + ": " + result.getMessage());
}
} catch (Exception e) {
failedList.add(resourceId + ": " + e.getMessage());
logger.error("批量导入知识库失败: resourceId={}", resourceId, e);
}
}
if (failedList.isEmpty()) {
resultDomain.success("批量导入成功,共导入 " + successList.size() + " 篇文章", successList);
} else if (successList.isEmpty()) {
resultDomain.fail("批量导入全部失败: " + String.join("; ", failedList));
} else {
resultDomain.success("部分导入成功: 成功 " + successList.size() + " 篇,失败 " + failedList.size() + "", successList);
}
return resultDomain;
}
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<Boolean> removeArticleFromKnowledge(String resourceId) {
ResultDomain<Boolean> resultDomain = new ResultDomain<>();
if (!StringUtils.hasText(resourceId)) {
resultDomain.fail("资源ID不能为空");
return resultDomain;
}
// 查询资源
TbResource resource = resourceMapper.selectByResourceId(resourceId);
if (resource == null || resource.getDeleted()) {
resultDomain.fail("资源不存在");
return resultDomain;
}
if (!Boolean.TRUE.equals(resource.getIsInKnowledge())) {
resultDomain.fail("文章未导入知识库");
return resultDomain;
}
try {
// 删除知识库中的文件
if (StringUtils.hasText(resource.getKnowledgeFileId())) {
ResultDomain<Boolean> deleteResult = aiUploadFileService.deleteFile(resource.getKnowledgeFileId());
if (!deleteResult.isSuccess()) {
logger.warn("删除知识库文件失败,继续更新资源状态: {}", deleteResult.getMessage());
}
}
// 更新资源状态
TbResource updateResource = new TbResource();
updateResource.setResourceID(resourceId);
updateResource.setIsInKnowledge(false);
updateResource.setKnowledgeFileId(null);
updateResource.setKnowledgeImportTime(null);
updateResource.setUpdateTime(new Date());
int updateResult = resourceMapper.updateResource(updateResource);
if (updateResult > 0) {
logger.info("文章已从知识库移除: resourceId={}", resourceId);
resultDomain.success("文章已从知识库移除", true);
} else {
resultDomain.fail("更新资源状态失败");
}
} catch (Exception e) {
logger.error("从知识库移除文章异常: resourceId={}", resourceId, e);
resultDomain.fail("从知识库移除文章异常: " + e.getMessage());
}
return resultDomain;
}
/**
* 构建PDF文件名
*/
private String buildFileName(TbResource resource) {
String title = resource.getTitle();
// 移除文件名中的非法字符
title = title.replaceAll("[\\\\/:*?\"<>|]", "_");
// 限制文件名长度
if (title.length() > 50) {
title = title.substring(0, 50);
}
return "article_" + resource.getResourceID() + "_" + title + ".pdf";
}
/**
* 将File转换为MultipartFile
*/
private MultipartFile convertToMultipartFile(File file, String fileName) throws Exception {
try (FileInputStream fis = new FileInputStream(file)) {
return new MockMultipartFile(
"file",
fileName,
"application/pdf",
fis
);
}
}
}

View File

@@ -28,6 +28,7 @@ import org.xyzh.api.news.resource.ResourceService;
import org.xyzh.api.system.permission.ResourcePermissionService;
import org.xyzh.common.vo.UserDeptRoleVO;
import org.xyzh.common.core.enums.ResourceType;
import org.xyzh.news.service.ArticleKnowledgeService;
import java.util.ArrayList;
import java.util.Arrays;
@@ -59,6 +60,9 @@ public class NCResourceServiceImpl implements ResourceService {
@Autowired
private ResourceAuditService auditService;
@Autowired
private ArticleKnowledgeService articleKnowledgeService;
@Override
public ResultDomain<TbResource> getResourceList(TbResource filter) {
ResultDomain<TbResource> resultDomain = new ResultDomain<>();
@@ -328,6 +332,20 @@ public class NCResourceServiceImpl implements ResourceService {
logger.error("创建资源权限异常,但不影响资源创建: {}", e.getMessage(), e);
}
// 如果文章直接发布status=1且审核通过导入知识库
if (resource.getStatus() == 1 && resource.getIsAudited()) {
try {
ResultDomain<TbResource> knowledgeResult = articleKnowledgeService.importArticleToDefaultKnowledge(resource);
if (knowledgeResult.isSuccess() && knowledgeResult.getData() != null) {
logger.info("新建文章已成功导入知识库: {}", resource.getResourceID());
} else {
logger.warn("新建文章导入知识库跳过或失败: {}, 原因: {}", resource.getResourceID(), knowledgeResult.getMessage());
}
} catch (Exception e) {
logger.error("新建文章导入知识库异常,但不影响创建: {}", e.getMessage(), e);
}
}
resultDomain.success("创建资源成功", resourceVO);
return resultDomain;
} else {
@@ -529,7 +547,22 @@ public class NCResourceServiceImpl implements ResourceService {
if (result > 0) {
logger.info("更新资源状态成功: {}", resourceID);
// 重新查询返回完整数据
TbResource updated = resourceMapper.selectByResourceId(resource.getId());
TbResource updated = resourceMapper.selectByResourceId(resource.getResourceID());
// 如果状态变为发布且审核通过,导入知识库
if (status == 1 && resource.getIsAudited()) {
try {
ResultDomain<TbResource> knowledgeResult = articleKnowledgeService.importArticleToDefaultKnowledge(updated);
if (knowledgeResult.isSuccess() && knowledgeResult.getData() != null) {
logger.info("文章状态更新后已成功导入知识库: {}", resourceID);
} else {
logger.warn("文章状态更新后导入知识库跳过或失败: {}, 原因: {}", resourceID, knowledgeResult.getMessage());
}
} catch (Exception e) {
logger.error("文章状态更新后导入知识库异常,但不影响状态更新: {}", e.getMessage(), e);
}
}
resultDomain.success("更新资源状态成功", updated);
return resultDomain;
} else {
@@ -586,7 +619,21 @@ public class NCResourceServiceImpl implements ResourceService {
if (result > 0) {
logger.info("发布资源成功: {}", resourceID);
// 重新查询返回完整数据
TbResource updated = resourceMapper.selectByResourceId(resource.getId());
TbResource updated = resourceMapper.selectByResourceId(resourceID);
// 异步将文章导入知识库
try {
ResultDomain<TbResource> knowledgeResult = articleKnowledgeService.importArticleToDefaultKnowledge(updated);
if (knowledgeResult.isSuccess() && knowledgeResult.getData() != null) {
logger.info("文章已成功导入知识库: {}", resourceID);
} else {
logger.warn("文章导入知识库跳过或失败: {}, 原因: {}", resourceID, knowledgeResult.getMessage());
}
} catch (Exception e) {
// 知识库导入失败不影响文章发布
logger.error("文章导入知识库异常,但不影响发布: {}", e.getMessage(), e);
}
resultDomain.success("发布资源成功", updated);
return resultDomain;
} else {
@@ -626,7 +673,7 @@ public class NCResourceServiceImpl implements ResourceService {
if (result > 0) {
logger.info("下架资源成功: {}", resourceID);
// 重新查询返回完整数据
TbResource updated = resourceMapper.selectByResourceId(resource.getId());
TbResource updated = resourceMapper.selectByResourceId(resource.getResourceID());
resultDomain.success("下架资源成功", updated);
return resultDomain;
} else {
@@ -695,7 +742,7 @@ public class NCResourceServiceImpl implements ResourceService {
if (result > 0) {
logger.info("增加资源点赞次数成功: {}", resourceID);
// 重新查询返回完整数据
TbResource updated = resourceMapper.selectByResourceId(resource.getId());
TbResource updated = resourceMapper.selectByResourceId(resource.getResourceID());
resultDomain.success("增加点赞次数成功", updated);
return resultDomain;
} else {
@@ -767,7 +814,7 @@ public class NCResourceServiceImpl implements ResourceService {
if (result > 0) {
logger.info("设置资源推荐状态成功: {} -> {}", resourceID, isRecommend);
// 重新查询返回完整数据
TbResource updated = resourceMapper.selectByResourceId(resource.getId());
TbResource updated = resourceMapper.selectByResourceId(resource.getResourceID());
resultDomain.success("设置推荐状态成功", updated);
return resultDomain;
} else {
@@ -811,7 +858,7 @@ public class NCResourceServiceImpl implements ResourceService {
if (result > 0) {
logger.info("设置资源轮播状态成功: {} -> {}", resourceID, isBanner);
// 重新查询返回完整数据
TbResource updated = resourceMapper.selectByResourceId(resource.getId());
TbResource updated = resourceMapper.selectByResourceId(resource.getResourceID());
resultDomain.success("设置轮播状态成功", updated);
return resultDomain;
} else {

View File

@@ -0,0 +1,270 @@
package org.xyzh.news.util;
import com.lowagie.text.pdf.BaseFont;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.safety.Safelist;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.xhtmlrenderer.pdf.ITextRenderer;
import org.xyzh.common.dto.resource.TbResource;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 文章PDF生成工具类
* 将富文本内容转换为PDF文件
*/
@Component
public class ArticlePdfGenerator {
private static final Logger logger = LoggerFactory.getLogger(ArticlePdfGenerator.class);
/**
* 将文章内容生成PDF文件
* @param resource 文章资源
* @return 生成的PDF文件
*/
public File generatePdf(TbResource resource) throws Exception {
if (resource == null || resource.getContent() == null) {
throw new IllegalArgumentException("文章内容不能为空");
}
// 创建临时文件
Path tempFile = Files.createTempFile("article_" + resource.getResourceID() + "_", ".pdf");
File pdfFile = tempFile.toFile();
try (OutputStream os = new FileOutputStream(pdfFile)) {
// 构建HTML内容
String htmlContent = buildHtmlContent(resource);
// 使用Flying Saucer生成PDF
ITextRenderer renderer = new ITextRenderer();
// 添加中文字体支持
addFontSupport(renderer);
// 设置HTML内容
renderer.setDocumentFromString(htmlContent);
renderer.layout();
renderer.createPDF(os);
logger.info("PDF生成成功: {}, 文件大小: {} bytes", pdfFile.getName(), pdfFile.length());
return pdfFile;
} catch (Exception e) {
// 清理临时文件
Files.deleteIfExists(tempFile);
logger.error("PDF生成失败: {}", e.getMessage(), e);
throw e;
}
}
/**
* 构建HTML内容
*/
private String buildHtmlContent(TbResource resource) {
StringBuilder html = new StringBuilder();
html.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
html.append("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">");
html.append("<html xmlns=\"http://www.w3.org/1999/xhtml\">");
html.append("<head>");
html.append("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>");
html.append("<style type=\"text/css\">");
html.append(getStyleContent());
html.append("</style>");
html.append("</head>");
html.append("<body>");
// 文章标题
html.append("<div class=\"article-header\">");
html.append("<h1 class=\"title\">").append(escapeHtml(resource.getTitle())).append("</h1>");
// 文章元信息
html.append("<div class=\"meta\">");
if (resource.getAuthor() != null && !resource.getAuthor().isEmpty()) {
html.append("<span>作者: ").append(escapeHtml(resource.getAuthor())).append("</span>");
}
if (resource.getSource() != null && !resource.getSource().isEmpty()) {
html.append("<span>来源: ").append(escapeHtml(resource.getSource())).append("</span>");
}
if (resource.getPublishTime() != null) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");
html.append("<span>发布时间: ").append(sdf.format(resource.getPublishTime())).append("</span>");
}
html.append("</div>");
html.append("</div>");
// 文章内容
html.append("<div class=\"article-content\">");
html.append(cleanHtmlContent(resource.getContent()));
html.append("</div>");
html.append("</body>");
html.append("</html>");
return html.toString();
}
/**
* 获取CSS样式
*/
private String getStyleContent() {
return """
@page {
size: A4;
margin: 2cm;
}
body {
font-family: SimSun, 'Microsoft YaHei', Arial, sans-serif;
font-size: 12pt;
line-height: 1.8;
color: #333;
}
.article-header {
margin-bottom: 30px;
border-bottom: 2px solid #1890ff;
padding-bottom: 20px;
}
.title {
font-size: 22pt;
font-weight: bold;
color: #1a1a1a;
margin-bottom: 15px;
text-align: center;
}
.meta {
font-size: 10pt;
color: #666;
text-align: center;
}
.meta span {
margin-right: 20px;
}
.article-content {
text-align: justify;
}
.article-content p {
text-indent: 2em;
margin-bottom: 12px;
}
.article-content img {
max-width: 100%;
height: auto;
display: block;
margin: 15px auto;
}
.article-content h1, .article-content h2, .article-content h3 {
margin-top: 20px;
margin-bottom: 10px;
color: #1a1a1a;
}
.article-content table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
}
.article-content table td, .article-content table th {
border: 1px solid #ddd;
padding: 8px;
}
.article-content blockquote {
border-left: 4px solid #1890ff;
padding-left: 15px;
margin: 15px 0;
color: #666;
}
""";
}
/**
* 清理HTML内容使其符合XHTML规范
*/
private String cleanHtmlContent(String htmlContent) {
if (htmlContent == null || htmlContent.isEmpty()) {
return "";
}
// 使用Jsoup解析和清理HTML
Document doc = Jsoup.parseBodyFragment(htmlContent);
doc.outputSettings()
.syntax(Document.OutputSettings.Syntax.xml)
.escapeMode(org.jsoup.nodes.Entities.EscapeMode.xhtml)
.charset(StandardCharsets.UTF_8);
// 处理图片标签,确保闭合
doc.select("img").forEach(img -> {
if (!img.hasAttr("alt")) {
img.attr("alt", "");
}
});
// 处理br标签
doc.select("br").forEach(br -> br.tagName("br"));
// 移除不支持的标签和属性
String cleaned = Jsoup.clean(doc.body().html(), "",
Safelist.relaxed()
.addTags("div", "span", "p", "br", "h1", "h2", "h3", "h4", "h5", "h6")
.addTags("table", "thead", "tbody", "tr", "td", "th")
.addTags("ul", "ol", "li", "blockquote", "pre", "code")
.addTags("strong", "em", "b", "i", "u", "s", "sub", "sup")
.addTags("img", "a")
.addAttributes("img", "src", "alt", "width", "height")
.addAttributes("a", "href")
.addAttributes(":all", "style", "class"),
new Document.OutputSettings().syntax(Document.OutputSettings.Syntax.xml));
return cleaned;
}
/**
* HTML转义
*/
private String escapeHtml(String text) {
if (text == null) {
return "";
}
return text.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#39;");
}
/**
* 添加中文字体支持
*/
private void addFontSupport(ITextRenderer renderer) {
try {
// 尝试添加系统字体
String[] fontPaths = {
"C:/Windows/Fonts/simsun.ttc", // Windows 宋体
"C:/Windows/Fonts/msyh.ttc", // Windows 微软雅黑
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", // Linux 文泉驿微米黑
"/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", // Linux 文泉驿正黑
"/System/Library/Fonts/PingFang.ttc" // macOS 苹方
};
for (String fontPath : fontPaths) {
File fontFile = new File(fontPath);
if (fontFile.exists()) {
renderer.getFontResolver().addFont(
fontPath,
BaseFont.IDENTITY_H,
BaseFont.NOT_EMBEDDED
);
logger.debug("已加载字体: {}", fontPath);
}
}
} catch (Exception e) {
logger.warn("加载字体失败,将使用默认字体: {}", e.getMessage());
}
}
}

View File

@@ -21,6 +21,9 @@
<result column="is_audited" property="isAudited" jdbcType="BOOLEAN"/>
<result column="is_recommend" property="isRecommend" jdbcType="BOOLEAN"/>
<result column="is_banner" property="isBanner" jdbcType="BOOLEAN"/>
<result column="is_in_knowledge" property="isInKnowledge" jdbcType="BOOLEAN"/>
<result column="knowledge_file_id" property="knowledgeFileId" jdbcType="VARCHAR"/>
<result column="knowledge_import_time" property="knowledgeImportTime" jdbcType="TIMESTAMP"/>
<result column="publish_time" property="publishTime" jdbcType="TIMESTAMP"/>
<result column="creator" property="creator" jdbcType="VARCHAR"/>
<result column="updater" property="updater" jdbcType="VARCHAR"/>
@@ -34,8 +37,8 @@
<sql id="Base_Column_List">
id, resource_id, title, content, summary, cover_image, tag_id, author, source,
source_url, view_count, like_count, collect_count, status, is_audited, is_recommend,
is_banner, publish_time, creator, updater, create_time, update_time,
delete_time, deleted
is_banner, is_in_knowledge, knowledge_file_id, knowledge_import_time, publish_time,
creator, updater, create_time, update_time, delete_time, deleted
</sql>
<!-- 通用条件 -->
@@ -260,6 +263,15 @@
<if test="isBanner != null">
is_banner = #{isBanner},
</if>
<if test="isInKnowledge != null">
is_in_knowledge = #{isInKnowledge},
</if>
<if test="knowledgeFileId != null">
knowledge_file_id = #{knowledgeFileId},
</if>
<if test="knowledgeImportTime != null">
knowledge_import_time = #{knowledgeImportTime},
</if>
<if test="isAudited != null">
is_audited = #{isAudited},
</if>
@@ -368,6 +380,9 @@
<result column="is_audited" property="isAudited" jdbcType="BOOLEAN"/>
<result column="is_recommend" property="isRecommend" jdbcType="BOOLEAN"/>
<result column="is_banner" property="isBanner" jdbcType="BOOLEAN"/>
<result column="is_in_knowledge" property="isInKnowledge" jdbcType="BOOLEAN"/>
<result column="knowledge_file_id" property="knowledgeFileId" jdbcType="VARCHAR"/>
<result column="knowledge_import_time" property="knowledgeImportTime" jdbcType="TIMESTAMP"/>
<result column="publish_time" property="publishTime" jdbcType="TIMESTAMP"/>
<result column="creator" property="creator" jdbcType="VARCHAR"/>
<result column="updater" property="updater" jdbcType="VARCHAR"/>