使用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% ...
随机推荐
- vim编辑操作
vim 插入模式 a 光标后 A 行尾 o 光标所在行下一行 O 光标所在行上一行 i 光标前 ...
- linux运维学习
export 和unset 设置和取消变量 echo 的双引号和单引号的区别:双引号里的会被替换,单引号里的都会直接输出.
- linux_NFS
NFS是什么? 网络文件系统,又叫共享存储,通过网络连接让不同主机之间实现共享存储. 应用于存放图片.附件.视频等用户上传文件 相关同类应用:大型网站nfs有压力,使用moosefs(mfs),Ghu ...
- flask_route错误:AttributeError: 'function' object has no attribute 'route'
问题: 路由完全正确,当只有一个名为home的函数处理这个路由时候,下一个路由处理函数,总是提示没有这个rotue属性 Traceback (most recent call last): File ...
- iOS UImage 与 RGB 裸数据的相互转换
iOS UImage 与 RGB 裸数据的相互转换 Touch the data of image in iOS Get data from a image 较简单,根据已有的 image 的属性,创 ...
- JavaScript 教程:对象
JavaScript 对象是拥有属性和方法的数据.学过编程语言的都知道,此处不再详述! 1.对象的定义: <script> </script> 对象也可以先创建,再添加属性和属 ...
- HTML5 拖放(Drag 和 Drop)详解与实例
简介 拖放是一种常见的特性,即抓取对象以后拖到另一个位置. 在 HTML5 中,拖放是标准的一部分,任何元素都能够拖放. 先点击一个小例子:在用户开始拖动 <p> 元素时执行 JavaSc ...
- 交换两个变量的值,不借助第三个变量的 三种方法(JS实现)
第一种:算术运算法 var a = 10; var b = 12; a = b - a; b = b - a; a = b + a; 它的原理是:把a.b看做数轴上的点,围绕两点间的距离来进行计算.具 ...
- 123 A. Prime Permutation
链接 http://codeforces.com/contest/123/problem/A 题目 You are given a string s, consisting of small Lati ...
- python 将验证码保存到本地 读取 写入
#验证码 #读取验证码网址.打开本地路径.写入.输入验证码 downpicture = urllib.request.urlopen(SecretCodeUrl).read() local = ope ...