feat: 后端全量更新 - 含所有本次需求
- Contract.php: 返回合约账户余额(balance_contract) - My.php: 地址管理增加BTC/ETH - AppContract.php: 一键平仓(closeall) - AppProxy.php: 代理专属注册链接 + 分级权限(L1/L2) - site.php: 手续费减半(0.018→0.009) - agent_permission_setup.sql: 代理权限SQL - crypto_news_crawler.py: 新闻自动采集脚本
This commit is contained in:
Executable
+1
@@ -0,0 +1 @@
|
||||
{"license":"regular","licenseto":"16556","licensekey":"kJhq0UoWeubY7wTp iQ2rtRBqZB2gL3NSwCgQLg==","menus":["btpanel","btpanel\/index","btpanel\/index\/index","btpanel\/index\/add","btpanel\/index\/edit","btpanel\/index\/del","btpanel\/index\/multi","btpanel\/crontab","btpanel\/crontab\/index","btpanel\/crontab\/add","btpanel\/crontab\/edit","btpanel\/crontab\/del","btpanel\/crontab\/multi","btpanel\/logs","btpanel\/logs\/index","btpanel\/logs\/add","btpanel\/logs\/edit","btpanel\/logs\/del","btpanel\/logs\/multi","btpanel\/monitor","btpanel\/monitor\/index","btpanel\/monitor\/add","btpanel\/monitor\/edit","btpanel\/monitor\/del","btpanel\/monitor\/multi"],"files":["application\\admin\\controller\\btpanel\\Ajax.php","application\\admin\\controller\\btpanel\\Crontab.php","application\\admin\\controller\\btpanel\\Index.php","application\\admin\\controller\\btpanel\\Logs.php","application\\admin\\controller\\btpanel\\Monitor.php","application\\admin\\view\\btpanel\\crontab\\add.html","application\\admin\\view\\btpanel\\crontab\\edit.html","application\\admin\\view\\btpanel\\crontab\\index.html","application\\admin\\view\\btpanel\\index\\index.html","application\\admin\\view\\btpanel\\logs\\index.html","application\\admin\\view\\btpanel\\monitor\\index.html","public\\assets\\js\\backend\\btpanel\\crontab.js","public\\assets\\js\\backend\\btpanel\\index.js","public\\assets\\js\\backend\\btpanel\\logs.js","public\\assets\\js\\backend\\btpanel\\monitor.js","public\\assets\\addons\\btpanel\\css\\codemirror.css","public\\assets\\addons\\btpanel\\images\\loading\\loading-0.gif","public\\assets\\addons\\btpanel\\images\\loading\\loading-1.gif","public\\assets\\addons\\btpanel\\images\\loading\\loading-2.gif","public\\assets\\addons\\btpanel\\images\\loading\\loading-3.gif","public\\assets\\addons\\btpanel\\images\\loading\\loading-4.gif","public\\assets\\addons\\btpanel\\images\\loading\\loading-5.gif","public\\assets\\addons\\btpanel\\images\\loading\\loading-6.gif","public\\assets\\addons\\btpanel\\images\\loading\\loading-7.gif","public\\assets\\addons\\btpanel\\images\\loading\\loading-8.gif","public\\assets\\addons\\btpanel\\images\\loading\\loading-bars.gif","public\\assets\\addons\\btpanel\\images\\loading\\loading.gif","public\\assets\\addons\\btpanel\\js\\codemirror.js","public\\assets\\addons\\btpanel\\js\\jquery.knob.js","public\\assets\\addons\\btpanel\\js\\loading.js"]}
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace addons\btpanel;
|
||||
|
||||
use app\common\library\Menu;
|
||||
use think\Addons;
|
||||
|
||||
/**
|
||||
* 插件
|
||||
*/
|
||||
class Btpanel extends Addons
|
||||
{
|
||||
|
||||
/**
|
||||
* 插件安装方法
|
||||
* @return bool
|
||||
*/
|
||||
public function install()
|
||||
{
|
||||
$menu = [];
|
||||
$config_file = ADDON_PATH . "btpanel" . DS . 'config' . DS . "menu.php";
|
||||
if (is_file($config_file)) {
|
||||
$menu = include $config_file;
|
||||
}
|
||||
if ($menu) {
|
||||
Menu::create($menu);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件卸载方法
|
||||
* @return bool
|
||||
*/
|
||||
public function uninstall()
|
||||
{
|
||||
$info = get_addon_info('btpanel');
|
||||
Menu::delete(isset($info['first_menu']) ? $info['first_menu'] : 'btpanel');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件启用方法
|
||||
*/
|
||||
public function enable()
|
||||
{
|
||||
$info = get_addon_info('btpanel');
|
||||
Menu::enable(isset($info['first_menu']) ? $info['first_menu'] : 'btpanel');
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件禁用方法
|
||||
*/
|
||||
public function disable()
|
||||
{
|
||||
$info = get_addon_info('btpanel');
|
||||
Menu::disable(isset($info['first_menu']) ? $info['first_menu'] : 'btpanel');
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
require.config({
|
||||
paths: {
|
||||
knob: "../addons/btpanel/js/jquery.knob",
|
||||
codemirror: "../addons/btpanel/js/codemirror",
|
||||
loading: "../addons/btpanel/js/loading",
|
||||
},
|
||||
shim: {
|
||||
knob: ['jquery'],
|
||||
codemirror: ['css!../addons/btpanel/js/codemirror.css'],
|
||||
loading: ['jquery']
|
||||
}
|
||||
});
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
[
|
||||
'name' => 'key',
|
||||
'title' => 'BT密钥',
|
||||
'type' => 'string',
|
||||
'value' => 'wnuIownhJtTCly6ttZOx3FjYkSQHtvs2',
|
||||
'rule' => 'required',
|
||||
'tip' => '请先在BT面板中开启API功能',
|
||||
'extend' => '',
|
||||
],
|
||||
[
|
||||
'name' => 'url',
|
||||
'title' => '访问地址',
|
||||
'type' => 'string',
|
||||
'value' => 'https://38.12.47.166:41235',
|
||||
'rule' => 'required',
|
||||
'tip' => '请填写BT面板地址,格式为"http://XXX.XXX.XXX.XXX:8888"',
|
||||
'extend' => '',
|
||||
],
|
||||
[
|
||||
'type' => 'bool',
|
||||
'name' => 'admin',
|
||||
'title' => '仅限admin用户访问',
|
||||
'value' => '0',
|
||||
'content' => [
|
||||
1 => '开启',
|
||||
0 => '关闭',
|
||||
],
|
||||
'tip' => '',
|
||||
'rule' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
[
|
||||
'type' => 'bool',
|
||||
'name' => 'linux',
|
||||
'title' => '强制linux系统',
|
||||
'value' => '1',
|
||||
'content' => [
|
||||
1 => '开启',
|
||||
0 => '关闭',
|
||||
],
|
||||
'tip' => '',
|
||||
'rule' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
[
|
||||
'type' => 'number',
|
||||
'name' => 'cycle',
|
||||
'title' => '首页刷新周期',
|
||||
'value' => '5000',
|
||||
'content' => '',
|
||||
'tip' => '毫秒',
|
||||
'rule' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
];
|
||||
Executable
+242
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
/**
|
||||
* 菜单配置文件
|
||||
*/
|
||||
|
||||
return [
|
||||
[
|
||||
"type" => "file",
|
||||
"name" => "btpanel",
|
||||
"title" => "宝塔管理",
|
||||
"icon" => "fa fa-list",
|
||||
"condition" => "",
|
||||
"remark" => "",
|
||||
"ismenu" => 1,
|
||||
"sublist" => [
|
||||
[
|
||||
"type" => "file",
|
||||
"name" => "btpanel/index",
|
||||
"title" => "控制台",
|
||||
"icon" => "fa fa-circle-o",
|
||||
"condition" => "",
|
||||
"remark" => "",
|
||||
"ismenu" => 1,
|
||||
"sublist" => [
|
||||
[
|
||||
"type" => "file",
|
||||
"name" => "btpanel/index/index",
|
||||
"title" => "首页",
|
||||
"icon" => "fa fa-circle-o",
|
||||
"condition" => "",
|
||||
"remark" => "",
|
||||
"ismenu" => 0
|
||||
],
|
||||
[
|
||||
"type" => "file",
|
||||
"name" => "btpanel/index/add",
|
||||
"title" => "添加",
|
||||
"icon" => "fa fa-circle-o",
|
||||
"condition" => "",
|
||||
"remark" => "",
|
||||
"ismenu" => 0
|
||||
],
|
||||
[
|
||||
"type" => "file",
|
||||
"name" => "btpanel/index/edit",
|
||||
"title" => "编辑",
|
||||
"icon" => "fa fa-circle-o",
|
||||
"condition" => "",
|
||||
"remark" => "",
|
||||
"ismenu" => 0
|
||||
],
|
||||
[
|
||||
"type" => "file",
|
||||
"name" => "btpanel/index/del",
|
||||
"title" => "删除",
|
||||
"icon" => "fa fa-circle-o",
|
||||
"condition" => "",
|
||||
"remark" => "",
|
||||
"ismenu" => 0
|
||||
],
|
||||
[
|
||||
"type" => "file",
|
||||
"name" => "btpanel/index/multi",
|
||||
"title" => "批量更新",
|
||||
"icon" => "fa fa-circle-o",
|
||||
"condition" => "",
|
||||
"remark" => "",
|
||||
"ismenu" => 0
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
"type" => "file",
|
||||
"name" => "btpanel/crontab",
|
||||
"title" => "定时任务",
|
||||
"icon" => "fa fa-circle-o",
|
||||
"condition" => "",
|
||||
"remark" => "",
|
||||
"ismenu" => 1,
|
||||
"sublist" => [
|
||||
[
|
||||
"type" => "file",
|
||||
"name" => "btpanel/crontab/index",
|
||||
"title" => "首页",
|
||||
"icon" => "fa fa-circle-o",
|
||||
"condition" => "",
|
||||
"remark" => "",
|
||||
"ismenu" => 0
|
||||
],
|
||||
[
|
||||
"type" => "file",
|
||||
"name" => "btpanel/crontab/add",
|
||||
"title" => "添加",
|
||||
"icon" => "fa fa-circle-o",
|
||||
"condition" => "",
|
||||
"remark" => "",
|
||||
"ismenu" => 0
|
||||
],
|
||||
[
|
||||
"type" => "file",
|
||||
"name" => "btpanel/crontab/edit",
|
||||
"title" => "编辑",
|
||||
"icon" => "fa fa-circle-o",
|
||||
"condition" => "",
|
||||
"remark" => "",
|
||||
"ismenu" => 0
|
||||
],
|
||||
[
|
||||
"type" => "file",
|
||||
"name" => "btpanel/crontab/del",
|
||||
"title" => "删除",
|
||||
"icon" => "fa fa-circle-o",
|
||||
"condition" => "",
|
||||
"remark" => "",
|
||||
"ismenu" => 0
|
||||
],
|
||||
[
|
||||
"type" => "file",
|
||||
"name" => "btpanel/crontab/multi",
|
||||
"title" => "批量更新",
|
||||
"icon" => "fa fa-circle-o",
|
||||
"condition" => "",
|
||||
"remark" => "",
|
||||
"ismenu" => 0
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
"type" => "file",
|
||||
"name" => "btpanel/logs",
|
||||
"title" => "运行日志",
|
||||
"icon" => "fa fa-circle-o",
|
||||
"condition" => "",
|
||||
"remark" => "",
|
||||
"ismenu" => 1,
|
||||
"sublist" => [
|
||||
[
|
||||
"type" => "file",
|
||||
"name" => "btpanel/logs/index",
|
||||
"title" => "首页",
|
||||
"icon" => "fa fa-circle-o",
|
||||
"condition" => "",
|
||||
"remark" => "",
|
||||
"ismenu" => 0
|
||||
],
|
||||
[
|
||||
"type" => "file",
|
||||
"name" => "btpanel/logs/add",
|
||||
"title" => "添加",
|
||||
"icon" => "fa fa-circle-o",
|
||||
"condition" => "",
|
||||
"remark" => "",
|
||||
"ismenu" => 0
|
||||
],
|
||||
[
|
||||
"type" => "file",
|
||||
"name" => "btpanel/logs/edit",
|
||||
"title" => "编辑",
|
||||
"icon" => "fa fa-circle-o",
|
||||
"condition" => "",
|
||||
"remark" => "",
|
||||
"ismenu" => 0
|
||||
],
|
||||
[
|
||||
"type" => "file",
|
||||
"name" => "btpanel/logs/del",
|
||||
"title" => "删除",
|
||||
"icon" => "fa fa-circle-o",
|
||||
"condition" => "",
|
||||
"remark" => "",
|
||||
"ismenu" => 0
|
||||
],
|
||||
[
|
||||
"type" => "file",
|
||||
"name" => "btpanel/logs/multi",
|
||||
"title" => "批量更新",
|
||||
"icon" => "fa fa-circle-o",
|
||||
"condition" => "",
|
||||
"remark" => "",
|
||||
"ismenu" => 0
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
"type" => "file",
|
||||
"name" => "btpanel/monitor",
|
||||
"title" => "服务器监控",
|
||||
"icon" => "fa fa-circle-o",
|
||||
"condition" => "",
|
||||
"remark" => "",
|
||||
"ismenu" => 1,
|
||||
"sublist" => [
|
||||
[
|
||||
"type" => "file",
|
||||
"name" => "btpanel/monitor/index",
|
||||
"title" => "首页",
|
||||
"icon" => "fa fa-circle-o",
|
||||
"condition" => "",
|
||||
"remark" => "",
|
||||
"ismenu" => 0
|
||||
],
|
||||
[
|
||||
"type" => "file",
|
||||
"name" => "btpanel/monitor/add",
|
||||
"title" => "添加",
|
||||
"icon" => "fa fa-circle-o",
|
||||
"condition" => "",
|
||||
"remark" => "",
|
||||
"ismenu" => 0
|
||||
],
|
||||
[
|
||||
"type" => "file",
|
||||
"name" => "btpanel/monitor/edit",
|
||||
"title" => "编辑",
|
||||
"icon" => "fa fa-circle-o",
|
||||
"condition" => "",
|
||||
"remark" => "",
|
||||
"ismenu" => 0
|
||||
],
|
||||
[
|
||||
"type" => "file",
|
||||
"name" => "btpanel/monitor/del",
|
||||
"title" => "删除",
|
||||
"icon" => "fa fa-circle-o",
|
||||
"condition" => "",
|
||||
"remark" => "",
|
||||
"ismenu" => 0
|
||||
],
|
||||
[
|
||||
"type" => "file",
|
||||
"name" => "btpanel/monitor/multi",
|
||||
"title" => "批量更新",
|
||||
"icon" => "fa fa-circle-o",
|
||||
"condition" => "",
|
||||
"remark" => "",
|
||||
"ismenu" => 0
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
name = btpanel
|
||||
title = Linux宝塔监控
|
||||
intro = Linux宝塔运行监控及定时任务管理
|
||||
author = Oo小刚oO
|
||||
website =
|
||||
version = 1.0.0
|
||||
state = 1
|
||||
url = /addons/btpanel
|
||||
first_menu = btpanel
|
||||
license = regular
|
||||
licenseto = 16556
|
||||
Executable
+628
@@ -0,0 +1,628 @@
|
||||
<?php
|
||||
|
||||
namespace addons\btpanel\library;
|
||||
|
||||
class Api
|
||||
{
|
||||
use \traits\controller\Jump;
|
||||
private $BT_KEY = "wnuIownhJtTCly6ttZOx3FjYkSQHtvs2"; //接口密钥
|
||||
private $BT_PANEL = "https://38.12.47.166:41235"; //面板地址
|
||||
|
||||
//如果希望多台面板,可以在实例化对象时,将面板地址与密钥传入
|
||||
public function __construct($bt_panel = null, $bt_key = null)
|
||||
{
|
||||
$config = get_addon_config('btpanel');
|
||||
if ($config['linux']) {
|
||||
if (!PATH_SEPARATOR == ':') {
|
||||
$this->error('该插件仅支持Linux版宝塔');
|
||||
}
|
||||
}
|
||||
$this->BT_PANEL = $bt_panel ?: $config['url'] ?: '';
|
||||
$this->BT_KEY = $bt_key ?: $config['key'] ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统基础统计
|
||||
*/
|
||||
public function getSystemTotal()
|
||||
{
|
||||
return $this->getData('system?action=GetSystemTotal');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实时状态信息(CPU、内存、网络、负载)
|
||||
*/
|
||||
public function getNetWork()
|
||||
{
|
||||
return $this->getData('system?action=GetNetWork');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取磁盘分区信息
|
||||
*/
|
||||
public function getDiskInfo()
|
||||
{
|
||||
return $this->getData('system?action=GetDiskInfo');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有安装任务
|
||||
*/
|
||||
public function getTaskCount()
|
||||
{
|
||||
return $this->getData('ajax?action=GetTaskCount');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查面板更新
|
||||
*/
|
||||
public function updatePanel()
|
||||
{
|
||||
return $this->getData('ajax?action=UpdatePanel');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网站列表
|
||||
* @param array $params p=>当前分页,limit=>取回行数,type=>分类标识(-1:分布分类,0默认分类),order=>排序规则(id desc),tojs=>分页JS回调,search=>搜索内容
|
||||
*/
|
||||
public function getWebSite($params)
|
||||
{
|
||||
return $this->getData('data?action=getData&table=sites', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网站端口及URL
|
||||
* @param array $id 网站ID
|
||||
*/
|
||||
public function getWebSiteDomain($id)
|
||||
{
|
||||
return $this->getData('data?action=getData&table=domain&list=true', ['search' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网站分类
|
||||
*/
|
||||
public function getSiteTypes()
|
||||
{
|
||||
return $this->getData('site?action=get_site_types');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已安装的PHP版本列表
|
||||
*/
|
||||
public function getPHPVersion()
|
||||
{
|
||||
return $this->getData('site?action=getPHPVersion');
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置网站到期时间
|
||||
* @param array $params id=>网站ID,edate=>到期时间(永久:0000-00-00)
|
||||
*/
|
||||
public function setEdate($params)
|
||||
{
|
||||
return $this->getData('site?action=setEdate', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改网站备注
|
||||
* @param array $params id=>网站ID,ps=>备注内容
|
||||
*/
|
||||
public function setPs($params)
|
||||
{
|
||||
return $this->getData('data?action=setPs&table=sites', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网站备份列表
|
||||
* @param array $params p=>当前分页,limit=>取回行数,type=>备份类型(固定传0),tojs=>分页JS回调,search=>网站ID
|
||||
*/
|
||||
public function getBackupData($params)
|
||||
{
|
||||
$params['type'] = 0;
|
||||
return $this->getData('data?action=getData&table=backup', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建网站备份
|
||||
* @param string $id 网站ID
|
||||
*/
|
||||
public function toBackup($id)
|
||||
{
|
||||
return $this->getData('site?action=ToBackup', ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除网站备份
|
||||
* @param string $id 网站ID
|
||||
*/
|
||||
public function delBackup($id)
|
||||
{
|
||||
return $this->getData('site?action=DelBackup', ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网站的域名列表
|
||||
* @param string $id 网站ID
|
||||
*/
|
||||
public function getDomainData($id)
|
||||
{
|
||||
return $this->getData('data?action=getData&table=domain', ['search' => $id, 'list' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加域名
|
||||
* @param String $id 网站ID
|
||||
* @param String $webname 网站名称
|
||||
* @param String $domain 要添加的域名:端口(80端口请忽略端口号)
|
||||
*/
|
||||
public function addDomain($id, $webname, $domain)
|
||||
{
|
||||
return $this->getData('site?action=AddDomain', [
|
||||
'id' => $id,
|
||||
'webname' => $webname,
|
||||
'domain' => $domain
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除域名
|
||||
* @param String $id 网站ID
|
||||
* @param String $webname 网站名称
|
||||
* @param String $domain 要删除的域名
|
||||
* @param String|numeric $port 该域名的端口
|
||||
*/
|
||||
public function delDomain($id, $webname, $domain, $port)
|
||||
{
|
||||
return $this->getData('site?action=DelDomain', [
|
||||
'id' => $id,
|
||||
'webname' => $webname,
|
||||
'domain' => $domain,
|
||||
'port' => $port
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可选的预定义伪静态列表
|
||||
* @param string $siteName 网站名称
|
||||
*/
|
||||
public function getRewriteList($siteName)
|
||||
{
|
||||
return $this->getData('site?action=GetR&table=domain', ['siteName' => $siteName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定域名伪静态规则内容(获取文件内容)
|
||||
* @param string $domain 网站域名
|
||||
*/
|
||||
public function getFileBody($domain)
|
||||
{
|
||||
return $this->getData('files?action=GetFileBody', ['path' => "/www/server/panel/vhost/rewrite/nginx/" . $domain . ".conf"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定域名伪静态规则内容(获取文件内容)
|
||||
* @param String $domain 网站域名
|
||||
* @param String $data 规则内容
|
||||
* @param String $encoding 文件编码,固定为'utf-8'
|
||||
*/
|
||||
public function saveFileBody($domain, $data, $encoding = "utf-8")
|
||||
{
|
||||
return $this->getData('files?action=SaveFileBody', [
|
||||
'path' => "/www/server/panel/vhost/rewrite/nginx/" . $domain . ".conf",
|
||||
'data' => $data,
|
||||
'encoding' => $encoding
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取回指定网站的跟目录
|
||||
* @param string $id 网站ID
|
||||
*/
|
||||
public function getSitesPath($id)
|
||||
{
|
||||
return $this->getData('data?action=getKey&table=sites&key=path', ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取回防跨站配置/运行目录/日志开关状态/可设置的运行目录列表/密码访问状态
|
||||
* @param string $id 网站ID
|
||||
*/
|
||||
public function getDirUserINI($id)
|
||||
{
|
||||
return $this->getData('site?action=GetDirUserINI', [
|
||||
'id' => $id,
|
||||
'path' => $this->getSitesPath($id)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置防跨站状态(自动取反)
|
||||
* @param string $id 网站ID
|
||||
*/
|
||||
public function setDirUserINI($id)
|
||||
{
|
||||
return $this->getData('site?action=setDirUserINI', ['path' => $this->getSitesPath($id)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否防写访问日志
|
||||
* @param string $id 网站ID
|
||||
*/
|
||||
public function logsOpen($id)
|
||||
{
|
||||
return $this->getData('site?action=logsOpen', ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改网站根目录
|
||||
* @param string $id 网站ID
|
||||
* @param string $path 新的网站根目录
|
||||
*/
|
||||
public function setPath($id, $path)
|
||||
{
|
||||
return $this->getData('site?action=SetPath', ['id' => $id, 'path' => $path]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否写访问日志
|
||||
* @param string $id 网站ID
|
||||
* @param string $runPath 基于网站跟目录的运行目录
|
||||
*/
|
||||
public function setSiteRunPath($id, $runPath)
|
||||
{
|
||||
return $this->getData('site?action=SetSiteRunPath', ['id' => $id, 'runPath' => $runPath]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置密码访问
|
||||
* @param string $id 网站ID
|
||||
* @param string $username 用户名
|
||||
* @param string $password 密码
|
||||
*/
|
||||
public function setHasPwd($id, $username, $password)
|
||||
{
|
||||
return $this->getData('site?action=SetHasPwd', ['id' => $id, 'username' => $username, 'password' => $password]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭密码访问
|
||||
* @param string $id 网站ID
|
||||
*/
|
||||
public function closeHasPwd($id)
|
||||
{
|
||||
return $this->getData('site?action=CloseHasPwd', ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流量限制相关配置(仅支持nginx)
|
||||
* @param string $id 网站ID
|
||||
*/
|
||||
public function getLimitNet($id)
|
||||
{
|
||||
return $this->getData('site?action=GetLimitNet', ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开启或保存流量限制相关配置(仅支持nginx)
|
||||
* @param string $id 网站ID
|
||||
* @param number $perserver 并发限制
|
||||
* @param number $perip 单IP限制
|
||||
* @param number $limit_rate 流量限制
|
||||
*/
|
||||
public function setLimitNet($id, $perserver, $perip, $limit_rate)
|
||||
{
|
||||
return $this->getData('site?action=SetLimitNet', [
|
||||
'id' => $id,
|
||||
'perserver' => $perserver,
|
||||
'perip' => $perip,
|
||||
'limit_rate' => $limit_rate
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭流量限制(仅支持nginx)
|
||||
* @param string $id 网站ID
|
||||
*/
|
||||
public function closeLimitNet($id)
|
||||
{
|
||||
return $this->getData('site?action=CloseLimitNet', ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取默认文档信息
|
||||
* @param string $id 网站ID
|
||||
*/
|
||||
public function getIndex($id)
|
||||
{
|
||||
return $this->getData('site?action=GetIndex', ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置默认文档
|
||||
* @param string $id 网站ID
|
||||
* @param string $Index 默认文档,用逗号隔开
|
||||
*/
|
||||
public function setIndex($id, $Index)
|
||||
{
|
||||
return $this->getData('site?action=SetIndex', ['id' => $id, 'Index' => $Index]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取面板日志
|
||||
*/
|
||||
public function getLogs($params, $type = "")
|
||||
{
|
||||
if ($type == "crontab") {
|
||||
return $this->getData('crontab?action=GetLogs', $params);
|
||||
} else {
|
||||
return $this->getData('data?action=getData', [
|
||||
'table' => 'logs',
|
||||
'limit' => $params['limit'] ?: 10,
|
||||
'p' => $params['p'] ?: 1,
|
||||
'tojs' => $params['tojs'] ?: 'pageTo'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取面板配置信息
|
||||
*
|
||||
*/
|
||||
public function getPanelErrorLogs()
|
||||
{
|
||||
return $this->getData('config?action=get_panel_error_logs');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取面板配置信息
|
||||
*
|
||||
*/
|
||||
public function getConfig()
|
||||
{
|
||||
return $this->getData('config?action=get_config');
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统监控设置
|
||||
*
|
||||
* @param bool $type 是否开启
|
||||
* @param integer $day 保存天数
|
||||
* @return void
|
||||
*/
|
||||
public function setControl($type = -1, $day = '')
|
||||
{
|
||||
return $this->getData('config?action=SetControl', ['type' => $type, 'day' => $day]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放内存
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function reMemory()
|
||||
{
|
||||
return $this->getData('system?action=ReMemory');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平均负载使用率
|
||||
*
|
||||
* @param string $start 开始时间
|
||||
* @param string $end 结束时间
|
||||
* @return void
|
||||
*/
|
||||
public function getLoadAverage($start, $end)
|
||||
{
|
||||
return $this->getData('ajax?action=get_load_average', ['start' => $start, 'end' => $end]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取CPU利用率
|
||||
*
|
||||
* @param string $start 开始时间
|
||||
* @param string $end 结束时间
|
||||
* @return void
|
||||
*/
|
||||
public function getCpuIo($start, $end)
|
||||
{
|
||||
return $this->getData('ajax?action=GetCpuIo', ['start' => $start, 'end' => $end]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取磁盘IO
|
||||
*
|
||||
* @param string $start 开始时间
|
||||
* @param string $end 结束时间
|
||||
* @return void
|
||||
*/
|
||||
public function getDiskIo($start, $end)
|
||||
{
|
||||
return $this->getData('ajax?action=GetDiskIo', ['start' => $start, 'end' => $end]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取磁盘IO
|
||||
*
|
||||
* @param string $start 开始时间
|
||||
* @param string $end 结束时间
|
||||
* @return void
|
||||
*/
|
||||
public function getNetWorkIo($start, $end)
|
||||
{
|
||||
return $this->getData('ajax?action=GetNetWorkIo', ['start' => $start, 'end' => $end]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前PHP版本
|
||||
*/
|
||||
public function getCliPhpVersion()
|
||||
{
|
||||
return $this->getData('config?action=get_cli_php_version');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取计划任务列表
|
||||
*/
|
||||
public function getCrontab()
|
||||
{
|
||||
return $this->getData('crontab?action=GetCrontab');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取存储空间
|
||||
*/
|
||||
public function getDataList($type = 'sites')
|
||||
{
|
||||
return $this->getData('crontab?action=GetDataList', ['type' => $type]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行任务脚本
|
||||
* @param string $id 任务ID
|
||||
*/
|
||||
public function startTask($id)
|
||||
{
|
||||
return $this->getData('crontab?action=StartTask', ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询任务信息
|
||||
* @param string $id 任务ID
|
||||
*/
|
||||
public function getCrontabFind($id)
|
||||
{
|
||||
return $this->getData('crontab?action=get_crond_find', ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改定时任务
|
||||
* @param array $params 任务参数
|
||||
*/
|
||||
public function modifyCrond($params)
|
||||
{
|
||||
return $this->getData('crontab?action=modify_crond', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建定时任务
|
||||
* @param array $params 任务参数
|
||||
*/
|
||||
public function addCrontab($params)
|
||||
{
|
||||
return $this->getData('crontab?action=AddCrontab', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除定时任务
|
||||
* @param string $id 任务ID
|
||||
*/
|
||||
public function delCrontab($id)
|
||||
{
|
||||
return $this->getData('crontab?action=DelCrontab', ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换任务状态
|
||||
* @param string $id 任务ID
|
||||
*/
|
||||
public function setCronStatus($id)
|
||||
{
|
||||
return $this->getData('crontab?action=set_cron_status', ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空计划任务日志
|
||||
* @param string $id 任务ID
|
||||
*/
|
||||
public function delLogs($id)
|
||||
{
|
||||
return $this->getData('crontab?action=DelLogs', ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造带有签名的关联数组
|
||||
*/
|
||||
private function GetKeyData()
|
||||
{
|
||||
$now_time = time();
|
||||
$p_data = array(
|
||||
'request_token' => md5($now_time . '' . md5($this->BT_KEY)),
|
||||
'request_time' => $now_time
|
||||
);
|
||||
return $p_data;
|
||||
}
|
||||
|
||||
private function getData($url, $data = [])
|
||||
{
|
||||
//拼接URL地址
|
||||
$url = $this->BT_PANEL . '/' . $url;
|
||||
//准备POST数据
|
||||
$p_data = $this->GetKeyData(); //取签名
|
||||
$p_data = array_merge($p_data, $data);
|
||||
//请求面板接口
|
||||
$result = $this->HttpPostCookie($url, $p_data);
|
||||
//解析JSON数据
|
||||
$data = json_decode($result, true);
|
||||
if ($data === null) {
|
||||
$snippet = is_string($result) ? mb_substr(trim($result), 0, 300) : '';
|
||||
$this->error('未能获取到数据,请开启宝塔API' . (function_exists('json_last_error_msg') ? ',JSON解析失败:' . json_last_error_msg() : '') . ($snippet ? ',返回:' . $snippet : ''));
|
||||
}
|
||||
if (!is_array($data)) {
|
||||
$snippet = is_string($result) ? mb_substr(trim($result), 0, 300) : '';
|
||||
$this->error('连接到宝塔服务器异常' . ($snippet ? ',返回:' . $snippet : ''));
|
||||
}
|
||||
if (isset($data['status']) && $data['status'] == false) {
|
||||
$msg = $data['msg'] ?? ($data['message'] ?? ($data['error'] ?? ($data['error_msg'] ?? ($data['errmsg'] ?? null))));
|
||||
if (is_string($msg) && $msg !== '') {
|
||||
$this->error($msg);
|
||||
}
|
||||
$snippet = mb_substr(trim(json_encode($data, JSON_UNESCAPED_UNICODE)), 0, 300);
|
||||
$this->error('连接到宝塔服务器异常' . ($snippet ? ',返回:' . $snippet : ''));
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起POST请求
|
||||
* @param String $url 目标网填,带http://
|
||||
* @param Array|String $data 欲提交的数据
|
||||
* @return string
|
||||
*/
|
||||
private function HttpPostCookie($url, $data, $timeout = 60)
|
||||
{
|
||||
//定义cookie保存位置
|
||||
$cookie_file = rtrim(sys_get_temp_dir(), '\\/') . DIRECTORY_SEPARATOR . 'btpanel_' . md5($this->BT_PANEL) . '.cookie';
|
||||
if (!file_exists($cookie_file)) {
|
||||
$fp = fopen($cookie_file, 'w+');
|
||||
if ($fp === false) {
|
||||
$this->error('无法创建Cookie文件:' . $cookie_file);
|
||||
}
|
||||
fclose($fp);
|
||||
}
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
|
||||
curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie_file);
|
||||
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_file);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_HEADER, 0);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
$output = curl_exec($ch);
|
||||
if ($output === false) {
|
||||
$this->error(curl_error($ch));
|
||||
}
|
||||
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($httpCode >= 400) {
|
||||
$snippet = is_string($output) ? mb_substr(trim($output), 0, 300) : '';
|
||||
$this->error('宝塔接口HTTP错误:' . $httpCode . ($snippet ? ',返回:' . $snippet : ''));
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
{"files":["application\\admin\\controller\\example\\Baidumap.php","application\\admin\\controller\\example\\Bootstraptable.php","application\\admin\\controller\\example\\Colorbadge.php","application\\admin\\controller\\example\\Controllerjump.php","application\\admin\\controller\\example\\Customform.php","application\\admin\\controller\\example\\Customsearch.php","application\\admin\\controller\\example\\Cxselect.php","application\\admin\\controller\\example\\Echarts.php","application\\admin\\controller\\example\\Multitable.php","application\\admin\\controller\\example\\Relationmodel.php","application\\admin\\controller\\example\\Tablelink.php","application\\admin\\controller\\example\\Tabletemplate.php","application\\admin\\model\\Area.php","application\\admin\\view\\example\\baidumap\\index.html","application\\admin\\view\\example\\baidumap\\map.html","application\\admin\\view\\example\\bootstraptable\\detail.html","application\\admin\\view\\example\\bootstraptable\\edit.html","application\\admin\\view\\example\\bootstraptable\\index.html","application\\admin\\view\\example\\colorbadge\\index.html","application\\admin\\view\\example\\controllerjump\\index.html","application\\admin\\view\\example\\customform\\index.html","application\\admin\\view\\example\\customsearch\\index.html","application\\admin\\view\\example\\cxselect\\index.html","application\\admin\\view\\example\\echarts\\index.html","application\\admin\\view\\example\\multitable\\index.html","application\\admin\\view\\example\\relationmodel\\index.html","application\\admin\\view\\example\\tablelink\\index.html","application\\admin\\view\\example\\tabletemplate\\index.html","public\\assets\\js\\backend\\example\\baidumap.js","public\\assets\\js\\backend\\example\\bootstraptable.js","public\\assets\\js\\backend\\example\\colorbadge.js","public\\assets\\js\\backend\\example\\controllerjump.js","public\\assets\\js\\backend\\example\\customform.js","public\\assets\\js\\backend\\example\\customsearch.js","public\\assets\\js\\backend\\example\\cxselect.js","public\\assets\\js\\backend\\example\\echarts.js","public\\assets\\js\\backend\\example\\multitable.js","public\\assets\\js\\backend\\example\\relationmodel.js","public\\assets\\js\\backend\\example\\tablelink.js","public\\assets\\js\\backend\\example\\tabletemplate.js","public\\assets\\addons\\example\\css\\common.css","public\\assets\\addons\\example\\img\\plus.png","public\\assets\\addons\\example\\js\\async.js"],"license":"regular","licenseto":"16556","licensekey":"fjlVuE8MOgT5m2yw yqj4tRlaeX30ZYA2j0MFgg==","domains":[],"licensecodes":[],"validations":[],"menus":["example","example\/bootstraptable","example\/bootstraptable\/index","example\/bootstraptable\/detail","example\/bootstraptable\/change","example\/bootstraptable\/del","example\/bootstraptable\/multi","example\/customsearch","example\/customsearch\/index","example\/customsearch\/del","example\/customsearch\/multi","example\/customform","example\/customform\/index","example\/tablelink","example\/tablelink\/index","example\/colorbadge","example\/colorbadge\/index","example\/colorbadge\/del","example\/colorbadge\/multi","example\/controllerjump","example\/controllerjump\/index","example\/controllerjump\/del","example\/controllerjump\/multi","example\/cxselect","example\/cxselect\/index","example\/cxselect\/del","example\/cxselect\/multi","example\/multitable","example\/multitable\/index","example\/multitable\/del","example\/multitable\/multi","example\/relationmodel","example\/relationmodel\/index","example\/relationmodel\/del","example\/relationmodel\/multi","example\/tabletemplate","example\/tabletemplate\/index","example\/tabletemplate\/detail","example\/tabletemplate\/del","example\/tabletemplate\/multi","example\/baidumap","example\/baidumap\/index","example\/baidumap\/map","example\/baidumap\/del","example\/echarts","example\/echarts\/index"]}
|
||||
Executable
+183
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
namespace addons\example;
|
||||
|
||||
use app\common\library\Menu;
|
||||
use think\Addons;
|
||||
|
||||
/**
|
||||
* Example
|
||||
*/
|
||||
class Example extends Addons
|
||||
{
|
||||
|
||||
/**
|
||||
* 插件安装方法
|
||||
* @return bool
|
||||
*/
|
||||
public function install()
|
||||
{
|
||||
$menu = [
|
||||
[
|
||||
'name' => 'example',
|
||||
'title' => '开发示例管理',
|
||||
'icon' => 'fa fa-magic',
|
||||
'sublist' => [
|
||||
[
|
||||
'name' => 'example/bootstraptable',
|
||||
'title' => '表格完整示例',
|
||||
'icon' => 'fa fa-table',
|
||||
'sublist' => [
|
||||
['name' => 'example/bootstraptable/index', 'title' => '查看'],
|
||||
['name' => 'example/bootstraptable/detail', 'title' => '详情'],
|
||||
['name' => 'example/bootstraptable/change', 'title' => '变更'],
|
||||
['name' => 'example/bootstraptable/del', 'title' => '删除'],
|
||||
['name' => 'example/bootstraptable/multi', 'title' => '批量更新'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'example/customsearch',
|
||||
'title' => '自定义搜索',
|
||||
'icon' => 'fa fa-table',
|
||||
'sublist' => [
|
||||
['name' => 'example/customsearch/index', 'title' => '查看'],
|
||||
['name' => 'example/customsearch/del', 'title' => '删除'],
|
||||
['name' => 'example/customsearch/multi', 'title' => '批量更新'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'example/customform',
|
||||
'title' => '自定义表单示例',
|
||||
'icon' => 'fa fa-edit',
|
||||
'sublist' => [
|
||||
['name' => 'example/customform/index', 'title' => '查看'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'example/tablelink',
|
||||
'title' => '表格联动示例',
|
||||
'icon' => 'fa fa-table',
|
||||
'remark' => '点击左侧日志列表,右侧的表格数据会显示指定管理员的日志列表',
|
||||
'sublist' => [
|
||||
['name' => 'example/tablelink/index', 'title' => '查看'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'example/colorbadge',
|
||||
'title' => '彩色角标',
|
||||
'icon' => 'fa fa-table',
|
||||
'remark' => '左侧彩色的角标会根据当前数据量的大小进行更新',
|
||||
'sublist' => [
|
||||
['name' => 'example/colorbadge/index', 'title' => '查看'],
|
||||
['name' => 'example/colorbadge/del', 'title' => '删除'],
|
||||
['name' => 'example/colorbadge/multi', 'title' => '批量更新'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'example/controllerjump',
|
||||
'title' => '控制器间跳转',
|
||||
'icon' => 'fa fa-table',
|
||||
'remark' => '点击IP地址可以跳转到新的选项卡中查看指定IP的数据',
|
||||
'sublist' => [
|
||||
['name' => 'example/controllerjump/index', 'title' => '查看'],
|
||||
['name' => 'example/controllerjump/del', 'title' => '删除'],
|
||||
['name' => 'example/controllerjump/multi', 'title' => '批量更新'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'example/cxselect',
|
||||
'title' => '多级联动',
|
||||
'icon' => 'fa fa-table',
|
||||
'remark' => '基于jquery.cxselect实现的多级联动',
|
||||
'sublist' => [
|
||||
['name' => 'example/cxselect/index', 'title' => '查看'],
|
||||
['name' => 'example/cxselect/del', 'title' => '删除'],
|
||||
['name' => 'example/cxselect/multi', 'title' => '批量更新'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'example/multitable',
|
||||
'title' => '多表格示例',
|
||||
'icon' => 'fa fa-table',
|
||||
'remark' => '展示在一个页面显示多个Bootstrap-table表格',
|
||||
'sublist' => [
|
||||
['name' => 'example/multitable/index', 'title' => '查看'],
|
||||
['name' => 'example/multitable/del', 'title' => '删除'],
|
||||
['name' => 'example/multitable/multi', 'title' => '批量更新'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'example/relationmodel',
|
||||
'title' => '关联模型示例',
|
||||
'icon' => 'fa fa-table',
|
||||
'remark' => '列表中的头像、用户名和昵称字段均从关联表中取出',
|
||||
'sublist' => [
|
||||
['name' => 'example/relationmodel/index', 'title' => '查看'],
|
||||
['name' => 'example/relationmodel/del', 'title' => '删除'],
|
||||
['name' => 'example/relationmodel/multi', 'title' => '批量更新'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'example/tabletemplate',
|
||||
'title' => '表格模板示例',
|
||||
'icon' => 'fa fa-table',
|
||||
'remark' => '',
|
||||
'sublist' => [
|
||||
['name' => 'example/tabletemplate/index', 'title' => '查看'],
|
||||
['name' => 'example/tabletemplate/detail', 'title' => '详情'],
|
||||
['name' => 'example/tabletemplate/del', 'title' => '删除'],
|
||||
['name' => 'example/tabletemplate/multi', 'title' => '批量更新'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'example/baidumap',
|
||||
'title' => '百度地图示例',
|
||||
'icon' => 'fa fa-map-pin',
|
||||
'sublist' => [
|
||||
['name' => 'example/baidumap/index', 'title' => '查看'],
|
||||
['name' => 'example/baidumap/map', 'title' => '详情'],
|
||||
['name' => 'example/baidumap/del', 'title' => '删除'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'example/echarts',
|
||||
'title' => '统计图表示例',
|
||||
'icon' => 'fa fa-bar-chart',
|
||||
'sublist' => [
|
||||
['name' => 'example/echarts/index', 'title' => '查看'],
|
||||
]
|
||||
],
|
||||
]
|
||||
]
|
||||
];
|
||||
Menu::create($menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件卸载方法
|
||||
* @return bool
|
||||
*/
|
||||
public function uninstall()
|
||||
{
|
||||
Menu::delete('example');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件启用方法
|
||||
*/
|
||||
public function enable()
|
||||
{
|
||||
Menu::enable('example');
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件禁用方法
|
||||
*/
|
||||
public function disable()
|
||||
{
|
||||
Menu::disable('example');
|
||||
}
|
||||
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
require.config({
|
||||
paths: {
|
||||
'async': '../addons/example/js/async',
|
||||
'BMap': ['//api.map.baidu.com/api?v=2.0&ak='],
|
||||
},
|
||||
shim: {
|
||||
'BMap': {
|
||||
deps: ['jquery'],
|
||||
exports: 'BMap'
|
||||
}
|
||||
}
|
||||
});
|
||||
Executable
+130
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
[
|
||||
'name' => 'condition1',
|
||||
'title' => '条件1',
|
||||
'type' => 'radio',
|
||||
'group' => '选项组一',
|
||||
'content' => [
|
||||
'value1' => '值1',
|
||||
'value2' => '值2',
|
||||
],
|
||||
'value' => 'value2',
|
||||
'rule' => 'required',
|
||||
'msg' => '',
|
||||
'tip' => '',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
[
|
||||
'name' => 'condition2',
|
||||
'title' => '条件2',
|
||||
'type' => 'checkbox',
|
||||
'group' => '选项组一',
|
||||
'visible' => 'condition1=value1',
|
||||
'content' => [
|
||||
'value1' => '值1',
|
||||
'value2' => '值2',
|
||||
'value3' => '值3',
|
||||
],
|
||||
'value' => 'value1,value2',
|
||||
'rule' => 'required',
|
||||
'msg' => '',
|
||||
'tip' => '',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
[
|
||||
'name' => 'condition3',
|
||||
'title' => '条件3',
|
||||
'type' => 'select',
|
||||
'group' => '选项组一',
|
||||
'visible' => 'condition1=value2',
|
||||
'content' => [
|
||||
'value1' => '值1',
|
||||
'value2' => '值2',
|
||||
],
|
||||
'value' => 'value1',
|
||||
'rule' => 'required',
|
||||
'msg' => '',
|
||||
'tip' => '',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
[
|
||||
'name' => 'condition4',
|
||||
'title' => '条件4',
|
||||
'type' => 'selects',
|
||||
'group' => '选项组一',
|
||||
'content' => [
|
||||
'value1' => '值1',
|
||||
'value2' => '值2',
|
||||
'value3' => '值3',
|
||||
],
|
||||
'value' => 'value1,value2',
|
||||
'rule' => 'required',
|
||||
'msg' => '',
|
||||
'tip' => '',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
[
|
||||
'name' => 'title',
|
||||
'title' => '标题',
|
||||
'type' => 'string',
|
||||
'group' => '选项组一',
|
||||
'visible' => 'condition3=value1',
|
||||
'content' => [],
|
||||
'value' => '3x',
|
||||
'rule' => 'required',
|
||||
'msg' => '',
|
||||
'tip' => '',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
[
|
||||
'name' => 'domain',
|
||||
'title' => '绑定二级域名前缀',
|
||||
'type' => 'string',
|
||||
'group' => '选项组二',
|
||||
'content' => [],
|
||||
'value' => '',
|
||||
'rule' => 'required',
|
||||
'msg' => '',
|
||||
'tip' => '',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
[
|
||||
'name' => 'rewrite',
|
||||
'title' => '伪静态',
|
||||
'type' => 'array',
|
||||
'group' => '选项组二',
|
||||
'content' => [],
|
||||
'value' => [
|
||||
'index/index' => '/example$',
|
||||
'demo/index' => '/example/d/[:name]',
|
||||
'demo/demo1' => '/example/d1/[:name]',
|
||||
'demo/demo2' => '/example/d2/[:name]',
|
||||
],
|
||||
'rule' => 'required',
|
||||
'msg' => '',
|
||||
'tip' => '',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
[
|
||||
'name' => '__tips__',
|
||||
'title' => '温馨提示',
|
||||
'type' => 'string',
|
||||
'content' => [
|
||||
],
|
||||
'value' => '这里是提示的文本内容',
|
||||
'rule' => '',
|
||||
'msg' => '',
|
||||
'tip' => '',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
];
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace addons\example\controller;
|
||||
|
||||
use think\addons\Controller;
|
||||
|
||||
/**
|
||||
* 测试控制器
|
||||
*/
|
||||
class Demo extends Controller
|
||||
{
|
||||
|
||||
protected $layout = 'default';
|
||||
protected $noNeedLogin = ['index', 'demo1'];
|
||||
protected $noNeedRight = ['*'];
|
||||
|
||||
public function index()
|
||||
{
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
public function demo1()
|
||||
{
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
public function demo2()
|
||||
{
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
}
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace addons\example\controller;
|
||||
|
||||
use think\addons\Controller;
|
||||
|
||||
class Index extends Controller
|
||||
{
|
||||
|
||||
protected $layout = 'default';
|
||||
|
||||
public function index()
|
||||
{
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
}
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
name = example
|
||||
title = 开发示例
|
||||
intro = FastAdmin多个开发示例
|
||||
author = FastAdmin
|
||||
website = https://www.fastadmin.net
|
||||
version = 1.1.1
|
||||
state = 1
|
||||
url = /addons/example
|
||||
license = regular
|
||||
licenseto = 16556
|
||||
Executable
+3795
File diff suppressed because it is too large
Load Diff
Executable
+33
@@ -0,0 +1,33 @@
|
||||
<!-- Page Content -->
|
||||
<div class="container">
|
||||
|
||||
<!-- Page Heading/Breadcrumbs -->
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header">无需登录页面
|
||||
<small>开发者示例</small>
|
||||
</h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{:addon_url('example/index/index')}">插件首页</a>
|
||||
</li>
|
||||
<li class="active">无需登录页面</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
|
||||
<!-- Content Row -->
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<p class="well">当前登录页面无需登录即可查看,当前请求的name值为:{$Request.param.name|htmlentities}</p>
|
||||
{if $user}
|
||||
<p class="well text-danger">但是如果你登录后可以浏览到这段隐藏的信息</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
|
||||
<hr>
|
||||
|
||||
</div>
|
||||
<!-- /.container -->
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
<!-- Page Content -->
|
||||
<div class="container">
|
||||
|
||||
<!-- Page Heading/Breadcrumbs -->
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header">需登录页面
|
||||
<small>开发者示例</small>
|
||||
</h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{:addon_url('example/index/index')}">插件首页</a>
|
||||
</li>
|
||||
<li class="active">需登录页面</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
|
||||
<!-- Content Row -->
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<p class="well">当前登录页面需要登录后才可以查看,你可以退出后再访问此页面,会提醒登录,当前请求的name值为:{$Request.param.name|htmlentities}</p>
|
||||
<p class="well">你好!{$user.nickname|htmlentities},<a href="{:url('index/user/logout')}">注销登录</a></p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
|
||||
<hr>
|
||||
|
||||
</div>
|
||||
<!-- /.container -->
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
<!-- Page Content -->
|
||||
<div class="container">
|
||||
|
||||
<!-- Page Heading/Breadcrumbs -->
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header">使用模板标签和变量
|
||||
<small>开发者示例</small>
|
||||
</h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{:addon_url('example/index/index')}">插件首页</a>
|
||||
</li>
|
||||
<li class="active">使用模板标签和变量</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
|
||||
<!-- Content Row -->
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<p class="well">当前请求的name值为:{$Request.param.name|htmlentities}</p>
|
||||
{literal}
|
||||
<pre>
|
||||
在插件视图中可以使用所有ThinkPHP5内支持的模板标签和变量,如
|
||||
|
||||
{$Think.server.script_name} // 输出$_SERVER['SCRIPT_NAME']变量
|
||||
{$Think.session.user_id} // 输出$_SESSION['user_id']变量
|
||||
{$Think.get.pageNumber} // 输出$_GET['pageNumber']变量
|
||||
{$Think.cookie.name} // 输出$_COOKIE['name']变量
|
||||
|
||||
// 调用Request对象的get方法 传入参数为id
|
||||
{$Request.get.id}
|
||||
// 调用Request对象的param方法 传入参数为name
|
||||
{$Request.param.name}
|
||||
// 调用Request对象的param方法 传入参数为user.nickname
|
||||
{$Request.param.user.nickname}
|
||||
// 调用Request对象的root方法
|
||||
{$Request.root}
|
||||
// 调用Request对象的root方法,并且传入参数true
|
||||
{$Request.root.true}
|
||||
// 调用Request对象的path方法
|
||||
{$Request.path}
|
||||
// 调用Request对象的module方法
|
||||
{$Request.module}
|
||||
// 调用Request对象的controller方法
|
||||
{$Request.controller}
|
||||
// 调用Request对象的action方法
|
||||
{$Request.action}
|
||||
// 调用Request对象的ext方法
|
||||
{$Request.ext}
|
||||
// 调用Request对象的host方法
|
||||
{$Request.host}
|
||||
// 调用Request对象的ip方法
|
||||
{$Request.ip}
|
||||
// 调用Request对象的header方法
|
||||
{$Request.header.accept-encoding}
|
||||
</pre>
|
||||
{/literal}
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
|
||||
<hr>
|
||||
|
||||
</div>
|
||||
<!-- /.container -->
|
||||
Executable
+111
File diff suppressed because one or more lines are too long
Executable
+124
@@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
|
||||
<title>开发示例 - {$site.name|htmlentities}</title>
|
||||
|
||||
<!-- Bootstrap Core CSS -->
|
||||
<link href="__CDN__/assets/libs/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link href="__ADDON__/css/common.css" rel="stylesheet">
|
||||
|
||||
<!-- Custom Fonts -->
|
||||
<link href="__CDN__/assets/libs/font-awesome/css/font-awesome.min.css" rel="stylesheet">
|
||||
|
||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://cdn.bootcdn.net/ajax/libs/html5shiv/3.7.0/html5shiv.min.js"></script>
|
||||
<script src="https://cdn.bootcdn.net/ajax/libs/respond.js/1.4.2/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
|
||||
<div class="container">
|
||||
<!-- Brand and toggle get grouped for better mobile display -->
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="{:addon_url('example/index/index')}">{$site.name|htmlentities}</a>
|
||||
</div>
|
||||
<!-- Collect the nav links, forms, and other content for toggling -->
|
||||
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li>
|
||||
<a href="{:addon_url('example/index/index')}">插件首页</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{:addon_url('example/demo/demo1', [':name'=>'s1'])}">无需登录页面</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{:addon_url('example/demo/demo2', [':name'=>'s2'])}">需登录页面</a>
|
||||
</li>
|
||||
{if $user}
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">欢迎你! {$user.nickname|htmlentities}<b class="caret"></b></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="{:url('index/user/index')}">会员中心</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{:url('index/user/profile')}">个人资料</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{:url('index/user/logout')}">退出登录</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
{else /}
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">会员中心 <b class="caret"></b></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="{:url('index/user/login')}">登录</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{:url('index/user/register')}">注册</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
<!-- /.navbar-collapse -->
|
||||
</div>
|
||||
<!-- /.container -->
|
||||
</nav>
|
||||
|
||||
{__CONTENT__}
|
||||
|
||||
<div class="container">
|
||||
<!-- Footer -->
|
||||
<footer>
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<p>Copyright © {$site.name|htmlentities} 2022</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
<!-- /.container -->
|
||||
|
||||
<!-- jQuery -->
|
||||
<script src="__CDN__/assets/libs/jquery/dist/jquery.min.js"></script>
|
||||
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script src="__CDN__/assets/libs/bootstrap/dist/js/bootstrap.min.js"></script>
|
||||
|
||||
<!-- Script to Activate the Carousel -->
|
||||
<script>
|
||||
$('.carousel').carousel({
|
||||
interval: 5000 //changes the speed
|
||||
})
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Executable
+1
File diff suppressed because one or more lines are too long
Executable
+297
@@ -0,0 +1,297 @@
|
||||
<?php
|
||||
|
||||
namespace addons\kefu;
|
||||
|
||||
use app\common\library\Menu;
|
||||
use think\Addons;
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* 插件
|
||||
*/
|
||||
class Kefu extends Addons
|
||||
{
|
||||
|
||||
/**
|
||||
* 插件安装方法
|
||||
* @return bool
|
||||
*/
|
||||
public function install()
|
||||
{
|
||||
// 创建菜单
|
||||
$menu = [
|
||||
[
|
||||
'name' => 'kefu',
|
||||
'title' => '客服管理',
|
||||
'icon' => 'fa fa-comment',
|
||||
'sublist' => [
|
||||
[
|
||||
'name' => 'kefu/config',
|
||||
'title' => '客服配置',
|
||||
'icon' => 'fa fa-circle-o',
|
||||
'weigh' => '99',
|
||||
'sublist' => [
|
||||
['name' => 'kefu/config/index', 'title' => '查看'],
|
||||
['name' => 'kefu/config/update', 'title' => '编辑'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'kefu/user',
|
||||
'title' => '用户管理',
|
||||
'icon' => 'fa fa-circle-o',
|
||||
'weigh' => '98',
|
||||
'sublist' => [
|
||||
['name' => 'kefu/user/index', 'title' => '查看'],
|
||||
['name' => 'kefu/user/edit', 'title' => '编辑'],
|
||||
['name' => 'kefu/user/del', 'title' => '删除'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'kefu/session',
|
||||
'title' => '会话管理',
|
||||
'icon' => 'fa fa-circle-o',
|
||||
'weigh' => '97',
|
||||
'sublist' => [
|
||||
['name' => 'kefu/session/index', 'title' => '查看'],
|
||||
['name' => 'kefu/session/del', 'title' => '删除'],
|
||||
['name' => 'kefu/session/recyclebin', 'title' => '回收站'],
|
||||
['name' => 'kefu/session/destroy', 'title' => '真实删除'],
|
||||
['name' => 'kefu/session/restore', 'title' => '还原'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'kefu/kbs',
|
||||
'title' => '知识库管理',
|
||||
'icon' => 'fa fa-circle-o',
|
||||
'weigh' => '96',
|
||||
'sublist' => [
|
||||
['name' => 'kefu/kbs/index', 'title' => '查看'],
|
||||
['name' => 'kefu/kbs/add', 'title' => '增加'],
|
||||
['name' => 'kefu/kbs/edit', 'title' => '编辑'],
|
||||
['name' => 'kefu/kbs/del', 'title' => '删除'],
|
||||
['name' => 'kefu/kbs/multi', 'title' => '批量更新'],
|
||||
['name' => 'kefu/kbs/recyclebin', 'title' => '回收站'],
|
||||
['name' => 'kefu/kbs/destroy', 'title' => '真实删除'],
|
||||
['name' => 'kefu/kbs/restore', 'title' => '还原'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'kefu/csrkpi',
|
||||
'title' => '客服代表管理',
|
||||
'icon' => 'fa fa-circle-o',
|
||||
'weigh' => '95',
|
||||
'sublist' => [
|
||||
['name' => 'kefu/csrkpi/index', 'title' => '查看'],
|
||||
['name' => 'kefu/csrkpi/add', 'title' => '添加'],
|
||||
['name' => 'kefu/csrkpi/edit', 'title' => '编辑'],
|
||||
['name' => 'kefu/csrkpi/del', 'title' => '删除'],
|
||||
['name' => 'kefu/csrkpi/multi', 'title' => '批量更新']
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'kefu/leavemessage',
|
||||
'title' => '用户留言管理',
|
||||
'icon' => 'fa fa-circle-o',
|
||||
'weigh' => '94',
|
||||
'sublist' => [
|
||||
['name' => 'kefu/leavemessage/index', 'title' => '查看'],
|
||||
['name' => 'kefu/leavemessage/edit', 'title' => '编辑'],
|
||||
['name' => 'kefu/leavemessage/del', 'title' => '删除']
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'kefu/record',
|
||||
'title' => '聊天记录汇总',
|
||||
'icon' => 'fa fa-circle-o',
|
||||
'weigh' => '93',
|
||||
'sublist' => [
|
||||
['name' => 'kefu/record/index', 'title' => '查看'],
|
||||
['name' => 'kefu/record/edit', 'title' => '编辑'],
|
||||
['name' => 'kefu/record/del', 'title' => '删除'],
|
||||
['name' => 'kefu/record/multi', 'title' => '批量更新']
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'kefu/fastreply',
|
||||
'title' => '快捷回复管理',
|
||||
'icon' => 'fa fa-circle-o',
|
||||
'weigh' => '92',
|
||||
'sublist' => [
|
||||
['name' => 'kefu/fastreply/index', 'title' => '查看'],
|
||||
['name' => 'kefu/fastreply/add', 'title' => '增加'],
|
||||
['name' => 'kefu/fastreply/edit', 'title' => '编辑'],
|
||||
['name' => 'kefu/fastreply/del', 'title' => '删除'],
|
||||
['name' => 'kefu/fastreply/multi', 'title' => '批量更新'],
|
||||
['name' => 'kefu/fastreply/recyclebin', 'title' => '回收站'],
|
||||
['name' => 'kefu/fastreply/destroy', 'title' => '真实删除'],
|
||||
['name' => 'kefu/fastreply/restore', 'title' => '还原'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'kefu/blacklist',
|
||||
'title' => '用户黑名单管理',
|
||||
'icon' => 'fa fa-circle-o',
|
||||
'weigh' => '91',
|
||||
'sublist' => [
|
||||
['name' => 'kefu/blacklist/index', 'title' => '查看'],
|
||||
['name' => 'kefu/blacklist/add', 'title' => '增加'],
|
||||
['name' => 'kefu/blacklist/edit', 'title' => '编辑'],
|
||||
['name' => 'kefu/blacklist/del', 'title' => '删除'],
|
||||
['name' => 'kefu/blacklist/recyclebin', 'title' => '回收站'],
|
||||
['name' => 'kefu/blacklist/destroy', 'title' => '真实删除'],
|
||||
['name' => 'kefu/blacklist/restore', 'title' => '还原'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'kefu/toolbar',
|
||||
'title' => '窗口工具栏管理',
|
||||
'icon' => 'fa fa-circle-o',
|
||||
'weigh' => '90',
|
||||
'remark' => '此功能用于管理会话窗口工具栏基本信息及状态,若需添加自定义工具,请先自行实现对应功能',
|
||||
'sublist' => [
|
||||
['name' => 'kefu/toolbar/index', 'title' => '查看'],
|
||||
['name' => 'kefu/toolbar/add', 'title' => '增加'],
|
||||
['name' => 'kefu/toolbar/edit', 'title' => '编辑'],
|
||||
['name' => 'kefu/toolbar/del', 'title' => '删除'],
|
||||
['name' => 'kefu/toolbar/recyclebin', 'title' => '回收站'],
|
||||
['name' => 'kefu/toolbar/destroy', 'title' => '真实删除'],
|
||||
['name' => 'kefu/toolbar/restore', 'title' => '还原'],
|
||||
]
|
||||
],
|
||||
]
|
||||
]
|
||||
];
|
||||
Menu::create($menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件更新方法
|
||||
* @return bool
|
||||
*/
|
||||
public function upgrade()
|
||||
{
|
||||
// v1.0.2 审查聊天记录
|
||||
if (!Db::name('auth_rule')->where('name', 'kefu/record/sessionRecord')->value('id')) {
|
||||
$menu = [
|
||||
['name' => 'kefu/record/sessionRecord', 'title' => '审查聊天记录']
|
||||
];
|
||||
Menu::create($menu, 'kefu/record');
|
||||
}
|
||||
|
||||
// v1.0.3 知识库
|
||||
if (!Db::name('auth_rule')->where('name', 'kefu/kbs')->value('id')) {
|
||||
$menu = [
|
||||
[
|
||||
'name' => 'kefu/kbs',
|
||||
'title' => '知识库管理',
|
||||
'icon' => 'fa fa-circle-o',
|
||||
'sublist' => [
|
||||
['name' => 'kefu/kbs/index', 'title' => '查看'],
|
||||
['name' => 'kefu/kbs/add', 'title' => '增加'],
|
||||
['name' => 'kefu/kbs/edit', 'title' => '编辑'],
|
||||
['name' => 'kefu/kbs/del', 'title' => '删除'],
|
||||
['name' => 'kefu/kbs/multi', 'title' => '批量更新'],
|
||||
['name' => 'kefu/kbs/recyclebin', 'title' => '回收站'],
|
||||
['name' => 'kefu/kbs/destroy', 'title' => '真实删除'],
|
||||
['name' => 'kefu/kbs/restore', 'title' => '还原'],
|
||||
]
|
||||
]
|
||||
];
|
||||
Menu::create($menu, 'kefu');
|
||||
}
|
||||
|
||||
// v1.0.3 客服代表管理
|
||||
$kefu_csrkpi_menu = Db::name('auth_rule')->where('name', 'kefu/csrkpi')->find();
|
||||
if ($kefu_csrkpi_menu['title'] == '客服绩效报表') {
|
||||
Db::name('auth_rule')->where('name', 'kefu/csrkpi')->update(['title' => '客服代表管理']);
|
||||
}
|
||||
if (!Db::name('auth_rule')->where('name', 'kefu/csrkpi/add')->value('id')) {
|
||||
$menu = [
|
||||
['name' => 'kefu/csrkpi/add', 'title' => '添加'],
|
||||
['name' => 'kefu/csrkpi/edit', 'title' => '编辑'],
|
||||
['name' => 'kefu/csrkpi/del', 'title' => '删除'],
|
||||
];
|
||||
Menu::create($menu, 'kefu/csrkpi');
|
||||
}
|
||||
|
||||
// v1.0.4 窗口工具栏管理
|
||||
if (!Db::name('auth_rule')->where('name', 'kefu/toolbar')->value('id')) {
|
||||
$menu = [
|
||||
[
|
||||
'name' => 'kefu/toolbar',
|
||||
'title' => '窗口工具栏管理',
|
||||
'icon' => 'fa fa-circle-o',
|
||||
'remark' => '此功能用于管理会话窗口工具栏基本信息及状态,若需添加自定义工具,请先自行实现对应功能',
|
||||
'sublist' => [
|
||||
['name' => 'kefu/toolbar/index', 'title' => '查看'],
|
||||
['name' => 'kefu/toolbar/add', 'title' => '增加'],
|
||||
['name' => 'kefu/toolbar/edit', 'title' => '编辑'],
|
||||
['name' => 'kefu/toolbar/del', 'title' => '删除'],
|
||||
['name' => 'kefu/toolbar/recyclebin', 'title' => '回收站'],
|
||||
['name' => 'kefu/toolbar/destroy', 'title' => '真实删除'],
|
||||
['name' => 'kefu/toolbar/restore', 'title' => '还原'],
|
||||
]
|
||||
]
|
||||
];
|
||||
Menu::create($menu, 'kefu');
|
||||
}
|
||||
|
||||
// v1.0.6 修复客服配置功能权限分配bug
|
||||
if (!Db::name('auth_rule')->where('name', 'kefu/config/update')->value('id')) {
|
||||
$menu = [
|
||||
['name' => 'kefu/config/update', 'title' => '编辑']
|
||||
];
|
||||
Menu::create($menu, 'kefu/config');
|
||||
|
||||
Db::name('auth_rule')->where('name', 'kefu/config/index')->update([
|
||||
'title' => '查看'
|
||||
]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件卸载方法
|
||||
* @return bool
|
||||
*/
|
||||
public function uninstall()
|
||||
{
|
||||
Menu::delete('kefu');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件启用方法
|
||||
* @return bool
|
||||
*/
|
||||
public function enable()
|
||||
{
|
||||
$this->upgrade();
|
||||
Menu::enable('kefu');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件禁用方法
|
||||
* @return bool
|
||||
*/
|
||||
public function disable()
|
||||
{
|
||||
Menu::disable('kefu');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加命令
|
||||
*/
|
||||
public function appInit($param)
|
||||
{
|
||||
if (request()->isCli()) {
|
||||
\think\Console::addDefaultCommands([
|
||||
'addons\kefu\library\GatewayWorker\start'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
if (Config.modulename == 'admin' && Config.controllername == 'index' && Config.actionname == 'index') {
|
||||
|
||||
require.config({
|
||||
paths: {
|
||||
'kefu': '../addons/kefu/js/kefu'
|
||||
},
|
||||
shim: {
|
||||
'kefu': {
|
||||
deps: ['css!../addons/kefu/css/kefu_admin_default.css'],
|
||||
exports: 'KeFu'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
require(['kefu'], function (KeFu) {
|
||||
KeFu.initialize(document.domain, 'admin');
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
try {
|
||||
var parentConifg = window.parent.Config;
|
||||
} catch (err) {
|
||||
var parentConifg = false;
|
||||
}
|
||||
|
||||
if (parentConifg && parentConifg.modulename == 'admin') {
|
||||
// 监听后台iframe内的快捷键打开会话窗口
|
||||
$(document).on('keyup', function (event) {
|
||||
|
||||
if (window.parent.KeFu) {
|
||||
|
||||
// console.log('当前按钮的code-iframe内:', event.keyCode);
|
||||
|
||||
// 对打开会话窗口的监听
|
||||
// 打开会话窗口快捷键[ctrl + /],若需修改,请拿到对应键的keyCode替换下一行的191即可,191代表[/]键的keyCode
|
||||
if (event.keyCode === 191 && event.ctrlKey) {
|
||||
|
||||
if (window.parent.KeFu.last_sender) {
|
||||
if (parseInt(window.parent.KeFu.last_sender) === window.parent.KeFu.session_id) {
|
||||
// 展开分组
|
||||
if (!window.parent.KeFu.group_show.dialogue) {
|
||||
$('#heading_dialogue a').click();
|
||||
}
|
||||
} else {
|
||||
window.parent.KeFu.changeSession(window.parent.KeFu.last_sender);
|
||||
window.parent.KeFu.last_sender = null;
|
||||
}
|
||||
} else if (window.parent.KeFu.window_is_show) {
|
||||
window.parent.KeFu.toggle_window('hide');
|
||||
}
|
||||
|
||||
if (!window.parent.KeFu.window_is_show) {
|
||||
window.parent.KeFu.toggle_window('show');
|
||||
}
|
||||
return ;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
require.config({
|
||||
paths: {
|
||||
'kefu': '../addons/kefu/js/kefu'
|
||||
},
|
||||
shim: {
|
||||
'kefu': {
|
||||
deps: ['css!../addons/kefu/css/kefu_default.css'],
|
||||
exports: 'KeFu'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
require(['kefu'], function (KeFu) {
|
||||
KeFu.initialize(document.domain, 'index');
|
||||
});
|
||||
}
|
||||
}
|
||||
Executable
+155
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
[
|
||||
'name' => '__tips__',
|
||||
'title' => '温馨提示',
|
||||
'type' => 'string',
|
||||
'content' => [],
|
||||
'value' => '1. <b><font color="red">本窗口的所有配置项,非技术人员不建议进行调整</font></b><br>'."\n"
|
||||
.' 2. 若需开启wss协议,请先配置<b>ssl证书</b>、<b>ssl证书KEY</b>并重启Workerman服务,才会生效;https站点必须配置wss<br>'."\n"
|
||||
.' 3. 仅需对外开放<b>WebSocket端口</b>,另外的两项端口<b>未被占用</b>即可<br>'."\n"
|
||||
.' 4. 消息提醒铃声建议大小100kb,文件无法上传请参考插件文档<a target="_blank" href="https://doc.fastadmin.net/kefu/38.html">常见问题</a>',
|
||||
'rule' => '',
|
||||
'msg' => '',
|
||||
'tip' => '',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
[
|
||||
'name' => 'wss_switch',
|
||||
'title' => 'wss协议',
|
||||
'type' => 'radio',
|
||||
'content' => [
|
||||
'不开启',
|
||||
'开启',
|
||||
],
|
||||
'value' => '1',
|
||||
'rule' => '',
|
||||
'tip' => '请先参考常见问题配置好wss服务再开启,否则将无法链接',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
[
|
||||
'name' => 'ssl_cert',
|
||||
'title' => 'ssl证书',
|
||||
'type' => 'string',
|
||||
'content' => [],
|
||||
'value' => '/www/wwwroot/jyshd/cert/server.pem',
|
||||
'rule' => '',
|
||||
'tip' => '请填写证书pem或crt文件的绝对路径',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
[
|
||||
'name' => 'ssl_cert_key',
|
||||
'title' => 'ssl证书KEY',
|
||||
'type' => 'string',
|
||||
'content' => [],
|
||||
'value' => '/www/wwwroot/jyshd/cert/server.key',
|
||||
'rule' => '',
|
||||
'tip' => '请填写证书密匙(key)文件的绝对路径',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
[
|
||||
'name' => 'websocket_port',
|
||||
'title' => 'WebSocket端口',
|
||||
'type' => 'string',
|
||||
'content' => [],
|
||||
'value' => '11818',
|
||||
'rule' => 'required,range(1024~65535)',
|
||||
'tip' => '请在安全组、防火墙等开放此端口',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
[
|
||||
'name' => 'register_port',
|
||||
'title' => '服务注册端口',
|
||||
'type' => 'string',
|
||||
'content' => [],
|
||||
'value' => '1819',
|
||||
'rule' => 'required,range(1024~65535)',
|
||||
'tip' => '无需对外开放,属未被占用的端口即可',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
[
|
||||
'name' => 'internal_start_port',
|
||||
'title' => '内部通讯起始端口',
|
||||
'type' => 'string',
|
||||
'content' => [],
|
||||
'value' => '3200',
|
||||
'rule' => 'required,range(1024~65535)',
|
||||
'tip' => '无需对外开放,属未被占用的端口即可',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
[
|
||||
'name' => 'gateway_process_number',
|
||||
'title' => 'Gateway进程数',
|
||||
'type' => 'string',
|
||||
'content' => [],
|
||||
'value' => '1',
|
||||
'rule' => '',
|
||||
'tip' => '设置为CPU核数相等的数量性能最好',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
[
|
||||
'name' => 'worker_process_number',
|
||||
'title' => 'BusinessWorker进程数',
|
||||
'type' => 'string',
|
||||
'content' => [],
|
||||
'value' => '2',
|
||||
'rule' => '',
|
||||
'tip' => '根据业务有无阻塞式IO,设为CPU核数的1-3倍',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
[
|
||||
'name' => 'ringing',
|
||||
'title' => '消息提醒铃声',
|
||||
'type' => 'file',
|
||||
'content' => [],
|
||||
'value' => '/assets/addons/kefu/audio/message_prompt.wav',
|
||||
'rule' => 'required,file',
|
||||
'tip' => '',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
[
|
||||
'name' => 'theme',
|
||||
'title' => '主题模板',
|
||||
'type' => 'string',
|
||||
'content' => [],
|
||||
'value' => 'default',
|
||||
'rule' => 'required',
|
||||
'msg' => '',
|
||||
'tip' => '请确保addons/kefu/view有相应的目录',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
[
|
||||
'name' => 'allow_domain',
|
||||
'title' => '跨站调用允许域名',
|
||||
'type' => 'text',
|
||||
'content' => [],
|
||||
'value' => '*',
|
||||
'rule' => '',
|
||||
'tip' => '一行一个,未在列表内的外站无法引用插件',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
[
|
||||
'name' => 'rule_out_url',
|
||||
'title' => '以下页面不启动<br>(一行一个)',
|
||||
'type' => 'text',
|
||||
'content' => [],
|
||||
'value' => 'http://kefu_local.com/index/user/index.html?tip=自动排除对应https地址、带参数地址,此处的URL带参数无效',
|
||||
'rule' => '',
|
||||
'tip' => '这些前台页面将不启动在线客服',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
];
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace addons\kefu\controller;
|
||||
|
||||
use think\addons\Controller;
|
||||
use think\Request;
|
||||
|
||||
class Base extends Controller
|
||||
{
|
||||
|
||||
public function __construct(Request $request = null)
|
||||
{
|
||||
parent::__construct($request);
|
||||
$config = get_addon_config('kefu');
|
||||
// 设定主题模板目录
|
||||
$this->view->engine->config('view_path', $this->view->engine->config('view_path') . trim($config['theme']) . DS);
|
||||
}
|
||||
|
||||
protected function _initialize()
|
||||
{
|
||||
parent::_initialize();
|
||||
}
|
||||
|
||||
}
|
||||
Executable
+493
@@ -0,0 +1,493 @@
|
||||
<?php
|
||||
|
||||
namespace addons\kefu\controller;
|
||||
|
||||
use addons\kefu\library\Common;
|
||||
use fast\Random;
|
||||
use think\Config;
|
||||
use think\Db;
|
||||
use think\Cookie;
|
||||
|
||||
class Index extends Base
|
||||
{
|
||||
protected $noNeedLogin = ['initialize', 'loadMessagePrompt', 'upload', 'mobile', 'index'];
|
||||
|
||||
protected $chat_config;
|
||||
|
||||
protected $token_info = false;// 用户、游客、管理员的资料
|
||||
|
||||
protected $token_list = [];// 要发送给前台的token(前台利用这些token链接websocket)
|
||||
|
||||
protected $referrer = '';
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->view->assign('chat_name', $this->chat_config['chat_name']);
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
public function mobile()
|
||||
{
|
||||
$this->view->assign('toolbar', $this->chat_config['toolbar']);// 工具栏配置
|
||||
$this->view->assign('chat_name', $this->chat_config['chat_name']);
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置、初始化AJAX请求过来的用户的身份等
|
||||
*/
|
||||
public function _initialize()
|
||||
{
|
||||
parent::_initialize();
|
||||
$this->referrer = Common::trajectoryAnalysis($this->request->get('referrer'));
|
||||
|
||||
// 读取工具栏
|
||||
$toolbar_temp = Db::name('kefu_toolbar')->where('status', 1)->where('deletetime', null)->select();
|
||||
// 以mark为键
|
||||
foreach ($toolbar_temp as $key => $value) {
|
||||
$value['icon_image'] = Common::imgSrcFill($value['icon_image'], false);
|
||||
$toolbar['toolbar'][$value['mark']] = $value;
|
||||
}
|
||||
|
||||
$this->chat_config = Db::name('kefu_config')->column('name,value');
|
||||
$kefu_config = get_addon_config('kefu');
|
||||
$kefu_config['__CDN__'] = config('view_replace_str.__CDN__');
|
||||
$kefu_config['__CDN__'] = $kefu_config['__CDN__'] ? $kefu_config['__CDN__'] : $this->request->domain();
|
||||
|
||||
// 上传配置
|
||||
$upload['upload'] = \app\common\model\Config::upload();
|
||||
// 上传信息配置后
|
||||
\think\Hook::listen("upload_config_init", $upload['upload']);
|
||||
if ($upload['upload']['storage'] != 'local') {
|
||||
$upload['upload']['cdnurl'] = $upload['upload']['cdnurl'] ? $upload['upload']['cdnurl'] : cdnurl('', true);
|
||||
$upload['upload']['uploadurl'] = preg_match('/^http(s)?:\/\//', $upload['upload']['uploadurl']) ? $upload['upload']['uploadurl'] : $this->request->domain() . $upload['upload']['uploadurl'];
|
||||
} else {
|
||||
$upload['upload']['cdnurl'] = $this->request->domain();
|
||||
$upload['upload']['uploadurl'] = addon_url('kefu/index/upload', [], '', true);
|
||||
}
|
||||
$this->chat_config = array_merge($this->chat_config, $kefu_config, $upload, $toolbar);
|
||||
unset($toolbar_temp);
|
||||
unset($toolbar);
|
||||
|
||||
// 跨域配置
|
||||
$allow_domain = explode(PHP_EOL, trim($this->chat_config['allow_domain'], PHP_EOL));
|
||||
$allow_domain = array_map("trim", $allow_domain);// 去除每个域名前后的空格
|
||||
$http_origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : false;
|
||||
|
||||
if ($http_origin && (in_array('*', $allow_domain) || in_array($http_origin, $allow_domain))) {
|
||||
header('Vary: Origin');
|
||||
header('Access-Control-Allow-Origin: ' . $http_origin);
|
||||
header('Access-Control-Allow-Credentials: true');
|
||||
header('Access-Control-Max-Age: 86400');
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
|
||||
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) {
|
||||
header("Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS");
|
||||
}
|
||||
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) {
|
||||
header("Access-Control-Allow-Headers: {$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}");
|
||||
}
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
if (isset($_SERVER['HTTP_ORIGIN']) && preg_replace('/^http(s)?:\/\//', '', $http_origin) != $_SERVER['HTTP_HOST']) {
|
||||
// $this->result(null, 0, '您的站点未被允许调用' . $this->chat_config['chat_name'], 'json');
|
||||
}
|
||||
}
|
||||
|
||||
// 页面排除
|
||||
if ($this->chat_config['rule_out_url'] && isset($_SERVER['HTTP_REFERER'])) {
|
||||
|
||||
$http_referer = $this->urlDealWith($_SERVER['HTTP_REFERER']);
|
||||
$rule_out_url = explode(PHP_EOL, $this->chat_config['rule_out_url']);
|
||||
|
||||
foreach ($rule_out_url as $key => $value) {
|
||||
if ($this->urlDealWith($value) == $http_referer) {
|
||||
$this->result(null, 401, $this->chat_config['chat_name'] . ' 当前页面被设置为不启动', 'json');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 用户登录
|
||||
$data = $this->request->only(['modulename', 'token', 'kefu_tourists_token']);
|
||||
$this->token_list['kefu_tourists_token'] = Cookie::get('kefu_user');
|
||||
if (!$this->token_list['kefu_tourists_token'] && isset($data['kefu_tourists_token'])) {
|
||||
$this->token_list['kefu_tourists_token'] = $data['kefu_tourists_token'];
|
||||
}
|
||||
|
||||
if (!isset($data['modulename'])) {
|
||||
if ($this->request->action() == 'mobile' || $this->request->action() == 'index') {
|
||||
$data['modulename'] = 'index';
|
||||
} else {
|
||||
$this->result(null, 0, $this->chat_config['chat_name'] . ' 模块未知' . $this->request->action(), 'json');
|
||||
}
|
||||
}
|
||||
|
||||
if ($data['modulename'] == 'admin') {
|
||||
|
||||
// 验证管理员身份
|
||||
$auth = \app\admin\library\Auth::instance();
|
||||
if ($auth->isLogin()) {
|
||||
$this->token_info = Common::checkAdmin(false, $auth->id);
|
||||
|
||||
if ($this->token_info) {
|
||||
|
||||
// workerman 中不支持 PHP session和cookie,所以 $auth 类失效
|
||||
// 此处对管理员 token 加密稍作修改,供客服自动登录使用
|
||||
$keeptime = 864000;
|
||||
$expiretime = time() + $keeptime;
|
||||
|
||||
// 原规则为单纯的id,若需修改附加的字符串,请将`Common::checkAdmin`方法里边的附加字符串一起修改
|
||||
$sign = $this->token_info['id'] . 'kefu_admin_sign_additional';
|
||||
|
||||
$key = md5(md5($sign) . md5($keeptime) . md5($expiretime) . $this->token_info['token']);
|
||||
$cookie_data = [$this->token_info['id'], $keeptime, $expiretime, $key];
|
||||
$this->token_list['kefu_token'] = implode('|', $cookie_data);
|
||||
unset($this->token_info['token']);
|
||||
}
|
||||
}
|
||||
|
||||
// 清理轨迹
|
||||
switch ($this->chat_config['trajectory_save_cycle']) {
|
||||
case 0:
|
||||
$where_time = 604800; // 清理7天前的
|
||||
break;
|
||||
case 1:
|
||||
$where_time = 2592000; // 30天
|
||||
break;
|
||||
case 2:
|
||||
$where_time = 5184000; // 60天
|
||||
break;
|
||||
|
||||
default:
|
||||
$where_time = false; // 不清理
|
||||
break;
|
||||
}
|
||||
|
||||
if ($where_time) {
|
||||
Db::name('kefu_trajectory')->where('createtime', '<', time() - $where_time)->delete();
|
||||
}
|
||||
|
||||
} elseif ($data['modulename'] != 'admin') {
|
||||
// 验证用户身份
|
||||
$auth = \app\common\library\Auth::instance();
|
||||
$token = Cookie::get('token');
|
||||
$token = (!$token && isset($data['token'])) ? $data['token'] : $token;
|
||||
|
||||
if ($token) {
|
||||
$auth->init($token);
|
||||
if ($auth->isLogin()) {
|
||||
$this->token_info = Common::checkKefuUser($this->token_list['kefu_tourists_token'], $auth->id);
|
||||
|
||||
if (!$this->token_info && !$this->token_list['kefu_tourists_token']) {
|
||||
// 该用户未绑定游客,建立游客身份并绑定
|
||||
$tourists = Common::createTourists($this->referrer . ' IP:' . $this->request->ip());
|
||||
if ($tourists) {
|
||||
$this->token_list['kefu_tourists_token'] = $tourists['kefu_user_cookie'];
|
||||
Cookie::set('kefu_user', $tourists['kefu_user_cookie'], 315360000);
|
||||
} else {
|
||||
$this->result(null, 401, $this->chat_config['chat_name'] . ' 游客创建失败!', 'json');
|
||||
}
|
||||
|
||||
$this->token_info = Common::checkKefuUser($this->token_list['kefu_tourists_token'], $auth->id);
|
||||
}
|
||||
|
||||
// workerman 中不支持 PHP session和cookie,所以 $auth 类失效
|
||||
// 在开启 $cookie_httponly 时,对用户的 token 加密稍作修改,供客服自动登录使用
|
||||
if ($this->token_info) {
|
||||
$cookie_httponly = config('cookie.httponly');
|
||||
if (!$cookie_httponly) {
|
||||
$this->token_list['kefu_token'] = $token;
|
||||
} else {
|
||||
|
||||
// 若需修改附加的字符串,请将`Events::onWebSocketConnect`方法里边的附加字符串一起修改
|
||||
// 先用 user_token 数据表中的token字段同样的加密算法对token进行加密,否则workerman无法识别用户身份
|
||||
$sign = Common::getEncryptedToken($token) . 'kefu_user_sign_additional';
|
||||
$key = md5(md5($auth->id) . md5($sign));
|
||||
$cookie_data = [$auth->id, $key];
|
||||
$this->token_list['kefu_token'] = implode('|', $cookie_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->token_list['kefu_tourists_token'] && !$this->token_info) {
|
||||
$tourists = Common::createTourists($this->referrer . ' IP:' . $this->request->ip());
|
||||
if ($tourists) {
|
||||
$this->token_list['kefu_tourists_token'] = $tourists['kefu_user_cookie'];
|
||||
Cookie::set('kefu_user', $tourists['kefu_user_cookie'], 315360000);
|
||||
} else {
|
||||
$this->result(null, 401, $this->chat_config['chat_name'] . ' 游客创建失败!', 'json');
|
||||
}
|
||||
}
|
||||
|
||||
if ($data['modulename'] != 'admin' && $this->token_list['kefu_tourists_token'] && !$this->token_info) {
|
||||
// 验证游客用户身份
|
||||
$this->token_info = Common::checkKefuUser($this->token_list['kefu_tourists_token'], 0);
|
||||
}
|
||||
|
||||
$this->view->assign('cdnurl', $kefu_config['__CDN__']);
|
||||
}
|
||||
|
||||
public function initialize()
|
||||
{
|
||||
$res_data = [];
|
||||
$data = $this->request->only(['modulename']);
|
||||
$current_url = $this->request->header('referer');
|
||||
|
||||
if ($this->token_info) {
|
||||
if (isset($this->token_info['blacklist']) && $this->token_info['blacklist']) {
|
||||
$this->result(null, 401, '!', 'json');// 黑名单用户
|
||||
}
|
||||
} else {
|
||||
$this->result(null, 401, $this->chat_config['chat_name'] . ' 无法识别用户!', 'json');
|
||||
}
|
||||
|
||||
$this->view->assign('toolbar', $this->chat_config['toolbar']);// 工具栏配置
|
||||
|
||||
if ($data['modulename'] == 'admin') {
|
||||
|
||||
if (isset($this->chat_config['toolbar']['fastreply'])) {
|
||||
// 快捷回复
|
||||
$fast_reply = Db::name('kefu_fast_reply')
|
||||
->where('admin_id=' . $this->token_info['id'] . ' OR admin_id=0')
|
||||
->where('status', '1')
|
||||
->where('deletetime', null)
|
||||
->select();
|
||||
|
||||
$fast_reply_temp = [];
|
||||
foreach ($fast_reply as $key => $value) {
|
||||
$fast_reply_temp[$value['id']] = $value;
|
||||
}
|
||||
unset($fast_reply);
|
||||
$res_data['fast_reply'] = $fast_reply_temp;
|
||||
|
||||
$this->view->assign('fast_reply', $fast_reply_temp);
|
||||
}
|
||||
|
||||
$res_data['window_html'] = $this->view->fetch(ROOT_PATH . 'addons/kefu/view/' . trim($this->chat_config['theme']) . '/modaltpl/admin.html');
|
||||
} else {
|
||||
|
||||
$this->chat_config['invite_box_img'] = $this->chat_config['invite_box_img'] ? Common::imgSrcFill($this->chat_config['invite_box_img'], false) : false;
|
||||
$this->chat_config['auto_invitation_switch'] = ($this->chat_config['invite_box_img']) ? $this->chat_config['auto_invitation_switch'] : 0;
|
||||
|
||||
// 只在有客服在线时弹出邀请框
|
||||
if ($this->chat_config['only_csr_online_invitation'] && $this->chat_config['auto_invitation_switch']) {
|
||||
$online_csr = Db::name('kefu_csr_config')
|
||||
->where('status', 3)
|
||||
->value('admin_id');
|
||||
$this->chat_config['auto_invitation_switch'] = $online_csr ? $this->chat_config['auto_invitation_switch'] : 0;
|
||||
}
|
||||
|
||||
// 前台轮播图
|
||||
$this->chat_config['slider_images'] = $this->chat_config['slider_images'] ? explode(',', trim($this->chat_config['slider_images'], ',')) : [];
|
||||
foreach ($this->chat_config['slider_images'] as $key => $value) {
|
||||
$this->chat_config['slider_images'][$key] = Common::imgSrcFill($value, false);
|
||||
}
|
||||
|
||||
$res_data['window_html'] = $this->view->fetch(ROOT_PATH . 'addons/kefu/view/' . trim($this->chat_config['theme']) . '/modaltpl/index.html');
|
||||
}
|
||||
|
||||
// 记录轨迹
|
||||
if (isset($this->token_info['trajectory'])) {
|
||||
// 用户轨迹
|
||||
$trajectory = [
|
||||
'user_id' => $this->token_info['id'],
|
||||
'csr_id' => $this->token_info['trajectory']['csr_id'],
|
||||
'log_type' => 0,
|
||||
'note' => $this->token_info['trajectory']['note'],
|
||||
'url' => $current_url,
|
||||
'referrer' => $this->referrer,
|
||||
'createtime' => time(),
|
||||
];
|
||||
|
||||
Db::name('kefu_trajectory')->insert($trajectory);
|
||||
}
|
||||
|
||||
// 窗口抖动配置处理
|
||||
$this->chat_config['is_shake'] = false;
|
||||
if ($this->chat_config['new_message_shake'] == 3) {
|
||||
$this->chat_config['is_shake'] = true;
|
||||
} elseif ($this->chat_config['new_message_shake'] == 1 && $data['modulename'] != 'admin') {
|
||||
$this->chat_config['is_shake'] = true;
|
||||
} elseif ($this->chat_config['new_message_shake'] == 2 && $data['modulename'] == 'admin') {
|
||||
$this->chat_config['is_shake'] = true;
|
||||
}
|
||||
|
||||
// 配置排除
|
||||
$except_config = [
|
||||
'allow_domain',
|
||||
'wechat_app_id',
|
||||
'wechat_app_secret',
|
||||
'wechat_encodingkey',
|
||||
'wechat_token',
|
||||
'worker_process_number',
|
||||
'csr_admin',
|
||||
'csr_distribution',
|
||||
'register_port',
|
||||
'gateway_process_number',
|
||||
'internal_start_port',
|
||||
'kbs_switch',
|
||||
'trajectory_save_cycle',
|
||||
'ssl_cert',
|
||||
'ssl_cert_key'
|
||||
];
|
||||
foreach ($except_config as $key => $value) {
|
||||
if (in_array($value, $except_config)) {
|
||||
unset($this->chat_config[$value]);
|
||||
}
|
||||
}
|
||||
|
||||
// 防止商品和订单卡片暴露后台地址
|
||||
if ($this->token_info['source'] != 'csr') {
|
||||
unset($this->chat_config['toolbar']['goods']['card_url']);
|
||||
unset($this->chat_config['toolbar']['order']['card_url']);
|
||||
}
|
||||
|
||||
$res_data['user_info'] = $this->token_info;
|
||||
$res_data['new_msg'] = Common::getUnreadMessages($this->token_info['user_id']);
|
||||
$this->chat_config['modulename'] = $data['modulename'];
|
||||
$res_data['config'] = $this->chat_config;
|
||||
$res_data['token_list'] = $this->token_list;
|
||||
$this->result($res_data, 1, 'ok', 'json');
|
||||
}
|
||||
|
||||
/**
|
||||
* 去除URL中的 index.php、https://、http://、去除参数
|
||||
* @param [type] $url [description]
|
||||
* @return string 处理结果
|
||||
*/
|
||||
private function urlDealWith($url)
|
||||
{
|
||||
$url = explode('?', $url);
|
||||
$url = isset($url[0]) ? $url[0] : ''; // 只要 ? 号前的字符串
|
||||
return str_replace(['http://', 'https://'], '', trim($url));
|
||||
}
|
||||
|
||||
/**
|
||||
* 供跨站下载来信提示音文件(未使用云存储)
|
||||
*/
|
||||
public function loadMessagePrompt()
|
||||
{
|
||||
$file = ROOT_PATH . 'public' . $this->chat_config['ringing'];
|
||||
header("Content-type:application/octet-stream");
|
||||
$filename = basename($file);
|
||||
header("Content-Disposition:attachment;filename = " . $filename);
|
||||
header("Accept-ranges:bytes");
|
||||
header("Accept-length:" . filesize($file));
|
||||
readfile($file);
|
||||
}
|
||||
|
||||
public function upload()
|
||||
{
|
||||
$file = $this->request->file('file');
|
||||
if (empty($file)) {
|
||||
$this->result(null, 0, '没有文件被上传或上传超过限制', 'json');
|
||||
}
|
||||
|
||||
//判断是否已经存在附件
|
||||
$sha1 = $file->hash();
|
||||
$extparam = $this->request->post();
|
||||
|
||||
$upload = Config::get('upload');
|
||||
|
||||
preg_match('/(\d+)(\w+)/', $upload['maxsize'], $matches);
|
||||
$type = strtolower($matches[2]);
|
||||
$typeDict = ['b' => 0, 'k' => 1, 'kb' => 1, 'm' => 2, 'mb' => 2, 'gb' => 3, 'g' => 3];
|
||||
$size = (int)$upload['maxsize'] * pow(1024, isset($typeDict[$type]) ? $typeDict[$type] : 0);
|
||||
$fileInfo = $file->getInfo();
|
||||
$suffix = strtolower(pathinfo($fileInfo['name'], PATHINFO_EXTENSION));
|
||||
$suffix = $suffix ? $suffix : 'file';
|
||||
|
||||
$mimetypeArr = explode(',', strtolower($upload['mimetype']));
|
||||
$typeArr = explode('/', $fileInfo['type']);
|
||||
|
||||
//禁止上传PHP和HTML文件
|
||||
if (in_array($fileInfo['type'], ['text/x-php', 'text/html']) || in_array($suffix, ['php', 'html', 'htm'])) {
|
||||
$this->error(__('上传格式限制'));
|
||||
}
|
||||
|
||||
//验证文件后缀
|
||||
if ($upload['mimetype'] !== '*' && (!in_array($suffix, $mimetypeArr) || (stripos($typeArr[0] . '/', $upload['mimetype']) !== false && (!in_array($fileInfo['type'], $mimetypeArr) && !in_array($typeArr[0] . '/*', $mimetypeArr))))) {
|
||||
$this->error(__('上传格式限制'));
|
||||
}
|
||||
|
||||
//验证是否为图片文件
|
||||
$imagewidth = $imageheight = 0;
|
||||
if (in_array($fileInfo['type'], [
|
||||
'image/gif',
|
||||
'image/jpg',
|
||||
'image/jpeg',
|
||||
'image/bmp',
|
||||
'image/png',
|
||||
'image/webp'
|
||||
]) || in_array($suffix, ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'webp'])) {
|
||||
$imgInfo = getimagesize($fileInfo['tmp_name']);
|
||||
if (!$imgInfo || !isset($imgInfo[0]) || !isset($imgInfo[1])) {
|
||||
$this->error(__('上传的文件不是图片'));
|
||||
}
|
||||
$imagewidth = isset($imgInfo[0]) ? $imgInfo[0] : $imagewidth;
|
||||
$imageheight = isset($imgInfo[1]) ? $imgInfo[1] : $imageheight;
|
||||
}
|
||||
|
||||
$replaceArr = [
|
||||
'{year}' => date("Y"),
|
||||
'{mon}' => date("m"),
|
||||
'{day}' => date("d"),
|
||||
'{hour}' => date("H"),
|
||||
'{min}' => date("i"),
|
||||
'{sec}' => date("s"),
|
||||
'{random}' => Random::alnum(16),
|
||||
'{random32}' => Random::alnum(32),
|
||||
'{filename}' => $suffix ? substr($fileInfo['name'], 0, strripos($fileInfo['name'], '.')) : $fileInfo['name'],
|
||||
'{suffix}' => $suffix,
|
||||
'{.suffix}' => $suffix ? '.' . $suffix : '',
|
||||
'{filemd5}' => md5_file($fileInfo['tmp_name']),
|
||||
];
|
||||
$savekey = $upload['savekey'];
|
||||
$savekey = str_replace(array_keys($replaceArr), array_values($replaceArr), $savekey);
|
||||
|
||||
$uploadDir = substr($savekey, 0, strripos($savekey, '/') + 1);
|
||||
$fileName = substr($savekey, strripos($savekey, '/') + 1);
|
||||
|
||||
$splInfo = $file->validate(['size' => $size])->move(ROOT_PATH . '/public' . $uploadDir, $fileName);
|
||||
if ($splInfo) {
|
||||
$admin_id = 0;
|
||||
$user_id = 0;
|
||||
|
||||
if ($this->token_info) {
|
||||
$user_info = Common::userInfo($this->token_info['user_id']);
|
||||
|
||||
if ($user_info['session_type'] == 0) {
|
||||
$user_id = $user_info['id'];
|
||||
} elseif ($user_info['session_type'] == 1) {
|
||||
$admin_id = $user_info['id'];
|
||||
}
|
||||
}
|
||||
|
||||
$params = [
|
||||
'admin_id' => $admin_id,
|
||||
'user_id' => $user_id,
|
||||
'filesize' => $fileInfo['size'],
|
||||
'imagewidth' => $imagewidth,
|
||||
'imageheight' => $imageheight,
|
||||
'imagetype' => $suffix,
|
||||
'imageframes' => 0,
|
||||
'mimetype' => $fileInfo['type'],
|
||||
'url' => $uploadDir . $splInfo->getSaveName(),
|
||||
'uploadtime' => time(),
|
||||
'storage' => 'local',
|
||||
'sha1' => $sha1,
|
||||
'extparam' => json_encode($extparam),
|
||||
];
|
||||
$attachment = model("common/attachment");
|
||||
$attachment->data(array_filter($params));
|
||||
$attachment->save();
|
||||
\think\Hook::listen("upload_after", $attachment);
|
||||
$this->result(['url' => $uploadDir . $splInfo->getSaveName()], 1, null, 'json');
|
||||
} else {
|
||||
// 上传失败获取错误信息
|
||||
$this->result(null, 1, $file->getError(), 'json');
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
<script>
|
||||
export default {
|
||||
onLaunch: function() {
|
||||
},
|
||||
onShow: function() {
|
||||
},
|
||||
onHide: function() {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/*每个页面公共css */
|
||||
</style>
|
||||
@@ -0,0 +1,633 @@
|
||||
<template>
|
||||
<view>
|
||||
<slot v-if="!nodes.length" />
|
||||
<!--#ifdef APP-PLUS-NVUE-->
|
||||
<web-view id="_top" ref="web" :style="'margin-top:-2px;height:'+height+'px'" @onPostMessage="_message" />
|
||||
<!--#endif-->
|
||||
<!--#ifndef APP-PLUS-NVUE-->
|
||||
<view id="_top" :style="showAm+(selectable?';user-select:text;-webkit-user-select:text':'')">
|
||||
<!--#ifdef H5 || MP-360-->
|
||||
<div :id="'rtf'+uid"></div>
|
||||
<!--#endif-->
|
||||
<!--#ifndef H5 || MP-360-->
|
||||
<trees :nodes="nodes" :lazyLoad="lazyLoad" :loading="loadingImg" />
|
||||
<!--#endif-->
|
||||
</view>
|
||||
<!--#endif-->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// #ifndef H5 || APP-PLUS-NVUE || MP-360
|
||||
import trees from './libs/trees';
|
||||
var cache = {},
|
||||
// #ifdef MP-WEIXIN || MP-TOUTIAO
|
||||
fs = uni.getFileSystemManager ? uni.getFileSystemManager() : null,
|
||||
// #endif
|
||||
Parser = require('./libs/MpHtmlParser.js');
|
||||
var dom;
|
||||
// 计算 cache 的 key
|
||||
function hash(str) {
|
||||
for (var i = str.length, val = 5381; i--;)
|
||||
val += (val << 5) + str.charCodeAt(i);
|
||||
return val;
|
||||
}
|
||||
// #endif
|
||||
// #ifdef H5 || APP-PLUS-NVUE || MP-360
|
||||
var rpx = uni.getSystemInfoSync().windowWidth / 750,
|
||||
cfg = require('./libs/config.js');
|
||||
// #endif
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
var weexDom = weex.requireModule('dom');
|
||||
// #endif
|
||||
/**
|
||||
* Parser 富文本组件
|
||||
* @tutorial https://github.com/jin-yufeng/Parser
|
||||
* @property {String} html 富文本数据
|
||||
* @property {Boolean} autopause 是否在播放一个视频时自动暂停其他视频
|
||||
* @property {Boolean} autoscroll 是否自动给所有表格添加一个滚动层
|
||||
* @property {Boolean} autosetTitle 是否自动将 title 标签中的内容设置到页面标题
|
||||
* @property {Number} compress 压缩等级
|
||||
* @property {String} domain 图片、视频等链接的主域名
|
||||
* @property {Boolean} lazyLoad 是否开启图片懒加载
|
||||
* @property {String} loadingImg 图片加载完成前的占位图
|
||||
* @property {Boolean} selectable 是否开启长按复制
|
||||
* @property {Object} tagStyle 标签的默认样式
|
||||
* @property {Boolean} showWithAnimation 是否使用渐显动画
|
||||
* @property {Boolean} useAnchor 是否使用锚点
|
||||
* @property {Boolean} useCache 是否缓存解析结果
|
||||
* @event {Function} parse 解析完成事件
|
||||
* @event {Function} load dom 加载完成事件
|
||||
* @event {Function} ready 所有图片加载完毕事件
|
||||
* @event {Function} error 错误事件
|
||||
* @event {Function} imgtap 图片点击事件
|
||||
* @event {Function} linkpress 链接点击事件
|
||||
* @author JinYufeng
|
||||
* @version 20200615
|
||||
* @listens MIT
|
||||
*/
|
||||
export default {
|
||||
name: 'parser',
|
||||
data() {
|
||||
return {
|
||||
// #ifdef H5 || MP-360
|
||||
uid: this._uid,
|
||||
// #endif
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
height: 1,
|
||||
// #endif
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
showAm: '',
|
||||
// #endif
|
||||
nodes: []
|
||||
}
|
||||
},
|
||||
// #ifndef H5 || APP-PLUS-NVUE || MP-360
|
||||
components: {
|
||||
trees
|
||||
},
|
||||
// #endif
|
||||
props: {
|
||||
html: String,
|
||||
autopause: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
autoscroll: Boolean,
|
||||
autosetTitle: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// #ifndef H5 || APP-PLUS-NVUE || MP-360
|
||||
compress: Number,
|
||||
loadingImg: String,
|
||||
useCache: Boolean,
|
||||
// #endif
|
||||
domain: String,
|
||||
lazyLoad: Boolean,
|
||||
selectable: Boolean,
|
||||
tagStyle: Object,
|
||||
showWithAnimation: Boolean,
|
||||
useAnchor: Boolean
|
||||
},
|
||||
watch: {
|
||||
html(html) {
|
||||
this.setContent(html);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 图片数组
|
||||
this.imgList = [];
|
||||
this.imgList.each = function(f) {
|
||||
for (var i = 0, len = this.length; i < len; i++)
|
||||
this.setItem(i, f(this[i], i, this));
|
||||
}
|
||||
this.imgList.setItem = function(i, src) {
|
||||
if (i == void 0 || !src) return;
|
||||
// #ifndef MP-ALIPAY || APP-PLUS
|
||||
// 去重
|
||||
if (src.indexOf('http') == 0 && this.includes(src)) {
|
||||
var newSrc = src.split('://')[0];
|
||||
for (var j = newSrc.length, c; c = src[j]; j++) {
|
||||
if (c == '/' && src[j - 1] != '/' && src[j + 1] != '/') break;
|
||||
newSrc += Math.random() > 0.5 ? c.toUpperCase() : c;
|
||||
}
|
||||
newSrc += src.substr(j);
|
||||
return this[i] = newSrc;
|
||||
}
|
||||
// #endif
|
||||
this[i] = src;
|
||||
// 暂存 data src
|
||||
if (src.includes('data:image')) {
|
||||
var filePath, info = src.match(/data:image\/(\S+?);(\S+?),(.+)/);
|
||||
if (!info) return;
|
||||
// #ifdef MP-WEIXIN || MP-TOUTIAO
|
||||
filePath = `${wx.env.USER_DATA_PATH}/${Date.now()}.${info[1]}`;
|
||||
fs && fs.writeFile({
|
||||
filePath,
|
||||
data: info[3],
|
||||
encoding: info[2],
|
||||
success: () => this[i] = filePath
|
||||
})
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
filePath = `_doc/parser_tmp/${Date.now()}.${info[1]}`;
|
||||
var bitmap = new plus.nativeObj.Bitmap();
|
||||
bitmap.loadBase64Data(src, () => {
|
||||
bitmap.save(filePath, {}, () => {
|
||||
bitmap.clear()
|
||||
this[i] = filePath;
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// #ifdef H5 || MP-360
|
||||
this.document = document.getElementById('rtf' + this._uid);
|
||||
// #endif
|
||||
// #ifndef H5 || APP-PLUS-NVUE || MP-360
|
||||
if (dom) this.document = new dom(this);
|
||||
// #endif
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
this.document = this.$refs.web;
|
||||
setTimeout(() => {
|
||||
// #endif
|
||||
if (this.html) this.setContent(this.html);
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
}, 30)
|
||||
// #endif
|
||||
},
|
||||
beforeDestroy() {
|
||||
// #ifdef H5 || MP-360
|
||||
if (this._observer) this._observer.disconnect();
|
||||
// #endif
|
||||
this.imgList.each(src => {
|
||||
// #ifdef APP-PLUS
|
||||
if (src && src.includes('_doc')) {
|
||||
plus.io.resolveLocalFileSystemURL(src, entry => {
|
||||
entry.remove();
|
||||
});
|
||||
}
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN || MP-TOUTIAO
|
||||
if (src && src.includes(uni.env.USER_DATA_PATH))
|
||||
fs && fs.unlink({
|
||||
filePath: src
|
||||
})
|
||||
// #endif
|
||||
})
|
||||
clearInterval(this._timer);
|
||||
},
|
||||
methods: {
|
||||
// #ifdef H5 || APP-PLUS-NVUE || MP-360
|
||||
_handleHtml(html, append) {
|
||||
if (!append) {
|
||||
// 处理 tag-style 和 userAgentStyles
|
||||
var style = '<style>@keyframes _show{0%{opacity:0}100%{opacity:1}}img{max-width:100%}';
|
||||
for (var item in cfg.userAgentStyles)
|
||||
style += `${item}{${cfg.userAgentStyles[item]}}`;
|
||||
for (item in this.tagStyle)
|
||||
style += `${item}{${this.tagStyle[item]}}`;
|
||||
style += '</style>';
|
||||
html = style + html;
|
||||
}
|
||||
// 处理 rpx
|
||||
if (html.includes('rpx'))
|
||||
html = html.replace(/[0-9.]+\s*rpx/g, $ => parseFloat($) * rpx + 'px');
|
||||
return html;
|
||||
},
|
||||
// #endif
|
||||
setContent(html, append) {
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
if (!html)
|
||||
return this.height = 1;
|
||||
if (append)
|
||||
this.$refs.web.evalJs("var b=document.createElement('div');b.innerHTML='" + html.replace(/'/g, "\\'") +
|
||||
"';document.getElementById('parser').appendChild(b)");
|
||||
else {
|
||||
html =
|
||||
'<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"><base href="' +
|
||||
this.domain + '"><div id="parser"' + (this.selectable ? '>' : ' style="user-select:none">') + this._handleHtml(html).replace(/\n/g, '\\n') +
|
||||
'</div><script>"use strict";function e(e){if(window.__dcloud_weex_postMessage||window.__dcloud_weex_){var t={data:[e]};window.__dcloud_weex_postMessage?window.__dcloud_weex_postMessage(t):window.__dcloud_weex_.postMessage(JSON.stringify(t))}}' +
|
||||
(this.showWithAnimation ? 'document.body.style.animation="_show .5s",' : '') +
|
||||
'setTimeout(function(){e({action:"load",text:document.body.innerText,height:document.getElementById("parser").scrollHeight+16})},50);\x3c/script>';
|
||||
this.$refs.web.evalJs("document.write('" + html.replace(/'/g, "\\'") + "');document.close()");
|
||||
}
|
||||
this.$refs.web.evalJs(
|
||||
'var t=document.getElementsByTagName("title");t.length&&e({action:"getTitle",title:t[0].innerText});for(var o,n=document.getElementsByTagName("style"),r=0;o=n[r++];)o.innerHTML=o.innerHTML.replace(/body/g,"#parser");for(var i,a=document.getElementsByTagName("img"),s=[],c=0,l=0;i=a[c];c++)i.onerror=function(){' +
|
||||
(cfg.errorImg ? 'this.src="' + cfg.errorImg + '",' : '') +
|
||||
'e({action:"error",source:"img",target:this})},i.hasAttribute("ignore")||"A"==i.parentElement.nodeName||(i.i=l++,s.push(i.src),i.onclick=function(){e({action:"preview",img:{i:this.i,src:this.src}})});e({action:"getImgList",imgList:s});for(var d,u=document.getElementsByTagName("a"),g=0;d=u[g];g++)d.onclick=function(){var t,o=this.getAttribute("href");if("#"==o[0]){var n=document.getElementById(o.substr(1));n&&(t=n.offsetTop)}return e({action:"linkpress",href:o,offset:t}),!1};for(var m,f=document.getElementsByTagName("video"),h=0;m=f[h];h++)m.style.maxWidth="100%",m.onerror=function(){e({action:"error",source:"video",target:this})}' +
|
||||
(this.autopause ? ',m.onplay=function(){for(var e,t=0;e=f[t];t++)e!=this&&e.pause()}' : '') +
|
||||
';for(var v,y=document.getElementsByTagName("audio"),_=0;v=y[_];_++)v.onerror=function(){e({action:"error",source:"audio",target:this})};' +
|
||||
(this.autoscroll ? 'for(var p,w=document.getElementsByTagName("table"),T=0;p=w[T];T++){var E=document.createElement("div");E.style.overflow="scroll",p.parentNode.replaceChild(E,p),E.appendChild(p)}' : '') +
|
||||
'(function(){return new Promise(function(e){var t=document.getElementById("parser"),o=t.scrollHeight,n=setInterval(function(){o==t.scrollHeight?(clearInterval(n),e(o)):o=t.scrollHeight},500)})})().then(function(t){e({action:"ready",height:t+16})})'
|
||||
)
|
||||
this.nodes = [1];
|
||||
// #endif
|
||||
// #ifdef H5 || MP-360
|
||||
if (!html) {
|
||||
if (this.rtf && !append) this.rtf.parentNode.removeChild(this.rtf);
|
||||
return;
|
||||
}
|
||||
var div = document.createElement('div');
|
||||
if (!append) {
|
||||
if (this.rtf) this.rtf.parentNode.removeChild(this.rtf);
|
||||
this.rtf = div;
|
||||
} else {
|
||||
if (!this.rtf) this.rtf = div;
|
||||
else this.rtf.appendChild(div);
|
||||
}
|
||||
div.innerHTML = this._handleHtml(html, append);
|
||||
for (var styles = this.rtf.getElementsByTagName('style'), i = 0, style; style = styles[i++];) {
|
||||
style.innerHTML = style.innerHTML.replace(/body/g, '#rtf' + this._uid);
|
||||
style.setAttribute('scoped', 'true');
|
||||
}
|
||||
// 懒加载
|
||||
if (!this._observer && this.lazyLoad && IntersectionObserver) {
|
||||
this._observer = new IntersectionObserver(changes => {
|
||||
for (let item, i = 0; item = changes[i++];) {
|
||||
if (item.isIntersecting) {
|
||||
item.target.src = item.target.getAttribute('data-src');
|
||||
item.target.removeAttribute('data-src');
|
||||
this._observer.unobserve(item.target);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
rootMargin: '500px 0px 500px 0px'
|
||||
})
|
||||
}
|
||||
var _ts = this;
|
||||
// 获取标题
|
||||
var title = this.rtf.getElementsByTagName('title');
|
||||
if (title.length && this.autosetTitle)
|
||||
uni.setNavigationBarTitle({
|
||||
title: title[0].innerText
|
||||
})
|
||||
// 图片处理
|
||||
this.imgList.length = 0;
|
||||
var imgs = this.rtf.getElementsByTagName('img');
|
||||
for (let i = 0, j = 0, img; img = imgs[i]; i++) {
|
||||
var src = img.getAttribute('src');
|
||||
if (this.domain && src) {
|
||||
if (src[0] == '/') {
|
||||
if (src[1] == '/')
|
||||
img.src = (this.domain.includes('://') ? this.domain.split('://')[0] : '') + ':' + src;
|
||||
else img.src = this.domain + src;
|
||||
} else if (!src.includes('://')) img.src = this.domain + '/' + src;
|
||||
}
|
||||
if (!img.hasAttribute('ignore') && img.parentElement.nodeName != 'A') {
|
||||
img.i = j++;
|
||||
_ts.imgList.push(img.src || img.getAttribute('data-src'));
|
||||
img.onclick = function() {
|
||||
var preview = true;
|
||||
this.ignore = () => preview = false;
|
||||
_ts.$emit('imgtap', this);
|
||||
if (preview) {
|
||||
uni.previewImage({
|
||||
current: this.i,
|
||||
urls: _ts.imgList
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
img.onerror = function() {
|
||||
if (cfg.errorImg)
|
||||
_ts.imgList[this.i] = this.src = cfg.errorImg;
|
||||
_ts.$emit('error', {
|
||||
source: 'img',
|
||||
target: this
|
||||
});
|
||||
}
|
||||
if (_ts.lazyLoad && this._observer && img.src && img.i != 0) {
|
||||
img.setAttribute('data-src', img.src);
|
||||
img.removeAttribute('src');
|
||||
this._observer.observe(img);
|
||||
}
|
||||
}
|
||||
// 链接处理
|
||||
var links = this.rtf.getElementsByTagName('a');
|
||||
for (var link of links) {
|
||||
link.onclick = function() {
|
||||
var jump = true,
|
||||
href = this.getAttribute('href');
|
||||
_ts.$emit('linkpress', {
|
||||
href,
|
||||
ignore: () => jump = false
|
||||
});
|
||||
if (jump && href) {
|
||||
if (href[0] == '#') {
|
||||
if (_ts.useAnchor) {
|
||||
_ts.navigateTo({
|
||||
id: href.substr(1)
|
||||
})
|
||||
}
|
||||
} else if (href.indexOf('http') == 0 || href.indexOf('//') == 0)
|
||||
return true;
|
||||
else
|
||||
uni.navigateTo({
|
||||
url: href
|
||||
})
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 视频处理
|
||||
var videos = this.rtf.getElementsByTagName('video');
|
||||
_ts.videoContexts = videos;
|
||||
for (let video, i = 0; video = videos[i++];) {
|
||||
video.style.maxWidth = '100%';
|
||||
video.onerror = function() {
|
||||
_ts.$emit('error', {
|
||||
source: 'video',
|
||||
target: this
|
||||
});
|
||||
}
|
||||
video.onplay = function() {
|
||||
if (_ts.autopause)
|
||||
for (let item, i = 0; item = _ts.videoContexts[i++];)
|
||||
if (item != this) item.pause();
|
||||
}
|
||||
}
|
||||
// 音频处理
|
||||
var audios = this.rtf.getElementsByTagName('audio');
|
||||
for (var audio of audios)
|
||||
audio.onerror = function() {
|
||||
_ts.$emit('error', {
|
||||
source: 'audio',
|
||||
target: this
|
||||
});
|
||||
}
|
||||
// 表格处理
|
||||
if (this.autoscroll) {
|
||||
var tables = this.rtf.getElementsByTagName('table');
|
||||
for (var table of tables) {
|
||||
let div = document.createElement('div');
|
||||
div.style.overflow = 'scroll';
|
||||
table.parentNode.replaceChild(div, table);
|
||||
div.appendChild(table);
|
||||
}
|
||||
}
|
||||
if (!append) this.document.appendChild(this.rtf);
|
||||
this.$nextTick(() => {
|
||||
this.nodes = [1];
|
||||
this.$emit('load');
|
||||
});
|
||||
setTimeout(() => this.showAm = '', 500);
|
||||
// #endif
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
// #ifndef H5 || MP-360
|
||||
var nodes;
|
||||
if (!html) return this.nodes = [];
|
||||
var parser = new Parser(html, this);
|
||||
// 缓存读取
|
||||
if (this.useCache) {
|
||||
var hashVal = hash(html);
|
||||
if (cache[hashVal])
|
||||
nodes = cache[hashVal];
|
||||
else {
|
||||
nodes = parser.parse();
|
||||
cache[hashVal] = nodes;
|
||||
}
|
||||
} else nodes = parser.parse();
|
||||
this.$emit('parse', nodes);
|
||||
if (append) this.nodes = this.nodes.concat(nodes);
|
||||
else this.nodes = nodes;
|
||||
if (nodes.length && nodes.title && this.autosetTitle)
|
||||
uni.setNavigationBarTitle({
|
||||
title: nodes.title
|
||||
})
|
||||
if (this.imgList) this.imgList.length = 0;
|
||||
this.videoContexts = [];
|
||||
this.$nextTick(() => {
|
||||
(function f(cs) {
|
||||
for (var i = cs.length; i--;) {
|
||||
if (cs[i].top) {
|
||||
cs[i].controls = [];
|
||||
cs[i].init();
|
||||
f(cs[i].$children);
|
||||
}
|
||||
}
|
||||
})(this.$children)
|
||||
this.$emit('load');
|
||||
})
|
||||
// #endif
|
||||
var height;
|
||||
clearInterval(this._timer);
|
||||
this._timer = setInterval(() => {
|
||||
// #ifdef H5 || MP-360
|
||||
this.rect = this.rtf.getBoundingClientRect();
|
||||
// #endif
|
||||
// #ifndef H5 || MP-360
|
||||
uni.createSelectorQuery().in(this)
|
||||
.select('#_top').boundingClientRect().exec(res => {
|
||||
if (!res) return;
|
||||
this.rect = res[0];
|
||||
// #endif
|
||||
if (this.rect.height == height) {
|
||||
this.$emit('ready', this.rect)
|
||||
clearInterval(this._timer);
|
||||
}
|
||||
height = this.rect.height;
|
||||
// #ifndef H5 || MP-360
|
||||
});
|
||||
// #endif
|
||||
}, 350);
|
||||
if (this.showWithAnimation && !append) this.showAm = 'animation:_show .5s';
|
||||
// #endif
|
||||
},
|
||||
getText(ns = this.nodes) {
|
||||
var txt = '';
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
txt = this._text;
|
||||
// #endif
|
||||
// #ifdef H5 || MP-360
|
||||
txt = this.rtf.innerText;
|
||||
// #endif
|
||||
// #ifndef H5 || APP-PLUS-NVUE || MP-360
|
||||
for (var i = 0, n; n = ns[i++];) {
|
||||
if (n.type == 'text') txt += n.text.replace(/ /g, '\u00A0').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/&/g, '&');
|
||||
else if (n.type == 'br') txt += '\n';
|
||||
else {
|
||||
// 块级标签前后加换行
|
||||
var block = n.name == 'p' || n.name == 'div' || n.name == 'tr' || n.name == 'li' || (n.name[0] == 'h' && n.name[1] >
|
||||
'0' && n.name[1] < '7');
|
||||
if (block && txt && txt[txt.length - 1] != '\n') txt += '\n';
|
||||
if (n.children) txt += this.getText(n.children);
|
||||
if (block && txt[txt.length - 1] != '\n') txt += '\n';
|
||||
else if (n.name == 'td' || n.name == 'th') txt += '\t';
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
return txt;
|
||||
},
|
||||
navigateTo(obj) {
|
||||
if (!this.useAnchor)
|
||||
return obj.fail && obj.fail({
|
||||
errMsg: 'Anchor is disabled'
|
||||
})
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
if (!obj.id)
|
||||
weexDom.scrollToElement(this.$refs.web);
|
||||
else
|
||||
this.$refs.web.evalJs('var pos=document.getElementById("' + obj.id +
|
||||
'");if(pos)post({action:"linkpress",href:"#",offset:pos.offsetTop+' + (obj.offset || 0) + '})');
|
||||
obj.success && obj.success({
|
||||
errMsg: 'pageScrollTo:ok'
|
||||
});
|
||||
// #endif
|
||||
// #ifdef H5 || MP-360
|
||||
if (!obj.id) {
|
||||
window.scrollTo(0, this.rtf.offsetTop);
|
||||
return obj.success && obj.success({
|
||||
errMsg: 'pageScrollTo:ok'
|
||||
});
|
||||
}
|
||||
var target = document.getElementById(obj.id);
|
||||
if (!target) return obj.fail && obj.fail({
|
||||
errMsg: 'Label not found'
|
||||
});
|
||||
obj.scrollTop = this.rtf.offsetTop + target.offsetTop + (obj.offset || 0);
|
||||
uni.pageScrollTo(obj);
|
||||
// #endif
|
||||
// #ifndef H5 || APP-PLUS-NVUE || MP-360
|
||||
var d = ' ';
|
||||
// #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
|
||||
d = '>>>';
|
||||
// #endif
|
||||
uni.createSelectorQuery().in(this).select('#_top' + (obj.id ? d + '#' + obj.id + ',#_top' + d + '.' + obj.id : '')).boundingClientRect()
|
||||
.selectViewport().scrollOffset().exec(res => {
|
||||
if (!res || !res[0])
|
||||
return obj.fail && obj.fail({
|
||||
errMsg: 'Label not found'
|
||||
});
|
||||
obj.scrollTop = res[1].scrollTop + res[0].top + (obj.offset || 0);
|
||||
// #ifdef MP-ALIPAY
|
||||
obj.duration = 300;
|
||||
my.
|
||||
// #endif
|
||||
// #ifndef MP-ALIPAY
|
||||
uni.
|
||||
// #endif
|
||||
pageScrollTo(obj);
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
getVideoContext(id) {
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
if (!id) return this.videoContexts;
|
||||
else
|
||||
for (var i = this.videoContexts.length; i--;)
|
||||
if (this.videoContexts[i].id == id) return this.videoContexts[i];
|
||||
// #endif
|
||||
},
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
_message(e) {
|
||||
// 接收 web-view 消息
|
||||
var data = e.detail.data[0];
|
||||
if (data.action == 'load') {
|
||||
this.$emit('load');
|
||||
this.height = data.height;
|
||||
this._text = data.text;
|
||||
} else if (data.action == 'getTitle') {
|
||||
if (this.autosetTitle)
|
||||
uni.setNavigationBarTitle({
|
||||
title: data.title
|
||||
})
|
||||
} else if (data.action == 'getImgList') {
|
||||
this.imgList.length = 0;
|
||||
for (var i = data.imgList.length; i--;)
|
||||
this.imgList.setItem(i, data.imgList[i]);
|
||||
} else if (data.action == 'preview') {
|
||||
var preview = true;
|
||||
data.img.ignore = () => preview = false;
|
||||
this.$emit('imgtap', data.img);
|
||||
if (preview)
|
||||
uni.previewImage({
|
||||
current: data.img.i,
|
||||
urls: this.imgList
|
||||
})
|
||||
} else if (data.action == 'linkpress') {
|
||||
var jump = true,
|
||||
href = data.href;
|
||||
this.$emit('linkpress', {
|
||||
href,
|
||||
ignore: () => jump = false
|
||||
})
|
||||
if (jump && href) {
|
||||
if (href[0] == '#') {
|
||||
if (this.useAnchor)
|
||||
weexDom.scrollToElement(this.$refs.web, {
|
||||
offset: data.offset
|
||||
})
|
||||
} else if (href.includes('://'))
|
||||
plus.runtime.openWeb(href);
|
||||
else
|
||||
uni.navigateTo({
|
||||
url: href
|
||||
})
|
||||
}
|
||||
} else if (data.action == 'error') {
|
||||
if (data.source == 'img' && cfg.errorImg)
|
||||
this.imgList.setItem(data.target.i, cfg.errorImg);
|
||||
this.$emit('error', {
|
||||
source: data.source,
|
||||
target: data.target
|
||||
})
|
||||
} else if (data.action == 'ready') {
|
||||
this.height = data.height;
|
||||
this.$nextTick(() => {
|
||||
uni.createSelectorQuery().in(this).select('#_top').boundingClientRect().exec(res => {
|
||||
this.rect = res[0];
|
||||
this.$emit('ready', res[0]);
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@keyframes _show {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* #ifdef MP-WEIXIN */
|
||||
:host {
|
||||
display: block;
|
||||
/* overflow: scroll; */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
</style>
|
||||
@@ -0,0 +1,97 @@
|
||||
const cfg = require('./config.js'),
|
||||
isLetter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
|
||||
|
||||
function CssHandler(tagStyle) {
|
||||
var styles = Object.assign(Object.create(null), cfg.userAgentStyles);
|
||||
for (var item in tagStyle)
|
||||
styles[item] = (styles[item] ? styles[item] + ';' : '') + tagStyle[item];
|
||||
this.styles = styles;
|
||||
}
|
||||
CssHandler.prototype.getStyle = function(data) {
|
||||
this.styles = new parser(data, this.styles).parse();
|
||||
}
|
||||
CssHandler.prototype.match = function(name, attrs) {
|
||||
var tmp, matched = (tmp = this.styles[name]) ? tmp + ';' : '';
|
||||
if (attrs.class) {
|
||||
var items = attrs.class.split(' ');
|
||||
for (var i = 0, item; item = items[i]; i++)
|
||||
if (tmp = this.styles['.' + item])
|
||||
matched += tmp + ';';
|
||||
}
|
||||
if (tmp = this.styles['#' + attrs.id])
|
||||
matched += tmp + ';';
|
||||
return matched;
|
||||
}
|
||||
module.exports = CssHandler;
|
||||
|
||||
function parser(data, init) {
|
||||
this.data = data;
|
||||
this.floor = 0;
|
||||
this.i = 0;
|
||||
this.list = [];
|
||||
this.res = init;
|
||||
this.state = this.Space;
|
||||
}
|
||||
parser.prototype.parse = function() {
|
||||
for (var c; c = this.data[this.i]; this.i++)
|
||||
this.state(c);
|
||||
return this.res;
|
||||
}
|
||||
parser.prototype.section = function() {
|
||||
return this.data.substring(this.start, this.i);
|
||||
}
|
||||
// 状态机
|
||||
parser.prototype.Space = function(c) {
|
||||
if (c == '.' || c == '#' || isLetter(c)) {
|
||||
this.start = this.i;
|
||||
this.state = this.Name;
|
||||
} else if (c == '/' && this.data[this.i + 1] == '*')
|
||||
this.Comment();
|
||||
else if (!cfg.blankChar[c] && c != ';')
|
||||
this.state = this.Ignore;
|
||||
}
|
||||
parser.prototype.Comment = function() {
|
||||
this.i = this.data.indexOf('*/', this.i) + 1;
|
||||
if (!this.i) this.i = this.data.length;
|
||||
this.state = this.Space;
|
||||
}
|
||||
parser.prototype.Ignore = function(c) {
|
||||
if (c == '{') this.floor++;
|
||||
else if (c == '}' && !--this.floor) this.state = this.Space;
|
||||
}
|
||||
parser.prototype.Name = function(c) {
|
||||
if (cfg.blankChar[c]) {
|
||||
this.list.push(this.section());
|
||||
this.state = this.NameSpace;
|
||||
} else if (c == '{') {
|
||||
this.list.push(this.section());
|
||||
this.Content();
|
||||
} else if (c == ',') {
|
||||
this.list.push(this.section());
|
||||
this.Comma();
|
||||
} else if (!isLetter(c) && (c < '0' || c > '9') && c != '-' && c != '_')
|
||||
this.state = this.Ignore;
|
||||
}
|
||||
parser.prototype.NameSpace = function(c) {
|
||||
if (c == '{') this.Content();
|
||||
else if (c == ',') this.Comma();
|
||||
else if (!cfg.blankChar[c]) this.state = this.Ignore;
|
||||
}
|
||||
parser.prototype.Comma = function() {
|
||||
while (cfg.blankChar[this.data[++this.i]]);
|
||||
if (this.data[this.i] == '{') this.Content();
|
||||
else {
|
||||
this.start = this.i--;
|
||||
this.state = this.Name;
|
||||
}
|
||||
}
|
||||
parser.prototype.Content = function() {
|
||||
this.start = ++this.i;
|
||||
if ((this.i = this.data.indexOf('}', this.i)) == -1) this.i = this.data.length;
|
||||
var content = this.section();
|
||||
for (var i = 0, item; item = this.list[i++];)
|
||||
if (this.res[item]) this.res[item] += ';' + content;
|
||||
else this.res[item] = content;
|
||||
this.list = [];
|
||||
this.state = this.Space;
|
||||
}
|
||||
@@ -0,0 +1,534 @@
|
||||
/**
|
||||
* html 解析器
|
||||
* @tutorial https://github.com/jin-yufeng/Parser
|
||||
* @version 20200615
|
||||
* @author JinYufeng
|
||||
* @listens MIT
|
||||
*/
|
||||
const cfg = require('./config.js'),
|
||||
blankChar = cfg.blankChar,
|
||||
CssHandler = require('./CssHandler.js'),
|
||||
windowWidth = uni.getSystemInfoSync().windowWidth;
|
||||
var emoji;
|
||||
|
||||
function MpHtmlParser(data, options = {}) {
|
||||
this.attrs = {};
|
||||
this.CssHandler = new CssHandler(options.tagStyle, windowWidth);
|
||||
this.data = data;
|
||||
this.domain = options.domain;
|
||||
this.DOM = [];
|
||||
this.i = this.start = this.audioNum = this.imgNum = this.videoNum = 0;
|
||||
options.prot = (this.domain || '').includes('://') ? this.domain.split('://')[0] : 'http';
|
||||
this.options = options;
|
||||
this.state = this.Text;
|
||||
this.STACK = [];
|
||||
// 工具函数
|
||||
this.bubble = () => {
|
||||
for (var i = this.STACK.length, item; item = this.STACK[--i];) {
|
||||
if (cfg.richOnlyTags[item.name]) {
|
||||
if (item.name == 'table' && !Object.hasOwnProperty.call(item, 'c')) item.c = 1;
|
||||
return false;
|
||||
}
|
||||
item.c = 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
this.decode = (val, amp) => {
|
||||
var i = -1,
|
||||
j, en;
|
||||
while (1) {
|
||||
if ((i = val.indexOf('&', i + 1)) == -1) break;
|
||||
if ((j = val.indexOf(';', i + 2)) == -1) break;
|
||||
if (val[i + 1] == '#') {
|
||||
en = parseInt((val[i + 2] == 'x' ? '0' : '') + val.substring(i + 2, j));
|
||||
if (!isNaN(en)) val = val.substr(0, i) + String.fromCharCode(en) + val.substr(j + 1);
|
||||
} else {
|
||||
en = val.substring(i + 1, j);
|
||||
if (cfg.entities[en] || en == amp)
|
||||
val = val.substr(0, i) + (cfg.entities[en] || '&') + val.substr(j + 1);
|
||||
}
|
||||
}
|
||||
return val;
|
||||
}
|
||||
this.getUrl = url => {
|
||||
if (url[0] == '/') {
|
||||
if (url[1] == '/') url = this.options.prot + ':' + url;
|
||||
else if (this.domain) url = this.domain + url;
|
||||
} else if (this.domain && url.indexOf('data:') != 0 && !url.includes('://'))
|
||||
url = this.domain + '/' + url;
|
||||
return url;
|
||||
}
|
||||
this.isClose = () => this.data[this.i] == '>' || (this.data[this.i] == '/' && this.data[this.i + 1] == '>');
|
||||
this.section = () => this.data.substring(this.start, this.i);
|
||||
this.parent = () => this.STACK[this.STACK.length - 1];
|
||||
this.siblings = () => this.STACK.length ? this.parent().children : this.DOM;
|
||||
}
|
||||
MpHtmlParser.prototype.parse = function() {
|
||||
if (emoji) this.data = emoji.parseEmoji(this.data);
|
||||
for (var c; c = this.data[this.i]; this.i++)
|
||||
this.state(c);
|
||||
if (this.state == this.Text) this.setText();
|
||||
while (this.STACK.length) this.popNode(this.STACK.pop());
|
||||
return this.DOM;
|
||||
}
|
||||
// 设置属性
|
||||
MpHtmlParser.prototype.setAttr = function() {
|
||||
var name = this.attrName.toLowerCase(),
|
||||
val = this.attrVal;
|
||||
if (cfg.boolAttrs[name]) this.attrs[name] = 'T';
|
||||
else if (val) {
|
||||
if (name == 'src' || (name == 'data-src' && !this.attrs.src)) this.attrs.src = this.getUrl(this.decode(val, 'amp'));
|
||||
else if (name == 'href' || name == 'style') this.attrs[name] = this.decode(val, 'amp');
|
||||
else if (name.substr(0, 5) != 'data-') this.attrs[name] = val;
|
||||
}
|
||||
this.attrVal = '';
|
||||
while (blankChar[this.data[this.i]]) this.i++;
|
||||
if (this.isClose()) this.setNode();
|
||||
else {
|
||||
this.start = this.i;
|
||||
this.state = this.AttrName;
|
||||
}
|
||||
}
|
||||
// 设置文本节点
|
||||
MpHtmlParser.prototype.setText = function() {
|
||||
var back, text = this.section();
|
||||
if (!text) return;
|
||||
text = (cfg.onText && cfg.onText(text, () => back = true)) || text;
|
||||
if (back) {
|
||||
this.data = this.data.substr(0, this.start) + text + this.data.substr(this.i);
|
||||
let j = this.start + text.length;
|
||||
for (this.i = this.start; this.i < j; this.i++) this.state(this.data[this.i]);
|
||||
return;
|
||||
}
|
||||
if (!this.pre) {
|
||||
// 合并空白符
|
||||
var tmp = [];
|
||||
for (let i = text.length, c; c = text[--i];)
|
||||
if (!blankChar[c] || (!blankChar[tmp[0]] && (c = ' '))) tmp.unshift(c);
|
||||
text = tmp.join('');
|
||||
}
|
||||
this.siblings().push({
|
||||
type: 'text',
|
||||
text: this.decode(text)
|
||||
});
|
||||
}
|
||||
// 设置元素节点
|
||||
MpHtmlParser.prototype.setNode = function() {
|
||||
var node = {
|
||||
name: this.tagName.toLowerCase(),
|
||||
attrs: this.attrs
|
||||
},
|
||||
close = cfg.selfClosingTags[node.name];
|
||||
this.attrs = {};
|
||||
if (!cfg.ignoreTags[node.name]) {
|
||||
// 处理属性
|
||||
var attrs = node.attrs,
|
||||
style = this.CssHandler.match(node.name, attrs, node) + (attrs.style || ''),
|
||||
styleObj = {};
|
||||
if (attrs.id) {
|
||||
if (this.options.compress & 1) attrs.id = void 0;
|
||||
else if (this.options.useAnchor) this.bubble();
|
||||
}
|
||||
if ((this.options.compress & 2) && attrs.class) attrs.class = void 0;
|
||||
switch (node.name) {
|
||||
case 'a':
|
||||
case 'ad': // #ifdef APP-PLUS
|
||||
case 'iframe':
|
||||
// #endif
|
||||
this.bubble();
|
||||
break;
|
||||
case 'font':
|
||||
if (attrs.color) {
|
||||
styleObj['color'] = attrs.color;
|
||||
attrs.color = void 0;
|
||||
}
|
||||
if (attrs.face) {
|
||||
styleObj['font-family'] = attrs.face;
|
||||
attrs.face = void 0;
|
||||
}
|
||||
if (attrs.size) {
|
||||
var size = parseInt(attrs.size);
|
||||
if (size < 1) size = 1;
|
||||
else if (size > 7) size = 7;
|
||||
var map = ['xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'];
|
||||
styleObj['font-size'] = map[size - 1];
|
||||
attrs.size = void 0;
|
||||
}
|
||||
break;
|
||||
case 'embed':
|
||||
// #ifndef APP-PLUS
|
||||
var src = node.attrs.src || '',
|
||||
type = node.attrs.type || '';
|
||||
if (type.includes('video') || src.includes('.mp4') || src.includes('.3gp') || src.includes('.m3u8'))
|
||||
node.name = 'video';
|
||||
else if (type.includes('audio') || src.includes('.m4a') || src.includes('.wav') || src.includes('.mp3') || src.includes(
|
||||
'.aac'))
|
||||
node.name = 'audio';
|
||||
else break;
|
||||
if (node.attrs.autostart)
|
||||
node.attrs.autoplay = 'T';
|
||||
node.attrs.controls = 'T';
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
this.bubble();
|
||||
break;
|
||||
// #endif
|
||||
case 'video':
|
||||
case 'audio':
|
||||
if (!attrs.id) attrs.id = node.name + (++this[`${node.name}Num`]);
|
||||
else this[`${node.name}Num`]++;
|
||||
if (node.name == 'video') {
|
||||
if (this.videoNum > 3)
|
||||
node.lazyLoad = 1;
|
||||
if (attrs.width) {
|
||||
styleObj.width = parseFloat(attrs.width) + (attrs.width.includes('%') ? '%' : 'px');
|
||||
attrs.width = void 0;
|
||||
}
|
||||
if (attrs.height) {
|
||||
styleObj.height = parseFloat(attrs.height) + (attrs.height.includes('%') ? '%' : 'px');
|
||||
attrs.height = void 0;
|
||||
}
|
||||
}
|
||||
attrs.source = [];
|
||||
if (attrs.src) {
|
||||
attrs.source.push(attrs.src);
|
||||
attrs.src = void 0;
|
||||
}
|
||||
this.bubble();
|
||||
break;
|
||||
case 'td':
|
||||
case 'th':
|
||||
if (attrs.colspan || attrs.rowspan)
|
||||
for (var k = this.STACK.length, item; item = this.STACK[--k];)
|
||||
if (item.name == 'table') {
|
||||
item.c = void 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (attrs.align) {
|
||||
styleObj['text-align'] = attrs.align;
|
||||
attrs.align = void 0;
|
||||
}
|
||||
// 压缩 style
|
||||
var styles = style.split(';');
|
||||
style = '';
|
||||
for (var i = 0, len = styles.length; i < len; i++) {
|
||||
var info = styles[i].split(':');
|
||||
if (info.length < 2) continue;
|
||||
let key = info[0].trim().toLowerCase(),
|
||||
value = info.slice(1).join(':').trim();
|
||||
if (value.includes('-webkit') || value.includes('-moz') || value.includes('-ms') || value.includes('-o') || value.includes(
|
||||
'safe'))
|
||||
style += `;${key}:${value}`;
|
||||
else if (!styleObj[key] || value.includes('import') || !styleObj[key].includes('import'))
|
||||
styleObj[key] = value;
|
||||
}
|
||||
if (node.name == 'img') {
|
||||
if (attrs.src && !attrs.ignore) {
|
||||
if (this.bubble())
|
||||
attrs.i = (this.imgNum++).toString();
|
||||
else attrs.ignore = 'T';
|
||||
}
|
||||
if (attrs.ignore) {
|
||||
style += ';-webkit-touch-callout:none';
|
||||
styleObj['max-width'] = '100%';
|
||||
}
|
||||
var width;
|
||||
if (styleObj.width) width = styleObj.width;
|
||||
else if (attrs.width) width = attrs.width.includes('%') ? attrs.width : attrs.width + 'px';
|
||||
if (width) {
|
||||
styleObj.width = width;
|
||||
attrs.width = '100%';
|
||||
if (parseInt(width) > windowWidth) {
|
||||
styleObj.height = '';
|
||||
if (attrs.height) attrs.height = void 0;
|
||||
}
|
||||
}
|
||||
if (styleObj.height) {
|
||||
attrs.height = styleObj.height;
|
||||
styleObj.height = '';
|
||||
} else if (attrs.height && !attrs.height.includes('%'))
|
||||
attrs.height += 'px';
|
||||
}
|
||||
for (var key in styleObj) {
|
||||
var value = styleObj[key];
|
||||
if (!value) continue;
|
||||
if (key.includes('flex') || key == 'order' || key == 'self-align') node.c = 1;
|
||||
// 填充链接
|
||||
if (value.includes('url')) {
|
||||
var j = value.indexOf('(');
|
||||
if (j++ != -1) {
|
||||
while (value[j] == '"' || value[j] == "'" || blankChar[value[j]]) j++;
|
||||
value = value.substr(0, j) + this.getUrl(value.substr(j));
|
||||
}
|
||||
}
|
||||
// 转换 rpx
|
||||
else if (value.includes('rpx'))
|
||||
value = value.replace(/[0-9.]+\s*rpx/g, $ => parseFloat($) * windowWidth / 750 + 'px');
|
||||
else if (key == 'white-space' && value.includes('pre'))
|
||||
this.pre = node.pre = true;
|
||||
style += `;${key}:${value}`;
|
||||
}
|
||||
style = style.substr(1);
|
||||
if (style) attrs.style = style;
|
||||
if (!close) {
|
||||
node.children = [];
|
||||
if (node.name == 'pre' && cfg.highlight) {
|
||||
this.remove(node);
|
||||
this.pre = node.pre = true;
|
||||
}
|
||||
this.siblings().push(node);
|
||||
this.STACK.push(node);
|
||||
} else if (!cfg.filter || cfg.filter(node, this) != false)
|
||||
this.siblings().push(node);
|
||||
} else {
|
||||
if (!close) this.remove(node);
|
||||
else if (node.name == 'source') {
|
||||
var parent = this.parent();
|
||||
if (parent && (parent.name == 'video' || parent.name == 'audio') && node.attrs.src)
|
||||
parent.attrs.source.push(node.attrs.src);
|
||||
} else if (node.name == 'base' && !this.domain) this.domain = node.attrs.href;
|
||||
}
|
||||
if (this.data[this.i] == '/') this.i++;
|
||||
this.start = this.i + 1;
|
||||
this.state = this.Text;
|
||||
}
|
||||
// 移除标签
|
||||
MpHtmlParser.prototype.remove = function(node) {
|
||||
var name = node.name,
|
||||
j = this.i;
|
||||
// 处理 svg
|
||||
var handleSvg = () => {
|
||||
var src = this.data.substring(j, this.i + 1);
|
||||
if (!node.attrs.xmlns) src = ' xmlns="http://www.w3.org/2000/svg"' + src;
|
||||
var i = j;
|
||||
while (this.data[j] != '<') j--;
|
||||
src = this.data.substring(j, i) + src;
|
||||
var parent = this.parent();
|
||||
if (node.attrs.width == '100%' && parent && (parent.attrs.style || '').includes('inline'))
|
||||
parent.attrs.style = 'width:300px;max-width:100%;' + parent.attrs.style;
|
||||
this.siblings().push({
|
||||
name: 'img',
|
||||
attrs: {
|
||||
src: 'data:image/svg+xml;utf8,' + src.replace(/#/g, '%23'),
|
||||
style: (/vertical[^;]+/.exec(node.attrs.style) || []).shift(),
|
||||
ignore: 'T'
|
||||
}
|
||||
})
|
||||
}
|
||||
if (node.name == 'svg' && this.data[j] == '/') return handleSvg(this.i++);
|
||||
while (1) {
|
||||
if ((this.i = this.data.indexOf('</', this.i + 1)) == -1) {
|
||||
if (name == 'pre' || name == 'svg') this.i = j;
|
||||
else this.i = this.data.length;
|
||||
return;
|
||||
}
|
||||
this.start = (this.i += 2);
|
||||
while (!blankChar[this.data[this.i]] && !this.isClose()) this.i++;
|
||||
if (this.section().toLowerCase() == name) {
|
||||
// 代码块高亮
|
||||
if (name == 'pre') {
|
||||
this.data = this.data.substr(0, j + 1) + cfg.highlight(this.data.substring(j + 1, this.i - 5), node.attrs) + this.data
|
||||
.substr(this.i - 5);
|
||||
return this.i = j;
|
||||
} else if (name == 'style')
|
||||
this.CssHandler.getStyle(this.data.substring(j + 1, this.i - 7));
|
||||
else if (name == 'title')
|
||||
this.DOM.title = this.data.substring(j + 1, this.i - 7);
|
||||
if ((this.i = this.data.indexOf('>', this.i)) == -1) this.i = this.data.length;
|
||||
if (name == 'svg') handleSvg();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 节点出栈处理
|
||||
MpHtmlParser.prototype.popNode = function(node) {
|
||||
// 空白符处理
|
||||
if (node.pre) {
|
||||
node.pre = this.pre = void 0;
|
||||
for (let i = this.STACK.length; i--;)
|
||||
if (this.STACK[i].pre)
|
||||
this.pre = true;
|
||||
}
|
||||
var siblings = this.siblings(),
|
||||
len = siblings.length,
|
||||
childs = node.children;
|
||||
if (node.name == 'head' || (cfg.filter && cfg.filter(node, this) == false))
|
||||
return siblings.pop();
|
||||
var attrs = node.attrs;
|
||||
// 替换一些标签名
|
||||
if (cfg.blockTags[node.name]) node.name = 'div';
|
||||
else if (!cfg.trustTags[node.name]) node.name = 'span';
|
||||
// 去除块标签前后空串
|
||||
if (node.name == 'div' || node.name == 'p' || node.name[0] == 't') {
|
||||
if (len > 1 && siblings[len - 2].text == ' ')
|
||||
siblings.splice(--len - 1, 1);
|
||||
if (childs.length && childs[childs.length - 1].text == ' ')
|
||||
childs.pop();
|
||||
}
|
||||
// 处理列表
|
||||
if (node.c && (node.name == 'ul' || node.name == 'ol')) {
|
||||
if ((node.attrs.style || '').includes('list-style:none')) {
|
||||
for (let i = 0, child; child = childs[i++];)
|
||||
if (child.name == 'li')
|
||||
child.name = 'div';
|
||||
} else if (node.name == 'ul') {
|
||||
var floor = 1;
|
||||
for (let i = this.STACK.length; i--;)
|
||||
if (this.STACK[i].name == 'ul') floor++;
|
||||
if (floor != 1)
|
||||
for (let i = childs.length; i--;)
|
||||
childs[i].floor = floor;
|
||||
} else {
|
||||
for (let i = 0, num = 1, child; child = childs[i++];)
|
||||
if (child.name == 'li') {
|
||||
child.type = 'ol';
|
||||
child.num = ((num, type) => {
|
||||
if (type == 'a') return String.fromCharCode(97 + (num - 1) % 26);
|
||||
if (type == 'A') return String.fromCharCode(65 + (num - 1) % 26);
|
||||
if (type == 'i' || type == 'I') {
|
||||
num = (num - 1) % 99 + 1;
|
||||
var one = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX'],
|
||||
ten = ['X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC'],
|
||||
res = (ten[Math.floor(num / 10) - 1] || '') + (one[num % 10 - 1] || '');
|
||||
if (type == 'i') return res.toLowerCase();
|
||||
return res;
|
||||
}
|
||||
return num;
|
||||
})(num++, attrs.type) + '.';
|
||||
}
|
||||
}
|
||||
}
|
||||
// 处理表格的边框
|
||||
if (node.name == 'table') {
|
||||
var padding = attrs.cellpadding,
|
||||
spacing = attrs.cellspacing,
|
||||
border = attrs.border;
|
||||
if (node.c) {
|
||||
this.bubble();
|
||||
attrs.style = (attrs.style || '') + ';display:table';
|
||||
if (!padding) padding = 2;
|
||||
if (!spacing) spacing = 2;
|
||||
}
|
||||
if (border) attrs.style = `border:${border}px solid gray;${attrs.style || ''}`;
|
||||
if (spacing) attrs.style = `border-spacing:${spacing}px;${attrs.style || ''}`;
|
||||
if (border || padding || node.c)
|
||||
(function f(ns) {
|
||||
for (var i = 0, n; n = ns[i]; i++) {
|
||||
if (n.type == 'text') continue;
|
||||
var style = n.attrs.style || '';
|
||||
if (node.c && n.name[0] == 't') {
|
||||
n.c = 1;
|
||||
style += ';display:table-' + (n.name == 'th' || n.name == 'td' ? 'cell' : (n.name == 'tr' ? 'row' : 'row-group'));
|
||||
}
|
||||
if (n.name == 'th' || n.name == 'td') {
|
||||
if (border) style = `border:${border}px solid gray;${style}`;
|
||||
if (padding) style = `padding:${padding}px;${style}`;
|
||||
} else f(n.children || []);
|
||||
if (style) n.attrs.style = style;
|
||||
}
|
||||
})(childs)
|
||||
if (this.options.autoscroll) {
|
||||
var table = Object.assign({}, node);
|
||||
node.name = 'div';
|
||||
node.attrs = {
|
||||
style: 'overflow:scroll'
|
||||
}
|
||||
node.children = [table];
|
||||
}
|
||||
}
|
||||
this.CssHandler.pop && this.CssHandler.pop(node);
|
||||
// 自动压缩
|
||||
if (node.name == 'div' && !Object.keys(attrs).length && childs.length == 1 && childs[0].name == 'div')
|
||||
siblings[len - 1] = childs[0];
|
||||
}
|
||||
// 状态机
|
||||
MpHtmlParser.prototype.Text = function(c) {
|
||||
if (c == '<') {
|
||||
var next = this.data[this.i + 1],
|
||||
isLetter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
|
||||
if (isLetter(next)) {
|
||||
this.setText();
|
||||
this.start = this.i + 1;
|
||||
this.state = this.TagName;
|
||||
} else if (next == '/') {
|
||||
this.setText();
|
||||
if (isLetter(this.data[++this.i + 1])) {
|
||||
this.start = this.i + 1;
|
||||
this.state = this.EndTag;
|
||||
} else this.Comment();
|
||||
} else if (next == '!') {
|
||||
this.setText();
|
||||
this.Comment();
|
||||
}
|
||||
}
|
||||
}
|
||||
MpHtmlParser.prototype.Comment = function() {
|
||||
var key;
|
||||
if (this.data.substring(this.i + 2, this.i + 4) == '--') key = '-->';
|
||||
else if (this.data.substring(this.i + 2, this.i + 9) == '[CDATA[') key = ']]>';
|
||||
else key = '>';
|
||||
if ((this.i = this.data.indexOf(key, this.i + 2)) == -1) this.i = this.data.length;
|
||||
else this.i += key.length - 1;
|
||||
this.start = this.i + 1;
|
||||
this.state = this.Text;
|
||||
}
|
||||
MpHtmlParser.prototype.TagName = function(c) {
|
||||
if (blankChar[c]) {
|
||||
this.tagName = this.section();
|
||||
while (blankChar[this.data[this.i]]) this.i++;
|
||||
if (this.isClose()) this.setNode();
|
||||
else {
|
||||
this.start = this.i;
|
||||
this.state = this.AttrName;
|
||||
}
|
||||
} else if (this.isClose()) {
|
||||
this.tagName = this.section();
|
||||
this.setNode();
|
||||
}
|
||||
}
|
||||
MpHtmlParser.prototype.AttrName = function(c) {
|
||||
if (c == '=' || blankChar[c] || this.isClose()) {
|
||||
this.attrName = this.section();
|
||||
if (blankChar[c])
|
||||
while (blankChar[this.data[++this.i]]);
|
||||
if (this.data[this.i] == '=') {
|
||||
while (blankChar[this.data[++this.i]]);
|
||||
this.start = this.i--;
|
||||
this.state = this.AttrValue;
|
||||
} else this.setAttr();
|
||||
}
|
||||
}
|
||||
MpHtmlParser.prototype.AttrValue = function(c) {
|
||||
if (c == '"' || c == "'") {
|
||||
this.start++;
|
||||
if ((this.i = this.data.indexOf(c, this.i + 1)) == -1) return this.i = this.data.length;
|
||||
this.attrVal = this.section();
|
||||
this.i++;
|
||||
} else {
|
||||
for (; !blankChar[this.data[this.i]] && !this.isClose(); this.i++);
|
||||
this.attrVal = this.section();
|
||||
}
|
||||
this.setAttr();
|
||||
}
|
||||
MpHtmlParser.prototype.EndTag = function(c) {
|
||||
if (blankChar[c] || c == '>' || c == '/') {
|
||||
var name = this.section().toLowerCase();
|
||||
for (var i = this.STACK.length; i--;)
|
||||
if (this.STACK[i].name == name) break;
|
||||
if (i != -1) {
|
||||
var node;
|
||||
while ((node = this.STACK.pop()).name != name) this.popNode(node);
|
||||
this.popNode(node);
|
||||
} else if (name == 'p' || name == 'br')
|
||||
this.siblings().push({
|
||||
name,
|
||||
attrs: {}
|
||||
});
|
||||
this.i = this.data.indexOf('>', this.i);
|
||||
this.start = this.i + 1;
|
||||
if (this.i == -1) this.i = this.data.length;
|
||||
else this.state = this.Text;
|
||||
}
|
||||
}
|
||||
module.exports = MpHtmlParser;
|
||||
@@ -0,0 +1,93 @@
|
||||
/* 配置文件 */
|
||||
// #ifdef MP-WEIXIN
|
||||
const canIUse = wx.canIUse('editor'); // 高基础库标识,用于兼容
|
||||
// #endif
|
||||
module.exports = {
|
||||
// 出错占位图
|
||||
errorImg: null,
|
||||
// 过滤器函数
|
||||
filter: null,
|
||||
// 代码高亮函数
|
||||
highlight: null,
|
||||
// 文本处理函数
|
||||
onText: null,
|
||||
// 实体编码列表
|
||||
entities: {
|
||||
quot: '"',
|
||||
apos: "'",
|
||||
semi: ';',
|
||||
nbsp: '\xA0',
|
||||
ensp: '\u2002',
|
||||
emsp: '\u2003',
|
||||
ndash: '–',
|
||||
mdash: '—',
|
||||
middot: '·',
|
||||
lsquo: '‘',
|
||||
rsquo: '’',
|
||||
ldquo: '“',
|
||||
rdquo: '”',
|
||||
bull: '•',
|
||||
hellip: '…'
|
||||
},
|
||||
blankChar: makeMap(' ,\xA0,\t,\r,\n,\f'),
|
||||
boolAttrs: makeMap('allowfullscreen,autoplay,autostart,controls,ignore,loop,muted'),
|
||||
// 块级标签,将被转为 div
|
||||
blockTags: makeMap('address,article,aside,body,caption,center,cite,footer,header,html,nav,section' + (
|
||||
// #ifdef MP-WEIXIN
|
||||
canIUse ? '' :
|
||||
// #endif
|
||||
',pre')),
|
||||
// 将被移除的标签
|
||||
ignoreTags: makeMap(
|
||||
'area,base,canvas,frame,input,link,map,meta,param,script,source,style,svg,textarea,title,track,wbr'
|
||||
// #ifdef MP-WEIXIN
|
||||
+ (canIUse ? ',rp' : '')
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
+ ',iframe'
|
||||
// #endif
|
||||
),
|
||||
// 只能被 rich-text 显示的标签
|
||||
richOnlyTags: makeMap('a,colgroup,fieldset,legend,table'
|
||||
// #ifdef MP-WEIXIN
|
||||
+ (canIUse ? ',bdi,bdo,caption,rt,ruby' : '')
|
||||
// #endif
|
||||
),
|
||||
// 自闭合的标签
|
||||
selfClosingTags: makeMap(
|
||||
'area,base,br,col,circle,ellipse,embed,frame,hr,img,input,line,link,meta,param,path,polygon,rect,source,track,use,wbr'
|
||||
),
|
||||
// 信任的标签
|
||||
trustTags: makeMap(
|
||||
'a,abbr,ad,audio,b,blockquote,br,code,col,colgroup,dd,del,dl,dt,div,em,fieldset,h1,h2,h3,h4,h5,h6,hr,i,img,ins,label,legend,li,ol,p,q,source,span,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,title,ul,video'
|
||||
// #ifdef MP-WEIXIN
|
||||
+ (canIUse ? ',bdi,bdo,caption,pre,rt,ruby' : '')
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
+ ',embed,iframe'
|
||||
// #endif
|
||||
),
|
||||
// 默认的标签样式
|
||||
userAgentStyles: {
|
||||
address: 'font-style:italic',
|
||||
big: 'display:inline;font-size:1.2em',
|
||||
blockquote: 'background-color:#f6f6f6;border-left:3px solid #dbdbdb;color:#6c6c6c;padding:5px 0 5px 10px',
|
||||
caption: 'display:table-caption;text-align:center',
|
||||
center: 'text-align:center',
|
||||
cite: 'font-style:italic',
|
||||
dd: 'margin-left:40px',
|
||||
mark: 'background-color:yellow',
|
||||
pre: 'font-family:monospace;white-space:pre;overflow:scroll',
|
||||
s: 'text-decoration:line-through',
|
||||
small: 'display:inline;font-size:0.8em',
|
||||
u: 'text-decoration:underline'
|
||||
}
|
||||
}
|
||||
|
||||
function makeMap(str) {
|
||||
var map = Object.create(null),
|
||||
list = str.split(',');
|
||||
for (var i = list.length; i--;)
|
||||
map[list[i]] = true;
|
||||
return map;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
var inline = {
|
||||
abbr: 1,
|
||||
b: 1,
|
||||
big: 1,
|
||||
code: 1,
|
||||
del: 1,
|
||||
em: 1,
|
||||
i: 1,
|
||||
ins: 1,
|
||||
label: 1,
|
||||
q: 1,
|
||||
small: 1,
|
||||
span: 1,
|
||||
strong: 1
|
||||
}
|
||||
module.exports = {
|
||||
use: function(item) {
|
||||
return !item.c && !inline[item.name] && (item.attrs.style || '').indexOf('display:inline') == -1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,519 @@
|
||||
<template>
|
||||
<view class="interlayer">
|
||||
<block v-for="(n, i) in nodes" v-bind:key="i">
|
||||
<!--图片-->
|
||||
<view v-if="n.name=='img'" :class="'_img '+n.attrs.class" :style="n.attrs.style" :data-attrs="n.attrs" @tap="imgtap">
|
||||
<rich-text v-if="controls[i]!=0" :nodes="[{attrs:{src:loading&&(controls[i]||0)<2?loading:(lazyLoad&&!controls[i]?placeholder:(controls[i]==3?errorImg:n.attrs.src||'')),alt:n.attrs.alt||'',width:n.attrs.width||'',style:'-webkit-touch-callout:none;max-width:100%;display:block'+(n.attrs.height?';height:'+n.attrs.height:'')},name:'img'}]" />
|
||||
<image class="_image" :src="lazyLoad&&!controls[i]?placeholder:n.attrs.src" :lazy-load="lazyLoad"
|
||||
:show-menu-by-longpress="!n.attrs.ignore" :data-i="i" :data-index="n.attrs.i" data-source="img" @load="loadImg"
|
||||
@error="error" />
|
||||
</view>
|
||||
<!--文本-->
|
||||
<text v-else-if="n.type=='text'" decode>{{n.text}}</text>
|
||||
<!--#ifndef MP-BAIDU-->
|
||||
<text v-else-if="n.name=='br'">\n</text>
|
||||
<!--#endif-->
|
||||
<!--视频-->
|
||||
<view v-else-if="((n.lazyLoad&&!n.attrs.autoplay)||(n.name=='video'&&!loadVideo))&&controls[i]==undefined" :id="n.attrs.id" :class="'_video '+(n.attrs.class||'')"
|
||||
:style="n.attrs.style" :data-i="i" @tap="_loadVideo" />
|
||||
<video v-else-if="n.name=='video'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :autoplay="n.attrs.autoplay||controls[i]==0"
|
||||
:controls="!n.attrs.autoplay||n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted" :poster="n.attrs.poster" :src="n.attrs.source[controls[i]||0]"
|
||||
:unit-id="n.attrs['unit-id']" :data-id="n.attrs.id" :data-i="i" data-source="video" @error="error" @play="play" />
|
||||
<!--音频-->
|
||||
<audio v-else-if="n.name=='audio'" :ref="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :author="n.attrs.author"
|
||||
:autoplay="n.attrs.autoplay" :controls="n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name" :poster="n.attrs.poster"
|
||||
:src="n.attrs.source[controls[i]||0]" :data-i="i" :data-id="n.attrs.id" data-source="audio"
|
||||
@error.native="error" @play.native="play" />
|
||||
<!--链接-->
|
||||
<view v-else-if="n.name=='a'" :id="n.attrs.id" :class="'_a '+(n.attrs.class||'')" hover-class="_hover" :style="n.attrs.style"
|
||||
:data-attrs="n.attrs" @tap="linkpress">
|
||||
<trees class="_span" :nodes="n.children" />
|
||||
</view>
|
||||
<!--广告-->
|
||||
<!--<ad v-else-if="n.name=='ad'" :class="n.attrs.class" :style="n.attrs.style" :unit-id="n.attrs['unit-id']" :appid="n.attrs.appid" :apid="n.attrs.apid" :type="n.attrs.type" :adpid="n.attrs.adpid" data-source="ad" @error="error" />-->
|
||||
<!--列表-->
|
||||
<view v-else-if="n.name=='li'" :id="n.attrs.id" :class="n.attrs.class" :style="(n.attrs.style||'')+';display:flex'">
|
||||
<view v-if="n.type=='ol'" class="_ol-bef">{{n.num}}</view>
|
||||
<view v-else class="_ul-bef">
|
||||
<view v-if="n.floor%3==0" class="_ul-p1">█</view>
|
||||
<view v-else-if="n.floor%3==2" class="_ul-p2" />
|
||||
<view v-else class="_ul-p1" style="border-radius:50%">█</view>
|
||||
</view>
|
||||
<!--#ifdef MP-ALIPAY-->
|
||||
<view class="_li">
|
||||
<trees :nodes="n.children" :lazyLoad="lazyLoad" :loading="loading" />
|
||||
</view>
|
||||
<!--#endif-->
|
||||
<!--#ifndef MP-ALIPAY-->
|
||||
<trees class="_li" :nodes="n.children" :lazyLoad="lazyLoad" :loading="loading" />
|
||||
<!--#endif-->
|
||||
</view>
|
||||
<!--表格-->
|
||||
<view v-else-if="n.name=='table'&&n.c" :id="n.attrs.id" :class="n.attrs.class" :style="(n.attrs.style||'')+';display:table'">
|
||||
<view v-for="(tbody, o) in n.children" v-bind:key="o" :class="tbody.attrs.class" :style="(tbody.attrs.style||'')+(tbody.name[0]=='t'?';display:table-'+(tbody.name=='tr'?'row':'row-group'):'')">
|
||||
<view v-for="(tr, p) in tbody.children" v-bind:key="p" :class="tr.attrs.class" :style="(tr.attrs.style||'')+(tr.name[0]=='t'?';display:table-'+(tr.name=='tr'?'row':'cell'):'')">
|
||||
<trees v-if="tr.name=='td'" :nodes="tr.children" />
|
||||
<block v-else>
|
||||
<!--#ifdef MP-ALIPAY-->
|
||||
<view v-for="(td, q) in tr.children" v-bind:key="q" :class="td.attrs.class" :style="(td.attrs.style||'')+(td.name[0]=='t'?';display:table-'+(td.name=='tr'?'row':'cell'):'')">
|
||||
<trees :nodes="td.children" />
|
||||
</view>
|
||||
<!--#endif-->
|
||||
<!--#ifndef MP-ALIPAY-->
|
||||
<trees v-for="(td, q) in tr.children" v-bind:key="q" :class="td.attrs.class" :style="(td.attrs.style||'')+(td.name[0]=='t'?';display:table-'+(td.name=='tr'?'row':'cell'):'')"
|
||||
:nodes="td.children" />
|
||||
<!--#endif-->
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!--#ifdef APP-PLUS-->
|
||||
<iframe v-else-if="n.name=='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen" :frameborder="n.attrs.frameborder"
|
||||
:width="n.attrs.width" :height="n.attrs.height" :src="n.attrs.src" />
|
||||
<embed v-else-if="n.name=='embed'" :style="n.attrs.style" :width="n.attrs.width" :height="n.attrs.height" :src="n.attrs.src" />
|
||||
<!--#endif-->
|
||||
<!--富文本-->
|
||||
<!--#ifdef MP-WEIXIN || MP-QQ || APP-PLUS-->
|
||||
<rich-text v-else-if="handler.use(n)" :id="n.attrs.id" :class="'_p __'+n.name" :nodes="[n]" />
|
||||
<!--#endif-->
|
||||
<!--#ifndef MP-WEIXIN || MP-QQ || APP-PLUS-->
|
||||
<rich-text v-else-if="!n.c" :id="n.attrs.id" :nodes="[n]" style="display:inline" />
|
||||
<!--#endif-->
|
||||
<!--#ifdef MP-ALIPAY-->
|
||||
<view v-else :id="n.attrs.id" :class="'_'+n.name+' '+(n.attrs.class||'')" :style="n.attrs.style">
|
||||
<trees :nodes="n.children" :lazyLoad="lazyLoad" :loading="loading" />
|
||||
</view>
|
||||
<!--#endif-->
|
||||
<!--#ifndef MP-ALIPAY-->
|
||||
<trees v-else :class="(n.attrs.id||'')+' _'+n.name+' '+(n.attrs.class||'')" :style="n.attrs.style" :nodes="n.children"
|
||||
:lazyLoad="lazyLoad" :loading="loading" />
|
||||
<!--#endif-->
|
||||
</block>
|
||||
</view>
|
||||
</template>
|
||||
<script module="handler" lang="wxs" src="./handler.wxs"></script>
|
||||
<script>
|
||||
global.Parser = {};
|
||||
import trees from './trees'
|
||||
const errorImg = require('../libs/config.js').errorImg;
|
||||
export default {
|
||||
components: {
|
||||
trees
|
||||
},
|
||||
name: 'trees',
|
||||
data() {
|
||||
return {
|
||||
controls: [],
|
||||
placeholder: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="300" height="225"/>',
|
||||
errorImg,
|
||||
loadVideo:
|
||||
// #ifdef APP-PLUS
|
||||
false
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
true
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
props: {
|
||||
nodes: Array,
|
||||
lazyLoad: Boolean,
|
||||
loading: String
|
||||
},
|
||||
mounted() {
|
||||
for (this.top = this.$parent; this.top.$options.name != 'parser'; this.top = this.top.$parent);
|
||||
this.init();
|
||||
},
|
||||
// #ifdef APP-PLUS
|
||||
beforeDestroy() {
|
||||
this.observer && this.observer.disconnect();
|
||||
},
|
||||
// #endif
|
||||
methods: {
|
||||
init() {
|
||||
for (var i = this.nodes.length, n; n = this.nodes[--i];) {
|
||||
if (n.name == 'img') {
|
||||
this.top.imgList.setItem(n.attrs.i, n.attrs.src);
|
||||
// #ifdef APP-PLUS
|
||||
if (this.lazyLoad && !this.observer) {
|
||||
this.observer = uni.createIntersectionObserver(this).relativeToViewport({
|
||||
top: 500,
|
||||
bottom: 500
|
||||
});
|
||||
this.$nextTick(() => {
|
||||
this.observer.observe('._img', res => {
|
||||
if (res.intersectionRatio) {
|
||||
for (var j = this.nodes.length; j--;)
|
||||
if (this.nodes[j].name == 'img')
|
||||
this.$set(this.controls, j, 1);
|
||||
this.observer.disconnect();
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
} else if (n.name == 'video' || n.name == 'audio') {
|
||||
var ctx;
|
||||
if (n.name == 'video') {
|
||||
ctx = uni.createVideoContext(n.attrs.id
|
||||
// #ifndef MP-BAIDU
|
||||
, this
|
||||
// #endif
|
||||
);
|
||||
} else if (this.$refs[n.attrs.id])
|
||||
ctx = this.$refs[n.attrs.id][0];
|
||||
if (ctx) {
|
||||
ctx.id = n.attrs.id;
|
||||
this.top.videoContexts.push(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
// #ifdef APP-PLUS
|
||||
// APP 上避免 video 错位需要延时渲染
|
||||
setTimeout(() => {
|
||||
this.loadVideo = true;
|
||||
}, 1000)
|
||||
// #endif
|
||||
},
|
||||
play(e) {
|
||||
var contexts = this.top.videoContexts;
|
||||
if (contexts.length > 1 && this.top.autopause)
|
||||
for (var i = contexts.length; i--;)
|
||||
if (contexts[i].id != e.currentTarget.dataset.id)
|
||||
contexts[i].pause();
|
||||
},
|
||||
imgtap(e) {
|
||||
var attrs = e.currentTarget.dataset.attrs;
|
||||
if (!attrs.ignore) {
|
||||
var preview = true,
|
||||
data = {
|
||||
id: e.target.id,
|
||||
src: attrs.src,
|
||||
ignore: () => preview = false
|
||||
};
|
||||
global.Parser.onImgtap && global.Parser.onImgtap(data);
|
||||
this.top.$emit('imgtap', data);
|
||||
if (preview) {
|
||||
var urls = this.top.imgList,
|
||||
current = urls[attrs.i] ? parseInt(attrs.i) : (urls = [attrs.src], 0);
|
||||
uni.previewImage({
|
||||
current,
|
||||
urls
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
loadImg(e) {
|
||||
var i = e.currentTarget.dataset.i;
|
||||
if (this.lazyLoad && !this.controls[i]) {
|
||||
// #ifdef QUICKAPP-WEBVIEW
|
||||
this.$set(this.controls, i, 0);
|
||||
this.$nextTick(function() {
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
this.$set(this.controls, i, 1);
|
||||
// #endif
|
||||
// #ifdef QUICKAPP-WEBVIEW
|
||||
})
|
||||
// #endif
|
||||
} else if (this.loading && this.controls[i] != 2) {
|
||||
// #ifdef QUICKAPP-WEBVIEW
|
||||
this.$set(this.controls, i, 0);
|
||||
this.$nextTick(function() {
|
||||
// #endif
|
||||
this.$set(this.controls, i, 2);
|
||||
// #ifdef QUICKAPP-WEBVIEW
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
linkpress(e) {
|
||||
var jump = true,
|
||||
attrs = e.currentTarget.dataset.attrs;
|
||||
attrs.ignore = () => jump = false;
|
||||
global.Parser.onLinkpress && global.Parser.onLinkpress(attrs);
|
||||
this.top.$emit('linkpress', attrs);
|
||||
if (jump) {
|
||||
// #ifdef MP
|
||||
if (attrs['app-id']) {
|
||||
return uni.navigateToMiniProgram({
|
||||
appId: attrs['app-id'],
|
||||
path: attrs.path
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
if (attrs.href) {
|
||||
if (attrs.href[0] == '#') {
|
||||
if (this.top.useAnchor)
|
||||
this.top.navigateTo({
|
||||
id: attrs.href.substring(1)
|
||||
})
|
||||
} else if (attrs.href.indexOf('http') == 0 || attrs.href.indexOf('//') == 0) {
|
||||
// #ifdef APP-PLUS
|
||||
plus.runtime.openWeb(attrs.href);
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
uni.setClipboardData({
|
||||
data: attrs.href,
|
||||
success: () =>
|
||||
uni.showToast({
|
||||
title: '链接已复制'
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
} else
|
||||
uni.navigateTo({
|
||||
url: attrs.href,
|
||||
fail() {
|
||||
uni.switchTab({
|
||||
url: attrs.href,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
error(e) {
|
||||
var target = e.currentTarget,
|
||||
source = target.dataset.source,
|
||||
i = target.dataset.i;
|
||||
if (source == 'video' || source == 'audio') {
|
||||
// 加载其他 source
|
||||
var index = this.controls[i] ? this.controls[i].i + 1 : 1;
|
||||
if (index < this.nodes[i].attrs.source.length)
|
||||
this.$set(this.controls, i, index);
|
||||
if (e.detail.__args__)
|
||||
e.detail = e.detail.__args__[0];
|
||||
} else if (errorImg && source == 'img') {
|
||||
this.top.imgList.setItem(target.dataset.index, errorImg);
|
||||
this.$set(this.controls, i, 3);
|
||||
}
|
||||
this.top && this.top.$emit('error', {
|
||||
source,
|
||||
target,
|
||||
errMsg: e.detail.errMsg
|
||||
});
|
||||
},
|
||||
_loadVideo(e) {
|
||||
this.$set(this.controls, e.target.dataset.i, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 在这里引入自定义样式 */
|
||||
|
||||
/* 链接和图片效果 */
|
||||
._a {
|
||||
display: inline;
|
||||
padding: 1.5px 0 1.5px 0;
|
||||
color: #366092;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
._hover {
|
||||
text-decoration: underline;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
._img {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* #ifdef MP-WEIXIN */
|
||||
:host {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
|
||||
/* #ifdef MP || QUICKAPP-WEBVIEW */
|
||||
.interlayer {
|
||||
display: inherit;
|
||||
flex-direction: inherit;
|
||||
flex-wrap: inherit;
|
||||
align-content: inherit;
|
||||
align-items: inherit;
|
||||
justify-content: inherit;
|
||||
width: 100%;
|
||||
white-space: inherit;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
|
||||
._b,
|
||||
._strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
._blockquote,
|
||||
._div,
|
||||
._p,
|
||||
._ol,
|
||||
._ul,
|
||||
._li {
|
||||
display: block;
|
||||
}
|
||||
|
||||
._code {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
._del {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
._em,
|
||||
._i {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
._h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
._h2 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
._h3 {
|
||||
font-size: 1.17em;
|
||||
}
|
||||
|
||||
._h5 {
|
||||
font-size: 0.83em;
|
||||
}
|
||||
|
||||
._h6 {
|
||||
font-size: 0.67em;
|
||||
}
|
||||
|
||||
._h1,
|
||||
._h2,
|
||||
._h3,
|
||||
._h4,
|
||||
._h5,
|
||||
._h6 {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
._image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
._ins {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
._li {
|
||||
flex: 1;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
._ol-bef {
|
||||
width: 36px;
|
||||
margin-right: 5px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
._ul-bef {
|
||||
margin: 0 12px 0 23px;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
._ol-bef,
|
||||
._ul_bef {
|
||||
flex: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
._ul-p1 {
|
||||
display: inline-block;
|
||||
width: 0.3em;
|
||||
height: 0.3em;
|
||||
overflow: hidden;
|
||||
line-height: 0.3em;
|
||||
}
|
||||
|
||||
._ul-p2 {
|
||||
display: inline-block;
|
||||
width: 0.23em;
|
||||
height: 0.23em;
|
||||
border: 0.05em solid black;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
._q::before {
|
||||
content: '"';
|
||||
}
|
||||
|
||||
._q::after {
|
||||
content: '"';
|
||||
}
|
||||
|
||||
._sub {
|
||||
font-size: smaller;
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
._sup {
|
||||
font-size: smaller;
|
||||
vertical-align: super;
|
||||
}
|
||||
|
||||
/* #ifdef MP-ALIPAY || APP-PLUS || QUICKAPP-WEBVIEW*/
|
||||
._abbr,
|
||||
._b,
|
||||
._code,
|
||||
._del,
|
||||
._em,
|
||||
._i,
|
||||
._ins,
|
||||
._label,
|
||||
._q,
|
||||
._span,
|
||||
._strong,
|
||||
._sub,
|
||||
._sup {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
|
||||
/* #ifdef MP-WEIXIN || MP-QQ */
|
||||
.__bdo,
|
||||
.__bdi,
|
||||
.__ruby,
|
||||
.__rt {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
._video {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 300px;
|
||||
height: 225px;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
._video::after {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin: -15px 0 0 -15px;
|
||||
content: '';
|
||||
border-color: transparent transparent transparent white;
|
||||
border-style: solid;
|
||||
border-width: 15px 0 15px 30px;
|
||||
}
|
||||
</style>
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
App.mpType = 'app'
|
||||
|
||||
const app = new Vue({
|
||||
...App
|
||||
})
|
||||
app.$mount()
|
||||
Executable
+119
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"name" : "在线客服",
|
||||
"appid" : "__UNI__4E887D0",
|
||||
"description" : "FastAdmin在线客服插件uni-app",
|
||||
"versionName" : "1.0.0",
|
||||
"versionCode" : "100",
|
||||
"transformPx" : false,
|
||||
/* 5+App特有相关 */
|
||||
"app-plus" : {
|
||||
"usingComponents" : true,
|
||||
"nvueCompiler" : "uni-app",
|
||||
"compilerVersion" : 3,
|
||||
"splashscreen" : {
|
||||
"alwaysShowBeforeRender" : true,
|
||||
"waiting" : true,
|
||||
"autoclose" : true,
|
||||
"delay" : 0
|
||||
},
|
||||
/* 模块配置 */
|
||||
"modules" : {},
|
||||
/* 应用发布信息 */
|
||||
"distribute" : {
|
||||
/* android打包配置 */
|
||||
"android" : {
|
||||
"permissions" : [
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WRITE_CONTACTS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
|
||||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CALL_PHONE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||
]
|
||||
},
|
||||
/* ios打包配置 */
|
||||
"ios" : {},
|
||||
/* SDK配置 */
|
||||
"sdkConfigs" : {},
|
||||
"icons" : {
|
||||
"android" : {
|
||||
"hdpi" : "",
|
||||
"xhdpi" : "",
|
||||
"xxhdpi" : "",
|
||||
"xxxhdpi" : ""
|
||||
},
|
||||
"ios" : {
|
||||
"appstore" : "",
|
||||
"ipad" : {
|
||||
"app" : "",
|
||||
"app@2x" : "",
|
||||
"notification" : "",
|
||||
"notification@2x" : "",
|
||||
"proapp@2x" : "",
|
||||
"settings" : "",
|
||||
"settings@2x" : "",
|
||||
"spotlight" : "",
|
||||
"spotlight@2x" : ""
|
||||
},
|
||||
"iphone" : {
|
||||
"app@2x" : "",
|
||||
"app@3x" : "",
|
||||
"notification@2x" : "",
|
||||
"notification@3x" : "",
|
||||
"settings@2x" : "",
|
||||
"settings@3x" : "",
|
||||
"spotlight@2x" : "",
|
||||
"spotlight@3x" : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
/* 快应用特有相关 */
|
||||
"quickapp" : {},
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin" : {
|
||||
"appid" : "wx4552387af63efaec",
|
||||
"setting" : {
|
||||
"urlCheck" : false,
|
||||
"es6" : false
|
||||
},
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-alipay" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-baidu" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-toutiao" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"h5" : {
|
||||
"router" : {
|
||||
"mode" : "hash",
|
||||
"base" : "/h5/"
|
||||
},
|
||||
"devServer" : {
|
||||
"https" : false
|
||||
},
|
||||
"domain" : "kefu.zhuangzhizhinengkeji.com",
|
||||
"title" : "在线客服"
|
||||
}
|
||||
}
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
|
||||
{
|
||||
"path" : "pages/kefu/index",
|
||||
"style" : {
|
||||
"navigationBarTitleText":"在线客服",
|
||||
"navigationBarTextStyle": "black"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/kefu/kefu",
|
||||
"style": {
|
||||
"navigationBarTitleText":"链接中...",
|
||||
"navigationBarTextStyle": "black"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "在线客服",
|
||||
"navigationBarBackgroundColor": "#F8F8F8",
|
||||
"backgroundColor": "#F8F8F8"
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
const expression = [
|
||||
{'title':'[zy]', 'src':'/emoji/1.png'},{'title':'[zm]', 'src':'/emoji/2.png'},{'title':'[jy]', 'src':'/emoji/3.png'},
|
||||
{'title':'[jyb]', 'src':'/emoji/4.png'},{'title':'[bx]', 'src':'/emoji/5.png'},{'title':'[kzn]', 'src':'/emoji/6.png'},
|
||||
{'title':'[gg]', 'src':'/emoji/7.png'},{'title':'[ll]', 'src':'/emoji/8.png'},{'title':'[jyll]', 'src':'/emoji/9.png'},
|
||||
{'title':'[o]', 'src':'/emoji/10.png'},{'title':'[yz]', 'src':'/emoji/11.png'},{'title':'[wx]', 'src':'/emoji/12.png'},
|
||||
{'title':'[zyb]', 'src':'/emoji/13.png'},{'title':'[tp]', 'src':'/emoji/14.png'},{'title':'[wxb]', 'src':'/emoji/15.png'},
|
||||
{'title':'[zyc]', 'src':'/emoji/16.png'},{'title':'[llb]', 'src':'/emoji/17.png'},{'title':'[xm]', 'src':'/emoji/18.png'},
|
||||
{'title':'[qz]', 'src':'/emoji/19.png'},{'title':'[zmb]', 'src':'/emoji/20.png'},{'title':'[kx]', 'src':'/emoji/21.png'},
|
||||
{'title':'[mm]', 'src':'/emoji/22.png'},{'title':'[bz]', 'src':'/emoji/23.png'},{'title':'[bkx]', 'src':'/emoji/24.png'},
|
||||
{'title':'[mg]', 'src':'/emoji/25.png'},{'title':'[pz]', 'src':'/emoji/26.png'},{'title':'[pzb]', 'src':'/emoji/27.png'},
|
||||
{'title':'[wxc]', 'src':'/emoji/28.png'},{'title':'[jyc]', 'src':'/emoji/29.png'},{'title':'[jyd]', 'src':'/emoji/30.png'},
|
||||
{'title':'[dm]', 'src':'/emoji/31.png'},{'title':'[tpb]', 'src':'/emoji/32.png'},{'title':'[tpc]', 'src':'/emoji/33.png'},
|
||||
{'title':'[tpd]', 'src':'/emoji/34.png'},{'title':'[ly]', 'src':'/emoji/35.png'},{'title':'[zyd]', 'src':'/emoji/36.png'},
|
||||
];
|
||||
|
||||
export default {
|
||||
baseURL: 'kefu.com', // 启动workerman服务的域名,无需填写协议和端口
|
||||
https_switch: false, // 是否启用https协议(默认关,正式版必开,且需要参考文档创建wss服务)
|
||||
expression
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<view class="main">
|
||||
<navigator url="/pages/kefu/kefu">打开客服会话窗口-游客</navigator>
|
||||
|
||||
<!-- <navigator url="/pages/kefu/kefu?token=b3cc58e6-b433-48a3-8dc9-8a28e4036b62">打开客服会话窗口-指定用户</navigator> -->
|
||||
|
||||
<!-- 可选参数,用户的 `token` 和 固定客服ID `fixed_csr` -->
|
||||
<!-- 不传递 `token` 则自动建立游客身份 -->
|
||||
<!-- 后台转移过客服代表的客户,不支持指定 `fixed_csr`-->
|
||||
<!-- 本客服系统客户端,依赖FastAdmin插件:`workerman在线客服系统`无法单独使用,插件介绍/官网地址:https://www.fastadmin.net/store/kefu.html -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.main{
|
||||
height: 50vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.main navigator{
|
||||
border: 1px solid #F2F2F2;
|
||||
padding: 20rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
</style>
|
||||
+1626
File diff suppressed because it is too large
Load Diff
Executable
+28
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>站外调用在线客服例子</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
|
||||
<!-- 在线客服依赖以下css和js文件,请按需引入 -->
|
||||
<link rel="stylesheet" type="text/css" href="http://kefu.cn/assets/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="http://kefu.cn/assets/addons/kefu/css/kefu_default.css">
|
||||
<script type="text/javascript" src="http://kefu.cn/assets/libs/jquery/dist/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="http://kefu.cn/assets/addons/kefu/js/kefu.js"></script>
|
||||
<script type="text/javascript" src="http://kefu.cn/assets/libs/fastadmin-layer/dist/layer.js"></script>
|
||||
<script type="text/javascript" src="http://kefu.cn/assets/libs/bootstrap/dist/js/bootstrap.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="text/javascript">
|
||||
KeFu.initialize('kefu.cn', 'index');
|
||||
// 参数一为在线客服所在网站的域名(启动Workerman服务的网站的域名)
|
||||
// 参数二为模块名,站外直接填写index
|
||||
// 参数三为初始化完成后的回调方法
|
||||
// 参数四为指定客服,可在此处填写客服代表的后台账户id
|
||||
// 若要站外调用,请于后台-》插件管理-》本插件的配置中-》跨站调用允许域名-》填写外站的域名
|
||||
|
||||
// 您也可以参考站内其他模块调用的可运行示例,访问路径 `http://您的域名/addons/kefu`
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
name = kefu
|
||||
title = Workerman在线客服
|
||||
intro = 一款基于WebSocket的在线客服插件
|
||||
author = 白衣素袖
|
||||
website = https://www.fastadmin.net
|
||||
version = 1.0.7
|
||||
state = 1
|
||||
url = /addons/kefu
|
||||
license = regular
|
||||
licenseto = 16556
|
||||
Executable
+269
@@ -0,0 +1,269 @@
|
||||
-- ----------------------------
|
||||
-- 配置表
|
||||
-- ----------------------------
|
||||
CREATE TABLE IF NOT EXISTS `__PREFIX__kefu_config` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`name` varchar(50) NOT NULL DEFAULT '' COMMENT '变量名',
|
||||
`value` text COMMENT '变量值',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='客服配置表';
|
||||
|
||||
-- ----------------------------
|
||||
-- 插入配置项
|
||||
-- ----------------------------
|
||||
BEGIN;
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_config` VALUES ('1', 'chat_name', '在线客服');
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_config` VALUES ('2', 'ecs_exit', '1');
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_config` VALUES ('3', 'send_message_key', '1');
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_config` VALUES ('4', 'new_user_tip', '您准备好体验在线客服系统了吗?');
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_config` VALUES ('5', 'new_user_msg', '这是一个欢迎新用户的消息,系统为用户成功分配客服后,自动以该客服身份发送此消息~单客服的欢迎消息请于:客服管理-》客服代表管理进行设置');
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_config` VALUES ('6', 'csr_distribution', '2');
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_config` VALUES ('7', 'announcement', '这是一条公告!你可以在后台管理->客服管理->会话窗口中进行更换公告内容!');
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_config` VALUES ('8', 'slider_images', '/assets/addons/kefu/img/slider1.jpg,/assets/addons/kefu/img/slider2.jpg');
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_config` VALUES ('9', 'chat_introduces', '<p style=\"line-height: 1.6;\">\r\n <b><span style=\"font-size: 16px;\">功能简介</span></b><br>\r\n</p>\r\n<p>\r\n <b>模块化开发</b><br>\r\n 强大的一键生成功能极速简化你的开发流程,加快你的项目开发\r\n</p>\r\n<p>\r\n <b>响应式布局</b><br>\r\n 自动适配,无需要担心兼容性问题\r\n</p>\r\n<p>\r\n <b>完善的权限管理</b><br>\r\n 自由分配子级权限、一个管理员可同时属于多个组别\r\n</p>\r\n<p>\r\n <b>通用的会员和API模块</b><br>\r\n 共用同一账号体系的Web端会员中心权限验证和API接口会员权限验证\r\n</p>\r\n<p>\r\n <b>丰富的应用市场</b><br>\r\n 第三方云存储、云短信、富文本编辑器、CMS、博客、文档生成,一切均可在线安装卸载\r\n</p>\r\n<p></p>');
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_config` VALUES ('10', 'auto_invitation_switch', '1');
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_config` VALUES ('11', 'auto_invitation_timing', '7');
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_config` VALUES ('12', 'invite_box_img', '/assets/addons/kefu/img/invite_box_img.jpg');
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_config` VALUES ('13', 'csr_admin', '1');
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_config` VALUES ('14', 'trajectory_save_cycle', '1');
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_config` VALUES ('15', 'wechat_app_id', '');
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_config` VALUES ('16', 'wechat_app_secret', '');
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_config` VALUES ('17', 'wechat_token', '');
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_config` VALUES ('18', 'wechat_encodingkey', '');
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_config` VALUES ('19', 'new_message_notice', '');
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_config` VALUES ('20', 'only_first_invitation', '1');
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_config` VALUES ('21', 'new_message_shake', '3');
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_config` VALUES ('22', 'only_csr_online_invitation', '1');
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_config` VALUES ('23', 'kbs_switch', '1');
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_config` VALUES ('24', 'input_status_display', '2');
|
||||
COMMIT;
|
||||
|
||||
-- ----------------------------
|
||||
-- 客服代表配置表
|
||||
-- ----------------------------
|
||||
CREATE TABLE IF NOT EXISTS `__PREFIX__kefu_csr_config` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`admin_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '绑定管理员',
|
||||
`ceiling` tinyint(3) unsigned NOT NULL DEFAULT '1' COMMENT '接待上限',
|
||||
`reception_count` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '当前接待量',
|
||||
`last_reception_time` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '上次接待时间',
|
||||
`keep_alive` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否保持在线',
|
||||
`welcome_msg` text COMMENT '欢迎语',
|
||||
`status` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '状态:0=离线,1=繁忙,2=离开,3=在线',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='客服代表(csr)配置表';
|
||||
|
||||
-- ----------------------------
|
||||
-- 插入客服代表
|
||||
-- ----------------------------
|
||||
BEGIN;
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_csr_config` VALUES ('1', '1', '8', '1', '1567046865', '0', '欢迎访问!', '0');
|
||||
COMMIT;
|
||||
|
||||
-- ----------------------------
|
||||
-- 用户留言表
|
||||
-- ----------------------------
|
||||
CREATE TABLE IF NOT EXISTS `__PREFIX__kefu_leave_message` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '用户(KeFu用户ID)',
|
||||
`name` varchar(50) NOT NULL DEFAULT '' COMMENT '姓名',
|
||||
`contact` varchar(50) NOT NULL DEFAULT '' COMMENT '联系方式',
|
||||
`message` text COMMENT '留言内容',
|
||||
`createtime` int(10) unsigned DEFAULT '0' COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='用户留言记录';
|
||||
|
||||
-- ----------------------------
|
||||
-- 客服接待记录
|
||||
-- ----------------------------
|
||||
CREATE TABLE IF NOT EXISTS `__PREFIX__kefu_reception_log` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`csr_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '客服代表ID',
|
||||
`user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '用户(KeFu用户ID)',
|
||||
`createtime` int(10) unsigned DEFAULT NULL COMMENT '接待时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='客服接待记录';
|
||||
|
||||
-- ----------------------------
|
||||
-- 聊天记录表
|
||||
-- ----------------------------
|
||||
CREATE TABLE IF NOT EXISTS `__PREFIX__kefu_record` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`session_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '会话ID',
|
||||
`sender_identity` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '发送人身份:0=客服,1=用户',
|
||||
`sender_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '发送人ID',
|
||||
`message_type` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '消息类型:0=富文本,1=图片,2=文件,3=系统消息,4=商品卡片,5=订单卡片',
|
||||
`message` text COMMENT '消息',
|
||||
`status` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '状态:0=未读,1=已读',
|
||||
`createtime` int(10) unsigned DEFAULT NULL COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='聊天记录表';
|
||||
|
||||
-- ----------------------------
|
||||
-- 会话表
|
||||
-- ----------------------------
|
||||
CREATE TABLE IF NOT EXISTS `__PREFIX__kefu_session` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户',
|
||||
`csr_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '客服代表ID',
|
||||
`createtime` int(10) unsigned DEFAULT NULL COMMENT '创建时间',
|
||||
`deletetime` int(10) unsigned DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='客服会话表';
|
||||
|
||||
-- ----------------------------
|
||||
-- 用户轨迹表
|
||||
-- ----------------------------
|
||||
CREATE TABLE IF NOT EXISTS `__PREFIX__kefu_trajectory` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'KeFu用户',
|
||||
`csr_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '客服代表',
|
||||
`log_type` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '轨迹类型:0=访问,1=被邀请,2=开始对话,3=拒绝会话,4=客服添加,5=关闭页面,6=留言,7=其他',
|
||||
`note` text COMMENT '轨迹详情',
|
||||
`url` text COMMENT '轨迹额外数据',
|
||||
`referrer` text COMMENT '来路',
|
||||
`createtime` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '添加时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='用户轨迹表';
|
||||
|
||||
-- ----------------------------
|
||||
-- 插件用户表
|
||||
-- ----------------------------
|
||||
CREATE TABLE IF NOT EXISTS `__PREFIX__kefu_user` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '对应用户ID',
|
||||
`avatar` varchar(100) NOT NULL DEFAULT '' COMMENT '头像',
|
||||
`nickname` varchar(50) NOT NULL DEFAULT '' COMMENT '昵称',
|
||||
`referrer` varchar(255) NOT NULL DEFAULT '' COMMENT '用户来路',
|
||||
`contact` varchar(100) NOT NULL DEFAULT '' COMMENT '联系方式',
|
||||
`note` varchar(255) NOT NULL DEFAULT '' COMMENT '客服备注',
|
||||
`token` varchar(59) NOT NULL DEFAULT '' COMMENT 'Session标识',
|
||||
`wechat_openid` varchar(28) NOT NULL DEFAULT '' COMMENT '微信openid',
|
||||
`createtime` int(10) unsigned DEFAULT NULL COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='客服用户表';
|
||||
|
||||
-- ---------------------------
|
||||
-- 黑名单表
|
||||
-- ---------------------------
|
||||
CREATE TABLE IF NOT EXISTS `__PREFIX__kefu_blacklist` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '被屏蔽人(KeFu用户ID)',
|
||||
`admin_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '操作客服',
|
||||
`createtime` int(10) unsigned DEFAULT NULL COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='客服黑名单表';
|
||||
|
||||
-- ---------------------------
|
||||
-- 快捷回复表
|
||||
-- ---------------------------
|
||||
CREATE TABLE IF NOT EXISTS `__PREFIX__kefu_fast_reply` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`admin_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '所属客服',
|
||||
`title` varchar(100) NOT NULL DEFAULT '' COMMENT '标题',
|
||||
`content` text NOT NULL COMMENT '回复内容',
|
||||
`status` enum('1','0') NOT NULL DEFAULT '1' COMMENT '状态:0=关闭,1=启用',
|
||||
`createtime` int(10) unsigned DEFAULT NULL COMMENT '创建时间',
|
||||
`deletetime` int(10) unsigned DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='快捷回复表';
|
||||
|
||||
-- ----------------------------
|
||||
-- 插入通用快捷回复
|
||||
-- ----------------------------
|
||||
BEGIN;
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_fast_reply` VALUES ('1', '0', '打招呼', '您好,请问有什么可以帮您?', '1', '1567332795', null);
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_fast_reply` VALUES ('2', '0', '询问联系方式', '您可以提供下您的联系方式么?您的电话是?或者QQ,我们可以更方便的联系您!', '1', '1567338640', null);
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_fast_reply` VALUES ('3', '0', '提示客户等待-处理中', '请稍等片刻,我们正在为您处理!', '1', '1567338672', null);
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_fast_reply` VALUES ('4', '0', '提示客户等待-问', '我去问一下,您稍等片刻~', '1', '1567338693', null);
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_fast_reply` VALUES ('5', '0', '道别', '那好,祝您生活愉快,再见!', '1', '1567338716', null);
|
||||
COMMIT;
|
||||
|
||||
-- ---------------------------
|
||||
-- 知识库表
|
||||
-- ---------------------------
|
||||
CREATE TABLE IF NOT EXISTS `__PREFIX__kefu_kbs` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`questions` text COMMENT '知识点',
|
||||
`match` tinyint(3) unsigned NOT NULL DEFAULT '100' COMMENT '自动回复匹配度',
|
||||
`answer` text COMMENT '问题答案',
|
||||
`admin_id` varchar(100) NOT NULL DEFAULT '' COMMENT '限定客服生效',
|
||||
`status` enum('2','1','0') NOT NULL DEFAULT '0' COMMENT '状态:0=关闭,1=启用,2=启用为万能知识',
|
||||
`weigh` int(10) NOT NULL DEFAULT '1' COMMENT '权重',
|
||||
`createtime` int(10) unsigned DEFAULT NULL COMMENT '创建时间',
|
||||
`deletetime` int(10) unsigned DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='知识库表';
|
||||
|
||||
-- ----------------------------
|
||||
-- 插入知识点
|
||||
-- ----------------------------
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_kbs` VALUES ('1', '万能知识', '100', '<p>我是来自知识库的万能知识~</p><p>我不计算匹配度,只要没有任何知识点被匹配到,且没被“限定客服生效”所限定,就会回复我了~</p><p><b><span style="font-size: 12px;">万能知识常用于限定客服才生效,若不需要请直接从知识库删除。</span></b></p>', '1', '2', '1', '1574075097', null);
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_kbs` VALUES ('2', '你好\r\n您好\r\n在吗\r\n在?\r\n在', '80', '<p>亲,在的呢~</p><p>这是一条来自知识库的自动回复~</p>', '', '1', '2', '1574072922', null);
|
||||
|
||||
-- ---------------------------
|
||||
-- 窗口工具栏表
|
||||
-- ---------------------------
|
||||
CREATE TABLE IF NOT EXISTS `__PREFIX__kefu_toolbar` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`position` enum('frontend','backend','general') NOT NULL DEFAULT 'backend' COMMENT '工具位置:backend=后台,frontend=前台,general=通用',
|
||||
`mark` varchar(20) NOT NULL DEFAULT '' COMMENT '唯一标识',
|
||||
`title` varchar(20) NOT NULL DEFAULT '' COMMENT '标题',
|
||||
`icon_image` varchar(200) NOT NULL DEFAULT '' COMMENT '图标',
|
||||
`data_api` varchar(200) NOT NULL DEFAULT '' COMMENT '数据接口Url',
|
||||
`card_url` varchar(200) NOT NULL DEFAULT '' COMMENT '消息卡片Url',
|
||||
`card_frontend_url` varchar(200) NOT NULL DEFAULT '' COMMENT '消息卡片Url(uni端)',
|
||||
`status` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '状态:0=隐藏,1=正常',
|
||||
`deletetime` int(10) unsigned DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='窗口工具栏表';
|
||||
|
||||
-- ----------------------------
|
||||
-- 插入预设工具
|
||||
-- ----------------------------
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_toolbar` VALUES ('6', 'general', 'expression', '发送表情', '/assets/addons/kefu/img/smiley.png', '', '', '', '1', null);
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_toolbar` VALUES ('5', 'general', 'file', '发送文件', '/assets/addons/kefu/img/attachment.png', '', '', '', '1', null);
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_toolbar` VALUES ('4', 'general', 'link', '发送链接', '/assets/addons/kefu/img/link.png', '', '', '', '1', null);
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_toolbar` VALUES ('3', 'backend', 'fastreply', '快捷回复', '/assets/addons/kefu/img/fastreply.png', '', '', '', '1', null);
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_toolbar` VALUES ('2', 'frontend', 'goods', '发送商品', '/assets/addons/kefu/img/goods.png', '/api/Kefu/goodsList', '/bISTVBHhuo.php/user/user', '', '0', null);
|
||||
INSERT IGNORE INTO `__PREFIX__kefu_toolbar` VALUES ('1', 'frontend', 'order', '发送订单', '/assets/addons/kefu/img/order.png', '/api/Kefu/orderList', '/bISTVBHhuo.php/kefu/csrkpi', '', '0', null);
|
||||
|
||||
-- ----------------------------
|
||||
-- 旧版本字段处理
|
||||
-- ----------------------------
|
||||
BEGIN;
|
||||
ALTER TABLE `__PREFIX__kefu_blacklist` ADD COLUMN `admin_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '操作客服' AFTER `user_id`;
|
||||
COMMIT;
|
||||
|
||||
BEGIN;
|
||||
ALTER TABLE `__PREFIX__kefu_csr_config` ADD COLUMN `keep_alive` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否保持在线' AFTER `last_reception_time`;
|
||||
COMMIT;
|
||||
|
||||
BEGIN;
|
||||
ALTER TABLE `__PREFIX__kefu_user` ADD COLUMN `referrer` varchar(255) NOT NULL DEFAULT '' COMMENT '用户来路' AFTER `nickname`;
|
||||
ALTER TABLE `__PREFIX__kefu_user` ADD COLUMN `contact` varchar(100) NOT NULL DEFAULT '' COMMENT '联系方式' AFTER `referrer`;
|
||||
ALTER TABLE `__PREFIX__kefu_user` ADD COLUMN `note` varchar(255) NOT NULL DEFAULT '' COMMENT '客服备注' AFTER `contact`;
|
||||
COMMIT;
|
||||
|
||||
BEGIN;
|
||||
ALTER TABLE `__PREFIX__kefu_record` MODIFY message_type tinyint(1) comment '消息类型:0=富文本,1=图片,2=文件,3=系统消息,4=商品卡片,5=订单卡片';
|
||||
COMMIT;
|
||||
|
||||
BEGIN;
|
||||
ALTER TABLE `__PREFIX__kefu_csr_config` ADD COLUMN `welcome_msg` text COMMENT '欢迎语' AFTER `keep_alive`;
|
||||
COMMIT;
|
||||
|
||||
BEGIN;
|
||||
ALTER TABLE `__PREFIX__kefu_blacklist` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
ALTER TABLE `__PREFIX__kefu_config` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
ALTER TABLE `__PREFIX__kefu_csr_config` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
ALTER TABLE `__PREFIX__kefu_fast_reply` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
ALTER TABLE `__PREFIX__kefu_kbs` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
ALTER TABLE `__PREFIX__kefu_leave_message` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
ALTER TABLE `__PREFIX__kefu_reception_log` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
ALTER TABLE `__PREFIX__kefu_record` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
ALTER TABLE `__PREFIX__kefu_session` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
ALTER TABLE `__PREFIX__kefu_toolbar` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
ALTER TABLE `__PREFIX__kefu_trajectory` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
ALTER TABLE `__PREFIX__kefu_user` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
COMMIT;
|
||||
Executable
+1320
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,409 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
/**
|
||||
* 用于检测业务代码死循环或者长时间阻塞等问题
|
||||
* 如果发现业务卡死,可以将下面declare打开(去掉//注释),并执行php start.php reload
|
||||
* 然后观察一段时间workerman.log看是否有process_timeout异常
|
||||
*/
|
||||
|
||||
namespace addons\kefu\library\GatewayWorker\Applications\KeFu;
|
||||
|
||||
//declare(ticks=1);
|
||||
|
||||
use addons\kefu\library\Common;
|
||||
use GatewayWorker\Lib\Gateway;
|
||||
use Workerman\Lib\Timer;
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* 主逻辑
|
||||
* 主要是处理 onConnect onMessage onClose 三个方法
|
||||
* onConnect 和 onClose 如果不需要可以不用实现并删除
|
||||
*/
|
||||
class Events
|
||||
{
|
||||
/**
|
||||
* WebSocket 链接成功
|
||||
*
|
||||
* @param int $client_id data
|
||||
* @param $[data] [websocket握手时的http头数据,包含get、server等变量]
|
||||
*/
|
||||
public static function onWebSocketConnect($client_id, $data)
|
||||
{
|
||||
|
||||
// 安全检查
|
||||
array_walk_recursive($data, ['addons\kefu\library\Common', 'checkVariable']);
|
||||
|
||||
$now_time = time();
|
||||
$initialize_data = [];
|
||||
$initialize_data['chat_name'] = Db::name('kefu_config')->where('name', 'chat_name')->value('value');
|
||||
$agreement = (stripos($data['server']['HTTP_ORIGIN'], 'https://') === false) ? 'http://' : 'https://';
|
||||
$_SESSION['cdn_url'] = $agreement . $data['server']['SERVER_NAME']; //设置服务器域名
|
||||
$kefu_config = get_addon_config('kefu');
|
||||
|
||||
$upload = \app\common\model\Config::upload();
|
||||
// 上传信息配置后
|
||||
\think\Hook::listen("upload_config_init", $upload);
|
||||
$_SESSION['cdn_url'] = $upload['cdnurl'] ? $upload['cdnurl'] : $_SESSION['cdn_url'];
|
||||
|
||||
|
||||
// 获取连接人信息
|
||||
$token_info = false;
|
||||
|
||||
if (!isset($data['get']['modulename'])) {
|
||||
|
||||
Gateway::sendToClient($client_id, json_encode([
|
||||
'code' => 0,
|
||||
'msgtype' => 'clear',
|
||||
'msg' => $initialize_data['chat_name'] . ' 模块未知',
|
||||
]));
|
||||
return;
|
||||
}
|
||||
|
||||
if ($data['get']['modulename'] == 'admin' && isset($data['get']['token'])) {
|
||||
// 验证管理员身份
|
||||
$token_info = Common::checkAdmin($data['get']['token']);
|
||||
|
||||
// 设置定时器,定时检测管理员身份是否过期
|
||||
$_SESSION['auth_timer_id'] = Timer::add(30, function ($client_id, $token) {
|
||||
$token_info = Common::checkAdmin($token);
|
||||
if (!$token_info) {
|
||||
Gateway::closeClient($client_id);
|
||||
}
|
||||
}, [$client_id, $data['get']['token']]);
|
||||
|
||||
} elseif ($data['get']['modulename'] != 'admin' && isset($data['get']['token'])) {
|
||||
// 验证FA用户身份
|
||||
$user_id = Common::checkFaUser($data['get']['token']);
|
||||
if ($user_id) {
|
||||
// 验证KeFu用户身份
|
||||
$token_info = Common::checkKefuUser('', $user_id);
|
||||
if ($token_info) {
|
||||
// 设置定时器,定时检测用户身份是否过期
|
||||
$_SESSION['auth_timer_id'] = Timer::add(60, function ($client_id, $token) {
|
||||
$user_id = Common::checkFaUser($token);
|
||||
if (!$user_id) {
|
||||
Gateway::closeClient($client_id);
|
||||
}
|
||||
}, [$client_id, $data['get']['token']]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ($data['get']['modulename'] != 'admin' && isset($data['get']['kefu_tourists_token']) && !$token_info) {
|
||||
// 验证KeFu用户身份
|
||||
$token_info = Common::checkKefuUser($data['get']['kefu_tourists_token'], 0);
|
||||
}
|
||||
|
||||
if ($token_info) {
|
||||
|
||||
if (isset($token_info['token'])) {
|
||||
unset($token_info['token']);
|
||||
}
|
||||
|
||||
if (isset($token_info['blacklist']) && $token_info['blacklist']) {
|
||||
Gateway::sendToClient($client_id, json_encode([
|
||||
'code' => 0,
|
||||
'msgtype' => 'clear',
|
||||
'msg' => $initialize_data['chat_name'] . ' 黑名单用户!',
|
||||
]));
|
||||
return;
|
||||
}
|
||||
|
||||
Gateway::bindUid($client_id, $token_info['user_id']);
|
||||
$_SESSION['user_id'] = $token_info['user_id'];
|
||||
} else {
|
||||
|
||||
Gateway::sendToClient($client_id, json_encode([
|
||||
'code' => 0,
|
||||
'msgtype' => 'clear',
|
||||
'msg' => $initialize_data['chat_name'] . ' 无法识别链接用户身份,请重新登录!',
|
||||
]));
|
||||
return;
|
||||
}
|
||||
|
||||
if ($data['get']['modulename'] == 'admin') {
|
||||
|
||||
// 读取会话列表
|
||||
$session = Db::name('kefu_session')
|
||||
->alias('s')
|
||||
->field('s.*,CONCAT(u.id,"||user") as session_user,u.user_id as fu_user_id,u.avatar,u.nickname,u.wechat_openid,fu.avatar as fu_avatar,fu.nickname as fu_nickname')
|
||||
->join('kefu_user u', 'u.id=s.user_id')
|
||||
->join('user fu', 'u.user_id=fu.id', 'LEFT')
|
||||
->where('s.csr_id', $token_info['id'])
|
||||
->where('s.deletetime', null)
|
||||
->limit(40)
|
||||
->order('s.createtime desc')
|
||||
->select();
|
||||
|
||||
$session = array_reverse($session, false); // 会话分组时数组键将被逆转,最终给到前台的则是可以直接for in的数组
|
||||
|
||||
// 会话列表分组 在线的且上次消息时间在最近的-放入对话中 不在线的或者上次消息时间较久的放入最近沟通
|
||||
$session_temp = [];
|
||||
foreach ($session as $key => $value) {
|
||||
|
||||
// 最后一条聊天记录
|
||||
$last_message = Db::name('kefu_record')
|
||||
->where('session_id', $value['id'])
|
||||
->order('createtime desc')
|
||||
->find();
|
||||
|
||||
$value['last_message'] = Common::formatMessage($last_message);
|
||||
$value['last_time'] = Common::formatSessionTime(isset($last_message['createtime']) ? $last_message['createtime'] : null);
|
||||
|
||||
$value['online'] = $value['wechat_openid'] ? 1 : Gateway::isUidOnline($value['session_user']);
|
||||
$value['avatar'] = $value['fu_avatar'] ? $value['fu_avatar'] : $value['avatar'];
|
||||
$value['nickname'] = $value['fu_nickname'] ? $value['fu_nickname'] : $value['nickname'];
|
||||
$value['avatar'] = Common::imgSrcFill($value['avatar'], true);
|
||||
|
||||
// 用户发来的未读消息数
|
||||
$value['unread_msg_count'] = Db::name('kefu_record')
|
||||
->where('session_id', $value['id'])
|
||||
->where('sender_identity', 1)
|
||||
->where('sender_id', $value['user_id'])
|
||||
->where('status', 0)
|
||||
->count('id');
|
||||
|
||||
$last_time = isset($last_message['createtime']) ? $last_message['createtime'] : $value['createtime'];
|
||||
|
||||
$dialogue_time = $value['wechat_openid'] ? 600 : 43200; // 这个时间内的会话计入会话中
|
||||
|
||||
if ($value['online'] || ($now_time - $last_time < $dialogue_time) || $value['unread_msg_count'] > 0) {
|
||||
$session_temp['dialogue'][] = $value;
|
||||
} else {
|
||||
$session_temp['recently'][] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// 客服上线
|
||||
$reception_count = isset($session_temp['dialogue']) ? count($session_temp['dialogue']) : false;
|
||||
if ($reception_count) {
|
||||
Db::name('kefu_csr_config')->where('admin_id', $token_info['id'])->update([
|
||||
'reception_count' => $reception_count,
|
||||
]);
|
||||
}
|
||||
|
||||
// 获取访问中(邀请中)的用户->查询对应的用户信息
|
||||
$invitation = Gateway::getAllUidList();
|
||||
$invitation_user_ids = [];
|
||||
|
||||
foreach ($invitation as $key => $value) {
|
||||
$invitation_user_id = explode('||', $value);
|
||||
|
||||
if (isset($invitation_user_id[1]) && $invitation_user_id[1] != 'csr' && (int)$invitation_user_id[0] > 0) {
|
||||
$invitation_user_ids[] = (int)$invitation_user_id[0];
|
||||
}
|
||||
}
|
||||
|
||||
$invitation_user_ids = implode(',', $invitation_user_ids);
|
||||
$invitation = Db::name('kefu_user')
|
||||
->alias('u')
|
||||
->field('u.id,u.avatar,u.nickname,u.createtime,s.id as sid,fu.avatar as fu_avatar,fu.nickname as fu_nickname')
|
||||
->join('user fu', 'u.user_id=fu.id', 'LEFT')
|
||||
->join('kefu_session s', 's.user_id=u.id', 'LEFT')
|
||||
->whereIn('u.id', $invitation_user_ids)
|
||||
->where('s.id', null)
|
||||
->select();
|
||||
|
||||
foreach ($invitation as $key => $value) {
|
||||
|
||||
/*$trajectory = Db::name('kefu_trajectory')
|
||||
->where('user_id', $value['id'])
|
||||
->order('id desc')
|
||||
->find();*/
|
||||
|
||||
$invitation[$key]['id'] = 'invitation||' . $value['id'];
|
||||
$invitation[$key]['avatar'] = $value['fu_avatar'] ? $value['fu_avatar'] : $value['avatar'];
|
||||
$invitation[$key]['avatar'] = Common::imgSrcFill($invitation[$key]['avatar'], true);
|
||||
$invitation[$key]['nickname'] = $value['fu_nickname'] ? $value['fu_nickname'] : $value['nickname'];
|
||||
$invitation[$key]['online'] = 1;
|
||||
$invitation[$key]['unread_msg_count'] = 0;
|
||||
$invitation[$key]['last_message'] = '';
|
||||
$invitation[$key]['session_user'] = $value['id'] . '||user';
|
||||
$invitation[$key]['last_time'] = Common::formatSessionTime($value['createtime']);
|
||||
}
|
||||
|
||||
$session_temp['invitation'] = $invitation;
|
||||
|
||||
$initialize_data['session'] = $session_temp;
|
||||
|
||||
// 获取状态
|
||||
$token_info['status_text'] = Common::csrStatus(null);
|
||||
$tourists = 'not';
|
||||
|
||||
} else {
|
||||
|
||||
if (!Db::name('kefu_session')->where('user_id', $token_info['id'])->value('id')) {
|
||||
|
||||
// 无客服游客-供前台建立会话
|
||||
$avatar = $token_info['fu_avatar'] ? $token_info['fu_avatar'] : $token_info['avatar'];
|
||||
$tourists = [
|
||||
'id' => 'invitation||' . $token_info['id'],
|
||||
'avatar' => Common::imgSrcFill($avatar, true),
|
||||
'nickname' => $token_info['fu_nickname'] ? $token_info['fu_nickname'] : $token_info['nickname'],
|
||||
'online' => 1,
|
||||
'unread_msg_count' => 0,
|
||||
'session_user' => $token_info['id'] . '||user',
|
||||
'last_message' => '',
|
||||
'last_time' => Common::formatSessionTime($token_info['createtime']),
|
||||
];
|
||||
} else {
|
||||
$tourists = 'not';
|
||||
}
|
||||
}
|
||||
|
||||
$initialize_data['modulename'] = $data['get']['modulename'];
|
||||
$initialize_data['user_info'] = $token_info;
|
||||
$initialize_data['new_msg'] = Common::getUnreadMessages($_SESSION['user_id'], true);
|
||||
|
||||
// 向当前client_id发送数据
|
||||
Gateway::sendToClient($client_id, json_encode(['msgtype' => 'initialize', 'data' => $initialize_data]));
|
||||
|
||||
// 向所有人发送
|
||||
Gateway::sendToAll(json_encode([
|
||||
'msgtype' => 'online',
|
||||
'user_id' => $_SESSION['user_id'],
|
||||
'user_name' => isset($token_info['fu_nickname']) ? ($token_info['fu_nickname'] . '(' . $token_info['nickname'] . ')') : $token_info['nickname'],
|
||||
'tourists' => $tourists,
|
||||
'modulename' => $data['get']['modulename'],
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* 当客户端发来消息时触发
|
||||
* @param int $client_id 连接id
|
||||
* @param mixed $message 具体消息
|
||||
*/
|
||||
public static function onMessage($client_id, $message)
|
||||
{
|
||||
$chat_name = Db::name('kefu_config')->where('name', 'chat_name')->value('value');
|
||||
|
||||
// 分发到控制器
|
||||
$data = json_decode($message, true);
|
||||
|
||||
// 安全检查
|
||||
array_walk_recursive($data, ['addons\kefu\library\Common', 'checkVariable']);
|
||||
|
||||
if (!is_array($data) || !isset($data['c']) || !isset($data['a'])) {
|
||||
|
||||
common::showMsg($client_id, $chat_name . ' 错误的请求!');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($data['c'] == 'clear') {
|
||||
Gateway::closeClient($client_id);
|
||||
return '';
|
||||
}
|
||||
|
||||
$filename = __DIR__ . '/controller/' . $data['c'] . '.php'; //载入文件类似/controller/index.php
|
||||
|
||||
if (file_exists($filename)) {
|
||||
|
||||
require_once $filename;
|
||||
|
||||
/*
|
||||
检查要访问的类是否存在
|
||||
*/
|
||||
if (!class_exists($data['c'], false)) {
|
||||
|
||||
common::showMsg($client_id, $chat_name . ' 您访问的控制器不存在!');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
|
||||
common::showMsg($client_id, $chat_name . ' 您访问的文件并存在!');
|
||||
return;
|
||||
}
|
||||
|
||||
$o = new $data['c'](); // 新建对象
|
||||
|
||||
if (!method_exists($o, $data['a'])) {
|
||||
|
||||
common::showMsg($client_id, $chat_name . ' 您访问的方法并存在!');
|
||||
return;
|
||||
}
|
||||
|
||||
$data['data'] = isset($data['data']) ? $data['data'] : '';
|
||||
|
||||
call_user_func_array([$o, $data['a']], [$client_id, $data['data']]); //调用对象$o($c)里的方法$a
|
||||
}
|
||||
|
||||
/**
|
||||
* 当用户断开连接时触发
|
||||
* @param int $client_id 连接id
|
||||
*/
|
||||
public static function onClose($client_id)
|
||||
{
|
||||
if (isset($_SESSION['auth_timer_id'])) {
|
||||
Timer::del($_SESSION['auth_timer_id']);
|
||||
}
|
||||
|
||||
// 向所有人发送
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
|
||||
// 此user_id下还有其他链接
|
||||
try {
|
||||
if (Gateway::getClientIdByUid($_SESSION['user_id'])) {
|
||||
return;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
|
||||
}
|
||||
|
||||
$user_info = Common::userInfo($_SESSION['user_id']);
|
||||
|
||||
if ($user_info['source'] == 'user') {
|
||||
|
||||
$csr_id = Db::name('kefu_session')->where('user_id', $user_info['id'])->value('csr_id');
|
||||
|
||||
if ($csr_id) {
|
||||
|
||||
$reception_count = Db::name('kefu_csr_config')
|
||||
->where('admin_id', $csr_id)
|
||||
->value('reception_count');
|
||||
|
||||
if ($reception_count > 0) {
|
||||
Db::name('kefu_csr_config')->where('admin_id', $csr_id)->setDec('reception_count');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} elseif ($user_info['source'] == 'csr' && $user_info['status'] == 3) {
|
||||
// 客服保持在线
|
||||
$keep_alive = Db::name('kefu_csr_config')->where('admin_id', $user_info['id'])->value('keep_alive');
|
||||
if ($keep_alive) {
|
||||
return;
|
||||
}
|
||||
|
||||
Common::csrStatus(0);
|
||||
|
||||
// 客服下线
|
||||
Db::name('kefu_csr_config')->where('admin_id', $user_info['id'])->update([
|
||||
'reception_count' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
Gateway::sendToAll(json_encode([
|
||||
'msgtype' => 'offline',
|
||||
'user_id' => $_SESSION['user_id'],
|
||||
]));
|
||||
|
||||
}
|
||||
|
||||
Db::clear();
|
||||
}
|
||||
|
||||
}
|
||||
+1014
File diff suppressed because it is too large
Load Diff
+37
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
use GatewayWorker\BusinessWorker;
|
||||
use Workerman\Worker;
|
||||
|
||||
// 自动加载类
|
||||
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||
// 获取插件配置
|
||||
$kefu_config = get_addon_config('kefu');
|
||||
// bussinessWorker 进程
|
||||
$worker = new BusinessWorker();
|
||||
// worker名称
|
||||
$worker->name = 'KeFuBusinessWorker';
|
||||
// bussinessWorker进程数量
|
||||
$worker->count = $kefu_config['worker_process_number'];
|
||||
// 服务注册地址
|
||||
$worker->registerAddress = '127.0.0.1:' . $kefu_config['register_port'];
|
||||
//设置处理业务的类,此处制定Events的命名空间
|
||||
$worker->eventHandler = 'addons\kefu\library\GatewayWorker\Applications\KeFu\Events';
|
||||
|
||||
// 如果不是在根目录启动,则运行runAll方法
|
||||
if (!defined('GLOBAL_START')) {
|
||||
Worker::runAll();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
use GatewayWorker\Gateway;
|
||||
use Workerman\Worker;
|
||||
|
||||
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||
|
||||
// gateway 进程
|
||||
$kefu_config = get_addon_config('kefu');
|
||||
|
||||
$context = [];
|
||||
$ssl_start = false;
|
||||
if ($kefu_config['wss_switch'] && $kefu_config['ssl_cert'] && $kefu_config['ssl_cert_key']) {
|
||||
$context ['ssl'] = [
|
||||
// 使用绝对路径
|
||||
'local_cert' => $kefu_config['ssl_cert'], // 也可以是crt文件
|
||||
'local_pk' => $kefu_config['ssl_cert_key'],
|
||||
'verify_peer' => false,
|
||||
//'allow_self_signed' => true, //如果是自签名证书开启此选项
|
||||
];
|
||||
|
||||
$ssl_start = true;
|
||||
}
|
||||
|
||||
$gateway = new Gateway("websocket://0.0.0.0:" . $kefu_config['websocket_port'], $context);
|
||||
|
||||
if ($ssl_start) {
|
||||
// 开始SSL
|
||||
$gateway->transport = 'ssl';
|
||||
}
|
||||
|
||||
// gateway名称,status方便查看
|
||||
$gateway->name = 'KeFuGateway' . ($ssl_start ? '-wss' : '');
|
||||
|
||||
// gateway进程数
|
||||
$gateway->count = $kefu_config['gateway_process_number'];
|
||||
|
||||
// 本机ip,分布式部署时使用内网ip
|
||||
$gateway->lanIp = '127.0.0.1';
|
||||
|
||||
// 内部通讯起始端口,假如$gateway->count=4,起始端口为4000
|
||||
// 则一般会使用4000 4001 4002 4003 4个端口作为内部通讯端口
|
||||
$gateway->startPort = $kefu_config['internal_start_port'];
|
||||
|
||||
// 服务注册地址
|
||||
$gateway->registerAddress = '127.0.0.1:' . $kefu_config['register_port'];
|
||||
|
||||
// 心跳间隔
|
||||
$gateway->pingInterval = 30;
|
||||
|
||||
$gateway->pingNotResponseLimit = 1;
|
||||
|
||||
// 心跳数据
|
||||
$gateway->pingData = '';
|
||||
|
||||
// 如果不是在根目录启动,则运行runAll方法
|
||||
if (!defined('GLOBAL_START')) {
|
||||
Worker::runAll();
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of workerman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
use GatewayWorker\Register;
|
||||
use Workerman\Worker;
|
||||
|
||||
// 自动加载类
|
||||
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||
|
||||
// 获取插件配置
|
||||
$kefu_config = get_addon_config('kefu');
|
||||
// register 必须是text协议
|
||||
$register = new Register('text://0.0.0.0:' . $kefu_config['register_port']);
|
||||
|
||||
// 如果不是在根目录启动,则运行runAll方法
|
||||
if (!defined('GLOBAL_START')) {
|
||||
Worker::runAll();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
/*内部通信服务*/
|
||||
|
||||
use GatewayWorker\Gateway;
|
||||
use Workerman\Autoloader;
|
||||
use Workerman\Worker;
|
||||
|
||||
// 自动加载类
|
||||
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||
Autoloader::setRootPath(__DIR__);
|
||||
|
||||
$kefu_config = get_addon_config('kefu');
|
||||
|
||||
$internal_gateway = new Gateway("Text://127.0.0.1:" . ($kefu_config['register_port'] + 100));
|
||||
$internal_gateway->name = 'KeFuGateway';
|
||||
$internal_gateway->startPort = $kefu_config['internal_start_port'] + 1000;
|
||||
$internal_gateway->registerAddress = '127.0.0.1:' . $kefu_config['register_port'];// 端口为start_register.php中监听的端口
|
||||
|
||||
// 如果不是在根目录启动,则运行runAll方法
|
||||
if (!defined('GLOBAL_START')) {
|
||||
Worker::runAll();
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2009-2015 walkor<walkor@workerman.net> and contributors (see https://github.com/walkor/workerman/contributors)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
GatewayWorker windows 版本
|
||||
=================
|
||||
|
||||
GatewayWorker基于[Workerman](https://github.com/walkor/Workerman)开发的一个项目框架,用于快速开发长连接应用,例如app推送服务端、即时IM服务端、游戏服务端、物联网、智能家居等等。
|
||||
|
||||
GatewayWorker使用经典的Gateway和Worker进程模型。Gateway进程负责维持客户端连接,并转发客户端的数据给Worker进程处理;Worker进程负责处理实际的业务逻辑,并将结果推送给对应的客户端。Gateway服务和Worker服务可以分开部署在不同的服务器上,实现分布式集群。
|
||||
|
||||
GatewayWorker提供非常方便的API,可以全局广播数据、可以向某个群体广播数据、也可以向某个特定客户端推送数据。配合Workerman的定时器,也可以定时推送数据。
|
||||
|
||||
GatewayWorker Linux 版本
|
||||
======================
|
||||
Linux 版本GatewayWorker 在这里 https://github.com/walkor/GatewayWorker
|
||||
|
||||
启动
|
||||
=======
|
||||
双击start_for_win.bat
|
||||
|
||||
Applications\YourApp测试方法
|
||||
======
|
||||
使用telnet命令测试(不要使用windows自带的telnet)
|
||||
```shell
|
||||
telnet 127.0.0.1 8282
|
||||
Trying 127.0.0.1...
|
||||
Connected to 127.0.0.1.
|
||||
Escape character is '^]'.
|
||||
Hello 3
|
||||
3 login
|
||||
haha
|
||||
3 said haha
|
||||
```
|
||||
|
||||
手册
|
||||
=======
|
||||
http://www.workerman.net/gatewaydoc/
|
||||
|
||||
使用GatewayWorker-for-win开发的项目
|
||||
=======
|
||||
## [tadpole](http://kedou.workerman.net/)
|
||||
[Live demo](http://kedou.workerman.net/)
|
||||
[Source code](https://github.com/walkor/workerman)
|
||||

|
||||
|
||||
## [chat room](http://chat.workerman.net/)
|
||||
[Live demo](http://chat.workerman.net/)
|
||||
[Source code](https://github.com/walkor/workerman-chat)
|
||||

|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name" : "workerman/gateway-worker-demo",
|
||||
"keywords": ["distributed","communication"],
|
||||
"homepage": "http://www.workerman.net",
|
||||
"license" : "MIT",
|
||||
"require": {
|
||||
"workerman/gateway-worker" : ">=3.0.0"
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "ab9c3e87dac1a4a30c63a47de76a8217",
|
||||
"packages": [
|
||||
{
|
||||
"name": "workerman/gateway-worker",
|
||||
"version": "v3.0.18",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/walkor/GatewayWorker.git",
|
||||
"reference": "50d3a77deb7f7fb206d641ee0307ae1c41d5d41d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/walkor/GatewayWorker/zipball/50d3a77deb7f7fb206d641ee0307ae1c41d5d41d",
|
||||
"reference": "50d3a77deb7f7fb206d641ee0307ae1c41d5d41d",
|
||||
"shasum": "",
|
||||
"mirrors": [
|
||||
{
|
||||
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
|
||||
"preferred": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"workerman/workerman": ">=3.5.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"GatewayWorker\\": "./src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"homepage": "http://www.workerman.net",
|
||||
"keywords": [
|
||||
"communication",
|
||||
"distributed"
|
||||
],
|
||||
"time": "2020-07-15T06:45:01+00:00"
|
||||
},
|
||||
{
|
||||
"name": "workerman/workerman",
|
||||
"version": "v4.0.10",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/walkor/Workerman.git",
|
||||
"reference": "132a277b1836464c8fb99e9146ca161de8d7199f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/walkor/Workerman/zipball/132a277b1836464c8fb99e9146ca161de8d7199f",
|
||||
"reference": "132a277b1836464c8fb99e9146ca161de8d7199f",
|
||||
"shasum": "",
|
||||
"mirrors": [
|
||||
{
|
||||
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
|
||||
"preferred": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-event": "For better performance. "
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Workerman\\": "./"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "walkor",
|
||||
"email": "walkor@workerman.net",
|
||||
"homepage": "http://www.workerman.net",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "An asynchronous event driven PHP framework for easily building fast, scalable network applications.",
|
||||
"homepage": "http://www.workerman.net",
|
||||
"keywords": [
|
||||
"asynchronous",
|
||||
"event-loop"
|
||||
],
|
||||
"time": "2020-09-15T09:22:45+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": [],
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "1.1.0"
|
||||
}
|
||||
Executable
+76
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
/**
|
||||
* run with command
|
||||
* php start.php start
|
||||
*/
|
||||
|
||||
namespace addons\kefu\library\gatewayworker;
|
||||
|
||||
ini_set('display_errors', 'on');
|
||||
|
||||
use think\Config;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Argument;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
use think\Db;
|
||||
use think\Exception;
|
||||
use think\exception\PDOException;
|
||||
use Workerman\Worker;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class start extends Command
|
||||
{
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('kefu')
|
||||
->addArgument('action', Argument::OPTIONAL, "action start [d]|stop|restart|status")
|
||||
->addArgument('type', Argument::OPTIONAL, "d -d")
|
||||
->setDescription('KeFu 会话服务');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
global $argv;
|
||||
$action = trim($input->getArgument('action'));
|
||||
$type = trim($input->getArgument('type')) ? '-d' : '';
|
||||
|
||||
$argv[0] = 'chat';
|
||||
$argv[1] = $action;
|
||||
$argv[2] = $type ? '-d' : '';
|
||||
$this->start();
|
||||
}
|
||||
|
||||
private function start()
|
||||
{
|
||||
if (strpos(strtolower(PHP_OS), 'win') === 0) {
|
||||
exit("Windows下不支持窗口启动,请手动运行(not support windows, please use):public/kefu_start_for_win.bat\n");
|
||||
}
|
||||
|
||||
// 检查扩展
|
||||
if (!extension_loaded('pcntl')) {
|
||||
exit("Please install pcntl extension. See http://doc.workerman.net/appendices/install-extension.html\n");
|
||||
}
|
||||
|
||||
if (!extension_loaded('posix')) {
|
||||
exit("Please install posix extension. See http://doc.workerman.net/appendices/install-extension.html\n");
|
||||
}
|
||||
|
||||
// 标记是全局启动
|
||||
define('GLOBAL_START', 1);
|
||||
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
// 加载所有Applications/*/start.php,以便启动所有服务
|
||||
foreach (glob(__DIR__ . '/Applications/*/start*.php') as $start_file) {
|
||||
require_once $start_file;
|
||||
}
|
||||
|
||||
// 运行所有服务
|
||||
Worker::runAll();
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
php Applications\FsatChat\start_register.php Applications\FsatChat\start_gateway.php Applications\FsatChat\start_businessworker.php Applications\FsatChat\start_text_gateway.php
|
||||
pause
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace addons\kefu\library;
|
||||
|
||||
class StrComparison
|
||||
{
|
||||
protected $str1;
|
||||
protected $str2;
|
||||
protected $c = [];
|
||||
|
||||
/*
|
||||
* 返回串一和串二的最长公共子序列
|
||||
*/
|
||||
public function getLCS($str1, $str2, $len1 = 0, $len2 = 0)
|
||||
{
|
||||
$this->str1 = $str1;
|
||||
$this->str2 = $str2;
|
||||
if ($len1 == 0) {
|
||||
$len1 = strlen($str1);
|
||||
}
|
||||
if ($len2 == 0) {
|
||||
$len2 = strlen($str2);
|
||||
}
|
||||
$this->initC($len1, $len2);
|
||||
return $this->printLCS($this->c, $len1 - 1, $len2 - 1);
|
||||
}
|
||||
|
||||
/*
|
||||
* 返回两个串的相似度
|
||||
*/
|
||||
public function getSimilar($str1, $str2)
|
||||
{
|
||||
$len1 = strlen($str1);
|
||||
$len2 = strlen($str2);
|
||||
$len = strlen($this->getLCS($str1, $str2, $len1, $len2));
|
||||
$similar = $len * 2 / ($len1 + $len2);
|
||||
return round($similar * 100, 2);
|
||||
}
|
||||
|
||||
public function initC($len1, $len2)
|
||||
{
|
||||
for ($i = 0; $i < $len1; $i++) {
|
||||
$this->c[$i][0] = 0;
|
||||
}
|
||||
for ($j = 0; $j < $len2; $j++) {
|
||||
$this->c[0][$j] = 0;
|
||||
}
|
||||
for ($i = 1; $i < $len1; $i++) {
|
||||
for ($j = 1; $j < $len2; $j++) {
|
||||
if ($this->str1[$i] == $this->str2[$j]) {
|
||||
$this->c[$i][$j] = $this->c[$i - 1][$j - 1] + 1;
|
||||
} elseif ($this->c[$i - 1][$j] >= $this->c[$i][$j - 1]) {
|
||||
$this->c[$i][$j] = $this->c[$i - 1][$j];
|
||||
} else {
|
||||
$this->c[$i][$j] = $this->c[$i][$j - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function printLCS($c, $i, $j)
|
||||
{
|
||||
if ($i == 0 || $j == 0) {
|
||||
if ($this->str1[$i] == $this->str2[$j]) {
|
||||
return $this->str2[$j];
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
if ($this->str1[$i] == $this->str2[$j]) {
|
||||
return $this->printLCS($this->c, $i - 1, $j - 1) . $this->str2[$j];
|
||||
} elseif ($this->c[$i - 1][$j] >= $this->c[$i][$j - 1]) {
|
||||
return $this->printLCS($this->c, $i - 1, $j);
|
||||
} else {
|
||||
return $this->printLCS($this->c, $i, $j - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace addons\kefu\library\WechatCrypto;
|
||||
|
||||
/**
|
||||
* error code 说明.
|
||||
* <ul>
|
||||
* <li>-40001: 签名验证错误</li>
|
||||
* <li>-40002: xml解析失败</li>
|
||||
* <li>-40003: sha加密生成签名失败</li>
|
||||
* <li>-40004: encodingAesKey 非法</li>
|
||||
* <li>-40005: appid 校验错误</li>
|
||||
* <li>-40006: aes 加密失败</li>
|
||||
* <li>-40007: aes 解密失败</li>
|
||||
* <li>-40008: 解密后得到的buffer非法</li>
|
||||
* <li>-40009: base64加密失败</li>
|
||||
* <li>-40010: base64解密失败</li>
|
||||
* <li>-40011: 生成xml失败</li>
|
||||
* </ul>
|
||||
*/
|
||||
class ErrorCode
|
||||
{
|
||||
public static $OK = 0;
|
||||
public static $ValidateSignatureError = -40001;
|
||||
public static $ParseXmlError = -40002;
|
||||
public static $ComputeSignatureError = -40003;
|
||||
public static $IllegalAesKey = -40004;
|
||||
public static $ValidateAppidError = -40005;
|
||||
public static $EncryptAESError = -40006;
|
||||
public static $DecryptAESError = -40007;
|
||||
public static $IllegalBuffer = -40008;
|
||||
public static $EncodeBase64Error = -40009;
|
||||
public static $DecodeBase64Error = -40010;
|
||||
public static $GenReturnXmlError = -40011;
|
||||
}
|
||||
|
||||
?>
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace addons\kefu\library\WechatCrypto;
|
||||
|
||||
use addons\kefu\library\WechatCrypto\ErrorCode;
|
||||
|
||||
/**
|
||||
* PKCS7Encoder class
|
||||
*
|
||||
* 提供基于PKCS7算法的加解密接口.
|
||||
*/
|
||||
class PKCS7Encoder
|
||||
{
|
||||
public static $block_size = 32;
|
||||
|
||||
/**
|
||||
* 对需要加密的明文进行填充补位
|
||||
* @param $text 需要进行填充补位操作的明文
|
||||
* @return 补齐明文字符串
|
||||
*/
|
||||
function encode($text)
|
||||
{
|
||||
$block_size = PKCS7Encoder::$block_size;
|
||||
$text_length = strlen($text);
|
||||
//计算需要填充的位数
|
||||
$amount_to_pad = PKCS7Encoder::$block_size - ($text_length % PKCS7Encoder::$block_size);
|
||||
if ($amount_to_pad == 0) {
|
||||
$amount_to_pad = PKCS7Encoder::block_size;
|
||||
}
|
||||
//获得补位所用的字符
|
||||
$pad_chr = chr($amount_to_pad);
|
||||
$tmp = "";
|
||||
for ($index = 0; $index < $amount_to_pad; $index++) {
|
||||
$tmp .= $pad_chr;
|
||||
}
|
||||
return $text . $tmp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对解密后的明文进行补位删除
|
||||
* @param decrypted 解密后的明文
|
||||
* @return 删除填充补位后的明文
|
||||
*/
|
||||
function decode($text)
|
||||
{
|
||||
|
||||
$pad = ord(substr($text, -1));
|
||||
if ($pad < 1 || $pad > 32) {
|
||||
$pad = 0;
|
||||
}
|
||||
return substr($text, 0, (strlen($text) - $pad));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace addons\kefu\library\WechatCrypto;
|
||||
|
||||
use addons\kefu\library\WechatCrypto\ErrorCode;
|
||||
use addons\kefu\library\WechatCrypto\PKCS7Encoder;
|
||||
|
||||
|
||||
/**
|
||||
* Prpcrypt class
|
||||
*
|
||||
* 提供接收和推送给公众平台消息的加解密接口.
|
||||
*/
|
||||
class Prpcrypt
|
||||
{
|
||||
public $key;
|
||||
|
||||
function __construct($k)
|
||||
{
|
||||
$this->key = base64_decode($k . "=");
|
||||
}
|
||||
|
||||
/**
|
||||
* 对明文进行加密
|
||||
* @param string $text 需要加密的明文
|
||||
* @return string 加密后的密文
|
||||
*/
|
||||
public function encrypt($text, $appid)
|
||||
{
|
||||
|
||||
try {
|
||||
//获得16位随机字符串,填充到明文之前
|
||||
$random = $this->getRandomStr();
|
||||
$text = $random . pack("N", strlen($text)) . $text . $appid;
|
||||
$iv = substr($this->key, 0, 16);
|
||||
$pkc_encoder = new PKCS7Encoder;
|
||||
$text = $pkc_encoder->encode($text);
|
||||
$encrypted = openssl_encrypt($text, 'AES-256-CBC', $this->key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv);
|
||||
|
||||
//print(base64_encode($encrypted));
|
||||
//使用BASE64对加密后的字符串进行编码
|
||||
return [ErrorCode::$OK, base64_encode($encrypted)];
|
||||
} catch (Exception $e) {
|
||||
//print $e;
|
||||
return [ErrorCode::$EncryptAESError, null];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机生成16位字符串
|
||||
* @return string 生成的字符串
|
||||
*/
|
||||
function getRandomStr()
|
||||
{
|
||||
|
||||
$str = "";
|
||||
$str_pol = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";
|
||||
$max = strlen($str_pol) - 1;
|
||||
for ($i = 0; $i < 16; $i++) {
|
||||
$str .= $str_pol[mt_rand(0, $max)];
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对密文进行解密
|
||||
* @param string $encrypted 需要解密的密文
|
||||
* @return string 解密得到的明文
|
||||
*/
|
||||
public function decrypt($encrypted, $appid)
|
||||
{
|
||||
|
||||
try {
|
||||
//使用BASE64对需要解密的字符串进行解码
|
||||
$ciphertext_dec = base64_decode($encrypted);
|
||||
$iv = substr($this->key, 0, 16);
|
||||
$decrypted = openssl_decrypt($ciphertext_dec, 'AES-256-CBC', $this->key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv);
|
||||
|
||||
} catch (Exception $e) {
|
||||
return [ErrorCode::$DecryptAESError, null];
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
//去除补位字符
|
||||
$pkc_encoder = new PKCS7Encoder;
|
||||
$result = $pkc_encoder->decode($decrypted);
|
||||
//去除16位随机字符串,网络字节序和AppId
|
||||
if (strlen($result) < 16) {
|
||||
return "";
|
||||
}
|
||||
$content = substr($result, 16, strlen($result));
|
||||
$len_list = unpack("N", substr($content, 0, 4));
|
||||
$xml_len = $len_list[1];
|
||||
$xml_content = substr($content, 4, $xml_len);
|
||||
$from_appid = substr($content, $xml_len + 4);
|
||||
} catch (Exception $e) {
|
||||
//print $e;
|
||||
return [ErrorCode::$IllegalBuffer, null];
|
||||
}
|
||||
if ($from_appid != $appid) {
|
||||
return [ErrorCode::$ValidateAppidError, null];
|
||||
}
|
||||
return [0, $xml_content];
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace addons\kefu\library\WechatCrypto;
|
||||
|
||||
use addons\kefu\library\WechatCrypto\ErrorCode;
|
||||
|
||||
/**
|
||||
* SHA1 class
|
||||
*
|
||||
* 计算公众平台的消息签名接口.
|
||||
*/
|
||||
class SHA1
|
||||
{
|
||||
/**
|
||||
* 用SHA1算法生成安全签名
|
||||
* @param string $token 票据
|
||||
* @param string $timestamp 时间戳
|
||||
* @param string $nonce 随机字符串
|
||||
* @param string $encrypt 密文消息
|
||||
*/
|
||||
public function getSHA1($token, $timestamp, $nonce, $encrypt_msg)
|
||||
{
|
||||
//排序
|
||||
try {
|
||||
$array = [$encrypt_msg, $token, $timestamp, $nonce];
|
||||
sort($array, SORT_STRING);
|
||||
$str = implode($array);
|
||||
return [ErrorCode::$OK, sha1($str)];
|
||||
} catch (Exception $e) {
|
||||
//print $e . "\n";
|
||||
return [ErrorCode::$ComputeSignatureError, null];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
?>
|
||||
+323
@@ -0,0 +1,323 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 对公众平台发送给公众账号的消息加解密代码.
|
||||
* 白衣素袖:增加消息处理辅助方法
|
||||
* @copyright Copyright (c) 1998-2014 Tencent Inc.
|
||||
*/
|
||||
|
||||
namespace addons\kefu\library\WechatCrypto;
|
||||
|
||||
use think\Db;
|
||||
use EasyWeChat\Factory;
|
||||
use GatewayWorker\Lib\Gateway;
|
||||
use addons\kefu\library\Common;
|
||||
use addons\kefu\library\WechatCrypto\SHA1;
|
||||
use addons\kefu\library\WechatCrypto\ErrorCode;
|
||||
use addons\kefu\library\WechatCrypto\XMLParse;
|
||||
use addons\kefu\library\WechatCrypto\Prpcrypt;
|
||||
|
||||
/**
|
||||
* 1.第三方回复加密消息给公众平台;
|
||||
* 2.第三方收到公众平台发送的消息,验证消息的安全性,并对消息进行解密。
|
||||
*/
|
||||
class WXBizMsgCrypt
|
||||
{
|
||||
// 微信配置
|
||||
private $wechat = [];
|
||||
|
||||
// EasyWeChat APP
|
||||
public $wechatapp = null;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
$wechat_temp = Db::name('kefu_config')
|
||||
->whereIn('name', 'wechat_app_id,wechat_app_secret,wechat_token,wechat_encodingkey')
|
||||
->select();
|
||||
|
||||
foreach ($wechat_temp as $key => $value) {
|
||||
$this->wechat[$value['name']] = $value['value'];
|
||||
}
|
||||
|
||||
// 初始化EasyWeChat
|
||||
$config = [
|
||||
'app_id' => $this->wechat['wechat_app_id'],
|
||||
'secret' => $this->wechat['wechat_app_secret'],
|
||||
'token' => $this->wechat['wechat_token'],
|
||||
'aes_key' => $this->wechat['wechat_encodingkey'],
|
||||
/*'log' => [
|
||||
'level' => 'debug',
|
||||
'file' => RUNTIME_PATH . 'log/kefu_wechat.log',
|
||||
],*/
|
||||
];
|
||||
$this->wechatapp = Factory::miniProgram($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将公众平台回复用户的消息加密打包.
|
||||
* <ol>
|
||||
* <li>对要发送的消息进行AES-CBC加密</li>
|
||||
* <li>生成安全签名</li>
|
||||
* <li>将消息密文和安全签名打包成xml格式</li>
|
||||
* </ol>
|
||||
*
|
||||
* @param $replyMsg string 公众平台待回复用户的消息,xml格式的字符串
|
||||
* @param $timeStamp string 时间戳,可以自己生成,也可以用URL参数的timestamp
|
||||
* @param $nonce string 随机串,可以自己生成,也可以用URL参数的nonce
|
||||
* @param &$encryptMsg string 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串,
|
||||
* 当return返回0时有效
|
||||
*
|
||||
* @return int 成功0,失败返回对应的错误码
|
||||
*/
|
||||
public function encryptMsg($replyMsg, $timeStamp, $nonce, &$encryptMsg)
|
||||
{
|
||||
$pc = new Prpcrypt($this->wechat['wechat_encodingkey']);
|
||||
|
||||
//加密
|
||||
$array = $pc->encrypt($replyMsg, $this->wechat['wechat_app_id']);
|
||||
$ret = $array[0];
|
||||
if ($ret != 0) {
|
||||
return $ret;
|
||||
}
|
||||
|
||||
if ($timeStamp == null) {
|
||||
$timeStamp = time();
|
||||
}
|
||||
$encrypt = $array[1];
|
||||
|
||||
//生成安全签名
|
||||
$sha1 = new SHA1;
|
||||
$array = $sha1->getSHA1($this->wechat['wechat_token'], $timeStamp, $nonce, $encrypt);
|
||||
$ret = $array[0];
|
||||
if ($ret != 0) {
|
||||
return $ret;
|
||||
}
|
||||
$signature = $array[1];
|
||||
|
||||
//生成发送的xml
|
||||
$xmlparse = new XMLParse;
|
||||
$encryptMsg = $xmlparse->generate($encrypt, $signature, $timeStamp, $nonce);
|
||||
return ErrorCode::$OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检验消息的真实性,并且获取解密后的明文.
|
||||
* <ol>
|
||||
* <li>利用收到的密文生成安全签名,进行签名验证</li>
|
||||
* <li>若验证通过,则提取xml中的加密消息</li>
|
||||
* <li>对消息进行解密</li>
|
||||
* </ol>
|
||||
*
|
||||
* @param $msgSignature string 签名串,对应URL参数的msg_signature
|
||||
* @param $timestamp string 时间戳 对应URL参数的timestamp
|
||||
* @param $nonce string 随机串,对应URL参数的nonce
|
||||
* @param $postData string 密文,对应POST请求的数据
|
||||
* @param &$msg string 解密后的原文,当return返回0时有效
|
||||
*
|
||||
* @return int 成功0,失败返回对应的错误码
|
||||
*/
|
||||
public function decryptMsg($msgSignature, $timestamp = null, $nonce, $encrypt, &$msg)
|
||||
{
|
||||
if (strlen($this->wechat['wechat_encodingkey']) != 43) {
|
||||
return ErrorCode::$IllegalAesKey;
|
||||
}
|
||||
|
||||
$pc = new Prpcrypt($this->wechat['wechat_encodingkey']);
|
||||
|
||||
if ($timestamp == null) {
|
||||
$timestamp = time();
|
||||
}
|
||||
|
||||
//验证安全签名
|
||||
$sha1 = new SHA1;
|
||||
$array = $sha1->getSHA1($this->wechat['wechat_token'], $timestamp, $nonce, $encrypt);
|
||||
$ret = $array[0];
|
||||
|
||||
if ($ret != 0) {
|
||||
return $ret;
|
||||
}
|
||||
|
||||
$signature = $array[1];
|
||||
if ($signature != $msgSignature) {
|
||||
return ErrorCode::$ValidateSignatureError;
|
||||
}
|
||||
|
||||
$result = $pc->decrypt($encrypt, $this->wechat['wechat_app_id']);
|
||||
if ($result[0] != 0) {
|
||||
return $result[0];
|
||||
}
|
||||
$msg = $result[1];
|
||||
|
||||
return ErrorCode::$OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存用户发送的图片至服务器
|
||||
* @param [type] $media_id 临时素材ID
|
||||
* @param string $save_dir 保存目录
|
||||
* @return string 文件完整路径
|
||||
*/
|
||||
public function saveImg($media_id, $save_dir = './uploads/')
|
||||
{
|
||||
$stream = $this->wechatapp->media->get($media_id);
|
||||
if ($stream instanceof \EasyWeChat\Kernel\Http\StreamResponse) {
|
||||
|
||||
$save_dir = $save_dir . date('Ymd') . '/';
|
||||
|
||||
if (!is_dir($save_dir)) {
|
||||
if (!mkdir($save_dir, 0777, true)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$filename = $stream->save($save_dir);
|
||||
|
||||
return $save_dir . $filename;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息至小程序用户
|
||||
* @param [type] $message 文本或消息对象
|
||||
* @param [type] $openid [description]
|
||||
* @param [type] $sender 带标识的发送人
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public function sendMessage($message, $openid, $sender = false)
|
||||
{
|
||||
$res = $this->wechatapp->customer_service->message($message)->to($openid)->send();
|
||||
|
||||
switch ($res['errcode']) {
|
||||
case '-1':
|
||||
$msg = '发送失败,系统繁忙,请重试!';
|
||||
break;
|
||||
case '40001':
|
||||
$msg = '发送失败,AppSecret错误!';
|
||||
break;
|
||||
case '40002':
|
||||
$msg = '发送失败,凭证不合法!';
|
||||
break;
|
||||
case '40003':
|
||||
$msg = '发送失败,openid不合法!';
|
||||
break;
|
||||
case '45015':
|
||||
$msg = '发送失败,回复时间超限制!';
|
||||
break;
|
||||
case '45047':
|
||||
$msg = '发送条数超限,用户将不会收到此消息!';
|
||||
break;
|
||||
case '48001':
|
||||
$msg = '请确保小程序已获取发送客服消息功能的API权限!';
|
||||
break;
|
||||
|
||||
default:
|
||||
$msg = '';
|
||||
break;
|
||||
}
|
||||
|
||||
// 只在workerman环境(客服发送消息时),进行提示
|
||||
if (class_exists('\GatewayWorker\Lib\Gateway') && $msg && $sender) {
|
||||
Gateway::sendToUid($sender, json_encode([
|
||||
'code' => 0,
|
||||
'msgtype' => 'show_msg',
|
||||
'msg' => $msg,
|
||||
]));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立用户->分配客服
|
||||
* @param [type] $open_id [description]
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public function userInitialize($open_id)
|
||||
{
|
||||
$kefu_user = Db::name('kefu_user')->where('wechat_openid', $open_id)->find();
|
||||
|
||||
if (!$kefu_user) {
|
||||
|
||||
// 建立用户
|
||||
$tourists_max_id = Db::name('kefu_user')->max('id');
|
||||
|
||||
$kefu_user = [
|
||||
'avatar' => '', // 随机头像->算了算了
|
||||
'nickname' => '小程序用户 ' . $tourists_max_id,
|
||||
'wechat_openid' => $open_id,
|
||||
'createtime' => time(),
|
||||
];
|
||||
|
||||
if (Db::name('kefu_user')->insert($kefu_user)) {
|
||||
$kefu_user['id'] = Db::name('kefu_user')->getLastInsID();
|
||||
}
|
||||
}
|
||||
|
||||
// 查询之前的客服代表
|
||||
$session = Db::name('kefu_session')
|
||||
->alias('s')
|
||||
->field('s.*,a.id as admin_id,a.nickname')
|
||||
->join('admin a', 's.csr_id=a.id')
|
||||
->where('s.user_id', $kefu_user['id'])
|
||||
->where('s.deletetime', null)
|
||||
->find();
|
||||
|
||||
// 有客服代表,但客服代表不在线,重新分配
|
||||
$is_csr_distribution = false;
|
||||
if ($session) {
|
||||
$csr_status = Db::name('kefu_csr_config')->where('admin_id', $session['admin_id'])->value('status');
|
||||
|
||||
if ($csr_status != 3) {
|
||||
$is_csr_distribution = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$session || $is_csr_distribution) {
|
||||
|
||||
// 客服分配
|
||||
$csr = Common::getAppropriateCsr();
|
||||
if ($csr) {
|
||||
$session = Common::distributionCsr($csr, $kefu_user['id'] . '||user');
|
||||
} else {
|
||||
$data = [
|
||||
'session' => false,
|
||||
'kefu_user' => $kefu_user,
|
||||
'code' => 1,
|
||||
'msg' => '非常抱歉,当前无在线客服,您可以直接在此留言,谢谢您的支持!',
|
||||
];
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
if ($session) {
|
||||
|
||||
// 记录客服接待人数
|
||||
Db::name('kefu_csr_config')->where('admin_id', $session['admin_id'])->inc('reception_count')->update([
|
||||
'last_reception_time' => time(),
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'session' => $session,
|
||||
'kefu_user' => $kefu_user,
|
||||
'code' => 2,
|
||||
];
|
||||
return $data;
|
||||
} else {
|
||||
|
||||
$data = [
|
||||
'session' => false,
|
||||
'code' => 0,
|
||||
'msg' => '分配客服代表失败!',
|
||||
];
|
||||
return $data;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace addons\kefu\library\WechatCrypto;
|
||||
|
||||
use addons\kefu\library\WechatCrypto\ErrorCode;
|
||||
|
||||
/**
|
||||
* XMLParse class
|
||||
*
|
||||
* 提供提取消息格式中的密文及生成回复消息格式的接口.
|
||||
*/
|
||||
class XMLParse
|
||||
{
|
||||
|
||||
/**
|
||||
* 提取出xml数据包中的加密消息
|
||||
* @param string $xmltext 待提取的xml字符串
|
||||
* @return string 提取出的加密消息字符串
|
||||
*/
|
||||
public function extract($xmltext)
|
||||
{
|
||||
libxml_disable_entity_loader(true);
|
||||
try {
|
||||
$xml = new DOMDocument();
|
||||
$xml->loadXML($xmltext);
|
||||
$array_e = $xml->getElementsByTagName('Encrypt');
|
||||
$array_a = $xml->getElementsByTagName('ToUserName');
|
||||
$encrypt = $array_e->item(0)->nodeValue;
|
||||
$tousername = $array_a->item(0)->nodeValue;
|
||||
return [0, $encrypt, $tousername];
|
||||
} catch (Exception $e) {
|
||||
//print $e . "\n";
|
||||
return [ErrorCode::$ParseXmlError, null, null];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成xml消息
|
||||
* @param string $encrypt 加密后的消息密文
|
||||
* @param string $signature 安全签名
|
||||
* @param string $timestamp 时间戳
|
||||
* @param string $nonce 随机字符串
|
||||
*/
|
||||
public function generate($encrypt, $signature, $timestamp, $nonce)
|
||||
{
|
||||
$format = "<xml>
|
||||
<Encrypt><![CDATA[%s]]></Encrypt>
|
||||
<MsgSignature><![CDATA[%s]]></MsgSignature>
|
||||
<TimeStamp>%s</TimeStamp>
|
||||
<Nonce><![CDATA[%s]]></Nonce>
|
||||
</xml>";
|
||||
return sprintf($format, $encrypt, $signature, $timestamp, $nonce);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
?>
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{$chat_name}</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
|
||||
<!-- 在线客服依赖以下css和js文件,请按需引入 -->
|
||||
<link href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="__CDN__/assets/addons/kefu/css/kefu_default.css" rel="stylesheet">
|
||||
|
||||
<script src=https://cdn.staticfile.org/jquery/2.1.4/jquery.min.js></script>
|
||||
<script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
|
||||
<script src="https://cdn.staticfile.org/layer/2.3/layer.js"></script>
|
||||
<script src="__CDN__/assets/addons/kefu/js/kefu.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="text/javascript">
|
||||
KeFu.initialize(document.domain, 'index', function () {
|
||||
KeFu.toggle_window('show');// 打开窗口
|
||||
});
|
||||
// 参数一为在线客服所在网站的域名(启动Workerman服务的网站的域名)
|
||||
// 参数二为模块名,站外直接填写index
|
||||
// 参数三为初始化完成后的回调方法
|
||||
// 参数四为指定客服,可在此处填写客服代表的后台账户id
|
||||
// 若要站外调用,请于后台-》插件管理-》本插件的配置中-》跨站调用允许域名-》填写外站的域名
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+116
@@ -0,0 +1,116 @@
|
||||
<!-- FastAdmin在线客服插件不是开源产品,所有文字、图片、样式、风格等版权归在线客服作者所有,如有复制、仿冒、抄袭、盗用,FastAdmin和在线客服作者将追究法律责任 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{$chat_name}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<meta name="renderer" content="webkit">
|
||||
<link href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.staticfile.org/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<link href="__CDN__/assets/addons/kefu/css/kefu_mobile.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="kefu_container">
|
||||
<header class="main_header">
|
||||
<span id="header_title"></span>
|
||||
<span id="csr_status" style="color: rgb(60, 118, 61);"></span>
|
||||
<span id="kefu_error">链接中...</span>
|
||||
<!-- <i class="fa fa-close fa-lg fa-fw back_page"></i> -->
|
||||
</header>
|
||||
|
||||
<div class="content_wrapper">
|
||||
|
||||
<!-- 聊天消息列表 -->
|
||||
<div class="chat_wrapper"></div>
|
||||
|
||||
<!--留言板-->
|
||||
<div id="kefu_leave_message" style="display: none;" class="panel panel-success kefu_window_view">
|
||||
<div class="panel-heading">非常抱歉,当前无客服在线,您留言后我们将尽快与您联系</div>
|
||||
<div class="panel-body">
|
||||
<form method="get" action="" class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label for="c-name" class="col-sm-2 control-label">姓名</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" name="name" class="form-control" id="c-name" placeholder="请输入您的姓名">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="c-contact" class="col-sm-2 control-label">联系方式</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" name="contact" class="form-control" id="c-contact" placeholder="请输入手机/QQ/微信号">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="c-message" class="col-sm-2 control-label">留言内容</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea rows="5" name="message" class="form-control" id="c-message" placeholder="遇到的问题、所需服务、产品等,我们将尽快与您取得联系"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button type="button" class="btn btn-success">确认留言</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 消息输入 -->
|
||||
<div id="kefu_emoji"></div>
|
||||
|
||||
<!--客服输入状态-->
|
||||
<div id="kefu_input_status"></div>
|
||||
|
||||
<div class="write">
|
||||
<div class="widget_textarea">
|
||||
<textarea id="kefu_message" spellcheck="false" style="height: 35px;"></textarea>
|
||||
</div>
|
||||
<div class="write_right">
|
||||
{present name="toolbar.expression"}
|
||||
<i class="smiley" title="{$toolbar.expression.title}" style="background: url('{$toolbar.expression.icon_image}') no-repeat center;background-size: 100% 100%;"></i>
|
||||
{/present}
|
||||
<i class="more" style="background:url('__CDN__/assets/addons/kefu/img/more.png') no-repeat center;background-size:100% 100%;"></i>
|
||||
<button class="btn btn-success btn-sm send_btn">发送</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: none;" class="footer_div">
|
||||
<div class="toolbar">
|
||||
{present name="toolbar.file"}
|
||||
<div class="toolbar_item select_file">
|
||||
<input id="chatfile" size="1" width="20" type="file" name="chatfile">
|
||||
<img src="{$toolbar.file.icon_image}" alt="{$toolbar.file.title}" />
|
||||
<div>{$toolbar.file.title}</div>
|
||||
</div>
|
||||
{/present}
|
||||
{present name="toolbar.goods"}
|
||||
<div class="toolbar_item" data-toolbar_type="goods">
|
||||
<img src="{$toolbar.goods.icon_image}" alt="{$toolbar.goods.title}" />
|
||||
<div>{$toolbar.goods.title}</div>
|
||||
</div>
|
||||
{/present}
|
||||
{present name="toolbar.order"}
|
||||
<div class="toolbar_item" data-toolbar_type="order">
|
||||
<img src="{$toolbar.order.icon_image}" alt="{$toolbar.order.title}" />
|
||||
<div>{$toolbar.order.title}</div>
|
||||
</div>
|
||||
{/present}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: none;" class="select_model">
|
||||
<div class="close_select_model">关闭</div>
|
||||
<div class="card_list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src=https://cdn.staticfile.org/jquery/2.1.4/jquery.min.js></script>
|
||||
<script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
|
||||
<script src="https://cdn.staticfile.org/layer/2.3/layer.js"></script>
|
||||
<script src="__CDN__/assets/addons/kefu/js/kefu_mobile.js"></script>
|
||||
<script>
|
||||
KeFu.initialize(document.domain, 'index');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+262
@@ -0,0 +1,262 @@
|
||||
<!-- KeFu-Admin-Template -->
|
||||
<ins class="KeFu">
|
||||
|
||||
<!-- 右侧悬浮按钮 -->
|
||||
<img src="{$cdnurl}/assets/addons/kefu/img/buoy1.png" class="kefu_button" id="kefu_button" data-html="true" data-container="body" data-toggle="popover" data-placement="left" data-content="" alt="客服浮标" />
|
||||
<!-- 右侧悬浮按钮-end -->
|
||||
|
||||
<!-- 聊天窗口 -->
|
||||
|
||||
<div class="modal fade bs-example-modal-lg" id="KeFuModal" tabindex="-1" role="dialog" aria-labelledby="KeFuModal">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">
|
||||
<span id="modal-title">在线客服</span>
|
||||
<div class="dropdown" id="kefu_csr_status">
|
||||
|
||||
<button class="btn btn-default btn-xs dropdown-toggle" type="button" data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false">
|
||||
<span class="kefu_status">状态</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
|
||||
<ul class="dropdown-menu" aria-labelledby="kefu_status_menu">
|
||||
<li><a data-status="3" href="#">在线</a></li>
|
||||
<li><a data-status="2" href="#">离开</a></li>
|
||||
<li><a data-status="1" href="#">繁忙</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<span id="kefu_error">链接中...</span>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="kefu-left">
|
||||
<div class="top">
|
||||
<input type="text" autocomplete="off" id="kefu_search_input" placeholder="搜索其实很简单"/>
|
||||
</div>
|
||||
|
||||
<div id="session_list_search" class="search_users">
|
||||
|
||||
</div>
|
||||
|
||||
<div class="panel-group" id="session_panel" role="tablist" aria-multiselectable="false">
|
||||
|
||||
<!-- data-parent="#session_panel" 若需自动折叠,可在a标签上添加此属性 -->
|
||||
<div class="panel panel-info dialogue_panel">
|
||||
<div class="panel-heading" role="tab" id="heading_dialogue">
|
||||
<h4 class="panel-title">
|
||||
<a role="button" data-toggle="collapse" href="#collapse_dialogue"
|
||||
aria-expanded="false" aria-controls="collapse_dialogue">对话中<span
|
||||
class="red_dot text-danger">•</span></a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="collapse_dialogue" class="panel-collapse collapse" role="tabpanel"
|
||||
aria-labelledby="heading_dialogue">
|
||||
<div class="panel-body">
|
||||
<ul id="session_list_dialogue" class="people">
|
||||
<div class="none_session">这里没有会话哦~</div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-info invitation_panel">
|
||||
<div class="panel-heading" role="tab" id="heading_invitation">
|
||||
<h4 class="panel-title">
|
||||
<a role="button" data-toggle="collapse" href="#collapse_invitation"
|
||||
aria-expanded="false" aria-controls="collapse_invitation">可邀请<span
|
||||
class="red_dot text-danger">•</span></a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="collapse_invitation" class="panel-collapse collapse" role="tabpanel"
|
||||
aria-labelledby="heading_invitation">
|
||||
<div class="panel-body">
|
||||
<ul id="session_list_invitation" class="people">
|
||||
<div class="none_session">这里没有会话哦~</div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-info recently_panel">
|
||||
<div class="panel-heading" role="tab" id="heading_recently">
|
||||
<h4 class="panel-title">
|
||||
<a role="button" data-toggle="collapse" href="#collapse_recently"
|
||||
aria-expanded="false" aria-controls="collapse_recently">最近沟通<span
|
||||
class="red_dot text-danger">•</span></a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="collapse_recently" class="panel-collapse collapse" role="tabpanel"
|
||||
aria-labelledby="heading_recently">
|
||||
<div class="panel-body">
|
||||
<ul id="session_list_recently" class="people">
|
||||
<div class="none_session">这里没有会话哦~</div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kefu-right">
|
||||
<div class="top">
|
||||
<span>
|
||||
<span id="session_user_name" class="name">无会话</span>
|
||||
<span class="top_span" id="kefu_blacklist">拉黑名单</span>
|
||||
<span class="top_span" id="kefu_view_trajectory">查看轨迹</span>
|
||||
<span class="top_span" id="kefu_view_user_card">用户名片</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="chat">
|
||||
<div class="chat_scroll kefu_window_view" id="kefu_scroll">
|
||||
|
||||
</div>
|
||||
<div class="VivaTimeline kefu_window_view" id="kefu_trajectory">
|
||||
<dl id="kefu_trajectory_log"></dl>
|
||||
</div>
|
||||
<div class="panel panel-success kefu_window_view" id="kefu_user_card">
|
||||
<div class="panel-heading">用户名片</div>
|
||||
<div class="panel-body">
|
||||
<form method="get" action="" class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">用户</label>
|
||||
<div class="col-sm-7">
|
||||
<input id="card-user" type="text" readonly="readonly"
|
||||
class="form-control"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="card-nickname" class="col-sm-3 control-label">用户昵称</label>
|
||||
<div class="col-sm-7">
|
||||
<input id="card-nickname" name="nickname" type="text"
|
||||
placeholder="请输入用户昵称" class="form-control"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="card-referrer" class="col-sm-3 control-label">用户来路</label>
|
||||
<div class="col-sm-7">
|
||||
<textarea id="card-referrer" name="referrer" rows="2"
|
||||
class="form-control" placeholder=""></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="card-contact" class="col-sm-3 control-label">联系方式</label>
|
||||
<div class="col-sm-7">
|
||||
<textarea id="card-contact" name="contact" rows="2" class="form-control"
|
||||
placeholder="QQ/微信/手机号等"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="card-note" class="col-sm-3 control-label">备注</label>
|
||||
<div class="col-sm-7">
|
||||
<textarea id="card-note" name="note" rows="4" class="form-control"
|
||||
placeholder="请输入备注,备注信息用户不可见"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-3 col-sm-7">
|
||||
<button id="card-button" type="button" class="btn btn-success">确认修改
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 按需渲染表情包 -->
|
||||
{present name="toolbar.expression"}
|
||||
<div class="kefu_emoji">
|
||||
<div id="kefu_emoji">
|
||||
{for start="1" end="37"}
|
||||
<img class="emoji" title="" src="{$cdnurl}/assets/addons/kefu/img/emoji/{$i}.png">
|
||||
{/for}
|
||||
</div>
|
||||
</div>
|
||||
{/present}
|
||||
|
||||
<!-- 按需渲染自动回复 -->
|
||||
{present name="toolbar.fastreply"}
|
||||
<div class="kefu_fast_reply_list">
|
||||
<table class="table table-striped table-hover" id="kefu_fast_reply_list">
|
||||
{volist name="fast_reply" id="reply"}
|
||||
<tr data-id='{$reply.id}'>
|
||||
<td>{$reply.title}</td>
|
||||
</tr>
|
||||
{/volist}
|
||||
</table>
|
||||
</div>
|
||||
{/present}
|
||||
|
||||
<!-- 准备链接输入面板 -->
|
||||
{present name="toolbar.link"}
|
||||
<div class="kefu_link_form">
|
||||
<div id="kefu_link_form">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">链接说明</span>
|
||||
<input type="text" placeholder="选填" class="form-control" id="kefu_link_form_ins"/>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">链接地址</span>
|
||||
<input type="text" value="http://" class="form-control" id="kefu_link_form_url"/>
|
||||
</div>
|
||||
<div class="link_form_buttons">
|
||||
<button type="button" class="btn btn-success btn-sm">确认</button>
|
||||
<button type="button" class="btn btn-default btn-sm">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/present}
|
||||
|
||||
<!--用户输入状态-->
|
||||
<div class="input_status">
|
||||
<div id="kefu_input_status"></div>
|
||||
</div>
|
||||
|
||||
<div class="write">
|
||||
<div class="write_top">
|
||||
{present name="toolbar.expression"}
|
||||
<i class="toolbar_icon smiley" title="{$toolbar.expression.title}" style="background: url('{$toolbar.expression.icon_image}') no-repeat center;background-size: 80% 80%;"></i>
|
||||
{/present}
|
||||
|
||||
{present name="toolbar.file"}
|
||||
<div class="select_file">
|
||||
<input title="{$toolbar.file.title}" id="chatfile" size="1" width="20" type="file" name="chatfile">
|
||||
<i class="attach toolbar_icon" style="background: url('{$toolbar.file.icon_image}') no-repeat center;background-size: 80% 80%;"></i>
|
||||
</div>
|
||||
{/present}
|
||||
|
||||
{present name="toolbar.link"}
|
||||
<i class="toolbar_icon link" title="{$toolbar.link.title}" style="background: url('{$toolbar.link.icon_image}') no-repeat center;background-size: 80% 80%;"></i>
|
||||
{/present}
|
||||
|
||||
<span title="按下Ctrl+Enter换行" class="write_top_span" id="send_tis">按下Enter发送消息</span>
|
||||
|
||||
{present name="toolbar.fastreply"}
|
||||
<span class="write_top_span kefu_fast_reply">{$toolbar.fastreply.title}</span>
|
||||
{/present}
|
||||
</div>
|
||||
<pre contenteditable="plaintext-only" id="kefu_message"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 客服端右击用户菜单 -->
|
||||
<ul class="dropdown-menu" style="z-index: 1060;" id="kefu_menu">
|
||||
<li class="kefu_menu_item transfer" data-action="transfer"><a href="#">转接会话</a></li>
|
||||
<li class="kefu_menu_item invitation" data-action="invitation"><a href="#">邀请对话</a></li>
|
||||
<li class="kefu_menu_item" data-action="edit_nickname"><a href="#">修改昵称</a></li>
|
||||
<li class="kefu_menu_item" data-action="del"><a href="#">删除会话</a></li>
|
||||
</ul>
|
||||
|
||||
<!-- 聊天窗口-end -->
|
||||
</ins>
|
||||
<!-- KeFu-Template-end
|
||||
Executable
+149
@@ -0,0 +1,149 @@
|
||||
<!-- KeFu-Template -->
|
||||
<ins class="KeFu">
|
||||
|
||||
<!-- 右侧悬浮按钮 -->
|
||||
<img src="{$cdnurl}/assets/addons/kefu/img/buoy1.png" class="kefu_button" id="kefu_button" data-html="true" data-container="body" data-toggle="popover" data-placement="left" data-content="" alt="客服浮标" />
|
||||
<!-- 右侧悬浮按钮-end -->
|
||||
|
||||
<!-- 聊天窗口 -->
|
||||
|
||||
<div class="modal fade bs-example-modal-lg" id="KeFuModal" tabindex="-1" role="dialog" aria-labelledby="KeFuModal">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">
|
||||
<span id="modal-title"></span>
|
||||
<span id="csr_status"></span>
|
||||
<span id="kefu_error">链接中...</span>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="kefu-left">
|
||||
<div class="alert alert-warning-light announcement">
|
||||
<i class="fa fa-bell-o"></i><span id="announcement"></span>
|
||||
</div>
|
||||
<div class="chat">
|
||||
<div class="chat_scroll kefu_window_view" id="kefu_scroll"></div>
|
||||
<div id="kefu_leave_message" style="display: none;" class="panel panel-success kefu_window_view">
|
||||
<div class="panel-heading">非常抱歉,当前无客服在线,您留言后我们将尽快与您联系</div>
|
||||
<div class="panel-body">
|
||||
<form method="get" action="" class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label for="c-name" class="col-sm-2 control-label">姓名</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" name="name" class="form-control" id="c-name" placeholder="请输入您的姓名">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="c-contact" class="col-sm-2 control-label">联系方式</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" name="contact" class="form-control" id="c-contact" placeholder="请输入手机/QQ/微信号">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="c-message" class="col-sm-2 control-label">留言内容</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea rows="5" name="message" class="form-control" id="c-message" placeholder="遇到的问题、所需服务、产品等,我们将尽快与您取得联系"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button type="button" class="btn btn-success">确认留言</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 按需渲染表情包 -->
|
||||
{present name="toolbar.expression"}
|
||||
<div class="kefu_emoji">
|
||||
<div id="kefu_emoji">
|
||||
{for start="1" end="37"}
|
||||
<img class="emoji" src="{$cdnurl}/assets/addons/kefu/img/emoji/{$i}.png">
|
||||
{/for}
|
||||
</div>
|
||||
</div>
|
||||
{/present}
|
||||
|
||||
<!-- 准备链接输入面板 -->
|
||||
{present name="toolbar.link"}
|
||||
<div class="kefu_link_form">
|
||||
<div id="kefu_link_form">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">链接说明</span>
|
||||
<input type="text" placeholder="选填" class="form-control" id="kefu_link_form_ins"/>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">链接地址</span>
|
||||
<input type="text" value="http://" class="form-control" id="kefu_link_form_url"/>
|
||||
</div>
|
||||
<div class="link_form_buttons">
|
||||
<button type="button" class="btn btn-success btn-sm">确认</button>
|
||||
<button type="button" class="btn btn-default btn-sm">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/present}
|
||||
|
||||
<!--商品和订单选择框-->
|
||||
<div class="goods_select_model">
|
||||
<div id="goods_select_model">
|
||||
<div class="project_list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--客服输入状态-->
|
||||
<div class="input_status">
|
||||
<div id="kefu_input_status"></div>
|
||||
</div>
|
||||
|
||||
<div class="write">
|
||||
<div class="write_top">
|
||||
|
||||
{present name="toolbar.expression"}
|
||||
<i class="toolbar_icon smiley" title="{$toolbar.expression.title}" style="background: url('{$toolbar.expression.icon_image}') no-repeat center;background-size: 80% 80%;"></i>
|
||||
{/present}
|
||||
|
||||
{present name="toolbar.file"}
|
||||
<div class="select_file">
|
||||
<input title="{$toolbar.file.title}" id="chatfile" size="1" width="20" type="file" name="chatfile">
|
||||
<i class="attach toolbar_icon" style="background: url('{$toolbar.file.icon_image}') no-repeat center;background-size: 80% 80%;"></i>
|
||||
</div>
|
||||
{/present}
|
||||
|
||||
<!--防止xss,链接功能取消-->
|
||||
<!--{present name="toolbar.link"}
|
||||
<i class="toolbar_icon link" title="{$toolbar.link.title}" style="background: url('{$toolbar.link.icon_image}') no-repeat center;background-size: 80% 80%;"></i>
|
||||
{/present}-->
|
||||
|
||||
{present name="toolbar.goods"}
|
||||
<i class="toolbar_icon goods" title="{$toolbar.goods.title}" style="background: url('{$toolbar.goods.icon_image}') no-repeat center;background-size: 80% 80%;"></i>
|
||||
{/present}
|
||||
|
||||
{present name="toolbar.order"}
|
||||
<i class="toolbar_icon order" title="{$toolbar.order.title}" style="background: url('{$toolbar.order.icon_image}') no-repeat center;background-size: 80% 80%;"></i>
|
||||
{/present}
|
||||
|
||||
<span title="按下Ctrl+Enter换行" id="send_tis">按下Enter发送消息</span>
|
||||
</div>
|
||||
<pre contenteditable="plaintext-only" id="kefu_message"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kefu-right">
|
||||
<div id="kefu_chat_slide_f"></div>
|
||||
<div class="chat_introduces"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 聊天窗口-end -->
|
||||
</ins>
|
||||
<!-- KeFu-Template-end -->
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
{"license":"regular","licenseto":"16556","licensekey":"7P9ZKjXLBSIwWObi 9uI02YSv\/lACsiPp+RDcR1UdyqzclNt9TjhV5+sAPbg="}
|
||||
Executable
+264
@@ -0,0 +1,264 @@
|
||||
<?php
|
||||
|
||||
namespace addons\qcloudsms;
|
||||
|
||||
use addons\qcloudsms\library\SmsSingleSender;
|
||||
use addons\qcloudsms\library\SmsVoicePromptSender;
|
||||
use addons\qcloudsms\library\SmsVoiceverifyCodeSender;
|
||||
use addons\qcloudsms\library\TtsVoiceSender;
|
||||
use think\Addons;
|
||||
use think\Config;
|
||||
use telesign\sdk\messaging\MessagingClient;
|
||||
use telesign\sdk\voice\VoiceClient;
|
||||
|
||||
use AlibabaCloud\Client\AlibabaCloud;
|
||||
use AlibabaCloud\Client\Exception\ClientException;
|
||||
use AlibabaCloud\Client\Exception\ServerException;
|
||||
|
||||
|
||||
|
||||
// 导入对应产品模块的client
|
||||
use TencentCloud\Sms\V20210111\SmsClient;
|
||||
// 导入要请求接口对应的Request类
|
||||
use TencentCloud\Sms\V20210111\Models\SendSmsRequest;
|
||||
use TencentCloud\Common\Exception\TencentCloudSDKException;
|
||||
use TencentCloud\Common\Credential;
|
||||
// 导入可选配置类
|
||||
use TencentCloud\Common\Profile\ClientProfile;
|
||||
use TencentCloud\Common\Profile\HttpProfile;
|
||||
|
||||
/**
|
||||
* 插件
|
||||
*/
|
||||
class Qcloudsms extends Addons
|
||||
{
|
||||
private $appid = null;
|
||||
private $appkey = null;
|
||||
private $config = null;
|
||||
private $sender = null;
|
||||
private $sendError = '';
|
||||
|
||||
public function ConfigInit()
|
||||
{
|
||||
$this->config = $this->getConfig();
|
||||
//如果使用语音短信 更换成语音短信模板
|
||||
if ($this->config['isVoice'] == 1) {
|
||||
$this->config['template'] = $this->config['voiceTemplate'];
|
||||
//语音短信 需要另行设置Aappid 与Appkey
|
||||
$this->appid = $this->config['voiceAppid'];
|
||||
$this->appkey = $this->config['voiceAppkey'];
|
||||
} else {
|
||||
$this->appid = $this->config['appid'];
|
||||
$this->appkey = $this->config['appkey'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 短信发送行为
|
||||
* @param Sms $params
|
||||
* @return boolean
|
||||
*/
|
||||
public function smsSend(&$params) {
|
||||
|
||||
//腾讯
|
||||
try {
|
||||
/* 必要步骤:
|
||||
* 实例化一个认证对象,入参需要传入腾讯云账户密钥对secretId,secretKey。
|
||||
* 这里采用的是从环境变量读取的方式,需要在环境变量中先设置这两个值。
|
||||
* 你也可以直接在代码中写死密钥对,但是小心不要将代码复制、上传或者分享给他人,
|
||||
* 以免泄露密钥对危及你的财产安全。
|
||||
* CAM密匙查询: https://console.cloud.tencent.com/cam/capi*/
|
||||
$cred = new Credential("IKIDhLn151pT0bItU3UsBQabWNYacdVIuoYF", "lPzb9sTGuNsZ6c3MugUOAmNq9874vrlb");
|
||||
//$cred = new Credential(getenv("TENCENTCLOUD_SECRET_ID"), getenv("TENCENTCLOUD_SECRET_KEY"));
|
||||
// 实例化一个http选项,可选的,没有特殊需求可以跳过
|
||||
$httpProfile = new HttpProfile();
|
||||
// 配置代理
|
||||
// $httpProfile->setProxy("https://ip:port");
|
||||
$httpProfile->setReqMethod("GET"); // post请求(默认为post请求)
|
||||
$httpProfile->setReqTimeout(30); // 请求超时时间,单位为秒(默认60秒)
|
||||
$httpProfile->setEndpoint("sms.tencentcloudapi.com"); // 指定接入地域域名(默认就近接入)
|
||||
// 实例化一个client选项,可选的,没有特殊需求可以跳过
|
||||
$clientProfile = new ClientProfile();
|
||||
$clientProfile->setSignMethod("TC3-HMAC-SHA256"); // 指定签名算法(默认为HmacSHA256)
|
||||
$clientProfile->setHttpProfile($httpProfile);
|
||||
// 实例化要请求产品(以sms为例)的client对象,clientProfile是可选的
|
||||
// 第二个参数是地域信息,根据您选择的国际站地域,如您选择的是新加坡国际站,则应该填入字符串ap-singapore,地域列表可参考https://intl.cloud.tencent.com/document/api/382/40466?lang=en#region-list
|
||||
$client = new SmsClient($cred, "ap-singapore", $clientProfile);
|
||||
// 实例化一个 sms 发送短信请求对象,每个接口都会对应一个request对象。
|
||||
$req = new SendSmsRequest();
|
||||
/* 短信应用ID: 短信SdkAppId在 [短信控制台] 添加应用后生成的实际SdkAppId,示例如1400006666 */
|
||||
$req->SmsSdkAppId = "2400000981";
|
||||
/* 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名,签名信息可登录 [短信控制台] 查看 */
|
||||
$req->SignName = "";
|
||||
/* 短信码号扩展号: 默认未开通,如需开通请联系 [sms helper] */
|
||||
$req->ExtendCode = "";
|
||||
/* 下发手机号码,采用 E.164 标准,+[国家或地区码][手机号]
|
||||
* 示例如:+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号*/
|
||||
$phone_number = $params['m_prefix'] . $params['mobile'];
|
||||
$req->PhoneNumberSet = array($phone_number);
|
||||
/* 国际/港澳台短信 SenderId: 中国大陆地区短信填空,默认未开通,如需开通请联系 [sms helper] */
|
||||
$req->SenderId = "";
|
||||
/* 用户的 session 内容: 可以携带用户侧 ID 等上下文信息,server 会原样返回 */
|
||||
$req->SessionContext = "【turkey global】Your OTP code is {1} Don't share this code with others dated!";
|
||||
/* 模板 ID: 必须填写已审核通过的模板 ID。模板ID可登录 [短信控制台] 查看 */
|
||||
$req->TemplateId = "2905310"; //2905133
|
||||
/* 模板参数: 若无模板参数,则设置为空*/
|
||||
$req->TemplateParamSet = array($params['code']);
|
||||
// 通过client对象调用SendSms方法发起请求。注意请求方法名与请求对象是对应的
|
||||
// 返回的resp是一个SendSmsResponse类的实例,与请求对象对应
|
||||
$resp = $client->SendSms($req);
|
||||
// 输出json格式的字符串回包
|
||||
// print_r($resp->toJsonString());
|
||||
// 也可以取出单个值。
|
||||
// 您可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义
|
||||
// print_r($resp->TotalCount);
|
||||
return true;
|
||||
}
|
||||
catch(TencentCloudSDKException $e) {
|
||||
// echo $e;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// AlibabaCloud::accessKeyClient('LTAI5tGjBhYq4u2e13f4cAQ6', 'X20XXcrKZHbGjpHHeTo3rfNuhMgeKg')
|
||||
// ->regionId('ap-southeast-1')
|
||||
// ->asGlobalClient();
|
||||
// $phone_number = $params['m_prefix'] . $params['mobile'];
|
||||
// $result = AlibabaCloud::rpcRequest()
|
||||
// ->product('Dysmsapi')
|
||||
// ->host('dysmsapi.ap-southeast-1.aliyuncs.com')
|
||||
// ->version('2018-05-01')
|
||||
// ->action('SendMessageToGlobe')
|
||||
// ->method('POST')
|
||||
// ->options([
|
||||
// 'query' => [
|
||||
// "To" => $phone_number,
|
||||
// // "From" => "1234567890",
|
||||
// "Message" => "【Poly】Your OTP code is " . $params['code'] . " Don't share this code with others dated",
|
||||
// ],
|
||||
// ])
|
||||
// ->request();
|
||||
//// print_r($result->toArray());
|
||||
//
|
||||
// return true;
|
||||
// if ($send_res['status']['code'] == 200) {
|
||||
// return true;
|
||||
// } else {
|
||||
// return true;
|
||||
// }
|
||||
// die;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 短信发送通知
|
||||
* @param array $params
|
||||
* @return boolean
|
||||
*/
|
||||
public function smsNotice(&$params)
|
||||
{
|
||||
$this->ConfigInit();
|
||||
try {
|
||||
if ($this->config['isTemplateSender'] == 1) {
|
||||
$templateID = $this->config['template'][$params['template']];
|
||||
|
||||
if ($this->config['isVoice'] != 1) {
|
||||
//普通短信发送
|
||||
$this->sender = new SmsSingleSender($this->appid, $this->appkey);
|
||||
$result = $this->sender->sendWithParam("86", $params['mobile'], $templateID, ["{$params['msg']}"], $this->config['sign'], "", "");
|
||||
} else {
|
||||
//语音短信发送
|
||||
$this->sender = new TtsVoiceSender($this->appid, $this->appkey);
|
||||
//参数: 国家码,手机号、模板ID、模板参数、播放次数(可选字段)、用户的session内容,服务器端原样返回(可选字段)
|
||||
$result = $this->sender->send("86", $params['mobile'], $templateID, [$params['msg']]);
|
||||
}
|
||||
} else {
|
||||
//判断是否是语音短信
|
||||
if ($this->config['isVoice'] != 1) {
|
||||
$this->sender = new SmsSingleSender($this->appid, $this->appkey);
|
||||
//参数:短信类型{1营销短信,0普通短信 }、国家码、手机号、短信内容、扩展码(可留空)、服务的原样返回的参数
|
||||
$result = $this->sender->send($params['type'], '86', $params['mobile'], $params['msg'], "", "");
|
||||
} else {
|
||||
$this->sender = new SmsVoicePromptSender($this->appid, $this->appkey);
|
||||
//参数:国家码、手机号、语音类型(目前固定为2)、短信内容、播放次数(默认2次)、服务的原样返回的参数
|
||||
$result = $this->sender->send('86', $params['mobile'], 2, $params['msg']);
|
||||
}
|
||||
}
|
||||
$rsp = (array)json_decode($result, true);
|
||||
if ($rsp['result'] == 0 && $rsp['errmsg'] == 'OK') {
|
||||
return true;
|
||||
} else {
|
||||
//记录错误信息
|
||||
$this->setError($rsp);
|
||||
return false;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
var_dump($e);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录失败信息
|
||||
* @param [type] $err [description]
|
||||
*/
|
||||
private function setError($err)
|
||||
{
|
||||
$this->sendError = $err;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取失败信息
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public function getError()
|
||||
{
|
||||
return $this->sendError;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测验证是否正确
|
||||
* @param Sms $params
|
||||
* @return boolean
|
||||
*/
|
||||
public function smsCheck(&$params)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件安装方法
|
||||
* @return bool
|
||||
*/
|
||||
public function install()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件卸载方法
|
||||
* @return bool
|
||||
*/
|
||||
public function uninstall()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件启用方法
|
||||
* @return bool
|
||||
*/
|
||||
public function enable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件禁用方法
|
||||
* @return bool
|
||||
*/
|
||||
public function disable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Executable
+139
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
return array(
|
||||
array(
|
||||
'name' => 'appid',
|
||||
'title' => '应用AppID',
|
||||
'type' => 'string',
|
||||
'content' =>
|
||||
array(),
|
||||
'value' => '',
|
||||
'rule' => 'required',
|
||||
'msg' => '',
|
||||
'tip' => '',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
),
|
||||
array(
|
||||
'name' => 'appkey',
|
||||
'title' => '应用AppKEY',
|
||||
'type' => 'string',
|
||||
'content' =>
|
||||
array(),
|
||||
'value' => '',
|
||||
'rule' => 'required',
|
||||
'msg' => '',
|
||||
'tip' => '',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
),
|
||||
array(
|
||||
'name' => 'voiceAppid',
|
||||
'title' => '语音短信AppID',
|
||||
'type' => 'string',
|
||||
'content' =>
|
||||
array(),
|
||||
'value' => '',
|
||||
'rule' => 'required',
|
||||
'msg' => '使用语音短信必须设置',
|
||||
'tip' => '',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
),
|
||||
array(
|
||||
'name' => 'voiceAppkey',
|
||||
'title' => '语音短信AppKEY',
|
||||
'type' => 'string',
|
||||
'content' =>
|
||||
array(),
|
||||
'value' => '',
|
||||
'rule' => 'required',
|
||||
'msg' => '使用语音短信必须设置',
|
||||
'tip' => '',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
),
|
||||
array(
|
||||
'name' => 'sign',
|
||||
'title' => '签名',
|
||||
'type' => 'string',
|
||||
'content' =>
|
||||
array(),
|
||||
'value' => 'your sign',
|
||||
'rule' => 'required',
|
||||
'msg' => '',
|
||||
'tip' => '',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
),
|
||||
array(
|
||||
'name' => 'isVoice',
|
||||
'title' => '是否使用语音短信',
|
||||
'type' => 'radio',
|
||||
'content' =>
|
||||
array(
|
||||
0 => '否',
|
||||
1 => '是',
|
||||
),
|
||||
'value' => '0',
|
||||
'rule' => 'required',
|
||||
'msg' => '',
|
||||
'tip' => '',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
),
|
||||
array(
|
||||
'name' => 'isTemplateSender',
|
||||
'title' => '是否使用短信模板发送',
|
||||
'type' => 'radio',
|
||||
'content' =>
|
||||
array(
|
||||
0 => '否',
|
||||
1 => '是',
|
||||
),
|
||||
'value' => '1',
|
||||
'rule' => 'required',
|
||||
'msg' => '',
|
||||
'tip' => '',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
),
|
||||
array(
|
||||
'name' => 'template',
|
||||
'title' => '短信模板',
|
||||
'type' => 'array',
|
||||
'content' =>
|
||||
array(),
|
||||
'value' =>
|
||||
array(
|
||||
'register' => '',
|
||||
'resetpwd' => '',
|
||||
'changepwd' => '',
|
||||
'profile' => '',
|
||||
),
|
||||
'rule' => 'required',
|
||||
'msg' => '',
|
||||
'tip' => '',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
),
|
||||
array(
|
||||
'name' => 'voiceTemplate',
|
||||
'title' => '语音短信模板',
|
||||
'type' => 'array',
|
||||
'content' =>
|
||||
array(),
|
||||
'value' =>
|
||||
array(
|
||||
'register' => '',
|
||||
'resetpwd' => '',
|
||||
'changepwd' => '',
|
||||
'profile' => '',
|
||||
),
|
||||
'rule' => 'required',
|
||||
'msg' => '',
|
||||
'tip' => '',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
),
|
||||
);
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace addons\qcloudsms\controller;
|
||||
|
||||
use think\addons\Controller;
|
||||
|
||||
class Index extends Controller
|
||||
{
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->error("当前插件暂无前台页面");
|
||||
}
|
||||
|
||||
}
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
name = qcloudsms
|
||||
title = 腾讯云短信发送插件
|
||||
intro = 腾讯云短信发送插件
|
||||
author = Seacent
|
||||
website = https://www.seacent.com
|
||||
version = 1.0.3
|
||||
state = 1
|
||||
url = /addons/qcloudsms
|
||||
license = regular
|
||||
licenseto = 16556
|
||||
Executable
+69
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace addons\qcloudsms\library;
|
||||
|
||||
use addons\qcloudsms\library\SmsSenderUtil;
|
||||
|
||||
|
||||
/**
|
||||
* 按语音文件fid发送语音通知类
|
||||
*
|
||||
*/
|
||||
class FileVoiceSender
|
||||
{
|
||||
private $url;
|
||||
private $appid;
|
||||
private $appkey;
|
||||
private $util;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param string $appid sdkappid
|
||||
* @param string $appkey sdkappid对应的appkey
|
||||
*/
|
||||
public function __construct($appid, $appkey)
|
||||
{
|
||||
$this->url = "https://cloud.tim.qq.com/v5/tlsvoicesvr/sendfvoice";
|
||||
$this->appid = $appid;
|
||||
$this->appkey = $appkey;
|
||||
$this->util = new SmsSenderUtil();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 按语音文件fid发送语音通知
|
||||
*
|
||||
* @param string $nationCode 国家码,如 86 为中国
|
||||
* @param string $phoneNumber 不带国家码的手机号
|
||||
* @param string $fid 语音文件fid
|
||||
* @param string $playtimes 播放次数,可选,最多3次,默认2次
|
||||
* @param string $ext 用户的session内容,服务端原样返回,可选字段,不需要可填空串
|
||||
* @return string 应答json字符串,详细内容参见腾讯云协议文档
|
||||
*/
|
||||
public function send($nationCode, $phoneNumber, $fid, $playtimes = 2, $ext = "")
|
||||
{
|
||||
$random = $this->util->getRandom();
|
||||
$curTime = time();
|
||||
$wholeUrl = $this->url . "?sdkappid=" . $this->appid . "&random=" . $random;
|
||||
|
||||
// 按照协议组织 post 包体
|
||||
$data = new \stdClass();
|
||||
$tel = new \stdClass();
|
||||
$tel->nationcode = "".$nationCode;
|
||||
$tel->mobile = "".$phoneNumber;
|
||||
$data->tel = $tel;
|
||||
$data->fid = $fid;
|
||||
$data->playtimes = $playtimes;
|
||||
|
||||
// app凭证
|
||||
$data->sig = $this->util->calculateSig($this->appkey, $random,
|
||||
$curTime, array($phoneNumber));
|
||||
|
||||
// unix时间戳,请求发起时间,如果和系统时间相差超过10分钟则会返回失败
|
||||
$data->time = $curTime;
|
||||
$data->ext = $ext;
|
||||
|
||||
return $this->util->sendCurlPost($wholeUrl, $data);
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace addons\qcloudsms\library;
|
||||
|
||||
use addons\qcloudsms\library\SmsSenderUtil;
|
||||
|
||||
/**
|
||||
* 拉取单个手机短信状态类
|
||||
*
|
||||
*/
|
||||
class SmsMobileStatusPuller
|
||||
{
|
||||
private $url;
|
||||
private $appid;
|
||||
private $appkey;
|
||||
private $util;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param string $appid sdkappid
|
||||
* @param string $appkey sdkappid对应的appkey
|
||||
*/
|
||||
public function __construct($appid, $appkey)
|
||||
{
|
||||
$this->url = "https://yun.tim.qq.com/v5/tlssmssvr/pullstatus4mobile";
|
||||
$this->appid = $appid;
|
||||
$this->appkey = $appkey;
|
||||
$this->util = new SmsSenderUtil();
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取回执结果
|
||||
*
|
||||
* @param int $type 拉取类型,0表示回执结果,1表示回复信息
|
||||
* @param string $nationCode 国家码,如 86 为中国
|
||||
* @param string $mobile 不带国家码的手机号
|
||||
* @param int $beginTime 开始时间(unix timestamp)
|
||||
* @param int $endTime 结束时间(unix timestamp)
|
||||
* @param int $max 拉取最大条数,最多100
|
||||
* @return string 应答json字符串,详细内容参见腾讯云协议文档
|
||||
*/
|
||||
private function pull($type, $nationCode, $mobile, $beginTime, $endTime, $max)
|
||||
{
|
||||
$random = $this->util->getRandom();
|
||||
$curTime = time();
|
||||
$wholeUrl = $this->url . "?sdkappid=" . $this->appid . "&random=" . $random;
|
||||
|
||||
$data = new \stdClass();
|
||||
$data->sig = $this->util->calculateSigForPuller($this->appkey, $random, $curTime);
|
||||
$data->time = $curTime;
|
||||
$data->type = $type;
|
||||
$data->max = $max;
|
||||
$data->begin_time = $beginTime;
|
||||
$data->end_time = $endTime;
|
||||
$data->nationcode = $nationCode;
|
||||
$data->mobile = $mobile;
|
||||
|
||||
return $this->util->sendCurlPost($wholeUrl, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取回执结果
|
||||
*
|
||||
* @param string $nationCode 国家码,如 86 为中国
|
||||
* @param string $mobile 不带国家码的手机号
|
||||
* @param int $beginTime 开始时间(unix timestamp)
|
||||
* @param int $endTime 结束时间(unix timestamp)
|
||||
* @param int $max 拉取最大条数,最多100
|
||||
* @return string 应答json字符串,详细内容参见腾讯云协议文档
|
||||
*/
|
||||
public function pullCallback($nationCode, $mobile, $beginTime, $endTime, $max)
|
||||
{
|
||||
return $this->pull(0, $nationCode, $mobile, $beginTime, $endTime, $max);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取回复信息
|
||||
*
|
||||
* @param string $nationCode 国家码,如 86 为中国
|
||||
* @param string $mobile 不带国家码的手机号
|
||||
* @param int $beginTime 开始时间(unix timestamp)
|
||||
* @param int $endTime 结束时间(unix timestamp)
|
||||
* @param int $max 拉取最大条数,最多100
|
||||
* @return string 应答json字符串,详细内容参见腾讯云协议文档
|
||||
*/
|
||||
public function pullReply($nationCode, $mobile, $beginTime, $endTime, $max)
|
||||
{
|
||||
return $this->pull(1, $nationCode, $mobile, $beginTime, $endTime, $max);
|
||||
}
|
||||
}
|
||||
Executable
+99
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace addons\qcloudsms\library;
|
||||
|
||||
use addons\qcloudsms\library\SmsSenderUtil;
|
||||
|
||||
/**
|
||||
* 群发短信类
|
||||
*
|
||||
*/
|
||||
class SmsMultiSender
|
||||
{
|
||||
private $url;
|
||||
private $appid;
|
||||
private $appkey;
|
||||
private $util;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param string $appid sdkappid
|
||||
* @param string $appkey sdkappid对应的appkey
|
||||
*/
|
||||
public function __construct($appid, $appkey)
|
||||
{
|
||||
$this->url = "https://yun.tim.qq.com/v5/tlssmssvr/sendmultisms2";
|
||||
$this->appid = $appid;
|
||||
$this->appkey = $appkey;
|
||||
$this->util = new SmsSenderUtil();
|
||||
}
|
||||
|
||||
/**
|
||||
* 普通群发
|
||||
*
|
||||
* 普通群发需明确指定内容,如果有多个签名,请在内容中以【】的方式添加到信息内容中,
|
||||
* 否则系统将使用默认签名。
|
||||
*
|
||||
*
|
||||
* @param int $type 短信类型,0 为普通短信,1 营销短信
|
||||
* @param string $nationCode 国家码,如 86 为中国
|
||||
* @param array $phoneNumbers 不带国家码的手机号列表
|
||||
* @param string $msg 信息内容,必须与申请的模板格式一致,否则将返回错误
|
||||
* @param string $extend 扩展码,可填空串
|
||||
* @param string $ext 服务端原样返回的参数,可填空串
|
||||
* @return string 应答json字符串,详细内容参见腾讯云协议文档
|
||||
*/
|
||||
public function send($type, $nationCode, $phoneNumbers, $msg, $extend = "", $ext = "")
|
||||
{
|
||||
$random = $this->util->getRandom();
|
||||
$curTime = time();
|
||||
$wholeUrl = $this->url . "?sdkappid=" . $this->appid . "&random=" . $random;
|
||||
|
||||
$data = new \stdClass();
|
||||
$data->tel = $this->util->phoneNumbersToArray($nationCode, $phoneNumbers);
|
||||
$data->type = $type;
|
||||
$data->msg = $msg;
|
||||
$data->sig = $this->util->calculateSig($this->appkey, $random,
|
||||
$curTime, $phoneNumbers);
|
||||
$data->time = $curTime;
|
||||
$data->extend = $extend;
|
||||
$data->ext = $ext;
|
||||
|
||||
return $this->util->sendCurlPost($wholeUrl, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定模板群发
|
||||
*
|
||||
*
|
||||
* @param string $nationCode 国家码,如 86 为中国
|
||||
* @param array $phoneNumbers 不带国家码的手机号列表
|
||||
* @param int $templId 模板id
|
||||
* @param array $params 模板参数列表,如模板 {1}...{2}...{3},那么需要带三个参数
|
||||
* @param string $sign 签名,如果填空串,系统会使用默认签名
|
||||
* @param string $extend 扩展码,可填空串
|
||||
* @param string $ext 服务端原样返回的参数,可填空串
|
||||
* @return string 应答json字符串,详细内容参见腾讯云协议文档
|
||||
*/
|
||||
public function sendWithParam($nationCode, $phoneNumbers, $templId, $params,
|
||||
$sign = "", $extend = "", $ext = "")
|
||||
{
|
||||
$random = $this->util->getRandom();
|
||||
$curTime = time();
|
||||
$wholeUrl = $this->url . "?sdkappid=" . $this->appid . "&random=" . $random;
|
||||
|
||||
$data = new \stdClass();
|
||||
$data->tel = $this->util->phoneNumbersToArray($nationCode, $phoneNumbers);
|
||||
$data->sign = $sign;
|
||||
$data->tpl_id = $templId;
|
||||
$data->params = $params;
|
||||
$data->sig = $this->util->calculateSigForTemplAndPhoneNumbers(
|
||||
$this->appkey, $random, $curTime, $phoneNumbers);
|
||||
$data->time = $curTime;
|
||||
$data->extend = $extend;
|
||||
$data->ext = $ext;
|
||||
|
||||
return $this->util->sendCurlPost($wholeUrl, $data);
|
||||
}
|
||||
}
|
||||
Executable
+211
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
|
||||
namespace addons\qcloudsms\library;
|
||||
|
||||
/**
|
||||
* 发送Util类
|
||||
*
|
||||
*/
|
||||
class SmsSenderUtil
|
||||
{
|
||||
/**
|
||||
* 生成随机数
|
||||
*
|
||||
* @return int 随机数结果
|
||||
*/
|
||||
public function getRandom()
|
||||
{
|
||||
return rand(100000, 999999);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成签名
|
||||
*
|
||||
* @param string $appkey sdkappid对应的appkey
|
||||
* @param string $random 随机正整数
|
||||
* @param string $curTime 当前时间
|
||||
* @param array $phoneNumbers 手机号码
|
||||
* @return string 签名结果
|
||||
*/
|
||||
public function calculateSig($appkey, $random, $curTime, $phoneNumbers)
|
||||
{
|
||||
$phoneNumbersString = $phoneNumbers[0];
|
||||
for ($i = 1; $i < count($phoneNumbers); $i++) {
|
||||
$phoneNumbersString .= ("," . $phoneNumbers[$i]);
|
||||
}
|
||||
|
||||
return hash("sha256", "appkey=".$appkey."&random=".$random
|
||||
."&time=".$curTime."&mobile=".$phoneNumbersString);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成签名
|
||||
*
|
||||
* @param string $appkey sdkappid对应的appkey
|
||||
* @param string $random 随机正整数
|
||||
* @param string $curTime 当前时间
|
||||
* @param array $phoneNumbers 手机号码
|
||||
* @return string 签名结果
|
||||
*/
|
||||
public function calculateSigForTemplAndPhoneNumbers($appkey, $random,
|
||||
$curTime, $phoneNumbers)
|
||||
{
|
||||
$phoneNumbersString = $phoneNumbers[0];
|
||||
for ($i = 1; $i < count($phoneNumbers); $i++) {
|
||||
$phoneNumbersString .= ("," . $phoneNumbers[$i]);
|
||||
}
|
||||
|
||||
return hash("sha256", "appkey=".$appkey."&random=".$random
|
||||
."&time=".$curTime."&mobile=".$phoneNumbersString);
|
||||
}
|
||||
|
||||
public function phoneNumbersToArray($nationCode, $phoneNumbers)
|
||||
{
|
||||
$i = 0;
|
||||
$tel = array();
|
||||
do {
|
||||
$telElement = new \stdClass();
|
||||
$telElement->nationcode = $nationCode;
|
||||
$telElement->mobile = $phoneNumbers[$i];
|
||||
array_push($tel, $telElement);
|
||||
} while (++$i < count($phoneNumbers));
|
||||
|
||||
return $tel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成签名
|
||||
*
|
||||
* @param string $appkey sdkappid对应的appkey
|
||||
* @param string $random 随机正整数
|
||||
* @param string $curTime 当前时间
|
||||
* @param array $phoneNumber 手机号码
|
||||
* @return string 签名结果
|
||||
*/
|
||||
public function calculateSigForTempl($appkey, $random, $curTime, $phoneNumber)
|
||||
{
|
||||
$phoneNumbers = array($phoneNumber);
|
||||
|
||||
return $this->calculateSigForTemplAndPhoneNumbers($appkey, $random,
|
||||
$curTime, $phoneNumbers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成签名
|
||||
*
|
||||
* @param string $appkey sdkappid对应的appkey
|
||||
* @param string $random 随机正整数
|
||||
* @param string $curTime 当前时间
|
||||
* @return string 签名结果
|
||||
*/
|
||||
public function calculateSigForPuller($appkey, $random, $curTime)
|
||||
{
|
||||
return hash("sha256", "appkey=".$appkey."&random=".$random
|
||||
."&time=".$curTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成上传文件授权
|
||||
*
|
||||
* @param string $appkey sdkappid对应的appkey
|
||||
* @param string $random 随机正整数
|
||||
* @param string $curTime 当前时间
|
||||
* @param array $fileSha1Sum 文件sha1sum
|
||||
* @return string 授权结果
|
||||
*/
|
||||
public function calculateAuth($appkey, $random, $curTime, $fileSha1Sum)
|
||||
{
|
||||
return hash("sha256", "appkey=".$appkey."&random=".$random
|
||||
."&time=".$curTime."&content-sha1=".$fileSha1Sum);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成sha1sum
|
||||
*
|
||||
* @param string $content 内容
|
||||
* @return string 内容sha1散列值
|
||||
*/
|
||||
public function sha1sum($content)
|
||||
{
|
||||
return hash("sha1", $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送请求
|
||||
*
|
||||
* @param string $url 请求地址
|
||||
* @param array $dataObj 请求内容
|
||||
* @return string 应答json字符串
|
||||
*/
|
||||
public function sendCurlPost($url, $dataObj)
|
||||
{
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_URL, $url);
|
||||
curl_setopt($curl, CURLOPT_HEADER, 0);
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($curl, CURLOPT_POST, 1);
|
||||
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 60);
|
||||
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($dataObj));
|
||||
curl_setopt($curl, CURLOPT_HTTPHEADER, array(
|
||||
'Content-Type: application/json; charset=utf-8',
|
||||
'Content-Length: ' . strlen(json_encode($dataObj)))
|
||||
);
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
|
||||
|
||||
$ret = curl_exec($curl);
|
||||
if (false == $ret) {
|
||||
// curl_exec failed
|
||||
$result = "{ \"result\":" . -2 . ",\"errmsg\":\"" . curl_error($curl) . "\"}";
|
||||
} else {
|
||||
$rsp = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
if (200 != $rsp) {
|
||||
$result = "{ \"result\":" . -1 . ",\"errmsg\":\"". $rsp
|
||||
. " " . curl_error($curl) ."\"}";
|
||||
} else {
|
||||
$result = $ret;
|
||||
}
|
||||
}
|
||||
|
||||
curl_close($curl);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送请求
|
||||
*
|
||||
* @param string $req 请求对象
|
||||
* @return string 应答json字符串
|
||||
*/
|
||||
public function fetch($req)
|
||||
{
|
||||
$curl = curl_init();
|
||||
|
||||
curl_setopt($curl, CURLOPT_URL, $req->url);
|
||||
curl_setopt($curl, CURLOPT_HTTPHEADER, $req->headers);
|
||||
curl_setopt($curl, CURLOPT_POSTFIELDS, $req->body);
|
||||
curl_setopt($curl, CURLOPT_HEADER, 0);
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($curl, CURLOPT_POST, 1);
|
||||
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 60);
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
|
||||
|
||||
$result = curl_exec($curl);
|
||||
|
||||
if (false == $result) {
|
||||
// curl_exec failed
|
||||
$result = "{ \"result\":" . -2 . ",\"errmsg\":\"" . curl_error($curl) . "\"}";
|
||||
} else {
|
||||
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
if (200 != $code) {
|
||||
$result = "{ \"result\":" . -1 . ",\"errmsg\":\"". $rsp
|
||||
. " " . curl_error($curl) ."\"}";
|
||||
}
|
||||
}
|
||||
curl_close($curl);
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
Executable
+107
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace addons\qcloudsms\library;
|
||||
|
||||
use addons\qcloudsms\library\SmsSenderUtil;
|
||||
|
||||
/**
|
||||
* 单发短信类
|
||||
*
|
||||
*/
|
||||
class SmsSingleSender
|
||||
{
|
||||
private $url;
|
||||
private $appid;
|
||||
private $appkey;
|
||||
private $util;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param string $appid sdkappid
|
||||
* @param string $appkey sdkappid对应的appkey
|
||||
*/
|
||||
public function __construct($appid, $appkey)
|
||||
{
|
||||
$this->url = "https://yun.tim.qq.com/v5/tlssmssvr/sendsms";
|
||||
$this->appid = $appid;
|
||||
$this->appkey = $appkey;
|
||||
$this->util = new SmsSenderUtil();
|
||||
}
|
||||
|
||||
/**
|
||||
* 普通单发
|
||||
*
|
||||
* 普通单发需明确指定内容,如果有多个签名,请在内容中以【】的方式添加到信息内容中,否则系统将使用默认签名。
|
||||
*
|
||||
* @param int $type 短信类型,0 为普通短信,1 营销短信
|
||||
* @param string $nationCode 国家码,如 86 为中国
|
||||
* @param string $phoneNumber 不带国家码的手机号
|
||||
* @param string $msg 信息内容,必须与申请的模板格式一致,否则将返回错误
|
||||
* @param string $extend 扩展码,可填空串
|
||||
* @param string $ext 服务端原样返回的参数,可填空串
|
||||
* @return string 应答json字符串,详细内容参见腾讯云协议文档
|
||||
*/
|
||||
public function send($type, $nationCode, $phoneNumber, $msg, $extend = "", $ext = "")
|
||||
{
|
||||
$random = $this->util->getRandom();
|
||||
$curTime = time();
|
||||
$wholeUrl = $this->url . "?sdkappid=" . $this->appid . "&random=" . $random;
|
||||
|
||||
// 按照协议组织 post 包体
|
||||
$data = new \stdClass();
|
||||
$tel = new \stdClass();
|
||||
$tel->nationcode = "".$nationCode;
|
||||
$tel->mobile = "".$phoneNumber;
|
||||
|
||||
$data->tel = $tel;
|
||||
$data->type = (int)$type;
|
||||
$data->msg = $msg;
|
||||
$data->sig = hash("sha256",
|
||||
"appkey=".$this->appkey."&random=".$random."&time="
|
||||
.$curTime."&mobile=".$phoneNumber, FALSE);
|
||||
$data->time = $curTime;
|
||||
$data->extend = $extend;
|
||||
$data->ext = $ext;
|
||||
|
||||
return $this->util->sendCurlPost($wholeUrl, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定模板单发
|
||||
*
|
||||
* @param string $nationCode 国家码,如 86 为中国
|
||||
* @param string $phoneNumber 不带国家码的手机号
|
||||
* @param int $templId 模板 id
|
||||
* @param array $params 模板参数列表,如模板 {1}...{2}...{3},那么需要带三个参数
|
||||
* @param string $sign 签名,如果填空串,系统会使用默认签名
|
||||
* @param string $extend 扩展码,可填空串
|
||||
* @param string $ext 服务端原样返回的参数,可填空串
|
||||
* @return string 应答json字符串,详细内容参见腾讯云协议文档
|
||||
*/
|
||||
public function sendWithParam($nationCode, $phoneNumber, $templId = 0, $params,
|
||||
$sign = "", $extend = "", $ext = "")
|
||||
{
|
||||
$random = $this->util->getRandom();
|
||||
$curTime = time();
|
||||
$wholeUrl = $this->url . "?sdkappid=" . $this->appid . "&random=" . $random;
|
||||
|
||||
// 按照协议组织 post 包体
|
||||
$data = new \stdClass();
|
||||
$tel = new \stdClass();
|
||||
$tel->nationcode = "".$nationCode;
|
||||
$tel->mobile = "".$phoneNumber;
|
||||
|
||||
$data->tel = $tel;
|
||||
$data->sig = $this->util->calculateSigForTempl($this->appkey, $random,
|
||||
$curTime, $phoneNumber);
|
||||
$data->tpl_id = $templId;
|
||||
$data->params = $params;
|
||||
$data->sign = $sign;
|
||||
$data->time = $curTime;
|
||||
$data->extend = $extend;
|
||||
$data->ext = $ext;
|
||||
|
||||
return $this->util->sendCurlPost($wholeUrl, $data);
|
||||
}
|
||||
}
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace addons\qcloudsms\library;
|
||||
|
||||
use addons\qcloudsms\library\SmsSenderUtil;
|
||||
|
||||
/**
|
||||
* 拉取短信状态类
|
||||
*
|
||||
*/
|
||||
class SmsStatusPuller
|
||||
{
|
||||
private $url;
|
||||
private $appid;
|
||||
private $appkey;
|
||||
private $util;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param string $appid sdkappid
|
||||
* @param string $appkey sdkappid对应的appkey
|
||||
*/
|
||||
public function __construct($appid, $appkey)
|
||||
{
|
||||
$this->url = "https://yun.tim.qq.com/v5/tlssmssvr/pullstatus";
|
||||
$this->appid = $appid;
|
||||
$this->appkey = $appkey;
|
||||
$this->util = new SmsSenderUtil();
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取回执结果
|
||||
*
|
||||
* @param int $type 拉取类型,0表示回执结果,1表示回复信息
|
||||
* @param int $max 最大条数,最多100
|
||||
* @return string 应答json字符串,详细内容参见腾讯云协议文档
|
||||
*/
|
||||
private function pull($type, $max)
|
||||
{
|
||||
$random = $this->util->getRandom();
|
||||
$curTime = time();
|
||||
$wholeUrl = $this->url . "?sdkappid=" . $this->appid . "&random=" . $random;
|
||||
|
||||
$data = new \stdClass();
|
||||
$data->sig = $this->util->calculateSigForPuller($this->appkey, $random, $curTime);
|
||||
$data->time = $curTime;
|
||||
$data->type = $type;
|
||||
$data->max = $max;
|
||||
|
||||
return $this->util->sendCurlPost($wholeUrl, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取回执结果
|
||||
*
|
||||
* @param int $max 拉取最大条数,最多100
|
||||
* @return string 应答json字符串,详细内容参见腾讯云协议文档
|
||||
*/
|
||||
public function pullCallback($max)
|
||||
{
|
||||
return $this->pull(0, $max);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取回复信息
|
||||
*
|
||||
* @param int $max 拉取最大条数,最多100
|
||||
* @return string 应答json字符串,详细内容参见腾讯云协议文档
|
||||
*/
|
||||
public function pullReply($max)
|
||||
{
|
||||
return $this->pull(1, $max);
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace addons\qcloudsms\library;
|
||||
|
||||
use addons\qcloudsms\library\SmsSenderUtil;
|
||||
|
||||
/**
|
||||
* 发送语音通知类
|
||||
*
|
||||
*/
|
||||
class SmsVoicePromptSender
|
||||
{
|
||||
private $url;
|
||||
private $appid;
|
||||
private $appkey;
|
||||
private $util;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param string $appid sdkappid
|
||||
* @param string $appkey sdkappid对应的appkey
|
||||
*/
|
||||
public function __construct($appid, $appkey)
|
||||
{
|
||||
$this->url = "https://yun.tim.qq.com/v5/tlsvoicesvr/sendvoiceprompt";
|
||||
$this->appid = $appid;
|
||||
$this->appkey = $appkey;
|
||||
$this->util = new SmsSenderUtil();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 发送语音通知
|
||||
*
|
||||
* @param string $nationCode 国家码,如 86 为中国
|
||||
* @param string $phoneNumber 不带国家码的手机号
|
||||
* @param string $prompttype 语音类型,目前固定为2
|
||||
* @param string $msg 信息内容,必须与申请的模板格式一致,否则将返回错误
|
||||
* @param string $playtimes 播放次数,可选,最多3次,默认2次
|
||||
* @param string $ext 用户的session内容,服务端原样返回,可选字段,不需要可填空串
|
||||
* @return string 应答json字符串,详细内容参见腾讯云协议文档
|
||||
*/
|
||||
public function send($nationCode, $phoneNumber, $prompttype, $msg, $playtimes = 2, $ext = "")
|
||||
{
|
||||
$random = $this->util->getRandom();
|
||||
$curTime = time();
|
||||
$wholeUrl = $this->url . "?sdkappid=" . $this->appid . "&random=" . $random;
|
||||
|
||||
// 按照协议组织 post 包体
|
||||
$data = new \stdClass();
|
||||
$tel = new \stdClass();
|
||||
$tel->nationcode = "".$nationCode;
|
||||
$tel->mobile = "".$phoneNumber;
|
||||
|
||||
$data->tel = $tel;
|
||||
// 通知内容,utf8编码,支持中文英文、数字及组合,需要和语音内容模版相匹配
|
||||
$data->promptfile = $msg;
|
||||
// 固定值 2
|
||||
$data->prompttype = $prompttype;
|
||||
$data->playtimes = $playtimes;
|
||||
// app凭证
|
||||
$data->sig = hash("sha256",
|
||||
"appkey=".$this->appkey."&random=".$random."&time="
|
||||
.$curTime."&mobile=".$phoneNumber, FALSE);
|
||||
// unix时间戳,请求发起时间,如果和系统时间相差超过10分钟则会返回失败
|
||||
$data->time = $curTime;
|
||||
$data->ext = $ext;
|
||||
return $this->util->sendCurlPost($wholeUrl, $data);
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace addons\qcloudsms\library;
|
||||
|
||||
use addons\qcloudsms\library\SmsSenderUtil;
|
||||
|
||||
/**
|
||||
* 发送语音验证码类
|
||||
*
|
||||
*/
|
||||
class SmsVoiceVerifyCodeSender
|
||||
{
|
||||
private $url;
|
||||
private $appid;
|
||||
private $appkey;
|
||||
private $util;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param string $appid sdkappid
|
||||
* @param string $appkey sdkappid对应的appkey
|
||||
*/
|
||||
public function __construct($appid, $appkey)
|
||||
{
|
||||
$this->url = "https://yun.tim.qq.com/v5/tlsvoicesvr/sendvoice";
|
||||
$this->appid = $appid;
|
||||
$this->appkey = $appkey;
|
||||
$this->util = new SmsSenderUtil();
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送语音验证码
|
||||
*
|
||||
* @param string $nationCode 国家码,如 86 为中国
|
||||
* @param string $phoneNumber 不带国家码的手机号
|
||||
* @param string $msg 信息内容,必须与申请的模板格式一致,否则将返回错误
|
||||
* @param int $playtimes 播放次数,可选,最多3次,默认2次
|
||||
* @param string $ext 用户的session内容,服务端原样返回,可选字段,不需要可填空串
|
||||
* @return string 应答json字符串,详细内容参见腾讯云协议文档
|
||||
*/
|
||||
public function send($nationCode, $phoneNumber, $msg, $playtimes = 2, $ext = "")
|
||||
{
|
||||
$random = $this->util->getRandom();
|
||||
$curTime = time();
|
||||
$wholeUrl = $this->url . "?sdkappid=" . $this->appid . "&random=" . $random;
|
||||
|
||||
// 按照协议组织 post 包体
|
||||
$data = new \stdClass();
|
||||
$tel = new \stdClass();
|
||||
$tel->nationcode = "".$nationCode;
|
||||
$tel->mobile = "".$phoneNumber;
|
||||
|
||||
$data->tel = $tel;
|
||||
$data->msg = $msg;
|
||||
$data->playtimes = $playtimes;
|
||||
// app凭证
|
||||
$data->sig = hash("sha256",
|
||||
"appkey=".$this->appkey."&random=".$random."&time="
|
||||
.$curTime."&mobile=".$phoneNumber, FALSE);
|
||||
// unix时间戳,请求发起时间,如果和系统时间相差超过10分钟则会返回失败
|
||||
$data->time = $curTime;
|
||||
$data->ext = $ext;
|
||||
|
||||
return $this->util->sendCurlPost($wholeUrl, $data);
|
||||
}
|
||||
}
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace addons\qcloudsms\library;
|
||||
|
||||
use addons\qcloudsms\library\SmsSenderUtil;
|
||||
|
||||
|
||||
/**
|
||||
* 指定模板发送语音通知类
|
||||
*
|
||||
*/
|
||||
class TtsVoiceSender
|
||||
{
|
||||
private $url;
|
||||
private $appid;
|
||||
private $appkey;
|
||||
private $util;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param string $appid sdkappid
|
||||
* @param string $appkey sdkappid对应的appkey
|
||||
*/
|
||||
public function __construct($appid, $appkey)
|
||||
{
|
||||
$this->url = "https://cloud.tim.qq.com/v5/tlsvoicesvr/sendtvoice";
|
||||
$this->appid = $appid;
|
||||
$this->appkey = $appkey;
|
||||
$this->util = new SmsSenderUtil();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 指定模板发送语音短信
|
||||
*
|
||||
* @param string $nationCode 国家码,如 86 为中国
|
||||
* @param string $phoneNumber 不带国家码的手机号
|
||||
* @param int $templId 模板 id
|
||||
* @param array $params 模板参数列表,如模板 {1}...{2}...{3},需要带三个参数
|
||||
* @param string $playtimes 播放次数,可选,最多3次,默认2次
|
||||
* @param string $ext 用户的session内容,服务端原样返回,可选字段,不需要可填空串
|
||||
* @return string 应答json字符串,详细内容参见腾讯云协议文档
|
||||
*/
|
||||
public function send($nationCode, $phoneNumber, $templId, $params, $playtimes = 2, $ext = "")
|
||||
{
|
||||
/*var_dump($nationCode);
|
||||
var_dump($phoneNumber);
|
||||
var_dump($templId);
|
||||
var_dump($params);
|
||||
var_dump($playtimes);
|
||||
exit();*/
|
||||
$random = $this->util->getRandom();
|
||||
$curTime = time();
|
||||
$wholeUrl = $this->url . "?sdkappid=" . $this->appid . "&random=" . $random;
|
||||
|
||||
// 按照协议组织 post 包体
|
||||
$data = new \stdClass();
|
||||
$tel = new \stdClass();
|
||||
$tel->nationcode = "".$nationCode;
|
||||
$tel->mobile = "".$phoneNumber;
|
||||
$data->tel = $tel;
|
||||
$data->tpl_id = $templId;
|
||||
$data->params = $params;
|
||||
$data->playtimes = $playtimes;
|
||||
|
||||
// app凭证
|
||||
$data->sig = $this->util->calculateSig($this->appkey, $random,
|
||||
$curTime, array($phoneNumber));
|
||||
|
||||
// unix时间戳,请求发起时间,如果和系统时间相差超过10分钟则会返回失败
|
||||
$data->time = $curTime;
|
||||
$data->ext = $ext;
|
||||
//var_dump($data);exit();
|
||||
return $this->util->sendCurlPost($wholeUrl, $data);
|
||||
}
|
||||
}
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
{"license":"regular","licenseto":"16556","licensekey":"SMgil3C0jFHIwQ2q m8gyQ649xwDwSnvkxcZdxGoduHQC3hVFuliavICi2t8=","files":["public\\assets\\addons\\summernote\\css\\summernote.css","public\\assets\\addons\\summernote\\font\\summernote.eot","public\\assets\\addons\\summernote\\font\\summernote.ttf","public\\assets\\addons\\summernote\\font\\summernote.woff","public\\assets\\addons\\summernote\\js\\summernote.js","public\\assets\\addons\\summernote\\js\\summernote.min.js","public\\assets\\addons\\summernote\\lang\\summernote-zh-CN.js","public\\assets\\addons\\summernote\\lang\\summernote-zh-CN.min.js","public\\assets\\addons\\summernote\\lang\\summernote-zh-TW.js","public\\assets\\addons\\summernote\\lang\\summernote-zh-TW.min.js"]}
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace addons\summernote;
|
||||
|
||||
use think\Addons;
|
||||
|
||||
/**
|
||||
* Summernote富文本编辑器
|
||||
*/
|
||||
class Summernote extends Addons
|
||||
{
|
||||
|
||||
/**
|
||||
* 插件安装方法
|
||||
* @return bool
|
||||
*/
|
||||
public function install()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件卸载方法
|
||||
* @return bool
|
||||
*/
|
||||
public function uninstall()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
require.config({
|
||||
paths: {
|
||||
'summernote': '../addons/summernote/lang/summernote-zh-CN.min'
|
||||
},
|
||||
shim: {
|
||||
'summernote': ['../addons/summernote/js/summernote.min', 'css!../addons/summernote/css/summernote.css'],
|
||||
}
|
||||
});
|
||||
require(['form', 'upload'], function (Form, Upload) {
|
||||
var _bindevent = Form.events.bindevent;
|
||||
Form.events.bindevent = function (form) {
|
||||
_bindevent.apply(this, [form]);
|
||||
try {
|
||||
//绑定summernote事件
|
||||
if ($(".summernote,.editor", form).size() > 0) {
|
||||
require(['summernote'], function () {
|
||||
var imageButton = function (context) {
|
||||
var ui = $.summernote.ui;
|
||||
var button = ui.button({
|
||||
contents: '<i class="fa fa-file-image-o"/>',
|
||||
tooltip: __('Choose'),
|
||||
click: function () {
|
||||
parent.Fast.api.open("general/attachment/select?element_id=&multiple=true&mimetype=image/*", __('Choose'), {
|
||||
callback: function (data) {
|
||||
var urlArr = data.url.split(/\,/);
|
||||
$.each(urlArr, function () {
|
||||
var url = Fast.api.cdnurl(this);
|
||||
context.invoke('editor.insertImage', url);
|
||||
});
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return button.render();
|
||||
};
|
||||
var attachmentButton = function (context) {
|
||||
var ui = $.summernote.ui;
|
||||
var button = ui.button({
|
||||
contents: '<i class="fa fa-file"/>',
|
||||
tooltip: __('Choose'),
|
||||
click: function () {
|
||||
parent.Fast.api.open("general/attachment/select?element_id=&multiple=true&mimetype=*", __('Choose'), {
|
||||
callback: function (data) {
|
||||
var urlArr = data.url.split(/\,/);
|
||||
$.each(urlArr, function () {
|
||||
var url = Fast.api.cdnurl(this);
|
||||
var node = $("<a href='" + url + "'>" + url + "</a>");
|
||||
context.invoke('insertNode', node[0]);
|
||||
});
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return button.render();
|
||||
};
|
||||
|
||||
$(".summernote,.editor", form).summernote({
|
||||
height: 250,
|
||||
lang: 'zh-CN',
|
||||
fontNames: [
|
||||
'Arial', 'Arial Black', 'Serif', 'Sans', 'Courier',
|
||||
'Courier New', 'Comic Sans MS', 'Helvetica', 'Impact', 'Lucida Grande',
|
||||
"Open Sans", "Hiragino Sans GB", "Microsoft YaHei",
|
||||
'微软雅黑', '宋体', '黑体', '仿宋', '楷体', '幼圆',
|
||||
],
|
||||
fontNamesIgnoreCheck: [
|
||||
"Open Sans", "Microsoft YaHei",
|
||||
'微软雅黑', '宋体', '黑体', '仿宋', '楷体', '幼圆'
|
||||
],
|
||||
toolbar: [
|
||||
['style', ['style', 'undo', 'redo']],
|
||||
['font', ['bold', 'underline', 'strikethrough', 'clear']],
|
||||
['fontname', ['color', 'fontname', 'fontsize']],
|
||||
['para', ['ul', 'ol', 'paragraph', 'height']],
|
||||
['table', ['table', 'hr']],
|
||||
['insert', ['link', 'picture', 'video']],
|
||||
['select', ['image', 'attachment']],
|
||||
['view', ['fullscreen', 'codeview', 'help']],
|
||||
],
|
||||
buttons: {
|
||||
image: imageButton,
|
||||
attachment: attachmentButton,
|
||||
},
|
||||
dialogsInBody: true,
|
||||
followingToolbar: false,
|
||||
callbacks: {
|
||||
onChange: function (contents) {
|
||||
$(this).val(contents);
|
||||
$(this).trigger('change');
|
||||
},
|
||||
onInit: function () {
|
||||
},
|
||||
onImageUpload: function (files) {
|
||||
var that = this;
|
||||
//依次上传图片
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
Upload.api.send(files[i], function (data) {
|
||||
var url = Fast.api.cdnurl(data.url);
|
||||
$(that).summernote("insertImage", url, 'filename');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
});
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
];
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace addons\summernote\controller;
|
||||
|
||||
use think\addons\Controller;
|
||||
|
||||
class Index extends Controller
|
||||
{
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->error("当前插件暂无前台页面");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
name = summernote
|
||||
title = Summernote富文本编辑器
|
||||
intro = 修改后台默认编辑器为Summernote
|
||||
author = Karson
|
||||
website = http://www.fastadmin.net
|
||||
version = 1.0.4
|
||||
state = 1
|
||||
url = /addons/summernote
|
||||
license = regular
|
||||
licenseto = 16556
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
{"license":"regular","licenseto":"16556","licensekey":"HwDQbqJWIPSRsNf4 2Y6yyJiwGyLCvlxFmynb9W\/ODYiwo9+52hxRGVgN0pA="}
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace addons\translate;
|
||||
|
||||
use app\common\library\Menu;
|
||||
use think\Addons;
|
||||
|
||||
/**
|
||||
* 插件
|
||||
*/
|
||||
class Translate extends Addons
|
||||
{
|
||||
|
||||
/**
|
||||
* 插件安装方法
|
||||
* @return bool
|
||||
*/
|
||||
public function install()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件卸载方法
|
||||
* @return bool
|
||||
*/
|
||||
public function uninstall()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件启用方法
|
||||
* @return bool
|
||||
*/
|
||||
public function enable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件禁用方法
|
||||
* @return bool
|
||||
*/
|
||||
public function disable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 应用初始化
|
||||
*/
|
||||
public function appInit()
|
||||
{
|
||||
\think\Loader::addNamespace('Stichoza\GoogleTranslate', ADDON_PATH . 'translate' . DS . 'library' . DS . 'google-translate-php' . DS . 'src' . DS);
|
||||
\think\Loader::addNamespace('GuzzleHttp\Psr7', ADDON_PATH . 'translate' . DS . 'library' . DS . 'guzzlehttp' . DS . 'psr7' . DS . 'src' . DS);
|
||||
\think\Loader::addNamespace('GuzzleHttp', ADDON_PATH . 'translate' . DS . 'library' . DS . 'guzzlehttp' . DS. 'guzzle' . DS . 'src'. DS);
|
||||
\think\Loader::addNamespace('GuzzleHttp\Promise', ADDON_PATH . 'translate' . DS . 'library' . DS . 'guzzlehttp' . DS . 'promises' . DS . 'src'. DS);
|
||||
require_once ADDON_PATH . 'translate' . DS . 'library' . DS . 'google-translate-php' . DS . 'src'. DS . 'common.php';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Executable
+259
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
[
|
||||
'name' => 'proxy',
|
||||
'title' => '代理IP',
|
||||
'type' => 'text',
|
||||
'content' =>
|
||||
[
|
||||
],
|
||||
'value' => '',
|
||||
'rule' => '',
|
||||
'msg' => '',
|
||||
'tip' => '一行一个代理,比如:http://127.0.0.1:1080',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
[
|
||||
'name' => 'language',
|
||||
'title' => '语言',
|
||||
'type' => 'select',
|
||||
'content' =>
|
||||
[
|
||||
'af' => '南非荷兰语',
|
||||
'sq' => '阿尔巴尼亚语',
|
||||
'am' => '阿姆哈拉语',
|
||||
'ar' => '阿拉伯语',
|
||||
'hy' => '亚美尼亚语',
|
||||
'az' => '阿塞拜疆语',
|
||||
'eu' => '巴斯克语',
|
||||
'be' => '白俄罗斯语',
|
||||
'bn' => '孟加拉语',
|
||||
'bs' => '波斯尼亚语',
|
||||
'bg' => '保加利亚语',
|
||||
'ca' => '加泰罗尼亚语',
|
||||
'ceb' => '宿务语',
|
||||
'zh-CN' => '中文(简体)',
|
||||
'zh-TW' => '中文(繁体)',
|
||||
'co' => '科西嘉语',
|
||||
'hr' => '克罗地亚语',
|
||||
'cs' => '捷克语',
|
||||
'da' => '丹麦语',
|
||||
'nl' => '荷兰语',
|
||||
'en' => '英语',
|
||||
'eo' => '世界语',
|
||||
'et' => '爱沙尼亚语',
|
||||
'fi' => '芬兰语',
|
||||
'fr' => '法语',
|
||||
'fy' => '弗里斯兰语',
|
||||
'gl' => '加利西亚语',
|
||||
'ka' => '格鲁吉亚语',
|
||||
'de' => '德语',
|
||||
'el' => '希腊语',
|
||||
'gu' => '古吉拉特语',
|
||||
'ht' => '海地克里奥尔语',
|
||||
'ha' => '豪萨语',
|
||||
'haw' => '夏威夷语',
|
||||
'he' => '希伯来语',
|
||||
'hi' => '印地语',
|
||||
'hmn' => '苗语',
|
||||
'hu' => '匈牙利语',
|
||||
'is' => '冰岛语',
|
||||
'ig' => '伊博语',
|
||||
'id' => '印度尼西亚语',
|
||||
'ga' => '爱尔兰语',
|
||||
'it' => '意大利语',
|
||||
'ja' => '日语',
|
||||
'jv' => '爪哇语',
|
||||
'kn' => '卡纳达语',
|
||||
'kk' => '哈萨克语',
|
||||
'km' => '高棉文',
|
||||
'ko' => '韩语',
|
||||
'ku' => '库尔德语',
|
||||
'ky' => '吉尔吉斯语',
|
||||
'lo' => '老挝语',
|
||||
'la' => '拉丁文',
|
||||
'lv' => '拉脱维亚语',
|
||||
'lt' => '立陶宛语',
|
||||
'lb' => '卢森堡语',
|
||||
'mk' => '马其顿语',
|
||||
'mg' => '马尔加什语',
|
||||
'ms' => '马来语',
|
||||
'ml' => '马拉雅拉姆文',
|
||||
'mt' => '马耳他语',
|
||||
'mi' => '毛利语',
|
||||
'mr' => '马拉地语',
|
||||
'mn' => '蒙古文',
|
||||
'my' => '缅甸语',
|
||||
'ne' => '尼泊尔语',
|
||||
'no' => '挪威语',
|
||||
'ny' => '尼杨扎语(齐切瓦语)',
|
||||
'ps' => '普什图语',
|
||||
'fa' => '波斯语',
|
||||
'pl' => '波兰语',
|
||||
'pt' => '葡萄牙语(葡萄牙、巴西)',
|
||||
'pa' => '旁遮普语',
|
||||
'ro' => '罗马尼亚语',
|
||||
'ru' => '俄语',
|
||||
'sm' => '萨摩亚语',
|
||||
'gd' => '苏格兰盖尔语',
|
||||
'sr' => '塞尔维亚语',
|
||||
'st' => '塞索托语',
|
||||
'sn' => '修纳语',
|
||||
'sd' => '信德语',
|
||||
'si' => '僧伽罗语',
|
||||
'sk' => '斯洛伐克语',
|
||||
'sl' => '斯洛文尼亚语',
|
||||
'so' => '索马里语',
|
||||
'es' => '西班牙语',
|
||||
'su' => '巽他语',
|
||||
'sw' => '斯瓦希里语',
|
||||
'sv' => '瑞典语',
|
||||
'tl' => '塔加路语(菲律宾语)',
|
||||
'tg' => '塔吉克语',
|
||||
'ta' => '泰米尔语',
|
||||
'te' => '泰卢固语',
|
||||
'th' => '泰文',
|
||||
'tr' => '土耳其语',
|
||||
'uk' => '乌克兰语',
|
||||
'ur' => '乌尔都语',
|
||||
'uz' => '乌兹别克语',
|
||||
'vi' => '越南语',
|
||||
'cy' => '威尔士语',
|
||||
'xh' => '班图语',
|
||||
'yi' => '意第绪语',
|
||||
'yo' => '约鲁巴语',
|
||||
'zu' => '祖鲁语',
|
||||
],
|
||||
'value' => 'af',
|
||||
'rule' => '',
|
||||
'msg' => '',
|
||||
'tip' => '',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
[
|
||||
'name' => 'en_language',
|
||||
'title' => '英文语言',
|
||||
'type' => 'select',
|
||||
'content' =>
|
||||
[
|
||||
'af' => 'Afrikaans',
|
||||
'sq' => 'Albanian',
|
||||
'am' => 'Amharic',
|
||||
'ar' => 'Arabic',
|
||||
'hy' => 'Armenian',
|
||||
'az' => 'Azerbaijani',
|
||||
'eu' => 'Basque',
|
||||
'be' => 'Belarusian',
|
||||
'bn' => 'Bengali',
|
||||
'bs' => 'Bosnian',
|
||||
'bg' => 'Bulgarian',
|
||||
'ca' => 'Catalan',
|
||||
'ceb' => 'Cebuano',
|
||||
'zh-CN' => 'Chinese (Simplified)',
|
||||
'zh-TW' => 'Chinese (Traditional)',
|
||||
'co' => 'Corsican',
|
||||
'hr' => 'Croatian',
|
||||
'cs' => 'Czech',
|
||||
'da' => 'Danish',
|
||||
'nl' => 'Dutch',
|
||||
'en' => 'English',
|
||||
'eo' => 'Esperanto',
|
||||
'et' => 'Estonian',
|
||||
'fi' => 'Finnish',
|
||||
'fr' => 'French',
|
||||
'fy' => 'Frisian',
|
||||
'gl' => 'Galician',
|
||||
'ka' => 'Georgian',
|
||||
'de' => 'German',
|
||||
'el' => 'Greek',
|
||||
'gu' => 'Gujarati',
|
||||
'ht' => 'Haitian Creole',
|
||||
'ha' => 'Hausa',
|
||||
'haw' => 'Hawaiian',
|
||||
'he' => 'Hebrew',
|
||||
'hi' => 'Hindi',
|
||||
'hmn' => 'Hmong',
|
||||
'hu' => 'Hungarian',
|
||||
'is' => 'Icelandic',
|
||||
'ig' => 'Igbo',
|
||||
'id' => 'Indonesian',
|
||||
'ga' => 'Irish',
|
||||
'it' => 'Italian',
|
||||
'ja' => 'Japanese',
|
||||
'jv' => 'Javanese',
|
||||
'kn' => 'Kannada',
|
||||
'kk' => 'Kazakh',
|
||||
'km' => 'Khmer',
|
||||
'rw' => 'Kinyarwanda',
|
||||
'ko' => 'Korean',
|
||||
'ku' => 'Kurdish',
|
||||
'ky' => 'Kyrgyz',
|
||||
'lo' => 'Lao',
|
||||
'la' => 'Latin',
|
||||
'lv' => 'Latvian',
|
||||
'lt' => 'Lithuanian',
|
||||
'lb' => 'Luxembourgish',
|
||||
'mk' => 'Macedonian',
|
||||
'mg' => 'Malagasy',
|
||||
'ms' => 'Malay',
|
||||
'ml' => 'Malayalam',
|
||||
'mt' => 'Maltese',
|
||||
'mi' => 'Maori',
|
||||
'mr' => 'Marathi',
|
||||
'mn' => 'Mongolian',
|
||||
'my' => 'Myanmar (Burmese)',
|
||||
'ne' => 'Nepali',
|
||||
'no' => 'Norwegian',
|
||||
'ny' => 'Nyanja (Chichewa)',
|
||||
'or' => 'Odia (Oriya)',
|
||||
'ps' => 'Pashto',
|
||||
'fa' => 'Persian',
|
||||
'pl' => 'Polish',
|
||||
'pt' => 'Portuguese (Portugal, Brazil)',
|
||||
'pa' => 'Punjabi',
|
||||
'ro' => 'Romanian',
|
||||
'ru' => 'Russian',
|
||||
'sm' => 'Samoan',
|
||||
'gd' => 'Scots Gaelic',
|
||||
'sr' => 'Serbian',
|
||||
'st' => 'Sesotho',
|
||||
'sn' => 'Shona',
|
||||
'sd' => 'Sindhi',
|
||||
'si' => 'Sinhala (Sinhalese)',
|
||||
'sk' => 'Slovak',
|
||||
'sl' => 'Slovenian',
|
||||
'so' => 'Somali',
|
||||
'es' => 'Spanish',
|
||||
'su' => 'Sundanese',
|
||||
'sw' => 'Swahili',
|
||||
'sv' => 'Swedish',
|
||||
'tl' => 'Tagalog (Filipino)',
|
||||
'tg' => 'Tajik',
|
||||
'ta' => 'Tamil',
|
||||
'tt' => 'Tatar',
|
||||
'te' => 'Telugu',
|
||||
'th' => 'Thai',
|
||||
'tr' => 'Turkish',
|
||||
'tk' => 'Turkmen',
|
||||
'uk' => 'Ukrainian',
|
||||
'ur' => 'Urdu',
|
||||
'ug' => 'Uyghur',
|
||||
'uz' => 'Uzbek',
|
||||
'vi' => 'Vietnamese',
|
||||
'cy' => 'Welsh',
|
||||
'xh' => 'Xhosa',
|
||||
'yi' => 'Yiddish',
|
||||
'yo' => 'Yoruba',
|
||||
'zu' => 'Zulu',
|
||||
],
|
||||
'value' => 'af',
|
||||
'rule' => '',
|
||||
'msg' => '',
|
||||
'tip' => '',
|
||||
'ok' => '',
|
||||
'extend' => '',
|
||||
],
|
||||
];
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace addons\translate\controller;
|
||||
|
||||
use think\addons\Controller;
|
||||
|
||||
class Index extends Controller
|
||||
{
|
||||
|
||||
public function index()
|
||||
{
|
||||
$languageList = get_addon_fullconfig('translate');
|
||||
$languageList = $languageList[1]['content'];
|
||||
|
||||
$this->view->assign('postUrl', addon_url('translate/index/translate'));
|
||||
$this->view->assign('languageList', json_encode($languageList));
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
public function translate()
|
||||
{
|
||||
$source = $this->request->post("source");
|
||||
$sourceLang = $this->request->post("source_lang");
|
||||
$targetLang = $this->request->post("target_lang");
|
||||
$res = doGoogleTranslate($source, $targetLang, $sourceLang, true);
|
||||
return json_encode($res);
|
||||
}
|
||||
|
||||
}
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
name = translate
|
||||
title = 谷歌自动翻译
|
||||
intro = 免费的谷歌翻译插件,可以自动识别语种 自动翻译成所有指定的语种 支持swoole协程并行翻译
|
||||
author = proxy
|
||||
website = https://www.fastadmin.net
|
||||
version = 1.0.0
|
||||
state = 1
|
||||
url = /addons/translate
|
||||
license = regular
|
||||
licenseto = 16556
|
||||
@@ -0,0 +1,9 @@
|
||||
language: php
|
||||
php:
|
||||
- 7.1
|
||||
- 7.2
|
||||
- 7.3
|
||||
|
||||
before_script:
|
||||
- composer self-update
|
||||
- composer install --prefer-source --no-interaction --dev
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2013 Levan Velijanashvili
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
Google Translate PHP
|
||||
====================
|
||||
|
||||
[](https://travis-ci.org/Stichoza/google-translate-php) [](https://packagist.org/packages/stichoza/google-translate-php) [](https://packagist.org/packages/stichoza/google-translate-php) [](https://packagist.org/packages/stichoza/google-translate-php) [](https://www.patreon.com/stichoza) [](https://paypal.me/stichoza)
|
||||
|
||||
Free Google Translate API PHP Package. Translates totally free of charge.
|
||||
|
||||
---
|
||||
|
||||
- **[Installation](#installation)**
|
||||
- **[Basic Usage](#basic-usage)**
|
||||
- [Advanced Usage](#advanced-usage)
|
||||
- [Language Detection](#language-detection)
|
||||
- [Using Raw Response](#using-raw-response)
|
||||
- [Custom URL](#custom-url)
|
||||
- [HTTP Client Configuration](#http-client-configuration)
|
||||
- [Custom Token Generator](#custom-token-generator)
|
||||
- [Errors and Exception Handling](#errors-and-exception-handling)
|
||||
- [Known Limitations](#known-limitations)
|
||||
- [Disclaimer](#disclaimer)
|
||||
- [Donation](#donation)
|
||||
|
||||
## Installation
|
||||
|
||||
Install this package via [Composer](https://getcomposer.org/).
|
||||
|
||||
```
|
||||
composer require stichoza/google-translate-php
|
||||
```
|
||||
|
||||
> Note: **PHP 7.1 or later** is required. For older versoins, use `^3.2` version of this package (see [old docs](https://github.com/Stichoza/google-translate-php/tree/3.2#google-translate-php)).
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Create GoogleTranslate object
|
||||
|
||||
```php
|
||||
use Stichoza\GoogleTranslate\GoogleTranslate;
|
||||
|
||||
$tr = new GoogleTranslate('en'); // Translates into English
|
||||
```
|
||||
Or you can change languages later
|
||||
```php
|
||||
$tr = new GoogleTranslate(); // Translates to 'en' from auto-detected language by default
|
||||
$tr->setSource('en'); // Translate from English
|
||||
$tr->setSource(); // Detect language automatically
|
||||
$tr->setTarget('ka'); // Translate to Georgian
|
||||
```
|
||||
Translate sentences
|
||||
```php
|
||||
echo $tr->translate('Hello World!');
|
||||
```
|
||||
Also, you can also use method chaining
|
||||
```php
|
||||
echo $tr->setSource('en')->setTarget('ka')->translate('Goodbye');
|
||||
```
|
||||
Or call a shorthand static method `trans`
|
||||
```php
|
||||
echo GoogleTranslate::trans('Hello again', 'ka', 'en');
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Language Detection
|
||||
|
||||
To detect language automatically, just set the source language to `null`:
|
||||
|
||||
```php
|
||||
$tr = new GoogleTranslate('es', null); // Or simply do not pass the second parameter
|
||||
```
|
||||
|
||||
```php
|
||||
$tr->setSource(); // Another way
|
||||
```
|
||||
|
||||
Use `getLastDetectedSource()` to get detected language:
|
||||
|
||||
```php
|
||||
$tr = new GoogleTranslate('fr');
|
||||
|
||||
$text = $tr->translate('Hello World!');
|
||||
|
||||
echo $tr->getLastDetectedSource(); // Output: en
|
||||
```
|
||||
|
||||
Return value will be `null` if the language couldn't be detected.
|
||||
|
||||
Supported languages are listed in [Google API docs](https://cloud.google.com/translate/docs/languages).
|
||||
|
||||
### Using Raw Response
|
||||
|
||||
For advanced usage, you might need the raw results that Google Translate provides. you can use `getResponse` method for that.
|
||||
|
||||
```php
|
||||
$responseArray = $tr->getResponse('Hello world!');
|
||||
```
|
||||
|
||||
### Custom URL
|
||||
|
||||
You can override the default Google Translate url by `setUrl` method. Useful for some countries
|
||||
|
||||
```php
|
||||
$tr->setUrl('http://translate.google.cn/translate_a/single');
|
||||
```
|
||||
|
||||
### HTTP Client Configuration
|
||||
|
||||
This package uses [Guzzle](https://github.com/guzzle/guzzle) for HTTP requests. You can pass an array of [guzzle client configuration options](http://docs.guzzlephp.org/en/latest/request-options.html) as a third parameter to `GoogleTranslate` constructor, or just use `setOptions` method.
|
||||
|
||||
You can configure proxy, user-agent, default headers, connection timeout and so on using this options.
|
||||
|
||||
```php
|
||||
$tr = new GoogleTranslate('en', 'ka', [
|
||||
'timeout' => 10,
|
||||
'proxy' => [
|
||||
'http' => 'tcp://localhost:8125',
|
||||
'https' => 'tcp://localhost:9124'
|
||||
],
|
||||
'headers' => [
|
||||
'User-Agent' => 'Foo/5.0 Lorem Ipsum Browser'
|
||||
]
|
||||
]);
|
||||
```
|
||||
|
||||
```php
|
||||
// Set proxy to tcp://localhost:8090
|
||||
$tr->setOptions(['proxy' => 'tcp://localhost:8090'])->translate('Hello');
|
||||
|
||||
// Set proxy to socks5://localhost:1080
|
||||
$tr->setOptions(['proxy' => 'socks5://localhost:1080'])->translate('World');
|
||||
```
|
||||
|
||||
For more information, see [Creating a Client](http://guzzle.readthedocs.org/en/latest/quickstart.html#creating-a-client) section in Guzzle docs (6.x version).
|
||||
|
||||
### Custom Token Generator
|
||||
|
||||
You can override the token generator class by passing a generator object as a fourth parameter of constructor or just use `setTokenProvider` method.
|
||||
|
||||
Generator must implement `Stichoza\GoogleTranslate\Tokens\TokenProviderInterface`.
|
||||
|
||||
```php
|
||||
use Stichoza\GoogleTranslate\Tokens\TokenProviderInterface;
|
||||
|
||||
class MyTokenGenerator implements TokenProviderInterface
|
||||
{
|
||||
public function generateToken(string $source, string $target, string $text) : string
|
||||
{
|
||||
// Your code here
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And use:
|
||||
|
||||
```php
|
||||
$tr->setTokenProvider(new MyTokenGenerator);
|
||||
```
|
||||
|
||||
### Errors and Exception Handling
|
||||
|
||||
Static method `trans()` and non-static `translate()` and `getResponse()` will throw following Exceptions:
|
||||
|
||||
- `ErrorException` If the HTTP request fails for some reason.
|
||||
- `UnexpectedValueException` If data received from Google cannot be decoded.
|
||||
|
||||
In addition, `translate()` and `trans()` methods will return `null` if there is no translation available.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- `503 Service Unavailable` response:
|
||||
If you are getting this error, it is most likely that Google has banned your external IP address and/or [requires you to solve a CAPTCHA](https://github.com/Stichoza/google-translate-php/issues/18). This is not a bug in this package. Google has become stricter, and it seems like they keep lowering the number of allowed requests per IP per a certain amount of time. Try sending less requests to stay under the radar, or change your IP frequently ([for example using proxies](#http-client-configuration)). Please note that once an IP is banned, even if it's only temporary, the ban can last from a few minutes to more than 12-24 hours, as each case is different.
|
||||
- `429 Too Many Requests` response:
|
||||
This error is basically the same as explained above.
|
||||
- `413 Request Entity Too Large` response:
|
||||
This error means that your input string is too long. Google only allows a maximum of 5000 characters to be translated at once. If you want to translate a longer text, you can split it to shorter parts, and translate them one-by-one.
|
||||
- `403 Forbidden` response:
|
||||
This is not an issue with this package. Google Translate itself has some problems when it comes to translating some characters and HTML entities. See https://github.com/Stichoza/google-translate-php/issues/119#issuecomment-558078133
|
||||
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This package is developed for educational purposes only. Do not depend on this package as it may break anytime as it is based on crawling the Google Translate website. Consider buying [Official Google Translate API](https://cloud.google.com/translate/) for other types of usage.
|
||||
|
||||
## Donation
|
||||
|
||||
If this package helped you reduce your time to develop something, or it solved any major problems you had, feel free give me a cup of coffee :)
|
||||
|
||||
- [Patreon](https://www.patreon.com/stichoza)
|
||||
- [PayPal](https://paypal.me/stichoza)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "stichoza/google-translate-php",
|
||||
"description": "Free Google Translate API PHP Package",
|
||||
"keywords": ["google", "translate", "translator", "php"],
|
||||
"license": "MIT",
|
||||
"homepage": "http://github.com/Stichoza/google-translate-php",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Levan Velijanashvili",
|
||||
"email": "me@stichoza.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.1",
|
||||
"guzzlehttp/guzzle": "~6.0",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^7.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Stichoza\\GoogleTranslate\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Stichoza\\GoogleTranslate\\Tests\\": "tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit backupGlobals="false"
|
||||
backupStaticAttributes="false"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
convertErrorsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
processIsolation="false"
|
||||
stopOnFailure="false"
|
||||
syntaxCheck="false">
|
||||
<testsuites>
|
||||
<testsuite name="Application Test Suite">
|
||||
<directory>./tests/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
@@ -0,0 +1,341 @@
|
||||
<?php
|
||||
|
||||
namespace Stichoza\GoogleTranslate;
|
||||
|
||||
use ErrorException;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use Stichoza\GoogleTranslate\Tokens\GoogleTokenGenerator;
|
||||
use Stichoza\GoogleTranslate\Tokens\TokenProviderInterface;
|
||||
use UnexpectedValueException;
|
||||
|
||||
/**
|
||||
* Free Google Translate API PHP Package.
|
||||
*
|
||||
* @author Levan Velijanashvili <me@stichoza.com>
|
||||
* @link http://stichoza.com/
|
||||
* @license MIT
|
||||
*/
|
||||
class GoogleTranslate
|
||||
{
|
||||
/**
|
||||
* @var \GuzzleHttp\Client HTTP Client
|
||||
*/
|
||||
protected $client;
|
||||
|
||||
/**
|
||||
* @var string|null Source language - from where the string should be translated
|
||||
*/
|
||||
protected $source;
|
||||
|
||||
/**
|
||||
* @var string Target language - to which language string should be translated
|
||||
*/
|
||||
protected $target;
|
||||
|
||||
/**
|
||||
* @var string|null Last detected source language
|
||||
*/
|
||||
protected $lastDetectedSource;
|
||||
|
||||
/**
|
||||
* @var string Google Translate URL base
|
||||
*/
|
||||
protected $usUrl = 'https://translate.google.com/translate_a/single';
|
||||
|
||||
protected $url = 'https://translate.google.cn/translate_a/single';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @var array Dynamic GuzzleHttp client options
|
||||
*/
|
||||
protected $options = [];
|
||||
|
||||
/**
|
||||
* @var array URL Parameters
|
||||
*/
|
||||
protected $urlParams = [
|
||||
'client' => 'webapp',
|
||||
'hl' => 'en',
|
||||
'dt' => [
|
||||
't', // Translate
|
||||
'bd', // Full translate with synonym ($bodyArray[1])
|
||||
'at', // Other translate ($bodyArray[5] - in google translate page this shows when click on translated word)
|
||||
'ex', // Example part ($bodyArray[13])
|
||||
'ld', // I don't know ($bodyArray[8])
|
||||
'md', // Definition part with example ($bodyArray[12])
|
||||
'qca', // I don't know ($bodyArray[8])
|
||||
'rw', // Read also part ($bodyArray[14])
|
||||
'rm', // I don't know
|
||||
'ss' // Full synonym ($bodyArray[11])
|
||||
],
|
||||
'sl' => null, // Source language
|
||||
'tl' => null, // Target language
|
||||
'q' => null, // String to translate
|
||||
'ie' => 'UTF-8', // Input encoding
|
||||
'oe' => 'UTF-8', // Output encoding
|
||||
'multires' => 1,
|
||||
'otf' => 0,
|
||||
'pc' => 1,
|
||||
'trs' => 1,
|
||||
'ssel' => 0,
|
||||
'tsel' => 0,
|
||||
'kc' => 1,
|
||||
'tk' => null,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array Regex key-value patterns to replace on response data
|
||||
*/
|
||||
protected $resultRegexes = [
|
||||
'/,+/' => ',',
|
||||
'/\[,/' => '[',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var TokenProviderInterface Token provider
|
||||
*/
|
||||
protected $tokenProvider;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* For more information about HTTP client configuration options, see "Request Options" in
|
||||
* GuzzleHttp docs: http://docs.guzzlephp.org/en/stable/request-options.html
|
||||
*
|
||||
* @param string $target Target language
|
||||
* @param string|null $source Source language
|
||||
* @param array|null $options Associative array of http client configuration options
|
||||
* @param TokenProviderInterface|null $tokenProvider
|
||||
*/
|
||||
public function __construct(string $target = 'en', string $source = null, array $options = null, TokenProviderInterface $tokenProvider = null)
|
||||
{
|
||||
$this->client = new Client();
|
||||
$this->setTokenProvider($tokenProvider ?? new GoogleTokenGenerator)
|
||||
->setOptions($options) // Options are already set in client constructor tho.
|
||||
->setSource($source)
|
||||
->setTarget($target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set target language for translation.
|
||||
*
|
||||
* @param string $target Language code
|
||||
* @return GoogleTranslate
|
||||
*/
|
||||
public function setTarget(string $target): self
|
||||
{
|
||||
$this->target = $target;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set source language for translation.
|
||||
*
|
||||
* @param string|null $source Language code
|
||||
* @return GoogleTranslate
|
||||
*/
|
||||
public function setSource(string $source = null): self
|
||||
{
|
||||
$this->source = $source ?? 'auto';
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Google Translate URL base
|
||||
*
|
||||
* @param string $url Google Translate URL base
|
||||
* @return GoogleTranslate
|
||||
*/
|
||||
public function setUrl(string $url): self
|
||||
{
|
||||
$this->url = $url;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set GuzzleHttp client options.
|
||||
*
|
||||
* @param array $options guzzleHttp client options.
|
||||
* @return GoogleTranslate
|
||||
*/
|
||||
public function setOptions(array $options = null): self
|
||||
{
|
||||
$this->options = $options ?? [];
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set token provider.
|
||||
*
|
||||
* @param TokenProviderInterface $tokenProvider
|
||||
* @return GoogleTranslate
|
||||
*/
|
||||
public function setTokenProvider(TokenProviderInterface $tokenProvider): self
|
||||
{
|
||||
$this->tokenProvider = $tokenProvider;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last detected source language
|
||||
*
|
||||
* @return string|null Last detected source language
|
||||
*/
|
||||
public function getLastDetectedSource(): ?string
|
||||
{
|
||||
return $this->lastDetectedSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override translate method for static call.
|
||||
*
|
||||
* @param string $string
|
||||
* @param string $target
|
||||
* @param string|null $source
|
||||
* @param array $options
|
||||
* @param TokenProviderInterface|null $tokenProvider
|
||||
* @return null|string
|
||||
* @throws ErrorException If the HTTP request fails
|
||||
* @throws UnexpectedValueException If received data cannot be decoded
|
||||
*/
|
||||
public static function trans(string $string, string $target = 'en', string $source = null, array $options = [], TokenProviderInterface $tokenProvider = null): ?string
|
||||
{
|
||||
return (new self)
|
||||
->setTokenProvider($tokenProvider ?? new GoogleTokenGenerator)
|
||||
->setOptions($options) // Options are already set in client constructor tho.
|
||||
->setSource($source)
|
||||
->setTarget($target)
|
||||
->translate($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate text.
|
||||
*
|
||||
* This can be called from instance method translate() using __call() magic method.
|
||||
* Use $instance->translate($string) instead.
|
||||
*
|
||||
* @param string $string String to translate
|
||||
* @return string|null
|
||||
* @throws ErrorException If the HTTP request fails
|
||||
* @throws UnexpectedValueException If received data cannot be decoded
|
||||
*/
|
||||
public function translate(string $string): ?string
|
||||
{
|
||||
/*
|
||||
* if source lang and target lang are the same
|
||||
* just return the string without any request to google
|
||||
*/
|
||||
if ($this->source == $this->target) return $string;
|
||||
|
||||
$responseArray = $this->getResponse($string);
|
||||
|
||||
/*
|
||||
* if response in text and the content has zero the empty returns true, lets check
|
||||
* if response is string and not empty and create array for further logic
|
||||
*/
|
||||
if (is_string($responseArray) && $responseArray != '') {
|
||||
$responseArray = [$responseArray];
|
||||
}
|
||||
|
||||
// Check if translation exists
|
||||
if (!isset($responseArray[0]) || empty($responseArray[0])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Detect languages
|
||||
$detectedLanguages = [];
|
||||
|
||||
// the response contains only single translation, don't create loop that will end with
|
||||
// invalid foreach and warning
|
||||
if (!is_string($responseArray)) {
|
||||
foreach ($responseArray as $item) {
|
||||
if (is_string($item)) {
|
||||
$detectedLanguages[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Another case of detected language
|
||||
if (isset($responseArray[count($responseArray) - 2][0][0])) {
|
||||
$detectedLanguages[] = $responseArray[count($responseArray) - 2][0][0];
|
||||
}
|
||||
|
||||
// Set initial detected language to null
|
||||
$this->lastDetectedSource = null;
|
||||
|
||||
// Iterate and set last detected language
|
||||
foreach ($detectedLanguages as $lang) {
|
||||
if ($this->isValidLocale($lang)) {
|
||||
$this->lastDetectedSource = $lang;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// the response can be sometimes an translated string.
|
||||
if (is_string($responseArray)) {
|
||||
return $responseArray;
|
||||
} else {
|
||||
if (is_array($responseArray[0])) {
|
||||
return (string) array_reduce($responseArray[0], function ($carry, $item) {
|
||||
$carry .= $item[0];
|
||||
return $carry;
|
||||
});
|
||||
} else {
|
||||
return (string) $responseArray[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get response array.
|
||||
*
|
||||
* @param string $string String to translate
|
||||
* @throws ErrorException If the HTTP request fails
|
||||
* @throws UnexpectedValueException If received data cannot be decoded
|
||||
* @return array|string Response
|
||||
*/
|
||||
public function getResponse(string $string): array
|
||||
{
|
||||
$queryArray = array_merge($this->urlParams, [
|
||||
'sl' => $this->source,
|
||||
'tl' => $this->target,
|
||||
'tk' => $this->tokenProvider->generateToken($this->source, $this->target, $string),
|
||||
'q' => $string
|
||||
]);
|
||||
|
||||
$queryUrl = preg_replace('/%5B(?:[0-9]|[1-9][0-9]+)%5D=/', '=', http_build_query($queryArray));
|
||||
|
||||
try {
|
||||
$response = $this->client->get($this->url, [
|
||||
'query' => $queryUrl,
|
||||
] + $this->options);
|
||||
} catch (RequestException $e) {
|
||||
throw new ErrorException($e->getMessage());
|
||||
}
|
||||
|
||||
$body = $response->getBody(); // Get response body
|
||||
|
||||
// Modify body to avoid json errors
|
||||
$bodyJson = preg_replace(array_keys($this->resultRegexes), array_values($this->resultRegexes), $body);
|
||||
|
||||
// Decode JSON data
|
||||
if (($bodyArray = json_decode($bodyJson, true)) === null) {
|
||||
throw new UnexpectedValueException('Data cannot be decoded or it is deeper than the recursion limit');
|
||||
}
|
||||
|
||||
return $bodyArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if given locale is valid.
|
||||
*
|
||||
* @param string $lang Langauge code to verify
|
||||
* @return bool
|
||||
*/
|
||||
protected function isValidLocale(string $lang): bool
|
||||
{
|
||||
return (bool) preg_match('/^([a-z]{2})(-[A-Z]{2})?$/', $lang);
|
||||
}
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace Stichoza\GoogleTranslate\Tokens;
|
||||
|
||||
/**
|
||||
* Google Token Generator.
|
||||
*
|
||||
* Thanks to @helen5106 and @tehmaestro and few other cool guys
|
||||
* at https://github.com/Stichoza/google-translate-php/issues/32
|
||||
*/
|
||||
class GoogleTokenGenerator implements TokenProviderInterface
|
||||
{
|
||||
/**
|
||||
* Generate and return a token.
|
||||
*
|
||||
* @param string $source Source language
|
||||
* @param string $target Target language
|
||||
* @param string $text Text to translate
|
||||
* @return string Token
|
||||
*/
|
||||
public function generateToken(string $source, string $target, string $text): string
|
||||
{
|
||||
return $this->TL($text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a valid Google Translate request token.
|
||||
*
|
||||
* @param string $a text to translate
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function TL($a)
|
||||
{
|
||||
$tkk = $this->TKK();
|
||||
$b = $tkk[0];
|
||||
|
||||
for ($d = [], $e = 0, $f = 0; $f < $this->JS_length($a); $f++) {
|
||||
$g = $this->JS_charCodeAt($a, $f);
|
||||
if (128 > $g) {
|
||||
$d[$e++] = $g;
|
||||
} else {
|
||||
if (2048 > $g) {
|
||||
$d[$e++] = $g >> 6 | 192;
|
||||
} else {
|
||||
if (55296 == ($g & 64512) && $f + 1 < $this->JS_length($a) && 56320 == ($this->JS_charCodeAt($a, $f + 1) & 64512)) {
|
||||
$g = 65536 + (($g & 1023) << 10) + ($this->JS_charCodeAt($a, ++$f) & 1023);
|
||||
$d[$e++] = $g >> 18 | 240;
|
||||
$d[$e++] = $g >> 12 & 63 | 128;
|
||||
} else {
|
||||
$d[$e++] = $g >> 12 | 224;
|
||||
}
|
||||
$d[$e++] = $g >> 6 & 63 | 128;
|
||||
}
|
||||
$d[$e++] = $g & 63 | 128;
|
||||
}
|
||||
}
|
||||
$a = $b;
|
||||
for ($e = 0; $e < count($d); $e++) {
|
||||
$a += $d[$e];
|
||||
$a = $this->RL($a, '+-a^+6');
|
||||
}
|
||||
$a = $this->RL($a, '+-3^+b+-f');
|
||||
$a ^= $tkk[1] ? $tkk[1] + 0 : 0;
|
||||
if (0 > $a) {
|
||||
$a = ($a & 2147483647) + 2147483648;
|
||||
}
|
||||
$a = fmod($a, pow(10, 6));
|
||||
|
||||
return $a.'.'.($a ^ $b);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
private function TKK()
|
||||
{
|
||||
return ['406398', (561666268 + 1526272306)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Process token data by applying multiple operations.
|
||||
* (Params are safe, no need for multibyte functions)
|
||||
*
|
||||
* @param int $a
|
||||
* @param string $b
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function RL($a, $b)
|
||||
{
|
||||
for ($c = 0; $c < strlen($b) - 2; $c += 3) {
|
||||
$d = $b[$c + 2];
|
||||
$d = 'a' <= $d ? ord($d[0]) - 87 : intval($d);
|
||||
$d = '+' == $b[$c + 1] ? $this->unsignedRightShift($a, $d) : $a << $d;
|
||||
$a = '+' == $b[$c] ? ($a + $d & 4294967295) : $a ^ $d;
|
||||
}
|
||||
|
||||
return $a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsigned right shift implementation
|
||||
* https://msdn.microsoft.com/en-us/library/342xfs5s(v=vs.94).aspx
|
||||
* http://stackoverflow.com/a/43359819/2953830
|
||||
*
|
||||
* @param $a
|
||||
* @param $b
|
||||
*
|
||||
* @return number
|
||||
*/
|
||||
private function unsignedRightShift($a, $b)
|
||||
{
|
||||
if ($b >= 32 || $b < -32) {
|
||||
$m = (int)($b / 32);
|
||||
$b = $b - ($m * 32);
|
||||
}
|
||||
|
||||
if ($b < 0) {
|
||||
$b = 32 + $b;
|
||||
}
|
||||
|
||||
if ($b == 0) {
|
||||
return (($a >> 1) & 0x7fffffff) * 2 + (($a >> $b) & 1);
|
||||
}
|
||||
|
||||
if ($a < 0) {
|
||||
$a = ($a >> 1);
|
||||
$a &= 2147483647;
|
||||
$a |= 0x40000000;
|
||||
$a = ($a >> ($b - 1));
|
||||
} else {
|
||||
$a = ($a >> $b);
|
||||
}
|
||||
|
||||
return $a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JS charCodeAt equivalent result with UTF-16 encoding
|
||||
*
|
||||
* @param string $str
|
||||
* @param int $index
|
||||
*
|
||||
* @return number
|
||||
*/
|
||||
private function JS_charCodeAt($str, $index)
|
||||
{
|
||||
$utf16 = mb_convert_encoding($str, 'UTF-16LE', 'UTF-8');
|
||||
return ord($utf16[$index*2]) + (ord($utf16[$index*2+1]) << 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JS equivalent string length with UTF-16 encoding
|
||||
*
|
||||
* @param string $str
|
||||
*
|
||||
* @return number
|
||||
*/
|
||||
private function JS_length($str)
|
||||
{
|
||||
$utf16 = mb_convert_encoding($str, 'UTF-16LE', 'UTF-8');
|
||||
return strlen($utf16)/2;
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Stichoza\GoogleTranslate\Tokens;
|
||||
|
||||
/**
|
||||
* A nice interface for providing tokens.
|
||||
*/
|
||||
class SampleTokenGenerator implements TokenProviderInterface
|
||||
{
|
||||
/**
|
||||
* Generate a fake token just as an example.
|
||||
*
|
||||
* @param string $source Source language
|
||||
* @param string $target Target langiage
|
||||
* @param string $text Text to translate
|
||||
* @return string Token
|
||||
*/
|
||||
public function generateToken(string $source, string $target, string $text): string
|
||||
{
|
||||
return sprintf('%d.%d', rand(10000, 99999), rand(10000, 99999));
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Stichoza\GoogleTranslate\Tokens;
|
||||
|
||||
/**
|
||||
* A nice interface for providing tokens.
|
||||
*/
|
||||
interface TokenProviderInterface
|
||||
{
|
||||
/**
|
||||
* Generate and return a token.
|
||||
*
|
||||
* @param string $source Source language
|
||||
* @param string $target Target langiage
|
||||
* @param string $text Text to translate
|
||||
* @return string Token
|
||||
*/
|
||||
public function generateToken(string $source, string $target, string $text): string;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user