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
(目录一小部分)