Spring Boot + MyBatis-Plus 构建粉丝空间站:从数据库设计到API开发实战

Spring BootMyBatis-Plus粉丝空间站
于 2026-08-04 04:25:12 修改
·本内容遵循CC 4.0 BY-SA版权协议

最近在开发社区类应用时,很多开发者都希望为自己的用户打造一个专属的、可互动的个人主页。这种需求催生了“粉丝空间站”或“个人中心”模块的流行。它不仅仅是展示用户信息的静态页面,更是集动态发布、粉丝互动、成就展示于一体的综合性功能。本文将手把手带你从零搭建一个功能完整的“粉丝空间站”后端服务,涵盖数据库设计、核心接口开发、粉丝关系处理以及性能优化等关键环节。无论你是想为个人项目增添亮点,还是为企业级应用开发用户社区,这套方案都能提供清晰的实现路径和可复用的代码。

1. 核心概念与业务场景分析

“粉丝空间站”本质上是一个以用户为中心的子应用,它聚合了用户相关的所有动态、社交关系和个性化数据。理解其核心构成是设计的第一步。

1.1 什么是粉丝空间站?

在技术实现层面,一个典型的粉丝空间站包含以下几个核心模块:

  1. 用户主页:展示用户的基础信息(头像、昵称、简介)、统计数据(粉丝数、关注数、获赞数)以及个人标签。
  2. 内容动态流:展示该用户发布的所有内容,如文章、视频、状态等,通常按时间倒序排列。
  3. 社交关系:清晰展示“关注”与“粉丝”列表,并处理“互相关注”等关系状态。
  4. 互动功能:允许访客在空间站内进行点赞、评论、私信等操作。
  5. 成就与勋章系统:展示用户的等级、勋章、活跃度等,增强用户粘性和荣誉感。

从架构上看,它不是一个独立的服务,而是深度依赖用户服务、内容服务、关系服务和消息服务的聚合型展示层。

1.2 关键业务逻辑与挑战

实现空间站面临几个典型挑战:

  • 数据聚合:一个页面需要调用多个微服务接口,如何保证高效和数据的最终一致性?
  • 关系状态判断:当前访客与空间站主人的关系(未关注、已关注、互相关注、自己)需要实时、准确地判断。
  • 动态列表分页:用户发布的内容可能非常多,需要高效的分页查询,并可能涉及多种内容类型的混合排序。
  • 访问控制与隐私:某些动态或信息可能仅对粉丝或好友可见,需要精细的权限校验。

本文将采用 Spring Boot + MyBatis-Plus 作为核心框架,MySQL 作为数据库,通过模块化的设计逐一解决这些问题。

2. 环境准备与项目初始化

在开始编码前,我们需要搭建好开发环境并初始化项目结构。

2.1 技术栈与版本说明

  • 后端框架:Spring Boot 2.7.x (本文示例使用 2.7.18)
  • Java SDK:JDK 8 或 11 (本文使用 JDK 11)
  • 构建工具:Maven 3.6+
  • 数据库:MySQL 5.7+ (本文使用 MySQL 8.0)
  • ORM框架:MyBatis-Plus 3.5.x
  • 依赖管理:通过 Maven 进行
  • IDE:IntelliJ IDEA 或 Eclipse

重要提示:版本号请根据你的实际环境进行调整。不同版本间可能存在细微差异,本文重点在于演示设计思路和核心代码,遇到依赖冲突时可查阅官方文档。

2.2 初始化 Spring Boot 项目

你可以通过 Spring Initializr 网站或 IDE 内置工具创建项目。选择以下依赖:

  • Spring Web
  • MyBatis Framework
  • MySQL Driver
  • Lombok (用于简化POJO类)

创建完成后,pom.xml 中需要额外添加 MyBatis-Plus 的依赖:

XML
<!-- pom.xml -->
<dependencies>
<!-- Spring Boot 基础依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
 
<!-- 数据库相关 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
 
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

2.3 数据库连接配置

application.ymlapplication.properties 中配置数据库连接。这里以 YAML 格式为例:

YAML
# src/main/resources/application.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/fan_space_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: your_password
 
# MyBatis-Plus 配置
mybatis-plus:
configuration:
# 下划线转驼峰
map-underscore-to-camel-case: true
# 日志实现
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
# 全局逻辑删除字段名
logic-delete-field: is_deleted
# 逻辑已删除值
logic-delete-value: 1
# 逻辑未删除值
logic-not-delete-value: 0

2.4 项目结构预览

一个清晰的项目结构有助于维护。建议按功能模块划分:

TEXT
src/main/java/com/example/fanspace/
├── FanSpaceApplication.java # 启动类
├── config/ # 配置类
├── controller/ # 控制层
│ ├── SpaceStationController.java # 空间站主控制器
│ └── RelationController.java # 关注关系控制器
├── entity/ # 实体类
│ ├── User.java
│ ├── UserRelation.java
│ └── Content.java
├── mapper/ # MyBatis Mapper接口
│ ├── UserMapper.java
│ ├── UserRelationMapper.java
│ └── ContentMapper.java
├── service/ # 服务层
│ ├── ISpaceStationService.java
│ ├── impl/SpaceStationServiceImpl.java
│ ├── IRelationService.java
│ └── impl/RelationServiceImpl.java
└── dto/ # 数据传输对象
├── SpaceStationDTO.java
├── UserSimpleDTO.java
└── ContentDTO.java

3. 数据库设计与核心表结构

数据库设计是业务实现的基石。我们至少需要三张核心表:用户表、用户关系表、内容表。

3.1 用户表 (user)

存储用户的基本信息。

SQL
-- 创建数据库
CREATE DATABASE IF NOT EXISTS `fan_space_db` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
 
USE `fan_space_db`;
 
-- 用户表
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`username` varchar(50) NOT NULL COMMENT '用户名,唯一',
`nickname` varchar(100) DEFAULT NULL COMMENT '用户昵称',
`avatar_url` varchar(500) DEFAULT NULL COMMENT '头像URL',
`introduction` varchar(500) DEFAULT NULL COMMENT '个人简介',
`fans_count` int(11) NOT NULL DEFAULT '0' COMMENT '粉丝数',
`follow_count` int(11) NOT NULL DEFAULT '0' COMMENT '关注数',
`like_count` int(11) NOT NULL DEFAULT '0' COMMENT '获赞总数',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除标志',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`),
KEY `idx_nickname` (`nickname`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';

设计要点

  • fans_count, follow_count 等计数字段冗余存储,避免频繁的关联查询,用空间换时间。
  • 使用 utf8mb4 字符集以支持完整的 Unicode(如 Emoji)。
  • 添加逻辑删除字段 is_deleted,便于数据恢复。

3.2 用户关系表 (user_relation)

记录用户之间的关注关系,这是粉丝系统的核心。

SQL
-- 用户关系表
CREATE TABLE `user_relation` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`from_user_id` bigint(20) NOT NULL COMMENT '关注者ID',
`to_user_id` bigint(20) NOT NULL COMMENT '被关注者ID',
`relation_status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '关系状态:1-已关注,0-已取消',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '关注时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_from_to` (`from_user_id`,`to_user_id`),
KEY `idx_to_user` (`to_user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户关注关系表';

设计要点

  • 唯一索引 uk_from_to 确保同一对用户只能有一条关注记录。
  • relation_status 用于软删除,取消关注时更新状态而非物理删除,便于重建关系和数据分析。
  • 索引 idx_to_user 优化了“查询我的粉丝”这类查询。

3.3 内容表 (content)

存储用户发布的动态内容。

SQL
-- 内容表
CREATE TABLE `content` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` bigint(20) NOT NULL COMMENT '发布者ID',
`content_type` tinyint(4) NOT NULL DEFAULT '1' COMMENT '内容类型:1-文章,2-视频,3-动态',
`title` varchar(255) DEFAULT NULL COMMENT '标题',
`text_content` text COMMENT '文本内容',
`media_url` varchar(500) DEFAULT NULL COMMENT '媒体资源URL',
`like_count` int(11) NOT NULL DEFAULT '0' COMMENT '点赞数',
`comment_count` int(11) NOT NULL DEFAULT '0' COMMENT '评论数',
`view_count` int(11) NOT NULL DEFAULT '0' COMMENT '浏览数',
`is_public` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否公开:1-公开,0-仅粉丝/好友可见',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除标志',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户内容表';

设计要点

  • content_type 支持多种内容形式。
  • is_public 字段为实现隐私控制(如“仅粉丝可见”)打下基础。
  • 联合索引 (user_id, create_time) 对于查询某个用户的最新动态性能极佳,在实际中可根据查询模式考虑添加。

4. 实体类与 Mapper 层实现

使用 MyBatis-Plus 可以极大简化数据库操作。首先创建对应的实体类和 Mapper 接口。

4.1 用户实体与 Mapper

JAVA
// src/main/java/com/example/fanspace/entity/User.java
package com.example.fanspace.entity;
 
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
 
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String nickname;
private String avatarUrl;
private String introduction;
private Integer fansCount;
private Integer followCount;
private Integer likeCount;
 
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
 
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
 
@TableLogic
private Integer isDeleted;
}
JAVA
// src/main/java/com/example/fanspace/mapper/UserMapper.java
package com.example.fanspace.mapper;
 
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.fanspace.entity.User;
import org.apache.ibatis.annotations.Mapper;
 
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 继承 BaseMapper 已包含基础的 CRUD 方法
}

