Skip to content

feat: posts 模块——用户原创文章直接落库#35

Merged
longsizhuo merged 2 commits into
mainfrom
feat/posts-module
May 24, 2026
Merged

feat: posts 模块——用户原创文章直接落库#35
longsizhuo merged 2 commits into
mainfrom
feat/posts-module

Conversation

@longsizhuo
Copy link
Copy Markdown
Member

背景

实现计划 A 节:站内编辑器写完后直接 POST /api/posts 落库,不走 Git PR 流程。文章在 /feed 原创 Tab 和个人主页展示,带"转正"入口可一键跳 GitHub 发 PR 升级为 Fumadocs contributor。

变更内容

数据库(src/main/resources/schema.sql

  • 追加 posts 表:author_id FKslug(作者内唯一)、titledescriptiontags JSONBcontent_mdcover_urlvisibilitystatuspromoted_pr_urlpromoted_atview_count、时间戳
  • 追加 idx_posts_authoridx_posts_feed 索引
  • SPRING_SQL_INIT_MODE=always 启动自动建表,无需手动迁移,幂等

新增包 com.involutionhell.backend.posts

文件
model Post record、PostStatusPostVisibility 常量类
dto PostRequest(写请求)、PostView(详情含 contentMd)、PostSummaryView(列表摘要)
repository PostRepository 接口 + JdbcPostRepository(裸 JDBC,镜像 JdbcSharedLinkRepository,tags JSONB 用 Types.OTHER
service PostService:slug 自动生成(title→kebab-case+去重后缀)、owner 校验(非作者返回 403)、分页 feed
controller PostController:7 个端点

7 个 API 端点

方法 路径 鉴权
POST /api/posts 需登录
PUT /api/posts/{id} 需登录 + owner
DELETE /api/posts/{id} 需登录 + owner
GET /api/posts/mine 需登录
GET /api/posts/feed 公开
GET /api/posts/{username}/{slug} 公开
POST /api/posts/{id}/promote 需登录 + owner

SaTokenConfigure

  • GET /api/posts/feedGET /api/posts/*/* 加入公开白名单
  • 写接口由方法级 @SaCheckLogin 守卫

文档

  • docs/posts/README.md:完整 API 契约 + 数据库表结构 + 部署说明
  • docs/dev1.md:端点速查表新增 posts 入口

验证

本地 curl 全部 7 个端点通过(backend 8081 + PostgreSQL 127.0.0.1:5432):

  • 创建(201)、更新(200)、删除(200)、mine(200)、feed 匿名访问(200)、详情匿名访问(200)、promote(200)
  • owner 校验:admin 操作 alice 文章返回 403

部署清单(上线时)

feat/posts-module 合并 main 后,重建后端镜像时所有变更一次性上线:

cd /home/ubuntu/involution-hell
git pull origin main
docker compose build backend
docker compose up -d backend

重点:新镜像启动时 schema.sql 自动建 posts 表(IF NOT EXISTS 幂等)SaTokenConfigure 白名单随新镜像生效,线上 GET /api/posts/feedGET /api/posts/*/* 匿名访问恢复正常。当前 8080 生产实例是旧镜像(2026-04-21),上线前这两个公开接口会 401,属预期。

镜像 community/SharedLink 的 JDBC 范式,新增完整的 posts 功能:
- schema.sql 追加 posts 表(JSONB tags、slug 唯一约束、feed 索引)
- model/Post record + PostStatus / PostVisibility 常量类
- dto/PostRequest + PostView + PostSummaryView(含作者信息冗余)
- JdbcPostRepository:JSONB tags 读写、slug 前缀去重查询
- PostService:slug 自动生成(title→kebab-case+去重后缀)、owner 校验
- PostController:7 个端点(创建/更新/删除/mine/feed/详情/转正)
- SaTokenConfigure 白名单:GET /api/posts/feed + GET /api/posts/*/*
Copilot AI review requested due to automatic review settings May 24, 2026 15:45
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

该 PR 为后端新增 posts 模块,使站内编辑器产出的原创文章可直接通过 REST API 落库并在 /feed 与详情页公开展示,同时提供“转正”记录入口以衔接后续 GitHub PR 流程。

Changes:

  • schema.sql 中新增 posts 表及相关索引(作者维度、feed 维度)。
  • 新增 com.involutionhell.backend.posts 包:model/dto/repository/service/controller 完整链路与 7 个 API 端点。
  • Sa-Token 放行 posts 公开读接口,并补充 docs 文档与开发速查表。

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/main/resources/schema.sql 新增 posts 表结构与索引以支持原创文章落库与 feed 查询
src/main/java/com/involutionhell/backend/posts/service/PostService.java 核心业务逻辑:slug 生成/去重、owner 校验、feed 分页与视图组装
src/main/java/com/involutionhell/backend/posts/repository/PostRepository.java posts 数据访问接口定义
src/main/java/com/involutionhell/backend/posts/repository/JdbcPostRepository.java 基于 Spring JDBC 的 posts 表读写实现(含 JSONB tags 处理)
src/main/java/com/involutionhell/backend/posts/controller/PostController.java 暴露 posts 的 7 个 REST 端点并接入 Sa-Token 登录态
src/main/java/com/involutionhell/backend/common/config/SaTokenConfigure.java 将 posts 公开读接口加入匿名白名单
src/main/java/com/involutionhell/backend/posts/README.md 新增模块内 README(职责/端点概览)
src/main/java/com/involutionhell/backend/posts/model/PostVisibility.java 可见性常量定义
src/main/java/com/involutionhell/backend/posts/model/PostStatus.java 状态常量定义
src/main/java/com/involutionhell/backend/posts/model/Post.java posts 领域对象 record
src/main/java/com/involutionhell/backend/posts/dto/PostView.java 详情视图 DTO(含 contentMd)
src/main/java/com/involutionhell/backend/posts/dto/PostSummaryView.java 列表摘要 DTO(不含 contentMd)
src/main/java/com/involutionhell/backend/posts/dto/PostRequest.java 写请求 DTO
docs/posts/README.md 对外 API 契约与表结构/部署说明文档
docs/dev1.md 开发端点速查表补充 posts 相关入口

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +135 to +146
// slug 处理:前端可传新 slug(改 URL 场景),不传则沿用旧 slug
String newSlug = (req.slug() != null && !req.slug().isBlank())
? req.slug()
: existing.slug();

// 若 slug 改变,需要检查新 slug 是否与该作者其他文章冲突
if (!newSlug.equals(existing.slug())) {
boolean taken = postRepo.findByAuthorAndSlug(callerId, newSlug).isPresent();
if (taken) {
throw new IllegalArgumentException("slug 已被使用:" + newSlug);
}
}
Comment on lines +273 to +279
/**
* 按 id 查文章,不存在则抛 404 语义的 IllegalArgumentException。
*/
private Post requirePost(Long postId) {
return postRepo.findById(postId)
.orElseThrow(() -> new IllegalArgumentException("文章不存在:" + postId));
}
Comment on lines +47 to +53
@PostMapping
@SaCheckLogin
public ResponseEntity<ApiResponse<PostView>> create(@RequestBody PostRequest req) {
long authorId = StpUtil.getLoginIdAsLong();
PostView view = postService.create(authorId, req);
return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.ok(view));
}
Comment on lines +59 to +66
@PutMapping("/{id}")
@SaCheckLogin
public ApiResponse<PostView> update(@PathVariable Long id,
@RequestBody PostRequest req) {
long callerId = StpUtil.getLoginIdAsLong();
PostView view = postService.update(callerId, id, req);
return ApiResponse.ok(view);
}
Comment on lines +245 to +256
List<Post> posts = postRepo.findFeed(safeLimit, safeOffset);

// 批量拼作者信息:每篇独立查一次(MVP 量级可接受;后续可加缓存或 JOIN 优化)
return posts.stream()
.map(p -> {
UserAccount author = userAccountRepository.findById(p.authorId()).orElse(null);
String username = author != null ? author.username() : "unknown";
String displayName = author != null ? author.displayName() : "";
String avatarUrl = author != null ? author.avatarUrl() : null;
return PostSummaryView.from(p, username, displayName, avatarUrl);
})
.toList();
Comment on lines +152 to +156
log.info("post updated: id={} author={}", postId, callerId);

Post updated = postRepo.findById(postId)
.orElseThrow(() -> new IllegalStateException("post not found after update: " + postId));
return buildView(updated);
@longsizhuo longsizhuo merged commit 1ceda79 into main May 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants