【Travel and Make Friends】day06—Instant Messaging

Table of contents

1. Instant messaging

1.1. What is instant messaging? ​edit

1.2. Function description

1.3. Technical solution

2. Huanxin

2.1. Introduction to development

2.2. Huanxin Console

2.3. Interface description

3. Extract ring message components

3.1, write HuanXinTemplate

3.2, write Properties object

3.3. Configuration

3.4. Test

4. User system integration

4.1. Register Huanxin user

4.2. Query Huanxin user information

4.3. Huanxin user ID query user information

4.4. Send a message to the client

4.5. Data processing

5. Contact management

5.1. View user details

5.2. View Strangers' Messages

5.3. Reply to strangers' messages

5.4. Add contacts

5.5. Contact list

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:

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-autoconfigthe 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-serverapplication.ymlThe 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-serverthe 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

RecommendUserApiAnd RecommendUserApiImplwrite 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 TanhuaServiceand 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

MessageServiceSupplementary 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 FriendApiand FriendApiImplwrite methods to add friends

FriendApiImplImplementation 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

MessagesControllerAdd 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

MessageServiceAdd 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 FriendApiandFriendApiImpl

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

Guess you like

Origin blog.csdn.net/weixin_45481821/article/details/130043946