4.2 用户关系实体与 Mapper

JAVA
// src/main/java/com/example/fanspace/entity/UserRelation.java
package com.example.fanspace.entity;
 
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
 
@Data
@TableName("user_relation")
public class UserRelation {
@TableId(type = IdType.AUTO)
private Long id;
private Long fromUserId;
private Long toUserId;
private Integer relationStatus;
 
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
 
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}
JAVA
// src/main/java/com/example/fanspace/mapper/UserRelationMapper.java
package com.example.fanspace.mapper;
 
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.fanspace.entity.UserRelation;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
 
@Mapper
public interface UserRelationMapper extends BaseMapper<UserRelation> {
 
/**
* 查询用户的粉丝ID列表(关注状态为1)
*/
@Select("SELECT from_user_id FROM user_relation WHERE to_user_id = #{userId} AND relation_status = 1")
List<Long> selectFansIds(@Param("userId") Long userId);
 
/**
* 查询用户的关注ID列表(关注状态为1)
*/
@Select("SELECT to_user_id FROM user_relation WHERE from_user_id = #{userId} AND relation_status = 1")
List<Long> selectFollowIds(@Param("userId") Long userId);
 
/**
* 查询两个用户之间的关注关系状态
*/
@Select("SELECT relation_status FROM user_relation WHERE from_user_id = #{fromUserId} AND to_user_id = #{toUserId}")
Integer selectRelationStatus(@Param("fromUserId") Long fromUserId, @Param("toUserId") Long toUserId);
}

4.3 自动填充时间字段

创建一个元数据处理器,自动填充 createTimeupdateTime

JAVA
// src/main/java/com/example/fanspace/config/MyMetaObjectHandler.java
package com.example.fanspace.config;
 
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
 
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
 
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}

5. 核心业务逻辑实现

接下来实现空间站的核心业务:获取空间站主页信息和处理关注关系。

5.1 数据传输对象定义

首先定义用于接口返回的 DTO,它们聚合了多个实体或计算后的数据。

JAVA
// src/main/java/com/example/fanspace/dto/UserSimpleDTO.java
package com.example.fanspace.dto;
 
import lombok.Data;
 
@Data
public class UserSimpleDTO {
private Long id;
private String nickname;
private String avatarUrl;
private String introduction;
// 可以添加更多简化字段
}
JAVA
// src/main/java/com/example/fanspace/dto/ContentDTO.java
package com.example.fanspace.dto;
 
import lombok.Data;
import java.time.LocalDateTime;
 
