Overview
This tutorial covers implementing the following features:
- Likes, loves, and comments notification lists
- System announcements
- User profile page
- Chat with strangers functionality
- Who viewed my profile feature
- Notification Lists (Likes, Loves, Comments)
The notification center displays interactions from other users on your content. Since these three types share identical logic, we implement them together.
API endpoints:
- Likes: https://mock-java.itheima.net/project/35/interface/api/779
- Comments: https://mock-java.itheima.net/project/35/interface/api/785
- Loves: https://mock-java.itheima.net/project/35/interface/api/791
1.1 Dubbo Service Layer
1.1.1 Interface Definition
//com.socialapp.dubbo.server.api.SocialContentApi
/**
* Retrieve likes received on user's content
*/
PageInfo<Interaction> fetchReceivedLikes(Long userId, Integer page, Integer pageSize);
/**
* Retrieve loves received on user's content
*/
PageInfo<Interaction> fetchReceivedLoves(Long userId, Integer page, Integer pageSize);
/**
* Retrieve comments received on user's content
*/
PageInfo<Interaction> fetchReceivedComments(Long userId, Integer page, Integer pageSize);
1.1.2 Implementation
//com.socialapp.dubbo.server.api.SocialContentApiImpl
@Override
public PageInfo<Interaction> fetchReceivedLikes(Long userId, Integer page, Integer pageSize) {
return this.fetchInteractionsByType(userId, InteractionType.LIKE, page, pageSize);
}
@Override
public PageInfo<Interaction> fetchReceivedLoves(Long userId, Integer page, Integer pageSize) {
return this.fetchInteractionsByType(userId, InteractionType.LOVE, page, pageSize);
}
@Override
public PageInfo<Interaction> fetchReceivedComments(Long userId, Integer page, Integer pageSize) {
return this.fetchInteractionsByType(userId, InteractionType.COMMENT, page, pageSize);
}
private PageInfo<Interaction> fetchInteractionsByType(Long userId, InteractionType type, Integer page, Integer pageSize) {
PageRequest request = PageRequest.of(page - 1, pageSize,
Sort.by(Sort.Order.desc("created")));
Query query = new Query(Criteria
.where("contentOwnerId").is(userId)
.and("interactionType").is(type.getCode())).with(request);
List<Interaction> interactions = this.mongoTemplate.find(query, Interaction.class);
PageInfo<Interaction> result = new PageInfo<>();
result.setPageNum(page);
result.setPageSize(pageSize);
result.setRecords(interactions);
return result;
}
1.2 Application Service Layer
1.2.1 Notification Response Object
package com.socialapp.server.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class InteractionNotificationDto {
private String id;
private String avatar;
private String nickname;
private String timestamp; //format: "2019-09-08 10:07"
}
1.2.2 NotificationController
//com.socialapp.server.controller.NotificationController
/**
* Get likes notification list
*/
@GetMapping("likes")
public ResponseEntity<PaginatedResult> getLikesNotifications(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "pagesize", defaultValue = "10") Integer pageSize) {
try {
PaginatedResult result = this.notificationService.getLikesNotifications(page, pageSize);
return ResponseEntity.ok(result);
} catch (Exception e) {
log.error("Failed to retrieve likes notifications", e);
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
/**
* Get comments notification list
*/
@GetMapping("comments")
public ResponseEntity<PaginatedResult> getCommentsNotifications(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "pagesize", defaultValue = "10") Integer pageSize) {
try {
PaginatedResult result = this.notificationService.getCommentsNotifications(page, pageSize);
return ResponseEntity.ok(result);
} catch (Exception e) {
log.error("Failed to retrieve comments notifications", e);
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
/**
* Get loves notification list
*/
@GetMapping("loves")
public ResponseEntity<PaginatedResult> getLovesNotifications(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "pagesize", defaultValue = "10") Integer pageSize) {
try {
PaginatedResult result = this.notificationService.getLovesNotifications(page, pageSize);
return ResponseEntity.ok(result);
} catch (Exception e) {
log.error("Failed to retrieve loves notifications", e);
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
1.2.3 NotificationService
//com.socialapp.server.service.NotificationService
public PaginatedResult getLikesNotifications(Integer page, Integer pageSize) {
AuthenticatedUser currentUser = AuthenticatedUserContext.get();
PageInfo<Interaction> pageData = this.socialContentApi.fetchReceivedLikes(
currentUser.getId(), page, pageSize);
return this.assembleNotificationResponse(pageData);
}
public PaginatedResult getLovesNotifications(Integer page, Integer pageSize) {
AuthenticatedUser currentUser = AuthenticatedUserContext.get();
PageInfo<Interaction> pageData = this.socialContentApi.fetchReceivedLoves(
currentUser.getId(), page, pageSize);
return this.assembleNotificationResponse(pageData);
}
public PaginatedResult getCommentsNotifications(Integer page, Integer pageSize) {
AuthenticatedUser currentUser = AuthenticatedUserContext.get();
PageInfo<Interaction> pageData = this.socialContentApi.fetchReceivedComments(
currentUser.getId(), page, pageSize);
return this.assembleNotificationResponse(pageData);
}
private PaginatedResult assembleNotificationResponse(PageInfo<Interaction> pageData) {
PaginatedResult result = new PaginatedResult();
result.setPage(pageData.getPageNum());
result.setPagesize(pageData.getPageSize());
List<Interaction> items = pageData.getRecords();
if (CollectionUtils.isEmpty(items)) {
return result;
}
List<Object> userIdCollection = items.stream()
.map(Interaction::getActorId)
.collect(Collectors.toList());
List<UserProfile> profiles = this.userProfileService.fetchProfilesByIds(userIdCollection);
List<InteractionNotificationDto> notifications = new ArrayList<>();
for (Interaction interaction : items) {
UserProfile profile = profiles.stream()
.filter(p -> p.getUserId().equals(interaction.getActorId()))
.findFirst()
.orElse(null);
if (profile != null) {
InteractionNotificationDto dto = new InteractionNotificationDto();
dto.setId(interaction.getId().toHexString());
dto.setAvatar(profile.getAvatarUrl());
dto.setNickname(profile.getDisplayName());
dto.setTimestamp(DateTimeFormatter.format(
new Date(interaction.getCreated()), "yyyy-MM-dd HH:mm"));
notifications.add(dto);
}
}
result.setItems(notifications);
return result;
}
1.3 Testing2. System Announcements
Announcements are official notifications broadcast by administrators to all users.
API endpoint: https://mock-java.itheima.net/project/35/interface/api/797
2.1 Database Schema
CREATE TABLE `system_announcements` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`title` varchar(200) DEFAULT NULL,
`content` text,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `created_at` (`created_at`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- Sample data
INSERT INTO `system_announcements` (`id`, `title`, `content`, `created_at`, `updated_at`)
VALUES ('1', 'New Version Released', 'Summer party event now available', '2019-10-14 11:06:34', '2019-10-14 11:06:37');
INSERT INTO `system_announcements` (`id`, `title`, `content`, `created_at`, `updated_at`)
VALUES ('2', 'New Circles Feature', 'Groups feature is now live', '2019-10-14 11:09:31', '2019-10-14 11:09:33');
INSERT INTO `system_announcements` (`id`, `title`, `content`, `created_at`, `updated_at`)
VALUES ('3', 'Holiday Notice', 'Service remains operational during holidays', '2019-10-14 11:10:01', '2019-10-14 11:10:04');
2.2 Entity Model
package com.socialapp.common.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Announcement extends BaseEntity {
private Long id;
private String title;
private String content;
}
2.3 AnnouncementMapper
package com.socialapp.common.repository;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.socialapp.common.entity.Announcement;
public interface AnnouncementMapper extends BaseMapper<Announcement> {
}
2.4 AnnouncementService
package com.socialapp.server.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryCondition;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.socialapp.common.repository.AnnouncementMapper;
import com.socialapp.common.entity.Announcement;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AnnouncementService {
@Autowired
private AnnouncementMapper announcementMapper;
public IPage<Announcement> fetchAnnouncements(Integer page, Integer pageSize) {
QueryCondition condition = new QueryCondition();
condition.orderByDesc("createdAt");
return this.announcementMapper.selectPage(
new Page<Announcement>(page, pageSize), condition);
}
}
2.5 Response DTO
package com.socialapp.server.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AnnouncementDto {
private String id;
private String title;
private String content;
private String postedAt;
}
2.6 NotificationController
/**
* Get system announcements
*/
@GetMapping("announcements")
@PublicEndpoint //No authentication required
public ResponseEntity<PaginatedResult> getAnnouncements(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "pagesize", defaultValue = "10") Integer pageSize) {
try {
PaginatedResult result = this.notificationService.getAnnouncements(page, pageSize);
return ResponseEntity.ok(result);
} catch (Exception e) {
log.error("Failed to fetch announcements", e);
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
2.7 NotificationService
public PaginatedResult getAnnouncements(Integer page, Integer pageSize) {
IPage<Announcement> announcementPage =
this.announcementService.fetchAnnouncements(page, pageSize);
List<AnnouncementDto> announcementList = new ArrayList<>();
for (Announcement record : announcementPage.getRecords()) {
AnnouncementDto dto = new AnnouncementDto();
dto.setId(record.getId().toString());
dto.setTitle(record.getTitle());
dto.setContent(record.getContent());
dto.setPostedAt(DateTimeFormatter.format(
record.getCreatedAt(), "yyyy-MM-dd HH:mm"));
announcementList.add(dto);
}
PaginatedResult result = new PaginatedResult();
result.setPage(page);
result.setPagesize(pageSize);
result.setItems(announcementList);
return result;
}
2.8 Testing3. User Profile Page
Clicking on featured users or recommendations navigates to their profile page, displaying personal information, compatibility scores, and photo albums.
3.1 User Information
3.1.1 Dubbo Service Layer
The compatibility score is calculated based on recommendation algorithms.
3.1.1.1 Interface Definition
//com.socialapp.dubbo.server.api.RecommendationApi
/**
* Calculate compatibility score between two users
*/
Double calculateCompatibility(Long userId, Long targetUserId);
3.1.1.2 Implementation
//com.socialapp.dubbo.server.api.RecommendationApiImpl
@Override
public Double calculateCompatibility(Long userId, Long targetUserId) {
Query query = Query.query(Criteria.where("targetUserId").is(targetUserId)
.and("userId").is(userId));
CompatibilityRecord record = this.mongoTemplate.findOne(query, CompatibilityRecord.class);
if (record != null) {
return record.getScore();
}
return null;
}
3.1.2 Application Service Layer
API endpoint: https://mock-java.itheima.net/project/35/interface/api/629
3.1.2.1 UserProfileController
package com.socialapp.server.controller;
import com.socialapp.server.service.UserProfileService;
import com.socialapp.server.vo.UserProfileSummary;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("userprofile")
public class UserProfileController {
@Autowired
private UserProfileService userProfileService;
/**
* Retrieve user profile information
*/
@GetMapping("{userId}/details")
public ResponseEntity<UserProfileSummary> getUserProfile(
@PathVariable("userId") Long userId) {
try {
UserProfileSummary profile = this.userProfileService.getUserProfile(userId);
return ResponseEntity.ok(profile);
} catch (Exception e) {
log.error("Error fetching user profile", e);
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
3.1.2.2 UserProfileService
package com.socialapp.server.service;
import cn.hutool.core.util.StrUtil;
import com.socialapp.common.entity.User;
import com.socialapp.common.entity.UserProfile;
import com.socialapp.common.utils.UserContext;
import com.socialapp.server.vo.UserProfileSummary;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserProfileService {
@Autowired
private UserProfileService userProfileService;
@Autowired
private CompatibilityService compatibilityService;
public UserProfileSummary getUserProfile(Long userId) {
UserProfile profileData = this.userProfileService.fetchProfileByUserId(userId);
if (profileData == null) {
return null;
}
UserProfileSummary summary = new UserProfileSummary();
summary.setUserId(userId);
summary.setAge(profileData.getAge());
summary.setGender(profileData.getGender().name().toLowerCase());
summary.setNickname(profileData.getNickName());
summary.setTags(StrUtil.split(profileData.getTags(), ','));
summary.setAvatar(profileData.getAvatarUrl());
//Compatibility score
User currentUser = UserContext.get();
summary.setCompatibilityScore(
this.compatibilityService.getScore(userId, currentUser.getId()).longValue());
return summary;
}
}
3.1.2.3 CompatibilityService
//com.socialapp.server.service.CompatibilityService
public Double getScore(Long userId, Long targetUserId) {
Double score = this.recommendationApi.calculateCompatibility(userId, targetUserId);
if (score != null) {
return score;
}
return 98.0; //Default score
}
3.1.3 Testing
3.2 Photo Albums
3.2.1 Dubbo Service Layer
3.2.1.1 Interface Definition
//com.socialapp.dubbo.server.api.SocialContentApi
/**
* Retrieve user's photo album
*/
PageInfo<Post> fetchUserAlbum(Long userId, Integer page, Integer pageSize);
3.2.1.2 Implementation
//com.socialapp.dubbo.server.api.SocialContentApiImpl
@Override
public PageInfo<Post> fetchUserAlbum(Long userId, Integer page, Integer pageSize) {
PageInfo<Post> result = new PageInfo<>();
result.setPageNum(page);
result.setPageSize(pageSize);
PageRequest request = PageRequest.of(page - 1, pageSize,
Sort.by(Sort.Order.desc("created")));
Query query = new Query().with(request);
List<AlbumEntry> albumEntries =
this.mongoTemplate.find(query, AlbumEntry.class, "album_" + userId);
if (CollectionUtils.isEmpty(albumEntries)) {
return result;
}
List<Object> postIds = albumEntries.stream()
.map(AlbumEntry::getPostId)
.collect(Collectors.toList());
Query postQuery = Query.query(Criteria.where("id").in(postIds))
.with(Sort.by(Sort.Order.desc("created")));
List<Post> posts = this.mongoTemplate.find(postQuery, Post.class);
result.setRecords(posts);
return result;
}
3.2.2 Applicaiton Service Layer
API endpoint: https://mock-java.itheima.net/project/35/interface/api/689
3.2.2.1 SocialFeedController
//com.socialapp.server.controller.SocialFeedController
/**
* Get user's complete activity feed
*/
@GetMapping("all")
public ResponseEntity<PaginatedResult> getUserAlbum(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "pagesize", defaultValue = "10") Integer pageSize,
@RequestParam(value = "userId") Long userId) {
try {
PaginatedResult result = this.feedService.getUserAlbum(userId, page, pageSize);
return ResponseEntity.ok(result);
} catch (Exception e) {
log.error("Error retrieving user album", e);
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
3.2.2.2 SocialFeedService
//com.socialapp.server.service.SocialFeedService
public PaginatedResult getUserAlbum(Long userId, Integer page, Integer pageSize) {
PaginatedResult result = new PaginatedResult();
result.setPage(page);
result.setPagesize(pageSize);
PageInfo<Post> pageData = this.socialContentApi.fetchUserAlbum(userId, page, pageSize);
if (CollectionUtils.isEmpty(pageData.getRecords())) {
return result;
}
result.setItems(this.transformToFeedItems(pageData.getRecords()));
return result;
}
3.2.3 Testing
3.3 Integration Testing4. Chat with Strangers
Clicking "Chat" on a user profile opens a question prompt. After answering, the system sends a connection request. If the recipient accepts, both users become connections.
User A clicks "Chat" on User B's profile:
4.1 Stranger Questions
Questions must be stored in the database to display when initiating chat.
4.1.1 Database Schema
CREATE TABLE `user_questions` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) DEFAULT NULL,
`question_text` varchar(200) DEFAULT NULL,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
4.1.2 Question Entity
package com.socialapp.common.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserQuestion extends BaseEntity {
private Long id;
private Long userId;
private String questionText;
}
4.1.3 QuestionRepository
package com.socialapp.common.repository;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.socialapp.common.entity.UserQuestion;
public interface QuestionRepository extends BaseMapper<UserQuestion> {
}
4.1.4 QuestionService
package com.socialapp.server.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryCondition;
import com.socialapp.common.repository.QuestionRepository;
import com.socialapp.common.entity.UserQuestion;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class QuestionService {
@Autowired
private QuestionRepository questionRepository;
public UserQuestion getUserQuestion(Long userId) {
QueryCondition condition = new QueryCondition();
condition.eq("user_id", userId);
return this.questionRepository.selectOne(condition);
}
}
4.2 Application Service Layer
API endpoint: https://mock-java.itheima.net/project/35/interface/api/635
4.2.1 UserProfileController
//com.socialapp.server.controller.UserProfileController
@GetMapping("strangerQuestions")
public ResponseEntity<String> fetchQuestion(@RequestParam("userId") Long userId) {
try {
String question = this.userProfileService.fetchQuestion(userId);
return ResponseEntity.ok(question);
} catch (Exception e) {
log.error("Error fetching question", e);
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
4.2.2 UserProfileService
//com.socialapp.server.service.UserProfileService
public String fetchQuestion(Long userId) {
UserQuestion question = this.questionService.getUserQuestion(userId);
if (question != null) {
return question.getQuestionText();
}
return "What are your hobbies?";
}
4.2.3 Testing
4.3 Submitting Question Answers
Clicking "Send" on the question dialog sends a system message with admin privileges.
4.3.1 Dubbo Service Layer
4.3.1.1 Interface Definition
//com.socialapp.dubbo.server.api.ChatApi
/**
* Send message as system administrator
*/
Boolean dispatchAdminMessage(String recipientUsername, MessageType type, String content);
package com.socialapp.dubbo.server.enums;
public enum MessageType {
TEXT("txt"), IMAGE("img"), LOCATION("loc"),
AUDIO("audio"), VIDEO("video"), FILE("file");
private final String code;
MessageType(String code) {
this.code = code;
}
public String getCode() {
return code;
}
}
4.3.1.2 Implementation
//com.socialapp.dubbo.server.api.ChatApiImpl
@Override
public Boolean dispatchAdminMessage(String recipient, MessageType type, String content) {
String endpoint = this.chatConfig.getBaseUrl()
+ this.chatConfig.getOrgName() + "/"
+ this.chatConfig.getAppName() + "/messages";
try {
String payload = JSONUtil.createObj()
.set("target_type", "users")
.set("target", JSONUtil.createArray().set(recipient))
.set("msg", JSONUtil.createObj()
.set("type", type.getCode())
.set("msg", content)).toString();
return this.httpClient.post(endpoint, payload).isSuccessful();
} catch (Exception e) {
log.error("Failed to send message to {}", recipient, e);
}
return false;
}
4.3.2 Application Service Layer
API endpoint: https://mock-java.itheima.net/project/35/interface/api/641
4.3.2.1 UserProfileController
//com.socialapp.server.controller.UserProfileController
/**
* Submit answer to stranger question
*/
@PostMapping("strangerQuestions")
public ResponseEntity<Void> submitAnswer(@RequestBody Map<String, Object> payload) {
try {
Long targetUserId = Long.valueOf(payload.get("userId").toString());
String answer = payload.get("reply").toString();
Boolean success = this.userProfileService.submitAnswer(targetUserId, answer);
if (success) {
return ResponseEntity.ok(null);
}
} catch (Exception e) {
log.error("Error submitting answer", e);
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
4.3.2.2 UserProfileService
//com.socialapp.server.service.UserProfileService
@DubboReference(version = "1.0.0")
private ChatApi chatApi;
public Boolean submitAnswer(Long targetUserId, String answer) {
User currentUser = UserContext.get();
UserProfile profile = this.userProfileService.fetchProfileByUserId(currentUser.getId());
Map<String, Object> message = new HashMap<>();
message.put("userId", currentUser.getId());
message.put("chatUsername", "CHAT_" + currentUser.getId());
message.put("displayName", profile.getNickName());
message.put("question", this.fetchQuestion(targetUserId));
message.put("answer", answer);
return this.chatApi.dispatchAdminMessage(
"CHAT_" + targetUserId, MessageType.TEXT, JSONUtil.toJsonStr(message));
}
4.3.3 Testing5. Who Viewed My Profile
Track visitors to your profile. Each visitor is recorded once per day. When fetching the list, return the last 5 visitors since the previous query time, or the 5 most recent visitors if no previous query exists.
5.1 Dubbo Service Layer
5.1.1 Entity Model
package com.socialapp.dubbo.server.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.mapping.Document;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "profile_visitors")
public class ProfileVisitor implements Serializable {
private static final long serialVersionUID = 2811682148052386573L;
private ObjectId id;
private Long profileOwnerId;
private Long visitorId;
private String sourceChannel;
private Long visitTimestamp;
private Double compatibilityScore;
}
5.1.2 Interface Definition
package com.socialapp.dubbo.server.api;
import com.socialapp.dubbo.server.model.ProfileVisitor;
import java.util.List;
public interface VisitorTrackingApi {
/**
* Record a profile visit
*/
String recordVisit(Long profileOwnerId, Long visitorId, String source);
/**
* Retrieve recent visitors. Returns last 5 visitors since last query,
* or 5 most recent if first query.
*/
List<ProfileVisitor> fetchRecentVisitors(Long profileOwnerId);
}
5.1.3 Implementation
package com.socialapp.dubbo.server.api;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.dubbo.config.annotation.Service;
import com.socialapp.dubbo.server.model.CompatibilityRecord;
import com.socialapp.dubbo.server.model.ProfileVisitor;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.List;
@Service(version = "1.0.0")
public class VisitorTrackingApiImpl implements VisitorTrackingApi {
@Autowired
private MongoTemplate mongoTemplate;
private static final String VISITOR_INDEX_KEY = "PROFILE_VISITOR";
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public String recordVisit(Long profileOwnerId, Long visitorId, String source) {
if (ObjectUtil.hasEmpty(profileOwnerId, visitorId, source)) {
return null;
}
String today = DateUtil.today();
Long dayStart = DateUtil.parseDateTime(today + " 00:00:00").getTime();
Long dayEnd = DateUtil.parseDateTime(today + " 23:59:59").getTime();
Query checkQuery = Query.query(Criteria.where("profileOwnerId").is(profileOwnerId)
.and("visitorId").is(visitorId)
.andOperator(Criteria.where("visitTimestamp").gte(dayStart),
Criteria.where("visitTimestamp").lte(dayEnd)
)
);
long existingCount = this.mongoTemplate.count(checkQuery, ProfileVisitor.class);
if (existingCount > 0) {
return null;
}
ProfileVisitor visitor = new ProfileVisitor();
visitor.setSourceChannel(source);
visitor.setVisitorId(visitorId);
visitor.setProfileOwnerId(profileOwnerId);
visitor.setVisitTimestamp(System.currentTimeMillis());
visitor.setId(ObjectId.get());
this.mongoTemplate.save(visitor);
return visitor.getId().toHexString();
}
@Override
public List<ProfileVisitor> fetchRecentVisitors(Long profileOwnerId) {
Long lastQueryTime = this.redisTemplate.opsForHash()
.get(VISITOR_INDEX_KEY, String.valueOf(profileOwnerId));
PageRequest pageRequest = PageRequest.of(0, 5, Sort.by(Sort.Order.desc("visitTimestamp")));
Query query = Query.query(Criteria.where("profileOwnerId").is(profileOwnerId))
.with(pageRequest);
if (lastQueryTime != null) {
query.addCriteria(Criteria.where("visitTimestamp").gte(lastQueryTime));
}
List<ProfileVisitor> visitors = this.mongoTemplate.find(query, ProfileVisitor.class);
for (ProfileVisitor visitor : visitors) {
Query scoreQuery = Query.query(Criteria.where("targetUserId")
.is(profileOwnerId).and("userId").is(visitor.getVisitorId())
);
CompatibilityRecord record = this.mongoTemplate.findOne(scoreQuery, CompatibilityRecord.class);
if (record != null) {
visitor.setCompatibilityScore(record.getScore());
} else {
visitor.setCompatibilityScore(90.0);
}
}
return visitors;
}
}
5.1.4 Unit Tests
package com.socialapp.dubbo.server.api;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class VisitorTrackingApiTests {
@Autowired
private VisitorTrackingApi visitorTrackingApi;
@Test
public void testRecordVisit() {
this.visitorTrackingApi.recordVisit(1L, 2L, "Profile Page");
this.visitorTrackingApi.recordVisit(1L, 3L, "Profile Page");
this.visitorTrackingApi.recordVisit(1L, 2L, "Profile Page");
}
@Test
public void testFetchRecentVisitors() {
this.visitorTrackingApi.fetchRecentVisitors(1L)
.forEach(System.out::println);
}
}
5.2 Application Service Layer
API ednpoint: https://mock-java.itheima.net/project/35/interface/api/743
package com.socialapp.server.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class VisitorSummaryDto {
private Long userId;
private String avatar;
private String nickname;
private String gender;
private Integer age;
private String[] tags;
private Integer compatibilityScore;
}
5.2.2 SocialFeedController
//com.socialapp.server.controller.SocialFeedController
/**
* Get profile visitors
*/
@GetMapping("visitors")
public ResponseEntity<List<VisitorSummaryDto>> getProfileVisitors() {
try {
List<VisitorSummaryDto> visitors = this.feedService.getProfileVisitors();
return ResponseEntity.ok(visitors);
} catch (Exception e) {
log.error("Error fetching visitors", e);
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
5.2.3 SocialFeedService
//com.socialapp.server.service.SocialFeedService
public List<VisitorSummaryDto> getProfileVisitors() {
User currentUser = UserContext.get();
List<ProfileVisitor> visitors =
this.visitorTrackingApi.fetchRecentVisitors(currentUser.getId());
if (CollectionUtils.isEmpty(visitors)) {
return Collections.emptyList();
}
List<Object> visitorIds = visitors.stream()
.map(ProfileVisitor::getVisitorId)
.collect(Collectors.toList());
List<UserProfile> profiles =
this.userProfileService.fetchProfilesByIds(visitorIds);
List<VisitorSummaryDto> visitorDtos = new ArrayList<>();
for (ProfileVisitor visitor : visitors) {
UserProfile profile = profiles.stream()
.filter(p -> p.getUserId().equals(visitor.getVisitorId()))
.findFirst()
.orElse(null);
if (profile != null) {
VisitorSummaryDto dto = new VisitorSummaryDto();
dto.setAge(profile.getAge());
dto.setAvatar(profile.getAvatarUrl());
dto.setGender(profile.getGender().name().toLowerCase());
dto.setUserId(profile.getUserId());
dto.setNickname(profile.getNickName());
dto.setTags(profile.getTags().split(","));
dto.setCompatibilityScore(visitor.getCompatibilityScore().intValue());
visitorDtos.add(dto);
}
}
return visitorDtos;
}
5.3 Recording Visitor Data
//com.socialapp.server.service.UserProfileService
public UserProfileSummary getUserProfile(Long userId) {
UserProfile profileData = this.userProfileService.fetchProfileByUserId(userId);
if (profileData == null) {
return null;
}
UserProfileSummary summary = new UserProfileSummary();
summary.setUserId(userId);
summary.setAge(profileData.getAge());
summary.setGender(profileData.getGender().name().toLowerCase());
summary.setNickname(profileData.getNickName());
summary.setTags(StrUtil.split(profileData.getTags(), ','));
summary.setAvatar(profileData.getAvatarUrl());
User currentUser = UserContext.get();
summary.setCompatibilityScore(
this.compatibilityService.getScore(userId, currentUser.getId()).longValue());
//Record this visit
this.visitorTrackingApi.recordVisit(userId, currentUser.getId(), "Profile Page");
return summary;
}