Table of contents
1.1. What is instant messaging? edit
2.1. Introduction to development
3. Extract ring message components
4.2. Query Huanxin user information
4.3. Huanxin user ID query user information
4.4. Send a message to the client
5.3. Reply to strangers' messages
1. Instant messaging
1.1. What is instant messaging?
1.2. Function description
The chat function similar to WeChat is also provided in the Tanhua dating project, and users can chat with friends or strangers.
If it is a stranger, use the "chat" function to say hello, and if the other party agrees, they become friends and can chat.
If strangers like each other, they will become friends and can chat.
In the message interface, you can also view: likes, comments, likes, announcements and other news information.
1.3. Technical solution
The implementation of high-concurrency instant messaging is still very challenging. There are many points to consider. In addition to implementing functions, concurrency, traffic, load, servers, disaster recovery, etc. must also be considered. Although it is difficult, it is not unattainable.
For real instant messaging, there are often two options:
-
Option One:
-
Self-realization, from design to architecture to implementation.
-
Technical aspects can be used: Netty + WebSocket + RocketMQ + MongoDB + Redis + ZooKeeper + MySQL
-
Option II:
-
Docking with third-party services is complete.
-
This method is simple, and only needs to be connected according to the third-party API.
-
Such as: Huanxin, NetEase, Ronglian Cloud Communication, etc.
-
-
How to choose?
If it is a large and medium-sized enterprise doing projects, you can choose independent research and development. If it is a small and medium-sized enterprise developing small and medium-sized projects, you can choose the second option. Option 1 requires a lot of manpower and material support, long development cycle, high cost, but strong controllability. The second solution is low cost, short development cycle, and can be quickly integrated for functional development, but it is not as good in terms of controllability.
The Tanhua Dating Project chooses option 2 to implement.
2. Huanxin
Official website: Huanxin - the pioneer of China's IM instant messaging cloud service! Stable and robust, message must reach, billion-level concurrent instant messaging cloud
2.1. Introduction to development
Platform architecture:
integrated:
The integration of Huanxin and the user system mainly occurs in two places, server-side integration and client-side integration.
Flower detection integration:
-
The front end of Tanhua uses AndroidSDK for integration
-
Back-end integrated user system
-
Documentation: User System Integration [IM Development Documentation]
-
2.2. Huanxin Console
If you need to use the Huanxin platform, you must register, and you can create an application after logging in. Users with less than 100 Huanxin users can use it for free, and users with more than 100 must register for the enterprise version.
Enterprise Edition Price:
Create an application:
Created:
2.3. Interface description
2.3.1, Appkey data structure
After you apply for AppKey, you will get a string in the format of xxxx#xxxx , which can only consist of lowercase letters and numbers. AppKey is the unique identifier of Huanxin application. The first half of org_name is the unique identifier of the tenant under the multi-tenant system, and the second half of app_name is the unique identifier of the app under the tenant (the application id filled in when creating an app in the ring letter background is the app_name). In the following REST API, requests to /{org_name}/{app_name} are made for a unique appkey. At present, the appkey registered by Huanxin cannot be deleted by the user. If you want to delete the APP, you need to contact Huanxin to complete the operation.
Appkey | xxxx | delimiter | xxxx |
---|---|---|---|
The unique identifier of Huanxin application | org_name | # | app_name |
2.3.2, Huanxin ID data structure
As a chat channel, Huanxin only needs to provide Huanxin ID (that is, IM user name) and password.
name | field name | type of data | describe |
---|---|---|---|
Huanxin ID | username | String | Unique username within the scope of the AppKey. |
user password | password | String | The password used by the user to log in to Huanxin. |
2.3.4. Obtain administrator privileges
The REST API provided by Huanxin requires permission to access. The permission is reflected by carrying the token when sending the HTTP request. The following describes the way to obtain the token. Note: The parameters such as {APP client_id} used in API description need to be replaced with specific values.
Important reminder: The server will return the validity period of the token when obtaining the token. For the specific value, refer to the value of the expires_in field returned by the interface. Due to network delays and other reasons, the system does not guarantee that the token is absolutely valid within the validity period indicated by this value. If you find that the token is used abnormally, please obtain a new token again. For example, "http response code" returns 401. In addition, please do not frequently send requests to the server to obtain tokens. If the same account sends this request more than a certain frequency, it will be blocked by the server. Remember, remember! !
client_id and client_secret can be seen on the APP details page of Huanxin management background .
HTTP Request
post | /{org_name}/{app_name}/token |
---|---|
Request Headers
parameter | illustrate |
---|---|
Content-Type | application/json |
Request Body
parameter | illustrate |
---|---|
grant_type | client_credentials |
client_id | The client_id of the app can be found on the app details page |
client_secret | The client_secret of the app can be found on the app details page |
Response Body
parameter | illustrate |
---|---|
access_token | valid token string |
expires_in | The valid time of the token, in seconds, does not need to be obtained repeatedly during the valid period |
application | The UUID value of the current App |
3. Extract ring message components
Extract ring letter components to tanhua-autoconfig
the project
3.1, write HuanXinTemplate
@Slf4j
public class HuanXinTemplate {
private EMService service;
public HuanXinTemplate(HuanXinProperties properties) {
EMProperties emProperties = EMProperties.builder()
.setAppkey(properties.getAppkey())
.setClientId(properties.getClientId())
.setClientSecret(properties.getClientSecret())
.build();
service = new EMService(emProperties);
}
//创建环信用户
public Boolean createUser(String username,String password) {
try {
//创建环信用户
service.user().create(username.toLowerCase(), password)
.block();
return true;
}catch (Exception e) {
e.printStackTrace();
log.error("创建环信用户失败~");
}
return false;
}
//添加联系人
public Boolean addContact(String username1,String username2) {
try {
//创建环信用户
service.contact().add(username1,username2)
.block();
return true;
}catch (Exception e) {
log.error("添加联系人失败~");
}
return false;
}
//删除联系人
public Boolean deleteContact(String username1,String username2) {
try {
//创建环信用户
service.contact().remove(username1,username2)
.block();
return true;
}catch (Exception e) {
log.error("删除联系人失败~");
}
return false;
}
//发送消息
public Boolean sendMsg(String username,String content) {
try {
//接收人用户列表
Set<String> set = CollUtil.newHashSet(username);
//文本消息
EMTextMessage message = new EMTextMessage().text(content);
//发送消息 from:admin是管理员发送
service.message().send("admin","users",
set,message,null).block();
return true;
}catch (Exception e) {
log.error("删除联系人失败~");
}
return false;
}
}
3.2, write Properties object
@Configuration
@ConfigurationProperties(prefix = "tanhua.huanxin")
@Data
public class HuanXinProperties {
private String appkey;
private String clientId;
private String clientSecret;
}
3.3. Configuration
tanhua-app-server
application.yml
The file configuration of the project is as follows
tanhua:
huanxin:
appkey: 1110201018107234#tanhua
clientId: YXA6nxJJ_pdEQ_eYUlqcRicS4w
clientSecret: YXA6GMUxVEZhAvxlMn4OvHSXbWuEUTE
3.4. Test
@RunWith(SpringRunner.class)
@SpringBootTest
public class HuanXinTest {
@Autowired
private HuanXinTemplate template;
@Test
public void testRegister() {
template.createUser("user01","123456");
}
}
4. User system integration
Write the logic of user system integration into tanhua-server
the system.
-
When Tanhua users register, they need to register the user information in the Huanxin system
-
For old data: write a unit test method to register in batches to Huanxin
-
For new users: modify the code (when the user registers, it will be automatically registered to Huanxin)
-
-
The APP obtains the Huanxin user password of the current user from the server, and automatically logs into the Huanxin system
-
Write an interface to get the username and password of the current user in Huanxin
-
-
The APP automatically obtains the information data sent by the Huanxin server
4.1. Register Huanxin user
In the user login logic, when registering for the first time, register user information to Huanxin
/**
* 验证登录
* @param phone
* @param code
*/
public Map loginVerification(String phone, String code) {
//1、从redis中获取下发的验证码
String redisCode = redisTemplate.opsForValue().get("CHECK_CODE_" + phone);
//2、对验证码进行校验(验证码是否存在,是否和输入的验证码一致)
if(StringUtils.isEmpty(redisCode) || !redisCode.equals(code)) {
//验证码无效
throw new BusinessException(ErrorResult.loginError());
}
//3、删除redis中的验证码
redisTemplate.delete("CHECK_CODE_" + phone);
//4、通过手机号码查询用户
User user = userApi.findByMobile(phone);
boolean isNew = false;
//5、如果用户不存在,创建用户保存到数据库中
if(user == null) {
user = new User();
user.setMobile(phone);
user.setPassword(DigestUtils.md5Hex("123456"));
Long userId = userApi.save(user);
user.setId(userId);
isNew = true;
//注册环信用户
String hxUser = "hx"+user.getId();
Boolean create = huanXinTemplate.createUser(hxUser, Constants.INIT_PASSWORD);
if(create) {
user.setHxUser(hxUser);
user.setHxPassword(Constants.INIT_PASSWORD);
userApi.update(user);
}
}
//6、通过JWT生成token(存入id和手机号码)
Map tokenMap = new HashMap();
tokenMap.put("id",user.getId());
tokenMap.put("mobile",phone);
String token = JwtUtils.getToken(tokenMap);
//7、构造返回值
Map retMap = new HashMap();
retMap.put("token",token);
retMap.put("isNew",isNew);
return retMap;
}
4.2. Query Huanxin user information
In the app, after logging in, the user needs to log in to Huanxin according to the username and password. Since the username and password are stored in the background, an interface needs to be provided to return.
4.2.1 API interface
API address: http://192.168.136.160:3000/project/19/interface/api/85
4.2.2 vo object
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class HuanXinUserVo {
private String username;
private String password;
}
4.2.3 Code implementation
Write HuanXinController implementation:
4.3. Huanxin user ID query user information
When chatting with friends, it is completely based on the ring letter server. For a better page effect, it is necessary to display the basic information of the user, which requires querying the user through the Huanxin user id.
MessagesController
@RestController
@RequestMapping("/messages")
public class MessagesController {
@Autowired
private MessagesService messagesService;
@GetMapping("/userinfo")
public ResponseEntity userinfo(String huanxinId) {
UserInfoVo vo = messagesService.findUserInfoByHuanxin(huanxinId);
return ResponseEntity.ok(vo);
}
}
MessagesService
@Service
public class MessagesService {
@DubboReference
private UserApi userApi;
@DubboReference
private UserInfoApi userInfoApi;
@DubboReference
private FriendApi friendApi;
@Autowired
private HuanXinTemplate huanXinTemplate;
/**
* 根据环信id查询用户详情
*/
public UserInfoVo findUserInfoByHuanxin(String huanxinId) {
//1、根据环信id查询用户
User user = userApi.findByHuanxin(huanxinId);
//2、根据用户id查询用户详情
UserInfo userInfo = userInfoApi.findById(user.getId());
UserInfoVo vo = new UserInfoVo();
BeanUtils.copyProperties(userInfo,vo); //copy同名同类型的属性
if(userInfo.getAge() != null) {
vo.setAge(userInfo.getAge().toString());
}
return vo;
}
}
4.4. Send a message to the client
At present, the docking of the user system has been completed. Next, we will test and send messages. The scene is as follows:
Click "Chat", and a stranger message will be sent to the other party, which is sent by the system.
We temporarily send through the console of Huanxin:
Message content:
{"userId":106,"huanXinId":"hx106","nickname":"Dark Horse Girl","strangerQuestion":"Do you like to see the blue sea or climb the majestic mountains?","reply": "I like fallen leaves in autumn, spring water in summer, and snow in winter, as long as I have you, everything is fine~"}
You can see that the message has been received.
4.5. Data processing
New registered users can already be synchronized to the Huanxin server, while old data needs to be manually processed by programmers.
Note: The use of test accounts supports up to 100 users
@Test
public void register() {
for (int i = 1; i < 106; i++) {
User user = userApi.findById(Long.valueOf(i));
if(user != null) {
Boolean create = template.createUser("hx" + user.getId(), Constants.INIT_PASSWORD);
if (create){
user.setHxUser("hx" + user.getId());
user.setHxPassword(Constants.INIT_PASSWORD);
userApi.update(user);
}
}
}
}
5. Contact management
System business covered:
-
View interested users, click "Chat", and view questions from strangers
-
Answer a question from a stranger and send a message to the interested user (a request to add a friend sent)
-
The other party gets a message (sent by the server)
-
-
The other party checks the message: If the two hit it off (click to chat, the two parties will be added as friends)
-
Record the friendship relationship into Tanhua's MongoDB database
-
Record friend relationship to Huanxin
-
-
After becoming a friend, you can view the friend list
-
Send a message to the target friend (not related to the server)
5.1. View user details
On the homepage, you can view the detailed information of interested persons. Click "Chat" to view the other party's questions
5.1.1, mock interface
Address: http://192.168.136.160:3000/project/19/interface/api/103
5.1.2、TanhuaController
/**
* 查看佳人详情
*/
@GetMapping("/{id}/personalInfo")
public ResponseEntity personalInfo(@PathVariable("id") Long userId) {
TodayBest best = tanhuaService.personalInfo(userId);
return ResponseEntity.ok(best);
}
5.1.3、TanhuaService
//查看佳人详情
public TodayBest personalInfo(Long userId) {
//1、根据用户id查询,用户详情
UserInfo userInfo = userInfoApi.findById(userId);
//2、根据操作人id和查看的用户id,查询两者的推荐数据
RecommendUser user = recommendUserApi.queryByUserId(userId,UserHolder.getUserId());
//3、构造返回值
return TodayBest.init(userInfo,user);
}
5.1.4, API interface and implementation class
RecommendUserApi
And RecommendUserApiImpl
write the method of querying the user's fate value
@Override
public RecommendUser queryByUserId(Long userId, Long toUserId) {
Criteria criteria = Criteria.where("toUserId").is(toUserId).and("userId").is(userId);
Query query = Query.query(criteria);
RecommendUser user = mongoTemplate.findOne(query, RecommendUser.class);
if(user == null) {
user = new RecommendUser();
user.setUserId(userId);
user.setToUserId(toUserId);
//构建缘分值
user.setScore(95d);
}
return user;
}
5.2. View Strangers' Messages
Click "Chat" to view the other party's questions
5.2.1, mock interface
Address: http://192.168.136.160:3000/project/19/interface/api/124
5.2.2、TanhuaController
/**
* 查看陌生人问题
*/
@GetMapping("/strangerQuestions")
public ResponseEntity strangerQuestions(Long userId) {
String questions = tanhuaService.strangerQuestions(userId);
return ResponseEntity.ok(questions);
}
5.2.3、TanhuaService
//查看陌生人问题
public String strangerQuestions(Long userId) {
Question question = questionApi.findByUserId(userId);
return question == null ? "你喜欢java编程吗?" : question.getTxt();
}
5.3. Reply to strangers' messages
need:
-
Send a stranger message to the target user through the server
5.3.1, mock interface
Address: http://192.168.136.160:3000/project/19/interface/api/106
5.3.2、TanhuaController
/**
* 回复陌生人问题
*/
@PostMapping("/strangerQuestions")
public ResponseEntity replyQuestions(@RequestBody Map map) {
//前端传递的userId:是Integer类型的
String obj = map.get("userId").toString();
Long userId = Long.valueOf(obj);
String reply = map.get("reply").toString();
tanhuaService.replyQuestions(userId,reply);
return ResponseEntity.ok(null);
}
5.3.3、TanhuaService
Create TanhuaService
and write methods to complete the function of replying to strangers' messages
{"userId":106,"huanXinId":"hx106","nickname":"Dark Horse Girl","strangerQuestion":"Do you like to see the blue sea or climb the majestic mountains?","reply": "I like fallen leaves in autumn, spring water in summer, and snow in winter, as long as I have you, everything is fine~"}
//回复陌生人问题
public void replyQuestions(Long userId, String reply) {
//1、构造消息数据
Long currentUserId = UserHolder.getUserId();
UserInfo userInfo = userInfoApi.findById(currentUserId);
Map map = new HashMap();
map.put("userId",currentUserId);
map.put("huanXinId", Constants.HX_USER_PREFIX+currentUserId);
map.put("nickname",userInfo.getNickname());
map.put("strangerQuestion",strangerQuestions(userId));
map.put("reply",reply);
String message = JSON.toJSONString(map);
//2、调用template对象,发送消息
Boolean aBoolean = template.sendMsg(Constants.HX_USER_PREFIX + userId, message);//1、接受方的环信id,2、消息
if(!aBoolean) {
throw new BusinessException(ErrorResult.error());
}
}
5.4. Add contacts
After the user obtains the stranger's message, click "chat", and it will become a contact (friend).
accomplish:
-
Write friends to MongoDB
-
Register friendship with Huanxin
5.4.1, mock interface
Address: http://192.168.136.160:3000/project/19/interface/api/205
5.4.2. Define MessagesController
/**
* 添加好友
*/
@PostMapping("/contacts")
public ResponseEntity contacts(@RequestBody Map map) {
Long friendId = Long.valueOf(map.get("userId").toString());
messagesService.contacts(friendId);
return ResponseEntity.ok(null);
}
5.4.3, write Service method
MessageService
Supplementary method of adding contacts
//添加好友关系
public void contacts(Long friendId) {
//1、将好友关系注册到环信
Boolean aBoolean = huanXinTemplate.addContact(Constants.HX_USER_PREFIX + UserHolder.getUserId(),
Constants.HX_USER_PREFIX + friendId);
if(!aBoolean) {
throw new BusinessException(ErrorResult.error());
}
//2、如果注册成功,记录好友关系到mongodb
friendApi.save(UserHolder.getUserId(),friendId);
}
5.4.4, dubbo service
Create FriendApi
and FriendApiImpl
write methods to add friends
FriendApiImpl
Implementation class
@Override
public void save(Long userId, Long friendId) {
//1、保存自己的好友数据
Query query1 = Query.query(Criteria.where("userId").is(userId).and("frinedId").is(friendId));
//1.1 判断好友关系是否存在
if(!mongoTemplate.exists(query1, Friend.class)) {
//1.2 如果不存在,保存
Friend friend1 = new Friend();
friend1.setUserId(userId);
friend1.setFriendId(friendId);
friend1.setCreated(System.currentTimeMillis());
mongoTemplate.save(friend1);
}
//2、保存好友的数据
Query query2 = Query.query(Criteria.where("userId").is(friendId).and("frinedId").is(userId));
//2.1 判断好友关系是否存在
if(!mongoTemplate.exists(query2, Friend.class)) {
//2.2 如果不存在,保存
Friend friend1 = new Friend();
friend1.setUserId(friendId);
friend1.setFriendId(userId);
friend1.setCreated(System.currentTimeMillis());
mongoTemplate.save(friend1);
}
}
5.4.5. Test
You can see that the friend has been added successfully.
5.5. Contact list
Contact list: Query friend list data in pages (tanhua-users: friend relationship table)
5.5.1, mock interface
Address: http://192.168.136.160:3000/project/19/interface/api/202
Response data structure:
5.5.2. Define ContactVo
package com.tanhua.domain.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ContactVo implements Serializable {
private Long id;
private String userId;
private String avatar;
private String nickname;
private String gender;
private Integer age;
private String city;
public static ContactVo init(UserInfo userInfo) {
ContactVo vo = new ContactVo();
if(userInfo != null) {
BeanUtils.copyProperties(userInfo,vo);
vo.setUserId("hx"+userInfo.getId().toString());
}
return vo;
}
}
5.5.3, write MessagesController
MessagesController
Add a query contact list method in
/**
* 分页查询联系人列表
*/
@GetMapping("/contacts")
public ResponseEntity contacts(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pagesize,
String keyword) {
PageResult pr = messagesService.findFriends(page,pagesize,keyword);
return ResponseEntity.ok(pr);
}
5.5.4, write Service
MessageService
Add a query contact method in
//分页查询联系人列表
public PageResult findFriends(Integer page, Integer pagesize, String keyword) {
//1、调用API查询当前用户的好友数据 -- List<Friend>
List<Friend> list = friendApi.findByUserId(UserHolder.getUserId(),page,pagesize);
if(CollUtil.isEmpty(list)) {
return new PageResult();
}
//2、提取数据列表中的好友id
List<Long> userIds = CollUtil.getFieldValues(list, "friendId", Long.class);
//3、调用UserInfoAPI查询好友的用户详情
UserInfo info = new UserInfo();
info.setNickname(keyword);
Map<Long, UserInfo> map = userInfoApi.findByIds(userIds, info);
//4、构造VO对象
List<ContactVo> vos = new ArrayList<>();
for (Friend friend : list) {
UserInfo userInfo = map.get(friend.getFriendId());
if(userInfo != null) {
ContactVo vo = ContactVo.init(userInfo);
vos.add(vo);
}
}
return new PageResult(page,pagesize,0l,vos);
}
5.5.5, dubbo service
Add pagination query method in FriendApi
andFriendApiImpl
@Override
public List<Friend> findByUserId(Long userId, Integer page, Integer pagesize) {
Criteria criteria = Criteria.where("userId").is(userId);
Query query = Query.query(criteria).skip((page - 1) * pagesize).limit(pagesize)
.with(Sort.by(Sort.Order.desc("created")));
return mongoTemplate.find(query,Friend.class);
}