This commit is contained in:
weiz 2023-09-26 18:09:46 +08:00
commit 22d166de26
4030 changed files with 527708 additions and 0 deletions

8
.gitignore vendored Executable file
View File

@ -0,0 +1,8 @@
.idea
.code
public/nginx.htaccess
runtime/*
vendor/workerman/*.log
vendor/workerman/*.pid
.env
start_for_win.bat

32
LICENSE.txt Executable file
View File

@ -0,0 +1,32 @@
ThinkPHP遵循Apache2开源协议发布并提供免费使用。
版权所有Copyright © 2006-2016 by ThinkPHP (http://thinkphp.cn)
All rights reserved。
ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。
Apache Licence是著名的非盈利开源组织Apache采用的协议。
该协议和BSD类似鼓励代码共享和尊重原作者的著作权
允许代码修改,再作为开源或商业软件发布。需要满足
的条件:
1 需要给代码的用户一份Apache Licence
2 如果你修改了代码,需要在被修改的文件中说明;
3 在延伸的代码中(修改和有源代码衍生的代码中)需要
带有原来代码中的协议,商标,专利声明和其他原来作者规
定需要包含的说明;
4 如果再发布的产品中包含一个Notice文件则在Notice文
件中需要带有本协议内容。你可以在Notice中增加自己的
许可但不可以表现为对Apache Licence构成更改。
具体的协议参考http://www.apache.org/licenses/LICENSE-2.0
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

36
README.en.md Executable file
View File

@ -0,0 +1,36 @@
# IM即时聊天
#### Description
im后端代码需要配合前端使用
#### Software Architecture
Software architecture description
#### Installation
1. xxxx
2. xxxx
3. xxxx
#### Instructions
1. xxxx
2. xxxx
3. xxxx
#### Contribution
1. Fork the repository
2. Create Feat_xxx branch
3. Commit your code
4. Create Pull Request
#### Gitee Feature
1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
4. The most valuable open source project [GVP](https://gitee.com/gvp)
5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

192
README.md Executable file
View File

@ -0,0 +1,192 @@
# IM即时聊天
#### 介绍
Raingad-IM是一个开源的即时通信demo需要前后端配合使用主要用于学习交流为大家提供即时通讯的开发思路许多功能需要自行开发开发的初衷旨在快速建立企业内部通讯系统、内网交流、社区交流。
| 类型 | 链接 |
| --------- | ---- |
| 前端源码 | https://gitee.com/raingad/im-chat-front |
| 后端源码 | https://gitee.com/raingad/im-instant-chat |
| web端演示 | http://im.raingad.com/index.html |
| 移动端H5演示 | http://im.raingad.com/h5 |
| 安卓APP演示 | https://emoji.raingad.com/file/raingad.apk |
体验账号13800000002 密码123456
尾号2、3、4......18、19、20 都是
体验账号13800000020 密码123456
#### 支持功能
1. 支持单聊和群聊,支持发送表情、图片、语音、视频和文件消息
2. 单聊支持消息已读未读的状态显示,在线状态显示
3. 群聊创建、删除和群成员管理、群公告、群禁言等
4. 支持置顶联系人,消息免打扰;支持设置新消息声音提醒,浏览器通知
5. 支持一对一音视频通话已打通web端和移动端
6. 支持文件、图片和绝大部分媒体文件在线预览
7. 支持移动端H5、APP和小程序部分功能不兼容支持简易后台管理
8. 全新支持企业模式和社区模式,社区模式支持注册、添加好友功能
#### 最新更新
**v-3.0.0** (2023年9月4日)
<font color="red">不兼容之前的版本!!!</font>
1. 全新升级,不兼容之前版本.
2. 修改登录方式新增JWT token登录不再服务器缓存登录信息节约服务器资源。
3. 新增聊天内容加密,聊天数据更安全,请仔细阅读 `.example.env` 里面的配置.
4. 消息推送服务可以自定义端口,已集成到环境配置文件中 `.example.env` 仅支持linuxwindow请按照原来的方式操作。
5. 新增websocket连接认证未认证会被断开连接。
6. 新增群二维码,移动端可以扫码加群。暂未实现聊天中长按图片识别二维码加群。
7. 修复聊天记录重复加载的情况。
8. 修复历史消息不会实时置为已读的情况。
9. 优化若干样式和bug
**v-2.8.25** (2023年8月25日)
1. 优化移动端的websocket连接
2. 移动端新增记住密码
3. 优化消息接收的处理以及上下线状态
4. 移动端完善用户资料修改,密码修改等
5. 打通移动端和web端的音视频互通有bug但是可以忽略已开源web端音视频源码
6. 新增音视频通话消息类型
**v-2.8.1** (2023年8月1日)
1. 移动端新增消息撤回、复制消息的发送状态1对1消息已读、未读状态
2. 移动端修复聊天页面中,消息不能滚动到底部的情况
3. 移动端新增音视频通话引导用户获取音视频权限
4. web端新增截屏功能功能用的第三方插件虽然有瑕疵但是很强大
5. 优化websocket掉线问题增加客户端主动心跳
6. 优化web端浏览器通知
7. 移动端支持社区模式,加好友等功能
8. 新增邮件加密方式选择
9. 兼容php7.4
10. 修复element-ui图标有时候加载乱码
**v-2.7.14** (2023年7月14日)
1. 支持简易后台管理
2. 全新支持企业模式和社区模式,可自由切换
3. 新增阿里云、七牛云、腾讯云等对象储存
4. 新增群头像自动生成
5. 新增人员资料查看
6. 新增文件管理,可以快速发送到聊天。
7. 新增移动端1对1音视频通话不和web端互通
**v-2.5.30** (2023年5月20日)
1. 新增windows系统的支持建议windows仅用于开发环境正式环境请使用linux。
2. 新增企业模式下全局发送消息的演示页面。
**v-1.10.30** (2022年10月30日)
1. 升级vue-cli2到vue-cli3
2. 优化发送按键和换行键
3. 新增语音消息、视频消息
4. 新增支持音视频通话peerjs
5. 使用sass依赖替代node-sass这东西太坑了
**v-0.1.0** (2021年04月24日)
1. 项目建立,只是有项目需要,才开发了这个应用
#### 软件架构
后端技术栈:`thinkphp6+workerman+redis`
前端技术栈:`vue2+Lemon-IMUI+element-UI`
#### 安装教程
##### 源码下载
- 克隆代码到本地:
```
git clone https://gitee.com/raingad/im-instant-chat.git
```
- 进入项目目录,执行:
```
composer install
```
或者
- 下载完整源码放到自己的服务器上。请注意看gitee项目主页顶部 `右侧的发行版`,请在发行版中下载最新发布的版本。
##### 开始安装
1. 开启伪静态下面只展示nginx的Apache的已内置
```
location ~* (runtime|application)/{
return 403;
}
location / {
if (!-e $request_filename){
rewrite ^(.*)$ /index.php?s=$1 last; break;
}
}
```
3. 访问你的ip或者域名即可进入自定义安装向导。
4. 先参考下一章“启动消息推送服务”,再来安装程序最佳。
ps必须要取消禁用的函数 `shell_exec` `proc_open` `pcntl_exec`。其它的根据调试情况来取消禁用
##### 如果安装失败
1. 进入 `public\sql\database.sql` 将数据库导入自己的数据库。
2. 进入项目根目录,修改 `example.env``.env` ,并修改数据库相应的参数,**请仔细阅读env中的配置说明**。
PS如需开启聊天文件存入oss需要在后台中进行配置配置后不要再对环境配置文件进行修改。
#### 启动消息推送服务
因为是聊天软件需要用到websockt所以我们需要启动workerman系统已经内置了相应的服务可以在后台管理首页进行运行服务但是首次使用需要先进行调试。
1. 进入项目根目录 运行 `php think worker:gateway start -d`,或者运行 `php start.php start -d` 即可运行消息服务,测试时不要`-d`。windows下请直接运行根目录下的`start_for_win.bat`文件由于Workerman在Windows下有诸多使用限制所以正式环境建议用Linux系统windows系统仅建议用于开发环境。
2. 消息服务需要放行 8282 端口,如需修改,请修改环境噢配置文件中`WORKEER` 板块的相应参数。windows用户请修改 [ `app\worker\start_gateway.php`] 中的 8282 端口。端口号根据情况需改如果修改了端口号需要将前端的程序修改并重新打包上传到项目的public目录下。
3. 系统采用直接用域名作为websocket服务的地址所以监听端口需要在网站的nginx中配置代理并监听8282端口。
```
location /wss
{
proxy_pass http://127.0.0.1:8282;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header X-Real-IP $remote_addr;
}
```
4. 更多关于workerman的使用请进入[workerman官网](https://www.workerman.net/)官网进行查阅。
5. 部署完成之后管理员账号密码为:`administrator` `123456`,管理入口在聊天界面的左下角。
#### 安装部署服务
服务器要求:
| 所需环境 | 版本 | 备注 |
| --------- | ---- | ---- |
| linux | >= 7.0 | 以下的版本未做测试 |
| php | >= 7.1 | 不兼容8 |
| mysql | >= 5.7 | 必须要5.7及以上 |
| redis | >= 5.0 | |
| workerman | >= 4.0 | 用于消息服务部署 |
作者提供本系统的安装服务包括后端和前端部署到线上保证项目的完美运行200元/次安装服务可赠送web端音视频通话源码如有需要可以进群联系作者
#### 交流群
如果有什么问题请留言或者加入我们的QQ群
创作不易点个star吧
[QQ 交流群336921267](https://jq.qq.com/?_wv=1027&k=jMQAt9lh)

1
app/.htaccess Executable file
View File

@ -0,0 +1 @@
deny from all

22
app/AppService.php Executable file
View File

@ -0,0 +1,22 @@
<?php
declare (strict_types = 1);
namespace app;
use think\Service;
/**
* 应用服务类
*/
class AppService extends Service
{
public function register()
{
// 服务注册
}
public function boot()
{
// 服务启动
}
}

141
app/BaseController.php Executable file
View File

@ -0,0 +1,141 @@
<?php
declare (strict_types = 1);
namespace app;
use think\App;
use think\exception\ValidateException;
use think\Validate;
use app\manage\model\{Config};
use think\facade\Cache;
use thans\jwt\facade\JWTAuth;
/**
* 控制器基础类
*/
abstract class BaseController
{
/**
* Request实例
* @var \think\Request
*/
protected $request;
/**
* 应用实例
* @var \think\App
*/
protected $app;
/**
* 是否批量验证
* @var bool
*/
protected $batchValidate = false;
/**
* 控制器中间件
* @var array
*/
protected $middleware = [];
/**
* 是否批量验证
* @var bool
*/
protected $userInfo = [];
/**
* 接收的post数据
* @var bool
*/
protected $postData = [];
protected $uid = 0;
protected $globalConfig = [];
protected $chatSetting = [];
/**
* 构造方法
* @access public
* @param App $app 应用对象
*/
public function __construct(App $app)
{
$this->app = $app;
$this->request = $this->app->request;
// 控制器初始化
$this->initialize();
}
// 初始化
protected function initialize()
{
$this->userInfo=$this->request->userInfo;
$this->uid=$this->userInfo['user_id'] ?? 0;
$config=Config::getSystemInfo();
if($config){
$this->globalConfig = $config;
$this->chatSetting = $config['chatInfo'] ?? [];
}
// 验证版本,如果不一致,就需要退出重新登陆
$version =config('app.app_version');
$oldVersion=Cache::get('app_version');
if($version!=$oldVersion){
Cache::set('app_version',$version);
JWTAuth::refresh();
Cache::delete('systemInfo');
}
}
/**
* 验证数据
* @access protected
* @param array $data 数据
* @param string|array $validate 验证器名或者验证规则数组
* @param array $message 提示信息
* @param bool $batch 是否批量验证
* @return array|string|true
* @throws ValidateException
*/
protected function validate(array $data, $validate, array $message = [], bool $batch = false)
{
if (is_array($validate)) {
$v = new Validate();
$v->rule($validate);
} else {
if (strpos($validate, '.')) {
// 支持场景
[$validate, $scene] = explode('.', $validate);
}
$class = false !== strpos($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate);
$v = new $class();
if (!empty($scene)) {
$v->scene($scene);
}
}
$v->message($message);
// 是否批量验证
if ($batch || $this->batchValidate) {
$v->batch(true);
}
return $v->failException(true)->check($data);
}
/**
* 自动获取前端传递的分页数量
* @param \think\Model|\think\model\relation\HasMany $model
* @return \think\Paginator
*/
protected function paginate($model)
{
$limit = $this->request->param('limit', 20);
return $model->paginate($limit);
}
}

103
app/BaseModel.php Executable file
View File

@ -0,0 +1,103 @@
<?php
/**
* Created by PhpStorm
* User xiekunyu@kaishanlaw.com
* Date 2021/7/9 16:15
*/
namespace app;
use think\facade\Db;
use think\Model;
class BaseModel extends Model
{
protected $defaultSoftDelete = 0;
protected $error = '';
protected static $db_prefix = 'yu_';
protected static $userInfo = null;
protected static $uid = null;
protected static function init()
{
self::$db_prefix = config('database.connections.mysql.prefix') ?: "yu_";
self::initModel();
}
// 加载模型自动处理
public static function initModel()
{
self::$userInfo=request()->userInfo ?? null;
self::$uid=request()->userInfo['user_id'] ?? null;
}
/**
* 获取树状信息
* @param array $config
*/
public static function getCheckNode($arr, $pid, $field = "parent_id", $table = '')
{
if (!$table) {
$res = self::find($pid);
} else {
$res = Db::name($table)->find($pid);
}
if ($res) {
if ($res[$field] > 0) {
array_unshift($arr, $res[$field]);
return self::getCheckNode($arr, $res[$field], $field, $table);
}
}
return $arr;
}
// 获取错误信息
public function getError()
{
return $this->error;
}
/**
* 获取模型的json字段数组
* @return array
*/
public function getJsonFieldName(): array
{
return $this->json;
}
// 匹配列表信息
public static function filterIdr($data, $many, $field)
{
if ($many) {
$idr = \utils\Arr::arrayToString($data, $field, false);
} else {
$idr = [];
if (is_array($field)) {
foreach ($field as $v) {
$idr[] = $data[$v];
}
} else {
$idr = [$data[$field]];
}
}
$key = array_search(0, $idr);
if ($key) {
array_splice($idr, $key, 1);
}
$idr = array_unique($idr);
return $idr ? : [];
}
// 获取某一项数据的统计
public static function getTotal($map,$where=[],$field,$group){
return self::field($field)
->where($map)
->where($where)
->group($group)
->select()->toArray();
}
}

58
app/ExceptionHandle.php Executable file
View File