@Data
public class ContentDTO {
private Long id;
private Integer contentType;
private String title;
private String textContent;
private String mediaUrl;
private Integer likeCount;
private Integer commentCount;
private LocalDateTime createTime;
// 发布者简单信息
private UserSimpleDTO author;
}
JAVA
// src/main/java/com/example/fanspace/dto/SpaceStationDTO.java
package com.example.fanspace.dto;
 
import lombok.Data;
import java.util.List;
 
@Data
public class SpaceStationDTO {
// 空间站主人信息
private UserSimpleDTO hostUser;
// 统计信息
private Long fansCount;
private Long followCount;
private Long contentCount;
// 当前访客与主人的关系:-1-未登录,0-自己,1-已关注,2-未关注,3-互相关注
private Integer relationWithVisitor;
// 最新动态列表(分页)
private List<ContentDTO> recentContents;
}

5.2 空间站服务实现

服务层负责聚合数据和处理业务逻辑。

JAVA
// src/main/java/com/example/fanspace/service/ISpaceStationService.java
package com.example.fanspace.service;
 
import com.example.fanspace.dto.SpaceStationDTO;
 
public interface ISpaceStationService {
/**
* 获取用户的粉丝空间站主页信息
* @param hostUserId 空间站主人ID
* @param visitorUserId 当前访客ID (可为null,表示未登录)
* @param page 页码
* @param size 每页大小
* @return 空间站数据
*/
SpaceStationDTO getSpaceStation(Long hostUserId, Long visitorUserId, Integer page, Integer size);
}
JAVA
// src/main/java/com/example/fanspace/service/impl/SpaceStationServiceImpl.java
package com.example.fanspace.service.impl;
 
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.fanspace.dto.*;
import com.example.fanspace.entity.Content;
import com.example.fanspace.entity.User;
import com.example.fanspace.mapper.ContentMapper;
import com.example.fanspace.mapper.UserMapper;
import com.example.fanspace.mapper.UserRelationMapper;
import com.example.fanspace.service.ISpaceStationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
 
@Slf4j
@Service
@RequiredArgsConstructor
public class SpaceStationServiceImpl implements ISpaceStationService {
 
private final UserMapper userMapper;
private final UserRelationMapper userRelationMapper;
private final ContentMapper contentMapper;
 
@Override
public SpaceStationDTO getSpaceStation(Long hostUserId, Long visitorUserId, Integer page, Integer size) {
SpaceStationDTO dto = new SpaceStationDTO();
 
// 1. 查询空间站主人信息
User hostUser = userMapper.selectById(hostUserId);
if (hostUser == null) {
throw new RuntimeException("用户不存在");
}
UserSimpleDTO hostUserDTO = new UserSimpleDTO();
BeanUtils.copyProperties(hostUser, hostUserDTO);
dto.setHostUser(hostUserDTO);
 
// 2. 设置统计信息 (直接从user表冗余字段获取,性能好)
dto.setFansCount(hostUser.getFansCount().longValue());
dto.setFollowCount(hostUser.getFollowCount().longValue());
 
// 查询内容总数
LambdaQueryWrapper<Content> contentQuery = new LambdaQueryWrapper<>();
contentQuery.eq(Content::getUserId, hostUserId).eq(Content::getIsDeleted, 0);
Long contentCount = contentMapper.selectCount(contentQuery);
dto.setContentCount(contentCount);
 
// 3. 判断当前访客与主人的关系
dto.setRelationWithVisitor(calculateRelation(hostUserId, visitorUserId));
 
// 4. 查询主人的最新动态 (分页)
Page<Content> contentPage = new Page<>(page, size);
LambdaQueryWrapper<Content> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Content::getUserId, hostUserId)
.eq(Content::getIsDeleted, 0)
.orderByDesc(Content::getCreateTime);
Page<Content> pageResult = contentMapper.selectPage(contentPage, queryWrapper);
 
List<Content> contentList = pageResult.getRecords();
List<ContentDTO> contentDTOList = contentList.stream().map(content -> {
ContentDTO contentDTO = new ContentDTO();
BeanUtils.copyProperties(content, contentDTO);
// 这里可以设置作者信息,简单起见直接用hostUserDTO
contentDTO.setAuthor(hostUserDTO);
return contentDTO;
}).collect(Collectors.toList());
dto.setRecentContents(contentDTOList);
 
return dto;
}
 
