基于laravel+workerman+easywechat的公众号客服系统

1、流程:用户在公众号发送文本消息,通过公众号消息模板推送通知给客服,客服点击通知,跳转小程序与用户对话。用户可以直接在公众号内与客服对话。

2、所用技术栈:                 

"php": "^7.3|^8.0",
"laravel/framework": "^8.54",
"overtrue/laravel-wechat": "^6.0",
"overtrue/wechat": "~5.0",
"workerman/gateway-worker": "^3.0",
"workerman/workerman": "^4.0",

3、首先截获用户在公众号发送的消息(workerman搭建不再赘述,详情可查看官方文档)

WechatController.php

    public function wechetCheck()
    {
        $app = Factory::officialAccount($this->config);
        $app->server->push(function ($message) use ($app) {
            switch ($message['MsgType']) {
                case 'event' :
                    {
                        // do something
                    }
                    break;
                case 'text':
                {
                    //检查用户是否咨询过,咨询过则用原message_id
                    $openId = $message['FromUserName'];
                    $LogServerMessage = new LogServerMessage();
                    $checkMessage = $LogServerMessage->getIdByOpenId($openId);
                    if (!$checkMessage) {
                        $messageId = $LogServerMessage->newMessage($openId)->id;
                    } else {
                        $messageId = $checkMessage->id;
                    }
                    $LogServer = new LogServer();
                    $serverRes = $LogServer->addLog($messageId, $openId, 0, $message['Content']);
                    // 发送消息模板,按自己的实际情况编写
                    // do something
                    }
                    //由于我的服务器同时运行了多个workerman,需要指定registerAddress发送消息
                    Gateway::$registerAddress = '127.0.0.1:1237';
                    Gateway::sendToGroup("server_{$messageId}", json_encode([
                        'type' => 'message',
                        'content' => $message['Content'],
                        'id' => $serverRes->id
                    ]));
                    return "消息提示";
                }
            }
        });
        return $app->server->serve();
    }

    //发送消息给用户,配置路由为 /server-message-list
    public function sendServerMsgToUser(Request $request)
    {
        $messageId = $request->input('message_id');
        $content = $request->input('content');
        $toUserOpenId = $request->input('to_user_open_id');
        $fromUserOpenId = $request->input('from_user_open_id');
        $app = Factory::officialAccount($this->config);
        $app->customer_service->message($content)->to($toUserOpenId)->send();
        $LogServer = new LogServer();
        $server = $LogServer->addLog($messageId, $fromUserOpenId, 1, $content);
        $data['status'] = 'success';
        $data['content'] = $server->id;
        return $data;
    }

 LogServerMessage.php(表结构很简单,就只保存open_id)

    public function newMessage($openId)
    {
        return $this->create([
            'open_id' => $openId
        ]);
    }

    public function getIdByOpenId($openId)
    {
        return $this->where('open_id', $openId)->first(['id']);
    }

 LogServer.php(记录消息详情)

    /***
    *$userType 0:用户;1:客服;
    ***/
    public function addLog($messageId, $openId, $userType, $content)
    {
        return $this->create([
            'message_id' => $messageId,
            'open_id' => $openId,
            'user_type' => $userType,
            'content' => $content
        ]);
    }

    public function getListByMessageId($messageId)
    {
        return $this->where('message_id', $messageId)->orderBy('id', 'asc')->get(['id', 'user_type', 'content', 'open_id']);
    }

4、前端连接websocket,我这里使用的是uniapp

<template>
    <view class="h-100 w-100 p-1">
        <scroll-view class="content-wrap bg p-1" :scroll-y="true"  :scroll-into-view="scrollViewItem">
            <view v-for="(item, index) in list" :key="index" class="w-90p"
                  :class="[parseInt(item.user_type) === 0 ? '' : 'msg-server-flex']">
                <view class="msg border-round border p-05 m-b-05" :id="`msg_${item.id}`"
                      :class="[parseInt(item.user_type) === 0 ? 'msg-customer' : 'msg-server']">
                <span>
                    {{ item.content }}
                </span>
                </view>
            </view>
        </scroll-view>
        <view class="flex flex-center flex-col footer w-90p">
            <u-input :border="true" style="width: 100%" type="textarea" v-model="content"/>
            <u-button :custom-style="longPop" size="mini" type="primary-color" :hair-line="false" @click="sendMessage">
                发送
            </u-button>
        </view>
    </view>