@ -0,0 +1,58 @@
<?php
namespace app;
use think\db\exception\DataNotFoundException;
use think\db\exception\ModelNotFoundException;
use think\exception\Handle;
use think\exception\HttpException;
use think\exception\HttpResponseException;
use think\exception\ValidateException;
use think\Response;
use Throwable;
/**
* 应用异常处理类
*/
class ExceptionHandle extends Handle
{
/**
* 不需要记录信息(日志)的异常类列表
* @var array
*/
protected $ignoreReport = [
HttpException::class,
HttpResponseException::class,
ModelNotFoundException::class,
DataNotFoundException::class,
ValidateException::class,
];
/**
* 记录异常信息(包括日志或者其它方式记录)
*
* @access public
* @param Throwable $exception
* @return void
*/
public function report(Throwable $exception): void
{
// 使用内置的方式记录异常日志
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* @access public
* @param \think\Request $request
* @param Throwable $e
* @return Response
*/
public function render($request, Throwable $e): Response
{
// 添加自定义异常处理机制
// 其他错误交给系统处理
return parent::render($request, $e);
}
}

8
app/Request.php Executable file
View File

@ -0,0 +1,8 @@
<?php
namespace app;
// 应用请求对象类
class Request extends \think\Request
{
}

872
app/common.php Executable file
View File

@ -0,0 +1,872 @@
<?php
// 应用公共文件
use SingKa\Sms\SkSms;
use GatewayClient\Gateway;
use \utils\Str;
/**
* 框架内部默认ajax返回
* @param string $msg 提示信息
* @param string $redirect 重定向类型 current|parent|''
* @param string $alert 父层弹框信息
* @param bool $close 是否关闭当前层
* @param string $url 重定向地址
* @param string $data 附加数据
* @param int $code 错误码
* @param array $extend 扩展数据
* @param int $count 总数
*/
function success($msg = '操作成功', $data = '', $count = 0, $page = 1, $code = 0)
{
return ret($code, $msg, $data, $count, $page);
}
/**
* 返回警告json信息
*/
function warning($msg = '操作失败', $data = '', $count = 0, $page = 1 , $code = 400)
{
return success($msg, $data, $count, $page, $code);
}
/**
* 返回错误json信息
*/
function error($msg = '操作失败', $code = 502)
{
return ret($code, '系统错误:'.$msg);
}
/**
* 提前终止信息
*/
function shutdown($msg = '禁止访问', $code = 401)
{
exit(json_encode(['code' => $code, 'msg' => $msg, 'data' => []]));
}
/**
* ajax数据返回规范格式
* @param array $data 返回的数据,默认空数组
* @param string $msg 信息
* @param int $code 错误码0-未出现错误|其他出现错误
* @param array $extend 扩展数据
*/
function ret($code, $msg = "",$data = [],$count=0, $page=0)
{
$ret = ["code" =>$code, "msg" => $msg,'count'=>$count, "data" => $data,'page'=>$page];
return json($ret);
}
/* @param string $string 原文或者密文
* @param string $operation 操作(ENCODE | DECODE), 默认为 DECODE
* @param string $key 密钥
* @param int $expiry 密文有效期, 加密时候有效, 单位 0 为永久有效
* @return string 处理后的 原文或者 经过 base64_encode 处理后的密文
*
* @example
*
* $a = authcode('abc', 'ENCODE', 'key');
* $b = authcode($a, 'DECODE', 'key'); // $b(abc)
*
* $a = authcode('abc', 'ENCODE', 'key', 3600);
* $b = authcode('abc', 'DECODE', 'key'); // 在一个小时内,$b(abc),否则 $b 为空
*/
function authcode($string, $operation = 'DECODE', $key = '', $expiry = 3600) {
$ckey_length = 4;
// 随机密钥长度 取值 0-32;
// 加入随机密钥,可以令密文无任何规律,即便是原文和密钥完全相同,加密结果也会每次不同,增大破解难度。
// 取值越大,密文变动规律越大,密文变化 = 16 的 $ckey_length 次方
// 当此值为 0 时,则不产生随机密钥
$key = md5($key ? $key : 'default_key'); //这里可以填写默认key值
$keya = md5(substr($key, 0, 16));
$keyb = md5(substr($key, 16, 16));
$keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : '';
$cryptkey = $keya.md5($keya.$keyc);
$key_length = strlen($cryptkey);
$string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;
$string_length = strlen($string);
$result = '';
$box = range(0, 255);
$rndkey = array();
for($i = 0; $i <= 255; $i++) {
$rndkey[$i] = ord($cryptkey[$i % $key_length]);
}
for($j = $i = 0; $i < 256; $i++) {
$j = ($j + $box[$i] + $rndkey[$i]) % 256;
$tmp = $box[$i];
$box[$i] = $box[$j];
$box[$j] = $tmp;
}
for($a = $j = $i = 0; $i < $string_length; $i++) {
$a = ($a + 1) % 256;
$j = ($j + $box[$a]) % 256;
$tmp = $box[$a];
$box[$a] = $box[$j];
$box[$j] = $tmp;
$result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
}
if($operation == 'DECODE') {
if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {
return substr($result, 26);
} else {
return '';
}
} else {
return $keyc.str_replace('=', '', base64_encode($result));
}
}
function ssoTokenEncode($str,$key='lvzhesso',$expire=0){
$ids=encryptIds($str);
return authcode($ids,"ENCODE",$key,$expire);
}
function ssoTokenDecode($str,$key='lvzhesso'){
$ids=authcode($str,"DECODE",$key);
try{
return decryptIds($ids);
}catch(\Exception $e){
return '';
}
}
//id加密
function encryptIds($str)
{
$hash = config('hashids');
return \Hashids\Hashids::instance($hash['length'], $hash['salt'])->encode($str);
}
//id解密
function decryptIds($str)
{
$hash = config('hashids');
return \Hashids\Hashids::instance($hash['length'], $hash['salt'])->decode($str);
}
/**
* 短信发送示例
*
* @mobile 短信发送对象手机号码
* @action 短信发送场景,会自动传入短信模板
* @parme 短信内容数组
*/
function sendSms($mobile, $action, $parme)
{
$config = config('sms');
//$this->SmsDefaultDriver是从数据库中读取的短信默认驱动
$driver = $config['driver'] ?: 'aliyun';
$conf=$config[$driver];
$sms = new SkSms($driver, $conf);//传入短信驱动和配置信息
//判断短信发送驱动,非阿里云和七牛云,需将内容数组主键序号化
if ($driver == 'aliyun') {
$result = $sms->$action($mobile, $parme);
} elseif ($driver == 'qiniu') {
$result = $sms->$action([$mobile], $parme);
} elseif ($driver == 'upyun') {
$result = $sms->$action($mobile, implode('|', restoreArray($parme)));
} else {
$result = $sms->$action($mobile, restoreArray($parme));
}
if ($result['code'] == 200) {
$data['code'] = 200;
$data['msg'] = '短信发送成功';
} else {
$data['code'] = $result['code'];
$data['msg'] = $result['msg'];
}
return $data;
}
/**
* 数组主键序号化
*
* @arr 需要转换的数组
*/
function restoreArray($arr)
{
if (!is_array($arr)){
return $arr;
}
$c = 0;
$new = [];
foreach ($arr as $key => $value) {
$new[$c] = $value;
$c++;
}
return $new;
}
//密码生成规则
function password_hash_tp($password,$salt)
{
return md5($salt.$password.$salt);
}
// 获取url中的主机名
function getHost($url){
if(!preg_match('/http[s]:\/\/[\w.]+[\w\/]*[\w.]*\??[\w=&\+\%]*/is',$url)){
return '';
}
$search = '~^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?~i';
$url = trim($url);
preg_match_all($search, $url ,$rr);
return $rr[4][0];
}
//根据姓名画头像
function circleAvatar($str,$s,$uid=0,$is_save=0,$save_path=''){
//定义输出为图像类型
header("content-type:image/png");
$str =$str?:"律者";
$uid =$uid?:rand(0,10);
$text=\utils\Str::getLastName($str,2);
$width = $height = $s?:80;
if($width<40 or $width>120){
$width = $height =80;
}
$colors=['#F56C6C','#E6A23C','#fbbd08','#67C23A','#39b54a','#1cbbb4','#409EFF','#6739b6','#e239ff','#e03997'];
$color=hex2rgb($colors[(int)$uid%10]);
$size=$width/4;
$textLeft=($height/2)-$size-$width/10;
if($width<=80){
$text=\utils\Str::getLastName($str,1);
$size=$width/2;
$textLeft=$size/3;
}
//新建图象
$pic=imagecreate($width,$height);
//定义黑白颜色
$background=imagecolorallocate($pic,$color['r'],$color['g'],$color['b']);
$textColor=imagecolorallocate($pic,255,255,255);
imagefill($pic,0,0,$background);//填充背景色
//定义字体
$font=root_path()."/public/static/fonts/PingFangHeavy.ttf";
//写 TTF 文字到图中
imagettftext($pic,$size,0,$textLeft,($height/2)+$size/2,$textColor,$font,$text);
if($is_save){
$path=$save_path."/".$uid.".png";
$dir = pathinfo($path,PATHINFO_DIRNAME);
if(!is_dir($dir)){
$file_create_res = mkdir($dir,0777,true);
if(!$file_create_res){
return false;//没有创建成功
}
}
imagepng($pic,$path);
imagedestroy($pic);
return $path;
}else{
//输出图象
imagepng($pic);
//结束图形,释放内存空间
imagedestroy($pic);
return $pic;
}
}
//头像拼接
function avatarUrl($path, $str = "",$uid=0,$s=80)
{
$str = Str::strFilter($str);
if ($path) {
// 判断头像路径中是否有http
if (strpos($path, 'http') !== false) {
$url = $path;
} else {
$url = getDiskUrl() .'/'. ltrim($path,'/') ;
}
}else {
if($str){
$url=request()->domain()."/avatar/".$str.'/'.$s.'/'.$uid;
}else{
$url='';
}
}
return $url;
}
// 获取文件的地址
function getFileUrl($path){
return getDiskUrl() .'/'. ltrim($path,'/') ;
}
/**
* 十六进制 RGB
*/
function hex2rgb($hexColor)
{
$color = str_replace('#', '', $hexColor);
if (strlen($color) > 3) {
$rgb = array(
'r' => hexdec(substr($color, 0, 2)),
'g' => hexdec(substr($color, 2, 2)),
'b' => hexdec(substr($color, 4, 2))
);
} else {
$color = $hexColor;
$r = substr($color, 0, 1) . substr($color, 0, 1);
$g = substr($color, 1, 1) . substr($color, 1, 1);
$b = substr($color, 2, 1) . substr($color, 2, 1);
$rgb = array(
'r' => hexdec($r),
'g' => hexdec($g),
'b' => hexdec($b)
);
}
return $rgb;
}
/**
* 将数组按字母A-Z排序
* @return [type] [description]
*/
function chartSort($array, $field,$isGroup=true,$chart='chart')
{
$newArray = [];
foreach ($array as $k => &$v) {
$v[$chart] = getFirstChart($v[$field]);
$newArray[] = $v;
}
$data = [];
if($isGroup){
foreach ($newArray as $k => $v) {
if (array_key_exists($v[$chart], $data)) {
$data[$v[$chart]][] = $v;
} else {
$data[$v[$chart]] = [];
$data[$v[$chart]][] = $v;
}
}
ksort($data);
}else{
return $newArray;
}
return $data;
}
/**
* 返回取汉字的第一个字的首字母
* @param [type] $str [string]
* @return [type] [strind]
*/
function getFirstChart($str)
{
$str = str_replace(' ', '', $str);
if (empty($str)) {
return '#';
}
$char = ord($str[0]);
if ($char >= ord('A') && $char <= ord('z')) {
return strtoupper($str[0]);
}
$s1 = iconv('UTF-8', 'gb2312//IGNORE', $str);
$s2 = iconv('gb2312', 'UTF-8//IGNORE', $s1);
$s = $s2 == $str ? $s1 : $str;
$asc = ord($s[0]) * 256 + ord($s[1]) - 65536;
if ($asc >= -20319 && $asc <= -20284) return 'A';
if ($asc >= -20283 && $asc <= -19776) return 'B';
if ($asc >= -19775 && $asc <= -19219) return 'C';
if ($asc >= -19218 && $asc <= -18711) return 'D';
if ($asc >= -18710 && $asc <= -18527) return 'E';
if ($asc >= -18526 && $asc <= -18240) return 'F';
if ($asc >= -18239 && $asc <= -17923) return 'G';
if ($asc >= -17922 && $asc <= -17418) return 'H';
if ($asc >= -17417 && $asc <= -16475) return 'J';
if ($asc >= -16474 && $asc <= -16213) return 'K';
if ($asc >= -16212 && $asc <= -15641) return 'L';
if ($asc >= -15640 && $asc <= -15166) return 'M';
if ($asc >= -15165 && $asc <= -14923) return 'N';
if ($asc >= -14922 && $asc <= -14915) return 'O';
if ($asc >= -14914 && $asc <= -14631) return 'P';
if ($asc >= -14630 && $asc <= -14150) return 'Q';
if ($asc >= -14149 && $asc <= -14091) return 'R';
if ($asc >= -14090 && $asc <= -13319) return 'S';
if ($asc >= -13318 && $asc <= -12839) return 'T';
if ($asc >= -12838 && $asc <= -12557) return 'W';
if ($asc >= -12556 && $asc <= -11848) return 'X';
if ($asc >= -11847 && $asc <= -11056) return 'Y';
if ($asc >= -11055 && $asc <= -10247) return 'Z';
return "#";
}
// 拼接聊天对象
function chat_identify($from_user,$to_user){
$identify=[$from_user,$to_user];
sort($identify);
return implode('-',$identify);
}
//数组中获取ID字符串
function arrayToString($array,$field,$isStr=true){
$idArr = [];
foreach ($array as $k => $v) {
if(is_array($field)){
foreach($field as $val){
$idArr[]=$v[$val];
}
}else{
$idArr[] = $v[$field];
}
}
if ($isStr) {
$idStr = implode(',', $idArr);
return $idStr;
} else {
return $idArr;
}
}
// 根据文件后缀进行分类
function getFileType($ext,$rst=false){
$ext=strtolower($ext);
$image=['jpg','jpeg','png','bmp','gif'];
$radio=['mp3','wav','wmv','amr'];
$video=['mp4','3gp','avi','m2v','mkv','mov'];
$doc=['ppt','pptx','doc','docx','xls','xlsx','pdf','txt','md'];
$msgType='file';
if(in_array($ext,$doc)){
$fileType=1;
}elseif(in_array($ext,$image)){
$fileType=2;
$msgType='image';
}elseif(in_array($ext,$radio)){
$fileType=3;
$msgType='voice';
}elseif(in_array($ext,$video)){
$fileType=4;
$msgType='video';
}else{
$fileType=9;
}
if($rst){
return $msgType;
}else{
return $fileType;
}
}
/**
* 二位数组排序
* $array 需要排序的数组
* $sort_key 需要排序的字段
* $sort_order 正序还是倒序
* $sort_type 排序的类型:数字,字母
*/
function sortArray($arrays, $sort_key, $sort_order = SORT_ASC, $sort_type = SORT_NUMERIC)
{
if (is_array($arrays)) {
foreach ($arrays as $array) {
if (is_array($array)) {
$key_arrays[] = $array[$sort_key];
} else {
return false;
}
}
} else {
return false;
}
array_multisort($key_arrays, $sort_order, $sort_type, $arrays);
return $arrays;
}
//gateway向web页面推送消息
function wsSendMsg($user, $type, $data, $isGroup=0)
{
$message = json_encode([
'type' => $type,
'time' => time(),
'data' => $data
]);
try{
Gateway::$registerAddress = config('gateway.registerAddress');
if (!$user) {
Gateway::sendToAll($message);
} else {
if (!$isGroup) {
$send = 'sendToUid';
} else {
$send = "sendToGroup";
}
Gateway::$send($user, $message);
}
}catch(\Exception $e){
//忽略错误
}
}
// 预览文件
function previewUrl($url){
$previewConf=config('preview');
$preview='';
$suffix=explode('.',$url);
$ext=$suffix[count($suffix)-1];
$media=['jpg','jpeg','png','bmp','gif','pdf','mp3','wav','wmv','amr','mp4','3gp','avi','m2v','mkv','mov','webp'];
$doc=['ppt','pptx','doc','docx','xls','xlsx','pdf'];
if(in_array($ext,$media) && $previewConf['own']){
$preview=$previewConf['own']."view.html?src=".$url;
}elseif(in_array($ext,$doc) && $previewConf['yzdcs']){
$preview=$previewConf['yzdcs'].'?k='.$previewConf['keycode'].'&url='.$url;
}else{
$preview=rtrim(request()->domain(),'/')."/view.html?src=".$url;
}
return $preview;
}
/**
* 解析sql语句
* @param string $content sql内容
* @param int $limit 如果为1则只返回一条sql语句默认返回所有
* @param array $prefix 替换表前缀
* @return array|string 除去注释之后的sql语句数组或一条语句
*/
function parse_sql($sql = '', $limit = 0, $prefix = []) {
// 被替换的前缀
$from = '';
// 要替换的前缀
$to = '';
// 替换表前缀
if (!empty($prefix)) {
$to = current($prefix);
$from = current(array_flip($prefix));
}
if ($sql != '') {
// 纯sql内容
$pure_sql = [];
// 多行注释标记
$comment = false;
// 按行分割,兼容多个平台
$sql = str_replace(["\r\n", "\r"], "\n", $sql);
$sql = explode("\n", trim($sql));
// 循环处理每一行
foreach ($sql as $key => $line) {
// 跳过空行
if ($line == '') {
continue;
}
// 跳过以#或者--开头的单行注释
if (preg_match("/^(#|--)/", $line)) {
continue;
}
// 跳过以/**/包裹起来的单行注释
if (preg_match("/^\/\*(.*?)\*\//", $line)) {
continue;
}
// 多行注释开始
if (substr($line, 0, 2) == '/*') {
$comment = true;
continue;
}
// 多行注释结束
if (substr($line, -2) == '*/') {
$comment = false;
continue;
}
// 多行注释没有结束,继续跳过
if ($comment) {
continue;
}
// 替换表前缀
if ($from != '') {
$line = str_replace('`'.$from, '`'.$to, $line);
}
if ($line == 'BEGIN;' || $line =='COMMIT;') {
continue;
}
// sql语句
array_push($pure_sql, $line);
}
// 只返回一条语句
if ($limit == 1) {
return implode("",$pure_sql);
}
// 以数组形式返回sql语句
$pure_sql = implode("\n",$pure_sql);
$pure_sql = explode(";\n", $pure_sql);
return $pure_sql;
} else {
return $limit == 1 ? '' : [];
}
}
/**
* 更新或添加环境变量
*
* @param string $key 环境变量的键
* @param string $value 环境变量的值
* @return bool 成功返回 true,失败返回 false
*/
function updateEnv($key, $value)
{
$envFile = app()->getRootPath() . '.env';
if (!file_exists($envFile) || !is_writable($envFile)){
return false;
}
// 读取 .env 文件内容
$envContent = file_get_contents($envFile);
$keyPattern = preg_quote($key, '/');
$pattern = "/^{$keyPattern}=(.*)\$/m";
if (preg_match($pattern, $envContent)) {
// 如果找到了键值对,替换其值
$replacement = "{$key}={$value}";
$newEnvContent = preg_replace($pattern, $replacement, $envContent);
} else {
// 如果没有找到键值对,添加新的键值对
$newEnvContent = $envContent . PHP_EOL . "{$key}={$value}";
}
// 保存更新后的 .env 文件内容
return file_put_contents($envFile, $newEnvContent) !== false;
}
// 获取文件的域名
function getDiskUrl(){
$disk=env('filesystem.driver','local');
$url=request()->domain();
if($disk=='aliyun'){
$url=env('filesystem.aliyun_url','');
}elseif($disk=='qiniu'){
$url=env('filesystem.qiniu_url','');
}elseif($disk=='qcloud'){
$url=env('filesystem.qcloud_cdn','');
}
$url=rtrim($url,'/');
return $url;
}
/**
* 合成图片
* @param array $pic_list [图片列表数组]
* @param boolean $is_save [是否保存true保存false输出到浏览器]
* @param string $save_path [保存路径]
* @return boolean|string
*/
function getGroupAvatar($pic_list=array(),$is_save=false,$save_path=''){
//验证参数
if(empty($pic_list) || empty($save_path)){
return false;
}
if($is_save){
//如果需要保存,需要传保存地址
if(empty($save_path)){
return false;
}
}
// 只操作前9个图片
$pic_list = array_slice($pic_list, 0, 9);
//设置背景图片宽高
$bg_w = 150; // 背景图片宽度
$bg_h = 150; // 背景图片高度
//新建一个真彩色图像作为背景
$background = imagecreatetruecolor($bg_w,$bg_h);
//为真彩色画布创建白灰色背景,再设置为透明
$color = imagecolorallocate($background, 202, 201, 201);
imagefill($background, 0, 0, $color);
imageColorTransparent($background, $color);
//根据图片个数设置图片位置
$pic_count = count($pic_list);
$lineArr = array();//需要换行的位置
$space_x = 3;
$space_y = 3;
$line_x = 0;
switch($pic_count) {
case 1: // 正中间
$start_x = intval($bg_w/4); // 开始位置X
$start_y = intval($bg_h/4); // 开始位置Y
$pic_w = intval($bg_w/2); // 宽度
$pic_h = intval($bg_h/2); // 高度
break;
case 2: // 中间位置并排
$start_x = 2;
$start_y = intval($bg_h/4) + 3;
$pic_w = intval($bg_w/2) - 5;
$pic_h = intval($bg_h/2) - 5;
$space_x = 5;
break;
case 3:
$start_x = 40; // 开始位置X
$start_y = 5; // 开始位置Y
$pic_w = intval($bg_w/2) - 5; // 宽度
$pic_h = intval($bg_h/2) - 5; // 高度
$lineArr = array(2);
$line_x = 4;
break;
case 4:
$start_x = 4; // 开始位置X
$start_y = 5; // 开始位置Y
$pic_w = intval($bg_w/2) - 5; // 宽度
$pic_h = intval($bg_h/2) - 5; // 高度
$lineArr = array(3);
$line_x = 4;
break;
case 5:
$start_x = 30; // 开始位置X
$start_y = 30; // 开始位置Y
$pic_w = intval($bg_w/3) - 5; // 宽度
$pic_h = intval($bg_h/3) - 5; // 高度
$lineArr = array(3);
$line_x = 5;
break;
case 6:
$start_x = 5; // 开始位置X
$start_y = 30; // 开始位置Y
$pic_w = intval($bg_w/3) - 5; // 宽度
$pic_h = intval($bg_h/3) - 5; // 高度
$lineArr = array(4);
$line_x = 5;
break;
case 7:
$start_x = 53; // 开始位置X
$start_y = 5; // 开始位置Y
$pic_w = intval($bg_w/3) - 5; // 宽度
$pic_h = intval($bg_h/3) - 5; // 高度
$lineArr = array(2,5);
$line_x = 5;
break;
case 8:
$start_x = 30; // 开始位置X
$start_y = 5; // 开始位置Y
$pic_w = intval($bg_w/3) - 5; // 宽度
$pic_h = intval($bg_h/3) - 5; // 高度
$lineArr = array(3,6);
$line_x = 5;
break;
case 9:
$start_x = 5; // 开始位置X
$start_y = 5; // 开始位置Y
$pic_w = intval($bg_w/3) - 5; // 宽度
$pic_h = intval($bg_h/3) - 5; // 高度
$lineArr = array(4,7);
$line_x = 5;
break;
}
foreach( $pic_list as $k=>$pic_path ) {
$kk = $k + 1;
if ( in_array($kk, $lineArr) ) {
$start_x = $line_x;
$start_y = $start_y + $pic_h + $space_y;
}
//获取图片文件扩展类型和mime类型判断是否是正常图片文件
//非正常图片文件,相应位置空着,跳过处理
$image_mime_info = @getimagesize($pic_path);
if($image_mime_info && !empty($image_mime_info['mime'])){
$mime_arr = explode('/',$image_mime_info['mime']);
if(is_array($mime_arr) && $mime_arr[0] == 'image' && !empty($mime_arr[1])){
switch($mime_arr[1]) {
case 'jpg':
case 'jpeg':
$imagecreatefromjpeg = 'imagecreatefromjpeg';
break;
case 'png':
$imagecreatefromjpeg = 'imagecreatefrompng';
break;
case 'gif':
default:
$imagecreatefromjpeg = 'imagecreatefromstring';
$pic_path = file_get_contents($pic_path);
break;
}
//创建一个新图像
$resource = $imagecreatefromjpeg($pic_path);
//将图像中的一块矩形区域拷贝到另一个背景图像中
// $start_x,$start_y 放置在背景中的起始位置
// 0,0 裁剪的源头像的起点位置
// $pic_w,$pic_h copy后的高度和宽度
imagecopyresized($background,$resource,$start_x,$start_y,0,0,$pic_w,$pic_h,imagesx($resource),imagesy($resource));
}
}
// 最后两个参数为原始图片宽度和高度倒数两个参数为copy时的图片宽度和高度
$start_x = $start_x + $pic_w + $space_x;
}
if($is_save){
$dir = pathinfo($save_path,PATHINFO_DIRNAME);
if(!is_dir($dir)){
$file_create_res = mkdir($dir,0777,true);
if(!$file_create_res){
return false;//没有创建成功
}
}
$res = imagejpeg($background,$save_path);
imagedestroy($background);
if($res){
return true;
}else{
return false;
}
}else{
//直接输出
header("Content-type: image/jpg");
imagejpeg($background);
imagedestroy($background);
}
}
/**
* 获取一个唯一token
* @return string
*/
function getOnlyToken()
{
return md5(uniqid(md5(microtime(true)), true));
}
// 设置排序规则
function orderBy($field, $type, $prefix = '', $default = 'update_time')
{
$type=is_numeric($type)?($type==1?'asc':'desc'):$type;
if ($field) {
$order = $prefix . $field . ' ' . $type;
} else {
$order = $prefix . $default . ' desc';
}
return $order;
}
// 获取文件后缀图片
function getExtUrl($path){
$ext=explode('.',$path);
$ext=end($ext);
// 如果是图片文件,就直接返回图片地址
$image=['jpg','jpeg','png','bmp','gif','webp'];
if(in_array($ext,$image)){
return getFileUrl($path);
}
$extUrl='/static/img/ext/'.strtoupper($ext).'.png';
// 判断文件是否存在
if(!file_exists(public_path().$extUrl)){
$extUrl='/static/img/ext/folder.png';
}
return request()->domain().$extUrl;
}
// 字符串内容加解密函数
function str_encipher($str,$encode=true,$key=''){
if($key==''){
$key=config('app.aes_chat_key');
}
if($key==''){
return $str;
}
if($encode){
$s=\utils\Aes::encrypt($str,$key);
}else{
$s=\utils\Aes::decrypt($str,$key) ?:'';
}
return $s;
}

297
app/common/controller/Pub.php Executable file
View File

@ -0,0 +1,297 @@
<?php
namespace app\common\controller;
use think\App;
use app\enterprise\model\{User,Group};
use app\index\controller\Extension;
use think\facade\Session;
use think\facade\Cache;
use think\facade\Db;
use GatewayClient\Gateway;
use app\manage\model\Config;
use thans\jwt\facade\JWTAuth;
/**
* 控制器基础类
*/
class Pub
{
/**
* Request实例
* @var \think\Request
*/
protected $request;
/**
* 应用实例
* @var \think\App
*/
protected $app;
/**
* 构造方法
* @access public
* @param App $app 应用对象
*/
public function __construct(App $app)
{
Gateway::$registerAddress = config('gateway.registerAddress');
$this->app = $app;
$this->request = $this->app->request;
// 控制器初始化
// $this->initialize();
}
public function login(){
$param=request()->param();
$userInfo=User::where(['account'=> $param['account']])->withoutField('register_ip,login_count,update_time,create_time')->find();
if($userInfo==null){
return warning('当前用户不存在!');
}elseif($userInfo['status']==0){
return warning('您的账号已被禁用');
}else{
$password=password_hash_tp($param['password'],$userInfo['salt']);
$code=$param['code'] ?? '';
if($code){
if($code!=Cache::get($param['account'])){
return warning('验证码错误!');
}
Cache::delete($param['account']);
}else{
if($password!=$userInfo['password']){
return warning('密码错误!');
}
}
$userInfo['avatar']=avatarUrl($userInfo['avatar'],$userInfo['realname'],$userInfo['user_id']);
// 如果用户已经有设置
$setting=$userInfo['setting'] ?: '';
if($setting){
$setting['hideMessageName']= $setting['hideMessageName']=='true' ? true : false;
$setting['hideMessageTime']= $setting['hideMessageTime']=='true' ? true : false;
$setting['avatarCricle']= $setting['avatarCricle']=='true' ? true : false;
$setting['isVoice']= $setting['isVoice']=='true' ? true : false;
$setting['sendKey']=(int)$setting['sendKey'];
$userInfo['setting']=$setting;
}
//如果登录信息中含有client——id则自动进行绑定
$client_id=$this->request->param('client_id');
if($client_id){
$this->doBindUid($userInfo['user_id'],$client_id);
}
$update=[
'last_login_time'=>time(),
'last_login_ip'=>$this->request->ip(),
'login_count'=>Db::raw('login_count+1')
];
User::where('user_id',$userInfo['user_id'])->update($update);
$userInfo['qrUrl']=request()->domain().'/scan/u/'.encryptIds($userInfo['user_id']);
unset($userInfo['password'],$userInfo['salt']);
$userInfo['displayName']=$userInfo['realname'];
$userInfo['id']=$userInfo['user_id'];
$authToken=User::refreshToken($userInfo,$param['terminal'] ?? 'web');
$data=[
'sessionId'=>Session::getId(),
'authToken'=>$authToken,
'userInfo'=>$userInfo
];
return success('登录成功!',$data);
}
}
//退出登录
public function logout(){
try {
$jwtData = JWTAuth::auth();
} catch (\Exception $e) {
return success('退出成功!');
}
$userInfo = $jwtData['info']->getValue();
//解密token中的用户信息
$userInfo = str_encipher($userInfo,false, config('app.aes_token_key'));
if (!$userInfo) {
return success('退出成功!');
}
//解析json
$userInfo = (array)json_decode($userInfo, true);
if($userInfo){
$client_id=$this->request->param('client_id','');
if($client_id){
Gateway::unbindUid($client_id,$userInfo['user_id']);
}
wsSendMsg(0,'isOnline',['id'=>$userInfo['user_id'],'is_online'=>0]);
}
JWTAuth::invalidate(JWTAuth::token()->get());
return success('退出成功!');
}
// 注册用户
public function register(){
try{
$data = $this->request->param();
$systemInfo=Config::getSystemInfo();
// 判断系统是否开启注册
if($systemInfo['sysInfo']['regtype']==2){
$inviteCode=$data['inviteCode'] ?? '';
if(!$inviteCode){
return warning('当前系统已关闭注册功能!');
}
if(!Cache::get($inviteCode)){
return warning('邀请码已失效!');
}
}
$code=$data['code'] ?? '';
if($code){
if($code!=Cache::get($data['account'])){
return warning('验证码错误!');
}
Cache::delete($data['account']);
}
$user=new User();
$verify=$user->checkAccount($data);
if(!$verify){
return warning($user->getError());
}
$salt=\utils\Str::random(4);
$data['password'] = password_hash_tp($data['password'],$salt);
$data['salt'] =$salt;
$data['register_ip'] =$this->request->ip();
$data['name_py'] = pinyin_sentence($data['realname']);
$user->save($data);
$data['user_id']=$user->user_id;
// 监听用户注册后的操作
event('UserRegister',$data);
return success('注册成功', $data);
}catch (\Exception $e){
return error($e->getMessage());
}
}
//头像生成
public function avatar(){
circleAvatar(input('str'),input('s')?:80,input('uid'));die;
}
/**
* 将用户UId绑定到消息推送服务中
* @return \think\response\Json
*/
public function bindUid(){
$client_id=$this->request->param('client_id');
$user_id=$this->request->param('user_id');
try{
$this->doBindUid($user_id,$client_id);
}catch(\Exception $e){
// 未找到用户
}
return success('');
}
// 执行绑定
public function doBindUid($user_id,$client_id){
// 如果当前ID在线将其他地方登陆挤兑下线
if(Gateway::isUidOnline($user_id)){
wsSendMsg($user_id,'offline',['id'=>$user_id,'client_id'=>$client_id,'isMobile'=>$this->request->isMobile()]);
}
Gateway::bindUid($client_id, $user_id);
// 查询团队,如果有团队则加入团队
$group=Group::getMyGroup(['gu.user_id'=>$user_id,'gu.status'=>1]);
if($group){
$group=$group->toArray();
$group_ids=arrayToString($group,'group_id',false);
foreach($group_ids as $v){
Gateway::joinGroup($client_id, $v);
}
}
wsSendMsg(0,'isOnline',['id'=>$user_id,'is_online'=>1]);
}
// 下线通知
public function offline(){
$user_id=input('user_id');
try{
$client_ids=Gateway::getClientIdByUid($user_id);
// 一个终端登录时才发送下线通知
if(count($client_ids)<2){
wsSendMsg(0,'isOnline',['id'=>$user_id,'is_online'=>0]);
}
}catch(\Exception $e){
// 未找到用户
}
return success('');
}
/**
* 将用户团队绑定到消息推送服务中
* @return \think\response\Json
*/
public function bindGroup(){
$client_id=input('client_id');
$group_id=input('group_id');
$group_id = explode('-', $group_id)[1];
Gateway::joinGroup($client_id, $group_id);
return success();
}
// 获取系统配置信息
public function getSystemInfo(){
$systemInfo=Config::getSystemInfo();
$systemInfo['demon_mode']=env('app.demon_mode',false);
return success('',$systemInfo);
}
// 发送验证码
public function sendCode(){
$account=$this->request->param('account');
$type=$this->request->param('type',1);
if(in_array($type,[3,4]) && !$account){
$userInfo=request()->userInfo;
$acType=\utils\Regular::check_account($userInfo['account']);
if($acType){
$account=$userInfo['account'];
}else{
$account=$userInfo['email'];
}
};
$acType=\utils\Regular::check_account($account);
if(!$acType){
return warning('账户必须为手机号或者邮箱');
}
if(Cache::get($account.'_time')) return warning('请一分钟后再试!');
if($type==1){
$text='登录账户';
$actions="login";
}elseif($type==2){
$text='注册账户';
$actions="register";
}elseif($type==3){
$text='修改密码';
$actions="changePassword";
}else{
$text="修改账户";
$actions="changeUserinfo";
}
$code=rand(100000,999999);
Cache::set($account,$code,300);
Cache::set($account.'_time',$code,60);
if($acType==2){
$conf=Config::where(['name'=>'smtp'])->value('value');
$conf['temp']='code';
$mail=new \mail\Mail($conf);
$mail->sendEmail([$account],$text,$code);
return success('发送成功');
}else{
$parmes=[
'code'=>$code
];
$res=sendSms($account,$actions,$parmes);
return success($res['msg']);
}
}
}

208
app/common/controller/Upload.php Executable file
View File

@ -0,0 +1,208 @@
<?php
/**
* lvzheAdmin [a web admin based ThinkPHP5]
* @author xiekunyu<raingad@foxmail.com>
*/
namespace app\common\controller;
use app\BaseController;
use app\enterprise\model\{File as FileModel,Message,User};
use app\manage\model\{Config};
use think\facade\Filesystem;
use think\facade\Request;
use think\File;
class Upload extends BaseController
{
protected $middleware = ['checkAuth'];
protected $disk='';
protected $url='';
public function __construct()
{
parent::__construct(app());
$this->disk=env('filesystem.driver','local');
$this->url=getDiskUrl().'/';
}
/**
* 文件上传
*/
public function upload($data,$path,$prefix = "",$fileObj = true)
{
$message=$data['message'] ?? '';
if($message){
$message=json_decode($message,true);
}
$uid=request()->userInfo['user_id'];
if($fileObj){
$filePath = $path;
}else{
$filePath = new File($path);
}
$info=$this->getFileInfo($filePath,$path,$fileObj);
if($info['ext']=='' && $message){
$pathInfo = pathinfo($message['fileName'] ?? '');
$info['ext'] = $pathInfo['extension'];
$info['name'] =$message['fileName'] ?? '';
}
$conf=Config::where(['name'=>'fileUpload'])->value('value');
if($conf['size']*1024*1024 < $info['size']){
return shutdown('文件大小超过限制');
}
// 兼容uniapp文件上传
if($info['ext']=='' && isset($data['ext'])){
$info['ext']=$data['ext'];
}
if(!in_array($info['ext'],$conf['fileExt'])){
return shutdown('文件格式不支持');
}
$fileType=getFileType($info['ext']);
if($fileType==2){
$filecate="image";
}elseif($fileType==3){
$msgType=$message['type'] ?? '';
// 如果是语音消息,类型才为语音,否者为文件,主要是兼容发送音频文件
if($msgType=='voice'){
$filecate="voice";
}else{
$filecate="file";
}
}elseif($fileType==4){
$filecate="video";
}else{
$filecate="file";
}
if(!$prefix){
$prefix=$filecate.'/'.$uid.'/'.date('Y-m-d')."/";
}
$name=str_replace('.'.$info['ext'],'',$info['name']);
$file=FileModel::where(['md5'=>$info['md5']])->find();
// 判断文件是否存在,如果有则不再上传
if(!$file){
$newName = uniqid() . '.' . $info['ext'];
$object = $prefix . $newName;
if($this->disk=='local'){
$object='storage/'.$object;
}
Filesystem::disk($this->disk)->putFileAs($prefix, $filePath, $newName);
}else{
$object = $file['src'];
}
// 把左边的/去掉再加上,避免有些有/有些没有
$object='/'.ltrim($object,'/');
$ret = [
"src" => $object,
"name" => $name,
"cate" => $fileType,
"size" => $info['size'],
"md5" => $info['md5'],
"file_type" => $info['mime'],
"ext" => $info['ext'],
"type" =>2,
'user_id'=>$uid,
];
if($message){
// 自动获取视频第一帧,视频并且是使用的阿里云
if($message['type']=='video' && $this->disk=='aliyun'){
$message['extends']['poster']=$this->url.$ret['src'].'?x-oss-process=video/snapshot,t_1000,m_fast,w_800,f_png';
}else{
$message['extends']['poster']='https://im.file.raingad.com/static/image/video.png';
}
// 如果发送的文件是图片、视频、音频则将消息类型改为对应的类型
if(in_array($fileType,[2,3,4])){
$message['type']=$filecate;
}
$newFile=new FileModel;
// 录音就不保存了
if($message['type']!='voice'){
$newFile->save($ret);
}
$message['content']=$ret['src'];
$message['file_id']=$newFile->file_id ?? 0;
$message['file_cate']=$fileType;
$message['file_size']=$info['size'];
$message['file_name']= $name.'.'.$info['ext'];
$message['user_id']= $uid;
$data=Message::sendMessage($message);
return $data;
}else{
return $ret;
}
}
// 上传一般文件
public function uploadFile(){
$param=$this->request->param();
try{
$file=request()->file('file');
$info=$this->upload($param,$file);
return success("上传成功",$info);
} catch(\Exception $e) {
return error($e->getMessage());
}
}
// 获取上传文件的信息
protected function getFileInfo($file,$path,$isObj=false){
$info= [
'path'=>$file->getRealPath(),
'size'=>$file->getSize(),
'mime'=>$file->getMime(),
'ext'=>$file->extension(),
'md5'=>$file->md5(),
];
if($isObj){
$info['name']=$file->getOriginalName();
}else{
// 根据路径获取文件名
$pathInfo = pathinfo($path);
$info['name'] = $pathInfo['basename'];
}
return $info;
}
// 上传图片
public function uploadImage(){
$param=request::param();
try{
$file=request()->file('file');
$info=$this->upload($param,$file,'image/'.date('Y-m-d').'/');
$url=$this->url.$info['src'];
return success("上传成功",$url);
} catch(\Exception $e) {
return error($e->getMessage());
}
}
// 普通上传头像
public function uploadAvatar(){
$param=request::param();
try{
$file=request()->file('file');
$uid=request()->userInfo['user_id'];
$info=$this->upload($param,$file,'avatar/'.$uid.'/');
User::where(['user_id'=>$uid])->update(['avatar'=>$info['src']]);
$url=$this->url.$info['src'];
return success("上传成功",$url);
} catch(\Exception $e) {
return error($e->getMessage());
}
}
// 服务器上传头像
public function uploadLocalAvatar($file,$param,$uid){
try{
$info=$this->upload($param,$file,'avatar/'.$uid.'/',false);
return $info['src'];
} catch(\Exception $e) {
return $e->getMessage().$e->getLine();
}
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace app\common\listener;
use app\enterprise\model\{User};
// 监听用户注册后的操作
class UserRegister
{
public function handle(User $user,$data){
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace app\common\middleware;
use Exception;
use thans\jwt\exception\TokenInvalidException;
use thans\jwt\facade\JWTAuth;
//验证权限
class CheckAuth
{
public function handle($request, \Closure $next)
{
try {
$jwtData = JWTAuth::auth();
} catch (Exception $exception) {
//token有误
if (get_class($exception) == TokenInvalidException::class) {
return shutdown('登陆信息有误 请重新登录', -1);
}
$errorMsgArr = [
'Must have token' => '请先登陆系统',
'The token is in blacklist.' => '登陆已失效 请重新登陆',
'The token is expired.' => '登陆已过期 请重新登陆',
'The token is in blacklist grace period list.' => '登陆已过期 请重新登陆'
];
return shutdown($errorMsgArr[$exception->getMessage()] ?? $exception->getMessage(), -1);
}
$userInfo = $jwtData['info']->getValue();
//解密token中的用户信息
$userInfo = str_encipher($userInfo,false, config('app.aes_token_key'));
if (!$userInfo) {
return shutdown('用户信息有误,请重新登陆', -1);
}
//解析json
$userInfo = (array)json_decode($userInfo, true);
//已经登陆,将用户信息存入请求头
$request->userInfo = $userInfo;
$request->uid = $userInfo['id'];
$request->userToken = JWTAuth::token()->get();
return $next($request);
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace app\common\middleware;
//验证权限
class ManageAuth
{
public function handle($request, \Closure $next)
{
// 设置演示模式,演示模式下无法修改配置
$request->demonMode=env('app.demon_mode',false);
if(!$request->demonMode){
if($request->userInfo['user_id']!=1 && $request->userInfo['role']!=2){
shutdown('您没有权限访问该接口',-1);
}
}else{
$rules=[
'user/add',
'user/edit',
'user/del',
'user/setrole',
'user/setstatus',
'user/editpassword',
'group/del',
'group/changeowner',
'group/delgroupuser',
'task/starttask',
'task/stoptask',
'config/setconfig'
];
// 获取pathinfo信息
$pathinfo = strtolower($request->pathinfo());
if(in_array($pathinfo,$rules)){
return shutdown('演示模式下无法操作!',400);
}
}
return $next($request);
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace app\common\task;
use yunwuxin\cron\Task;
use think\Exception;
use app\manage\model\{Config};
use app\enterprise\model\Message;
// 自动清理消息定时任务
class ClearMessage extends Task
{
// 定时任务日志内容
protected $content='';
protected $path='';
protected $daytime=86400;
/**
* 自动写入定时任务日志
* @return \think\response\Json
*/
protected function writeLog($text)
{
$this->path = root_path() . 'crontab.txt';
$content = '重置中!';
if (!file_exists($this->path)) {
fopen($this->path, 'w');
}
if (date('d') != 10) {
$content = file_get_contents($this->path);
}
file_put_contents($this->path, $content . date('Y-m-d H:i:s') . '' . $text . PHP_EOL);
}
public function configure()
{
//设置每天8点执行
$this->dailyAt('02:00');
}
/**
* 执行任务
* @return mixed
*/
protected function execute()
{
$this->writeLog('任务开始执行');
if(date('H:i')!='02:00'){
return false;
}
try {
$config=Config::getSystemInfo();
$status=$config['chatInfo']['msgClear'] ?? false;
$days=$config['chatInfo']['msgClearDay'] ?? 0;
if($status && $days){
$time=time() - ($days * $this->daytime);
$where[]=['create_time','<',$time];
// $where[]=['is_last','=',0];
Message::where($where)->delete();
}
$this->writeLog('消息清理成功');
} catch (Exception $e) {
$this->writeLog('消息清理失败:'.$e->getMessage());
}
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace app\enterprise\controller;
use app\BaseController;
use app\enterprise\model\{File,User,Message};
use think\facade\View;
class Files extends BaseController
{
// 文件列表
public function index()
{
$param = $this->request->param();
$is_all = $param['is_all'] ?? 0;
$map = [];
$data=[];
// 如果是查询全部就查询file表否则查询message表
if ($is_all) {
if ($param['cate'] ?? 0) {
$map[] = ['cate', '=', $param['cate']];
}
$model = new File();
if ($param['keywords'] ?? '') {
$model = $model->where('name', 'like', '%' . $param['keywords'] . '%');
}
$list = $this->paginate($model->where($map)->order('file_id desc'));
if ($list) {
$data = $list->toArray()['data'];
$userList = User::matchUser($data, true, 'user_id', 120);
foreach ($data as $k => $v) {
$url=getFileUrl($v['src']);
$data[$k]['src'] =$url;
$data[$k]['preview'] = previewUrl($url);
$data[$k]['extUrl'] = getExtUrl($v['src']);
$data[$k]['name'] = $v['name'].'.'.$v['ext'];
$data[$k]['msg_type'] = getFileType($v['ext'],true);
$data[$k]['user_id_info'] = $userList[$v['user_id']] ?? [];
$data[$k]['download'] = request()->domain().'/filedown/'.encryptIds($v['file_id']);
}
}
} else {
$map = [
['file_id', '>', 0],
['type', '<>', 'voice'],
['is_group', '=', 0],
['is_undo', '=', 0],
];
if ($param['cate'] ?? 0) {
$map[] = ['file_cate', '=', $param['cate']];
}
$user_id = $this->uid;
$model = new Message();
if ($param['keywords'] ?? '') {
$map[] = ['file_name', 'like', '%' . $param['keywords'] . '%'];
}
$role = $param['role'] ?? 0;
$where=[];
if($role==1){
$map[] = ['from_user', '=', $user_id];
}elseif($role==2){
$map[] = ['to_user', '=', $user_id];
}else{
$where='(from_user='.$user_id.' or to_user='.$user_id.')';
}
$list = $this->paginate($model->where($map)->where($where)->order('create_time desc'));
if ($list) {
$data = $list->toArray()['data'];
$userList = User::matchUser($data, true, 'from_user', 120);
foreach ($data as $k => $v) {
$content=str_encipher($v['content'],false);
$url=getFileUrl($content);
$data[$k]['src'] = $url;
$data[$k]['preview'] = previewUrl($url);
$data[$k]['extUrl'] = getExtUrl($content);
$data[$k]['cate'] = $v['file_cate'];
$data[$k]['name'] = $v['file_name'];
$data[$k]['size'] = $v['file_size'];
$data[$k]['msg_type'] = $v['type'];
$ext=explode('.',$content);
$data[$k]['ext'] = end($ext);
$data[$k]['user_id_info'] = $userList[$v['from_user']] ?? [];
$data[$k]['download'] = request()->domain().'/filedown/'.encryptIds($v['file_id']);
}
}
}
return success('', $data, $list->total(), $list->currentPage());
}
}

View File

@ -0,0 +1,168 @@
<?php
namespace app\enterprise\controller;
use app\BaseController;
use app\enterprise\model\{Friend as FriendModel,User};
class Friend extends BaseController
{
// 好友申请列表
public function index()
{
$param = $this->request->param();
$map = [];
$map[]=['is_invite','=',1];
$isMine=$param['is_mine'] ?? 0;
if($isMine){
// 我发起的
$map[]=['create_user','=',$this->uid];
}else{
// 我收到的
$map[]=['friend_user_id','=',$this->uid];
}
$data=[];
$model = new FriendModel();
$list = $this->paginate($model->where($map)->order('friend_id desc'));
if ($list) {
$data = $list->toArray()['data'];
$userList = User::matchUser($data, true, ['create_user','friend_user_id'], 120);
foreach ($data as $k => $v) {
$data[$k]['create_user_info'] = $userList[$v['create_user']] ?? [];
$data[$k]['user_id_info'] = $userList[$v['friend_user_id']] ?? [];
$data[$k]['is_group'] = 0;
}
}
return success('', $data,$list->total(),$list->currentPage());
}
// 添加好友
public function add()
{
$param = $this->request->param();
$user_id=$param['user_id'] ?? 0;
$friend=FriendModel::where(['friend_user_id'=>$user_id,'create_user'=>$this->uid])->find();
if($friend){
if($friend->status==1){
return warning('你们已经是好友了');
}elseif($friend->status==2){
return warning('你已经申请过了,请等待对方同意');
}
}
$status=2;
$otherFriend=FriendModel::where(['friend_user_id'=>$this->uid,'create_user'=>$user_id])->find();
if($otherFriend){
if($otherFriend->status>0){
$status=1;
}
}
$model = new FriendModel();
$data=[
'friend_user_id'=>$user_id,
'status'=>$status,
'create_user'=>$this->uid,
'remark'=>$param['remark'],
'is_invite'=>1 // 是否为发起方
];
$model->save($data);
$msg=[
'fromUser'=>[
'id'=>'system',
'nickname'=>'新朋友',
'avatar'=>'',
],
'toContactId'=>'system',
'id'=>uniqid(),
'is_group'=>2,
'content'=>"添加您为好友",
'status'=>'succeed',
'sendTime'=>time()*1000,
'type'=>'event',
'fileSize'=>0,
'fileName'=>'',
];
// 发送好友申请
wsSendMsg($user_id,'simple',$msg);
return success('添加成功');
}
// 接受或者拒绝好友申请
public function update()
{
$param = $this->request->param();
$friend=FriendModel::find($param['friend_id']);
if(!$friend){
return warning('申请不存在');
}
$map=[
'friend_id'=>$param['friend_id']
];
FriendModel::where($map)->update(['status'=>$param['status']]);
// 如果是接收,就添加到好友列表
if($param['status']){
$data=[
'friend_user_id'=>$friend->create_user,
'create_user'=>$this->uid,
];
$newFriend=FriendModel::where($data)->find();
if($newFriend){
FriendModel::where($data)->update(['status'=>1]);
return success('你们已经是好友了');
}else{
$data['status']=1;
FriendModel::create($data);
}
// 将对方的信息发送给我,把我的信息发送对方
$user=User::setContact($friend->create_user);
if($user){
wsSendMsg($this->uid,'appendContact',$user);
}
$myInfo=User::setContact($this->uid);
if($myInfo){
wsSendMsg($friend->create_user,'appendContact',$myInfo);
}
}
return success('操作成功');
}
// 删除好友
public function del()
{
$param = $this->request->param();
$map=['friend_user_id'=>$param['id'],'create_user'=>$this->uid];
$friend=FriendModel::where($map)->find();
if(!$friend){
return warning('好友不存在');
}
// 需要删除双方的好友关系
FriendModel::where($map)->delete();
FriendModel::where(['friend_user_id'=>$this->uid,'create_user'=>$param['id']])->delete();
// 性质和删除群聊一样
wsSendMsg($param['id'],'removeGroup',['group_id'=>$this->uid]);
return success('删除成功');
}
// 设置好友备注
public function setNickname()
{
$param = $this->request->param();
if(!$param['nickname']){
return warning('备注不能为空');
}
FriendModel::update(['nickname'=>$param['nickname']],['friend_id'=>$param['friend_id']]);
return success('设置成功');
}
// 获取最新的一条和申请的总数
public function getApplyMsg(){
$model = new FriendModel();
$map[]=['friend_user_id','=',$this->uid];
$map[]=['status','=',2];
$count=$model->where($map)->count();
return success('', $count);
}
}

View File

@ -0,0 +1,406 @@
<?php
namespace app\enterprise\controller;
use app\BaseController;
use app\enterprise\model\{User,Group as GroupModel,GroupUser,Message};
use think\Exception;
use think\facade\Db;
use app\common\controller\Upload;
class Group extends BaseController
{
protected $setting=['manage' => 0, 'invite' => 1, 'nospeak' => 0];
// 获取联系人列表
public function getAllUser(){
$param=$this->request->param();
$user_ids=isset($param['user_ids'])?$param['user_ids']:[];
$groupId=$param['group_id'] ?? '';
$group_id='';
if($groupId){
$group_id=explode('-',$groupId)[1];
}
$data=User::getAllUser([['status','=',1],['user_id','<>',$this->userInfo['user_id']]],$user_ids,$this->uid,$group_id);
return success('',$data);
}
// 获取群成员
public function groupUserList()
{
$param = $this->request->param();
try {
$group_id = explode('-', $param['group_id'])[1];
$data = GroupUser::getGroupUser(['group_id' => $group_id]);
return success('', $data);
} catch (Exception $e) {
return error($e->getMessage());
}
}
// 获取群基本信息
public function groupInfo()
{
$param = $this->request->param();
try {
$jm='qr';
$groupId=$param['group_id'] ?? '';
$groupInfo = explode('-', $groupId);
$group_id=$groupInfo[1];
$group=GroupModel::find($group_id)->toArray();
$userList=User::matchUser($group,false,'owner_id');
$userCount=GroupUser::where(['group_id'=>$group_id])->count();
$userInfo=$userList[$group['owner_id']];
$expire=time()+7*86400;
$token=urlencode(authcode($this->uid.'-'.$group_id,'ENCODE', $jm,7*86400));
$qrUrl=request()->domain().'/scan/g/'.$token;
$group['id']=$groupId;
$group['qrUrl']=$qrUrl;
$group['qrExpire']=date('m月d日',$expire);
$group['userInfo']=$userInfo;
$group['ownerName']=$userInfo['realname'];
$group['groupUserCount']=$userCount;
$group['displayName']=$group['name'];
$group['avatar']=avatarUrl($group['avatar'],$group['name'],$group['group_id'],120);
$group['setting']=$group['setting']?json_decode($group['setting'],true):['manage' => 0, 'invite' => 1, 'nospeak' => 0];
$group['isJoin']=GroupUser::where(['group_id'=>$group_id,'user_id'=>$this->uid])->value('role') ?: 0;
return success('', $group);
} catch (Exception $e) {
return error($e->getMessage());
}
}
// 修改团队名称
public function editGroupName()
{
$param = $this->request->param();
$group_id = explode('-', $param['id'])[1];
$role=GroupUser::where(['group_id'=>$group_id,'user_id'=>$this->userInfo['user_id']])->value('role');
if($role>2){
return warning('你没有操作权限,只有群主和群管理员才可以修改!');
}
GroupModel::where(['group_id' => $group_id])->update(['name' => $param['displayName'],'name_py'=>pinyin_sentence($param['displayName'])]);
$param['editUserName'] = $this->userInfo['realname'];
$action='editGroupName';
event('GroupChange', ['action' => $action, 'group_id' => $group_id, 'param' => $param]);
wsSendMsg($group_id, $action, $param, 1);
return success('修改成功');
}
// 添加群成员
public function addGroupUser(){
$param = $this->request->param();
$uid=$this->userInfo['user_id'];
$group_id = explode('-', $param['id'])[1];
$user_ids=$param['user_ids'];
$groupUserCount=GroupUser::where(['group_id'=>$group_id,'status'=>1])->count();
if((count($user_ids) + $groupUserCount) > $this->chatSetting['groupUserMax'] && $this->chatSetting['groupUserMax']!=0){
return warning("人数不能超过".$this->chatSetting['groupUserMax']."人!");
}
$data=[];
try{
foreach($user_ids as $k=>$v){
$data[]=[
'group_id'=>$group_id,
'user_id'=>$v,
'role'=>3,
'invite_id'=>$uid
];
}
$groupUser=new GroupUser;
$groupUser->saveAll($data);
$url=GroupModel::setGroupAvatar($group_id);
wsSendMsg($group_id,"addGroupUser",['group_id'=>$param['id'],'avatar'=>$url],1);
return success('添加成功');
}catch(Exception $e){
return error($e->getMessage());
}
}
// 设置管理员
public function setManager(){
$param = $this->request->param();
$uid=$this->userInfo['user_id'];
$group_id = explode('-', $param['id'])[1];
$user_id=$param['user_id'];
$role=$param['role'];
if(!GroupUser::checkAuth(['group_id'=>$group_id,'user_id'=>$uid])){
return warning('您没有操作权限!');
}
$groupUser=GroupUser::where(['group_id'=>$group_id,'user_id'=>$user_id])->find();
if($groupUser){
$groupUser->role=$role;
$groupUser->save();
wsSendMsg($group_id,"setManager",['group_id'=>$param['id']],1);
return success('设置成功');
}else{
return warning('设置失败!');
}
}
// 添加群聊
public function add(){
$param = $this->request->param();
$uid=$this->userInfo['user_id'];
$user_ids=$param['user_ids'];
if($this->chatSetting['groupChat']==0){
return warning("您没有创建群聊的权限!");
}
if(count($user_ids)>$this->chatSetting['groupUserMax'] && $this->chatSetting['groupUserMax']!=0){
return warning("人数不能超过".$this->chatSetting['groupUserMax']."人!");
}
if(count($user_ids)<=1){
return warning("请至少选择两人!");
}
// 将自己也加入群聊
$user_ids[]=$this->userInfo['user_id'];
Db::startTrans();
$setting=$this->setting;
try{
$create=[
'create_user'=>$uid,
'owner_id'=>$uid,
'name'=>"群聊",
'name_py'=>"qunliao",
'setting'=>json_encode($setting),
];
$name=$param['name'] ?? '';
if($name){
$create['name']=$name;
$create['name_py']=pinyin_sentence($name);
}
$group=new GroupModel();
$group->save($create);
$group_id=$group->group_id;
$data=[];
sort($user_ids);
foreach($user_ids as $k=>$v){
$info=[
'user_id'=>$v,
'invite_id'=>$uid,
'status'=>1,
'role'=>3,
'group_id'=>$group_id
];
if($v==$uid){
$info['invite_id']=0;
$info['role']=1;
}
$data[]=$info;
}
$groupUser=new GroupUser();
$groupUser->saveAll($data);
$url=GroupModel::setGroupAvatar($group_id);
$groupInfo=[
'displayName'=>$create['name'],
'owner_id'=>$create['owner_id'],
'role'=>3,
'name_py'=>$create['name_py'],
'id'=>'group-'.$group_id,
'avatar'=>avatarUrl($url,$create['name'],$group_id,120),
'is_group'=>1,
'lastContent'=>$this->userInfo['realname'].' 创建了群聊',
'lastSendTime'=>time()*1000,
'index'=>"[2]群聊",
'is_notice'=>1,
'is_top'=>0,
'setting'=>$setting,
];
Message::create([
'from_user'=>$uid,
'to_user'=>$group_id,
'content'=>str_encipher('创建了群聊'),
'type'=>'event',
'is_group'=>1,
'is_read'=>1,
'is_last'=>1,
'chat_identify'=>'group-'.$group_id
]);
wsSendMsg($user_ids, 'addGroup', $groupInfo);
Db::commit();
$groupInfo['role']=1;
return success('',$groupInfo);
}catch(Exception $e){
Db::rollback();
return error($e->getMessage());
}
}
// 移除成员
public function removeUser(){
$param = $this->request->param();
$uid=$this->userInfo['user_id'];
$group_id = explode('-', $param['id'])[1];
$user_id=$param['user_id'];
$role=GroupUser::where(['group_id'=>$group_id,'user_id'=>$uid])->value('role');
if($role>2 && $user_id!=$uid){
return warning('您没有操作权限!');
}
$groupUser=GroupUser::where(['group_id'=>$group_id,'user_id'=>$user_id])->find();
if(($groupUser && $groupUser['role']>$role) || $user_id==$uid){
GroupUser::destroy($groupUser->id);
}else{
return warning('您的权限不够!');
}
$url=GroupModel::setGroupAvatar($group_id);
wsSendMsg($group_id,"removeUser",['group_id'=>$param['id'],'avatar'=>$url],1);
return success('删除成功');
}
// 解散团队
public function removeGroup(){
$param = $this->request->param();
$uid=$this->userInfo['user_id'];
$group_id = explode('-', $param['id'])[1];
$role=GroupUser::where(['group_id'=>$group_id,'user_id'=>$uid])->value('role');
if($role>1){
return warning('您没有操作权限!');
}
Db::startTrans();
try{
// 删除团队成员
GroupUser::where(['group_id'=>$group_id])->delete();
// 删除团队
GroupModel::destroy($group_id);
wsSendMsg($group_id,"removeGroup",['group_id'=>$param['id']],1);
Db::commit();
return success();
}catch(Exception $e){
Db::rollback();
return error($e->getMessage());
}
}
// 设置公告
public function setNotice(){
$param = $this->request->param();
$uid=$this->userInfo['user_id'];
$group_id = explode('-', $param['id'])[1];
if($param['notice']==''){
return warning('请输入内容!');
}
$role=GroupUser::where(['group_id'=>$group_id,'user_id'=>$uid])->value('role');
// if($role>2){
// return warning('您没有操作权限!');
// }
GroupModel::update(['notice'=>$param['notice']],['group_id'=>$group_id]);
wsSendMsg($group_id,"setNotice",['group_id'=>$param['id'],'notice'=>$param['notice']],1);
return success('');
}
// 群聊设置
public function groupSetting(){
$param = $this->request->param();
$uid=$this->userInfo['user_id'];
$group_id = explode('-', $param['id'])[1];
$role=GroupUser::where(['group_id'=>$group_id,'user_id'=>$uid])->value('role');
if($role!=1){
return warning('您没有操作权限!');
}
$setting=json_encode($param['setting']);
GroupModel::update(['setting'=>$setting],['group_id'=>$group_id]);
wsSendMsg($group_id,"groupSetting",['group_id'=>$param['id'],'setting'=>$param['setting']],1);
return success('');
}
//生成群聊头像
protected function setGroupAvatar($group_id){
$userList=GroupUser::where('group_id',$group_id)->limit(9)->column('user_id');
$userList=User::where('user_id','in',$userList)->select()->toArray();
$imgList=[];
$dirPath=app()->getRootPath().'public/temp';
foreach($userList as $k=>$v){
if($v['avatar']){
$imgList[]=avatarUrl($v['avatar'],$v['realname'],$v['user_id']);
}else{
$imgList[]=circleAvatar($v['realname'],80,$v['user_id'],1,$dirPath);
}
}
$groupId='group_'.$group_id;
$path=$dirPath.'/'.$groupId.'.jpg';
$a = getGroupAvatar($imgList,1,$path);
$url='';
if($a){
$upload=new Upload();
$newPath=$upload->uploadLocalAvatar($path,[],$groupId);
if($newPath){
GroupModel::where('group_id',$group_id)->update(['avatar'=>$newPath]);
$url=avatarUrl($newPath);
}
}
// 删除目录下的所有文件
$files = glob($dirPath . '/*'); // 获取目录下所有文件路径
foreach ($files as $file) {
if (is_file($file)) { // 如果是文件则删除
unlink($file);
}
}
return $url;
}
// 加入群
public function joinGroup(){
$param = $this->request->param();
$uid=$this->userInfo['user_id'];
$group_id = explode('-', $param['group_id'])[1];
// event('GroupChange', ['action' => 'joinGroup', 'group_id' => $group_id, 'param' => $param]);
// exit();
$inviteUid=$param['inviteUid'] ?? '';
$groupUserCount=GroupUser::where(['group_id'=>$group_id,'status'=>1])->count();
if(($groupUserCount+1) > $this->chatSetting['groupUserMax'] && $this->chatSetting['groupUserMax']!=0){
return warning("人数不能超过".$this->chatSetting['groupUserMax']."人!");
}
try{
$data=[
'group_id'=>$group_id,
'user_id'=>$uid,
'role'=>3,
'invite_id'=>$inviteUid
];
GroupUser::create($data);
$url=GroupModel::setGroupAvatar($group_id);
$action='joinGroup';
event('GroupChange', ['action' => $action, 'group_id' => $group_id, 'param' => $param]);
wsSendMsg($group_id,"addGroupUser",['group_id'=>$param['group_id'],'avatar'=>$url],1);
return success('加入成功');
}catch(Exception $e){
return error($e->getMessage());
}
}
// 更换群主
public function changeOwner()
{
$user_id = $this->request->param('user_id');
$id = $this->request->param('id');
$group_id = explode('-', $id)[1];
$uid=$this->userInfo['user_id'];
$group=GroupModel::where('group_id',$group_id)->find();
if(!$group){
return warning('群组不存在');
}
$user=User::where('user_id',$user_id)->find();
if(!$user){
return warning('用户不存在');
}
$role=GroupUser::where(['group_id'=>$group_id,'user_id'=>$uid])->value('role');
if($role>1){
return warning('您没有操作权限!');
}
Db::startTrans();
try{
GroupUser::where('group_id',$group_id)->where('user_id',$user_id)->update(['role'=>1]);
GroupUser::where('group_id',$group_id)->where('user_id',$group->owner_id)->update(['role'=>3]);
$group->owner_id=$user_id;
$group->save();
wsSendMsg($group_id,"changeOwner",['group_id'=>'group-'.$group_id,'user_id'=>$user_id],1);
Db::commit();
return success('转让成功');
}catch (\Exception $e){
Db::rollback();
return warning('更换失败');
}
}
}

667
app/enterprise/controller/Im.php Executable file
View File

@ -0,0 +1,667 @@
<?php
namespace app\enterprise\controller;
use app\BaseController;
use think\facade\Request;
use think\facade\Db;
use app\enterprise\model\{User, Message, GroupUser, Friend};
use GatewayClient\Gateway;
use Exception;
use League\Flysystem\Util;
use think\facade\Cache;
use Char0n\FFMpegPHP\Movie;
class Im extends BaseController
{
protected $fileType = ['file', 'image','video','voice'];
// 获取联系人列表
public function getContacts()
{
$data = User::getUserList([['status', '=', 1], ['user_id', '<>', $this->userInfo['user_id']]], $this->userInfo['user_id']);
$count=Friend::where(['status'=>2,'friend_user_id'=>$this->uid])->count();
$time=Friend::where(['friend_user_id'=>$this->uid,'is_invite'=>1])->order('create_time desc')->value('create_time');
return success('', $data,$count,$time*1000);
}
//发送消息
public function sendMessage()
{
$param = $this->request->param();
$param['user_id'] = $this->userInfo['user_id'];
$is_group=$param['is_group']??0;
$chatSetting=$this->chatSetting;
if($is_group==0 && $chatSetting['simpleChat']==0){
return warning('目前禁止用户私聊!');
}
// 如果是单聊,并且是社区模式,需要判断是否是好友
if($is_group==0 && $this->globalConfig['sysInfo']['runMode']==2){
$friend=Friend::where(['friend_user_id'=>$this->uid,'create_user'=>$param['toContactId']])->find();
if(!$friend){
return warning('您不在TA的好友列表不能发消息');
}
$otherFriend=Friend::where(['friend_user_id'=>$param['toContactId'],'create_user'=>$this->uid])->find();
if(!$otherFriend){
return warning('TA还不是您的好友不能发消息');
}
}
$data = Message::sendMessage($param);
if ($data) {
return success('', $data);
} else {
return error('发送失败');
}
}
//转发消息
public function forwardMessage()
{
$param = $this->request->param();
$userIds=$param['user_ids'] ?? [];
if(!$userIds || count($userIds)>5){
return warning('请选择转发的用户或者数量不操作5个');
}
$msg_id=$param['msg_id'] ?? 0;
$message=Message::find($msg_id);
if(!$message){
return warning('消息不存在');
}
$message=$message->toArray();
$userInfo=$this->userInfo;
try{
$is_group=0;
$error=0;
$chatSetting=$this->chatSetting;
foreach($userIds as $k=>$v){
$msgInfo=$message;
if(strpos($v,'group')!==false){
$is_group=1;
}else{
$is_group=0;
}
if($is_group==0 && $chatSetting['simpleChat']==0){
$error++;
continue;
}
$msgInfo['id']=\utils\Str::getUuid();
$msgInfo['status']='successd';
$msgInfo['user_id']=$userInfo['user_id'];
$msgInfo['sendTime']=time()*1000;
$msgInfo['toContactId']=$v;
$msgInfo['content']=str_encipher($msgInfo['content'],false);
$msgInfo['fromUser']=[
'id'=>$userInfo['user_id'],
'avatar'=>avatarUrl($userInfo['avatar'],$userInfo['realname'],$userInfo['user_id'],120),
'displayName'=>$userInfo['realname']
];
$msgInfo['is_group']=$is_group;
// 如果是单聊,并且是社区模式,需要判断是否是好友
if($is_group==0 && $this->globalConfig['sysInfo']['runMode']==2){
$friend=Friend::where(['friend_user_id'=>$this->uid,'create_user'=>$v])->find();
if(!$friend){
$error++;
continue;
}
$otherFriend=Friend::where(['friend_user_id'=>$v,'create_user'=>$this->uid])->find();
if(!$otherFriend){
$error++;
continue;
}
}
Message::sendMessage($msgInfo);
}
}catch(\Exception $e){
return error($e->getMessage());
}
if ($error) {
$text='由于规则限制,转发失败'.$error.'条';
} else {
$text='转发成功';
}
return success($text);
}
// 获取用户信息
public function getUserInfo()
{
$user_id = $this->request->param('user_id');
$user=User::find($user_id);
if(!$user){
return error('用户不存在');
}
$user->avatar=avatarUrl($user->avatar,$user->realname,$user->user_id,120);
// 查询好友关系
$friend=Friend::where(['friend_user_id'=>$user_id,'create_user'=>$this->userInfo['user_id']])->find();
$user->friend=$friend ? : '';
$location='';
if($user->last_login_ip){
$location=implode(" ", \Ip::find($user->last_login_ip));
}
$user->location=$location;
$user->password='';
return success('', $user);
}
// 搜索用户
public function searchUser(){
$keywords=$this->request->param('keywords','');
if(!$keywords){
return success('',[]);
}
$map=['status'=>1,'account'=>$keywords];
$list=User::where($map)->field(User::$defaultField)->where([['account','<>',$this->userInfo['account']]])->select()->toArray();
if($list){
$ids=array_column($list,'user_id');
$friendList=Friend::getFriend([['create_user','=',$this->uid],['friend_user_id','in',$ids]]);
foreach($list as $k=>$v){
$list[$k]['avatar']=avatarUrl($v['avatar'],$v['realname'],$v['user_id'],120);
$list[$k]['friend']=$friendList[$v['user_id']] ?? '';
}
}
return success('', $list);
}
// 获取聊天记录
public function getMessageList()
{
$param = $this->request->param();
$is_group = isset($param['is_group']) ? $param['is_group'] : 0;
// 设置当前聊天消息为已读
$chat_identify = $this->setIsRead($is_group, $param['toContactId']);
$type = isset($param['type']) ? $param['type'] : '';
$map = ['chat_identify' => $chat_identify, 'status' => 1, 'is_group' => $is_group];
$where = [];
if ($type && $type != "all") {
$map['type'] = $type;
} else {
if (isset($param['type'])) {
$where[] = ['type', '<>', 'event'];
}
}
$keywords = isset($param['keywords']) ? $param['keywords'] : '';
if ($keywords && in_array($type, ['text', 'all'])) {
$where[] = ['content', 'like', '%' . $keywords . '%'];
}
$listRows = $param['limit'] ?: 20;
$pageSize = $param['page'] ?: 1;
$last_id = $param['last_id'] ?? 0;
if($last_id){
$where[]=['msg_id','<',$last_id];
}
$list = Message::getList($map, $where, 'msg_id desc', $listRows, $pageSize);
$data = $this->recombileMsg($list);
// 如果是消息管理器则不用倒序
if (!isset($param['type'])) {
$data = array_reverse($data);
}
return success('', $data, $list->total());
}
protected function recombileMsg($list)
{
$data = [];
$userInfo = $this->userInfo;
if ($list) {
$listData = $list->toArray()['data'];
$userList = User::matchUser($listData, true, 'from_user', 120);
foreach ($listData as $k => $v) {
// 屏蔽已删除的消息
if ($v['del_user']) {
$delUser = explode(',', $v['del_user']);
if (in_array($userInfo['user_id'], $delUser)) {
unset($list[$k]);
continue;
// $v['type']="event";
// $v['content']="删除了一条消息";
}
}
$content = str_encipher($v['content'],false);
$preview = '';
if (in_array($v['type'], $this->fileType)) {
$content = getFileUrl($content);
$preview = previewUrl($content);
}
$fromUser = $userList[$v['from_user']];
// 处理撤回的消息
if ($v['type'] == "event") {
if ($v['from_user'] == $userInfo['user_id']) {
$content = "" . $content;
} elseif ($v['is_group'] == 1) {
$content = $fromUser['realname'] . $content;
} else {
$content = "对方" . $content;
}
}
$data[] = [
'msg_id' => $v['msg_id'],
'id' => $v['id'],
'status' => "succeed",
'type' => $v['type'],
'sendTime' => $v['create_time'] * 1000,
'content' => $content,
'preview' => $preview,
'download' => $v['file_id'] ? request()->domain().'/filedown/'.encryptIds($v['file_id']) : '',
'is_read' => $v['is_read'],
'is_group' => $v['is_group'],
'toContactId' => $v['to_user'],
'from_user' => $v['from_user'],
'file_id' => $v['file_id'],
'file_cate' => $v['file_cate'],
'fileName' => $v['file_name'],
'fileSize' => $v['file_size'],
'fromUser' => $fromUser,
'extends'=>is_string($v['extends'])?json_decode($v['extends'],true) : $v['extends']
];
}
}
return $data;
}
// 设置当前窗口的消息默认为已读
public function setMsgIsRead()
{
$param = $this->request->param();
$this->setIsRead($param['is_group'], $param['toContactId']);
// 判断是否是一个二维数组
if (is_array($param['messages'][0] ?? '')) {
$messages=$param['messages'];
} else {
$messages=[$param['messages']];
}
if (!$param['is_group']) {
wsSendMsg($param['fromUser'], 'isRead', $messages, 0);
}
return success('');
}
// 设置消息已读
protected function setIsRead($is_group, $to_user)
{
if ($is_group) {
$chat_identify = $to_user;
$toContactId = explode('-', $to_user)[1];
// 更新群里面我的所有未读消息为0
GroupUser::editGroupUser(['user_id' => $this->userInfo['user_id'], 'group_id' => $toContactId], ['unread' => 0]);
} else {
$chat_identify = chat_identify($this->userInfo['user_id'], $to_user);
// 更新我的未读消息为0
Message::update(['is_read' => 1], [['chat_identify', '=', $chat_identify], ['to_user', '=', $this->userInfo['user_id']]]);
// 告诉对方我阅读了消息
wsSendMsg($to_user, 'readAll', ['toContactId' => $this->userInfo['user_id']]);
}
return $chat_identify;
}
// 聊天设置
public function setting()
{
$param = $this->request->param();
if ($param) {
User::where(['user_id' => $this->userInfo['user_id']])->update(['setting' => $param]);
return success('');
}
return warning('设置失败');
}
// 撤回消息
public function undoMessage()
{
$param = $this->request->param();
$id = $param['id'];
$message = Message::where(['id' => $id])->find();
if ($message) {
$text = "撤回了一条消息";
$fromUserName = "对方";
$toContactId = $message['to_user'];
if ($message['is_group'] == 1) {
$fromUserName = $this->userInfo['realname'];
$toContactId = explode('-', $message['chat_identify'])[1];
}
$message->content = str_encipher($text);
$message->type = 'event';
$message->is_undo = 1;
$message->create_time = time();
$message->save();
$data = $message->toArray();
$data['content'] = $fromUserName . $text;
wsSendMsg($toContactId, 'undoMessage', $data, $message['is_group']);
return success('');
} else {
return warning();
}
}
// 删除消息
public function removeMessage()
{
$param = $this->request->param();
$id = $param['id'];
$map = ['id' => $id];
$message = Message::where($map)->find();
if ($message) {
$message->del_user = $this->userInfo['user_id'];
if ($message['is_group'] == 1) {
if ($message['del_user']) {
$message->del_user .= ',' . $this->userInfo['user_id'];
}
} else {
if ($message['del_user'] > 0) {
$message->where($map)->delete();
return success('删除成功!');
}
}
$message->save();
return success('');
} else {
return warning('');
}
}
// 消息免打扰
public function isNotice()
{
$param = $this->request->param();
$user_id = $this->userInfo['user_id'];
$id = $param['id'];
if ($param['is_group'] == 1) {
$group_id = explode('-', $param['id'])[1];
GroupUser::update(['is_notice' => $param['is_notice']], ['user_id' => $user_id, 'group_id' => $group_id]);
} else {
$map = ['create_user' => $user_id, 'friend_user_id' => $id];
$friend = Friend::where($map)->find();
try {
if ($friend) {
$friend->is_notice = $param['is_notice'];
$friend->save();
} else {
$info = [
'create_user' => $user_id,
'friend_user_id' => $id,
'is_notice' => $param['is_notice']
];
Friend::create($info);
}
return success('');
} catch (Exception $e) {
return error($e->getMessage());
}
}
wsSendMsg($user_id,"setIsNotice",['id'=>$id,'is_notice'=>$param['is_notice'],'is_group'=>$param['is_group']]);
return success('');
}
// 设置聊天置顶
public function setChatTop()
{
$param = $this->request->param();
$user_id = $this->userInfo['user_id'];
$is_group = $param['is_group'] ?: 0;
$id = $param['id'];
try {
if ($is_group == 1) {
$group_id = explode('-', $param['id'])[1];
GroupUser::update(['is_top' => $param['is_top']], ['user_id' => $user_id, 'group_id' => $group_id]);
} else {
$map = ['create_user' => $user_id, 'friend_user_id' => $id];
$friend = Friend::where($map)->find();
if ($friend) {
$friend->is_top = $param['is_top'];
$friend->save();
} else {
$info = [
'create_user' => $user_id,
'friend_user_id' => $id,
'is_top' => $param['is_top']
];
Friend::create($info);
}
}
wsSendMsg($user_id,"setChatTop",['id'=>$id,'is_top'=>$param['is_top'],'is_group'=>$is_group]);
return success('');
} catch (Exception $e) {
return error($e->getMessage());
}
}
// 删除聊天
public function delChat()
{
$param = $this->request->param();
$user_id = $this->userInfo['user_id'];
$is_group = $param['is_group'] ?: 0;
$id = $param['id'];
if(!$is_group){
$chat_identify=chat_identify($user_id,$id);
}else{
return success('');
}
Message::where(['chat_identify' => $chat_identify])->update(['is_last' => 0]);
return success('');
}
// 向用户发送消息
public function sendToMsg(){
$param=$this->request->param();
$toContactId=$param['toContactId'];
$type=$param['type'];
$status=$param['status'];
$event=$param['event'] ?? 'calling';
if($event=='calling'){
$status=3;
}
$sdp=$param['sdp'] ?? '';
$iceCandidate=$param['iceCandidate'] ?? '';
$callTime=$param['callTime'] ?? '';
$msg_id=$param['msg_id'] ?? '';
$id=$param['id'] ?? '';
$code=($param['code'] ?? '') ?: 901;
// 如果该用户不在线,则发送忙线
Gateway::$registerAddress = config('gateway.registerAddress');
if(!Gateway::isUidOnline($toContactId)){
$toContactId=$this->userInfo['user_id'];
$code=907;
$event='busy';
sleep(1);
}
switch($code){
case 902:
$content='已取消通话';
break;
case 903:
$content='已拒绝';
break;
case 905:
$content='未接通';
break;
case 906:
$content='通话时长 '.date("i:s",$callTime);
break;
case 907:
$content='忙线中';
break;
case 908:
$content='其他端已操作';
break;
default:
$content=$type==1 ?'视频通话' : '语音通话';
break;
}
switch($event){
case 'calling':
$content=$type==1 ?'视频通话' : '语音通话';
break;
case 'acceptRtc':
$content='接听通话请求';
break;
case 'iceCandidate':
$content='数据交换中';
break;
}
$userInfo=$this->userInfo;
$userInfo['id']=$userInfo['user_id'];
$data=[
'id'=>$id,
'msg_id'=>$msg_id,
'sendTime'=>time()*1000,
'toContactId'=>$toContactId,
'content'=>$content,
'type'=>'webrtc',
'status'=>'succeed',
'is_group'=>0,
'is_read'=>0,
'fromUser'=>$userInfo,
'extends'=>[
'type'=>$type, //通话类型1视频0语音。
'status'=>$status, //1拨打方2接听方
'event'=>$event,
'callTime'=>$callTime,
'sdp'=>$sdp,
'code'=>$code, //通话状态:呼叫901取消902拒绝903接听904未接通905接通后挂断906忙线907,其他端操作908
'iceCandidate'=>$iceCandidate,
'isMobile'=>$this->request->isMobile() ? 1 : 0,
]
];
if($event=='calling'){
$chat_identify=chat_identify($userInfo['id'],$toContactId);
$msg=[
'from_user'=>$userInfo['id'],
'to_user'=>$toContactId,
'id'=>$id,
'content'=>str_encipher($content),
'chat_identify'=>$chat_identify,
'create_time'=>time(),
'type'=>$data['type'],
'is_group'=>0,
'is_read'=>0,
'extends'=>$data['extends'],
];
$message=new Message();
$message->update(['is_last'=>0],['chat_identify'=>$chat_identify]);
$message->save($msg);
$msg_id=$message->msg_id;
$data['msg_id']=$msg_id;
// 将接收人设置为发送人才能定位到该消息
$data['toContactId']=$userInfo['id'];
$data['toUser']=$toContactId;
}elseif($event=='hangup'){
$message=Message::where(['id'=>$id])->find();
if(!$message){
return error('通话失败!');
}
if($message){
$message->content=str_encipher($content);
$extends=$message->extends;
$extends['code']=$code;
$extends['callTime']=$callTime;
$message->extends=$extends;
$message->save();
}
}
wsSendMsg($toContactId,'webrtc',$data);
$wsData=$data;
if(in_array($event,['calling','acceptRtc','hangup'])){
if(in_array($event,['acceptRtc','hangup'])){
$data['extends']['event']='otherOpt'; //其他端操作
}
$data['toContactId']=$toContactId;
wsSendMsg($userInfo['id'],'webrtc',$data);
}
return success('',$wsData);
}
// 修改密码
public function editPassword()
{
if(env('app.demon_mode',false)){
return warning('演示模式不支持修改');
}
$user_id = $this->userInfo['user_id'];
$user=User::find($user_id);
if(!$user){
return warning('用户不存在');
}
$account=$user->account;
$code=$this->request->param('code','');
$originalPassword = $this->request->param('originalPassword', '');
if($code){
if(Cache::get($account)!=$code){
return warning('验证码不正确!');
}
}elseif($originalPassword){
if(password_hash_tp($originalPassword,$user->salt)!= $user->password){
return warning('原密码不正确!');
}
}else{
return warning('参数错误!');
}
try{
$password = $this->request->param('password','');
if($password){
$salt=$user->salt;
$user->password= password_hash_tp($password,$salt);
}
$user->save();
return success('修改成功');
}catch (\Exception $e){
return error('修改失败');
}
}
// 修改用户信息
public function updateUserInfo(){
try{
$data = $this->request->param();
$user=User::find($this->uid);
if(!$user){
return warning('用户不存在');
}
$user->realname =$data['realname'];
$user->email =$data['email'];
$user->motto=$data['motto'];
$user->sex =$data['sex'];
$user->name_py= pinyin_sentence($data['realname']);
$user->save();
return success('修改成功', $data);
}catch (\Exception $e){
return error($e->getMessage());
}
}
// 修改账户
public function editAccount(){
if(env('app.demon_mode',false)){
return warning('演示模式不支持修改');
}
$code=$this->request->param('code','');
$newCode=$this->request->param('newCode','');
$account=$this->request->param('account','');
$isUser=User::where('account',$account)->find();
if($isUser){
return warning('账户已存在');
}
$user=User::find($this->uid);
if(!$user){
return warning('用户不存在');
}
// 如果已经认证过了,则需要验证验证码
if($user->is_auth){
if(Cache::get($user->account)!=$code){
return warning('验证码不正确!');
}
}
if(Cache::get($account)!=$newCode){
return warning('新账户验证码不正确!');
}
try{
$user->account=$account;
$user->save();
return success('修改成功');
}catch (\Exception $e){
return error('修改失败');
}
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace app\enterprise\listener;
use app\enterprise\model\{Group,User};
use GatewayClient\Gateway;
// 监听群聊变更事件
class GroupChange
{
public function handle(Group $group,User $user,$data){
Gateway::$registerAddress = config('gateway.registerAddress');
if($data['action'] == 'joinGroup'){
Gateway::joinGroup(request()->header('clientId'),$data['group_id']);
}
}
}

4
app/enterprise/middleware.php Executable file
View File

@ -0,0 +1,4 @@
<?php
return [
"checkAuth"
];

14
app/enterprise/model/File.php Executable file
View File

@ -0,0 +1,14 @@
<?php
/**
* raingad IM [ThinkPHP6]
* @author xiekunyu <raingad@foxmail.com>
*/
namespace app\enterprise\model;
use app\BaseModel;
class File extends BaseModel
{
protected $pk="file_id";
}

28
app/enterprise/model/Friend.php Executable file
View File

@ -0,0 +1,28 @@
<?php
/**
* raingad IM [ThinkPHP6]
* @author xiekunyu <raingad@foxmail.com>
*/
namespace app\enterprise\model;
use app\BaseModel;
use think\facade\Db;
class Friend extends BaseModel
{
protected $pk="friend_id";
public static function getFriend($map){
$list=self::where($map)->select();
$data=[];
if($list){
$list=$list->toArray();
foreach($list as $k=>$v){
$data[$v['friend_user_id']]=$v;
}
}
return $data;
}
}

60
app/enterprise/model/Group.php Executable file
View File

@ -0,0 +1,60 @@
<?php
/**
* raingad IM [ThinkPHP6]
* @author xiekunyu <raingad@foxmail.com>
*/
namespace app\enterprise\model;
use app\BaseModel;
use think\facade\Db;
use app\common\controller\Upload;
class Group extends BaseModel
{
protected $pk="group_id";
// 获取我的团队
public static function getMyGroup($map){
return Db::name('group_user')
->alias('gu')
->field('gr.group_id,gr.avatar,gr.name as displayName,gu.unread,gr.name_py,gr.owner_id,gr.notice,gu.role,gu.is_notice,gu.is_top,gr.setting')
->join('group gr','gu.group_id=gr.group_id','left')
->where($map)
->select();
}
//生成群聊头像
public static function setGroupAvatar($group_id){
$userList=GroupUser::where('group_id',$group_id)->limit(9)->column('user_id');
$userList=User::where('user_id','in',$userList)->select()->toArray();
$imgList=[];
$dirPath=app()->getRootPath().'public/temp';
foreach($userList as $k=>$v){
if($v['avatar']){
$imgList[]=avatarUrl($v['avatar'],$v['realname'],$v['user_id']);
}else{
$imgList[]=circleAvatar($v['realname'],80,$v['user_id'],1,$dirPath);
}
}
$groupId='group_'.$group_id;
$path=$dirPath.'/'.$groupId.'.jpg';
$a = getGroupAvatar($imgList,1,$path);
$url='';
if($a){
$upload=new Upload();
$newPath=$upload->uploadLocalAvatar($path,[],$groupId);
if($newPath){
Group::where('group_id',$group_id)->update(['avatar'=>$newPath]);
$url=avatarUrl($newPath);
}
}
// 删除目录下的所有文件
$files = glob($dirPath . '/*'); // 获取目录下所有文件路径
foreach ($files as $file) {
if (is_file($file)) { // 如果是文件则删除
unlink($file);
}
}
return $url;
}
}

View File

@ -0,0 +1,35 @@
<?php
/**
* raingad IM [ThinkPHP6]
* @author xiekunyu <raingad@foxmail.com>
*/
namespace app\enterprise\model;
use app\BaseModel;
use think\facade\Db;
class GroupUser extends BaseModel
{
protected $pk="id";
// 编辑团队信息
public static function editGroupUser($map,$data){
return self::where($map)->update($data);
}
// 获取团队成员列表
public static function getGroupUser($map){
$data=self::where($map)->order('role asc')->select();
return User::matchAllUser($data,true,'user_id');
}
// 验证权限
public static function checkAuth($map,$role=1){
$info=self::where($map)->find()->toArray();
if($info['role']<=$role){
return true;
}else{
return false;
}
}
}

146
app/enterprise/model/Message.php Executable file
View File

@ -0,0 +1,146 @@
<?php
/**
* raingad IM [ThinkPHP6]
* @author xiekunyu <raingad@foxmail.com>
*/
namespace app\enterprise\model;
use app\BaseModel;
use think\facade\Db;
use think\facade\Request;
class Message extends BaseModel
{
protected $pk="msg_id";
protected $json = ["extends"];
protected $jsonAssoc = true;
protected static $fileType=['file','image','video','voice'];
// 添加聊天记录
public static function addData($data){
return Db::name('message')->insert($data);
}
// 更新消息状态
public static function editData($update,$map){
return Db::name('message')->where($map)->update($update);
}
// 查询聊天记录
public static function getList($map,$where,$sort,$listRows,$pageSize){
$list= Db::name('message')
->where($map)
->where($where)
->order($sort)
->paginate(['list_rows'=>$listRows,'page'=>$pageSize]);
return $list;
}
// 发送消息
public static function sendMessage($param){
$toContactId=$param['toContactId'];
$is_group=$param['is_group']?:0;
if(!$is_group){
$chat_identify=chat_identify($param['user_id'],$toContactId);
$is_read=0;
}else{
$chat_identify=$toContactId;
$toContactIdArr=explode('-',$toContactId);
$toContactId=$toContactIdArr[1];
$is_read=1;
if(!self::nospeak($toContactId,$param['user_id'])){
return shutdown("群聊已禁言!");
}
}
$fileSzie=isset($param['file_size'])?$param['file_size']:'';
$fileName=isset($param['file_name'])?$param['file_name']:'';
$ossUrl=getDiskUrl();
// 如果是转发图片文件的消息,必须把域名去除掉
$content=$param['content'];
if(in_array($param['type'],self::$fileType)){
if(strpos($param['content'],$ossUrl)!==false){
$content=str_replace($ossUrl,'',$param['content']);
}
}
$data=[
'from_user'=>$param['user_id'],
'to_user'=>$toContactId,
'id'=>$param['id'],
'content'=>str_encipher($content,true),
'chat_identify'=>$chat_identify,
'create_time'=>time(),
'type'=>$param['type'],
'is_group'=>$is_group,
'is_read'=>$is_read,
'file_id'=>$param['file_id'] ?? 0,
"file_cate"=>$param['file_cate'] ?? 0,
'file_size'=>$fileSzie,
'file_name'=>$fileName,
'extends'=>($param['extends'] ?? null) ? $param['extends'] : null,
];
$message=new self();
$message->update(['is_last'=>0],['chat_identify'=>$chat_identify]);
$message->save($data);
// 拼接消息推送
$type=$is_group?'group':'simple';
$sendData=$param;
$sendData['status']='succeed';
$sendData['msg_id']=$message->msg_id;
$sendData['is_read']=0;
$sendData['to_user']=$toContactId;
$sendData['sendTime']=(int)$sendData['sendTime'];
//这里我也不知为啥单聊要把发送对象设置为自己的ID。
if($is_group){
$sendData['toContactId']=$param['toContactId'];
// 将团队所有成员的未读状态+1
GroupUser::editGroupUser([['group_id','=',$toContactId],['user_id','<>',$param['user_id']]],['unread'=>Db::raw('unread+1')]);
}else{
$sendData['toContactId']=$param['user_id'];
}
$sendData['fromUser']['id']=(int)$sendData['fromUser']['id'];
$sendData['fileSize']=$fileSzie;
$sendData['fileName']=$fileName;
if(in_array($sendData['type'],self::$fileType)){
$sendData['content']=getFileUrl($sendData['content']);
if($sendData['type']=='image'){
$pre=1;
}else{
$pre=2;
}
$sendData['preview']=previewUrl($sendData['content'],$pre);
}
if($is_group==0){
$toContactId=[$toContactId,$param['user_id']];
}
$sendData['toUser']=$param['toContactId'];
// 向发送方发送消息
wsSendMsg($toContactId,$type,$sendData,$is_group);
$sendData['toContactId']=$param['toContactId'];
return $sendData;
}
// 群禁言
public static function nospeak($group_id,$user_id){
$group=Group::find($group_id);
if($group->owner_id==$user_id){
return true;
}
if($group->setting){
$setting=json_decode($group->setting,true);
$nospeak=isset($setting['nospeak'])?$setting['nospeak']:0;
$role=GroupUser::where(['group_id'=>$group_id,'user_id'=>$user_id])->value('role');
if($nospeak==1 && $role>2){
return false;
}elseif($nospeak==2 && $role!=1){
return false;
}
}
return true;
}
public function matchData(){
}
}

377
app/enterprise/model/User.php Executable file
View File

@ -0,0 +1,377 @@
<?php
/**
* raingad IM [ThinkPHP6]
* @author xiekunyu <raingad@foxmail.com>
*/
namespace app\enterprise\model;
use GatewayClient\Gateway;
use app\BaseModel;
use think\facade\Db;
use think\facade\Request;
use think\model\concern\SoftDelete;
use app\manage\model\Config;
use thans\jwt\facade\JWTAuth;
class User extends BaseModel
{
use SoftDelete;
protected $pk = "user_id";
public static $defaultField = 'user_id,realname,account,avatar,name_py,email,last_login_ip';
protected $json = ['setting'];
protected $jsonAssoc = true;
public function getUid()
{
return self::$uid;
}
//查询用户信息
public static function getUserInfo($map=[])
{
if(!$map){
return self::$userInfo;
}
$data = self::where($map)->find();
if ($data) {
$data = $data->toArray();
}
return $data;
}
/**
* 刷新用户token 之前token将被拉黑
* 修改用户数据后 调用该方法 并返回前台更新token
* @param array $info 用户信息
* @param string $terminal 客户端标识
* @return string
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public static function refreshToken($info,$terminal)
{
$info = str_encipher(json_encode($info),true, config('app.aes_token_key'));
$authToken = 'bearer '.JWTAuth::builder(['info' => $info, 'terminal' => $terminal]);
return $authToken;
}
// 获取所有用户列表
public static function getAllUser($map, $user_ids = [],$user_id,$group_id = 0)
{
$field = self::$defaultField;
$list=[];
if($group_id){
$groupUser=GroupUser::where([['group_id','=',$group_id],['role','<>',1],['status','=',1]])->column('user_id');
if($groupUser){
$list=User::where([['user_id','in',$groupUser]])->field($field)->select()->toArray();
}
}else{
$config=Config::getSystemInfo();
// 如果是社区模式,就只查询自己的好友,如果是企业模式,就查询所有用户
if($config['sysInfo']['runMode']==1){
$list = self::where($map)->field($field)->select()->toArray();
}else{
$friendList = Friend::getFriend(['create_user' => $user_id,'status'=>1]);
$userList = array_keys($friendList);
$list = self::where($map)->where('user_id', 'in', $userList)->field($field)->select()->toArray();
}
}
foreach ($list as $k => $v) {
$list[$k]['disabled'] = false;
$list[$k]['avatar'] = avatarUrl($v['avatar'], $v['realname'], $v['user_id']);
if ($user_ids) {
if (in_array($v['user_id'], $user_ids)) {
$list[$k]['disabled'] = true;
}
}
}
return $list;
}
//查询用户列表
public static function getUserList($map, $user_id, $field = "")
{
if (!$field) {
$field = self::$defaultField;
}
$friendList = Friend::getFriend(['create_user' => $user_id]);
$config=Config::getSystemInfo();
// 如果是社区模式,就只查询自己的好友,如果是企业模式,就查询所有用户
if($config['sysInfo']['runMode']==1){
$list = self::where($map)->field($field)->select();
}else{
$userList = array_keys($friendList);
$list = self::where($map)->where('user_id', 'in', $userList)->field($field)->select();
}
$list_chart = chartSort($list, 'realname', false, 'index');
// 查询未读消息
$unread = Db::name('message')
->field('from_user,count(msg_id) as unread')
->where([['to_user', '=', $user_id], ['is_read', '=', 0], ['is_group', '=', 0]])
->group('from_user')
->select();
// 查询最近的联系人
$map1 = [['to_user', '=', $user_id], ['is_last', '=', 1], ['is_group', '=', 0]];
$map2 = [['from_user', '=', $user_id], ['is_last', '=', 1], ['is_group', '=', 0]];
$msgField = 'from_user,to_user,content as lastContent,create_time as lastSendTime,chat_identify,type,del_user';
$lasMsgList = Db::name('message')
->field($msgField)
->whereOr([$map1, $map2])
->order('create_time desc')
->select();
// 查询群聊
$group = Group::getMyGroup(['gu.user_id' => $user_id, 'gu.status' => 1]);
if ($group) {
$group = $group->toArray();
$group_ids = arrayToString($group, 'group_id');
$getGroupLastMsg = Db::name('message')->field($msgField)->where([['to_user', 'in', $group_ids], ['is_group', '=', 1], ['is_last', '=', 1]])->select();
foreach ($group as $k => $v) {
$setting = $v['setting'] ? json_decode($v['setting'], true) : ['manage' => 0, 'invite' => 1, 'nospeak' => 0];
$group_id = 'group-' . $v['group_id'];
$group[$k]['id'] = $group_id;
$group[$k]['account'] = $group_id;
$group[$k]['avatar'] = avatarUrl($v['avatar'], $v['displayName'], $v['group_id'], 120);
$group[$k]['name_py'] = $v['name_py'];
$group[$k]['owner_id'] = $v['owner_id'];
$group[$k]['role'] = $v['role'];
$group[$k]['is_group'] = 1;
$group[$k]['setting'] = $setting;
$group[$k]['index'] = "[2]群聊";
$group[$k]['realname'] = $v['displayName'] . " [群聊]";
$group[$k]['is_notice'] = $v['is_notice'];
$group[$k]['is_top'] = $v['is_top'];
$group[$k]['is_online'] = 1;
if ($getGroupLastMsg) {
foreach ($getGroupLastMsg as $val) {
if ($val['to_user'] == $v['group_id']) {
$group[$k]['type'] =$val['type'];
$group[$k]['lastContent'] = str_encipher($val['lastContent'],false);
$group[$k]['lastSendTime'] = $val['lastSendTime'] * 1000;
break;
}
}
}
}
}
try{
Gateway::$registerAddress = config('gateway.registerAddress');
$onlineList=Gateway::getAllUidList();
}catch(\Exception $e){
$onlineList=[];
}
foreach ($list_chart as $k => $v) {
// 是否有消息通知或者置顶聊天
$friend = isset($friendList[$v['user_id']]) ? $friendList[$v['user_id']] : [];
$list_chart[$k]['id'] = $v['user_id'];
$list_chart[$k]['displayName'] = ($friend['nickname'] ?? '') ? : $v['realname'];
$list_chart[$k]['name_py'] = $v['name_py'];
$list_chart[$k]['avatar'] = avatarUrl($v['avatar'], $v['realname'], $v['user_id'], 120);
$list_chart[$k]['lastContent'] = '';
$list_chart[$k]['unread'] = 0;
$list_chart[$k]['lastSendTime'] = time() * 1000;
$list_chart[$k]['is_group'] = 0;
$list_chart[$k]['setting'] = [];
$list_chart[$k]['last_login_ip'] = $v['last_login_ip'];
$list_chart[$k]['location'] =$v['last_login_ip'] ? implode(" ", \Ip::find($v['last_login_ip'])) : "未知";
$is_online=0;
if(isset($onlineList[$v['user_id']])){
$is_online=1;
}
$list_chart[$k]['is_online'] = $is_online;
$is_top = 0;
$is_notice = 1;
if ($friend) {
$is_top = $friend['is_top'];
$is_notice = $friend['is_notice'];
}
$list_chart[$k]['is_top'] = $is_top;
$list_chart[$k]['is_notice'] = $is_notice;
if ($unread) {
foreach ($unread as $val) {
if ($val['from_user'] == $v['user_id']) {
$list_chart[$k]['unread'] = $val['unread'];
break;
}
}
}
if ($lasMsgList) {
foreach ($lasMsgList as $val) {
if ($val['from_user'] == $v['user_id'] || $val['to_user'] == $v['user_id']) {
$content = str_encipher($val['lastContent'],false);
// 屏蔽已删除的消息
if ($val['del_user']) {
$delUser = explode(',', $val['del_user']);
if (in_array($user_id, $delUser)) {
$content = "";
}
}
$list_chart[$k]['type'] = $val['type'];
$list_chart[$k]['lastContent'] = $content;
$list_chart[$k]['lastSendTime'] = $val['lastSendTime'] * 1000;
break;
}
}
}
}
// 合并群聊和联系人
$data = array_merge($list_chart, $group);
return $data;
}
public static function getList($map)
{
return self::field(self::$defaultField)->where($map)->select();
}
// 匹配用户列表信息(返回用户信息)
public static function matchUser($data, $many = false, $field = 'user_id', $cs = 80)
{
if ($many) {
$idr = arrayToString($data, $field, false);
} else {
$idr = [];
if (is_array($field)) {
foreach ($field as $v) {
$idr[] = $data[$v];
}
} else {
$idr = [$data[$field]];
}
}
$key = array_search(0, $idr);
if ($key) {
array_splice($idr, $key, 1);
}
$userList = self::where([['user_id', 'in', $idr]])->field(self::$defaultField)->select()->toArray();
$list = [];
foreach ($userList as $v) {
$v['avatar'] = avatarUrl($v['avatar'], $v['realname'], $v['user_id'], $cs);
$v['id'] = $v['user_id'];
$list[$v['user_id']] = $v;
}
return $list;
}
// 匹配用户列表信息返回data
public static function matchAllUser($data, $many = false, $field = 'user_id', $key = "userInfo", $cs = 80)
{
if ($many) {
$idr = arrayToString($data, $field);
$userList = self::getList([['user_id', 'in', $idr]]);
foreach ($data as $k => $v) {
foreach ($userList as $vv) {
if ($v[$field] == $vv['user_id']) {
$data[$k][$key] = [
'id' => $vv['user_id'],
'displayName' => $vv['realname'],
'account' => $vv['account'],
'name_py' => $vv['name_py'],
'avatar' => avatarUrl($vv['avatar'], $vv['realname'], $vv['user_id'], $cs),
];
}
}
}
} else {
$user = self::getUserInfo(['user_id' => $data[$field]]);
$data[$key] = [
'id' => $user['user_id'],
'displayName' => $user['realname'],
'account' => $user['account'],
'name_py' => $user['name_py'],
'avatar' => avatarUrl($user['avatar'], $user['realname'], $user['user_id']),
];
}
return $data;
}
// 将用户信息转换成联系人信息
public static function setContact($user_id){
$user=self::where('user_id',$user_id)->field(self::$defaultField)->find();
if($user){
$user['avatar']=avatarUrl($user['avatar'],$user['realname'],$user['user_id']);
$user['id']=$user['user_id'];
$user['displayName']=$user['realname'];
$user['lastContent']="你们已经成功添加为好友,现在开始聊天吧!";
$user['unread']=0;
$user['lastSendTime']=time() * 1000;
$user['is_group']=0;
$user['is_top']=0;
$user['is_notice']=1;
$user['is_online']=1;
$user['type']='event';
$user['index']=getFirstChart($user['realname']);
return $user;
}else{
return false;
}
}
// 验证账号的合法性
public function checkAccount(&$data){
$user_id=$data['user_id'] ?? 0;
if($user_id){
$user=self::find($data['user_id']);
if(!$user){
$this->error='账户不存在';
return false;
}
if($user->user_id==1 && self::$uid!=1){
$this->error='超管账户只有自己才能修改';
return false;
}
$other=self::where([['account','=',$data['account']],['user_id','<>',$data['user_id']]])->find();
if($other){
$this->error='账户已存在';
return false;
}
}else{
$user=self::where('account',$data['account'])->find();
if($user){
$this->error='账户已存在';
return false;
}
}
$config=Config::getSystemInfo();
$regauth=$config['sysInfo']['regauth'] ?? 0;
$acType=\utils\Regular::check_account($data['account']);
switch($regauth){
case 1:
if($acType!=1){
$this->error='当前系统只允许账号为手机号!';
return false;
}
break;
case 2:
if($acType!=2){
$this->error='当前系统只允许账号为邮箱!';
return false;
}
break;
case 3:
// 验证账号是否为手机号或者邮箱
if(!$acType){
$this->error='账户必须为手机号或者邮箱';
return false;
}
break;
default:
break;
}
$data['is_auth'] =$regauth ? 1 : 0;
$email=$data['email'] ?? '';
if($data['is_auth'] && $acType==2 && !$email){
$data['email'] =$data['account'];
}
return true;
}
}

View File

@ -0,0 +1,24 @@
<?php
/**
* lvzhe [a web admin based ThinkPHP5]
*/
namespace app\enterprise\validate;
use think\Validate;
class User extends Validate
{
protected $rule = [
'account|帐号' => 'require',
'password|密码' => 'require',
'captcha|验证码' => 'require|captcha',
'oldpassword|旧密码' => 'require',
'repassword|重复密码' => 'require',
];
protected $scene = [
'password' => ['password', 'oldpassword', 'repassword'],
'login' => ['account', 'password'],
];
}

19
app/event.php Executable file
View File

@ -0,0 +1,19 @@
<?php
// 事件定义文件
return [
'bind' => [
],
'listen' => [
'AppInit' => [],
'HttpRun' => [],
'HttpEnd' => [],
'LogLevel' => [],
'LogWrite' => [],
'UserRegister'=>['app\common\listener\UserRegister'],
'GroupChange'=>['app\enterprise\listener\GroupChange'],
],
'subscribe' => [
],
];

121
app/index/controller/Index.php Executable file
View File

@ -0,0 +1,121 @@
<?php
namespace app\index\controller;
use app\enterprise\model\{File,Group,User};
use think\facade\View;
class Index
{
public function index()
{
if (!file_exists(PUBLIC_PATH . "install.lock")) {
return redirect(url('index/install/index'));
}
return redirect("/index.html");
}
public function view()
{
return view::fetch();
}
// 头像生成
public function avatar()
{
circleAvatar(input('str'), input('s') ?: 80, input('uid'));die;
}
// 文件下载
public function download()
{
if (strpos($_SERVER['HTTP_USER_AGENT'], 'MicroMessenger') !== false) {
throw new \think\Exception('请使用浏览器下载!',400);
}
$param = request()->param();
$file_id = $param['file_id'] ?? 0;
if (!$file_id) {
throw new \think\Exception('参数错误', 502);
}
try {
$file_id = decryptIds($file_id);
} catch (\Exception $e) {
throw new \think\Exception($e->getMessage(), 400);
}
$file = File::find($file_id);
if (!$file) {
throw new \think\Exception('该文件不存在!',404);
}
$file = $file->toArray();
// 兼容本地文件下载
$fileUrl=getDiskUrl();
if($fileUrl==request()->domain()){
$url=rtrim(public_path(),'/').$file['src'];
}else{
$url= getFileUrl($file['src']);
}
return \utils\File::download($url, $file['name'] . '.' . $file['ext'], $file['size'], $file['ext']);
}
// 扫码获取信息
public function scanQr(){
$param=request()->param();
$action=$param['action'] ?? '';
$token=$param['token'] ?? '';
$realToken=$param['realToken'] ?? '';
if(request()->isPost() && $action && $token && $realToken){
$actions=[
'g'=>'group',
'u'=>'user',
];
$a=$actions[$action] ?? '';
if(!$a){
return warning('二维码已失效');
}
return $this->$a($param);
}else{
return $this->index();
}
}
protected function group($param)
{
$token=authcode(urldecode($param['realToken']),"DECODE", 'qr');
if(!$token){
return warning('二维码已失效');
}
$groupInfo=explode('-',$token);
$uid=$groupInfo[0];
$group_id=$groupInfo[1];
$group=Group::find($group_id);
if($group){
$group=$group->toArray();
$group['avatar']=avatarUrl($group['avatar'],$group['name'],$group_id,120);
$group['invite_id']=$uid;
$group['id']='group-'.$group_id;
$group['action']='groupInfo';
return success('',$group);
}else{
return warning('二维码已失效');
}
}
protected function user($param)
{
$id=decryptIds($param['token']);
if(!$id){
return warning('二维码已失效');
}
$user=User::where(['user_id'=>$id])->field(User::$defaultField)->find();
if($user){
$user=$user->toArray();
$user['avatar']=avatarUrl($user['avatar'],$user['realname'],$user['user_id'],120);
$user['id']=$user['user_id'];
$user['action']='userInfo';
return success('',$user);
}else{
return warning('二维码已失效');
}
}
}

528
app/index/controller/Install.php Executable file
View File

@ -0,0 +1,528 @@
<?php
// +----------------------------------------------------------------------
// | Description: 安装
// +----------------------------------------------------------------------
// | Author: xiekunyu | raingad@foxmail.com
// +----------------------------------------------------------------------
namespace app\index\controller;
use think\facade\Request;
use think\facade\Db;
use think\facade\View;
use think\facade\Config;
use Env;
class Install
{
// private $count = 100;
// private $now = 0;
protected $status=1;
public function _initialize()
{
/*防止跨域*/
header('Access-Control-Allow-Origin: '.$_SERVER['HTTP_ORIGIN']);
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header("Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, authKey, sessionId");
}
/**
* [index 安装步骤]
* @author Michael_xu
* @param
*/
public function index()
{
$protocol = strpos(strtolower($_SERVER['SERVER_PROTOCOL']), 'https') === false ? 'http' : 'https';
if (file_exists(PUBLIC_PATH . "install.lock")) {
echo "<meta http-equiv='content-type' content='text/html; charset=UTF-8'> <script>alert('请勿重复安装!');location.href='".$protocol."://".$_SERVER["HTTP_HOST"]."';</script>";
die();
}
if (!file_exists(PUBLIC_PATH . "sql/database.sql")) {
echo "<meta http-equiv='content-type' content='text/html; charset=UTF-8'> <script>alert('缺少必要的数据库文件!');location.href='".$protocol."://".$_SERVER["HTTP_HOST"]."';</script>";
die();
}
return View::fetch('index');
}
// 检测环境配置和文件夹读写权限
public function getEnv()
{
$data = [];
$data['env'] = self::checkEnv();
$data['dir'] = self::checkDir();
$data['version'] = $this->version();
$data['status'] = $this->status;
return success('',$data);
}
//版本
public function version()
{
$res = include(CONF_PATH.'version.php');
return $res ? : array('VERSION' => '0.5.18','RELEASE' => '20210518');
}
// 检查数据库
public function checkDatabase(){
if (file_exists(PUBLIC_PATH . "install.lock")) {
return warning('请勿重复安装!');
}
if (!file_exists(PUBLIC_PATH . "sql/database.sql")) {
return warning('缺少必要的数据库文件!');
}
$temp = request()->param();
$db_config = $temp['form'];
$db_config['type'] = 'mysql';
if (empty($db_config['hostname'])) {
return warning('请填写数据库主机!');
}
if (empty($db_config['hostport'])) {
return warning('请填写数据库端口!');
}
if (preg_match('/[^0-9]/', $db_config['hostport'])) {
return warning('数据库端口只能是数字!');
}
if (empty($db_config['database'])) {
return warning('请填写数据库名!');
}
if (empty($db_config['username'])) {
return warning('请填写数据库用户名!');
}
if (empty($db_config['password'])) {
return warning('请填写数据库密码!');
}
if (empty($db_config['prefix'])) {
return warning('请填写表前缀!');
}
if (empty($db_config['redishost'])) {
return warning('请填写redis主机地址!');
}
if (empty($db_config['redisport'])) {
return warning('请填写redis端口!');
}
if (preg_match('/[^a-z0-9_]/i', $db_config['prefix'])) {
return warning('表前缀只能包含数字、字母和下划线!');
}
// 创建数据库配置文件
self::mkDatabase($db_config);
// 检测数据库连接
try{
$conn=mysqli_connect($db_config['hostname'], $db_config['username'], $db_config['password']);
// 检测连接
if ($conn->connect_error) {
return warning("连接失败: " . $conn->connect_error);
}
// 创建数据库
$sql = "CREATE DATABASE IF NOT EXISTS `".$db_config['database']."` default collate utf8_general_ci ";
if ($conn->query($sql) === TRUE) {
return success('数据库连接成功',['status'=>1]);
} else{
return warning('没有找到您填写的数据库名且无法创建!请检查连接账号是否有创建数据库的权限!');
}
}catch(\Exception $e){
return warning('数据库连接失败,请检查数据库配置!');
}
}
// 执行安装
public function install(){
$db_config=Config::get('database.connections.mysql');
$sql = file_get_contents( PUBLIC_PATH . "sql/database.sql");
$sqlList = parse_sql($sql, 0, ['yu_' => $db_config['prefix']]);
$install_count=0;
if ($sqlList) {
$sqlList = array_filter($sqlList);
$install_count = count($sqlList);
foreach ($sqlList as $k=>$v) {
try {
$temp_sql = $v.';';
Db::query($temp_sql);
} catch(\Exception $e) {
touch(PUBLIC_PATH . "install.lock");
return error('数据库sql安装出错请操作数据库手动导入sql文件'.$e->getMessage());
}
}
}
touch(PUBLIC_PATH . "install.lock");
return success('安装成功',['status'=>$this->status],$install_count);
}
//ajax 进度条
public function progress()
{
$data['length'] = session('install_count');
$data['now'] = session('install_now');
return success('',$data);
}
//添加database.php文件
private function mkDatabase(array $data)
{
$code = <<<INFO
APP_DEBUG = true
[APP]
DEFAULT_TIMEZONE = Asia/Shanghai
[DATABASE]
TYPE = {$data['type']}
HOSTNAME = {$data['hostname']}
DATABASE = {$data['database']}
USERNAME = {$data['username']}
PASSWORD = {$data['password']}
HOSTPORT = {$data['hostport']}
CHARSET = utf8
DEBUG = true
prefix = {$data['prefix']}
[LANG]
default_lang = zh-cn
[REDIS]
HOST = {$data['redishost']}
PORT = {$data['redisport']}
PASSWORD ={$data['redispass']}
[AES]
TOKEN_KEY = tHTi8USApxsdfnhTM
LOGIN_KEY = t2fe6HMnmssswDVi2
#聊天内容加密,如果不加密则留空,一旦加密就不能修改,如果修改了需要清空所有聊天记录
CHAT_KEY =
[JWT]
SECRET = 17b190c0d612321f94f57325ae5a8b4c
TTL = 2592000
[WORKER]
NAME = businessWorker
PORT = 8282
# 根据自己的核心数而配置
COUNT = 1
START_PORT = 2300
REGISTER_ADDRESS =127.0.0.1:1236
lAN_IP = 127.0.0.1
# 分部署部署只需要启动一个gateway其他的gateway只需要配置register_address即可
REGISTER_DEPLOY = true
#配置预览功能,本系统主要使用第三方的预览工具,比如永中云转换,自带预览系统
[PREVIEW]
# 自带预览系统URL主要用于预览媒体文件已内置必须要有最后的/斜杠
own=http://view.riangad.com/
# 永中云文件预览,主要用于文档预览,必须要有最后的/斜杠
yzdcs=
# 永中云api code
keycode=17444844212312
# 配置对象储存,主要用于聊天文件储存,可以通过后台进行配置
[FILESYSTEM]
driver=local
aliyun_accessId=false
aliyun_accessSecret=false
aliyun_bucket=false
aliyun_endpoint=false
aliyun_url=false
qiniu_accessKey=false
qiniu_secretKey=false
qiniu_bucket=false
qiniu_url=false
qcloud_region=false
qcloud_appId=false
qcloud_secretId=false
qcloud_secretKey=false
qcloud_bucket=false
qcloud_cdn=false
INFO;
@file_put_contents( root_path().'.env', $code);
$database=env('database.database');
// 判断写入是否成功
if (empty($database) || $database != $data['database']) {
return warning('[.env]数据库配置写入失败!');
}
return true;
}
//添加database.php文件
private function mkDatabase1(array $data)
{
$code = <<<INFO
<?php
return [
// 自定义时间查询规则
'time_query_rule' => [],
// 自动写入时间戳字段
// true为自动识别类型 false关闭
// 字符串则明确指定时间字段类型 支持 int timestamp datetime date
'auto_timestamp' => true,
// 时间字段取出后的默认时间格式
'datetime_format' => 'Y-m-d H:i:s',
'default' => '{$data['type']}',
'connections' => [
'mysql' => [
// 数据库类型
'type' =>env('database.type', '{$data['type']}'),
// 服务器地址
'hostname' => env('database.hostname','{$data['hostname']}'),
// 数据库名
'database' => env('database.database','{$data['database']}'),
// 用户名
'username' => env('database.username','{$data['username']}'),
// 密码
'password' => env('database.password','{$data['password']}'),
// 端口
'hostport' => env('database.hostport','{$data['hostport']}'),
// 数据库连接参数
'params' => [],
// 数据库编码默认采用utf8
'charset' => env('database.charset', 'utf8'),
// 数据库表前缀
'prefix' => env('database.prefix', '{$data['prefix']}'),
// 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
'deploy' => 0,
// 数据库读写是否分离 主从式有效
'rw_separate' => false,
// 读写分离后 主服务器数量
'master_num' => 1,
// 指定从服务器序号
'slave_no' => '',
// 是否严格检查字段是否存在
'fields_strict' => true,
// 是否需要断线重连
'break_reconnect' => false,
// 监听SQL
'trigger_sql' => env('app_debug', true),
// 开启字段缓存
'fields_cache' => false,
// 字段缓存路径
'schema_cache_path' => app()->getRuntimePath() . 'schema' . DIRECTORY_SEPARATOR,
]
]
];
INFO;
file_put_contents( CONF_PATH.'database.php', $code);
// 判断写入是否成功
$config = include CONF_PATH.'database.php';
if (empty($config['database']) || $config['database'] != $data['database']) {
return warning('[config/database.php]数据库配置写入失败!');
}
return true;
}
//检查目录权限
public function check_dir_iswritable($dir_path){
$dir_path=str_replace( '\\','/',$dir_path);
$is_writale=1;
if (!is_dir($dir_path)) {
$is_writale=0;
return $is_writale;
} else {
$file_hd=@fopen($dir_path.'/test.txt','w');
if (!$file_hd) {
@fclose($file_hd);
@unlink($dir_path.'/test.txt');
$is_writale=0;
return $is_writale;
}
$dir_hd = opendir($dir_path);
while (false !== ($file=readdir($dir_hd))) {
if ($file != "." && $file != "..") {
if (is_file($dir_path.'/'.$file)) {
//文件不可写,直接返回
if (!is_writable($dir_path.'/'.$file)) {
return 0;
}
} else {
$file_hd2=@fopen($dir_path.'/'.$file.'/test.txt','w');
if (!$file_hd2) {
@fclose($file_hd2);
@unlink($dir_path.'/'.$file.'/test.txt');
$is_writale=0;
return $is_writale;
}
//递归
$is_writale=$this->check_dir_iswritable($dir_path.'/'.$file);
}
}
}
}
return $is_writale;
}
/**
* 环境检测
* @return array
*/
private function checkEnv()
{
// $items = [
// 'os' => ['操作系统', PHP_OS, '类Unix', 'ok'],
// 'php' => ['PHP版本', PHP_VERSION, '7.3 ( <em style="color: #888; font-size: 12px;">>= 7.0</em> )', 'ok','性能更佳'],
// 'gd' => ['gd', '开启', '开启', 'ok'],
// 'openssl' => ['openssl', '开启', '开启', 'ok'],
// 'pdo' => ['pdo', '开启', '开启', 'ok'],
// ];
$items = [
['name'=>'操作系统','alias'=>'os','value'=>PHP_OS,'status'=> 'ok','description'=>"操作系统需要类Unix"],
['name'=>'PHP版本','alias'=>'version','value'=> PHP_VERSION, 'status'=>'ok','description'=>"PHP版本必须大于7.0"],
['name'=>'gd库','alias'=>'gd', 'value'=>'开启', 'status'=>'ok','description'=>"开启GD库"],
['name'=>'pdo','alias'=>'pdo', 'value'=>'开启', 'status'=>'ok','description'=>"PDO扩展"],
['name'=>'openssl','alias'=>'openssl', 'value'=>'开启', 'status'=>'ok','description'=>"OPENSSL扩展"],
['name'=>'pcntl','alias'=>'pcntl', 'value'=>'开启', 'status'=>'ok','description'=>"pcntl扩展消息推送必须开启"],
['name'=>'posix','alias'=>'posix', 'value'=>'开启', 'status'=>'ok','description'=>"posix扩展消息推送必须开启"],
['name'=>'event','alias'=>'event', 'value'=>'开启', 'status'=>'ok','description'=>"event选择安装,处理消息推送高并发"],
];
foreach($items as $k=>$v){
$status='ok';
switch($v['alias']){
case 'php':
if (substr($v['value'],0,3) < '7.0') {
$status='no';
$this->status=0;
}
break;
case 'gd':
if (!extension_loaded('gd')) {
$items[$k]['value'] = '未开启';
$status='no';
$this->status=0;
}
break;
case 'openssl':
if (!extension_loaded('openssl')) {
$items[$k]['value'] = '未开启';
$status='no';
$this->status=0;
}
break;
case 'pdo':
if (!extension_loaded('pdo')) {
$this->status=0;
$items[$k]['value'] = '未开启';
$status='no';
}
break;
case 'pcntl':
if (PHP_OS === 'Linux') {
if (!extension_loaded('pcntl')) {
$items[$k]['value'] = '未开启';
$status='no';
}
} else {
$items[$k]['value'] = 'win无需开启';
}
break;
case 'posix':
if (PHP_OS === 'Linux') {
if (!extension_loaded('posix')) {
$this->status=0;
$items[$k]['value'] = '未开启';
$status='no';
}
} else {
$items[$k]['value'] = 'win无需开启';
}
break;
case 'event':
if (PHP_OS === 'Linux') {
if (!extension_loaded('event')) {
$items[$k]['value'] = '未开启';
$status='no';
}
} else {
$items[$k]['value'] = 'win无需开启';
}
break;
}
$items[$k]['status'] = $status;
}
return $items;
}
/**
* 目录权限检查
* @return array
*/
private function checkDir()
{
$items = [
['dir', root_path().'app', 'app', '读写', '读写', 'ok'],
['dir', root_path().'extend', 'extend', '读写', '读写', 'ok'],
['dir', root_path().'runtime', './temp', '读写', '读写', 'ok'],
['dir', root_path().'public', './upload', '读写', '读写', 'ok'],
['file', root_path().'config', 'config', '读写', '读写', 'ok'],
];
$items = [
['path'=>root_path().'app', 'dir'=>'app', 'value'=>'读写', 'type'=>'dir','status'=>'ok'],
['path'=>root_path().'extend', 'dir'=>'extend', 'value'=>'读写', 'type'=>'dir','status'=>'ok'],
['path'=> root_path().'runtime', 'dir'=>'runtime', 'value'=>'读写', 'type'=>'dir','status'=>'ok'],
['path'=>root_path().'public', 'dir'=>'public', 'value'=>'读写', 'type'=>'dir','status'=>'ok'],
['path'=>root_path().'config', 'dir'=>'config', 'value'=>'读写', 'type'=>'file','status'=>'ok'],
];
$status=1;
foreach ($items as $k=>$v) {
if ($v['type'] == 'dir') {// 文件夹
if (!is_writable($v['path'])) {
if (is_dir($v['path'])) {
$items[$k]['value'] = '不可写';
$items[$k]['status'] = 'no';
} else {
$items[$k]['value'] = '不存在';
$items[$k]['status'] = 'no';
}
$this->status=0;
}
} else {// 文件
if (!is_writable($v['path'])) {
$items[$k]['value'] = '不可写';
$items[$k]['status'] = 'no';
$this->status=0;
}
}
}
return $items;
}
/**
* 验证序列号
* @param
* @return
*/
public function checkCodeOld($username) {
$encryption = md5($username);
$substr = substr($username, strlen($username)-6);
$subArr = str_split($substr, 1);
$code = '';
for ($i = 0; $i <= 5; $i++) {
$code .= $encryption[$subArr[$i]];
}
return $code;
}
//写入license文件
private function mkLicense($wkcode)
{
file_put_contents( CONF_PATH.'license.dat', $wkcode);
// 判断写入是否成功
// $config = include CONF_PATH.'license.dat';
// if (empty($config)) {
// return resultArray(['error' => 'license配置写入失败']);
// }
return true;
}
}

15
app/index/route/app.php Executable file
View File

@ -0,0 +1,15 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
use think\facade\Route;
Route::rule('avatar/:str/:s/:uid','index/avatar');
Route::rule('view','index/index/view');
Route::rule('filedown/:file_id','index/download');
Route::rule('scan/:action/:token','index/scanQr');

102
app/manage/controller/Config.php Executable file
View File

@ -0,0 +1,102 @@
<?php
/**
* Created by PhpStorm
* User Julyssn
* Date 2022/12/14 17:24
*/
namespace app\manage\controller;
use app\BaseController;
use app\manage\model\{Config as Conf};
use think\facade\Cache;
class Config extends BaseController
{
/**
* 获取单个配置
* @return \think\response\Json
*/
public function getInfo()
{
$name=$this->request->param('name');
$data = Conf::where(['name'=>$name])->value('value');
return success('', $data);
}
/**
* 获取配置
* @return \think\response\Json
*/
public function getAllConfig()
{
$name=['sysInfo','chatInfo','smtp','fileUpload'];
$list = Conf::where(['name'=>$name])->select();
return success('', $list);
}
/**
* 修改配置
* @return \think\response\Json
*/
public function setConfig()
{
$name = $this->request->param('name');
$value = $this->request->param('value');
if(Conf::where(['name'=>$name])->find()){
Conf::where(['name'=>$name])->update(['value'=>$value]);
}else{
Conf::create(['name'=>$name,'value'=>$value]);
}
if($name=='fileUpload'){
updateEnv('driver',$value['disk']);
updateEnv('own',$value['preview']);
foreach ($value['aliyun'] as $k=>$v){
if($v){
updateEnv('aliyun_'.$k,$v);
}
}
foreach ($value['qiniu'] as $k=>$v){
if($v){
updateEnv('qiniu_'.$k,$v);
}
}
foreach ($value['qcloud'] as $k=>$v){
if($v){
updateEnv('qcloud_'.$k,$v);
}
}
}else{
// 更新系统缓存
Conf::getSystemInfo(true);
}
return success('保存成功');
}
/**
* 获取邀请链接
* @return \think\response\Json
*/
public function getInviteLink(){
$uid=$this->userInfo['user_id'];
// 邀请码仅两天有效
$code=\utils\Str::random(8);
Cache::set($code,$uid,172800);
$url=request()->domain().'/index.html/#/register?inviteCode='.$code;
return success('',$url);
}
// 发送测试邮件
public function sendTestEmail(){
$email=$this->request->param('email');
if(!$email || !(\utils\Regular::is_email($email))){
return warning('请输入正确的邮箱');
}
$conf=Conf::where(['name'=>'smtp'])->value('value');
$mail=new \mail\Mail($conf);
$mail->sendEmail([$email],'测试邮件','这是一封测试邮件,当您收到之后表明您的所有配置都是正确的!');
return success('发送成功');
}
}

166
app/manage/controller/Group.php Executable file
View File

@ -0,0 +1,166 @@
<?php
/**
* Created by PhpStorm
* User raingad@foxmail.com
* Date 2022/12/14 17:24
*/
namespace app\manage\controller;
use app\BaseController;
use app\enterprise\model\{User as UserModel,GroupUser,Group as GroupModel};
use think\facade\Db;
class Group extends BaseController
{
// 获取群聊列表
public function index()
{
$map = [];
$model=new GroupModel();
$param = $this->request->param();
//搜索关键词
if ($keyword = $this->request->param('keywords')) {
$model = $model->whereLike('name|name_py', '%' . $keyword . '%');
}
// 排序
$order='group_id DESC';
if ($param['order_field'] ?? '') {
$order = orderBy($param['order_field'],$param['order_type'] ?? 1);
}
$list = $this->paginate($model->where($map)->order($order));
if ($list) {
$data = $list->toArray()['data'];
$userList=UserModel::matchUser($data,true,'owner_id',120);
foreach($data as $k=>$v){
$data[$k]['avatar']=avatarUrl($v['avatar'],$v['name'],$v['group_id'],120);
$data[$k]['owner_id_info']=$userList[$v['owner_id']] ?? [];
}
}
return success('', $data, $list->total(), $list->currentPage());
}
// 更换群主
public function changeOwner()
{
$group_id = $this->request->param('group_id');
$user_id = $this->request->param('user_id');
$group=GroupModel::where('group_id',$group_id)->find();
if(!$group){
return warning('群组不存在');
}
$user=UserModel::where('user_id',$user_id)->find();
if(!$user){
return warning('用户不存在');
}
Db::startTrans();
try{
GroupUser::where('group_id',$group_id)->where('user_id',$user_id)->update(['role'=>1]);
GroupUser::where('group_id',$group_id)->where('user_id',$group->owner_id)->update(['role'=>3]);
$group->owner_id=$user_id;
$group->save();
wsSendMsg($group_id,"changeOwner",['group_id'=>'group-'.$group_id,'user_id'=>$user_id],1);
Db::commit();
return success('保存成功');
}catch (\Exception $e){
Db::rollback();
return warning('更换失败');
}
}
// 解散群聊
public function del()
{
$group_id = $this->request->param('group_id');
$group=GroupModel::where('group_id',$group_id)->find();
if(!$group){
return warning('群组不存在');
}
Db::startTrans();
try{
// 删除团队成员
GroupUser::where('group_id',$group_id)->delete();
// 删除团队
GroupModel::destroy($group_id);
wsSendMsg($group_id,"removeGroup",['group_id'=>'group-'.$group_id],1);
Db::commit();
return success('解散成功');
}catch (\Exception $e){
Db::rollback();
return warning('解散失败');
}
}
// 添加群成员
public function addGroupUser(){
$param = $this->request->param();
$uid=$this->userInfo['user_id'];
$group_id = $param['group_id'];
$group=GroupModel::where('group_id',$group_id)->find();
if(!$group){
return warning('群组不存在');
}
$user_ids=$param['user_ids'];
$data=[];
try{
foreach($user_ids as $k=>$v){
$data[]=[
'group_id'=>$group_id,
'user_id'=>$v,
'role'=>3,
'invite_id'=>$uid
];
}
$groupUser=new GroupUser;
$groupUser->saveAll($data);
$url=GroupModel::setGroupAvatar($group_id);
wsSendMsg($group_id,"addGroupUser",['group_id'=>"group-".$group_id,'avatar'=>$url],1);
return success('添加成功');
}catch(\Exception $e){
return error($e->getMessage());
}
}
// 删除群成员
public function delGroupUser(){
$param = $this->request->param();
$group_id = $param['group_id'];
$group=GroupModel::where('group_id',$group_id)->find();
if(!$group){
return warning('群组不存在');
}
$user_id=$param['user_id'];
$groupUser=GroupUser::where(['group_id'=>$group_id,'user_id'=>$user_id])->find();
if($groupUser){
$groupUser->delete();
wsSendMsg($group_id,"removeUser",['group_id'=>'group-'.$group_id],1);
return success('删除成功');
}else{
return warning('删除失败!');
}
}
// 设置管理员
public function setManager(){
$param = $this->request->param();
$group_id = $param['group_id'];
$group=GroupModel::where('group_id',$group_id)->find();
if(!$group){
return warning('群组不存在');
}
$user_id=$param['user_id'];
$role=$param['role'];
$groupUser=GroupUser::where(['group_id'=>$group_id,'user_id'=>$user_id])->find();
if($groupUser){
$groupUser->role=$role;
$groupUser->save();
wsSendMsg($group_id,"setManager",['group_id'=>'group-'.$group_id],1);
return success('设置成功');
}else{
return warning('设置失败!');
}
}
}

188
app/manage/controller/Task.php Executable file
View File

@ -0,0 +1,188 @@
<?php
/**
* Created by PhpStorm
* User Julyssn
* Date 2022/12/14 17:24
*/
namespace app\manage\controller;
use app\BaseController;
use easyTask\Terminal;
use think\App;
use think\facade\Console;
use think\Response;
class Task extends BaseController
{
/**
* 项目根目录
* @var string
*/
protected $rootPath;
protected $taskNames = [
'schedule' => '计划任务',
'queue' => '消息队列',
'worker' => '消息推送'
];
public function __construct(App $app)
{
parent::__construct($app);
$this->rootPath = root_path();
chdir($this->rootPath);
}
/**
* 任务列表
* @return Response
*/
public function getTaskList()
{
$data = $this->taskMsg();
if (!count($data)) {
return warning('进程未启动');
}
foreach ($data as &$datum) {
$expName = explode('_', $datum['name']);
$datum['remark'] = $this->taskNames[$expName[count($expName) - 1]];
}
unset($datum);
return success('', $data);
}
/**
* 启动全部进程
* @return Response
*/
public function startTask()
{
if(strpos(strtolower(PHP_OS), 'win') === 0)
{
return warning("windows启动请运行根目录下的start_for_win.bat");
}
if (count($this->taskMsg())) {
return warning('进程已启动');
}
// 启动
$out = Terminal::instance(2)->exec('php think task start');
if (!count($this->analysisMsg($out))) {
return warning('启动失败');
}
return success('启动成功');
}
/**
* 强制停止全部进程
* @return Response
*/
public function stopTask()
{
if (!count($this->taskMsg())) {
return warning('进程未启动');
}
// 强制停止
Terminal::instance(2)->exec('php think task stop force');
return success('停止成功');
}
/**
* 获取单个任务日志
* @return Response
*/
public function getTaskLog()
{
$name = $this->request->param('name');
$path = $this->rootPath . 'runtime' . DIRECTORY_SEPARATOR . 'easy_task' . DIRECTORY_SEPARATOR . 'Std' . DIRECTORY_SEPARATOR;
if (!file_exists($path . 'exec_' . $name . '.std')) {
$expName = explode('_', $name);
$name = $expName[count($expName) - 1];
if (!file_exists($path . 'exec_' . $name . '.std')) {
return warning('日志不存在');
}
}
return success('', file_get_contents($path . 'exec_' . $name . '.std'));
}
/**
* 清理单个任务日志
* @return Response
*/
public function clearTaskLog()
{
$name = $this->request->param('name');
$path = $this->rootPath . 'runtime' . DIRECTORY_SEPARATOR . 'easy_task' . DIRECTORY_SEPARATOR . 'Std' . DIRECTORY_SEPARATOR;
if (!file_exists($path . 'exec_' . $name . '.std')) {
$expName = explode('_', $name);
$name = $expName[count($expName) - 1];
if (!file_exists($path . 'exec_' . $name . '.std')) {
return warning('日志不存在');
}
}
file_put_contents($path . 'exec_' . $name . '.std', '');
return success('清理成功');
}
/**
* 获取运行状态
* @return array
*/
private function taskMsg()
{
$out = Terminal::instance(2)->exec('php think task status');
return $this->analysisMsg($out);
}
/**
* 解析数据
* @param string $out 带解析数据
* @return array
*/
private function analysisMsg(string $out)
{
$re = '/│ *([\w+]+) *│ *([\w+]+)[ ]*│ *([\w+]+|[0-9- :]+) *│ *([\w+]+) *│ *([\w+]+) *│ *([\w+]+) *│/m';
preg_match_all($re, $out, $matches, PREG_SET_ORDER, 0);
if (!count($matches)) {
return [];
}
$data = [];
$names = $matches[0];
unset($names[0]);
$names = array_values($names);
unset($matches[0]);
foreach ($matches as $match) {
$temp = [];
foreach ($match as $key => $item) {
if ($key !== 0) {
$temp[$names[$key - 1]] = $item;
}
}
$data[] = $temp;
}
return $data;
}
}

192
app/manage/controller/User.php Executable file
View File

@ -0,0 +1,192 @@
<?php
/**
* Created by PhpStorm
* User raingad@foxmail.com
* Date 2022/12/14 17:24
*/
namespace app\manage\controller;
use app\BaseController;
use app\enterprise\model\{User as UserModel,GroupUser,Friend};
use app\manage\model\Config;
use think\facade\Db;
class User extends BaseController
{
// 获取用户列表
public function index()
{
$map = [];
$model=new UserModel();
$param = $this->request->param();
//搜索关键词
if ($keyword = $this->request->param('keywords')) {
$model = $model->whereLike('realname|account|name_py|email', '%' . $keyword . '%');
}
// 排序
$order='user_id DESC';
if ($param['order_field'] ?? '') {
$order = orderBy($param['order_field'],$param['order_type'] ?? 1);
}
$list = $this->paginate($model->where($map)->order($order));
if ($list) {
$data = $list->toArray()['data'];
foreach($data as $k=>$v){
$data[$k]['avatar']=avatarUrl($v['avatar'],$v['realname'],$v['user_id'],120);
$data[$k]['location']=$v['last_login_ip'] ? implode(" ", \Ip::find($v['last_login_ip'])) : '--';
$data[$k]['reg_location']=$v['register_ip'] ? implode(" ", \Ip::find($v['register_ip'])) : '--';
$data[$k]['last_login_time']=$v['last_login_time'] ? date('Y-m-d H:i:s',$v['last_login_time']) : '--';
unset($data[$k]['password']);
}
}
return success('', $data, $list->total(), $list->currentPage());
}
// 添加用户
public function add()
{
try{
$data = $this->request->param();
$user=new UserModel();
$verify=$user->checkAccount($data);
if(!$verify){
return warning($user->getError());
}
$salt=\utils\Str::random(4);
$data['password'] = password_hash_tp($data['password'],$salt);
$data['salt'] =$salt;
$data['register_ip'] =$this->request->ip();
$data['name_py'] = pinyin_sentence($data['realname']);
$user->save($data);
$data['user_id']=$user->user_id;
return success('添加成功', $data);
}catch (\Exception $e){
return error('添加失败');
}
}
// 修改用户
public function edit()
{
try{
$data = $this->request->param();
$user=new UserModel();
$verify=$user->checkAccount($data);
if(!$verify){
return warning($user->getError());
}
$user=UserModel::find($data['user_id']);
$user->account =$data['account'];
$user->realname =$data['realname'];
$user->email =$data['email'];
$user->remark=$data['remark'];
$user->sex =$data['sex'];
// 只有超管才能设置管理员
if($this->userInfo['user_id']==1){
$user->role =$data['role'];
}
$user->status =$data['status'];
$user->name_py= pinyin_sentence($data['realname']);
$user->save();
return success('修改成功', $data);
}catch (\Exception $e){
return error('修改失败');
}
}
// 删除用户
public function del()
{
$user_id = $this->request->param('user_id');
$user=UserModel::find($user_id);
if(!$user || $user->user_id==1){
return warning('用户不存在');
}
Db::startTrans();
try{
// 删除其好友关系
Friend::where('create_user', $user_id)->whereOr(['friend_user_id'=>$user_id])->delete();
// 删除其群组关系
GroupUser::where('user_id', $user_id)->delete();
UserModel::destroy($user_id);
Db::commit();
return success('删除成功');
}catch (\Exception $e){
Db::rollback();
return error($e->getMessage());
}
}
// 修改用户状态
public function setStatus()
{
$user_id = $this->request->param('user_id');
$user=UserModel::find($user_id);
if(!$user){
return warning('用户不存在');
}
try{
$status = $this->request->param('status',0);
UserModel::where('user_id', $user_id)->update(['status'=>$status]);
return success('修改成功');
}catch (\Exception $e){
return error('修改失败');
}
}
// 获取用户信息
public function detail()
{
$user_id = $this->request->param('user_id');
$user=UserModel::find($user_id);
if(!$user){
return error('用户不存在');
}
$user->avatar=avatarUrl($user->avatar,$user->realname,$user->user_id,120);
$location='';
if($user->last_login_ip){
$location=implode(" ", \Ip::find($user->last_login_ip));
}
$user->location=$location;
$user->password='';
return success('', $user);
}
// 设置用户角色
public function setRole()
{
$user_id = $this->request->param('user_id');
$user=UserModel::find($user_id);
if(!$user){
return warning('用户不存在');
}
try{
$role = $this->request->param('role');
UserModel::where('user_id', $user_id)->update(['role'=>$role]);
return success('修改成功');
}catch (\Exception $e){
return error('修改失败');
}
}
// 修改密码
public function editPassword()
{
$user_id = $this->request->param('user_id');
$user=UserModel::find($user_id);
if(!$user){
return warning('用户不存在');
}
try{
$password = $this->request->param('password','');
if($password){
$salt=$user->salt;
$user->password= password_hash_tp($password,$salt);
}
$user->save();
return success('修改成功');
}catch (\Exception $e){
return error('修改失败');
}
}
}

5
app/manage/middleware.php Executable file
View File

@ -0,0 +1,5 @@
<?php
return [
"checkAuth",
"manageAuth"
];

45
app/manage/model/Config.php Executable file
View File

@ -0,0 +1,45 @@
<?php
/**
* raingad IM [ThinkPHP6]
* @author xiekunyu <raingad@foxmail.com>
*/
namespace app\manage\model;
use app\BaseModel;
use think\facade\Cache;
class Config extends BaseModel
{
protected $json = ['value'];
protected $jsonAssoc = true;
// 获取系统配置信息
public static function getSystemInfo($update=false){
$name='systemInfo';
// $auth=request()->header('Authorization');
$nameFields=['sysInfo','fileUpload','chatInfo'];
// 如果是登录状态才会返回chatINfo
// if($auth){
// $name='all'.$name;
// $nameFields[]="chatInfo";
// }
if(Cache::has($name) && !$update){
$systemInfo=Cache::get($name);
}else{
$systemInfo=[];
$conf=Config::where([['name','in',$nameFields]])->select()->toArray();
foreach($conf as $v){
$value=[];
if($v['name']=='fileUpload'){
$value['size'] = $v['value']['size'];
$value['preview'] = $v['value']['preview'];
$value['fileExt'] = $v['value']['fileExt'];
}else{
$value=$v['value'];
}
$systemInfo[$v['name']]=$value;
}
Cache::set($name,$systemInfo,7*86400);
}
return $systemInfo;
}
}

19
app/middleware.php Executable file
View File

@ -0,0 +1,19 @@
<?php
// 全局中间件定义文件
return [
// 全局请求缓存
// 'think\middleware\CheckRequestCache',
// 多语言加载
// 'think\middleware\LoadLangPack',
// Session初始化
'think\middleware\SessionInit',
// 页面Trace调试
// 'think\middleware\TraceDebug',
];

9
app/provider.php Executable file
View File

@ -0,0 +1,9 @@
<?php
use app\ExceptionHandle;
use app\Request;
// 容器Provider定义文件
return [
'think\Request' => Request::class,
'think\exception\Handle' => ExceptionHandle::class,
];

9
app/service.php Executable file
View File

@ -0,0 +1,9 @@
<?php
use app\AppService;
// 系统服务定义文件
// 服务在完成全局初始化之后执行
return [
AppService::class,
];

105
app/worker/Application.php Executable file
View File

@ -0,0 +1,105 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
namespace app\worker;
use think\App;
use think\exception\Handle;
use think\exception\HttpException;
use Workerman\Connection\TcpConnection;
use Workerman\Protocols\Http\Response;
/**
* Worker应用对象
*/
class Application extends App
{
/**
* 处理Worker请求
* @access public
* @param \Workerman\Connection\TcpConnection $connection
* @param void
*/
public function worker(TcpConnection $connection)
{
try {
$this->beginTime = microtime(true);
$this->beginMem = memory_get_usage();
$this->db->clearQueryTimes();
$pathinfo = ltrim(strpos($_SERVER['REQUEST_URI'], '?') ? strstr($_SERVER['REQUEST_URI'], '?', true) : $_SERVER['REQUEST_URI'], '/');
$this->request
->setPathinfo($pathinfo)
->withInput($GLOBALS['HTTP_RAW_POST_DATA']);
while (ob_get_level() > 1) {
ob_end_clean();
}
ob_start();
$response = $this->http->run();
$content = ob_get_clean();
ob_start();
$response->send();
$this->http->end($response);
$content .= ob_get_clean() ?: '';
$this->httpResponseCode($response->getCode());
$header=[];
foreach ($response->getHeader() as $name => $val) {
// 发送头部信息
$header[$name] =!is_null($val) ? $val : '';
}
if (strtolower($_SERVER['HTTP_CONNECTION']) === "keep-alive") {
$connection->send(new Response(200, $header, $content));
} else {
$connection->close(new Response(200, $header, $content));
}
} catch (HttpException | \Exception | \Throwable $e) {
$this->exception($connection, $e);
}
}
/**
* 是否运行在命令行下
* @return bool
*/
public function runningInConsole(): bool
{
return false;
}
protected function httpResponseCode($code = 200)
{
new Response($code);
}
protected function exception($connection, $e)
{
if ($e instanceof \Exception) {
$handler = $this->make(Handle::class);
$handler->report($e);
$resp = $handler->render($this->request, $e);
$content = $resp->getContent();
$code = $resp->getCode();
$this->httpResponseCode(new Response($code, [], $content));
$connection->send($content);
} else {
$connection->send(new Response(500, [], $e->getMessage()));
}
}
}

142
app/worker/Events.php Executable file
View File

@ -0,0 +1,142 @@
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace app\worker;
/**
* 推送主逻辑
* 主要是处理 onMessage onClose
*/
use GatewayWorker\Lib\Gateway;
use app\worker\Application;
use think\facade\Config;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Parser;
use thans\jwt\provider\JWT\Lcobucci;
use utils\Aes;
class Events
{
// 使用TP框架
public static function onWorkerStart()
{
$app = new Application;
$app->initialize();
}
// 当有客户端连接时将client_id返回让mvc框架判断当前uid并执行绑定
public static function onConnect($client_id)
{
Gateway::sendToClient($client_id, json_encode(array(
'type' => 'init',
'client_id' => $client_id
)));
}
/**
* 有消息时
* @param int $client_id
* @param mixed $message
*/
public static function onMessage($client_id, $message)
{
// 客户端传递的是json数据
$message_data = json_decode($message, true);
if(!$message_data)
{
return ;
}
// 根据类型执行不同的业务
switch($message_data['type'])
{
// 客户端回应服务端的心跳
case 'pong':
break;
case 'ping':
self::sendStatus($client_id);
break;
case 'bindUid':
self::auth($client_id,$message_data);
break;
}
return;
}
protected static function sendStatus($client_id){
$uid=$_SESSION['user_id'] ?? 0;
$multiport=false;
if($uid){
$arr=Gateway::getClientIdByUid($uid);
if(count($arr)>1){
$multiport=true;
}
}
Gateway::sendToClient($client_id, json_encode(array(
'type' => 'pong',
'multiport' => $multiport,
)));
}
//验证用户的真实性并绑定
protected static function auth($client_id, $msg){
$token=$msg['token'] ?? '';
$config = Config::get('jwt');
$keys = $config['secret'] ?: [
'public' => $config['public_key'],
'private' => $config['private_key'],
'password' => $config['password'],
];
$provider = new Lcobucci(new Builder(), new Parser(), $config['algo'], $keys);
try {
$token=str_replace('bearer ','',$token);
$jwtData = $provider->decode((string)$token);
} catch (\Exception $exception) {
self::closeClient($client_id);
}
$userInfo = $jwtData['info']->getValue();
//解密token中的用户信息
$userInfo = Aes::decrypt($userInfo, config('app.aes_token_key'));
//解析json
$userInfo = (array)json_decode($userInfo, true);
if(!$userInfo){
self::closeClient($client_id);
}
$_SESSION['user_id']=$userInfo['user_id'];
self::sendStatus($client_id);
}
//断开连接
protected static function closeClient($client_id){
$_SESSION['user_id']=null;
Gateway::closeClient($client_id);
}
/**
* 当断开连接时
* @param int $client_id
*/
public static function onClose($client_id)
{
$user_id=$_SESSION['user_id'];
if($user_id){
Gateway::sendToAll(json_encode(array(
'type' => 'isOnline',
'time' => time(),
'data' => ['id'=>$user_id,'is_online'=>0]
)));
}
}
}

View File

@ -0,0 +1,201 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
namespace app\worker\command;
use GatewayWorker\BusinessWorker;
use GatewayWorker\Gateway;
use GatewayWorker\Register;
use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\input\Option;
use think\console\Output;
use think\facade\Config;
use Workerman\Worker;
/**
* Worker 命令行类
*/
class GatewayWorker extends Command
{
public function configure()
{
$this->setName('worker:gateway')
->addArgument('action', Argument::OPTIONAL, "start|stop|restart|reload|status|connections", 'start')
->addOption('host', 'H', Option::VALUE_OPTIONAL, 'the host of workerman server.', null)
->addOption('port', 'p', Option::VALUE_OPTIONAL, 'the port of workerman server.', null)
->addOption('daemon', 'd', Option::VALUE_NONE, 'Run the workerman server in daemon mode.')
->setDescription('GatewayWorker Server for ThinkPHP');
}
public function execute(Input $input, Output $output)
{
$action = $input->getArgument('action');
if (DIRECTORY_SEPARATOR !== '\\') {
if (!in_array($action, ['start', 'stop', 'reload', 'restart', 'status', 'connections'])) {
$output->writeln("Invalid argument action:{$action}, Expected start|stop|restart|reload|status|connections .");
exit(1);
}
global $argv;
array_shift($argv);
array_shift($argv);
array_unshift($argv, 'think', $action);
} else {
$output->writeln("GatewayWorker Not Support On Windows.");
exit(1);
}
if ('start' == $action) {
$output->writeln('Starting GatewayWorker server...');
}
$option = Config::get('gateway');
if ($input->hasOption('host')) {
$host = $input->getOption('host');
} else {
$host = !empty($option['host']) ? $option['host'] : '0.0.0.0';
}
if ($input->hasOption('port')) {
$port = $input->getOption('port');
} else {
$port = !empty($option['port']) ? $option['port'] : '2347';
}
$this->start($host, (int) $port, $option);
}
/**
* 启动
* @access public
* @param string $host 监听地址
* @param integer $port 监听端口
* @param array $option 参数
* @return void
*/
public function start(string $host, int $port, array $option = [])
{
$registerAddress = !empty($option['registerAddress']) ? $option['registerAddress'] : '127.0.0.1:1236';
if (!empty($option['register_deploy'])) {
// 分布式部署的时候其它服务器可以关闭register服务
// 注意需要设置不同的lanIp
$this->register($registerAddress);
}
// 启动businessWorker
if (!empty($option['businessWorker_deploy'])) {
$this->businessWorker($registerAddress, $option['businessWorker'] ?? []);
}
// 启动gateway
if (!empty($option['gateway_deploy'])) {
$this->gateway($registerAddress, $host, $port, $option);
}
Worker::runAll();
}
/**
* 启动register
* @access public
* @param string $registerAddress
* @return void
*/
public function register(string $registerAddress)
{
// 初始化register
new Register('text://' . $registerAddress);
}
/**
* 启动businessWorker
* @access public
* @param string $registerAddress registerAddress
* @param array $option 参数
* @return void
*/
public function businessWorker(string $registerAddress, array $option = [])
{
// 初始化 bussinessWorker 进程
$worker = new BusinessWorker();
$this->option($worker, $option);
$worker->registerAddress = $registerAddress;
}
/**
* 启动gateway
* @access public
* @param string $registerAddress registerAddress
* @param string $host 服务地址
* @param integer $port 监听端口
* @param array $option 参数
* @return void
*/
public function gateway(string $registerAddress, string $host, int $port, array $option = [])
{
// 初始化 gateway 进程
if (!empty($option['socket'])) {
$socket = $option['socket'];
unset($option['socket']);
} else {
$protocol = !empty($option['protocol']) ? $option['protocol'] : 'websocket';
$socket = $protocol . '://' . $host . ':' . $port;
unset($option['host'], $option['port'], $option['protocol']);
}
$gateway = new Gateway($socket, $option['context'] ?? []);
// 以下设置参数都可以在配置文件中重新定义覆盖
$gateway->name = 'Gateway';
$gateway->count = 4;
$gateway->lanIp = '127.0.0.1';
$gateway->startPort = 2000;
$gateway->pingInterval = 30;
$gateway->pingNotResponseLimit = 0;
$gateway->pingData = '{"type":"ping"}';
$gateway->registerAddress = $registerAddress;
// 全局静态属性设置
foreach ($option as $name => $val) {
if (in_array($name, ['stdoutFile', 'daemonize', 'pidFile', 'logFile'])) {
Worker::${$name} = $val;
unset($option[$name]);
}
}
$this->option($gateway, $option);
}
/**
* 设置参数
* @access protected
* @param Worker $worker Worker对象
* @param array $option 参数
* @return void
*/
protected function option(Worker $worker, array $option = [])
{
// 设置参数
if (!empty($option)) {
foreach ($option as $key => $val) {
$worker->$key = $val;
}
}
}
}

View File

@ -0,0 +1,32 @@
<?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 \Workerman\Worker;
use \GatewayWorker\BusinessWorker;
require_once __DIR__ . '/../../vendor/autoload.php';
// bussinessWorker 进程
$worker = new BusinessWorker();
// worker名称
$worker->name = 'PushBusinessWorker';
// bussinessWorker进程数量
$worker->count = 1;
// 服务注册地址
$worker->registerAddress = '127.0.0.1:1236';
$worker->eventHandler = 'app\worker\Events';
// 如果不是在根目录启动则运行runAll方法
if(!defined('GLOBAL_START'))
{
Worker::runAll();
}

42
app/worker/start_gateway.php Executable file
View File

@ -0,0 +1,42 @@
<?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 \Workerman\Worker;
use \GatewayWorker\Gateway;
use \Workerman\Autoloader;
require_once __DIR__ . '/../../vendor/autoload.php';
// gateway 进程
$gateway = new Gateway("Websocket://0.0.0.0:8282");
// 设置名称方便status时查看
$gateway->name = 'pushMessage';
// 设置进程数gateway进程数建议与cpu核数相同
$gateway->count = 1;
// 分布式部署时请设置成内网ip非127.0.0.1
$gateway->lanIp = '127.0.0.1';
// 内部通讯起始端口。假如$gateway->count=4起始端口为2300
// 则一般会使用2300 2301 2302 2303 4个端口作为内部通讯端口
$gateway->startPort = 2300;
// 心跳间隔
$gateway->pingInterval = 20;
// 心跳数据
$gateway->pingData = '{"type":"ping"}';
// 服务注册地址
$gateway->registerAddress = '127.0.0.1:1236';
// 如果不是在根目录启动则运行runAll方法
if(!defined('GLOBAL_START'))
{
Worker::runAll();
}

25
app/worker/start_register.php Executable file
View File

@ -0,0 +1,25 @@
<?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 \Workerman\Worker;
use \GatewayWorker\Register;
require_once __DIR__ . '/../../vendor/autoload.php';
// register 服务必须是text协议
$register = new Register('text://0.0.0.0:1236');
// 如果不是在根目录启动则运行runAll方法
if(!defined('GLOBAL_START'))
{
Worker::runAll();
}

69
composer.json Executable file
View File

@ -0,0 +1,69 @@
{
"name": "topthink/think",
"description": "the new thinkphp framework",
"type": "project",
"keywords": [
"framework",
"thinkphp",
"ORM"
],
"homepage": "http://thinkphp.cn/",
"license": "Apache-2.0",
"authors": [{
"name": "liu21st",
"email": "liu21st@gmail.com"
},
{
"name": "yunwuxin",
"email": "448901948@qq.com"
}
],
"require": {
"php": ">=7.1.0",
"topthink/framework": "^6.0.0",
"topthink/think-orm": "^2.0",
"jasny/sso": "^0.3.0",
"xiaodi/think-pullword": "^1.0",
"topthink/think-view": "^1.0",
"aliyuncs/oss-sdk-php": "^2.3",
"tcwei/imglazyload": "^1.3",
"tcwei/imgsrc": "^2.0",
"topthink/think-captcha": "^3.0",
"alibabacloud/client": "^1.5",
"xiaodi/think-pinyin": "^1.0",
"workerman/workerman": "^4.0",
"workerman/gateway-worker": "^3.0",
"workerman/gatewayclient": "^3.0",
"topthink/think-multi-app": "^1.0",
"thans/thinkphp-filesystem-cloud": "^1.0",
"topthink/think-queue": "^3.0",
"yunwuxin/think-cron": "^3.0",
"swiftmailer/swiftmailer": "^6.0",
"thans/tp-jwt-auth": "^1.3",
"singka/singka-sms": "^1.6",
"char0n/ffmpeg-php": "^3.0.0",
"ext-gd": "*"
},
"require-dev": {
"symfony/var-dumper": "^4.2",
"topthink/think-trace": "^1.0"
},
"autoload": {
"psr-4": {
"app\\": "app"
},
"psr-0": {
"": "extend/"
}
},
"config": {
"preferred-install": "dist",
"secure-http": false
},
"scripts": {
"post-autoload-dump": [
"@php think service:discover",
"@php think vendor:publish"
]
}
}

4872
composer.lock generated Executable file

File diff suppressed because it is too large Load Diff

45
config/app.php Executable file
View File

@ -0,0 +1,45 @@
<?php
// +----------------------------------------------------------------------
// | 应用设置
// +----------------------------------------------------------------------
return [
'app_name' =>"Raingad-IM",
'app_logo' =>"https://im.file.raingad.com/logo/logo.png",
'app_version' =>"3.0.4",
'app_release' =>"20230914",
// 应用地址
'app_host' => env('app.host', ''),
// 应用的命名空间
'app_namespace' => '',
// 是否启用路由
'with_route' => true,
'app_express' => true,
// 默认应用
'default_app' => 'index',
// 默认时区
'default_timezone' => 'Asia/Shanghai',
// 应用映射(自动多应用模式有效)
'app_map' => [],
// 域名绑定(自动多应用模式有效)
'domain_bind' => [],
// 禁止URL访问的应用列表自动多应用模式有效
'deny_app_list' => [],
// 异常页面的模板文件
'exception_tmpl' => app()->getThinkPath() . 'tpl/think_exception.tpl',
// 错误显示信息,非调试模式有效
'error_message' => '页面错误!请稍后再试~',
// 显示错误信息
'show_error_msg' => false,
'auto_multi_app' =>true,
//用户token加密用的秘钥
'aes_token_key' => env('AES_TOKEN_KEY', ''),
//用户LOGIN加密用的秘钥
'aes_login_key' => env('AES_LOGIN_KEY', ''),
//用户chat加密用的秘钥
'aes_chat_key' => env('AES_CHAT_KEY', ''),
];

38
config/cache.php Executable file
View File

@ -0,0 +1,38 @@
<?php
// +----------------------------------------------------------------------
// | 缓存设置
// +----------------------------------------------------------------------
return [
// 默认缓存驱动
'default' => env('cache.driver', 'redis'),
// 缓存连接方式配置
'stores' => [
'file' => [
// 驱动方式
'type' => 'File',
// 缓存保存目录
'path' => '',
// 缓存前缀
'prefix' => '',
// 缓存有效期 0表示永久缓存
'expire' => 0,
// 缓存标签前缀
'tag_prefix' => 'tag:',
// 序列化机制 例如 ['serialize', 'unserialize']
'serialize' => [],
],
'redis' => [
// 驱动方式
'type' => 'redis',
'host' =>env('redis.host', '127.0.0.1'),
'port' => env('redis.port', '6379'),
'password' => env('redis.password', ''),
// 缓存前缀
'prefix' => env('redis.prefix', ''),
]
// 更多的缓存连接
],
];

19
config/captcha.php Executable file
View File

@ -0,0 +1,19 @@
<?php
return [
// 验证码字符集合
'codeSet' => '2345678abcdefhijkmnpqrstuvwxyzABCDEFGHJKLMNPQRTUVWXY',
// 验证码字体大小(px)
'fontSize' => 20,
// 是否画混淆曲线
'useCurve' => false,
// 验证码图片高度
'imageH' => 40,
// 验证码图片宽度
'imageW' => 150,
// 验证码位数
'length' => 4,
// 验证成功后是否重置
'reset' => true
];

14
config/console.php Executable file
View File

@ -0,0 +1,14 @@
<?php
// +----------------------------------------------------------------------
// | 控制台配置
// +----------------------------------------------------------------------
return [
// 指令定义
'commands' => [
'queue:work' => think\queue\command\Work::class,
'queue:listen' => think\queue\command\Listen::class,
'queue:Restart' => think\queue\command\Restart::class,
'task' => task\command\Task::class,
'worker:gateway' => app\worker\command\GatewayWorker::class
],
];

18
config/cookie.php Executable file
View File

@ -0,0 +1,18 @@
<?php
// +----------------------------------------------------------------------
// | Cookie设置
// +----------------------------------------------------------------------
return [
// cookie 保存时间
'expire' => 0,
// cookie 保存路径
'path' => '/',
// cookie 有效域名
'domain' => '',
// cookie 启用安全传输
'secure' => false,
// httponly设置
'httponly' => false,
// 是否使用 setcookie
'setcookie' => true,
];

7
config/cron.php Executable file
View File

@ -0,0 +1,7 @@
<?php
return [
'tasks' => [
\app\common\task\ClearMessage::class, //任务的完整类名
]
];

62
config/database.php Executable file
View File

@ -0,0 +1,62 @@
<?php
return [
// 默认使用的数据库连接配置
'default' => env('database.driver', 'mysql'),
// 自定义时间查询规则
'time_query_rule' => [],
// 自动写入时间戳字段
// true为自动识别类型 false关闭
// 字符串则明确指定时间字段类型 支持 int timestamp datetime date
'auto_timestamp' => true,
// 时间字段取出后的默认时间格式
'datetime_format' => 'Y-m-d H:i:s',
// 数据库连接配置信息
'connections' => [
'mysql' => [
// 数据库类型
'type' => env('database.type', 'mysql'),
// 服务器地址
'hostname' => env('database.hostname', '127.0.0.1'),
// 数据库名
'database' => env('database.database', ''),
// 用户名
'username' => env('database.username', 'root'),
// 密码
'password' => env('database.password', ''),
// 端口
'hostport' => env('database.hostport', '3306'),
// 数据库连接参数
'params' => [],
// 数据库编码默认采用utf8
'charset' => env('database.charset', 'utf8'),
// 数据库表前缀
'prefix' => env('database.prefix', ''),
// 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
'deploy' => 0,
// 数据库读写是否分离 主从式有效
'rw_separate' => false,
// 读写分离后 主服务器数量
'master_num' => 1,
// 指定从服务器序号
'slave_no' => '',
// 是否严格检查字段是否存在
'fields_strict' => true,
// 是否需要断线重连
'break_reconnect' => false,
// 监听SQL
'trigger_sql' => env('app_debug', true),
// 开启字段缓存
'fields_cache' => false,
// 字段缓存路径
'schema_cache_path' => app()->getRuntimePath() . 'schema' . DIRECTORY_SEPARATOR,
],
// 更多的数据库配置信息
],
];

42
config/filesystem.php Executable file
View File

@ -0,0 +1,42 @@
<?php
return [
// 默认磁盘
'default' => env('filesystem.driver', 'local'),
// 磁盘列表
'disks' => [
'local' => [
'type' => 'local',
'root' => app()->getRootPath() . 'public/storage',
],
// 更多的磁盘配置信息
'aliyun' => [
'type' => 'aliyun',
'accessId' => env('filesystem.aliyun_accessId',''),
'accessSecret' => env('filesystem.aliyun_accessSecret',''),
'bucket' => env('filesystem.aliyun_bucket',''),
'endpoint' => env('filesystem.aliyun_endpoint',''),
'url' => env('filesystem.aliyun_url',''),//不要斜杠结尾此处为URL地址域名。
],
'qiniu' => [
'type' => 'qiniu',
'accessKey' => env('filesystem.qiniu_accessKey',''),
'secretKey' => env('filesystem.qiniu_secretKey',''),
'bucket' => env('filesystem.qiniu_bucket',''),
'url' => env('filesystem.qiniu_url',''),//不要斜杠结尾此处为URL地址域名。
],
'qcloud' => [
'type' => 'qcloud',
'region' => env('filesystem.qcloud_region',''),//bucket 所属区域 英文
'appId' => env('filesystem.qcloud_appId',''), // 域名中数字部分
'secretId' => env('filesystem.qcloud_secretId',''),
'secretKey' => env('filesystem.qcloud_secretKey',''),
'bucket' => env('filesystem.qcloud_bucket',''),
'timeout' => 60,
'connect_timeout' => 60,
'cdn' => env('filesystem.qcloud_cdn',''),
'scheme' => 'https',
'read_from_cdn' => false,
]
],
];

33
config/gateway.php Executable file
View File

@ -0,0 +1,33 @@
<?php
return [
// 扩展自身需要的配置
'protocol' => 'websocket', // 协议 支持 tcp udp unix http websocket text
'host' => '0.0.0.0', // 监听地址
'port' => env('worker_port',8282), // 监听端口
'socket' => '', // 完整监听地址
'context' => [], // socket 上下文选项
'register_deploy' => env('worker_register_deploy',true), // 是否需要部署register
'businessWorker_deploy' => true, // 是否需要部署businessWorker
'gateway_deploy' => true, // 是否需要部署gateway
// Register配置
'registerAddress' => env('worker_register_address','127.0.0.1:1236'),
// Gateway配置
'name' => env('worker_name','pushGateWay'),
'count' => env('worker_count',1),
'lanIp' => env('worker_lan_ip','127.0.0.1'),
'startPort' => env('worker_start_port',2300),
'daemonize' => false,
'pingInterval' => 20,
'pingNotResponseLimit' => 0,
'pingData' => '{"type":"ping"}',
// BusinsessWorker配置
'businessWorker' => [
'name' => 'BusinessWorker',
'count' => 1,
'eventHandler' => 'app\worker\Events',
],
];

16
config/hashids.php Executable file
View File

@ -0,0 +1,16 @@
<?php
/**
* tpAdmin [a web admin based ThinkPHP5]
*
* @author yuan1994 <tianpian0805@gmail.com>
* @link http://tpadmin.yuan1994.com/
* @copyright 2016 yuan1994 all rights reserved.
* @license http://www.apache.org/licenses/LICENSE-2.0
*/
return [
// Hashids 的配置项
'length' => 12, // 加密字符串长度
'salt' => 'raingads', // 加密盐值
'alphabet' => '', // 字符仓库,不填写默认为扩展里的字符仓库
];

21
config/jwt.php Executable file
View File

@ -0,0 +1,21 @@
<?php
return [
'secret' => env('JWT_SECRET'),
//Asymmetric key
'public_key' => env('JWT_PUBLIC_KEY'),
'private_key' => env('JWT_PRIVATE_KEY'),
'password' => env('JWT_PASSWORD'),
//JWT time to live
'ttl' => env('JWT_TTL', 60),
//Refresh time to live
'refresh_ttl' => env('JWT_REFRESH_TTL', 20160),
//JWT hashing algorithm
'algo' => env('JWT_ALGO', 'HS256'),
//token获取方式数组靠前值优先
'token_mode' => ['header', 'cookie', 'param'],
//黑名单后有效期
'blacklist_grace_period' => env('BLACKLIST_GRACE_PERIOD', 10),
'blacklist_storage' => thans\jwt\provider\storage\Tp5::class,
];

25
config/lang.php Executable file
View File

@ -0,0 +1,25 @@
<?php
// +----------------------------------------------------------------------
// | 多语言设置
// +----------------------------------------------------------------------
return [
// 默认语言
'default_lang' => env('lang.default_lang', 'zh-cn'),
// 允许的语言列表
'allow_lang_list' => [],
// 多语言自动侦测变量名
'detect_var' => 'lang',
// 是否使用Cookie记录
'use_cookie' => true,
// 多语言cookie变量
'cookie_var' => 'think_lang',
// 扩展语言包
'extend_list' => [],
// Accept-Language转义为对应语言包名称
'accept_language' => [
'zh-hans-cn' => 'zh-cn',
],
// 是否支持语言分组
'allow_group' => false,
];

45
config/log.php Executable file
View File

@ -0,0 +1,45 @@
<?php
// +----------------------------------------------------------------------
// | 日志设置
// +----------------------------------------------------------------------
return [
// 默认日志记录通道
'default' => env('log.channel', 'file'),
// 日志记录级别
'level' => [],
// 日志类型记录的通道 ['error'=>'email',...]
'type_channel' => [],
// 关闭全局日志写入
'close' => false,
// 全局日志处理 支持闭包
'processor' => null,
// 日志通道列表
'channels' => [
'file' => [
// 日志记录方式
'type' => 'File',
// 日志保存目录
'path' => '',
// 单文件日志写入
'single' => false,
// 独立日志级别
'apart_level' => [],
// 最大日志文件数量
'max_files' => 0,
// 使用JSON格式记录
'json' => false,
// 日志处理
'processor' => null,
// 关闭通道日志写入
'close' => false,
// 日志输出格式化
'format' => '[%s][%s] %s',
// 是否实时写入
'realtime_write' => false,
],
// 其它日志通道配置
],
];

11
config/middleware.php Executable file
View File

@ -0,0 +1,11 @@
<?php
// 中间件配置
return [
// 别名或分组
'alias' => [
'checkAuth'=>app\common\middleware\CheckAuth::class,
'manageAuth'=>app\common\middleware\ManageAuth::class,
],
// 优先级设置,此数组中的中间件会按照数组中的顺序优先执行
'priority' => [],
];

6
config/preview.php Executable file
View File

@ -0,0 +1,6 @@
<?php
return [
'own'=>env('preview.own', ''),
'yzdcs'=>env('preview.yzdcs', ''),
'keycode'=>env('preview.keycode', ''),
];

39
config/queue.php Executable file
View File

@ -0,0 +1,39 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2016 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: yunwuxin <448901948@qq.com>
// +----------------------------------------------------------------------
return [
'default' => 'sync',
'connections' => [
'sync' => [
'type' => 'sync',
],
'database' => [
'type' => 'database',
'queue' => 'default',
'table' => 'jobs',
'connection' => null,
],
'redis' => [
'type' => 'redis',
'queue' => 'default',
'host' => '127.0.0.1',
'port' => 6379,
'password' => '',
'select' => 0,
'timeout' => 0,
'persistent' => false,
],
],
'failed' => [
'type' => 'none',
'table' => 'failed_jobs',
],
];

45
config/route.php Executable file
View File

@ -0,0 +1,45 @@
<?php
// +----------------------------------------------------------------------
// | 路由设置
// +----------------------------------------------------------------------
return [
// pathinfo分隔符
'pathinfo_depr' => '/',
// URL伪静态后缀
'url_html_suffix' => 'html',
// URL普通方式参数 用于自动生成
'url_common_param' => true,
// 是否开启路由延迟解析
'url_lazy_route' => false,
// 是否强制使用路由
'url_route_must' => false,
// 合并路由规则
'route_rule_merge' => false,
// 路由是否完全匹配
'route_complete_match' => false,
// 访问控制器层名称
'controller_layer' => 'controller',
// 空控制器名
'empty_controller' => 'Error',
// 是否使用控制器后缀
'controller_suffix' => false,
// 默认的路由变量规则
'default_route_pattern' => '[\w\.]+',
// 是否开启请求缓存 true自动缓存 支持设置请求缓存规则
'request_cache_key' => false,
// 请求缓存有效期
'request_cache_expire' => null,
// 全局请求缓存排除规则
'request_cache_except' => [],
// 默认控制器名
'default_controller' => 'Index',
// 默认操作名
'default_action' => 'index',
// 操作方法后缀
'action_suffix' => '',
// 默认JSONP格式返回的处理方法
'default_jsonp_handler' => 'jsonpReturn',
// 默认JSONP处理方法
'var_jsonp_handler' => 'callback',
];

19
config/session.php Executable file
View File

@ -0,0 +1,19 @@
<?php
// +----------------------------------------------------------------------
// | 会话设置
// +----------------------------------------------------------------------
return [
// session name
'name' => 'PHPSESSID',
// SESSION_ID的提交变量,解决flash上传跨域
'var_session_id' => '',
// 驱动方式 支持file cache
'type' => 'file',
// 存储连接标识 当type使用cache的时候有效
'store' => null,
// 过期时间
'expire' => 9*3600,
// 前缀
'prefix' => '',
];

159
config/sms.php Executable file
View File

@ -0,0 +1,159 @@
<?php
// +----------------------------------------------------------------------
// | 胜家云 [ SingKa Cloud ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2020 https://www.singka.net All rights reserved.
// +----------------------------------------------------------------------
// | 宁波晟嘉网络科技有限公司
// +----------------------------------------------------------------------
// | Author: ShyComet <shycomet@qq.com>
// +----------------------------------------------------------------------
return [
'driver' => 'aliyun', // 驱动器
'aliyun' => [
'version' => '2017-05-25',
'host' => 'dysmsapi.aliyuncs.com',
'scheme' => 'http',
'region_id' => 'cn-hangzhou',
'access_key' => '',
'access_secret' => '',
'sign_name' => '',
'actions' => [
'register' => [
'actions_name' => '注册验证',
'template_id' => 'SMS_53115055',
],
'login' => [
'actions_name' => '登录验证',
'template_id' => 'SMS_53115057',
],
'changePassword' => [
'actions_name' => '修改密码',
'template_id' => 'SMS_53115053',
],
'changeUserinfo' => [
'actions_name' => '变更信息',
'template_id' => 'SMS_53115052',
],
],
],
'ucloud' => [
'public_key' => '',
'private_key' => '',
'project_id' => '',
'base_url' => 'https://api.ucloud.cn',
'sign_name' => '',
'actions' => [
'register' => [
'actions_name' => '注册验证',
'template_id' => 'UTA1910164E29F4',
],
'login' => [
'actions_name' => '登录验证',
'template_id' => 'UTA1910164E29F4',
],
'changePassword' => [
'actions_name' => '修改密码',
'template_id' => 'UTA1910164E29F4',
],
'changeUserinfo' => [
'actions_name' => '变更信息',
'template_id' => 'UTA1910164E29F4',
],
],
],
'qcloud' => [
'appid' => '',
'appkey' => '',
'sign_name' => '',
'actions' => [
'register' => [
'actions_name' => '注册验证',
'template_id' => '566198',
],
'login' => [
'actions_name' => '登录验证',
'template_id' => '566197',
],
'changePassword' => [
'actions_name' => '修改密码',
'template_id' => '566199',
],
'changeUserinfo' => [
'actions_name' => '变更信息',
'template_id' => '566200',
],
],
],
'qiniu' => [
'AccessKey' => '',
'SecretKey' => '',
'actions' => [
'register' => [
'actions_name' => '注册验证',
'template_id' => '1246849772845797376',
],
'login' => [
'actions_name' => '登录验证',
'template_id' => '1246849654881001472',
],
'changePassword' => [
'actions_name' => '修改密码',
'template_id' => '1246849964902977536',
],
'changeUserinfo' => [
'actions_name' => '变更信息',
'template_id' => '1246849860733243392',
],
],
],
'upyun' => [
'id' => '',
'token' => '',
'apiurl' => '',
'actions' => [
'register' => [
'actions_name' => '注册验证',
'template_id' => '2591',
],
'login' => [
'actions_name' => '登录验证',
'template_id' => '2592',
],
'changePassword' => [
'actions_name' => '修改密码',
'template_id' => '2590',
],
'changeUserinfo' => [
'actions_name' => '变更信息',
'template_id' => '2589',
],
],
],
'huawei' => [
'url' => '',
'appKey' => '',
'appSecret' => '',
'sender' => '',
'signature' => '',
'statusCallback' => '',
'actions' => [
'register' => [
'actions_name' => '注册验证',
'template_id' => '2591',
],
'login' => [
'actions_name' => '登录验证',
'template_id' => '2592',
],
'changePassword' => [
'actions_name' => '修改密码',
'template_id' => '2590',
],
'changeUserinfo' => [
'actions_name' => '变更信息',
'template_id' => '2589',
],
],
]
];

10
config/trace.php Executable file
View File

@ -0,0 +1,10 @@
<?php
// +----------------------------------------------------------------------
// | Trace设置 开启调试模式后有效
// +----------------------------------------------------------------------
return [
// 内置Html和Console两种方式 支持扩展
'type' => 'Html',
// 读取的日志通道名
'channel' => '',
];

20
config/usms.php Executable file
View File

@ -0,0 +1,20 @@
<?php
// +----------------------------------------------------------------------
// | 胜家云 [ SingKa Cloud ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2019 https://www.singka.net All rights reserved.
// +----------------------------------------------------------------------
// | 宁波晟嘉网络科技有限公司
// +----------------------------------------------------------------------
// | Author: ShyComet <shycomet@qq.com>
// +----------------------------------------------------------------------
return [
//API秘钥之公钥 可在后台查找
'PUBLIC_KEY' => '',
//API秘钥之私钥 可在后台查找
'PRIVATE_KEY' => '',
//项目ID 登录Ucloud后台可以查找
'PROJECT_ID' => '',
//API通信地址
'BASE_URL' => 'https://api.ucloud.cn',
];

28
config/view.php Executable file
View File

@ -0,0 +1,28 @@
<?php
// +----------------------------------------------------------------------
// | 模板设置
// +----------------------------------------------------------------------
return [
// 模板引擎类型使用Think
'type' => 'Think',
// 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 3 保持操作方法
'auto_rule' => 1,
// 模板目录名
'view_dir_name' => 'view',
// 模板后缀
'view_suffix' => 'html',
// 模板文件名分隔符
'view_depr' => DIRECTORY_SEPARATOR,
// 模板引擎普通标签开始标记
'tpl_begin' => '{',
// 模板引擎普通标签结束标记
'tpl_end' => '}',
// 标签库标签开始标记
'taglib_begin' => '{',
// 标签库标签结束标记
'taglib_end' => '}',
'tpl_replace_string' => [
'__STATIC__'=>'/static',
]
];

73
example.env Executable file
View File

@ -0,0 +1,73 @@
APP_DEBUG = true
[APP]
DEFAULT_TIMEZONE = Asia/Shanghai
[DATABASE]
TYPE = mysql
HOSTNAME = 127.0.0.1
DATABASE = im
USERNAME = root
PASSWORD = My01020304
HOSTPORT = 3306
CHARSET = utf8mb4
DEBUG = true
prefix = yu_
[LANG]
default_lang = zh-cn
[REDIS]
HOST = 127.0.0.1
PORT = 6379
PASSWORD =
PREFIX =
[AES]
TOKEN_KEY = tHTi8USApxsdfnhTM
LOGIN_KEY = t2fe6HMnmssswDVi2
#聊天内容加密,如果不加密则留空,一旦加密就不能修改,如果修改了需要清空所有聊天记录
CHAT_KEY =
[JWT]
SECRET = 17b190c0d612321f94f57325ae5a8b4c
TTL = 2592000
[WORKER]
NAME = businessWorker
PORT = 8282
# 根据自己的核心数而配置
COUNT = 1
START_PORT = 2300
REGISTER_ADDRESS =127.0.0.1:1236
lAN_IP = 127.0.0.1
# 分部署部署只需要启动一个gateway其他的gateway只需要配置register_address即可
REGISTER_DEPLOY = true
#配置预览功能,本系统主要使用第三方的预览工具,比如永中云转换,自带预览系统
[PREVIEW]
# 自带预览系统URL主要用于预览媒体文件已内置必须要有最后的/斜杠
own=
# 永中云文件预览,主要用于文档预览,必须要有最后的/斜杠
yzdcs=http://domain/
# 永中云api code
keycode=17444844212312
# 配置对象储存,主要用于聊天文件储存,可以通过后台进行配置
[FILESYSTEM]
driver=local
aliyun_accessId=false
aliyun_accessSecret=false
aliyun_bucket=false
aliyun_endpoint=false
aliyun_url=false
qiniu_accessKey=false
qiniu_secretKey=false
qiniu_bucket=false
qiniu_url=false
qcloud_region=false
qcloud_appId=false
qcloud_secretId=false
qcloud_secretKey=false
qcloud_bucket=false
qcloud_cdn=false

146
extend/Agent.php Executable file
View File

@ -0,0 +1,146 @@
<?php
/**
* tpAdmin [a web admin based ThinkPHP5]
*
* @author yuan1994 <tianpian0805@gmail.com>
* @link http://tpadmin.yuan1994.com/
* @copyright 2016 yuan1994 all rights reserved.
* @license http://www.apache.org/licenses/LICENSE-2.0
*/
//------------------------
// 根据user-agent获取浏览器版本操作系统
//-------------------------
class Agent
{
public static function getVesion($v){
if(isset($v[1])){
$version= $v[1];
}else{
$version=0;
}
return $version;
}
/**
* 获取客户端浏览器信息 添加win10 edge浏览器判断
* @param null
* @author Jea杨
* @return string
*/
public static function getBroswer()
{
$sys = $_SERVER['HTTP_USER_AGENT']; //获取用户代理字符串
if (stripos($sys, "Firefox/") > 0) {
preg_match("/Firefox\/([^;)]+)+/i", $sys, $v);
$exp[0] = "Firefox";
$exp[1]=self::getVesion($v);
} elseif (stripos($sys, "Maxthon") > 0) {
preg_match("/Maxthon\/([\d\.]+)/", $sys, $v);
$exp[0] = "傲游";
$exp[1]=self::getVesion($v);
} elseif (stripos($sys, "MSIE") > 0) {
preg_match("/MSIE\s+([^;)]+)+/i", $sys, $v);
$exp[0] = "IE";
$exp[1]=self::getVesion($v);
} elseif (stripos($sys, "OPR") > 0) {
preg_match("/OPR\/([\d\.]+)/", $sys, $v);
$exp[0] = "Opera";
$exp[1]=self::getVesion($v);
} elseif (stripos($sys, "Edge") > 0) {
//win10 Edge浏览器 添加了chrome内核标记 在判断Chrome之前匹配
preg_match("/Edge\/([\d\.]+)/", $sys, $v);
$exp[0] = "Edge";
$exp[1]=self::getVesion($v);
} elseif (stripos($sys, "Chrome") > 0) {
preg_match("/Chrome\/([\d\.]+)/", $sys, $v);
$exp[0] = "Chrome";
$exp[1]=self::getVesion($v);
} elseif (stripos($sys, 'rv:') > 0 && stripos($sys, 'Gecko') > 0) {
preg_match("/rv:([\d\.]+)/", $sys, $v);
$exp[0] = "IE";
$exp[1]=self::getVesion($v);
} elseif (stripos($sys, 'Safari') > 0) {
preg_match("/safari\/([^\s]+)/i", $sys, $v);
$exp[0] = "Safari";
$exp[1]=self::getVesion($v);
} else {
$exp[0] = "未知浏览器";
$exp[1] = "0";
}
return $exp[0] . '(' . $exp[1] . ')';
}
/**
* 获取客户端操作系统信息包括win10
* @param null
* @author Jea杨
* @return string
*/
public static function getOs()
{
$agent = $_SERVER['HTTP_USER_AGENT'];
if (preg_match('/win/i', $agent) && strpos($agent, '95')) {
$os = 'Windows 95';
} else if (preg_match('/win 9x/i', $agent) && strpos($agent, '4.90')) {
$os = 'Windows ME';
} else if (preg_match('/win/i', $agent) && preg_match('/98/i', $agent)) {
$os = 'Windows 98';
} else if (preg_match('/win/i', $agent) && preg_match('/nt 6.0/i', $agent)) {
$os = 'Windows Vista';
} else if (preg_match('/win/i', $agent) && preg_match('/nt 6.1/i', $agent)) {
$os = 'Windows 7';
} else if (preg_match('/win/i', $agent) && preg_match('/nt 6.2/i', $agent)) {
$os = 'Windows 8';
} else if (preg_match('/win/i', $agent) && preg_match('/nt 10.0/i', $agent)) {
$os = 'Windows 10';#添加win10判断
} else if (preg_match('/win/i', $agent) && preg_match('/nt 5.1/i', $agent)) {
$os = 'Windows XP';
} else if (preg_match('/win/i', $agent) && preg_match('/nt 5/i', $agent)) {
$os = 'Windows 2000';
} else if (preg_match('/win/i', $agent) && preg_match('/nt/i', $agent)) {
$os = 'Windows NT';
} else if (preg_match('/win/i', $agent) && preg_match('/32/i', $agent)) {
$os = 'Windows 32';
} else if (preg_match('/linux/i', $agent)) {
$os = 'Linux';
} else if (preg_match('/unix/i', $agent)) {
$os = 'Unix';
} else if (preg_match('/sun/i', $agent) && preg_match('/os/i', $agent)) {
$os = 'SunOS';
} else if (preg_match('/ibm/i', $agent) && preg_match('/os/i', $agent)) {
$os = 'IBM OS/2';
} else if (preg_match('/Mac/i', $agent)) {
$os = 'Mac';
} else if (preg_match('/PowerPC/i', $agent)) {
$os = 'PowerPC';
} else if (preg_match('/AIX/i', $agent)) {
$os = 'AIX';
} else if (preg_match('/HPUX/i', $agent)) {
$os = 'HPUX';
} else if (preg_match('/NetBSD/i', $agent)) {
$os = 'NetBSD';
} else if (preg_match('/BSD/i', $agent)) {
$os = 'BSD';
} else if (preg_match('/OSF1/i', $agent)) {
$os = 'OSF1';
} else if (preg_match('/IRIX/i', $agent)) {
$os = 'IRIX';
} else if (preg_match('/FreeBSD/i', $agent)) {
$os = 'FreeBSD';
} else if (preg_match('/teleport/i', $agent)) {
$os = 'teleport';
} else if (preg_match('/flashget/i', $agent)) {
$os = 'flashget';
} else if (preg_match('/webzip/i', $agent)) {
$os = 'webzip';
} else if (preg_match('/offline/i', $agent)) {
$os = 'offline';
} elseif (preg_match('/ucweb|MQQBrowser|J2ME|IUC|3GW100|LG-MMS|i60|Motorola|MAUI|m9|ME860|maui|C8500|gt|k-touch|X8|htc|GT-S5660|UNTRUSTED|SCH|tianyu|lenovo|SAMSUNG/i', $agent)) {
$os = 'mobile';
} else {
$os = '未知操作系统';
}
return $os;
}
}

View File

@ -0,0 +1,57 @@
<?php
/*
Hashids
http://hashids.org/php
(c) 2013 Ivan Akimov
https://github.com/ivanakimov/hashids.php
hashids may be freely distributed under the MIT license.
*/
namespace Hashids;
/**
* HashGenerator is a contract for generating hashes
*/
interface HashGenerator {
/**
* Encodes a variable number of parameters to generate a hash
*
* @param mixed ...
*
* @return string the generated hash
*/
public function encode();
/**
* Decodes a hash to the original parameter values
*
* @param string $hash the hash to decode
*
* @return array
*/
public function decode($hash);
/**
* Encodes hexadecimal values to generate a hash
*
* @param string $str hexadecimal string
*
* @return string the generated hash
*/
public function encode_hex($str);
/**
* Decodes hexadecimal hash
*
* @param string $hash
*
* @return string hexadecimal string
*/
public function decode_hex($hash);
}

374
extend/Hashids/Hashids.php Executable file
View File

@ -0,0 +1,374 @@
<?php
/*
Hashids
http://hashids.org/php
(c) 2013 Ivan Akimov
https://github.com/ivanakimov/hashids.php
hashids may be freely distributed under the MIT license.
*/
namespace Hashids;
class Hashids implements HashGenerator {
const VERSION = '1.0.5';
/* internal settings */
const MIN_ALPHABET_LENGTH = 16;
const SEP_DIV = 3.5;
const GUARD_DIV = 12;
/* error messages */
const E_ALPHABET_LENGTH = 'alphabet must contain at least %d unique characters';
const E_ALPHABET_SPACE = 'alphabet cannot contain spaces';
/* set at constructor */
private $_alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
private $_seps = 'cfhistuCFHISTU';
private $_min_hash_length = 0;
private $_math_functions = array();
private $_max_int_value = 1000000000;
private static $instance; //单例
/**
* 初始化
* @param array $options
* @return static
*/
public static function instance($length = null, $salt = null, $alphabet = null)
{
if (is_null(self::$instance)) {
if ($length === null) $length = config("hashids.length");
if ($salt === null) $salt = config("hashids.salt");
if ($alphabet === null) $alphabet = config("hashids.alphabet");
self::$instance = new static($salt, $length, $alphabet);
}
return self::$instance;
}
public function __construct($salt = '', $min_hash_length = 8, $alphabet = '') {
/* if either math precision library is present, raise $this->_max_int_value */
if (function_exists('gmp_add')) {
$this->_math_functions['add'] = 'gmp_add';
$this->_math_functions['div'] = 'gmp_div';
$this->_math_functions['str'] = 'gmp_strval';
} else if (function_exists('bcadd')) {
$this->_math_functions['add'] = 'bcadd';
$this->_math_functions['div'] = 'bcdiv';
$this->_math_functions['str'] = 'strval';
}
$this->_lower_max_int_value = $this->_max_int_value;
if ($this->_math_functions) {
$this->_max_int_value = PHP_INT_MAX;
}
/* handle parameters */
$this->_salt = $salt;
if ((int)$min_hash_length > 0) {
$this->_min_hash_length = (int)$min_hash_length;
}
if ($alphabet) {
$this->_alphabet = implode('', array_unique(str_split($alphabet)));
}
if (strlen($this->_alphabet) < self::MIN_ALPHABET_LENGTH) {
throw new \Exception(sprintf(self::E_ALPHABET_LENGTH, self::MIN_ALPHABET_LENGTH));
}
if (is_int(strpos($this->_alphabet, ' '))) {
throw new \Exception(self::E_ALPHABET_SPACE);
}
$alphabet_array = str_split($this->_alphabet);
$seps_array = str_split($this->_seps);
$this->_seps = implode('', array_intersect($alphabet_array, $seps_array));
$this->_alphabet = implode('', array_diff($alphabet_array, $seps_array));
$this->_seps = $this->_consistent_shuffle($this->_seps, $this->_salt);
if (!$this->_seps || (strlen($this->_alphabet) / strlen($this->_seps)) > self::SEP_DIV) {
$seps_length = (int)ceil(strlen($this->_alphabet) / self::SEP_DIV);
if ($seps_length == 1) {
$seps_length++;
}
if ($seps_length > strlen($this->_seps)) {
$diff = $seps_length - strlen($this->_seps);
$this->_seps .= substr($this->_alphabet, 0, $diff);
$this->_alphabet = substr($this->_alphabet, $diff);
} else {
$this->_seps = substr($this->_seps, 0, $seps_length);
}
}
$this->_alphabet = $this->_consistent_shuffle($this->_alphabet, $this->_salt);
$guard_count = (int)ceil(strlen($this->_alphabet) / self::GUARD_DIV);
if (strlen($this->_alphabet) < 3) {
$this->_guards = substr($this->_seps, 0, $guard_count);
$this->_seps = substr($this->_seps, $guard_count);
} else {
$this->_guards = substr($this->_alphabet, 0, $guard_count);
$this->_alphabet = substr($this->_alphabet, $guard_count);
}
}
public function encode() {
$ret = '';
$numbers = func_get_args();
if (func_num_args() == 1 && is_array(func_get_arg(0))) {
$numbers = $numbers[0];
}
if (!$numbers) {
return $ret;
}
foreach ($numbers as $number) {
$is_number = ctype_digit((string)$number);
if (!$is_number || $number < 0 || $number > $this->_max_int_value) {
return $ret;
}
}
return $this->_encode($numbers);
}
public function decode($hash) {
$ret = array();
if (!$hash || !is_string($hash) || !trim($hash)) {
return $ret;
}
return $this->_decode(trim($hash), $this->_alphabet);
}
public function encode_hex($str) {
if (!ctype_xdigit((string)$str)) {
return '';
}
$numbers = trim(chunk_split($str, 12, ' '));
$numbers = explode(' ', $numbers);
foreach ($numbers as $i => $number) {
$numbers[$i] = hexdec('1' . $number);
}
return call_user_func_array(array($this, 'encode'), $numbers);
}
public function decode_hex($hash) {
$ret = "";
$numbers = $this->decode($hash);
foreach ($numbers as $i => $number) {
$ret .= substr(dechex($number), 1);
}
return $ret;
}
public function get_max_int_value() {
return $this->_max_int_value;
}
private function _encode(array $numbers) {
$alphabet = $this->_alphabet;
$numbers_size = sizeof($numbers);
$numbers_hash_int = 0;
foreach ($numbers as $i => $number) {
$numbers_hash_int += ($number % ($i + 100));
}
$lottery = $ret = $alphabet[$numbers_hash_int % strlen($alphabet)];
foreach ($numbers as $i => $number) {
$alphabet = $this->_consistent_shuffle($alphabet, substr($lottery . $this->_salt . $alphabet, 0, strlen($alphabet)));
$ret .= $last = $this->_hash($number, $alphabet);
if ($i + 1 < $numbers_size) {
$number %= (ord($last) + $i);
$seps_index = $number % strlen($this->_seps);
$ret .= $this->_seps[$seps_index];
}
}
if (strlen($ret) < $this->_min_hash_length) {
$guard_index = ($numbers_hash_int + ord($ret[0])) % strlen($this->_guards);
$guard = $this->_guards[$guard_index];
$ret = $guard . $ret;
if (strlen($ret) < $this->_min_hash_length) {
$guard_index = ($numbers_hash_int + ord($ret[2])) % strlen($this->_guards);
$guard = $this->_guards[$guard_index];
$ret .= $guard;
}
}
$half_length = (int)(strlen($alphabet) / 2);
while (strlen($ret) < $this->_min_hash_length) {
$alphabet = $this->_consistent_shuffle($alphabet, $alphabet);
$ret = substr($alphabet, $half_length) . $ret . substr($alphabet, 0, $half_length);
$excess = strlen($ret) - $this->_min_hash_length;
if ($excess > 0) {
$ret = substr($ret, $excess / 2, $this->_min_hash_length);
}
}
return $ret;
}
private function _decode($hash, $alphabet) {
$ret = array();
$hash_breakdown = str_replace(str_split($this->_guards), ' ', $hash);
$hash_array = explode(' ', $hash_breakdown);
$i = 0;
if (sizeof($hash_array) == 3 || sizeof($hash_array) == 2) {
$i = 1;
}
$hash_breakdown = $hash_array[$i];
if (isset($hash_breakdown[0])) {
$lottery = $hash_breakdown[0];
$hash_breakdown = substr($hash_breakdown, 1);
$hash_breakdown = str_replace(str_split($this->_seps), ' ', $hash_breakdown);
$hash_array = explode(' ', $hash_breakdown);
foreach ($hash_array as $sub_hash) {
$alphabet = $this->_consistent_shuffle($alphabet, substr($lottery . $this->_salt . $alphabet, 0, strlen($alphabet)));
$ret[] = (int)$this->_unhash($sub_hash, $alphabet);
}
if ($this->_encode($ret) != $hash) {
$ret = array();
}
}
// return $ret;
//修改为直接返回字符串
return $ret[0];
}
private function _consistent_shuffle($alphabet, $salt) {
if (!strlen($salt)) {
return $alphabet;
}
for ($i = strlen($alphabet) - 1, $v = 0, $p = 0; $i > 0; $i--, $v++) {
$v %= strlen($salt);
$p += $int = ord($salt[$v]);
$j = ($int + $v + $p) % $i;
$temp = $alphabet[$j];
$alphabet[$j] = $alphabet[$i];
$alphabet[$i] = $temp;
}
return $alphabet;
}
private function _hash($input, $alphabet) {
$hash = '';
$alphabet_length = strlen($alphabet);
do {
$hash = $alphabet[$input % $alphabet_length] . $hash;
if ($input > $this->_lower_max_int_value && $this->_math_functions) {
$input = $this->_math_functions['str']($this->_math_functions['div']($input, $alphabet_length));
} else {
$input = (int)($input / $alphabet_length);
}
} while ($input);
return $hash;
}
private function _unhash($input, $alphabet) {
$number = 0;
if (strlen($input) && $alphabet) {
$alphabet_length = strlen($alphabet);
$input_chars = str_split($input);
foreach ($input_chars as $i => $char) {
$pos = strpos($alphabet, $char);
if ($this->_math_functions) {
$number = $this->_math_functions['str']($this->_math_functions['add']($number, $pos * pow($alphabet_length, (strlen($input) - $i - 1))));
} else {
$number += $pos * pow($alphabet_length, (strlen($input) - $i - 1));
}
}
}
return $number;
}
}

109
extend/Ip.php Executable file
View File

@ -0,0 +1,109 @@
<?php
/*
全球 IPv4 地址归属地数据库(17MON.CN )
高春辉(pAUL gAO) <gaochunhui@gmail.com>
Build 20141009 版权所有 17MON.CN
(C) 2006 - 2014 保留所有权利
请注意及时更新 IP 数据库版本
数据问题请加 QQ : 346280296
Code for PHP 5.3+ only
*/
class Ip
{
private static $ip = NULL;
private static $fp = NULL;
private static $offset = NULL;
private static $index = NULL;
private static $cached = array();
public static function find($ip)
{
if (empty($ip) === TRUE)
{
return 'N/A';
}
$nip = gethostbyname($ip);
$ipdot = explode('.', $nip);
if ($ipdot[0] < 0 || $ipdot[0] > 255 || count($ipdot) !== 4)
{
return 'N/A';
}
if (isset(self::$cached[$nip]) === TRUE)
{
return self::$cached[$nip];
}
if (self::$fp === NULL)
{
self::init();
}
$nip2 = pack('N', ip2long($nip));
$tmp_offset = (int)$ipdot[0] * 4;
$start = unpack('Vlen', self::$index[$tmp_offset] . self::$index[$tmp_offset + 1] . self::$index[$tmp_offset + 2] . self::$index[$tmp_offset + 3]);
$index_offset = $index_length = NULL;
$max_comp_len = self::$offset['len'] - 1024 - 4;
for ($start = $start['len'] * 8 + 1024; $start < $max_comp_len; $start += 8)
{
if (self::$index[$start] . self::$index[$start + 1] . self::$index[$start + 2] . self::$index[$start + 3] >= $nip2)
{
$index_offset = unpack('Vlen', self::$index[$start + 4] . self::$index[$start + 5] . self::$index[$start + 6] . "\x0");
$index_length = unpack('Clen', self::$index[$start + 7]);
break;
}
}
if ($index_offset === NULL)
{
return 'N/A';
}
fseek(self::$fp, self::$offset['len'] + $index_offset['len'] - 1024);
self::$cached[$nip] = explode("\t", fread(self::$fp, $index_length['len']));
return self::$cached[$nip];
}
private static function init()
{
if (self::$fp === NULL)
{
self::$ip = new self();
self::$fp = fopen(__DIR__ . '/ip/17monipdb.dat', 'rb');
if (self::$fp === FALSE)
{
throw new Exception('Invalid 17monipdb.dat file!');
}
self::$offset = unpack('Nlen', fread(self::$fp, 4));
if (self::$offset['len'] < 4)
{
throw new Exception('Invalid 17monipdb.dat file!');
}
self::$index = fread(self::$fp, self::$offset['len'] - 4);
}
}
public function __destruct()
{
if (self::$fp !== NULL)
{
fclose(self::$fp);
}
}
}
?>

94
extend/easyTask/Check.php Executable file
View File

@ -0,0 +1,94 @@
<?php
namespace easyTask;
/**
* Class Check
* @package easyTask
*/
class Check
{
/**
* 待检查扩展列表
* @var array
*/
private static $waitExtends = [
//Win
'1' => [
'json',
'curl',
'com_dotnet',
'mbstring',
],
//Linux
'2' => [
'json',
'curl',
'pcntl',
'posix',
'mbstring',
]
];
/**
* 待检查函数列表
* @var array
*/
private static $waitFunctions = [
//Win
'1' => [
'umask',
'sleep',
'usleep',
'ob_start',
'ob_end_clean',
'ob_get_contents',
],
//Linux
'2' => [
'umask',
'chdir',
'sleep',
'usleep',
'ob_start',
'ob_end_clean',
'ob_get_contents',
'pcntl_fork',
'posix_setsid',
'posix_getpid',
'posix_getppid',
'pcntl_wait',
'posix_kill',
'pcntl_signal',
'pcntl_alarm',
'pcntl_waitpid',
'pcntl_signal_dispatch',
]
];
/**
* 解析运行环境
* @param int $currentOs
*/
public static function analysis($currentOs)
{
//检查扩展
$waitExtends = static::$waitExtends[$currentOs];
foreach ($waitExtends as $extend)
{
if (!extension_loaded($extend))
{
Helper::showSysError("php_{$extend}.(dll/so) is not load,please check php.ini file");
}
}
//检查函数
$waitFunctions = static::$waitFunctions[$currentOs];
foreach ($waitFunctions as $func)
{
if (!function_exists($func))
{
Helper::showSysError("function $func may be disabled,please check disable_functions in php.ini");
}
}
}
}

129
extend/easyTask/Command.php Executable file
View File

@ -0,0 +1,129 @@
<?php
namespace easyTask;
use \Closure as Closure;
/**
* Class Command
* @package easyTask
*/
class Command
{
/**
* 通讯文件
*/
private $msgFile;
/**
* 构造函数
* @throws
*/
public function __construct()
{
$this->initMsgFile();
}
/**
* 初始化文件
*/
private function initMsgFile()
{
//创建文件
$path = Helper::getCsgPath();
$file = $path . '%s.csg';
$this->msgFile = sprintf($file, md5(__FILE__));
if (!file_exists($this->msgFile))
{
if (!file_put_contents($this->msgFile, '[]', LOCK_EX))
{
Helper::showError('failed to create msgFile');
}
}
}
/**
* 获取数据
* @return array
* @throws
*/
public function get()
{
$content = @file_get_contents($this->msgFile);
if (!$content)
{
return [];
}
$data = json_decode($content, true);
return is_array($data) ? $data : [];
}
/**
* 重置数据
* @param array $data
*/
public function set($data)
{
file_put_contents($this->msgFile, json_encode($data), LOCK_EX);
}
/**
* 投递数据
* @param array $command
*/
public function push($command)
{
$data = $this->get();
array_push($data, $command);
$this->set($data);
}
/**
* 发送命令
* @param array $command
*/
public function send($command)
{
$command['time'] = time();
$this->push($command);
}
/**
* 接收命令
* @param string $msgType 消息类型
* @param mixed $command 收到的命令
*/
public function receive($msgType, &$command)
{
$data = $this->get();
if (empty($data)) {
return;
}
foreach ($data as $key => $item)
{
if ($item['msgType'] == $msgType)
{
$command = $item;
unset($data[$key]);
break;
}
}
$this->set($data);
}
/**
* 根据命令执行对应操作
* @param int $msgType 消息类型
* @param Closure $func 执行函数
* @param int $time 等待方时间戳
*/
public function waitCommandForExecute($msgType, $func, $time)
{
$command = '';
$this->receive($msgType, $command);
if (!$command || (!empty($command['time']) && $command['time'] < $time))
{
return;
}
$func($command);
}
}

36
extend/easyTask/Env.php Executable file
View File

@ -0,0 +1,36 @@
<?php
namespace easyTask;
/**
* Class Env
* @package easyTask
*/
class Env
{
/**
* collection
* @var array
*/
private static $collection;
/**
* Set
* @param string $key
* @param mixed $value
*/
public static function set($key, $value)
{
static::$collection[$key] = $value;
}
/**
* Get
* @param string $key
* @return mixed
*/
public static function get($key)
{
return isset(static::$collection[$key]) ? static::$collection[$key] : false;
}
}

113
extend/easyTask/Error.php Executable file
View File

@ -0,0 +1,113 @@
<?php
namespace easyTask;
use easyTask\Exception\ErrorException;
use \Closure as Closure;
/**
* Class Error
* @package easyTask
*/
class Error
{
/**
* Register Error
*/
public static function register()
{
error_reporting(E_ALL);
set_error_handler([__CLASS__, 'appError']);
set_exception_handler([__CLASS__, 'appException']);
register_shutdown_function([__CLASS__, 'appShutdown']);
}
/**
* appError
* (E_ERROR|E_PARSE|E_CORE_ERROR|E_CORE_WARNING|E_COMPILE_ERROR|E_COMPILE_WARNING|E_STRICT)
* @param string $errno
* @param string $errStr
* @param string $errFile
* @param int $errLine
* @throws
*/
public static function appError($errno, $errStr, $errFile, $errLine)
{
//组装异常
$type = 'error';
$exception = new ErrorException($errno, $errStr, $errFile, $errLine);
//日志记录
static::report($type, $exception);
}
/**
* appException
* @param mixed $exception (Exception|Throwable)
* @throws
*/
public static function appException($exception)
{
//日志记录
$type = 'exception';
static::report($type, $exception);
}
/**
* appShutdown
* (Fatal Error|Parse Error)
* @throws
*/
public static function appShutdown()
{
//存在错误
$type = 'warring';
if (($error = error_get_last()) != null)
{
//日志记录
$exception = new ErrorException($error['type'], $error['message'], $error['file'], $error['line']);
static::report($type, $exception);
}
}
/**
* Report
* @param string $type
* @param ErrorException $exception
*/
public static function report($type, $exception)
{
//标准化日志
$text = Helper::formatException($exception, $type);
//本地日志储存
Helper::writeLog($text);
//同步模式输出
if (!Env::get('daemon')) echo($text);
//回调上报信息
$notify = Env::get('notifyHand');
if ($notify)
{
//闭包回调
if ($notify instanceof Closure)
{
$notify($exception);
return;
}
//Http回调
$request = [
'errStr' => $exception->getMessage(),
'errFile' => $exception->getFile(),
'errLine' => $exception->getLine(),
];
$result = Helper::curl($notify, $request);
if (!$result || $result != 'success')
{
Helper::showError("request http api $notify failed", false, 'warring', true);
}
}
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace easyTask\Exception;
/**
* Class ErrorException
* @package EasyTask\Exception
*/
class ErrorException extends \Exception
{
/**
* 错误级别
* @var int
*/
protected $severity;
/**
* 构造函数
* ErrorException constructor.
* @param string $severity
* @param string $errStr
* @param string $errFile
* @param string $errLine
*/
public function __construct($severity, $errStr, $errFile, $errLine)
{
$this->line = $errLine;
$this->file = $errFile;
$this->code = 0;
$this->message = $errStr;
$this->severity = $severity;
}
}

503
extend/easyTask/Helper.php Executable file
View File

@ -0,0 +1,503 @@
<?php
namespace easyTask;
use easyTask\Exception\ErrorException;
use \Exception as Exception;
use \Throwable as Throwable;
/**
* Class Helper
* @package easyTask
*/
class Helper
{
/**
* 睡眠函数
* @param int $time 时间
* @param int $type 类型:1 2毫秒
*/
public static function sleep($time, $type = 1)
{
if ($type == 2) $time *= 1000;
$type == 1 ? sleep($time) : usleep($time);
}
/**
* 设置进程标题
* @param string $title
*/
public static function cli_set_process_title($title)
{
set_error_handler(function () {
});
if (function_exists('cli_set_process_title')) {
cli_set_process_title($title);
}
restore_error_handler();
}
/**
* 设置掩码
*/
public static function setMask()
{
umask(0);
}
/**
* 设置代码页
* @param int $code
*/
public static function setCodePage($code = 65001)
{
$ds = DIRECTORY_SEPARATOR;
$codePageBinary = "C:{$ds}Windows{$ds}System32{$ds}chcp.com";
if (file_exists($codePageBinary) && static::canUseExcCommand()) {
@shell_exec("{$codePageBinary} {$code}");
}
}
/**
* 获取命令行输入
* @param int $type
* @return string|array
*/
public static function getCliInput($type = 1)
{
//输入参数
$argv = $_SERVER['argv'];
//组装PHP路径
array_unshift($argv, Env::get('phpPath'));
//自动校正
foreach ($argv as $key => $value) {
if (file_exists($value)) {
$argv[$key] = realpath($value);
}
}
//返回
if ($type == 1) {
return join(' ', $argv);
}
return $argv;
}
/**
* 设置PHP二进制文件
* @param string $path
*/
public static function setPhpPath($path = '')
{
if (!$path) $path = self::getBinary();;
Env::set('phpPath', $path);
}
/**
* 获取进程二进制文件
* @return string
*/
public static function getBinary()
{
return PHP_BINARY;
}
/**
* 是否Win平台
* @return bool
*/
public static function isWin()
{
return (DIRECTORY_SEPARATOR == '\\') ? true : false;
}
/**
* 开启异步信号
* @return bool
*/
public static function openAsyncSignal()
{
return pcntl_async_signals(true);
}
/**
* 是否支持异步信号
* @return bool
*/
public static function canUseAsyncSignal()
{
return (function_exists('pcntl_async_signals'));
}
/**
* 是否支持event事件
* @return bool
*/
public static function canUseEvent()
{
return (extension_loaded('event'));
}
/**
* 是否可执行命令
* @return bool
*/
public static function canUseExcCommand()
{
return function_exists('shell_exec');
}
/**
* 获取运行时目录
* @return string
*/
public static function getRunTimePath()
{
$path = Env::get('runTimePath') ? Env::get('runTimePath') : sys_get_temp_dir();
if (!is_dir($path)) {
static::showSysError('please set runTimePath');
}
$path = $path . DIRECTORY_SEPARATOR . Env::get('prefix') . DIRECTORY_SEPARATOR;
$path = str_replace(DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR, $path);
return $path;
}
/**
* 获取Win进程目录
* @return string
*/
public static function getWinPath()
{
return Helper::getRunTimePath() . 'Win' . DIRECTORY_SEPARATOR;
}
/**
* 获取日志目录
* @return string
*/
public static function getLogPath()
{
return Helper::getRunTimePath() . 'Log' . DIRECTORY_SEPARATOR;
}
/**
* 获取进程命令通信目录
* @return string
*/
public static function getCsgPath()
{
return Helper::getRunTimePath() . 'Csg' . DIRECTORY_SEPARATOR;
}
/**
* 获取进程队列目录
* @return string
*/
public static function getQuePath()
{
return Helper::getRunTimePath() . 'Que' . DIRECTORY_SEPARATOR;
}
/**
* 获取进程锁目录
* @return string
*/
public static function getLokPath()
{
return Helper::getRunTimePath() . 'Lok' . DIRECTORY_SEPARATOR;
}
/**
* 获取标准输入输出目录
* @return string
*/
public static function getStdPath()
{
return Helper::getRunTimePath() . 'Std' . DIRECTORY_SEPARATOR;
}
/**
* 初始化所有目录
*/
public static function initAllPath()
{
$paths = [
static::getRunTimePath(),
static::getWinPath(),
static::getLogPath(),
static::getLokPath(),
static::getQuePath(),
static::getCsgPath(),
static::getStdPath(),
];
foreach ($paths as $path) {
if (!is_dir($path)) {
mkdir($path, 0777, true);
}
}
}
/**
* 保存标准输入|输出
* @param string $char 输入|输出
*/
public static function saveStdChar($char)
{
$path = static::getStdPath();
$file = $path . date('Y_m_d') . '.std';
$char = static::convert_char($char);
file_put_contents($file, $char, FILE_APPEND);
}
/**
* 保存日志
* @param string $message
*/
public static function writeLog($message)
{
//日志文件
$path = Helper::getLogPath();
$file = $path . date('Y_m_d') . '.log';
//加锁保存
$message = static::convert_char($message);
file_put_contents($file, $message, FILE_APPEND | LOCK_EX);
}
/**
* 保存类型日志
* @param string $message
* @param string $type
* @param bool $isExit
*/
public static function writeTypeLog($message, $type = 'info', $isExit = false)
{
//格式化信息
$text = Helper::formatMessage($message, $type);
//记录日志
static::writeLog($text);
if ($isExit) exit();
}
/**
* 编码转换
* @param string $char
* @param string $coding
* @return string
*/
public static function convert_char($char, $coding = 'UTF-8')
{
$encode_arr = ['UTF-8', 'ASCII', 'GBK', 'GB2312', 'BIG5', 'JIS', 'eucjp-win', 'sjis-win', 'EUC-JP'];
$encoded = mb_detect_encoding($char, $encode_arr);
if ($encoded) {
$char = mb_convert_encoding($char, $coding, $encoded);
}
return $char;
}
/**
* 格式化异常信息
* @param ErrorException|Exception|Throwable $exception
* @param string $type
* @return string
*/
public static function formatException($exception, $type = 'exception')
{
//参数
$pid = getmypid();
$date = date('Y/m/d H:i:s', time());
//组装
return $date . " [$type] : errStr:" . $exception->getMessage() . ',errFile:' . $exception->getFile() . ',errLine:' . $exception->getLine() . " (pid:$pid)" . PHP_EOL;
}
/**
* 格式化异常信息
* @param string $message
* @param string $type
* @return string
*/
public static function formatMessage($message, $type = 'error')
{
//参数
$pid = getmypid();
$date = date('Y/m/d H:i:s', time());
//组装
return $date . " [$type] : " . $message . " (pid:$pid)" . PHP_EOL;
}
/**
* 检查任务时间是否合法
* @param mixed $time
*/
public static function checkTaskTime($time)
{
if (is_int($time)) {
if ($time < 0) static::showSysError('time must be greater than or equal to 0');
} elseif (is_float($time)) {
if (!static::canUseEvent()) static::showSysError('please install php_event.(dll/so) extend for using milliseconds');
} else {
static::showSysError('time parameter is an unsupported type');
}
}
/**
* 输出字符串
* @param string $char
* @param bool $exit
*/
public static function output($char, $exit = false)
{
echo $char;
if ($exit) exit();
}
/**
* 输出信息
* @param string $message
* @param bool $isExit
* @param string $type
* @throws
*/
public static function showInfo($message, $isExit = false, $type = 'info')
{
//格式化信息
$text = static::formatMessage($message, $type);
//记录日志
static::writeLog($text);
//输出信息
static::output($text, $isExit);
}
/**
* 输出错误
* @param string $errStr
* @param bool $isExit
* @param string $type
* @param bool $log
* @throws
*/
public static function showError($errStr, $isExit = true, $type = 'error', $log = true)
{
//格式化信息
$text = static::formatMessage($errStr, $type);
//记录日志
if ($log) static::writeLog($text);
//输出信息
static::output($text, $isExit);
}
/**
* 输出系统错误
* @param string $errStr
* @param bool $isExit
* @param string $type
* @throws
*/
public static function showSysError($errStr, $isExit = true, $type = 'warring')
{
//格式化信息
$text = static::formatMessage($errStr, $type);
//输出信息
static::output($text, $isExit);
}
/**
* 输出异常
* @param mixed $exception
* @param string $type
* @param bool $isExit
* @throws
*/
public static function showException($exception, $type = 'exception', $isExit = true)
{
//格式化信息
$text = static::formatException($exception, $type);
//记录日志
Helper::writeLog($text);
//输出信息
static::output($text, $isExit);
}
/**
* 控制台输出表格
* @param array $data
* @param boolean $exit
*/
public static function showTable($data, $exit = true)
{
//提取表头
$header = array_keys($data['0']);
//组装数据
foreach ($data as $key => $row) {
$data[$key] = array_values($row);
}
//输出表格
$table = new Table();
$table->setHeader($header);
$table->setStyle('box');
$table->setRows($data);
$render = static::convert_char($table->render());
if ($exit) {
exit($render);
}
echo($render);
}
/**
* 通过Curl方式提交数据
*
* @param string $url 目标URL
* @param null $data 提交的数据
* @param bool $return_array 是否转成数组
* @param null $header 请求头信息 array("Content-Type: application/json")
*
* @return array|mixed
*/
public static function curl($url, $data = null, $return_array = false, $header = null)
{
//初始化curl
$curl = curl_init();
//设置超时
curl_setopt($curl, CURLOPT_TIMEOUT, 30);
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
if (is_array($header)) {
curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
}
if ($data) {
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
}
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
//运行curl获取结果
$result = @curl_exec($curl);
//关闭句柄
curl_close($curl);
//转成数组
if ($return_array) {
return json_decode($result, true);
}
//返回结果
return $result;
}
}

52
extend/easyTask/Lock.php Executable file
View File

@ -0,0 +1,52 @@
<?php
namespace easyTask;
use \Closure as Closure;
/**
* Class Lock
* @package easyTask
*/
class Lock
{
/**
* 锁文件
* @var string
*/
private $file;
/**
* 构造函数
* @param string $name
*/
public function __construct($name = 'lock')
{
//初始化文件
$path = Helper::getLokPath();
$this->file = $path . md5($name);
if (!file_exists($this->file))
{
@file_put_contents($this->file, '');
}
}
/**
* 加锁执行
* @param Closure $func
* @param bool $block
* @return mixed
*/
public function execute($func, $block = true)
{
$fp = fopen($this->file, 'r');
$is_flock = $block ? flock($fp, LOCK_EX) : flock($fp, LOCK_EX | LOCK_NB);
$call_back = null;
if ($is_flock)
{
$call_back = $func();
flock($fp, LOCK_UN);
}
fclose($fp);
return $call_back;
}
}

307
extend/easyTask/Process/Linux.php Executable file
View File

@ -0,0 +1,307 @@
<?php
namespace easyTask\Process;
use EasyTask\Env;
use EasyTask\Helper;
use \Closure as Closure;
use \Throwable as Throwable;
/**
* Class Linux
* @package EasyTask\Process
*/
class Linux extends Process
{
/**
* 进程执行记录
* @var array
*/
protected $processList = [];
/**
* 构造函数
* @var array $taskList
*/
public function __construct($taskList)
{
parent::__construct($taskList);
if (Env::get('canAsync'))
{
Helper::openAsyncSignal();
}
}
/**
* 开始运行
*/
public function start()
{
//发送命令
$this->commander->send([
'type' => 'start',
'msgType' => 2
]);
//异步处理
if (Env::get('daemon'))
{
Helper::setMask();
$this->fork(
function () {
$sid = posix_setsid();
if ($sid < 0)
{
Helper::showError('set child processForManager failed,please try again');
}
$this->allocate();
},
function () {
pcntl_wait($status, WNOHANG);
$this->status();
}
);
}
//同步处理
$this->allocate();
}
/**
* 分配进程处理任务
*/
protected function allocate()
{
foreach ($this->taskList as $item)
{
//提取参数
$prefix = Env::get('prefix');
$item['data'] = date('Y-m-d H:i:s');
$item['alas'] = "{$prefix}_{$item['alas']}";
$used = $item['used'];
//根据Worker数分配进程
for ($i = 0; $i < $used; $i++)
{
$this->forkItemExec($item);
}
}
//常驻守护
$this->daemonWait();
}
/**
* 创建子进程
* @param Closure $childInvoke
* @param Closure $mainInvoke
*/
protected function fork($childInvoke, $mainInvoke)
{
$pid = pcntl_fork();
if ($pid == -1)
{
Helper::showError('fork child process failed,please try again');
}
elseif ($pid)
{
$mainInvoke($pid);
}
else
{
$childInvoke();
}
}
/**
* 创建任务执行的子进程
* @param array $item
*/
protected function forkItemExec($item)
{
$this->fork(
function () use ($item) {
$this->invoker($item);
},
function ($pid) use ($item) {
//write_log
$ppid = posix_getpid();
$this->processList[] = ['pid' => $pid, 'name' => $item['alas'], 'item' => $item, 'started' => $item['data'], 'time' => $item['time'], 'status' => 'active', 'ppid' => $ppid];
//set not block
pcntl_wait($status, WNOHANG);
}
);
}
/**
* 执行器
* @param array $item
* @throws Throwable
*/
protected function invoker($item)
{
//输出信息
$item['ppid'] = posix_getppid();
$text = "this worker {$item['alas']}";
Helper::writeTypeLog("$text is start");
//进程标题
Helper::cli_set_process_title($item['alas']);
//Kill信号
pcntl_signal(SIGTERM, function () use ($text) {
Helper::writeTypeLog("listened kill command, $text not to exit the program for safety");
});
//执行任务
$this->executeInvoker($item);
}
/**
* 通过闹钟信号执行
* @param array $item
*/
protected function invokeByDefault($item)
{
//安装信号管理
pcntl_signal(SIGALRM, function () use ($item) {
pcntl_alarm($item['time']);
$this->execute($item);
}, false);
//发送闹钟信号
pcntl_alarm($item['time']);
//挂起进程(同步调用信号,异步CPU休息)
while (true)
{
//CPU休息
Helper::sleep(1);
//信号处理(同步/异步)
if (!Env::get('canAsync')) pcntl_signal_dispatch();
}
}
/**
* 检查常驻进程是否存活
* @param array $item
*/
protected function checkDaemonForExit($item)
{
if (!posix_kill($item['ppid'], 0))
{
Helper::writeTypeLog("listened exit command, this worker {$item['alas']} is exiting safely", 'info', true);
}
}
/**
* 守护进程常驻
*/
protected function daemonWait()
{
//设置进程标题
Helper::cli_set_process_title(Env::get('prefix'));
//输出信息
$text = "this manager";
Helper::writeTypeLog("$text is start");
if (!Env::get('daemon'))
{
Helper::showTable($this->processStatus(), false);
Helper::showInfo('start success,press ctrl+c to stop');
}
//Kill信号
pcntl_signal(SIGTERM, function () use ($text) {
Helper::writeTypeLog("listened kill command $text is exiting safely", 'info', true);
});
//挂起进程
while (true)
{
//CPU休息
Helper::sleep(1);
//接收命令start/status/stop
$this->commander->waitCommandForExecute(2, function ($command) use ($text) {
$exitText = "listened exit command, $text is exiting safely";
$statusText = "listened status command, $text is reported";
$forceExitText = "listened exit command, $text is exiting unsafely";
if ($command['type'] == 'start')
{
if ($command['time'] > $this->startTime)
{
Helper::writeTypeLog($forceExitText);
posix_kill(0, SIGKILL);
}
}
if ($command['type'] == 'status')
{
$report = $this->processStatus();
$this->commander->send([
'type' => 'status',
'msgType' => 1,
'status' => $report,
]);
Helper::writeTypeLog($statusText);
}
if ($command['type'] == 'stop')
{
if ($command['force'])
{
Helper::writeTypeLog($forceExitText);
posix_kill(0, SIGKILL);
}
else
{
Helper::writeTypeLog($exitText);
exit();
}
}
}, $this->startTime);
//信号调度
if (!Env::get('canAsync')) pcntl_signal_dispatch();
//检查进程
if (Env::get('canAutoRec')) $this->processStatus();
}
}
/**
* 查看进程状态
* @return array
*/
protected function processStatus()
{
$report = [];
foreach ($this->processList as $key => $item)
{
//提取参数
$pid = $item['pid'];
//进程状态
$rel = pcntl_waitpid($pid, $status, WNOHANG);
if ($rel == -1 || $rel > 0)
{
//标记状态
$item['status'] = 'stop';
//进程退出,重新fork
if (Env::get('canAutoRec'))
{
$this->forkItemExec($item['item']);
Helper::writeTypeLog("the worker {$item['name']}(pid:{$pid}) is stop,try to fork a new one");
unset($this->processList[$key]);
}
}
//记录状态
unset($item['item']);
$report[] = $item;
}
return $report;
}
}

View File

@ -0,0 +1,252 @@
<?php
namespace easyTask\Process;
use easyTask\Command;
use easyTask\Env;
use easyTask\Error;
use easyTask\Helper;
use easyTask\Terminal;
use \Event as Event;
use \EventBase as EventBase;
use \EventConfig as EventConfig;
use \Exception as Exception;
use \Throwable as Throwable;
/**
* Class Process
* @package easyTask\Process
*/
abstract class Process
{
/**
* 进程启动时间
* @var int
*/
protected $startTime;
/**
* 任务总数
* @var int
*/
protected $taskCount;
/**
* 任务列表
* @var array
*/
protected $taskList;
/**
* 进程命令管理
* @var Command
*/
protected $commander;
/**
* 构造函数
* @param array $taskList
*/
public function __construct($taskList)
{
$this->startTime = time();
$this->taskList = $taskList;
$this->setTaskCount();
$this->commander = new Command();
}
/**
* 开始运行
*/
abstract public function start();
/**
* 运行状态
*/
public function status()
{
//发送命令
$this->commander->send([
'type' => 'status',
'msgType' => 2
]);
$this->masterWaitExit();
}
/**
* 停止运行
* @param bool $force 是否强制
*/
public function stop($force = false)
{
//发送命令
$force = $force ?: true;
$this->commander->send([
'type' => 'stop',
'force' => $force,
'msgType' => 2
]);
}
/**
* 初始化任务数量
*/
protected function setTaskCount()
{
$count = 0;
foreach ($this->taskList as $key => $item) {
$count += (int)$item['used'];
}
$this->taskCount = $count;
}
/**
* 检查是否可写标准输出日志
* @return bool
*/
protected function canWriteStd()
{
return Env::get('daemon') && !Env::get('closeStdOutLog');
}
/**
* 执行任务代码
* @param array $item
* @throws
*/
protected function execute($item)
{
//根据任务类型执行
$daemon = Env::get('daemon');
//Std_Start
if ($this->canWriteStd()) ob_start();
try {
$type = $item['type'];
switch ($type) {
case 1:
$func = $item['func'];
$func();
break;
case 2:
call_user_func([$item['class'], $item['func']]);
break;
case 3:
$object = new $item['class']();
call_user_func([$object, $item['func']]);
break;
default:
// 原始代码保留
// $result = shell_exec($item['command']);
// if ($result) {
// echo $result . PHP_EOL;
// Helper::output($result);
// }
// if ($result === false) {
// $errorResult = 'failed to execute ' . $item['alas'] . ' task' . PHP_EOL;
// Helper::output($errorResult);
// }
// 修改运行方式 为Terminal
Terminal::instance(1, $item['alas'])->exec($item['command']);
}
} catch (Exception $exception) {
if (Helper::isWin()) {
Helper::showException($exception, 'exception', !$daemon);
} else {
if (!$daemon) throw $exception;
Helper::writeLog(Helper::formatException($exception));
}
} catch (Throwable $exception) {
if (Helper::isWin()) {
Helper::showException($exception, 'exception', !$daemon);
} else {
if (!$daemon) throw $exception;
Helper::writeLog(Helper::formatException($exception));
}
}
//Std_End
if ($this->canWriteStd()) {
$stdChar = ob_get_contents();
if ($stdChar) Helper::saveStdChar($stdChar);
ob_end_clean();
}
//检查常驻进程存活
$this->checkDaemonForExit($item);
}
/**
* 执行任务
* @param array $item
* @throws Throwable
*/
protected function executeInvoker($item)
{
if ($item['time'] === 0) {
$this->invokerByDirect($item);
} else {
Env::get('canEvent') ? $this->invokeByEvent($item) : $this->invokeByDefault($item);
}
}
/**
* 通过Event事件执行
* @param array $item
*/
protected function invokeByEvent($item)
{
//创建Event事件
$eventConfig = new EventConfig();
$eventBase = new EventBase($eventConfig);
$event = new Event($eventBase, -1, Event::TIMEOUT | Event::PERSIST, function () use ($item) {
try {
$this->execute($item);
} catch (Throwable $exception) {
$type = 'exception';
Error::report($type, $exception);
$this->checkDaemonForExit($item);
}
});
//添加事件
$event->add($item['time']);
//事件循环
$eventBase->loop();
}
/**
* 普通执行
* @param array $item
* @throws Throwable
*/
protected function invokerByDirect($item)
{
$this->execute($item);
exit;
}
/**
* 主进程等待结束退出
*/
protected function masterWaitExit()
{
$i = $this->taskCount + 3;
while ($i--) {
//接收汇报
$this->commander->waitCommandForExecute(1, function ($report) {
if ($report['type'] == 'status' && $report['status']) {
Helper::showTable($report['status']);
}
}, $this->startTime);
//CPU休息
Helper::sleep(1);
}
Helper::showInfo('this cpu is too busy,please use status command try again');
exit;
}
}

459
extend/easyTask/Process/Win.php Executable file
View File

@ -0,0 +1,459 @@
<?php
namespace easyTask\Process;
use easyTask\Wts;
use easyTask\Wpc;
use easyTask\Env;
use easyTask\Helper;
use \Exception as Exception;
use \Throwable as Throwable;
/**
* Class Win
* @package easyTask\Process
*/
class Win extends Process
{
/**
* Wts服务
* @var Wts
*/
protected $wts;
/**
* 虚拟进程列表
* @var array
*/
protected $workerList;
/**
* 实体进程容器
* @var array
*/
protected $wpcContainer;
/**
* AutoRec事件
* @var bool
*/
protected $autoRecEvent;
/**
* 构造函数
* @param array $taskList
*/
public function __construct($taskList)
{
$this->wts = new Wts();
parent::__construct($taskList);
}
/**
* 开始运行
*/
public function start()
{
//构建基础
$this->make();
//启动检查
$this->checkForRun();
//进程分配
$func = function ($name) {
$this->executeByProcessName($name);
};
if (!$this->wts->allocateProcess($func))
{
Helper::showError('unexpected error, process has been allocated');
}
}
/**
* 启动检查
*/
protected function checkForRun()
{
if (!Env::get('phpPath'))
{
Helper::showError('please use setPhpPath api to set phpPath');
}
if (!$this->chkCanStart())
{
Helper::showError('please close the running process first');
}
}
/**
* 检查进程
* @return bool
*/
protected function chkCanStart()
{
$workerList = $this->workerList;
foreach ($workerList as $name => $item)
{
$status = $this->wts->getProcessStatus($name);
if (!$status)
{
return true;
}
}
return false;
}
/**
* 跟进进程名称执行任务
* @param string $name
* @throws Exception|Throwable
*/
protected function executeByProcessName($name)
{
switch ($name)
{
case 'master':
$this->master();
break;
case 'manager':
$this->manager();
break;
default:
$this->invoker($name);
}
}
/**
* 构建任务
*/
protected function make()
{
$list = [];
if (!$this->wts->getProcessStatus('manager'))
{
$list = ['master', 'manager'];
}
foreach ($list as $name)
{
$this->wts->joinProcess($name);
}
foreach ($this->taskList as $key => $item)
{
//提取参数
$alas = $item['alas'];
$used = $item['used'];
//根据Worker数构建
for ($i = 0; $i < $used; $i++)
{
$name = $item['name'] = $alas . '___' . $i;
$this->workerList[$name] = $item;
$this->wts->joinProcess($name);
}
}
}
/**
* 主进程
* @throws Exception
*/
protected function master()
{
//创建常驻进程
$this->forkItemExec();
//查询状态
$i = $this->taskCount + 15;
while ($i--)
{
$status = $this->wts->getProcessStatus('manager');
if ($status)
{
$this->status();
break;
}
Helper::sleep(1);
}
}
/**
* 常驻进程
*/
protected function manager()
{
//分配子进程
$this->allocate();
//后台常驻运行
$this->daemonWait();
}
/**
* 分配子进程
*/
protected function allocate()
{
//清理进程信息
$this->wts->cleanProcessInfo();
foreach ($this->taskList as $key => $item)
{
//提取参数
$used = $item['used'];
//根据Worker数创建子进程
for ($i = 0; $i < $used; $i++)
{
$this->joinWpcContainer($this->forkItemExec());
}
}
}
/**
* 注册实体进程
* @param Wpc $wpc
*/
protected function joinWpcContainer($wpc)
{
$this->wpcContainer[] = $wpc;
foreach ($this->wpcContainer as $key => $wpc)
{
if ($wpc->hasExited())
{
unset($this->wpcContainer[$key]);
}
}
}
/**
* 创建任务执行子进程
* @return Wpc
*/
protected function forkItemExec()
{
$wpc = null;
try
{
//提取参数
$argv = Helper::getCliInput(2);
$file = array_shift($argv);;
$char = join(' ', $argv);
$work = dirname(array_shift($argv));
$style = Env::get('daemon') ? 1 : 0;
//创建进程
$wpc = new Wpc();
$wpc->setFile($file);
$wpc->setArgument($char);
$wpc->setStyle($style);
$wpc->setWorkDir($work);
$pid = $wpc->start();
if (!$pid) Helper::showError('create process failed,please try again', true);
}
catch (Exception $exception)
{
Helper::showError(Helper::convert_char($exception->getMessage()), true);
}
return $wpc;
}
/**
* 执行器
* @param string $name 任务名称
* @throws Throwable
*/
protected function invoker($name)
{
//提取字典
$taskDict = $this->workerList;
if (!isset($taskDict[$name]))
{
Helper::showError("the task name $name is not exist" . json_encode($taskDict));
}
//提取Task字典
$item = $taskDict[$name];
//输出信息
$pid = getmypid();
$title = Env::get('prefix') . '_' . $item['alas'];
Helper::showInfo("this worker $title is start");
//设置进程标题
Helper::cli_set_process_title($title);
//保存进程信息
$item['pid'] = $pid;
$this->wts->saveProcessInfo([
'pid' => $pid,
'name' => $item['name'],
'alas' => $item['alas'],
'started' => date('Y-m-d H:i:s', $this->startTime),
'time' => $item['time']
]);
//执行任务
$this->executeInvoker($item);
}
/**
* 通过默认定时执行
* @param array $item 执行项目
* @throws Throwable
*/
protected function invokeByDefault($item)
{
while (true)
{
//CPU休息
Helper::sleep($item['time']);
//执行任务
$this->execute($item);
}
exit;
}
/**
* 检查常驻进程是否存活
* @param array $item
*/
protected function checkDaemonForExit($item)
{
//检查进程存活
$status = $this->wts->getProcessStatus('manager');
if (!$status)
{
$text = Env::get('prefix') . '_' . $item['alas'];
Helper::showInfo("listened exit command, this worker $text is exiting safely", true);
}
}
/**
* 后台常驻运行
*/
protected function daemonWait()
{
//进程标题
Helper::cli_set_process_title(Env::get('prefix'));
//输出信息
$text = "this manager";
Helper::showInfo("$text is start");;
//挂起进程
while (true)
{
//CPU休息
Helper::sleep(1);
//接收命令status/stop
$this->commander->waitCommandForExecute(2, function ($command) use ($text) {
$commandType = $command['type'];
switch ($commandType)
{
case 'status':
$this->commander->send([
'type' => 'status',
'msgType' => 1,
'status' => $this->getReport(),
]);
Helper::showInfo("listened status command, $text is reported");
break;
case 'stop':
if ($command['force']) $this->stopWorkerByForce();
Helper::showInfo("listened exit command, $text is exiting safely", true);
break;
}
}, $this->startTime);
//检查进程
if (Env::get('canAutoRec'))
{
$this->getReport(true);
if ($this->autoRecEvent)
{
$this->autoRecEvent = false;
}
}
}
}
/**
* 获取报告
* @param bool $output
* @return array
* @throws
*/
protected function getReport($output = false)
{
$report = $this->workerStatus($this->taskCount);
foreach ($report as $key => $item)
{
if ($item['status'] == 'stop' && Env::get('canAutoRec'))
{
$this->joinWpcContainer($this->forkItemExec());
if ($output)
{
$this->autoRecEvent = true;
Helper::showInfo("the worker {$item['name']}(pid:{$item['pid']}) is stop,try to fork a new one");
}
}
}
return $report;
}
/**
* 查看进程状态
* @param int $count
* @return array
*/
protected function workerStatus($count)
{
//构建报告
$report = $infoData = [];
$tryTotal = 10;
while ($tryTotal--)
{
Helper::sleep(1);
$infoData = $this->wts->getProcessInfo();
if ($count == count($infoData)) break;
}
//组装数据
$pid = getmypid();
$prefix = Env::get('prefix');
foreach ($infoData as $name => $item)
{
$report[] = [
'pid' => $item['pid'],
'name' => "{$prefix}_{$item['alas']}",
'started' => $item['started'],
'time' => $item['time'],
'status' => $this->wts->getProcessStatus($name) ? 'active' : 'stop',
'ppid' => $pid,
];
}
return $report;
}
/**
* 强制关闭所有进程
*/
protected function stopWorkerByForce()
{
foreach ($this->wpcContainer as $wpc)
{
try
{
$wpc->stop(2);
}
catch (Exception $exception)
{
Helper::showError(Helper::convert_char($exception->getMessage()), false);
}
}
}
}

87
extend/easyTask/Queue.php Executable file
View File

@ -0,0 +1,87 @@
<?php
namespace easyTask;
/**
* Class Queue
* @package easyTask
*/
class Queue
{
/**
* 进程锁
* @var Lock
*/
private $lock;
/**
* 队列文件
* @var string
*/
private $queFile;
/**
* 构造函数
* @param string $name
* @throws
*/
public function __construct($name = 'queue')
{
//创建进程锁
$this->lock = new Lock($name);
//创建队列文件
$path = Helper::getQuePath();
$file = $path . '%s.dat';
$this->queFile = sprintf($file, md5($name));
if (!file_exists($this->queFile))
{
if (!file_put_contents($this->queFile, '[]', LOCK_EX))
{
Helper::showError('crate queFile failed,please try again');
}
}
}
/**
* 向队列投递数据
* @param string $item
*/
public function push($item)
{
$this->lock->execute(function () use ($item) {
//read
$content = file_get_contents($this->queFile);
$queue_data = $content ? json_decode($content, true) : [];
$queue_data = is_array($queue_data) ? $queue_data : [];
//write
array_push($queue_data, $item);
if (!file_put_contents($this->queFile, json_encode($queue_data)))
{
Helper::showError('failed to save data to queue file');
}
});
}
/**
* 从队列弹出数据
* @return string|null
*/
public function shift()
{
return $this->lock->execute(function () {
//read
$content = file_get_contents($this->queFile);
$queue_data = $content ? json_decode($content, true) : [];
$queue_data = is_array($queue_data) ? $queue_data : [];
//shift+write
$value = array_shift($queue_data);
if (!file_put_contents($this->queFile, json_encode($queue_data)))
{
Helper::showError('failed to save data to queue file');
}
return $value;
});
}
}

292
extend/easyTask/Table.php Executable file
View File

@ -0,0 +1,292 @@
<?php
namespace easyTask;
/**
* Class Table
* @package easyTask
*/
class Table
{
const ALIGN_LEFT = 1;
const ALIGN_RIGHT = 0;
const ALIGN_CENTER = 2;
/**
* 头信息数据
* @var array
*/
protected $header = [];
/**
* 头部对齐方式 默认1 ALGIN_LEFT 0 ALIGN_RIGHT 2 ALIGN_CENTER
* @var int
*/
protected $headerAlign = 1;
/**
* 表格数据(二维数组)
* @var array
*/
protected $rows = [];
/**
* 单元格对齐方式 默认1 ALGIN_LEFT 0 ALIGN_RIGHT 2 ALIGN_CENTER
* @var int
*/
protected $cellAlign = 1;
/**
* 单元格宽度信息
* @var array
*/
protected $colWidth = [];
/**
* 表格输出样式
* @var string
*/
protected $style = 'default';
/**
* 表格样式定义
* @var array
*/
protected $format = [
'compact' => [],
'default' => [
'top' => ['+', '-', '+', '+'],
'cell' => ['|', ' ', '|', '|'],
'middle' => ['+', '-', '+', '+'],
'bottom' => ['+', '-', '+', '+'],
'cross-top' => ['+', '-', '-', '+'],
'cross-bottom' => ['+', '-', '-', '+'],
],
'markdown' => [
'top' => [' ', ' ', ' ', ' '],
'cell' => ['|', ' ', '|', '|'],
'middle' => ['|', '-', '|', '|'],
'bottom' => [' ', ' ', ' ', ' '],
'cross-top' => ['|', ' ', ' ', '|'],
'cross-bottom' => ['|', ' ', ' ', '|'],
],
'borderless' => [
'top' => ['=', '=', ' ', '='],
'cell' => [' ', ' ', ' ', ' '],
'middle' => ['=', '=', ' ', '='],
'bottom' => ['=', '=', ' ', '='],
'cross-top' => ['=', '=', ' ', '='],
'cross-bottom' => ['=', '=', ' ', '='],
],
'box' => [
'top' => ['┌', '─', '┬', '┐'],
'cell' => ['│', ' ', '│', '│'],
'middle' => ['├', '─', '┼', '┤'],
'bottom' => ['└', '─', '┴', '┘'],
'cross-top' => ['├', '─', '┴', '┤'],
'cross-bottom' => ['├', '─', '┬', '┤'],
],
'box-double' => [
'top' => ['╔', '═', '╤', '╗'],
'cell' => ['║', ' ', '│', '║'],
'middle' => ['╠', '─', '╪', '╣'],
'bottom' => ['╚', '═', '╧', '╝'],
'cross-top' => ['╠', '═', '╧', '╣'],
'cross-bottom' => ['╠', '═', '╤', '╣'],
],
];
/**
* 设置表格头信息 以及对齐方式
* @param array $header 要输出的Header信息
* @param int $align 对齐方式 默认1 ALGIN_LEFT 0 ALIGN_RIGHT 2 ALIGN_CENTER
* @return void
*/
public function setHeader($header, $align = self::ALIGN_LEFT)
{
$this->header = $header;
$this->headerAlign = $align;
$this->checkColWidth($header);
}
/**
* 设置输出表格数据 及对齐方式
* @param array $rows 要输出的表格数据(二维数组)
* @param int $align 对齐方式 默认1 ALGIN_LEFT 0 ALIGN_RIGHT 2 ALIGN_CENTER
* @return void
*/
public function setRows($rows, $align = self::ALIGN_LEFT)
{
$this->rows = $rows;
$this->cellAlign = $align;
foreach ($rows as $row)
{
$this->checkColWidth($row);
}
}
/**
* 检查列数据的显示宽度
* @param mixed $row 行数据
* @return void
*/
protected function checkColWidth($row)
{
if (is_array($row))
{
foreach ($row as $key => $cell)
{
if (!isset($this->colWidth[$key]) || strlen($cell) > $this->colWidth[$key])
{
$this->colWidth[$key] = strlen($cell);
}
}
}
}
/**
* 增加一行表格数据
* @param mixed $row 行数据
* @param bool $first 是否在开头插入
* @return void
*/
public function addRow($row, $first = false)
{
if ($first)
{
array_unshift($this->rows, $row);
}
else
{
$this->rows[] = $row;
}
$this->checkColWidth($row);
}
/**
* 设置输出表格的样式
* @param string $style 样式名
* @return void
*/
public function setStyle($style)
{
$this->style = isset($this->format[$style]) ? $style : 'default';
}
/**
* 输出分隔行
* @param string $pos 位置
* @return string
*/
protected function renderSeparator($pos)
{
$style = $this->getStyle($pos);
$array = [];
foreach ($this->colWidth as $width)
{
$array[] = str_repeat($style[1], $width + 2);
}
return $style[0] . implode($style[2], $array) . $style[3] . PHP_EOL;
}
/**
* 输出表格头部
* @return string
*/
protected function renderHeader()
{
$style = $this->getStyle('cell');
$content = $this->renderSeparator('top');
foreach ($this->header as $key => $header)
{
$array[] = ' ' . str_pad($header, $this->colWidth[$key], $style[1], $this->headerAlign);
}
if (!empty($array))
{
$content .= $style[0] . implode(' ' . $style[2], $array) . ' ' . $style[3] . PHP_EOL;
if ($this->rows)
{
$content .= $this->renderSeparator('middle');
}
}
return $content;
}
/**
* 获取风格
* @param string $style
* @return array
*/
protected function getStyle($style)
{
if ($this->format[$this->style])
{
$style = $this->format[$this->style][$style];
}
else
{
$style = [' ', ' ', ' ', ' '];
}
return $style;
}
/**
* 输出表格
* @param array $dataList 表格数据
* @return string
*/
public function render($dataList = [])
{
if ($dataList)
{
$this->setRows($dataList);
}
// 输出头部
$content = $this->renderHeader();
$style = $this->getStyle('cell');
if ($this->rows)
{
foreach ($this->rows as $row)
{
if (is_string($row) && '-' === $row)
{
$content .= $this->renderSeparator('middle');
}
elseif (is_scalar($row))
{
$content .= $this->renderSeparator('cross-top');
$array = str_pad($row, 3 * (count($this->colWidth) - 1) + array_reduce($this->colWidth, function ($a, $b) {
return $a + $b;
}));
$content .= $style[0] . ' ' . $array . ' ' . $style[3] . PHP_EOL;
$content .= $this->renderSeparator('cross-bottom');
}
else
{
$array = [];
foreach ($row as $key => $val)
{
$array[] = ' ' . str_pad($val, $this->colWidth[$key], ' ', $this->cellAlign);
}
$content .= $style[0] . implode(' ' . $style[2], $array) . ' ' . $style[3] . PHP_EOL;
}
}
}
$content .= $this->renderSeparator('bottom');
return $content;
}
}

334
extend/easyTask/Task.php Executable file
View File

@ -0,0 +1,334 @@
<?php
namespace easyTask;
use \Closure as Closure;
use easyTask\Process\Linux;
use easyTask\Process\Win;
use \ReflectionClass as ReflectionClass;
use \ReflectionMethod as ReflectionMethod;
use \ReflectionException as ReflectionException;
/**
* Class Task
* @package easyTask
*/
class Task
{
/**
* 任务列表
* @var array
*/
private $taskList = [];
/**
* 构造函数
*/
public function __construct()
{
//检查运行环境
$currentOs = Helper::isWin() ? 1 : 2;
Check::analysis($currentOs);
$this->initialise($currentOs);
}
/**
* 进程初始化
* @param int $currentOs
*/
private function initialise($currentOs)
{
//初始化基础配置
Env::set('prefix', 'Task');
Env::set('canEvent', Helper::canUseEvent());
Env::set('currentOs', $currentOs);
Env::set('canAsync', Helper::canUseAsyncSignal());
Env::set('closeErrorRegister', false);
//初始化PHP_BIN|CODE_PAGE
if ($currentOs == 1) {
Helper::setPhpPath();
Helper::setCodePage();
}
}
/**
* 设置是否守护进程
* @param bool $daemon
* @return $this
*/
public function setDaemon($daemon = false)
{
Env::set('daemon', $daemon);
return $this;
}
/**
* 设置任务前缀
* @param string $prefix
* @return $this
*/
public function setPrefix($prefix = 'Task')
{
if (Env::get('runTimePath')) {
Helper::showSysError('should use setPrefix before setRunTimePath');
}
Env::set('prefix', $prefix);
return $this;
}
/**
* 设置PHP执行路径(windows)
* @param string $path
* @return $this
*/
public function setPhpPath($path)
{
$file = realpath($path);
if (!file_exists($file)) {
Helper::showSysError("the path {$path} is not exists");
}
Helper::setPhpPath($path);
return $this;
}
/**
* 设置时区
* @param string $timeIdent
* @return $this
*/
public function setTimeZone($timeIdent)
{
date_default_timezone_set($timeIdent);
return $this;
}
/**
* 设置运行时目录
* @param string $path
* @return $this
*/
public function setRunTimePath($path)
{
if (!is_dir($path)) {
Helper::showSysError("the path {$path} is not exist");
}
if (!is_writable($path)) {
Helper::showSysError("the path {$path} is not writeable");
}
Env::set('runTimePath', realpath($path));
return $this;
}
/**
* 设置子进程自动恢复
* @param bool $isRec
* @return $this
*/
public function setAutoRecover($isRec = false)
{
Env::set('canAutoRec', $isRec);
return $this;
}
/**
* 设置关闭标准输出的日志
* @param bool $close
* @return $this
*/
public function setCloseStdOutLog($close = false)
{
Env::set('closeStdOutLog', $close);
return $this;
}
/**
* 设置关闭系统异常注册
* @param bool $isReg 是否关闭
* @return $this
*/
public function setCloseErrorRegister($isReg = false)
{
Env::set('closeErrorRegister', $isReg);
return $this;
}
/**
* 异常通知
* @param string|Closure $notify
* @return $this
*/
public function setErrorRegisterNotify($notify)
{
if (Env::get('closeErrorRegister')) {
Helper::showSysError('you must set closeErrorRegister as false before use this api');
}
if (!$notify instanceof Closure && !is_string($notify)) {
Helper::showSysError('notify parameter can only be string or closure');
}
Env::set('notifyHand', $notify);
return $this;
}
/**
* 新增匿名函数作为任务
* @param Closure $func 匿名函数
* @param string $alas 任务别名
* @param mixed $time 定时器间隔
* @param int $used 定时器占用进程数
* @return $this
* @throws
*/
public function addFunc($func, $alas, $time = 1, $used = 1)
{
$uniqueId = md5($alas);
if (!($func instanceof Closure)) {
Helper::showSysError('func must instanceof Closure');
}
if (isset($this->taskList[$uniqueId])) {
Helper::showSysError("task $alas already exists");
}
Helper::checkTaskTime($time);
$this->taskList[$uniqueId] = [
'type' => 1,
'func' => $func,
'alas' => $alas,
'time' => $time,
'used' => $used
];
return $this;
}
/**
* 新增类作为任务
* @param string $class 类名称
* @param string $func 方法名称
* @param string $alas 任务别名
* @param mixed $time 定时器间隔
* @param int $used 定时器占用进程数
* @return $this
* @throws
*/
public function addClass($class, $func, $alas, $time = 1, $used = 1)
{
$uniqueId = md5($alas);
if (!class_exists($class)) {
Helper::showSysError("class {$class} is not exist");
}
if (isset($this->taskList[$uniqueId])) {
Helper::showSysError("task $alas already exists");
}
try {
$reflect = new ReflectionClass($class);
if (!$reflect->hasMethod($func)) {
Helper::showSysError("class {$class}'s func {$func} is not exist");
}
$method = new ReflectionMethod($class, $func);
if (!$method->isPublic()) {
Helper::showSysError("class {$class}'s func {$func} must public");
}
Helper::checkTaskTime($time);
$this->taskList[$uniqueId] = [
'type' => $method->isStatic() ? 2 : 3,
'func' => $func,
'alas' => $alas,
'time' => $time,
'used' => $used,
'class' => $class
];
} catch (ReflectionException $exception) {
Helper::showException($exception);
}
return $this;
}
/**
* 新增指令作为任务
* @param string $command 指令
* @param string $alas 任务别名
* @param mixed $time 定时器间隔
* @param int $used 定时器占用进程数
* @return $this
*/
public function addCommand($command, $alas, $time = 1, $used = 1)
{
$uniqueId = md5($alas);
if (!Helper::canUseExcCommand()) {
Helper::showSysError('please open the disabled function of shell_exec');
}
if (isset($this->taskList[$uniqueId])) {
Helper::showSysError("task $alas already exists");
}
Helper::checkTaskTime($time);
$this->taskList[$uniqueId] = [
'type' => 4,
'alas' => $alas,
'time' => $time,
'used' => $used,
'command' => $command,
];
return $this;
}
/**
* 获取进程管理实例
* @return Win | Linux
*/
private function getProcess()
{
$taskList = $this->taskList;
$currentOs = Env::get('currentOs');
if ($currentOs == 1) {
return (new Win($taskList));
} else {
return (new Linux($taskList));
}
}
/**
* 开始运行
* @throws
*/
public function start()
{
if (!$this->taskList) {
Helper::showSysError('please add task to run');
}
//异常注册
if (!Env::get('closeErrorRegister')) {
Error::register();
}
//目录构建
Helper::initAllPath();
//进程启动
$process = $this->getProcess();
$process->start();
}
/**
* 运行状态
* @throws
*/
public function status()
{
$process = $this->getProcess();
$process->status();
}
/**
* 停止运行
* @param bool $force 是否强制
* @throws
*/
public function stop($force = false)
{
$process = $this->getProcess();
$process->stop($force);
}
}

103
extend/easyTask/Terminal.php Executable file
View File

@ -0,0 +1,103 @@
<?php
/**
* Created by PhpStorm
* User Julyssn
* Date 2022/12/15 11:03
*/
namespace easyTask;
class Terminal
{
/**
* @var object 对象实例
*/
protected static $instance;
protected $rootPath;
/**
* 命令执行输出文件
*/
protected $outputFile = null;
/**
* proc_open 的参数
*/
protected $descriptorsPec = [];
protected $pipes = null;
protected $procStatus = null;
protected $runType = 1;
/**
* @param int $runType 1 task使用 输出连续记录 2 普通使用 输出读取后删除
* @return object|static
*/
public static function instance($runType, $outputName = null)
{
if (is_null(self::$instance)) {
self::$instance = new static($runType, $outputName);
}
return self::$instance;
}
public function __construct($runType, $outputName = null)
{
$this->rootPath = root_path();
$this->runType = $runType;
// 初始化日志文件
if ($this->runType === 1) {
$outputDir = Helper::getStdPath();
$this->outputFile = $outputDir . 'exec_' . $outputName . '.std';
} else {
$outputDir = $this->rootPath . 'runtime' . DIRECTORY_SEPARATOR;
$this->outputFile = $outputDir . 'exec_' . getOnlyToken() . '.log';
file_put_contents($this->outputFile, '');
}
// 命令执行结果输出到文件而不是管道
$this->descriptorsPec = [0 => ['pipe', 'r'], 1 => ['file', $this->outputFile, 'a'], 2 => ['file', $this->outputFile, 'a']];
}
public function __destruct()
{
// 类销毁 删除文件,type为2才删除
if ($this->runType == 2) {
unlink($this->outputFile);
}
}
public function exec(string $command)
{
$this->process = proc_open($command, $this->descriptorsPec, $this->pipes, $this->rootPath);
foreach ($this->pipes as $pipe) {
fclose($pipe);
}
proc_close($this->process);
if ($this->runType == 2) {
$contents = file_get_contents($this->outputFile);
return $contents;
}
}
public function getProcStatus(): bool
{
$status = proc_get_status($this->process);
return (bool)$status['running'];
}
}

246
extend/easyTask/Wpc.php Executable file
View File

@ -0,0 +1,246 @@
<?php
namespace easyTask;
use \Com as Com;
use \Exception as Exception;
/**
* Class Wpc
* @package easyTask
*/
class Wpc
{
/**
* Wpc实例
* @var null
*/
private $instance = null;
/**
* Wpc constructor.
* @return $this
*/
public function __construct()
{
$this->instance = new Com('Wpc.Core');
return $this;
}
/**
* 获取Com_Variant
* @return Com
*/
public function getInstance()
{
return $this->instance;
}
/**
* 设置进程文件
* @param string $filename
* @return $this
* @throws Exception
*/
public function setFile($filename)
{
$filename = realpath($filename);
if (!file_exists($filename))
{
throw new Exception("the file:{$filename} is not exist");
}
$this->instance->SetFile($filename);
return $this;
}
/**
* 设置进程域
* @param string $domain
* @return $this
*/
public function setDomain($domain)
{
$domain = (string)$domain;
$this->instance->SetDomain($domain);
return $this;
}
/**
* 设置进程参数
* @param string $argument
* @return $this
*/
public function setArgument($argument)
{
$argument = (string)$argument;
$this->instance->SetArgument($argument);
return $this;
}
/**
* 设置进程是否带窗口
* @param bool $set
* @return $this
*/
public function setNoWindow($set)
{
$set = (bool)$set;
$this->instance->SetNoWindow($set);
return $this;
}
/**
* 设置启动进程的用户
* @param string $username
* @return $this
*/
public function setUsername($username)
{
$username = (string)$username;
$this->instance->SetUsername($username);
return $this;
}
/**
* 设置启动进程的密码
* @param string $password
* @return $this
*/
public function setPassword($password)
{
$password = (string)$password;
$this->instance->SetPassword($password);
return $this;
}
/**
* 设置进程风格
* @param int $style (0.正常 1.隐藏 2.最小化 3.最大化)
* @return $this
*/
public function setStyle($style)
{
$style = (int)$style;
$this->instance->SetStyle($style);
return $this;
}
/**
* 设置进程工作目录
* @param string $path
* @return $this
* @throws Exception
*/
public function setWorkDir($path)
{
$path = realpath($path);
if (!is_dir($path))
{
throw new Exception("the path:{$path} is not exist");
}
$this->instance->SetWorkDir($path);
return $this;
}
/**
* 设置等待关联进程退出
* @param int $timeOut 超时时间
* @return $this
* @throws Exception
*/
public function setWaitForExit($timeOut = 1024)
{
$timeOut = (int)$timeOut;
$this->instance->SetWaitForExit($timeOut);
return $this;
}
/**
* 获取进程ID
* @return int
*/
public function getPid()
{
return $this->instance->GetPid();
}
/**
* 获取进程sessionId
* @return int
*/
public function getSessionId()
{
return $this->instance->GetSessionId();
}
/**
* 获取程是否已经退出
* @return bool
*/
public function hasExited()
{
return $this->instance->HasExited();
}
/**
* 获取进程名称
* @return string
*/
public function getProcessName()
{
return $this->instance->GetProcessName();
}
/**
* 获取进程打开的资源句柄数
* @return int
*/
public function getHandleCount()
{
return $this->instance->GetHandleCount();
}
/**
* 获取进程主窗口标题
* @return string
*/
public function getMainWindowTitle()
{
return $this->instance->GetMainWindowTitle();
}
/**
* 获取进程启动时间
* @return string
*/
public function getStartTime()
{
return $this->instance->GetStartTime();
}
/**
* 获取进程停止时间
* @return string
*/
public function getStopTime()
{
return $this->instance->GetStopTime();
}
/**
* 启动进程
* @return int 进程id
*/
public function start()
{
return $this->instance->Start();
}
/**
* 停止进程
* @param int $force (1.正常停止 2.强制停止)
*/
public function stop($force = 1)
{
$this->instance->Stop($force);
}
}

164
extend/easyTask/Wts.php Executable file
View File

@ -0,0 +1,164 @@
<?php
namespace easyTask;
use \Closure as Closure;
/**
* Class Wts
* @package easyTask
*/
class Wts
{
/**
* 进程锁
* @var Lock
*/
private $lock;
/**
* 进程名称列表
* @var array
*/
private $processNames = [];
/**
* 构造函数
*/
public function __construct()
{
//创建进程锁
$this->lock = new Lock();
//创建进程信息文件
$processFile = $this->getProcessInfoFile();
if (!file_exists($processFile))
{
file_put_contents($processFile, '');
}
}
/**
* 注册进程名称
* @param string $name
*/
public function joinProcess($name)
{
$this->processNames[] = $name;
$file = $this->getProcessFile($name);
if (!file_exists($file))
{
file_put_contents($file, $name);
}
}
/**
* 获取进程文件名
* @param string $name 进程名称
* @return string
*/
public function getProcessFile($name)
{
$runPath = Helper::getWinPath();
return $runPath . md5($name) . '.win';
}
/**
* 获取进程保存信息的文件名
* @return string
*/
public function getProcessInfoFile()
{
$runPath = Helper::getWinPath();
$infoFile = md5(__FILE__) . '.win';
return $runPath . $infoFile;
}
/**
* 获取进程状态
* @param string $name 进程名称
* @return bool
*/
public function getProcessStatus($name)
{
$file = $this->getProcessFile($name);
if (!file_exists($file))
{
return false;
}
$fp = fopen($file, "r");
if (flock($fp, LOCK_EX | LOCK_NB))
{
return false;
}
return true;
}
/**
* 获取进程信息(非阻塞)
* @return array
*/
public function getProcessInfo()
{
$file = $this->getProcessInfoFile();
$info = file_get_contents($file);
$info = json_decode($info, true);
return is_array($info) ? $info : [];
}
/**
* 清理进程信息
*/
public function cleanProcessInfo()
{
//加锁执行
$this->lock->execute(function () {
@file_put_contents($this->getProcessInfoFile(), '');
});
}
/**
* 保存进程信息
* @param array $info
*/
public function saveProcessInfo($info)
{
//加锁执行
$this->lock->execute(function () use ($info) {
//进程信息文件
$name = $info['name'];
$file = $this->getProcessInfoFile();
//读取原数据
$content = @file_get_contents($file);
$oldInfo = $content ? json_decode($content, true) : [$name => $info];
//追加数据
$oldInfo ? $oldInfo[$name] = $info : $oldInfo = $info;
file_put_contents($file, json_encode($oldInfo));
});
}
/**
* 分配进程
* @param Closure $func
* @return bool
*/
public function allocateProcess($func)
{
$processNames = $this->processNames;
foreach ($processNames as $name)
{
$file = $this->getProcessFile($name);
$fp = fopen($file, 'w');
if (flock($fp, LOCK_EX | LOCK_NB))
{
$func($name);
flock($fp, LOCK_UN);
return true;
}
fclose($fp);
}
return false;
}
}

BIN
extend/ip/17monipdb.dat Executable file

Binary file not shown.

292
extend/ip/readme.txt Executable file
View File

@ -0,0 +1,292 @@
<?php
/*
全球 IPv4 地址归属地数据库(IPIP.NET 版)
--- 2016 年 6 月引言 ---
这是 IP 库的第十五个公开版本。
半年不见,目前的数据条目数超过 320000 条了。
基于目前的客户购买情况,我们会在 7 月 1 日开始执行新价格方案。6 月份购买的不受影响,请潜在客户尽快考虑。
--
高春辉
Paul Gao
gaochunhui@gmail.com
http://www.ipip.net/
--- 2015 年 12 月引言 ---
这是 IP 库的第十四个公开版本。
目前的数据条目数超过 220000 条了。
我们一直在发布 DISCUZ 和 ECSHOP 的专用版本,但是不知道是用户变少还是用户不关心还是如何,这两年几乎无人咨询相关版本的事情,所以我们从 2016 年起,不再发布针对 DISCUZ 和 ECSHOP 的免费版。
这次的免费版也有一些小变化,不过对于不关心的用户来说,没有变化。;-)
--
高春辉
Paul Gao
gaochunhui@gmail.com
http://www.ipip.net/
--- 2015 年 10 月引言 ---
这是 IP 库的第十三个公开版本。
目前的数据条目数超过 200000 条了。
这个月底,我们这个事情,就已经两岁啦。
--
高春辉
Paul Gao
gaochunhui@gmail.com
http://www.ipip.net/
--- 2015 年 8 月引言 ---
这是 IP 库的第十二个公开版本。
目前的数据条目数接近 180000 条了,我们基本上能做到两个月新增一万条左右,注意是新增,不包括修改。
认可我们的数据质量的用户和客户越来越多,在一个 2015 TOP100 互联网公司名单里,我们在里面找到了有将近 20 家客户了。
我们和合作伙伴一起合作的的区县级 IP 数据库也赶在七月底上线了,有兴趣的请联系我们。
--
高春辉
Paul Gao
gaochunhui@gmail.com
http://www.ipip.net/
--- 2015 年 6 月引言 ---
这是 IP 库的第十一个公开版本。
目前的数据条目数超过了 170000 条了。希望下一个万条可以更快的达成。
正在做些互联网基础设施相关的新事情,等有一定结果的时候,再来汇报吧。
寻求 Golang 语言开发人员具体请看链接http://www.lagou.com/jobs/649340.html
--
高春辉
Paul Gao
gaochunhui@gmail.com
http://www.ipip.net/
--- 2015 年 4 月引言 ---
这是 IP 库的第十个公开版本。
三月份发布了 CDN 版本,针对节点调度需求做了一定的优化,也有了基本的英文版了。
目前的数据条目数超过了 160000 条了,算是个小里程碑,也有了新的专职编辑正在学习中,希望更好的维护这个数据库。
如果有人对基于全球 IP 分布以及连通性方面的数据挖掘以及相关的企业级服务有兴趣,请联系我,我们正在找专职的人员。
--
高春辉
Paul Gao
gaochunhui@gmail.com
http://www.ipip.net/
--- 2015 年 2 月引言 ---
这是 IP 库的第九个公开版本。
我们已经更换了新的官网http://www.ipip.net/
这个月也会尽力发布 CDN 的专属版本以及英文版本。
基站数据库也会看时间发布正式版。
--
高春辉
Paul Gao
gaochunhui@gmail.com
http://www.ipip.net/
--- 2014 年 12 月引言 ---
这是 IP 库的第八个公开版本。
11 月份算是 IP 库进展最大的一个月了,我们对数据进行了全面的扫描,并且在微博上发了一篇长文章后,带来了不少付费客户,远超乎我的预期,也让我对这个事情有了新的想法。
文章地址是: http://www.evernote.com/l/AAHsqnNK9T9LkqZb9LW-fLjzkx0B4Dj90lY/ ,希望您也看看。
我们也针对高级付费客户开发了 DATX 格式版本,同时集成经纬度、时区信息,在读取速度上,同等环境下是 8000QPS 对 48000QPS 的区别。
我们同时也发布了官方支持的 JAVA 版读取代码。
这个月会把网站重新改版,更换域名,发布英文版网站和数据。
下个月就是 2015 年,明年我们在努力提高数据准确度的情况下,会提高付费客户的价格,为什么提价,我会写文章来解释的,如果您需要,请在本月购买,我们承诺老客户老价格。
我们也会针对 CDN、DNS、VPN 厂商的需求,专门发布对应的版本。
另外免费版的发布情况也可能会做调整,不作任何保证。
--
高春辉
Paul Gao
gaochunhui@gmail.com
http://www.17mon.cn/
--- 2014 年 10 月引言 ---
这是 IP 库的第七个公开版本。
最近几个月在以新的方式在对 IP 数据进行标注,可以更加保证数据的正确性,目前完成度 80%,数据量接近十四万行,预计在 10 月底可以初步完成。
使用官方 PHP 代码的朋友,请更新一下代码,之前犯了一个变量赋值的错误,导致缓存一直不会被命中。感谢 QQ 群里那位名字写得很乱的朋友发现的问题。做事认真严谨很重要。
另外预告一下,明年初,我们会对付费版的价格进行调整,价格只高不低,当然之前购买的用户不会受影响。
解释一下,我们毕竟在这个事情上投入了大量的精力,而且按照价格和准确度的情况来看,不说最好的,也是极好的了,再加上国内用户购买意愿低,我们只能考虑先提价的方式,毕竟对于任何一家对 IP 库有更高需求的业务,一个月付 200、300 元能够得到数据库并能保证及时更新的话,远比自己雇一个人去更新维护的成本低很多很多,何况 IP 库这件事水也很深,雇来的人的能力就能比我们做强吗?
如果未来可以有足够付费用户的时候,我们也会考虑调低价格的。也请大家多理解多支持吧。
我们也在做国家、城市经纬度和相关时区的数据,有需要的可以试试。
年底之前也争取把域名换掉,也省得很多人问我 17MON 代表啥意思。:-)
再求专职维护人员,有兴趣可以找我聊。
--
高春辉
Paul Gao
gaochunhui@gmail.com
http://www.17mon.cn/
--- 2014 年 8 月引言 ---
这是 IP 库的第六个公开版本。
这个月参加了 ThinkInLAMP 2014 PHP 技术峰会,做了一次有关 IP 数据库的演讲,也算是对将近一年多时间的投入的一个总结。大家有兴趣的,可以看看我的 ppt。
实在是精力有限,为承诺计,从这个月开始,将每两个月发布免费版了。如果您需要更好的服务,请购买付费版。
--
高春辉
Paul Gao
gaochunhui@gmail.com
http://www.17mon.cn/
--- 2014 年 7 月引言 ---
这是 IP 库的第五个公开版本。
这个月公司和家里事情比较多,更新的慢了一点,还请大家谅解。
--
高春辉
Paul Gao
gaochunhui@gmail.com
http://www.17mon.cn/
--- 2014 年 6 月引言 ---
这是 IP 库的第四个公开版本。
祝大家六一、端午快乐,这次我们增加了一个字段,原来是把城市和单位放在了一起,这样比如在有学校名称的时候,就没法知道所在具体城市了。
这种情况被客户们投诉了。这次趁着三天假的机会,下决心将其分开,这回大家都满意了吧?:-)
因为增加了字段,所以对于字段有明确要求的,请仔细核查数据和相关代码,如果有问题,请到 QQ 群: 346280296 中反馈。
--
高春辉
Paul Gao
gaochunhui@gmail.com
http://www.17mon.cn/
--- 2014 年 5 月引言 ---
这是 IP 库的第三个公开版本。
祝大家五一快乐,假期之后的一周后,付费计划将上线。另外 WINDOWS 版的客户端也会同期上线。
已经有公司与我联系了购买了付费服务,我很欣慰,呵呵。
--
高春辉
Paul Gao
gaochunhui@gmail.com
http://www.17mon.cn/
--- 2014 年 4 月引言 ---
这是 IP 库的第二个公开版本。
这个版本放开了国内市一级的数据,和网站版的数据相比,只有运营商的数据和国外到市一级的数据没有放开,我想对于一般的应用来说足够用了。
这个月,更大的收获是,校准 IP 库的方法,进一步的成型,可以更少的工作、更高的正确率,对于做付费计划,我更有信心了。
付费计划应该在本月底之前上线。希望对于数据的数据和更新频度以及支持有要求的,请一定给予支持。不然这个事情没有办法长期执行下去。
如果您的软件或者应用里需要内置 IP 库,可以找我来谈更紧密的合作和更新方式。
如果您有任何问题,请到 QQ 群来说。
不多说,睡觉去了。。。
--
高春辉
Paul Gao
gaochunhui@gmail.com
http://www.17mon.cn/
--- 2014 年 3 月引言 ---
经过半年多的不断努力,终于可以发布一个公开的版本了。
IP 地址是互联网的基础部分,那么 IP 归属地数据库同样也很重要。
之前在 ECSHOP 时期就研究过,只是最终没有更深入。
终于半公半私都有关系的原因,下定决心做了,我一个人为主,也有同事很大的帮助。
这半年多时间以来,知识见涨,尤其是全球地理方面的知识。:-)
这次为了发布,也专门改进了库的生成方式,更准确(之前的生成代码有个小问题,导致 ECSHOP、DISCUZ 的也有同样问题,只是轻易碰不上,这次一并都改进了),库的体积也更小。
言归正传,有空再闲扯。
想使用很简单,把 17monipdb.dat 和 IP.class.php 放在同一个目录,使用 IP::find('x.x.x.x') 调用即可。
工程师们请注意:目前为了向后兼容增加更多项目,并且方便进行二次处理,所以两个文本使用制表符分割;目前使用 UTF-8 字符集,其它字符集请自行转义;欢迎大家补充其它语言的版本。
几点说明:
1、维护这个数据库是个长期工作甚至是一个无期工作所以有打算做付费维护的考虑请各位工程师们理解其辛苦程度。不是为赚钱而是为了更好的维护数据库让数据更准确更有价值。
2、目前公开版仅发布国内具体到省国外具体到国家的格式数据。一般来说够用了。基于准确度优先的问题我们暂时没有做除了直辖市或者可以明确到市的数据即使有目前也比较少。这个还请见谅您可以保持关注。数据库的更新周期暂定一个月左右。17mon.cn 所对外的数据是完全版也是最新版,有需要可以使用 http://ip.17mon.cn/ 进行查询。
3、有些 IP 段不会标注国家,是因为要么是路由器 IP 段,要么是做了 ANYCAST 技术,无法具体定位,一般来说,针对普通用户进行定位,不会碰到此类 IP只有面对 CDN、DNS、服务器、路由器等等所在的 IP才有可能碰到。
4、在整理过程中感谢 DNSPOD 的建议和数据支持,部分数据参考了纯真 IP 库、淘宝 IP 库、腾讯 IP 库、新浪 IP 库、中国互联网广告行业 IP 库的数据,还包括 BGP.HE.NET 以及全球各大地区的 IP 管理机构的 WHOIS 信息数据,感谢给我帮助和支持的大家还有我的同事,还有很多基础性的文章和资料,包括中国地图出版社的美国地图和欧洲地图。
5、为了便于查询您使用的 IP 库版本,将 255.255.255.0 - 255.255.255.255 作为版本数据输出,您需要了解版本的话,请使用 IP::find('255.255.255.255') 查询即可。
6、我们为了自己也为了他人方便集成了一个大全版有兴趣者可访问 http://ip.17mon.cn/ 。
7、数据量超大更不要提未来的 IPv6 了,尤其我们为了准确,尽量使用实证方式维护数据,错误难免,请加 QQ 群: 346280296 进行讨论。
8、如果您所在公司有更准确的 IP 数据库需求,可以与我联系,希望可以发挥各自长处合作共建,而不是各自单打独斗。
说完了。
--
高春辉
Paul Gao
gaochunhui@gmail.com
http://www.17mon.cn/
*/
?>

95
extend/mail/Mail.php Executable file
View File

@ -0,0 +1,95 @@
<?php
/**
* Created by PhpStorm
* User Julyssn
* Date 2021/8/3 13:50
*/
namespace mail;
use Swift_Mailer;
use Swift_Message;
use Swift_SmtpTransport;
use think\facade\View;
class Mail
{
public $Config = [
'driver' => 'smtp', // 邮件驱动, 支持 smtp|sendmail|mail 三种驱动
'host' => 'smtp.qq.com', // SMTP服务器地址
'port' => 465, // SMTP服务器端口号,一般为25
'addr' => '', // 发件邮箱地址
'pass' => '', // 发件邮箱密码
'sign' => '', // 发件邮箱名称
'content_type' => 'text/html', // 默认文本内容 text/html|text/plain
'charset' => 'utf-8', // 默认字符集
'security' => 'ssl', // 加密方式 null|ssl|tls, QQ邮箱必须使用ssl
'temp' => '', //邮件模板
'logo' => '', //邮件logo
];
public function __construct($config)
{
$this->Config = array_merge($this->Config, $config);
//默认模板
$this->Config['temp'] = $this->Config['temp'] ?: 'temp';
$this->Config['logo'] = $this->Config['logo'] ?: 'https://im.file.raingad.com/logo/logo.png';
}
public function sendEmail(array $toEmails, $title, $content)
{
// 创建Transport对象设置邮件服务器和端口号并设置用户名和密码以供验证
$transport = (new Swift_SmtpTransport($this->Config['host'], $this->Config['port'], $this->Config['security']))
->setUsername($this->Config['addr'])
->setPassword($this->Config['pass']);
//创建mailer对象
$mailer = new Swift_Mailer($transport);
//创建message对象
$message = (new Swift_Message($title));//设置邮件主题
//用关联数组设置发件人地址,可以设置多个发件人
$message->setFrom([$this->Config['addr'] => $this->Config['sign']]);
//用关联数组设置收件人地址,可以设置多个收件人
$message->setTo($toEmails);
//设置邮件内容
$data = [
'logo' => $this->Config['logo'],
'title' => $title,
'content' => $content,
'time' => date('Y-m-d H:i:s'),
'name' => $this->Config['sign']
];
$html = View::fetch(dirname(__FILE__) . '/' . $this->Config['temp'] . '.html', ['data' => $data]);
$message->setBody($html, 'text/html');
// //创建attachment对象content-type这个参数可以省略
// $attachment = Swift_Attachment::fromPath('image.jpg', 'image/jpeg')->setFilename('cool.jpg');
// //添加附件
// $message->attach($attachment);
// //添加抄送人
// $message->setCc(array(
// 'Cc@qq.com' => 'Cc'
// ));
// //添加密送人
// $message->setBcc(array(
// 'Bcc@qq.com' => 'Bcc'
// ));
// //设置邮件回执
// $message->setReadReceiptTo('receipt@163.com');
//发送邮件
return $mailer->send($message);
}
}

57
extend/mail/code.html Executable file
View File

@ -0,0 +1,57 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="zh-CN">
<head>
<base target="_blank" />
<style type="text/css">::-webkit-scrollbar{ display: none; }</style>
<style id="cloudAttachStyle" type="text/css">#divNeteaseBigAttach, #divNeteaseBigAttach_bak{display:none;}</style>
<style id="blockquoteStyle" type="text/css">blockquote{display:none;}</style>
<style type="text/css">
body{font-size:14px;font-family:arial,verdana,sans-serif;line-height:1.666;padding:0;margin:0;overflow:auto;white-space:normal;word-wrap:break-word;min-height:100px}
td, input, button, select, body{font-family:Helvetica, \'Microsoft Yahei\', verdana}
pre {white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word;width:95%}
th,td{font-family:arial,verdana,sans-serif;line-height:1.666}
img{ border:0}
header,footer,section,aside,article,nav,hgroup,figure,figcaption{display:block}
blockquote{margin-right:0px}
</style>
</head>
<body tabindex="0" role="listitem">
<table width="700" border="0" align="center" cellspacing="0" style="width:700px;">
<tbody>
<tr>
<td>
<div style="width:700px;margin:0 auto;border-bottom:1px solid #ccc;margin-bottom:30px;">
<table border="0" cellpadding="0" cellspacing="0" width="700" height="39" style="font:12px Tahoma, Arial, 宋体;">
<tbody><tr><td width="210"></td></tr></tbody>
</table>
</div>
<div style="width:680px;padding:0 10px;margin:0 auto;">
<div style="line-height:1.5;font-size:14px;margin-bottom:25px;color:#4d4d4d;">
<strong style="display:block;margin-bottom:15px;">尊敬的用户:<span style="color:#f60;font-size: 16px;"></span>您好!</strong>
<strong style="display:block;margin-bottom:15px;">
您正在进行<span style="color: red">{$data.title}</span>操作,请在验证码输入框中输入:<span style="color:#f60;font-size: 24px">{$data.content}</span>,以完成操作。
</strong>
</div>
<div style="margin-bottom:30px;">
<small style="display:block;margin-bottom:20px;font-size:12px;">
<p style="color:#747474;">
注意:此操作可能会修改您的密码、登录邮箱或绑定手机。如非本人操作,请及时登录并修改密码以保证帐户安全
<br>(工作人员不会向你索取此验证码,请勿泄漏!)
</p>
</small>
</div>
</div>
<div style="width:700px;margin:0 auto;">
<div style="padding:10px 10px 0;border-top:1px solid #ccc;color:#747474;margin-bottom:20px;line-height:1.3em;font-size:12px;">
<p>此为系统邮件,请勿回复<br>
请保管好您的邮箱,避免账号被他人盗用
</p>
<p>{$data.sign ?? ''}</p>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</body>
</html>

49
extend/mail/temp.html Executable file
View File

@ -0,0 +1,49 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="zh-CN">
<head>
<base target="_blank" />
<style type="text/css">::-webkit-scrollbar{ display: none; }</style>
<style id="cloudAttachStyle" type="text/css">#divNeteaseBigAttach, #divNeteaseBigAttach_bak{display:none;}</style>
<style id="blockquoteStyle" type="text/css">blockquote{display:none;}</style>
<style type="text/css">
body{font-size:14px;font-family:arial,verdana,sans-serif;line-height:1.666;padding:0;margin:0;overflow:auto;white-space:normal;word-wrap:break-word;min-height:100px}
td, input, button, select, body{font-family:Helvetica, \'Microsoft Yahei\', verdana}
pre {white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word;width:95%}
th,td{font-family:arial,verdana,sans-serif;line-height:1.666}
img{ border:0}
header,footer,section,aside,article,nav,hgroup,figure,figcaption{display:block}
blockquote{margin-right:0px}
</style>
</head>
<body tabindex="0" role="listitem">
<table width="700" border="0" align="center" cellspacing="0" style="width:700px;">
<tbody>
<tr>
<td>
<div style="width:700px;margin:0 auto;border-bottom:1px solid #ccc;margin-bottom:30px;">
<table border="0" cellpadding="0" cellspacing="0" width="700" height="39" style="font:12px Tahoma, Arial, 宋体;">
<tbody><tr><td width="210"></td></tr></tbody>
</table>
</div>
<div style="width:680px;padding:0 10px;margin:0 auto;">
<div style="line-height:1.5;font-size:14px;margin-bottom:25px;color:#4d4d4d;">
<strong style="display:block;margin-bottom:15px;">尊敬的用户:您好!</strong>
<strong style="display:block;margin-bottom:15px;">
{$data.content}
</strong>
</div>
</div>
<div style="width:700px;margin:0 auto;">
<div style="padding:10px 10px 0;border-top:1px solid #ccc;color:#747474;margin-bottom:20px;line-height:1.3em;font-size:12px;">
<p>此为系统邮件,请勿回复<br>
请保管好您的邮箱,避免账号被他人盗用
</p>
<p>Raingad-IM</p>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</body>
</html>

74
extend/task/command/Task.php Executable file
View File

@ -0,0 +1,74 @@
<?php
/**
* Created by PhpStorm
* User Julyssn
* Date 2022/12/14 16:12
*/
namespace task\command;
use easyTask\Helper;
use easyTask\Task as EasyTask;
use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\Output;
use think\helper\Str;
class Task extends Command
{
protected function configure()
{
// 指令配置
$this->setName('task')
->addArgument('action', Argument::OPTIONAL, "action", '')
->addArgument('force', Argument::OPTIONAL, "force", '')
->setDescription('the task command');
}
protected function execute(Input $input, Output $output)
{
//获取输入参数
$action = trim($input->getArgument('action'));
$force = trim($input->getArgument('force'));
$rootPath = root_path();
// 配置任务每隔20秒访问2次网站
$task = new EasyTask();
// 设置常驻内存
$task->setDaemon(!Helper::isWin());
// 设置项目名称 获取运行目录文件夹名称
$task->setPrefix('easy_task');
// 设置子进程挂掉自动重启
$task->setAutoRecover(true);
// 设置运行时目录(日志或缓存目录)
$task->setRunTimePath($rootPath . 'runtime');
// 消息推送
$task->addCommand('php think worker:gateway start', 'worker', 0);
// 定时任务
$task->addCommand('php think cron:schedule', 'schedule', 0);
// 律者队列
$task->addCommand('php think queue:listen --sleep 0.3 --queue lvzhe', 'queue', 0);
// 根据命令执行
if ($action == 'start') {
$task->start();
} elseif ($action == 'status') {
$task->status();
} elseif ($action == 'stop') {
$force = ($force == 'force'); //是否强制停止
$task->stop($force);
} else {
exit('Command is not exist');
}
}
}

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