/**
* 计算关系状态
* -1: 访客未登录
* 0: 访客是自己
* 1: 访客已关注主人
* 2: 访客未关注主人
* 3: 互相关注
*/
private Integer calculateRelation(Long hostUserId, Long visitorUserId) {
if (visitorUserId == null) {
return -1; // 未登录
}
if (hostUserId.equals(visitorUserId)) {
return 0; // 自己访问自己
}
 
// 查询访客是否关注了主人
Integer visitorToHost = userRelationMapper.selectRelationStatus(visitorUserId, hostUserId);
// 查询主人是否关注了访客
Integer hostToVisitor = userRelationMapper.selectRelationStatus(hostUserId, visitorUserId);
 
boolean isVisitorFollowHost = visitorToHost != null && visitorToHost == 1;
boolean isHostFollowVisitor = hostToVisitor != null && hostToVisitor == 1;
 
if (isVisitorFollowHost && isHostFollowVisitor) {
return 3; // 互相关注
} else if (isVisitorFollowHost) {
return 1; // 已关注
} else {
return 2; // 未关注
}
}
}

5.3 关注关系服务实现

关注/取关是粉丝系统的核心交互。

JAVA
// src/main/java/com/example/fanspace/service/IRelationService.java
package com.example.fanspace.service;
 
public interface IRelationService {
/**
* 关注用户
*/
boolean followUser(Long fromUserId, Long toUserId);
 
/**
* 取消关注
*/
boolean unfollowUser(Long fromUserId, Long toUserId);
}
JAVA
// src/main/java/com/example/fanspace/service/impl/RelationServiceImpl.java
package com.example.fanspace.service.impl;
 
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.example.fanspace.entity.User;
import com.example.fanspace.entity.UserRelation;
import com.example.fanspace.mapper.UserMapper;
import com.example.fanspace.mapper.UserRelationMapper;
import com.example.fanspace.service.IRelationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
 
@Slf4j
@Service
@RequiredArgsConstructor
public class RelationServiceImpl implements IRelationService {
 
private final UserRelationMapper userRelationMapper;
private final UserMapper userMapper;
 
@Override
@Transactional(rollbackFor = Exception.class)
public boolean followUser(Long fromUserId, Long toUserId) {
// 1. 不能关注自己
if (fromUserId.equals(toUserId)) {
throw new RuntimeException("不能关注自己");
}
 
// 2. 检查是否已关注
LambdaQueryWrapper<UserRelation> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserRelation::getFromUserId, fromUserId)
.eq(UserRelation::getToUserId, toUserId);
UserRelation existingRelation = userRelationMapper.selectOne(queryWrapper);
 
if (existingRelation != null) {
// 已存在记录,检查状态
if (existingRelation.getRelationStatus() == 1) {
return true; // 已经关注,直接返回成功
} else {
// 之前取消过,现在重新关注
existingRelation.setRelationStatus(1);
existingRelation.setUpdateTime(LocalDateTime.now());
userRelationMapper.updateById(existingRelation);
}
} else {
// 新建关注关系
UserRelation newRelation = new UserRelation();
newRelation.setFromUserId(fromUserId);
newRelation.setToUserId(toUserId);
newRelation.setRelationStatus(1);
userRelationMapper.insert(newRelation);
}
 
// 3. 更新双方的计数 (需要处理并发,这里简单演示,生产环境建议用CAS或分布式锁)
userMapper.updateFansCount(toUserId, 1); // 对方的粉丝数+1
userMapper.updateFollowCount(fromUserId, 1); // 我的关注数+1
 
log.info("用户 {} 关注了用户 {}", fromUserId, toUserId);
return true;
}
 
