整理下目录 明天周六有时间写
开始
我司是做B端SaaS业务的,移动端比较有代表性的项目是办公微服务,下面涵盖了员工食堂、便利店、物业公司的入驻办公楼服务。既然有B端用户又有C端用户,那就避免不了两个端之间有交流,于是需要开发一款类似淘宝阿里旺旺功能的B2C的即时通讯客服系统。使用的websocket技术是基于workman开发的phpsocket.io。
业务场景分析
业务场景还是相对比较复杂的,因为我们公司的C端相当于是写字楼租户,有自己的后台,相当于说C端有前台、后台账号两个身份。B端也有前台、后台账号。排列组合,近B端与C端通信就有四种情况需要考虑。
第一版我们只考虑C端前台账号和B端所有的账号通信问题。不考虑C2C、B2B的情况。由于是客服系统,需要考虑到B端子账号所有有权限的账号都可被看做是客服,存在一个客服分配的情况。
数据表设计
数据表这块走了一些弯路,之前只用了一张表来存储消息记录。主流的C2C私聊的即时通讯一张表也许就够用了,但是这里分为B端和C端,且B端是店铺为单位的,后面的客服不是唯一的,一张表的话,逻辑不是很清晰。于是我对B端、C端的消息记录分开存放。
im_message_c2b
CREATE TABLE `im_message_c2b` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`uuid` char(36) CHARACTER SET utf8 NOT NULL DEFAULT '' COMMENT '全局唯一uuid',
`store_id` int(11) unsigned NOT NULL COMMENT '店铺id',
`store_name` varchar(100) CHARACTER SET utf8 NOT NULL DEFAULT '' COMMENT '店铺名称',
`type` smallint(3) unsigned NOT NULL DEFAULT '0' COMMENT '消息类型(0-系统消息 1-用户消息)',
`chat_id` int(11) DEFAULT NULL COMMENT '对话id',
`sender_from` tinyint(1) DEFAULT NULL COMMENT '发送人 1前台 2后台',
`sender_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '消息发送者ID(前台账号ID,若为系统消息此字段为0)',
`sender_name` varchar(100) CHARACTER SET utf8 NOT NULL DEFAULT '' COMMENT '消息发送者名称(仅用于前台展示,不做字段关联)',
`subject` varchar(255) NOT NULL DEFAULT '' COMMENT '消息标题(仅支持文字)',
`content_type` int(11) NOT NULL DEFAULT '1' COMMENT '消息类型 1文字 2图片 3文件',
`content` text COMMENT '消息内容(可支持文字、图片、表情、链接等)',
`receiver_from` tinyint(1) DEFAULT NULL COMMENT '接收人 1前台 2后台',
`receiver_id` int(11) unsigned DEFAULT NULL COMMENT '消息接收者ID(前台账号ID)',
`receiver_name` varchar(100) CHARACTER SET utf8 DEFAULT '' COMMENT '消息接收者名称(仅用于前台展示,不做字段关联)',
`disabled` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否删除(0-否 1-是)',
`add_time` int(10) unsigned DEFAULT NULL COMMENT '发送时间(unix时间戳)',
`state` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '状态 默认0未发送 1发送成功 2发送失败',
`is_read` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '默认0未读 1已读',
PRIMARY KEY (`id`) USING BTREE,
KEY ```receiver_id``` (`receiver_id`) USING BTREE,
KEY ```add_time``` (`add_time`) USING BTREE,
KEY ```mixkey``` (`sender_id`,`receiver_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=164 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='C端发给B端的消息';
im_message_b2c
CREATE TABLE `im_message_b2c` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`uuid` char(36) CHARACTER SET utf8 NOT NULL DEFAULT '' COMMENT '全局唯一uuid',
`store_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '店铺id',
`store_name` varchar(100) CHARACTER SET utf8 NOT NULL DEFAULT '' COMMENT '店铺名称',
`type` smallint(3) unsigned NOT NULL DEFAULT '0' COMMENT '消息类型(0-系统消息 1-用户消息)',
`chat_id` int(11) DEFAULT '0' COMMENT '对话id',
`sender_from` tinyint(1) DEFAULT '0' COMMENT '发送人 1前台 2后台',
`sender_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '消息发送者ID(前台账号ID,若为系统消息此字段为0)',
`sender_name` varchar(100) CHARACTER SET utf8 NOT NULL DEFAULT '' COMMENT '消息发送者名称(仅用于前台展示,不做字段关联)',
`subject` varchar(255) NOT NULL DEFAULT '' COMMENT '消息标题(仅支持文字)',
`content_type` int(11) NOT NULL DEFAULT '1' COMMENT '消息类型 1文字 2图片 3文件',
`content` text COMMENT '消息内容(可支持文字、图片、表情、链接等)',
`receiver_from` tinyint(1) DEFAULT '0' COMMENT '接收人 1前台 2后台',
`receiver_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '消息接收者ID(前台账号ID)',
`receiver_name` varchar(100) CHARACTER SET utf8 NOT NULL DEFAULT '' COMMENT '消息接收者名称(仅用于前台展示,不做字段关联)',
`disabled` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否删除(0-否 1-是)',
`add_time` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '发送时间(unix时间戳)',
`state` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '状态 默认0未发送 1发送成功 2发送失败',
`is_read` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '默认0未读 1已读',
PRIMARY KEY (`id`) USING BTREE,
KEY ```receiver_id``` (`receiver_id`) USING BTREE,
KEY ```add_time``` (`add_time`) USING BTREE,
KEY ```mixkey``` (`sender_id`,`receiver_id`) USING BTREE,
KEY ```type``` (`type`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=682754 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='B端发给C端的消息';
这里着重说明的一点是,为了让通信的uid逻辑性一致,receiver_id、sender_id都是取前台账号(后台账号与前台有对应关系),用receiver_from、sender_from区分是前台还是后台,uid用front、back前缀区分是前台还是后台。
另外还有记录会话id的表、聊天媒体文件的表,在这就不做记录了。
主要逻辑处理
-
聊天列表
-
对于C端聊天列表,肯定是group by store_id,按照店铺去分组。
-
按照一般的逻辑,首先列表页排序肯定是按最后一次发送消息的时间戳倒序去排序的。并且要显示最新一条与店铺对话的消息。
-
这里就很好玩了,因为mysql语句执行顺序group by是在order by前面的,所以兼顾这两个的用法是这里的重点。为实现上述逻辑,我的sql如下:
SELECT * from (
select * from (
select * from (SELECT store_id,type,store_name,receiver_id as chat_user_id,receiver_name as chat_user_name,content,add_time FROM im_message_c2b WHERE sender_id = {$data['user_id']} AND type = 1 order by add_time desc limit 10000) as a group by store_id
union all
select * from (SELECT store_id,type,store_name,sender_id,sender_name,content,add_time FROM im_message_b2c WHERE receiver_id = {$data['user_id']} order by add_time desc limit 10000) as b group by b.store_id
) c order by add_time desc limit 10000
) as d GROUP BY store_id ORDER BY add_time desc LIMIT $data[offset],$data[limit]
-
(为什么加limit,因为mysql5.7后就得加,不加group by还是按最原始的排序)
-
B端对话逻辑列表逻辑类似,不过是以发送人id分组的。
-
聊天记录
- C端聊天记录:以店铺id分组,查找出与当前店铺所有聊天记录。
- B端聊天记录:查看店铺与用户所有聊天记录(可能存在分配到其他客服,另一个客服要知道之前聊的内容)。
- 聊天记录的显示涉及到一个问题,就是消息间隔大于三分钟的话,要显示一个发送时间。我这边想到的方案是,两两比对消息的时间戳,如果大于3分钟,则插入一条时间戳记录,回头把所有的数据放在json里面返给前端。要注意的是,遍历数组同时又插入数组,很容易导致条件判断的错误。贴一下我写的算法。
// 两两比对,间隔大于两分钟插入时间显示
$i = 0;
//$messageLog['rows']是消息数组
foreach ($messageLog['rows'] as $k => $v) {
$count = count($messageLog['rows']);
if ($i < $count - 1) {
$head_time = $messageLog['rows'][$i]['add_time'];
$next_time = $messageLog['rows'][$i+1]['add_time'];
if ($head_time - $next_time >= 120) {
$arr = array(
['time' => $head_time,'mark' => 'middle']
);
// 如果间隔大于2min,插一条记录显示时间
array_splice($messageLog['rows'], $i+1, 0, $arr);
$i += 2; //插入记录了,跳过时间显示这条数据的比对,永远只比对原始的消息时间戳
} else {
$i ++;
}
}
}
-
标记为已读
- 当点开聊天页面的时候,肯定要标记双方的消息记录为已读。
- 当发送消息的时候,当前时间戳之前的双方消息,也要标记为已读。
聊天列表
聊天记录
标记为已读
分配客服
离线消息如何处理
群发消息-大数据量处理