1. Video Interaction Features
The logic for video interactions such as likes, comments, and follows mirrors the implementation used for the social circle features. However, specific adjustments are required to handle Video entities, particularly when retrieving the publisher's ID.
1.1 Backend Service Implementation
The interaction service needs to determine the target object type (Video, Post, or Comment) to correctly set the publisher information.
// VideoInteractionService.java
public Boolean recordInteraction(Long userId, String targetId, InteractionType type, String content) {
try {
InteractionRecord record = new InteractionRecord();
record.setId(ObjectId.get());
record.setUserId(userId);
record.setTargetId(new ObjectId(targetId));
record.setInteractionType(type.getCode());
record.setContent(content);
record.setTimestamp(System.currentTimeMillis());
// Attempt to find the publisher
Publish post = this.postRepository.findById(targetId);
if (post != null) {
record.setPublisherId(post.getUserId());
} else {
// Check if it's a comment
InteractionRecord parentComment = this.commentRepository.findById(targetId);
if (parentComment != null) {
record.setPublisherId(parentComment.getUserId());
} else {
// Fallback to checking Video entity
Video video = this.videoRepository.findById(targetId);
if (video != null) {
record.setPublisherId(video.getUserId());
} else {
return false;
}
}
}
this.mongoTemplate.save(record);
return true;
} catch (Exception e) {
log.error("Error recording interaction for user {} on target {}", userId, targetId, e);
return false;
}
}
The VideoRepository interface defines the lookup method:
public interface VideoRepository {
Video findById(String videoId);
}
// Implementation
@Override
public Video findById(String videoId) {
return mongoTemplate.findById(new ObjectId(videoId), Video.class);
}
1.2 Controller Endpoints
API endpoints handle the user requests for liking and disliking videos.
// VideoController.java
@PostMapping("/{id}/like")
public ResponseEntity<Long> likeVideo(@PathVariable("id") String videoId) {
try {
Long count = videoService.likeVideo(videoId);
return ResponseEntity.ok(count);
} catch (Exception e) {
log.error("Like video failed", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@PostMapping("/{id}/dislike")
public ResponseEntity<Long> dislikeVideo(@PathVariable("id") String videoId) {
try {
Long count = videoService.dislikeVideo(videoId);
return ResponseEntity.ok(count);
} catch (Exception e) {
log.error("Dislike video failed", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
1.3 Service Layer
The service layer delegates the logic to the interaction API and retrieves updated counts.
// VideoService.java
public Long likeVideo(String videoId) {
User currentUser = UserContext.getCurrentUser();
boolean success = interactionApi.like(currentUser.getId(), videoId);
return success ? interactionApi.getLikeCount(videoId) : null;
}
public Long dislikeVideo(String videoId) {
User currentUser = UserContext.getCurrentUser();
boolean success = interactionApi.dislike(currentUser.getId(), videoId);
return success ? interactionApi.getLikeCount(videoId) : null;
}
1.4 Fetching Video List with Interactions
When retrieving the video list, the system now populates interaction status (liked, followed) and counts.
// VideoService.java
public PageResult getVideoFeed(int page, int size) {
User currentUser = UserContext.getCurrentUser();
PageResult result = new PageResult();
PageInfo<Video> pageInfo = videoApi.getVideoFeed(currentUser.getId(), page, size);
List<Video> videos = pageInfo.getRecords();
if (CollUtil.isEmpty(videos)) {
return result;
}
// Fetch user info
List<Long> userIds = videos.stream().map(Video::getUserId).collect(Collectors.toList());
Map<Long, UserInfo> userMap = userInfoService.findByIds(userIds);
List<VideoVo> voList = new ArrayList<>();
for (Video video : videos) {
VideoVo vo = new VideoVo();
vo.setId(video.getId().toHexString());
vo.setVideoUrl(video.getVideoUrl());
vo.setCover(video.getCoverUrl());
// Populate interaction data
vo.setLikeCount(Convert.toInt(interactionApi.getLikeCount(vo.getId())));
vo.setHasLiked(interactionApi.hasUserLiked(currentUser.getId(), vo.getId()) ? 1 : 0);
vo.setCommentCount(Convert.toInt(interactionApi.getCommentCount(vo.getId())));
vo.setHasFollowed(videoApi.isFollowing(currentUser.getId(), video.getUserId()) ? 1 : 0);
// Populate user info
UserInfo userInfo = userMap.get(video.getUserId());
if (userInfo != null) {
vo.setNickname(userInfo.getNickname());
vo.setAvatar(userInfo.getAvatar());
}
voList.add(vo);
}
result.setItems(voList);
return result;
}
2. User Follow Functionality
Users can follow video authors. This requires a MongoDB entity to store the relationship and Redis for fast lookups.
2.1 Entity Definition
@Data
@Document(collection = "video_follow")
public class VideoFollow implements Serializable {
private ObjectId id;
private Long userId; // Follower
private Long followUserId; // Target
private Long created;
}
2.2 Follow Service Implementation
The service manages the relationship in MongoDB and caches the status in Redis.
// VideoApiImpl.java
private static final String FOLLOW_KEY_PREFIX = "VIDEO_FOLLOW:";
@Override
public Boolean followUser(Long userId, Long targetUserId) {
if (userId == null || targetUserId == null) return false;
// Check Redis first
if (isFollowing(userId, targetUserId)) return false;
try {
VideoFollow follow = new VideoFollow();
follow.setId(ObjectId.get());
follow.setUserId(userId);
follow.setFollowUserId(targetUserId);
follow.setCreated(System.currentTimeMillis());
mongoTemplate.save(follow);
// Update Redis Cache
String key = FOLLOW_KEY_PREFIX + userId;
redisTemplate.opsForHash().put(key, String.valueOf(targetUserId), "1");
return true;
} catch (Exception e) {
log.error("Follow user failed", e);
return false;
}
}
@Override
public Boolean unfollowUser(Long userId, Long targetUserId) {
// Remove from Mongo
Query query = Query.query(Criteria.where("userId").is(userId)
.and("followUserId").is(targetUserId));
DeleteResult result = mongoTemplate.remove(query, VideoFollow.class);
if (result.getDeletedCount() > 0) {
// Remove from Redis
String key = FOLLOW_KEY_PREFIX + userId;
redisTemplate.opsForHash().delete(key, String.valueOf(targetUserId));
return true;
}
return false;
}
@Override
public Boolean isFollowing(Long userId, Long targetUserId) {
String key = FOLLOW_KEY_PREFIX + userId;
return redisTemplate.opsForHash().hasKey(key, String.valueOf(targetUserId));
}
3. Instant Messaging Architecture
For instant messaging, the project integrates with a third-party service provider (Easemob/Huanxin) to handle high-concurrency message delivery, connection stability, and protocol management. This approach significantly reduces development time compared to building a proprietary WebSocket solution.
4. Easemob Integration
4.1 Configuration
Configuration properties are defined for the Easemob API connection.
# easemob.properties
easemob.api.url=https://a1.easemob.com/
easemob.org.name=your_org
easemob.app.name=your_app
easemob.client.id=client_id
easemob.client.secret=client_secret
4.2 Token Management
Access tokens are cached in Redis to avoid repeated authentication requests.
// TokenService.java
public String getAccessToken() {
String token = redisTemplate.opsForValue().get("EASEMOB_TOKEN");
if (StrUtil.isNotEmpty(token)) {
return token;
}
return refreshToken();
}
public String refreshToken() {
String url = config.getUrl() + config.getOrgName() + "/" + config.getAppName() + "/token";
Map<String, Object> body = new HashMap<>();
body.put("grant_type", "client_credentials");
body.put("client_id", config.getClientId());
body.put("client_secret", config.getClientSecret());
HttpResponse response = HttpUtil.createPost(url)
.body(JSONUtil.toJsonStr(body))
.execute();
JSONObject json = JSONUtil.parseObj(response.body());
String newToken = json.getStr("access_token");
long expiresIn = json.getLong("expires_in");
// Cache for 1 hour less than actual expiry
redisTemplate.opsForValue().set("EASEMOB_TOKEN", newToken, expiresIn - 3600, TimeUnit.SECONDS);
return newToken;
}
4.3 Request Retry Mechanism
To handle token expiration (401 errors) gracefully, the system uses Spring Retry to automatically refresh the token and retry the request.
// RequestExecutor.java
@Service
@EnableRetry
public class RequestExecutor {
@Autowired
private TokenService tokenService;
@Retryable(value = UnauthorizedException.class, maxAttempts = 3, backoff = @Backoff(delay = 1000))
public String executeRequest(String url, String body, HttpMethod method) {
String token = tokenService.getAccessToken();
HttpRequest request = HttpUtil.createRequest(method, url)
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json");
if (body != null) request.body(body);
HttpResponse response = request.execute();
if (response.getStatus() == 401) {
tokenService.refreshToken();
throw new UnauthorizedException("Token expired");
}
return response.body();
}
}
5. User System Synchronization
When a user registers on the main platform, they must also be registered with the IM service.
5.1 Entity Mapping
@Data
@TableName("tb_im_user")
public class ImUser {
private Long id;
private Long userId; // System User ID
private String username; // IM Username
private String password; // IM Password
private Date created;
}
5.2 Registration Logic
// ImUserService.java
public Boolean registerImUser(Long systemUserId) {
ImUser imUser = new ImUser();
imUser.setUserId(systemUserId);
imUser.setUsername("hx_" + systemUserId); // Prefix for IM username
imUser.setPassword(UUID.randomUUID().toString()); // Random password
imUser.setCreated(new Date());
// Call Easemob API
String url = config.getUrl() + config.getOrgName() + "/" + config.getAppName() + "/users";
String body = JSONUtil.toJsonStr(Collections.singletonList(imUser));
String response = requestExecutor.executeRequest(url, body, HttpMethod.POST);
if (response != null) {
// Save to local database
imUserMapper.insert(imUser);
return true;
}
return false;
}
6. Contact Management
Adding a contact involves storing the relationship in the local database and notifying the IM platform.
6.1 Backend Logic
// ContactService.java
public Boolean addContact(Long currentUserId, Long targetUserId) {
// 1. Save locally
UserContact contact = new UserContact();
contact.setUserId(currentUserId);
contact.setFriendId(targetUserId);
contact.setCreated(System.currentTimeMillis());
contactRepository.save(contact);
// 2. Add to Easemob
String url = config.getUrl() + config.getOrgName() + "/" + config.getAppName() +
"/users/hx_" + currentUserId + "/contacts/users/hx_" + targetUserId;
String response = requestExecutor.executeRequest(url, null, HttpMethod.POST);
return response != null;
}
6.2 Fetching Contact List
The contact list is retrieved from the local database, enriched with user profile information.
// ContactService.java
public PageResult getContacts(int page, int size, String keyword) {
User currentUser = UserContext.getCurrentUser();
List<UserContact> contacts = contactRepository.findByUserId(currentUser.getId());
if (CollUtil.isEmpty(contacts)) return new PageResult();
List<Long> friendIds = contacts.stream().map(UserContact::getFriendId).collect(Collectors.toList());
QueryWrapper<UserInfo> wrapper = new QueryWrapper<>();
wrapper.in("user_id", friendIds);
if (StrUtil.isNotEmpty(keyword)) {
wrapper.like("nickname", keyword);
}
List<UserInfo> profiles = userInfoMapper.selectList(wrapper);
List<ContactVo> voList = profiles.stream().map(p -> {
ContactVo vo = new ContactVo();
vo.setId(p.getUserId());
vo.setNickname(p.getNickname());
vo.setAvatar(p.getAvatar());
vo.setUserId("hx_" + p.getUserId());
return vo;
}).collect(Collectors.toList());
return new PageResult(voList);
}