Implementing Video Social Features and Instant Messaging Integration

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

Tags: java Spring Boot Instant Messaging Video Service Easemob

Posted on Sat, 09 May 2026 11:41:20 +0000 by al3x8730