博客系统实战
使用 LCGYL Framework 构建一个完整的博客系统。
项目概述
功能特性
- 用户注册和登录
- 文章发布和编辑
- 评论系统
- 标签和分类
- 文章搜索
- 用户关注
- 点赞和收藏
技术栈
- LCGYL Framework
- MySQL 数据库
- Redis 缓存
- Elasticsearch 搜索
数据库设计
用户表
sql
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
avatar VARCHAR(255),
bio TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);文章表
sql
CREATE TABLE articles (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
title VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
summary VARCHAR(500),
cover_image VARCHAR(255),
view_count INT DEFAULT 0,
like_count INT DEFAULT 0,
comment_count INT DEFAULT 0,
status VARCHAR(20) DEFAULT 'DRAFT',
published_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);评论表
sql
CREATE TABLE comments (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
article_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
content TEXT NOT NULL,
parent_id BIGINT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (article_id) REFERENCES articles(id),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (parent_id) REFERENCES comments(id)
);实体类
User 实体
java
public class User {
private Long id;
private String username;
private String email;
private String password;
private String avatar;
private String bio;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// Getters and Setters
}Article 实体
java
public class Article {
private Long id;
private Long userId;
private String title;
private String content;
private String summary;
private String coverImage;
private Integer viewCount;
private Integer likeCount;
private Integer commentCount;
private ArticleStatus status;
private LocalDateTime publishedAt;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// Getters and Setters
}
public enum ArticleStatus {
DRAFT, // 草稿
PUBLISHED, // 已发布
ARCHIVED // 已归档
}Repository 层
UserRepository
java
@Component
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
@Inject
public UserRepository(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public Optional<User> findById(Long id) {
String sql = "SELECT * FROM users WHERE id = ?";
return jdbcTemplate.queryForOptional(sql, User.class, id);
}
public Optional<User> findByUsername(String username) {
String sql = "SELECT * FROM users WHERE username = ?";
return jdbcTemplate.queryForOptional(sql, User.class, username);
}
public Optional<User> findByEmail(String email) {
String sql = "SELECT * FROM users WHERE email = ?";
return jdbcTemplate.queryForOptional(sql, User.class, email);
}
@Transactional
public User save(User user) {
if (user.getId() == null) {
String sql = "INSERT INTO users (username, email, password, avatar, bio) " +
"VALUES (?, ?, ?, ?, ?)";
Long id = jdbcTemplate.insert(sql,
user.getUsername(),
user.getEmail(),
user.getPassword(),
user.getAvatar(),
user.getBio()
);
user.setId(id);
} else {
String sql = "UPDATE users SET username = ?, email = ?, avatar = ?, bio = ? " +
"WHERE id = ?";
jdbcTemplate.update(sql,
user.getUsername(),
user.getEmail(),
user.getAvatar(),
user.getBio(),
user.getId()
);
}
return user;
}
}ArticleRepository
java
@Component
public class ArticleRepository {
private final JdbcTemplate jdbcTemplate;
public Optional<Article> findById(Long id) {
String sql = "SELECT * FROM articles WHERE id = ?";
return jdbcTemplate.queryForOptional(sql, Article.class, id);
}
public Page<Article> findByUserId(Long userId, int page, int size) {
String countSql = "SELECT COUNT(*) FROM articles WHERE user_id = ?";
long total = jdbcTemplate.queryForLong(countSql, userId);
String sql = "SELECT * FROM articles WHERE user_id = ? " +
"ORDER BY created_at DESC LIMIT ? OFFSET ?";
List<Article> articles = jdbcTemplate.queryForList(sql, Article.class,
userId, size, page * size);
return new Page<>(articles, page, size, total);
}
public Page<Article> findPublished(int page, int size) {
String countSql = "SELECT COUNT(*) FROM articles WHERE status = 'PUBLISHED'";
long total = jdbcTemplate.queryForLong(countSql);
String sql = "SELECT * FROM articles WHERE status = 'PUBLISHED' " +
"ORDER BY published_at DESC LIMIT ? OFFSET ?";
List<Article> articles = jdbcTemplate.queryForList(sql, Article.class,
size, page * size);
return new Page<>(articles, page, size, total);
}
@Transactional
public Article save(Article article) {
if (article.getId() == null) {
String sql = "INSERT INTO articles (user_id, title, content, summary, " +
"cover_image, status, published_at) VALUES (?, ?, ?, ?, ?, ?, ?)";
Long id = jdbcTemplate.insert(sql,
article.getUserId(),
article.getTitle(),
article.getContent(),
article.getSummary(),
article.getCoverImage(),
article.getStatus().name(),
article.getPublishedAt()
);
article.setId(id);
} else {
String sql = "UPDATE articles SET title = ?, content = ?, summary = ?, " +
"cover_image = ?, status = ?, published_at = ? WHERE id = ?";
jdbcTemplate.update(sql,
article.getTitle(),
article.getContent(),
article.getSummary(),
article.getCoverImage(),
article.getStatus().name(),
article.getPublishedAt(),
article.getId()
);
}
return article;
}
@Transactional
public void incrementViewCount(Long articleId) {
String sql = "UPDATE articles SET view_count = view_count + 1 WHERE id = ?";
jdbcTemplate.update(sql, articleId);
}
}Service 层
UserService
java
@Component
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider tokenProvider;
@Inject
public UserService(UserRepository userRepository,
PasswordEncoder passwordEncoder,
JwtTokenProvider tokenProvider) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.tokenProvider = tokenProvider;
}
@Transactional
public User register(RegisterRequest request) {
// 检查用户名是否已存在
if (userRepository.findByUsername(request.getUsername()).isPresent()) {
throw new BusinessException("用户名已存在");
}
// 检查邮箱是否已存在
if (userRepository.findByEmail(request.getEmail()).isPresent()) {
throw new BusinessException("邮箱已被注册");
}
// 创建用户
User user = new User();
user.setUsername(request.getUsername());
user.setEmail(request.getEmail());
user.setPassword(passwordEncoder.encode(request.getPassword()));
return userRepository.save(user);
}
public String login(LoginRequest request) {
// 查找用户
User user = userRepository.findByUsername(request.getUsername())
.orElseThrow(() -> new BusinessException("用户名或密码错误"));
// 验证密码
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
throw new BusinessException("用户名或密码错误");
}
// 生成 Token
Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId());
claims.put("username", user.getUsername());
return tokenProvider.generateToken(claims);
}
public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new NotFoundException("用户不存在"));
}
@Transactional
public User updateProfile(Long userId, UpdateProfileRequest request) {
User user = getUserById(userId);
user.setAvatar(request.getAvatar());
user.setBio(request.getBio());
return userRepository.save(user);
}
}ArticleService
java
@Component
public class ArticleService {
private final ArticleRepository articleRepository;
private final UserRepository userRepository;
private final EventBus eventBus;
private final Cache<Long, Article> articleCache;
@Inject
public ArticleService(ArticleRepository articleRepository,
UserRepository userRepository,
EventBus eventBus) {
this.articleRepository = articleRepository;
this.userRepository = userRepository;
this.eventBus = eventBus;
this.articleCache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
}
@Transactional
public Article createArticle(Long userId, CreateArticleRequest request) {
// 验证用户
userRepository.findById(userId)
.orElseThrow(() -> new NotFoundException("用户不存在"));
// 创建文章
Article article = new Article();
article.setUserId(userId);
article.setTitle(request.getTitle());
article.setContent(request.getContent());
article.setSummary(request.getSummary());
article.setCoverImage(request.getCoverImage());
article.setStatus(ArticleStatus.DRAFT);
article = articleRepository.save(article);
// 发布事件
eventBus.publish(new ArticleCreatedEvent(article));
return article;
}
@Transactional
public Article publishArticle(Long userId, Long articleId) {
Article article = getArticleById(articleId);
// 验证权限
if (!article.getUserId().equals(userId)) {
throw new ForbiddenException("无权操作此文章");
}
// 发布文章
article.setStatus(ArticleStatus.PUBLISHED);
article.setPublishedAt(LocalDateTime.now());
article = articleRepository.save(article);
// 清除缓存
articleCache.invalidate(articleId);
// 发布事件
eventBus.publish(new ArticlePublishedEvent(article));
return article;
}
public Article getArticleById(Long id) {
return articleCache.get(id, () -> {
Article article = articleRepository.findById(id)
.orElseThrow(() -> new NotFoundException("文章不存在"));
// 增加浏览量
articleRepository.incrementViewCount(id);
return article;
});
}
public Page<Article> getPublishedArticles(int page, int size) {
return articleRepository.findPublished(page, size);
}
public Page<Article> getUserArticles(Long userId, int page, int size) {
return articleRepository.findByUserId(userId, page, size);
}
}Controller 层
UserController
java
@RestController("/api/users")
public class UserController {
private final UserService userService;
@PostMapping("/register")
public ApiResponse<User> register(@RequestBody @Valid RegisterRequest request) {
User user = userService.register(request);
return ApiResponse.success(user);
}
@PostMapping("/login")
public ApiResponse<LoginResponse> login(@RequestBody @Valid LoginRequest request) {
String token = userService.login(request);
return ApiResponse.success(new LoginResponse(token));
}
@GetMapping("/{id}")
public ApiResponse<User> getUser(@PathVariable Long id) {
User user = userService.getUserById(id);
return ApiResponse.success(user);
}
@PutMapping("/profile")
@RequiresAuthentication
public ApiResponse<User> updateProfile(
@RequestBody @Valid UpdateProfileRequest request,
@CurrentUser User currentUser
) {
User user = userService.updateProfile(currentUser.getId(), request);
return ApiResponse.success(user);
}
}ArticleController
java
@RestController("/api/articles")
public class ArticleController {
private final ArticleService articleService;
@PostMapping
@RequiresAuthentication
public ApiResponse<Article> createArticle(
@RequestBody @Valid CreateArticleRequest request,
@CurrentUser User currentUser
) {
Article article = articleService.createArticle(currentUser.getId(), request);
return ApiResponse.success(article);
}
@PostMapping("/{id}/publish")
@RequiresAuthentication
public ApiResponse<Article> publishArticle(
@PathVariable Long id,
@CurrentUser User currentUser
) {
Article article = articleService.publishArticle(currentUser.getId(), id);
return ApiResponse.success(article);
}
@GetMapping("/{id}")
public ApiResponse<Article> getArticle(@PathVariable Long id) {
Article article = articleService.getArticleById(id);
return ApiResponse.success(article);
}
@GetMapping
public ApiResponse<Page<Article>> listArticles(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size
) {
Page<Article> articles = articleService.getPublishedArticles(page, size);
return ApiResponse.success(articles);
}
@GetMapping("/user/{userId}")
public ApiResponse<Page<Article>> getUserArticles(
@PathVariable Long userId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size
) {
Page<Article> articles = articleService.getUserArticles(userId, page, size);
return ApiResponse.success(articles);
}
}事件处理
ArticleCreatedEvent
java
public record ArticleCreatedEvent(Article article) implements Event {
}
@MessageListener(topic = "article.created")
public class ArticleCreatedListener {
private final SearchService searchService;
private final NotificationService notificationService;
public void onArticleCreated(ArticleCreatedEvent event) {
Article article = event.article();
// 索引到搜索引擎
searchService.indexArticle(article);
// 通知关注者
notificationService.notifyFollowers(article.getUserId(),
"发布了新文章:" + article.getTitle());
}
}运行应用
java
@ComponentScan("com.example.blog")
public class BlogApplication {
public static void main(String[] args) {
Application app = Application.run(BlogApplication.class, args);
app.startWebServer(8080);
}
}