@Override
@Transactional(rollbackFor = Exception.class)
public boolean unfollowUser(Long fromUserId, Long toUserId) {
// 1. 查询关系记录
LambdaUpdateWrapper<UserRelation> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(UserRelation::getFromUserId, fromUserId)
.eq(UserRelation::getToUserId, toUserId)
.set(UserRelation::getRelationStatus, 0)
.set(UserRelation::getUpdateTime, LocalDateTime.now());
 
int updated = userRelationMapper.update(null, updateWrapper);
 
if (updated > 0) {
// 2. 更新双方的计数
userMapper.updateFansCount(toUserId, -1); // 对方的粉丝数-1
userMapper.updateFollowCount(fromUserId, -1); // 我的关注数-1
log.info("用户 {} 取消关注了用户 {}", fromUserId, toUserId);
return true;
}
return false; // 原本就没有关注关系
}
}

注意:上面的 userMapper.updateFansCountupdateFollowCount 需要自定义 SQL 方法。在 UserMapper.java 中添加:

JAVA
// 在 UserMapper.java 中添加
@Update("UPDATE user SET fans_count = fans_count + #{increment} WHERE id = #{userId}")
int updateFansCount(@Param("userId") Long userId, @Param("increment") int increment);
 
@Update("UPDATE user SET follow_count = follow_count + #{increment} WHERE id = #{userId}")
int updateFollowCount(@Param("userId") Long userId, @Param("increment") int increment);

6. 控制器层与 API 设计

将服务暴露为 RESTful API。

6.1 空间站主页 API

JAVA
// src/main/java/com/example/fanspace/controller/SpaceStationController.java
package com.example.fanspace.controller;
 
import com.example.fanspace.dto.SpaceStationDTO;
import com.example.fanspace.service.ISpaceStationService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
 
@RestController
@RequestMapping("/api/space")
@RequiredArgsConstructor
public class SpaceStationController {
 
private final ISpaceStationService spaceStationService;
 
/**
* 获取用户空间站主页
* GET /api/space/{userId}?page=1&size=10
* 注意:实际项目中 visitorUserId 应从登录态(如JWT Token)中获取,这里简化通过参数传递。
*/
@GetMapping("/{userId}")
public SpaceStationDTO getSpaceStation(@PathVariable Long userId,
@RequestParam(required = false) Long visitorId,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
// visitorId 可为空,表示未登录用户访问
return spaceStationService.getSpaceStation(userId, visitorId, page, size);
}
}

6.2 关注/取关 API

JAVA
// src/main/java/com/example/fanspace/controller/RelationController.java
package com.example.fanspace.controller;
 
import com.example.fanspace.service.IRelationService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
 
import java.util.HashMap;
import java.util.Map;
 
@RestController
@RequestMapping("/api/relation")
@RequiredArgsConstructor
public class RelationController {
 
private final IRelationService relationService;
 
/**
* 关注用户
* POST /api/relation/follow
* Body: { "fromUserId": 1, "toUserId": 2 }
*/
@PostMapping("/follow")
public Map<String, Object> follow(@RequestBody Map<String, Long> request) {
Long fromUserId = request.get("fromUserId");
Long toUserId = request.get("toUserId");
boolean success = relationService.followUser(fromUserId, toUserId);
 
Map<String, Object> result = new HashMap<>();
result.put("success", success);
result.put("message", success ? "关注成功" : "关注失败");
return result;
}
 
/**
* 取消关注
* POST /api/relation/unfollow
* Body: { "fromUserId": 1, "toUserId": 2 }
*/
@PostMapping("/unfollow")
public Map<String, Object> unfollow(@RequestBody Map<String, Long> request) {
Long fromUserId = request.get("fromUserId");
Long toUserId = request.get("toUserId");
boolean success = relationService.unfollowUser(fromUserId, toUserId);
 
Map<String, Object> result = new HashMap<>();
result.put("success", success);
result.put("message", success ? "取消关注成功" : "取消关注失败或未关注");
return result;
}
}

7. 功能测试与 API 调用示例

启动 Spring Boot 应用后,我们可以使用 Postman 或 curl 进行测试。

7.1 准备测试数据

首先,向数据库插入一些测试用户和内容。

