Demand background

At present, the company is an innovative Internet Technology Co., LTD., developing B2C, B2B and O2O businesses in the automotive aftermarket.

In the early stage, we built a service platform linking 6 self-owned large auto professional maintenance centers, 12 self-owned auto parts and auto maintenance equipment sales centers, more than 3,000 auto repair and auto parts enterprises and more than 100,000 individual customers.

Customer retention and supplier consultation are all based on wechat group chat. Offer your services in hundreds of group chats every day. Need real time in the group product quotation, frame number recognition, image recognition, keyword feedback. Data push and other functions. In such demand situations, the human cost is huge.

And I as a front-end developer, usually also like to write technology blog and make friends, I have also created WeChat technology exchange group and WeChat public, usually I will under the article posted on the public, and my personal qr code, add WeChat to interested friends then I pull them into the group of these, but I keep friends agree WeChat validation, As a developer, this kind of repetitive work is absolutely intolerable. Based on this situation and the company’s business situation, we found out and learned about Wechaty, and found that its functions can cover both enterprise and individual wechat. And can customize the development of their own needs.

introduce

What is Wechaty

Wechat personal account is very powerful and flexible, which is a very suitable carrier for ChatBot. It can flexibly send voice messages, videos, pictures and text without restriction, and supports multi-group chat. However, to use a wechat personal account as a ChatBot requires access to wechat through an unofficial third-party library. As of the end of 2018, wechat did not have any official ChatBot API released.

Wechaty is an open source chat bot SDK that supports personal accounts and wechat. It is a Node.js application built in Typescript. Support a variety of wechat access solutions, including web, iPad, ios, Windows, Android, etc. Supports Linux, Windows, Darwin(OSX/Mac) and Docker.

On GitHub, you can find many third-party libraries that support Wechat personal account access, most of which are implemented based on Web Wechat API, such as WeixinBot based on Python, Wechaty based on Node.js, etc. Wechaty is one of the few open source libraries that support non-Web protocols, most of which are commercially proprietary closed source libraries.

With just 6 lines of code, you can build a wechat robot function using your personal account to automatically manage wechat messages.

import { Wechaty } from 'wechaty'

Wechaty.instance()
.on('scan'.qrcode= > console.log('Scan login:' + qrcode))
.on('login'.user= > console.log('Login successful:' + user))
.on('message'.message= > console.log('Received message:' + message))
.on('friendship'.friendship= > console.log('Received a friend request:' + friendship))
.on('room-invite'.invitation= > console.log('Received an invitation to join the group:' + invitation))
.start()
Copy the code

More features include

  • Message processing: Keyword reply
  • Group management: automatically join the group, pull people, kick people
  • Automatically process friend requests
  • Intelligent chat: After simple configuration, you can join the intelligent chat system to complete specified tasks
  • . Use your own imagination

Every interaction you can think of. It’s possible on wechat.

Pull the weather forecast at regular times every day.

Send good morning and good night messages to your loved ones every day.

What idiom solitaire. Quick questions, quick answers and so on

Wechaty’s features are not free

200/ month fee, if you are a personal development may be considered. But you can apply for a free token through the community for up to 15 days to try and use and develop a small robot to determine if you need to buy and use it.

About the application address I put here Wechaty Token application and use documentation and FAQs

Develop enterprise wechat robot based on wechaty-Puppet-Service

The directory structure

