thinkPHP5+Angular的游戏客服会话

玩家与客服的在线聊天系统,可以同时接待多个玩家会话,后台直接走缓存,不走数据库。

最近捣鼓的一个东西,起先我这边要走3个方向,游戏前段=》PHP后台=》web后台管理页面,

后台是thinkPHP5.0做的,web后台是Angualr2.X 版本做的,采用排队方式

游戏前端还有web后台通过不断调用后台接口,间接实现轮询的方式,从后台返回的数据来显示给用户或者客服进行聊天会话

结合起来要注意几个方面的问题

第一个是玩家突然关闭页面,因为无法截取玩家或者客服是否关闭浏览器页面,所以要注意玩家掉线或者客服掉线情况

第二玩家可能开个浏览器进行多次排队的情况(用户想法预测不到);

第三个就是客服准备接待同步通知玩家接待。

这些功能说白了就是数组方面的操作。

先上后台代码


//客服
public function game_get($name)
{
    $msg = Cache::get('msg' . $name);
    $close = Cache::get('close_msg' . $name);
    $new_time = strtotime(date("Y-m-d H:i:s", time())); //获取最新时间戳
    $result = array();
    $service_past_time = Cache::get('service_past' . $name);//获取客服缓存时间
    $player_past_time = Cache::get('player_past' . $name);//获取玩家缓存时间
    if (Request::instance()->has('service_id')) {
        Cache::set('service_past' . $name, $new_time, 600); //600秒并不是固定
    }
    if (Request::instance()->has('player_id')) {
        Cache::set('player_past' . $name, $new_time, 600);
    }
    if ($close != false) {   //手动退出会话    
        $result['close'] = $close;
        $this->exit_reception($name);
    }
    if ($service_past_time + 10 < $new_time) {
        $this->exit_reception($name);
        $result['close'] = 4;//客服掉线
    }
    if ($player_past_time + 10 < $new_time) {
        $this->exit_reception($name);
        $result['close'] = 3;//用户掉线
    }
    $result['result'] = 1; //正常返回标识  
    $result['list'] = $msg; //数据
    echo json_encode($result);
}
//客服或者玩家输入消息接口
public function game_set($id, $msg, $name)
{
    $old_msg = Cache::get('msg' . $name);  
    $time = strtotime(date("Y-m-d H:i:s", time()));
    if ($old_msg == false) {   //缓存为空
        $k = array();
        Cache::set('msg' . $name, $k, 3600);
    }
    $old_msg = Cache::get('msg' . $name);  //重新拿值
    $packet = array('id' => $id, 'msg' => $msg, 'time' => $time + $id);
    array_push($old_msg, $packet);  //将数据塞入数组内
    $result = array();
    $result['result'] = 1;
    $result['list'] = $old_msg;
    Cache::set('msg' . $name, $old_msg, 3600);
    echo json_encode($result);
}

//排队
public function line_up($id, $name, $img_url)
{
    if ($id == null) { //解决容错
        return;
    }
    $time = date("H:i:s", time());
    $line_up = Cache::get('line_up');
    if ($line_up == false) {  如果当前无人排队则塞入空
        $k = array();
        Cache::set('line_up', $k);
    }
    $line_up = Cache::get('line_up');
    foreach ($line_up as $key => $value) { //重复排队问题
        if ($id == $value['id']) {
            unset($line_up[$key]);
            Cache::set('line_up', $line_up);
        }
    }
    $new_line_up = Cache::get('line_up');
    $packet = array('id' => $id, 'name' => $name, 'time' => $time, 'img_url' => $img_url);
    array_push($new_line_up, $packet);   //新的玩家排队,塞入队列中
    Cache::set('line_up', $new_line_up);
    $count = count($new_line_up);//排队人数

    $result = array();
    $result['count'] = $count;
    $result['list'] = $new_line_up;
    echo json_encode($result);
}

//后台排队通知接口,给web后台显示当前排队人数,并拿到玩家头像
public function user_line_up($token)
{
    if (!$this->auth($token))
        return;
    $line_up = Cache::get('line_up');
    $new_line_up = array_values($line_up);
    $img_url = SystemConfigModel::get('user_icon');
    $count = count($line_up);
    $result = array();
    $result['count'] = $count;
    $result['list'] = $new_line_up;
    $result['url'] = $img_url['value'];
    echo json_encode($result);
}

//全部标记为已读,清空排队人数
public function read_line_up($token)
{
    if (!$this->auth($token)) {
        return;
    }
    Cache::rm('line_up');
}

//退出排队,玩家关闭客服页面时,或者正在接待玩家,清除玩家排队状态
public function out_line_up($id)
{
    $line_up = Cache::get('line_up');
    foreach ($line_up as $key => $value) {
        if ($id == $value['id']) {
            unset($line_up[$key]);
            Cache::set('line_up', $line_up);
        }
    }
}
//退出会话,清空玩家信息的缓存
public function exit_reception($name)
{
    Cache::rm('msg' . $name);
    Cache::rm('player_past') . $name;
    Cache::rm('service_past') . $name;
}
//游戏当前排队人数 ,游戏前端轮询接口,如果返回reception,表示玩家可以进入会话页面与客服会话
public function line_up_people($id)
{
    $line_up = Cache::get('line_up');
    $count = array();
    $reception = Cache::get('reception' . $id);
    if ($reception != false) {
        $count['reception'] = $reception;
    }
    $count['count'] = count($line_up);
    echo json_encode($count);
}



public function close_msg($id, $name) //客服关闭会话
{   //退出会话消息
    Cache::rm('msg' . $name);
    Cache::rm('reception' . $id);
    Cache::set('close_msg' . $name, 1, 3); //退出会话缓存
    $this->out_line_up($id);
}

//客服接待
public function reception($token)
{
    if (!$this->auth($token))
        return;
    $line_up = Cache::get('line_up');
    if ($line_up == false) {            //无人排队
        $result = array();
        $result['result'] = 0;
        echo json_encode($result);
    } else {
        $result = array();
        $result['list'] = array_shift($line_up);
        $result['count'] = count($line_up);//总数
         echo json_encode($result);
        Cache::set('line_up', $line_up);
    }
}

//通知玩家接待
public function now_reception($player_id, $name, $token)  //写入双方过期时间,如果不重新获取消息便清空缓存告知双方,玩家/客服掉线
{
    if (!$this->auth($token))
        return;
    Cache::set('reception' . $player_id, 1, 5);
    $time = strtotime(date("Y-m-d H:i:s", time()));
    Cache::set('player_past' . $name, $time, 100);
    Cache::set('service_past' . $name, $time, 100);
    $this->out_line_up($player_id);
}
以上后台接口

接下来就是web后台Angular的操作了。

建立一个客服service.comoponent 客服服务chat.service,界面需要慢慢秀才会好看,我写这个没有什么设计图,只是自己想搞成什么样就是怎么样的

import {Component, OnDestroy} from '@angular/core';
import {Http} from '@angular/http';
import {GlobalState} from '../../../global.state';
import {ChatData} from "./chat_data.component";


