chore: 后端其余代码文件

This commit is contained in:
li
2026-03-30 15:16:23 +08:00
parent f1d46898ed
commit 5ace8fb3ab
6706 changed files with 1321537 additions and 0 deletions
Executable
+10
View File
@@ -0,0 +1,10 @@
{
"directory": "public/assets/libs",
"ignoredDependencies": [
"es6-promise",
"file-saver",
"html2canvas",
"jspdf",
"jspdf-autotable"
]
}
Executable
+11
View File
@@ -0,0 +1,11 @@
[app]
debug = false
trace = false
[database]
hostname = 127.0.0.1
database = fastadmin
username = root
password = root
hostport = 3306
prefix = fa_
Executable
+6
View File
@@ -0,0 +1,6 @@
/runtime/*
/nbproject/private/
/public/uploads/*
/application/api/controller/logs/*
/vendor/workerman/workerman.log
/.idea/*
Executable
+271
View File
@@ -0,0 +1,271 @@
# 部署说明(FastAdmin / ThinkPHP5
本文档用于指导你将本项目部署到生产环境(Linux + Nginx/Apache + PHP-FPM + MySQL + Redis)。项目为 **FastAdminThinkPHP 5.0.x** 多模块应用,Web 根目录为 `public/`
## 1. 技术栈与关键组件
- **后端框架**ThinkPHP 5.0.x`topthink/framework ~5.0.24`
- **后台框架**FastAdmin(含插件机制 addons
- **数据库**:MySQL(配置文件 `application/database.php`,默认表前缀 `fa_`
- **缓存/队列**Redis
- Redis 业务配置:`application/config.php``redis`
- 队列配置:`application/extra/queue.php``connector=redis`,默认 `select=5`
- **队列**`topthink/think-queue 1.1.6`(命令:`php think queue:listen` / `php think queue:work --daemon`
- **定时任务**:项目内存在 API 形式的定时任务入口(见 `application/api/controller/Cron.php`),以及 K 线生成脚本(见根目录 `NewTradeKline*.php``TradeKline.php`
## 2. 服务器与运行环境要求
- **PHP**>= 7.0(建议 7.2/7.4,更高版本需要你自行验证兼容性)
- **PHP 扩展**(至少):
- `pdo_mysql`
- `curl`
- `json`
- 以及常用扩展:`mbstring``openssl``gd`/`imagick`(如涉及图片处理)
- **MySQL**5.7/8.0(推荐 5.7 起)
- **Redis**5.x/6.x
- **Web Server**Nginx 或 Apache
## 3. 代码发布目录建议
假设你发布到:
- 项目目录:`/www/wwwroot/exchange-admin`(示例)
- Web 根目录:`/www/wwwroot/exchange-admin/public`
注意:ThinkPHP/FastAdmin **必须** 将站点根指向 `public/`,不要直接指向项目根目录。
## 4. 目录结构说明(部署相关)
部署与运维时,重点关注下面这些目录/文件的用途:
- **`public/`**
- Web 根目录(Nginx/Apache 的 `root` 必须指向这里)
- 包含 `index.php` 应用入口
- 上传目录通常在 `public/uploads/`(按实际生成)
- **`application/`**
- ThinkPHP 应用代码(多模块)
- 常见模块:
- `application/admin/`:后台模块
- `application/api/`API 模块
- `application/index/`:前台模块
- 核心配置:
- `application/config.php`:应用配置(含 `redis` 等)
- `application/database.php`:数据库配置(通过 `Env::get('database.xxx', ...)`
- 扩展配置:`application/extra/`(如 `queue.php``site.php``upload.php`
- 目录用途细分(重点:控制器在哪、对应哪个模块):
- `application/admin/`
- `controller/`:后台控制器(典型:`app\admin\controller\Index``Dashboard`
- `model/`:后台模块模型
- `view/`:后台页面模板
- `command/`:后台相关 CLI 命令(安装/资源压缩/插件等)
- `config.php`:后台模块的补充配置(会覆盖/补充全局配置)
- `application/api/`
- `controller/`API 控制器(典型:`app\api\controller\Index``Cron``Job`
- `job/`:队列任务类(如 `app\api\job\Dismiss`,由 `think-queue` 消费执行)
- `common.php` / `config.php`API 模块内的补充逻辑与配置
- `application/index/`
- `controller/`:前台控制器(典型:`app\index\controller\Index`
- `view/`:前台模板
- `application/kefu/`
- `controller/`:客服/网关相关启动类(Workerman/GatewayWorker,典型:`app\kefu\controller\Sgateway`
- `application/common/`
- `controller/`:控制器基类与公共逻辑
- `Backend.php`:后台控制器基类(`app\common\controller\Backend`
- `Api.php`API 控制器基类(`app\common\controller\Api`
- `Frontend.php`:前台控制器基类(`app\common\controller\Frontend`
- `model/`:公共模型
- `view/`:公共视图/模板
- `application/extra/`
- `queue.php`:队列配置
- `site.php`:站点配置(后台“系统配置”常会生成/覆盖此文件)
- `upload.php`:上传配置
- `addons.php`:插件 Hook/路由相关配置
- 模块/控制器与 URL 的关系(常见规则):
- ThinkPHP5 默认格式:`/{module}/{controller}/{action}`
- 例如:
- `application/api/controller/Index.php` -> `/api/index/*`
- `application/admin/controller/Index.php` -> 后台入口文件 + `/index/*`
- `public/index.php` 为统一入口;后台入口通常是 `public/admin.php`(安装后可能被重命名为随机文件名)。
- **`addons/`**
- FastAdmin 插件目录(启用/禁用插件会影响系统行为与路由)
- **`runtime/`**
- 运行时目录(日志、缓存等)
- **生产环境通常需要可写权限**(由 PHP-FPM 运行用户写入)
- **`vendor/`**
- Composer 依赖(生产环境需要完整存在;一般不建议放到 Web 根可访问路径下)
- **`think`**
- ThinkPHP 控制台入口(命令行)
- 常用:安装 `php think install`、队列 `php think queue:work --daemon`
部署建议:
- **只暴露 `public/` 为 Web 根**,其余目录不要通过 Web Server 直接访问。
- 确保 `runtime/``public/uploads/` 对 PHP-FPM 用户可写。
## 5. 环境配置(.env
根目录存在示例文件:`.env.sample`,你可以复制为 `.env`
- **Linux**`cp .env.sample .env`
- **Windows**:复制并重命名
示例内容(节选):
- `app.debug` / `app.trace`
- `database.hostname` / `database.database` / `database.username` / `database.password` / `database.hostport` / `database.prefix`
说明:
- `application/config.php``application/database.php` 会通过 `think\Env::get()` 读取环境变量(即 `.env` 或系统环境变量)。
- 生产环境请确保:`[app] debug=false`
## 6. 初始化安装(推荐:命令行安装)
本项目内置 FastAdmin 安装命令:`php think install`(见 `application/admin/command/Install.php`)。
### 6.1 方式 A:命令行安装(推荐)
在项目根目录执行:
- `php think install --hostname=127.0.0.1 --hostport=3306 --database=YOUR_DB --username=YOUR_USER --password=YOUR_PASS --prefix=fa_`
安装过程会自动:
- 创建数据库(若权限允许)
- 导入初始化 SQL`application/admin/command/Install/fastadmin.sql`
- 写入/更新 `application/database.php` 中的 `Env::get('database.xxx', '...')` 默认值
-`public/admin.php` **重命名为随机文件名**(提升安全性)
- 写入安装锁:`application/admin/command/Install/install.lock`
安装完成后终端会输出:
- 后台入口(随机文件名)
- 后台账号与密码
### 6.2 方式 B:手工导入 SQL(不推荐,除非你很清楚自己在做什么)
你也可以手工导入:
- `application/admin/command/Install/fastadmin.sql`
然后自行配置数据库连接、后台入口等。但因为安装命令还会改写 `application/database.php` 和重命名后台入口文件,手工方式更容易遗漏步骤。
## 7. Nginx 配置示例
请按你的域名与证书路径调整,重点是:
- `root` 指向 `public/`
- `index` 包含 `index.php`
- `try_files` 将请求转发到 `index.php`ThinkPHP 路由)
示例:
```nginx
server {
listen 80;
server_name example.com;
root /www/wwwroot/exchange-admin/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass 127.0.0.1:9000; # 按实际 php-fpm 调整
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
location ~ /\. {
deny all;
}
}
```
## 8. 目录权限与运行权限
生产环境常见需要可写的目录(以实际情况为准):
- `runtime/`
- `public/uploads/`(上传文件)
建议:
- 让 PHP-FPM 运行用户(如 `www`/`nginx`)对上述目录具备写权限。
## 8. 队列(Redis)部署
本项目已配置 `application/extra/queue.php` 使用 Redis 队列。
### 8.1 启动队列消费者
在项目根目录:
- **监听模式**`php think queue:listen`
- **Work 模式**`php think queue:work --daemon`
建议生产环境使用 **Supervisor** 守护进程常驻(或 systemd),保证进程异常退出可自动拉起。
### 8.2 关键注意点
- Redis 连接参数来源:`application/extra/queue.php`(读取 `Env::get('redis.host')``Env::get('redis.port')`
- 本项目队列 Redis DB 索引默认:`select => 5`
## 9. 定时任务 / Cron 部署
项目中存在两类定时/循环任务:
### 9.1 API 形式定时任务(`application/api/controller/Cron.php`
`Cron.php` 内部通过宝塔面板 API`addons/btpanel`)去创建 crontab,并大量使用 `curl` 调用 `https://cs.400110.cn/api/...` 这类接口。
建议你按实际业务:
- 确认定时任务目标域名(`Cron.php` 里的 `$url`)是否为你的生产域
- 明确定时任务应当在 **业务服务器内部** 调用还是由外部定时器触发
### 9.2 K 线生成脚本(根目录 `NewTradeKline*.php` / `TradeKline.php`
这些脚本通过 `define('BIND_MODULE', 'api/xxx/index')` 绑定到 API 模块执行。
常见用法(参考 `Cron.php` 中的示例):
- `php NewTradeKlines.php start -d`
- `php NewTradeKlines.php stop`
说明:这些命令风格类似 **Workerman** 的启动方式,建议用守护进程/面板托管,避免意外退出。
## 10. HTTPS 证书与私钥
根目录存在:
- `server.key`
- `server.pem`
生产环境请确保:
- 私钥文件权限严格控制
- 不要将真实生产私钥提交到公开仓库
## 11. 安全与敏感信息检查(强烈建议)
我在配置里看到了疑似明文敏感信息(例如 `application/extra/site.php` 中的 `mail_smtp_pass` 等)。生产部署前请务必:
- 替换为你自己的邮箱/短信/第三方平台配置
- **避免明文写死**,优先改为 `.env` 或服务器环境变量注入
- 检查 `application/extra/site.php``application/database.php`、各插件配置是否包含生产密钥
## 12. 常见问题排查
- **访问首页跳转安装**:检查 `application/admin/command/Install/install.lock` 是否存在;未安装时 `public/index.php` 会跳转到 `install.php`
- **500 / 空白页**:先确认 PHP 版本与扩展;再看 `runtime/log/` 下日志(或 ThinkPHP 日志目录)。
- **上传失败**:检查 `public/uploads` 权限,以及 `application/extra/upload.php` 的限制(大小、后缀)。
- **队列不执行**:确认 Redis 可用、`queue.php``select` 是否一致,且 `php think queue:work --daemon` 常驻。
---
如你希望我把 Supervisor/systemd 的完整配置也写进来,请告诉我:你的生产环境是 **CentOS/Ubuntu/Debian** 哪一种,以及 PHP-FPM 的服务名与运行用户(`www-data`/`nginx`/`www`)。
Executable
+191
View File
@@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "{}" replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same "printed page" as the copyright notice for easier identification within
third-party archives.
Copyright 2017 Karson
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+9
View File
@@ -0,0 +1,9 @@
<?php
/**
* 交易对K线图数据生成,每分钟执行一次
*/
define('WEB_PATH', str_replace('\\', '/', dirname(__FILE__)) .'/');
define('BIND_MODULE','api/new_trade_kline/index');
define('APP_PATH', WEB_PATH . 'application/');
// 加载框架引导文件
require WEB_PATH . 'thinkphp/start.php';
+9
View File
@@ -0,0 +1,9 @@
<?php
/**
* 交易对K线图数据生成,每分钟执行一次
*/
define('WEB_PATH', str_replace('\\', '/', dirname(__FILE__)) .'/');
define('BIND_MODULE','api/new_trade_klines/index');
define('APP_PATH', WEB_PATH . 'application/');
// 加载框架引导文件
require WEB_PATH . 'thinkphp/start.php';
Executable
+97
View File
@@ -0,0 +1,97 @@
# exchange.v5ico_turkey_admin
本仓库为 **FastAdminThinkPHP5** 后端管理系统项目。Web 入口在 `public/`,支持多模块(如 `admin``api``index` 等)。
生产部署请优先阅读:`DEPLOYMENT.md`
## 1. 技术栈
- **框架**ThinkPHP 5.0.x`topthink/framework ~5.0.24`
- **后台框架**FastAdmin
- **数据库**MySQL(配置:`application/database.php`
- **缓存/队列**:Redis(业务配置:`application/config.php``redis`;队列配置:`application/extra/queue.php`
- **队列**`topthink/think-queue 1.1.6`
## 2. 目录结构(关键目录)
- **`public/`**Web 根目录(Nginx/Apache 的 root 必须指向这里)
- **`application/`**:业务代码
- **`application/admin/`**:后台模块
- **`application/api/`**API 模块(含 Cron/Job 等入口)
- **`application/index/`**:前台/首页模块
- **`application/extra/`**:扩展配置(`site.php``upload.php``queue.php` 等)
- **`addons/`**FastAdmin 插件(如 `kefu``translate``qcloudsms` 等)
- **`runtime/`**:运行时目录(日志、缓存等,通常需要可写权限)
- **`think`**:ThinkPHP 控制台入口(命令行)
## 3. 环境配置(.env
根目录存在示例:`.env.sample`。你可以复制为 `.env` 并按实际环境修改。
- **生产环境**:建议 `app.debug=false`
- 数据库连接与表前缀通过 `Env::get('database.xxx', ...)` 读取。
## 4. 初始化安装
### 4.1 命令行安装(推荐)
项目内置安装命令(见 `application/admin/command/Install.php`):
- `php think install --hostname=127.0.0.1 --hostport=3306 --database=YOUR_DB --username=YOUR_USER --password=YOUR_PASS --prefix=fa_`
安装过程中会:
- 导入初始化 SQL`application/admin/command/Install/fastadmin.sql`
- 写入/更新 `application/database.php`
- 写入安装锁:`application/admin/command/Install/install.lock`
-`public/admin.php` 重命名为随机文件名(后台入口更安全)
安装完成后会输出:后台入口、后台账号与密码。
### 4.2 Web 安装入口说明
`public/index.php` 会检测 `install.lock`,未安装时会跳转到 `install.php`
说明:本仓库中未看到 `public/install.php` 文件,实际安装建议使用命令行 `php think install`
## 5. 开发运行(本地)
### 5.1 启动方式
ThinkPHP5 常见运行方式:
- 使用 Nginx/Apache 指向 `public/`
- 或使用 PHP 内置服务(仅开发调试):将 Web 根指向 `public/`
### 5.2 资源压缩(可选)
项目内置资源压缩命令(见 `application/admin/command/Min.php`),会调用 `node` 执行 `r.js`
- `php think min --module=backend --resource=all --optimize=none`
如需使用该命令,请确保服务器/本机已安装 Node.js。
## 6. 队列(Redis
队列配置:`application/extra/queue.php`(默认 `connector=redis`,并使用 Redis 的 `select=5`)。
启动消费者:
- `php think queue:listen`
- `php think queue:work --daemon`
示例队列接口:`application/api/controller/Job.php`(调用 `think\Queue::push(...)`)。
## 7. 定时任务 / K线脚本
- **定时任务入口(API**`application/api/controller/Cron.php`
- **K线生成脚本**(根目录):
- `NewTradeKline.php`
- `NewTradeKlines.php`
- `TradeKline.php`
这些脚本通过 `BIND_MODULE` 绑定到 `application/api` 下对应模块执行,可用于定时/守护进程方式运行。生产部署建议参考 `DEPLOYMENT.md`
## 8. 更多文档
- **生产部署教程**`DEPLOYMENT.md`
Executable
+9
View File
@@ -0,0 +1,9 @@
<?php
/**
* 交易对K线图数据生成,每分钟执行一次
*/
define('WEB_PATH', str_replace('\\', '/', dirname(__FILE__)) .'/');
define('BIND_MODULE','api/trade_kline/index');
define('APP_PATH', WEB_PATH . 'application/');
// 加载框架引导文件
require WEB_PATH . 'thinkphp/start.php';
+1
View File
@@ -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"]}
+59
View File
@@ -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');
}
}
Vendored Executable
+12
View File
@@ -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']
}
});
+58
View File
@@ -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' => '',
],
];
+242
View File
@@ -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
]
]
]
]
]
];
+11
View File
@@ -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
+628
View File
@@ -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=>网站IDedate=>到期时间(永久:0000-00-00
*/
public function setEdate($params)
{
return $this->getData('site?action=setEdate', $params);
}
/**
* 修改网站备注
* @param array $params id=>网站IDps=>备注内容
*/
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;
}
}
+1
View File
@@ -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"]}
+183
View File
@@ -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');
}
}
Vendored Executable
+12
View File
@@ -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'
}
}
});
+130
View File
@@ -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' => '',
],
];
+32
View File
@@ -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();
}
}
+17
View File
@@ -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();
}
}
+10
View File
@@ -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
+3795
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -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 -->
+31
View File
@@ -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 -->
+67
View File
@@ -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 -->
File diff suppressed because one or more lines are too long
+124
View File
@@ -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 &copy; {$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>
+1
View File
File diff suppressed because one or more lines are too long
+297
View File
@@ -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'
]);
}
}
}
Vendored Executable
+80
View File
@@ -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');
});
}
}
+155
View File
@@ -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' => '',
],
];
+24
View File
@@ -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();
}
}
+493
View File
@@ -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');
}
}
}
+14
View File
@@ -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(/&nbsp;/g, '\u00A0').replace(/&lt;/g, '<').replace(/&gt;/g, '>')
.replace(/&amp;/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>
+11
View File
@@ -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()
+119
View File
@@ -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" : "在线客服"
}
}
+24
View File
@@ -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
View File
@@ -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
View File
@@ -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>
File diff suppressed because it is too large Load Diff
+28
View File
@@ -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>
+10
View File
@@ -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
+269
View File
@@ -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;
+1320
View File
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();
}
}
File diff suppressed because it is too large Load Diff
@@ -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
View File
@@ -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.
+46
View File
@@ -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)
![workerman-todpole](http://www.workerman.net/img/workerman-todpole.png)
## [chat room](http://chat.workerman.net/)
[Live demo](http://chat.workerman.net/)
[Source code](https://github.com/walkor/workerman-chat)
![workerman-chat](http://www.workerman.net/img/workerman-chat.png)
+9
View File
@@ -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
View File
@@ -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"
}
+76
View File
@@ -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
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInitc6a5fd2d7f53c16cfcaa25c7be9ade39::getLoader();
+445
View File
@@ -0,0 +1,445 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see http://www.php-fig.org/psr/psr-0/
* @see http://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
// PSR-4
private $prefixLengthsPsr4 = array();
private $prefixDirsPsr4 = array();
private $fallbackDirsPsr4 = array();
// PSR-0
private $prefixesPsr0 = array();
private $fallbackDirsPsr0 = array();
private $useIncludePath = false;
private $classMap = array();
private $classMapAuthoritative = false;
private $missingClasses = array();
private $apcuPrefix;
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', $this->prefixesPsr0);
}
return array();
}
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array $classMap Class to filename map
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*/
public function add($prefix, $paths, $prepend = false)
{
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
(array) $paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
(array) $paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = (array) $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
(array) $paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
(array) $paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
(array) $paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
(array) $paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 base directories
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
/**
* Unregisters this instance as an autoloader.
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return bool|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*/
function includeFile($file)
{
include $file;
}
+21
View File
@@ -0,0 +1,21 @@
Copyright (c) Nils Adermann, Jordi Boggiano
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.
@@ -0,0 +1,9 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
);
@@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
);
+11
View File
@@ -0,0 +1,11 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Workerman\\' => array($vendorDir . '/workerman/workerman'),
'GatewayWorker\\' => array($vendorDir . '/workerman/gateway-worker/src'),
);
+55
View File
@@ -0,0 +1,55 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInitc6a5fd2d7f53c16cfcaa25c7be9ade39
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInitc6a5fd2d7f53c16cfcaa25c7be9ade39', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInitc6a5fd2d7f53c16cfcaa25c7be9ade39', 'loadClassLoader'));
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
if ($useStaticLoader) {
require_once __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInitc6a5fd2d7f53c16cfcaa25c7be9ade39::getInitializer($loader));
} else {
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}
$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
$loader->setPsr4($namespace, $path);
}
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}
}
$loader->register(true);
return $loader;
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInitc6a5fd2d7f53c16cfcaa25c7be9ade39
{
public static $prefixLengthsPsr4 = array (
'W' =>
array (
'Workerman\\' => 10,
),
'G' =>
array (
'GatewayWorker\\' => 14,
),
);
public static $prefixDirsPsr4 = array (
'Workerman\\' =>
array (
0 => __DIR__ . '/..' . '/workerman/workerman',
),
'GatewayWorker\\' =>
array (
0 => __DIR__ . '/..' . '/workerman/gateway-worker/src',
),
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInitc6a5fd2d7f53c16cfcaa25c7be9ade39::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInitc6a5fd2d7f53c16cfcaa25c7be9ade39::$prefixDirsPsr4;
}, null, ClassLoader::class);
}
}
+98
View File
@@ -0,0 +1,98 @@
[
{
"name": "workerman/gateway-worker",
"version": "v3.0.18",
"version_normalized": "3.0.18.0",
"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"
},
"time": "2020-07-15T06:45:01+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"GatewayWorker\\": "./src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"homepage": "http://www.workerman.net",
"keywords": [
"communication",
"distributed"
]
},
{
"name": "workerman/workerman",
"version": "v4.0.10",
"version_normalized": "4.0.10.0",
"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. "
},
"time": "2020-09-15T09:22:45+00:00",
"type": "library",
"installation-source": "dist",
"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"
]
}
]
@@ -0,0 +1 @@
10110
@@ -0,0 +1 @@
21279
@@ -0,0 +1 @@
2563
@@ -0,0 +1 @@
32232
@@ -0,0 +1 @@
3738
@@ -0,0 +1 @@
24153
@@ -0,0 +1,4 @@
.buildpath
.project
.settings
.idea
@@ -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.
@@ -0,0 +1,38 @@
GatewayWorker
=================
GatewayWorker基于[Workerman](https://github.com/walkor/Workerman)开发的一个项目框架,用于快速开发长连接应用,例如app推送服务端、即时IM服务端、游戏服务端、物联网、智能家居等等。
GatewayWorker使用经典的Gateway和Worker进程模型。Gateway进程负责维持客户端连接,并转发客户端的数据给Worker进程处理;Worker进程负责处理实际的业务逻辑,并将结果推送给对应的客户端。Gateway服务和Worker服务可以分开部署在不同的服务器上,实现分布式集群。
GatewayWorker提供非常方便的API,可以全局广播数据、可以向某个群体广播数据、也可以向某个特定客户端推送数据。配合Workerman的定时器,也可以定时推送数据。
快速开始
======
开发者可以从一个简单的demo开始(demo中包含了GatewayWorker内核,以及start_gateway.php start_business.php等启动入口文件)<br>
[点击这里下载demo](http://www.workerman.net/download/GatewayWorker.zip)。<br>
demo说明见源码readme。
手册
=======
http://www.workerman.net/gatewaydoc/
安装内核
=======
只安装GatewayWorker内核文件(不包含start_gateway.php start_businessworker.php等启动入口文件)
```
composer require workerman/gateway-worker
```
使用GatewayWorker开发的项目
=======
## [tadpole](http://kedou.workerman.net/)
[Live demo](http://kedou.workerman.net/)
[Source code](https://github.com/walkor/workerman)
![workerman todpole](http://www.workerman.net/img/workerman-todpole.png)
## [chat room](http://chat.workerman.net/)
[Live demo](http://chat.workerman.net/)
[Source code](https://github.com/walkor/workerman-chat)
![workerman-chat](http://www.workerman.net/img/workerman-chat.png)
@@ -0,0 +1,12 @@
{
"name" : "workerman/gateway-worker",
"keywords": ["distributed","communication"],
"homepage": "http://www.workerman.net",
"license" : "MIT",
"require": {
"workerman/workerman" : ">=3.5.0"
},
"autoload": {
"psr-4": {"GatewayWorker\\": "./src"}
}
}
@@ -0,0 +1,561 @@
<?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
*/
namespace GatewayWorker;
use Workerman\Connection\TcpConnection;
use Workerman\Worker;
use Workerman\Lib\Timer;
use Workerman\Connection\AsyncTcpConnection;
use GatewayWorker\Protocols\GatewayProtocol;
use GatewayWorker\Lib\Context;
/**
*
* BusinessWorker 用于处理Gateway转发来的数据
*
* @author walkor<walkor@workerman.net>
*
*/
class BusinessWorker extends Worker
{
/**
* 保存与 gateway 的连接 connection 对象
*
* @var array
*/
public $gatewayConnections = array();
/**
* 注册中心地址
*
* @var string|array
*/
public $registerAddress = '127.0.0.1:1236';
/**
* 事件处理类,默认是 Event 类
*
* @var string
*/
public $eventHandler = 'Events';
/**
* 业务超时时间,可用来定位程序卡在哪里
*
* @var int
*/
public $processTimeout = 30;
/**
* 业务超时时间,可用来定位程序卡在哪里
*
* @var callable
*/
public $processTimeoutHandler = '\\Workerman\\Worker::log';
/**
* 秘钥
*
* @var string
*/
public $secretKey = '';
/**
* businessWorker进程将消息转发给gateway进程的发送缓冲区大小
*
* @var int
*/
public $sendToGatewayBufferSize = 10240000;
/**
* 保存用户设置的 worker 启动回调
*
* @var callback
*/
protected $_onWorkerStart = null;
/**
* 保存用户设置的 workerReload 回调
*
* @var callback
*/
protected $_onWorkerReload = null;
/**
* 保存用户设置的 workerStop 回调
*
* @var callback
*/
protected $_onWorkerStop= null;
/**
* 到注册中心的连接
*
* @var AsyncTcpConnection
*/
protected $_registerConnection = null;
/**
* 处于连接状态的 gateway 通讯地址
*
* @var array
*/
protected $_connectingGatewayAddresses = array();
/**
* 所有 geteway 内部通讯地址
*
* @var array
*/
protected $_gatewayAddresses = array();
/**
* 等待连接个 gateway 地址
*
* @var array
*/
protected $_waitingConnectGatewayAddresses = array();
/**
* Event::onConnect 回调
*
* @var callback
*/
protected $_eventOnConnect = null;
/**
* Event::onMessage 回调
*
* @var callback
*/
protected $_eventOnMessage = null;
/**
* Event::onClose 回调
*
* @var callback
*/
protected $_eventOnClose = null;
/**
* websocket回调
*
* @var null
*/
protected $_eventOnWebSocketConnect = null;
/**
* SESSION 版本缓存
*
* @var array
*/
protected $_sessionVersion = array();
/**
* 用于保持长连接的心跳时间间隔
*
* @var int
*/
const PERSISTENCE_CONNECTION_PING_INTERVAL = 25;
/**
* 构造函数
*
* @param string $socket_name
* @param array $context_option
*/
public function __construct($socket_name = '', $context_option = array())
{
parent::__construct($socket_name, $context_option);
$backrace = debug_backtrace();
$this->_autoloadRootPath = dirname($backrace[0]['file']);
}
/**
* {@inheritdoc}
*/
public function run()
{
$this->_onWorkerStart = $this->onWorkerStart;
$this->_onWorkerReload = $this->onWorkerReload;
$this->_onWorkerStop = $this->onWorkerStop;
$this->onWorkerStop = array($this, 'onWorkerStop');
$this->onWorkerStart = array($this, 'onWorkerStart');
$this->onWorkerReload = array($this, 'onWorkerReload');
parent::run();
}
/**
* 当进程启动时一些初始化工作
*
* @return void
*/
protected function onWorkerStart()
{
if (!class_exists('\Protocols\GatewayProtocol')) {
class_alias('GatewayWorker\Protocols\GatewayProtocol', 'Protocols\GatewayProtocol');
}
if (!is_array($this->registerAddress)) {
$this->registerAddress = array($this->registerAddress);
}
$this->connectToRegister();
\GatewayWorker\Lib\Gateway::setBusinessWorker($this);
\GatewayWorker\Lib\Gateway::$secretKey = $this->secretKey;
if ($this->_onWorkerStart) {
call_user_func($this->_onWorkerStart, $this);
}
if (is_callable($this->eventHandler . '::onWorkerStart')) {
call_user_func($this->eventHandler . '::onWorkerStart', $this);
}
if (function_exists('pcntl_signal')) {
// 业务超时信号处理
pcntl_signal(SIGALRM, array($this, 'timeoutHandler'), false);
} else {
$this->processTimeout = 0;
}
// 设置回调
if (is_callable($this->eventHandler . '::onConnect')) {
$this->_eventOnConnect = $this->eventHandler . '::onConnect';
}
if (is_callable($this->eventHandler . '::onMessage')) {
$this->_eventOnMessage = $this->eventHandler . '::onMessage';
} else {
echo "Waring: {$this->eventHandler}::onMessage is not callable\n";
}
if (is_callable($this->eventHandler . '::onClose')) {
$this->_eventOnClose = $this->eventHandler . '::onClose';
}
if (is_callable($this->eventHandler . '::onWebSocketConnect')) {
$this->_eventOnWebSocketConnect = $this->eventHandler . '::onWebSocketConnect';
}
}
/**
* onWorkerReload 回调
*
* @param Worker $worker
*/
protected function onWorkerReload($worker)
{
// 防止进程立刻退出
$worker->reloadable = false;
// 延迟 0.05 秒退出,避免 BusinessWorker 瞬间全部退出导致没有可用的 BusinessWorker 进程
Timer::add(0.05, array('Workerman\Worker', 'stopAll'));
// 执行用户定义的 onWorkerReload 回调
if ($this->_onWorkerReload) {
call_user_func($this->_onWorkerReload, $this);
}
}
/**
* 当进程关闭时一些清理工作
*
* @return void
*/
protected function onWorkerStop()
{
if ($this->_onWorkerStop) {
call_user_func($this->_onWorkerStop, $this);
}
if (is_callable($this->eventHandler . '::onWorkerStop')) {
call_user_func($this->eventHandler . '::onWorkerStop', $this);
}
}
/**
* 连接服务注册中心
*
* @return void
*/
public function connectToRegister()
{
foreach ($this->registerAddress as $register_address) {
$register_connection = new AsyncTcpConnection("text://{$register_address}");
$secret_key = $this->secretKey;
$register_connection->onConnect = function () use ($register_connection, $secret_key, $register_address) {
$register_connection->send('{"event":"worker_connect","secret_key":"' . $secret_key . '"}');
// 如果Register服务器不在本地服务器,则需要保持心跳
if (strpos($register_address, '127.0.0.1') !== 0) {
$register_connection->ping_timer = Timer::add(self::PERSISTENCE_CONNECTION_PING_INTERVAL, function () use ($register_connection) {
$register_connection->send('{"event":"ping"}');
});
}
};
$register_connection->onClose = function ($register_connection) {
if(!empty($register_connection->ping_timer)) {
Timer::del($register_connection->ping_timer);
}
$register_connection->reconnect(1);
};
$register_connection->onMessage = array($this, 'onRegisterConnectionMessage');
$register_connection->connect();
}
}
/**
* 当注册中心发来消息时
*
* @return void
*/
public function onRegisterConnectionMessage($register_connection, $data)
{
$data = json_decode($data, true);
if (!isset($data['event'])) {
echo "Received bad data from Register\n";
return;
}
$event = $data['event'];
switch ($event) {
case 'broadcast_addresses':
if (!is_array($data['addresses'])) {
echo "Received bad data from Register. Addresses empty\n";
return;
}
$addresses = $data['addresses'];
$this->_gatewayAddresses = array();
foreach ($addresses as $addr) {
$this->_gatewayAddresses[$addr] = $addr;
}
$this->checkGatewayConnections($addresses);
break;
default:
echo "Receive bad event:$event from Register.\n";
}
}
/**
* 当 gateway 转发来数据时
*
* @param TcpConnection $connection
* @param mixed $data
*/
public function onGatewayMessage($connection, $data)
{
$cmd = $data['cmd'];
if ($cmd === GatewayProtocol::CMD_PING) {
return;
}
// 上下文数据
Context::$client_ip = $data['client_ip'];
Context::$client_port = $data['client_port'];
Context::$local_ip = $data['local_ip'];
Context::$local_port = $data['local_port'];
Context::$connection_id = $data['connection_id'];
Context::$client_id = Context::addressToClientId($data['local_ip'], $data['local_port'],
$data['connection_id']);
// $_SERVER 变量
$_SERVER = array(
'REMOTE_ADDR' => long2ip($data['client_ip']),
'REMOTE_PORT' => $data['client_port'],
'GATEWAY_ADDR' => long2ip($data['local_ip']),
'GATEWAY_PORT' => $data['gateway_port'],
'GATEWAY_CLIENT_ID' => Context::$client_id,
);
// 检查session版本,如果是过期的session数据则拉取最新的数据
if ($cmd !== GatewayProtocol::CMD_ON_CLOSE && isset($this->_sessionVersion[Context::$client_id]) && $this->_sessionVersion[Context::$client_id] !== crc32($data['ext_data'])) {
$_SESSION = Context::$old_session = \GatewayWorker\Lib\Gateway::getSession(Context::$client_id);
$this->_sessionVersion[Context::$client_id] = crc32($data['ext_data']);
} else {
if (!isset($this->_sessionVersion[Context::$client_id])) {
$this->_sessionVersion[Context::$client_id] = crc32($data['ext_data']);
}
// 尝试解析 session
if ($data['ext_data'] != '') {
Context::$old_session = $_SESSION = Context::sessionDecode($data['ext_data']);
} else {
Context::$old_session = $_SESSION = null;
}
}
if ($this->processTimeout) {
pcntl_alarm($this->processTimeout);
}
// 尝试执行 Event::onConnection、Event::onMessage、Event::onClose
switch ($cmd) {
case GatewayProtocol::CMD_ON_CONNECT:
if ($this->_eventOnConnect) {
call_user_func($this->_eventOnConnect, Context::$client_id);
}
break;
case GatewayProtocol::CMD_ON_MESSAGE:
if ($this->_eventOnMessage) {
call_user_func($this->_eventOnMessage, Context::$client_id, $data['body']);
}
break;
case GatewayProtocol::CMD_ON_CLOSE:
unset($this->_sessionVersion[Context::$client_id]);
if ($this->_eventOnClose) {
call_user_func($this->_eventOnClose, Context::$client_id);
}
break;
case GatewayProtocol::CMD_ON_WEBSOCKET_CONNECT:
if ($this->_eventOnWebSocketConnect) {
call_user_func($this->_eventOnWebSocketConnect, Context::$client_id, $data['body']);
}
break;
}
if ($this->processTimeout) {
pcntl_alarm(0);
}
// session 必须是数组
if ($_SESSION !== null && !is_array($_SESSION)) {
throw new \Exception('$_SESSION must be an array. But $_SESSION=' . var_export($_SESSION, true) . ' is not array.');
}
// 判断 session 是否被更改
if ($_SESSION !== Context::$old_session && $cmd !== GatewayProtocol::CMD_ON_CLOSE) {
$session_str_now = $_SESSION !== null ? Context::sessionEncode($_SESSION) : '';
\GatewayWorker\Lib\Gateway::setSocketSession(Context::$client_id, $session_str_now);
$this->_sessionVersion[Context::$client_id] = crc32($session_str_now);
}
Context::clear();
}
/**
* 当与 Gateway 的连接断开时触发
*
* @param TcpConnection $connection
* @return void
*/
public function onGatewayClose($connection)
{
$addr = $connection->remoteAddress;
unset($this->gatewayConnections[$addr], $this->_connectingGatewayAddresses[$addr]);
if (isset($this->_gatewayAddresses[$addr]) && !isset($this->_waitingConnectGatewayAddresses[$addr])) {
Timer::add(1, array($this, 'tryToConnectGateway'), array($addr), false);
$this->_waitingConnectGatewayAddresses[$addr] = $addr;
}
}
/**
* 尝试连接 Gateway 内部通讯地址
*
* @param string $addr
*/
public function tryToConnectGateway($addr)
{
if (!isset($this->gatewayConnections[$addr]) && !isset($this->_connectingGatewayAddresses[$addr]) && isset($this->_gatewayAddresses[$addr])) {
$gateway_connection = new AsyncTcpConnection("GatewayProtocol://$addr");
$gateway_connection->remoteAddress = $addr;
$gateway_connection->onConnect = array($this, 'onConnectGateway');
$gateway_connection->onMessage = array($this, 'onGatewayMessage');
$gateway_connection->onClose = array($this, 'onGatewayClose');
$gateway_connection->onError = array($this, 'onGatewayError');
$gateway_connection->maxSendBufferSize = $this->sendToGatewayBufferSize;
if (TcpConnection::$defaultMaxSendBufferSize == $gateway_connection->maxSendBufferSize) {
$gateway_connection->maxSendBufferSize = 50 * 1024 * 1024;
}
$gateway_data = GatewayProtocol::$empty;
$gateway_data['cmd'] = GatewayProtocol::CMD_WORKER_CONNECT;
$gateway_data['body'] = json_encode(array(
'worker_key' =>"{$this->name}:{$this->id}",
'secret_key' => $this->secretKey,
));
$gateway_connection->send($gateway_data);
$gateway_connection->connect();
$this->_connectingGatewayAddresses[$addr] = $addr;
}
unset($this->_waitingConnectGatewayAddresses[$addr]);
}
/**
* 检查 gateway 的通信端口是否都已经连
* 如果有未连接的端口,则尝试连接
*
* @param array $addresses_list
*/
public function checkGatewayConnections($addresses_list)
{
if (empty($addresses_list)) {
return;
}
foreach ($addresses_list as $addr) {
if (!isset($this->_waitingConnectGatewayAddresses[$addr])) {
$this->tryToConnectGateway($addr);
}
}
}
/**
* 当连接上 gateway 的通讯端口时触发
* 将连接 connection 对象保存起来
*
* @param TcpConnection $connection
* @return void
*/
public function onConnectGateway($connection)
{
$this->gatewayConnections[$connection->remoteAddress] = $connection;
unset($this->_connectingGatewayAddresses[$connection->remoteAddress], $this->_waitingConnectGatewayAddresses[$connection->remoteAddress]);
}
/**
* 当与 gateway 的连接出现错误时触发
*
* @param TcpConnection $connection
* @param int $error_no
* @param string $error_msg
*/
public function onGatewayError($connection, $error_no, $error_msg)
{
echo "GatewayConnection Error : $error_no ,$error_msg\n";
}
/**
* 获取所有 Gateway 内部通讯地址
*
* @return array
*/
public function getAllGatewayAddresses()
{
return $this->_gatewayAddresses;
}
/**
* 业务超时回调
*
* @param int $signal
* @throws \Exception
*/
public function timeoutHandler($signal)
{
switch ($signal) {
// 超时时钟
case SIGALRM:
// 超时异常
$e = new \Exception("process_timeout", 506);
$trace_str = $e->getTraceAsString();
// 去掉第一行timeoutHandler的调用栈
$trace_str = $e->getMessage() . ":\n" . substr($trace_str, strpos($trace_str, "\n") + 1) . "\n";
// 开发者没有设置超时处理函数,或者超时处理函数返回空则执行退出
if (!$this->processTimeoutHandler || !call_user_func($this->processTimeoutHandler, $trace_str, $e)) {
Worker::stopAll();
}
break;
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,136 @@
<?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
*/
namespace GatewayWorker\Lib;
use Exception;
/**
* 上下文 包含当前用户 uid 内部通信 local_ip local_port socket_id,以及客户端 client_ip client_port
*/
class Context
{
/**
* 内部通讯 id
*
* @var string
*/
public static $local_ip;
/**
* 内部通讯端口
*
* @var int
*/
public static $local_port;
/**
* 客户端 ip
*
* @var string
*/
public static $client_ip;
/**
* 客户端端口
*
* @var int
*/
public static $client_port;
/**
* client_id
*
* @var string
*/
public static $client_id;
/**
* 连接 connection->id
*
* @var int
*/
public static $connection_id;
/**
* 旧的session
*
* @var string
*/
public static $old_session;
/**
* 编码 session
*
* @param mixed $session_data
* @return string
*/
public static function sessionEncode($session_data = '')
{
if ($session_data !== '') {
return serialize($session_data);
}
return '';
}
/**
* 解码 session
*
* @param string $session_buffer
* @return mixed
*/
public static function sessionDecode($session_buffer)
{
return unserialize($session_buffer);
}
/**
* 清除上下文
*
* @return void
*/
public static function clear()
{
self::$local_ip = self::$local_port = self::$client_ip = self::$client_port =
self::$client_id = self::$connection_id = self::$old_session = null;
}
/**
* 通讯地址到 client_id 的转换
*
* @param int $local_ip
* @param int $local_port
* @param int $connection_id
* @return string
*/
public static function addressToClientId($local_ip, $local_port, $connection_id)
{
return bin2hex(pack('NnN', $local_ip, $local_port, $connection_id));
}
/**
* client_id 到通讯地址的转换
*
* @param string $client_id
* @return array
* @throws Exception
*/
public static function clientIdToAddress($client_id)
{
if (strlen($client_id) !== 20) {
echo new Exception("client_id $client_id is invalid");
return false;
}
return unpack('Nlocal_ip/nlocal_port/Nconnection_id', pack('H*', $client_id));
}
}
@@ -0,0 +1,76 @@
<?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
*/
namespace GatewayWorker\Lib;
use Config\Db as DbConfig;
use Exception;
/**
* 数据库类
*/
class Db
{
/**
* 实例数组
*
* @var array
*/
protected static $instance = array();
/**
* 获取实例
*
* @param string $config_name
* @return DbConnection
* @throws Exception
*/
public static function instance($config_name)
{
if (!isset(DbConfig::$$config_name)) {
echo "\\Config\\Db::$config_name not set\n";
throw new Exception("\\Config\\Db::$config_name not set\n");
}
if (empty(self::$instance[$config_name])) {
$config = DbConfig::$$config_name;
self::$instance[$config_name] = new DbConnection($config['host'], $config['port'],
$config['user'], $config['password'], $config['dbname'],$config['charset']);
}
return self::$instance[$config_name];
}
/**
* 关闭数据库实例
*
* @param string $config_name
*/
public static function close($config_name)
{
if (isset(self::$instance[$config_name])) {
self::$instance[$config_name]->closeConnection();
self::$instance[$config_name] = null;
}
}
/**
* 关闭所有数据库实例
*/
public static function closeAll()
{
foreach (self::$instance as $connection) {
$connection->closeConnection();
}
self::$instance = array();
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,216 @@
<?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
*/
namespace GatewayWorker\Protocols;
/**
* Gateway 与 Worker 间通讯的二进制协议
*
* struct GatewayProtocol
* {
* unsigned int pack_len,
* unsigned char cmd,//命令字
* unsigned int local_ip,
* unsigned short local_port,
* unsigned int client_ip,
* unsigned short client_port,
* unsigned int connection_id,
* unsigned char flag,
* unsigned short gateway_port,
* unsigned int ext_len,
* char[ext_len] ext_data,
* char[pack_length-HEAD_LEN] body//包体
* }
* NCNnNnNCnN
*/
class GatewayProtocol
{
// 发给workergateway有一个新的连接
const CMD_ON_CONNECT = 1;
// 发给worker的,客户端有消息
const CMD_ON_MESSAGE = 3;
// 发给worker上的关闭链接事件
const CMD_ON_CLOSE = 4;
// 发给gateway的向单个用户发送数据
const CMD_SEND_TO_ONE = 5;
// 发给gateway的向所有用户发送数据
const CMD_SEND_TO_ALL = 6;
// 发给gateway的踢出用户
// 1、如果有待发消息,将在发送完后立即销毁用户连接
// 2、如果无待发消息,将立即销毁用户连接
const CMD_KICK = 7;
// 发给gateway的立即销毁用户连接
const CMD_DESTROY = 8;
// 发给gateway,通知用户session更新
const CMD_UPDATE_SESSION = 9;
// 获取在线状态
const CMD_GET_ALL_CLIENT_SESSIONS = 10;
// 判断是否在线
const CMD_IS_ONLINE = 11;
// client_id绑定到uid
const CMD_BIND_UID = 12;
// 解绑
const CMD_UNBIND_UID = 13;
// 向uid发送数据
const CMD_SEND_TO_UID = 14;
// 根据uid获取绑定的clientid
const CMD_GET_CLIENT_ID_BY_UID = 15;
// 加入组
const CMD_JOIN_GROUP = 20;
// 离开组
const CMD_LEAVE_GROUP = 21;
// 向组成员发消息
const CMD_SEND_TO_GROUP = 22;
// 获取组成员
const CMD_GET_CLIENT_SESSIONS_BY_GROUP = 23;
// 获取组在线连接数
const CMD_GET_CLIENT_COUNT_BY_GROUP = 24;
// 按照条件查找
const CMD_SELECT = 25;
// 获取在线的群组ID
const CMD_GET_GROUP_ID_LIST = 26;
// 取消分组
const CMD_UNGROUP = 27;
// worker连接gateway事件
const CMD_WORKER_CONNECT = 200;
// 心跳
const CMD_PING = 201;
// GatewayClient连接gateway事件
const CMD_GATEWAY_CLIENT_CONNECT = 202;
// 根据client_id获取session
const CMD_GET_SESSION_BY_CLIENT_ID = 203;
// 发给gateway,覆盖session
const CMD_SET_SESSION = 204;
// 当websocket握手时触发,只有websocket协议支持此命令字
const CMD_ON_WEBSOCKET_CONNECT = 205;
// 包体是标量
const FLAG_BODY_IS_SCALAR = 0x01;
// 通知gateway在send时不调用协议encode方法,在广播组播时提升性能
const FLAG_NOT_CALL_ENCODE = 0x02;
/**
* 包头长度
*
* @var int
*/
const HEAD_LEN = 28;
public static $empty = array(
'cmd' => 0,
'local_ip' => 0,
'local_port' => 0,
'client_ip' => 0,
'client_port' => 0,
'connection_id' => 0,
'flag' => 0,
'gateway_port' => 0,
'ext_data' => '',
'body' => '',
);
/**
* 返回包长度
*
* @param string $buffer
* @return int return current package length
*/
public static function input($buffer)
{
if (strlen($buffer) < self::HEAD_LEN) {
return 0;
}
$data = unpack("Npack_len", $buffer);
return $data['pack_len'];
}
/**
* 获取整个包的 buffer
*
* @param mixed $data
* @return string
*/
public static function encode($data)
{
$flag = (int)is_scalar($data['body']);
if (!$flag) {
$data['body'] = serialize($data['body']);
}
$data['flag'] |= $flag;
$ext_len = strlen($data['ext_data']);
$package_len = self::HEAD_LEN + $ext_len + strlen($data['body']);
return pack("NCNnNnNCnN", $package_len,
$data['cmd'], $data['local_ip'],
$data['local_port'], $data['client_ip'],
$data['client_port'], $data['connection_id'],
$data['flag'], $data['gateway_port'],
$ext_len) . $data['ext_data'] . $data['body'];
}
/**
* 从二进制数据转换为数组
*
* @param string $buffer
* @return array
*/
public static function decode($buffer)
{
$data = unpack("Npack_len/Ccmd/Nlocal_ip/nlocal_port/Nclient_ip/nclient_port/Nconnection_id/Cflag/ngateway_port/Next_len",
$buffer);
if ($data['ext_len'] > 0) {
$data['ext_data'] = substr($buffer, self::HEAD_LEN, $data['ext_len']);
if ($data['flag'] & self::FLAG_BODY_IS_SCALAR) {
$data['body'] = substr($buffer, self::HEAD_LEN + $data['ext_len']);
} else {
$data['body'] = unserialize(substr($buffer, self::HEAD_LEN + $data['ext_len']));
}
} else {
$data['ext_data'] = '';
if ($data['flag'] & self::FLAG_BODY_IS_SCALAR) {
$data['body'] = substr($buffer, self::HEAD_LEN);
} else {
$data['body'] = unserialize(substr($buffer, self::HEAD_LEN));
}
}
return $data;
}
}
@@ -0,0 +1,190 @@
<?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
*/
namespace GatewayWorker;
use Workerman\Worker;
use Workerman\Lib\Timer;
/**
*
* 注册中心,用于注册 Gateway 和 BusinessWorker
*
* @author walkor<walkor@workerman.net>
*
*/
class Register extends Worker
{
/**
* {@inheritdoc}
*/
public $name = 'Register';
/**
* {@inheritdoc}
*/
public $reloadable = false;
/**
* 秘钥
* @var string
*/
public $secretKey = '';
/**
* 所有 gateway 的连接
*
* @var array
*/
protected $_gatewayConnections = array();
/**
* 所有 worker 的连接
*
* @var array
*/
protected $_workerConnections = array();
/**
* 进程启动时间
*
* @var int
*/
protected $_startTime = 0;
/**
* {@inheritdoc}
*/
public function run()
{
// 设置 onMessage 连接回调
$this->onConnect = array($this, 'onConnect');
// 设置 onMessage 回调
$this->onMessage = array($this, 'onMessage');
// 设置 onClose 回调
$this->onClose = array($this, 'onClose');
// 记录进程启动的时间
$this->_startTime = time();
// 强制使用text协议
$this->protocol = '\Workerman\Protocols\Text';
// 运行父方法
parent::run();
}
/**
* 设置个定时器,将未及时发送验证的连接关闭
*
* @param \Workerman\Connection\ConnectionInterface $connection
* @return void
*/
public function onConnect($connection)
{
$connection->timeout_timerid = Timer::add(10, function () use ($connection) {
Worker::log("Register auth timeout (".$connection->getRemoteIp()."). See http://doc2.workerman.net/register-auth-timeout.html");
$connection->close();
}, null, false);
}
/**
* 设置消息回调
*
* @param \Workerman\Connection\ConnectionInterface $connection
* @param string $buffer
* @return void
*/
public function onMessage($connection, $buffer)
{
// 删除定时器
Timer::del($connection->timeout_timerid);
$data = @json_decode($buffer, true);
if (empty($data['event'])) {
$error = "Bad request for Register service. Request info(IP:".$connection->getRemoteIp().", Request Buffer:$buffer). See http://doc2.workerman.net/register-auth-timeout.html";
Worker::log($error);
return $connection->close($error);
}
$event = $data['event'];
$secret_key = isset($data['secret_key']) ? $data['secret_key'] : '';
// 开始验证
switch ($event) {
// 是 gateway 连接
case 'gateway_connect':
if (empty($data['address'])) {
echo "address not found\n";
return $connection->close();
}
if ($secret_key !== $this->secretKey) {
Worker::log("Register: Key does not match ".var_export($secret_key, true)." !== ".var_export($this->secretKey, true));
return $connection->close();
}
$this->_gatewayConnections[$connection->id] = $data['address'];
$this->broadcastAddresses();
break;
// 是 worker 连接
case 'worker_connect':
if ($secret_key !== $this->secretKey) {
Worker::log("Register: Key does not match ".var_export($secret_key, true)." !== ".var_export($this->secretKey, true));
return $connection->close();
}
$this->_workerConnections[$connection->id] = $connection;
$this->broadcastAddresses($connection);
break;
case 'ping':
break;
default:
Worker::log("Register unknown event:$event IP: ".$connection->getRemoteIp()." Buffer:$buffer. See http://doc2.workerman.net/register-auth-timeout.html");
$connection->close();
}
}
/**
* 连接关闭时
*
* @param \Workerman\Connection\ConnectionInterface $connection
*/
public function onClose($connection)
{
if (isset($this->_gatewayConnections[$connection->id])) {
unset($this->_gatewayConnections[$connection->id]);
$this->broadcastAddresses();
}
if (isset($this->_workerConnections[$connection->id])) {
unset($this->_workerConnections[$connection->id]);
}
}
/**
* 向 BusinessWorker 广播 gateway 内部通讯地址
*
* @param \Workerman\Connection\ConnectionInterface $connection
*/
public function broadcastAddresses($connection = null)
{
$data = array(
'event' => 'broadcast_addresses',
'addresses' => array_unique(array_values($this->_gatewayConnections)),
);
$buffer = json_encode($data);
if ($connection) {
$connection->send($buffer);
return;
}
foreach ($this->_workerConnections as $con) {
$con->send($buffer);
}
}
}
@@ -0,0 +1,6 @@
logs
.buildpath
.project
.settings
.idea
.DS_Store

Some files were not shown because too many files have changed in this diff Show More