</template>

<script>
export default {
    name: "index",
    data() {
        return {
            messageId: '',
            list: [],
            user: this.$common.getUserInfo(),
            content: '',
            longPop: {
                width: '100%',
                margin: '0.5rem auto'
            },
            toUserOpenId: '',
            fromUserOpenId: '',
            scrollViewItem: ''
        }
    },
    methods: {
        getMessage() {
            const data = {
                uid: this.user.uid,
                token: this.user.token,
                message_id: this.messageId
            }
            this.$ajax.post(`${this.$url}/server-message-list`, data).then(res => {
                this.list = []
                if (res.data.status === 'success') {
                    this.list = res.data.content.list
                    this.toUserOpenId = res.data.content.open_id
                    this.fromUserOpenId = res.data.content.server_open_id
                    this.scrollToBottom(this.list[this.list.length - 1].id)
                }
            })
        },
        sendMessage() {
            const data = {
                uid: this.user.uid,
                token: this.user.token,
                content: this.content,
                to_user_open_id: this.toUserOpenId,
                from_user_open_id: this.fromUserOpenId,
                message_id: this.messageId
            }
            this.$ajax.post(`${this.$url}/send-server-msg-to-user`, data).then(res => {
                this.list.push({
                    content: this.content,
                    user_type: 1,
                    id: res.data.content
                })
                this.content = ''
                this.scrollToBottom(res.data.content)
            })
        },
        scrollToBottom(id){
            this.scrollViewItem = `msg_${id}`
        }
    },
    onl oad(option) {
        this.messageId = option.message_id
        this.getMessage()
        uni.connectSocket({
            url: `wss://your-wesocket?message_id=${this.messageId}`,
            success: res => console.log('success:', res),
            error: res => console.log('error:', res),
            complete: res => console.log('complete:', res),
        })
        const _this = this
        uni.onSocketMessage(function (res) {
            const data = JSON.parse(res.data)
            if (data.type === 'message') {
                _this.list.push({
                    content: data.content,
                    user_type: 0,
                    id: data.id
                })
                _this.scrollToBottom(data.id)
            }
        });
    }
}
</script>


<style scoped lang="scss">
@import "src/static/style/common.scss";

.bg {
    background-color: #fcfcfc;
}

.p-1 {
    padding: 25rpx;
}


.h-100 {
    height: 100vh;
}

.w-100 {
    width: 100vw;
}


.w-90p {
    width: 90%;
}


.p-05 {
    padding: 15rpx;
}


.m-b-05 {
    margin-bottom: 15rpx;
}

.border-round {
    border-radius: 5px;
}

.border{
    border: 1px solid #efefef;
}


.flex {
    display: flex;
}

.flex-center {
    justify-content: center;
    align-items: center;
}

.flex-col {
    flex-direction: column;
}

.content-wrap {
    height: calc(100vh - 300rpx);
    overflow-y: auto;
}

.footer {
    position: fixed;
    left: 5%;
    bottom: 30rpx;
    background-color: white;
}

.msg {
    max-width: 45%;
    display: inline-block;
}

.msg-customer {
    background-color: white;
}

.msg-server {
    background-color: $green;
    color: white;
}

.msg-server-flex {
    display: flex;
    justify-content: flex-end;
}
</style>

上一篇:消息中间件系列教程(15) -RabbitMQ-基于全局消息ID解决幂等性问题


下一篇:【开发者portal在线开发插件系列五】命令的响应,即命令结果的上报(mid的使用)