@Component({
    selector: 'reception-player',
    template: `        
            <div class="col-md-12" style="height: 400px;position: absolute;margin: 0 auto">
                <div class="col-md-12"  style="background: white;border-top-left-radius: 2em;border-top-right-radius:2em;">
                    <hr/>
                    <span style="font-size: 19px;color: black">当前排队人数:{{count}}
                    <button class="btn btn-success" (click)="reception()">接待</button>
                    </span>
                    <button class="badge badge-secondary float-right pull-right btn btn-success" (click)="chat_min()">最小化</button>
                </div>
                <div class="col-md-4" style="height: 400px;float: left;overflow:hidden;background: white;border-bottom-left-radius: 2em;">
                    <div class="list-group" style="margin-top: 3px;" *ngFor="let item of reception_arr">
                        <div class="list-group-item hread_hover" (click)="user_chat(item.id,item.name)"
                             [ngClass]="{'bk': item.id==this.player_id}">
                            <div class="img-area">
                                <img src="{{'http://'+user_icon+item.img_url}}" style="width: 45px;height: 45px;"/>
                            </div>
                            <a style="margin-left: 30px;color: black">{{item.name}}</a>
                            <a class="fa fa-close" style="position: absolute;right:5px;font-size: 15px;"
                               [ngClass]="{'fa-spin':item.id==this.player_id}" (click)="close_msg(item.id,item.name)">
                            </a>
                        </div>
                    </div>
                </div>
                <div class="col-md-8" style="background: #ebf0f5;height: 400px;float: right;border-bottom-right-radius: 2em;">
                    <div style="height: 300px;overflow-y: scroll;margin-top: 15px" id="chat_height">
                        <div class="list-group " *ngFor="let items of messages">
                            <button type="button" class="list-group-item chat_msg_left hread_hover"
                                    *ngIf="items.id==player_id">
                                <!--<img src="{{'http://'+user_icon+'avater_man_1.png'}}" style="width: 35px;height: 35px;"/>-->
                                <audio *ngIf="this.audio_music==true" id="myAudio" autoplay>
                                    <source src="../../../../assets/music/yx.mp3" type="audio/mp3"/>
                                </audio>
                                {{items.msg}}
                            </button>
                            <button type="button" class="list-group-item chat_msg_right hread_hover"
                                    *ngIf="items.id==service_id">
                                {{items.msg}}
                            </button>
                        </div>
                    </div>
                    <hr/>
                    <div class="input-group">
                        <input type="text" class="form-control" [(ngModel)]="set_msg"
                               aria-describedby="inputWarning2Status" style="color: black"/>
                        <span class="input-group-btn">
                    <button class="btn btn-success btn-lg" type="button" (click)="set()">发送</button>
                    </span>
                    </div>
                </div>
                    <ngb-alert class="col-md-12" *ngIf="user_close==true" [dismissible]="false">{{tip_msg}}</ngb-alert>
            </div>
    `,
    styles: [`        
        .bk {
            background: silver;
        }

        .hread_hover:hover {
            background: salmon;
        }

        .chat_msg_left {
            position: relative;
            left: 0px;
            margin-top: 12px;
            word-wrap: break-word;
            width: 50%;
            border-radius: 10px;
            background: yellow;
            border-radius: 10px;
        }

        .chat_msg_right {
            position: relative;
            right: -49%;
            margin-top: 12px;
            width: 50%;
            border-radius: 10px;
            word-wrap: break-word;
            background: deepskyblue;
            border-radius: 10px;
        }

        .img-area {
            float: left;
            width: 36px;
        img {
            width: 36px;
            height: 36px;
        }
        img > div {
            width: 36px;
            height: 36px;
            border-radius: 4px;
            font-size: 24px;
            text-align: center;
        }
        }
    `]
})
export class ReceptionPlayerComponent implements OnDestroy {
    public get_timer;
    public height_bottom;
    public audio_music: boolean = false;
    public end_time: number = 0;
    public messages: Array<any> = new Array();
    public count;
    public user_icon: any;
    public img_url: any;
    public url = new Array();
    public reception_arr = new Array();
    public set_msg;
    public player_id;
    public service_id;
    public tip_msg;
    public user_close: boolean;
    public player_name = "";
    public timer: Array<any> = new Array();
    public userInfo: Array<any> = new Array();
    public msgs: Array<any> = new Array();

    constructor(private http: Http, private state: GlobalState,public chat_date:ChatData) {
        this.get_timer = setInterval(() => {
            this.ngAfterViewInit();
        }, 800);
        this.http.get(this.state.host + "admin/system/admin_info" + '/token/' + this.state.token)
            .toPromise().then(data => {
            var result = data.json();
            this.service_id = result.id;
        });
    }
    ngAfterViewInit() {
        this.http.get(this.state.host + "admin/system/user_line_up/token/" + this.state.token)
            .toPromise().then(data => {
            var packet = data.json();
            this.count = packet.count;
            this.user_icon = packet.url;
            this.chat_date.wait_player=packet.list;
            this.chat_date.icon=this.user_icon;
            this.chat_date.count=this.count;
        });
        if (this.player_name != "") {
            this.messages = this.msgs[this.player_name];
        }
    }
    chat_min(){
        if(this.state.chats==false){
            this.state.chats=true;
        }else{
            this.state.chats=false;
        }
    }
    user_chat(id: any, name: any) {
        this.player_id = id;

        this.player_name = name;
        this.messages = this.msgs[this.player_name];
        this.height_bottom = document.getElementById('chat_height').scrollHeight;
        document.getElementById('chat_height').scrollTo(0, this.height_bottom + 30);
    }

