Skip to content

博客系统实战

使用 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);
    }
}

下一步

Released under the Apache License 2.0