SQL
-- 插入测试用户
INSERT INTO `user` (username, nickname, avatar_url, introduction) VALUES
('user1', '技术爱好者小明', 'https://example.com/avatar1.jpg', '热爱编程,分享技术'),
('user2', '设计达人小红', 'https://example.com/avatar2.jpg', '专注UI/UX设计');
 
-- 插入测试内容 (假设 user1 的 id 是 1)
INSERT INTO `content` (user_id, content_type, title, text_content) VALUES
(1, 1, 'Spring Boot入门指南', '这是一篇关于Spring Boot的入门文章...'),
(1, 3, '今天天气真好', '分享一张随手拍的照片。');

7.2 测试获取空间站信息

请求

TEXT
GET http://localhost:8080/api/space/1?visitorId=2&page=1&size=5

预期响应

JSON
{
"hostUser": {
"id": 1,
"nickname": "技术爱好者小明",
"avatarUrl": "https://example.com/avatar1.jpg",
"introduction": "热爱编程,分享技术"
},
"fansCount": 0,
"followCount": 0,
"contentCount": 2,
"relationWithVisitor": 2, // visitorId=2 未关注 hostId=1
"recentContents": [
{
"id": 2,
"contentType": 3,
"title": "今天天气真好",
"textContent": "分享一张随手拍的照片。",
"likeCount": 0,
"commentCount": 0,
"createTime": "2023-10-27T10:30:00",
"author": {
"id": 1,
"nickname": "技术爱好者小明",
"avatarUrl": "https://example.com/avatar1.jpg",
"introduction": "热爱编程,分享技术"
}
},
{
"id": 1,
"contentType": 1,
"title": "Spring Boot入门指南",
"textContent": "这是一篇关于Spring Boot的入门文章...",
"likeCount": 0,
"commentCount": 0,
"createTime": "2023-10-26T15:20:00",
"author": { ... }
}
]
}

7.3 测试关注功能

请求

TEXT
POST http://localhost:8080/api/relation/follow
Content-Type: application/json
 
{
"fromUserId": 2,
"toUserId": 1
}

预期响应

JSON
{
"success": true,
"message": "关注成功"
}

再次调用空间站 API (visitorId=2),relationWithVisitor 字段会变为 1(已关注),并且 hostUserfansCount 会+1,用户2的 followCount 也会+1。

8. 常见问题与排查思路

在实际开发和部署中,你可能会遇到以下问题:

问题现象 可能原因 排查思路与解决方案
启动报错:Failed to configure a DataSource 数据库连接配置错误或驱动未加载。 1. 检查 application.yml 中的 url, username, password
2. 确认 MySQL 服务已启动。
3. 检查 pom.xml 中 MySQL 驱动依赖版本是否兼容。
调用关注 API 后,计数没有更新。 1. 事务未生效。
2. 更新语句执行失败。
3. 并发更新导致计数不准。
1. 确保 Service 方法上有 @Transactional 注解。
2. 查看 MyBatis-Plus 日志,确认 UPDATE 语句已执行。
3. 生产环境必须优化:使用 UPDATE user SET fans_count = fans_count + 1 WHERE id = ? 这类原子操作,或使用 Redis 缓存计数,定期同步到 DB。
查询空间站信息非常慢,尤其是粉丝/关注列表很长时。 1. 未对 user_id, create_time 等字段建立索引。
2. 分页查询深度过大。
3. 多次循环查询数据库(N+1问题)。
1. 为 user_relation 表的 from_user_idto_user_id 建立索引。
2. 为 content 表的 user_idcreate_time 建立联合索引。
3. 对于粉丝列表,考虑使用游标分页或时间戳分页代替 LIMIT offset, size
4. 使用 MyBatis-Plus 的 selectPage 进行物理分页。
重复关注导致唯一约束冲突 (uk_from_to)。 代码逻辑未处理“已关注”的状态,重复插入。 followUser 方法中,先查询是否存在有效记录 (relation_status=1),存在则直接返回;存在但状态为0则更新;不存在才插入。本文代码已实现此逻辑。
隐私内容被未授权用户看到。 查询内容时未过滤 is_public 字段或未做权限校验。 在查询动态列表时,根据访客关系动态添加查询条件。例如,如果访客不是粉丝且不是本人,则只查询 is_public = 1 的内容。在 SpaceStationServiceImpl 的查询条件中增加逻辑判断。