├─ config │ ├─ ├.js// Config file├ ─ ─ package. Json ├ ─ ─ service │ ├ ─ ─ bot - service │ │ ├ ─ ─ the error - service. Js │ │ ├ ─ ─ friendship - service. Js │ │ ├ ─ ─ │ ├── Login-service │ ├── │ ├─function-service.js│ │ │ └ ─ ─index.js│ │ ├ ─ ─logout-service.js│ │ ├ ─ ─message-service│ │ │ ├ ─ ─function-service.js│ │ │ └ ─ ─index.js│ │ ├ ─ ─ready-service│ │ │ ├ ─ ─function-service.js│ │ │ └ ─ ─index.js│ │ ├ ─ ─room-invite-service.js│ │ ├ ─ ─room-join-service.js│ │ ├ ─ ─room-leave-service.js│ │ ├ ─ ─room-topic-service.js│ │ └ ─ ─scan-service│ │ └ ─ ─index.js│ ├ ─ ─common-service│ │ ├ ─ ─chatbot-service.js│ │ ├ ─ ─ding-service.js│ │ └ ─ ─oss-service.js│ └ ─ ─redis-service│ └ ─ ─index.js├ ─ ─src│ └ ─ ─main.js/ / entrance ├ ─ ─store│ └ ─ ─index.js// Global Store Object ├─utils│ ├ ─ ─oss.js/ / ali cloudossCertification │ └ ─ ─redis.js // redis└─ Certified Loginyarn.lock
Copy the code

src/main.js

const { Wechaty } = require('wechaty')                                          // Robot puppet

const { onScan } = require(".. /service/bot-service/scan-service")               // This event is triggered when the robot needs to scan to log in.
const { onLogin } = require(".. /service/bot-service/login-service")             // When the robot successfully logs in, an event will be triggered and the information of the currently logged robot will be passed in the event
const { onLogout } = require(".. /service/bot-service/logout-service")           // When the robot detects the logout, it will trigger an event and pass the robot's information in the event.
const { onReady } = require(".. /service/bot-service/ready-service")             // This event is emitted when all data has been loaded. In wechaty-puppet-Padchat, this means that Contact and Room information has been loaded.
const { onMessage } = require(".. /service/bot-service/message-service")         // This event is triggered when the robot receives a message.
const { onRoomInvite } = require(".. /service/bot-service/room-invite-service")  // This event is triggered when a group invitation is received.
const { onRoomTopic } = require(".. /service/bot-service/room-topic-service")    // This event is triggered when someone changes the group name.
const { onRoomJoin } = require(".. /service/bot-service/room-join-service")      // This event is triggered when someone enters a wechat group. This event is triggered when a robot actively enters a wechat group.
const { onRoomleave } = require(".. /service/bot-service/room-leave-service")    // This is triggered when the bot removes a user from a group chat. Active user withdrawal is undetectable.
const { onFriendship } = require(".. /service/bot-service/friendship-service")   // This event is triggered when someone sends a friend request to the robot.
const { onHeartbeat } = require('.. /service/bot-service/heartbeat-service')     // Get the robot's heartbeat.
const { onError } = require('.. /service/bot-service/error-service')             // An error event is raised when something goes wrong inside the robot.


const { wechatyToken } = require('.. /config/index') // Robot token
const { globalData } = require('.. /store/index') // Global objects

globalData.bot = new Wechaty({
    puppet: 'wechaty-puppet-service'.puppetOptions: {
        token: wechatyToken
    }
});

globalData.bot
    .on('scan', onScan)
    .on('login', onLogin)
    .on('logout', onLogout)
    .on('ready', onReady)
    .on('message', onMessage)
    .on('room-invite', onRoomInvite)
    .on('room-topic', onRoomTopic)
    .on('room-join', onRoomJoin)
    .on('room-leave', onRoomleave)
    .on('friendship', onFriendship)
    .on('heartbeat', onHeartbeat)
    .on('error', onError)
    .start()
Copy the code

Specific function realization and code

  • Sweep the login code

After the Node is started, the onScan event is triggered, the login QR code is printed on the console, and the login is scanned

const QrcodeTerminal = require('qrcode-terminal');
const { ScanStatus } = require('wechaty-puppet')

/ * * *@method OnScan triggers this event when the robot needs to scan to log in. It is recommended that you install the qrcode-terminal package (run NPM install qrcode-terminal) so that you can see the qrcode directly on the command line. *@param {*} qrcode 
 * @param {*} status 
 */
