feat: 后端全量更新 - 含所有本次需求

- Contract.php: 返回合约账户余额(balance_contract)
- My.php: 地址管理增加BTC/ETH
- AppContract.php: 一键平仓(closeall)
- AppProxy.php: 代理专属注册链接 + 分级权限(L1/L2)
- site.php: 手续费减半(0.018→0.009)
- agent_permission_setup.sql: 代理权限SQL
- crypto_news_crawler.py: 新闻自动采集脚本
This commit is contained in:
li
2026-03-30 20:16:32 +08:00
commit 1b24994e74
6721 changed files with 1308571 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
+5
View File
@@ -0,0 +1,5 @@
runtime/
vendor/
.DS_Store
*.log
.env
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
+78
View File
@@ -0,0 +1,78 @@
<?php
namespace addons\kefu\library;
class StrComparison
{
protected $str1;
protected $str2;
protected $c = [];
/*
* 返回串一和串二的最长公共子序列
*/
public function getLCS($str1, $str2, $len1 = 0, $len2 = 0)
{
$this->str1 = $str1;
$this->str2 = $str2;
if ($len1 == 0) {
$len1 = strlen($str1);
}
if ($len2 == 0) {
$len2 = strlen($str2);
}
$this->initC($len1, $len2);
return $this->printLCS($this->c, $len1 - 1, $len2 - 1);
}
/*
* 返回两个串的相似度
*/
public function getSimilar($str1, $str2)
{
$len1 = strlen($str1);
$len2 = strlen($str2);
$len = strlen($this->getLCS($str1, $str2, $len1, $len2));
$similar = $len * 2 / ($len1 + $len2);
return round($similar * 100, 2);
}
public function initC($len1, $len2)
{
for ($i = 0; $i < $len1; $i++) {
$this->c[$i][0] = 0;
}
for ($j = 0; $j < $len2; $j++) {
$this->c[0][$j] = 0;
}
for ($i = 1; $i < $len1; $i++) {
for ($j = 1; $j < $len2; $j++) {
if ($this->str1[$i] == $this->str2[$j]) {
$this->c[$i][$j] = $this->c[$i - 1][$j - 1] + 1;
} elseif ($this->c[$i - 1][$j] >= $this->c[$i][$j - 1]) {
$this->c[$i][$j] = $this->c[$i - 1][$j];
} else {
$this->c[$i][$j] = $this->c[$i][$j - 1];
}
}
}
}
public function printLCS($c, $i, $j)
{
if ($i == 0 || $j == 0) {
if ($this->str1[$i] == $this->str2[$j]) {
return $this->str2[$j];
} else {
return "";
}
}
if ($this->str1[$i] == $this->str2[$j]) {
return $this->printLCS($this->c, $i - 1, $j - 1) . $this->str2[$j];
} elseif ($this->c[$i - 1][$j] >= $this->c[$i][$j - 1]) {
return $this->printLCS($this->c, $i - 1, $j);
} else {
return $this->printLCS($this->c, $i, $j - 1);
}
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace addons\kefu\library\WechatCrypto;
/**
* error code 说明.
* <ul>
* <li>-40001: 签名验证错误</li>
* <li>-40002: xml解析失败</li>
* <li>-40003: sha加密生成签名失败</li>
* <li>-40004: encodingAesKey 非法</li>
* <li>-40005: appid 校验错误</li>
* <li>-40006: aes 加密失败</li>
* <li>-40007: aes 解密失败</li>
* <li>-40008: 解密后得到的buffer非法</li>
* <li>-40009: base64加密失败</li>
* <li>-40010: base64解密失败</li>
* <li>-40011: 生成xml失败</li>
* </ul>
*/
class ErrorCode
{
public static $OK = 0;
public static $ValidateSignatureError = -40001;
public static $ParseXmlError = -40002;
public static $ComputeSignatureError = -40003;
public static $IllegalAesKey = -40004;
public static $ValidateAppidError = -40005;
public static $EncryptAESError = -40006;
public static $DecryptAESError = -40007;
public static $IllegalBuffer = -40008;
public static $EncodeBase64Error = -40009;
public static $DecodeBase64Error = -40010;
public static $GenReturnXmlError = -40011;
}
?>
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace addons\kefu\library\WechatCrypto;
use addons\kefu\library\WechatCrypto\ErrorCode;
/**
* PKCS7Encoder class
*
* 提供基于PKCS7算法的加解密接口.
*/
class PKCS7Encoder
{
public static $block_size = 32;
/**
* 对需要加密的明文进行填充补位
* @param $text 需要进行填充补位操作的明文
* @return 补齐明文字符串
*/
function encode($text)
{
$block_size = PKCS7Encoder::$block_size;
$text_length = strlen($text);
//计算需要填充的位数
$amount_to_pad = PKCS7Encoder::$block_size - ($text_length % PKCS7Encoder::$block_size);
if ($amount_to_pad == 0) {
$amount_to_pad = PKCS7Encoder::block_size;
}
//获得补位所用的字符
$pad_chr = chr($amount_to_pad);
$tmp = "";
for ($index = 0; $index < $amount_to_pad; $index++) {
$tmp .= $pad_chr;
}
return $text . $tmp;
}
/**
* 对解密后的明文进行补位删除
* @param decrypted 解密后的明文
* @return 删除填充补位后的明文
*/
function decode($text)
{
$pad = ord(substr($text, -1));
if ($pad < 1 || $pad > 32) {
$pad = 0;
}
return substr($text, 0, (strlen($text) - $pad));
}
}
?>
+108
View File
@@ -0,0 +1,108 @@
<?php
namespace addons\kefu\library\WechatCrypto;
use addons\kefu\library\WechatCrypto\ErrorCode;
use addons\kefu\library\WechatCrypto\PKCS7Encoder;
/**
* Prpcrypt class
*
* 提供接收和推送给公众平台消息的加解密接口.
*/
class Prpcrypt
{
public $key;
function __construct($k)
{
$this->key = base64_decode($k . "=");
}
/**
* 对明文进行加密
* @param string $text 需要加密的明文
* @return string 加密后的密文
*/
public function encrypt($text, $appid)
{
try {
//获得16位随机字符串,填充到明文之前
$random = $this->getRandomStr();
$text = $random . pack("N", strlen($text)) . $text . $appid;
$iv = substr($this->key, 0, 16);
$pkc_encoder = new PKCS7Encoder;
$text = $pkc_encoder->encode($text);
$encrypted = openssl_encrypt($text, 'AES-256-CBC', $this->key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv);
//print(base64_encode($encrypted));
//使用BASE64对加密后的字符串进行编码
return [ErrorCode::$OK, base64_encode($encrypted)];
} catch (Exception $e) {
//print $e;
return [ErrorCode::$EncryptAESError, null];
}
}
/**
* 随机生成16位字符串
* @return string 生成的字符串
*/
function getRandomStr()
{
$str = "";
$str_pol = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";
$max = strlen($str_pol) - 1;
for ($i = 0; $i < 16; $i++) {
$str .= $str_pol[mt_rand(0, $max)];
}
return $str;
}
/**
* 对密文进行解密
* @param string $encrypted 需要解密的密文
* @return string 解密得到的明文
*/
public function decrypt($encrypted, $appid)
{
try {
//使用BASE64对需要解密的字符串进行解码
$ciphertext_dec = base64_decode($encrypted);
$iv = substr($this->key, 0, 16);
$decrypted = openssl_decrypt($ciphertext_dec, 'AES-256-CBC', $this->key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv);
} catch (Exception $e) {
return [ErrorCode::$DecryptAESError, null];
}
try {
//去除补位字符
$pkc_encoder = new PKCS7Encoder;
$result = $pkc_encoder->decode($decrypted);
//去除16位随机字符串,网络字节序和AppId
if (strlen($result) < 16) {
return "";
}
$content = substr($result, 16, strlen($result));
$len_list = unpack("N", substr($content, 0, 4));
$xml_len = $len_list[1];
$xml_content = substr($content, 4, $xml_len);
$from_appid = substr($content, $xml_len + 4);
} catch (Exception $e) {
//print $e;
return [ErrorCode::$IllegalBuffer, null];
}
if ($from_appid != $appid) {
return [ErrorCode::$ValidateAppidError, null];
}
return [0, $xml_content];
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace addons\kefu\library\WechatCrypto;
use addons\kefu\library\WechatCrypto\ErrorCode;
/**
* SHA1 class
*
* 计算公众平台的消息签名接口.
*/
class SHA1
{
/**
* 用SHA1算法生成安全签名
* @param string $token 票据
* @param string $timestamp 时间戳
* @param string $nonce 随机字符串
* @param string $encrypt 密文消息
*/
public function getSHA1($token, $timestamp, $nonce, $encrypt_msg)
{
//排序
try {
$array = [$encrypt_msg, $token, $timestamp, $nonce];
sort($array, SORT_STRING);
$str = implode($array);
return [ErrorCode::$OK, sha1($str)];
} catch (Exception $e) {
//print $e . "\n";
return [ErrorCode::$ComputeSignatureError, null];
}
}
}
?>
+323
View File
@@ -0,0 +1,323 @@
<?php
/**
* 对公众平台发送给公众账号的消息加解密代码.
* 白衣素袖:增加消息处理辅助方法
* @copyright Copyright (c) 1998-2014 Tencent Inc.
*/
namespace addons\kefu\library\WechatCrypto;
use think\Db;
use EasyWeChat\Factory;
use GatewayWorker\Lib\Gateway;
use addons\kefu\library\Common;
use addons\kefu\library\WechatCrypto\SHA1;
use addons\kefu\library\WechatCrypto\ErrorCode;
use addons\kefu\library\WechatCrypto\XMLParse;
use addons\kefu\library\WechatCrypto\Prpcrypt;
/**
* 1.第三方回复加密消息给公众平台;
* 2.第三方收到公众平台发送的消息,验证消息的安全性,并对消息进行解密。
*/
class WXBizMsgCrypt
{
// 微信配置
private $wechat = [];
// EasyWeChat APP
public $wechatapp = null;
/**
* 构造函数
*/
public function __construct()
{
$wechat_temp = Db::name('kefu_config')
->whereIn('name', 'wechat_app_id,wechat_app_secret,wechat_token,wechat_encodingkey')
->select();
foreach ($wechat_temp as $key => $value) {
$this->wechat[$value['name']] = $value['value'];
}
// 初始化EasyWeChat
$config = [
'app_id' => $this->wechat['wechat_app_id'],
'secret' => $this->wechat['wechat_app_secret'],
'token' => $this->wechat['wechat_token'],
'aes_key' => $this->wechat['wechat_encodingkey'],
/*'log' => [
'level' => 'debug',
'file' => RUNTIME_PATH . 'log/kefu_wechat.log',
],*/
];
$this->wechatapp = Factory::miniProgram($config);
}
/**
* 将公众平台回复用户的消息加密打包.
* <ol>
* <li>对要发送的消息进行AES-CBC加密</li>
* <li>生成安全签名</li>
* <li>将消息密文和安全签名打包成xml格式</li>
* </ol>
*
* @param $replyMsg string 公众平台待回复用户的消息,xml格式的字符串
* @param $timeStamp string 时间戳,可以自己生成,也可以用URL参数的timestamp
* @param $nonce string 随机串,可以自己生成,也可以用URL参数的nonce
* @param &$encryptMsg string 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串,
* 当return返回0时有效
*
* @return int 成功0,失败返回对应的错误码
*/
public function encryptMsg($replyMsg, $timeStamp, $nonce, &$encryptMsg)
{
$pc = new Prpcrypt($this->wechat['wechat_encodingkey']);
//加密
$array = $pc->encrypt($replyMsg, $this->wechat['wechat_app_id']);
$ret = $array[0];
if ($ret != 0) {
return $ret;
}
if ($timeStamp == null) {
$timeStamp = time();
}
$encrypt = $array[1];
//生成安全签名
$sha1 = new SHA1;
$array = $sha1->getSHA1($this->wechat['wechat_token'], $timeStamp, $nonce, $encrypt);
$ret = $array[0];
if ($ret != 0) {
return $ret;
}
$signature = $array[1];
//生成发送的xml
$xmlparse = new XMLParse;
$encryptMsg = $xmlparse->generate($encrypt, $signature, $timeStamp, $nonce);
return ErrorCode::$OK;
}
/**
* 检验消息的真实性,并且获取解密后的明文.
* <ol>
* <li>利用收到的密文生成安全签名,进行签名验证</li>
* <li>若验证通过,则提取xml中的加密消息</li>
* <li>对消息进行解密</li>
* </ol>
*
* @param $msgSignature string 签名串,对应URL参数的msg_signature
* @param $timestamp string 时间戳 对应URL参数的timestamp
* @param $nonce string 随机串,对应URL参数的nonce
* @param $postData string 密文,对应POST请求的数据
* @param &$msg string 解密后的原文,当return返回0时有效
*
* @return int 成功0,失败返回对应的错误码
*/
public function decryptMsg($msgSignature, $timestamp = null, $nonce, $encrypt, &$msg)
{
if (strlen($this->wechat['wechat_encodingkey']) != 43) {
return ErrorCode::$IllegalAesKey;
}
$pc = new Prpcrypt($this->wechat['wechat_encodingkey']);
if ($timestamp == null) {
$timestamp = time();
}
//验证安全签名
$sha1 = new SHA1;
$array = $sha1->getSHA1($this->wechat['wechat_token'], $timestamp, $nonce, $encrypt);
$ret = $array[0];
if ($ret != 0) {
return $ret;
}
$signature = $array[1];
if ($signature != $msgSignature) {
return ErrorCode::$ValidateSignatureError;
}
$result = $pc->decrypt($encrypt, $this->wechat['wechat_app_id']);
if ($result[0] != 0) {
return $result[0];
}
$msg = $result[1];
return ErrorCode::$OK;
}
/**
* 保存用户发送的图片至服务器
* @param [type] $media_id 临时素材ID
* @param string $save_dir 保存目录
* @return string 文件完整路径
*/
public function saveImg($media_id, $save_dir = './uploads/')
{
$stream = $this->wechatapp->media->get($media_id);
if ($stream instanceof \EasyWeChat\Kernel\Http\StreamResponse) {
$save_dir = $save_dir . date('Ymd') . '/';
if (!is_dir($save_dir)) {
if (!mkdir($save_dir, 0777, true)) {
return false;
}
}
$filename = $stream->save($save_dir);
return $save_dir . $filename;
} else {
return false;
}
}
/**
* 发送消息至小程序用户
* @param [type] $message 文本或消息对象
* @param [type] $openid [description]
* @param [type] $sender 带标识的发送人
* @return [type] [description]
*/
public function sendMessage($message, $openid, $sender = false)
{
$res = $this->wechatapp->customer_service->message($message)->to($openid)->send();
switch ($res['errcode']) {
case '-1':
$msg = '发送失败,系统繁忙,请重试!';
break;
case '40001':
$msg = '发送失败,AppSecret错误!';
break;
case '40002':
$msg = '发送失败,凭证不合法!';
break;
case '40003':
$msg = '发送失败,openid不合法!';
break;
case '45015':
$msg = '发送失败,回复时间超限制!';
break;
case '45047':
$msg = '发送条数超限,用户将不会收到此消息!';
break;
case '48001':
$msg = '请确保小程序已获取发送客服消息功能的API权限!';
break;
default:
$msg = '';
break;
}
// 只在workerman环境(客服发送消息时),进行提示
if (class_exists('\GatewayWorker\Lib\Gateway') && $msg && $sender) {
Gateway::sendToUid($sender, json_encode([
'code' => 0,
'msgtype' => 'show_msg',
'msg' => $msg,
]));
}
return true;
}
/**
* 建立用户->分配客服
* @param [type] $open_id [description]
* @return [type] [description]
*/
public function userInitialize($open_id)
{
$kefu_user = Db::name('kefu_user')->where('wechat_openid', $open_id)->find();
if (!$kefu_user) {
// 建立用户
$tourists_max_id = Db::name('kefu_user')->max('id');
$kefu_user = [
'avatar' => '', // 随机头像->算了算了
'nickname' => '小程序用户 ' . $tourists_max_id,
'wechat_openid' => $open_id,
'createtime' => time(),
];
if (Db::name('kefu_user')->insert($kefu_user)) {
$kefu_user['id'] = Db::name('kefu_user')->getLastInsID();
}
}
// 查询之前的客服代表
$session = Db::name('kefu_session')
->alias('s')
->field('s.*,a.id as admin_id,a.nickname')
->join('admin a', 's.csr_id=a.id')
->where('s.user_id', $kefu_user['id'])
->where('s.deletetime', null)
->find();
// 有客服代表,但客服代表不在线,重新分配
$is_csr_distribution = false;
if ($session) {
$csr_status = Db::name('kefu_csr_config')->where('admin_id', $session['admin_id'])->value('status');
if ($csr_status != 3) {
$is_csr_distribution = true;
}
}
if (!$session || $is_csr_distribution) {
// 客服分配
$csr = Common::getAppropriateCsr();
if ($csr) {
$session = Common::distributionCsr($csr, $kefu_user['id'] . '||user');
} else {
$data = [
'session' => false,
'kefu_user' => $kefu_user,
'code' => 1,
'msg' => '非常抱歉,当前无在线客服,您可以直接在此留言,谢谢您的支持!',
];
return $data;
}
}
if ($session) {
// 记录客服接待人数
Db::name('kefu_csr_config')->where('admin_id', $session['admin_id'])->inc('reception_count')->update([
'last_reception_time' => time(),
]);
$data = [
'session' => $session,
'kefu_user' => $kefu_user,
'code' => 2,
];
return $data;
} else {
$data = [
'session' => false,
'code' => 0,
'msg' => '分配客服代表失败!',
];
return $data;
}
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
namespace addons\kefu\library\WechatCrypto;
use addons\kefu\library\WechatCrypto\ErrorCode;
/**
* XMLParse class
*
* 提供提取消息格式中的密文及生成回复消息格式的接口.
*/
class XMLParse
{
/**
* 提取出xml数据包中的加密消息
* @param string $xmltext 待提取的xml字符串
* @return string 提取出的加密消息字符串
*/
public function extract($xmltext)
{
libxml_disable_entity_loader(true);
try {
$xml = new DOMDocument();
$xml->loadXML($xmltext);
$array_e = $xml->getElementsByTagName('Encrypt');
$array_a = $xml->getElementsByTagName('ToUserName');
$encrypt = $array_e->item(0)->nodeValue;
$tousername = $array_a->item(0)->nodeValue;
return [0, $encrypt, $tousername];
} catch (Exception $e) {
//print $e . "\n";
return [ErrorCode::$ParseXmlError, null, null];
}
}
/**
* 生成xml消息
* @param string $encrypt 加密后的消息密文
* @param string $signature 安全签名
* @param string $timestamp 时间戳
* @param string $nonce 随机字符串
*/
public function generate($encrypt, $signature, $timestamp, $nonce)
{
$format = "<xml>
<Encrypt><![CDATA[%s]]></Encrypt>
<MsgSignature><![CDATA[%s]]></MsgSignature>
<TimeStamp>%s</TimeStamp>
<Nonce><![CDATA[%s]]></Nonce>
</xml>";
return sprintf($format, $encrypt, $signature, $timestamp, $nonce);
}
}
?>
+29
View File
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{$chat_name}</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<!-- 在线客服依赖以下css和js文件,请按需引入 -->
<link href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<link href="__CDN__/assets/addons/kefu/css/kefu_default.css" rel="stylesheet">
<script src=https://cdn.staticfile.org/jquery/2.1.4/jquery.min.js></script>
<script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="https://cdn.staticfile.org/layer/2.3/layer.js"></script>
<script src="__CDN__/assets/addons/kefu/js/kefu.js"></script>
</head>
<body>
<script type="text/javascript">
KeFu.initialize(document.domain, 'index', function () {
KeFu.toggle_window('show');// 打开窗口
});
// 参数一为在线客服所在网站的域名(启动Workerman服务的网站的域名)
// 参数二为模块名,站外直接填写index
// 参数三为初始化完成后的回调方法
// 参数四为指定客服,可在此处填写客服代表的后台账户id
// 若要站外调用,请于后台-》插件管理-》本插件的配置中-》跨站调用允许域名-》填写外站的域名
</script>
</body>
</html>
+116
View File
@@ -0,0 +1,116 @@
<!-- FastAdmin在线客服插件不是开源产品,所有文字、图片、样式、风格等版权归在线客服作者所有,如有复制、仿冒、抄袭、盗用,FastAdmin和在线客服作者将追究法律责任 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{$chat_name}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<meta name="renderer" content="webkit">
<link href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.staticfile.org/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<link href="__CDN__/assets/addons/kefu/css/kefu_mobile.css" rel="stylesheet">
</head>
<body>
<div class="kefu_container">
<header class="main_header">
<span id="header_title"></span>
<span id="csr_status" style="color: rgb(60, 118, 61);"></span>
<span id="kefu_error">链接中...</span>
<!-- <i class="fa fa-close fa-lg fa-fw back_page"></i> -->
</header>
<div class="content_wrapper">
<!-- 聊天消息列表 -->
<div class="chat_wrapper"></div>
<!--留言板-->
<div id="kefu_leave_message" style="display: none;" class="panel panel-success kefu_window_view">
<div class="panel-heading">非常抱歉,当前无客服在线,您留言后我们将尽快与您联系</div>
<div class="panel-body">
<form method="get" action="" class="form-horizontal">
<div class="form-group">
<label for="c-name" class="col-sm-2 control-label">姓名</label>
<div class="col-sm-10">
<input type="text" name="name" class="form-control" id="c-name" placeholder="请输入您的姓名">
</div>
</div>
<div class="form-group">
<label for="c-contact" class="col-sm-2 control-label">联系方式</label>
<div class="col-sm-10">
<input type="text" name="contact" class="form-control" id="c-contact" placeholder="请输入手机/QQ/微信号">
</div>
</div>
<div class="form-group">
<label for="c-message" class="col-sm-2 control-label">留言内容</label>
<div class="col-sm-10">
<textarea rows="5" name="message" class="form-control" id="c-message" placeholder="遇到的问题、所需服务、产品等,我们将尽快与您取得联系"></textarea>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="button" class="btn btn-success">确认留言</button>
</div>
</div>
</form>
</div>
</div>
<!-- 消息输入 -->
<div id="kefu_emoji"></div>
<!--客服输入状态-->
<div id="kefu_input_status"></div>
<div class="write">
<div class="widget_textarea">
<textarea id="kefu_message" spellcheck="false" style="height: 35px;"></textarea>
</div>
<div class="write_right">
{present name="toolbar.expression"}
<i class="smiley" title="{$toolbar.expression.title}" style="background: url('{$toolbar.expression.icon_image}') no-repeat center;background-size: 100% 100%;"></i>
{/present}
<i class="more" style="background:url('__CDN__/assets/addons/kefu/img/more.png') no-repeat center;background-size:100% 100%;"></i>
<button class="btn btn-success btn-sm send_btn">发送</button>
</div>
</div>
<div style="display: none;" class="footer_div">
<div class="toolbar">
{present name="toolbar.file"}
<div class="toolbar_item select_file">
<input id="chatfile" size="1" width="20" type="file" name="chatfile">
<img src="{$toolbar.file.icon_image}" alt="{$toolbar.file.title}" />
<div>{$toolbar.file.title}</div>
</div>
{/present}
{present name="toolbar.goods"}
<div class="toolbar_item" data-toolbar_type="goods">
<img src="{$toolbar.goods.icon_image}" alt="{$toolbar.goods.title}" />
<div>{$toolbar.goods.title}</div>
</div>
{/present}
{present name="toolbar.order"}
<div class="toolbar_item" data-toolbar_type="order">
<img src="{$toolbar.order.icon_image}" alt="{$toolbar.order.title}" />
<div>{$toolbar.order.title}</div>
</div>
{/present}
</div>
</div>
<div style="display: none;" class="select_model">
<div class="close_select_model">关闭</div>
<div class="card_list"></div>
</div>
</div>
</div>
<script src=https://cdn.staticfile.org/jquery/2.1.4/jquery.min.js></script>
<script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="https://cdn.staticfile.org/layer/2.3/layer.js"></script>
<script src="__CDN__/assets/addons/kefu/js/kefu_mobile.js"></script>
<script>
KeFu.initialize(document.domain, 'index');
</script>
</body>
</html>
+262
View File
@@ -0,0 +1,262 @@
<!-- KeFu-Admin-Template -->
<ins class="KeFu">
<!-- 右侧悬浮按钮 -->
<img src="{$cdnurl}/assets/addons/kefu/img/buoy1.png" class="kefu_button" id="kefu_button" data-html="true" data-container="body" data-toggle="popover" data-placement="left" data-content="" alt="客服浮标" />
<!-- 右侧悬浮按钮-end -->
<!-- 聊天窗口 -->
<div class="modal fade bs-example-modal-lg" id="KeFuModal" tabindex="-1" role="dialog" aria-labelledby="KeFuModal">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">
<span id="modal-title">在线客服</span>
<div class="dropdown" id="kefu_csr_status">
<button class="btn btn-default btn-xs dropdown-toggle" type="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<span class="kefu_status">状态</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="kefu_status_menu">
<li><a data-status="3" href="#">在线</a></li>
<li><a data-status="2" href="#">离开</a></li>
<li><a data-status="1" href="#">繁忙</a></li>
</ul>
</div>
<span id="kefu_error">链接中...</span>
</h4>
</div>
<div class="modal-body">
<div class="kefu-left">
<div class="top">
<input type="text" autocomplete="off" id="kefu_search_input" placeholder="搜索其实很简单"/>
</div>
<div id="session_list_search" class="search_users">
</div>
<div class="panel-group" id="session_panel" role="tablist" aria-multiselectable="false">
<!-- data-parent="#session_panel" 若需自动折叠,可在a标签上添加此属性 -->
<div class="panel panel-info dialogue_panel">
<div class="panel-heading" role="tab" id="heading_dialogue">
<h4 class="panel-title">
<a role="button" data-toggle="collapse" href="#collapse_dialogue"
aria-expanded="false" aria-controls="collapse_dialogue">对话中<span
class="red_dot text-danger"></span></a>
</h4>
</div>
<div id="collapse_dialogue" class="panel-collapse collapse" role="tabpanel"
aria-labelledby="heading_dialogue">
<div class="panel-body">
<ul id="session_list_dialogue" class="people">
<div class="none_session">这里没有会话哦~</div>
</ul>
</div>
</div>
</div>
<div class="panel panel-info invitation_panel">
<div class="panel-heading" role="tab" id="heading_invitation">
<h4 class="panel-title">
<a role="button" data-toggle="collapse" href="#collapse_invitation"
aria-expanded="false" aria-controls="collapse_invitation">可邀请<span
class="red_dot text-danger"></span></a>
</h4>
</div>
<div id="collapse_invitation" class="panel-collapse collapse" role="tabpanel"
aria-labelledby="heading_invitation">
<div class="panel-body">
<ul id="session_list_invitation" class="people">
<div class="none_session">这里没有会话哦~</div>
</ul>
</div>
</div>
</div>
<div class="panel panel-info recently_panel">
<div class="panel-heading" role="tab" id="heading_recently">
<h4 class="panel-title">
<a role="button" data-toggle="collapse" href="#collapse_recently"
aria-expanded="false" aria-controls="collapse_recently">最近沟通<span
class="red_dot text-danger"></span></a>
</h4>
</div>
<div id="collapse_recently" class="panel-collapse collapse" role="tabpanel"
aria-labelledby="heading_recently">
<div class="panel-body">
<ul id="session_list_recently" class="people">
<div class="none_session">这里没有会话哦~</div>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="kefu-right">
<div class="top">
<span>
<span id="session_user_name" class="name">无会话</span>
<span class="top_span" id="kefu_blacklist">拉黑名单</span>
<span class="top_span" id="kefu_view_trajectory">查看轨迹</span>
<span class="top_span" id="kefu_view_user_card">用户名片</span>
</span>
</div>
<div class="chat">
<div class="chat_scroll kefu_window_view" id="kefu_scroll">
</div>
<div class="VivaTimeline kefu_window_view" id="kefu_trajectory">
<dl id="kefu_trajectory_log"></dl>
</div>
<div class="panel panel-success kefu_window_view" id="kefu_user_card">
<div class="panel-heading">用户名片</div>
<div class="panel-body">
<form method="get" action="" class="form-horizontal">
<div class="form-group">
<label class="col-sm-3 control-label">用户</label>
<div class="col-sm-7">
<input id="card-user" type="text" readonly="readonly"
class="form-control"/>
</div>
</div>
<div class="form-group">
<label for="card-nickname" class="col-sm-3 control-label">用户昵称</label>
<div class="col-sm-7">
<input id="card-nickname" name="nickname" type="text"
placeholder="请输入用户昵称" class="form-control"/>
</div>
</div>
<div class="form-group">
<label for="card-referrer" class="col-sm-3 control-label">用户来路</label>
<div class="col-sm-7">
<textarea id="card-referrer" name="referrer" rows="2"
class="form-control" placeholder=""></textarea>
</div>
</div>
<div class="form-group">
<label for="card-contact" class="col-sm-3 control-label">联系方式</label>
<div class="col-sm-7">
<textarea id="card-contact" name="contact" rows="2" class="form-control"
placeholder="QQ/微信/手机号等"></textarea>
</div>
</div>
<div class="form-group">
<label for="card-note" class="col-sm-3 control-label">备注</label>
<div class="col-sm-7">
<textarea id="card-note" name="note" rows="4" class="form-control"
placeholder="请输入备注,备注信息用户不可见"></textarea>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-7">
<button id="card-button" type="button" class="btn btn-success">确认修改
</button>
</div>
</div>
</form>
</div>
</div>
</div>
<!-- 按需渲染表情包 -->
{present name="toolbar.expression"}
<div class="kefu_emoji">
<div id="kefu_emoji">
{for start="1" end="37"}
<img class="emoji" title="" src="{$cdnurl}/assets/addons/kefu/img/emoji/{$i}.png">
{/for}
</div>
</div>
{/present}
<!-- 按需渲染自动回复 -->
{present name="toolbar.fastreply"}
<div class="kefu_fast_reply_list">
<table class="table table-striped table-hover" id="kefu_fast_reply_list">
{volist name="fast_reply" id="reply"}
<tr data-id='{$reply.id}'>
<td>{$reply.title}</td>
</tr>
{/volist}
</table>
</div>
{/present}
<!-- 准备链接输入面板 -->
{present name="toolbar.link"}
<div class="kefu_link_form">
<div id="kefu_link_form">
<div class="input-group">
<span class="input-group-addon">链接说明</span>
<input type="text" placeholder="选填" class="form-control" id="kefu_link_form_ins"/>
</div>
<div class="input-group">
<span class="input-group-addon">链接地址</span>
<input type="text" value="http://" class="form-control" id="kefu_link_form_url"/>
</div>
<div class="link_form_buttons">
<button type="button" class="btn btn-success btn-sm">确认</button>
<button type="button" class="btn btn-default btn-sm">取消</button>
</div>
</div>
</div>
{/present}
<!--用户输入状态-->
<div class="input_status">
<div id="kefu_input_status"></div>
</div>
<div class="write">
<div class="write_top">
{present name="toolbar.expression"}
<i class="toolbar_icon smiley" title="{$toolbar.expression.title}" style="background: url('{$toolbar.expression.icon_image}') no-repeat center;background-size: 80% 80%;"></i>
{/present}
{present name="toolbar.file"}
<div class="select_file">
<input title="{$toolbar.file.title}" id="chatfile" size="1" width="20" type="file" name="chatfile">
<i class="attach toolbar_icon" style="background: url('{$toolbar.file.icon_image}') no-repeat center;background-size: 80% 80%;"></i>
</div>
{/present}
{present name="toolbar.link"}
<i class="toolbar_icon link" title="{$toolbar.link.title}" style="background: url('{$toolbar.link.icon_image}') no-repeat center;background-size: 80% 80%;"></i>
{/present}
<span title="按下Ctrl+Enter换行" class="write_top_span" id="send_tis">按下Enter发送消息</span>
{present name="toolbar.fastreply"}
<span class="write_top_span kefu_fast_reply">{$toolbar.fastreply.title}</span>
{/present}
</div>
<pre contenteditable="plaintext-only" id="kefu_message"></pre>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 客服端右击用户菜单 -->
<ul class="dropdown-menu" style="z-index: 1060;" id="kefu_menu">
<li class="kefu_menu_item transfer" data-action="transfer"><a href="#">转接会话</a></li>
<li class="kefu_menu_item invitation" data-action="invitation"><a href="#">邀请对话</a></li>
<li class="kefu_menu_item" data-action="edit_nickname"><a href="#">修改昵称</a></li>
<li class="kefu_menu_item" data-action="del"><a href="#">删除会话</a></li>
</ul>
<!-- 聊天窗口-end -->
</ins>
<!-- KeFu-Template-end
+149
View File
@@ -0,0 +1,149 @@
<!-- KeFu-Template -->
<ins class="KeFu">
<!-- 右侧悬浮按钮 -->
<img src="{$cdnurl}/assets/addons/kefu/img/buoy1.png" class="kefu_button" id="kefu_button" data-html="true" data-container="body" data-toggle="popover" data-placement="left" data-content="" alt="客服浮标" />
<!-- 右侧悬浮按钮-end -->
<!-- 聊天窗口 -->
<div class="modal fade bs-example-modal-lg" id="KeFuModal" tabindex="-1" role="dialog" aria-labelledby="KeFuModal">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">
<span id="modal-title"></span>
<span id="csr_status"></span>
<span id="kefu_error">链接中...</span>
</h4>
</div>
<div class="modal-body">
<div class="kefu-left">
<div class="alert alert-warning-light announcement">
<i class="fa fa-bell-o"></i><span id="announcement"></span>
</div>
<div class="chat">
<div class="chat_scroll kefu_window_view" id="kefu_scroll"></div>
<div id="kefu_leave_message" style="display: none;" class="panel panel-success kefu_window_view">
<div class="panel-heading">非常抱歉,当前无客服在线,您留言后我们将尽快与您联系</div>
<div class="panel-body">
<form method="get" action="" class="form-horizontal">
<div class="form-group">
<label for="c-name" class="col-sm-2 control-label">姓名</label>
<div class="col-sm-10">
<input type="text" name="name" class="form-control" id="c-name" placeholder="请输入您的姓名">
</div>
</div>
<div class="form-group">
<label for="c-contact" class="col-sm-2 control-label">联系方式</label>
<div class="col-sm-10">
<input type="text" name="contact" class="form-control" id="c-contact" placeholder="请输入手机/QQ/微信号">
</div>
</div>
<div class="form-group">
<label for="c-message" class="col-sm-2 control-label">留言内容</label>
<div class="col-sm-10">
<textarea rows="5" name="message" class="form-control" id="c-message" placeholder="遇到的问题、所需服务、产品等,我们将尽快与您取得联系"></textarea>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="button" class="btn btn-success">确认留言</button>
</div>
</div>
</form>
</div>
</div>
</div>
<!-- 按需渲染表情包 -->
{present name="toolbar.expression"}
<div class="kefu_emoji">
<div id="kefu_emoji">
{for start="1" end="37"}
<img class="emoji" src="{$cdnurl}/assets/addons/kefu/img/emoji/{$i}.png">
{/for}
</div>
</div>
{/present}
<!-- 准备链接输入面板 -->
{present name="toolbar.link"}
<div class="kefu_link_form">
<div id="kefu_link_form">
<div class="input-group">
<span class="input-group-addon">链接说明</span>
<input type="text" placeholder="选填" class="form-control" id="kefu_link_form_ins"/>
</div>
<div class="input-group">
<span class="input-group-addon">链接地址</span>
<input type="text" value="http://" class="form-control" id="kefu_link_form_url"/>
</div>
<div class="link_form_buttons">
<button type="button" class="btn btn-success btn-sm">确认</button>
<button type="button" class="btn btn-default btn-sm">取消</button>
</div>
</div>
</div>
{/present}
<!--商品和订单选择框-->
<div class="goods_select_model">
<div id="goods_select_model">
<div class="project_list"></div>
</div>
</div>
<!--客服输入状态-->
<div class="input_status">
<div id="kefu_input_status"></div>
</div>
<div class="write">
<div class="write_top">
{present name="toolbar.expression"}
<i class="toolbar_icon smiley" title="{$toolbar.expression.title}" style="background: url('{$toolbar.expression.icon_image}') no-repeat center;background-size: 80% 80%;"></i>
{/present}
{present name="toolbar.file"}
<div class="select_file">
<input title="{$toolbar.file.title}" id="chatfile" size="1" width="20" type="file" name="chatfile">
<i class="attach toolbar_icon" style="background: url('{$toolbar.file.icon_image}') no-repeat center;background-size: 80% 80%;"></i>
</div>
{/present}
<!--防止xss,链接功能取消-->
<!--{present name="toolbar.link"}
<i class="toolbar_icon link" title="{$toolbar.link.title}" style="background: url('{$toolbar.link.icon_image}') no-repeat center;background-size: 80% 80%;"></i>
{/present}-->
{present name="toolbar.goods"}
<i class="toolbar_icon goods" title="{$toolbar.goods.title}" style="background: url('{$toolbar.goods.icon_image}') no-repeat center;background-size: 80% 80%;"></i>
{/present}
{present name="toolbar.order"}
<i class="toolbar_icon order" title="{$toolbar.order.title}" style="background: url('{$toolbar.order.icon_image}') no-repeat center;background-size: 80% 80%;"></i>
{/present}
<span title="按下Ctrl+Enter换行" id="send_tis">按下Enter发送消息</span>
</div>
<pre contenteditable="plaintext-only" id="kefu_message"></pre>
</div>
</div>
<div class="kefu-right">
<div id="kefu_chat_slide_f"></div>
<div class="chat_introduces"></div>
</div>
</div>
</div>
</div>
</div>
<!-- 聊天窗口-end -->
</ins>
<!-- KeFu-Template-end -->
+1
View File
@@ -0,0 +1 @@
{"license":"regular","licenseto":"16556","licensekey":"7P9ZKjXLBSIwWObi 9uI02YSv\/lACsiPp+RDcR1UdyqzclNt9TjhV5+sAPbg="}
+264
View File
@@ -0,0 +1,264 @@
<?php
namespace addons\qcloudsms;
use addons\qcloudsms\library\SmsSingleSender;
use addons\qcloudsms\library\SmsVoicePromptSender;
use addons\qcloudsms\library\SmsVoiceverifyCodeSender;
use addons\qcloudsms\library\TtsVoiceSender;
use think\Addons;
use think\Config;
use telesign\sdk\messaging\MessagingClient;
use telesign\sdk\voice\VoiceClient;
use AlibabaCloud\Client\AlibabaCloud;
use AlibabaCloud\Client\Exception\ClientException;
use AlibabaCloud\Client\Exception\ServerException;
// 导入对应产品模块的client
use TencentCloud\Sms\V20210111\SmsClient;
// 导入要请求接口对应的Request类
use TencentCloud\Sms\V20210111\Models\SendSmsRequest;
use TencentCloud\Common\Exception\TencentCloudSDKException;
use TencentCloud\Common\Credential;
// 导入可选配置类
use TencentCloud\Common\Profile\ClientProfile;
use TencentCloud\Common\Profile\HttpProfile;
/**
* 插件
*/
class Qcloudsms extends Addons
{
private $appid = null;
private $appkey = null;
private $config = null;
private $sender = null;
private $sendError = '';
public function ConfigInit()
{
$this->config = $this->getConfig();
//如果使用语音短信 更换成语音短信模板
if ($this->config['isVoice'] == 1) {
$this->config['template'] = $this->config['voiceTemplate'];
//语音短信 需要另行设置Aappid 与Appkey
$this->appid = $this->config['voiceAppid'];
$this->appkey = $this->config['voiceAppkey'];
} else {
$this->appid = $this->config['appid'];
$this->appkey = $this->config['appkey'];
}
}
/**
* 短信发送行为
* @param Sms $params
* @return boolean
*/
public function smsSend(&$params) {
//腾讯
try {
/* 必要步骤:
* 实例化一个认证对象,入参需要传入腾讯云账户密钥对secretIdsecretKey。
* 这里采用的是从环境变量读取的方式,需要在环境变量中先设置这两个值。
* 你也可以直接在代码中写死密钥对,但是小心不要将代码复制、上传或者分享给他人,
* 以免泄露密钥对危及你的财产安全。
* CAM密匙查询: https://console.cloud.tencent.com/cam/capi*/
$cred = new Credential("IKIDhLn151pT0bItU3UsBQabWNYacdVIuoYF", "lPzb9sTGuNsZ6c3MugUOAmNq9874vrlb");
//$cred = new Credential(getenv("TENCENTCLOUD_SECRET_ID"), getenv("TENCENTCLOUD_SECRET_KEY"));
// 实例化一个http选项,可选的,没有特殊需求可以跳过
$httpProfile = new HttpProfile();
// 配置代理
// $httpProfile->setProxy("https://ip:port");
$httpProfile->setReqMethod("GET"); // post请求(默认为post请求)
$httpProfile->setReqTimeout(30); // 请求超时时间,单位为秒(默认60秒)
$httpProfile->setEndpoint("sms.tencentcloudapi.com"); // 指定接入地域域名(默认就近接入)
// 实例化一个client选项,可选的,没有特殊需求可以跳过
$clientProfile = new ClientProfile();
$clientProfile->setSignMethod("TC3-HMAC-SHA256"); // 指定签名算法(默认为HmacSHA256)
$clientProfile->setHttpProfile($httpProfile);
// 实例化要请求产品(以sms为例)的client对象,clientProfile是可选的
// 第二个参数是地域信息,根据您选择的国际站地域,如您选择的是新加坡国际站,则应该填入字符串ap-singapore,地域列表可参考https://intl.cloud.tencent.com/document/api/382/40466?lang=en#region-list
$client = new SmsClient($cred, "ap-singapore", $clientProfile);
// 实例化一个 sms 发送短信请求对象,每个接口都会对应一个request对象。
$req = new SendSmsRequest();
/* 短信应用ID: 短信SdkAppId在 [短信控制台] 添加应用后生成的实际SdkAppId,示例如1400006666 */
$req->SmsSdkAppId = "2400000981";
/* 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名,签名信息可登录 [短信控制台] 查看 */
$req->SignName = "";
/* 短信码号扩展号: 默认未开通,如需开通请联系 [sms helper] */
$req->ExtendCode = "";
/* 下发手机号码,采用 E.164 标准,+[国家或地区码][手机号]
* 示例如:+8613711112222, 其中前面有一个+号 86为国家码,13711112222为手机号,最多不要超过200个手机号*/
$phone_number = $params['m_prefix'] . $params['mobile'];
$req->PhoneNumberSet = array($phone_number);
/* 国际/港澳台短信 SenderId: 中国大陆地区短信填空,默认未开通,如需开通请联系 [sms helper] */
$req->SenderId = "";
/* 用户的 session 内容: 可以携带用户侧 ID 等上下文信息,server 会原样返回 */
$req->SessionContext = "【turkey global】Your OTP code is {1} Don't share this code with others dated!";
/* 模板 ID: 必须填写已审核通过的模板 ID。模板ID可登录 [短信控制台] 查看 */
$req->TemplateId = "2905310"; //2905133
/* 模板参数: 若无模板参数,则设置为空*/
$req->TemplateParamSet = array($params['code']);
// 通过client对象调用SendSms方法发起请求。注意请求方法名与请求对象是对应的
// 返回的resp是一个SendSmsResponse类的实例,与请求对象对应
$resp = $client->SendSms($req);
// 输出json格式的字符串回包
// print_r($resp->toJsonString());
// 也可以取出单个值。
// 您可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义
// print_r($resp->TotalCount);
return true;
}
catch(TencentCloudSDKException $e) {
// echo $e;
return true;
}
// AlibabaCloud::accessKeyClient('LTAI5tGjBhYq4u2e13f4cAQ6', 'X20XXcrKZHbGjpHHeTo3rfNuhMgeKg')
// ->regionId('ap-southeast-1')
// ->asGlobalClient();
// $phone_number = $params['m_prefix'] . $params['mobile'];
// $result = AlibabaCloud::rpcRequest()
// ->product('Dysmsapi')
// ->host('dysmsapi.ap-southeast-1.aliyuncs.com')
// ->version('2018-05-01')
// ->action('SendMessageToGlobe')
// ->method('POST')
// ->options([
// 'query' => [
// "To" => $phone_number,
// // "From" => "1234567890",
// "Message" => "【Poly】Your OTP code is " . $params['code'] . " Don't share this code with others dated",
// ],
// ])
// ->request();
//// print_r($result->toArray());
//
// return true;
// if ($send_res['status']['code'] == 200) {
// return true;
// } else {
// return true;
// }
// die;
}
/**
* 短信发送通知
* @param array $params
* @return boolean
*/
public function smsNotice(&$params)
{
$this->ConfigInit();
try {
if ($this->config['isTemplateSender'] == 1) {
$templateID = $this->config['template'][$params['template']];
if ($this->config['isVoice'] != 1) {
//普通短信发送
$this->sender = new SmsSingleSender($this->appid, $this->appkey);
$result = $this->sender->sendWithParam("86", $params['mobile'], $templateID, ["{$params['msg']}"], $this->config['sign'], "", "");
} else {
//语音短信发送
$this->sender = new TtsVoiceSender($this->appid, $this->appkey);
//参数: 国家码,手机号、模板ID、模板参数、播放次数(可选字段)、用户的session内容,服务器端原样返回(可选字段)
$result = $this->sender->send("86", $params['mobile'], $templateID, [$params['msg']]);
}
} else {
//判断是否是语音短信
if ($this->config['isVoice'] != 1) {
$this->sender = new SmsSingleSender($this->appid, $this->appkey);
//参数:短信类型{1营销短信,0普通短信 }、国家码、手机号、短信内容、扩展码(可留空)、服务的原样返回的参数
$result = $this->sender->send($params['type'], '86', $params['mobile'], $params['msg'], "", "");
} else {
$this->sender = new SmsVoicePromptSender($this->appid, $this->appkey);
//参数:国家码、手机号、语音类型(目前固定为2)、短信内容、播放次数(默认2次)、服务的原样返回的参数
$result = $this->sender->send('86', $params['mobile'], 2, $params['msg']);
}
}
$rsp = (array)json_decode($result, true);
if ($rsp['result'] == 0 && $rsp['errmsg'] == 'OK') {
return true;
} else {
//记录错误信息
$this->setError($rsp);
return false;
}
} catch (\Exception $e) {
var_dump($e);
exit();
}
}
/**
* 记录失败信息
* @param [type] $err [description]
*/
private function setError($err)
{
$this->sendError = $err;
}
/**
* 获取失败信息
* @return [type] [description]
*/
public function getError()
{
return $this->sendError;
}
/**
* 检测验证是否正确
* @param Sms $params
* @return boolean
*/
public function smsCheck(&$params)
{
return true;
}
/**
* 插件安装方法
* @return bool
*/
public function install()
{
return true;
}
/**
* 插件卸载方法
* @return bool
*/
public function uninstall()
{
return true;
}
/**
* 插件启用方法
* @return bool
*/
public function enable()
{
return true;
}
/**
* 插件禁用方法
* @return bool
*/
public function disable()
{
return true;
}
}
+139
View File
@@ -0,0 +1,139 @@
<?php
return array(
array(
'name' => 'appid',
'title' => '应用AppID',
'type' => 'string',
'content' =>
array(),
'value' => '',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
),
array(
'name' => 'appkey',
'title' => '应用AppKEY',
'type' => 'string',
'content' =>
array(),
'value' => '',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
),
array(
'name' => 'voiceAppid',
'title' => '语音短信AppID',
'type' => 'string',
'content' =>
array(),
'value' => '',
'rule' => 'required',
'msg' => '使用语音短信必须设置',
'tip' => '',
'ok' => '',
'extend' => '',
),
array(
'name' => 'voiceAppkey',
'title' => '语音短信AppKEY',
'type' => 'string',
'content' =>
array(),
'value' => '',
'rule' => 'required',
'msg' => '使用语音短信必须设置',
'tip' => '',
'ok' => '',
'extend' => '',
),
array(
'name' => 'sign',
'title' => '签名',
'type' => 'string',
'content' =>
array(),
'value' => 'your sign',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
),
array(
'name' => 'isVoice',
'title' => '是否使用语音短信',
'type' => 'radio',
'content' =>
array(
0 => '否',
1 => '是',
),
'value' => '0',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
),
array(
'name' => 'isTemplateSender',
'title' => '是否使用短信模板发送',
'type' => 'radio',
'content' =>
array(
0 => '否',
1 => '是',
),
'value' => '1',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
),
array(
'name' => 'template',
'title' => '短信模板',
'type' => 'array',
'content' =>
array(),
'value' =>
array(
'register' => '',
'resetpwd' => '',
'changepwd' => '',
'profile' => '',
),
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
),
array(
'name' => 'voiceTemplate',
'title' => '语音短信模板',
'type' => 'array',
'content' =>
array(),
'value' =>
array(
'register' => '',
'resetpwd' => '',
'changepwd' => '',
'profile' => '',
),
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
),
);
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace addons\qcloudsms\controller;
use think\addons\Controller;
class Index extends Controller
{
public function index()
{
$this->error("当前插件暂无前台页面");
}
}
+10
View File
@@ -0,0 +1,10 @@
name = qcloudsms
title = 腾讯云短信发送插件
intro = 腾讯云短信发送插件
author = Seacent
website = https://www.seacent.com
version = 1.0.3
state = 1
url = /addons/qcloudsms
license = regular
licenseto = 16556
+69
View File
@@ -0,0 +1,69 @@
<?php
namespace addons\qcloudsms\library;
use addons\qcloudsms\library\SmsSenderUtil;
/**
* 按语音文件fid发送语音通知类
*
*/
class FileVoiceSender
{
private $url;
private $appid;
private $appkey;
private $util;
/**
* 构造函数
*
* @param string $appid sdkappid
* @param string $appkey sdkappid对应的appkey
*/
public function __construct($appid, $appkey)
{
$this->url = "https://cloud.tim.qq.com/v5/tlsvoicesvr/sendfvoice";
$this->appid = $appid;
$this->appkey = $appkey;
$this->util = new SmsSenderUtil();
}
/**
*
* 按语音文件fid发送语音通知
*
* @param string $nationCode 国家码,如 86 为中国
* @param string $phoneNumber 不带国家码的手机号
* @param string $fid 语音文件fid
* @param string $playtimes 播放次数,可选,最多3次,默认2次
* @param string $ext 用户的session内容,服务端原样返回,可选字段,不需要可填空串
* @return string 应答json字符串,详细内容参见腾讯云协议文档
*/
public function send($nationCode, $phoneNumber, $fid, $playtimes = 2, $ext = "")
{
$random = $this->util->getRandom();
$curTime = time();
$wholeUrl = $this->url . "?sdkappid=" . $this->appid . "&random=" . $random;
// 按照协议组织 post 包体
$data = new \stdClass();
$tel = new \stdClass();
$tel->nationcode = "".$nationCode;
$tel->mobile = "".$phoneNumber;
$data->tel = $tel;
$data->fid = $fid;
$data->playtimes = $playtimes;
// app凭证
$data->sig = $this->util->calculateSig($this->appkey, $random,
$curTime, array($phoneNumber));
// unix时间戳,请求发起时间,如果和系统时间相差超过10分钟则会返回失败
$data->time = $curTime;
$data->ext = $ext;
return $this->util->sendCurlPost($wholeUrl, $data);
}
}
+91
View File
@@ -0,0 +1,91 @@
<?php
namespace addons\qcloudsms\library;
use addons\qcloudsms\library\SmsSenderUtil;
/**
* 拉取单个手机短信状态类
*
*/
class SmsMobileStatusPuller
{
private $url;
private $appid;
private $appkey;
private $util;
/**
* 构造函数
*
* @param string $appid sdkappid
* @param string $appkey sdkappid对应的appkey
*/
public function __construct($appid, $appkey)
{
$this->url = "https://yun.tim.qq.com/v5/tlssmssvr/pullstatus4mobile";
$this->appid = $appid;
$this->appkey = $appkey;
$this->util = new SmsSenderUtil();
}
/**
* 拉取回执结果
*
* @param int $type 拉取类型,0表示回执结果,1表示回复信息
* @param string $nationCode 国家码,如 86 为中国
* @param string $mobile 不带国家码的手机号
* @param int $beginTime 开始时间(unix timestamp)
* @param int $endTime 结束时间(unix timestamp)
* @param int $max 拉取最大条数,最多100
* @return string 应答json字符串,详细内容参见腾讯云协议文档
*/
private function pull($type, $nationCode, $mobile, $beginTime, $endTime, $max)
{
$random = $this->util->getRandom();
$curTime = time();
$wholeUrl = $this->url . "?sdkappid=" . $this->appid . "&random=" . $random;
$data = new \stdClass();
$data->sig = $this->util->calculateSigForPuller($this->appkey, $random, $curTime);
$data->time = $curTime;
$data->type = $type;
$data->max = $max;
$data->begin_time = $beginTime;
$data->end_time = $endTime;
$data->nationcode = $nationCode;
$data->mobile = $mobile;
return $this->util->sendCurlPost($wholeUrl, $data);
}
/**
* 拉取回执结果
*
* @param string $nationCode 国家码,如 86 为中国
* @param string $mobile 不带国家码的手机号
* @param int $beginTime 开始时间(unix timestamp)
* @param int $endTime 结束时间(unix timestamp)
* @param int $max 拉取最大条数,最多100
* @return string 应答json字符串,详细内容参见腾讯云协议文档
*/
public function pullCallback($nationCode, $mobile, $beginTime, $endTime, $max)
{
return $this->pull(0, $nationCode, $mobile, $beginTime, $endTime, $max);
}
/**
* 拉取回复信息
*
* @param string $nationCode 国家码,如 86 为中国
* @param string $mobile 不带国家码的手机号
* @param int $beginTime 开始时间(unix timestamp)
* @param int $endTime 结束时间(unix timestamp)
* @param int $max 拉取最大条数,最多100
* @return string 应答json字符串,详细内容参见腾讯云协议文档
*/
public function pullReply($nationCode, $mobile, $beginTime, $endTime, $max)
{
return $this->pull(1, $nationCode, $mobile, $beginTime, $endTime, $max);
}
}
+99
View File
@@ -0,0 +1,99 @@
<?php
namespace addons\qcloudsms\library;
use addons\qcloudsms\library\SmsSenderUtil;
/**
* 群发短信类
*
*/
class SmsMultiSender
{
private $url;
private $appid;
private $appkey;
private $util;
/**
* 构造函数
*
* @param string $appid sdkappid
* @param string $appkey sdkappid对应的appkey
*/
public function __construct($appid, $appkey)
{
$this->url = "https://yun.tim.qq.com/v5/tlssmssvr/sendmultisms2";
$this->appid = $appid;
$this->appkey = $appkey;
$this->util = new SmsSenderUtil();
}
/**
* 普通群发
*
* 普通群发需明确指定内容,如果有多个签名,请在内容中以【】的方式添加到信息内容中,
* 否则系统将使用默认签名。
*
*
* @param int $type 短信类型,0 为普通短信,1 营销短信
* @param string $nationCode 国家码,如 86 为中国
* @param array $phoneNumbers 不带国家码的手机号列表
* @param string $msg 信息内容,必须与申请的模板格式一致,否则将返回错误
* @param string $extend 扩展码,可填空串
* @param string $ext 服务端原样返回的参数,可填空串
* @return string 应答json字符串,详细内容参见腾讯云协议文档
*/
public function send($type, $nationCode, $phoneNumbers, $msg, $extend = "", $ext = "")
{
$random = $this->util->getRandom();
$curTime = time();
$wholeUrl = $this->url . "?sdkappid=" . $this->appid . "&random=" . $random;
$data = new \stdClass();
$data->tel = $this->util->phoneNumbersToArray($nationCode, $phoneNumbers);
$data->type = $type;
$data->msg = $msg;
$data->sig = $this->util->calculateSig($this->appkey, $random,
$curTime, $phoneNumbers);
$data->time = $curTime;
$data->extend = $extend;
$data->ext = $ext;
return $this->util->sendCurlPost($wholeUrl, $data);
}
/**
* 指定模板群发
*
*
* @param string $nationCode 国家码,如 86 为中国
* @param array $phoneNumbers 不带国家码的手机号列表
* @param int $templId 模板id
* @param array $params 模板参数列表,如模板 {1}...{2}...{3},那么需要带三个参数
* @param string $sign 签名,如果填空串,系统会使用默认签名
* @param string $extend 扩展码,可填空串
* @param string $ext 服务端原样返回的参数,可填空串
* @return string 应答json字符串,详细内容参见腾讯云协议文档
*/
public function sendWithParam($nationCode, $phoneNumbers, $templId, $params,
$sign = "", $extend = "", $ext = "")
{
$random = $this->util->getRandom();
$curTime = time();
$wholeUrl = $this->url . "?sdkappid=" . $this->appid . "&random=" . $random;
$data = new \stdClass();
$data->tel = $this->util->phoneNumbersToArray($nationCode, $phoneNumbers);
$data->sign = $sign;
$data->tpl_id = $templId;
$data->params = $params;
$data->sig = $this->util->calculateSigForTemplAndPhoneNumbers(
$this->appkey, $random, $curTime, $phoneNumbers);
$data->time = $curTime;
$data->extend = $extend;
$data->ext = $ext;
return $this->util->sendCurlPost($wholeUrl, $data);
}
}
+211
View File
@@ -0,0 +1,211 @@
<?php
namespace addons\qcloudsms\library;
/**
* 发送Util类
*
*/
class SmsSenderUtil
{
/**
* 生成随机数
*
* @return int 随机数结果
*/
public function getRandom()
{
return rand(100000, 999999);
}
/**
* 生成签名
*
* @param string $appkey sdkappid对应的appkey
* @param string $random 随机正整数
* @param string $curTime 当前时间
* @param array $phoneNumbers 手机号码
* @return string 签名结果
*/
public function calculateSig($appkey, $random, $curTime, $phoneNumbers)
{
$phoneNumbersString = $phoneNumbers[0];
for ($i = 1; $i < count($phoneNumbers); $i++) {
$phoneNumbersString .= ("," . $phoneNumbers[$i]);
}
return hash("sha256", "appkey=".$appkey."&random=".$random
."&time=".$curTime."&mobile=".$phoneNumbersString);
}
/**
* 生成签名
*
* @param string $appkey sdkappid对应的appkey
* @param string $random 随机正整数
* @param string $curTime 当前时间
* @param array $phoneNumbers 手机号码
* @return string 签名结果
*/
public function calculateSigForTemplAndPhoneNumbers($appkey, $random,
$curTime, $phoneNumbers)
{
$phoneNumbersString = $phoneNumbers[0];
for ($i = 1; $i < count($phoneNumbers); $i++) {
$phoneNumbersString .= ("," . $phoneNumbers[$i]);
}
return hash("sha256", "appkey=".$appkey."&random=".$random
."&time=".$curTime."&mobile=".$phoneNumbersString);
}
public function phoneNumbersToArray($nationCode, $phoneNumbers)
{
$i = 0;
$tel = array();
do {
$telElement = new \stdClass();
$telElement->nationcode = $nationCode;
$telElement->mobile = $phoneNumbers[$i];
array_push($tel, $telElement);
} while (++$i < count($phoneNumbers));
return $tel;
}
/**
* 生成签名
*
* @param string $appkey sdkappid对应的appkey
* @param string $random 随机正整数
* @param string $curTime 当前时间
* @param array $phoneNumber 手机号码
* @return string 签名结果
*/
public function calculateSigForTempl($appkey, $random, $curTime, $phoneNumber)
{
$phoneNumbers = array($phoneNumber);
return $this->calculateSigForTemplAndPhoneNumbers($appkey, $random,
$curTime, $phoneNumbers);
}
/**
* 生成签名
*
* @param string $appkey sdkappid对应的appkey
* @param string $random 随机正整数
* @param string $curTime 当前时间
* @return string 签名结果
*/
public function calculateSigForPuller($appkey, $random, $curTime)
{
return hash("sha256", "appkey=".$appkey."&random=".$random
."&time=".$curTime);
}
/**
* 生成上传文件授权
*
* @param string $appkey sdkappid对应的appkey
* @param string $random 随机正整数
* @param string $curTime 当前时间
* @param array $fileSha1Sum 文件sha1sum
* @return string 授权结果
*/
public function calculateAuth($appkey, $random, $curTime, $fileSha1Sum)
{
return hash("sha256", "appkey=".$appkey."&random=".$random
."&time=".$curTime."&content-sha1=".$fileSha1Sum);
}
/**
* 生成sha1sum
*
* @param string $content 内容
* @return string 内容sha1散列值
*/
public function sha1sum($content)
{
return hash("sha1", $content);
}
/**
* 发送请求
*
* @param string $url 请求地址
* @param array $dataObj 请求内容
* @return string 应答json字符串
*/
public function sendCurlPost($url, $dataObj)
{
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_HEADER, 0);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 60);
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($dataObj));
curl_setopt($curl, CURLOPT_HTTPHEADER, array(
'Content-Type: application/json; charset=utf-8',
'Content-Length: ' . strlen(json_encode($dataObj)))
);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
$ret = curl_exec($curl);
if (false == $ret) {
// curl_exec failed
$result = "{ \"result\":" . -2 . ",\"errmsg\":\"" . curl_error($curl) . "\"}";
} else {
$rsp = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if (200 != $rsp) {
$result = "{ \"result\":" . -1 . ",\"errmsg\":\"". $rsp
. " " . curl_error($curl) ."\"}";
} else {
$result = $ret;
}
}
curl_close($curl);
return $result;
}
/**
* 发送请求
*
* @param string $req 请求对象
* @return string 应答json字符串
*/
public function fetch($req)
{
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $req->url);
curl_setopt($curl, CURLOPT_HTTPHEADER, $req->headers);
curl_setopt($curl, CURLOPT_POSTFIELDS, $req->body);
curl_setopt($curl, CURLOPT_HEADER, 0);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 60);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
$result = curl_exec($curl);
if (false == $result) {
// curl_exec failed
$result = "{ \"result\":" . -2 . ",\"errmsg\":\"" . curl_error($curl) . "\"}";
} else {
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if (200 != $code) {
$result = "{ \"result\":" . -1 . ",\"errmsg\":\"". $rsp
. " " . curl_error($curl) ."\"}";
}
}
curl_close($curl);
return $result;
}
}
+107
View File
@@ -0,0 +1,107 @@
<?php
namespace addons\qcloudsms\library;
use addons\qcloudsms\library\SmsSenderUtil;
/**
* 单发短信类
*
*/
class SmsSingleSender
{
private $url;
private $appid;
private $appkey;
private $util;
/**
* 构造函数
*
* @param string $appid sdkappid
* @param string $appkey sdkappid对应的appkey
*/
public function __construct($appid, $appkey)
{
$this->url = "https://yun.tim.qq.com/v5/tlssmssvr/sendsms";
$this->appid = $appid;
$this->appkey = $appkey;
$this->util = new SmsSenderUtil();
}
/**
* 普通单发
*
* 普通单发需明确指定内容,如果有多个签名,请在内容中以【】的方式添加到信息内容中,否则系统将使用默认签名。
*
* @param int $type 短信类型,0 为普通短信,1 营销短信
* @param string $nationCode 国家码,如 86 为中国
* @param string $phoneNumber 不带国家码的手机号
* @param string $msg 信息内容,必须与申请的模板格式一致,否则将返回错误
* @param string $extend 扩展码,可填空串
* @param string $ext 服务端原样返回的参数,可填空串
* @return string 应答json字符串,详细内容参见腾讯云协议文档
*/
public function send($type, $nationCode, $phoneNumber, $msg, $extend = "", $ext = "")
{
$random = $this->util->getRandom();
$curTime = time();
$wholeUrl = $this->url . "?sdkappid=" . $this->appid . "&random=" . $random;
// 按照协议组织 post 包体
$data = new \stdClass();
$tel = new \stdClass();
$tel->nationcode = "".$nationCode;
$tel->mobile = "".$phoneNumber;
$data->tel = $tel;
$data->type = (int)$type;
$data->msg = $msg;
$data->sig = hash("sha256",
"appkey=".$this->appkey."&random=".$random."&time="
.$curTime."&mobile=".$phoneNumber, FALSE);
$data->time = $curTime;
$data->extend = $extend;
$data->ext = $ext;
return $this->util->sendCurlPost($wholeUrl, $data);
}
/**
* 指定模板单发
*
* @param string $nationCode 国家码,如 86 为中国
* @param string $phoneNumber 不带国家码的手机号
* @param int $templId 模板 id
* @param array $params 模板参数列表,如模板 {1}...{2}...{3},那么需要带三个参数
* @param string $sign 签名,如果填空串,系统会使用默认签名
* @param string $extend 扩展码,可填空串
* @param string $ext 服务端原样返回的参数,可填空串
* @return string 应答json字符串,详细内容参见腾讯云协议文档
*/
public function sendWithParam($nationCode, $phoneNumber, $templId = 0, $params,
$sign = "", $extend = "", $ext = "")
{
$random = $this->util->getRandom();
$curTime = time();
$wholeUrl = $this->url . "?sdkappid=" . $this->appid . "&random=" . $random;
// 按照协议组织 post 包体
$data = new \stdClass();
$tel = new \stdClass();
$tel->nationcode = "".$nationCode;
$tel->mobile = "".$phoneNumber;
$data->tel = $tel;
$data->sig = $this->util->calculateSigForTempl($this->appkey, $random,
$curTime, $phoneNumber);
$data->tpl_id = $templId;
$data->params = $params;
$data->sign = $sign;
$data->time = $curTime;
$data->extend = $extend;
$data->ext = $ext;
return $this->util->sendCurlPost($wholeUrl, $data);
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php
namespace addons\qcloudsms\library;
use addons\qcloudsms\library\SmsSenderUtil;
/**
* 拉取短信状态类
*
*/
class SmsStatusPuller
{
private $url;
private $appid;
private $appkey;
private $util;
/**
* 构造函数
*
* @param string $appid sdkappid
* @param string $appkey sdkappid对应的appkey
*/
public function __construct($appid, $appkey)
{
$this->url = "https://yun.tim.qq.com/v5/tlssmssvr/pullstatus";
$this->appid = $appid;
$this->appkey = $appkey;
$this->util = new SmsSenderUtil();
}
/**
* 拉取回执结果
*
* @param int $type 拉取类型,0表示回执结果,1表示回复信息
* @param int $max 最大条数,最多100
* @return string 应答json字符串,详细内容参见腾讯云协议文档
*/
private function pull($type, $max)
{
$random = $this->util->getRandom();
$curTime = time();
$wholeUrl = $this->url . "?sdkappid=" . $this->appid . "&random=" . $random;
$data = new \stdClass();
$data->sig = $this->util->calculateSigForPuller($this->appkey, $random, $curTime);
$data->time = $curTime;
$data->type = $type;
$data->max = $max;
return $this->util->sendCurlPost($wholeUrl, $data);
}
/**
* 拉取回执结果
*
* @param int $max 拉取最大条数,最多100
* @return string 应答json字符串,详细内容参见腾讯云协议文档
*/
public function pullCallback($max)
{
return $this->pull(0, $max);
}
/**
* 拉取回复信息
*
* @param int $max 拉取最大条数,最多100
* @return string 应答json字符串,详细内容参见腾讯云协议文档
*/
public function pullReply($max)
{
return $this->pull(1, $max);
}
}
+71
View File
@@ -0,0 +1,71 @@
<?php
namespace addons\qcloudsms\library;
use addons\qcloudsms\library\SmsSenderUtil;
/**
* 发送语音通知类
*
*/
class SmsVoicePromptSender
{
private $url;
private $appid;
private $appkey;
private $util;
/**
* 构造函数
*
* @param string $appid sdkappid
* @param string $appkey sdkappid对应的appkey
*/
public function __construct($appid, $appkey)
{
$this->url = "https://yun.tim.qq.com/v5/tlsvoicesvr/sendvoiceprompt";
$this->appid = $appid;
$this->appkey = $appkey;
$this->util = new SmsSenderUtil();
}
/**
*
* 发送语音通知
*
* @param string $nationCode 国家码,如 86 为中国
* @param string $phoneNumber 不带国家码的手机号
* @param string $prompttype 语音类型,目前固定为2
* @param string $msg 信息内容,必须与申请的模板格式一致,否则将返回错误
* @param string $playtimes 播放次数,可选,最多3次,默认2次
* @param string $ext 用户的session内容,服务端原样返回,可选字段,不需要可填空串
* @return string 应答json字符串,详细内容参见腾讯云协议文档
*/
public function send($nationCode, $phoneNumber, $prompttype, $msg, $playtimes = 2, $ext = "")
{
$random = $this->util->getRandom();
$curTime = time();
$wholeUrl = $this->url . "?sdkappid=" . $this->appid . "&random=" . $random;
// 按照协议组织 post 包体
$data = new \stdClass();
$tel = new \stdClass();
$tel->nationcode = "".$nationCode;
$tel->mobile = "".$phoneNumber;
$data->tel = $tel;
// 通知内容,utf8编码,支持中文英文、数字及组合,需要和语音内容模版相匹配
$data->promptfile = $msg;
// 固定值 2
$data->prompttype = $prompttype;
$data->playtimes = $playtimes;
// app凭证
$data->sig = hash("sha256",
"appkey=".$this->appkey."&random=".$random."&time="
.$curTime."&mobile=".$phoneNumber, FALSE);
// unix时间戳,请求发起时间,如果和系统时间相差超过10分钟则会返回失败
$data->time = $curTime;
$data->ext = $ext;
return $this->util->sendCurlPost($wholeUrl, $data);
}
}
+67
View File
@@ -0,0 +1,67 @@
<?php
namespace addons\qcloudsms\library;
use addons\qcloudsms\library\SmsSenderUtil;
/**
* 发送语音验证码类
*
*/
class SmsVoiceVerifyCodeSender
{
private $url;
private $appid;
private $appkey;
private $util;
/**
* 构造函数
*
* @param string $appid sdkappid
* @param string $appkey sdkappid对应的appkey
*/
public function __construct($appid, $appkey)
{
$this->url = "https://yun.tim.qq.com/v5/tlsvoicesvr/sendvoice";
$this->appid = $appid;
$this->appkey = $appkey;
$this->util = new SmsSenderUtil();
}
/**
* 发送语音验证码
*
* @param string $nationCode 国家码,如 86 为中国
* @param string $phoneNumber 不带国家码的手机号
* @param string $msg 信息内容,必须与申请的模板格式一致,否则将返回错误
* @param int $playtimes 播放次数,可选,最多3次,默认2次
* @param string $ext 用户的session内容,服务端原样返回,可选字段,不需要可填空串
* @return string 应答json字符串,详细内容参见腾讯云协议文档
*/
public function send($nationCode, $phoneNumber, $msg, $playtimes = 2, $ext = "")
{
$random = $this->util->getRandom();
$curTime = time();
$wholeUrl = $this->url . "?sdkappid=" . $this->appid . "&random=" . $random;
// 按照协议组织 post 包体
$data = new \stdClass();
$tel = new \stdClass();
$tel->nationcode = "".$nationCode;
$tel->mobile = "".$phoneNumber;
$data->tel = $tel;
$data->msg = $msg;
$data->playtimes = $playtimes;
// app凭证
$data->sig = hash("sha256",
"appkey=".$this->appkey."&random=".$random."&time="
.$curTime."&mobile=".$phoneNumber, FALSE);
// unix时间戳,请求发起时间,如果和系统时间相差超过10分钟则会返回失败
$data->time = $curTime;
$data->ext = $ext;
return $this->util->sendCurlPost($wholeUrl, $data);
}
}
+77
View File
@@ -0,0 +1,77 @@
<?php
namespace addons\qcloudsms\library;
use addons\qcloudsms\library\SmsSenderUtil;
/**
* 指定模板发送语音通知类
*
*/
class TtsVoiceSender
{
private $url;
private $appid;
private $appkey;
private $util;
/**
* 构造函数
*
* @param string $appid sdkappid
* @param string $appkey sdkappid对应的appkey
*/
public function __construct($appid, $appkey)
{
$this->url = "https://cloud.tim.qq.com/v5/tlsvoicesvr/sendtvoice";
$this->appid = $appid;
$this->appkey = $appkey;
$this->util = new SmsSenderUtil();
}
/**
*
* 指定模板发送语音短信
*
* @param string $nationCode 国家码,如 86 为中国
* @param string $phoneNumber 不带国家码的手机号
* @param int $templId 模板 id
* @param array $params 模板参数列表,如模板 {1}...{2}...{3},需要带三个参数
* @param string $playtimes 播放次数,可选,最多3次,默认2次
* @param string $ext 用户的session内容,服务端原样返回,可选字段,不需要可填空串
* @return string 应答json字符串,详细内容参见腾讯云协议文档
*/
public function send($nationCode, $phoneNumber, $templId, $params, $playtimes = 2, $ext = "")
{
/*var_dump($nationCode);
var_dump($phoneNumber);
var_dump($templId);
var_dump($params);
var_dump($playtimes);
exit();*/
$random = $this->util->getRandom();
$curTime = time();
$wholeUrl = $this->url . "?sdkappid=" . $this->appid . "&random=" . $random;
// 按照协议组织 post 包体
$data = new \stdClass();
$tel = new \stdClass();
$tel->nationcode = "".$nationCode;
$tel->mobile = "".$phoneNumber;
$data->tel = $tel;
$data->tpl_id = $templId;
$data->params = $params;
$data->playtimes = $playtimes;
// app凭证
$data->sig = $this->util->calculateSig($this->appkey, $random,
$curTime, array($phoneNumber));
// unix时间戳,请求发起时间,如果和系统时间相差超过10分钟则会返回失败
$data->time = $curTime;
$data->ext = $ext;
//var_dump($data);exit();
return $this->util->sendCurlPost($wholeUrl, $data);
}
}
+1
View File
@@ -0,0 +1 @@
{"license":"regular","licenseto":"16556","licensekey":"SMgil3C0jFHIwQ2q m8gyQ649xwDwSnvkxcZdxGoduHQC3hVFuliavICi2t8=","files":["public\\assets\\addons\\summernote\\css\\summernote.css","public\\assets\\addons\\summernote\\font\\summernote.eot","public\\assets\\addons\\summernote\\font\\summernote.ttf","public\\assets\\addons\\summernote\\font\\summernote.woff","public\\assets\\addons\\summernote\\js\\summernote.js","public\\assets\\addons\\summernote\\js\\summernote.min.js","public\\assets\\addons\\summernote\\lang\\summernote-zh-CN.js","public\\assets\\addons\\summernote\\lang\\summernote-zh-CN.min.js","public\\assets\\addons\\summernote\\lang\\summernote-zh-TW.js","public\\assets\\addons\\summernote\\lang\\summernote-zh-TW.min.js"]}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace addons\summernote;
use think\Addons;
/**
* Summernote富文本编辑器
*/
class Summernote extends Addons
{
/**
* 插件安装方法
* @return bool
*/
public function install()
{
return true;
}
/**
* 插件卸载方法
* @return bool
*/
public function uninstall()
{
return true;
}
}
+114
View File
@@ -0,0 +1,114 @@
require.config({
paths: {
'summernote': '../addons/summernote/lang/summernote-zh-CN.min'
},
shim: {
'summernote': ['../addons/summernote/js/summernote.min', 'css!../addons/summernote/css/summernote.css'],
}
});
require(['form', 'upload'], function (Form, Upload) {
var _bindevent = Form.events.bindevent;
Form.events.bindevent = function (form) {
_bindevent.apply(this, [form]);
try {
//绑定summernote事件
if ($(".summernote,.editor", form).size() > 0) {
require(['summernote'], function () {
var imageButton = function (context) {
var ui = $.summernote.ui;
var button = ui.button({
contents: '<i class="fa fa-file-image-o"/>',
tooltip: __('Choose'),
click: function () {
parent.Fast.api.open("general/attachment/select?element_id=&multiple=true&mimetype=image/*", __('Choose'), {
callback: function (data) {
var urlArr = data.url.split(/\,/);
$.each(urlArr, function () {
var url = Fast.api.cdnurl(this);
context.invoke('editor.insertImage', url);
});
}
});
return false;
}
});
return button.render();
};
var attachmentButton = function (context) {
var ui = $.summernote.ui;
var button = ui.button({
contents: '<i class="fa fa-file"/>',
tooltip: __('Choose'),
click: function () {
parent.Fast.api.open("general/attachment/select?element_id=&multiple=true&mimetype=*", __('Choose'), {
callback: function (data) {
var urlArr = data.url.split(/\,/);
$.each(urlArr, function () {
var url = Fast.api.cdnurl(this);
var node = $("<a href='" + url + "'>" + url + "</a>");
context.invoke('insertNode', node[0]);
});
}
});
return false;
}
});
return button.render();
};
$(".summernote,.editor", form).summernote({
height: 250,
lang: 'zh-CN',
fontNames: [
'Arial', 'Arial Black', 'Serif', 'Sans', 'Courier',
'Courier New', 'Comic Sans MS', 'Helvetica', 'Impact', 'Lucida Grande',
"Open Sans", "Hiragino Sans GB", "Microsoft YaHei",
'微软雅黑', '宋体', '黑体', '仿宋', '楷体', '幼圆',
],
fontNamesIgnoreCheck: [
"Open Sans", "Microsoft YaHei",
'微软雅黑', '宋体', '黑体', '仿宋', '楷体', '幼圆'
],
toolbar: [
['style', ['style', 'undo', 'redo']],
['font', ['bold', 'underline', 'strikethrough', 'clear']],
['fontname', ['color', 'fontname', 'fontsize']],
['para', ['ul', 'ol', 'paragraph', 'height']],
['table', ['table', 'hr']],
['insert', ['link', 'picture', 'video']],
['select', ['image', 'attachment']],
['view', ['fullscreen', 'codeview', 'help']],
],
buttons: {
image: imageButton,
attachment: attachmentButton,
},
dialogsInBody: true,
followingToolbar: false,
callbacks: {
onChange: function (contents) {
$(this).val(contents);
$(this).trigger('change');
},
onInit: function () {
},
onImageUpload: function (files) {
var that = this;
//依次上传图片
for (var i = 0; i < files.length; i++) {
Upload.api.send(files[i], function (data) {
var url = Fast.api.cdnurl(data.url);
$(that).summernote("insertImage", url, 'filename');
});
}
}
}
});
});
}
} catch (e) {
}
};
});
+5
View File
@@ -0,0 +1,5 @@
<?php
return [
];
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace addons\summernote\controller;
use think\addons\Controller;
class Index extends Controller
{
public function index()
{
$this->error("当前插件暂无前台页面");
}
}
+10
View File
@@ -0,0 +1,10 @@
name = summernote
title = Summernote富文本编辑器
intro = 修改后台默认编辑器为Summernote
author = Karson
website = http://www.fastadmin.net
version = 1.0.4
state = 1
url = /addons/summernote
license = regular
licenseto = 16556
+1
View File
@@ -0,0 +1 @@
{"license":"regular","licenseto":"16556","licensekey":"HwDQbqJWIPSRsNf4 2Y6yyJiwGyLCvlxFmynb9W\/ODYiwo9+52hxRGVgN0pA="}
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace addons\translate;
use app\common\library\Menu;
use think\Addons;
/**
* 插件
*/
class Translate extends Addons
{
/**
* 插件安装方法
* @return bool
*/
public function install()
{
return true;
}
/**
* 插件卸载方法
* @return bool
*/
public function uninstall()
{
return true;
}
/**
* 插件启用方法
* @return bool
*/
public function enable()
{
return true;
}
/**
* 插件禁用方法
* @return bool
*/
public function disable()
{
return true;
}
/**
* 应用初始化
*/
public function appInit()
{
\think\Loader::addNamespace('Stichoza\GoogleTranslate', ADDON_PATH . 'translate' . DS . 'library' . DS . 'google-translate-php' . DS . 'src' . DS);
\think\Loader::addNamespace('GuzzleHttp\Psr7', ADDON_PATH . 'translate' . DS . 'library' . DS . 'guzzlehttp' . DS . 'psr7' . DS . 'src' . DS);
\think\Loader::addNamespace('GuzzleHttp', ADDON_PATH . 'translate' . DS . 'library' . DS . 'guzzlehttp' . DS. 'guzzle' . DS . 'src'. DS);
\think\Loader::addNamespace('GuzzleHttp\Promise', ADDON_PATH . 'translate' . DS . 'library' . DS . 'guzzlehttp' . DS . 'promises' . DS . 'src'. DS);
require_once ADDON_PATH . 'translate' . DS . 'library' . DS . 'google-translate-php' . DS . 'src'. DS . 'common.php';
}
}
+259
View File
@@ -0,0 +1,259 @@
<?php
return [
[
'name' => 'proxy',
'title' => '代理IP',
'type' => 'text',
'content' =>
[
],
'value' => '',
'rule' => '',
'msg' => '',
'tip' => '一行一个代理,比如:http://127.0.0.1:1080',
'ok' => '',
'extend' => '',
],
[
'name' => 'language',
'title' => '语言',
'type' => 'select',
'content' =>
[
'af' => '南非荷兰语',
'sq' => '阿尔巴尼亚语',
'am' => '阿姆哈拉语',
'ar' => '阿拉伯语',
'hy' => '亚美尼亚语',
'az' => '阿塞拜疆语',
'eu' => '巴斯克语',
'be' => '白俄罗斯语',
'bn' => '孟加拉语',
'bs' => '波斯尼亚语',
'bg' => '保加利亚语',
'ca' => '加泰罗尼亚语',
'ceb' => '宿务语',
'zh-CN' => '中文(简体)',
'zh-TW' => '中文(繁体)',
'co' => '科西嘉语',
'hr' => '克罗地亚语',
'cs' => '捷克语',
'da' => '丹麦语',
'nl' => '荷兰语',
'en' => '英语',
'eo' => '世界语',
'et' => '爱沙尼亚语',
'fi' => '芬兰语',
'fr' => '法语',
'fy' => '弗里斯兰语',
'gl' => '加利西亚语',
'ka' => '格鲁吉亚语',
'de' => '德语',
'el' => '希腊语',
'gu' => '古吉拉特语',
'ht' => '海地克里奥尔语',
'ha' => '豪萨语',
'haw' => '夏威夷语',
'he' => '希伯来语',
'hi' => '印地语',
'hmn' => '苗语',
'hu' => '匈牙利语',
'is' => '冰岛语',
'ig' => '伊博语',
'id' => '印度尼西亚语',
'ga' => '爱尔兰语',
'it' => '意大利语',
'ja' => '日语',
'jv' => '爪哇语',
'kn' => '卡纳达语',
'kk' => '哈萨克语',
'km' => '高棉文',
'ko' => '韩语',
'ku' => '库尔德语',
'ky' => '吉尔吉斯语',
'lo' => '老挝语',
'la' => '拉丁文',
'lv' => '拉脱维亚语',
'lt' => '立陶宛语',
'lb' => '卢森堡语',
'mk' => '马其顿语',
'mg' => '马尔加什语',
'ms' => '马来语',
'ml' => '马拉雅拉姆文',
'mt' => '马耳他语',
'mi' => '毛利语',
'mr' => '马拉地语',
'mn' => '蒙古文',
'my' => '缅甸语',
'ne' => '尼泊尔语',
'no' => '挪威语',
'ny' => '尼杨扎语(齐切瓦语)',
'ps' => '普什图语',
'fa' => '波斯语',
'pl' => '波兰语',
'pt' => '葡萄牙语(葡萄牙、巴西)',
'pa' => '旁遮普语',
'ro' => '罗马尼亚语',
'ru' => '俄语',
'sm' => '萨摩亚语',
'gd' => '苏格兰盖尔语',
'sr' => '塞尔维亚语',
'st' => '塞索托语',
'sn' => '修纳语',
'sd' => '信德语',
'si' => '僧伽罗语',
'sk' => '斯洛伐克语',
'sl' => '斯洛文尼亚语',
'so' => '索马里语',
'es' => '西班牙语',
'su' => '巽他语',
'sw' => '斯瓦希里语',
'sv' => '瑞典语',
'tl' => '塔加路语(菲律宾语)',
'tg' => '塔吉克语',
'ta' => '泰米尔语',
'te' => '泰卢固语',
'th' => '泰文',
'tr' => '土耳其语',
'uk' => '乌克兰语',
'ur' => '乌尔都语',
'uz' => '乌兹别克语',
'vi' => '越南语',
'cy' => '威尔士语',
'xh' => '班图语',
'yi' => '意第绪语',
'yo' => '约鲁巴语',
'zu' => '祖鲁语',
],
'value' => 'af',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'en_language',
'title' => '英文语言',
'type' => 'select',
'content' =>
[
'af' => 'Afrikaans',
'sq' => 'Albanian',
'am' => 'Amharic',
'ar' => 'Arabic',
'hy' => 'Armenian',
'az' => 'Azerbaijani',
'eu' => 'Basque',
'be' => 'Belarusian',
'bn' => 'Bengali',
'bs' => 'Bosnian',
'bg' => 'Bulgarian',
'ca' => 'Catalan',
'ceb' => 'Cebuano',
'zh-CN' => 'Chinese (Simplified)',
'zh-TW' => 'Chinese (Traditional)',
'co' => 'Corsican',
'hr' => 'Croatian',
'cs' => 'Czech',
'da' => 'Danish',
'nl' => 'Dutch',
'en' => 'English',
'eo' => 'Esperanto',
'et' => 'Estonian',
'fi' => 'Finnish',
'fr' => 'French',
'fy' => 'Frisian',
'gl' => 'Galician',
'ka' => 'Georgian',
'de' => 'German',
'el' => 'Greek',
'gu' => 'Gujarati',
'ht' => 'Haitian Creole',
'ha' => 'Hausa',
'haw' => 'Hawaiian',
'he' => 'Hebrew',
'hi' => 'Hindi',
'hmn' => 'Hmong',
'hu' => 'Hungarian',
'is' => 'Icelandic',
'ig' => 'Igbo',
'id' => 'Indonesian',
'ga' => 'Irish',
'it' => 'Italian',
'ja' => 'Japanese',
'jv' => 'Javanese',
'kn' => 'Kannada',
'kk' => 'Kazakh',
'km' => 'Khmer',
'rw' => 'Kinyarwanda',
'ko' => 'Korean',
'ku' => 'Kurdish',
'ky' => 'Kyrgyz',
'lo' => 'Lao',
'la' => 'Latin',
'lv' => 'Latvian',
'lt' => 'Lithuanian',
'lb' => 'Luxembourgish',
'mk' => 'Macedonian',
'mg' => 'Malagasy',
'ms' => 'Malay',
'ml' => 'Malayalam',
'mt' => 'Maltese',
'mi' => 'Maori',
'mr' => 'Marathi',
'mn' => 'Mongolian',
'my' => 'Myanmar (Burmese)',
'ne' => 'Nepali',
'no' => 'Norwegian',
'ny' => 'Nyanja (Chichewa)',
'or' => 'Odia (Oriya)',
'ps' => 'Pashto',
'fa' => 'Persian',
'pl' => 'Polish',
'pt' => 'Portuguese (Portugal, Brazil)',
'pa' => 'Punjabi',
'ro' => 'Romanian',
'ru' => 'Russian',
'sm' => 'Samoan',
'gd' => 'Scots Gaelic',
'sr' => 'Serbian',
'st' => 'Sesotho',
'sn' => 'Shona',
'sd' => 'Sindhi',
'si' => 'Sinhala (Sinhalese)',
'sk' => 'Slovak',
'sl' => 'Slovenian',
'so' => 'Somali',
'es' => 'Spanish',
'su' => 'Sundanese',
'sw' => 'Swahili',
'sv' => 'Swedish',
'tl' => 'Tagalog (Filipino)',
'tg' => 'Tajik',
'ta' => 'Tamil',
'tt' => 'Tatar',
'te' => 'Telugu',
'th' => 'Thai',
'tr' => 'Turkish',
'tk' => 'Turkmen',
'uk' => 'Ukrainian',
'ur' => 'Urdu',
'ug' => 'Uyghur',
'uz' => 'Uzbek',
'vi' => 'Vietnamese',
'cy' => 'Welsh',
'xh' => 'Xhosa',
'yi' => 'Yiddish',
'yo' => 'Yoruba',
'zu' => 'Zulu',
],
'value' => 'af',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
];
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace addons\translate\controller;
use think\addons\Controller;
class Index extends Controller
{
public function index()
{
$languageList = get_addon_fullconfig('translate');
$languageList = $languageList[1]['content'];
$this->view->assign('postUrl', addon_url('translate/index/translate'));
$this->view->assign('languageList', json_encode($languageList));
return $this->view->fetch();
}
public function translate()
{
$source = $this->request->post("source");
$sourceLang = $this->request->post("source_lang");
$targetLang = $this->request->post("target_lang");
$res = doGoogleTranslate($source, $targetLang, $sourceLang, true);
return json_encode($res);
}
}
+10
View File
@@ -0,0 +1,10 @@
name = translate
title = 谷歌自动翻译
intro = 免费的谷歌翻译插件,可以自动识别语种 自动翻译成所有指定的语种 支持swoole协程并行翻译
author = proxy
website = https://www.fastadmin.net
version = 1.0.0
state = 1
url = /addons/translate
license = regular
licenseto = 16556

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