使用Websocket框架之GatewayWorker开发电商平台买家与卖家实时通讯
前段时间公司提了一个新的需求,在商品的详情页要实现站内买家和商品卖家实时通讯的功能以方便沟通促成交易,要开发此功能当时首先考虑到的就是swoole和workerman了,从网上大概了解了一下关于这两款工具的阐述,功能都是相当强大的,考虑到项目的进度问题,还是选择上手容易比较快的GatewayWorker和框架TP5。
先看一下我们前端设计高大上的模板,分别是用户和卖家后台。 功能还是比较全的,几乎模仿的是QQ。
业务上的大概需求是,用户在进入某个商品详情页下,给用户提供一个和卖家沟通的接口,根据商品的ID找到对应的卖家,类似于淘宝,还有发送图片,发送对应的商品链接;商户后台也差不多。
我们的平台上有虚拟商品和实体商品两大分类,当时也考虑到了消息的读取状态。我的表最初设计如下,没有加任何的索引,考虑的或许也不够周全,有见地的前辈还望指点一二!
DROP TABLE IF EXISTS `hp_chat_log`;
CREATE TABLE `hp_chat_log` (
`id` int() unsigned NOT NULL AUTO_INCREMENT COMMENT '聊天记录表主键id',
`user_id` int() unsigned NOT NULL DEFAULT '' COMMENT '用户id',
`merchant_id` varchar() COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '商家id',
`send_message` text COLLATE utf8_unicode_ci NOT NULL,
`send_message_type` tinyint() NOT NULL DEFAULT '' COMMENT '发送消息类型(1:普通文本;2:商品链接,3:用户发送图片)',
`sender` tinyint() NOT NULL DEFAULT '' COMMENT '发送方。1:用户。2:商家',
`send_time` int() NOT NULL DEFAULT '' COMMENT '发送时间',
`read_status` tinyint() unsigned NOT NULL DEFAULT '' COMMENT '是否已读。0:未读取。1:已读取',
`acc_isonline` tinyint() NOT NULL DEFAULT '' COMMENT '接收方是否在线 (0:不在线;1:在线)',
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT= DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
模板有了,表设计好了,接下来就是搭建服务了,当前项目开发的框架用的是TP5,选择的Websocket框架是GatewayWorker框架,关于GatewayWorker与TP5的整合方法可以看我的这篇文章,讲到了在Linux和
Windows下的整合安装。
http://www.cnblogs.com/wt645631686/p/7219519.html
整合好了之后需要根据当前服务器的一些端口配置在修改一些默认的配置,因为需要客户端通过指定的端口建立连接。
TP5整合好了之后Gateway和workerman的主体目录结构都在TP5的框架目录vendor下的workerman目录下。需要修改里面gateway目录下的一些文件的端口及IP地址配置。
配置完成之后,进入项目目录,按照workerman官方手册提供的使用方法,用命令php start.php start启动socket服务,如以下截图,分别是1238和8282端口。当然可以在后台运行,详细的使用方法请参考手册。
启动好了之后那么就需要在客户端开始下手了,我们项目里是在前端页面里用建立的链接。看前端代码
当前的所有代码并不是最终的,目前只是阶段性开发,后期在项目中逐步完善。
var ws;
// 连接服务端
function connect() {
// 创建websocket
ws = new WebSocket("ws://"+document.domain+":8282"); //这里如果使用127.0.0.1或者localhost会出现连接失败。当时为了方便以后的维护,这里在php的全局文件里定义了一个常量来定义ip,后来本地开发完提交到linux服务器环境之后发现链接失败!按照此行代码会有效连接~
console.log(ws);
ws.onopen = onopen;
ws.onmessage = onmessage;
ws.onclose = function(e) {
console.log(e);
console.log("连接关闭,定时重连");
connect();
};
ws.onerror = function(e) {
console.log(e);
console.log("出现错误");
};
}
// 握手
function onopen()
{
var joint = '{"type":"handshake","role":"user"}';
ws.send(joint);
}
// 服务端发来消息时
function onmessage(e)
{
var data = JSON.parse(e.data);
console.log(data);
switch(data['type']){
// 服务端ping客户端
case 'ping':
ws.send('{"type":"pong"}');
break;
// 登录 更新用户列表
case 'handshake':
bindUid(data.client_id);
$('#client_id').val(data.client_id);
break;
// 提醒
case 'reception':
//{"type":"say","from_client_id":xxx,"to_client_id":"all/client_id","content":"xxx","time":"xxx"}
warn(data['content'], data['time'], data['timestamp']);
break;
}
} //绑定uid
function bindUid (client_id) {
var bindUrl = "{:url('push/push/BindUserClientId')}";
$.post(bindUrl, {client_id: client_id}, function(data){
console.log(data);
}, 'json');
} //发送连接
function sendLink () {
sendTrigger('link');
}
// 发送信息
function sendMessage (){
sendTrigger('message');
}
function sendTrigger(sendType) {
var toMid = $('#toMid').val();
var pid = $('#pid').val();
var message = $("footer .send_content").val();
var client_id = $('#client_id').val();
var sendUrl = "{:url('push/push/SendMessageToMerchant')}";
$.ajax({
url:sendUrl,
type:'POST',
data:{message:message,toMid:toMid,pid:pid,client_id:client_id,sendType:sendType},
async:false,
dataType:'JSON',
success:function(data){
data = JSON.parse(data);
if (data.status < ) {
alert('发送失败,请稍后再试!');
} else {
$('#send_timestamp').val(data.timeStamp);
$('#send_timestr').val(data.timeStr);
if (sendType == 'link') {
$('#main').append(data.html);
}
}
}
})
} // 提醒
function warn(content, time, prevTmestamp){
var V_image = $('#V_image').val();
var str = '<div class="chat-receiver">' + timestampWarn(prevTmestamp, time) + '<div class="chat-avatar"><img src="'+ V_image+ '" alt=""></div>
<div class="chat-content"><div class="chat-triangle"></div><span>' + content + '</span></div>';
domChange(str);
$("#main").scrollTop($("#main")[].scrollHeight); } //发送
function sender(content, time, prevTmestamp) {
var user_image = $('#user_image').val();
var str = '<div class="chat-sender">' + timestampWarn(prevTmestamp, time) + '<div class="chat-avatar"><img src="' +user_image + '" alt="">
</div><div class="chat-content"><div class="chat-triangle"></div>' +
'<span>' + content + '</span></div></div>';
domChange(str);
} //消息时间控制
function timestampWarn (nowTimestamp , nowTime) {
var prevTimestamp = $('#prev_timestamp').val();
$('#prev_timestamp').val(nowTimestamp);
var timeOffset = ;
var accTime = '';
if ((nowTimestamp - prevTimestamp) > timeOffset) {
accTime = '<div style="clear:both;"></div><p class="chat-history-date">' + nowTime + '</p>';
}
return accTime;
}
<body onload="connect();">
</body>
在开发过程中,修改的GatewayWorker文件并不多,除了几个主要的端口及IP需要修改之外,仅仅修改一个重要的文件就够了,那就是Push模块(项目的通讯模块)同级的Events.php文件。看一下项目需求里
修改后的代码,一个方法;
/**
* 当客户端发来消息时触发
* @param int $client_id 连接id
* @param mixed $message 具体消息
*/
public static function onMessage($client_id, $message)
{
$message_data = json_decode($message,true);
if (!$message_data) {
return ;
}
switch($message_data['type']) {
case 'pong':
return;
case 'handshake':
$new_message = [
'type' => $message_data['type'],
'client_id' => $client_id,
'time' => date('H:i:s')
];
Gateway::sendToClient($client_id, json_encode($new_message));
return;
case 'send':
if (!isset($message_data['toClientUid'])) {
throw new \Exception("toClient not set. client_ip:{$_SERVER['REMOTE_ADDR']}");
}
$toUid = $message_data['toClientUid'];
$message = $message_data['content'];
$new_message = [
'type' => 'reception',
'content' => $message,
'time' => date('H:i:s'),
'timestamp' => time(),
'c_type' => $message_data['c_type'],
'primary' => $message_data['Db_id']
];
//发送者角色
$source_info = explode('_', $message_data['source']);
if ($source_info[] == 'U') {
//为了安全,特意做了加密
$new_message['source'] = encrypt_hopeband($source_info[], 'E', 'XXXXXXX');
} return Gateway::sendToUid($toUid, json_encode($new_message));
}
}
然后看一下Push模块下的控制器文件,在配合前端在绑定客户端ID及发送信息做的一些处理。
class Push extends Base{ protected static $user_headimage = '';
protected static $uid = null; public function __construct () {
parent::__construct();
$this->checkUserLogin();
//用户头像昵称等信息
self::$uid = session('userinfo.uid');
$user_info = Hmodel\User::getUserChatinfoById(self::$uid);
self::$user_headimage = json_decode($user_info['headimgurl'],true)[]; } public function chatAction () {
$product_id = intval(input('param.pid', , 'int'));
$toMid = Hmodel\Product::getMidByProductid($product_id);
if ($toMid === false) notFund(); $productHtml = $this->returnProductData2Html($product_id, 'default'); $int_toMid = substr($toMid, );
$V_headInfo = Pmodel\Push::getVmerchantHeadImageByVid($int_toMid);
$V_headimage = is_not_empty_array($V_headInfo) ? json_decode($V_headInfo['headimgurl'])[] :'/uploads/logo.png'; if (substr($toMid, , ) == 'V') {
$chatLogData = Pmodel\Push::getChatlogByUseridAndVid(self::$uid, $int_toMid);
if (is_not_empty_array($chatLogData)) {
$chatLog = self::chatlogData2Html($chatLogData, $V_headimage);
}
} elseif (substr($toMid, , ) == 'E') { } $view = new View;
$view->assign('pHtml', $productHtml);
$view->assign('toMid', $toMid);
$view->assign('pid', $product_id);
$view->assign('chatlogHtml', $chatLog);
$view->assign('role', 'user');
$view->assign('user_image', self::$user_headimage);
$view->assign('V_image', $V_headimage);
return $view->fetch();
} private static function chatlogData2Html ($data = [], $V_headimage = '') {
$todayTimestamp = strtotime(date('Y-m-d'));
$html = '';
foreach ($data as $k => $v) {
$date = $v['send_time'] < $todayTimestamp ? date('Y/m/d H:i:s', $v['send_time']) : date('H:i:s', $v['send_time']);
$time_nodes = '';
if (($data[$k]['send_time'] - $data[$k-]['send_time']) > ) {
$time_nodes = '<div style="clear:both;"></div><p class="chat-history-date">' .$date.'</p>';
}
//sender->发送方 1:用户。2:商家
if ($v['sender'] == ) { //send_message_type->发送消息类型 (1:普通文本;2:商品链接)
if ($v['send_message_type'] == ) {
$html.= '<div class="chat-sender">';
$html.= $time_nodes;
$html.= '<div class="chat-avatar"><img src="' . self::$user_headimage . '" alt=""></div>';
$html.= '<div class="chat-content"><div class="chat-triangle"></div><span>' . $v['send_message'].'</span></div>';
$html.= '</div>';
}elseif ($v['send_message_type'] == ) {
$html.= $time_nodes;
$product_info = json_decode($v['send_message'],true);
$html.= self::productData2SendHtml($product_info);
}elseif ($v['send_message_type'] == ) {
$images_arr = json_decode($v['send_message'],true); $html.= '<div class="chat-sender">';
$html.= $time_nodes;
$html.= '<div class="chat-avatar"><img src="' . self::$user_headimage . '" alt=""></div>';
$html.= '<div class="chat-content"><div class="chat-triangle"></div>';
foreach ($images_arr as $v ) {
$html.= '<img src="' .WEB_SITE. '/' . $v . '" style="max-width:85%">';
}
$html.= '</div>';
$html.= '</div>';
}
}else {
if ($v['send_message_type'] == ) {
$html.= '<div class="chat-receiver">';
$html.= '<div class="chat-avatar"><img src="' . $V_headimage . '" alt=""></div>';
$html.= '<div class="chat-content"><div class="chat-triangle"></div><span>' . $v['send_message'].'</span></div>';
$html.= '</div>';
}elseif ($v['send_message_type'] == ) { }
}
}
return $html;
}
public function BindUserClientIdAction () {
if (!Request::instance()->isPost()) { notFund(); }
$bindUserid = 'U_' . session('userinfo.uid');
$client_id = input("param.client_id", , "string");
// 设置GatewayWorker服务的Register服务ip和端口
Gateway::$registerAddress = SOCKET_SERVER_PORT;
// client_id与uid绑定
// Gateway::closeClient($client_id);
return Gateway::bindUid($client_id, $bindUserid);
}
//用户发送消息给商家
public function SendMessageToMerchantAction () {
if (!Request::instance()->isPost()) { notFund(); }
$message = $_POST['message'];
$toMid = input('post.toMid', '' , 'string');
$product_id= input('post.pid', , 'int');
$client_id = input('post.client_id', '', 'string');
$sendType = input('post.sendType', '', 'string'); if (!in_array($sendType,['link', 'message'])) { //客户端错误
return json_encode(['status' => -]);
}
if (strlen($client_id) != ) { //客户端错误
return json_encode(['status' => -]);
}
if (!is_not_empty_string($toMid) || !is_positive_integer($product_id)) { //系统错误
return json_encode(['status' => -]);
}
$db_toMid = Hmodel\Product::getMidByProductid($product_id); //数据错误
if ($db_toMid != $toMid) {
return json_encode(['status' => -]);
}
require_once dirname(dirname(__FILE__)) . '/Events.php'; $uid = session('userinfo.uid');
$accIsOnline = Gateway::isUidOnline($toMid) == ? : ; //判读商家是否在线
$message_type = ;
if ($sendType == 'link') {
$message_type = ;
$productData = $this->referProductData($product_id);
unset($productData['product_price']);
unset($productData['score']);
unset($productData['product_stock']);
unset($productData['product_param']);
unset($productData['product_desc']);
unset($productData['product_main']);
unset($productData['category_id']);
unset($productData['merchant_id']);
$message = json_encode($productData);
}
//Log入库
$insertId = Pmodel\Push::addChatLog($uid, $toMid, $message, $message_type, , $accIsOnline);
if($message_type == ){
if(!is_numeric($message)){
$message = '"'.$message.'"';
}
if ($message == '') {
$message = '';
}
}
if ($insertId === false) { //入库失败(服务器故障)
return json_encode(['status' => -]);
}
$Worker = new \Events;
$message_json = '{"type":"send","source":"U_' . $uid . '","toClientUid":"' . $toMid . '","content":' . $message .',
"c_type": ' . $message_type .', "Db_id":' . $insertId . '}'; $Worker::onMessage($client_id, $message_json);
//成功返回相关数据
return json_encode([
'status' => ,
'timeStamp' => time(),
'timeStr' => date('H:i:s'),
'html' => $message_type == ? '' : self::productData2SendHtml($productData)
]); }
//商家发送信息给用户
public function sendMessageToUserAction () {
if (!Request::instance()->isPost()) { notFund(); }
$post_message = is_not_empty_string($_POST['message']) ? $_POST['message'] : '';
$toUserCode = input('post.toUserCode', '' , 'string');
$toU_uid = encrypt_hopeband($toUserCode, 'D', 'xxxxx');
$V_client_id = input('post.client_id', '', 'string');
$V_uid_code = input('post.myCode', '', 'string');
$V_uid = encrypt_hopeband($V_uid_code, 'D', 'xxxxx');
$make_message = [];
$message = '';
self::trimImageAndTextinfo2str($post_message, $make_message); if (is_not_empty_array($make_message)) {
foreach ( $make_message as &$v ) {
$message .= self::checkIflegalAndReturn($v);
} } if (strlen($V_client_id) != || Gateway::isOnline($V_client_id) != ) { //客户端错误
return json_encode(['status' => -]);
}
$V_merchantInfo = Pmodel\Push::getVmerchantInfoByVid($V_uid);
if (!is_not_empty_array($V_merchantInfo)) { //商家信息不存在
return json_encode(['status' => -]);
}
require_once dirname(dirname(__FILE__)) . '/Events.php'; $accIsOnline = Gateway::isUidOnline('U_' . $toU_uid) == ? : ; //判读用户是否在线
$message_type = ;
//Log入库
$insertId = Pmodel\Push::addChatLog($toU_uid, 'V_' . $V_uid, $message, , , $accIsOnline);
if ($insertId === false) { //入库失败(服务器故障)
return json_encode(['status' => -]);
}
$Worker = new \Events;
$img_encrypt_code = encrypt_hopeband('Hp_(legal)', 'E', 'Hp_HopeBand_Chat_img');
$message = str_replace($img_encrypt_code .' src="', $img_encrypt_code . " src='", $message);
$message = str_replace('">', "'>", $message);
$message_json = '{"type":"send","toClientUid":"U_' . $toU_uid . '","content":"' . $message .'","Db_id": "' . $insertId . '"}';
$Worker::onMessage($client_id, $message_json);
//成功返回相关数据
return json_encode([
'status' => ,
'timeStamp' => time(),
'timeStr' => date('H:i:s'),
]); }
额外还有一些关于消息处理方面的;
//验证是否是否是图片,如果是并且返回图片地址,否则返回字符串
private static function checkIflegalAndReturn ( $message = '') {
header('content-type:text/html; charset=utf-8');
$img_encrypt_code = encrypt_hopeband('Hp_(legal)', 'E', 'Hp_HopeBand_Chat_img');
if (substr($message, , ) != '<img src="data:image/' || substr($message, strpos($message, ';', ) ,) != ';base64,' ||
substr($message, -, ) != '">') {
return $message;
}
$preg = '/<img.*?src="(.*?)".*?>/is';
preg_match( $preg, $message,$arr);
$img_src = self::base64_upload($arr[]);
return '<img '.$img_encrypt_code.' src="' . $img_src . '">';
} //把接受到的消息文本和图片有序提出并解析
private static function trimImageAndTextinfo2str ($message = '', &$message_arr = []) {
if (!is_not_empty_string($message)) return '';
$img_start_code = '<img src="data:image/';
$img_end_code = '">';
$tmp_message = strlen($message);
$initial = substr($message,,strlen($img_start_code)); if ($initial == $img_start_code) {
$start = strpos($message, $img_start_code, );
$end = strpos($message, $img_end_code , );
$message_arr[] = substr($message, , $end + );
$message = substr($message, $end + );
}else{
$start = strpos($message, $img_start_code);
if ($start !== false) {
$message_arr[] = substr($message, , $start);
$message = substr($message, $start);
}else{
//防止xss攻击
$message_arr[]= string_remove_xss(htmlspecialchars_decode($message));
}
}
if (($tmp_message) != strlen($message) && is_not_empty_string($message)) {
self::trimImageAndTextinfo2str($message, $message_arr);
}
return $message_arr; } private static function base64_upload($base64 = '') {
$base64_image = str_replace(' ', '+', $base64);
if (preg_match('/^(data:\s*image\/(\w+);base64,)/', $base64_image, $result)){
if($result[] == 'jpeg'){
$image_name = getCode(,).'.jpg';
}else{
$image_name = getCode(,).'.'.$result[];
}
$image_file = "./upload/chat".'/'.date('Y').'/'.date('m').'/'.date('d').'/'.$image_name; //服务器文件存储路径
//判断文件路径是否存在
$path = "./upload/chat".'/'.date('Y').'/'.date('m').'/'.date('d').'/';
is_dir($path) or mkdir($path,,true);
if (file_put_contents($image_file, base64_decode(str_replace($result[], '', $base64_image)))){
return $image_file;
}else{
return false;
}
}else{
return false;
}
}
微信的图片上传
JS部分
/* 退款选择图片 -------- start */ $("#chooseImage").click(function()
{ wx.chooseImage(
{
count: , // 默认9
sizeType: ['original','compressed'], // 可以指定是原图还是压缩图,默认二者都有
sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
success: function (res)
{
images.localId = res.localIds; // 返回选定照片的本地ID列表,localId可以作为img标签的src属性显示图片
//for(var j = ;j<images.localId.length;j++)
// {
// var str = '<div serverId='+images.serverId[j]+' class="chat-sender"><div class="chat-avatar">
//<img src="/home/push/img/1.png" alt="">
//</div><div class="chat-content"><div class="chat-triangle"></div>
//<img src='+images.localId[j]+' data-originalDrawing-src='+images.localId[j]+' data-preview-src="" data-preview-group="1"/></div></div>'
// $("#main").append(str); // }
var t = ;
var i = , length = images.localId.length;
images.serverId = []; /* upload 方法 -------- start */
function upload()
{
wx.uploadImage(
{
localId: images.localId[i],
success: function (res)
{
i++; images.serverId.push(res.serverId); if (i < length) { upload(); } var str = '<div serverId='+res.serverId+' class="chat-sender"><div class="chat-avatar">
<img src="/home/push/img/1.png" alt="">
</div><div class="chat-content"><div class="chat-triangle"></div>
<img src='+images.localId[i-]+' data-originalDrawing-src='+images.localId[i-]+' data-preview-src="" data-preview-group="1"/>
</div></div>'
$("#main").append(str); if(i >= length ) uploadImageToDb(images.serverId); },
fail: function (res){ }
}); }
/* upload 方法 -------- end */ upload(); }
})
}); function uploadImageToDb(images){
var str = "";
var upUrl = "http://xxxxxx.com/push/push/uploadImgage";
var toMid = $('#toMid').val();
var client_id = $('#client_id').val();
$.post(upUrl,{images:images,toMid:toMid,client_id:client_id},function(data){
if(data == ){
for(var n = ; n < $(".chat-sender").length ; n++){
str = $(".chat-sender").eq(n).attr("serverId")+",";
for(var z=;z<data.length;z++){
if(data[z] == str){
$(".chat-sender").eq(n).find(".chat-content").append('<div class="chat-sender">上传失败</div>');
}
}
} } })
}
/* 退款选择图片 -------- end */
后台部分
//微信上传图片
public function uploadImgageAction () {
if (!Request::instance()->isPost()) { notFund(); }
$images = $_POST['images'];
if (empty($images)) die;
$toMid = input('post.toMid', '' , 'string');
$client_id = input('post.client_id', '', 'string');
if (strlen($client_id) != ) { //客户端错误
return json_encode(['status' => -]);
}
if (!is_not_empty_string($toMid)) { //系统错误
return json_encode(['status' => -]);
} require_once dirname(dirname(__FILE__)) . '/Events.php'; $accIsOnline = Gateway::isUidOnline($toMid) == ? : ; //判读商家是否在线
$message_type = ;
//微信上传图片处理Start
$res = json_decode(file_get_contents("access_token.json"));
foreach ($res as $key => $value) {
if($key == 'access_token'){
$access_token = $value;
}
}
$data = [];
foreach ($images as $k => $v) {
$str = date('YmdHis').rand(,).'.jpg';
$targetName = './upload/chat/'.$str;
if (!file_exists("./upload/chat/")) {
mkdir("./upload/chat/", , true);
}
$ch = curl_init("http://file.api.weixin.qq.com/cgi-bin/media/get?access_token=".$access_token."&media_id=".$v);
$fp = fopen($targetName, 'wb');
curl_setopt($ch, CURLOPT_FILE, $fp);
curl_setopt($ch, CURLOPT_HEADER, );
$msg["status"] = curl_exec($ch);
$msg["filename"] = $str;
curl_close($ch);
fclose($fp);
$data[] = $targetName;
}
//微信上传图片处理End
if (!is_not_empty_array($data)) { //微信服务器端图片上传错误
return json_encode(['status' => -]);
}
$message = json_encode($data);
//Log入库
$insertId = Pmodel\Push::addChatLog(self::$uid, $toMid, $message, $message_type, , $accIsOnline);
if ($insertId === false) { //入库失败(服务器故障)
return json_encode(['status' => -]);
}
$Worker = new \Events;
$message_json = '{"type":"send","source":"U_' . self::$uid . '","toClientUid":"' . $toMid . '","content":' . $message .',
"c_type": ' . $message_type .', "Db_id":' . $insertId . '}';
$Worker::onMessage($client_id, $message_json);
//成功返回相关数据
return json_encode([
'status' => ,
'timeStamp' => time(),
'timeStr' => date('H:i:s')
]); }
其他一些不是很重要的代码就不拿出来了。
当前项目只是一个简单的需求,并没有把GatewayWorker很多强大的功能体现出来,大家以后在项目开发中遇到更为复杂的需求,参考官方手册提供的一些Demo就可以慢慢实现并开发出更为健壮的项目!
使用Websocket框架之GatewayWorker开发电商平台买家与卖家实时通讯的更多相关文章
- 基于TP5使用Websocket框架之GatewayWorker开发电商平台买家与卖家实时通讯
https://www.cnblogs.com/wt645631686/p/7366924.html 前段时间公司提了一个新的需求,在商品的详情页要实现站内买家和商品卖家实时通讯的功能以方便沟通促成交 ...
- PHP基于TP5使用Websocket框架之GatewayWorker开发电商平台买家与卖家实时通讯
前段时间公司提了一个新的需求,在商品的详情页要实现站内买家和商品卖家实时通讯的功能以方便沟通促成交易,要开发此功能当时首先考虑到的就是swoole和workerman了,从网上大概了解了一下关于这两款 ...
- 开发电商平台用PHP语言和JAVA语言有什么区别?哪种语言更好?
现在很多行业都通过电子商务拓展业务,所以商城系统开发成为很多企业的刚性需求.一般有一点技术基础的客户应该知道目前商城系统开发主流语言有两个,PHP和Java.那么很多客户朋友会纠结是选择哪个语言开发好 ...
- 【基于微信小程序的社区电商平台】需求分析心得——小豆芽
一.项目内容 基于微信小程序,做一个社区电商平台,抓住社区电商的特点,做出特色,与微信集成,实现商品的个性化发布,以及个性化营销. 个性化发布:用户可以在应用上直接发布自己的商品,通过搜索心愿单可以查 ...
- Android通用框架设计与完整电商APP开发系列文章
作者|傅猿猿 责编|Javen205 有福利 有福利 有福利 鸣谢 感谢@傅猿猿 邀请写此系列文章 Android通用框架设计与完整电商APP开发 课程介绍 [导学视频] [课程详细介绍] 以下是部分 ...
- 以太坊开发DApp实战教程——用区块链、星际文件系统(IPFS)、Node.js和MongoDB来构建电商平台(一)
第一节 简介 欢迎和我们一起来用以太坊开发构建一个去中心化电商DApp!我们将用区块链.星际文件系统(IPFS).Node.js和MongoDB来构建电商平台类似淘宝的在线电商应用,卖家可以自由地出售 ...
- Thinkphp5.0 仿百度糯米 开发多商家 电商平台(完整版)
目录第1章 课程简介第2章 需求分析第3章 快速掌握thinkphp5第4章 任性的TP5模块第5章 生活服务分类管理模块第6章 百度地图应用封装第7章 打造属于TP5自己的发送邮件服务第8章 商户模 ...
- Java生鲜电商平台-SpringCloud微服务开发中的数据架构设计实战精讲
Java生鲜电商平台-SpringCloud微服务开发中的数据架构设计实战精讲 Java生鲜电商平台: 微服务是当前非常流行的技术框架,通过服务的小型化.原子化以及分布式架构的弹性伸缩和高可用性, ...
- 从电商平台促销活动看电商app开发趋势
据亿合科技小编了解到:尽管各大电商平台都进入了品质和品牌时代,但对于消费者来说,低价依然是一个有吸引力的因素.尼尔森<网络购物者趋势研究>报告显示,2016年价格敏感型购物者的比例从15% ...
随机推荐
- [转]js 正则表达式
一.正则表达式中包括的元素 1.原子(普通字符:a-z A-Z 0-9 .原子表. 转义字符) 2.元字符 (有特殊功能的字符) 3.模式修正符 (系统内置部分字符 i .m.S.U…) 二.正则表达 ...
- java常用类--系统相关
java提供了System类和Runtime类来与程序的运行平台进行交互. System类 System类代表java程序的运行平台,程序不能创建这个类的对象,System类提供了一些类field和方 ...
- MySQL中union和order by一起使用的方法
MySQL中union和order by是可以一起使用的,但是在使用中需要注意一些小问题,下面通过例子来说明.首先看下面的t1表. 1.如果直接用如下sql语句是会报错:Incorrect usage ...
- ClearCase config_spec
1.使用分支前要在vob创建branch type,Config_Spec不能自动创建branch type: 2.如果可能,最好在以前确定的label上进行新的工作,避免维护复杂的config_s ...
- 4个强大的Linux服务器监控工具[转]
本文介绍了一些可以用来监控网络使用情况的Linux命令行工具.这些工具可以监控通过网络接口传输的数据,并测量目前哪些数据所传输的速度.入站流量和出站流量分开来显示. 一些命令可以显示单个进程所使用的带 ...
- Centos7 动态创建文件系统
linux 想要动态扩展文件系统,需要将磁盘做成LVM动态卷 以centos 7为例 挂载两块磁盘 vdb vdc 安装 ssm 管理磁盘工具 yum -y install syste ...
- python3中,os.path模块下常用的用法总结
abspath basename dirname exists getatime getctime getmtime getsize isabs isdir isfile islink ismount ...
- 51NOD 1220 约数之和 [杜教筛]
1220 约数之和 题意:求\(\sum_{i=1}^n \sum_{j=1}^n \sigma_1(ij)\) \[ \sigma_0(ij) = \sum_{x\mid i}\sum_{y\mi ...
- 剑指offer得意之作——顺时针打印矩阵
题目: 输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3 ...
- 小甲鱼OD学习第12讲
这次我们的任务是破解这个需要特定的注册码的软件,如下图 我们从字符串入手,输入register,搜索 我们点击 查找下一个,看看有什么有用的字符串,如下图 然后,在下方,我们发现了 Regis ...