    player_type(id, name) {
        var time = setInterval(() => {
            this.http.get(this.state.host + "admin/system/game_get" + '/service_id/' + this.service_id + '/name/' + name)
                .toPromise().then(data => {
                var packet = data.json();
                if (packet.list == false) {
                    if (packet.close == 3) {
                        this.user_close = true;
                        this.tip_msg = name + "掉线";
                        setTimeout(() => {
                            this.user_close = false;
                        }, 2000);
                        this.exit_reception(id, name);
                    }
                    return;
                }
                if (packet.close == 1) {
                    this.user_close = true;
                    this.tip_msg = name + "退出会话";
                    setTimeout(() => {
                        this.user_close = false;
                    }, 2000);
                    this.exit_reception(this.player_id, name);
                }
                else if (packet.close == 3) {
                    this.user_close = true;
                    this.tip_msg = name + "掉线";
                    setTimeout(() => {
                        this.user_close = false;
                    }, 2000)
                    this.exit_reception(this.player_id, name);

                }
                else {
                    this.msgs[name] = packet.list;
                    var len = this.msgs[name].length;
                    if (this.msgs[name] != "" && this.msgs[name].length != 0) {
                        if (this.msgs[name][len - 1].id == id) {
                            if (this.end_time != this.msgs[name][len - 1].time) {
                                this.end_time = this.msgs[name][len - 1].time;
                                this.audio_music = true;
                            } else {
                                this.audio_music = false;
                            }
                        }
                    }
                }

            });
        }, 1000);
        this.timer.push(time);
        this.user_chat(this.player_id, name);
    }
    
    //接待
    reception() {
        if (this.reception_arr.length > 5) {
            this.tip_msg = "客服最多同时接待5个玩家";
            this.user_close = true;
            setTimeout(() => {
                this.user_close = false;
            }, 5000);
        }
        else {
            this.http.get(this.state.host + "admin/system/reception" + '/token/' + this.state.token)
                .toPromise().then(data => {
                var result = data.json();
                if (result.result == 0) {
                    this.tip_msg = "当前无人请求客服";
                    this.user_close = true;
                    setTimeout(() => {
                        this.user_close = false;
                    }, 5000);
                }
                else {
                    setTimeout(() => {
                        this.player_id = result.list.id;
                        this.player_name = result.list.name;
                        this.reception_arr.push(result.list);
                        this.userInfo = this.reception_arr;
                        //通知玩家
                        this.http.get(this.state.host + "admin/system/now_reception" + '/player_id/' + this.player_id + '/name/' + this.player_name + '/token/' + this.state.token)
                            .toPromise().then(data => {
                            this.player_type(this.player_id, this.player_name);
                        });
                    }, 500);
                }
            });
        }
    }
    // private getChatData(id: number): number {
    //     for (let i = 0; i < this.userInfo.length; i++) {
    //         if (this.userInfo[i].id == id) {
    //             return i;
    //         }
    //     }
    // }
    //输入消息
    set() {
        var url: string = this.state.host + "admin/system/game_set/?";
        url = url + 'id=' + this.service_id;
        url = url + '&msg=' + this.set_msg;
        url = url + '&name=' + this.player_name;
        this.http.get(url)
            .toPromise().then(data => {
            // setTimeout(() => {
            this.height_bottom = document.getElementById('chat_height').scrollHeight;
            document.getElementById('chat_height').scrollTo(0, this.height_bottom + 30);
            this.user_chat(this.player_id, this.player_name);
        });
    }
    exit_reception(id, name) {
        this.msgs[name] = [''];
        for (var i = 0; i < this.reception_arr.length; i++) {
            if (this.reception_arr[i].id == id) {
                clearInterval(this.timer[i]);
                this.timer.splice(i, 1);
                this.reception_arr.splice(i, 1);
            }
        }

    }

    close_msg(id, name) {
        this.exit_reception(id, name);
        var url: string = this.state.host + "admin/system/close_msg/name/" + this.player_name + '/id/' + this.player_id;
        this.http.get(url)
            .toPromise().then(data => {
            this.user_chat(id, name);
            this.tip_msg = '关闭玩家' + name + "会话";
            this.user_close = true;
            setTimeout(() => {
                this.user_close = false;
            }, 2000);
        });
    }

    ngOnDestroy() {
        clearInterval(this.get_timer);

    }
}
服务:

import { Injectable} from "@angular/core";
import {Http} from "@angular/http";
import {GlobalState} from "../../../global.state";
@Injectable()
export class ChatData
{
    public timer:Array<any> = new Array();
    public wait_player:Array<any>=new Array();
    public icon:any;
    public count:number;
    constructor(private http:Http,private state:GlobalState){


    }

}
其实更多就是客服把后台请求接口还有一系列的逻辑操作放在服务,这样会显得更加有层次感,现在服务仅仅存放数据用,供给其他页面调用,或者成为其他页面的viewchild

目录一小部分









评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值