Implementing Today's Best Match and User Recommendation with Caching
- Homepage feature explanation
- System architecture overview
- Implementing the 'Today's Best Match' feature
- Implementing the recommended users list
- Adding caching to API endpoints
- Integration testing with the frontend
1. Homepage
After successful login, users enter the homepage. The homepage features include 'Today's Best Match', 'Recommended Friends', 'Tanhua', and 'Search Nearby'.
2. System Architecture
After developing the login functionality in the SSO system, we now need to implement other features. In the overall architecture, the service project that communicates with the APP is called my-tanhua-server. The core business logic is implemented via Dubbo in a project called my-tanhua-dubbo. The architecture diagram is as follows:
Key points:
- Client APP sends requests to Nginx, which then routes requests to the SSO system or the server system.
- The SSO system handles third-party platform integration, data caching, message sending, and user registration/login.
- The server system provides API service support for the APP (i.e., all non-login API requests go through this service, acting as an intermediary).
- The token included in user requests needs to be validated by the SSO system.
- It invokes Dubbo services via RPC, where the Dubbo service interacts with MongoDB for CRUD operations.
- Some data is cached in Redis to improve query performance.
- User data queries are performed against a MySQL database.
2.1. Nginx Service
2.1.1. Installation
The installation package is provided in the resources: nginx-1.17.3.zip.
Install it in any directory and start it using the command: start nginx.exe
To reload the configuration file: nginx.exe -s reload
2.1.2. Configuration
Edit the nginx.conf file in the conf directory:
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
location /user/ { # Requests starting with /user/ are forwarded to the SSO system
client_max_body_size 300m; # Set max request body size, solves large file upload issues
proxy_connect_timeout 300s; # Proxy connection timeout
proxy_send_timeout 300s; # Proxy send timeout
proxy_read_timeout 300s; # Proxy read timeout
proxy_pass http://127.0.0.1:18080; # Forward request
}
location / { # Requests not matched above are handled here
client_max_body_size 300m;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
proxy_pass http://127.0.0.1:18081; # Forward request to server system
}
}
}
2.1.3. Testing
2.2. Setting up the Server Project
2.2.1. Import Dependencies
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>my-tanhua</artifactId>
<groupId>cn.itcast.tanhua</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>my-tanhua-server</artifactId>
<dependencies>
<dependency>
<groupId>cn.itcast.tanhua</groupId>
<artifactId>my-tanhua-dubbo-interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Dubbo Spring Boot support -->
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>
<!-- Dubbo framework -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
</dependency>
<!-- Zookeeper dependency -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
</dependencies>
</project>
2.2.2. Application Properties
application.properties:
spring.application.name = itcast-tanhua-server
server.port = 18081
# Database connection
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.31.81:3306/mytanhua?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
# Enum package scan
mybatis-plus.type-enums-package=com.tanhua.server.enums
# Table prefix
mybatis-plus.global-config.db-config.table-prefix=tb_
# ID strategy: auto-increment
mybatis-plus.global-config.db-config.id-type=auto
# Dubbo registry configuration
spring.dubbo.application.name = itcast-tanhua-server
dubbo.registry.address = zookeeper://192.168.31.81:2181
dubbo.registry.client = zkclient
dubbo.registry.timeout = 60000
dubbo.consumer.timeout = 60000
# SSO system service address
tanhua.sso.url=http://127.0.0.1
# Default recommended user for Today's Best Match
tanhua.sso.default.user=2
2.2.3. Server Application Entry Point
package com.tanhua.server;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan("com.tanhua.server.mapper")
@SpringBootApplication
public class ServerApplication {
public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
}
}
2.3. Setting up the Dubbo Project
my-tanhua-dubbo is the parent project for the Dubbo projects:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>my-tanhua</artifactId>
<groupId>cn.itcast.tanhua</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>my-tanhua-dubbo</artifactId>
<packaging>pom</packaging>
<modules>
<module>my-tanhua-dubbo-interface</module>
<module>my-tanhua-dubbo-service</module>
</modules>
</project>
2.3.1. Creating the my-tanhua-dubbo-interface Project
This project defines the interfaces and entity objects for the Dubbo services.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>my-tanhua-dubbo</artifactId>
<groupId>cn.itcast.tanhua</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>my-tanhua-dubbo-interface</artifactId>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
</dependencies>
</project>
2.3.2. Creating the my-tanhua-dubbo-service Project
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>my-tanhua-dubbo</artifactId>
<groupId>cn.itcast.tanhua</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>my-tanhua-dubbo-service</artifactId>
<dependencies>
<!-- Interface dependency -->
<dependency>
<groupId>cn.itcast.tanhua</groupId>
<artifactId>my-tanhua-dubbo-interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Dubbo Spring Boot support -->
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>
<!-- Dubbo framework -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
</dependency>
<!-- Zookeeper dependency -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
</dependency>
<!-- MongoDB related dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-sync</artifactId>
</dependency>
<!-- Other utility dependencies -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
</dependencies>
</project>
application.properties for the service:
# Spring boot application
spring.application.name = itcast-tanhua-dubbo-service
# Dubbo scan package configuration
dubbo.scan.basePackages = com.tanhua.dubbo.server
dubbo.application.name = dubbo-provider-tanhua
# Dubbo exposed port
dubbo.protocol.name = dubbo
dubbo.protocol.port = 20880
# Dubbo registry configuration
dubbo.registry.address = zookeeper://192.168.31.81:2181
dubbo.registry.client = zkclient
dubbo.registry.timeout = 60000
# Spring Boot MongoDB configuration
spring.data.mongodb.username=tanhua
spring.data.mongodb.password=l3SCjl0HvmSkTtiSbN0Swv40spYnHhDV
spring.data.mongodb.authentication-database=admin
spring.data.mongodb.database=tanhua
spring.data.mongodb.port=27017
spring.data.mongodb.host=192.168.31.81
Dubbo Service Application Entry Point:
package com.tanhua.dubbo.server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DubboApplication {
public static void main(String[] args) {
SpringApplication.run(DubboApplication.class, args);
}
}
2.4. Project Structure
The final project structure is as follows:
The relationships between projects are shown below:
3. Today's Best Match
The 'Today's Best Match' feature recommends the user with the highest affinity score. The affinity score is calculated based on user behavior, such as clicks, likes, comments, education level, marital status, etc.
Implementation approach: We will first assume that the recommendation results are already available. We only need to query the user with the highest affinity score from these results. The actual recommendation logic will be covered in later lessons.
Flow:
3.1. Data Structure
// Collection: recommend_user
{
"userId": 1001, // Recommended user ID
"toUserId": 1002, // Target user ID (the recipient of the recommendation)
"score": 90, // Recommendation score (affinity value)
"date": "2019/1/1" // Date of calculation
}
There are already 4855 test data records available:
3.2. Writing the Dubbo Service
3.2.1. Define the Interface
In the my-tanhua-dubbo-interface project:
package com.tanhua.dubbo.server.api;
import com.tanhua.dubbo.server.vo.PageInfo;
import com.tanhua.dubbo.server.pojo.RecommendUser;
public interface RecommendUserApi {
/**
* Query the user with the highest recommendation score for a given user.
*
* @param userId Target user ID
* @return RecommendUser with the highest score, or null if none
*/
RecommendUser queryWithMaxScore(Long userId);
/**
* Query a paginated list of recommended users, sorted by score in descending order.
*
* @param userId Target user ID
* @param pageNum Page number (1-based)
* @param pageSize Number of items per page
* @return PageInfo containing the list of recommended users
*/
PageInfo<RecommendUser> queryPageInfo(Long userId, Integer pageNum, Integer pageSize);
}
Entity class:
package com.tanhua.dubbo.server.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "recommend_user")
public class RecommendUser implements java.io.Serializable {
private static final long serialVersionUID = -4296017160071130962L;
@Id
private ObjectId id; // Primary key
@Indexed
private Long userId; // Recommended user ID
private Long toUserId; // Target user ID
@Indexed
private Double score; // Recommendation score
private String date; // Date
}
PageInfo VO:
package com.tanhua.dubbo.server.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.Collections;
import java.util.List;
@Data
@AllArgsConstructor
public class PageInfo<T> implements java.io.Serializable {
private static final long serialVersionUID = -2105385689859184204L;
private Integer total = 0;
private Integer pageNum = 0;
private Integer pageSize = 0;
private List<T> records = Collections.emptyList();
}
3.2.2. Implement the Service
In the my-tanhua-dubbo-service module:
package com.tanhua.dubbo.server.api;
import com.alibaba.dubbo.config.annotation.Service;
import com.tanhua.dubbo.server.pojo.RecommendUser;
import com.tanhua.dubbo.server.vo.PageInfo;
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 java.util.List;
@Service(version = "1.0.0")
public class RecommendUserApiImpl implements RecommendUserApi {
@Autowired
private MongoTemplate mongoTemplate;
@Override
public RecommendUser queryWithMaxScore(Long userId) {
Query query = Query.query(Criteria.where("toUserId").is(userId))
.with(Sort.by(Sort.Order.desc("score")))
.limit(1);
return mongoTemplate.findOne(query, RecommendUser.class);
}
@Override
public PageInfo<RecommendUser> queryPageInfo(Long userId, Integer pageNum, Integer pageSize) {
PageRequest pageRequest = PageRequest.of(pageNum - 1, pageSize, Sort.by(Sort.Order.desc("score")));
Query query = Query.query(Criteria.where("toUserId").is(userId)).with(pageRequest);
List<RecommendUser> recommendUserList = mongoTemplate.find(query, RecommendUser.class);
return new PageInfo<>(0, pageNum, pageSize, recommendUserList);
}
}
3.2.3. Testing
package com.tanhua.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 TestRecommendUserApi {
@Autowired
private RecommendUserApi recommendUserApi;
@Test
public void testQueryWithMaxScore() {
System.out.println(recommendUserApi.queryWithMaxScore(1L));
System.out.println(recommendUserApi.queryWithMaxScore(8L));
System.out.println(recommendUserApi.queryWithMaxScore(26L));
}
@Test
public void testQueryPageInfo() {
System.out.println(recommendUserApi.queryPageInfo(1L, 1, 5));
System.out.println(recommendUserApi.queryPageInfo(1L, 2, 5));
System.out.println(recommendUserApi.queryPageInfo(1L, 3, 5));
}
}
3.3. Implementing the 'Today's Best Match' Service
Now we will implement the service layer for the 'Today's Best Match' feature.
3.3.1. Base Classes and Enums
SexEnum:
package com.tanhua.server.enums;
import com.baomidou.mybatisplus.core.enums.IEnum;
public enum SexEnum implements IEnum<Integer> {
MAN(1, "男"),
WOMAN(2, "女"),
UNKNOWN(3, "未知");
private int value;
private String desc;
SexEnum(int value, String desc) {
this.value = value;
this.desc = desc;
}
@Override
public Integer getValue() {
return this.value;
}
@Override
public String toString() {
return this.desc;
}
}
BasePojo:
package com.tanhua.server.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import java.util.Date;
public abstract class BasePojo {
@TableField(fill = FieldFill.INSERT)
private Date created;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updated;
}
User:
package com.tanhua.server.pojo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class User extends BasePojo {
private Long id;
private String mobile;
@JsonIgnore
private String password;
}
UserInfo:
package com.tanhua.server.pojo;
import com.tanhua.server.enums.SexEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserInfo extends BasePojo {
private Long id;
private Long userId;
private String nickName;
private String logo;
private String tags;
private SexEnum sex;
private Integer age;
private String edu;
private String city;
private String birthday;
private String coverPic;
private String industry;
private String income;
private String marriage;
}
3.3.2. Implementation Details
TodayBest VO:
package com.tanhua.server.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TodayBest {
private Long id;
private String avatar;
private String nickname;
private String gender; // "man" or "woman"
private Integer age;
private String[] tags;
private Long fateValue; // Affinity score
}
TodayBestController:
package com.tanhua.server.controller;
import com.tanhua.server.service.TodayBestService;
import com.tanhua.server.vo.TodayBest;
import lombok.extern.slf4j.Slf4j;
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("tanhua")
@Slf4j
public class TodayBestController {
@Autowired
private TodayBestService todayBestService;
@GetMapping("todayBest")
public ResponseEntity<TodayBest> queryTodayBest(@RequestHeader("Authorization") String token) {
try {
TodayBest todayBest = todayBestService.queryTodayBest(token);
if (todayBest != null) {
return ResponseEntity.ok(todayBest);
}
} catch (Exception e) {
log.error("Error querying Today's Best Match. token = {}", token, e);
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
}
}
TodayBestService:
package com.tanhua.server.service;
import com.tanhua.server.pojo.User;
import com.tanhua.server.pojo.UserInfo;
import com.tanhua.server.vo.TodayBest;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Service
public class TodayBestService {
@Autowired
private UserService userService;
@Autowired
private RecommendUserService recommendUserService;
@Autowired
private UserInfoService userInfoService;
@Value("${tanhua.sso.default.user}")
private Long defaultUser;
public TodayBest queryTodayBest(String token) {
// Validate token via SSO interface
User user = userService.queryUserByToken(token);
if (user == null) {
return null; // Token is invalid or expired
}
// Query the recommended user (Today's Best Match)
TodayBest todayBest = recommendUserService.queryTodayBest(user.getId());
if (todayBest == null) {
// Provide a default recommended user
todayBest = new TodayBest();
todayBest.setId(defaultUser);
todayBest.setFateValue(80L); // Constant value
}
// Fill in personal information
UserInfo userInfo = userInfoService.queryUserInfoByUserId(todayBest.getId());
if (userInfo == null) {
return null;
}
todayBest.setAvatar(userInfo.getLogo());
todayBest.setNickname(userInfo.getNickName());
todayBest.setTags(StringUtils.split(userInfo.getTags(), ','));
todayBest.setGender(userInfo.getSex().getValue() == 1 ? "man" : "woman");
todayBest.setAge(userInfo.getAge());
return todayBest;
}
}
UserService:
package com.tanhua.server.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tanhua.server.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
@Slf4j
public class UserService {
@Autowired
private RestTemplate restTemplate;
@Value("${tanhua.sso.url}")
private String ssoUrl;
private static final ObjectMapper MAPPER = new ObjectMapper();
public User queryUserByToken(String token) {
String url = ssoUrl + "/user/" + token;
try {
String data = restTemplate.getForObject(url, String.class);
if (StringUtils.isEmpty(data)) {
return null;
}
return MAPPER.readValue(data, User.class);
} catch (Exception e) {
log.error("Error validating token. token = {}", token, e);
}
return null;
}
}
RestTemplate Configuration:
package com.tanhua.server.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.Charset;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
RestTemplate restTemplate = new RestTemplate(factory);
// Support Chinese encoding
restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(Charset.forName("UTF-8")));
return restTemplate;
}
@Bean
public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setReadTimeout(5000);
factory.setConnectTimeout(5000);
return factory;
}
}
RecommendUserService:
package com.tanhua.server.service;
import com.alibaba.dubbo.config.annotation.Reference;
import com.tanhua.dubbo.server.api.RecommendUserApi;
import com.tanhua.dubbo.server.pojo.RecommendUser;
import com.tanhua.server.vo.TodayBest;
import org.springframework.stereotype.Service;
@Service
public class RecommendUserService {
@Reference(version = "1.0.0")
private RecommendUserApi recommendUserApi;
public TodayBest queryTodayBest(Long userId) {
RecommendUser recommendUser = recommendUserApi.queryWithMaxScore(userId);
if (recommendUser == null) {
return null;
}
TodayBest todayBest = new TodayBest();
todayBest.setId(recommendUser.getUserId());
// Affinity score, floored to integer
double score = Math.floor(recommendUser.getScore());
todayBest.setFateValue((long) score);
return todayBest;
}
public PageInfo<RecommendUser> queryRecommendUserList(Long userId, Integer page, Integer pageSize) {
return recommendUserApi.queryPageInfo(userId, page, pageSize);
}
}
UserInfoService:
package com.tanhua.server.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.tanhua.server.mapper.UserInfoMapper;
import com.tanhua.server.pojo.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserInfoService {
@Autowired
private UserInfoMapper userInfoMapper;
public UserInfo queryUserInfoByUserId(Long userId) {
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", userId);
return userInfoMapper.selectOne(queryWrapper);
}
public List<UserInfo> queryUserInfoList(QueryWrapper<UserInfo> queryWrapper) {
return userInfoMapper.selectList(queryWrapper);
}
}
UserInfoMapper:
package com.tanhua.server.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.tanhua.server.pojo.UserInfo;
public interface UserInfoMapper extends BaseMapper<UserInfo> {
}
3.3.3. Testing
Unit test for the Dubbo service:
package com.tanhua.server;
import com.tanhua.server.service.RecommendUserService;
import com.tanhua.server.vo.TodayBest;
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.SpringJUnit4ClassRunner;
@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class TestRecommendUserApi {
@Autowired
private RecommendUserService recommendUserService;
@Test
public void testQueryTodayBest() {
TodayBest todayBest = recommendUserService.queryTodayBest(1L);
System.out.println(todayBest);
}
}
Integration testing requires the SSO and Dubbo services to be started.
3.3.4. Fix: @Reference Injection Returning null
Add @EnableDubbo to the ServerApplication entry point (though it might work without it).
3.3.5. Fix: MongoDB Auto Configuration
Since the server project includes MongoDB dependencies but does not use MongoDB directly, we need to exclude MongoDB auto-configuration to avoid startup errors.
package com.tanhua.server;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
@MapperScan("com.tanhua.server.mapper")
@SpringBootApplication(exclude = {MongoAutoConfiguration.class, MongoDataAutoConfiguration.class})
public class ServerApplication {
public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
}
}
4. Recommended Users List
4.1. Data Models
RecommendUserQueryParam:
package com.tanhua.server.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RecommendUserQueryParam {
private Integer page = 1;
private Integer pagesize = 10;
private String gender; // "man" or "woman"
private String lastLogin;
private Integer age;
private String city;
private String education;
}
PageResult:
package com.tanhua.server.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Collections;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult {
private Integer counts = 0;
private Integer pagesize = 0;
private Integer pages = 0;
private Integer page = 0;
private List<?> items = Collections.emptyList();
}
4.2. Controller Method
Add the followign method to TodayBestController:
@GetMapping("recommendation")
public ResponseEntity<PageResult> queryRecommendation(@RequestHeader("Authorization") String token,
RecommendUserQueryParam queryParam) {
try {
PageResult pageResult = todayBestService.queryRecommendation(token, queryParam);
if (pageResult != null) {
return ResponseEntity.ok(pageResult);
}
} catch (Exception e) {
log.error("Error querying recommended user list. token = {}", token, e);
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
}
4.3. Service Implementation
Add the following method to TodayBestService:
public PageResult queryRecommendation(String token, RecommendUserQueryParam queryParam) {
// Validate token
User user = userService.queryUserByToken(token);
if (user == null) {
return null;
}
PageResult pageResult = new PageResult();
pageResult.setPage(queryParam.getPage());
pageResult.setPagesize(queryParam.getPagesize());
PageInfo<RecommendUser> pageInfo = recommendUserService.queryRecommendUserList(user.getId(),
queryParam.getPage(), queryParam.getPagesize());
List<RecommendUser> records = pageInfo.getRecords();
if (org.springframework.util.CollectionUtils.isEmpty(records)) {
return pageResult;
}
// Collect user IDs
Set<Long> userIds = records.stream().map(RecommendUser::getUserId).collect(Collectors.toSet());
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.in("user_id", userIds);
if (StringUtils.isNotEmpty(queryParam.getGender())) {
queryWrapper.eq("sex", StringUtils.equals(queryParam.getGender(), "man") ? 1 : 2);
}
if (StringUtils.isNotEmpty(queryParam.getCity())) {
queryWrapper.like("city", queryParam.getCity());
}
if (queryParam.getAge() != null) {
queryWrapper.le("age", queryParam.getAge());
}
List<UserInfo> userInfoList = userInfoService.queryUserInfoList(queryWrapper);
if (org.springframework.util.CollectionUtils.isEmpty(userInfoList)) {
return pageResult;
}
// Build response list
List<TodayBest> todayBests = new ArrayList<>();
for (UserInfo userInfo : userInfoList) {
TodayBest todayBest = new TodayBest();
todayBest.setId(userInfo.getUserId());
todayBest.setAvatar(userInfo.getLogo());
todayBest.setNickname(userInfo.getNickName());
todayBest.setTags(StringUtils.split(userInfo.getTags(), ','));
todayBest.setGender(userInfo.getSex().getValue() == 1 ? "man" : "woman");
todayBest.setAge(userInfo.getAge());
// Set fate value from recommendation data
for (RecommendUser record : records) {
if (record.getUserId().longValue() == userInfo.getUserId().longValue()) {
double score = Math.floor(record.getScore());
todayBest.setFateValue((long) score);
break;
}
}
todayBests.add(todayBest);
}
// Sort by fate value in descending order
todayBests.sort((o1, o2) -> Long.compare(o2.getFateValue(), o1.getFateValue()));
pageResult.setItems(todayBests);
return pageResult;
}
4.4. Testing
5. Caching
It is beneficial to cache API responses, especially for GET requests. To avoid repetitive caching logic in each method, we implement a generic solution.
Approach:
- Use an interceptor to intercept requests and check for cached data.
- Use
ResponseBodyAdviceto intercept responses and cache the data in Redis. - Create a
@Cacheannotation to mark which controllers should be cached. Only methods with this annotation and GET mapping will be cached.
Flow:
5.1. Custom Annotation
package com.tanhua.server.utils;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Cache {
/**
* Cache duration in seconds. Default is 60 seconds.
*/
String time() default "60";
}
5.2. Cache Interceptor
package com.tanhua.server.interceptor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tanhua.server.utils.Cache;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class RedisCacheInterceptor implements HandlerInterceptor {
@Value("${tanhua.cache.enable}")
private Boolean enable;
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final ObjectMapper MAPPER = new ObjectMapper();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!enable) {
return true;
}
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
if (!handlerMethod.hasMethodAnnotation(GetMapping.class)) {
return true;
}
if (!handlerMethod.hasMethodAnnotation(Cache.class)) {
return true;
}
// Check if cache exists
String redisKey = createRedisKey(request);
String cacheData = redisTemplate.opsForValue().get(redisKey);
if (StringUtils.isEmpty(cacheData)) {
// Cache miss, proceed to controller
return true;
}
// Cache hit, write data to response
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.getWriter().write(cacheData);
return false; // Skip further processing
}
/**
* Generates Redis key: SERVER_CACHE_DATA_MD5(url + params + token)
*/
public static String createRedisKey(HttpServletRequest request) throws Exception {
String url = request.getRequestURI();
String param = MAPPER.writeValueAsString(request.getParameterMap());
String token = request.getHeader("Authorization");
String data = url + "_" + param + "_" + token;
return "SERVER_CACHE_DATA_" + DigestUtils.md5Hex(data);
}
}
5.3. Response Body Advice for Caching
package com.tanhua.server.interceptor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tanhua.server.utils.Cache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.MethodParameter;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.concurrent.TimeUnit;
@ControllerAdvice
public class MyResponseBodyAdvice implements ResponseBodyAdvice {
@Value("${tanhua.cache.enable}")
private Boolean enable;
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final ObjectMapper MAPPER = new ObjectMapper();
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return enable &&
returnType.hasMethodAnnotation(GetMapping.class) &&
returnType.hasMethodAnnotation(Cache.class);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body == null) {
return null;
}
try {
String redisValue;
if (body instanceof String) {
redisValue = (String) body;
} else {
redisValue = MAPPER.writeValueAsString(body);
}
String redisKey = RedisCacheInterceptor.createRedisKey(
((ServletServerHttpRequest) request).getServletRequest());
Cache cache = returnType.getMethodAnnotation(Cache.class);
redisTemplate.opsForValue().set(redisKey, redisValue, Long.parseLong(cache.time()), TimeUnit.SECONDS);
} catch (Exception e) {
e.printStackTrace();
}
return body;
}
}
5.4. Configuration
application.properties additions:
# Whether to enable data caching
tanhua.cache.enable=false
# Redis cluster configuration
spring.redis.jedis.pool.max-wait = 5000ms
spring.redis.jedis.pool.max-Idle = 100
spring.redis.jedis.pool.min-Idle = 10
spring.redis.timeout = 10s
spring.redis.cluster.nodes = 192.168.31.81:6379,192.168.31.81:6380,192.168.31.81:6381
spring.redis.cluster.max-redirects=5
5.5. Register Interceptor
package com.tanhua.server.config;
import com.tanhua.server.interceptor.RedisCacheInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private RedisCacheInterceptor redisCacheInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(redisCacheInterceptor).addPathPatterns("/**");
}
}
5.6. Testing
Test the caching functionality by enabling caching and verifying that responses are cached.
6. Integration Testing
When testing, note that the user data is limited. You may need to comment out some filter conditions to get results.
Final results should look similar to the mockups.