【知识获取与分享社区项目 | 项目日记第 4 天】元数据完善、DeepSeek 摘要生成与正式发布实现

前言
上一篇我们已经完成了发布系统的前半段:创建草稿、OSS 预签名直传和内容确认。
今天我们来继续整理后半段:更新标题、标签、图片列表、可见性、摘要,并接入 DeepSeek AI 实现一键生成文章摘要,最后将草稿正式发布。
这部分的流程是:
上传确认完成 -> 更新元数据 -> AI 生成摘要 -> 保存摘要 -> 发布草稿
一、元数据更新功能
1. 功能需求
正文上传到 OSS 之后,还需要补充这些信息:
- 标题
- 分类 ID
- 标签列表
- 图片 URL 列表
- 可见性
- 是否置顶
- 摘要描述
接口路径:
PATCH /api/v1/knowposts/{id}
请求 DTO:
public record KnowPostPatchRequest(
String title,
Long tagId,
@Size(max = 20) List<String> tags,
@Size(max = 20) List<String> imgUrls,
String visible,
Boolean isTop,
String description
) {}
这里的 tags 和 imgUrls 最多 20 个,避免一次提交过大的数组。
二、更新元数据实现
Controller 层:
@PatchMapping("/{id}")
public ResponseEntity<Void> patchMetadata(@PathVariable("id") long id,
@Valid @RequestBody KnowPostPatchRequest request,
@AuthenticationPrincipal Jwt jwt) {
long userId = jwtService.extractUserId(jwt);
service.updateMetadata(
userId,
id,
request.title(),
request.tagId(),
request.tags(),
request.imgUrls(),
request.visible(),
request.isTop(),
request.description()
);
return ResponseEntity.noContent().build();
}
Service 层:
@Transactional
public void updateMetadata(long creatorId,
long id,
String title,
Long tagId,
List<String> tags,
List<String> imgUrls,
String visible,
Boolean isTop,
String description) {
invalidateCache(id);
KnowPost post = KnowPost.builder()
.id(id)
.creatorId(creatorId)
.title(title)
.tagId(tagId)
.tags(toJsonOrNull(tags))
.imgUrls(toJsonOrNull(imgUrls))
.visible(visible)
.isTop(isTop)
.description(description)
.type("image_text")
.updateTime(Instant.now())
.build();
int updated = mapper.updateMetadata(post);
if (updated == 0) {
throw new BusinessException(ErrorCode.BAD_REQUEST, "草稿不存在或无权限");
}
try {
long outId = idGen.nextId();
String payload = objectMapper.writeValueAsString(
Map.of("entity", "knowpost", "op", "upsert", "id", id)
);
outboxMapper.insert(outId, "knowpost", id, "KnowPostMetadataUpdated", payload);
} catch (Exception e) {
log.warn("Outbox event after metadata update failed, post {}: {}", id, e.getMessage());
}
invalidateCache(id);
}
这里有两个重点。
第一,tags 和 imgUrls 会转成 JSON 字符串:
private String toJsonOrNull(List<String> list) {
if (list == null) {
return null;
}
try {
return objectMapper.writeValueAsString(list);
} catch (JsonProcessingException e) {
throw new BusinessException(ErrorCode.BAD_REQUEST, "JSON 处理失败");
}
}
第二,元数据更新后会写入 Outbox 事件,后续可以驱动搜索索引更新。
三、Mapper 动态 SQL
<update id="updateMetadata" parameterType="com.tongji.knowpost.model.KnowPost">
UPDATE know_posts
<set>
<if test="title != null">title = #{title},</if>
<if test="tagId != null">tag_id = #{tagId},</if>
<if test="tags != null">tags = #{tags},</if>
<if test="imgUrls != null">img_urls = #{imgUrls},</if>
<if test="visible != null">visible = #{visible},</if>
<if test="isTop != null">is_top = #{isTop},</if>
<if test="description != null">description = #{description},</if>
<if test="type != null">type = #{type},</if>
update_time = #{updateTime}
</set>
WHERE id = #{id} AND creator_id = #{creatorId}
</update>
这里使用 MyBatis 动态 SQL,非常适合草稿编辑。
前端传什么字段,就更新什么字段;没传的字段保持不变。
四、DeepSeek AI 生成文章摘要
1. 功能需求
摘要生成接口路径:
POST /api/v1/knowposts/description/suggest
请求体:
{
"content": "这里是 Markdown 正文内容..."
}
响应:
{
"description": "生成的不超过50字的中文描述"
}
这个接口只负责生成摘要,不直接保存数据库。前端拿到摘要后,再通过元数据更新接口保存到 description 字段。
2. Controller 层实现
@RestController
@RequestMapping(path = "/api/v1/knowposts", produces = MediaType.APPLICATION_JSON_VALUE)
@RequiredArgsConstructor
public class KnowPostAiController {
private final KnowPostDescriptionService descriptionService;
@PostMapping(path = "/description/suggest",
consumes = MediaType.APPLICATION_JSON_VALUE)
public DescriptionSuggestResponse suggest(
@Valid @RequestBody DescriptionSuggestRequest req) {
String desc = descriptionService.generateDescription(req.content());
return new DescriptionSuggestResponse(desc);
}
}
请求 DTO:
public record DescriptionSuggestRequest(
@NotBlank(message = "content 不能为空") String content
) {}
五、Spring AI 接入 DeepSeek
项目通过 Spring AI 接入 DeepSeek:
@Configuration
public class LlmConfig {
@Bean
public ChatClient chatClient(
@Qualifier("deepSeekChatModel") ChatModel chatModel) {
return ChatClient.builder(chatModel).build();
}
}
依赖如下:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-deepseek</artifactId>
</dependency>
摘要生成 Service:
@Service
@RequiredArgsConstructor
public class KnowPostDescriptionServiceImpl implements KnowPostDescriptionService {
private final ChatClient chatClient;
public String generateDescription(String content) {
if (content == null || content.trim().isEmpty()) {
throw new BusinessException(ErrorCode.BAD_REQUEST, "正文内容不能为空");
}
String system = "你是中文文案编辑。请基于用户提供的知文正文,生成一个中文描述,简洁有吸引力,且不超过50个汉字。不输出解释或多段,只输出结果。";
String user = "正文如下:\n\n" + content + "\n\n请直接给出不超过50字的中文描述。";
try {
String result = chatClient
.prompt()
.system(system)
.user(user)
.options(DeepSeekChatOptions.builder()
.model("deepseek-chat")
.temperature(0.8)
.maxTokens(120)
.build())
.call()
.content();
return postProcess(result);
} catch (Exception e) {
throw new BusinessException(
ErrorCode.INTERNAL_ERROR,
"大模型调用失败: " + e.getMessage()
);
}
}
}
这里使用 system 约束模型输出风格,使用 user 提供正文内容,最终调用 deepseek-chat 生成摘要。
六、摘要后处理
private String postProcess(String text) {
if (text == null) {
return "";
}
String t = Normalizer.normalize(text, Normalizer.Form.NFKC)
.replaceAll("\r\n|\r|\n", " ")
.replaceAll("\\s+", " ")
.trim();
t = t.replaceAll("^[\"'“”‘’]+|[\"'“”‘’]+$", "")
.replaceAll("[。!!??;;、]+$", "");
int limit = 50;
int count = t.codePointCount(0, t.length());
if (count <= limit) {
return t;
}
StringBuilder sb = new StringBuilder();
int i = 0, added = 0;
while (i < t.length() && added < limit) {
int cp = t.codePointAt(i);
sb.appendCodePoint(cp);
i += Character.charCount(cp);
added++;
}
return sb.toString();
}
大模型输出不是强约束结果,所以后端必须做兜底处理。
这里做了这些事情:
- 去掉换行
- 合并空格
- 去掉前后引号
- 去掉结尾多余标点
- 截断到 50 字以内
这一步可以保证最终写入 description VARCHAR(50) 的内容更加稳定。
七、正式发布草稿
1. 功能需求
当正文、图片、标题、标签、摘要都准备好后,调用发布接口:
POST /api/v1/knowposts/{id}/publish
2. Controller 层实现
@PostMapping("/{id}/publish")
public ResponseEntity<Void> publish(@PathVariable("id") long id,
@AuthenticationPrincipal Jwt jwt) {
long userId = jwtService.extractUserId(jwt);
service.publish(userId, id);
return ResponseEntity.noContent().build();
}
3. Service 层实现
@Transactional
public void publish(long creatorId, long id) {
int updated = mapper.publish(id, creatorId);
if (updated == 0) {
throw new BusinessException(ErrorCode.BAD_REQUEST, "草稿不存在或无权限");
}
try {
userCounterService.incrementPosts(creatorId, 1);
} catch (Exception ignored) {}
try {
long outId = idGen.nextId();
String payload = objectMapper.writeValueAsString(
Map.of("entity", "knowpost", "op", "upsert", "id", id)
);
outboxMapper.insert(outId, "knowpost", id, "KnowPostPublished", payload);
} catch (Exception e) {
log.warn("Outbox event after publish failed, post {}: {}", id, e.getMessage());
}
try {
ragIndexService.ensureIndexed(id);
} catch (Exception e) {
log.warn("Pre-index after publish failed, post {}: {}", id, e.getMessage());
}
}
发布成功后,系统做了几件事:
- 状态改为
published - 写入发布时间
- 用户作品数加一
- 写入 Outbox 事件,驱动搜索索引更新
- 触发 RAG 预索引,减少后续问答冷启动
4. Mapper 层实现
<update id="publish">
UPDATE know_posts
SET status = 'published',
publish_time = NOW(),
update_time = NOW()
WHERE id = #{id}
AND creator_id = #{creatorId}
</update>
这里依然通过 creator_id 控制权限,保证用户只能发布自己的草稿。
八、知识点总结
1. MyBatis 动态 SQL
元数据更新使用 <set> 和 <if>,可以实现部分字段更新,非常适合草稿编辑场景。
2. AI 生成结果要做业务兜底
DeepSeek 可以生成摘要,但后端仍然要做长度截断、格式清理和异常处理,不能完全依赖模型输出。
3. 发布动作不只是改状态
正式发布后,系统还会联动用户计数、搜索索引和 RAG 预索引。
这说明发布系统不是孤立模块,而是内容平台很多后续能力的入口。
总结
这一篇主要整理了发布系统的后半段:元数据更新、DeepSeek 摘要生成、摘要保存和正式发布。
到这里,知文从一个空草稿,逐步补充正文、图片、标题、标签、摘要,最后变成一篇已发布内容。相比普通 CRUD,这个模块更贴近真实内容平台的业务流程,也体现了“发布系统”作为内容生产入口的价值。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)