const onScan = async (qrcode, status) => {
    try {
        if (status === ScanStatus.Waiting) {
            console.log('========================👉${status}👈 = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n `)
            QrcodeTerminal.generate(qrcode, {
                small: true}}})catch (error) {
        console.log('onScan', error)
    }

}

module.exports = { onScan }
Copy the code
  • Login successful

The onLogin event will be triggered after the successful code-sweep login. The login information and robot information will be received through the event, and the login notification will be sent to the nail group through the nail interface

const { notificationLoginInformation } = require('.. /.. /common-service/ding-service')
const { updateBotInfo } = require('./function-service')
const { globalData } = require('.. /.. /.. /store/index')

/ * * *@method OnLogin When the robot successfully logs in, it will trigger an event and pass the information of the currently logged robot * in the event@param {*} botInfo 
 */
const onLogin = async botInfo => {
    try {
        console.log('= = = = = = = = = = = = = = = = = = = = = = = = 👉 onLogin 👈 = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n')
        console.log('Robot Information:The ${JSON.stringify(botInfo)}\n\n`)
        console.log(` // \\ // \\ // ##DDDDDDDDDDDDDDDDDDDDDD## ## DDDDDDDDDDDDDDDDDDDD ## ## DDDDDDDDDDDDDDDDDDDD ## ## hh hh ## ## ## ## ## ## ## ## ## ### ## #### ## ## hh // \\ hh ## ## ## ## ## ## ## ## ## ## hh // \\ hh ## ## ## ## ## ## ## ## ## ## hh hh ## ## ## ## ## ## ## ## ## ## ## hh wwww hh ## ## ## ## ## ## ## ## #### ## hh hh ## ## ## ## ## ## ## ## ## ## ## ### ## ## ### ##MMMMMMMMMMMMMMMM ## ## MMMMMMMMMMMMMMMMMMMMMMMMMMMMMM ## wechat robot name: [${botInfo.payload.name}] The login is successful. \n\n `)
        // Globally store robot information
        globalData.botPayload = botInfo.payload
        // Update the list of robots
        updateBotInfo()
        // Robot login/logout notification pin interface
        notificationLoginInformation(true)}catch (error) {
        console.log(`onLogin: ${error}`)}}module.exports = { onLogin }
Copy the code

– Robot logout Abnormally The onLogout event will be triggered when the Node service is abnormal on the terminal or after the robot exits from the mobile phone to log in. The event also pins the notification information within the group and destroys some timers running in the program

const { notificationLoginInformation } = require('.. /common-service//ding-service')
const { globalData } = require('.. /.. /store/index')

/ * * *@method OnLogout When the robot detects that it has logged out, an event is triggered and information about the robot is passed during the event. *@param {*} botInfo 
 */
const onLogout = async botInfo => {
    try {
        console.log('= = = = = = = = = = = = = = = = = = = = = = = = 👉 onLogout 👈 = = = = = = = = = = = = = = = = = = = = = = = =')
        console.log('When the bot detects a logout, it will issue a logout with the contact of the currently logged in user. `)
        // Globally store robot information
        globalData.botPayload = botInfo.payload
        const { updateRoomInfoTimer, chatbotSayQueueTimer } = globalData
        // The robot exits the empty timer
        if (updateRoomInfoTimer) {
            clearTimeout(updateRoomInfoTimer)
        }
        if (chatbotSayQueueTimer) {
            clearInterval(chatbotSayQueueTimer)
        }
        // Robot login/logout notification pin interface
        notificationLoginInformation(false)}catch (error) {
        console.log(` error in onLogout:${error}`)}}module.exports = { onLogout }
Copy the code
  • Message reception processing

When wechat receives a new message, the onMessage event will be triggered, and different logical processing will be made based on the judgment of the message within the event, whether the message is within the group or private chat message. To achieve business requirements. Part of the code is as follows

const dayjs = require('dayjs');
const { say } = require('.. /.. /common-service/chatbot-service')
const {
    isCanSay,
    roomIdentifyVin,
    rooImageIdentifyVin,
    contactIdentifyVin,
    contactImageIdentifyVin,
    messageProcessing,
    storageRoomMessage,
    storageContactMessage,
} = require('./function-service')
const {
    roomMessageFeedback,
    contactMessageFeedback
} = require('.. /.. /common-service/ding-service')
const { globalData } = require('.. /.. /.. /store/index');
const { Message } = require('wechaty');

/ * * *@method OnMessage Triggers this event when the robot receives a message. *@param {*} message 
 */
const onMessage = async message => {
    try {
        console.log('= = = = = = = = = = = = = = = = = = = = = = = = 👉 onMessage 👈 = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n')
        // Robot info
        const { botPayload } = globalData
        // Get the contact who sent the message
        const contact = message.from()
        If the message is not in the wechat group, null is returned
        const room = message.room()
        // Check if the message was sent by a robot
        const isSelf = message.self()
        // Process the message content
        const text = await messageProcessing(message)
        / / the console. The log (` = = = = = = = = = = = = = = = = = = = = = = = = 👉 processing news content is: ${text} 👈 = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n `)
        // The message is empty and not processed
        if(! text)return

        // Message details
        const messagePayload = message.payload
        / / the console. The log (` = = = = = = = = = = = = = = = = = = = = = = = = 👉 message details: ${JSON. Stringify (messagePayload)} 👈 = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n `)
        // Message contact details
        const contactPayload = contact.payload
        / / the console. The log (` = = = = = = = = = = = = = = = = = = = = = = = = 👉 message contact details: ${JSON. Stringify (contactPayload)} 👈 = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n `)
        / / group of news
        if (room) {
            console.log(` = = = = = = = = = = = = = = = = = = = = = = = = 👈 👉 group chat message = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n `)
                // Do the corresponding logical processing
            // Private message
        } else {
            console.log(` = = = = = = = = = = = = = = = = = = = = = = = = 👉 private chat messages 👈 = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n `)
            console.log('News time:${dayjs(messagePayload.timestamp).format('YYYY-MM-DD HH:mm:ss')}\n\ N wechat name:${contactPayload.name}\n\n wechat type:${contactPayload.type}\n\n Remarks nickname:${contactPayload.alias}\n\ N Message content:${text}\n\n Message type:${messagePayload.type}\n\n`); }}catch (error) {
        console.log(` onMessage:${error}`)}}module.exports = { onMessage }
Copy the code

As for the other lifecycle and hook functions. You can refer to the documentation to make your own application robot

Encapsulated SAY method

This is because the say() method is called in multiple places and does different data processing depending on what message types are sending. You’ll see it in the future, so here’s a say method I’ve encapsulated for your reference

const { MiniProgram, UrlLink, FileBox } = require('wechaty')
const dayjs = require('dayjs');
const { DelayQueueExector } = require('rx-queue');
const { redisHexists, redisHset, redisHget, redisSet, redisLpush } = require('.. /redis-service/index')
const { globalData } = require('.. /.. /store/index')

const delay = new DelayQueueExector(10000);

/ * * *@method Say The robot sends a message@param {*} MessageType messageType 7 text, 1 file, 6 picture, 3 personal card, 14 card link 9 small program *@param {*} Sender source room | | personal * instance objects@param {*} * / messageInfo content
/** * messageInfo data structure * tetx: string text message mandatory * fileUrl: string file message mandatory * imageUr: string image message mandatory * cardId: String Personal business card message Mandatory * linkInfo: Object Card message mandatory * Description: String description * thumbnailUrl: String Thumbnail address * title: String title * URL: String jump address */

async function say({ messageType, sender, messageInfo }) {
    // console.log(messageType);
    // console.log(sender);
    // console.log(messageInfo);
    try {
        return new Promise(async (resolve, reject) => {
            // Robot info
            const { bot } = globalData
            // Enumerate message types
            const MessageType = {
                text: 7./ / text
                fromFile: 1./ / file
                fromUrl: 6./ / picture
                contactCard: 3.// Personal card
                urlLink: 14.// Card link
                miniProgram: 9./ / small programs

            }

            // The content does not exist
            if(! messageInfo) {return
            }
            // The content of the message to send
            let content


            switch (messageType) {
                7 / / text
                case MessageType.text:
                    content = messageInfo.text
                    break;
                / / file 1
                case MessageType.fromFile:
                    content = FileBox.fromFile(messageInfo.fromFile)
                    break;
                / / picture 6
                case MessageType.fromUrl:
                    content = FileBox.fromUrl(messageInfo.fromUrl)
                    break;
                // Personal card 3
                case MessageType.contactCard:
                    content = await bot.Contact.load('1688853777824721')
                    break;
                / / links to 14
                case MessageType.urlLink:
                    content = new UrlLink({
                        description: 'WeChat Bot SDK for Individual Account, Powered by TypeScript, Docker, and Love'.thumbnailUrl: 'https://avatars0.githubusercontent.com/u/25162437?s=200&v=4'.title: 'Welcome to Wechaty'.url: 'https://github.com/wechaty/wechaty',})break;
                // small program 9
                case MessageType.miniProgram:
                    content = new MiniProgram({
                        appid: 'wx60090841b63b6250'.title: 'I'm using Authing for identity, you should try it too'.pagePath: 'pages/home/home.html'.description: 'Identity Manager'.thumbUrl: '30590201000452305002010002041092541302033d0af802040b30feb602045df0c2c5042b777875706c6f61645f313735333533393532303440636 86174726f6f6d3131355f313537363035393538390204010400030201000400'.thumbKey: '42f8609e62817ae45cf7d8fefb532e83'});break;
                default:
                    break;
            }
            delay.execute(async () => {
                sender.say(content)
                    .then(value= > {
                        console.log(` = = = = = = = = = = = = = = = = = = = = = = = = 👉 robot reply 👈 = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n `)
                        resolve()
                    })
                    .catch(reason= > {
                        console.log(` = = = = = = = = = = = = = = = = = = = = = = = = 😢 robot sending messages failure 😢 = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n `, reason)
                    })
            })
        })
    } catch (error) {
        console.log('error in say', error); }}module.exports = {
    say
}
Copy the code

By the way, I also made a layer of encapsulation for the judgment of the message format in the onMessage event, here for your reference.

/ * * *@method MessageProcessing Processes message content *@param {*} message 
 */
async function messageProcessing(message) {
    try {
        return new Promise(async (resolve, reject) => {
            console.log(` = = = = = = = = = = = = = = = = = = = = = = = = 👉 messageProcessing 👈 = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n `)
            // Message details
            const messagePayload = message.payload
            // Get the text content of the message.
            let text = message.text()

            /** * Unknown: 0, Attachment: 1, Audio: 2, Contact: 3, ChatHistory: 4, Emoticon: 5, Image: 6, Text: 7, Location: 8, MiniProgram: 9, GroupNote: 10, Transfer: 11, RedEnvelope: 12, Recalled: 13, Url: 14, Video: 15 */
            // Message type
            switch (messagePayload.type) {
                0 / / accessories
                case Message.Type.Unknown:
                    console.log('========================👉 The message type is unknown:${messagePayload.type}👈 = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n `)
                    text = '[You have received an unknown message, please check it on your phone]'
                    break;
                / / the attachment 1
                case Message.Type.Attachment:
                    console.log('========================👉 The message type is attachment:${messagePayload.type}👈 = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n `)
                    // Do not know what to do
                    text = '[You received an attachment, please check it on your phone]'
                    break;
                Audio / / 2
                case Message.Type.Audio:
                    console.log('========================👉 Message type is audio:${messagePayload.type}👈 = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n `)
                    text = '[You have received a voice message, please view it on your phone]'
                    break;
                // Personal card 3
                case Message.Type.Contact:
                    console.log('========================👉 The message type is personal card:${messagePayload.type}👈 = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n `)
                    text = '[You have received a personal card, please check it on your phone]'
                    break;
                // Chat history 4
                case Message.Type.ChatHistory:
                    console.log('========================👉 The message type is chat history:${messagePayload.type}👈 = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n `)
                    text = '[You have received chat records, please view them on your mobile phone]'
                    break;
                // Emoji 5
                case Message.Type.Emoticon:
                    console.log('========================👉 Message type is emoji:${messagePayload.type}👈 = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n `)
                    text = '[You received emojis, please check them on your phone]'
                    // Do not know what to do
                    break;
                / / picture 6
                case Message.Type.Image:
                    console.log('========================👉 The message type is picture:${messagePayload.type}👈 = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n `)
                    // Upload the image to Aliyun to obtain the image address
                    text = await addImageOss(message)
                    break;
                7 / / text
                case Message.Type.Text:
                    console.log('========================👉 The message type is text:${messagePayload.type}👈 = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n `)
                    // go to space newline
                    text = text.replace(/\s+/g.' ')
                    break;
                / / position 8
                case Message.Type.Location:
                    console.log('========================👉 Message type is location:${messagePayload.type}👈 = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n `)
                    text = '[You received a picture message, please view it on your phone]'
                    break;
                // small program 9
                case Message.Type.MiniProgram:
                    console.log('========================👉 message type is small program:${messagePayload.type}👈 = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n `)
                    text = '[You received a small program message, please view it on your phone]'
                    break;
                // GroupNote 10
                case Message.Type.GroupNote:
                    console.log('========================👉 Message type: GroupNote:${messagePayload.type}👈 = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n `)
                    text = '[You received a GroupNote, please view it on your phone]'
                    break;
                // Transfer 11
                case Message.Type.Transfer:
                    console.log('========================👉 The message type is Transfer:${messagePayload.type}👈 = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n `)
                    text = '[You have received a Transfer, please check it on your phone]'
                    break;
                / / a red envelope 12
                case Message.Type.RedEnvelope:
                    console.log('========================👉 the message type is hongbao:${messagePayload.type}👈 = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n `)
                    text = '[You received a red envelope, please check it on your mobile phone]'
                    break;
                // Recalled 13
                case Message.Type.Recalled:
                    console.log('========================👉 sends a message at active:${messagePayload.type}👈 = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n `)
                    text = '[When you receive a Recalled message, check it on your phone]
                    break;
                // Link to address 14
                case Message.Type.Url:
                    console.log('========================👉 Message type is link address:${messagePayload.type}👈 = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n `)
                    // Do not know what to do
                    text = '[You received a link message, please view it on your phone]'
                    break;
                / / video 15
                case Message.Type.Video:
                    console.log('========================👉 Message type is video:${messagePayload.type}👈 = = = = = = = = = = = = = = = = = = = = = = = = \ n \ n `)
                    text = '[You received a video message, please view it on your phone]'
                    break;
                default:
                    text = ' '
                    break;
            }
            resolve(text)
        })
    } catch (error) {
        console.log('error in messageProcessing', error); }}Copy the code

The reason for this layer of encapsulation is that our business needs to store chat content in Redis and mysql data stores. Convenient data correction and query later use.

Implemented functions

What are some of the features we implemented with wechaty?

– Feedback messages based on keywords

  • Identify the image
  • Key words instruction binding group information. Configure groups according to different instructions.
  • Redis stores robot information. Store and synchronize group information in Redis and mysql. Background configuration corresponds to whether certain functions are enabled.
  • Timed message sending
  • The group invitation automatically passes. After joining the group, you can set the logical judgment function of data storage
  • Friend applications are automatically approved, keyword applications are automatically invited to different groups, feature coverage and so on
  • And so on.

The last

If you want to use my things, just pull down the code config.js to replace the token and some configuration information. Of course, I am constantly updating, with more and more functions, so the code in the warehouse will be a little different from the article. But because so much of the code involves incoming enterprise sensitive information. I had to rewrite a minimum executable demo for your reference only.

❤️ do me a favor

If you find this article inspiring, I’d like to ask you to do me a small favor:

  1. Like, so that more people can see this content (collection does not like, is a rogue -_-)

  2. Pay attention to the public account “Tomatology front-end”, I will regularly update and release front-end related information and project case experience for your reference.

  3. Adding a friend may not help you a lot, but there are some business issues that can be discussed and exchanged.