9. 性能优化与最佳实践

当用户量增长后,最初的实现可能会遇到性能瓶颈。以下是一些优化方向:

9.1 计数器的优化

直接更新数据库的 fans_count 字段在高并发下会成为热点。推荐方案:

  1. 使用 Redis 缓存计数:在 Redis 中使用 HINCRBY 命令进行原子增减。
    JAVA
    // 示例:关注时
    redisTemplate.opsForHash().increment("user:stats:" + toUserId, "fansCount", 1);
    redisTemplate.opsForHash().increment("user:stats:" + fromUserId, "followCount", 1);
  2. 异步持久化:通过定时任务或消息队列,将 Redis 中的计数批量同步到数据库。
  3. 数据库优化:如果坚持用数据库,确保更新语句是原子的 (SET count = count + 1),并对 id 主键更新。

9.2 粉丝/关注列表查询优化

当列表数据量很大时:

  1. 使用游标分页:避免使用 LIMIT offset, size 在深度分页时的性能问题。改用 WHERE id > last_id LIMIT size
  2. 缓存热门用户的关系列表:对于粉丝数巨大的用户(如大V),其粉丝列表可以缓存在 Redis 中,并设置合理的过期时间。
  3. 异步加载与前端优化:前端采用无限滚动,后端接口支持按需加载,首次只返回前 N 条。

9.3 动态流(Feed)架构考虑

空间站展示的是“我的动态”。更复杂的场景是“我关注的人的动态”(即 Feed 流)。此时架构需要升级:

  • 推模式 (Write Fan-out):用户发布动态时,系统将该动态写入其所有粉丝的收件箱(如一个 Timeline 表或 Redis Sorted Set)。读请求直接读取收件箱。适合粉丝数有限的场景。
  • 拉模式 (Read Fan-out):用户查看动态时,实时去查询所有关注的人的最新动态,然后聚合排序。适合关系链较长的场景。
  • 混合模式:对活跃用户和大V采用推模式,对普通用户采用拉模式,或者对近期动态推,历史动态拉。

9.4 安全性建议

  1. 接口权限校验:本文示例为简化,通过参数传递用户ID。生产环境必须从安全的上下文中获取(如 Spring Security 的 SecurityContextHolder 或 JWT Token 解析),防止用户冒充他人进行操作。
  2. 防刷机制:关注/取关接口需要添加频率限制,防止恶意刷接口。
  3. 数据脱敏:返回用户信息时,注意过滤手机号、邮箱等敏感信息。
  4. SQL 注入防护:坚持使用 MyBatis-Plus 的 LambdaQueryWrapper@Param 注解的参数绑定方式,切勿手动拼接 SQL 字符串。

10. 扩展功能与后续迭代

一个基础的粉丝空间站已经搭建完成。你可以在此基础上继续丰富功能:

  1. 消息通知:当用户被关注、被点赞、被评论时,通过 WebSocket 或消息队列发送实时通知。
  2. 隐私设置:允许用户设置动态可见范围(公开、仅粉丝、仅自己、指定好友列表)。
  3. 黑名单/屏蔽功能:允许用户屏蔽他人,被屏蔽的用户无法查看空间站或进行互动。
  4. 数据看板:为空间站主人提供数据概览,如粉丝增长趋势、动态阅读量分析等。
  5. 勋章与等级系统:根据用户的活跃度、粉丝数、内容质量等计算成长值,授予相应勋章和等级,并在空间站展示。

通过本文的实践,你已经掌握了构建一个粉丝空间站后端服务的核心流程。从数据库设计、业务逻辑实现到 API 暴露,每一步都力求清晰和可操作。在实际项目中,你需要根据业务复杂度,在性能、安全性和可扩展性上做出更多权衡与设计。建议先从核心功能 MVP 开始,快速上线验证,再根据用户反馈和数据表现,逐步迭代优化。